diff options
Diffstat (limited to 'toolkit/mozapps/installer/packager.py')
-rw-r--r-- | toolkit/mozapps/installer/packager.py | 415 |
1 files changed, 415 insertions, 0 deletions
diff --git a/toolkit/mozapps/installer/packager.py b/toolkit/mozapps/installer/packager.py new file mode 100644 index 000000000..f2dc3fac6 --- /dev/null +++ b/toolkit/mozapps/installer/packager.py @@ -0,0 +1,415 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +from mozpack.packager.formats import ( + FlatFormatter, + JarFormatter, + OmniJarFormatter, +) +from mozpack.packager import ( + preprocess_manifest, + preprocess, + Component, + SimpleManifestSink, +) +from mozpack.files import ( + GeneratedFile, + FileFinder, + File, +) +from mozpack.copier import ( + FileCopier, + Jarrer, +) +from mozpack.errors import errors +from mozpack.unify import UnifiedBuildFinder +import mozpack.path as mozpath +import buildconfig +from argparse import ArgumentParser +import os +from StringIO import StringIO +import subprocess +import platform +import mozinfo + +# List of libraries to shlibsign. +SIGN_LIBS = [ + 'softokn3', + 'nssdbm3', + 'freebl3', + 'freeblpriv3', + 'freebl_32fpu_3', + 'freebl_32int_3', + 'freebl_32int64_3', + 'freebl_64fpu_3', + 'freebl_64int_3', +] + + +class ToolLauncher(object): + ''' + Helper to execute tools like xpcshell with the appropriate environment. + launcher = ToolLauncher() + launcher.tooldir = '/path/to/tools' + launcher.launch(['xpcshell', '-e', 'foo.js']) + ''' + def __init__(self): + self.tooldir = None + + def launch(self, cmd, extra_linker_path=None, extra_env={}): + ''' + Launch the given command, passed as a list. The first item in the + command list is the program name, without a path and without a suffix. + These are determined from the tooldir member and the BIN_SUFFIX value. + An extra_linker_path may be passed to give an additional directory + to add to the search paths for the dynamic linker. + An extra_env dict may be passed to give additional environment + variables to export when running the command. + ''' + assert self.tooldir + cmd[0] = os.path.join(self.tooldir, 'bin', + cmd[0] + buildconfig.substs['BIN_SUFFIX']) + if not extra_linker_path: + extra_linker_path = os.path.join(self.tooldir, 'bin') + env = dict(os.environ) + for p in ['LD_LIBRARY_PATH', 'DYLD_LIBRARY_PATH']: + if p in env: + env[p] = extra_linker_path + ':' + env[p] + else: + env[p] = extra_linker_path + for e in extra_env: + env[e] = extra_env[e] + + # For VC12+, make sure we can find the right bitness of pgort1x0.dll + if not buildconfig.substs.get('HAVE_64BIT_BUILD'): + for e in ('VS140COMNTOOLS', 'VS120COMNTOOLS'): + if e not in env: + continue + + vcdir = os.path.abspath(os.path.join(env[e], '../../VC/bin')) + if os.path.exists(vcdir): + env['PATH'] = '%s;%s' % (vcdir, env['PATH']) + break + + # Work around a bug in Python 2.7.2 and lower where unicode types in + # environment variables aren't handled by subprocess. + for k, v in env.items(): + if isinstance(v, unicode): + env[k] = v.encode('utf-8') + + print >>errors.out, 'Executing', ' '.join(cmd) + errors.out.flush() + return subprocess.call(cmd, env=env) + + def can_launch(self): + return self.tooldir is not None + +launcher = ToolLauncher() + + +class LibSignFile(File): + ''' + File class for shlibsign signatures. + ''' + def copy(self, dest, skip_if_older=True): + assert isinstance(dest, basestring) + # os.path.getmtime returns a result in seconds with precision up to the + # microsecond. But microsecond is too precise because shutil.copystat + # only copies milliseconds, and seconds is not enough precision. + if os.path.exists(dest) and skip_if_older and \ + int(os.path.getmtime(self.path) * 1000) <= \ + int(os.path.getmtime(dest) * 1000): + return False + if launcher.launch(['shlibsign', '-v', '-o', dest, '-i', self.path]): + errors.fatal('Error while signing %s' % self.path) + + +def precompile_cache(registry, source_path, gre_path, app_path): + ''' + Create startup cache for the given application directory, using the + given GRE path. + - registry is a FileRegistry-like instance where to add the startup cache. + - source_path is the base path of the package. + - gre_path is the GRE path, relative to source_path. + - app_path is the application path, relative to source_path. + Startup cache for all resources under resource://app/ are generated, + except when gre_path == app_path, in which case it's under + resource://gre/. + ''' + from tempfile import mkstemp + source_path = os.path.abspath(source_path) + if app_path != gre_path: + resource = 'app' + else: + resource = 'gre' + app_path = os.path.join(source_path, app_path) + gre_path = os.path.join(source_path, gre_path) + + fd, cache = mkstemp('.zip') + os.close(fd) + os.remove(cache) + + try: + extra_env = {'MOZ_STARTUP_CACHE': cache} + if buildconfig.substs.get('MOZ_TSAN'): + extra_env['TSAN_OPTIONS'] = 'report_bugs=0' + if buildconfig.substs.get('MOZ_ASAN'): + extra_env['ASAN_OPTIONS'] = 'detect_leaks=0' + if launcher.launch(['xpcshell', '-g', gre_path, '-a', app_path, + '-f', os.path.join(os.path.dirname(__file__), + 'precompile_cache.js'), + '-e', 'precompile_startupcache("resource://%s/");' + % resource], + extra_linker_path=gre_path, + extra_env=extra_env): + errors.fatal('Error while running startup cache precompilation') + return + from mozpack.mozjar import JarReader + jar = JarReader(cache) + resource = '/resource/%s/' % resource + for f in jar: + if resource in f.filename: + path = f.filename[f.filename.index(resource) + len(resource):] + if registry.contains(path): + registry.add(f.filename, GeneratedFile(f.read())) + jar.close() + finally: + if os.path.exists(cache): + os.remove(cache) + + +class RemovedFiles(GeneratedFile): + ''' + File class for removed-files. Is used as a preprocessor parser. + ''' + def __init__(self, copier): + self.copier = copier + GeneratedFile.__init__(self, '') + + def handle_line(self, str): + f = str.strip() + if not f: + return + if self.copier.contains(f): + errors.error('Removal of packaged file(s): %s' % f) + self.content += f + '\n' + + +def split_define(define): + ''' + Give a VAR[=VAL] string, returns a (VAR, VAL) tuple, where VAL defaults to + 1. Numeric VALs are returned as ints. + ''' + if '=' in define: + name, value = define.split('=', 1) + try: + value = int(value) + except ValueError: + pass + return (name, value) + return (define, 1) + + +class NoPkgFilesRemover(object): + ''' + Formatter wrapper to handle NO_PKG_FILES. + ''' + def __init__(self, formatter, has_manifest): + assert 'NO_PKG_FILES' in os.environ + self._formatter = formatter + self._files = os.environ['NO_PKG_FILES'].split() + if has_manifest: + self._error = errors.error + self._msg = 'NO_PKG_FILES contains file listed in manifest: %s' + else: + self._error = errors.warn + self._msg = 'Skipping %s' + + def add_base(self, base, *args): + self._formatter.add_base(base, *args) + + def add(self, path, content): + if not any(mozpath.match(path, spec) for spec in self._files): + self._formatter.add(path, content) + else: + self._error(self._msg % path) + + def add_manifest(self, entry): + self._formatter.add_manifest(entry) + + def add_interfaces(self, path, content): + self._formatter.add_interfaces(path, content) + + def contains(self, path): + return self._formatter.contains(path) + + +def main(): + parser = ArgumentParser() + parser.add_argument('-D', dest='defines', action='append', + metavar="VAR[=VAL]", help='Define a variable') + parser.add_argument('--format', default='omni', + help='Choose the chrome format for packaging ' + + '(omni, jar or flat ; default: %(default)s)') + parser.add_argument('--removals', default=None, + help='removed-files source file') + parser.add_argument('--ignore-errors', action='store_true', default=False, + help='Transform errors into warnings.') + parser.add_argument('--minify', action='store_true', default=False, + help='Make some files more compact while packaging') + parser.add_argument('--minify-js', action='store_true', + help='Minify JavaScript files while packaging.') + parser.add_argument('--js-binary', + help='Path to js binary. This is used to verify ' + 'minified JavaScript. If this is not defined, ' + 'minification verification will not be performed.') + parser.add_argument('--jarlog', default='', help='File containing jar ' + + 'access logs') + parser.add_argument('--optimizejars', action='store_true', default=False, + help='Enable jar optimizations') + parser.add_argument('--unify', default='', + help='Base directory of another build to unify with') + parser.add_argument('--disable-compression', action='store_false', + dest='compress', default=True, + help='Disable jar compression') + parser.add_argument('manifest', default=None, nargs='?', + help='Manifest file name') + parser.add_argument('source', help='Source directory') + parser.add_argument('destination', help='Destination directory') + parser.add_argument('--non-resource', nargs='+', metavar='PATTERN', + default=[], + help='Extra files not to be considered as resources') + args = parser.parse_args() + + defines = dict(buildconfig.defines) + if args.ignore_errors: + errors.ignore_errors() + + if args.defines: + for name, value in [split_define(d) for d in args.defines]: + defines[name] = value + + copier = FileCopier() + if args.format == 'flat': + formatter = FlatFormatter(copier) + elif args.format == 'jar': + formatter = JarFormatter(copier, compress=args.compress, optimize=args.optimizejars) + elif args.format == 'omni': + formatter = OmniJarFormatter(copier, + buildconfig.substs['OMNIJAR_NAME'], + compress=args.compress, + optimize=args.optimizejars, + non_resources=args.non_resource) + else: + errors.fatal('Unknown format: %s' % args.format) + + # Adjust defines according to the requested format. + if isinstance(formatter, OmniJarFormatter): + defines['MOZ_OMNIJAR'] = 1 + elif 'MOZ_OMNIJAR' in defines: + del defines['MOZ_OMNIJAR'] + + respath = '' + if 'RESPATH' in defines: + respath = SimpleManifestSink.normalize_path(defines['RESPATH']) + while respath.startswith('/'): + respath = respath[1:] + + if args.unify: + def is_native(path): + path = os.path.abspath(path) + return platform.machine() in mozpath.split(path) + + # Invert args.unify and args.source if args.unify points to the + # native architecture. + args.source, args.unify = sorted([args.source, args.unify], + key=is_native, reverse=True) + if is_native(args.source) and not buildconfig.substs['CROSS_COMPILE']: + launcher.tooldir = args.source + elif not buildconfig.substs['CROSS_COMPILE']: + launcher.tooldir = mozpath.join(buildconfig.topobjdir, 'dist') + + with errors.accumulate(): + finder_args = dict( + minify=args.minify, + minify_js=args.minify_js, + ) + if args.js_binary: + finder_args['minify_js_verify_command'] = [ + args.js_binary, + os.path.join(os.path.abspath(os.path.dirname(__file__)), + 'js-compare-ast.js') + ] + if args.unify: + finder = UnifiedBuildFinder(FileFinder(args.source), + FileFinder(args.unify), + **finder_args) + else: + finder = FileFinder(args.source, **finder_args) + if 'NO_PKG_FILES' in os.environ: + sinkformatter = NoPkgFilesRemover(formatter, + args.manifest is not None) + else: + sinkformatter = formatter + sink = SimpleManifestSink(finder, sinkformatter) + if args.manifest: + preprocess_manifest(sink, args.manifest, defines) + else: + sink.add(Component(''), 'bin/*') + sink.close(args.manifest is not None) + + if args.removals: + removals_in = StringIO(open(args.removals).read()) + removals_in.name = args.removals + removals = RemovedFiles(copier) + preprocess(removals_in, removals, defines) + copier.add(mozpath.join(respath, 'removed-files'), removals) + + # shlibsign libraries + if launcher.can_launch(): + if not mozinfo.isMac and buildconfig.substs.get('COMPILE_ENVIRONMENT'): + for lib in SIGN_LIBS: + libbase = mozpath.join(respath, '%s%s') \ + % (buildconfig.substs['DLL_PREFIX'], lib) + libname = '%s%s' % (libbase, buildconfig.substs['DLL_SUFFIX']) + if copier.contains(libname): + copier.add(libbase + '.chk', + LibSignFile(os.path.join(args.destination, + libname))) + + # Setup preloading + if args.jarlog and os.path.exists(args.jarlog): + from mozpack.mozjar import JarLog + log = JarLog(args.jarlog) + for p, f in copier: + if not isinstance(f, Jarrer): + continue + key = JarLog.canonicalize(os.path.join(args.destination, p)) + if key in log: + f.preload(log[key]) + + # Fill startup cache + if isinstance(formatter, OmniJarFormatter) and launcher.can_launch() \ + and buildconfig.substs['MOZ_DISABLE_STARTUPCACHE'] != '1': + gre_path = None + def get_bases(): + for b in sink.packager.get_bases(addons=False): + for p in (mozpath.join('bin', b), b): + if os.path.exists(os.path.join(args.source, p)): + yield p + break + for base in sorted(get_bases()): + if not gre_path: + gre_path = base + omnijar_path = mozpath.join(sink.normalize_path(base), + buildconfig.substs['OMNIJAR_NAME']) + if formatter.contains(omnijar_path): + precompile_cache(formatter.copier[omnijar_path], + args.source, gre_path, base) + + copier.copy(args.destination) + + +if __name__ == '__main__': + main() |