Skip to content

Commit 74063f7

Browse files
committed
Build: implment build.jobs config file key
Allow people to use `build.jobs` to execute pre/post steps. ```yaml build: jobs: pre_install: - echo `date` - python path/to/myscript.py pre_build: - sed -i **/*.rst -e "s|{version}|v3.5.1|g" ```
1 parent 2e3f208 commit 74063f7

File tree

3 files changed

+84
-29
lines changed

3 files changed

+84
-29
lines changed

readthedocs/config/config.py

+35-1
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
from .find import find_one
1616
from .models import (
1717
Build,
18+
BuildJobs,
1819
BuildTool,
1920
BuildWithTools,
2021
Conda,
@@ -751,6 +752,10 @@ def validate_conda(self):
751752
conda['environment'] = validate_path(environment, self.base_path)
752753
return conda
753754

755+
# NOTE: I think we should rename `BuildWithTools` to `BuildWithOs` since
756+
# `os` is the main and mandatory key that makes the diference
757+
#
758+
# NOTE: `build.jobs` can't be used without using `build.os`
754759
def validate_build_config_with_tools(self):
755760
"""
756761
Validates the build object (new format).
@@ -769,6 +774,22 @@ def validate_build_config_with_tools(self):
769774
for tool in tools.keys():
770775
validate_choice(tool, self.settings['tools'].keys())
771776

777+
jobs = {}
778+
with self.catch_validation_error("build.jobs"):
779+
# FIXME: should we use `default={}` or kept the `None` here and
780+
# shortcircuit the rest of the logic?
781+
jobs = self.pop_config("build.jobs", default={})
782+
validate_dict(jobs)
783+
# NOTE: besides validating that each key is one of the expected
784+
# ones, we could validate the value of each of them is a list of
785+
# commands. However, I don't think we should validate the "command"
786+
# looks like a real command.
787+
for job in jobs.keys():
788+
validate_choice(
789+
job,
790+
BuildJobs.__slots__,
791+
)
792+
772793
if not tools:
773794
self.error(
774795
key='build.tools',
@@ -780,6 +801,16 @@ def validate_build_config_with_tools(self):
780801
code=CONFIG_REQUIRED,
781802
)
782803

804+
build["jobs"] = {}
805+
for job in BuildJobs.__slots__:
806+
build["jobs"][job] = []
807+
808+
for job, commands in jobs.items():
809+
with self.catch_validation_error(f"build.jobs.{job}"):
810+
build["jobs"][job] = [
811+
validate_string(command) for command in validate_list(commands)
812+
]
813+
783814
build['tools'] = {}
784815
for tool, version in tools.items():
785816
with self.catch_validation_error(f'build.tools.{tool}'):
@@ -1263,7 +1294,10 @@ def build(self):
12631294
return BuildWithTools(
12641295
os=build['os'],
12651296
tools=tools,
1266-
apt_packages=build['apt_packages'],
1297+
jobs=BuildJobs(
1298+
**{job: commands for job, commands in build["jobs"].items()}
1299+
),
1300+
apt_packages=build["apt_packages"],
12671301
)
12681302
return Build(**build)
12691303

readthedocs/config/models.py

+17-1
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ def __init__(self, **kwargs):
3737

3838
class BuildWithTools(Base):
3939

40-
__slots__ = ('os', 'tools', 'apt_packages')
40+
__slots__ = ("os", "tools", "jobs", "apt_packages")
4141

4242
def __init__(self, **kwargs):
4343
kwargs.setdefault('apt_packages', [])
@@ -49,6 +49,22 @@ class BuildTool(Base):
4949
__slots__ = ('version', 'full_version')
5050

5151

52+
class BuildJobs(Base):
53+
54+
__slots__ = (
55+
"pre_checkout",
56+
"post_checkout",
57+
"pre_system_dependencies",
58+
"post_system_dependencies",
59+
"pre_create_environment",
60+
"post_create_environment",
61+
"pre_install",
62+
"post_install",
63+
"pre_build",
64+
"post_build",
65+
)
66+
67+
5268
class Python(Base):
5369

5470
__slots__ = ('version', 'install', 'use_system_site_packages')

readthedocs/doc_builder/director.py

+32-27
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import os
2+
import shlex
23
from collections import defaultdict
34

45
import structlog
@@ -65,7 +66,7 @@ def setup_vcs(self):
6566
),
6667
)
6768

68-
environment = self.data.environment_class(
69+
self.vcs_environment = self.data.environment_class(
6970
project=self.data.project,
7071
version=self.data.version,
7172
build=self.data.build,
@@ -74,17 +75,17 @@ def setup_vcs(self):
7475
# ca-certificate package which is compatible with Lets Encrypt
7576
container_image=settings.RTD_DOCKER_BUILD_SETTINGS["os"]["ubuntu-20.04"],
7677
)
77-
with environment:
78+
with self.vcs_environment:
7879
before_vcs.send(
7980
sender=self.data.version,
80-
environment=environment,
81+
environment=self.vcs_environment,
8182
)
8283

8384
# Create the VCS repository where all the commands are going to be
8485
# executed for a particular VCS type
8586
self.vcs_repository = self.data.project.vcs_repo(
8687
version=self.data.version.slug,
87-
environment=environment,
88+
environment=self.vcs_environment,
8889
verbose_name=self.data.version.verbose_name,
8990
version_type=self.data.version.type,
9091
)
@@ -208,15 +209,17 @@ def checkout(self):
208209
self.vcs_repository.update_submodules(self.data.config)
209210

210211
def post_checkout(self):
211-
commands = [] # self.data.config.build.jobs.post_checkout
212+
commands = self.data.config.build.jobs.post_checkout
212213
for command in commands:
213-
self.build_environment.run(command)
214+
# TODO: we could make a helper `self.run(environment, command)`
215+
# that handles split and escape command
216+
self.vcs_environment.run(*shlex.split(command), escape_command=False)
214217

215218
# System dependencies (``build.apt_packages``)
216219
def pre_system_dependencies(self):
217-
commands = [] # self.data.config.build.jobs.pre_system_dependencies
220+
commands = self.data.config.build.jobs.pre_system_dependencies
218221
for command in commands:
219-
self.build_environment.run(command)
222+
self.build_environment.run(*shlex.split(command), escape_command=False)
220223

221224
# NOTE: `system_dependencies` should not be possible to override by the
222225
# user because it's executed as ``RTD_DOCKER_USER`` (e.g. ``root``) user.
@@ -254,58 +257,60 @@ def system_dependencies(self):
254257
)
255258

256259
def post_system_dependencies(self):
257-
pass
260+
commands = self.data.config.build.jobs.post_system_dependencies
261+
for command in commands:
262+
self.build_environment.run(*shlex.split(command), escape_command=False)
258263

259264
# Language environment
260265
def pre_create_environment(self):
261-
commands = [] # self.data.config.build.jobs.pre_create_environment
266+
commands = self.data.config.build.jobs.pre_create_environment
262267
for command in commands:
263-
self.build_environment.run(command)
268+
self.build_environment.run(*shlex.split(command), escape_command=False)
264269

265270
def create_environment(self):
266271
commands = [] # self.data.config.build.jobs.create_environment
267272
for command in commands:
268-
self.build_environment.run(command)
273+
self.build_environment.run(*shlex.split(command), escape_command=False)
269274

270275
if not commands:
271276
self.language_environment.setup_base()
272277

273278
def post_create_environment(self):
274-
commands = [] # self.data.config.build.jobs.post_create_environment
279+
commands = self.data.config.build.jobs.post_create_environment
275280
for command in commands:
276-
self.build_environment.run(command)
281+
self.build_environment.run(*shlex.split(command), escape_command=False)
277282

278283
# Install
279284
def pre_install(self):
280-
commands = [] # self.data.config.build.jobs.pre_install
285+
commands = self.data.config.build.jobs.pre_install
281286
for command in commands:
282-
self.build_environment.run(command)
287+
self.build_environment.run(*shlex.split(command), escape_command=False)
283288

284289
def install(self):
285290
commands = [] # self.data.config.build.jobs.install
286291
for command in commands:
287-
self.build_environment.run(command)
292+
self.build_environment.run(*shlex.split(command), escape_command=False)
288293

289294
if not commands:
290295
self.language_environment.install_core_requirements()
291296
self.language_environment.install_requirements()
292297

293298
def post_install(self):
294-
commands = [] # self.data.config.build.jobs.post_install
299+
commands = self.data.config.build.jobs.post_install
295300
for command in commands:
296-
self.build_environment.run(command)
301+
self.build_environment.run(*shlex.split(command), escape_command=False)
297302

298303
# Build
299304
def pre_build(self):
300-
commands = [] # self.data.config.build.jobs.pre_build
305+
commands = self.data.config.build.jobs.pre_build
301306
for command in commands:
302-
self.build_environment.run(command)
307+
self.build_environment.run(*shlex.split(command), escape_command=False)
303308

304309
def build_html(self):
305310
commands = [] # self.data.config.build.jobs.build.html
306311
if commands:
307312
for command in commands:
308-
self.build_environment.run(command)
313+
self.build_environment.run(*shlex.split(command), escape_command=False)
309314
return True
310315

311316
return self.build_docs_class(self.data.config.doctype)
@@ -317,7 +322,7 @@ def build_pdf(self):
317322
commands = [] # self.data.config.build.jobs.build.pdf
318323
if commands:
319324
for command in commands:
320-
self.build_environment.run(command)
325+
self.build_environment.run(*shlex.split(command), escape_command=False)
321326
return True
322327

323328
# Mkdocs has no pdf generation currently.
@@ -336,7 +341,7 @@ def build_htmlzip(self):
336341
commands = [] # self.data.config.build.jobs.build.htmlzip
337342
if commands:
338343
for command in commands:
339-
self.build_environment.run(command)
344+
self.build_environment.run(*shlex.split(command), escape_command=False)
340345
return True
341346

342347
# We don't generate a zip for mkdocs currently.
@@ -351,7 +356,7 @@ def build_epub(self):
351356
commands = [] # self.data.config.build.jobs.build.epub
352357
if commands:
353358
for command in commands:
354-
self.build_environment.run(command)
359+
self.build_environment.run(*shlex.split(command), escape_command=False)
355360
return True
356361

357362
# Mkdocs has no epub generation currently.
@@ -360,9 +365,9 @@ def build_epub(self):
360365
return False
361366

362367
def post_build(self):
363-
commands = [] # self.data.config.build.jobs.post_build
368+
commands = self.data.config.build.jobs.post_build
364369
for command in commands:
365-
self.build_environment.run(command)
370+
self.build_environment.run(*shlex.split(command), escape_command=False)
366371

367372
# Helpers
368373
#

0 commit comments

Comments
 (0)