cmake.py 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645
  1. # Distributed under the OSI-approved BSD 3-Clause License. See accompanying
  2. # file Copyright.txt or https://cmake.org/licensing for details.
  3. import os
  4. import re
  5. from dataclasses import dataclass
  6. from typing import Any, List, cast
  7. # Override much of pygments' CMakeLexer.
  8. # We need to parse CMake syntax definitions, not CMake code.
  9. # For hard test cases that use much of the syntax below, see
  10. # - module/FindPkgConfig.html (with "glib-2.0>=2.10 gtk+-2.0" and similar)
  11. # - module/ExternalProject.html (with http:// https:// git@; also has command options -E --build)
  12. # - manual/cmake-buildsystem.7.html (with nested $<..>; relative and absolute paths, "::")
  13. from pygments.lexers import CMakeLexer
  14. from pygments.token import Name, Operator, Punctuation, String, Text, Comment, Generic, Whitespace, Number
  15. from pygments.lexer import bygroups
  16. # RE to split multiple command signatures
  17. sig_end_re = re.compile(r'(?<=[)])\n')
  18. # Notes on regular expressions below:
  19. # - [\.\+-] are needed for string constants like gtk+-2.0
  20. # - Unix paths are recognized by '/'; support for Windows paths may be added if needed
  21. # - (\\.) allows for \-escapes (used in manual/cmake-language.7)
  22. # - $<..$<..$>..> nested occurrence in cmake-buildsystem
  23. # - Nested variable evaluations are only supported in a limited capacity. Only
  24. # one level of nesting is supported and at most one nested variable can be present.
  25. CMakeLexer.tokens["root"] = [
  26. (r'\b(\w+)([ \t]*)(\()', bygroups(Name.Function, Text, Name.Function), '#push'), # fctn(
  27. (r'\(', Name.Function, '#push'),
  28. (r'\)', Name.Function, '#pop'),
  29. (r'\[', Punctuation, '#push'),
  30. (r'\]', Punctuation, '#pop'),
  31. (r'[|;,.=*\-]', Punctuation),
  32. (r'\\\\', Punctuation), # used in commands/source_group
  33. (r'[:]', Operator),
  34. (r'[<>]=', Punctuation), # used in FindPkgConfig.cmake
  35. (r'\$<', Operator, '#push'), # $<...>
  36. (r'<[^<|]+?>(\w*\.\.\.)?', Name.Variable), # <expr>
  37. (r'(\$\w*\{)([^\}\$]*)?(?:(\$\w*\{)([^\}]+?)(\}))?([^\}]*?)(\})', # ${..} $ENV{..}, possibly nested
  38. bygroups(Operator, Name.Tag, Operator, Name.Tag, Operator, Name.Tag, Operator)),
  39. (r'([A-Z]+\{)(.+?)(\})', bygroups(Operator, Name.Tag, Operator)), # DATA{ ...}
  40. (r'[a-z]+(@|(://))((\\.)|[\w.+-:/\\])+', Name.Attribute), # URL, git@, ...
  41. (r'/\w[\w\.\+-/\\]*', Name.Attribute), # absolute path
  42. (r'/', Name.Attribute),
  43. (r'\w[\w\.\+-]*/[\w.+-/\\]*', Name.Attribute), # relative path
  44. (r'[A-Z]((\\.)|[\w.+-])*[a-z]((\\.)|[\w.+-])*', Name.Builtin), # initial A-Z, contains a-z
  45. (r'@?[A-Z][A-Z0-9_]*', Name.Constant),
  46. (r'[a-z_]((\\;)|(\\ )|[\w.+-])*', Name.Builtin),
  47. (r'[0-9][0-9\.]*', Number),
  48. (r'(?s)"(\\"|[^"])*"', String), # "string"
  49. (r'\.\.\.', Name.Variable),
  50. (r'<', Operator, '#push'), # <..|..> is different from <expr>
  51. (r'>', Operator, '#pop'),
  52. (r'\n', Whitespace),
  53. (r'[ \t]+', Whitespace),
  54. (r'#.*\n', Comment),
  55. # (r'[^<>\])\}\|$"# \t\n]+', Name.Exception), # fallback, for debugging only
  56. ]
  57. from docutils.utils.code_analyzer import Lexer, LexerError
  58. from docutils.parsers.rst import Directive, directives
  59. from docutils.transforms import Transform
  60. from docutils import io, nodes
  61. from sphinx.directives import ObjectDescription, nl_escape_re
  62. from sphinx.domains import Domain, ObjType
  63. from sphinx.roles import XRefRole
  64. from sphinx.util.nodes import make_refnode
  65. from sphinx.util import logging, ws_re
  66. from sphinx import addnodes
  67. logger = logging.getLogger(__name__)
  68. sphinx_before_1_4 = False
  69. sphinx_before_1_7_2 = False
  70. try:
  71. from sphinx import version_info
  72. if version_info < (1, 4):
  73. sphinx_before_1_4 = True
  74. if version_info < (1, 7, 2):
  75. sphinx_before_1_7_2 = True
  76. except ImportError:
  77. # The `sphinx.version_info` tuple was added in Sphinx v1.2:
  78. sphinx_before_1_4 = True
  79. sphinx_before_1_7_2 = True
  80. if sphinx_before_1_7_2:
  81. # Monkey patch for sphinx generating invalid content for qcollectiongenerator
  82. # https://github.com/sphinx-doc/sphinx/issues/1435
  83. from sphinx.util.pycompat import htmlescape
  84. from sphinx.builders.qthelp import QtHelpBuilder
  85. old_build_keywords = QtHelpBuilder.build_keywords
  86. def new_build_keywords(self, title, refs, subitems):
  87. old_items = old_build_keywords(self, title, refs, subitems)
  88. new_items = []
  89. for item in old_items:
  90. before, rest = item.split("ref=\"", 1)
  91. ref, after = rest.split("\"")
  92. if ("<" in ref and ">" in ref):
  93. new_items.append(before + "ref=\"" + htmlescape(ref) + "\"" + after)
  94. else:
  95. new_items.append(item)
  96. return new_items
  97. QtHelpBuilder.build_keywords = new_build_keywords
  98. @dataclass
  99. class ObjectEntry:
  100. docname: str
  101. objtype: str
  102. node_id: str
  103. name: str
  104. class CMakeModule(Directive):
  105. required_arguments = 1
  106. optional_arguments = 0
  107. final_argument_whitespace = True
  108. option_spec = {'encoding': directives.encoding}
  109. def __init__(self, *args, **keys):
  110. self.re_start = re.compile(r'^#\[(?P<eq>=*)\[\.rst:$')
  111. Directive.__init__(self, *args, **keys)
  112. def run(self):
  113. settings = self.state.document.settings
  114. if not settings.file_insertion_enabled:
  115. raise self.warning('"%s" directive disabled.' % self.name)
  116. env = self.state.document.settings.env
  117. rel_path, path = env.relfn2path(self.arguments[0])
  118. path = os.path.normpath(path)
  119. encoding = self.options.get('encoding', settings.input_encoding)
  120. e_handler = settings.input_encoding_error_handler
  121. try:
  122. settings.record_dependencies.add(path)
  123. f = io.FileInput(source_path=path, encoding=encoding,
  124. error_handler=e_handler)
  125. except UnicodeEncodeError as error:
  126. msg = ('Problems with "%s" directive path:\n'
  127. 'Cannot encode input file path "%s" '
  128. '(wrong locale?).' % (self.name, path))
  129. raise self.severe(msg)
  130. except IOError as error:
  131. msg = 'Problems with "%s" directive path:\n%s.' % (self.name, error)
  132. raise self.severe(msg)
  133. raw_lines = f.read().splitlines()
  134. f.close()
  135. rst = None
  136. lines = []
  137. for line in raw_lines:
  138. if rst is not None and rst != '#':
  139. # Bracket mode: check for end bracket
  140. pos = line.find(rst)
  141. if pos >= 0:
  142. if line[0] == '#':
  143. line = ''
  144. else:
  145. line = line[0:pos]
  146. rst = None
  147. else:
  148. # Line mode: check for .rst start (bracket or line)
  149. m = self.re_start.match(line)
  150. if m:
  151. rst = ']%s]' % m.group('eq')
  152. line = ''
  153. elif line == '#.rst:':
  154. rst = '#'
  155. line = ''
  156. elif rst == '#':
  157. if line == '#' or line[:2] == '# ':
  158. line = line[2:]
  159. else:
  160. rst = None
  161. line = ''
  162. elif rst is None:
  163. line = ''
  164. lines.append(line)
  165. if rst is not None and rst != '#':
  166. raise self.warning('"%s" found unclosed bracket "#[%s[.rst:" in %s' %
  167. (self.name, rst[1:-1], path))
  168. self.state_machine.insert_input(lines, path)
  169. return []
  170. class _cmake_index_entry:
  171. def __init__(self, desc):
  172. self.desc = desc
  173. def __call__(self, title, targetid, main = 'main'):
  174. # See https://github.com/sphinx-doc/sphinx/issues/2673
  175. if sphinx_before_1_4:
  176. return ('pair', u'%s ; %s' % (self.desc, title), targetid, main)
  177. else:
  178. return ('pair', u'%s ; %s' % (self.desc, title), targetid, main, None)
  179. _cmake_index_objs = {
  180. 'command': _cmake_index_entry('command'),
  181. 'cpack_gen': _cmake_index_entry('cpack generator'),
  182. 'envvar': _cmake_index_entry('envvar'),
  183. 'generator': _cmake_index_entry('generator'),
  184. 'genex': _cmake_index_entry('genex'),
  185. 'guide': _cmake_index_entry('guide'),
  186. 'manual': _cmake_index_entry('manual'),
  187. 'module': _cmake_index_entry('module'),
  188. 'policy': _cmake_index_entry('policy'),
  189. 'prop_cache': _cmake_index_entry('cache property'),
  190. 'prop_dir': _cmake_index_entry('directory property'),
  191. 'prop_gbl': _cmake_index_entry('global property'),
  192. 'prop_inst': _cmake_index_entry('installed file property'),
  193. 'prop_sf': _cmake_index_entry('source file property'),
  194. 'prop_test': _cmake_index_entry('test property'),
  195. 'prop_tgt': _cmake_index_entry('target property'),
  196. 'variable': _cmake_index_entry('variable'),
  197. }
  198. class CMakeTransform(Transform):
  199. # Run this transform early since we insert nodes we want
  200. # treated as if they were written in the documents.
  201. default_priority = 210
  202. def __init__(self, document, startnode):
  203. Transform.__init__(self, document, startnode)
  204. self.titles = {}
  205. def parse_title(self, docname):
  206. """Parse a document title as the first line starting in [A-Za-z0-9<$]
  207. or fall back to the document basename if no such line exists.
  208. The cmake --help-*-list commands also depend on this convention.
  209. Return the title or False if the document file does not exist.
  210. """
  211. env = self.document.settings.env
  212. title = self.titles.get(docname)
  213. if title is None:
  214. fname = os.path.join(env.srcdir, docname+'.rst')
  215. try:
  216. f = open(fname, 'r')
  217. except IOError:
  218. title = False
  219. else:
  220. for line in f:
  221. if len(line) > 0 and (line[0].isalnum() or line[0] == '<' or line[0] == '$'):
  222. title = line.rstrip()
  223. break
  224. f.close()
  225. if title is None:
  226. title = os.path.basename(docname)
  227. self.titles[docname] = title
  228. return title
  229. def apply(self):
  230. env = self.document.settings.env
  231. # Treat some documents as cmake domain objects.
  232. objtype, sep, tail = env.docname.partition('/')
  233. make_index_entry = _cmake_index_objs.get(objtype)
  234. if make_index_entry:
  235. title = self.parse_title(env.docname)
  236. # Insert the object link target.
  237. if objtype == 'command':
  238. targetname = title.lower()
  239. elif objtype == 'guide' and not tail.endswith('/index'):
  240. targetname = tail
  241. else:
  242. if objtype == 'genex':
  243. m = CMakeXRefRole._re_genex.match(title)
  244. if m:
  245. title = m.group(1)
  246. targetname = title
  247. targetid = '%s:%s' % (objtype, targetname)
  248. targetnode = nodes.target('', '', ids=[targetid])
  249. self.document.note_explicit_target(targetnode)
  250. self.document.insert(0, targetnode)
  251. # Insert the object index entry.
  252. indexnode = addnodes.index()
  253. indexnode['entries'] = [make_index_entry(title, targetid)]
  254. self.document.insert(0, indexnode)
  255. # Add to cmake domain object inventory
  256. domain = cast(CMakeDomain, env.get_domain('cmake'))
  257. domain.note_object(objtype, targetname, targetid, targetid)
  258. class CMakeObject(ObjectDescription):
  259. def handle_signature(self, sig, signode):
  260. # called from sphinx.directives.ObjectDescription.run()
  261. signode += addnodes.desc_name(sig, sig)
  262. if self.objtype == 'genex':
  263. m = CMakeXRefRole._re_genex.match(sig)
  264. if m:
  265. sig = m.group(1)
  266. return sig
  267. def add_target_and_index(self, name, sig, signode):
  268. if self.objtype == 'command':
  269. targetname = name.lower()
  270. else:
  271. targetname = name
  272. targetid = '%s:%s' % (self.objtype, targetname)
  273. if targetid not in self.state.document.ids:
  274. signode['names'].append(targetid)
  275. signode['ids'].append(targetid)
  276. signode['first'] = (not self.names)
  277. self.state.document.note_explicit_target(signode)
  278. domain = cast(CMakeDomain, self.env.get_domain('cmake'))
  279. domain.note_object(self.objtype, targetname, targetid, targetid,
  280. location=signode)
  281. make_index_entry = _cmake_index_objs.get(self.objtype)
  282. if make_index_entry:
  283. self.indexnode['entries'].append(make_index_entry(name, targetid))
  284. class CMakeSignatureObject(CMakeObject):
  285. object_type = 'signature'
  286. BREAK_ALL = 'all'
  287. BREAK_SMART = 'smart'
  288. BREAK_VERBATIM = 'verbatim'
  289. BREAK_CHOICES = {BREAK_ALL, BREAK_SMART, BREAK_VERBATIM}
  290. def break_option(argument):
  291. return directives.choice(argument, CMakeSignatureObject.BREAK_CHOICES)
  292. option_spec = {
  293. 'target': directives.unchanged,
  294. 'break': break_option,
  295. }
  296. def _break_signature_all(sig: str) -> str:
  297. return ws_re.sub(' ', sig)
  298. def _break_signature_verbatim(sig: str) -> str:
  299. lines = [ws_re.sub('\xa0', line.strip()) for line in sig.split('\n')]
  300. return ' '.join(lines)
  301. def _break_signature_smart(sig: str) -> str:
  302. tokens = []
  303. for line in sig.split('\n'):
  304. token = ''
  305. delim = ''
  306. for c in line.strip():
  307. if len(delim) == 0 and ws_re.match(c):
  308. if len(token):
  309. tokens.append(ws_re.sub('\xa0', token))
  310. token = ''
  311. else:
  312. if c == '[':
  313. delim += ']'
  314. elif c == '<':
  315. delim += '>'
  316. elif len(delim) and c == delim[-1]:
  317. delim = delim[:-1]
  318. token += c
  319. if len(token):
  320. tokens.append(ws_re.sub('\xa0', token))
  321. return ' '.join(tokens)
  322. def __init__(self, *args, **kwargs):
  323. self.targetnames = {}
  324. self.break_style = CMakeSignatureObject.BREAK_SMART
  325. super().__init__(*args, **kwargs)
  326. def get_signatures(self) -> List[str]:
  327. content = nl_escape_re.sub('', self.arguments[0])
  328. lines = sig_end_re.split(content)
  329. if self.break_style == CMakeSignatureObject.BREAK_VERBATIM:
  330. fixup = CMakeSignatureObject._break_signature_verbatim
  331. elif self.break_style == CMakeSignatureObject.BREAK_SMART:
  332. fixup = CMakeSignatureObject._break_signature_smart
  333. else:
  334. fixup = CMakeSignatureObject._break_signature_all
  335. return [fixup(line.strip()) for line in lines]
  336. def handle_signature(self, sig, signode):
  337. language = 'cmake'
  338. classes = ['code', 'cmake', 'highlight']
  339. node = addnodes.desc_name(sig, '', classes=classes)
  340. try:
  341. tokens = Lexer(sig, language, 'short')
  342. except LexerError as error:
  343. if self.state.document.settings.report_level > 2:
  344. # Silently insert without syntax highlighting.
  345. tokens = Lexer(sig, language, 'none')
  346. else:
  347. raise self.warning(error)
  348. for classes, value in tokens:
  349. if value == '\xa0':
  350. node += nodes.inline(value, value, classes=['nbsp'])
  351. elif classes:
  352. node += nodes.inline(value, value, classes=classes)
  353. else:
  354. node += nodes.Text(value)
  355. signode.clear()
  356. signode += node
  357. return sig
  358. def add_target_and_index(self, name, sig, signode):
  359. sig = sig.replace('\xa0', ' ')
  360. if sig in self.targetnames:
  361. sigargs = self.targetnames[sig]
  362. else:
  363. def extract_keywords(params):
  364. for p in params:
  365. if p[0].isalpha():
  366. yield p
  367. else:
  368. return
  369. keywords = extract_keywords(sig.split('(')[1].split())
  370. sigargs = ' '.join(keywords)
  371. targetname = sigargs.lower()
  372. targetid = nodes.make_id(targetname)
  373. if targetid not in self.state.document.ids:
  374. signode['names'].append(targetname)
  375. signode['ids'].append(targetid)
  376. signode['first'] = (not self.names)
  377. self.state.document.note_explicit_target(signode)
  378. # Register the signature as a command object.
  379. command = sig.split('(')[0].lower()
  380. refname = f'{command}({sigargs})'
  381. refid = f'command:{command}({targetname})'
  382. domain = cast(CMakeDomain, self.env.get_domain('cmake'))
  383. domain.note_object('command', name=refname, target_id=refid,
  384. node_id=targetid, location=signode)
  385. def run(self):
  386. self.break_style = CMakeSignatureObject.BREAK_ALL
  387. targets = self.options.get('target')
  388. if targets is not None:
  389. signatures = self.get_signatures()
  390. targets = [t.strip() for t in targets.split('\n')]
  391. for signature, target in zip(signatures, targets):
  392. self.targetnames[signature] = target
  393. self.break_style = (
  394. self.options.get('break', CMakeSignatureObject.BREAK_SMART))
  395. return super().run()
  396. class CMakeXRefRole(XRefRole):
  397. # See sphinx.util.nodes.explicit_title_re; \x00 escapes '<'.
  398. _re = re.compile(r'^(.+?)(\s*)(?<!\x00)<(.*?)>$', re.DOTALL)
  399. _re_ref = re.compile(r'^.*\s<\w+([(][\w\s]+[)])?>$', re.DOTALL)
  400. _re_genex = re.compile(r'^\$<([^<>:]+)(:[^<>]+)?>$', re.DOTALL)
  401. _re_guide = re.compile(r'^([^<>/]+)/([^<>]*)$', re.DOTALL)
  402. def __call__(self, typ, rawtext, text, *args, **keys):
  403. if typ == 'cmake:command':
  404. m = CMakeXRefRole._re_ref.match(text)
  405. if m is None:
  406. text = f'{text} <{text}>'
  407. elif typ == 'cmake:genex':
  408. m = CMakeXRefRole._re_genex.match(text)
  409. if m:
  410. text = '%s <%s>' % (text, m.group(1))
  411. elif typ == 'cmake:guide':
  412. m = CMakeXRefRole._re_guide.match(text)
  413. if m:
  414. text = '%s <%s>' % (m.group(2), text)
  415. # CMake cross-reference targets frequently contain '<' so escape
  416. # any explicit `<target>` with '<' not preceded by whitespace.
  417. while True:
  418. m = CMakeXRefRole._re.match(text)
  419. if m and len(m.group(2)) == 0:
  420. text = '%s\x00<%s>' % (m.group(1), m.group(3))
  421. else:
  422. break
  423. return XRefRole.__call__(self, typ, rawtext, text, *args, **keys)
  424. # We cannot insert index nodes using the result_nodes method
  425. # because CMakeXRefRole is processed before substitution_reference
  426. # nodes are evaluated so target nodes (with 'ids' fields) would be
  427. # duplicated in each evaluated substitution replacement. The
  428. # docutils substitution transform does not allow this. Instead we
  429. # use our own CMakeXRefTransform below to add index entries after
  430. # substitutions are completed.
  431. #
  432. # def result_nodes(self, document, env, node, is_ref):
  433. # pass
  434. class CMakeXRefTransform(Transform):
  435. # Run this transform early since we insert nodes we want
  436. # treated as if they were written in the documents, but
  437. # after the sphinx (210) and docutils (220) substitutions.
  438. default_priority = 221
  439. def apply(self):
  440. env = self.document.settings.env
  441. # Find CMake cross-reference nodes and add index and target
  442. # nodes for them.
  443. for ref in self.document.traverse(addnodes.pending_xref):
  444. if not ref['refdomain'] == 'cmake':
  445. continue
  446. objtype = ref['reftype']
  447. make_index_entry = _cmake_index_objs.get(objtype)
  448. if not make_index_entry:
  449. continue
  450. objname = ref['reftarget']
  451. if objtype == 'guide' and CMakeXRefRole._re_guide.match(objname):
  452. # Do not index cross-references to guide sections.
  453. continue
  454. if objtype == 'command':
  455. # Index signature references to their parent command.
  456. objname = objname.split('(')[0].lower()
  457. targetnum = env.new_serialno('index-%s:%s' % (objtype, objname))
  458. targetid = 'index-%s-%s:%s' % (targetnum, objtype, objname)
  459. targetnode = nodes.target('', '', ids=[targetid])
  460. self.document.note_explicit_target(targetnode)
  461. indexnode = addnodes.index()
  462. indexnode['entries'] = [make_index_entry(objname, targetid, '')]
  463. ref.replace_self([indexnode, targetnode, ref])
  464. class CMakeDomain(Domain):
  465. """CMake domain."""
  466. name = 'cmake'
  467. label = 'CMake'
  468. object_types = {
  469. 'command': ObjType('command', 'command'),
  470. 'cpack_gen': ObjType('cpack_gen', 'cpack_gen'),
  471. 'envvar': ObjType('envvar', 'envvar'),
  472. 'generator': ObjType('generator', 'generator'),
  473. 'genex': ObjType('genex', 'genex'),
  474. 'guide': ObjType('guide', 'guide'),
  475. 'variable': ObjType('variable', 'variable'),
  476. 'module': ObjType('module', 'module'),
  477. 'policy': ObjType('policy', 'policy'),
  478. 'prop_cache': ObjType('prop_cache', 'prop_cache'),
  479. 'prop_dir': ObjType('prop_dir', 'prop_dir'),
  480. 'prop_gbl': ObjType('prop_gbl', 'prop_gbl'),
  481. 'prop_inst': ObjType('prop_inst', 'prop_inst'),
  482. 'prop_sf': ObjType('prop_sf', 'prop_sf'),
  483. 'prop_test': ObjType('prop_test', 'prop_test'),
  484. 'prop_tgt': ObjType('prop_tgt', 'prop_tgt'),
  485. 'manual': ObjType('manual', 'manual'),
  486. }
  487. directives = {
  488. 'command': CMakeObject,
  489. 'envvar': CMakeObject,
  490. 'genex': CMakeObject,
  491. 'signature': CMakeSignatureObject,
  492. 'variable': CMakeObject,
  493. # Other `object_types` cannot be created except by the `CMakeTransform`
  494. }
  495. roles = {
  496. 'command': CMakeXRefRole(fix_parens = True, lowercase = True),
  497. 'cpack_gen': CMakeXRefRole(),
  498. 'envvar': CMakeXRefRole(),
  499. 'generator': CMakeXRefRole(),
  500. 'genex': CMakeXRefRole(),
  501. 'guide': CMakeXRefRole(),
  502. 'variable': CMakeXRefRole(),
  503. 'module': CMakeXRefRole(),
  504. 'policy': CMakeXRefRole(),
  505. 'prop_cache': CMakeXRefRole(),
  506. 'prop_dir': CMakeXRefRole(),
  507. 'prop_gbl': CMakeXRefRole(),
  508. 'prop_inst': CMakeXRefRole(),
  509. 'prop_sf': CMakeXRefRole(),
  510. 'prop_test': CMakeXRefRole(),
  511. 'prop_tgt': CMakeXRefRole(),
  512. 'manual': CMakeXRefRole(),
  513. }
  514. initial_data = {
  515. 'objects': {}, # fullname -> docname, objtype
  516. }
  517. def clear_doc(self, docname):
  518. to_clear = set()
  519. for fullname, obj in self.data['objects'].items():
  520. if obj.docname == docname:
  521. to_clear.add(fullname)
  522. for fullname in to_clear:
  523. del self.data['objects'][fullname]
  524. def resolve_xref(self, env, fromdocname, builder,
  525. typ, target, node, contnode):
  526. targetid = f'{typ}:{target}'
  527. obj = self.data['objects'].get(targetid)
  528. if obj is None and typ == 'command':
  529. # If 'command(args)' wasn't found, try just 'command'.
  530. # TODO: remove this fallback? warn?
  531. # logger.warning(f'no match for {targetid}')
  532. command = target.split('(')[0]
  533. targetid = f'{typ}:{command}'
  534. obj = self.data['objects'].get(targetid)
  535. if obj is None:
  536. # TODO: warn somehow?
  537. return None
  538. return make_refnode(builder, fromdocname, obj.docname, obj.node_id,
  539. contnode, target)
  540. def note_object(self, objtype: str, name: str, target_id: str,
  541. node_id: str, location: Any = None):
  542. if target_id in self.data['objects']:
  543. other = self.data['objects'][target_id].docname
  544. logger.warning(
  545. f'CMake object {target_id!r} also described in {other!r}',
  546. location=location)
  547. self.data['objects'][target_id] = ObjectEntry(
  548. self.env.docname, objtype, node_id, name)
  549. def get_objects(self):
  550. for refname, obj in self.data['objects'].items():
  551. yield (refname, obj.name, obj.objtype, obj.docname, obj.node_id, 1)
  552. def setup(app):
  553. app.add_directive('cmake-module', CMakeModule)
  554. app.add_transform(CMakeTransform)
  555. app.add_transform(CMakeXRefTransform)
  556. app.add_domain(CMakeDomain)
  557. return {"parallel_read_safe": True}