1# Copyright (c) 2019 Intel Corporation
2#
3# SPDX-License-Identifier: Apache-2.0
4
5# based on http://protips.readthedocs.io/link-roles.html
6
7import re
8import subprocess
9from collections.abc import Sequence
10from pathlib import Path
11from typing import Any, Final
12
13from docutils import nodes
14from sphinx.util import logging
15
16ZEPHYR_BASE: Final[str] = Path(__file__).parents[3]
17
18try:
19    import west.manifest
20
21    try:
22        west_manifest = west.manifest.Manifest.from_file()
23    except west.util.WestNotFound:
24        west_manifest = None
25except ImportError:
26    west_manifest = None
27
28
29logger = logging.getLogger(__name__)
30
31
32def get_github_rev():
33    try:
34        output = subprocess.check_output(
35            "git describe --exact-match", shell=True, stderr=subprocess.DEVNULL
36        )
37    except subprocess.CalledProcessError:
38        return "main"
39
40    return output.strip().decode("utf-8")
41
42
43def setup(app):
44    app.add_role("zephyr_file", modulelink("zephyr"))
45    app.add_role("zephyr_raw", modulelink("zephyr", format="raw"))
46    app.add_role("module_file", modulelink())
47
48    app.add_config_value("link_roles_manifest_baseurl", None, "env")
49    app.add_config_value("link_roles_manifest_project", None, "env")
50    app.add_config_value("link_roles_manifest_project_broken_links_ignore_globs", [], "env")
51
52    # The role just creates new nodes based on information in the
53    # arguments; its behavior doesn't depend on any other documents.
54    return {
55        "parallel_read_safe": True,
56        "parallel_write_safe": True,
57    }
58
59
60def modulelink(default_module=None, format="blob"):
61    def role(
62        name: str,
63        rawtext: str,
64        text: str,
65        lineno: int,
66        inliner,
67        options: dict[str, Any] | None = None,
68        content: Sequence[str] = (),
69    ):
70        if options is None:
71            options = {}
72        module = default_module
73        rev = get_github_rev()
74        config = inliner.document.settings.env.app.config
75        baseurl = config.link_roles_manifest_baseurl
76        source, line = inliner.reporter.get_source_and_line(lineno)
77        trace = f"at '{source}:{line}'"
78
79        m = re.search(r"(.*)\s*<(.*)>", text)
80        if m:
81            link_text = m.group(1)
82            link = m.group(2)
83        else:
84            link_text = text
85            link = text
86
87        module_match = re.search(r"(.+?):\s*(.+)", link)
88        if module_match:
89            module = module_match.group(1).strip()
90            link = module_match.group(2).strip()
91
92        # Try to get a module repository's GitHub URL from the manifest.
93        #
94        # This allows e.g. building the docs in downstream Zephyr-based
95        # software with forks of the zephyr repository, and getting
96        # :zephyr_file: / :zephyr_raw: output that links to the fork,
97        # instead of mainline zephyr.
98        projects = [p.name for p in west_manifest.projects] if west_manifest else []
99        if module in projects:
100            project = west_manifest.get_projects([module])[0]
101            baseurl = project.url
102            rev = project.revision
103        # No module provided
104        elif module is None:
105            raise ValueError(
106                f"Role 'module_file' must take a module as an argument\n\t{trace}"
107            )
108        # Invalid module provided
109        elif module != config.link_roles_manifest_project:
110            logger.debug(f"Module {module} not found in the west manifest")
111        # Baseurl for manifest project not set
112        elif baseurl is None:
113            raise ValueError(
114                f"Configuration value `link_roles_manifest_baseurl` not set\n\t{trace}"
115            )
116
117        if module == config.link_roles_manifest_project:
118            p = Path(source).relative_to(inliner.document.settings.env.srcdir)
119            if not any(
120                p.match(glob)
121                for glob in config.link_roles_manifest_project_broken_links_ignore_globs
122            ) and not Path(ZEPHYR_BASE, link).exists():
123                logger.warning(
124                    f"{link} not found in {config.link_roles_manifest_project} {trace}"
125                )
126
127        url = f"{baseurl}/{format}/{rev}/{link}"
128        node = nodes.reference(rawtext, link_text, refuri=url, **options)
129        return [node], []
130
131    return role
132