Skip to content

Commit 2cde412

Browse files
Build: use environment variable $READTHEDOCS_OUTPUT to define output directory (#9913)
* Build: use environment variable to define output directory `READTHEDOCS_OUTPUT` points to the path where the user's repository was clonned plus `_readthedocs/` subdirectory. This way we can make the command nicer: ``` python -m sphinx -T -E -j auto -b singlehtml -d _build/doctrees -D language=en . $READTHEDOCS_OUTPUT/html ``` Also, we gives our users a clear contract to where to find those files, even if we change the path of the underlying variable in the future. * Log environment during build commands * Love the environment in the container build as well * Build: do not escape `$READTHEDOCS_OUTPUT` variable Add a simple hack to avoid escaping this internal variable since we need it to determine the output path. * Build: use `$READTHEDOCS_OUTPUT` variable for MkDocs builder * Log: do not log the environment It could contain some private data. * Build: organize absolute output directory for host and container There are some commands that are executed from inside the container where `$READTHEDOCS_OUTPUT` variable is defined and we can use it. However, there are other commands executed from the host where that variable is not defined and/or it cannot be used (e.g. as a `cwd=` argument for Docker run command). * Test: update tests to use the new variable * Build: rename variables to make it more clear Use `absolute_container_output_dir` and `absolute_host_output_dir` to differentiate them in a clear way. * Lint --------- Co-authored-by: Eric Holscher <[email protected]>
1 parent 09bdbb8 commit 2cde412

File tree

5 files changed

+109
-64
lines changed

5 files changed

+109
-64
lines changed

readthedocs/doc_builder/backends/mkdocs.py

+1-10
Original file line numberDiff line numberDiff line change
@@ -296,17 +296,8 @@ def build(self):
296296
'mkdocs',
297297
self.builder,
298298
"--clean",
299-
# ``site_dir`` is relative to where the mkdocs.yaml file is
300-
# https://www.mkdocs.org/user-guide/configuration/#site_dir
301-
# Example:
302-
#
303-
# when ``--config-file=docs/mkdocs.yml``,
304-
# it must be ``--site-dir=../_readthedocs/html``
305299
"--site-dir",
306-
os.path.join(
307-
os.path.relpath(self.project_path, os.path.dirname(self.yaml_file)),
308-
self.build_dir,
309-
),
300+
os.path.join("$READTHEDOCS_OUTPUT", "html"),
310301
"--config-file",
311302
os.path.relpath(self.yaml_file, self.project_path),
312303
]

readthedocs/doc_builder/backends/sphinx.py

+78-39
Original file line numberDiff line numberDiff line change
@@ -42,16 +42,53 @@ class BaseSphinx(BaseBuilder):
4242
# an output file, the parsed source files are cached as "doctree pickles".
4343
sphinx_doctrees_dir = "_build/doctrees"
4444

45-
# Output directory relative to where the repository was cloned
46-
# (e.g. "_readthedocs/<format>")
45+
# Output directory relative to $READTHEDOCS_OUTPUT
46+
# (e.g. "html", "htmlzip" or "pdf")
4747
relative_output_dir = None
4848

4949
def __init__(self, *args, **kwargs):
5050
super().__init__(*args, **kwargs)
5151
self.config_file = self.config.sphinx.configuration
52-
self.absolute_output_dir = os.path.abspath(
53-
os.path.join(self.project_path, self.relative_output_dir)
52+
53+
# We cannot use `$READTHEDOCS_OUTPUT` environment variable for
54+
# `absolute_host_output_dir` because it's not defined in the host. So,
55+
# we have to re-calculate its value. We will remove this limitation
56+
# when we execute the whole building from inside the Docker container
57+
# (instead behing a hybrid as it is now)
58+
#
59+
# We need to have two different paths that point to the exact same
60+
# directory. How is that? The directory is mounted into a different
61+
# location inside the container:
62+
#
63+
# 1. path in the host:
64+
# /home/docs/checkouts/readthedocs.org/user_builds/<project>/
65+
# 2. path in the container:
66+
# /usr/src/app/checkouts/readthedocs.org/user_builds/b9cbc24c8841/test-builds/
67+
#
68+
# Besides, the variable `$READTHEDOCS_OUTPUT` is not defined in the
69+
# host, so we have to expand it using the full host's path. This
70+
# variable cannot be used in cwd= due to a limitation of the Docker API
71+
# (I guess) since I received an error when trying that. So, we have to
72+
# fully expand it.
73+
#
74+
# That said, we need:
75+
#
76+
# * use the path in the host, for all the operations that are done via
77+
# Python from the app (e.g. os.path.join, glob.glob, etc)
78+
#
79+
# * use the path in the container, for all the operations that are
80+
# executed inside the container via Docker API using shell commands
81+
self.absolute_host_output_dir = os.path.join(
82+
os.path.join(
83+
self.project.checkout_path(self.version.slug),
84+
"_readthedocs/",
85+
),
86+
self.relative_output_dir,
87+
)
88+
self.absolute_container_output_dir = os.path.join(
89+
"$READTHEDOCS_OUTPUT", self.relative_output_dir
5490
)
91+
5592
try:
5693
if not self.config_file:
5794
self.config_file = self.project.conf_file(self.version.slug)
@@ -309,9 +346,7 @@ def build(self):
309346
# https://github.com/readthedocs/readthedocs.org/pull/9888#issuecomment-1384649346
310347
".",
311348
# Sphinx's output build directory (OUTPUTDIR)
312-
os.path.relpath(
313-
self.absolute_output_dir, os.path.dirname(self.config_file)
314-
),
349+
self.absolute_container_output_dir,
315350
]
316351
)
317352
cmd_ret = self.run(
@@ -338,7 +373,7 @@ def sphinx_parallel_arg(self):
338373

339374

340375
class HtmlBuilder(BaseSphinx):
341-
relative_output_dir = "_readthedocs/html"
376+
relative_output_dir = "html"
342377

343378
def __init__(self, *args, **kwargs):
344379
super().__init__(*args, **kwargs)
@@ -367,18 +402,16 @@ def __init__(self, *args, **kwargs):
367402

368403
class LocalMediaBuilder(BaseSphinx):
369404
sphinx_builder = 'readthedocssinglehtmllocalmedia'
370-
relative_output_dir = "_readthedocs/htmlzip"
405+
relative_output_dir = "htmlzip"
371406

372407
def _post_build(self):
373408
"""Internal post build to create the ZIP file from the HTML output."""
374-
target_file = os.path.abspath(
375-
os.path.join(
376-
self.absolute_output_dir,
377-
# TODO: shouldn't this name include the name of the version as well?
378-
# It seems we were using the project's slug previously.
379-
# So, keeping it like that for now until we decide make that adjustment.
380-
f"{self.project.slug}.zip",
381-
)
409+
target_file = os.path.join(
410+
self.absolute_container_output_dir,
411+
# TODO: shouldn't this name include the name of the version as well?
412+
# It seems we were using the project's slug previously.
413+
# So, keeping it like that for now until we decide make that adjustment.
414+
f"{self.project.slug}.zip",
382415
)
383416

384417
# **SECURITY CRITICAL: Advisory GHSA-hqwg-gjqw-h5wg**
@@ -390,15 +423,15 @@ def _post_build(self):
390423
dirname = f"{self.project.slug}-{self.version.slug}"
391424
self.run(
392425
"mv",
393-
self.relative_output_dir,
426+
self.absolute_container_output_dir,
394427
str(tmp_dir / dirname),
395428
cwd=self.project_path,
396429
record=False,
397430
)
398431
self.run(
399432
"mkdir",
400433
"--parents",
401-
self.relative_output_dir,
434+
self.absolute_container_output_dir,
402435
cwd=self.project_path,
403436
record=False,
404437
)
@@ -416,17 +449,19 @@ def _post_build(self):
416449
class EpubBuilder(BaseSphinx):
417450

418451
sphinx_builder = "epub"
419-
relative_output_dir = "_readthedocs/epub"
452+
relative_output_dir = "epub"
420453

421454
def _post_build(self):
422455
"""Internal post build to cleanup EPUB output directory and leave only one .epub file."""
423456
temp_epub_file = f"/tmp/{self.project.slug}-{self.version.slug}.epub"
424457
target_file = os.path.join(
425-
self.absolute_output_dir,
458+
self.absolute_container_output_dir,
426459
f"{self.project.slug}.epub",
427460
)
428461

429-
epub_sphinx_filepaths = glob(os.path.join(self.absolute_output_dir, "*.epub"))
462+
epub_sphinx_filepaths = glob(
463+
os.path.join(self.absolute_host_output_dir, "*.epub")
464+
)
430465
if epub_sphinx_filepaths:
431466
# NOTE: we currently support only one .epub per version
432467
epub_filepath = epub_sphinx_filepaths[0]
@@ -437,14 +472,14 @@ def _post_build(self):
437472
self.run(
438473
"rm",
439474
"--recursive",
440-
self.relative_output_dir,
475+
self.absolute_container_output_dir,
441476
cwd=self.project_path,
442477
record=False,
443478
)
444479
self.run(
445480
"mkdir",
446481
"--parents",
447-
self.relative_output_dir,
482+
self.absolute_container_output_dir,
448483
cwd=self.project_path,
449484
record=False,
450485
)
@@ -481,7 +516,7 @@ class PdfBuilder(BaseSphinx):
481516

482517
"""Builder to generate PDF documentation."""
483518

484-
relative_output_dir = "_readthedocs/pdf"
519+
relative_output_dir = "pdf"
485520
sphinx_builder = "latex"
486521
pdf_file_name = None
487522

@@ -504,14 +539,12 @@ def build(self):
504539
# https://github.com/readthedocs/readthedocs.org/pull/9888#issuecomment-1384649346
505540
".",
506541
# Sphinx's output build directory (OUTPUTDIR)
507-
os.path.relpath(
508-
self.absolute_output_dir, os.path.dirname(self.config_file)
509-
),
542+
self.absolute_container_output_dir,
510543
cwd=os.path.dirname(self.config_file),
511544
bin_path=self.python_env.venv_bin(),
512545
)
513546

514-
tex_files = glob(os.path.join(self.absolute_output_dir, "*.tex"))
547+
tex_files = glob(os.path.join(self.absolute_host_output_dir, "*.tex"))
515548
if not tex_files:
516549
raise BuildUserError("No TeX files were found.")
517550

@@ -526,7 +559,7 @@ def _build_latexmk(self, cwd):
526559
# https://github.com/sphinx-doc/sphinx/blob/master/sphinx/texinputs/Makefile_t
527560
images = []
528561
for extension in ("png", "gif", "jpg", "jpeg"):
529-
images.extend(Path(self.absolute_output_dir).glob(f"*.{extension}"))
562+
images.extend(Path(self.absolute_host_output_dir).glob(f"*.{extension}"))
530563

531564
# FIXME: instead of checking by language here, what we want to check if
532565
# ``latex_engine`` is ``platex``
@@ -536,13 +569,13 @@ def _build_latexmk(self, cwd):
536569
# step. I don't know exactly why but most of the documentation that
537570
# I read differentiate this language from the others. I suppose
538571
# it's because it mix kanji (Chinese) with its own symbols.
539-
pdfs = Path(self.absolute_output_dir).glob("*.pdf")
572+
pdfs = Path(self.absolute_host_output_dir).glob("*.pdf")
540573

541574
for image in itertools.chain(images, pdfs):
542575
self.run(
543576
'extractbb',
544577
image.name,
545-
cwd=self.absolute_output_dir,
578+
cwd=self.absolute_host_output_dir,
546579
record=False,
547580
)
548581

@@ -553,7 +586,7 @@ def _build_latexmk(self, cwd):
553586
self.run(
554587
'cat',
555588
rcfile,
556-
cwd=self.absolute_output_dir,
589+
cwd=self.absolute_host_output_dir,
557590
)
558591

559592
if self.build_env.command_class == DockerBuildCommand:
@@ -581,7 +614,7 @@ def _build_latexmk(self, cwd):
581614
cls=latex_class,
582615
cmd=cmd,
583616
warn_only=True,
584-
cwd=self.absolute_output_dir,
617+
cwd=self.absolute_host_output_dir,
585618
)
586619

587620
self.pdf_file_name = f'{self.project.slug}.pdf'
@@ -597,13 +630,19 @@ def _post_build(self):
597630
# TODO: merge this with ePUB since it's pretty much the same
598631
temp_pdf_file = f"/tmp/{self.project.slug}-{self.version.slug}.pdf"
599632
target_file = os.path.join(
600-
self.absolute_output_dir,
633+
self.absolute_container_output_dir,
601634
self.pdf_file_name,
602635
)
603636

604637
# NOTE: we currently support only one .pdf per version
605-
pdf_sphinx_filepath = os.path.join(self.absolute_output_dir, self.pdf_file_name)
606-
if os.path.exists(pdf_sphinx_filepath):
638+
pdf_sphinx_filepath = os.path.join(
639+
self.absolute_container_output_dir, self.pdf_file_name
640+
)
641+
pdf_sphinx_filepath_host = os.path.join(
642+
self.absolute_host_output_dir,
643+
self.pdf_file_name,
644+
)
645+
if os.path.exists(pdf_sphinx_filepath_host):
607646
self.run(
608647
"mv",
609648
pdf_sphinx_filepath,
@@ -614,14 +653,14 @@ def _post_build(self):
614653
self.run(
615654
"rm",
616655
"-r",
617-
self.relative_output_dir,
656+
self.absolute_container_output_dir,
618657
cwd=self.project_path,
619658
record=False,
620659
)
621660
self.run(
622661
"mkdir",
623662
"-p",
624-
self.relative_output_dir,
663+
self.absolute_container_output_dir,
625664
cwd=self.project_path,
626665
record=False,
627666
)

readthedocs/doc_builder/director.py

+3
Original file line numberDiff line numberDiff line change
@@ -543,6 +543,9 @@ def get_rtd_env_vars(self):
543543
"READTHEDOCS_VERSION_NAME": self.data.version.verbose_name,
544544
"READTHEDOCS_PROJECT": self.data.project.slug,
545545
"READTHEDOCS_LANGUAGE": self.data.project.language,
546+
"READTHEDOCS_OUTPUT": os.path.join(
547+
self.data.project.checkout_path(self.data.version.slug), "_readthedocs/"
548+
),
546549
}
547550
return env
548551

readthedocs/doc_builder/environments.py

+7-2
Original file line numberDiff line numberDiff line change
@@ -130,8 +130,6 @@ def __str__(self):
130130
# commands, which is not supported anymore
131131
def run(self):
132132
"""Set up subprocess and execute command."""
133-
log.info("Running build command.", command=self.get_command(), cwd=self.cwd)
134-
135133
self.start_time = datetime.utcnow()
136134
environment = self._environment.copy()
137135
if 'DJANGO_SETTINGS_MODULE' in environment:
@@ -145,6 +143,13 @@ def run(self):
145143
env_paths.insert(0, self.bin_path)
146144
environment['PATH'] = ':'.join(env_paths)
147145

146+
log.info(
147+
"Running build command.",
148+
command=self.get_command(),
149+
cwd=self.cwd,
150+
environment=environment,
151+
)
152+
148153
try:
149154
# When using ``shell=True`` the command should be flatten
150155
command = self.command

0 commit comments

Comments
 (0)