3 # SPDX-FileCopyrightText: Copyright The SCons Foundation (https://scons.org)
4 # SPDX-License-Identifier: MIT
6 """Module for handling SCons documentation processing.
8 This module parses home-brew XML files that document important SCons
9 components. Currently it handles Builders, Environment functions/methods,
10 Construction Variables, and Tools (further expansion is possible). These
11 documentation snippets are turned into files with content and reference
12 tags that can be included into the manpage and/or user guide, which
13 prevents a lot of duplication.
15 In general, you can use any DocBook tag in the input, and this module
16 just adds processing various home-brew tags to try to make life a
21 <builder name="BUILDER">
23 <para>This is the summary description of an SCons Builder.
24 It will get placed in the man page,
25 and in the appropriate User's Guide appendix.
26 The name of this builder may be interpolated
27 anywhere in the document by specifying the
28 &b-BUILDER; element. A link to this definition may be
29 interpolated by specifying the &b-link-BUILDER; element.
32 Unlike normal XML, blank lines are significant in these
33 descriptions and serve to separate paragraphs.
34 They'll get replaced in DocBook output with appropriate tags
35 to indicate a new paragraph.
38 print("this is example code, it will be offset and indented")
45 <scons_function name="FUNCTION">
46 <arguments signature="SIGTYPE">
47 (arg1, arg2, key=value)
50 <para>This is the summary description of an SCons function.
51 It will get placed in the man page,
52 and in the appropriate User's Guide appendix.
53 If the "signature" attribute is specified, SIGTYPE may be one
54 of "global", "env" or "both" (the default if omitted is "both"),
55 to indicate the signature applies to the global form or the
56 environment form, or to generate both with the same signature
57 (excepting the insertion of "env.").
58 This allows for the cases of
59 describing that only one signature should be generated,
60 or both signatures should be generated and they differ,
61 or both signatures should be generated and they are the same.
62 The name of this function may be interpolated
63 anywhere in the document by specifying the
64 &f-FUNCTION; element or the &f-env-FUNCTION; element.
65 Links to this definition may be interpolated by specifying
66 the &f-link-FUNCTION: or &f-link-env-FUNCTION; element.
70 print("this is example code, it will be offset and indented")
75 Construction variable example:
77 <cvar name="VARIABLE">
79 <para>This is the summary description of a construction variable.
80 It will get placed in the man page,
81 and in the appropriate User's Guide appendix.
82 The name of this construction variable may be interpolated
83 anywhere in the document by specifying the
84 &cv-VARIABLE; element. A link to this definition may be
85 interpolated by specifying the &cv-link-VARIABLE; element.
89 print("this is example code, it will be offset and indented")
98 <para>This is the summary description of an SCons Tool.
99 It will get placed in the man page,
100 and in the appropriate User's Guide appendix.
101 The name of this tool may be interpolated
102 anywhere in the document by specifying the
103 &t-TOOL; element. A link to this definition may be
104 interpolated by specifying the &t-link-TOOL; element.
108 print("this is example code, it will be offset and indented")
121 from lxml
import etree
124 import xml
.etree
.ElementTree
as etree
126 raise ImportError("Failed to import ElementTree from any known place")
128 # patterns to help trim XML passed in as strings
129 re_entity
= re
.compile(r
"&([^;]+);")
130 re_entity_header
= re
.compile(r
"<!DOCTYPE\s+sconsdoc\s+[^\]]+\]>")
132 # Namespace for the SCons Docbook XSD
133 dbxsd
= "http://www.scons.org/dbxsd/v1.0"
134 # Namsespace pattern to help identify an scons-xml file read as bytes
135 dbxsdpat
= b
'xmlns="%s"' % dbxsd
.encode('utf-8')
136 # Namespace map identifier for the SCons Docbook XSD
138 # Namespace for schema instances
139 xsi
= "http://www.w3.org/2001/XMLSchema-instance"
141 # Header comment with copyright (unused at present)
142 copyright_comment
= """
143 SPDX-FileCopyrightText: Copyright The SCons Foundation (https://scons.org)
144 SPDX-License-Identifier: MIT
145 SPDX-FileType: DOCUMENTATION
147 This file is processed by the bin/SConsDoc.py module.
150 def isSConsXml(fpath
):
151 """ Check whether the given file is an SCons XML file.
153 It is SCons XML if it contains the default target namespace definition
154 described by dbxsdpat
158 with
open(fpath
, 'rb') as f
:
160 if content
.find(dbxsdpat
) >= 0:
167 def remove_entities(content
):
168 # Cut out entity inclusions
169 content
= re_entity_header
.sub("", content
, re
.M
)
170 # Cut out entities themselves
171 content
= re_entity
.sub(lambda match
: match
.group(1), content
)
175 default_xsd
= os
.path
.join('doc', 'xsd', 'scons.xsd')
180 class Libxml2ValidityHandler
:
186 def error(self
, msg
, data
):
188 raise Exception("Error handler did not receive correct argument")
189 self
.errors
.append(msg
)
191 def warning(self
, msg
, data
):
193 raise Exception("Warning handler did not receive correct argument")
194 self
.warnings
.append(msg
)
198 def __init__(self
, name_
, uri_
):
202 def getEntityString(self
):
203 txt
= """ <!ENTITY %(perc)s %(name)s SYSTEM "%(uri)s">
205 """ % {'perc': perc
, 'name': self
.name
, 'uri': self
.uri
}
210 class DoctypeDeclaration
:
211 def __init__(self
, name_
=None):
214 if self
.name
is None:
215 # Add default entries
216 self
.name
= "sconsdoc"
217 self
.addEntity("scons", "../scons.mod")
218 self
.addEntity("builders-mod", "builders.mod")
219 self
.addEntity("functions-mod", "functions.mod")
220 self
.addEntity("tools-mod", "tools.mod")
221 self
.addEntity("variables-mod", "variables.mod")
223 def addEntity(self
, name
, uri
):
224 self
.entries
.append(DoctypeEntity(name
, uri
))
226 def createDoctype(self
):
227 content
= '<!DOCTYPE %s [\n' % self
.name
228 for e
in self
.entries
:
229 content
+= e
.getEntityString()
239 def newNode(tag
, **kwargs
):
240 return etree
.Element(tag
, **kwargs
)
243 def newSubNode(parent
, tag
, **kwargs
):
244 return etree
.SubElement(parent
, tag
, **kwargs
)
247 def newEtreeNode(tag
, init_ns
=False, **kwargs
):
249 NSMAP
= {None: dbxsd
,
251 return etree
.Element(tag
, nsmap
=NSMAP
, **kwargs
)
253 return etree
.Element(tag
, **kwargs
)
257 return copy
.deepcopy(node
)
260 def appendNode(parent
, child
):
264 def hasAttribute(node
, att
):
265 return att
in node
.attrib
268 def getAttribute(node
, att
):
269 return node
.attrib
[att
]
272 def setAttribute(node
, att
, value
):
273 node
.attrib
[att
] = value
280 def appendCvLink(root
, key
, lntail
):
281 linknode
= etree
.Entity('cv-link-' + key
)
282 linknode
.tail
= lntail
283 root
.append(linknode
)
286 def setText(root
, txt
):
294 def setTail(root
, txt
):
298 def writeGenTree(root
, fp
):
299 dt
= DoctypeDeclaration()
300 fp
.write(etree
.tostring(root
, encoding
="utf-8",
302 doctype
=dt
.createDoctype()).decode('utf-8'))
305 def writeTree(root
, fpath
):
306 with
open(fpath
, 'wb') as fp
:
307 fp
.write(etree
.tostring(root
, encoding
="utf-8",
311 def prettyPrintFile(fpath
):
312 with
open(fpath
,'rb') as fin
:
313 tree
= etree
.parse(fin
)
314 pretty_content
= etree
.tostring(tree
, encoding
="utf-8",
317 with
open(fpath
,'wb') as fout
:
318 fout
.write(pretty_content
)
321 def decorateWithHeader(root
):
322 root
.attrib
["{"+xsi
+"}schemaLocation"] = "%s %s/scons.xsd" % (dbxsd
, dbxsd
)
325 def newXmlTree(self
, root
):
326 """ Return a XML file tree with the correct namespaces set,
327 the element root as top entry and the given header comment.
329 NSMAP
= {None: dbxsd
, 'xsi' : xsi
}
330 t
= etree
.Element(root
, nsmap
=NSMAP
)
331 return self
.decorateWithHeader(t
)
333 # singleton to cache parsed xmlschema..
337 def validateXml(fpath
, xmlschema_context
):
339 if TreeFactory
.xmlschema
is None:
340 TreeFactory
.xmlschema
= etree
.XMLSchema(xmlschema_context
)
342 doc
= etree
.parse(fpath
)
343 except Exception as e
:
344 print("ERROR: %s fails to parse:"%fpath
)
349 TreeFactory
.xmlschema
.assertValid(doc
)
350 except etree
.XMLSchemaValidateError
as e
:
351 print("ERROR: %s fails to validate:" % fpath
)
353 print(e
.error_log
.last_error
.message
)
354 print("In file: [%s]" % e
.error_log
.last_error
.filename
)
355 print("Line : %d" % e
.error_log
.last_error
.line
)
358 except Exception as e
:
359 print("ERROR: %s fails to validate:" % fpath
)
366 def findAll(root
, tag
, ns
=None, xp_ctxt
=None, nsmap
=None):
367 expression
= ".//{%s}%s" % (nsmap
[ns
], tag
)
368 if not ns
or not nsmap
:
369 expression
= ".//%s" % tag
370 return root
.findall(expression
)
373 def findAllChildrenOf(root
, tag
, ns
=None, xp_ctxt
=None, nsmap
=None):
374 expression
= "./{%s}%s/*" % (nsmap
[ns
], tag
)
375 if not ns
or not nsmap
:
376 expression
= "./%s/*" % tag
377 return root
.findall(expression
)
380 def convertElementTree(root
):
381 """ Convert the given tree of etree.Element
382 entries to a list of tree nodes for the
392 self
.nsmap
= {'dbx': dbxsd
}
395 self
.xpath_context
= None
397 def parseContent(self
, content
, include_entities
=True):
398 """ Parses the given text content as XML
400 This is the setup portion, called from parseContent in
401 an SConsDocHandler instance - see the notes there.
403 if not include_entities
:
404 content
= remove_entities(content
)
405 # Create domtree from given content string
406 self
.root
= etree
.fromstring(content
)
408 def parseXmlFile(self
, fpath
):
409 # Create domtree from file
410 parser
= etree
.XMLParser(load_dtd
=True, resolve_entities
=False)
411 domtree
= etree
.parse(fpath
, parser
)
412 self
.root
= domtree
.getroot()
415 if self
.doc
is not None:
417 if self
.xpath_context
is not None:
418 self
.xpath_context
.xpathFreeContext()
422 def validate_all_xml(dpaths
, xsdfile
=default_xsd
):
423 xmlschema_context
= etree
.parse(xsdfile
)
427 if dp
.endswith('.xml') and isSConsXml(dp
):
431 for path
, dirs
, files
in os
.walk(dp
):
433 if f
.endswith('.xml'):
434 fp
= os
.path
.join(path
, f
)
439 fpaths
= sorted(fpaths
)
440 for idx
, fp
in enumerate(fpaths
):
441 fpath
= os
.path
.join(path
, fp
)
442 print("%.2f%s (%d/%d) %s" % (float(idx
+ 1) * 100.0 /float(len(fpaths
)),
443 perc
, idx
+ 1, len(fpaths
), fp
))
445 if not tf
.validateXml(fp
, xmlschema_context
):
456 def __init__(self
, name
):
458 self
.sort_name
= name
.lower()
459 if self
.sort_name
[0] == '_':
460 self
.sort_name
= self
.sort_name
[1:]
464 self
.arguments
= None
465 def cmp_name(self
, name
):
469 def __eq__(self
, other
):
470 return self
.sort_name
== other
.sort_name
471 def __lt__(self
, other
):
472 return self
.sort_name
< other
.sort_name
479 class Function(Item
):
484 def __init__(self
, name
):
485 super().__init
__(name
)
486 self
.entity
= self
.name
.replace('+', 'X')
489 class ConstructionVariable(Item
):
494 def __init__(self
, signature
, body
=None):
498 self
.signature
= signature
500 s
= ''.join(self
.body
).strip()
502 for m
in re
.findall(r
'([a-zA-Z/_]+|[^a-zA-Z/_]+)', s
):
506 return ' '.join(result
)
507 def append(self
, data
):
508 self
.body
.append(data
)
511 class SConsDocHandler
:
518 def parseItems(self
, domelem
, xpath_context
, nsmap
):
521 for i
in tf
.findAll(domelem
, "item", dbxid
, xpath_context
, nsmap
):
526 items
.append(txt
.strip())
530 def parseUsesSets(self
, domelem
, xpath_context
, nsmap
):
534 for u
in tf
.findAll(domelem
, "uses", dbxid
, xpath_context
, nsmap
):
535 uses
.extend(self
.parseItems(u
, xpath_context
, nsmap
))
536 for s
in tf
.findAll(domelem
, "sets", dbxid
, xpath_context
, nsmap
):
537 sets
.extend(self
.parseItems(s
, xpath_context
, nsmap
))
539 return sorted(uses
), sorted(sets
)
541 def parseInstance(self
, domelem
, map, Class
,
542 xpath_context
, nsmap
, include_entities
=True):
544 if tf
.hasAttribute(domelem
, 'name'):
545 name
= tf
.getAttribute(domelem
, 'name')
549 instance
= Class(name
)
551 uses
, sets
= self
.parseUsesSets(domelem
, xpath_context
, nsmap
)
552 instance
.uses
.extend(uses
)
553 instance
.sets
.extend(sets
)
555 # Parse summary and function arguments
556 for s
in tf
.findAllChildrenOf(domelem
, "summary", dbxid
, xpath_context
, nsmap
):
557 if instance
.summary
is None:
558 instance
.summary
= []
559 instance
.summary
.append(tf
.copyNode(s
))
560 for a
in tf
.findAll(domelem
, "arguments", dbxid
, xpath_context
, nsmap
):
561 if instance
.arguments
is None:
562 instance
.arguments
= []
563 instance
.arguments
.append(tf
.copyNode(a
))
565 def parseDomtree(self
, root
, xpath_context
=None, nsmap
=None, include_entities
=True):
567 for b
in tf
.findAll(root
, "builder", dbxid
, xpath_context
, nsmap
):
568 self
.parseInstance(b
, self
.builders
, Builder
,
569 xpath_context
, nsmap
, include_entities
)
571 for f
in tf
.findAll(root
, "scons_function", dbxid
, xpath_context
, nsmap
):
572 self
.parseInstance(f
, self
.functions
, Function
,
573 xpath_context
, nsmap
, include_entities
)
575 for t
in tf
.findAll(root
, "tool", dbxid
, xpath_context
, nsmap
):
576 self
.parseInstance(t
, self
.tools
, Tool
,
577 xpath_context
, nsmap
, include_entities
)
579 for c
in tf
.findAll(root
, "cvar", dbxid
, xpath_context
, nsmap
):
580 self
.parseInstance(c
, self
.cvars
, ConstructionVariable
,
581 xpath_context
, nsmap
, include_entities
)
583 def parseContent(self
, content
, include_entities
=True):
584 """Parse the given content as XML.
586 This method is used when we generate the basic lists of entities
587 for the builders, tools and functions. So we usually don't
588 bother about namespaces and resolving entities here...
589 this is handled in parseXmlFile below (step 2 of the overall process).
593 t
.parseContent(content
, include_entities
)
595 self
.parseDomtree(t
.root
, t
.xpath_context
, t
.nsmap
, include_entities
)
597 def parseXmlFile(self
, fpath
):
600 t
.parseXmlFile(fpath
)
602 self
.parseDomtree(t
.root
, t
.xpath_context
, t
.nsmap
)
604 def importfile(path
):
605 """Import a Python source file or compiled file given its path."""
606 from importlib
.util
import MAGIC_NUMBER
607 with
open(path
, 'rb') as ifp
:
608 is_bytecode
= MAGIC_NUMBER
== ifp
.read(len(MAGIC_NUMBER
))
609 filename
= os
.path
.basename(path
)
610 name
, ext
= os
.path
.splitext(filename
)
612 loader
= importlib
._bootstrap
_external
.SourcelessFileLoader(name
, path
)
614 loader
= importlib
._bootstrap
_external
.SourceFileLoader(name
, path
)
615 # XXX We probably don't need to pass in the loader here.
616 spec
= importlib
.util
.spec_from_file_location(name
, path
, loader
=loader
)
618 return importlib
._bootstrap
._load
(spec
)
620 raise Exception(path
, sys
.exc_info())
624 # indent-tabs-mode:nil
626 # vim: set expandtab tabstop=4 shiftwidth=4: