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