summaryrefslogtreecommitdiffstats
path: root/python/mozbuild/mozbuild/jar.py
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--python/mozbuild/mozbuild/jar.py648
1 files changed, 648 insertions, 0 deletions
diff --git a/python/mozbuild/mozbuild/jar.py b/python/mozbuild/mozbuild/jar.py
new file mode 100644
index 0000000000..f7d10f7fed
--- /dev/null
+++ b/python/mozbuild/mozbuild/jar.py
@@ -0,0 +1,648 @@
+# 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/.
+
+"""jarmaker.py provides a python class to package up chrome content by
+processing jar.mn files.
+
+See the documentation for jar.mn on MDC for further details on the format.
+"""
+
+import errno
+import io
+import logging
+import os
+import re
+import sys
+from time import localtime
+
+import mozpack.path as mozpath
+import six
+from mozpack.files import FileFinder
+from MozZipFile import ZipFile
+from six import BytesIO
+
+from mozbuild.action.buildlist import addEntriesToListFile
+from mozbuild.preprocessor import Preprocessor
+from mozbuild.util import ensure_bytes
+
+if sys.platform == "win32":
+ from ctypes import WinError, windll
+
+ CreateHardLink = windll.kernel32.CreateHardLinkA
+
+__all__ = ["JarMaker"]
+
+
+class ZipEntry(object):
+ """Helper class for jar output.
+
+ This class defines a simple file-like object for a zipfile.ZipEntry
+ so that we can consecutively write to it and then close it.
+ This methods hooks into ZipFile.writestr on close().
+ """
+
+ def __init__(self, name, zipfile):
+ self._zipfile = zipfile
+ self._name = name
+ self._inner = BytesIO()
+
+ def write(self, content):
+ """Append the given content to this zip entry"""
+
+ self._inner.write(ensure_bytes(content))
+ return
+
+ def close(self):
+ """The close method writes the content back to the zip file."""
+
+ self._zipfile.writestr(self._name, self._inner.getvalue())
+
+
+def getModTime(aPath):
+ if not os.path.isfile(aPath):
+ return localtime(0)
+ mtime = os.stat(aPath).st_mtime
+ return localtime(mtime)
+
+
+class JarManifestEntry(object):
+ def __init__(self, output, source, is_locale=False, preprocess=False):
+ self.output = output
+ self.source = source
+ self.is_locale = is_locale
+ self.preprocess = preprocess
+
+
+class JarInfo(object):
+ def __init__(self, base_or_jarinfo, name=None):
+ if name is None:
+ assert isinstance(base_or_jarinfo, JarInfo)
+ self.base = base_or_jarinfo.base
+ self.name = base_or_jarinfo.name
+ else:
+ assert not isinstance(base_or_jarinfo, JarInfo)
+ self.base = base_or_jarinfo or ""
+ self.name = name
+ # For compatibility with existing jar.mn files, if there is no
+ # base, the jar name is under chrome/
+ if not self.base:
+ self.name = mozpath.join("chrome", self.name)
+ self.relativesrcdir = None
+ self.chrome_manifests = []
+ self.entries = []
+
+
+class DeprecatedJarManifest(Exception):
+ pass
+
+
+class JarManifestParser(object):
+
+ ignore = re.compile("\s*(\#.*)?$")
+ jarline = re.compile(
+ """
+ (?:
+ (?:\[(?P<base>[\w\d.\-\_\\\/{}@]+)\]\s*)? # optional [base/path]
+ (?P<jarfile>[\w\d.\-\_\\\/{}]+).jar\: # filename.jar:
+ |
+ (?:\s*(\#.*)?) # comment
+ )\s*$ # whitespaces
+ """,
+ re.VERBOSE,
+ )
+ relsrcline = re.compile("relativesrcdir\s+(?P<relativesrcdir>.+?):")
+ regline = re.compile("\%\s+(.*)$")
+ entryre = "(?P<optPreprocess>\*)?(?P<optOverwrite>\+?)\s+"
+ entryline = re.compile(
+ entryre
+ + (
+ "(?P<output>[\w\d.\-\_\\\/\+\@]+)\s*"
+ "(\((?P<locale>\%?)(?P<source>[\w\d.\-\_\\\/\@\*]+)\))?\s*$"
+ )
+ )
+
+ def __init__(self):
+ self._current_jar = None
+ self._jars = []
+
+ def write(self, line):
+ # A Preprocessor instance feeds the parser through calls to this method.
+
+ # Ignore comments and empty lines
+ if self.ignore.match(line):
+ return
+
+ # A jar manifest file can declare several different sections, each of
+ # which applies to a given "jar file". Each of those sections starts
+ # with "<name>.jar:", in which case the path is assumed relative to
+ # a "chrome" directory, or "[<base/path>] <subpath/name>.jar:", where
+ # a base directory is given (usually pointing at the root of the
+ # application or addon) and the jar path is given relative to the base
+ # directory.
+ if self._current_jar is None:
+ m = self.jarline.match(line)
+ if not m:
+ raise RuntimeError(line)
+ if m.group("jarfile"):
+ self._current_jar = JarInfo(m.group("base"), m.group("jarfile"))
+ self._jars.append(self._current_jar)
+ return
+
+ # Within each section, there can be three different types of entries:
+
+ # - indications of the relative source directory we pretend to be in
+ # when considering localization files, in the following form;
+ # "relativesrcdir <path>:"
+ m = self.relsrcline.match(line)
+ if m:
+ if self._current_jar.chrome_manifests or self._current_jar.entries:
+ self._current_jar = JarInfo(self._current_jar)
+ self._jars.append(self._current_jar)
+ self._current_jar.relativesrcdir = m.group("relativesrcdir")
+ return
+
+ # - chrome manifest entries, prefixed with "%".
+ m = self.regline.match(line)
+ if m:
+ rline = " ".join(m.group(1).split())
+ if rline not in self._current_jar.chrome_manifests:
+ self._current_jar.chrome_manifests.append(rline)
+ return
+
+ # - entries indicating files to be part of the given jar. They are
+ # formed thusly:
+ # "<dest_path>"
+ # or
+ # "<dest_path> (<source_path>)"
+ # The <dest_path> is where the file(s) will be put in the chrome jar.
+ # The <source_path> is where the file(s) can be found in the source
+ # directory. The <source_path> may start with a "%" for files part
+ # of a localization directory, in which case the "%" counts as the
+ # locale.
+ # Each entry can be prefixed with "*" for preprocessing.
+ m = self.entryline.match(line)
+ if m:
+ if m.group("optOverwrite"):
+ raise DeprecatedJarManifest('The "+" prefix is not supported anymore')
+ self._current_jar.entries.append(
+ JarManifestEntry(
+ m.group("output"),
+ m.group("source") or mozpath.basename(m.group("output")),
+ is_locale=bool(m.group("locale")),
+ preprocess=bool(m.group("optPreprocess")),
+ )
+ )
+ return
+
+ self._current_jar = None
+ self.write(line)
+
+ def __iter__(self):
+ return iter(self._jars)
+
+
+class JarMaker(object):
+ """JarMaker reads jar.mn files and process those into jar files or
+ flat directories, along with chrome.manifest files.
+ """
+
+ def __init__(
+ self, outputFormat="flat", useJarfileManifest=True, useChromeManifest=False
+ ):
+
+ self.outputFormat = outputFormat
+ self.useJarfileManifest = useJarfileManifest
+ self.useChromeManifest = useChromeManifest
+ self.pp = Preprocessor()
+ self.topsourcedir = None
+ self.sourcedirs = []
+ self.localedirs = None
+ self.l10nbase = None
+ self.relativesrcdir = None
+ self.rootManifestAppId = None
+ self._seen_output = set()
+
+ def getCommandLineParser(self):
+ """Get a optparse.OptionParser for jarmaker.
+
+ This OptionParser has the options for jarmaker as well as
+ the options for the inner PreProcessor.
+ """
+
+ # HACK, we need to unescape the string variables we get,
+ # the perl versions didn't grok strings right
+
+ p = self.pp.getCommandLineParser(unescapeDefines=True)
+ p.add_option(
+ "-f",
+ type="choice",
+ default="jar",
+ choices=("jar", "flat", "symlink"),
+ help="fileformat used for output",
+ metavar="[jar, flat, symlink]",
+ )
+ p.add_option("-v", action="store_true", dest="verbose", help="verbose output")
+ p.add_option("-q", action="store_false", dest="verbose", help="verbose output")
+ p.add_option(
+ "-e",
+ action="store_true",
+ help="create chrome.manifest instead of jarfile.manifest",
+ )
+ p.add_option(
+ "-s", type="string", action="append", default=[], help="source directory"
+ )
+ p.add_option("-t", type="string", help="top source directory")
+ p.add_option(
+ "-c",
+ "--l10n-src",
+ type="string",
+ action="append",
+ help="localization directory",
+ )
+ p.add_option(
+ "--l10n-base",
+ type="string",
+ action="store",
+ help="merged directory to be used for localization (requires relativesrcdir)",
+ )
+ p.add_option(
+ "--relativesrcdir",
+ type="string",
+ help="relativesrcdir to be used for localization",
+ )
+ p.add_option("-d", type="string", help="base directory")
+ p.add_option(
+ "--root-manifest-entry-appid",
+ type="string",
+ help="add an app id specific root chrome manifest entry.",
+ )
+ return p
+
+ def finalizeJar(
+ self, jardir, jarbase, jarname, chromebasepath, register, doZip=True
+ ):
+ """Helper method to write out the chrome registration entries to
+ jarfile.manifest or chrome.manifest, or both.
+
+ The actual file processing is done in updateManifest.
+ """
+
+ # rewrite the manifest, if entries given
+ if not register:
+ return
+
+ chromeManifest = os.path.join(jardir, jarbase, "chrome.manifest")
+
+ if self.useJarfileManifest:
+ self.updateManifest(
+ os.path.join(jardir, jarbase, jarname + ".manifest"),
+ chromebasepath.format(""),
+ register,
+ )
+ if jarname != "chrome":
+ addEntriesToListFile(
+ chromeManifest, ["manifest {0}.manifest".format(jarname)]
+ )
+ if self.useChromeManifest:
+ chromebase = os.path.dirname(jarname) + "/"
+ self.updateManifest(
+ chromeManifest, chromebasepath.format(chromebase), register
+ )
+
+ # If requested, add a root chrome manifest entry (assumed to be in the parent directory
+ # of chromeManifest) with the application specific id. In cases where we're building
+ # lang packs, the root manifest must know about application sub directories.
+
+ if self.rootManifestAppId:
+ rootChromeManifest = os.path.join(
+ os.path.normpath(os.path.dirname(chromeManifest)),
+ "..",
+ "chrome.manifest",
+ )
+ rootChromeManifest = os.path.normpath(rootChromeManifest)
+ chromeDir = os.path.basename(
+ os.path.dirname(os.path.normpath(chromeManifest))
+ )
+ logging.info(
+ "adding '%s' entry to root chrome manifest appid=%s"
+ % (chromeDir, self.rootManifestAppId)
+ )
+ addEntriesToListFile(
+ rootChromeManifest,
+ [
+ "manifest %s/chrome.manifest application=%s"
+ % (chromeDir, self.rootManifestAppId)
+ ],
+ )
+
+ def updateManifest(self, manifestPath, chromebasepath, register):
+ """updateManifest replaces the % in the chrome registration entries
+ with the given chrome base path, and updates the given manifest file.
+ """
+ myregister = dict.fromkeys(
+ map(lambda s: s.replace("%", chromebasepath), register)
+ )
+ addEntriesToListFile(manifestPath, six.iterkeys(myregister))
+
+ def makeJar(self, infile, jardir):
+ """makeJar is the main entry point to JarMaker.
+
+ It takes the input file, the output directory, the source dirs and the
+ top source dir as argument, and optionally the l10n dirs.
+ """
+
+ # making paths absolute, guess srcdir if file and add to sourcedirs
+ def _normpath(p):
+ return os.path.normpath(os.path.abspath(p))
+
+ self.topsourcedir = _normpath(self.topsourcedir)
+ self.sourcedirs = [_normpath(p) for p in self.sourcedirs]
+ if self.localedirs:
+ self.localedirs = [_normpath(p) for p in self.localedirs]
+ elif self.relativesrcdir:
+ self.localedirs = self.generateLocaleDirs(self.relativesrcdir)
+ if isinstance(infile, six.text_type):
+ logging.info("processing " + infile)
+ self.sourcedirs.append(_normpath(os.path.dirname(infile)))
+ pp = self.pp.clone()
+ pp.out = JarManifestParser()
+ pp.do_include(infile)
+
+ for info in pp.out:
+ self.processJarSection(info, jardir)
+
+ def generateLocaleDirs(self, relativesrcdir):
+ if os.path.basename(relativesrcdir) == "locales":
+ # strip locales
+ l10nrelsrcdir = os.path.dirname(relativesrcdir)
+ else:
+ l10nrelsrcdir = relativesrcdir
+ locdirs = []
+
+ # generate locales merge or en-US
+ if self.l10nbase:
+ locdirs.append(os.path.join(self.l10nbase, l10nrelsrcdir))
+ else:
+ # add en-US if it's not l10n
+ locdirs.append(os.path.join(self.topsourcedir, relativesrcdir, "en-US"))
+ return locdirs
+
+ def processJarSection(self, jarinfo, jardir):
+ """Internal method called by makeJar to actually process a section
+ of a jar.mn file.
+ """
+
+ # chromebasepath is used for chrome registration manifests
+ # {0} is getting replaced with chrome/ for chrome.manifest, and with
+ # an empty string for jarfile.manifest
+
+ chromebasepath = "{0}" + os.path.basename(jarinfo.name)
+ if self.outputFormat == "jar":
+ chromebasepath = "jar:" + chromebasepath + ".jar!"
+ chromebasepath += "/"
+
+ jarfile = os.path.join(jardir, jarinfo.base, jarinfo.name)
+ jf = None
+ if self.outputFormat == "jar":
+ # jar
+ jarfilepath = jarfile + ".jar"
+ try:
+ os.makedirs(os.path.dirname(jarfilepath))
+ except OSError as error:
+ if error.errno != errno.EEXIST:
+ raise
+ jf = ZipFile(jarfilepath, "a", lock=True)
+ outHelper = self.OutputHelper_jar(jf)
+ else:
+ outHelper = getattr(self, "OutputHelper_" + self.outputFormat)(jarfile)
+
+ if jarinfo.relativesrcdir:
+ self.localedirs = self.generateLocaleDirs(jarinfo.relativesrcdir)
+
+ for e in jarinfo.entries:
+ self._processEntryLine(e, outHelper, jf)
+
+ self.finalizeJar(
+ jardir, jarinfo.base, jarinfo.name, chromebasepath, jarinfo.chrome_manifests
+ )
+ if jf is not None:
+ jf.close()
+
+ def _processEntryLine(self, e, outHelper, jf):
+ out = e.output
+ src = e.source
+
+ # pick the right sourcedir -- l10n, topsrc or src
+
+ if e.is_locale:
+ # If the file is a Fluent l10n resource, we want to skip the
+ # 'en-US' fallbacking.
+ #
+ # To achieve that, we're testing if we have more than one localedir,
+ # and if the last of those has 'en-US' in it.
+ # If that's the case, we're removing the last one.
+ if (
+ e.source.endswith(".ftl")
+ and len(self.localedirs) > 1
+ and "en-US" in self.localedirs[-1]
+ ):
+ src_base = self.localedirs[:-1]
+ else:
+ src_base = self.localedirs
+ elif src.startswith("/"):
+ # path/in/jar/file_name.xul (/path/in/sourcetree/file_name.xul)
+ # refers to a path relative to topsourcedir, use that as base
+ # and strip the leading '/'
+ src_base = [self.topsourcedir]
+ src = src[1:]
+ else:
+ # use srcdirs and the objdir (current working dir) for relative paths
+ src_base = self.sourcedirs + [os.getcwd()]
+
+ if "*" in src:
+
+ def _prefix(s):
+ for p in s.split("/"):
+ if "*" not in p:
+ yield p + "/"
+
+ prefix = "".join(_prefix(src))
+ emitted = set()
+ for _srcdir in src_base:
+ finder = FileFinder(_srcdir)
+ for path, _ in finder.find(src):
+ # If the path was already seen in one of the other source
+ # directories, skip it. That matches the non-wildcard case
+ # below, where we pick the first existing file.
+ reduced_path = path[len(prefix) :]
+ if reduced_path in emitted:
+ continue
+ emitted.add(reduced_path)
+ e = JarManifestEntry(
+ mozpath.join(out, reduced_path),
+ path,
+ is_locale=e.is_locale,
+ preprocess=e.preprocess,
+ )
+ self._processEntryLine(e, outHelper, jf)
+ return
+
+ # check if the source file exists
+ realsrc = None
+ for _srcdir in src_base:
+ if os.path.isfile(os.path.join(_srcdir, src)):
+ realsrc = os.path.join(_srcdir, src)
+ break
+ if realsrc is None:
+ if jf is not None:
+ jf.close()
+ raise RuntimeError(
+ 'File "{0}" not found in {1}'.format(src, ", ".join(src_base))
+ )
+
+ if out in self._seen_output:
+ raise RuntimeError("%s already added" % out)
+ self._seen_output.add(out)
+
+ if e.preprocess:
+ outf = outHelper.getOutput(out, mode="w")
+ inf = io.open(realsrc, encoding="utf-8")
+ pp = self.pp.clone()
+ if src[-4:] == ".css":
+ pp.setMarker("%")
+ pp.out = outf
+ pp.do_include(inf)
+ pp.failUnused(realsrc)
+ outf.close()
+ inf.close()
+ return
+
+ # copy or symlink if newer
+
+ if getModTime(realsrc) > outHelper.getDestModTime(e.output):
+ if self.outputFormat == "symlink":
+ outHelper.symlink(realsrc, out)
+ return
+ outf = outHelper.getOutput(out)
+
+ # open in binary mode, this can be images etc
+
+ inf = open(realsrc, "rb")
+ outf.write(inf.read())
+ outf.close()
+ inf.close()
+
+ class OutputHelper_jar(object):
+ """Provide getDestModTime and getOutput for a given jarfile."""
+
+ def __init__(self, jarfile):
+ self.jarfile = jarfile
+
+ def getDestModTime(self, aPath):
+ try:
+ info = self.jarfile.getinfo(aPath)
+ return info.date_time
+ except Exception:
+ return localtime(0)
+
+ def getOutput(self, name, mode="wb"):
+ return ZipEntry(name, self.jarfile)
+
+ class OutputHelper_flat(object):
+ """Provide getDestModTime and getOutput for a given flat
+ output directory. The helper method ensureDirFor is used by
+ the symlink subclass.
+ """
+
+ def __init__(self, basepath):
+ self.basepath = basepath
+
+ def getDestModTime(self, aPath):
+ return getModTime(os.path.join(self.basepath, aPath))
+
+ def getOutput(self, name, mode="wb"):
+ out = self.ensureDirFor(name)
+
+ # remove previous link or file
+ try:
+ os.remove(out)
+ except OSError as e:
+ if e.errno != errno.ENOENT:
+ raise
+ if "b" in mode:
+ return io.open(out, mode)
+ else:
+ return io.open(out, mode, encoding="utf-8", newline="\n")
+
+ def ensureDirFor(self, name):
+ out = os.path.join(self.basepath, name)
+ outdir = os.path.dirname(out)
+ if not os.path.isdir(outdir):
+ try:
+ os.makedirs(outdir)
+ except OSError as error:
+ if error.errno != errno.EEXIST:
+ raise
+ return out
+
+ class OutputHelper_symlink(OutputHelper_flat):
+ """Subclass of OutputHelper_flat that provides a helper for
+ creating a symlink including creating the parent directories.
+ """
+
+ def symlink(self, src, dest):
+ out = self.ensureDirFor(dest)
+
+ # remove previous link or file
+ try:
+ os.remove(out)
+ except OSError as e:
+ if e.errno != errno.ENOENT:
+ raise
+ if sys.platform != "win32":
+ os.symlink(src, out)
+ else:
+ # On Win32, use ctypes to create a hardlink
+ rv = CreateHardLink(ensure_bytes(out), ensure_bytes(src), None)
+ if rv == 0:
+ raise WinError()
+
+
+def main(args=None):
+ args = args or sys.argv
+ jm = JarMaker()
+ p = jm.getCommandLineParser()
+ (options, args) = p.parse_args(args)
+ jm.outputFormat = options.f
+ jm.sourcedirs = options.s
+ jm.topsourcedir = options.t
+ if options.e:
+ jm.useChromeManifest = True
+ jm.useJarfileManifest = False
+ if options.l10n_base:
+ if not options.relativesrcdir:
+ p.error("relativesrcdir required when using l10n-base")
+ if options.l10n_src:
+ p.error("both l10n-src and l10n-base are not supported")
+ jm.l10nbase = options.l10n_base
+ jm.relativesrcdir = options.relativesrcdir
+ jm.localedirs = options.l10n_src
+ if options.root_manifest_entry_appid:
+ jm.rootManifestAppId = options.root_manifest_entry_appid
+ noise = logging.INFO
+ if options.verbose is not None:
+ noise = options.verbose and logging.DEBUG or logging.WARN
+ if sys.version_info[:2] > (2, 3):
+ logging.basicConfig(format="%(message)s")
+ else:
+ logging.basicConfig()
+ logging.getLogger().setLevel(noise)
+ topsrc = options.t
+ topsrc = os.path.normpath(os.path.abspath(topsrc))
+ if not args:
+ infile = sys.stdin
+ else:
+ (infile,) = args
+ infile = six.ensure_text(infile)
+ jm.makeJar(infile, options.d)