Skip to content

Commit ee1eb04

Browse files
Build: expose READTHEDOCS_VIRTUALENV_PATH variable (#9971)
* Build: expose `READTHEDOCS_VIRTUALENV_PATH` variable This variable points to where Read the Docs created the virtualenv. Closes #9629 * Build: use our own variable `$READTHEDOCS_VIRTUALENV_PATH` This variable contains the path we need to call the Python executable. * Build: use environment variables for Conda as well * Tests: add missing variable * Lint * Lint again * Missing : * Test: update env variables * Test: make it work locally and CicleCI * Docs: add link to the builds page * Refactor: make `venv_bin` more concrete to match venv/conda * Build: missing return * API: sanitize build command properly * Update readthedocs/api/v2/serializers.py Co-authored-by: Eric Holscher <[email protected]> --------- Co-authored-by: Eric Holscher <[email protected]>
1 parent 58e38e0 commit ee1eb04

File tree

7 files changed

+59
-18
lines changed

7 files changed

+59
-18
lines changed

docs/user/environment-variables.rst

+7
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,13 @@ Read the Docs builders set the following environment variables automatically for
5050

5151
:Examples: ``en``, ``it``, ``de_AT``, ``es``, ``pt_BR``
5252

53+
.. envvar:: READTHEDOCS_VIRTUALENV_PATH
54+
55+
Path for the :ref:`virtualenv that was created for this build <builds:Understanding what's going on>`.
56+
Only exists for builds using Virtualenv and not Conda.
57+
58+
:Example: ``/home/docs/checkouts/readthedocs.org/user_builds/project/envs/version``
59+
5360
User-defined environment variables
5461
----------------------------------
5562

readthedocs/api/v2/serializers.py

+10-1
Original file line numberDiff line numberDiff line change
@@ -170,8 +170,17 @@ def get_command(self, obj):
170170
docroot = re.sub("/[0-9a-z]+/?$", "", settings.DOCROOT, count=1)
171171
container_hash = "/([0-9a-z]+/)?"
172172

173+
command = obj.command
173174
regex = f"{docroot}{container_hash}{project_slug}/envs/{version_slug}(/bin/)?"
174-
command = re.sub(regex, "", obj.command, count=1)
175+
command = re.sub(regex, "", command, count=1)
176+
177+
# Remove explicit variable names we use to run commands,
178+
# since users don't care about these.
179+
regex = r"^\$READTHEDOCS_VIRTUALENV_PATH/bin/"
180+
command = re.sub(regex, "", command, count=1)
181+
182+
regex = r"^\$CONDA_ENVS_PATH/\\$CONDA_DEFAULT_ENV/bin/"
183+
command = re.sub(regex, "", command, count=1)
175184
return command
176185

177186

readthedocs/doc_builder/director.py

+4
Original file line numberDiff line numberDiff line change
@@ -556,6 +556,7 @@ def get_build_env_vars(self):
556556
if self.data.config.conda is not None:
557557
env.update(
558558
{
559+
# NOTE: should these be prefixed with "READTHEDOCS_"?
559560
"CONDA_ENVS_PATH": os.path.join(
560561
self.data.project.doc_path, "conda"
561562
),
@@ -577,6 +578,9 @@ def get_build_env_vars(self):
577578
self.data.version.slug,
578579
"bin",
579580
),
581+
"READTHEDOCS_VIRTUALENV_PATH": os.path.join(
582+
self.data.project.doc_path, "envs", self.data.version.slug
583+
),
580584
}
581585
)
582586

readthedocs/doc_builder/environments.py

+12-1
Original file line numberDiff line numberDiff line change
@@ -380,7 +380,18 @@ def get_wrapped_command(self):
380380

381381
def _escape_command(self, cmd):
382382
r"""Escape the command by prefixing suspicious chars with `\`."""
383-
return self.bash_escape_re.sub(r'\\\1', cmd)
383+
command = self.bash_escape_re.sub(r"\\\1", cmd)
384+
385+
# HACK: avoid escaping variables that we need to use in the commands
386+
not_escape_variables = (
387+
"READTHEDOCS_OUTPUT",
388+
"READTHEDOCS_VIRTUALENV_PATH",
389+
"CONDA_ENVS_PATH",
390+
"CONDA_DEFAULT_ENV",
391+
)
392+
for variable in not_escape_variables:
393+
command = command.replace(f"\\${variable}", f"${variable}")
394+
return command
384395

385396

386397
class BaseEnvironment:

readthedocs/doc_builder/python_environments.py

+22-15
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,12 @@ def install_package(self, install):
5353
:param install: A install object from the config module.
5454
:type install: readthedocs.config.models.PythonInstall
5555
"""
56+
# NOTE: `venv_bin` requires `prefixes`.
57+
# However, it's overwritten in the subclasses and
58+
# it forces passing the `prefixes=` attribute.
59+
# I'm not sure how to solve this, so I'm skipping this check for now.
60+
# pylint: disable=no-value-for-parameter
61+
5662
if install.method == PIP:
5763
# Prefix ./ so pip installs from a local path rather than pypi
5864
local_path = (
@@ -89,17 +95,17 @@ def install_package(self, install):
8995
bin_path=self.venv_bin(),
9096
)
9197

92-
def venv_bin(self, filename=None):
98+
def venv_bin(self, prefixes, filename=None):
9399
"""
94100
Return path to the virtualenv bin path, or a specific binary.
95101
96102
:param filename: If specified, add this filename to the path return
103+
:param prefixes: List of path prefixes to include in the resulting path
97104
:returns: Path to virtualenv bin or filename in virtualenv bin
98105
"""
99-
parts = [self.venv_path(), 'bin']
100106
if filename is not None:
101-
parts.append(filename)
102-
return os.path.join(*parts)
107+
prefixes.append(filename)
108+
return os.path.join(*prefixes)
103109

104110

105111
class Virtualenv(PythonEnvironment):
@@ -110,8 +116,10 @@ class Virtualenv(PythonEnvironment):
110116
.. _virtualenv: https://virtualenv.pypa.io/
111117
"""
112118

113-
def venv_path(self):
114-
return os.path.join(self.project.doc_path, 'envs', self.version.slug)
119+
# pylint: disable=arguments-differ
120+
def venv_bin(self, filename=None):
121+
prefixes = ["$READTHEDOCS_VIRTUALENV_PATH", "bin"]
122+
return super().venv_bin(prefixes, filename=filename)
115123

116124
def setup_base(self):
117125
"""
@@ -133,9 +141,7 @@ def setup_base(self):
133141
cli_args.append('--system-site-packages')
134142

135143
# Append the positional destination argument
136-
cli_args.append(
137-
self.venv_path(),
138-
)
144+
cli_args.append("$READTHEDOCS_VIRTUALENV_PATH")
139145

140146
self.build_env.run(
141147
self.config.python_interpreter,
@@ -167,7 +173,9 @@ def install_core_requirements(self):
167173
)
168174
cmd = pip_install_cmd + [pip_version, 'setuptools<58.3.0']
169175
self.build_env.run(
170-
*cmd, bin_path=self.venv_bin(), cwd=self.checkout_path
176+
*cmd,
177+
bin_path=self.venv_bin(),
178+
cwd=self.checkout_path,
171179
)
172180

173181
requirements = []
@@ -318,8 +326,10 @@ class Conda(PythonEnvironment):
318326
.. _Conda: https://conda.io/docs/
319327
"""
320328

321-
def venv_path(self):
322-
return os.path.join(self.project.doc_path, 'conda', self.version.slug)
329+
# pylint: disable=arguments-differ
330+
def venv_bin(self, filename=None):
331+
prefixes = ["$CONDA_ENVS_PATH", "$CONDA_DEFAULT_ENV", "bin"]
332+
return super().venv_bin(prefixes, filename=filename)
323333

324334
def conda_bin_name(self):
325335
"""
@@ -354,9 +364,6 @@ def _update_conda_startup(self):
354364
)
355365

356366
def setup_base(self):
357-
conda_env_path = os.path.join(self.project.doc_path, 'conda')
358-
os.path.join(conda_env_path, self.version.slug)
359-
360367
if self.project.has_feature(Feature.UPDATE_CONDA_STARTUP):
361368
self._update_conda_startup()
362369

readthedocs/projects/tests/test_build_tasks.py

+3
Original file line numberDiff line numberDiff line change
@@ -294,6 +294,9 @@ def test_get_env_vars(self, load_yaml_config, build_environment, config, externa
294294
"bin",
295295
),
296296
PUBLIC_TOKEN="a1b2c3",
297+
# Local and Circle are different values.
298+
# We only check it's present, but not its value.
299+
READTHEDOCS_VIRTUALENV_PATH=mock.ANY,
297300
)
298301
if not external:
299302
expected_build_env_vars["PRIVATE_TOKEN"] = "a1b2c3"

readthedocs/rtd_tests/tests/test_api.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -334,7 +334,7 @@ def test_response_finished_and_success(self):
334334
buildcommandresult = get(
335335
BuildCommandResult,
336336
build=build,
337-
command="/home/docs/checkouts/readthedocs.org/user_builds/myproject/envs/myversion/bin/python -m pip install --upgrade --no-cache-dir pip setuptools<58.3.0",
337+
command="python -m pip install --upgrade --no-cache-dir pip setuptools<58.3.0",
338338
exit_code=0,
339339
)
340340
resp = client.get('/api/v2/build/{build}/'.format(build=build.pk))

0 commit comments

Comments
 (0)