diff --git a/readthedocs/doc_builder/backends/mkdocs.py b/readthedocs/doc_builder/backends/mkdocs.py index 36f2e1e5d6f..acc9843f2e2 100644 --- a/readthedocs/doc_builder/backends/mkdocs.py +++ b/readthedocs/doc_builder/backends/mkdocs.py @@ -1,7 +1,5 @@ """ MkDocs_ backend for building docs. - -.. _MkDocs: http://www.mkdocs.org/ """ import os @@ -14,34 +12,25 @@ from readthedocs.doc_builder.base import BaseBuilder from readthedocs.projects.constants import MKDOCS from readthedocs.projects.constants import MKDOCS_HTML +from readthedocs.projects.exceptions import UserFileNotFound -log = structlog.get_logger(__name__) - - -def get_absolute_static_url(): +class BaseMkdocs(BaseBuilder): """ - Get the fully qualified static URL from settings. + MkDocs builder. - Mkdocs needs a full domain because it tries to link to local files. + This class and the following class use a different build method + than the Sphinx builders. We don't use `make` and instead call + the Python module directly. """ - static_url = settings.STATIC_URL - if not static_url.startswith("http"): - domain = settings.PRODUCTION_DOMAIN - static_url = "http://{}{}".format(domain, static_url) - - return static_url - - -class BaseMkdocs(BaseBuilder): - """Mkdocs builder.""" - - # The default theme for mkdocs is the 'mkdocs' theme - DEFAULT_THEME_NAME = "mkdocs" + type = "mkdocs" def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) + self.old_artifact_path = os.path.join( + self.project_path, self.config.mkdocs.build_dir + ) # This is the *MkDocs* yaml file self.yaml_file = self.get_yaml_config() @@ -50,63 +39,48 @@ def get_final_doctype(self): """ Select a doctype based on the ``use_directory_urls`` setting. - https://www.mkdocs.org/user-guide/configuration/#use_directory_urls + MkDocs produces a different style of html depending on the + ``use_directory_urls`` setting. We use this to set the doctype. """ + with safe_open(self.yaml_file, "r") as f: + config = yaml.safe_load(f) - # TODO: we should eventually remove this method completely and stop - # relying on "loading the `mkdocs.yml` file in a safe way just to know - # if it's a MKDOCS or MKDOCS_HTML documentation type". - - # Allow symlinks, but only the ones that resolve inside the base directory. - with safe_open( - self.yaml_file, - "r", - allow_symlinks=True, - base_path=self.project_path, - ) as fh: - config = yaml_load_safely(fh) - use_directory_urls = config.get("use_directory_urls", True) - return MKDOCS if use_directory_urls else MKDOCS_HTML + if config.get("use_directory_urls"): + return MKDOCS_HTML + return MKDOCS def get_yaml_config(self): - """Find the ``mkdocs.yml`` file in the project root.""" - mkdocs_path = self.config.mkdocs.configuration - if not mkdocs_path: - mkdocs_path = "mkdocs.yml" - return os.path.join( - self.project_path, - mkdocs_path, - ) - - def show_conf(self): - """Show the current ``mkdocs.yaml`` being used.""" - # Write the mkdocs.yml to the build logs - self.run( - "cat", - os.path.relpath(self.yaml_file, self.project_path), - cwd=self.project_path, - ) + """Find the MkDocs yaml configuration file.""" + # We support both `.yml` and `.yaml` extensions + for extension in ["yml", "yaml"]: + config_file = os.path.join( + self.project_path, f"mkdocs.{extension}" + ) + if os.path.exists(config_file): + return config_file + + # If we didn't find any, return the default one + return os.path.join(self.project_path, "mkdocs.yml") def build(self): build_command = [ self.python_env.venv_bin(filename="python"), "-m", "mkdocs", - self.builder, + "build", "--clean", "--site-dir", - os.path.join("$READTHEDOCS_OUTPUT", "html"), + self.old_artifact_path, "--config-file", os.path.relpath(self.yaml_file, self.project_path), ] + if self.config.mkdocs.fail_on_warning: build_command.append("--strict") cmd_ret = self.run( - *build_command, - cwd=self.project_path, - bin_path=self.python_env.venv_bin(), + *build_command, cwd=self.project_path, bin_path=self.python_env.venv_bin() ) - return cmd_ret.successful + return cmd_ret.success class MkdocsHTML(BaseMkdocs): diff --git a/readthedocs/projects/exceptions.py b/readthedocs/projects/exceptions.py index 466d09a174d..feee331df65 100644 --- a/readthedocs/projects/exceptions.py +++ b/readthedocs/projects/exceptions.py @@ -5,16 +5,16 @@ class ProjectConfigurationError(BuildUserError): - """Error raised trying to configure a project for build.""" - - NOT_FOUND = "project:sphinx:conf-py-not-found" - MULTIPLE_CONF_FILES = "project:sphinx:multiple-conf-py-files-found" + NOT_FOUND = "project:configuration:not-found" + MULTIPLE_CONF_FILES = "project:configuration:multiple-conf-files" + INVALID_VERSION = "project:configuration:invalid-version" + INVALID = "project:configuration:invalid" + MKDOCS_YAML_NOT_FOUND = "project:configuration:mkdocs-yaml-not-found" class UserFileNotFound(BuildUserError): FILE_NOT_FOUND = "project:file:not-found" - - + class RepositoryError(BuildUserError): """Failure during repository operation.""" diff --git a/readthedocs/projects/notifications.py b/readthedocs/projects/notifications.py index 9b9e5d43e7a..f24749f7f5a 100644 --- a/readthedocs/projects/notifications.py +++ b/readthedocs/projects/notifications.py @@ -129,13 +129,19 @@ id=ProjectConfigurationError.MULTIPLE_CONF_FILES, header=_("Multiple Sphinx configuration files found"), body=_( - textwrap.dedent( - """ - We found more than one conf.py and are not sure which one to use. - Please, specify the correct file under the Advanced settings tab - in the project's Admin. - """ - ).strip(), + "We found multiple Sphinx configuration files in your repository. " + "Please, remove one of them and specify the correct one in your " + "configuration file." + ), + type=ERROR, + ), + Message( + id=ProjectConfigurationError.MKDOCS_YAML_NOT_FOUND, + header=_("MkDocs configuration file not found"), + body=_( + "We could not find a MkDocs configuration file in your repository. " + "Please, make sure you have a 'mkdocs.yml' file in your repository " + "and try again." ), type=ERROR, ), diff --git a/readthedocs/projects/tests/test_build_tasks.py b/readthedocs/projects/tests/test_build_tasks.py index 4d3f48bfc84..72b59550678 100644 --- a/readthedocs/projects/tests/test_build_tasks.py +++ b/readthedocs/projects/tests/test_build_tasks.py @@ -282,6 +282,13 @@ def test_build_updates_documentation_type(self, load_yaml_config): validate=True, ) + mkdocs_path = os.path.join( + self.project.checkout_path(self.version.slug), + "mkdocs.yml" + ) + with open(mkdocs_path, "w") as f: + f.write("site_name: Test Project\n") + # Create the artifact paths, so that `store_build_artifacts` # properly runs: https://github.com/readthedocs/readthedocs.org/blob/faa611fad689675f81101ea643770a6b669bf529/readthedocs/projects/tasks/builds.py#L798-L804 os.makedirs(self.project.artifact_path(version=self.version.slug, type_="html")) @@ -2578,6 +2585,15 @@ def test_mkdocs_fail_on_warning(self, load_yaml_config): validate=True, ) + mkdocs_path = os.path.join( + self.project.checkout_path(self.version.slug), + "docs", + "mkdocs.yaml" + ) + os.makedirs(os.path.dirname(mkdocs_path), exist_ok=True) + with open(mkdocs_path, "w") as f: + f.write("site_name: Test Project\n") + self._trigger_update_docs_task() self.mocker.mocks["environment.run"].assert_has_calls(