python/mozbuild/mozpack/packager/__init__.py

Fri, 16 Jan 2015 18:13:44 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Fri, 16 Jan 2015 18:13:44 +0100
branch
TOR_BUG_9701
changeset 14
925c144e1f1f
permissions
-rw-r--r--

Integrate suggestion from review to improve consistency with existing code.

     1 # This Source Code Form is subject to the terms of the Mozilla Public
     2 # License, v. 2.0. If a copy of the MPL was not distributed with this
     3 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
     5 from mozbuild.preprocessor import Preprocessor
     6 import re
     7 import os
     8 from mozpack.errors import errors
     9 from mozpack.chrome.manifest import (
    10     Manifest,
    11     ManifestChrome,
    12     ManifestInterfaces,
    13     is_manifest,
    14     parse_manifest,
    15 )
    16 import mozpack.path
    17 from collections import deque
    20 class Component(object):
    21     '''
    22     Class that represents a component in a package manifest.
    23     '''
    24     def __init__(self, name, destdir=''):
    25         if name.find(' ') > 0:
    26             errors.fatal('Malformed manifest: space in component name "%s"'
    27                          % component)
    28         self._name = name
    29         self._destdir = destdir
    31     def __repr__(self):
    32         s = self.name
    33         if self.destdir:
    34             s += ' destdir="%s"' % self.destdir
    35         return s
    37     @property
    38     def name(self):
    39         return self._name
    41     @property
    42     def destdir(self):
    43         return self._destdir
    45     @staticmethod
    46     def _triples(lst):
    47         '''
    48         Split [1, 2, 3, 4, 5, 6, 7] into [(1, 2, 3), (4, 5, 6)].
    49         '''
    50         return zip(*[iter(lst)] * 3)
    52     KEY_VALUE_RE = re.compile(r'''
    53         \s*                 # optional whitespace.
    54         ([a-zA-Z0-9_]+)     # key.
    55         \s*=\s*             # optional space around =.
    56         "([^"]*)"           # value without surrounding quotes.
    57         (?:\s+|$)
    58         ''', re.VERBOSE)
    60     @staticmethod
    61     def _split_options(string):
    62         '''
    63         Split 'key1="value1" key2="value2"' into
    64         {'key1':'value1', 'key2':'value2'}.
    66         Returned keys and values are all strings.
    68         Throws ValueError if the input is malformed.
    69         '''
    70         options = {}
    71         splits = Component.KEY_VALUE_RE.split(string)
    72         if len(splits) % 3 != 1:
    73             # This should never happen -- we expect to always split
    74             # into ['', ('key', 'val', '')*].
    75             raise ValueError("Bad input")
    76         if splits[0]:
    77             raise ValueError('Unrecognized input ' + splits[0])
    78         for key, val, no_match in Component._triples(splits[1:]):
    79             if no_match:
    80                 raise ValueError('Unrecognized input ' + no_match)
    81             options[key] = val
    82         return options
    84     @staticmethod
    85     def _split_component_and_options(string):
    86         '''
    87         Split 'name key1="value1" key2="value2"' into
    88         ('name', {'key1':'value1', 'key2':'value2'}).
    90         Returned name, keys and values are all strings.
    92         Raises ValueError if the input is malformed.
    93         '''
    94         splits = string.strip().split(None, 1)
    95         if not splits:
    96             raise ValueError('No component found')
    97         component = splits[0].strip()
    98         if not component:
    99             raise ValueError('No component found')
   100         if not re.match('[a-zA-Z0-9_\-]+$', component):
   101             raise ValueError('Bad component name ' + component)
   102         options = Component._split_options(splits[1]) if len(splits) > 1 else {}
   103         return component, options
   105     @staticmethod
   106     def from_string(string):
   107         '''
   108         Create a component from a string.
   109         '''
   110         try:
   111             name, options = Component._split_component_and_options(string)
   112         except ValueError as e:
   113             errors.fatal('Malformed manifest: %s' % e)
   114             return
   115         destdir = options.pop('destdir', '')
   116         if options:
   117             errors.fatal('Malformed manifest: options %s not recognized'
   118                          % options.keys())
   119         return Component(name, destdir=destdir)
   122 class PackageManifestParser(object):
   123     '''
   124     Class for parsing of a package manifest, after preprocessing.
   126     A package manifest is a list of file paths, with some syntaxic sugar:
   127         [] designates a toplevel component. Example: [xpcom]
   128         - in front of a file specifies it to be removed
   129         * wildcard support
   130         ** expands to all files and zero or more directories
   131         ; file comment
   133     The parser takes input from the preprocessor line by line, and pushes
   134     parsed information to a sink object.
   136     The add and remove methods of the sink object are called with the
   137     current Component instance and a path.
   138     '''
   139     def __init__(self, sink):
   140         '''
   141         Initialize the package manifest parser with the given sink.
   142         '''
   143         self._component = Component('')
   144         self._sink = sink
   146     def handle_line(self, str):
   147         '''
   148         Handle a line of input and push the parsed information to the sink
   149         object.
   150         '''
   151         # Remove comments.
   152         str = str.strip()
   153         if not str or str.startswith(';'):
   154             return
   155         if str.startswith('[') and str.endswith(']'):
   156             self._component = Component.from_string(str[1:-1])
   157         elif str.startswith('-'):
   158             str = str[1:]
   159             self._sink.remove(self._component, str)
   160         elif ',' in str:
   161             errors.fatal('Incompatible syntax')
   162         else:
   163             self._sink.add(self._component, str)
   166 class PreprocessorOutputWrapper(object):
   167     '''
   168     File-like helper to handle the preprocessor output and send it to a parser.
   169     The parser's handle_line method is called in the relevant errors.context.
   170     '''
   171     def __init__(self, preprocessor, parser):
   172         self._parser = parser
   173         self._pp = preprocessor
   175     def write(self, str):
   176         file = os.path.normpath(os.path.abspath(self._pp.context['FILE']))
   177         with errors.context(file, self._pp.context['LINE']):
   178             self._parser.handle_line(str)
   181 def preprocess(input, parser, defines={}):
   182     '''
   183     Preprocess the file-like input with the given defines, and send the
   184     preprocessed output line by line to the given parser.
   185     '''
   186     pp = Preprocessor()
   187     pp.context.update(defines)
   188     pp.do_filter('substitution')
   189     pp.out = PreprocessorOutputWrapper(pp, parser)
   190     pp.do_include(input)
   193 def preprocess_manifest(sink, manifest, defines={}):
   194     '''
   195     Preprocess the given file-like manifest with the given defines, and push
   196     the parsed information to a sink. See PackageManifestParser documentation
   197     for more details on the sink.
   198     '''
   199     preprocess(manifest, PackageManifestParser(sink), defines)
   202 class CallDeque(deque):
   203     '''
   204     Queue of function calls to make.
   205     '''
   206     def append(self, function, *args):
   207         deque.append(self, (errors.get_context(), function, args))
   209     def execute(self):
   210         while True:
   211             try:
   212                 context, function, args = self.popleft()
   213             except IndexError:
   214                 return
   215             if context:
   216                 with errors.context(context[0], context[1]):
   217                     function(*args)
   218             else:
   219                 function(*args)
   222 class SimplePackager(object):
   223     '''
   224     Helper used to translate and buffer instructions from the
   225     SimpleManifestSink to a formatter. Formatters expect some information to be
   226     given first that the simple manifest contents can't guarantee before the
   227     end of the input.
   228     '''
   229     def __init__(self, formatter):
   230         self.formatter = formatter
   231         # Queue for formatter.add_interfaces()/add_manifest() calls.
   232         self._queue = CallDeque()
   233         # Queue for formatter.add_manifest() calls for ManifestChrome.
   234         self._chrome_queue = CallDeque()
   235         # Queue for formatter.add() calls.
   236         self._file_queue = CallDeque()
   237         # All manifest paths imported.
   238         self._manifests = set()
   239         # All manifest paths included from some other manifest.
   240         self._included_manifests = set()
   241         self._closed = False
   243     def add(self, path, file):
   244         '''
   245         Add the given BaseFile instance with the given path.
   246         '''
   247         assert not self._closed
   248         if is_manifest(path):
   249             self._add_manifest_file(path, file)
   250         elif path.endswith('.xpt'):
   251             self._queue.append(self.formatter.add_interfaces, path, file)
   252         else:
   253             self._file_queue.append(self.formatter.add, path, file)
   255     def _add_manifest_file(self, path, file):
   256         '''
   257         Add the given BaseFile with manifest file contents with the given path.
   258         '''
   259         self._manifests.add(path)
   260         base = ''
   261         if hasattr(file, 'path'):
   262             # Find the directory the given path is relative to.
   263             b = mozpack.path.normsep(file.path)
   264             if b.endswith('/' + path) or b == path:
   265                 base = os.path.normpath(b[:-len(path)])
   266         for e in parse_manifest(base, path, file.open()):
   267             # ManifestResources need to be given after ManifestChrome, so just
   268             # put all ManifestChrome in a separate queue to make them first.
   269             if isinstance(e, ManifestChrome):
   270                 # e.move(e.base) just returns a clone of the entry.
   271                 self._chrome_queue.append(self.formatter.add_manifest,
   272                                           e.move(e.base))
   273             elif not isinstance(e, (Manifest, ManifestInterfaces)):
   274                 self._queue.append(self.formatter.add_manifest, e.move(e.base))
   275             if isinstance(e, Manifest):
   276                 if e.flags:
   277                     errors.fatal('Flags are not supported on ' +
   278                                  '"manifest" entries')
   279                 self._included_manifests.add(e.path)
   281     def get_bases(self):
   282         '''
   283         Return all paths under which root manifests have been found. Root
   284         manifests are manifests that are included in no other manifest.
   285         '''
   286         return set(mozpack.path.dirname(m)
   287                    for m in self._manifests - self._included_manifests)
   289     def close(self):
   290         '''
   291         Push all instructions to the formatter.
   292         '''
   293         self._closed = True
   294         for base in self.get_bases():
   295             if base:
   296                 self.formatter.add_base(base)
   297         self._chrome_queue.execute()
   298         self._queue.execute()
   299         self._file_queue.execute()
   302 class SimpleManifestSink(object):
   303     '''
   304     Parser sink for "simple" package manifests. Simple package manifests use
   305     the format described in the PackageManifestParser documentation, but don't
   306     support file removals, and require manifests, interfaces and chrome data to
   307     be explicitely listed.
   308     Entries starting with bin/ are searched under bin/ in the FileFinder, but
   309     are packaged without the bin/ prefix.
   310     '''
   311     def __init__(self, finder, formatter):
   312         '''
   313         Initialize the SimpleManifestSink. The given FileFinder is used to
   314         get files matching the patterns given in the manifest. The given
   315         formatter does the packaging job.
   316         '''
   317         self._finder = finder
   318         self.packager = SimplePackager(formatter)
   319         self._closed = False
   320         self._manifests = set()
   322     @staticmethod
   323     def normalize_path(path):
   324         '''
   325         Remove any bin/ prefix.
   326         '''
   327         if mozpack.path.basedir(path, ['bin']) == 'bin':
   328             return mozpack.path.relpath(path, 'bin')
   329         return path
   331     def add(self, component, pattern):
   332         '''
   333         Add files with the given pattern in the given component.
   334         '''
   335         assert not self._closed
   336         added = False
   337         for p, f in self._finder.find(pattern):
   338             added = True
   339             if is_manifest(p):
   340                 self._manifests.add(p)
   341             dest = mozpack.path.join(component.destdir, SimpleManifestSink.normalize_path(p))
   342             self.packager.add(dest, f)
   343         if not added:
   344             errors.error('Missing file(s): %s' % pattern)
   346     def remove(self, component, pattern):
   347         '''
   348         Remove files with the given pattern in the given component.
   349         '''
   350         assert not self._closed
   351         errors.fatal('Removal is unsupported')
   353     def close(self, auto_root_manifest=True):
   354         '''
   355         Add possibly missing bits and push all instructions to the formatter.
   356         '''
   357         if auto_root_manifest:
   358             # Simple package manifests don't contain the root manifests, so
   359             # find and add them.
   360             paths = [mozpack.path.dirname(m) for m in self._manifests]
   361             path = mozpack.path.dirname(mozpack.path.commonprefix(paths))
   362             for p, f in self._finder.find(mozpack.path.join(path,
   363                                           'chrome.manifest')):
   364                 if not p in self._manifests:
   365                     self.packager.add(SimpleManifestSink.normalize_path(p), f)
   366         self.packager.close()

mercurial