from __future__ import absolute_import
from mozbuild.preprocessor import Preprocessor
import re
import os
from mozpack.errors import errors
from mozpack.chrome.manifest import (
Manifest,
ManifestBinaryComponent,
ManifestChrome,
ManifestInterfaces,
is_manifest,
parse_manifest,
)
from mozpack.files import (
ExecutableFile,
)
import mozpack.path as mozpath
from collections import deque
import json
class Component(object):
def __init__(self, name, destdir='', xz_compress=False):
if name.find(' ') > 0:
errors.fatal('Malformed manifest: space in component name "%s"' % name)
self._name = name
self._destdir = destdir
self._xz_compress = xz_compress
def __repr__(self):
s = self.name
if self.destdir:
s += ' destdir="%s"' % self.destdir
if self.xz_compress:
s += ' xz_compress="1"'
return s
@property
def name(self):
return self._name
@property
def destdir(self):
return self._destdir
@property
def xz_compress(self):
return self._xz_compress
@staticmethod
def _triples(lst):
return zip(*[iter(lst)] * 3)
KEY_VALUE_RE = re.compile(r'''
\s* ([a-zA-Z0-9_]+) \s*=\s* "([^"]*)" (?:\s+|$)
''', re.VERBOSE)
@staticmethod
def _split_options(string):
options = {}
splits = Component.KEY_VALUE_RE.split(string)
if len(splits) % 3 != 1:
raise ValueError("Bad input")
if splits[0]:
raise ValueError('Unrecognized input ' + splits[0])
for key, val, no_match in Component._triples(splits[1:]):
if no_match:
raise ValueError('Unrecognized input ' + no_match)
options[key] = val
return options
@staticmethod
def _split_component_and_options(string):
splits = string.strip().split(None, 1)
if not splits:
raise ValueError('No component found')
component = splits[0].strip()
if not component:
raise ValueError('No component found')
if not re.match('[a-zA-Z0-9_\-]+$', component):
raise ValueError('Bad component name ' + component)
options = Component._split_options(splits[1]) if len(splits) > 1 else {}
return component, options
@staticmethod
def from_string(string):
try:
name, options = Component._split_component_and_options(string)
except ValueError as e:
errors.fatal('Malformed manifest: %s' % e)
return
destdir = options.pop('destdir', '')
xz_compress = options.pop('xz_compress', '0') != '0'
if options:
errors.fatal('Malformed manifest: options %s not recognized'
% options.keys())
return Component(name, destdir=destdir, xz_compress=xz_compress)
class PackageManifestParser(object):
def __init__(self, sink):
self._component = Component('')
self._sink = sink
def handle_line(self, str):
str = str.strip()
if not str or str.startswith(';'):
return
if str.startswith('[') and str.endswith(']'):
self._component = Component.from_string(str[1:-1])
elif str.startswith('-'):
str = str[1:]
self._sink.remove(self._component, str)
elif ',' in str:
errors.fatal('Incompatible syntax')
else:
self._sink.add(self._component, str)
class PreprocessorOutputWrapper(object):
def __init__(self, preprocessor, parser):
self._parser = parser
self._pp = preprocessor
def write(self, str):
with errors.context(self._pp.context['FILE'], self._pp.context['LINE']):
self._parser.handle_line(str)
def preprocess(input, parser, defines={}):
pp = Preprocessor()
pp.context.update(defines)
pp.do_filter('substitution')
pp.out = PreprocessorOutputWrapper(pp, parser)
pp.do_include(input)
def preprocess_manifest(sink, manifest, defines={}):
preprocess(manifest, PackageManifestParser(sink), defines)
class CallDeque(deque):
def append(self, function, *args):
deque.append(self, (errors.get_context(), function, args))
def execute(self):
while True:
try:
context, function, args = self.popleft()
except IndexError:
return
if context:
with errors.context(context[0], context[1]):
function(*args)
else:
function(*args)
class SimplePackager(object):
def __init__(self, formatter):
self.formatter = formatter
self._queue = CallDeque()
self._chrome_queue = CallDeque()
self._file_queue = CallDeque()
self._addons = {}
self._manifests = set()
self._included_manifests = {}
self._closed = False
UNPACK_ADDON_RE = re.compile(r'''(?:
<em:unpack>true</em:unpack>
|em:unpack=(?P<quote>["']?)true(?P=quote)
)''', re.VERBOSE)
def add(self, path, file):
assert not self._closed
if is_manifest(path):
self._add_manifest_file(path, file)
elif path.endswith('.xpt'):
self._queue.append(self.formatter.add_interfaces, path, file)
else:
self._file_queue.append(self.formatter.add, path, file)
if mozpath.basename(path) == 'install.rdf':
addon = True
install_rdf = file.open().read()
if self.UNPACK_ADDON_RE.search(install_rdf):
addon = 'unpacked'
self._add_addon(mozpath.dirname(path), addon)
elif mozpath.basename(path) == 'manifest.json':
manifest = file.open().read()
try:
parsed = json.loads(manifest)
except ValueError:
pass
if isinstance(parsed, dict) and 'manifest_version' in parsed:
self._add_addon(mozpath.dirname(path), True)
def _add_addon(self, path, addon_type):
if mozpath.basedir(path, self._addons) is not None:
return
for dir in self._addons:
if mozpath.basedir(dir, [path]) is not None:
del self._addons[dir]
break
self._addons[path] = addon_type
def _add_manifest_file(self, path, file):
self._manifests.add(path)
base = ''
if hasattr(file, 'path'):
b = mozpath.normsep(file.path)
if b.endswith('/' + path) or b == path:
base = os.path.normpath(b[:-len(path)])
for e in parse_manifest(base, path, file.open()):
if isinstance(e, ManifestChrome):
self._chrome_queue.append(self.formatter.add_manifest,
e.move(e.base))
elif not isinstance(e, (Manifest, ManifestInterfaces)):
self._queue.append(self.formatter.add_manifest, e.move(e.base))
if isinstance(e, ManifestBinaryComponent):
addon = mozpath.basedir(e.base, self._addons)
if addon:
self._addons[addon] = 'unpacked'
if isinstance(e, Manifest):
if e.flags:
errors.fatal('Flags are not supported on ' +
'"manifest" entries')
self._included_manifests[e.path] = path
def get_bases(self, addons=True):
all_bases = set(mozpath.dirname(m)
for m in self._manifests
- set(self._included_manifests))
if not addons:
all_bases -= set(self._addons)
else:
all_bases |= set(self._addons)
return all_bases
def close(self):
self._closed = True
bases = self.get_bases()
broken_bases = sorted(
m for m, includer in self._included_manifests.iteritems()
if mozpath.basedir(m, bases) != mozpath.basedir(includer, bases))
for m in broken_bases:
errors.fatal('"%s" is included from "%s", which is outside "%s"' %
(m, self._included_manifests[m],
mozpath.basedir(m, bases)))
for base in sorted(bases):
self.formatter.add_base(base, self._addons.get(base, False))
self._chrome_queue.execute()
self._queue.execute()
self._file_queue.execute()
class SimpleManifestSink(object):
def __init__(self, finder, formatter):
self._finder = finder
self.packager = SimplePackager(formatter)
self._closed = False
self._manifests = set()
@staticmethod
def normalize_path(path):
if mozpath.basedir(path, ['bin']) == 'bin':
return mozpath.relpath(path, 'bin')
return path
def add(self, component, pattern):
assert not self._closed
added = False
for p, f in self._finder.find(pattern):
added = True
if is_manifest(p):
self._manifests.add(p)
dest = mozpath.join(component.destdir, SimpleManifestSink.normalize_path(p))
if isinstance(f, ExecutableFile):
f.xz_compress = component.xz_compress
self.packager.add(dest, f)
if not added:
errors.error('Missing file(s): %s' % pattern)
def remove(self, component, pattern):
assert not self._closed
errors.fatal('Removal is unsupported')
def close(self, auto_root_manifest=True):
if auto_root_manifest:
paths = [mozpath.dirname(m) for m in self._manifests]
path = mozpath.dirname(mozpath.commonprefix(paths))
for p, f in self._finder.find(mozpath.join(path,
'chrome.manifest')):
if p not in self._manifests:
self.packager.add(SimpleManifestSink.normalize_path(p), f)
self.packager.close()