diff --git a/readthedocs/config/config.py b/readthedocs/config/config.py index a36d42fa53e..124afd1a2f3 100644 --- a/readthedocs/config/config.py +++ b/readthedocs/config/config.py @@ -790,6 +790,11 @@ def validate_build_config_with_tools(self): BuildJobs.__slots__, ) + commands = [] + with self.catch_validation_error("build.commands"): + commands = self.pop_config("build.commands") + validate_list(commands) + if not tools: self.error( key='build.tools', @@ -801,6 +806,13 @@ def validate_build_config_with_tools(self): code=CONFIG_REQUIRED, ) + if commands and jobs: + self.error( + key="build.commands", + message="The keys build.jobs and build.commands can't be used together.", + code=INVALID_KEYS_COMBINATION, + ) + build["jobs"] = {} for job, commands in jobs.items(): with self.catch_validation_error(f"build.jobs.{job}"): @@ -808,6 +820,11 @@ def validate_build_config_with_tools(self): validate_string(command) for command in validate_list(commands) ] + build["commands"] = [] + for command in commands: + with self.catch_validation_error("build.commands"): + build["commands"].append(validate_string(command)) + build['tools'] = {} for tool, version in tools.items(): with self.catch_validation_error(f'build.tools.{tool}'): @@ -1292,6 +1309,7 @@ def build(self): os=build['os'], tools=tools, jobs=BuildJobs(**build["jobs"]), + commands=build["commands"], apt_packages=build["apt_packages"], ) return Build(**build) diff --git a/readthedocs/config/models.py b/readthedocs/config/models.py index fa64ab983ef..864d670f099 100644 --- a/readthedocs/config/models.py +++ b/readthedocs/config/models.py @@ -37,10 +37,11 @@ def __init__(self, **kwargs): class BuildWithTools(Base): - __slots__ = ("os", "tools", "jobs", "apt_packages") + __slots__ = ("os", "tools", "jobs", "apt_packages", "commands") def __init__(self, **kwargs): - kwargs.setdefault('apt_packages', []) + kwargs.setdefault("apt_packages", []) + kwargs.setdefault("commands", []) super().__init__(**kwargs) diff --git a/readthedocs/doc_builder/director.py b/readthedocs/doc_builder/director.py index ced8621d7a1..5b2cb1bd29d 100644 --- a/readthedocs/doc_builder/director.py +++ b/readthedocs/doc_builder/director.py @@ -1,4 +1,5 @@ import os +import shutil from collections import defaultdict import structlog @@ -335,6 +336,24 @@ def run_build_job(self, job): for command in commands: environment.run(*command.split(), escape_command=False, cwd=cwd) + def run_build_commands(self): + cwd = self.data.project.checkout_path(self.data.version.slug) + environment = self.vcs_environment + for command in self.data.config.build.commands: + environment.run(*command.split(), escape_command=False, cwd=cwd) + + # Copy files to artifacts path so they are uploaded to S3 + target = self.data.project.artifact_path( + version=self.data.version.slug, + type_="sphinx", + ) + artifacts_path = os.path.join(cwd, "output") + shutil.copytree( + artifacts_path, + target, + # ignore=shutil.ignore_patterns(*self.ignore_patterns), + ) + # Helpers # # TODO: move somewhere or change names to make them private or something to diff --git a/readthedocs/projects/tasks/builds.py b/readthedocs/projects/tasks/builds.py index 0027278f6ca..f7692b8fdf2 100644 --- a/readthedocs/projects/tasks/builds.py +++ b/readthedocs/projects/tasks/builds.py @@ -6,6 +6,7 @@ """ import signal import socket +from collections import defaultdict import structlog from celery import Task @@ -595,6 +596,16 @@ def execute(self): # ``__exit__`` self.data.build_director.create_build_environment() with self.data.build_director.build_environment: + + # NOTE: check if the build uses `build.commands` and only run those + if self.data.config.build.commands: + self.update_build(state=BUILD_STATE_BUILDING) + self.data.build_director.run_build_commands() + + self.data.outcomes = defaultdict(lambda: False) + self.data.outcomes["html"] = True + return + # Installing self.update_build(state=BUILD_STATE_INSTALLING) self.data.build_director.setup_environment()