Skip to content

Commit 88e942f

Browse files
committed
Build: POC of build.commands
Minimal implementation of `build.commands` as a POC to show how it could work. It's using a `.readthedocs.yaml` similar to this one: ```yaml version: 2 build: os: ubuntu-20.04 commands: - mkdir output/ - echo "Hello world" > output/index.html tools: python: "3" ``` This, of course, is not a good implementation and it's done pretty quick as a way to show what parts of the code are required to be touched. I think this will help with the discussion about how the UX around `build.commands` could work. It defines an "implicit contract" where the `output/` folder under the repository checkout will be uploaded to S3 and no HTML integrations will be done.
1 parent 0b36039 commit 88e942f

File tree

4 files changed

+56
-9
lines changed

4 files changed

+56
-9
lines changed

readthedocs/config/config.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -791,6 +791,11 @@ def validate_build_config_with_tools(self):
791791
BuildJobs.__slots__,
792792
)
793793

794+
commands = []
795+
with self.catch_validation_error("build.commands"):
796+
commands = self.pop_config("build.commands")
797+
validate_list(commands)
798+
794799
if not tools:
795800
self.error(
796801
key='build.tools',
@@ -802,13 +807,25 @@ def validate_build_config_with_tools(self):
802807
code=CONFIG_REQUIRED,
803808
)
804809

810+
if commands and jobs:
811+
self.error(
812+
key="build.commands",
813+
message="The keys build.jobs and build.commands can't be used together.",
814+
code=INVALID_KEYS_COMBINATION,
815+
)
816+
805817
build["jobs"] = {}
806818
for job, commands in jobs.items():
807819
with self.catch_validation_error(f"build.jobs.{job}"):
808820
build["jobs"][job] = [
809821
validate_string(command) for command in validate_list(commands)
810822
]
811823

824+
build["commands"] = []
825+
for command in commands:
826+
with self.catch_validation_error("build.commands"):
827+
build["commands"].append(validate_string(command))
828+
812829
build['tools'] = {}
813830
for tool, version in tools.items():
814831
with self.catch_validation_error(f'build.tools.{tool}'):
@@ -1293,6 +1310,7 @@ def build(self):
12931310
os=build['os'],
12941311
tools=tools,
12951312
jobs=BuildJobs(**build["jobs"]),
1313+
commands=build["commands"],
12961314
apt_packages=build["apt_packages"],
12971315
)
12981316
return Build(**build)

readthedocs/config/models.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,10 +37,11 @@ def __init__(self, **kwargs):
3737

3838
class BuildWithTools(Base):
3939

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

4242
def __init__(self, **kwargs):
43-
kwargs.setdefault('apt_packages', [])
43+
kwargs.setdefault("apt_packages", [])
44+
kwargs.setdefault("commands", [])
4445
super().__init__(**kwargs)
4546

4647

readthedocs/doc_builder/director.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import os
2+
import shutil
23
from collections import defaultdict
34

45
import structlog
@@ -335,6 +336,24 @@ def run_build_job(self, job):
335336
for command in commands:
336337
environment.run(*command.split(), escape_command=False, cwd=cwd)
337338

339+
def run_build_commands(self):
340+
cwd = self.data.project.checkout_path(self.data.version.slug)
341+
environment = self.vcs_environment
342+
for command in self.data.config.build.commands:
343+
environment.run(*command.split(), escape_command=False, cwd=cwd)
344+
345+
# Copy files to artifacts path so they are uploaded to S3
346+
target = self.data.project.artifact_path(
347+
version=self.data.version.slug,
348+
type_="sphinx",
349+
)
350+
artifacts_path = os.path.join(cwd, "output")
351+
shutil.copytree(
352+
artifacts_path,
353+
target,
354+
# ignore=shutil.ignore_patterns(*self.ignore_patterns),
355+
)
356+
338357
# Helpers
339358
#
340359
# TODO: move somewhere or change names to make them private or something to

readthedocs/projects/tasks/builds.py

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
"""
77
import signal
88
import socket
9+
from collections import defaultdict
910

1011
import structlog
1112
from celery import Task
@@ -601,13 +602,21 @@ def execute(self):
601602
self.data.build_director.create_build_environment()
602603
with self.data.build_director.build_environment:
603604
try:
604-
# Installing
605-
self.update_build(state=BUILD_STATE_INSTALLING)
606-
self.data.build_director.setup_environment()
607-
608-
# Building
609-
self.update_build(state=BUILD_STATE_BUILDING)
610-
self.data.build_director.build()
605+
# NOTE: check if the build uses `build.commands` and only run those
606+
if self.data.config.build.commands:
607+
self.update_build(state=BUILD_STATE_BUILDING)
608+
self.data.build_director.run_build_commands()
609+
610+
self.data.outcomes = defaultdict(lambda: False)
611+
self.data.outcomes["html"] = True
612+
else:
613+
# Installing
614+
self.update_build(state=BUILD_STATE_INSTALLING)
615+
self.data.build_director.setup_environment()
616+
617+
# Building
618+
self.update_build(state=BUILD_STATE_BUILDING)
619+
self.data.build_director.build()
611620
finally:
612621
self.data.build_data = self.collect_build_data()
613622

0 commit comments

Comments
 (0)