Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Build: implementation of build.commands #9150

Merged
merged 37 commits into from
Jun 2, 2022
Merged
Show file tree
Hide file tree
Changes from 32 commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
88e942f
Build: POC of `build.commands`
humitos Apr 27, 2022
e578554
Docs: reference for `build.commands`
humitos May 23, 2022
5365087
Build: solve problem with sanitized output
humitos May 23, 2022
2286206
Build: decouple `install_build_tools` from `PythonEnvironment`
humitos May 23, 2022
97412b2
Template: add warning note on build details' page
humitos May 23, 2022
a22e3bd
Build: fail the build if `build.commands` without `output/` folder
humitos May 23, 2022
27d1781
Test: `build.commands` test cases
humitos May 23, 2022
e55d008
Merge branch 'main' of github.com:readthedocs/readthedocs.org into hu…
humitos May 23, 2022
243fde5
Typo in order of arguments
humitos May 23, 2022
66b41a8
Tests: fix mock patch's path
humitos May 23, 2022
935664a
Tests: compare against "" since we are not returning `None`
humitos May 23, 2022
958c2ce
Docs: mention that integrations aren't supported
humitos May 23, 2022
108357d
Docs: briefly mention to `build.commands` in "Build Customization"
humitos May 23, 2022
7f233ea
Use `.. code-block` because it accepts `:caption:`
humitos May 23, 2022
beeed60
Links with `:ref:` fixed
humitos May 23, 2022
464afbb
Remove caption
humitos May 23, 2022
2e4579a
Link to the correct page from UI
humitos May 23, 2022
e2f90b6
Docs: introduce extend and override at the beginning of the doc
humitos May 30, 2022
5c3e7cf
Docs: add caption to YAML examples
humitos May 30, 2022
f30f867
Docs: improves from suggestions
humitos May 30, 2022
6adb483
Build: make output path a constant and better error message
humitos May 30, 2022
c24c691
Build: use `_readthedocs/html` as output folder
humitos May 30, 2022
8b1fc27
Merge branch 'main' of github.com:readthedocs/readthedocs.org into hu…
humitos May 30, 2022
ad4eb4a
Build: import error
humitos May 30, 2022
996c97f
Build: return GENERIC doctype when building with `build.commands`
humitos May 30, 2022
1a9bf4f
Apply suggestions from code review
humitos Jun 1, 2022
d94f339
Build: doctype uses insternal config object
humitos Jun 1, 2022
472750c
Build: detect Python commands that require reshiming
humitos Jun 1, 2022
cd6f3c8
Build: `config.build.commands` is always present
humitos Jun 1, 2022
99efb00
Build: update the `Version.documentation_type` properly
humitos Jun 1, 2022
338fde5
Build: check for `commands` and emptyness
humitos Jun 1, 2022
b72906e
Test: `build.commands` and `Version.documentation_type` update
humitos Jun 1, 2022
4b7c14f
Build: update `Version.documentation_type` properly
humitos Jun 2, 2022
b5d168a
Build: define `reshim_commands` as set
humitos Jun 2, 2022
61bb432
Docs: minor typo
humitos Jun 2, 2022
632eb7c
Build: improve error message when no output folder
humitos Jun 2, 2022
9df29dd
Build: bugfix when working with sets and tuples
humitos Jun 2, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
75 changes: 65 additions & 10 deletions docs/user/build-customization.rst
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,18 @@ Build customization

Read the Docs has a :doc:`well-defined build process <builds>` that works for many projects,
but we offer additional customization to support more uses of our platform.
This page explains how to extend the build process, using :term:`user-defined build jobs` to execute custom commands.
This page explains how to extend the build process using :term:`user-defined build jobs` to execute custom commands,
and also how to override the build process completely:

`Extend the build process`_
If you are using Sphinx or Mkdocs and need to execute additional commands.

`Override the build process`_
If you want full control over your build. This option supports any tool that generates HTML as part of the build.


Extend the build process
------------------------

In the normal build process,
the pre-defined jobs ``checkout``, ``system_dependencies``, ``create_environment``, ``install``, ``build`` and ``upload`` are executed.
Expand Down Expand Up @@ -37,7 +48,8 @@ These jobs can be declared by using a :doc:`/config-file/index` with the :ref:`c
Let's say the project requires commands to be executed *before* installing any dependency into the Python environment and *after* the build has finished.
In that case, a config file similar to this one can be used:

.. code:: yaml
.. code-block:: yaml
:caption: .readthedocs.yaml

version: 2
build:
Expand All @@ -63,7 +75,7 @@ There are some caveats to knowing when using user-defined jobs:


Examples
--------
++++++++

We've included some common examples where using :ref:`config-file/v2:build.jobs` will be useful.
These examples may require some adaptation for each projects' use case,
Expand All @@ -77,7 +89,8 @@ Read the Docs does not perform a full clone on ``checkout`` job to reduce networ
Because of this, extensions that depend on the full Git history will fail.
To avoid this, it's possible to unshallow the clone done by Read the Docs:

.. code:: yaml
.. code-block:: yaml
:caption: .readthedocs.yaml

version: 2
build:
Expand All @@ -94,7 +107,8 @@ Generate documentation from annotated sources with Doxygen

It's possible to run Doxygen as part of the build process to generate documentation from annotated sources:

.. code:: yaml
.. code-block:: yaml
:caption: .readthedocs.yaml

version: 2
build:
Expand All @@ -114,7 +128,8 @@ Use MkDocs extensions with extra required steps
There are some MkDocs extensions that require specific commands to be run to generate extra pages before performing the build.
For example, `pydoc-markdown <http://niklasrosenstein.github.io/pydoc-markdown/>`_

.. code:: yaml
.. code-block:: yaml
:caption: .readthedocs.yaml

version: 2
build:
Expand All @@ -135,7 +150,8 @@ In case this happens and the project is using any kind of extension that generat
this could cause an invalid version number to be generated.
In that case, the Git index can be updated to ignore the files that Read the Docs has modified.

.. code:: yaml
.. code-block:: yaml
:caption: .readthedocs.yaml

version: 2
build:
Expand All @@ -154,7 +170,8 @@ Sphinx comes with a `linkcheck <https://www.sphinx-doc.org/en/master/usage/build
This helps ensure that all external links are still valid and readers aren't linked to non-existent pages.


.. code:: yaml
.. code-block:: yaml
:caption: .readthedocs.yaml

version: 2
build:
Expand All @@ -173,7 +190,8 @@ In case the repository contains large files that are tracked with Git LFS,
there are some extra steps required to be able to download their content.
It's possible to use ``post_checkout`` user-defined job for this.

.. code:: yaml
.. code-block:: yaml
:caption: .readthedocs.yaml

version: 2
build:
Expand Down Expand Up @@ -204,7 +222,8 @@ Install Node.js dependencies
It's possible to install Node.js together with the required dependencies by using :term:`user-defined build jobs`.
To setup it, you need to define the version of Node.js to use and install the dependencies by using ``build.jobs.post_install``:

.. code:: yaml
.. code-block:: yaml
:caption: .readthedocs.yaml

version: 2
build:
Expand All @@ -218,3 +237,39 @@ To setup it, you need to define the version of Node.js to use and install the de
- npm ci
# Install any other extra dependencies to build the docs
- npm install -g jsdoc


Override the build process
--------------------------

.. warning::

This feature is in a *beta phase* and could suffer incompatible changes or even removed completely in the near feature.
It does not yet support some of the Read the Docs' features like the :term:`flyout menu`, search and ads.
However, we do plan to support these features in the future.
Use this feature at your own risk.


If your project requires full control of the build process,
and :ref:`extending the build process <build-customization:extend the build process>` is not enough,
all the commands executed during builds can be overridden using the :ref:`config-file/v2:build.commands` configuration file key.

For example, if your project uses `Pelican <https://blog.getpelican.com/>`_ instead of Sphinx for it's documentation,
humitos marked this conversation as resolved.
Show resolved Hide resolved
your project could use the following configuration file:

.. code-block:: yaml
:caption: .readthedocs.yaml

version: 2
build:
os: "ubuntu-22.04"
tools:
python: "3.10"
commands:
- pip install pelican[markdown]
- pelican --settings docs/pelicanconf.py --output _readthedocs/html/ docs/


As Read the Docs does not have control over the build process,
you are responsible for running all the commands required to install requirements and build the documentation properly.
Once the build process finishes, the ``_readthedocs/html/`` folder will be hosted.
37 changes: 37 additions & 0 deletions docs/user/config-file/v2.rst
Original file line number Diff line number Diff line change
Expand Up @@ -466,6 +466,43 @@ See :doc:`/build-customization` for more details.
:Default: ``{}``


build.commands
``````````````

Specify a list of commands that Read the Docs will run on the build process.
When ``build.commands`` is used, none of the :term:`pre-defined build jobs` will be executed.
(see :doc:`/build-customization` for more details).
This allows you to run custom commands and control the build process completely.
The ``_readthedocs/html`` directory (relative to the checkout's path) will be uploaded and hosted by Read the Docs.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This still feels really small compared to how important the information is, but we can improve it over time.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree. This has to be a lot more prominent. However,

  1. I wasn't sure how to make it fit here in a good way
  2. I'm not sure this is the right place for explaining this more. From the config file, we should point users to the "Build customization" page and explain all the things in detail there. Otherwise, we will be overloading the config file reference

I'm thinking we may need a section in the "Build customization" page that list all the considerations required to use build.commands or something like that we can link from here. I feel I'm not coming with a good idea of how to write this, tho 🤷🏼


.. warning::

This feature is in a *beta phase* and could suffer incompatible changes or even removed completely in the near feature.
It does not yet support some of the Read the Docs' integrations like the :term:`flyout menu`, search and ads.
However, integrating all of them is part of the plan.
Use it under your own responsibility.
humitos marked this conversation as resolved.
Show resolved Hide resolved

.. code-block:: yaml
humitos marked this conversation as resolved.
Show resolved Hide resolved

version: 2

build:
os: ubuntu-22.04
tools:
python: "3.10"
commands:
- pip install pelican
- pelican --settings docs/pelicanconf.py --output _readthedocs/html/ docs/

.. note::

``build.os`` and ``build.tools`` are also required when using ``build.commands``.
humitos marked this conversation as resolved.
Show resolved Hide resolved

:Type: ``list``
:Required: ``false``
:Default: ``[]``


sphinx
~~~~~~

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# Generated by Django 3.2.13 on 2022-05-30 10:24

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
("builds", "0043_add_cancelled_state"),
]

operations = [
migrations.AlterField(
model_name="version",
name="documentation_type",
field=models.CharField(
choices=[
("sphinx", "Sphinx Html"),
("mkdocs", "Mkdocs"),
("sphinx_htmldir", "Sphinx HtmlDir"),
("sphinx_singlehtml", "Sphinx Single Page HTML"),
("mkdocs_html", "Mkdocs Html Pages"),
("generic", "Generic"),
],
default="sphinx",
help_text="Type of documentation the version was built with.",
max_length=20,
verbose_name="Documentation type",
),
),
]
22 changes: 22 additions & 0 deletions readthedocs/config/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
from django.conf import settings

from readthedocs.config.utils import list_to_dict, to_dict
from readthedocs.projects.constants import GENERIC

from .find import find_one
from .models import (
Expand Down Expand Up @@ -791,6 +792,11 @@ def validate_build_config_with_tools(self):
BuildJobs.__slots__,
)

commands = []
with self.catch_validation_error("build.commands"):
commands = self.pop_config("build.commands", default=[])
validate_list(commands)

if not tools:
self.error(
key='build.tools',
Expand All @@ -802,13 +808,25 @@ 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}"):
build["jobs"][job] = [
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}'):
Expand Down Expand Up @@ -1293,6 +1311,7 @@ def build(self):
os=build['os'],
tools=tools,
jobs=BuildJobs(**build["jobs"]),
commands=build["commands"],
apt_packages=build["apt_packages"],
)
return Build(**build)
Expand Down Expand Up @@ -1326,6 +1345,9 @@ def mkdocs(self):

@property
def doctype(self):
if "commands" in self._config["build"] and self._config["build"]["commands"]:
return GENERIC

if self.mkdocs:
return 'mkdocs'
return self.sphinx.builder
Expand Down
5 changes: 3 additions & 2 deletions readthedocs/config/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)


Expand Down
54 changes: 54 additions & 0 deletions readthedocs/config/tests/test_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -1013,6 +1013,59 @@ def test_new_build_config_conflict_with_build_python_version(self):
build.validate()
assert excinfo.value.key == 'python.version'

def test_commands_build_config(self):
build = self.get_build_config(
{
"build": {
"os": "ubuntu-20.04",
"tools": {"python": "3.8"},
"commands": ["pip install pelican", "pelican content"],
},
},
)
build.validate()
assert isinstance(build.build, BuildWithTools)
assert build.build.commands == ["pip install pelican", "pelican content"]

def test_commands_build_config_invalid_command(self):
build = self.get_build_config(
{
"build": {
"os": "ubuntu-20.04",
"tools": {"python": "3.8"},
"commands": "command as string",
},
},
)
with raises(InvalidConfig) as excinfo:
build.validate()
assert excinfo.value.key == "build.commands"

def test_commands_build_config_invalid_no_os(self):
build = self.get_build_config(
{
"build": {
"commands": ["pip install pelican", "pelican content"],
},
},
)
with raises(InvalidConfig) as excinfo:
build.validate()
assert excinfo.value.key == "build.commands"

def test_commands_build_config_invalid_no_tools(self):
build = self.get_build_config(
{
"build": {
"os": "ubuntu-22.04",
"commands": ["pip install pelican", "pelican content"],
},
},
)
with raises(InvalidConfig) as excinfo:
build.validate()
assert excinfo.value.key == "build.tools"

@pytest.mark.parametrize("value", ["", None, "pre_invalid"])
def test_jobs_build_config_invalid_jobs(self, value):
build = self.get_build_config(
Expand Down Expand Up @@ -2379,6 +2432,7 @@ def test_as_dict_new_build_config(self, tmpdir):
'full_version': settings.RTD_DOCKER_BUILD_SETTINGS['tools']['nodejs']['16'],
},
},
"commands": [],
"jobs": {
"pre_checkout": [],
"post_checkout": [],
Expand Down
Loading