diff options
Diffstat (limited to 'bin/SConsDoc.py')
| -rw-r--r-- | bin/SConsDoc.py | 823 |
1 files changed, 823 insertions, 0 deletions
diff --git a/bin/SConsDoc.py b/bin/SConsDoc.py new file mode 100644 index 0000000..b42e994 --- /dev/null +++ b/bin/SConsDoc.py @@ -0,0 +1,823 @@ +#!/usr/bin/env python +# +# Copyright (c) 2010 The SCons Foundation +# +# Permission is hereby granted, free of charge, to any person obtaining +# a copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to +# permit persons to whom the Software is furnished to do so, subject to +# the following conditions: +# +# The above copyright notice and this permission notice shall be included +# in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY +# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE +# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +# +# +# Module for handling SCons documentation processing. +# + +__doc__ = """ +This module parses home-brew XML files that document various things +in SCons. Right now, it handles Builders, functions, construction +variables, and Tools, but we expect it to get extended in the future. + +In general, you can use any DocBook tag in the input, and this module +just adds processing various home-brew tags to try to make life a +little easier. + +Builder example: + + <builder name="BUILDER"> + <summary> + <para>This is the summary description of an SCons Builder. + It will get placed in the man page, + and in the appropriate User's Guide appendix. + The name of any builder may be interpolated + anywhere in the document by specifying the + &b-BUILDER; element. It need not be on a line by itself.</para> + + Unlike normal XML, blank lines are significant in these + descriptions and serve to separate paragraphs. + They'll get replaced in DocBook output with appropriate tags + to indicate a new paragraph. + + <example> + print "this is example code, it will be offset and indented" + </example> + </summary> + </builder> + +Function example: + + <scons_function name="FUNCTION"> + <arguments> + (arg1, arg2, key=value) + </arguments> + <summary> + <para>This is the summary description of an SCons function. + It will get placed in the man page, + and in the appropriate User's Guide appendix. + The name of any builder may be interpolated + anywhere in the document by specifying the + &f-FUNCTION; element. It need not be on a line by itself.</para> + + <example> + print "this is example code, it will be offset and indented" + </example> + </summary> + </scons_function> + +Construction variable example: + + <cvar name="VARIABLE"> + <summary> + <para>This is the summary description of a construction variable. + It will get placed in the man page, + and in the appropriate User's Guide appendix. + The name of any construction variable may be interpolated + anywhere in the document by specifying the + &t-VARIABLE; element. It need not be on a line by itself.</para> + + <example> + print "this is example code, it will be offset and indented" + </example> + </summary> + </cvar> + +Tool example: + + <tool name="TOOL"> + <summary> + <para>This is the summary description of an SCons Tool. + It will get placed in the man page, + and in the appropriate User's Guide appendix. + The name of any tool may be interpolated + anywhere in the document by specifying the + &t-TOOL; element. It need not be on a line by itself.</para> + + <example> + print "this is example code, it will be offset and indented" + </example> + </summary> + </tool> +""" + +import imp +import os.path +import re +import sys +import copy + +# Do we have libxml2/libxslt/lxml? +has_libxml2 = True +try: + import libxml2 + import libxslt +except: + has_libxml2 = False + try: + import lxml + except: + print("Failed to import either libxml2/libxslt or lxml") + sys.exit(1) + +has_etree = False +if not has_libxml2: + try: + from lxml import etree + has_etree = True + except ImportError: + pass +if not has_etree: + try: + # Python 2.5 + import xml.etree.cElementTree as etree + except ImportError: + try: + # Python 2.5 + import xml.etree.ElementTree as etree + except ImportError: + try: + # normal cElementTree install + import cElementTree as etree + except ImportError: + try: + # normal ElementTree install + import elementtree.ElementTree as etree + except ImportError: + print("Failed to import ElementTree from any known place") + sys.exit(1) + +re_entity = re.compile("\&([^;]+);") +re_entity_header = re.compile("<!DOCTYPE\s+sconsdoc\s+[^\]]+\]>") + +# Namespace for the SCons Docbook XSD +dbxsd="http://www.scons.org/dbxsd/v1.0" +# Namespace map identifier for the SCons Docbook XSD +dbxid="dbx" +# Namespace for schema instances +xsi = "http://www.w3.org/2001/XMLSchema-instance" + +# Header comment with copyright +copyright_comment = """ +Copyright (c) 2001 - 2014 The SCons Foundation + +This file is processed by the bin/SConsDoc.py module. +See its __doc__ string for a discussion of the format. +""" + +def isSConsXml(fpath): + """ Check whether the given file is a SCons XML file, i.e. it + contains the default target namespace definition. + """ + try: + f = open(fpath,'r') + content = f.read() + f.close() + if content.find('xmlns="%s"' % dbxsd) >= 0: + return True + except: + pass + + return False + +def remove_entities(content): + # Cut out entity inclusions + content = re_entity_header.sub("", content, re.M) + # Cut out entities themselves + content = re_entity.sub(lambda match: match.group(1), content) + + return content + +default_xsd = os.path.join('doc','xsd','scons.xsd') + +ARG = "dbscons" + +class Libxml2ValidityHandler: + + def __init__(self): + self.errors = [] + self.warnings = [] + + def error(self, msg, data): + if data != ARG: + raise Exception, "Error handler did not receive correct argument" + self.errors.append(msg) + + def warning(self, msg, data): + if data != ARG: + raise Exception, "Warning handler did not receive correct argument" + self.warnings.append(msg) + + +class DoctypeEntity: + def __init__(self, name_, uri_): + self.name = name_ + self.uri = uri_ + + def getEntityString(self): + txt = """ <!ENTITY %(perc)s %(name)s SYSTEM "%(uri)s"> + %(perc)s%(name)s; +""" % {'perc' : perc, 'name' : self.name, 'uri' : self.uri} + + return txt + +class DoctypeDeclaration: + def __init__(self, name_=None): + self.name = name_ + self.entries = [] + if self.name is None: + # Add default entries + self.name = "sconsdoc" + self.addEntity("scons", "../scons.mod") + self.addEntity("builders-mod", "builders.mod") + self.addEntity("functions-mod", "functions.mod") + self.addEntity("tools-mod", "tools.mod") + self.addEntity("variables-mod", "variables.mod") + + def addEntity(self, name, uri): + self.entries.append(DoctypeEntity(name, uri)) + + def createDoctype(self): + content = '<!DOCTYPE %s [\n' % self.name + for e in self.entries: + content += e.getEntityString() + content += ']>\n' + + return content + +if not has_libxml2: + class TreeFactory: + def __init__(self): + pass + + def newNode(self, tag): + return etree.Element(tag) + + def newEtreeNode(self, tag, init_ns=False): + if init_ns: + NSMAP = {None: dbxsd, + 'xsi' : xsi} + return etree.Element(tag, nsmap=NSMAP) + + return etree.Element(tag) + + def copyNode(self, node): + return copy.deepcopy(node) + + def appendNode(self, parent, child): + parent.append(child) + + def hasAttribute(self, node, att): + return att in node.attrib + + def getAttribute(self, node, att): + return node.attrib[att] + + def setAttribute(self, node, att, value): + node.attrib[att] = value + + def getText(self, root): + return root.text + + def setText(self, root, txt): + root.text = txt + + def writeGenTree(self, root, fp): + dt = DoctypeDeclaration() + fp.write(etree.tostring(root, xml_declaration=True, + encoding="UTF-8", pretty_print=True, + doctype=dt.createDoctype())) + + def writeTree(self, root, fpath): + fp = open(fpath, 'w') + fp.write(etree.tostring(root, xml_declaration=True, + encoding="UTF-8", pretty_print=True)) + fp.close() + + def prettyPrintFile(self, fpath): + fin = open(fpath,'r') + tree = etree.parse(fin) + pretty_content = etree.tostring(tree, pretty_print=True) + fin.close() + + fout = open(fpath,'w') + fout.write(pretty_content) + fout.close() + + def decorateWithHeader(self, root): + root.attrib["{"+xsi+"}schemaLocation"] = "%s %s/scons.xsd" % (dbxsd, dbxsd) + return root + + def newXmlTree(self, root): + """ Return a XML file tree with the correct namespaces set, + the element root as top entry and the given header comment. + """ + NSMAP = {None: dbxsd, + 'xsi' : xsi} + t = etree.Element(root, nsmap=NSMAP) + return self.decorateWithHeader(t) + + def validateXml(self, fpath, xmlschema_context): + # Use lxml + xmlschema = etree.XMLSchema(xmlschema_context) + try: + doc = etree.parse(fpath) + except Exception, e: + print "ERROR: %s fails to parse:"%fpath + print e + return False + doc.xinclude() + try: + xmlschema.assertValid(doc) + except Exception, e: + print "ERROR: %s fails to validate:" % fpath + print e + return False + return True + + def findAll(self, root, tag, ns=None, xp_ctxt=None, nsmap=None): + expression = ".//{%s}%s" % (nsmap[ns], tag) + if not ns or not nsmap: + expression = ".//%s" % tag + return root.findall(expression) + + def findAllChildrenOf(self, root, tag, ns=None, xp_ctxt=None, nsmap=None): + expression = "./{%s}%s/*" % (nsmap[ns], tag) + if not ns or not nsmap: + expression = "./%s/*" % tag + return root.findall(expression) + + def convertElementTree(self, root): + """ Convert the given tree of etree.Element + entries to a list of tree nodes for the + current XML toolkit. + """ + return [root] + +else: + class TreeFactory: + def __init__(self): + pass + + def newNode(self, tag): + return libxml2.newNode(tag) + + def newEtreeNode(self, tag, init_ns=False): + return etree.Element(tag) + + def copyNode(self, node): + return node.copyNode(1) + + def appendNode(self, parent, child): + if hasattr(parent, 'addChild'): + parent.addChild(child) + else: + parent.append(child) + + def hasAttribute(self, node, att): + if hasattr(node, 'hasProp'): + return node.hasProp(att) + return att in node.attrib + + def getAttribute(self, node, att): + if hasattr(node, 'prop'): + return node.prop(att) + return node.attrib[att] + + def setAttribute(self, node, att, value): + if hasattr(node, 'setProp'): + node.setProp(att, value) + else: + node.attrib[att] = value + + def getText(self, root): + if hasattr(root, 'getContent'): + return root.getContent() + return root.text + + def setText(self, root, txt): + if hasattr(root, 'setContent'): + root.setContent(txt) + else: + root.text = txt + + def writeGenTree(self, root, fp): + doc = libxml2.newDoc('1.0') + dtd = doc.newDtd("sconsdoc", None, None) + doc.addChild(dtd) + doc.setRootElement(root) + content = doc.serialize("UTF-8", 1) + dt = DoctypeDeclaration() + # This is clearly a hack, but unfortunately libxml2 + # doesn't support writing PERs (Parsed Entity References). + # So, we simply replace the empty doctype with the + # text we need... + content = content.replace("<!DOCTYPE sconsdoc>", dt.createDoctype()) + fp.write(content) + doc.freeDoc() + + def writeTree(self, root, fpath): + fp = open(fpath, 'w') + doc = libxml2.newDoc('1.0') + doc.setRootElement(root) + fp.write(doc.serialize("UTF-8", 1)) + doc.freeDoc() + fp.close() + + def prettyPrintFile(self, fpath): + # Read file and resolve entities + doc = libxml2.readFile(fpath, None, libxml2d.XML_PARSE_NOENT) + fp = open(fpath, 'w') + # Prettyprint + fp.write(doc.serialize("UTF-8", 1)) + fp.close() + # Cleanup + doc.freeDoc() + + def decorateWithHeader(self, root): + # Register the namespaces + ns = root.newNs(dbxsd, None) + xi = root.newNs(xsi, 'xsi') + root.setNs(ns) #put this node in the target namespace + + root.setNsProp(xi, 'schemaLocation', "%s %s/scons.xsd" % (dbxsd, dbxsd)) + + return root + + def newXmlTree(self, root): + """ Return a XML file tree with the correct namespaces set, + the element root as top entry and the given header comment. + """ + t = libxml2.newNode(root) + return self.decorateWithHeader(t) + + def validateXml(self, fpath, xmlschema_context): + # Create validation context + validation_context = xmlschema_context.schemaNewValidCtxt() + # Set error/warning handlers + eh = Libxml2ValidityHandler() + validation_context.setValidityErrorHandler(eh.error, eh.warning, ARG) + # Read file and resolve entities + doc = libxml2.readFile(fpath, None, libxml2.XML_PARSE_NOENT) + doc.xincludeProcessFlags(libxml2.XML_PARSE_NOENT) + err = validation_context.schemaValidateDoc(doc) + # Cleanup + doc.freeDoc() + del validation_context + + if err or eh.errors: + for e in eh.errors: + print e.rstrip("\n") + print "%s fails to validate" % fpath + return False + + return True + + def findAll(self, root, tag, ns=None, xpath_context=None, nsmap=None): + if hasattr(root, 'xpathEval') and xpath_context: + # Use the xpath context + xpath_context.setContextNode(root) + expression = ".//%s" % tag + if ns: + expression = ".//%s:%s" % (ns, tag) + return xpath_context.xpathEval(expression) + else: + expression = ".//{%s}%s" % (nsmap[ns], tag) + if not ns or not nsmap: + expression = ".//%s" % tag + return root.findall(expression) + + def findAllChildrenOf(self, root, tag, ns=None, xpath_context=None, nsmap=None): + if hasattr(root, 'xpathEval') and xpath_context: + # Use the xpath context + xpath_context.setContextNode(root) + expression = "./%s/node()" % tag + if ns: + expression = "./%s:%s/node()" % (ns, tag) + + return xpath_context.xpathEval(expression) + else: + expression = "./{%s}%s/node()" % (nsmap[ns], tag) + if not ns or not nsmap: + expression = "./%s/node()" % tag + return root.findall(expression) + + def expandChildElements(self, child): + """ Helper function for convertElementTree, + converts a single child recursively. + """ + nchild = self.newNode(child.tag) + # Copy attributes + for key, val in child.attrib: + self.setAttribute(nchild, key, val) + elements = [] + # Add text + if child.text: + t = libxml2.newText(child.text) + self.appendNode(nchild, t) + # Add children + for c in child: + for n in self.expandChildElements(c): + self.appendNode(nchild, n) + elements.append(nchild) + # Add tail + if child.tail: + tail = libxml2.newText(child.tail) + elements.append(tail) + + return elements + + def convertElementTree(self, root): + """ Convert the given tree of etree.Element + entries to a list of tree nodes for the + current XML toolkit. + """ + nroot = self.newNode(root.tag) + # Copy attributes + for key, val in root.attrib: + self.setAttribute(nroot, key, val) + elements = [] + # Add text + if root.text: + t = libxml2.newText(root.text) + self.appendNode(nroot, t) + # Add children + for c in root: + for n in self.expandChildElements(c): + self.appendNode(nroot, n) + elements.append(nroot) + # Add tail + if root.tail: + tail = libxml2.newText(root.tail) + elements.append(tail) + + return elements + +tf = TreeFactory() + + +class SConsDocTree: + def __init__(self): + self.nsmap = {'dbx' : dbxsd} + self.doc = None + self.root = None + self.xpath_context = None + + def parseContent(self, content, include_entities=True): + """ Parses the given content as XML file. This method + is used when we generate the basic lists of entities + for the builders, tools and functions. + So we usually don't bother about namespaces and resolving + entities here...this is handled in parseXmlFile below + (step 2 of the overall process). + """ + if not include_entities: + content = remove_entities(content) + # Create domtree from given content string + self.root = etree.fromstring(content) + + def parseXmlFile(self, fpath): + nsmap = {'dbx' : dbxsd} + if not has_libxml2: + # Create domtree from file + domtree = etree.parse(fpath) + self.root = domtree.getroot() + else: + # Read file and resolve entities + self.doc = libxml2.readFile(fpath, None, libxml2.XML_PARSE_NOENT) + self.root = self.doc.getRootElement() + # Create xpath context + self.xpath_context = self.doc.xpathNewContext() + # Register namespaces + for key, val in self.nsmap.iteritems(): + self.xpath_context.xpathRegisterNs(key, val) + + def __del__(self): + if self.doc is not None: + self.doc.freeDoc() + if self.xpath_context is not None: + self.xpath_context.xpathFreeContext() + +perc="%" + +def validate_all_xml(dpaths, xsdfile=default_xsd): + xmlschema_context = None + if not has_libxml2: + # Use lxml + xmlschema_context = etree.parse(xsdfile) + else: + # Use libxml2 and prepare the schema validation context + ctxt = libxml2.schemaNewParserCtxt(xsdfile) + xmlschema_context = ctxt.schemaParse() + del ctxt + + fpaths = [] + for dp in dpaths: + if dp.endswith('.xml') and isSConsXml(dp): + path='.' + fpaths.append(dp) + else: + for path, dirs, files in os.walk(dp): + for f in files: + if f.endswith('.xml'): + fp = os.path.join(path, f) + if isSConsXml(fp): + fpaths.append(fp) + + fails = [] + for idx, fp in enumerate(fpaths): + fpath = os.path.join(path, fp) + print "%.2f%s (%d/%d) %s" % (float(idx+1)*100.0/float(len(fpaths)), + perc, idx+1, len(fpaths),fp) + + if not tf.validateXml(fp, xmlschema_context): + fails.append(fp) + continue + + if has_libxml2: + # Cleanup + del xmlschema_context + + if fails: + return False + + return True + +class Item(object): + def __init__(self, name): + self.name = name + self.sort_name = name.lower() + if self.sort_name[0] == '_': + self.sort_name = self.sort_name[1:] + self.sets = [] + self.uses = [] + self.summary = None + self.arguments = None + def cmp_name(self, name): + if name[0] == '_': + name = name[1:] + return name.lower() + def __cmp__(self, other): + return cmp(self.sort_name, other.sort_name) + +class Builder(Item): + pass + +class Function(Item): + def __init__(self, name): + super(Function, self).__init__(name) + +class Tool(Item): + def __init__(self, name): + Item.__init__(self, name) + self.entity = self.name.replace('+', 'X') + +class ConstructionVariable(Item): + pass + +class Arguments(object): + def __init__(self, signature, body=None): + if not body: + body = [] + self.body = body + self.signature = signature + def __str__(self): + s = ''.join(self.body).strip() + result = [] + for m in re.findall('([a-zA-Z/_]+|[^a-zA-Z/_]+)', s): + if ' ' in m: + m = '"%s"' % m + result.append(m) + return ' '.join(result) + def append(self, data): + self.body.append(data) + +class SConsDocHandler(object): + def __init__(self): + self.builders = {} + self.functions = {} + self.tools = {} + self.cvars = {} + + def parseItems(self, domelem, xpath_context, nsmap): + items = [] + + for i in tf.findAll(domelem, "item", dbxid, xpath_context, nsmap): + txt = tf.getText(i) + if txt is not None: + txt = txt.strip() + if len(txt): + items.append(txt.strip()) + + return items + + def parseUsesSets(self, domelem, xpath_context, nsmap): + uses = [] + sets = [] + + for u in tf.findAll(domelem, "uses", dbxid, xpath_context, nsmap): + uses.extend(self.parseItems(u, xpath_context, nsmap)) + for s in tf.findAll(domelem, "sets", dbxid, xpath_context, nsmap): + sets.extend(self.parseItems(s, xpath_context, nsmap)) + + return sorted(uses), sorted(sets) + + def parseInstance(self, domelem, map, Class, + xpath_context, nsmap, include_entities=True): + name = 'unknown' + if tf.hasAttribute(domelem, 'name'): + name = tf.getAttribute(domelem, 'name') + try: + instance = map[name] + except KeyError: + instance = Class(name) + map[name] = instance + uses, sets = self.parseUsesSets(domelem, xpath_context, nsmap) + instance.uses.extend(uses) + instance.sets.extend(sets) + if include_entities: + # Parse summary and function arguments + for s in tf.findAllChildrenOf(domelem, "summary", dbxid, xpath_context, nsmap): + if instance.summary is None: + instance.summary = [] + instance.summary.append(tf.copyNode(s)) + for a in tf.findAll(domelem, "arguments", dbxid, xpath_context, nsmap): + if instance.arguments is None: + instance.arguments = [] + instance.arguments.append(tf.copyNode(a)) + + def parseDomtree(self, root, xpath_context=None, nsmap=None, include_entities=True): + # Process Builders + for b in tf.findAll(root, "builder", dbxid, xpath_context, nsmap): + self.parseInstance(b, self.builders, Builder, + xpath_context, nsmap, include_entities) + # Process Functions + for f in tf.findAll(root, "scons_function", dbxid, xpath_context, nsmap): + self.parseInstance(f, self.functions, Function, + xpath_context, nsmap, include_entities) + # Process Tools + for t in tf.findAll(root, "tool", dbxid, xpath_context, nsmap): + self.parseInstance(t, self.tools, Tool, + xpath_context, nsmap, include_entities) + # Process CVars + for c in tf.findAll(root, "cvar", dbxid, xpath_context, nsmap): + self.parseInstance(c, self.cvars, ConstructionVariable, + xpath_context, nsmap, include_entities) + + def parseContent(self, content, include_entities=True): + """ Parses the given content as XML file. This method + is used when we generate the basic lists of entities + for the builders, tools and functions. + So we usually don't bother about namespaces and resolving + entities here...this is handled in parseXmlFile below + (step 2 of the overall process). + """ + # Create doctree + t = SConsDocTree() + t.parseContent(content, include_entities) + # Parse it + self.parseDomtree(t.root, t.xpath_context, t.nsmap, include_entities) + + def parseXmlFile(self, fpath): + # Create doctree + t = SConsDocTree() + t.parseXmlFile(fpath) + # Parse it + self.parseDomtree(t.root, t.xpath_context, t.nsmap) + +# lifted from Ka-Ping Yee's way cool pydoc module. +def importfile(path): + """Import a Python source file or compiled file given its path.""" + magic = imp.get_magic() + file = open(path, 'r') + if file.read(len(magic)) == magic: + kind = imp.PY_COMPILED + else: + kind = imp.PY_SOURCE + file.close() + filename = os.path.basename(path) + name, ext = os.path.splitext(filename) + file = open(path, 'r') + try: + module = imp.load_module(name, file, path, (ext, 'r', kind)) + except ImportError, e: + sys.stderr.write("Could not import %s: %s\n" % (path, e)) + return None + file.close() + return module + +# Local Variables: +# tab-width:4 +# indent-tabs-mode:nil +# End: +# vim: set expandtab tabstop=4 shiftwidth=4: |
