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
7from __future__ import print_function
8from __future__ import unicode_literals
9import re
10import subprocess
11from docutils import nodes
12
13try:
14    import west.manifest
15
16    try:
17        west_manifest = west.manifest.Manifest.from_file()
18    except west.util.WestNotFound:
19        west_manifest = None
20except ImportError:
21    west_manifest = None
22
23
24def get_github_rev():
25    try:
26        output = subprocess.check_output(
27            "git describe --exact-match", shell=True, stderr=subprocess.DEVNULL
28        )
29    except subprocess.CalledProcessError:
30        return "main"
31
32    return output.strip().decode("utf-8")
33
34
35def setup(app):
36    app.add_role("zephyr_file", modulelink("zephyr"))
37    app.add_role("zephyr_raw", modulelink("zephyr", format="raw"))
38    app.add_role("module_file", modulelink())
39
40    app.add_config_value("link_roles_manifest_baseurl", None, "env")
41    app.add_config_value("link_roles_manifest_project", None, "env")
42
43    # The role just creates new nodes based on information in the
44    # arguments; its behavior doesn't depend on any other documents.
45    return {
46        "parallel_read_safe": True,
47        "parallel_write_safe": True,
48    }
49
50
51def modulelink(default_module=None, format="blob"):
52    def role(name, rawtext, text, lineno, inliner, options={}, content=[]):
53        # Set default values
54        module = default_module
55        rev = get_github_rev()
56        config = inliner.document.settings.env.app.config
57        baseurl = config.link_roles_manifest_baseurl
58        trace = f"at '{inliner.parent.source}', line {lineno}"
59
60        m = re.search(r"(.*)\s*<(.*)>", text)
61        if m:
62            link_text = m.group(1)
63            link = m.group(2)
64        else:
65            link_text = text
66            link = text
67
68        module_match = re.search(r"(.+?):\s*(.+)", link)
69        if module_match:
70            module = module_match.group(1).strip()
71            link = module_match.group(2).strip()
72
73        # Try to get a module repository's GitHub URL from the manifest.
74        #
75        # This allows e.g. building the docs in downstream Zephyr-based
76        # software with forks of the zephyr repository, and getting
77        # :zephyr_file: / :zephyr_raw: output that links to the fork,
78        # instead of mainline zephyr.
79        projects = [p.name for p in west_manifest.projects] if west_manifest else []
80        if module in projects:
81            project = west_manifest.get_projects([module])[0]
82            baseurl = project.url
83            rev = project.revision
84        # No module provided
85        elif module is None:
86            raise ValueError(
87                f"Role 'module_file' must take a module as an argument\n\t{trace}"
88            )
89        # Invalid module provided
90        elif module != config.link_roles_manifest_project:
91            raise ModuleNotFoundError(
92                f"Module {module} not found in the west manifest\n\t{trace}"
93            )
94        # Baseurl for manifest project not set
95        elif baseurl is None:
96            raise ValueError(
97                f"Configuration value `link_roles_manifest_baseurl` not set\n\t{trace}"
98            )
99
100        url = f"{baseurl}/{format}/{rev}/{link}"
101        node = nodes.reference(rawtext, link_text, refuri=url, **options)
102        return [node], []
103
104    return role
105