import codecs
import json
import os
import re
from collections import deque
import mozpack.path as mozpath
from mozbuild.preprocessor import Preprocessor
from mozpack.chrome.manifest import (
Manifest,
ManifestBinaryComponent,
ManifestChrome,
ManifestInterfaces,
is_manifest,
parse_manifest,
)
from mozpack.errors import errors
class Component:
def __init__(self, name, destdir=""):
if name.find(" ") > 0:
errors.fatal('Malformed manifest: space in component name "%s"' % name)
self._name = name
self._destdir = destdir
def __repr__(self):
s = self.name
if self.destdir:
s += ' destdir="%s"' % self.destdir
return s
@property
def name(self):
return self._name
@property
def destdir(self):
return self._destdir
@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", "")
if options:
errors.fatal(
"Malformed manifest: options %s not recognized" % options.keys()
)
return Component(name, destdir=destdir)
class PackageManifestParser:
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:
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:
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().decode()
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, codecs.getreader("utf-8")(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.items()
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:
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))
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()