diff options
Diffstat (limited to 'python/mozbuild')
926 files changed, 98749 insertions, 0 deletions
diff --git a/python/mozbuild/.ruff.toml b/python/mozbuild/.ruff.toml new file mode 100644 index 0000000000..ba54f854aa --- /dev/null +++ b/python/mozbuild/.ruff.toml @@ -0,0 +1,9 @@ +extend = "../../pyproject.toml" +src = [ + # Treat direct imports in the test modules as first party. + "mozpack/test", + "mozbuild/test", +] + +[isort] +known-first-party = ["mozbuild"] diff --git a/python/mozbuild/metrics.yaml b/python/mozbuild/metrics.yaml new file mode 100644 index 0000000000..068dd6a389 --- /dev/null +++ b/python/mozbuild/metrics.yaml @@ -0,0 +1,140 @@ +# 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/. + +# If this file is changed, update the generated docs: +# https://firefox-source-docs.mozilla.org/mach/telemetry.html#updating-generated-metrics-docs + +# Adding a new metric? We have docs for that! +# https://mozilla.github.io/glean/book/user/metrics/adding-new-metrics.html + +--- +$schema: moz://mozilla.org/schemas/glean/metrics/1-0-0 + +mozbuild: + compiler: + type: string + description: The compiler type in use (CC_TYPE), such as "clang" or "gcc". + lifetime: application + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1291053 + data_reviews: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1291053#c34 + notification_emails: + - build-telemetry@mozilla.com + - mhentges@mozilla.com + expires: never + send_in_pings: + - usage + artifact: + type: boolean + description: True if `--enable-artifact-builds`. + lifetime: application + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1291053 + data_reviews: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1291053#c34 + notification_emails: + - build-telemetry@mozilla.com + - mhentges@mozilla.com + expires: never + send_in_pings: + - usage + debug: + type: boolean + description: True if `--enable-debug`. + lifetime: application + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1291053 + data_reviews: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1291053#c34 + notification_emails: + - build-telemetry@mozilla.com + - mhentges@mozilla.com + expires: never + send_in_pings: + - usage + opt: + type: boolean + description: True if `--enable-optimize`. + lifetime: application + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1291053 + data_reviews: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1291053#c34 + notification_emails: + - build-telemetry@mozilla.com + - mhentges@mozilla.com + expires: never + send_in_pings: + - usage + ccache: + type: boolean + description: True if `--with-ccache`. + lifetime: application + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1291053 + data_reviews: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1291053#c34 + notification_emails: + - build-telemetry@mozilla.com + - mhentges@mozilla.com + expires: never + send_in_pings: + - usage + sccache: + type: boolean + description: True if ccache in use is sccache. + lifetime: application + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1291053 + data_reviews: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1291053#c34 + notification_emails: + - build-telemetry@mozilla.com + - mhentges@mozilla.com + expires: never + send_in_pings: + - usage + icecream: + type: boolean + description: True if icecream in use. + lifetime: application + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1291053 + data_reviews: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1291053#c34 + notification_emails: + - build-telemetry@mozilla.com + - mhentges@mozilla.com + expires: never + send_in_pings: + - usage + clobber: + type: boolean + description: True if the build was a clobber/full build. + lifetime: application + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1526072 + data_reviews: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1526072#c15 + notification_emails: + - build-telemetry@mozilla.com + - mhentges@mozilla.com + expires: never + send_in_pings: + - usage + project: + type: string + description: The project being built. + lifetime: application + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1654084 + data_reviews: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1654084#c2 + notification_emails: + - build-telemetry@mozilla.com + - mhentges@mozilla.com + expires: never + send_in_pings: + - usage diff --git a/python/mozbuild/mozbuild/__init__.py b/python/mozbuild/mozbuild/__init__.py new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/python/mozbuild/mozbuild/__init__.py diff --git a/python/mozbuild/mozbuild/action/__init__.py b/python/mozbuild/mozbuild/action/__init__.py new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/python/mozbuild/mozbuild/action/__init__.py diff --git a/python/mozbuild/mozbuild/action/buildlist.py b/python/mozbuild/mozbuild/action/buildlist.py new file mode 100644 index 0000000000..ed7149eb87 --- /dev/null +++ b/python/mozbuild/mozbuild/action/buildlist.py @@ -0,0 +1,48 @@ +# 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/. + +"""A generic script to add entries to a file +if the entry does not already exist. + +Usage: buildlist.py <filename> <entry> [<entry> ...] +""" +import io +import os +import sys + +from mozbuild.util import ensureParentDir, lock_file + + +def addEntriesToListFile(listFile, entries): + """Given a file ``listFile`` containing one entry per line, + add each entry in ``entries`` to the file, unless it is already + present.""" + ensureParentDir(listFile) + lock = lock_file(listFile + ".lck") + try: + if os.path.exists(listFile): + f = io.open(listFile) + existing = set(x.strip() for x in f.readlines()) + f.close() + else: + existing = set() + for e in entries: + if e not in existing: + existing.add(e) + with io.open(listFile, "w", newline="\n") as f: + f.write("\n".join(sorted(existing)) + "\n") + finally: + del lock # Explicitly release the lock_file to free it + + +def main(args): + if len(args) < 2: + print("Usage: buildlist.py <list file> <entry> [<entry> ...]", file=sys.stderr) + return 1 + + return addEntriesToListFile(args[0], args[1:]) + + +if __name__ == "__main__": + sys.exit(main(sys.argv[1:])) diff --git a/python/mozbuild/mozbuild/action/check_binary.py b/python/mozbuild/mozbuild/action/check_binary.py new file mode 100644 index 0000000000..b30635bd9b --- /dev/null +++ b/python/mozbuild/mozbuild/action/check_binary.py @@ -0,0 +1,327 @@ +# 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/. + +import argparse +import os +import re +import subprocess +import sys + +import buildconfig +from mozpack.executables import ELF, UNKNOWN, get_type +from packaging.version import Version + +from mozbuild.util import memoize + +STDCXX_MAX_VERSION = Version("3.4.19") +CXXABI_MAX_VERSION = Version("1.3.7") +GLIBC_MAX_VERSION = Version("2.17") +LIBGCC_MAX_VERSION = Version("4.8") + +PLATFORM = buildconfig.substs["OS_TARGET"] +READELF = buildconfig.substs.get("READELF", "readelf") + +ADDR_RE = re.compile(r"[0-9a-f]{8,16}") + +if buildconfig.substs.get("HAVE_64BIT_BUILD"): + GUESSED_NSMODULE_SIZE = 8 +else: + GUESSED_NSMODULE_SIZE = 4 + + +get_type = memoize(get_type) + + +@memoize +def get_output(*cmd): + env = dict(os.environ) + env[b"LC_ALL"] = b"C" + return subprocess.check_output(cmd, env=env, universal_newlines=True).splitlines() + + +class Skip(RuntimeError): + pass + + +class Empty(RuntimeError): + pass + + +def at_least_one(iter): + saw_one = False + for item in iter: + saw_one = True + yield item + if not saw_one: + raise Empty() + + +# Iterates the symbol table on ELF binaries. +def iter_elf_symbols(binary, all=False): + ty = get_type(binary) + # Static libraries are ar archives. Assume they are ELF. + if ty == UNKNOWN and open(binary, "rb").read(8) == b"!<arch>\n": + ty = ELF + assert ty == ELF + + def looks_like_readelf_data(data): + return len(data) >= 8 and data[0].endswith(":") and data[0][:-1].isdigit() + + for line in get_output( + READELF, "--wide", "--syms" if all else "--dyn-syms", binary + ): + data = line.split() + if not looks_like_readelf_data(data): + # Older versions of llvm-readelf would use .hash for --dyn-syms, + # which would add an extra column at the beginning. + data = data[1:] + if not looks_like_readelf_data(data): + continue + n, addr, size, type, bind, vis, index, name = data[:8] + + if "@" in name: + name, ver = name.rsplit("@", 1) + while name.endswith("@"): + name = name[:-1] + else: + ver = None + yield { + "addr": int(addr, 16), + # readelf output may contain decimal values or hexadecimal + # values prefixed with 0x for the size. Let python autodetect. + "size": int(size, 0), + "name": name, + "version": ver, + } + + +def iter_readelf_dynamic(binary): + for line in get_output(READELF, "-d", binary): + data = line.split(None, 2) + if data and len(data) == 3 and data[0].startswith("0x"): + yield data[1].rstrip(")").lstrip("("), data[2] + + +def check_binary_compat(binary): + if get_type(binary) != ELF: + raise Skip() + checks = ( + ("libstdc++", "GLIBCXX_", STDCXX_MAX_VERSION), + ("libstdc++", "CXXABI_", CXXABI_MAX_VERSION), + ("libgcc", "GCC_", LIBGCC_MAX_VERSION), + ("libc", "GLIBC_", GLIBC_MAX_VERSION), + ) + + unwanted = {} + try: + for sym in at_least_one(iter_elf_symbols(binary)): + # Only check versions on undefined symbols + if sym["addr"] != 0: + continue + + # No version to check + if not sym["version"]: + continue + + for _, prefix, max_version in checks: + if sym["version"].startswith(prefix): + version = Version(sym["version"][len(prefix) :]) + if version > max_version: + unwanted.setdefault(prefix, []).append(sym) + except Empty: + raise RuntimeError("Could not parse readelf output?") + if unwanted: + error = [] + for lib, prefix, _ in checks: + if prefix in unwanted: + error.append( + "We do not want these {} symbol versions to be used:".format(lib) + ) + error.extend( + " {} ({})".format(s["name"], s["version"]) for s in unwanted[prefix] + ) + raise RuntimeError("\n".join(error)) + + +def check_textrel(binary): + if get_type(binary) != ELF: + raise Skip() + try: + for tag, value in at_least_one(iter_readelf_dynamic(binary)): + if tag == "TEXTREL" or (tag == "FLAGS" and "TEXTREL" in value): + raise RuntimeError( + "We do not want text relocations in libraries and programs" + ) + except Empty: + raise RuntimeError("Could not parse readelf output?") + + +def ishex(s): + try: + int(s, 16) + return True + except ValueError: + return False + + +def is_libxul(binary): + basename = os.path.basename(binary).lower() + return "xul" in basename + + +def check_pt_load(binary): + if get_type(binary) != ELF or not is_libxul(binary): + raise Skip() + count = 0 + for line in get_output(READELF, "-l", binary): + data = line.split() + if data and data[0] == "LOAD": + count += 1 + if count <= 1: + raise RuntimeError("Expected more than one PT_LOAD segment") + + +def check_mozglue_order(binary): + if PLATFORM != "Android": + raise Skip() + # While this is very unlikely (libc being added by the compiler at the end + # of the linker command line), if libmozglue.so ends up after libc.so, all + # hell breaks loose, so better safe than sorry, and check it's actually the + # case. + try: + mozglue = libc = None + for n, (tag, value) in enumerate(at_least_one(iter_readelf_dynamic(binary))): + if tag == "NEEDED": + if "[libmozglue.so]" in value: + mozglue = n + elif "[libc.so]" in value: + libc = n + if libc is None: + raise RuntimeError("libc.so is not linked?") + if mozglue is not None and libc < mozglue: + raise RuntimeError("libmozglue.so must be linked before libc.so") + except Empty: + raise RuntimeError("Could not parse readelf output?") + + +def check_networking(binary): + retcode = 0 + networking_functions = set( + [ + # socketpair is not concerning; it is restricted to AF_UNIX + "connect", + "accept", + "listen", + "getsockname", + "getsockopt", + "recv", + "send", + # We would be concerned by recvmsg and sendmsg; but we believe + # they are okay as documented in 1376621#c23 + "gethostbyname", + "gethostbyaddr", + "gethostent", + "sethostent", + "endhostent", + "gethostent_r", + "gethostbyname2", + "gethostbyaddr_r", + "gethostbyname_r", + "gethostbyname2_r", + "getservent", + "getservbyname", + "getservbyport", + "setservent", + "getprotoent", + "getprotobyname", + "getprotobynumber", + "setprotoent", + "endprotoent", + ] + ) + bad_occurences_names = set() + + try: + for sym in at_least_one(iter_elf_symbols(binary, all=True)): + if sym["addr"] == 0 and sym["name"] in networking_functions: + bad_occurences_names.add(sym["name"]) + except Empty: + raise RuntimeError("Could not parse readelf output?") + + basename = os.path.basename(binary) + if bad_occurences_names: + s = ( + "TEST-UNEXPECTED-FAIL | check_networking | {} | Identified {} " + + "networking function(s) being imported in the rust static library ({})" + ) + print( + s.format( + basename, + len(bad_occurences_names), + ",".join(sorted(bad_occurences_names)), + ), + file=sys.stderr, + ) + retcode = 1 + elif buildconfig.substs.get("MOZ_AUTOMATION"): + print("TEST-PASS | check_networking | {}".format(basename)) + return retcode + + +def checks(binary): + # The clang-plugin is built as target but is really a host binary. + # Cheat and pretend we weren't called. + if "clang-plugin" in binary: + return 0 + checks = [] + if buildconfig.substs.get("MOZ_STDCXX_COMPAT") and PLATFORM == "Linux": + checks.append(check_binary_compat) + + # Disabled for local builds because of readelf performance: See bug 1472496 + if not buildconfig.substs.get("DEVELOPER_OPTIONS"): + checks.append(check_textrel) + checks.append(check_pt_load) + checks.append(check_mozglue_order) + + retcode = 0 + basename = os.path.basename(binary) + for c in checks: + try: + name = c.__name__ + c(binary) + if buildconfig.substs.get("MOZ_AUTOMATION"): + print("TEST-PASS | {} | {}".format(name, basename)) + except Skip: + pass + except RuntimeError as e: + print( + "TEST-UNEXPECTED-FAIL | {} | {} | {}".format(name, basename, str(e)), + file=sys.stderr, + ) + retcode = 1 + return retcode + + +def main(args): + parser = argparse.ArgumentParser(description="Check built binaries") + + parser.add_argument( + "--networking", + action="store_true", + help="Perform checks for networking functions", + ) + + parser.add_argument( + "binary", metavar="PATH", help="Location of the binary to check" + ) + + options = parser.parse_args(args) + + if options.networking: + return check_networking(options.binary) + return checks(options.binary) + + +if __name__ == "__main__": + sys.exit(main(sys.argv[1:])) diff --git a/python/mozbuild/mozbuild/action/download_wpt_manifest.py b/python/mozbuild/mozbuild/action/download_wpt_manifest.py new file mode 100644 index 0000000000..84f4a15d14 --- /dev/null +++ b/python/mozbuild/mozbuild/action/download_wpt_manifest.py @@ -0,0 +1,21 @@ +# 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/. + +# This action is used to generate the wpt manifest + +import sys + +import buildconfig + + +def main(): + print("Downloading wpt manifest") + sys.path.insert(0, buildconfig.topsrcdir) + import manifestupdate + + return 0 if manifestupdate.run(buildconfig.topsrcdir, buildconfig.topobjdir) else 1 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/python/mozbuild/mozbuild/action/dump_env.py b/python/mozbuild/mozbuild/action/dump_env.py new file mode 100644 index 0000000000..ec178700eb --- /dev/null +++ b/python/mozbuild/mozbuild/action/dump_env.py @@ -0,0 +1,30 @@ +# 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/. + +# We invoke a Python program to dump our environment in order to get +# native paths printed on Windows so that these paths can be incorporated +# into Python configure's environment. +import os +import sys + +sys.path.append(os.path.join(os.path.dirname(__file__), "..")) + +from shellutil import quote + + +def environ(): + # We would use six.ensure_text but the global Python isn't guaranteed to have + # the correct version of six installed. + def ensure_text(s): + if sys.version_info > (3, 0) or isinstance(s, unicode): + # os.environ always returns string keys and values in Python 3. + return s + else: + return s.decode("utf-8") + + return [(ensure_text(k), ensure_text(v)) for (k, v) in os.environ.items()] + + +for key, value in environ(): + print("%s=%s" % (key, quote(value))) diff --git a/python/mozbuild/mozbuild/action/dumpsymbols.py b/python/mozbuild/mozbuild/action/dumpsymbols.py new file mode 100644 index 0000000000..0af2c1c4e5 --- /dev/null +++ b/python/mozbuild/mozbuild/action/dumpsymbols.py @@ -0,0 +1,109 @@ +# 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/. + +import argparse +import os +import shutil +import subprocess +import sys + +import buildconfig + + +def dump_symbols(target, tracking_file, count_ctors=False): + # Our tracking file, if present, will contain path(s) to the previously generated + # symbols. Remove them in this case so we don't simply accumulate old symbols + # during incremental builds. + if os.path.isfile(os.path.normpath(tracking_file)): + with open(tracking_file, "r") as fh: + files = fh.read().splitlines() + dirs = set(os.path.dirname(f) for f in files) + for d in dirs: + shutil.rmtree( + os.path.join(buildconfig.topobjdir, "dist", "crashreporter-symbols", d), + ignore_errors=True, + ) + + # Build default args for symbolstore.py based on platform. + sym_store_args = [] + + dump_syms_bin = buildconfig.substs["DUMP_SYMS"] + os_arch = buildconfig.substs["OS_ARCH"] + if os_arch == "WINNT": + sym_store_args.extend(["-c", "--vcs-info"]) + if "PDBSTR" in buildconfig.substs: + sym_store_args.append("-i") + elif os_arch == "Darwin": + cpu = { + "x86": "i386", + "aarch64": "arm64", + }.get(buildconfig.substs["TARGET_CPU"], buildconfig.substs["TARGET_CPU"]) + sym_store_args.extend(["-c", "-a", cpu, "--vcs-info"]) + elif os_arch == "Linux": + sym_store_args.extend(["-c", "--vcs-info"]) + + sym_store_args.append( + "--install-manifest=%s,%s" + % ( + os.path.join( + buildconfig.topobjdir, "_build_manifests", "install", "dist_include" + ), + os.path.join(buildconfig.topobjdir, "dist", "include"), + ) + ) + objcopy = buildconfig.substs.get("OBJCOPY") + if objcopy: + os.environ["OBJCOPY"] = objcopy + + if buildconfig.substs.get("MOZ_THUNDERBIRD"): + sym_store_args.extend(["-s", os.path.join(buildconfig.topsrcdir, "comm")]) + + args = ( + [ + sys.executable, + os.path.join( + buildconfig.topsrcdir, + "toolkit", + "crashreporter", + "tools", + "symbolstore.py", + ), + ] + + sym_store_args + + [ + "-s", + buildconfig.topsrcdir, + dump_syms_bin, + os.path.join(buildconfig.topobjdir, "dist", "crashreporter-symbols"), + os.path.abspath(target), + ] + ) + if count_ctors: + args.append("--count-ctors") + print("Running: %s" % " ".join(args)) + out_files = subprocess.check_output(args, universal_newlines=True) + with open(tracking_file, "w", encoding="utf-8", newline="\n") as fh: + fh.write(out_files) + fh.flush() + + +def main(argv): + parser = argparse.ArgumentParser( + usage="Usage: dumpsymbols.py <library or program> <tracking file>" + ) + parser.add_argument( + "--count-ctors", + action="store_true", + default=False, + help="Count static initializers", + ) + parser.add_argument("library_or_program", help="Path to library or program") + parser.add_argument("tracking_file", help="Tracking file") + args = parser.parse_args() + + return dump_symbols(args.library_or_program, args.tracking_file, args.count_ctors) + + +if __name__ == "__main__": + sys.exit(main(sys.argv[1:])) diff --git a/python/mozbuild/mozbuild/action/exe_7z_archive.py b/python/mozbuild/mozbuild/action/exe_7z_archive.py new file mode 100644 index 0000000000..b0d35be2bf --- /dev/null +++ b/python/mozbuild/mozbuild/action/exe_7z_archive.py @@ -0,0 +1,89 @@ +# 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/. + +import os +import shutil +import subprocess +import sys +import tempfile + +import buildconfig +import mozpack.path as mozpath + +from mozbuild.base import BuildEnvironmentNotFoundException + + +def archive_exe(pkg_dir, tagfile, sfx_package, package, use_upx): + tmpdir = tempfile.mkdtemp(prefix="tmp") + try: + if pkg_dir: + shutil.move(pkg_dir, "core") + + if use_upx: + final_sfx = mozpath.join(tmpdir, "7zSD.sfx") + upx = buildconfig.substs.get("UPX", "upx") + wine = buildconfig.substs.get("WINE") + if wine and upx.lower().endswith(".exe"): + cmd = [wine, upx] + else: + cmd = [upx] + subprocess.check_call( + cmd + + [ + "--best", + "-o", + final_sfx, + sfx_package, + ] + ) + else: + final_sfx = sfx_package + + try: + sevenz = buildconfig.config.substs["7Z"] + except BuildEnvironmentNotFoundException: + # configure hasn't been run, just use the default + sevenz = "7z" + subprocess.check_call( + [ + sevenz, + "a", + "-r", + "-t7z", + mozpath.join(tmpdir, "app.7z"), + "-mx", + "-m0=BCJ2", + "-m1=LZMA:d25", + "-m2=LZMA:d19", + "-m3=LZMA:d19", + "-mb0:1", + "-mb0s1:2", + "-mb0s2:3", + ] + ) + + with open(package, "wb") as o: + for i in [final_sfx, tagfile, mozpath.join(tmpdir, "app.7z")]: + shutil.copyfileobj(open(i, "rb"), o) + os.chmod(package, 0o0755) + finally: + if pkg_dir: + shutil.move("core", pkg_dir) + shutil.rmtree(tmpdir) + + +def main(args): + if len(args) != 4: + print( + "Usage: exe_7z_archive.py <pkg_dir> <tagfile> <sfx_package> <package> <use_upx>", + file=sys.stderr, + ) + return 1 + else: + archive_exe(args[0], args[1], args[2], args[3], args[4]) + return 0 + + +if __name__ == "__main__": + sys.exit(main(sys.argv[1:])) diff --git a/python/mozbuild/mozbuild/action/fat_aar.py b/python/mozbuild/mozbuild/action/fat_aar.py new file mode 100644 index 0000000000..d17d4696a0 --- /dev/null +++ b/python/mozbuild/mozbuild/action/fat_aar.py @@ -0,0 +1,185 @@ +# 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/. + +""" +Fetch and unpack architecture-specific Maven zips, verify cross-architecture +compatibility, and ready inputs to an Android multi-architecture fat AAR build. +""" + +import argparse +import sys +from collections import OrderedDict, defaultdict +from hashlib import sha1 # We don't need a strong hash to compare inputs. +from io import BytesIO +from zipfile import ZipFile + +import mozpack.path as mozpath +import six +from mozpack.copier import FileCopier +from mozpack.files import JarFinder +from mozpack.mozjar import JarReader +from mozpack.packager.unpack import UnpackFinder + + +def fat_aar(distdir, aars_paths, no_process=False, no_compatibility_check=False): + if no_process: + print("Not processing architecture-specific artifact Maven AARs.") + return 0 + + # Map {filename: {fingerprint: [arch1, arch2, ...]}}. + diffs = defaultdict(lambda: defaultdict(list)) + missing_arch_prefs = set() + # Collect multi-architecture inputs to the fat AAR. + copier = FileCopier() + + for arch, aar_path in aars_paths.items(): + # Map old non-architecture-specific path to new architecture-specific path. + old_rewrite_map = { + "greprefs.js": "{}/greprefs.js".format(arch), + "defaults/pref/geckoview-prefs.js": "defaults/pref/{}/geckoview-prefs.js".format( + arch + ), + } + + # Architecture-specific preferences files. + arch_prefs = set(old_rewrite_map.values()) + missing_arch_prefs |= set(arch_prefs) + + jar_finder = JarFinder(aar_path, JarReader(aar_path)) + for path, fileobj in UnpackFinder(jar_finder): + # Native libraries go straight through. + if mozpath.match(path, "jni/**"): + copier.add(path, fileobj) + + elif path in arch_prefs: + copier.add(path, fileobj) + + elif path in ("classes.jar", "annotations.zip"): + # annotations.zip differs due to timestamps, but the contents should not. + + # `JarReader` fails on the non-standard `classes.jar` produced by Gradle/aapt, + # and it's not worth working around, so we use Python's zip functionality + # instead. + z = ZipFile(BytesIO(fileobj.open().read())) + for r in z.namelist(): + fingerprint = sha1(z.open(r).read()).hexdigest() + diffs["{}!/{}".format(path, r)][fingerprint].append(arch) + + else: + fingerprint = sha1(six.ensure_binary(fileobj.open().read())).hexdigest() + # There's no need to distinguish `target.maven.zip` from `assets/omni.ja` here, + # since in practice they will never overlap. + diffs[path][fingerprint].append(arch) + + missing_arch_prefs.discard(path) + + # Some differences are allowed across the architecture-specific AARs. We could allow-list + # the actual content, but it's not necessary right now. + allow_pattern_list = { + "AndroidManifest.xml", # Min SDK version is different for 32- and 64-bit builds. + "classes.jar!/org/mozilla/gecko/util/HardwareUtils.class", # Min SDK as well. + "classes.jar!/org/mozilla/geckoview/BuildConfig.class", + # Each input captures its CPU architecture. + "chrome/toolkit/content/global/buildconfig.html", + # Bug 1556162: localized resources are not deterministic across + # per-architecture builds triggered from the same push. + "**/*.ftl", + "**/*.dtd", + "**/*.properties", + } + + not_allowed = OrderedDict() + + def format_diffs(ds): + # Like ' armeabi-v7a, arm64-v8a -> XXX\n x86, x86_64 -> YYY'. + return "\n".join( + sorted( + " {archs} -> {fingerprint}".format( + archs=", ".join(sorted(archs)), fingerprint=fingerprint + ) + for fingerprint, archs in ds.items() + ) + ) + + for p, ds in sorted(diffs.items()): + if len(ds) <= 1: + # Only one hash across all inputs: roll on. + continue + + if any(mozpath.match(p, pat) for pat in allow_pattern_list): + print( + 'Allowed: Path "{path}" has architecture-specific versions:\n{ds_repr}'.format( + path=p, ds_repr=format_diffs(ds) + ) + ) + continue + + not_allowed[p] = ds + + for p, ds in not_allowed.items(): + print( + 'Disallowed: Path "{path}" has architecture-specific versions:\n{ds_repr}'.format( + path=p, ds_repr=format_diffs(ds) + ) + ) + + for missing in sorted(missing_arch_prefs): + print( + "Disallowed: Inputs missing expected architecture-specific input: {missing}".format( + missing=missing + ) + ) + + if not no_compatibility_check and (missing_arch_prefs or not_allowed): + return 1 + + output_dir = mozpath.join(distdir, "output") + copier.copy(output_dir) + + return 0 + + +_ALL_ARCHS = ("armeabi-v7a", "arm64-v8a", "x86_64", "x86") + + +def main(argv): + description = """Unpack architecture-specific Maven AARs, verify cross-architecture +compatibility, and ready inputs to an Android multi-architecture fat AAR build.""" + + parser = argparse.ArgumentParser(description=description) + parser.add_argument( + "--no-process", action="store_true", help="Do not process Maven AARs." + ) + parser.add_argument( + "--no-compatibility-check", + action="store_true", + help="Do not fail if Maven AARs are not compatible.", + ) + parser.add_argument("--distdir", required=True) + + for arch in _ALL_ARCHS: + command_line_flag = arch.replace("_", "-") + parser.add_argument("--{}".format(command_line_flag), dest=arch) + + args = parser.parse_args(argv) + + args_dict = vars(args) + + aars_paths = { + arch: args_dict.get(arch) for arch in _ALL_ARCHS if args_dict.get(arch) + } + + if not aars_paths: + raise ValueError("You must provide at least one AAR file!") + + return fat_aar( + args.distdir, + aars_paths, + no_process=args.no_process, + no_compatibility_check=args.no_compatibility_check, + ) + + +if __name__ == "__main__": + sys.exit(main(sys.argv[1:])) diff --git a/python/mozbuild/mozbuild/action/file_generate.py b/python/mozbuild/mozbuild/action/file_generate.py new file mode 100644 index 0000000000..5cb479ea21 --- /dev/null +++ b/python/mozbuild/mozbuild/action/file_generate.py @@ -0,0 +1,154 @@ +# 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/. + +# Given a Python script and arguments describing the output file, and +# the arguments that can be used to generate the output file, call the +# script's |main| method with appropriate arguments. + +import argparse +import importlib.util +import os +import sys +import traceback + +import buildconfig +import six + +from mozbuild.makeutil import Makefile +from mozbuild.pythonutil import iter_modules_in_path +from mozbuild.util import FileAvoidWrite + + +def main(argv): + parser = argparse.ArgumentParser( + "Generate a file from a Python script", add_help=False + ) + parser.add_argument( + "--locale", metavar="locale", type=six.text_type, help="The locale in use." + ) + parser.add_argument( + "python_script", + metavar="python-script", + type=six.text_type, + help="The Python script to run", + ) + parser.add_argument( + "method_name", + metavar="method-name", + type=six.text_type, + help="The method of the script to invoke", + ) + parser.add_argument( + "output_file", + metavar="output-file", + type=six.text_type, + help="The file to generate", + ) + parser.add_argument( + "dep_file", + metavar="dep-file", + type=six.text_type, + help="File to write any additional make dependencies to", + ) + parser.add_argument( + "dep_target", + metavar="dep-target", + type=six.text_type, + help="Make target to use in the dependencies file", + ) + parser.add_argument( + "additional_arguments", + metavar="arg", + nargs=argparse.REMAINDER, + help="Additional arguments to the script's main() method", + ) + + args = parser.parse_args(argv) + + kwargs = {} + if args.locale: + kwargs["locale"] = args.locale + script = args.python_script + # Permit the script to import modules from the same directory in which it + # resides. The justification for doing this is that if we were invoking + # the script as: + # + # python script arg1... + # + # then importing modules from the script's directory would come for free. + # Since we're invoking the script in a roundabout way, we provide this + # bit of convenience. + sys.path.append(os.path.dirname(script)) + spec = importlib.util.spec_from_file_location("script", script) + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + method = args.method_name + if not hasattr(module, method): + print( + 'Error: script "{0}" is missing a {1} method'.format(script, method), + file=sys.stderr, + ) + return 1 + + ret = 1 + try: + with FileAvoidWrite(args.output_file, readmode="rb") as output: + try: + ret = module.__dict__[method]( + output, *args.additional_arguments, **kwargs + ) + except Exception: + # Ensure that we don't overwrite the file if the script failed. + output.avoid_writing_to_file() + raise + + # The following values indicate a statement of success: + # - a set() (see below) + # - 0 + # - False + # - None + # + # Everything else is an error (so scripts can conveniently |return + # 1| or similar). If a set is returned, the elements of the set + # indicate additional dependencies that will be listed in the deps + # file. Python module imports are automatically included as + # dependencies. + if isinstance(ret, set): + deps = set(six.ensure_text(s) for s in ret) + # The script succeeded, so reset |ret| to indicate that. + ret = None + else: + deps = set() + + # Only write out the dependencies if the script was successful + if not ret: + # Add dependencies on any python modules that were imported by + # the script. + deps |= set( + six.ensure_text(s) + for s in iter_modules_in_path( + buildconfig.topsrcdir, buildconfig.topobjdir + ) + ) + # Add dependencies on any buildconfig items that were accessed + # by the script. + deps |= set(six.ensure_text(s) for s in buildconfig.get_dependencies()) + + mk = Makefile() + mk.create_rule([args.dep_target]).add_dependencies(deps) + with FileAvoidWrite(args.dep_file) as dep_file: + mk.dump(dep_file) + else: + # Ensure that we don't overwrite the file if the script failed. + output.avoid_writing_to_file() + + except IOError as e: + print('Error opening file "{0}"'.format(e.filename), file=sys.stderr) + traceback.print_exc() + return 1 + return ret + + +if __name__ == "__main__": + sys.exit(main(sys.argv[1:])) diff --git a/python/mozbuild/mozbuild/action/file_generate_wrapper.py b/python/mozbuild/mozbuild/action/file_generate_wrapper.py new file mode 100644 index 0000000000..b6c030bbf6 --- /dev/null +++ b/python/mozbuild/mozbuild/action/file_generate_wrapper.py @@ -0,0 +1,38 @@ +# 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/. + +import json +import os +import subprocess +import sys +from pathlib import Path + +import buildconfig + + +def action(fh, script, target_dir, *args): + fh.close() + os.unlink(fh.name) + + args = list(args) + objdir = Path.cwd() + topsrcdir = Path(buildconfig.topsrcdir) + + def make_absolute(base_path, p): + return Path(base_path) / Path(p.lstrip("/")) + + try: + abs_target_dir = str(make_absolute(objdir, target_dir)) + abs_script = make_absolute(topsrcdir, script) + script = [str(abs_script)] + if abs_script.suffix == ".py": + script = [sys.executable] + script + subprocess.check_call(script + args, cwd=abs_target_dir) + except Exception: + relative = os.path.relpath(__file__, topsrcdir) + print( + "%s:action caught exception. params=%s\n" + % (relative, json.dumps([script, target_dir] + args, indent=2)) + ) + raise diff --git a/python/mozbuild/mozbuild/action/generate_symbols_file.py b/python/mozbuild/mozbuild/action/generate_symbols_file.py new file mode 100644 index 0000000000..955a676c08 --- /dev/null +++ b/python/mozbuild/mozbuild/action/generate_symbols_file.py @@ -0,0 +1,95 @@ +# 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/. + +import argparse +import os +from io import StringIO + +import buildconfig + +from mozbuild.preprocessor import Preprocessor +from mozbuild.util import DefinesAction + + +def generate_symbols_file(output, *args): + """ """ + parser = argparse.ArgumentParser() + parser.add_argument("input") + parser.add_argument("-D", action=DefinesAction) + parser.add_argument("-U", action="append", default=[]) + args = parser.parse_args(args) + input = os.path.abspath(args.input) + + pp = Preprocessor() + pp.context.update(buildconfig.defines["ALLDEFINES"]) + if args.D: + pp.context.update(args.D) + for undefine in args.U: + if undefine in pp.context: + del pp.context[undefine] + # Hack until MOZ_DEBUG_FLAGS are simply part of buildconfig.defines + if buildconfig.substs.get("MOZ_DEBUG"): + pp.context["DEBUG"] = "1" + # Ensure @DATA@ works as expected (see the Windows section further below) + if buildconfig.substs["OS_TARGET"] == "WINNT": + pp.context["DATA"] = "DATA" + else: + pp.context["DATA"] = "" + pp.out = StringIO() + pp.do_filter("substitution") + pp.do_include(input) + + symbols = [s.strip() for s in pp.out.getvalue().splitlines() if s.strip()] + + libname, ext = os.path.splitext(os.path.basename(output.name)) + + if buildconfig.substs["OS_TARGET"] == "WINNT": + # A def file is generated for MSVC link.exe that looks like the + # following: + # LIBRARY library.dll + # EXPORTS + # symbol1 + # symbol2 + # ... + # + # link.exe however requires special markers for data symbols, so in + # that case the symbols look like: + # data_symbol1 DATA + # data_symbol2 DATA + # ... + # + # In the input file, this is just annotated with the following syntax: + # data_symbol1 @DATA@ + # data_symbol2 @DATA@ + # ... + # The DATA variable is "simply" expanded by the preprocessor, to + # nothing on non-Windows, such that we only get the symbol name on + # those platforms, and to DATA on Windows, so that the "DATA" part + # is, in fact, part of the symbol name as far as the symbols variable + # is concerned. + assert ext == ".def" + output.write("LIBRARY %s\nEXPORTS\n %s\n" % (libname, "\n ".join(symbols))) + elif ( + buildconfig.substs.get("GCC_USE_GNU_LD") + or buildconfig.substs["OS_TARGET"] == "SunOS" + ): + # A linker version script is generated for GNU LD that looks like the + # following: + # liblibrary.so { + # global: + # symbol1; + # symbol2; + # ... + # local: + # *; + # }; + output.write( + "%s {\nglobal:\n %s;\nlocal:\n *;\n};" % (libname, ";\n ".join(symbols)) + ) + elif buildconfig.substs["OS_TARGET"] == "Darwin": + # A list of symbols is generated for Apple ld that simply lists all + # symbols, with an underscore prefix. + output.write("".join("_%s\n" % s for s in symbols)) + + return set(pp.includes) diff --git a/python/mozbuild/mozbuild/action/html_fragment_preprocesor.py b/python/mozbuild/mozbuild/action/html_fragment_preprocesor.py new file mode 100644 index 0000000000..f957318a7f --- /dev/null +++ b/python/mozbuild/mozbuild/action/html_fragment_preprocesor.py @@ -0,0 +1,101 @@ +import json +import re +import xml.etree.ElementTree as ET +from pathlib import Path + +JS_FILE_TEMPLATE = """\ +/* 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/. */ + +const EXPORTED_SYMBOLS = ["getHTMLFragment"]; + +const Fragments = {json_string}; + +/* + * Loads HTML fragment strings pulled from fragment documents. + * @param key - key identifying HTML fragment + * + * @return raw HTML/XHTML string + */ +const getHTMLFragment = key => Fragments[key]; +""" + +RE_COLLAPSE_WHITESPACE = re.compile(r"\s+") + + +def get_fragment_key(path, template_name=None): + key = Path(path).stem + if template_name: + key += "/" + template_name + return key + + +def fill_html_fragments_map(fragment_map, path, template, doctype=None): + # collape white space + for elm in template.iter(): + if elm.text: + elm.text = RE_COLLAPSE_WHITESPACE.sub(" ", elm.text) + if elm.tail: + elm.tail = RE_COLLAPSE_WHITESPACE.sub(" ", elm.tail) + key = get_fragment_key(path, template.attrib.get("name")) + xml = "".join(ET.tostring(elm, encoding="unicode") for elm in template).strip() + if doctype: + xml = doctype + "\n" + xml + fragment_map[key] = xml + + +def get_html_fragments_from_file(fragment_map, path): + for _, (name, value) in ET.iterparse(path, events=["start-ns"]): + ET.register_namespace(name, value) + tree = ET.parse(path) + root = tree.getroot() + sub_templates = root.findall("{http://www.w3.org/1999/xhtml}template") + # if all nested nodes are templates then treat as list of templates + if len(sub_templates) == len(root): + doctype = "" + for template in sub_templates: + if template.get("doctype") == "true": + doctype = template.text.strip() + break + for template in sub_templates: + if template.get("doctype") != "true": + fill_html_fragments_map(fragment_map, path, template, doctype) + else: + fill_html_fragments_map(fragment_map, path, root, None) + + +def generate(output, *inputs): + """Builds an html fragments loader JS file from the input xml file(s) + + The xml files are expected to be in the format of: + `<template>...xhtml markup...</template>` + + or `<template><template name="fragment_name">...xhtml markup...</template>...</template>` + Where there are multiple templates. All markup is expected to be properly namespaced. + + In the JS file, calling getHTMLFragment(key) will return the HTML string from the xml file + that matches the key. + + The key format is `filename_without_extension/template_name` for files with + multiple templates, or just `filename_without_extension` for files with one template. + `filename_without_extension` is the xml filename without the .xml extension + and `template_name` is the name attribute of template node containing the xml fragment. + + Arguments: + output -- File handle to JS file being generated + inputs -- list of xml filenames to include in loader + + Returns: + The set of dependencies which should trigger this command to be re-run. + This is ultimately returned to the build system for use by the backend + to ensure that incremental rebuilds happen when any dependency changes. + """ + + fragment_map = {} + for file in inputs: + get_html_fragments_from_file(fragment_map, file) + json_string = json.dumps(fragment_map, separators=(",", ":")) + contents = JS_FILE_TEMPLATE.format(json_string=json_string) + output.write(contents) + return set(inputs) diff --git a/python/mozbuild/mozbuild/action/install.py b/python/mozbuild/mozbuild/action/install.py new file mode 100644 index 0000000000..02f0f2694a --- /dev/null +++ b/python/mozbuild/mozbuild/action/install.py @@ -0,0 +1,22 @@ +# 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/. + +# A simple script to invoke mozinstall from the command line without depending +# on a build config. + +import sys + +import mozinstall + + +def main(args): + if len(args) != 2: + print("Usage: install.py [src] [dest]") + return 1 + src, dest = args + mozinstall.install(src, dest) + + +if __name__ == "__main__": + sys.exit(main(sys.argv[1:])) diff --git a/python/mozbuild/mozbuild/action/jar_maker.py b/python/mozbuild/mozbuild/action/jar_maker.py new file mode 100644 index 0000000000..72e9e97944 --- /dev/null +++ b/python/mozbuild/mozbuild/action/jar_maker.py @@ -0,0 +1,15 @@ +# 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/. + +import sys + +import mozbuild.jar + + +def main(args): + return mozbuild.jar.main(args) + + +if __name__ == "__main__": + sys.exit(main(sys.argv[1:])) diff --git a/python/mozbuild/mozbuild/action/l10n_merge.py b/python/mozbuild/mozbuild/action/l10n_merge.py new file mode 100644 index 0000000000..1a04d60107 --- /dev/null +++ b/python/mozbuild/mozbuild/action/l10n_merge.py @@ -0,0 +1,42 @@ +# 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/. + +import argparse +import os +import shutil +import sys + +from mozbuild.util import ensureParentDir + + +def main(argv): + parser = argparse.ArgumentParser(description="Merge l10n files.") + parser.add_argument("--output", help="Path to write merged output") + parser.add_argument("--ref-file", help="Path to reference file (en-US)") + parser.add_argument("--l10n-file", help="Path to locale file") + + args = parser.parse_args(argv) + + from compare_locales.compare import ContentComparer, Observer + from compare_locales.paths import File + + cc = ContentComparer([Observer()]) + cc.compare( + File(args.ref_file, args.ref_file, ""), + File(args.l10n_file, args.l10n_file, ""), + args.output, + ) + + ensureParentDir(args.output) + if not os.path.exists(args.output): + src = args.l10n_file + if not os.path.exists(args.l10n_file): + src = args.ref_file + shutil.copy(src, args.output) + + return 0 + + +if __name__ == "__main__": + main(sys.argv[1:]) diff --git a/python/mozbuild/mozbuild/action/langpack_localeNames.json b/python/mozbuild/mozbuild/action/langpack_localeNames.json new file mode 100644 index 0000000000..4e81a4c6dc --- /dev/null +++ b/python/mozbuild/mozbuild/action/langpack_localeNames.json @@ -0,0 +1,430 @@ +{ + "ach": { + "english": "Acoli", + "native": "Acholi" + }, + "af": { + "native": "Afrikaans" + }, + "an": { + "english": "Aragonese", + "native": "Aragonés" + }, + "ar": { + "english": "Arabic", + "native": "العربية" + }, + "ast": { + "english": "Asturian", + "native": "Asturianu" + }, + "az": { + "english": "Azerbaijani", + "native": "AzÉ™rbaycanca" + }, + "be": { + "english": "Belarusian", + "native": "БеларуÑкаÑ" + }, + "bg": { + "english": "Bulgarian", + "native": "БългарÑки" + }, + "bn": { + "english": "Bangla", + "native": "বাংলা" + }, + "bo": { + "english": "Tibetan", + "native": "བོད་སà¾à½‘" + }, + "br": { + "english": "Breton", + "native": "Brezhoneg" + }, + "brx": { + "english": "Bodo", + "native": "बड़ो" + }, + "bs": { + "english": "Bosnian", + "native": "Bosanski" + }, + "ca": { + "english": "Catalan", + "native": "Català " + }, + "ca-valencia": { + "english": "Catalan, Valencian", + "native": "Català (Valencià )" + }, + "cak": { + "native": "Kaqchikel" + }, + "cs": { + "english": "Czech", + "native": "ÄŒeÅ¡tina" + }, + "cy": { + "english": "Welsh", + "native": "Cymraeg" + }, + "da": { + "english": "Danish", + "native": "Dansk" + }, + "de": { + "english": "German", + "native": "Deutsch" + }, + "dsb": { + "english": "Lower Sorbian", + "native": "Dolnoserbšćina" + }, + "el": { + "english": "Greek", + "native": "Ελληνικά" + }, + "en-CA": { + "native": "English (CA)" + }, + "en-GB": { + "native": "English (GB)" + }, + "en-US": { + "native": "English (US)" + }, + "eo": { + "native": "Esperanto" + }, + "es-AR": { + "english": "Spanish, Argentina", + "native": "Español (AR)" + }, + "es-CL": { + "english": "Spanish, Chile", + "native": "Español (CL)" + }, + "es-ES": { + "english": "Spanish, Spain", + "native": "Español (ES)" + }, + "es-MX": { + "english": "Spanish, Mexico", + "native": "Español (MX)" + }, + "et": { + "english": "Estonian", + "native": "Eesti" + }, + "eu": { + "english": "Basque", + "native": "Euskara" + }, + "fa": { + "english": "Persian", + "native": "Ùارسی" + }, + "ff": { + "english": "Fulah", + "native": "Pulaar" + }, + "fi": { + "english": "Finnish", + "native": "Suomi" + }, + "fr": { + "english": "French", + "native": "Français" + }, + "fur": { + "english": "Friulian", + "native": "Furlan" + }, + "fy-NL": { + "english": "Frisian", + "native": "Frysk" + }, + "ga-IE": { + "english": "Irish", + "native": "Gaeilge" + }, + "gd": { + "english": "Scottish Gaelic", + "native": "Gà idhlig" + }, + "gl": { + "english": "Galician", + "native": "Galego" + }, + "gn": { + "native": "Guarani" + }, + "gu-IN": { + "english": "Gujarati", + "native": "ગà«àªœàª°àª¾àª¤à«€" + }, + "he": { + "english": "Hebrew", + "native": "עברית" + }, + "hi-IN": { + "english": "Hindi", + "native": "हिनà¥à¤¦à¥€" + }, + "hr": { + "english": "Croatian", + "native": "Hrvatski" + }, + "hsb": { + "english": "Upper Sorbian", + "native": "Hornjoserbšćina" + }, + "hu": { + "english": "Hungarian", + "native": "Magyar" + }, + "hy-AM": { + "english": "Armenian", + "native": "Õ°Õ¡ÕµÕ¥Ö€Õ¥Õ¶" + }, + "ia": { + "native": "Interlingua" + }, + "id": { + "english": "Indonesian", + "native": "Indonesia" + }, + "is": { + "english": "Icelandic", + "native": "Islenska" + }, + "it": { + "english": "Italian", + "native": "Italiano" + }, + "ja": { + "english": "Japanese", + "native": "日本語" + }, + "ja-JP-mac": { + "english": "Japanese", + "native": "日本語" + }, + "ka": { + "english": "Georgian", + "native": "ქáƒáƒ თული" + }, + "kab": { + "english": "Kabyle", + "native": "Taqbaylit" + }, + "kk": { + "english": "Kazakh", + "native": "қазақ тілі" + }, + "km": { + "english": "Khmer", + "native": "ážáŸ’មែរ" + }, + "kn": { + "english": "Kannada", + "native": "ಕನà³à²¨à²¡" + }, + "ko": { + "english": "Korean", + "native": "í•œêµì–´" + }, + "lij": { + "english": "Ligurian", + "native": "Ligure" + }, + "lo": { + "english": "Lao", + "native": "ລາວ" + }, + "lt": { + "english": "Lithuanian", + "native": "Lietuvių" + }, + "ltg": { + "english": "Latgalian", + "native": "LatgalÄ«Å¡u" + }, + "lv": { + "english": "Latvian", + "native": "LatvieÅ¡u" + }, + "mk": { + "english": "Macedonian", + "native": "македонÑки" + }, + "ml": { + "english": "Malayalam", + "native": "മലയാളം" + }, + "mr": { + "english": "Marathi", + "native": "मराठी" + }, + "ms": { + "english": "Malay", + "native": "Melayu" + }, + "my": { + "english": "Burmese", + "native": "မြန်မာ" + }, + "nb-NO": { + "english": "Norwegian BokmÃ¥l", + "native": "Norsk BokmÃ¥l" + }, + "ne-NP": { + "english": "Nepali", + "native": "नेपाली" + }, + "nl": { + "english": "Dutch", + "native": "Nederlands" + }, + "nn-NO": { + "english": "Norwegian Nynorsk", + "native": "Nynorsk" + }, + "oc": { + "native": "Occitan" + }, + "or": { + "english": "Odia", + "native": "ଓଡ଼ିଆ" + }, + "pa-IN": { + "english": "Punjabi", + "native": "ਪੰਜਾਬੀ" + }, + "pl": { + "english": "Polish", + "native": "Polski" + }, + "pt-BR": { + "english": "Brazilian Portuguese", + "native": "Português (BR)" + }, + "pt-PT": { + "english": "Portuguese", + "native": "Português (PT)" + }, + "rm": { + "english": "Romansh", + "native": "Rumantsch" + }, + "ro": { + "english": "Romanian", + "native": "Română" + }, + "ru": { + "english": "Russian", + "native": "РуÑÑкий" + }, + "sat": { + "english": "Santali", + "native": "ᱥᱟᱱᱛᱟᱲᱤ" + }, + "sc": { + "english": "Sardinian", + "native": "Sardu" + }, + "sco": { + "native": "Scots" + }, + "si": { + "english": "Sinhala", + "native": "සිංහල" + }, + "sk": { + "english": "Slovak", + "native": "SlovenÄina" + }, + "sl": { + "english": "Slovenian", + "native": "SlovenÅ¡Äina" + }, + "son": { + "english": "Songhai", + "native": "SoÅ‹ay" + }, + "sq": { + "english": "Albanian", + "native": "Shqip" + }, + "sr": { + "english": "Serbian", + "native": "CрпÑки" + }, + "sv-SE": { + "english": "Swedish", + "native": "Svenska" + }, + "szl": { + "english": "Silesian", + "native": "ÅšlÅnsko" + }, + "ta": { + "english": "Tamil", + "native": "தமிழà¯" + }, + "te": { + "english": "Telugu", + "native": "తెలà±à°—à±" + }, + "tg": { + "english": "Tajik", + "native": "Тоҷикӣ" + }, + "th": { + "english": "Thai", + "native": "ไทย" + }, + "tl": { + "english": "Filipino", + "native": "Tagalog" + }, + "tr": { + "english": "Turkish", + "native": "Türkçe" + }, + "trs": { + "native": "Triqui" + }, + "uk": { + "english": "Ukrainian", + "native": "УкраїнÑька" + }, + "ur": { + "english": "Urdu", + "native": "اردو" + }, + "uz": { + "english": "Uzbek", + "native": "O‘zbek" + }, + "vi": { + "english": "Vietnamese", + "native": "Tiếng Việt" + }, + "wo": { + "native": "Wolof" + }, + "xh": { + "english": "Xhosa", + "native": "IsiXhosa" + }, + "zh-CN": { + "english": "Simplified Chinese", + "native": "简体ä¸æ–‡" + }, + "zh-TW": { + "english": "Traditional Chinese", + "native": "æ£é«”ä¸æ–‡" + } +} diff --git a/python/mozbuild/mozbuild/action/langpack_manifest.py b/python/mozbuild/mozbuild/action/langpack_manifest.py new file mode 100644 index 0000000000..ffe32f567e --- /dev/null +++ b/python/mozbuild/mozbuild/action/langpack_manifest.py @@ -0,0 +1,592 @@ +# 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/. + +### +# This script generates a web manifest JSON file based on the xpi-stage +# directory structure. It extracts data necessary to produce the complete +# manifest file for a language pack: +# from the `langpack-manifest.ftl` file in the locale directory; +# from chrome registry entries; +# and from other information in the `xpi-stage` directory. +### + +import argparse +import datetime +import io +import json +import logging +import os +import re +import sys +import time + +import fluent.syntax.ast as FTL +import mozpack.path as mozpath +import mozversioncontrol +import requests +from fluent.syntax.parser import FluentParser +from mozpack.chrome.manifest import Manifest, ManifestLocale, parse_manifest +from redo import retry + +from mozbuild.configure.util import Version + + +def write_file(path, content): + with io.open(path, "w", encoding="utf-8") as out: + out.write(content + "\n") + + +pushlog_api_url = "{0}/json-rev/{1}" + + +def get_build_date(): + """Return the current date or SOURCE_DATE_EPOCH, if set.""" + return datetime.datetime.utcfromtimestamp( + int(os.environ.get("SOURCE_DATE_EPOCH", time.time())) + ) + + +### +# Retrieves a UTC datetime of the push for the current commit from a +# mercurial clone directory. The SOURCE_DATE_EPOCH environment +# variable is honored, for reproducibility. +# +# Args: +# path (str) - path to a directory +# +# Returns: +# (datetime) - a datetime object +# +# Example: +# dt = get_dt_from_hg("/var/vcs/l10n-central/pl") +# dt == datetime(2017, 10, 11, 23, 31, 54, 0) +### +def get_dt_from_hg(path): + with mozversioncontrol.get_repository_object(path=path) as repo: + phase = repo._run("log", "-r", ".", "-T" "{phase}") + if phase.strip() != "public": + return get_build_date() + repo_url = repo._run("paths", "default") + repo_url = repo_url.strip().replace("ssh://", "https://") + repo_url = repo_url.replace("hg://", "https://") + cs = repo._run("log", "-r", ".", "-T" "{node}") + + url = pushlog_api_url.format(repo_url, cs) + session = requests.Session() + + def get_pushlog(): + try: + response = session.get(url) + response.raise_for_status() + except Exception as e: + msg = "Failed to retrieve push timestamp using {}\nError: {}".format(url, e) + raise Exception(msg) + + return response.json() + + data = retry(get_pushlog) + try: + date = data["pushdate"][0] + except KeyError as exc: + msg = "{}\ndata is: {}".format( + str(exc), json.dumps(data, indent=2, sort_keys=True) + ) + raise KeyError(msg) + + return datetime.datetime.utcfromtimestamp(date) + + +### +# Generates timestamp for a locale based on its path. +# If possible, will use the commit timestamp from HG repository, +# and if that fails, will generate the timestamp for `now`. +# +# The timestamp format is "{year}{month}{day}{hour}{minute}{second}" and +# the datetime stored in it is using UTC timezone. +# +# Args: +# path (str) - path to the locale directory +# +# Returns: +# (str) - a timestamp string +# +# Example: +# ts = get_timestamp_for_locale("/var/vcs/l10n-central/pl") +# ts == "20170914215617" +### +def get_timestamp_for_locale(path): + dt = None + if os.path.isdir(os.path.join(path, ".hg")): + dt = get_dt_from_hg(path) + + if dt is None: + dt = get_build_date() + + dt = dt.replace(microsecond=0) + return dt.strftime("%Y%m%d%H%M%S") + + +### +# Parses an FTL file into a key-value pair object. +# Does not support attributes, terms, variables, functions or selectors; +# only messages with values consisting of text elements and literals. +# +# Args: +# path (str) - a path to an FTL file +# +# Returns: +# (dict) - A mapping of message keys to formatted string values. +# Empty if the file at `path` was not found. +# +# Example: +# res = parse_flat_ftl('./browser/langpack-metadata.ftl') +# res == { +# 'langpack-title': 'Polski', +# 'langpack-creator': 'mozilla.org', +# 'langpack-contributors': 'Joe Solon, Suzy Solon' +# } +### +def parse_flat_ftl(path): + parser = FluentParser(with_spans=False) + try: + with open(path, encoding="utf-8") as file: + res = parser.parse(file.read()) + except FileNotFoundError as err: + logging.warning(err) + return {} + + result = {} + for entry in res.body: + if isinstance(entry, FTL.Message) and isinstance(entry.value, FTL.Pattern): + flat = "" + for elem in entry.value.elements: + if isinstance(elem, FTL.TextElement): + flat += elem.value + elif isinstance(elem.expression, FTL.Literal): + flat += elem.expression.parse()["value"] + else: + name = type(elem.expression).__name__ + raise Exception(f"Unsupported {name} for {entry.id.name} in {path}") + result[entry.id.name] = flat.strip() + return result + + +## +# Generates the title and description for the langpack. +# +# Uses data stored in a JSON file next to this source, +# which is expected to have the following format: +# Record<string, { native: string, english?: string }> +# +# If an English name is given and is different from the native one, +# it will be included in the description and, if within the character limits, +# also in the name. +# +# Length limit for names is 45 characters, for descriptions is 132, +# return values are truncated if needed. +# +# NOTE: If you're updating the native locale names, +# you should also update the data in +# toolkit/components/mozintl/mozIntl.sys.mjs. +# +# Args: +# app (str) - Application name +# locale (str) - Locale identifier +# +# Returns: +# (str, str) - Tuple of title and description +# +### +def get_title_and_description(app, locale): + dir = os.path.dirname(__file__) + with open(os.path.join(dir, "langpack_localeNames.json"), encoding="utf-8") as nf: + names = json.load(nf) + + nameCharLimit = 45 + descCharLimit = 132 + nameTemplate = "Language: {}" + descTemplate = "{} Language Pack for {}" + + if locale in names: + data = names[locale] + native = data["native"] + english = data["english"] if "english" in data else native + + if english != native: + title = nameTemplate.format(f"{native} ({english})") + if len(title) > nameCharLimit: + title = nameTemplate.format(native) + description = descTemplate.format(app, f"{native} ({locale}) – {english}") + else: + title = nameTemplate.format(native) + description = descTemplate.format(app, f"{native} ({locale})") + else: + title = nameTemplate.format(locale) + description = descTemplate.format(app, locale) + + return title[:nameCharLimit], description[:descCharLimit] + + +### +# Build the manifest author string based on the author string +# and optionally adding the list of contributors, if provided. +# +# Args: +# ftl (dict) - a key-value mapping of locale-specific strings +# +# Returns: +# (str) - a string to be placed in the author field of the manifest.json +# +# Example: +# s = get_author({ +# 'langpack-creator': 'mozilla.org', +# 'langpack-contributors': 'Joe Solon, Suzy Solon' +# }) +# s == 'mozilla.org (contributors: Joe Solon, Suzy Solon)' +### +def get_author(ftl): + author = ftl["langpack-creator"] if "langpack-creator" in ftl else "mozilla.org" + contrib = ftl["langpack-contributors"] if "langpack-contributors" in ftl else "" + if contrib: + return f"{author} (contributors: {contrib})" + else: + return author + + +## +# Converts the list of chrome manifest entry flags to the list of platforms +# for the langpack manifest. +# +# The list of result platforms is taken from AppConstants.platform. +# +# Args: +# flags (FlagList) - a list of Chrome Manifest entry flags +# +# Returns: +# (list) - a list of platform the entry applies to +# +# Example: +# str(flags) == "os==MacOS os==Windows" +# platforms = convert_entry_flags_to_platform_codes(flags) +# platforms == ['mac', 'win'] +# +# The method supports only `os` flag name and equality operator. +# It will throw if tried with other flags or operators. +### +def convert_entry_flags_to_platform_codes(flags): + if not flags: + return None + + ret = [] + for key in flags: + if key != "os": + raise Exception("Unknown flag name") + + for value in flags[key].values: + if value[0] != "==": + raise Exception("Inequality flag cannot be converted") + + if value[1] == "Android": + ret.append("android") + elif value[1] == "LikeUnix": + ret.append("linux") + elif value[1] == "Darwin": + ret.append("macosx") + elif value[1] == "WINNT": + ret.append("win") + else: + raise Exception("Unknown flag value {0}".format(value[1])) + + return ret + + +### +# Recursively parse a chrome manifest file appending new entries +# to the result list +# +# The function can handle two entry types: 'locale' and 'manifest' +# +# Args: +# path (str) - a path to a chrome manifest +# base_path (str) - a path to the base directory all chrome registry +# entries will be relative to +# chrome_entries (list) - a list to which entries will be appended to +# +# Example: +# +# chrome_entries = {} +# parse_manifest('./chrome.manifest', './', chrome_entries) +# +# chrome_entries == [ +# { +# 'type': 'locale', +# 'alias': 'devtools', +# 'locale': 'pl', +# 'platforms': null, +# 'path': 'chrome/pl/locale/pl/devtools/' +# }, +# { +# 'type': 'locale', +# 'alias': 'autoconfig', +# 'locale': 'pl', +# 'platforms': ['win', 'mac'], +# 'path': 'chrome/pl/locale/pl/autoconfig/' +# }, +# ] +### +def parse_chrome_manifest(path, base_path, chrome_entries): + for entry in parse_manifest(None, path): + if isinstance(entry, Manifest): + parse_chrome_manifest( + os.path.join(os.path.dirname(path), entry.relpath), + base_path, + chrome_entries, + ) + elif isinstance(entry, ManifestLocale): + entry_path = os.path.join( + os.path.relpath(os.path.dirname(path), base_path), entry.relpath + ) + chrome_entries.append( + { + "type": "locale", + "alias": entry.name, + "locale": entry.id, + "platforms": convert_entry_flags_to_platform_codes(entry.flags), + "path": mozpath.normsep(entry_path), + } + ) + else: + raise Exception("Unknown type {0}".format(entry.name)) + + +### +# Gets the version to use in the langpack. +# +# This uses the env variable MOZ_BUILD_DATE if it exists to expand the version +# to be unique in automation. +# +# Args: +# app_version - Application version +# +# Returns: +# str - Version to use +# +### +def get_version_maybe_buildid(app_version): + def _extract_numeric_part(part): + matches = re.compile("[^\d]").search(part) + if matches: + part = part[0 : matches.start()] + if len(part) == 0: + return "0" + return part + + parts = [_extract_numeric_part(part) for part in app_version.split(".")] + + buildid = os.environ.get("MOZ_BUILD_DATE") + if buildid and len(buildid) != 14: + print("Ignoring invalid MOZ_BUILD_DATE: %s" % buildid, file=sys.stderr) + buildid = None + + if buildid: + # Use simple versioning format, see: Bug 1793925 - The version string + # should start with: <firefox major>.<firefox minor> + version = ".".join(parts[0:2]) + # We then break the buildid into two version parts so that the full + # version looks like: <firefox major>.<firefox minor>.YYYYMMDD.HHmmss + date, time = buildid[:8], buildid[8:] + # Leading zeros are not allowed. + time = time.lstrip("0") + if len(time) == 0: + time = "0" + version = f"{version}.{date}.{time}" + else: + version = ".".join(parts) + + return version + + +### +# Generates a new web manifest dict with values specific for a language pack. +# +# Args: +# locstr (str) - A string with a comma separated list of locales +# for which resources are embedded in the +# language pack +# min_app_ver (str) - A minimum version of the application the language +# resources are for +# max_app_ver (str) - A maximum version of the application the language +# resources are for +# app_name (str) - The name of the application the language +# resources are for +# ftl (dict) - A dictionary of locale-specific strings +# chrome_entries (dict) - A dictionary of chrome registry entries +# +# Returns: +# (dict) - a web manifest +# +# Example: +# manifest = create_webmanifest( +# 'pl', +# '57.0', +# '57.0.*', +# 'Firefox', +# '/var/vcs/l10n-central', +# {'langpack-title': 'Polski'}, +# chrome_entries +# ) +# manifest == { +# 'languages': { +# 'pl': { +# 'version': '201709121481', +# 'chrome_resources': { +# 'alert': 'chrome/pl/locale/pl/alert/', +# 'branding': 'browser/chrome/pl/locale/global/', +# 'global-platform': { +# 'macosx': 'chrome/pl/locale/pl/global-platform/mac/', +# 'win': 'chrome/pl/locale/pl/global-platform/win/', +# 'linux': 'chrome/pl/locale/pl/global-platform/unix/', +# 'android': 'chrome/pl/locale/pl/global-platform/unix/', +# }, +# 'forms': 'browser/chrome/pl/locale/forms/', +# ... +# } +# } +# }, +# 'sources': { +# 'browser': { +# 'base_path': 'browser/' +# } +# }, +# 'browser_specific_settings': { +# 'gecko': { +# 'strict_min_version': '57.0', +# 'strict_max_version': '57.0.*', +# 'id': 'langpack-pl@mozilla.org', +# } +# }, +# 'version': '57.0', +# 'name': 'Polski Language Pack', +# ... +# } +### +def create_webmanifest( + locstr, + version, + min_app_ver, + max_app_ver, + app_name, + l10n_basedir, + langpack_eid, + ftl, + chrome_entries, +): + locales = list(map(lambda loc: loc.strip(), locstr.split(","))) + main_locale = locales[0] + title, description = get_title_and_description(app_name, main_locale) + author = get_author(ftl) + + manifest = { + "langpack_id": main_locale, + "manifest_version": 2, + "browser_specific_settings": { + "gecko": { + "id": langpack_eid, + "strict_min_version": min_app_ver, + "strict_max_version": max_app_ver, + } + }, + "name": title, + "description": description, + "version": get_version_maybe_buildid(version), + "languages": {}, + "sources": {"browser": {"base_path": "browser/"}}, + "author": author, + } + + cr = {} + for entry in chrome_entries: + if entry["type"] == "locale": + platforms = entry["platforms"] + if platforms: + if entry["alias"] not in cr: + cr[entry["alias"]] = {} + for platform in platforms: + cr[entry["alias"]][platform] = entry["path"] + else: + assert entry["alias"] not in cr + cr[entry["alias"]] = entry["path"] + else: + raise Exception("Unknown type {0}".format(entry["type"])) + + for loc in locales: + manifest["languages"][loc] = { + "version": get_timestamp_for_locale(os.path.join(l10n_basedir, loc)), + "chrome_resources": cr, + } + + return json.dumps(manifest, indent=2, ensure_ascii=False) + + +def main(args): + parser = argparse.ArgumentParser() + parser.add_argument( + "--locales", help="List of language codes provided by the langpack" + ) + parser.add_argument("--app-version", help="Version of the application") + parser.add_argument( + "--max-app-ver", help="Max version of the application the langpack is for" + ) + parser.add_argument( + "--app-name", help="Name of the application the langpack is for" + ) + parser.add_argument( + "--l10n-basedir", help="Base directory for locales used in the language pack" + ) + parser.add_argument( + "--langpack-eid", help="Language pack id to use for this locale" + ) + parser.add_argument( + "--metadata", + help="FTL file defining langpack metadata", + ) + parser.add_argument("--input", help="Langpack directory.") + + args = parser.parse_args(args) + + chrome_entries = [] + parse_chrome_manifest( + os.path.join(args.input, "chrome.manifest"), args.input, chrome_entries + ) + + ftl = parse_flat_ftl(args.metadata) + + # Mangle the app version to set min version (remove patch level) + min_app_version = args.app_version + if "a" not in min_app_version: # Don't mangle alpha versions + v = Version(min_app_version) + if args.app_name == "SeaMonkey": + # SeaMonkey is odd in that <major> hasn't changed for many years. + # So min is <major>.<minor>.0 + min_app_version = "{}.{}.0".format(v.major, v.minor) + else: + # Language packs should be minversion of {major}.0 + min_app_version = "{}.0".format(v.major) + + res = create_webmanifest( + args.locales, + args.app_version, + min_app_version, + args.max_app_ver, + args.app_name, + args.l10n_basedir, + args.langpack_eid, + ftl, + chrome_entries, + ) + write_file(os.path.join(args.input, "manifest.json"), res) + + +if __name__ == "__main__": + main(sys.argv[1:]) diff --git a/python/mozbuild/mozbuild/action/make_dmg.py b/python/mozbuild/mozbuild/action/make_dmg.py new file mode 100644 index 0000000000..6dc19450fb --- /dev/null +++ b/python/mozbuild/mozbuild/action/make_dmg.py @@ -0,0 +1,67 @@ +# 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/. + +import argparse +import platform +import sys +from pathlib import Path + +from mozpack import dmg + +from mozbuild.bootstrap import bootstrap_toolchain +from mozbuild.repackaging.application_ini import get_application_ini_value + +is_linux = platform.system() == "Linux" + + +def main(args): + parser = argparse.ArgumentParser( + description="Explode a DMG into its relevant files" + ) + + parser.add_argument("--dsstore", help="DSStore file from") + parser.add_argument("--background", help="Background file from") + parser.add_argument("--icon", help="Icon file from") + parser.add_argument("--volume-name", help="Disk image volume name") + + parser.add_argument("inpath", metavar="PATH_IN", help="Location of files to pack") + parser.add_argument("dmgfile", metavar="DMG_OUT", help="DMG File to create") + + options = parser.parse_args(args) + + extra_files = [] + if options.dsstore: + extra_files.append((options.dsstore, ".DS_Store")) + if options.background: + extra_files.append((options.background, ".background/background.png")) + if options.icon: + extra_files.append((options.icon, ".VolumeIcon.icns")) + + if options.volume_name: + volume_name = options.volume_name + else: + volume_name = get_application_ini_value( + options.inpath, "App", "CodeName", fallback="Name" + ) + + # Resolve required tools + dmg_tool = bootstrap_toolchain("dmg/dmg") + hfs_tool = bootstrap_toolchain("dmg/hfsplus") + mkfshfs_tool = bootstrap_toolchain("hfsplus/newfs_hfs") + + dmg.create_dmg( + source_directory=Path(options.inpath), + output_dmg=Path(options.dmgfile), + volume_name=volume_name, + extra_files=extra_files, + dmg_tool=dmg_tool, + hfs_tool=hfs_tool, + mkfshfs_tool=mkfshfs_tool, + ) + + return 0 + + +if __name__ == "__main__": + sys.exit(main(sys.argv[1:])) diff --git a/python/mozbuild/mozbuild/action/make_unzip.py b/python/mozbuild/mozbuild/action/make_unzip.py new file mode 100644 index 0000000000..e4d2902f53 --- /dev/null +++ b/python/mozbuild/mozbuild/action/make_unzip.py @@ -0,0 +1,25 @@ +# 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/. + +import subprocess +import sys + +import buildconfig + + +def make_unzip(package): + subprocess.check_call([buildconfig.substs["UNZIP"], package]) + + +def main(args): + if len(args) != 1: + print("Usage: make_unzip.py <package>", file=sys.stderr) + return 1 + else: + make_unzip(args[0]) + return 0 + + +if __name__ == "__main__": + sys.exit(main(sys.argv[1:])) diff --git a/python/mozbuild/mozbuild/action/node.py b/python/mozbuild/mozbuild/action/node.py new file mode 100644 index 0000000000..fca0745b80 --- /dev/null +++ b/python/mozbuild/mozbuild/action/node.py @@ -0,0 +1,137 @@ +# 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/. + +import pipes +import subprocess +import sys + +import buildconfig +import six + +SCRIPT_ALLOWLIST = [buildconfig.topsrcdir + "/devtools/client/shared/build/build.js"] + +ALLOWLIST_ERROR = """ +%s is not +in SCRIPT_ALLOWLIST in python/mozbuild/mozbuild/action/node.py. +Using NodeJS from moz.build is currently in beta, and node +scripts to be executed need to be added to the allowlist and +reviewed by a build peer so that we can get a better sense of +how support should evolve. (To consult a build peer, raise a +question in the #build channel at https://chat.mozilla.org.) +""" + + +def is_script_in_allowlist(script_path): + if script_path in SCRIPT_ALLOWLIST: + return True + + return False + + +def execute_node_cmd(node_cmd_list): + """Execute the given node command list. + + Arguments: + node_cmd_list -- a list of the command and arguments to be executed + + Returns: + The set of dependencies which should trigger this command to be re-run. + This is ultimately returned to the build system for use by the backend + to ensure that incremental rebuilds happen when any dependency changes. + + The node script is expected to output lines for all of the dependencies + to stdout, each prefixed by the string "dep:". These lines will make up + the returned set of dependencies. Any line not so-prefixed will simply be + printed to stderr instead. + """ + + try: + printable_cmd = " ".join(pipes.quote(arg) for arg in node_cmd_list) + print('Executing "{}"'.format(printable_cmd), file=sys.stderr) + sys.stderr.flush() + + # We need to redirect stderr to a pipe because + # https://github.com/nodejs/node/issues/14752 causes issues with make. + proc = subprocess.Popen( + node_cmd_list, stdout=subprocess.PIPE, stderr=subprocess.PIPE + ) + + stdout, stderr = proc.communicate() + retcode = proc.wait() + + if retcode != 0: + print(stderr, file=sys.stderr) + sys.stderr.flush() + sys.exit(retcode) + + # Process the node script output + # + # XXX Starting with an empty list means that node scripts can + # (intentionally or inadvertently) remove deps. Do we want this? + deps = [] + for line in stdout.splitlines(): + line = six.ensure_text(line) + if "dep:" in line: + deps.append(line.replace("dep:", "")) + else: + print(line, file=sys.stderr) + sys.stderr.flush() + + return set(deps) + + except subprocess.CalledProcessError as err: + # XXX On Mac (and elsewhere?) "OSError: [Errno 13] Permission denied" + # (at least sometimes) means "node executable not found". Can we + # disambiguate this from real "Permission denied" errors so that we + # can log such problems more clearly? + print( + """Failed with %s. Be sure to check that your mozconfig doesn't + have --disable-nodejs in it. If it does, try removing that line and + building again.""" + % str(err), + file=sys.stderr, + ) + sys.exit(1) + + +def generate(output, node_script, *files): + """Call the given node_script to transform the given modules. + + Arguments: + output -- a dummy file, used by the build system. Can be ignored. + node_script -- the script to be executed. Must be in the SCRIPT_ALLOWLIST + files -- files to be transformed, will be passed to the script as arguments + + Returns: + The set of dependencies which should trigger this command to be re-run. + This is ultimately returned to the build system for use by the backend + to ensure that incremental rebuilds happen when any dependency changes. + """ + + node_interpreter = buildconfig.substs.get("NODEJS") + if not node_interpreter: + print( + """NODEJS not set. Be sure to check that your mozconfig doesn't + have --disable-nodejs in it. If it does, try removing that line + and building again.""", + file=sys.stderr, + ) + sys.exit(1) + + node_script = six.ensure_text(node_script) + if not isinstance(node_script, six.text_type): + print( + "moz.build file didn't pass a valid node script name to execute", + file=sys.stderr, + ) + sys.exit(1) + + if not is_script_in_allowlist(node_script): + print(ALLOWLIST_ERROR % (node_script), file=sys.stderr) + sys.exit(1) + + node_cmd_list = [node_interpreter, node_script] + node_cmd_list.extend(files) + + return execute_node_cmd(node_cmd_list) diff --git a/python/mozbuild/mozbuild/action/package_generated_sources.py b/python/mozbuild/mozbuild/action/package_generated_sources.py new file mode 100644 index 0000000000..c2d4f99009 --- /dev/null +++ b/python/mozbuild/mozbuild/action/package_generated_sources.py @@ -0,0 +1,55 @@ +# 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/. + +import argparse +import os +import sys + +import buildconfig +import mozpack.path as mozpath +from mozpack.archive import create_tar_gz_from_files +from mozpack.files import BaseFile + +from mozbuild.generated_sources import get_generated_sources + + +def main(argv): + parser = argparse.ArgumentParser(description="Produce archive of generated sources") + parser.add_argument("outputfile", help="File to write output to") + args = parser.parse_args(argv) + + objdir_abspath = mozpath.abspath(buildconfig.topobjdir) + + def is_valid_entry(entry): + if isinstance(entry[1], BaseFile): + entry_abspath = mozpath.abspath(entry[1].path) + else: + entry_abspath = mozpath.abspath(entry[1]) + if not entry_abspath.startswith(objdir_abspath): + print( + "Warning: omitting generated source [%s] from archive" % entry_abspath, + file=sys.stderr, + ) + return False + # We allow gtest-related files not to be present when not linking gtest + # during the compile. + if ( + "LINK_GTEST_DURING_COMPILE" not in buildconfig.substs + and "/gtest/" in entry_abspath + and not os.path.exists(entry_abspath) + ): + print( + "Warning: omitting non-existing file [%s] from archive" % entry_abspath, + file=sys.stderr, + ) + return False + return True + + files = dict(filter(is_valid_entry, get_generated_sources())) + with open(args.outputfile, "wb") as fh: + create_tar_gz_from_files(fh, files, compresslevel=5) + + +if __name__ == "__main__": + sys.exit(main(sys.argv[1:])) diff --git a/python/mozbuild/mozbuild/action/preprocessor.py b/python/mozbuild/mozbuild/action/preprocessor.py new file mode 100644 index 0000000000..41b1ec832f --- /dev/null +++ b/python/mozbuild/mozbuild/action/preprocessor.py @@ -0,0 +1,23 @@ +# 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/. + +import sys + +from mozbuild.preprocessor import Preprocessor + + +def generate(output, *args): + pp = Preprocessor() + pp.out = output + pp.handleCommandLine(list(args), True) + return set(pp.includes) + + +def main(args): + pp = Preprocessor() + pp.handleCommandLine(args, True) + + +if __name__ == "__main__": + main(sys.argv[1:]) diff --git a/python/mozbuild/mozbuild/action/process_define_files.py b/python/mozbuild/mozbuild/action/process_define_files.py new file mode 100644 index 0000000000..83c1985449 --- /dev/null +++ b/python/mozbuild/mozbuild/action/process_define_files.py @@ -0,0 +1,115 @@ +# 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/. + +import argparse +import os +import re +import sys + +import mozpack.path as mozpath +from buildconfig import topobjdir, topsrcdir + +from mozbuild.backend.configenvironment import PartialConfigEnvironment + + +def process_define_file(output, input): + """Creates the given config header. A config header is generated by + taking the corresponding source file and replacing some *#define/#undef* + occurences: + + - "#undef NAME" is turned into "#define NAME VALUE" + - "#define NAME" is unchanged + - "#define NAME ORIGINAL_VALUE" is turned into "#define NAME VALUE" + - "#undef UNKNOWN_NAME" is turned into "/* #undef UNKNOWN_NAME */" + - Whitespaces are preserved. + + As a special rule, "#undef ALLDEFINES" is turned into "#define NAME + VALUE" for all the defined variables. + """ + + path = os.path.abspath(input) + + config = PartialConfigEnvironment(topobjdir) + + if mozpath.basedir( + path, [mozpath.join(topsrcdir, "js/src")] + ) and not config.substs.get("JS_STANDALONE"): + config = PartialConfigEnvironment(mozpath.join(topobjdir, "js", "src")) + + with open(path, "r") as input: + r = re.compile( + r"^\s*#\s*(?P<cmd>[a-z]+)(?:\s+(?P<name>\S+)(?:\s+(?P<value>\S+))?)?", re.U + ) + for l in input: + m = r.match(l) + if m: + cmd = m.group("cmd") + name = m.group("name") + value = m.group("value") + if name: + if name == "ALLDEFINES": + if cmd == "define": + raise Exception( + "`#define ALLDEFINES` is not allowed in a " + "CONFIGURE_DEFINE_FILE" + ) + + def define_for_name(name, val): + """WebRTC files like to define WINVER and _WIN32_WINNT + via the command line, which raises a mass of macro + redefinition warnings. Just handle those macros + specially here.""" + define = "#define {name} {val}".format(name=name, val=val) + if name in ("_WIN32_IE", "_WIN32_WINNT", "WIN32", "WINVER"): + return "#if !defined({name})\n{define}\n#endif".format( + name=name, define=define + ) + return define + + defines = "\n".join( + sorted( + define_for_name(name, val) + for name, val in config.defines["ALLDEFINES"].items() + ) + ) + l = l[: m.start("cmd") - 1] + defines + l[m.end("name") :] + elif cmd == "define": + if value and name in config.defines: + l = ( + l[: m.start("value")] + + str(config.defines[name]) + + l[m.end("value") :] + ) + elif cmd == "undef": + if name in config.defines: + l = ( + l[: m.start("cmd")] + + "define" + + l[m.end("cmd") : m.end("name")] + + " " + + str(config.defines[name]) + + l[m.end("name") :] + ) + else: + l = "/* " + l[: m.end("name")] + " */" + l[m.end("name") :] + + output.write(l) + + deps = {path} + deps.update(config.get_dependencies()) + return deps + + +def main(argv): + parser = argparse.ArgumentParser(description="Process define files.") + + parser.add_argument("input", help="Input define file.") + + args = parser.parse_args(argv) + + return process_define_file(sys.stdout, args.input) + + +if __name__ == "__main__": + sys.exit(main(sys.argv[1:])) diff --git a/python/mozbuild/mozbuild/action/process_install_manifest.py b/python/mozbuild/mozbuild/action/process_install_manifest.py new file mode 100644 index 0000000000..4db95ec7d3 --- /dev/null +++ b/python/mozbuild/mozbuild/action/process_install_manifest.py @@ -0,0 +1,123 @@ +# 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/. + +import argparse +import os +import sys +import time + +from mozpack.copier import FileCopier, FileRegistry +from mozpack.errors import errors +from mozpack.files import BaseFile, FileFinder +from mozpack.manifests import InstallManifest + +from mozbuild.util import DefinesAction + +COMPLETE = ( + "Elapsed: {elapsed:.2f}s; From {dest}: Kept {existing} existing; " + "Added/updated {updated}; " + "Removed {rm_files} files and {rm_dirs} directories." +) + + +def process_manifest(destdir, paths, track, no_symlinks=False, defines={}): + if os.path.exists(track): + # We use the same format as install manifests for the tracking + # data. + manifest = InstallManifest(path=track) + remove_unaccounted = FileRegistry() + dummy_file = BaseFile() + + finder = FileFinder(destdir, find_dotfiles=True) + for dest in manifest._dests: + for p, f in finder.find(dest): + remove_unaccounted.add(p, dummy_file) + + remove_empty_directories = True + remove_all_directory_symlinks = True + + else: + # If tracking is enabled and there is no file, we don't want to + # be removing anything. + remove_unaccounted = False + remove_empty_directories = False + remove_all_directory_symlinks = False + + manifest = InstallManifest() + for path in paths: + manifest |= InstallManifest(path=path) + + copier = FileCopier() + link_policy = "copy" if no_symlinks else "symlink" + manifest.populate_registry( + copier, defines_override=defines, link_policy=link_policy + ) + with errors.accumulate(): + result = copier.copy( + destdir, + remove_unaccounted=remove_unaccounted, + remove_all_directory_symlinks=remove_all_directory_symlinks, + remove_empty_directories=remove_empty_directories, + ) + + if track: + # We should record files that we actually copied. + # It is too late to expand wildcards when the track file is read. + manifest.write(path=track, expand_pattern=True) + + return result + + +def main(argv): + parser = argparse.ArgumentParser(description="Process install manifest files.") + + parser.add_argument("destdir", help="Destination directory.") + parser.add_argument("manifests", nargs="+", help="Path to manifest file(s).") + parser.add_argument( + "--no-symlinks", + action="store_true", + help="Do not install symbolic links. Always copy files", + ) + parser.add_argument( + "--track", + metavar="PATH", + required=True, + help="Use installed files tracking information from the given path.", + ) + parser.add_argument( + "-D", + action=DefinesAction, + dest="defines", + metavar="VAR[=VAL]", + help="Define a variable to override what is specified in the manifest", + ) + + args = parser.parse_args(argv) + + start = time.monotonic() + + result = process_manifest( + args.destdir, + args.manifests, + track=args.track, + no_symlinks=args.no_symlinks, + defines=args.defines, + ) + + elapsed = time.monotonic() - start + + print( + COMPLETE.format( + elapsed=elapsed, + dest=args.destdir, + existing=result.existing_files_count, + updated=result.updated_files_count, + rm_files=result.removed_files_count, + rm_dirs=result.removed_directories_count, + ) + ) + + +if __name__ == "__main__": + main(sys.argv[1:]) diff --git a/python/mozbuild/mozbuild/action/symbols_archive.py b/python/mozbuild/mozbuild/action/symbols_archive.py new file mode 100644 index 0000000000..75ecb71d17 --- /dev/null +++ b/python/mozbuild/mozbuild/action/symbols_archive.py @@ -0,0 +1,89 @@ +# 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/. + +import argparse +import os +import sys + +import mozpack.path as mozpath +from mozpack.files import FileFinder + + +def make_archive(archive_name, base, exclude, include): + compress = ["**/*.sym"] + finder = FileFinder(base, ignore=exclude) + if not include: + include = ["*"] + archive_basename = os.path.basename(archive_name) + + def fill_archive(add_file): + for pat in include: + for p, f in finder.find(pat): + print(' Adding to "%s":\n\t"%s"' % (archive_basename, p)) + add_file(p, f) + + with open(archive_name, "wb") as fh: + if archive_basename.endswith(".zip"): + from mozpack.mozjar import JarWriter + + with JarWriter(fileobj=fh, compress_level=5) as writer: + + def add_file(p, f): + should_compress = any(mozpath.match(p, pat) for pat in compress) + writer.add( + p.encode("utf-8"), + f, + mode=f.mode, + compress=should_compress, + skip_duplicates=True, + ) + + fill_archive(add_file) + elif archive_basename.endswith(".tar.zst"): + import tarfile + + import zstandard + + ctx = zstandard.ZstdCompressor(threads=-1) + with ctx.stream_writer(fh) as zstdwriter: + with tarfile.open( + mode="w|", fileobj=zstdwriter, bufsize=1024 * 1024 + ) as tar: + + def add_file(p, f): + info = tar.gettarinfo(os.path.join(base, p), p) + tar.addfile(info, f.open()) + + fill_archive(add_file) + else: + raise Exception( + "Unsupported archive format for {}".format(archive_basename) + ) + + +def main(argv): + parser = argparse.ArgumentParser(description="Produce a symbols archive") + parser.add_argument("archive", help="Which archive to generate") + parser.add_argument("base", help="Base directory to package") + parser.add_argument( + "--full-archive", action="store_true", help="Generate a full symbol archive" + ) + + args = parser.parse_args(argv) + + excludes = [] + includes = [] + + if args.full_archive: + # We allow symbols for tests to be included when building on try + if os.environ.get("MH_BRANCH", "unknown") != "try": + excludes = ["*test*", "*Test*"] + else: + includes = ["**/*.sym"] + + make_archive(args.archive, args.base, excludes, includes) + + +if __name__ == "__main__": + main(sys.argv[1:]) diff --git a/python/mozbuild/mozbuild/action/test_archive.py b/python/mozbuild/mozbuild/action/test_archive.py new file mode 100644 index 0000000000..1e2fcc15c9 --- /dev/null +++ b/python/mozbuild/mozbuild/action/test_archive.py @@ -0,0 +1,911 @@ +# 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/. + +# This action is used to produce test archives. +# +# Ideally, the data in this file should be defined in moz.build files. +# It is defined inline because this was easiest to make test archive +# generation faster. + +import argparse +import itertools +import os +import sys +import time + +import buildconfig +import mozpack.path as mozpath +from manifestparser import TestManifest +from mozpack.archive import create_tar_gz_from_files +from mozpack.copier import FileRegistry +from mozpack.files import ExistingFile, FileFinder +from mozpack.manifests import InstallManifest +from mozpack.mozjar import JarWriter +from reftest import ReftestManifest + +from mozbuild.util import ensureParentDir + +STAGE = mozpath.join(buildconfig.topobjdir, "dist", "test-stage") + +TEST_HARNESS_BINS = [ + "BadCertAndPinningServer", + "DelegatedCredentialsServer", + "EncryptedClientHelloServer", + "FaultyServer", + "GenerateOCSPResponse", + "OCSPStaplingServer", + "SanctionsTestServer", + "SmokeDMD", + "certutil", + "crashinject", + "geckodriver", + "http3server", + "content_analysis_sdk_agent", + "minidumpwriter", + "pk12util", + "screenshot", + "screentopng", + "ssltunnel", + "xpcshell", + "plugin-container", +] + +TEST_HARNESS_DLLS = ["crashinjectdll", "mozglue"] + +GMP_TEST_PLUGIN_DIRS = ["gmp-fake/**", "gmp-fakeopenh264/**"] + +# These entries will be used by artifact builds to re-construct an +# objdir with the appropriate generated support files. +OBJDIR_TEST_FILES = { + "xpcshell": { + "source": buildconfig.topobjdir, + "base": "_tests/xpcshell", + "pattern": "**", + "dest": "xpcshell/tests", + }, + "mochitest": { + "source": buildconfig.topobjdir, + "base": "_tests/testing", + "pattern": "mochitest/**", + }, +} + + +ARCHIVE_FILES = { + "common": [ + { + "source": STAGE, + "base": "", + "pattern": "**", + "ignore": [ + "cppunittest/**", + "condprof/**", + "gtest/**", + "mochitest/**", + "reftest/**", + "talos/**", + "raptor/**", + "awsy/**", + "web-platform/**", + "xpcshell/**", + "updater-dep/**", + "jsreftest/**", + "jit-test/**", + "jittest/**", # To make the ignore checker happy + "perftests/**", + "fuzztest/**", + ], + }, + {"source": buildconfig.topobjdir, "base": "_tests", "pattern": "modules/**"}, + { + "source": buildconfig.topsrcdir, + "base": "testing/marionette", + "patterns": ["client/**", "harness/**", "mach_test_package_commands.py"], + "dest": "marionette", + "ignore": ["client/docs", "harness/marionette_harness/tests"], + }, + { + "source": buildconfig.topsrcdir, + "base": "", + "manifests": [ + "testing/marionette/harness/marionette_harness/tests/unit-tests.toml" + ], + # We also need the manifests and harness_unit tests + "pattern": "testing/marionette/harness/marionette_harness/tests/**", + "dest": "marionette/tests", + }, + {"source": buildconfig.topobjdir, "base": "_tests", "pattern": "mozbase/**"}, + { + "source": buildconfig.topsrcdir, + "base": "testing", + "pattern": "firefox-ui/**", + "ignore": ["firefox-ui/tests"], + }, + { + "source": buildconfig.topsrcdir, + "base": "", + "pattern": "testing/firefox-ui/tests", + "dest": "firefox-ui/tests", + }, + { + "source": buildconfig.topsrcdir, + "base": "toolkit/components/telemetry/tests/marionette", + "pattern": "/**", + "dest": "telemetry/marionette", + }, + {"source": buildconfig.topsrcdir, "base": "testing", "pattern": "tps/**"}, + { + "source": buildconfig.topsrcdir, + "base": "services/sync/", + "pattern": "tps/**", + }, + { + "source": buildconfig.topsrcdir, + "base": "services/sync/tests/tps", + "pattern": "**", + "dest": "tps/tests", + }, + { + "source": buildconfig.topsrcdir, + "base": "testing/web-platform/tests/tools/wptserve", + "pattern": "**", + "dest": "tools/wptserve", + }, + { + "source": buildconfig.topsrcdir, + "base": "testing/web-platform/tests/tools/third_party", + "pattern": "**", + "dest": "tools/wpt_third_party", + }, + { + "source": buildconfig.topsrcdir, + "base": "python/mozterm", + "pattern": "**", + "dest": "tools/mozterm", + }, + { + "source": buildconfig.topsrcdir, + "base": "xpcom/geckoprocesstypes_generator", + "pattern": "**", + "dest": "tools/geckoprocesstypes_generator", + }, + { + "source": buildconfig.topsrcdir, + "base": "third_party/python/six", + "pattern": "**", + "dest": "tools/six", + }, + { + "source": buildconfig.topsrcdir, + "base": "third_party/python/distro", + "pattern": "**", + "dest": "tools/distro", + }, + {"source": buildconfig.topobjdir, "base": "", "pattern": "mozinfo.json"}, + { + "source": buildconfig.topobjdir, + "base": "dist/bin", + "patterns": [ + "%s%s" % (f, buildconfig.substs["BIN_SUFFIX"]) + for f in TEST_HARNESS_BINS + ] + + [ + "%s%s%s" + % ( + buildconfig.substs["DLL_PREFIX"], + f, + buildconfig.substs["DLL_SUFFIX"], + ) + for f in TEST_HARNESS_DLLS + ], + "dest": "bin", + }, + { + "source": buildconfig.topobjdir, + "base": "dist/bin", + "patterns": GMP_TEST_PLUGIN_DIRS, + "dest": "bin/plugins", + }, + { + "source": buildconfig.topobjdir, + "base": "dist/bin", + "patterns": ["dmd.py", "fix_stacks.py"], + "dest": "bin", + }, + { + "source": buildconfig.topobjdir, + "base": "dist/bin/components", + "patterns": ["httpd.sys.mjs"], + "dest": "bin/components", + }, + { + "source": buildconfig.topsrcdir, + "base": "build/pgo/certs", + "pattern": "**", + "dest": "certs", + }, + ], + "cppunittest": [ + {"source": STAGE, "base": "", "pattern": "cppunittest/**"}, + # We don't ship these files if startup cache is disabled, which is + # rare. But it shouldn't matter for test archives. + { + "source": buildconfig.topsrcdir, + "base": "startupcache/test", + "pattern": "TestStartupCacheTelemetry.*", + "dest": "cppunittest", + }, + { + "source": buildconfig.topsrcdir, + "base": "testing", + "pattern": "runcppunittests.py", + "dest": "cppunittest", + }, + { + "source": buildconfig.topsrcdir, + "base": "testing", + "pattern": "remotecppunittests.py", + "dest": "cppunittest", + }, + { + "source": buildconfig.topsrcdir, + "base": "testing", + "pattern": "cppunittest.toml", + "dest": "cppunittest", + }, + { + "source": buildconfig.topobjdir, + "base": "", + "pattern": "mozinfo.json", + "dest": "cppunittest", + }, + ], + "gtest": [{"source": STAGE, "base": "", "pattern": "gtest/**"}], + "mochitest": [ + OBJDIR_TEST_FILES["mochitest"], + { + "source": buildconfig.topobjdir, + "base": "_tests/testing", + "pattern": "mochitest/**", + }, + {"source": STAGE, "base": "", "pattern": "mochitest/**"}, + { + "source": buildconfig.topobjdir, + "base": "", + "pattern": "mozinfo.json", + "dest": "mochitest", + }, + { + "source": buildconfig.topobjdir, + "base": "dist/xpi-stage", + "pattern": "mochijar/**", + "dest": "mochitest", + }, + { + "source": buildconfig.topobjdir, + "base": "dist/xpi-stage", + "pattern": "specialpowers/**", + "dest": "mochitest/extensions", + }, + # Needed by Windows a11y browser tests. + { + "source": buildconfig.topobjdir, + "base": "accessible/interfaces/ia2", + "pattern": "IA2Typelib.tlb", + "dest": "mochitest", + }, + ], + "mozharness": [ + { + "source": buildconfig.topsrcdir, + "base": "testing/mozharness", + "pattern": "**", + }, + { + "source": buildconfig.topsrcdir, + "base": "", + "pattern": "third_party/python/_venv/**", + }, + { + "source": buildconfig.topsrcdir, + "base": "testing/mozbase/manifestparser", + "pattern": "manifestparser/**", + }, + { + "source": buildconfig.topsrcdir, + "base": "testing/mozbase/mozfile", + "pattern": "mozfile/**", + }, + { + "source": buildconfig.topsrcdir, + "base": "testing/mozbase/mozinfo", + "pattern": "mozinfo/**", + }, + { + "source": buildconfig.topsrcdir, + "base": "testing/mozbase/mozlog", + "pattern": "mozlog/**", + }, + { + "source": buildconfig.topsrcdir, + "base": "python/mozterm", + "pattern": "mozterm/**", + }, + { + "source": buildconfig.topsrcdir, + "base": "testing/mozbase/mozprocess", + "pattern": "mozprocess/**", + }, + { + "source": buildconfig.topsrcdir, + "base": "testing/mozbase/mozsystemmonitor", + "pattern": "mozsystemmonitor/**", + }, + { + "source": buildconfig.topsrcdir, + "base": "third_party/python/six", + "pattern": "six.py", + }, + { + "source": buildconfig.topsrcdir, + "base": "third_party/python/toml", + "pattern": "**", + }, + { + "source": buildconfig.topsrcdir, + "base": "third_party/python/tomlkit", + "pattern": "**", + }, + { + "source": buildconfig.topsrcdir, + "base": "third_party/python/distro", + "pattern": "distro/**", + }, + { + "source": buildconfig.topsrcdir, + "base": "third_party/python/packaging", + "pattern": "**", + }, + { + "source": buildconfig.topsrcdir, + "base": "python/mozbuild/mozbuild/action", + "pattern": "tooltool.py", + "dest": "external_tools", + }, + ], + "reftest": [ + {"source": buildconfig.topobjdir, "base": "_tests", "pattern": "reftest/**"}, + { + "source": buildconfig.topobjdir, + "base": "", + "pattern": "mozinfo.json", + "dest": "reftest", + }, + { + "source": buildconfig.topsrcdir, + "base": "", + "manifests": [ + "layout/reftests/reftest.list", + "layout/reftests/reftest-qr.list", + "testing/crashtest/crashtests.list", + ], + "dest": "reftest/tests", + }, + { + "source": buildconfig.topobjdir, + "base": "dist/xpi-stage", + "pattern": "reftest/**", + "dest": "reftest", + }, + { + "source": buildconfig.topobjdir, + "base": "dist/xpi-stage", + "pattern": "specialpowers/**", + "dest": "reftest", + }, + ], + "talos": [ + {"source": buildconfig.topsrcdir, "base": "testing", "pattern": "talos/**"}, + { + "source": buildconfig.topsrcdir, + "base": "testing/profiles", + "pattern": "**", + "dest": "talos/talos/profile_data", + }, + { + "source": buildconfig.topsrcdir, + "base": "third_party/webkit/PerformanceTests", + "pattern": "**", + "dest": "talos/talos/tests/webkit/PerformanceTests/", + }, + ], + "perftests": [ + {"source": buildconfig.topsrcdir, "pattern": "testing/mozbase/**"}, + {"source": buildconfig.topsrcdir, "pattern": "testing/condprofile/**"}, + {"source": buildconfig.topsrcdir, "pattern": "testing/performance/**"}, + {"source": buildconfig.topsrcdir, "pattern": "third_party/python/**"}, + {"source": buildconfig.topsrcdir, "pattern": "tools/lint/eslint/**"}, + {"source": buildconfig.topsrcdir, "pattern": "**/perftest_*.js"}, + {"source": buildconfig.topsrcdir, "pattern": "**/hooks_*py"}, + {"source": buildconfig.topsrcdir, "pattern": "build/autoconf/**"}, + {"source": buildconfig.topsrcdir, "pattern": "build/moz.configure/**"}, + {"source": buildconfig.topsrcdir, "pattern": "python/**"}, + {"source": buildconfig.topsrcdir, "pattern": "build/mach_initialize.py"}, + { + "source": buildconfig.topsrcdir, + "pattern": "python/sites/build.txt", + }, + { + "source": buildconfig.topsrcdir, + "pattern": "python/sites/common.txt", + }, + { + "source": buildconfig.topsrcdir, + "pattern": "python/sites/mach.txt", + }, + {"source": buildconfig.topsrcdir, "pattern": "mach/**"}, + { + "source": buildconfig.topsrcdir, + "pattern": "testing/web-platform/tests/tools/third_party/certifi/**", + }, + {"source": buildconfig.topsrcdir, "pattern": "testing/mozharness/**"}, + {"source": buildconfig.topsrcdir, "pattern": "browser/config/**"}, + { + "source": buildconfig.topobjdir, + "base": "_tests/modules", + "pattern": "**", + "dest": "bin/modules", + }, + { + "source": buildconfig.topobjdir, + "base": "dist/bin", + "patterns": [ + "browser/**", + "chrome/**", + "chrome.manifest", + "components/**", + "content_analysis_sdk_agent", + "http3server", + "*.ini", + "*.toml", + "localization/**", + "modules/**", + "update.locale", + "greprefs.js", + ], + "dest": "bin", + }, + { + "source": buildconfig.topsrcdir, + "base": "netwerk/test/http3serverDB", + "pattern": "**", + "dest": "netwerk/test/http3serverDB", + }, + ], + "condprof": [ + { + "source": buildconfig.topsrcdir, + "base": "testing", + "pattern": "condprofile/**", + }, + { + "source": buildconfig.topsrcdir, + "base": "testing/mozbase/mozfile", + "pattern": "**", + "dest": "condprofile/mozfile", + }, + { + "source": buildconfig.topsrcdir, + "base": "testing/mozbase/mozprofile", + "pattern": "**", + "dest": "condprofile/mozprofile", + }, + { + "source": buildconfig.topsrcdir, + "base": "testing/mozbase/mozdevice", + "pattern": "**", + "dest": "condprofile/mozdevice", + }, + { + "source": buildconfig.topsrcdir, + "base": "testing/mozbase/mozlog", + "pattern": "**", + "dest": "condprofile/mozlog", + }, + { + "source": buildconfig.topsrcdir, + "base": "third_party/python/virtualenv", + "pattern": "**", + "dest": "condprofile/virtualenv", + }, + ], + "raptor": [ + {"source": buildconfig.topsrcdir, "base": "testing", "pattern": "raptor/**"}, + { + "source": buildconfig.topsrcdir, + "base": "testing/profiles", + "pattern": "**", + "dest": "raptor/raptor/profile_data", + }, + { + "source": buildconfig.topsrcdir, + "base": "third_party/webkit/PerformanceTests", + "pattern": "**", + "dest": "raptor/raptor/tests/webkit/PerformanceTests/", + }, + ], + "awsy": [ + {"source": buildconfig.topsrcdir, "base": "testing", "pattern": "awsy/**"} + ], + "web-platform": [ + { + "source": buildconfig.topsrcdir, + "base": "testing", + "pattern": "web-platform/meta/**", + }, + { + "source": buildconfig.topsrcdir, + "base": "testing", + "pattern": "web-platform/mozilla/**", + }, + { + "source": buildconfig.topsrcdir, + "base": "testing", + "pattern": "web-platform/tests/**", + "ignore": ["web-platform/tests/tools/wpt_third_party"], + }, + { + "source": buildconfig.topobjdir, + "base": "_tests", + "pattern": "web-platform/**", + }, + { + "source": buildconfig.topobjdir, + "base": "", + "pattern": "mozinfo.json", + "dest": "web-platform", + }, + ], + "xpcshell": [ + OBJDIR_TEST_FILES["xpcshell"], + { + "source": buildconfig.topsrcdir, + "base": "testing/xpcshell", + "patterns": [ + "head.js", + "mach_test_package_commands.py", + "moz-http2/**", + "node-http2/**", + "node_ip/**", + "node-ws/**", + "dns-packet/**", + "remotexpcshelltests.py", + "runxpcshelltests.py", + "selftest.py", + "xpcshellcommandline.py", + ], + "dest": "xpcshell", + }, + {"source": STAGE, "base": "", "pattern": "xpcshell/**"}, + { + "source": buildconfig.topobjdir, + "base": "", + "pattern": "mozinfo.json", + "dest": "xpcshell", + }, + { + "source": buildconfig.topobjdir, + "base": "build", + "pattern": "automation.py", + "dest": "xpcshell", + }, + { + "source": buildconfig.topsrcdir, + "base": "testing/profiles", + "pattern": "**", + "dest": "xpcshell/profile_data", + }, + { + "source": buildconfig.topobjdir, + "base": "dist/bin", + "pattern": "http3server%s" % buildconfig.substs["BIN_SUFFIX"], + "dest": "xpcshell/http3server", + }, + { + "source": buildconfig.topsrcdir, + "base": "netwerk/test/http3serverDB", + "pattern": "**", + "dest": "xpcshell/http3server/http3serverDB", + }, + ], + "updater-dep": [ + { + "source": buildconfig.topobjdir, + "base": "_tests/updater-dep", + "pattern": "**", + "dest": "updater-dep", + }, + # Required by the updater on Linux + { + "source": buildconfig.topobjdir, + "base": "config/external/sqlite", + "pattern": "libmozsqlite3.so", + "dest": "updater-dep", + }, + ], + "jsreftest": [{"source": STAGE, "base": "", "pattern": "jsreftest/**"}], + "fuzztest": [ + {"source": buildconfig.topsrcdir, "pattern": "tools/fuzzing/smoke/**"} + ], + "jittest": [ + { + "source": buildconfig.topsrcdir, + "base": "js/src", + "pattern": "jit-test/**", + "dest": "jit-test", + }, + { + "source": buildconfig.topsrcdir, + "base": "js/src/tests", + "pattern": "non262/shell.js", + "dest": "jit-test/tests", + }, + { + "source": buildconfig.topsrcdir, + "base": "js/src/tests", + "pattern": "non262/Math/shell.js", + "dest": "jit-test/tests", + }, + { + "source": buildconfig.topsrcdir, + "base": "js/src/tests", + "pattern": "non262/reflect-parse/Match.js", + "dest": "jit-test/tests", + }, + { + "source": buildconfig.topsrcdir, + "base": "js/src/tests", + "pattern": "lib/**", + "dest": "jit-test/tests", + }, + { + "source": buildconfig.topsrcdir, + "base": "js/src", + "pattern": "jsapi.h", + "dest": "jit-test", + }, + ], +} + +if buildconfig.substs.get("MOZ_CODE_COVERAGE"): + ARCHIVE_FILES["common"].append( + { + "source": buildconfig.topsrcdir, + "base": "python/mozbuild/", + "patterns": ["mozpack/**", "mozbuild/codecoverage/**"], + } + ) + + +if buildconfig.substs.get("MOZ_ASAN") and buildconfig.substs.get("CLANG_CL"): + asan_dll = { + "source": buildconfig.topobjdir, + "base": "dist/bin", + "pattern": os.path.basename(buildconfig.substs["MOZ_CLANG_RT_ASAN_LIB_PATH"]), + "dest": "bin", + } + ARCHIVE_FILES["common"].append(asan_dll) + + +if buildconfig.substs.get("commtopsrcdir"): + commtopsrcdir = buildconfig.substs.get("commtopsrcdir") + mozharness_comm = { + "source": commtopsrcdir, + "base": "testing/mozharness", + "pattern": "**", + } + ARCHIVE_FILES["mozharness"].append(mozharness_comm) + marionette_comm = { + "source": commtopsrcdir, + "base": "", + "manifest": "testing/marionette/unit-tests.toml", + "dest": "marionette/tests/comm", + } + ARCHIVE_FILES["common"].append(marionette_comm) + thunderbirdinstance = { + "source": commtopsrcdir, + "base": "testing/marionette", + "pattern": "thunderbirdinstance.py", + "dest": "marionette/client/marionette_driver", + } + ARCHIVE_FILES["common"].append(thunderbirdinstance) + + +# "common" is our catch all archive and it ignores things from other archives. +# Verify nothing sneaks into ARCHIVE_FILES without a corresponding exclusion +# rule in the "common" archive. +for k, v in ARCHIVE_FILES.items(): + # Skip mozharness because it isn't staged. + if k in ("common", "mozharness"): + continue + + ignores = set( + itertools.chain(*(e.get("ignore", []) for e in ARCHIVE_FILES["common"])) + ) + + if not any(p.startswith("%s/" % k) for p in ignores): + raise Exception('"common" ignore list probably should contain %s' % k) + + +def find_generated_harness_files(): + # TEST_HARNESS_FILES end up in an install manifest at + # $topsrcdir/_build_manifests/install/_tests. + manifest = InstallManifest( + mozpath.join(buildconfig.topobjdir, "_build_manifests", "install", "_tests") + ) + registry = FileRegistry() + manifest.populate_registry(registry) + # Conveniently, the generated files we care about will already + # exist in the objdir, so we can identify relevant files if + # they're an `ExistingFile` instance. + return [ + mozpath.join("_tests", p) + for p in registry.paths() + if isinstance(registry[p], ExistingFile) + ] + + +def find_files(archive): + extra_entries = [] + generated_harness_files = find_generated_harness_files() + + if archive == "common": + # Construct entries ensuring all our generated harness files are + # packaged in the common tests archive. + packaged_paths = set() + for entry in OBJDIR_TEST_FILES.values(): + pat = mozpath.join(entry["base"], entry["pattern"]) + del entry["pattern"] + patterns = [] + for path in generated_harness_files: + if mozpath.match(path, pat): + patterns.append(path[len(entry["base"]) + 1 :]) + packaged_paths.add(path) + if patterns: + entry["patterns"] = patterns + extra_entries.append(entry) + entry = {"source": buildconfig.topobjdir, "base": "_tests", "patterns": []} + for path in set(generated_harness_files) - packaged_paths: + entry["patterns"].append(path[len("_tests") + 1 :]) + extra_entries.append(entry) + + for entry in ARCHIVE_FILES[archive] + extra_entries: + source = entry["source"] + dest = entry.get("dest") + base = entry.get("base", "") + + pattern = entry.get("pattern") + patterns = entry.get("patterns", []) + if pattern: + patterns.append(pattern) + + manifest = entry.get("manifest") + manifests = entry.get("manifests", []) + if manifest: + manifests.append(manifest) + if manifests: + dirs = find_manifest_dirs(os.path.join(source, base), manifests) + patterns.extend({"{}/**".format(d) for d in dirs}) + + ignore = list(entry.get("ignore", [])) + ignore.extend(["**/.flake8", "**/.mkdir.done", "**/*.pyc"]) + + if archive not in ("common", "updater-dep") and base.startswith("_tests"): + # We may have generated_harness_files to exclude from this entry. + for path in generated_harness_files: + if path.startswith(base): + ignore.append(path[len(base) + 1 :]) + + common_kwargs = {"find_dotfiles": True, "ignore": ignore} + + finder = FileFinder(os.path.join(source, base), **common_kwargs) + + for pattern in patterns: + for p, f in finder.find(pattern): + if dest: + p = mozpath.join(dest, p) + yield p, f + + +def find_manifest_dirs(topsrcdir, manifests): + """Routine to retrieve directories specified in a manifest, relative to topsrcdir. + + It does not recurse into manifests, as we currently have no need for that. + """ + dirs = set() + + for p in manifests: + p = os.path.join(topsrcdir, p) + + if p.endswith(".ini") or p.endswith(".toml"): + test_manifest = TestManifest() + test_manifest.read(p) + dirs |= set([os.path.dirname(m) for m in test_manifest.manifests()]) + + elif p.endswith(".list"): + m = ReftestManifest() + m.load(p) + dirs |= m.dirs + + else: + raise Exception( + '"{}" is not a supported manifest format.'.format( + os.path.splitext(p)[1] + ) + ) + + dirs = {mozpath.normpath(d[len(topsrcdir) :]).lstrip("/") for d in dirs} + + # Filter out children captured by parent directories because duplicates + # will confuse things later on. + def parents(p): + while True: + p = mozpath.dirname(p) + if not p: + break + yield p + + seen = set() + for d in sorted(dirs, key=len): + if not any(p in seen for p in parents(d)): + seen.add(d) + + return sorted(seen) + + +def main(argv): + parser = argparse.ArgumentParser(description="Produce test archives") + parser.add_argument("archive", help="Which archive to generate") + parser.add_argument("outputfile", help="File to write output to") + + args = parser.parse_args(argv) + + out_file = args.outputfile + if not out_file.endswith((".tar.gz", ".zip")): + raise Exception("expected tar.gz or zip output file") + + file_count = 0 + t_start = time.monotonic() + ensureParentDir(out_file) + res = find_files(args.archive) + with open(out_file, "wb") as fh: + # Experimentation revealed that level 5 is significantly faster and has + # marginally larger sizes than higher values and is the sweet spot + # for optimal compression. Read the detailed commit message that + # introduced this for raw numbers. + if out_file.endswith(".tar.gz"): + files = dict(res) + create_tar_gz_from_files(fh, files, compresslevel=5) + file_count = len(files) + elif out_file.endswith(".zip"): + with JarWriter(fileobj=fh, compress_level=5) as writer: + for p, f in res: + writer.add( + p.encode("utf-8"), f.read(), mode=f.mode, skip_duplicates=True + ) + file_count += 1 + else: + raise Exception("unhandled file extension: %s" % out_file) + + duration = time.monotonic() - t_start + zip_size = os.path.getsize(args.outputfile) + basename = os.path.basename(args.outputfile) + print( + "Wrote %d files in %d bytes to %s in %.2fs" + % (file_count, zip_size, basename, duration) + ) + + +if __name__ == "__main__": + sys.exit(main(sys.argv[1:])) diff --git a/python/mozbuild/mozbuild/action/tooltool.py b/python/mozbuild/mozbuild/action/tooltool.py new file mode 100755 index 0000000000..6b53db31e8 --- /dev/null +++ b/python/mozbuild/mozbuild/action/tooltool.py @@ -0,0 +1,1699 @@ +#!/usr/bin/env python3 + +# tooltool is a lookaside cache implemented in Python +# Copyright (C) 2011 John H. Ford <john@johnford.info> +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation version 2 +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA +# 02110-1301, USA. + +# A manifest file specifies files in that directory that are stored +# elsewhere. This file should only list files in the same directory +# in which the manifest file resides and it should be called +# 'manifest.tt' + +import base64 +import calendar +import hashlib +import hmac +import json +import logging +import math +import optparse +import os +import pprint +import re +import shutil +import ssl +import stat +import sys +import tarfile +import tempfile +import threading +import time +import zipfile +from contextlib import closing, contextmanager +from functools import wraps +from io import open +from random import random +from subprocess import PIPE, Popen + +if os.name == "nt": + import certifi + +__version__ = "1.4.0" + +# Allowed request header characters: +# !#$%&'()*+,-./:;<=>?@[]^_`{|}~ and space, a-z, A-Z, 0-9, \, " +REQUEST_HEADER_ATTRIBUTE_CHARS = re.compile( + r"^[ a-zA-Z0-9_\!#\$%&'\(\)\*\+,\-\./\:;<\=>\?@\[\]\^`\{\|\}~]*$" +) +DEFAULT_MANIFEST_NAME = "manifest.tt" +TOOLTOOL_PACKAGE_SUFFIX = ".TOOLTOOL-PACKAGE" +HAWK_VER = 1 +PY3 = sys.version_info[0] == 3 + +if PY3: + six_binary_type = bytes + unicode = ( + str # Silence `pyflakes` from reporting `undefined name 'unicode'` in Python 3. + ) + import urllib.request as urllib2 + from http.client import HTTPConnection, HTTPSConnection + from urllib.error import HTTPError, URLError + from urllib.parse import urljoin, urlparse + from urllib.request import Request +else: + six_binary_type = str + import urllib2 + from httplib import HTTPConnection, HTTPSConnection + from urllib2 import HTTPError, Request, URLError + from urlparse import urljoin, urlparse + + +log = logging.getLogger(__name__) + + +# Vendored code from `redo` module +def retrier(attempts=5, sleeptime=10, max_sleeptime=300, sleepscale=1.5, jitter=1): + """ + This function originates from redo 2.0.3 https://github.com/mozilla-releng/redo + A generator function that sleeps between retries, handles exponential + backoff and jitter. The action you are retrying is meant to run after + retrier yields. + """ + jitter = jitter or 0 # py35 barfs on the next line if jitter is None + if jitter > sleeptime: + # To prevent negative sleep times + raise Exception( + "jitter ({}) must be less than sleep time ({})".format(jitter, sleeptime) + ) + + sleeptime_real = sleeptime + for _ in range(attempts): + log.debug("attempt %i/%i", _ + 1, attempts) + + yield sleeptime_real + + if jitter: + sleeptime_real = sleeptime + random.uniform(-jitter, jitter) + # our jitter should scale along with the sleeptime + jitter = jitter * sleepscale + else: + sleeptime_real = sleeptime + + sleeptime *= sleepscale + + if sleeptime_real > max_sleeptime: + sleeptime_real = max_sleeptime + + # Don't need to sleep the last time + if _ < attempts - 1: + log.debug( + "sleeping for %.2fs (attempt %i/%i)", sleeptime_real, _ + 1, attempts + ) + time.sleep(sleeptime_real) + + +def retry( + action, + attempts=5, + sleeptime=60, + max_sleeptime=5 * 60, + sleepscale=1.5, + jitter=1, + retry_exceptions=(Exception,), + cleanup=None, + args=(), + kwargs={}, + log_args=True, +): + """ + This function originates from redo 2.0.3 https://github.com/mozilla-releng/redo + Calls an action function until it succeeds, or we give up. + """ + assert callable(action) + assert not cleanup or callable(cleanup) + + action_name = getattr(action, "__name__", action) + if log_args and (args or kwargs): + log_attempt_args = ( + "retry: calling %s with args: %s," " kwargs: %s, attempt #%d", + action_name, + args, + kwargs, + ) + else: + log_attempt_args = ("retry: calling %s, attempt #%d", action_name) + + if max_sleeptime < sleeptime: + log.debug("max_sleeptime %d less than sleeptime %d", max_sleeptime, sleeptime) + + n = 1 + for _ in retrier( + attempts=attempts, + sleeptime=sleeptime, + max_sleeptime=max_sleeptime, + sleepscale=sleepscale, + jitter=jitter, + ): + try: + logfn = log.info if n != 1 else log.debug + logfn_args = log_attempt_args + (n,) + logfn(*logfn_args) + return action(*args, **kwargs) + except retry_exceptions: + log.debug("retry: Caught exception: ", exc_info=True) + if cleanup: + cleanup() + if n == attempts: + log.info("retry: Giving up on %s", action_name) + raise + continue + finally: + n += 1 + + +def retriable(*retry_args, **retry_kwargs): + """ + This function originates from redo 2.0.3 https://github.com/mozilla-releng/redo + A decorator factory for retry(). Wrap your function in @retriable(...) to + give it retry powers! + """ + + def _retriable_factory(func): + @wraps(func) + def _retriable_wrapper(*args, **kwargs): + return retry(func, args=args, kwargs=kwargs, *retry_args, **retry_kwargs) + + return _retriable_wrapper + + return _retriable_factory + + +# end of vendored code from redo module + + +def request_has_data(req): + if PY3: + return req.data is not None + return req.has_data() + + +def get_hexdigest(val): + return hashlib.sha512(val).hexdigest() + + +class FileRecordJSONEncoderException(Exception): + pass + + +class InvalidManifest(Exception): + pass + + +class ExceptionWithFilename(Exception): + def __init__(self, filename): + Exception.__init__(self) + self.filename = filename + + +class BadFilenameException(ExceptionWithFilename): + pass + + +class DigestMismatchException(ExceptionWithFilename): + pass + + +class MissingFileException(ExceptionWithFilename): + pass + + +class InvalidCredentials(Exception): + pass + + +class BadHeaderValue(Exception): + pass + + +def parse_url(url): + url_parts = urlparse(url) + url_dict = { + "scheme": url_parts.scheme, + "hostname": url_parts.hostname, + "port": url_parts.port, + "path": url_parts.path, + "resource": url_parts.path, + "query": url_parts.query, + } + if len(url_dict["query"]) > 0: + url_dict["resource"] = "%s?%s" % ( + url_dict["resource"], # pragma: no cover + url_dict["query"], + ) + + if url_parts.port is None: + if url_parts.scheme == "http": + url_dict["port"] = 80 + elif url_parts.scheme == "https": # pragma: no cover + url_dict["port"] = 443 + return url_dict + + +def utc_now(offset_in_seconds=0.0): + return int(math.floor(calendar.timegm(time.gmtime()) + float(offset_in_seconds))) + + +def random_string(length): + return base64.urlsafe_b64encode(os.urandom(length))[:length] + + +def prepare_header_val(val): + if isinstance(val, six_binary_type): + val = val.decode("utf-8") + + if not REQUEST_HEADER_ATTRIBUTE_CHARS.match(val): + raise BadHeaderValue( # pragma: no cover + "header value value={val} contained an illegal character".format( + val=repr(val) + ) + ) + + return val + + +def parse_content_type(content_type): # pragma: no cover + if content_type: + return content_type.split(";")[0].strip().lower() + else: + return "" + + +def calculate_payload_hash(algorithm, payload, content_type): # pragma: no cover + parts = [ + part if isinstance(part, six_binary_type) else part.encode("utf8") + for part in [ + "hawk." + str(HAWK_VER) + ".payload\n", + parse_content_type(content_type) + "\n", + payload or "", + "\n", + ] + ] + + p_hash = hashlib.new(algorithm) + for p in parts: + p_hash.update(p) + + log.debug( + "calculating payload hash from:\n{parts}".format(parts=pprint.pformat(parts)) + ) + + return base64.b64encode(p_hash.digest()) + + +def validate_taskcluster_credentials(credentials): + if not hasattr(credentials, "__getitem__"): + raise InvalidCredentials( + "credentials must be a dict-like object" + ) # pragma: no cover + try: + credentials["clientId"] + credentials["accessToken"] + except KeyError: # pragma: no cover + etype, val, tb = sys.exc_info() + raise InvalidCredentials("{etype}: {val}".format(etype=etype, val=val)) + + +def normalize_header_attr(val): + if isinstance(val, six_binary_type): + return val.decode("utf-8") + return val # pragma: no cover + + +def normalize_string( + mac_type, + timestamp, + nonce, + method, + name, + host, + port, + content_hash, +): + return "\n".join( + [ + normalize_header_attr(header) + # The blank lines are important. They follow what the Node Hawk lib does. + for header in [ + "hawk." + str(HAWK_VER) + "." + mac_type, + timestamp, + nonce, + method or "", + name or "", + host, + port, + content_hash or "", + "", # for ext which is empty in this case + "", # Add trailing new line. + ] + ] + ) + + +def calculate_mac( + mac_type, + access_token, + algorithm, + timestamp, + nonce, + method, + name, + host, + port, + content_hash, +): + normalized = normalize_string( + mac_type, timestamp, nonce, method, name, host, port, content_hash + ) + log.debug("normalized resource for mac calc: {norm}".format(norm=normalized)) + digestmod = getattr(hashlib, algorithm) + + if not isinstance(normalized, six_binary_type): + normalized = normalized.encode("utf8") + + if not isinstance(access_token, six_binary_type): + access_token = access_token.encode("ascii") + + result = hmac.new(access_token, normalized, digestmod) + return base64.b64encode(result.digest()) + + +def make_taskcluster_header(credentials, req): + validate_taskcluster_credentials(credentials) + + url = req.get_full_url() + method = req.get_method() + algorithm = "sha256" + timestamp = str(utc_now()) + nonce = random_string(6) + url_parts = parse_url(url) + + content_hash = None + if request_has_data(req): + if PY3: + data = req.data + else: + data = req.get_data() + content_hash = calculate_payload_hash( # pragma: no cover + algorithm, + data, + # maybe we should detect this from req.headers but we anyway expect json + content_type="application/json", + ) + + mac = calculate_mac( + "header", + credentials["accessToken"], + algorithm, + timestamp, + nonce, + method, + url_parts["resource"], + url_parts["hostname"], + str(url_parts["port"]), + content_hash, + ) + + header = 'Hawk mac="{}"'.format(prepare_header_val(mac)) + + if content_hash: # pragma: no cover + header = '{}, hash="{}"'.format(header, prepare_header_val(content_hash)) + + header = '{header}, id="{id}", ts="{ts}", nonce="{nonce}"'.format( + header=header, + id=prepare_header_val(credentials["clientId"]), + ts=prepare_header_val(timestamp), + nonce=prepare_header_val(nonce), + ) + + log.debug("Hawk header for URL={} method={}: {}".format(url, method, header)) + + return header + + +class FileRecord(object): + def __init__( + self, + filename, + size, + digest, + algorithm, + unpack=False, + version=None, + visibility=None, + ): + object.__init__(self) + if "/" in filename or "\\" in filename: + log.error( + "The filename provided contains path information and is, therefore, invalid." + ) + raise BadFilenameException(filename=filename) + self.filename = filename + self.size = size + self.digest = digest + self.algorithm = algorithm + self.unpack = unpack + self.version = version + self.visibility = visibility + + def __eq__(self, other): + if self is other: + return True + if ( + self.filename == other.filename + and self.size == other.size + and self.digest == other.digest + and self.algorithm == other.algorithm + and self.version == other.version + and self.visibility == other.visibility + ): + return True + else: + return False + + def __ne__(self, other): + return not self.__eq__(other) + + def __str__(self): + return repr(self) + + def __repr__(self): + return ( + "%s.%s(filename='%s', size=%s, digest='%s', algorithm='%s', visibility=%r)" + % ( + __name__, + self.__class__.__name__, + self.filename, + self.size, + self.digest, + self.algorithm, + self.visibility, + ) + ) + + def present(self): + # Doesn't check validity + return os.path.exists(self.filename) + + def validate_size(self): + if self.present(): + return self.size == os.path.getsize(self.filename) + else: + log.debug("trying to validate size on a missing file, %s", self.filename) + raise MissingFileException(filename=self.filename) + + def validate_digest(self): + if self.present(): + with open(self.filename, "rb") as f: + return self.digest == digest_file(f, self.algorithm) + else: + log.debug("trying to validate digest on a missing file, %s', self.filename") + raise MissingFileException(filename=self.filename) + + def validate(self): + if self.size is None or self.validate_size(): + if self.validate_digest(): + return True + return False + + def describe(self): + if self.present() and self.validate(): + return "'%s' is present and valid" % self.filename + elif self.present(): + return "'%s' is present and invalid" % self.filename + else: + return "'%s' is absent" % self.filename + + +def create_file_record(filename, algorithm): + fo = open(filename, "rb") + stored_filename = os.path.split(filename)[1] + fr = FileRecord( + stored_filename, + os.path.getsize(filename), + digest_file(fo, algorithm), + algorithm, + ) + fo.close() + return fr + + +class FileRecordJSONEncoder(json.JSONEncoder): + def encode_file_record(self, obj): + if not issubclass(type(obj), FileRecord): + err = ( + "FileRecordJSONEncoder is only for FileRecord and lists of FileRecords, " + "not %s" % obj.__class__.__name__ + ) + log.warn(err) + raise FileRecordJSONEncoderException(err) + else: + rv = { + "filename": obj.filename, + "size": obj.size, + "algorithm": obj.algorithm, + "digest": obj.digest, + } + if obj.unpack: + rv["unpack"] = True + if obj.version: + rv["version"] = obj.version + if obj.visibility is not None: + rv["visibility"] = obj.visibility + return rv + + def default(self, f): + if issubclass(type(f), list): + record_list = [] + for i in f: + record_list.append(self.encode_file_record(i)) + return record_list + else: + return self.encode_file_record(f) + + +class FileRecordJSONDecoder(json.JSONDecoder): + + """I help the json module materialize a FileRecord from + a JSON file. I understand FileRecords and lists of + FileRecords. I ignore things that I don't expect for now""" + + # TODO: make this more explicit in what it's looking for + # and error out on unexpected things + + def process_file_records(self, obj): + if isinstance(obj, list): + record_list = [] + for i in obj: + record = self.process_file_records(i) + if issubclass(type(record), FileRecord): + record_list.append(record) + return record_list + required_fields = [ + "filename", + "size", + "algorithm", + "digest", + ] + if isinstance(obj, dict): + missing = False + for req in required_fields: + if req not in obj: + missing = True + break + + if not missing: + unpack = obj.get("unpack", False) + version = obj.get("version", None) + visibility = obj.get("visibility", None) + rv = FileRecord( + obj["filename"], + obj["size"], + obj["digest"], + obj["algorithm"], + unpack, + version, + visibility, + ) + log.debug("materialized %s" % rv) + return rv + return obj + + def decode(self, s): + decoded = json.JSONDecoder.decode(self, s) + rv = self.process_file_records(decoded) + return rv + + +class Manifest(object): + valid_formats = ("json",) + + def __init__(self, file_records=None): + self.file_records = file_records or [] + + def __eq__(self, other): + if self is other: + return True + if len(self.file_records) != len(other.file_records): + log.debug("Manifests differ in number of files") + return False + # sort the file records by filename before comparing + mine = sorted((fr.filename, fr) for fr in self.file_records) + theirs = sorted((fr.filename, fr) for fr in other.file_records) + return mine == theirs + + def __ne__(self, other): + return not self.__eq__(other) + + def __deepcopy__(self, memo): + # This is required for a deep copy + return Manifest(self.file_records[:]) + + def __copy__(self): + return Manifest(self.file_records) + + def copy(self): + return Manifest(self.file_records[:]) + + def present(self): + return all(i.present() for i in self.file_records) + + def validate_sizes(self): + return all(i.validate_size() for i in self.file_records) + + def validate_digests(self): + return all(i.validate_digest() for i in self.file_records) + + def validate(self): + return all(i.validate() for i in self.file_records) + + def load(self, data_file, fmt="json"): + assert fmt in self.valid_formats + if fmt == "json": + try: + self.file_records.extend( + json.load(data_file, cls=FileRecordJSONDecoder) + ) + except ValueError: + raise InvalidManifest("trying to read invalid manifest file") + + def loads(self, data_string, fmt="json"): + assert fmt in self.valid_formats + if fmt == "json": + try: + self.file_records.extend( + json.loads(data_string, cls=FileRecordJSONDecoder) + ) + except ValueError: + raise InvalidManifest("trying to read invalid manifest file") + + def dump(self, output_file, fmt="json"): + assert fmt in self.valid_formats + if fmt == "json": + return json.dump( + self.file_records, + output_file, + indent=2, + separators=(",", ": "), + cls=FileRecordJSONEncoder, + ) + + def dumps(self, fmt="json"): + assert fmt in self.valid_formats + if fmt == "json": + return json.dumps( + self.file_records, + indent=2, + separators=(",", ": "), + cls=FileRecordJSONEncoder, + ) + + +def digest_file(f, a): + """I take a file like object 'f' and return a hex-string containing + of the result of the algorithm 'a' applied to 'f'.""" + h = hashlib.new(a) + chunk_size = 1024 * 10 + data = f.read(chunk_size) + while data: + h.update(data) + data = f.read(chunk_size) + name = repr(f.name) if hasattr(f, "name") else "a file" + log.debug("hashed %s with %s to be %s", name, a, h.hexdigest()) + return h.hexdigest() + + +def execute(cmd): + """Execute CMD, logging its stdout at the info level""" + process = Popen(cmd, shell=True, stdout=PIPE) + while True: + line = process.stdout.readline() + if not line: + break + log.info(line.replace("\n", " ")) + return process.wait() == 0 + + +def open_manifest(manifest_file): + """I know how to take a filename and load it into a Manifest object""" + if os.path.exists(manifest_file): + manifest = Manifest() + with open(manifest_file, "r" if PY3 else "rb") as f: + manifest.load(f) + log.debug("loaded manifest from file '%s'" % manifest_file) + return manifest + else: + log.debug("tried to load absent file '%s' as manifest" % manifest_file) + raise InvalidManifest("manifest file '%s' does not exist" % manifest_file) + + +def list_manifest(manifest_file): + """I know how print all the files in a location""" + try: + manifest = open_manifest(manifest_file) + except InvalidManifest as e: + log.error( + "failed to load manifest file at '%s': %s" + % ( + manifest_file, + str(e), + ) + ) + return False + for f in manifest.file_records: + print( + "{}\t{}\t{}".format( + "P" if f.present() else "-", + "V" if f.present() and f.validate() else "-", + f.filename, + ) + ) + return True + + +def validate_manifest(manifest_file): + """I validate that all files in a manifest are present and valid but + don't fetch or delete them if they aren't""" + try: + manifest = open_manifest(manifest_file) + except InvalidManifest as e: + log.error( + "failed to load manifest file at '%s': %s" + % ( + manifest_file, + str(e), + ) + ) + return False + invalid_files = [] + absent_files = [] + for f in manifest.file_records: + if not f.present(): + absent_files.append(f) + elif not f.validate(): + invalid_files.append(f) + if len(invalid_files + absent_files) == 0: + return True + else: + return False + + +def add_files(manifest_file, algorithm, filenames, version, visibility, unpack): + # returns True if all files successfully added, False if not + # and doesn't catch library Exceptions. If any files are already + # tracked in the manifest, return will be False because they weren't + # added + all_files_added = True + # Create a old_manifest object to add to + if os.path.exists(manifest_file): + old_manifest = open_manifest(manifest_file) + else: + old_manifest = Manifest() + log.debug("creating a new manifest file") + new_manifest = Manifest() # use a different manifest for the output + for filename in filenames: + log.debug("adding %s" % filename) + path, name = os.path.split(filename) + new_fr = create_file_record(filename, algorithm) + new_fr.version = version + new_fr.visibility = visibility + new_fr.unpack = unpack + log.debug("appending a new file record to manifest file") + add = True + for fr in old_manifest.file_records: + log.debug( + "manifest file has '%s'" + % "', ".join([x.filename for x in old_manifest.file_records]) + ) + if new_fr == fr: + log.info("file already in old_manifest") + add = False + elif filename == fr.filename: + log.error( + "manifest already contains a different file named %s" % filename + ) + add = False + if add: + new_manifest.file_records.append(new_fr) + log.debug("added '%s' to manifest" % filename) + else: + all_files_added = False + # copy any files in the old manifest that aren't in the new one + new_filenames = set(fr.filename for fr in new_manifest.file_records) + for old_fr in old_manifest.file_records: + if old_fr.filename not in new_filenames: + new_manifest.file_records.append(old_fr) + if PY3: + with open(manifest_file, mode="w") as output: + new_manifest.dump(output, fmt="json") + else: + with open(manifest_file, mode="wb") as output: + new_manifest.dump(output, fmt="json") + return all_files_added + + +def touch(f): + """Used to modify mtime in cached files; + mtime is used by the purge command""" + try: + os.utime(f, None) + except OSError: + log.warn("impossible to update utime of file %s" % f) + + +def _urlopen(req): + ssl_context = None + if os.name == "nt": + ssl_context = ssl.create_default_context(cafile=certifi.where()) + return urllib2.urlopen(req, context=ssl_context) + + +@contextmanager +@retriable(sleeptime=2) +def request(url, auth_file=None): + req = Request(url) + _authorize(req, auth_file) + with closing(_urlopen(req)) as f: + log.debug("opened %s for reading" % url) + yield f + + +def fetch_file(base_urls, file_record, grabchunk=1024 * 4, auth_file=None, region=None): + # A file which is requested to be fetched that exists locally will be + # overwritten by this function + fd, temp_path = tempfile.mkstemp(dir=os.getcwd()) + os.close(fd) + fetched_path = None + for base_url in base_urls: + # Generate the URL for the file on the server side + url = urljoin(base_url, "%s/%s" % (file_record.algorithm, file_record.digest)) + if region is not None: + url += "?region=" + region + + log.info("Attempting to fetch from '%s'..." % base_url) + + # Well, the file doesn't exist locally. Let's fetch it. + try: + with request(url, auth_file) as f, open(temp_path, mode="wb") as out: + k = True + size = 0 + while k: + # TODO: print statistics as file transfers happen both for info and to stop + # buildbot timeouts + indata = f.read(grabchunk) + out.write(indata) + size += len(indata) + if len(indata) == 0: + k = False + log.info( + "File %s fetched from %s as %s" + % (file_record.filename, base_url, temp_path) + ) + fetched_path = temp_path + break + except (URLError, HTTPError, ValueError): + log.info( + "...failed to fetch '%s' from %s" % (file_record.filename, base_url), + exc_info=True, + ) + except IOError: # pragma: no cover + log.info( + "failed to write to temporary file for '%s'" % file_record.filename, + exc_info=True, + ) + + # cleanup temp file in case of issues + if fetched_path: + return os.path.split(fetched_path)[1] + else: + try: + os.remove(temp_path) + except OSError: # pragma: no cover + pass + return None + + +def clean_path(dirname): + """Remove a subtree if is exists. Helper for unpack_file().""" + if os.path.exists(dirname): + log.info("rm tree: %s" % dirname) + shutil.rmtree(dirname) + + +CHECKSUM_SUFFIX = ".checksum" + + +def validate_tar_member(member, path): + def _is_within_directory(directory, target): + real_directory = os.path.realpath(directory) + real_target = os.path.realpath(target) + prefix = os.path.commonprefix([real_directory, real_target]) + return prefix == real_directory + + member_path = os.path.join(path, member.name) + if not _is_within_directory(path, member_path): + raise Exception("Attempted path traversal in tar file: " + member.name) + if member.issym(): + link_path = os.path.join(os.path.dirname(member_path), member.linkname) + if not _is_within_directory(path, link_path): + raise Exception("Attempted link path traversal in tar file: " + member.name) + if member.mode & (stat.S_ISUID | stat.S_ISGID): + raise Exception("Attempted setuid or setgid in tar file: " + member.name) + + +def safe_extract(tar, path=".", *, numeric_owner=False): + def _files(tar, path): + for member in tar: + validate_tar_member(member, path) + yield member + + tar.extractall(path, members=_files(tar, path), numeric_owner=numeric_owner) + + +def unpack_file(filename): + """Untar `filename`, assuming it is uncompressed or compressed with bzip2, + xz, gzip, zst, or unzip a zip file. The file is assumed to contain a single + directory with a name matching the base of the given filename. + Xz support is handled by shelling out to 'tar'.""" + if os.path.isfile(filename) and tarfile.is_tarfile(filename): + tar_file, zip_ext = os.path.splitext(filename) + base_file, tar_ext = os.path.splitext(tar_file) + clean_path(base_file) + log.info('untarring "%s"' % filename) + with tarfile.open(filename) as tar: + safe_extract(tar) + elif os.path.isfile(filename) and filename.endswith(".tar.zst"): + import zstandard + + base_file = filename.replace(".tar.zst", "") + clean_path(base_file) + log.info('untarring "%s"' % filename) + dctx = zstandard.ZstdDecompressor() + with dctx.stream_reader(open(filename, "rb")) as fileobj: + with tarfile.open(fileobj=fileobj, mode="r|") as tar: + safe_extract(tar) + elif os.path.isfile(filename) and zipfile.is_zipfile(filename): + base_file = filename.replace(".zip", "") + clean_path(base_file) + log.info('unzipping "%s"' % filename) + z = zipfile.ZipFile(filename) + z.extractall() + z.close() + else: + log.error("Unknown archive extension for filename '%s'" % filename) + return False + return True + + +def fetch_files( + manifest_file, + base_urls, + filenames=[], + cache_folder=None, + auth_file=None, + region=None, +): + # Lets load the manifest file + try: + manifest = open_manifest(manifest_file) + except InvalidManifest as e: + log.error( + "failed to load manifest file at '%s': %s" + % ( + manifest_file, + str(e), + ) + ) + return False + + # we want to track files already in current working directory AND valid + # we will not need to fetch these + present_files = [] + + # We want to track files that fail to be fetched as well as + # files that are fetched + failed_files = [] + fetched_files = [] + + # Files that we want to unpack. + unpack_files = [] + + # Lets go through the manifest and fetch the files that we want + for f in manifest.file_records: + # case 1: files are already present + if f.present(): + if f.validate(): + present_files.append(f.filename) + if f.unpack: + unpack_files.append(f.filename) + else: + # we have an invalid file here, better to cleanup! + # this invalid file needs to be replaced with a good one + # from the local cash or fetched from a tooltool server + log.info( + "File %s is present locally but it is invalid, so I will remove it " + "and try to fetch it" % f.filename + ) + os.remove(os.path.join(os.getcwd(), f.filename)) + + # check if file is already in cache + if cache_folder and f.filename not in present_files: + try: + shutil.copy( + os.path.join(cache_folder, f.digest), + os.path.join(os.getcwd(), f.filename), + ) + log.info( + "File %s retrieved from local cache %s" % (f.filename, cache_folder) + ) + touch(os.path.join(cache_folder, f.digest)) + + filerecord_for_validation = FileRecord( + f.filename, f.size, f.digest, f.algorithm + ) + if filerecord_for_validation.validate(): + present_files.append(f.filename) + if f.unpack: + unpack_files.append(f.filename) + else: + # the file copied from the cache is invalid, better to + # clean up the cache version itself as well + log.warn( + "File %s retrieved from cache is invalid! I am deleting it from the " + "cache as well" % f.filename + ) + os.remove(os.path.join(os.getcwd(), f.filename)) + os.remove(os.path.join(cache_folder, f.digest)) + except IOError: + log.info( + "File %s not present in local cache folder %s" + % (f.filename, cache_folder) + ) + + # now I will try to fetch all files which are not already present and + # valid, appending a suffix to avoid race conditions + temp_file_name = None + # 'filenames' is the list of filenames to be managed, if this variable + # is a non empty list it can be used to filter if filename is in + # present_files, it means that I have it already because it was already + # either in the working dir or in the cache + if ( + f.filename in filenames or len(filenames) == 0 + ) and f.filename not in present_files: + log.debug("fetching %s" % f.filename) + temp_file_name = fetch_file( + base_urls, f, auth_file=auth_file, region=region + ) + if temp_file_name: + fetched_files.append((f, temp_file_name)) + else: + failed_files.append(f.filename) + else: + log.debug("skipping %s" % f.filename) + + # lets ensure that fetched files match what the manifest specified + for localfile, temp_file_name in fetched_files: + # since I downloaded to a temp file, I need to perform all validations on the temp file + # this is why filerecord_for_validation is created + + filerecord_for_validation = FileRecord( + temp_file_name, localfile.size, localfile.digest, localfile.algorithm + ) + + if filerecord_for_validation.validate(): + # great! + # I can rename the temp file + log.info( + "File integrity verified, renaming %s to %s" + % (temp_file_name, localfile.filename) + ) + os.rename( + os.path.join(os.getcwd(), temp_file_name), + os.path.join(os.getcwd(), localfile.filename), + ) + + if localfile.unpack: + unpack_files.append(localfile.filename) + + # if I am using a cache and a new file has just been retrieved from a + # remote location, I need to update the cache as well + if cache_folder: + log.info("Updating local cache %s..." % cache_folder) + try: + if not os.path.exists(cache_folder): + log.info("Creating cache in %s..." % cache_folder) + os.makedirs(cache_folder, 0o0700) + shutil.copy( + os.path.join(os.getcwd(), localfile.filename), + os.path.join(cache_folder, localfile.digest), + ) + log.info( + "Local cache %s updated with %s" + % (cache_folder, localfile.filename) + ) + touch(os.path.join(cache_folder, localfile.digest)) + except (OSError, IOError): + log.warning( + "Impossible to add file %s to cache folder %s" + % (localfile.filename, cache_folder), + exc_info=False, + ) + else: + failed_files.append(localfile.filename) + log.error("'%s'" % filerecord_for_validation.describe()) + os.remove(temp_file_name) + + # Unpack files that need to be unpacked. + for filename in unpack_files: + if not unpack_file(filename): + failed_files.append(filename) + + # If we failed to fetch or validate a file, we need to fail + if len(failed_files) > 0: + log.error("The following files failed: '%s'" % "', ".join(failed_files)) + return False + return True + + +def freespace(p): + "Returns the number of bytes free under directory `p`" + if sys.platform == "win32": # pragma: no cover + # os.statvfs doesn't work on Windows + import win32file + + secsPerClus, bytesPerSec, nFreeClus, totClus = win32file.GetDiskFreeSpace(p) + return secsPerClus * bytesPerSec * nFreeClus + else: + r = os.statvfs(p) + return r.f_frsize * r.f_bavail + + +def purge(folder, gigs): + """If gigs is non 0, it deletes files in `folder` until `gigs` GB are free, + starting from older files. If gigs is 0, a full purge will be performed. + No recursive deletion of files in subfolder is performed.""" + + full_purge = bool(gigs == 0) + gigs *= 1024 * 1024 * 1024 + + if not full_purge and freespace(folder) >= gigs: + log.info("No need to cleanup") + return + + files = [] + for f in os.listdir(folder): + p = os.path.join(folder, f) + # it deletes files in folder without going into subfolders, + # assuming the cache has a flat structure + if not os.path.isfile(p): + continue + mtime = os.path.getmtime(p) + files.append((mtime, p)) + + # iterate files sorted by mtime + for _, f in sorted(files): + log.info("removing %s to free up space" % f) + try: + os.remove(f) + except OSError: + log.info("Impossible to remove %s" % f, exc_info=True) + if not full_purge and freespace(folder) >= gigs: + break + + +def _log_api_error(e): + if hasattr(e, "hdrs") and e.hdrs["content-type"] == "application/json": + json_resp = json.load(e.fp) + log.error( + "%s: %s" % (json_resp["error"]["name"], json_resp["error"]["description"]) + ) + else: + log.exception("Error making RelengAPI request:") + + +def _authorize(req, auth_file): + is_taskcluster_auth = False + + if not auth_file: + try: + taskcluster_env_keys = { + "clientId": "TASKCLUSTER_CLIENT_ID", + "accessToken": "TASKCLUSTER_ACCESS_TOKEN", + } + auth_content = {k: os.environ[v] for k, v in taskcluster_env_keys.items()} + is_taskcluster_auth = True + except KeyError: + return + else: + with open(auth_file) as f: + auth_content = f.read().strip() + try: + auth_content = json.loads(auth_content) + is_taskcluster_auth = True + except Exception: + pass + + if is_taskcluster_auth: + taskcluster_header = make_taskcluster_header(auth_content, req) + log.debug("Using taskcluster credentials in %s" % auth_file) + req.add_unredirected_header("Authorization", taskcluster_header) + else: + log.debug("Using Bearer token in %s" % auth_file) + req.add_unredirected_header("Authorization", "Bearer %s" % auth_content) + + +def _send_batch(base_url, auth_file, batch, region): + url = urljoin(base_url, "upload") + if region is not None: + url += "?region=" + region + data = json.dumps(batch) + if PY3: + data = data.encode("utf-8") + req = Request(url, data, {"Content-Type": "application/json"}) + _authorize(req, auth_file) + try: + resp = _urlopen(req) + except (URLError, HTTPError) as e: + _log_api_error(e) + return None + return json.load(resp)["result"] + + +def _s3_upload(filename, file): + # urllib2 does not support streaming, so we fall back to good old httplib + url = urlparse(file["put_url"]) + cls = HTTPSConnection if url.scheme == "https" else HTTPConnection + host, port = url.netloc.split(":") if ":" in url.netloc else (url.netloc, 443) + port = int(port) + conn = cls(host, port) + try: + req_path = "%s?%s" % (url.path, url.query) if url.query else url.path + with open(filename, "rb") as f: + content = f.read() + content_length = len(content) + f.seek(0) + conn.request( + "PUT", + req_path, + f, + { + "Content-Type": "application/octet-stream", + "Content-Length": str(content_length), + }, + ) + resp = conn.getresponse() + resp_body = resp.read() + conn.close() + if resp.status != 200: + raise RuntimeError( + "Non-200 return from AWS: %s %s\n%s" + % (resp.status, resp.reason, resp_body) + ) + except Exception: + file["upload_exception"] = sys.exc_info() + file["upload_ok"] = False + else: + file["upload_ok"] = True + + +def _notify_upload_complete(base_url, auth_file, file): + req = Request(urljoin(base_url, "upload/complete/%(algorithm)s/%(digest)s" % file)) + _authorize(req, auth_file) + try: + _urlopen(req) + except HTTPError as e: + if e.code != 409: + _log_api_error(e) + return + # 409 indicates that the upload URL hasn't expired yet and we + # should retry after a delay + to_wait = int(e.headers.get("X-Retry-After", 60)) + log.warning("Waiting %d seconds for upload URLs to expire" % to_wait) + time.sleep(to_wait) + _notify_upload_complete(base_url, auth_file, file) + except Exception: + log.exception("While notifying server of upload completion:") + + +def upload(manifest, message, base_urls, auth_file, region): + try: + manifest = open_manifest(manifest) + except InvalidManifest: + log.exception("failed to load manifest file at '%s'") + return False + + # verify the manifest, since we'll need the files present to upload + if not manifest.validate(): + log.error("manifest is invalid") + return False + + if any(fr.visibility is None for fr in manifest.file_records): + log.error("All files in a manifest for upload must have a visibility set") + + # convert the manifest to an upload batch + batch = { + "message": message, + "files": {}, + } + for fr in manifest.file_records: + batch["files"][fr.filename] = { + "size": fr.size, + "digest": fr.digest, + "algorithm": fr.algorithm, + "visibility": fr.visibility, + } + + # make the upload request + resp = _send_batch(base_urls[0], auth_file, batch, region) + if not resp: + return None + files = resp["files"] + + # Upload the files, each in a thread. This allows us to start all of the + # uploads before any of the URLs expire. + threads = {} + for filename, file in files.items(): + if "put_url" in file: + log.info("%s: starting upload" % (filename,)) + thd = threading.Thread(target=_s3_upload, args=(filename, file)) + thd.daemon = 1 + thd.start() + threads[filename] = thd + else: + log.info("%s: already exists on server" % (filename,)) + + # re-join all of those threads as they exit + success = True + while threads: + for filename, thread in list(threads.items()): + if not thread.is_alive(): + # _s3_upload has annotated file with result information + file = files[filename] + thread.join() + if file["upload_ok"]: + log.info("%s: uploaded" % filename) + else: + log.error( + "%s: failed" % filename, exc_info=file["upload_exception"] + ) + success = False + del threads[filename] + + # notify the server that the uploads are completed. If the notification + # fails, we don't consider that an error (the server will notice + # eventually) + for filename, file in files.items(): + if "put_url" in file and file["upload_ok"]: + log.info("notifying server of upload completion for %s" % (filename,)) + _notify_upload_complete(base_urls[0], auth_file, file) + + return success + + +def send_operation_on_file(data, base_urls, digest, auth_file): + url = base_urls[0] + url = urljoin(url, "file/sha512/" + digest) + + data = json.dumps(data) + + req = Request(url, data, {"Content-Type": "application/json"}) + req.get_method = lambda: "PATCH" + + _authorize(req, auth_file) + + try: + _urlopen(req) + except (URLError, HTTPError) as e: + _log_api_error(e) + return False + return True + + +def change_visibility(base_urls, digest, visibility, auth_file): + data = [ + { + "op": "set_visibility", + "visibility": visibility, + } + ] + return send_operation_on_file(data, base_urls, digest, auth_file) + + +def delete_instances(base_urls, digest, auth_file): + data = [ + { + "op": "delete_instances", + } + ] + return send_operation_on_file(data, base_urls, digest, auth_file) + + +def process_command(options, args): + """I know how to take a list of program arguments and + start doing the right thing with them""" + cmd = args[0] + cmd_args = args[1:] + log.debug("processing '%s' command with args '%s'" % (cmd, '", "'.join(cmd_args))) + log.debug("using options: %s" % options) + + if cmd == "list": + return list_manifest(options["manifest"]) + if cmd == "validate": + return validate_manifest(options["manifest"]) + elif cmd == "add": + return add_files( + options["manifest"], + options["algorithm"], + cmd_args, + options["version"], + options["visibility"], + options["unpack"], + ) + elif cmd == "purge": + if options["cache_folder"]: + purge(folder=options["cache_folder"], gigs=options["size"]) + else: + log.critical("please specify the cache folder to be purged") + return False + elif cmd == "fetch": + return fetch_files( + options["manifest"], + options["base_url"], + cmd_args, + cache_folder=options["cache_folder"], + auth_file=options.get("auth_file"), + region=options.get("region"), + ) + elif cmd == "upload": + if not options.get("message"): + log.critical("upload command requires a message") + return False + return upload( + options.get("manifest"), + options.get("message"), + options.get("base_url"), + options.get("auth_file"), + options.get("region"), + ) + elif cmd == "change-visibility": + if not options.get("digest"): + log.critical("change-visibility command requires a digest option") + return False + if not options.get("visibility"): + log.critical("change-visibility command requires a visibility option") + return False + return change_visibility( + options.get("base_url"), + options.get("digest"), + options.get("visibility"), + options.get("auth_file"), + ) + elif cmd == "delete": + if not options.get("digest"): + log.critical("change-visibility command requires a digest option") + return False + return delete_instances( + options.get("base_url"), + options.get("digest"), + options.get("auth_file"), + ) + else: + log.critical('command "%s" is not implemented' % cmd) + return False + + +def main(argv, _skip_logging=False): + # Set up option parsing + parser = optparse.OptionParser() + parser.add_option( + "-q", + "--quiet", + default=logging.INFO, + dest="loglevel", + action="store_const", + const=logging.ERROR, + ) + parser.add_option( + "-v", "--verbose", dest="loglevel", action="store_const", const=logging.DEBUG + ) + parser.add_option( + "-m", + "--manifest", + default=DEFAULT_MANIFEST_NAME, + dest="manifest", + action="store", + help="specify the manifest file to be operated on", + ) + parser.add_option( + "-d", + "--algorithm", + default="sha512", + dest="algorithm", + action="store", + help="hashing algorithm to use (only sha512 is allowed)", + ) + parser.add_option( + "--digest", + default=None, + dest="digest", + action="store", + help="digest hash to change visibility for", + ) + parser.add_option( + "--visibility", + default=None, + dest="visibility", + choices=["internal", "public"], + help='Visibility level of this file; "internal" is for ' + "files that cannot be distributed out of the company " + 'but not for secrets; "public" files are available to ' + "anyone without restriction", + ) + parser.add_option( + "--unpack", + default=False, + dest="unpack", + action="store_true", + help="Request unpacking this file after fetch." + " This is helpful with tarballs.", + ) + parser.add_option( + "--version", + default=None, + dest="version", + action="store", + help="Version string for this file. This annotates the " + "manifest entry with a version string to help " + "identify the contents.", + ) + parser.add_option( + "-o", + "--overwrite", + default=False, + dest="overwrite", + action="store_true", + help="UNUSED; present for backward compatibility", + ) + parser.add_option( + "--url", + dest="base_url", + action="append", + help="RelengAPI URL ending with /tooltool/; default " + "is appropriate for Mozilla", + ) + parser.add_option( + "-c", "--cache-folder", dest="cache_folder", help="Local cache folder" + ) + parser.add_option( + "-s", + "--size", + help="free space required (in GB)", + dest="size", + type="float", + default=0.0, + ) + parser.add_option( + "-r", + "--region", + help="Preferred AWS region for upload or fetch; " "example: --region=us-west-2", + ) + parser.add_option( + "--message", + help='The "commit message" for an upload; format with a bug number ' + "and brief comment", + dest="message", + ) + parser.add_option( + "--authentication-file", + help="Use the RelengAPI token found in the given file to " + "authenticate to the RelengAPI server.", + dest="auth_file", + ) + + (options_obj, args) = parser.parse_args(argv[1:]) + + if not options_obj.base_url: + tooltool_host = os.environ.get("TOOLTOOL_HOST", "tooltool.mozilla-releng.net") + taskcluster_proxy_url = os.environ.get("TASKCLUSTER_PROXY_URL") + if taskcluster_proxy_url: + tooltool_url = "{}/{}".format(taskcluster_proxy_url, tooltool_host) + else: + tooltool_url = "https://{}".format(tooltool_host) + + options_obj.base_url = [tooltool_url] + + # ensure all URLs have a trailing slash + def add_slash(url): + return url if url.endswith("/") else (url + "/") + + options_obj.base_url = [add_slash(u) for u in options_obj.base_url] + + # expand ~ in --authentication-file + if options_obj.auth_file: + options_obj.auth_file = os.path.expanduser(options_obj.auth_file) + + # Dictionaries are easier to work with + options = vars(options_obj) + + log.setLevel(options["loglevel"]) + + # Set up logging, for now just to the console + if not _skip_logging: # pragma: no cover + ch = logging.StreamHandler() + cf = logging.Formatter("%(levelname)s - %(message)s") + ch.setFormatter(cf) + log.addHandler(ch) + + if options["algorithm"] != "sha512": + parser.error("only --algorithm sha512 is supported") + + if len(args) < 1: + parser.error("You must specify a command") + + return 0 if process_command(options, args) else 1 + + +if __name__ == "__main__": # pragma: no cover + sys.exit(main(sys.argv)) diff --git a/python/mozbuild/mozbuild/action/unify_symbols.py b/python/mozbuild/mozbuild/action/unify_symbols.py new file mode 100644 index 0000000000..4e96a010b2 --- /dev/null +++ b/python/mozbuild/mozbuild/action/unify_symbols.py @@ -0,0 +1,49 @@ +# 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/. + +import argparse + +from mozpack.copier import FileCopier +from mozpack.errors import errors +from mozpack.files import FileFinder +from mozpack.unify import UnifiedFinder + + +class UnifiedSymbolsFinder(UnifiedFinder): + def unify_file(self, path, file1, file2): + # We expect none of the files to overlap. + if not file2: + return file1 + if not file1: + return file2 + errors.error( + "{} is in both {} and {}".format( + path, self._finder1.base, self._finder2.base + ) + ) + + +def main(): + parser = argparse.ArgumentParser( + description="Merge two crashreporter symbols directories." + ) + parser.add_argument("dir1", help="Directory") + parser.add_argument("dir2", help="Directory to merge") + + options = parser.parse_args() + + dir1_finder = FileFinder(options.dir1) + dir2_finder = FileFinder(options.dir2) + finder = UnifiedSymbolsFinder(dir1_finder, dir2_finder) + + copier = FileCopier() + with errors.accumulate(): + for p, f in finder: + copier.add(p, f) + + copier.copy(options.dir1, skip_if_older=False) + + +if __name__ == "__main__": + main() diff --git a/python/mozbuild/mozbuild/action/unify_tests.py b/python/mozbuild/mozbuild/action/unify_tests.py new file mode 100644 index 0000000000..d94ebade1b --- /dev/null +++ b/python/mozbuild/mozbuild/action/unify_tests.py @@ -0,0 +1,65 @@ +# 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/. + +import argparse +import os + +import buildconfig +import mozpack.path as mozpath +from mozpack.copier import FileCopier +from mozpack.errors import errors +from mozpack.files import FileFinder +from mozpack.unify import UnifiedFinder + + +class UnifiedTestFinder(UnifiedFinder): + def unify_file(self, path, file1, file2): + unified = super(UnifiedTestFinder, self).unify_file(path, file1, file2) + basename = mozpath.basename(path) + if basename == "mozinfo.json": + # The mozinfo.json files contain processor info, which differs + # between both ends. + # Remove the block when this assert is hit. + assert not unified + errors.ignore_errors() + self._report_difference(path, file1, file2) + errors.ignore_errors(False) + return file1 + elif basename == "dump_syms_mac": + # At the moment, the dump_syms_mac executable is a x86_64 binary + # on both ends. We can't create a universal executable from twice + # the same executable. + # When this assert hits, remove this block. + assert file1.open().read() == file2.open().read() + return file1 + return unified + + +def main(): + parser = argparse.ArgumentParser( + description="Merge two directories, creating Universal binaries for " + "executables and libraries they contain." + ) + parser.add_argument("dir1", help="Directory") + parser.add_argument("dir2", help="Directory to merge") + + options = parser.parse_args() + + buildconfig.substs["OS_ARCH"] = "Darwin" + buildconfig.substs["LIPO"] = os.environ.get("LIPO") + + dir1_finder = FileFinder(options.dir1, find_executables=True, find_dotfiles=True) + dir2_finder = FileFinder(options.dir2, find_executables=True, find_dotfiles=True) + finder = UnifiedTestFinder(dir1_finder, dir2_finder) + + copier = FileCopier() + with errors.accumulate(): + for p, f in finder: + copier.add(p, f) + + copier.copy(options.dir1, skip_if_older=False) + + +if __name__ == "__main__": + main() diff --git a/python/mozbuild/mozbuild/action/unpack_dmg.py b/python/mozbuild/mozbuild/action/unpack_dmg.py new file mode 100644 index 0000000000..2453a3fa73 --- /dev/null +++ b/python/mozbuild/mozbuild/action/unpack_dmg.py @@ -0,0 +1,52 @@ +# 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/. + +import argparse +import sys +from pathlib import Path + +from mozpack import dmg + +from mozbuild.bootstrap import bootstrap_toolchain + + +def _path_or_none(input: str): + if not input: + return None + return Path(input) + + +def main(args): + parser = argparse.ArgumentParser( + description="Explode a DMG into its relevant files" + ) + + parser.add_argument("--dsstore", help="DSStore file from") + parser.add_argument("--background", help="Background file from") + parser.add_argument("--icon", help="Icon file from") + + parser.add_argument("dmgfile", metavar="DMG_IN", help="DMG File to Unpack") + parser.add_argument( + "outpath", metavar="PATH_OUT", help="Location to put unpacked files" + ) + + options = parser.parse_args(args) + + dmg_tool = bootstrap_toolchain("dmg/dmg") + hfs_tool = bootstrap_toolchain("dmg/hfsplus") + + dmg.extract_dmg( + dmgfile=Path(options.dmgfile), + output=Path(options.outpath), + dmg_tool=_path_or_none(dmg_tool), + hfs_tool=_path_or_none(hfs_tool), + dsstore=_path_or_none(options.dsstore), + background=_path_or_none(options.background), + icon=_path_or_none(options.icon), + ) + return 0 + + +if __name__ == "__main__": + sys.exit(main(sys.argv[1:])) diff --git a/python/mozbuild/mozbuild/action/webidl.py b/python/mozbuild/mozbuild/action/webidl.py new file mode 100644 index 0000000000..ade25b4c0c --- /dev/null +++ b/python/mozbuild/mozbuild/action/webidl.py @@ -0,0 +1,17 @@ +# 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/. + +import sys + +from mozwebidlcodegen import create_build_system_manager + + +def main(argv): + """Perform WebIDL code generation required by the build system.""" + manager = create_build_system_manager() + manager.generate_build_files() + + +if __name__ == "__main__": + sys.exit(main(sys.argv[1:])) diff --git a/python/mozbuild/mozbuild/action/wrap_rustc.py b/python/mozbuild/mozbuild/action/wrap_rustc.py new file mode 100644 index 0000000000..d865438c47 --- /dev/null +++ b/python/mozbuild/mozbuild/action/wrap_rustc.py @@ -0,0 +1,79 @@ +# 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/. + +import argparse +import os +import subprocess +import sys + + +def parse_outputs(crate_output, dep_outputs, pass_l_flag): + env = {} + args = [] + + def parse_line(line): + if line.startswith("cargo:"): + return line[len("cargo:") :].split("=", 1) + + def parse_file(f): + with open(f) as fh: + return [parse_line(line.rstrip()) for line in fh.readlines()] + + for f in dep_outputs: + for entry in parse_file(f): + if not entry: + continue + key, value = entry + if key == "rustc-link-search": + args += ["-L", value] + elif key == "rustc-flags": + flags = value.split() + for flag, val in zip(flags[0::2], flags[1::2]): + if flag == "-l" and f == crate_output: + args += ["-l", val] + elif flag == "-L": + args += ["-L", val] + else: + raise Exception( + "Unknown flag passed through " + '"cargo:rustc-flags": "%s"' % flag + ) + elif key == "rustc-link-lib" and f == crate_output: + args += ["-l", value] + elif key == "rustc-cfg" and f == crate_output: + args += ["--cfg", value] + elif key == "rustc-env" and f == crate_output: + env_key, env_value = value.split("=", 1) + env[env_key] = env_value + elif key == "rerun-if-changed": + pass + elif key == "rerun-if-env-changed": + pass + elif key == "warning": + pass + elif key: + # Todo: Distinguish between direct and transitive + # dependencies so we can pass metadata environment + # variables correctly. + pass + + return env, args + + +def wrap_rustc(args): + parser = argparse.ArgumentParser() + parser.add_argument("--crate-out", nargs="?") + parser.add_argument("--deps-out", nargs="*") + parser.add_argument("--cwd") + parser.add_argument("--pass-l-flag", action="store_true") + parser.add_argument("--cmd", nargs=argparse.REMAINDER) + args = parser.parse_args(args) + + new_env, new_args = parse_outputs(args.crate_out, args.deps_out, args.pass_l_flag) + os.environ.update(new_env) + return subprocess.Popen(args.cmd + new_args, cwd=args.cwd).wait() + + +if __name__ == "__main__": + sys.exit(wrap_rustc(sys.argv[1:])) diff --git a/python/mozbuild/mozbuild/action/xpccheck.py b/python/mozbuild/mozbuild/action/xpccheck.py new file mode 100644 index 0000000000..87dd32848f --- /dev/null +++ b/python/mozbuild/mozbuild/action/xpccheck.py @@ -0,0 +1,108 @@ +# 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/. + +"""A generic script to verify all test files are in the +corresponding .toml file. + +Usage: xpccheck.py <directory> [<directory> ...] +""" + +import os +import sys +from glob import glob + +import manifestparser + + +def getIniTests(testdir): + mp = manifestparser.ManifestParser(strict=False) + mp.read(os.path.join(testdir, "xpcshell.toml")) + return mp.tests + + +def verifyDirectory(initests, directory): + files = glob(os.path.join(os.path.abspath(directory), "test_*")) + for f in files: + if not os.path.isfile(f): + continue + + name = os.path.basename(f) + if name.endswith(".in"): + name = name[:-3] + + if not name.endswith(".js"): + continue + + found = False + for test in initests: + if os.path.join(os.path.abspath(directory), name) == test["path"]: + found = True + break + + if not found: + print( + ( + "TEST-UNEXPECTED-FAIL | xpccheck | test " + "%s is missing from test manifest %s!" + ) + % ( + name, + os.path.join(directory, "xpcshell.toml"), + ), + file=sys.stderr, + ) + sys.exit(1) + + +def verifyIniFile(initests, directory): + files = glob(os.path.join(os.path.abspath(directory), "test_*")) + for test in initests: + name = test["path"].split("/")[-1] + + found = False + for f in files: + fname = f.split("/")[-1] + if fname.endswith(".in"): + fname = ".in".join(fname.split(".in")[:-1]) + + if os.path.join(os.path.abspath(directory), fname) == test["path"]: + found = True + break + + if not found: + print( + ( + "TEST-UNEXPECTED-FAIL | xpccheck | found " + "%s in xpcshell.toml and not in directory '%s'" + ) + % ( + name, + directory, + ), + file=sys.stderr, + ) + sys.exit(1) + + +def main(argv): + if len(argv) < 2: + print( + "Usage: xpccheck.py <topsrcdir> <directory> [<directory> ...]", + file=sys.stderr, + ) + sys.exit(1) + + for d in argv[1:]: + # xpcshell-unpack is a copy of xpcshell sibling directory and in the Makefile + # we copy all files (including xpcshell.toml from the sibling directory. + if d.endswith("toolkit/mozapps/extensions/test/xpcshell-unpack"): + continue + + initests = getIniTests(d) + verifyDirectory(initests, d) + verifyIniFile(initests, d) + + +if __name__ == "__main__": + main(sys.argv[1:]) diff --git a/python/mozbuild/mozbuild/action/xpidl-process.py b/python/mozbuild/mozbuild/action/xpidl-process.py new file mode 100755 index 0000000000..0a126c729d --- /dev/null +++ b/python/mozbuild/mozbuild/action/xpidl-process.py @@ -0,0 +1,152 @@ +#!/usr/bin/env python +# 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/. + +# This script is used to generate an output header and xpt file for +# input IDL file(s). It's purpose is to directly support the build +# system. The API will change to meet the needs of the build system. + +import argparse +import os +import sys + +import six +from buildconfig import topsrcdir +from mozpack import path as mozpath +from xpidl import jsonxpt +from xpidl.header import print_header +from xpidl.rust import print_rust_bindings +from xpidl.rust_macros import print_rust_macros_bindings +from xpidl.xpidl import IDLParser + +from mozbuild.makeutil import Makefile +from mozbuild.pythonutil import iter_modules_in_path +from mozbuild.util import FileAvoidWrite + + +def process( + input_dirs, + inc_paths, + bindings_conf, + header_dir, + xpcrs_dir, + xpt_dir, + deps_dir, + module, + idl_files, +): + p = IDLParser() + + xpts = [] + mk = Makefile() + rule = mk.create_rule() + + glbl = {} + exec(open(bindings_conf, encoding="utf-8").read(), glbl) + webidlconfig = glbl["DOMInterfaces"] + + # Write out dependencies for Python modules we import. If this list isn't + # up to date, we will not re-process XPIDL files if the processor changes. + rule.add_dependencies(six.ensure_text(s) for s in iter_modules_in_path(topsrcdir)) + + for path in idl_files: + basename = os.path.basename(path) + stem, _ = os.path.splitext(basename) + idl_data = open(path, encoding="utf-8").read() + + idl = p.parse(idl_data, filename=path) + idl.resolve(inc_paths, p, webidlconfig) + + header_path = os.path.join(header_dir, "%s.h" % stem) + rs_rt_path = os.path.join(xpcrs_dir, "rt", "%s.rs" % stem) + rs_bt_path = os.path.join(xpcrs_dir, "bt", "%s.rs" % stem) + + xpts.append(jsonxpt.build_typelib(idl)) + + rule.add_dependencies(six.ensure_text(s) for s in idl.deps) + + # The print_* functions don't actually do anything with the + # passed-in path other than writing it into the file to let people + # know where the original source was. This script receives + # absolute paths, which are not so great to embed in header files + # (they mess with deterministic generation of files on different + # machines, Searchfox logic, shared compilation caches, etc.), so + # we pass in fake paths that are the same across compilations, but + # should still enable people to figure out where to go. + relpath = mozpath.relpath(path, topsrcdir) + + with FileAvoidWrite(header_path) as fh: + print_header(idl, fh, path, relpath) + + with FileAvoidWrite(rs_rt_path) as fh: + print_rust_bindings(idl, fh, relpath) + + with FileAvoidWrite(rs_bt_path) as fh: + print_rust_macros_bindings(idl, fh, relpath) + + # NOTE: We don't use FileAvoidWrite here as we may re-run this code due to a + # number of different changes in the code, which may not cause the .xpt + # files to be changed in any way. This means that make will re-run us every + # time a build is run whether or not anything changed. To fix this we + # unconditionally write out the file. + xpt_path = os.path.join(xpt_dir, "%s.xpt" % module) + with open(xpt_path, "w", encoding="utf-8", newline="\n") as fh: + jsonxpt.write(jsonxpt.link(xpts), fh) + + rule.add_targets([six.ensure_text(xpt_path)]) + if deps_dir: + deps_path = os.path.join(deps_dir, "%s.pp" % module) + with FileAvoidWrite(deps_path) as fh: + mk.dump(fh) + + +def main(argv): + parser = argparse.ArgumentParser() + parser.add_argument( + "--depsdir", help="Directory in which to write dependency files." + ) + parser.add_argument( + "--bindings-conf", help="Path to the WebIDL binding configuration file." + ) + parser.add_argument( + "--input-dir", + dest="input_dirs", + action="append", + default=[], + help="Directory(ies) in which to find source .idl files.", + ) + parser.add_argument("headerdir", help="Directory in which to write header files.") + parser.add_argument( + "xpcrsdir", help="Directory in which to write rust xpcom binding files." + ) + parser.add_argument("xptdir", help="Directory in which to write xpt file.") + parser.add_argument( + "module", help="Final module name to use for linked output xpt file." + ) + parser.add_argument("idls", nargs="+", help="Source .idl file(s).") + parser.add_argument( + "-I", + dest="incpath", + action="append", + default=[], + help="Extra directories where to look for included .idl files.", + ) + + args = parser.parse_args(argv) + incpath = [os.path.join(topsrcdir, p) for p in args.incpath] + process( + args.input_dirs, + incpath, + args.bindings_conf, + args.headerdir, + args.xpcrsdir, + args.xptdir, + args.depsdir, + args.module, + args.idls, + ) + + +if __name__ == "__main__": + main(sys.argv[1:]) diff --git a/python/mozbuild/mozbuild/action/zip.py b/python/mozbuild/mozbuild/action/zip.py new file mode 100644 index 0000000000..327147bcee --- /dev/null +++ b/python/mozbuild/mozbuild/action/zip.py @@ -0,0 +1,50 @@ +# 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/. + +# This script creates a zip file, but will also strip any binaries +# it finds before adding them to the zip. + +import argparse +import sys + +import mozpack.path as mozpath +from mozpack.copier import Jarrer +from mozpack.errors import errors +from mozpack.files import FileFinder +from mozpack.path import match + + +def main(args): + parser = argparse.ArgumentParser() + parser.add_argument( + "-C", + metavar="DIR", + default=".", + help="Change to given directory before considering " "other paths", + ) + parser.add_argument("--strip", action="store_true", help="Strip executables") + parser.add_argument( + "-x", + metavar="EXCLUDE", + default=[], + action="append", + help="Exclude files that match the pattern", + ) + parser.add_argument("zip", help="Path to zip file to write") + parser.add_argument("input", nargs="+", help="Path to files to add to zip") + args = parser.parse_args(args) + + jarrer = Jarrer() + + with errors.accumulate(): + finder = FileFinder(args.C, find_executables=args.strip) + for path in args.input: + for p, f in finder.find(path): + if not any([match(p, exclude) for exclude in args.x]): + jarrer.add(p, f) + jarrer.copy(mozpath.join(args.C, args.zip)) + + +if __name__ == "__main__": + main(sys.argv[1:]) diff --git a/python/mozbuild/mozbuild/analyze/__init__.py b/python/mozbuild/mozbuild/analyze/__init__.py new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/python/mozbuild/mozbuild/analyze/__init__.py diff --git a/python/mozbuild/mozbuild/analyze/hg.py b/python/mozbuild/mozbuild/analyze/hg.py new file mode 100644 index 0000000000..605ff6838e --- /dev/null +++ b/python/mozbuild/mozbuild/analyze/hg.py @@ -0,0 +1,176 @@ +# 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/. + +import bisect +import gzip +import json +import math +from collections import Counter +from datetime import datetime, timedelta + +import mozpack.path as mozpath +import requests + +PUSHLOG_CHUNK_SIZE = 500 + +URL = "https://hg.mozilla.org/mozilla-central/json-pushes?" + + +def unix_epoch(date): + return (date - datetime(1970, 1, 1)).total_seconds() + + +def unix_from_date(n, today): + return unix_epoch(today - timedelta(days=n)) + + +def get_lastpid(session): + return session.get(URL + "&version=2").json()["lastpushid"] + + +def get_pushlog_chunk(session, start, end): + # returns pushes sorted by date + res = session.get( + URL + + "version=1&startID={0}&\ + endID={1}&full=1".format( + start, end + ) + ).json() + return sorted(res.items(), key=lambda x: x[1]["date"]) + + +def collect_data(session, date): + if date < 1206031764: # first push + raise Exception("No pushes exist before March 20, 2008.") + lastpushid = get_lastpid(session) + data = [] + start_id = lastpushid - PUSHLOG_CHUNK_SIZE + end_id = lastpushid + 1 + while True: + res = get_pushlog_chunk(session, start_id, end_id) + starting_date = res[0][1]["date"] # date of oldest push in chunk + dates = [x[1]["date"] for x in res] + if starting_date < date: + i = bisect.bisect_left(dates, date) + data.append(res[i:]) + return data + else: + data.append(res) + end_id = start_id + 1 + start_id = start_id - PUSHLOG_CHUNK_SIZE + + +def get_data(epoch): + session = requests.Session() + data = collect_data(session, epoch) + return {k: v for sublist in data for (k, v) in sublist} + + +class Pushlog(object): + def __init__(self, days): + info = get_data(unix_from_date(days, datetime.today())) + self.pushlog = info + self.pids = self.get_pids() + self.pushes = self.make_pushes() + self.files = [l for p in self.pushes for l in set(p.files)] + self.file_set = set(self.files) + self.file_count = Counter(self.files) + + def make_pushes(self): + pids = self.pids + all_pushes = self.pushlog + return [Push(pid, all_pushes[str(pid)]) for pid in pids] + + def get_pids(self): + keys = self.pushlog.keys() + keys.sort() + return keys + + +class Push(object): + def __init__(self, pid, p_dict): + self.id = pid + self.date = p_dict["date"] + self.files = [f for x in p_dict["changesets"] for f in x["files"]] + + +class Report(object): + def __init__(self, days, path=None, cost_dict=None): + obj = Pushlog(days) + self.file_set = obj.file_set + self.file_count = obj.file_count + self.name = str(days) + "day_report" + self.cost_dict = self.get_cost_dict(path, cost_dict) + + def get_cost_dict(self, path, cost_dict): + if path is not None: + with gzip.open(path) as file: + return json.loads(file.read()) + else: + if cost_dict is not None: + return cost_dict + else: + raise Exception + + def organize_data(self): + costs = self.cost_dict + counts = self.file_count + res = [] + for f in self.file_set: + cost = costs.get(f) + count = counts.get(f) + if cost is not None: + res.append((f, cost, count, round(cost * count, 3))) + return res + + def get_sorted_report(self, format): + res = self.organize_data() + res.sort(key=(lambda x: x[3]), reverse=True) + + def ms_to_mins_secs(ms): + secs = ms / 1000.0 + mins = secs / 60 + secs = secs % 60 + return "%d:%02d" % (math.trunc(mins), int(round(secs))) + + if format in ("html", "pretty"): + res = [ + (f, ms_to_mins_secs(cost), count, ms_to_mins_secs(total)) + for (f, cost, count, total) in res + ] + + return res + + def cut(self, size, lst): + if len(lst) <= size: + return lst + else: + return lst[:size] + + def generate_output(self, format, limit, dst): + import tablib + + data = tablib.Dataset(headers=["FILE", "TIME", "CHANGES", "TOTAL"]) + res = self.get_sorted_report(format) + if limit is not None: + res = self.cut(limit, res) + for x in res: + data.append(x) + if format == "pretty": + print(data) + else: + file_name = self.name + "." + format + content = None + data.export(format) + if format == "csv": + content = data.csv + elif format == "json": + content = data.json + else: + content = data.html + file_path = mozpath.join(dst, file_name) + with open(file_path, "wb") as f: + f.write(content) + print("Created report: %s" % file_path) diff --git a/python/mozbuild/mozbuild/android_version_code.py b/python/mozbuild/mozbuild/android_version_code.py new file mode 100644 index 0000000000..3b4025bec7 --- /dev/null +++ b/python/mozbuild/mozbuild/android_version_code.py @@ -0,0 +1,197 @@ +# 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/. + +import argparse +import math +import sys +import time + +# Builds before this build ID use the v0 version scheme. Builds after this +# build ID use the v1 version scheme. +V1_CUTOFF = 20150801000000 # YYYYmmddHHMMSS + + +def android_version_code_v0(buildid, cpu_arch=None, min_sdk=0, max_sdk=0): + base = int(str(buildid)[:10]) + # None is interpreted as arm. + if not cpu_arch or cpu_arch == "armeabi-v7a": + # Increment by MIN_SDK_VERSION -- this adds 9 to every build ID as a + # minimum. Our split APK starts at 15. + return base + min_sdk + 0 + elif cpu_arch in ["x86"]: + # Increment the version code by 3 for x86 builds so they are offered to + # x86 phones that have ARM emulators, beating the 2-point advantage that + # the v15+ ARMv7 APK has. If we change our splits in the future, we'll + # need to do this further still. + return base + min_sdk + 3 + else: + raise ValueError( + "Don't know how to compute android:versionCode " + "for CPU arch %s" % cpu_arch + ) + + +def android_version_code_v1(buildid, cpu_arch=None, min_sdk=0, max_sdk=0): + """Generate a v1 android:versionCode. + The important consideration is that version codes be monotonically + increasing (per Android package name) for all published builds. The input + build IDs are based on timestamps and hence are always monotonically + increasing. + + The generated v1 version codes look like (in binary): + + 0111 1000 0010 tttt tttt tttt tttt txpg + + The 17 bits labelled 't' represent the number of hours since midnight on + September 1, 2015. (2015090100 in YYYYMMMDDHH format.) This yields a + little under 15 years worth of hourly build identifiers, since 2**17 / (366 + * 24) =~ 14.92. + + The bits labelled 'x', 'p', and 'g' are feature flags. + + The bit labelled 'x' is 1 if the build is for an x86 or x86-64 architecture, + and 0 otherwise, which means the build is for an ARM or ARM64 architecture. + (Fennec no longer supports ARMv6, so ARM is equivalent to ARMv7. + + ARM64 is also known as AArch64; it is logically ARMv8.) + + For the same release, x86 and x86_64 builds have higher version codes and + take precedence over ARM builds, so that they are preferred over ARM on + devices that have ARM emulation. + + The bit labelled 'p' is 1 if the build is for a 64-bit architecture (x86-64 + or ARM64), and 0 otherwise, which means the build is for a 32-bit + architecture (x86 or ARM). 64-bit builds have higher version codes so + they take precedence over 32-bit builds on devices that support 64-bit. + + The bit labelled 'g' is 1 if the build targets a recent API level, which + is currently always the case, because Firefox no longer ships releases that + are split by API levels. However, we may reintroduce a split in the future, + in which case the release that targets an older API level will + + We throw an explanatory exception when we are within one calendar year of + running out of build events. This gives lots of time to update the version + scheme. The responsible individual should then bump the range (to allow + builds to continue) and use the time remaining to update the version scheme + via the reserved high order bits. + + N.B.: the reserved 0 bit to the left of the highest order 't' bit can, + sometimes, be used to bump the version scheme. In addition, by reducing the + granularity of the build identifiers (for example, moving to identifying + builds every 2 or 4 hours), the version scheme may be adjusted further still + without losing a (valuable) high order bit. + """ + + def hours_since_cutoff(buildid): + # The ID is formatted like YYYYMMDDHHMMSS (using + # datetime.now().strftime('%Y%m%d%H%M%S'); see build/variables.py). + # The inverse function is time.strptime. + # N.B.: the time module expresses time as decimal seconds since the + # epoch. + fmt = "%Y%m%d%H%M%S" + build = time.strptime(str(buildid), fmt) + cutoff = time.strptime(str(V1_CUTOFF), fmt) + return int( + math.floor((time.mktime(build) - time.mktime(cutoff)) / (60.0 * 60.0)) + ) + + # Of the 21 low order bits, we take 17 bits for builds. + base = hours_since_cutoff(buildid) + if base < 0: + raise ValueError( + "Something has gone horribly wrong: cannot calculate " + "android:versionCode from build ID %s: hours underflow " + "bits allotted!" % buildid + ) + if base > 2**17: + raise ValueError( + "Something has gone horribly wrong: cannot calculate " + "android:versionCode from build ID %s: hours overflow " + "bits allotted!" % buildid + ) + if base > 2**17 - 366 * 24: + raise ValueError( + "Running out of low order bits calculating " + "android:versionCode from build ID %s: " + "; YOU HAVE ONE YEAR TO UPDATE THE VERSION SCHEME." % buildid + ) + + version = 0b1111000001000000000000000000000 + # We reserve 1 "middle" high order bit for the future, and 3 low order bits + # for architecture and APK splits. + version |= base << 3 + + # 'x' bit is 1 for x86/x86-64 architectures (`None` is interpreted as ARM). + if cpu_arch in ["x86", "x86_64"]: + version |= 1 << 2 + elif not cpu_arch or cpu_arch in ["armeabi-v7a", "arm64-v8a"]: + pass + else: + raise ValueError( + "Don't know how to compute android:versionCode " + "for CPU arch %s" % cpu_arch + ) + + # 'p' bit is 1 for 64-bit architectures. + if cpu_arch in ["arm64-v8a", "x86_64"]: + version |= 1 << 1 + elif cpu_arch in ["armeabi-v7a", "x86"]: + pass + else: + raise ValueError( + "Don't know how to compute android:versionCode " + "for CPU arch %s" % cpu_arch + ) + + # 'g' bit is currently always 1, but may depend on `min_sdk` in the future. + version |= 1 << 0 + + return version + + +def android_version_code(buildid, *args, **kwargs): + base = int(str(buildid)) + if base < V1_CUTOFF: + return android_version_code_v0(buildid, *args, **kwargs) + else: + return android_version_code_v1(buildid, *args, **kwargs) + + +def main(argv): + parser = argparse.ArgumentParser("Generate an android:versionCode", add_help=False) + parser.add_argument( + "--verbose", action="store_true", default=False, help="Be verbose" + ) + parser.add_argument( + "--with-android-cpu-arch", + dest="cpu_arch", + choices=["armeabi", "armeabi-v7a", "arm64-v8a", "x86", "x86_64"], + help="The target CPU architecture", + ) + parser.add_argument( + "--with-android-min-sdk-version", + dest="min_sdk", + type=int, + default=0, + help="The minimum target SDK", + ) + parser.add_argument( + "--with-android-max-sdk-version", + dest="max_sdk", + type=int, + default=0, + help="The maximum target SDK", + ) + parser.add_argument("buildid", type=int, help="The input build ID") + + args = parser.parse_args(argv) + code = android_version_code( + args.buildid, cpu_arch=args.cpu_arch, min_sdk=args.min_sdk, max_sdk=args.max_sdk + ) + print(code) + return 0 + + +if __name__ == "__main__": + sys.exit(main(sys.argv[1:])) diff --git a/python/mozbuild/mozbuild/artifact_builds.py b/python/mozbuild/mozbuild/artifact_builds.py new file mode 100644 index 0000000000..a4d2a0bdd2 --- /dev/null +++ b/python/mozbuild/mozbuild/artifact_builds.py @@ -0,0 +1,27 @@ +# 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/. + +# The values correspond to entries at +# https://tools.taskcluster.net/index/artifacts/#gecko.v2.mozilla-central.latest/gecko.v2.mozilla-central.latest +JOB_CHOICES = { + "android-arm-opt", + "android-arm-debug", + "android-x86-opt", + "android-x86_64-opt", + "android-x86_64-debug", + "android-aarch64-opt", + "android-aarch64-debug", + "linux-opt", + "linux-debug", + "linux64-opt", + "linux64-debug", + "macosx64-opt", + "macosx64-debug", + "win32-opt", + "win32-debug", + "win64-opt", + "win64-debug", + "win64-aarch64-opt", + "win64-aarch64-debug", +} diff --git a/python/mozbuild/mozbuild/artifact_cache.py b/python/mozbuild/mozbuild/artifact_cache.py new file mode 100644 index 0000000000..572953e1f7 --- /dev/null +++ b/python/mozbuild/mozbuild/artifact_cache.py @@ -0,0 +1,251 @@ +# 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/. + +""" +Fetch and cache artifacts from URLs. + +This module manages fetching artifacts from URLS and purging old +artifacts using a simple Least Recently Used cache. + +This module requires certain modules be importable from the ambient Python +environment. Consumers will need to arrange this themselves. + +The bulk of the complexity is in managing and persisting several caches. If +we found a Python LRU cache that pickled cleanly, we could remove a lot of +this code! Sadly, I found no such candidate implementations, so we pickle +pylru caches manually. + +None of the instances (or the underlying caches) are safe for concurrent use. +A future need, perhaps. +""" + + +import binascii +import hashlib +import logging +import os + +import dlmanager +import mozpack.path as mozpath +import six +import six.moves.urllib.parse as urlparse + +from mozbuild.util import mkdir + +# Using 'DownloadManager' through the provided interface we +# can't directly specify a 'chunk_size' for the 'Download' it manages. +# One way to get it to use the 'chunk_size' we want is to monkeypatch +# the defaults of the init function for the 'Download' class. +CHUNK_SIZE = 16 * 1024 * 1024 # 16 MB in bytes. +dl_init = dlmanager.Download.__init__ +dl_init.__defaults__ = ( + dl_init.__defaults__[:1] + (CHUNK_SIZE,) + dl_init.__defaults__[2:] +) + + +# Minimum number of downloaded artifacts to keep. Each artifact can be very large, +# so don't make this to large! +MIN_CACHED_ARTIFACTS = 12 + +# Maximum size of the downloaded artifacts to keep in cache, in bytes (2GiB). +MAX_CACHED_ARTIFACTS_SIZE = 2 * 1024 * 1024 * 1024 + + +class ArtifactPersistLimit(dlmanager.PersistLimit): + """Handle persistence for a cache of artifacts. + + When instantiating a DownloadManager, it starts by filling the + PersistLimit instance it's given with register_dir_content. + In practice, this registers all the files already in the cache directory. + After a download finishes, the newly downloaded file is registered, and the + oldest files registered to the PersistLimit instance are removed depending + on the size and file limits it's configured for. + + This is all good, but there are a few tweaks we want here: + + - We have pickle files in the cache directory that we don't want purged. + - Files that were just downloaded in the same session shouldn't be + purged. (if for some reason we end up downloading more than the default + max size, we don't want the files to be purged) + + To achieve this, this subclass of PersistLimit inhibits the register_file + method for pickle files and tracks what files were downloaded in the same + session to avoid removing them. + + The register_file method may be used to register cache matches too, so that + later sessions know they were freshly used. + """ + + def __init__(self, log=None): + super(ArtifactPersistLimit, self).__init__( + size_limit=MAX_CACHED_ARTIFACTS_SIZE, file_limit=MIN_CACHED_ARTIFACTS + ) + self._log = log + self._registering_dir = False + self._downloaded_now = set() + + def log(self, *args, **kwargs): + if self._log: + self._log(*args, **kwargs) + + def register_file(self, path): + if ( + path.endswith(".pickle") + or path.endswith(".checksum") + or os.path.basename(path) == ".metadata_never_index" + ): + return + if not self._registering_dir: + # Touch the file so that subsequent calls to a mach artifact + # command know it was recently used. While remove_old_files + # is based on access time, in various cases, the access time is not + # updated when just reading the file, so we force an update. + try: + os.utime(path, None) + except OSError: + pass + self._downloaded_now.add(path) + super(ArtifactPersistLimit, self).register_file(path) + + def register_dir_content(self, directory, pattern="*"): + self._registering_dir = True + super(ArtifactPersistLimit, self).register_dir_content(directory, pattern) + self._registering_dir = False + + def remove_old_files(self): + from dlmanager import fs + + files = sorted(self.files, key=lambda f: f.stat.st_atime) + kept = [] + while len(files) > self.file_limit and self._files_size >= self.size_limit: + f = files.pop(0) + if f.path in self._downloaded_now: + kept.append(f) + continue + try: + fs.remove(f.path) + except WindowsError: + # For some reason, on automation, we can't remove those files. + # So for now, ignore the error. + kept.append(f) + continue + self.log( + logging.INFO, + "artifact", + {"filename": f.path}, + "Purged artifact {filename}", + ) + self._files_size -= f.stat.st_size + self.files = files + kept + + def remove_all(self): + from dlmanager import fs + + for f in self.files: + fs.remove(f.path) + self._files_size = 0 + self.files = [] + + +class ArtifactCache(object): + """Fetch artifacts from URLS and purge least recently used artifacts from disk.""" + + def __init__(self, cache_dir, log=None, skip_cache=False): + mkdir(cache_dir, not_indexed=True) + self._cache_dir = cache_dir + self._log = log + self._skip_cache = skip_cache + self._persist_limit = ArtifactPersistLimit(log) + self._download_manager = dlmanager.DownloadManager( + self._cache_dir, persist_limit=self._persist_limit + ) + self._last_dl_update = -1 + + def log(self, *args, **kwargs): + if self._log: + self._log(*args, **kwargs) + + def fetch(self, url, force=False): + fname = os.path.basename(url) + try: + # Use the file name from the url if it looks like a hash digest. + if len(fname) not in (32, 40, 56, 64, 96, 128): + raise TypeError() + binascii.unhexlify(fname) + except (TypeError, binascii.Error): + # We download to a temporary name like HASH[:16]-basename to + # differentiate among URLs with the same basenames. We used to then + # extract the build ID from the downloaded artifact and use it to make a + # human readable unique name, but extracting build IDs is time consuming + # (especially on Mac OS X, where we must mount a large DMG file). + hash = hashlib.sha256(six.ensure_binary(url)).hexdigest()[:16] + # Strip query string and fragments. + basename = os.path.basename(urlparse.urlparse(url).path) + fname = hash + "-" + basename + + path = os.path.abspath(mozpath.join(self._cache_dir, fname)) + if self._skip_cache and os.path.exists(path): + self.log( + logging.INFO, + "artifact", + {"path": path}, + "Skipping cache: removing cached downloaded artifact {path}", + ) + os.remove(path) + + try: + dl = self._download_manager.download(url, fname) + + def download_progress(dl, bytes_so_far, total_size): + if not total_size: + return + percent = (float(bytes_so_far) / total_size) * 100 + now = int(percent / 5) + if now == self._last_dl_update: + return + self._last_dl_update = now + self.log( + logging.INFO, + "artifact", + { + "bytes_so_far": bytes_so_far, + "total_size": total_size, + "percent": percent, + }, + "Downloading... {percent:02.1f} %", + ) + + if dl: + self.log( + logging.INFO, + "artifact", + {"path": path}, + "Downloading artifact to local cache: {path}", + ) + dl.set_progress(download_progress) + dl.wait() + else: + self.log( + logging.INFO, + "artifact", + {"path": path}, + "Using artifact from local cache: {path}", + ) + # Avoid the file being removed if it was in the cache already. + path = os.path.join(self._cache_dir, fname) + self._persist_limit.register_file(path) + + return os.path.abspath(mozpath.join(self._cache_dir, fname)) + finally: + # Cancel any background downloads in progress. + self._download_manager.cancel() + + def clear_cache(self): + if self._skip_cache: + self.log( + logging.INFO, "artifact", {}, "Skipping cache: ignoring clear_cache!" + ) + return + + self._persist_limit.remove_all() diff --git a/python/mozbuild/mozbuild/artifact_commands.py b/python/mozbuild/mozbuild/artifact_commands.py new file mode 100644 index 0000000000..62406f406a --- /dev/null +++ b/python/mozbuild/mozbuild/artifact_commands.py @@ -0,0 +1,625 @@ +# 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 __future__ import absolute_import + +import argparse +import hashlib +import json +import logging +import os +import shutil +from collections import OrderedDict + +# As a result of the selective module loading changes, this import has to be +# done here. It is not explicitly used, but it has an implicit side-effect +# (bringing in TASKCLUSTER_ROOT_URL) which is necessary. +import gecko_taskgraph.main # noqa: F401 +import mozversioncontrol +import six +from mach.decorators import Command, CommandArgument, SubCommand + +from mozbuild.artifact_builds import JOB_CHOICES +from mozbuild.base import MachCommandConditions as conditions +from mozbuild.util import ensureParentDir + +_COULD_NOT_FIND_ARTIFACTS_TEMPLATE = ( + "ERROR!!!!!! Could not find artifacts for a toolchain build named " + "`{build}`. Local commits, dirty/stale files, and other changes in your " + "checkout may cause this error. Make sure you are on a fresh, current " + "checkout of mozilla-central. Beware that commands like `mach bootstrap` " + "and `mach artifact` are unlikely to work on any versions of the code " + "besides recent revisions of mozilla-central." +) + + +class SymbolsAction(argparse.Action): + def __call__(self, parser, namespace, values, option_string=None): + # If this function is called, it means the --symbols option was given, + # so we want to store the value `True` if no explicit value was given + # to the option. + setattr(namespace, self.dest, values or True) + + +class ArtifactSubCommand(SubCommand): + def __call__(self, func): + after = SubCommand.__call__(self, func) + args = [ + CommandArgument("--tree", metavar="TREE", type=str, help="Firefox tree."), + CommandArgument( + "--job", metavar="JOB", choices=JOB_CHOICES, help="Build job." + ), + CommandArgument( + "--verbose", "-v", action="store_true", help="Print verbose output." + ), + ] + for arg in args: + after = arg(after) + return after + + +# Fetch and install binary artifacts from Mozilla automation. + + +@Command( + "artifact", + category="post-build", + description="Use pre-built artifacts to build Firefox.", +) +def artifact(command_context): + """Download, cache, and install pre-built binary artifacts to build Firefox. + + Use ``mach build`` as normal to freshen your installed binary libraries: + artifact builds automatically download, cache, and install binary + artifacts from Mozilla automation, replacing whatever may be in your + object directory. Use ``mach artifact last`` to see what binary artifacts + were last used. + + Never build libxul again! + + """ + pass + + +def _make_artifacts( + command_context, + tree=None, + job=None, + skip_cache=False, + download_tests=True, + download_symbols=False, + download_maven_zip=False, + no_process=False, +): + state_dir = command_context._mach_context.state_dir + cache_dir = os.path.join(state_dir, "package-frontend") + + hg = None + if conditions.is_hg(command_context): + hg = command_context.substs["HG"] + + git = None + if conditions.is_git(command_context): + git = command_context.substs["GIT"] + + # If we're building Thunderbird, we should be checking for comm-central artifacts. + topsrcdir = command_context.substs.get("commtopsrcdir", command_context.topsrcdir) + + if download_maven_zip: + if download_tests: + raise ValueError("--maven-zip requires --no-tests") + if download_symbols: + raise ValueError("--maven-zip requires no --symbols") + if not no_process: + raise ValueError("--maven-zip requires --no-process") + + from mozbuild.artifacts import Artifacts + + artifacts = Artifacts( + tree, + command_context.substs, + command_context.defines, + job, + log=command_context.log, + cache_dir=cache_dir, + skip_cache=skip_cache, + hg=hg, + git=git, + topsrcdir=topsrcdir, + download_tests=download_tests, + download_symbols=download_symbols, + download_maven_zip=download_maven_zip, + no_process=no_process, + mozbuild=command_context, + ) + return artifacts + + +@ArtifactSubCommand( + "artifact", + "install", + "Install a good pre-built artifact.", +) +@CommandArgument( + "source", + metavar="SRC", + nargs="?", + type=str, + help="Where to fetch and install artifacts from. Can be omitted, in " + "which case the current hg repository is inspected; an hg revision; " + "a remote URL; or a local file.", + default=None, +) +@CommandArgument( + "--skip-cache", + action="store_true", + help="Skip all local caches to force re-fetching remote artifacts.", + default=False, +) +@CommandArgument("--no-tests", action="store_true", help="Don't install tests.") +@CommandArgument("--symbols", nargs="?", action=SymbolsAction, help="Download symbols.") +@CommandArgument("--distdir", help="Where to install artifacts to.") +@CommandArgument( + "--no-process", + action="store_true", + help="Don't process (unpack) artifact packages, just download them.", +) +@CommandArgument( + "--maven-zip", action="store_true", help="Download Maven zip (Android-only)." +) +def artifact_install( + command_context, + source=None, + skip_cache=False, + tree=None, + job=None, + verbose=False, + no_tests=False, + symbols=False, + distdir=None, + no_process=False, + maven_zip=False, +): + command_context._set_log_level(verbose) + artifacts = _make_artifacts( + command_context, + tree=tree, + job=job, + skip_cache=skip_cache, + download_tests=not no_tests, + download_symbols=symbols, + download_maven_zip=maven_zip, + no_process=no_process, + ) + + return artifacts.install_from(source, distdir or command_context.distdir) + + +@ArtifactSubCommand( + "artifact", + "clear-cache", + "Delete local artifacts and reset local artifact cache.", +) +def artifact_clear_cache(command_context, tree=None, job=None, verbose=False): + command_context._set_log_level(verbose) + artifacts = _make_artifacts(command_context, tree=tree, job=job) + artifacts.clear_cache() + return 0 + + +@SubCommand( + "artifact", + "toolchain", +) +@CommandArgument("--verbose", "-v", action="store_true", help="Print verbose output.") +@CommandArgument( + "--cache-dir", + metavar="DIR", + help="Directory where to store the artifacts cache", +) +@CommandArgument( + "--skip-cache", + action="store_true", + help="Skip all local caches to force re-fetching remote artifacts.", + default=False, +) +@CommandArgument( + "--from-build", + metavar="BUILD", + nargs="+", + help="Download toolchains resulting from the given build(s); " + "BUILD is a name of a toolchain task, e.g. linux64-clang", +) +@CommandArgument( + "--from-task", + metavar="TASK_ID:ARTIFACT", + nargs="+", + help="Download toolchain artifact from a given task.", +) +@CommandArgument( + "--tooltool-manifest", + metavar="MANIFEST", + help="Explicit tooltool manifest to process", +) +@CommandArgument( + "--no-unpack", action="store_true", help="Do not unpack any downloaded file" +) +@CommandArgument( + "--retry", type=int, default=4, help="Number of times to retry failed downloads" +) +@CommandArgument( + "--bootstrap", + action="store_true", + help="Whether this is being called from bootstrap. " + "This verifies the toolchain is annotated as a toolchain used for local development.", +) +@CommandArgument( + "--artifact-manifest", + metavar="FILE", + help="Store a manifest about the downloaded taskcluster artifacts", +) +def artifact_toolchain( + command_context, + verbose=False, + cache_dir=None, + skip_cache=False, + from_build=(), + from_task=(), + tooltool_manifest=None, + no_unpack=False, + retry=0, + bootstrap=False, + artifact_manifest=None, +): + """Download, cache and install pre-built toolchains.""" + import time + + import redo + import requests + from taskgraph.util.taskcluster import get_artifact_url + + from mozbuild.action.tooltool import FileRecord, open_manifest, unpack_file + from mozbuild.artifacts import ArtifactCache + + start = time.monotonic() + command_context._set_log_level(verbose) + # Normally, we'd use command_context.log_manager.enable_unstructured(), + # but that enables all logging, while we only really want tooltool's + # and it also makes structured log output twice. + # So we manually do what it does, and limit that to the tooltool + # logger. + if command_context.log_manager.terminal_handler: + logging.getLogger("mozbuild.action.tooltool").addHandler( + command_context.log_manager.terminal_handler + ) + logging.getLogger("redo").addHandler( + command_context.log_manager.terminal_handler + ) + command_context.log_manager.terminal_handler.addFilter( + command_context.log_manager.structured_filter + ) + if not cache_dir: + cache_dir = os.path.join(command_context._mach_context.state_dir, "toolchains") + + tooltool_host = os.environ.get("TOOLTOOL_HOST", "tooltool.mozilla-releng.net") + taskcluster_proxy_url = os.environ.get("TASKCLUSTER_PROXY_URL") + if taskcluster_proxy_url: + tooltool_url = "{}/{}".format(taskcluster_proxy_url, tooltool_host) + else: + tooltool_url = "https://{}".format(tooltool_host) + + cache = ArtifactCache( + cache_dir=cache_dir, log=command_context.log, skip_cache=skip_cache + ) + + class DownloadRecord(FileRecord): + def __init__(self, url, *args, **kwargs): + super(DownloadRecord, self).__init__(*args, **kwargs) + self.url = url + self.basename = self.filename + + def fetch_with(self, cache): + self.filename = cache.fetch(self.url) + return self.filename + + def validate(self): + if self.size is None and self.digest is None: + return True + return super(DownloadRecord, self).validate() + + class ArtifactRecord(DownloadRecord): + def __init__(self, task_id, artifact_name): + for _ in redo.retrier(attempts=retry + 1, sleeptime=60): + cot = cache._download_manager.session.get( + get_artifact_url(task_id, "public/chain-of-trust.json") + ) + if cot.status_code >= 500: + continue + cot.raise_for_status() + break + else: + cot.raise_for_status() + + digest = algorithm = None + data = json.loads(cot.text) + for algorithm, digest in ( + data.get("artifacts", {}).get(artifact_name, {}).items() + ): + pass + + name = os.path.basename(artifact_name) + artifact_url = get_artifact_url( + task_id, + artifact_name, + use_proxy=not artifact_name.startswith("public/"), + ) + super(ArtifactRecord, self).__init__( + artifact_url, name, None, digest, algorithm, unpack=True + ) + + records = OrderedDict() + downloaded = [] + + if tooltool_manifest: + manifest = open_manifest(tooltool_manifest) + for record in manifest.file_records: + url = "{}/{}/{}".format(tooltool_url, record.algorithm, record.digest) + records[record.filename] = DownloadRecord( + url, + record.filename, + record.size, + record.digest, + record.algorithm, + unpack=record.unpack, + version=record.version, + visibility=record.visibility, + ) + + if from_build: + if "MOZ_AUTOMATION" in os.environ: + command_context.log( + logging.ERROR, + "artifact", + {}, + "Do not use --from-build in automation; all dependencies " + "should be determined in the decision task.", + ) + return 1 + from taskgraph.optimize.strategies import IndexSearch + + from mozbuild.toolchains import toolchain_task_definitions + + tasks = toolchain_task_definitions() + + for b in from_build: + user_value = b + + if not b.startswith("toolchain-"): + b = "toolchain-{}".format(b) + + task = tasks.get(b) + if not task: + command_context.log( + logging.ERROR, + "artifact", + {"build": user_value}, + "Could not find a toolchain build named `{build}`", + ) + return 1 + + # Ensure that toolchains installed by `mach bootstrap` have the + # `local-toolchain attribute set. Taskgraph ensures that these + # are built on trunk projects, so the task will be available to + # install here. + if bootstrap and not task.attributes.get("local-toolchain"): + command_context.log( + logging.ERROR, + "artifact", + {"build": user_value}, + "Toolchain `{build}` is not annotated as used for local development.", + ) + return 1 + + artifact_name = task.attributes.get("toolchain-artifact") + command_context.log( + logging.DEBUG, + "artifact", + { + "name": artifact_name, + "index": task.optimization.get("index-search"), + }, + "Searching for {name} in {index}", + ) + deadline = None + task_id = IndexSearch().should_replace_task( + task, {}, deadline, task.optimization.get("index-search", []) + ) + if task_id in (True, False) or not artifact_name: + command_context.log( + logging.ERROR, + "artifact", + {"build": user_value}, + _COULD_NOT_FIND_ARTIFACTS_TEMPLATE, + ) + # Get and print some helpful info for diagnosis. + repo = mozversioncontrol.get_repository_object( + command_context.topsrcdir + ) + if not isinstance(repo, mozversioncontrol.SrcRepository): + changed_files = set(repo.get_outgoing_files()) | set( + repo.get_changed_files() + ) + if changed_files: + command_context.log( + logging.ERROR, + "artifact", + {}, + "Hint: consider reverting your local changes " + "to the following files: %s" % sorted(changed_files), + ) + if "TASKCLUSTER_ROOT_URL" in os.environ: + command_context.log( + logging.ERROR, + "artifact", + {"build": user_value}, + "Due to the environment variable TASKCLUSTER_ROOT_URL " + "being set, the artifacts were expected to be found " + "on {}. If this was unintended, unset " + "TASKCLUSTER_ROOT_URL and try again.".format( + os.environ["TASKCLUSTER_ROOT_URL"] + ), + ) + return 1 + + command_context.log( + logging.DEBUG, + "artifact", + {"name": artifact_name, "task_id": task_id}, + "Found {name} in {task_id}", + ) + + record = ArtifactRecord(task_id, artifact_name) + records[record.filename] = record + + # Handle the list of files of the form task_id:path from --from-task. + for f in from_task or (): + task_id, colon, name = f.partition(":") + if not colon: + command_context.log( + logging.ERROR, + "artifact", + {}, + "Expected an argument of the form task_id:path", + ) + return 1 + record = ArtifactRecord(task_id, name) + records[record.filename] = record + + for record in six.itervalues(records): + command_context.log( + logging.INFO, + "artifact", + {"name": record.basename}, + "Setting up artifact {name}", + ) + valid = False + # sleeptime is 60 per retry.py, used by tooltool_wrapper.sh + for attempt, _ in enumerate(redo.retrier(attempts=retry + 1, sleeptime=60)): + try: + record.fetch_with(cache) + except ( + requests.exceptions.HTTPError, + requests.exceptions.ChunkedEncodingError, + requests.exceptions.ConnectionError, + ) as e: + if isinstance(e, requests.exceptions.HTTPError): + # The relengapi proxy likes to return error 400 bad request + # which seems improbably to be due to our (simple) GET + # being borked. + status = e.response.status_code + should_retry = status >= 500 or status == 400 + else: + should_retry = True + + if should_retry or attempt < retry: + level = logging.WARN + else: + level = logging.ERROR + command_context.log(level, "artifact", {}, str(e)) + if not should_retry: + break + if attempt < retry: + command_context.log( + logging.INFO, "artifact", {}, "Will retry in a moment..." + ) + continue + try: + valid = record.validate() + except Exception: + pass + if not valid: + os.unlink(record.filename) + if attempt < retry: + command_context.log( + logging.INFO, + "artifact", + {}, + "Corrupt download. Will retry in a moment...", + ) + continue + + downloaded.append(record) + break + + if not valid: + command_context.log( + logging.ERROR, + "artifact", + {"name": record.basename}, + "Failed to download {name}", + ) + return 1 + + artifacts = {} if artifact_manifest else None + + for record in downloaded: + local = os.path.join(os.getcwd(), record.basename) + if os.path.exists(local): + os.unlink(local) + # unpack_file needs the file with its final name to work + # (https://github.com/mozilla/build-tooltool/issues/38), so we + # need to copy it, even though we remove it later. Use hard links + # when possible. + try: + os.link(record.filename, local) + except Exception: + shutil.copy(record.filename, local) + # Keep a sha256 of each downloaded file, for the chain-of-trust + # validation. + if artifact_manifest is not None: + with open(local, "rb") as fh: + h = hashlib.sha256() + while True: + data = fh.read(1024 * 1024) + if not data: + break + h.update(data) + artifacts[record.url] = {"sha256": h.hexdigest()} + if record.unpack and not no_unpack: + unpack_file(local) + os.unlink(local) + + if not downloaded: + command_context.log(logging.ERROR, "artifact", {}, "Nothing to download") + if from_task: + return 1 + + if artifacts: + ensureParentDir(artifact_manifest) + with open(artifact_manifest, "w") as fh: + json.dump(artifacts, fh, indent=4, sort_keys=True) + + if "MOZ_AUTOMATION" in os.environ: + end = time.monotonic() + + perfherder_data = { + "framework": {"name": "build_metrics"}, + "suites": [ + { + "name": "mach_artifact_toolchain", + "value": end - start, + "lowerIsBetter": True, + "shouldAlert": False, + "subtests": [], + } + ], + } + command_context.log( + logging.INFO, + "perfherder", + {"data": json.dumps(perfherder_data)}, + "PERFHERDER_DATA: {data}", + ) + + return 0 diff --git a/python/mozbuild/mozbuild/artifacts.py b/python/mozbuild/mozbuild/artifacts.py new file mode 100644 index 0000000000..c82a39694c --- /dev/null +++ b/python/mozbuild/mozbuild/artifacts.py @@ -0,0 +1,1671 @@ +# 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/. + +""" +Fetch build artifacts from a Firefox tree. + +This provides an (at-the-moment special purpose) interface to download Android +artifacts from Mozilla's Task Cluster. + +This module performs the following steps: + +* find a candidate hg parent revision. At one time we used the local pushlog, + which required the mozext hg extension. This isn't feasible with git, and it + is only mildly less efficient to not use the pushlog, so we don't use it even + when querying hg. + +* map the candidate parent to candidate Task Cluster tasks and artifact + locations. Pushlog entries might not correspond to tasks (yet), and those + tasks might not produce the desired class of artifacts. + +* fetch fresh Task Cluster artifacts and purge old artifacts, using a simple + Least Recently Used cache. + +* post-process fresh artifacts, to speed future installation. In particular, + extract relevant files from Mac OS X DMG files into a friendly archive format + so we don't have to mount DMG files frequently. + +This module requires certain modules be importable from the ambient Python +environment. ``mach artifact`` ensures these modules are available, but other +consumers will need to arrange this themselves. +""" + + +import collections +import functools +import glob +import logging +import operator +import os +import pickle +import re +import shutil +import stat +import subprocess +import tarfile +import tempfile +import zipfile +from contextlib import contextmanager +from io import BufferedReader +from urllib.parse import urlparse + +import buildconfig +import mozinstall +import mozpack.path as mozpath +import pylru +import requests +import six +from mach.util import UserError +from mozpack import executables +from mozpack.files import JarFinder, TarFinder +from mozpack.mozjar import JarReader, JarWriter +from mozpack.packager.unpack import UnpackFinder +from taskgraph.util.taskcluster import find_task_id, get_artifact_url, list_artifacts + +from mozbuild.artifact_builds import JOB_CHOICES +from mozbuild.artifact_cache import ArtifactCache +from mozbuild.util import FileAvoidWrite, ensureParentDir, mkdir + +# Number of candidate pushheads to cache per parent changeset. +NUM_PUSHHEADS_TO_QUERY_PER_PARENT = 50 + +# Number of parent changesets to consider as possible pushheads. +# There isn't really such a thing as a reasonable default here, because we don't +# know how many pushheads we'll need to look at to find a build with our artifacts, +# and we don't know how many changesets will be in each push. For now we assume +# we'll find a build in the last 50 pushes, assuming each push contains 10 changesets. +NUM_REVISIONS_TO_QUERY = 500 + +MAX_CACHED_TASKS = 400 # Number of pushheads to cache Task Cluster task data for. + +# Downloaded artifacts are cached, and a subset of their contents extracted for +# easy installation. This is most noticeable on Mac OS X: since mounting and +# copying from DMG files is very slow, we extract the desired binaries to a +# separate archive for fast re-installation. +PROCESSED_SUFFIX = ".processed.jar" + + +class ArtifactJob(object): + trust_domain = "gecko" + default_candidate_trees = [ + "releases/mozilla-release", + ] + nightly_candidate_trees = [ + "mozilla-central", + "integration/autoland", + ] + beta_candidate_trees = [ + "releases/mozilla-beta", + ] + # The list below list should be updated when we have new ESRs. + esr_candidate_trees = [ + "releases/mozilla-esr115", + ] + try_tree = "try" + + # These are a subset of TEST_HARNESS_BINS in testing/mochitest/Makefile.in. + # Each item is a pair of (pattern, (src_prefix, dest_prefix), where src_prefix + # is the prefix of the pattern relevant to its location in the archive, and + # dest_prefix is the prefix to be added that will yield the final path relative + # to dist/. + test_artifact_patterns = { + ("bin/BadCertAndPinningServer", ("bin", "bin")), + ("bin/DelegatedCredentialsServer", ("bin", "bin")), + ("bin/EncryptedClientHelloServer", ("bin", "bin")), + ("bin/FaultyServer", ("bin", "bin")), + ("bin/GenerateOCSPResponse", ("bin", "bin")), + ("bin/OCSPStaplingServer", ("bin", "bin")), + ("bin/SanctionsTestServer", ("bin", "bin")), + ("bin/certutil", ("bin", "bin")), + ("bin/geckodriver", ("bin", "bin")), + ("bin/pk12util", ("bin", "bin")), + ("bin/screentopng", ("bin", "bin")), + ("bin/ssltunnel", ("bin", "bin")), + ("bin/xpcshell", ("bin", "bin")), + ("bin/plugin-container", ("bin", "bin")), + ("bin/http3server", ("bin", "bin")), + ("bin/plugins/gmp-*/*/*", ("bin/plugins", "bin")), + ("bin/plugins/*", ("bin/plugins", "plugins")), + } + + # We can tell our input is a test archive by this suffix, which happens to + # be the same across platforms. + _test_zip_archive_suffix = ".common.tests.zip" + _test_tar_archive_suffix = ".common.tests.tar.gz" + + # A map of extra archives to fetch and unpack. An extra archive might + # include optional build output to incorporate into the local artifact + # build. Test archives and crashreporter symbols could be extra archives + # but they require special handling; this mechanism is generic and intended + # only for the simplest cases. + # + # Each suffix key matches a candidate archive (i.e., an artifact produced by + # an upstream build). Each value is itself a dictionary that must contain + # the following keys: + # + # - `description`: a purely informational string description. + # - `src_prefix`: entry names in the archive with leading `src_prefix` will + # have the prefix stripped. + # - `dest_prefix`: entry names in the archive will have `dest_prefix` + # prepended. + # + # The entries in the archive, suitably renamed, will be extracted into `dist`. + _extra_archives = { + ".xpt_artifacts.zip": { + "description": "XPT Artifacts", + "src_prefix": "", + "dest_prefix": "xpt_artifacts", + }, + } + _extra_archive_suffixes = tuple(sorted(_extra_archives.keys())) + + def __init__( + self, + log=None, + download_tests=True, + download_symbols=False, + download_maven_zip=False, + substs=None, + mozbuild=None, + ): + self._package_re = re.compile(self.package_re) + self._tests_re = None + if download_tests: + self._tests_re = re.compile( + r"public/build/(en-US/)?target\.common\.tests\.(zip|tar\.gz)$" + ) + self._maven_zip_re = None + if download_maven_zip: + self._maven_zip_re = re.compile(r"public/build/target\.maven\.zip$") + self._log = log + self._substs = substs + self._symbols_archive_suffix = None + if download_symbols == "full": + self._symbols_archive_suffix = "crashreporter-symbols-full.tar.zst" + elif download_symbols: + self._symbols_archive_suffix = "crashreporter-symbols.zip" + self._mozbuild = mozbuild + self._candidate_trees = None + + def log(self, *args, **kwargs): + if self._log: + self._log(*args, **kwargs) + + def find_candidate_artifacts(self, artifacts): + # TODO: Handle multiple artifacts, taking the latest one. + tests_artifact = None + maven_zip_artifact = None + for artifact in artifacts: + name = artifact["name"] + if self._maven_zip_re: + if self._maven_zip_re.match(name): + maven_zip_artifact = name + yield name + else: + continue + elif self._package_re and self._package_re.match(name): + yield name + elif self._tests_re and self._tests_re.match(name): + tests_artifact = name + yield name + elif self._symbols_archive_suffix and name.endswith( + self._symbols_archive_suffix + ): + yield name + elif name.endswith(ArtifactJob._extra_archive_suffixes): + yield name + else: + self.log( + logging.DEBUG, + "artifact", + {"name": name}, + "Not yielding artifact named {name} as a candidate artifact", + ) + if self._tests_re and not tests_artifact: + raise ValueError( + 'Expected tests archive matching "{re}", but ' + "found none!".format(re=self._tests_re) + ) + if self._maven_zip_re and not maven_zip_artifact: + raise ValueError( + 'Expected Maven zip archive matching "{re}", but ' + "found none!".format(re=self._maven_zip_re) + ) + + @contextmanager + def get_writer(self, **kwargs): + with JarWriter(**kwargs) as writer: + yield writer + + def process_artifact(self, filename, processed_filename): + if filename.endswith(ArtifactJob._test_zip_archive_suffix) and self._tests_re: + return self.process_tests_zip_artifact(filename, processed_filename) + if filename.endswith(ArtifactJob._test_tar_archive_suffix) and self._tests_re: + return self.process_tests_tar_artifact(filename, processed_filename) + if self._symbols_archive_suffix and filename.endswith( + self._symbols_archive_suffix + ): + return self.process_symbols_archive(filename, processed_filename) + if filename.endswith(ArtifactJob._extra_archive_suffixes): + return self.process_extra_archive(filename, processed_filename) + return self.process_package_artifact(filename, processed_filename) + + def process_package_artifact(self, filename, processed_filename): + raise NotImplementedError( + "Subclasses must specialize process_package_artifact!" + ) + + def process_tests_zip_artifact(self, filename, processed_filename): + from mozbuild.action.test_archive import OBJDIR_TEST_FILES + + added_entry = False + + with self.get_writer(file=processed_filename, compress_level=5) as writer: + reader = JarReader(filename) + for filename, entry in six.iteritems(reader.entries): + for pattern, (src_prefix, dest_prefix) in self.test_artifact_patterns: + if not mozpath.match(filename, pattern): + continue + destpath = mozpath.relpath(filename, src_prefix) + destpath = mozpath.join(dest_prefix, destpath) + self.log( + logging.DEBUG, + "artifact", + {"destpath": destpath}, + "Adding {destpath} to processed archive", + ) + mode = entry["external_attr"] >> 16 + writer.add(destpath.encode("utf-8"), reader[filename], mode=mode) + added_entry = True + break + + if filename.endswith(".toml"): + # The artifact build writes test .toml files into the object + # directory; they don't come from the upstream test archive. + self.log( + logging.DEBUG, + "artifact", + {"filename": filename}, + "Skipping test INI file {filename}", + ) + continue + + for files_entry in OBJDIR_TEST_FILES.values(): + origin_pattern = files_entry["pattern"] + leaf_filename = filename + if "dest" in files_entry: + dest = files_entry["dest"] + origin_pattern = mozpath.join(dest, origin_pattern) + leaf_filename = filename[len(dest) + 1 :] + if mozpath.match(filename, origin_pattern): + destpath = mozpath.join( + "..", files_entry["base"], leaf_filename + ) + mode = entry["external_attr"] >> 16 + writer.add( + destpath.encode("utf-8"), reader[filename], mode=mode + ) + + if not added_entry: + raise ValueError( + 'Archive format changed! No pattern from "{patterns}"' + "matched an archive path.".format( + patterns=LinuxArtifactJob.test_artifact_patterns + ) + ) + + def process_tests_tar_artifact(self, filename, processed_filename): + from mozbuild.action.test_archive import OBJDIR_TEST_FILES + + added_entry = False + + with self.get_writer(file=processed_filename, compress_level=5) as writer: + with tarfile.open(filename) as reader: + for filename, entry in TarFinder(filename, reader): + for ( + pattern, + (src_prefix, dest_prefix), + ) in self.test_artifact_patterns: + if not mozpath.match(filename, pattern): + continue + + destpath = mozpath.relpath(filename, src_prefix) + destpath = mozpath.join(dest_prefix, destpath) + self.log( + logging.DEBUG, + "artifact", + {"destpath": destpath}, + "Adding {destpath} to processed archive", + ) + mode = entry.mode + writer.add(destpath.encode("utf-8"), entry.open(), mode=mode) + added_entry = True + break + + if filename.endswith(".toml"): + # The artifact build writes test .toml files into the object + # directory; they don't come from the upstream test archive. + self.log( + logging.DEBUG, + "artifact", + {"filename": filename}, + "Skipping test INI file {filename}", + ) + continue + + for files_entry in OBJDIR_TEST_FILES.values(): + origin_pattern = files_entry["pattern"] + leaf_filename = filename + if "dest" in files_entry: + dest = files_entry["dest"] + origin_pattern = mozpath.join(dest, origin_pattern) + leaf_filename = filename[len(dest) + 1 :] + if mozpath.match(filename, origin_pattern): + destpath = mozpath.join( + "..", files_entry["base"], leaf_filename + ) + mode = entry.mode + writer.add( + destpath.encode("utf-8"), entry.open(), mode=mode + ) + + if not added_entry: + raise ValueError( + 'Archive format changed! No pattern from "{patterns}"' + "matched an archive path.".format( + patterns=LinuxArtifactJob.test_artifact_patterns + ) + ) + + def process_symbols_archive( + self, filename, processed_filename, skip_compressed=False + ): + with self.get_writer(file=processed_filename, compress_level=5) as writer: + for filename, entry in self.iter_artifact_archive(filename): + if skip_compressed and filename.endswith(".gz"): + self.log( + logging.DEBUG, + "artifact", + {"filename": filename}, + "Skipping compressed ELF debug symbol file {filename}", + ) + continue + destpath = mozpath.join("crashreporter-symbols", filename) + self.log( + logging.INFO, + "artifact", + {"destpath": destpath}, + "Adding {destpath} to processed archive", + ) + writer.add(destpath.encode("utf-8"), entry) + + def process_extra_archive(self, filename, processed_filename): + for suffix, extra_archive in ArtifactJob._extra_archives.items(): + if filename.endswith(suffix): + self.log( + logging.INFO, + "artifact", + {"filename": filename, "description": extra_archive["description"]}, + '"{filename}" is a recognized extra archive ({description})', + ) + break + else: + raise ValueError('"{}" is not a recognized extra archive!'.format(filename)) + + src_prefix = extra_archive["src_prefix"] + dest_prefix = extra_archive["dest_prefix"] + + with self.get_writer(file=processed_filename, compress_level=5) as writer: + for filename, entry in self.iter_artifact_archive(filename): + if not filename.startswith(src_prefix): + self.log( + logging.DEBUG, + "artifact", + {"filename": filename, "src_prefix": src_prefix}, + "Skipping extra archive item {filename} " + "that does not start with {src_prefix}", + ) + continue + destpath = mozpath.relpath(filename, src_prefix) + destpath = mozpath.join(dest_prefix, destpath) + self.log( + logging.INFO, + "artifact", + {"destpath": destpath}, + "Adding {destpath} to processed archive", + ) + writer.add(destpath.encode("utf-8"), entry) + + def iter_artifact_archive(self, filename): + if filename.endswith(".zip"): + reader = JarReader(filename) + for filename in reader.entries: + yield filename, reader[filename] + elif filename.endswith(".tar.zst") and self._mozbuild is not None: + self._mozbuild._ensure_zstd() + import zstandard + + ctx = zstandard.ZstdDecompressor() + uncompressed = ctx.stream_reader(open(filename, "rb")) + with tarfile.open( + mode="r|", fileobj=uncompressed, bufsize=1024 * 1024 + ) as reader: + while True: + info = reader.next() + if info is None: + break + yield info.name, reader.extractfile(info) + else: + raise RuntimeError("Unsupported archive type for %s" % filename) + + @property + def candidate_trees(self): + if not self._candidate_trees: + self._candidate_trees = self.select_candidate_trees() + return self._candidate_trees + + def select_candidate_trees(self): + source_repo = buildconfig.substs.get("MOZ_SOURCE_REPO", "") + version_display = buildconfig.substs.get("MOZ_APP_VERSION_DISPLAY") + + if "esr" in version_display or "esr" in source_repo: + return self.esr_candidate_trees + elif re.search(r"a\d+$", version_display): + return self.nightly_candidate_trees + elif re.search(r"b\d+$", version_display): + return self.beta_candidate_trees + + return self.default_candidate_trees + + +class AndroidArtifactJob(ArtifactJob): + package_re = r"public/build/geckoview_example\.apk$" + product = "mobile" + + package_artifact_patterns = {"**/*.so"} + + def process_package_artifact(self, filename, processed_filename): + # Extract all .so files into the root, which will get copied into dist/bin. + with self.get_writer(file=processed_filename, compress_level=5) as writer: + for p, f in UnpackFinder(JarFinder(filename, JarReader(filename))): + if not any( + mozpath.match(p, pat) for pat in self.package_artifact_patterns + ): + continue + + dirname, basename = os.path.split(p) + self.log( + logging.DEBUG, + "artifact", + {"basename": basename}, + "Adding {basename} to processed archive", + ) + + basedir = "bin" + if not basename.endswith(".so"): + basedir = mozpath.join("bin", dirname.lstrip("assets/")) + basename = mozpath.join(basedir, basename) + writer.add(basename.encode("utf-8"), f.open()) + + def process_symbols_archive(self, filename, processed_filename): + ArtifactJob.process_symbols_archive( + self, filename, processed_filename, skip_compressed=True + ) + + if not self._symbols_archive_suffix.startswith("crashreporter-symbols-full."): + return + + import gzip + + with self.get_writer(file=processed_filename, compress_level=5) as writer: + for filename, entry in self.iter_artifact_archive(filename): + if not filename.endswith(".gz"): + continue + + # Uncompress "libxul.so/D3271457813E976AE7BF5DAFBABABBFD0/libxul.so.dbg.gz" + # into "libxul.so.dbg". + # + # After running `settings append target.debug-file-search-paths $file`, + # where file=/path/to/topobjdir/dist/crashreporter-symbols, + # Android Studio's lldb (7.0.0, at least) will find the ELF debug symbol files. + # + # There are other paths that will work but none seem more desireable. See + # https://github.com/llvm-mirror/lldb/blob/882670690ca69d9dd96b7236c620987b11894af9/source/Host/common/Symbols.cpp#L324. + basename = os.path.basename(filename).replace(".gz", "") + destpath = mozpath.join("crashreporter-symbols", basename) + self.log( + logging.DEBUG, + "artifact", + {"destpath": destpath}, + "Adding uncompressed ELF debug symbol file " + "{destpath} to processed archive", + ) + writer.add(destpath.encode("utf-8"), gzip.GzipFile(fileobj=entry)) + + +class LinuxArtifactJob(ArtifactJob): + package_re = r"public/build/target\.tar\.bz2$" + product = "firefox" + + _package_artifact_patterns = { + "{product}/crashreporter", + "{product}/dependentlibs.list", + "{product}/{product}", + "{product}/{product}-bin", + "{product}/minidump-analyzer", + "{product}/pingsender", + "{product}/platform.ini", + "{product}/plugin-container", + "{product}/updater", + "{product}/glxtest", + "{product}/v4l2test", + "{product}/vaapitest", + "{product}/**/*.so", + # Preserve signatures when present. + "{product}/**/*.sig", + } + + @property + def package_artifact_patterns(self): + return {p.format(product=self.product) for p in self._package_artifact_patterns} + + def process_package_artifact(self, filename, processed_filename): + added_entry = False + + with self.get_writer(file=processed_filename, compress_level=5) as writer: + with tarfile.open(filename) as reader: + for p, f in UnpackFinder(TarFinder(filename, reader)): + if not any( + mozpath.match(p, pat) for pat in self.package_artifact_patterns + ): + continue + + # We strip off the relative "firefox/" bit from the path, + # but otherwise preserve it. + destpath = mozpath.join("bin", mozpath.relpath(p, self.product)) + self.log( + logging.DEBUG, + "artifact", + {"destpath": destpath}, + "Adding {destpath} to processed archive", + ) + writer.add(destpath.encode("utf-8"), f.open(), mode=f.mode) + added_entry = True + + if not added_entry: + raise ValueError( + 'Archive format changed! No pattern from "{patterns}" ' + "matched an archive path.".format( + patterns=LinuxArtifactJob.package_artifact_patterns + ) + ) + + +class ResignJarWriter(JarWriter): + def __init__(self, job, **kwargs): + super().__init__(**kwargs) + self._job = job + + def add(self, name, data, mode=None): + if self._job._substs["HOST_OS_ARCH"] == "Darwin": + # Wrap in a BufferedReader so that executable.get_type can peek at the + # data signature without subsequent read() being affected. + data = BufferedReader(data) + if executables.get_type(data) == executables.MACHO: + # If the file is a Mach-O binary, we run `codesign -s - -f` against + # it to force a local codesign against the original binary, which is + # likely unsigned. As of writing, only arm64 macs require codesigned + # binaries, but it doesn't hurt to do it on intel macs as well + # preemptively, because they could end up with the same requirement + # in future versions of macOS. + tmp = tempfile.NamedTemporaryFile(delete=False) + try: + shutil.copyfileobj(data, tmp) + tmp.close() + self._job.log( + logging.DEBUG, + "artifact", + {"path": name.decode("utf-8")}, + "Re-signing {path}", + ) + subprocess.check_call( + ["codesign", "-s", "-", "-f", tmp.name], + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + ) + data = open(tmp.name, "rb") + finally: + os.unlink(tmp.name) + super().add(name, data, mode=mode) + + +class MacArtifactJob(ArtifactJob): + package_re = r"public/build/target\.dmg$" + product = "firefox" + + # These get copied into dist/bin without the path, so "root/a/b/c" -> "dist/bin/c". + _paths_no_keep_path = ( + "Contents/MacOS", + [ + "crashreporter.app/Contents/MacOS/crashreporter", + "{product}", + "{product}-bin", + "*.dylib", + "minidump-analyzer", + "pingsender", + "plugin-container.app/Contents/MacOS/plugin-container", + "updater.app/Contents/MacOS/org.mozilla.updater", + # 'xpcshell', + "XUL", + ], + ) + + @property + def paths_no_keep_path(self): + root, paths = self._paths_no_keep_path + return (root, [p.format(product=self.product) for p in paths]) + + @contextmanager + def get_writer(self, **kwargs): + with ResignJarWriter(self, **kwargs) as writer: + yield writer + + def process_package_artifact(self, filename, processed_filename): + tempdir = tempfile.mkdtemp() + oldcwd = os.getcwd() + try: + self.log( + logging.DEBUG, + "artifact", + {"tempdir": tempdir}, + "Unpacking DMG into {tempdir}", + ) + if self._substs["HOST_OS_ARCH"] == "Linux": + # This is a cross build, use hfsplus and dmg tools to extract the dmg. + os.chdir(tempdir) + with open(os.devnull, "wb") as devnull: + subprocess.check_call( + [ + self._substs["DMG_TOOL"], + "extract", + filename, + "extracted_img", + ], + stdout=devnull, + ) + subprocess.check_call( + [self._substs["HFS_TOOL"], "extracted_img", "extractall"], + stdout=devnull, + ) + else: + mozinstall.install(filename, tempdir) + + bundle_dirs = glob.glob(mozpath.join(tempdir, "*.app")) + if len(bundle_dirs) != 1: + raise ValueError( + "Expected one source bundle, found: {}".format(bundle_dirs) + ) + [source] = bundle_dirs + + # These get copied into dist/bin with the path, so "root/a/b/c" -> "dist/bin/a/b/c". + paths_keep_path = [ + ( + "Contents/Resources", + [ + "browser/components/libbrowsercomps.dylib", + "dependentlibs.list", + # 'firefox', + "gmp-clearkey/0.1/libclearkey.dylib", + # 'gmp-fake/1.0/libfake.dylib', + # 'gmp-fakeopenh264/1.0/libfakeopenh264.dylib', + "platform.ini", + ], + ) + ] + + with self.get_writer(file=processed_filename, compress_level=5) as writer: + root, paths = self.paths_no_keep_path + finder = UnpackFinder(mozpath.join(source, root)) + for path in paths: + for p, f in finder.find(path): + self.log( + logging.DEBUG, + "artifact", + {"path": p}, + "Adding {path} to processed archive", + ) + destpath = mozpath.join("bin", os.path.basename(p)) + writer.add(destpath.encode("utf-8"), f.open(), mode=f.mode) + + for root, paths in paths_keep_path: + finder = UnpackFinder(mozpath.join(source, root)) + for path in paths: + for p, f in finder.find(path): + self.log( + logging.DEBUG, + "artifact", + {"path": p}, + "Adding {path} to processed archive", + ) + destpath = mozpath.join("bin", p) + writer.add(destpath.encode("utf-8"), f.open(), mode=f.mode) + + finally: + os.chdir(oldcwd) + try: + shutil.rmtree(tempdir) + except (OSError, IOError): + self.log( + logging.WARN, + "artifact", + {"tempdir": tempdir}, + "Unable to delete {tempdir}", + ) + pass + + +class WinArtifactJob(ArtifactJob): + package_re = r"public/build/target\.(zip|tar\.gz)$" + product = "firefox" + + _package_artifact_patterns = { + "{product}/dependentlibs.list", + "{product}/platform.ini", + "{product}/**/*.dll", + "{product}/*.exe", + "{product}/*.tlb", + } + + @property + def package_artifact_patterns(self): + return {p.format(product=self.product) for p in self._package_artifact_patterns} + + # These are a subset of TEST_HARNESS_BINS in testing/mochitest/Makefile.in. + test_artifact_patterns = { + ("bin/BadCertAndPinningServer.exe", ("bin", "bin")), + ("bin/DelegatedCredentialsServer.exe", ("bin", "bin")), + ("bin/EncryptedClientHelloServer.exe", ("bin", "bin")), + ("bin/FaultyServer.exe", ("bin", "bin")), + ("bin/GenerateOCSPResponse.exe", ("bin", "bin")), + ("bin/OCSPStaplingServer.exe", ("bin", "bin")), + ("bin/SanctionsTestServer.exe", ("bin", "bin")), + ("bin/certutil.exe", ("bin", "bin")), + ("bin/geckodriver.exe", ("bin", "bin")), + ("bin/minidumpwriter.exe", ("bin", "bin")), + ("bin/pk12util.exe", ("bin", "bin")), + ("bin/screenshot.exe", ("bin", "bin")), + ("bin/ssltunnel.exe", ("bin", "bin")), + ("bin/xpcshell.exe", ("bin", "bin")), + ("bin/http3server.exe", ("bin", "bin")), + ("bin/content_analysis_sdk_agent.exe", ("bin", "bin")), + ("bin/plugins/gmp-*/*/*", ("bin/plugins", "bin")), + ("bin/plugins/*", ("bin/plugins", "plugins")), + ("bin/components/*", ("bin/components", "bin/components")), + } + + def process_package_artifact(self, filename, processed_filename): + added_entry = False + with self.get_writer(file=processed_filename, compress_level=5) as writer: + for p, f in UnpackFinder(JarFinder(filename, JarReader(filename))): + if not any( + mozpath.match(p, pat) for pat in self.package_artifact_patterns + ): + continue + + # strip off the relative "firefox/" bit from the path: + basename = mozpath.relpath(p, self.product) + basename = mozpath.join("bin", basename) + self.log( + logging.DEBUG, + "artifact", + {"basename": basename}, + "Adding {basename} to processed archive", + ) + writer.add(basename.encode("utf-8"), f.open(), mode=f.mode) + added_entry = True + + if not added_entry: + raise ValueError( + 'Archive format changed! No pattern from "{patterns}"' + "matched an archive path.".format(patterns=self.artifact_patterns) + ) + + +class ThunderbirdMixin(object): + trust_domain = "comm" + product = "thunderbird" + try_tree = "try-comm-central" + default_candidate_trees = [ + "releases/comm-release", + ] + nightly_candidate_trees = [ + "comm-central", + ] + beta_candidate_trees = [ + "releases/comm-beta", + ] + # The list below list should be updated when we have new ESRs. + esr_candidate_trees = [ + "releases/comm-esr115", + ] + + +class LinuxThunderbirdArtifactJob(ThunderbirdMixin, LinuxArtifactJob): + pass + + +class MacThunderbirdArtifactJob(ThunderbirdMixin, MacArtifactJob): + pass + + +class WinThunderbirdArtifactJob(ThunderbirdMixin, WinArtifactJob): + pass + + +def startswithwhich(s, prefixes): + for prefix in prefixes: + if s.startswith(prefix): + return prefix + + +MOZ_JOB_DETAILS = { + j: { + "android": AndroidArtifactJob, + "linux": LinuxArtifactJob, + "macosx": MacArtifactJob, + "win": WinArtifactJob, + }[startswithwhich(j, ("android", "linux", "macosx", "win"))] + for j in JOB_CHOICES +} +COMM_JOB_DETAILS = { + j: { + "android": None, + "linux": LinuxThunderbirdArtifactJob, + "macosx": MacThunderbirdArtifactJob, + "win": WinThunderbirdArtifactJob, + }[startswithwhich(j, ("android", "linux", "macosx", "win"))] + for j in JOB_CHOICES +} + + +def cachedmethod(cachefunc): + """Decorator to wrap a class or instance method with a memoizing callable that + saves results in a (possibly shared) cache. + """ + + def decorator(method): + def wrapper(self, *args, **kwargs): + mapping = cachefunc(self) + if mapping is None: + return method(self, *args, **kwargs) + key = (method.__name__, args, tuple(sorted(kwargs.items()))) + try: + value = mapping[key] + return value + except KeyError: + pass + result = method(self, *args, **kwargs) + mapping[key] = result + return result + + return functools.update_wrapper(wrapper, method) + + return decorator + + +class CacheManager(object): + """Maintain an LRU cache. Provide simple persistence, including support for + loading and saving the state using a "with" block. Allow clearing the cache + and printing the cache for debugging. + + Provide simple logging. + """ + + def __init__( + self, + cache_dir, + cache_name, + cache_size, + cache_callback=None, + log=None, + skip_cache=False, + ): + self._skip_cache = skip_cache + self._cache = pylru.lrucache(cache_size, callback=cache_callback) + self._cache_filename = mozpath.join(cache_dir, cache_name + "-cache.pickle") + self._log = log + mkdir(cache_dir, not_indexed=True) + + def log(self, *args, **kwargs): + if self._log: + self._log(*args, **kwargs) + + def load_cache(self): + if self._skip_cache: + self.log( + logging.INFO, "artifact", {}, "Skipping cache: ignoring load_cache!" + ) + return + + try: + items = pickle.load(open(self._cache_filename, "rb")) + for key, value in items: + self._cache[key] = value + except Exception as e: + # Corrupt cache, perhaps? Sadly, pickle raises many different + # exceptions, so it's not worth trying to be fine grained here. + # We ignore any exception, so the cache is effectively dropped. + self.log( + logging.INFO, + "artifact", + {"filename": self._cache_filename, "exception": repr(e)}, + "Ignoring exception unpickling cache file {filename}: {exception}", + ) + pass + + def dump_cache(self): + if self._skip_cache: + self.log( + logging.INFO, "artifact", {}, "Skipping cache: ignoring dump_cache!" + ) + return + + ensureParentDir(self._cache_filename) + pickle.dump( + list(reversed(list(self._cache.items()))), + open(self._cache_filename, "wb"), + -1, + ) + + def clear_cache(self): + if self._skip_cache: + self.log( + logging.INFO, "artifact", {}, "Skipping cache: ignoring clear_cache!" + ) + return + + with self: + self._cache.clear() + + def __enter__(self): + self.load_cache() + return self + + def __exit__(self, type, value, traceback): + self.dump_cache() + + +class PushheadCache(CacheManager): + """Helps map tree/revision pairs to parent pushheads according to the pushlog.""" + + def __init__(self, cache_dir, log=None, skip_cache=False): + CacheManager.__init__( + self, + cache_dir, + "pushhead_cache", + MAX_CACHED_TASKS, + log=log, + skip_cache=skip_cache, + ) + + @cachedmethod(operator.attrgetter("_cache")) + def parent_pushhead_id(self, tree, revision): + cset_url_tmpl = ( + "https://hg.mozilla.org/{tree}/json-pushes?" + "changeset={changeset}&version=2&tipsonly=1" + ) + req = requests.get( + cset_url_tmpl.format(tree=tree, changeset=revision), + headers={"Accept": "application/json"}, + ) + if req.status_code not in range(200, 300): + raise ValueError + result = req.json() + [found_pushid] = result["pushes"].keys() + return int(found_pushid) + + @cachedmethod(operator.attrgetter("_cache")) + def pushid_range(self, tree, start, end): + pushid_url_tmpl = ( + "https://hg.mozilla.org/{tree}/json-pushes?" + "startID={start}&endID={end}&version=2&tipsonly=1" + ) + + req = requests.get( + pushid_url_tmpl.format(tree=tree, start=start, end=end), + headers={"Accept": "application/json"}, + ) + result = req.json() + return [p["changesets"][-1] for p in result["pushes"].values()] + + +class TaskCache(CacheManager): + """Map candidate pushheads to Task Cluster task IDs and artifact URLs.""" + + def __init__(self, cache_dir, log=None, skip_cache=False): + CacheManager.__init__( + self, + cache_dir, + "artifact_url", + MAX_CACHED_TASKS, + log=log, + skip_cache=skip_cache, + ) + + @cachedmethod(operator.attrgetter("_cache")) + def artifacts(self, tree, job, artifact_job_class, rev): + # Grab the second part of the repo name, which is generally how things + # are indexed. Eg: 'integration/autoland' is indexed as + # 'autoland' + tree = tree.split("/")[1] if "/" in tree else tree + + if job.endswith("-opt"): + tree += ".shippable" + + namespace = "{trust_domain}.v2.{tree}.revision.{rev}.{product}.{job}".format( + trust_domain=artifact_job_class.trust_domain, + rev=rev, + tree=tree, + product=artifact_job_class.product, + job=job, + ) + self.log( + logging.DEBUG, + "artifact", + {"namespace": namespace}, + "Searching Taskcluster index with namespace: {namespace}", + ) + try: + taskId = find_task_id(namespace) + except KeyError: + # Not all revisions correspond to pushes that produce the job we + # care about; and even those that do may not have completed yet. + raise ValueError( + "Task for {namespace} does not exist (yet)!".format(namespace=namespace) + ) + + return taskId, list_artifacts(taskId) + + +class Artifacts(object): + """Maintain state to efficiently fetch build artifacts from a Firefox tree.""" + + def __init__( + self, + tree, + substs, + defines, + job=None, + log=None, + cache_dir=".", + hg=None, + git=None, + skip_cache=False, + topsrcdir=None, + download_tests=True, + download_symbols=False, + download_maven_zip=False, + no_process=False, + mozbuild=None, + ): + if (hg and git) or (not hg and not git): + raise ValueError("Must provide path to exactly one of hg and git") + + self._substs = substs + self._defines = defines + self._tree = tree + self._job = job or self._guess_artifact_job() + self._log = log + self._hg = hg + self._git = git + self._cache_dir = cache_dir + self._skip_cache = skip_cache + self._topsrcdir = topsrcdir + self._no_process = no_process + + app = self._substs.get("MOZ_BUILD_APP") + job_details = COMM_JOB_DETAILS if app == "comm/mail" else MOZ_JOB_DETAILS + + try: + cls = job_details[self._job] + self._artifact_job = cls( + log=self._log, + download_tests=download_tests, + download_symbols=download_symbols, + download_maven_zip=download_maven_zip, + substs=self._substs, + mozbuild=mozbuild, + ) + except KeyError: + self.log(logging.INFO, "artifact", {"job": self._job}, "Unknown job {job}") + raise KeyError("Unknown job") + + self._task_cache = TaskCache( + self._cache_dir, log=self._log, skip_cache=self._skip_cache + ) + self._artifact_cache = ArtifactCache( + self._cache_dir, log=self._log, skip_cache=self._skip_cache + ) + self._pushhead_cache = PushheadCache( + self._cache_dir, log=self._log, skip_cache=self._skip_cache + ) + + def log(self, *args, **kwargs): + if self._log: + self._log(*args, **kwargs) + + def run_hg(self, *args, **kwargs): + env = kwargs.get("env", {}) + env["HGPLAIN"] = "1" + kwargs["universal_newlines"] = True + return subprocess.check_output([self._hg] + list(args), **kwargs) + + def _guess_artifact_job(self): + # Add the "-debug" suffix to the guessed artifact job name + # if MOZ_DEBUG is enabled. + if self._substs.get("MOZ_DEBUG"): + target_suffix = "-debug" + else: + target_suffix = "-opt" + + if self._substs.get("MOZ_BUILD_APP", "") == "mobile/android": + if self._substs["ANDROID_CPU_ARCH"] == "x86_64": + return "android-x86_64" + target_suffix + if self._substs["ANDROID_CPU_ARCH"] == "x86": + return "android-x86" + target_suffix + if self._substs["ANDROID_CPU_ARCH"] == "arm64-v8a": + return "android-aarch64" + target_suffix + return "android-arm" + target_suffix + + target_64bit = False + if self._substs["TARGET_CPU"] == "x86_64": + target_64bit = True + + if self._defines.get("XP_LINUX", False): + return ("linux64" if target_64bit else "linux") + target_suffix + if self._defines.get("XP_WIN", False): + if self._substs["TARGET_CPU"] == "aarch64": + return "win64-aarch64" + target_suffix + return ("win64" if target_64bit else "win32") + target_suffix + if self._defines.get("XP_MACOSX", False): + # We only produce unified builds in automation, so the target_cpu + # check is not relevant. + return "macosx64" + target_suffix + raise Exception("Cannot determine default job for |mach artifact|!") + + def _pushheads_from_rev(self, rev, count): + """Queries hg.mozilla.org's json-pushlog for pushheads that are nearby + ancestors or `rev`. Multiple trees are queried, as the `rev` may + already have been pushed to multiple repositories. For each repository + containing `rev`, the pushhead introducing `rev` and the previous + `count` pushheads from that point are included in the output. + """ + + with self._pushhead_cache as pushhead_cache: + found_pushids = {} + + search_trees = self._artifact_job.candidate_trees + for tree in search_trees: + self.log( + logging.DEBUG, + "artifact", + {"tree": tree, "rev": rev}, + "Attempting to find a pushhead containing {rev} on {tree}.", + ) + try: + pushid = pushhead_cache.parent_pushhead_id(tree, rev) + found_pushids[tree] = pushid + except ValueError: + continue + + candidate_pushheads = collections.defaultdict(list) + + for tree, pushid in six.iteritems(found_pushids): + end = pushid + start = pushid - NUM_PUSHHEADS_TO_QUERY_PER_PARENT + + self.log( + logging.DEBUG, + "artifact", + { + "tree": tree, + "pushid": pushid, + "num": NUM_PUSHHEADS_TO_QUERY_PER_PARENT, + }, + "Retrieving the last {num} pushheads starting with id {pushid} on {tree}", + ) + for pushhead in pushhead_cache.pushid_range(tree, start, end): + candidate_pushheads[pushhead].append(tree) + + return candidate_pushheads + + def _get_hg_revisions_from_git(self): + rev_list = subprocess.check_output( + [ + self._git, + "rev-list", + "--topo-order", + "--max-count={num}".format(num=NUM_REVISIONS_TO_QUERY), + "HEAD", + ], + universal_newlines=True, + cwd=self._topsrcdir, + ) + + hg_hash_list = subprocess.check_output( + [self._git, "cinnabar", "git2hg"] + rev_list.splitlines(), + universal_newlines=True, + cwd=self._topsrcdir, + ) + + zeroes = "0" * 40 + + hashes = [] + for hg_hash in hg_hash_list.splitlines(): + hg_hash = hg_hash.strip() + if not hg_hash or hg_hash == zeroes: + continue + hashes.append(hg_hash) + if not hashes: + msg = ( + "Could not list any recent revisions in your clone. Does " + "your clone have git-cinnabar metadata? If not, consider " + "re-cloning using the directions at " + "https://github.com/glandium/git-cinnabar/wiki/Mozilla:-A-" + "git-workflow-for-Gecko-development" + ) + try: + subprocess.check_output( + [ + self._git, + "cat-file", + "-e", + "05e5d33a570d48aed58b2d38f5dfc0a7870ff8d3^{commit}", + ], + stderr=subprocess.STDOUT, + ) + # If the above commit exists, we're probably in a clone of + # `gecko-dev`, and this documentation applies. + msg += ( + "\n\nNOTE: Consider following the directions " + "at https://github.com/glandium/git-cinnabar/wiki/" + "Mozilla:-Using-a-git-clone-of-gecko%E2%80%90dev-" + "to-push-to-mercurial to resolve this issue." + ) + except subprocess.CalledProcessError: + pass + raise UserError(msg) + return hashes + + def _get_recent_public_revisions(self): + """Returns recent ancestors of the working parent that are likely to + to be known to Mozilla automation. + + If we're using git, retrieves hg revisions from git-cinnabar. + """ + if self._git: + return self._get_hg_revisions_from_git() + + # Mercurial updated the ordering of "last" in 4.3. We use revision + # numbers to order here to accommodate multiple versions of hg. + last_revs = self.run_hg( + "log", + "--template", + "{rev}:{node}\n", + "-r", + "last(public() and ::., {num})".format(num=NUM_REVISIONS_TO_QUERY), + cwd=self._topsrcdir, + ).splitlines() + + if len(last_revs) == 0: + raise UserError( + """\ +There are no public revisions. +This can happen if the repository is created from bundle file and never pulled +from remote. Please run `hg pull` and build again. +https://firefox-source-docs.mozilla.org/contributing/vcs/mercurial_bundles.html +""" + ) + + self.log( + logging.DEBUG, + "artifact", + {"len": len(last_revs)}, + "hg suggested {len} candidate revisions", + ) + + def to_pair(line): + rev, node = line.split(":", 1) + return (int(rev), node) + + pairs = [to_pair(r) for r in last_revs] + + # Python's tuple sort orders by first component: here, the (local) + # revision number. + nodes = [pair[1] for pair in sorted(pairs, reverse=True)] + + for node in nodes[:20]: + self.log( + logging.DEBUG, + "artifact", + {"node": node}, + "hg suggested candidate revision: {node}", + ) + self.log( + logging.DEBUG, + "artifact", + {"remaining": max(0, len(nodes) - 20)}, + "hg suggested candidate revision: and {remaining} more", + ) + + return nodes + + def _find_pushheads(self): + """Returns an iterator of recent pushhead revisions, starting with the + working parent. + """ + + last_revs = self._get_recent_public_revisions() + candidate_pushheads = [] + for rev in last_revs: + candidate_pushheads = self._pushheads_from_rev( + rev.rstrip(), NUM_PUSHHEADS_TO_QUERY_PER_PARENT + ) + if candidate_pushheads: + break + count = 0 + for rev in last_revs: + rev = rev.rstrip() + if not rev: + continue + if rev not in candidate_pushheads: + continue + count += 1 + yield candidate_pushheads[rev], rev + + if not count: + raise Exception( + "Could not find any candidate pushheads in the last {num} revisions.\n" + "Search started with {rev}, which must be known to Mozilla automation.\n\n" + "see https://firefox-source-docs.mozilla.org/contributing/build/artifact_builds.html".format( # noqa E501 + rev=last_revs[0], num=NUM_PUSHHEADS_TO_QUERY_PER_PARENT + ) + ) + + def find_pushhead_artifacts(self, task_cache, job, tree, pushhead): + try: + taskId, artifacts = task_cache.artifacts( + tree, job, self._artifact_job.__class__, pushhead + ) + except ValueError: + return None + + urls = [] + for artifact_name in self._artifact_job.find_candidate_artifacts(artifacts): + url = get_artifact_url(taskId, artifact_name) + urls.append(url) + if urls: + self.log( + logging.DEBUG, + "artifact", + {"pushhead": pushhead, "tree": tree}, + "Installing from remote pushhead {pushhead} on {tree}", + ) + return urls + return None + + def install_from_file(self, filename, distdir): + self.log( + logging.DEBUG, + "artifact", + {"filename": filename}, + "Installing from {filename}", + ) + + # Copy all .so files, avoiding modification where possible. + ensureParentDir(mozpath.join(distdir, ".dummy")) + + if self._no_process: + orig_basename = os.path.basename(filename) + # Turn 'HASH-target...' into 'target...' if possible. It might not + # be possible if the file is given directly on the command line. + before, _sep, after = orig_basename.rpartition("-") + if re.match(r"[0-9a-fA-F]{16}$", before): + orig_basename = after + path = mozpath.join(distdir, orig_basename) + with FileAvoidWrite(path, readmode="rb") as fh: + shutil.copyfileobj(open(filename, mode="rb"), fh) + self.log( + logging.DEBUG, + "artifact", + {"path": path}, + "Copied unprocessed artifact: to {path}", + ) + return + + # Do we need to post-process? + processed_filename = filename + PROCESSED_SUFFIX + + if self._skip_cache and os.path.exists(processed_filename): + self.log( + logging.INFO, + "artifact", + {"path": processed_filename}, + "Skipping cache: removing cached processed artifact {path}", + ) + os.remove(processed_filename) + + if not os.path.exists(processed_filename): + self.log( + logging.DEBUG, + "artifact", + {"filename": filename}, + "Processing contents of {filename}", + ) + self.log( + logging.DEBUG, + "artifact", + {"processed_filename": processed_filename}, + "Writing processed {processed_filename}", + ) + try: + self._artifact_job.process_artifact(filename, processed_filename) + except Exception as e: + # Delete the partial output of failed processing. + try: + os.remove(processed_filename) + except FileNotFoundError: + pass + raise e + + self._artifact_cache._persist_limit.register_file(processed_filename) + + self.log( + logging.DEBUG, + "artifact", + {"processed_filename": processed_filename}, + "Installing from processed {processed_filename}", + ) + + with zipfile.ZipFile(processed_filename) as zf: + for info in zf.infolist(): + n = mozpath.join(distdir, info.filename) + fh = FileAvoidWrite(n, readmode="rb") + shutil.copyfileobj(zf.open(info), fh) + file_existed, file_updated = fh.close() + self.log( + logging.DEBUG, + "artifact", + { + "updating": "Updating" if file_updated else "Not updating", + "filename": n, + }, + "{updating} {filename}", + ) + if not file_existed or file_updated: + # Libraries and binaries may need to be marked executable, + # depending on platform. + perms = ( + info.external_attr >> 16 + ) # See http://stackoverflow.com/a/434689. + perms |= ( + stat.S_IWUSR | stat.S_IRUSR | stat.S_IRGRP | stat.S_IROTH + ) # u+w, a+r. + os.chmod(n, perms) + return 0 + + def install_from_url(self, url, distdir): + self.log(logging.DEBUG, "artifact", {"url": url}, "Installing from {url}") + filename = self._artifact_cache.fetch(url) + return self.install_from_file(filename, distdir) + + def _install_from_hg_pushheads(self, hg_pushheads, distdir): + """Iterate pairs (hg_hash, {tree-set}) associating hg revision hashes + and tree-sets they are known to be in, trying to download and + install from each. + """ + + urls = None + count = 0 + # with blocks handle handle persistence. + with self._task_cache as task_cache: + for trees, hg_hash in hg_pushheads: + for tree in trees: + count += 1 + self.log( + logging.DEBUG, + "artifact", + {"hg_hash": hg_hash, "tree": tree}, + "Trying to find artifacts for hg revision {hg_hash} on tree {tree}.", + ) + urls = self.find_pushhead_artifacts( + task_cache, self._job, tree, hg_hash + ) + if urls: + for url in urls: + if self.install_from_url(url, distdir): + return 1 + return 0 + + self.log( + logging.ERROR, + "artifact", + {"count": count}, + "Tried {count} pushheads, no built artifacts found.", + ) + return 1 + + def install_from_recent(self, distdir): + hg_pushheads = self._find_pushheads() + return self._install_from_hg_pushheads(hg_pushheads, distdir) + + def install_from_revset(self, revset, distdir): + revision = None + try: + if self._hg: + revision = self.run_hg( + "log", "--template", "{node}\n", "-r", revset, cwd=self._topsrcdir + ).strip() + elif self._git: + revset = subprocess.check_output( + [self._git, "rev-parse", "%s^{commit}" % revset], + stderr=open(os.devnull, "w"), + universal_newlines=True, + cwd=self._topsrcdir, + ).strip() + else: + # Fallback to the exception handling case from both hg and git + raise subprocess.CalledProcessError() + except subprocess.CalledProcessError: + # If the mercurial of git commands above failed, it means the given + # revset is not known locally to the VCS. But if the revset looks + # like a complete sha1, assume it is a mercurial sha1 that hasn't + # been pulled, and use that. + if re.match(r"^[A-Fa-f0-9]{40}$", revset): + revision = revset + + if revision is None and self._git: + revision = subprocess.check_output( + [self._git, "cinnabar", "git2hg", revset], + universal_newlines=True, + cwd=self._topsrcdir, + ).strip() + + if revision == "0" * 40 or revision is None: + raise ValueError( + "revision specification must resolve to a commit known to hg" + ) + if len(revision.split("\n")) != 1: + raise ValueError( + "revision specification must resolve to exactly one commit" + ) + + self.log( + logging.INFO, + "artifact", + {"revset": revset, "revision": revision}, + "Will only accept artifacts from a pushhead at {revision} " + '(matched revset "{revset}").', + ) + # Include try in our search to allow pulling from a specific push. + pushheads = [ + ( + self._artifact_job.candidate_trees + [self._artifact_job.try_tree], + revision, + ) + ] + return self._install_from_hg_pushheads(pushheads, distdir) + + def install_from_task(self, taskId, distdir): + artifacts = list_artifacts(taskId) + + urls = [] + for artifact_name in self._artifact_job.find_candidate_artifacts(artifacts): + url = get_artifact_url(taskId, artifact_name) + urls.append(url) + if not urls: + raise ValueError( + "Task {taskId} existed, but no artifacts found!".format(taskId=taskId) + ) + for url in urls: + if self.install_from_url(url, distdir): + return 1 + return 0 + + def install_from(self, source, distdir): + """Install artifacts from a ``source`` into the given ``distdir``.""" + if (source and os.path.isfile(source)) or "MOZ_ARTIFACT_FILE" in os.environ: + source = source or os.environ["MOZ_ARTIFACT_FILE"] + for source in source.split(os.pathsep): + ret = self.install_from_file(source, distdir) + if ret: + return ret + return 0 + + if (source and urlparse(source).scheme) or "MOZ_ARTIFACT_URL" in os.environ: + source = source or os.environ["MOZ_ARTIFACT_URL"] + for source in source.split(): + ret = self.install_from_url(source, distdir) + if ret: + return ret + return 0 + + if source or "MOZ_ARTIFACT_REVISION" in os.environ: + source = source or os.environ["MOZ_ARTIFACT_REVISION"] + return self.install_from_revset(source, distdir) + + for var in ( + "MOZ_ARTIFACT_TASK_%s" % self._job.upper().replace("-", "_"), + "MOZ_ARTIFACT_TASK", + ): + if var in os.environ: + return self.install_from_task(os.environ[var], distdir) + + return self.install_from_recent(distdir) + + def clear_cache(self): + self.log(logging.INFO, "artifact", {}, "Deleting cached artifacts and caches.") + self._task_cache.clear_cache() + self._artifact_cache.clear_cache() + self._pushhead_cache.clear_cache() diff --git a/python/mozbuild/mozbuild/backend/__init__.py b/python/mozbuild/mozbuild/backend/__init__.py new file mode 100644 index 0000000000..e7097eb614 --- /dev/null +++ b/python/mozbuild/mozbuild/backend/__init__.py @@ -0,0 +1,27 @@ +# 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/. + +backends = { + "Clangd": "mozbuild.backend.clangd", + "ChromeMap": "mozbuild.codecoverage.chrome_map", + "CompileDB": "mozbuild.compilation.database", + "CppEclipse": "mozbuild.backend.cpp_eclipse", + "FasterMake": "mozbuild.backend.fastermake", + "FasterMake+RecursiveMake": None, + "RecursiveMake": "mozbuild.backend.recursivemake", + "StaticAnalysis": "mozbuild.backend.static_analysis", + "TestManifest": "mozbuild.backend.test_manifest", + "VisualStudio": "mozbuild.backend.visualstudio", +} + + +def get_backend_class(name): + if "+" in name: + from mozbuild.backend.base import HybridBackend + + return HybridBackend(*(get_backend_class(name) for name in name.split("+"))) + + class_name = "%sBackend" % name + module = __import__(backends[name], globals(), locals(), [class_name]) + return getattr(module, class_name) diff --git a/python/mozbuild/mozbuild/backend/base.py b/python/mozbuild/mozbuild/backend/base.py new file mode 100644 index 0000000000..0f95942f51 --- /dev/null +++ b/python/mozbuild/mozbuild/backend/base.py @@ -0,0 +1,389 @@ +# 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/. + +import errno +import io +import itertools +import os +import time +from abc import ABCMeta, abstractmethod +from contextlib import contextmanager + +import mozpack.path as mozpath +import six +from mach.mixin.logging import LoggingMixin + +from mozbuild.base import ExecutionSummary + +from ..frontend.data import ContextDerived +from ..frontend.reader import EmptyConfig +from ..preprocessor import Preprocessor +from ..pythonutil import iter_modules_in_path +from ..util import FileAvoidWrite, simple_diff +from .configenvironment import ConfigEnvironment + + +class BuildBackend(LoggingMixin): + """Abstract base class for build backends. + + A build backend is merely a consumer of the build configuration (the output + of the frontend processing). It does something with said data. What exactly + is the discretion of the specific implementation. + """ + + __metaclass__ = ABCMeta + + def __init__(self, environment): + assert isinstance(environment, (ConfigEnvironment, EmptyConfig)) + self.populate_logger() + + self.environment = environment + + # Files whose modification should cause a new read and backend + # generation. + self.backend_input_files = set() + + # Files generated by the backend. + self._backend_output_files = set() + + self._environments = {} + self._environments[environment.topobjdir] = environment + + # The number of backend files created. + self._created_count = 0 + + # The number of backend files updated. + self._updated_count = 0 + + # The number of unchanged backend files. + self._unchanged_count = 0 + + # The number of deleted backend files. + self._deleted_count = 0 + + # The total wall time spent in the backend. This counts the time the + # backend writes out files, etc. + self._execution_time = 0.0 + + # Mapping of changed file paths to diffs of the changes. + self.file_diffs = {} + + self.dry_run = False + + self._init() + + def summary(self): + return ExecutionSummary( + self.__class__.__name__.replace("Backend", "") + + " backend executed in {execution_time:.2f}s\n " + "{total:d} total backend files; " + "{created:d} created; " + "{updated:d} updated; " + "{unchanged:d} unchanged; " + "{deleted:d} deleted", + execution_time=self._execution_time, + total=self._created_count + self._updated_count + self._unchanged_count, + created=self._created_count, + updated=self._updated_count, + unchanged=self._unchanged_count, + deleted=self._deleted_count, + ) + + def _init(self): + """Hook point for child classes to perform actions during __init__. + + This exists so child classes don't need to implement __init__. + """ + + def consume(self, objs): + """Consume a stream of TreeMetadata instances. + + This is the main method of the interface. This is what takes the + frontend output and does something with it. + + Child classes are not expected to implement this method. Instead, the + base class consumes objects and calls methods (possibly) implemented by + child classes. + """ + + # Previously generated files. + list_file = mozpath.join( + self.environment.topobjdir, "backend.%s" % self.__class__.__name__ + ) + backend_output_list = set() + if os.path.exists(list_file): + with open(list_file) as fh: + backend_output_list.update( + mozpath.normsep(p) for p in fh.read().splitlines() + ) + + for obj in objs: + obj_start = time.monotonic() + if not self.consume_object(obj) and not isinstance(self, PartialBackend): + raise Exception("Unhandled object of type %s" % type(obj)) + self._execution_time += time.monotonic() - obj_start + + if isinstance(obj, ContextDerived) and not isinstance(self, PartialBackend): + self.backend_input_files |= obj.context_all_paths + + # Pull in all loaded Python as dependencies so any Python changes that + # could influence our output result in a rescan. + self.backend_input_files |= set( + iter_modules_in_path(self.environment.topsrcdir, self.environment.topobjdir) + ) + + finished_start = time.monotonic() + self.consume_finished() + self._execution_time += time.monotonic() - finished_start + + # Purge backend files created in previous run, but not created anymore + delete_files = backend_output_list - self._backend_output_files + for path in delete_files: + full_path = mozpath.join(self.environment.topobjdir, path) + try: + with io.open(full_path, mode="r", encoding="utf-8") as existing: + old_content = existing.read() + if old_content: + self.file_diffs[full_path] = simple_diff( + full_path, old_content.splitlines(), None + ) + except IOError: + pass + try: + if not self.dry_run: + os.unlink(full_path) + self._deleted_count += 1 + except OSError: + pass + # Remove now empty directories + for dir in set(mozpath.dirname(d) for d in delete_files): + try: + os.removedirs(dir) + except OSError: + pass + + # Write out the list of backend files generated, if it changed. + if backend_output_list != self._backend_output_files: + with self._write_file(list_file) as fh: + fh.write("\n".join(sorted(self._backend_output_files))) + else: + # Always update its mtime if we're not in dry-run mode. + if not self.dry_run: + with open(list_file, "a"): + os.utime(list_file, None) + + # Write out the list of input files for the backend + with self._write_file("%s.in" % list_file) as fh: + fh.write( + "\n".join(sorted(mozpath.normsep(f) for f in self.backend_input_files)) + ) + + @abstractmethod + def consume_object(self, obj): + """Consumes an individual TreeMetadata instance. + + This is the main method used by child classes to react to build + metadata. + """ + + def consume_finished(self): + """Called when consume() has completed handling all objects.""" + + def build(self, config, output, jobs, verbose, what=None): + """Called when 'mach build' is executed. + + This should return the status value of a subprocess, where 0 denotes + success and any other value is an error code. A return value of None + indicates that the default 'make -f client.mk' should run. + """ + return None + + def _write_purgecaches(self, config): + """Write .purgecaches sentinels. + + The purgecaches mechanism exists to allow the platform to + invalidate the XUL cache (which includes some JS) at application + startup-time. The application checks for .purgecaches in the + application directory, which varies according to + --enable-application/--enable-project. There's a further wrinkle on + macOS, where the real application directory is part of a Cocoa bundle + produced from the regular application directory by the build + system. In this case, we write to both locations, since the + build system recreates the Cocoa bundle from the contents of the + regular application directory and might remove a sentinel + created here. + """ + + app = config.substs["MOZ_BUILD_APP"] + if app == "mobile/android": + # In order to take effect, .purgecaches sentinels would need to be + # written to the Android device file system. + return + + root = mozpath.join(config.topobjdir, "dist", "bin") + + if app == "browser": + root = mozpath.join(config.topobjdir, "dist", "bin", "browser") + + purgecaches_dirs = [root] + if app == "browser" and "cocoa" == config.substs["MOZ_WIDGET_TOOLKIT"]: + bundledir = mozpath.join( + config.topobjdir, + "dist", + config.substs["MOZ_MACBUNDLE_NAME"], + "Contents", + "Resources", + "browser", + ) + purgecaches_dirs.append(bundledir) + + for dir in purgecaches_dirs: + with open(mozpath.join(dir, ".purgecaches"), "wt") as f: + f.write("\n") + + def post_build(self, config, output, jobs, verbose, status): + """Called late during 'mach build' execution, after `build(...)` has finished. + + `status` is the status value returned from `build(...)`. + + In the case where `build` returns `None`, this is called after + the default `make` command has completed, with the status of + that command. + + This should return the status value from `build(...)`, or the + status value of a subprocess, where 0 denotes success and any + other value is an error code. + + If an exception is raised, ``mach build`` will fail with a + non-zero exit code. + """ + self._write_purgecaches(config) + + return status + + @contextmanager + def _write_file(self, path=None, fh=None, readmode="r"): + """Context manager to write a file. + + This is a glorified wrapper around FileAvoidWrite with integration to + update the summary data on this instance. + + Example usage: + + with self._write_file('foo.txt') as fh: + fh.write('hello world') + """ + + if path is not None: + assert fh is None + fh = FileAvoidWrite( + path, capture_diff=True, dry_run=self.dry_run, readmode=readmode + ) + else: + assert fh is not None + + dirname = mozpath.dirname(fh.name) + try: + os.makedirs(dirname) + except OSError as error: + if error.errno != errno.EEXIST: + raise + + yield fh + + self._backend_output_files.add( + mozpath.relpath(fh.name, self.environment.topobjdir) + ) + existed, updated = fh.close() + if fh.diff: + self.file_diffs[fh.name] = fh.diff + if not existed: + self._created_count += 1 + elif updated: + self._updated_count += 1 + else: + self._unchanged_count += 1 + + @contextmanager + def _get_preprocessor(self, obj): + """Returns a preprocessor with a few predefined values depending on + the given BaseConfigSubstitution(-like) object, and all the substs + in the current environment.""" + pp = Preprocessor() + srcdir = mozpath.dirname(obj.input_path) + pp.context.update( + { + k: " ".join(v) if isinstance(v, list) else v + for k, v in six.iteritems(obj.config.substs) + } + ) + pp.context.update( + top_srcdir=obj.topsrcdir, + topobjdir=obj.topobjdir, + srcdir=srcdir, + srcdir_rel=mozpath.relpath(srcdir, mozpath.dirname(obj.output_path)), + relativesrcdir=mozpath.relpath(srcdir, obj.topsrcdir) or ".", + DEPTH=mozpath.relpath(obj.topobjdir, mozpath.dirname(obj.output_path)) + or ".", + ) + pp.do_filter("attemptSubstitution") + pp.setMarker(None) + with self._write_file(obj.output_path) as fh: + pp.out = fh + yield pp + + +class PartialBackend(BuildBackend): + """A PartialBackend is a BuildBackend declaring that its consume_object + method may not handle all build configuration objects it's passed, and + that it's fine.""" + + +def HybridBackend(*backends): + """A HybridBackend is the combination of one or more PartialBackends + with a non-partial BuildBackend. + + Build configuration objects are passed to each backend, stopping at the + first of them that declares having handled them. + """ + assert len(backends) >= 2 + assert all(issubclass(b, PartialBackend) for b in backends[:-1]) + assert not (issubclass(backends[-1], PartialBackend)) + assert all(issubclass(b, BuildBackend) for b in backends) + + class TheHybridBackend(BuildBackend): + def __init__(self, environment): + self._backends = [b(environment) for b in backends] + super(TheHybridBackend, self).__init__(environment) + + def consume_object(self, obj): + return any(b.consume_object(obj) for b in self._backends) + + def consume_finished(self): + for backend in self._backends: + backend.consume_finished() + + for attr in ( + "_execution_time", + "_created_count", + "_updated_count", + "_unchanged_count", + "_deleted_count", + ): + setattr(self, attr, sum(getattr(b, attr) for b in self._backends)) + + for b in self._backends: + self.file_diffs.update(b.file_diffs) + for attr in ("backend_input_files", "_backend_output_files"): + files = getattr(self, attr) + files |= getattr(b, attr) + + name = "+".join( + itertools.chain( + (b.__name__.replace("Backend", "") for b in backends[:-1]), + (b.__name__ for b in backends[-1:]), + ) + ) + + return type(str(name), (TheHybridBackend,), {}) diff --git a/python/mozbuild/mozbuild/backend/cargo_build_defs.py b/python/mozbuild/mozbuild/backend/cargo_build_defs.py new file mode 100644 index 0000000000..c60fd2abf6 --- /dev/null +++ b/python/mozbuild/mozbuild/backend/cargo_build_defs.py @@ -0,0 +1,87 @@ +# 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/. + +cargo_extra_outputs = { + "bindgen": ["tests.rs", "host-target.txt"], + "cssparser": ["tokenizer.rs"], + "gleam": ["gl_and_gles_bindings.rs", "gl_bindings.rs", "gles_bindings.rs"], + "khronos_api": ["webgl_exts.rs"], + "libloading": ["libglobal_static.a", "src/os/unix/global_static.o"], + "lmdb-sys": ["liblmdb.a", "midl.o", "mdb.o"], + "num-integer": ["rust_out.o"], + "num-traits": ["rust_out.o"], + "selectors": ["ascii_case_insensitive_html_attributes.rs"], + "style": [ + "gecko/atom_macro.rs", + "gecko/bindings.rs", + "gecko/pseudo_element_definition.rs", + "gecko/structs.rs", + "gecko_properties.rs", + "longhands/background.rs", + "longhands/border.rs", + "longhands/box.rs", + "longhands/color.rs", + "longhands/column.rs", + "longhands/counters.rs", + "longhands/effects.rs", + "longhands/font.rs", + "longhands/inherited_box.rs", + "longhands/inherited_svg.rs", + "longhands/inherited_table.rs", + "longhands/inherited_text.rs", + "longhands/inherited_ui.rs", + "longhands/list.rs", + "longhands/margin.rs", + "longhands/outline.rs", + "longhands/padding.rs", + "longhands/position.rs", + "longhands/svg.rs", + "longhands/table.rs", + "longhands/text.rs", + "longhands/ui.rs", + "longhands/xul.rs", + "properties.rs", + "shorthands/background.rs", + "shorthands/border.rs", + "shorthands/box.rs", + "shorthands/color.rs", + "shorthands/column.rs", + "shorthands/counters.rs", + "shorthands/effects.rs", + "shorthands/font.rs", + "shorthands/inherited_box.rs", + "shorthands/inherited_svg.rs", + "shorthands/inherited_table.rs", + "shorthands/inherited_text.rs", + "shorthands/inherited_ui.rs", + "shorthands/list.rs", + "shorthands/margin.rs", + "shorthands/outline.rs", + "shorthands/padding.rs", + "shorthands/position.rs", + "shorthands/svg.rs", + "shorthands/table.rs", + "shorthands/text.rs", + "shorthands/ui.rs", + "shorthands/xul.rs", + ], + "webrender": ["shaders.rs"], + "geckodriver": ["build-info.rs"], + "gecko-profiler": ["gecko/bindings.rs"], + "crc": ["crc64_constants.rs", "crc32_constants.rs"], + "bzip2-sys": [ + "bzip2-1.0.6/blocksort.o", + "bzip2-1.0.6/bzlib.o", + "bzip2-1.0.6/compress.o", + "bzip2-1.0.6/crctable.o", + "bzip2-1.0.6/decompress.o", + "bzip2-1.0.6/huffman.o", + "bzip2-1.0.6/randtable.o", + "libbz2.a", + ], + "clang-sys": ["common.rs", "dynamic.rs"], + "target-lexicon": ["host.rs"], + "baldrdash": ["bindings.rs"], + "typenum": ["op.rs", "consts.rs"], +} diff --git a/python/mozbuild/mozbuild/backend/clangd.py b/python/mozbuild/mozbuild/backend/clangd.py new file mode 100644 index 0000000000..5db5610ae6 --- /dev/null +++ b/python/mozbuild/mozbuild/backend/clangd.py @@ -0,0 +1,126 @@ +# 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/. + +# This module provides a backend for `clangd` in order to have support for +# code completion, compile errors, go-to-definition and more. +# It is based on `database.py` with the difference that we don't generate +# an unified `compile_commands.json` but we generate a per file basis `command` in +# `objdir/clangd/compile_commands.json` + +import os + +import mozpack.path as mozpath + +from mozbuild.compilation.database import CompileDBBackend + + +def find_vscode_cmd(): + import shutil + import sys + + # Try to look up the `code` binary on $PATH, and use it if present. This + # should catch cases like being run from within a vscode-remote shell, + # even if vscode itself is also installed on the remote host. + path = shutil.which("code") + if path is not None: + return [path] + + cmd_and_path = [] + + # If the binary wasn't on $PATH, try to find it in a variety of other + # well-known install locations based on the current platform. + if sys.platform.startswith("darwin"): + cmd_and_path = [ + {"path": "/usr/local/bin/code", "cmd": ["/usr/local/bin/code"]}, + { + "path": "/Applications/Visual Studio Code.app", + "cmd": ["open", "/Applications/Visual Studio Code.app", "--args"], + }, + { + "path": "/Applications/Visual Studio Code - Insiders.app", + "cmd": [ + "open", + "/Applications/Visual Studio Code - Insiders.app", + "--args", + ], + }, + ] + elif sys.platform.startswith("win"): + from pathlib import Path + + vscode_path = mozpath.join( + str(Path.home()), + "AppData", + "Local", + "Programs", + "Microsoft VS Code", + "Code.exe", + ) + vscode_insiders_path = mozpath.join( + str(Path.home()), + "AppData", + "Local", + "Programs", + "Microsoft VS Code Insiders", + "Code - Insiders.exe", + ) + cmd_and_path = [ + {"path": vscode_path, "cmd": [vscode_path]}, + {"path": vscode_insiders_path, "cmd": [vscode_insiders_path]}, + ] + elif sys.platform.startswith("linux"): + cmd_and_path = [ + {"path": "/usr/local/bin/code", "cmd": ["/usr/local/bin/code"]}, + {"path": "/snap/bin/code", "cmd": ["/snap/bin/code"]}, + {"path": "/usr/bin/code", "cmd": ["/usr/bin/code"]}, + {"path": "/usr/bin/code-insiders", "cmd": ["/usr/bin/code-insiders"]}, + ] + + # Did we guess the path? + for element in cmd_and_path: + if os.path.exists(element["path"]): + return element["cmd"] + + # Path cannot be found + return None + + +class ClangdBackend(CompileDBBackend): + """ + Configuration that generates the backend for clangd, it is used with `clangd` + extension for vscode + """ + + def _init(self): + CompileDBBackend._init(self) + + def _get_compiler_args(self, cenv, canonical_suffix): + compiler_args = super(ClangdBackend, self)._get_compiler_args( + cenv, canonical_suffix + ) + if compiler_args is None: + return None + + if len(compiler_args) and compiler_args[0].endswith("ccache"): + compiler_args.pop(0) + return compiler_args + + def _build_cmd(self, cmd, filename, unified): + cmd = list(cmd) + + cmd.append(filename) + + return cmd + + def _outputfile_path(self): + clangd_cc_path = os.path.join(self.environment.topobjdir, "clangd") + + if not os.path.exists(clangd_cc_path): + os.mkdir(clangd_cc_path) + + # Output the database (a JSON file) to objdir/clangd/compile_commands.json + return mozpath.join(clangd_cc_path, "compile_commands.json") + + def _process_unified_sources(self, obj): + self._process_unified_sources_without_mapping(obj) diff --git a/python/mozbuild/mozbuild/backend/common.py b/python/mozbuild/mozbuild/backend/common.py new file mode 100644 index 0000000000..b1c90f31fd --- /dev/null +++ b/python/mozbuild/mozbuild/backend/common.py @@ -0,0 +1,625 @@ +# 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/. + +import itertools +import json +import os +from collections import defaultdict +from operator import itemgetter + +import mozpack.path as mozpath +import six +from mozpack.chrome.manifest import parse_manifest_line + +from mozbuild.backend.base import BuildBackend +from mozbuild.frontend.context import ( + VARIABLES, + Context, + ObjDirPath, + Path, + RenamedSourcePath, +) +from mozbuild.frontend.data import ( + BaseProgram, + ChromeManifestEntry, + ConfigFileSubstitution, + Exports, + FinalTargetFiles, + FinalTargetPreprocessedFiles, + GeneratedFile, + HostLibrary, + HostSources, + IPDLCollection, + LocalizedFiles, + LocalizedPreprocessedFiles, + SandboxedWasmLibrary, + SharedLibrary, + Sources, + StaticLibrary, + UnifiedSources, + WebIDLCollection, + XPCOMComponentManifests, + XPIDLModule, +) +from mozbuild.jar import DeprecatedJarManifest, JarManifestParser +from mozbuild.preprocessor import Preprocessor +from mozbuild.util import mkdir + + +class XPIDLManager(object): + """Helps manage XPCOM IDLs in the context of the build system.""" + + class Module(object): + def __init__(self): + self.idl_files = set() + self.directories = set() + self._stems = set() + + def add_idls(self, idls): + self.idl_files.update(idl.full_path for idl in idls) + self.directories.update(mozpath.dirname(idl.full_path) for idl in idls) + self._stems.update( + mozpath.splitext(mozpath.basename(idl))[0] for idl in idls + ) + + def stems(self): + return iter(self._stems) + + def __init__(self, config): + self.config = config + self.topsrcdir = config.topsrcdir + self.topobjdir = config.topobjdir + + self._idls = set() + self.modules = defaultdict(self.Module) + + def link_module(self, module): + """Links an XPIDL module with with this instance.""" + for idl in module.idl_files: + basename = mozpath.basename(idl.full_path) + + if basename in self._idls: + raise Exception("IDL already registered: %s" % basename) + self._idls.add(basename) + + self.modules[module.name].add_idls(module.idl_files) + + def idl_stems(self): + """Return an iterator of stems of the managed IDL files. + + The stem of an IDL file is the basename of the file with no .idl extension. + """ + return itertools.chain(*[m.stems() for m in six.itervalues(self.modules)]) + + +class BinariesCollection(object): + """Tracks state of binaries produced by the build.""" + + def __init__(self): + self.shared_libraries = [] + self.programs = [] + + +class CommonBackend(BuildBackend): + """Holds logic common to all build backends.""" + + def _init(self): + self._idl_manager = XPIDLManager(self.environment) + self._binaries = BinariesCollection() + self._configs = set() + self._generated_sources = set() + + def consume_object(self, obj): + self._configs.add(obj.config) + + if isinstance(obj, XPIDLModule): + # TODO bug 1240134 tracks not processing XPIDL files during + # artifact builds. + self._idl_manager.link_module(obj) + + elif isinstance(obj, ConfigFileSubstitution): + # Do not handle ConfigFileSubstitution for Makefiles. Leave that + # to other + if mozpath.basename(obj.output_path) == "Makefile": + return False + with self._get_preprocessor(obj) as pp: + pp.do_include(obj.input_path) + self.backend_input_files.add(obj.input_path) + + elif isinstance(obj, WebIDLCollection): + self._handle_webidl_collection(obj) + + elif isinstance(obj, IPDLCollection): + self._handle_ipdl_sources( + obj.objdir, + list(sorted(obj.all_sources())), + list(sorted(obj.all_preprocessed_sources())), + list(sorted(obj.all_regular_sources())), + ) + + elif isinstance(obj, XPCOMComponentManifests): + self._handle_xpcom_collection(obj) + + elif isinstance(obj, UnifiedSources): + if obj.generated_files: + self._handle_generated_sources(obj.generated_files) + + # Unified sources aren't relevant to artifact builds. + if self.environment.is_artifact_build: + return True + + if obj.have_unified_mapping: + self._write_unified_files(obj.unified_source_mapping, obj.objdir) + if hasattr(self, "_process_unified_sources"): + self._process_unified_sources(obj) + + elif isinstance(obj, BaseProgram): + self._binaries.programs.append(obj) + return False + + elif isinstance(obj, SharedLibrary): + self._binaries.shared_libraries.append(obj) + return False + + elif isinstance(obj, SandboxedWasmLibrary): + self._handle_generated_sources( + [mozpath.join(obj.relobjdir, f"{obj.basename}.h")] + ) + return False + + elif isinstance(obj, (Sources, HostSources)): + if obj.generated_files: + self._handle_generated_sources(obj.generated_files) + return False + + elif isinstance(obj, GeneratedFile): + for f in obj.outputs: + if f == "cbindgen-metadata.json": + # FIXME (bug 1865785) + # + # The content of cbindgen-metadata.json is not sorted and + # the order is not consistent across multiple runs. + # + # Exclude this file in order to avoid breaking the + # taskcluster/ci/diffoscope/reproducible.yml jobs. + continue + fullpath = ObjDirPath(obj._context, "!" + f).full_path + self._handle_generated_sources([fullpath]) + return False + + elif isinstance(obj, Exports): + objdir_files = [ + f.full_path + for path, files in obj.files.walk() + for f in files + if isinstance(f, ObjDirPath) + ] + if objdir_files: + self._handle_generated_sources(objdir_files) + return False + + else: + return False + + return True + + def consume_finished(self): + if len(self._idl_manager.modules): + self._write_rust_xpidl_summary(self._idl_manager) + self._handle_idl_manager(self._idl_manager) + self._handle_xpidl_sources() + + for config in self._configs: + self.backend_input_files.add(config.source) + + # Write out a machine-readable file describing binaries. + topobjdir = self.environment.topobjdir + with self._write_file(mozpath.join(topobjdir, "binaries.json")) as fh: + d = { + "shared_libraries": sorted( + (s.to_dict() for s in self._binaries.shared_libraries), + key=itemgetter("basename"), + ), + "programs": sorted( + (p.to_dict() for p in self._binaries.programs), + key=itemgetter("program"), + ), + } + json.dump(d, fh, sort_keys=True, indent=4) + + # Write out a file listing generated sources. + with self._write_file(mozpath.join(topobjdir, "generated-sources.json")) as fh: + d = {"sources": sorted(self._generated_sources)} + json.dump(d, fh, sort_keys=True, indent=4) + + def _expand_libs(self, input_bin): + os_libs = [] + shared_libs = [] + static_libs = [] + objs = [] + + seen_objs = set() + seen_libs = set() + + def add_objs(lib): + for o in lib.objs: + if o in seen_objs: + continue + + seen_objs.add(o) + objs.append(o) + + def expand(lib, recurse_objs, system_libs): + if isinstance(lib, (HostLibrary, StaticLibrary, SandboxedWasmLibrary)): + if lib.no_expand_lib: + static_libs.append(lib) + recurse_objs = False + elif recurse_objs: + add_objs(lib) + + for l in lib.linked_libraries: + expand(l, recurse_objs, system_libs) + + if system_libs: + for l in lib.linked_system_libs: + if l not in seen_libs: + seen_libs.add(l) + os_libs.append(l) + + elif isinstance(lib, SharedLibrary): + if lib not in seen_libs: + seen_libs.add(lib) + shared_libs.append(lib) + + add_objs(input_bin) + + system_libs = not isinstance( + input_bin, (HostLibrary, StaticLibrary, SandboxedWasmLibrary) + ) + for lib in input_bin.linked_libraries: + if isinstance(lib, (HostLibrary, StaticLibrary, SandboxedWasmLibrary)): + expand(lib, True, system_libs) + elif isinstance(lib, SharedLibrary): + if lib not in seen_libs: + seen_libs.add(lib) + shared_libs.append(lib) + + for lib in input_bin.linked_system_libs: + if lib not in seen_libs: + seen_libs.add(lib) + os_libs.append(lib) + + return (objs, shared_libs, os_libs, static_libs) + + def _make_ar_response_file(self, objdir, objs, name): + if not objs: + return None + + if not self.environment.substs.get("AR_SUPPORTS_RESPONSE_FILE"): + return None + + response_file_path = mozpath.join(objdir, name) + ref = "@" + response_file_path + content = "\n".join(objs) + + mkdir(objdir) + with self._write_file(response_file_path) as fh: + fh.write(content) + + return ref + + def _make_list_file(self, kind, objdir, objs, name): + if not objs: + return None + if kind == "target": + list_style = self.environment.substs.get("EXPAND_LIBS_LIST_STYLE") + else: + # The host compiler is not necessarily the same kind as the target + # compiler, so we can't be sure EXPAND_LIBS_LIST_STYLE is the right + # style to use ; however, all compilers support the `list` type, so + # use that. That doesn't cause any practical problem because where + # it really matters to use something else than `list` is when + # linking tons of objects (because of command line argument limits), + # which only really happens for libxul. + list_style = "list" + list_file_path = mozpath.join(objdir, name) + objs = [os.path.relpath(o, objdir) for o in objs] + if list_style == "linkerscript": + ref = list_file_path + content = "\n".join('INPUT("%s")' % o for o in objs) + elif list_style == "filelist": + ref = "-Wl,-filelist," + list_file_path + content = "\n".join(objs) + elif list_style == "list": + ref = "@" + list_file_path + content = "\n".join(objs) + else: + return None + + mkdir(objdir) + with self._write_file(list_file_path) as fh: + fh.write(content) + + return ref + + def _handle_generated_sources(self, files): + self._generated_sources.update( + mozpath.relpath(f, self.environment.topobjdir) for f in files + ) + + def _handle_xpidl_sources(self): + bindings_rt_dir = mozpath.join( + self.environment.topobjdir, "dist", "xpcrs", "rt" + ) + bindings_bt_dir = mozpath.join( + self.environment.topobjdir, "dist", "xpcrs", "bt" + ) + include_dir = mozpath.join(self.environment.topobjdir, "dist", "include") + + self._handle_generated_sources( + itertools.chain.from_iterable( + ( + mozpath.join(include_dir, "%s.h" % stem), + mozpath.join(bindings_rt_dir, "%s.rs" % stem), + mozpath.join(bindings_bt_dir, "%s.rs" % stem), + ) + for stem in self._idl_manager.idl_stems() + ) + ) + + def _handle_webidl_collection(self, webidls): + bindings_dir = mozpath.join(self.environment.topobjdir, "dom", "bindings") + + all_inputs = set(webidls.all_static_sources()) + for s in webidls.all_non_static_basenames(): + all_inputs.add(mozpath.join(bindings_dir, s)) + + generated_events_stems = webidls.generated_events_stems() + exported_stems = webidls.all_regular_stems() + + # The WebIDL manager reads configuration from a JSON file. So, we + # need to write this file early. + o = dict( + webidls=sorted(all_inputs), + generated_events_stems=sorted(generated_events_stems), + exported_stems=sorted(exported_stems), + example_interfaces=sorted(webidls.example_interfaces), + ) + + file_lists = mozpath.join(bindings_dir, "file-lists.json") + with self._write_file(file_lists) as fh: + json.dump(o, fh, sort_keys=True, indent=2) + + import mozwebidlcodegen + + manager = mozwebidlcodegen.create_build_system_manager( + self.environment.topsrcdir, + self.environment.topobjdir, + mozpath.join(self.environment.topobjdir, "dist"), + ) + self._handle_generated_sources(manager.expected_build_output_files()) + self._write_unified_files( + webidls.unified_source_mapping, bindings_dir, poison_windows_h=True + ) + self._handle_webidl_build( + bindings_dir, + webidls.unified_source_mapping, + webidls, + manager.expected_build_output_files(), + manager.GLOBAL_DEFINE_FILES, + ) + + def _handle_xpcom_collection(self, manifests): + components_dir = mozpath.join(manifests.topobjdir, "xpcom", "components") + + # The code generators read their configuration from this file, so it + # needs to be written early. + o = dict(manifests=sorted(manifests.all_sources())) + + conf_file = mozpath.join(components_dir, "manifest-lists.json") + with self._write_file(conf_file) as fh: + json.dump(o, fh, sort_keys=True, indent=2) + + def _write_unified_file( + self, unified_file, source_filenames, output_directory, poison_windows_h=False + ): + with self._write_file(mozpath.join(output_directory, unified_file)) as f: + f.write("#define MOZ_UNIFIED_BUILD\n") + includeTemplate = '#include "%(cppfile)s"' + if poison_windows_h: + includeTemplate += ( + "\n" + "#if defined(_WINDOWS_) && !defined(MOZ_WRAPPED_WINDOWS_H)\n" + '#pragma message("wrapper failure reason: " MOZ_WINDOWS_WRAPPER_DISABLED_REASON)\n' # noqa + '#error "%(cppfile)s included unwrapped windows.h"\n' + "#endif" + ) + includeTemplate += ( + "\n" + "#ifdef PL_ARENA_CONST_ALIGN_MASK\n" + '#error "%(cppfile)s uses PL_ARENA_CONST_ALIGN_MASK, ' + 'so it cannot be built in unified mode."\n' + "#undef PL_ARENA_CONST_ALIGN_MASK\n" + "#endif\n" + "#ifdef INITGUID\n" + '#error "%(cppfile)s defines INITGUID, ' + 'so it cannot be built in unified mode."\n' + "#undef INITGUID\n" + "#endif" + ) + f.write( + "\n".join(includeTemplate % {"cppfile": s} for s in source_filenames) + ) + + def _write_unified_files( + self, unified_source_mapping, output_directory, poison_windows_h=False + ): + for unified_file, source_filenames in unified_source_mapping: + self._write_unified_file( + unified_file, source_filenames, output_directory, poison_windows_h + ) + + def localized_path(self, relativesrcdir, filename): + """Return the localized path for a file. + + Given ``relativesrcdir``, a path relative to the topsrcdir, return a path to ``filename`` + from the current locale as specified by ``MOZ_UI_LOCALE``, using ``L10NBASEDIR`` as the + parent directory for non-en-US locales. + """ + ab_cd = self.environment.substs["MOZ_UI_LOCALE"][0] + l10nbase = mozpath.join(self.environment.substs["L10NBASEDIR"], ab_cd) + # Filenames from LOCALIZED_FILES will start with en-US/. + if filename.startswith("en-US/"): + e, filename = filename.split("en-US/") + assert not e + if ab_cd == "en-US": + return mozpath.join( + self.environment.topsrcdir, relativesrcdir, "en-US", filename + ) + if mozpath.basename(relativesrcdir) == "locales": + l10nrelsrcdir = mozpath.dirname(relativesrcdir) + else: + l10nrelsrcdir = relativesrcdir + return mozpath.join(l10nbase, l10nrelsrcdir, filename) + + def _consume_jar_manifest(self, obj): + # Ideally, this would all be handled somehow in the emitter, but + # this would require all the magic surrounding l10n and addons in + # the recursive make backend to die, which is not going to happen + # any time soon enough. + # Notably missing: + # - DEFINES from config/config.mk + # - The equivalent of -e when USE_EXTENSION_MANIFEST is set in + # moz.build, but it doesn't matter in dist/bin. + pp = Preprocessor() + if obj.defines: + pp.context.update(obj.defines.defines) + pp.context.update(self.environment.defines) + ab_cd = obj.config.substs["MOZ_UI_LOCALE"][0] + pp.context.update(AB_CD=ab_cd) + pp.out = JarManifestParser() + try: + pp.do_include(obj.path.full_path) + except DeprecatedJarManifest as e: + raise DeprecatedJarManifest( + "Parsing error while processing %s: %s" % (obj.path.full_path, e) + ) + self.backend_input_files |= pp.includes + + for jarinfo in pp.out: + jar_context = Context( + allowed_variables=VARIABLES, config=obj._context.config + ) + jar_context.push_source(obj._context.main_path) + jar_context.push_source(obj.path.full_path) + + install_target = obj.install_target + if jarinfo.base: + install_target = mozpath.normpath( + mozpath.join(install_target, jarinfo.base) + ) + jar_context["FINAL_TARGET"] = install_target + if obj.defines: + jar_context["DEFINES"] = obj.defines.defines + files = jar_context["FINAL_TARGET_FILES"] + files_pp = jar_context["FINAL_TARGET_PP_FILES"] + localized_files = jar_context["LOCALIZED_FILES"] + localized_files_pp = jar_context["LOCALIZED_PP_FILES"] + + for e in jarinfo.entries: + if e.is_locale: + if jarinfo.relativesrcdir: + src = "/%s" % jarinfo.relativesrcdir + else: + src = "" + src = mozpath.join(src, "en-US", e.source) + else: + src = e.source + + src = Path(jar_context, src) + + if "*" not in e.source and not os.path.exists(src.full_path): + if e.is_locale: + raise Exception( + "%s: Cannot find %s (tried %s)" + % (obj.path, e.source, src.full_path) + ) + if e.source.startswith("/"): + src = Path(jar_context, "!" + e.source) + else: + # This actually gets awkward if the jar.mn is not + # in the same directory as the moz.build declaring + # it, but it's how it works in the recursive make, + # not that anything relies on that, but it's simpler. + src = Path(obj._context, "!" + e.source) + + output_basename = mozpath.basename(e.output) + if output_basename != src.target_basename: + src = RenamedSourcePath(jar_context, (src, output_basename)) + path = mozpath.dirname(mozpath.join(jarinfo.name, e.output)) + + if e.preprocess: + if "*" in e.source: + raise Exception( + "%s: Wildcards are not supported with " + "preprocessing" % obj.path + ) + if e.is_locale: + localized_files_pp[path] += [src] + else: + files_pp[path] += [src] + else: + if e.is_locale: + localized_files[path] += [src] + else: + files[path] += [src] + + if files: + self.consume_object(FinalTargetFiles(jar_context, files)) + if files_pp: + self.consume_object(FinalTargetPreprocessedFiles(jar_context, files_pp)) + if localized_files: + self.consume_object(LocalizedFiles(jar_context, localized_files)) + if localized_files_pp: + self.consume_object( + LocalizedPreprocessedFiles(jar_context, localized_files_pp) + ) + + for m in jarinfo.chrome_manifests: + entry = parse_manifest_line( + mozpath.dirname(jarinfo.name), + m.replace("%", mozpath.basename(jarinfo.name) + "/"), + ) + self.consume_object( + ChromeManifestEntry( + jar_context, "%s.manifest" % jarinfo.name, entry + ) + ) + + def _write_rust_xpidl_summary(self, manager): + """Write out a rust file which includes the generated xpcom rust modules""" + topobjdir = self.environment.topobjdir + + include_tmpl = 'include!(mozbuild::objdir_path!("dist/xpcrs/%s/%s.rs"))' + + # Ensure deterministic output files. + stems = sorted(manager.idl_stems()) + + with self._write_file( + mozpath.join(topobjdir, "dist", "xpcrs", "rt", "all.rs") + ) as fh: + fh.write("// THIS FILE IS GENERATED - DO NOT EDIT\n\n") + for stem in stems: + fh.write(include_tmpl % ("rt", stem)) + fh.write(";\n") + + with self._write_file( + mozpath.join(topobjdir, "dist", "xpcrs", "bt", "all.rs") + ) as fh: + fh.write("// THIS FILE IS GENERATED - DO NOT EDIT\n\n") + fh.write("&[\n") + for stem in stems: + fh.write(include_tmpl % ("bt", stem)) + fh.write(",\n") + fh.write("]\n") diff --git a/python/mozbuild/mozbuild/backend/configenvironment.py b/python/mozbuild/mozbuild/backend/configenvironment.py new file mode 100644 index 0000000000..770db73339 --- /dev/null +++ b/python/mozbuild/mozbuild/backend/configenvironment.py @@ -0,0 +1,356 @@ +# 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/. + +import json +import os +import sys +from collections import OrderedDict +from collections.abc import Iterable +from pathlib import Path +from types import ModuleType + +import mozpack.path as mozpath +import six + +from mozbuild.shellutil import quote as shell_quote +from mozbuild.util import ( + FileAvoidWrite, + ReadOnlyDict, + memoized_property, + system_encoding, +) + + +class ConfigStatusFailure(Exception): + """Error loading config.status""" + + +class BuildConfig(object): + """Represents the output of configure.""" + + _CODE_CACHE = {} + + def __init__(self): + self.topsrcdir = None + self.topobjdir = None + self.defines = {} + self.substs = {} + self.files = [] + self.mozconfig = None + + @classmethod + def from_config_status(cls, path): + """Create an instance from a config.status file.""" + code_cache = cls._CODE_CACHE + mtime = os.path.getmtime(path) + + # cache the compiled code as it can be reused + # we cache it the first time, or if the file changed + if path not in code_cache or code_cache[path][0] != mtime: + # Add config.status manually to sys.modules so it gets picked up by + # iter_modules_in_path() for automatic dependencies. + mod = ModuleType("config.status") + mod.__file__ = path + sys.modules["config.status"] = mod + + with open(path, "rt") as fh: + source = fh.read() + code_cache[path] = ( + mtime, + compile(source, path, "exec", dont_inherit=1), + ) + + g = {"__builtins__": __builtins__, "__file__": path} + l = {} + try: + exec(code_cache[path][1], g, l) + except Exception: + raise ConfigStatusFailure() + + config = BuildConfig() + + for name in l["__all__"]: + setattr(config, name, l[name]) + + return config + + +class ConfigEnvironment(object): + """Perform actions associated with a configured but bare objdir. + + The purpose of this class is to preprocess files from the source directory + and output results in the object directory. + + There are two types of files: config files and config headers, + each treated through a different member function. + + Creating a ConfigEnvironment requires a few arguments: + - topsrcdir and topobjdir are, respectively, the top source and + the top object directory. + - defines is a dict filled from AC_DEFINE and AC_DEFINE_UNQUOTED in autoconf. + - substs is a dict filled from AC_SUBST in autoconf. + + ConfigEnvironment automatically defines one additional substs variable + from all the defines: + - ACDEFINES contains the defines in the form -DNAME=VALUE, for use on + preprocessor command lines. The order in which defines were given + when creating the ConfigEnvironment is preserved. + + and two other additional subst variables from all the other substs: + - ALLSUBSTS contains the substs in the form NAME = VALUE, in sorted + order, for use in autoconf.mk. It includes ACDEFINES. + Only substs with a VALUE are included, such that the resulting file + doesn't change when new empty substs are added. + This results in less invalidation of build dependencies in the case + of autoconf.mk.. + - ALLEMPTYSUBSTS contains the substs with an empty value, in the form NAME =. + + ConfigEnvironment expects a "top_srcdir" subst to be set with the top + source directory, in msys format on windows. It is used to derive a + "srcdir" subst when treating config files. It can either be an absolute + path or a path relative to the topobjdir. + """ + + def __init__( + self, + topsrcdir, + topobjdir, + defines=None, + substs=None, + source=None, + mozconfig=None, + ): + if not source: + source = mozpath.join(topobjdir, "config.status") + self.source = source + self.defines = ReadOnlyDict(defines or {}) + self.substs = dict(substs or {}) + self.topsrcdir = mozpath.abspath(topsrcdir) + self.topobjdir = mozpath.abspath(topobjdir) + self.mozconfig = mozpath.abspath(mozconfig) if mozconfig else None + self.lib_prefix = self.substs.get("LIB_PREFIX", "") + if "LIB_SUFFIX" in self.substs: + self.lib_suffix = ".%s" % self.substs["LIB_SUFFIX"] + self.dll_prefix = self.substs.get("DLL_PREFIX", "") + self.dll_suffix = self.substs.get("DLL_SUFFIX", "") + self.host_dll_prefix = self.substs.get("HOST_DLL_PREFIX", "") + self.host_dll_suffix = self.substs.get("HOST_DLL_SUFFIX", "") + if self.substs.get("IMPORT_LIB_SUFFIX"): + self.import_prefix = self.lib_prefix + self.import_suffix = ".%s" % self.substs["IMPORT_LIB_SUFFIX"] + else: + self.import_prefix = self.dll_prefix + self.import_suffix = self.dll_suffix + if self.substs.get("HOST_IMPORT_LIB_SUFFIX"): + self.host_import_prefix = self.substs.get("HOST_LIB_PREFIX", "") + self.host_import_suffix = ".%s" % self.substs["HOST_IMPORT_LIB_SUFFIX"] + else: + self.host_import_prefix = self.host_dll_prefix + self.host_import_suffix = self.host_dll_suffix + self.bin_suffix = self.substs.get("BIN_SUFFIX", "") + + global_defines = [name for name in self.defines] + self.substs["ACDEFINES"] = " ".join( + [ + "-D%s=%s" % (name, shell_quote(self.defines[name]).replace("$", "$$")) + for name in sorted(global_defines) + ] + ) + + def serialize(name, obj): + if isinstance(obj, six.string_types): + return obj + if isinstance(obj, Iterable): + return " ".join(obj) + raise Exception("Unhandled type %s for %s", type(obj), str(name)) + + self.substs["ALLSUBSTS"] = "\n".join( + sorted( + [ + "%s = %s" % (name, serialize(name, self.substs[name])) + for name in self.substs + if self.substs[name] + ] + ) + ) + self.substs["ALLEMPTYSUBSTS"] = "\n".join( + sorted(["%s =" % name for name in self.substs if not self.substs[name]]) + ) + + self.substs = ReadOnlyDict(self.substs) + + @property + def is_artifact_build(self): + return self.substs.get("MOZ_ARTIFACT_BUILDS", False) + + @memoized_property + def acdefines(self): + acdefines = dict((name, self.defines[name]) for name in self.defines) + return ReadOnlyDict(acdefines) + + @staticmethod + def from_config_status(path): + config = BuildConfig.from_config_status(path) + + return ConfigEnvironment( + config.topsrcdir, config.topobjdir, config.defines, config.substs, path + ) + + +class PartialConfigDict(object): + """Facilitates mapping the config.statusd defines & substs with dict-like access. + + This allows a buildconfig client to use buildconfig.defines['FOO'] (and + similar for substs), where the value of FOO is delay-loaded until it is + needed. + """ + + def __init__(self, config_statusd, typ, environ_override=False): + self._dict = {} + self._datadir = mozpath.join(config_statusd, typ) + self._config_track = mozpath.join(self._datadir, "config.track") + self._files = set() + self._environ_override = environ_override + + def _load_config_track(self): + existing_files = set() + try: + with open(self._config_track) as fh: + existing_files.update(fh.read().splitlines()) + except IOError: + pass + return existing_files + + def _write_file(self, key, value): + filename = mozpath.join(self._datadir, key) + with FileAvoidWrite(filename) as fh: + to_write = json.dumps(value, indent=4) + fh.write(to_write.encode(system_encoding)) + return filename + + def _fill_group(self, values): + # Clear out any cached values. This is mostly for tests that will check + # the environment, write out a new set of variables, and then check the + # environment again. Normally only configure ends up calling this + # function, and other consumers create their own + # PartialConfigEnvironments in new python processes. + self._dict = {} + + existing_files = self._load_config_track() + existing_files = {Path(f) for f in existing_files} + + new_files = set() + for k, v in six.iteritems(values): + new_files.add(Path(self._write_file(k, v))) + + for filename in existing_files - new_files: + # We can't actually os.remove() here, since make would not see that the + # file has been removed and that the target needs to be updated. Instead + # we just overwrite the file with a value of None, which is equivalent + # to a non-existing file. + with FileAvoidWrite(filename) as fh: + json.dump(None, fh) + + with FileAvoidWrite(self._config_track) as fh: + for f in sorted(new_files): + fh.write("%s\n" % f) + + def __getitem__(self, key): + if self._environ_override: + if (key not in ("CPP", "CXXCPP", "SHELL")) and (key in os.environ): + return os.environ[key] + + if key not in self._dict: + data = None + try: + filename = mozpath.join(self._datadir, key) + self._files.add(filename) + with open(filename) as f: + data = json.load(f) + except IOError: + pass + self._dict[key] = data + + if self._dict[key] is None: + raise KeyError("'%s'" % key) + return self._dict[key] + + def __setitem__(self, key, value): + self._dict[key] = value + + def get(self, key, default=None): + return self[key] if key in self else default + + def __contains__(self, key): + try: + return self[key] is not None + except KeyError: + return False + + def iteritems(self): + existing_files = self._load_config_track() + for f in existing_files: + # The track file contains filenames, and the basename is the + # variable name. + var = mozpath.basename(f) + yield var, self[var] + + +class PartialConfigEnvironment(object): + """Allows access to individual config.status items via config.statusd/* files. + + This class is similar to the full ConfigEnvironment, which uses + config.status, except this allows access and tracks dependencies to + individual configure values. It is intended to be used during the build + process to handle things like GENERATED_FILES, CONFIGURE_DEFINE_FILES, and + anything else that may need to access specific substs or defines. + + Creating a PartialConfigEnvironment requires only the topobjdir, which is + needed to distinguish between the top-level environment and the js/src + environment. + + The PartialConfigEnvironment automatically defines one additional subst variable + from all the defines: + + - ACDEFINES contains the defines in the form -DNAME=VALUE, for use on + preprocessor command lines. The order in which defines were given + when creating the ConfigEnvironment is preserved. + + and one additional define from all the defines as a dictionary: + + - ALLDEFINES contains all of the global defines as a dictionary. This is + intended to be used instead of the defines structure from config.status so + that scripts can depend directly on its value. + """ + + def __init__(self, topobjdir): + config_statusd = mozpath.join(topobjdir, "config.statusd") + self.substs = PartialConfigDict(config_statusd, "substs", environ_override=True) + self.defines = PartialConfigDict(config_statusd, "defines") + self.topobjdir = topobjdir + + def write_vars(self, config): + substs = config["substs"].copy() + defines = config["defines"].copy() + + global_defines = [name for name in config["defines"]] + acdefines = " ".join( + [ + "-D%s=%s" + % (name, shell_quote(config["defines"][name]).replace("$", "$$")) + for name in sorted(global_defines) + ] + ) + substs["ACDEFINES"] = acdefines + + all_defines = OrderedDict() + for k in global_defines: + all_defines[k] = config["defines"][k] + defines["ALLDEFINES"] = all_defines + + self.substs._fill_group(substs) + self.defines._fill_group(defines) + + def get_dependencies(self): + return ["$(wildcard %s)" % f for f in self.substs._files | self.defines._files] diff --git a/python/mozbuild/mozbuild/backend/cpp_eclipse.py b/python/mozbuild/mozbuild/backend/cpp_eclipse.py new file mode 100644 index 0000000000..f2bd5ecd85 --- /dev/null +++ b/python/mozbuild/mozbuild/backend/cpp_eclipse.py @@ -0,0 +1,876 @@ +# 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/. + +import errno +import glob +import os +import shutil +import subprocess +from xml.sax.saxutils import quoteattr + +from mozbuild.base import ExecutionSummary + +from ..frontend.data import ComputedFlags +from .common import CommonBackend + +# TODO Have ./mach eclipse generate the workspace and index it: +# /Users/bgirard/mozilla/eclipse/eclipse/eclipse/eclipse -application org.eclipse.cdt.managedbuilder.core.headlessbuild -data $PWD/workspace -importAll $PWD/eclipse +# Open eclipse: +# /Users/bgirard/mozilla/eclipse/eclipse/eclipse/eclipse -data $PWD/workspace + + +class CppEclipseBackend(CommonBackend): + """Backend that generates Cpp Eclipse project files.""" + + def __init__(self, environment): + if os.name == "nt": + raise Exception( + "Eclipse is not supported on Windows. " + "Consider using Visual Studio instead." + ) + super(CppEclipseBackend, self).__init__(environment) + + def _init(self): + CommonBackend._init(self) + + self._args_for_dirs = {} + self._project_name = "Gecko" + self._workspace_dir = self._get_workspace_path() + self._workspace_lang_dir = os.path.join( + self._workspace_dir, ".metadata/.plugins/org.eclipse.cdt.core" + ) + self._project_dir = os.path.join(self._workspace_dir, self._project_name) + self._overwriting_workspace = os.path.isdir(self._workspace_dir) + + self._macbundle = self.environment.substs["MOZ_MACBUNDLE_NAME"] + self._appname = self.environment.substs["MOZ_APP_NAME"] + self._bin_suffix = self.environment.substs["BIN_SUFFIX"] + self._cxx = self.environment.substs["CXX"] + # Note: We need the C Pre Processor (CPP) flags, not the CXX flags + self._cppflags = self.environment.substs.get("CPPFLAGS", "") + + def summary(self): + return ExecutionSummary( + "CppEclipse backend executed in {execution_time:.2f}s\n" + 'Generated Cpp Eclipse workspace in "{workspace:s}".\n' + "If missing, import the project using File > Import > General > Existing Project into workspace\n" + "\n" + "Run with: eclipse -data {workspace:s}\n", + execution_time=self._execution_time, + workspace=self._workspace_dir, + ) + + def _get_workspace_path(self): + return CppEclipseBackend.get_workspace_path( + self.environment.topsrcdir, self.environment.topobjdir + ) + + @staticmethod + def get_workspace_path(topsrcdir, topobjdir): + # Eclipse doesn't support having the workspace inside the srcdir. + # Since most people have their objdir inside their srcdir it's easier + # and more consistent to just put the workspace along side the srcdir + srcdir_parent = os.path.dirname(topsrcdir) + workspace_dirname = "eclipse_" + os.path.basename(topobjdir) + return os.path.join(srcdir_parent, workspace_dirname) + + def consume_object(self, obj): + reldir = getattr(obj, "relsrcdir", None) + + # Note that unlike VS, Eclipse' indexer seem to crawl the headers and + # isn't picky about the local includes. + if isinstance(obj, ComputedFlags): + args = self._args_for_dirs.setdefault( + "tree/" + reldir, {"includes": [], "defines": []} + ) + # use the same args for any objdirs we include: + if reldir == "dom/bindings": + self._args_for_dirs.setdefault("generated-webidl", args) + if reldir == "ipc/ipdl": + self._args_for_dirs.setdefault("generated-ipdl", args) + + includes = args["includes"] + if "BASE_INCLUDES" in obj.flags and obj.flags["BASE_INCLUDES"]: + includes += obj.flags["BASE_INCLUDES"] + if "LOCAL_INCLUDES" in obj.flags and obj.flags["LOCAL_INCLUDES"]: + includes += obj.flags["LOCAL_INCLUDES"] + + defs = args["defines"] + if "DEFINES" in obj.flags and obj.flags["DEFINES"]: + defs += obj.flags["DEFINES"] + if "LIBRARY_DEFINES" in obj.flags and obj.flags["LIBRARY_DEFINES"]: + defs += obj.flags["LIBRARY_DEFINES"] + + return True + + def consume_finished(self): + settings_dir = os.path.join(self._project_dir, ".settings") + launch_dir = os.path.join(self._project_dir, "RunConfigurations") + workspace_settings_dir = os.path.join( + self._workspace_dir, ".metadata/.plugins/org.eclipse.core.runtime/.settings" + ) + + for dir_name in [ + self._project_dir, + settings_dir, + launch_dir, + workspace_settings_dir, + self._workspace_lang_dir, + ]: + try: + os.makedirs(dir_name) + except OSError as e: + if e.errno != errno.EEXIST: + raise + + project_path = os.path.join(self._project_dir, ".project") + with open(project_path, "w") as fh: + self._write_project(fh) + + cproject_path = os.path.join(self._project_dir, ".cproject") + with open(cproject_path, "w") as fh: + self._write_cproject(fh) + + language_path = os.path.join(settings_dir, "language.settings.xml") + with open(language_path, "w") as fh: + self._write_language_settings(fh) + + workspace_language_path = os.path.join( + self._workspace_lang_dir, "language.settings.xml" + ) + with open(workspace_language_path, "w") as fh: + workspace_lang_settings = WORKSPACE_LANGUAGE_SETTINGS_TEMPLATE + workspace_lang_settings = workspace_lang_settings.replace( + "@COMPILER_FLAGS@", self._cxx + " " + self._cppflags + ) + fh.write(workspace_lang_settings) + + self._write_launch_files(launch_dir) + + core_resources_prefs_path = os.path.join( + workspace_settings_dir, "org.eclipse.core.resources.prefs" + ) + with open(core_resources_prefs_path, "w") as fh: + fh.write(STATIC_CORE_RESOURCES_PREFS) + + core_runtime_prefs_path = os.path.join( + workspace_settings_dir, "org.eclipse.core.runtime.prefs" + ) + with open(core_runtime_prefs_path, "w") as fh: + fh.write(STATIC_CORE_RUNTIME_PREFS) + + ui_prefs_path = os.path.join(workspace_settings_dir, "org.eclipse.ui.prefs") + with open(ui_prefs_path, "w") as fh: + fh.write(STATIC_UI_PREFS) + + cdt_ui_prefs_path = os.path.join( + workspace_settings_dir, "org.eclipse.cdt.ui.prefs" + ) + cdt_ui_prefs = STATIC_CDT_UI_PREFS + # Here we generate the code formatter that will show up in the UI with + # the name "Mozilla". The formatter is stored as a single line of XML + # in the org.eclipse.cdt.ui.formatterprofiles pref. + cdt_ui_prefs += """org.eclipse.cdt.ui.formatterprofiles=<?xml version\="1.0" encoding\="UTF-8" standalone\="no"?>\\n<profiles version\="1">\\n<profile kind\="CodeFormatterProfile" name\="Mozilla" version\="1">\\n""" + XML_PREF_TEMPLATE = """<setting id\="@PREF_NAME@" value\="@PREF_VAL@"/>\\n""" + for line in FORMATTER_SETTINGS.splitlines(): + [pref, val] = line.split("=") + cdt_ui_prefs += XML_PREF_TEMPLATE.replace("@PREF_NAME@", pref).replace( + "@PREF_VAL@", val + ) + cdt_ui_prefs += "</profile>\\n</profiles>\\n" + with open(cdt_ui_prefs_path, "w") as fh: + fh.write(cdt_ui_prefs) + + cdt_core_prefs_path = os.path.join( + workspace_settings_dir, "org.eclipse.cdt.core.prefs" + ) + with open(cdt_core_prefs_path, "w") as fh: + cdt_core_prefs = STATIC_CDT_CORE_PREFS + # When we generated the code formatter called "Mozilla" above, we + # also set it to be the active formatter. When a formatter is set + # as the active formatter all its prefs are set in this prefs file, + # so we need add those now: + cdt_core_prefs += FORMATTER_SETTINGS + fh.write(cdt_core_prefs) + + editor_prefs_path = os.path.join( + workspace_settings_dir, "org.eclipse.ui.editors.prefs" + ) + with open(editor_prefs_path, "w") as fh: + fh.write(EDITOR_SETTINGS) + + # Now import the project into the workspace + self._import_project() + + def _import_project(self): + # If the workspace already exists then don't import the project again because + # eclipse doesn't handle this properly + if self._overwriting_workspace: + return + + # We disable the indexer otherwise we're forced to index + # the whole codebase when importing the project. Indexing the project can take 20 minutes. + self._write_noindex() + + try: + subprocess.check_call( + [ + "eclipse", + "-application", + "-nosplash", + "org.eclipse.cdt.managedbuilder.core.headlessbuild", + "-data", + self._workspace_dir, + "-importAll", + self._project_dir, + ] + ) + except OSError as e: + # Remove the workspace directory so we re-generate it and + # try to import again when the backend is invoked again. + shutil.rmtree(self._workspace_dir) + + if e.errno == errno.ENOENT: + raise Exception( + "Failed to launch eclipse to import project. " + "Ensure 'eclipse' is in your PATH and try again" + ) + else: + raise + finally: + self._remove_noindex() + + def _write_noindex(self): + noindex_path = os.path.join( + self._project_dir, ".settings/org.eclipse.cdt.core.prefs" + ) + with open(noindex_path, "w") as fh: + fh.write(NOINDEX_TEMPLATE) + + def _remove_noindex(self): + # Below we remove the config file that temporarily disabled the indexer + # while we were importing the project. Unfortunately, CDT doesn't + # notice indexer settings changes in config files when it restarts. To + # work around that we remove the index database here to force it to: + for f in glob.glob(os.path.join(self._workspace_lang_dir, "Gecko.*.pdom")): + os.remove(f) + + noindex_path = os.path.join( + self._project_dir, ".settings/org.eclipse.cdt.core.prefs" + ) + # This may fail if the entire tree has been removed; that's fine. + try: + os.remove(noindex_path) + except OSError as e: + if e.errno != errno.ENOENT: + raise + + def _write_language_settings(self, fh): + def add_abs_include_path(absinclude): + assert absinclude[:3] == "-I/" + return LANGUAGE_SETTINGS_TEMPLATE_DIR_INCLUDE.replace( + "@INCLUDE_PATH@", absinclude[2:] + ) + + def add_objdir_include_path(relpath): + p = os.path.join(self.environment.topobjdir, relpath) + return LANGUAGE_SETTINGS_TEMPLATE_DIR_INCLUDE.replace("@INCLUDE_PATH@", p) + + def add_define(name, value): + define = LANGUAGE_SETTINGS_TEMPLATE_DIR_DEFINE + define = define.replace("@NAME@", name) + # We use quoteattr here because some defines contain characters + # such as "<" and '"' which need proper XML escaping. + define = define.replace("@VALUE@", quoteattr(value)) + return define + + fh.write(LANGUAGE_SETTINGS_TEMPLATE_HEADER) + + # Unfortunately, whenever we set a user defined include path or define + # on a directory, Eclipse ignores user defined include paths and defines + # on ancestor directories. That means that we need to add all the + # common include paths and defines to every single directory entry that + # we add settings for. (Fortunately that doesn't appear to have a + # noticeable impact on the time it takes to open the generated Eclipse + # project.) We do that by generating a template here that we can then + # use for each individual directory in the loop below. + # + dirsettings_template = LANGUAGE_SETTINGS_TEMPLATE_DIR_HEADER + + # Add OS_COMPILE_CXXFLAGS args (same as OS_COMPILE_CFLAGS): + dirsettings_template = dirsettings_template.replace( + "@PREINCLUDE_FILE_PATH@", + os.path.join(self.environment.topobjdir, "dist/include/mozilla-config.h"), + ) + dirsettings_template += add_define("MOZILLA_CLIENT", "1") + + # Add EXTRA_INCLUDES args: + dirsettings_template += add_objdir_include_path("dist/include") + + # Add OS_INCLUDES args: + # XXX media/webrtc/trunk/webrtc's moz.builds reset this. + dirsettings_template += add_objdir_include_path("dist/include/nspr") + dirsettings_template += add_objdir_include_path("dist/include/nss") + + # Finally, add anything else that makes things work better. + # + # Because of https://developer.mozilla.org/en-US/docs/Eclipse_CDT#Headers_are_only_parsed_once + # we set MOZILLA_INTERNAL_API for all directories to make sure + # headers are indexed with MOZILLA_INTERNAL_API set. Unfortunately + # this means that MOZILLA_EXTERNAL_API code will suffer. + # + # TODO: If we're doing this for MOZILLA_EXTERNAL_API then we may want + # to do it for other LIBRARY_DEFINES's defines too. Well, at least for + # STATIC_EXPORTABLE_JS_API which may be important to JS people. + # (The other two LIBRARY_DEFINES defines -- MOZ_HAS_MOZGLUE and + # IMPL_LIBXUL -- don't affect much and probably don't matter to anyone). + # + # TODO: Should we also always set DEBUG so that DEBUG code is always + # indexed? Or is there significant amounts of non-DEBUG code that + # would be adversely affected? + # + # TODO: Investigate whether the ordering of directories in the project + # file can be used to our advantage so that the first indexing of + # important headers has the defines we want. + # + dirsettings_template += add_objdir_include_path("ipc/ipdl/_ipdlheaders") + dirsettings_template += add_define("MOZILLA_INTERNAL_API", "1") + + for path, args in self._args_for_dirs.items(): + dirsettings = dirsettings_template + dirsettings = dirsettings.replace("@RELATIVE_PATH@", path) + for i in args["includes"]: + dirsettings += add_abs_include_path(i) + for d in args["defines"]: + assert d[:2] == "-D" or d[:2] == "-U" + if d[:2] == "-U": + # gfx/harfbuzz/src uses -UDEBUG, at least on Mac + # netwerk/sctp/src uses -U__APPLE__ on Mac + # XXX We should make this code smart enough to remove existing defines. + continue + d = d[2:] # get rid of leading "-D" + name_value = d.split("=", 1) + name = name_value[0] + value = "" + if len(name_value) == 2: + value = name_value[1] + dirsettings += add_define(name, str(value)) + dirsettings += LANGUAGE_SETTINGS_TEMPLATE_DIR_FOOTER + fh.write(dirsettings) + + fh.write( + LANGUAGE_SETTINGS_TEMPLATE_FOOTER.replace( + "@COMPILER_FLAGS@", self._cxx + " " + self._cppflags + ) + ) + + def _write_launch_files(self, launch_dir): + bin_dir = os.path.join(self.environment.topobjdir, "dist") + + # TODO Improve binary detection + if self._macbundle: + exe_path = os.path.join(bin_dir, self._macbundle, "Contents/MacOS") + else: + exe_path = os.path.join(bin_dir, "bin") + + exe_path = os.path.join(exe_path, self._appname + self._bin_suffix) + + main_gecko_launch = os.path.join(launch_dir, "gecko.launch") + with open(main_gecko_launch, "w") as fh: + launch = GECKO_LAUNCH_CONFIG_TEMPLATE + launch = launch.replace("@LAUNCH_PROGRAM@", exe_path) + launch = launch.replace("@LAUNCH_ARGS@", "-P -no-remote") + fh.write(launch) + + # TODO Add more launch configs (and delegate calls to mach) + + def _write_project(self, fh): + project = PROJECT_TEMPLATE + + project = project.replace("@PROJECT_NAME@", self._project_name) + project = project.replace("@PROJECT_TOPSRCDIR@", self.environment.topsrcdir) + project = project.replace( + "@GENERATED_IPDL_FILES@", + os.path.join(self.environment.topobjdir, "ipc", "ipdl"), + ) + project = project.replace( + "@GENERATED_WEBIDL_FILES@", + os.path.join(self.environment.topobjdir, "dom", "bindings"), + ) + fh.write(project) + + def _write_cproject(self, fh): + cproject_header = CPROJECT_TEMPLATE_HEADER + cproject_header = cproject_header.replace( + "@PROJECT_TOPSRCDIR@", self.environment.topobjdir + ) + cproject_header = cproject_header.replace( + "@MACH_COMMAND@", os.path.join(self.environment.topsrcdir, "mach") + ) + fh.write(cproject_header) + fh.write(CPROJECT_TEMPLATE_FOOTER) + + +PROJECT_TEMPLATE = """<?xml version="1.0" encoding="UTF-8"?> +<projectDescription> + <name>@PROJECT_NAME@</name> + <comment></comment> + <projects> + </projects> + <buildSpec> + <buildCommand> + <name>org.eclipse.cdt.managedbuilder.core.genmakebuilder</name> + <triggers>clean,full,incremental,</triggers> + <arguments> + </arguments> + </buildCommand> + <buildCommand> + <name>org.eclipse.cdt.managedbuilder.core.ScannerConfigBuilder</name> + <triggers></triggers> + <arguments> + </arguments> + </buildCommand> + </buildSpec> + <natures> + <nature>org.eclipse.cdt.core.cnature</nature> + <nature>org.eclipse.cdt.core.ccnature</nature> + <nature>org.eclipse.cdt.managedbuilder.core.managedBuildNature</nature> + <nature>org.eclipse.cdt.managedbuilder.core.ScannerConfigNature</nature> + </natures> + <linkedResources> + <link> + <name>tree</name> + <type>2</type> + <location>@PROJECT_TOPSRCDIR@</location> + </link> + <link> + <name>generated-ipdl</name> + <type>2</type> + <location>@GENERATED_IPDL_FILES@</location> + </link> + <link> + <name>generated-webidl</name> + <type>2</type> + <location>@GENERATED_WEBIDL_FILES@</location> + </link> + </linkedResources> + <filteredResources> + <filter> + <id>17111971</id> + <name>tree</name> + <type>30</type> + <matcher> + <id>org.eclipse.ui.ide.multiFilter</id> + <arguments>1.0-name-matches-false-false-obj-*</arguments> + </matcher> + </filter> + <filter> + <id>14081994</id> + <name>tree</name> + <type>22</type> + <matcher> + <id>org.eclipse.ui.ide.multiFilter</id> + <arguments>1.0-name-matches-false-false-*.rej</arguments> + </matcher> + </filter> + <filter> + <id>25121970</id> + <name>tree</name> + <type>22</type> + <matcher> + <id>org.eclipse.ui.ide.multiFilter</id> + <arguments>1.0-name-matches-false-false-*.orig</arguments> + </matcher> + </filter> + <filter> + <id>10102004</id> + <name>tree</name> + <type>10</type> + <matcher> + <id>org.eclipse.ui.ide.multiFilter</id> + <arguments>1.0-name-matches-false-false-.hg</arguments> + </matcher> + </filter> + <filter> + <id>23122002</id> + <name>tree</name> + <type>22</type> + <matcher> + <id>org.eclipse.ui.ide.multiFilter</id> + <arguments>1.0-name-matches-false-false-*.pyc</arguments> + </matcher> + </filter> + </filteredResources> +</projectDescription> +""" + +CPROJECT_TEMPLATE_HEADER = """<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<?fileVersion 4.0.0?> + +<cproject storage_type_id="org.eclipse.cdt.core.XmlProjectDescriptionStorage"> + <storageModule moduleId="org.eclipse.cdt.core.settings"> + <cconfiguration id="0.1674256904"> + <storageModule buildSystemId="org.eclipse.cdt.managedbuilder.core.configurationDataProvider" id="0.1674256904" moduleId="org.eclipse.cdt.core.settings" name="Default"> + <externalSettings/> + <extensions> + <extension id="org.eclipse.cdt.core.VCErrorParser" point="org.eclipse.cdt.core.ErrorParser"/> + <extension id="org.eclipse.cdt.core.GmakeErrorParser" point="org.eclipse.cdt.core.ErrorParser"/> + <extension id="org.eclipse.cdt.core.CWDLocator" point="org.eclipse.cdt.core.ErrorParser"/> + <extension id="org.eclipse.cdt.core.GCCErrorParser" point="org.eclipse.cdt.core.ErrorParser"/> + <extension id="org.eclipse.cdt.core.GASErrorParser" point="org.eclipse.cdt.core.ErrorParser"/> + <extension id="org.eclipse.cdt.core.GLDErrorParser" point="org.eclipse.cdt.core.ErrorParser"/> + </extensions> + </storageModule> + <storageModule moduleId="cdtBuildSystem" version="4.0.0"> + <configuration artifactName="${ProjName}" buildProperties="" description="" id="0.1674256904" name="Default" parent="org.eclipse.cdt.build.core.prefbase.cfg"> + <folderInfo id="0.1674256904." name="/" resourcePath=""> + <toolChain id="cdt.managedbuild.toolchain.gnu.cross.exe.debug.1276586933" name="Cross GCC" superClass="cdt.managedbuild.toolchain.gnu.cross.exe.debug"> + <targetPlatform archList="all" binaryParser="" id="cdt.managedbuild.targetPlatform.gnu.cross.710759961" isAbstract="false" osList="all" superClass="cdt.managedbuild.targetPlatform.gnu.cross"/> + <builder arguments="--log-no-times build" buildPath="@PROJECT_TOPSRCDIR@" command="@MACH_COMMAND@" enableCleanBuild="false" incrementalBuildTarget="binaries" id="org.eclipse.cdt.build.core.settings.default.builder.1437267827" keepEnvironmentInBuildfile="false" name="Gnu Make Builder" superClass="org.eclipse.cdt.build.core.settings.default.builder"/> + </toolChain> + </folderInfo> +""" +CPROJECT_TEMPLATE_FILEINFO = """ <fileInfo id="0.1674256904.474736658" name="Layers.cpp" rcbsApplicability="disable" resourcePath="tree/gfx/layers/Layers.cpp" toolsToInvoke="org.eclipse.cdt.build.core.settings.holder.582514939.463639939"> + <tool id="org.eclipse.cdt.build.core.settings.holder.582514939.463639939" name="GNU C++" superClass="org.eclipse.cdt.build.core.settings.holder.582514939"> + <option id="org.eclipse.cdt.build.core.settings.holder.symbols.232300236" superClass="org.eclipse.cdt.build.core.settings.holder.symbols" valueType="definedSymbols"> + <listOptionValue builtIn="false" value="BENWA=BENWAVAL"/> + </option> + <inputType id="org.eclipse.cdt.build.core.settings.holder.inType.1942876228" languageId="org.eclipse.cdt.core.g++" languageName="GNU C++" sourceContentType="org.eclipse.cdt.core.cxxSource,org.eclipse.cdt.core.cxxHeader" superClass="org.eclipse.cdt.build.core.settings.holder.inType"/> + </tool> + </fileInfo> +""" +CPROJECT_TEMPLATE_FOOTER = """ + <sourceEntries> + <entry excluding="**/lib*|**/third_party/|tree/*.xcodeproj/|tree/.cargo/|tree/.vscode/|tree/build/|tree/extensions/|tree/gfx/angle/|tree/gfx/cairo/|tree/gfx/skia/skia/|tree/intl/icu/|tree/js/|tree/media/|tree/modules/freetype2|tree/modules/pdfium/|tree/netwerk/|tree/netwerk/sctp|tree/netwerk/srtp|tree/nsprpub/lib|tree/nsprpub/pr/src|tree/other-licenses/|tree/parser/|tree/python/|tree/security/nss/|tree/tools/" flags="VALUE_WORKSPACE_PATH" kind="sourcePath" name=""/> + </sourceEntries> + </configuration> + </storageModule> + <storageModule moduleId="org.eclipse.cdt.core.externalSettings"/> + </cconfiguration> + </storageModule> + <storageModule moduleId="cdtBuildSystem" version="4.0.0"> + <project id="Empty.null.1281234804" name="Empty"/> + </storageModule> + <storageModule moduleId="scannerConfiguration"> + <autodiscovery enabled="true" problemReportingEnabled="true" selectedProfileId=""/> + <scannerConfigBuildInfo instanceId="0.1674256904"> + <autodiscovery enabled="true" problemReportingEnabled="true" selectedProfileId=""/> + </scannerConfigBuildInfo> + </storageModule> + <storageModule moduleId="refreshScope" versionNumber="2"> + <configuration configurationName="Default"/> + </storageModule> + <storageModule moduleId="org.eclipse.cdt.core.LanguageSettingsProviders"/> +</cproject> +""" + +WORKSPACE_LANGUAGE_SETTINGS_TEMPLATE = """<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<plugin> + <extension point="org.eclipse.cdt.core.LanguageSettingsProvider"> + <provider class="org.eclipse.cdt.managedbuilder.language.settings.providers.GCCBuiltinSpecsDetector" console="true" id="org.eclipse.cdt.managedbuilder.core.GCCBuiltinSpecsDetector" keep-relative-paths="false" name="CDT GCC Built-in Compiler Settings" parameter="@COMPILER_FLAGS@ -E -P -v -dD "${INPUTS}""> + <language-scope id="org.eclipse.cdt.core.gcc"/> + <language-scope id="org.eclipse.cdt.core.g++"/> + </provider> + </extension> +</plugin> +""" + + +# The settings set via this template can be found in the UI by opening +# the Properties for a directory in the Project Explorer tab, then going to +# C/C++ General > Preprocessor Include Paths, Macros, etc., selecting the +# C++ item from the Languages column, and then expanding the +# CDT User Settings Entries item to the right. + +LANGUAGE_SETTINGS_TEMPLATE_HEADER = """<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<project> + <configuration id="0.1674256904" name="Default"> + <extension point="org.eclipse.cdt.core.LanguageSettingsProvider"> + <provider class="org.eclipse.cdt.core.language.settings.providers.LanguageSettingsGenericProvider" id="org.eclipse.cdt.ui.UserLanguageSettingsProvider" name="CDT User Setting Entries" prefer-non-shared="true" store-entries-with-project="true"> + <language id="org.eclipse.cdt.core.g++"> +""" + +LANGUAGE_SETTINGS_TEMPLATE_DIR_HEADER = """ <resource project-relative-path="@RELATIVE_PATH@"> + <entry kind="includeFile" name="@PREINCLUDE_FILE_PATH@"> + <flag value="LOCAL"/> + </entry> +""" + +LANGUAGE_SETTINGS_TEMPLATE_DIR_INCLUDE = """ <entry kind="includePath" name="@INCLUDE_PATH@"> + <flag value="LOCAL"/> + </entry> +""" + +LANGUAGE_SETTINGS_TEMPLATE_DIR_DEFINE = """ <entry kind="macro" name="@NAME@" value=@VALUE@/> +""" + +LANGUAGE_SETTINGS_TEMPLATE_DIR_FOOTER = """ </resource> +""" + +LANGUAGE_SETTINGS_TEMPLATE_FOOTER = """ </language> + </provider> + <provider class="org.eclipse.cdt.internal.build.crossgcc.CrossGCCBuiltinSpecsDetector" console="false" env-hash="-859273372804152468" id="org.eclipse.cdt.build.crossgcc.CrossGCCBuiltinSpecsDetector" keep-relative-paths="false" name="CDT Cross GCC Built-in Compiler Settings" parameter="@COMPILER_FLAGS@ -E -P -v -dD "${INPUTS}" -std=c++11" prefer-non-shared="true" store-entries-with-project="true"> + <language-scope id="org.eclipse.cdt.core.gcc"/> + <language-scope id="org.eclipse.cdt.core.g++"/> + </provider> + <provider-reference id="org.eclipse.cdt.managedbuilder.core.MBSLanguageSettingsProvider" ref="shared-provider"/> + </extension> + </configuration> +</project> +""" + + +GECKO_LAUNCH_CONFIG_TEMPLATE = """<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<launchConfiguration type="org.eclipse.cdt.launch.applicationLaunchType"> +<booleanAttribute key="org.eclipse.cdt.dsf.gdb.AUTO_SOLIB" value="true"/> +<listAttribute key="org.eclipse.cdt.dsf.gdb.AUTO_SOLIB_LIST"/> +<stringAttribute key="org.eclipse.cdt.dsf.gdb.DEBUG_NAME" value="lldb"/> +<booleanAttribute key="org.eclipse.cdt.dsf.gdb.DEBUG_ON_FORK" value="false"/> +<stringAttribute key="org.eclipse.cdt.dsf.gdb.GDB_INIT" value=""/> +<booleanAttribute key="org.eclipse.cdt.dsf.gdb.NON_STOP" value="false"/> +<booleanAttribute key="org.eclipse.cdt.dsf.gdb.REVERSE" value="false"/> +<listAttribute key="org.eclipse.cdt.dsf.gdb.SOLIB_PATH"/> +<stringAttribute key="org.eclipse.cdt.dsf.gdb.TRACEPOINT_MODE" value="TP_NORMAL_ONLY"/> +<booleanAttribute key="org.eclipse.cdt.dsf.gdb.UPDATE_THREADLIST_ON_SUSPEND" value="false"/> +<booleanAttribute key="org.eclipse.cdt.dsf.gdb.internal.ui.launching.LocalApplicationCDebuggerTab.DEFAULTS_SET" value="true"/> +<intAttribute key="org.eclipse.cdt.launch.ATTR_BUILD_BEFORE_LAUNCH_ATTR" value="2"/> +<stringAttribute key="org.eclipse.cdt.launch.COREFILE_PATH" value=""/> +<stringAttribute key="org.eclipse.cdt.launch.DEBUGGER_ID" value="gdb"/> +<stringAttribute key="org.eclipse.cdt.launch.DEBUGGER_START_MODE" value="run"/> +<booleanAttribute key="org.eclipse.cdt.launch.DEBUGGER_STOP_AT_MAIN" value="false"/> +<stringAttribute key="org.eclipse.cdt.launch.DEBUGGER_STOP_AT_MAIN_SYMBOL" value="main"/> +<stringAttribute key="org.eclipse.cdt.launch.PROGRAM_ARGUMENTS" value="@LAUNCH_ARGS@"/> +<stringAttribute key="org.eclipse.cdt.launch.PROGRAM_NAME" value="@LAUNCH_PROGRAM@"/> +<stringAttribute key="org.eclipse.cdt.launch.PROJECT_ATTR" value="Gecko"/> +<booleanAttribute key="org.eclipse.cdt.launch.PROJECT_BUILD_CONFIG_AUTO_ATTR" value="true"/> +<stringAttribute key="org.eclipse.cdt.launch.PROJECT_BUILD_CONFIG_ID_ATTR" value=""/> +<booleanAttribute key="org.eclipse.cdt.launch.use_terminal" value="true"/> +<listAttribute key="org.eclipse.debug.core.MAPPED_RESOURCE_PATHS"> +<listEntry value="/gecko"/> +</listAttribute> +<listAttribute key="org.eclipse.debug.core.MAPPED_RESOURCE_TYPES"> +<listEntry value="4"/> +</listAttribute> +<booleanAttribute key="org.eclipse.debug.ui.ATTR_LAUNCH_IN_BACKGROUND" value="false"/> +<stringAttribute key="process_factory_id" value="org.eclipse.cdt.dsf.gdb.GdbProcessFactory"/> +</launchConfiguration> +""" + + +EDITOR_SETTINGS = """eclipse.preferences.version=1 +lineNumberRuler=true +overviewRuler_migration=migrated_3.1 +printMargin=true +printMarginColumn=80 +showCarriageReturn=false +showEnclosedSpaces=false +showLeadingSpaces=false +showLineFeed=false +showWhitespaceCharacters=true +spacesForTabs=true +tabWidth=2 +undoHistorySize=200 +""" + + +STATIC_CORE_RESOURCES_PREFS = """eclipse.preferences.version=1 +refresh.enabled=true +""" + +STATIC_CORE_RUNTIME_PREFS = """eclipse.preferences.version=1 +content-types/org.eclipse.cdt.core.cxxSource/file-extensions=mm +content-types/org.eclipse.core.runtime.xml/file-extensions=xul +content-types/org.eclipse.wst.jsdt.core.jsSource/file-extensions=jsm +""" + +STATIC_UI_PREFS = """eclipse.preferences.version=1 +showIntro=false +""" + +STATIC_CDT_CORE_PREFS = """eclipse.preferences.version=1 +indexer.updatePolicy=0 +""" + +FORMATTER_SETTINGS = """org.eclipse.cdt.core.formatter.alignment_for_arguments_in_method_invocation=16 +org.eclipse.cdt.core.formatter.alignment_for_assignment=16 +org.eclipse.cdt.core.formatter.alignment_for_base_clause_in_type_declaration=80 +org.eclipse.cdt.core.formatter.alignment_for_binary_expression=16 +org.eclipse.cdt.core.formatter.alignment_for_compact_if=16 +org.eclipse.cdt.core.formatter.alignment_for_conditional_expression=34 +org.eclipse.cdt.core.formatter.alignment_for_conditional_expression_chain=18 +org.eclipse.cdt.core.formatter.alignment_for_constructor_initializer_list=48 +org.eclipse.cdt.core.formatter.alignment_for_declarator_list=16 +org.eclipse.cdt.core.formatter.alignment_for_enumerator_list=48 +org.eclipse.cdt.core.formatter.alignment_for_expression_list=0 +org.eclipse.cdt.core.formatter.alignment_for_expressions_in_array_initializer=16 +org.eclipse.cdt.core.formatter.alignment_for_member_access=0 +org.eclipse.cdt.core.formatter.alignment_for_overloaded_left_shift_chain=16 +org.eclipse.cdt.core.formatter.alignment_for_parameters_in_method_declaration=16 +org.eclipse.cdt.core.formatter.alignment_for_throws_clause_in_method_declaration=16 +org.eclipse.cdt.core.formatter.brace_position_for_array_initializer=end_of_line +org.eclipse.cdt.core.formatter.brace_position_for_block=end_of_line +org.eclipse.cdt.core.formatter.brace_position_for_block_in_case=next_line_shifted +org.eclipse.cdt.core.formatter.brace_position_for_method_declaration=next_line +org.eclipse.cdt.core.formatter.brace_position_for_namespace_declaration=end_of_line +org.eclipse.cdt.core.formatter.brace_position_for_switch=end_of_line +org.eclipse.cdt.core.formatter.brace_position_for_type_declaration=next_line +org.eclipse.cdt.core.formatter.comment.min_distance_between_code_and_line_comment=1 +org.eclipse.cdt.core.formatter.comment.never_indent_line_comments_on_first_column=true +org.eclipse.cdt.core.formatter.comment.preserve_white_space_between_code_and_line_comments=true +org.eclipse.cdt.core.formatter.compact_else_if=true +org.eclipse.cdt.core.formatter.continuation_indentation=2 +org.eclipse.cdt.core.formatter.continuation_indentation_for_array_initializer=2 +org.eclipse.cdt.core.formatter.format_guardian_clause_on_one_line=false +org.eclipse.cdt.core.formatter.indent_access_specifier_compare_to_type_header=false +org.eclipse.cdt.core.formatter.indent_access_specifier_extra_spaces=0 +org.eclipse.cdt.core.formatter.indent_body_declarations_compare_to_access_specifier=true +org.eclipse.cdt.core.formatter.indent_body_declarations_compare_to_namespace_header=false +org.eclipse.cdt.core.formatter.indent_breaks_compare_to_cases=true +org.eclipse.cdt.core.formatter.indent_declaration_compare_to_template_header=true +org.eclipse.cdt.core.formatter.indent_empty_lines=false +org.eclipse.cdt.core.formatter.indent_statements_compare_to_block=true +org.eclipse.cdt.core.formatter.indent_statements_compare_to_body=true +org.eclipse.cdt.core.formatter.indent_switchstatements_compare_to_cases=true +org.eclipse.cdt.core.formatter.indent_switchstatements_compare_to_switch=false +org.eclipse.cdt.core.formatter.indentation.size=2 +org.eclipse.cdt.core.formatter.insert_new_line_after_opening_brace_in_array_initializer=do not insert +org.eclipse.cdt.core.formatter.insert_new_line_after_template_declaration=insert +org.eclipse.cdt.core.formatter.insert_new_line_at_end_of_file_if_missing=do not insert +org.eclipse.cdt.core.formatter.insert_new_line_before_catch_in_try_statement=do not insert +org.eclipse.cdt.core.formatter.insert_new_line_before_closing_brace_in_array_initializer=do not insert +org.eclipse.cdt.core.formatter.insert_new_line_before_colon_in_constructor_initializer_list=do not insert +org.eclipse.cdt.core.formatter.insert_new_line_before_else_in_if_statement=do not insert +org.eclipse.cdt.core.formatter.insert_new_line_before_identifier_in_function_declaration=insert +org.eclipse.cdt.core.formatter.insert_new_line_before_while_in_do_statement=do not insert +org.eclipse.cdt.core.formatter.insert_new_line_in_empty_block=insert +org.eclipse.cdt.core.formatter.insert_space_after_assignment_operator=insert +org.eclipse.cdt.core.formatter.insert_space_after_binary_operator=insert +org.eclipse.cdt.core.formatter.insert_space_after_closing_angle_bracket_in_template_arguments=insert +org.eclipse.cdt.core.formatter.insert_space_after_closing_angle_bracket_in_template_parameters=insert +org.eclipse.cdt.core.formatter.insert_space_after_closing_brace_in_block=insert +org.eclipse.cdt.core.formatter.insert_space_after_closing_paren_in_cast=insert +org.eclipse.cdt.core.formatter.insert_space_after_colon_in_base_clause=insert +org.eclipse.cdt.core.formatter.insert_space_after_colon_in_case=insert +org.eclipse.cdt.core.formatter.insert_space_after_colon_in_conditional=insert +org.eclipse.cdt.core.formatter.insert_space_after_colon_in_labeled_statement=insert +org.eclipse.cdt.core.formatter.insert_space_after_comma_in_array_initializer=insert +org.eclipse.cdt.core.formatter.insert_space_after_comma_in_base_types=insert +org.eclipse.cdt.core.formatter.insert_space_after_comma_in_declarator_list=insert +org.eclipse.cdt.core.formatter.insert_space_after_comma_in_enum_declarations=insert +org.eclipse.cdt.core.formatter.insert_space_after_comma_in_expression_list=insert +org.eclipse.cdt.core.formatter.insert_space_after_comma_in_method_declaration_parameters=insert +org.eclipse.cdt.core.formatter.insert_space_after_comma_in_method_declaration_throws=insert +org.eclipse.cdt.core.formatter.insert_space_after_comma_in_method_invocation_arguments=insert +org.eclipse.cdt.core.formatter.insert_space_after_comma_in_template_arguments=insert +org.eclipse.cdt.core.formatter.insert_space_after_comma_in_template_parameters=insert +org.eclipse.cdt.core.formatter.insert_space_after_opening_angle_bracket_in_template_arguments=do not insert +org.eclipse.cdt.core.formatter.insert_space_after_opening_angle_bracket_in_template_parameters=do not insert +org.eclipse.cdt.core.formatter.insert_space_after_opening_brace_in_array_initializer=insert +org.eclipse.cdt.core.formatter.insert_space_after_opening_bracket=do not insert +org.eclipse.cdt.core.formatter.insert_space_after_opening_paren_in_cast=do not insert +org.eclipse.cdt.core.formatter.insert_space_after_opening_paren_in_catch=do not insert +org.eclipse.cdt.core.formatter.insert_space_after_opening_paren_in_exception_specification=do not insert +org.eclipse.cdt.core.formatter.insert_space_after_opening_paren_in_for=do not insert +org.eclipse.cdt.core.formatter.insert_space_after_opening_paren_in_if=do not insert +org.eclipse.cdt.core.formatter.insert_space_after_opening_paren_in_method_declaration=do not insert +org.eclipse.cdt.core.formatter.insert_space_after_opening_paren_in_method_invocation=do not insert +org.eclipse.cdt.core.formatter.insert_space_after_opening_paren_in_parenthesized_expression=do not insert +org.eclipse.cdt.core.formatter.insert_space_after_opening_paren_in_switch=do not insert +org.eclipse.cdt.core.formatter.insert_space_after_opening_paren_in_while=do not insert +org.eclipse.cdt.core.formatter.insert_space_after_postfix_operator=do not insert +org.eclipse.cdt.core.formatter.insert_space_after_prefix_operator=do not insert +org.eclipse.cdt.core.formatter.insert_space_after_question_in_conditional=insert +org.eclipse.cdt.core.formatter.insert_space_after_semicolon_in_for=insert +org.eclipse.cdt.core.formatter.insert_space_after_unary_operator=do not insert +org.eclipse.cdt.core.formatter.insert_space_before_assignment_operator=insert +org.eclipse.cdt.core.formatter.insert_space_before_binary_operator=insert +org.eclipse.cdt.core.formatter.insert_space_before_closing_angle_bracket_in_template_arguments=do not insert +org.eclipse.cdt.core.formatter.insert_space_before_closing_angle_bracket_in_template_parameters=do not insert +org.eclipse.cdt.core.formatter.insert_space_before_closing_brace_in_array_initializer=insert +org.eclipse.cdt.core.formatter.insert_space_before_closing_bracket=do not insert +org.eclipse.cdt.core.formatter.insert_space_before_closing_paren_in_cast=do not insert +org.eclipse.cdt.core.formatter.insert_space_before_closing_paren_in_catch=do not insert +org.eclipse.cdt.core.formatter.insert_space_before_closing_paren_in_exception_specification=do not insert +org.eclipse.cdt.core.formatter.insert_space_before_closing_paren_in_for=do not insert +org.eclipse.cdt.core.formatter.insert_space_before_closing_paren_in_if=do not insert +org.eclipse.cdt.core.formatter.insert_space_before_closing_paren_in_method_declaration=do not insert +org.eclipse.cdt.core.formatter.insert_space_before_closing_paren_in_method_invocation=do not insert +org.eclipse.cdt.core.formatter.insert_space_before_closing_paren_in_parenthesized_expression=do not insert +org.eclipse.cdt.core.formatter.insert_space_before_closing_paren_in_switch=do not insert +org.eclipse.cdt.core.formatter.insert_space_before_closing_paren_in_while=do not insert +org.eclipse.cdt.core.formatter.insert_space_before_colon_in_base_clause=insert +org.eclipse.cdt.core.formatter.insert_space_before_colon_in_case=do not insert +org.eclipse.cdt.core.formatter.insert_space_before_colon_in_conditional=insert +org.eclipse.cdt.core.formatter.insert_space_before_colon_in_default=do not insert +org.eclipse.cdt.core.formatter.insert_space_before_colon_in_labeled_statement=do not insert +org.eclipse.cdt.core.formatter.insert_space_before_comma_in_array_initializer=do not insert +org.eclipse.cdt.core.formatter.insert_space_before_comma_in_base_types=do not insert +org.eclipse.cdt.core.formatter.insert_space_before_comma_in_declarator_list=do not insert +org.eclipse.cdt.core.formatter.insert_space_before_comma_in_enum_declarations=do not insert +org.eclipse.cdt.core.formatter.insert_space_before_comma_in_expression_list=do not insert +org.eclipse.cdt.core.formatter.insert_space_before_comma_in_method_declaration_parameters=do not insert +org.eclipse.cdt.core.formatter.insert_space_before_comma_in_method_declaration_throws=do not insert +org.eclipse.cdt.core.formatter.insert_space_before_comma_in_method_invocation_arguments=do not insert +org.eclipse.cdt.core.formatter.insert_space_before_comma_in_template_arguments=do not insert +org.eclipse.cdt.core.formatter.insert_space_before_comma_in_template_parameters=do not insert +org.eclipse.cdt.core.formatter.insert_space_before_opening_angle_bracket_in_template_arguments=do not insert +org.eclipse.cdt.core.formatter.insert_space_before_opening_angle_bracket_in_template_parameters=do not insert +org.eclipse.cdt.core.formatter.insert_space_before_opening_brace_in_array_initializer=insert +org.eclipse.cdt.core.formatter.insert_space_before_opening_brace_in_block=insert +org.eclipse.cdt.core.formatter.insert_space_before_opening_brace_in_method_declaration=insert +org.eclipse.cdt.core.formatter.insert_space_before_opening_brace_in_namespace_declaration=insert +org.eclipse.cdt.core.formatter.insert_space_before_opening_brace_in_switch=insert +org.eclipse.cdt.core.formatter.insert_space_before_opening_brace_in_type_declaration=insert +org.eclipse.cdt.core.formatter.insert_space_before_opening_bracket=do not insert +org.eclipse.cdt.core.formatter.insert_space_before_opening_paren_in_catch=insert +org.eclipse.cdt.core.formatter.insert_space_before_opening_paren_in_exception_specification=insert +org.eclipse.cdt.core.formatter.insert_space_before_opening_paren_in_for=insert +org.eclipse.cdt.core.formatter.insert_space_before_opening_paren_in_if=insert +org.eclipse.cdt.core.formatter.insert_space_before_opening_paren_in_method_declaration=do not insert +org.eclipse.cdt.core.formatter.insert_space_before_opening_paren_in_method_invocation=do not insert +org.eclipse.cdt.core.formatter.insert_space_before_opening_paren_in_parenthesized_expression=do not insert +org.eclipse.cdt.core.formatter.insert_space_before_opening_paren_in_switch=insert +org.eclipse.cdt.core.formatter.insert_space_before_opening_paren_in_while=insert +org.eclipse.cdt.core.formatter.insert_space_before_postfix_operator=do not insert +org.eclipse.cdt.core.formatter.insert_space_before_prefix_operator=do not insert +org.eclipse.cdt.core.formatter.insert_space_before_question_in_conditional=insert +org.eclipse.cdt.core.formatter.insert_space_before_semicolon=do not insert +org.eclipse.cdt.core.formatter.insert_space_before_semicolon_in_for=do not insert +org.eclipse.cdt.core.formatter.insert_space_before_unary_operator=do not insert +org.eclipse.cdt.core.formatter.insert_space_between_empty_braces_in_array_initializer=do not insert +org.eclipse.cdt.core.formatter.insert_space_between_empty_brackets=do not insert +org.eclipse.cdt.core.formatter.insert_space_between_empty_parens_in_exception_specification=do not insert +org.eclipse.cdt.core.formatter.insert_space_between_empty_parens_in_method_declaration=do not insert +org.eclipse.cdt.core.formatter.insert_space_between_empty_parens_in_method_invocation=do not insert +org.eclipse.cdt.core.formatter.join_wrapped_lines=false +org.eclipse.cdt.core.formatter.keep_else_statement_on_same_line=false +org.eclipse.cdt.core.formatter.keep_empty_array_initializer_on_one_line=false +org.eclipse.cdt.core.formatter.keep_imple_if_on_one_line=false +org.eclipse.cdt.core.formatter.keep_then_statement_on_same_line=false +org.eclipse.cdt.core.formatter.lineSplit=80 +org.eclipse.cdt.core.formatter.number_of_empty_lines_to_preserve=1 +org.eclipse.cdt.core.formatter.put_empty_statement_on_new_line=true +org.eclipse.cdt.core.formatter.tabulation.char=space +org.eclipse.cdt.core.formatter.tabulation.size=2 +org.eclipse.cdt.core.formatter.use_tabs_only_for_leading_indentations=false +""" + +STATIC_CDT_UI_PREFS = """eclipse.preferences.version=1 +buildConsoleLines=10000 +Console.limitConsoleOutput=false +ensureNewlineAtEOF=false +formatter_profile=_Mozilla +formatter_settings_version=1 +org.eclipse.cdt.ui.formatterprofiles.version=1 +removeTrailingWhitespace=true +removeTrailingWhitespaceEditedLines=true +scalability.numberOfLines=15000 +markOccurrences=true +markOverloadedOperatorsOccurrences=true +stickyOccurrences=false +""" + +NOINDEX_TEMPLATE = """eclipse.preferences.version=1 +indexer/indexerId=org.eclipse.cdt.core.nullIndexer +""" diff --git a/python/mozbuild/mozbuild/backend/fastermake.py b/python/mozbuild/mozbuild/backend/fastermake.py new file mode 100644 index 0000000000..c423e00c32 --- /dev/null +++ b/python/mozbuild/mozbuild/backend/fastermake.py @@ -0,0 +1,300 @@ +# 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 operator import itemgetter + +import mozpack.path as mozpath +import six +from mozpack.manifests import InstallManifest + +from mozbuild.backend.base import PartialBackend +from mozbuild.backend.make import MakeBackend +from mozbuild.frontend.context import ObjDirPath, Path +from mozbuild.frontend.data import ( + ChromeManifestEntry, + FinalTargetFiles, + FinalTargetPreprocessedFiles, + GeneratedFile, + JARManifest, + LocalizedFiles, + LocalizedPreprocessedFiles, + XPIDLModule, +) +from mozbuild.makeutil import Makefile +from mozbuild.util import OrderedDefaultDict + + +class FasterMakeBackend(MakeBackend, PartialBackend): + def _init(self): + super(FasterMakeBackend, self)._init() + + self._manifest_entries = OrderedDefaultDict(set) + + self._install_manifests = OrderedDefaultDict(InstallManifest) + + self._dependencies = OrderedDefaultDict(list) + self._l10n_dependencies = OrderedDefaultDict(list) + + self._has_xpidl = False + + self._generated_files_map = {} + self._generated_files = [] + + def _add_preprocess(self, obj, path, dest, target=None, **kwargs): + if target is None: + target = mozpath.basename(path) + # This matches what PP_TARGETS do in config/rules. + if target.endswith(".in"): + target = target[:-3] + if target.endswith(".css"): + kwargs["marker"] = "%" + depfile = mozpath.join( + self.environment.topobjdir, + "faster", + ".deps", + mozpath.join(obj.install_target, dest, target).replace("/", "_"), + ) + self._install_manifests[obj.install_target].add_preprocess( + mozpath.join(obj.srcdir, path), + mozpath.join(dest, target), + depfile, + **kwargs + ) + + def consume_object(self, obj): + if isinstance(obj, JARManifest) and obj.install_target.startswith("dist/bin"): + self._consume_jar_manifest(obj) + + elif isinstance( + obj, (FinalTargetFiles, FinalTargetPreprocessedFiles) + ) and obj.install_target.startswith("dist/bin"): + ab_cd = self.environment.substs["MOZ_UI_LOCALE"][0] + localized = isinstance(obj, (LocalizedFiles, LocalizedPreprocessedFiles)) + defines = obj.defines or {} + if defines: + defines = defines.defines + for path, files in obj.files.walk(): + for f in files: + # For localized files we need to find the file from the locale directory. + if localized and not isinstance(f, ObjDirPath) and ab_cd != "en-US": + src = self.localized_path(obj.relsrcdir, f) + + dep_target = "install-%s" % obj.install_target + + if "*" not in src: + merge = mozpath.abspath( + mozpath.join( + self.environment.topobjdir, + "l10n_merge", + obj.relsrcdir, + f, + ) + ) + self._l10n_dependencies[dep_target].append( + (merge, f.full_path, src) + ) + src = merge + else: + src = f.full_path + + if isinstance(obj, FinalTargetPreprocessedFiles): + self._add_preprocess( + obj, src, path, target=f.target_basename, defines=defines + ) + elif "*" in f: + + def _prefix(s): + for p in mozpath.split(s): + if "*" not in p: + yield p + "/" + + prefix = "".join(_prefix(src)) + + if "*" in f.target_basename: + target = path + else: + target = mozpath.join(path, f.target_basename) + self._install_manifests[obj.install_target].add_pattern_link( + prefix, src[len(prefix) :], target + ) + else: + self._install_manifests[obj.install_target].add_link( + src, mozpath.join(path, f.target_basename) + ) + if isinstance(f, ObjDirPath): + dep_target = "install-%s" % obj.install_target + dep = mozpath.relpath(f.full_path, self.environment.topobjdir) + if dep in self._generated_files_map: + # Only the first output file is specified as a + # dependency. If there are multiple output files + # from a single GENERATED_FILES invocation that are + # installed, we only want to run the command once. + dep = self._generated_files_map[dep] + self._dependencies[dep_target].append(dep) + + elif isinstance(obj, ChromeManifestEntry) and obj.install_target.startswith( + "dist/bin" + ): + top_level = mozpath.join(obj.install_target, "chrome.manifest") + if obj.path != top_level: + entry = "manifest %s" % mozpath.relpath(obj.path, obj.install_target) + self._manifest_entries[top_level].add(entry) + self._manifest_entries[obj.path].add(str(obj.entry)) + + elif isinstance(obj, GeneratedFile): + if obj.outputs: + first_output = mozpath.relpath( + mozpath.join(obj.objdir, obj.outputs[0]), self.environment.topobjdir + ) + for o in obj.outputs[1:]: + fullpath = mozpath.join(obj.objdir, o) + self._generated_files_map[ + mozpath.relpath(fullpath, self.environment.topobjdir) + ] = first_output + self._generated_files.append(obj) + return False + + elif isinstance(obj, XPIDLModule): + self._has_xpidl = True + # We're not actually handling XPIDL files. + return False + + else: + return False + + return True + + def consume_finished(self): + mk = Makefile() + # Add the default rule at the very beginning. + mk.create_rule(["default"]) + mk.add_statement("TOPSRCDIR = %s" % self.environment.topsrcdir) + mk.add_statement("TOPOBJDIR = %s" % self.environment.topobjdir) + mk.add_statement("MDDEPDIR = .deps") + mk.add_statement("TOUCH ?= touch") + mk.add_statement("include $(TOPSRCDIR)/config/makefiles/functions.mk") + mk.add_statement("include $(TOPSRCDIR)/config/AB_rCD.mk") + mk.add_statement("AB_CD = en-US") + if not self._has_xpidl: + mk.add_statement("NO_XPIDL = 1") + + # Add a few necessary variables inherited from configure + for var in ( + "PYTHON3", + "ACDEFINES", + "MOZ_BUILD_APP", + "MOZ_WIDGET_TOOLKIT", + ): + value = self.environment.substs.get(var) + if value is not None: + mk.add_statement("%s = %s" % (var, value)) + + install_manifests_bases = self._install_manifests.keys() + + # Add information for chrome manifest generation + manifest_targets = [] + + for target, entries in six.iteritems(self._manifest_entries): + manifest_targets.append(target) + install_target = mozpath.basedir(target, install_manifests_bases) + self._install_manifests[install_target].add_content( + "".join("%s\n" % e for e in sorted(entries)), + mozpath.relpath(target, install_target), + ) + + # Add information for install manifests. + mk.add_statement( + "INSTALL_MANIFESTS = %s" % " ".join(sorted(self._install_manifests.keys())) + ) + + # Add dependencies we inferred: + for target, deps in sorted(six.iteritems(self._dependencies)): + mk.create_rule([target]).add_dependencies( + "$(TOPOBJDIR)/%s" % d for d in sorted(deps) + ) + + # This is not great, but it's better to have some dependencies on these Python files. + python_deps = [ + "$(TOPSRCDIR)/python/mozbuild/mozbuild/action/l10n_merge.py", + "$(TOPSRCDIR)/third_party/python/compare-locales/compare_locales/compare.py", + "$(TOPSRCDIR)/third_party/python/compare-locales/compare_locales/paths.py", + ] + # Add l10n dependencies we inferred: + for target, deps in sorted(six.iteritems(self._l10n_dependencies)): + mk.create_rule([target]).add_dependencies( + "%s" % d[0] for d in sorted(deps, key=itemgetter(0)) + ) + for merge, ref_file, l10n_file in deps: + rule = mk.create_rule([merge]).add_dependencies( + [ref_file, l10n_file] + python_deps + ) + rule.add_commands( + [ + "$(PYTHON3) -m mozbuild.action.l10n_merge " + "--output {} --ref-file {} --l10n-file {}".format( + merge, ref_file, l10n_file + ) + ] + ) + # Add a dummy rule for the l10n file since it might not exist. + mk.create_rule([l10n_file]) + + mk.add_statement("include $(TOPSRCDIR)/config/faster/rules.mk") + + for base, install_manifest in six.iteritems(self._install_manifests): + with self._write_file( + mozpath.join( + self.environment.topobjdir, + "faster", + "install_%s" % base.replace("/", "_"), + ) + ) as fh: + install_manifest.write(fileobj=fh) + + # Write a single unified manifest for consumption by |mach watch|. + # Since this doesn't start 'install_', it's not processed by the build. + unified_manifest = InstallManifest() + for base, install_manifest in six.iteritems(self._install_manifests): + # Expect 'dist/bin/**', which includes 'dist/bin' with no trailing slash. + assert base.startswith("dist/bin") + base = base[len("dist/bin") :] + if base and base[0] == "/": + base = base[1:] + unified_manifest.add_entries_from(install_manifest, base=base) + + with self._write_file( + mozpath.join( + self.environment.topobjdir, "faster", "unified_install_dist_bin" + ) + ) as fh: + unified_manifest.write(fileobj=fh) + + for obj in self._generated_files: + for stmt in self._format_statements_for_generated_file(obj, "default"): + mk.add_statement(stmt) + + with self._write_file( + mozpath.join(self.environment.topobjdir, "faster", "Makefile") + ) as fh: + mk.dump(fh, removal_guard=False) + + def _pretty_path(self, path, obj): + if path.startswith(self.environment.topobjdir): + return mozpath.join( + "$(TOPOBJDIR)", mozpath.relpath(path, self.environment.topobjdir) + ) + elif path.startswith(self.environment.topsrcdir): + return mozpath.join( + "$(TOPSRCDIR)", mozpath.relpath(path, self.environment.topsrcdir) + ) + else: + return path + + def _format_generated_file_input_name(self, path, obj): + return self._pretty_path(path.full_path, obj) + + def _format_generated_file_output_name(self, path, obj): + if not isinstance(path, Path): + path = ObjDirPath(obj._context, "!" + path) + return self._pretty_path(path.full_path, obj) diff --git a/python/mozbuild/mozbuild/backend/mach_commands.py b/python/mozbuild/mozbuild/backend/mach_commands.py new file mode 100644 index 0000000000..3feedefc42 --- /dev/null +++ b/python/mozbuild/mozbuild/backend/mach_commands.py @@ -0,0 +1,422 @@ +# 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/. + +import argparse +import logging +import os +import subprocess +import sys + +import mozpack.path as mozpath +from mach.decorators import Command, CommandArgument +from mozfile import which + +from mozbuild import build_commands + + +@Command( + "ide", + category="devenv", + description="Generate a project and launch an IDE.", + virtualenv_name="build", +) +@CommandArgument("ide", choices=["eclipse", "visualstudio", "vscode"]) +@CommandArgument( + "--no-interactive", + default=False, + action="store_true", + help="Just generate the configuration", +) +@CommandArgument("args", nargs=argparse.REMAINDER) +def run(command_context, ide, no_interactive, args): + interactive = not no_interactive + + if ide == "eclipse": + backend = "CppEclipse" + elif ide == "visualstudio": + backend = "VisualStudio" + elif ide == "vscode": + backend = "Clangd" + + if ide == "eclipse" and not which("eclipse"): + command_context.log( + logging.ERROR, + "ide", + {}, + "Eclipse CDT 8.4 or later must be installed in your PATH.", + ) + command_context.log( + logging.ERROR, + "ide", + {}, + "Download: http://www.eclipse.org/cdt/downloads.php", + ) + return 1 + + if ide == "vscode": + rc = build_commands.configure(command_context) + + if rc != 0: + return rc + + # First install what we can through install manifests. + rc = command_context._run_make( + directory=command_context.topobjdir, + target="pre-export", + line_handler=None, + ) + if rc != 0: + return rc + + # Then build the rest of the build dependencies by running the full + # export target, because we can't do anything better. + for target in ("export", "pre-compile"): + rc = command_context._run_make( + directory=command_context.topobjdir, + target=target, + line_handler=None, + ) + if rc != 0: + return rc + else: + # Here we refresh the whole build. 'build export' is sufficient here and is + # probably more correct but it's also nice having a single target to get a fully + # built and indexed project (gives a easy target to use before go out to lunch). + res = command_context._mach_context.commands.dispatch( + "build", command_context._mach_context + ) + if res != 0: + return 1 + + # Generate or refresh the IDE backend. + python = command_context.virtualenv_manager.python_path + config_status = os.path.join(command_context.topobjdir, "config.status") + args = [python, config_status, "--backend=%s" % backend] + res = command_context._run_command_in_objdir( + args=args, pass_thru=True, ensure_exit_code=False + ) + if res != 0: + return 1 + + if ide == "eclipse": + eclipse_workspace_dir = get_eclipse_workspace_path(command_context) + subprocess.check_call(["eclipse", "-data", eclipse_workspace_dir]) + elif ide == "visualstudio": + visual_studio_workspace_dir = get_visualstudio_workspace_path(command_context) + subprocess.call(["explorer.exe", visual_studio_workspace_dir]) + elif ide == "vscode": + return setup_vscode(command_context, interactive) + + +def get_eclipse_workspace_path(command_context): + from mozbuild.backend.cpp_eclipse import CppEclipseBackend + + return CppEclipseBackend.get_workspace_path( + command_context.topsrcdir, command_context.topobjdir + ) + + +def get_visualstudio_workspace_path(command_context): + return os.path.normpath( + os.path.join(command_context.topobjdir, "msvc", "mozilla.sln") + ) + + +def setup_vscode(command_context, interactive): + from mozbuild.backend.clangd import find_vscode_cmd + + # Check if platform has VSCode installed + if interactive: + vscode_cmd = find_vscode_cmd() + if vscode_cmd is None: + choice = prompt_bool( + "VSCode cannot be found, and may not be installed. Proceed?" + ) + if not choice: + return 1 + + vscode_settings = mozpath.join( + command_context.topsrcdir, ".vscode", "settings.json" + ) + + new_settings = {} + artifact_prefix = "" + if command_context.config_environment.is_artifact_build: + artifact_prefix = ( + "\nArtifact build configured: Skipping clang and rust setup. " + "If you later switch to a full build, please re-run this command." + ) + else: + new_settings = setup_clangd_rust_in_vscode(command_context) + + relobjdir = mozpath.relpath(command_context.topobjdir, command_context.topsrcdir) + + # Add file associations. + new_settings = { + **new_settings, + "files.associations": { + "*.jsm": "javascript", + "*.sjs": "javascript", + }, + # Note, the top-level editor settings are left as default to allow the + # user's defaults (if any) to take effect. + "[javascript][javascriptreact][typescript][typescriptreact][json][jsonc][html]": { + "editor.defaultFormatter": "esbenp.prettier-vscode", + "editor.formatOnSave": True, + }, + "files.exclude": {"obj-*": True, relobjdir: True}, + "files.watcherExclude": {"obj-*": True, relobjdir: True}, + } + + import difflib + import json + + # Load the existing .vscode/settings.json file, to check if if needs to + # be created or updated. + try: + with open(vscode_settings) as fh: + old_settings_str = fh.read() + except FileNotFoundError: + print( + "Configuration for {} will be created.{}".format( + vscode_settings, artifact_prefix + ) + ) + old_settings_str = None + + if old_settings_str is None: + # No old settings exist + with open(vscode_settings, "w") as fh: + json.dump(new_settings, fh, indent=4) + else: + # Merge our new settings with the existing settings, and check if we + # need to make changes. Only prompt & write out the updated config + # file if settings actually changed. + try: + old_settings = json.loads(old_settings_str) + prompt_prefix = "" + except ValueError: + old_settings = {} + prompt_prefix = ( + "\n**WARNING**: Parsing of existing settings file failed. " + "Existing settings will be lost!" + ) + + # If we've got an old section with the formatting configuration, remove it + # so that we effectively "upgrade" the user to include json from the new + # settings. The user is presented with the diffs so should spot any issues. + deprecated = [ + "[javascript][javascriptreact][typescript][typescriptreact]", + "[javascript][javascriptreact][typescript][typescriptreact][json]", + "[javascript][javascriptreact][typescript][typescriptreact][json][html]", + ] + for entry in deprecated: + if entry in old_settings: + old_settings.pop(entry) + + settings = {**old_settings, **new_settings} + + if old_settings != settings: + # Prompt the user with a diff of the changes we're going to make + new_settings_str = json.dumps(settings, indent=4) + if interactive: + print( + "\nThe following modifications to {settings} will occur:\n{diff}".format( + settings=vscode_settings, + diff="".join( + difflib.unified_diff( + old_settings_str.splitlines(keepends=True), + new_settings_str.splitlines(keepends=True), + "a/.vscode/settings.json", + "b/.vscode/settings.json", + n=30, + ) + ), + ) + ) + choice = prompt_bool( + "{}{}\nProceed with modifications to {}?".format( + artifact_prefix, prompt_prefix, vscode_settings + ) + ) + if not choice: + return 1 + + with open(vscode_settings, "w") as fh: + fh.write(new_settings_str) + + if not interactive: + return 0 + + # Open vscode with new configuration, or ask the user to do so if the + # binary was not found. + if vscode_cmd is None: + print( + "Please open VS Code manually and load directory: {}".format( + command_context.topsrcdir + ) + ) + return 0 + + rc = subprocess.call(vscode_cmd + [command_context.topsrcdir]) + + if rc != 0: + command_context.log( + logging.ERROR, + "ide", + {}, + "Unable to open VS Code. Please open VS Code manually and load " + "directory: {}".format(command_context.topsrcdir), + ) + return rc + + return 0 + + +def setup_clangd_rust_in_vscode(command_context): + clangd_cc_path = mozpath.join(command_context.topobjdir, "clangd") + + # Verify if the required files are present + clang_tools_path = mozpath.join( + command_context._mach_context.state_dir, "clang-tools" + ) + clang_tidy_bin = mozpath.join(clang_tools_path, "clang-tidy", "bin") + + clangd_path = mozpath.join( + clang_tidy_bin, + "clangd" + command_context.config_environment.substs.get("BIN_SUFFIX", ""), + ) + + if not os.path.exists(clangd_path): + command_context.log( + logging.ERROR, + "ide", + {}, + "Unable to locate clangd in {}.".format(clang_tidy_bin), + ) + rc = get_clang_tools(command_context, clang_tools_path) + + if rc != 0: + return rc + + import multiprocessing + + from mozbuild.code_analysis.utils import ClangTidyConfig + + clang_tidy_cfg = ClangTidyConfig(command_context.topsrcdir) + + if sys.platform == "win32": + cargo_check_command = [sys.executable, "mach"] + else: + cargo_check_command = ["./mach"] + + cargo_check_command += [ + "--log-no-times", + "cargo", + "check", + "-j", + str(multiprocessing.cpu_count() // 2), + "--all-crates", + "--message-format-json", + ] + + clang_tidy = {} + clang_tidy["Checks"] = ",".join(clang_tidy_cfg.checks) + clang_tidy.update(clang_tidy_cfg.checks_config) + + # Write .clang-tidy yml + import yaml + + with open(".clang-tidy", "w") as file: + yaml.dump(clang_tidy, file) + + clangd_cfg = { + "CompileFlags": { + "CompilationDatabase": clangd_cc_path, + } + } + + with open(".clangd", "w") as file: + yaml.dump(clangd_cfg, file) + + return { + "clangd.path": clangd_path, + "clangd.arguments": [ + "-j", + str(multiprocessing.cpu_count() // 2), + "--limit-results", + "0", + "--completion-style", + "detailed", + "--background-index", + "--all-scopes-completion", + "--log", + "info", + "--pch-storage", + "disk", + "--clang-tidy", + "--header-insertion=never", + ], + "rust-analyzer.server.extraEnv": { + # Point rust-analyzer at the real target directory used by our + # build, so it can discover the files created when we run `./mach + # cargo check`. + "CARGO_TARGET_DIR": command_context.topobjdir, + }, + "rust-analyzer.cargo.buildScripts.overrideCommand": cargo_check_command, + "rust-analyzer.check.overrideCommand": cargo_check_command, + } + + +def get_clang_tools(command_context, clang_tools_path): + import shutil + + if os.path.isdir(clang_tools_path): + shutil.rmtree(clang_tools_path) + + # Create base directory where we store clang binary + os.mkdir(clang_tools_path) + + from mozbuild.artifact_commands import artifact_toolchain + + job, _ = command_context.platform + + if job is None: + command_context.log( + logging.ERROR, + "ide", + {}, + "The current platform isn't supported. " + "Currently only the following platforms are " + "supported: win32/win64, linux64 and macosx64.", + ) + return 1 + + job += "-clang-tidy" + + # We want to unpack data in the clang-tidy mozbuild folder + currentWorkingDir = os.getcwd() + os.chdir(clang_tools_path) + rc = artifact_toolchain( + command_context, verbose=False, from_build=[job], no_unpack=False, retry=0 + ) + # Change back the cwd + os.chdir(currentWorkingDir) + + return rc + + +def prompt_bool(prompt, limit=5): + """Prompts the user with prompt and requires a boolean value.""" + from distutils.util import strtobool + + for _ in range(limit): + try: + return strtobool(input(prompt + " [Y/N]\n")) + except ValueError: + print( + "ERROR! Please enter a valid option! Please use any of the following:" + " Y, N, True, False, 1, 0" + ) + return False diff --git a/python/mozbuild/mozbuild/backend/make.py b/python/mozbuild/mozbuild/backend/make.py new file mode 100644 index 0000000000..e93e49093a --- /dev/null +++ b/python/mozbuild/mozbuild/backend/make.py @@ -0,0 +1,139 @@ +# 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/. + +import mozpack.path as mozpath + +from mozbuild.frontend.data import GeneratedFile +from mozbuild.shellutil import quote as shell_quote + +from .common import CommonBackend + + +class MakeBackend(CommonBackend): + """Class encapsulating logic for backends that use Make.""" + + def _init(self): + CommonBackend._init(self) + + def _format_statements_for_generated_file(self, obj, tier, extra_dependencies=""): + """Return the list of statements to write to the Makefile for this + GeneratedFile. + + This function will invoke _format_generated_file_input_name and + _format_generated_file_output_name to munge the input/output filenames + before sending them to the output. + """ + assert isinstance(obj, GeneratedFile) + + # Localized generated files can use {AB_CD} and {AB_rCD} in their + # output paths. + if obj.localized: + substs = {"AB_CD": "$(AB_CD)", "AB_rCD": "$(AB_rCD)"} + else: + substs = {} + + outputs = [] + needs_AB_rCD = False + for o in obj.outputs: + needs_AB_rCD = needs_AB_rCD or ("AB_rCD" in o) + try: + outputs.append( + self._format_generated_file_output_name(o.format(**substs), obj) + ) + except KeyError as e: + raise ValueError( + "%s not in %s is not a valid substitution in %s" + % (e.args[0], ", ".join(sorted(substs.keys())), o) + ) + + first_output = outputs[0] + dep_file = mozpath.join( + mozpath.dirname(first_output), + "$(MDDEPDIR)", + "%s.pp" % mozpath.basename(first_output), + ) + # The stub target file needs to go in MDDEPDIR so that it doesn't + # get written into generated Android resource directories, breaking + # Gradle tooling and/or polluting the Android packages. + stub_file = mozpath.join( + mozpath.dirname(first_output), + "$(MDDEPDIR)", + "%s.stub" % mozpath.basename(first_output), + ) + + if obj.inputs: + inputs = [ + self._format_generated_file_input_name(f, obj) for f in obj.inputs + ] + else: + inputs = [] + + force = "" + if obj.force: + force = " FORCE" + elif obj.localized: + force = " $(if $(IS_LANGUAGE_REPACK),FORCE)" + + ret = [] + + if obj.script: + # If we are doing an artifact build, we don't run compiler, so + # we can skip generated files that are needed during compile, + # or let the rule run as the result of something depending on + # it. + if ( + not (obj.required_before_compile or obj.required_during_compile) + or not self.environment.is_artifact_build + ): + if tier and not needs_AB_rCD: + # Android localized resources have special Makefile + # handling. + + # Double-colon tiers via a variable that the backend adds as a dependency + # later. See https://bugzilla.mozilla.org/show_bug.cgi?id=1645986#c0 as + # to why. + if tier in ("export", "pre-compile", "libs", "misc"): + dep = "%s_TARGETS" % tier.replace("-", "_").upper() + ret.append("%s += %s" % (dep, stub_file)) + else: + ret.append("%s: %s" % (tier, stub_file)) + for output in outputs: + ret.append("%s: %s ;" % (output, stub_file)) + ret.append("EXTRA_MDDEPEND_FILES += %s" % dep_file) + + ret.append( + ( + """{stub}: {script}{inputs}{backend}{force} +\t$(REPORT_BUILD) +\t$(call py_action,file_generate {output},{locale}{script} """ # wrap for E501 + """{method} {output} {dep_file} {stub}{inputs}{flags}) +\t@$(TOUCH) $@ +""" + ).format( + stub=stub_file, + output=first_output, + dep_file=dep_file, + inputs=" " + " ".join(inputs) if inputs else "", + flags=" " + " ".join(shell_quote(f) for f in obj.flags) + if obj.flags + else "", + backend=" " + extra_dependencies if extra_dependencies else "", + # Locale repacks repack multiple locales from a single configured objdir, + # so standard mtime dependencies won't work properly when the build is re-run + # with a different locale as input. IS_LANGUAGE_REPACK will reliably be set + # in this situation, so simply force the generation to run in that case. + force=force, + locale="--locale=$(AB_CD) " if obj.localized else "", + script=obj.script, + method=obj.method, + ) + ) + + return ret + + def _format_generated_file_input_name(self, path, obj): + raise NotImplementedError("Subclass must implement") + + def _format_generated_file_output_name(self, path, obj): + raise NotImplementedError("Subclass must implement") diff --git a/python/mozbuild/mozbuild/backend/recursivemake.py b/python/mozbuild/mozbuild/backend/recursivemake.py new file mode 100644 index 0000000000..1180d9976e --- /dev/null +++ b/python/mozbuild/mozbuild/backend/recursivemake.py @@ -0,0 +1,1956 @@ +# 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/. + +import io +import logging +import os +import re +from collections import defaultdict, namedtuple +from itertools import chain +from operator import itemgetter + +import mozpack.path as mozpath +import six +from mozpack.manifests import InstallManifest +from six import StringIO + +from mozbuild import frontend +from mozbuild.frontend.context import ( + AbsolutePath, + ObjDirPath, + Path, + RenamedSourcePath, + SourcePath, +) +from mozbuild.shellutil import quote as shell_quote + +from ..frontend.data import ( + BaseLibrary, + BaseProgram, + BaseRustLibrary, + ChromeManifestEntry, + ComputedFlags, + ConfigFileSubstitution, + ContextDerived, + Defines, + DirectoryTraversal, + ExternalLibrary, + FinalTargetFiles, + FinalTargetPreprocessedFiles, + GeneratedFile, + HostDefines, + HostLibrary, + HostProgram, + HostRustProgram, + HostSharedLibrary, + HostSimpleProgram, + HostSources, + InstallationTarget, + JARManifest, + Linkable, + LocalInclude, + LocalizedFiles, + LocalizedPreprocessedFiles, + ObjdirFiles, + ObjdirPreprocessedFiles, + PerSourceFlag, + Program, + RustProgram, + RustTests, + SandboxedWasmLibrary, + SharedLibrary, + SimpleProgram, + Sources, + StaticLibrary, + TestManifest, + VariablePassthru, + WasmSources, + XPIDLModule, +) +from ..makeutil import Makefile +from ..util import FileAvoidWrite, OrderedDefaultDict, ensureParentDir, pairwise +from .common import CommonBackend +from .make import MakeBackend + +# To protect against accidentally adding logic to Makefiles that belong in moz.build, +# we check if moz.build-like variables are defined in Makefiles. If they are, we throw +# an error to encourage the usage of moz.build instead. +_MOZBUILD_ONLY_VARIABLES = set(frontend.context.VARIABLES.keys()) - { + # The migration to moz.build from Makefiles still isn't complete, and there's still + # some straggling Makefile logic that uses variables that only moz.build should + # use. + # These remaining variables are excluded from our blacklist. As the variables here + # are migrated from Makefiles in the future, they should be removed from this + # "override" list. + "XPI_NAME", + "USE_EXTENSION_MANIFEST", + "CFLAGS", + "CXXFLAGS", +} + +DEPRECATED_VARIABLES = [ + "ALLOW_COMPILER_WARNINGS", + "EXPORT_LIBRARY", + "EXTRA_LIBS", + "FAIL_ON_WARNINGS", + "HOST_LIBS", + "LIBXUL_LIBRARY", + "MOCHITEST_A11Y_FILES", + "MOCHITEST_BROWSER_FILES", + "MOCHITEST_BROWSER_FILES_PARTS", + "MOCHITEST_CHROME_FILES", + "MOCHITEST_FILES", + "MOCHITEST_FILES_PARTS", + "MOCHITEST_METRO_FILES", + "MOCHITEST_ROBOCOP_FILES", + "MODULE_OPTIMIZE_FLAGS", + "MOZ_CHROME_FILE_FORMAT", + "SHORT_LIBNAME", + "TESTING_JS_MODULES", + "TESTING_JS_MODULE_DIR", +] + +MOZBUILD_VARIABLES_MESSAGE = "It should only be defined in moz.build files." + +DEPRECATED_VARIABLES_MESSAGE = ( + "This variable has been deprecated. It does nothing. It must be removed " + "in order to build." +) + + +def make_quote(s): + return s.replace("#", r"\#").replace("$", "$$") + + +class BackendMakeFile(object): + """Represents a generated backend.mk file. + + This is both a wrapper around a file handle as well as a container that + holds accumulated state. + + It's worth taking a moment to explain the make dependencies. The + generated backend.mk as well as the Makefile.in (if it exists) are in the + GLOBAL_DEPS list. This means that if one of them changes, all targets + in that Makefile are invalidated. backend.mk also depends on all of its + input files. + + It's worth considering the effect of file mtimes on build behavior. + + Since we perform an "all or none" traversal of moz.build files (the whole + tree is scanned as opposed to individual files), if we were to blindly + write backend.mk files, the net effect of updating a single mozbuild file + in the tree is all backend.mk files have new mtimes. This would in turn + invalidate all make targets across the whole tree! This would effectively + undermine incremental builds as any mozbuild change would cause the entire + tree to rebuild! + + The solution is to not update the mtimes of backend.mk files unless they + actually change. We use FileAvoidWrite to accomplish this. + """ + + def __init__(self, srcdir, objdir, environment, topsrcdir, topobjdir, dry_run): + self.topsrcdir = topsrcdir + self.srcdir = srcdir + self.objdir = objdir + self.relobjdir = mozpath.relpath(objdir, topobjdir) + self.environment = environment + self.name = mozpath.join(objdir, "backend.mk") + + self.xpt_name = None + + self.fh = FileAvoidWrite(self.name, capture_diff=True, dry_run=dry_run) + self.fh.write("# THIS FILE WAS AUTOMATICALLY GENERATED. DO NOT EDIT.\n") + self.fh.write("\n") + + def write(self, buf): + self.fh.write(buf) + + def write_once(self, buf): + buf = six.ensure_text(buf) + if "\n" + buf not in six.ensure_text(self.fh.getvalue()): + self.write(buf) + + # For compatibility with makeutil.Makefile + def add_statement(self, stmt): + self.write("%s\n" % stmt) + + def close(self): + if self.xpt_name: + # We just recompile all xpidls because it's easier and less error + # prone. + self.fh.write("NONRECURSIVE_TARGETS += export\n") + self.fh.write("NONRECURSIVE_TARGETS_export += xpidl\n") + self.fh.write( + "NONRECURSIVE_TARGETS_export_xpidl_DIRECTORY = " + "$(DEPTH)/xpcom/xpidl\n" + ) + self.fh.write("NONRECURSIVE_TARGETS_export_xpidl_TARGETS += " "export\n") + + return self.fh.close() + + @property + def diff(self): + return self.fh.diff + + +class RecursiveMakeTraversal(object): + """ + Helper class to keep track of how the "traditional" recursive make backend + recurses subdirectories. This is useful until all adhoc rules are removed + from Makefiles. + + Each directory may have one or more types of subdirectories: + - (normal) dirs + - tests + """ + + SubDirectoryCategories = ["dirs", "tests"] + SubDirectoriesTuple = namedtuple("SubDirectories", SubDirectoryCategories) + + class SubDirectories(SubDirectoriesTuple): + def __new__(self): + return RecursiveMakeTraversal.SubDirectoriesTuple.__new__(self, [], []) + + def __init__(self): + self._traversal = {} + self._attached = set() + + def add(self, dir, dirs=[], tests=[]): + """ + Adds a directory to traversal, registering its subdirectories, + sorted by categories. If the directory was already added to + traversal, adds the new subdirectories to the already known lists. + """ + subdirs = self._traversal.setdefault(dir, self.SubDirectories()) + for key, value in (("dirs", dirs), ("tests", tests)): + assert key in self.SubDirectoryCategories + # Callers give us generators + value = list(value) + getattr(subdirs, key).extend(value) + self._attached |= set(value) + + @staticmethod + def default_filter(current, subdirs): + """ + Default filter for use with compute_dependencies and traverse. + """ + return current, [], subdirs.dirs + subdirs.tests + + def call_filter(self, current, filter): + """ + Helper function to call a filter from compute_dependencies and + traverse. + """ + return filter(current, self.get_subdirs(current)) + + def compute_dependencies(self, filter=None): + """ + Compute make dependencies corresponding to the registered directory + traversal. + + filter is a function with the following signature: + def filter(current, subdirs) + + where current is the directory being traversed, and subdirs the + SubDirectories instance corresponding to it. + The filter function returns a tuple (filtered_current, filtered_parallel, + filtered_dirs) where filtered_current is either current or None if + the current directory is to be skipped, and filtered_parallel and + filtered_dirs are lists of parallel directories and sequential + directories, which can be rearranged from whatever is given in the + SubDirectories members. + + The default filter corresponds to a default recursive traversal. + + """ + filter = filter or self.default_filter + + deps = {} + + def recurse(start_node, prev_nodes=None): + current, parallel, sequential = self.call_filter(start_node, filter) + if current is not None: + if start_node != "": + deps[start_node] = prev_nodes + prev_nodes = (start_node,) + if start_node not in self._traversal: + return prev_nodes + parallel_nodes = [] + for node in parallel: + nodes = recurse(node, prev_nodes) + if nodes and nodes != ("",): + parallel_nodes.extend(nodes) + if parallel_nodes: + prev_nodes = tuple(parallel_nodes) + for dir in sequential: + prev_nodes = recurse(dir, prev_nodes) + return prev_nodes + + return recurse(""), deps + + def traverse(self, start, filter=None): + """ + Iterate over the filtered subdirectories, following the traditional + make traversal order. + """ + if filter is None: + filter = self.default_filter + + current, parallel, sequential = self.call_filter(start, filter) + if current is not None: + yield start + if start not in self._traversal: + return + for node in parallel: + for n in self.traverse(node, filter): + yield n + for dir in sequential: + for d in self.traverse(dir, filter): + yield d + + def get_subdirs(self, dir): + """ + Returns all direct subdirectories under the given directory. + """ + result = self._traversal.get(dir, self.SubDirectories()) + if dir == "": + unattached = set(self._traversal) - self._attached - set([""]) + if unattached: + new_result = self.SubDirectories() + new_result.dirs.extend(result.dirs) + new_result.dirs.extend(sorted(unattached)) + new_result.tests.extend(result.tests) + result = new_result + return result + + +class RecursiveMakeBackend(MakeBackend): + """Backend that integrates with the existing recursive make build system. + + This backend facilitates the transition from Makefile.in to moz.build + files. + + This backend performs Makefile.in -> Makefile conversion. It also writes + out .mk files containing content derived from moz.build files. Both are + consumed by the recursive make builder. + + This backend may eventually evolve to write out non-recursive make files. + However, as long as there are Makefile.in files in the tree, we are tied to + recursive make and thus will need this backend. + """ + + def _init(self): + MakeBackend._init(self) + + self._backend_files = {} + self._idl_dirs = set() + + self._makefile_in_count = 0 + self._makefile_out_count = 0 + + self._test_manifests = {} + + self.backend_input_files.add( + mozpath.join(self.environment.topobjdir, "config", "autoconf.mk") + ) + + self._install_manifests = defaultdict(InstallManifest) + # The build system relies on some install manifests always existing + # even if they are empty, because the directories are still filled + # by the build system itself, and the install manifests are only + # used for a "magic" rm -rf. + self._install_manifests["dist_public"] + self._install_manifests["dist_private"] + + self._traversal = RecursiveMakeTraversal() + self._compile_graph = OrderedDefaultDict(set) + self._rust_targets = set() + self._gkrust_target = None + self._pre_compile = set() + + # For a given file produced by the build, gives the top-level target + # that will produce it. + self._target_per_file = {} + # A set of dependencies that need correlation after everything has + # been processed. + self._post_process_dependencies = [] + + self._no_skip = { + "pre-export": set(), + "export": set(), + "libs": set(), + "misc": set(), + "tools": set(), + "check": set(), + "syms": set(), + } + + def summary(self): + summary = super(RecursiveMakeBackend, self).summary() + summary.extend( + "; {makefile_in:d} -> {makefile_out:d} Makefile", + makefile_in=self._makefile_in_count, + makefile_out=self._makefile_out_count, + ) + return summary + + def _get_backend_file_for(self, obj): + # For generated files that we put in the export or misc tiers, we use the + # top-level backend file, except for localized files, which we need to keep + # in each directory for dependencies from jar manifests for l10n repacks. + if ( + isinstance(obj, GeneratedFile) + and not obj.required_during_compile + and not obj.localized + ): + objdir = self.environment.topobjdir + else: + objdir = obj.objdir + + if objdir not in self._backend_files: + self._backend_files[objdir] = BackendMakeFile( + obj.srcdir, + objdir, + obj.config, + obj.topsrcdir, + self.environment.topobjdir, + self.dry_run, + ) + return self._backend_files[objdir] + + def consume_object(self, obj): + """Write out build files necessary to build with recursive make.""" + + if not isinstance(obj, ContextDerived): + return False + + backend_file = self._get_backend_file_for(obj) + + consumed = CommonBackend.consume_object(self, obj) + + # CommonBackend handles XPIDLModule, but we want to do + # some extra things for them. + if isinstance(obj, XPIDLModule): + backend_file.xpt_name = "%s.xpt" % obj.name + self._idl_dirs.add(obj.relobjdir) + + # If CommonBackend acknowledged the object, we're done with it. + if consumed: + return True + + if not isinstance(obj, Defines): + self.consume_object(obj.defines) + + if isinstance(obj, Linkable): + self._process_test_support_file(obj) + self._process_relrhack(obj) + + if isinstance(obj, DirectoryTraversal): + self._process_directory_traversal(obj, backend_file) + elif isinstance(obj, ConfigFileSubstitution): + # Other ConfigFileSubstitution should have been acked by + # CommonBackend. + assert os.path.basename(obj.output_path) == "Makefile" + self._create_makefile(obj) + elif isinstance(obj, Sources): + suffix_map = { + ".s": "ASFILES", + ".c": "CSRCS", + ".m": "CMSRCS", + ".mm": "CMMSRCS", + ".cpp": "CPPSRCS", + ".S": "SSRCS", + } + variables = [suffix_map[obj.canonical_suffix]] + for files, base, cls, prefix in ( + (obj.static_files, backend_file.srcdir, SourcePath, ""), + (obj.generated_files, backend_file.objdir, ObjDirPath, "!"), + ): + for f in sorted(files): + p = self._pretty_path( + cls(obj._context, prefix + mozpath.relpath(f, base)), + backend_file, + ) + for var in variables: + backend_file.write("%s += %s\n" % (var, p)) + self._compile_graph[mozpath.join(backend_file.relobjdir, "target-objects")] + elif isinstance(obj, HostSources): + suffix_map = { + ".c": "HOST_CSRCS", + ".mm": "HOST_CMMSRCS", + ".cpp": "HOST_CPPSRCS", + } + variables = [suffix_map[obj.canonical_suffix]] + for files, base, cls, prefix in ( + (obj.static_files, backend_file.srcdir, SourcePath, ""), + (obj.generated_files, backend_file.objdir, ObjDirPath, "!"), + ): + for f in sorted(files): + p = self._pretty_path( + cls(obj._context, prefix + mozpath.relpath(f, base)), + backend_file, + ) + for var in variables: + backend_file.write("%s += %s\n" % (var, p)) + self._compile_graph[mozpath.join(backend_file.relobjdir, "host-objects")] + elif isinstance(obj, WasmSources): + suffix_map = {".c": "WASM_CSRCS", ".cpp": "WASM_CPPSRCS"} + variables = [suffix_map[obj.canonical_suffix]] + for files, base, cls, prefix in ( + (obj.static_files, backend_file.srcdir, SourcePath, ""), + (obj.generated_files, backend_file.objdir, ObjDirPath, "!"), + ): + for f in sorted(files): + p = self._pretty_path( + cls(obj._context, prefix + mozpath.relpath(f, base)), + backend_file, + ) + for var in variables: + backend_file.write("%s += %s\n" % (var, p)) + self._compile_graph[mozpath.join(backend_file.relobjdir, "target-objects")] + elif isinstance(obj, VariablePassthru): + # Sorted so output is consistent and we don't bump mtimes. + for k, v in sorted(obj.variables.items()): + if isinstance(v, list): + for item in v: + backend_file.write( + "%s += %s\n" % (k, make_quote(shell_quote(item))) + ) + elif isinstance(v, bool): + if v: + backend_file.write("%s := 1\n" % k) + elif isinstance(v, Path): + path = self._pretty_path(Path(obj._context, v), backend_file) + backend_file.write("%s := %s\n" % (k, path)) + else: + backend_file.write("%s := %s\n" % (k, v)) + elif isinstance(obj, HostDefines): + self._process_defines(obj, backend_file, which="HOST_DEFINES") + elif isinstance(obj, Defines): + self._process_defines(obj, backend_file) + + elif isinstance(obj, GeneratedFile): + if obj.required_before_export: + tier = "pre-export" + elif obj.required_before_compile: + tier = "export" + elif obj.required_during_compile: + tier = "pre-compile" + else: + tier = "misc" + relobjdir = mozpath.relpath(obj.objdir, self.environment.topobjdir) + if tier == "pre-compile": + self._pre_compile.add(relobjdir) + else: + self._no_skip[tier].add(relobjdir) + backend_file.write_once("include $(topsrcdir)/config/AB_rCD.mk\n") + reldir = mozpath.relpath(obj.objdir, backend_file.objdir) + if not reldir: + for out in obj.outputs: + out = mozpath.join(relobjdir, out) + assert out not in self._target_per_file + self._target_per_file[out] = (relobjdir, tier) + for input in obj.inputs: + if isinstance(input, ObjDirPath): + self._post_process_dependencies.append((relobjdir, tier, input)) + # For generated files that we handle in the top-level backend file, + # we want to have a `directory/tier` target depending on the file. + # For the others, we want a `tier` target. + if tier != "pre-compile" and reldir: + tier = "%s/%s" % (reldir, tier) + for stmt in self._format_statements_for_generated_file( + obj, tier, extra_dependencies="backend.mk" if obj.flags else "" + ): + backend_file.write(stmt + "\n") + + elif isinstance(obj, JARManifest): + self._no_skip["misc"].add(backend_file.relobjdir) + backend_file.write("JAR_MANIFEST := %s\n" % obj.path.full_path) + + elif isinstance(obj, RustProgram): + self._process_rust_program(obj, backend_file) + self._process_linked_libraries(obj, backend_file) + # Hook the program into the compile graph. + build_target = self._build_target_for_obj(obj) + self._compile_graph[build_target] + self._rust_targets.add(build_target) + + elif isinstance(obj, HostRustProgram): + self._process_host_rust_program(obj, backend_file) + self._process_linked_libraries(obj, backend_file) + # Hook the program into the compile graph. + build_target = self._build_target_for_obj(obj) + self._compile_graph[build_target] + self._rust_targets.add(build_target) + + elif isinstance(obj, RustTests): + self._process_rust_tests(obj, backend_file) + + elif isinstance(obj, Program): + self._process_program(obj, backend_file) + self._process_linked_libraries(obj, backend_file) + self._no_skip["syms"].add(backend_file.relobjdir) + + elif isinstance(obj, HostProgram): + self._process_host_program(obj, backend_file) + self._process_linked_libraries(obj, backend_file) + self._target_per_file[obj.output_path.full_path] = ( + backend_file.relobjdir, + obj.KIND, + ) + + elif isinstance(obj, SimpleProgram): + self._process_simple_program(obj, backend_file) + self._process_linked_libraries(obj, backend_file) + self._no_skip["syms"].add(backend_file.relobjdir) + + elif isinstance(obj, HostSimpleProgram): + self._process_host_simple_program(obj.program, backend_file) + self._process_linked_libraries(obj, backend_file) + + elif isinstance(obj, LocalInclude): + self._process_local_include(obj.path, backend_file) + + elif isinstance(obj, PerSourceFlag): + self._process_per_source_flag(obj, backend_file) + + elif isinstance(obj, ComputedFlags): + self._process_computed_flags(obj, backend_file) + + elif isinstance(obj, InstallationTarget): + self._process_installation_target(obj, backend_file) + + elif isinstance(obj, BaseRustLibrary): + self.backend_input_files.add(obj.cargo_file) + self._process_rust_library(obj, backend_file) + # No need to call _process_linked_libraries, because Rust + # libraries are self-contained objects at this point. + + # Hook the library into the compile graph. + build_target = self._build_target_for_obj(obj) + self._compile_graph[build_target] + self._rust_targets.add(build_target) + if obj.is_gkrust: + self._gkrust_target = build_target + + elif isinstance(obj, SharedLibrary): + self._process_shared_library(obj, backend_file) + self._process_linked_libraries(obj, backend_file) + self._no_skip["syms"].add(backend_file.relobjdir) + + elif isinstance(obj, StaticLibrary): + self._process_static_library(obj, backend_file) + self._process_linked_libraries(obj, backend_file) + + elif isinstance(obj, SandboxedWasmLibrary): + self._process_sandboxed_wasm_library(obj, backend_file) + + elif isinstance(obj, HostLibrary): + self._process_linked_libraries(obj, backend_file) + + elif isinstance(obj, HostSharedLibrary): + self._process_host_shared_library(obj, backend_file) + self._process_linked_libraries(obj, backend_file) + + elif isinstance(obj, ObjdirFiles): + self._process_objdir_files(obj, obj.files, backend_file) + + elif isinstance(obj, ObjdirPreprocessedFiles): + self._process_final_target_pp_files( + obj, obj.files, backend_file, "OBJDIR_PP_FILES" + ) + + elif isinstance(obj, LocalizedFiles): + self._process_localized_files(obj, obj.files, backend_file) + + elif isinstance(obj, LocalizedPreprocessedFiles): + self._process_localized_pp_files(obj, obj.files, backend_file) + + elif isinstance(obj, FinalTargetFiles): + self._process_final_target_files(obj, obj.files, backend_file) + + elif isinstance(obj, FinalTargetPreprocessedFiles): + self._process_final_target_pp_files( + obj, obj.files, backend_file, "DIST_FILES" + ) + + elif isinstance(obj, ChromeManifestEntry): + self._process_chrome_manifest_entry(obj, backend_file) + + elif isinstance(obj, TestManifest): + self._process_test_manifest(obj, backend_file) + + else: + return False + + return True + + def _fill_root_mk(self): + """ + Create two files, root.mk and root-deps.mk, the first containing + convenience variables, and the other dependency definitions for a + hopefully proper directory traversal. + """ + for tier, no_skip in self._no_skip.items(): + self.log( + logging.DEBUG, + "fill_root_mk", + {"number": len(no_skip), "tier": tier}, + "Using {number} directories during {tier}", + ) + + def should_skip(tier, dir): + if tier in self._no_skip: + return dir not in self._no_skip[tier] + return False + + # Traverse directories in parallel, and skip static dirs + def parallel_filter(current, subdirs): + all_subdirs = subdirs.dirs + subdirs.tests + if should_skip(tier, current) or current.startswith("subtiers/"): + current = None + return current, all_subdirs, [] + + # build everything in parallel, including static dirs + # Because of bug 925236 and possible other unknown race conditions, + # don't parallelize the libs tier. + def libs_filter(current, subdirs): + if should_skip("libs", current) or current.startswith("subtiers/"): + current = None + return current, [], subdirs.dirs + subdirs.tests + + # Because of bug 925236 and possible other unknown race conditions, + # don't parallelize the tools tier. There aren't many directories for + # this tier anyways. + def tools_filter(current, subdirs): + if should_skip("tools", current) or current.startswith("subtiers/"): + current = None + return current, [], subdirs.dirs + subdirs.tests + + filters = [ + ("export", parallel_filter), + ("libs", libs_filter), + ("misc", parallel_filter), + ("tools", tools_filter), + ("check", parallel_filter), + ] + + root_deps_mk = Makefile() + + # Fill the dependencies for traversal of each tier. + for tier, filter in sorted(filters, key=itemgetter(0)): + main, all_deps = self._traversal.compute_dependencies(filter) + for dir, deps in sorted(all_deps.items()): + if deps is not None or (dir in self._idl_dirs and tier == "export"): + rule = root_deps_mk.create_rule(["%s/%s" % (dir, tier)]) + if deps: + rule.add_dependencies( + "%s/%s" % (d, tier) for d in sorted(deps) if d + ) + rule = root_deps_mk.create_rule(["recurse_%s" % tier]) + if main: + rule.add_dependencies("%s/%s" % (d, tier) for d in sorted(main)) + + rule = root_deps_mk.create_rule(["recurse_pre-compile"]) + rule.add_dependencies("%s/pre-compile" % d for d in sorted(self._pre_compile)) + + targets_with_pre_compile = sorted( + t for t in self._compile_graph if mozpath.dirname(t) in self._pre_compile + ) + for t in targets_with_pre_compile: + relobjdir = mozpath.dirname(t) + rule = root_deps_mk.create_rule([t]) + rule.add_dependencies(["%s/pre-compile" % relobjdir]) + + all_compile_deps = ( + six.moves.reduce(lambda x, y: x | y, self._compile_graph.values()) + if self._compile_graph + else set() + ) + # Include the following as dependencies of the top recursion target for + # compilation: + # - nodes that are not dependended upon by anything. Typically, this + # would include programs, that need to be recursed, but that nothing + # depends on. + # - nodes that are rust targets. + compile_roots = [ + t + for t, deps in six.iteritems(self._compile_graph) + if t in self._rust_targets or t not in all_compile_deps + ] + + def add_category_rules(category, roots, graph): + rule = root_deps_mk.create_rule(["recurse_%s" % category]) + # Directories containing rust compilations don't generally depend + # on other directories in the tree, so putting them first here will + # start them earlier in the build. + rust_roots = sorted(r for r in roots if r in self._rust_targets) + if category == "compile" and rust_roots: + rust_rule = root_deps_mk.create_rule(["recurse_rust"]) + rust_rule.add_dependencies(rust_roots) + # Ensure our cargo invocations are serialized, and gecko comes + # first. Cargo will lock on the build output directory anyway, + # so trying to run things in parallel is not useful. Dependencies + # for gecko are especially expensive to build and parallelize + # poorly, so prioritizing these will save some idle time in full + # builds. + for prior_target, target in pairwise( + sorted( + [t for t in rust_roots], key=lambda t: t != self._gkrust_target + ) + ): + r = root_deps_mk.create_rule([target]) + r.add_dependencies([prior_target]) + + rule.add_dependencies(chain(rust_roots, sorted(roots))) + for target, deps in sorted(graph.items()): + if deps: + rule = root_deps_mk.create_rule([target]) + rule.add_dependencies(sorted(deps)) + + non_default_roots = defaultdict(list) + non_default_graphs = defaultdict(lambda: OrderedDefaultDict(set)) + + for root in compile_roots: + # If this is a non-default target, separate the root from the + # rest of the compile graph. + target_name = mozpath.basename(root) + + if target_name not in ("target", "target-objects", "host", "host-objects"): + non_default_roots[target_name].append(root) + non_default_graphs[target_name][root] = self._compile_graph[root] + del self._compile_graph[root] + + for root in chain(*non_default_roots.values()): + compile_roots.remove(root) + dirname = mozpath.dirname(root) + # If a directory only contains non-default compile targets, we don't + # attempt to dump symbols there. + if ( + dirname in self._no_skip["syms"] + and "%s/target" % dirname not in self._compile_graph + ): + self._no_skip["syms"].remove(dirname) + + add_category_rules("compile", compile_roots, self._compile_graph) + for category, graph in sorted(six.iteritems(non_default_graphs)): + add_category_rules(category, non_default_roots[category], graph) + + for relobjdir, tier, input in self._post_process_dependencies: + dep = self._target_per_file.get(input.full_path) + if dep: + rule = root_deps_mk.create_rule([mozpath.join(relobjdir, tier)]) + rule.add_dependencies([mozpath.join(*dep)]) + + root_mk = Makefile() + + # Fill root.mk with the convenience variables. + for tier, filter in filters: + all_dirs = self._traversal.traverse("", filter) + root_mk.add_statement("%s_dirs := %s" % (tier, " ".join(all_dirs))) + + # Need a list of compile targets because we can't use pattern rules: + # https://savannah.gnu.org/bugs/index.php?42833 + root_mk.add_statement( + "pre_compile_targets := %s" + % " ".join(sorted("%s/pre-compile" % p for p in self._pre_compile)) + ) + root_mk.add_statement( + "compile_targets := %s" + % " ".join(sorted(set(self._compile_graph.keys()) | all_compile_deps)) + ) + root_mk.add_statement( + "syms_targets := %s" + % " ".join(sorted(set("%s/syms" % d for d in self._no_skip["syms"]))) + ) + root_mk.add_statement( + "rust_targets := %s" % " ".join(sorted(self._rust_targets)) + ) + + root_mk.add_statement( + "non_default_tiers := %s" % " ".join(sorted(non_default_roots.keys())) + ) + + for category, graphs in sorted(six.iteritems(non_default_graphs)): + category_dirs = [mozpath.dirname(target) for target in graphs.keys()] + root_mk.add_statement("%s_dirs := %s" % (category, " ".join(category_dirs))) + + root_mk.add_statement("include root-deps.mk") + + with self._write_file( + mozpath.join(self.environment.topobjdir, "root.mk") + ) as root: + root_mk.dump(root, removal_guard=False) + + with self._write_file( + mozpath.join(self.environment.topobjdir, "root-deps.mk") + ) as root_deps: + root_deps_mk.dump(root_deps, removal_guard=False) + + def _add_unified_build_rules( + self, + makefile, + unified_source_mapping, + unified_files_makefile_variable="unified_files", + include_curdir_build_rules=True, + ): + # In case it's a generator. + unified_source_mapping = sorted(unified_source_mapping) + + explanation = ( + "\n" + "# We build files in 'unified' mode by including several files\n" + "# together into a single source file. This cuts down on\n" + "# compilation times and debug information size." + ) + makefile.add_statement(explanation) + + all_sources = " ".join(source for source, _ in unified_source_mapping) + makefile.add_statement( + "%s := %s" % (unified_files_makefile_variable, all_sources) + ) + + if include_curdir_build_rules: + makefile.add_statement( + "\n" + '# Make sometimes gets confused between "foo" and "$(CURDIR)/foo".\n' + "# Help it out by explicitly specifiying dependencies." + ) + makefile.add_statement( + "all_absolute_unified_files := \\\n" + " $(addprefix $(CURDIR)/,$(%s))" % unified_files_makefile_variable + ) + rule = makefile.create_rule(["$(all_absolute_unified_files)"]) + rule.add_dependencies(["$(CURDIR)/%: %"]) + + def _check_blacklisted_variables(self, makefile_in, makefile_content): + if "EXTERNALLY_MANAGED_MAKE_FILE" in makefile_content: + # Bypass the variable restrictions for externally managed makefiles. + return + + for l in makefile_content.splitlines(): + l = l.strip() + # Don't check comments + if l.startswith("#"): + continue + for x in chain(_MOZBUILD_ONLY_VARIABLES, DEPRECATED_VARIABLES): + if x not in l: + continue + + # Finding the variable name in the Makefile is not enough: it + # may just appear as part of something else, like DIRS appears + # in GENERATED_DIRS. + if re.search(r"\b%s\s*[:?+]?=" % x, l): + if x in _MOZBUILD_ONLY_VARIABLES: + message = MOZBUILD_VARIABLES_MESSAGE + else: + message = DEPRECATED_VARIABLES_MESSAGE + raise Exception( + "Variable %s is defined in %s. %s" % (x, makefile_in, message) + ) + + def consume_finished(self): + CommonBackend.consume_finished(self) + + for objdir, backend_file in sorted(self._backend_files.items()): + srcdir = backend_file.srcdir + with self._write_file(fh=backend_file) as bf: + makefile_in = mozpath.join(srcdir, "Makefile.in") + makefile = mozpath.join(objdir, "Makefile") + + # If Makefile.in exists, use it as a template. Otherwise, + # create a stub. + stub = not os.path.exists(makefile_in) + if not stub: + self.log( + logging.DEBUG, + "substitute_makefile", + {"path": makefile}, + "Substituting makefile: {path}", + ) + self._makefile_in_count += 1 + + # In the export and libs tiers, we don't skip directories + # containing a Makefile.in. + # topobjdir is handled separatedly, don't do anything for + # it. + if bf.relobjdir: + for tier in ("export", "libs"): + self._no_skip[tier].add(bf.relobjdir) + else: + self.log( + logging.DEBUG, + "stub_makefile", + {"path": makefile}, + "Creating stub Makefile: {path}", + ) + + obj = self.Substitution() + obj.output_path = makefile + obj.input_path = makefile_in + obj.topsrcdir = backend_file.topsrcdir + obj.topobjdir = bf.environment.topobjdir + obj.config = bf.environment + self._create_makefile(obj, stub=stub) + with io.open(obj.output_path, encoding="utf-8") as fh: + content = fh.read() + # Directories with a Makefile containing a tools target, or + # XPI_PKGNAME can't be skipped and must run during the + # 'tools' tier. + for t in ("XPI_PKGNAME", "tools"): + if t not in content: + continue + if t == "tools" and not re.search( + r"(?:^|\s)tools.*::", content, re.M + ): + continue + if objdir == self.environment.topobjdir: + continue + self._no_skip["tools"].add( + mozpath.relpath(objdir, self.environment.topobjdir) + ) + + # Directories with a Makefile containing a check target + # can't be skipped and must run during the 'check' tier. + if re.search(r"(?:^|\s)check.*::", content, re.M): + self._no_skip["check"].add( + mozpath.relpath(objdir, self.environment.topobjdir) + ) + + # Detect any Makefile.ins that contain variables on the + # moz.build-only list + self._check_blacklisted_variables(makefile_in, content) + + self._fill_root_mk() + + # Make the master test manifest files. + for flavor, t in self._test_manifests.items(): + install_prefix, manifests = t + manifest_stem = mozpath.join(install_prefix, "%s.toml" % flavor) + self._write_master_test_manifest( + mozpath.join(self.environment.topobjdir, "_tests", manifest_stem), + manifests, + ) + + # Catch duplicate inserts. + try: + self._install_manifests["_tests"].add_optional_exists(manifest_stem) + except ValueError: + pass + + self._write_manifests("install", self._install_manifests) + + ensureParentDir(mozpath.join(self.environment.topobjdir, "dist", "foo")) + + def _pretty_path_parts(self, path, backend_file): + assert isinstance(path, Path) + if isinstance(path, SourcePath): + if path.full_path.startswith(backend_file.srcdir): + return "$(srcdir)", path.full_path[len(backend_file.srcdir) :] + if path.full_path.startswith(backend_file.topsrcdir): + return "$(topsrcdir)", path.full_path[len(backend_file.topsrcdir) :] + elif isinstance(path, ObjDirPath): + if path.full_path.startswith(backend_file.objdir): + return "", path.full_path[len(backend_file.objdir) + 1 :] + if path.full_path.startswith(self.environment.topobjdir): + return "$(DEPTH)", path.full_path[len(self.environment.topobjdir) :] + + return "", path.full_path + + def _pretty_path(self, path, backend_file): + return "".join(self._pretty_path_parts(path, backend_file)) + + def _process_unified_sources(self, obj): + backend_file = self._get_backend_file_for(obj) + + suffix_map = { + ".c": "UNIFIED_CSRCS", + ".m": "UNIFIED_CMSRCS", + ".mm": "UNIFIED_CMMSRCS", + ".cpp": "UNIFIED_CPPSRCS", + } + + var = suffix_map[obj.canonical_suffix] + non_unified_var = var[len("UNIFIED_") :] + + if obj.have_unified_mapping: + self._add_unified_build_rules( + backend_file, + obj.unified_source_mapping, + unified_files_makefile_variable=var, + include_curdir_build_rules=False, + ) + backend_file.write("%s += $(%s)\n" % (non_unified_var, var)) + else: + # Sorted so output is consistent and we don't bump mtimes. + source_files = list(sorted(obj.files)) + + backend_file.write("%s += %s\n" % (non_unified_var, " ".join(source_files))) + + self._compile_graph[mozpath.join(backend_file.relobjdir, "target-objects")] + + def _process_directory_traversal(self, obj, backend_file): + """Process a data.DirectoryTraversal instance.""" + fh = backend_file.fh + + def relativize(base, dirs): + return (mozpath.relpath(d.translated, base) for d in dirs) + + if obj.dirs: + fh.write( + "DIRS := %s\n" % " ".join(relativize(backend_file.objdir, obj.dirs)) + ) + self._traversal.add( + backend_file.relobjdir, + dirs=relativize(self.environment.topobjdir, obj.dirs), + ) + + # The directory needs to be registered whether subdirectories have been + # registered or not. + self._traversal.add(backend_file.relobjdir) + + def _process_defines(self, obj, backend_file, which="DEFINES"): + """Output the DEFINES rules to the given backend file.""" + defines = list(obj.get_defines()) + if defines: + defines = " ".join(shell_quote(d) for d in defines) + backend_file.write_once("%s += %s\n" % (which, defines)) + + def _process_installation_target(self, obj, backend_file): + # A few makefiles need to be able to override the following rules via + # make XPI_NAME=blah commands, so we default to the lazy evaluation as + # much as possible here to avoid breaking things. + if obj.xpiname: + backend_file.write("XPI_NAME = %s\n" % (obj.xpiname)) + if obj.subdir: + backend_file.write("DIST_SUBDIR = %s\n" % (obj.subdir)) + if obj.target and not obj.is_custom(): + backend_file.write("FINAL_TARGET = $(DEPTH)/%s\n" % (obj.target)) + else: + backend_file.write( + "FINAL_TARGET = $(if $(XPI_NAME),$(DIST)/xpi-stage/$(XPI_NAME)," + "$(DIST)/bin)$(DIST_SUBDIR:%=/%)\n" + ) + + if not obj.enabled: + backend_file.write("NO_DIST_INSTALL := 1\n") + + def _handle_idl_manager(self, manager): + build_files = self._install_manifests["xpidl"] + + for p in ("Makefile", "backend.mk", ".deps/.mkdir.done"): + build_files.add_optional_exists(p) + + for stem in manager.idl_stems(): + self._install_manifests["dist_include"].add_optional_exists("%s.h" % stem) + + for module in manager.modules: + build_files.add_optional_exists(mozpath.join(".deps", "%s.pp" % module)) + + modules = manager.modules + xpt_modules = sorted(modules.keys()) + + mk = Makefile() + all_directories = set() + + for module_name in xpt_modules: + module = manager.modules[module_name] + all_directories |= module.directories + deps = sorted(module.idl_files) + + # It may seem strange to have the .idl files listed as + # prerequisites both here and in the auto-generated .pp files. + # It is necessary to list them here to handle the case where a + # new .idl is added to an xpt. If we add a new .idl and nothing + # else has changed, the new .idl won't be referenced anywhere + # except in the command invocation. Therefore, the .xpt won't + # be rebuilt because the dependencies say it is up to date. By + # listing the .idls here, we ensure the make file has a + # reference to the new .idl. Since the new .idl presumably has + # an mtime newer than the .xpt, it will trigger xpt generation. + + mk.add_statement("%s_deps := %s" % (module_name, " ".join(deps))) + + build_files.add_optional_exists("%s.xpt" % module_name) + + mk.add_statement("all_idl_dirs := %s" % " ".join(sorted(all_directories))) + + rules = StringIO() + mk.dump(rules, removal_guard=False) + + # Create dependency for output header so we force regeneration if the + # header was deleted. This ideally should not be necessary. However, + # some processes (such as PGO at the time this was implemented) wipe + # out dist/include without regard to our install manifests. + + obj = self.Substitution() + obj.output_path = mozpath.join( + self.environment.topobjdir, "config", "makefiles", "xpidl", "Makefile" + ) + obj.input_path = mozpath.join( + self.environment.topsrcdir, "config", "makefiles", "xpidl", "Makefile.in" + ) + obj.topsrcdir = self.environment.topsrcdir + obj.topobjdir = self.environment.topobjdir + obj.config = self.environment + self._create_makefile( + obj, + extra=dict( + xpidl_rules=rules.getvalue(), xpidl_modules=" ".join(xpt_modules) + ), + ) + + def _process_program(self, obj, backend_file): + backend_file.write( + "PROGRAM = %s\n" % self._pretty_path(obj.output_path, backend_file) + ) + if not obj.cxx_link and not self.environment.bin_suffix: + backend_file.write("PROG_IS_C_ONLY_%s := 1\n" % obj.program) + + def _process_host_program(self, program, backend_file): + backend_file.write( + "HOST_PROGRAM = %s\n" % self._pretty_path(program.output_path, backend_file) + ) + + def _process_rust_program_base( + self, obj, backend_file, target_variable, target_cargo_variable + ): + backend_file.write_once("CARGO_FILE := %s\n" % obj.cargo_file) + target_dir = mozpath.normpath(backend_file.environment.topobjdir) + backend_file.write_once("CARGO_TARGET_DIR := %s\n" % target_dir) + backend_file.write("%s += $(DEPTH)/%s\n" % (target_variable, obj.location)) + backend_file.write("%s += %s\n" % (target_cargo_variable, obj.name)) + + def _process_rust_program(self, obj, backend_file): + self._process_rust_program_base( + obj, backend_file, "RUST_PROGRAMS", "RUST_CARGO_PROGRAMS" + ) + + def _process_host_rust_program(self, obj, backend_file): + self._process_rust_program_base( + obj, backend_file, "HOST_RUST_PROGRAMS", "HOST_RUST_CARGO_PROGRAMS" + ) + + def _process_rust_tests(self, obj, backend_file): + if obj.config.substs.get("MOZ_RUST_TESTS"): + # If --enable-rust-tests has been set, run these as a part of + # make check. + self._no_skip["check"].add(backend_file.relobjdir) + backend_file.write("check:: force-cargo-test-run\n") + build_target = self._build_target_for_obj(obj) + self._compile_graph[build_target] + self._process_non_default_target(obj, "force-cargo-test-run", backend_file) + backend_file.write_once("CARGO_FILE := $(srcdir)/Cargo.toml\n") + backend_file.write_once("RUST_TESTS := %s\n" % " ".join(obj.names)) + backend_file.write_once("RUST_TEST_FEATURES := %s\n" % " ".join(obj.features)) + + def _process_simple_program(self, obj, backend_file): + if obj.is_unit_test: + backend_file.write("CPP_UNIT_TESTS += %s\n" % obj.program) + assert obj.cxx_link + else: + backend_file.write("SIMPLE_PROGRAMS += %s\n" % obj.program) + if not obj.cxx_link and not self.environment.bin_suffix: + backend_file.write("PROG_IS_C_ONLY_%s := 1\n" % obj.program) + + def _process_host_simple_program(self, program, backend_file): + backend_file.write("HOST_SIMPLE_PROGRAMS += %s\n" % program) + + def _process_relrhack(self, obj): + if isinstance( + obj, (SimpleProgram, Program, SharedLibrary) + ) and obj.config.substs.get("RELRHACK"): + # When building with RELR-based ELF hack, we need to build the relevant parts + # before any target. + node = self._compile_graph[self._build_target_for_obj(obj)] + node.add("build/unix/elfhack/host") + node.add("build/unix/elfhack/inject/target-objects") + + def _process_test_support_file(self, obj): + # Ensure test support programs and libraries are tracked by an + # install manifest for the benefit of the test packager. + if not obj.install_target.startswith("_tests"): + return + + dest_basename = None + if isinstance(obj, BaseLibrary): + dest_basename = obj.lib_name + elif isinstance(obj, BaseProgram): + dest_basename = obj.program + if dest_basename is None: + return + + self._install_manifests["_tests"].add_optional_exists( + mozpath.join(obj.install_target[len("_tests") + 1 :], dest_basename) + ) + + def _process_test_manifest(self, obj, backend_file): + # Much of the logic in this function could be moved to CommonBackend. + for source in obj.source_relpaths: + self.backend_input_files.add(mozpath.join(obj.topsrcdir, source)) + + # Don't allow files to be defined multiple times unless it is allowed. + # We currently allow duplicates for non-test files or test files if + # the manifest is listed as a duplicate. + for source, (dest, is_test) in obj.installs.items(): + try: + self._install_manifests["_test_files"].add_link(source, dest) + except ValueError: + if not obj.dupe_manifest and is_test: + raise + + for base, pattern, dest in obj.pattern_installs: + try: + self._install_manifests["_test_files"].add_pattern_link( + base, pattern, dest + ) + except ValueError: + if not obj.dupe_manifest: + raise + + for dest in obj.external_installs: + try: + self._install_manifests["_test_files"].add_optional_exists(dest) + except ValueError: + if not obj.dupe_manifest: + raise + + m = self._test_manifests.setdefault(obj.flavor, (obj.install_prefix, set())) + m[1].add(obj.manifest_obj_relpath) + + try: + from reftest import ReftestManifest + + if isinstance(obj.manifest, ReftestManifest): + # Mark included files as part of the build backend so changes + # result in re-config. + self.backend_input_files |= obj.manifest.manifests + except ImportError: + # Ignore errors caused by the reftest module not being present. + # This can happen when building SpiderMonkey standalone, for example. + pass + + def _process_local_include(self, local_include, backend_file): + d, path = self._pretty_path_parts(local_include, backend_file) + if isinstance(local_include, ObjDirPath) and not d: + # path doesn't start with a slash in this case + d = "$(CURDIR)/" + elif d == "$(DEPTH)": + d = "$(topobjdir)" + quoted_path = shell_quote(path) if path else path + if quoted_path != path: + path = quoted_path[0] + d + quoted_path[1:] + else: + path = d + path + backend_file.write("LOCAL_INCLUDES += -I%s\n" % path) + + def _process_per_source_flag(self, per_source_flag, backend_file): + for flag in per_source_flag.flags: + backend_file.write( + "%s_FLAGS += %s\n" % (mozpath.basename(per_source_flag.file_name), flag) + ) + + def _process_computed_flags(self, computed_flags, backend_file): + for var, flags in computed_flags.get_flags(): + backend_file.write( + "COMPUTED_%s += %s\n" + % (var, " ".join(make_quote(shell_quote(f)) for f in flags)) + ) + + def _process_non_default_target(self, libdef, target_name, backend_file): + backend_file.write("%s:: %s\n" % (libdef.output_category, target_name)) + backend_file.write("MOZBUILD_NON_DEFAULT_TARGETS += %s\n" % target_name) + + def _process_shared_library(self, libdef, backend_file): + backend_file.write_once("LIBRARY_NAME := %s\n" % libdef.basename) + backend_file.write("FORCE_SHARED_LIB := 1\n") + backend_file.write("IMPORT_LIBRARY := %s\n" % libdef.import_name) + backend_file.write("SHARED_LIBRARY := %s\n" % libdef.lib_name) + if libdef.soname: + backend_file.write("DSO_SONAME := %s\n" % libdef.soname) + if libdef.symbols_file: + if libdef.symbols_link_arg: + backend_file.write("EXTRA_DSO_LDOPTS += %s\n" % libdef.symbols_link_arg) + if not libdef.cxx_link: + backend_file.write("LIB_IS_C_ONLY := 1\n") + if libdef.output_category: + self._process_non_default_target(libdef, libdef.lib_name, backend_file) + # Override the install rule target for this library. This is hacky, + # but can go away as soon as we start building libraries in their + # final location (bug 1459764). + backend_file.write("SHARED_LIBRARY_TARGET := %s\n" % libdef.output_category) + + def _process_static_library(self, libdef, backend_file): + backend_file.write_once("LIBRARY_NAME := %s\n" % libdef.basename) + backend_file.write("FORCE_STATIC_LIB := 1\n") + backend_file.write("REAL_LIBRARY := %s\n" % libdef.lib_name) + if libdef.no_expand_lib: + backend_file.write("NO_EXPAND_LIBS := 1\n") + + def _process_sandboxed_wasm_library(self, libdef, backend_file): + backend_file.write("WASM_ARCHIVE := %s\n" % libdef.basename) + + def _process_rust_library(self, libdef, backend_file): + backend_file.write_once( + "%s := %s\n" % (libdef.LIB_FILE_VAR, libdef.import_name) + ) + backend_file.write_once("CARGO_FILE := $(srcdir)/Cargo.toml\n") + # Need to normalize the path so Cargo sees the same paths from all + # possible invocations of Cargo with this CARGO_TARGET_DIR. Otherwise, + # Cargo's dependency calculations don't work as we expect and we wind + # up recompiling lots of things. + target_dir = mozpath.normpath(backend_file.environment.topobjdir) + backend_file.write("CARGO_TARGET_DIR := %s\n" % target_dir) + if libdef.features: + backend_file.write( + "%s := %s\n" % (libdef.FEATURES_VAR, " ".join(libdef.features)) + ) + if libdef.output_category: + self._process_non_default_target(libdef, libdef.import_name, backend_file) + + def _process_host_shared_library(self, libdef, backend_file): + backend_file.write("HOST_SHARED_LIBRARY = %s\n" % libdef.lib_name) + + def _build_target_for_obj(self, obj): + if hasattr(obj, "output_category") and obj.output_category: + target_name = obj.output_category + elif isinstance(obj, BaseRustLibrary): + target_name = f"{obj.KIND}-objects" + else: + target_name = obj.KIND + if target_name == "wasm": + target_name = "target" + return "%s/%s" % ( + mozpath.relpath(obj.objdir, self.environment.topobjdir), + target_name, + ) + + def _process_linked_libraries(self, obj, backend_file): + def pretty_relpath(lib, name): + return os.path.normpath( + mozpath.join(mozpath.relpath(lib.objdir, obj.objdir), name) + ) + + objs, shared_libs, os_libs, static_libs = self._expand_libs(obj) + + obj_target = obj.name + if isinstance(obj, Program): + obj_target = self._pretty_path(obj.output_path, backend_file) + + objs_ref = " \\\n ".join(os.path.relpath(o, obj.objdir) for o in objs) + if isinstance(obj, (SimpleProgram, Program, SharedLibrary)): + # Don't bother with a list file if we're only linking objects built + # in this directory. + if objs == obj.objs: + backend_file.write_once("%s_OBJS := %s\n" % (obj.name, objs_ref)) + else: + list_file_path = "%s.list" % obj.name.replace(".", "_") + list_file_ref = self._make_list_file( + obj.KIND, obj.objdir, objs, list_file_path + ) + backend_file.write_once("%s_OBJS := %s\n" % (obj.name, list_file_ref)) + backend_file.write_once("%s: %s\n" % (obj_target, list_file_path)) + backend_file.write("%s: %s\n" % (obj_target, objs_ref)) + + elif ( + not isinstance( + obj, + ( + HostLibrary, + HostRustProgram, + RustProgram, + StaticLibrary, + SandboxedWasmLibrary, + ), + ) + or isinstance(obj, (StaticLibrary, SandboxedWasmLibrary)) + and obj.no_expand_lib + ): + response_file_path = "%s.list" % obj.name.replace(".", "_") + response_file_ref = self._make_ar_response_file( + obj.objdir, objs, response_file_path + ) + if response_file_ref: + backend_file.write_once( + "%s_OBJS := %s\n" % (obj.name, response_file_ref) + ) + backend_file.write_once("%s: %s\n" % (obj_target, response_file_path)) + else: + backend_file.write_once("%s_OBJS := %s\n" % (obj.name, objs_ref)) + backend_file.write("%s: %s\n" % (obj_target, objs_ref)) + if getattr(obj, "symbols_file", None): + backend_file.write_once("%s: %s\n" % (obj_target, obj.symbols_file)) + + for lib in shared_libs: + assert obj.KIND != "host" and obj.KIND != "wasm" + backend_file.write_once( + "SHARED_LIBS += %s\n" % pretty_relpath(lib, lib.import_name) + ) + + # We have to link any Rust libraries after all intermediate static + # libraries have been listed to ensure that the Rust libraries are + # searched after the C/C++ objects that might reference Rust symbols. + var = "HOST_LIBS" if obj.KIND == "host" else "STATIC_LIBS" + for lib in chain( + (l for l in static_libs if not isinstance(l, BaseRustLibrary)), + (l for l in static_libs if isinstance(l, BaseRustLibrary)), + ): + backend_file.write_once( + "%s += %s\n" % (var, pretty_relpath(lib, lib.import_name)) + ) + + for lib in os_libs: + if obj.KIND == "target": + backend_file.write_once("OS_LIBS += %s\n" % lib) + elif obj.KIND == "host": + backend_file.write_once("HOST_EXTRA_LIBS += %s\n" % lib) + + if not isinstance(obj, (StaticLibrary, HostLibrary)) or obj.no_expand_lib: + # This will create the node even if there aren't any linked libraries. + build_target = self._build_target_for_obj(obj) + self._compile_graph[build_target] + + # Make the build target depend on all the target/host-objects that + # recursively are linked into it. + def recurse_libraries(obj): + for lib in obj.linked_libraries: + if ( + isinstance(lib, (StaticLibrary, HostLibrary)) + and not lib.no_expand_lib + ): + recurse_libraries(lib) + elif not isinstance(lib, ExternalLibrary): + self._compile_graph[build_target].add( + self._build_target_for_obj(lib) + ) + relobjdir = mozpath.relpath(obj.objdir, self.environment.topobjdir) + objects_target = mozpath.join(relobjdir, "%s-objects" % obj.KIND) + if objects_target in self._compile_graph: + self._compile_graph[build_target].add(objects_target) + + recurse_libraries(obj) + + # Process library-based defines + self._process_defines(obj.lib_defines, backend_file) + + def _add_install_target(self, backend_file, install_target, tier, dest, files): + self._no_skip[tier].add(backend_file.relobjdir) + for f in files: + backend_file.write("%s_FILES += %s\n" % (install_target, f)) + backend_file.write("%s_DEST := %s\n" % (install_target, dest)) + backend_file.write("%s_TARGET := %s\n" % (install_target, tier)) + backend_file.write("INSTALL_TARGETS += %s\n" % install_target) + + def _process_final_target_files(self, obj, files, backend_file): + target = obj.install_target + path = mozpath.basedir( + target, ("dist/bin", "dist/xpi-stage", "_tests", "dist/include") + ) + if not path: + raise Exception("Cannot install to " + target) + + # Exports are not interesting to artifact builds. + if path == "dist/include" and self.environment.is_artifact_build: + return + + manifest = path.replace("/", "_") + install_manifest = self._install_manifests[manifest] + reltarget = mozpath.relpath(target, path) + + for path, files in files.walk(): + target_var = (mozpath.join(target, path) if path else target).replace( + "/", "_" + ) + # We don't necessarily want to combine these, because non-wildcard + # absolute files tend to be libraries, and we don't want to mix + # those in with objdir headers that will be installed during export. + # (See bug 1642882 for details.) + objdir_files = [] + absolute_files = [] + + for f in files: + assert not isinstance(f, RenamedSourcePath) + dest_dir = mozpath.join(reltarget, path) + dest_file = mozpath.join(dest_dir, f.target_basename) + if not isinstance(f, ObjDirPath): + if "*" in f: + if f.startswith("/") or isinstance(f, AbsolutePath): + basepath, wild = os.path.split(f.full_path) + if "*" in basepath: + raise Exception( + "Wildcards are only supported in the filename part" + " of srcdir-relative or absolute paths." + ) + + install_manifest.add_pattern_link(basepath, wild, dest_dir) + else: + install_manifest.add_pattern_link(f.srcdir, f, dest_dir) + elif isinstance(f, AbsolutePath): + if not f.full_path.lower().endswith((".dll", ".pdb", ".so")): + raise Exception( + "Absolute paths installed to FINAL_TARGET_FILES must" + " only be shared libraries or associated debug" + " information." + ) + install_manifest.add_optional_exists(dest_file) + absolute_files.append(f.full_path) + else: + install_manifest.add_link(f.full_path, dest_file) + else: + install_manifest.add_optional_exists(dest_file) + objdir_files.append(self._pretty_path(f, backend_file)) + install_location = "$(DEPTH)/%s" % mozpath.join(target, path) + if objdir_files: + tier = "export" if obj.install_target == "dist/include" else "misc" + # We cannot generate multilocale.txt during misc at the moment. + if objdir_files[0] == "multilocale.txt": + tier = "libs" + self._add_install_target( + backend_file, target_var, tier, install_location, objdir_files + ) + if absolute_files: + # Unfortunately, we can't use _add_install_target because on + # Windows, the absolute file paths that we want to install + # from often have spaces. So we write our own rule. + self._no_skip["misc"].add(backend_file.relobjdir) + backend_file.write( + "misc::\n%s\n" + % "\n".join( + "\t$(INSTALL) %s %s" + % (make_quote(shell_quote(f)), install_location) + for f in absolute_files + ) + ) + + def _process_final_target_pp_files(self, obj, files, backend_file, name): + # Bug 1177710 - We'd like to install these via manifests as + # preprocessed files. But they currently depend on non-standard flags + # being added via some Makefiles, so for now we just pass them through + # to the underlying Makefile.in. + # + # Note that if this becomes a manifest, OBJDIR_PP_FILES will likely + # still need to use PP_TARGETS internally because we can't have an + # install manifest for the root of the objdir. + for i, (path, files) in enumerate(files.walk()): + self._no_skip["misc"].add(backend_file.relobjdir) + var = "%s_%d" % (name, i) + for f in files: + backend_file.write( + "%s += %s\n" % (var, self._pretty_path(f, backend_file)) + ) + backend_file.write( + "%s_PATH := $(DEPTH)/%s\n" + % (var, mozpath.join(obj.install_target, path)) + ) + backend_file.write("%s_TARGET := misc\n" % var) + backend_file.write("PP_TARGETS += %s\n" % var) + + def _write_localized_files_files(self, files, name, backend_file): + for f in files: + if not isinstance(f, ObjDirPath): + # The emitter asserts that all srcdir files start with `en-US/` + e, f = f.split("en-US/") + assert not e + if "*" in f: + # We can't use MERGE_FILE for wildcards because it takes + # only the first match internally. This is only used + # in one place in the tree currently so we'll hardcode + # that specific behavior for now. + backend_file.write( + "%s += $(wildcard $(LOCALE_SRCDIR)/%s)\n" % (name, f) + ) + else: + backend_file.write("%s += $(call MERGE_FILE,%s)\n" % (name, f)) + else: + # Objdir files are allowed from LOCALIZED_GENERATED_FILES + backend_file.write( + "%s += %s\n" % (name, self._pretty_path(f, backend_file)) + ) + + def _process_localized_files(self, obj, files, backend_file): + target = obj.install_target + path = mozpath.basedir(target, ("dist/bin",)) + if not path: + raise Exception("Cannot install localized files to " + target) + for i, (path, files) in enumerate(files.walk()): + name = "LOCALIZED_FILES_%d" % i + self._no_skip["misc"].add(backend_file.relobjdir) + self._write_localized_files_files(files, name + "_FILES", backend_file) + # Use FINAL_TARGET here because some l10n repack rules set + # XPI_NAME to generate langpacks. + backend_file.write("%s_DEST = $(FINAL_TARGET)/%s\n" % (name, path)) + backend_file.write("%s_TARGET := misc\n" % name) + backend_file.write("INSTALL_TARGETS += %s\n" % name) + + def _process_localized_pp_files(self, obj, files, backend_file): + target = obj.install_target + path = mozpath.basedir(target, ("dist/bin",)) + if not path: + raise Exception("Cannot install localized files to " + target) + for i, (path, files) in enumerate(files.walk()): + name = "LOCALIZED_PP_FILES_%d" % i + self._no_skip["misc"].add(backend_file.relobjdir) + self._write_localized_files_files(files, name, backend_file) + # Use FINAL_TARGET here because some l10n repack rules set + # XPI_NAME to generate langpacks. + backend_file.write("%s_PATH = $(FINAL_TARGET)/%s\n" % (name, path)) + backend_file.write("%s_TARGET := misc\n" % name) + # Localized files will have different content in different + # localizations, and some preprocessed files may not have + # any preprocessor directives. + backend_file.write( + "%s_FLAGS := --silence-missing-directive-warnings\n" % name + ) + backend_file.write("PP_TARGETS += %s\n" % name) + + def _process_objdir_files(self, obj, files, backend_file): + # We can't use an install manifest for the root of the objdir, since it + # would delete all the other files that get put there by the build + # system. + for i, (path, files) in enumerate(files.walk()): + self._no_skip["misc"].add(backend_file.relobjdir) + for f in files: + backend_file.write( + "OBJDIR_%d_FILES += %s\n" % (i, self._pretty_path(f, backend_file)) + ) + backend_file.write("OBJDIR_%d_DEST := $(topobjdir)/%s\n" % (i, path)) + backend_file.write("OBJDIR_%d_TARGET := misc\n" % i) + backend_file.write("INSTALL_TARGETS += OBJDIR_%d\n" % i) + + def _process_chrome_manifest_entry(self, obj, backend_file): + fragment = Makefile() + rule = fragment.create_rule(targets=["misc:"]) + + top_level = mozpath.join(obj.install_target, "chrome.manifest") + if obj.path != top_level: + path = mozpath.relpath(obj.path, obj.install_target) + args = [ + mozpath.join("$(DEPTH)", top_level), + make_quote(shell_quote("manifest %s" % path)), + ] + rule.add_commands( + ["$(call py_action,buildlist %s,%s)" % (path, " ".join(args))] + ) + args = [ + mozpath.join("$(DEPTH)", obj.path), + make_quote(shell_quote(str(obj.entry))), + ] + rule.add_commands( + ["$(call py_action,buildlist %s,%s)" % (obj.entry.path, " ".join(args))] + ) + fragment.dump(backend_file.fh, removal_guard=False) + + self._no_skip["misc"].add(obj.relsrcdir) + + def _write_manifests(self, dest, manifests): + man_dir = mozpath.join(self.environment.topobjdir, "_build_manifests", dest) + + for k, manifest in manifests.items(): + with self._write_file(mozpath.join(man_dir, k)) as fh: + manifest.write(fileobj=fh) + + def _write_master_test_manifest(self, path, manifests): + with self._write_file(path) as master: + master.write( + "# THIS FILE WAS AUTOMATICALLY GENERATED. DO NOT MODIFY BY HAND.\n\n" + ) + + for manifest in sorted(manifests): + master.write('["include:%s"]\n' % manifest) + + class Substitution(object): + """BaseConfigSubstitution-like class for use with _create_makefile.""" + + __slots__ = ("input_path", "output_path", "topsrcdir", "topobjdir", "config") + + def _create_makefile(self, obj, stub=False, extra=None): + """Creates the given makefile. Makefiles are treated the same as + config files, but some additional header and footer is added to the + output. + + When the stub argument is True, no source file is used, and a stub + makefile with the default header and footer only is created. + """ + with self._get_preprocessor(obj) as pp: + if extra: + pp.context.update(extra) + if not pp.context.get("autoconfmk", ""): + pp.context["autoconfmk"] = "autoconf.mk" + pp.handleLine( + "# THIS FILE WAS AUTOMATICALLY GENERATED. DO NOT MODIFY BY HAND.\n" + ) + pp.handleLine("DEPTH := @DEPTH@\n") + pp.handleLine("topobjdir := @topobjdir@\n") + pp.handleLine("topsrcdir := @top_srcdir@\n") + pp.handleLine("srcdir := @srcdir@\n") + pp.handleLine("srcdir_rel := @srcdir_rel@\n") + pp.handleLine("relativesrcdir := @relativesrcdir@\n") + pp.handleLine("include $(DEPTH)/config/@autoconfmk@\n") + if not stub: + pp.do_include(obj.input_path) + # Empty line to avoid failures when last line in Makefile.in ends + # with a backslash. + pp.handleLine("\n") + pp.handleLine("include $(topsrcdir)/config/recurse.mk\n") + if not stub: + # Adding the Makefile.in here has the desired side-effect + # that if the Makefile.in disappears, this will force + # moz.build traversal. This means that when we remove empty + # Makefile.in files, the old file will get replaced with + # the autogenerated one automatically. + self.backend_input_files.add(obj.input_path) + + self._makefile_out_count += 1 + + def _handle_linked_rust_crates(self, obj, extern_crate_file): + backend_file = self._get_backend_file_for(obj) + + backend_file.write("RS_STATICLIB_CRATE_SRC := %s\n" % extern_crate_file) + + def _handle_ipdl_sources( + self, + ipdl_dir, + sorted_ipdl_sources, + sorted_nonstatic_ipdl_sources, + sorted_static_ipdl_sources, + ): + # Write out a master list of all IPDL source files. + mk = Makefile() + + sorted_nonstatic_ipdl_basenames = list() + for source in sorted_nonstatic_ipdl_sources: + basename = os.path.basename(source) + sorted_nonstatic_ipdl_basenames.append(basename) + rule = mk.create_rule([basename]) + rule.add_dependencies([source]) + rule.add_commands( + [ + "$(RM) $@", + "$(call py_action,preprocessor $@,$(DEFINES) $(ACDEFINES) " + "$< -o $@)", + ] + ) + + mk.add_statement( + "ALL_IPDLSRCS := %s %s" + % ( + " ".join(sorted_nonstatic_ipdl_basenames), + " ".join(sorted_static_ipdl_sources), + ) + ) + + # Preprocessed ipdl files are generated in ipdl_dir. + mk.add_statement( + "IPDLDIRS := %s %s" + % ( + ipdl_dir, + " ".join( + sorted(set(mozpath.dirname(p) for p in sorted_static_ipdl_sources)) + ), + ) + ) + + with self._write_file(mozpath.join(ipdl_dir, "ipdlsrcs.mk")) as ipdls: + mk.dump(ipdls, removal_guard=False) + + def _handle_webidl_build( + self, + bindings_dir, + unified_source_mapping, + webidls, + expected_build_output_files, + global_define_files, + ): + include_dir = mozpath.join(self.environment.topobjdir, "dist", "include") + for f in expected_build_output_files: + if f.startswith(include_dir): + self._install_manifests["dist_include"].add_optional_exists( + mozpath.relpath(f, include_dir) + ) + + # We pass WebIDL info to make via a completely generated make file. + mk = Makefile() + mk.add_statement( + "nonstatic_webidl_files := %s" + % " ".join(sorted(webidls.all_non_static_basenames())) + ) + mk.add_statement( + "globalgen_sources := %s" % " ".join(sorted(global_define_files)) + ) + mk.add_statement( + "test_sources := %s" + % " ".join(sorted("%sBinding.cpp" % s for s in webidls.all_test_stems())) + ) + + # Add rules to preprocess bindings. + # This should ideally be using PP_TARGETS. However, since the input + # filenames match the output filenames, the existing PP_TARGETS rules + # result in circular dependencies and other make weirdness. One + # solution is to rename the input or output files repsectively. See + # bug 928195 comment 129. + for source in sorted(webidls.all_preprocessed_sources()): + basename = os.path.basename(source) + rule = mk.create_rule([basename]) + # GLOBAL_DEPS would be used here, but due to the include order of + # our makefiles it's not set early enough to be useful, so we use + # WEBIDL_PP_DEPS, which has analagous content. + rule.add_dependencies([source, "$(WEBIDL_PP_DEPS)"]) + rule.add_commands( + [ + # Remove the file before writing so bindings that go from + # static to preprocessed don't end up writing to a symlink, + # which would modify content in the source directory. + "$(RM) $@", + "$(call py_action,preprocessor $@,$(DEFINES) $(ACDEFINES) " + "$< -o $@)", + ] + ) + + self._add_unified_build_rules( + mk, + unified_source_mapping, + unified_files_makefile_variable="unified_binding_cpp_files", + ) + + webidls_mk = mozpath.join(bindings_dir, "webidlsrcs.mk") + with self._write_file(webidls_mk) as fh: + mk.dump(fh, removal_guard=False) + + # Add the test directory to the compile graph. + if self.environment.substs.get("ENABLE_TESTS"): + self._compile_graph[ + mozpath.join( + mozpath.relpath(bindings_dir, self.environment.topobjdir), + "test", + "target-objects", + ) + ] + + def _format_generated_file_input_name(self, path, obj): + if obj.localized: + # Localized generated files can have locale-specific inputs, which + # are indicated by paths starting with `en-US/` or containing + # `locales/en-US/`. + if "locales/en-US" in path: + # We need an "absolute source path" relative to + # topsrcdir, like "/source/path". + if not path.startswith("/"): + path = "/" + mozpath.relpath(path.full_path, obj.topsrcdir) + e, f = path.split("locales/en-US/", 1) + assert f + return "$(call MERGE_RELATIVE_FILE,{},{}locales)".format( + f, e if not e.startswith("/") else e[len("/") :] + ) + elif path.startswith("en-US/"): + e, f = path.split("en-US/", 1) + assert not e + return "$(call MERGE_FILE,%s)" % f + return self._pretty_path(path, self._get_backend_file_for(obj)) + else: + return self._pretty_path(path, self._get_backend_file_for(obj)) + + def _format_generated_file_output_name(self, path, obj): + if not isinstance(path, Path): + path = ObjDirPath(obj._context, "!" + path) + return self._pretty_path(path, self._get_backend_file_for(obj)) diff --git a/python/mozbuild/mozbuild/backend/static_analysis.py b/python/mozbuild/mozbuild/backend/static_analysis.py new file mode 100644 index 0000000000..2b3ce96e75 --- /dev/null +++ b/python/mozbuild/mozbuild/backend/static_analysis.py @@ -0,0 +1,52 @@ +# 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/. + +# This module provides a backend static-analysis, like clang-tidy and coverity. +# The main difference between this and the default database backend is that this one +# tracks folders that can be built in the non-unified environment and generates +# the coresponding build commands for the files. + +import os + +import mozpack.path as mozpath + +from mozbuild.compilation.database import CompileDBBackend + + +class StaticAnalysisBackend(CompileDBBackend): + def _init(self): + CompileDBBackend._init(self) + self.non_unified_build = [] + + # List of directories can be built outside of the unified build system. + with open( + mozpath.join(self.environment.topsrcdir, "build", "non-unified-compat") + ) as fh: + content = fh.readlines() + self.non_unified_build = [ + mozpath.join(self.environment.topsrcdir, line.strip()) + for line in content + ] + + def _build_cmd(self, cmd, filename, unified): + cmd = list(cmd) + # Maybe the file is in non-unified environment or it resides under a directory + # that can also be built in non-unified environment + if unified is None or any( + filename.startswith(path) for path in self.non_unified_build + ): + cmd.append(filename) + else: + cmd.append(unified) + + return cmd + + def _outputfile_path(self): + database_path = os.path.join(self.environment.topobjdir, "static-analysis") + + if not os.path.exists(database_path): + os.mkdir(database_path) + + # Output the database (a JSON file) to objdir/static-analysis/compile_commands.json + return mozpath.join(database_path, "compile_commands.json") diff --git a/python/mozbuild/mozbuild/backend/test_manifest.py b/python/mozbuild/mozbuild/backend/test_manifest.py new file mode 100644 index 0000000000..ba1e5135f4 --- /dev/null +++ b/python/mozbuild/mozbuild/backend/test_manifest.py @@ -0,0 +1,110 @@ +# 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 collections import defaultdict + +import mozpack.path as mozpath +import six +import six.moves.cPickle as pickle + +from mozbuild.backend.base import PartialBackend +from mozbuild.frontend.data import TestManifest + + +class TestManifestBackend(PartialBackend): + """Partial backend that generates test metadata files.""" + + def _init(self): + self.tests_by_path = defaultdict(list) + self.installs_by_path = defaultdict(list) + self.deferred_installs = set() + self.manifest_defaults = {} + + # Add config.status so performing a build will invalidate this backend. + self.backend_input_files.add( + mozpath.join(self.environment.topobjdir, "config.status") + ) + + def consume_object(self, obj): + if not isinstance(obj, TestManifest): + return + + self.backend_input_files.add(obj.path) + self.backend_input_files |= obj.context_all_paths + for source in obj.source_relpaths: + self.backend_input_files.add(mozpath.join(obj.topsrcdir, source)) + try: + from reftest import ReftestManifest + + if isinstance(obj.manifest, ReftestManifest): + # Mark included files as part of the build backend so changes + # result in re-config. + self.backend_input_files |= obj.manifest.manifests + except ImportError: + # Ignore errors caused by the reftest module not being present. + # This can happen when building SpiderMonkey standalone, for example. + pass + + for test in obj.tests: + self.add(test, obj.flavor, obj.topsrcdir) + self.add_defaults(obj.manifest) + self.add_installs(obj, obj.topsrcdir) + + def consume_finished(self): + topobjdir = self.environment.topobjdir + + with self._write_file( + mozpath.join(topobjdir, "all-tests.pkl"), readmode="rb" + ) as fh: + pickle.dump(dict(self.tests_by_path), fh, protocol=2) + + with self._write_file( + mozpath.join(topobjdir, "test-defaults.pkl"), readmode="rb" + ) as fh: + pickle.dump(self.manifest_defaults, fh, protocol=2) + + path = mozpath.join(topobjdir, "test-installs.pkl") + with self._write_file(path, readmode="rb") as fh: + pickle.dump( + { + k: v + for k, v in self.installs_by_path.items() + if k in self.deferred_installs + }, + fh, + protocol=2, + ) + + def add(self, t, flavor, topsrcdir): + t = dict(t) + t["flavor"] = flavor + + path = mozpath.normpath(t["path"]) + manifest = mozpath.normpath(t["manifest"]) + assert mozpath.basedir(path, [topsrcdir]) + assert mozpath.basedir(manifest, [topsrcdir]) + + key = path[len(topsrcdir) + 1 :] + t["file_relpath"] = key + t["dir_relpath"] = mozpath.dirname(key) + t["srcdir_relpath"] = key + t["manifest_relpath"] = manifest[len(topsrcdir) + 1 :] + + self.tests_by_path[key].append(t) + + def add_defaults(self, manifest): + if not hasattr(manifest, "manifest_defaults"): + return + for sub_manifest, defaults in manifest.manifest_defaults.items(): + self.manifest_defaults[sub_manifest] = defaults + + def add_installs(self, obj, topsrcdir): + for src, (dest, _) in six.iteritems(obj.installs): + key = src[len(topsrcdir) + 1 :] + self.installs_by_path[key].append((src, dest)) + for src, pat, dest in obj.pattern_installs: + key = mozpath.join(src[len(topsrcdir) + 1 :], pat) + self.installs_by_path[key].append((src, pat, dest)) + for path in obj.deferred_installs: + self.deferred_installs.add(path[2:]) diff --git a/python/mozbuild/mozbuild/backend/visualstudio.py b/python/mozbuild/mozbuild/backend/visualstudio.py new file mode 100644 index 0000000000..ccf3c65a68 --- /dev/null +++ b/python/mozbuild/mozbuild/backend/visualstudio.py @@ -0,0 +1,710 @@ +# 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/. + +# This file contains a build backend for generating Visual Studio project +# files. + +import errno +import os +import re +import sys +import uuid +from pathlib import Path +from xml.dom import getDOMImplementation + +from mozpack.files import FileFinder + +from mozbuild.base import ExecutionSummary + +from ..frontend.data import ( + Defines, + HostProgram, + HostSources, + Library, + LocalInclude, + Program, + SandboxedWasmLibrary, + Sources, + UnifiedSources, +) +from .common import CommonBackend + +MSBUILD_NAMESPACE = "http://schemas.microsoft.com/developer/msbuild/2003" +MSNATVIS_NAMESPACE = "http://schemas.microsoft.com/vstudio/debugger/natvis/2010" + + +def get_id(name): + if sys.version_info[0] == 2: + name = name.encode("utf-8") + return str(uuid.uuid5(uuid.NAMESPACE_URL, name)).upper() + + +def visual_studio_product_to_solution_version(version): + if version == "2017": + return "12.00", "15" + elif version == "2019": + return "12.00", "16" + elif version == "2022": + return "12.00", "17" + else: + raise Exception("Unknown version seen: %s" % version) + + +def visual_studio_product_to_platform_toolset_version(version): + if version == "2017": + return "v141" + elif version == "2019": + return "v142" + elif version == "2022": + return "v143" + else: + raise Exception("Unknown version seen: %s" % version) + + +class VisualStudioBackend(CommonBackend): + """Generate Visual Studio project files. + + This backend is used to produce Visual Studio projects and a solution + to foster developing Firefox with Visual Studio. + + This backend is currently considered experimental. There are many things + not optimal about how it works. + """ + + def _init(self): + CommonBackend._init(self) + + # These should eventually evolve into parameters. + self._out_dir = os.path.join(self.environment.topobjdir, "msvc") + self._projsubdir = "projects" + + self._version = self.environment.substs.get("MSVS_VERSION", "2017") + + self._paths_to_sources = {} + self._paths_to_includes = {} + self._paths_to_defines = {} + self._paths_to_configs = {} + self._libs_to_paths = {} + self._progs_to_paths = {} + + def summary(self): + return ExecutionSummary( + "VisualStudio backend executed in {execution_time:.2f}s\n" + "Generated Visual Studio solution at {path:s}", + execution_time=self._execution_time, + path=os.path.join(self._out_dir, "mozilla.sln"), + ) + + def consume_object(self, obj): + reldir = getattr(obj, "relsrcdir", None) + + if hasattr(obj, "config") and reldir not in self._paths_to_configs: + self._paths_to_configs[reldir] = obj.config + + if isinstance(obj, Sources): + self._add_sources(reldir, obj) + + elif isinstance(obj, HostSources): + self._add_sources(reldir, obj) + + elif isinstance(obj, UnifiedSources): + # XXX we should be letting CommonBackend.consume_object call this + # for us instead. + self._process_unified_sources(obj) + + elif isinstance(obj, Library) and not isinstance(obj, SandboxedWasmLibrary): + self._libs_to_paths[obj.basename] = reldir + + elif isinstance(obj, Program) or isinstance(obj, HostProgram): + self._progs_to_paths[obj.program] = reldir + + elif isinstance(obj, Defines): + self._paths_to_defines.setdefault(reldir, {}).update(obj.defines) + + elif isinstance(obj, LocalInclude): + includes = self._paths_to_includes.setdefault(reldir, []) + includes.append(obj.path.full_path) + + # Just acknowledge everything. + return True + + def _add_sources(self, reldir, obj): + s = self._paths_to_sources.setdefault(reldir, set()) + s.update(obj.files) + + def _process_unified_sources(self, obj): + reldir = getattr(obj, "relsrcdir", None) + + s = self._paths_to_sources.setdefault(reldir, set()) + s.update(obj.files) + + def consume_finished(self): + out_dir = self._out_dir + out_proj_dir = os.path.join(self._out_dir, self._projsubdir) + + projects = self._write_projects_for_sources( + self._libs_to_paths, "library", out_proj_dir + ) + projects.update( + self._write_projects_for_sources( + self._progs_to_paths, "binary", out_proj_dir + ) + ) + + # Generate projects that can be used to build common targets. + for target in ("export", "binaries", "tools", "full"): + basename = "target_%s" % target + command = "$(SolutionDir)\\mach.bat build" + if target != "full": + command += " %s" % target + + project_id = self._write_vs_project( + out_proj_dir, + basename, + target, + build_command=command, + clean_command="$(SolutionDir)\\mach.bat clobber", + ) + + projects[basename] = (project_id, basename, target) + + # A project that can be used to regenerate the visual studio projects. + basename = "target_vs" + project_id = self._write_vs_project( + out_proj_dir, + basename, + "visual-studio", + build_command="$(SolutionDir)\\mach.bat build-backend -b VisualStudio", + ) + projects[basename] = (project_id, basename, "visual-studio") + + # Write out a shared property file with common variables. + props_path = os.path.join(out_proj_dir, "mozilla.props") + with self._write_file(props_path, readmode="rb") as fh: + self._write_props(fh) + + # Generate some wrapper scripts that allow us to invoke mach inside + # a MozillaBuild-like environment. We currently only use the batch + # script. We'd like to use the PowerShell script. However, it seems + # to buffer output from within Visual Studio (surely this is + # configurable) and the default execution policy of PowerShell doesn't + # allow custom scripts to be executed. + with self._write_file(os.path.join(out_dir, "mach.bat"), readmode="rb") as fh: + self._write_mach_batch(fh) + + with self._write_file(os.path.join(out_dir, "mach.ps1"), readmode="rb") as fh: + self._write_mach_powershell(fh) + + # Write out a solution file to tie it all together. + solution_path = os.path.join(out_dir, "mozilla.sln") + with self._write_file(solution_path, readmode="rb") as fh: + self._write_solution(fh, projects) + + def _write_projects_for_sources(self, sources, prefix, out_dir): + projects = {} + for item, path in sorted(sources.items()): + config = self._paths_to_configs.get(path, None) + sources = self._paths_to_sources.get(path, set()) + sources = set(os.path.join("$(TopSrcDir)", path, s) for s in sources) + sources = set(os.path.normpath(s) for s in sources) + + finder = FileFinder(os.path.join(self.environment.topsrcdir, path)) + + headers = [t[0] for t in finder.find("*.h")] + headers = [ + os.path.normpath(os.path.join("$(TopSrcDir)", path, f)) for f in headers + ] + + includes = [ + os.path.join("$(TopSrcDir)", path), + os.path.join("$(TopObjDir)", path), + ] + includes.extend(self._paths_to_includes.get(path, [])) + includes.append("$(TopObjDir)\\dist\\include\\nss") + includes.append("$(TopObjDir)\\dist\\include") + + for v in ( + "NSPR_CFLAGS", + "NSS_CFLAGS", + "MOZ_JPEG_CFLAGS", + "MOZ_PNG_CFLAGS", + "MOZ_ZLIB_CFLAGS", + "MOZ_PIXMAN_CFLAGS", + ): + if not config: + break + + args = config.substs.get(v, []) + + for i, arg in enumerate(args): + if arg.startswith("-I"): + includes.append(os.path.normpath(arg[2:])) + + # Pull in system defaults. + includes.append("$(DefaultIncludes)") + + includes = [os.path.normpath(i) for i in includes] + + defines = [] + for k, v in self._paths_to_defines.get(path, {}).items(): + if v is True: + defines.append(k) + else: + defines.append("%s=%s" % (k, v)) + + debugger = None + if prefix == "binary": + if item.startswith(self.environment.substs["MOZ_APP_NAME"]): + app_args = "-no-remote -profile $(TopObjDir)\\tmp\\profile-default" + if self.environment.substs.get("MOZ_LAUNCHER_PROCESS", False): + app_args += " -wait-for-browser" + debugger = ("$(TopObjDir)\\dist\\bin\\%s" % item, app_args) + else: + debugger = ("$(TopObjDir)\\dist\\bin\\%s" % item, "") + + basename = "%s_%s" % (prefix, item) + + project_id = self._write_vs_project( + out_dir, + basename, + item, + includes=includes, + forced_includes=["$(TopObjDir)\\dist\\include\\mozilla-config.h"], + defines=defines, + headers=headers, + sources=sources, + debugger=debugger, + ) + + projects[basename] = (project_id, basename, item) + + return projects + + def _write_solution(self, fh, projects): + # Visual Studio appears to write out its current version in the + # solution file. Instead of trying to figure out what version it will + # write, try to parse the version out of the existing file and use it + # verbatim. + vs_version = None + try: + with open(fh.name, "rb") as sfh: + for line in sfh: + if line.startswith(b"VisualStudioVersion = "): + vs_version = line.split(b" = ", 1)[1].strip() + except IOError as e: + if e.errno != errno.ENOENT: + raise + + format_version, comment_version = visual_studio_product_to_solution_version( + self._version + ) + # This is a Visual C++ Project type. + project_type = "8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942" + + # Visual Studio seems to require this header. + fh.write( + "Microsoft Visual Studio Solution File, Format Version %s\r\n" + % format_version + ) + fh.write("# Visual Studio %s\r\n" % comment_version) + + if vs_version: + fh.write("VisualStudioVersion = %s\r\n" % vs_version) + + # Corresponds to VS2013. + fh.write("MinimumVisualStudioVersion = 12.0.31101.0\r\n") + + binaries_id = projects["target_binaries"][0] + + # Write out entries for each project. + for key in sorted(projects): + project_id, basename, name = projects[key] + path = os.path.join(self._projsubdir, "%s.vcxproj" % basename) + + fh.write( + 'Project("{%s}") = "%s", "%s", "{%s}"\r\n' + % (project_type, name, path, project_id) + ) + + # Make all libraries depend on the binaries target. + if key.startswith("library_"): + fh.write("\tProjectSection(ProjectDependencies) = postProject\r\n") + fh.write("\t\t{%s} = {%s}\r\n" % (binaries_id, binaries_id)) + fh.write("\tEndProjectSection\r\n") + + fh.write("EndProject\r\n") + + # Write out solution folders for organizing things. + + # This is the UUID you use for solution folders. + container_id = "2150E333-8FDC-42A3-9474-1A3956D46DE8" + + def write_container(desc): + cid = get_id(desc) + fh.write( + 'Project("{%s}") = "%s", "%s", "{%s}"\r\n' + % (container_id, desc, desc, cid) + ) + fh.write("EndProject\r\n") + + return cid + + library_id = write_container("Libraries") + target_id = write_container("Build Targets") + binary_id = write_container("Binaries") + + fh.write("Global\r\n") + + # Make every project a member of our one configuration. + fh.write("\tGlobalSection(SolutionConfigurationPlatforms) = preSolution\r\n") + fh.write("\t\tBuild|Win32 = Build|Win32\r\n") + fh.write("\tEndGlobalSection\r\n") + + # Set every project's active configuration to the one configuration and + # set up the default build project. + fh.write("\tGlobalSection(ProjectConfigurationPlatforms) = postSolution\r\n") + for name, project in sorted(projects.items()): + fh.write("\t\t{%s}.Build|Win32.ActiveCfg = Build|Win32\r\n" % project[0]) + + # Only build the full build target by default. + # It's important we don't write multiple entries here because they + # conflict! + if name == "target_full": + fh.write("\t\t{%s}.Build|Win32.Build.0 = Build|Win32\r\n" % project[0]) + + fh.write("\tEndGlobalSection\r\n") + + fh.write("\tGlobalSection(SolutionProperties) = preSolution\r\n") + fh.write("\t\tHideSolutionNode = FALSE\r\n") + fh.write("\tEndGlobalSection\r\n") + + # Associate projects with containers. + fh.write("\tGlobalSection(NestedProjects) = preSolution\r\n") + for key in sorted(projects): + project_id = projects[key][0] + + if key.startswith("library_"): + container_id = library_id + elif key.startswith("target_"): + container_id = target_id + elif key.startswith("binary_"): + container_id = binary_id + else: + raise Exception("Unknown project type: %s" % key) + + fh.write("\t\t{%s} = {%s}\r\n" % (project_id, container_id)) + fh.write("\tEndGlobalSection\r\n") + + fh.write("EndGlobal\r\n") + + def _write_props(self, fh): + impl = getDOMImplementation() + doc = impl.createDocument(MSBUILD_NAMESPACE, "Project", None) + + project = doc.documentElement + project.setAttribute("xmlns", MSBUILD_NAMESPACE) + project.setAttribute("ToolsVersion", "4.0") + + ig = project.appendChild(doc.createElement("ImportGroup")) + ig.setAttribute("Label", "PropertySheets") + + pg = project.appendChild(doc.createElement("PropertyGroup")) + pg.setAttribute("Label", "UserMacros") + + ig = project.appendChild(doc.createElement("ItemGroup")) + + def add_var(k, v): + e = pg.appendChild(doc.createElement(k)) + e.appendChild(doc.createTextNode(v)) + + e = ig.appendChild(doc.createElement("BuildMacro")) + e.setAttribute("Include", k) + + e = e.appendChild(doc.createElement("Value")) + e.appendChild(doc.createTextNode("$(%s)" % k)) + + natvis = ig.appendChild(doc.createElement("Natvis")) + natvis.setAttribute("Include", "../../../toolkit/library/gecko.natvis") + + add_var("TopObjDir", os.path.normpath(self.environment.topobjdir)) + add_var("TopSrcDir", os.path.normpath(self.environment.topsrcdir)) + add_var("PYTHON", "$(TopObjDir)\\_virtualenv\\Scripts\\python.exe") + add_var("MACH", "$(TopSrcDir)\\mach") + + # From MozillaBuild. + add_var("DefaultIncludes", os.environ.get("INCLUDE", "")) + + fh.write(b"\xef\xbb\xbf") + doc.writexml(fh, addindent=" ", newl="\r\n") + + def _create_natvis_type( + self, doc, visualizer, name, displayString, stringView=None + ): + t = visualizer.appendChild(doc.createElement("Type")) + t.setAttribute("Name", name) + + ds = t.appendChild(doc.createElement("DisplayString")) + ds.appendChild(doc.createTextNode(displayString)) + + if stringView is not None: + sv = t.appendChild(doc.createElement("DisplayString")) + sv.appendChild(doc.createTextNode(stringView)) + + def _create_natvis_simple_string_type(self, doc, visualizer, name): + self._create_natvis_type( + doc, visualizer, name + "<char16_t>", "{mData,su}", "mData,su" + ) + self._create_natvis_type( + doc, visualizer, name + "<char>", "{mData,s}", "mData,s" + ) + + def _create_natvis_string_tuple_type(self, doc, visualizer, chartype, formatstring): + t = visualizer.appendChild(doc.createElement("Type")) + t.setAttribute("Name", "nsTSubstringTuple<" + chartype + ">") + + ds1 = t.appendChild(doc.createElement("DisplayString")) + ds1.setAttribute("Condition", "mHead != nullptr") + ds1.appendChild( + doc.createTextNode("{mHead,na} {mFragB->mData," + formatstring + "}") + ) + + ds2 = t.appendChild(doc.createElement("DisplayString")) + ds2.setAttribute("Condition", "mHead == nullptr") + ds2.appendChild( + doc.createTextNode( + "{mFragA->mData," + + formatstring + + "} {mFragB->mData," + + formatstring + + "}" + ) + ) + + def _relevant_environment_variables(self): + # Write out the environment variables, presumably coming from + # MozillaBuild. + for k, v in sorted(os.environ.items()): + if not re.match("^[a-zA-Z0-9_]+$", k): + continue + + if k in ("OLDPWD", "PS1"): + continue + + if k.startswith("_"): + continue + + yield k, v + + yield "TOPSRCDIR", self.environment.topsrcdir + yield "TOPOBJDIR", self.environment.topobjdir + + def _write_mach_powershell(self, fh): + for k, v in self._relevant_environment_variables(): + fh.write(b'$env:%s = "%s"\r\n' % (k.encode("utf-8"), v.encode("utf-8"))) + + relpath = os.path.relpath( + self.environment.topsrcdir, self.environment.topobjdir + ).replace("\\", "/") + + fh.write( + b'$bashargs = "%s/mach", "--log-no-times"\r\n' % relpath.encode("utf-8") + ) + fh.write(b"$bashargs = $bashargs + $args\r\n") + + fh.write(b"$expanded = $bashargs -join ' '\r\n") + fh.write(b'$procargs = "-c", $expanded\r\n') + + if (Path(os.environ["MOZILLABUILD"]) / "msys2").exists(): + bash_path = rb"msys2\usr\bin\bash" + else: + bash_path = rb"msys\bin\bash" + + fh.write( + b"Start-Process -WorkingDirectory $env:TOPOBJDIR " + b"-FilePath $env:MOZILLABUILD\\%b " + b"-ArgumentList $procargs " + b"-Wait -NoNewWindow\r\n" % bash_path + ) + + def _write_mach_batch(self, fh): + """Write out a batch script that builds the tree. + + The script "bootstraps" into the MozillaBuild environment by setting + the environment variables that are active in the current MozillaBuild + environment. Then, it builds the tree. + """ + for k, v in self._relevant_environment_variables(): + fh.write(b'SET "%s=%s"\r\n' % (k.encode("utf-8"), v.encode("utf-8"))) + + fh.write(b"cd %TOPOBJDIR%\r\n") + + # We need to convert Windows-native paths to msys paths. Easiest way is + # relative paths, since munging c:\ to /c/ is slightly more + # complicated. + relpath = os.path.relpath( + self.environment.topsrcdir, self.environment.topobjdir + ).replace("\\", "/") + + if (Path(os.environ["MOZILLABUILD"]) / "msys2").exists(): + bash_path = rb"msys2\usr\bin\bash" + else: + bash_path = rb"msys\bin\bash" + + # We go through mach because it has the logic for choosing the most + # appropriate build tool. + fh.write( + b'"%%MOZILLABUILD%%\\%b" ' + b'-c "%s/mach --log-no-times %%1 %%2 %%3 %%4 %%5 %%6 %%7"' + % (bash_path, relpath.encode("utf-8")) + ) + + def _write_vs_project(self, out_dir, basename, name, **kwargs): + root = "%s.vcxproj" % basename + project_id = get_id(basename) + + with self._write_file(os.path.join(out_dir, root), readmode="rb") as fh: + project_id, name = VisualStudioBackend.write_vs_project( + fh, self._version, project_id, name, **kwargs + ) + + with self._write_file( + os.path.join(out_dir, "%s.user" % root), readmode="rb" + ) as fh: + fh.write('<?xml version="1.0" encoding="utf-8"?>\r\n') + fh.write('<Project ToolsVersion="4.0" xmlns="%s">\r\n' % MSBUILD_NAMESPACE) + fh.write("</Project>\r\n") + + return project_id + + @staticmethod + def write_vs_project( + fh, + version, + project_id, + name, + includes=[], + forced_includes=[], + defines=[], + build_command=None, + clean_command=None, + debugger=None, + headers=[], + sources=[], + ): + impl = getDOMImplementation() + doc = impl.createDocument(MSBUILD_NAMESPACE, "Project", None) + + project = doc.documentElement + project.setAttribute("DefaultTargets", "Build") + project.setAttribute("ToolsVersion", "4.0") + project.setAttribute("xmlns", MSBUILD_NAMESPACE) + + ig = project.appendChild(doc.createElement("ItemGroup")) + ig.setAttribute("Label", "ProjectConfigurations") + + pc = ig.appendChild(doc.createElement("ProjectConfiguration")) + pc.setAttribute("Include", "Build|Win32") + + c = pc.appendChild(doc.createElement("Configuration")) + c.appendChild(doc.createTextNode("Build")) + + p = pc.appendChild(doc.createElement("Platform")) + p.appendChild(doc.createTextNode("Win32")) + + pg = project.appendChild(doc.createElement("PropertyGroup")) + pg.setAttribute("Label", "Globals") + + n = pg.appendChild(doc.createElement("ProjectName")) + n.appendChild(doc.createTextNode(name)) + + k = pg.appendChild(doc.createElement("Keyword")) + k.appendChild(doc.createTextNode("MakeFileProj")) + + g = pg.appendChild(doc.createElement("ProjectGuid")) + g.appendChild(doc.createTextNode("{%s}" % project_id)) + + rn = pg.appendChild(doc.createElement("RootNamespace")) + rn.appendChild(doc.createTextNode("mozilla")) + + pts = pg.appendChild(doc.createElement("PlatformToolset")) + pts.appendChild( + doc.createTextNode( + visual_studio_product_to_platform_toolset_version(version) + ) + ) + + i = project.appendChild(doc.createElement("Import")) + i.setAttribute("Project", "$(VCTargetsPath)\\Microsoft.Cpp.Default.props") + + ig = project.appendChild(doc.createElement("ImportGroup")) + ig.setAttribute("Label", "ExtensionTargets") + + ig = project.appendChild(doc.createElement("ImportGroup")) + ig.setAttribute("Label", "ExtensionSettings") + + ig = project.appendChild(doc.createElement("ImportGroup")) + ig.setAttribute("Label", "PropertySheets") + i = ig.appendChild(doc.createElement("Import")) + i.setAttribute("Project", "mozilla.props") + + pg = project.appendChild(doc.createElement("PropertyGroup")) + pg.setAttribute("Label", "Configuration") + ct = pg.appendChild(doc.createElement("ConfigurationType")) + ct.appendChild(doc.createTextNode("Makefile")) + + pg = project.appendChild(doc.createElement("PropertyGroup")) + pg.setAttribute("Condition", "'$(Configuration)|$(Platform)'=='Build|Win32'") + + if build_command: + n = pg.appendChild(doc.createElement("NMakeBuildCommandLine")) + n.appendChild(doc.createTextNode(build_command)) + + if clean_command: + n = pg.appendChild(doc.createElement("NMakeCleanCommandLine")) + n.appendChild(doc.createTextNode(clean_command)) + + if includes: + n = pg.appendChild(doc.createElement("NMakeIncludeSearchPath")) + n.appendChild(doc.createTextNode(";".join(includes))) + + if forced_includes: + n = pg.appendChild(doc.createElement("NMakeForcedIncludes")) + n.appendChild(doc.createTextNode(";".join(forced_includes))) + + if defines: + n = pg.appendChild(doc.createElement("NMakePreprocessorDefinitions")) + n.appendChild(doc.createTextNode(";".join(defines))) + + if debugger: + n = pg.appendChild(doc.createElement("LocalDebuggerCommand")) + n.appendChild(doc.createTextNode(debugger[0])) + + n = pg.appendChild(doc.createElement("LocalDebuggerCommandArguments")) + n.appendChild(doc.createTextNode(debugger[1])) + + # Sets IntelliSense to use c++17 Language Standard + n = pg.appendChild(doc.createElement("AdditionalOptions")) + n.appendChild(doc.createTextNode("/std:c++17")) + + i = project.appendChild(doc.createElement("Import")) + i.setAttribute("Project", "$(VCTargetsPath)\\Microsoft.Cpp.props") + + i = project.appendChild(doc.createElement("Import")) + i.setAttribute("Project", "$(VCTargetsPath)\\Microsoft.Cpp.targets") + + # Now add files to the project. + ig = project.appendChild(doc.createElement("ItemGroup")) + for header in sorted(headers or []): + n = ig.appendChild(doc.createElement("ClInclude")) + n.setAttribute("Include", header) + + ig = project.appendChild(doc.createElement("ItemGroup")) + for source in sorted(sources or []): + n = ig.appendChild(doc.createElement("ClCompile")) + n.setAttribute("Include", source) + + fh.write(b"\xef\xbb\xbf") + doc.writexml(fh, addindent=" ", newl="\r\n") + + return project_id, name diff --git a/python/mozbuild/mozbuild/base.py b/python/mozbuild/mozbuild/base.py new file mode 100644 index 0000000000..75eb76b459 --- /dev/null +++ b/python/mozbuild/mozbuild/base.py @@ -0,0 +1,1114 @@ +# 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/. + +import errno +import io +import json +import logging +import multiprocessing +import os +import subprocess +import sys +from pathlib import Path + +import mozpack.path as mozpath +import six +from mach.mixin.process import ProcessExecutionMixin +from mozboot.mozconfig import MozconfigFindException +from mozfile import which +from mozversioncontrol import ( + GitRepository, + HgRepository, + InvalidRepoPath, + MissingConfigureInfo, + MissingVCSTool, + get_repository_from_build_config, + get_repository_object, +) + +from .backend.configenvironment import ConfigEnvironment, ConfigStatusFailure +from .configure import ConfigureSandbox +from .controller.clobber import Clobberer +from .mozconfig import MozconfigLoader, MozconfigLoadException +from .util import memoize, memoized_property + +try: + import psutil +except Exception: + psutil = None + + +class BadEnvironmentException(Exception): + """Base class for errors raised when the build environment is not sane.""" + + +class BuildEnvironmentNotFoundException(BadEnvironmentException, AttributeError): + """Raised when we could not find a build environment.""" + + +class ObjdirMismatchException(BadEnvironmentException): + """Raised when the current dir is an objdir and doesn't match the mozconfig.""" + + def __init__(self, objdir1, objdir2): + self.objdir1 = objdir1 + self.objdir2 = objdir2 + + def __str__(self): + return "Objdir mismatch: %s != %s" % (self.objdir1, self.objdir2) + + +class BinaryNotFoundException(Exception): + """Raised when the binary is not found in the expected location.""" + + def __init__(self, path): + self.path = path + + def __str__(self): + return "Binary expected at {} does not exist.".format(self.path) + + def help(self): + return "It looks like your program isn't built. You can run |./mach build| to build it." + + +class MozbuildObject(ProcessExecutionMixin): + """Base class providing basic functionality useful to many modules. + + Modules in this package typically require common functionality such as + accessing the current config, getting the location of the source directory, + running processes, etc. This classes provides that functionality. Other + modules can inherit from this class to obtain this functionality easily. + """ + + def __init__( + self, + topsrcdir, + settings, + log_manager, + topobjdir=None, + mozconfig=MozconfigLoader.AUTODETECT, + virtualenv_name=None, + ): + """Create a new Mozbuild object instance. + + Instances are bound to a source directory, a ConfigSettings instance, + and a LogManager instance. The topobjdir may be passed in as well. If + it isn't, it will be calculated from the active mozconfig. + """ + self.topsrcdir = mozpath.realpath(topsrcdir) + self.settings = settings + + self.populate_logger() + self.log_manager = log_manager + + self._make = None + self._topobjdir = mozpath.realpath(topobjdir) if topobjdir else topobjdir + self._mozconfig = mozconfig + self._config_environment = None + self._virtualenv_name = virtualenv_name or "common" + self._virtualenv_manager = None + + @classmethod + def from_environment(cls, cwd=None, detect_virtualenv_mozinfo=True, **kwargs): + """Create a MozbuildObject by detecting the proper one from the env. + + This examines environment state like the current working directory and + creates a MozbuildObject from the found source directory, mozconfig, etc. + + The role of this function is to identify a topsrcdir, topobjdir, and + mozconfig file. + + If the current working directory is inside a known objdir, we always + use the topsrcdir and mozconfig associated with that objdir. + + If the current working directory is inside a known srcdir, we use that + topsrcdir and look for mozconfigs using the default mechanism, which + looks inside environment variables. + + If the current Python interpreter is running from a virtualenv inside + an objdir, we use that as our objdir. + + If we're not inside a srcdir or objdir, an exception is raised. + + detect_virtualenv_mozinfo determines whether we should look for a + mozinfo.json file relative to the virtualenv directory. This was + added to facilitate testing. Callers likely shouldn't change the + default. + """ + + cwd = os.path.realpath(cwd or os.getcwd()) + topsrcdir = None + topobjdir = None + mozconfig = MozconfigLoader.AUTODETECT + + def load_mozinfo(path): + info = json.load(io.open(path, "rt", encoding="utf-8")) + topsrcdir = info.get("topsrcdir") + topobjdir = os.path.dirname(path) + mozconfig = info.get("mozconfig") + return topsrcdir, topobjdir, mozconfig + + for dir_path in [str(path) for path in [cwd] + list(Path(cwd).parents)]: + # If we find a mozinfo.json, we are in the objdir. + mozinfo_path = os.path.join(dir_path, "mozinfo.json") + if os.path.isfile(mozinfo_path): + topsrcdir, topobjdir, mozconfig = load_mozinfo(mozinfo_path) + break + + if not topsrcdir: + # See if we're running from a Python virtualenv that's inside an objdir. + # sys.prefix would look like "$objdir/_virtualenvs/$virtualenv/". + # Note that virtualenv-based objdir detection work for instrumented builds, + # because they aren't created in the scoped "instrumentated" objdir. + # However, working-directory-ancestor-based objdir resolution should fully + # cover that case. + mozinfo_path = os.path.join(sys.prefix, "..", "..", "mozinfo.json") + if detect_virtualenv_mozinfo and os.path.isfile(mozinfo_path): + topsrcdir, topobjdir, mozconfig = load_mozinfo(mozinfo_path) + + if not topsrcdir: + topsrcdir = str(Path(__file__).parent.parent.parent.parent.resolve()) + + topsrcdir = mozpath.realpath(topsrcdir) + if topobjdir: + topobjdir = mozpath.realpath(topobjdir) + + if topsrcdir == topobjdir: + raise BadEnvironmentException( + "The object directory appears " + "to be the same as your source directory (%s). This build " + "configuration is not supported." % topsrcdir + ) + + # If we can't resolve topobjdir, oh well. We'll figure out when we need + # one. + return cls( + topsrcdir, None, None, topobjdir=topobjdir, mozconfig=mozconfig, **kwargs + ) + + def resolve_mozconfig_topobjdir(self, default=None): + topobjdir = self.mozconfig.get("topobjdir") or default + if not topobjdir: + return None + + if "@CONFIG_GUESS@" in topobjdir: + topobjdir = topobjdir.replace("@CONFIG_GUESS@", self.resolve_config_guess()) + + if not os.path.isabs(topobjdir): + topobjdir = os.path.abspath(os.path.join(self.topsrcdir, topobjdir)) + + return mozpath.normsep(os.path.normpath(topobjdir)) + + def build_out_of_date(self, output, dep_file): + if not os.path.isfile(output): + print(" Output reference file not found: %s" % output) + return True + if not os.path.isfile(dep_file): + print(" Dependency file not found: %s" % dep_file) + return True + + deps = [] + with io.open(dep_file, "r", encoding="utf-8", newline="\n") as fh: + deps = fh.read().splitlines() + + mtime = os.path.getmtime(output) + for f in deps: + try: + dep_mtime = os.path.getmtime(f) + except OSError as e: + if e.errno == errno.ENOENT: + print(" Input not found: %s" % f) + return True + raise + if dep_mtime > mtime: + print(" %s is out of date with respect to %s" % (output, f)) + return True + return False + + def backend_out_of_date(self, backend_file): + if not os.path.isfile(backend_file): + return True + + # Check if any of our output files have been removed since + # we last built the backend, re-generate the backend if + # so. + outputs = [] + with io.open(backend_file, "r", encoding="utf-8", newline="\n") as fh: + outputs = fh.read().splitlines() + for output in outputs: + if not os.path.isfile(mozpath.join(self.topobjdir, output)): + return True + + dep_file = "%s.in" % backend_file + return self.build_out_of_date(backend_file, dep_file) + + @property + def topobjdir(self): + if self._topobjdir is None: + self._topobjdir = self.resolve_mozconfig_topobjdir( + default="obj-@CONFIG_GUESS@" + ) + + return self._topobjdir + + @property + def virtualenv_manager(self): + from mach.site import CommandSiteManager + from mach.util import get_state_dir, get_virtualenv_base_dir + + if self._virtualenv_manager is None: + self._virtualenv_manager = CommandSiteManager.from_environment( + self.topsrcdir, + lambda: get_state_dir( + specific_to_topsrcdir=True, topsrcdir=self.topsrcdir + ), + self._virtualenv_name, + get_virtualenv_base_dir(self.topsrcdir), + ) + + return self._virtualenv_manager + + @virtualenv_manager.setter + def virtualenv_manager(self, command_site_manager): + self._virtualenv_manager = command_site_manager + + @staticmethod + @memoize + def get_base_mozconfig_info(topsrcdir, path, env_mozconfig): + # env_mozconfig is only useful for unittests, which change the value of + # the environment variable, which has an impact on autodetection (when + # path is MozconfigLoader.AUTODETECT), and memoization wouldn't account + # for it without the explicit (unused) argument. + out = six.StringIO() + env = os.environ + if path and path != MozconfigLoader.AUTODETECT: + env = dict(env) + env["MOZCONFIG"] = path + + # We use python configure to get mozconfig content and the value for + # --target (from mozconfig if necessary, guessed otherwise). + + # Modified configure sandbox that replaces '--help' dependencies with + # `always`, such that depends functions with a '--help' dependency are + # not automatically executed when including files. We don't want all of + # those from init.configure to execute, only a subset. + class ReducedConfigureSandbox(ConfigureSandbox): + def depends_impl(self, *args, **kwargs): + args = tuple( + a + if not isinstance(a, six.string_types) or a != "--help" + else self._always.sandboxed + for a in args + ) + return super(ReducedConfigureSandbox, self).depends_impl( + *args, **kwargs + ) + + # This may be called recursively from configure itself for $reasons, + # so avoid logging to the same logger (configure uses "moz.configure") + logger = logging.getLogger("moz.configure.reduced") + handler = logging.StreamHandler(out) + logger.addHandler(handler) + # If this were true, logging would still propagate to "moz.configure". + logger.propagate = False + sandbox = ReducedConfigureSandbox( + {}, + environ=env, + argv=["mach"], + logger=logger, + ) + base_dir = os.path.join(topsrcdir, "build", "moz.configure") + try: + sandbox.include_file(os.path.join(base_dir, "init.configure")) + # Force mozconfig options injection before getting the target. + sandbox._value_for(sandbox["mozconfig_options"]) + return { + "mozconfig": sandbox._value_for(sandbox["mozconfig"]), + "target": sandbox._value_for(sandbox["real_target"]), + "project": sandbox._value_for(sandbox._options["project"]), + "artifact-builds": sandbox._value_for( + sandbox._options["artifact-builds"] + ), + } + except SystemExit: + print(out.getvalue()) + raise + + @property + def base_mozconfig_info(self): + return self.get_base_mozconfig_info( + self.topsrcdir, self._mozconfig, os.environ.get("MOZCONFIG") + ) + + @property + def mozconfig(self): + """Returns information about the current mozconfig file. + + This a dict as returned by MozconfigLoader.read_mozconfig() + """ + return self.base_mozconfig_info["mozconfig"] + + @property + def config_environment(self): + """Returns the ConfigEnvironment for the current build configuration. + + This property is only available once configure has executed. + + If configure's output is not available, this will raise. + """ + if self._config_environment: + return self._config_environment + + config_status = os.path.join(self.topobjdir, "config.status") + + if not os.path.exists(config_status) or not os.path.getsize(config_status): + raise BuildEnvironmentNotFoundException( + "config.status not available. Run configure." + ) + + try: + self._config_environment = ConfigEnvironment.from_config_status( + config_status + ) + except ConfigStatusFailure as e: + six.raise_from( + BuildEnvironmentNotFoundException( + "config.status is outdated or broken. Run configure." + ), + e, + ) + + return self._config_environment + + @property + def defines(self): + return self.config_environment.defines + + @property + def substs(self): + return self.config_environment.substs + + @property + def distdir(self): + return os.path.join(self.topobjdir, "dist") + + @property + def bindir(self): + return os.path.join(self.topobjdir, "dist", "bin") + + @property + def includedir(self): + return os.path.join(self.topobjdir, "dist", "include") + + @property + def statedir(self): + return os.path.join(self.topobjdir, ".mozbuild") + + @property + def platform(self): + """Returns current platform and architecture name""" + import mozinfo + + platform_name = None + bits = str(mozinfo.info["bits"]) + if mozinfo.isLinux: + platform_name = "linux" + bits + elif mozinfo.isWin: + platform_name = "win" + bits + elif mozinfo.isMac: + platform_name = "macosx" + bits + + return platform_name, bits + "bit" + + @memoized_property + def repository(self): + """Get a `mozversioncontrol.Repository` object for the + top source directory.""" + # We try to obtain a repo using the configured VCS info first. + # If we don't have a configure context, fall back to auto-detection. + try: + return get_repository_from_build_config(self) + except ( + BuildEnvironmentNotFoundException, + MissingConfigureInfo, + MissingVCSTool, + ): + pass + + return get_repository_object(self.topsrcdir) + + def reload_config_environment(self): + """Force config.status to be re-read and return the new value + of ``self.config_environment``. + """ + self._config_environment = None + return self.config_environment + + def mozbuild_reader( + self, config_mode="build", vcs_revision=None, vcs_check_clean=True + ): + """Obtain a ``BuildReader`` for evaluating moz.build files. + + Given arguments, returns a ``mozbuild.frontend.reader.BuildReader`` + that can be used to evaluate moz.build files for this repo. + + ``config_mode`` is either ``build`` or ``empty``. If ``build``, + ``self.config_environment`` is used. This requires a configured build + system to work. If ``empty``, an empty config is used. ``empty`` is + appropriate for file-based traversal mode where ``Files`` metadata is + read. + + If ``vcs_revision`` is defined, it specifies a version control revision + to use to obtain files content. The default is to use the filesystem. + This mode is only supported with Mercurial repositories. + + If ``vcs_revision`` is not defined and the version control checkout is + sparse, this implies ``vcs_revision='.'``. + + If ``vcs_revision`` is ``.`` (denotes the parent of the working + directory), we will verify that the working directory is clean unless + ``vcs_check_clean`` is False. This prevents confusion due to uncommitted + file changes not being reflected in the reader. + """ + from mozpack.files import MercurialRevisionFinder + + from mozbuild.frontend.reader import BuildReader, EmptyConfig, default_finder + + if config_mode == "build": + config = self.config_environment + elif config_mode == "empty": + config = EmptyConfig(self.topsrcdir) + else: + raise ValueError("unknown config_mode value: %s" % config_mode) + + try: + repo = self.repository + except InvalidRepoPath: + repo = None + + if ( + repo + and repo != "SOURCE" + and not vcs_revision + and repo.sparse_checkout_present() + ): + vcs_revision = "." + + if vcs_revision is None: + finder = default_finder + else: + # If we failed to detect the repo prior, check again to raise its + # exception. + if not repo: + self.repository + assert False + + if repo.name != "hg": + raise Exception("do not support VCS reading mode for %s" % repo.name) + + if vcs_revision == "." and vcs_check_clean: + with repo: + if not repo.working_directory_clean(): + raise Exception( + "working directory is not clean; " + "refusing to use a VCS-based finder" + ) + + finder = MercurialRevisionFinder( + self.topsrcdir, rev=vcs_revision, recognize_repo_paths=True + ) + + return BuildReader(config, finder=finder) + + def is_clobber_needed(self): + if not os.path.exists(self.topobjdir): + return False + return Clobberer(self.topsrcdir, self.topobjdir).clobber_needed() + + def get_binary_path(self, what="app", validate_exists=True, where="default"): + """Obtain the path to a compiled binary for this build configuration. + + The what argument is the program or tool being sought after. See the + code implementation for supported values. + + If validate_exists is True (the default), we will ensure the found path + exists before returning, raising an exception if it doesn't. + + If where is 'staged-package', we will return the path to the binary in + the package staging directory. + + If no arguments are specified, we will return the main binary for the + configured XUL application. + """ + + if where not in ("default", "staged-package"): + raise Exception("Don't know location %s" % where) + + substs = self.substs + + stem = self.distdir + if where == "staged-package": + stem = os.path.join(stem, substs["MOZ_APP_NAME"]) + + if substs["OS_ARCH"] == "Darwin" and "MOZ_MACBUNDLE_NAME" in substs: + stem = os.path.join(stem, substs["MOZ_MACBUNDLE_NAME"], "Contents", "MacOS") + elif where == "default": + stem = os.path.join(stem, "bin") + + leaf = None + + leaf = (substs["MOZ_APP_NAME"] if what == "app" else what) + substs[ + "BIN_SUFFIX" + ] + path = os.path.join(stem, leaf) + + if validate_exists and not os.path.exists(path): + raise BinaryNotFoundException(path) + + return path + + def resolve_config_guess(self): + return self.base_mozconfig_info["target"].alias + + def notify(self, msg): + """Show a desktop notification with the supplied message + + On Linux and Mac, this will show a desktop notification with the message, + but on Windows we can only flash the screen. + """ + if "MOZ_NOSPAM" in os.environ or "MOZ_AUTOMATION" in os.environ: + return + + try: + if sys.platform.startswith("darwin"): + notifier = which("terminal-notifier") + if not notifier: + raise Exception( + "Install terminal-notifier to get " + "a notification when the build finishes." + ) + self.run_process( + [ + notifier, + "-title", + "Mozilla Build System", + "-group", + "mozbuild", + "-message", + msg, + ], + ensure_exit_code=False, + ) + elif sys.platform.startswith("win"): + from ctypes import POINTER, WINFUNCTYPE, Structure, sizeof, windll + from ctypes.wintypes import BOOL, DWORD, HANDLE, UINT + + class FLASHWINDOW(Structure): + _fields_ = [ + ("cbSize", UINT), + ("hwnd", HANDLE), + ("dwFlags", DWORD), + ("uCount", UINT), + ("dwTimeout", DWORD), + ] + + FlashWindowExProto = WINFUNCTYPE(BOOL, POINTER(FLASHWINDOW)) + FlashWindowEx = FlashWindowExProto(("FlashWindowEx", windll.user32)) + FLASHW_CAPTION = 0x01 + FLASHW_TRAY = 0x02 + FLASHW_TIMERNOFG = 0x0C + + # GetConsoleWindows returns NULL if no console is attached. We + # can't flash nothing. + console = windll.kernel32.GetConsoleWindow() + if not console: + return + + params = FLASHWINDOW( + sizeof(FLASHWINDOW), + console, + FLASHW_CAPTION | FLASHW_TRAY | FLASHW_TIMERNOFG, + 3, + 0, + ) + FlashWindowEx(params) + else: + notifier = which("notify-send") + if not notifier: + raise Exception( + "Install notify-send (usually part of " + "the libnotify package) to get a notification when " + "the build finishes." + ) + self.run_process( + [ + notifier, + "--app-name=Mozilla Build System", + "Mozilla Build System", + msg, + ], + ensure_exit_code=False, + ) + except Exception as e: + self.log( + logging.WARNING, + "notifier-failed", + {"error": str(e)}, + "Notification center failed: {error}", + ) + + def _ensure_objdir_exists(self): + if os.path.isdir(self.statedir): + return + + os.makedirs(self.statedir) + + def _ensure_state_subdir_exists(self, subdir): + path = os.path.join(self.statedir, subdir) + + if os.path.isdir(path): + return + + os.makedirs(path) + + def _get_state_filename(self, filename, subdir=None): + path = self.statedir + + if subdir: + path = os.path.join(path, subdir) + + return os.path.join(path, filename) + + def _wrap_path_argument(self, arg): + return PathArgument(arg, self.topsrcdir, self.topobjdir) + + def _run_make( + self, + directory=None, + filename=None, + target=None, + log=True, + srcdir=False, + line_handler=None, + append_env=None, + explicit_env=None, + ignore_errors=False, + ensure_exit_code=0, + silent=True, + print_directory=True, + pass_thru=False, + num_jobs=0, + job_size=0, + keep_going=False, + ): + """Invoke make. + + directory -- Relative directory to look for Makefile in. + filename -- Explicit makefile to run. + target -- Makefile target(s) to make. Can be a string or iterable of + strings. + srcdir -- If True, invoke make from the source directory tree. + Otherwise, make will be invoked from the object directory. + silent -- If True (the default), run make in silent mode. + print_directory -- If True (the default), have make print directories + while doing traversal. + """ + self._ensure_objdir_exists() + + args = [self.substs["GMAKE"]] + + if directory: + args.extend(["-C", directory.replace(os.sep, "/")]) + + if filename: + args.extend(["-f", filename]) + + if num_jobs == 0 and self.mozconfig["make_flags"]: + flags = iter(self.mozconfig["make_flags"]) + for flag in flags: + if flag == "-j": + try: + flag = flags.next() + except StopIteration: + break + try: + num_jobs = int(flag) + except ValueError: + args.append(flag) + elif flag.startswith("-j"): + try: + num_jobs = int(flag[2:]) + except (ValueError, IndexError): + break + else: + args.append(flag) + + if num_jobs == 0: + if job_size == 0: + job_size = 2.0 if self.substs.get("CC_TYPE") == "gcc" else 1.0 # GiB + + cpus = multiprocessing.cpu_count() + if not psutil or not job_size: + num_jobs = cpus + else: + mem_gb = psutil.virtual_memory().total / 1024**3 + from_mem = round(mem_gb / job_size) + num_jobs = max(1, min(cpus, from_mem)) + print( + " Parallelism determined by memory: using %d jobs for %d cores " + "based on %.1f GiB RAM and estimated job size of %.1f GiB" + % (num_jobs, cpus, mem_gb, job_size) + ) + + args.append("-j%d" % num_jobs) + + if ignore_errors: + args.append("-k") + + if silent: + args.append("-s") + + # Print entering/leaving directory messages. Some consumers look at + # these to measure progress. + if print_directory: + args.append("-w") + + if keep_going: + args.append("-k") + + if isinstance(target, list): + args.extend(target) + elif target: + args.append(target) + + fn = self._run_command_in_objdir + + if srcdir: + fn = self._run_command_in_srcdir + + append_env = dict(append_env or ()) + append_env["MACH"] = "1" + + params = { + "args": args, + "line_handler": line_handler, + "append_env": append_env, + "explicit_env": explicit_env, + "log_level": logging.INFO, + "require_unix_environment": False, + "ensure_exit_code": ensure_exit_code, + "pass_thru": pass_thru, + # Make manages its children, so mozprocess doesn't need to bother. + # Having mozprocess manage children can also have side-effects when + # building on Windows. See bug 796840. + "ignore_children": True, + } + + if log: + params["log_name"] = "make" + + return fn(**params) + + def _run_command_in_srcdir(self, **args): + return self.run_process(cwd=self.topsrcdir, **args) + + def _run_command_in_objdir(self, **args): + return self.run_process(cwd=self.topobjdir, **args) + + def _is_windows(self): + return os.name in ("nt", "ce") + + def _is_osx(self): + return "darwin" in str(sys.platform).lower() + + def _spawn(self, cls): + """Create a new MozbuildObject-derived class instance from ourselves. + + This is used as a convenience method to create other + MozbuildObject-derived class instances. It can only be used on + classes that have the same constructor arguments as us. + """ + + return cls( + self.topsrcdir, self.settings, self.log_manager, topobjdir=self.topobjdir + ) + + def activate_virtualenv(self): + self.virtualenv_manager.activate() + + def _set_log_level(self, verbose): + self.log_manager.terminal_handler.setLevel( + logging.INFO if not verbose else logging.DEBUG + ) + + def _ensure_zstd(self): + try: + import zstandard # noqa: F401 + except (ImportError, AttributeError): + self.activate_virtualenv() + self.virtualenv_manager.install_pip_requirements( + os.path.join(self.topsrcdir, "build", "zstandard_requirements.txt") + ) + + +class MachCommandBase(MozbuildObject): + """Base class for mach command providers that wish to be MozbuildObjects. + + This provides a level of indirection so MozbuildObject can be refactored + without having to change everything that inherits from it. + """ + + def __init__(self, context, virtualenv_name=None, metrics=None, no_auto_log=False): + # Attempt to discover topobjdir through environment detection, as it is + # more reliable than mozconfig when cwd is inside an objdir. + topsrcdir = context.topdir + topobjdir = None + detect_virtualenv_mozinfo = True + if hasattr(context, "detect_virtualenv_mozinfo"): + detect_virtualenv_mozinfo = getattr(context, "detect_virtualenv_mozinfo") + try: + dummy = MozbuildObject.from_environment( + cwd=context.cwd, detect_virtualenv_mozinfo=detect_virtualenv_mozinfo + ) + topsrcdir = dummy.topsrcdir + topobjdir = dummy._topobjdir + if topobjdir: + # If we're inside a objdir and the found mozconfig resolves to + # another objdir, we abort. The reasoning here is that if you + # are inside an objdir you probably want to perform actions on + # that objdir, not another one. This prevents accidental usage + # of the wrong objdir when the current objdir is ambiguous. + config_topobjdir = dummy.resolve_mozconfig_topobjdir() + + if config_topobjdir and not Path(topobjdir).samefile( + Path(config_topobjdir) + ): + raise ObjdirMismatchException(topobjdir, config_topobjdir) + except BuildEnvironmentNotFoundException: + pass + except ObjdirMismatchException as e: + print( + "Ambiguous object directory detected. We detected that " + "both %s and %s could be object directories. This is " + "typically caused by having a mozconfig pointing to a " + "different object directory from the current working " + "directory. To solve this problem, ensure you do not have a " + "default mozconfig in searched paths." % (e.objdir1, e.objdir2) + ) + sys.exit(1) + + except MozconfigLoadException as e: + print(e) + sys.exit(1) + + MozbuildObject.__init__( + self, + topsrcdir, + context.settings, + context.log_manager, + topobjdir=topobjdir, + virtualenv_name=virtualenv_name, + ) + + self._mach_context = context + self.metrics = metrics + + # Incur mozconfig processing so we have unified error handling for + # errors. Otherwise, the exceptions could bubble back to mach's error + # handler. + try: + self.mozconfig + + except MozconfigFindException as e: + print(e) + sys.exit(1) + + except MozconfigLoadException as e: + print(e) + sys.exit(1) + + # Always keep a log of the last command, but don't do that for mach + # invokations from scripts (especially not the ones done by the build + # system itself). + try: + fileno = getattr(sys.stdout, "fileno", lambda: None)() + except io.UnsupportedOperation: + fileno = None + if fileno and os.isatty(fileno) and not no_auto_log: + self._ensure_state_subdir_exists(".") + logfile = self._get_state_filename("last_log.json") + try: + fd = open(logfile, "wt") + self.log_manager.add_json_handler(fd) + except Exception as e: + self.log( + logging.WARNING, + "mach", + {"error": str(e)}, + "Log will not be kept for this command: {error}.", + ) + + def _sub_mach(self, argv): + return subprocess.call( + [sys.executable, os.path.join(self.topsrcdir, "mach")] + argv + ) + + +class MachCommandConditions(object): + """A series of commonly used condition functions which can be applied to + mach commands with providers deriving from MachCommandBase. + """ + + @staticmethod + def is_firefox(cls): + """Must have a Firefox build.""" + if hasattr(cls, "substs"): + return cls.substs.get("MOZ_BUILD_APP") == "browser" + return False + + @staticmethod + def is_jsshell(cls): + """Must have a jsshell build.""" + if hasattr(cls, "substs"): + return cls.substs.get("MOZ_BUILD_APP") == "js" + return False + + @staticmethod + def is_thunderbird(cls): + """Must have a Thunderbird build.""" + if hasattr(cls, "substs"): + return cls.substs.get("MOZ_BUILD_APP") == "comm/mail" + return False + + @staticmethod + def is_firefox_or_thunderbird(cls): + """Must have a Firefox or Thunderbird build.""" + return MachCommandConditions.is_firefox( + cls + ) or MachCommandConditions.is_thunderbird(cls) + + @staticmethod + def is_android(cls): + """Must have an Android build.""" + if hasattr(cls, "substs"): + return cls.substs.get("MOZ_WIDGET_TOOLKIT") == "android" + return False + + @staticmethod + def is_not_android(cls): + """Must not have an Android build.""" + if hasattr(cls, "substs"): + return cls.substs.get("MOZ_WIDGET_TOOLKIT") != "android" + return False + + @staticmethod + def is_firefox_or_android(cls): + """Must have a Firefox or Android build.""" + return MachCommandConditions.is_firefox( + cls + ) or MachCommandConditions.is_android(cls) + + @staticmethod + def has_build(cls): + """Must have a build.""" + return MachCommandConditions.is_firefox_or_android( + cls + ) or MachCommandConditions.is_thunderbird(cls) + + @staticmethod + def has_build_or_shell(cls): + """Must have a build or a shell build.""" + return MachCommandConditions.has_build(cls) or MachCommandConditions.is_jsshell( + cls + ) + + @staticmethod + def is_hg(cls): + """Must have a mercurial source checkout.""" + try: + return isinstance(cls.repository, HgRepository) + except InvalidRepoPath: + return False + + @staticmethod + def is_git(cls): + """Must have a git source checkout.""" + try: + return isinstance(cls.repository, GitRepository) + except InvalidRepoPath: + return False + + @staticmethod + def is_artifact_build(cls): + """Must be an artifact build.""" + if hasattr(cls, "substs"): + return getattr(cls, "substs", {}).get("MOZ_ARTIFACT_BUILDS") + return False + + @staticmethod + def is_non_artifact_build(cls): + """Must not be an artifact build.""" + if hasattr(cls, "substs"): + return not MachCommandConditions.is_artifact_build(cls) + return False + + @staticmethod + def is_buildapp_in(cls, apps): + """Must have a build for one of the given app""" + for app in apps: + attr = getattr(MachCommandConditions, "is_{}".format(app), None) + if attr and attr(cls): + return True + return False + + +class PathArgument(object): + """Parse a filesystem path argument and transform it in various ways.""" + + def __init__(self, arg, topsrcdir, topobjdir, cwd=None): + self.arg = arg + self.topsrcdir = topsrcdir + self.topobjdir = topobjdir + self.cwd = os.getcwd() if cwd is None else cwd + + def relpath(self): + """Return a path relative to the topsrcdir or topobjdir. + + If the argument is a path to a location in one of the base directories + (topsrcdir or topobjdir), then strip off the base directory part and + just return the path within the base directory.""" + + abspath = os.path.abspath(os.path.join(self.cwd, self.arg)) + + # If that path is within topsrcdir or topobjdir, return an equivalent + # path relative to that base directory. + for base_dir in [self.topobjdir, self.topsrcdir]: + if abspath.startswith(os.path.abspath(base_dir)): + return mozpath.relpath(abspath, base_dir) + + return mozpath.normsep(self.arg) + + def srcdir_path(self): + return mozpath.join(self.topsrcdir, self.relpath()) + + def objdir_path(self): + return mozpath.join(self.topobjdir, self.relpath()) + + +class ExecutionSummary(dict): + """Helper for execution summaries.""" + + def __init__(self, summary_format, **data): + self._summary_format = "" + assert "execution_time" in data + self.extend(summary_format, **data) + + def extend(self, summary_format, **data): + self._summary_format += summary_format + self.update(data) + + def __str__(self): + return self._summary_format.format(**self) + + def __getattr__(self, key): + return self[key] diff --git a/python/mozbuild/mozbuild/bootstrap.py b/python/mozbuild/mozbuild/bootstrap.py new file mode 100644 index 0000000000..60a307145c --- /dev/null +++ b/python/mozbuild/mozbuild/bootstrap.py @@ -0,0 +1,61 @@ +# 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/. + +import functools +import io +import logging +import os +from pathlib import Path + +from mozbuild.configure import ConfigureSandbox + + +def _raw_sandbox(extra_args=[]): + # Here, we don't want an existing mozconfig to interfere with what we + # do, neither do we want the default for --enable-bootstrap (which is not + # always on) to prevent this from doing something. + out = io.StringIO() + logger = logging.getLogger("moz.configure") + handler = logging.StreamHandler(out) + logger.addHandler(handler) + logger.propagate = False + sandbox = ConfigureSandbox( + {}, + argv=["configure"] + + ["--enable-bootstrap", f"MOZCONFIG={os.devnull}"] + + extra_args, + logger=logger, + ) + return sandbox + + +@functools.lru_cache(maxsize=None) +def _bootstrap_sandbox(): + sandbox = _raw_sandbox() + moz_configure = ( + Path(__file__).parent.parent.parent.parent / "build" / "moz.configure" + ) + sandbox.include_file(str(moz_configure / "init.configure")) + # bootstrap_search_path_order has a dependency on developer_options, which + # is not defined in init.configure. Its value doesn't matter for us, though. + sandbox["developer_options"] = sandbox["always"] + sandbox.include_file(str(moz_configure / "bootstrap.configure")) + return sandbox + + +def bootstrap_toolchain(toolchain_job): + # Expand the `bootstrap_path` template for the given toolchain_job, and execute the + # expanded function via `_value_for`, which will trigger autobootstrap. + # Returns the path to the toolchain. + sandbox = _bootstrap_sandbox() + return sandbox._value_for(sandbox["bootstrap_path"](toolchain_job)) + + +def bootstrap_all_toolchains_for(configure_args=[]): + sandbox = _raw_sandbox(configure_args) + moz_configure = Path(__file__).parent.parent.parent.parent / "moz.configure" + sandbox.include_file(str(moz_configure)) + for depend in sandbox._depends.values(): + if depend.name == "bootstrap_path": + depend.result() diff --git a/python/mozbuild/mozbuild/build_commands.py b/python/mozbuild/mozbuild/build_commands.py new file mode 100644 index 0000000000..3299ca712e --- /dev/null +++ b/python/mozbuild/mozbuild/build_commands.py @@ -0,0 +1,369 @@ +# 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/. + +import argparse +import os +import subprocess +from urllib.parse import quote + +import mozpack.path as mozpath +from mach.decorators import Command, CommandArgument + +from mozbuild.backend import backends +from mozbuild.mozconfig import MozconfigLoader +from mozbuild.util import MOZBUILD_METRICS_PATH + +BUILD_WHAT_HELP = """ +What to build. Can be a top-level make target or a relative directory. If +multiple options are provided, they will be built serially. BUILDING ONLY PARTS +OF THE TREE CAN RESULT IN BAD TREE STATE. USE AT YOUR OWN RISK. +""".strip() + + +def _set_priority(priority, verbose): + # Choose the Windows API structure to standardize on. + PRIO_CLASS_BY_KEY = { + "idle": "IDLE_PRIORITY_CLASS", + "less": "BELOW_NORMAL_PRIORITY_CLASS", + "normal": "NORMAL_PRIORITY_CLASS", + "more": "ABOVE_NORMAL_PRIORITY_CLASS", + "high": "HIGH_PRIORITY_CLASS", + } + try: + prio_class = PRIO_CLASS_BY_KEY[priority] + except KeyError: + raise KeyError(f"priority '{priority}' not in {list(PRIO_CLASS_BY_KEY)}") + + if "nice" in dir(os): + # Translate the Windows priority classes into niceness values. + NICENESS_BY_PRIO_CLASS = { + "IDLE_PRIORITY_CLASS": 19, + "BELOW_NORMAL_PRIORITY_CLASS": 10, + "NORMAL_PRIORITY_CLASS": 0, + "ABOVE_NORMAL_PRIORITY_CLASS": -10, + "HIGH_PRIORITY_CLASS": -20, + } + niceness = NICENESS_BY_PRIO_CLASS[prio_class] + + os.nice(niceness) + if verbose: + print(f"os.nice({niceness})") + return True + + try: + import psutil + + prio_class_val = getattr(psutil, prio_class) + except ModuleNotFoundError: + return False + except AttributeError: + return False + + psutil.Process().nice(prio_class_val) + if verbose: + print(f"psutil.Process().nice(psutil.{prio_class})") + return True + + +# Interface to build the tree. + + +@Command( + "build", + category="build", + description="Build the tree.", + metrics_path=MOZBUILD_METRICS_PATH, + virtualenv_name="build", +) +@CommandArgument( + "--jobs", + "-j", + default="0", + metavar="jobs", + type=int, + help="Number of concurrent jobs to run. Default is based on the number of " + "CPUs and the estimated size of the jobs (see --job-size).", +) +@CommandArgument( + "--job-size", + default="0", + metavar="size", + type=float, + help="Estimated RAM required, in GiB, for each parallel job. Used to " + "compute a default number of concurrent jobs.", +) +@CommandArgument( + "-C", + "--directory", + default=None, + help="Change to a subdirectory of the build directory first.", +) +@CommandArgument("what", default=None, nargs="*", help=BUILD_WHAT_HELP) +@CommandArgument( + "-v", + "--verbose", + action="store_true", + help="Verbose output for what commands the build is running.", +) +@CommandArgument( + "--keep-going", + action="store_true", + help="Keep building after an error has occurred", +) +@CommandArgument( + "--priority", + default="less", + metavar="priority", + type=str, + help="idle/less/normal/more/high. (Default less)", +) +def build( + command_context, + what=None, + jobs=0, + job_size=0, + directory=None, + verbose=False, + keep_going=False, + priority="less", +): + """Build the source tree. + + With no arguments, this will perform a full build. + + Positional arguments define targets to build. These can be make targets + or patterns like "<dir>/<target>" to indicate a make target within a + directory. + + There are a few special targets that can be used to perform a partial + build faster than what `mach build` would perform: + + * binaries - compiles and links all C/C++ sources and produces shared + libraries and executables (binaries). + + * faster - builds JavaScript, XUL, CSS, etc files. + + "binaries" and "faster" almost fully complement each other. However, + there are build actions not captured by either. If things don't appear to + be rebuilding, perform a vanilla `mach build` to rebuild the world. + """ + from mozbuild.controller.building import BuildDriver + + command_context.log_manager.enable_all_structured_loggers() + + loader = MozconfigLoader(command_context.topsrcdir) + mozconfig = loader.read_mozconfig(loader.AUTODETECT) + configure_args = mozconfig["configure_args"] + doing_pgo = configure_args and "MOZ_PGO=1" in configure_args + # Force verbosity on automation. + verbose = verbose or bool(os.environ.get("MOZ_AUTOMATION", False)) + # Keep going by default on automation so that we exhaust as many errors as + # possible. + keep_going = keep_going or bool(os.environ.get("MOZ_AUTOMATION", False)) + append_env = None + + # By setting the current process's priority, by default our child processes + # will also inherit this same priority. + if not _set_priority(priority, verbose): + print("--priority not supported on this platform.") + + if doing_pgo: + if what: + raise Exception("Cannot specify targets (%s) in MOZ_PGO=1 builds" % what) + instr = command_context._spawn(BuildDriver) + orig_topobjdir = instr._topobjdir + instr._topobjdir = mozpath.join(instr._topobjdir, "instrumented") + + append_env = {"MOZ_PROFILE_GENERATE": "1"} + status = instr.build( + command_context.metrics, + what=what, + jobs=jobs, + job_size=job_size, + directory=directory, + verbose=verbose, + keep_going=keep_going, + mach_context=command_context._mach_context, + append_env=append_env, + ) + if status != 0: + return status + + # Packaging the instrumented build is required to get the jarlog + # data. + status = instr._run_make( + directory=".", + target="package", + silent=not verbose, + ensure_exit_code=False, + append_env=append_env, + ) + if status != 0: + return status + + pgo_env = os.environ.copy() + if instr.config_environment.substs.get("CC_TYPE") in ("clang", "clang-cl"): + pgo_env["LLVM_PROFDATA"] = instr.config_environment.substs.get( + "LLVM_PROFDATA" + ) + pgo_env["JARLOG_FILE"] = mozpath.join(orig_topobjdir, "jarlog/en-US.log") + pgo_cmd = [ + command_context.virtualenv_manager.python_path, + mozpath.join(command_context.topsrcdir, "build/pgo/profileserver.py"), + ] + subprocess.check_call(pgo_cmd, cwd=instr.topobjdir, env=pgo_env) + + # Set the default build to MOZ_PROFILE_USE + append_env = {"MOZ_PROFILE_USE": "1"} + + driver = command_context._spawn(BuildDriver) + return driver.build( + command_context.metrics, + what=what, + jobs=jobs, + job_size=job_size, + directory=directory, + verbose=verbose, + keep_going=keep_going, + mach_context=command_context._mach_context, + append_env=append_env, + ) + + +@Command( + "configure", + category="build", + description="Configure the tree (run configure and config.status).", + metrics_path=MOZBUILD_METRICS_PATH, + virtualenv_name="build", +) +@CommandArgument( + "options", default=None, nargs=argparse.REMAINDER, help="Configure options" +) +def configure( + command_context, + options=None, + buildstatus_messages=False, + line_handler=None, +): + from mozbuild.controller.building import BuildDriver + + command_context.log_manager.enable_all_structured_loggers() + driver = command_context._spawn(BuildDriver) + + return driver.configure( + command_context.metrics, + options=options, + buildstatus_messages=buildstatus_messages, + line_handler=line_handler, + ) + + +@Command( + "resource-usage", + category="post-build", + description="Show a profile of the build in the Firefox Profiler.", + virtualenv_name="build", +) +@CommandArgument( + "--address", + default="localhost", + help="Address the HTTP server should listen on.", +) +@CommandArgument( + "--port", + type=int, + default=0, + help="Port number the HTTP server should listen on.", +) +@CommandArgument( + "--browser", + default="firefox", + help="Web browser to automatically open. See webbrowser Python module.", +) +@CommandArgument("--url", help="URL of a build profile to display") +def resource_usage(command_context, address=None, port=None, browser=None, url=None): + import webbrowser + + from mozbuild.html_build_viewer import BuildViewerServer + + server = BuildViewerServer(address, port) + + if url: + server.add_resource_json_url("profile", url) + else: + profile = command_context._get_state_filename("profile_build_resources.json") + if not os.path.exists(profile): + print( + "Build resources not available. If you have performed a " + "build and receive this message, the psutil Python package " + "likely failed to initialize properly." + ) + return 1 + + server.add_resource_json_file("profile", profile) + + profiler_url = "https://profiler.firefox.com/from-url/" + quote( + server.url + "resources/profile", "" + ) + try: + webbrowser.get(browser).open_new_tab(profiler_url) + except Exception: + print("Cannot get browser specified, trying the default instead.") + try: + browser = webbrowser.get().open_new_tab(profiler_url) + except Exception: + print("Please open %s in a browser." % profiler_url) + + server.run() + + +@Command( + "build-backend", + category="build", + description="Generate a backend used to build the tree.", + virtualenv_name="build", +) +@CommandArgument("-d", "--diff", action="store_true", help="Show a diff of changes.") +# It would be nice to filter the choices below based on +# conditions, but that is for another day. +@CommandArgument( + "-b", + "--backend", + nargs="+", + choices=sorted(backends), + help="Which backend to build.", +) +@CommandArgument("-v", "--verbose", action="store_true", help="Verbose output.") +@CommandArgument( + "-n", + "--dry-run", + action="store_true", + help="Do everything except writing files out.", +) +def build_backend(command_context, backend, diff=False, verbose=False, dry_run=False): + python = command_context.virtualenv_manager.python_path + config_status = os.path.join(command_context.topobjdir, "config.status") + + if not os.path.exists(config_status): + print( + "config.status not found. Please run |mach configure| " + "or |mach build| prior to building the %s build backend." % backend + ) + return 1 + + args = [python, config_status] + if backend: + args.append("--backend") + args.extend(backend) + if diff: + args.append("--diff") + if verbose: + args.append("--verbose") + if dry_run: + args.append("--dry-run") + + return command_context._run_command_in_objdir( + args=args, pass_thru=True, ensure_exit_code=False + ) diff --git a/python/mozbuild/mozbuild/chunkify.py b/python/mozbuild/mozbuild/chunkify.py new file mode 100644 index 0000000000..b2c1057450 --- /dev/null +++ b/python/mozbuild/mozbuild/chunkify.py @@ -0,0 +1,56 @@ +# 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/. + +# This file is a direct clone of +# https://github.com/bhearsum/chunkify/blob/master/chunkify/__init__.py +# of version 1.2. Its license (MPL2) is contained in repo root LICENSE file. +# Please make modifications there where possible. + +from itertools import islice + + +class ChunkingError(Exception): + pass + + +def split_evenly(n, chunks): + """Split an integer into evenly distributed list + + >>> split_evenly(7, 3) + [3, 2, 2] + + >>> split_evenly(12, 3) + [4, 4, 4] + + >>> split_evenly(35, 10) + [4, 4, 4, 4, 4, 3, 3, 3, 3, 3] + + >>> split_evenly(1, 2) + Traceback (most recent call last): + ... + ChunkingError: Number of chunks is greater than number + + """ + if n < chunks: + raise ChunkingError("Number of chunks is greater than number") + if n % chunks == 0: + # Either we can evenly split or only 1 chunk left + return [n // chunks] * chunks + # otherwise the current chunk should be a bit larger + max_size = n // chunks + 1 + return [max_size] + split_evenly(n - max_size, chunks - 1) + + +def chunkify(things, this_chunk, chunks): + if this_chunk > chunks: + raise ChunkingError("this_chunk is greater than total chunks") + + dist = split_evenly(len(things), chunks) + start = sum(dist[: this_chunk - 1]) + end = start + dist[this_chunk - 1] + + try: + return things[start:end] + except TypeError: + return islice(things, start, end) diff --git a/python/mozbuild/mozbuild/code_analysis/__init__.py b/python/mozbuild/mozbuild/code_analysis/__init__.py new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/python/mozbuild/mozbuild/code_analysis/__init__.py diff --git a/python/mozbuild/mozbuild/code_analysis/mach_commands.py b/python/mozbuild/mozbuild/code_analysis/mach_commands.py new file mode 100644 index 0000000000..a65d35c3cf --- /dev/null +++ b/python/mozbuild/mozbuild/code_analysis/mach_commands.py @@ -0,0 +1,1971 @@ +# 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/. +import concurrent.futures +import json +import logging +import multiprocessing +import ntpath +import os +import pathlib +import posixpath +import re +import shutil +import subprocess +import sys +import tempfile +import xml.etree.ElementTree as ET +from types import SimpleNamespace + +import mozpack.path as mozpath +import six +import yaml +from mach.decorators import Command, CommandArgument, SubCommand +from mach.main import Mach +from mozversioncontrol import get_repository_object +from six.moves import input + +from mozbuild import build_commands +from mozbuild.controller.clobber import Clobberer +from mozbuild.nodeutil import find_node_executable +from mozbuild.util import memoize + + +# Function used to run clang-format on a batch of files. It is a helper function +# in order to integrate into the futures ecosystem clang-format. +def run_one_clang_format_batch(args): + try: + subprocess.check_output(args) + except subprocess.CalledProcessError as e: + return e + + +def build_repo_relative_path(abs_path, repo_path): + """Build path relative to repository root""" + + if os.path.islink(abs_path): + abs_path = mozpath.realpath(abs_path) + + return mozpath.relpath(abs_path, repo_path) + + +def prompt_bool(prompt, limit=5): + """Prompts the user with prompt and requires a boolean value.""" + from distutils.util import strtobool + + for _ in range(limit): + try: + return strtobool(input(prompt + "[Y/N]\n")) + except ValueError: + print( + "ERROR! Please enter a valid option! Please use any of the following:" + " Y, N, True, False, 1, 0" + ) + return False + + +class StaticAnalysisSubCommand(SubCommand): + def __call__(self, func): + after = SubCommand.__call__(self, func) + args = [ + CommandArgument( + "--verbose", "-v", action="store_true", help="Print verbose output." + ) + ] + for arg in args: + after = arg(after) + return after + + +class StaticAnalysisMonitor(object): + def __init__(self, srcdir, objdir, checks, total): + self._total = total + self._processed = 0 + self._current = None + self._srcdir = srcdir + + import copy + + self._checks = copy.deepcopy(checks) + + # Transform the configuration to support Regex + for item in self._checks: + if item["name"] == "-*": + continue + item["name"] = item["name"].replace("*", ".*") + + from mozbuild.compilation.warnings import WarningsCollector, WarningsDatabase + + self._warnings_database = WarningsDatabase() + + def on_warning(warning): + # Output paths relative to repository root if the paths are under repo tree + warning["filename"] = build_repo_relative_path( + warning["filename"], self._srcdir + ) + + self._warnings_database.insert(warning) + + self._warnings_collector = WarningsCollector(on_warning, objdir=objdir) + + @property + def num_files(self): + return self._total + + @property + def num_files_processed(self): + return self._processed + + @property + def current_file(self): + return self._current + + @property + def warnings_db(self): + return self._warnings_database + + def on_line(self, line): + warning = None + + try: + warning = self._warnings_collector.process_line(line) + except Exception: + pass + + if line.find("clang-tidy") != -1: + filename = line.split(" ")[-1] + if os.path.isfile(filename): + self._current = build_repo_relative_path(filename, self._srcdir) + else: + self._current = None + self._processed = self._processed + 1 + return (warning, False) + if warning is not None: + + def get_check_config(checker_name): + # get the matcher from self._checks that is the 'name' field + for item in self._checks: + if item["name"] == checker_name: + return item + + # We are using a regex in order to also match 'mozilla-.* like checkers' + matcher = re.match(item["name"], checker_name) + if matcher is not None and matcher.group(0) == checker_name: + return item + + check_config = get_check_config(warning["flag"]) + if check_config is not None: + warning["reliability"] = check_config.get("reliability", "low") + warning["reason"] = check_config.get("reason") + warning["publish"] = check_config.get("publish", True) + elif warning["flag"] == "clang-diagnostic-error": + # For a "warning" that is flagged as "clang-diagnostic-error" + # set it as "publish" + warning["publish"] = True + + return (warning, True) + + +# Utilities for running C++ static analysis checks and format. + +# List of file extension to consider (should start with dot) +_format_include_extensions = (".cpp", ".c", ".cc", ".h", ".m", ".mm") +# File contaning all paths to exclude from formatting +_format_ignore_file = ".clang-format-ignore" + +# (TOOLS) Function return codes +TOOLS_SUCCESS = 0 +TOOLS_FAILED_DOWNLOAD = 1 +TOOLS_UNSUPORTED_PLATFORM = 2 +TOOLS_CHECKER_NO_TEST_FILE = 3 +TOOLS_CHECKER_RETURNED_NO_ISSUES = 4 +TOOLS_CHECKER_RESULT_FILE_NOT_FOUND = 5 +TOOLS_CHECKER_DIFF_FAILED = 6 +TOOLS_CHECKER_NOT_FOUND = 7 +TOOLS_CHECKER_FAILED_FILE = 8 +TOOLS_CHECKER_LIST_EMPTY = 9 +TOOLS_GRADLE_FAILED = 10 + + +@Command( + "clang-tidy", + category="devenv", + description="Convenience alias for the static-analysis command", +) +def clang_tidy(command_context): + # If no arguments are provided, just print a help message. + """Detailed documentation: + https://firefox-source-docs.mozilla.org/code-quality/static-analysis/index.html + """ + mach = Mach(os.getcwd()) + + def populate_context(key=None): + if key == "topdir": + return command_context.topsrcdir + + mach.populate_context_handler = populate_context + mach.run(["static-analysis", "--help"]) + + +@Command( + "static-analysis", + category="devenv", + description="Run C++ static analysis checks using clang-tidy", +) +def static_analysis(command_context): + # If no arguments are provided, just print a help message. + """Detailed documentation: + https://firefox-source-docs.mozilla.org/code-quality/static-analysis/index.html + """ + mach = Mach(os.getcwd()) + + def populate_context(key=None): + if key == "topdir": + return command_context.topsrcdir + + mach.populate_context_handler = populate_context + mach.run(["static-analysis", "--help"]) + + +@StaticAnalysisSubCommand( + "static-analysis", "check", "Run the checks using the helper tool" +) +@CommandArgument( + "source", + nargs="*", + default=[".*"], + help="Source files to be analyzed (regex on path). " + "Can be omitted, in which case the entire code base " + "is analyzed. The source argument is ignored if " + "there is anything fed through stdin, in which case " + "the analysis is only performed on the files changed " + "in the patch streamed through stdin. This is called " + "the diff mode.", +) +@CommandArgument( + "--checks", + "-c", + default="-*", + metavar="checks", + help="Static analysis checks to enable. By default, this enables only " + "checks that are published here: https://mzl.la/2DRHeTh, but can be any " + "clang-tidy checks syntax.", +) +@CommandArgument( + "--jobs", + "-j", + default="0", + metavar="jobs", + type=int, + help="Number of concurrent jobs to run. Default is the number of CPUs.", +) +@CommandArgument( + "--strip", + "-p", + default="1", + metavar="NUM", + help="Strip NUM leading components from file names in diff mode.", +) +@CommandArgument( + "--fix", + "-f", + default=False, + action="store_true", + help="Try to autofix errors detected by clang-tidy checkers.", +) +@CommandArgument( + "--header-filter", + "-h-f", + default="", + metavar="header_filter", + help="Regular expression matching the names of the headers to " + "output diagnostics from. Diagnostics from the main file " + "of each translation unit are always displayed", +) +@CommandArgument( + "--output", "-o", default=None, help="Write clang-tidy output in a file" +) +@CommandArgument( + "--format", + default="text", + choices=("text", "json"), + help="Output format to write in a file", +) +@CommandArgument( + "--outgoing", + default=False, + action="store_true", + help="Run static analysis checks on outgoing files from mercurial repository", +) +def check( + command_context, + source=None, + jobs=2, + strip=1, + verbose=False, + checks="-*", + fix=False, + header_filter="", + output=None, + format="text", + outgoing=False, +): + from mozbuild.controller.building import ( + StaticAnalysisFooter, + StaticAnalysisOutputManager, + ) + + command_context._set_log_level(verbose) + command_context.activate_virtualenv() + command_context.log_manager.enable_unstructured() + + rc, clang_paths = get_clang_tools(command_context, verbose=verbose) + if rc != 0: + return rc + + if not _is_version_eligible(command_context, clang_paths): + return 1 + + rc, _compile_db, compilation_commands_path = _build_compile_db( + command_context, verbose=verbose + ) + rc = rc or _build_export(command_context, jobs=jobs, verbose=verbose) + if rc != 0: + return rc + + # Use outgoing files instead of source files + if outgoing: + repo = get_repository_object(command_context.topsrcdir) + files = repo.get_outgoing_files() + source = get_abspath_files(command_context, files) + + # Split in several chunks to avoid hitting Python's limit of 100 groups in re + compile_db = json.loads(open(_compile_db, "r").read()) + total = 0 + import re + + chunk_size = 50 + for offset in range(0, len(source), chunk_size): + source_chunks = [ + re.escape(f) for f in source[offset : offset + chunk_size].copy() + ] + name_re = re.compile("(" + ")|(".join(source_chunks) + ")") + for f in compile_db: + if name_re.search(f["file"]): + total = total + 1 + + # Filter source to remove excluded files + source = _generate_path_list(command_context, source, verbose=verbose) + + if not total or not source: + command_context.log( + logging.INFO, + "static-analysis", + {}, + "There are no files eligible for analysis. Please note that 'header' files " + "cannot be used for analysis since they do not consist compilation units.", + ) + return 0 + + # Escape the files from source + source = [re.escape(f) for f in source] + + cwd = command_context.topobjdir + + monitor = StaticAnalysisMonitor( + command_context.topsrcdir, + command_context.topobjdir, + get_clang_tidy_config(command_context).checks_with_data, + total, + ) + + footer = StaticAnalysisFooter(command_context.log_manager.terminal, monitor) + + with StaticAnalysisOutputManager( + command_context.log_manager, monitor, footer + ) as output_manager: + import math + + batch_size = int(math.ceil(float(len(source)) / multiprocessing.cpu_count())) + for i in range(0, len(source), batch_size): + args = _get_clang_tidy_command( + command_context, + clang_paths, + compilation_commands_path, + checks=checks, + header_filter=header_filter, + sources=source[i : (i + batch_size)], + jobs=jobs, + fix=fix, + ) + rc = command_context.run_process( + args=args, + ensure_exit_code=False, + line_handler=output_manager.on_line, + cwd=cwd, + ) + + command_context.log( + logging.WARNING, + "warning_summary", + {"count": len(monitor.warnings_db)}, + "{count} warnings present.", + ) + + # Write output file + if output is not None: + output_manager.write(output, format) + + return rc + + +def get_abspath_files(command_context, files): + return [mozpath.join(command_context.topsrcdir, f) for f in files] + + +def get_files_with_commands(command_context, compile_db, source): + """ + Returns an array of dictionaries having file_path with build command + """ + + compile_db = json.load(open(compile_db, "r")) + + commands_list = [] + + for f in source: + # It must be a C/C++ file + _, ext = os.path.splitext(f) + + if ext.lower() not in _format_include_extensions: + command_context.log( + logging.INFO, "static-analysis", {}, "Skipping {}".format(f) + ) + continue + file_with_abspath = os.path.join(command_context.topsrcdir, f) + for f in compile_db: + # Found for a file that we are looking + if file_with_abspath == f["file"]: + commands_list.append(f) + + return commands_list + + +@memoize +def get_clang_tidy_config(command_context): + from mozbuild.code_analysis.utils import ClangTidyConfig + + return ClangTidyConfig(command_context.topsrcdir) + + +def _get_required_version(command_context): + version = get_clang_tidy_config(command_context).version + if version is None: + command_context.log( + logging.ERROR, + "static-analysis", + {}, + "ERROR: Unable to find 'package_version' in config.yml", + ) + return version + + +def _get_current_version(command_context, clang_paths): + # Because the fact that we ship together clang-tidy and clang-format + # we are sure that these two will always share the same version. + # Thus in order to determine that the version is compatible we only + # need to check one of them, going with clang-format + cmd = [clang_paths._clang_format_path, "--version"] + version_info = None + try: + version_info = ( + subprocess.check_output(cmd, stderr=subprocess.STDOUT) + .decode("utf-8") + .strip() + ) + + if "MOZ_AUTOMATION" in os.environ: + # Only show it in the CI + command_context.log( + logging.INFO, + "static-analysis", + {}, + "{} Version = {} ".format(clang_paths._clang_format_path, version_info), + ) + + except subprocess.CalledProcessError as e: + command_context.log( + logging.ERROR, + "static-analysis", + {}, + "Error determining the version clang-tidy/format binary, please see the " + "attached exception: \n{}".format(e.output), + ) + return version_info + + +def _is_version_eligible(command_context, clang_paths, log_error=True): + version = _get_required_version(command_context) + if version is None: + return False + + current_version = _get_current_version(command_context, clang_paths) + if current_version is None: + return False + version = "clang-format version " + version + if version in current_version: + return True + + if log_error: + command_context.log( + logging.ERROR, + "static-analysis", + {}, + "ERROR: You're using an old or incorrect version ({}) of clang-format binary. " + "Please update to a more recent one (at least > {}) " + "by running: './mach bootstrap' ".format( + _get_current_version(command_context, clang_paths), + _get_required_version(command_context), + ), + ) + + return False + + +def _get_clang_tidy_command( + command_context, + clang_paths, + compilation_commands_path, + checks, + header_filter, + sources, + jobs, + fix, +): + if checks == "-*": + checks = ",".join(get_clang_tidy_config(command_context).checks) + + common_args = [ + "-clang-tidy-binary", + clang_paths._clang_tidy_path, + "-clang-apply-replacements-binary", + clang_paths._clang_apply_replacements, + "-checks=%s" % checks, + "-extra-arg=-DMOZ_CLANG_PLUGIN", + ] + + # Flag header-filter is passed in order to limit the diagnostic messages only + # to the specified header files. When no value is specified the default value + # is considered to be the source in order to limit the diagnostic message to + # the source files or folders. + common_args += [ + "-header-filter=%s" + % (header_filter if len(header_filter) else "|".join(sources)) + ] + + # From our configuration file, config.yaml, we build the configuration list, for + # the checkers that are used. These configuration options are used to better fit + # the checkers to our code. + cfg = get_clang_tidy_config(command_context).checks_config + if cfg: + common_args += ["-config=%s" % yaml.dump(cfg)] + + if fix: + common_args += ["-fix"] + + return ( + [ + command_context.virtualenv_manager.python_path, + clang_paths._run_clang_tidy_path, + "-j", + str(jobs), + "-p", + compilation_commands_path, + ] + + common_args + + sources + ) + + +@StaticAnalysisSubCommand( + "static-analysis", + "autotest", + "Run the auto-test suite in order to determine that" + " the analysis did not regress.", +) +@CommandArgument( + "--dump-results", + "-d", + default=False, + action="store_true", + help="Generate the baseline for the regression test. Based on" + " this baseline we will test future results.", +) +@CommandArgument( + "--intree-tool", + "-i", + default=False, + action="store_true", + help="Use a pre-aquired in-tree clang-tidy package from the automation env." + " This option is only valid on automation environments.", +) +@CommandArgument( + "checker_names", + nargs="*", + default=[], + help="Checkers that are going to be auto-tested.", +) +def autotest( + command_context, + verbose=False, + dump_results=False, + intree_tool=False, + checker_names=[], +): + # If 'dump_results' is True than we just want to generate the issues files for each + # checker in particulat and thus 'force_download' becomes 'False' since we want to + # do this on a local trusted clang-tidy package. + command_context._set_log_level(verbose) + command_context.activate_virtualenv() + dump_results = dump_results + + force_download = not dump_results + + # Configure the tree or download clang-tidy package, depending on the option that we choose + if intree_tool: + clang_paths = SimpleNamespace() + if "MOZ_AUTOMATION" not in os.environ: + command_context.log( + logging.INFO, + "static-analysis", + {}, + "The `autotest` with `--intree-tool` can only be ran in automation.", + ) + return 1 + if "MOZ_FETCHES_DIR" not in os.environ: + command_context.log( + logging.INFO, + "static-analysis", + {}, + "`MOZ_FETCHES_DIR` is missing from the environment variables.", + ) + return 1 + + _, config, _ = _get_config_environment(command_context) + clang_tools_path = os.environ["MOZ_FETCHES_DIR"] + clang_paths._clang_tidy_path = mozpath.join( + clang_tools_path, + "clang-tidy", + "bin", + "clang-tidy" + config.substs.get("HOST_BIN_SUFFIX", ""), + ) + clang_paths._clang_format_path = mozpath.join( + clang_tools_path, + "clang-tidy", + "bin", + "clang-format" + config.substs.get("HOST_BIN_SUFFIX", ""), + ) + clang_paths._clang_apply_replacements = mozpath.join( + clang_tools_path, + "clang-tidy", + "bin", + "clang-apply-replacements" + config.substs.get("HOST_BIN_SUFFIX", ""), + ) + clang_paths._run_clang_tidy_path = mozpath.join( + clang_tools_path, "clang-tidy", "bin", "run-clang-tidy" + ) + clang_paths._clang_format_diff = mozpath.join( + clang_tools_path, "clang-tidy", "share", "clang", "clang-format-diff.py" + ) + + # Ensure that clang-tidy is present + rc = not os.path.exists(clang_paths._clang_tidy_path) + else: + rc, clang_paths = get_clang_tools( + command_context, force=force_download, verbose=verbose + ) + + if rc != 0: + command_context.log( + logging.ERROR, + "ERROR: static-analysis", + {}, + "ERROR: clang-tidy unable to locate package.", + ) + return TOOLS_FAILED_DOWNLOAD + + clang_paths._clang_tidy_base_path = mozpath.join( + command_context.topsrcdir, "tools", "clang-tidy" + ) + + # For each checker run it + platform, _ = command_context.platform + + if platform not in get_clang_tidy_config(command_context).platforms: + command_context.log( + logging.ERROR, + "static-analysis", + {}, + "ERROR: RUNNING: clang-tidy autotest for platform {} not supported.".format( + platform + ), + ) + return TOOLS_UNSUPORTED_PLATFORM + + max_workers = multiprocessing.cpu_count() + + command_context.log( + logging.INFO, + "static-analysis", + {}, + "RUNNING: clang-tidy autotest for platform {0} with {1} workers.".format( + platform, max_workers + ), + ) + + # List all available checkers + cmd = [clang_paths._clang_tidy_path, "-list-checks", "-checks=*"] + clang_output = subprocess.check_output(cmd, stderr=subprocess.STDOUT).decode( + "utf-8" + ) + available_checks = clang_output.split("\n")[1:] + clang_tidy_checks = [c.strip() for c in available_checks if c] + + # Build the dummy compile_commands.json + compilation_commands_path = _create_temp_compilation_db(command_context) + checkers_test_batch = [] + checkers_results = [] + with concurrent.futures.ThreadPoolExecutor(max_workers=max_workers) as executor: + futures = [] + for item in get_clang_tidy_config(command_context).checks_with_data: + # Skip if any of the following statements is true: + # 1. Checker attribute 'publish' is False. + not_published = not bool(item.get("publish", True)) + # 2. Checker has restricted-platforms and current platform is not of them. + ignored_platform = ( + "restricted-platforms" in item + and platform not in item["restricted-platforms"] + ) + # 3. Checker name is mozilla-* or -*. + ignored_checker = item["name"] in ["mozilla-*", "-*"] + # 4. List checker_names is passed and the current checker is not part of the + # list or 'publish' is False + checker_not_in_list = checker_names and ( + item["name"] not in checker_names or not_published + ) + if ( + not_published + or ignored_platform + or ignored_checker + or checker_not_in_list + ): + continue + checkers_test_batch.append(item["name"]) + futures.append( + executor.submit( + _verify_checker, + command_context, + clang_paths, + compilation_commands_path, + dump_results, + clang_tidy_checks, + item, + checkers_results, + ) + ) + + error_code = TOOLS_SUCCESS + for future in concurrent.futures.as_completed(futures): + # Wait for every task to finish + ret_val = future.result() + if ret_val != TOOLS_SUCCESS: + # We are interested only in one error and we don't break + # the execution of for loop since we want to make sure that all + # tasks finished. + error_code = ret_val + + if error_code != TOOLS_SUCCESS: + command_context.log( + logging.INFO, + "static-analysis", + {}, + "FAIL: the following clang-tidy check(s) failed:", + ) + for failure in checkers_results: + checker_error = failure["checker-error"] + checker_name = failure["checker-name"] + info1 = failure["info1"] + info2 = failure["info2"] + info3 = failure["info3"] + + message_to_log = "" + if checker_error == TOOLS_CHECKER_NOT_FOUND: + message_to_log = ( + "\tChecker " + "{} not present in this clang-tidy version.".format( + checker_name + ) + ) + elif checker_error == TOOLS_CHECKER_NO_TEST_FILE: + message_to_log = ( + "\tChecker " + "{0} does not have a test file - {0}.cpp".format(checker_name) + ) + elif checker_error == TOOLS_CHECKER_RETURNED_NO_ISSUES: + message_to_log = ( + "\tChecker {0} did not find any issues in its test file, " + "clang-tidy output for the run is:\n{1}" + ).format(checker_name, info1) + elif checker_error == TOOLS_CHECKER_RESULT_FILE_NOT_FOUND: + message_to_log = ( + "\tChecker {0} does not have a result file - {0}.json" + ).format(checker_name) + elif checker_error == TOOLS_CHECKER_DIFF_FAILED: + message_to_log = ( + "\tChecker {0}\nExpected: {1}\n" + "Got: {2}\n" + "clang-tidy output for the run is:\n" + "{3}" + ).format(checker_name, info1, info2, info3) + + print("\n" + message_to_log) + + # Also delete the tmp folder + shutil.rmtree(compilation_commands_path) + return error_code + + # Run the analysis on all checkers at the same time only if we don't dump results. + if not dump_results: + ret_val = _run_analysis_batch( + command_context, + clang_paths, + compilation_commands_path, + checkers_test_batch, + ) + if ret_val != TOOLS_SUCCESS: + shutil.rmtree(compilation_commands_path) + return ret_val + + command_context.log( + logging.INFO, "static-analysis", {}, "SUCCESS: clang-tidy all tests passed." + ) + # Also delete the tmp folder + shutil.rmtree(compilation_commands_path) + + +def _run_analysis( + command_context, + clang_paths, + compilation_commands_path, + checks, + header_filter, + sources, + jobs=1, + fix=False, + print_out=False, +): + cmd = _get_clang_tidy_command( + command_context, + clang_paths, + compilation_commands_path, + checks=checks, + header_filter=header_filter, + sources=sources, + jobs=jobs, + fix=fix, + ) + + try: + clang_output = subprocess.check_output(cmd, stderr=subprocess.STDOUT).decode( + "utf-8" + ) + except subprocess.CalledProcessError as e: + print(e.output) + return None + return _parse_issues(command_context, clang_output), clang_output + + +def _run_analysis_batch(command_context, clang_paths, compilation_commands_path, items): + command_context.log( + logging.INFO, + "static-analysis", + {}, + "RUNNING: clang-tidy checker batch analysis.", + ) + if not len(items): + command_context.log( + logging.ERROR, + "static-analysis", + {}, + "ERROR: clang-tidy checker list is empty!", + ) + return TOOLS_CHECKER_LIST_EMPTY + + issues, clang_output = _run_analysis( + command_context, + clang_paths, + compilation_commands_path, + checks="-*," + ",".join(items), + header_filter="", + sources=[ + mozpath.join(clang_paths._clang_tidy_base_path, "test", checker) + ".cpp" + for checker in items + ], + print_out=True, + ) + + if issues is None: + return TOOLS_CHECKER_FAILED_FILE + + failed_checks = [] + failed_checks_baseline = [] + for checker in items: + test_file_path_json = ( + mozpath.join(clang_paths._clang_tidy_base_path, "test", checker) + ".json" + ) + # Read the pre-determined issues + baseline_issues = _get_autotest_stored_issues(test_file_path_json) + + # We also stored the 'reliability' index so strip that from the baseline_issues + baseline_issues[:] = [ + item for item in baseline_issues if "reliability" not in item + ] + + found = all([element_base in issues for element_base in baseline_issues]) + + if not found: + failed_checks.append(checker) + failed_checks_baseline.append(baseline_issues) + + if len(failed_checks) > 0: + command_context.log( + logging.ERROR, + "static-analysis", + {}, + "ERROR: The following check(s) failed for bulk analysis: " + + " ".join(failed_checks), + ) + + for failed_check, baseline_issue in zip(failed_checks, failed_checks_baseline): + print( + "\tChecker {0} expect following results: \n\t\t{1}".format( + failed_check, baseline_issue + ) + ) + + print( + "This is the output generated by clang-tidy for the bulk build:\n{}".format( + clang_output + ) + ) + return TOOLS_CHECKER_DIFF_FAILED + + return TOOLS_SUCCESS + + +def _create_temp_compilation_db(command_context): + directory = tempfile.mkdtemp(prefix="cc") + with open(mozpath.join(directory, "compile_commands.json"), "w") as file_handler: + compile_commands = [] + director = mozpath.join( + command_context.topsrcdir, "tools", "clang-tidy", "test" + ) + for item in get_clang_tidy_config(command_context).checks: + if item in ["-*", "mozilla-*"]: + continue + file = item + ".cpp" + element = {} + element["directory"] = director + element["command"] = "cpp -std=c++17 " + file + element["file"] = mozpath.join(director, file) + compile_commands.append(element) + + json.dump(compile_commands, file_handler) + file_handler.flush() + + return directory + + +@StaticAnalysisSubCommand( + "static-analysis", "install", "Install the static analysis helper tool" +) +@CommandArgument( + "source", + nargs="?", + type=str, + help="Where to fetch a local archive containing the static-analysis and " + "format helper tool." + "It will be installed in ~/.mozbuild/clang-tools." + "Can be omitted, in which case the latest clang-tools " + "helper for the platform would be automatically detected and installed.", +) +@CommandArgument( + "--skip-cache", + action="store_true", + help="Skip all local caches to force re-fetching the helper tool.", + default=False, +) +@CommandArgument( + "--force", + action="store_true", + help="Force re-install even though the tool exists in mozbuild.", + default=False, +) +def install( + command_context, + source=None, + skip_cache=False, + force=False, + verbose=False, +): + command_context._set_log_level(verbose) + rc, _ = get_clang_tools( + command_context, + force=force, + skip_cache=skip_cache, + source=source, + verbose=verbose, + ) + return rc + + +@StaticAnalysisSubCommand( + "static-analysis", + "clear-cache", + "Delete local helpers and reset static analysis helper tool cache", +) +def clear_cache(command_context, verbose=False): + command_context._set_log_level(verbose) + rc, _ = get_clang_tools( + command_context, + force=True, + download_if_needed=True, + skip_cache=True, + verbose=verbose, + ) + + if rc != 0: + return rc + + from mozbuild.artifact_commands import artifact_clear_cache + + return artifact_clear_cache(command_context) + + +@StaticAnalysisSubCommand( + "static-analysis", + "print-checks", + "Print a list of the static analysis checks performed by default", +) +def print_checks(command_context, verbose=False): + command_context._set_log_level(verbose) + rc, clang_paths = get_clang_tools(command_context, verbose=verbose) + + if rc != 0: + return rc + + args = [ + clang_paths._clang_tidy_path, + "-list-checks", + "-checks=%s" % get_clang_tidy_config(command_context).checks, + ] + + return command_context.run_process(args=args, pass_thru=True) + + +@Command( + "prettier-format", + category="misc", + description="Run prettier on current changes", +) +@CommandArgument( + "--path", + "-p", + nargs=1, + required=True, + help="Specify the path to reformat to stdout.", +) +@CommandArgument( + "--assume-filename", + "-a", + nargs=1, + required=True, + help="This option is usually used in the context of hg-formatsource." + "When reading from stdin, Prettier assumes this " + "filename to decide which style and parser to use.", +) +def prettier_format(command_context, path, assume_filename): + # With assume_filename we want to have stdout clean since the result of the + # format will be redirected to stdout. + + binary, _ = find_node_executable() + prettier = os.path.join( + command_context.topsrcdir, "node_modules", "prettier", "bin-prettier.js" + ) + path = os.path.join(command_context.topsrcdir, path[0]) + + # Bug 1564824. Prettier fails on patches with moved files where the + # original directory also does not exist. + assume_dir = os.path.dirname( + os.path.join(command_context.topsrcdir, assume_filename[0]) + ) + assume_filename = assume_filename[0] if os.path.isdir(assume_dir) else path + + # We use --stdin-filepath in order to better determine the path for + # the prettier formatter when it is ran outside of the repo, for example + # by the extension hg-formatsource. + args = [binary, prettier, "--stdin-filepath", assume_filename] + + process = subprocess.Popen(args, stdin=subprocess.PIPE) + with open(path, "rb") as fin: + process.stdin.write(fin.read()) + process.stdin.close() + process.wait() + return process.returncode + + +@Command( + "clang-format", + category="misc", + description="Run clang-format on current changes", +) +@CommandArgument( + "--show", + "-s", + action="store_const", + const="stdout", + dest="output_path", + help="Show diff output on stdout instead of applying changes", +) +@CommandArgument( + "--assume-filename", + "-a", + nargs=1, + default=None, + help="This option is usually used in the context of hg-formatsource." + "When reading from stdin, clang-format assumes this " + "filename to look for a style config file (with " + "-style=file) and to determine the language. When " + "specifying this option only one file should be used " + "as an input and the output will be forwarded to stdin. " + "This option also impairs the download of the clang-tools " + "and assumes the package is already located in it's default " + "location", +) +@CommandArgument( + "--path", "-p", nargs="+", default=None, help="Specify the path(s) to reformat" +) +@CommandArgument( + "--commit", + "-c", + default=None, + help="Specify a commit to reformat from. " + "For git you can also pass a range of commits (foo..bar) " + "to format all of them at the same time.", +) +@CommandArgument( + "--output", + "-o", + default=None, + dest="output_path", + help="Specify a file handle to write clang-format raw output instead of " + "applying changes. This can be stdout or a file path.", +) +@CommandArgument( + "--format", + "-f", + choices=("diff", "json"), + default="diff", + dest="output_format", + help="Specify the output format used: diff is the raw patch provided by " + "clang-format, json is a list of atomic changes to process.", +) +@CommandArgument( + "--outgoing", + default=False, + action="store_true", + help="Run clang-format on outgoing files from mercurial repository.", +) +def clang_format( + command_context, + assume_filename, + path, + commit, + output_path=None, + output_format="diff", + verbose=False, + outgoing=False, +): + # Run clang-format or clang-format-diff on the local changes + # or files/directories + if path is None and outgoing: + repo = get_repository_object(command_context.topsrcdir) + path = repo.get_outgoing_files() + + if path: + # Create the full path list + def path_maker(f_name): + return os.path.join(command_context.topsrcdir, f_name) + + path = map(path_maker, path) + + os.chdir(command_context.topsrcdir) + + # Load output file handle, either stdout or a file handle in write mode + output = None + if output_path is not None: + output = sys.stdout if output_path == "stdout" else open(output_path, "w") + + # With assume_filename we want to have stdout clean since the result of the + # format will be redirected to stdout. Only in case of errror we + # write something to stdout. + # We don't actually want to get the clang-tools here since we want in some + # scenarios to do this in parallel so we relay on the fact that the tools + # have already been downloaded via './mach bootstrap' or directly via + # './mach static-analysis install' + if assume_filename: + rc, clang_paths = _set_clang_tools_paths(command_context) + if rc != 0: + print("clang-format: Unable to set path to clang-format tools.") + return rc + + if not _do_clang_tools_exist(clang_paths): + print("clang-format: Unable to set locate clang-format tools.") + return 1 + + if not _is_version_eligible(command_context, clang_paths): + return 1 + else: + rc, clang_paths = get_clang_tools(command_context, verbose=verbose) + if rc != 0: + return rc + + if path is None: + return _run_clang_format_diff( + command_context, + clang_paths._clang_format_diff, + clang_paths._clang_format_path, + commit, + output, + ) + + if assume_filename: + return _run_clang_format_in_console( + command_context, clang_paths._clang_format_path, path, assume_filename + ) + + return _run_clang_format_path( + command_context, clang_paths._clang_format_path, path, output, output_format + ) + + +def _verify_checker( + command_context, + clang_paths, + compilation_commands_path, + dump_results, + clang_tidy_checks, + item, + checkers_results, +): + check = item["name"] + test_file_path = mozpath.join(clang_paths._clang_tidy_base_path, "test", check) + test_file_path_cpp = test_file_path + ".cpp" + test_file_path_json = test_file_path + ".json" + + command_context.log( + logging.INFO, + "static-analysis", + {}, + "RUNNING: clang-tidy checker {}.".format(check), + ) + + # Structured information in case a checker fails + checker_error = { + "checker-name": check, + "checker-error": "", + "info1": "", + "info2": "", + "info3": "", + } + + # Verify if this checker actually exists + if check not in clang_tidy_checks: + checker_error["checker-error"] = TOOLS_CHECKER_NOT_FOUND + checkers_results.append(checker_error) + return TOOLS_CHECKER_NOT_FOUND + + # Verify if the test file exists for this checker + if not os.path.exists(test_file_path_cpp): + checker_error["checker-error"] = TOOLS_CHECKER_NO_TEST_FILE + checkers_results.append(checker_error) + return TOOLS_CHECKER_NO_TEST_FILE + + issues, clang_output = _run_analysis( + command_context, + clang_paths, + compilation_commands_path, + checks="-*," + check, + header_filter="", + sources=[test_file_path_cpp], + ) + if issues is None: + return TOOLS_CHECKER_FAILED_FILE + + # Verify to see if we got any issues, if not raise exception + if not issues: + checker_error["checker-error"] = TOOLS_CHECKER_RETURNED_NO_ISSUES + checker_error["info1"] = clang_output + checkers_results.append(checker_error) + return TOOLS_CHECKER_RETURNED_NO_ISSUES + + # Also store the 'reliability' index for this checker + issues.append({"reliability": item["reliability"]}) + + if dump_results: + _build_autotest_result(test_file_path_json, json.dumps(issues)) + else: + if not os.path.exists(test_file_path_json): + # Result file for test not found maybe regenerate it? + checker_error["checker-error"] = TOOLS_CHECKER_RESULT_FILE_NOT_FOUND + checkers_results.append(checker_error) + return TOOLS_CHECKER_RESULT_FILE_NOT_FOUND + + # Read the pre-determined issues + baseline_issues = _get_autotest_stored_issues(test_file_path_json) + + # Compare the two lists + if issues != baseline_issues: + checker_error["checker-error"] = TOOLS_CHECKER_DIFF_FAILED + checker_error["info1"] = baseline_issues + checker_error["info2"] = issues + checker_error["info3"] = clang_output + checkers_results.append(checker_error) + return TOOLS_CHECKER_DIFF_FAILED + + return TOOLS_SUCCESS + + +def _build_autotest_result(file, issues): + with open(file, "w") as f: + f.write(issues) + + +def _get_autotest_stored_issues(file): + with open(file) as f: + return json.load(f) + + +def _parse_issues(command_context, clang_output): + """ + Parse clang-tidy output into structured issues + """ + + # Limit clang output parsing to 'Enabled checks:' + end = re.search(r"^Enabled checks:\n", clang_output, re.MULTILINE) + if end is not None: + clang_output = clang_output[: end.start() - 1] + + platform, _ = command_context.platform + re_strip_colors = re.compile(r"\x1b\[[\d;]+m", re.MULTILINE) + filtered = re_strip_colors.sub("", clang_output) + # Starting with clang 8, for the diagnostic messages we have multiple `LF CR` + # in order to be compatiable with msvc compiler format, and for this + # we are not interested to match the end of line. + regex_string = r"(.+):(\d+):(\d+): (warning|error): ([^\[\]\n]+)(?: \[([\.\w-]+)\])" + + # For non 'win' based platforms we also need the 'end of the line' regex + if platform not in ("win64", "win32"): + regex_string += "?$" + + regex_header = re.compile(regex_string, re.MULTILINE) + + # Sort headers by positions + headers = sorted(regex_header.finditer(filtered), key=lambda h: h.start()) + issues = [] + for _, header in enumerate(headers): + header_group = header.groups() + element = [header_group[3], header_group[4], header_group[5]] + issues.append(element) + return issues + + +def _get_config_environment(command_context): + ran_configure = False + config = None + + try: + config = command_context.config_environment + except Exception: + command_context.log( + logging.WARNING, + "static-analysis", + {}, + "Looks like configure has not run yet, running it now...", + ) + + clobber = Clobberer(command_context.topsrcdir, command_context.topobjdir) + + if clobber.clobber_needed(): + choice = prompt_bool( + "Configuration has changed and Clobber is needed. " + "Do you want to proceed?" + ) + if not choice: + command_context.log( + logging.ERROR, + "static-analysis", + {}, + "ERROR: Without Clobber we cannot continue execution!", + ) + return (1, None, None) + os.environ["AUTOCLOBBER"] = "1" + + rc = build_commands.configure(command_context) + if rc != 0: + return (rc, config, ran_configure) + ran_configure = True + try: + config = command_context.config_environment + except Exception: + pass + + return (0, config, ran_configure) + + +def _build_compile_db(command_context, verbose=False): + compilation_commands_path = mozpath.join( + command_context.topobjdir, "static-analysis" + ) + compile_db = mozpath.join(compilation_commands_path, "compile_commands.json") + + if os.path.exists(compile_db): + return 0, compile_db, compilation_commands_path + + rc, config, ran_configure = _get_config_environment(command_context) + if rc != 0: + return rc, compile_db, compilation_commands_path + + if ran_configure: + # Configure may have created the compilation database if the + # mozconfig enables building the CompileDB backend by default, + # So we recurse to see if the file exists once again. + return _build_compile_db(command_context, verbose=verbose) + + if config: + print( + "Looks like a clang compilation database has not been " + "created yet, creating it now..." + ) + rc = build_commands.build_backend( + command_context, ["StaticAnalysis"], verbose=verbose + ) + if rc != 0: + return rc, compile_db, compilation_commands_path + assert os.path.exists(compile_db) + return 0, compile_db, compilation_commands_path + + +def _build_export(command_context, jobs, verbose=False): + def on_line(line): + command_context.log(logging.INFO, "build_output", {"line": line}, "{line}") + + # First install what we can through install manifests. + rc = command_context._run_make( + directory=command_context.topobjdir, + target="pre-export", + line_handler=None, + silent=not verbose, + ) + if rc != 0: + return rc + + # Then build the rest of the build dependencies by running the full + # export target, because we can't do anything better. + for target in ("export", "pre-compile"): + rc = command_context._run_make( + directory=command_context.topobjdir, + target=target, + line_handler=None, + silent=not verbose, + num_jobs=jobs, + ) + if rc != 0: + return rc + + return 0 + + +def _set_clang_tools_paths(command_context): + rc, config, _ = _get_config_environment(command_context) + + clang_paths = SimpleNamespace() + + if rc != 0: + return rc, clang_paths + + clang_paths._clang_tools_path = mozpath.join( + command_context._mach_context.state_dir, "clang-tools" + ) + clang_paths._clang_tidy_path = mozpath.join( + clang_paths._clang_tools_path, + "clang-tidy", + "bin", + "clang-tidy" + config.substs.get("HOST_BIN_SUFFIX", ""), + ) + clang_paths._clang_format_path = mozpath.join( + clang_paths._clang_tools_path, + "clang-tidy", + "bin", + "clang-format" + config.substs.get("HOST_BIN_SUFFIX", ""), + ) + clang_paths._clang_apply_replacements = mozpath.join( + clang_paths._clang_tools_path, + "clang-tidy", + "bin", + "clang-apply-replacements" + config.substs.get("HOST_BIN_SUFFIX", ""), + ) + clang_paths._run_clang_tidy_path = mozpath.join( + clang_paths._clang_tools_path, + "clang-tidy", + "bin", + "run-clang-tidy", + ) + clang_paths._clang_format_diff = mozpath.join( + clang_paths._clang_tools_path, + "clang-tidy", + "share", + "clang", + "clang-format-diff.py", + ) + return 0, clang_paths + + +def _do_clang_tools_exist(clang_paths): + return ( + os.path.exists(clang_paths._clang_tidy_path) + and os.path.exists(clang_paths._clang_format_path) + and os.path.exists(clang_paths._clang_apply_replacements) + and os.path.exists(clang_paths._run_clang_tidy_path) + ) + + +def get_clang_tools( + command_context, + force=False, + skip_cache=False, + source=None, + download_if_needed=True, + verbose=False, +): + rc, clang_paths = _set_clang_tools_paths(command_context) + + if rc != 0: + return rc, clang_paths + + if ( + _do_clang_tools_exist(clang_paths) + and _is_version_eligible(command_context, clang_paths, log_error=False) + and not force + ): + return 0, clang_paths + + if os.path.isdir(clang_paths._clang_tools_path) and download_if_needed: + # The directory exists, perhaps it's corrupted? Delete it + # and start from scratch. + shutil.rmtree(clang_paths._clang_tools_path) + return get_clang_tools( + command_context, + force=force, + skip_cache=skip_cache, + source=source, + verbose=verbose, + download_if_needed=download_if_needed, + ) + + # Create base directory where we store clang binary + os.mkdir(clang_paths._clang_tools_path) + + if source: + return _get_clang_tools_from_source(command_context, clang_paths, source) + + if not download_if_needed: + return 0, clang_paths + + from mozbuild.bootstrap import bootstrap_toolchain + + bootstrap_toolchain("clang-tools/clang-tidy") + + return 0 if _is_version_eligible(command_context, clang_paths) else 1, clang_paths + + +def _get_clang_tools_from_source(command_context, clang_paths, filename): + from mozbuild.action.tooltool import unpack_file + + clang_tidy_path = mozpath.join( + command_context._mach_context.state_dir, "clang-tools" + ) + + currentWorkingDir = os.getcwd() + os.chdir(clang_tidy_path) + + unpack_file(filename) + + # Change back the cwd + os.chdir(currentWorkingDir) + + clang_path = mozpath.join(clang_tidy_path, "clang") + + if not os.path.isdir(clang_path): + raise Exception("Extracted the archive but didn't find the expected output") + + assert os.path.exists(clang_paths._clang_tidy_path) + assert os.path.exists(clang_paths._clang_format_path) + assert os.path.exists(clang_paths._clang_apply_replacements) + assert os.path.exists(clang_paths._run_clang_tidy_path) + return 0, clang_paths + + +def _get_clang_format_diff_command(command_context, commit): + if command_context.repository.name == "hg": + args = ["hg", "diff", "-U0"] + if commit: + args += ["-c", commit] + else: + args += ["-r", ".^"] + for dot_extension in _format_include_extensions: + args += ["--include", "glob:**{0}".format(dot_extension)] + args += ["--exclude", "listfile:{0}".format(_format_ignore_file)] + else: + commit_range = "HEAD" # All uncommitted changes. + if commit: + commit_range = ( + commit if ".." in commit else "{}~..{}".format(commit, commit) + ) + args = ["git", "diff", "--no-color", "-U0", commit_range, "--"] + for dot_extension in _format_include_extensions: + args += ["*{0}".format(dot_extension)] + # git-diff doesn't support an 'exclude-from-files' param, but + # allow to add individual exclude pattern since v1.9, see + # https://git-scm.com/docs/gitglossary#gitglossary-aiddefpathspecapathspec + with open(_format_ignore_file, "rb") as exclude_pattern_file: + for pattern in exclude_pattern_file.readlines(): + pattern = six.ensure_str(pattern.rstrip()) + pattern = pattern.replace(".*", "**") + if not pattern or pattern.startswith("#"): + continue # empty or comment + magics = ["exclude"] + if pattern.startswith("^"): + magics += ["top"] + pattern = pattern[1:] + args += [":({0}){1}".format(",".join(magics), pattern)] + return args + + +def _run_clang_format_diff( + command_context, clang_format_diff, clang_format, commit, output_file +): + # Run clang-format on the diff + # Note that this will potentially miss a lot things + from subprocess import PIPE, CalledProcessError, Popen, check_output + + diff_process = Popen( + _get_clang_format_diff_command(command_context, commit), stdout=PIPE + ) + args = [sys.executable, clang_format_diff, "-p1", "-binary=%s" % clang_format] + + if not output_file: + args.append("-i") + try: + output = check_output(args, stdin=diff_process.stdout) + if output_file: + # We want to print the diffs + print(output, file=output_file) + + return 0 + except CalledProcessError as e: + # Something wrong happend + print("clang-format: An error occured while running clang-format-diff.") + return e.returncode + + +def _is_ignored_path(command_context, ignored_dir_re, f): + # path needs to be relative to the src root + root_dir = command_context.topsrcdir + os.sep + if f.startswith(root_dir): + f = f[len(root_dir) :] + # the ignored_dir_re regex uses / on all platforms + return re.match(ignored_dir_re, f.replace(os.sep, "/")) + + +def _generate_path_list(command_context, paths, verbose=True): + path_to_third_party = os.path.join(command_context.topsrcdir, _format_ignore_file) + ignored_dir = [] + with open(path_to_third_party, "r") as fh: + for line in fh: + # Remove comments and empty lines + if line.startswith("#") or len(line.strip()) == 0: + continue + # The regexp is to make sure we are managing relative paths + ignored_dir.append(r"^[\./]*" + line.rstrip()) + + # Generates the list of regexp + ignored_dir_re = "(%s)" % "|".join(ignored_dir) + extensions = _format_include_extensions + + path_list = [] + for f in paths: + if _is_ignored_path(command_context, ignored_dir_re, f): + # Early exit if we have provided an ignored directory + if verbose: + print("static-analysis: Ignored third party code '{0}'".format(f)) + continue + + if os.path.isdir(f): + # Processing a directory, generate the file list + for folder, subs, files in os.walk(f): + subs.sort() + for filename in sorted(files): + f_in_dir = posixpath.join(pathlib.Path(folder).as_posix(), filename) + if f_in_dir.endswith(extensions) and not _is_ignored_path( + command_context, ignored_dir_re, f_in_dir + ): + # Supported extension and accepted path + path_list.append(f_in_dir) + else: + # Make sure that the file exists and it has a supported extension + if os.path.isfile(f) and f.endswith(extensions): + path_list.append(f) + + return path_list + + +def _run_clang_format_in_console(command_context, clang_format, paths, assume_filename): + path_list = _generate_path_list(command_context, assume_filename, False) + + if path_list == []: + return 0 + + # We use -assume-filename in order to better determine the path for + # the .clang-format when it is ran outside of the repo, for example + # by the extension hg-formatsource + args = [clang_format, "-assume-filename={}".format(assume_filename[0])] + + process = subprocess.Popen(args, stdin=subprocess.PIPE) + with open(paths[0], "r") as fin: + process.stdin.write(fin.read()) + process.stdin.close() + process.wait() + return process.returncode + + +def _get_clang_format_cfg(command_context, current_dir): + clang_format_cfg_path = mozpath.join(current_dir, ".clang-format") + + if os.path.exists(clang_format_cfg_path): + # Return found path for .clang-format + return clang_format_cfg_path + + if current_dir != command_context.topsrcdir: + # Go to parent directory + return _get_clang_format_cfg(command_context, os.path.split(current_dir)[0]) + # We have reached command_context.topsrcdir so return None + return None + + +def _copy_clang_format_for_show_diff( + command_context, current_dir, cached_clang_format_cfg, tmpdir +): + # Lookup for .clang-format first in cache + clang_format_cfg = cached_clang_format_cfg.get(current_dir, None) + + if clang_format_cfg is None: + # Go through top directories + clang_format_cfg = _get_clang_format_cfg(command_context, current_dir) + + # This is unlikely to happen since we must have .clang-format from + # command_context.topsrcdir but in any case we should handle a potential error + if clang_format_cfg is None: + print("Cannot find corresponding .clang-format.") + return 1 + + # Cache clang_format_cfg for potential later usage + cached_clang_format_cfg[current_dir] = clang_format_cfg + + # Copy .clang-format to the tmp dir where the formatted file is copied + shutil.copy(clang_format_cfg, tmpdir) + return 0 + + +def _run_clang_format_path( + command_context, clang_format, paths, output_file, output_format +): + # Run clang-format on files or directories directly + from subprocess import CalledProcessError, check_output + + if output_format == "json": + # Get replacements in xml, then process to json + args = [clang_format, "-output-replacements-xml"] + else: + args = [clang_format, "-i"] + + if output_file: + # We just want to show the diff, we create the directory to copy it + tmpdir = os.path.join(command_context.topobjdir, "tmp") + if not os.path.exists(tmpdir): + os.makedirs(tmpdir) + + path_list = _generate_path_list(command_context, paths) + + if path_list == []: + return + + print("Processing %d file(s)..." % len(path_list)) + + if output_file: + patches = {} + cached_clang_format_cfg = {} + for i in range(0, len(path_list)): + l = path_list[i : (i + 1)] + + # Copy the files into a temp directory + # and run clang-format on the temp directory + # and show the diff + original_path = l[0] + local_path = ntpath.basename(original_path) + current_dir = ntpath.dirname(original_path) + target_file = os.path.join(tmpdir, local_path) + faketmpdir = os.path.dirname(target_file) + if not os.path.isdir(faketmpdir): + os.makedirs(faketmpdir) + shutil.copy(l[0], faketmpdir) + l[0] = target_file + + ret = _copy_clang_format_for_show_diff( + command_context, current_dir, cached_clang_format_cfg, faketmpdir + ) + if ret != 0: + return ret + + # Run clang-format on the list + try: + output = check_output(args + l) + if output and output_format == "json": + # Output a relative path in json patch list + relative_path = os.path.relpath( + original_path, command_context.topsrcdir + ) + patches[relative_path] = _parse_xml_output(original_path, output) + except CalledProcessError as e: + # Something wrong happend + print("clang-format: An error occured while running clang-format.") + return e.returncode + + # show the diff + if output_format == "diff": + diff_command = ["diff", "-u", original_path, target_file] + try: + output = check_output(diff_command) + except CalledProcessError as e: + # diff -u returns 0 when no change + # here, we expect changes. if we are here, this means that + # there is a diff to show + if e.output: + # Replace the temp path by the path relative to the repository to + # display a valid patch + relative_path = os.path.relpath( + original_path, command_context.topsrcdir + ) + # We must modify the paths in order to be compatible with the + # `diff` format. + original_path_diff = os.path.join("a", relative_path) + target_path_diff = os.path.join("b", relative_path) + e.output = e.output.decode("utf-8") + patch = e.output.replace( + "+++ {}".format(target_file), + "+++ {}".format(target_path_diff), + ).replace( + "-- {}".format(original_path), + "-- {}".format(original_path_diff), + ) + patches[original_path] = patch + + if output_format == "json": + output = json.dumps(patches, indent=4) + else: + # Display all the patches at once + output = "\n".join(patches.values()) + + # Output to specified file or stdout + print(output, file=output_file) + + shutil.rmtree(tmpdir) + return 0 + + # Run clang-format in parallel trying to saturate all of the available cores. + import math + + max_workers = multiprocessing.cpu_count() + + # To maximize CPU usage when there are few items to handle, + # underestimate the number of items per batch, then dispatch + # outstanding items across workers. Per definition, each worker will + # handle at most one outstanding item. + batch_size = int(math.floor(float(len(path_list)) / max_workers)) + outstanding_items = len(path_list) - batch_size * max_workers + + batches = [] + + i = 0 + while i < len(path_list): + num_items = batch_size + (1 if outstanding_items > 0 else 0) + batches.append(args + path_list[i : (i + num_items)]) + + outstanding_items -= 1 + i += num_items + + error_code = None + + with concurrent.futures.ThreadPoolExecutor(max_workers=max_workers) as executor: + futures = [] + for batch in batches: + futures.append(executor.submit(run_one_clang_format_batch, batch)) + + for future in concurrent.futures.as_completed(futures): + # Wait for every task to finish + ret_val = future.result() + if ret_val is not None: + error_code = ret_val + + if error_code is not None: + return error_code + return 0 + + +def _parse_xml_output(path, clang_output): + """ + Parse the clang-format XML output to convert it in a JSON compatible + list of patches, and calculates line level informations from the + character level provided changes. + """ + content = six.ensure_str(open(path, "r").read()) + + def _nb_of_lines(start, end): + return len(content[start:end].splitlines()) + + def _build(replacement): + offset = int(replacement.attrib["offset"]) + length = int(replacement.attrib["length"]) + last_line = content.rfind("\n", 0, offset) + return { + "replacement": replacement.text, + "char_offset": offset, + "char_length": length, + "line": _nb_of_lines(0, offset), + "line_offset": last_line != -1 and (offset - last_line) or 0, + "lines_modified": _nb_of_lines(offset, offset + length), + } + + return [ + _build(replacement) + for replacement in ET.fromstring(clang_output).findall("replacement") + ] diff --git a/python/mozbuild/mozbuild/code_analysis/moz.build b/python/mozbuild/mozbuild/code_analysis/moz.build new file mode 100644 index 0000000000..bb49fbcd2f --- /dev/null +++ b/python/mozbuild/mozbuild/code_analysis/moz.build @@ -0,0 +1,8 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# 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/. + +with Files("**"): + BUG_COMPONENT = ("Firefox Build System", "Source Code Analysis") diff --git a/python/mozbuild/mozbuild/code_analysis/utils.py b/python/mozbuild/mozbuild/code_analysis/utils.py new file mode 100644 index 0000000000..e3931aa7e4 --- /dev/null +++ b/python/mozbuild/mozbuild/code_analysis/utils.py @@ -0,0 +1,138 @@ +# 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/. + +import logging + +import mozpack.path as mozpath +import yaml + +from mozbuild.util import memoized_property + + +class ClangTidyConfig(object): + def __init__(self, mozilla_src): + self._clang_tidy_config = self._get_clang_tidy_config(mozilla_src) + + def _get_clang_tidy_config(self, mozilla_src): + try: + file_handler = open( + mozpath.join(mozilla_src, "tools", "clang-tidy", "config.yaml") + ) + config = yaml.safe_load(file_handler) + except Exception: + self.log( + logging.ERROR, + "clang-tidy-config", + {}, + "Looks like config.yaml is not valid, we are going to use default" + " values for the rest of the analysis for clang-tidy.", + ) + return None + return config + + @memoized_property + def checks(self): + """ + Returns a list with all activated checks + """ + + checks = ["-*"] + try: + config = self._clang_tidy_config + for item in config["clang_checkers"]: + if item.get("publish", True): + checks.append(item["name"]) + except Exception: + self.log( + logging.ERROR, + "clang-tidy-config", + {}, + "Looks like config.yaml is not valid, so we are unable to " + "determine default checkers, using '-checks=-*,mozilla-*'", + ) + checks.append("mozilla-*") + finally: + return checks + + @memoized_property + def checks_with_data(self): + """ + Returns a list with all activated checks plus metadata for each check + """ + + checks_with_data = [{"name": "-*"}] + try: + config = self._clang_tidy_config + for item in config["clang_checkers"]: + if item.get("publish", True): + checks_with_data.append(item) + except Exception: + self.log( + logging.ERROR, + "clang-tidy-config", + {}, + "Looks like config.yaml is not valid, so we are unable to " + "determine default checkers, using '-checks=-*,mozilla-*'", + ) + checks_with_data.append({"name": "mozilla-*", "reliability": "high"}) + finally: + return checks_with_data + + @memoized_property + def checks_config(self): + """ + Returns the configuation for all checks + """ + + config_list = [] + checks_config = {} + try: + config = self._clang_tidy_config + for checker in config["clang_checkers"]: + if checker.get("publish", True) and "config" in checker: + for checker_option in checker["config"]: + # Verify if the format of the Option is correct, + # possibilities are: + # 1. CheckerName.Option + # 2. Option -> that will become CheckerName.Option + if not checker_option["key"].startswith(checker["name"]): + checker_option["key"] = "{}.{}".format( + checker["name"], checker_option["key"] + ) + config_list += checker["config"] + checks_config["CheckOptions"] = config_list + except Exception: + self.log( + logging.ERROR, + "clang-tidy-config", + {}, + "Looks like config.yaml is not valid, so we are unable to " + "determine configuration for checkers, so using default", + ) + checks_config = None + finally: + return checks_config + + @memoized_property + def version(self): + """ + Returns version of clang-tidy suitable for this configuration file + """ + + if "package_version" in self._clang_tidy_config: + return self._clang_tidy_config["package_version"] + self.log( + logging.ERROR, + "clang-tidy-confis", + {}, + "Unable to find 'package_version' in the config.yml", + ) + return None + + @memoized_property + def platforms(self): + """ + Returns a list of platforms suitable to work with `clang-tidy` + """ + return self._clang_tidy_config.get("platforms", []) diff --git a/python/mozbuild/mozbuild/codecoverage/__init__.py b/python/mozbuild/mozbuild/codecoverage/__init__.py new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/python/mozbuild/mozbuild/codecoverage/__init__.py diff --git a/python/mozbuild/mozbuild/codecoverage/chrome_map.py b/python/mozbuild/mozbuild/codecoverage/chrome_map.py new file mode 100644 index 0000000000..79cedd2faf --- /dev/null +++ b/python/mozbuild/mozbuild/codecoverage/chrome_map.py @@ -0,0 +1,175 @@ +# 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/. + +import json +import os +import re + +import mozpack.path as mozpath +import six +from mach.config import ConfigSettings +from mach.logging import LoggingManager +from mozpack.copier import FileRegistry +from mozpack.files import PreprocessedFile +from mozpack.manifests import InstallManifest + +from mozbuild.backend.common import CommonBackend +from mozbuild.base import MozbuildObject +from mozbuild.frontend.data import ( + ChromeManifestEntry, + FinalTargetFiles, + FinalTargetPreprocessedFiles, + JARManifest, +) + +from .manifest_handler import ChromeManifestHandler + +_line_comment_re = re.compile('^//@line (\d+) "(.+)"$') + + +def generate_pp_info(path, topsrcdir): + with open(path, encoding="utf-8") as fh: + # (start, end) -> (included_source, start) + section_info = dict() + + this_section = None + + def finish_section(pp_end): + pp_start, inc_source, inc_start = this_section + section_info[str(pp_start) + "," + str(pp_end)] = inc_source, inc_start + + for count, line in enumerate(fh): + # Regex are quite slow, so bail out early. + if not line.startswith("//@line"): + continue + m = re.match(_line_comment_re, line) + if m: + if this_section: + finish_section(count + 1) + inc_start, inc_source = m.groups() + + # Special case to handle $SRCDIR prefixes + src_dir_prefix = "$SRCDIR" + parts = mozpath.split(inc_source) + if parts[0] == src_dir_prefix: + inc_source = mozpath.join(*parts[1:]) + else: + inc_source = mozpath.relpath(inc_source, topsrcdir) + + pp_start = count + 2 + this_section = pp_start, inc_source, int(inc_start) + + if this_section: + finish_section(count + 2) + + return section_info + + +# This build backend is assuming the build to have happened already, as it is parsing +# built preprocessed files to generate data to map them to the original sources. + + +class ChromeMapBackend(CommonBackend): + def _init(self): + CommonBackend._init(self) + + log_manager = LoggingManager() + self._cmd = MozbuildObject( + self.environment.topsrcdir, + ConfigSettings(), + log_manager, + self.environment.topobjdir, + ) + self._install_mapping = {} + self.manifest_handler = ChromeManifestHandler() + + def consume_object(self, obj): + if isinstance(obj, JARManifest): + self._consume_jar_manifest(obj) + if isinstance(obj, ChromeManifestEntry): + self.manifest_handler.handle_manifest_entry(obj.entry) + if isinstance(obj, (FinalTargetFiles, FinalTargetPreprocessedFiles)): + self._handle_final_target_files(obj) + return True + + def _handle_final_target_files(self, obj): + for path, files in obj.files.walk(): + for f in files: + dest = mozpath.join(obj.install_target, path, f.target_basename) + obj_path = mozpath.join(self.environment.topobjdir, dest) + if obj_path.endswith(".in"): + obj_path = obj_path[:-3] + if isinstance(obj, FinalTargetPreprocessedFiles): + assert os.path.exists(obj_path), "%s should exist" % obj_path + pp_info = generate_pp_info(obj_path, obj.topsrcdir) + else: + pp_info = None + + base = ( + obj.topobjdir + if f.full_path.startswith(obj.topobjdir) + else obj.topsrcdir + ) + self._install_mapping[dest] = ( + mozpath.relpath(f.full_path, base), + pp_info, + ) + + def consume_finished(self): + mp = os.path.join( + self.environment.topobjdir, "_build_manifests", "install", "_tests" + ) + install_manifest = InstallManifest(mp) + reg = FileRegistry() + install_manifest.populate_registry(reg) + + for dest, src in reg: + if not hasattr(src, "path"): + continue + + if not os.path.isabs(dest): + dest = "_tests/" + dest + + obj_path = mozpath.join(self.environment.topobjdir, dest) + if isinstance(src, PreprocessedFile): + assert os.path.exists(obj_path), "%s should exist" % obj_path + pp_info = generate_pp_info(obj_path, self.environment.topsrcdir) + else: + pp_info = None + + rel_src = mozpath.relpath(src.path, self.environment.topsrcdir) + self._install_mapping[dest] = rel_src, pp_info + + # Our result has four parts: + # A map from url prefixes to objdir directories: + # { "chrome://mozapps/content/": [ "dist/bin/chrome/toolkit/content/mozapps" ], ... } + # A map of overrides. + # A map from objdir paths to sourcedir paths, and an object storing mapping + # information for preprocessed files: + # { "dist/bin/browser/chrome/browser/content/browser/aboutSessionRestore.js": + # [ "$topsrcdir/browser/components/sessionstore/content/aboutSessionRestore.js", {} ], + # ... } + # An object containing build configuration information. + outputfile = os.path.join(self.environment.topobjdir, "chrome-map.json") + with self._write_file(outputfile) as fh: + chrome_mapping = self.manifest_handler.chrome_mapping + overrides = self.manifest_handler.overrides + json.dump( + [ + {k: list(v) for k, v in six.iteritems(chrome_mapping)}, + overrides, + self._install_mapping, + { + "topobjdir": mozpath.normpath(self.environment.topobjdir), + "MOZ_APP_NAME": self.environment.substs.get("MOZ_APP_NAME"), + "OMNIJAR_NAME": self.environment.substs.get("OMNIJAR_NAME"), + "MOZ_MACBUNDLE_NAME": self.environment.substs.get( + "MOZ_MACBUNDLE_NAME" + ), + }, + ], + fh, + sort_keys=True, + indent=2, + ) diff --git a/python/mozbuild/mozbuild/codecoverage/lcov_rewriter.py b/python/mozbuild/mozbuild/codecoverage/lcov_rewriter.py new file mode 100644 index 0000000000..6c137a979b --- /dev/null +++ b/python/mozbuild/mozbuild/codecoverage/lcov_rewriter.py @@ -0,0 +1,776 @@ +# 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/. + +import json +import os +import sys +from argparse import ArgumentParser + +try: + import urlparse +except ImportError: + import urllib.parse as urlparse + +import mozpack.path as mozpath +from mozpack.chrome.manifest import parse_manifest +from six import viewitems + +from .manifest_handler import ChromeManifestHandler + + +class LcovRecord(object): + __slots__ = ( + "test_name", + "source_file", + "functions", + "function_exec_counts", + "function_count", + "covered_function_count", + "branches", + "branch_count", + "covered_branch_count", + "lines", + "line_count", + "covered_line_count", + ) + + def __init__(self): + self.functions = {} + self.function_exec_counts = {} + self.branches = {} + self.lines = {} + + def __iadd__(self, other): + # These shouldn't differ. + self.source_file = other.source_file + if hasattr(other, "test_name"): + self.test_name = other.test_name + self.functions.update(other.functions) + + for name, count in viewitems(other.function_exec_counts): + self.function_exec_counts[name] = count + self.function_exec_counts.get( + name, 0 + ) + + for key, taken in viewitems(other.branches): + self.branches[key] = taken + self.branches.get(key, 0) + + for line, (exec_count, checksum) in viewitems(other.lines): + new_exec_count = exec_count + if line in self.lines: + old_exec_count, _ = self.lines[line] + new_exec_count += old_exec_count + self.lines[line] = new_exec_count, checksum + + self.resummarize() + return self + + def resummarize(self): + # Re-calculate summaries after generating or splitting a record. + self.function_count = len(self.functions.keys()) + # Function records may have moved between files, so filter here. + self.function_exec_counts = { + fn_name: count + for fn_name, count in viewitems(self.function_exec_counts) + if fn_name in self.functions.values() + } + self.covered_function_count = len( + [c for c in self.function_exec_counts.values() if c] + ) + self.line_count = len(self.lines) + self.covered_line_count = len([c for c, _ in self.lines.values() if c]) + self.branch_count = len(self.branches) + self.covered_branch_count = len([c for c in self.branches.values() if c]) + + +class RecordRewriter(object): + # Helper class for rewriting/spliting individual lcov records according + # to what the preprocessor did. + def __init__(self): + self._ranges = None + + def _get_range(self, line): + for start, end in self._ranges: + if line < start: + return None + if line < end: + return start, end + return None + + def _get_mapped_line(self, line, r): + inc_source, inc_start = self._current_pp_info[r] + start, end = r + offs = line - start + return inc_start + offs + + def _get_record(self, inc_source): + if inc_source in self._additions: + gen_rec = self._additions[inc_source] + else: + gen_rec = LcovRecord() + gen_rec.source_file = inc_source + self._additions[inc_source] = gen_rec + return gen_rec + + def _rewrite_lines(self, record): + rewritten_lines = {} + for ln, line_info in viewitems(record.lines): + r = self._get_range(ln) + if r is None: + rewritten_lines[ln] = line_info + continue + new_ln = self._get_mapped_line(ln, r) + inc_source, _ = self._current_pp_info[r] + + if inc_source != record.source_file: + gen_rec = self._get_record(inc_source) + gen_rec.lines[new_ln] = line_info + continue + + # Move exec_count to the new lineno. + rewritten_lines[new_ln] = line_info + + record.lines = rewritten_lines + + def _rewrite_functions(self, record): + rewritten_fns = {} + + # Sometimes we get multiple entries for a named function ("top-level", for + # instance). It's not clear the records that result are well-formed, but + # we act as though if a function has multiple FN's, the corresponding + # FNDA's are all the same. + for ln, fn_name in viewitems(record.functions): + r = self._get_range(ln) + if r is None: + rewritten_fns[ln] = fn_name + continue + new_ln = self._get_mapped_line(ln, r) + inc_source, _ = self._current_pp_info[r] + if inc_source != record.source_file: + gen_rec = self._get_record(inc_source) + gen_rec.functions[new_ln] = fn_name + if fn_name in record.function_exec_counts: + gen_rec.function_exec_counts[fn_name] = record.function_exec_counts[ + fn_name + ] + continue + rewritten_fns[new_ln] = fn_name + record.functions = rewritten_fns + + def _rewrite_branches(self, record): + rewritten_branches = {} + for (ln, block_number, branch_number), taken in viewitems(record.branches): + r = self._get_range(ln) + if r is None: + rewritten_branches[ln, block_number, branch_number] = taken + continue + new_ln = self._get_mapped_line(ln, r) + inc_source, _ = self._current_pp_info[r] + if inc_source != record.source_file: + gen_rec = self._get_record(inc_source) + gen_rec.branches[(new_ln, block_number, branch_number)] = taken + continue + rewritten_branches[(new_ln, block_number, branch_number)] = taken + + record.branches = rewritten_branches + + def rewrite_record(self, record, pp_info): + # Rewrite the lines in the given record according to preprocessor info + # and split to additional records when pp_info has included file info. + self._current_pp_info = dict( + [(tuple([int(l) for l in k.split(",")]), v) for k, v in pp_info.items()] + ) + self._ranges = sorted(self._current_pp_info.keys()) + self._additions = {} + self._rewrite_lines(record) + self._rewrite_functions(record) + self._rewrite_branches(record) + + record.resummarize() + + generated_records = self._additions.values() + for r in generated_records: + r.resummarize() + return generated_records + + +class LcovFile(object): + # Simple parser/pretty-printer for lcov format. + # lcov parsing based on http://ltp.sourceforge.net/coverage/lcov/geninfo.1.php + + # TN:<test name> + # SF:<absolute path to the source file> + # FN:<line number of function start>,<function name> + # FNDA:<execution count>,<function name> + # FNF:<number of functions found> + # FNH:<number of function hit> + # BRDA:<line number>,<block number>,<branch number>,<taken> + # BRF:<number of branches found> + # BRH:<number of branches hit> + # DA:<line number>,<execution count>[,<checksum>] + # LF:<number of instrumented lines> + # LH:<number of lines with a non-zero execution count> + # end_of_record + PREFIX_TYPES = { + "TN": 0, + "SF": 0, + "FN": 1, + "FNDA": 1, + "FNF": 0, + "FNH": 0, + "BRDA": 3, + "BRF": 0, + "BRH": 0, + "DA": 2, + "LH": 0, + "LF": 0, + } + + def __init__(self, lcov_paths): + self.lcov_paths = lcov_paths + + def iterate_records(self, rewrite_source=None): + current_source_file = None + current_pp_info = None + current_lines = [] + for lcov_path in self.lcov_paths: + with open(lcov_path, "r", encoding="utf-8") as lcov_fh: + for line in lcov_fh: + line = line.rstrip() + if not line: + continue + + if line == "end_of_record": + # We skip records that we couldn't rewrite, that is records for which + # rewrite_url returns None. + if current_source_file is not None: + yield (current_source_file, current_pp_info, current_lines) + current_source_file = None + current_pp_info = None + current_lines = [] + continue + + colon = line.find(":") + prefix = line[:colon] + + if prefix == "SF": + sf = line[(colon + 1) :] + res = ( + rewrite_source(sf) + if rewrite_source is not None + else (sf, None) + ) + if res is None: + current_lines.append(line) + else: + current_source_file, current_pp_info = res + current_lines.append("SF:" + current_source_file) + else: + current_lines.append(line) + + def parse_record(self, record_content): + self.current_record = LcovRecord() + + for line in record_content: + colon = line.find(":") + + prefix = line[:colon] + + # We occasionally end up with multi-line scripts in data: + # uris that will trip up the parser, just skip them for now. + if colon < 0 or prefix not in self.PREFIX_TYPES: + continue + + args = line[(colon + 1) :].split(",", self.PREFIX_TYPES[prefix]) + + def try_convert(a): + try: + return int(a) + except ValueError: + return a + + args = [try_convert(a) for a in args] + + try: + LcovFile.__dict__["parse_" + prefix](self, *args) + except ValueError: + print("Encountered an error in %s:\n%s" % (self.lcov_fh.name, line)) + raise + except KeyError: + print("Invalid lcov line start in %s:\n%s" % (self.lcov_fh.name, line)) + raise + except TypeError: + print("Invalid lcov line start in %s:\n%s" % (self.lcov_fh.name, line)) + raise + + ret = self.current_record + self.current_record = LcovRecord() + return ret + + def print_file(self, fh, rewrite_source, rewrite_record): + for source_file, pp_info, record_content in self.iterate_records( + rewrite_source + ): + if pp_info is not None: + record = self.parse_record(record_content) + for r in rewrite_record(record, pp_info): + fh.write(self.format_record(r)) + fh.write(self.format_record(record)) + else: + fh.write("\n".join(record_content) + "\nend_of_record\n") + + def format_record(self, record): + out_lines = [] + for name in LcovRecord.__slots__: + if hasattr(record, name): + out_lines.append(LcovFile.__dict__["format_" + name](self, record)) + return "\n".join(out_lines) + "\nend_of_record\n" + + def format_test_name(self, record): + return "TN:%s" % record.test_name + + def format_source_file(self, record): + return "SF:%s" % record.source_file + + def format_functions(self, record): + # Sorting results gives deterministic output (and is a lot faster than + # using OrderedDict). + fns = [] + for start_lineno, fn_name in sorted(viewitems(record.functions)): + fns.append("FN:%s,%s" % (start_lineno, fn_name)) + return "\n".join(fns) + + def format_function_exec_counts(self, record): + fndas = [] + for name, exec_count in sorted(viewitems(record.function_exec_counts)): + fndas.append("FNDA:%s,%s" % (exec_count, name)) + return "\n".join(fndas) + + def format_function_count(self, record): + return "FNF:%s" % record.function_count + + def format_covered_function_count(self, record): + return "FNH:%s" % record.covered_function_count + + def format_branches(self, record): + brdas = [] + for key in sorted(record.branches): + taken = record.branches[key] + taken = "-" if taken == 0 else taken + brdas.append("BRDA:%s" % ",".join(map(str, list(key) + [taken]))) + return "\n".join(brdas) + + def format_branch_count(self, record): + return "BRF:%s" % record.branch_count + + def format_covered_branch_count(self, record): + return "BRH:%s" % record.covered_branch_count + + def format_lines(self, record): + das = [] + for line_no, (exec_count, checksum) in sorted(viewitems(record.lines)): + s = "DA:%s,%s" % (line_no, exec_count) + if checksum: + s += ",%s" % checksum + das.append(s) + return "\n".join(das) + + def format_line_count(self, record): + return "LF:%s" % record.line_count + + def format_covered_line_count(self, record): + return "LH:%s" % record.covered_line_count + + def parse_TN(self, test_name): + self.current_record.test_name = test_name + + def parse_SF(self, source_file): + self.current_record.source_file = source_file + + def parse_FN(self, start_lineno, fn_name): + self.current_record.functions[start_lineno] = fn_name + + def parse_FNDA(self, exec_count, fn_name): + self.current_record.function_exec_counts[fn_name] = exec_count + + def parse_FNF(self, function_count): + self.current_record.function_count = function_count + + def parse_FNH(self, covered_function_count): + self.current_record.covered_function_count = covered_function_count + + def parse_BRDA(self, line_number, block_number, branch_number, taken): + taken = 0 if taken == "-" else taken + self.current_record.branches[(line_number, block_number, branch_number)] = taken + + def parse_BRF(self, branch_count): + self.current_record.branch_count = branch_count + + def parse_BRH(self, covered_branch_count): + self.current_record.covered_branch_count = covered_branch_count + + def parse_DA(self, line_number, execution_count, checksum=None): + self.current_record.lines[line_number] = (execution_count, checksum) + + def parse_LH(self, covered_line_count): + self.current_record.covered_line_count = covered_line_count + + def parse_LF(self, line_count): + self.current_record.line_count = line_count + + +class UrlFinderError(Exception): + pass + + +class UrlFinder(object): + # Given a "chrome://" or "resource://" url, uses data from the UrlMapBackend + # and install manifests to find a path to the source file and the corresponding + # (potentially pre-processed) file in the objdir. + def __init__(self, chrome_map_path, appdir, gredir, extra_chrome_manifests): + # Cached entries + self._final_mapping = {} + + try: + with open(chrome_map_path, "r", encoding="utf-8") as fh: + url_prefixes, overrides, install_info, buildconfig = json.load(fh) + except IOError: + print( + "Error reading %s. Run |./mach build-backend -b ChromeMap| to " + "populate the ChromeMap backend." % chrome_map_path + ) + raise + + self.topobjdir = buildconfig["topobjdir"] + self.MOZ_APP_NAME = buildconfig["MOZ_APP_NAME"] + self.OMNIJAR_NAME = buildconfig["OMNIJAR_NAME"] + + # These are added dynamically in nsIResProtocolHandler, we might + # need to get them at run time. + if "resource:///" not in url_prefixes: + url_prefixes["resource:///"] = [appdir] + if "resource://gre/" not in url_prefixes: + url_prefixes["resource://gre/"] = [gredir] + + self._url_prefixes = url_prefixes + self._url_overrides = overrides + + self._respath = None + + mac_bundle_name = buildconfig["MOZ_MACBUNDLE_NAME"] + if mac_bundle_name: + self._respath = mozpath.join( + "dist", mac_bundle_name, "Contents", "Resources" + ) + + if not extra_chrome_manifests: + extra_path = os.path.join(self.topobjdir, "_tests", "extra.manifest") + if os.path.isfile(extra_path): + extra_chrome_manifests = [extra_path] + + if extra_chrome_manifests: + self._populate_chrome(extra_chrome_manifests) + + self._install_mapping = install_info + + def _populate_chrome(self, manifests): + handler = ChromeManifestHandler() + for m in manifests: + path = os.path.abspath(m) + for e in parse_manifest(None, path): + handler.handle_manifest_entry(e) + self._url_overrides.update(handler.overrides) + self._url_prefixes.update(handler.chrome_mapping) + + def _find_install_prefix(self, objdir_path): + def _prefix(s): + for p in mozpath.split(s): + if "*" not in p: + yield p + "/" + + offset = 0 + for leaf in reversed(mozpath.split(objdir_path)): + offset += len(leaf) + if objdir_path[:-offset] in self._install_mapping: + pattern_prefix, is_pp = self._install_mapping[objdir_path[:-offset]] + full_leaf = objdir_path[len(objdir_path) - offset :] + src_prefix = "".join(_prefix(pattern_prefix)) + self._install_mapping[objdir_path] = ( + mozpath.join(src_prefix, full_leaf), + is_pp, + ) + break + offset += 1 + + def _install_info(self, objdir_path): + if objdir_path not in self._install_mapping: + # If our path is missing, some prefix of it may be in the install + # mapping mapped to a wildcard. + self._find_install_prefix(objdir_path) + if objdir_path not in self._install_mapping: + raise UrlFinderError("Couldn't find entry in manifest for %s" % objdir_path) + return self._install_mapping[objdir_path] + + def _abs_objdir_install_info(self, term): + obj_relpath = term[len(self.topobjdir) + 1 :] + res = self._install_info(obj_relpath) + + # Some urls on osx will refer to paths in the mac bundle, so we + # re-interpret them as being their original location in dist/bin. + if not res and self._respath and obj_relpath.startswith(self._respath): + obj_relpath = obj_relpath.replace(self._respath, "dist/bin") + res = self._install_info(obj_relpath) + + if not res: + raise UrlFinderError("Couldn't find entry in manifest for %s" % obj_relpath) + return res + + def find_files(self, url): + # Returns a tuple of (source file, pp_info) + # for the given "resource:", "chrome:", or "file:" uri. + term = url + if term in self._url_overrides: + term = self._url_overrides[term] + + if os.path.isabs(term) and term.startswith(self.topobjdir): + source_path, pp_info = self._abs_objdir_install_info(term) + return source_path, pp_info + + for prefix, dests in viewitems(self._url_prefixes): + if term.startswith(prefix): + for dest in dests: + if not dest.endswith("/"): + dest += "/" + objdir_path = term.replace(prefix, dest) + + while objdir_path.startswith("//"): + # The mochitest harness produces some wonky file:// uris + # that need to be fixed. + objdir_path = objdir_path[1:] + + try: + if os.path.isabs(objdir_path) and objdir_path.startswith( + self.topobjdir + ): + return self._abs_objdir_install_info(objdir_path) + else: + src_path, pp_info = self._install_info(objdir_path) + return mozpath.normpath(src_path), pp_info + except UrlFinderError: + pass + + if dest.startswith("resource://") or dest.startswith("chrome://"): + result = self.find_files(term.replace(prefix, dest)) + if result: + return result + + raise UrlFinderError("No objdir path for %s" % term) + + def rewrite_url(self, url): + # This applies one-off rules and returns None for urls that we aren't + # going to be able to resolve to a source file ("about:" urls, for + # instance). + if url in self._final_mapping: + return self._final_mapping[url] + if url.endswith("> eval"): + return None + if url.endswith("> Function"): + return None + if " -> " in url: + url = url.split(" -> ")[1].rstrip() + if "?" in url: + url = url.split("?")[0] + + url_obj = urlparse.urlparse(url) + if url_obj.scheme == "jar": + app_name = self.MOZ_APP_NAME + omnijar_name = self.OMNIJAR_NAME + + if app_name in url: + if omnijar_name in url: + # e.g. file:///home/worker/workspace/build/application/firefox/omni.ja!/components/MainProcessSingleton.js # noqa + parts = url_obj.path.split(omnijar_name + "!", 1) + elif ".xpi!" in url: + # e.g. file:///home/worker/workspace/build/application/firefox/browser/features/e10srollout@mozilla.org.xpi!/bootstrap.js # noqa + parts = url_obj.path.split(".xpi!", 1) + else: + # We don't know how to handle this jar: path, so return it to the + # caller to make it print a warning. + return url_obj.path, None + + dir_parts = parts[0].rsplit(app_name + "/", 1) + url = mozpath.normpath( + mozpath.join( + self.topobjdir, + "dist", + "bin", + dir_parts[1].lstrip("/"), + parts[1].lstrip("/"), + ) + ) + elif ".xpi!" in url: + # This matching mechanism is quite brittle and based on examples seen in the wild. + # There's no rule to match the XPI name to the path in dist/xpi-stage. + parts = url_obj.path.split(".xpi!", 1) + addon_name = os.path.basename(parts[0]) + if "-test@mozilla.org" in addon_name: + addon_name = addon_name[: -len("-test@mozilla.org")] + elif addon_name.endswith("@mozilla.org"): + addon_name = addon_name[: -len("@mozilla.org")] + url = mozpath.normpath( + mozpath.join( + self.topobjdir, + "dist", + "xpi-stage", + addon_name, + parts[1].lstrip("/"), + ) + ) + elif url_obj.scheme == "file" and os.path.isabs(url_obj.path): + path = url_obj.path + if not os.path.isfile(path): + # This may have been in a profile directory that no + # longer exists. + return None + if not path.startswith(self.topobjdir): + return path, None + url = url_obj.path + elif url_obj.scheme in ("http", "https", "javascript", "data", "about"): + return None + + result = self.find_files(url) + self._final_mapping[url] = result + return result + + +class LcovFileRewriter(object): + # Class for partial parses of LCOV format and rewriting to resolve urls + # and preprocessed file lines. + def __init__( + self, + chrome_map_path, + appdir="dist/bin/browser/", + gredir="dist/bin/", + extra_chrome_manifests=[], + ): + self.url_finder = UrlFinder( + chrome_map_path, appdir, gredir, extra_chrome_manifests + ) + self.pp_rewriter = RecordRewriter() + + def rewrite_files(self, in_paths, output_file, output_suffix): + unknowns = set() + found_valid = [False] + + def rewrite_source(url): + try: + res = self.url_finder.rewrite_url(url) + if res is None: + return None + except Exception as e: + if url not in unknowns: + # The exception can contain random filename used by + # test cases, and there can be character that cannot be + # encoded with the stdout encoding. + sys.stdout.buffer.write( + ( + "Error: %s.\nCouldn't find source info for %s, removing record\n" + % (e, url) + ).encode(sys.stdout.encoding, errors="replace") + ) + unknowns.add(url) + return None + + source_file, pp_info = res + # We can't assert that the file exists here, because we don't have the source + # checkout available on test machines. We can bring back this assertion when + # bug 1432287 is fixed. + # assert os.path.isfile(source_file), "Couldn't find mapped source file %s at %s!" % ( + # url, source_file) + + found_valid[0] = True + + return res + + in_paths = [os.path.abspath(in_path) for in_path in in_paths] + + if output_file: + lcov_file = LcovFile(in_paths) + with open(output_file, "w+", encoding="utf-8") as out_fh: + lcov_file.print_file( + out_fh, rewrite_source, self.pp_rewriter.rewrite_record + ) + else: + for in_path in in_paths: + lcov_file = LcovFile([in_path]) + with open(in_path + output_suffix, "w+", encoding="utf-8") as out_fh: + lcov_file.print_file( + out_fh, rewrite_source, self.pp_rewriter.rewrite_record + ) + + if not found_valid[0]: + print("WARNING: No valid records found in %s" % in_paths) + return + + +def main(): + parser = ArgumentParser( + description="Given a set of gcov .info files produced " + "by spidermonkey's code coverage, re-maps file urls " + "back to source files and lines in preprocessed files " + "back to their original locations." + ) + parser.add_argument( + "--chrome-map-path", + default="chrome-map.json", + help="Path to the chrome-map.json file.", + ) + parser.add_argument( + "--app-dir", + default="dist/bin/browser/", + help="Prefix of the appdir in use. This is used to map " + "urls starting with resource:///. It may differ by " + "app, but defaults to the valid value for firefox.", + ) + parser.add_argument( + "--gre-dir", + default="dist/bin/", + help="Prefix of the gre dir in use. This is used to map " + "urls starting with resource://gre. It may differ by " + "app, but defaults to the valid value for firefox.", + ) + parser.add_argument( + "--output-suffix", default=".out", help="The suffix to append to output files." + ) + parser.add_argument( + "--extra-chrome-manifests", + nargs="+", + help="Paths to files containing extra chrome registration.", + ) + parser.add_argument( + "--output-file", + default="", + help="The output file where the results are merged. Leave empty to make the rewriter not " + "merge files.", + ) + parser.add_argument("files", nargs="+", help="The set of files to process.") + + args = parser.parse_args() + + rewriter = LcovFileRewriter( + args.chrome_map_path, args.app_dir, args.gre_dir, args.extra_chrome_manifests + ) + + files = [] + for f in args.files: + if os.path.isdir(f): + files += [os.path.join(f, e) for e in os.listdir(f)] + else: + files.append(f) + + rewriter.rewrite_files(files, args.output_file, args.output_suffix) + + +if __name__ == "__main__": + main() diff --git a/python/mozbuild/mozbuild/codecoverage/manifest_handler.py b/python/mozbuild/mozbuild/codecoverage/manifest_handler.py new file mode 100644 index 0000000000..1f67b4089c --- /dev/null +++ b/python/mozbuild/mozbuild/codecoverage/manifest_handler.py @@ -0,0 +1,52 @@ +# 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 collections import defaultdict + +try: + import urlparse +except ImportError: + import urllib.parse as urlparse + +import mozpack.path as mozpath +from mozpack.chrome.manifest import ( + Manifest, + ManifestChrome, + ManifestOverride, + ManifestResource, + parse_manifest, +) + + +class ChromeManifestHandler(object): + def __init__(self): + self.overrides = {} + self.chrome_mapping = defaultdict(set) + + def handle_manifest_entry(self, entry): + format_strings = { + "content": "chrome://%s/content/", + "resource": "resource://%s/", + "locale": "chrome://%s/locale/", + "skin": "chrome://%s/skin/", + } + + if isinstance(entry, (ManifestChrome, ManifestResource)): + if isinstance(entry, ManifestResource): + dest = entry.target + url = urlparse.urlparse(dest) + if not url.scheme: + dest = mozpath.normpath(mozpath.join(entry.base, dest)) + if url.scheme == "file": + dest = mozpath.normpath(url.path) + else: + dest = mozpath.normpath(entry.path) + + base_uri = format_strings[entry.type] % entry.name + self.chrome_mapping[base_uri].add(dest) + if isinstance(entry, ManifestOverride): + self.overrides[entry.overloaded] = entry.overload + if isinstance(entry, Manifest): + for e in parse_manifest(None, entry.path): + self.handle_manifest_entry(e) diff --git a/python/mozbuild/mozbuild/codecoverage/packager.py b/python/mozbuild/mozbuild/codecoverage/packager.py new file mode 100644 index 0000000000..92254a96f5 --- /dev/null +++ b/python/mozbuild/mozbuild/codecoverage/packager.py @@ -0,0 +1,71 @@ +# 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/. + +import argparse +import errno +import json +import sys + +import buildconfig +import mozpack.path as mozpath +from mozpack.copier import FileRegistry, Jarrer +from mozpack.files import FileFinder, GeneratedFile +from mozpack.manifests import InstallManifest, UnreadableInstallManifest + + +def describe_install_manifest(manifest, dest_dir): + try: + manifest = InstallManifest(manifest) + except UnreadableInstallManifest: + raise IOError(errno.EINVAL, "Error parsing manifest file", manifest) + + reg = FileRegistry() + + mapping = {} + manifest.populate_registry(reg) + dest_dir = mozpath.join(buildconfig.topobjdir, dest_dir) + for dest_file, src in reg: + if hasattr(src, "path"): + dest_path = mozpath.join(dest_dir, dest_file) + relsrc_path = mozpath.relpath(src.path, buildconfig.topsrcdir) + mapping[dest_path] = relsrc_path + + return mapping + + +def package_coverage_data(root, output_file): + finder = FileFinder(root) + jarrer = Jarrer() + for p, f in finder.find("**/*.gcno"): + jarrer.add(p, f) + + dist_include_manifest = mozpath.join( + buildconfig.topobjdir, "_build_manifests", "install", "dist_include" + ) + linked_files = describe_install_manifest(dist_include_manifest, "dist/include") + mapping_file = GeneratedFile(json.dumps(linked_files, sort_keys=True)) + jarrer.add("linked-files-map.json", mapping_file) + jarrer.copy(output_file) + + +def cli(args=sys.argv[1:]): + parser = argparse.ArgumentParser() + parser.add_argument( + "-o", "--output-file", dest="output_file", help="Path to save packaged data to." + ) + parser.add_argument( + "--root", dest="root", default=None, help="Root directory to search from." + ) + args = parser.parse_args(args) + + if not args.root: + from buildconfig import topobjdir + + args.root = topobjdir + + return package_coverage_data(args.root, args.output_file) + + +if __name__ == "__main__": + sys.exit(cli()) diff --git a/python/mozbuild/mozbuild/compilation/__init__.py b/python/mozbuild/mozbuild/compilation/__init__.py new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/python/mozbuild/mozbuild/compilation/__init__.py diff --git a/python/mozbuild/mozbuild/compilation/codecomplete.py b/python/mozbuild/mozbuild/compilation/codecomplete.py new file mode 100644 index 0000000000..b5a466b729 --- /dev/null +++ b/python/mozbuild/mozbuild/compilation/codecomplete.py @@ -0,0 +1,55 @@ +# 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/. + +# This modules provides functionality for dealing with code completion. + +from mach.decorators import Command, CommandArgument + +from mozbuild.shellutil import quote as shell_quote +from mozbuild.shellutil import split as shell_split + + +# Instropection commands. + + +@Command( + "compileflags", + category="devenv", + description="Display the compilation flags for a given source file", +) +@CommandArgument( + "what", default=None, help="Source file to display compilation flags for" +) +def compileflags(command_context, what): + from mozbuild.compilation import util + from mozbuild.util import resolve_target_to_make + + if not util.check_top_objdir(command_context.topobjdir): + return 1 + + path_arg = command_context._wrap_path_argument(what) + + make_dir, make_target = resolve_target_to_make( + command_context.topobjdir, path_arg.relpath() + ) + + if make_dir is None and make_target is None: + return 1 + + build_vars = util.get_build_vars(make_dir, command_context) + + if what.endswith(".c"): + cc = "CC" + name = "COMPILE_CFLAGS" + else: + cc = "CXX" + name = "COMPILE_CXXFLAGS" + + if name not in build_vars: + return + + # Drop the first flag since that is the pathname of the compiler. + flags = (shell_split(build_vars[cc]) + shell_split(build_vars[name]))[1:] + + print(" ".join(shell_quote(arg) for arg in util.sanitize_cflags(flags))) diff --git a/python/mozbuild/mozbuild/compilation/database.py b/python/mozbuild/mozbuild/compilation/database.py new file mode 100644 index 0000000000..e741c88a81 --- /dev/null +++ b/python/mozbuild/mozbuild/compilation/database.py @@ -0,0 +1,244 @@ +# 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/. + +# This modules provides functionality for dealing with code completion. + +import os +from collections import OrderedDict, defaultdict + +import mozpack.path as mozpath + +from mozbuild.backend.common import CommonBackend +from mozbuild.frontend.data import ( + ComputedFlags, + DirectoryTraversal, + PerSourceFlag, + Sources, + VariablePassthru, +) +from mozbuild.shellutil import quote as shell_quote +from mozbuild.util import expand_variables + + +class CompileDBBackend(CommonBackend): + def _init(self): + CommonBackend._init(self) + + # The database we're going to dump out to. + self._db = OrderedDict() + + # The cache for per-directory flags + self._flags = {} + + self._envs = {} + self._local_flags = defaultdict(dict) + self._per_source_flags = defaultdict(list) + + def _build_cmd(self, cmd, filename, unified): + cmd = list(cmd) + if unified is None: + cmd.append(filename) + else: + cmd.append(unified) + + return cmd + + def consume_object(self, obj): + # Those are difficult directories, that will be handled later. + if obj.relsrcdir in ( + "build/unix/elfhack", + "build/unix/elfhack/inject", + "build/clang-plugin", + "build/clang-plugin/tests", + ): + return True + + consumed = CommonBackend.consume_object(self, obj) + + if consumed: + return True + + if isinstance(obj, DirectoryTraversal): + self._envs[obj.objdir] = obj.config + + elif isinstance(obj, Sources): + # For other sources, include each source file. + for f in obj.files: + self._build_db_line( + obj.objdir, obj.relsrcdir, obj.config, f, obj.canonical_suffix + ) + + elif isinstance(obj, VariablePassthru): + for var in ("MOZBUILD_CMFLAGS", "MOZBUILD_CMMFLAGS"): + if var in obj.variables: + self._local_flags[obj.objdir][var] = obj.variables[var] + + elif isinstance(obj, PerSourceFlag): + self._per_source_flags[obj.file_name].extend(obj.flags) + + elif isinstance(obj, ComputedFlags): + for var, flags in obj.get_flags(): + self._local_flags[obj.objdir]["COMPUTED_%s" % var] = flags + + return True + + def consume_finished(self): + CommonBackend.consume_finished(self) + + db = [] + + for (directory, filename, unified), cmd in self._db.items(): + env = self._envs[directory] + cmd = self._build_cmd(cmd, filename, unified) + variables = { + "DIST": mozpath.join(env.topobjdir, "dist"), + "DEPTH": env.topobjdir, + "MOZILLA_DIR": env.topsrcdir, + "topsrcdir": env.topsrcdir, + "topobjdir": env.topobjdir, + } + variables.update(self._local_flags[directory]) + c = [] + for a in cmd: + accum = "" + for word in expand_variables(a, variables).split(): + # We can't just split() the output of expand_variables since + # there can be spaces enclosed by quotes, e.g. '"foo bar"'. + # Handle that case by checking whether there are an even + # number of double-quotes in the word and appending it to + # the accumulator if not. Meanwhile, shlex.split() and + # mozbuild.shellutil.split() aren't able to properly handle + # this and break in various ways, so we can't use something + # off-the-shelf. + has_quote = bool(word.count('"') % 2) + if accum and has_quote: + c.append(accum + " " + word) + accum = "" + elif accum and not has_quote: + accum += " " + word + elif not accum and has_quote: + accum = word + else: + c.append(word) + # Tell clangd to keep parsing to the end of a file, regardless of + # how many errors are encountered. (Unified builds mean that we + # encounter a lot of errors parsing some files.) + c.insert(-1, "-ferror-limit=0") + + per_source_flags = self._per_source_flags.get(filename) + if per_source_flags is not None: + c.extend(per_source_flags) + db.append( + { + "directory": directory, + "command": " ".join(shell_quote(a) for a in c), + "file": mozpath.join(directory, filename), + } + ) + + import json + + outputfile = self._outputfile_path() + with self._write_file(outputfile) as jsonout: + json.dump(db, jsonout, indent=0) + + def _outputfile_path(self): + # Output the database (a JSON file) to objdir/compile_commands.json + return os.path.join(self.environment.topobjdir, "compile_commands.json") + + def _process_unified_sources_without_mapping(self, obj): + for f in list(sorted(obj.files)): + self._build_db_line( + obj.objdir, obj.relsrcdir, obj.config, f, obj.canonical_suffix + ) + + def _process_unified_sources(self, obj): + if not obj.have_unified_mapping: + return self._process_unified_sources_without_mapping(obj) + + # For unified sources, only include the unified source file. + # Note that unified sources are never used for host sources. + for f in obj.unified_source_mapping: + self._build_db_line( + obj.objdir, obj.relsrcdir, obj.config, f[0], obj.canonical_suffix + ) + for entry in f[1]: + self._build_db_line( + obj.objdir, + obj.relsrcdir, + obj.config, + entry, + obj.canonical_suffix, + unified=f[0], + ) + + def _handle_idl_manager(self, idl_manager): + pass + + def _handle_ipdl_sources( + self, + ipdl_dir, + sorted_ipdl_sources, + sorted_nonstatic_ipdl_sources, + sorted_static_ipdl_sources, + ): + pass + + def _handle_webidl_build( + self, + bindings_dir, + unified_source_mapping, + webidls, + expected_build_output_files, + global_define_files, + ): + for f in unified_source_mapping: + self._build_db_line(bindings_dir, None, self.environment, f[0], ".cpp") + + COMPILERS = { + ".c": "CC", + ".cpp": "CXX", + ".m": "CC", + ".mm": "CXX", + } + + CFLAGS = { + ".c": "CFLAGS", + ".cpp": "CXXFLAGS", + ".m": "CFLAGS", + ".mm": "CXXFLAGS", + } + + def _get_compiler_args(self, cenv, canonical_suffix): + if canonical_suffix not in self.COMPILERS: + return None + return cenv.substs[self.COMPILERS[canonical_suffix]].split() + + def _build_db_line( + self, objdir, reldir, cenv, filename, canonical_suffix, unified=None + ): + compiler_args = self._get_compiler_args(cenv, canonical_suffix) + if compiler_args is None: + return + db = self._db.setdefault( + (objdir, filename, unified), + compiler_args + ["-o", "/dev/null", "-c"], + ) + reldir = reldir or mozpath.relpath(objdir, cenv.topobjdir) + + def append_var(name): + value = cenv.substs.get(name) + if not value: + return + if isinstance(value, str): + value = value.split() + db.extend(value) + + db.append("$(COMPUTED_%s)" % self.CFLAGS[canonical_suffix]) + if canonical_suffix == ".m": + append_var("OS_COMPILE_CMFLAGS") + db.append("$(MOZBUILD_CMFLAGS)") + elif canonical_suffix == ".mm": + append_var("OS_COMPILE_CMMFLAGS") + db.append("$(MOZBUILD_CMMFLAGS)") diff --git a/python/mozbuild/mozbuild/compilation/util.py b/python/mozbuild/mozbuild/compilation/util.py new file mode 100644 index 0000000000..fc06382a3b --- /dev/null +++ b/python/mozbuild/mozbuild/compilation/util.py @@ -0,0 +1,64 @@ +# 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/. + +import os + + +def check_top_objdir(topobjdir): + top_make = os.path.join(topobjdir, "Makefile") + if not os.path.exists(top_make): + print( + "Your tree has not been built yet. Please run " + "|mach build| with no arguments." + ) + return False + return True + + +def get_build_vars(directory, cmd): + build_vars = {} + + def on_line(line): + elements = [s.strip() for s in line.split("=", 1)] + + if len(elements) != 2: + return + + build_vars[elements[0]] = elements[1] + + try: + old_logger = cmd.log_manager.replace_terminal_handler(None) + cmd._run_make( + directory=directory, + target="showbuild", + log=False, + print_directory=False, + num_jobs=1, + silent=True, + line_handler=on_line, + ) + finally: + cmd.log_manager.replace_terminal_handler(old_logger) + + return build_vars + + +def sanitize_cflags(flags): + # We filter out -Xclang arguments as clang based tools typically choke on + # passing these flags down to the clang driver. -Xclang tells the clang + # driver driver to pass whatever comes after it down to clang cc1, which is + # why we skip -Xclang and the argument immediately after it. Here is an + # example: the following two invocations pass |-foo -bar -baz| to cc1: + # clang -cc1 -foo -bar -baz + # clang -Xclang -foo -Xclang -bar -Xclang -baz + sanitized = [] + saw_xclang = False + for flag in flags: + if flag == "-Xclang": + saw_xclang = True + elif saw_xclang: + saw_xclang = False + else: + sanitized.append(flag) + return sanitized diff --git a/python/mozbuild/mozbuild/compilation/warnings.py b/python/mozbuild/mozbuild/compilation/warnings.py new file mode 100644 index 0000000000..4f0ef57e51 --- /dev/null +++ b/python/mozbuild/mozbuild/compilation/warnings.py @@ -0,0 +1,392 @@ +# 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/. + +# This modules provides functionality for dealing with compiler warnings. + +import errno +import io +import json +import os +import re + +import mozpack.path as mozpath +import six + +from mozbuild.util import hash_file + +# Regular expression to strip ANSI color sequences from a string. This is +# needed to properly analyze Clang compiler output, which may be colorized. +# It assumes ANSI escape sequences. +RE_STRIP_COLORS = re.compile(r"\x1b\[[\d;]+m") + +# This captures Clang diagnostics with the standard formatting. +RE_CLANG_WARNING_AND_ERROR = re.compile( + r""" + (?P<file>[^:]+) + : + (?P<line>\d+) + : + (?P<column>\d+) + : + \s(?P<type>warning|error):\s + (?P<message>.+) + \[(?P<flag>[^\]]+) + """, + re.X, +) + +# This captures Clang-cl warning format. +RE_CLANG_CL_WARNING_AND_ERROR = re.compile( + r""" + (?P<file>.*) + \((?P<line>\d+),(?P<column>\d+)\) + \s?:\s+(?P<type>warning|error):\s + (?P<message>.*) + \[(?P<flag>[^\]]+) + """, + re.X, +) + +IN_FILE_INCLUDED_FROM = "In file included from " + + +class CompilerWarning(dict): + """Represents an individual compiler warning.""" + + def __init__(self): + dict.__init__(self) + + self["filename"] = None + self["line"] = None + self["column"] = None + self["message"] = None + self["flag"] = None + + def copy(self): + """Returns a copy of this compiler warning.""" + w = CompilerWarning() + w.update(self) + return w + + # Since we inherit from dict, functools.total_ordering gets confused. + # Thus, we define a key function, a generic comparison, and then + # implement all the rich operators with those; approach is from: + # http://regebro.wordpress.com/2010/12/13/python-implementing-rich-comparison-the-correct-way/ + def _cmpkey(self): + return (self["filename"], self["line"], self["column"]) + + def _compare(self, other, func): + if not isinstance(other, CompilerWarning): + return NotImplemented + + return func(self._cmpkey(), other._cmpkey()) + + def __eq__(self, other): + return self._compare(other, lambda s, o: s == o) + + def __neq__(self, other): + return self._compare(other, lambda s, o: s != o) + + def __lt__(self, other): + return self._compare(other, lambda s, o: s < o) + + def __le__(self, other): + return self._compare(other, lambda s, o: s <= o) + + def __gt__(self, other): + return self._compare(other, lambda s, o: s > o) + + def __ge__(self, other): + return self._compare(other, lambda s, o: s >= o) + + def __hash__(self): + """Define so this can exist inside a set, etc.""" + return hash(tuple(sorted(self.items()))) + + +class WarningsDatabase(object): + """Holds a collection of warnings. + + The warnings database is a semi-intelligent container that holds warnings + encountered during builds. + + The warnings database is backed by a JSON file. But, that is transparent + to consumers. + + Under most circumstances, the warnings database is insert only. When a + warning is encountered, the caller simply blindly inserts it into the + database. The database figures out whether it is a dupe, etc. + + During the course of development, it is common for warnings to change + slightly as source code changes. For example, line numbers will disagree. + The WarningsDatabase handles this by storing the hash of a file a warning + occurred in. At warning insert time, if the hash of the file does not match + what is stored in the database, the existing warnings for that file are + purged from the database. + + Callers should periodically prune old, invalid warnings from the database + by calling prune(). A good time to do this is at the end of a build. + """ + + def __init__(self): + """Create an empty database.""" + self._files = {} + + def __len__(self): + i = 0 + for value in self._files.values(): + i += len(value["warnings"]) + + return i + + def __iter__(self): + for value in self._files.values(): + for warning in value["warnings"]: + yield warning + + def __contains__(self, item): + for value in self._files.values(): + for warning in value["warnings"]: + if warning == item: + return True + + return False + + @property + def warnings(self): + """All the CompilerWarning instances in this database.""" + for value in self._files.values(): + for w in value["warnings"]: + yield w + + def type_counts(self, dirpath=None): + """Returns a mapping of warning types to their counts.""" + + types = {} + for value in self._files.values(): + for warning in value["warnings"]: + if dirpath and not mozpath.normsep(warning["filename"]).startswith( + dirpath + ): + continue + flag = warning["flag"] + count = types.get(flag, 0) + count += 1 + + types[flag] = count + + return types + + def has_file(self, filename): + """Whether we have any warnings for the specified file.""" + return filename in self._files + + def warnings_for_file(self, filename): + """Obtain the warnings for the specified file.""" + f = self._files.get(filename, {"warnings": []}) + + for warning in f["warnings"]: + yield warning + + def insert(self, warning, compute_hash=True): + assert isinstance(warning, CompilerWarning) + + filename = warning["filename"] + + new_hash = None + + if compute_hash: + new_hash = hash_file(filename) + + if filename in self._files: + if new_hash != self._files[filename]["hash"]: + del self._files[filename] + + value = self._files.get( + filename, + { + "hash": new_hash, + "warnings": set(), + }, + ) + + value["warnings"].add(warning) + + self._files[filename] = value + + def prune(self): + """Prune the contents of the database. + + This removes warnings that are no longer valid. A warning is no longer + valid if the file it was in no longer exists or if the content has + changed. + + The check for changed content catches the case where a file previously + contained warnings but no longer does. + """ + + # Need to calculate up front since we are mutating original object. + filenames = list(six.iterkeys(self._files)) + for filename in filenames: + if not os.path.exists(filename): + del self._files[filename] + continue + + if self._files[filename]["hash"] is None: + continue + + current_hash = hash_file(filename) + if current_hash != self._files[filename]["hash"]: + del self._files[filename] + continue + + def serialize(self, fh): + """Serialize the database to an open file handle.""" + obj = {"files": {}} + + # All this hackery because JSON can't handle sets. + for k, v in six.iteritems(self._files): + obj["files"][k] = {} + + for k2, v2 in six.iteritems(v): + normalized = v2 + if isinstance(v2, set): + normalized = list(v2) + obj["files"][k][k2] = normalized + + to_write = six.ensure_text(json.dumps(obj, indent=2)) + fh.write(to_write) + + def deserialize(self, fh): + """Load serialized content from a handle into the current instance.""" + obj = json.load(fh) + + self._files = obj["files"] + + # Normalize data types. + for filename, value in six.iteritems(self._files): + if "warnings" in value: + normalized = set() + for d in value["warnings"]: + w = CompilerWarning() + w.update(d) + normalized.add(w) + + self._files[filename]["warnings"] = normalized + + def load_from_file(self, filename): + """Load the database from a file.""" + with io.open(filename, "r", encoding="utf-8") as fh: + self.deserialize(fh) + + def save_to_file(self, filename): + """Save the database to a file.""" + try: + # Ensure the directory exists + os.makedirs(os.path.dirname(filename)) + except OSError as e: + if e.errno != errno.EEXIST: + raise + with io.open(filename, "w", encoding="utf-8", newline="\n") as fh: + self.serialize(fh) + + +class WarningsCollector(object): + """Collects warnings from text data. + + Instances of this class receive data (usually the output of compiler + invocations) and parse it into warnings. + + The collector works by incrementally receiving data, usually line-by-line + output from the compiler. Therefore, it can maintain state to parse + multi-line warning messages. + """ + + def __init__(self, cb, objdir=None): + """Initialize a new collector. + + ``cb`` is a callable that is called with a ``CompilerWarning`` + instance whenever a new warning is parsed. + + ``objdir`` is the object directory. Used for normalizing paths. + """ + self.cb = cb + self.objdir = objdir + self.included_from = [] + + def process_line(self, line): + """Take a line of text and process it for a warning.""" + + filtered = RE_STRIP_COLORS.sub("", line) + + # Clang warnings in files included from the one(s) being compiled will + # start with "In file included from /path/to/file:line:". Here, we + # record those. + if filtered.startswith(IN_FILE_INCLUDED_FROM): + included_from = filtered[len(IN_FILE_INCLUDED_FROM) :] + + parts = included_from.split(":") + + self.included_from.append(parts[0]) + + return + + warning = CompilerWarning() + filename = None + + # TODO make more efficient so we run minimal regexp matches. + match_clang = RE_CLANG_WARNING_AND_ERROR.match(filtered) + match_clang_cl = RE_CLANG_CL_WARNING_AND_ERROR.match(filtered) + if match_clang: + d = match_clang.groupdict() + + filename = d["file"] + warning["type"] = d["type"] + warning["line"] = int(d["line"]) + warning["column"] = int(d["column"]) + warning["flag"] = d["flag"] + warning["message"] = d["message"].rstrip() + + elif match_clang_cl: + d = match_clang_cl.groupdict() + + filename = d["file"] + warning["type"] = d["type"] + warning["line"] = int(d["line"]) + warning["column"] = int(d["column"]) + warning["flag"] = d["flag"] + warning["message"] = d["message"].rstrip() + + else: + self.included_from = [] + return None + + filename = os.path.normpath(filename) + + # Sometimes we get relative includes. These typically point to files in + # the object directory. We try to resolve the relative path. + if not os.path.isabs(filename): + filename = self._normalize_relative_path(filename) + + warning["filename"] = filename + + self.cb(warning) + + return warning + + def _normalize_relative_path(self, filename): + # Special case files in dist/include. + idx = filename.find("/dist/include") + if idx != -1: + return self.objdir + filename[idx:] + + for included_from in self.included_from: + source_dir = os.path.dirname(included_from) + + candidate = os.path.normpath(os.path.join(source_dir, filename)) + + if os.path.exists(candidate): + return candidate + + return filename diff --git a/python/mozbuild/mozbuild/config_status.py b/python/mozbuild/mozbuild/config_status.py new file mode 100644 index 0000000000..8e8a7f625b --- /dev/null +++ b/python/mozbuild/mozbuild/config_status.py @@ -0,0 +1,184 @@ +# 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/. + +# Combined with build/autoconf/config.status.m4, ConfigStatus is an almost +# drop-in replacement for autoconf 2.13's config.status, with features +# borrowed from autoconf > 2.5, and additional features. + +import logging +import os +import sys +import time +from argparse import ArgumentParser +from itertools import chain + +from mach.logging import LoggingManager + +from mozbuild.backend import backends, get_backend_class +from mozbuild.backend.configenvironment import ConfigEnvironment +from mozbuild.base import MachCommandConditions +from mozbuild.frontend.emitter import TreeMetadataEmitter +from mozbuild.frontend.reader import BuildReader +from mozbuild.mozinfo import write_mozinfo +from mozbuild.util import FileAvoidWrite, process_time + +log_manager = LoggingManager() + + +ANDROID_IDE_ADVERTISEMENT = """ +============= +ADVERTISEMENT + +You are building GeckoView. After your build completes, you can open +the top source directory in Android Studio directly and build using Gradle. +See the documentation at + +https://firefox-source-docs.mozilla.org/mobile/android/geckoview/contributor/geckoview-quick-start.html#build-using-android-studio +============= +""".strip() + + +def config_status( + topobjdir=".", + topsrcdir=".", + defines=None, + substs=None, + source=None, + mozconfig=None, + args=sys.argv[1:], +): + """Main function, providing config.status functionality. + + Contrary to config.status, it doesn't use CONFIG_FILES or CONFIG_HEADERS + variables. + + Without the -n option, this program acts as config.status and considers + the current directory as the top object directory, even when config.status + is in a different directory. It will, however, treat the directory + containing config.status as the top object directory with the -n option. + + The options to this function are passed when creating the + ConfigEnvironment. These lists, as well as the actual wrapper script + around this function, are meant to be generated by configure. + See build/autoconf/config.status.m4. + """ + + if "CONFIG_FILES" in os.environ: + raise Exception( + "Using the CONFIG_FILES environment variable is not " "supported." + ) + if "CONFIG_HEADERS" in os.environ: + raise Exception( + "Using the CONFIG_HEADERS environment variable is not " "supported." + ) + + if not os.path.isabs(topsrcdir): + raise Exception( + "topsrcdir must be defined as an absolute directory: " "%s" % topsrcdir + ) + + default_backends = ["RecursiveMake"] + default_backends = (substs or {}).get("BUILD_BACKENDS", ["RecursiveMake"]) + + parser = ArgumentParser() + parser.add_argument( + "-v", + "--verbose", + dest="verbose", + action="store_true", + help="display verbose output", + ) + parser.add_argument( + "-n", + dest="not_topobjdir", + action="store_true", + help="do not consider current directory as top object directory", + ) + parser.add_argument( + "-d", "--diff", action="store_true", help="print diffs of changed files." + ) + parser.add_argument( + "-b", + "--backend", + nargs="+", + choices=sorted(backends), + default=default_backends, + help="what backend to build (default: %s)." % " ".join(default_backends), + ) + parser.add_argument( + "--dry-run", action="store_true", help="do everything except writing files out." + ) + options = parser.parse_args(args) + + # Without -n, the current directory is meant to be the top object directory + if not options.not_topobjdir: + topobjdir = os.path.realpath(".") + + env = ConfigEnvironment( + topsrcdir, + topobjdir, + defines=defines, + substs=substs, + source=source, + mozconfig=mozconfig, + ) + + with FileAvoidWrite(os.path.join(topobjdir, "mozinfo.json")) as f: + write_mozinfo(f, env, os.environ) + + cpu_start = process_time() + time_start = time.monotonic() + + # Make appropriate backend instances, defaulting to RecursiveMakeBackend, + # or what is in BUILD_BACKENDS. + selected_backends = [get_backend_class(b)(env) for b in options.backend] + + if options.dry_run: + for b in selected_backends: + b.dry_run = True + + reader = BuildReader(env) + emitter = TreeMetadataEmitter(env) + # This won't actually do anything because of the magic of generators. + definitions = emitter.emit(reader.read_topsrcdir()) + + log_level = logging.DEBUG if options.verbose else logging.INFO + log_manager.add_terminal_logging(level=log_level) + log_manager.enable_unstructured() + + print("Reticulating splines...", file=sys.stderr) + if len(selected_backends) > 1: + definitions = list(definitions) + + for the_backend in selected_backends: + the_backend.consume(definitions) + + execution_time = 0.0 + for obj in chain((reader, emitter), selected_backends): + summary = obj.summary() + print(summary, file=sys.stderr) + execution_time += summary.execution_time + if hasattr(obj, "gyp_summary"): + summary = obj.gyp_summary() + print(summary, file=sys.stderr) + + cpu_time = process_time() - cpu_start + wall_time = time.monotonic() - time_start + efficiency = cpu_time / wall_time if wall_time else 100 + untracked = wall_time - execution_time + + print( + "Total wall time: {:.2f}s; CPU time: {:.2f}s; Efficiency: " + "{:.0%}; Untracked: {:.2f}s".format(wall_time, cpu_time, efficiency, untracked), + file=sys.stderr, + ) + + if options.diff: + for the_backend in selected_backends: + for path, diff in sorted(the_backend.file_diffs.items()): + print("\n".join(diff)) + + # Advertise Android Studio if it is appropriate. + if MachCommandConditions.is_android(env): + print(ANDROID_IDE_ADVERTISEMENT) diff --git a/python/mozbuild/mozbuild/configure/__init__.py b/python/mozbuild/mozbuild/configure/__init__.py new file mode 100644 index 0000000000..5a4edf3fb8 --- /dev/null +++ b/python/mozbuild/mozbuild/configure/__init__.py @@ -0,0 +1,1314 @@ +# 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/. + +import codecs +import inspect +import logging +import os +import re +import sys +import types +from collections import OrderedDict +from contextlib import contextmanager +from functools import wraps + +import mozpack.path as mozpath +import six +from six.moves import builtins as __builtin__ + +from mozbuild.configure.help import HelpFormatter +from mozbuild.configure.options import ( + HELP_OPTIONS_CATEGORY, + CommandLineHelper, + ConflictingOptionError, + InvalidOptionError, + Option, + OptionValue, +) +from mozbuild.configure.util import ConfigureOutputHandler, LineIO, getpreferredencoding +from mozbuild.util import ( + ReadOnlyDict, + ReadOnlyNamespace, + memoize, + memoized_property, + system_encoding, +) + +# TRACE logging level, below (thus more verbose than) DEBUG +TRACE = 5 + + +class ConfigureError(Exception): + pass + + +class SandboxDependsFunction(object): + """Sandbox-visible representation of @depends functions.""" + + def __init__(self, unsandboxed): + self._or = unsandboxed.__or__ + self._and = unsandboxed.__and__ + self._getattr = unsandboxed.__getattr__ + + def __call__(self, *arg, **kwargs): + raise ConfigureError("The `%s` function may not be called" % self.__name__) + + def __or__(self, other): + if not isinstance(other, SandboxDependsFunction): + raise ConfigureError( + "Can only do binary arithmetic operations " + "with another @depends function." + ) + return self._or(other).sandboxed + + def __and__(self, other): + if not isinstance(other, SandboxDependsFunction): + raise ConfigureError( + "Can only do binary arithmetic operations " + "with another @depends function." + ) + return self._and(other).sandboxed + + def __cmp__(self, other): + raise ConfigureError("Cannot compare @depends functions.") + + def __eq__(self, other): + raise ConfigureError("Cannot compare @depends functions.") + + def __hash__(self): + return object.__hash__(self) + + def __ne__(self, other): + raise ConfigureError("Cannot compare @depends functions.") + + def __lt__(self, other): + raise ConfigureError("Cannot compare @depends functions.") + + def __le__(self, other): + raise ConfigureError("Cannot compare @depends functions.") + + def __gt__(self, other): + raise ConfigureError("Cannot compare @depends functions.") + + def __ge__(self, other): + raise ConfigureError("Cannot compare @depends functions.") + + def __getattr__(self, key): + return self._getattr(key).sandboxed + + def __nonzero__(self): + raise ConfigureError("Cannot do boolean operations on @depends functions.") + + +class DependsFunction(object): + __slots__ = ( + "_func", + "_name", + "dependencies", + "when", + "sandboxed", + "sandbox", + "_result", + ) + + def __init__(self, sandbox, func, dependencies, when=None): + assert isinstance(sandbox, ConfigureSandbox) + assert not inspect.isgeneratorfunction(func) + # Allow non-functions when there are no dependencies. This is equivalent + # to passing a lambda that returns the given value. + if not (inspect.isroutine(func) or not dependencies): + print(func) + assert inspect.isroutine(func) or not dependencies + self._func = func + self._name = getattr(func, "__name__", None) + self.dependencies = dependencies + self.sandboxed = wraps(func)(SandboxDependsFunction(self)) + self.sandbox = sandbox + self.when = when + sandbox._depends[self.sandboxed] = self + + # Only @depends functions with a dependency on '--help' are executed + # immediately. Everything else is queued for later execution. + if sandbox._help_option in dependencies: + sandbox._value_for(self) + elif not sandbox._help: + sandbox._execution_queue.append((sandbox._value_for, (self,))) + + @property + def name(self): + return self._name + + @name.setter + def name(self, value): + self._name = value + + @property + def sandboxed_dependencies(self): + return [ + d.sandboxed if isinstance(d, DependsFunction) else d + for d in self.dependencies + ] + + @memoize + def result(self): + if self.when and not self.sandbox._value_for(self.when): + return None + + if inspect.isroutine(self._func): + resolved_args = [self.sandbox._value_for(d) for d in self.dependencies] + return self._func(*resolved_args) + return self._func + + def __repr__(self): + return "<%s %s(%s)>" % ( + self.__class__.__name__, + self.name, + ", ".join(repr(d) for d in self.dependencies), + ) + + def __or__(self, other): + if isinstance(other, SandboxDependsFunction): + other = self.sandbox._depends.get(other) + assert isinstance(other, DependsFunction) + assert self.sandbox is other.sandbox + return CombinedDependsFunction(self.sandbox, self.or_impl, (self, other)) + + @staticmethod + def or_impl(iterable): + # Applies "or" to all the items of iterable. + # e.g. if iterable contains a, b and c, returns `a or b or c`. + for i in iterable: + if i: + return i + return i + + def __and__(self, other): + if isinstance(other, SandboxDependsFunction): + other = self.sandbox._depends.get(other) + assert isinstance(other, DependsFunction) + assert self.sandbox is other.sandbox + return CombinedDependsFunction(self.sandbox, self.and_impl, (self, other)) + + @staticmethod + def and_impl(iterable): + # Applies "and" to all the items of iterable. + # e.g. if iterable contains a, b and c, returns `a and b and c`. + for i in iterable: + if not i: + return i + return i + + def __getattr__(self, key): + if key.startswith("_"): + return super(DependsFunction, self).__getattr__(key) + # Our function may return None or an object that simply doesn't have + # the wanted key. In that case, just return None. + return TrivialDependsFunction( + self.sandbox, lambda x: getattr(x, key, None), [self], self.when + ) + + +class TrivialDependsFunction(DependsFunction): + """Like a DependsFunction, but the linter won't expect it to have a + dependency on --help ever.""" + + +class CombinedDependsFunction(DependsFunction): + def __init__(self, sandbox, func, dependencies): + flatten_deps = [] + for d in dependencies: + if isinstance(d, CombinedDependsFunction) and d._func is func: + for d2 in d.dependencies: + if d2 not in flatten_deps: + flatten_deps.append(d2) + elif d not in flatten_deps: + flatten_deps.append(d) + + super(CombinedDependsFunction, self).__init__(sandbox, func, flatten_deps) + + @memoize + def result(self): + resolved_args = (self.sandbox._value_for(d) for d in self.dependencies) + return self._func(resolved_args) + + def __eq__(self, other): + return ( + isinstance(other, self.__class__) + and self._func is other._func + and set(self.dependencies) == set(other.dependencies) + ) + + def __hash__(self): + return object.__hash__(self) + + def __ne__(self, other): + return not self == other + + +class SandboxedGlobal(dict): + """Identifiable dict type for use as function global""" + + +def forbidden_import(*args, **kwargs): + raise ImportError("Importing modules is forbidden") + + +class ConfigureSandbox(dict): + """Represents a sandbox for executing Python code for build configuration. + This is a different kind of sandboxing than the one used for moz.build + processing. + + The sandbox has 9 primitives: + - option + - depends + - template + - imports + - include + - set_config + - set_define + - imply_option + - only_when + + `option`, `include`, `set_config`, `set_define` and `imply_option` are + functions. `depends`, `template`, and `imports` are decorators. `only_when` + is a context_manager. + + These primitives are declared as name_impl methods to this class and + the mapping name -> name_impl is done automatically in __getitem__. + + Additional primitives should be frowned upon to keep the sandbox itself as + simple as possible. Instead, helpers should be created within the sandbox + with the existing primitives. + + The sandbox is given, at creation, a dict where the yielded configuration + will be stored. + + config = {} + sandbox = ConfigureSandbox(config) + sandbox.run(path) + do_stuff(config) + """ + + # The default set of builtins. We expose unicode as str to make sandboxed + # files more python3-ready. + BUILTINS = ReadOnlyDict( + { + b: getattr(__builtin__, b, None) + for b in ( + "AssertionError", + "False", + "None", + "True", + "__build_class__", # will be None on py2 + "all", + "any", + "bool", + "dict", + "enumerate", + "getattr", + "hasattr", + "int", + "isinstance", + "len", + "list", + "max", + "min", + "range", + "set", + "sorted", + "tuple", + "zip", + ) + }, + __import__=forbidden_import, + str=six.text_type, + ) + + # Expose a limited set of functions from os.path + OS = ReadOnlyNamespace( + path=ReadOnlyNamespace( + **{ + k: getattr(mozpath, k, getattr(os.path, k)) + for k in ( + "abspath", + "basename", + "dirname", + "isabs", + "join", + "normcase", + "normpath", + "realpath", + "relpath", + ) + } + ) + ) + + def __init__( + self, + config, + environ=os.environ, + argv=sys.argv, + stdout=sys.stdout, + stderr=sys.stderr, + logger=None, + ): + dict.__setitem__(self, "__builtins__", self.BUILTINS) + + self._environ = dict(environ) + + self._paths = [] + self._all_paths = set() + self._templates = set() + # Associate SandboxDependsFunctions to DependsFunctions. + self._depends = OrderedDict() + self._seen = set() + # Store the @imports added to a given function. + self._imports = {} + + self._options = OrderedDict() + # Store raw option (as per command line or environment) for each Option + self._raw_options = OrderedDict() + + # Store options added with `imply_option`, and the reason they were + # added (which can either have been given to `imply_option`, or + # inferred. Their order matters, so use a list. + self._implied_options = [] + + # Store all results from _prepare_function + self._prepared_functions = set() + + # Queue of functions to execute, with their arguments + self._execution_queue = [] + + # Store the `when`s associated to some options. + self._conditions = {} + + # A list of conditions to apply as a default `when` for every *_impl() + self._default_conditions = [] + + self._helper = CommandLineHelper(environ, argv) + + assert isinstance(config, dict) + self._config = config + + # Tracks how many templates "deep" we are in the stack. + self._template_depth = 0 + + logging.addLevelName(TRACE, "TRACE") + if logger is None: + logger = moz_logger = logging.getLogger("moz.configure") + logger.setLevel(logging.DEBUG) + formatter = logging.Formatter("%(levelname)s: %(message)s") + handler = ConfigureOutputHandler(stdout, stderr) + handler.setFormatter(formatter) + queue_debug = handler.queue_debug + logger.addHandler(handler) + + else: + assert isinstance(logger, logging.Logger) + moz_logger = None + + @contextmanager + def queue_debug(): + yield + + self._logger = logger + + # Some callers will manage to log a bytestring with characters in it + # that can't be converted to ascii. Make our log methods robust to this + # by detecting the encoding that a producer is likely to have used. + encoding = getpreferredencoding() + + def wrapped_log_method(logger, key): + method = getattr(logger, key) + + def wrapped(*args, **kwargs): + out_args = [ + six.ensure_text(arg, encoding=encoding or "utf-8") + if isinstance(arg, six.binary_type) + else arg + for arg in args + ] + return method(*out_args, **kwargs) + + return wrapped + + log_namespace = { + k: wrapped_log_method(logger, k) + for k in ("debug", "info", "warning", "error") + } + log_namespace["queue_debug"] = queue_debug + self.log_impl = ReadOnlyNamespace(**log_namespace) + + self._help = None + self._help_option = self.option_impl( + "--help", help="print this message", category=HELP_OPTIONS_CATEGORY + ) + self._seen.add(self._help_option) + + self._always = DependsFunction(self, lambda: True, []) + self._never = DependsFunction(self, lambda: False, []) + + if self._value_for(self._help_option): + self._help = HelpFormatter(argv[0]) + self._help.add(self._help_option) + elif moz_logger: + handler = logging.FileHandler( + "config.log", mode="w", delay=True, encoding="utf-8" + ) + handler.setFormatter(formatter) + logger.addHandler(handler) + + def include_file(self, path): + """Include one file in the sandbox. Users of this class probably want + to use `run` instead. + + Note: this will execute all template invocations, as well as @depends + functions that depend on '--help', but nothing else. + """ + + if self._paths: + path = mozpath.join(mozpath.dirname(self._paths[-1]), path) + path = mozpath.normpath(path) + if not mozpath.basedir(path, (mozpath.dirname(self._paths[0]),)): + raise ConfigureError( + "Cannot include `%s` because it is not in a subdirectory " + "of `%s`" % (path, mozpath.dirname(self._paths[0])) + ) + else: + path = mozpath.realpath(mozpath.abspath(path)) + if path in self._all_paths: + raise ConfigureError( + "Cannot include `%s` because it was included already." % path + ) + self._paths.append(path) + self._all_paths.add(path) + + with open(path, "rb") as fh: + source = fh.read() + + code = self.get_compiled_source(source, path) + exec(code, self) + + self._paths.pop(-1) + + @staticmethod + @memoize + def get_compiled_source(source, path): + return compile(source, path, "exec") + + def run(self, path=None): + """Executes the given file within the sandbox, as well as everything + pending from any other included file, and ensure the overall + consistency of the executed script(s).""" + if path: + self.include_file(path) + + for option in six.itervalues(self._options): + # All options must be referenced by some @depends function + if option not in self._seen: + raise ConfigureError( + "Option `%s` is not handled ; reference it with a @depends" + % option.option + ) + + self._value_for(option) + + # All implied options should exist. + for implied_option in self._implied_options: + value = self._resolve(implied_option.value) + if value is not None: + # There are two ways to end up here: either the implied option + # is unknown, or it's known but there was a dependency loop + # that prevented the implication from being applied. + option = self._options.get(implied_option.name) + if not option: + raise ConfigureError( + "`%s`, emitted from `%s` line %d, is unknown." + % ( + implied_option.option, + implied_option.caller[0], + implied_option.caller[1], + ) + ) + # If the option is known, check that the implied value doesn't + # conflict with what value was attributed to the option. + if implied_option.when and not self._value_for(implied_option.when): + continue + option_value = self._value_for_option(option) + if value != option_value: + reason = implied_option.reason + if isinstance(reason, Option): + reason = self._raw_options.get(reason) or reason.option + reason = reason.split("=", 1)[0] + value = OptionValue.from_(value) + raise InvalidOptionError( + "'%s' implied by '%s' conflicts with '%s' from the %s" + % ( + value.format(option.option), + reason, + option_value.format(option.option), + option_value.origin, + ) + ) + + # All options should have been removed (handled) by now. + for arg in self._helper: + without_value = arg.split("=", 1)[0] + msg = "Unknown option: %s" % without_value + if self._help: + self._logger.warning(msg) + else: + raise InvalidOptionError(msg) + + # Run the execution queue + for func, args in self._execution_queue: + func(*args) + + if self._help: + with LineIO(self.log_impl.info) as out: + self._help.usage(out) + + def __getitem__(self, key): + impl = "%s_impl" % key + func = getattr(self, impl, None) + if func: + return func + + return super(ConfigureSandbox, self).__getitem__(key) + + def __setitem__(self, key, value): + if ( + key in self.BUILTINS + or key == "__builtins__" + or hasattr(self, "%s_impl" % key) + ): + raise KeyError("Cannot reassign builtins") + + if inspect.isfunction(value) and value not in self._templates: + value = self._prepare_function(value) + + elif ( + not isinstance(value, SandboxDependsFunction) + and value not in self._templates + and not (inspect.isclass(value) and issubclass(value, Exception)) + ): + raise KeyError( + "Cannot assign `%s` because it is neither a " + "@depends nor a @template" % key + ) + + if isinstance(value, SandboxDependsFunction): + self._depends[value].name = key + + return super(ConfigureSandbox, self).__setitem__(key, value) + + def _resolve(self, arg): + if isinstance(arg, SandboxDependsFunction): + return self._value_for_depends(self._depends[arg]) + return arg + + def _value_for(self, obj): + if isinstance(obj, SandboxDependsFunction): + assert obj in self._depends + return self._value_for_depends(self._depends[obj]) + + elif isinstance(obj, DependsFunction): + return self._value_for_depends(obj) + + elif isinstance(obj, Option): + return self._value_for_option(obj) + + assert False + + @memoize + def _value_for_depends(self, obj): + value = obj.result() + self._logger.log(TRACE, "%r = %r", obj, value) + return value + + @memoize + def _value_for_option(self, option): + implied = {} + matching_implied_options = [ + o for o in self._implied_options if o.name in (option.name, option.env) + ] + # Update self._implied_options before going into the loop with the non-matching + # options. + self._implied_options = [ + o for o in self._implied_options if o.name not in (option.name, option.env) + ] + + for implied_option in matching_implied_options: + if implied_option.when and not self._value_for(implied_option.when): + continue + + value = self._resolve(implied_option.value) + + if value is not None: + value = OptionValue.from_(value) + opt = value.format(implied_option.option) + self._helper.add(opt, "implied") + implied[opt] = implied_option + + try: + value, option_string = self._helper.handle(option) + except ConflictingOptionError as e: + reason = implied[e.arg].reason + if isinstance(reason, Option): + reason = self._raw_options.get(reason) or reason.option + reason = reason.split("=", 1)[0] + raise InvalidOptionError( + "'%s' implied by '%s' conflicts with '%s' from the %s" + % (e.arg, reason, e.old_arg, e.old_origin) + ) + + if value.origin == "implied": + recursed_value = getattr(self, "__value_for_option").get((option,)) + if recursed_value is not None: + filename, line = implied[value.format(option.option)].caller + raise ConfigureError( + "'%s' appears somewhere in the direct or indirect dependencies when " + "resolving imply_option at %s:%d" % (option.option, filename, line) + ) + + if option_string: + self._raw_options[option] = option_string + + when = self._conditions.get(option) + # If `when` resolves to a false-ish value, we always return None. + # This makes option(..., when='--foo') equivalent to + # option(..., when=depends('--foo')(lambda x: x)). + if when and not self._value_for(when) and value is not None: + # If the option was passed explicitly, we throw an error that + # the option is not available. Except when the option was passed + # from the environment, because that would be too cumbersome. + if value.origin not in ("default", "environment"): + raise InvalidOptionError( + "%s is not available in this configuration" + % option_string.split("=", 1)[0] + ) + self._logger.log(TRACE, "%r = None", option) + return None + + self._logger.log(TRACE, "%r = %r", option, value) + return value + + def _dependency(self, arg, callee_name, arg_name=None): + if isinstance(arg, six.string_types): + prefix, name, values = Option.split_option(arg) + if values != (): + raise ConfigureError("Option must not contain an '='") + if name not in self._options: + raise ConfigureError( + "'%s' is not a known option. " "Maybe it's declared too late?" % arg + ) + arg = self._options[name] + self._seen.add(arg) + elif isinstance(arg, SandboxDependsFunction): + assert arg in self._depends + arg = self._depends[arg] + else: + raise TypeError( + "Cannot use object of type '%s' as %sargument to %s" + % ( + type(arg).__name__, + "`%s` " % arg_name if arg_name else "", + callee_name, + ) + ) + return arg + + def _normalize_when(self, when, callee_name): + if when is True: + when = self._always + elif when is False: + when = self._never + elif when is not None: + when = self._dependency(when, callee_name, "when") + + if self._default_conditions: + # Create a pseudo @depends function for the combination of all + # default conditions and `when`. + dependencies = [when] if when else [] + dependencies.extend(self._default_conditions) + if len(dependencies) == 1: + return dependencies[0] + return CombinedDependsFunction(self, all, dependencies) + return when + + @contextmanager + def only_when_impl(self, when): + """Implementation of only_when() + + `only_when` is a context manager that essentially makes calls to + other sandbox functions within the context block ignored. + """ + when = self._normalize_when(when, "only_when") + if when and self._default_conditions[-1:] != [when]: + self._default_conditions.append(when) + yield + self._default_conditions.pop() + else: + yield + + def option_impl(self, *args, **kwargs): + """Implementation of option() + This function creates and returns an Option() object, passing it the + resolved arguments (uses the result of functions when functions are + passed). In most cases, the result of this function is not expected to + be used. + Command line argument/environment variable parsing for this Option is + handled here. + """ + when = self._normalize_when(kwargs.get("when"), "option") + args = [self._resolve(arg) for arg in args] + kwargs = {k: self._resolve(v) for k, v in six.iteritems(kwargs) if k != "when"} + # The Option constructor needs to look up the stack to infer a category + # for the Option, since the category is based on the filename where the + # Option is defined. However, if the Option is defined in a template, we + # want the category to reference the caller of the template rather than + # the caller of the option() function. + kwargs["define_depth"] = self._template_depth * 3 + option = Option(*args, **kwargs) + if when: + self._conditions[option] = when + if option.name in self._options: + raise ConfigureError("Option `%s` already defined" % option.option) + if option.env in self._options: + raise ConfigureError("Option `%s` already defined" % option.env) + if option.name: + self._options[option.name] = option + if option.env: + self._options[option.env] = option + + if self._help and (when is None or self._value_for(when)): + self._help.add(option) + + return option + + def depends_impl(self, *args, **kwargs): + """Implementation of @depends() + This function is a decorator. It returns a function that subsequently + takes a function and returns a dummy function. The dummy function + identifies the actual function for the sandbox, while preventing + further function calls from within the sandbox. + + @depends() takes a variable number of option strings or dummy function + references. The decorated function is called as soon as the decorator + is called, and the arguments it receives are the OptionValue or + function results corresponding to each of the arguments to @depends. + As an exception, when a HelpFormatter is attached, only functions that + have '--help' in their @depends argument list are called. + + The decorated function is altered to use a different global namespace + for its execution. This different global namespace exposes a limited + set of functions from os.path. + """ + for k in kwargs: + if k != "when": + raise TypeError( + "depends_impl() got an unexpected keyword argument '%s'" % k + ) + + when = self._normalize_when(kwargs.get("when"), "@depends") + + if not when and not args: + raise ConfigureError("@depends needs at least one argument") + + dependencies = tuple(self._dependency(arg, "@depends") for arg in args) + + conditions = [ + self._conditions[d] + for d in dependencies + if d in self._conditions and isinstance(d, Option) + ] + for c in conditions: + if c != when: + raise ConfigureError( + "@depends function needs the same `when` " + "as options it depends on" + ) + + def decorator(func): + if inspect.isgeneratorfunction(func): + raise ConfigureError( + "Cannot decorate generator functions with @depends" + ) + if inspect.isroutine(func): + if func in self._templates: + raise TypeError("Cannot use a @template function here") + func = self._prepare_function(func) + elif isinstance(func, SandboxDependsFunction): + raise TypeError("Cannot nest @depends functions") + elif dependencies: + raise TypeError( + "Cannot wrap literal values in @depends with dependencies" + ) + depends = DependsFunction(self, func, dependencies, when=when) + return depends.sandboxed + + return decorator + + def include_impl(self, what, when=None): + """Implementation of include(). + Allows to include external files for execution in the sandbox. + It is possible to use a @depends function as argument, in which case + the result of the function is the file name to include. This latter + feature is only really meant for --enable-application/--enable-project. + """ + with self.only_when_impl(when): + what = self._resolve(what) + if what: + if not isinstance(what, six.string_types): + raise TypeError("Unexpected type: '%s'" % type(what).__name__) + self.include_file(what) + + def template_impl(self, func): + """Implementation of @template. + This function is a decorator. Template functions are called + immediately. They are altered so that their global namespace exposes + a limited set of functions from os.path, as well as `depends` and + `option`. + Templates allow to simplify repetitive constructs, or to implement + helper decorators and somesuch. + """ + + def update_globals(glob): + glob.update( + (k[: -len("_impl")], getattr(self, k)) + for k in dir(self) + if k.endswith("_impl") and k != "template_impl" + ) + glob.update((k, v) for k, v in six.iteritems(self) if k not in glob) + + template = self._prepare_function(func, update_globals) + + # Any function argument to the template must be prepared to be sandboxed. + # If the template itself returns a function (in which case, it's very + # likely a decorator), that function must be prepared to be sandboxed as + # well. + def wrap_template(template): + isfunction = inspect.isfunction + + def maybe_prepare_function(obj): + if isfunction(obj): + return self._prepare_function(obj) + return obj + + # The following function may end up being prepared to be sandboxed, + # so it mustn't depend on anything from the global scope in this + # file. It can however depend on variables from the closure, thus + # maybe_prepare_function and isfunction are declared above to be + # available there. + @self.wraps(template) + def wrapper(*args, **kwargs): + args = [maybe_prepare_function(arg) for arg in args] + kwargs = {k: maybe_prepare_function(v) for k, v in kwargs.items()} + self._template_depth += 1 + ret = template(*args, **kwargs) + self._template_depth -= 1 + if isfunction(ret): + # We can't expect the sandboxed code to think about all the + # details of implementing decorators, so do some of the + # work for them. If the function takes exactly one function + # as argument and returns a function, it must be a + # decorator, so mark the returned function as wrapping the + # function passed in. + if len(args) == 1 and not kwargs and isfunction(args[0]): + ret = self.wraps(args[0])(ret) + return wrap_template(ret) + return ret + + return wrapper + + wrapper = wrap_template(template) + self._templates.add(wrapper) + return wrapper + + def wraps(self, func): + return wraps(func) + + RE_MODULE = re.compile(r"^[a-zA-Z0-9_.]+$") + + def imports_impl(self, _import, _from=None, _as=None): + """Implementation of @imports. + This decorator imports the given _import from the given _from module + optionally under a different _as name. + The options correspond to the various forms for the import builtin. + + @imports('sys') + @imports(_from='mozpack', _import='path', _as='mozpath') + """ + for value, required in ((_import, True), (_from, False), (_as, False)): + if not isinstance(value, six.string_types) and ( + required or value is not None + ): + raise TypeError("Unexpected type: '%s'" % type(value).__name__) + if value is not None and not self.RE_MODULE.match(value): + raise ValueError("Invalid argument to @imports: '%s'" % value) + if _as and "." in _as: + raise ValueError("Invalid argument to @imports: '%s'" % _as) + + def decorator(func): + if func in self._templates: + raise ConfigureError("@imports must appear after @template") + if func in self._depends: + raise ConfigureError("@imports must appear after @depends") + # For the imports to apply in the order they appear in the + # .configure file, we accumulate them in reverse order and apply + # them later. + imports = self._imports.setdefault(func, []) + imports.insert(0, (_from, _import, _as)) + return func + + return decorator + + def _apply_imports(self, func, glob): + for _from, _import, _as in self._imports.pop(func, ()): + self._get_one_import(_from, _import, _as, glob) + + def _handle_wrapped_import(self, _from, _import, _as, glob): + """Given the name of a module, "import" a mocked package into the glob + iff the module is one that we wrap (either for the sandbox or for the + purpose of testing). Applies if the wrapped module is exposed by an + attribute of `self`. + + For example, if the import statement is `from os import environ`, then + this function will set + glob['environ'] = self._wrapped_os.environ. + + Iff this function handles the given import, return True. + """ + module = (_from or _import).split(".")[0] + attr = "_wrapped_" + module + wrapped = getattr(self, attr, None) + if wrapped: + if _as or _from: + obj = self._recursively_get_property( + module, (_from + "." if _from else "") + _import, wrapped + ) + glob[_as or _import] = obj + else: + glob[module] = wrapped + return True + else: + return False + + def _recursively_get_property(self, module, what, wrapped): + """Traverse the wrapper object `wrapped` (which represents the module + `module`) and return the property represented by `what`, which may be a + series of nested attributes. + + For example, if `module` is 'os' and `what` is 'os.path.join', + return `wrapped.path.join`. + """ + if what == module: + return wrapped + assert what.startswith(module + ".") + attrs = what[len(module + ".") :].split(".") + for attr in attrs: + wrapped = getattr(wrapped, attr) + return wrapped + + @memoized_property + def _wrapped_os(self): + wrapped_os = {} + exec("from os import *", {}, wrapped_os) + # Special case os and os.environ so that os.environ is our copy of + # the environment. + wrapped_os["environ"] = self._environ + # Also override some os.path functions with ours. + wrapped_path = {} + exec("from os.path import *", {}, wrapped_path) + wrapped_path.update(self.OS.path.__dict__) + wrapped_os["path"] = ReadOnlyNamespace(**wrapped_path) + return ReadOnlyNamespace(**wrapped_os) + + @memoized_property + def _wrapped_subprocess(self): + wrapped_subprocess = {} + exec("from subprocess import *", {}, wrapped_subprocess) + + def wrap(function): + def wrapper(*args, **kwargs): + if kwargs.get("env") is None and self._environ: + kwargs["env"] = dict(self._environ) + + return function(*args, **kwargs) + + return wrapper + + for f in ("call", "check_call", "check_output", "Popen", "run"): + # `run` is new to python 3.5. In case this still runs from python2 + # code, avoid failing here. + if f in wrapped_subprocess: + wrapped_subprocess[f] = wrap(wrapped_subprocess[f]) + + return ReadOnlyNamespace(**wrapped_subprocess) + + @memoized_property + def _wrapped_six(self): + if six.PY3: + return six + wrapped_six = {} + exec("from six import *", {}, wrapped_six) + wrapped_six_moves = {} + exec("from six.moves import *", {}, wrapped_six_moves) + wrapped_six_moves_builtins = {} + exec("from six.moves.builtins import *", {}, wrapped_six_moves_builtins) + + # Special case for the open() builtin, because otherwise, using it + # fails with "IOError: file() constructor not accessible in + # restricted mode". We also make open() look more like python 3's, + # decoding to unicode strings unless the mode says otherwise. + def wrapped_open(name, mode=None, buffering=None): + args = (name,) + kwargs = {} + if buffering is not None: + kwargs["buffering"] = buffering + if mode is not None: + args += (mode,) + if "b" in mode: + return open(*args, **kwargs) + kwargs["encoding"] = system_encoding + return codecs.open(*args, **kwargs) + + wrapped_six_moves_builtins["open"] = wrapped_open + wrapped_six_moves["builtins"] = ReadOnlyNamespace(**wrapped_six_moves_builtins) + wrapped_six["moves"] = ReadOnlyNamespace(**wrapped_six_moves) + + return ReadOnlyNamespace(**wrapped_six) + + def _get_one_import(self, _from, _import, _as, glob): + """Perform the given import, placing the result into the dict glob.""" + if not _from and _import == "__builtin__": + raise Exception("Importing __builtin__ is forbidden") + if _from == "__builtin__": + _from = "six.moves.builtins" + # The special `__sandbox__` module gives access to the sandbox + # instance. + if not _from and _import == "__sandbox__": + glob[_as or _import] = self + return + if self._handle_wrapped_import(_from, _import, _as, glob): + return + # If we've gotten this far, we should just do a normal import. + # Until this proves to be a performance problem, just construct an + # import statement and execute it. + import_line = "%simport %s%s" % ( + ("from %s " % _from) if _from else "", + _import, + (" as %s" % _as) if _as else "", + ) + exec(import_line, {}, glob) + + def _resolve_and_set(self, data, name, value, when=None): + # Don't set anything when --help was on the command line + if self._help: + return + if when and not self._value_for(when): + return + name = self._resolve(name) + if name is None: + return + if not isinstance(name, six.string_types): + raise TypeError("Unexpected type: '%s'" % type(name).__name__) + if name in data: + raise ConfigureError( + "Cannot add '%s' to configuration: Key already " "exists" % name + ) + value = self._resolve(value) + if value is not None: + if self._logger.isEnabledFor(TRACE): + if data is self._config: + self._logger.log(TRACE, "set_config(%s, %r)", name, value) + elif data is self._config.get("DEFINES"): + self._logger.log(TRACE, "set_define(%s, %r)", name, value) + data[name] = value + + def set_config_impl(self, name, value, when=None): + """Implementation of set_config(). + Set the configuration items with the given name to the given value. + Both `name` and `value` can be references to @depends functions, + in which case the result from these functions is used. If the result + of either function is None, the configuration item is not set. + """ + when = self._normalize_when(when, "set_config") + + self._execution_queue.append( + (self._resolve_and_set, (self._config, name, value, when)) + ) + + def set_define_impl(self, name, value, when=None): + """Implementation of set_define(). + Set the define with the given name to the given value. Both `name` and + `value` can be references to @depends functions, in which case the + result from these functions is used. If the result of either function + is None, the define is not set. If the result is False, the define is + explicitly undefined (-U). + """ + when = self._normalize_when(when, "set_define") + + defines = self._config.setdefault("DEFINES", {}) + self._execution_queue.append( + (self._resolve_and_set, (defines, name, value, when)) + ) + + def imply_option_impl(self, option, value, reason=None, when=None): + """Implementation of imply_option(). + Injects additional options as if they had been passed on the command + line. The `option` argument is a string as in option()'s `name` or + `env`. The option must be declared after `imply_option` references it. + The `value` argument indicates the value to pass to the option. + It can be: + - True. In this case `imply_option` injects the positive option + + (--enable-foo/--with-foo). + imply_option('--enable-foo', True) + imply_option('--disable-foo', True) + + are both equivalent to `--enable-foo` on the command line. + + - False. In this case `imply_option` injects the negative option + + (--disable-foo/--without-foo). + imply_option('--enable-foo', False) + imply_option('--disable-foo', False) + + are both equivalent to `--disable-foo` on the command line. + + - None. In this case `imply_option` does nothing. + imply_option('--enable-foo', None) + imply_option('--disable-foo', None) + + are both equivalent to not passing any flag on the command line. + + - a string or a tuple. In this case `imply_option` injects the positive + option with the given value(s). + + imply_option('--enable-foo', 'a') + imply_option('--disable-foo', 'a') + + are both equivalent to `--enable-foo=a` on the command line. + imply_option('--enable-foo', ('a', 'b')) + imply_option('--disable-foo', ('a', 'b')) + + are both equivalent to `--enable-foo=a,b` on the command line. + + Because imply_option('--disable-foo', ...) can be misleading, it is + recommended to use the positive form ('--enable' or '--with') for + `option`. + + The `value` argument can also be (and usually is) a reference to a + @depends function, in which case the result of that function will be + used as per the descripted mapping above. + + The `reason` argument indicates what caused the option to be implied. + It is necessary when it cannot be inferred from the `value`. + """ + + when = self._normalize_when(when, "imply_option") + + # Don't do anything when --help was on the command line + if self._help: + return + if not reason and isinstance(value, SandboxDependsFunction): + deps = self._depends[value].dependencies + possible_reasons = [d for d in deps if d != self._help_option] + if len(possible_reasons) == 1: + if isinstance(possible_reasons[0], Option): + reason = possible_reasons[0] + frame = inspect.currentframe() + line = frame.f_back.f_lineno + filename = frame.f_back.f_code.co_filename + if not reason and ( + isinstance(value, (bool, tuple)) or isinstance(value, six.string_types) + ): + # A reason can be provided automatically when imply_option + # is called with an immediate value. + reason = "imply_option at %s:%s" % (filename, line) + + if not reason: + raise ConfigureError( + "Cannot infer what implies '%s'. Please add a `reason` to " + "the `imply_option` call." % option + ) + + prefix, name, values = Option.split_option(option) + if values != (): + raise ConfigureError("Implied option must not contain an '='") + + self._implied_options.append( + ReadOnlyNamespace( + option=option, + prefix=prefix, + name=name, + value=value, + caller=(filename, line), + reason=reason, + when=when, + ) + ) + + def _prepare_function(self, func, update_globals=None): + """Alter the given function global namespace with the common ground + for @depends, and @template. + """ + if not inspect.isfunction(func): + raise TypeError("Unexpected type: '%s'" % type(func).__name__) + if func in self._prepared_functions: + return func + + glob = SandboxedGlobal( + (k, v) + for k, v in six.iteritems(func.__globals__) + if (isinstance(v, types.FunctionType) and v not in self._templates) + or (isinstance(v, type) and issubclass(v, Exception)) + ) + glob.update( + __builtins__=self.BUILTINS, + __file__=self._paths[-1] if self._paths else "", + __name__=self._paths[-1] if self._paths else "", + os=self.OS, + log=self.log_impl, + namespace=ReadOnlyNamespace, + ) + if update_globals: + update_globals(glob) + + # The execution model in the sandbox doesn't guarantee the execution + # order will always be the same for a given function, and if it uses + # variables from a closure that are changed after the function is + # declared, depending when the function is executed, the value of the + # variable can differ. For consistency, we force the function to use + # the value from the earliest it can be run, which is at declaration. + # Note this is not entirely bullet proof (if the value is e.g. a list, + # the list contents could have changed), but covers the bases. + closure = None + if func.__closure__: + + def makecell(content): + def f(): + content + + return f.__closure__[0] + + closure = tuple(makecell(cell.cell_contents) for cell in func.__closure__) + + new_func = self.wraps(func)( + types.FunctionType( + func.__code__, glob, func.__name__, func.__defaults__, closure + ) + ) + + @self.wraps(new_func) + def wrapped(*args, **kwargs): + if func in self._imports: + self._apply_imports(func, glob) + return new_func(*args, **kwargs) + + self._prepared_functions.add(wrapped) + return wrapped diff --git a/python/mozbuild/mozbuild/configure/check_debug_ranges.py b/python/mozbuild/mozbuild/configure/check_debug_ranges.py new file mode 100644 index 0000000000..f82624c14f --- /dev/null +++ b/python/mozbuild/mozbuild/configure/check_debug_ranges.py @@ -0,0 +1,68 @@ +# 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/. + +# This script returns the number of items for the DW_AT_ranges corresponding +# to a given compilation unit. This is used as a helper to find a bug in some +# versions of GNU ld. + +import re +import subprocess +import sys + + +def get_range_for(compilation_unit, debug_info): + """Returns the range offset for a given compilation unit + in a given debug_info.""" + name = ranges = "" + search_cu = False + for nfo in debug_info.splitlines(): + if "DW_TAG_compile_unit" in nfo: + search_cu = True + elif "DW_TAG_" in nfo or not nfo.strip(): + if name == compilation_unit and ranges != "": + return int(ranges, 16) + name = ranges = "" + search_cu = False + if search_cu: + if "DW_AT_name" in nfo: + name = nfo.rsplit(None, 1)[1] + elif "DW_AT_ranges" in nfo: + ranges = nfo.rsplit(None, 1)[1] + return None + + +def get_range_length(range, debug_ranges): + """Returns the number of items in the range starting at the + given offset.""" + length = 0 + for line in debug_ranges.splitlines(): + m = re.match("\s*([0-9a-fA-F]+)\s+([0-9a-fA-F]+)\s+([0-9a-fA-F]+)", line) + if m and int(m.group(1), 16) == range: + length += 1 + return length + + +def main(bin, compilation_unit): + p = subprocess.Popen( + ["objdump", "-W", bin], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + universal_newlines=True, + ) + (out, err) = p.communicate() + sections = re.split("\n(Contents of the|The section) ", out) + debug_info = [s for s in sections if s.startswith(".debug_info")] + debug_ranges = [s for s in sections if s.startswith(".debug_ranges")] + if not debug_ranges or not debug_info: + return 0 + + range = get_range_for(compilation_unit, debug_info[0]) + if range is not None: + return get_range_length(range, debug_ranges[0]) + + return -1 + + +if __name__ == "__main__": + print(main(*sys.argv[1:])) diff --git a/python/mozbuild/mozbuild/configure/constants.py b/python/mozbuild/mozbuild/configure/constants.py new file mode 100644 index 0000000000..d69d9c08ef --- /dev/null +++ b/python/mozbuild/mozbuild/configure/constants.py @@ -0,0 +1,161 @@ +# 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 collections import OrderedDict + +from mozbuild.util import EnumString + + +class RaiseErrorOnUse(str): + def __init__(self, msg): + self.msg = msg + + def __eq__(self, other): + raise RuntimeError(self.msg) + + def __ne__(self, other): + self.__eq__(other) + + def __repr__(self): + return f"{self.__class__.__name__}({self.msg!r})" + + +class CompilerType(EnumString): + POSSIBLE_VALUES = ( + "clang", + "clang-cl", + "gcc", + "msvc", + ) + + +class OS(EnumString): + POSSIBLE_VALUES = ( + "Android", + "DragonFly", + "FreeBSD", + "GNU", + "NetBSD", + "OpenBSD", + "OSX", + "SunOS", + "WINNT", + "WASI", + ) + + +class Kernel(EnumString): + POSSIBLE_VALUES = ( + "Darwin", + "DragonFly", + "FreeBSD", + "kFreeBSD", + "Linux", + "NetBSD", + "OpenBSD", + "SunOS", + "WINNT", + "WASI", + ) + + +CPU_bitness = { + "aarch64": 64, + "Alpha": 64, + "arm": 32, + "hppa": 32, + "ia64": 64, + "loongarch64": 64, + "m68k": 32, + "mips32": 32, + "mips64": 64, + "ppc": 32, + "ppc64": 64, + "riscv64": 64, + "s390": 32, + "s390x": 64, + "sh4": 32, + "sparc": 32, + "sparc64": 64, + "x86": 32, + "x86_64": 64, + "wasm32": 32, +} + + +class CPU(EnumString): + POSSIBLE_VALUES = CPU_bitness.keys() + + +class Endianness(EnumString): + POSSIBLE_VALUES = ( + "big", + "little", + ) + + +class WindowsBinaryType(EnumString): + POSSIBLE_VALUES = ( + "win32", + "win64", + ) + + +class Abi(EnumString): + POSSIBLE_VALUES = ( + "msvc", + "mingw", + ) + + +# The order of those checks matter +CPU_preprocessor_checks = OrderedDict( + ( + ("x86", "__i386__ || _M_IX86"), + ("x86_64", "__x86_64__ || _M_X64"), + ("arm", "__arm__ || _M_ARM"), + ("aarch64", "__aarch64__ || _M_ARM64"), + ("ia64", "__ia64__"), + ("s390x", "__s390x__"), + ("s390", "__s390__"), + ("ppc64", "__powerpc64__"), + ("ppc", "__powerpc__"), + ("Alpha", "__alpha__"), + ("hppa", "__hppa__"), + ("sparc64", "__sparc__ && __arch64__"), + ("sparc", "__sparc__"), + ("m68k", "__m68k__"), + ("mips64", "__mips64"), + ("mips32", "__mips__"), + ("riscv64", "__riscv && __riscv_xlen == 64"), + ("loongarch64", "__loongarch64"), + ("sh4", "__sh__"), + ("wasm32", "__wasm32__"), + ) +) + +assert sorted(CPU_preprocessor_checks.keys()) == sorted(CPU.POSSIBLE_VALUES) + +kernel_preprocessor_checks = { + "Darwin": "__APPLE__", + "DragonFly": "__DragonFly__", + "FreeBSD": "__FreeBSD__", + "kFreeBSD": "__FreeBSD_kernel__", + "Linux": "__linux__", + "NetBSD": "__NetBSD__", + "OpenBSD": "__OpenBSD__", + "SunOS": "__sun__", + "WINNT": "_WIN32 || __CYGWIN__", + "WASI": "__wasi__", +} + +assert sorted(kernel_preprocessor_checks.keys()) == sorted(Kernel.POSSIBLE_VALUES) + +OS_preprocessor_checks = { + "Android": "__ANDROID__", +} + +# We intentionally don't include all possible OSes in our checks, because we +# only care about OS mismatches for specific target OSes. +# assert sorted(OS_preprocessor_checks.keys()) == sorted(OS.POSSIBLE_VALUES) diff --git a/python/mozbuild/mozbuild/configure/help.py b/python/mozbuild/mozbuild/configure/help.py new file mode 100644 index 0000000000..132e61deb9 --- /dev/null +++ b/python/mozbuild/mozbuild/configure/help.py @@ -0,0 +1,121 @@ +# 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/. + +import os +import re +from collections import defaultdict + +from mozbuild.configure.options import Option + + +class HelpFormatter(object): + def __init__(self, argv0): + self.intro = ["Usage: %s [options]" % os.path.basename(argv0)] + self.options = [] + + def add(self, option): + assert isinstance(option, Option) + if option.possible_origins == ("implied",): + # Don't display help if our option can only be implied. + return + if ( + option.default + and len(option.default) == 0 + and option.choices + and option.nargs in ("?", "*") + ): + # Uncommon case where the option defaults to an enabled value, + # but can take values. The help should mention both the disabling + # flag and the enabling flag that takes values. + # Because format_options_by_category does not handle the original + # Option very well, we create two fresh ones for what should appear + # in the help. + option_1 = Option( + option.option, + default=False, + choices=option.choices, + help=option.help, + define_depth=1, + ) + option_2 = Option( + option.option, default=True, help=option.help, define_depth=1 + ) + self.options.append(option_1) + self.options.append(option_2) + else: + self.options.append(option) + + def format_options_by_category(self, options_by_category): + ret = [] + for category, options in options_by_category.items(): + ret.append(" " + category + ":") + for option in options: + opt = option.option + if option.choices: + opt += "={%s}" % ",".join(option.choices) + help = self.format_help(option) + if len(option.default): + if help: + help += " " + help += "[%s]" % ",".join(option.default) + + if len(opt) > 24 or not help: + ret.append(" %s" % opt) + if help: + ret.append("%s%s" % (" " * 30, help)) + else: + ret.append(" %-24s %s" % (opt, help)) + ret.append("") + return ret + + RE_FORMAT = re.compile(r"{([^|}]*)\|([^|}]*)}") + + # Return formatted help text for --{enable,disable,with,without}-* options. + # + # Format is the following syntax: + # {String for --enable or --with|String for --disable or --without} + # + # For example, '{Enable|Disable} optimizations' will be formatted to + # 'Enable optimizations' if the options's prefix is 'enable' or 'with', + # and formatted to 'Disable optimizations' if the options's prefix is + # 'disable' or 'without'. + def format_help(self, option): + if not option.help: + return "" + + if option.prefix in ("enable", "with"): + replacement = r"\1" + elif option.prefix in ("disable", "without"): + replacement = r"\2" + else: + return option.help + + return self.RE_FORMAT.sub(replacement, option.help) + + def usage(self, out): + options_by_category = defaultdict(list) + env_by_category = defaultdict(list) + for option in self.options: + target = options_by_category if option.name else env_by_category + target[option.category].append(option) + if options_by_category: + options_formatted = [ + "Options: [defaults in brackets after descriptions]" + ] + self.format_options_by_category(options_by_category) + else: + options_formatted = [] + if env_by_category: + env_formatted = [ + "Environment variables:" + ] + self.format_options_by_category(env_by_category) + else: + env_formatted = [] + print( + "\n\n".join( + "\n".join(t) + for t in (self.intro, options_formatted, env_formatted) + if t + ), + file=out, + ) diff --git a/python/mozbuild/mozbuild/configure/lint.py b/python/mozbuild/mozbuild/configure/lint.py new file mode 100644 index 0000000000..c6bda9a540 --- /dev/null +++ b/python/mozbuild/mozbuild/configure/lint.py @@ -0,0 +1,357 @@ +# 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/. + +import inspect +import re +import sys +import types +from dis import Bytecode +from functools import wraps +from io import StringIO + +from mozbuild.util import memoize + +from . import ( + CombinedDependsFunction, + ConfigureError, + ConfigureSandbox, + DependsFunction, + SandboxDependsFunction, + SandboxedGlobal, + TrivialDependsFunction, +) +from .help import HelpFormatter + + +def code_replace(code, co_filename, co_name, co_firstlineno): + if sys.version_info < (3, 8): + codetype_args = [ + code.co_argcount, + code.co_kwonlyargcount, + code.co_nlocals, + code.co_stacksize, + code.co_flags, + code.co_code, + code.co_consts, + code.co_names, + code.co_varnames, + co_filename, + co_name, + co_firstlineno, + code.co_lnotab, + ] + return types.CodeType(*codetype_args) + else: + return code.replace( + co_filename=co_filename, co_name=co_name, co_firstlineno=co_firstlineno + ) + + +class LintSandbox(ConfigureSandbox): + def __init__(self, environ=None, argv=None, stdout=None, stderr=None): + out = StringIO() + stdout = stdout or out + stderr = stderr or out + environ = environ or {} + argv = argv or [] + self._wrapped = {} + self._has_imports = set() + self._bool_options = [] + self._bool_func_options = [] + self.LOG = "" + super(LintSandbox, self).__init__( + {}, environ=environ, argv=argv, stdout=stdout, stderr=stderr + ) + + def run(self, path=None): + if path: + self.include_file(path) + + for dep in self._depends.values(): + self._check_dependencies(dep) + + def _raise_from(self, exception, obj, line=0): + """ + Raises the given exception as if it were emitted from the given + location. + + The location is determined from the values of obj and line. + - `obj` can be a function or DependsFunction, in which case + `line` corresponds to the line within the function the exception + will be raised from (as an offset from the function's firstlineno). + - `obj` can be a stack frame, in which case `line` is ignored. + """ + + def thrower(e): + raise e + + if isinstance(obj, DependsFunction): + obj, _ = self.unwrap(obj._func) + + if inspect.isfunction(obj): + funcname = obj.__name__ + filename = obj.__code__.co_filename + firstline = obj.__code__.co_firstlineno + line += firstline - 1 + elif inspect.isframe(obj): + funcname = obj.f_code.co_name + filename = obj.f_code.co_filename + firstline = obj.f_code.co_firstlineno + line = obj.f_lineno - 1 + else: + # Don't know how to handle the given location, still raise the + # exception. + raise exception + + # Create a new function from the above thrower that pretends + # the `raise` line is on the line given as argument. + + code = code_replace( + thrower.__code__, + co_filename=filename, + co_name=funcname, + co_firstlineno=line, + ) + + thrower = types.FunctionType( + code, + thrower.__globals__, + funcname, + thrower.__defaults__, + thrower.__closure__, + ) + thrower(exception) + + def _check_dependencies(self, obj): + if isinstance(obj, CombinedDependsFunction) or obj in ( + self._always, + self._never, + ): + return + if not inspect.isroutine(obj._func): + return + func, glob = self.unwrap(obj._func) + func_args = inspect.getfullargspec(func) + if func_args.varkw: + e = ConfigureError( + "Keyword arguments are not allowed in @depends functions" + ) + self._raise_from(e, func) + + all_args = list(func_args.args) + if func_args.varargs: + all_args.append(func_args.varargs) + used_args = set() + + for instr in Bytecode(func): + if instr.opname in ("LOAD_FAST", "LOAD_CLOSURE"): + if instr.argval in all_args: + used_args.add(instr.argval) + + for num, arg in enumerate(all_args): + if arg not in used_args: + dep = obj.dependencies[num] + if dep != self._help_option or not self._need_help_dependency(obj): + if isinstance(dep, DependsFunction): + dep = dep.name + else: + dep = dep.option + e = ConfigureError("The dependency on `%s` is unused" % dep) + self._raise_from(e, func) + + def _need_help_dependency(self, obj): + if isinstance(obj, (CombinedDependsFunction, TrivialDependsFunction)): + return False + if isinstance(obj, DependsFunction): + if obj in (self._always, self._never) or not inspect.isroutine(obj._func): + return False + func, glob = self.unwrap(obj._func) + # We allow missing --help dependencies for functions that: + # - don't use @imports + # - don't have a closure + # - don't use global variables + if func in self._has_imports or func.__closure__: + return True + for instr in Bytecode(func): + if instr.opname in ("LOAD_GLOBAL", "STORE_GLOBAL"): + # There is a fake os module when one is not imported, + # and it's allowed for functions without a --help + # dependency. + if instr.argval == "os" and glob.get("os") is self.OS: + continue + if instr.argval in self.BUILTINS: + continue + if instr.argval in "namespace": + continue + return True + return False + + def _missing_help_dependency(self, obj): + if isinstance(obj, DependsFunction) and self._help_option in obj.dependencies: + return False + return self._need_help_dependency(obj) + + @memoize + def _value_for_depends(self, obj): + with_help = self._help_option in obj.dependencies + if with_help: + for arg in obj.dependencies: + if self._missing_help_dependency(arg): + e = ConfigureError( + "Missing '--help' dependency because `%s` depends on " + "'--help' and `%s`" % (obj.name, arg.name) + ) + self._raise_from(e, arg) + elif self._missing_help_dependency(obj): + e = ConfigureError("Missing '--help' dependency") + self._raise_from(e, obj) + return super(LintSandbox, self)._value_for_depends(obj) + + def option_impl(self, *args, **kwargs): + result = super(LintSandbox, self).option_impl(*args, **kwargs) + when = self._conditions.get(result) + if when: + self._value_for(when) + + self._check_option(result, *args, **kwargs) + + return result + + def _check_option(self, option, *args, **kwargs): + if len(args) == 0: + return + + self._check_prefix_for_bool_option(*args, **kwargs) + self._check_help_for_option(option, *args, **kwargs) + + def _check_prefix_for_bool_option(self, *args, **kwargs): + name = args[0] + default = kwargs.get("default") + + if type(default) != bool: + return + + table = { + True: { + "enable": "disable", + "with": "without", + }, + False: { + "disable": "enable", + "without": "with", + }, + } + for prefix, replacement in table[default].items(): + if name.startswith("--{}-".format(prefix)): + frame = inspect.currentframe() + while frame and frame.f_code.co_name != self.option_impl.__name__: + frame = frame.f_back + e = ConfigureError( + "{} should be used instead of " + "{} with default={}".format( + name.replace( + "--{}-".format(prefix), "--{}-".format(replacement) + ), + name, + default, + ) + ) + self._raise_from(e, frame.f_back if frame else None) + + def _check_help_for_option(self, option, *args, **kwargs): + if not option.prefix: + return + + check = None + + default = kwargs.get("default") + if isinstance(default, SandboxDependsFunction): + default = self._resolve(default) + if type(default) is not str: + check = "of non-constant default" + + if ( + option.default + and len(option.default) == 0 + and option.choices + and option.nargs in ("?", "*") + ): + check = "it can be both disabled and enabled with an optional value" + + if not check: + return + + help = kwargs["help"] + match = re.search(HelpFormatter.RE_FORMAT, help) + if match: + return + + if option.prefix in ("enable", "disable"): + rule = "{Enable|Disable}" + else: + rule = "{With|Without}" + + frame = inspect.currentframe() + while frame and frame.f_code.co_name != self.option_impl.__name__: + frame = frame.f_back + e = ConfigureError('`help` should contain "{}" because {}'.format(rule, check)) + self._raise_from(e, frame.f_back if frame else None) + + def unwrap(self, func): + glob = func.__globals__ + while func in self._wrapped: + if isinstance(func.__globals__, SandboxedGlobal): + glob = func.__globals__ + func = self._wrapped[func] + return func, glob + + def wraps(self, func): + def do_wraps(wrapper): + self._wrapped[wrapper] = func + return wraps(func)(wrapper) + + return do_wraps + + def imports_impl(self, _import, _from=None, _as=None): + wrapper = super(LintSandbox, self).imports_impl(_import, _from=_from, _as=_as) + + def decorator(func): + self._has_imports.add(func) + return wrapper(func) + + return decorator + + def _prepare_function(self, func, update_globals=None): + wrapped = super(LintSandbox, self)._prepare_function(func, update_globals) + _, glob = self.unwrap(wrapped) + imports = set() + for _from, _import, _as in self._imports.get(func, ()): + if _as: + imports.add(_as) + else: + what = _import.split(".")[0] + imports.add(what) + if _from == "__builtin__" and _import in glob["__builtins__"]: + e = NameError( + "builtin '{}' doesn't need to be imported".format(_import) + ) + self._raise_from(e, func) + for instr in Bytecode(func): + code = func.__code__ + if ( + instr.opname == "LOAD_GLOBAL" + and instr.argval not in glob + and instr.argval not in imports + and instr.argval not in glob["__builtins__"] + and instr.argval not in code.co_varnames[: code.co_argcount] + ): + # Raise the same kind of error as what would happen during + # execution. + e = NameError("global name '{}' is not defined".format(instr.argval)) + if instr.starts_line is None: + self._raise_from(e, func) + else: + self._raise_from(e, func, instr.starts_line - code.co_firstlineno) + + return wrapped diff --git a/python/mozbuild/mozbuild/configure/options.py b/python/mozbuild/mozbuild/configure/options.py new file mode 100644 index 0000000000..874f4cad74 --- /dev/null +++ b/python/mozbuild/mozbuild/configure/options.py @@ -0,0 +1,617 @@ +# 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/. + +import inspect +import os +import sys +from collections import OrderedDict + +import six + +HELP_OPTIONS_CATEGORY = "Help options" +# List of whitelisted option categories. If you want to add a new category, +# simply add it to this list; however, exercise discretion as +# "./configure --help" becomes less useful if there are an excessive number of +# categories. +_ALL_CATEGORIES = (HELP_OPTIONS_CATEGORY,) + + +def _infer_option_category(define_depth): + stack_frame = inspect.currentframe() + for _ in range(3 + define_depth): + stack_frame = stack_frame.f_back + try: + path = os.path.relpath(stack_frame.f_code.co_filename) + except ValueError: + # If this call fails, it means the relative path couldn't be determined + # (e.g. because this file is on a different drive than the cwd on a + # Windows machine). That's fine, just use the absolute filename. + path = stack_frame.f_code.co_filename + return "Options from " + path + + +def istupleofstrings(obj): + return ( + isinstance(obj, tuple) + and len(obj) + and all(isinstance(o, six.string_types) for o in obj) + ) + + +class OptionValue(tuple): + """Represents the value of a configure option. + + This class is not meant to be used directly. Use its subclasses instead. + + The `origin` attribute holds where the option comes from (e.g. environment, + command line, or default) + """ + + def __new__(cls, values=(), origin="unknown"): + return super(OptionValue, cls).__new__(cls, values) + + def __init__(self, values=(), origin="unknown"): + self.origin = origin + + def format(self, option): + if option.startswith("--"): + prefix, name, values = Option.split_option(option) + assert values == () + for prefix_set in ( + ("disable", "enable"), + ("without", "with"), + ): + if prefix in prefix_set: + prefix = prefix_set[int(bool(self))] + break + if prefix: + option = "--%s-%s" % (prefix, name) + elif self: + option = "--%s" % name + else: + return "" + if len(self): + return "%s=%s" % (option, ",".join(self)) + return option + elif self and not len(self): + return "%s=1" % option + return "%s=%s" % (option, ",".join(self)) + + def __eq__(self, other): + # This is to catch naive comparisons against strings and other + # types in moz.configure files, as it is really easy to write + # value == 'foo'. We only raise a TypeError for instances that + # have content, because value-less instances (like PositiveOptionValue + # and NegativeOptionValue) are common and it is trivial to + # compare these. + if not isinstance(other, tuple) and len(self): + raise TypeError( + "cannot compare a populated %s against an %s; " + "OptionValue instances are tuples - did you mean to " + "compare against member elements using [x]?" + % (type(other).__name__, type(self).__name__) + ) + + # Allow explicit tuples to be compared. + if type(other) == tuple: + return tuple.__eq__(self, other) + elif isinstance(other, bool): + return bool(self) == other + # Else we're likely an OptionValue class. + elif type(other) != type(self): + return False + else: + return super(OptionValue, self).__eq__(other) + + def __ne__(self, other): + return not self.__eq__(other) + + def __repr__(self): + return "%s%s" % (self.__class__.__name__, super(OptionValue, self).__repr__()) + + @staticmethod + def from_(value): + if isinstance(value, OptionValue): + return value + elif value is True: + return PositiveOptionValue() + elif value is False or value == (): + return NegativeOptionValue() + elif isinstance(value, six.string_types): + return PositiveOptionValue((value,)) + elif isinstance(value, tuple): + return PositiveOptionValue(value) + else: + raise TypeError("Unexpected type: '%s'" % type(value).__name__) + + +class PositiveOptionValue(OptionValue): + """Represents the value for a positive option (--enable/--with/--foo) + in the form of a tuple for when values are given to the option (in the form + --option=value[,value2...]. + """ + + def __nonzero__(self): # py2 + return True + + def __bool__(self): # py3 + return True + + +class NegativeOptionValue(OptionValue): + """Represents the value for a negative option (--disable/--without) + + This is effectively an empty tuple with a `origin` attribute. + """ + + def __new__(cls, origin="unknown"): + return super(NegativeOptionValue, cls).__new__(cls, origin=origin) + + def __init__(self, origin="unknown"): + super(NegativeOptionValue, self).__init__(origin=origin) + + +class InvalidOptionError(Exception): + pass + + +class ConflictingOptionError(InvalidOptionError): + def __init__(self, message, **format_data): + if format_data: + message = message.format(**format_data) + super(ConflictingOptionError, self).__init__(message) + for k, v in six.iteritems(format_data): + setattr(self, k, v) + + +class Option(object): + """Represents a configure option + + A configure option can be a command line flag or an environment variable + or both. + + - `name` is the full command line flag (e.g. --enable-foo). + - `env` is the environment variable name (e.g. ENV) + - `nargs` is the number of arguments the option may take. It can be a + number or the special values '?' (0 or 1), '*' (0 or more), or '+' (1 or + more). + - `default` can be used to give a default value to the option. When the + `name` of the option starts with '--enable-' or '--with-', the implied + default is a NegativeOptionValue (disabled). When it starts with + '--disable-' or '--without-', the implied default is an empty + PositiveOptionValue (enabled). + - `choices` restricts the set of values that can be given to the option. + - `help` is the option description for use in the --help output. + - `possible_origins` is a tuple of strings that are origins accepted for + this option. Example origins are 'mozconfig', 'implied', and 'environment'. + - `category` is a human-readable string used only for categorizing command- + line options when displaying the output of `configure --help`. If not + supplied, the script will attempt to infer an appropriate category based + on the name of the file where the option was defined. If supplied it must + be in the _ALL_CATEGORIES list above. + - `define_depth` should generally only be used by templates that are used + to instantiate an option indirectly. Set this to a positive integer to + force the script to look into a deeper stack frame when inferring the + `category`. + """ + + __slots__ = ( + "id", + "prefix", + "name", + "env", + "nargs", + "default", + "choices", + "help", + "possible_origins", + "category", + "define_depth", + ) + + def __init__( + self, + name=None, + env=None, + nargs=None, + default=None, + possible_origins=None, + choices=None, + category=None, + help=None, + define_depth=0, + ): + if not name and not env: + raise InvalidOptionError( + "At least an option name or an environment variable name must " + "be given" + ) + if name: + if not isinstance(name, six.string_types): + raise InvalidOptionError("Option must be a string") + if not name.startswith("--"): + raise InvalidOptionError("Option must start with `--`") + if "=" in name: + raise InvalidOptionError("Option must not contain an `=`") + if not name.islower(): + raise InvalidOptionError("Option must be all lowercase") + if env: + if not isinstance(env, six.string_types): + raise InvalidOptionError("Environment variable name must be a string") + if not env.isupper(): + raise InvalidOptionError( + "Environment variable name must be all uppercase" + ) + if nargs not in (None, "?", "*", "+") and not ( + isinstance(nargs, int) and nargs >= 0 + ): + raise InvalidOptionError( + "nargs must be a positive integer, '?', '*' or '+'" + ) + if ( + not isinstance(default, six.string_types) + and not isinstance(default, (bool, type(None))) + and not istupleofstrings(default) + ): + raise InvalidOptionError( + "default must be a bool, a string or a tuple of strings" + ) + if choices and not istupleofstrings(choices): + raise InvalidOptionError("choices must be a tuple of strings") + if category and not isinstance(category, six.string_types): + raise InvalidOptionError("Category must be a string") + if category and category not in _ALL_CATEGORIES: + raise InvalidOptionError( + "Category must either be inferred or in the _ALL_CATEGORIES " + "list in options.py: %s" % ", ".join(_ALL_CATEGORIES) + ) + if not isinstance(define_depth, int): + raise InvalidOptionError("DefineDepth must be an integer") + if not help: + raise InvalidOptionError("A help string must be provided") + if possible_origins and not istupleofstrings(possible_origins): + raise InvalidOptionError("possible_origins must be a tuple of strings") + self.possible_origins = possible_origins + + if name: + prefix, name, values = self.split_option(name) + assert values == () + + # --disable and --without options mean the default is enabled. + # --enable and --with options mean the default is disabled. + # However, we allow a default to be given so that the default + # can be affected by other factors. + if prefix: + if default is None: + default = prefix in ("disable", "without") + elif default is False: + prefix = { + "disable": "enable", + "without": "with", + }.get(prefix, prefix) + elif default is True: + prefix = { + "enable": "disable", + "with": "without", + }.get(prefix, prefix) + else: + prefix = "" + + self.prefix = prefix + self.name = name + self.env = env + if default in (None, False): + self.default = NegativeOptionValue(origin="default") + elif isinstance(default, tuple): + self.default = PositiveOptionValue(default, origin="default") + elif default is True: + self.default = PositiveOptionValue(origin="default") + else: + self.default = PositiveOptionValue((default,), origin="default") + if nargs is None: + nargs = 0 + if len(self.default) == 1: + nargs = "?" + elif len(self.default) > 1: + nargs = "*" + elif choices: + nargs = 1 + self.nargs = nargs + has_choices = choices is not None + if isinstance(self.default, PositiveOptionValue): + if has_choices and len(self.default) == 0 and nargs not in ("?", "*"): + raise InvalidOptionError( + "A `default` must be given along with `choices`" + ) + if not self._validate_nargs(len(self.default)): + raise InvalidOptionError("The given `default` doesn't satisfy `nargs`") + if has_choices and not all(d in choices for d in self.default): + raise InvalidOptionError( + "The `default` value must be one of %s" + % ", ".join("'%s'" % c for c in choices) + ) + elif has_choices: + maxargs = self.maxargs + if len(choices) < maxargs and maxargs != sys.maxsize: + raise InvalidOptionError("Not enough `choices` for `nargs`") + self.choices = choices + self.help = help + self.category = category or _infer_option_category(define_depth) + + @staticmethod + def split_option(option): + """Split a flag or variable into a prefix, a name and values + + Variables come in the form NAME=values (no prefix). + Flags come in the form --name=values or --prefix-name=values + where prefix is one of 'with', 'without', 'enable' or 'disable'. + The '=values' part is optional. Values are separated with commas. + """ + if not isinstance(option, six.string_types): + raise InvalidOptionError("Option must be a string") + + elements = option.split("=", 1) + name = elements[0] + values = tuple(elements[1].split(",")) if len(elements) == 2 else () + if name.startswith("--"): + name = name[2:] + if not name.islower(): + raise InvalidOptionError("Option must be all lowercase") + elements = name.split("-", 1) + prefix = elements[0] + if len(elements) == 2 and prefix in ( + "enable", + "disable", + "with", + "without", + ): + return prefix, elements[1], values + else: + if name.startswith("-"): + raise InvalidOptionError( + "Option must start with two dashes instead of one" + ) + if name.islower(): + raise InvalidOptionError( + 'Environment variable name "%s" must be all uppercase' % name + ) + return "", name, values + + @staticmethod + def _join_option(prefix, name): + # The constraints around name and env in __init__ make it so that + # we can distinguish between flags and environment variables with + # islower/isupper. + if name.isupper(): + assert not prefix + return name + elif prefix: + return "--%s-%s" % (prefix, name) + return "--%s" % name + + @property + def option(self): + if self.prefix or self.name: + return self._join_option(self.prefix, self.name) + else: + return self.env + + @property + def minargs(self): + if isinstance(self.nargs, int): + return self.nargs + return 1 if self.nargs == "+" else 0 + + @property + def maxargs(self): + if isinstance(self.nargs, int): + return self.nargs + return 1 if self.nargs == "?" else sys.maxsize + + def _validate_nargs(self, num): + minargs, maxargs = self.minargs, self.maxargs + return num >= minargs and num <= maxargs + + def get_value(self, option=None, origin="unknown"): + """Given a full command line option (e.g. --enable-foo=bar) or a + variable assignment (FOO=bar), returns the corresponding OptionValue. + + Note: variable assignments can come from either the environment or + from the command line (e.g. `../configure CFLAGS=-O2`) + """ + if not option: + return self.default + + if self.possible_origins and origin not in self.possible_origins: + raise InvalidOptionError( + "%s can not be set by %s. Values are accepted from: %s" + % (option, origin, ", ".join(self.possible_origins)) + ) + + prefix, name, values = self.split_option(option) + option = self._join_option(prefix, name) + + assert name in (self.name, self.env) + + if prefix in ("disable", "without"): + if values != (): + raise InvalidOptionError("Cannot pass a value to %s" % option) + return NegativeOptionValue(origin=origin) + + if name == self.env: + if values == ("",): + return NegativeOptionValue(origin=origin) + if self.nargs in (0, "?", "*") and values == ("1",): + return PositiveOptionValue(origin=origin) + + values = PositiveOptionValue(values, origin=origin) + + if not self._validate_nargs(len(values)): + raise InvalidOptionError( + "%s takes %s value%s" + % ( + option, + { + "?": "0 or 1", + "*": "0 or more", + "+": "1 or more", + }.get(self.nargs, str(self.nargs)), + "s" if (not isinstance(self.nargs, int) or self.nargs != 1) else "", + ) + ) + + if len(values) and self.choices: + relative_result = None + for val in values: + if self.nargs in ("+", "*"): + if val.startswith(("+", "-")): + if relative_result is None: + relative_result = list(self.default) + sign = val[0] + val = val[1:] + if sign == "+": + if val not in relative_result: + relative_result.append(val) + else: + try: + relative_result.remove(val) + except ValueError: + pass + + if val not in self.choices: + raise InvalidOptionError( + "'%s' is not one of %s" + % (val, ", ".join("'%s'" % c for c in self.choices)) + ) + + if relative_result is not None: + values = PositiveOptionValue(relative_result, origin=origin) + + return values + + def __repr__(self): + return "<%s [%s]>" % (self.__class__.__name__, self.option) + + +class CommandLineHelper(object): + """Helper class to handle the various ways options can be given either + on the command line of through the environment. + + For instance, an Option('--foo', env='FOO') can be passed as --foo on the + command line, or as FOO=1 in the environment *or* on the command line. + + If multiple variants are given, command line is prefered over the + environment, and if different values are given on the command line, the + last one wins. (This mimicks the behavior of autoconf, avoiding to break + existing mozconfigs using valid options in weird ways) + + Extra options can be added afterwards through API calls. For those, + conflicting values will raise an exception. + """ + + def __init__(self, environ=os.environ, argv=sys.argv): + self._environ = dict(environ) + self._args = OrderedDict() + self._extra_args = OrderedDict() + self._origins = {} + self._last = 0 + + assert argv and not argv[0].startswith("--") + for arg in argv[1:]: + self.add(arg, "command-line", self._args) + + def add(self, arg, origin="command-line", args=None): + assert origin != "default" + prefix, name, values = Option.split_option(arg) + if args is None: + args = self._extra_args + if args is self._extra_args and name in self._extra_args: + old_arg = self._extra_args[name][0] + old_prefix, _, old_values = Option.split_option(old_arg) + if prefix != old_prefix or values != old_values: + raise ConflictingOptionError( + "Cannot add '{arg}' to the {origin} set because it " + "conflicts with '{old_arg}' that was added earlier", + arg=arg, + origin=origin, + old_arg=old_arg, + old_origin=self._origins[old_arg], + ) + self._last += 1 + args[name] = arg, self._last + self._origins[arg] = origin + + def _prepare(self, option, args): + arg = None + origin = "command-line" + from_name = args.get(option.name) + from_env = args.get(option.env) + if from_name and from_env: + arg1, pos1 = from_name + arg2, pos2 = from_env + arg, pos = (arg1, pos1) if abs(pos1) > abs(pos2) else (arg2, pos2) + if args is self._extra_args and ( + option.get_value(arg1) != option.get_value(arg2) + ): + origin = self._origins[arg] + old_arg = arg2 if abs(pos1) > abs(pos2) else arg1 + raise ConflictingOptionError( + "Cannot add '{arg}' to the {origin} set because it " + "conflicts with '{old_arg}' that was added earlier", + arg=arg, + origin=origin, + old_arg=old_arg, + old_origin=self._origins[old_arg], + ) + elif from_name or from_env: + arg, pos = from_name if from_name else from_env + elif option.env and args is self._args: + env = self._environ.get(option.env) + if env is not None: + arg = "%s=%s" % (option.env, env) + origin = "environment" + + origin = self._origins.get(arg, origin) + + for k in (option.name, option.env): + try: + del args[k] + except KeyError: + pass + + return arg, origin + + def handle(self, option): + """Return the OptionValue corresponding to the given Option instance, + depending on the command line, environment, and extra arguments, and + the actual option or variable that set it. + Only works once for a given Option. + """ + assert isinstance(option, Option) + + arg, origin = self._prepare(option, self._args) + ret = option.get_value(arg, origin) + + extra_arg, extra_origin = self._prepare(option, self._extra_args) + extra_ret = option.get_value(extra_arg, extra_origin) + + if extra_ret.origin == "default": + return ret, arg + + if ret.origin != "default" and extra_ret != ret: + raise ConflictingOptionError( + "Cannot add '{arg}' to the {origin} set because it conflicts " + "with {old_arg} from the {old_origin} set", + arg=extra_arg, + origin=extra_ret.origin, + old_arg=arg, + old_origin=ret.origin, + ) + + return extra_ret, extra_arg + + def __iter__(self): + for d in (self._args, self._extra_args): + for arg, pos in six.itervalues(d): + yield arg diff --git a/python/mozbuild/mozbuild/configure/util.py b/python/mozbuild/mozbuild/configure/util.py new file mode 100644 index 0000000000..a58dc4d3f4 --- /dev/null +++ b/python/mozbuild/mozbuild/configure/util.py @@ -0,0 +1,235 @@ +# 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/. + +import codecs +import io +import itertools +import locale +import logging +import os +import sys +from collections import deque +from contextlib import contextmanager + +import six +from looseversion import LooseVersion + + +def getpreferredencoding(): + # locale._parse_localename makes locale.getpreferredencoding + # return None when LC_ALL is C, instead of e.g. 'US-ASCII' or + # 'ANSI_X3.4-1968' when it uses nl_langinfo. + encoding = None + try: + encoding = locale.getpreferredencoding() + except ValueError: + # On english OSX, LC_ALL is UTF-8 (not en-US.UTF-8), and + # that throws off locale._parse_localename, which ends up + # being used on e.g. homebrew python. + if os.environ.get("LC_ALL", "").upper() == "UTF-8": + encoding = "utf-8" + return encoding + + +class Version(LooseVersion): + """A simple subclass of looseversion.LooseVersion. + Adds attributes for `major`, `minor`, `patch` for the first three + version components so users can easily pull out major/minor + versions, like: + + v = Version('1.2b') + v.major == 1 + v.minor == 2 + v.patch == 0 + """ + + def __init__(self, version): + # Can't use super, LooseVersion's base class is not a new-style class. + LooseVersion.__init__(self, version) + # Take the first three integer components, stopping at the first + # non-integer and padding the rest with zeroes. + (self.major, self.minor, self.patch) = list( + itertools.chain( + itertools.takewhile(lambda x: isinstance(x, int), self.version), + (0, 0, 0), + ) + )[:3] + + +class ConfigureOutputHandler(logging.Handler): + """A logging handler class that sends info messages to stdout and other + messages to stderr. + + Messages sent to stdout are not formatted with the attached Formatter. + Additionally, if they end with '... ', no newline character is printed, + making the next message printed follow the '... '. + + Only messages above log level INFO (included) are logged. + + Messages below that level can be kept until an ERROR message is received, + at which point the last `maxlen` accumulated messages below INFO are + printed out. This feature is only enabled under the `queue_debug` context + manager. + """ + + def __init__(self, stdout=sys.stdout, stderr=sys.stderr, maxlen=20): + super(ConfigureOutputHandler, self).__init__() + + # Python has this feature where it sets the encoding of pipes to + # ascii, which blatantly fails when trying to print out non-ascii. + def fix_encoding(fh): + if six.PY3: + return fh + try: + isatty = fh.isatty() + except AttributeError: + isatty = True + + if not isatty: + encoding = getpreferredencoding() + if encoding: + return codecs.getwriter(encoding)(fh) + return fh + + self._stdout = fix_encoding(stdout) + self._stderr = fix_encoding(stderr) if stdout != stderr else self._stdout + try: + fd1 = self._stdout.fileno() + fd2 = self._stderr.fileno() + self._same_output = self._is_same_output(fd1, fd2) + except (AttributeError, io.UnsupportedOperation): + self._same_output = self._stdout == self._stderr + self._stdout_waiting = None + self._debug = deque(maxlen=maxlen + 1) + self._keep_if_debug = self.THROW + self._queue_is_active = False + + @staticmethod + def _is_same_output(fd1, fd2): + if fd1 == fd2: + return True + stat1 = os.fstat(fd1) + stat2 = os.fstat(fd2) + return stat1.st_ino == stat2.st_ino and stat1.st_dev == stat2.st_dev + + # possible values for _stdout_waiting + WAITING = 1 + INTERRUPTED = 2 + + # possible values for _keep_if_debug + THROW = 0 + KEEP = 1 + PRINT = 2 + + def emit(self, record): + try: + if record.levelno == logging.INFO: + stream = self._stdout + msg = six.ensure_text(record.getMessage()) + if self._stdout_waiting == self.INTERRUPTED and self._same_output: + msg = " ... %s" % msg + self._stdout_waiting = msg.endswith("... ") + if msg.endswith("... "): + self._stdout_waiting = self.WAITING + else: + self._stdout_waiting = None + msg = "%s\n" % msg + elif record.levelno < logging.INFO and self._keep_if_debug != self.PRINT: + if self._keep_if_debug == self.KEEP: + self._debug.append(record) + return + else: + if record.levelno >= logging.ERROR and len(self._debug): + self._emit_queue() + + if self._stdout_waiting == self.WAITING and self._same_output: + self._stdout_waiting = self.INTERRUPTED + self._stdout.write("\n") + self._stdout.flush() + stream = self._stderr + msg = "%s\n" % self.format(record) + stream.write(msg) + stream.flush() + except (KeyboardInterrupt, SystemExit, IOError): + raise + except Exception: + self.handleError(record) + + @contextmanager + def queue_debug(self): + if self._queue_is_active: + yield + return + self._queue_is_active = True + self._keep_if_debug = self.KEEP + try: + yield + except Exception: + self._emit_queue() + # The exception will be handled and very probably printed out by + # something upper in the stack. + raise + finally: + self._queue_is_active = False + self._keep_if_debug = self.THROW + self._debug.clear() + + def _emit_queue(self): + self._keep_if_debug = self.PRINT + if len(self._debug) == self._debug.maxlen: + r = self._debug.popleft() + self.emit( + logging.LogRecord( + r.name, + r.levelno, + r.pathname, + r.lineno, + "<truncated - see config.log for full output>", + (), + None, + ) + ) + while True: + try: + self.emit(self._debug.popleft()) + except IndexError: + break + self._keep_if_debug = self.KEEP + + +class LineIO(object): + """File-like class that sends each line of the written data to a callback + (without carriage returns). + """ + + def __init__(self, callback, errors="strict"): + self._callback = callback + self._buf = "" + self._encoding = getpreferredencoding() + self._errors = errors + + def write(self, buf): + buf = six.ensure_text(buf, encoding=self._encoding or "utf-8") + lines = buf.splitlines() + if not lines: + return + if self._buf: + lines[0] = self._buf + lines[0] + self._buf = "" + if not buf.endswith("\n"): + self._buf = lines.pop() + + for line in lines: + self._callback(line) + + def close(self): + if self._buf: + self._callback(self._buf) + self._buf = "" + + def __enter__(self): + return self + + def __exit__(self, *args): + self.close() diff --git a/python/mozbuild/mozbuild/controller/__init__.py b/python/mozbuild/mozbuild/controller/__init__.py new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/python/mozbuild/mozbuild/controller/__init__.py diff --git a/python/mozbuild/mozbuild/controller/building.py b/python/mozbuild/mozbuild/controller/building.py new file mode 100644 index 0000000000..30f8136f26 --- /dev/null +++ b/python/mozbuild/mozbuild/controller/building.py @@ -0,0 +1,1857 @@ +# 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/. + +import errno +import getpass +import io +import json +import logging +import os +import re +import subprocess +import sys +import time +from collections import Counter, OrderedDict, namedtuple +from itertools import dropwhile, islice, takewhile +from textwrap import TextWrapper + +import six +from mach.site import CommandSiteManager + +try: + import psutil +except Exception: + psutil = None + +import mozfile +import mozpack.path as mozpath +from mach.mixin.logging import LoggingMixin +from mach.util import get_state_dir, get_virtualenv_base_dir +from mozsystemmonitor.resourcemonitor import SystemResourceMonitor +from mozterm.widgets import Footer + +from ..backend import get_backend_class +from ..base import MozbuildObject +from ..compilation.warnings import WarningsCollector, WarningsDatabase +from ..telemetry import get_cpu_brand +from ..testing import install_test_files +from ..util import FileAvoidWrite, mkdir, resolve_target_to_make +from .clobber import Clobberer + +FINDER_SLOW_MESSAGE = """ +=================== +PERFORMANCE WARNING + +The OS X Finder application (file indexing used by Spotlight) used a lot of CPU +during the build - an average of %f%% (100%% is 1 core). This made your build +slower. + +Consider adding ".noindex" to the end of your object directory name to have +Finder ignore it. Or, add an indexing exclusion through the Spotlight System +Preferences. +=================== +""".strip() + + +INSTALL_TESTS_CLOBBER = "".join( + [ + TextWrapper().fill(line) + "\n" + for line in """ +The build system was unable to install tests because the CLOBBER file has \ +been updated. This means if you edited any test files, your changes may not \ +be picked up until a full/clobber build is performed. + +The easiest and fastest way to perform a clobber build is to run: + + $ mach clobber + $ mach build + +If you did not modify any test files, it is safe to ignore this message \ +and proceed with running tests. To do this run: + + $ touch {clobber_file} +""".splitlines() + ] +) + +CLOBBER_REQUESTED_MESSAGE = """ +=================== +The CLOBBER file was updated prior to this build. A clobber build may be +required to succeed, but we weren't expecting it to. + +Please consider filing a bug for this failure if you have reason to believe +this is a clobber bug and not due to local changes. +=================== +""".strip() + + +BuildOutputResult = namedtuple( + "BuildOutputResult", ("warning", "state_changed", "message") +) + + +class TierStatus(object): + """Represents the state and progress of tier traversal. + + The build system is organized into linear phases called tiers. Each tier + executes in the order it was defined, 1 at a time. + """ + + def __init__(self, resources): + """Accepts a SystemResourceMonitor to record results against.""" + self.tiers = OrderedDict() + self.tier_status = OrderedDict() + self.resources = resources + + def set_tiers(self, tiers): + """Record the set of known tiers.""" + for tier in tiers: + self.tiers[tier] = dict( + begin_time=None, + finish_time=None, + duration=None, + ) + self.tier_status[tier] = None + + def begin_tier(self, tier): + """Record that execution of a tier has begun.""" + self.tier_status[tier] = "active" + t = self.tiers[tier] + t["begin_time"] = time.monotonic() + self.resources.begin_phase(tier) + + def finish_tier(self, tier): + """Record that execution of a tier has finished.""" + self.tier_status[tier] = "finished" + t = self.tiers[tier] + t["finish_time"] = time.monotonic() + t["duration"] = self.resources.finish_phase(tier) + + +def record_cargo_timings(resource_monitor, timings_path): + cargo_start = 0 + try: + with open(timings_path) as fh: + # Extrace the UNIT_DATA list from the cargo timing HTML file. + unit_data = dropwhile(lambda l: l.rstrip() != "const UNIT_DATA = [", fh) + unit_data = islice(unit_data, 1, None) + lines = takewhile(lambda l: l.rstrip() != "];", unit_data) + entries = json.loads("[" + "".join(lines) + "]") + # Normalize the entries so that any change in data format would + # trigger the exception handler that skips this (we don't want the + # build to fail in that case) + data = [ + ( + "{} v{}{}".format( + entry["name"], entry["version"], entry.get("target", "") + ), + entry["start"] or 0, + entry["duration"] or 0, + ) + for entry in entries + ] + starts = [ + start + for marker, start in resource_monitor._active_markers.items() + if marker.startswith("Rust:") + ] + # The build system is not supposed to be running more than one cargo + # at the same time, which thankfully makes it easier to find the start + # of the one we got the timings for. + if len(starts) != 1: + return + cargo_start = starts[0] + except Exception: + return + + if not cargo_start: + return + + for name, start, duration in data: + resource_monitor.record_marker( + "RustCrate", cargo_start + start, cargo_start + start + duration, name + ) + + +class BuildMonitor(MozbuildObject): + """Monitors the output of the build.""" + + def init(self, warnings_path, terminal): + """Create a new monitor. + + warnings_path is a path of a warnings database to use. + """ + self._warnings_path = warnings_path + self.resources = SystemResourceMonitor( + poll_interval=0.1, + metadata={"CPUName": get_cpu_brand()}, + ) + self._resources_started = False + + self.tiers = TierStatus(self.resources) + + self.warnings_database = WarningsDatabase() + if os.path.exists(warnings_path): + try: + self.warnings_database.load_from_file(warnings_path) + except ValueError: + os.remove(warnings_path) + + # Contains warnings unique to this invocation. Not populated with old + # warnings. + self.instance_warnings = WarningsDatabase() + + self._terminal = terminal + + def on_warning(warning): + # Skip `errors` + if warning["type"] == "error": + return + + filename = warning["filename"] + + if not os.path.exists(filename): + raise Exception("Could not find file containing warning: %s" % filename) + + self.warnings_database.insert(warning) + # Make a copy so mutations don't impact other database. + self.instance_warnings.insert(warning.copy()) + + self._warnings_collector = WarningsCollector(on_warning, objdir=self.topobjdir) + + self.build_objects = [] + self.build_dirs = set() + + def start(self): + """Record the start of the build.""" + self.start_time = time.monotonic() + self._finder_start_cpu = self._get_finder_cpu_usage() + + def start_resource_recording(self): + # This should be merged into start() once bug 892342 lands. + self.resources.start() + self._resources_started = True + + def on_line(self, line): + """Consume a line of output from the build system. + + This will parse the line for state and determine whether more action is + needed. + + Returns a BuildOutputResult instance. + + In this named tuple, warning will be an object describing a new parsed + warning. Otherwise it will be None. + + state_changed indicates whether the build system changed state with + this line. If the build system changed state, the caller may want to + query this instance for the current state in order to update UI, etc. + + message is either None, or the content of a message to be + displayed to the user. + """ + message = None + + # If the previous line was colored (eg. for a compiler warning), our + # line will start with the ansi reset sequence. Strip it to ensure it + # does not interfere with our parsing of the line. + plain_line = self._terminal.strip(line) if self._terminal else line.strip() + if plain_line.startswith("BUILDSTATUS"): + args = plain_line.split() + + _, _, disambiguator = args.pop(0).partition("@") + action = args.pop(0) + update_needed = True + + if action == "TIERS": + self.tiers.set_tiers(args) + update_needed = False + elif action == "TIER_START": + tier = args[0] + self.tiers.begin_tier(tier) + elif action == "TIER_FINISH": + (tier,) = args + self.tiers.finish_tier(tier) + elif action == "OBJECT_FILE": + self.build_objects.append(args[0]) + self.resources.begin_marker("Object", args[0], disambiguator) + update_needed = False + elif action.startswith("START_"): + self.resources.begin_marker( + action[len("START_") :], " ".join(args), disambiguator + ) + update_needed = False + elif action.startswith("END_"): + self.resources.end_marker( + action[len("END_") :], " ".join(args), disambiguator + ) + update_needed = False + elif action == "BUILD_VERBOSE": + build_dir = args[0] + if build_dir not in self.build_dirs: + self.build_dirs.add(build_dir) + message = build_dir + update_needed = False + else: + raise Exception("Unknown build status: %s" % action) + + return BuildOutputResult(None, update_needed, message) + + elif plain_line.startswith("Timing report saved to "): + cargo_timings = plain_line[len("Timing report saved to ") :] + record_cargo_timings(self.resources, cargo_timings) + return BuildOutputResult(None, False, None) + + warning = None + message = line + + try: + warning = self._warnings_collector.process_line(line) + except Exception: + pass + + return BuildOutputResult(warning, False, message) + + def stop_resource_recording(self): + if self._resources_started: + self.resources.stop() + + self._resources_started = False + + def finish(self): + """Record the end of the build.""" + self.stop_resource_recording() + self.end_time = time.monotonic() + self._finder_end_cpu = self._get_finder_cpu_usage() + self.elapsed = self.end_time - self.start_time + + self.warnings_database.prune() + self.warnings_database.save_to_file(self._warnings_path) + + def record_usage(self): + build_resources_profile_path = None + try: + # When running on automation, we store the resource usage data in + # the upload path, alongside, for convenience, a copy of the HTML + # viewer. + if "MOZ_AUTOMATION" in os.environ and "UPLOAD_PATH" in os.environ: + build_resources_profile_path = mozpath.join( + os.environ["UPLOAD_PATH"], "profile_build_resources.json" + ) + else: + build_resources_profile_path = self._get_state_filename( + "profile_build_resources.json" + ) + with io.open( + build_resources_profile_path, "w", encoding="utf-8", newline="\n" + ) as fh: + to_write = six.ensure_text( + json.dumps(self.resources.as_profile(), separators=(",", ":")) + ) + fh.write(to_write) + except Exception as e: + self.log( + logging.WARNING, + "build_resources_error", + {"msg": str(e)}, + "Exception when writing resource usage file: {msg}", + ) + try: + if build_resources_profile_path and os.path.exists( + build_resources_profile_path + ): + os.remove(build_resources_profile_path) + except Exception: + # In case there's an exception for some reason, ignore it. + pass + + def _get_finder_cpu_usage(self): + """Obtain the CPU usage of the Finder app on OS X. + + This is used to detect high CPU usage. + """ + if not sys.platform.startswith("darwin"): + return None + + if not psutil: + return None + + for proc in psutil.process_iter(): + if proc.name != "Finder": + continue + + if proc.username != getpass.getuser(): + continue + + # Try to isolate system finder as opposed to other "Finder" + # processes. + if not proc.exe.endswith("CoreServices/Finder.app/Contents/MacOS/Finder"): + continue + + return proc.get_cpu_times() + + return None + + def have_high_finder_usage(self): + """Determine whether there was high Finder CPU usage during the build. + + Returns True if there was high Finder CPU usage, False if there wasn't, + or None if there is nothing to report. + """ + if not self._finder_start_cpu: + return None, None + + # We only measure if the measured range is sufficiently long. + if self.elapsed < 15: + return None, None + + if not self._finder_end_cpu: + return None, None + + start = self._finder_start_cpu + end = self._finder_end_cpu + + start_total = start.user + start.system + end_total = end.user + end.system + + cpu_seconds = end_total - start_total + + # If Finder used more than 25% of 1 core during the build, report an + # error. + finder_percent = cpu_seconds / self.elapsed * 100 + + return finder_percent > 25, finder_percent + + def have_excessive_swapping(self): + """Determine whether there was excessive swapping during the build. + + Returns a tuple of (excessive, swap_in, swap_out). All values are None + if no swap information is available. + """ + if not self.have_resource_usage: + return None, None, None + + swap_in = sum(m.swap.sin for m in self.resources.measurements) + swap_out = sum(m.swap.sout for m in self.resources.measurements) + + # The threshold of 1024 MB has been arbitrarily chosen. + # + # Choosing a proper value that is ideal for everyone is hard. We will + # likely iterate on the logic until people are generally satisfied. + # If a value is too low, the eventual warning produced does not carry + # much meaning. If the threshold is too high, people may not see the + # warning and the warning will thus be ineffective. + excessive = swap_in > 512 * 1048576 or swap_out > 512 * 1048576 + return excessive, swap_in, swap_out + + @property + def have_resource_usage(self): + """Whether resource usage is available.""" + return self.resources.start_time is not None + + def get_resource_usage(self): + """Produce a data structure containing the low-level resource usage information. + + This data structure can e.g. be serialized into JSON and saved for + subsequent analysis. + + If no resource usage is available, None is returned. + """ + if not self.have_resource_usage: + return None + + cpu_percent = self.resources.aggregate_cpu_percent(phase=None, per_cpu=False) + io = self.resources.aggregate_io(phase=None) + + return dict( + cpu_percent=cpu_percent, + io=io, + ) + + def log_resource_usage(self, usage): + """Summarize the resource usage of this build in a log message.""" + + if not usage: + return + + params = dict( + duration=self.end_time - self.start_time, + cpu_percent=usage["cpu_percent"], + io_read_bytes=usage["io"].read_bytes, + io_write_bytes=usage["io"].write_bytes, + ) + + message = ( + "Overall system resources - Wall time: {duration:.0f}s; " + "CPU: {cpu_percent:.0f}%; " + "Read bytes: {io_read_bytes}; Write bytes: {io_write_bytes}; " + ) + + if hasattr(usage["io"], "read_time") and hasattr(usage["io"], "write_time"): + params.update( + io_read_time=usage["io"].read_time, + io_write_time=usage["io"].write_time, + ) + message += "Read time: {io_read_time}; Write time: {io_write_time}" + + self.log(logging.WARNING, "resource_usage", params, message) + + excessive, sin, sout = self.have_excessive_swapping() + if excessive is not None and (sin or sout): + sin /= 1048576 + sout /= 1048576 + self.log( + logging.WARNING, + "swap_activity", + {"sin": sin, "sout": sout}, + "Swap in/out (MB): {sin}/{sout}", + ) + + def ccache_stats(self, ccache=None): + ccache_stats = None + + if ccache is None: + ccache = mozfile.which("ccache") + if ccache: + # With CCache v3.7+ we can use --print-stats + has_machine_format = CCacheStats.check_version_3_7_or_newer(ccache) + try: + output = subprocess.check_output( + [ccache, "--print-stats" if has_machine_format else "-s"], + universal_newlines=True, + ) + ccache_stats = CCacheStats(output, has_machine_format) + except ValueError as e: + self.log(logging.WARNING, "ccache", {"msg": str(e)}, "{msg}") + return ccache_stats + + +class TerminalLoggingHandler(logging.Handler): + """Custom logging handler that works with terminal window dressing. + + This class should probably live elsewhere, like the mach core. Consider + this a proving ground for its usefulness. + """ + + def __init__(self): + logging.Handler.__init__(self) + + self.fh = sys.stdout + self.footer = None + + def flush(self): + self.acquire() + + try: + self.fh.flush() + finally: + self.release() + + def emit(self, record): + msg = self.format(record) + + self.acquire() + + try: + if self.footer: + self.footer.clear() + + self.fh.write(msg) + self.fh.write("\n") + + if self.footer: + self.footer.draw() + + # If we don't flush, the footer may not get drawn. + self.fh.flush() + finally: + self.release() + + +class BuildProgressFooter(Footer): + """Handles display of a build progress indicator in a terminal. + + When mach builds inside a blessed-supported terminal, it will render + progress information collected from a BuildMonitor. This class converts the + state of BuildMonitor into terminal output. + """ + + def __init__(self, terminal, monitor): + Footer.__init__(self, terminal) + self.tiers = six.viewitems(monitor.tiers.tier_status) + + def draw(self): + """Draws this footer in the terminal.""" + + if not self.tiers: + return + + # The drawn terminal looks something like: + # TIER: static export libs tools + + parts = [("bold", "TIER:")] + append = parts.append + for tier, status in self.tiers: + if status is None: + append(tier) + elif status == "finished": + append(("green", tier)) + else: + append(("underline_yellow", tier)) + + self.write(parts) + + +class OutputManager(LoggingMixin): + """Handles writing job output to a terminal or log.""" + + def __init__(self, log_manager, footer): + self.populate_logger() + + self.footer = None + terminal = log_manager.terminal + + # TODO convert terminal footer to config file setting. + if not terminal: + return + if os.environ.get("INSIDE_EMACS", None): + return + + if os.environ.get("MACH_NO_TERMINAL_FOOTER", None): + footer = None + + self.t = terminal + self.footer = footer + + self._handler = TerminalLoggingHandler() + self._handler.setFormatter(log_manager.terminal_formatter) + self._handler.footer = self.footer + + old = log_manager.replace_terminal_handler(self._handler) + self._handler.level = old.level + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_value, traceback): + if self.footer: + self.footer.clear() + # Prevents the footer from being redrawn if logging occurs. + self._handler.footer = None + + def write_line(self, line): + if self.footer: + self.footer.clear() + + print(line) + + if self.footer: + self.footer.draw() + + def refresh(self): + if not self.footer: + return + + self.footer.clear() + self.footer.draw() + + +class BuildOutputManager(OutputManager): + """Handles writing build output to a terminal, to logs, etc.""" + + def __init__(self, log_manager, monitor, footer): + self.monitor = monitor + OutputManager.__init__(self, log_manager, footer) + + def __exit__(self, exc_type, exc_value, traceback): + OutputManager.__exit__(self, exc_type, exc_value, traceback) + + # Ensure the resource monitor is stopped because leaving it running + # could result in the process hanging on exit because the resource + # collection child process hasn't been told to stop. + self.monitor.stop_resource_recording() + + def on_line(self, line): + warning, state_changed, message = self.monitor.on_line(line) + + if message: + self.log(logging.INFO, "build_output", {"line": message}, "{line}") + elif state_changed: + have_handler = hasattr(self, "handler") + if have_handler: + self.handler.acquire() + try: + self.refresh() + finally: + if have_handler: + self.handler.release() + + +class StaticAnalysisFooter(Footer): + """Handles display of a static analysis progress indicator in a terminal.""" + + def __init__(self, terminal, monitor): + Footer.__init__(self, terminal) + self.monitor = monitor + + def draw(self): + """Draws this footer in the terminal.""" + + monitor = self.monitor + total = monitor.num_files + processed = monitor.num_files_processed + percent = "(%.2f%%)" % (processed * 100.0 / total) + parts = [ + ("bright_black", "Processing"), + ("yellow", str(processed)), + ("bright_black", "of"), + ("yellow", str(total)), + ("bright_black", "files"), + ("green", percent), + ] + if monitor.current_file: + parts.append(("bold", monitor.current_file)) + + self.write(parts) + + +class StaticAnalysisOutputManager(OutputManager): + """Handles writing static analysis output to a terminal or file.""" + + def __init__(self, log_manager, monitor, footer): + self.monitor = monitor + self.raw = "" + OutputManager.__init__(self, log_manager, footer) + + def on_line(self, line): + warning, relevant = self.monitor.on_line(line) + if relevant: + self.raw += line + "\n" + + if warning: + self.log( + logging.INFO, + "compiler_warning", + warning, + "Warning: {flag} in {filename}: {message}", + ) + + if relevant: + self.log(logging.INFO, "build_output", {"line": line}, "{line}") + else: + have_handler = hasattr(self, "handler") + if have_handler: + self.handler.acquire() + try: + self.refresh() + finally: + if have_handler: + self.handler.release() + + def write(self, path, output_format): + assert output_format in ("text", "json"), "Invalid output format {}".format( + output_format + ) + path = mozpath.realpath(path) + + if output_format == "json": + self.monitor._warnings_database.save_to_file(path) + + else: + with io.open(path, "w", encoding="utf-8", newline="\n") as f: + f.write(self.raw) + + self.log( + logging.INFO, + "write_output", + {"path": path, "format": output_format}, + "Wrote {format} output in {path}", + ) + + +class CCacheStats(object): + """Holds statistics from ccache. + + Instances can be subtracted from each other to obtain differences. + print() or str() the object to show a ``ccache -s`` like output + of the captured stats. + + """ + + STATS_KEYS = [ + # (key, description) + # Refer to stats.c in ccache project for all the descriptions. + ("stats_zeroed", ("stats zeroed", "stats zero time")), + ("stats_updated", "stats updated"), + ("cache_hit_direct", "cache hit (direct)"), + ("cache_hit_preprocessed", "cache hit (preprocessed)"), + ("cache_hit_rate", "cache hit rate"), + ("cache_miss", "cache miss"), + ("link", "called for link"), + ("preprocessing", "called for preprocessing"), + ("multiple", "multiple source files"), + ("stdout", "compiler produced stdout"), + ("no_output", "compiler produced no output"), + ("empty_output", "compiler produced empty output"), + ("failed", "compile failed"), + ("error", "ccache internal error"), + ("preprocessor_error", "preprocessor error"), + ("cant_use_pch", "can't use precompiled header"), + ("compiler_missing", "couldn't find the compiler"), + ("cache_file_missing", "cache file missing"), + ("bad_args", "bad compiler arguments"), + ("unsupported_lang", "unsupported source language"), + ("compiler_check_failed", "compiler check failed"), + ("autoconf", "autoconf compile/link"), + ("unsupported_code_directive", "unsupported code directive"), + ("unsupported_compiler_option", "unsupported compiler option"), + ("out_stdout", "output to stdout"), + ("out_device", "output to a non-regular file"), + ("no_input", "no input file"), + ("bad_extra_file", "error hashing extra file"), + ("num_cleanups", "cleanups performed"), + ("cache_files", "files in cache"), + ("cache_size", "cache size"), + ("cache_max_size", "max cache size"), + ] + + SKIP_LINES = ( + "cache directory", + "primary config", + "secondary config", + ) + + STATS_KEYS_3_7_PLUS = { + "stats_zeroed_timestamp": "stats_zeroed", + "stats_updated_timestamp": "stats_updated", + "direct_cache_hit": "cache_hit_direct", + "preprocessed_cache_hit": "cache_hit_preprocessed", + # "cache_hit_rate" is not provided + "cache_miss": "cache_miss", + "called_for_link": "link", + "called_for_preprocessing": "preprocessing", + "multiple_source_files": "multiple", + "compiler_produced_stdout": "stdout", + "compiler_produced_no_output": "no_output", + "compiler_produced_empty_output": "empty_output", + "compile_failed": "failed", + "internal_error": "error", + "preprocessor_error": "preprocessor_error", + "could_not_use_precompiled_header": "cant_use_pch", + "could_not_find_compiler": "compiler_missing", + "missing_cache_file": "cache_file_missing", + "bad_compiler_arguments": "bad_args", + "unsupported_source_language": "unsupported_lang", + "compiler_check_failed": "compiler_check_failed", + "autoconf_test": "autoconf", + "unsupported_code_directive": "unsupported_code_directive", + "unsupported_compiler_option": "unsupported_compiler_option", + "output_to_stdout": "out_stdout", + "output_to_a_non_file": "out_device", + "no_input_file": "no_input", + "error_hashing_extra_file": "bad_extra_file", + "cleanups_performed": "num_cleanups", + "files_in_cache": "cache_files", + "cache_size_kibibyte": "cache_size", + # "cache_max_size" is obsolete and not printed anymore + } + + ABSOLUTE_KEYS = {"cache_files", "cache_size", "cache_max_size"} + FORMAT_KEYS = {"cache_size", "cache_max_size"} + + GiB = 1024**3 + MiB = 1024**2 + KiB = 1024 + + def __init__(self, output=None, has_machine_format=False): + """Construct an instance from the output of ccache -s.""" + self._values = {} + + if not output: + return + + if has_machine_format: + self._parse_machine_format(output) + else: + self._parse_human_format(output) + + def _parse_machine_format(self, output): + for line in output.splitlines(): + line = line.strip() + key, _, value = line.partition("\t") + stat_key = self.STATS_KEYS_3_7_PLUS.get(key) + if stat_key: + value = int(value) + if key.endswith("_kibibyte"): + value *= 1024 + self._values[stat_key] = value + + (direct, preprocessed, miss) = self.hit_rates() + self._values["cache_hit_rate"] = (direct + preprocessed) * 100 + + def _parse_human_format(self, output): + for line in output.splitlines(): + line = line.strip() + if line: + self._parse_line(line) + + def _parse_line(self, line): + line = six.ensure_text(line) + for stat_key, stat_description in self.STATS_KEYS: + if line.startswith(stat_description): + raw_value = self._strip_prefix(line, stat_description) + self._values[stat_key] = self._parse_value(raw_value) + break + else: + if not line.startswith(self.SKIP_LINES): + raise ValueError("Failed to parse ccache stats output: %s" % line) + + @staticmethod + def _strip_prefix(line, prefix): + if isinstance(prefix, tuple): + for p in prefix: + line = CCacheStats._strip_prefix(line, p) + return line + return line[len(prefix) :].strip() if line.startswith(prefix) else line + + @staticmethod + def _parse_value(raw_value): + try: + # ccache calls strftime with '%c' (src/stats.c) + ts = time.strptime(raw_value, "%c") + return int(time.mktime(ts)) + except ValueError: + if raw_value == "never": + return 0 + pass + + value = raw_value.split() + unit = "" + if len(value) == 1: + numeric = value[0] + elif len(value) == 2: + numeric, unit = value + else: + raise ValueError("Failed to parse ccache stats value: %s" % raw_value) + + if "." in numeric: + numeric = float(numeric) + else: + numeric = int(numeric) + + if unit in ("GB", "Gbytes"): + unit = CCacheStats.GiB + elif unit in ("MB", "Mbytes"): + unit = CCacheStats.MiB + elif unit in ("KB", "Kbytes"): + unit = CCacheStats.KiB + else: + unit = 1 + + return int(numeric * unit) + + def hit_rate_message(self): + return ( + "ccache (direct) hit rate: {:.1%}; (preprocessed) hit rate: {:.1%};" + " miss rate: {:.1%}".format(*self.hit_rates()) + ) + + def hit_rates(self): + direct = self._values["cache_hit_direct"] + preprocessed = self._values["cache_hit_preprocessed"] + miss = self._values["cache_miss"] + total = float(direct + preprocessed + miss) + + if total > 0: + direct /= total + preprocessed /= total + miss /= total + + return (direct, preprocessed, miss) + + def __sub__(self, other): + result = CCacheStats() + + for k, prefix in self.STATS_KEYS: + if k not in self._values and k not in other._values: + continue + + our_value = self._values.get(k, 0) + other_value = other._values.get(k, 0) + + if k in self.ABSOLUTE_KEYS: + result._values[k] = our_value + else: + result._values[k] = our_value - other_value + + return result + + def __str__(self): + LEFT_ALIGN = 34 + lines = [] + + for stat_key, stat_description in self.STATS_KEYS: + if stat_key not in self._values: + continue + + value = self._values[stat_key] + + if stat_key in self.FORMAT_KEYS: + value = "%15s" % self._format_value(value) + else: + value = "%8u" % value + + if isinstance(stat_description, tuple): + stat_description = stat_description[0] + + lines.append("%s%s" % (stat_description.ljust(LEFT_ALIGN), value)) + + return "\n".join(lines) + + def __nonzero__(self): + relative_values = [ + v for k, v in self._values.items() if k not in self.ABSOLUTE_KEYS + ] + return all(v >= 0 for v in relative_values) and any( + v > 0 for v in relative_values + ) + + def __bool__(self): + return self.__nonzero__() + + @staticmethod + def _format_value(v): + if v > CCacheStats.GiB: + return "%.1f Gbytes" % (float(v) / CCacheStats.GiB) + elif v > CCacheStats.MiB: + return "%.1f Mbytes" % (float(v) / CCacheStats.MiB) + else: + return "%.1f Kbytes" % (float(v) / CCacheStats.KiB) + + @staticmethod + def check_version_3_7_or_newer(ccache): + output_version = subprocess.check_output( + [ccache, "--version"], universal_newlines=True + ) + return CCacheStats._is_version_3_7_or_newer(output_version) + + @staticmethod + def _is_version_3_7_or_newer(output): + if "ccache version" not in output: + return False + + major = 0 + minor = 0 + + for line in output.splitlines(): + version = re.search(r"ccache version (\d+).(\d+).*", line) + if version: + major = int(version.group(1)) + minor = int(version.group(2)) + break + + return ((major << 8) + minor) >= ((3 << 8) + 7) + + +class BuildDriver(MozbuildObject): + """Provides a high-level API for build actions.""" + + def __init__(self, *args, **kwargs): + MozbuildObject.__init__(self, *args, virtualenv_name="build", **kwargs) + self.metrics = None + self.mach_context = None + + def build( + self, + metrics, + what=None, + jobs=0, + job_size=0, + directory=None, + verbose=False, + keep_going=False, + mach_context=None, + append_env=None, + ): + warnings_path = self._get_state_filename("warnings.json") + monitor = self._spawn(BuildMonitor) + monitor.init(warnings_path, self.log_manager.terminal) + status = self._build( + monitor, + metrics, + what, + jobs, + job_size, + directory, + verbose, + keep_going, + mach_context, + append_env, + ) + + record_usage = True + + # On automation, only record usage for plain `mach build` + if "MOZ_AUTOMATION" in os.environ and what: + record_usage = False + + if record_usage: + monitor.record_usage() + + return status + + def _build( + self, + monitor, + metrics, + what=None, + jobs=0, + job_size=0, + directory=None, + verbose=False, + keep_going=False, + mach_context=None, + append_env=None, + ): + """Invoke the build backend. + + ``what`` defines the thing to build. If not defined, the default + target is used. + """ + self.metrics = metrics + self.mach_context = mach_context + footer = BuildProgressFooter(self.log_manager.terminal, monitor) + + # Disable indexing in objdir because it is not necessary and can slow + # down builds. + mkdir(self.topobjdir, not_indexed=True) + + with BuildOutputManager(self.log_manager, monitor, footer) as output: + monitor.start() + + if directory is not None and not what: + print("Can only use -C/--directory with an explicit target " "name.") + return 1 + + if directory is not None: + directory = mozpath.normsep(directory) + if directory.startswith("/"): + directory = directory[1:] + + monitor.start_resource_recording() + + if self._check_clobber(self.mozconfig, os.environ): + return 1 + + self.mach_context.command_attrs["clobber"] = False + self.metrics.mozbuild.clobber.set(False) + config = None + try: + config = self.config_environment + except Exception: + # If we don't already have a config environment this is either + # a fresh objdir or $OBJDIR/config.status has been removed for + # some reason, which indicates a clobber of sorts. + self.mach_context.command_attrs["clobber"] = True + self.metrics.mozbuild.clobber.set(True) + + # Record whether a clobber was requested so we can print + # a special message later if the build fails. + clobber_requested = False + + # Write out any changes to the current mozconfig in case + # they should invalidate configure. + self._write_mozconfig_json() + + previous_backend = None + if config is not None: + previous_backend = config.substs.get("BUILD_BACKENDS", [None])[0] + + config_rc = None + # Even if we have a config object, it may be out of date + # if something that influences its result has changed. + if config is None or self.build_out_of_date( + mozpath.join(self.topobjdir, "config.status"), + mozpath.join(self.topobjdir, "config_status_deps.in"), + ): + if previous_backend and "Make" not in previous_backend: + clobber_requested = self._clobber_configure() + + if config is None: + print(" Config object not found by mach.") + + config_rc = self.configure( + metrics, + buildstatus_messages=True, + line_handler=output.on_line, + append_env=append_env, + ) + + if config_rc != 0: + return config_rc + + config = self.reload_config_environment() + + if config.substs.get("MOZ_USING_CCACHE"): + ccache = config.substs.get("CCACHE") + ccache_start = monitor.ccache_stats(ccache) + else: + ccache_start = None + + # Collect glean metrics + substs = config.substs + mozbuild_metrics = metrics.mozbuild + mozbuild_metrics.compiler.set(substs.get("CC_TYPE", None)) + + def get_substs_flag(name): + return bool(substs.get(name, None)) + + host = substs.get("host") + monitor.resources.metadata["oscpu"] = host + target = substs.get("target") + if host != target: + monitor.resources.metadata["abi"] = target + + product_name = substs.get("MOZ_BUILD_APP") + app_displayname = substs.get("MOZ_APP_DISPLAYNAME") + if app_displayname: + product_name = app_displayname + app_version = substs.get("MOZ_APP_VERSION") + if app_version: + product_name += " " + app_version + monitor.resources.metadata["product"] = product_name + + mozbuild_metrics.artifact.set(get_substs_flag("MOZ_ARTIFACT_BUILDS")) + mozbuild_metrics.debug.set(get_substs_flag("MOZ_DEBUG")) + mozbuild_metrics.opt.set(get_substs_flag("MOZ_OPTIMIZE")) + mozbuild_metrics.ccache.set(get_substs_flag("CCACHE")) + using_sccache = get_substs_flag("MOZ_USING_SCCACHE") + mozbuild_metrics.sccache.set(using_sccache) + mozbuild_metrics.icecream.set(get_substs_flag("CXX_IS_ICECREAM")) + mozbuild_metrics.project.set(substs.get("MOZ_BUILD_APP", "")) + + all_backends = config.substs.get("BUILD_BACKENDS", [None]) + active_backend = all_backends[0] + + status = None + + if not config_rc and any( + [ + self.backend_out_of_date( + mozpath.join(self.topobjdir, "backend.%sBackend" % backend) + ) + for backend in all_backends + ] + ): + print("Build configuration changed. Regenerating backend.") + args = [ + config.substs["PYTHON3"], + mozpath.join(self.topobjdir, "config.status"), + ] + self.run_process(args, cwd=self.topobjdir, pass_thru=True) + + if jobs == 0: + for param in self.mozconfig.get("make_extra") or []: + key, value = param.split("=", 1) + if key == "MOZ_PARALLEL_BUILD": + jobs = int(value) + + if "Make" not in active_backend: + backend_cls = get_backend_class(active_backend)(config) + status = backend_cls.build(self, output, jobs, verbose, what) + + if status and clobber_requested: + for line in CLOBBER_REQUESTED_MESSAGE.splitlines(): + self.log( + logging.WARNING, "clobber", {"msg": line.rstrip()}, "{msg}" + ) + + if what and status is None: + # Collect target pairs. + target_pairs = [] + for target in what: + path_arg = self._wrap_path_argument(target) + + if directory is not None: + make_dir = mozpath.join(self.topobjdir, directory) + make_target = target + else: + make_dir, make_target = resolve_target_to_make( + self.topobjdir, path_arg.relpath() + ) + + if make_dir is None and make_target is None: + return 1 + + if config.is_artifact_build and target.startswith("installers-"): + # See https://bugzilla.mozilla.org/show_bug.cgi?id=1387485 + print( + "Localized Builds are not supported with Artifact Builds enabled.\n" + "You should disable Artifact Builds (Use --disable-compile-environment " + "in your mozconfig instead) then re-build to proceed." + ) + return 1 + + # See bug 886162 - we don't want to "accidentally" build + # the entire tree (if that's really the intent, it's + # unlikely they would have specified a directory.) + if not make_dir and not make_target: + print( + "The specified directory doesn't contain a " + "Makefile and the first parent with one is the " + "root of the tree. Please specify a directory " + "with a Makefile or run |mach build| if you " + "want to build the entire tree." + ) + return 1 + + target_pairs.append((make_dir, make_target)) + + # Build target pairs. + for make_dir, make_target in target_pairs: + # We don't display build status messages during partial + # tree builds because they aren't reliable there. This + # could potentially be fixed if the build monitor were more + # intelligent about encountering undefined state. + no_build_status = "1" if make_dir is not None else "" + tgt_env = dict(append_env or {}) + tgt_env["NO_BUILDSTATUS_MESSAGES"] = no_build_status + status = self._run_make( + directory=make_dir, + target=make_target, + line_handler=output.on_line, + log=False, + print_directory=False, + ensure_exit_code=False, + num_jobs=jobs, + job_size=job_size, + silent=not verbose, + append_env=tgt_env, + keep_going=keep_going, + ) + + if status != 0: + break + + elif status is None: + # If the backend doesn't specify a build() method, then just + # call client.mk directly. + status = self._run_client_mk( + line_handler=output.on_line, + jobs=jobs, + job_size=job_size, + verbose=verbose, + keep_going=keep_going, + append_env=append_env, + ) + + self.log( + logging.WARNING, + "warning_summary", + {"count": len(monitor.warnings_database)}, + "{count} compiler warnings present.", + ) + + # Try to run the active build backend's post-build step, if possible. + try: + active_backend = config.substs.get("BUILD_BACKENDS", [None])[0] + if active_backend: + backend_cls = get_backend_class(active_backend)(config) + new_status = backend_cls.post_build( + self, output, jobs, verbose, status + ) + status = new_status + except Exception as ex: + self.log( + logging.DEBUG, + "post_build", + {"ex": str(ex)}, + "Unable to run active build backend's post-build step; " + + "failing the build due to exception: {ex}.", + ) + if not status: + # If the underlying build provided a failing status, pass + # it through; otherwise, fail. + status = 1 + + monitor.finish() + + if status == 0: + usage = monitor.get_resource_usage() + if usage: + self.mach_context.command_attrs["usage"] = usage + monitor.log_resource_usage(usage) + + # Print the collected compiler warnings. This is redundant with + # inline output from the compiler itself. However, unlike inline + # output, this list is sorted and grouped by file, making it + # easier to triage output. + # + # Only do this if we had a successful build. If the build failed, + # there are more important things in the log to look for than + # whatever code we warned about. + if not status: + # Suppress warnings for 3rd party projects in local builds + # until we suppress them for real. + # TODO remove entries/feature once we stop generating warnings + # in these directories. + pathToThirdparty = mozpath.join( + self.topsrcdir, "tools", "rewriting", "ThirdPartyPaths.txt" + ) + + pathToGenerated = mozpath.join( + self.topsrcdir, "tools", "rewriting", "Generated.txt" + ) + + if os.path.exists(pathToThirdparty): + with io.open( + pathToThirdparty, encoding="utf-8", newline="\n" + ) as f, io.open(pathToGenerated, encoding="utf-8", newline="\n") as g: + # Normalize the path (no trailing /) + LOCAL_SUPPRESS_DIRS = tuple( + [line.strip("\n/") for line in f] + + [line.strip("\n/") for line in g] + ) + else: + # For application based on gecko like thunderbird + LOCAL_SUPPRESS_DIRS = () + + suppressed_by_dir = Counter() + + THIRD_PARTY_CODE = "third-party code" + suppressed = set( + w.replace("-Wno-error=", "-W") + for w in substs.get("WARNINGS_CFLAGS", []) + + substs.get("WARNINGS_CXXFLAGS", []) + if w.startswith("-Wno-error=") + ) + warnings = [] + for warning in sorted(monitor.instance_warnings): + path = mozpath.normsep(warning["filename"]) + if path.startswith(self.topsrcdir): + path = path[len(self.topsrcdir) + 1 :] + + warning["normpath"] = path + + if "MOZ_AUTOMATION" not in os.environ: + if path.startswith(LOCAL_SUPPRESS_DIRS): + suppressed_by_dir[THIRD_PARTY_CODE] += 1 + continue + + if warning["flag"] in suppressed: + suppressed_by_dir[mozpath.dirname(path)] += 1 + continue + + warnings.append(warning) + + if THIRD_PARTY_CODE in suppressed_by_dir: + suppressed_third_party_code = [ + (THIRD_PARTY_CODE, suppressed_by_dir.pop(THIRD_PARTY_CODE)) + ] + else: + suppressed_third_party_code = [] + for d, count in suppressed_third_party_code + sorted( + suppressed_by_dir.items() + ): + self.log( + logging.WARNING, + "suppressed_warning", + {"dir": d, "count": count}, + "(suppressed {count} warnings in {dir})", + ) + + for warning in warnings: + if warning["column"] is not None: + self.log( + logging.WARNING, + "compiler_warning", + warning, + "warning: {normpath}:{line}:{column} [{flag}] " "{message}", + ) + else: + self.log( + logging.WARNING, + "compiler_warning", + warning, + "warning: {normpath}:{line} [{flag}] {message}", + ) + + high_finder, finder_percent = monitor.have_high_finder_usage() + if high_finder: + print(FINDER_SLOW_MESSAGE % finder_percent) + + if config.substs.get("MOZ_USING_CCACHE"): + ccache_end = monitor.ccache_stats(ccache) + else: + ccache_end = None + + ccache_diff = None + if ccache_start and ccache_end: + ccache_diff = ccache_end - ccache_start + if ccache_diff: + self.log( + logging.INFO, + "ccache", + {"msg": ccache_diff.hit_rate_message()}, + "{msg}", + ) + + notify_minimum_time = 300 + try: + notify_minimum_time = int(os.environ.get("MACH_NOTIFY_MINTIME", "300")) + except ValueError: + # Just stick with the default + pass + + if monitor.elapsed > notify_minimum_time: + # Display a notification when the build completes. + self.notify("Build complete" if not status else "Build failed") + + if status: + if what and any( + [target for target in what if target not in ("faster", "binaries")] + ): + print( + "Hey! Builds initiated with `mach build " + "$A_SPECIFIC_TARGET` may not always work, even if the " + "code being built is correct. Consider doing a bare " + "`mach build` instead." + ) + return status + + if monitor.have_resource_usage: + excessive, swap_in, swap_out = monitor.have_excessive_swapping() + # if excessive: + # print(EXCESSIVE_SWAP_MESSAGE) + + print("To view a profile of the build, run |mach " "resource-usage|.") + + long_build = monitor.elapsed > 1200 + + if long_build: + output.on_line( + "We know it took a while, but your build finally finished successfully!" + ) + if not using_sccache: + output.on_line( + "If you are building Firefox often, SCCache can save you a lot " + "of time. You can learn more here: " + "https://firefox-source-docs.mozilla.org/setup/" + "configuring_build_options.html#sccache" + ) + else: + output.on_line("Your build was successful!") + + # Only for full builds because incremental builders likely don't + # need to be burdened with this. + if not what: + try: + # Fennec doesn't have useful output from just building. We should + # arguably make the build action useful for Fennec. Another day... + if self.substs["MOZ_BUILD_APP"] != "mobile/android": + print("To take your build for a test drive, run: |mach run|") + app = self.substs["MOZ_BUILD_APP"] + if app in ("browser", "mobile/android"): + print( + "For more information on what to do now, see " + "https://firefox-source-docs.mozilla.org/setup/contributing_code.html" # noqa + ) + except Exception: + # Ignore Exceptions in case we can't find config.status (such + # as when doing OSX Universal builds) + pass + + return status + + def configure( + self, + metrics, + options=None, + buildstatus_messages=False, + line_handler=None, + append_env=None, + ): + # Disable indexing in objdir because it is not necessary and can slow + # down builds. + self.metrics = metrics + mkdir(self.topobjdir, not_indexed=True) + self._write_mozconfig_json() + + def on_line(line): + self.log(logging.INFO, "build_output", {"line": line}, "{line}") + + line_handler = line_handler or on_line + + append_env = dict(append_env or {}) + + # Back when client.mk was used, `mk_add_options "export ..."` lines + # from the mozconfig would spill into the configure environment, so + # add that for backwards compatibility. + for line in self.mozconfig["make_extra"] or []: + if line.startswith("export "): + k, eq, v = line[len("export ") :].partition("=") + if eq == "=": + append_env[k] = v + + build_site = CommandSiteManager.from_environment( + self.topsrcdir, + lambda: get_state_dir(specific_to_topsrcdir=True, topsrcdir=self.topsrcdir), + "build", + get_virtualenv_base_dir(self.topsrcdir), + ) + build_site.ensure() + + command = [build_site.python_path, mozpath.join(self.topsrcdir, "configure.py")] + if options: + command.extend(options) + + if buildstatus_messages: + append_env["MOZ_CONFIGURE_BUILDSTATUS"] = "1" + line_handler("BUILDSTATUS TIERS configure") + line_handler("BUILDSTATUS TIER_START configure") + + env = os.environ.copy() + env.update(append_env) + + with subprocess.Popen( + command, + cwd=self.topobjdir, + env=env, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + universal_newlines=True, + ) as process: + for line in process.stdout: + line_handler(line.rstrip()) + status = process.wait() + if buildstatus_messages: + line_handler("BUILDSTATUS TIER_FINISH configure") + if status: + print('*** Fix above errors and then restart with "./mach build"') + else: + print("Configure complete!") + print("Be sure to run |mach build| to pick up any changes") + + return status + + def install_tests(self): + """Install test files.""" + + if self.is_clobber_needed(): + print( + INSTALL_TESTS_CLOBBER.format( + clobber_file=mozpath.join(self.topobjdir, "CLOBBER") + ) + ) + sys.exit(1) + + install_test_files(mozpath.normpath(self.topsrcdir), self.topobjdir, "_tests") + + def _clobber_configure(self): + # This is an optimistic treatment of the CLOBBER file for when we have + # some trust in the build system: an update to the CLOBBER file is + # interpreted to mean that configure will fail during an incremental + # build, which is handled by removing intermediate configure artifacts + # and subsections of the objdir related to python and testing before + # proceeding. + clobberer = Clobberer(self.topsrcdir, self.topobjdir) + clobber_output = io.StringIO() + res = clobberer.maybe_do_clobber(os.getcwd(), False, clobber_output) + required, performed, message = res + assert not performed + if not required: + return False + + def remove_objdir_path(path): + path = mozpath.join(self.topobjdir, path) + self.log( + logging.WARNING, + "clobber", + {"path": path}, + "CLOBBER file has been updated, removing {path}.", + ) + mozfile.remove(path) + + # Remove files we think could cause "configure" clobber bugs. + for f in ("old-configure.vars", "config.cache", "configure.pkl"): + remove_objdir_path(f) + remove_objdir_path(mozpath.join("js", "src", f)) + + rm_dirs = [ + # Stale paths in our virtualenv may cause build-backend + # to fail. + "_virtualenvs", + # Some tests may accumulate state in the objdir that may + # become invalid after srcdir changes. + "_tests", + ] + + for d in rm_dirs: + remove_objdir_path(d) + + os.utime(mozpath.join(self.topobjdir, "CLOBBER"), None) + return True + + def _write_mozconfig_json(self): + mozconfig_json = mozpath.join(self.topobjdir, ".mozconfig.json") + with FileAvoidWrite(mozconfig_json) as fh: + to_write = six.ensure_text( + json.dumps( + { + "topsrcdir": self.topsrcdir, + "topobjdir": self.topobjdir, + "mozconfig": self.mozconfig, + }, + sort_keys=True, + indent=2, + ) + ) + # json.dumps in python2 inserts some trailing whitespace while + # json.dumps in python3 does not, which defeats the FileAvoidWrite + # mechanism. Strip the trailing whitespace to avoid rewriting this + # file unnecessarily. + to_write = "\n".join([line.rstrip() for line in to_write.splitlines()]) + fh.write(to_write) + + def _run_client_mk( + self, + target=None, + line_handler=None, + jobs=0, + job_size=0, + verbose=None, + keep_going=False, + append_env=None, + ): + append_env = dict(append_env or {}) + append_env["TOPSRCDIR"] = self.topsrcdir + + append_env["CONFIG_GUESS"] = self.resolve_config_guess() + + mozconfig = self.mozconfig + + mozconfig_make_lines = [] + for arg in mozconfig["make_extra"] or []: + mozconfig_make_lines.append(arg) + + if mozconfig["make_flags"]: + mozconfig_make_lines.append( + "MOZ_MAKE_FLAGS=%s" % " ".join(mozconfig["make_flags"]) + ) + objdir = mozpath.normsep(self.topobjdir) + mozconfig_make_lines.append("MOZ_OBJDIR=%s" % objdir) + mozconfig_make_lines.append("OBJDIR=%s" % objdir) + + if mozconfig["path"]: + mozconfig_make_lines.append( + "FOUND_MOZCONFIG=%s" % mozpath.normsep(mozconfig["path"]) + ) + mozconfig_make_lines.append("export FOUND_MOZCONFIG") + + # The .mozconfig.mk file only contains exported variables and lines with + # UPLOAD_EXTRA_FILES. + mozconfig_filtered_lines = [ + line + for line in mozconfig_make_lines + # Bug 1418122 investigate why UPLOAD_EXTRA_FILES is special and + # remove it. + if line.startswith("export ") or "UPLOAD_EXTRA_FILES" in line + ] + + mozconfig_client_mk = mozpath.join(self.topobjdir, ".mozconfig-client-mk") + with FileAvoidWrite(mozconfig_client_mk) as fh: + fh.write("\n".join(mozconfig_make_lines)) + + mozconfig_mk = mozpath.join(self.topobjdir, ".mozconfig.mk") + with FileAvoidWrite(mozconfig_mk) as fh: + fh.write("\n".join(mozconfig_filtered_lines)) + + # Copy the original mozconfig to the objdir. + mozconfig_objdir = mozpath.join(self.topobjdir, ".mozconfig") + if mozconfig["path"]: + with open(mozconfig["path"], "r") as ifh: + with FileAvoidWrite(mozconfig_objdir) as ofh: + ofh.write(ifh.read()) + else: + try: + os.unlink(mozconfig_objdir) + except OSError as e: + if e.errno != errno.ENOENT: + raise + + if mozconfig_make_lines: + self.log( + logging.WARNING, + "mozconfig_content", + { + "path": mozconfig["path"], + "content": "\n ".join(mozconfig_make_lines), + }, + "Adding make options from {path}\n {content}", + ) + + append_env["OBJDIR"] = mozpath.normsep(self.topobjdir) + + return self._run_make( + srcdir=True, + filename="client.mk", + ensure_exit_code=False, + print_directory=False, + target=target, + line_handler=line_handler, + log=False, + num_jobs=jobs, + job_size=job_size, + silent=not verbose, + keep_going=keep_going, + append_env=append_env, + ) + + def _check_clobber(self, mozconfig, env): + """Run `Clobberer.maybe_do_clobber`, log the result and return a status bool. + + Wraps the clobbering logic in `Clobberer.maybe_do_clobber` to provide logging + and handling of the `AUTOCLOBBER` mozconfig option. + + Return a bool indicating whether the clobber reached an error state. For example, + return `True` if the clobber was required but not completed, and return `False` if + the clobber was not required and not completed. + """ + auto_clobber = any( + [ + env.get("AUTOCLOBBER", False), + (mozconfig["env"] or {}).get("added", {}).get("AUTOCLOBBER", False), + "AUTOCLOBBER=1" in (mozconfig["make_extra"] or []), + ] + ) + from mozbuild.base import BuildEnvironmentNotFoundException + + substs = dict() + try: + substs = self.substs + except BuildEnvironmentNotFoundException: + # We'll just use an empty substs if there is no config. + pass + clobberer = Clobberer(self.topsrcdir, self.topobjdir, substs) + clobber_output = six.StringIO() + res = clobberer.maybe_do_clobber(os.getcwd(), auto_clobber, clobber_output) + clobber_output.seek(0) + for line in clobber_output.readlines(): + self.log(logging.WARNING, "clobber", {"msg": line.rstrip()}, "{msg}") + + clobber_required, clobber_performed, clobber_message = res + if clobber_required and not clobber_performed: + for line in clobber_message.splitlines(): + self.log(logging.WARNING, "clobber", {"msg": line.rstrip()}, "{msg}") + return True + + if clobber_performed and env.get("TINDERBOX_OUTPUT"): + self.log( + logging.WARNING, + "clobber", + {"msg": "TinderboxPrint: auto clobber"}, + "{msg}", + ) + + return False diff --git a/python/mozbuild/mozbuild/controller/clobber.py b/python/mozbuild/mozbuild/controller/clobber.py new file mode 100644 index 0000000000..45f19070cf --- /dev/null +++ b/python/mozbuild/mozbuild/controller/clobber.py @@ -0,0 +1,243 @@ +# 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/. + +r"""This module contains code for managing clobbering of the tree.""" + +import errno +import os +import subprocess +import sys +from textwrap import TextWrapper + +from mozfile.mozfile import remove as mozfileremove +from mozpack import path as mozpath + +CLOBBER_MESSAGE = "".join( + [ + TextWrapper().fill(line) + "\n" + for line in """ +The CLOBBER file has been updated, indicating that an incremental build since \ +your last build will probably not work. A full/clobber build is required. + +The reason for the clobber is: + +{clobber_reason} + +Clobbering can be performed automatically. However, we didn't automatically \ +clobber this time because: + +{no_reason} + +The easiest and fastest way to clobber is to run: + + $ mach clobber + +If you know this clobber doesn't apply to you or you're feeling lucky -- \ +Well, are ya? -- you can ignore this clobber requirement by running: + + $ touch {clobber_file} +""".splitlines() + ] +) + + +class Clobberer(object): + def __init__(self, topsrcdir, topobjdir, substs=None): + """Create a new object to manage clobbering the tree. + + It is bound to a top source directory and to a specific object + directory. + """ + assert os.path.isabs(topsrcdir) + assert os.path.isabs(topobjdir) + + self.topsrcdir = mozpath.normpath(topsrcdir) + self.topobjdir = mozpath.normpath(topobjdir) + self.src_clobber = mozpath.join(topsrcdir, "CLOBBER") + self.obj_clobber = mozpath.join(topobjdir, "CLOBBER") + if substs: + self.substs = substs + else: + self.substs = dict() + + def clobber_needed(self): + """Returns a bool indicating whether a tree clobber is required.""" + + # No object directory clobber file means we're good. + if not os.path.exists(self.obj_clobber): + return False + + # No source directory clobber means we're running from a source package + # that doesn't use clobbering. + if not os.path.exists(self.src_clobber): + return False + + # Object directory clobber older than current is fine. + if os.path.getmtime(self.src_clobber) <= os.path.getmtime(self.obj_clobber): + return False + + return True + + def clobber_cause(self): + """Obtain the cause why a clobber is required. + + This reads the cause from the CLOBBER file. + + This returns a list of lines describing why the clobber was required. + Each line is stripped of leading and trailing whitespace. + """ + with open(self.src_clobber, "rt") as fh: + lines = [l.strip() for l in fh.readlines()] + return [l for l in lines if l and not l.startswith("#")] + + def have_winrm(self): + # `winrm -h` should print 'winrm version ...' and exit 1 + try: + p = subprocess.Popen( + ["winrm.exe", "-h"], stdout=subprocess.PIPE, stderr=subprocess.STDOUT + ) + return p.wait() == 1 and p.stdout.read().startswith("winrm") + except Exception: + return False + + def collect_subdirs(self, root, exclude): + """Gathers a list of subdirectories excluding specified items.""" + paths = [] + try: + for p in os.listdir(root): + if p not in exclude: + paths.append(mozpath.join(root, p)) + except OSError as e: + if e.errno != errno.ENOENT: + raise + + return paths + + def delete_dirs(self, root, paths_to_delete): + """Deletes the given subdirectories in an optimal way.""" + procs = [] + for p in sorted(paths_to_delete): + path = mozpath.join(root, p) + if ( + sys.platform.startswith("win") + and self.have_winrm() + and os.path.isdir(path) + ): + procs.append(subprocess.Popen(["winrm", "-rf", path])) + else: + # We use mozfile because it is faster than shutil.rmtree(). + mozfileremove(path) + + for p in procs: + p.wait() + + def remove_objdir(self, full=True): + """Remove the object directory. + + ``full`` controls whether to fully delete the objdir. If False, + some directories (e.g. Visual Studio Project Files) will not be + deleted. + """ + # Determine where cargo build artifacts are stored + RUST_TARGET_VARS = ("RUST_HOST_TARGET", "RUST_TARGET") + rust_targets = set( + [self.substs[x] for x in RUST_TARGET_VARS if x in self.substs] + ) + rust_build_kind = "release" + if self.substs.get("MOZ_DEBUG_RUST"): + rust_build_kind = "debug" + + # Top-level files and directories to not clobber by default. + no_clobber = {".mozbuild", "msvc", "_virtualenvs"} + + # Hold off on clobbering cargo build artifacts + no_clobber |= rust_targets + + if full: + paths = [self.topobjdir] + else: + paths = self.collect_subdirs(self.topobjdir, no_clobber) + + self.delete_dirs(self.topobjdir, paths) + + # Now handle cargo's build artifacts and skip removing the incremental + # compilation cache. + for target in rust_targets: + cargo_path = mozpath.join(self.topobjdir, target, rust_build_kind) + paths = self.collect_subdirs( + cargo_path, + { + "incremental", + }, + ) + self.delete_dirs(cargo_path, paths) + + def maybe_do_clobber(self, cwd, allow_auto=False, fh=sys.stderr): + """Perform a clobber if it is required. Maybe. + + This is the API the build system invokes to determine if a clobber + is needed and to automatically perform that clobber if we can. + + This returns a tuple of (bool, bool, str). The elements are: + + - Whether a clobber was/is required. + - Whether a clobber was performed. + - The reason why the clobber failed or could not be performed. This + will be None if no clobber is required or if we clobbered without + error. + """ + assert cwd + cwd = mozpath.normpath(cwd) + + if not self.clobber_needed(): + print("Clobber not needed.", file=fh) + return False, False, None + + # So a clobber is needed. We only perform a clobber if we are + # allowed to perform an automatic clobber (off by default) and if the + # current directory is not under the object directory. The latter is + # because operating systems, filesystems, and shell can throw fits + # if the current working directory is deleted from under you. While it + # can work in some scenarios, we take the conservative approach and + # never try. + if not allow_auto: + return ( + True, + False, + self._message( + "Automatic clobbering is not enabled\n" + ' (add "mk_add_options AUTOCLOBBER=1" to your ' + "mozconfig)." + ), + ) + + if cwd.startswith(self.topobjdir) and cwd != self.topobjdir: + return ( + True, + False, + self._message( + "Cannot clobber while the shell is inside the object directory." + ), + ) + + print("Automatically clobbering %s" % self.topobjdir, file=fh) + try: + self.remove_objdir(False) + print("Successfully completed auto clobber.", file=fh) + return True, True, None + except IOError as error: + return ( + True, + False, + self._message("Error when automatically clobbering: " + str(error)), + ) + + def _message(self, reason): + lines = [" " + line for line in self.clobber_cause()] + + return CLOBBER_MESSAGE.format( + clobber_reason="\n".join(lines), + no_reason=" " + reason, + clobber_file=self.obj_clobber, + ) diff --git a/python/mozbuild/mozbuild/doctor.py b/python/mozbuild/mozbuild/doctor.py new file mode 100644 index 0000000000..315a00e7c0 --- /dev/null +++ b/python/mozbuild/mozbuild/doctor.py @@ -0,0 +1,605 @@ +# 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/. + +import enum +import locale +import os +import socket +import subprocess +import sys +from pathlib import Path +from typing import Callable, List, Optional, Union + +import attr +import mozpack.path as mozpath +import mozversioncontrol +import psutil +import requests +from packaging.version import Version + +# Minimum recommended logical processors in system. +PROCESSORS_THRESHOLD = 4 + +# Minimum recommended total system memory, in gigabytes. +MEMORY_THRESHOLD = 7.4 + +# Minimum recommended free space on each disk, in gigabytes. +FREESPACE_THRESHOLD = 10 + +# Latest MozillaBuild version. +LATEST_MOZILLABUILD_VERSION = Version("4.0") + +DISABLE_LASTACCESS_WIN = """ +Disable the last access time feature? +This improves the speed of file and +directory access by deferring Last Access Time modification on disk by up to an +hour. Backup programs that rely on this feature may be affected. +https://technet.microsoft.com/en-us/library/cc785435.aspx +""" + +COMPILED_LANGUAGE_FILE_EXTENSIONS = [ + ".cc", + ".cxx", + ".c", + ".cpp", + ".h", + ".hpp", + ".rs", + ".rlib", + ".mk", +] + + +def get_mount_point(path: str) -> str: + """Return the mount point for a given path.""" + while path != "/" and not os.path.ismount(path): + path = mozpath.abspath(mozpath.join(path, os.pardir)) + return path + + +class CheckStatus(enum.Enum): + # Check is okay. + OK = enum.auto() + # We found an issue. + WARNING = enum.auto() + # We found an issue that will break build/configure/etc. + FATAL = enum.auto() + # The check was skipped. + SKIPPED = enum.auto() + + +@attr.s +class DoctorCheck: + # Name of the check. + name = attr.ib() + # Lines to display on screen. + display_text = attr.ib() + # `CheckStatus` for this given check. + status = attr.ib() + # Function to be called to fix the issues, if applicable. + fix = attr.ib(default=None) + + +CHECKS = {} + + +def check(func: Callable): + """Decorator that registers a function as a doctor check. + + The function should return a `DoctorCheck` or be an iterator of + checks. + """ + CHECKS[func.__name__] = func + + +@check +def dns(**kwargs) -> DoctorCheck: + """Check DNS is queryable.""" + try: + socket.getaddrinfo("mozilla.org", 80) + return DoctorCheck( + name="dns", + status=CheckStatus.OK, + display_text=["DNS query for mozilla.org completed successfully."], + ) + + except socket.gaierror: + return DoctorCheck( + name="dns", + status=CheckStatus.FATAL, + display_text=["Could not query DNS for mozilla.org."], + ) + + +@check +def internet(**kwargs) -> DoctorCheck: + """Check the internet is reachable via HTTPS.""" + try: + resp = requests.get("https://mozilla.org") + resp.raise_for_status() + + return DoctorCheck( + name="internet", + status=CheckStatus.OK, + display_text=["Internet is reachable."], + ) + + except Exception: + return DoctorCheck( + name="internet", + status=CheckStatus.FATAL, + display_text=["Could not reach a known website via HTTPS."], + ) + + +@check +def ssh(**kwargs) -> DoctorCheck: + """Check the status of `ssh hg.mozilla.org` for common errors.""" + try: + # We expect this command to return exit code 1 even when we hit + # the successful code path, since we don't specify a `pash` command. + proc = subprocess.run( + ["ssh", "hg.mozilla.org"], + encoding="utf-8", + stderr=subprocess.PIPE, + stdout=subprocess.PIPE, + ) + + # Command output from a successful `pash` run. + if "has privileges to access Mercurial over" in proc.stdout: + return DoctorCheck( + name="ssh", + status=CheckStatus.OK, + display_text=["SSH is properly configured for access to hg."], + ) + + if "Permission denied" in proc.stdout: + # Parse proc.stdout for username, which looks like: + # `<username>@hg.mozilla.org: Permission denied (reason)` + login_string = proc.stdout.split()[0] + username, _host = login_string.split("@hg.mozilla.org") + + # `<username>` should be an email. + if "@" not in username: + return DoctorCheck( + name="ssh", + status=CheckStatus.FATAL, + display_text=[ + "SSH username `{}` is not an email address.".format(username), + "hg.mozilla.org logins should be in the form `user@domain.com`.", + ], + ) + + return DoctorCheck( + name="ssh", + status=CheckStatus.WARNING, + display_text=[ + "SSH username `{}` does not have permission to push to " + "hg.mozilla.org.".format(username) + ], + ) + + if "Mercurial access is currently disabled on your account" in proc.stdout: + return DoctorCheck( + name="ssh", + status=CheckStatus.FATAL, + display_text=[ + "You previously had push access to hgmo, but due to inactivity", + "your access was revoked. Please file a bug in Bugzilla under", + "`Infrastructure & Operations :: Infrastructure: LDAP` to request", + "access.", + ], + ) + + return DoctorCheck( + name="ssh", + status=CheckStatus.WARNING, + display_text=[ + "Unexpected output from `ssh hg.mozilla.org`:", + proc.stdout, + ], + ) + + except subprocess.CalledProcessError: + return DoctorCheck( + name="ssh", + status=CheckStatus.WARNING, + display_text=["Could not run `ssh hg.mozilla.org`."], + ) + + +@check +def cpu(**kwargs) -> DoctorCheck: + """Check the host machine has the recommended processing power to develop Firefox.""" + cpu_count = psutil.cpu_count() + if cpu_count < PROCESSORS_THRESHOLD: + status = CheckStatus.WARNING + desc = "%d logical processors detected, <%d" % (cpu_count, PROCESSORS_THRESHOLD) + else: + status = CheckStatus.OK + desc = "%d logical processors detected, >=%d" % ( + cpu_count, + PROCESSORS_THRESHOLD, + ) + + return DoctorCheck(name="cpu", display_text=[desc], status=status) + + +@check +def memory(**kwargs) -> DoctorCheck: + """Check the host machine has the recommended memory to develop Firefox.""" + memory = psutil.virtual_memory().total + # Convert to gigabytes. + memory_GB = memory / 1024**3.0 + if memory_GB < MEMORY_THRESHOLD: + status = CheckStatus.WARNING + desc = "%.1fGB of physical memory, <%.1fGB" % (memory_GB, MEMORY_THRESHOLD) + else: + status = CheckStatus.OK + desc = "%.1fGB of physical memory, >%.1fGB" % (memory_GB, MEMORY_THRESHOLD) + + return DoctorCheck(name="memory", display_text=[desc], status=status) + + +@check +def storage_freespace(topsrcdir: str, topobjdir: str, **kwargs) -> List[DoctorCheck]: + """Check the host machine has the recommended disk space to develop Firefox.""" + topsrcdir_mount = get_mount_point(topsrcdir) + topobjdir_mount = get_mount_point(topobjdir) + + mounts = [ + ("topsrcdir", topsrcdir, topsrcdir_mount), + ("topobjdir", topobjdir, topobjdir_mount), + ] + + mountpoint_line = topsrcdir_mount != topobjdir_mount + checks = [] + + for purpose, path, mount in mounts: + if not mountpoint_line: + mountpoint_line = True + continue + + desc = ["%s = %s" % (purpose, path)] + + try: + usage = psutil.disk_usage(mount) + freespace, size = usage.free, usage.total + freespace_GB = freespace / 1024**3 + size_GB = size / 1024**3 + if freespace_GB < FREESPACE_THRESHOLD: + status = CheckStatus.WARNING + desc.append( + "mountpoint = %s\n%dGB of %dGB free, <%dGB" + % (mount, freespace_GB, size_GB, FREESPACE_THRESHOLD) + ) + else: + status = CheckStatus.OK + desc.append( + "mountpoint = %s\n%dGB of %dGB free, >=%dGB" + % (mount, freespace_GB, size_GB, FREESPACE_THRESHOLD) + ) + + except OSError: + status = CheckStatus.FATAL + desc.append("path invalid") + + checks.append( + DoctorCheck(name="%s mount check" % mount, status=status, display_text=desc) + ) + + return checks + + +def fix_lastaccess_win(): + """Run `fsutil` to fix lastaccess behaviour.""" + try: + print("Disabling filesystem lastaccess") + + command = ["fsutil", "behavior", "set", "disablelastaccess", "1"] + subprocess.check_output(command) + + print("Filesystem lastaccess disabled.") + + except subprocess.CalledProcessError: + print("Could not disable filesystem lastaccess.") + + +@check +def fs_lastaccess( + topsrcdir: str, topobjdir: str, **kwargs +) -> Union[DoctorCheck, List[DoctorCheck]]: + """Check for the `lastaccess` behaviour on the filsystem, which can slow + down filesystem operations.""" + if sys.platform.startswith("win"): + # See 'fsutil behavior': + # https://technet.microsoft.com/en-us/library/cc785435.aspx + try: + command = ["fsutil", "behavior", "query", "disablelastaccess"] + fsutil_output = subprocess.check_output(command, encoding="utf-8") + disablelastaccess = int(fsutil_output.partition("=")[2][1]) + except subprocess.CalledProcessError: + return DoctorCheck( + name="lastaccess", + status=CheckStatus.WARNING, + display_text=["unable to check lastaccess behavior"], + ) + + if disablelastaccess in {1, 3}: + return DoctorCheck( + name="lastaccess", + status=CheckStatus.OK, + display_text=["lastaccess disabled systemwide"], + ) + elif disablelastaccess in {0, 2}: + return DoctorCheck( + name="lastaccess", + status=CheckStatus.WARNING, + display_text=["lastaccess enabled"], + fix=fix_lastaccess_win, + ) + + # `disablelastaccess` should be a value between 0-3. + return DoctorCheck( + name="lastaccess", + status=CheckStatus.WARNING, + display_text=["Could not parse `fsutil` for lastaccess behavior."], + ) + + elif any( + sys.platform.startswith(prefix) for prefix in ["freebsd", "linux", "openbsd"] + ): + topsrcdir_mount = get_mount_point(topsrcdir) + topobjdir_mount = get_mount_point(topobjdir) + mounts = [ + ("topsrcdir", topsrcdir, topsrcdir_mount), + ("topobjdir", topobjdir, topobjdir_mount), + ] + + common_mountpoint = topsrcdir_mount == topobjdir_mount + + mount_checks = [] + for _purpose, _path, mount in mounts: + mount_checks.append(check_mount_lastaccess(mount)) + if common_mountpoint: + break + + return mount_checks + + # Return "SKIPPED" if this test is not relevant. + return DoctorCheck( + name="lastaccess", + display_text=["lastaccess not relevant for this platform."], + status=CheckStatus.SKIPPED, + ) + + +def check_mount_lastaccess(mount: str) -> DoctorCheck: + """Check `lastaccess` behaviour for a Linux mount.""" + partitions = psutil.disk_partitions(all=True) + atime_opts = {"atime", "noatime", "relatime", "norelatime"} + option = "" + fstype = "" + for partition in partitions: + if partition.mountpoint == mount: + mount_opts = set(partition.opts.split(",")) + intersection = list(atime_opts & mount_opts) + fstype = partition.fstype + if len(intersection) == 1: + option = intersection[0] + break + + if fstype == "tmpfs": + status = CheckStatus.OK + desc = "%s is a tmpfs so noatime/reltime is not needed" % (mount) + elif not option: + status = CheckStatus.WARNING + if sys.platform.startswith("linux"): + option = "noatime/relatime" + else: + option = "noatime" + desc = "%s has no explicit %s mount option" % (mount, option) + elif option == "atime" or option == "norelatime": + status = CheckStatus.WARNING + desc = "%s has %s mount option" % (mount, option) + elif option == "noatime" or option == "relatime": + status = CheckStatus.OK + desc = "%s has %s mount option" % (mount, option) + + return DoctorCheck( + name="%s mount lastaccess" % mount, status=status, display_text=[desc] + ) + + +@check +def mozillabuild(**kwargs) -> DoctorCheck: + """Check that MozillaBuild is the latest version.""" + if not sys.platform.startswith("win"): + return DoctorCheck( + name="mozillabuild", + status=CheckStatus.SKIPPED, + display_text=["Non-Windows platform, MozillaBuild not relevant"], + ) + + MOZILLABUILD = mozpath.normpath(os.environ.get("MOZILLABUILD", "")) + if not MOZILLABUILD or not os.path.exists(MOZILLABUILD): + return DoctorCheck( + name="mozillabuild", + status=CheckStatus.WARNING, + display_text=["Not running under MozillaBuild."], + ) + + try: + with open(mozpath.join(MOZILLABUILD, "VERSION"), "r") as fh: + local_version = fh.readline() + + if not local_version: + return DoctorCheck( + name="mozillabuild", + status=CheckStatus.WARNING, + display_text=["Could not get local MozillaBuild version."], + ) + + if Version(local_version) < LATEST_MOZILLABUILD_VERSION: + status = CheckStatus.WARNING + desc = "MozillaBuild %s in use, <%s" % ( + local_version, + LATEST_MOZILLABUILD_VERSION, + ) + + else: + status = CheckStatus.OK + desc = "MozillaBuild %s in use" % local_version + + except (IOError, ValueError): + status = CheckStatus.FATAL + desc = "MozillaBuild version not found" + + return DoctorCheck(name="mozillabuild", status=status, display_text=[desc]) + + +@check +def bad_locale_utf8(**kwargs) -> DoctorCheck: + """Check to detect the invalid locale `UTF-8` on pre-3.8 Python.""" + if sys.version_info >= (3, 8): + return DoctorCheck( + name="utf8 locale", + status=CheckStatus.SKIPPED, + display_text=["Python version has fixed utf-8 locale bug."], + ) + + try: + # This line will attempt to get and parse the locale. + locale.getdefaultlocale() + + return DoctorCheck( + name="utf8 locale", + status=CheckStatus.OK, + display_text=["Python's locale is set to a valid value."], + ) + except ValueError: + return DoctorCheck( + name="utf8 locale", + status=CheckStatus.FATAL, + display_text=[ + "Your Python is using an invalid value for its locale.", + "Either update Python to version 3.8+, or set the following variables in ", + "your environment:", + " export LC_ALL=en_US.UTF-8", + " export LANG=en_US.UTF-8", + ], + ) + + +@check +def artifact_build( + topsrcdir: str, configure_args: Optional[List[str]], **kwargs +) -> DoctorCheck: + """Check that if Artifact Builds are enabled, that no + source files that would not be compiled are changed""" + + if configure_args is None or "--enable-artifact-builds" not in configure_args: + return DoctorCheck( + name="artifact_build", + status=CheckStatus.SKIPPED, + display_text=[ + "Artifact Builds are not enabled. No need to proceed checking for changed files." + ], + ) + + repo = mozversioncontrol.get_repository_object(topsrcdir) + changed_files = [ + Path(file) + for file in set(repo.get_outgoing_files()) | set(repo.get_changed_files()) + ] + + compiled_language_files_changed = "" + for file in changed_files: + if ( + file.suffix in COMPILED_LANGUAGE_FILE_EXTENSIONS + or file.stem.lower() == "makefile" + and not file.suffix == ".py" + ): + compiled_language_files_changed += ' - "' + str(file) + '"\n' + + if compiled_language_files_changed: + return DoctorCheck( + name="artifact_build", + status=CheckStatus.FATAL, + display_text=[ + "Artifact Builds are enabled, but the following files from compiled languages " + f"have been modified: \n{compiled_language_files_changed}\nThese files will " + "not be compiled, and your changes will not be realized in the build output." + "\n\nIf you want these changes to be realized, you should re-run './mach " + 'boostrap` and select a build that does not state "Artifact Mode".' + "\nFor additional information on Artifact Builds see: " + "https://firefox-source-docs.mozilla.org/contributing/build/" + "artifact_builds.html" + ], + ) + + return DoctorCheck( + name="artifact_build", + status=CheckStatus.OK, + display_text=["No Artifact Build conflicts found."], + ) + + +def run_doctor(fix: bool = False, verbose: bool = False, **kwargs) -> int: + """Run the doctor checks. + + If `fix` is `True`, run fixing functions for issues that can be resolved + automatically. + + By default, only print output from checks that result in a warning or + fatal issue. `verbose` will cause all output to be printed to the screen. + """ + issues_found = False + + fixes = [] + for _name, check_func in CHECKS.items(): + results = check_func(**kwargs) + + if isinstance(results, DoctorCheck): + results = [results] + + for result in results: + if result.status == CheckStatus.SKIPPED and not verbose: + continue + + if result.status != CheckStatus.OK: + # If we ever have a non-OK status, we shouldn't print + # the "No issues detected" line. + issues_found = True + + if result.status != CheckStatus.OK or verbose: + print("\n".join(result.display_text)) + + if result.fix: + fixes.append(result.fix) + + if not issues_found: + print("No issues detected.") + return 0 + + # If we can fix something but the user didn't ask us to, advise + # them to run with `--fix`. + if not fix: + if fixes: + print( + "Some of the issues found can be fixed; run " + "`./mach doctor --fix` to fix them." + ) + return 1 + + # Attempt to run the fix functions. + fixer_fail = 0 + for fixer in fixes: + try: + fixer() + except Exception: + fixer_fail = 1 + pass + + return fixer_fail diff --git a/python/mozbuild/mozbuild/dotproperties.py b/python/mozbuild/mozbuild/dotproperties.py new file mode 100644 index 0000000000..9b615cc43f --- /dev/null +++ b/python/mozbuild/mozbuild/dotproperties.py @@ -0,0 +1,86 @@ +# 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/. + +# This file contains utility functions for reading .properties files + +import codecs +import re +import sys + +import six + +if sys.version_info[0] == 3: + str_type = str +else: + str_type = basestring + + +class DotProperties: + r"""A thin representation of a key=value .properties file.""" + + def __init__(self, file=None): + self._properties = {} + if file: + self.update(file) + + def update(self, file): + """Updates properties from a file name or file-like object. + + Ignores empty lines and comment lines.""" + + if isinstance(file, str_type): + f = codecs.open(file, "r", "utf-8") + else: + f = file + + for l in f.readlines(): + line = l.strip() + if not line or line.startswith("#"): + continue + (k, v) = re.split("\s*=\s*", line, 1) + self._properties[k] = v + + def get(self, key, default=None): + return self._properties.get(key, default) + + def get_list(self, prefix): + """Turns {'list.0':'foo', 'list.1':'bar'} into ['foo', 'bar']. + + Returns [] to indicate an empty or missing list.""" + + if not prefix.endswith("."): + prefix = prefix + "." + indexes = [] + for k, v in six.iteritems(self._properties): + if not k.startswith(prefix): + continue + key = k[len(prefix) :] + if "." in key: + # We have something like list.sublist.0. + continue + indexes.append(int(key)) + return [self._properties[prefix + str(index)] for index in sorted(indexes)] + + def get_dict(self, prefix, required_keys=[]): + """Turns {'foo.title':'title', ...} into {'title':'title', ...}. + + If ``|required_keys|`` is present, it must be an iterable of required key + names. If a required key is not present, ValueError is thrown. + + Returns {} to indicate an empty or missing dict.""" + + if not prefix.endswith("."): + prefix = prefix + "." + + D = dict( + (k[len(prefix) :], v) + for k, v in six.iteritems(self._properties) + if k.startswith(prefix) and "." not in k[len(prefix) :] + ) + + for required_key in required_keys: + if required_key not in D: + raise ValueError("Required key %s not present" % required_key) + + return D diff --git a/python/mozbuild/mozbuild/faster_daemon.py b/python/mozbuild/mozbuild/faster_daemon.py new file mode 100644 index 0000000000..13fb07a79c --- /dev/null +++ b/python/mozbuild/mozbuild/faster_daemon.py @@ -0,0 +1,328 @@ +# 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/. + +""" +Use pywatchman to watch source directories and perform partial +``mach build faster`` builds. +""" + +import datetime +import sys +import time + +import mozpack.path as mozpath + +# Watchman integration cribbed entirely from +# https://github.com/facebook/watchman/blob/19aebfebb0b5b0b5174b3914a879370ffc5dac37/python/bin/watchman-wait +import pywatchman +from mozpack.copier import FileCopier +from mozpack.manifests import InstallManifest + +import mozbuild.util +from mozbuild.backend import get_backend_class + + +def print_line(prefix, m, now=None): + now = now or datetime.datetime.utcnow() + print("[%s %sZ] %s" % (prefix, now.isoformat(), m)) + + +def print_copy_result(elapsed, destdir, result, verbose=True): + COMPLETE = ( + "Elapsed: {elapsed:.2f}s; From {dest}: Kept {existing} existing; " + "Added/updated {updated}; " + "Removed {rm_files} files and {rm_dirs} directories." + ) + + print_line( + "watch", + COMPLETE.format( + elapsed=elapsed, + dest=destdir, + existing=result.existing_files_count, + updated=result.updated_files_count, + rm_files=result.removed_files_count, + rm_dirs=result.removed_directories_count, + ), + ) + + +class FasterBuildException(Exception): + def __init__(self, message, cause): + Exception.__init__(self, message) + self.cause = cause + + +class FasterBuildChange(object): + def __init__(self): + self.unrecognized = set() + self.input_to_outputs = {} + self.output_to_inputs = {} + + +class Daemon(object): + def __init__(self, config_environment): + self.config_environment = config_environment + self._client = None + + @property + def defines(self): + defines = dict(self.config_environment.acdefines) + # These additions work around warts in the build system: see + # http://searchfox.org/mozilla-central/rev/ad093e98f42338effe2e2513e26c3a311dd96422/config/faster/rules.mk#92-93 + defines.update( + { + "AB_CD": "en-US", + } + ) + return defines + + @mozbuild.util.memoized_property + def file_copier(self): + # TODO: invalidate the file copier when the build system + # itself changes, i.e., the underlying unified manifest + # changes. + file_copier = FileCopier() + + unified_manifest = InstallManifest( + mozpath.join( + self.config_environment.topobjdir, "faster", "unified_install_dist_bin" + ) + ) + + unified_manifest.populate_registry(file_copier, defines_override=self.defines) + + return file_copier + + def subscribe_to_topsrcdir(self): + self.subscribe_to_dir("topsrcdir", self.config_environment.topsrcdir) + + def subscribe_to_dir(self, name, dir_to_watch): + query = { + "empty_on_fresh_instance": True, + "expression": [ + "allof", + ["type", "f"], + [ + "not", + [ + "anyof", + ["dirname", ".hg"], + ["name", ".hg", "wholename"], + ["dirname", ".git"], + ["name", ".git", "wholename"], + ], + ], + ], + "fields": ["name"], + } + watch = self.client.query("watch-project", dir_to_watch) + if "warning" in watch: + print("WARNING: ", watch["warning"], file=sys.stderr) + + root = watch["watch"] + if "relative_path" in watch: + query["relative_root"] = watch["relative_path"] + + # Get the initial clock value so that we only get updates. + # Wait 30s to allow for slow Windows IO. See + # https://facebook.github.io/watchman/docs/cmd/clock.html. + query["since"] = self.client.query("clock", root, {"sync_timeout": 30000})[ + "clock" + ] + + return self.client.query("subscribe", root, name, query) + + def changed_files(self): + # In theory we can parse just the result variable here, but + # the client object will accumulate all subscription results + # over time, so we ask it to remove and return those values. + files = set() + + data = self.client.getSubscription("topsrcdir") + if data: + for dat in data: + files |= set( + [ + mozpath.normpath( + mozpath.join(self.config_environment.topsrcdir, f) + ) + for f in dat.get("files", []) + ] + ) + + return files + + def incremental_copy(self, copier, force=False, verbose=True): + # Just like the 'repackage' target in browser/app/Makefile.in. + if "cocoa" == self.config_environment.substs["MOZ_WIDGET_TOOLKIT"]: + bundledir = mozpath.join( + self.config_environment.topobjdir, + "dist", + self.config_environment.substs["MOZ_MACBUNDLE_NAME"], + "Contents", + "Resources", + ) + start = time.monotonic() + result = copier.copy( + bundledir, + skip_if_older=not force, + remove_unaccounted=False, + remove_all_directory_symlinks=False, + remove_empty_directories=False, + ) + print_copy_result( + time.monotonic() - start, bundledir, result, verbose=verbose + ) + + destdir = mozpath.join(self.config_environment.topobjdir, "dist", "bin") + start = time.monotonic() + result = copier.copy( + destdir, + skip_if_older=not force, + remove_unaccounted=False, + remove_all_directory_symlinks=False, + remove_empty_directories=False, + ) + print_copy_result(time.monotonic() - start, destdir, result, verbose=verbose) + + def input_changes(self, verbose=True): + """ + Return an iterator of `FasterBuildChange` instances as inputs + to the faster build system change. + """ + + # TODO: provide the debug diagnostics we want: this print is + # not immediately before the watch. + if verbose: + print_line("watch", "Connecting to watchman") + # TODO: figure out why a large timeout is required for the + # client, and a robust strategy for retrying timed out + # requests. + self.client = pywatchman.client(timeout=5.0) + + try: + if verbose: + print_line("watch", "Checking watchman capabilities") + # TODO: restrict these capabilities to the minimal set. + self.client.capabilityCheck( + required=[ + "clock-sync-timeout", + "cmd-watch-project", + "term-dirname", + "wildmatch", + ] + ) + + if verbose: + print_line( + "watch", + "Subscribing to {}".format(self.config_environment.topsrcdir), + ) + self.subscribe_to_topsrcdir() + if verbose: + print_line( + "watch", "Watching {}".format(self.config_environment.topsrcdir) + ) + + input_to_outputs = self.file_copier.input_to_outputs_tree() + for input, outputs in input_to_outputs.items(): + if not outputs: + raise Exception( + "Refusing to watch input ({}) with no outputs".format(input) + ) + + while True: + try: + self.client.receive() + + changed = self.changed_files() + if not changed: + continue + + result = FasterBuildChange() + + for change in changed: + if change in input_to_outputs: + result.input_to_outputs[change] = set( + input_to_outputs[change] + ) + else: + result.unrecognized.add(change) + + for input, outputs in result.input_to_outputs.items(): + for output in outputs: + if output not in result.output_to_inputs: + result.output_to_inputs[output] = set() + result.output_to_inputs[output].add(input) + + yield result + + except pywatchman.SocketTimeout: + # Let's check to see if we're still functional. + self.client.query("version") + + except pywatchman.CommandError as e: + # Abstract away pywatchman errors. + raise FasterBuildException( + e, + "Command error using pywatchman to watch {}".format( + self.config_environment.topsrcdir + ), + ) + + except pywatchman.SocketTimeout as e: + # Abstract away pywatchman errors. + raise FasterBuildException( + e, + "Socket timeout using pywatchman to watch {}".format( + self.config_environment.topsrcdir + ), + ) + + finally: + self.client.close() + + def output_changes(self, verbose=True): + """ + Return an iterator of `FasterBuildChange` instances as outputs + from the faster build system are updated. + """ + for change in self.input_changes(verbose=verbose): + now = datetime.datetime.utcnow() + + for unrecognized in sorted(change.unrecognized): + print_line("watch", "! {}".format(unrecognized), now=now) + + all_outputs = set() + for input in sorted(change.input_to_outputs): + outputs = change.input_to_outputs[input] + + print_line("watch", "< {}".format(input), now=now) + for output in sorted(outputs): + print_line("watch", "> {}".format(output), now=now) + all_outputs |= outputs + + if all_outputs: + partial_copier = FileCopier() + for output in all_outputs: + partial_copier.add(output, self.file_copier[output]) + + self.incremental_copy(partial_copier, force=True, verbose=verbose) + yield change + + def watch(self, verbose=True): + try: + active_backend = self.config_environment.substs.get( + "BUILD_BACKENDS", [None] + )[0] + if active_backend: + backend_cls = get_backend_class(active_backend)(self.config_environment) + except Exception: + backend_cls = None + + for change in self.output_changes(verbose=verbose): + # Try to run the active build backend's post-build step, if possible. + if backend_cls: + backend_cls.post_build(self.config_environment, None, 1, False, 0) diff --git a/python/mozbuild/mozbuild/frontend/__init__.py b/python/mozbuild/mozbuild/frontend/__init__.py new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/python/mozbuild/mozbuild/frontend/__init__.py diff --git a/python/mozbuild/mozbuild/frontend/context.py b/python/mozbuild/mozbuild/frontend/context.py new file mode 100644 index 0000000000..551ea00feb --- /dev/null +++ b/python/mozbuild/mozbuild/frontend/context.py @@ -0,0 +1,3143 @@ +# 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/. + +###################################################################### +# DO NOT UPDATE THIS FILE WITHOUT SIGN-OFF FROM A BUILD MODULE PEER. # +###################################################################### + +r"""This module contains the data structure (context) holding the configuration +from a moz.build. The data emitted by the frontend derives from those contexts. + +It also defines the set of variables and functions available in moz.build. +If you are looking for the absolute authority on what moz.build files can +contain, you've come to the right place. +""" + +import itertools +import operator +import os +from collections import Counter, OrderedDict +from types import FunctionType + +import mozpack.path as mozpath +import six + +from mozbuild.util import ( + HierarchicalStringList, + ImmutableStrictOrderingOnAppendList, + KeyedDefaultDict, + List, + ReadOnlyKeyedDefaultDict, + StrictOrderingOnAppendList, + StrictOrderingOnAppendListWithAction, + StrictOrderingOnAppendListWithFlagsFactory, + TypedList, + TypedNamedTuple, + memoize, + memoized_property, +) + +from .. import schedules +from ..testing import read_manifestparser_manifest, read_reftest_manifest + + +class ContextDerivedValue(object): + """Classes deriving from this one receive a special treatment in a + Context. See Context documentation. + """ + + __slots__ = () + + +class Context(KeyedDefaultDict): + """Represents a moz.build configuration context. + + Instances of this class are filled by the execution of sandboxes. + At the core, a Context is a dict, with a defined set of possible keys we'll + call variables. Each variable is associated with a type. + + When reading a value for a given key, we first try to read the existing + value. If a value is not found and it is defined in the allowed variables + set, we return a new instance of the class for that variable. We don't + assign default instances until they are accessed because this makes + debugging the end-result much simpler. Instead of a data structure with + lots of empty/default values, you have a data structure with only the + values that were read or touched. + + Instances of variables classes are created by invoking ``class_name()``, + except when class_name derives from ``ContextDerivedValue`` or + ``SubContext``, in which case ``class_name(instance_of_the_context)`` or + ``class_name(self)`` is invoked. A value is added to those calls when + instances are created during assignment (setitem). + + allowed_variables is a dict of the variables that can be set and read in + this context instance. Keys in this dict are the strings representing keys + in this context which are valid. Values are tuples of stored type, + assigned type, default value, a docstring describing the purpose of the + variable, and a tier indicator (see comment above the VARIABLES declaration + in this module). + + config is the ConfigEnvironment for this context. + """ + + def __init__(self, allowed_variables={}, config=None, finder=None): + self._allowed_variables = allowed_variables + self.main_path = None + self.current_path = None + # There aren't going to be enough paths for the performance of scanning + # a list to be a problem. + self._all_paths = [] + self.config = config + self._sandbox = None + self._finder = finder + KeyedDefaultDict.__init__(self, self._factory) + + def push_source(self, path): + """Adds the given path as source of the data from this context and make + it the current path for the context.""" + assert os.path.isabs(path) + if not self.main_path: + self.main_path = path + else: + # Callers shouldn't push after main_path has been popped. + assert self.current_path + self.current_path = path + # The same file can be pushed twice, so don't remove any previous + # occurrence. + self._all_paths.append(path) + + def pop_source(self): + """Get back to the previous current path for the context.""" + assert self.main_path + assert self.current_path + last = self._all_paths.pop() + # Keep the popped path in the list of all paths, but before the main + # path so that it's not popped again. + self._all_paths.insert(0, last) + if last == self.main_path: + self.current_path = None + else: + self.current_path = self._all_paths[-1] + return last + + def add_source(self, path): + """Adds the given path as source of the data from this context.""" + assert os.path.isabs(path) + if not self.main_path: + self.main_path = self.current_path = path + # Insert at the beginning of the list so that it's always before the + # main path. + if path not in self._all_paths: + self._all_paths.insert(0, path) + + @property + def error_is_fatal(self): + """Returns True if the error function should be fatal.""" + return self.config and getattr(self.config, "error_is_fatal", True) + + @property + def all_paths(self): + """Returns all paths ever added to the context.""" + return set(self._all_paths) + + @property + def source_stack(self): + """Returns the current stack of pushed sources.""" + if not self.current_path: + return [] + return self._all_paths[self._all_paths.index(self.main_path) :] + + @memoized_property + def objdir(self): + return mozpath.join(self.config.topobjdir, self.relobjdir).rstrip("/") + + @memoize + def _srcdir(self, path): + return mozpath.join(self.config.topsrcdir, self._relsrcdir(path)).rstrip("/") + + @property + def srcdir(self): + return self._srcdir(self.current_path or self.main_path) + + @memoize + def _relsrcdir(self, path): + return mozpath.relpath(mozpath.dirname(path), self.config.topsrcdir) + + @property + def relsrcdir(self): + assert self.main_path + return self._relsrcdir(self.current_path or self.main_path) + + @memoized_property + def relobjdir(self): + assert self.main_path + return mozpath.relpath(mozpath.dirname(self.main_path), self.config.topsrcdir) + + def _factory(self, key): + """Function called when requesting a missing key.""" + defaults = self._allowed_variables.get(key) + if not defaults: + raise KeyError("global_ns", "get_unknown", key) + + # If the default is specifically a lambda (or, rather, any function + # --but not a class that can be called), then it is actually a rule to + # generate the default that should be used. + default = defaults[0] + if issubclass(default, ContextDerivedValue): + return default(self) + else: + return default() + + def _validate(self, key, value, is_template=False): + """Validates whether the key is allowed and if the value's type + matches. + """ + stored_type, input_type, docs = self._allowed_variables.get( + key, (None, None, None) + ) + + if stored_type is None or not is_template and key in TEMPLATE_VARIABLES: + raise KeyError("global_ns", "set_unknown", key, value) + + # If the incoming value is not the type we store, we try to convert + # it to that type. This relies on proper coercion rules existing. This + # is the responsibility of whoever defined the symbols: a type should + # not be in the allowed set if the constructor function for the stored + # type does not accept an instance of that type. + if not isinstance(value, (stored_type, input_type)): + raise ValueError("global_ns", "set_type", key, value, input_type) + + return stored_type + + def __setitem__(self, key, value): + stored_type = self._validate(key, value) + + if not isinstance(value, stored_type): + if issubclass(stored_type, ContextDerivedValue): + value = stored_type(self, value) + else: + value = stored_type(value) + + return KeyedDefaultDict.__setitem__(self, key, value) + + def update(self, iterable={}, **kwargs): + """Like dict.update(), but using the context's setitem. + + This function is transactional: if setitem fails for one of the values, + the context is not updated at all.""" + if isinstance(iterable, dict): + iterable = iterable.items() + + update = {} + for key, value in itertools.chain(iterable, kwargs.items()): + stored_type = self._validate(key, value) + # Don't create an instance of stored_type if coercion is needed, + # until all values are validated. + update[key] = (value, stored_type) + for key, (value, stored_type) in update.items(): + if not isinstance(value, stored_type): + update[key] = stored_type(value) + else: + update[key] = value + KeyedDefaultDict.update(self, update) + + +class TemplateContext(Context): + def __init__(self, template=None, allowed_variables={}, config=None): + self.template = template + super(TemplateContext, self).__init__(allowed_variables, config) + + def _validate(self, key, value): + return Context._validate(self, key, value, True) + + +class SubContext(Context, ContextDerivedValue): + """A Context derived from another Context. + + Sub-contexts are intended to be used as context managers. + + Sub-contexts inherit paths and other relevant state from the parent + context. + """ + + def __init__(self, parent): + assert isinstance(parent, Context) + + Context.__init__(self, allowed_variables=self.VARIABLES, config=parent.config) + + # Copy state from parent. + for p in parent.source_stack: + self.push_source(p) + self._sandbox = parent._sandbox + + def __enter__(self): + if not self._sandbox or self._sandbox() is None: + raise Exception("a sandbox is required") + + self._sandbox().push_subcontext(self) + + def __exit__(self, exc_type, exc_value, traceback): + self._sandbox().pop_subcontext(self) + + +class InitializedDefines(ContextDerivedValue, OrderedDict): + def __init__(self, context, value=None): + OrderedDict.__init__(self) + for define in context.config.substs.get("MOZ_DEBUG_DEFINES", ()): + self[define] = 1 + if value: + if not isinstance(value, OrderedDict): + raise ValueError("Can only initialize with another OrderedDict") + self.update(value) + + def update(self, *other, **kwargs): + # Since iteration over non-ordered dicts is non-deterministic, this dict + # will be populated in an unpredictable order unless the argument to + # update() is also ordered. (It's important that we maintain this + # invariant so we can be sure that running `./mach build-backend` twice + # in a row without updating any files in the workspace generates exactly + # the same output.) + if kwargs: + raise ValueError("Cannot call update() with kwargs") + if other: + if not isinstance(other[0], OrderedDict): + raise ValueError("Can only call update() with another OrderedDict") + return super(InitializedDefines, self).update(*other, **kwargs) + raise ValueError("No arguments passed to update()") + + +class BaseCompileFlags(ContextDerivedValue, dict): + def __init__(self, context): + self._context = context + + klass_name = self.__class__.__name__ + for k, v, build_vars in self.flag_variables: + if not isinstance(k, six.text_type): + raise ValueError("Flag %s for %s is not a string" % (k, klass_name)) + if not isinstance(build_vars, tuple): + raise ValueError( + "Build variables `%s` for %s in %s is not a tuple" + % (build_vars, k, klass_name) + ) + + self._known_keys = set(k for k, v, _ in self.flag_variables) + + # Providing defaults here doesn't play well with multiple templates + # modifying COMPILE_FLAGS from the same moz.build, because the merge + # done after the template runs can't tell which values coming from + # a template were set and which were provided as defaults. + template_name = getattr(context, "template", None) + if template_name in (None, "Gyp"): + dict.__init__( + self, + ( + (k, v if v is None else TypedList(six.text_type)(v)) + for k, v, _ in self.flag_variables + ), + ) + else: + dict.__init__(self) + + +class HostCompileFlags(BaseCompileFlags): + def __init__(self, context): + self._context = context + main_src_dir = mozpath.dirname(context.main_path) + + self.flag_variables = ( + ( + "HOST_CXXFLAGS", + context.config.substs.get("HOST_CXXFLAGS"), + ("HOST_CXXFLAGS", "HOST_CXX_LDFLAGS"), + ), + ( + "HOST_CFLAGS", + context.config.substs.get("HOST_CFLAGS"), + ("HOST_CFLAGS", "HOST_C_LDFLAGS"), + ), + ( + "HOST_OPTIMIZE", + self._optimize_flags(), + ("HOST_CFLAGS", "HOST_CXXFLAGS", "HOST_C_LDFLAGS", "HOST_CXX_LDFLAGS"), + ), + ("RTL", None, ("HOST_CFLAGS", "HOST_C_LDFLAGS")), + ("HOST_DEFINES", None, ("HOST_CFLAGS", "HOST_CXXFLAGS")), + ("MOZBUILD_HOST_CFLAGS", [], ("HOST_CFLAGS", "HOST_C_LDFLAGS")), + ("MOZBUILD_HOST_CXXFLAGS", [], ("HOST_CXXFLAGS", "HOST_CXX_LDFLAGS")), + ( + "BASE_INCLUDES", + ["-I%s" % main_src_dir, "-I%s" % context.objdir], + ("HOST_CFLAGS", "HOST_CXXFLAGS"), + ), + ("LOCAL_INCLUDES", None, ("HOST_CFLAGS", "HOST_CXXFLAGS")), + ( + "EXTRA_INCLUDES", + ["-I%s/dist/include" % context.config.topobjdir], + ("HOST_CFLAGS", "HOST_CXXFLAGS"), + ), + ( + "WARNINGS_CFLAGS", + context.config.substs.get("WARNINGS_HOST_CFLAGS"), + ("HOST_CFLAGS",), + ), + ( + "WARNINGS_CXXFLAGS", + context.config.substs.get("WARNINGS_HOST_CXXFLAGS"), + ("HOST_CXXFLAGS",), + ), + ) + BaseCompileFlags.__init__(self, context) + + def _optimize_flags(self): + # We don't use MOZ_OPTIMIZE here because we don't want + # --disable-optimize to make in-tree host tools slow. Doing so can + # potentially make build times significantly worse. + return self._context.config.substs.get("HOST_OPTIMIZE_FLAGS") or [] + + +class AsmFlags(BaseCompileFlags): + def __init__(self, context): + self._context = context + self.flag_variables = ( + ("DEFINES", None, ("SFLAGS",)), + ("LIBRARY_DEFINES", None, ("SFLAGS",)), + ("OS", context.config.substs.get("ASFLAGS"), ("ASFLAGS", "SFLAGS")), + ("DEBUG", self._debug_flags(), ("ASFLAGS", "SFLAGS")), + ("LOCAL_INCLUDES", None, ("SFLAGS",)), + ("MOZBUILD", None, ("ASFLAGS", "SFLAGS")), + ) + BaseCompileFlags.__init__(self, context) + + def _debug_flags(self): + debug_flags = [] + if self._context.config.substs.get( + "MOZ_DEBUG" + ) or self._context.config.substs.get("MOZ_DEBUG_SYMBOLS"): + if self._context.get("USE_NASM"): + if self._context.config.substs.get("OS_ARCH") == "WINNT": + debug_flags += ["-F", "cv8"] + elif self._context.config.substs.get("OS_ARCH") != "Darwin": + debug_flags += ["-F", "dwarf"] + elif ( + self._context.config.substs.get("OS_ARCH") == "WINNT" + and self._context.config.substs.get("TARGET_CPU") == "aarch64" + ): + # armasm64 accepts a paucity of options compared to ml/ml64. + pass + else: + debug_flags += self._context.config.substs.get( + "MOZ_DEBUG_FLAGS", "" + ).split() + return debug_flags + + +class LinkFlags(BaseCompileFlags): + def __init__(self, context): + self._context = context + + self.flag_variables = ( + ("OS", self._os_ldflags(), ("LDFLAGS",)), + ( + "MOZ_HARDENING_LDFLAGS", + context.config.substs.get("MOZ_HARDENING_LDFLAGS"), + ("LDFLAGS",), + ), + ("DEFFILE", None, ("LDFLAGS",)), + ("MOZBUILD", None, ("LDFLAGS",)), + ( + "FIX_LINK_PATHS", + context.config.substs.get("MOZ_FIX_LINK_PATHS"), + ("LDFLAGS",), + ), + ( + "OPTIMIZE", + ( + context.config.substs.get("MOZ_OPTIMIZE_LDFLAGS", []) + if context.config.substs.get("MOZ_OPTIMIZE") + else [] + ), + ("LDFLAGS",), + ), + ( + "CETCOMPAT", + ( + context.config.substs.get("MOZ_CETCOMPAT_LDFLAGS") + if context.config.substs.get("NIGHTLY_BUILD") + else [] + ), + ("LDFLAGS",), + ), + ) + BaseCompileFlags.__init__(self, context) + + def _os_ldflags(self): + flags = self._context.config.substs.get("OS_LDFLAGS", [])[:] + + if self._context.config.substs.get( + "MOZ_DEBUG" + ) or self._context.config.substs.get("MOZ_DEBUG_SYMBOLS"): + flags += self._context.config.substs.get("MOZ_DEBUG_LDFLAGS", []) + + # TODO: This is pretty convoluted, and isn't really a per-context thing, + # configure would be a better place to aggregate these. + if all( + [ + self._context.config.substs.get("OS_ARCH") == "WINNT", + not self._context.config.substs.get("GNU_CC"), + not self._context.config.substs.get("MOZ_DEBUG"), + ] + ): + if self._context.config.substs.get("MOZ_OPTIMIZE"): + flags.append("-OPT:REF,ICF") + + return flags + + +class TargetCompileFlags(BaseCompileFlags): + """Base class that encapsulates some common logic between CompileFlags and + WasmCompileFlags. + """ + + def _debug_flags(self): + if self._context.config.substs.get( + "MOZ_DEBUG" + ) or self._context.config.substs.get("MOZ_DEBUG_SYMBOLS"): + return self._context.config.substs.get("MOZ_DEBUG_FLAGS", "").split() + return [] + + def _warnings_as_errors(self): + warnings_as_errors = self._context.config.substs.get("WARNINGS_AS_ERRORS") + if warnings_as_errors: + return [warnings_as_errors] + + def _optimize_flags(self): + if not self._context.config.substs.get("MOZ_OPTIMIZE"): + return [] + optimize_flags = None + if self._context.config.substs.get("MOZ_PGO"): + optimize_flags = self._context.config.substs.get("MOZ_PGO_OPTIMIZE_FLAGS") + if not optimize_flags: + # If MOZ_PGO_OPTIMIZE_FLAGS is empty we fall back to + # MOZ_OPTIMIZE_FLAGS. Presently this occurs on Windows. + optimize_flags = self._context.config.substs.get("MOZ_OPTIMIZE_FLAGS") + return optimize_flags + + def __setitem__(self, key, value): + if key not in self._known_keys: + raise ValueError( + "Invalid value. `%s` is not a compile flags " "category." % key + ) + if key in self and self[key] is None: + raise ValueError( + "`%s` may not be set in COMPILE_FLAGS from moz.build, this " + "value is resolved from the emitter." % key + ) + if not ( + isinstance(value, list) + and all(isinstance(v, six.string_types) for v in value) + ): + raise ValueError( + "A list of strings must be provided as a value for a compile " + "flags category." + ) + dict.__setitem__(self, key, value) + + +class CompileFlags(TargetCompileFlags): + def __init__(self, context): + main_src_dir = mozpath.dirname(context.main_path) + self._context = context + + self.flag_variables = ( + ("STL", context.config.substs.get("STL_FLAGS"), ("CXXFLAGS",)), + ( + "VISIBILITY", + context.config.substs.get("VISIBILITY_FLAGS"), + ("CXXFLAGS", "CFLAGS"), + ), + ( + "MOZ_HARDENING_CFLAGS", + context.config.substs.get("MOZ_HARDENING_CFLAGS"), + ("CXXFLAGS", "CFLAGS", "CXX_LDFLAGS", "C_LDFLAGS"), + ), + ("DEFINES", None, ("CXXFLAGS", "CFLAGS")), + ("LIBRARY_DEFINES", None, ("CXXFLAGS", "CFLAGS")), + ( + "BASE_INCLUDES", + ["-I%s" % main_src_dir, "-I%s" % context.objdir], + ("CXXFLAGS", "CFLAGS"), + ), + ("LOCAL_INCLUDES", None, ("CXXFLAGS", "CFLAGS")), + ( + "EXTRA_INCLUDES", + ["-I%s/dist/include" % context.config.topobjdir], + ("CXXFLAGS", "CFLAGS"), + ), + ( + "OS_INCLUDES", + list( + itertools.chain( + *( + context.config.substs.get(v, []) + for v in ( + "NSPR_CFLAGS", + "NSS_CFLAGS", + "MOZ_JPEG_CFLAGS", + "MOZ_PNG_CFLAGS", + "MOZ_ZLIB_CFLAGS", + "MOZ_PIXMAN_CFLAGS", + "MOZ_ICU_CFLAGS", + ) + ) + ) + ), + ("CXXFLAGS", "CFLAGS"), + ), + ("RTL", None, ("CXXFLAGS", "CFLAGS")), + ( + "OS_COMPILE_CFLAGS", + context.config.substs.get("OS_COMPILE_CFLAGS"), + ("CFLAGS",), + ), + ( + "OS_COMPILE_CXXFLAGS", + context.config.substs.get("OS_COMPILE_CXXFLAGS"), + ("CXXFLAGS",), + ), + ( + "OS_CPPFLAGS", + context.config.substs.get("OS_CPPFLAGS"), + ("CXXFLAGS", "CFLAGS", "CXX_LDFLAGS", "C_LDFLAGS"), + ), + ( + "OS_CFLAGS", + context.config.substs.get("OS_CFLAGS"), + ("CFLAGS", "C_LDFLAGS"), + ), + ( + "OS_CXXFLAGS", + context.config.substs.get("OS_CXXFLAGS"), + ("CXXFLAGS", "CXX_LDFLAGS"), + ), + ( + "DEBUG", + self._debug_flags(), + ("CFLAGS", "CXXFLAGS", "CXX_LDFLAGS", "C_LDFLAGS"), + ), + ( + "CLANG_PLUGIN", + context.config.substs.get("CLANG_PLUGIN_FLAGS"), + ("CFLAGS", "CXXFLAGS", "CXX_LDFLAGS", "C_LDFLAGS"), + ), + ( + "OPTIMIZE", + self._optimize_flags(), + ("CFLAGS", "CXXFLAGS", "CXX_LDFLAGS", "C_LDFLAGS"), + ), + ( + "FRAMEPTR", + context.config.substs.get("MOZ_FRAMEPTR_FLAGS"), + ("CFLAGS", "CXXFLAGS", "CXX_LDFLAGS", "C_LDFLAGS"), + ), + ( + "WARNINGS_AS_ERRORS", + self._warnings_as_errors(), + ("CXXFLAGS", "CFLAGS", "CXX_LDFLAGS", "C_LDFLAGS"), + ), + ( + "WARNINGS_CFLAGS", + context.config.substs.get("WARNINGS_CFLAGS"), + ("CFLAGS",), + ), + ( + "WARNINGS_CXXFLAGS", + context.config.substs.get("WARNINGS_CXXFLAGS"), + ("CXXFLAGS",), + ), + ("MOZBUILD_CFLAGS", None, ("CFLAGS",)), + ("MOZBUILD_CXXFLAGS", None, ("CXXFLAGS",)), + ( + "COVERAGE", + context.config.substs.get("COVERAGE_CFLAGS"), + ("CXXFLAGS", "CFLAGS"), + ), + ( + "PASS_MANAGER", + context.config.substs.get("MOZ_PASS_MANAGER_FLAGS"), + ("CXXFLAGS", "CFLAGS"), + ), + ( + "FILE_PREFIX_MAP", + context.config.substs.get("MOZ_FILE_PREFIX_MAP_FLAGS"), + ("CXXFLAGS", "CFLAGS"), + ), + ( + # See bug 414641 + "NO_STRICT_ALIASING", + ["-fno-strict-aliasing"], + ("CXXFLAGS", "CFLAGS"), + ), + ( + # Disable floating-point contraction by default. + "FP_CONTRACT", + ( + ["-Xclang"] + if context.config.substs.get("CC_TYPE") == "clang-cl" + else [] + ) + + ["-ffp-contract=off"], + ("CXXFLAGS", "CFLAGS"), + ), + ) + + TargetCompileFlags.__init__(self, context) + + +class WasmFlags(TargetCompileFlags): + def __init__(self, context): + main_src_dir = mozpath.dirname(context.main_path) + self._context = context + + self.flag_variables = ( + ("LIBRARY_DEFINES", None, ("WASM_CXXFLAGS", "WASM_CFLAGS")), + ( + "BASE_INCLUDES", + ["-I%s" % main_src_dir, "-I%s" % context.objdir], + ("WASM_CXXFLAGS", "WASM_CFLAGS"), + ), + ("LOCAL_INCLUDES", None, ("WASM_CXXFLAGS", "WASM_CFLAGS")), + ( + "EXTRA_INCLUDES", + ["-I%s/dist/include" % context.config.topobjdir], + ("WASM_CXXFLAGS", "WASM_CFLAGS"), + ), + ( + "OS_INCLUDES", + list( + itertools.chain( + *( + context.config.substs.get(v, []) + for v in ( + "NSPR_CFLAGS", + "NSS_CFLAGS", + "MOZ_JPEG_CFLAGS", + "MOZ_PNG_CFLAGS", + "MOZ_ZLIB_CFLAGS", + "MOZ_PIXMAN_CFLAGS", + ) + ) + ) + ), + ("WASM_CXXFLAGS", "WASM_CFLAGS"), + ), + ("DEBUG", self._debug_flags(), ("WASM_CFLAGS", "WASM_CXXFLAGS")), + ( + "CLANG_PLUGIN", + context.config.substs.get("CLANG_PLUGIN_FLAGS"), + ("WASM_CFLAGS", "WASM_CXXFLAGS"), + ), + ("OPTIMIZE", self._optimize_flags(), ("WASM_CFLAGS", "WASM_CXXFLAGS")), + ( + "WARNINGS_AS_ERRORS", + self._warnings_as_errors(), + ("WASM_CXXFLAGS", "WASM_CFLAGS"), + ), + ("MOZBUILD_CFLAGS", None, ("WASM_CFLAGS",)), + ("MOZBUILD_CXXFLAGS", None, ("WASM_CXXFLAGS",)), + ("WASM_CFLAGS", context.config.substs.get("WASM_CFLAGS"), ("WASM_CFLAGS",)), + ( + "WASM_CXXFLAGS", + context.config.substs.get("WASM_CXXFLAGS"), + ("WASM_CXXFLAGS",), + ), + ("WASM_DEFINES", None, ("WASM_CFLAGS", "WASM_CXXFLAGS")), + ("MOZBUILD_WASM_CFLAGS", None, ("WASM_CFLAGS",)), + ("MOZBUILD_WASM_CXXFLAGS", None, ("WASM_CXXFLAGS",)), + ( + "NEWPM", + context.config.substs.get("MOZ_NEW_PASS_MANAGER_FLAGS"), + ("WASM_CFLAGS", "WASM_CXXFLAGS"), + ), + ( + "FILE_PREFIX_MAP", + context.config.substs.get("MOZ_FILE_PREFIX_MAP_FLAGS"), + ("WASM_CFLAGS", "WASM_CXXFLAGS"), + ), + ("STL", context.config.substs.get("STL_FLAGS"), ("WASM_CXXFLAGS",)), + ) + + TargetCompileFlags.__init__(self, context) + + def _debug_flags(self): + substs = self._context.config.substs + if substs.get("MOZ_DEBUG") or substs.get("MOZ_DEBUG_SYMBOLS"): + return ["-g"] + return [] + + def _optimize_flags(self): + if not self._context.config.substs.get("MOZ_OPTIMIZE"): + return [] + + # We don't want `MOZ_{PGO_,}OPTIMIZE_FLAGS here because they may contain + # optimization flags that aren't suitable for wasm (e.g. -freorder-blocks). + # Just optimize for size in all cases; we may want to make this + # configurable. + return ["-Os"] + + +class FinalTargetValue(ContextDerivedValue, six.text_type): + def __new__(cls, context, value=""): + if not value: + value = "dist/" + if context["XPI_NAME"]: + value += "xpi-stage/" + context["XPI_NAME"] + else: + value += "bin" + if context["DIST_SUBDIR"]: + value += "/" + context["DIST_SUBDIR"] + return six.text_type.__new__(cls, value) + + +def Enum(*values): + assert len(values) + default = values[0] + + class EnumClass(object): + def __new__(cls, value=None): + if value is None: + return default + if value in values: + return value + raise ValueError( + "Invalid value. Allowed values are: %s" + % ", ".join(repr(v) for v in values) + ) + + return EnumClass + + +class PathMeta(type): + """Meta class for the Path family of classes. + + It handles calling __new__ with the right arguments in cases where a Path + is instantiated with another instance of Path instead of having received a + context. + + It also makes Path(context, value) instantiate one of the + subclasses depending on the value, allowing callers to do + standard type checking (isinstance(path, ObjDirPath)) instead + of checking the value itself (path.startswith('!')). + """ + + def __call__(cls, context, value=None): + if isinstance(context, Path): + assert value is None + value = context + context = context.context + else: + assert isinstance(context, Context) + if isinstance(value, Path): + context = value.context + if not issubclass(cls, (SourcePath, ObjDirPath, AbsolutePath)): + if value.startswith("!"): + cls = ObjDirPath + elif value.startswith("%"): + cls = AbsolutePath + else: + cls = SourcePath + return super(PathMeta, cls).__call__(context, value) + + +class Path(six.with_metaclass(PathMeta, ContextDerivedValue, six.text_type)): + """Stores and resolves a source path relative to a given context + + This class is used as a backing type for some of the sandbox variables. + It expresses paths relative to a context. Supported paths are: + - '/topsrcdir/relative/paths' + - 'srcdir/relative/paths' + - '!/topobjdir/relative/paths' + - '!objdir/relative/paths' + - '%/filesystem/absolute/paths' + """ + + def __new__(cls, context, value=None): + self = super(Path, cls).__new__(cls, value) + self.context = context + self.srcdir = context.srcdir + return self + + def join(self, *p): + """ContextDerived equivalent of `mozpath.join(self, *p)`, returning a + new Path instance. + """ + return Path(self.context, mozpath.join(self, *p)) + + def __cmp__(self, other): + # We expect this function to never be called to avoid issues in the + # switch from Python 2 to 3. + raise AssertionError() + + def _cmp(self, other, op): + if isinstance(other, Path) and self.srcdir != other.srcdir: + return op(self.full_path, other.full_path) + return op(six.text_type(self), other) + + def __eq__(self, other): + return self._cmp(other, operator.eq) + + def __ne__(self, other): + return self._cmp(other, operator.ne) + + def __lt__(self, other): + return self._cmp(other, operator.lt) + + def __gt__(self, other): + return self._cmp(other, operator.gt) + + def __le__(self, other): + return self._cmp(other, operator.le) + + def __ge__(self, other): + return self._cmp(other, operator.ge) + + def __repr__(self): + return "<%s (%s)%s>" % (self.__class__.__name__, self.srcdir, self) + + def __hash__(self): + return hash(self.full_path) + + @memoized_property + def target_basename(self): + return mozpath.basename(self.full_path) + + +class SourcePath(Path): + """Like Path, but limited to paths in the source directory.""" + + def __new__(cls, context, value=None): + if value.startswith("!"): + raise ValueError(f'Object directory paths are not allowed\nPath: "{value}"') + if value.startswith("%"): + raise ValueError( + f'Filesystem absolute paths are not allowed\nPath: "{value}"' + ) + self = super(SourcePath, cls).__new__(cls, context, value) + + if value.startswith("/"): + path = None + if not path or not os.path.exists(path): + path = mozpath.join(context.config.topsrcdir, value[1:]) + else: + path = mozpath.join(self.srcdir, value) + self.full_path = mozpath.normpath(path) + return self + + @memoized_property + def translated(self): + """Returns the corresponding path in the objdir. + + Ideally, we wouldn't need this function, but the fact that both source + path under topsrcdir and the external source dir end up mixed in the + objdir (aka pseudo-rework), this is needed. + """ + return ObjDirPath(self.context, "!%s" % self).full_path + + +class RenamedSourcePath(SourcePath): + """Like SourcePath, but with a different base name when installed. + + The constructor takes a tuple of (source, target_basename). + + This class is not meant to be exposed to moz.build sandboxes as of now, + and is not supported by the RecursiveMake backend. + """ + + def __new__(cls, context, value): + assert isinstance(value, tuple) + source, target_basename = value + self = super(RenamedSourcePath, cls).__new__(cls, context, source) + self._target_basename = target_basename + return self + + @property + def target_basename(self): + return self._target_basename + + +class ObjDirPath(Path): + """Like Path, but limited to paths in the object directory.""" + + def __new__(cls, context, value=None): + if not value.startswith("!"): + raise ValueError("Object directory paths must start with ! prefix") + self = super(ObjDirPath, cls).__new__(cls, context, value) + + if value.startswith("!/"): + path = mozpath.join(context.config.topobjdir, value[2:]) + else: + path = mozpath.join(context.objdir, value[1:]) + self.full_path = mozpath.normpath(path) + return self + + +class AbsolutePath(Path): + """Like Path, but allows arbitrary paths outside the source and object directories.""" + + def __new__(cls, context, value=None): + if not value.startswith("%"): + raise ValueError("Absolute paths must start with % prefix") + if not os.path.isabs(value[1:]): + raise ValueError("Path '%s' is not absolute" % value[1:]) + self = super(AbsolutePath, cls).__new__(cls, context, value) + self.full_path = mozpath.normpath(value[1:]) + return self + + +@memoize +def ContextDerivedTypedList(klass, base_class=List): + """Specialized TypedList for use with ContextDerivedValue types.""" + assert issubclass(klass, ContextDerivedValue) + + class _TypedList(ContextDerivedValue, TypedList(klass, base_class)): + def __init__(self, context, iterable=[], **kwargs): + self.context = context + super(_TypedList, self).__init__(iterable, **kwargs) + + def normalize(self, e): + if not isinstance(e, klass): + e = klass(self.context, e) + return e + + return _TypedList + + +@memoize +def ContextDerivedTypedListWithItems(type, base_class=List): + """Specialized TypedList for use with ContextDerivedValue types.""" + + class _TypedListWithItems(ContextDerivedTypedList(type, base_class)): + def __getitem__(self, name): + name = self.normalize(name) + return super(_TypedListWithItems, self).__getitem__(name) + + return _TypedListWithItems + + +@memoize +def ContextDerivedTypedRecord(*fields): + """Factory for objects with certain properties and dynamic + type checks. + + This API is extremely similar to the TypedNamedTuple API, + except that properties may be mutated. This supports syntax like: + + .. code-block:: python + + VARIABLE_NAME.property += [ + 'item1', + 'item2', + ] + """ + + class _TypedRecord(ContextDerivedValue): + __slots__ = tuple([name for name, _ in fields]) + + def __init__(self, context): + for fname, ftype in self._fields.items(): + if issubclass(ftype, ContextDerivedValue): + setattr(self, fname, self._fields[fname](context)) + else: + setattr(self, fname, self._fields[fname]()) + + def __setattr__(self, name, value): + if name in self._fields and not isinstance(value, self._fields[name]): + value = self._fields[name](value) + object.__setattr__(self, name, value) + + _TypedRecord._fields = dict(fields) + return _TypedRecord + + +class Schedules(object): + """Similar to a ContextDerivedTypedRecord, but with different behavior + for the properties: + + * VAR.inclusive can only be appended to (+=), and can only contain values + from mozbuild.schedules.INCLUSIVE_COMPONENTS + + * VAR.exclusive can only be assigned to (no +=), and can only contain + values from mozbuild.schedules.ALL_COMPONENTS + """ + + __slots__ = ("_exclusive", "_inclusive") + + def __init__(self, inclusive=None, exclusive=None): + if inclusive is None: + self._inclusive = TypedList(Enum(*schedules.INCLUSIVE_COMPONENTS))() + else: + self._inclusive = inclusive + if exclusive is None: + self._exclusive = ImmutableStrictOrderingOnAppendList( + schedules.EXCLUSIVE_COMPONENTS + ) + else: + self._exclusive = exclusive + + # inclusive is mutable but cannot be assigned to (+= only) + @property + def inclusive(self): + return self._inclusive + + @inclusive.setter + def inclusive(self, value): + if value is not self._inclusive: + raise AttributeError("Cannot assign to this value - use += instead") + unexpected = [v for v in value if v not in schedules.INCLUSIVE_COMPONENTS] + if unexpected: + raise Exception( + "unexpected inclusive component(s) " + ", ".join(unexpected) + ) + + # exclusive is immutable but can be set (= only) + @property + def exclusive(self): + return self._exclusive + + @exclusive.setter + def exclusive(self, value): + if not isinstance(value, (tuple, list)): + raise Exception("expected a tuple or list") + unexpected = [v for v in value if v not in schedules.ALL_COMPONENTS] + if unexpected: + raise Exception( + "unexpected exclusive component(s) " + ", ".join(unexpected) + ) + self._exclusive = ImmutableStrictOrderingOnAppendList(sorted(value)) + + # components provides a synthetic summary of all components + @property + def components(self): + return list(sorted(set(self._inclusive) | set(self._exclusive))) + + # The `Files` context uses | to combine SCHEDULES from multiple levels; at this + # point the immutability is no longer needed so we use plain lists + def __or__(self, other): + inclusive = self._inclusive + other._inclusive + if other._exclusive == self._exclusive: + exclusive = self._exclusive + elif self._exclusive == schedules.EXCLUSIVE_COMPONENTS: + exclusive = other._exclusive + elif other._exclusive == schedules.EXCLUSIVE_COMPONENTS: + exclusive = self._exclusive + else: + # in a case where two SCHEDULES.exclusive set different values, take + # the later one; this acts the way we expect assignment to work. + exclusive = other._exclusive + return Schedules(inclusive=inclusive, exclusive=exclusive) + + +@memoize +def ContextDerivedTypedHierarchicalStringList(type): + """Specialized HierarchicalStringList for use with ContextDerivedValue + types.""" + + class _TypedListWithItems(ContextDerivedValue, HierarchicalStringList): + __slots__ = ("_strings", "_children", "_context") + + def __init__(self, context): + self._strings = ContextDerivedTypedList(type, StrictOrderingOnAppendList)( + context + ) + self._children = {} + self._context = context + + def _get_exportvariable(self, name): + child = self._children.get(name) + if not child: + child = self._children[name] = _TypedListWithItems(self._context) + return child + + return _TypedListWithItems + + +def OrderedPathListWithAction(action): + """Returns a class which behaves as a StrictOrderingOnAppendList, but + invokes the given callable with each input and a context as it is + read, storing a tuple including the result and the original item. + + This used to extend moz.build reading to make more data available in + filesystem-reading mode. + """ + + class _OrderedListWithAction( + ContextDerivedTypedList(SourcePath, StrictOrderingOnAppendListWithAction) + ): + def __init__(self, context, *args): + def _action(item): + return item, action(context, item) + + super(_OrderedListWithAction, self).__init__(context, action=_action, *args) + + return _OrderedListWithAction + + +ManifestparserManifestList = OrderedPathListWithAction(read_manifestparser_manifest) +ReftestManifestList = OrderedPathListWithAction(read_reftest_manifest) + +BugzillaComponent = TypedNamedTuple( + "BugzillaComponent", [("product", six.text_type), ("component", six.text_type)] +) +SchedulingComponents = ContextDerivedTypedRecord( + ("inclusive", TypedList(six.text_type, StrictOrderingOnAppendList)), + ("exclusive", TypedList(six.text_type, StrictOrderingOnAppendList)), +) + +GeneratedFilesList = StrictOrderingOnAppendListWithFlagsFactory( + {"script": six.text_type, "inputs": list, "force": bool, "flags": list} +) + + +class Files(SubContext): + """Metadata attached to files. + + It is common to want to annotate files with metadata, such as which + Bugzilla component tracks issues with certain files. This sub-context is + where we stick that metadata. + + The argument to this sub-context is a file matching pattern that is applied + against the host file's directory. If the pattern matches a file whose info + is currently being sought, the metadata attached to this instance will be + applied to that file. + + Patterns are collections of filename characters with ``/`` used as the + directory separate (UNIX-style paths) and ``*`` and ``**`` used to denote + wildcard matching. + + Patterns without the ``*`` character are literal matches and will match at + most one entity. + + Patterns with ``*`` or ``**`` are wildcard matches. ``*`` matches files + at least within a single directory. ``**`` matches files across several + directories. + + ``foo.html`` + Will match only the ``foo.html`` file in the current directory. + ``*.jsm`` + Will match all ``.jsm`` files in the current directory. + ``**/*.cpp`` + Will match all ``.cpp`` files in this and all child directories. + ``foo/*.css`` + Will match all ``.css`` files in the ``foo/`` directory. + ``bar/*`` + Will match all files in the ``bar/`` directory and all of its + children directories. + ``bar/**`` + This is equivalent to ``bar/*`` above. + ``bar/**/foo`` + Will match all ``foo`` files in the ``bar/`` directory and all of its + children directories. + + The difference in behavior between ``*`` and ``**`` is only evident if + a pattern follows the ``*`` or ``**``. A pattern ending with ``*`` is + greedy. ``**`` is needed when you need an additional pattern after the + wildcard. e.g. ``**/foo``. + """ + + VARIABLES = { + "BUG_COMPONENT": ( + BugzillaComponent, + tuple, + """The bug component that tracks changes to these files. + + Values are a 2-tuple of unicode describing the Bugzilla product and + component. e.g. ``('Firefox Build System', 'General')``. + """, + ), + "FINAL": ( + bool, + bool, + """Mark variable assignments as finalized. + + During normal processing, values from newer Files contexts + overwrite previously set values. Last write wins. This behavior is + not always desired. ``FINAL`` provides a mechanism to prevent + further updates to a variable. + + When ``FINAL`` is set, the value of all variables defined in this + context are marked as frozen and all subsequent writes to them + are ignored during metadata reading. + + See :ref:`mozbuild_files_metadata_finalizing` for more info. + """, + ), + "SCHEDULES": ( + Schedules, + list, + """Maps source files to the CI tasks that should be scheduled when + they change. The tasks are grouped by named components, and those + names appear again in the taskgraph configuration + `($topsrcdir/taskgraph/). + + Some components are "inclusive", meaning that changes to most files + do not schedule them, aside from those described in a Files + subcontext. For example, py-lint tasks need not be scheduled for + most changes, but should be scheduled when any Python file changes. + Such components are named by appending to `SCHEDULES.inclusive`: + + with Files('**.py'): + SCHEDULES.inclusive += ['py-lint'] + + Other components are 'exclusive', meaning that changes to most + files schedule them, but some files affect only one or two + components. For example, most files schedule builds and tests of + Firefox for Android, OS X, Windows, and Linux, but files under + `mobile/android/` affect Android builds and tests exclusively, so + builds for other operating systems are not needed. Test suites + provide another example: most files schedule reftests, but changes + to reftest scripts need only schedule reftests and no other suites. + + Exclusive components are named by setting `SCHEDULES.exclusive`: + + with Files('mobile/android/**'): + SCHEDULES.exclusive = ['android'] + """, + ), + } + + def __init__(self, parent, *patterns): + super(Files, self).__init__(parent) + self.patterns = patterns + self.finalized = set() + + def __iadd__(self, other): + assert isinstance(other, Files) + + for k, v in other.items(): + if k == "SCHEDULES" and "SCHEDULES" in self: + self["SCHEDULES"] = self["SCHEDULES"] | v + continue + + # Ignore updates to finalized flags. + if k in self.finalized: + continue + + # Only finalize variables defined in this instance. + if k == "FINAL": + self.finalized |= set(other) - {"FINAL"} + continue + + self[k] = v + + return self + + def asdict(self): + """Return this instance as a dict with built-in data structures. + + Call this to obtain an object suitable for serializing. + """ + d = {} + if "BUG_COMPONENT" in self: + bc = self["BUG_COMPONENT"] + d["bug_component"] = (bc.product, bc.component) + + return d + + @staticmethod + def aggregate(files): + """Given a mapping of path to Files, obtain aggregate results. + + Consumers may want to extract useful information from a collection of + Files describing paths. e.g. given the files info data for N paths, + recommend a single bug component based on the most frequent one. This + function provides logic for deriving aggregate knowledge from a + collection of path File metadata. + + Note: the intent of this function is to operate on the result of + :py:func:`mozbuild.frontend.reader.BuildReader.files_info`. The + :py:func:`mozbuild.frontend.context.Files` instances passed in are + thus the "collapsed" (``__iadd__``ed) results of all ``Files`` from all + moz.build files relevant to a specific path, not individual ``Files`` + instances from a single moz.build file. + """ + d = {} + + bug_components = Counter() + + for f in files.values(): + bug_component = f.get("BUG_COMPONENT") + if bug_component: + bug_components[bug_component] += 1 + + d["bug_component_counts"] = [] + for c, count in bug_components.most_common(): + component = (c.product, c.component) + d["bug_component_counts"].append((c, count)) + + if "recommended_bug_component" not in d: + d["recommended_bug_component"] = component + recommended_count = count + elif count == recommended_count: + # Don't recommend a component if it doesn't have a clear lead. + d["recommended_bug_component"] = None + + # In case no bug components. + d.setdefault("recommended_bug_component", None) + + return d + + +# This defines functions that create sub-contexts. +# +# Values are classes that are SubContexts. The class name will be turned into +# a function that when called emits an instance of that class. +# +# Arbitrary arguments can be passed to the class constructor. The first +# argument is always the parent context. It is up to each class to perform +# argument validation. +SUBCONTEXTS = [Files] + +for cls in SUBCONTEXTS: + if not issubclass(cls, SubContext): + raise ValueError("SUBCONTEXTS entry not a SubContext class: %s" % cls) + + if not hasattr(cls, "VARIABLES"): + raise ValueError("SUBCONTEXTS entry does not have VARIABLES: %s" % cls) + +SUBCONTEXTS = {cls.__name__: cls for cls in SUBCONTEXTS} + + +# This defines the set of mutable global variables. +# +# Each variable is a tuple of: +# +# (storage_type, input_types, docs) + +VARIABLES = { + "SOURCES": ( + ContextDerivedTypedListWithItems( + Path, + StrictOrderingOnAppendListWithFlagsFactory({"no_pgo": bool, "flags": List}), + ), + list, + """Source code files. + + This variable contains a list of source code files to compile. + Accepts assembler, C, C++, Objective C/C++. + """, + ), + "FILES_PER_UNIFIED_FILE": ( + int, + int, + """The number of source files to compile into each unified source file. + + """, + ), + "IS_RUST_LIBRARY": ( + bool, + bool, + """Whether the current library defined by this moz.build is built by Rust. + + The library defined by this moz.build should have a build definition in + a Cargo.toml file that exists in this moz.build's directory. + """, + ), + "IS_GKRUST": ( + bool, + bool, + """Whether the current library defined by this moz.build is gkrust. + + Indicates whether the current library contains rust for libxul. + """, + ), + "RUST_LIBRARY_FEATURES": ( + List, + list, + """Cargo features to activate for this library. + + This variable should not be used directly; you should be using the + RustLibrary template instead. + """, + ), + "HOST_RUST_LIBRARY_FEATURES": ( + List, + list, + """Cargo features to activate for this host library. + + This variable should not be used directly; you should be using the + HostRustLibrary template instead. + """, + ), + "RUST_TESTS": ( + TypedList(six.text_type), + list, + """Names of Rust tests to build and run via `cargo test`. + """, + ), + "RUST_TEST_FEATURES": ( + TypedList(six.text_type), + list, + """Cargo features to activate for RUST_TESTS. + """, + ), + "UNIFIED_SOURCES": ( + ContextDerivedTypedList(Path, StrictOrderingOnAppendList), + list, + """Source code files that can be compiled together. + + This variable contains a list of source code files to compile, + that can be concatenated all together and built as a single source + file. This can help make the build faster and reduce the debug info + size. + """, + ), + "GENERATED_FILES": ( + GeneratedFilesList, + list, + """Generic generated files. + + Unless you have a reason not to, use the GeneratedFile template rather + than referencing GENERATED_FILES directly. The GeneratedFile template + has all the same arguments as the attributes listed below (``script``, + ``inputs``, ``flags``, ``force``), plus an additional ``entry_point`` + argument to specify a particular function to run in the given script. + + This variable contains a list of files for the build system to + generate at export time. The generation method may be declared + with optional ``script``, ``inputs``, ``flags``, and ``force`` + attributes on individual entries. + If the optional ``script`` attribute is not present on an entry, it + is assumed that rules for generating the file are present in + the associated Makefile.in. + + Example:: + + GENERATED_FILES += ['bar.c', 'baz.c', 'foo.c'] + bar = GENERATED_FILES['bar.c'] + bar.script = 'generate.py' + bar.inputs = ['datafile-for-bar'] + foo = GENERATED_FILES['foo.c'] + foo.script = 'generate.py' + foo.inputs = ['datafile-for-foo'] + + This definition will generate bar.c by calling the main method of + generate.py with a open (for writing) file object for bar.c, and + the string ``datafile-for-bar``. In a similar fashion, the main + method of generate.py will also be called with an open + (for writing) file object for foo.c and the string + ``datafile-for-foo``. Please note that only string arguments are + supported for passing to scripts, and that all arguments provided + to the script should be filenames relative to the directory in which + the moz.build file is located. + + To enable using the same script for generating multiple files with + slightly different non-filename parameters, alternative entry points + into ``script`` can be specified:: + + GENERATED_FILES += ['bar.c'] + bar = GENERATED_FILES['bar.c'] + bar.script = 'generate.py:make_bar' + + The chosen script entry point may optionally return a set of strings, + indicating extra files the output depends on. + + When the ``flags`` attribute is present, the given list of flags is + passed as extra arguments following the inputs. + + When the ``force`` attribute is present, the file is generated every + build, regardless of whether it is stale. This is special to the + RecursiveMake backend and intended for special situations only (e.g., + localization). Please consult a build peer (on the #build channel at + https://chat.mozilla.org) before using ``force``. + """, + ), + "DEFINES": ( + InitializedDefines, + dict, + """Dictionary of compiler defines to declare. + + These are passed in to the compiler as ``-Dkey='value'`` for string + values, ``-Dkey=value`` for numeric values, or ``-Dkey`` if the + value is True. Note that for string values, the outer-level of + single-quotes will be consumed by the shell. If you want to have + a string-literal in the program, the value needs to have + double-quotes. + + Example:: + + DEFINES['NS_NO_XPCOM'] = True + DEFINES['MOZ_EXTENSIONS_DB_SCHEMA'] = 15 + DEFINES['DLL_SUFFIX'] = '".so"' + + This will result in the compiler flags ``-DNS_NO_XPCOM``, + ``-DMOZ_EXTENSIONS_DB_SCHEMA=15``, and ``-DDLL_SUFFIX='".so"'``, + respectively. + + Note that these entries are not necessarily passed to the assembler. + Whether they are depends on the type of assembly file. As an + alternative, you may add a ``-DKEY=value`` entry to ``ASFLAGS``. + """, + ), + "DELAYLOAD_DLLS": ( + List, + list, + """Delay-loaded DLLs. + + This variable contains a list of DLL files which the module being linked + should load lazily. This only has an effect when building with MSVC. + """, + ), + "DIRS": ( + ContextDerivedTypedList(SourcePath), + list, + """Child directories to descend into looking for build frontend files. + + This works similarly to the ``DIRS`` variable in make files. Each str + value in the list is the name of a child directory. When this file is + done parsing, the build reader will descend into each listed directory + and read the frontend file there. If there is no frontend file, an error + is raised. + + Values are relative paths. They can be multiple directory levels + above or below. Use ``..`` for parent directories and ``/`` for path + delimiters. + """, + ), + "FINAL_TARGET_FILES": ( + ContextDerivedTypedHierarchicalStringList(Path), + list, + """List of files to be installed into the application directory. + + ``FINAL_TARGET_FILES`` will copy (or symlink, if the platform supports it) + the contents of its files to the directory specified by + ``FINAL_TARGET`` (typically ``dist/bin``). Files that are destined for a + subdirectory can be specified by accessing a field, or as a dict access. + For example, to export ``foo.png`` to the top-level directory and + ``bar.svg`` to the directory ``images/do-not-use``, append to + ``FINAL_TARGET_FILES`` like so:: + + FINAL_TARGET_FILES += ['foo.png'] + FINAL_TARGET_FILES.images['do-not-use'] += ['bar.svg'] + """, + ), + "FINAL_TARGET_PP_FILES": ( + ContextDerivedTypedHierarchicalStringList(Path), + list, + """Like ``FINAL_TARGET_FILES``, with preprocessing. + """, + ), + "LOCALIZED_FILES": ( + ContextDerivedTypedHierarchicalStringList(Path), + list, + """List of locale-dependent files to be installed into the application + directory. + + This functions similarly to ``FINAL_TARGET_FILES``, but the files are + sourced from the locale directory and will vary per localization. + For an en-US build, this is functionally equivalent to + ``FINAL_TARGET_FILES``. For a build with ``--enable-ui-locale``, + the file will be taken from ``$LOCALE_SRCDIR``, with the leading + ``en-US`` removed. For a l10n repack of an en-US build, the file + will be taken from the first location where it exists from: + * the merged locale directory if it exists + * ``$LOCALE_SRCDIR`` with the leading ``en-US`` removed + * the in-tree en-US location + + Source directory paths specified here must must include a leading ``en-US``. + Wildcards are allowed, and will be expanded at the time of locale packaging to match + files in the locale directory. + + Object directory paths are allowed here only if the path matches an entry in + ``LOCALIZED_GENERATED_FILES``. + + Files that are missing from a locale will typically have the en-US + version used, but for wildcard expansions only files from the + locale directory will be used, even if that means no files will + be copied. + + Example:: + + LOCALIZED_FILES.foo += [ + 'en-US/foo.js', + 'en-US/things/*.ini', + ] + + If this was placed in ``toolkit/locales/moz.build``, it would copy + ``toolkit/locales/en-US/foo.js`` and + ``toolkit/locales/en-US/things/*.ini`` to ``$(DIST)/bin/foo`` in an + en-US build, and in a build of a different locale (or a repack), + it would copy ``$(LOCALE_SRCDIR)/toolkit/foo.js`` and + ``$(LOCALE_SRCDIR)/toolkit/things/*.ini``. + """, + ), + "LOCALIZED_PP_FILES": ( + ContextDerivedTypedHierarchicalStringList(Path), + list, + """Like ``LOCALIZED_FILES``, with preprocessing. + + Note that the ``AB_CD`` define is available and expands to the current + locale being packaged, as with preprocessed entries in jar manifests. + """, + ), + "LOCALIZED_GENERATED_FILES": ( + GeneratedFilesList, + list, + """Like ``GENERATED_FILES``, but for files whose content varies based on the locale in use. + + For simple cases of text substitution, prefer ``LOCALIZED_PP_FILES``. + + Refer to the documentation of ``GENERATED_FILES``; for the most part things work the same. + The two major differences are: + 1. The function in the Python script will be passed an additional keyword argument `locale` + which provides the locale in use, i.e. ``en-US``. + 2. The ``inputs`` list may contain paths to files that will be taken from the locale + source directory (see ``LOCALIZED_FILES`` for a discussion of the specifics). Paths + in ``inputs`` starting with ``en-US/`` or containing ``locales/en-US/`` are considered + localized files. + + To place the generated output file in a specific location, list its objdir path in + ``LOCALIZED_FILES``. + + In addition, ``LOCALIZED_GENERATED_FILES`` can use the special substitutions ``{AB_CD}`` + and ``{AB_rCD}`` in their output paths. ``{AB_CD}`` expands to the current locale during + multi-locale builds and single-locale repacks and ``{AB_rCD}`` expands to an + Android-specific encoding of the current locale. Both expand to the empty string when the + current locale is ``en-US``. + """, + ), + "OBJDIR_FILES": ( + ContextDerivedTypedHierarchicalStringList(Path), + list, + """List of files to be installed anywhere in the objdir. Use sparingly. + + ``OBJDIR_FILES`` is similar to FINAL_TARGET_FILES, but it allows copying + anywhere in the object directory. This is intended for various one-off + cases, not for general use. If you wish to add entries to OBJDIR_FILES, + please consult a build peer (on the #build channel at https://chat.mozilla.org). + """, + ), + "OBJDIR_PP_FILES": ( + ContextDerivedTypedHierarchicalStringList(Path), + list, + """Like ``OBJDIR_FILES``, with preprocessing. Use sparingly. + """, + ), + "FINAL_LIBRARY": ( + six.text_type, + six.text_type, + """Library in which the objects of the current directory will be linked. + + This variable contains the name of a library, defined elsewhere with + ``LIBRARY_NAME``, in which the objects of the current directory will be + linked. + """, + ), + "CPP_UNIT_TESTS": ( + StrictOrderingOnAppendList, + list, + """Compile a list of C++ unit test names. + + Each name in this variable corresponds to an executable built from the + corresponding source file with the same base name. + + If the configuration token ``BIN_SUFFIX`` is set, its value will be + automatically appended to each name. If a name already ends with + ``BIN_SUFFIX``, the name will remain unchanged. + """, + ), + "FORCE_SHARED_LIB": ( + bool, + bool, + """Whether the library in this directory is a shared library. + """, + ), + "FORCE_STATIC_LIB": ( + bool, + bool, + """Whether the library in this directory is a static library. + """, + ), + "USE_STATIC_LIBS": ( + bool, + bool, + """Whether the code in this directory is a built against the static + runtime library. + + This variable only has an effect when building with MSVC. + """, + ), + "HOST_SOURCES": ( + ContextDerivedTypedList(Path, StrictOrderingOnAppendList), + list, + """Source code files to compile with the host compiler. + + This variable contains a list of source code files to compile. + with the host compiler. + """, + ), + "WASM_SOURCES": ( + ContextDerivedTypedList(Path, StrictOrderingOnAppendList), + list, + """Source code files to compile with the wasm compiler. + """, + ), + "HOST_LIBRARY_NAME": ( + six.text_type, + six.text_type, + """Name of target library generated when cross compiling. + """, + ), + "LIBRARY_DEFINES": ( + OrderedDict, + dict, + """Dictionary of compiler defines to declare for the entire library. + + This variable works like DEFINES, except that declarations apply to all + libraries that link into this library via FINAL_LIBRARY. + """, + ), + "LIBRARY_NAME": ( + six.text_type, + six.text_type, + """The code name of the library generated for a directory. + + By default STATIC_LIBRARY_NAME and SHARED_LIBRARY_NAME take this name. + In ``example/components/moz.build``,:: + + LIBRARY_NAME = 'xpcomsample' + + would generate ``example/components/libxpcomsample.so`` on Linux, or + ``example/components/xpcomsample.lib`` on Windows. + """, + ), + "SHARED_LIBRARY_NAME": ( + six.text_type, + six.text_type, + """The name of the static library generated for a directory, if it needs to + differ from the library code name. + + Implies FORCE_SHARED_LIB. + """, + ), + "SANDBOXED_WASM_LIBRARY_NAME": ( + six.text_type, + six.text_type, + """The name of the static sandboxed wasm library generated for a directory. + """, + ), + "SHARED_LIBRARY_OUTPUT_CATEGORY": ( + six.text_type, + six.text_type, + """The output category for this context's shared library. If set this will + correspond to the build command that will build this shared library, and + the library will not be built as part of the default build. + """, + ), + "RUST_LIBRARY_OUTPUT_CATEGORY": ( + six.text_type, + six.text_type, + """The output category for this context's rust library. If set this will + correspond to the build command that will build this rust library, and + the library will not be built as part of the default build. + """, + ), + "IS_FRAMEWORK": ( + bool, + bool, + """Whether the library to build should be built as a framework on OSX. + + This implies the name of the library won't be prefixed nor suffixed. + Implies FORCE_SHARED_LIB. + """, + ), + "STATIC_LIBRARY_NAME": ( + six.text_type, + six.text_type, + """The name of the static library generated for a directory, if it needs to + differ from the library code name. + + Implies FORCE_STATIC_LIB. + """, + ), + "USE_LIBS": ( + StrictOrderingOnAppendList, + list, + """List of libraries to link to programs and libraries. + """, + ), + "HOST_USE_LIBS": ( + StrictOrderingOnAppendList, + list, + """List of libraries to link to host programs and libraries. + """, + ), + "HOST_OS_LIBS": ( + List, + list, + """List of system libraries for host programs and libraries. + """, + ), + "LOCAL_INCLUDES": ( + ContextDerivedTypedList(Path, StrictOrderingOnAppendList), + list, + """Additional directories to be searched for include files by the compiler. + """, + ), + "NO_PGO": ( + bool, + bool, + """Whether profile-guided optimization is disable in this directory. + """, + ), + "OS_LIBS": ( + List, + list, + """System link libraries. + + This variable contains a list of system libaries to link against. + """, + ), + "RCFILE": ( + Path, + six.text_type, + """The program .rc file. + + This variable can only be used on Windows. + """, + ), + "RCINCLUDE": ( + Path, + six.text_type, + """The resource script file to be included in the default .res file. + + This variable can only be used on Windows. + """, + ), + "DEFFILE": ( + Path, + six.text_type, + """The program .def (module definition) file. + + This variable can only be used on Windows. + """, + ), + "SYMBOLS_FILE": ( + Path, + six.text_type, + """A file containing a list of symbols to export from a shared library. + + The given file contains a list of symbols to be exported, and is + preprocessed. + A special marker "@DATA@" must be added after a symbol name if it + points to data instead of code, so that the Windows linker can treat + them correctly. + """, + ), + "SIMPLE_PROGRAMS": ( + StrictOrderingOnAppendList, + list, + """Compile a list of executable names. + + Each name in this variable corresponds to an executable built from the + corresponding source file with the same base name. + + If the configuration token ``BIN_SUFFIX`` is set, its value will be + automatically appended to each name. If a name already ends with + ``BIN_SUFFIX``, the name will remain unchanged. + """, + ), + "SONAME": ( + six.text_type, + six.text_type, + """The soname of the shared object currently being linked + + soname is the "logical name" of a shared object, often used to provide + version backwards compatibility. This variable makes sense only for + shared objects, and is supported only on some unix platforms. + """, + ), + "HOST_SIMPLE_PROGRAMS": ( + StrictOrderingOnAppendList, + list, + """Compile a list of host executable names. + + Each name in this variable corresponds to a hosst executable built + from the corresponding source file with the same base name. + + If the configuration token ``HOST_BIN_SUFFIX`` is set, its value will + be automatically appended to each name. If a name already ends with + ``HOST_BIN_SUFFIX``, the name will remain unchanged. + """, + ), + "RUST_PROGRAMS": ( + StrictOrderingOnAppendList, + list, + """Compile a list of Rust host executable names. + + Each name in this variable corresponds to an executable built from + the Cargo.toml in the same directory. + """, + ), + "HOST_RUST_PROGRAMS": ( + StrictOrderingOnAppendList, + list, + """Compile a list of Rust executable names. + + Each name in this variable corresponds to an executable built from + the Cargo.toml in the same directory. + """, + ), + "CONFIGURE_SUBST_FILES": ( + ContextDerivedTypedList(SourcePath, StrictOrderingOnAppendList), + list, + """Output files that will be generated using configure-like substitution. + + This is a substitute for ``AC_OUTPUT`` in autoconf. For each path in this + list, we will search for a file in the srcdir having the name + ``{path}.in``. The contents of this file will be read and variable + patterns like ``@foo@`` will be substituted with the values of the + ``AC_SUBST`` variables declared during configure. + """, + ), + "CONFIGURE_DEFINE_FILES": ( + ContextDerivedTypedList(SourcePath, StrictOrderingOnAppendList), + list, + """Output files generated from configure/config.status. + + This is a substitute for ``AC_CONFIG_HEADER`` in autoconf. This is very + similar to ``CONFIGURE_SUBST_FILES`` except the generation logic takes + into account the values of ``AC_DEFINE`` instead of ``AC_SUBST``. + """, + ), + "EXPORTS": ( + ContextDerivedTypedHierarchicalStringList(Path), + list, + """List of files to be exported, and in which subdirectories. + + ``EXPORTS`` is generally used to list the include files to be exported to + ``dist/include``, but it can be used for other files as well. This variable + behaves as a list when appending filenames for export in the top-level + directory. Files can also be appended to a field to indicate which + subdirectory they should be exported to. For example, to export + ``foo.h`` to the top-level directory, and ``bar.h`` to ``mozilla/dom/``, + append to ``EXPORTS`` like so:: + + EXPORTS += ['foo.h'] + EXPORTS.mozilla.dom += ['bar.h'] + + Entries in ``EXPORTS`` are paths, so objdir paths may be used, but + any files listed from the objdir must also be listed in + ``GENERATED_FILES``. + """, + ), + "PROGRAM": ( + six.text_type, + six.text_type, + """Compiled executable name. + + If the configuration token ``BIN_SUFFIX`` is set, its value will be + automatically appended to ``PROGRAM``. If ``PROGRAM`` already ends with + ``BIN_SUFFIX``, ``PROGRAM`` will remain unchanged. + """, + ), + "HOST_PROGRAM": ( + six.text_type, + six.text_type, + """Compiled host executable name. + + If the configuration token ``HOST_BIN_SUFFIX`` is set, its value will be + automatically appended to ``HOST_PROGRAM``. If ``HOST_PROGRAM`` already + ends with ``HOST_BIN_SUFFIX``, ``HOST_PROGRAM`` will remain unchanged. + """, + ), + "DIST_INSTALL": ( + Enum(None, False, True), + bool, + """Whether to install certain files into the dist directory. + + By default, some files types are installed in the dist directory, and + some aren't. Set this variable to True to force the installation of + some files that wouldn't be installed by default. Set this variable to + False to force to not install some files that would be installed by + default. + + This is confusing for historical reasons, but eventually, the behavior + will be made explicit. + """, + ), + "JAR_MANIFESTS": ( + ContextDerivedTypedList(SourcePath, StrictOrderingOnAppendList), + list, + """JAR manifest files that should be processed as part of the build. + + JAR manifests are files in the tree that define how to package files + into JARs and how chrome registration is performed. For more info, + see :ref:`jar_manifests`. + """, + ), + # IDL Generation. + "XPIDL_SOURCES": ( + ContextDerivedTypedList(SourcePath, StrictOrderingOnAppendList), + list, + """XPCOM Interface Definition Files (xpidl). + + This is a list of files that define XPCOM interface definitions. + Entries must be files that exist. Entries are almost certainly ``.idl`` + files. + """, + ), + "XPIDL_MODULE": ( + six.text_type, + six.text_type, + """XPCOM Interface Definition Module Name. + + This is the name of the ``.xpt`` file that is created by linking + ``XPIDL_SOURCES`` together. If unspecified, it defaults to be the same + as ``MODULE``. + """, + ), + "XPCOM_MANIFESTS": ( + ContextDerivedTypedList(SourcePath, StrictOrderingOnAppendList), + list, + """XPCOM Component Manifest Files. + + This is a list of files that define XPCOM components to be added + to the component registry. + """, + ), + "PREPROCESSED_IPDL_SOURCES": ( + StrictOrderingOnAppendList, + list, + """Preprocessed IPDL source files. + + These files will be preprocessed, then parsed and converted to + ``.cpp`` files. + """, + ), + "IPDL_SOURCES": ( + StrictOrderingOnAppendList, + list, + """IPDL source files. + + These are ``.ipdl`` files that will be parsed and converted to + ``.cpp`` files. + """, + ), + "WEBIDL_FILES": ( + StrictOrderingOnAppendList, + list, + """WebIDL source files. + + These will be parsed and converted to ``.cpp`` and ``.h`` files. + """, + ), + "GENERATED_EVENTS_WEBIDL_FILES": ( + StrictOrderingOnAppendList, + list, + """WebIDL source files for generated events. + + These will be parsed and converted to ``.cpp`` and ``.h`` files. + """, + ), + "TEST_WEBIDL_FILES": ( + StrictOrderingOnAppendList, + list, + """Test WebIDL source files. + + These will be parsed and converted to ``.cpp`` and ``.h`` files + if tests are enabled. + """, + ), + "GENERATED_WEBIDL_FILES": ( + StrictOrderingOnAppendList, + list, + """Generated WebIDL source files. + + These will be generated from some other files. + """, + ), + "PREPROCESSED_TEST_WEBIDL_FILES": ( + StrictOrderingOnAppendList, + list, + """Preprocessed test WebIDL source files. + + These will be preprocessed, then parsed and converted to .cpp + and ``.h`` files if tests are enabled. + """, + ), + "PREPROCESSED_WEBIDL_FILES": ( + StrictOrderingOnAppendList, + list, + """Preprocessed WebIDL source files. + + These will be preprocessed before being parsed and converted. + """, + ), + "WEBIDL_EXAMPLE_INTERFACES": ( + StrictOrderingOnAppendList, + list, + """Names of example WebIDL interfaces to build as part of the build. + + Names in this list correspond to WebIDL interface names defined in + WebIDL files included in the build from one of the *WEBIDL_FILES + variables. + """, + ), + # Test declaration. + "A11Y_MANIFESTS": ( + ManifestparserManifestList, + list, + """List of manifest files defining a11y tests. + """, + ), + "BROWSER_CHROME_MANIFESTS": ( + ManifestparserManifestList, + list, + """List of manifest files defining browser chrome tests. + """, + ), + "ANDROID_INSTRUMENTATION_MANIFESTS": ( + ManifestparserManifestList, + list, + """List of manifest files defining Android instrumentation tests. + """, + ), + "FIREFOX_UI_FUNCTIONAL_MANIFESTS": ( + ManifestparserManifestList, + list, + """List of manifest files defining firefox-ui-functional tests. + """, + ), + "MARIONETTE_MANIFESTS": ( + ManifestparserManifestList, + list, + """List of manifest files defining marionette tests. + """, + ), + "METRO_CHROME_MANIFESTS": ( + ManifestparserManifestList, + list, + """List of manifest files defining metro browser chrome tests. + """, + ), + "MOCHITEST_CHROME_MANIFESTS": ( + ManifestparserManifestList, + list, + """List of manifest files defining mochitest chrome tests. + """, + ), + "MOCHITEST_MANIFESTS": ( + ManifestparserManifestList, + list, + """List of manifest files defining mochitest tests. + """, + ), + "REFTEST_MANIFESTS": ( + ReftestManifestList, + list, + """List of manifest files defining reftests. + + These are commonly named reftest.list. + """, + ), + "CRASHTEST_MANIFESTS": ( + ReftestManifestList, + list, + """List of manifest files defining crashtests. + + These are commonly named crashtests.list. + """, + ), + "XPCSHELL_TESTS_MANIFESTS": ( + ManifestparserManifestList, + list, + """List of manifest files defining xpcshell tests. + """, + ), + "PYTHON_UNITTEST_MANIFESTS": ( + ManifestparserManifestList, + list, + """List of manifest files defining python unit tests. + """, + ), + "PERFTESTS_MANIFESTS": ( + ManifestparserManifestList, + list, + """List of manifest files defining MozPerftest performance tests. + """, + ), + "CRAMTEST_MANIFESTS": ( + ManifestparserManifestList, + list, + """List of manifest files defining cram unit tests. + """, + ), + "TELEMETRY_TESTS_CLIENT_MANIFESTS": ( + ManifestparserManifestList, + list, + """List of manifest files defining telemetry client tests. + """, + ), + # The following variables are used to control the target of installed files. + "XPI_NAME": ( + six.text_type, + six.text_type, + """The name of an extension XPI to generate. + + When this variable is present, the results of this directory will end up + being packaged into an extension instead of the main dist/bin results. + """, + ), + "DIST_SUBDIR": ( + six.text_type, + six.text_type, + """The name of an alternate directory to install files to. + + When this variable is present, the results of this directory will end up + being placed in the $(DIST_SUBDIR) subdirectory of where it would + otherwise be placed. + """, + ), + "FINAL_TARGET": ( + FinalTargetValue, + six.text_type, + """The name of the directory to install targets to. + + The directory is relative to the top of the object directory. The + default value is dependent on the values of XPI_NAME and DIST_SUBDIR. If + neither are present, the result is dist/bin. If XPI_NAME is present, the + result is dist/xpi-stage/$(XPI_NAME). If DIST_SUBDIR is present, then + the $(DIST_SUBDIR) directory of the otherwise default value is used. + """, + ), + "USE_EXTENSION_MANIFEST": ( + bool, + bool, + """Controls the name of the manifest for JAR files. + + By default, the name of the manifest is ${JAR_MANIFEST}.manifest. + Setting this variable to ``True`` changes the name of the manifest to + chrome.manifest. + """, + ), + "GYP_DIRS": ( + StrictOrderingOnAppendListWithFlagsFactory( + { + "variables": dict, + "input": six.text_type, + "sandbox_vars": dict, + "no_chromium": bool, + "no_unified": bool, + "non_unified_sources": StrictOrderingOnAppendList, + "action_overrides": dict, + } + ), + list, + """Defines a list of object directories handled by gyp configurations. + + Elements of this list give the relative object directory. For each + element of the list, GYP_DIRS may be accessed as a dictionary + (GYP_DIRS[foo]). The object this returns has attributes that need to be + set to further specify gyp processing: + - input, gives the path to the root gyp configuration file for that + object directory. + - variables, a dictionary containing variables and values to pass + to the gyp processor. + - sandbox_vars, a dictionary containing variables and values to + pass to the mozbuild processor on top of those derived from gyp + configuration. + - no_chromium, a boolean which if set to True disables some + special handling that emulates gyp_chromium. + - no_unified, a boolean which if set to True disables source + file unification entirely. + - non_unified_sources, a list containing sources files, relative to + the current moz.build, that should be excluded from source file + unification. + - action_overrides, a dict of action_name to values of the `script` + attribute to use for GENERATED_FILES for the specified action. + + Typical use looks like: + GYP_DIRS += ['foo', 'bar'] + GYP_DIRS['foo'].input = 'foo/foo.gyp' + GYP_DIRS['foo'].variables = { + 'foo': 'bar', + (...) + } + (...) + """, + ), + "SPHINX_TREES": ( + dict, + dict, + """Describes what the Sphinx documentation tree will look like. + + Keys are relative directories inside the final Sphinx documentation + tree to install files into. Values are directories (relative to this + file) whose content to copy into the Sphinx documentation tree. + """, + ), + "SPHINX_PYTHON_PACKAGE_DIRS": ( + StrictOrderingOnAppendList, + list, + """Directories containing Python packages that Sphinx documents. + """, + ), + "COMPILE_FLAGS": ( + CompileFlags, + dict, + """Recipe for compile flags for this context. Not to be manipulated + directly. + """, + ), + "LINK_FLAGS": ( + LinkFlags, + dict, + """Recipe for linker flags for this context. Not to be manipulated + directly. + """, + ), + "WASM_FLAGS": ( + WasmFlags, + dict, + """Recipe for wasm flags for this context. Not to be + manipulated directly. + """, + ), + "ASM_FLAGS": ( + AsmFlags, + dict, + """Recipe for linker flags for this context. Not to be + manipulated directly. + """, + ), + "CFLAGS": ( + List, + list, + """Flags passed to the C compiler for all of the C source files + declared in this directory. + + Note that the ordering of flags matters here, these flags will be + added to the compiler's command line in the same order as they + appear in the moz.build file. + """, + ), + "CXXFLAGS": ( + List, + list, + """Flags passed to the C++ compiler for all of the C++ source files + declared in this directory. + + Note that the ordering of flags matters here; these flags will be + added to the compiler's command line in the same order as they + appear in the moz.build file. + """, + ), + "HOST_COMPILE_FLAGS": ( + HostCompileFlags, + dict, + """Recipe for host compile flags for this context. Not to be manipulated + directly. + """, + ), + "HOST_DEFINES": ( + InitializedDefines, + dict, + """Dictionary of compiler defines to declare for host compilation. + See ``DEFINES`` for specifics. + """, + ), + "WASM_CFLAGS": ( + List, + list, + """Flags passed to the C-to-wasm compiler for all of the C + source files declared in this directory. + + Note that the ordering of flags matters here, these flags will be + added to the compiler's command line in the same order as they + appear in the moz.build file. + """, + ), + "WASM_CXXFLAGS": ( + List, + list, + """Flags passed to the C++-to-wasm compiler for all of the + C++ source files declared in this directory. + + Note that the ordering of flags matters here; these flags will be + added to the compiler's command line in the same order as they + appear in the moz.build file. + """, + ), + "WASM_DEFINES": ( + InitializedDefines, + dict, + """Dictionary of compiler defines to declare for wasm compilation. + See ``DEFINES`` for specifics. + """, + ), + "WASM_LIBS": ( + List, + list, + """Wasm system link libraries. + + This variable contains a list of wasm system libaries to link against. + """, + ), + "CMFLAGS": ( + List, + list, + """Flags passed to the Objective-C compiler for all of the Objective-C + source files declared in this directory. + + Note that the ordering of flags matters here; these flags will be + added to the compiler's command line in the same order as they + appear in the moz.build file. + """, + ), + "CMMFLAGS": ( + List, + list, + """Flags passed to the Objective-C++ compiler for all of the + Objective-C++ source files declared in this directory. + + Note that the ordering of flags matters here; these flags will be + added to the compiler's command line in the same order as they + appear in the moz.build file. + """, + ), + "ASFLAGS": ( + List, + list, + """Flags passed to the assembler for all of the assembly source files + declared in this directory. + + Note that the ordering of flags matters here; these flags will be + added to the assembler's command line in the same order as they + appear in the moz.build file. + """, + ), + "HOST_CFLAGS": ( + List, + list, + """Flags passed to the host C compiler for all of the C source files + declared in this directory. + + Note that the ordering of flags matters here, these flags will be + added to the compiler's command line in the same order as they + appear in the moz.build file. + """, + ), + "HOST_CXXFLAGS": ( + List, + list, + """Flags passed to the host C++ compiler for all of the C++ source files + declared in this directory. + + Note that the ordering of flags matters here; these flags will be + added to the compiler's command line in the same order as they + appear in the moz.build file. + """, + ), + "LDFLAGS": ( + List, + list, + """Flags passed to the linker when linking all of the libraries and + executables declared in this directory. + + Note that the ordering of flags matters here; these flags will be + added to the linker's command line in the same order as they + appear in the moz.build file. + """, + ), + "EXTRA_DSO_LDOPTS": ( + List, + list, + """Flags passed to the linker when linking a shared library. + + Note that the ordering of flags matter here, these flags will be + added to the linker's command line in the same order as they + appear in the moz.build file. + """, + ), + "WIN32_EXE_LDFLAGS": ( + List, + list, + """Flags passed to the linker when linking a Windows .exe executable + declared in this directory. + + Note that the ordering of flags matter here, these flags will be + added to the linker's command line in the same order as they + appear in the moz.build file. + + This variable only has an effect on Windows. + """, + ), + "TEST_HARNESS_FILES": ( + ContextDerivedTypedHierarchicalStringList(Path), + list, + """List of files to be installed for test harnesses. + + ``TEST_HARNESS_FILES`` can be used to install files to any directory + under $objdir/_tests. Files can be appended to a field to indicate + which subdirectory they should be exported to. For example, + to export ``foo.py`` to ``_tests/foo``, append to + ``TEST_HARNESS_FILES`` like so:: + TEST_HARNESS_FILES.foo += ['foo.py'] + + Files from topsrcdir and the objdir can also be installed by prefixing + the path(s) with a '/' character and a '!' character, respectively:: + TEST_HARNESS_FILES.path += ['/build/bar.py', '!quux.py'] + """, + ), + "NO_EXPAND_LIBS": ( + bool, + bool, + """Forces to build a real static library, and no corresponding fake + library. + """, + ), + "USE_NASM": ( + bool, + bool, + """Use the nasm assembler to assemble assembly files from SOURCES. + + By default, the build will use the toolchain assembler, $(AS), to + assemble source files in assembly language (.s or .asm files). Setting + this value to ``True`` will cause it to use nasm instead. + + If nasm is not available on this system, or does not support the + current target architecture, an error will be raised. + """, + ), + "USE_INTEGRATED_CLANGCL_AS": ( + bool, + bool, + """Use the integrated clang-cl assembler to assemble assembly files from SOURCES. + + This allows using clang-cl to assemble assembly files which is useful + on platforms like aarch64 where the alternative is to have to run a + pre-processor to generate files with suitable syntax. + """, + ), +} + +# Sanity check: we don't want any variable above to have a list as storage type. +for name, (storage_type, input_types, docs) in VARIABLES.items(): + if storage_type == list: + raise RuntimeError('%s has a "list" storage type. Use "List" instead.' % name) + +# Set of variables that are only allowed in templates: +TEMPLATE_VARIABLES = { + "CPP_UNIT_TESTS", + "FORCE_SHARED_LIB", + "HOST_PROGRAM", + "HOST_LIBRARY_NAME", + "HOST_SIMPLE_PROGRAMS", + "IS_FRAMEWORK", + "IS_GKRUST", + "LIBRARY_NAME", + "PROGRAM", + "SIMPLE_PROGRAMS", +} + +# Add a note to template variable documentation. +for name in TEMPLATE_VARIABLES: + if name not in VARIABLES: + raise RuntimeError("%s is in TEMPLATE_VARIABLES but not in VARIABLES." % name) + storage_type, input_types, docs = VARIABLES[name] + docs += "This variable is only available in templates.\n" + VARIABLES[name] = (storage_type, input_types, docs) + + +# The set of functions exposed to the sandbox. +# +# Each entry is a tuple of: +# +# (function returning the corresponding function from a given sandbox, +# (argument types), docs) +# +# The first element is an attribute on Sandbox that should be a function type. +# +FUNCTIONS = { + "include": ( + lambda self: self._include, + (SourcePath,), + """Include another mozbuild file in the context of this one. + + This is similar to a ``#include`` in C languages. The filename passed to + the function will be read and its contents will be evaluated within the + context of the calling file. + + If a relative path is given, it is evaluated as relative to the file + currently being processed. If there is a chain of multiple include(), + the relative path computation is from the most recent/active file. + + If an absolute path is given, it is evaluated from ``TOPSRCDIR``. In + other words, ``include('/foo')`` references the path + ``TOPSRCDIR + '/foo'``. + + Example usage + ^^^^^^^^^^^^^ + + Include ``sibling.build`` from the current directory.:: + + include('sibling.build') + + Include ``foo.build`` from a path within the top source directory:: + + include('/elsewhere/foo.build') + """, + ), + "export": ( + lambda self: self._export, + (str,), + """Make the specified variable available to all child directories. + + The variable specified by the argument string is added to the + environment of all directories specified in the DIRS and TEST_DIRS + variables. If those directories themselves have child directories, + the variable will be exported to all of them. + + The value used for the variable is the final value at the end of the + moz.build file, so it is possible (but not recommended style) to place + the export before the definition of the variable. + + This function is limited to the upper-case variables that have special + meaning in moz.build files. + + NOTE: Please consult with a build peer (on the #build channel at + https://chat.mozilla.org) before adding a new use of this function. + + Example usage + ^^^^^^^^^^^^^ + + To make all children directories install as the given extension:: + + XPI_NAME = 'cool-extension' + export('XPI_NAME') + """, + ), + "warning": ( + lambda self: self._warning, + (str,), + """Issue a warning. + + Warnings are string messages that are printed during execution. + + Warnings are ignored during execution. + """, + ), + "error": ( + lambda self: self._error, + (str,), + """Issue a fatal error. + + If this function is called, processing is aborted immediately. + """, + ), + "template": ( + lambda self: self._template_decorator, + (FunctionType,), + """Decorator for template declarations. + + Templates are a special kind of functions that can be declared in + mozbuild files. Uppercase variables assigned in the function scope + are considered to be the result of the template. + + Contrary to traditional python functions: + - return values from template functions are ignored, + - template functions don't have access to the global scope. + + Example template + ^^^^^^^^^^^^^^^^ + + The following ``Program`` template sets two variables ``PROGRAM`` and + ``USE_LIBS``. ``PROGRAM`` is set to the argument given on the template + invocation, and ``USE_LIBS`` to contain "mozglue":: + + @template + def Program(name): + PROGRAM = name + USE_LIBS += ['mozglue'] + + Template invocation + ^^^^^^^^^^^^^^^^^^^ + + A template is invoked in the form of a function call:: + + Program('myprog') + + The result of the template, being all the uppercase variable it sets + is mixed to the existing set of variables defined in the mozbuild file + invoking the template:: + + FINAL_TARGET = 'dist/other' + USE_LIBS += ['mylib'] + Program('myprog') + USE_LIBS += ['otherlib'] + + The above mozbuild results in the following variables set: + + - ``FINAL_TARGET`` is 'dist/other' + - ``USE_LIBS`` is ['mylib', 'mozglue', 'otherlib'] + - ``PROGRAM`` is 'myprog' + + """, + ), +} + + +TestDirsPlaceHolder = List() + + +# Special variables. These complement VARIABLES. +# +# Each entry is a tuple of: +# +# (function returning the corresponding value from a given context, type, docs) +# +SPECIAL_VARIABLES = { + "TOPSRCDIR": ( + lambda context: context.config.topsrcdir, + str, + """Constant defining the top source directory. + + The top source directory is the parent directory containing the source + code and all build files. It is typically the root directory of a + cloned repository. + """, + ), + "TOPOBJDIR": ( + lambda context: context.config.topobjdir, + str, + """Constant defining the top object directory. + + The top object directory is the parent directory which will contain + the output of the build. This is commonly referred to as "the object + directory." + """, + ), + "RELATIVEDIR": ( + lambda context: context.relsrcdir, + str, + """Constant defining the relative path of this file. + + The relative path is from ``TOPSRCDIR``. This is defined as relative + to the main file being executed, regardless of whether additional + files have been included using ``include()``. + """, + ), + "SRCDIR": ( + lambda context: context.srcdir, + str, + """Constant defining the source directory of this file. + + This is the path inside ``TOPSRCDIR`` where this file is located. It + is the same as ``TOPSRCDIR + RELATIVEDIR``. + """, + ), + "OBJDIR": ( + lambda context: context.objdir, + str, + """The path to the object directory for this file. + + Is is the same as ``TOPOBJDIR + RELATIVEDIR``. + """, + ), + "CONFIG": ( + lambda context: ReadOnlyKeyedDefaultDict( + lambda key: context.config.substs.get(key) + ), + dict, + """Dictionary containing the current configuration variables. + + All the variables defined by the configuration system are available + through this object. e.g. ``ENABLE_TESTS``, ``CFLAGS``, etc. + + Values in this container are read-only. Attempts at changing values + will result in a run-time error. + + Access to an unknown variable will return None. + """, + ), + "EXTRA_COMPONENTS": ( + lambda context: context["FINAL_TARGET_FILES"].components._strings, + list, + """Additional component files to distribute. + + This variable contains a list of files to copy into + ``$(FINAL_TARGET)/components/``. + """, + ), + "EXTRA_PP_COMPONENTS": ( + lambda context: context["FINAL_TARGET_PP_FILES"].components._strings, + list, + """Javascript XPCOM files. + + This variable contains a list of files to preprocess. Generated + files will be installed in the ``/components`` directory of the distribution. + """, + ), + "JS_PREFERENCE_FILES": ( + lambda context: context["FINAL_TARGET_FILES"].defaults.pref._strings, + list, + """Exported JavaScript files. + + A list of files copied into the dist directory for packaging and installation. + Path will be defined for gre or application prefs dir based on what is building. + """, + ), + "JS_PREFERENCE_PP_FILES": ( + lambda context: context["FINAL_TARGET_PP_FILES"].defaults.pref._strings, + list, + """Like JS_PREFERENCE_FILES, preprocessed.. + """, + ), + "RESOURCE_FILES": ( + lambda context: context["FINAL_TARGET_FILES"].res, + list, + """List of resources to be exported, and in which subdirectories. + + ``RESOURCE_FILES`` is used to list the resource files to be exported to + ``dist/bin/res``, but it can be used for other files as well. This variable + behaves as a list when appending filenames for resources in the top-level + directory. Files can also be appended to a field to indicate which + subdirectory they should be exported to. For example, to export + ``foo.res`` to the top-level directory, and ``bar.res`` to ``fonts/``, + append to ``RESOURCE_FILES`` like so:: + + RESOURCE_FILES += ['foo.res'] + RESOURCE_FILES.fonts += ['bar.res'] + """, + ), + "CONTENT_ACCESSIBLE_FILES": ( + lambda context: context["FINAL_TARGET_FILES"].contentaccessible, + list, + """List of files which can be accessed by web content through resource:// URIs. + + ``CONTENT_ACCESSIBLE_FILES`` is used to list the files to be exported + to ``dist/bin/contentaccessible``. Files can also be appended to a + field to indicate which subdirectory they should be exported to. + """, + ), + "EXTRA_JS_MODULES": ( + lambda context: context["FINAL_TARGET_FILES"].modules, + list, + """Additional JavaScript files to distribute. + + This variable contains a list of files to copy into + ``$(FINAL_TARGET)/modules. + """, + ), + "EXTRA_PP_JS_MODULES": ( + lambda context: context["FINAL_TARGET_PP_FILES"].modules, + list, + """Additional JavaScript files to distribute. + + This variable contains a list of files to copy into + ``$(FINAL_TARGET)/modules``, after preprocessing. + """, + ), + "TESTING_JS_MODULES": ( + lambda context: context["TEST_HARNESS_FILES"].modules, + list, + """JavaScript modules to install in the test-only destination. + + Some JavaScript modules (JSMs) are test-only and not distributed + with Firefox. This variable defines them. + + To install modules in a subdirectory, use properties of this + variable to control the final destination. e.g. + + ``TESTING_JS_MODULES.foo += ['module.jsm']``. + """, + ), + "TEST_DIRS": ( + lambda context: context["DIRS"] + if context.config.substs.get("ENABLE_TESTS") + else TestDirsPlaceHolder, + list, + """Like DIRS but only for directories that contain test-only code. + + If tests are not enabled, this variable will be ignored. + + This variable may go away once the transition away from Makefiles is + complete. + """, + ), +} + +# Deprecation hints. +DEPRECATION_HINTS = { + "ASM_FLAGS": """ + Please use + + ASFLAGS + + instead of manipulating ASM_FLAGS directly. + """, + "CPP_UNIT_TESTS": """ + Please use' + + CppUnitTests(['foo', 'bar']) + + instead of + + CPP_UNIT_TESTS += ['foo', 'bar'] + """, + "DISABLE_STL_WRAPPING": """ + Please use + + DisableStlWrapping() + + instead of + + DISABLE_STL_WRAPPING = True + """, + "HOST_PROGRAM": """ + Please use + + HostProgram('foo') + + instead of + + HOST_PROGRAM = 'foo' + """, + "HOST_LIBRARY_NAME": """ + Please use + + HostLibrary('foo') + + instead of + + HOST_LIBRARY_NAME = 'foo' + """, + "HOST_SIMPLE_PROGRAMS": """ + Please use + + HostSimplePrograms(['foo', 'bar']) + + instead of + + HOST_SIMPLE_PROGRAMS += ['foo', 'bar']" + """, + "LIBRARY_NAME": """ + Please use + + Library('foo') + + instead of + + LIBRARY_NAME = 'foo' + """, + "NO_VISIBILITY_FLAGS": """ + Please use + + NoVisibilityFlags() + + instead of + + NO_VISIBILITY_FLAGS = True + """, + "PROGRAM": """ + Please use + + Program('foo') + + instead of + + PROGRAM = 'foo'" + """, + "SIMPLE_PROGRAMS": """ + Please use + + SimplePrograms(['foo', 'bar']) + + instead of + + SIMPLE_PROGRAMS += ['foo', 'bar']" + """, + "ALLOW_COMPILER_WARNINGS": """ + Please use + + AllowCompilerWarnings() + + instead of + + ALLOW_COMPILER_WARNINGS = True + """, + "FORCE_SHARED_LIB": """ + Please use + + SharedLibrary('foo') + + instead of + + Library('foo') [ or LIBRARY_NAME = 'foo' ] + FORCE_SHARED_LIB = True + """, + "IS_FRAMEWORK": """ + Please use + + Framework('foo') + + instead of + + Library('foo') [ or LIBRARY_NAME = 'foo' ] + IS_FRAMEWORK = True + """, + "IS_GKRUST": """ + Please use + + RustLibrary('gkrust', ... is_gkrust=True) + + instead of + + RustLibrary('gkrust') [ or LIBRARY_NAME = 'gkrust' ] + IS_GKRUST = True + """, + "TOOL_DIRS": "Please use the DIRS variable instead.", + "TEST_TOOL_DIRS": "Please use the TEST_DIRS variable instead.", + "PARALLEL_DIRS": "Please use the DIRS variable instead.", + "NO_DIST_INSTALL": """ + Please use + + DIST_INSTALL = False + + instead of + + NO_DIST_INSTALL = True + """, + "GENERATED_SOURCES": """ + Please use + + SOURCES += [ '!foo.cpp' ] + + instead of + + GENERATED_SOURCES += [ 'foo.cpp'] + """, + "GENERATED_INCLUDES": """ + Please use + + LOCAL_INCLUDES += [ '!foo' ] + + instead of + + GENERATED_INCLUDES += [ 'foo' ] + """, + "DIST_FILES": """ + Please use + + FINAL_TARGET_PP_FILES += [ 'foo' ] + + instead of + + DIST_FILES += [ 'foo' ] + """, +} + +# Make sure that all template variables have a deprecation hint. +for name in TEMPLATE_VARIABLES: + if name not in DEPRECATION_HINTS: + raise RuntimeError("Missing deprecation hint for %s" % name) diff --git a/python/mozbuild/mozbuild/frontend/data.py b/python/mozbuild/mozbuild/frontend/data.py new file mode 100644 index 0000000000..4217196400 --- /dev/null +++ b/python/mozbuild/mozbuild/frontend/data.py @@ -0,0 +1,1369 @@ +# 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/. + +r"""Data structures representing Mozilla's source tree. + +The frontend files are parsed into static data structures. These data +structures are defined in this module. + +All data structures of interest are children of the TreeMetadata class. + +Logic for populating these data structures is not defined in this class. +Instead, what we have here are dumb container classes. The emitter module +contains the code for converting executed mozbuild files into these data +structures. +""" + +from collections import OrderedDict, defaultdict + +import mozpack.path as mozpath +import six +from mozpack.chrome.manifest import ManifestEntry + +from mozbuild.frontend.context import ObjDirPath, SourcePath + +from ..testing import all_test_flavors +from ..util import group_unified_files +from .context import FinalTargetValue + + +class TreeMetadata(object): + """Base class for all data being captured.""" + + __slots__ = () + + def to_dict(self): + return {k.lower(): getattr(self, k) for k in self.DICT_ATTRS} + + +class ContextDerived(TreeMetadata): + """Build object derived from a single Context instance. + + It holds fields common to all context derived classes. This class is likely + never instantiated directly but is instead derived from. + """ + + __slots__ = ( + "context_main_path", + "context_all_paths", + "topsrcdir", + "topobjdir", + "relsrcdir", + "srcdir", + "objdir", + "config", + "_context", + ) + + def __init__(self, context): + TreeMetadata.__init__(self) + + # Capture the files that were evaluated to fill this context. + self.context_main_path = context.main_path + self.context_all_paths = context.all_paths + + # Basic directory state. + self.topsrcdir = context.config.topsrcdir + self.topobjdir = context.config.topobjdir + + self.relsrcdir = context.relsrcdir + self.srcdir = context.srcdir + self.objdir = context.objdir + + self.config = context.config + + self._context = context + + @property + def install_target(self): + return self._context["FINAL_TARGET"] + + @property + def installed(self): + return self._context["DIST_INSTALL"] is not False + + @property + def defines(self): + defines = self._context["DEFINES"] + return Defines(self._context, defines) if defines else None + + @property + def relobjdir(self): + return mozpath.relpath(self.objdir, self.topobjdir) + + +class HostMixin(object): + @property + def defines(self): + defines = self._context["HOST_DEFINES"] + return HostDefines(self._context, defines) if defines else None + + +class DirectoryTraversal(ContextDerived): + """Describes how directory traversal for building should work. + + This build object is likely only of interest to the recursive make backend. + Other build backends should (ideally) not attempt to mimic the behavior of + the recursive make backend. The only reason this exists is to support the + existing recursive make backend while the transition to mozbuild frontend + files is complete and we move to a more optimal build backend. + + Fields in this class correspond to similarly named variables in the + frontend files. + """ + + __slots__ = ("dirs",) + + def __init__(self, context): + ContextDerived.__init__(self, context) + + self.dirs = [] + + +class BaseConfigSubstitution(ContextDerived): + """Base class describing autogenerated files as part of config.status.""" + + __slots__ = ("input_path", "output_path", "relpath") + + def __init__(self, context): + ContextDerived.__init__(self, context) + + self.input_path = None + self.output_path = None + self.relpath = None + + +class ConfigFileSubstitution(BaseConfigSubstitution): + """Describes a config file that will be generated using substitutions.""" + + +class VariablePassthru(ContextDerived): + """A dict of variables to pass through to backend.mk unaltered. + + The purpose of this object is to facilitate rapid transitioning of + variables from Makefile.in to moz.build. In the ideal world, this class + does not exist and every variable has a richer class representing it. + As long as we rely on this class, we lose the ability to have flexibility + in our build backends since we will continue to be tied to our rules.mk. + """ + + __slots__ = "variables" + + def __init__(self, context): + ContextDerived.__init__(self, context) + self.variables = {} + + +class ComputedFlags(ContextDerived): + """Aggregate flags for consumption by various backends.""" + + __slots__ = ("flags",) + + def __init__(self, context, reader_flags): + ContextDerived.__init__(self, context) + self.flags = reader_flags + + def resolve_flags(self, key, value): + # Bypass checks done by CompileFlags that would keep us from + # setting a value here. + dict.__setitem__(self.flags, key, value) + + def get_flags(self): + flags = defaultdict(list) + for key, _, dest_vars in self.flags.flag_variables: + value = self.flags.get(key) + if value: + for dest_var in dest_vars: + flags[dest_var].extend(value) + return sorted(flags.items()) + + +class XPIDLModule(ContextDerived): + """Describes an XPIDL module to be compiled.""" + + __slots__ = ("name", "idl_files") + + def __init__(self, context, name, idl_files): + ContextDerived.__init__(self, context) + + assert all(isinstance(idl, SourcePath) for idl in idl_files) + self.name = name + self.idl_files = idl_files + + +class BaseDefines(ContextDerived): + """Context derived container object for DEFINES/HOST_DEFINES, + which are OrderedDicts. + """ + + __slots__ = "defines" + + def __init__(self, context, defines): + ContextDerived.__init__(self, context) + self.defines = defines + + def get_defines(self): + for define, value in six.iteritems(self.defines): + if value is True: + yield ("-D%s" % define) + elif value is False: + yield ("-U%s" % define) + else: + yield ("-D%s=%s" % (define, value)) + + def update(self, more_defines): + if isinstance(more_defines, Defines): + self.defines.update(more_defines.defines) + else: + self.defines.update(more_defines) + + +class Defines(BaseDefines): + pass + + +class HostDefines(BaseDefines): + pass + + +class WasmDefines(BaseDefines): + pass + + +class WebIDLCollection(ContextDerived): + """Collects WebIDL info referenced during the build.""" + + def __init__(self, context): + ContextDerived.__init__(self, context) + self.sources = set() + self.generated_sources = set() + self.generated_events_sources = set() + self.preprocessed_sources = set() + self.test_sources = set() + self.preprocessed_test_sources = set() + self.example_interfaces = set() + + def all_regular_sources(self): + return ( + self.sources + | self.generated_sources + | self.generated_events_sources + | self.preprocessed_sources + ) + + def all_regular_basenames(self): + return [mozpath.basename(source) for source in self.all_regular_sources()] + + def all_regular_stems(self): + return [mozpath.splitext(b)[0] for b in self.all_regular_basenames()] + + def all_regular_bindinggen_stems(self): + for stem in self.all_regular_stems(): + yield "%sBinding" % stem + + for source in self.generated_events_sources: + yield mozpath.splitext(mozpath.basename(source))[0] + + def all_regular_cpp_basenames(self): + for stem in self.all_regular_bindinggen_stems(): + yield "%s.cpp" % stem + + def all_test_sources(self): + return self.test_sources | self.preprocessed_test_sources + + def all_test_basenames(self): + return [mozpath.basename(source) for source in self.all_test_sources()] + + def all_test_stems(self): + return [mozpath.splitext(b)[0] for b in self.all_test_basenames()] + + def all_test_cpp_basenames(self): + return sorted("%sBinding.cpp" % s for s in self.all_test_stems()) + + def all_static_sources(self): + return self.sources | self.generated_events_sources | self.test_sources + + def all_non_static_sources(self): + return self.generated_sources | self.all_preprocessed_sources() + + def all_non_static_basenames(self): + return [mozpath.basename(s) for s in self.all_non_static_sources()] + + def all_preprocessed_sources(self): + return self.preprocessed_sources | self.preprocessed_test_sources + + def all_sources(self): + return set(self.all_regular_sources()) | set(self.all_test_sources()) + + def all_basenames(self): + return [mozpath.basename(source) for source in self.all_sources()] + + def all_stems(self): + return [mozpath.splitext(b)[0] for b in self.all_basenames()] + + def generated_events_basenames(self): + return [mozpath.basename(s) for s in self.generated_events_sources] + + def generated_events_stems(self): + return [mozpath.splitext(b)[0] for b in self.generated_events_basenames()] + + @property + def unified_source_mapping(self): + # Bindings are compiled in unified mode to speed up compilation and + # to reduce linker memory size. Note that test bindings are separated + # from regular ones so tests bindings aren't shipped. + return list( + group_unified_files( + sorted(self.all_regular_cpp_basenames()), + unified_prefix="UnifiedBindings", + unified_suffix="cpp", + files_per_unified_file=32, + ) + ) + + def all_source_files(self): + from mozwebidlcodegen import WebIDLCodegenManager + + return sorted(list(WebIDLCodegenManager.GLOBAL_DEFINE_FILES)) + sorted( + set(p for p, _ in self.unified_source_mapping) + ) + + +class IPDLCollection(ContextDerived): + """Collects IPDL files during the build.""" + + def __init__(self, context): + ContextDerived.__init__(self, context) + self.sources = set() + self.preprocessed_sources = set() + + def all_sources(self): + return self.sources | self.preprocessed_sources + + def all_regular_sources(self): + return self.sources + + def all_preprocessed_sources(self): + return self.preprocessed_sources + + def all_source_files(self): + # Source files generated by IPDL are built as generated UnifiedSources + # from the context which included the IPDL file, rather than the context + # which builds the IPDLCollection, so we report no files here. + return [] + + +class XPCOMComponentManifests(ContextDerived): + """Collects XPCOM manifest files during the build.""" + + def __init__(self, context): + ContextDerived.__init__(self, context) + self.manifests = set() + + def all_sources(self): + return self.manifests + + def all_source_files(self): + return [] + + +class LinkageWrongKindError(Exception): + """Error thrown when trying to link objects of the wrong kind""" + + +class Linkable(ContextDerived): + """Generic context derived container object for programs and libraries""" + + __slots__ = ( + "cxx_link", + "lib_defines", + "linked_libraries", + "linked_system_libs", + "sources", + ) + + def __init__(self, context): + ContextDerived.__init__(self, context) + self.cxx_link = False + self.linked_libraries = [] + self.linked_system_libs = [] + self.lib_defines = Defines(context, OrderedDict()) + self.sources = defaultdict(list) + + def link_library(self, obj): + assert isinstance(obj, BaseLibrary) + if obj.KIND != self.KIND: + raise LinkageWrongKindError("%s != %s" % (obj.KIND, self.KIND)) + self.linked_libraries.append(obj) + if obj.cxx_link and not isinstance(obj, SharedLibrary): + self.cxx_link = True + obj.refs.append(self) + + def link_system_library(self, lib): + # The '$' check is here as a special temporary rule, allowing the + # inherited use of make variables, most notably in TK_LIBS. + if not lib.startswith("$") and not lib.startswith("-"): + type_var = "HOST_CC_TYPE" if self.KIND == "host" else "CC_TYPE" + compiler_type = self.config.substs.get(type_var) + if compiler_type in ("gcc", "clang"): + lib = "-l%s" % lib + elif self.KIND == "host": + lib = "%s%s%s" % ( + self.config.host_import_prefix, + lib, + self.config.host_import_suffix, + ) + else: + lib = "%s%s%s" % ( + self.config.import_prefix, + lib, + self.config.import_suffix, + ) + self.linked_system_libs.append(lib) + + def source_files(self): + all_sources = [] + # This is ordered for reproducibility and consistently w/ + # config/rules.mk + for suffix in (".c", ".S", ".cpp", ".m", ".mm", ".s"): + all_sources += self.sources.get(suffix, []) + return all_sources + + def _get_objs(self, sources): + obj_prefix = "" + if self.KIND == "host": + obj_prefix = "host_" + + return [ + mozpath.join( + self.objdir, + "%s%s.%s" + % ( + obj_prefix, + mozpath.splitext(mozpath.basename(f))[0], + self._obj_suffix(), + ), + ) + for f in sources + ] + + def _obj_suffix(self): + """Can be overridden by a base class for custom behavior.""" + return self.config.substs.get("OBJ_SUFFIX", "") + + @property + def objs(self): + return self._get_objs(self.source_files()) + + +class BaseProgram(Linkable): + """Context derived container object for programs, which is a unicode + string. + + This class handles automatically appending a binary suffix to the program + name. + If the suffix is not defined, the program name is unchanged. + Otherwise, if the program name ends with the given suffix, it is unchanged + Otherwise, the suffix is appended to the program name. + """ + + __slots__ = "program" + + DICT_ATTRS = {"install_target", "KIND", "program", "relobjdir"} + + def __init__(self, context, program, is_unit_test=False): + Linkable.__init__(self, context) + + bin_suffix = context.config.substs.get(self.SUFFIX_VAR, "") + if not program.endswith(bin_suffix): + program += bin_suffix + self.program = program + self.is_unit_test = is_unit_test + + @property + def output_path(self): + if self.installed: + return ObjDirPath( + self._context, "!/" + mozpath.join(self.install_target, self.program) + ) + else: + return ObjDirPath(self._context, "!" + self.program) + + def __repr__(self): + return "<%s: %s/%s>" % (type(self).__name__, self.relobjdir, self.program) + + @property + def name(self): + return self.program + + +class Program(BaseProgram): + """Context derived container object for PROGRAM""" + + SUFFIX_VAR = "BIN_SUFFIX" + KIND = "target" + + +class HostProgram(HostMixin, BaseProgram): + """Context derived container object for HOST_PROGRAM""" + + SUFFIX_VAR = "HOST_BIN_SUFFIX" + KIND = "host" + + @property + def install_target(self): + return "dist/host/bin" + + +class SimpleProgram(BaseProgram): + """Context derived container object for each program in SIMPLE_PROGRAMS""" + + SUFFIX_VAR = "BIN_SUFFIX" + KIND = "target" + + def source_files(self): + for srcs in self.sources.values(): + for f in srcs: + if ( + mozpath.basename(mozpath.splitext(f)[0]) + == mozpath.splitext(self.program)[0] + ): + return [f] + return [] + + +class HostSimpleProgram(HostMixin, BaseProgram): + """Context derived container object for each program in + HOST_SIMPLE_PROGRAMS""" + + SUFFIX_VAR = "HOST_BIN_SUFFIX" + KIND = "host" + + def source_files(self): + for srcs in self.sources.values(): + for f in srcs: + if ( + "host_%s" % mozpath.basename(mozpath.splitext(f)[0]) + == mozpath.splitext(self.program)[0] + ): + return [f] + return [] + + +def cargo_output_directory(context, target_var): + # cargo creates several directories and places its build artifacts + # in those directories. The directory structure depends not only + # on the target, but also what sort of build we are doing. + rust_build_kind = "release" + if context.config.substs.get("MOZ_DEBUG_RUST"): + rust_build_kind = "debug" + return mozpath.join(context.config.substs[target_var], rust_build_kind) + + +# We pretend Rust programs are Linkable, despite Cargo handling all the details +# of linking things. This is used to declare in-tree dependencies. +class BaseRustProgram(Linkable): + __slots__ = ( + "name", + "cargo_file", + "location", + "SUFFIX_VAR", + "KIND", + "TARGET_SUBST_VAR", + ) + + def __init__(self, context, name, cargo_file): + Linkable.__init__(self, context) + self.name = name + self.cargo_file = cargo_file + # Skip setting properties below which depend on cargo + # when we don't have a compile environment. The required + # config keys won't be available, but the instance variables + # that we don't set should never be accessed by the actual + # build in that case. + if not context.config.substs.get("COMPILE_ENVIRONMENT"): + return + cargo_dir = cargo_output_directory(context, self.TARGET_SUBST_VAR) + exe_file = "%s%s" % (name, context.config.substs.get(self.SUFFIX_VAR, "")) + self.location = mozpath.join(cargo_dir, exe_file) + + +class RustProgram(BaseRustProgram): + SUFFIX_VAR = "BIN_SUFFIX" + KIND = "target" + TARGET_SUBST_VAR = "RUST_TARGET" + + +class HostRustProgram(BaseRustProgram): + SUFFIX_VAR = "HOST_BIN_SUFFIX" + KIND = "host" + TARGET_SUBST_VAR = "RUST_HOST_TARGET" + + +class RustTests(ContextDerived): + __slots__ = ("names", "features", "output_category") + + def __init__(self, context, names, features): + ContextDerived.__init__(self, context) + self.names = names + self.features = features + self.output_category = "rusttests" + + +class BaseLibrary(Linkable): + """Generic context derived container object for libraries.""" + + __slots__ = ("basename", "lib_name", "import_name", "refs") + + def __init__(self, context, basename): + Linkable.__init__(self, context) + + self.basename = self.lib_name = basename + if self.lib_name: + self.lib_name = "%s%s%s" % ( + context.config.lib_prefix, + self.lib_name, + context.config.lib_suffix, + ) + self.import_name = self.lib_name + + self.refs = [] + + def __repr__(self): + return "<%s: %s/%s>" % (type(self).__name__, self.relobjdir, self.lib_name) + + @property + def name(self): + return self.lib_name + + +class Library(BaseLibrary): + """Context derived container object for a library""" + + KIND = "target" + __slots__ = () + + def __init__(self, context, basename, real_name=None): + BaseLibrary.__init__(self, context, real_name or basename) + self.basename = basename + + +class StaticLibrary(Library): + """Context derived container object for a static library""" + + __slots__ = ("link_into", "no_expand_lib") + + def __init__( + self, context, basename, real_name=None, link_into=None, no_expand_lib=False + ): + Library.__init__(self, context, basename, real_name) + self.link_into = link_into + self.no_expand_lib = no_expand_lib + + +class SandboxedWasmLibrary(Library): + """Context derived container object for a static sandboxed wasm library""" + + # This is a real static library; make it known to the build system. + no_expand_lib = True + KIND = "wasm" + + def __init__(self, context, basename, real_name=None): + Library.__init__(self, context, basename, real_name) + + # Wasm libraries are not going to compile unless we have a compiler + # for them. + assert context.config.substs["WASM_CC"] and context.config.substs["WASM_CXX"] + + self.lib_name = "%s%s%s" % ( + context.config.dll_prefix, + real_name or basename, + context.config.dll_suffix, + ) + + def _obj_suffix(self): + """Can be overridden by a base class for custom behavior.""" + return self.config.substs.get("WASM_OBJ_SUFFIX", "") + + +class BaseRustLibrary(object): + slots = ( + "cargo_file", + "crate_type", + "dependencies", + "deps_path", + "features", + "output_category", + "is_gkrust", + ) + + def init( + self, + context, + basename, + cargo_file, + crate_type, + dependencies, + features, + is_gkrust, + ): + self.is_gkrust = is_gkrust + self.cargo_file = cargo_file + self.crate_type = crate_type + # We need to adjust our naming here because cargo replaces '-' in + # package names defined in Cargo.toml with underscores in actual + # filenames. But we need to keep the basename consistent because + # many other things in the build system depend on that. + assert self.crate_type == "staticlib" + self.lib_name = "%s%s%s" % ( + context.config.lib_prefix, + basename.replace("-", "_"), + context.config.lib_suffix, + ) + self.dependencies = dependencies + self.features = features + self.output_category = context.get("RUST_LIBRARY_OUTPUT_CATEGORY") + # Skip setting properties below which depend on cargo + # when we don't have a compile environment. The required + # config keys won't be available, but the instance variables + # that we don't set should never be accessed by the actual + # build in that case. + if not context.config.substs.get("COMPILE_ENVIRONMENT"): + return + build_dir = mozpath.join( + context.config.topobjdir, + cargo_output_directory(context, self.TARGET_SUBST_VAR), + ) + self.import_name = mozpath.join(build_dir, self.lib_name) + self.deps_path = mozpath.join(build_dir, "deps") + + +class RustLibrary(StaticLibrary, BaseRustLibrary): + """Context derived container object for a rust static library""" + + KIND = "target" + TARGET_SUBST_VAR = "RUST_TARGET" + FEATURES_VAR = "RUST_LIBRARY_FEATURES" + LIB_FILE_VAR = "RUST_LIBRARY_FILE" + __slots__ = BaseRustLibrary.slots + + def __init__( + self, + context, + basename, + cargo_file, + crate_type, + dependencies, + features, + is_gkrust=False, + link_into=None, + ): + StaticLibrary.__init__( + self, + context, + basename, + link_into=link_into, + # A rust library is a real static library ; make + # it known to the build system. + no_expand_lib=True, + ) + BaseRustLibrary.init( + self, + context, + basename, + cargo_file, + crate_type, + dependencies, + features, + is_gkrust, + ) + + +class SharedLibrary(Library): + """Context derived container object for a shared library""" + + __slots__ = ( + "soname", + "variant", + "symbols_file", + "output_category", + "symbols_link_arg", + ) + + DICT_ATTRS = { + "basename", + "import_name", + "install_target", + "lib_name", + "relobjdir", + "soname", + } + + FRAMEWORK = 1 + MAX_VARIANT = 2 + + def __init__( + self, + context, + basename, + real_name=None, + soname=None, + variant=None, + symbols_file=False, + ): + assert variant in range(1, self.MAX_VARIANT) or variant is None + Library.__init__(self, context, basename, real_name) + self.variant = variant + self.lib_name = real_name or basename + self.output_category = context.get("SHARED_LIBRARY_OUTPUT_CATEGORY") + assert self.lib_name + + if variant == self.FRAMEWORK: + self.import_name = self.lib_name + else: + self.import_name = "%s%s%s" % ( + context.config.import_prefix, + self.lib_name, + context.config.import_suffix, + ) + self.lib_name = "%s%s%s" % ( + context.config.dll_prefix, + self.lib_name, + context.config.dll_suffix, + ) + if soname: + self.soname = "%s%s%s" % ( + context.config.dll_prefix, + soname, + context.config.dll_suffix, + ) + else: + self.soname = self.lib_name + + if symbols_file is False: + # No symbols file. + self.symbols_file = None + elif symbols_file is True: + # Symbols file with default name. + if context.config.substs["OS_TARGET"] == "WINNT": + self.symbols_file = "%s.def" % self.lib_name + else: + self.symbols_file = "%s.symbols" % self.lib_name + else: + # Explicitly provided name. + self.symbols_file = symbols_file + + if self.symbols_file: + os_target = context.config.substs["OS_TARGET"] + if os_target == "Darwin": + self.symbols_link_arg = ( + "-Wl,-exported_symbols_list," + self.symbols_file + ) + elif os_target == "SunOS": + self.symbols_link_arg = ( + "-z gnu-version-script-compat -Wl,--version-script," + + self.symbols_file + ) + elif os_target == "WINNT": + if context.config.substs.get("GNU_CC"): + self.symbols_link_arg = self.symbols_file + else: + self.symbols_link_arg = "-DEF:" + self.symbols_file + elif context.config.substs.get("GCC_USE_GNU_LD"): + self.symbols_link_arg = "-Wl,--version-script," + self.symbols_file + + +class HostSharedLibrary(HostMixin, Library): + """Context derived container object for a host shared library. + + This class supports less things than SharedLibrary does for target shared + libraries. Currently has enough build system support to build the clang + plugin.""" + + KIND = "host" + + def __init__(self, context, basename): + Library.__init__(self, context, basename) + self.lib_name = "%s%s%s" % ( + context.config.host_dll_prefix, + self.basename, + context.config.host_dll_suffix, + ) + + +class ExternalLibrary(object): + """Empty mixin for libraries built by an external build system.""" + + +class ExternalStaticLibrary(StaticLibrary, ExternalLibrary): + """Context derived container for static libraries built by an external + build system.""" + + +class ExternalSharedLibrary(SharedLibrary, ExternalLibrary): + """Context derived container for shared libraries built by an external + build system.""" + + +class HostLibrary(HostMixin, BaseLibrary): + """Context derived container object for a host library""" + + KIND = "host" + no_expand_lib = False + + +class HostRustLibrary(HostLibrary, BaseRustLibrary): + """Context derived container object for a host rust library""" + + KIND = "host" + TARGET_SUBST_VAR = "RUST_HOST_TARGET" + FEATURES_VAR = "HOST_RUST_LIBRARY_FEATURES" + LIB_FILE_VAR = "HOST_RUST_LIBRARY_FILE" + __slots__ = BaseRustLibrary.slots + no_expand_lib = True + + def __init__( + self, + context, + basename, + cargo_file, + crate_type, + dependencies, + features, + is_gkrust, + ): + HostLibrary.__init__(self, context, basename) + BaseRustLibrary.init( + self, + context, + basename, + cargo_file, + crate_type, + dependencies, + features, + is_gkrust, + ) + + +class TestManifest(ContextDerived): + """Represents a manifest file containing information about tests.""" + + __slots__ = ( + # The type of test manifest this is. + "flavor", + # Maps source filename to destination filename. The destination + # path is relative from the tests root directory. Values are 2-tuples + # of (destpath, is_test_file) where the 2nd item is True if this + # item represents a test file (versus a support file). + "installs", + # A list of pattern matching installs to perform. Entries are + # (base, pattern, dest). + "pattern_installs", + # Where all files for this manifest flavor are installed in the unified + # test package directory. + "install_prefix", + # Set of files provided by an external mechanism. + "external_installs", + # Set of files required by multiple test directories, whose installation + # will be resolved when running tests. + "deferred_installs", + # The full path of this manifest file. + "path", + # The directory where this manifest is defined. + "directory", + # The parsed manifestparser.TestManifest instance. + "manifest", + # List of tests. Each element is a dict of metadata. + "tests", + # The relative path of the parsed manifest within the srcdir. + "manifest_relpath", + # The relative path of the parsed manifest within the objdir. + "manifest_obj_relpath", + # The relative paths to all source files for this manifest. + "source_relpaths", + # If this manifest is a duplicate of another one, this is the + # manifestparser.TestManifest of the other one. + "dupe_manifest", + ) + + def __init__( + self, + context, + path, + manifest, + flavor=None, + install_prefix=None, + relpath=None, + sources=(), + dupe_manifest=False, + ): + ContextDerived.__init__(self, context) + + assert flavor in all_test_flavors() + + self.path = path + self.directory = mozpath.dirname(path) + self.manifest = manifest + self.flavor = flavor + self.install_prefix = install_prefix + self.manifest_relpath = relpath + self.manifest_obj_relpath = relpath + self.source_relpaths = sources + self.dupe_manifest = dupe_manifest + self.installs = {} + self.pattern_installs = [] + self.tests = [] + self.external_installs = set() + self.deferred_installs = set() + + +class LocalInclude(ContextDerived): + """Describes an individual local include path.""" + + __slots__ = ("path",) + + def __init__(self, context, path): + ContextDerived.__init__(self, context) + + self.path = path + + +class PerSourceFlag(ContextDerived): + """Describes compiler flags specified for individual source files.""" + + __slots__ = ("file_name", "flags") + + def __init__(self, context, file_name, flags): + ContextDerived.__init__(self, context) + + self.file_name = file_name + self.flags = flags + + +class JARManifest(ContextDerived): + """Describes an individual JAR manifest file and how to process it. + + This class isn't very useful for optimizing backends yet because we don't + capture defines. We can't capture defines safely until all of them are + defined in moz.build and not Makefile.in files. + """ + + __slots__ = ("path",) + + def __init__(self, context, path): + ContextDerived.__init__(self, context) + + self.path = path + + +class BaseSources(ContextDerived): + """Base class for files to be compiled during the build.""" + + __slots__ = ("files", "static_files", "generated_files", "canonical_suffix") + + def __init__(self, context, static_files, generated_files, canonical_suffix): + ContextDerived.__init__(self, context) + + # Sorted so output is consistent and we don't bump mtimes, but always + # order generated files after static ones to be consistent across build + # environments, which may have different objdir paths relative to + # topsrcdir. + self.static_files = sorted(static_files) + self.generated_files = sorted(generated_files) + self.files = self.static_files + self.generated_files + self.canonical_suffix = canonical_suffix + + +class Sources(BaseSources): + """Represents files to be compiled during the build.""" + + def __init__(self, context, static_files, generated_files, canonical_suffix): + BaseSources.__init__( + self, context, static_files, generated_files, canonical_suffix + ) + + +class PgoGenerateOnlySources(BaseSources): + """Represents files to be compiled during the build. + + These files are only used during the PGO generation phase.""" + + def __init__(self, context, files): + BaseSources.__init__(self, context, files, [], ".cpp") + + +class HostSources(HostMixin, BaseSources): + """Represents files to be compiled for the host during the build.""" + + def __init__(self, context, static_files, generated_files, canonical_suffix): + BaseSources.__init__( + self, context, static_files, generated_files, canonical_suffix + ) + + +class WasmSources(BaseSources): + """Represents files to be compiled with the wasm compiler during the build.""" + + def __init__(self, context, static_files, generated_files, canonical_suffix): + BaseSources.__init__( + self, context, static_files, generated_files, canonical_suffix + ) + + +class UnifiedSources(BaseSources): + """Represents files to be compiled in a unified fashion during the build.""" + + __slots__ = ("have_unified_mapping", "unified_source_mapping") + + def __init__(self, context, static_files, generated_files, canonical_suffix): + BaseSources.__init__( + self, context, static_files, generated_files, canonical_suffix + ) + + unified_build = context.config.substs.get("ENABLE_UNIFIED_BUILD", False) + files_per_unified_file = ( + context.get("FILES_PER_UNIFIED_FILE", 16) if unified_build else 1 + ) + + self.have_unified_mapping = files_per_unified_file > 1 + + if self.have_unified_mapping: + # On Windows, path names have a maximum length of 255 characters, + # so avoid creating extremely long path names. + unified_prefix = context.relsrcdir + if len(unified_prefix) > 20: + unified_prefix = unified_prefix[-20:].split("/", 1)[-1] + unified_prefix = unified_prefix.replace("/", "_") + + suffix = self.canonical_suffix[1:] + unified_prefix = "Unified_%s_%s" % (suffix, unified_prefix) + self.unified_source_mapping = list( + group_unified_files( + # NOTE: self.files is already (partially) sorted, and we + # intentionally do not re-sort it here to avoid a dependency + # on the build environment's objdir path. + self.files, + unified_prefix=unified_prefix, + unified_suffix=suffix, + files_per_unified_file=files_per_unified_file, + ) + ) + + +class InstallationTarget(ContextDerived): + """Describes the rules that affect where files get installed to.""" + + __slots__ = ("xpiname", "subdir", "target", "enabled") + + def __init__(self, context): + ContextDerived.__init__(self, context) + + self.xpiname = context.get("XPI_NAME", "") + self.subdir = context.get("DIST_SUBDIR", "") + self.target = context["FINAL_TARGET"] + self.enabled = context["DIST_INSTALL"] is not False + + def is_custom(self): + """Returns whether or not the target is not derived from the default + given xpiname and subdir.""" + + return ( + FinalTargetValue(dict(XPI_NAME=self.xpiname, DIST_SUBDIR=self.subdir)) + == self.target + ) + + +class FinalTargetFiles(ContextDerived): + """Sandbox container object for FINAL_TARGET_FILES, which is a + HierarchicalStringList. + + We need an object derived from ContextDerived for use in the backend, so + this object fills that role. It just has a reference to the underlying + HierarchicalStringList, which is created when parsing FINAL_TARGET_FILES. + """ + + __slots__ = "files" + + def __init__(self, sandbox, files): + ContextDerived.__init__(self, sandbox) + self.files = files + + +class FinalTargetPreprocessedFiles(ContextDerived): + """Sandbox container object for FINAL_TARGET_PP_FILES, which is a + HierarchicalStringList. + + We need an object derived from ContextDerived for use in the backend, so + this object fills that role. It just has a reference to the underlying + HierarchicalStringList, which is created when parsing + FINAL_TARGET_PP_FILES. + """ + + __slots__ = "files" + + def __init__(self, sandbox, files): + ContextDerived.__init__(self, sandbox) + self.files = files + + +class LocalizedFiles(FinalTargetFiles): + """Sandbox container object for LOCALIZED_FILES, which is a + HierarchicalStringList. + """ + + pass + + +class LocalizedPreprocessedFiles(FinalTargetPreprocessedFiles): + """Sandbox container object for LOCALIZED_PP_FILES, which is a + HierarchicalStringList. + """ + + pass + + +class ObjdirFiles(FinalTargetFiles): + """Sandbox container object for OBJDIR_FILES, which is a + HierarchicalStringList. + """ + + @property + def install_target(self): + return "" + + +class ObjdirPreprocessedFiles(FinalTargetPreprocessedFiles): + """Sandbox container object for OBJDIR_PP_FILES, which is a + HierarchicalStringList. + """ + + @property + def install_target(self): + return "" + + +class TestHarnessFiles(FinalTargetFiles): + """Sandbox container object for TEST_HARNESS_FILES, + which is a HierarchicalStringList. + """ + + @property + def install_target(self): + return "_tests" + + +class Exports(FinalTargetFiles): + """Context derived container object for EXPORTS, which is a + HierarchicalStringList. + + We need an object derived from ContextDerived for use in the backend, so + this object fills that role. It just has a reference to the underlying + HierarchicalStringList, which is created when parsing EXPORTS. + """ + + @property + def install_target(self): + return "dist/include" + + +class GeneratedFile(ContextDerived): + """Represents a generated file.""" + + __slots__ = ( + "script", + "method", + "outputs", + "inputs", + "flags", + "required_before_export", + "required_before_compile", + "required_during_compile", + "localized", + "force", + "py2", + ) + + def __init__( + self, + context, + script, + method, + outputs, + inputs, + flags=(), + localized=False, + force=False, + py2=False, + required_during_compile=None, + ): + ContextDerived.__init__(self, context) + self.script = script + self.method = method + self.outputs = outputs if isinstance(outputs, tuple) else (outputs,) + self.inputs = inputs + self.flags = flags + self.localized = localized + self.force = force + self.py2 = py2 + + if self.config.substs.get("MOZ_WIDGET_TOOLKIT") == "android": + # In GeckoView builds we process Jinja files during pre-export + self.required_before_export = [ + f for f in self.inputs if f.endswith(".jinja") + ] + else: + self.required_before_export = False + + suffixes = [ + ".h", + ".py", + ".rs", + # We need to compile Java to generate JNI wrappers for native code + # compilation to consume. + "android_apks", + ".profdata", + ".webidl", + ] + + try: + lib_suffix = context.config.substs["LIB_SUFFIX"] + suffixes.append("." + lib_suffix) + except KeyError: + # Tests may not define LIB_SUFFIX + pass + + suffixes = tuple(suffixes) + + self.required_before_compile = [ + f + for f in self.outputs + if f.endswith(suffixes) or "stl_wrappers/" in f or "xpidl.stub" in f + ] + + if required_during_compile is None: + self.required_during_compile = [ + f + for f in self.outputs + if f.endswith( + (".asm", ".c", ".cpp", ".inc", ".m", ".mm", ".def", "symverscript") + ) + ] + else: + self.required_during_compile = required_during_compile + + +class ChromeManifestEntry(ContextDerived): + """Represents a chrome.manifest entry.""" + + __slots__ = ("path", "entry") + + def __init__(self, context, manifest_path, entry): + ContextDerived.__init__(self, context) + assert isinstance(entry, ManifestEntry) + self.path = mozpath.join(self.install_target, manifest_path) + # Ensure the entry is relative to the directory containing the + # manifest path. + entry = entry.rebase(mozpath.dirname(manifest_path)) + # Then add the install_target to the entry base directory. + self.entry = entry.move(mozpath.dirname(self.path)) diff --git a/python/mozbuild/mozbuild/frontend/emitter.py b/python/mozbuild/mozbuild/frontend/emitter.py new file mode 100644 index 0000000000..ac8a360bb1 --- /dev/null +++ b/python/mozbuild/mozbuild/frontend/emitter.py @@ -0,0 +1,1928 @@ +# 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/. + +import logging +import os +import sys +import time +import traceback +from collections import OrderedDict, defaultdict + +import mozinfo +import mozpack.path as mozpath +import six +import toml +from mach.mixin.logging import LoggingMixin +from mozpack.chrome.manifest import Manifest + +from mozbuild.base import ExecutionSummary +from mozbuild.util import OrderedDefaultDict, memoize + +from ..testing import REFTEST_FLAVORS, TEST_MANIFESTS, SupportFilesConverter +from .context import Context, ObjDirPath, Path, SourcePath, SubContext +from .data import ( + BaseRustProgram, + ChromeManifestEntry, + ComputedFlags, + ConfigFileSubstitution, + Defines, + DirectoryTraversal, + Exports, + ExternalSharedLibrary, + ExternalStaticLibrary, + FinalTargetFiles, + FinalTargetPreprocessedFiles, + GeneratedFile, + HostDefines, + HostLibrary, + HostProgram, + HostRustLibrary, + HostRustProgram, + HostSharedLibrary, + HostSimpleProgram, + HostSources, + InstallationTarget, + IPDLCollection, + JARManifest, + Library, + Linkable, + LocalInclude, + LocalizedFiles, + LocalizedPreprocessedFiles, + ObjdirFiles, + ObjdirPreprocessedFiles, + PerSourceFlag, + Program, + RustLibrary, + RustProgram, + RustTests, + SandboxedWasmLibrary, + SharedLibrary, + SimpleProgram, + Sources, + StaticLibrary, + TestHarnessFiles, + TestManifest, + UnifiedSources, + VariablePassthru, + WasmDefines, + WasmSources, + WebIDLCollection, + XPCOMComponentManifests, + XPIDLModule, +) +from .reader import SandboxValidationError + + +class TreeMetadataEmitter(LoggingMixin): + """Converts the executed mozbuild files into data structures. + + This is a bridge between reader.py and data.py. It takes what was read by + reader.BuildReader and converts it into the classes defined in the data + module. + """ + + def __init__(self, config): + self.populate_logger() + + self.config = config + + mozinfo.find_and_update_from_json(config.topobjdir) + + self.info = dict(mozinfo.info) + + self._libs = OrderedDefaultDict(list) + self._binaries = OrderedDict() + self._compile_dirs = set() + self._host_compile_dirs = set() + self._wasm_compile_dirs = set() + self._asm_compile_dirs = set() + self._compile_flags = dict() + self._compile_as_flags = dict() + self._linkage = [] + self._static_linking_shared = set() + self._crate_verified_local = set() + self._crate_directories = dict() + self._idls = defaultdict(set) + + # Keep track of external paths (third party build systems), starting + # from what we run a subconfigure in. We'll eliminate some directories + # as we traverse them with moz.build (e.g. js/src). + subconfigures = os.path.join(self.config.topobjdir, "subconfigures") + paths = [] + if os.path.exists(subconfigures): + paths = open(subconfigures).read().splitlines() + self._external_paths = set(mozpath.normsep(d) for d in paths) + + self._emitter_time = 0.0 + self._object_count = 0 + self._test_files_converter = SupportFilesConverter() + + def summary(self): + return ExecutionSummary( + "Processed into {object_count:d} build config descriptors in " + "{execution_time:.2f}s", + execution_time=self._emitter_time, + object_count=self._object_count, + ) + + def emit(self, output, emitfn=None): + """Convert the BuildReader output into data structures. + + The return value from BuildReader.read_topsrcdir() (a generator) is + typically fed into this function. + """ + contexts = {} + emitfn = emitfn or self.emit_from_context + + def emit_objs(objs): + for o in objs: + self._object_count += 1 + yield o + + for out in output: + # Nothing in sub-contexts is currently of interest to us. Filter + # them all out. + if isinstance(out, SubContext): + continue + + if isinstance(out, Context): + # Keep all contexts around, we will need them later. + contexts[os.path.normcase(out.objdir)] = out + + start = time.monotonic() + # We need to expand the generator for the timings to work. + objs = list(emitfn(out)) + self._emitter_time += time.monotonic() - start + + for o in emit_objs(objs): + yield o + + else: + raise Exception("Unhandled output type: %s" % type(out)) + + # Don't emit Linkable objects when COMPILE_ENVIRONMENT is not set + if self.config.substs.get("COMPILE_ENVIRONMENT"): + start = time.monotonic() + objs = list(self._emit_libs_derived(contexts)) + self._emitter_time += time.monotonic() - start + + for o in emit_objs(objs): + yield o + + def _emit_libs_derived(self, contexts): + # First aggregate idl sources. + webidl_attrs = [ + ("GENERATED_EVENTS_WEBIDL_FILES", lambda c: c.generated_events_sources), + ("GENERATED_WEBIDL_FILES", lambda c: c.generated_sources), + ("PREPROCESSED_TEST_WEBIDL_FILES", lambda c: c.preprocessed_test_sources), + ("PREPROCESSED_WEBIDL_FILES", lambda c: c.preprocessed_sources), + ("TEST_WEBIDL_FILES", lambda c: c.test_sources), + ("WEBIDL_FILES", lambda c: c.sources), + ("WEBIDL_EXAMPLE_INTERFACES", lambda c: c.example_interfaces), + ] + ipdl_attrs = [ + ("IPDL_SOURCES", lambda c: c.sources), + ("PREPROCESSED_IPDL_SOURCES", lambda c: c.preprocessed_sources), + ] + xpcom_attrs = [("XPCOM_MANIFESTS", lambda c: c.manifests)] + + idl_sources = {} + for root, cls, attrs in ( + (self.config.substs.get("WEBIDL_ROOT"), WebIDLCollection, webidl_attrs), + (self.config.substs.get("IPDL_ROOT"), IPDLCollection, ipdl_attrs), + ( + self.config.substs.get("XPCOM_ROOT"), + XPCOMComponentManifests, + xpcom_attrs, + ), + ): + if root: + collection = cls(contexts[os.path.normcase(root)]) + for var, src_getter in attrs: + src_getter(collection).update(self._idls[var]) + + idl_sources[root] = collection.all_source_files() + if isinstance(collection, WebIDLCollection): + # Test webidl sources are added here as a somewhat special + # case. + idl_sources[mozpath.join(root, "test")] = [ + s for s in collection.all_test_cpp_basenames() + ] + + yield collection + + # Next do FINAL_LIBRARY linkage. + for lib in (l for libs in self._libs.values() for l in libs): + if not isinstance(lib, (StaticLibrary, RustLibrary)) or not lib.link_into: + continue + if lib.link_into not in self._libs: + raise SandboxValidationError( + 'FINAL_LIBRARY ("%s") does not match any LIBRARY_NAME' + % lib.link_into, + contexts[os.path.normcase(lib.objdir)], + ) + candidates = self._libs[lib.link_into] + + # When there are multiple candidates, but all are in the same + # directory and have a different type, we want all of them to + # have the library linked. The typical usecase is when building + # both a static and a shared library in a directory, and having + # that as a FINAL_LIBRARY. + if ( + len(set(type(l) for l in candidates)) == len(candidates) + and len(set(l.objdir for l in candidates)) == 1 + ): + for c in candidates: + c.link_library(lib) + else: + raise SandboxValidationError( + 'FINAL_LIBRARY ("%s") matches a LIBRARY_NAME defined in ' + "multiple places:\n %s" + % (lib.link_into, "\n ".join(l.objdir for l in candidates)), + contexts[os.path.normcase(lib.objdir)], + ) + + # ...and USE_LIBS linkage. + for context, obj, variable in self._linkage: + self._link_libraries(context, obj, variable, idl_sources) + + def recurse_refs(lib): + for o in lib.refs: + yield o + if isinstance(o, StaticLibrary): + for q in recurse_refs(o): + yield q + + # Check that all static libraries refering shared libraries in + # USE_LIBS are linked into a shared library or program. + for lib in self._static_linking_shared: + if all(isinstance(o, StaticLibrary) for o in recurse_refs(lib)): + shared_libs = sorted( + l.basename + for l in lib.linked_libraries + if isinstance(l, SharedLibrary) + ) + raise SandboxValidationError( + 'The static "%s" library is not used in a shared library ' + "or a program, but USE_LIBS contains the following shared " + "library names:\n %s\n\nMaybe you can remove the " + 'static "%s" library?' + % (lib.basename, "\n ".join(shared_libs), lib.basename), + contexts[os.path.normcase(lib.objdir)], + ) + + @memoize + def rust_libraries(obj): + libs = [] + for o in obj.linked_libraries: + if isinstance(o, (HostRustLibrary, RustLibrary)): + libs.append(o) + elif isinstance(o, (HostLibrary, StaticLibrary, SandboxedWasmLibrary)): + libs.extend(rust_libraries(o)) + return libs + + def check_rust_libraries(obj): + rust_libs = set(rust_libraries(obj)) + if len(rust_libs) <= 1: + return + if isinstance(obj, (Library, HostLibrary)): + what = '"%s" library' % obj.basename + else: + what = '"%s" program' % obj.name + raise SandboxValidationError( + "Cannot link the following Rust libraries into the %s:\n" + "%s\nOnly one is allowed." + % ( + what, + "\n".join( + " - %s" % r.basename + for r in sorted(rust_libs, key=lambda r: r.basename) + ), + ), + contexts[os.path.normcase(obj.objdir)], + ) + + # Propagate LIBRARY_DEFINES to all child libraries recursively. + def propagate_defines(outerlib, defines): + outerlib.lib_defines.update(defines) + for lib in outerlib.linked_libraries: + # Propagate defines only along FINAL_LIBRARY paths, not USE_LIBS + # paths. + if ( + isinstance(lib, StaticLibrary) + and lib.link_into == outerlib.basename + ): + propagate_defines(lib, defines) + + for lib in (l for libs in self._libs.values() for l in libs): + if isinstance(lib, Library): + propagate_defines(lib, lib.lib_defines) + check_rust_libraries(lib) + yield lib + + for lib in (l for libs in self._libs.values() for l in libs): + lib_defines = list(lib.lib_defines.get_defines()) + if lib_defines: + objdir_flags = self._compile_flags[lib.objdir] + objdir_flags.resolve_flags("LIBRARY_DEFINES", lib_defines) + + objdir_flags = self._compile_as_flags.get(lib.objdir) + if objdir_flags: + objdir_flags.resolve_flags("LIBRARY_DEFINES", lib_defines) + + for flags_obj in self._compile_flags.values(): + yield flags_obj + + for flags_obj in self._compile_as_flags.values(): + yield flags_obj + + for obj in self._binaries.values(): + if isinstance(obj, Linkable): + check_rust_libraries(obj) + yield obj + + LIBRARY_NAME_VAR = { + "host": "HOST_LIBRARY_NAME", + "target": "LIBRARY_NAME", + "wasm": "SANDBOXED_WASM_LIBRARY_NAME", + } + + ARCH_VAR = {"host": "HOST_OS_ARCH", "target": "OS_TARGET"} + + STDCXXCOMPAT_NAME = {"host": "host_stdc++compat", "target": "stdc++compat"} + + def _link_libraries(self, context, obj, variable, extra_sources): + """Add linkage declarations to a given object.""" + assert isinstance(obj, Linkable) + + if context.objdir in extra_sources: + # All "extra sources" are .cpp for the moment, and happen to come + # first in order. + obj.sources[".cpp"] = extra_sources[context.objdir] + obj.sources[".cpp"] + + for path in context.get(variable, []): + self._link_library(context, obj, variable, path) + + # Link system libraries from OS_LIBS/HOST_OS_LIBS. + for lib in context.get(variable.replace("USE", "OS"), []): + obj.link_system_library(lib) + + # We have to wait for all the self._link_library calls above to have + # happened for obj.cxx_link to be final. + # FIXME: Theoretically, HostSharedLibrary shouldn't be here (bug + # 1474022). + if ( + not isinstance( + obj, (StaticLibrary, HostLibrary, HostSharedLibrary, BaseRustProgram) + ) + and obj.cxx_link + ): + if ( + context.config.substs.get("MOZ_STDCXX_COMPAT") + and context.config.substs.get(self.ARCH_VAR.get(obj.KIND)) == "Linux" + ): + self._link_library( + context, obj, variable, self.STDCXXCOMPAT_NAME[obj.KIND] + ) + if obj.KIND == "target": + if "pure_virtual" in self._libs: + self._link_library(context, obj, variable, "pure_virtual") + for lib in context.config.substs.get("STLPORT_LIBS", []): + obj.link_system_library(lib) + + def _link_library(self, context, obj, variable, path): + force_static = path.startswith("static:") and obj.KIND == "target" + if force_static: + path = path[7:] + name = mozpath.basename(path) + dir = mozpath.dirname(path) + candidates = [l for l in self._libs[name] if l.KIND == obj.KIND] + if dir: + if dir.startswith("/"): + dir = mozpath.normpath(mozpath.join(obj.topobjdir, dir[1:])) + else: + dir = mozpath.normpath(mozpath.join(obj.objdir, dir)) + dir = mozpath.relpath(dir, obj.topobjdir) + candidates = [l for l in candidates if l.relobjdir == dir] + if not candidates: + # If the given directory is under one of the external + # (third party) paths, use a fake library reference to + # there. + for d in self._external_paths: + if dir.startswith("%s/" % d): + candidates = [ + self._get_external_library(dir, name, force_static) + ] + break + + if not candidates: + raise SandboxValidationError( + '%s contains "%s", but there is no "%s" %s in %s.' + % (variable, path, name, self.LIBRARY_NAME_VAR[obj.KIND], dir), + context, + ) + + if len(candidates) > 1: + # If there's more than one remaining candidate, it could be + # that there are instances for the same library, in static and + # shared form. + libs = {} + for l in candidates: + key = mozpath.join(l.relobjdir, l.basename) + if force_static: + if isinstance(l, StaticLibrary): + libs[key] = l + else: + if key in libs and isinstance(l, SharedLibrary): + libs[key] = l + if key not in libs: + libs[key] = l + candidates = list(libs.values()) + if force_static and not candidates: + if dir: + raise SandboxValidationError( + '%s contains "static:%s", but there is no static ' + '"%s" %s in %s.' + % (variable, path, name, self.LIBRARY_NAME_VAR[obj.KIND], dir), + context, + ) + raise SandboxValidationError( + '%s contains "static:%s", but there is no static "%s" ' + "%s in the tree" + % (variable, name, name, self.LIBRARY_NAME_VAR[obj.KIND]), + context, + ) + + if not candidates: + raise SandboxValidationError( + '%s contains "%s", which does not match any %s in the tree.' + % (variable, path, self.LIBRARY_NAME_VAR[obj.KIND]), + context, + ) + + elif len(candidates) > 1: + paths = (mozpath.join(l.relsrcdir, "moz.build") for l in candidates) + raise SandboxValidationError( + '%s contains "%s", which matches a %s defined in multiple ' + "places:\n %s" + % ( + variable, + path, + self.LIBRARY_NAME_VAR[obj.KIND], + "\n ".join(paths), + ), + context, + ) + + elif force_static and not isinstance(candidates[0], StaticLibrary): + raise SandboxValidationError( + '%s contains "static:%s", but there is only a shared "%s" ' + "in %s. You may want to add FORCE_STATIC_LIB=True in " + '%s/moz.build, or remove "static:".' + % ( + variable, + path, + name, + candidates[0].relobjdir, + candidates[0].relobjdir, + ), + context, + ) + + elif isinstance(obj, StaticLibrary) and isinstance( + candidates[0], SharedLibrary + ): + self._static_linking_shared.add(obj) + obj.link_library(candidates[0]) + + @memoize + def _get_external_library(self, dir, name, force_static): + # Create ExternalStaticLibrary or ExternalSharedLibrary object with a + # context more or less truthful about where the external library is. + context = Context(config=self.config) + context.add_source(mozpath.join(self.config.topsrcdir, dir, "dummy")) + if force_static: + return ExternalStaticLibrary(context, name) + else: + return ExternalSharedLibrary(context, name) + + def _parse_and_check_cargo_file(self, context): + """Parse the Cargo.toml file in context and return a Python object + representation of it. Raise a SandboxValidationError if the Cargo.toml + file does not exist. Return a tuple of (config, cargo_file).""" + cargo_file = mozpath.join(context.srcdir, "Cargo.toml") + if not os.path.exists(cargo_file): + raise SandboxValidationError( + "No Cargo.toml file found in %s" % cargo_file, context + ) + with open(cargo_file, "r") as f: + content = toml.load(f) + + crate_name = content.get("package", {}).get("name") + if not crate_name: + raise SandboxValidationError( + f"{cargo_file} doesn't contain a crate name?!?", context + ) + + hack_name = "mozilla-central-workspace-hack" + dep = f'{hack_name} = {{ version = "0.1", features = ["{crate_name}"], optional = true }}' + dep_dict = toml.loads(dep)[hack_name] + hint = ( + "\n\nYou may also need to adjust the build/workspace-hack/Cargo.toml" + f" file to add the {crate_name} feature." + ) + + workspace_hack = content.get("dependencies", {}).get(hack_name) + if not workspace_hack: + raise SandboxValidationError( + f"{cargo_file} doesn't contain the workspace hack.\n\n" + f"Add the following to dependencies:\n{dep}{hint}", + context, + ) + + if workspace_hack != dep_dict: + raise SandboxValidationError( + f"{cargo_file} needs an update to its {hack_name} dependency.\n\n" + f"Adjust the dependency to:\n{dep}{hint}", + context, + ) + + return content, cargo_file + + def _verify_deps( + self, context, crate_dir, crate_name, dependencies, description="Dependency" + ): + """Verify that a crate's dependencies all specify local paths.""" + for dep_crate_name, values in six.iteritems(dependencies): + # A simple version number. + if isinstance(values, (six.binary_type, six.text_type)): + raise SandboxValidationError( + "%s %s of crate %s does not list a path" + % (description, dep_crate_name, crate_name), + context, + ) + + dep_path = values.get("path", None) + if not dep_path: + raise SandboxValidationError( + "%s %s of crate %s does not list a path" + % (description, dep_crate_name, crate_name), + context, + ) + + # Try to catch the case where somebody listed a + # local path for development. + if os.path.isabs(dep_path): + raise SandboxValidationError( + "%s %s of crate %s has a non-relative path" + % (description, dep_crate_name, crate_name), + context, + ) + + if not os.path.exists( + mozpath.join(context.config.topsrcdir, crate_dir, dep_path) + ): + raise SandboxValidationError( + "%s %s of crate %s refers to a non-existent path" + % (description, dep_crate_name, crate_name), + context, + ) + + def _rust_library( + self, context, libname, static_args, is_gkrust=False, cls=RustLibrary + ): + # We need to note any Rust library for linking purposes. + config, cargo_file = self._parse_and_check_cargo_file(context) + crate_name = config["package"]["name"] + + if crate_name != libname: + raise SandboxValidationError( + "library %s does not match Cargo.toml-defined package %s" + % (libname, crate_name), + context, + ) + + # Check that the [lib.crate-type] field is correct + lib_section = config.get("lib", None) + if not lib_section: + raise SandboxValidationError( + "Cargo.toml for %s has no [lib] section" % libname, context + ) + + crate_type = lib_section.get("crate-type", None) + if not crate_type: + raise SandboxValidationError( + "Can't determine a crate-type for %s from Cargo.toml" % libname, context + ) + + crate_type = crate_type[0] + if crate_type != "staticlib": + raise SandboxValidationError( + "crate-type %s is not permitted for %s" % (crate_type, libname), context + ) + + dependencies = set(six.iterkeys(config.get("dependencies", {}))) + + features = context.get(cls.FEATURES_VAR, []) + unique_features = set(features) + if len(features) != len(unique_features): + raise SandboxValidationError( + "features for %s should not contain duplicates: %s" + % (libname, features), + context, + ) + + return cls( + context, + libname, + cargo_file, + crate_type, + dependencies, + features, + is_gkrust, + **static_args, + ) + + def _handle_linkables(self, context, passthru, generated_files): + linkables = [] + host_linkables = [] + wasm_linkables = [] + + def add_program(prog, var): + if var.startswith("HOST_"): + host_linkables.append(prog) + else: + linkables.append(prog) + + def check_unique_binary(program, kind): + if program in self._binaries: + raise SandboxValidationError( + 'Cannot use "%s" as %s name, ' + "because it is already used in %s" + % (program, kind, self._binaries[program].relsrcdir), + context, + ) + + for kind, cls in [("PROGRAM", Program), ("HOST_PROGRAM", HostProgram)]: + program = context.get(kind) + if program: + check_unique_binary(program, kind) + self._binaries[program] = cls(context, program) + self._linkage.append( + ( + context, + self._binaries[program], + kind.replace("PROGRAM", "USE_LIBS"), + ) + ) + add_program(self._binaries[program], kind) + + all_rust_programs = [] + for kind, cls in [ + ("RUST_PROGRAMS", RustProgram), + ("HOST_RUST_PROGRAMS", HostRustProgram), + ]: + programs = context[kind] + if not programs: + continue + + all_rust_programs.append((programs, kind, cls)) + + # Verify Rust program definitions. + if all_rust_programs: + config, cargo_file = self._parse_and_check_cargo_file(context) + bin_section = config.get("bin", None) + if not bin_section: + raise SandboxValidationError( + "Cargo.toml in %s has no [bin] section" % context.srcdir, context + ) + + defined_binaries = {b["name"] for b in bin_section} + + for programs, kind, cls in all_rust_programs: + for program in programs: + if program not in defined_binaries: + raise SandboxValidationError( + "Cannot find Cargo.toml definition for %s" % program, + context, + ) + + check_unique_binary(program, kind) + self._binaries[program] = cls(context, program, cargo_file) + self._linkage.append( + ( + context, + self._binaries[program], + kind.replace("RUST_PROGRAMS", "USE_LIBS"), + ) + ) + add_program(self._binaries[program], kind) + + for kind, cls in [ + ("SIMPLE_PROGRAMS", SimpleProgram), + ("CPP_UNIT_TESTS", SimpleProgram), + ("HOST_SIMPLE_PROGRAMS", HostSimpleProgram), + ]: + for program in context[kind]: + if program in self._binaries: + raise SandboxValidationError( + 'Cannot use "%s" in %s, ' + "because it is already used in %s" + % (program, kind, self._binaries[program].relsrcdir), + context, + ) + self._binaries[program] = cls( + context, program, is_unit_test=kind == "CPP_UNIT_TESTS" + ) + self._linkage.append( + ( + context, + self._binaries[program], + "HOST_USE_LIBS" + if kind == "HOST_SIMPLE_PROGRAMS" + else "USE_LIBS", + ) + ) + add_program(self._binaries[program], kind) + + host_libname = context.get("HOST_LIBRARY_NAME") + libname = context.get("LIBRARY_NAME") + + if host_libname: + if host_libname == libname: + raise SandboxValidationError( + "LIBRARY_NAME and HOST_LIBRARY_NAME must have a different value", + context, + ) + + is_rust_library = context.get("IS_RUST_LIBRARY") + if is_rust_library: + lib = self._rust_library(context, host_libname, {}, cls=HostRustLibrary) + elif context.get("FORCE_SHARED_LIB"): + lib = HostSharedLibrary(context, host_libname) + else: + lib = HostLibrary(context, host_libname) + self._libs[host_libname].append(lib) + self._linkage.append((context, lib, "HOST_USE_LIBS")) + host_linkables.append(lib) + + final_lib = context.get("FINAL_LIBRARY") + if not libname and final_lib: + # If no LIBRARY_NAME is given, create one. + libname = context.relsrcdir.replace("/", "_") + + static_lib = context.get("FORCE_STATIC_LIB") + shared_lib = context.get("FORCE_SHARED_LIB") + + static_name = context.get("STATIC_LIBRARY_NAME") + shared_name = context.get("SHARED_LIBRARY_NAME") + + is_framework = context.get("IS_FRAMEWORK") + + soname = context.get("SONAME") + + lib_defines = context.get("LIBRARY_DEFINES") + + wasm_lib = context.get("SANDBOXED_WASM_LIBRARY_NAME") + + shared_args = {} + static_args = {} + + if final_lib: + if static_lib: + raise SandboxValidationError( + "FINAL_LIBRARY implies FORCE_STATIC_LIB. " + "Please remove the latter.", + context, + ) + if shared_lib: + raise SandboxValidationError( + "FINAL_LIBRARY conflicts with FORCE_SHARED_LIB. " + "Please remove one.", + context, + ) + if is_framework: + raise SandboxValidationError( + "FINAL_LIBRARY conflicts with IS_FRAMEWORK. " "Please remove one.", + context, + ) + static_args["link_into"] = final_lib + static_lib = True + + if libname: + if is_framework: + if soname: + raise SandboxValidationError( + "IS_FRAMEWORK conflicts with SONAME. " "Please remove one.", + context, + ) + shared_lib = True + shared_args["variant"] = SharedLibrary.FRAMEWORK + + if not static_lib and not shared_lib: + static_lib = True + + if static_name: + if not static_lib: + raise SandboxValidationError( + "STATIC_LIBRARY_NAME requires FORCE_STATIC_LIB", context + ) + static_args["real_name"] = static_name + + if shared_name: + if not shared_lib: + raise SandboxValidationError( + "SHARED_LIBRARY_NAME requires FORCE_SHARED_LIB", context + ) + shared_args["real_name"] = shared_name + + if soname: + if not shared_lib: + raise SandboxValidationError( + "SONAME requires FORCE_SHARED_LIB", context + ) + shared_args["soname"] = soname + + if context.get("NO_EXPAND_LIBS"): + if not static_lib: + raise SandboxValidationError( + "NO_EXPAND_LIBS can only be set for static libraries.", context + ) + static_args["no_expand_lib"] = True + + if shared_lib and static_lib: + if not static_name and not shared_name: + raise SandboxValidationError( + "Both FORCE_STATIC_LIB and FORCE_SHARED_LIB are True, " + "but neither STATIC_LIBRARY_NAME or " + "SHARED_LIBRARY_NAME is set. At least one is required.", + context, + ) + if static_name and not shared_name and static_name == libname: + raise SandboxValidationError( + "Both FORCE_STATIC_LIB and FORCE_SHARED_LIB are True, " + "but STATIC_LIBRARY_NAME is the same as LIBRARY_NAME, " + "and SHARED_LIBRARY_NAME is unset. Please either " + "change STATIC_LIBRARY_NAME or LIBRARY_NAME, or set " + "SHARED_LIBRARY_NAME.", + context, + ) + if shared_name and not static_name and shared_name == libname: + raise SandboxValidationError( + "Both FORCE_STATIC_LIB and FORCE_SHARED_LIB are True, " + "but SHARED_LIBRARY_NAME is the same as LIBRARY_NAME, " + "and STATIC_LIBRARY_NAME is unset. Please either " + "change SHARED_LIBRARY_NAME or LIBRARY_NAME, or set " + "STATIC_LIBRARY_NAME.", + context, + ) + if shared_name and static_name and shared_name == static_name: + raise SandboxValidationError( + "Both FORCE_STATIC_LIB and FORCE_SHARED_LIB are True, " + "but SHARED_LIBRARY_NAME is the same as " + "STATIC_LIBRARY_NAME. Please change one of them.", + context, + ) + + symbols_file = context.get("SYMBOLS_FILE") + if symbols_file: + if not shared_lib: + raise SandboxValidationError( + "SYMBOLS_FILE can only be used with a SHARED_LIBRARY.", context + ) + if context.get("DEFFILE"): + raise SandboxValidationError( + "SYMBOLS_FILE cannot be used along DEFFILE.", context + ) + if isinstance(symbols_file, SourcePath): + if not os.path.exists(symbols_file.full_path): + raise SandboxValidationError( + "Path specified in SYMBOLS_FILE does not exist: %s " + "(resolved to %s)" % (symbols_file, symbols_file.full_path), + context, + ) + shared_args["symbols_file"] = True + else: + if symbols_file.target_basename not in generated_files: + raise SandboxValidationError( + ( + "Objdir file specified in SYMBOLS_FILE not in " + + "GENERATED_FILES: %s" + ) + % (symbols_file,), + context, + ) + shared_args["symbols_file"] = symbols_file.target_basename + + if shared_lib: + lib = SharedLibrary(context, libname, **shared_args) + self._libs[libname].append(lib) + self._linkage.append((context, lib, "USE_LIBS")) + linkables.append(lib) + if not lib.installed: + generated_files.add(lib.lib_name) + if symbols_file and isinstance(symbols_file, SourcePath): + script = mozpath.join( + mozpath.dirname(mozpath.dirname(__file__)), + "action", + "generate_symbols_file.py", + ) + defines = () + if lib.defines: + defines = lib.defines.get_defines() + yield GeneratedFile( + context, + script, + "generate_symbols_file", + lib.symbols_file, + [symbols_file], + defines, + required_during_compile=[lib.symbols_file], + ) + if static_lib: + is_rust_library = context.get("IS_RUST_LIBRARY") + if is_rust_library: + lib = self._rust_library( + context, + libname, + static_args, + is_gkrust=bool(context.get("IS_GKRUST")), + ) + else: + lib = StaticLibrary(context, libname, **static_args) + self._libs[libname].append(lib) + self._linkage.append((context, lib, "USE_LIBS")) + linkables.append(lib) + + if lib_defines: + if not libname: + raise SandboxValidationError( + "LIBRARY_DEFINES needs a " "LIBRARY_NAME to take effect", + context, + ) + lib.lib_defines.update(lib_defines) + + if wasm_lib: + if wasm_lib == libname: + raise SandboxValidationError( + "SANDBOXED_WASM_LIBRARY_NAME and LIBRARY_NAME must have a " + "different value.", + context, + ) + if wasm_lib == host_libname: + raise SandboxValidationError( + "SANDBOXED_WASM_LIBRARY_NAME and HOST_LIBRARY_NAME must " + "have a different value.", + context, + ) + if wasm_lib == shared_name: + raise SandboxValidationError( + "SANDBOXED_WASM_LIBRARY_NAME and SHARED_NAME must have a " + "different value.", + context, + ) + if wasm_lib == static_name: + raise SandboxValidationError( + "SANDBOXED_WASM_LIBRARY_NAME and STATIC_NAME must have a " + "different value.", + context, + ) + lib = SandboxedWasmLibrary(context, wasm_lib) + self._libs[libname].append(lib) + wasm_linkables.append(lib) + self._wasm_compile_dirs.add(context.objdir) + + seen = {} + for symbol in ("SOURCES", "UNIFIED_SOURCES"): + for src in context.get(symbol, []): + basename = os.path.splitext(os.path.basename(src))[0] + if basename in seen: + other_src, where = seen[basename] + extra = "" + if "UNIFIED_SOURCES" in (symbol, where): + extra = " in non-unified builds" + raise SandboxValidationError( + f"{src} from {symbol} would have the same object name " + f"as {other_src} from {where}{extra}.", + context, + ) + seen[basename] = (src, symbol) + + # Only emit sources if we have linkables defined in the same context. + # Note the linkables are not emitted in this function, but much later, + # after aggregation (because of e.g. USE_LIBS processing). + if not (linkables or host_linkables or wasm_linkables): + return + + # TODO: objdirs with only host things in them shouldn't need target + # flags, but there's at least one Makefile.in (in + # build/unix/elfhack) that relies on the value of LDFLAGS being + # passed to one-off rules. + self._compile_dirs.add(context.objdir) + + if host_linkables or any( + isinstance(l, (RustLibrary, RustProgram)) for l in linkables + ): + self._host_compile_dirs.add(context.objdir) + + sources = defaultdict(list) + gen_sources = defaultdict(list) + all_flags = {} + for symbol in ("SOURCES", "HOST_SOURCES", "UNIFIED_SOURCES", "WASM_SOURCES"): + srcs = sources[symbol] + gen_srcs = gen_sources[symbol] + context_srcs = context.get(symbol, []) + seen_sources = set() + for f in context_srcs: + if f in seen_sources: + raise SandboxValidationError( + "Source file should only " + "be added to %s once: %s" % (symbol, f), + context, + ) + seen_sources.add(f) + full_path = f.full_path + if isinstance(f, SourcePath): + srcs.append(full_path) + else: + assert isinstance(f, Path) + gen_srcs.append(full_path) + if symbol == "SOURCES": + context_flags = context_srcs[f] + if context_flags: + all_flags[full_path] = context_flags + + if isinstance(f, SourcePath) and not os.path.exists(full_path): + raise SandboxValidationError( + "File listed in %s does not " + "exist: '%s'" % (symbol, full_path), + context, + ) + + # Process the .cpp files generated by IPDL as generated sources within + # the context which declared the IPDL_SOURCES attribute. + ipdl_root = self.config.substs.get("IPDL_ROOT") + for symbol in ("IPDL_SOURCES", "PREPROCESSED_IPDL_SOURCES"): + context_srcs = context.get(symbol, []) + for f in context_srcs: + root, ext = mozpath.splitext(mozpath.basename(f)) + + suffix_map = { + ".ipdlh": [".cpp"], + ".ipdl": [".cpp", "Child.cpp", "Parent.cpp"], + } + if ext not in suffix_map: + raise SandboxValidationError( + "Unexpected extension for IPDL source %s" % ext + ) + + gen_sources["UNIFIED_SOURCES"].extend( + mozpath.join(ipdl_root, root + suffix) for suffix in suffix_map[ext] + ) + + no_pgo = context.get("NO_PGO") + no_pgo_sources = [f for f, flags in six.iteritems(all_flags) if flags.no_pgo] + if no_pgo: + if no_pgo_sources: + raise SandboxValidationError( + "NO_PGO and SOURCES[...].no_pgo " "cannot be set at the same time", + context, + ) + passthru.variables["NO_PROFILE_GUIDED_OPTIMIZE"] = no_pgo + if no_pgo_sources: + passthru.variables["NO_PROFILE_GUIDED_OPTIMIZE"] = no_pgo_sources + + # A map from "canonical suffixes" for a particular source file + # language to the range of suffixes associated with that language. + # + # We deliberately don't list the canonical suffix in the suffix list + # in the definition; we'll add it in programmatically after defining + # things. + suffix_map = { + ".s": set([".asm"]), + ".c": set(), + ".m": set(), + ".mm": set(), + ".cpp": set([".cc", ".cxx"]), + ".S": set(), + } + + # The inverse of the above, mapping suffixes to their canonical suffix. + canonicalized_suffix_map = {} + for suffix, alternatives in six.iteritems(suffix_map): + alternatives.add(suffix) + for a in alternatives: + canonicalized_suffix_map[a] = suffix + + # A map from moz.build variables to the canonical suffixes of file + # kinds that can be listed therein. + all_suffixes = list(suffix_map.keys()) + varmap = dict( + SOURCES=(Sources, all_suffixes), + HOST_SOURCES=(HostSources, [".c", ".mm", ".cpp"]), + UNIFIED_SOURCES=(UnifiedSources, [".c", ".mm", ".m", ".cpp"]), + ) + # Only include a WasmSources context if there are any WASM_SOURCES. + # (This is going to matter later because we inject an extra .c file to + # compile with the wasm compiler if, and only if, there are any WASM + # sources.) + if sources["WASM_SOURCES"] or gen_sources["WASM_SOURCES"]: + varmap["WASM_SOURCES"] = (WasmSources, [".c", ".cpp"]) + # Track whether there are any C++ source files. + # Technically this won't do the right thing for SIMPLE_PROGRAMS in + # a directory with mixed C and C++ source, but it's not that important. + cxx_sources = defaultdict(bool) + + # Source files to track for linkables associated with this context. + ctxt_sources = defaultdict(lambda: defaultdict(list)) + + for variable, (klass, suffixes) in varmap.items(): + # Group static and generated files by their canonical suffixes, and + # ensure we haven't been given filetypes that we don't recognize. + by_canonical_suffix = defaultdict(lambda: {"static": [], "generated": []}) + for srcs, key in ( + (sources[variable], "static"), + (gen_sources[variable], "generated"), + ): + for f in srcs: + canonical_suffix = canonicalized_suffix_map.get( + mozpath.splitext(f)[1] + ) + if canonical_suffix not in suffixes: + raise SandboxValidationError( + "%s has an unknown file type." % f, context + ) + by_canonical_suffix[canonical_suffix][key].append(f) + + # Yield an object for each canonical suffix, grouping generated and + # static sources together to allow them to be unified together. + for canonical_suffix in sorted(by_canonical_suffix.keys()): + if canonical_suffix in (".cpp", ".mm"): + cxx_sources[variable] = True + elif canonical_suffix in (".s", ".S"): + self._asm_compile_dirs.add(context.objdir) + src_group = by_canonical_suffix[canonical_suffix] + obj = klass( + context, + src_group["static"], + src_group["generated"], + canonical_suffix, + ) + srcs = list(obj.files) + if isinstance(obj, UnifiedSources) and obj.have_unified_mapping: + srcs = sorted(dict(obj.unified_source_mapping).keys()) + ctxt_sources[variable][canonical_suffix] += srcs + yield obj + + if ctxt_sources: + for linkable in linkables: + for target_var in ("SOURCES", "UNIFIED_SOURCES"): + for suffix, srcs in ctxt_sources[target_var].items(): + linkable.sources[suffix] += srcs + for host_linkable in host_linkables: + for suffix, srcs in ctxt_sources["HOST_SOURCES"].items(): + host_linkable.sources[suffix] += srcs + for wasm_linkable in wasm_linkables: + for suffix, srcs in ctxt_sources["WASM_SOURCES"].items(): + wasm_linkable.sources[suffix] += srcs + + for f, flags in sorted(six.iteritems(all_flags)): + if flags.flags: + ext = mozpath.splitext(f)[1] + yield PerSourceFlag(context, f, flags.flags) + + # If there are any C++ sources, set all the linkables defined here + # to require the C++ linker. + for vars, linkable_items in ( + (("SOURCES", "UNIFIED_SOURCES"), linkables), + (("HOST_SOURCES",), host_linkables), + ): + for var in vars: + if cxx_sources[var]: + for l in linkable_items: + l.cxx_link = True + break + + def emit_from_context(self, context): + """Convert a Context to tree metadata objects. + + This is a generator of mozbuild.frontend.data.ContextDerived instances. + """ + + # We only want to emit an InstallationTarget if one of the consulted + # variables is defined. Later on, we look up FINAL_TARGET, which has + # the side-effect of populating it. So, we need to do this lookup + # early. + if any(k in context for k in ("FINAL_TARGET", "XPI_NAME", "DIST_SUBDIR")): + yield InstallationTarget(context) + + # We always emit a directory traversal descriptor. This is needed by + # the recursive make backend. + for o in self._emit_directory_traversal_from_context(context): + yield o + + for obj in self._process_xpidl(context): + yield obj + + computed_flags = ComputedFlags(context, context["COMPILE_FLAGS"]) + computed_link_flags = ComputedFlags(context, context["LINK_FLAGS"]) + computed_host_flags = ComputedFlags(context, context["HOST_COMPILE_FLAGS"]) + computed_as_flags = ComputedFlags(context, context["ASM_FLAGS"]) + computed_wasm_flags = ComputedFlags(context, context["WASM_FLAGS"]) + + # Proxy some variables as-is until we have richer classes to represent + # them. We should aim to keep this set small because it violates the + # desired abstraction of the build definition away from makefiles. + passthru = VariablePassthru(context) + varlist = [ + "EXTRA_DSO_LDOPTS", + "RCFILE", + "RCINCLUDE", + "WIN32_EXE_LDFLAGS", + "USE_EXTENSION_MANIFEST", + "WASM_LIBS", + ] + for v in varlist: + if v in context and context[v]: + passthru.variables[v] = context[v] + + if ( + context.config.substs.get("OS_TARGET") == "WINNT" + and context["DELAYLOAD_DLLS"] + ): + if context.config.substs.get("CC_TYPE") != "clang": + context["LDFLAGS"].extend( + [("-DELAYLOAD:%s" % dll) for dll in context["DELAYLOAD_DLLS"]] + ) + else: + context["LDFLAGS"].extend( + [ + ("-Wl,-Xlink=-DELAYLOAD:%s" % dll) + for dll in context["DELAYLOAD_DLLS"] + ] + ) + context["OS_LIBS"].append("delayimp") + + for v in ["CMFLAGS", "CMMFLAGS"]: + if v in context and context[v]: + passthru.variables["MOZBUILD_" + v] = context[v] + + for v in ["CXXFLAGS", "CFLAGS"]: + if v in context and context[v]: + computed_flags.resolve_flags("MOZBUILD_%s" % v, context[v]) + + for v in ["WASM_CFLAGS", "WASM_CXXFLAGS"]: + if v in context and context[v]: + computed_wasm_flags.resolve_flags("MOZBUILD_%s" % v, context[v]) + + for v in ["HOST_CXXFLAGS", "HOST_CFLAGS"]: + if v in context and context[v]: + computed_host_flags.resolve_flags("MOZBUILD_%s" % v, context[v]) + + if "LDFLAGS" in context and context["LDFLAGS"]: + computed_link_flags.resolve_flags("MOZBUILD", context["LDFLAGS"]) + + deffile = context.get("DEFFILE") + if deffile and context.config.substs.get("OS_TARGET") == "WINNT": + if isinstance(deffile, SourcePath): + if not os.path.exists(deffile.full_path): + raise SandboxValidationError( + "Path specified in DEFFILE does not exist: %s " + "(resolved to %s)" % (deffile, deffile.full_path), + context, + ) + path = mozpath.relpath(deffile.full_path, context.objdir) + else: + path = deffile.target_basename + + if context.config.substs.get("GNU_CC"): + computed_link_flags.resolve_flags("DEFFILE", [path]) + else: + computed_link_flags.resolve_flags("DEFFILE", ["-DEF:" + path]) + + dist_install = context["DIST_INSTALL"] + if dist_install is True: + passthru.variables["DIST_INSTALL"] = True + elif dist_install is False: + passthru.variables["NO_DIST_INSTALL"] = True + + # Ideally, this should be done in templates, but this is difficult at + # the moment because USE_STATIC_LIBS can be set after a template + # returns. Eventually, with context-based templates, it will be + # possible. + if context.config.substs.get( + "OS_ARCH" + ) == "WINNT" and not context.config.substs.get("GNU_CC"): + use_static_lib = context.get( + "USE_STATIC_LIBS" + ) and not context.config.substs.get("MOZ_ASAN") + rtl_flag = "-MT" if use_static_lib else "-MD" + if context.config.substs.get("MOZ_DEBUG") and not context.config.substs.get( + "MOZ_NO_DEBUG_RTL" + ): + rtl_flag += "d" + computed_flags.resolve_flags("RTL", [rtl_flag]) + if not context.config.substs.get("CROSS_COMPILE"): + computed_host_flags.resolve_flags("RTL", [rtl_flag]) + + generated_files = set() + localized_generated_files = set() + for obj in self._process_generated_files(context): + for f in obj.outputs: + generated_files.add(f) + if obj.localized: + localized_generated_files.add(f) + yield obj + + for path in context["CONFIGURE_SUBST_FILES"]: + sub = self._create_substitution(ConfigFileSubstitution, context, path) + generated_files.add(str(sub.relpath)) + yield sub + + for defines_var, cls, backend_flags in ( + ("DEFINES", Defines, (computed_flags, computed_as_flags)), + ("HOST_DEFINES", HostDefines, (computed_host_flags,)), + ("WASM_DEFINES", WasmDefines, (computed_wasm_flags,)), + ): + defines = context.get(defines_var) + if defines: + defines_obj = cls(context, defines) + if isinstance(defines_obj, Defines): + # DEFINES have consumers outside the compile command line, + # HOST_DEFINES do not. + yield defines_obj + else: + # If we don't have explicitly set defines we need to make sure + # initialized values if present end up in computed flags. + defines_obj = cls(context, context[defines_var]) + + defines_from_obj = list(defines_obj.get_defines()) + if defines_from_obj: + for flags in backend_flags: + flags.resolve_flags(defines_var, defines_from_obj) + + idl_vars = ( + "GENERATED_EVENTS_WEBIDL_FILES", + "GENERATED_WEBIDL_FILES", + "PREPROCESSED_TEST_WEBIDL_FILES", + "PREPROCESSED_WEBIDL_FILES", + "TEST_WEBIDL_FILES", + "WEBIDL_FILES", + "IPDL_SOURCES", + "PREPROCESSED_IPDL_SOURCES", + "XPCOM_MANIFESTS", + ) + for context_var in idl_vars: + for name in context.get(context_var, []): + self._idls[context_var].add(mozpath.join(context.srcdir, name)) + # WEBIDL_EXAMPLE_INTERFACES do not correspond to files. + for name in context.get("WEBIDL_EXAMPLE_INTERFACES", []): + self._idls["WEBIDL_EXAMPLE_INTERFACES"].add(name) + + local_includes = [] + for local_include in context.get("LOCAL_INCLUDES", []): + full_path = local_include.full_path + if not isinstance(local_include, ObjDirPath): + if not os.path.exists(full_path): + raise SandboxValidationError( + "Path specified in LOCAL_INCLUDES does not exist: %s (resolved to %s)" + % (local_include, full_path), + context, + ) + if not os.path.isdir(full_path): + raise SandboxValidationError( + "Path specified in LOCAL_INCLUDES " + "is a filename, but a directory is required: %s " + "(resolved to %s)" % (local_include, full_path), + context, + ) + if ( + full_path == context.config.topsrcdir + or full_path == context.config.topobjdir + ): + raise SandboxValidationError( + "Path specified in LOCAL_INCLUDES " + "(%s) resolves to the topsrcdir or topobjdir (%s), which is " + "not allowed" % (local_include, full_path), + context, + ) + include_obj = LocalInclude(context, local_include) + local_includes.append(include_obj.path.full_path) + yield include_obj + + computed_flags.resolve_flags( + "LOCAL_INCLUDES", ["-I%s" % p for p in local_includes] + ) + computed_as_flags.resolve_flags( + "LOCAL_INCLUDES", ["-I%s" % p for p in local_includes] + ) + computed_host_flags.resolve_flags( + "LOCAL_INCLUDES", ["-I%s" % p for p in local_includes] + ) + computed_wasm_flags.resolve_flags( + "LOCAL_INCLUDES", ["-I%s" % p for p in local_includes] + ) + + for obj in self._handle_linkables(context, passthru, generated_files): + yield obj + + generated_files.update( + [ + "%s%s" % (k, self.config.substs.get("BIN_SUFFIX", "")) + for k in self._binaries.keys() + ] + ) + + components = [] + for var, cls in ( + ("EXPORTS", Exports), + ("FINAL_TARGET_FILES", FinalTargetFiles), + ("FINAL_TARGET_PP_FILES", FinalTargetPreprocessedFiles), + ("LOCALIZED_FILES", LocalizedFiles), + ("LOCALIZED_PP_FILES", LocalizedPreprocessedFiles), + ("OBJDIR_FILES", ObjdirFiles), + ("OBJDIR_PP_FILES", ObjdirPreprocessedFiles), + ("TEST_HARNESS_FILES", TestHarnessFiles), + ): + all_files = context.get(var) + if not all_files: + continue + if dist_install is False and var != "TEST_HARNESS_FILES": + raise SandboxValidationError( + "%s cannot be used with DIST_INSTALL = False" % var, context + ) + has_prefs = False + has_resources = False + for base, files in all_files.walk(): + if var == "TEST_HARNESS_FILES" and not base: + raise SandboxValidationError( + "Cannot install files to the root of TEST_HARNESS_FILES", + context, + ) + if base == "components": + components.extend(files) + if base == "defaults/pref": + has_prefs = True + if mozpath.split(base)[0] == "res": + has_resources = True + for f in files: + if var in ( + "FINAL_TARGET_PP_FILES", + "OBJDIR_PP_FILES", + "LOCALIZED_PP_FILES", + ) and not isinstance(f, SourcePath): + raise SandboxValidationError( + ("Only source directory paths allowed in " + "%s: %s") + % (var, f), + context, + ) + if var.startswith("LOCALIZED_"): + if isinstance(f, SourcePath): + if f.startswith("en-US/"): + pass + elif "locales/en-US/" in f: + pass + else: + raise SandboxValidationError( + "%s paths must start with `en-US/` or " + "contain `locales/en-US/`: %s" % (var, f), + context, + ) + + if not isinstance(f, ObjDirPath): + path = f.full_path + if "*" not in path and not os.path.exists(path): + raise SandboxValidationError( + "File listed in %s does not exist: %s" % (var, path), + context, + ) + else: + # TODO: Bug 1254682 - The '/' check is to allow + # installing files generated from other directories, + # which is done occasionally for tests. However, it + # means we don't fail early if the file isn't actually + # created by the other moz.build file. + if f.target_basename not in generated_files and "/" not in f: + raise SandboxValidationError( + ( + "Objdir file listed in %s not in " + + "GENERATED_FILES: %s" + ) + % (var, f), + context, + ) + + if var.startswith("LOCALIZED_"): + # Further require that LOCALIZED_FILES are from + # LOCALIZED_GENERATED_FILES. + if f.target_basename not in localized_generated_files: + raise SandboxValidationError( + ( + "Objdir file listed in %s not in " + + "LOCALIZED_GENERATED_FILES: %s" + ) + % (var, f), + context, + ) + else: + # Additionally, don't allow LOCALIZED_GENERATED_FILES to be used + # in anything *but* LOCALIZED_FILES. + if f.target_basename in localized_generated_files: + raise SandboxValidationError( + ( + "Outputs of LOCALIZED_GENERATED_FILES cannot " + "be used in %s: %s" + ) + % (var, f), + context, + ) + + # Addons (when XPI_NAME is defined) and Applications (when + # DIST_SUBDIR is defined) use a different preferences directory + # (default/preferences) from the one the GRE uses (defaults/pref). + # Hence, we move the files from the latter to the former in that + # case. + if has_prefs and (context.get("XPI_NAME") or context.get("DIST_SUBDIR")): + all_files.defaults.preferences += all_files.defaults.pref + del all_files.defaults._children["pref"] + + if has_resources and ( + context.get("DIST_SUBDIR") or context.get("XPI_NAME") + ): + raise SandboxValidationError( + "RESOURCES_FILES cannot be used with DIST_SUBDIR or " "XPI_NAME.", + context, + ) + + yield cls(context, all_files) + + for c in components: + if c.endswith(".manifest"): + yield ChromeManifestEntry( + context, + "chrome.manifest", + Manifest("components", mozpath.basename(c)), + ) + + rust_tests = context.get("RUST_TESTS", []) + if rust_tests: + # TODO: more sophisticated checking of the declared name vs. + # contents of the Cargo.toml file. + features = context.get("RUST_TEST_FEATURES", []) + + yield RustTests(context, rust_tests, features) + + for obj in self._process_test_manifests(context): + yield obj + + for obj in self._process_jar_manifests(context): + yield obj + + computed_as_flags.resolve_flags("MOZBUILD", context.get("ASFLAGS")) + + if context.get("USE_NASM") is True: + nasm = context.config.substs.get("NASM") + if not nasm: + raise SandboxValidationError("nasm is not available", context) + passthru.variables["AS"] = nasm + passthru.variables["AS_DASH_C_FLAG"] = "" + passthru.variables["ASOUTOPTION"] = "-o " + computed_as_flags.resolve_flags( + "OS", context.config.substs.get("NASM_ASFLAGS", []) + ) + + if context.get("USE_INTEGRATED_CLANGCL_AS") is True: + if context.config.substs.get("CC_TYPE") != "clang-cl": + raise SandboxValidationError("clang-cl is not available", context) + passthru.variables["AS"] = context.config.substs.get("CC") + passthru.variables["AS_DASH_C_FLAG"] = "-c" + passthru.variables["ASOUTOPTION"] = "-o " + + if passthru.variables: + yield passthru + + if context.objdir in self._compile_dirs: + self._compile_flags[context.objdir] = computed_flags + yield computed_link_flags + + if context.objdir in self._asm_compile_dirs: + self._compile_as_flags[context.objdir] = computed_as_flags + + if context.objdir in self._host_compile_dirs: + yield computed_host_flags + + if context.objdir in self._wasm_compile_dirs: + yield computed_wasm_flags + + def _create_substitution(self, cls, context, path): + sub = cls(context) + sub.input_path = "%s.in" % path.full_path + sub.output_path = path.translated + sub.relpath = path + + return sub + + def _process_xpidl(self, context): + # XPIDL source files get processed and turned into .h and .xpt files. + # If there are multiple XPIDL files in a directory, they get linked + # together into a final .xpt, which has the name defined by + # XPIDL_MODULE. + xpidl_module = context["XPIDL_MODULE"] + + if not xpidl_module: + if context["XPIDL_SOURCES"]: + raise SandboxValidationError( + "XPIDL_MODULE must be defined if " "XPIDL_SOURCES is defined.", + context, + ) + return + + if not context["XPIDL_SOURCES"]: + raise SandboxValidationError( + "XPIDL_MODULE cannot be defined " "unless there are XPIDL_SOURCES", + context, + ) + + if context["DIST_INSTALL"] is False: + self.log( + logging.WARN, + "mozbuild_warning", + dict(path=context.main_path), + "{path}: DIST_INSTALL = False has no effect on XPIDL_SOURCES.", + ) + + for idl in context["XPIDL_SOURCES"]: + if not os.path.exists(idl.full_path): + raise SandboxValidationError( + "File %s from XPIDL_SOURCES " "does not exist" % idl.full_path, + context, + ) + + yield XPIDLModule(context, xpidl_module, context["XPIDL_SOURCES"]) + + def _process_generated_files(self, context): + for path in context["CONFIGURE_DEFINE_FILES"]: + script = mozpath.join( + mozpath.dirname(mozpath.dirname(__file__)), + "action", + "process_define_files.py", + ) + yield GeneratedFile( + context, + script, + "process_define_file", + six.text_type(path), + [Path(context, path + ".in")], + ) + + generated_files = context.get("GENERATED_FILES") or [] + localized_generated_files = context.get("LOCALIZED_GENERATED_FILES") or [] + if not (generated_files or localized_generated_files): + return + + for localized, gen in ( + (False, generated_files), + (True, localized_generated_files), + ): + for f in gen: + flags = gen[f] + outputs = f + inputs = [] + if flags.script: + method = "main" + script = SourcePath(context, flags.script).full_path + + # Deal with cases like "C:\\path\\to\\script.py:function". + if ".py:" in script: + script, method = script.rsplit(".py:", 1) + script += ".py" + + if not os.path.exists(script): + raise SandboxValidationError( + "Script for generating %s does not exist: %s" % (f, script), + context, + ) + if os.path.splitext(script)[1] != ".py": + raise SandboxValidationError( + "Script for generating %s does not end in .py: %s" + % (f, script), + context, + ) + else: + script = None + method = None + + for i in flags.inputs: + p = Path(context, i) + if isinstance(p, SourcePath) and not os.path.exists(p.full_path): + raise SandboxValidationError( + "Input for generating %s does not exist: %s" + % (f, p.full_path), + context, + ) + inputs.append(p) + + yield GeneratedFile( + context, + script, + method, + outputs, + inputs, + flags.flags, + localized=localized, + force=flags.force, + ) + + def _process_test_manifests(self, context): + for prefix, info in TEST_MANIFESTS.items(): + for path, manifest in context.get("%s_MANIFESTS" % prefix, []): + for obj in self._process_test_manifest(context, info, path, manifest): + yield obj + + for flavor in REFTEST_FLAVORS: + for path, manifest in context.get("%s_MANIFESTS" % flavor.upper(), []): + for obj in self._process_reftest_manifest( + context, flavor, path, manifest + ): + yield obj + + def _process_test_manifest(self, context, info, manifest_path, mpmanifest): + flavor, install_root, install_subdir, package_tests = info + + path = manifest_path.full_path + manifest_dir = mozpath.dirname(path) + manifest_reldir = mozpath.dirname( + mozpath.relpath(path, context.config.topsrcdir) + ) + manifest_sources = [ + mozpath.relpath(pth, context.config.topsrcdir) + for pth in mpmanifest.source_files + ] + install_prefix = mozpath.join(install_root, install_subdir) + + try: + if not mpmanifest.tests: + raise SandboxValidationError("Empty test manifest: %s" % path, context) + + defaults = mpmanifest.manifest_defaults[os.path.normpath(path)] + obj = TestManifest( + context, + path, + mpmanifest, + flavor=flavor, + install_prefix=install_prefix, + relpath=mozpath.join(manifest_reldir, mozpath.basename(path)), + sources=manifest_sources, + dupe_manifest="dupe-manifest" in defaults, + ) + + filtered = mpmanifest.tests + + missing = [t["name"] for t in filtered if not os.path.exists(t["path"])] + if missing: + raise SandboxValidationError( + "Test manifest (%s) lists " + "test that does not exist: %s" % (path, ", ".join(missing)), + context, + ) + + out_dir = mozpath.join(install_prefix, manifest_reldir) + + def process_support_files(test): + install_info = self._test_files_converter.convert_support_files( + test, install_root, manifest_dir, out_dir + ) + + obj.pattern_installs.extend(install_info.pattern_installs) + for source, dest in install_info.installs: + obj.installs[source] = (dest, False) + obj.external_installs |= install_info.external_installs + for install_path in install_info.deferred_installs: + if all( + [ + "*" not in install_path, + not os.path.isfile( + mozpath.join(context.config.topsrcdir, install_path[2:]) + ), + install_path not in install_info.external_installs, + ] + ): + raise SandboxValidationError( + "Error processing test " + "manifest %s: entry in support-files not present " + "in the srcdir: %s" % (path, install_path), + context, + ) + + obj.deferred_installs |= install_info.deferred_installs + + for test in filtered: + obj.tests.append(test) + + # Some test files are compiled and should not be copied into the + # test package. They function as identifiers rather than files. + if package_tests: + manifest_relpath = mozpath.relpath( + test["path"], mozpath.dirname(test["manifest"]) + ) + obj.installs[mozpath.normpath(test["path"])] = ( + (mozpath.join(out_dir, manifest_relpath)), + True, + ) + + process_support_files(test) + + for path, m_defaults in mpmanifest.manifest_defaults.items(): + process_support_files(m_defaults) + + # We also copy manifests into the output directory, + # including manifests from [include:foo] directives. + for mpath in mpmanifest.manifests(): + mpath = mozpath.normpath(mpath) + out_path = mozpath.join(out_dir, mozpath.basename(mpath)) + obj.installs[mpath] = (out_path, False) + + # Some manifests reference files that are auto generated as + # part of the build or shouldn't be installed for some + # reason. Here, we prune those files from the install set. + # FUTURE we should be able to detect autogenerated files from + # other build metadata. Once we do that, we can get rid of this. + for f in defaults.get("generated-files", "").split(): + # We re-raise otherwise the stack trace isn't informative. + try: + del obj.installs[mozpath.join(manifest_dir, f)] + except KeyError: + raise SandboxValidationError( + "Error processing test " + "manifest %s: entry in generated-files not present " + "elsewhere in manifest: %s" % (path, f), + context, + ) + + yield obj + except (AssertionError, Exception): + raise SandboxValidationError( + "Error processing test " + "manifest file %s: %s" + % (path, "\n".join(traceback.format_exception(*sys.exc_info()))), + context, + ) + + def _process_reftest_manifest(self, context, flavor, manifest_path, manifest): + manifest_full_path = manifest_path.full_path + manifest_reldir = mozpath.dirname( + mozpath.relpath(manifest_full_path, context.config.topsrcdir) + ) + + # reftest manifests don't come from manifest parser. But they are + # similar enough that we can use the same emitted objects. Note + # that we don't perform any installs for reftests. + obj = TestManifest( + context, + manifest_full_path, + manifest, + flavor=flavor, + install_prefix="%s/" % flavor, + relpath=mozpath.join(manifest_reldir, mozpath.basename(manifest_path)), + ) + obj.tests = list(sorted(manifest.tests, key=lambda t: t["path"])) + + yield obj + + def _process_jar_manifests(self, context): + jar_manifests = context.get("JAR_MANIFESTS", []) + if len(jar_manifests) > 1: + raise SandboxValidationError( + "While JAR_MANIFESTS is a list, " + "it is currently limited to one value.", + context, + ) + + for path in jar_manifests: + yield JARManifest(context, path) + + # Temporary test to look for jar.mn files that creep in without using + # the new declaration. Before, we didn't require jar.mn files to + # declared anywhere (they were discovered). This will detect people + # relying on the old behavior. + if os.path.exists(os.path.join(context.srcdir, "jar.mn")): + if "jar.mn" not in jar_manifests: + raise SandboxValidationError( + "A jar.mn exists but it " + "is not referenced in the moz.build file. " + "Please define JAR_MANIFESTS.", + context, + ) + + def _emit_directory_traversal_from_context(self, context): + o = DirectoryTraversal(context) + o.dirs = context.get("DIRS", []) + + # Some paths have a subconfigure, yet also have a moz.build. Those + # shouldn't end up in self._external_paths. + if o.objdir: + self._external_paths -= {o.relobjdir} + + yield o diff --git a/python/mozbuild/mozbuild/frontend/gyp_reader.py b/python/mozbuild/mozbuild/frontend/gyp_reader.py new file mode 100644 index 0000000000..d6e87b1557 --- /dev/null +++ b/python/mozbuild/mozbuild/frontend/gyp_reader.py @@ -0,0 +1,509 @@ +# 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/. + +import os +import sys +import time +from collections.abc import Iterable + +import gyp +import gyp.msvs_emulation +import mozpack.path as mozpath +import six +from mozpack.files import FileFinder + +from mozbuild import shellutil +from mozbuild.util import expand_variables + +from .context import VARIABLES, ObjDirPath, SourcePath, TemplateContext +from .sandbox import alphabetical_sorted + +# Define this module as gyp.generator.mozbuild so that gyp can use it +# as a generator under the name "mozbuild". +sys.modules["gyp.generator.mozbuild"] = sys.modules[__name__] + +# build/gyp_chromium does this: +# script_dir = os.path.dirname(os.path.realpath(__file__)) +# chrome_src = os.path.abspath(os.path.join(script_dir, os.pardir)) +# sys.path.insert(0, os.path.join(chrome_src, 'tools', 'gyp', 'pylib')) +# We're not importing gyp_chromium, but we want both script_dir and +# chrome_src for the default includes, so go backwards from the pylib +# directory, which is the parent directory of gyp module. +chrome_src = mozpath.abspath( + mozpath.join(mozpath.dirname(gyp.__file__), "../../../../..") +) +script_dir = mozpath.join(chrome_src, "build") + + +# Default variables gyp uses when evaluating gyp files. +generator_default_variables = {} +for dirname in [ + "INTERMEDIATE_DIR", + "SHARED_INTERMEDIATE_DIR", + "PRODUCT_DIR", + "LIB_DIR", + "SHARED_LIB_DIR", +]: + # Some gyp steps fail if these are empty(!). + generator_default_variables[dirname] = "$" + dirname + +for unused in [ + "RULE_INPUT_PATH", + "RULE_INPUT_ROOT", + "RULE_INPUT_NAME", + "RULE_INPUT_DIRNAME", + "RULE_INPUT_EXT", + "EXECUTABLE_PREFIX", + "EXECUTABLE_SUFFIX", + "STATIC_LIB_PREFIX", + "STATIC_LIB_SUFFIX", + "SHARED_LIB_PREFIX", + "SHARED_LIB_SUFFIX", + "LINKER_SUPPORTS_ICF", +]: + generator_default_variables[unused] = "" + + +class GypContext(TemplateContext): + """Specialized Context for use with data extracted from Gyp. + + config is the ConfigEnvironment for this context. + relobjdir is the object directory that will be used for this context, + relative to the topobjdir defined in the ConfigEnvironment. + """ + + def __init__(self, config, relobjdir): + self._relobjdir = relobjdir + TemplateContext.__init__( + self, template="Gyp", allowed_variables=VARIABLES, config=config + ) + + +def handle_actions(actions, context, action_overrides): + idir = "$INTERMEDIATE_DIR/" + for action in actions: + name = action["action_name"] + if name not in action_overrides: + raise RuntimeError("GYP action %s not listed in action_overrides" % name) + outputs = action["outputs"] + if len(outputs) > 1: + raise NotImplementedError( + "GYP actions with more than one output not supported: %s" % name + ) + output = outputs[0] + if not output.startswith(idir): + raise NotImplementedError( + "GYP actions outputting to somewhere other than " + "<(INTERMEDIATE_DIR) not supported: %s" % output + ) + output = output[len(idir) :] + context["GENERATED_FILES"] += [output] + g = context["GENERATED_FILES"][output] + g.script = action_overrides[name] + g.inputs = action["inputs"] + + +def handle_copies(copies, context): + dist = "$PRODUCT_DIR/dist/" + for copy in copies: + dest = copy["destination"] + if not dest.startswith(dist): + raise NotImplementedError( + "GYP copies to somewhere other than <(PRODUCT_DIR)/dist not supported: %s" + % dest + ) + dest_paths = dest[len(dist) :].split("/") + exports = context["EXPORTS"] + while dest_paths: + exports = getattr(exports, dest_paths.pop(0)) + exports += sorted(copy["files"], key=lambda x: x.lower()) + + +def process_gyp_result( + gyp_result, + gyp_dir_attrs, + path, + config, + output, + non_unified_sources, + action_overrides, +): + flat_list, targets, data = gyp_result + no_chromium = gyp_dir_attrs.no_chromium + no_unified = gyp_dir_attrs.no_unified + + # Process all targets from the given gyp files and its dependencies. + # The path given to AllTargets needs to use os.sep, while the frontend code + # gives us paths normalized with forward slash separator. + for target in sorted( + gyp.common.AllTargets(flat_list, targets, path.replace("/", os.sep)) + ): + build_file, target_name, toolset = gyp.common.ParseQualifiedTarget(target) + + # Each target is given its own objdir. The base of that objdir + # is derived from the relative path from the root gyp file path + # to the current build_file, placed under the given output + # directory. Since several targets can be in a given build_file, + # separate them in subdirectories using the build_file basename + # and the target_name. + reldir = mozpath.relpath(mozpath.dirname(build_file), mozpath.dirname(path)) + subdir = "%s_%s" % ( + mozpath.splitext(mozpath.basename(build_file))[0], + target_name, + ) + # Emit a context for each target. + context = GypContext( + config, + mozpath.relpath(mozpath.join(output, reldir, subdir), config.topobjdir), + ) + context.add_source(mozpath.abspath(build_file)) + # The list of included files returned by gyp are relative to build_file + for f in data[build_file]["included_files"]: + context.add_source( + mozpath.abspath(mozpath.join(mozpath.dirname(build_file), f)) + ) + + spec = targets[target] + + # Derive which gyp configuration to use based on MOZ_DEBUG. + c = "Debug" if config.substs.get("MOZ_DEBUG") else "Release" + if c not in spec["configurations"]: + raise RuntimeError( + "Missing %s gyp configuration for target %s " + "in %s" % (c, target_name, build_file) + ) + target_conf = spec["configurations"][c] + + if "actions" in spec: + handle_actions(spec["actions"], context, action_overrides) + if "copies" in spec: + handle_copies(spec["copies"], context) + + use_libs = [] + libs = [] + + def add_deps(s): + for t in s.get("dependencies", []) + s.get("dependencies_original", []): + ty = targets[t]["type"] + if ty in ("static_library", "shared_library"): + l = targets[t]["target_name"] + if l not in use_libs: + use_libs.append(l) + # Manually expand out transitive dependencies-- + # gyp won't do this for static libs or none targets. + if ty in ("static_library", "none"): + add_deps(targets[t]) + libs.extend(spec.get("libraries", [])) + + # XXX: this sucks, but webrtc breaks with this right now because + # it builds a library called 'gtest' and we just get lucky + # that it isn't in USE_LIBS by that name anywhere. + if no_chromium: + add_deps(spec) + + os_libs = [] + for l in libs: + if l.startswith("-"): + if l.startswith("-l"): + # Remove "-l" for consumption in OS_LIBS. Other flags + # are passed through unchanged. + l = l[2:] + if l not in os_libs: + os_libs.append(l) + elif l.endswith(".lib"): + l = l[:-4] + if l not in os_libs: + os_libs.append(l) + elif l: + # For library names passed in from moz.build. + l = os.path.basename(l) + if l not in use_libs: + use_libs.append(l) + + if spec["type"] == "none": + if not ("actions" in spec or "copies" in spec): + continue + elif spec["type"] in ("static_library", "shared_library", "executable"): + # Remove leading 'lib' from the target_name if any, and use as + # library name. + name = six.ensure_text(spec["target_name"]) + if spec["type"] in ("static_library", "shared_library"): + if name.startswith("lib"): + name = name[3:] + context["LIBRARY_NAME"] = name + else: + context["PROGRAM"] = name + if spec["type"] == "shared_library": + context["FORCE_SHARED_LIB"] = True + elif ( + spec["type"] == "static_library" + and spec.get("variables", {}).get("no_expand_libs", "0") == "1" + ): + # PSM links a NSS static library, but our folded libnss + # doesn't actually export everything that all of the + # objects within would need, so that one library + # should be built as a real static library. + context["NO_EXPAND_LIBS"] = True + if use_libs: + context["USE_LIBS"] = sorted(use_libs, key=lambda s: s.lower()) + if os_libs: + context["OS_LIBS"] = os_libs + # gyp files contain headers and asm sources in sources lists. + sources = [] + unified_sources = [] + extensions = set() + use_defines_in_asflags = False + for f in spec.get("sources", []): + ext = mozpath.splitext(f)[-1] + extensions.add(ext) + if f.startswith("$INTERMEDIATE_DIR/"): + s = ObjDirPath(context, f.replace("$INTERMEDIATE_DIR/", "!")) + else: + s = SourcePath(context, f) + if ext == ".h": + continue + if ext == ".def": + context["SYMBOLS_FILE"] = s + elif ext != ".S" and not no_unified and s not in non_unified_sources: + unified_sources.append(s) + else: + sources.append(s) + # The Mozilla build system doesn't use DEFINES for building + # ASFILES. + if ext == ".s": + use_defines_in_asflags = True + + # The context expects alphabetical order when adding sources + context["SOURCES"] = alphabetical_sorted(sources) + context["UNIFIED_SOURCES"] = alphabetical_sorted(unified_sources) + + defines = target_conf.get("defines", []) + if config.substs["CC_TYPE"] == "clang-cl" and no_chromium: + msvs_settings = gyp.msvs_emulation.MsvsSettings(spec, {}) + # Hack: MsvsSettings._TargetConfig tries to compare a str to an int, + # so convert manually. + msvs_settings.vs_version.short_name = int( + msvs_settings.vs_version.short_name + ) + defines.extend(msvs_settings.GetComputedDefines(c)) + for define in defines: + if "=" in define: + name, value = define.split("=", 1) + context["DEFINES"][name] = value + else: + context["DEFINES"][define] = True + + product_dir_dist = "$PRODUCT_DIR/dist/" + for include in target_conf.get("include_dirs", []): + if include.startswith(product_dir_dist): + # special-case includes of <(PRODUCT_DIR)/dist/ to match + # handle_copies above. This is used for NSS' exports. + include = "!/dist/include/" + include[len(product_dir_dist) :] + elif include.startswith(config.topobjdir): + # NSPR_INCLUDE_DIR gets passed into the NSS build this way. + include = "!/" + mozpath.relpath(include, config.topobjdir) + else: + # moz.build expects all LOCAL_INCLUDES to exist, so ensure they do. + # + # NB: gyp files sometimes have actual absolute paths (e.g. + # /usr/include32) and sometimes paths that moz.build considers + # absolute, i.e. starting from topsrcdir. There's no good way + # to tell them apart here, and the actual absolute paths are + # likely bogus. In any event, actual absolute paths will be + # filtered out by trying to find them in topsrcdir. + # + # We do allow !- and %-prefixed paths, assuming they come + # from moz.build and will be handled the same way as if they + # were given to LOCAL_INCLUDES in moz.build. + if include.startswith("/"): + resolved = mozpath.abspath( + mozpath.join(config.topsrcdir, include[1:]) + ) + elif not include.startswith(("!", "%")): + resolved = mozpath.abspath( + mozpath.join(mozpath.dirname(build_file), include) + ) + if not include.startswith(("!", "%")) and not os.path.exists( + resolved + ): + continue + context["LOCAL_INCLUDES"] += [include] + + context["ASFLAGS"] = target_conf.get("asflags_mozilla", []) + if use_defines_in_asflags and defines: + context["ASFLAGS"] += ["-D" + d for d in defines] + if config.substs["OS_TARGET"] == "SunOS": + context["LDFLAGS"] = target_conf.get("ldflags", []) + flags = target_conf.get("cflags_mozilla", []) + if flags: + suffix_map = { + ".c": "CFLAGS", + ".cpp": "CXXFLAGS", + ".cc": "CXXFLAGS", + ".m": "CMFLAGS", + ".mm": "CMMFLAGS", + } + variables = (suffix_map[e] for e in extensions if e in suffix_map) + for var in variables: + for f in flags: + # We may be getting make variable references out of the + # gyp data, and we don't want those in emitted data, so + # substitute them with their actual value. + f = expand_variables(f, config.substs).split() + if not f: + continue + # the result may be a string or a list. + if isinstance(f, six.string_types): + context[var].append(f) + else: + context[var].extend(f) + else: + # Ignore other types because we don't have + # anything using them, and we're not testing them. They can be + # added when that becomes necessary. + raise NotImplementedError("Unsupported gyp target type: %s" % spec["type"]) + + if not no_chromium: + # Add some features to all contexts. Put here in case LOCAL_INCLUDES + # order matters. + context["LOCAL_INCLUDES"] += [ + "!/ipc/ipdl/_ipdlheaders", + "/ipc/chromium/src", + ] + # These get set via VC project file settings for normal GYP builds. + if config.substs["OS_TARGET"] == "WINNT": + context["DEFINES"]["UNICODE"] = True + context["DEFINES"]["_UNICODE"] = True + context["COMPILE_FLAGS"]["OS_INCLUDES"] = [] + + for key, value in gyp_dir_attrs.sandbox_vars.items(): + if context.get(key) and isinstance(context[key], list): + # If we have a key from sanbox_vars that's also been + # populated here we use the value from sandbox_vars as our + # basis rather than overriding outright. + context[key] = value + context[key] + elif context.get(key) and isinstance(context[key], dict): + context[key].update(value) + else: + context[key] = value + + yield context + + +# A version of gyp.Load that doesn't return the generator (because module objects +# aren't Pickle-able, and we don't use it anyway). +def load_gyp(*args): + _, flat_list, targets, data = gyp.Load(*args) + return flat_list, targets, data + + +class GypProcessor(object): + """Reads a gyp configuration in the background using the given executor and + emits GypContexts for the backend to process. + + config is a ConfigEnvironment, path is the path to a root gyp configuration + file, and output is the base path under which the objdir for the various + gyp dependencies will be. gyp_dir_attrs are attributes set for the dir + from moz.build. + """ + + def __init__( + self, + config, + gyp_dir_attrs, + path, + output, + executor, + action_overrides, + non_unified_sources, + ): + self._path = path + self._config = config + self._output = output + self._non_unified_sources = non_unified_sources + self._gyp_dir_attrs = gyp_dir_attrs + self._action_overrides = action_overrides + self.execution_time = 0.0 + self._results = [] + + # gyp expects plain str instead of unicode. The frontend code gives us + # unicode strings, so convert them. + if config.substs["CC_TYPE"] == "clang-cl": + # This isn't actually used anywhere in this generator, but it's needed + # to override the registry detection of VC++ in gyp. + os.environ.update( + { + "GYP_MSVS_OVERRIDE_PATH": "fake_path", + "GYP_MSVS_VERSION": config.substs["MSVS_VERSION"], + } + ) + + params = { + "parallel": False, + "generator_flags": {}, + "build_files": [path], + "root_targets": None, + } + # The NSS gyp configuration uses CC and CFLAGS to determine the + # floating-point ABI on arm. + os.environ.update( + CC=config.substs["CC"], + CFLAGS=shellutil.quote(*config.substs["CC_BASE_FLAGS"]), + ) + + if gyp_dir_attrs.no_chromium: + includes = [] + depth = mozpath.dirname(path) + else: + depth = chrome_src + # Files that gyp_chromium always includes + includes = [mozpath.join(script_dir, "gyp_includes", "common.gypi")] + finder = FileFinder(chrome_src) + includes.extend( + mozpath.join(chrome_src, name) + for name, _ in finder.find("*/supplement.gypi") + ) + + # We need to normalize EnumStrings to strings because the gyp code is + # not guaranteed to like them. + def normalize(obj): + if isinstance(obj, dict): + return {k: normalize(v) for k, v in obj.items()} + if isinstance(obj, str): # includes EnumStrings + return str(obj) + if isinstance(obj, Iterable): + return [normalize(o) for o in obj] + return obj + + str_vars = normalize(gyp_dir_attrs.variables) + str_vars["python"] = sys.executable + self._gyp_loader_future = executor.submit( + load_gyp, [path], "mozbuild", str_vars, includes, depth, params + ) + + @property + def results(self): + if self._results: + for res in self._results: + yield res + else: + # We report our execution time as the time spent blocked in a call + # to `result`, which is the only case a gyp processor will + # contribute significantly to total wall time. + t0 = time.monotonic() + flat_list, targets, data = self._gyp_loader_future.result() + self.execution_time += time.monotonic() - t0 + results = [] + for res in process_gyp_result( + (flat_list, targets, data), + self._gyp_dir_attrs, + self._path, + self._config, + self._output, + self._non_unified_sources, + self._action_overrides, + ): + results.append(res) + yield res + self._results = results diff --git a/python/mozbuild/mozbuild/frontend/mach_commands.py b/python/mozbuild/mozbuild/frontend/mach_commands.py new file mode 100644 index 0000000000..6d379977df --- /dev/null +++ b/python/mozbuild/mozbuild/frontend/mach_commands.py @@ -0,0 +1,338 @@ +# 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/. + +import json +import os +import sys +from collections import defaultdict + +import mozpack.path as mozpath +from mach.decorators import Command, CommandArgument, SubCommand + +TOPSRCDIR = os.path.abspath(os.path.join(__file__, "../../../../../")) + + +class InvalidPathException(Exception): + """Represents an error due to an invalid path.""" + + +@Command( + "mozbuild-reference", + category="build-dev", + description="View reference documentation on mozbuild files.", + virtualenv_name="docs", +) +@CommandArgument( + "symbol", + default=None, + nargs="*", + help="Symbol to view help on. If not specified, all will be shown.", +) +@CommandArgument( + "--name-only", + "-n", + default=False, + action="store_true", + help="Print symbol names only.", +) +def reference(command_context, symbol, name_only=False): + import mozbuild.frontend.context as m + from mozbuild.sphinx import ( + format_module, + function_reference, + special_reference, + variable_reference, + ) + + if name_only: + for s in sorted(m.VARIABLES.keys()): + print(s) + + for s in sorted(m.FUNCTIONS.keys()): + print(s) + + for s in sorted(m.SPECIAL_VARIABLES.keys()): + print(s) + + return 0 + + if len(symbol): + for s in symbol: + if s in m.VARIABLES: + for line in variable_reference(s, *m.VARIABLES[s]): + print(line) + continue + elif s in m.FUNCTIONS: + for line in function_reference(s, *m.FUNCTIONS[s]): + print(line) + continue + elif s in m.SPECIAL_VARIABLES: + for line in special_reference(s, *m.SPECIAL_VARIABLES[s]): + print(line) + continue + + print("Could not find symbol: %s" % s) + return 1 + + return 0 + + for line in format_module(m): + print(line) + + return 0 + + +@Command( + "file-info", category="build-dev", description="Query for metadata about files." +) +def file_info(command_context): + """Show files metadata derived from moz.build files. + + moz.build files contain "Files" sub-contexts for declaring metadata + against file patterns. This command suite is used to query that data. + """ + + +@SubCommand( + "file-info", + "bugzilla-component", + "Show Bugzilla component info for files listed.", +) +@CommandArgument("-r", "--rev", help="Version control revision to look up info from") +@CommandArgument( + "--format", + choices={"json", "plain"}, + default="plain", + help="Output format", + dest="fmt", +) +@CommandArgument("paths", nargs="+", help="Paths whose data to query") +def file_info_bugzilla(command_context, paths, rev=None, fmt=None): + """Show Bugzilla component for a set of files. + + Given a requested set of files (which can be specified using + wildcards), print the Bugzilla component for each file. + """ + components = defaultdict(set) + try: + for p, m in _get_files_info(command_context, paths, rev=rev).items(): + components[m.get("BUG_COMPONENT")].add(p) + except InvalidPathException as e: + print(e) + return 1 + + if fmt == "json": + data = {} + for component, files in components.items(): + if not component: + continue + for f in files: + data[f] = [component.product, component.component] + + json.dump(data, sys.stdout, sort_keys=True, indent=2) + return + elif fmt == "plain": + comp_to_file = sorted( + ( + "UNKNOWN" + if component is None + else "%s :: %s" % (component.product, component.component), + sorted(files), + ) + for component, files in components.items() + ) + for component, files in comp_to_file: + print(component) + for f in files: + print(" %s" % f) + else: + print("unhandled output format: %s" % fmt) + return 1 + + +@SubCommand( + "file-info", "missing-bugzilla", "Show files missing Bugzilla component info" +) +@CommandArgument("-r", "--rev", help="Version control revision to look up info from") +@CommandArgument( + "--format", + choices={"json", "plain"}, + dest="fmt", + default="plain", + help="Output format", +) +@CommandArgument("paths", nargs="+", help="Paths whose data to query") +def file_info_missing_bugzilla(command_context, paths, rev=None, fmt=None): + missing = set() + + try: + for p, m in _get_files_info(command_context, paths, rev=rev).items(): + if "BUG_COMPONENT" not in m: + missing.add(p) + except InvalidPathException as e: + print(e) + return 1 + + if fmt == "json": + json.dump({"missing": sorted(missing)}, sys.stdout, indent=2) + return + elif fmt == "plain": + for f in sorted(missing): + print(f) + else: + print("unhandled output format: %s" % fmt) + return 1 + + +@SubCommand( + "file-info", + "bugzilla-automation", + "Perform Bugzilla metadata analysis as required for automation", +) +@CommandArgument("out_dir", help="Where to write files") +def bugzilla_automation(command_context, out_dir): + """Analyze and validate Bugzilla metadata as required by automation. + + This will write out JSON and gzipped JSON files for Bugzilla metadata. + + The exit code will be non-0 if Bugzilla metadata fails validation. + """ + import gzip + + missing_component = set() + seen_components = set() + component_by_path = {} + + # TODO operate in VCS space. This requires teaching the VCS reader + # to understand wildcards and/or for the relative path issue in the + # VCS finder to be worked out. + for p, m in sorted(_get_files_info(command_context, ["**"]).items()): + if "BUG_COMPONENT" not in m: + missing_component.add(p) + print( + "FileToBugzillaMappingError: Missing Bugzilla component: " + "%s - Set the BUG_COMPONENT in the moz.build file to fix " + "the issue." % p + ) + continue + + c = m["BUG_COMPONENT"] + seen_components.add(c) + component_by_path[p] = [c.product, c.component] + + print("Examined %d files" % len(component_by_path)) + + # We also have a normalized versions of the file to components mapping + # that requires far less storage space by eliminating redundant strings. + indexed_components = { + i: [c.product, c.component] for i, c in enumerate(sorted(seen_components)) + } + components_index = {tuple(v): k for k, v in indexed_components.items()} + normalized_component = {"components": indexed_components, "paths": {}} + + for p, c in component_by_path.items(): + d = normalized_component["paths"] + while "/" in p: + base, p = p.split("/", 1) + d = d.setdefault(base, {}) + + d[p] = components_index[tuple(c)] + + if not os.path.exists(out_dir): + os.makedirs(out_dir) + + components_json = os.path.join(out_dir, "components.json") + print("Writing %s" % components_json) + with open(components_json, "w") as fh: + json.dump(component_by_path, fh, sort_keys=True, indent=2) + + missing_json = os.path.join(out_dir, "missing.json") + print("Writing %s" % missing_json) + with open(missing_json, "w") as fh: + json.dump({"missing": sorted(missing_component)}, fh, indent=2) + + indexed_components_json = os.path.join(out_dir, "components-normalized.json") + print("Writing %s" % indexed_components_json) + with open(indexed_components_json, "w") as fh: + # Don't indent so file is as small as possible. + json.dump(normalized_component, fh, sort_keys=True) + + # Write compressed versions of JSON files. + for p in (components_json, indexed_components_json, missing_json): + gzip_path = "%s.gz" % p + print("Writing %s" % gzip_path) + with open(p, "rb") as ifh, gzip.open(gzip_path, "wb") as ofh: + while True: + data = ifh.read(32768) + if not data: + break + ofh.write(data) + + # Causes CI task to fail if files are missing Bugzilla annotation. + if missing_component: + return 1 + + +def _get_files_info(command_context, paths, rev=None): + reader = command_context.mozbuild_reader(config_mode="empty", vcs_revision=rev) + + # Normalize to relative from topsrcdir. + relpaths = [] + for p in paths: + a = mozpath.abspath(p) + if not mozpath.basedir(a, [command_context.topsrcdir]): + raise InvalidPathException("path is outside topsrcdir: %s" % p) + + relpaths.append(mozpath.relpath(a, command_context.topsrcdir)) + + # Expand wildcards. + # One variable is for ordering. The other for membership tests. + # (Membership testing on a list can be slow.) + allpaths = [] + all_paths_set = set() + for p in relpaths: + if "*" not in p: + if p not in all_paths_set: + if not os.path.exists(mozpath.join(command_context.topsrcdir, p)): + print("(%s does not exist; ignoring)" % p, file=sys.stderr) + continue + + all_paths_set.add(p) + allpaths.append(p) + continue + + if rev: + raise InvalidPathException("cannot use wildcard in version control mode") + + # finder is rooted at / for now. + # TODO bug 1171069 tracks changing to relative. + search = mozpath.join(command_context.topsrcdir, p)[1:] + for path, f in reader.finder.find(search): + path = path[len(command_context.topsrcdir) :] + if path not in all_paths_set: + all_paths_set.add(path) + allpaths.append(path) + + return reader.files_info(allpaths) + + +@SubCommand( + "file-info", "schedules", "Show the combined SCHEDULES for the files listed." +) +@CommandArgument("paths", nargs="+", help="Paths whose data to query") +def file_info_schedules(command_context, paths): + """Show what is scheduled by the given files. + + Given a requested set of files (which can be specified using + wildcards), print the total set of scheduled components. + """ + from mozbuild.frontend.reader import BuildReader, EmptyConfig + + config = EmptyConfig(TOPSRCDIR) + reader = BuildReader(config) + schedules = set() + for p, m in reader.files_info(paths).items(): + schedules |= set(m["SCHEDULES"].components) + + print(", ".join(schedules)) diff --git a/python/mozbuild/mozbuild/frontend/reader.py b/python/mozbuild/mozbuild/frontend/reader.py new file mode 100644 index 0000000000..d0066e1071 --- /dev/null +++ b/python/mozbuild/mozbuild/frontend/reader.py @@ -0,0 +1,1419 @@ +# 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/. + +# This file contains code for reading metadata from the build system into +# data structures. + +r"""Read build frontend files into data structures. + +In terms of code architecture, the main interface is BuildReader. BuildReader +starts with a root mozbuild file. It creates a new execution environment for +this file, which is represented by the Sandbox class. The Sandbox class is used +to fill a Context, representing the output of an individual mozbuild file. The + +The BuildReader contains basic logic for traversing a tree of mozbuild files. +It does this by examining specific variables populated during execution. +""" + +import ast +import functools +import inspect +import logging +import os +import sys +import textwrap +import time +import traceback +import types +from collections import OrderedDict, defaultdict +from concurrent.futures.process import ProcessPoolExecutor +from io import StringIO +from itertools import chain +from multiprocessing import cpu_count + +import mozpack.path as mozpath +from mozpack.files import FileFinder + +from mozbuild.backend.configenvironment import ConfigEnvironment +from mozbuild.base import ExecutionSummary +from mozbuild.util import ( + EmptyValue, + HierarchicalStringList, + ReadOnlyDefaultDict, + memoize, +) + +from .context import ( + DEPRECATION_HINTS, + FUNCTIONS, + SPECIAL_VARIABLES, + SUBCONTEXTS, + VARIABLES, + Context, + ContextDerivedValue, + Files, + SourcePath, + SubContext, + TemplateContext, +) +from .sandbox import ( + Sandbox, + SandboxError, + SandboxExecutionError, + SandboxLoadError, + default_finder, +) + + +def log(logger, level, action, params, formatter): + logger.log(level, formatter, extra={"action": action, "params": params}) + + +class EmptyConfig(object): + """A config object that is empty. + + This config object is suitable for using with a BuildReader on a vanilla + checkout, without any existing configuration. The config is simply + bootstrapped from a top source directory path. + """ + + class PopulateOnGetDict(ReadOnlyDefaultDict): + """A variation on ReadOnlyDefaultDict that populates during .get(). + + This variation is needed because CONFIG uses .get() to access members. + Without it, None (instead of our EmptyValue types) would be returned. + """ + + def get(self, key, default=None): + return self[key] + + default_substs = { + # These 2 variables are used semi-frequently and it isn't worth + # changing all the instances. + "MOZ_APP_NAME": "empty", + "MOZ_CHILD_PROCESS_NAME": "empty", + # Needed to prevent js/src's config.status from loading. + "JS_STANDALONE": "1", + } + + def __init__(self, topsrcdir, substs=None): + self.topsrcdir = topsrcdir + self.topobjdir = "" + + self.substs = self.PopulateOnGetDict(EmptyValue, substs or self.default_substs) + self.defines = self.substs + self.error_is_fatal = False + + +def is_read_allowed(path, config): + """Whether we are allowed to load a mozbuild file at the specified path. + + This is used as cheap security to ensure the build is isolated to known + source directories. + + We are allowed to read from the main source directory and any defined + external source directories. The latter is to allow 3rd party applications + to hook into our build system. + """ + assert os.path.isabs(path) + assert os.path.isabs(config.topsrcdir) + + path = mozpath.normpath(path) + topsrcdir = mozpath.normpath(config.topsrcdir) + + if mozpath.basedir(path, [topsrcdir]): + return True + + return False + + +class SandboxCalledError(SandboxError): + """Represents an error resulting from calling the error() function.""" + + def __init__(self, file_stack, message): + SandboxError.__init__(self, file_stack) + self.message = message + + +class MozbuildSandbox(Sandbox): + """Implementation of a Sandbox tailored for mozbuild files. + + We expose a few useful functions and expose the set of variables defining + Mozilla's build system. + + context is a Context instance. + + metadata is a dict of metadata that can be used during the sandbox + evaluation. + """ + + def __init__(self, context, metadata={}, finder=default_finder): + assert isinstance(context, Context) + + Sandbox.__init__(self, context, finder=finder) + + self._log = logging.getLogger(__name__) + + self.metadata = dict(metadata) + exports = self.metadata.get("exports", {}) + self.exports = set(exports.keys()) + context.update(exports) + self.templates = self.metadata.setdefault("templates", {}) + self.special_variables = self.metadata.setdefault( + "special_variables", SPECIAL_VARIABLES + ) + self.functions = self.metadata.setdefault("functions", FUNCTIONS) + self.subcontext_types = self.metadata.setdefault("subcontexts", SUBCONTEXTS) + + def __getitem__(self, key): + if key in self.special_variables: + return self.special_variables[key][0](self._context) + if key in self.functions: + return self._create_function(self.functions[key]) + if key in self.subcontext_types: + return self._create_subcontext(self.subcontext_types[key]) + if key in self.templates: + return self._create_template_wrapper(self.templates[key]) + return Sandbox.__getitem__(self, key) + + def __contains__(self, key): + if any( + key in d + for d in ( + self.special_variables, + self.functions, + self.subcontext_types, + self.templates, + ) + ): + return True + + return Sandbox.__contains__(self, key) + + def __setitem__(self, key, value): + if key in self.special_variables and value is self[key]: + return + if ( + key in self.special_variables + or key in self.functions + or key in self.subcontext_types + ): + raise KeyError('Cannot set "%s" because it is a reserved keyword' % key) + if key in self.exports: + self._context[key] = value + self.exports.remove(key) + return + Sandbox.__setitem__(self, key, value) + + def exec_file(self, path): + """Override exec_file to normalize paths and restrict file loading. + + Paths will be rejected if they do not fall under topsrcdir or one of + the external roots. + """ + + # realpath() is needed for true security. But, this isn't for security + # protection, so it is omitted. + if not is_read_allowed(path, self._context.config): + raise SandboxLoadError( + self._context.source_stack, sys.exc_info()[2], illegal_path=path + ) + + Sandbox.exec_file(self, path) + + def _export(self, varname): + """Export the variable to all subdirectories of the current path.""" + + exports = self.metadata.setdefault("exports", dict()) + if varname in exports: + raise Exception("Variable has already been exported: %s" % varname) + + try: + # Doing a regular self._context[varname] causes a set as a side + # effect. By calling the dict method instead, we don't have any + # side effects. + exports[varname] = dict.__getitem__(self._context, varname) + except KeyError: + self.last_name_error = KeyError("global_ns", "get_unknown", varname) + raise self.last_name_error + + def recompute_exports(self): + """Recompute the variables to export to subdirectories with the current + values in the subdirectory.""" + + if "exports" in self.metadata: + for key in self.metadata["exports"]: + self.metadata["exports"][key] = self[key] + + def _include(self, path): + """Include and exec another file within the context of this one.""" + + # path is a SourcePath + self.exec_file(path.full_path) + + def _warning(self, message): + # FUTURE consider capturing warnings in a variable instead of printing. + print("WARNING: %s" % message, file=sys.stderr) + + def _error(self, message): + if self._context.error_is_fatal: + raise SandboxCalledError(self._context.source_stack, message) + else: + self._warning(message) + + def _template_decorator(self, func): + """Registers a template function.""" + + if not inspect.isfunction(func): + raise Exception( + "`template` is a function decorator. You must " + "use it as `@template` preceding a function declaration." + ) + + name = func.__name__ + + if name in self.templates: + raise KeyError( + 'A template named "%s" was already declared in %s.' + % (name, self.templates[name].path) + ) + + if name.islower() or name.isupper() or name[0].islower(): + raise NameError("Template function names must be CamelCase.") + + self.templates[name] = TemplateFunction(func, self) + + @memoize + def _create_subcontext(self, cls): + """Return a function object that creates SubContext instances.""" + + def fn(*args, **kwargs): + return cls(self._context, *args, **kwargs) + + return fn + + @memoize + def _create_function(self, function_def): + """Returns a function object for use within the sandbox for the given + function definition. + + The wrapper function does type coercion on the function arguments + """ + func, args_def, doc = function_def + + def function(*args): + def coerce(arg, type): + if not isinstance(arg, type): + if issubclass(type, ContextDerivedValue): + arg = type(self._context, arg) + else: + arg = type(arg) + return arg + + args = [coerce(arg, type) for arg, type in zip(args, args_def)] + return func(self)(*args) + + return function + + @memoize + def _create_template_wrapper(self, template): + """Returns a function object for use within the sandbox for the given + TemplateFunction instance.. + + When a moz.build file contains a reference to a template call, the + sandbox needs a function to execute. This is what this method returns. + That function creates a new sandbox for execution of the template. + After the template is executed, the data from its execution is merged + with the context of the calling sandbox. + """ + + def template_wrapper(*args, **kwargs): + context = TemplateContext( + template=template.name, + allowed_variables=self._context._allowed_variables, + config=self._context.config, + ) + context.add_source(self._context.current_path) + for p in self._context.all_paths: + context.add_source(p) + + sandbox = MozbuildSandbox( + context, + metadata={ + # We should arguably set these defaults to something else. + # Templates, for example, should arguably come from the state + # of the sandbox from when the template was declared, not when + # it was instantiated. Bug 1137319. + "functions": self.metadata.get("functions", {}), + "special_variables": self.metadata.get("special_variables", {}), + "subcontexts": self.metadata.get("subcontexts", {}), + "templates": self.metadata.get("templates", {}), + }, + finder=self._finder, + ) + + template.exec_in_sandbox(sandbox, *args, **kwargs) + + # This is gross, but allows the merge to happen. Eventually, the + # merging will go away and template contexts emitted independently. + klass = self._context.__class__ + self._context.__class__ = TemplateContext + # The sandbox will do all the necessary checks for these merges. + for key, value in context.items(): + if isinstance(value, dict): + self[key].update(value) + elif isinstance(value, (list, HierarchicalStringList)): + self[key] += value + else: + self[key] = value + self._context.__class__ = klass + + for p in context.all_paths: + self._context.add_source(p) + + return template_wrapper + + +class TemplateFunction(object): + def __init__(self, func, sandbox): + self.path = func.__code__.co_filename + self.name = func.__name__ + + code = func.__code__ + firstlineno = code.co_firstlineno + lines = sandbox._current_source.splitlines(True) + if lines: + # Older versions of python 2.7 had a buggy inspect.getblock() that + # would ignore the last line if it didn't terminate with a newline. + if not lines[-1].endswith("\n"): + lines[-1] += "\n" + lines = inspect.getblock(lines[firstlineno - 1 :]) + + # The code lines we get out of inspect.getsourcelines look like + # @template + # def Template(*args, **kwargs): + # VAR = 'value' + # ... + func_ast = ast.parse("".join(lines), self.path) + # Remove decorators + func_ast.body[0].decorator_list = [] + # Adjust line numbers accordingly + ast.increment_lineno(func_ast, firstlineno - 1) + + # When using a custom dictionary for function globals/locals, Cpython + # actually never calls __getitem__ and __setitem__, so we need to + # modify the AST so that accesses to globals are properly directed + # to a dict. + self._global_name = "_data" + # In case '_data' is a name used for a variable in the function code, + # prepend more underscores until we find an unused name. + while ( + self._global_name in code.co_names or self._global_name in code.co_varnames + ): + self._global_name += "_" + func_ast = self.RewriteName(sandbox, self._global_name).visit(func_ast) + + # Execute the rewritten code. That code now looks like: + # def Template(*args, **kwargs): + # _data['VAR'] = 'value' + # ... + # The result of executing this code is the creation of a 'Template' + # function object in the global namespace. + glob = {"__builtins__": sandbox._builtins} + func = types.FunctionType( + compile(func_ast, self.path, "exec"), + glob, + self.name, + func.__defaults__, + func.__closure__, + ) + func() + + self._func = glob[self.name] + + def exec_in_sandbox(self, sandbox, *args, **kwargs): + """Executes the template function in the given sandbox.""" + # Create a new function object associated with the execution sandbox + glob = {self._global_name: sandbox, "__builtins__": sandbox._builtins} + func = types.FunctionType( + self._func.__code__, + glob, + self.name, + self._func.__defaults__, + self._func.__closure__, + ) + sandbox.exec_function(func, args, kwargs, self.path, becomes_current_path=False) + + class RewriteName(ast.NodeTransformer): + """AST Node Transformer to rewrite variable accesses to go through + a dict. + """ + + def __init__(self, sandbox, global_name): + self._sandbox = sandbox + self._global_name = global_name + + def visit_Name(self, node): + # Modify uppercase variable references and names known to the + # sandbox as if they were retrieved from a dict instead. + if not node.id.isupper() and node.id not in self._sandbox: + return node + + def c(new_node): + return ast.copy_location(new_node, node) + + return c( + ast.Subscript( + value=c(ast.Name(id=self._global_name, ctx=ast.Load())), + slice=c(ast.Index(value=c(ast.Str(s=node.id)))), + ctx=node.ctx, + ) + ) + + +class SandboxValidationError(Exception): + """Represents an error encountered when validating sandbox results.""" + + def __init__(self, message, context): + Exception.__init__(self, message) + self.context = context + + def __str__(self): + s = StringIO() + + delim = "=" * 30 + s.write("\n%s\nFATAL ERROR PROCESSING MOZBUILD FILE\n%s\n\n" % (delim, delim)) + + s.write("The error occurred while processing the following file or ") + s.write("one of the files it includes:\n") + s.write("\n") + s.write(" %s/moz.build\n" % self.context.srcdir) + s.write("\n") + + s.write("The error occurred when validating the result of ") + s.write("the execution. The reported error is:\n") + s.write("\n") + s.write( + "".join( + " %s\n" % l + for l in super(SandboxValidationError, self).__str__().splitlines() + ) + ) + s.write("\n") + + return s.getvalue() + + +class BuildReaderError(Exception): + """Represents errors encountered during BuildReader execution. + + The main purpose of this class is to facilitate user-actionable error + messages. Execution errors should say: + + - Why they failed + - Where they failed + - What can be done to prevent the error + + A lot of the code in this class should arguably be inside sandbox.py. + However, extraction is somewhat difficult given the additions + MozbuildSandbox has over Sandbox (e.g. the concept of included files - + which affect error messages, of course). + """ + + def __init__( + self, + file_stack, + trace, + sandbox_exec_error=None, + sandbox_load_error=None, + validation_error=None, + other_error=None, + sandbox_called_error=None, + ): + self.file_stack = file_stack + self.trace = trace + self.sandbox_called_error = sandbox_called_error + self.sandbox_exec = sandbox_exec_error + self.sandbox_load = sandbox_load_error + self.validation_error = validation_error + self.other = other_error + + @property + def main_file(self): + return self.file_stack[-1] + + @property + def actual_file(self): + # We report the file that called out to the file that couldn't load. + if self.sandbox_load is not None: + if len(self.sandbox_load.file_stack) > 1: + return self.sandbox_load.file_stack[-2] + + if len(self.file_stack) > 1: + return self.file_stack[-2] + + if self.sandbox_error is not None and len(self.sandbox_error.file_stack): + return self.sandbox_error.file_stack[-1] + + return self.file_stack[-1] + + @property + def sandbox_error(self): + return self.sandbox_exec or self.sandbox_load or self.sandbox_called_error + + def __str__(self): + s = StringIO() + + delim = "=" * 30 + s.write("\n%s\nFATAL ERROR PROCESSING MOZBUILD FILE\n%s\n\n" % (delim, delim)) + + s.write("The error occurred while processing the following file:\n") + s.write("\n") + s.write(" %s\n" % self.actual_file) + s.write("\n") + + if self.actual_file != self.main_file and not self.sandbox_load: + s.write("This file was included as part of processing:\n") + s.write("\n") + s.write(" %s\n" % self.main_file) + s.write("\n") + + if self.sandbox_error is not None: + self._print_sandbox_error(s) + elif self.validation_error is not None: + s.write("The error occurred when validating the result of ") + s.write("the execution. The reported error is:\n") + s.write("\n") + s.write( + "".join(" %s\n" % l for l in str(self.validation_error).splitlines()) + ) + s.write("\n") + else: + s.write("The error appears to be part of the %s " % __name__) + s.write("Python module itself! It is possible you have stumbled ") + s.write("across a legitimate bug.\n") + s.write("\n") + + for l in traceback.format_exception( + type(self.other), self.other, self.trace + ): + s.write(str(l)) + + return s.getvalue() + + def _print_sandbox_error(self, s): + # Try to find the frame of the executed code. + script_frame = None + + # We don't currently capture the trace for SandboxCalledError. + # Therefore, we don't get line numbers from the moz.build file. + # FUTURE capture this. + trace = getattr(self.sandbox_error, "trace", None) + frames = [] + if trace: + frames = traceback.extract_tb(trace) + for frame in frames: + if frame[0] == self.actual_file: + script_frame = frame + + # Reset if we enter a new execution context. This prevents errors + # in this module from being attributes to a script. + elif frame[0] == __file__ and frame[2] == "exec_function": + script_frame = None + + if script_frame is not None: + s.write("The error was triggered on line %d " % script_frame[1]) + s.write("of this file:\n") + s.write("\n") + s.write(" %s\n" % script_frame[3]) + s.write("\n") + + if self.sandbox_called_error is not None: + self._print_sandbox_called_error(s) + return + + if self.sandbox_load is not None: + self._print_sandbox_load_error(s) + return + + self._print_sandbox_exec_error(s) + + def _print_sandbox_called_error(self, s): + assert self.sandbox_called_error is not None + + s.write("A moz.build file called the error() function.\n") + s.write("\n") + s.write("The error it encountered is:\n") + s.write("\n") + s.write(" %s\n" % self.sandbox_called_error.message) + s.write("\n") + s.write("Correct the error condition and try again.\n") + + def _print_sandbox_load_error(self, s): + assert self.sandbox_load is not None + + if self.sandbox_load.illegal_path is not None: + s.write("The underlying problem is an illegal file access. ") + s.write("This is likely due to trying to access a file ") + s.write("outside of the top source directory.\n") + s.write("\n") + s.write("The path whose access was denied is:\n") + s.write("\n") + s.write(" %s\n" % self.sandbox_load.illegal_path) + s.write("\n") + s.write("Modify the script to not access this file and ") + s.write("try again.\n") + return + + if self.sandbox_load.read_error is not None: + if not os.path.exists(self.sandbox_load.read_error): + s.write("The underlying problem is we referenced a path ") + s.write("that does not exist. That path is:\n") + s.write("\n") + s.write(" %s\n" % self.sandbox_load.read_error) + s.write("\n") + s.write("Either create the file if it needs to exist or ") + s.write("do not reference it.\n") + else: + s.write("The underlying problem is a referenced path could ") + s.write("not be read. The trouble path is:\n") + s.write("\n") + s.write(" %s\n" % self.sandbox_load.read_error) + s.write("\n") + s.write("It is possible the path is not correct. Is it ") + s.write("pointing to a directory? It could also be a file ") + s.write("permissions issue. Ensure that the file is ") + s.write("readable.\n") + + return + + # This module is buggy if you see this. + raise AssertionError("SandboxLoadError with unhandled properties!") + + def _print_sandbox_exec_error(self, s): + assert self.sandbox_exec is not None + + inner = self.sandbox_exec.exc_value + + if isinstance(inner, SyntaxError): + s.write("The underlying problem is a Python syntax error ") + s.write("on line %d:\n" % inner.lineno) + s.write("\n") + s.write(" %s\n" % inner.text) + if inner.offset: + s.write((" " * (inner.offset + 4)) + "^\n") + s.write("\n") + s.write("Fix the syntax error and try again.\n") + return + + if isinstance(inner, KeyError): + self._print_keyerror(inner, s) + elif isinstance(inner, ValueError): + self._print_valueerror(inner, s) + else: + self._print_exception(inner, s) + + def _print_keyerror(self, inner, s): + if not inner.args or inner.args[0] not in ("global_ns", "local_ns"): + self._print_exception(inner, s) + return + + if inner.args[0] == "global_ns": + import difflib + + verb = None + if inner.args[1] == "get_unknown": + verb = "read" + elif inner.args[1] == "set_unknown": + verb = "write" + elif inner.args[1] == "reassign": + s.write("The underlying problem is an attempt to reassign ") + s.write("a reserved UPPERCASE variable.\n") + s.write("\n") + s.write("The reassigned variable causing the error is:\n") + s.write("\n") + s.write(" %s\n" % inner.args[2]) + s.write("\n") + s.write('Maybe you meant "+=" instead of "="?\n') + return + else: + raise AssertionError("Unhandled global_ns: %s" % inner.args[1]) + + s.write("The underlying problem is an attempt to %s " % verb) + s.write("a reserved UPPERCASE variable that does not exist.\n") + s.write("\n") + s.write("The variable %s causing the error is:\n" % verb) + s.write("\n") + s.write(" %s\n" % inner.args[2]) + s.write("\n") + close_matches = difflib.get_close_matches( + inner.args[2], VARIABLES.keys(), 2 + ) + if close_matches: + s.write("Maybe you meant %s?\n" % " or ".join(close_matches)) + s.write("\n") + + if inner.args[2] in DEPRECATION_HINTS: + s.write( + "%s\n" % textwrap.dedent(DEPRECATION_HINTS[inner.args[2]]).strip() + ) + return + + s.write("Please change the file to not use this variable.\n") + s.write("\n") + s.write("For reference, the set of valid variables is:\n") + s.write("\n") + s.write(", ".join(sorted(VARIABLES.keys())) + "\n") + return + + s.write("The underlying problem is a reference to an undefined ") + s.write("local variable:\n") + s.write("\n") + s.write(" %s\n" % inner.args[2]) + s.write("\n") + s.write("Please change the file to not reference undefined ") + s.write("variables and try again.\n") + + def _print_valueerror(self, inner, s): + if not inner.args or inner.args[0] not in ("global_ns", "local_ns"): + self._print_exception(inner, s) + return + + assert inner.args[1] == "set_type" + + s.write("The underlying problem is an attempt to write an illegal ") + s.write("value to a special variable.\n") + s.write("\n") + s.write("The variable whose value was rejected is:\n") + s.write("\n") + s.write(" %s" % inner.args[2]) + s.write("\n") + s.write("The value being written to it was of the following type:\n") + s.write("\n") + s.write(" %s\n" % type(inner.args[3]).__name__) + s.write("\n") + s.write("This variable expects the following type(s):\n") + s.write("\n") + if type(inner.args[4]) == type: + s.write(" %s\n" % inner.args[4].__name__) + else: + for t in inner.args[4]: + s.write(" %s\n" % t.__name__) + s.write("\n") + s.write("Change the file to write a value of the appropriate type ") + s.write("and try again.\n") + + def _print_exception(self, e, s): + s.write("An error was encountered as part of executing the file ") + s.write("itself. The error appears to be the fault of the script.\n") + s.write("\n") + s.write("The error as reported by Python is:\n") + s.write("\n") + s.write(" %s\n" % traceback.format_exception_only(type(e), e)) + + +class BuildReader(object): + """Read a tree of mozbuild files into data structures. + + This is where the build system starts. You give it a tree configuration + (the output of configuration) and it executes the moz.build files and + collects the data they define. + + The reader can optionally call a callable after each sandbox is evaluated + but before its evaluated content is processed. This gives callers the + opportunity to modify contexts before side-effects occur from their + content. This callback receives the ``Context`` containing the result of + each sandbox evaluation. Its return value is ignored. + """ + + def __init__(self, config, finder=default_finder): + self.config = config + + self._log = logging.getLogger(__name__) + self._read_files = set() + self._execution_stack = [] + self.finder = finder + + # Finder patterns to ignore when searching for moz.build files. + ignores = { + # Ignore fake moz.build files used for testing moz.build. + "python/mozbuild/mozbuild/test", + "testing/mozbase/moztest/tests/data", + # Ignore object directories. + "obj*", + } + + # Also ignore any other directories that could be objdirs, but don't + # necessarily start with the string 'obj'. + objdir_finder = FileFinder(self.config.topsrcdir, ignore=ignores) + for path, _file in objdir_finder.find("*/config.status"): + ignores.add(os.path.dirname(path)) + del objdir_finder + + self._relevant_mozbuild_finder = FileFinder( + self.config.topsrcdir, ignore=ignores + ) + + max_workers = cpu_count() + if sys.platform.startswith("win"): + # In python 3, on Windows, ProcessPoolExecutor uses + # _winapi.WaitForMultipleObjects, which doesn't work on large + # number of objects. It also has some automatic capping to avoid + # _winapi.WaitForMultipleObjects being unhappy as a consequence, + # but that capping is actually insufficient in python 3.7 and 3.8 + # (as well as inexistent in older versions). So we cap ourselves + # to 60, see https://bugs.python.org/issue26903#msg365886. + max_workers = min(max_workers, 60) + self._gyp_worker_pool = ProcessPoolExecutor(max_workers=max_workers) + self._gyp_processors = [] + self._execution_time = 0.0 + self._file_count = 0 + self._gyp_execution_time = 0.0 + self._gyp_file_count = 0 + + def summary(self): + return ExecutionSummary( + "Finished reading {file_count:d} moz.build files in " + "{execution_time:.2f}s", + file_count=self._file_count, + execution_time=self._execution_time, + ) + + def gyp_summary(self): + return ExecutionSummary( + "Read {file_count:d} gyp files in parallel contributing " + "{execution_time:.2f}s to total wall time", + file_count=self._gyp_file_count, + execution_time=self._gyp_execution_time, + ) + + def read_topsrcdir(self): + """Read the tree of linked moz.build files. + + This starts with the tree's top-most moz.build file and descends into + all linked moz.build files until all relevant files have been evaluated. + + This is a generator of Context instances. As each moz.build file is + read, a new Context is created and emitted. + """ + path = mozpath.join(self.config.topsrcdir, "moz.build") + for r in self.read_mozbuild(path, self.config): + yield r + all_gyp_paths = set() + for g in self._gyp_processors: + for gyp_context in g.results: + all_gyp_paths |= gyp_context.all_paths + yield gyp_context + self._gyp_execution_time += g.execution_time + self._gyp_file_count += len(all_gyp_paths) + self._gyp_worker_pool.shutdown() + + def all_mozbuild_paths(self): + """Iterator over all available moz.build files. + + This method has little to do with the reader. It should arguably belong + elsewhere. + """ + # In the future, we may traverse moz.build files by looking + # for DIRS references in the AST, even if a directory is added behind + # a conditional. For now, just walk the filesystem. + for path, f in self._relevant_mozbuild_finder.find("**/moz.build"): + yield path + + def find_variables_from_ast(self, variables, path=None): + """Finds all assignments to the specified variables by parsing + moz.build abstract syntax trees. + + This function only supports two cases, as detailed below. + + 1) A dict. Keys and values should both be strings, e.g: + + VARIABLE['foo'] = 'bar' + + This is an `Assign` node with a `Subscript` target. The `Subscript`'s + value is a `Name` node with id "VARIABLE". The slice of this target is + an `Index` node and its value is a `Str` with value "foo". + + 2) A simple list. Values should be strings, e.g: The target of the + assignment should be a Name node. Values should be a List node, + whose elements are Str nodes. e.g: + + VARIABLE += ['foo'] + + This is an `AugAssign` node with a `Name` target with id "VARIABLE". + The value is a `List` node containing one `Str` element whose value is + "foo". + + With a little work, this function could support other types of + assignment. But if we end up writing a lot of AST code, it might be + best to import a high-level AST manipulation library into the tree. + + Args: + variables (list): A list of variable assignments to capture. + path (str): A path relative to the source dir. If specified, only + `moz.build` files relevant to this path will be parsed. Otherwise + all `moz.build` files are parsed. + + Returns: + A generator that generates tuples of the form `(<moz.build path>, + <variable name>, <key>, <value>)`. The `key` will only be + defined if the variable is an object, otherwise it is `None`. + """ + + if isinstance(variables, str): + variables = [variables] + + def assigned_variable(node): + # This is not correct, but we don't care yet. + if hasattr(node, "targets"): + # Nothing in moz.build does multi-assignment (yet). So error if + # we see it. + assert len(node.targets) == 1 + + target = node.targets[0] + else: + target = node.target + + if isinstance(target, ast.Subscript): + if not isinstance(target.value, ast.Name): + return None, None + name = target.value.id + elif isinstance(target, ast.Name): + name = target.id + else: + return None, None + + if name not in variables: + return None, None + + key = None + if isinstance(target, ast.Subscript): + # We need to branch to deal with python version differences. + if isinstance(target.slice, ast.Constant): + # Python >= 3.9 + assert isinstance(target.slice.value, str) + key = target.slice.value + else: + # Others + assert isinstance(target.slice, ast.Index) + assert isinstance(target.slice.value, ast.Str) + key = target.slice.value.s + + return name, key + + def assigned_values(node): + value = node.value + if isinstance(value, ast.List): + for v in value.elts: + assert isinstance(v, ast.Str) + yield v.s + else: + assert isinstance(value, ast.Str) + yield value.s + + assignments = [] + + class Visitor(ast.NodeVisitor): + def helper(self, node): + name, key = assigned_variable(node) + if not name: + return + + for v in assigned_values(node): + assignments.append((name, key, v)) + + def visit_Assign(self, node): + self.helper(node) + + def visit_AugAssign(self, node): + self.helper(node) + + if path: + mozbuild_paths = chain(*self._find_relevant_mozbuilds([path]).values()) + else: + mozbuild_paths = self.all_mozbuild_paths() + + for p in mozbuild_paths: + assignments[:] = [] + full = os.path.join(self.config.topsrcdir, p) + + with open(full, "rb") as fh: + source = fh.read() + + tree = ast.parse(source, full) + Visitor().visit(tree) + + for name, key, value in assignments: + yield p, name, key, value + + def read_mozbuild(self, path, config, descend=True, metadata={}): + """Read and process a mozbuild file, descending into children. + + This starts with a single mozbuild file, executes it, and descends into + other referenced files per our traversal logic. + + The traversal logic is to iterate over the ``*DIRS`` variables, treating + each element as a relative directory path. For each encountered + directory, we will open the moz.build file located in that + directory in a new Sandbox and process it. + + If descend is True (the default), we will descend into child + directories and files per variable values. + + Arbitrary metadata in the form of a dict can be passed into this + function. This feature is intended to facilitate the build reader + injecting state and annotations into moz.build files that is + independent of the sandbox's execution context. + + Traversal is performed depth first (for no particular reason). + """ + self._execution_stack.append(path) + try: + for s in self._read_mozbuild( + path, config, descend=descend, metadata=metadata + ): + yield s + + except BuildReaderError as bre: + raise bre + + except SandboxCalledError as sce: + raise BuildReaderError( + list(self._execution_stack), sys.exc_info()[2], sandbox_called_error=sce + ) + + except SandboxExecutionError as se: + raise BuildReaderError( + list(self._execution_stack), sys.exc_info()[2], sandbox_exec_error=se + ) + + except SandboxLoadError as sle: + raise BuildReaderError( + list(self._execution_stack), sys.exc_info()[2], sandbox_load_error=sle + ) + + except SandboxValidationError as ve: + raise BuildReaderError( + list(self._execution_stack), sys.exc_info()[2], validation_error=ve + ) + + except Exception as e: + raise BuildReaderError( + list(self._execution_stack), sys.exc_info()[2], other_error=e + ) + + def _read_mozbuild(self, path, config, descend, metadata): + path = mozpath.normpath(path) + log( + self._log, + logging.DEBUG, + "read_mozbuild", + {"path": path}, + "Reading file: {path}".format(path=path), + ) + + if path in self._read_files: + log( + self._log, + logging.WARNING, + "read_already", + {"path": path}, + "File already read. Skipping: {path}".format(path=path), + ) + return + + self._read_files.add(path) + + time_start = time.monotonic() + + topobjdir = config.topobjdir + + relpath = mozpath.relpath(path, config.topsrcdir) + reldir = mozpath.dirname(relpath) + + if mozpath.dirname(relpath) == "js/src" and not config.substs.get( + "JS_STANDALONE" + ): + config = ConfigEnvironment.from_config_status( + mozpath.join(topobjdir, reldir, "config.status") + ) + config.topobjdir = topobjdir + + context = Context(VARIABLES, config, self.finder) + sandbox = MozbuildSandbox(context, metadata=metadata, finder=self.finder) + sandbox.exec_file(path) + self._execution_time += time.monotonic() - time_start + self._file_count += len(context.all_paths) + + # Yield main context before doing any processing. This gives immediate + # consumers an opportunity to change state before our remaining + # processing is performed. + yield context + + # We need the list of directories pre-gyp processing for later. + dirs = list(context.get("DIRS", [])) + + curdir = mozpath.dirname(path) + + for target_dir in context.get("GYP_DIRS", []): + gyp_dir = context["GYP_DIRS"][target_dir] + for v in ("input", "variables"): + if not getattr(gyp_dir, v): + raise SandboxValidationError( + "Missing value for " 'GYP_DIRS["%s"].%s' % (target_dir, v), + context, + ) + + # The make backend assumes contexts for sub-directories are + # emitted after their parent, so accumulate the gyp contexts. + # We could emit the parent context before processing gyp + # configuration, but we need to add the gyp objdirs to that context + # first. + from .gyp_reader import GypProcessor + + non_unified_sources = set() + for s in gyp_dir.non_unified_sources: + source = SourcePath(context, s) + if not self.finder.get(source.full_path): + raise SandboxValidationError("Cannot find %s." % source, context) + non_unified_sources.add(source) + action_overrides = {} + for action, script in gyp_dir.action_overrides.items(): + action_overrides[action] = SourcePath(context, script) + + gyp_processor = GypProcessor( + context.config, + gyp_dir, + mozpath.join(curdir, gyp_dir.input), + mozpath.join(context.objdir, target_dir), + self._gyp_worker_pool, + action_overrides, + non_unified_sources, + ) + self._gyp_processors.append(gyp_processor) + + for subcontext in sandbox.subcontexts: + yield subcontext + + # Traverse into referenced files. + + # It's very tempting to use a set here. Unfortunately, the recursive + # make backend needs order preserved. Once we autogenerate all backend + # files, we should be able to convert this to a set. + recurse_info = OrderedDict() + for d in dirs: + if d in recurse_info: + raise SandboxValidationError( + "Directory (%s) registered multiple times" + % (mozpath.relpath(d.full_path, context.srcdir)), + context, + ) + + recurse_info[d] = {} + for key in sandbox.metadata: + if key == "exports": + sandbox.recompute_exports() + + recurse_info[d][key] = dict(sandbox.metadata[key]) + + for path, child_metadata in recurse_info.items(): + child_path = path.join("moz.build").full_path + + # Ensure we don't break out of the topsrcdir. We don't do realpath + # because it isn't necessary. If there are symlinks in the srcdir, + # that's not our problem. We're not a hosted application: we don't + # need to worry about security too much. + if not is_read_allowed(child_path, context.config): + raise SandboxValidationError( + "Attempting to process file outside of allowed paths: %s" + % child_path, + context, + ) + + if not descend: + continue + + for res in self.read_mozbuild( + child_path, context.config, metadata=child_metadata + ): + yield res + + self._execution_stack.pop() + + def _find_relevant_mozbuilds(self, paths): + """Given a set of filesystem paths, find all relevant moz.build files. + + We assume that a moz.build file in the directory ancestry of a given path + is relevant to that path. Let's say we have the following files on disk:: + + moz.build + foo/moz.build + foo/baz/moz.build + foo/baz/file1 + other/moz.build + other/file2 + + If ``foo/baz/file1`` is passed in, the relevant moz.build files are + ``moz.build``, ``foo/moz.build``, and ``foo/baz/moz.build``. For + ``other/file2``, the relevant moz.build files are ``moz.build`` and + ``other/moz.build``. + + Returns a dict of input paths to a list of relevant moz.build files. + The root moz.build file is first and the leaf-most moz.build is last. + """ + root = self.config.topsrcdir + result = {} + + @memoize + def exists(path): + return self._relevant_mozbuild_finder.get(path) is not None + + def itermozbuild(path): + subpath = "" + yield "moz.build" + for part in mozpath.split(path): + subpath = mozpath.join(subpath, part) + yield mozpath.join(subpath, "moz.build") + + for path in sorted(paths): + path = mozpath.normpath(path) + if os.path.isabs(path): + if not mozpath.basedir(path, [root]): + raise Exception("Path outside topsrcdir: %s" % path) + path = mozpath.relpath(path, root) + + result[path] = [p for p in itermozbuild(path) if exists(p)] + + return result + + def read_relevant_mozbuilds(self, paths): + """Read and process moz.build files relevant for a set of paths. + + For an iterable of relative-to-root filesystem paths ``paths``, + find all moz.build files that may apply to them based on filesystem + hierarchy and read those moz.build files. + + The return value is a 2-tuple. The first item is a dict mapping each + input filesystem path to a list of Context instances that are relevant + to that path. The second item is a list of all Context instances. Each + Context instance is in both data structures. + """ + relevants = self._find_relevant_mozbuilds(paths) + + topsrcdir = self.config.topsrcdir + + # Source moz.build file to directories to traverse. + dirs = defaultdict(set) + # Relevant path to absolute paths of relevant contexts. + path_mozbuilds = {} + + # There is room to improve this code (and the code in + # _find_relevant_mozbuilds) to better handle multiple files in the same + # directory. Bug 1136966 tracks. + for path, mbpaths in relevants.items(): + path_mozbuilds[path] = [mozpath.join(topsrcdir, p) for p in mbpaths] + + for i, mbpath in enumerate(mbpaths[0:-1]): + source_dir = mozpath.dirname(mbpath) + target_dir = mozpath.dirname(mbpaths[i + 1]) + + d = mozpath.normpath(mozpath.join(topsrcdir, mbpath)) + dirs[d].add(mozpath.relpath(target_dir, source_dir)) + + # Exporting doesn't work reliably in tree traversal mode. Override + # the function to no-op. + functions = dict(FUNCTIONS) + + def export(sandbox): + return lambda varname: None + + functions["export"] = tuple([export] + list(FUNCTIONS["export"][1:])) + + metadata = { + "functions": functions, + } + + contexts = defaultdict(list) + all_contexts = [] + for context in self.read_mozbuild( + mozpath.join(topsrcdir, "moz.build"), self.config, metadata=metadata + ): + # Explicitly set directory traversal variables to override default + # traversal rules. + if not isinstance(context, SubContext): + for v in ("DIRS", "GYP_DIRS"): + context[v][:] = [] + + context["DIRS"] = sorted(dirs[context.main_path]) + + contexts[context.main_path].append(context) + all_contexts.append(context) + + result = {} + for path, paths in path_mozbuilds.items(): + result[path] = functools.reduce( + lambda x, y: x + y, (contexts[p] for p in paths), [] + ) + + return result, all_contexts + + def files_info(self, paths): + """Obtain aggregate data from Files for a set of files. + + Given a set of input paths, determine which moz.build files may + define metadata for them, evaluate those moz.build files, and + apply file metadata rules defined within to determine metadata + values for each file requested. + + Essentially, for each input path: + + 1. Determine the set of moz.build files relevant to that file by + looking for moz.build files in ancestor directories. + 2. Evaluate moz.build files starting with the most distant. + 3. Iterate over Files sub-contexts. + 4. If the file pattern matches the file we're seeking info on, + apply attribute updates. + 5. Return the most recent value of attributes. + """ + paths, _ = self.read_relevant_mozbuilds(paths) + + r = {} + + # Only do wildcard matching if the '*' character is present. + # Otherwise, mozpath.match will match directories, which we've + # arbitrarily chosen to not allow. + def path_matches_pattern(relpath, pattern): + if pattern == relpath: + return True + + return "*" in pattern and mozpath.match(relpath, pattern) + + for path, ctxs in paths.items(): + # Should be normalized by read_relevant_mozbuilds. + assert "\\" not in path + + flags = Files(Context()) + + for ctx in ctxs: + if not isinstance(ctx, Files): + continue + + # read_relevant_mozbuilds() normalizes paths and ensures that + # the contexts have paths in the ancestry of the path. When + # iterating over tens of thousands of paths, mozpath.relpath() + # can be very expensive. So, given our assumptions about paths, + # we implement an optimized version. + ctx_rel_dir = ctx.relsrcdir + if ctx_rel_dir: + assert path.startswith(ctx_rel_dir) + relpath = path[len(ctx_rel_dir) + 1 :] + else: + relpath = path + + if any(path_matches_pattern(relpath, p) for p in ctx.patterns): + flags += ctx + + r[path] = flags + + return r diff --git a/python/mozbuild/mozbuild/frontend/sandbox.py b/python/mozbuild/mozbuild/frontend/sandbox.py new file mode 100644 index 0000000000..dfead3ce54 --- /dev/null +++ b/python/mozbuild/mozbuild/frontend/sandbox.py @@ -0,0 +1,313 @@ +# 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/. + +r"""Python sandbox implementation for build files. + +This module contains classes for Python sandboxes that execute in a +highly-controlled environment. + +The main class is `Sandbox`. This provides an execution environment for Python +code and is used to fill a Context instance for the takeaway information from +the execution. + +Code in this module takes a different approach to exception handling compared +to what you'd see elsewhere in Python. Arguments to built-in exceptions like +KeyError are machine parseable. This machine-friendly data is used to present +user-friendly error messages in the case of errors. +""" + +import os +import sys +import weakref + +import six +from mozpack.files import FileFinder + +from mozbuild.util import ReadOnlyDict + +from .context import Context + +default_finder = FileFinder("/") + + +def alphabetical_sorted(iterable, key=lambda x: x.lower(), reverse=False): + """sorted() replacement for the sandbox, ordering alphabetically by + default. + """ + return sorted(iterable, key=key, reverse=reverse) + + +class SandboxError(Exception): + def __init__(self, file_stack): + self.file_stack = file_stack + + +class SandboxExecutionError(SandboxError): + """Represents errors encountered during execution of a Sandbox. + + This is a simple container exception. It's purpose is to capture state + so something else can report on it. + """ + + def __init__(self, file_stack, exc_type, exc_value, trace): + SandboxError.__init__(self, file_stack) + + self.exc_type = exc_type + self.exc_value = exc_value + self.trace = trace + + +class SandboxLoadError(SandboxError): + """Represents errors encountered when loading a file for execution. + + This exception represents errors in a Sandbox that occurred as part of + loading a file. The error could have occurred in the course of executing + a file. If so, the file_stack will be non-empty and the file that caused + the load will be on top of the stack. + """ + + def __init__(self, file_stack, trace, illegal_path=None, read_error=None): + SandboxError.__init__(self, file_stack) + + self.trace = trace + self.illegal_path = illegal_path + self.read_error = read_error + + +class Sandbox(dict): + """Represents a sandbox for executing Python code. + + This class provides a sandbox for execution of a single mozbuild frontend + file. The results of that execution is stored in the Context instance given + as the ``context`` argument. + + Sandbox is effectively a glorified wrapper around compile() + exec(). You + point it at some Python code and it executes it. The main difference from + executing Python code like normal is that the executed code is very limited + in what it can do: the sandbox only exposes a very limited set of Python + functionality. Only specific types and functions are available. This + prevents executed code from doing things like import modules, open files, + etc. + + Sandbox instances act as global namespace for the sandboxed execution + itself. They shall not be used to access the results of the execution. + Those results are available in the given Context instance after execution. + + The Sandbox itself is responsible for enforcing rules such as forbidding + reassignment of variables. + + Implementation note: Sandbox derives from dict because exec() insists that + what it is given for namespaces is a dict. + """ + + # The default set of builtins. + BUILTINS = ReadOnlyDict( + { + # Only real Python built-ins should go here. + "None": None, + "False": False, + "True": True, + "sorted": alphabetical_sorted, + "int": int, + "set": set, + "tuple": tuple, + } + ) + + def __init__(self, context, finder=default_finder): + """Initialize a Sandbox ready for execution.""" + self._builtins = self.BUILTINS + dict.__setitem__(self, "__builtins__", self._builtins) + + assert isinstance(self._builtins, ReadOnlyDict) + assert isinstance(context, Context) + + # Contexts are modeled as a stack because multiple context managers + # may be active. + self._active_contexts = [context] + + # Seen sub-contexts. Will be populated with other Context instances + # that were related to execution of this instance. + self.subcontexts = [] + + # We need to record this because it gets swallowed as part of + # evaluation. + self._last_name_error = None + + # Current literal source being executed. + self._current_source = None + + self._finder = finder + + @property + def _context(self): + return self._active_contexts[-1] + + def exec_file(self, path): + """Execute code at a path in the sandbox. + + The path must be absolute. + """ + assert os.path.isabs(path) + + try: + source = six.ensure_text(self._finder.get(path).read()) + except Exception: + raise SandboxLoadError( + self._context.source_stack, sys.exc_info()[2], read_error=path + ) + + self.exec_source(source, path) + + def exec_source(self, source, path=""): + """Execute Python code within a string. + + The passed string should contain Python code to be executed. The string + will be compiled and executed. + + You should almost always go through exec_file() because exec_source() + does not perform extra path normalization. This can cause relative + paths to behave weirdly. + """ + + def execute(): + # compile() inherits the __future__ from the module by default. We + # do want Unicode literals. + code = compile(source, path, "exec") + # We use ourself as the global namespace for the execution. There + # is no need for a separate local namespace as moz.build execution + # is flat, namespace-wise. + old_source = self._current_source + self._current_source = source + try: + exec(code, self) + finally: + self._current_source = old_source + + self.exec_function(execute, path=path) + + def exec_function( + self, func, args=(), kwargs={}, path="", becomes_current_path=True + ): + """Execute function with the given arguments in the sandbox.""" + if path and becomes_current_path: + self._context.push_source(path) + + old_sandbox = self._context._sandbox + self._context._sandbox = weakref.ref(self) + + # We don't have to worry about bytecode generation here because we are + # too low-level for that. However, we could add bytecode generation via + # the marshall module if parsing performance were ever an issue. + + old_source = self._current_source + self._current_source = None + try: + func(*args, **kwargs) + except SandboxError as e: + raise e + except NameError as e: + # A NameError is raised when a variable could not be found. + # The original KeyError has been dropped by the interpreter. + # However, we should have it cached in our instance! + + # Unless a script is doing something wonky like catching NameError + # itself (that would be silly), if there is an exception on the + # global namespace, that's our error. + actual = e + + if self._last_name_error is not None: + actual = self._last_name_error + source_stack = self._context.source_stack + if not becomes_current_path: + # Add current file to the stack because it wasn't added before + # sandbox execution. + source_stack.append(path) + raise SandboxExecutionError( + source_stack, type(actual), actual, sys.exc_info()[2] + ) + + except Exception: + # Need to copy the stack otherwise we get a reference and that is + # mutated during the finally. + exc = sys.exc_info() + source_stack = self._context.source_stack + if not becomes_current_path: + # Add current file to the stack because it wasn't added before + # sandbox execution. + source_stack.append(path) + raise SandboxExecutionError(source_stack, exc[0], exc[1], exc[2]) + finally: + self._current_source = old_source + self._context._sandbox = old_sandbox + if path and becomes_current_path: + self._context.pop_source() + + def push_subcontext(self, context): + """Push a SubContext onto the execution stack. + + When called, the active context will be set to the specified context, + meaning all variable accesses will go through it. We also record this + SubContext as having been executed as part of this sandbox. + """ + self._active_contexts.append(context) + if context not in self.subcontexts: + self.subcontexts.append(context) + + def pop_subcontext(self, context): + """Pop a SubContext off the execution stack. + + SubContexts must be pushed and popped in opposite order. This is + validated as part of the function call to ensure proper consumer API + use. + """ + popped = self._active_contexts.pop() + assert popped == context + + def __getitem__(self, key): + if key.isupper(): + try: + return self._context[key] + except Exception as e: + self._last_name_error = e + raise + + return dict.__getitem__(self, key) + + def __setitem__(self, key, value): + if key in self._builtins or key == "__builtins__": + raise KeyError("Cannot reassign builtins") + + if key.isupper(): + # Forbid assigning over a previously set value. Interestingly, when + # doing FOO += ['bar'], python actually does something like: + # foo = namespace.__getitem__('FOO') + # foo.__iadd__(['bar']) + # namespace.__setitem__('FOO', foo) + # This means __setitem__ is called with the value that is already + # in the dict, when doing +=, which is permitted. + if key in self._context and self._context[key] is not value: + raise KeyError("global_ns", "reassign", key) + + if ( + key not in self._context + and isinstance(value, (list, dict)) + and not value + ): + raise KeyError("Variable %s assigned an empty value." % key) + + self._context[key] = value + else: + dict.__setitem__(self, key, value) + + def get(self, key, default=None): + raise NotImplementedError("Not supported") + + def __iter__(self): + raise NotImplementedError("Not supported") + + def __contains__(self, key): + if key.isupper(): + return key in self._context + return dict.__contains__(self, key) diff --git a/python/mozbuild/mozbuild/gen_test_backend.py b/python/mozbuild/mozbuild/gen_test_backend.py new file mode 100644 index 0000000000..ce499fe90a --- /dev/null +++ b/python/mozbuild/mozbuild/gen_test_backend.py @@ -0,0 +1,53 @@ +# 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/. + +import os +import sys + +import mozpack.path as mozpath + +from mozbuild.backend.test_manifest import TestManifestBackend +from mozbuild.base import BuildEnvironmentNotFoundException, MozbuildObject +from mozbuild.frontend.emitter import TreeMetadataEmitter +from mozbuild.frontend.reader import BuildReader, EmptyConfig + + +def gen_test_backend(): + build_obj = MozbuildObject.from_environment() + try: + config = build_obj.config_environment + except BuildEnvironmentNotFoundException: + # Create a stub config.status file, since the TestManifest backend needs + # to be re-created if configure runs. If the file doesn't exist, + # mozbuild continually thinks the TestManifest backend is out of date + # and tries to regenerate it. + + if not os.path.isdir(build_obj.topobjdir): + os.makedirs(build_obj.topobjdir) + + config_status = mozpath.join(build_obj.topobjdir, "config.status") + open(config_status, "w").close() + + print("No build detected, test metadata may be incomplete.") + + # If 'JS_STANDALONE' is set, tests that don't require an objdir won't + # be picked up due to bug 1345209. + substs = EmptyConfig.default_substs + if "JS_STANDALONE" in substs: + del substs["JS_STANDALONE"] + + config = EmptyConfig(build_obj.topsrcdir, substs) + config.topobjdir = build_obj.topobjdir + + reader = BuildReader(config) + emitter = TreeMetadataEmitter(config) + backend = TestManifestBackend(config) + + context = reader.read_topsrcdir() + data = emitter.emit(context, emitfn=emitter._process_test_manifests) + backend.consume(data) + + +if __name__ == "__main__": + sys.exit(gen_test_backend()) diff --git a/python/mozbuild/mozbuild/generated_sources.py b/python/mozbuild/mozbuild/generated_sources.py new file mode 100644 index 0000000000..e22e71e5f6 --- /dev/null +++ b/python/mozbuild/mozbuild/generated_sources.py @@ -0,0 +1,75 @@ +# 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/. + +import hashlib +import json +import os + +import mozpack.path as mozpath +from mozpack.files import FileFinder + +GENERATED_SOURCE_EXTS = (".rs", ".c", ".h", ".cc", ".cpp") + + +def sha512_digest(data): + """ + Generate the SHA-512 digest of `data` and return it as a hex string. + """ + return hashlib.sha512(data).hexdigest() + + +def get_filename_with_digest(name, contents): + """ + Return the filename that will be used to store the generated file + in the S3 bucket, consisting of the SHA-512 digest of `contents` + joined with the relative path `name`. + """ + digest = sha512_digest(contents) + return mozpath.join(digest, name) + + +def get_generated_sources(): + """ + Yield tuples of `(objdir-rel-path, file)` for generated source files + in this objdir, where `file` is either an absolute path to the file or + a `mozpack.File` instance. + """ + import buildconfig + + # First, get the list of generated sources produced by the build backend. + gen_sources = os.path.join(buildconfig.topobjdir, "generated-sources.json") + with open(gen_sources, "r") as f: + data = json.load(f) + for f in data["sources"]: + # Exclute symverscript + if mozpath.basename(f) != "symverscript": + yield f, mozpath.join(buildconfig.topobjdir, f) + # Next, return all the files in $objdir/ipc/ipdl/_ipdlheaders. + base = "ipc/ipdl/_ipdlheaders" + finder = FileFinder(mozpath.join(buildconfig.topobjdir, base)) + for p, f in finder.find("**/*.h"): + yield mozpath.join(base, p), f + # Next, return any source files that were generated into the Rust + # object directory. + rust_build_kind = "debug" if buildconfig.substs.get("MOZ_DEBUG_RUST") else "release" + base = mozpath.join(buildconfig.substs["RUST_TARGET"], rust_build_kind, "build") + finder = FileFinder(mozpath.join(buildconfig.topobjdir, base)) + for p, f in finder: + if p.endswith(GENERATED_SOURCE_EXTS): + yield mozpath.join(base, p), f + + +def get_s3_region_and_bucket(): + """ + Return a tuple of (region, bucket) giving the AWS region and S3 + bucket to which generated sources should be uploaded. + """ + region = "us-west-2" + level = os.environ.get("MOZ_SCM_LEVEL", "1") + bucket = { + "1": "gecko-generated-sources-l1", + "2": "gecko-generated-sources-l2", + "3": "gecko-generated-sources", + }[level] + return (region, bucket) diff --git a/python/mozbuild/mozbuild/gn_processor.py b/python/mozbuild/mozbuild/gn_processor.py new file mode 100644 index 0000000000..a77b6c7759 --- /dev/null +++ b/python/mozbuild/mozbuild/gn_processor.py @@ -0,0 +1,797 @@ +# 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/. + +import argparse +import json +import os +import subprocess +import sys +import tempfile +from collections import defaultdict, deque +from copy import deepcopy +from pathlib import Path +from shutil import which + +import mozpack.path as mozpath +import six + +from mozbuild.bootstrap import bootstrap_toolchain +from mozbuild.frontend.sandbox import alphabetical_sorted +from mozbuild.util import mkdir + +license_header = """# 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/. +""" + +generated_header = """ + ### This moz.build was AUTOMATICALLY GENERATED from a GN config, ### + ### DO NOT edit it by hand. ### +""" + + +class MozbuildWriter(object): + def __init__(self, fh): + self._fh = fh + self.indent = "" + self._indent_increment = 4 + + # We need to correlate a small amount of state here to figure out + # which library template to use ("Library()" or "SharedLibrary()") + self._library_name = None + self._shared_library = None + + def mb_serialize(self, v): + if isinstance(v, list): + if len(v) <= 1: + return repr(v) + # Pretty print a list + raw = json.dumps(v, indent=self._indent_increment) + # Add the indent of the current indentation level + return raw.replace("\n", "\n" + self.indent) + if isinstance(v, bool): + return repr(v) + return '"%s"' % v + + def finalize(self): + if self._library_name: + self.write("\n") + if self._shared_library: + self.write_ln( + "SharedLibrary(%s)" % self.mb_serialize(self._library_name) + ) + else: + self.write_ln("Library(%s)" % self.mb_serialize(self._library_name)) + + def write(self, content): + self._fh.write(content) + + def write_ln(self, line): + self.write(self.indent) + self.write(line) + self.write("\n") + + def write_attrs(self, context_attrs): + for k in sorted(context_attrs.keys()): + v = context_attrs[k] + if isinstance(v, (list, set)): + self.write_mozbuild_list(k, v) + elif isinstance(v, dict): + self.write_mozbuild_dict(k, v) + else: + self.write_mozbuild_value(k, v) + + def write_mozbuild_list(self, key, value): + if value: + self.write("\n") + self.write(self.indent + key) + self.write(" += [\n " + self.indent) + self.write( + (",\n " + self.indent).join( + alphabetical_sorted(self.mb_serialize(v) for v in value) + ) + ) + self.write("\n") + self.write_ln("]") + + def write_mozbuild_value(self, key, value): + if value: + if key == "LIBRARY_NAME": + self._library_name = value + elif key == "FORCE_SHARED_LIB": + self._shared_library = True + else: + self.write("\n") + self.write_ln("%s = %s" % (key, self.mb_serialize(value))) + self.write("\n") + + def write_mozbuild_dict(self, key, value): + # Templates we need to use instead of certain values. + replacements = ( + ( + ("COMPILE_FLAGS", '"WARNINGS_AS_ERRORS"', "[]"), + "AllowCompilerWarnings()", + ), + ) + if value: + self.write("\n") + if key == "GeneratedFile": + self.write_ln("GeneratedFile(") + self.indent += " " * self._indent_increment + for o in value["outputs"]: + self.write_ln("%s," % (self.mb_serialize(o))) + for k, v in sorted(value.items()): + if k == "outputs": + continue + self.write_ln("%s=%s," % (k, self.mb_serialize(v))) + self.indent = self.indent[self._indent_increment :] + self.write_ln(")") + return + for k in sorted(value.keys()): + v = value[k] + subst_vals = key, self.mb_serialize(k), self.mb_serialize(v) + wrote_ln = False + for flags, tmpl in replacements: + if subst_vals == flags: + self.write_ln(tmpl) + wrote_ln = True + + if not wrote_ln: + self.write_ln("%s[%s] = %s" % subst_vals) + + def write_condition(self, values): + def mk_condition(k, v): + if not v: + return 'not CONFIG["%s"]' % k + return 'CONFIG["%s"] == %s' % (k, self.mb_serialize(v)) + + self.write("\n") + self.write("if ") + self.write( + " and ".join(mk_condition(k, values[k]) for k in sorted(values.keys())) + ) + self.write(":\n") + self.indent += " " * self._indent_increment + + def terminate_condition(self): + assert len(self.indent) >= self._indent_increment + self.indent = self.indent[self._indent_increment :] + + +def find_deps(all_targets, target): + all_deps = set() + queue = deque([target]) + while queue: + item = queue.popleft() + all_deps.add(item) + for dep in all_targets[item]["deps"]: + if dep not in all_deps: + queue.append(dep) + return all_deps + + +def filter_gn_config(path, gn_result, sandbox_vars, input_vars, gn_target): + gen_path = path / "gen" + # Translates the raw output of gn into just what we'll need to generate a + # mozbuild configuration. + gn_out = {"targets": {}, "sandbox_vars": sandbox_vars} + + cpus = { + "arm64": "aarch64", + "x64": "x86_64", + "mipsel": "mips32", + "mips64el": "mips64", + } + oses = { + "android": "Android", + "linux": "Linux", + "mac": "Darwin", + "openbsd": "OpenBSD", + "win": "WINNT", + } + + mozbuild_args = { + "MOZ_DEBUG": "1" if input_vars.get("is_debug") else None, + "OS_TARGET": oses[input_vars["target_os"]], + "TARGET_CPU": cpus.get(input_vars["target_cpu"], input_vars["target_cpu"]), + } + if "use_x11" in input_vars: + mozbuild_args["MOZ_X11"] = "1" if input_vars["use_x11"] else None + + gn_out["mozbuild_args"] = mozbuild_args + all_deps = find_deps(gn_result["targets"], gn_target) + + for target_fullname in all_deps: + raw_spec = gn_result["targets"][target_fullname] + + if raw_spec["type"] == "action": + # Special handling for the action type to avoid putting empty + # arrays of args, script and outputs on all other types in `spec`. + spec = {} + for spec_attr in ( + "type", + "args", + "script", + "outputs", + ): + spec[spec_attr] = raw_spec.get(spec_attr, []) + if spec_attr == "outputs": + # Rebase outputs from an absolute path in the temp dir to a + # path relative to the target dir. + spec[spec_attr] = [ + mozpath.relpath(d, path) for d in spec[spec_attr] + ] + gn_out["targets"][target_fullname] = spec + + # TODO: 'executable' will need to be handled here at some point as well. + if raw_spec["type"] not in ("static_library", "shared_library", "source_set"): + continue + + spec = {} + for spec_attr in ( + "type", + "sources", + "defines", + "include_dirs", + "cflags", + "cflags_c", + "cflags_cc", + "cflags_objc", + "cflags_objcc", + "deps", + "libs", + ): + spec[spec_attr] = raw_spec.get(spec_attr, []) + if spec_attr == "defines": + spec[spec_attr] = [ + d + for d in spec[spec_attr] + if "CR_XCODE_VERSION" not in d + and "CR_SYSROOT_HASH" not in d + and "_FORTIFY_SOURCE" not in d + ] + if spec_attr == "include_dirs": + # Rebase outputs from an absolute path in the temp dir to a path + # relative to the target dir. + spec[spec_attr] = [ + d if gen_path != Path(d) else "!//gen" for d in spec[spec_attr] + ] + + gn_out["targets"][target_fullname] = spec + + return gn_out + + +def process_gn_config( + gn_config, topsrcdir, srcdir, non_unified_sources, sandbox_vars, mozilla_flags +): + # Translates a json gn config into attributes that can be used to write out + # moz.build files for this configuration. + + # Much of this code is based on similar functionality in `gyp_reader.py`. + + mozbuild_attrs = {"mozbuild_args": gn_config.get("mozbuild_args", None), "dirs": {}} + + targets = gn_config["targets"] + + project_relsrcdir = mozpath.relpath(srcdir, topsrcdir) + + non_unified_sources = set([mozpath.normpath(s) for s in non_unified_sources]) + + def target_info(fullname): + path, name = target_fullname.split(":") + # Stripping '//' gives us a path relative to the project root, + # adding a suffix avoids name collisions with libraries already + # in the tree (like "webrtc"). + return path.lstrip("//"), name + "_gn" + + def resolve_path(path): + # GN will have resolved all these paths relative to the root of the + # project indicated by "//". + if path.startswith("//"): + path = path[2:] + if not path.startswith("/"): + path = "/%s/%s" % (project_relsrcdir, path) + return path + + # Process all targets from the given gn project and its dependencies. + for target_fullname, spec in six.iteritems(targets): + target_path, target_name = target_info(target_fullname) + context_attrs = {} + + # Remove leading 'lib' from the target_name if any, and use as + # library name. + name = target_name + if spec["type"] in ("static_library", "shared_library", "source_set", "action"): + if name.startswith("lib"): + name = name[3:] + context_attrs["LIBRARY_NAME"] = six.ensure_text(name) + else: + raise Exception( + "The following GN target type is not currently " + 'consumed by moz.build: "%s". It may need to be ' + "added, or you may need to re-run the " + "`GnConfigGen` step." % spec["type"] + ) + + if spec["type"] == "shared_library": + context_attrs["FORCE_SHARED_LIB"] = True + + if spec["type"] == "action" and "script" in spec: + flags = [ + resolve_path(spec["script"]), + resolve_path(""), + ] + spec.get("args", []) + context_attrs["GeneratedFile"] = { + "script": "/python/mozbuild/mozbuild/action/file_generate_wrapper.py", + "entry_point": "action", + "outputs": [resolve_path(f) for f in spec["outputs"]], + "flags": flags, + } + + sources = [] + unified_sources = [] + extensions = set() + use_defines_in_asflags = False + + for f in spec.get("sources", []): + f = f.lstrip("//") + ext = mozpath.splitext(f)[-1] + extensions.add(ext) + src = "%s/%s" % (project_relsrcdir, f) + if ext == ".h" or ext == ".inc": + continue + elif ext == ".def": + context_attrs["SYMBOLS_FILE"] = src + elif ext != ".S" and src not in non_unified_sources: + unified_sources.append("/%s" % src) + else: + sources.append("/%s" % src) + # The Mozilla build system doesn't use DEFINES for building + # ASFILES. + if ext == ".s": + use_defines_in_asflags = True + + context_attrs["SOURCES"] = sources + context_attrs["UNIFIED_SOURCES"] = unified_sources + + context_attrs["DEFINES"] = {} + for define in spec.get("defines", []): + if "=" in define: + name, value = define.split("=", 1) + context_attrs["DEFINES"][name] = value + else: + context_attrs["DEFINES"][define] = True + + context_attrs["LOCAL_INCLUDES"] = [] + for include in spec.get("include_dirs", []): + if include.startswith("!"): + include = "!" + resolve_path(include[1:]) + else: + include = resolve_path(include) + # moz.build expects all LOCAL_INCLUDES to exist, so ensure they do. + resolved = mozpath.abspath(mozpath.join(topsrcdir, include[1:])) + if not os.path.exists(resolved): + # GN files may refer to include dirs that are outside of the + # tree or we simply didn't vendor. Print a warning in this case. + if not resolved.endswith("gn-output/gen"): + print( + "Included path: '%s' does not exist, dropping include from GN " + "configuration." % resolved, + file=sys.stderr, + ) + continue + context_attrs["LOCAL_INCLUDES"] += [include] + + context_attrs["ASFLAGS"] = spec.get("asflags_mozilla", []) + if use_defines_in_asflags and context_attrs["DEFINES"]: + context_attrs["ASFLAGS"] += ["-D" + d for d in context_attrs["DEFINES"]] + suffix_map = { + ".c": ("CFLAGS", ["cflags", "cflags_c"]), + ".cpp": ("CXXFLAGS", ["cflags", "cflags_cc"]), + ".cc": ("CXXFLAGS", ["cflags", "cflags_cc"]), + ".m": ("CMFLAGS", ["cflags", "cflags_objc"]), + ".mm": ("CMMFLAGS", ["cflags", "cflags_objcc"]), + } + variables = (suffix_map[e] for e in extensions if e in suffix_map) + for var, flag_keys in variables: + flags = [ + _f for _k in flag_keys for _f in spec.get(_k, []) if _f in mozilla_flags + ] + for f in flags: + # the result may be a string or a list. + if isinstance(f, six.string_types): + context_attrs.setdefault(var, []).append(f) + else: + context_attrs.setdefault(var, []).extend(f) + + context_attrs["OS_LIBS"] = [] + for lib in spec.get("libs", []): + lib_name = os.path.splitext(lib)[0] + if lib.endswith(".framework"): + context_attrs["OS_LIBS"] += ["-framework " + lib_name] + else: + context_attrs["OS_LIBS"] += [lib_name] + + # Add some features to all contexts. Put here in case LOCAL_INCLUDES + # order matters. + context_attrs["LOCAL_INCLUDES"] += [ + "!/ipc/ipdl/_ipdlheaders", + "/ipc/chromium/src", + "/tools/profiler/public", + ] + # These get set via VC project file settings for normal GYP builds. + # TODO: Determine if these defines are needed for GN builds. + if gn_config["mozbuild_args"]["OS_TARGET"] == "WINNT": + context_attrs["DEFINES"]["UNICODE"] = True + context_attrs["DEFINES"]["_UNICODE"] = True + + context_attrs["COMPILE_FLAGS"] = {"OS_INCLUDES": []} + + for key, value in sandbox_vars.items(): + if context_attrs.get(key) and isinstance(context_attrs[key], list): + # If we have a key from sandbox_vars that's also been + # populated here we use the value from sandbox_vars as our + # basis rather than overriding outright. + context_attrs[key] = value + context_attrs[key] + elif context_attrs.get(key) and isinstance(context_attrs[key], dict): + context_attrs[key].update(value) + else: + context_attrs[key] = value + + target_relsrcdir = mozpath.join(project_relsrcdir, target_path, target_name) + mozbuild_attrs["dirs"][target_relsrcdir] = context_attrs + + return mozbuild_attrs + + +def find_common_attrs(config_attributes): + # Returns the intersection of the given configs and prunes the inputs + # to no longer contain these common attributes. + + common_attrs = deepcopy(config_attributes[0]) + + def make_intersection(reference, input_attrs): + # Modifies `reference` so that after calling this function it only + # contains parts it had in common with in `input_attrs`. + + for k, input_value in input_attrs.items(): + # Anything in `input_attrs` must match what's already in + # `reference`. + common_value = reference.get(k) + if common_value: + if isinstance(input_value, list): + reference[k] = [ + i + for i in common_value + if input_value.count(i) == common_value.count(i) + ] + elif isinstance(input_value, dict): + reference[k] = { + key: value + for key, value in common_value.items() + if key in input_value and value == input_value[key] + } + elif input_value != common_value: + del reference[k] + elif k in reference: + del reference[k] + + # Additionally, any keys in `reference` that aren't in `input_attrs` + # must be deleted. + for k in set(reference.keys()) - set(input_attrs.keys()): + del reference[k] + + def make_difference(reference, input_attrs): + # Modifies `input_attrs` so that after calling this function it contains + # no parts it has in common with in `reference`. + for k, input_value in list(six.iteritems(input_attrs)): + common_value = reference.get(k) + if common_value: + if isinstance(input_value, list): + input_attrs[k] = [ + i + for i in input_value + if common_value.count(i) != input_value.count(i) + ] + elif isinstance(input_value, dict): + input_attrs[k] = { + key: value + for key, value in input_value.items() + if key not in common_value + } + else: + del input_attrs[k] + + for config_attr_set in config_attributes[1:]: + make_intersection(common_attrs, config_attr_set) + + for config_attr_set in config_attributes: + make_difference(common_attrs, config_attr_set) + + return common_attrs + + +def write_mozbuild( + topsrcdir, + srcdir, + non_unified_sources, + gn_configs, + mozilla_flags, + write_mozbuild_variables, +): + all_mozbuild_results = [] + + for gn_config in gn_configs: + mozbuild_attrs = process_gn_config( + gn_config, + topsrcdir, + srcdir, + non_unified_sources, + gn_config["sandbox_vars"], + mozilla_flags, + ) + all_mozbuild_results.append(mozbuild_attrs) + + # Translate {config -> {dirs -> build info}} into + # {dirs -> [(config, build_info)]} + configs_by_dir = defaultdict(list) + for config_attrs in all_mozbuild_results: + mozbuild_args = config_attrs["mozbuild_args"] + dirs = config_attrs["dirs"] + for d, build_data in dirs.items(): + configs_by_dir[d].append((mozbuild_args, build_data)) + + mozbuilds = set() + for relsrcdir, configs in sorted(configs_by_dir.items()): + target_srcdir = mozpath.join(topsrcdir, relsrcdir) + mkdir(target_srcdir) + + target_mozbuild = mozpath.join(target_srcdir, "moz.build") + mozbuilds.add(target_mozbuild) + with open(target_mozbuild, "w") as fh: + mb = MozbuildWriter(fh) + mb.write(license_header) + mb.write("\n") + mb.write(generated_header) + + try: + if relsrcdir in write_mozbuild_variables["INCLUDE_TK_CFLAGS_DIRS"]: + mb.write('if CONFIG["MOZ_WIDGET_TOOLKIT"] == "gtk":\n') + mb.write(' CXXFLAGS += CONFIG["MOZ_GTK3_CFLAGS"]\n') + except KeyError: + pass + try: + if ( + relsrcdir + in write_mozbuild_variables["INCLUDE_SYSTEM_LIBVPX_HANDLING"] + ): + mb.write('if not CONFIG["MOZ_SYSTEM_LIBVPX"]:\n') + mb.write(' LOCAL_INCLUDES += [ "/media/libvpx/libvpx/" ]\n') + mb.write(' CXXFLAGS += CONFIG["MOZ_LIBVPX_CFLAGS"]\n') + except KeyError: + pass + + all_args = [args for args, _ in configs] + + # Start with attributes that will be a part of the mozconfig + # for every configuration, then factor by other potentially useful + # combinations. + # FIXME: this is a time-bomb. See bug 1775202. + for attrs in ( + (), + ("MOZ_DEBUG",), + ("OS_TARGET",), + ("TARGET_CPU",), + ("MOZ_DEBUG", "OS_TARGET"), + ("OS_TARGET", "MOZ_X11"), + ("OS_TARGET", "TARGET_CPU"), + ("OS_TARGET", "TARGET_CPU", "MOZ_X11"), + ("OS_TARGET", "TARGET_CPU", "MOZ_DEBUG"), + ("OS_TARGET", "TARGET_CPU", "MOZ_DEBUG", "MOZ_X11"), + ): + conditions = set() + for args in all_args: + cond = tuple(((k, args.get(k) or "") for k in attrs)) + conditions.add(cond) + + for cond in sorted(conditions): + common_attrs = find_common_attrs( + [ + attrs + for args, attrs in configs + if all((args.get(k) or "") == v for k, v in cond) + ] + ) + if any(common_attrs.values()): + if cond: + mb.write_condition(dict(cond)) + mb.write_attrs(common_attrs) + if cond: + mb.terminate_condition() + + mb.finalize() + + dirs_mozbuild = mozpath.join(srcdir, "moz.build") + mozbuilds.add(dirs_mozbuild) + with open(dirs_mozbuild, "w") as fh: + mb = MozbuildWriter(fh) + mb.write(license_header) + mb.write("\n") + mb.write(generated_header) + + # Not every srcdir is present for every config, which needs to be + # reflected in the generated root moz.build. + dirs_by_config = { + tuple(v["mozbuild_args"].items()): set(v["dirs"].keys()) + for v in all_mozbuild_results + } + + for attrs in ( + (), + ("OS_TARGET",), + ("OS_TARGET", "TARGET_CPU"), + ("OS_TARGET", "TARGET_CPU", "MOZ_X11"), + ): + conditions = set() + for args in dirs_by_config.keys(): + cond = tuple(((k, dict(args).get(k) or "") for k in attrs)) + conditions.add(cond) + + for cond in sorted(conditions): + common_dirs = None + for args, dir_set in dirs_by_config.items(): + if all((dict(args).get(k) or "") == v for k, v in cond): + if common_dirs is None: + common_dirs = deepcopy(dir_set) + else: + common_dirs &= dir_set + + for args, dir_set in dirs_by_config.items(): + if all(dict(args).get(k) == v for k, v in cond): + dir_set -= common_dirs + + if common_dirs: + if cond: + mb.write_condition(dict(cond)) + mb.write_mozbuild_list("DIRS", ["/%s" % d for d in common_dirs]) + if cond: + mb.terminate_condition() + + # Remove possibly stale moz.builds + for root, dirs, files in os.walk(srcdir): + if "moz.build" in files: + file = os.path.join(root, "moz.build") + if file not in mozbuilds: + os.unlink(file) + + +def generate_gn_config( + srcdir, + gn_binary, + input_variables, + sandbox_variables, + gn_target, +): + def str_for_arg(v): + if v in (True, False): + return str(v).lower() + return '"%s"' % v + + input_variables = input_variables.copy() + input_variables.update( + { + "concurrent_links": 1, + "action_pool_depth": 1, + } + ) + + if input_variables["target_os"] == "win": + input_variables.update( + { + "visual_studio_path": "/", + "visual_studio_version": 2015, + "wdk_path": "/", + } + ) + if input_variables["target_os"] == "mac": + input_variables.update( + { + "mac_sdk_path": "/", + "enable_wmax_tokens": False, + } + ) + + gn_args = "--args=%s" % " ".join( + ["%s=%s" % (k, str_for_arg(v)) for k, v in six.iteritems(input_variables)] + ) + with tempfile.TemporaryDirectory() as tempdir: + # On Mac, `tempdir` starts with /var which is a symlink to /private/var. + # We resolve the symlinks in `tempdir` here so later usage with + # relpath() does not lead to unexpected results, should it be used + # together with another path that has symlinks resolved. + resolved_tempdir = Path(tempdir).resolve() + gen_args = [gn_binary, "gen", str(resolved_tempdir), gn_args, "--ide=json"] + print('Running "%s"' % " ".join(gen_args), file=sys.stderr) + subprocess.check_call(gen_args, cwd=srcdir, stderr=subprocess.STDOUT) + + gn_config_file = resolved_tempdir / "project.json" + + with open(gn_config_file, "r") as fh: + gn_out = json.load(fh) + gn_out = filter_gn_config( + resolved_tempdir, gn_out, sandbox_variables, input_variables, gn_target + ) + return gn_out + + +def main(): + parser = argparse.ArgumentParser() + parser.add_argument("config", help="configuration in json format") + args = parser.parse_args() + + gn_binary = bootstrap_toolchain("gn/gn") or which("gn") + if not gn_binary: + raise Exception("The GN program must be present to generate GN configs.") + + with open(args.config, "r") as fh: + config = json.load(fh) + + topsrcdir = Path(__file__).parent.parent.parent.parent.resolve() + + vars_set = [] + for is_debug in (True, False): + for target_os in ("android", "linux", "mac", "openbsd", "win"): + target_cpus = ["x64"] + if target_os in ("android", "linux", "mac", "win", "openbsd"): + target_cpus.append("arm64") + if target_os in ("android", "linux"): + target_cpus.append("arm") + if target_os in ("android", "linux", "win"): + target_cpus.append("x86") + if target_os in ("linux", "openbsd"): + target_cpus.append("riscv64") + if target_os == "linux": + target_cpus.extend(["ppc64", "mipsel", "mips64el"]) + for target_cpu in target_cpus: + vars = { + "host_cpu": "x64", + "is_debug": is_debug, + "target_cpu": target_cpu, + "target_os": target_os, + } + if target_os == "linux": + for use_x11 in (True, False): + vars["use_x11"] = use_x11 + vars_set.append(vars.copy()) + else: + if target_os == "openbsd": + vars["use_x11"] = True + vars_set.append(vars) + + gn_configs = [] + for vars in vars_set: + gn_configs.append( + generate_gn_config( + topsrcdir / config["target_dir"], + gn_binary, + vars, + config["gn_sandbox_variables"], + config["gn_target"], + ) + ) + + print("Writing moz.build files") + write_mozbuild( + topsrcdir, + topsrcdir / config["target_dir"], + config["non_unified_sources"], + gn_configs, + config["mozilla_flags"], + config["write_mozbuild_variables"], + ) + + +if __name__ == "__main__": + main() diff --git a/python/mozbuild/mozbuild/html_build_viewer.py b/python/mozbuild/mozbuild/html_build_viewer.py new file mode 100644 index 0000000000..7a99ff7744 --- /dev/null +++ b/python/mozbuild/mozbuild/html_build_viewer.py @@ -0,0 +1,122 @@ +# 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/. + +# This module contains code for running an HTTP server to view build info. +import http.server +import json +import os + +import requests + + +class HTTPHandler(http.server.BaseHTTPRequestHandler): + def do_GET(self): + s = self.server.wrapper + p = self.path + + if p == "/build_resources.json": + self.send_response(200) + self.send_header("Content-Type", "application/json; charset=utf-8") + self.end_headers() + + keys = sorted(s.json_files.keys()) + s = json.dumps({"files": ["resources/%s" % k for k in keys]}) + self.wfile.write(s.encode("utf-8")) + return + + if p.startswith("/resources/"): + key = p[len("/resources/") :] + + if key not in s.json_files: + self.send_error(404) + return + + self.send_response(200) + self.send_header("Content-Type", "application/json; charset=utf-8") + self.send_header( + "Access-Control-Allow-Origin", "https://profiler.firefox.com" + ) + self.end_headers() + + self.wfile.write(s.json_files[key]) + self.server.wrapper.do_shutdown = True + return + + if p == "/": + p = "/build_resources.html" + + self.serve_docroot(s.doc_root, p[1:]) + + def do_POST(self): + if self.path == "/shutdown": + self.server.wrapper.do_shutdown = True + self.send_response(200) + return + + self.send_error(404) + + def serve_docroot(self, root, path): + local_path = os.path.normpath(os.path.join(root, path)) + + # Cheap security. This doesn't resolve symlinks, etc. But, it should be + # acceptable since this server only runs locally. + if not local_path.startswith(root): + self.send_error(404) + + if not os.path.exists(local_path): + self.send_error(404) + return + + if os.path.isdir(local_path): + self.send_error(500) + return + + self.send_response(200) + ct = "text/plain" + if path.endswith(".html"): + ct = "text/html" + + self.send_header("Content-Type", ct) + self.end_headers() + + with open(local_path, "rb") as fh: + self.wfile.write(fh.read()) + + +class BuildViewerServer(object): + def __init__(self, address="localhost", port=0): + # TODO use pkg_resources to obtain HTML resources. + pkg_dir = os.path.dirname(os.path.abspath(__file__)) + doc_root = os.path.join(pkg_dir, "resources", "html-build-viewer") + assert os.path.isdir(doc_root) + + self.doc_root = doc_root + self.json_files = {} + + self.server = http.server.HTTPServer((address, port), HTTPHandler) + self.server.wrapper = self + self.do_shutdown = False + + @property + def url(self): + hostname, port = self.server.server_address + return "http://%s:%d/" % (hostname, port) + + def add_resource_json_file(self, key, path): + """Register a resource JSON file with the server. + + The file will be made available under the name/key specified.""" + with open(path, "rb") as fh: + self.json_files[key] = fh.read() + + def add_resource_json_url(self, key, url): + """Register a resource JSON file at a URL.""" + r = requests.get(url) + if r.status_code != 200: + raise Exception("Non-200 HTTP response code") + self.json_files[key] = r.text + + def run(self): + while not self.do_shutdown: + self.server.handle_request() diff --git a/python/mozbuild/mozbuild/jar.py b/python/mozbuild/mozbuild/jar.py new file mode 100644 index 0000000000..a595cee164 --- /dev/null +++ b/python/mozbuild/mozbuild/jar.py @@ -0,0 +1,646 @@ +# 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(r"\s*(#.*)?$") + jarline = re.compile( + r""" + (?: + (?:\[(?P<base>[\w\d.\-_\/{}@]+)\]\s*)? # optional [base/path] + (?P<jarfile>[\w\d.\-_\/{}]+)\.jar: # filename.jar: + | + (?:\s*(\#.*)?) # comment + )\s*$ # whitespaces + """, + re.VERBOSE, + ) + relsrcline = re.compile(r"relativesrcdir\s+(?P<relativesrcdir>.+?):") + regline = re.compile(r"%\s+(.*)$") + entryre = r"(?P<optPreprocess>\*)?(?P<optOverwrite>\+?)\s+" + entryline = re.compile( + entryre + + ( + r"(?P<output>[\w\d.\-\_\/+@]+)\s*" + r"(\((?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) diff --git a/python/mozbuild/mozbuild/mach_commands.py b/python/mozbuild/mozbuild/mach_commands.py new file mode 100644 index 0000000000..38723f4e0f --- /dev/null +++ b/python/mozbuild/mozbuild/mach_commands.py @@ -0,0 +1,2952 @@ +# 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/. + +import argparse +import errno +import itertools +import json +import logging +import operator +import os +import os.path +import platform +import re +import shutil +import subprocess +import sys +import tempfile +import time +from os import path +from pathlib import Path + +import mozpack.path as mozpath +from mach.decorators import ( + Command, + CommandArgument, + CommandArgumentGroup, + SubCommand, +) +from mozfile import load_source + +from mozbuild.base import ( + BinaryNotFoundException, + BuildEnvironmentNotFoundException, + MozbuildObject, +) +from mozbuild.base import MachCommandConditions as conditions +from mozbuild.util import MOZBUILD_METRICS_PATH + +here = os.path.abspath(os.path.dirname(__file__)) + +EXCESSIVE_SWAP_MESSAGE = """ +=================== +PERFORMANCE WARNING + +Your machine experienced a lot of swap activity during the build. This is +possibly a sign that your machine doesn't have enough physical memory or +not enough available memory to perform the build. It's also possible some +other system activity during the build is to blame. + +If you feel this message is not appropriate for your machine configuration, +please file a Firefox Build System :: General bug at +https://bugzilla.mozilla.org/enter_bug.cgi?product=Firefox%20Build%20System&component=General +and tell us about your machine and build configuration so we can adjust the +warning heuristic. +=================== +""" + + +class StoreDebugParamsAndWarnAction(argparse.Action): + def __call__(self, parser, namespace, values, option_string=None): + sys.stderr.write( + "The --debugparams argument is deprecated. Please " + + "use --debugger-args instead.\n\n" + ) + setattr(namespace, self.dest, values) + + +@Command( + "watch", + category="post-build", + description="Watch and re-build (parts of) the tree.", + conditions=[conditions.is_firefox], + virtualenv_name="watch", +) +@CommandArgument( + "-v", + "--verbose", + action="store_true", + help="Verbose output for what commands the watcher is running.", +) +def watch(command_context, verbose=False): + """Watch and re-build (parts of) the source tree.""" + if not conditions.is_artifact_build(command_context): + print( + "WARNING: mach watch only rebuilds the `mach build faster` parts of the tree!" + ) + + if not command_context.substs.get("WATCHMAN", None): + print( + "mach watch requires watchman to be installed and found at configure time. See " + "https://developer.mozilla.org/docs/Mozilla/Developer_guide/Build_Instructions/Incremental_builds_with_filesystem_watching" # noqa + ) + return 1 + + from mozbuild.faster_daemon import Daemon + + daemon = Daemon(command_context.config_environment) + + try: + return daemon.watch() + except KeyboardInterrupt: + # Suppress ugly stack trace when user hits Ctrl-C. + sys.exit(3) + + +CARGO_CONFIG_NOT_FOUND_ERROR_MSG = """\ +The sub-command {subcommand} is not currently configured to be used with ./mach cargo. +To do so, add the corresponding file in <mozilla-root-dir>/build/cargo, following other examples in this directory""" + + +def _cargo_config_yaml_schema(): + from voluptuous import All, Boolean, Required, Schema + + def starts_with_cargo(s): + if s.startswith("cargo-"): + return s + else: + raise ValueError + + return Schema( + { + # The name of the command (not checked for now, but maybe + # later) + Required("command"): All(str, starts_with_cargo), + # Whether `make` should stop immediately in case + # of error returned by the command. Default: False + "continue_on_error": Boolean, + # Whether this command requires pre_export and export build + # targets to have run. Defaults to bool(cargo_build_flags). + "requires_export": Boolean, + # Build flags to use. If this variable is not + # defined here, the build flags are generated automatically and are + # the same as for `cargo build`. See available substitutions at the + # end. + "cargo_build_flags": [str], + # Extra build flags to use. These flags are added + # after the cargo_build_flags both when they are provided or + # automatically generated. See available substitutions at the end. + "cargo_extra_flags": [str], + # Available substitutions for `cargo_*_flags`: + # * {arch}: architecture target + # * {crate}: current crate name + # * {directory}: Directory of the current crate within the source tree + # * {features}: Rust features (for `--features`) + # * {manifest}: full path of `Cargo.toml` file + # * {target}: `--lib` for library, `--bin CRATE` for executables + # * {topsrcdir}: Top directory of sources + } + ) + + +@Command( + "cargo", + category="build", + description="Run `cargo <cargo_command>` on a given crate. Defaults to gkrust.", + metrics_path=MOZBUILD_METRICS_PATH, +) +@CommandArgument( + "cargo_command", + default=None, + help="Target to cargo, must be one of the commands in config/cargo/", +) +@CommandArgument( + "--all-crates", + action="store_true", + help="Check all of the crates in the tree.", +) +@CommandArgument( + "-p", "--package", default=None, help="The specific crate name to check." +) +@CommandArgument( + "--jobs", + "-j", + default="0", + nargs="?", + metavar="jobs", + type=int, + help="Run the tests in parallel using multiple processes.", +) +@CommandArgument("-v", "--verbose", action="store_true", help="Verbose output.") +@CommandArgument( + "--message-format-json", + action="store_true", + help="Emit error messages as JSON.", +) +@CommandArgument( + "--continue-on-error", + action="store_true", + help="Do not return an error exit code if the subcommands errors out.", +) +@CommandArgument( + "subcommand_args", + nargs=argparse.REMAINDER, + help="These arguments are passed as-is to the cargo subcommand.", +) +def cargo( + command_context, + cargo_command, + all_crates=None, + package=None, + jobs=0, + verbose=False, + message_format_json=False, + continue_on_error=False, + subcommand_args=[], +): + import yaml + + from mozbuild.controller.building import BuildDriver + + command_context.log_manager.enable_all_structured_loggers() + + topsrcdir = Path(mozpath.normpath(command_context.topsrcdir)) + cargodir = Path(topsrcdir / "build" / "cargo") + + cargo_command_basename = "cargo-" + cargo_command + ".yaml" + cargo_command_fullname = Path(cargodir / cargo_command_basename) + if path.exists(cargo_command_fullname): + with open(cargo_command_fullname) as fh: + yaml_config = yaml.load(fh, Loader=yaml.FullLoader) + schema = _cargo_config_yaml_schema() + schema(yaml_config) + if not yaml_config: + yaml_config = {} + else: + print(CARGO_CONFIG_NOT_FOUND_ERROR_MSG.format(subcommand=cargo_command)) + return 1 + + # print("yaml_config = ", yaml_config) + + yaml_config.setdefault("continue_on_error", False) + continue_on_error = continue_on_error or yaml_config["continue_on_error"] is True + + cargo_build_flags = yaml_config.get("cargo_build_flags") + if cargo_build_flags is not None: + cargo_build_flags = " ".join(cargo_build_flags) + cargo_extra_flags = yaml_config.get("cargo_extra_flags") + if cargo_extra_flags is not None: + cargo_extra_flags = " ".join(cargo_extra_flags) + requires_export = yaml_config.get("requires_export", bool(cargo_build_flags)) + + ret = 0 + if requires_export: + # This directory is created during export. If it's not there, + # export hasn't run already. + deps = Path(command_context.topobjdir) / ".deps" + if not deps.exists(): + build = command_context._spawn(BuildDriver) + ret = build.build( + command_context.metrics, + what=["pre-export", "export"], + jobs=jobs, + verbose=verbose, + mach_context=command_context._mach_context, + ) + else: + try: + command_context.config_environment + except BuildEnvironmentNotFoundException: + build = command_context._spawn(BuildDriver) + ret = build.configure( + command_context.metrics, + buildstatus_messages=False, + ) + if ret != 0: + return ret + + # XXX duplication with `mach vendor rust` + crates_and_roots = { + "gkrust": {"directory": "toolkit/library/rust", "library": True}, + "gkrust-gtest": {"directory": "toolkit/library/gtest/rust", "library": True}, + "geckodriver": {"directory": "testing/geckodriver", "library": False}, + } + + if all_crates: + crates = crates_and_roots.keys() + elif package: + crates = [package] + else: + crates = ["gkrust"] + + if subcommand_args: + subcommand_args = " ".join(subcommand_args) + + for crate in crates: + crate_info = crates_and_roots.get(crate, None) + if not crate_info: + print( + "Cannot locate crate %s. Please check your spelling or " + "add the crate information to the list." % crate + ) + return 1 + + targets = [ + "force-cargo-library-%s" % cargo_command, + "force-cargo-host-library-%s" % cargo_command, + "force-cargo-program-%s" % cargo_command, + "force-cargo-host-program-%s" % cargo_command, + ] + + directory = crate_info["directory"] + # you can use these variables in 'cargo_build_flags' + subst = { + "arch": '"$(RUST_TARGET)"', + "crate": crate, + "directory": directory, + "features": '"$(RUST_LIBRARY_FEATURES)"', + "manifest": str(Path(topsrcdir / directory / "Cargo.toml")), + "target": "--lib" if crate_info["library"] else "--bin " + crate, + "topsrcdir": str(topsrcdir), + } + + if subcommand_args: + targets = targets + [ + "cargo_extra_cli_flags=%s" % (subcommand_args.format(**subst)) + ] + if cargo_build_flags: + targets = targets + [ + "cargo_build_flags=%s" % (cargo_build_flags.format(**subst)) + ] + + append_env = {} + if cargo_extra_flags: + append_env["CARGO_EXTRA_FLAGS"] = cargo_extra_flags.format(**subst) + if message_format_json: + append_env["USE_CARGO_JSON_MESSAGE_FORMAT"] = "1" + if continue_on_error: + append_env["CARGO_CONTINUE_ON_ERROR"] = "1" + if cargo_build_flags: + append_env["CARGO_NO_AUTO_ARG"] = "1" + else: + append_env[ + "ADD_RUST_LTOABLE" + ] = "force-cargo-library-{s:s} force-cargo-program-{s:s}".format( + s=cargo_command + ) + + ret = command_context._run_make( + srcdir=False, + directory=directory, + ensure_exit_code=0, + silent=not verbose, + print_directory=False, + target=targets, + num_jobs=jobs, + append_env=append_env, + ) + if ret != 0: + return ret + + return 0 + + +@SubCommand( + "cargo", + "vet", + description="Run `cargo vet`.", +) +@CommandArgument("arguments", nargs=argparse.REMAINDER) +def cargo_vet(command_context, arguments, stdout=None, env=os.environ): + from mozbuild.bootstrap import bootstrap_toolchain + + # Logging of commands enables logging from `bootstrap_toolchain` that we + # don't want to expose. Disable them temporarily. + logger = logging.getLogger("gecko_taskgraph.generator") + level = logger.getEffectiveLevel() + logger.setLevel(logging.ERROR) + + env = env.copy() + cargo_vet = bootstrap_toolchain("cargo-vet") + if cargo_vet: + env["PATH"] = os.pathsep.join([cargo_vet, env["PATH"]]) + logger.setLevel(level) + try: + cargo = command_context.substs["CARGO"] + except (BuildEnvironmentNotFoundException, KeyError): + # Default if this tree isn't configured. + from mozfile import which + + cargo = which("cargo", path=env["PATH"]) + if not cargo: + raise OSError( + errno.ENOENT, + ( + "Could not find 'cargo' on your $PATH. " + "Hint: have you run `mach build` or `mach configure`?" + ), + ) + + locked = "--locked" in arguments + if locked: + # The use of --locked requires .cargo/config to exist, but other things, + # like cargo update, don't want it there, so remove it once we're done. + topsrcdir = Path(command_context.topsrcdir) + shutil.copyfile( + topsrcdir / ".cargo" / "config.in", topsrcdir / ".cargo" / "config" + ) + + try: + res = subprocess.run( + [cargo, "vet"] + arguments, + cwd=command_context.topsrcdir, + stdout=stdout, + env=env, + ) + finally: + if locked: + (topsrcdir / ".cargo" / "config").unlink() + + # When the function is invoked without stdout set (the default when running + # as a mach subcommand), exit with the returncode from cargo vet. + # When the function is invoked with stdout (direct function call), return + # the full result from subprocess.run. + return res if stdout else res.returncode + + +@Command( + "doctor", + category="devenv", + description="Diagnose and fix common development environment issues.", +) +@CommandArgument( + "--fix", + default=False, + action="store_true", + help="Attempt to fix found problems.", +) +@CommandArgument( + "--verbose", + default=False, + action="store_true", + help="Print verbose information found by checks.", +) +def doctor(command_context, fix=False, verbose=False): + """Diagnose common build environment problems""" + from mozbuild.doctor import run_doctor + + return run_doctor( + topsrcdir=command_context.topsrcdir, + topobjdir=command_context.topobjdir, + configure_args=command_context.mozconfig["configure_args"], + fix=fix, + verbose=verbose, + ) + + +CLOBBER_CHOICES = {"objdir", "python", "gradle"} + + +@Command( + "clobber", + category="build", + description="Clobber the tree (delete the object directory).", + no_auto_log=True, +) +@CommandArgument( + "what", + default=["objdir", "python"], + nargs="*", + help="Target to clobber, must be one of {{{}}} (default " + "objdir and python).".format(", ".join(CLOBBER_CHOICES)), +) +@CommandArgument("--full", action="store_true", help="Perform a full clobber") +def clobber(command_context, what, full=False): + """Clean up the source and object directories. + + Performing builds and running various commands generate various files. + + Sometimes it is necessary to clean up these files in order to make + things work again. This command can be used to perform that cleanup. + + The `objdir` target removes most files in the current object directory + (where build output is stored). Some files (like Visual Studio project + files) are not removed by default. If you would like to remove the + object directory in its entirety, run with `--full`. + + The `python` target will clean up Python's generated files (virtualenvs, + ".pyc", "__pycache__", etc). + + The `gradle` target will remove the "gradle" subdirectory of the object + directory. + + By default, the command clobbers the `objdir` and `python` targets. + """ + what = set(what) + invalid = what - CLOBBER_CHOICES + if invalid: + print( + "Unknown clobber target(s): {}. Choose from {{{}}}".format( + ", ".join(invalid), ", ".join(CLOBBER_CHOICES) + ) + ) + return 1 + + ret = 0 + if "objdir" in what: + from mozbuild.controller.clobber import Clobberer + + try: + substs = command_context.substs + except BuildEnvironmentNotFoundException: + substs = {} + + try: + Clobberer( + command_context.topsrcdir, command_context.topobjdir, substs + ).remove_objdir(full) + except OSError as e: + if sys.platform.startswith("win"): + if isinstance(e, WindowsError) and e.winerror in (5, 32): + command_context.log( + logging.ERROR, + "file_access_error", + {"error": e}, + "Could not clobber because a file was in use. If the " + "application is running, try closing it. {error}", + ) + return 1 + raise + + if "python" in what: + if conditions.is_hg(command_context): + cmd = [ + "hg", + "--config", + "extensions.purge=", + "purge", + "--all", + "-I", + "glob:**.py[cdo]", + "-I", + "glob:**/__pycache__", + ] + elif conditions.is_git(command_context): + cmd = ["git", "clean", "-d", "-f", "-x", "*.py[cdo]", "*/__pycache__/*"] + else: + cmd = ["find", ".", "-type", "f", "-name", "*.py[cdo]", "-delete"] + subprocess.call(cmd, cwd=command_context.topsrcdir) + cmd = [ + "find", + ".", + "-type", + "d", + "-name", + "__pycache__", + "-empty", + "-delete", + ] + ret = subprocess.call(cmd, cwd=command_context.topsrcdir) + + # We'll keep this around to delete the legacy "_virtualenv" dir folders + # so that people don't get confused if they see it and try to manipulate + # it but it has no effect. + shutil.rmtree( + mozpath.join(command_context.topobjdir, "_virtualenvs"), + ignore_errors=True, + ) + from mach.util import get_virtualenv_base_dir + + virtualenv_dir = Path(get_virtualenv_base_dir(command_context.topsrcdir)) + + for specific_venv in virtualenv_dir.iterdir(): + if specific_venv.name == "mach": + # We can't delete the "mach" virtualenv with clobber + # since it's the one doing the clobbering. It always + # has to be removed manually. + pass + else: + shutil.rmtree(specific_venv, ignore_errors=True) + + if "gradle" in what: + shutil.rmtree( + mozpath.join(command_context.topobjdir, "gradle"), ignore_errors=True + ) + + return ret + + +@Command( + "show-log", category="post-build", description="Display mach logs", no_auto_log=True +) +@CommandArgument( + "log_file", + nargs="?", + type=argparse.FileType("rb"), + help="Filename to read log data from. Defaults to the log of the last " + "mach command.", +) +def show_log(command_context, log_file=None): + """Show mach logs + If we're in a terminal context, the log is piped to 'less' + for more convenient viewing. + (https://man7.org/linux/man-pages/man1/less.1.html) + """ + if not log_file: + path = command_context._get_state_filename("last_log.json") + log_file = open(path, "rb") + + if os.isatty(sys.stdout.fileno()): + env = dict(os.environ) + if "LESS" not in env: + # Sensible default flags if none have been set in the user environment. + env["LESS"] = "FRX" + less = subprocess.Popen( + ["less"], stdin=subprocess.PIPE, env=env, encoding="UTF-8" + ) + + try: + # Create a new logger handler with the stream being the stdin of our 'less' + # process so that we can pipe the logger output into 'less' + less_handler = logging.StreamHandler(stream=less.stdin) + less_handler.setFormatter( + command_context.log_manager.terminal_handler.formatter + ) + less_handler.setLevel(command_context.log_manager.terminal_handler.level) + + # replace the existing terminal handler with the new one for 'less' while + # still keeping the original one to set back later + original_handler = command_context.log_manager.replace_terminal_handler( + less_handler + ) + + # Save this value so we can set it back to the original value later + original_logging_raise_exceptions = logging.raiseExceptions + + # We need to explicitly disable raising exceptions inside logging so + # that we can catch them here ourselves to ignore the ones we want + logging.raiseExceptions = False + + # Parses the log file line by line and streams + # (to less.stdin) the relevant records we want + handle_log_file(command_context, log_file) + + # At this point we've piped the entire log file to + # 'less', so we can close the input stream + less.stdin.close() + + # Wait for the user to manually terminate `less` + less.wait() + except OSError as os_error: + # (POSIX) errno.EPIPE: BrokenPipeError: [Errno 32] Broken pipe + # (Windows) errno.EINVAL: OSError: [Errno 22] Invalid argument + if os_error.errno == errno.EPIPE or os_error.errno == errno.EINVAL: + # If the user manually terminates 'less' before the entire log file + # is piped (without scrolling close enough to the bottom) we will get + # one of these errors (depends on the OS) because the logger will still + # attempt to stream to the now invalid less.stdin. To prevent a bunch + # of errors being shown after a user terminates 'less', we just catch + # the first of those exceptions here, and stop parsing the log file. + pass + else: + raise + except Exception: + raise + finally: + # Ensure these values are changed back to the originals, regardless of outcome + command_context.log_manager.replace_terminal_handler(original_handler) + logging.raiseExceptions = original_logging_raise_exceptions + else: + # Not in a terminal context, so just handle the log file with the + # default stream without piping it to a pager (less) + handle_log_file(command_context, log_file) + + +def handle_log_file(command_context, log_file): + start_time = 0 + for line in log_file: + created, action, params = json.loads(line) + if not start_time: + start_time = created + command_context.log_manager.terminal_handler.formatter.start_time = created + if "line" in params: + record = logging.makeLogRecord( + { + "created": created, + "name": command_context._logger.name, + "levelno": logging.INFO, + "msg": "{line}", + "params": params, + "action": action, + } + ) + command_context._logger.handle(record) + + +# Provide commands for inspecting warnings. + + +def database_path(command_context): + return command_context._get_state_filename("warnings.json") + + +def get_warnings_database(command_context): + from mozbuild.compilation.warnings import WarningsDatabase + + path = database_path(command_context) + + database = WarningsDatabase() + + if os.path.exists(path): + database.load_from_file(path) + + return database + + +@Command( + "warnings-summary", + category="post-build", + description="Show a summary of compiler warnings.", +) +@CommandArgument( + "-C", + "--directory", + default=None, + help="Change to a subdirectory of the build directory first.", +) +@CommandArgument( + "report", + default=None, + nargs="?", + help="Warnings report to display. If not defined, show the most recent report.", +) +def summary(command_context, directory=None, report=None): + database = get_warnings_database(command_context) + + if directory: + dirpath = join_ensure_dir(command_context.topsrcdir, directory) + if not dirpath: + return 1 + else: + dirpath = None + + type_counts = database.type_counts(dirpath) + sorted_counts = sorted(type_counts.items(), key=operator.itemgetter(1)) + + total = 0 + for k, v in sorted_counts: + print("%d\t%s" % (v, k)) + total += v + + print("%d\tTotal" % total) + + +@Command( + "warnings-list", + category="post-build", + description="Show a list of compiler warnings.", +) +@CommandArgument( + "-C", + "--directory", + default=None, + help="Change to a subdirectory of the build directory first.", +) +@CommandArgument( + "--flags", default=None, nargs="+", help="Which warnings flags to match." +) +@CommandArgument( + "report", + default=None, + nargs="?", + help="Warnings report to display. If not defined, show the most recent report.", +) +def list_warnings(command_context, directory=None, flags=None, report=None): + database = get_warnings_database(command_context) + + by_name = sorted(database.warnings) + + topsrcdir = mozpath.normpath(command_context.topsrcdir) + + if directory: + directory = mozpath.normsep(directory) + dirpath = join_ensure_dir(topsrcdir, directory) + if not dirpath: + return 1 + + if flags: + # Flatten lists of flags. + flags = set(itertools.chain(*[flaglist.split(",") for flaglist in flags])) + + for warning in by_name: + filename = mozpath.normsep(warning["filename"]) + + if filename.startswith(topsrcdir): + filename = filename[len(topsrcdir) + 1 :] + + if directory and not filename.startswith(directory): + continue + + if flags and warning["flag"] not in flags: + continue + + if warning["column"] is not None: + print( + "%s:%d:%d [%s] %s" + % ( + filename, + warning["line"], + warning["column"], + warning["flag"], + warning["message"], + ) + ) + else: + print( + "%s:%d [%s] %s" + % (filename, warning["line"], warning["flag"], warning["message"]) + ) + + +def join_ensure_dir(dir1, dir2): + dir1 = mozpath.normpath(dir1) + dir2 = mozpath.normsep(dir2) + joined_path = mozpath.join(dir1, dir2) + if os.path.isdir(joined_path): + return joined_path + print("Specified directory not found.") + return None + + +@Command("gtest", category="testing", description="Run GTest unit tests (C++ tests).") +@CommandArgument( + "gtest_filter", + default="*", + nargs="?", + metavar="gtest_filter", + help="test_filter is a ':'-separated list of wildcard patterns " + "(called the positive patterns), optionally followed by a '-' " + "and another ':'-separated pattern list (called the negative patterns)." + "Test names are of the format SUITE.NAME. Use --list-tests to see all.", +) +@CommandArgument("--list-tests", action="store_true", help="list all available tests") +@CommandArgument( + "--jobs", + "-j", + default="1", + nargs="?", + metavar="jobs", + type=int, + help="Run the tests in parallel using multiple processes.", +) +@CommandArgument( + "--tbpl-parser", + "-t", + action="store_true", + help="Output test results in a format that can be parsed by TBPL.", +) +@CommandArgument( + "--shuffle", + "-s", + action="store_true", + help="Randomize the execution order of tests.", +) +@CommandArgument( + "--enable-webrender", + action="store_true", + default=False, + dest="enable_webrender", + help="Enable the WebRender compositor in Gecko.", +) +@CommandArgumentGroup("Android") +@CommandArgument( + "--package", + default="org.mozilla.geckoview.test_runner", + group="Android", + help="Package name of test app.", +) +@CommandArgument( + "--adbpath", dest="adb_path", group="Android", help="Path to adb binary." +) +@CommandArgument( + "--deviceSerial", + dest="device_serial", + group="Android", + help="adb serial number of remote device. " + "Required when more than one device is connected to the host. " + "Use 'adb devices' to see connected devices.", +) +@CommandArgument( + "--remoteTestRoot", + dest="remote_test_root", + group="Android", + help="Remote directory to use as test root (eg. /data/local/tmp/test_root).", +) +@CommandArgument( + "--libxul", dest="libxul_path", group="Android", help="Path to gtest libxul.so." +) +@CommandArgument( + "--no-install", + action="store_true", + default=False, + group="Android", + help="Skip the installation of the APK.", +) +@CommandArgumentGroup("debugging") +@CommandArgument( + "--debug", + action="store_true", + group="debugging", + help="Enable the debugger. Not specifying a --debugger option will result in " + "the default debugger being used.", +) +@CommandArgument( + "--debugger", + default=None, + type=str, + group="debugging", + help="Name of debugger to use.", +) +@CommandArgument( + "--debugger-args", + default=None, + metavar="params", + type=str, + group="debugging", + help="Command-line arguments to pass to the debugger itself; " + "split as the Bourne shell would.", +) +def gtest( + command_context, + shuffle, + jobs, + gtest_filter, + list_tests, + tbpl_parser, + enable_webrender, + package, + adb_path, + device_serial, + remote_test_root, + libxul_path, + no_install, + debug, + debugger, + debugger_args, +): + # We lazy build gtest because it's slow to link + try: + command_context.config_environment + except Exception: + print("Please run |./mach build| before |./mach gtest|.") + return 1 + + res = command_context._mach_context.commands.dispatch( + "build", command_context._mach_context, what=["recurse_gtest"] + ) + if res: + print("Could not build xul-gtest") + return res + + if command_context.substs.get("MOZ_WIDGET_TOOLKIT") == "cocoa": + command_context._run_make( + directory="browser/app", target="repackage", ensure_exit_code=True + ) + + cwd = os.path.join(command_context.topobjdir, "_tests", "gtest") + + if not os.path.isdir(cwd): + os.makedirs(cwd) + + if conditions.is_android(command_context): + if jobs != 1: + print("--jobs is not supported on Android and will be ignored") + if debug or debugger or debugger_args: + print("--debug options are not supported on Android and will be ignored") + from mozrunner.devices.android_device import InstallIntent + + return android_gtest( + command_context, + cwd, + shuffle, + gtest_filter, + package, + adb_path, + device_serial, + remote_test_root, + libxul_path, + InstallIntent.NO if no_install else InstallIntent.YES, + ) + + if ( + package + or adb_path + or device_serial + or remote_test_root + or libxul_path + or no_install + ): + print("One or more Android-only options will be ignored") + + app_path = command_context.get_binary_path("app") + args = [app_path, "-unittest", "--gtest_death_test_style=threadsafe"] + + if ( + sys.platform.startswith("win") + and "MOZ_LAUNCHER_PROCESS" in command_context.defines + ): + args.append("--wait-for-browser") + + if list_tests: + args.append("--gtest_list_tests") + + if debug or debugger or debugger_args: + args = _prepend_debugger_args(args, debugger, debugger_args) + if not args: + return 1 + + # Use GTest environment variable to control test execution + # For details see: + # https://google.github.io/googletest/advanced.html#running-test-programs-advanced-options + gtest_env = {"GTEST_FILTER": gtest_filter} + + # Note: we must normalize the path here so that gtest on Windows sees + # a MOZ_GMP_PATH which has only Windows dir seperators, because + # nsIFile cannot open the paths with non-Windows dir seperators. + xre_path = os.path.join(os.path.normpath(command_context.topobjdir), "dist", "bin") + gtest_env["MOZ_XRE_DIR"] = xre_path + gtest_env["MOZ_GMP_PATH"] = os.pathsep.join( + os.path.join(xre_path, p, "1.0") for p in ("gmp-fake", "gmp-fakeopenh264") + ) + + gtest_env["MOZ_RUN_GTEST"] = "True" + + if shuffle: + gtest_env["GTEST_SHUFFLE"] = "True" + + if tbpl_parser: + gtest_env["MOZ_TBPL_PARSER"] = "True" + + if enable_webrender: + gtest_env["MOZ_WEBRENDER"] = "1" + gtest_env["MOZ_ACCELERATED"] = "1" + else: + gtest_env["MOZ_WEBRENDER"] = "0" + + if jobs == 1: + return command_context.run_process( + args=args, + append_env=gtest_env, + cwd=cwd, + ensure_exit_code=False, + pass_thru=True, + ) + + import functools + + from mozprocess import ProcessHandlerMixin + + def handle_line(job_id, line): + # Prepend the jobId + line = "[%d] %s" % (job_id + 1, line.strip()) + command_context.log(logging.INFO, "GTest", {"line": line}, "{line}") + + gtest_env["GTEST_TOTAL_SHARDS"] = str(jobs) + processes = {} + for i in range(0, jobs): + gtest_env["GTEST_SHARD_INDEX"] = str(i) + processes[i] = ProcessHandlerMixin( + [app_path, "-unittest"], + cwd=cwd, + env=gtest_env, + processOutputLine=[functools.partial(handle_line, i)], + universal_newlines=True, + ) + processes[i].run() + + exit_code = 0 + for process in processes.values(): + status = process.wait() + if status: + exit_code = status + + # Clamp error code to 255 to prevent overflowing multiple of + # 256 into 0 + if exit_code > 255: + exit_code = 255 + + return exit_code + + +def android_gtest( + command_context, + test_dir, + shuffle, + gtest_filter, + package, + adb_path, + device_serial, + remote_test_root, + libxul_path, + install, +): + # setup logging for mozrunner + from mozlog.commandline import setup_logging + + format_args = {"level": command_context._mach_context.settings["test"]["level"]} + default_format = command_context._mach_context.settings["test"]["format"] + setup_logging("mach-gtest", {}, {default_format: sys.stdout}, format_args) + + # ensure that a device is available and test app is installed + from mozrunner.devices.android_device import get_adb_path, verify_android_device + + verify_android_device( + command_context, install=install, app=package, device_serial=device_serial + ) + + if not adb_path: + adb_path = get_adb_path(command_context) + if not libxul_path: + libxul_path = os.path.join( + command_context.topobjdir, "dist", "bin", "gtest", "libxul.so" + ) + + # run gtest via remotegtests.py + exit_code = 0 + + path = os.path.join("testing", "gtest", "remotegtests.py") + load_source("remotegtests", path) + + import remotegtests + + tester = remotegtests.RemoteGTests() + if not tester.run_gtest( + test_dir, + shuffle, + gtest_filter, + package, + adb_path, + device_serial, + remote_test_root, + libxul_path, + None, + ): + exit_code = 1 + tester.cleanup() + + return exit_code + + +@Command( + "package", + category="post-build", + description="Package the built product for distribution as an APK, DMG, etc.", +) +@CommandArgument( + "-v", + "--verbose", + action="store_true", + help="Verbose output for what commands the packaging process is running.", +) +def package(command_context, verbose=False): + """Package the built product for distribution.""" + ret = command_context._run_make( + directory=".", target="package", silent=not verbose, ensure_exit_code=False + ) + if ret == 0: + command_context.notify("Packaging complete") + return ret + + +def _get_android_install_parser(): + parser = argparse.ArgumentParser() + parser.add_argument( + "--app", + default="org.mozilla.geckoview_example", + help="Android package to install (default: org.mozilla.geckoview_example)", + ) + parser.add_argument( + "--verbose", + "-v", + action="store_true", + help="Print verbose output when installing.", + ) + parser.add_argument( + "--aab", + action="store_true", + help="Install as AAB (Android App Bundle)", + ) + return parser + + +def setup_install_parser(): + build = MozbuildObject.from_environment(cwd=here) + if conditions.is_android(build): + return _get_android_install_parser() + return argparse.ArgumentParser() + + +@Command( + "install", + category="post-build", + conditions=[conditions.has_build], + parser=setup_install_parser, + description="Install the package on the machine (or device in the case of Android).", +) +def install(command_context, **kwargs): + """Install a package.""" + if conditions.is_android(command_context): + from mozrunner.devices.android_device import ( + InstallIntent, + verify_android_device, + ) + + ret = ( + verify_android_device(command_context, install=InstallIntent.YES, **kwargs) + == 0 + ) + else: + ret = command_context._run_make( + directory=".", target="install", ensure_exit_code=False + ) + + if ret == 0: + command_context.notify("Install complete") + return ret + + +def _get_android_run_parser(): + parser = argparse.ArgumentParser() + group = parser.add_argument_group("The compiled program") + group.add_argument( + "--app", + default="org.mozilla.geckoview_example", + help="Android package to run (default: org.mozilla.geckoview_example)", + ) + group.add_argument( + "--intent", + default="android.intent.action.VIEW", + help="Android intent action to launch with " + "(default: android.intent.action.VIEW)", + ) + group.add_argument( + "--setenv", + dest="env", + action="append", + default=[], + help="Set target environment variable, like FOO=BAR", + ) + group.add_argument( + "--profile", + "-P", + default=None, + help="Path to Gecko profile, like /path/to/host/profile " + "or /path/to/target/profile", + ) + group.add_argument("--url", default=None, help="URL to open") + group.add_argument( + "--aab", + action="store_true", + default=False, + help="Install app as Android App Bundle (AAB).", + ) + group.add_argument( + "--no-install", + action="store_true", + default=False, + help="Do not try to install application on device before running " + "(default: False)", + ) + group.add_argument( + "--no-wait", + action="store_true", + default=False, + help="Do not wait for application to start before returning " + "(default: False)", + ) + group.add_argument( + "--enable-fission", + action="store_true", + help="Run the program with Fission (site isolation) enabled.", + ) + group.add_argument( + "--fail-if-running", + action="store_true", + default=False, + help="Fail if application is already running (default: False)", + ) + group.add_argument( + "--restart", + action="store_true", + default=False, + help="Stop the application if it is already running (default: False)", + ) + + group = parser.add_argument_group("Debugging") + group.add_argument("--debug", action="store_true", help="Enable the lldb debugger.") + group.add_argument( + "--debugger", + default=None, + type=str, + help="Name of lldb compatible debugger to use.", + ) + group.add_argument( + "--debugger-args", + default=None, + metavar="params", + type=str, + help="Command-line arguments to pass to the debugger itself; " + "split as the Bourne shell would.", + ) + group.add_argument( + "--no-attach", + action="store_true", + default=False, + help="Start the debugging servers on the device but do not " + "attach any debuggers.", + ) + group.add_argument( + "--use-existing-process", + action="store_true", + default=False, + help="Select an existing process to debug.", + ) + return parser + + +def _get_jsshell_run_parser(): + parser = argparse.ArgumentParser() + group = parser.add_argument_group("the compiled program") + group.add_argument( + "params", + nargs="...", + default=[], + help="Command-line arguments to be passed through to the program. Not " + "specifying a --profile or -P option will result in a temporary profile " + "being used.", + ) + + group = parser.add_argument_group("debugging") + group.add_argument( + "--debug", + action="store_true", + help="Enable the debugger. Not specifying a --debugger option will result " + "in the default debugger being used.", + ) + group.add_argument( + "--debugger", default=None, type=str, help="Name of debugger to use." + ) + group.add_argument( + "--debugger-args", + default=None, + metavar="params", + type=str, + help="Command-line arguments to pass to the debugger itself; " + "split as the Bourne shell would.", + ) + group.add_argument( + "--debugparams", + action=StoreDebugParamsAndWarnAction, + default=None, + type=str, + dest="debugger_args", + help=argparse.SUPPRESS, + ) + + return parser + + +def _get_desktop_run_parser(): + parser = argparse.ArgumentParser() + group = parser.add_argument_group("the compiled program") + group.add_argument( + "params", + nargs="...", + default=[], + help="Command-line arguments to be passed through to the program. Not " + "specifying a --profile or -P option will result in a temporary profile " + "being used.", + ) + group.add_argument("--packaged", action="store_true", help="Run a packaged build.") + group.add_argument( + "--app", help="Path to executable to run (default: output of ./mach build)" + ) + group.add_argument( + "--remote", + "-r", + action="store_true", + help="Do not pass the --no-remote argument by default.", + ) + group.add_argument( + "--background", + "-b", + action="store_true", + help="Do not pass the --foreground argument by default on Mac.", + ) + group.add_argument( + "--noprofile", + "-n", + action="store_true", + help="Do not pass the --profile argument by default.", + ) + group.add_argument( + "--disable-e10s", + action="store_true", + help="Run the program with electrolysis disabled.", + ) + group.add_argument( + "--enable-crash-reporter", + action="store_true", + help="Run the program with the crash reporter enabled.", + ) + group.add_argument( + "--disable-fission", + action="store_true", + help="Run the program with Fission (site isolation) disabled.", + ) + group.add_argument( + "--setpref", + action="append", + default=[], + help="Set the specified pref before starting the program. Can be set " + "multiple times. Prefs can also be set in ~/.mozbuild/machrc in the " + "[runprefs] section - see `./mach settings` for more information.", + ) + group.add_argument( + "--temp-profile", + action="store_true", + help="Run the program using a new temporary profile created inside " + "the objdir.", + ) + group.add_argument( + "--macos-open", + action="store_true", + help="On macOS, run the program using the open(1) command. Per open(1), " + "the browser is launched \"just as if you had double-clicked the file's " + 'icon". The browser can not be launched under a debugger with this ' + "option.", + ) + + group = parser.add_argument_group("debugging") + group.add_argument( + "--debug", + action="store_true", + help="Enable the debugger. Not specifying a --debugger option will result " + "in the default debugger being used.", + ) + group.add_argument( + "--debugger", default=None, type=str, help="Name of debugger to use." + ) + group.add_argument( + "--debugger-args", + default=None, + metavar="params", + type=str, + help="Command-line arguments to pass to the debugger itself; " + "split as the Bourne shell would.", + ) + group.add_argument( + "--debugparams", + action=StoreDebugParamsAndWarnAction, + default=None, + type=str, + dest="debugger_args", + help=argparse.SUPPRESS, + ) + + group = parser.add_argument_group("DMD") + group.add_argument( + "--dmd", + action="store_true", + help="Enable DMD. The following arguments have no effect without this.", + ) + group.add_argument( + "--mode", + choices=["live", "dark-matter", "cumulative", "scan"], + help="Profiling mode. The default is 'dark-matter'.", + ) + group.add_argument( + "--stacks", + choices=["partial", "full"], + help="Allocation stack trace coverage. The default is 'partial'.", + ) + group.add_argument( + "--show-dump-stats", action="store_true", help="Show stats when doing dumps." + ) + + return parser + + +def setup_run_parser(): + build = MozbuildObject.from_environment(cwd=here) + if conditions.is_android(build): + return _get_android_run_parser() + if conditions.is_jsshell(build): + return _get_jsshell_run_parser() + return _get_desktop_run_parser() + + +@Command( + "run", + category="post-build", + conditions=[conditions.has_build_or_shell], + parser=setup_run_parser, + description="Run the compiled program, possibly under a debugger or DMD.", +) +def run(command_context, **kwargs): + """Run the compiled program.""" + if conditions.is_android(command_context): + return _run_android(command_context, **kwargs) + if conditions.is_jsshell(command_context): + return _run_jsshell(command_context, **kwargs) + return _run_desktop(command_context, **kwargs) + + +def _run_android( + command_context, + app="org.mozilla.geckoview_example", + intent=None, + env=[], + profile=None, + url=None, + aab=False, + no_install=None, + no_wait=None, + fail_if_running=None, + restart=None, + enable_fission=False, + debug=False, + debugger=None, + debugger_args=None, + no_attach=False, + use_existing_process=False, +): + from mozrunner.devices.android_device import ( + InstallIntent, + _get_device, + verify_android_device, + ) + from six.moves import shlex_quote + + if app == "org.mozilla.geckoview_example": + activity_name = "org.mozilla.geckoview_example.GeckoViewActivity" + elif app == "org.mozilla.geckoview.test_runner": + activity_name = "org.mozilla.geckoview.test_runner.TestRunnerActivity" + elif "fennec" in app or "firefox" in app: + activity_name = "org.mozilla.gecko.BrowserApp" + else: + raise RuntimeError("Application not recognized: {}".format(app)) + + # If we want to debug an existing process, we implicitly do not want + # to kill it and pave over its installation with a new one. + if debug and use_existing_process: + no_install = True + + # `verify_android_device` respects `DEVICE_SERIAL` if it is set and sets it otherwise. + verify_android_device( + command_context, + app=app, + aab=aab, + debugger=debug, + install=InstallIntent.NO if no_install else InstallIntent.YES, + ) + device_serial = os.environ.get("DEVICE_SERIAL") + if not device_serial: + print("No ADB devices connected.") + return 1 + + device = _get_device(command_context.substs, device_serial=device_serial) + + if debug: + # This will terminate any existing processes, so we skip it when we + # want to attach to an existing one. + if not use_existing_process: + command_context.log( + logging.INFO, + "run", + {"app": app}, + "Setting {app} as the device debug app", + ) + device.shell("am set-debug-app -w --persistent %s" % app) + else: + # Make sure that the app doesn't block waiting for jdb + device.shell("am clear-debug-app") + + if not debug or not use_existing_process: + args = [] + if profile: + if os.path.isdir(profile): + host_profile = profile + # Always /data/local/tmp, rather than `device.test_root`, because + # GeckoView only takes its configuration file from /data/local/tmp, + # and we want to follow suit. + target_profile = "/data/local/tmp/{}-profile".format(app) + device.rm(target_profile, recursive=True, force=True) + device.push(host_profile, target_profile) + command_context.log( + logging.INFO, + "run", + { + "host_profile": host_profile, + "target_profile": target_profile, + }, + 'Pushed profile from host "{host_profile}" to ' + 'target "{target_profile}"', + ) + else: + target_profile = profile + command_context.log( + logging.INFO, + "run", + {"target_profile": target_profile}, + 'Using profile from target "{target_profile}"', + ) + + args = ["--profile", shlex_quote(target_profile)] + + # FIXME: When android switches to using Fission by default, + # MOZ_FORCE_DISABLE_FISSION will need to be configured correctly. + if enable_fission: + env.append("MOZ_FORCE_ENABLE_FISSION=1") + + extras = {} + for i, e in enumerate(env): + extras["env{}".format(i)] = e + if args: + extras["args"] = " ".join(args) + + if env or args: + restart = True + + if restart: + fail_if_running = False + command_context.log( + logging.INFO, + "run", + {"app": app}, + "Stopping {app} to ensure clean restart.", + ) + device.stop_application(app) + + # We'd prefer to log the actual `am start ...` command, but it's not trivial + # to wire the device's logger to mach's logger. + command_context.log( + logging.INFO, + "run", + {"app": app, "activity_name": activity_name}, + "Starting {app}/{activity_name}.", + ) + + device.launch_application( + app_name=app, + activity_name=activity_name, + intent=intent, + extras=extras, + url=url, + wait=not no_wait, + fail_if_running=fail_if_running, + ) + + if not debug: + return 0 + + from mozrunner.devices.android_device import run_lldb_server + + socket_file = run_lldb_server(app, command_context.substs, device_serial) + if not socket_file: + command_context.log( + logging.ERROR, + "run", + {"msg": "Failed to obtain a socket file!"}, + "{msg}", + ) + return 1 + + # Give lldb-server a chance to start + command_context.log( + logging.INFO, + "run", + {"msg": "Pausing to ensure lldb-server has started..."}, + "{msg}", + ) + time.sleep(1) + + if use_existing_process: + + def _is_geckoview_process(proc_name, pkg_name): + if not proc_name.startswith(pkg_name): + # Definitely not our package + return False + if len(proc_name) == len(pkg_name): + # Parent process from our package + return True + if proc_name[len(pkg_name)] == ":": + # Child process from our package + return True + # Process name is a prefix of our package name + return False + + # If we're going to attach to an existing process, we need to know + # who we're attaching to. Obtain a list of all processes associated + # with our desired app. + proc_list = [ + proc[:-1] + for proc in device.get_process_list() + if _is_geckoview_process(proc[1], app) + ] + + if not proc_list: + command_context.log( + logging.ERROR, + "run", + {"app": app}, + "No existing {app} processes found", + ) + return 1 + elif len(proc_list) == 1: + pid = proc_list[0][0] + else: + # Prompt the user to determine which process we should use + entries = [ + "%2d: %6d %s" % (n, p[0], p[1]) + for n, p in enumerate(proc_list, start=1) + ] + prompt = "\n".join(["\nPlease select a process:\n"] + entries) + "\n\n" + valid_range = range(1, len(proc_list) + 1) + + while True: + response = int(input(prompt).strip()) + if response in valid_range: + break + command_context.log( + logging.ERROR, "run", {"msg": "Invalid response"}, "{msg}" + ) + pid = proc_list[response - 1][0] + else: + # We're not using an existing process, so there should only be our + # parent process at this time. + pids = device.pidof(app_name=app) + if len(pids) != 1: + command_context.log( + logging.ERROR, + "run", + {"msg": "Not sure which pid to attach to!"}, + "{msg}", + ) + return 1 + pid = pids[0] + + command_context.log( + logging.INFO, "run", {"pid": str(pid)}, "Debuggee pid set to {pid}..." + ) + + lldb_connect_url = "unix-abstract-connect://" + socket_file + local_jdb_port = device.forward("tcp:0", "jdwp:%d" % pid) + + if no_attach: + command_context.log( + logging.INFO, + "run", + {"pid": str(pid), "url": lldb_connect_url}, + "To debug native code, connect lldb to {url} and attach to pid {pid}", + ) + command_context.log( + logging.INFO, + "run", + {"port": str(local_jdb_port)}, + "To debug Java code, connect jdb using tcp to localhost:{port}", + ) + return 0 + + # Beyond this point we want to be able to automatically clean up after ourselves, + # so we enter the following try block. + try: + command_context.log( + logging.INFO, "run", {"msg": "Starting debugger..."}, "{msg}" + ) + + if not use_existing_process: + # The app is waiting for jdb to attach and will not continue running + # until we do so. + def _jdb_ping(local_jdb_port): + jdb_process = subprocess.Popen( + ["jdb", "-attach", "localhost:%d" % local_jdb_port], + stdin=subprocess.PIPE, + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + encoding="utf-8", + ) + # Wait a bit to provide enough time for jdb and lldb to connect + # to the debuggee + time.sleep(5) + # NOTE: jdb cannot detach while the debuggee is frozen in lldb, + # so its process might not necessarily exit immediately once the + # quit command has been issued. + jdb_process.communicate(input="quit\n") + + # We run this in the background while lldb attaches in the foreground + from threading import Thread + + jdb_thread = Thread(target=_jdb_ping, args=[local_jdb_port]) + jdb_thread.start() + + LLDBINIT = """ +settings set target.inline-breakpoint-strategy always +settings append target.exec-search-paths {obj_xul} +settings append target.exec-search-paths {obj_mozglue} +settings append target.exec-search-paths {obj_nss} +platform select remote-android +platform connect {connect_url} +process attach {continue_flag}-p {pid!s} +""".lstrip() + + obj_xul = os.path.join(command_context.topobjdir, "toolkit", "library", "build") + obj_mozglue = os.path.join(command_context.topobjdir, "mozglue", "build") + obj_nss = os.path.join(command_context.topobjdir, "security") + + if use_existing_process: + continue_flag = "" + else: + # Tell lldb to continue after attaching; instead we'll break at + # the initial SEGVHandler, similarly to how things work when we + # attach using Android Studio. Doing this gives Android a chance + # to dismiss the "Waiting for Debugger" dialog. + continue_flag = "-c " + + try: + # Write out our lldb startup commands to a temp file. We'll pass its + # name to lldb on its command line. + with tempfile.NamedTemporaryFile( + mode="wt", encoding="utf-8", newline="\n", delete=False + ) as tmp: + tmp_lldb_start_script = tmp.name + tmp.write( + LLDBINIT.format( + obj_xul=obj_xul, + obj_mozglue=obj_mozglue, + obj_nss=obj_nss, + connect_url=lldb_connect_url, + continue_flag=continue_flag, + pid=pid, + ) + ) + + our_debugger_args = "-s %s" % tmp_lldb_start_script + if debugger_args: + full_debugger_args = " ".join([debugger_args, our_debugger_args]) + else: + full_debugger_args = our_debugger_args + + args = _prepend_debugger_args([], debugger, full_debugger_args) + if not args: + return 1 + + return command_context.run_process( + args=args, ensure_exit_code=False, pass_thru=True + ) + finally: + os.remove(tmp_lldb_start_script) + finally: + device.remove_forwards("tcp:%d" % local_jdb_port) + device.shell("pkill -f lldb-server", enable_run_as=True) + if not use_existing_process: + device.shell("am clear-debug-app") + + +def _run_jsshell(command_context, params, debug, debugger, debugger_args): + try: + binpath = command_context.get_binary_path("app") + except BinaryNotFoundException as e: + command_context.log(logging.ERROR, "run", {"error": str(e)}, "ERROR: {error}") + command_context.log(logging.INFO, "run", {"help": e.help()}, "{help}") + return 1 + + args = [binpath] + + if params: + args.extend(params) + + extra_env = {"RUST_BACKTRACE": "full"} + + if debug or debugger or debugger_args: + if "INSIDE_EMACS" in os.environ: + command_context.log_manager.terminal_handler.setLevel(logging.WARNING) + + import mozdebug + + if not debugger: + # No debugger name was provided. Look for the default ones on + # current OS. + debugger = mozdebug.get_default_debugger_name( + mozdebug.DebuggerSearch.KeepLooking + ) + + if debugger: + debuggerInfo = mozdebug.get_debugger_info(debugger, debugger_args) + + if not debugger or not debuggerInfo: + print("Could not find a suitable debugger in your PATH.") + return 1 + + # Prepend the debugger args. + args = [debuggerInfo.path] + debuggerInfo.args + args + + return command_context.run_process( + args=args, ensure_exit_code=False, pass_thru=True, append_env=extra_env + ) + + +def _run_desktop( + command_context, + params, + packaged, + app, + remote, + background, + noprofile, + disable_e10s, + enable_crash_reporter, + disable_fission, + setpref, + temp_profile, + macos_open, + debug, + debugger, + debugger_args, + dmd, + mode, + stacks, + show_dump_stats, +): + from mozprofile import Preferences, Profile + + try: + if packaged: + binpath = command_context.get_binary_path(where="staged-package") + else: + binpath = app or command_context.get_binary_path("app") + except BinaryNotFoundException as e: + command_context.log(logging.ERROR, "run", {"error": str(e)}, "ERROR: {error}") + if packaged: + command_context.log( + logging.INFO, + "run", + { + "help": "It looks like your build isn't packaged. " + "You can run |./mach package| to package it." + }, + "{help}", + ) + else: + command_context.log(logging.INFO, "run", {"help": e.help()}, "{help}") + return 1 + + args = [] + if macos_open: + if debug: + print( + "The browser can not be launched in the debugger " + "when using the macOS open command." + ) + return 1 + try: + m = re.search(r"^.+\.app", binpath) + apppath = m.group(0) + args = ["open", apppath, "--args"] + except Exception as e: + print( + "Couldn't get the .app path from the binary path. " + "The macOS open option can only be used on macOS" + ) + print(e) + return 1 + else: + args = [binpath] + + if params: + args.extend(params) + + if not remote: + args.append("-no-remote") + + if not background and sys.platform == "darwin": + args.append("-foreground") + + if ( + sys.platform.startswith("win") + and "MOZ_LAUNCHER_PROCESS" in command_context.defines + ): + args.append("-wait-for-browser") + + no_profile_option_given = all( + p not in params for p in ["-profile", "--profile", "-P"] + ) + no_backgroundtask_mode_option_given = all( + p not in params for p in ["-backgroundtask", "--backgroundtask"] + ) + if ( + no_profile_option_given + and no_backgroundtask_mode_option_given + and not noprofile + ): + prefs = { + "browser.aboutConfig.showWarning": False, + "browser.shell.checkDefaultBrowser": False, + "general.warnOnAboutConfig": False, + } + prefs.update(command_context._mach_context.settings.runprefs) + prefs.update([p.split("=", 1) for p in setpref]) + for pref in prefs: + prefs[pref] = Preferences.cast(prefs[pref]) + + tmpdir = os.path.join(command_context.topobjdir, "tmp") + if not os.path.exists(tmpdir): + os.makedirs(tmpdir) + + if temp_profile: + path = tempfile.mkdtemp(dir=tmpdir, prefix="profile-") + else: + path = os.path.join(tmpdir, "profile-default") + + profile = Profile(path, preferences=prefs) + args.append("-profile") + args.append(profile.profile) + + if not no_profile_option_given and setpref: + print("setpref is only supported if a profile is not specified") + return 1 + + some_debugging_option = debug or debugger or debugger_args + + # By default, because Firefox is a GUI app, on Windows it will not + # 'create' a console to which stdout/stderr is printed. This means + # printf/dump debugging is invisible. We default to adding the + # -attach-console argument to fix this. We avoid this if we're launched + # under a debugger (which can do its own picking up of stdout/stderr). + # We also check for both the -console and -attach-console flags: + # -console causes Firefox to create a separate window; + # -attach-console just ends us up with output that gets relayed via mach. + # We shouldn't override the user using -console. For more info, see + # https://bugzilla.mozilla.org/show_bug.cgi?id=1257155 + if ( + sys.platform.startswith("win") + and not some_debugging_option + and "-console" not in args + and "--console" not in args + and "-attach-console" not in args + and "--attach-console" not in args + ): + args.append("-attach-console") + + extra_env = { + "MOZ_DEVELOPER_REPO_DIR": command_context.topsrcdir, + "MOZ_DEVELOPER_OBJ_DIR": command_context.topobjdir, + "RUST_BACKTRACE": "full", + } + + if not enable_crash_reporter: + extra_env["MOZ_CRASHREPORTER_DISABLE"] = "1" + else: + extra_env["MOZ_CRASHREPORTER"] = "1" + + if disable_e10s: + extra_env["MOZ_FORCE_DISABLE_E10S"] = "1" + + if disable_fission: + extra_env["MOZ_FORCE_DISABLE_FISSION"] = "1" + + if some_debugging_option: + if "INSIDE_EMACS" in os.environ: + command_context.log_manager.terminal_handler.setLevel(logging.WARNING) + + import mozdebug + + if not debugger: + # No debugger name was provided. Look for the default ones on + # current OS. + debugger = mozdebug.get_default_debugger_name( + mozdebug.DebuggerSearch.KeepLooking + ) + + if debugger: + debuggerInfo = mozdebug.get_debugger_info(debugger, debugger_args) + + if not debugger or not debuggerInfo: + print("Could not find a suitable debugger in your PATH.") + return 1 + + # Parameters come from the CLI. We need to convert them before + # their use. + if debugger_args: + from mozbuild import shellutil + + try: + debugger_args = shellutil.split(debugger_args) + except shellutil.MetaCharacterException as e: + print( + "The --debugger-args you passed require a real shell to parse them." + ) + print("(We can't handle the %r character.)" % e.char) + return 1 + + # Prepend the debugger args. + args = [debuggerInfo.path] + debuggerInfo.args + args + + if dmd: + dmd_params = [] + + if mode: + dmd_params.append("--mode=" + mode) + if stacks: + dmd_params.append("--stacks=" + stacks) + if show_dump_stats: + dmd_params.append("--show-dump-stats=yes") + + if dmd_params: + extra_env["DMD"] = " ".join(dmd_params) + else: + extra_env["DMD"] = "1" + + return command_context.run_process( + args=args, ensure_exit_code=False, pass_thru=True, append_env=extra_env + ) + + +@Command( + "buildsymbols", + category="post-build", + description="Produce a package of Breakpad-format symbols.", +) +def buildsymbols(command_context): + """Produce a package of debug symbols suitable for use with Breakpad.""" + return command_context._run_make( + directory=".", target="buildsymbols", ensure_exit_code=False + ) + + +@Command( + "environment", + category="build-dev", + description="Show info about the mach and build environment.", +) +@CommandArgument( + "--format", + default="pretty", + choices=["pretty", "json"], + help="Print data in the given format.", +) +@CommandArgument("--output", "-o", type=str, help="Output to the given file.") +@CommandArgument("--verbose", "-v", action="store_true", help="Print verbose output.") +def environment(command_context, format, output=None, verbose=False): + func = {"pretty": _environment_pretty, "json": _environment_json}[ + format.replace(".", "_") + ] + + if output: + # We want to preserve mtimes if the output file already exists + # and the content hasn't changed. + from mozbuild.util import FileAvoidWrite + + with FileAvoidWrite(output) as out: + return func(command_context, out, verbose) + return func(command_context, sys.stdout, verbose) + + +def _environment_pretty(command_context, out, verbose): + state_dir = command_context._mach_context.state_dir + + print("platform:\n\t%s" % platform.platform(), file=out) + print("python version:\n\t%s" % sys.version, file=out) + print("python prefix:\n\t%s" % sys.prefix, file=out) + print("mach cwd:\n\t%s" % command_context._mach_context.cwd, file=out) + print("os cwd:\n\t%s" % os.getcwd(), file=out) + print("mach directory:\n\t%s" % command_context._mach_context.topdir, file=out) + print("state directory:\n\t%s" % state_dir, file=out) + + print("object directory:\n\t%s" % command_context.topobjdir, file=out) + + if command_context.mozconfig["path"]: + print("mozconfig path:\n\t%s" % command_context.mozconfig["path"], file=out) + if command_context.mozconfig["configure_args"]: + print("mozconfig configure args:", file=out) + for arg in command_context.mozconfig["configure_args"]: + print("\t%s" % arg, file=out) + + if command_context.mozconfig["make_extra"]: + print("mozconfig extra make args:", file=out) + for arg in command_context.mozconfig["make_extra"]: + print("\t%s" % arg, file=out) + + if command_context.mozconfig["make_flags"]: + print("mozconfig make flags:", file=out) + for arg in command_context.mozconfig["make_flags"]: + print("\t%s" % arg, file=out) + + config = None + + try: + config = command_context.config_environment + + except Exception: + pass + + if config: + print("config topsrcdir:\n\t%s" % config.topsrcdir, file=out) + print("config topobjdir:\n\t%s" % config.topobjdir, file=out) + + if verbose: + print("config substitutions:", file=out) + for k in sorted(config.substs): + print("\t%s: %s" % (k, config.substs[k]), file=out) + + print("config defines:", file=out) + for k in sorted(config.defines): + print("\t%s" % k, file=out) + + +def _environment_json(command_context, out, verbose): + import json + + class EnvironmentEncoder(json.JSONEncoder): + def default(self, obj): + if isinstance(obj, MozbuildObject): + result = { + "topsrcdir": obj.topsrcdir, + "topobjdir": obj.topobjdir, + "mozconfig": obj.mozconfig, + } + if verbose: + result["substs"] = obj.substs + result["defines"] = obj.defines + return result + elif isinstance(obj, set): + return list(obj) + return json.JSONEncoder.default(self, obj) + + json.dump(command_context, cls=EnvironmentEncoder, sort_keys=True, fp=out) + + +@Command( + "repackage", + category="misc", + description="Repackage artifacts into different formats.", +) +def repackage(command_context): + """Repackages artifacts into different formats. + + This is generally used after packages are signed by the signing + scriptworkers in order to bundle things up into shippable formats, such as a + .dmg on OSX or an installer exe on Windows. + """ + print("Usage: ./mach repackage [dmg|pkg|installer|mar] [args...]") + + +@SubCommand( + "repackage", + "deb", + description="Repackage a tar file into a .deb for Linux", + virtualenv_name="repackage-deb", +) +@CommandArgument( + "--input", "-i", type=str, required=True, help="Input tarfile filename" +) +@CommandArgument("--output", "-o", type=str, required=True, help="Output .deb filename") +@CommandArgument("--arch", type=str, required=True, help="One of ['x86', 'x86_64']") +@CommandArgument( + "--version", + type=str, + required=True, + help="The Firefox version used to create the installer", +) +@CommandArgument( + "--build-number", + type=str, + required=True, + help="The release's build number", +) +@CommandArgument( + "--templates", + type=str, + required=True, + help="Location of the templates used to generate the debian/ directory files", +) +@CommandArgument( + "--release-product", + type=str, + required=True, + help="The product being shipped. Used to disambiguate beta/devedition etc.", +) +@CommandArgument( + "--release-type", + type=str, + required=True, + help="The release being shipped. Used to disambiguate nightly/try etc.", +) +def repackage_deb( + command_context, + input, + output, + arch, + version, + build_number, + templates, + release_product, + release_type, +): + if not os.path.exists(input): + print("Input file does not exist: %s" % input) + return 1 + + template_dir = os.path.join( + command_context.topsrcdir, + templates, + ) + + from fluent.runtime.fallback import FluentLocalization, FluentResourceLoader + + from mozbuild.repackaging.deb import repackage_deb + + repackage_deb( + command_context.log, + input, + output, + template_dir, + arch, + version, + build_number, + release_product, + release_type, + FluentLocalization, + FluentResourceLoader, + ) + + +@SubCommand( + "repackage", + "deb-l10n", + description="Repackage a .xpi langpack file into a .deb for Linux", +) +@CommandArgument( + "--input-xpi-file", type=str, required=True, help="Path to the XPI file" +) +@CommandArgument( + "--input-tar-file", + type=str, + required=True, + help="Path to tar archive that contains application.ini", +) +@CommandArgument( + "--version", + type=str, + required=True, + help="The Firefox version used to create the installer", +) +@CommandArgument( + "--build-number", + type=str, + required=True, + help="The release's build number", +) +@CommandArgument("--output", "-o", type=str, required=True, help="Output filename") +@CommandArgument( + "--templates", + type=str, + required=True, + help="Location of the templates used to generate the debian/ directory files", +) +@CommandArgument( + "--release-product", + type=str, + required=True, + help="The product being shipped. Used to disambiguate beta/devedition etc.", +) +def repackage_deb_l10n( + command_context, + input_xpi_file, + input_tar_file, + output, + version, + build_number, + templates, + release_product, +): + for input_file in (input_xpi_file, input_tar_file): + if not os.path.exists(input_file): + print("Input file does not exist: %s" % input_file) + return 1 + + template_dir = os.path.join( + command_context.topsrcdir, + templates, + ) + + from mozbuild.repackaging.deb import repackage_deb_l10n + + repackage_deb_l10n( + input_xpi_file, + input_tar_file, + output, + template_dir, + version, + build_number, + release_product, + ) + + +@SubCommand("repackage", "dmg", description="Repackage a tar file into a .dmg for OSX") +@CommandArgument("--input", "-i", type=str, required=True, help="Input filename") +@CommandArgument("--output", "-o", type=str, required=True, help="Output filename") +@CommandArgument( + "--attribution_sentinel", type=str, required=False, help="DMGs with attribution." +) +def repackage_dmg(command_context, input, output, attribution_sentinel): + if not os.path.exists(input): + print("Input file does not exist: %s" % input) + return 1 + + from mozbuild.repackaging.dmg import repackage_dmg + + repackage_dmg(input, output, attribution_sentinel) + + +@SubCommand("repackage", "pkg", description="Repackage a tar file into a .pkg for OSX") +@CommandArgument("--input", "-i", type=str, required=True, help="Input filename") +@CommandArgument("--output", "-o", type=str, required=True, help="Output filename") +def repackage_pkg(command_context, input, output): + if not os.path.exists(input): + print("Input file does not exist: %s" % input) + return 1 + + from mozbuild.repackaging.pkg import repackage_pkg + + repackage_pkg(input, output) + + +@SubCommand( + "repackage", "installer", description="Repackage into a Windows installer exe" +) +@CommandArgument( + "--tag", + type=str, + required=True, + help="The .tag file used to build the installer", +) +@CommandArgument( + "--setupexe", + type=str, + required=True, + help="setup.exe file inside the installer", +) +@CommandArgument( + "--package", + type=str, + required=False, + help="Optional package .zip for building a full installer", +) +@CommandArgument("--output", "-o", type=str, required=True, help="Output filename") +@CommandArgument( + "--package-name", + type=str, + required=False, + help="Name of the package being rebuilt", +) +@CommandArgument( + "--sfx-stub", type=str, required=True, help="Path to the self-extraction stub." +) +@CommandArgument( + "--use-upx", + required=False, + action="store_true", + help="Run UPX on the self-extraction stub.", +) +def repackage_installer( + command_context, + tag, + setupexe, + package, + output, + package_name, + sfx_stub, + use_upx, +): + from mozbuild.repackaging.installer import repackage_installer + + repackage_installer( + topsrcdir=command_context.topsrcdir, + tag=tag, + setupexe=setupexe, + package=package, + output=output, + package_name=package_name, + sfx_stub=sfx_stub, + use_upx=use_upx, + ) + + +@SubCommand("repackage", "msi", description="Repackage into a MSI") +@CommandArgument( + "--wsx", + type=str, + required=True, + help="The wsx file used to build the installer", +) +@CommandArgument( + "--version", + type=str, + required=True, + help="The Firefox version used to create the installer", +) +@CommandArgument( + "--locale", type=str, required=True, help="The locale of the installer" +) +@CommandArgument( + "--arch", type=str, required=True, help="The architecture you are building." +) +@CommandArgument("--setupexe", type=str, required=True, help="setup.exe installer") +@CommandArgument("--candle", type=str, required=False, help="location of candle binary") +@CommandArgument("--light", type=str, required=False, help="location of light binary") +@CommandArgument("--output", "-o", type=str, required=True, help="Output filename") +def repackage_msi( + command_context, + wsx, + version, + locale, + arch, + setupexe, + candle, + light, + output, +): + from mozbuild.repackaging.msi import repackage_msi + + repackage_msi( + topsrcdir=command_context.topsrcdir, + wsx=wsx, + version=version, + locale=locale, + arch=arch, + setupexe=setupexe, + candle=candle, + light=light, + output=output, + ) + + +@SubCommand("repackage", "msix", description="Repackage into an MSIX") +@CommandArgument( + "--input", + type=str, + help="Package (ZIP) or directory to repackage. Defaults to $OBJDIR/dist/bin", +) +@CommandArgument( + "--version", + type=str, + help="The Firefox version used to create the package " + "(Default: generated from package 'application.ini')", +) +@CommandArgument( + "--channel", + type=str, + choices=["official", "beta", "aurora", "nightly", "unofficial"], + help="Release channel.", +) +@CommandArgument( + "--distribution-dir", + metavar="DISTRIBUTION", + nargs="*", + dest="distribution_dirs", + default=[], + help="List of distribution directories to include.", +) +@CommandArgument( + "--arch", + type=str, + choices=["x86", "x86_64", "aarch64"], + help="The architecture you are building.", +) +@CommandArgument( + "--vendor", + type=str, + default="Mozilla", + required=False, + help="The vendor to use in the Package/Identity/Name string to use in the App Manifest." + + " Defaults to 'Mozilla'.", +) +@CommandArgument( + "--identity-name", + type=str, + default=None, + required=False, + help="The Package/Identity/Name string to use in the App Manifest." + + " Defaults to '<vendor>.Firefox', '<vendor>.FirefoxBeta', etc.", +) +@CommandArgument( + "--publisher", + type=str, + # This default is baked into enough places under `browser/` that we need + # not extract a constant. + default="CN=Mozilla Corporation, OU=MSIX Packaging", + required=False, + help="The Package/Identity/Publisher string to use in the App Manifest." + + " It must match the subject on the certificate used for signing.", +) +@CommandArgument( + "--publisher-display-name", + type=str, + default="Mozilla Corporation", + required=False, + help="The Package/Properties/PublisherDisplayName string to use in the App Manifest. " + + " Defaults to 'Mozilla Corporation'.", +) +@CommandArgument( + "--makeappx", + type=str, + default=None, + help="makeappx/makemsix binary name (required if you haven't run configure)", +) +@CommandArgument( + "--verbose", + default=False, + action="store_true", + help="Be verbose. (Default: false)", +) +@CommandArgument( + "--output", "-o", type=str, help="Output filename (Default: auto-generated)" +) +@CommandArgument( + "--sign", + default=False, + action="store_true", + help="Sign repackaged MSIX with self-signed certificate for local testing. " + "(Default: false)", +) +def repackage_msix( + command_context, + input, + version=None, + channel=None, + distribution_dirs=[], + arch=None, + identity_name=None, + vendor=None, + publisher=None, + publisher_display_name=None, + verbose=False, + output=None, + makeappx=None, + sign=False, +): + from mozbuild.repackaging.msix import repackage_msix + + command_context._set_log_level(verbose) + + firefox_to_msix_channel = { + "release": "official", + "beta": "beta", + "aurora": "aurora", + "nightly": "nightly", + } + + if not input: + if os.path.exists(command_context.bindir): + input = command_context.bindir + else: + command_context.log( + logging.ERROR, + "repackage-msix-no-input", + {}, + "No build found in objdir, please run ./mach build or pass --input", + ) + return 1 + + if not os.path.exists(input): + command_context.log( + logging.ERROR, + "repackage-msix-invalid-input", + {"input": input}, + "Input file or directory for msix repackaging does not exist: {input}", + ) + return 1 + + if not channel: + # Only try to guess the channel when this is clearly a local build. + if input.endswith("bin"): + channel = firefox_to_msix_channel.get( + command_context.defines.get("MOZ_UPDATE_CHANNEL"), "unofficial" + ) + else: + command_context.log( + logging.ERROR, + "repackage-msix-invalid-channel", + {}, + "Could not determine channel, please set --channel", + ) + return 1 + + if not arch: + # Only try to guess the arch when this is clearly a local build. + if input.endswith("bin"): + if command_context.substs["TARGET_CPU"] in ("x86", "x86_64", "aarch64"): + arch = command_context.substs["TARGET_CPU"] + + if not arch: + command_context.log( + logging.ERROR, + "repackage-msix-couldnt-detect-arch", + {}, + "Could not automatically detect architecture for msix repackaging. " + "Please pass --arch", + ) + return 1 + + output = repackage_msix( + input, + command_context.topsrcdir, + channel=channel, + arch=arch, + displayname=identity_name, + vendor=vendor, + publisher=publisher, + publisher_display_name=publisher_display_name, + version=version, + distribution_dirs=distribution_dirs, + # Configure this run. + force=True, + verbose=verbose, + log=command_context.log, + output=output, + makeappx=makeappx, + ) + + if sign: + repackage_sign_msix(command_context, output, force=False, verbose=verbose) + + command_context.log( + logging.INFO, + "msix", + {"output": output}, + "Wrote MSIX: {output}", + ) + + +@SubCommand("repackage", "sign-msix", description="Sign an MSIX for local testing") +@CommandArgument("--input", type=str, required=True, help="MSIX to sign.") +@CommandArgument( + "--force", + default=False, + action="store_true", + help="Force recreating self-signed certificate. (Default: false)", +) +@CommandArgument( + "--verbose", + default=False, + action="store_true", + help="Be verbose. (Default: false)", +) +def repackage_sign_msix(command_context, input, force=False, verbose=False): + from mozbuild.repackaging.msix import sign_msix + + command_context._set_log_level(verbose) + + sign_msix(input, force=force, log=command_context.log, verbose=verbose) + + return 0 + + +@SubCommand("repackage", "mar", description="Repackage into complete MAR file") +@CommandArgument("--input", "-i", type=str, required=True, help="Input filename") +@CommandArgument("--mar", type=str, required=True, help="Mar binary path") +@CommandArgument("--output", "-o", type=str, required=True, help="Output filename") +@CommandArgument( + "--arch", type=str, required=True, help="The architecture you are building." +) +@CommandArgument("--mar-channel-id", type=str, help="Mar channel id") +def repackage_mar(command_context, input, mar, output, arch, mar_channel_id): + from mozbuild.repackaging.mar import repackage_mar + + repackage_mar( + command_context.topsrcdir, + input, + mar, + output, + arch=arch, + mar_channel_id=mar_channel_id, + ) + + +@Command( + "package-multi-locale", + category="post-build", + description="Package a multi-locale version of the built product " + "for distribution as an APK, DMG, etc.", +) +@CommandArgument( + "--locales", + metavar="LOCALES", + nargs="+", + required=True, + help="List of locales to package", +) +@CommandArgument( + "--verbose", action="store_true", help="Log informative status messages." +) +def package_l10n(command_context, verbose=False, locales=[]): + if "RecursiveMake" not in command_context.substs["BUILD_BACKENDS"]: + print( + "Artifact builds do not support localization. " + "If you know what you are doing, you can use:\n" + "ac_add_options --disable-compile-environment\n" + "export BUILD_BACKENDS=FasterMake,RecursiveMake\n" + "in your mozconfig." + ) + return 1 + + locales = sorted(locale for locale in locales if locale != "en-US") + + append_env = { + # We are only (re-)packaging, we don't want to (re-)build + # anything inside Gradle. + "GRADLE_INVOKED_WITHIN_MACH_BUILD": "1", + "MOZ_CHROME_MULTILOCALE": " ".join(locales), + } + + command_context.log( + logging.INFO, + "package-multi-locale", + {"locales": locales}, + "Processing chrome Gecko resources for locales {locales}", + ) + command_context._run_make( + directory=command_context.topobjdir, + target=["chrome-{}".format(locale) for locale in locales], + append_env=append_env, + pass_thru=False, + print_directory=False, + ensure_exit_code=True, + ) + + if command_context.substs["MOZ_BUILD_APP"] == "mobile/android": + command_context.log( + logging.INFO, + "package-multi-locale", + {}, + "Invoking `mach android assemble-app`", + ) + command_context.run_process( + [ + mozpath.join(command_context.topsrcdir, "mach"), + "android", + "assemble-app", + ], + append_env=append_env, + pass_thru=True, + ensure_exit_code=True, + cwd=mozpath.join(command_context.topsrcdir), + ) + + if command_context.substs["MOZ_BUILD_APP"] == "browser": + command_context.log( + logging.INFO, "package-multi-locale", {}, "Repackaging browser" + ) + command_context._run_make( + directory=mozpath.join(command_context.topobjdir, "browser", "app"), + target=["tools"], + append_env=append_env, + pass_thru=True, + ensure_exit_code=True, + ) + + command_context.log( + logging.INFO, + "package-multi-locale", + {}, + "Invoking multi-locale `mach package`", + ) + target = ["package"] + if command_context.substs["MOZ_BUILD_APP"] == "mobile/android": + target.append("AB_CD=multi") + + command_context._run_make( + directory=command_context.topobjdir, + target=target, + append_env=append_env, + pass_thru=True, + ensure_exit_code=True, + ) + + if command_context.substs["MOZ_BUILD_APP"] == "mobile/android": + command_context.log( + logging.INFO, + "package-multi-locale", + {}, + "Invoking `mach android archive-geckoview`", + ) + command_context.run_process( + [ + mozpath.join(command_context.topsrcdir, "mach"), + "android", + "archive-geckoview", + ], + append_env=append_env, + pass_thru=True, + ensure_exit_code=True, + cwd=mozpath.join(command_context.topsrcdir), + ) + + # This is tricky: most Android build commands will regenerate the + # omnijar, producing a `res/multilocale.txt` that does not contain the + # set of locales packaged by this command. To avoid regenerating, we + # set a special environment variable. + print( + "Execute `env MOZ_CHROME_MULTILOCALE='{}' ".format( + append_env["MOZ_CHROME_MULTILOCALE"] + ) + + "mach android install-geckoview_example` " + + "to install the multi-locale geckoview_example and test APKs." + ) + + return 0 + + +def _prepend_debugger_args(args, debugger, debugger_args): + """ + Given an array with program arguments, prepend arguments to run it under a + debugger. + + :param args: The executable and arguments used to run the process normally. + :param debugger: The debugger to use, or empty to use the default debugger. + :param debugger_args: Any additional parameters to pass to the debugger. + """ + + import mozdebug + + if not debugger: + # No debugger name was provided. Look for the default ones on + # current OS. + debugger = mozdebug.get_default_debugger_name( + mozdebug.DebuggerSearch.KeepLooking + ) + + if debugger: + debuggerInfo = mozdebug.get_debugger_info(debugger, debugger_args) + + if not debugger or not debuggerInfo: + print("Could not find a suitable debugger in your PATH.") + return None + + # Parameters come from the CLI. We need to convert them before + # their use. + if debugger_args: + from mozbuild import shellutil + + try: + debugger_args = shellutil.split(debugger_args) + except shellutil.MetaCharacterException as e: + print("The --debugger_args you passed require a real shell to parse them.") + print("(We can't handle the %r character.)" % e.char) + return None + + # Prepend the debugger args. + args = [debuggerInfo.path] + debuggerInfo.args + args + return args diff --git a/python/mozbuild/mozbuild/makeutil.py b/python/mozbuild/mozbuild/makeutil.py new file mode 100644 index 0000000000..76691c5fa1 --- /dev/null +++ b/python/mozbuild/mozbuild/makeutil.py @@ -0,0 +1,209 @@ +# 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/. + +import os +import re +from collections.abc import Iterable + +import six + + +class Makefile(object): + """Provides an interface for writing simple makefiles + + Instances of this class are created, populated with rules, then + written. + """ + + def __init__(self): + self._statements = [] + + def create_rule(self, targets=()): + """ + Create a new rule in the makefile for the given targets. + Returns the corresponding Rule instance. + """ + targets = list(targets) + for target in targets: + assert isinstance(target, six.text_type) + rule = Rule(targets) + self._statements.append(rule) + return rule + + def add_statement(self, statement): + """ + Add a raw statement in the makefile. Meant to be used for + simple variable assignments. + """ + assert isinstance(statement, six.text_type) + self._statements.append(statement) + + def dump(self, fh, removal_guard=True): + """ + Dump all the rules to the given file handle. Optionally (and by + default), add guard rules for file removals (empty rules for other + rules' dependencies) + """ + all_deps = set() + all_targets = set() + for statement in self._statements: + if isinstance(statement, Rule): + statement.dump(fh) + all_deps.update(statement.dependencies()) + all_targets.update(statement.targets()) + else: + fh.write("%s\n" % statement) + if removal_guard: + guard = Rule(sorted(all_deps - all_targets)) + guard.dump(fh) + + +class _SimpleOrderedSet(object): + """ + Simple ordered set, specialized for used in Rule below only. + It doesn't expose a complete API, and normalizes path separators + at insertion. + """ + + def __init__(self): + self._list = [] + self._set = set() + + def __nonzero__(self): + return bool(self._set) + + def __bool__(self): + return bool(self._set) + + def __iter__(self): + return iter(self._list) + + def __contains__(self, key): + return key in self._set + + def update(self, iterable): + def _add(iterable): + emitted = set() + for i in iterable: + i = i.replace(os.sep, "/") + if i not in self._set and i not in emitted: + yield i + emitted.add(i) + + added = list(_add(iterable)) + self._set.update(added) + self._list.extend(added) + + +class Rule(object): + """Class handling simple rules in the form: + target1 target2 ... : dep1 dep2 ... + command1 command2 ... + """ + + def __init__(self, targets=()): + self._targets = _SimpleOrderedSet() + self._dependencies = _SimpleOrderedSet() + self._commands = [] + self.add_targets(targets) + + def add_targets(self, targets): + """Add additional targets to the rule.""" + assert isinstance(targets, Iterable) and not isinstance( + targets, six.string_types + ) + targets = list(targets) + for target in targets: + assert isinstance(target, six.text_type) + self._targets.update(targets) + return self + + def add_dependencies(self, deps): + """Add dependencies to the rule.""" + assert isinstance(deps, Iterable) and not isinstance(deps, six.string_types) + deps = list(deps) + for dep in deps: + assert isinstance(dep, six.text_type) + self._dependencies.update(deps) + return self + + def add_commands(self, commands): + """Add commands to the rule.""" + assert isinstance(commands, Iterable) and not isinstance( + commands, six.string_types + ) + commands = list(commands) + for command in commands: + assert isinstance(command, six.text_type) + self._commands.extend(commands) + return self + + def targets(self): + """Return an iterator on the rule targets.""" + # Ensure the returned iterator is actually just that, an iterator. + # Avoids caller fiddling with the set itself. + return iter(self._targets) + + def dependencies(self): + """Return an iterator on the rule dependencies.""" + return iter(d for d in self._dependencies if d not in self._targets) + + def commands(self): + """Return an iterator on the rule commands.""" + return iter(self._commands) + + def dump(self, fh): + """ + Dump the rule to the given file handle. + """ + if not self._targets: + return + fh.write("%s:" % " ".join(self._targets)) + if self._dependencies: + fh.write(" %s" % " ".join(self.dependencies())) + fh.write("\n") + for cmd in self._commands: + fh.write("\t%s\n" % cmd) + + +# colon followed by anything except a slash (Windows path detection) +_depfilesplitter = re.compile(r":(?![\\/])") + + +def read_dep_makefile(fh): + """ + Read the file handler containing a dep makefile (simple makefile only + containing dependencies) and returns an iterator of the corresponding Rules + it contains. Ignores removal guard rules. + """ + + rule = "" + for line in fh.readlines(): + line = six.ensure_text(line) + assert not line.startswith("\t") + line = line.strip() + if line.endswith("\\"): + rule += line[:-1] + else: + rule += line + split_rule = _depfilesplitter.split(rule, 1) + if len(split_rule) > 1 and split_rule[1].strip(): + yield Rule(split_rule[0].strip().split()).add_dependencies( + split_rule[1].strip().split() + ) + rule = "" + + if rule: + raise Exception("Makefile finishes with a backslash. Expected more input.") + + +def write_dep_makefile(fh, target, deps): + """ + Write a Makefile containing only target's dependencies to the file handle + specified. + """ + mk = Makefile() + rule = mk.create_rule(targets=[target]) + rule.add_dependencies(deps) + mk.dump(fh, removal_guard=True) diff --git a/python/mozbuild/mozbuild/mozconfig.py b/python/mozbuild/mozbuild/mozconfig.py new file mode 100644 index 0000000000..4322acbeed --- /dev/null +++ b/python/mozbuild/mozbuild/mozconfig.py @@ -0,0 +1,402 @@ +# 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/. + +import os +import re +import subprocess +import sys +import traceback +from pathlib import Path +from textwrap import dedent + +import six +from mozboot.mozconfig import find_mozconfig +from mozpack import path as mozpath + +MOZCONFIG_BAD_EXIT_CODE = """ +Evaluation of your mozconfig exited with an error. This could be triggered +by a command inside your mozconfig failing. Please change your mozconfig +to not error and/or to catch errors in executed commands. +""".strip() + +MOZCONFIG_BAD_OUTPUT = """ +Evaluation of your mozconfig produced unexpected output. This could be +triggered by a command inside your mozconfig failing or producing some warnings +or error messages. Please change your mozconfig to not error and/or to catch +errors in executed commands. +""".strip() + + +class MozconfigLoadException(Exception): + """Raised when a mozconfig could not be loaded properly. + + This typically indicates a malformed or misbehaving mozconfig file. + """ + + def __init__(self, path, message, output=None): + self.path = path + self.output = output + + message = ( + dedent( + """ + Error loading mozconfig: {path} + + {message} + """ + ) + .format(path=self.path, message=message) + .lstrip() + ) + + if self.output: + message += dedent( + """ + mozconfig output: + + {output} + """ + ).format(output="\n".join([six.ensure_text(s) for s in self.output])) + + Exception.__init__(self, message) + + +class MozconfigLoader(object): + """Handles loading and parsing of mozconfig files.""" + + RE_MAKE_VARIABLE = re.compile( + r""" + ^\s* # Leading whitespace + (?P<var>[a-zA-Z_0-9]+) # Variable name + \s* [?:]?= \s* # Assignment operator surrounded by optional + # spaces + (?P<value>.*$)""", # Everything else (likely the value) + re.VERBOSE, + ) + + IGNORE_SHELL_VARIABLES = {"_", "BASH_ARGV", "BASH_ARGV0", "BASH_ARGC"} + + ENVIRONMENT_VARIABLES = {"CC", "CXX", "CFLAGS", "CXXFLAGS", "LDFLAGS", "MOZ_OBJDIR"} + + AUTODETECT = object() + + def __init__(self, topsrcdir): + self.topsrcdir = topsrcdir + + @property + def _loader_script(self): + our_dir = os.path.abspath(os.path.dirname(__file__)) + + return os.path.join(our_dir, "mozconfig_loader") + + def read_mozconfig(self, path=None): + """Read the contents of a mozconfig into a data structure. + + This takes the path to a mozconfig to load. If the given path is + AUTODETECT, will try to find a mozconfig from the environment using + find_mozconfig(). + + mozconfig files are shell scripts. So, we can't just parse them. + Instead, we run the shell script in a wrapper which allows us to record + state from execution. Thus, the output from a mozconfig is a friendly + static data structure. + """ + if path is self.AUTODETECT: + path = find_mozconfig(self.topsrcdir) + if isinstance(path, Path): + path = str(path) + + result = { + "path": path, + "topobjdir": None, + "configure_args": None, + "make_flags": None, + "make_extra": None, + "env": None, + "vars": None, + } + + if path is None: + if "MOZ_OBJDIR" in os.environ: + result["topobjdir"] = os.environ["MOZ_OBJDIR"] + return result + + path = mozpath.normsep(path) + + result["configure_args"] = [] + result["make_extra"] = [] + result["make_flags"] = [] + + # Since mozconfig_loader is a shell script, running it "normally" + # actually leads to two shell executions on Windows. Avoid this by + # directly calling sh mozconfig_loader. + shell = "sh" + env = dict(os.environ) + env["PYTHONIOENCODING"] = "utf-8" + + if "MOZILLABUILD" in os.environ: + mozillabuild = os.environ["MOZILLABUILD"] + if (Path(mozillabuild) / "msys2").exists(): + shell = mozillabuild + "/msys2/usr/bin/sh" + else: + shell = mozillabuild + "/msys/bin/sh" + prefer_mozillabuild_path = [ + os.path.dirname(shell), + str(Path(mozillabuild) / "bin"), + env["PATH"], + ] + env["PATH"] = os.pathsep.join(prefer_mozillabuild_path) + if sys.platform == "win32": + shell = shell + ".exe" + + command = [ + mozpath.normsep(shell), + mozpath.normsep(self._loader_script), + mozpath.normsep(self.topsrcdir), + mozpath.normsep(path), + mozpath.normsep(sys.executable), + mozpath.join(mozpath.dirname(self._loader_script), "action", "dump_env.py"), + ] + + try: + # We need to capture stderr because that's where the shell sends + # errors if execution fails. + output = six.ensure_text( + subprocess.check_output( + command, + stderr=subprocess.STDOUT, + cwd=self.topsrcdir, + env=env, + universal_newlines=True, + encoding="utf-8", + ) + ) + except subprocess.CalledProcessError as e: + lines = e.output.splitlines() + + # Output before actual execution shouldn't be relevant. + try: + index = lines.index("------END_BEFORE_SOURCE") + lines = lines[index + 1 :] + except ValueError: + pass + + raise MozconfigLoadException(path, MOZCONFIG_BAD_EXIT_CODE, lines) + + try: + parsed = self._parse_loader_output(output) + except AssertionError: + # _parse_loader_output uses assertions to verify the + # well-formedness of the shell output; when these fail, it + # generally means there was a problem with the output, but we + # include the assertion traceback just to be sure. + print("Assertion failed in _parse_loader_output:") + traceback.print_exc() + raise MozconfigLoadException( + path, MOZCONFIG_BAD_OUTPUT, output.splitlines() + ) + + def diff_vars(vars_before, vars_after): + set1 = set(vars_before.keys()) - self.IGNORE_SHELL_VARIABLES + set2 = set(vars_after.keys()) - self.IGNORE_SHELL_VARIABLES + added = set2 - set1 + removed = set1 - set2 + maybe_modified = set1 & set2 + changed = {"added": {}, "removed": {}, "modified": {}, "unmodified": {}} + + for key in added: + changed["added"][key] = vars_after[key] + + for key in removed: + changed["removed"][key] = vars_before[key] + + for key in maybe_modified: + if vars_before[key] != vars_after[key]: + changed["modified"][key] = (vars_before[key], vars_after[key]) + elif key in self.ENVIRONMENT_VARIABLES: + # In order for irrelevant environment variable changes not + # to incur in re-running configure, only a set of + # environment variables are stored when they are + # unmodified. Otherwise, changes such as using a different + # terminal window, or even rebooting, would trigger + # reconfigures. + changed["unmodified"][key] = vars_after[key] + + return changed + + result["env"] = diff_vars(parsed["env_before"], parsed["env_after"]) + + # Environment variables also appear as shell variables, but that's + # uninteresting duplication of information. Filter them out. + def filt(x, y): + return {k: v for k, v in x.items() if k not in y} + + result["vars"] = diff_vars( + filt(parsed["vars_before"], parsed["env_before"]), + filt(parsed["vars_after"], parsed["env_after"]), + ) + + result["configure_args"] = [self._expand(o) for o in parsed["ac"]] + + if "MOZ_OBJDIR" in parsed["env_before"]: + result["topobjdir"] = parsed["env_before"]["MOZ_OBJDIR"] + + mk = [self._expand(o) for o in parsed["mk"]] + + for o in mk: + match = self.RE_MAKE_VARIABLE.match(o) + + if match is None: + result["make_extra"].append(o) + continue + + name, value = match.group("var"), match.group("value") + + if name == "MOZ_MAKE_FLAGS": + result["make_flags"] = value.split() + continue + + if name == "MOZ_OBJDIR": + result["topobjdir"] = value + if parsed["env_before"].get("MOZ_PROFILE_GENERATE") == "1": + # If MOZ_OBJDIR is specified in the mozconfig, we need to + # make sure that the '/instrumented' directory gets appended + # for the first build to avoid an objdir mismatch when + # running 'mach package' on Windows. + result["topobjdir"] = mozpath.join( + result["topobjdir"], "instrumented" + ) + continue + + result["make_extra"].append(o) + + return result + + def _parse_loader_output(self, output): + mk_options = [] + ac_options = [] + before_source = {} + after_source = {} + env_before_source = {} + env_after_source = {} + + current = None + current_type = None + in_variable = None + + for line in output.splitlines(): + if not line: + continue + + if line.startswith("------BEGIN_"): + assert current_type is None + assert current is None + assert not in_variable + current_type = line[len("------BEGIN_") :] + current = [] + continue + + if line.startswith("------END_"): + assert not in_variable + section = line[len("------END_") :] + assert current_type == section + + if current_type == "AC_OPTION": + ac_options.append("\n".join(current)) + elif current_type == "MK_OPTION": + mk_options.append("\n".join(current)) + + current = None + current_type = None + continue + + assert current_type is not None + + vars_mapping = { + "BEFORE_SOURCE": before_source, + "AFTER_SOURCE": after_source, + "ENV_BEFORE_SOURCE": env_before_source, + "ENV_AFTER_SOURCE": env_after_source, + } + + if current_type in vars_mapping: + # mozconfigs are sourced using the Bourne shell (or at least + # in Bourne shell mode). This means |set| simply lists + # variables from the current shell (not functions). (Note that + # if Bash is installed in /bin/sh it acts like regular Bourne + # and doesn't print functions.) So, lines should have the + # form: + # + # key='value' + # key=value + # + # The only complication is multi-line variables. Those have the + # form: + # + # key='first + # second' + + # TODO Bug 818377 Properly handle multi-line variables of form: + # $ foo="a='b' + # c='d'" + # $ set + # foo='a='"'"'b'"'"' + # c='"'"'d'"'" + + name = in_variable + value = None + if in_variable: + # Reached the end of a multi-line variable. + if line.endswith("'") and not line.endswith("\\'"): + current.append(line[:-1]) + value = "\n".join(current) + in_variable = None + else: + current.append(line) + continue + else: + equal_pos = line.find("=") + + if equal_pos < 1: + # TODO log warning? + continue + + name = line[0:equal_pos] + value = line[equal_pos + 1 :] + + if len(value): + has_quote = value[0] == "'" + + if has_quote: + value = value[1:] + + # Lines with a quote not ending in a quote are multi-line. + if has_quote and not value.endswith("'"): + in_variable = name + current.append(value) + continue + else: + value = value[:-1] if has_quote else value + + assert name is not None + + vars_mapping[current_type][name] = value + + current = [] + + continue + + current.append(line) + + return { + "mk": mk_options, + "ac": ac_options, + "vars_before": before_source, + "vars_after": after_source, + "env_before": env_before_source, + "env_after": env_after_source, + } + + def _expand(self, s): + return s.replace("@TOPSRCDIR@", self.topsrcdir) diff --git a/python/mozbuild/mozbuild/mozconfig_loader b/python/mozbuild/mozbuild/mozconfig_loader new file mode 100755 index 0000000000..29355c69a2 --- /dev/null +++ b/python/mozbuild/mozbuild/mozconfig_loader @@ -0,0 +1,48 @@ +#!/bin/sh +# 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/. + +# This script provides an execution environment for mozconfig scripts. +# This script is not meant to be called by users. Instead, some +# higher-level driver invokes it and parses the machine-tailored output. + +set -e + +ac_add_options() { + for _mozconfig_opt; do + echo "------BEGIN_AC_OPTION" + echo $_mozconfig_opt + echo "------END_AC_OPTION" + done +} + +mk_add_options() { + for _mozconfig_opt; do + echo "------BEGIN_MK_OPTION" + echo $_mozconfig_opt + echo "------END_MK_OPTION" + done +} + +echo "------BEGIN_ENV_BEFORE_SOURCE" +"$3" "$4" +echo "------END_ENV_BEFORE_SOURCE" + +echo "------BEGIN_BEFORE_SOURCE" +set +echo "------END_BEFORE_SOURCE" + +topsrcdir="$1" + +. "$2" + +unset topsrcdir + +echo "------BEGIN_AFTER_SOURCE" +set +echo "------END_AFTER_SOURCE" + +echo "------BEGIN_ENV_AFTER_SOURCE" +"$3" "$4" +echo "------END_ENV_AFTER_SOURCE" diff --git a/python/mozbuild/mozbuild/mozinfo.py b/python/mozbuild/mozbuild/mozinfo.py new file mode 100644 index 0000000000..b640c5f1d0 --- /dev/null +++ b/python/mozbuild/mozbuild/mozinfo.py @@ -0,0 +1,168 @@ +# 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/. + +# This module produces a JSON file that provides basic build info and +# configuration metadata. + +import json +import os +import re + +import six + + +def build_dict(config, env=os.environ): + """ + Build a dict containing data about the build configuration from + the environment. + """ + substs = config.substs + + # Check that all required variables are present first. + required = ["TARGET_CPU", "OS_TARGET"] + missing = [r for r in required if r not in substs] + if missing: + raise Exception( + "Missing required environment variables: %s" % ", ".join(missing) + ) + + d = {} + d["topsrcdir"] = config.topsrcdir + d["topobjdir"] = config.topobjdir + + if config.mozconfig: + d["mozconfig"] = config.mozconfig + + # os + o = substs["OS_TARGET"] + known_os = {"Linux": "linux", "WINNT": "win", "Darwin": "mac", "Android": "android"} + if o in known_os: + d["os"] = known_os[o] + else: + # Allow unknown values, just lowercase them. + d["os"] = o.lower() + + # Widget toolkit, just pass the value directly through. + d["toolkit"] = substs.get("MOZ_WIDGET_TOOLKIT") + + # Application name + if "MOZ_APP_NAME" in substs: + d["appname"] = substs["MOZ_APP_NAME"] + + # Build app name + if "MOZ_BUILD_APP" in substs: + d["buildapp"] = substs["MOZ_BUILD_APP"] + + # processor + p = substs["TARGET_CPU"] + # do some slight massaging for some values + # TODO: retain specific values in case someone wants them? + if p.startswith("arm"): + p = "arm" + elif re.match("i[3-9]86", p): + p = "x86" + d["processor"] = p + # hardcoded list of 64-bit CPUs + if p in ["x86_64", "ppc64", "aarch64"]: + d["bits"] = 64 + # hardcoded list of known 32-bit CPUs + elif p in ["x86", "arm", "ppc"]: + d["bits"] = 32 + # other CPUs will wind up with unknown bits + + d["debug"] = substs.get("MOZ_DEBUG") == "1" + d["nightly_build"] = substs.get("NIGHTLY_BUILD") == "1" + d["early_beta_or_earlier"] = substs.get("EARLY_BETA_OR_EARLIER") == "1" + d["release_or_beta"] = substs.get("RELEASE_OR_BETA") == "1" + d["devedition"] = substs.get("MOZ_DEV_EDITION") == "1" + d["pgo"] = substs.get("MOZ_PGO") == "1" + d["crashreporter"] = bool(substs.get("MOZ_CRASHREPORTER")) + d["normandy"] = substs.get("MOZ_NORMANDY") == "1" + d["datareporting"] = bool(substs.get("MOZ_DATA_REPORTING")) + d["healthreport"] = substs.get("MOZ_SERVICES_HEALTHREPORT") == "1" + d["sync"] = substs.get("MOZ_SERVICES_SYNC") == "1" + # FIXME(emilio): We need to update a lot of WPT expectations before removing this. + d["stylo"] = True + d["asan"] = substs.get("MOZ_ASAN") == "1" + d["tsan"] = substs.get("MOZ_TSAN") == "1" + d["ubsan"] = substs.get("MOZ_UBSAN") == "1" + d["telemetry"] = substs.get("MOZ_TELEMETRY_REPORTING") == "1" + d["tests_enabled"] = substs.get("ENABLE_TESTS") == "1" + d["bin_suffix"] = substs.get("BIN_SUFFIX", "") + d["require_signing"] = substs.get("MOZ_REQUIRE_SIGNING") == "1" + d["official"] = bool(substs.get("MOZILLA_OFFICIAL")) + d["updater"] = substs.get("MOZ_UPDATER") == "1" + d["artifact"] = substs.get("MOZ_ARTIFACT_BUILDS") == "1" + d["ccov"] = substs.get("MOZ_CODE_COVERAGE") == "1" + d["cc_type"] = substs.get("CC_TYPE") + d["domstreams"] = substs.get("MOZ_DOM_STREAMS") == "1" + d["isolated_process"] = ( + substs.get("MOZ_ANDROID_CONTENT_SERVICE_ISOLATED_PROCESS") == "1" + ) + + def guess_platform(): + if d["buildapp"] == "browser": + p = d["os"] + if p == "mac": + p = "macosx64" + elif d["bits"] == 64: + p = "{}64".format(p) + elif p in ("win",): + p = "{}32".format(p) + + if d["asan"]: + p = "{}-asan".format(p) + + return p + + if d["buildapp"] == "mobile/android": + if d["processor"] == "x86": + return "android-x86" + if d["processor"] == "x86_64": + return "android-x86_64" + if d["processor"] == "aarch64": + return "android-aarch64" + return "android-arm" + + def guess_buildtype(): + if d["asan"]: + return "asan" + if d["tsan"]: + return "tsan" + if d["ccov"]: + return "ccov" + if d["debug"]: + return "debug" + if d["pgo"]: + return "pgo" + return "opt" + + # if buildapp or bits are unknown, we don't have a configuration similar to + # any in automation and the guesses are useless. + if "buildapp" in d and (d["os"] == "mac" or "bits" in d): + d["platform_guess"] = guess_platform() + d["buildtype_guess"] = guess_buildtype() + d["buildtype"] = guess_buildtype() + + if ( + d.get("buildapp", "") == "mobile/android" + and "MOZ_ANDROID_MIN_SDK_VERSION" in substs + ): + d["android_min_sdk"] = substs["MOZ_ANDROID_MIN_SDK_VERSION"] + + return d + + +def write_mozinfo(file, config, env=os.environ): + """Write JSON data about the configuration specified in config and an + environment variable dict to ``|file|``, which may be a filename or file-like + object. + See build_dict for information about what environment variables are used, + and what keys are produced. + """ + build_conf = build_dict(config, env) + if isinstance(file, six.text_type): + file = open(file, "wt") + + json.dump(build_conf, file, sort_keys=True, indent=4) diff --git a/python/mozbuild/mozbuild/nodeutil.py b/python/mozbuild/mozbuild/nodeutil.py new file mode 100644 index 0000000000..42f2627cd9 --- /dev/null +++ b/python/mozbuild/mozbuild/nodeutil.py @@ -0,0 +1,126 @@ +# 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/. + +import os +import platform +import subprocess + +from mozboot.util import get_tools_dir +from mozfile import which +from packaging.version import Version +from six import PY3 + +NODE_MIN_VERSION = Version("12.22.12") +NPM_MIN_VERSION = Version("6.14.16") + + +def find_node_paths(): + """Determines the possible paths for node executables. + + Returns a list of paths, which includes the build state directory. + """ + mozbuild_tools_dir = get_tools_dir() + + if platform.system() == "Windows": + mozbuild_node_path = os.path.join(mozbuild_tools_dir, "node") + else: + mozbuild_node_path = os.path.join(mozbuild_tools_dir, "node", "bin") + + # We still fallback to the PATH, since on OSes that don't have toolchain + # artifacts available to download, Node may be coming from $PATH. + paths = [mozbuild_node_path] + os.environ.get("PATH").split(os.pathsep) + + if platform.system() == "Windows": + paths += [ + "%s\\nodejs" % os.environ.get("SystemDrive"), + os.path.join(os.environ.get("ProgramFiles"), "nodejs"), + os.path.join(os.environ.get("PROGRAMW6432"), "nodejs"), + os.path.join(os.environ.get("PROGRAMFILES"), "nodejs"), + ] + + return paths + + +def check_executable_version(exe, wrap_call_with_node=False): + """Determine the version of a Node executable by invoking it. + + May raise ``subprocess.CalledProcessError`` or ``ValueError`` on failure. + """ + out = None + # npm may be a script (Except on Windows), so we must call it with node. + if wrap_call_with_node and platform.system() != "Windows": + binary, _ = find_node_executable() + if binary: + out = ( + subprocess.check_output( + [binary, exe, "--version"], universal_newlines=PY3 + ) + .lstrip("v") + .rstrip() + ) + + # If we can't find node, or we don't need to wrap it, fallback to calling + # direct. + if not out: + out = ( + subprocess.check_output([exe, "--version"], universal_newlines=PY3) + .lstrip("v") + .rstrip() + ) + return Version(out) + + +def find_node_executable( + nodejs_exe=os.environ.get("NODEJS"), min_version=NODE_MIN_VERSION +): + """Find a Node executable from the mozbuild directory. + + Returns a tuple containing the the path to an executable binary and a + version tuple. Both tuple entries will be None if a Node executable + could not be resolved. + """ + if nodejs_exe: + try: + version = check_executable_version(nodejs_exe) + except (subprocess.CalledProcessError, ValueError): + return None, None + + if version >= min_version: + return nodejs_exe, version.release + + return None, None + + # "nodejs" is first in the tuple on the assumption that it's only likely to + # exist on systems (probably linux distros) where there is a program in the path + # called "node" that does something else. + return find_executable("node", min_version) + + +def find_npm_executable(min_version=NPM_MIN_VERSION): + """Find a Node executable from the mozbuild directory. + + Returns a tuple containing the the path to an executable binary and a + version tuple. Both tuple entries will be None if a Node executable + could not be resolved. + """ + return find_executable("npm", min_version, True) + + +def find_executable(name, min_version, use_node_for_version_check=False): + paths = find_node_paths() + exe = which(name, path=paths) + + if not exe: + return None, None + + # Verify we can invoke the executable and its version is acceptable. + try: + version = check_executable_version(exe, use_node_for_version_check) + except (subprocess.CalledProcessError, ValueError): + return None, None + + if version < min_version: + return None, None + + return exe, version.release diff --git a/python/mozbuild/mozbuild/preprocessor.py b/python/mozbuild/mozbuild/preprocessor.py new file mode 100644 index 0000000000..c81357efa4 --- /dev/null +++ b/python/mozbuild/mozbuild/preprocessor.py @@ -0,0 +1,938 @@ +# 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/. +r""" +This is a very primitive line based preprocessor, for times when using +a C preprocessor isn't an option. + +It currently supports the following grammar for expressions, whitespace is +ignored: + +expression : + and_cond ( '||' expression ) ? ; +and_cond: + test ( '&&' and_cond ) ? ; +test: + unary ( ( '==' | '!=' ) unary ) ? ; +unary : + '!'? value ; +value : + [0-9]+ # integer + | 'defined(' \w+ ')' + | \w+ # string identifier or value; +""" + +import errno +import io +import os +import re +import sys +from optparse import OptionParser + +import six +from mozpack.path import normsep + +from mozbuild.makeutil import Makefile + +# hack around win32 mangling our line endings +# http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/65443 +if sys.platform == "win32": + import msvcrt + + msvcrt.setmode(sys.stdout.fileno(), os.O_BINARY) + os.linesep = "\n" + + +__all__ = ["Context", "Expression", "Preprocessor", "preprocess"] + + +def _to_text(a): + # We end up converting a lot of different types (text_type, binary_type, + # int, etc.) to Unicode in this script. This function handles all of those + # possibilities. + if isinstance(a, (six.text_type, six.binary_type)): + return six.ensure_text(a) + return six.text_type(a) + + +def path_starts_with(path, prefix): + if os.altsep: + prefix = prefix.replace(os.altsep, os.sep) + path = path.replace(os.altsep, os.sep) + prefix = [os.path.normcase(p) for p in prefix.split(os.sep)] + path = [os.path.normcase(p) for p in path.split(os.sep)] + return path[: len(prefix)] == prefix + + +class Expression: + def __init__(self, expression_string): + """ + Create a new expression with this string. + The expression will already be parsed into an Abstract Syntax Tree. + """ + self.content = expression_string + self.offset = 0 + self.__ignore_whitespace() + self.e = self.__get_logical_or() + if self.content: + raise Expression.ParseError(self) + + def __get_logical_or(self): + """ + Production: and_cond ( '||' expression ) ? + """ + if not len(self.content): + return None + rv = Expression.__AST("logical_op") + # test + rv.append(self.__get_logical_and()) + self.__ignore_whitespace() + if self.content[:2] != "||": + # no logical op needed, short cut to our prime element + return rv[0] + # append operator + rv.append(Expression.__ASTLeaf("op", self.content[:2])) + self.__strip(2) + self.__ignore_whitespace() + rv.append(self.__get_logical_or()) + self.__ignore_whitespace() + return rv + + def __get_logical_and(self): + """ + Production: test ( '&&' and_cond ) ? + """ + if not len(self.content): + return None + rv = Expression.__AST("logical_op") + # test + rv.append(self.__get_equality()) + self.__ignore_whitespace() + if self.content[:2] != "&&": + # no logical op needed, short cut to our prime element + return rv[0] + # append operator + rv.append(Expression.__ASTLeaf("op", self.content[:2])) + self.__strip(2) + self.__ignore_whitespace() + rv.append(self.__get_logical_and()) + self.__ignore_whitespace() + return rv + + def __get_equality(self): + """ + Production: unary ( ( '==' | '!=' ) unary ) ? + """ + if not len(self.content): + return None + rv = Expression.__AST("equality") + # unary + rv.append(self.__get_unary()) + self.__ignore_whitespace() + if not re.match("[=!]=", self.content): + # no equality needed, short cut to our prime unary + return rv[0] + # append operator + rv.append(Expression.__ASTLeaf("op", self.content[:2])) + self.__strip(2) + self.__ignore_whitespace() + rv.append(self.__get_unary()) + self.__ignore_whitespace() + return rv + + def __get_unary(self): + """ + Production: '!'? value + """ + # eat whitespace right away, too + not_ws = re.match(r"!\s*", self.content) + if not not_ws: + return self.__get_value() + rv = Expression.__AST("not") + self.__strip(not_ws.end()) + rv.append(self.__get_value()) + self.__ignore_whitespace() + return rv + + def __get_value(self): + r""" + Production: ( [0-9]+ | 'defined(' \w+ ')' | \w+ ) + Note that the order is important, and the expression is kind-of + ambiguous as \w includes 0-9. One could make it unambiguous by + removing 0-9 from the first char of a string literal. + """ + rv = None + m = re.match(r"defined\s*\(\s*(\w+)\s*\)", self.content) + if m: + word_len = m.end() + rv = Expression.__ASTLeaf("defined", m.group(1)) + else: + word_len = re.match("[0-9]*", self.content).end() + if word_len: + value = int(self.content[:word_len]) + rv = Expression.__ASTLeaf("int", value) + else: + word_len = re.match(r"\w*", self.content).end() + if word_len: + rv = Expression.__ASTLeaf("string", self.content[:word_len]) + else: + raise Expression.ParseError(self) + self.__strip(word_len) + self.__ignore_whitespace() + return rv + + def __ignore_whitespace(self): + ws_len = re.match(r"\s*", self.content).end() + self.__strip(ws_len) + return + + def __strip(self, length): + """ + Remove a given amount of chars from the input and update + the offset. + """ + self.content = self.content[length:] + self.offset += length + + def evaluate(self, context): + """ + Evaluate the expression with the given context + """ + + # Helper function to evaluate __get_equality results + def eval_equality(tok): + left = opmap[tok[0].type](tok[0]) + right = opmap[tok[2].type](tok[2]) + rv = left == right + if tok[1].value == "!=": + rv = not rv + return rv + + # Helper function to evaluate __get_logical_and and __get_logical_or results + def eval_logical_op(tok): + left = opmap[tok[0].type](tok[0]) + right = opmap[tok[2].type](tok[2]) + if tok[1].value == "&&": + return left and right + elif tok[1].value == "||": + return left or right + raise Expression.ParseError(self) + + # Mapping from token types to evaluator functions + # Apart from (non-)equality, all these can be simple lambda forms. + opmap = { + "logical_op": eval_logical_op, + "equality": eval_equality, + "not": lambda tok: not opmap[tok[0].type](tok[0]), + "string": lambda tok: context[tok.value], + "defined": lambda tok: tok.value in context, + "int": lambda tok: tok.value, + } + + return opmap[self.e.type](self.e) + + class __AST(list): + """ + Internal class implementing Abstract Syntax Tree nodes + """ + + def __init__(self, type): + self.type = type + super(self.__class__, self).__init__(self) + + class __ASTLeaf: + """ + Internal class implementing Abstract Syntax Tree leafs + """ + + def __init__(self, type, value): + self.value = value + self.type = type + + def __str__(self): + return self.value.__str__() + + def __repr__(self): + return self.value.__repr__() + + class ParseError(Exception): + """ + Error raised when parsing fails. + It has two members, offset and content, which give the offset of the + error and the offending content. + """ + + def __init__(self, expression): + self.offset = expression.offset + self.content = expression.content[:3] + + def __str__(self): + return 'Unexpected content at offset {0}, "{1}"'.format( + self.offset, self.content + ) + + +class Context(dict): + """ + This class holds variable values by subclassing dict, and while it + truthfully reports True and False on + + name in context + + it returns the variable name itself on + + context["name"] + + to reflect the ambiguity between string literals and preprocessor + variables. + """ + + def __getitem__(self, key): + if key in self: + return super(self.__class__, self).__getitem__(key) + return key + + +class Preprocessor: + """ + Class for preprocessing text files. + """ + + class Error(RuntimeError): + def __init__(self, cpp, MSG, context): + self.file = cpp.context["FILE"] + self.line = cpp.context["LINE"] + self.key = MSG + RuntimeError.__init__(self, (self.file, self.line, self.key, context)) + + def __init__(self, defines=None, marker="#"): + self.context = Context() + self.context.update({"FILE": "", "LINE": 0, "DIRECTORY": os.path.abspath(".")}) + try: + # Can import globally because of bootstrapping issues. + from buildconfig import topobjdir, topsrcdir + except ImportError: + # Allow this script to still work independently of a configured objdir. + topsrcdir = topobjdir = None + self.topsrcdir = topsrcdir + self.topobjdir = topobjdir + self.curdir = "." + self.actionLevel = 0 + self.disableLevel = 0 + # ifStates can be + # 0: hadTrue + # 1: wantsTrue + # 2: #else found + self.ifStates = [] + self.checkLineNumbers = False + + # A list of (filter_name, filter_function) pairs. + self.filters = [] + + self.cmds = {} + for cmd, level in ( + ("define", 0), + ("undef", 0), + ("if", sys.maxsize), + ("ifdef", sys.maxsize), + ("ifndef", sys.maxsize), + ("else", 1), + ("elif", 1), + ("elifdef", 1), + ("elifndef", 1), + ("endif", sys.maxsize), + ("expand", 0), + ("literal", 0), + ("filter", 0), + ("unfilter", 0), + ("include", 0), + ("includesubst", 0), + ("error", 0), + ): + self.cmds[cmd] = (level, getattr(self, "do_" + cmd)) + self.out = sys.stdout + self.setMarker(marker) + self.varsubst = re.compile(r"@(?P<VAR>\w+)@", re.U) + self.includes = set() + self.silenceMissingDirectiveWarnings = False + if defines: + self.context.update(defines) + + def failUnused(self, file): + msg = None + if self.actionLevel == 0 and not self.silenceMissingDirectiveWarnings: + msg = "no preprocessor directives found" + elif self.actionLevel == 1: + msg = "no useful preprocessor directives found" + if msg: + + class Fake(object): + pass + + fake = Fake() + fake.context = { + "FILE": file, + "LINE": None, + } + raise Preprocessor.Error(fake, msg, None) + + def setMarker(self, aMarker): + """ + Set the marker to be used for processing directives. + Used for handling CSS files, with pp.setMarker('%'), for example. + The given marker may be None, in which case no markers are processed. + """ + self.marker = aMarker + if aMarker: + instruction_prefix = r"\s*{0}" + instruction_cmd = r"(?P<cmd>[a-z]+)(?:\s+(?P<args>.*?))?\s*$" + instruction_fmt = instruction_prefix + instruction_cmd + ambiguous_fmt = instruction_prefix + r"\s+" + instruction_cmd + + self.instruction = re.compile(instruction_fmt.format(aMarker)) + self.comment = re.compile(aMarker, re.U) + self.ambiguous_comment = re.compile(ambiguous_fmt.format(aMarker)) + else: + + class NoMatch(object): + def match(self, *args): + return False + + self.instruction = self.comment = NoMatch() + + def setSilenceDirectiveWarnings(self, value): + """ + Sets whether missing directive warnings are silenced, according to + ``value``. The default behavior of the preprocessor is to emit + such warnings. + """ + self.silenceMissingDirectiveWarnings = value + + def addDefines(self, defines): + """ + Adds the specified defines to the preprocessor. + ``defines`` may be a dictionary object or an iterable of key/value pairs + (as tuples or other iterables of length two) + """ + self.context.update(defines) + + def clone(self): + """ + Create a clone of the current processor, including line ending + settings, marker, variable definitions, output stream. + """ + rv = Preprocessor() + rv.context.update(self.context) + rv.setMarker(self.marker) + rv.out = self.out + return rv + + def processFile(self, input, output, depfile=None): + """ + Preprocesses the contents of the ``input`` stream and writes the result + to the ``output`` stream. If ``depfile`` is set, the dependencies of + ``output`` file are written to ``depfile`` in Makefile format. + """ + self.out = output + + self.do_include(input, False) + self.failUnused(input.name) + + if depfile: + mk = Makefile() + mk.create_rule([output.name]).add_dependencies(self.includes) + mk.dump(depfile) + + def computeDependencies(self, input): + """ + Reads the ``input`` stream, and computes the dependencies for that input. + """ + try: + old_out = self.out + self.out = None + self.do_include(input, False) + + return self.includes + finally: + self.out = old_out + + def applyFilters(self, aLine): + for f in self.filters: + aLine = f[1](aLine) + return aLine + + def noteLineInfo(self): + # Record the current line and file. Called once before transitioning + # into or out of an included file and after writing each line. + self.line_info = self.context["FILE"], self.context["LINE"] + + def write(self, aLine): + """ + Internal method for handling output. + """ + if not self.out: + return + + next_line, next_file = self.context["LINE"], self.context["FILE"] + if self.checkLineNumbers: + expected_file, expected_line = self.line_info + expected_line += 1 + if ( + expected_line != next_line + or expected_file + and expected_file != next_file + ): + self.out.write( + '//@line {line} "{file}"\n'.format(line=next_line, file=next_file) + ) + self.noteLineInfo() + + filteredLine = self.applyFilters(aLine) + if filteredLine != aLine: + self.actionLevel = 2 + self.out.write(filteredLine) + + def handleCommandLine(self, args, defaultToStdin=False): + """ + Parse a commandline into this parser. + Uses OptionParser internally, no args mean sys.argv[1:]. + """ + + def get_output_file(path, encoding=None): + if encoding is None: + encoding = "utf-8" + dir = os.path.dirname(path) + if dir: + try: + os.makedirs(dir) + except OSError as error: + if error.errno != errno.EEXIST: + raise + return io.open(path, "w", encoding=encoding, newline="\n") + + p = self.getCommandLineParser() + options, args = p.parse_args(args=args) + out = self.out + depfile = None + + if options.output: + out = get_output_file(options.output, options.output_encoding) + elif options.output_encoding: + raise Preprocessor.Error( + self, "--output-encoding doesn't work without --output", None + ) + if defaultToStdin and len(args) == 0: + args = [sys.stdin] + if options.depend: + raise Preprocessor.Error(self, "--depend doesn't work with stdin", None) + if options.depend: + if not options.output: + raise Preprocessor.Error( + self, "--depend doesn't work with stdout", None + ) + depfile = get_output_file(options.depend) + + if args: + for f in args: + if not isinstance(f, io.TextIOBase): + f = io.open(f, "r", encoding="utf-8") + with f as input_: + self.processFile(input=input_, output=out) + if depfile: + mk = Makefile() + mk.create_rule([six.ensure_text(options.output)]).add_dependencies( + self.includes + ) + mk.dump(depfile) + depfile.close() + + if options.output: + out.close() + + def getCommandLineParser(self, unescapeDefines=False): + escapedValue = re.compile('".*"$') + numberValue = re.compile(r"\d+$") + + def handleD(option, opt, value, parser): + vals = value.split("=", 1) + if len(vals) == 1: + vals.append(1) + elif unescapeDefines and escapedValue.match(vals[1]): + # strip escaped string values + vals[1] = vals[1][1:-1] + elif numberValue.match(vals[1]): + vals[1] = int(vals[1]) + self.context[vals[0]] = vals[1] + + def handleU(option, opt, value, parser): + del self.context[value] + + def handleF(option, opt, value, parser): + self.do_filter(value) + + def handleMarker(option, opt, value, parser): + self.setMarker(value) + + def handleSilenceDirectiveWarnings(option, opt, value, parse): + self.setSilenceDirectiveWarnings(True) + + p = OptionParser() + p.add_option( + "-D", + action="callback", + callback=handleD, + type="string", + metavar="VAR[=VAL]", + help="Define a variable", + ) + p.add_option( + "-U", + action="callback", + callback=handleU, + type="string", + metavar="VAR", + help="Undefine a variable", + ) + p.add_option( + "-F", + action="callback", + callback=handleF, + type="string", + metavar="FILTER", + help="Enable the specified filter", + ) + p.add_option( + "-o", + "--output", + type="string", + default=None, + metavar="FILENAME", + help="Output to the specified file instead of stdout", + ) + p.add_option( + "--depend", + type="string", + default=None, + metavar="FILENAME", + help="Generate dependencies in the given file", + ) + p.add_option( + "--marker", + action="callback", + callback=handleMarker, + type="string", + help="Use the specified marker instead of #", + ) + p.add_option( + "--silence-missing-directive-warnings", + action="callback", + callback=handleSilenceDirectiveWarnings, + help="Don't emit warnings about missing directives", + ) + p.add_option( + "--output-encoding", + type="string", + default=None, + metavar="ENCODING", + help="Encoding to use for the output", + ) + return p + + def handleLine(self, aLine): + """ + Handle a single line of input (internal). + """ + if self.actionLevel == 0 and self.comment.match(aLine): + self.actionLevel = 1 + m = self.instruction.match(aLine) + if m: + args = None + cmd = m.group("cmd") + try: + args = m.group("args") + except IndexError: + pass + if cmd not in self.cmds: + raise Preprocessor.Error(self, "INVALID_CMD", aLine) + level, cmd = self.cmds[cmd] + if level >= self.disableLevel: + cmd(args) + if cmd != "literal": + self.actionLevel = 2 + elif self.disableLevel == 0: + if self.comment.match(aLine): + # make sure the comment is not ambiguous with a command + m = self.ambiguous_comment.match(aLine) + if m: + cmd = m.group("cmd") + if cmd in self.cmds: + raise Preprocessor.Error(self, "AMBIGUOUS_COMMENT", aLine) + else: + self.write(aLine) + + # Instruction handlers + # These are named do_'instruction name' and take one argument + + # Variables + def do_define(self, args): + m = re.match(r"(?P<name>\w+)(?:\s(?P<value>.*))?", args, re.U) + if not m: + raise Preprocessor.Error(self, "SYNTAX_DEF", args) + val = "" + if m.group("value"): + val = self.applyFilters(m.group("value")) + try: + val = int(val) + except Exception: + pass + self.context[m.group("name")] = val + + def do_undef(self, args): + m = re.match(r"(?P<name>\w+)$", args, re.U) + if not m: + raise Preprocessor.Error(self, "SYNTAX_DEF", args) + if args in self.context: + del self.context[args] + + # Logic + def ensure_not_else(self): + if len(self.ifStates) == 0 or self.ifStates[-1] == 2: + sys.stderr.write( + "WARNING: bad nesting of #else in %s\n" % self.context["FILE"] + ) + + def do_if(self, args, replace=False): + if self.disableLevel and not replace: + self.disableLevel += 1 + return + val = None + try: + e = Expression(args) + val = e.evaluate(self.context) + except Exception: + # XXX do real error reporting + raise Preprocessor.Error(self, "SYNTAX_ERR", args) + if isinstance(val, six.text_type) or isinstance(val, six.binary_type): + # we're looking for a number value, strings are false + val = False + if not val: + self.disableLevel = 1 + if replace: + if val: + self.disableLevel = 0 + self.ifStates[-1] = self.disableLevel + else: + self.ifStates.append(self.disableLevel) + + def do_ifdef(self, args, replace=False): + if self.disableLevel and not replace: + self.disableLevel += 1 + return + if re.search(r"\W", args, re.U): + raise Preprocessor.Error(self, "INVALID_VAR", args) + if args not in self.context: + self.disableLevel = 1 + if replace: + if args in self.context: + self.disableLevel = 0 + self.ifStates[-1] = self.disableLevel + else: + self.ifStates.append(self.disableLevel) + + def do_ifndef(self, args, replace=False): + if self.disableLevel and not replace: + self.disableLevel += 1 + return + if re.search(r"\W", args, re.U): + raise Preprocessor.Error(self, "INVALID_VAR", args) + if args in self.context: + self.disableLevel = 1 + if replace: + if args not in self.context: + self.disableLevel = 0 + self.ifStates[-1] = self.disableLevel + else: + self.ifStates.append(self.disableLevel) + + def do_else(self, args, ifState=2): + self.ensure_not_else() + hadTrue = self.ifStates[-1] == 0 + self.ifStates[-1] = ifState # in-else + if hadTrue: + self.disableLevel = 1 + return + self.disableLevel = 0 + + def do_elif(self, args): + if self.disableLevel == 1: + if self.ifStates[-1] == 1: + self.do_if(args, replace=True) + else: + self.do_else(None, self.ifStates[-1]) + + def do_elifdef(self, args): + if self.disableLevel == 1: + if self.ifStates[-1] == 1: + self.do_ifdef(args, replace=True) + else: + self.do_else(None, self.ifStates[-1]) + + def do_elifndef(self, args): + if self.disableLevel == 1: + if self.ifStates[-1] == 1: + self.do_ifndef(args, replace=True) + else: + self.do_else(None, self.ifStates[-1]) + + def do_endif(self, args): + if self.disableLevel > 0: + self.disableLevel -= 1 + if self.disableLevel == 0: + self.ifStates.pop() + + # output processing + def do_expand(self, args): + lst = re.split(r"__(\w+)__", args, re.U) + + def vsubst(v): + if v in self.context: + return _to_text(self.context[v]) + return "" + + for i in range(1, len(lst), 2): + lst[i] = vsubst(lst[i]) + lst.append("\n") # add back the newline + self.write(six.moves.reduce(lambda x, y: x + y, lst, "")) + + def do_literal(self, args): + self.write(args + "\n") + + def do_filter(self, args): + filters = [f for f in args.split(" ") if hasattr(self, "filter_" + f)] + if len(filters) == 0: + return + current = dict(self.filters) + for f in filters: + current[f] = getattr(self, "filter_" + f) + self.filters = [(fn, current[fn]) for fn in sorted(current.keys())] + return + + def do_unfilter(self, args): + filters = args.split(" ") + current = dict(self.filters) + for f in filters: + if f in current: + del current[f] + self.filters = [(fn, current[fn]) for fn in sorted(current.keys())] + return + + # Filters + # + # emptyLines: Strips blank lines from the output. + def filter_emptyLines(self, aLine): + if aLine == "\n": + return "" + return aLine + + # dumbComments: Empties out lines that consists of optional whitespace + # followed by a `//`. + def filter_dumbComments(self, aLine): + return re.sub(r"^\s*//.*", "", aLine) + + # substitution: variables wrapped in @ are replaced with their value. + def filter_substitution(self, aLine, fatal=True): + def repl(matchobj): + varname = matchobj.group("VAR") + if varname in self.context: + return _to_text(self.context[varname]) + if fatal: + raise Preprocessor.Error(self, "UNDEFINED_VAR", varname) + return matchobj.group(0) + + return self.varsubst.sub(repl, aLine) + + # attemptSubstitution: variables wrapped in @ are replaced with their + # value, or an empty string if the variable is not defined. + def filter_attemptSubstitution(self, aLine): + return self.filter_substitution(aLine, fatal=False) + + # File ops + def do_include(self, args, filters=True): + """ + Preprocess a given file. + args can either be a file name, or a file-like object. + Files should be opened, and will be closed after processing. + """ + isName = isinstance(args, six.string_types) + oldCheckLineNumbers = self.checkLineNumbers + self.checkLineNumbers = False + if isName: + try: + args = _to_text(args) + if filters: + args = self.applyFilters(args) + if not os.path.isabs(args): + args = os.path.join(self.curdir, args) + args = io.open(args, "r", encoding="utf-8") + except Preprocessor.Error: + raise + except Exception: + raise Preprocessor.Error(self, "FILE_NOT_FOUND", _to_text(args)) + self.checkLineNumbers = bool( + re.search(r"\.(js|jsm|java|webidl)(?:\.in)?$", args.name) + ) + oldFile = self.context["FILE"] + oldLine = self.context["LINE"] + oldDir = self.context["DIRECTORY"] + oldCurdir = self.curdir + self.noteLineInfo() + + if args.isatty(): + # we're stdin, use '-' and '' for file and dir + self.context["FILE"] = "-" + self.context["DIRECTORY"] = "" + self.curdir = "." + else: + abspath = os.path.abspath(args.name) + self.curdir = os.path.dirname(abspath) + self.includes.add(six.ensure_text(abspath)) + if self.topobjdir and path_starts_with(abspath, self.topobjdir): + abspath = "$OBJDIR" + normsep(abspath[len(self.topobjdir) :]) + elif self.topsrcdir and path_starts_with(abspath, self.topsrcdir): + abspath = "$SRCDIR" + normsep(abspath[len(self.topsrcdir) :]) + self.context["FILE"] = abspath + self.context["DIRECTORY"] = os.path.dirname(abspath) + self.context["LINE"] = 0 + + for l in args: + self.context["LINE"] += 1 + self.handleLine(l) + if isName: + args.close() + + self.context["FILE"] = oldFile + self.checkLineNumbers = oldCheckLineNumbers + self.context["LINE"] = oldLine + self.context["DIRECTORY"] = oldDir + self.curdir = oldCurdir + + def do_includesubst(self, args): + args = self.filter_substitution(args) + self.do_include(args) + + def do_error(self, args): + raise Preprocessor.Error(self, "Error: ", _to_text(args)) + + +def preprocess(includes=[sys.stdin], defines={}, output=sys.stdout, marker="#"): + pp = Preprocessor(defines=defines, marker=marker) + for f in includes: + with io.open(f, "r", encoding="utf-8") as input: + pp.processFile(input=input, output=output) + return pp.includes + + +# Keep this module independently executable. +if __name__ == "__main__": + pp = Preprocessor() + pp.handleCommandLine(None, True) diff --git a/python/mozbuild/mozbuild/pythonutil.py b/python/mozbuild/mozbuild/pythonutil.py new file mode 100644 index 0000000000..a3540647f9 --- /dev/null +++ b/python/mozbuild/mozbuild/pythonutil.py @@ -0,0 +1,23 @@ +# 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/. + +import os +import sys + + +def iter_modules_in_path(*paths): + paths = [os.path.abspath(os.path.normcase(p)) + os.sep for p in paths] + for name, module in sys.modules.items(): + if getattr(module, "__file__", None) is None: + continue + if module.__file__ is None: + continue + path = module.__file__ + + if path.endswith(".pyc"): + path = path[:-1] + path = os.path.abspath(os.path.normcase(path)) + + if any(path.startswith(p) for p in paths): + yield path diff --git a/python/mozbuild/mozbuild/repackaging/__init__.py b/python/mozbuild/mozbuild/repackaging/__init__.py new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/python/mozbuild/mozbuild/repackaging/__init__.py diff --git a/python/mozbuild/mozbuild/repackaging/application_ini.py b/python/mozbuild/mozbuild/repackaging/application_ini.py new file mode 100644 index 0000000000..f11c94f781 --- /dev/null +++ b/python/mozbuild/mozbuild/repackaging/application_ini.py @@ -0,0 +1,66 @@ +# 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.files import FileFinder +from six import string_types +from six.moves import configparser + + +def get_application_ini_value( + finder_or_application_directory, section, value, fallback=None +): + """Find string with given `section` and `value` in any `application.ini` + under given directory or finder. + + If string is not found and `fallback` is given, find string with given + `section` and `fallback` instead. + + Raises an `Exception` if no string is found.""" + + return next( + get_application_ini_values( + finder_or_application_directory, + dict(section=section, value=value, fallback=fallback), + ) + ) + + +def get_application_ini_values(finder_or_application_directory, *args): + """Find multiple strings for given `section` and `value` pairs. + Additional `args` should be dictionaries with keys `section`, `value`, + and optional `fallback`. Returns an iterable of strings, one for each + dictionary provided. + + `fallback` is treated as with `get_application_ini_value`. + + Raises an `Exception` if any string is not found.""" + + if isinstance(finder_or_application_directory, string_types): + finder = FileFinder(finder_or_application_directory) + else: + finder = finder_or_application_directory + + # Packages usually have a top-level `firefox/` directory; search below it. + for p, f in finder.find("**/application.ini"): + data = f.open().read().decode("utf-8") + parser = configparser.ConfigParser() + parser.read_string(data) + + for d in args: + rc = None + try: + rc = parser.get(d["section"], d["value"]) + except configparser.NoOptionError: + if "fallback" not in d: + raise + else: + rc = parser.get(d["section"], d["fallback"]) + + if rc is None: + raise Exception("Input does not contain an application.ini file") + + yield rc + + # Process only the first `application.ini`. + break diff --git a/python/mozbuild/mozbuild/repackaging/deb.py b/python/mozbuild/mozbuild/repackaging/deb.py new file mode 100644 index 0000000000..739fa5bfe4 --- /dev/null +++ b/python/mozbuild/mozbuild/repackaging/deb.py @@ -0,0 +1,745 @@ +# 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/. + +import datetime +import json +import logging +import os +import shutil +import subprocess +import tarfile +import tempfile +import zipfile +from email.utils import format_datetime +from pathlib import Path +from string import Template + +import mozfile +import mozpack.path as mozpath +import requests +from mozilla_version.gecko import GeckoVersion +from redo import retry + +from mozbuild.repackaging.application_ini import get_application_ini_values + + +class NoDebPackageFound(Exception): + """Raised when no .deb is found after calling dpkg-buildpackage""" + + def __init__(self, deb_file_path) -> None: + super().__init__( + f"No {deb_file_path} package found after calling dpkg-buildpackage" + ) + + +class HgServerError(Exception): + """Raised when Hg responds with an error code that is not 404 (i.e. when there is an outage)""" + + def __init__(self, msg) -> None: + super().__init__(msg) + + +_DEB_ARCH = { + "all": "all", + "x86": "i386", + "x86_64": "amd64", +} +# At the moment the Firefox build baseline is jessie. +# The debian-repackage image defined in taskcluster/docker/debian-repackage/Dockerfile +# bootstraps the /srv/jessie-i386 and /srv/jessie-amd64 chroot environments we use to +# create the `.deb` repackages. By running the repackage using chroot we generate shared +# library dependencies that match the Firefox build baseline +# defined in taskcluster/scripts/misc/build-sysroot.sh +_DEB_DIST = "jessie" + + +def repackage_deb( + log, + infile, + output, + template_dir, + arch, + version, + build_number, + release_product, + release_type, + fluent_localization, + fluent_resource_loader, +): + if not tarfile.is_tarfile(infile): + raise Exception("Input file %s is not a valid tarfile." % infile) + + tmpdir = _create_temporary_directory(arch) + source_dir = os.path.join(tmpdir, "source") + try: + mozfile.extract_tarball(infile, source_dir) + application_ini_data = _load_application_ini_data(infile, version, build_number) + build_variables = _get_build_variables( + application_ini_data, + arch, + depends="${shlibs:Depends},", + release_product=release_product, + ) + + _copy_plain_deb_config(template_dir, source_dir) + _render_deb_templates( + template_dir, + source_dir, + build_variables, + exclude_file_names=["package-prefs.js"], + ) + + app_name = application_ini_data["name"] + with open( + mozpath.join(source_dir, app_name.lower(), "is-packaged-app"), "w" + ) as f: + f.write("This is a packaged app.\n") + + _inject_deb_distribution_folder(source_dir, app_name) + _inject_deb_desktop_entry_file( + log, + source_dir, + build_variables, + release_product, + release_type, + fluent_localization, + fluent_resource_loader, + ) + _inject_deb_prefs_file(source_dir, app_name, template_dir) + _generate_deb_archive( + source_dir, + target_dir=tmpdir, + output_file_path=output, + build_variables=build_variables, + arch=arch, + ) + + finally: + shutil.rmtree(tmpdir) + + +def repackage_deb_l10n( + input_xpi_file, + input_tar_file, + output, + template_dir, + version, + build_number, + release_product, +): + arch = "all" + + tmpdir = _create_temporary_directory(arch) + source_dir = os.path.join(tmpdir, "source") + try: + langpack_metadata = _extract_langpack_metadata(input_xpi_file) + langpack_dir = mozpath.join(source_dir, "firefox", "distribution", "extensions") + application_ini_data = _load_application_ini_data( + input_tar_file, version, build_number + ) + langpack_id = langpack_metadata["langpack_id"] + if release_product == "devedition": + depends = ( + f"firefox-devedition (= {application_ini_data['deb_pkg_version']})" + ) + else: + depends = f"{application_ini_data['remoting_name']} (= {application_ini_data['deb_pkg_version']})" + build_variables = _get_build_variables( + application_ini_data, + arch, + depends=depends, + # Debian package names are only lowercase + package_name_suffix=f"-l10n-{langpack_id.lower()}", + description_suffix=f" - {langpack_metadata['description']}", + release_product=release_product, + ) + _copy_plain_deb_config(template_dir, source_dir) + _render_deb_templates(template_dir, source_dir, build_variables) + + os.makedirs(langpack_dir, exist_ok=True) + shutil.copy( + input_xpi_file, + mozpath.join( + langpack_dir, + f"{langpack_metadata['browser_specific_settings']['gecko']['id']}.xpi", + ), + ) + _generate_deb_archive( + source_dir=source_dir, + target_dir=tmpdir, + output_file_path=output, + build_variables=build_variables, + arch=arch, + ) + finally: + shutil.rmtree(tmpdir) + + +def _extract_application_ini_data(input_tar_file): + with tempfile.TemporaryDirectory() as d: + with tarfile.open(input_tar_file) as tar: + application_ini_files = [ + tar_info + for tar_info in tar.getmembers() + if tar_info.name.endswith("/application.ini") + ] + if len(application_ini_files) == 0: + raise ValueError( + f"Cannot find any application.ini file in archive {input_tar_file}" + ) + if len(application_ini_files) > 1: + raise ValueError( + f"Too many application.ini files found in archive {input_tar_file}. " + f"Found: {application_ini_files}" + ) + + tar.extract(application_ini_files[0], path=d) + + application_ini_data = _extract_application_ini_data_from_directory(d) + + return application_ini_data + + +def _load_application_ini_data(infile, version, build_number): + extracted_application_ini_data = _extract_application_ini_data(infile) + parsed_application_ini_data = _parse_application_ini_data( + extracted_application_ini_data, version, build_number + ) + return parsed_application_ini_data + + +def _parse_application_ini_data(application_ini_data, version, build_number): + application_ini_data["timestamp"] = datetime.datetime.strptime( + application_ini_data["build_id"], "%Y%m%d%H%M%S" + ) + + application_ini_data["remoting_name"] = application_ini_data[ + "remoting_name" + ].lower() + + application_ini_data["deb_pkg_version"] = _get_deb_pkg_version( + version, application_ini_data["build_id"], build_number + ) + + return application_ini_data + + +def _get_deb_pkg_version(version, build_id, build_number): + gecko_version = GeckoVersion.parse(version) + deb_pkg_version = ( + f"{gecko_version}~{build_id}" + if gecko_version.is_nightly + else f"{gecko_version}~build{build_number}" + ) + return deb_pkg_version + + +def _extract_application_ini_data_from_directory(application_directory): + values = get_application_ini_values( + application_directory, + dict(section="App", value="Name"), + dict(section="App", value="CodeName", fallback="Name"), + dict(section="App", value="Vendor"), + dict(section="App", value="RemotingName"), + dict(section="App", value="BuildID"), + ) + + data = { + "name": next(values), + "display_name": next(values), + "vendor": next(values), + "remoting_name": next(values), + "build_id": next(values), + } + + return data + + +def _get_build_variables( + application_ini_data, + arch, + depends, + package_name_suffix="", + description_suffix="", + release_product="", +): + if release_product == "devedition": + deb_pkg_install_path = "usr/lib/firefox-devedition" + deb_pkg_name = f"firefox-devedition{package_name_suffix}" + else: + deb_pkg_install_path = f"usr/lib/{application_ini_data['remoting_name']}" + deb_pkg_name = f"{application_ini_data['remoting_name']}{package_name_suffix}" + return { + "DEB_DESCRIPTION": f"{application_ini_data['vendor']} {application_ini_data['display_name']}" + f"{description_suffix}", + "DEB_PKG_INSTALL_PATH": deb_pkg_install_path, + "DEB_PKG_NAME": deb_pkg_name, + "DEB_PKG_VERSION": application_ini_data["deb_pkg_version"], + "DEB_CHANGELOG_DATE": format_datetime(application_ini_data["timestamp"]), + "DEB_ARCH_NAME": _DEB_ARCH[arch], + "DEB_DEPENDS": depends, + } + + +def _copy_plain_deb_config(input_template_dir, source_dir): + template_dir_filenames = os.listdir(input_template_dir) + plain_filenames = [ + mozpath.basename(filename) + for filename in template_dir_filenames + if not filename.endswith(".in") and not filename.endswith(".js") + ] + os.makedirs(mozpath.join(source_dir, "debian"), exist_ok=True) + + for filename in plain_filenames: + shutil.copy( + mozpath.join(input_template_dir, filename), + mozpath.join(source_dir, "debian", filename), + ) + + +def _render_deb_templates( + input_template_dir, source_dir, build_variables, exclude_file_names=None +): + exclude_file_names = [] if exclude_file_names is None else exclude_file_names + + template_dir_filenames = os.listdir(input_template_dir) + template_filenames = [ + mozpath.basename(filename) + for filename in template_dir_filenames + if filename.endswith(".in") and filename not in exclude_file_names + ] + os.makedirs(mozpath.join(source_dir, "debian"), exist_ok=True) + + for file_name in template_filenames: + with open(mozpath.join(input_template_dir, file_name)) as f: + template = Template(f.read()) + with open(mozpath.join(source_dir, "debian", Path(file_name).stem), "w") as f: + f.write(template.substitute(build_variables)) + + +def _inject_deb_distribution_folder(source_dir, app_name): + with tempfile.TemporaryDirectory() as git_clone_dir: + subprocess.check_call( + [ + "git", + "clone", + "https://github.com/mozilla-partners/deb.git", + git_clone_dir, + ], + ) + shutil.copytree( + mozpath.join(git_clone_dir, "desktop/deb/distribution"), + mozpath.join(source_dir, app_name.lower(), "distribution"), + ) + + +def _inject_deb_prefs_file(source_dir, app_name, template_dir): + src = mozpath.join(template_dir, "package-prefs.js") + dst = mozpath.join(source_dir, app_name.lower(), "defaults/pref") + shutil.copy(src, dst) + + +def _inject_deb_desktop_entry_file( + log, + source_dir, + build_variables, + release_product, + release_type, + fluent_localization, + fluent_resource_loader, +): + desktop_entry_file_text = _generate_browser_desktop_entry_file_text( + log, + build_variables, + release_product, + release_type, + fluent_localization, + fluent_resource_loader, + ) + desktop_entry_file_filename = f"{build_variables['DEB_PKG_NAME']}.desktop" + os.makedirs(mozpath.join(source_dir, "debian"), exist_ok=True) + with open( + mozpath.join(source_dir, "debian", desktop_entry_file_filename), "w" + ) as f: + f.write(desktop_entry_file_text) + + +def _generate_browser_desktop_entry_file_text( + log, + build_variables, + release_product, + release_type, + fluent_localization, + fluent_resource_loader, +): + localizations = _create_fluent_localizations( + fluent_resource_loader, fluent_localization, release_type, release_product, log + ) + desktop_entry = _generate_browser_desktop_entry(build_variables, localizations) + desktop_entry_file_text = "\n".join(desktop_entry) + return desktop_entry_file_text + + +def _create_fluent_localizations( + fluent_resource_loader, fluent_localization, release_type, release_product, log +): + brand_fluent_filename = "brand.ftl" + l10n_central_url = "https://hg.mozilla.org/l10n-central" + desktop_entry_fluent_filename = "linuxDesktopEntry.ftl" + + l10n_dir = tempfile.mkdtemp() + + loader = fluent_resource_loader(os.path.join(l10n_dir, "{locale}")) + + localizations = {} + linux_l10n_changesets = _load_linux_l10n_changesets( + "browser/locales/l10n-changesets.json" + ) + locales = ["en-US"] + locales.extend(linux_l10n_changesets.keys()) + en_US_brand_fluent_filename = _get_en_US_brand_fluent_filename( + brand_fluent_filename, release_type, release_product + ) + + for locale in locales: + locale_dir = os.path.join(l10n_dir, locale) + os.mkdir(locale_dir) + localized_desktop_entry_filename = os.path.join( + locale_dir, desktop_entry_fluent_filename + ) + if locale == "en-US": + en_US_desktop_entry_fluent_filename = os.path.join( + "browser", "locales", "en-US", "browser", desktop_entry_fluent_filename + ) + shutil.copyfile( + en_US_desktop_entry_fluent_filename, + localized_desktop_entry_filename, + ) + else: + non_en_US_fluent_resource_file_url = f"{l10n_central_url}/{locale}/raw-file/{linux_l10n_changesets[locale]['revision']}/browser/browser/{desktop_entry_fluent_filename}" + response = requests.get(non_en_US_fluent_resource_file_url) + response = retry( + requests.get, + args=[non_en_US_fluent_resource_file_url], + attempts=5, + sleeptime=3, + jitter=2, + ) + mgs = "Missing {fluent_resource_file_name} for {locale}: received HTTP {status_code} for GET {resource_file_url}" + params = { + "fluent_resource_file_name": desktop_entry_fluent_filename, + "locale": locale, + "resource_file_url": non_en_US_fluent_resource_file_url, + "status_code": response.status_code, + } + action = "repackage-deb" + if response.status_code == 404: + log( + logging.WARNING, + action, + params, + mgs, + ) + continue + if response.status_code != 200: + log( + logging.ERROR, + action, + params, + mgs, + ) + raise HgServerError(mgs.format(**params)) + + with open(localized_desktop_entry_filename, "w", encoding="utf-8") as f: + f.write(response.text) + + shutil.copyfile( + en_US_brand_fluent_filename, + os.path.join(locale_dir, brand_fluent_filename), + ) + + fallbacks = [locale] + if locale != "en-US": + fallbacks.append("en-US") + localizations[locale] = fluent_localization( + fallbacks, [desktop_entry_fluent_filename, brand_fluent_filename], loader + ) + + return localizations + + +def _get_en_US_brand_fluent_filename( + brand_fluent_filename, release_type, release_product +): + branding_fluent_filename_template = os.path.join( + "browser/branding/{brand}/locales/en-US", brand_fluent_filename + ) + if release_type == "nightly": + return branding_fluent_filename_template.format(brand="nightly") + elif release_type == "release" or release_type == "release-rc": + return branding_fluent_filename_template.format(brand="official") + elif release_type == "beta" and release_product == "firefox": + return branding_fluent_filename_template.format(brand="official") + elif release_type == "beta" and release_product == "devedition": + return branding_fluent_filename_template.format(brand="aurora") + else: + return branding_fluent_filename_template.format(brand="unofficial") + + +def _load_linux_l10n_changesets(l10n_changesets_filename): + with open(l10n_changesets_filename) as l10n_changesets_file: + l10n_changesets = json.load(l10n_changesets_file) + return { + locale: changeset + for locale, changeset in l10n_changesets.items() + if any(platform.startswith("linux") for platform in changeset["platforms"]) + } + + +def _generate_browser_desktop_entry(build_variables, localizations): + mime_types = [ + "application/json", + "application/pdf", + "application/rdf+xml", + "application/rss+xml", + "application/x-xpinstall", + "application/xhtml+xml", + "application/xml", + "audio/flac", + "audio/ogg", + "audio/webm", + "image/avif", + "image/gif", + "image/jpeg", + "image/png", + "image/svg+xml", + "image/webp", + "text/html", + "text/xml", + "video/ogg", + "video/webm", + "x-scheme-handler/chrome", + "x-scheme-handler/http", + "x-scheme-handler/https", + "x-scheme-handler/mailto", + ] + + categories = [ + "GNOME", + "GTK", + "Network", + "WebBrowser", + ] + + actions = [ + { + "name": "new-window", + "message": "desktop-action-new-window-name", + "command": f"{build_variables['DEB_PKG_NAME']} --new-window %u", + }, + { + "name": "new-private-window", + "message": "desktop-action-new-private-window-name", + "command": f"{build_variables['DEB_PKG_NAME']} --private-window %u", + }, + { + "name": "open-profile-manager", + "message": "desktop-action-open-profile-manager", + "command": f"{build_variables['DEB_PKG_NAME']} --ProfileManager", + }, + ] + + desktop_entry = _desktop_entry_section( + "Desktop Entry", + [ + { + "key": "Version", + "value": "1.0", + }, + { + "key": "Type", + "value": "Application", + }, + { + "key": "Exec", + "value": f"{build_variables['DEB_PKG_NAME']} %u", + }, + { + "key": "Terminal", + "value": "false", + }, + { + "key": "X-MultipleArgs", + "value": "false", + }, + { + "key": "Icon", + "value": build_variables["DEB_PKG_NAME"], + }, + { + "key": "StartupWMClass", + "value": "firefox-aurora" + if build_variables["DEB_PKG_NAME"] == "firefox-devedition" + else build_variables["DEB_PKG_NAME"], + }, + { + "key": "Categories", + "value": _desktop_entry_list(categories), + }, + { + "key": "MimeType", + "value": _desktop_entry_list(mime_types), + }, + { + "key": "StartupNotify", + "value": "true", + }, + { + "key": "Actions", + "value": _desktop_entry_list([action["name"] for action in actions]), + }, + {"key": "Name", "value": "desktop-entry-name", "l10n": True}, + {"key": "Comment", "value": "desktop-entry-comment", "l10n": True}, + {"key": "GenericName", "value": "desktop-entry-generic-name", "l10n": True}, + {"key": "Keywords", "value": "desktop-entry-keywords", "l10n": True}, + { + "key": "X-GNOME-FullName", + "value": "desktop-entry-x-gnome-full-name", + "l10n": True, + }, + ], + localizations, + ) + + for action in actions: + desktop_entry.extend( + _desktop_entry_section( + f"Desktop Action {action['name']}", + [ + { + "key": "Name", + "value": action["message"], + "l10n": True, + }, + { + "key": "Exec", + "value": action["command"], + }, + ], + localizations, + ) + ) + + return desktop_entry + + +def _desktop_entry_list(iterable): + delimiter = ";" + return f"{delimiter.join(iterable)}{delimiter}" + + +def _desktop_entry_attribute(key, value, locale=None, localizations=None): + if not locale and not localizations: + return f"{key}={value}" + if locale and locale == "en-US": + return f"{key}={localizations[locale].format_value(value)}" + else: + return f"{key}[{locale.replace('-', '_')}]={localizations[locale].format_value(value)}" + + +def _desktop_entry_section(header, attributes, localizations): + desktop_entry_section = [f"[{header}]"] + l10n_attributes = [attribute for attribute in attributes if attribute.get("l10n")] + non_l10n_attributes = [ + attribute for attribute in attributes if not attribute.get("l10n") + ] + for attribute in non_l10n_attributes: + desktop_entry_section.append( + _desktop_entry_attribute(attribute["key"], attribute["value"]) + ) + for attribute in l10n_attributes: + for locale in localizations: + desktop_entry_section.append( + _desktop_entry_attribute( + attribute["key"], attribute["value"], locale, localizations + ) + ) + desktop_entry_section.append("") + return desktop_entry_section + + +def _generate_deb_archive( + source_dir, target_dir, output_file_path, build_variables, arch +): + command = _get_command(arch) + subprocess.check_call(command, cwd=source_dir) + deb_arch = _DEB_ARCH[arch] + deb_file_name = f"{build_variables['DEB_PKG_NAME']}_{build_variables['DEB_PKG_VERSION']}_{deb_arch}.deb" + deb_file_path = mozpath.join(target_dir, deb_file_name) + + if not os.path.exists(deb_file_path): + raise NoDebPackageFound(deb_file_path) + + subprocess.check_call(["dpkg-deb", "--info", deb_file_path]) + shutil.move(deb_file_path, output_file_path) + + +def _get_command(arch): + deb_arch = _DEB_ARCH[arch] + command = [ + "dpkg-buildpackage", + # TODO: Use long options once we stop supporting Debian Jesse. They're more + # explicit. + # + # Long options were added in dpkg 1.18.8 which is part of Debian Stretch. + # + # https://git.dpkg.org/cgit/dpkg/dpkg.git/commit/?h=1.18.x&id=293bd243a19149165fc4fd8830b16a51d471a5e9 + # https://packages.debian.org/stretch/dpkg-dev + "-us", # --unsigned-source + "-uc", # --unsigned-changes + "-b", # --build=binary + ] + + if deb_arch != "all": + command.append(f"--host-arch={deb_arch}") + + if _is_chroot_available(arch): + flattened_command = " ".join(command) + command = [ + "chroot", + _get_chroot_path(arch), + "bash", + "-c", + f"cd /tmp/*/source; {flattened_command}", + ] + + return command + + +def _create_temporary_directory(arch): + if _is_chroot_available(arch): + return tempfile.mkdtemp(dir=f"{_get_chroot_path(arch)}/tmp") + else: + return tempfile.mkdtemp() + + +def _is_chroot_available(arch): + return os.path.isdir(_get_chroot_path(arch)) + + +def _get_chroot_path(arch): + deb_arch = "amd64" if arch == "all" else _DEB_ARCH[arch] + return f"/srv/{_DEB_DIST}-{deb_arch}" + + +_MANIFEST_FILE_NAME = "manifest.json" + + +def _extract_langpack_metadata(input_xpi_file): + with tempfile.TemporaryDirectory() as d: + with zipfile.ZipFile(input_xpi_file) as zip: + zip.extract(_MANIFEST_FILE_NAME, path=d) + + with open(mozpath.join(d, _MANIFEST_FILE_NAME)) as f: + return json.load(f) diff --git a/python/mozbuild/mozbuild/repackaging/dmg.py b/python/mozbuild/mozbuild/repackaging/dmg.py new file mode 100644 index 0000000000..d83ab0cb77 --- /dev/null +++ b/python/mozbuild/mozbuild/repackaging/dmg.py @@ -0,0 +1,56 @@ +# 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/. + +import tarfile +from pathlib import Path + +import mozfile +from mozpack.dmg import create_dmg + +from mozbuild.bootstrap import bootstrap_toolchain +from mozbuild.repackaging.application_ini import get_application_ini_value + + +def repackage_dmg(infile, output, attribution_sentinel=None): + if not tarfile.is_tarfile(infile): + raise Exception("Input file %s is not a valid tarfile." % infile) + + # Resolve required tools + dmg_tool = bootstrap_toolchain("dmg/dmg") + if not dmg_tool: + raise Exception("DMG tool not found") + hfs_tool = bootstrap_toolchain("dmg/hfsplus") + if not hfs_tool: + raise Exception("HFS tool not found") + mkfshfs_tool = bootstrap_toolchain("hfsplus/newfs_hfs") + if not mkfshfs_tool: + raise Exception("MKFSHFS tool not found") + + with mozfile.TemporaryDirectory() as tmp: + tmpdir = Path(tmp) + mozfile.extract_tarball(infile, tmpdir) + + # Remove the /Applications symlink. If we don't, an rsync command in + # create_dmg() will break, and create_dmg() re-creates the symlink anyway. + symlink = tmpdir / " " + if symlink.is_file(): + symlink.unlink() + + volume_name = get_application_ini_value( + str(tmpdir), "App", "CodeName", fallback="Name" + ) + + # The extra_files argument is empty [] because they are already a part + # of the original dmg produced by the build, and they remain in the + # tarball generated by the signing task. + create_dmg( + source_directory=tmpdir, + output_dmg=Path(output), + volume_name=volume_name, + extra_files=[], + dmg_tool=Path(dmg_tool), + hfs_tool=Path(hfs_tool), + mkfshfs_tool=Path(mkfshfs_tool), + attribution_sentinel=attribution_sentinel, + ) diff --git a/python/mozbuild/mozbuild/repackaging/installer.py b/python/mozbuild/mozbuild/repackaging/installer.py new file mode 100644 index 0000000000..9bd17613bf --- /dev/null +++ b/python/mozbuild/mozbuild/repackaging/installer.py @@ -0,0 +1,55 @@ +# 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/. + +import os +import shutil +import tempfile +import zipfile + +import mozpack.path as mozpath + +from mozbuild.action.exe_7z_archive import archive_exe +from mozbuild.util import ensureParentDir + + +def repackage_installer( + topsrcdir, tag, setupexe, package, output, package_name, sfx_stub, use_upx +): + if package and not zipfile.is_zipfile(package): + raise Exception("Package file %s is not a valid .zip file." % package) + if package is not None and package_name is None: + raise Exception("Package name must be provided, if a package is provided.") + if package is None and package_name is not None: + raise Exception( + "Package name must not be provided, if a package is not provided." + ) + + # We need the full path for the tag and output, since we chdir later. + tag = mozpath.realpath(tag) + output = mozpath.realpath(output) + ensureParentDir(output) + + tmpdir = tempfile.mkdtemp() + old_cwd = os.getcwd() + try: + if package: + z = zipfile.ZipFile(package) + z.extractall(tmpdir) + z.close() + + # Copy setup.exe into the root of the install dir, alongside the + # package. + shutil.copyfile(setupexe, mozpath.join(tmpdir, mozpath.basename(setupexe))) + + # archive_exe requires us to be in the directory where the package is + # unpacked (the tmpdir) + os.chdir(tmpdir) + + sfx_package = mozpath.join(topsrcdir, sfx_stub) + + archive_exe(package_name, tag, sfx_package, output, use_upx) + + finally: + os.chdir(old_cwd) + shutil.rmtree(tmpdir) diff --git a/python/mozbuild/mozbuild/repackaging/mar.py b/python/mozbuild/mozbuild/repackaging/mar.py new file mode 100644 index 0000000000..f215c17238 --- /dev/null +++ b/python/mozbuild/mozbuild/repackaging/mar.py @@ -0,0 +1,93 @@ +# 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/. + +import os +import shutil +import subprocess +import sys +import tarfile +import tempfile +import zipfile +from pathlib import Path + +import mozfile +import mozpack.path as mozpath + +from mozbuild.repackaging.application_ini import get_application_ini_value +from mozbuild.util import ensureParentDir + +_BCJ_OPTIONS = { + "x86": ["--x86"], + "x86_64": ["--x86"], + "aarch64": [], + # macOS Universal Builds + "macos-x86_64-aarch64": [], +} + + +def repackage_mar(topsrcdir, package, mar, output, arch=None, mar_channel_id=None): + if not zipfile.is_zipfile(package) and not tarfile.is_tarfile(package): + raise Exception("Package file %s is not a valid .zip or .tar file." % package) + if arch and arch not in _BCJ_OPTIONS: + raise Exception( + "Unknown architecture {}, available architectures: {}".format( + arch, list(_BCJ_OPTIONS.keys()) + ) + ) + + ensureParentDir(output) + tmpdir = tempfile.mkdtemp() + try: + if tarfile.is_tarfile(package): + filelist = mozfile.extract_tarball(package, tmpdir) + else: + z = zipfile.ZipFile(package) + z.extractall(tmpdir) + filelist = z.namelist() + z.close() + + toplevel_dirs = set([mozpath.split(f)[0] for f in filelist]) + excluded_stuff = set([" ", ".background", ".DS_Store", ".VolumeIcon.icns"]) + toplevel_dirs = toplevel_dirs - excluded_stuff + # Make sure the .zip file just contains a directory like 'firefox/' at + # the top, and find out what it is called. + if len(toplevel_dirs) != 1: + raise Exception( + "Package file is expected to have a single top-level directory" + "(eg: 'firefox'), not: %s" % toplevel_dirs + ) + ffxdir = mozpath.join(tmpdir, toplevel_dirs.pop()) + + make_full_update = mozpath.join( + topsrcdir, "tools/update-packaging/make_full_update.sh" + ) + + env = os.environ.copy() + env["MOZ_PRODUCT_VERSION"] = get_application_ini_value(tmpdir, "App", "Version") + env["MAR"] = mozpath.normpath(mar) + if arch: + env["BCJ_OPTIONS"] = " ".join(_BCJ_OPTIONS[arch]) + if mar_channel_id: + env["MAR_CHANNEL_ID"] = mar_channel_id + # The Windows build systems have xz installed but it isn't in the path + # like it is on Linux and Mac OS X so just use the XZ env var so the mar + # generation scripts can find it. + xz_path = mozpath.join(topsrcdir, "xz/xz.exe") + if os.path.exists(xz_path): + env["XZ"] = mozpath.normpath(xz_path) + + cmd = [make_full_update, output, ffxdir] + if sys.platform == "win32": + # make_full_update.sh is a bash script, and Windows needs to + # explicitly call out the shell to execute the script from Python. + + mozillabuild = os.environ["MOZILLABUILD"] + if (Path(mozillabuild) / "msys2").exists(): + cmd.insert(0, mozillabuild + "/msys2/usr/bin/bash.exe") + else: + cmd.insert(0, mozillabuild + "/msys/bin/bash.exe") + subprocess.check_call(cmd, env=env) + + finally: + shutil.rmtree(tmpdir) diff --git a/python/mozbuild/mozbuild/repackaging/msi.py b/python/mozbuild/mozbuild/repackaging/msi.py new file mode 100644 index 0000000000..1884b05afe --- /dev/null +++ b/python/mozbuild/mozbuild/repackaging/msi.py @@ -0,0 +1,121 @@ +# 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/. + +import os +import shutil +import subprocess +import sys +import tempfile +from xml.dom import minidom + +import mozpack.path as mozpath + +from mozbuild.util import ensureParentDir + +_MSI_ARCH = { + "x86": "x86", + "x86_64": "x64", +} + + +def update_wsx(wfile, pvalues): + parsed = minidom.parse(wfile) + + # construct a dictinary for the pre-processing options + # iterate over that list and add them to the wsx xml doc + for k, v in pvalues.items(): + entry = parsed.createProcessingInstruction("define", k + ' = "' + v + '"') + root = parsed.firstChild + parsed.insertBefore(entry, root) + # write out xml to new wfile + new_w_file = wfile + ".new" + with open(new_w_file, "w") as fh: + parsed.writexml(fh) + shutil.move(new_w_file, wfile) + return wfile + + +def repackage_msi( + topsrcdir, wsx, version, locale, arch, setupexe, candle, light, output +): + if sys.platform != "win32": + raise Exception("repackage msi only works on windows") + if not os.path.isdir(topsrcdir): + raise Exception("%s does not exist." % topsrcdir) + if not os.path.isfile(wsx): + raise Exception("%s does not exist." % wsx) + if version is None: + raise Exception("version name must be provided.") + if locale is None: + raise Exception("locale name must be provided.") + if arch is None or arch not in _MSI_ARCH.keys(): + raise Exception( + "arch name must be provided and one of {}.".format(_MSI_ARCH.keys()) + ) + if not os.path.isfile(setupexe): + raise Exception("%s does not exist." % setupexe) + if candle is not None and not os.path.isfile(candle): + raise Exception("%s does not exist." % candle) + if light is not None and not os.path.isfile(light): + raise Exception("%s does not exist." % light) + embeddedVersion = "0.0.0.0" + # Version string cannot contain 'a' or 'b' when embedding in msi manifest. + if "a" not in version and "b" not in version: + if version.endswith("esr"): + parts = version[:-3].split(".") + else: + parts = version.split(".") + while len(parts) < 4: + parts.append("0") + embeddedVersion = ".".join(parts) + + wsx = mozpath.realpath(wsx) + setupexe = mozpath.realpath(setupexe) + output = mozpath.realpath(output) + ensureParentDir(output) + + if sys.platform == "win32": + tmpdir = tempfile.mkdtemp() + old_cwd = os.getcwd() + try: + wsx_file = os.path.split(wsx)[1] + shutil.copy(wsx, tmpdir) + temp_wsx_file = os.path.join(tmpdir, wsx_file) + temp_wsx_file = mozpath.realpath(temp_wsx_file) + pre_values = { + "Vendor": "Mozilla", + "BrandFullName": "Mozilla Firefox", + "Version": version, + "AB_CD": locale, + "Architecture": _MSI_ARCH[arch], + "ExeSourcePath": setupexe, + "EmbeddedVersionCode": embeddedVersion, + } + # update wsx file with inputs from + newfile = update_wsx(temp_wsx_file, pre_values) + wix_object_file = os.path.join(tmpdir, "installer.wixobj") + env = os.environ.copy() + if candle is None: + candle = "candle.exe" + cmd = [candle, "-out", wix_object_file, newfile] + subprocess.check_call(cmd, env=env) + wix_installer = wix_object_file.replace(".wixobj", ".msi") + if light is None: + light = "light.exe" + light_cmd = [ + light, + "-cultures:neutral", + "-sw1076", + "-sw1079", + "-out", + wix_installer, + wix_object_file, + ] + subprocess.check_call(light_cmd, env=env) + os.remove(wix_object_file) + # mv file to output dir + shutil.move(wix_installer, output) + finally: + os.chdir(old_cwd) + shutil.rmtree(tmpdir) diff --git a/python/mozbuild/mozbuild/repackaging/msix.py b/python/mozbuild/mozbuild/repackaging/msix.py new file mode 100644 index 0000000000..762a33f1d1 --- /dev/null +++ b/python/mozbuild/mozbuild/repackaging/msix.py @@ -0,0 +1,1192 @@ +# 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/. + +r"""Repackage ZIP archives (or directories) into MSIX App Packages. + +# Known issues + +- The icons in the Start Menu have a solid colour tile behind them. I think + this is an issue with plating. +""" + +import functools +import itertools +import logging +import os +import re +import shutil +import subprocess +import sys +import time +import urllib +from collections import defaultdict +from pathlib import Path + +import mozpack.path as mozpath +from mach.util import get_state_dir +from mozfile import which +from mozpack.copier import FileCopier +from mozpack.files import FileFinder, JarFinder +from mozpack.manifests import InstallManifest +from mozpack.mozjar import JarReader +from mozpack.packager.unpack import UnpackFinder +from six.moves import shlex_quote + +from mozbuild.repackaging.application_ini import get_application_ini_values +from mozbuild.util import ensureParentDir + + +def log_copy_result(log, elapsed, destdir, result): + COMPLETE = ( + "Elapsed: {elapsed:.2f}s; From {dest}: Kept {existing} existing; " + "Added/updated {updated}; " + "Removed {rm_files} files and {rm_dirs} directories." + ) + copy_result = COMPLETE.format( + elapsed=elapsed, + dest=destdir, + existing=result.existing_files_count, + updated=result.updated_files_count, + rm_files=result.removed_files_count, + rm_dirs=result.removed_directories_count, + ) + log(logging.INFO, "msix", {"copy_result": copy_result}, "{copy_result}") + + +# See https://docs.microsoft.com/en-us/uwp/schemas/appxpackage/uapmanifestschema/element-identity. +_MSIX_ARCH = {"x86": "x86", "x86_64": "x64", "aarch64": "arm64"} + + +@functools.lru_cache(maxsize=None) +def sdk_tool_search_path(): + from mozbuild.configure import ConfigureSandbox + + sandbox = ConfigureSandbox({}, argv=["configure"]) + sandbox.include_file( + str(Path(__file__).parent.parent.parent.parent.parent / "moz.configure") + ) + return sandbox._value_for(sandbox["sdk_bin_path"]) + [ + "c:/Windows/System32/WindowsPowershell/v1.0" + ] + + +def find_sdk_tool(binary, log=None): + if binary.lower().endswith(".exe"): + binary = binary[:-4] + + maybe = os.environ.get(binary.upper()) + if maybe: + log( + logging.DEBUG, + "msix", + {"binary": binary, "path": maybe}, + "Found {binary} in environment: {path}", + ) + return mozpath.normsep(maybe) + + maybe = which(binary, extra_search_dirs=sdk_tool_search_path()) + if maybe: + log( + logging.DEBUG, + "msix", + {"binary": binary, "path": maybe}, + "Found {binary} on path: {path}", + ) + return mozpath.normsep(maybe) + + return None + + +def get_embedded_version(version, buildid): + r"""Turn a display version into "dotted quad" notation. + + N.b.: some parts of the MSIX packaging ecosystem require the final part of + the dotted quad to be identically 0, so we enforce that here. + """ + + # It's irritating to roll our own version parsing, but the tree doesn't seem + # to contain exactly what we need at this time. + version = version.rsplit("esr", 1)[0] + alpha = "a" in version + + tail = None + if "a" in version: + head, tail = version.rsplit("a", 1) + if tail != "1": + # Disallow anything beyond `X.Ya1`. + raise ValueError( + f"Alpha version not of the form X.0a1 is not supported: {version}" + ) + tail = buildid + elif "b" in version: + head, tail = version.rsplit("b", 1) + if len(head.split(".")) > 2: + raise ValueError( + f"Beta version not of the form X.YbZ is not supported: {version}" + ) + elif "rc" in version: + head, tail = version.rsplit("rc", 1) + if len(head.split(".")) > 2: + raise ValueError( + f"Release candidate version not of the form X.YrcZ is not supported: {version}" + ) + else: + head = version + + components = (head.split(".") + ["0", "0", "0"])[:3] + if tail: + components[2] = tail + + if alpha: + # Nightly builds are all `X.0a1`, which isn't helpful. Include build ID + # to disambiguate. But each part of the dotted quad is 16 bits, so we + # have to squash. + if components[1] != "0": + # Disallow anything beyond `X.0a1`. + raise ValueError( + f"Alpha version not of the form X.0a1 is not supported: {version}" + ) + + # Last two digits only to save space. Nightly builds in 2066 and 2099 + # will be impacted, but future us can deal with that. + year = buildid[2:4] + if year[0] == "0": + # Avoid leading zero, like `.0YMm`. + year = year[1:] + month = buildid[4:6] + day = buildid[6:8] + if day[0] == "0": + # Avoid leading zero, like `.0DHh`. + day = day[1:] + hour = buildid[8:10] + + components[1] = "".join((year, month)) + components[2] = "".join((day, hour)) + + version = "{}.{}.{}.0".format(*components) + + return version + + +def get_appconstants_sys_mjs_values(finder, *args): + r"""Extract values, such as the display version like `MOZ_APP_VERSION_DISPLAY: + "...";`, from the omnijar. This allows to determine the beta number, like + `X.YbW`, where the regular beta version is only `X.Y`. Takes a list of + names and returns an iterator of the unique such value found for each name. + Raises an exception if a name is not found or if multiple values are found. + """ + lines = defaultdict(list) + for _, f in finder.find("**/modules/AppConstants.sys.mjs"): + # MOZ_OFFICIAL_BRANDING is split across two lines, so remove line breaks + # immediately following ":"s so those values can be read. + data = f.open().read().decode("utf-8").replace(":\n", ":") + for line in data.splitlines(): + for arg in args: + if arg in line: + lines[arg].append(line) + + for arg in args: + (value,) = lines[arg] # We expect exactly one definition. + _, _, value = value.partition(":") + value = value.strip().strip('",;') + yield value + + +def get_branding(use_official, topsrcdir, build_app, finder, log=None): + """Figure out which branding directory to use.""" + conf_vars = mozpath.join(topsrcdir, build_app, "confvars.sh") + + def conf_vars_value(key): + lines = [line.strip() for line in open(conf_vars).readlines()] + for line in lines: + if line and line[0] == "#": + continue + if key not in line: + continue + _, _, value = line.partition("=") + if not value: + continue + log( + logging.INFO, + "msix", + {"key": key, "conf_vars": conf_vars, "value": value}, + "Read '{key}' from {conf_vars}: {value}", + ) + return value + log( + logging.ERROR, + "msix", + {"key": key, "conf_vars": conf_vars}, + "Unable to find '{key}' in {conf_vars}!", + ) + + # Branding defaults + branding_reason = "No branding set" + branding = conf_vars_value("MOZ_BRANDING_DIRECTORY") + + if use_official: + # Read MOZ_OFFICIAL_BRANDING_DIRECTORY from confvars.sh + branding_reason = "'MOZ_OFFICIAL_BRANDING' set" + branding = conf_vars_value("MOZ_OFFICIAL_BRANDING_DIRECTORY") + else: + # Check if --with-branding was used when building + log( + logging.INFO, + "msix", + {}, + "Checking buildconfig.html for --with-branding build flag.", + ) + for _, f in finder.find("**/chrome/toolkit/content/global/buildconfig.html"): + data = f.open().read().decode("utf-8") + match = re.search(r"--with-branding=([a-z/]+)", data) + if match: + branding_reason = "'--with-branding' set" + branding = match.group(1) + + log( + logging.INFO, + "msix", + { + "branding_reason": branding_reason, + "branding": branding, + }, + "{branding_reason}; Using branding from '{branding}'.", + ) + return mozpath.join(topsrcdir, branding) + + +def unpack_msix(input_msix, output, log=None, verbose=False): + r"""Unpack the given MSIX to the given output directory. + + MSIX packages are ZIP files, but they are Zip64/version 4.5 ZIP files, so + `mozjar.py` doesn't yet handle. Unpack using `unzip{.exe}` for simplicity. + + In addition, file names inside the MSIX package are URL quoted. URL unquote + here. + """ + + log( + logging.INFO, + "msix", + { + "input_msix": input_msix, + "output": output, + }, + "Unpacking input MSIX '{input_msix}' to directory '{output}'", + ) + + unzip = find_sdk_tool("unzip.exe", log=log) + if not unzip: + raise ValueError("unzip is required; set UNZIP or PATH") + + subprocess.check_call( + [unzip, input_msix, "-d", output] + (["-q"] if not verbose else []), + universal_newlines=True, + ) + + # Sanity check: is this an MSIX? + temp_finder = FileFinder(output) + if not temp_finder.contains("AppxManifest.xml"): + raise ValueError("MSIX file does not contain 'AppxManifest.xml'?") + + # Files in the MSIX are URL encoded/quoted; unquote here. + for dirpath, dirs, files in os.walk(output): + # This is a one way to update (in place, for os.walk) the variable `dirs` while iterating + # over it and `files`. + for i, (p, var) in itertools.chain( + enumerate((f, files) for f in files), enumerate((g, dirs) for g in dirs) + ): + q = urllib.parse.unquote(p) + if p != q: + log( + logging.DEBUG, + "msix", + { + "dirpath": dirpath, + "p": p, + "q": q, + }, + "URL unquoting '{p}' -> '{q}' in {dirpath}", + ) + + var[i] = q + os.rename(os.path.join(dirpath, p), os.path.join(dirpath, q)) + + # The "package root" of our MSIX packages is like "Mozilla Firefox Beta Package Root", i.e., it + # varies by channel. This is an easy way to determine it. + for p, _ in temp_finder.find("**/application.ini"): + relpath = os.path.split(p)[0] + + # The application executable, like `firefox.exe`, is in this directory. + return mozpath.normpath(mozpath.join(output, relpath)) + + +def repackage_msix( + dir_or_package, + topsrcdir, + channel=None, + distribution_dirs=[], + version=None, + vendor=None, + displayname=None, + app_name=None, + identity=None, + publisher=None, + publisher_display_name="Mozilla Corporation", + arch=None, + output=None, + force=False, + log=None, + verbose=False, + makeappx=None, +): + if not channel: + raise Exception("channel is required") + if channel not in ( + "official", + "beta", + "aurora", + "nightly", + "unofficial", + ): + raise Exception("channel is unrecognized: {}".format(channel)) + + # TODO: maybe we can fish this from the package directly? Maybe from a DLL, + # maybe from application.ini? + if arch is None or arch not in _MSIX_ARCH.keys(): + raise Exception( + "arch name must be provided and one of {}.".format(_MSIX_ARCH.keys()) + ) + + if not os.path.exists(dir_or_package): + raise Exception("{} does not exist".format(dir_or_package)) + + if ( + os.path.isfile(dir_or_package) + and os.path.splitext(dir_or_package)[1] == ".msix" + ): + # The convention is $MOZBUILD_STATE_PATH/cache/$FEATURE. + msix_dir = mozpath.normsep( + mozpath.join( + get_state_dir(), + "cache", + "mach-msix", + "msix-unpack", + ) + ) + + if os.path.exists(msix_dir): + shutil.rmtree(msix_dir) + ensureParentDir(msix_dir) + + dir_or_package = unpack_msix(dir_or_package, msix_dir, log=log, verbose=verbose) + + log( + logging.INFO, + "msix", + { + "input": dir_or_package, + }, + "Adding files from '{input}'", + ) + + if os.path.isdir(dir_or_package): + finder = FileFinder(dir_or_package) + else: + finder = JarFinder(dir_or_package, JarReader(dir_or_package)) + + values = get_application_ini_values( + finder, + dict(section="App", value="CodeName", fallback="Name"), + dict(section="App", value="Vendor"), + ) + + first = next(values) + if not displayname: + displayname = "Mozilla {}".format(first) + + if channel == "beta": + # Release (official) and Beta share branding. Differentiate Beta a little bit. + displayname += " Beta" + + second = next(values) + vendor = vendor or second + + # For `AppConstants.sys.mjs` and `brand.properties`, which are in the omnijar in packaged + # builds. The nested langpack XPI files can't be read by `mozjar.py`. + unpack_finder = UnpackFinder(finder, unpack_xpi=False) + + values = get_appconstants_sys_mjs_values( + unpack_finder, + "MOZ_OFFICIAL_BRANDING", + "MOZ_BUILD_APP", + "MOZ_APP_NAME", + "MOZ_APP_VERSION_DISPLAY", + "MOZ_BUILDID", + ) + try: + use_official_branding = {"true": True, "false": False}[next(values)] + except KeyError as err: + raise Exception( + f"Unexpected value '{err.args[0]}' found for 'MOZ_OFFICIAL_BRANDING'." + ) from None + + build_app = next(values) + + _temp = next(values) + if not app_name: + app_name = _temp + + if not version: + display_version = next(values) + buildid = next(values) + version = get_embedded_version(display_version, buildid) + log( + logging.INFO, + "msix", + { + "version": version, + "display_version": display_version, + "buildid": buildid, + }, + "AppConstants.sys.mjs display version is '{display_version}' and build ID is" + + " '{buildid}': embedded version will be '{version}'", + ) + + # TODO: Bug 1721922: localize this description via Fluent. + lines = [] + for _, f in unpack_finder.find("**/chrome/en-US/locale/branding/brand.properties"): + lines.extend( + line + for line in f.open().read().decode("utf-8").splitlines() + if "brandFullName" in line + ) + (brandFullName,) = lines # We expect exactly one definition. + _, _, brandFullName = brandFullName.partition("=") + brandFullName = brandFullName.strip() + + if channel == "beta": + # Release (official) and Beta share branding. Differentiate Beta a little bit. + brandFullName += " Beta" + + branding = get_branding( + use_official_branding, topsrcdir, build_app, unpack_finder, log + ) + if not os.path.isdir(branding): + raise Exception("branding dir {} does not exist".format(branding)) + + template = os.path.join(topsrcdir, build_app, "installer", "windows", "msix") + + # Discard everything after a '#' comment character. + locale_allowlist = set( + locale.partition("#")[0].strip().lower() + for locale in open(os.path.join(template, "msix-all-locales")).readlines() + if locale.partition("#")[0].strip() + ) + + # The convention is $MOZBUILD_STATE_PATH/cache/$FEATURE. + output_dir = mozpath.normsep( + mozpath.join( + get_state_dir(), "cache", "mach-msix", "msix-temp-{}".format(channel) + ) + ) + + # Like 'Firefox Package Root', 'Firefox Nightly Package Root', 'Firefox Beta + # Package Root'. This is `BrandFullName` in the installer, and we want to + # be close but to not match. By not matching, we hope to prevent confusion + # and/or errors between regularly installed builds and App Package builds. + instdir = "{} Package Root".format(displayname) + + # The standard package name is like "CompanyNoSpaces.ProductNoSpaces". + identity = identity or "{}.{}".format(vendor, displayname).replace(" ", "") + + # We might want to include the publisher ID hash here. I.e., + # "__{publisherID}". My locally produced MSIX was named like + # `Mozilla.MozillaFirefoxNightly_89.0.0.0_x64__4gf61r4q480j0`, suggesting also a + # missing field, but it's not necessary, since this is just an output file name. + package_output_name = "{identity}_{version}_{arch}".format( + identity=identity, version=version, arch=_MSIX_ARCH[arch] + ) + # The convention is $MOZBUILD_STATE_PATH/cache/$FEATURE. + default_output = mozpath.normsep( + mozpath.join( + get_state_dir(), "cache", "mach-msix", "{}.msix".format(package_output_name) + ) + ) + output = output or default_output + log(logging.INFO, "msix", {"output": output}, "Repackaging to: {output}") + + m = InstallManifest() + m.add_copy(mozpath.join(template, "Resources.pri"), "Resources.pri") + + m.add_pattern_copy(mozpath.join(branding, "msix", "Assets"), "**", "Assets") + m.add_pattern_copy(mozpath.join(template, "VFS"), "**", "VFS") + + copier = FileCopier() + + # TODO: Bug 1710147: filter out MSVCRT files and use a dependency instead. + for p, f in finder: + if not os.path.isdir(dir_or_package): + # In archived builds, `p` is like "firefox/firefox.exe"; we want just "firefox.exe". + pp = os.path.relpath(p, app_name) + else: + # In local builds and unpacked MSIX directories, `p` is like "firefox.exe" already. + pp = p + + if pp.startswith("distribution"): + # Treat any existing distribution as a distribution directory, + # potentially with language packs. This makes it easy to repack + # unpacked MSIXes. + distribution_dir = mozpath.join(dir_or_package, "distribution") + if distribution_dir not in distribution_dirs: + distribution_dirs.append(distribution_dir) + + continue + + copier.add(mozpath.normsep(mozpath.join("VFS", "ProgramFiles", instdir, pp)), f) + + # Locales to declare as supported in `AppxManifest.xml`. + locales = set(["en-US"]) + + for distribution_dir in [ + mozpath.join(template, "distribution") + ] + distribution_dirs: + log( + logging.INFO, + "msix", + {"dir": distribution_dir}, + "Adding distribution files from {dir}", + ) + + # In automation, we have no easy way to remap the names of artifacts fetched from dependent + # tasks. In particular, langpacks will be named like `target.langpack.xpi`. The fetch + # tasks do allow us to put them in a per-locale directory, so that the entire set can be + # fetched. Here we remap the names. + finder = FileFinder(distribution_dir) + + for p, f in finder: + locale = None + if os.path.basename(p) == "target.langpack.xpi": + # Turn "/path/to/LOCALE/target.langpack.xpi" into "LOCALE". This is how langpacks + # are presented in CI. + base, locale = os.path.split(os.path.dirname(p)) + + # Like "locale-LOCALE/langpack-LOCALE@firefox.mozilla.org.xpi". This is what AMO + # serves and how flatpak builds name langpacks, but not how snap builds name + # langpacks. I can't explain the discrepancy. + dest = mozpath.normsep( + mozpath.join( + base, + f"locale-{locale}", + f"langpack-{locale}@{app_name}.mozilla.org.xpi", + ) + ) + + log( + logging.DEBUG, + "msix", + {"path": p, "dest": dest}, + "Renaming langpack {path} to {dest}", + ) + + elif os.path.basename(p).startswith("langpack-"): + # Turn "/path/to/langpack-LOCALE@firefox.mozilla.org.xpi" into "LOCALE". This is + # how langpacks are presented from an unpacked MSIX. + _, _, locale = os.path.basename(p).partition("langpack-") + locale, _, _ = locale.partition("@") + dest = p + + else: + dest = p + + if locale: + locale = locale.strip().lower() + locales.add(locale) + log( + logging.DEBUG, + "msix", + {"locale": locale, "dest": dest}, + "Distributing locale '{locale}' from {dest}", + ) + + dest = mozpath.normsep( + mozpath.join("VFS", "ProgramFiles", instdir, "distribution", dest) + ) + if copier.contains(dest): + log( + logging.INFO, + "msix", + {"dest": dest, "path": mozpath.join(finder.base, p)}, + "Skipping duplicate: {dest} from {path}", + ) + continue + + log( + logging.DEBUG, + "msix", + {"dest": dest, "path": mozpath.join(finder.base, p)}, + "Adding distribution path: {dest} from {path}", + ) + + copier.add( + dest, + f, + ) + + locales.remove("en-US") + + # Windows MSIX packages support a finite set of locales: see + # https://docs.microsoft.com/en-us/windows/uwp/publish/supported-languages, which is encoded in + # https://searchfox.org/mozilla-central/source/browser/installer/windows/msix/msix-all-locales. + # We distribute all of the langpacks supported by the release channel in our MSIX, which is + # encoded in https://searchfox.org/mozilla-central/source/browser/locales/all-locales. But we + # only advertise support in the App manifest for the intersection of that set and the set of + # supported locales. + # + # We distribute all langpacks to avoid the following issue. Suppose a user manually installs a + # langpack that is not supported by Windows, and then updates the installed MSIX package. MSIX + # package upgrades are essentially paveover installs, so there is no opportunity for Firefox to + # update the langpack before the update. But, since all langpacks are bundled with the MSIX, + # that langpack will be up-to-date, preventing one class of YSOD. + unadvertised = set() + if locale_allowlist: + unadvertised = locales - locale_allowlist + locales = locales & locale_allowlist + for locale in sorted(unadvertised): + log( + logging.INFO, + "msix", + {"locale": locale}, + "Not advertising distributed locale '{locale}' that is not recognized by Windows", + ) + + locales = ["en-US"] + list(sorted(locales)) + resource_language_list = "\n".join( + f' <Resource Language="{locale}" />' for locale in locales + ) + + defines = { + "APPX_ARCH": _MSIX_ARCH[arch], + "APPX_DISPLAYNAME": brandFullName, + "APPX_DESCRIPTION": brandFullName, + # Like 'Mozilla.MozillaFirefox', 'Mozilla.MozillaFirefoxBeta', or + # 'Mozilla.MozillaFirefoxNightly'. + "APPX_IDENTITY": identity, + # Like 'Firefox Package Root', 'Firefox Nightly Package Root', 'Firefox + # Beta Package Root'. See above. + "APPX_INSTDIR": instdir, + # Like 'Firefox%20Package%20Root'. + "APPX_INSTDIR_QUOTED": urllib.parse.quote(instdir), + "APPX_PUBLISHER": publisher, + "APPX_PUBLISHER_DISPLAY_NAME": publisher_display_name, + "APPX_RESOURCE_LANGUAGE_LIST": resource_language_list, + "APPX_VERSION": version, + "MOZ_APP_DISPLAYNAME": displayname, + "MOZ_APP_NAME": app_name, + # Keep synchronized with `toolkit\mozapps\notificationserver\NotificationComServer.cpp`. + "MOZ_INOTIFICATIONACTIVATION_CLSID": "916f9b5d-b5b2-4d36-b047-03c7a52f81c8", + } + + m.add_preprocess( + mozpath.join(template, "AppxManifest.xml.in"), + "AppxManifest.xml", + [], + defines=defines, + marker="<!-- #", # So that we can have well-formed XML. + ) + m.populate_registry(copier) + + output_dir = mozpath.abspath(output_dir) + ensureParentDir(output_dir) + + start = time.monotonic() + result = copier.copy( + output_dir, remove_empty_directories=True, skip_if_older=not force + ) + if log: + log_copy_result(log, time.monotonic() - start, output_dir, result) + + if verbose: + # Dump AppxManifest.xml contents for ease of debugging. + log(logging.DEBUG, "msix", {}, "AppxManifest.xml") + log(logging.DEBUG, "msix", {}, ">>>") + for line in open(mozpath.join(output_dir, "AppxManifest.xml")).readlines(): + log(logging.DEBUG, "msix", {}, line[:-1]) # Drop trailing line terminator. + log(logging.DEBUG, "msix", {}, "<<<") + + if not makeappx: + makeappx = find_sdk_tool("makeappx.exe", log=log) + if not makeappx: + raise ValueError( + "makeappx is required; " "set MAKEAPPX or WINDOWSSDKDIR or PATH" + ) + + # `makeappx.exe` supports both slash and hyphen style arguments; `makemsix` + # supports only hyphen style. `makeappx.exe` allows to overwrite and to + # provide more feedback, so we prefer invoking with these flags. This will + # also accommodate `wine makeappx.exe`. + stdout = subprocess.run( + [makeappx], check=False, capture_output=True, universal_newlines=True + ).stdout + is_makeappx = "MakeAppx Tool" in stdout + + if is_makeappx: + args = [makeappx, "pack", "/d", output_dir, "/p", output, "/overwrite"] + else: + args = [makeappx, "pack", "-d", output_dir, "-p", output] + if verbose and is_makeappx: + args.append("/verbose") + joined = " ".join(shlex_quote(arg) for arg in args) + log(logging.INFO, "msix", {"args": args, "joined": joined}, "Invoking: {joined}") + + sys.stdout.flush() # Otherwise the subprocess output can be interleaved. + if verbose: + subprocess.check_call(args, universal_newlines=True) + else: + # Suppress output unless we fail. + try: + subprocess.check_output(args, universal_newlines=True) + except subprocess.CalledProcessError as e: + sys.stderr.write(e.output) + raise + + return output + + +def _sign_msix_win(output, force, log, verbose): + powershell_exe = find_sdk_tool("powershell.exe", log=log) + if not powershell_exe: + raise ValueError("powershell is required; " "set POWERSHELL or PATH") + + def powershell(argstring, check=True): + "Invoke `powershell.exe`. Arguments are given as a string to allow consumer to quote." + args = [powershell_exe, "-c", argstring] + joined = " ".join(shlex_quote(arg) for arg in args) + log( + logging.INFO, "msix", {"args": args, "joined": joined}, "Invoking: {joined}" + ) + return subprocess.run( + args, check=check, universal_newlines=True, capture_output=True + ).stdout + + signtool = find_sdk_tool("signtool.exe", log=log) + if not signtool: + raise ValueError( + "signtool is required; " "set SIGNTOOL or WINDOWSSDKDIR or PATH" + ) + + # Our first order of business is to find, or generate, a (self-signed) + # certificate. + + # These are baked into enough places under `browser/` that we need not + # extract constants. + vendor = "Mozilla" + publisher = "CN=Mozilla Corporation, OU=MSIX Packaging" + friendly_name = "Mozilla Corporation MSIX Packaging Test Certificate" + + # The convention is $MOZBUILD_STATE_PATH/cache/$FEATURE. + crt_path = mozpath.join( + get_state_dir(), + "cache", + "mach-msix", + "{}.crt".format(friendly_name).replace(" ", "_").lower(), + ) + crt_path = mozpath.abspath(crt_path) + ensureParentDir(crt_path) + + pfx_path = crt_path.replace(".crt", ".pfx") + + # TODO: maybe use an actual password. For now, just something that won't be + # brute-forced. + password = "193dbfc6-8ff7-4a95-8f32-6b4468626bd0" + + if force or not os.path.isfile(crt_path): + log( + logging.INFO, + "msix", + {"crt_path": crt_path}, + "Creating new self signed certificate at: {}".format(crt_path), + ) + + thumbprints = [ + thumbprint.strip() + for thumbprint in powershell( + ( + "Get-ChildItem -Path Cert:\CurrentUser\My" + '| Where-Object {{$_.Subject -Match "{}"}}' + '| Where-Object {{$_.FriendlyName -Match "{}"}}' + "| Select-Object -ExpandProperty Thumbprint" + ).format(vendor, friendly_name) + ).splitlines() + ] + if len(thumbprints) > 1: + raise Exception( + "Multiple certificates with friendly name found: {}".format( + friendly_name + ) + ) + + if len(thumbprints) == 1: + thumbprint = thumbprints[0] + else: + thumbprint = None + + if force or not thumbprint: + thumbprint = ( + powershell( + ( + 'New-SelfSignedCertificate -Type Custom -Subject "{}" ' + '-KeyUsage DigitalSignature -FriendlyName "{}"' + " -CertStoreLocation Cert:\CurrentUser\My" + ' -TextExtension @("2.5.29.37={{text}}1.3.6.1.5.5.7.3.3", ' + '"2.5.29.19={{text}}")' + "| Select-Object -ExpandProperty Thumbprint" + ).format(publisher, friendly_name) + ) + .strip() + .upper() + ) + + if not thumbprint: + raise Exception( + "Failed to find or create certificate with friendly name: {}".format( + friendly_name + ) + ) + + powershell( + 'Export-Certificate -Cert Cert:\CurrentUser\My\{} -FilePath "{}"'.format( + thumbprint, crt_path + ) + ) + log( + logging.INFO, + "msix", + {"crt_path": crt_path}, + "Exported public certificate: {crt_path}", + ) + + powershell( + ( + 'Export-PfxCertificate -Cert Cert:\CurrentUser\My\{} -FilePath "{}"' + ' -Password (ConvertTo-SecureString -String "{}" -Force -AsPlainText)' + ).format(thumbprint, pfx_path, password) + ) + log( + logging.INFO, + "msix", + {"pfx_path": pfx_path}, + "Exported private certificate: {pfx_path}", + ) + + # Second, to find the right thumbprint to use. We do this here in case + # we're coming back to an existing certificate. + + log( + logging.INFO, + "msix", + {"crt_path": crt_path}, + "Signing with existing self signed certificate: {crt_path}", + ) + + thumbprints = [ + thumbprint.strip() + for thumbprint in powershell( + 'Get-PfxCertificate -FilePath "{}" | Select-Object -ExpandProperty Thumbprint'.format( + crt_path + ) + ).splitlines() + ] + if len(thumbprints) > 1: + raise Exception("Multiple thumbprints found for PFX: {}".format(pfx_path)) + if len(thumbprints) == 0: + raise Exception("No thumbprints found for PFX: {}".format(pfx_path)) + thumbprint = thumbprints[0] + log( + logging.INFO, + "msix", + {"thumbprint": thumbprint}, + "Signing with certificate with thumbprint: {thumbprint}", + ) + + # Third, do the actual signing. + + args = [ + signtool, + "sign", + "/a", + "/fd", + "SHA256", + "/f", + pfx_path, + "/p", + password, + output, + ] + if not verbose: + subprocess.check_call(args, universal_newlines=True) + else: + # Suppress output unless we fail. + try: + subprocess.check_output(args, universal_newlines=True) + except subprocess.CalledProcessError as e: + sys.stderr.write(e.output) + raise + + # As a convenience to the user, tell how to use this certificate if it's not + # already trusted, and how to work with MSIX files more generally. + if verbose: + root_thumbprints = [ + root_thumbprint.strip() + for root_thumbprint in powershell( + "Get-ChildItem -Path Cert:\LocalMachine\Root\{} " + "| Select-Object -ExpandProperty Thumbprint".format(thumbprint), + check=False, + ).splitlines() + ] + if thumbprint not in root_thumbprints: + log( + logging.INFO, + "msix", + {"thumbprint": thumbprint}, + "Certificate with thumbprint not found in trusted roots: {thumbprint}", + ) + log( + logging.INFO, + "msix", + {"crt_path": crt_path, "output": output}, + r"""\ +# Usage +To trust this certificate (requires an elevated shell): +powershell -c 'Import-Certificate -FilePath "{crt_path}" -Cert Cert:\LocalMachine\Root\' +To verify this MSIX signature exists and is trusted: +powershell -c 'Get-AuthenticodeSignature -FilePath "{output}" | Format-List *' +To install this MSIX: +powershell -c 'Add-AppPackage -path "{output}"' +To see details after installing: +powershell -c 'Get-AppPackage -name Mozilla.MozillaFirefox(Beta,...)' + """.strip(), + ) + + return 0 + + +def _sign_msix_posix(output, force, log, verbose): + makeappx = find_sdk_tool("makeappx", log=log) + + if not makeappx: + raise ValueError("makeappx is required; " "set MAKEAPPX or PATH") + + openssl = find_sdk_tool("openssl", log=log) + + if not openssl: + raise ValueError("openssl is required; " "set OPENSSL or PATH") + + if "sign" not in subprocess.run(makeappx, capture_output=True).stdout.decode( + "utf-8" + ): + raise ValueError( + "makeappx must support 'sign' operation. ", + "You probably need to build Mozilla's version of it: ", + "https://github.com/mozilla/msix-packaging/tree/johnmcpms/signing", + ) + + def run_openssl(args, check=True, capture_output=True): + full_args = [openssl, *args] + joined = " ".join(shlex_quote(arg) for arg in full_args) + log( + logging.INFO, + "msix", + {"args": args}, + f"Invoking: {joined}", + ) + return subprocess.run( + full_args, + check=check, + capture_output=capture_output, + universal_newlines=True, + ) + + # These are baked into enough places under `browser/` that we need not + # extract constants. + cn = "Mozilla Corporation" + ou = "MSIX Packaging" + friendly_name = "Mozilla Corporation MSIX Packaging Test Certificate" + # Password is needed when generating the cert, but + # "makeappx" explicitly does _not_ support passing it + # so it ends up getting removed when we create the pfx + password = "temp" + + cache_dir = mozpath.join(get_state_dir(), "cache", "mach-msix") + ca_crt_path = mozpath.join(cache_dir, "MozillaMSIXCA.cer") + ca_key_path = mozpath.join(cache_dir, "MozillaMSIXCA.key") + csr_path = mozpath.join(cache_dir, "MozillaMSIX.csr") + crt_path = mozpath.join(cache_dir, "MozillaMSIX.cer") + key_path = mozpath.join(cache_dir, "MozillaMSIX.key") + pfx_path = mozpath.join( + cache_dir, + "{}.pfx".format(friendly_name).replace(" ", "_").lower(), + ) + pfx_path = mozpath.abspath(pfx_path) + ensureParentDir(pfx_path) + + if force or not os.path.isfile(pfx_path): + log( + logging.INFO, + "msix", + {"pfx_path": pfx_path}, + "Creating new self signed certificate at: {}".format(pfx_path), + ) + + # Ultimately, we only end up using the CA certificate + # and the pfx (aka pkcs12) bundle containing the signing key + # and certificate. The other things we create along the way + # are not used for subsequent signing for testing. + # To get those, we have to do a few things: + # 1) Create a new CA key and certificate + # 2) Create a new signing key + # 3) Create a CSR with that signing key + # 4) Create the certificate with the CA key+cert from the CSR + # 5) Convert the signing key and certificate to a pfx bundle + args = [ + "req", + "-x509", + "-days", + "7200", + "-sha256", + "-newkey", + "rsa:4096", + "-keyout", + ca_key_path, + "-out", + ca_crt_path, + "-outform", + "PEM", + "-subj", + f"/OU={ou} CA/CN={cn} CA", + "-passout", + f"pass:{password}", + ] + run_openssl(args) + args = [ + "genrsa", + "-des3", + "-out", + key_path, + "-passout", + f"pass:{password}", + ] + run_openssl(args) + args = [ + "req", + "-new", + "-key", + key_path, + "-out", + csr_path, + "-subj", + # We actually want these in the opposite order, to match what's + # included in the AppxManifest. Openssl ends up reversing these + # for some reason, so we put them in backwards here. + f"/OU={ou}/CN={cn}", + "-passin", + f"pass:{password}", + ] + run_openssl(args) + args = [ + "x509", + "-req", + "-sha256", + "-days", + "7200", + "-in", + csr_path, + "-CA", + ca_crt_path, + "-CAcreateserial", + "-CAkey", + ca_key_path, + "-out", + crt_path, + "-outform", + "PEM", + "-passin", + f"pass:{password}", + ] + run_openssl(args) + args = [ + "pkcs12", + "-export", + "-inkey", + key_path, + "-in", + crt_path, + "-name", + friendly_name, + "-passin", + f"pass:{password}", + # All three of these options (-keypbe, -certpbe, and -passout) + # are necessary to create a pfx bundle that won't even prompt + # for a password. If we miss one, we will still get a password + # prompt for the blank password. + "-keypbe", + "NONE", + "-certpbe", + "NONE", + "-passout", + "pass:", + "-out", + pfx_path, + ] + run_openssl(args) + + args = [makeappx, "sign", "-p", output, "-c", pfx_path] + if not verbose: + subprocess.check_call( + args, + universal_newlines=True, + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + ) + else: + # Suppress output unless we fail. + try: + subprocess.check_output(args, universal_newlines=True) + except subprocess.CalledProcessError as e: + sys.stderr.write(e.output) + raise + + if verbose: + log( + logging.INFO, + "msix", + { + "ca_crt_path": ca_crt_path, + "ca_crt": mozpath.basename(ca_crt_path), + "output_path": output, + "output": mozpath.basename(output), + }, + r"""\ +# Usage +First, transfer the root certificate ({ca_crt_path}) and signed MSIX +({output_path}) to a Windows machine. +To trust this certificate ({ca_crt_path}), run the following in an elevated shell: +powershell -c 'Import-Certificate -FilePath "{ca_crt}" -Cert Cert:\LocalMachine\Root\' +To verify this MSIX signature exists and is trusted: +powershell -c 'Get-AuthenticodeSignature -FilePath "{output}" | Format-List *' +To install this MSIX: +powershell -c 'Add-AppPackage -path "{output}"' +To see details after installing: +powershell -c 'Get-AppPackage -name Mozilla.MozillaFirefox(Beta,...)' + """.strip(), + ) + + +def sign_msix(output, force=False, log=None, verbose=False): + """Sign an MSIX with a locally generated self-signed certificate.""" + + if sys.platform.startswith("win"): + return _sign_msix_win(output, force, log, verbose) + else: + return _sign_msix_posix(output, force, log, verbose) diff --git a/python/mozbuild/mozbuild/repackaging/pkg.py b/python/mozbuild/mozbuild/repackaging/pkg.py new file mode 100644 index 0000000000..c6e276a5d3 --- /dev/null +++ b/python/mozbuild/mozbuild/repackaging/pkg.py @@ -0,0 +1,45 @@ +# 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/. + +import shutil +import tarfile +from pathlib import Path + +import mozfile +from mozpack.pkg import create_pkg + +from mozbuild.bootstrap import bootstrap_toolchain + + +def repackage_pkg(infile, output): + if not tarfile.is_tarfile(infile): + raise Exception("Input file %s is not a valid tarfile." % infile) + + xar_tool = bootstrap_toolchain("xar/xar") + if not xar_tool: + raise Exception("Could not find xar tool.") + mkbom_tool = bootstrap_toolchain("mkbom/mkbom") + if not mkbom_tool: + raise Exception("Could not find mkbom tool.") + # Note: CPIO isn't standard on all OS's + cpio_tool = shutil.which("cpio") + if not cpio_tool: + raise Exception("Could not find cpio.") + + with mozfile.TemporaryDirectory() as tmpdir: + mozfile.extract_tarball(infile, tmpdir) + + app_list = list(Path(tmpdir).glob("*.app")) + if len(app_list) != 1: + raise Exception( + "Input file should contain a single .app file. %s found." + % len(app_list) + ) + create_pkg( + source_app=Path(app_list[0]), + output_pkg=Path(output), + mkbom_tool=Path(mkbom_tool), + xar_tool=Path(xar_tool), + cpio_tool=Path(cpio_tool), + ) diff --git a/python/mozbuild/mozbuild/repackaging/test/python.toml b/python/mozbuild/mozbuild/repackaging/test/python.toml new file mode 100644 index 0000000000..d831439c78 --- /dev/null +++ b/python/mozbuild/mozbuild/repackaging/test/python.toml @@ -0,0 +1,4 @@ +[DEFAULT] +subsuite = "mozbuild" + +["test_msix.py"] diff --git a/python/mozbuild/mozbuild/repackaging/test/test_msix.py b/python/mozbuild/mozbuild/repackaging/test/test_msix.py new file mode 100644 index 0000000000..f6735dcc75 --- /dev/null +++ b/python/mozbuild/mozbuild/repackaging/test/test_msix.py @@ -0,0 +1,53 @@ +# 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/. + +import unittest + +from mozunit import main + +from mozbuild.repackaging.msix import get_embedded_version + + +class TestMSIX(unittest.TestCase): + def test_embedded_version(self): + """Test embedded version extraction.""" + + buildid = "YYYY0M0D0HMmSs" + for input, output in [ + ("X.0a1", "X.YY0M.D0H.0"), + ("X.YbZ", "X.Y.Z.0"), + ("X.Yesr", "X.Y.0.0"), + ("X.Y.Zesr", "X.Y.Z.0"), + ("X.YrcZ", "X.Y.Z.0"), + ("X.Y", "X.Y.0.0"), + ("X.Y.Z", "X.Y.Z.0"), + ]: + version = get_embedded_version(input, buildid) + self.assertEqual(version, output) + # Some parts of the MSIX packaging ecosystem require the final digit + # in the dotted quad to be 0. + self.assertTrue(version.endswith(".0")) + + buildid = "YYYYMmDdHhMmSs" + for input, output in [ + ("X.0a1", "X.YYMm.DdHh.0"), + ]: + version = get_embedded_version(input, buildid) + self.assertEqual(version, output) + # Some parts of the MSIX packaging ecosystem require the final digit + # in the dotted quad to be 0. + self.assertTrue(version.endswith(".0")) + + for input in [ + "X.Ya1", + "X.0a2", + "X.Y.ZbW", + "X.Y.ZrcW", + ]: + with self.assertRaises(ValueError): + get_embedded_version(input, buildid) + + +if __name__ == "__main__": + main() diff --git a/python/mozbuild/mozbuild/resources/html-build-viewer/build_resources.html b/python/mozbuild/mozbuild/resources/html-build-viewer/build_resources.html new file mode 100644 index 0000000000..9daf30178b --- /dev/null +++ b/python/mozbuild/mozbuild/resources/html-build-viewer/build_resources.html @@ -0,0 +1,694 @@ +<!-- 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/. --> +<!DOCTYPE html> +<html lang="en"> + <head> + <title>Build System Resource Usage</title> + + <meta charset='utf-8'> + <script src="https://d3js.org/d3.v3.min.js" charset="utf-8"></script> + <link rel="stylesheet" href="https://firefoxux.github.io/design-tokens/photon-colors/photon-colors.css" charset="utf-8"> + <style> + +svg { + overflow: visible; +} +body { + background-color: var(--grey-20); + font-family: sans-serif; + padding: 30px 80px; + display: flex; + flex-direction: column; +} + +h1 { + font-size: 2.3rem; + margin: 40px 0 20px; +} + +.dashboard-card { + padding: 32px 80px; + background-color: var(--white-100); + border-radius: 8px; + margin: 60px 0; +} + +h2 { + color: var(--grey-80); + margin-bottom: 30px; +} + +.chart { + padding-top: 20px; +} + +.grid-list ul { + list-style: none; + padding: 0; + margin: 0; + display: grid; + grid-template-columns: max-content max-content max-content; + gap: 15px 4px; +} + +.grid-list ul li { + grid-column: 1 / -1; + display: grid; + grid-template-columns: subgrid; + place-items: baseline; +} + +.grid-list ul li .label { + color: var(--grey-50); + padding-right: 40px; +} + +.grid-list ul li .value { + justify-self: end; + font-size: 1.3em; +} + +.axis path, +.axis line { + fill: none; + stroke: #000; + shape-rendering: crispEdges; +} + +.grid { + stroke: gray; + stroke-dasharray: 3, 2; + opacity: 0.4; +} + +.area { + fill: var(--blue-50); +} + +.graphs { + text-anchor: end; +} + +.timeline { + fill: var(--blue-40); + stroke: var(--blue-70); + stroke-width: 1; +} + +.short { + fill: var(--grey-50); + stroke: var(--blue-70); + stroke-width: 1; +} + +#tooltip { + z-index: 10; + position: fixed; + background: var(--white-100); + padding: 20px; + border-radius: 5px; + border: 1px solid var(--grey-20); +} + +.align-self-start { + align-self: start; +} + +/* utility-classes from Firefox Photon style guide "https://design.firefox.com/photon/"" */ + +.shadow-10 { + box-shadow: 0 1px 4px var(--grey-90-a10); +} + +.shadow-20 { + box-shadow: 0 2px 8px var(--grey-90-a10); +} + +.shadow-30 { + box-shadow: 0 4px 16px var(--grey-90-a10); +} + +.shadow-custom { + box-shadow: 0 8px 12px 1px rgba(29,17,51,.04),0 3px 16px 2px rgba(9,32,77,.12),0 5px 10px -3px rgba(29,17,51,.12); +} + + + </style> + </head> + <body> + <script> +var currentResources; + +/** + * Interface for a build resources JSON file. + */ +function BuildResources(data) { + if (data.version < 1 || data.version > 3) { + throw new Error("Unsupported version of the JSON format: " + data.version); + } + + this.resources = []; + + var cpu_fields = data.cpu_times_fields; + var io_fields = data.io_fields; + var virt_fields = data.virt_fields; + var swap_fields = data.swap_fields; + + function convert(dest, source, sourceKey, destKey, fields) { + var i = 0; + fields.forEach(function (field) { + dest[destKey][field] = source[sourceKey][i]; + i++; + }); + } + + var offset = data.start; + var cpu_times_totals = {}; + + cpu_fields.forEach(function (field) { + cpu_times_totals[field] = 0; + }); + + this.ioTotal = {}; + var i = 0; + io_fields.forEach(function (field) { + this.ioTotal[field] = data.overall.io[i]; + i++; + }.bind(this)); + + data.samples.forEach(function (sample) { + var entry = { + start: sample.start - offset, + end: sample.end - offset, + duration: sample.duration, + cpu_percent: sample.cpu_percent_mean, + cpu_times: {}, + cpu_times_percents: {}, + io: {}, + virt: {}, + swap: {}, + }; + + convert(entry, sample, "cpu_times_sum", "cpu_times", cpu_fields); + convert(entry, sample, "io", "io", io_fields); + convert(entry, sample, "virt", "virt", virt_fields); + convert(entry, sample, "swap", "swap", swap_fields); + + var total = 0; + for (var k in entry.cpu_times) { + cpu_times_totals[k] += entry.cpu_times[k]; + total += entry.cpu_times[k]; + } + + for (var k in entry.cpu_times) { + if (total == 0) { + if (k == "idle") { + entry.cpu_times_percents[k] = 100; + } else { + entry.cpu_times_percents[k] = 0; + } + } else { + entry.cpu_times_percents[k] = entry.cpu_times[k] / total * 100; + } + } + + this.resources.push(entry); + }.bind(this)); + + this.virt_fields = virt_fields; + this.cpu_times_fields = []; + + // Filter out CPU fields that have no values. + for (var k in cpu_times_totals) { + var v = cpu_times_totals[k]; + if (v) { + this.cpu_times_fields.push(k); + continue; + } + + this.resources.forEach(function (entry) { + delete entry.cpu_times[k]; + delete entry.cpu_times_percents[k]; + }); + } + + this.offset = offset; + this.data = data; +} + +BuildResources.prototype = Object.freeze({ + get start() { + return this.data.start; + }, + + get startDate() { + return new Date(this.start * 1000); + }, + + get end() { + return this.data.end; + }, + + get endDate() { + return new Date(this.end * 1000); + }, + + get duration() { + return this.data.duration; + }, + + get sample_times() { + var times = []; + this.resources.forEach(function (sample) { + times.push(sample.start); + }); + + return times; + }, + + get cpuPercent() { + return this.data.overall.cpu_percent_mean; + }, + + get tiers() { + var t = []; + + this.data.phases.forEach(function (e) { + t.push(e.name); + }); + + return t; + }, + + getTier: function (tier) { + for (var i = 0; i < this.data.phases.length; i++) { + var t = this.data.phases[i]; + + if (t.name == tier) { + return t; + } + } + }, +}); + +function format_percent(d, i) { + return d + "%"; +} + +const updateChartsOnResizeWindow = () => addEventListener('resize', updateResourcesGraph); +updateChartsOnResizeWindow(); + +// layout spacing to set charts widths +const marginBodyX = 8; +const paddingBodyX = 80; +const marginCardsX = 0; +const paddingCardsX = 80; + +function updateResourcesGraph() { + renderResources("cpu_graph", currentResources, 400, "cpu_times_fields", "cpu_times_percents", 100, format_percent, [ + ["nice", "#0d9fff"], + ["irq", "#ff0d9f"], + ["softirq", "#ff0d9f"], + ["steal", "#000000"], + ["guest", "#000000"], + ["guest_nice", "#000000"], + ["system", "var(--purple-80)"], + ["iowait", "#ff0d25"], + ["user", "var(--magenta-50)"], + ["idle", "var(--grey-20)"], + ]); + // On macos, there doesn't seem to be a combination of values that sums up to + // the total, so just use the percentage. Only macos has a "wired" value. + if ('wired' in currentResources.resources[0].virt) { + renderResources("mem_graph", currentResources, 200, "virt_fields", "virt", 100, format_percent, [ + ["percent", "var(--blue-50"], + ]); + } else { + renderResources("mem_graph", currentResources, 200, "virt_fields", "virt", currentResources.resources[0].virt['total'], d3.format("s"), [ + ["used", "var(--blue-50"], + ["buffers", "#f65c5c"], + ["cached", "var(--orange-50)"], + ["free", "var(--grey-20)"], + ]); + } + renderTimeline("tiers", currentResources); + document.getElementById("wall_time").textContent = Math.round(currentResources.duration * 100) / 100; + document.getElementById("start_date").textContent = currentResources.startDate.toLocaleString(); + document.getElementById("end_date").textContent = currentResources.endDate.toLocaleString(); + document.getElementById("cpu_percent").textContent = Math.round(currentResources.cpuPercent * 100) / 100; + document.getElementById("write_bytes").textContent = currentResources.ioTotal["write_bytes"]; + document.getElementById("read_bytes").textContent = currentResources.ioTotal["read_bytes"]; + document.getElementById("write_time").textContent = currentResources.ioTotal["write_time"]; + document.getElementById("read_time").textContent = currentResources.ioTotal["read_time"]; +} + +function renderKey(key) { + d3.json(key, function onResource(error, response) { + if (error) { + alert("Data not available. Is the server still running?"); + return; + } + + currentResources = new BuildResources(response); + updateResourcesGraph(); + }); +} + +function renderResources(id, resources, height, fields_attr, data_attr, max_value, tick_format, layers) { + document.getElementById(id).innerHTML = ""; + + const margin = {top: 20, right: 20, bottom: 20, left: 50}; + const width = window.innerWidth - 2 * (marginBodyX + paddingBodyX + marginCardsX + paddingCardsX) - margin.left; + var heightChart = height - margin.top - margin.bottom; + + var x = d3.scale.linear() + .range([0, width]) + .domain(d3.extent(resources.resources, function (d) { return d.start; })) + ; + var y = d3.scale.linear() + .range([heightChart, 0]) + .domain([0, max_value]) + ; + + var xAxis = d3.svg.axis() + .scale(x) + .orient("bottom") + ; + var yAxis = d3.svg.axis() + .scale(y) + .orient("left") + .tickFormat(tick_format) + ; + + var area = d3.svg.area() + .x(function (d) { return x(d.start); }) + .y0(function(d) { return y(d.y0); }) + .y1(function(d) { return y(d.y0 + d.y); }) + ; + + var stack = d3.layout.stack() + .values(function (d) { return d.values; }) + ; + + // Manually control the layer order because we want it consistent and want + // to inject some sanity. + var layers = layers.filter(function (l) { + return resources[fields_attr].indexOf(l[0]) != -1; + }); + + // Draw a legend. + var legend = d3.select("#" + id) + .append("svg") + .attr("width", width + margin.left + margin.right) + .attr("height", 15) + .append("g") + .attr("class", "legend") + ; + + legend.selectAll("g") + .data(layers) + .enter() + .append("g") + .each(function (d, i) { + var g = d3.select(this); + g.append("rect") + .attr("x", i * 100 + 20) + .attr("y", 0) + .attr("width", 10) + .attr("height", 10) + .style("fill", d[1]) + ; + g.append("text") + .attr("x", i * 100 + 40) + .attr("y", 10) + .attr("height", 10) + .attr("width", 70) + .text(d[0]) + ; + }) + ; + + var svg = d3.select("#" + id).append("svg") + .attr("width", width) + .attr("height", heightChart + margin.top + margin.bottom) + .append("g") + .attr("transform", "translate(" + margin.left + "," + margin.top + ")") + ; + + var data = stack(layers.map(function (layer) { + return { + name: layer[0], + color: layer[1], + values: resources.resources.map(function (d) { + return { + start: d.start, + y: d[data_attr][layer[0]], + }; + }), + }; + })); + + var graphs = svg.selectAll(".graphs") + .data(data) + .enter().append("g") + .attr("class", "graphs") + ; + + graphs.append("path") + .attr("class", "area") + .attr("d", function (d) { return area(d.values); }) + .style("fill", function (d) { return d.color; }) + ; + + svg.append("g") + .attr("class", "x axis") + .attr("transform", "translate(0," + heightChart + ")") + .call(xAxis) + ; + + svg.append("g") + .attr("class", "x grid") + .attr("transform", "translate(0," + heightChart + ")") + .call(xAxis.tickSize(-heightChart, 0, 0).tickFormat("")) + ; + + svg.append("g") + .attr("class", "y axis") + .call(yAxis) + ; + + svg.append("g") + .attr("class", "y grid") + .call(yAxis.tickSize(-width, 0, 0).tickFormat("")) + ; +} + +function renderTimeline(id, resources) { + document.getElementById(id).innerHTML = ""; + + var margin = {top: 20, right: 20, bottom: 20, left: 50}; + const width = window.innerWidth - 2 * (marginBodyX + paddingBodyX + marginCardsX + paddingCardsX) - margin.left; + + var x = d3.scale.linear() + .range([0, width]) + .domain(d3.extent(resources.resources, function (d) { return d.start; })) + ; + // Now we render a timeline of sorts of the tiers + // There is a row of rectangles that visualize divisions between the + // different items. We use the same x scale as the resource graph so times + // line up properly. + svg = d3.select("#" + id).append("svg") + .attr("width", width) + .attr("height", 100) + .append("g") + ; + + var y = d3.scale.linear().range([10, 0]).domain([0, 1]); + + resources.tiers.forEach(function (t, i) { + var tier = resources.getTier(t); + + var x_start = x(tier.start - resources.offset); + var x_end = x(tier.end - resources.offset); + + svg.append("rect") + .attr("x", x_start) + .attr("y", 20) + .attr("height", 30) + .attr("width", x_end - x_start) + .attr("class", "timeline tier") + .attr("tier", t) + ; + }); + + function getEntry(element) { + var tier = element.getAttribute("tier"); + + var entry = resources.getTier(tier); + entry.tier = tier; + + return entry; + } + + d3.selectAll(".timeline") + .on("mouseenter", function () { + var entry = getEntry(this); + + d3.select("#tt_tier").html(entry.tier); + d3.select("#tt_duration").html(entry.duration || "n/a"); + d3.select("#tt_cpu_percent").html(entry.cpu_percent_mean || "n/a"); + + d3.select("#tooltip").style("display", ""); + }) + .on("mouseleave", function () { + var tooltip = d3.select("#tooltip"); + tooltip.style("display", "none"); + }) + .on("mousemove", function () { + var e = d3.event; + x_offset = 10; + + if (e.clientX > window.innerWidth / 2) { + x_offset = -150; + } + + d3.select("#tooltip") + .style("left", (e.clientX + x_offset) + "px") + .style("top", (e.clientY + 10) + "px") + ; + }) + ; +} + +function initData(data) { + var list = d3.select("#list"); + // Clear the list if it wasn't already empty. + list.selectAll("*").remove(); + list.style("display", "none"); + + if (!data) { + return; + } + // If the data contains a list of files, use that list. + // Otherwise, we expect it's directly resources info data. + if (Object.keys(data).length == 1 && "files" in data) { + if (data.files.length > 1) { + for (file of data.files) { + list.append("option").attr("value", file).text(file); + } + list.style("display", "inline"); + } + renderKey(data.files[0]); + } else { + currentResources = new BuildResources(data); + updateResourcesGraph(); + } +} + +document.addEventListener("DOMContentLoaded", function() { + var list = d3.select("#list"); + list.on("change", function() {renderKey(this.value);}) + d3.json("build_resources.json", function onList(error, response) { + initData(response); + }); +}, false); + +document.addEventListener("drop", function(event) { + event.preventDefault(); + var uris = event.dataTransfer.getData("text/uri-list"); + if (uris) { + var data = { + files: uris.split(/\r\n|\r|\n/).filter(uri => !uri.startsWith("#")), + }; + initData(data); + } +}, false); + +document.addEventListener("dragover", function(event) { + // prevent default to allow drop + event.preventDefault(); +}, false); + + </script> + <h1>Build Resource Usage Report</h1> + + <div id="tooltip" class="shadow-30" style="display: none;"> + <div class="grid-list"><ul> + <li> + <div class="label">Tier</div> + <div class="value" id="tt_tier"></div> + </li> + <li> + <div class="label">Duration</div> + <div class="value" id="tt_duration"></div> + </li> + <li> + <div class="label">CPU %</div> + <div class="value" id="tt_cpu_percent"></div> + </li> + </ul></div> + </div> + + <select id="list" style="display: none"></select> + <div class="dashboard-card shadow-10"> + <h2>CPU</h2> + <div id="cpu_graph" class="chart"></div> + </div> + <div class="dashboard-card shadow-10"> + <h2>Memory</h2> + <div id="mem_graph" class="chart"></div> + </div> + <div class="dashboard-card shadow-10"> + <h2>Tiers</h2> + <div id="tiers"></div> + </div> + <div class="dashboard-card shadow-10 align-self-start"> + <h2>Summary</h2> + <div id="summary" class="grid-list" style="padding-top: 20px"> + <ul> + <li> + <div class="label">Wall Time (s)</div> + <div class="value" id="wall_time"></div> + <div class="unit">s</div> + </li> + <li> + <div class="label">Start Date</div> + <div class="value" id="start_date"></div> + <div class="unit"></div> + </li> + <li> + <div class="label">End Date</div> + <div class="value" id="end_date"></div> + <div class="unit"></div> + </li> + <li> + <div class="label">CPU %</div> + <div class="value" id="cpu_percent"></div> + <div class="unit">%</div> + </li> + <li> + <div class="label">Write Bytes</div> + <div class="value" id="write_bytes"></div> + <div class="unit">B</div> + </li> + <li> + <div class="label">Read Bytes</div> + <div class="value" id="read_bytes"></div> + <div class="unit">B</div> + </li> + <li> + <div class="label">Write Time</div> + <div class="value" id="write_time"></div> + <div class="unit"></div> + </li> + <li> + <div class="label">Read Time</div> + <div class="value" id="read_time"></div> + <div class="unit"></div> + </li> + </ul> + </div> + </div> + </body> +</html> diff --git a/python/mozbuild/mozbuild/schedules.py b/python/mozbuild/mozbuild/schedules.py new file mode 100644 index 0000000000..5f484ed377 --- /dev/null +++ b/python/mozbuild/mozbuild/schedules.py @@ -0,0 +1,77 @@ +# 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/. + +""" +Constants for SCHEDULES configuration in moz.build files and for +skip-unless-schedules optimizations in task-graph generation. +""" + +# TODO: ideally these lists could be specified in moz.build itself + +# Inclusive components are those which are scheduled when certain files are +# changed, but do not run by default. These are generally added to +# `SCHEDULES.inclusive` using `+=`, but can also be used as exclusive +# components for files which *only* affect the named component. +INCLUSIVE_COMPONENTS = [ + "docs", + "py-lint", + "js-lint", + "yaml-lint", + # inclusive test suites -- these *only* run when certain files have changed + "jittest", + "test-verify", + "test-verify-gpu", + "test-verify-wpt", + "test-coverage", + "test-coverage-wpt", + "jsreftest", + "android-hw-gfx", + "rusttests", +] +INCLUSIVE_COMPONENTS = sorted(INCLUSIVE_COMPONENTS) + +# Exclusive components are those which are scheduled by default, but for which +# some files *only* affect that component. For example, most files affect all +# platforms, but platform-specific files exclusively affect a single platform. +# These components are assigned to `SCHEDULES.exclusive` with `=`. Each comment +# denotes a new mutually exclusive set of groups that tasks can belong to. +EXCLUSIVE_COMPONENTS = [ + # os families + "android", + "linux", + "macosx", + "windows", + # broad test harness categories + "awsy", + "condprofile", + "cppunittest", + "firefox-ui", + "fuzztest", + "geckoview-junit", + "gtest", + "marionette", + "mochitest", + "raptor", + "reftest", + "talos", + "telemetry-tests-client", + "xpcshell", + "xpcshell-coverage", + "web-platform-tests", + # specific test suites + "crashtest", + "mochitest-a11y", + "mochitest-browser-a11y", + "mochitest-browser-media", + "mochitest-browser-chrome", + "mochitest-chrome", + "mochitest-plain", + "web-platform-tests-crashtest", + "web-platform-tests-print-reftest", + "web-platform-tests-reftest", + "web-platform-tests-wdspec", + "nss", +] +EXCLUSIVE_COMPONENTS = sorted(EXCLUSIVE_COMPONENTS) +ALL_COMPONENTS = INCLUSIVE_COMPONENTS + EXCLUSIVE_COMPONENTS diff --git a/python/mozbuild/mozbuild/shellutil.py b/python/mozbuild/mozbuild/shellutil.py new file mode 100644 index 0000000000..fab3ae6086 --- /dev/null +++ b/python/mozbuild/mozbuild/shellutil.py @@ -0,0 +1,210 @@ +# 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/. + +import re + + +def _tokens2re(**tokens): + # Create a pattern for non-escaped tokens, in the form: + # (?<!\\)(?:a|b|c...) + # This is meant to match patterns a, b, or c, or ... if they are not + # preceded by a backslash. + # where a, b, c... are in the form + # (?P<name>pattern) + # which matches the pattern and captures it in a named match group. + # The group names and patterns are given as arguments. + all_tokens = "|".join( + "(?P<%s>%s)" % (name, value) for name, value in tokens.items() + ) + nonescaped = r"(?<!\\)(?:%s)" % all_tokens + + # The final pattern matches either the above pattern, or an escaped + # backslash, captured in the "escape" match group. + return re.compile("(?:%s|%s)" % (nonescaped, r"(?P<escape>\\\\)")) + + +UNQUOTED_TOKENS_RE = _tokens2re( + whitespace=r"[\t\r\n ]+", + quote=r'[\'"]', + comment="#", + special=r"[<>&|`(){}$;\*\?]", + backslashed=r"\\[^\\]", +) + +DOUBLY_QUOTED_TOKENS_RE = _tokens2re( + quote='"', + backslashedquote=r'\\"', + special=r"\$", + backslashed=r'\\[^\\"]', +) + +ESCAPED_NEWLINES_RE = re.compile(r"\\\n") + +# This regexp contains the same characters as all those listed in +# UNQUOTED_TOKENS_RE. Please keep in sync. +SHELL_QUOTE_RE = re.compile(r"[\\\t\r\n \'\"#<>&|`(){}$;\*\?]") + + +class MetaCharacterException(Exception): + def __init__(self, char): + self.char = char + + +class _ClineSplitter(object): + """ + Parses a given command line string and creates a list of command + and arguments, with wildcard expansion. + """ + + def __init__(self, cline): + self.arg = None + self.cline = cline + self.result = [] + self._parse_unquoted() + + def _push(self, str): + """ + Push the given string as part of the current argument + """ + if self.arg is None: + self.arg = "" + self.arg += str + + def _next(self): + """ + Finalize current argument, effectively adding it to the list. + """ + if self.arg is None: + return + self.result.append(self.arg) + self.arg = None + + def _parse_unquoted(self): + """ + Parse command line remainder in the context of an unquoted string. + """ + while self.cline: + # Find the next token + m = UNQUOTED_TOKENS_RE.search(self.cline) + # If we find none, the remainder of the string can be pushed to + # the current argument and the argument finalized + if not m: + self._push(self.cline) + break + # The beginning of the string, up to the found token, is part of + # the current argument + if m.start(): + self._push(self.cline[: m.start()]) + self.cline = self.cline[m.end() :] + + match = {name: value for name, value in m.groupdict().items() if value} + if "quote" in match: + # " or ' start a quoted string + if match["quote"] == '"': + self._parse_doubly_quoted() + else: + self._parse_quoted() + elif "comment" in match: + # Comments are ignored. The current argument can be finalized, + # and parsing stopped. + break + elif "special" in match: + # Unquoted, non-escaped special characters need to be sent to a + # shell. + raise MetaCharacterException(match["special"]) + elif "whitespace" in match: + # Whitespaces terminate current argument. + self._next() + elif "escape" in match: + # Escaped backslashes turn into a single backslash + self._push("\\") + elif "backslashed" in match: + # Backslashed characters are unbackslashed + # e.g. echo \a -> a + self._push(match["backslashed"][1]) + else: + raise Exception("Shouldn't reach here") + if self.arg: + self._next() + + def _parse_quoted(self): + # Single quoted strings are preserved, except for the final quote + index = self.cline.find("'") + if index == -1: + raise Exception("Unterminated quoted string in command") + self._push(self.cline[:index]) + self.cline = self.cline[index + 1 :] + + def _parse_doubly_quoted(self): + if not self.cline: + raise Exception("Unterminated quoted string in command") + while self.cline: + m = DOUBLY_QUOTED_TOKENS_RE.search(self.cline) + if not m: + raise Exception("Unterminated quoted string in command") + self._push(self.cline[: m.start()]) + self.cline = self.cline[m.end() :] + match = {name: value for name, value in m.groupdict().items() if value} + if "quote" in match: + # a double quote ends the quoted string, so go back to + # unquoted parsing + return + elif "special" in match: + # Unquoted, non-escaped special characters in a doubly quoted + # string still have a special meaning and need to be sent to a + # shell. + raise MetaCharacterException(match["special"]) + elif "escape" in match: + # Escaped backslashes turn into a single backslash + self._push("\\") + elif "backslashedquote" in match: + # Backslashed double quotes are un-backslashed + self._push('"') + elif "backslashed" in match: + # Backslashed characters are kept backslashed + self._push(match["backslashed"]) + + +def split(cline): + """ + Split the given command line string. + """ + s = ESCAPED_NEWLINES_RE.sub("", cline) + return _ClineSplitter(s).result + + +def _quote(s): + """Given a string, returns a version that can be used literally on a shell + command line, enclosing it with single quotes if necessary. + + As a special case, if given an int, returns a string containing the int, + not enclosed in quotes. + """ + if type(s) == int: + return "%d" % s + + # Empty strings need to be quoted to have any significance + if s and not SHELL_QUOTE_RE.search(s) and not s.startswith("~"): + return s + + # Single quoted strings can contain any characters unescaped except the + # single quote itself, which can't even be escaped, so the string needs to + # be closed, an escaped single quote added, and reopened. + t = type(s) + return t("'%s'") % s.replace(t("'"), t("'\\''")) + + +def quote(*strings): + """Given one or more strings, returns a quoted string that can be used + literally on a shell command line. + + >>> quote('a', 'b') + "a b" + >>> quote('a b', 'c') + "'a b' c" + """ + return " ".join(_quote(s) for s in strings) + + +__all__ = ["MetaCharacterException", "split", "quote"] diff --git a/python/mozbuild/mozbuild/sphinx.py b/python/mozbuild/mozbuild/sphinx.py new file mode 100644 index 0000000000..4d7afb621c --- /dev/null +++ b/python/mozbuild/mozbuild/sphinx.py @@ -0,0 +1,293 @@ +# 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/. + +import importlib +from pathlib import Path + +from docutils import nodes +from docutils.parsers.rst import Directive +from mots.config import FileConfig +from mots.directory import Directory +from mots.export import export_to_format +from sphinx.util.docstrings import prepare_docstring +from sphinx.util.docutils import ReferenceRole + + +def function_reference(f, attr, args, doc): + lines = [] + + lines.extend( + [ + f, + "-" * len(f), + "", + ] + ) + + docstring = prepare_docstring(doc) + + lines.extend( + [ + docstring[0], + "", + ] + ) + + arg_types = [] + + for t in args: + if isinstance(t, list): + inner_types = [t2.__name__ for t2 in t] + arg_types.append(" | ".join(inner_types)) + continue + + arg_types.append(t.__name__) + + arg_s = "(%s)" % ", ".join(arg_types) + + lines.extend( + [ + ":Arguments: %s" % arg_s, + "", + ] + ) + + lines.extend(docstring[1:]) + lines.append("") + + return lines + + +def variable_reference(v, st_type, in_type, doc): + lines = [ + v, + "-" * len(v), + "", + ] + + docstring = prepare_docstring(doc) + + lines.extend( + [ + docstring[0], + "", + ] + ) + + lines.extend( + [ + ":Storage Type: ``%s``" % st_type.__name__, + ":Input Type: ``%s``" % in_type.__name__, + "", + ] + ) + + lines.extend(docstring[1:]) + lines.append("") + + return lines + + +def special_reference(v, func, typ, doc): + lines = [ + v, + "-" * len(v), + "", + ] + + docstring = prepare_docstring(doc) + + lines.extend( + [ + docstring[0], + "", + ":Type: ``%s``" % typ.__name__, + "", + ] + ) + + lines.extend(docstring[1:]) + lines.append("") + + return lines + + +def format_module(m): + lines = [] + + lines.extend( + [ + ".. note::", + " moz.build files' implementation includes a ``Path`` class.", + ] + ) + path_docstring_minus_summary = prepare_docstring(m.Path.__doc__)[2:] + lines.extend([" " + line for line in path_docstring_minus_summary]) + + for subcontext, cls in sorted(m.SUBCONTEXTS.items()): + lines.extend( + [ + ".. _mozbuild_subcontext_%s:" % subcontext, + "", + "Sub-Context: %s" % subcontext, + "=============" + "=" * len(subcontext), + "", + ] + ) + lines.extend(prepare_docstring(cls.__doc__)) + if lines[-1]: + lines.append("") + + for k, v in sorted(cls.VARIABLES.items()): + lines.extend(variable_reference(k, *v)) + + lines.extend( + [ + "Variables", + "=========", + "", + ] + ) + + for v in sorted(m.VARIABLES): + lines.extend(variable_reference(v, *m.VARIABLES[v])) + + lines.extend( + [ + "Functions", + "=========", + "", + ] + ) + + for func in sorted(m.FUNCTIONS): + lines.extend(function_reference(func, *m.FUNCTIONS[func])) + + lines.extend( + [ + "Special Variables", + "=================", + "", + ] + ) + + for v in sorted(m.SPECIAL_VARIABLES): + lines.extend(special_reference(v, *m.SPECIAL_VARIABLES[v])) + + return lines + + +def find_mots_config_path(app): + """Find and return mots config path if it exists.""" + base_path = Path(app.srcdir).parent + config_path = base_path / "mots.yaml" + if config_path.exists(): + return config_path + + +def export_mots(config_path): + """Load mots configuration and export it to file.""" + # Load from disk and initialize configuration and directory. + config = FileConfig(config_path) + config.load() + directory = Directory(config) + directory.load() + + # Fetch file format (i.e., "rst") and export path. + frmt = config.config["export"]["format"] + path = config_path.parent / config.config["export"]["path"] + + # Generate output. + output = export_to_format(directory, frmt) + + # Create export directory if it does not exist. + path.parent.mkdir(parents=True, exist_ok=True) + + # Write changes to disk. + with path.open("w", encoding="utf-8") as f: + f.write(output) + + +class MozbuildSymbols(Directive): + """Directive to insert mozbuild sandbox symbol information.""" + + required_arguments = 1 + + def run(self): + module = importlib.import_module(self.arguments[0]) + fname = module.__file__ + if fname.endswith(".pyc"): + fname = fname[0:-1] + + self.state.document.settings.record_dependencies.add(fname) + + # We simply format out the documentation as rst then feed it back + # into the parser for conversion. We don't even emit ourselves, so + # there's no record of us. + self.state_machine.insert_input(format_module(module), fname) + + return [] + + +class Searchfox(ReferenceRole): + """Role which links a relative path from the source to it's searchfox URL. + + Can be used like: + + See :searchfox:`browser/base/content/browser-places.js` for more details. + + Will generate a link to + ``https://searchfox.org/mozilla-central/source/browser/base/content/browser-places.js`` + + The example above will use the path as the text, to use custom text: + + See :searchfox:`this file <browser/base/content/browser-places.js>` for + more details. + + To specify a different source tree: + + See :searchfox:`mozilla-beta:browser/base/content/browser-places.js` + for more details. + """ + + def run(self): + base = "https://searchfox.org/{source}/source/{path}" + + if ":" in self.target: + source, path = self.target.split(":", 1) + else: + source = "mozilla-central" + path = self.target + + url = base.format(source=source, path=path) + + if self.has_explicit_title: + title = self.title + else: + title = path + + node = nodes.reference(self.rawtext, title, refuri=url, **self.options) + return [node], [] + + +def setup(app): + from moztreedocs import manager + + app.add_directive("mozbuildsymbols", MozbuildSymbols) + app.add_role("searchfox", Searchfox()) + + # Unlike typical Sphinx installs, our documentation is assembled from + # many sources and staged in a common location. This arguably isn't a best + # practice, but it was the easiest to implement at the time. + # + # Here, we invoke our custom code for staging/generating all our + # documentation. + + # Export and write "governance" documentation to disk. + config_path = find_mots_config_path(app) + if config_path: + export_mots(config_path) + + manager.generate_docs(app) + app.srcdir = manager.staging_dir diff --git a/python/mozbuild/mozbuild/telemetry.py b/python/mozbuild/mozbuild/telemetry.py new file mode 100644 index 0000000000..ce2e99ca73 --- /dev/null +++ b/python/mozbuild/mozbuild/telemetry.py @@ -0,0 +1,191 @@ +# 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/. + +""" +This file contains functions used for telemetry. +""" + +import os +import platform +import sys + +import distro +import mozpack.path as mozpath + + +def cpu_brand_linux(): + """ + Read the CPU brand string out of /proc/cpuinfo on Linux. + """ + with open("/proc/cpuinfo", "r") as f: + for line in f: + if line.startswith("model name"): + _, brand = line.split(": ", 1) + return brand.rstrip() + # not found? + return None + + +def cpu_brand_windows(): + """ + Read the CPU brand string from the registry on Windows. + """ + try: + import _winreg + except ImportError: + import winreg as _winreg + + try: + h = _winreg.OpenKey( + _winreg.HKEY_LOCAL_MACHINE, + r"HARDWARE\DESCRIPTION\System\CentralProcessor\0", + ) + (brand, ty) = _winreg.QueryValueEx(h, "ProcessorNameString") + if ty == _winreg.REG_SZ: + return brand + except WindowsError: + pass + return None + + +def cpu_brand_mac(): + """ + Get the CPU brand string via sysctl on macos. + """ + import ctypes + import ctypes.util + + libc = ctypes.cdll.LoadLibrary(ctypes.util.find_library("c")) + # First, find the required buffer size. + bufsize = ctypes.c_size_t(0) + result = libc.sysctlbyname( + b"machdep.cpu.brand_string", None, ctypes.byref(bufsize), None, 0 + ) + if result != 0: + return None + bufsize.value += 1 + buf = ctypes.create_string_buffer(bufsize.value) + # Now actually get the value. + result = libc.sysctlbyname( + b"machdep.cpu.brand_string", buf, ctypes.byref(bufsize), None, 0 + ) + if result != 0: + return None + + return buf.value.decode() + + +def get_cpu_brand(): + """ + Get the CPU brand string as returned by CPUID. + """ + return { + "Linux": cpu_brand_linux, + "Windows": cpu_brand_windows, + "Darwin": cpu_brand_mac, + }.get(platform.system(), lambda: None)() + + +def get_psutil_stats(): + """Return whether psutil exists and its associated stats. + + @returns (bool, int, int, int) whether psutil exists, the logical CPU count, + physical CPU count, and total number of bytes of memory. + """ + try: + import psutil + + return ( + True, + psutil.cpu_count(), + psutil.cpu_count(logical=False), + psutil.virtual_memory().total, + ) + except ImportError: + return False, None, None, None + + +def filter_args(command, argv, topsrcdir, topobjdir, cwd=None): + """ + Given the full list of command-line arguments, remove anything up to and including `command`, + and attempt to filter absolute pathnames out of any arguments after that. + """ + if cwd is None: + cwd = os.getcwd() + + # Each key is a pathname and the values are replacement sigils + paths = { + topsrcdir: "$topsrcdir/", + topobjdir: "$topobjdir/", + mozpath.normpath(os.path.expanduser("~")): "$HOME/", + # This might override one of the existing entries, that's OK. + # We don't use a sigil here because we treat all arguments as potentially relative + # paths, so we'd like to get them back as they were specified. + mozpath.normpath(cwd): "", + } + + args = list(argv) + while args: + a = args.pop(0) + if a == command: + break + + def filter_path(p): + p = mozpath.abspath(p) + base = mozpath.basedir(p, paths.keys()) + if base: + return paths[base] + mozpath.relpath(p, base) + # Best-effort. + return "<path omitted>" + + return [filter_path(arg) for arg in args] + + +def get_distro_and_version(): + if sys.platform.startswith("linux"): + dist, version, _ = distro.linux_distribution(full_distribution_name=False) + return dist, version + elif sys.platform.startswith("darwin"): + return "macos", platform.mac_ver()[0] + elif sys.platform.startswith("win32") or sys.platform.startswith("msys"): + ver = sys.getwindowsversion() + return "windows", "%s.%s.%s" % (ver.major, ver.minor, ver.build) + else: + return sys.platform, "" + + +def get_shell_info(): + """Returns if the current shell was opened by vscode and if it's a SSH connection""" + + return ( + True if "vscode" in os.getenv("TERM_PROGRAM", "") else False, + bool(os.getenv("SSH_CLIENT", False)), + ) + + +def get_vscode_running(): + """Return if the vscode is currently running.""" + try: + import psutil + + for proc in psutil.process_iter(): + try: + # On Windows we have "Code.exe" + # On MacOS we have "Code Helper (Renderer)" + # On Linux we have "" + if ( + proc.name == "Code.exe" + or proc.name == "Code Helper (Renderer)" + or proc.name == "code" + ): + return True + except Exception: + # may not be able to access process info for all processes + continue + except Exception: + # On some platforms, sometimes, the generator throws an + # exception preventing us to enumerate. + return False + + return False diff --git a/python/mozbuild/mozbuild/test/__init__.py b/python/mozbuild/mozbuild/test/__init__.py new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/python/mozbuild/mozbuild/test/__init__.py diff --git a/python/mozbuild/mozbuild/test/action/data/html_fragment_preprocesor/example_basic.xml b/python/mozbuild/mozbuild/test/action/data/html_fragment_preprocesor/example_basic.xml new file mode 100644 index 0000000000..251b4a3069 --- /dev/null +++ b/python/mozbuild/mozbuild/test/action/data/html_fragment_preprocesor/example_basic.xml @@ -0,0 +1,10 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- 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/. --> + +<template xmlns="http://www.w3.org/1999/xhtml"> + <div class="main"> + <p>Hello World</p> + </div> +</template> diff --git a/python/mozbuild/mozbuild/test/action/data/html_fragment_preprocesor/example_multiple_templates.xml b/python/mozbuild/mozbuild/test/action/data/html_fragment_preprocesor/example_multiple_templates.xml new file mode 100644 index 0000000000..2e249aec63 --- /dev/null +++ b/python/mozbuild/mozbuild/test/action/data/html_fragment_preprocesor/example_multiple_templates.xml @@ -0,0 +1,30 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- 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/. --> + +<template xmlns="http://www.w3.org/1999/xhtml"> + <template doctype="true"> + <![CDATA[ + <!DOCTYPE bindings [ + <!ENTITY % exampleDTD SYSTEM "chrome://global/locale/example.dtd"> + %exampleDTD; + ]> + ]]> + </template> + <template name="alpha"> + <div class="main"> + <p>Hello World</p> + </div> + </template> + <template name="beta"> + <div class="body"> + <p>Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</p> + </div> + </template> + <template name="charlie"> + <div class="footer"> + <p>Goodbye</p> + </div> + </template> +</template> diff --git a/python/mozbuild/mozbuild/test/action/data/html_fragment_preprocesor/example_xul.xml b/python/mozbuild/mozbuild/test/action/data/html_fragment_preprocesor/example_xul.xml new file mode 100644 index 0000000000..5e0ea0b34a --- /dev/null +++ b/python/mozbuild/mozbuild/test/action/data/html_fragment_preprocesor/example_xul.xml @@ -0,0 +1,14 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- 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/. --> + +<template xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" xmlns:html="http://www.w3.org/1999/xhtml"> + <html:link href="chrome://global/skin/example.css" rel="stylesheet"/> + <hbox id="label-box" part="label-box" flex="1" role="none"> + <image part="icon" role="none"/> + <label id="label" part="label" crop="end" flex="1" role="none"/> + <label id="highlightable-label" part="label" crop="end" flex="1" role="none"/> + </hbox> + <html:slot/> +</template> diff --git a/python/mozbuild/mozbuild/test/action/data/invalid/region.properties b/python/mozbuild/mozbuild/test/action/data/invalid/region.properties new file mode 100644 index 0000000000..d4d8109b69 --- /dev/null +++ b/python/mozbuild/mozbuild/test/action/data/invalid/region.properties @@ -0,0 +1,12 @@ +# A region.properties file with invalid unicode byte sequences. The +# sequences were cribbed from Markus Kuhn's "UTF-8 decoder capability +# and stress test", available at +# http://www.cl.cam.ac.uk/~mgk25/ucs/examples/UTF-8-test.txt + +# 3.5 Impossible bytes | +# | +# The following two bytes cannot appear in a correct UTF-8 string | +# | +# 3.5.1 fe = "þ" | +# 3.5.2 ff = "ÿ" | +# 3.5.3 fe fe ff ff = "þþÿÿ" | diff --git a/python/mozbuild/mozbuild/test/action/data/node/node-test-script.js b/python/mozbuild/mozbuild/test/action/data/node/node-test-script.js new file mode 100644 index 0000000000..f6dbfcc594 --- /dev/null +++ b/python/mozbuild/mozbuild/test/action/data/node/node-test-script.js @@ -0,0 +1,11 @@ +#! /usr/bin/env node +"use strict"; + +/* eslint-disable no-console */ + +let args = process.argv.slice(2); + +for (let arg of args) { + console.log(`dep:${arg}`); +} + diff --git a/python/mozbuild/mozbuild/test/action/test_buildlist.py b/python/mozbuild/mozbuild/test/action/test_buildlist.py new file mode 100644 index 0000000000..9a1d2738ed --- /dev/null +++ b/python/mozbuild/mozbuild/test/action/test_buildlist.py @@ -0,0 +1,96 @@ +# 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/. + +import os +import os.path +import unittest +from shutil import rmtree +from tempfile import mkdtemp + +import mozunit + +from mozbuild.action.buildlist import addEntriesToListFile + + +class TestBuildList(unittest.TestCase): + """ + Unit tests for buildlist.py + """ + + def setUp(self): + self.tmpdir = mkdtemp() + + def tearDown(self): + rmtree(self.tmpdir) + + # utility methods for tests + def touch(self, file, dir=None): + if dir is None: + dir = self.tmpdir + f = os.path.join(dir, file) + open(f, "w").close() + return f + + def assertFileContains(self, filename, l): + """Assert that the lines in the file |filename| are equal + to the contents of the list |l|, in order.""" + l = l[:] + f = open(filename, "r") + lines = [line.rstrip() for line in f.readlines()] + f.close() + for line in lines: + self.assertTrue( + len(l) > 0, + "ran out of expected lines! (expected '{0}', got '{1}')".format( + l, lines + ), + ) + self.assertEqual(line, l.pop(0)) + self.assertTrue( + len(l) == 0, + "not enough lines in file! (expected '{0}'," " got '{1}'".format(l, lines), + ) + + def test_basic(self): + "Test that addEntriesToListFile works when file doesn't exist." + testfile = os.path.join(self.tmpdir, "test.list") + l = ["a", "b", "c"] + addEntriesToListFile(testfile, l) + self.assertFileContains(testfile, l) + # ensure that attempting to add the same entries again doesn't change it + addEntriesToListFile(testfile, l) + self.assertFileContains(testfile, l) + + def test_append(self): + "Test adding new entries." + testfile = os.path.join(self.tmpdir, "test.list") + l = ["a", "b", "c"] + addEntriesToListFile(testfile, l) + self.assertFileContains(testfile, l) + l2 = ["x", "y", "z"] + addEntriesToListFile(testfile, l2) + l.extend(l2) + self.assertFileContains(testfile, l) + + def test_append_some(self): + "Test adding new entries mixed with existing entries." + testfile = os.path.join(self.tmpdir, "test.list") + l = ["a", "b", "c"] + addEntriesToListFile(testfile, l) + self.assertFileContains(testfile, l) + addEntriesToListFile(testfile, ["a", "x", "c", "z"]) + self.assertFileContains(testfile, ["a", "b", "c", "x", "z"]) + + def test_add_multiple(self): + """Test that attempting to add the same entry multiple times results in + only one entry being added.""" + testfile = os.path.join(self.tmpdir, "test.list") + addEntriesToListFile(testfile, ["a", "b", "a", "a", "b"]) + self.assertFileContains(testfile, ["a", "b"]) + addEntriesToListFile(testfile, ["c", "a", "c", "b", "c"]) + self.assertFileContains(testfile, ["a", "b", "c"]) + + +if __name__ == "__main__": + mozunit.main() diff --git a/python/mozbuild/mozbuild/test/action/test_html_fragment_preprocessor.py b/python/mozbuild/mozbuild/test/action/test_html_fragment_preprocessor.py new file mode 100644 index 0000000000..3cce1c5f94 --- /dev/null +++ b/python/mozbuild/mozbuild/test/action/test_html_fragment_preprocessor.py @@ -0,0 +1,196 @@ +import os +import unittest +import xml.etree.ElementTree as ET + +import mozpack.path as mozpath +import mozunit + +from mozbuild.action.html_fragment_preprocesor import ( + fill_html_fragments_map, + generate, + get_fragment_key, + get_html_fragments_from_file, +) + +test_data_path = mozpath.abspath(mozpath.dirname(__file__)) +test_data_path = mozpath.join(test_data_path, "data", "html_fragment_preprocesor") + + +def data(name): + return os.path.join(test_data_path, name) + + +TEST_PATH = "/some/path/somewhere/example.xml".replace("/", os.sep) +EXAMPLE_BASIC = data("example_basic.xml") +EXAMPLE_TEMPLATES = data("example_multiple_templates.xml") +EXAMPLE_XUL = data("example_xul.xml") +DUMMY_FILE = data("dummy.js") + + +class TestNode(unittest.TestCase): + """ + Tests for html_fragment_preprocesor.py. + """ + + maxDiff = None + + def assertXMLEqual(self, a, b, message): + aRoot = ET.fromstring(a) + bRoot = ET.fromstring(b) + self.assertXMLNodesEqual(aRoot, bRoot, message) + + def assertXMLNodesEqual(self, a, b, message, xpath=""): + xpath += "/" + a.tag + messageWithPath = message + " at " + xpath + self.assertEqual(a.tag, b.tag, messageWithPath + " tag name") + self.assertEqual(a.text, b.text, messageWithPath + " text") + self.assertEqual( + a.attrib.keys(), b.attrib.keys(), messageWithPath + " attribute names" + ) + for aKey, aValue in a.attrib.items(): + self.assertEqual( + aValue, + b.attrib[aKey], + messageWithPath + "[@" + aKey + "] attribute value", + ) + for aChild, bChild in zip(a, b): + self.assertXMLNodesEqual(aChild, bChild, message, xpath) + + def test_get_fragment_key_path(self): + key = get_fragment_key("/some/path/somewhere/example.xml") + self.assertEqual(key, "example") + + def test_get_fragment_key_with_named_template(self): + key = get_fragment_key(TEST_PATH, "some-template") + self.assertEqual(key, "example/some-template") + + def test_get_html_fragments_from_template_no_doctype_no_name(self): + key = "example" + fragment_map = {} + template = ET.Element("template") + p1 = ET.SubElement(template, "p") + p1.text = "Hello World" + p2 = ET.SubElement(template, "p") + p2.text = "Goodbye" + fill_html_fragments_map(fragment_map, TEST_PATH, template) + self.assertEqual(fragment_map[key], "<p>Hello World</p><p>Goodbye</p>") + + def test_get_html_fragments_from_named_template_with_html_element(self): + key = "example/some-template" + fragment_map = {} + template = ET.Element("template") + template.attrib["name"] = "some-template" + p = ET.SubElement(template, "p") + p.text = "Hello World" + fill_html_fragments_map(fragment_map, TEST_PATH, template) + self.assertEqual(fragment_map[key], "<p>Hello World</p>") + + def test_get_html_fragments_from_template_with_doctype(self): + key = "example" + doctype = "doctype definition goes here" + fragment_map = {} + template = ET.Element("template") + p = ET.SubElement(template, "p") + p.text = "Hello World" + fill_html_fragments_map(fragment_map, TEST_PATH, template, doctype) + self.assertEqual( + fragment_map[key], "doctype definition goes here\n<p>Hello World</p>" + ) + + def test_get_html_fragments_from_file_basic(self): + key = "example_basic" + fragment_map = {} + get_html_fragments_from_file(fragment_map, EXAMPLE_BASIC) + self.assertEqual( + fragment_map[key], + '<div xmlns="http://www.w3.org/1999/xhtml" class="main">' + + " <p>Hello World</p> </div>", + ) + + def test_get_html_fragments_from_file_multiple_templates(self): + key1 = "example_multiple_templates/alpha" + key2 = "example_multiple_templates/beta" + key3 = "example_multiple_templates/charlie" + fragment_map = {} + get_html_fragments_from_file(fragment_map, EXAMPLE_TEMPLATES) + self.assertIn("<p>Hello World</p>", fragment_map[key1], "Has HTML content") + self.assertIn( + '<!ENTITY % exampleDTD SYSTEM "chrome://global/locale/example.dtd">', + fragment_map[key1], + "Has doctype", + ) + self.assertIn("<p>Lorem ipsum", fragment_map[key2], "Has HTML content") + self.assertIn( + '<!ENTITY % exampleDTD SYSTEM "chrome://global/locale/example.dtd">', + fragment_map[key2], + "Has doctype", + ) + self.assertIn("<p>Goodbye</p>", fragment_map[key3], "Has HTML content") + self.assertIn( + '<!ENTITY % exampleDTD SYSTEM "chrome://global/locale/example.dtd">', + fragment_map[key3], + "Has doctype", + ) + + def test_get_html_fragments_from_file_with_xul(self): + key = "example_xul" + fragment_map = {} + get_html_fragments_from_file(fragment_map, EXAMPLE_XUL) + xml = "<root>" + fragment_map[key] + "</root>" + self.assertXMLEqual( + xml, + "<root>" + + '<html:link xmlns:html="http://www.w3.org/1999/xhtml" ' + + 'href="chrome://global/skin/example.css" rel="stylesheet">' + + "</html:link> " + + '<hbox xmlns="http://www.mozilla.org/keymaster/' + + 'gatekeeper/there.is.only.xul" flex="1" id="label-box" ' + + 'part="label-box" role="none"> ' + + '<image part="icon" role="none"></image> ' + + '<label crop="end" flex="1" id="label" part="label" ' + + 'role="none"></label> ' + + '<label crop="end" flex="1" id="highlightable-label" ' + + 'part="label" role="none"></label> ' + + "</hbox> " + + '<html:slot xmlns:html="http://www.w3.org/1999/xhtml">' + + "</html:slot></root>", + "XML values must match", + ) + + def test_generate(self): + with open(DUMMY_FILE, "w") as file: + deps = generate( + file, + EXAMPLE_BASIC, + EXAMPLE_TEMPLATES, + EXAMPLE_XUL, + ) + with open(DUMMY_FILE, "r") as file: + contents = file.read() + self.assertIn( + "<!ENTITY % exampleDTD SYSTEM", + contents, + "Has doctype", + ) + self.assertIn("<p>Lorem ipsum", contents, "Has HTML content") + self.assertIn('"example_basic"', contents, "Has basic fragment key") + self.assertIn( + '"example_multiple_templates/alpha"', + contents, + "Has multiple templates fragment key", + ) + self.assertIn('"example_xul"', contents, "Has XUL fragment key") + self.assertIn( + "const getHTMLFragment =", + contents, + "Has fragment loader method declaration", + ) + os.remove(DUMMY_FILE) + self.assertEqual(len(deps), 3, "deps are correct") + self.assertIn(EXAMPLE_BASIC, deps, "deps are correct") + self.assertIn(EXAMPLE_TEMPLATES, deps, "deps are correct") + self.assertIn(EXAMPLE_XUL, deps, "deps are correct") + + +if __name__ == "__main__": + mozunit.main() diff --git a/python/mozbuild/mozbuild/test/action/test_langpack_manifest.py b/python/mozbuild/mozbuild/test/action/test_langpack_manifest.py new file mode 100644 index 0000000000..12750bdd3b --- /dev/null +++ b/python/mozbuild/mozbuild/test/action/test_langpack_manifest.py @@ -0,0 +1,269 @@ +# -*- coding: utf-8 -*- + +# Any copyright is dedicated to the Public Domain. +# http://creativecommons.org/publicdomain/zero/1.0/ + +import json +import os +import shutil +import tempfile +import unittest + +import mozunit + +from mozbuild.action import langpack_manifest + + +class TestGenerateManifest(unittest.TestCase): + """ + Unit tests for langpack_manifest.py. + """ + + def test_parse_flat_ftl(self): + src = """ +langpack-creator = bar {"bar"} +langpack-contributors = { "" } +""" + tmp = tempfile.NamedTemporaryFile(mode="wt", suffix=".ftl", delete=False) + try: + tmp.write(src) + tmp.close() + ftl = langpack_manifest.parse_flat_ftl(tmp.name) + self.assertEqual(ftl["langpack-creator"], "bar bar") + self.assertEqual(ftl["langpack-contributors"], "") + finally: + os.remove(tmp.name) + + def test_parse_flat_ftl_missing(self): + ftl = langpack_manifest.parse_flat_ftl("./does-not-exist.ftl") + self.assertEqual(len(ftl), 0) + + def test_manifest(self): + ctx = { + "langpack-creator": "Suomennosprojekti", + "langpack-contributors": "Joe Smith, Mary White", + } + os.environ["MOZ_BUILD_DATE"] = "20210928100000" + manifest = langpack_manifest.create_webmanifest( + "fi", + "57.0.1", + "57.0", + "57.0.*", + "Firefox", + "/var/vcs/l10n-central", + "langpack-fi@firefox.mozilla.og", + ctx, + {}, + ) + + data = json.loads(manifest) + self.assertEqual(data["name"], "Language: Suomi (Finnish)") + self.assertEqual( + data["description"], "Firefox Language Pack for Suomi (fi) – Finnish" + ) + self.assertEqual( + data["author"], "Suomennosprojekti (contributors: Joe Smith, Mary White)" + ) + self.assertEqual(data["version"], "57.0.20210928.100000") + + def test_manifest_truncated_name(self): + ctx = { + "langpack-creator": "Mozilla.org / Softcatalà ", + "langpack-contributors": "Joe Smith, Mary White", + } + os.environ["MOZ_BUILD_DATE"] = "20210928100000" + manifest = langpack_manifest.create_webmanifest( + "ca-valencia", + "57.0.1", + "57.0", + "57.0.*", + "Firefox", + "/var/vcs/l10n-central", + "langpack-ca-valencia@firefox.mozilla.og", + ctx, + {}, + ) + + data = json.loads(manifest) + self.assertEqual(data["name"], "Language: Català (Valencià )") + self.assertEqual( + data["description"], + "Firefox Language Pack for Català (Valencià ) (ca-valencia) – Catalan, Valencian", + ) + + def test_manifest_name_untranslated(self): + ctx = { + "langpack-creator": "Mozilla.org", + "langpack-contributors": "Joe Smith, Mary White", + } + os.environ["MOZ_BUILD_DATE"] = "20210928100000" + manifest = langpack_manifest.create_webmanifest( + "en-US", + "57.0.1", + "57.0", + "57.0.*", + "Firefox", + "/var/vcs/l10n-central", + "langpack-ca-valencia@firefox.mozilla.og", + ctx, + {}, + ) + + data = json.loads(manifest) + self.assertEqual(data["name"], "Language: English (US)") + self.assertEqual( + data["description"], + "Firefox Language Pack for English (US) (en-US)", + ) + + def test_manifest_without_contributors(self): + ctx = { + "langpack-creator": "Suomennosprojekti", + "langpack-contributors": "", + } + manifest = langpack_manifest.create_webmanifest( + "fi", + "57.0.1", + "57.0", + "57.0.*", + "Firefox", + "/var/vcs/l10n-central", + "langpack-fi@firefox.mozilla.og", + ctx, + {}, + ) + + data = json.loads(manifest) + self.assertEqual(data["name"], "Language: Suomi (Finnish)") + self.assertEqual( + data["description"], "Firefox Language Pack for Suomi (fi) – Finnish" + ) + self.assertEqual(data["author"], "Suomennosprojekti") + + def test_manifest_truncation(self): + locale = ( + "Long locale code that will be truncated and will cause both " + "the name and the description to exceed the maximum number of " + "characters allowed in manifest.json" + ) + title, description = langpack_manifest.get_title_and_description( + "Firefox", locale + ) + + self.assertEqual(len(title), 45) + self.assertEqual(len(description), 132) + + def test_get_version_maybe_buildid(self): + for app_version, buildid, expected_version in [ + ("109", "", "109"), + ("109.0", "", "109.0"), + ("109.0.0", "", "109.0.0"), + ("109", "20210928", "109"), # buildid should be 14 chars + ("109", "20210928123456", "109.20210928.123456"), + ("109.0", "20210928123456", "109.0.20210928.123456"), + ("109.0.0", "20210928123456", "109.0.20210928.123456"), + ("109", "20230215023456", "109.20230215.23456"), + ("109.0", "20230215023456", "109.0.20230215.23456"), + ("109.0.0", "20230215023456", "109.0.20230215.23456"), + ("109", "20230215003456", "109.20230215.3456"), + ("109", "20230215000456", "109.20230215.456"), + ("109", "20230215000056", "109.20230215.56"), + ("109", "20230215000006", "109.20230215.6"), + ("109", "20230215000000", "109.20230215.0"), + ("109.1.2.3", "20230201000000", "109.1.20230201.0"), + ("109.0a1", "", "109.0"), + ("109a0.0b0", "", "109.0"), + ("109.0.0b1", "", "109.0.0"), + ("109.0.b1", "", "109.0.0"), + ("109..1", "", "109.0.1"), + ]: + os.environ["MOZ_BUILD_DATE"] = buildid + version = langpack_manifest.get_version_maybe_buildid(app_version) + self.assertEqual(version, expected_version) + + def test_main(self): + # We set this env variable so that the manifest.json version string + # uses a "buildid", see: `get_version_maybe_buildid()` for more + # information. + os.environ["MOZ_BUILD_DATE"] = "20210928100000" + + TEST_CASES = [ + { + "app_version": "112.0.1", + "max_app_version": "112.*", + "expected_version": "112.0.20210928.100000", + "expected_min_version": "112.0", + "expected_max_version": "112.*", + }, + { + "app_version": "112.1.0", + "max_app_version": "112.*", + "expected_version": "112.1.20210928.100000", + # We expect the second part to be "0" even if the app version + # has a minor part equal to "1". + "expected_min_version": "112.0", + "expected_max_version": "112.*", + }, + { + "app_version": "114.0a1", + "max_app_version": "114.*", + "expected_version": "114.0.20210928.100000", + # We expect the min version to be equal to the app version + # because we don't change alpha versions. + "expected_min_version": "114.0a1", + "expected_max_version": "114.*", + }, + ] + + tmpdir = tempfile.mkdtemp() + try: + # These files are required by the `main()` function. + for file in ["chrome.manifest", "empty-metadata.ftl"]: + langpack_manifest.write_file(os.path.join(tmpdir, file), "") + + for tc in TEST_CASES: + extension_id = "some@extension-id" + locale = "fr" + + args = [ + "--input", + tmpdir, + # This file has been created right above. + "--metadata", + "empty-metadata.ftl", + "--app-name", + "Firefox", + "--l10n-basedir", + "/var/vcs/l10n-central", + "--locales", + locale, + "--langpack-eid", + extension_id, + "--app-version", + tc["app_version"], + "--max-app-ver", + tc["max_app_version"], + ] + langpack_manifest.main(args) + + with open(os.path.join(tmpdir, "manifest.json")) as manifest_file: + manifest = json.load(manifest_file) + self.assertEqual(manifest["version"], tc["expected_version"]) + self.assertEqual(manifest["langpack_id"], locale) + self.assertEqual( + manifest["browser_specific_settings"], + { + "gecko": { + "id": extension_id, + "strict_min_version": tc["expected_min_version"], + "strict_max_version": tc["expected_max_version"], + } + }, + ) + finally: + shutil.rmtree(tmpdir, ignore_errors=True) + del os.environ["MOZ_BUILD_DATE"] + + +if __name__ == "__main__": + mozunit.main() diff --git a/python/mozbuild/mozbuild/test/action/test_node.py b/python/mozbuild/mozbuild/test/action/test_node.py new file mode 100644 index 0000000000..f1ab5afd17 --- /dev/null +++ b/python/mozbuild/mozbuild/test/action/test_node.py @@ -0,0 +1,80 @@ +# -*- coding: utf-8 -*- + +# Any copyright is dedicated to the Public Domain. +# http://creativecommons.org/publicdomain/zero/1.0/ + +import os +import unittest + +import buildconfig +import mozpack.path as mozpath +import mozunit + +from mozbuild.action.node import SCRIPT_ALLOWLIST, generate +from mozbuild.nodeutil import find_node_executable + +test_data_path = mozpath.abspath(mozpath.dirname(__file__)) +test_data_path = mozpath.join(test_data_path, "data", "node") + + +def data(name): + return os.path.join(test_data_path, name) + + +TEST_SCRIPT = data("node-test-script.js") +NONEXISTENT_TEST_SCRIPT = data("non-existent-test-script.js") + + +class TestNode(unittest.TestCase): + """ + Tests for node.py. + """ + + def setUp(self): + if not buildconfig.substs.get("NODEJS"): + buildconfig.substs["NODEJS"] = find_node_executable()[0] + SCRIPT_ALLOWLIST.append(TEST_SCRIPT) + + def tearDown(self): + try: + SCRIPT_ALLOWLIST.remove(TEST_SCRIPT) + except Exception: + pass + + def test_generate_no_returned_deps(self): + deps = generate("dummy_argument", TEST_SCRIPT) + + self.assertSetEqual(deps, set([])) + + def test_generate_returns_passed_deps(self): + deps = generate("dummy_argument", TEST_SCRIPT, "a", "b") + + self.assertSetEqual(deps, set(["a", "b"])) + + def test_called_process_error_handled(self): + SCRIPT_ALLOWLIST.append(NONEXISTENT_TEST_SCRIPT) + + with self.assertRaises(SystemExit) as cm: + generate("dummy_arg", NONEXISTENT_TEST_SCRIPT) + + self.assertEqual(cm.exception.code, 1) + SCRIPT_ALLOWLIST.remove(NONEXISTENT_TEST_SCRIPT) + + def test_nodejs_not_set(self): + buildconfig.substs["NODEJS"] = None + + with self.assertRaises(SystemExit) as cm: + generate("dummy_arg", TEST_SCRIPT) + + self.assertEqual(cm.exception.code, 1) + + def test_generate_missing_allowlist_entry_exit_code(self): + SCRIPT_ALLOWLIST.remove(TEST_SCRIPT) + with self.assertRaises(SystemExit) as cm: + generate("dummy_arg", TEST_SCRIPT) + + self.assertEqual(cm.exception.code, 1) + + +if __name__ == "__main__": + mozunit.main() diff --git a/python/mozbuild/mozbuild/test/action/test_process_install_manifest.py b/python/mozbuild/mozbuild/test/action/test_process_install_manifest.py new file mode 100644 index 0000000000..3aea4bca73 --- /dev/null +++ b/python/mozbuild/mozbuild/test/action/test_process_install_manifest.py @@ -0,0 +1,65 @@ +# -*- coding: utf-8 -*- + +# Any copyright is dedicated to the Public Domain. +# http://creativecommons.org/publicdomain/zero/1.0/ + +import os + +import mozunit +from mozpack.manifests import InstallManifest +from mozpack.test.test_files import TestWithTmpDir + +import mozbuild.action.process_install_manifest as process_install_manifest + + +class TestGenerateManifest(TestWithTmpDir): + """ + Unit tests for process_install_manifest.py. + """ + + def test_process_manifest(self): + source = self.tmppath("source") + os.mkdir(source) + os.mkdir("%s/base" % source) + os.mkdir("%s/base/foo" % source) + os.mkdir("%s/base2" % source) + + with open("%s/base/foo/file1" % source, "a"): + pass + + with open("%s/base/foo/file2" % source, "a"): + pass + + with open("%s/base2/file3" % source, "a"): + pass + + m = InstallManifest() + m.add_pattern_link("%s/base" % source, "**", "") + m.add_link("%s/base2/file3" % source, "foo/file3") + + p = self.tmppath("m") + m.write(path=p) + + dest = self.tmppath("dest") + track = self.tmppath("track") + + for i in range(2): + process_install_manifest.process_manifest(dest, [p], track) + + self.assertTrue(os.path.exists(self.tmppath("dest/foo/file1"))) + self.assertTrue(os.path.exists(self.tmppath("dest/foo/file2"))) + self.assertTrue(os.path.exists(self.tmppath("dest/foo/file3"))) + + m = InstallManifest() + m.write(path=p) + + for i in range(2): + process_install_manifest.process_manifest(dest, [p], track) + + self.assertFalse(os.path.exists(self.tmppath("dest/foo/file1"))) + self.assertFalse(os.path.exists(self.tmppath("dest/foo/file2"))) + self.assertFalse(os.path.exists(self.tmppath("dest/foo/file3"))) + + +if __name__ == "__main__": + mozunit.main() diff --git a/python/mozbuild/mozbuild/test/backend/__init__.py b/python/mozbuild/mozbuild/test/backend/__init__.py new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/python/mozbuild/mozbuild/test/backend/__init__.py diff --git a/python/mozbuild/mozbuild/test/backend/common.py b/python/mozbuild/mozbuild/test/backend/common.py new file mode 100644 index 0000000000..07cfa7540f --- /dev/null +++ b/python/mozbuild/mozbuild/test/backend/common.py @@ -0,0 +1,253 @@ +# 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/. + +import os +import unittest +from collections import defaultdict +from shutil import rmtree +from tempfile import mkdtemp + +import mozpack.path as mozpath +from mach.logging import LoggingManager + +from mozbuild.backend.configenvironment import ConfigEnvironment +from mozbuild.frontend.emitter import TreeMetadataEmitter +from mozbuild.frontend.reader import BuildReader + +log_manager = LoggingManager() +log_manager.add_terminal_logging() + + +test_data_path = mozpath.abspath(mozpath.dirname(__file__)) +test_data_path = mozpath.join(test_data_path, "data") + + +CONFIGS = defaultdict( + lambda: { + "defines": {}, + "substs": {"OS_TARGET": "WINNT"}, + }, + { + "binary-components": { + "defines": {}, + "substs": { + "LIB_PREFIX": "lib", + "LIB_SUFFIX": "a", + "COMPILE_ENVIRONMENT": "1", + }, + }, + "database": { + "defines": {}, + "substs": { + "CC": "clang", + "CXX": "clang++", + "LIB_PREFIX": "lib", + "LIB_SUFFIX": "a", + }, + }, + "rust-library": { + "defines": {}, + "substs": { + "COMPILE_ENVIRONMENT": "1", + "RUST_TARGET": "x86_64-unknown-linux-gnu", + "LIB_PREFIX": "lib", + "LIB_SUFFIX": "a", + }, + }, + "host-rust-library": { + "defines": {}, + "substs": { + "COMPILE_ENVIRONMENT": "1", + "RUST_HOST_TARGET": "x86_64-unknown-linux-gnu", + "RUST_TARGET": "armv7-linux-androideabi", + "LIB_PREFIX": "lib", + "LIB_SUFFIX": "a", + }, + }, + "host-rust-library-features": { + "defines": {}, + "substs": { + "COMPILE_ENVIRONMENT": "1", + "RUST_HOST_TARGET": "x86_64-unknown-linux-gnu", + "RUST_TARGET": "armv7-linux-androideabi", + "LIB_PREFIX": "lib", + "LIB_SUFFIX": "a", + }, + }, + "rust-library-features": { + "defines": {}, + "substs": { + "COMPILE_ENVIRONMENT": "1", + "RUST_TARGET": "x86_64-unknown-linux-gnu", + "LIB_PREFIX": "lib", + "LIB_SUFFIX": "a", + }, + }, + "rust-programs": { + "defines": {}, + "substs": { + "COMPILE_ENVIRONMENT": "1", + "RUST_TARGET": "i686-pc-windows-msvc", + "RUST_HOST_TARGET": "i686-pc-windows-msvc", + "BIN_SUFFIX": ".exe", + "HOST_BIN_SUFFIX": ".exe", + }, + }, + "test-support-binaries-tracked": { + "defines": {}, + "substs": { + "COMPILE_ENVIRONMENT": "1", + "LIB_SUFFIX": "dll", + "BIN_SUFFIX": ".exe", + }, + }, + "sources": { + "defines": {}, + "substs": { + "LIB_PREFIX": "lib", + "LIB_SUFFIX": "a", + }, + }, + "stub0": { + "defines": { + "MOZ_TRUE_1": "1", + "MOZ_TRUE_2": "1", + }, + "substs": { + "MOZ_FOO": "foo", + "MOZ_BAR": "bar", + }, + }, + "substitute_config_files": { + "defines": {}, + "substs": { + "MOZ_FOO": "foo", + "MOZ_BAR": "bar", + }, + }, + "test_config": { + "defines": { + "foo": "baz qux", + "baz": 1, + }, + "substs": { + "foo": "bar baz", + }, + }, + "visual-studio": { + "defines": {}, + "substs": { + "MOZ_APP_NAME": "my_app", + }, + }, + "prog-lib-c-only": { + "defines": {}, + "substs": { + "COMPILE_ENVIRONMENT": "1", + "LIB_SUFFIX": ".a", + "BIN_SUFFIX": "", + }, + }, + "gn-processor": { + "defines": {}, + "substs": { + "BUILD_BACKENDS": [ + "GnMozbuildWriter", + "RecursiveMake", + ], + "COMPILE_ENVIRONMENT": "1", + "STL_FLAGS": [], + "RUST_TARGET": "x86_64-unknown-linux-gnu", + "LIB_PREFIX": "lib", + "LIB_SUFFIX": "a", + "OS_TARGET": "Darwin", + }, + }, + "ipdl_sources": { + "defines": {}, + "substs": { + "COMPILE_ENVIRONMENT": "1", + "LIB_SUFFIX": ".a", + "BIN_SUFFIX": "", + }, + }, + "program-paths": { + "defines": {}, + "substs": { + "COMPILE_ENVIRONMENT": "1", + "BIN_SUFFIX": ".prog", + }, + }, + "linkage": { + "defines": {}, + "substs": { + "CC_TYPE": "clang", + "COMPILE_ENVIRONMENT": "1", + "LIB_SUFFIX": "a", + "BIN_SUFFIX": ".exe", + "DLL_SUFFIX": ".so", + "OBJ_SUFFIX": "o", + "EXPAND_LIBS_LIST_STYLE": "list", + }, + }, + }, +) + + +class BackendTester(unittest.TestCase): + def setUp(self): + self._old_env = dict(os.environ) + os.environ.pop("MOZ_OBJDIR", None) + + def tearDown(self): + os.environ.clear() + os.environ.update(self._old_env) + + def _get_environment(self, name): + """Obtain a new instance of a ConfigEnvironment for a known profile. + + A new temporary object directory is created for the environment. The + environment is cleaned up automatically when the test finishes. + """ + config = CONFIGS[name] + config["substs"]["MOZ_UI_LOCALE"] = "en-US" + + srcdir = mozpath.join(test_data_path, name) + config["substs"]["top_srcdir"] = srcdir + + # Create the objdir in the srcdir to ensure that they share the + # same drive on Windows. + objdir = mkdtemp(dir=srcdir) + self.addCleanup(rmtree, objdir) + + return ConfigEnvironment(srcdir, objdir, **config) + + def _emit(self, name, env=None): + env = env or self._get_environment(name) + reader = BuildReader(env) + emitter = TreeMetadataEmitter(env) + + return env, emitter.emit(reader.read_topsrcdir()) + + def _consume(self, name, cls, env=None): + env, objs = self._emit(name, env=env) + backend = cls(env) + backend.consume(objs) + + return env + + def _tree_paths(self, topdir, filename): + for dirpath, dirnames, filenames in os.walk(topdir): + for f in filenames: + if f == filename: + yield mozpath.relpath(mozpath.join(dirpath, f), topdir) + + def _mozbuild_paths(self, env): + return self._tree_paths(env.topsrcdir, "moz.build") + + def _makefile_in_paths(self, env): + return self._tree_paths(env.topsrcdir, "Makefile.in") + + +__all__ = ["BackendTester"] diff --git a/python/mozbuild/mozbuild/test/backend/data/build/app/moz.build b/python/mozbuild/mozbuild/test/backend/data/build/app/moz.build new file mode 100644 index 0000000000..27641b2080 --- /dev/null +++ b/python/mozbuild/mozbuild/test/backend/data/build/app/moz.build @@ -0,0 +1,54 @@ +DIST_SUBDIR = "app" + +EXTRA_JS_MODULES += [ + "../foo.jsm", +] + +EXTRA_JS_MODULES.child += [ + "../bar.jsm", +] + +EXTRA_PP_JS_MODULES += [ + "../baz.jsm", +] + +EXTRA_PP_JS_MODULES.child2 += [ + "../qux.jsm", +] + +FINAL_TARGET_FILES += [ + "../foo.ini", +] + +FINAL_TARGET_FILES.child += [ + "../bar.ini", +] + +FINAL_TARGET_PP_FILES += [ + "../baz.ini", + "../foo.css", +] + +FINAL_TARGET_PP_FILES.child2 += [ + "../qux.ini", +] + +EXTRA_COMPONENTS += [ + "../components.manifest", + "../foo.js", +] + +EXTRA_PP_COMPONENTS += [ + "../bar.js", +] + +JS_PREFERENCE_FILES += [ + "../prefs.js", +] + +JAR_MANIFESTS += [ + "../jar.mn", +] + +DEFINES["FOO"] = "bar" +DEFINES["BAR"] = True diff --git a/python/mozbuild/mozbuild/test/backend/data/build/bar.ini b/python/mozbuild/mozbuild/test/backend/data/build/bar.ini new file mode 100644 index 0000000000..91dcbe1536 --- /dev/null +++ b/python/mozbuild/mozbuild/test/backend/data/build/bar.ini @@ -0,0 +1 @@ +bar.ini diff --git a/python/mozbuild/mozbuild/test/backend/data/build/bar.js b/python/mozbuild/mozbuild/test/backend/data/build/bar.js new file mode 100644 index 0000000000..1a608e8a56 --- /dev/null +++ b/python/mozbuild/mozbuild/test/backend/data/build/bar.js @@ -0,0 +1,2 @@ +#filter substitution +bar.js: FOO is @FOO@ diff --git a/python/mozbuild/mozbuild/test/backend/data/build/bar.jsm b/python/mozbuild/mozbuild/test/backend/data/build/bar.jsm new file mode 100644 index 0000000000..05db2e2f6a --- /dev/null +++ b/python/mozbuild/mozbuild/test/backend/data/build/bar.jsm @@ -0,0 +1 @@ +bar.jsm diff --git a/python/mozbuild/mozbuild/test/backend/data/build/baz.ini b/python/mozbuild/mozbuild/test/backend/data/build/baz.ini new file mode 100644 index 0000000000..975a1e437d --- /dev/null +++ b/python/mozbuild/mozbuild/test/backend/data/build/baz.ini @@ -0,0 +1,2 @@ +#filter substitution +baz.ini: FOO is @FOO@ diff --git a/python/mozbuild/mozbuild/test/backend/data/build/baz.jsm b/python/mozbuild/mozbuild/test/backend/data/build/baz.jsm new file mode 100644 index 0000000000..f39ed02082 --- /dev/null +++ b/python/mozbuild/mozbuild/test/backend/data/build/baz.jsm @@ -0,0 +1,2 @@ +#filter substitution +baz.jsm: FOO is @FOO@ diff --git a/python/mozbuild/mozbuild/test/backend/data/build/components.manifest b/python/mozbuild/mozbuild/test/backend/data/build/components.manifest new file mode 100644 index 0000000000..b5bb87254c --- /dev/null +++ b/python/mozbuild/mozbuild/test/backend/data/build/components.manifest @@ -0,0 +1,2 @@ +component {foo} foo.js +component {bar} bar.js diff --git a/python/mozbuild/mozbuild/test/backend/data/build/foo.css b/python/mozbuild/mozbuild/test/backend/data/build/foo.css new file mode 100644 index 0000000000..1803d6c572 --- /dev/null +++ b/python/mozbuild/mozbuild/test/backend/data/build/foo.css @@ -0,0 +1,2 @@ +%filter substitution +foo.css: FOO is @FOO@ diff --git a/python/mozbuild/mozbuild/test/backend/data/build/foo.ini b/python/mozbuild/mozbuild/test/backend/data/build/foo.ini new file mode 100644 index 0000000000..c93c9d7658 --- /dev/null +++ b/python/mozbuild/mozbuild/test/backend/data/build/foo.ini @@ -0,0 +1 @@ +foo.ini diff --git a/python/mozbuild/mozbuild/test/backend/data/build/foo.js b/python/mozbuild/mozbuild/test/backend/data/build/foo.js new file mode 100644 index 0000000000..4fa71e2d27 --- /dev/null +++ b/python/mozbuild/mozbuild/test/backend/data/build/foo.js @@ -0,0 +1 @@ +foo.js diff --git a/python/mozbuild/mozbuild/test/backend/data/build/foo.jsm b/python/mozbuild/mozbuild/test/backend/data/build/foo.jsm new file mode 100644 index 0000000000..d58fd61c16 --- /dev/null +++ b/python/mozbuild/mozbuild/test/backend/data/build/foo.jsm @@ -0,0 +1 @@ +foo.jsm diff --git a/python/mozbuild/mozbuild/test/backend/data/build/jar.mn b/python/mozbuild/mozbuild/test/backend/data/build/jar.mn new file mode 100644 index 0000000000..393055c4ea --- /dev/null +++ b/python/mozbuild/mozbuild/test/backend/data/build/jar.mn @@ -0,0 +1,11 @@ +foo.jar: +% content bar %child/ +% content foo % + foo.js +* foo.css + bar.js (subdir/bar.js) + qux.js (subdir/bar.js) +* child/hoge.js (bar.js) +* child/baz.jsm + +% override chrome://foo/bar.svg#hello chrome://bar/bar.svg#hello diff --git a/python/mozbuild/mozbuild/test/backend/data/build/moz.build b/python/mozbuild/mozbuild/test/backend/data/build/moz.build new file mode 100644 index 0000000000..700516754d --- /dev/null +++ b/python/mozbuild/mozbuild/test/backend/data/build/moz.build @@ -0,0 +1,68 @@ +CONFIGURE_SUBST_FILES += [ + "/config/autoconf.mk", + "/config/emptyvars.mk", +] + +EXTRA_JS_MODULES += [ + "foo.jsm", +] + +EXTRA_JS_MODULES.child += [ + "bar.jsm", +] + +EXTRA_PP_JS_MODULES += [ + "baz.jsm", +] + +EXTRA_PP_JS_MODULES.child2 += [ + "qux.jsm", +] + +FINAL_TARGET_FILES += [ + "foo.ini", +] + +FINAL_TARGET_FILES.child += [ + "bar.ini", +] + +FINAL_TARGET_PP_FILES += [ + "baz.ini", +] + +FINAL_TARGET_PP_FILES.child2 += [ + "foo.css", + "qux.ini", +] + +EXTRA_COMPONENTS += [ + "components.manifest", + "foo.js", +] + +EXTRA_PP_COMPONENTS += [ + "bar.js", +] + +JS_PREFERENCE_FILES += [ + "prefs.js", +] + +RESOURCE_FILES += [ + "resource", +] + +RESOURCE_FILES.child += [ + "resource2", +] + +DEFINES["FOO"] = "foo" + +JAR_MANIFESTS += [ + "jar.mn", +] + +DIRS += [ + "app", +] diff --git a/python/mozbuild/mozbuild/test/backend/data/build/prefs.js b/python/mozbuild/mozbuild/test/backend/data/build/prefs.js new file mode 100644 index 0000000000..a030da9fd7 --- /dev/null +++ b/python/mozbuild/mozbuild/test/backend/data/build/prefs.js @@ -0,0 +1 @@ +prefs.js diff --git a/python/mozbuild/mozbuild/test/backend/data/build/qux.ini b/python/mozbuild/mozbuild/test/backend/data/build/qux.ini new file mode 100644 index 0000000000..3ce157eb6d --- /dev/null +++ b/python/mozbuild/mozbuild/test/backend/data/build/qux.ini @@ -0,0 +1,5 @@ +#ifdef BAR +qux.ini: BAR is defined +#else +qux.ini: BAR is not defined +#endif diff --git a/python/mozbuild/mozbuild/test/backend/data/build/qux.jsm b/python/mozbuild/mozbuild/test/backend/data/build/qux.jsm new file mode 100644 index 0000000000..9c5fe28d58 --- /dev/null +++ b/python/mozbuild/mozbuild/test/backend/data/build/qux.jsm @@ -0,0 +1,5 @@ +#ifdef BAR +qux.jsm: BAR is defined +#else +qux.jsm: BAR is not defined +#endif diff --git a/python/mozbuild/mozbuild/test/backend/data/build/resource b/python/mozbuild/mozbuild/test/backend/data/build/resource new file mode 100644 index 0000000000..91e75c679e --- /dev/null +++ b/python/mozbuild/mozbuild/test/backend/data/build/resource @@ -0,0 +1 @@ +resource diff --git a/python/mozbuild/mozbuild/test/backend/data/build/resource2 b/python/mozbuild/mozbuild/test/backend/data/build/resource2 new file mode 100644 index 0000000000..b7c2700964 --- /dev/null +++ b/python/mozbuild/mozbuild/test/backend/data/build/resource2 @@ -0,0 +1 @@ +resource2 diff --git a/python/mozbuild/mozbuild/test/backend/data/build/subdir/bar.js b/python/mozbuild/mozbuild/test/backend/data/build/subdir/bar.js new file mode 100644 index 0000000000..80c887a84a --- /dev/null +++ b/python/mozbuild/mozbuild/test/backend/data/build/subdir/bar.js @@ -0,0 +1 @@ +bar.js diff --git a/python/mozbuild/mozbuild/test/backend/data/database/bar.c b/python/mozbuild/mozbuild/test/backend/data/database/bar.c new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/python/mozbuild/mozbuild/test/backend/data/database/bar.c diff --git a/python/mozbuild/mozbuild/test/backend/data/database/baz.cpp b/python/mozbuild/mozbuild/test/backend/data/database/baz.cpp new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/python/mozbuild/mozbuild/test/backend/data/database/baz.cpp diff --git a/python/mozbuild/mozbuild/test/backend/data/database/build/non-unified-compat b/python/mozbuild/mozbuild/test/backend/data/database/build/non-unified-compat new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/python/mozbuild/mozbuild/test/backend/data/database/build/non-unified-compat diff --git a/python/mozbuild/mozbuild/test/backend/data/database/foo.c b/python/mozbuild/mozbuild/test/backend/data/database/foo.c new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/python/mozbuild/mozbuild/test/backend/data/database/foo.c diff --git a/python/mozbuild/mozbuild/test/backend/data/database/moz.build b/python/mozbuild/mozbuild/test/backend/data/database/moz.build new file mode 100644 index 0000000000..ebc5d05b5c --- /dev/null +++ b/python/mozbuild/mozbuild/test/backend/data/database/moz.build @@ -0,0 +1,14 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# Any copyright is dedicated to the Public Domain. +# http://creativecommons.org/publicdomain/zero/1.0/ + + +@template +def Library(name): + """Template for libraries.""" + LIBRARY_NAME = name + + +Library("dummy") + +SOURCES = ["bar.c", "baz.cpp", "foo.c", "qux.cpp"] diff --git a/python/mozbuild/mozbuild/test/backend/data/database/qux.cpp b/python/mozbuild/mozbuild/test/backend/data/database/qux.cpp new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/python/mozbuild/mozbuild/test/backend/data/database/qux.cpp diff --git a/python/mozbuild/mozbuild/test/backend/data/defines/moz.build b/python/mozbuild/mozbuild/test/backend/data/defines/moz.build new file mode 100644 index 0000000000..b603cac3ff --- /dev/null +++ b/python/mozbuild/mozbuild/test/backend/data/defines/moz.build @@ -0,0 +1,9 @@ +# Any copyright is dedicated to the Public Domain. +# http://creativecommons.org/publicdomain/zero/1.0/ + +value = "xyz" +DEFINES["FOO"] = True +DEFINES["BAZ"] = '"ab\'cd"' +DEFINES["QUX"] = False +DEFINES["BAR"] = 7 +DEFINES["VALUE"] = value diff --git a/python/mozbuild/mozbuild/test/backend/data/dist-files/install.rdf b/python/mozbuild/mozbuild/test/backend/data/dist-files/install.rdf new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/python/mozbuild/mozbuild/test/backend/data/dist-files/install.rdf diff --git a/python/mozbuild/mozbuild/test/backend/data/dist-files/main.js b/python/mozbuild/mozbuild/test/backend/data/dist-files/main.js new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/python/mozbuild/mozbuild/test/backend/data/dist-files/main.js diff --git a/python/mozbuild/mozbuild/test/backend/data/dist-files/moz.build b/python/mozbuild/mozbuild/test/backend/data/dist-files/moz.build new file mode 100644 index 0000000000..25961f149f --- /dev/null +++ b/python/mozbuild/mozbuild/test/backend/data/dist-files/moz.build @@ -0,0 +1,8 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# Any copyright is dedicated to the Public Domain. +# http://creativecommons.org/publicdomain/zero/1.0/ + +FINAL_TARGET_PP_FILES += [ + "install.rdf", + "main.js", +] diff --git a/python/mozbuild/mozbuild/test/backend/data/exports-generated/dom1.h b/python/mozbuild/mozbuild/test/backend/data/exports-generated/dom1.h new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/python/mozbuild/mozbuild/test/backend/data/exports-generated/dom1.h diff --git a/python/mozbuild/mozbuild/test/backend/data/exports-generated/foo.h b/python/mozbuild/mozbuild/test/backend/data/exports-generated/foo.h new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/python/mozbuild/mozbuild/test/backend/data/exports-generated/foo.h diff --git a/python/mozbuild/mozbuild/test/backend/data/exports-generated/gfx.h b/python/mozbuild/mozbuild/test/backend/data/exports-generated/gfx.h new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/python/mozbuild/mozbuild/test/backend/data/exports-generated/gfx.h diff --git a/python/mozbuild/mozbuild/test/backend/data/exports-generated/moz.build b/python/mozbuild/mozbuild/test/backend/data/exports-generated/moz.build new file mode 100644 index 0000000000..44c31a3d9c --- /dev/null +++ b/python/mozbuild/mozbuild/test/backend/data/exports-generated/moz.build @@ -0,0 +1,12 @@ +# Any copyright is dedicated to the Public Domain. +# http://creativecommons.org/publicdomain/zero/1.0/ + +EXPORTS += ["!bar.h", "foo.h"] +EXPORTS.mozilla += ["!mozilla2.h", "mozilla1.h"] +EXPORTS.mozilla.dom += ["!dom2.h", "!dom3.h", "dom1.h"] +EXPORTS.gfx += ["gfx.h"] + +GENERATED_FILES += ["bar.h"] +GENERATED_FILES += ["mozilla2.h"] +GENERATED_FILES += ["dom2.h"] +GENERATED_FILES += ["dom3.h"] diff --git a/python/mozbuild/mozbuild/test/backend/data/exports-generated/mozilla1.h b/python/mozbuild/mozbuild/test/backend/data/exports-generated/mozilla1.h new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/python/mozbuild/mozbuild/test/backend/data/exports-generated/mozilla1.h diff --git a/python/mozbuild/mozbuild/test/backend/data/exports/dom1.h b/python/mozbuild/mozbuild/test/backend/data/exports/dom1.h new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/python/mozbuild/mozbuild/test/backend/data/exports/dom1.h diff --git a/python/mozbuild/mozbuild/test/backend/data/exports/dom2.h b/python/mozbuild/mozbuild/test/backend/data/exports/dom2.h new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/python/mozbuild/mozbuild/test/backend/data/exports/dom2.h diff --git a/python/mozbuild/mozbuild/test/backend/data/exports/foo.h b/python/mozbuild/mozbuild/test/backend/data/exports/foo.h new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/python/mozbuild/mozbuild/test/backend/data/exports/foo.h diff --git a/python/mozbuild/mozbuild/test/backend/data/exports/gfx.h b/python/mozbuild/mozbuild/test/backend/data/exports/gfx.h new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/python/mozbuild/mozbuild/test/backend/data/exports/gfx.h diff --git a/python/mozbuild/mozbuild/test/backend/data/exports/moz.build b/python/mozbuild/mozbuild/test/backend/data/exports/moz.build new file mode 100644 index 0000000000..371f26f572 --- /dev/null +++ b/python/mozbuild/mozbuild/test/backend/data/exports/moz.build @@ -0,0 +1,8 @@ +# Any copyright is dedicated to the Public Domain. +# http://creativecommons.org/publicdomain/zero/1.0/ + +EXPORTS += ["foo.h"] +EXPORTS.mozilla += ["mozilla1.h", "mozilla2.h"] +EXPORTS.mozilla.dom += ["dom1.h", "dom2.h"] +EXPORTS.mozilla.gfx += ["gfx.h"] +EXPORTS.nspr.private += ["pprio.h"] diff --git a/python/mozbuild/mozbuild/test/backend/data/exports/mozilla1.h b/python/mozbuild/mozbuild/test/backend/data/exports/mozilla1.h new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/python/mozbuild/mozbuild/test/backend/data/exports/mozilla1.h diff --git a/python/mozbuild/mozbuild/test/backend/data/exports/mozilla2.h b/python/mozbuild/mozbuild/test/backend/data/exports/mozilla2.h new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/python/mozbuild/mozbuild/test/backend/data/exports/mozilla2.h diff --git a/python/mozbuild/mozbuild/test/backend/data/exports/pprio.h b/python/mozbuild/mozbuild/test/backend/data/exports/pprio.h new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/python/mozbuild/mozbuild/test/backend/data/exports/pprio.h diff --git a/python/mozbuild/mozbuild/test/backend/data/final-target-files-wildcard/bar.xyz b/python/mozbuild/mozbuild/test/backend/data/final-target-files-wildcard/bar.xyz new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/python/mozbuild/mozbuild/test/backend/data/final-target-files-wildcard/bar.xyz diff --git a/python/mozbuild/mozbuild/test/backend/data/final-target-files-wildcard/foo.xyz b/python/mozbuild/mozbuild/test/backend/data/final-target-files-wildcard/foo.xyz new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/python/mozbuild/mozbuild/test/backend/data/final-target-files-wildcard/foo.xyz diff --git a/python/mozbuild/mozbuild/test/backend/data/final-target-files-wildcard/moz.build b/python/mozbuild/mozbuild/test/backend/data/final-target-files-wildcard/moz.build new file mode 100644 index 0000000000..d665855234 --- /dev/null +++ b/python/mozbuild/mozbuild/test/backend/data/final-target-files-wildcard/moz.build @@ -0,0 +1,5 @@ +# 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/. + +FINAL_TARGET_FILES.foo += ["*.xyz"] diff --git a/python/mozbuild/mozbuild/test/backend/data/final_target/both/moz.build b/python/mozbuild/mozbuild/test/backend/data/final_target/both/moz.build new file mode 100644 index 0000000000..dfbda9183b --- /dev/null +++ b/python/mozbuild/mozbuild/test/backend/data/final_target/both/moz.build @@ -0,0 +1,6 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# Any copyright is dedicated to the Public Domain. +# http://creativecommons.org/publicdomain/zero/1.0/ + +XPI_NAME = "mycrazyxpi" +DIST_SUBDIR = "asubdir" diff --git a/python/mozbuild/mozbuild/test/backend/data/final_target/dist-subdir/moz.build b/python/mozbuild/mozbuild/test/backend/data/final_target/dist-subdir/moz.build new file mode 100644 index 0000000000..e44dd197ad --- /dev/null +++ b/python/mozbuild/mozbuild/test/backend/data/final_target/dist-subdir/moz.build @@ -0,0 +1,5 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# Any copyright is dedicated to the Public Domain. +# http://creativecommons.org/publicdomain/zero/1.0/ + +DIST_SUBDIR = "asubdir" diff --git a/python/mozbuild/mozbuild/test/backend/data/final_target/final-target/moz.build b/python/mozbuild/mozbuild/test/backend/data/final_target/final-target/moz.build new file mode 100644 index 0000000000..e008f94478 --- /dev/null +++ b/python/mozbuild/mozbuild/test/backend/data/final_target/final-target/moz.build @@ -0,0 +1,5 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# Any copyright is dedicated to the Public Domain. +# http://creativecommons.org/publicdomain/zero/1.0/ + +FINAL_TARGET = "random-final-target" diff --git a/python/mozbuild/mozbuild/test/backend/data/final_target/moz.build b/python/mozbuild/mozbuild/test/backend/data/final_target/moz.build new file mode 100644 index 0000000000..319062b78f --- /dev/null +++ b/python/mozbuild/mozbuild/test/backend/data/final_target/moz.build @@ -0,0 +1,5 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# Any copyright is dedicated to the Public Domain. +# http://creativecommons.org/publicdomain/zero/1.0/ + +DIRS += ["xpi-name", "dist-subdir", "both", "final-target"] diff --git a/python/mozbuild/mozbuild/test/backend/data/final_target/xpi-name/moz.build b/python/mozbuild/mozbuild/test/backend/data/final_target/xpi-name/moz.build new file mode 100644 index 0000000000..980810caa3 --- /dev/null +++ b/python/mozbuild/mozbuild/test/backend/data/final_target/xpi-name/moz.build @@ -0,0 +1,5 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# Any copyright is dedicated to the Public Domain. +# http://creativecommons.org/publicdomain/zero/1.0/ + +XPI_NAME = "mycrazyxpi" diff --git a/python/mozbuild/mozbuild/test/backend/data/generated-files-force/foo-data b/python/mozbuild/mozbuild/test/backend/data/generated-files-force/foo-data new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/python/mozbuild/mozbuild/test/backend/data/generated-files-force/foo-data diff --git a/python/mozbuild/mozbuild/test/backend/data/generated-files-force/generate-bar.py b/python/mozbuild/mozbuild/test/backend/data/generated-files-force/generate-bar.py new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/python/mozbuild/mozbuild/test/backend/data/generated-files-force/generate-bar.py diff --git a/python/mozbuild/mozbuild/test/backend/data/generated-files-force/generate-foo.py b/python/mozbuild/mozbuild/test/backend/data/generated-files-force/generate-foo.py new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/python/mozbuild/mozbuild/test/backend/data/generated-files-force/generate-foo.py diff --git a/python/mozbuild/mozbuild/test/backend/data/generated-files-force/moz.build b/python/mozbuild/mozbuild/test/backend/data/generated-files-force/moz.build new file mode 100644 index 0000000000..d86b7b09ea --- /dev/null +++ b/python/mozbuild/mozbuild/test/backend/data/generated-files-force/moz.build @@ -0,0 +1,14 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# Any copyright is dedicated to the Public Domain. +# http://creativecommons.org/publicdomain/zero/1.0/ + +GENERATED_FILES += ["bar.c", "foo.c", "quux.c"] + +bar = GENERATED_FILES["bar.c"] +bar.script = "generate-bar.py:baz" +bar.force = True + +foo = GENERATED_FILES["foo.c"] +foo.script = "generate-foo.py" +foo.inputs = ["foo-data"] +foo.force = False diff --git a/python/mozbuild/mozbuild/test/backend/data/generated-files/foo-data b/python/mozbuild/mozbuild/test/backend/data/generated-files/foo-data new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/python/mozbuild/mozbuild/test/backend/data/generated-files/foo-data diff --git a/python/mozbuild/mozbuild/test/backend/data/generated-files/generate-bar.py b/python/mozbuild/mozbuild/test/backend/data/generated-files/generate-bar.py new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/python/mozbuild/mozbuild/test/backend/data/generated-files/generate-bar.py diff --git a/python/mozbuild/mozbuild/test/backend/data/generated-files/generate-foo.py b/python/mozbuild/mozbuild/test/backend/data/generated-files/generate-foo.py new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/python/mozbuild/mozbuild/test/backend/data/generated-files/generate-foo.py diff --git a/python/mozbuild/mozbuild/test/backend/data/generated-files/moz.build b/python/mozbuild/mozbuild/test/backend/data/generated-files/moz.build new file mode 100644 index 0000000000..01b444238e --- /dev/null +++ b/python/mozbuild/mozbuild/test/backend/data/generated-files/moz.build @@ -0,0 +1,12 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# Any copyright is dedicated to the Public Domain. +# http://creativecommons.org/publicdomain/zero/1.0/ + +GENERATED_FILES += ["bar.c", "foo.h", "quux.c"] + +bar = GENERATED_FILES["bar.c"] +bar.script = "generate-bar.py:baz" + +foo = GENERATED_FILES["foo.h"] +foo.script = "generate-foo.py" +foo.inputs = ["foo-data"] diff --git a/python/mozbuild/mozbuild/test/backend/data/generated_includes/moz.build b/python/mozbuild/mozbuild/test/backend/data/generated_includes/moz.build new file mode 100644 index 0000000000..31f9042c0a --- /dev/null +++ b/python/mozbuild/mozbuild/test/backend/data/generated_includes/moz.build @@ -0,0 +1,5 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# Any copyright is dedicated to the Public Domain. +# http://creativecommons.org/publicdomain/zero/1.0/ + +LOCAL_INCLUDES += ["!/bar/baz", "!foo"] diff --git a/python/mozbuild/mozbuild/test/backend/data/host-defines/moz.build b/python/mozbuild/mozbuild/test/backend/data/host-defines/moz.build new file mode 100644 index 0000000000..f1a632c841 --- /dev/null +++ b/python/mozbuild/mozbuild/test/backend/data/host-defines/moz.build @@ -0,0 +1,9 @@ +# Any copyright is dedicated to the Public Domain. +# http://creativecommons.org/publicdomain/zero/1.0/ + +value = "xyz" +HOST_DEFINES["FOO"] = True +HOST_DEFINES["BAZ"] = '"ab\'cd"' +HOST_DEFINES["BAR"] = 7 +HOST_DEFINES["VALUE"] = value +HOST_DEFINES["QUX"] = False diff --git a/python/mozbuild/mozbuild/test/backend/data/host-rust-library-features/Cargo.toml b/python/mozbuild/mozbuild/test/backend/data/host-rust-library-features/Cargo.toml new file mode 100644 index 0000000000..d20d04fc7e --- /dev/null +++ b/python/mozbuild/mozbuild/test/backend/data/host-rust-library-features/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "hostrusttool" +version = "0.1.0" +authors = ["The Mozilla Project Developers"] + +[lib] +crate-type = ["staticlib"] + +[profile.dev] +panic = "abort" + +[profile.release] +panic = "abort" + +[dependencies] +mozilla-central-workspace-hack = { version = "0.1", features = ["hostrusttool"], optional = true } diff --git a/python/mozbuild/mozbuild/test/backend/data/host-rust-library-features/moz.build b/python/mozbuild/mozbuild/test/backend/data/host-rust-library-features/moz.build new file mode 100644 index 0000000000..96fccf2063 --- /dev/null +++ b/python/mozbuild/mozbuild/test/backend/data/host-rust-library-features/moz.build @@ -0,0 +1,22 @@ +# Any copyright is dedicated to the Public Domain. +# http://creativecommons.org/publicdomain/zero/1.0/ + + +@template +def HostLibrary(name): + """Template for libraries.""" + HOST_LIBRARY_NAME = name + + +@template +def HostRustLibrary(name, features=None): + """Template for Rust libraries.""" + HostLibrary(name) + + IS_RUST_LIBRARY = True + + if features: + HOST_RUST_LIBRARY_FEATURES = features + + +HostRustLibrary("hostrusttool", ["musthave", "cantlivewithout"]) diff --git a/python/mozbuild/mozbuild/test/backend/data/host-rust-library/Cargo.toml b/python/mozbuild/mozbuild/test/backend/data/host-rust-library/Cargo.toml new file mode 100644 index 0000000000..f052ae1cad --- /dev/null +++ b/python/mozbuild/mozbuild/test/backend/data/host-rust-library/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "hostrusttool" +version = "0.1.0" +authors = [ + "The Mozilla Project Developers", +] + +[lib] +crate-type = ["staticlib"] + +[profile.dev] +panic = "abort" + +[profile.release] +panic = "abort" + +[dependencies] +mozilla-central-workspace-hack = { version = "0.1", features = ["hostrusttool"], optional = true } diff --git a/python/mozbuild/mozbuild/test/backend/data/host-rust-library/moz.build b/python/mozbuild/mozbuild/test/backend/data/host-rust-library/moz.build new file mode 100644 index 0000000000..515f5d1a9f --- /dev/null +++ b/python/mozbuild/mozbuild/test/backend/data/host-rust-library/moz.build @@ -0,0 +1,22 @@ +# Any copyright is dedicated to the Public Domain. +# http://creativecommons.org/publicdomain/zero/1.0/ + + +@template +def HostLibrary(name): + """Template for libraries.""" + HOST_LIBRARY_NAME = name + + +@template +def HostRustLibrary(name, features=None): + """Template for Rust libraries.""" + HostLibrary(name) + + IS_RUST_LIBRARY = True + + if features: + HOST_RUST_LIBRARY_FEATURES = features + + +HostRustLibrary("hostrusttool") diff --git a/python/mozbuild/mozbuild/test/backend/data/install_substitute_config_files/moz.build b/python/mozbuild/mozbuild/test/backend/data/install_substitute_config_files/moz.build new file mode 100644 index 0000000000..c38b472911 --- /dev/null +++ b/python/mozbuild/mozbuild/test/backend/data/install_substitute_config_files/moz.build @@ -0,0 +1,6 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# Any copyright is dedicated to the Public Domain. +# http://creativecommons.org/publicdomain/zero/1.0/ + +# We want to test recursion into the subdir, so do the real work in 'sub' +DIRS += ["sub"] diff --git a/python/mozbuild/mozbuild/test/backend/data/install_substitute_config_files/sub/foo.h.in b/python/mozbuild/mozbuild/test/backend/data/install_substitute_config_files/sub/foo.h.in new file mode 100644 index 0000000000..da287dfcaa --- /dev/null +++ b/python/mozbuild/mozbuild/test/backend/data/install_substitute_config_files/sub/foo.h.in @@ -0,0 +1 @@ +#define MOZ_FOO @MOZ_FOO@ diff --git a/python/mozbuild/mozbuild/test/backend/data/install_substitute_config_files/sub/moz.build b/python/mozbuild/mozbuild/test/backend/data/install_substitute_config_files/sub/moz.build new file mode 100644 index 0000000000..1420a99a8f --- /dev/null +++ b/python/mozbuild/mozbuild/test/backend/data/install_substitute_config_files/sub/moz.build @@ -0,0 +1,7 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# Any copyright is dedicated to the Public Domain. +# http://creativecommons.org/publicdomain/zero/1.0/ + +CONFIGURE_SUBST_FILES = ["foo.h"] + +EXPORTS.out += ["!foo.h"] diff --git a/python/mozbuild/mozbuild/test/backend/data/ipdl_sources/bar/moz.build b/python/mozbuild/mozbuild/test/backend/data/ipdl_sources/bar/moz.build new file mode 100644 index 0000000000..f7d1560af3 --- /dev/null +++ b/python/mozbuild/mozbuild/test/backend/data/ipdl_sources/bar/moz.build @@ -0,0 +1,16 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# 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/. + +PREPROCESSED_IPDL_SOURCES += [ + "bar1.ipdl", +] + +IPDL_SOURCES += [ + "bar.ipdl", + "bar2.ipdlh", +] + +FINAL_LIBRARY = "dummy" diff --git a/python/mozbuild/mozbuild/test/backend/data/ipdl_sources/foo/moz.build b/python/mozbuild/mozbuild/test/backend/data/ipdl_sources/foo/moz.build new file mode 100644 index 0000000000..02e9f78154 --- /dev/null +++ b/python/mozbuild/mozbuild/test/backend/data/ipdl_sources/foo/moz.build @@ -0,0 +1,16 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# 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/. + +PREPROCESSED_IPDL_SOURCES += [ + "foo1.ipdl", +] + +IPDL_SOURCES += [ + "foo.ipdl", + "foo2.ipdlh", +] + +FINAL_LIBRARY = "dummy" diff --git a/python/mozbuild/mozbuild/test/backend/data/ipdl_sources/ipdl/moz.build b/python/mozbuild/mozbuild/test/backend/data/ipdl_sources/ipdl/moz.build new file mode 100644 index 0000000000..066397cb84 --- /dev/null +++ b/python/mozbuild/mozbuild/test/backend/data/ipdl_sources/ipdl/moz.build @@ -0,0 +1,9 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# 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/. + +# This file just exists to establish a directory as the IPDL root directory. + +FINAL_LIBRARY = "dummy" diff --git a/python/mozbuild/mozbuild/test/backend/data/ipdl_sources/moz.build b/python/mozbuild/mozbuild/test/backend/data/ipdl_sources/moz.build new file mode 100644 index 0000000000..4f0ddaa10e --- /dev/null +++ b/python/mozbuild/mozbuild/test/backend/data/ipdl_sources/moz.build @@ -0,0 +1,19 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# 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/. + + +@template +def Library(name): + LIBRARY_NAME = name + + +Library("dummy") + +DIRS += [ + "bar", + "foo", + "ipdl", +] diff --git a/python/mozbuild/mozbuild/test/backend/data/jar-manifests/moz.build b/python/mozbuild/mozbuild/test/backend/data/jar-manifests/moz.build new file mode 100644 index 0000000000..d988c0ff9b --- /dev/null +++ b/python/mozbuild/mozbuild/test/backend/data/jar-manifests/moz.build @@ -0,0 +1,7 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# 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/. + +JAR_MANIFESTS += ["jar.mn"] diff --git a/python/mozbuild/mozbuild/test/backend/data/linkage/moz.build b/python/mozbuild/mozbuild/test/backend/data/linkage/moz.build new file mode 100644 index 0000000000..f01a012760 --- /dev/null +++ b/python/mozbuild/mozbuild/test/backend/data/linkage/moz.build @@ -0,0 +1,11 @@ +# Any copyright is dedicated to the Public Domain. +# http://creativecommons.org/publicdomain/zero/1.0/ + +include("templates.mozbuild") + +DIRS += [ + "real", + "shared", + "prog", + "static", +] diff --git a/python/mozbuild/mozbuild/test/backend/data/linkage/prog/moz.build b/python/mozbuild/mozbuild/test/backend/data/linkage/prog/moz.build new file mode 100644 index 0000000000..3741f4be09 --- /dev/null +++ b/python/mozbuild/mozbuild/test/backend/data/linkage/prog/moz.build @@ -0,0 +1,11 @@ +# Any copyright is dedicated to the Public Domain. +# http://creativecommons.org/publicdomain/zero/1.0/ + +DIRS += ["qux"] + +Program("MyProgram") + +USE_LIBS += [ + "bar", + "baz", +] diff --git a/python/mozbuild/mozbuild/test/backend/data/linkage/prog/qux/moz.build b/python/mozbuild/mozbuild/test/backend/data/linkage/prog/qux/moz.build new file mode 100644 index 0000000000..3152de6211 --- /dev/null +++ b/python/mozbuild/mozbuild/test/backend/data/linkage/prog/qux/moz.build @@ -0,0 +1,6 @@ +# Any copyright is dedicated to the Public Domain. +# http://creativecommons.org/publicdomain/zero/1.0/ + +SOURCES += ["qux1.c"] + +SharedLibrary("qux") diff --git a/python/mozbuild/mozbuild/test/backend/data/linkage/prog/qux/qux1.c b/python/mozbuild/mozbuild/test/backend/data/linkage/prog/qux/qux1.c new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/python/mozbuild/mozbuild/test/backend/data/linkage/prog/qux/qux1.c diff --git a/python/mozbuild/mozbuild/test/backend/data/linkage/real/foo/foo1.c b/python/mozbuild/mozbuild/test/backend/data/linkage/real/foo/foo1.c new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/python/mozbuild/mozbuild/test/backend/data/linkage/real/foo/foo1.c diff --git a/python/mozbuild/mozbuild/test/backend/data/linkage/real/foo/foo2.c b/python/mozbuild/mozbuild/test/backend/data/linkage/real/foo/foo2.c new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/python/mozbuild/mozbuild/test/backend/data/linkage/real/foo/foo2.c diff --git a/python/mozbuild/mozbuild/test/backend/data/linkage/real/foo/moz.build b/python/mozbuild/mozbuild/test/backend/data/linkage/real/foo/moz.build new file mode 100644 index 0000000000..a0bd7526e6 --- /dev/null +++ b/python/mozbuild/mozbuild/test/backend/data/linkage/real/foo/moz.build @@ -0,0 +1,6 @@ +# Any copyright is dedicated to the Public Domain. +# http://creativecommons.org/publicdomain/zero/1.0/ + +SOURCES += ["foo1.c", "foo2.c"] + +FINAL_LIBRARY = "foo" diff --git a/python/mozbuild/mozbuild/test/backend/data/linkage/real/moz.build b/python/mozbuild/mozbuild/test/backend/data/linkage/real/moz.build new file mode 100644 index 0000000000..32f9c1d656 --- /dev/null +++ b/python/mozbuild/mozbuild/test/backend/data/linkage/real/moz.build @@ -0,0 +1,14 @@ +# Any copyright is dedicated to the Public Domain. +# http://creativecommons.org/publicdomain/zero/1.0/ + +DIRS += [ + "foo", +] + +NO_EXPAND_LIBS = True + +OS_LIBS += ["-lbaz"] + +USE_LIBS += ["static:baz"] + +Library("foo") diff --git a/python/mozbuild/mozbuild/test/backend/data/linkage/shared/baz/baz1.c b/python/mozbuild/mozbuild/test/backend/data/linkage/shared/baz/baz1.c new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/python/mozbuild/mozbuild/test/backend/data/linkage/shared/baz/baz1.c diff --git a/python/mozbuild/mozbuild/test/backend/data/linkage/shared/baz/moz.build b/python/mozbuild/mozbuild/test/backend/data/linkage/shared/baz/moz.build new file mode 100644 index 0000000000..3299fa28f4 --- /dev/null +++ b/python/mozbuild/mozbuild/test/backend/data/linkage/shared/baz/moz.build @@ -0,0 +1,6 @@ +# Any copyright is dedicated to the Public Domain. +# http://creativecommons.org/publicdomain/zero/1.0/ + +SOURCES += ["baz1.c"] + +FINAL_LIBRARY = "baz" diff --git a/python/mozbuild/mozbuild/test/backend/data/linkage/shared/moz.build b/python/mozbuild/mozbuild/test/backend/data/linkage/shared/moz.build new file mode 100644 index 0000000000..42d79fe1fd --- /dev/null +++ b/python/mozbuild/mozbuild/test/backend/data/linkage/shared/moz.build @@ -0,0 +1,14 @@ +# Any copyright is dedicated to the Public Domain. +# http://creativecommons.org/publicdomain/zero/1.0/ + +DIRS += [ + "baz", +] + +STATIC_LIBRARY_NAME = "baz_s" +FORCE_STATIC_LIB = True + +OS_LIBS += ["-lfoo"] +USE_LIBS += ["qux"] + +SharedLibrary("baz") diff --git a/python/mozbuild/mozbuild/test/backend/data/linkage/static/bar/bar1.cc b/python/mozbuild/mozbuild/test/backend/data/linkage/static/bar/bar1.cc new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/python/mozbuild/mozbuild/test/backend/data/linkage/static/bar/bar1.cc diff --git a/python/mozbuild/mozbuild/test/backend/data/linkage/static/bar/bar2.cc b/python/mozbuild/mozbuild/test/backend/data/linkage/static/bar/bar2.cc new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/python/mozbuild/mozbuild/test/backend/data/linkage/static/bar/bar2.cc diff --git a/python/mozbuild/mozbuild/test/backend/data/linkage/static/bar/bar_helper/bar_helper1.cpp b/python/mozbuild/mozbuild/test/backend/data/linkage/static/bar/bar_helper/bar_helper1.cpp new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/python/mozbuild/mozbuild/test/backend/data/linkage/static/bar/bar_helper/bar_helper1.cpp diff --git a/python/mozbuild/mozbuild/test/backend/data/linkage/static/bar/bar_helper/moz.build b/python/mozbuild/mozbuild/test/backend/data/linkage/static/bar/bar_helper/moz.build new file mode 100644 index 0000000000..12d0fc83fb --- /dev/null +++ b/python/mozbuild/mozbuild/test/backend/data/linkage/static/bar/bar_helper/moz.build @@ -0,0 +1,8 @@ +# Any copyright is dedicated to the Public Domain. +# http://creativecommons.org/publicdomain/zero/1.0/ + +SOURCES += [ + "bar_helper1.cpp", +] + +FINAL_LIBRARY = "bar" diff --git a/python/mozbuild/mozbuild/test/backend/data/linkage/static/bar/moz.build b/python/mozbuild/mozbuild/test/backend/data/linkage/static/bar/moz.build new file mode 100644 index 0000000000..d9d75803ed --- /dev/null +++ b/python/mozbuild/mozbuild/test/backend/data/linkage/static/bar/moz.build @@ -0,0 +1,13 @@ +# Any copyright is dedicated to the Public Domain. +# http://creativecommons.org/publicdomain/zero/1.0/ + +SOURCES += [ + "bar1.cc", + "bar2.cc", +] + +DIRS += [ + "bar_helper", +] + +FINAL_LIBRARY = "bar" diff --git a/python/mozbuild/mozbuild/test/backend/data/linkage/static/moz.build b/python/mozbuild/mozbuild/test/backend/data/linkage/static/moz.build new file mode 100644 index 0000000000..37b3d96cc7 --- /dev/null +++ b/python/mozbuild/mozbuild/test/backend/data/linkage/static/moz.build @@ -0,0 +1,12 @@ +# Any copyright is dedicated to the Public Domain. +# http://creativecommons.org/publicdomain/zero/1.0/ + +DIRS += [ + "bar", +] + +USE_LIBS += ["foo"] + +OS_LIBS += ["-lbar"] + +Library("bar") diff --git a/python/mozbuild/mozbuild/test/backend/data/linkage/templates.mozbuild b/python/mozbuild/mozbuild/test/backend/data/linkage/templates.mozbuild new file mode 100644 index 0000000000..4c360bae80 --- /dev/null +++ b/python/mozbuild/mozbuild/test/backend/data/linkage/templates.mozbuild @@ -0,0 +1,26 @@ +# Any copyright is dedicated to the Public Domain. +# http://creativecommons.org/publicdomain/zero/1.0/ + + +@template +def Library(name): + LIBRARY_NAME = name + + +@template +def SharedLibrary(name): + FORCE_SHARED_LIB = True + LIBRARY_NAME = name + + +@template +def Binary(): + # Add -lfoo for testing purposes. + OS_LIBS += ["foo"] + + +@template +def Program(name): + PROGRAM = name + + Binary() diff --git a/python/mozbuild/mozbuild/test/backend/data/local_includes/bar/baz/dummy_file_for_nonempty_directory b/python/mozbuild/mozbuild/test/backend/data/local_includes/bar/baz/dummy_file_for_nonempty_directory new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/python/mozbuild/mozbuild/test/backend/data/local_includes/bar/baz/dummy_file_for_nonempty_directory diff --git a/python/mozbuild/mozbuild/test/backend/data/local_includes/foo/dummy_file_for_nonempty_directory b/python/mozbuild/mozbuild/test/backend/data/local_includes/foo/dummy_file_for_nonempty_directory new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/python/mozbuild/mozbuild/test/backend/data/local_includes/foo/dummy_file_for_nonempty_directory diff --git a/python/mozbuild/mozbuild/test/backend/data/local_includes/moz.build b/python/mozbuild/mozbuild/test/backend/data/local_includes/moz.build new file mode 100644 index 0000000000..1c29ac2ea2 --- /dev/null +++ b/python/mozbuild/mozbuild/test/backend/data/local_includes/moz.build @@ -0,0 +1,5 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# Any copyright is dedicated to the Public Domain. +# http://creativecommons.org/publicdomain/zero/1.0/ + +LOCAL_INCLUDES += ["/bar/baz", "foo"] diff --git a/python/mozbuild/mozbuild/test/backend/data/localized-files/en-US/bar.ini b/python/mozbuild/mozbuild/test/backend/data/localized-files/en-US/bar.ini new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/python/mozbuild/mozbuild/test/backend/data/localized-files/en-US/bar.ini diff --git a/python/mozbuild/mozbuild/test/backend/data/localized-files/en-US/foo.js b/python/mozbuild/mozbuild/test/backend/data/localized-files/en-US/foo.js new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/python/mozbuild/mozbuild/test/backend/data/localized-files/en-US/foo.js diff --git a/python/mozbuild/mozbuild/test/backend/data/localized-files/moz.build b/python/mozbuild/mozbuild/test/backend/data/localized-files/moz.build new file mode 100644 index 0000000000..93a97c7b84 --- /dev/null +++ b/python/mozbuild/mozbuild/test/backend/data/localized-files/moz.build @@ -0,0 +1,9 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# Any copyright is dedicated to the Public Domain. +# http://creativecommons.org/publicdomain/zero/1.0/ + +LOCALIZED_FILES += [ + "en-US/abc/*.abc", + "en-US/bar.ini", + "en-US/foo.js", +] diff --git a/python/mozbuild/mozbuild/test/backend/data/localized-generated-files-AB_CD/en-US/localized-input b/python/mozbuild/mozbuild/test/backend/data/localized-generated-files-AB_CD/en-US/localized-input new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/python/mozbuild/mozbuild/test/backend/data/localized-generated-files-AB_CD/en-US/localized-input diff --git a/python/mozbuild/mozbuild/test/backend/data/localized-generated-files-AB_CD/foo-data b/python/mozbuild/mozbuild/test/backend/data/localized-generated-files-AB_CD/foo-data new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/python/mozbuild/mozbuild/test/backend/data/localized-generated-files-AB_CD/foo-data diff --git a/python/mozbuild/mozbuild/test/backend/data/localized-generated-files-AB_CD/generate-foo.py b/python/mozbuild/mozbuild/test/backend/data/localized-generated-files-AB_CD/generate-foo.py new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/python/mozbuild/mozbuild/test/backend/data/localized-generated-files-AB_CD/generate-foo.py diff --git a/python/mozbuild/mozbuild/test/backend/data/localized-generated-files-AB_CD/inner/locales/en-US/localized-input b/python/mozbuild/mozbuild/test/backend/data/localized-generated-files-AB_CD/inner/locales/en-US/localized-input new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/python/mozbuild/mozbuild/test/backend/data/localized-generated-files-AB_CD/inner/locales/en-US/localized-input diff --git a/python/mozbuild/mozbuild/test/backend/data/localized-generated-files-AB_CD/locales/en-US/localized-input b/python/mozbuild/mozbuild/test/backend/data/localized-generated-files-AB_CD/locales/en-US/localized-input new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/python/mozbuild/mozbuild/test/backend/data/localized-generated-files-AB_CD/locales/en-US/localized-input diff --git a/python/mozbuild/mozbuild/test/backend/data/localized-generated-files-AB_CD/moz.build b/python/mozbuild/mozbuild/test/backend/data/localized-generated-files-AB_CD/moz.build new file mode 100644 index 0000000000..2b0cf472c9 --- /dev/null +++ b/python/mozbuild/mozbuild/test/backend/data/localized-generated-files-AB_CD/moz.build @@ -0,0 +1,32 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# Any copyright is dedicated to the Public Domain. +# http://creativecommons.org/publicdomain/zero/1.0/ + +LOCALIZED_GENERATED_FILES += ["foo{AB_CD}.xyz"] + +foo = LOCALIZED_GENERATED_FILES["foo{AB_CD}.xyz"] +foo.script = "generate-foo.py" +foo.inputs = [ + "en-US/localized-input", + "non-localized-input", +] + +LOCALIZED_GENERATED_FILES += ["bar{AB_rCD}.xyz"] + +bar = LOCALIZED_GENERATED_FILES["bar{AB_rCD}.xyz"] +bar.script = "generate-foo.py" +bar.inputs = [ + # Absolute source path. + "/inner/locales/en-US/localized-input", + "non-localized-input", +] + +LOCALIZED_GENERATED_FILES += ["zot{AB_rCD}.xyz"] + +bar = LOCALIZED_GENERATED_FILES["zot{AB_rCD}.xyz"] +bar.script = "generate-foo.py" +bar.inputs = [ + # Relative source path. + "locales/en-US/localized-input", + "non-localized-input", +] diff --git a/python/mozbuild/mozbuild/test/backend/data/localized-generated-files-AB_CD/non-localized-input b/python/mozbuild/mozbuild/test/backend/data/localized-generated-files-AB_CD/non-localized-input new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/python/mozbuild/mozbuild/test/backend/data/localized-generated-files-AB_CD/non-localized-input diff --git a/python/mozbuild/mozbuild/test/backend/data/localized-generated-files-force/en-US/localized-input b/python/mozbuild/mozbuild/test/backend/data/localized-generated-files-force/en-US/localized-input new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/python/mozbuild/mozbuild/test/backend/data/localized-generated-files-force/en-US/localized-input diff --git a/python/mozbuild/mozbuild/test/backend/data/localized-generated-files-force/foo-data b/python/mozbuild/mozbuild/test/backend/data/localized-generated-files-force/foo-data new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/python/mozbuild/mozbuild/test/backend/data/localized-generated-files-force/foo-data diff --git a/python/mozbuild/mozbuild/test/backend/data/localized-generated-files-force/generate-foo.py b/python/mozbuild/mozbuild/test/backend/data/localized-generated-files-force/generate-foo.py new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/python/mozbuild/mozbuild/test/backend/data/localized-generated-files-force/generate-foo.py diff --git a/python/mozbuild/mozbuild/test/backend/data/localized-generated-files-force/moz.build b/python/mozbuild/mozbuild/test/backend/data/localized-generated-files-force/moz.build new file mode 100644 index 0000000000..26fb165e06 --- /dev/null +++ b/python/mozbuild/mozbuild/test/backend/data/localized-generated-files-force/moz.build @@ -0,0 +1,22 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# Any copyright is dedicated to the Public Domain. +# http://creativecommons.org/publicdomain/zero/1.0/ + +LOCALIZED_GENERATED_FILES += ["foo.xyz"] + +foo = LOCALIZED_GENERATED_FILES["foo.xyz"] +foo.script = "generate-foo.py" +foo.inputs = [ + "en-US/localized-input", + "non-localized-input", +] + +LOCALIZED_GENERATED_FILES += ["abc.xyz"] + +abc = LOCALIZED_GENERATED_FILES["abc.xyz"] +abc.script = "generate-foo.py" +abc.inputs = [ + "en-US/localized-input", + "non-localized-input", +] +abc.force = True diff --git a/python/mozbuild/mozbuild/test/backend/data/localized-generated-files-force/non-localized-input b/python/mozbuild/mozbuild/test/backend/data/localized-generated-files-force/non-localized-input new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/python/mozbuild/mozbuild/test/backend/data/localized-generated-files-force/non-localized-input diff --git a/python/mozbuild/mozbuild/test/backend/data/localized-generated-files/en-US/localized-input b/python/mozbuild/mozbuild/test/backend/data/localized-generated-files/en-US/localized-input new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/python/mozbuild/mozbuild/test/backend/data/localized-generated-files/en-US/localized-input diff --git a/python/mozbuild/mozbuild/test/backend/data/localized-generated-files/foo-data b/python/mozbuild/mozbuild/test/backend/data/localized-generated-files/foo-data new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/python/mozbuild/mozbuild/test/backend/data/localized-generated-files/foo-data diff --git a/python/mozbuild/mozbuild/test/backend/data/localized-generated-files/generate-foo.py b/python/mozbuild/mozbuild/test/backend/data/localized-generated-files/generate-foo.py new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/python/mozbuild/mozbuild/test/backend/data/localized-generated-files/generate-foo.py diff --git a/python/mozbuild/mozbuild/test/backend/data/localized-generated-files/moz.build b/python/mozbuild/mozbuild/test/backend/data/localized-generated-files/moz.build new file mode 100644 index 0000000000..f44325dfb1 --- /dev/null +++ b/python/mozbuild/mozbuild/test/backend/data/localized-generated-files/moz.build @@ -0,0 +1,15 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# Any copyright is dedicated to the Public Domain. +# http://creativecommons.org/publicdomain/zero/1.0/ + +LOCALIZED_GENERATED_FILES += ["foo.xyz"] + +foo = LOCALIZED_GENERATED_FILES["foo.xyz"] +foo.script = "generate-foo.py" +foo.inputs = [ + "en-US/localized-input", + "non-localized-input", +] + +# Also check that using it in LOCALIZED_FILES does the right thing. +LOCALIZED_FILES += ["!foo.xyz"] diff --git a/python/mozbuild/mozbuild/test/backend/data/localized-generated-files/non-localized-input b/python/mozbuild/mozbuild/test/backend/data/localized-generated-files/non-localized-input new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/python/mozbuild/mozbuild/test/backend/data/localized-generated-files/non-localized-input diff --git a/python/mozbuild/mozbuild/test/backend/data/localized-pp-files/en-US/bar.ini b/python/mozbuild/mozbuild/test/backend/data/localized-pp-files/en-US/bar.ini new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/python/mozbuild/mozbuild/test/backend/data/localized-pp-files/en-US/bar.ini diff --git a/python/mozbuild/mozbuild/test/backend/data/localized-pp-files/en-US/foo.js b/python/mozbuild/mozbuild/test/backend/data/localized-pp-files/en-US/foo.js new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/python/mozbuild/mozbuild/test/backend/data/localized-pp-files/en-US/foo.js diff --git a/python/mozbuild/mozbuild/test/backend/data/localized-pp-files/moz.build b/python/mozbuild/mozbuild/test/backend/data/localized-pp-files/moz.build new file mode 100644 index 0000000000..8cec207128 --- /dev/null +++ b/python/mozbuild/mozbuild/test/backend/data/localized-pp-files/moz.build @@ -0,0 +1,8 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# Any copyright is dedicated to the Public Domain. +# http://creativecommons.org/publicdomain/zero/1.0/ + +LOCALIZED_PP_FILES += [ + "en-US/bar.ini", + "en-US/foo.js", +] diff --git a/python/mozbuild/mozbuild/test/backend/data/prog-lib-c-only/c-library/c-library.c b/python/mozbuild/mozbuild/test/backend/data/prog-lib-c-only/c-library/c-library.c new file mode 100644 index 0000000000..3b09e769db --- /dev/null +++ b/python/mozbuild/mozbuild/test/backend/data/prog-lib-c-only/c-library/c-library.c @@ -0,0 +1,2 @@ +// Any copyright is dedicated to the Public Domain. +// http://creativecommons.org/publicdomain/zero/1.0/ diff --git a/python/mozbuild/mozbuild/test/backend/data/prog-lib-c-only/c-library/moz.build b/python/mozbuild/mozbuild/test/backend/data/prog-lib-c-only/c-library/moz.build new file mode 100644 index 0000000000..8e15d10c43 --- /dev/null +++ b/python/mozbuild/mozbuild/test/backend/data/prog-lib-c-only/c-library/moz.build @@ -0,0 +1,7 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# Any copyright is dedicated to the Public Domain. +# http://creativecommons.org/publicdomain/zero/1.0/ + +SharedLibrary("c_library") + +SOURCES = ["c-library.c"] diff --git a/python/mozbuild/mozbuild/test/backend/data/prog-lib-c-only/c-program/c_test_program.c b/python/mozbuild/mozbuild/test/backend/data/prog-lib-c-only/c-program/c_test_program.c new file mode 100644 index 0000000000..3b09e769db --- /dev/null +++ b/python/mozbuild/mozbuild/test/backend/data/prog-lib-c-only/c-program/c_test_program.c @@ -0,0 +1,2 @@ +// Any copyright is dedicated to the Public Domain. +// http://creativecommons.org/publicdomain/zero/1.0/ diff --git a/python/mozbuild/mozbuild/test/backend/data/prog-lib-c-only/c-program/moz.build b/python/mozbuild/mozbuild/test/backend/data/prog-lib-c-only/c-program/moz.build new file mode 100644 index 0000000000..27f2cd3d5d --- /dev/null +++ b/python/mozbuild/mozbuild/test/backend/data/prog-lib-c-only/c-program/moz.build @@ -0,0 +1,7 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# Any copyright is dedicated to the Public Domain. +# http://creativecommons.org/publicdomain/zero/1.0/ + +Program("c_test_program") + +SOURCES = ["c_test_program.c"] diff --git a/python/mozbuild/mozbuild/test/backend/data/prog-lib-c-only/c-simple-programs/c_simple_program.c b/python/mozbuild/mozbuild/test/backend/data/prog-lib-c-only/c-simple-programs/c_simple_program.c new file mode 100644 index 0000000000..3b09e769db --- /dev/null +++ b/python/mozbuild/mozbuild/test/backend/data/prog-lib-c-only/c-simple-programs/c_simple_program.c @@ -0,0 +1,2 @@ +// Any copyright is dedicated to the Public Domain. +// http://creativecommons.org/publicdomain/zero/1.0/ diff --git a/python/mozbuild/mozbuild/test/backend/data/prog-lib-c-only/c-simple-programs/moz.build b/python/mozbuild/mozbuild/test/backend/data/prog-lib-c-only/c-simple-programs/moz.build new file mode 100644 index 0000000000..db958d1d1f --- /dev/null +++ b/python/mozbuild/mozbuild/test/backend/data/prog-lib-c-only/c-simple-programs/moz.build @@ -0,0 +1,5 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# Any copyright is dedicated to the Public Domain. +# http://creativecommons.org/publicdomain/zero/1.0/ + +SimplePrograms(["c_simple_program"], ext=".c") diff --git a/python/mozbuild/mozbuild/test/backend/data/prog-lib-c-only/cxx-library/c-source.c b/python/mozbuild/mozbuild/test/backend/data/prog-lib-c-only/cxx-library/c-source.c new file mode 100644 index 0000000000..3b09e769db --- /dev/null +++ b/python/mozbuild/mozbuild/test/backend/data/prog-lib-c-only/cxx-library/c-source.c @@ -0,0 +1,2 @@ +// Any copyright is dedicated to the Public Domain. +// http://creativecommons.org/publicdomain/zero/1.0/ diff --git a/python/mozbuild/mozbuild/test/backend/data/prog-lib-c-only/cxx-library/cxx-library.cpp b/python/mozbuild/mozbuild/test/backend/data/prog-lib-c-only/cxx-library/cxx-library.cpp new file mode 100644 index 0000000000..3b09e769db --- /dev/null +++ b/python/mozbuild/mozbuild/test/backend/data/prog-lib-c-only/cxx-library/cxx-library.cpp @@ -0,0 +1,2 @@ +// Any copyright is dedicated to the Public Domain. +// http://creativecommons.org/publicdomain/zero/1.0/ diff --git a/python/mozbuild/mozbuild/test/backend/data/prog-lib-c-only/cxx-library/moz.build b/python/mozbuild/mozbuild/test/backend/data/prog-lib-c-only/cxx-library/moz.build new file mode 100644 index 0000000000..ee75ad0cb9 --- /dev/null +++ b/python/mozbuild/mozbuild/test/backend/data/prog-lib-c-only/cxx-library/moz.build @@ -0,0 +1,10 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# Any copyright is dedicated to the Public Domain. +# http://creativecommons.org/publicdomain/zero/1.0/ + +SharedLibrary("cxx-library") + +SOURCES = [ + "c-source.c", + "cxx-library.cpp", +] diff --git a/python/mozbuild/mozbuild/test/backend/data/prog-lib-c-only/cxx-program/cxx_test_program.cpp b/python/mozbuild/mozbuild/test/backend/data/prog-lib-c-only/cxx-program/cxx_test_program.cpp new file mode 100644 index 0000000000..3b09e769db --- /dev/null +++ b/python/mozbuild/mozbuild/test/backend/data/prog-lib-c-only/cxx-program/cxx_test_program.cpp @@ -0,0 +1,2 @@ +// Any copyright is dedicated to the Public Domain. +// http://creativecommons.org/publicdomain/zero/1.0/ diff --git a/python/mozbuild/mozbuild/test/backend/data/prog-lib-c-only/cxx-program/moz.build b/python/mozbuild/mozbuild/test/backend/data/prog-lib-c-only/cxx-program/moz.build new file mode 100644 index 0000000000..175f18c88a --- /dev/null +++ b/python/mozbuild/mozbuild/test/backend/data/prog-lib-c-only/cxx-program/moz.build @@ -0,0 +1,7 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# Any copyright is dedicated to the Public Domain. +# http://creativecommons.org/publicdomain/zero/1.0/ + +Program("cxx_test_program") + +SOURCES = ["cxx_test_program.cpp"] diff --git a/python/mozbuild/mozbuild/test/backend/data/prog-lib-c-only/cxx-simple-programs/cxx_simple_program.cpp b/python/mozbuild/mozbuild/test/backend/data/prog-lib-c-only/cxx-simple-programs/cxx_simple_program.cpp new file mode 100644 index 0000000000..3b09e769db --- /dev/null +++ b/python/mozbuild/mozbuild/test/backend/data/prog-lib-c-only/cxx-simple-programs/cxx_simple_program.cpp @@ -0,0 +1,2 @@ +// Any copyright is dedicated to the Public Domain. +// http://creativecommons.org/publicdomain/zero/1.0/ diff --git a/python/mozbuild/mozbuild/test/backend/data/prog-lib-c-only/cxx-simple-programs/moz.build b/python/mozbuild/mozbuild/test/backend/data/prog-lib-c-only/cxx-simple-programs/moz.build new file mode 100644 index 0000000000..e055370900 --- /dev/null +++ b/python/mozbuild/mozbuild/test/backend/data/prog-lib-c-only/cxx-simple-programs/moz.build @@ -0,0 +1,5 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# Any copyright is dedicated to the Public Domain. +# http://creativecommons.org/publicdomain/zero/1.0/ + +SimplePrograms(["cxx_simple_program"]) diff --git a/python/mozbuild/mozbuild/test/backend/data/prog-lib-c-only/moz.build b/python/mozbuild/mozbuild/test/backend/data/prog-lib-c-only/moz.build new file mode 100644 index 0000000000..7f0a6b430b --- /dev/null +++ b/python/mozbuild/mozbuild/test/backend/data/prog-lib-c-only/moz.build @@ -0,0 +1,35 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# Any copyright is dedicated to the Public Domain. +# http://creativecommons.org/publicdomain/zero/1.0/ + +DIRS += [ + "c-program", + "cxx-program", + "c-simple-programs", + "cxx-simple-programs", + "c-library", + "cxx-library", +] + + +@template +def Program(name): + PROGRAM = name + + +@template +def SimplePrograms(names, ext=".cpp"): + SIMPLE_PROGRAMS += names + SOURCES += ["%s%s" % (name, ext) for name in names] + + +@template +def Library(name): + LIBRARY_NAME = name + + +@template +def SharedLibrary(name): + Library(name) + + FORCE_SHARED_LIB = True diff --git a/python/mozbuild/mozbuild/test/backend/data/prog-lib-c-only/simple-programs/moz.build b/python/mozbuild/mozbuild/test/backend/data/prog-lib-c-only/simple-programs/moz.build new file mode 100644 index 0000000000..62966a58e1 --- /dev/null +++ b/python/mozbuild/mozbuild/test/backend/data/prog-lib-c-only/simple-programs/moz.build @@ -0,0 +1,3 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# Any copyright is dedicated to the Public Domain. +# http://creativecommons.org/publicdomain/zero/1.0/ diff --git a/python/mozbuild/mozbuild/test/backend/data/program-paths/dist-bin/moz.build b/python/mozbuild/mozbuild/test/backend/data/program-paths/dist-bin/moz.build new file mode 100644 index 0000000000..d8b952c014 --- /dev/null +++ b/python/mozbuild/mozbuild/test/backend/data/program-paths/dist-bin/moz.build @@ -0,0 +1,4 @@ +# Any copyright is dedicated to the Public Domain. +# http://creativecommons.org/publicdomain/zero/1.0/ + +Program("dist-bin") diff --git a/python/mozbuild/mozbuild/test/backend/data/program-paths/dist-subdir/moz.build b/python/mozbuild/mozbuild/test/backend/data/program-paths/dist-subdir/moz.build new file mode 100644 index 0000000000..fc2f664c01 --- /dev/null +++ b/python/mozbuild/mozbuild/test/backend/data/program-paths/dist-subdir/moz.build @@ -0,0 +1,5 @@ +# Any copyright is dedicated to the Public Domain. +# http://creativecommons.org/publicdomain/zero/1.0/ + +DIST_SUBDIR = "foo" +Program("dist-subdir") diff --git a/python/mozbuild/mozbuild/test/backend/data/program-paths/final-target/moz.build b/python/mozbuild/mozbuild/test/backend/data/program-paths/final-target/moz.build new file mode 100644 index 0000000000..a0d5805262 --- /dev/null +++ b/python/mozbuild/mozbuild/test/backend/data/program-paths/final-target/moz.build @@ -0,0 +1,5 @@ +# Any copyright is dedicated to the Public Domain. +# http://creativecommons.org/publicdomain/zero/1.0/ + +FINAL_TARGET = "final/target" +Program("final-target") diff --git a/python/mozbuild/mozbuild/test/backend/data/program-paths/moz.build b/python/mozbuild/mozbuild/test/backend/data/program-paths/moz.build new file mode 100644 index 0000000000..d1d087fd45 --- /dev/null +++ b/python/mozbuild/mozbuild/test/backend/data/program-paths/moz.build @@ -0,0 +1,15 @@ +# Any copyright is dedicated to the Public Domain. +# http://creativecommons.org/publicdomain/zero/1.0/ + + +@template +def Program(name): + PROGRAM = name + + +DIRS += [ + "dist-bin", + "dist-subdir", + "final-target", + "not-installed", +] diff --git a/python/mozbuild/mozbuild/test/backend/data/program-paths/not-installed/moz.build b/python/mozbuild/mozbuild/test/backend/data/program-paths/not-installed/moz.build new file mode 100644 index 0000000000..c725ab7326 --- /dev/null +++ b/python/mozbuild/mozbuild/test/backend/data/program-paths/not-installed/moz.build @@ -0,0 +1,5 @@ +# Any copyright is dedicated to the Public Domain. +# http://creativecommons.org/publicdomain/zero/1.0/ + +DIST_INSTALL = False +Program("not-installed") diff --git a/python/mozbuild/mozbuild/test/backend/data/resources/bar.res.in b/python/mozbuild/mozbuild/test/backend/data/resources/bar.res.in new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/python/mozbuild/mozbuild/test/backend/data/resources/bar.res.in diff --git a/python/mozbuild/mozbuild/test/backend/data/resources/cursor.cur b/python/mozbuild/mozbuild/test/backend/data/resources/cursor.cur new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/python/mozbuild/mozbuild/test/backend/data/resources/cursor.cur diff --git a/python/mozbuild/mozbuild/test/backend/data/resources/desktop1.ttf b/python/mozbuild/mozbuild/test/backend/data/resources/desktop1.ttf new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/python/mozbuild/mozbuild/test/backend/data/resources/desktop1.ttf diff --git a/python/mozbuild/mozbuild/test/backend/data/resources/desktop2.ttf b/python/mozbuild/mozbuild/test/backend/data/resources/desktop2.ttf new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/python/mozbuild/mozbuild/test/backend/data/resources/desktop2.ttf diff --git a/python/mozbuild/mozbuild/test/backend/data/resources/extra.manifest b/python/mozbuild/mozbuild/test/backend/data/resources/extra.manifest new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/python/mozbuild/mozbuild/test/backend/data/resources/extra.manifest diff --git a/python/mozbuild/mozbuild/test/backend/data/resources/font1.ttf b/python/mozbuild/mozbuild/test/backend/data/resources/font1.ttf new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/python/mozbuild/mozbuild/test/backend/data/resources/font1.ttf diff --git a/python/mozbuild/mozbuild/test/backend/data/resources/font2.ttf b/python/mozbuild/mozbuild/test/backend/data/resources/font2.ttf new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/python/mozbuild/mozbuild/test/backend/data/resources/font2.ttf diff --git a/python/mozbuild/mozbuild/test/backend/data/resources/foo.res b/python/mozbuild/mozbuild/test/backend/data/resources/foo.res new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/python/mozbuild/mozbuild/test/backend/data/resources/foo.res diff --git a/python/mozbuild/mozbuild/test/backend/data/resources/mobile.ttf b/python/mozbuild/mozbuild/test/backend/data/resources/mobile.ttf new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/python/mozbuild/mozbuild/test/backend/data/resources/mobile.ttf diff --git a/python/mozbuild/mozbuild/test/backend/data/resources/moz.build b/python/mozbuild/mozbuild/test/backend/data/resources/moz.build new file mode 100644 index 0000000000..619af26e64 --- /dev/null +++ b/python/mozbuild/mozbuild/test/backend/data/resources/moz.build @@ -0,0 +1,9 @@ +# Any copyright is dedicated to the Public Domain. +# http://creativecommons.org/publicdomain/zero/1.0/ + +RESOURCE_FILES += ["bar.res.in", "foo.res"] +RESOURCE_FILES.cursors += ["cursor.cur"] +RESOURCE_FILES.fonts += ["font1.ttf", "font2.ttf"] +RESOURCE_FILES.fonts.desktop += ["desktop1.ttf", "desktop2.ttf"] +RESOURCE_FILES.fonts.mobile += ["mobile.ttf"] +RESOURCE_FILES.tests += ["extra.manifest", "test.manifest"] diff --git a/python/mozbuild/mozbuild/test/backend/data/resources/test.manifest b/python/mozbuild/mozbuild/test/backend/data/resources/test.manifest new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/python/mozbuild/mozbuild/test/backend/data/resources/test.manifest diff --git a/python/mozbuild/mozbuild/test/backend/data/rust-library-features/Cargo.toml b/python/mozbuild/mozbuild/test/backend/data/rust-library-features/Cargo.toml new file mode 100644 index 0000000000..10c23d4f5d --- /dev/null +++ b/python/mozbuild/mozbuild/test/backend/data/rust-library-features/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "feature-library" +version = "0.1.0" +authors = [ + "The Mozilla Project Developers", +] + +[lib] +crate-type = ["staticlib"] + +[profile.dev] +panic = "abort" + +[profile.release] +panic = "abort" + +[dependencies] +mozilla-central-workspace-hack = { version = "0.1", features = ["feature-library"], optional = true } diff --git a/python/mozbuild/mozbuild/test/backend/data/rust-library-features/moz.build b/python/mozbuild/mozbuild/test/backend/data/rust-library-features/moz.build new file mode 100644 index 0000000000..f17f29b0e7 --- /dev/null +++ b/python/mozbuild/mozbuild/test/backend/data/rust-library-features/moz.build @@ -0,0 +1,20 @@ +# Any copyright is dedicated to the Public Domain. +# http://creativecommons.org/publicdomain/zero/1.0/ + + +@template +def Library(name): + """Template for libraries.""" + LIBRARY_NAME = name + + +@template +def RustLibrary(name, features): + """Template for Rust libraries.""" + Library(name) + + IS_RUST_LIBRARY = True + RUST_LIBRARY_FEATURES = features + + +RustLibrary("feature-library", ["musthave", "cantlivewithout"]) diff --git a/python/mozbuild/mozbuild/test/backend/data/rust-library/Cargo.toml b/python/mozbuild/mozbuild/test/backend/data/rust-library/Cargo.toml new file mode 100644 index 0000000000..5cca7c1e58 --- /dev/null +++ b/python/mozbuild/mozbuild/test/backend/data/rust-library/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "test-library" +version = "0.1.0" +authors = [ + "The Mozilla Project Developers", +] + +[lib] +crate-type = ["staticlib"] + +[profile.dev] +panic = "abort" + +[profile.release] +panic = "abort" + +[dependencies] +mozilla-central-workspace-hack = { version = "0.1", features = ["test-library"], optional = true } diff --git a/python/mozbuild/mozbuild/test/backend/data/rust-library/moz.build b/python/mozbuild/mozbuild/test/backend/data/rust-library/moz.build new file mode 100644 index 0000000000..b0f29a1ef5 --- /dev/null +++ b/python/mozbuild/mozbuild/test/backend/data/rust-library/moz.build @@ -0,0 +1,19 @@ +# Any copyright is dedicated to the Public Domain. +# http://creativecommons.org/publicdomain/zero/1.0/ + + +@template +def Library(name): + """Template for libraries.""" + LIBRARY_NAME = name + + +@template +def RustLibrary(name): + """Template for Rust libraries.""" + Library(name) + + IS_RUST_LIBRARY = True + + +RustLibrary("test-library") diff --git a/python/mozbuild/mozbuild/test/backend/data/rust-programs/code/Cargo.toml b/python/mozbuild/mozbuild/test/backend/data/rust-programs/code/Cargo.toml new file mode 100644 index 0000000000..c741c668a4 --- /dev/null +++ b/python/mozbuild/mozbuild/test/backend/data/rust-programs/code/Cargo.toml @@ -0,0 +1,13 @@ +[package] +authors = ["The Mozilla Project Developers"] +name = "testing" +version = "0.0.1" + +[[bin]] +name = "target" + +[[bin]] +name = "host" + +[dependencies] +mozilla-central-workspace-hack = { version = "0.1", features = ["testing"], optional = true } diff --git a/python/mozbuild/mozbuild/test/backend/data/rust-programs/code/moz.build b/python/mozbuild/mozbuild/test/backend/data/rust-programs/code/moz.build new file mode 100644 index 0000000000..f0efdb3799 --- /dev/null +++ b/python/mozbuild/mozbuild/test/backend/data/rust-programs/code/moz.build @@ -0,0 +1,6 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# Any copyright is dedicated to the Public Domain. +# http://creativecommons.org/publicdomain/zero/1.0/ + +RUST_PROGRAMS += ["target"] +HOST_RUST_PROGRAMS += ["host"] diff --git a/python/mozbuild/mozbuild/test/backend/data/rust-programs/moz.build b/python/mozbuild/mozbuild/test/backend/data/rust-programs/moz.build new file mode 100644 index 0000000000..cb635f6adb --- /dev/null +++ b/python/mozbuild/mozbuild/test/backend/data/rust-programs/moz.build @@ -0,0 +1,5 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# Any copyright is dedicated to the Public Domain. +# http://creativecommons.org/publicdomain/zero/1.0/ + +DIRS += ["code"] diff --git a/python/mozbuild/mozbuild/test/backend/data/sources/bar.cpp b/python/mozbuild/mozbuild/test/backend/data/sources/bar.cpp new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/python/mozbuild/mozbuild/test/backend/data/sources/bar.cpp diff --git a/python/mozbuild/mozbuild/test/backend/data/sources/bar.s b/python/mozbuild/mozbuild/test/backend/data/sources/bar.s new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/python/mozbuild/mozbuild/test/backend/data/sources/bar.s diff --git a/python/mozbuild/mozbuild/test/backend/data/sources/baz.c b/python/mozbuild/mozbuild/test/backend/data/sources/baz.c new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/python/mozbuild/mozbuild/test/backend/data/sources/baz.c diff --git a/python/mozbuild/mozbuild/test/backend/data/sources/foo.asm b/python/mozbuild/mozbuild/test/backend/data/sources/foo.asm new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/python/mozbuild/mozbuild/test/backend/data/sources/foo.asm diff --git a/python/mozbuild/mozbuild/test/backend/data/sources/foo.cpp b/python/mozbuild/mozbuild/test/backend/data/sources/foo.cpp new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/python/mozbuild/mozbuild/test/backend/data/sources/foo.cpp diff --git a/python/mozbuild/mozbuild/test/backend/data/sources/fuga.mm b/python/mozbuild/mozbuild/test/backend/data/sources/fuga.mm new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/python/mozbuild/mozbuild/test/backend/data/sources/fuga.mm diff --git a/python/mozbuild/mozbuild/test/backend/data/sources/hoge.mm b/python/mozbuild/mozbuild/test/backend/data/sources/hoge.mm new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/python/mozbuild/mozbuild/test/backend/data/sources/hoge.mm diff --git a/python/mozbuild/mozbuild/test/backend/data/sources/moz.build b/python/mozbuild/mozbuild/test/backend/data/sources/moz.build new file mode 100644 index 0000000000..40d5a8d38d --- /dev/null +++ b/python/mozbuild/mozbuild/test/backend/data/sources/moz.build @@ -0,0 +1,26 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# Any copyright is dedicated to the Public Domain. +# http://creativecommons.org/publicdomain/zero/1.0/ + + +@template +def Library(name): + """Template for libraries.""" + LIBRARY_NAME = name + + +Library("dummy") + +SOURCES += ["bar.s", "foo.asm"] + +HOST_SOURCES += ["bar.cpp", "foo.cpp"] +HOST_SOURCES += ["baz.c", "qux.c"] + +SOURCES += ["baz.c", "qux.c"] + +SOURCES += ["fuga.mm", "hoge.mm"] + +SOURCES += ["titi.S", "toto.S"] + +WASM_SOURCES += ["bar.cpp"] +WASM_SOURCES += ["baz.c"] diff --git a/python/mozbuild/mozbuild/test/backend/data/sources/qux.c b/python/mozbuild/mozbuild/test/backend/data/sources/qux.c new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/python/mozbuild/mozbuild/test/backend/data/sources/qux.c diff --git a/python/mozbuild/mozbuild/test/backend/data/sources/titi.S b/python/mozbuild/mozbuild/test/backend/data/sources/titi.S new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/python/mozbuild/mozbuild/test/backend/data/sources/titi.S diff --git a/python/mozbuild/mozbuild/test/backend/data/sources/toto.S b/python/mozbuild/mozbuild/test/backend/data/sources/toto.S new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/python/mozbuild/mozbuild/test/backend/data/sources/toto.S diff --git a/python/mozbuild/mozbuild/test/backend/data/stub0/Makefile.in b/python/mozbuild/mozbuild/test/backend/data/stub0/Makefile.in new file mode 100644 index 0000000000..02ff0a3f90 --- /dev/null +++ b/python/mozbuild/mozbuild/test/backend/data/stub0/Makefile.in @@ -0,0 +1,4 @@ +# Any copyright is dedicated to the Public Domain. +# http://creativecommons.org/publicdomain/zero/1.0/ + +FOO := foo diff --git a/python/mozbuild/mozbuild/test/backend/data/stub0/dir1/Makefile.in b/python/mozbuild/mozbuild/test/backend/data/stub0/dir1/Makefile.in new file mode 100644 index 0000000000..17c147d97a --- /dev/null +++ b/python/mozbuild/mozbuild/test/backend/data/stub0/dir1/Makefile.in @@ -0,0 +1,7 @@ +# Any copyright is dedicated to the Public Domain. +# http://creativecommons.org/publicdomain/zero/1.0/ + +include $(DEPTH)/config/autoconf.mk + +include $(topsrcdir)/config/rules.mk + diff --git a/python/mozbuild/mozbuild/test/backend/data/stub0/dir1/moz.build b/python/mozbuild/mozbuild/test/backend/data/stub0/dir1/moz.build new file mode 100644 index 0000000000..62966a58e1 --- /dev/null +++ b/python/mozbuild/mozbuild/test/backend/data/stub0/dir1/moz.build @@ -0,0 +1,3 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# Any copyright is dedicated to the Public Domain. +# http://creativecommons.org/publicdomain/zero/1.0/ diff --git a/python/mozbuild/mozbuild/test/backend/data/stub0/dir2/moz.build b/python/mozbuild/mozbuild/test/backend/data/stub0/dir2/moz.build new file mode 100644 index 0000000000..62966a58e1 --- /dev/null +++ b/python/mozbuild/mozbuild/test/backend/data/stub0/dir2/moz.build @@ -0,0 +1,3 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# Any copyright is dedicated to the Public Domain. +# http://creativecommons.org/publicdomain/zero/1.0/ diff --git a/python/mozbuild/mozbuild/test/backend/data/stub0/dir3/Makefile.in b/python/mozbuild/mozbuild/test/backend/data/stub0/dir3/Makefile.in new file mode 100644 index 0000000000..17c147d97a --- /dev/null +++ b/python/mozbuild/mozbuild/test/backend/data/stub0/dir3/Makefile.in @@ -0,0 +1,7 @@ +# Any copyright is dedicated to the Public Domain. +# http://creativecommons.org/publicdomain/zero/1.0/ + +include $(DEPTH)/config/autoconf.mk + +include $(topsrcdir)/config/rules.mk + diff --git a/python/mozbuild/mozbuild/test/backend/data/stub0/dir3/moz.build b/python/mozbuild/mozbuild/test/backend/data/stub0/dir3/moz.build new file mode 100644 index 0000000000..62966a58e1 --- /dev/null +++ b/python/mozbuild/mozbuild/test/backend/data/stub0/dir3/moz.build @@ -0,0 +1,3 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# Any copyright is dedicated to the Public Domain. +# http://creativecommons.org/publicdomain/zero/1.0/ diff --git a/python/mozbuild/mozbuild/test/backend/data/stub0/moz.build b/python/mozbuild/mozbuild/test/backend/data/stub0/moz.build new file mode 100644 index 0000000000..4f6e7cb318 --- /dev/null +++ b/python/mozbuild/mozbuild/test/backend/data/stub0/moz.build @@ -0,0 +1,7 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# Any copyright is dedicated to the Public Domain. +# http://creativecommons.org/publicdomain/zero/1.0/ + +DIRS += ["dir1"] +DIRS += ["dir2"] +TEST_DIRS += ["dir3"] diff --git a/python/mozbuild/mozbuild/test/backend/data/substitute_config_files/Makefile.in b/python/mozbuild/mozbuild/test/backend/data/substitute_config_files/Makefile.in new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/python/mozbuild/mozbuild/test/backend/data/substitute_config_files/Makefile.in diff --git a/python/mozbuild/mozbuild/test/backend/data/substitute_config_files/foo.in b/python/mozbuild/mozbuild/test/backend/data/substitute_config_files/foo.in new file mode 100644 index 0000000000..5331f1f051 --- /dev/null +++ b/python/mozbuild/mozbuild/test/backend/data/substitute_config_files/foo.in @@ -0,0 +1 @@ +TEST = @MOZ_FOO@ diff --git a/python/mozbuild/mozbuild/test/backend/data/substitute_config_files/moz.build b/python/mozbuild/mozbuild/test/backend/data/substitute_config_files/moz.build new file mode 100644 index 0000000000..bded13e07d --- /dev/null +++ b/python/mozbuild/mozbuild/test/backend/data/substitute_config_files/moz.build @@ -0,0 +1,5 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# Any copyright is dedicated to the Public Domain. +# http://creativecommons.org/publicdomain/zero/1.0/ + +CONFIGURE_SUBST_FILES = ["foo"] diff --git a/python/mozbuild/mozbuild/test/backend/data/test-manifest-shared-support/child/another-file.sjs b/python/mozbuild/mozbuild/test/backend/data/test-manifest-shared-support/child/another-file.sjs new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/python/mozbuild/mozbuild/test/backend/data/test-manifest-shared-support/child/another-file.sjs diff --git a/python/mozbuild/mozbuild/test/backend/data/test-manifest-shared-support/child/browser.toml b/python/mozbuild/mozbuild/test/backend/data/test-manifest-shared-support/child/browser.toml new file mode 100644 index 0000000000..98d02421df --- /dev/null +++ b/python/mozbuild/mozbuild/test/backend/data/test-manifest-shared-support/child/browser.toml @@ -0,0 +1,7 @@ +[DEFAULT] +support-files = [ + "another-file.sjs", + "data/**" +] + +["test_sub.js"] diff --git a/python/mozbuild/mozbuild/test/backend/data/test-manifest-shared-support/child/data/one.txt b/python/mozbuild/mozbuild/test/backend/data/test-manifest-shared-support/child/data/one.txt new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/python/mozbuild/mozbuild/test/backend/data/test-manifest-shared-support/child/data/one.txt diff --git a/python/mozbuild/mozbuild/test/backend/data/test-manifest-shared-support/child/data/two.txt b/python/mozbuild/mozbuild/test/backend/data/test-manifest-shared-support/child/data/two.txt new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/python/mozbuild/mozbuild/test/backend/data/test-manifest-shared-support/child/data/two.txt diff --git a/python/mozbuild/mozbuild/test/backend/data/test-manifest-shared-support/child/test_sub.js b/python/mozbuild/mozbuild/test/backend/data/test-manifest-shared-support/child/test_sub.js new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/python/mozbuild/mozbuild/test/backend/data/test-manifest-shared-support/child/test_sub.js diff --git a/python/mozbuild/mozbuild/test/backend/data/test-manifest-shared-support/mochitest.toml b/python/mozbuild/mozbuild/test/backend/data/test-manifest-shared-support/mochitest.toml new file mode 100644 index 0000000000..ab9ea26da7 --- /dev/null +++ b/python/mozbuild/mozbuild/test/backend/data/test-manifest-shared-support/mochitest.toml @@ -0,0 +1,9 @@ +[DEFAULT] +support-files = [ + "support-file.txt", + "!/child/test_sub.js", + "!/child/another-file.sjs", + "!/child/data/**", +] + +["test_foo.js"] diff --git a/python/mozbuild/mozbuild/test/backend/data/test-manifest-shared-support/moz.build b/python/mozbuild/mozbuild/test/backend/data/test-manifest-shared-support/moz.build new file mode 100644 index 0000000000..d28a244978 --- /dev/null +++ b/python/mozbuild/mozbuild/test/backend/data/test-manifest-shared-support/moz.build @@ -0,0 +1,5 @@ +# Any copyright is dedicated to the Public Domain. +# http://creativecommons.org/publicdomain/zero/1.0/ + +MOCHITEST_MANIFESTS += ["mochitest.toml"] +BROWSER_CHROME_MANIFESTS += ["child/browser.toml"] diff --git a/python/mozbuild/mozbuild/test/backend/data/test-manifest-shared-support/support-file.txt b/python/mozbuild/mozbuild/test/backend/data/test-manifest-shared-support/support-file.txt new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/python/mozbuild/mozbuild/test/backend/data/test-manifest-shared-support/support-file.txt diff --git a/python/mozbuild/mozbuild/test/backend/data/test-manifest-shared-support/test_foo.js b/python/mozbuild/mozbuild/test/backend/data/test-manifest-shared-support/test_foo.js new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/python/mozbuild/mozbuild/test/backend/data/test-manifest-shared-support/test_foo.js diff --git a/python/mozbuild/mozbuild/test/backend/data/test-manifests-backend-sources/mochitest-common.toml b/python/mozbuild/mozbuild/test/backend/data/test-manifests-backend-sources/mochitest-common.toml new file mode 100644 index 0000000000..5ecf5be39f --- /dev/null +++ b/python/mozbuild/mozbuild/test/backend/data/test-manifests-backend-sources/mochitest-common.toml @@ -0,0 +1,3 @@ +[DEFAULT] + +["test_bar.js"] diff --git a/python/mozbuild/mozbuild/test/backend/data/test-manifests-backend-sources/mochitest.toml b/python/mozbuild/mozbuild/test/backend/data/test-manifests-backend-sources/mochitest.toml new file mode 100644 index 0000000000..f75a9183c6 --- /dev/null +++ b/python/mozbuild/mozbuild/test/backend/data/test-manifests-backend-sources/mochitest.toml @@ -0,0 +1,5 @@ +[DEFAULT] + +["include:mochitest-common.ini"] + +["test_foo.js"] diff --git a/python/mozbuild/mozbuild/test/backend/data/test-manifests-backend-sources/moz.build b/python/mozbuild/mozbuild/test/backend/data/test-manifests-backend-sources/moz.build new file mode 100644 index 0000000000..f9bb774449 --- /dev/null +++ b/python/mozbuild/mozbuild/test/backend/data/test-manifests-backend-sources/moz.build @@ -0,0 +1,6 @@ +# Any copyright is dedicated to the Public Domain. +# http://creativecommons.org/publicdomain/zero/1.0/ + +MOCHITEST_MANIFESTS += [ + "mochitest.toml", +] diff --git a/python/mozbuild/mozbuild/test/backend/data/test-manifests-backend-sources/test_bar.js b/python/mozbuild/mozbuild/test/backend/data/test-manifests-backend-sources/test_bar.js new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/python/mozbuild/mozbuild/test/backend/data/test-manifests-backend-sources/test_bar.js diff --git a/python/mozbuild/mozbuild/test/backend/data/test-manifests-backend-sources/test_foo.js b/python/mozbuild/mozbuild/test/backend/data/test-manifests-backend-sources/test_foo.js new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/python/mozbuild/mozbuild/test/backend/data/test-manifests-backend-sources/test_foo.js diff --git a/python/mozbuild/mozbuild/test/backend/data/test-manifests-duplicate-support-files/mochitest1.toml b/python/mozbuild/mozbuild/test/backend/data/test-manifests-duplicate-support-files/mochitest1.toml new file mode 100644 index 0000000000..d0390b5278 --- /dev/null +++ b/python/mozbuild/mozbuild/test/backend/data/test-manifests-duplicate-support-files/mochitest1.toml @@ -0,0 +1,4 @@ +[DEFAULT] +support-files = ["support-file.txt"] + +["test_foo.js"] diff --git a/python/mozbuild/mozbuild/test/backend/data/test-manifests-duplicate-support-files/mochitest2.toml b/python/mozbuild/mozbuild/test/backend/data/test-manifests-duplicate-support-files/mochitest2.toml new file mode 100644 index 0000000000..5a8d230722 --- /dev/null +++ b/python/mozbuild/mozbuild/test/backend/data/test-manifests-duplicate-support-files/mochitest2.toml @@ -0,0 +1,4 @@ +[DEFAULT] +support-files = ["support-file.txt"] + +["test_bar.js"] diff --git a/python/mozbuild/mozbuild/test/backend/data/test-manifests-duplicate-support-files/moz.build b/python/mozbuild/mozbuild/test/backend/data/test-manifests-duplicate-support-files/moz.build new file mode 100644 index 0000000000..1723f844ca --- /dev/null +++ b/python/mozbuild/mozbuild/test/backend/data/test-manifests-duplicate-support-files/moz.build @@ -0,0 +1,7 @@ +# Any copyright is dedicated to the Public Domain. +# http://creativecommons.org/publicdomain/zero/1.0/ + +MOCHITEST_MANIFESTS += [ + "mochitest1.toml", + "mochitest2.toml", +] diff --git a/python/mozbuild/mozbuild/test/backend/data/test-manifests-duplicate-support-files/test_bar.js b/python/mozbuild/mozbuild/test/backend/data/test-manifests-duplicate-support-files/test_bar.js new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/python/mozbuild/mozbuild/test/backend/data/test-manifests-duplicate-support-files/test_bar.js diff --git a/python/mozbuild/mozbuild/test/backend/data/test-manifests-duplicate-support-files/test_foo.js b/python/mozbuild/mozbuild/test/backend/data/test-manifests-duplicate-support-files/test_foo.js new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/python/mozbuild/mozbuild/test/backend/data/test-manifests-duplicate-support-files/test_foo.js diff --git a/python/mozbuild/mozbuild/test/backend/data/test-manifests-package-tests/instrumentation.toml b/python/mozbuild/mozbuild/test/backend/data/test-manifests-package-tests/instrumentation.toml new file mode 100644 index 0000000000..cdebe72d8c --- /dev/null +++ b/python/mozbuild/mozbuild/test/backend/data/test-manifests-package-tests/instrumentation.toml @@ -0,0 +1,3 @@ +[DEFAULT] + +["not_packaged.java"] diff --git a/python/mozbuild/mozbuild/test/backend/data/test-manifests-package-tests/mochitest.js b/python/mozbuild/mozbuild/test/backend/data/test-manifests-package-tests/mochitest.js new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/python/mozbuild/mozbuild/test/backend/data/test-manifests-package-tests/mochitest.js diff --git a/python/mozbuild/mozbuild/test/backend/data/test-manifests-package-tests/mochitest.toml b/python/mozbuild/mozbuild/test/backend/data/test-manifests-package-tests/mochitest.toml new file mode 100644 index 0000000000..83f0d3f877 --- /dev/null +++ b/python/mozbuild/mozbuild/test/backend/data/test-manifests-package-tests/mochitest.toml @@ -0,0 +1,3 @@ +[DEFAULT] + +["mochitest.js"] diff --git a/python/mozbuild/mozbuild/test/backend/data/test-manifests-package-tests/moz.build b/python/mozbuild/mozbuild/test/backend/data/test-manifests-package-tests/moz.build new file mode 100644 index 0000000000..64b41ad939 --- /dev/null +++ b/python/mozbuild/mozbuild/test/backend/data/test-manifests-package-tests/moz.build @@ -0,0 +1,10 @@ +# Any copyright is dedicated to the Public Domain. +# http://creativecommons.org/publicdomain/zero/1.0/ + +MOCHITEST_MANIFESTS += [ + "mochitest.toml", +] + +ANDROID_INSTRUMENTATION_MANIFESTS += [ + "instrumentation.toml", +] diff --git a/python/mozbuild/mozbuild/test/backend/data/test-manifests-package-tests/not_packaged.java b/python/mozbuild/mozbuild/test/backend/data/test-manifests-package-tests/not_packaged.java new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/python/mozbuild/mozbuild/test/backend/data/test-manifests-package-tests/not_packaged.java diff --git a/python/mozbuild/mozbuild/test/backend/data/test-manifests-written/dir1/test_bar.js b/python/mozbuild/mozbuild/test/backend/data/test-manifests-written/dir1/test_bar.js new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/python/mozbuild/mozbuild/test/backend/data/test-manifests-written/dir1/test_bar.js diff --git a/python/mozbuild/mozbuild/test/backend/data/test-manifests-written/dir1/xpcshell.toml b/python/mozbuild/mozbuild/test/backend/data/test-manifests-written/dir1/xpcshell.toml new file mode 100644 index 0000000000..5ecf5be39f --- /dev/null +++ b/python/mozbuild/mozbuild/test/backend/data/test-manifests-written/dir1/xpcshell.toml @@ -0,0 +1,3 @@ +[DEFAULT] + +["test_bar.js"] diff --git a/python/mozbuild/mozbuild/test/backend/data/test-manifests-written/mochitest.js b/python/mozbuild/mozbuild/test/backend/data/test-manifests-written/mochitest.js new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/python/mozbuild/mozbuild/test/backend/data/test-manifests-written/mochitest.js diff --git a/python/mozbuild/mozbuild/test/backend/data/test-manifests-written/mochitest.toml b/python/mozbuild/mozbuild/test/backend/data/test-manifests-written/mochitest.toml new file mode 100644 index 0000000000..83f0d3f877 --- /dev/null +++ b/python/mozbuild/mozbuild/test/backend/data/test-manifests-written/mochitest.toml @@ -0,0 +1,3 @@ +[DEFAULT] + +["mochitest.js"] diff --git a/python/mozbuild/mozbuild/test/backend/data/test-manifests-written/moz.build b/python/mozbuild/mozbuild/test/backend/data/test-manifests-written/moz.build new file mode 100644 index 0000000000..db63557f94 --- /dev/null +++ b/python/mozbuild/mozbuild/test/backend/data/test-manifests-written/moz.build @@ -0,0 +1,9 @@ +# Any copyright is dedicated to the Public Domain. +# http://creativecommons.org/publicdomain/zero/1.0/ + +XPCSHELL_TESTS_MANIFESTS += [ + "dir1/xpcshell.toml", + "xpcshell.toml", +] + +MOCHITEST_MANIFESTS += ["mochitest.toml"] diff --git a/python/mozbuild/mozbuild/test/backend/data/test-manifests-written/xpcshell.js b/python/mozbuild/mozbuild/test/backend/data/test-manifests-written/xpcshell.js new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/python/mozbuild/mozbuild/test/backend/data/test-manifests-written/xpcshell.js diff --git a/python/mozbuild/mozbuild/test/backend/data/test-manifests-written/xpcshell.toml b/python/mozbuild/mozbuild/test/backend/data/test-manifests-written/xpcshell.toml new file mode 100644 index 0000000000..a1c88a8bad --- /dev/null +++ b/python/mozbuild/mozbuild/test/backend/data/test-manifests-written/xpcshell.toml @@ -0,0 +1,4 @@ +[DEFAULT] +support-files = ["support/**"] + +["xpcshell.js"] diff --git a/python/mozbuild/mozbuild/test/backend/data/test-support-binaries-tracked/moz.build b/python/mozbuild/mozbuild/test/backend/data/test-support-binaries-tracked/moz.build new file mode 100644 index 0000000000..eb83fd1826 --- /dev/null +++ b/python/mozbuild/mozbuild/test/backend/data/test-support-binaries-tracked/moz.build @@ -0,0 +1,5 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# Any copyright is dedicated to the Public Domain. +# http://creativecommons.org/publicdomain/zero/1.0/ + +DIRS += ["test", "src"] diff --git a/python/mozbuild/mozbuild/test/backend/data/test-support-binaries-tracked/src/moz.build b/python/mozbuild/mozbuild/test/backend/data/test-support-binaries-tracked/src/moz.build new file mode 100644 index 0000000000..69cde19c29 --- /dev/null +++ b/python/mozbuild/mozbuild/test/backend/data/test-support-binaries-tracked/src/moz.build @@ -0,0 +1,12 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# Any copyright is dedicated to the Public Domain. +# http://creativecommons.org/publicdomain/zero/1.0/ + + +@template +def Library(name): + """Template for libraries""" + LIBRARY_NAME = name + + +Library("foo") diff --git a/python/mozbuild/mozbuild/test/backend/data/test-support-binaries-tracked/test/moz.build b/python/mozbuild/mozbuild/test/backend/data/test-support-binaries-tracked/test/moz.build new file mode 100644 index 0000000000..a43f4083b3 --- /dev/null +++ b/python/mozbuild/mozbuild/test/backend/data/test-support-binaries-tracked/test/moz.build @@ -0,0 +1,32 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# Any copyright is dedicated to the Public Domain. +# http://creativecommons.org/publicdomain/zero/1.0/ + +FINAL_TARGET = "_tests/xpcshell/tests/mozbuildtest" + + +@template +def Library(name): + """Template for libraries""" + LIBRARY_NAME = name + + +@template +def SimplePrograms(names, ext=".cpp"): + """Template for simple program executables. + + Those have a single source with the same base name as the executable. + """ + SIMPLE_PROGRAMS += names + SOURCES += ["%s%s" % (name, ext) for name in names] + + +@template +def HostLibrary(name): + """Template for build tools libraries.""" + HOST_LIBRARY_NAME = name + + +Library("test-library") +HostLibrary("host-test-library") +SimplePrograms(["test-one", "test-two"]) diff --git a/python/mozbuild/mozbuild/test/backend/data/test-support-binaries-tracked/test/test-one.cpp b/python/mozbuild/mozbuild/test/backend/data/test-support-binaries-tracked/test/test-one.cpp new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/python/mozbuild/mozbuild/test/backend/data/test-support-binaries-tracked/test/test-one.cpp diff --git a/python/mozbuild/mozbuild/test/backend/data/test-support-binaries-tracked/test/test-two.cpp b/python/mozbuild/mozbuild/test/backend/data/test-support-binaries-tracked/test/test-two.cpp new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/python/mozbuild/mozbuild/test/backend/data/test-support-binaries-tracked/test/test-two.cpp diff --git a/python/mozbuild/mozbuild/test/backend/data/test_config/file.in b/python/mozbuild/mozbuild/test/backend/data/test_config/file.in new file mode 100644 index 0000000000..07aa30deb6 --- /dev/null +++ b/python/mozbuild/mozbuild/test/backend/data/test_config/file.in @@ -0,0 +1,3 @@ +#ifdef foo +@foo@ +@bar@ diff --git a/python/mozbuild/mozbuild/test/backend/data/test_config/moz.build b/python/mozbuild/mozbuild/test/backend/data/test_config/moz.build new file mode 100644 index 0000000000..5cf4c78f90 --- /dev/null +++ b/python/mozbuild/mozbuild/test/backend/data/test_config/moz.build @@ -0,0 +1,3 @@ +CONFIGURE_SUBST_FILES = [ + "file", +] diff --git a/python/mozbuild/mozbuild/test/backend/data/variable_passthru/Makefile.in b/python/mozbuild/mozbuild/test/backend/data/variable_passthru/Makefile.in new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/python/mozbuild/mozbuild/test/backend/data/variable_passthru/Makefile.in diff --git a/python/mozbuild/mozbuild/test/backend/data/variable_passthru/baz.def b/python/mozbuild/mozbuild/test/backend/data/variable_passthru/baz.def new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/python/mozbuild/mozbuild/test/backend/data/variable_passthru/baz.def diff --git a/python/mozbuild/mozbuild/test/backend/data/variable_passthru/moz.build b/python/mozbuild/mozbuild/test/backend/data/variable_passthru/moz.build new file mode 100644 index 0000000000..81595d2db3 --- /dev/null +++ b/python/mozbuild/mozbuild/test/backend/data/variable_passthru/moz.build @@ -0,0 +1,11 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# Any copyright is dedicated to the Public Domain. +# http://creativecommons.org/publicdomain/zero/1.0/ + +DELAYLOAD_DLLS = ["foo.dll", "bar.dll"] + +RCFILE = "foo.rc" +RCINCLUDE = "bar.rc" +DEFFILE = "baz.def" + +WIN32_EXE_LDFLAGS += ["-subsystem:console"] diff --git a/python/mozbuild/mozbuild/test/backend/data/variable_passthru/test1.c b/python/mozbuild/mozbuild/test/backend/data/variable_passthru/test1.c new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/python/mozbuild/mozbuild/test/backend/data/variable_passthru/test1.c diff --git a/python/mozbuild/mozbuild/test/backend/data/variable_passthru/test1.cpp b/python/mozbuild/mozbuild/test/backend/data/variable_passthru/test1.cpp new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/python/mozbuild/mozbuild/test/backend/data/variable_passthru/test1.cpp diff --git a/python/mozbuild/mozbuild/test/backend/data/variable_passthru/test1.mm b/python/mozbuild/mozbuild/test/backend/data/variable_passthru/test1.mm new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/python/mozbuild/mozbuild/test/backend/data/variable_passthru/test1.mm diff --git a/python/mozbuild/mozbuild/test/backend/data/variable_passthru/test2.c b/python/mozbuild/mozbuild/test/backend/data/variable_passthru/test2.c new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/python/mozbuild/mozbuild/test/backend/data/variable_passthru/test2.c diff --git a/python/mozbuild/mozbuild/test/backend/data/variable_passthru/test2.cpp b/python/mozbuild/mozbuild/test/backend/data/variable_passthru/test2.cpp new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/python/mozbuild/mozbuild/test/backend/data/variable_passthru/test2.cpp diff --git a/python/mozbuild/mozbuild/test/backend/data/variable_passthru/test2.mm b/python/mozbuild/mozbuild/test/backend/data/variable_passthru/test2.mm new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/python/mozbuild/mozbuild/test/backend/data/variable_passthru/test2.mm diff --git a/python/mozbuild/mozbuild/test/backend/data/visual-studio/dir1/bar.cpp b/python/mozbuild/mozbuild/test/backend/data/visual-studio/dir1/bar.cpp new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/python/mozbuild/mozbuild/test/backend/data/visual-studio/dir1/bar.cpp diff --git a/python/mozbuild/mozbuild/test/backend/data/visual-studio/dir1/foo.cpp b/python/mozbuild/mozbuild/test/backend/data/visual-studio/dir1/foo.cpp new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/python/mozbuild/mozbuild/test/backend/data/visual-studio/dir1/foo.cpp diff --git a/python/mozbuild/mozbuild/test/backend/data/visual-studio/dir1/moz.build b/python/mozbuild/mozbuild/test/backend/data/visual-studio/dir1/moz.build new file mode 100644 index 0000000000..ae1fc0c370 --- /dev/null +++ b/python/mozbuild/mozbuild/test/backend/data/visual-studio/dir1/moz.build @@ -0,0 +1,9 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# Any copyright is dedicated to the Public Domain. +# http://creativecommons.org/publicdomain/zero/1.0/ + +FINAL_LIBRARY = "test" +SOURCES += ["bar.cpp", "foo.cpp"] +LOCAL_INCLUDES += ["/includeA/foo"] +DEFINES["DEFINEFOO"] = True +DEFINES["DEFINEBAR"] = "bar" diff --git a/python/mozbuild/mozbuild/test/backend/data/visual-studio/moz.build b/python/mozbuild/mozbuild/test/backend/data/visual-studio/moz.build new file mode 100644 index 0000000000..a0a888fa01 --- /dev/null +++ b/python/mozbuild/mozbuild/test/backend/data/visual-studio/moz.build @@ -0,0 +1,7 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# Any copyright is dedicated to the Public Domain. +# http://creativecommons.org/publicdomain/zero/1.0/ + +DIRS += ["dir1"] + +Library("test") diff --git a/python/mozbuild/mozbuild/test/backend/data/xpidl/bar.idl b/python/mozbuild/mozbuild/test/backend/data/xpidl/bar.idl new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/python/mozbuild/mozbuild/test/backend/data/xpidl/bar.idl diff --git a/python/mozbuild/mozbuild/test/backend/data/xpidl/config/makefiles/xpidl/Makefile.in b/python/mozbuild/mozbuild/test/backend/data/xpidl/config/makefiles/xpidl/Makefile.in new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/python/mozbuild/mozbuild/test/backend/data/xpidl/config/makefiles/xpidl/Makefile.in diff --git a/python/mozbuild/mozbuild/test/backend/data/xpidl/foo.idl b/python/mozbuild/mozbuild/test/backend/data/xpidl/foo.idl new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/python/mozbuild/mozbuild/test/backend/data/xpidl/foo.idl diff --git a/python/mozbuild/mozbuild/test/backend/data/xpidl/moz.build b/python/mozbuild/mozbuild/test/backend/data/xpidl/moz.build new file mode 100644 index 0000000000..df521ac7c5 --- /dev/null +++ b/python/mozbuild/mozbuild/test/backend/data/xpidl/moz.build @@ -0,0 +1,6 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# Any copyright is dedicated to the Public Domain. +# http://creativecommons.org/publicdomain/zero/1.0/ + +XPIDL_MODULE = "my_module" +XPIDL_SOURCES = ["bar.idl", "foo.idl"] diff --git a/python/mozbuild/mozbuild/test/backend/test_build.py b/python/mozbuild/mozbuild/test/backend/test_build.py new file mode 100644 index 0000000000..3287ba5e57 --- /dev/null +++ b/python/mozbuild/mozbuild/test/backend/test_build.py @@ -0,0 +1,265 @@ +# 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/. + +import os +import shutil +import sys +import unittest +from contextlib import contextmanager +from tempfile import mkdtemp + +import buildconfig +import mozpack.path as mozpath +import six +from mozfile import which +from mozpack.files import FileFinder +from mozunit import main + +from mozbuild.backend import get_backend_class +from mozbuild.backend.configenvironment import ConfigEnvironment +from mozbuild.backend.fastermake import FasterMakeBackend +from mozbuild.backend.recursivemake import RecursiveMakeBackend +from mozbuild.base import MozbuildObject +from mozbuild.frontend.emitter import TreeMetadataEmitter +from mozbuild.frontend.reader import BuildReader +from mozbuild.util import ensureParentDir + + +def make_path(): + try: + return buildconfig.substs["GMAKE"] + except KeyError: + fetches_dir = os.environ.get("MOZ_FETCHES_DIR") + extra_search_dirs = () + if fetches_dir: + extra_search_dirs = (os.path.join(fetches_dir, "mozmake"),) + # Fallback for when running the test without an objdir. + for name in ("gmake", "make", "mozmake", "gnumake", "mingw32-make"): + path = which(name, extra_search_dirs=extra_search_dirs) + if path: + return path + + +BASE_SUBSTS = [ + ("PYTHON", mozpath.normsep(sys.executable)), + ("PYTHON3", mozpath.normsep(sys.executable)), + ("MOZ_UI_LOCALE", "en-US"), + ("GMAKE", make_path()), +] + + +class TestBuild(unittest.TestCase): + def setUp(self): + self._old_env = dict(os.environ) + os.environ.pop("MOZCONFIG", None) + os.environ.pop("MOZ_OBJDIR", None) + os.environ.pop("MOZ_PGO", None) + + def tearDown(self): + os.environ.clear() + os.environ.update(self._old_env) + + @contextmanager + def do_test_backend(self, *backends, **kwargs): + # Create the objdir in the srcdir to ensure that they share + # the same drive on Windows. + topobjdir = mkdtemp(dir=buildconfig.topsrcdir) + try: + config = ConfigEnvironment(buildconfig.topsrcdir, topobjdir, **kwargs) + reader = BuildReader(config) + emitter = TreeMetadataEmitter(config) + moz_build = mozpath.join(config.topsrcdir, "test.mozbuild") + definitions = list(emitter.emit(reader.read_mozbuild(moz_build, config))) + for backend in backends: + backend(config).consume(definitions) + + yield config + except Exception: + raise + finally: + if not os.environ.get("MOZ_NO_CLEANUP"): + shutil.rmtree(topobjdir) + + @contextmanager + def line_handler(self): + lines = [] + + def handle_make_line(line): + lines.append(line) + + try: + yield handle_make_line + except Exception: + print("\n".join(lines)) + raise + + if os.environ.get("MOZ_VERBOSE_MAKE"): + print("\n".join(lines)) + + def test_recursive_make(self): + substs = list(BASE_SUBSTS) + with self.do_test_backend(RecursiveMakeBackend, substs=substs) as config: + build = MozbuildObject(config.topsrcdir, None, None, config.topobjdir) + build._config_environment = config + overrides = [ + "install_manifest_depends=", + "MOZ_JAR_MAKER_FILE_FORMAT=flat", + "TEST_MOZBUILD=1", + ] + with self.line_handler() as handle_make_line: + build._run_make( + directory=config.topobjdir, + target=overrides, + silent=False, + line_handler=handle_make_line, + ) + + self.validate(config) + + def test_faster_recursive_make(self): + substs = list(BASE_SUBSTS) + [ + ("BUILD_BACKENDS", "FasterMake+RecursiveMake"), + ] + with self.do_test_backend( + get_backend_class("FasterMake+RecursiveMake"), substs=substs + ) as config: + buildid = mozpath.join(config.topobjdir, "config", "buildid") + ensureParentDir(buildid) + with open(buildid, "w") as fh: + fh.write("20100101012345\n") + + build = MozbuildObject(config.topsrcdir, None, None, config.topobjdir) + build._config_environment = config + overrides = [ + "install_manifest_depends=", + "MOZ_JAR_MAKER_FILE_FORMAT=flat", + "TEST_MOZBUILD=1", + ] + with self.line_handler() as handle_make_line: + build._run_make( + directory=config.topobjdir, + target=overrides, + silent=False, + line_handler=handle_make_line, + ) + + self.validate(config) + + def test_faster_make(self): + substs = list(BASE_SUBSTS) + [ + ("MOZ_BUILD_APP", "dummy_app"), + ("MOZ_WIDGET_TOOLKIT", "dummy_widget"), + ] + with self.do_test_backend( + RecursiveMakeBackend, FasterMakeBackend, substs=substs + ) as config: + buildid = mozpath.join(config.topobjdir, "config", "buildid") + ensureParentDir(buildid) + with open(buildid, "w") as fh: + fh.write("20100101012345\n") + + build = MozbuildObject(config.topsrcdir, None, None, config.topobjdir) + build._config_environment = config + overrides = [ + "TEST_MOZBUILD=1", + ] + with self.line_handler() as handle_make_line: + build._run_make( + directory=mozpath.join(config.topobjdir, "faster"), + target=overrides, + silent=False, + line_handler=handle_make_line, + ) + + self.validate(config) + + def validate(self, config): + self.maxDiff = None + test_path = mozpath.join( + "$SRCDIR", + "python", + "mozbuild", + "mozbuild", + "test", + "backend", + "data", + "build", + ) + + result = { + p: six.ensure_text(f.open().read()) + for p, f in FileFinder(mozpath.join(config.topobjdir, "dist")) + } + self.assertTrue(len(result)) + self.assertEqual( + result, + { + "bin/baz.ini": "baz.ini: FOO is foo\n", + "bin/child/bar.ini": "bar.ini\n", + "bin/child2/foo.css": "foo.css: FOO is foo\n", + "bin/child2/qux.ini": "qux.ini: BAR is not defined\n", + "bin/chrome.manifest": "manifest chrome/foo.manifest\n" + "manifest components/components.manifest\n", + "bin/chrome/foo.manifest": "content bar foo/child/\n" + "content foo foo/\n" + "override chrome://foo/bar.svg#hello " + "chrome://bar/bar.svg#hello\n", + "bin/chrome/foo/bar.js": "bar.js\n", + "bin/chrome/foo/child/baz.jsm": '//@line 2 "%s/baz.jsm"\nbaz.jsm: FOO is foo\n' + % (test_path), + "bin/chrome/foo/child/hoge.js": '//@line 2 "%s/bar.js"\nbar.js: FOO is foo\n' + % (test_path), + "bin/chrome/foo/foo.css": "foo.css: FOO is foo\n", + "bin/chrome/foo/foo.js": "foo.js\n", + "bin/chrome/foo/qux.js": "bar.js\n", + "bin/components/bar.js": '//@line 2 "%s/bar.js"\nbar.js: FOO is foo\n' + % (test_path), + "bin/components/components.manifest": "component {foo} foo.js\ncomponent {bar} bar.js\n", # NOQA: E501 + "bin/components/foo.js": "foo.js\n", + "bin/defaults/pref/prefs.js": "prefs.js\n", + "bin/foo.ini": "foo.ini\n", + "bin/modules/baz.jsm": '//@line 2 "%s/baz.jsm"\nbaz.jsm: FOO is foo\n' + % (test_path), + "bin/modules/child/bar.jsm": "bar.jsm\n", + "bin/modules/child2/qux.jsm": '//@line 4 "%s/qux.jsm"\nqux.jsm: BAR is not defined\n' # NOQA: E501 + % (test_path), + "bin/modules/foo.jsm": "foo.jsm\n", + "bin/res/resource": "resource\n", + "bin/res/child/resource2": "resource2\n", + "bin/app/baz.ini": "baz.ini: FOO is bar\n", + "bin/app/child/bar.ini": "bar.ini\n", + "bin/app/child2/qux.ini": "qux.ini: BAR is defined\n", + "bin/app/chrome.manifest": "manifest chrome/foo.manifest\n" + "manifest components/components.manifest\n", + "bin/app/chrome/foo.manifest": "content bar foo/child/\n" + "content foo foo/\n" + "override chrome://foo/bar.svg#hello " + "chrome://bar/bar.svg#hello\n", + "bin/app/chrome/foo/bar.js": "bar.js\n", + "bin/app/chrome/foo/child/baz.jsm": '//@line 2 "%s/baz.jsm"\nbaz.jsm: FOO is bar\n' + % (test_path), + "bin/app/chrome/foo/child/hoge.js": '//@line 2 "%s/bar.js"\nbar.js: FOO is bar\n' + % (test_path), + "bin/app/chrome/foo/foo.css": "foo.css: FOO is bar\n", + "bin/app/chrome/foo/foo.js": "foo.js\n", + "bin/app/chrome/foo/qux.js": "bar.js\n", + "bin/app/components/bar.js": '//@line 2 "%s/bar.js"\nbar.js: FOO is bar\n' + % (test_path), + "bin/app/components/components.manifest": "component {foo} foo.js\ncomponent {bar} bar.js\n", # NOQA: E501 + "bin/app/components/foo.js": "foo.js\n", + "bin/app/defaults/preferences/prefs.js": "prefs.js\n", + "bin/app/foo.css": "foo.css: FOO is bar\n", + "bin/app/foo.ini": "foo.ini\n", + "bin/app/modules/baz.jsm": '//@line 2 "%s/baz.jsm"\nbaz.jsm: FOO is bar\n' + % (test_path), + "bin/app/modules/child/bar.jsm": "bar.jsm\n", + "bin/app/modules/child2/qux.jsm": '//@line 2 "%s/qux.jsm"\nqux.jsm: BAR is defined\n' # NOQA: E501 + % (test_path), + "bin/app/modules/foo.jsm": "foo.jsm\n", + }, + ) + + +if __name__ == "__main__": + main() diff --git a/python/mozbuild/mozbuild/test/backend/test_configenvironment.py b/python/mozbuild/mozbuild/test/backend/test_configenvironment.py new file mode 100644 index 0000000000..7900cdd737 --- /dev/null +++ b/python/mozbuild/mozbuild/test/backend/test_configenvironment.py @@ -0,0 +1,73 @@ +# 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/. + +import os +import unittest + +import mozpack.path as mozpath +from mozunit import main + +import mozbuild.backend.configenvironment as ConfigStatus +from mozbuild.util import ReadOnlyDict + + +class ConfigEnvironment(ConfigStatus.ConfigEnvironment): + def __init__(self, *args, **kwargs): + ConfigStatus.ConfigEnvironment.__init__(self, *args, **kwargs) + # Be helpful to unit tests + if "top_srcdir" not in self.substs: + if os.path.isabs(self.topsrcdir): + top_srcdir = self.topsrcdir.replace(os.sep, "/") + else: + top_srcdir = mozpath.relpath(self.topsrcdir, self.topobjdir).replace( + os.sep, "/" + ) + + d = dict(self.substs) + d["top_srcdir"] = top_srcdir + self.substs = ReadOnlyDict(d) + + +class TestEnvironment(unittest.TestCase): + def test_auto_substs(self): + """Test the automatically set values of ACDEFINES, ALLSUBSTS + and ALLEMPTYSUBSTS. + """ + env = ConfigEnvironment( + ".", + ".", + defines={"foo": "bar", "baz": "qux 42", "abc": "d'e'f"}, + substs={ + "FOO": "bar", + "FOOBAR": "", + "ABC": "def", + "bar": "baz qux", + "zzz": '"abc def"', + "qux": "", + }, + ) + # Original order of the defines need to be respected in ACDEFINES + self.assertEqual( + env.substs["ACDEFINES"], + """-Dabc='d'\\''e'\\''f' -Dbaz='qux 42' -Dfoo=bar""", + ) + # Likewise for ALLSUBSTS, which also must contain ACDEFINES + self.assertEqual( + env.substs["ALLSUBSTS"], + '''ABC = def +ACDEFINES = -Dabc='d'\\''e'\\''f' -Dbaz='qux 42' -Dfoo=bar +FOO = bar +bar = baz qux +zzz = "abc def"''', + ) + # ALLEMPTYSUBSTS contains all substs with no value. + self.assertEqual( + env.substs["ALLEMPTYSUBSTS"], + """FOOBAR = +qux =""", + ) + + +if __name__ == "__main__": + main() diff --git a/python/mozbuild/mozbuild/test/backend/test_database.py b/python/mozbuild/mozbuild/test/backend/test_database.py new file mode 100644 index 0000000000..3bc0dfefb1 --- /dev/null +++ b/python/mozbuild/mozbuild/test/backend/test_database.py @@ -0,0 +1,91 @@ +# 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/. + +import json +import os + +import six +from mozunit import main + +from mozbuild.backend.clangd import ClangdBackend +from mozbuild.backend.static_analysis import StaticAnalysisBackend +from mozbuild.compilation.database import CompileDBBackend +from mozbuild.test.backend.common import BackendTester + + +class TestCompileDBBackends(BackendTester): + def perform_check(self, compile_commands_path, topsrcdir, topobjdir): + self.assertTrue(os.path.exists(compile_commands_path)) + compile_db = json.loads(open(compile_commands_path, "r").read()) + + # Verify that we have the same number of items + self.assertEqual(len(compile_db), 4) + + expected_db = [ + { + "directory": topobjdir, + "command": "clang -o /dev/null -c -ferror-limit=0 {}/bar.c".format( + topsrcdir + ), + "file": "{}/bar.c".format(topsrcdir), + }, + { + "directory": topobjdir, + "command": "clang -o /dev/null -c -ferror-limit=0 {}/foo.c".format( + topsrcdir + ), + "file": "{}/foo.c".format(topsrcdir), + }, + { + "directory": topobjdir, + "command": "clang++ -o /dev/null -c -ferror-limit=0 {}/baz.cpp".format( + topsrcdir + ), + "file": "{}/baz.cpp".format(topsrcdir), + }, + { + "directory": topobjdir, + "command": "clang++ -o /dev/null -c -ferror-limit=0 {}/qux.cpp".format( + topsrcdir + ), + "file": "{}/qux.cpp".format(topsrcdir), + }, + ] + + # Verify item consistency against `expected_db` + six.assertCountEqual(self, compile_db, expected_db) + + def test_database(self): + """Ensure we can generate a `compile_commands.json` and that is correct.""" + + env = self._consume("database", CompileDBBackend) + compile_commands_path = os.path.join(env.topobjdir, "compile_commands.json") + + self.perform_check(compile_commands_path, env.topsrcdir, env.topobjdir) + + def test_clangd(self): + """Ensure we can generate a `compile_commands.json` and that is correct. + in order to be used by ClandBackend""" + + env = self._consume("database", ClangdBackend) + compile_commands_path = os.path.join( + env.topobjdir, "clangd", "compile_commands.json" + ) + + self.perform_check(compile_commands_path, env.topsrcdir, env.topobjdir) + + def test_static_analysis(self): + """Ensure we can generate a `compile_commands.json` and that is correct. + in order to be used by StaticAnalysisBackend""" + + env = self._consume("database", StaticAnalysisBackend) + compile_commands_path = os.path.join( + env.topobjdir, "static-analysis", "compile_commands.json" + ) + + self.perform_check(compile_commands_path, env.topsrcdir, env.topobjdir) + + +if __name__ == "__main__": + main() diff --git a/python/mozbuild/mozbuild/test/backend/test_fastermake.py b/python/mozbuild/mozbuild/test/backend/test_fastermake.py new file mode 100644 index 0000000000..1c9670b091 --- /dev/null +++ b/python/mozbuild/mozbuild/test/backend/test_fastermake.py @@ -0,0 +1,42 @@ +# 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/. + +import os + +import mozpack.path as mozpath +from mozpack.copier import FileRegistry +from mozpack.manifests import InstallManifest +from mozunit import main + +from mozbuild.backend.fastermake import FasterMakeBackend +from mozbuild.test.backend.common import BackendTester + + +class TestFasterMakeBackend(BackendTester): + def test_basic(self): + """Ensure the FasterMakeBackend works without error.""" + env = self._consume("stub0", FasterMakeBackend) + self.assertTrue( + os.path.exists(mozpath.join(env.topobjdir, "backend.FasterMakeBackend")) + ) + self.assertTrue( + os.path.exists(mozpath.join(env.topobjdir, "backend.FasterMakeBackend.in")) + ) + + def test_final_target_files_wildcard(self): + """Ensure that wildcards in FINAL_TARGET_FILES work properly.""" + env = self._consume("final-target-files-wildcard", FasterMakeBackend) + m = InstallManifest( + path=mozpath.join(env.topobjdir, "faster", "install_dist_bin") + ) + self.assertEqual(len(m), 1) + reg = FileRegistry() + m.populate_registry(reg) + expected = [("foo/bar.xyz", "bar.xyz"), ("foo/foo.xyz", "foo.xyz")] + actual = [(path, mozpath.relpath(f.path, env.topsrcdir)) for (path, f) in reg] + self.assertEqual(expected, actual) + + +if __name__ == "__main__": + main() diff --git a/python/mozbuild/mozbuild/test/backend/test_partialconfigenvironment.py b/python/mozbuild/mozbuild/test/backend/test_partialconfigenvironment.py new file mode 100644 index 0000000000..13b1656981 --- /dev/null +++ b/python/mozbuild/mozbuild/test/backend/test_partialconfigenvironment.py @@ -0,0 +1,173 @@ +# 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/. + +import os +import unittest +from shutil import rmtree +from tempfile import mkdtemp + +import buildconfig +import mozpack.path as mozpath +from mozunit import main + +from mozbuild.backend.configenvironment import PartialConfigEnvironment + +config = { + "defines": { + "MOZ_FOO": "1", + "MOZ_BAR": "2", + }, + "substs": { + "MOZ_SUBST_1": "1", + "MOZ_SUBST_2": "2", + "CPP": "cpp", + }, +} + + +class TestPartial(unittest.TestCase): + def setUp(self): + self._old_env = dict(os.environ) + + def tearDown(self): + os.environ.clear() + os.environ.update(self._old_env) + + def _objdir(self): + objdir = mkdtemp(dir=buildconfig.topsrcdir) + self.addCleanup(rmtree, objdir) + return objdir + + def test_auto_substs(self): + """Test the automatically set values of ACDEFINES, and ALLDEFINES""" + env = PartialConfigEnvironment(self._objdir()) + env.write_vars(config) + self.assertEqual(env.substs["ACDEFINES"], "-DMOZ_BAR=2 -DMOZ_FOO=1") + self.assertEqual( + env.defines["ALLDEFINES"], + { + "MOZ_BAR": "2", + "MOZ_FOO": "1", + }, + ) + + def test_remove_subst(self): + """Test removing a subst from the config. The file should be overwritten with 'None'""" + env = PartialConfigEnvironment(self._objdir()) + path = mozpath.join(env.topobjdir, "config.statusd", "substs", "MYSUBST") + myconfig = config.copy() + env.write_vars(myconfig) + with self.assertRaises(KeyError): + _ = env.substs["MYSUBST"] + self.assertFalse(os.path.exists(path)) + + myconfig["substs"]["MYSUBST"] = "new" + env.write_vars(myconfig) + + self.assertEqual(env.substs["MYSUBST"], "new") + self.assertTrue(os.path.exists(path)) + + del myconfig["substs"]["MYSUBST"] + env.write_vars(myconfig) + with self.assertRaises(KeyError): + _ = env.substs["MYSUBST"] + # Now that the subst is gone, the file still needs to be present so that + # make can update dependencies correctly. Overwriting the file with + # 'None' is the same as deleting it as far as the + # PartialConfigEnvironment is concerned, but make can't track a + # dependency on a file that doesn't exist. + self.assertTrue(os.path.exists(path)) + + def _assert_deps(self, env, deps): + deps = sorted( + [ + "$(wildcard %s)" % (mozpath.join(env.topobjdir, "config.statusd", d)) + for d in deps + ] + ) + self.assertEqual(sorted(env.get_dependencies()), deps) + + def test_dependencies(self): + """Test getting dependencies on defines and substs.""" + env = PartialConfigEnvironment(self._objdir()) + env.write_vars(config) + self._assert_deps(env, []) + + self.assertEqual(env.defines["MOZ_FOO"], "1") + self._assert_deps(env, ["defines/MOZ_FOO"]) + + self.assertEqual(env.defines["MOZ_BAR"], "2") + self._assert_deps(env, ["defines/MOZ_FOO", "defines/MOZ_BAR"]) + + # Getting a define again shouldn't add a redundant dependency + self.assertEqual(env.defines["MOZ_FOO"], "1") + self._assert_deps(env, ["defines/MOZ_FOO", "defines/MOZ_BAR"]) + + self.assertEqual(env.substs["MOZ_SUBST_1"], "1") + self._assert_deps( + env, ["defines/MOZ_FOO", "defines/MOZ_BAR", "substs/MOZ_SUBST_1"] + ) + + with self.assertRaises(KeyError): + _ = env.substs["NON_EXISTENT"] + self._assert_deps( + env, + [ + "defines/MOZ_FOO", + "defines/MOZ_BAR", + "substs/MOZ_SUBST_1", + "substs/NON_EXISTENT", + ], + ) + self.assertEqual(env.substs.get("NON_EXISTENT"), None) + + def test_set_subst(self): + """Test setting a subst""" + env = PartialConfigEnvironment(self._objdir()) + env.write_vars(config) + + self.assertEqual(env.substs["MOZ_SUBST_1"], "1") + env.substs["MOZ_SUBST_1"] = "updated" + self.assertEqual(env.substs["MOZ_SUBST_1"], "updated") + + # A new environment should pull the result from the file again. + newenv = PartialConfigEnvironment(env.topobjdir) + self.assertEqual(newenv.substs["MOZ_SUBST_1"], "1") + + def test_env_override(self): + """Test overriding a subst with an environment variable""" + env = PartialConfigEnvironment(self._objdir()) + env.write_vars(config) + + self.assertEqual(env.substs["MOZ_SUBST_1"], "1") + self.assertEqual(env.substs["CPP"], "cpp") + + # Reset the environment and set some environment variables. + env = PartialConfigEnvironment(env.topobjdir) + os.environ["MOZ_SUBST_1"] = "subst 1 environ" + os.environ["CPP"] = "cpp environ" + + # The MOZ_SUBST_1 should be overridden by the environment, while CPP is + # a special variable and should not. + self.assertEqual(env.substs["MOZ_SUBST_1"], "subst 1 environ") + self.assertEqual(env.substs["CPP"], "cpp") + + def test_update(self): + """Test calling update on the substs or defines pseudo dicts""" + env = PartialConfigEnvironment(self._objdir()) + env.write_vars(config) + + mysubsts = {"NEW": "new"} + mysubsts.update(env.substs.iteritems()) + self.assertEqual(mysubsts["NEW"], "new") + self.assertEqual(mysubsts["CPP"], "cpp") + + mydefines = {"DEBUG": "1"} + mydefines.update(env.defines.iteritems()) + self.assertEqual(mydefines["DEBUG"], "1") + self.assertEqual(mydefines["MOZ_FOO"], "1") + + +if __name__ == "__main__": + main() diff --git a/python/mozbuild/mozbuild/test/backend/test_recursivemake.py b/python/mozbuild/mozbuild/test/backend/test_recursivemake.py new file mode 100644 index 0000000000..2d39308eb3 --- /dev/null +++ b/python/mozbuild/mozbuild/test/backend/test_recursivemake.py @@ -0,0 +1,1307 @@ +# 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/. + +import io +import os +import unittest + +import mozpack.path as mozpath +import six +import six.moves.cPickle as pickle +from mozpack.manifests import InstallManifest +from mozunit import main + +from mozbuild.backend.recursivemake import RecursiveMakeBackend, RecursiveMakeTraversal +from mozbuild.backend.test_manifest import TestManifestBackend +from mozbuild.frontend.emitter import TreeMetadataEmitter +from mozbuild.frontend.reader import BuildReader +from mozbuild.test.backend.common import BackendTester + + +class TestRecursiveMakeTraversal(unittest.TestCase): + def test_traversal(self): + traversal = RecursiveMakeTraversal() + traversal.add("", dirs=["A", "B", "C"]) + traversal.add("", dirs=["D"]) + traversal.add("A") + traversal.add("B", dirs=["E", "F"]) + traversal.add("C", dirs=["G", "H"]) + traversal.add("D", dirs=["I", "K"]) + traversal.add("D", dirs=["J", "L"]) + traversal.add("E") + traversal.add("F") + traversal.add("G") + traversal.add("H") + traversal.add("I", dirs=["M", "N"]) + traversal.add("J", dirs=["O", "P"]) + traversal.add("K", dirs=["Q", "R"]) + traversal.add("L", dirs=["S"]) + traversal.add("M") + traversal.add("N", dirs=["T"]) + traversal.add("O") + traversal.add("P", dirs=["U"]) + traversal.add("Q") + traversal.add("R", dirs=["V"]) + traversal.add("S", dirs=["W"]) + traversal.add("T") + traversal.add("U") + traversal.add("V") + traversal.add("W", dirs=["X"]) + traversal.add("X") + + parallels = set(("G", "H", "I", "J", "O", "P", "Q", "R", "U")) + + def filter(current, subdirs): + return ( + current, + [d for d in subdirs.dirs if d in parallels], + [d for d in subdirs.dirs if d not in parallels], + ) + + start, deps = traversal.compute_dependencies(filter) + self.assertEqual(start, ("X",)) + self.maxDiff = None + self.assertEqual( + deps, + { + "A": ("",), + "B": ("A",), + "C": ("F",), + "D": ("G", "H"), + "E": ("B",), + "F": ("E",), + "G": ("C",), + "H": ("C",), + "I": ("D",), + "J": ("D",), + "K": ("T", "O", "U"), + "L": ("Q", "V"), + "M": ("I",), + "N": ("M",), + "O": ("J",), + "P": ("J",), + "Q": ("K",), + "R": ("K",), + "S": ("L",), + "T": ("N",), + "U": ("P",), + "V": ("R",), + "W": ("S",), + "X": ("W",), + }, + ) + + self.assertEqual( + list(traversal.traverse("", filter)), + [ + "", + "A", + "B", + "E", + "F", + "C", + "G", + "H", + "D", + "I", + "M", + "N", + "T", + "J", + "O", + "P", + "U", + "K", + "Q", + "R", + "V", + "L", + "S", + "W", + "X", + ], + ) + + self.assertEqual(list(traversal.traverse("C", filter)), ["C", "G", "H"]) + + def test_traversal_2(self): + traversal = RecursiveMakeTraversal() + traversal.add("", dirs=["A", "B", "C"]) + traversal.add("A") + traversal.add("B", dirs=["D", "E", "F"]) + traversal.add("C", dirs=["G", "H", "I"]) + traversal.add("D") + traversal.add("E") + traversal.add("F") + traversal.add("G") + traversal.add("H") + traversal.add("I") + + start, deps = traversal.compute_dependencies() + self.assertEqual(start, ("I",)) + self.assertEqual( + deps, + { + "A": ("",), + "B": ("A",), + "C": ("F",), + "D": ("B",), + "E": ("D",), + "F": ("E",), + "G": ("C",), + "H": ("G",), + "I": ("H",), + }, + ) + + def test_traversal_filter(self): + traversal = RecursiveMakeTraversal() + traversal.add("", dirs=["A", "B", "C"]) + traversal.add("A") + traversal.add("B", dirs=["D", "E", "F"]) + traversal.add("C", dirs=["G", "H", "I"]) + traversal.add("D") + traversal.add("E") + traversal.add("F") + traversal.add("G") + traversal.add("H") + traversal.add("I") + + def filter(current, subdirs): + if current == "B": + current = None + return current, [], subdirs.dirs + + start, deps = traversal.compute_dependencies(filter) + self.assertEqual(start, ("I",)) + self.assertEqual( + deps, + { + "A": ("",), + "C": ("F",), + "D": ("A",), + "E": ("D",), + "F": ("E",), + "G": ("C",), + "H": ("G",), + "I": ("H",), + }, + ) + + def test_traversal_parallel(self): + traversal = RecursiveMakeTraversal() + traversal.add("", dirs=["A", "B", "C"]) + traversal.add("A") + traversal.add("B", dirs=["D", "E", "F"]) + traversal.add("C", dirs=["G", "H", "I"]) + traversal.add("D") + traversal.add("E") + traversal.add("F") + traversal.add("G") + traversal.add("H") + traversal.add("I") + traversal.add("J") + + def filter(current, subdirs): + return current, subdirs.dirs, [] + + start, deps = traversal.compute_dependencies(filter) + self.assertEqual(start, ("A", "D", "E", "F", "G", "H", "I", "J")) + self.assertEqual( + deps, + { + "A": ("",), + "B": ("",), + "C": ("",), + "D": ("B",), + "E": ("B",), + "F": ("B",), + "G": ("C",), + "H": ("C",), + "I": ("C",), + "J": ("",), + }, + ) + + +class TestRecursiveMakeBackend(BackendTester): + def test_basic(self): + """Ensure the RecursiveMakeBackend works without error.""" + env = self._consume("stub0", RecursiveMakeBackend) + self.assertTrue( + os.path.exists(mozpath.join(env.topobjdir, "backend.RecursiveMakeBackend")) + ) + self.assertTrue( + os.path.exists( + mozpath.join(env.topobjdir, "backend.RecursiveMakeBackend.in") + ) + ) + + def test_output_files(self): + """Ensure proper files are generated.""" + env = self._consume("stub0", RecursiveMakeBackend) + + expected = ["", "dir1", "dir2"] + + for d in expected: + out_makefile = mozpath.join(env.topobjdir, d, "Makefile") + out_backend = mozpath.join(env.topobjdir, d, "backend.mk") + + self.assertTrue(os.path.exists(out_makefile)) + self.assertTrue(os.path.exists(out_backend)) + + def test_makefile_conversion(self): + """Ensure Makefile.in is converted properly.""" + env = self._consume("stub0", RecursiveMakeBackend) + + p = mozpath.join(env.topobjdir, "Makefile") + + lines = [ + l.strip() for l in open(p, "rt").readlines()[1:] if not l.startswith("#") + ] + self.assertEqual( + lines, + [ + "DEPTH := .", + "topobjdir := %s" % env.topobjdir, + "topsrcdir := %s" % env.topsrcdir, + "srcdir := %s" % env.topsrcdir, + "srcdir_rel := %s" % mozpath.relpath(env.topsrcdir, env.topobjdir), + "relativesrcdir := .", + "include $(DEPTH)/config/autoconf.mk", + "", + "FOO := foo", + "", + "include $(topsrcdir)/config/recurse.mk", + ], + ) + + def test_missing_makefile_in(self): + """Ensure missing Makefile.in results in Makefile creation.""" + env = self._consume("stub0", RecursiveMakeBackend) + + p = mozpath.join(env.topobjdir, "dir2", "Makefile") + self.assertTrue(os.path.exists(p)) + + lines = [l.strip() for l in open(p, "rt").readlines()] + self.assertEqual(len(lines), 10) + + self.assertTrue(lines[0].startswith("# THIS FILE WAS AUTOMATICALLY")) + + def test_backend_mk(self): + """Ensure backend.mk file is written out properly.""" + env = self._consume("stub0", RecursiveMakeBackend) + + p = mozpath.join(env.topobjdir, "backend.mk") + + lines = [l.strip() for l in open(p, "rt").readlines()[2:]] + self.assertEqual(lines, ["DIRS := dir1 dir2"]) + + # Make env.substs writable to add ENABLE_TESTS + env.substs = dict(env.substs) + env.substs["ENABLE_TESTS"] = "1" + self._consume("stub0", RecursiveMakeBackend, env=env) + p = mozpath.join(env.topobjdir, "backend.mk") + + lines = [l.strip() for l in open(p, "rt").readlines()[2:]] + self.assertEqual(lines, ["DIRS := dir1 dir2 dir3"]) + + def test_mtime_no_change(self): + """Ensure mtime is not updated if file content does not change.""" + + env = self._consume("stub0", RecursiveMakeBackend) + + makefile_path = mozpath.join(env.topobjdir, "Makefile") + backend_path = mozpath.join(env.topobjdir, "backend.mk") + makefile_mtime = os.path.getmtime(makefile_path) + backend_mtime = os.path.getmtime(backend_path) + + reader = BuildReader(env) + emitter = TreeMetadataEmitter(env) + backend = RecursiveMakeBackend(env) + backend.consume(emitter.emit(reader.read_topsrcdir())) + + self.assertEqual(os.path.getmtime(makefile_path), makefile_mtime) + self.assertEqual(os.path.getmtime(backend_path), backend_mtime) + + def test_substitute_config_files(self): + """Ensure substituted config files are produced.""" + env = self._consume("substitute_config_files", RecursiveMakeBackend) + + p = mozpath.join(env.topobjdir, "foo") + self.assertTrue(os.path.exists(p)) + lines = [l.strip() for l in open(p, "rt").readlines()] + self.assertEqual(lines, ["TEST = foo"]) + + def test_install_substitute_config_files(self): + """Ensure we recurse into the dirs that install substituted config files.""" + env = self._consume("install_substitute_config_files", RecursiveMakeBackend) + + root_deps_path = mozpath.join(env.topobjdir, "root-deps.mk") + lines = [l.strip() for l in open(root_deps_path, "rt").readlines()] + + # Make sure we actually recurse into the sub directory during export to + # install the subst file. + self.assertTrue(any(l == "recurse_export: sub/export" for l in lines)) + + def test_variable_passthru(self): + """Ensure variable passthru is written out correctly.""" + env = self._consume("variable_passthru", RecursiveMakeBackend) + + backend_path = mozpath.join(env.topobjdir, "backend.mk") + lines = [l.strip() for l in open(backend_path, "rt").readlines()[2:]] + + expected = { + "RCFILE": ["RCFILE := $(srcdir)/foo.rc"], + "RCINCLUDE": ["RCINCLUDE := $(srcdir)/bar.rc"], + "WIN32_EXE_LDFLAGS": ["WIN32_EXE_LDFLAGS += -subsystem:console"], + } + + for var, val in expected.items(): + # print("test_variable_passthru[%s]" % (var)) + found = [str for str in lines if str.startswith(var)] + self.assertEqual(found, val) + + def test_sources(self): + """Ensure SOURCES, HOST_SOURCES and WASM_SOURCES are handled properly.""" + env = self._consume("sources", RecursiveMakeBackend) + + backend_path = mozpath.join(env.topobjdir, "backend.mk") + lines = [l.strip() for l in open(backend_path, "rt").readlines()[2:]] + + expected = { + "ASFILES": ["ASFILES += $(srcdir)/bar.s", "ASFILES += $(srcdir)/foo.asm"], + "CMMSRCS": ["CMMSRCS += $(srcdir)/fuga.mm", "CMMSRCS += $(srcdir)/hoge.mm"], + "CSRCS": ["CSRCS += $(srcdir)/baz.c", "CSRCS += $(srcdir)/qux.c"], + "HOST_CPPSRCS": [ + "HOST_CPPSRCS += $(srcdir)/bar.cpp", + "HOST_CPPSRCS += $(srcdir)/foo.cpp", + ], + "HOST_CSRCS": [ + "HOST_CSRCS += $(srcdir)/baz.c", + "HOST_CSRCS += $(srcdir)/qux.c", + ], + "SSRCS": ["SSRCS += $(srcdir)/titi.S", "SSRCS += $(srcdir)/toto.S"], + "WASM_CSRCS": ["WASM_CSRCS += $(srcdir)/baz.c"], + "WASM_CPPSRCS": ["WASM_CPPSRCS += $(srcdir)/bar.cpp"], + } + + for var, val in expected.items(): + found = [str for str in lines if str.startswith(var)] + self.assertEqual(found, val) + + def test_exports(self): + """Ensure EXPORTS is handled properly.""" + env = self._consume("exports", RecursiveMakeBackend) + + # EXPORTS files should appear in the dist_include install manifest. + m = InstallManifest( + path=mozpath.join( + env.topobjdir, "_build_manifests", "install", "dist_include" + ) + ) + self.assertEqual(len(m), 7) + self.assertIn("foo.h", m) + self.assertIn("mozilla/mozilla1.h", m) + self.assertIn("mozilla/dom/dom2.h", m) + + def test_generated_files(self): + """Ensure GENERATED_FILES is handled properly.""" + env = self._consume("generated-files", RecursiveMakeBackend) + + backend_path = mozpath.join(env.topobjdir, "backend.mk") + lines = [l.strip() for l in open(backend_path, "rt").readlines()[2:]] + + expected = [ + "include $(topsrcdir)/config/AB_rCD.mk", + "PRE_COMPILE_TARGETS += $(MDDEPDIR)/bar.c.stub", + "bar.c: $(MDDEPDIR)/bar.c.stub ;", + "EXTRA_MDDEPEND_FILES += $(MDDEPDIR)/bar.c.pp", + "$(MDDEPDIR)/bar.c.stub: %s/generate-bar.py" % env.topsrcdir, + "$(REPORT_BUILD)", + "$(call py_action,file_generate bar.c,%s/generate-bar.py baz bar.c $(MDDEPDIR)/bar.c.pp $(MDDEPDIR)/bar.c.stub)" # noqa + % env.topsrcdir, + "@$(TOUCH) $@", + "", + "EXPORT_TARGETS += $(MDDEPDIR)/foo.h.stub", + "foo.h: $(MDDEPDIR)/foo.h.stub ;", + "EXTRA_MDDEPEND_FILES += $(MDDEPDIR)/foo.h.pp", + "$(MDDEPDIR)/foo.h.stub: %s/generate-foo.py $(srcdir)/foo-data" + % (env.topsrcdir), + "$(REPORT_BUILD)", + "$(call py_action,file_generate foo.h,%s/generate-foo.py main foo.h $(MDDEPDIR)/foo.h.pp $(MDDEPDIR)/foo.h.stub $(srcdir)/foo-data)" # noqa + % (env.topsrcdir), + "@$(TOUCH) $@", + "", + ] + + self.maxDiff = None + self.assertEqual(lines, expected) + + def test_generated_files_force(self): + """Ensure GENERATED_FILES with .force is handled properly.""" + env = self._consume("generated-files-force", RecursiveMakeBackend) + + backend_path = mozpath.join(env.topobjdir, "backend.mk") + lines = [l.strip() for l in open(backend_path, "rt").readlines()[2:]] + + expected = [ + "include $(topsrcdir)/config/AB_rCD.mk", + "PRE_COMPILE_TARGETS += $(MDDEPDIR)/bar.c.stub", + "bar.c: $(MDDEPDIR)/bar.c.stub ;", + "EXTRA_MDDEPEND_FILES += $(MDDEPDIR)/bar.c.pp", + "$(MDDEPDIR)/bar.c.stub: %s/generate-bar.py FORCE" % env.topsrcdir, + "$(REPORT_BUILD)", + "$(call py_action,file_generate bar.c,%s/generate-bar.py baz bar.c $(MDDEPDIR)/bar.c.pp $(MDDEPDIR)/bar.c.stub)" # noqa + % env.topsrcdir, + "@$(TOUCH) $@", + "", + "PRE_COMPILE_TARGETS += $(MDDEPDIR)/foo.c.stub", + "foo.c: $(MDDEPDIR)/foo.c.stub ;", + "EXTRA_MDDEPEND_FILES += $(MDDEPDIR)/foo.c.pp", + "$(MDDEPDIR)/foo.c.stub: %s/generate-foo.py $(srcdir)/foo-data" + % (env.topsrcdir), + "$(REPORT_BUILD)", + "$(call py_action,file_generate foo.c,%s/generate-foo.py main foo.c $(MDDEPDIR)/foo.c.pp $(MDDEPDIR)/foo.c.stub $(srcdir)/foo-data)" # noqa + % (env.topsrcdir), + "@$(TOUCH) $@", + "", + ] + + self.maxDiff = None + self.assertEqual(lines, expected) + + def test_localized_generated_files(self): + """Ensure LOCALIZED_GENERATED_FILES is handled properly.""" + env = self._consume("localized-generated-files", RecursiveMakeBackend) + + backend_path = mozpath.join(env.topobjdir, "backend.mk") + lines = [l.strip() for l in open(backend_path, "rt").readlines()[2:]] + + expected = [ + "include $(topsrcdir)/config/AB_rCD.mk", + "MISC_TARGETS += $(MDDEPDIR)/foo.xyz.stub", + "foo.xyz: $(MDDEPDIR)/foo.xyz.stub ;", + "EXTRA_MDDEPEND_FILES += $(MDDEPDIR)/foo.xyz.pp", + "$(MDDEPDIR)/foo.xyz.stub: %s/generate-foo.py $(call MERGE_FILE,localized-input) $(srcdir)/non-localized-input $(if $(IS_LANGUAGE_REPACK),FORCE)" # noqa + % env.topsrcdir, + "$(REPORT_BUILD)", + "$(call py_action,file_generate foo.xyz,--locale=$(AB_CD) %s/generate-foo.py main foo.xyz $(MDDEPDIR)/foo.xyz.pp $(MDDEPDIR)/foo.xyz.stub $(call MERGE_FILE,localized-input) $(srcdir)/non-localized-input)" # noqa + % env.topsrcdir, + "@$(TOUCH) $@", + "", + "LOCALIZED_FILES_0_FILES += foo.xyz", + "LOCALIZED_FILES_0_DEST = $(FINAL_TARGET)/", + "LOCALIZED_FILES_0_TARGET := misc", + "INSTALL_TARGETS += LOCALIZED_FILES_0", + ] + + self.maxDiff = None + self.assertEqual(lines, expected) + + def test_localized_generated_files_force(self): + """Ensure LOCALIZED_GENERATED_FILES with .force is handled properly.""" + env = self._consume("localized-generated-files-force", RecursiveMakeBackend) + + backend_path = mozpath.join(env.topobjdir, "backend.mk") + lines = [l.strip() for l in open(backend_path, "rt").readlines()[2:]] + + expected = [ + "include $(topsrcdir)/config/AB_rCD.mk", + "MISC_TARGETS += $(MDDEPDIR)/foo.xyz.stub", + "foo.xyz: $(MDDEPDIR)/foo.xyz.stub ;", + "EXTRA_MDDEPEND_FILES += $(MDDEPDIR)/foo.xyz.pp", + "$(MDDEPDIR)/foo.xyz.stub: %s/generate-foo.py $(call MERGE_FILE,localized-input) $(srcdir)/non-localized-input $(if $(IS_LANGUAGE_REPACK),FORCE)" # noqa + % env.topsrcdir, + "$(REPORT_BUILD)", + "$(call py_action,file_generate foo.xyz,--locale=$(AB_CD) %s/generate-foo.py main foo.xyz $(MDDEPDIR)/foo.xyz.pp $(MDDEPDIR)/foo.xyz.stub $(call MERGE_FILE,localized-input) $(srcdir)/non-localized-input)" # noqa + % env.topsrcdir, + "@$(TOUCH) $@", + "", + "MISC_TARGETS += $(MDDEPDIR)/abc.xyz.stub", + "abc.xyz: $(MDDEPDIR)/abc.xyz.stub ;", + "EXTRA_MDDEPEND_FILES += $(MDDEPDIR)/abc.xyz.pp", + "$(MDDEPDIR)/abc.xyz.stub: %s/generate-foo.py $(call MERGE_FILE,localized-input) $(srcdir)/non-localized-input FORCE" # noqa + % env.topsrcdir, + "$(REPORT_BUILD)", + "$(call py_action,file_generate abc.xyz,--locale=$(AB_CD) %s/generate-foo.py main abc.xyz $(MDDEPDIR)/abc.xyz.pp $(MDDEPDIR)/abc.xyz.stub $(call MERGE_FILE,localized-input) $(srcdir)/non-localized-input)" # noqa + % env.topsrcdir, + "@$(TOUCH) $@", + "", + ] + + self.maxDiff = None + self.assertEqual(lines, expected) + + def test_localized_generated_files_AB_CD(self): + """Ensure LOCALIZED_GENERATED_FILES is handled properly + when {AB_CD} and {AB_rCD} are used.""" + env = self._consume("localized-generated-files-AB_CD", RecursiveMakeBackend) + + backend_path = mozpath.join(env.topobjdir, "backend.mk") + lines = [l.strip() for l in open(backend_path, "rt").readlines()[2:]] + + expected = [ + "include $(topsrcdir)/config/AB_rCD.mk", + "MISC_TARGETS += $(MDDEPDIR)/foo$(AB_CD).xyz.stub", + "foo$(AB_CD).xyz: $(MDDEPDIR)/foo$(AB_CD).xyz.stub ;", + "EXTRA_MDDEPEND_FILES += $(MDDEPDIR)/foo$(AB_CD).xyz.pp", + "$(MDDEPDIR)/foo$(AB_CD).xyz.stub: %s/generate-foo.py $(call MERGE_FILE,localized-input) $(srcdir)/non-localized-input $(if $(IS_LANGUAGE_REPACK),FORCE)" # noqa + % env.topsrcdir, + "$(REPORT_BUILD)", + "$(call py_action,file_generate foo$(AB_CD).xyz,--locale=$(AB_CD) %s/generate-foo.py main foo$(AB_CD).xyz $(MDDEPDIR)/foo$(AB_CD).xyz.pp $(MDDEPDIR)/foo$(AB_CD).xyz.stub $(call MERGE_FILE,localized-input) $(srcdir)/non-localized-input)" # noqa + % env.topsrcdir, + "@$(TOUCH) $@", + "", + "bar$(AB_rCD).xyz: $(MDDEPDIR)/bar$(AB_rCD).xyz.stub ;", + "EXTRA_MDDEPEND_FILES += $(MDDEPDIR)/bar$(AB_rCD).xyz.pp", + "$(MDDEPDIR)/bar$(AB_rCD).xyz.stub: %s/generate-foo.py $(call MERGE_RELATIVE_FILE,localized-input,inner/locales) $(srcdir)/non-localized-input $(if $(IS_LANGUAGE_REPACK),FORCE)" # noqa + % env.topsrcdir, + "$(REPORT_BUILD)", + "$(call py_action,file_generate bar$(AB_rCD).xyz,--locale=$(AB_CD) %s/generate-foo.py main bar$(AB_rCD).xyz $(MDDEPDIR)/bar$(AB_rCD).xyz.pp $(MDDEPDIR)/bar$(AB_rCD).xyz.stub $(call MERGE_RELATIVE_FILE,localized-input,inner/locales) $(srcdir)/non-localized-input)" # noqa + % env.topsrcdir, + "@$(TOUCH) $@", + "", + "zot$(AB_rCD).xyz: $(MDDEPDIR)/zot$(AB_rCD).xyz.stub ;", + "EXTRA_MDDEPEND_FILES += $(MDDEPDIR)/zot$(AB_rCD).xyz.pp", + "$(MDDEPDIR)/zot$(AB_rCD).xyz.stub: %s/generate-foo.py $(call MERGE_RELATIVE_FILE,localized-input,locales) $(srcdir)/non-localized-input $(if $(IS_LANGUAGE_REPACK),FORCE)" # noqa + % env.topsrcdir, + "$(REPORT_BUILD)", + "$(call py_action,file_generate zot$(AB_rCD).xyz,--locale=$(AB_CD) %s/generate-foo.py main zot$(AB_rCD).xyz $(MDDEPDIR)/zot$(AB_rCD).xyz.pp $(MDDEPDIR)/zot$(AB_rCD).xyz.stub $(call MERGE_RELATIVE_FILE,localized-input,locales) $(srcdir)/non-localized-input)" # noqa + % env.topsrcdir, + "@$(TOUCH) $@", + "", + ] + + self.maxDiff = None + self.assertEqual(lines, expected) + + def test_exports_generated(self): + """Ensure EXPORTS that are listed in GENERATED_FILES + are handled properly.""" + env = self._consume("exports-generated", RecursiveMakeBackend) + + # EXPORTS files should appear in the dist_include install manifest. + m = InstallManifest( + path=mozpath.join( + env.topobjdir, "_build_manifests", "install", "dist_include" + ) + ) + self.assertEqual(len(m), 8) + self.assertIn("foo.h", m) + self.assertIn("mozilla/mozilla1.h", m) + self.assertIn("mozilla/dom/dom1.h", m) + self.assertIn("gfx/gfx.h", m) + self.assertIn("bar.h", m) + self.assertIn("mozilla/mozilla2.h", m) + self.assertIn("mozilla/dom/dom2.h", m) + self.assertIn("mozilla/dom/dom3.h", m) + # EXPORTS files that are also GENERATED_FILES should be handled as + # INSTALL_TARGETS. + backend_path = mozpath.join(env.topobjdir, "backend.mk") + lines = [l.strip() for l in open(backend_path, "rt").readlines()[2:]] + expected = [ + "include $(topsrcdir)/config/AB_rCD.mk", + "dist_include_FILES += bar.h", + "dist_include_DEST := $(DEPTH)/dist/include/", + "dist_include_TARGET := export", + "INSTALL_TARGETS += dist_include", + "dist_include_mozilla_FILES += mozilla2.h", + "dist_include_mozilla_DEST := $(DEPTH)/dist/include/mozilla", + "dist_include_mozilla_TARGET := export", + "INSTALL_TARGETS += dist_include_mozilla", + "dist_include_mozilla_dom_FILES += dom2.h", + "dist_include_mozilla_dom_FILES += dom3.h", + "dist_include_mozilla_dom_DEST := $(DEPTH)/dist/include/mozilla/dom", + "dist_include_mozilla_dom_TARGET := export", + "INSTALL_TARGETS += dist_include_mozilla_dom", + ] + self.maxDiff = None + self.assertEqual(lines, expected) + + def test_resources(self): + """Ensure RESOURCE_FILES is handled properly.""" + env = self._consume("resources", RecursiveMakeBackend) + + # RESOURCE_FILES should appear in the dist_bin install manifest. + m = InstallManifest( + path=os.path.join(env.topobjdir, "_build_manifests", "install", "dist_bin") + ) + self.assertEqual(len(m), 10) + self.assertIn("res/foo.res", m) + self.assertIn("res/fonts/font1.ttf", m) + self.assertIn("res/fonts/desktop/desktop2.ttf", m) + + self.assertIn("res/bar.res.in", m) + self.assertIn("res/tests/test.manifest", m) + self.assertIn("res/tests/extra.manifest", m) + + def test_test_manifests_files_written(self): + """Ensure test manifests get turned into files.""" + env = self._consume("test-manifests-written", RecursiveMakeBackend) + + tests_dir = mozpath.join(env.topobjdir, "_tests") + m_master = mozpath.join( + tests_dir, "testing", "mochitest", "tests", "mochitest.toml" + ) + x_master = mozpath.join(tests_dir, "xpcshell", "xpcshell.toml") + self.assertTrue(os.path.exists(m_master)) + self.assertTrue(os.path.exists(x_master)) + + lines = [l.strip() for l in open(x_master, "rt").readlines()] + self.assertEqual( + lines, + [ + "# THIS FILE WAS AUTOMATICALLY GENERATED. DO NOT MODIFY BY HAND.", + "", + '["include:dir1/xpcshell.toml"]', + '["include:xpcshell.toml"]', + ], + ) + + def test_test_manifest_pattern_matches_recorded(self): + """Pattern matches in test manifests' support-files should be recorded.""" + env = self._consume("test-manifests-written", RecursiveMakeBackend) + m = InstallManifest( + path=mozpath.join( + env.topobjdir, "_build_manifests", "install", "_test_files" + ) + ) + + # This is not the most robust test in the world, but it gets the job + # done. + entries = [e for e in m._dests.keys() if "**" in e] + self.assertEqual(len(entries), 1) + self.assertIn("support/**", entries[0]) + + def test_test_manifest_deffered_installs_written(self): + """Shared support files are written to their own data file by the backend.""" + env = self._consume("test-manifest-shared-support", RecursiveMakeBackend) + + # First, read the generated for toml manifest contents. + test_files_manifest = mozpath.join( + env.topobjdir, "_build_manifests", "install", "_test_files" + ) + m = InstallManifest(path=test_files_manifest) + + # Then, synthesize one from the test-installs.pkl file. This should + # allow us to re-create a subset of the above. + env = self._consume("test-manifest-shared-support", TestManifestBackend) + test_installs_path = mozpath.join(env.topobjdir, "test-installs.pkl") + + with open(test_installs_path, "rb") as fh: + test_installs = pickle.load(fh) + + self.assertEqual( + set(test_installs.keys()), + set(["child/test_sub.js", "child/data/**", "child/another-file.sjs"]), + ) + for key in test_installs.keys(): + self.assertIn(key, test_installs) + + synthesized_manifest = InstallManifest() + for item, installs in test_installs.items(): + for install_info in installs: + if len(install_info) == 3: + synthesized_manifest.add_pattern_link(*install_info) + if len(install_info) == 2: + synthesized_manifest.add_link(*install_info) + + self.assertEqual(len(synthesized_manifest), 3) + for item, info in synthesized_manifest._dests.items(): + self.assertIn(item, m) + self.assertEqual(info, m._dests[item]) + + def test_xpidl_generation(self): + """Ensure xpidl files and directories are written out.""" + env = self._consume("xpidl", RecursiveMakeBackend) + + # Install manifests should contain entries. + install_dir = mozpath.join(env.topobjdir, "_build_manifests", "install") + self.assertTrue(os.path.isfile(mozpath.join(install_dir, "xpidl"))) + + m = InstallManifest(path=mozpath.join(install_dir, "xpidl")) + self.assertIn(".deps/my_module.pp", m) + + m = InstallManifest(path=mozpath.join(install_dir, "xpidl")) + self.assertIn("my_module.xpt", m) + + m = InstallManifest(path=mozpath.join(install_dir, "dist_include")) + self.assertIn("foo.h", m) + + p = mozpath.join(env.topobjdir, "config/makefiles/xpidl") + self.assertTrue(os.path.isdir(p)) + + self.assertTrue(os.path.isfile(mozpath.join(p, "Makefile"))) + + def test_test_support_files_tracked(self): + env = self._consume("test-support-binaries-tracked", RecursiveMakeBackend) + m = InstallManifest( + path=mozpath.join(env.topobjdir, "_build_manifests", "install", "_tests") + ) + self.assertEqual(len(m), 4) + self.assertIn("xpcshell/tests/mozbuildtest/test-library.dll", m) + self.assertIn("xpcshell/tests/mozbuildtest/test-one.exe", m) + self.assertIn("xpcshell/tests/mozbuildtest/test-two.exe", m) + self.assertIn("xpcshell/tests/mozbuildtest/host-test-library.dll", m) + + def test_old_install_manifest_deleted(self): + # Simulate an install manifest from a previous backend version. Ensure + # it is deleted. + env = self._get_environment("stub0") + purge_dir = mozpath.join(env.topobjdir, "_build_manifests", "install") + manifest_path = mozpath.join(purge_dir, "old_manifest") + os.makedirs(purge_dir) + m = InstallManifest() + m.write(path=manifest_path) + with open( + mozpath.join(env.topobjdir, "backend.RecursiveMakeBackend"), "w" + ) as f: + f.write("%s\n" % manifest_path) + + self.assertTrue(os.path.exists(manifest_path)) + self._consume("stub0", RecursiveMakeBackend, env) + self.assertFalse(os.path.exists(manifest_path)) + + def test_install_manifests_written(self): + env, objs = self._emit("stub0") + backend = RecursiveMakeBackend(env) + + m = InstallManifest() + backend._install_manifests["testing"] = m + m.add_link(__file__, "self") + backend.consume(objs) + + man_dir = mozpath.join(env.topobjdir, "_build_manifests", "install") + self.assertTrue(os.path.isdir(man_dir)) + + expected = ["testing"] + for e in expected: + full = mozpath.join(man_dir, e) + self.assertTrue(os.path.exists(full)) + + m2 = InstallManifest(path=full) + self.assertEqual(m, m2) + + def test_ipdl_sources(self): + """Test that PREPROCESSED_IPDL_SOURCES and IPDL_SOURCES are written to + ipdlsrcs.mk correctly.""" + env = self._get_environment("ipdl_sources") + + # Use the ipdl directory as the IPDL root for testing. + ipdl_root = mozpath.join(env.topobjdir, "ipdl") + + # Make substs writable so we can set the value of IPDL_ROOT to reflect + # the correct objdir. + env.substs = dict(env.substs) + env.substs["IPDL_ROOT"] = ipdl_root + + self._consume("ipdl_sources", RecursiveMakeBackend, env) + + manifest_path = mozpath.join(ipdl_root, "ipdlsrcs.mk") + lines = [l.strip() for l in open(manifest_path, "rt").readlines()] + + # Handle Windows paths correctly + topsrcdir = mozpath.normsep(env.topsrcdir) + + expected = [ + "ALL_IPDLSRCS := bar1.ipdl foo1.ipdl %s/bar/bar.ipdl %s/bar/bar2.ipdlh %s/foo/foo.ipdl %s/foo/foo2.ipdlh" # noqa + % tuple([topsrcdir] * 4), + "IPDLDIRS := %s %s/bar %s/foo" % (ipdl_root, topsrcdir, topsrcdir), + ] + + found = [str for str in lines if str.startswith(("ALL_IPDLSRCS", "IPDLDIRS"))] + self.assertEqual(found, expected) + + # Check that each directory declares the generated relevant .cpp files + # to be built in CPPSRCS. + # ENABLE_UNIFIED_BUILD defaults to False without mozilla-central's + # moz.configure so we don't see unified sources here. + for dir, expected in ( + (".", []), + ("ipdl", []), + ( + "bar", + [ + "CPPSRCS += " + + " ".join( + f"{ipdl_root}/{f}" + for f in [ + "bar.cpp", + "bar1.cpp", + "bar1Child.cpp", + "bar1Parent.cpp", + "bar2.cpp", + "barChild.cpp", + "barParent.cpp", + ] + ) + ], + ), + ( + "foo", + [ + "CPPSRCS += " + + " ".join( + f"{ipdl_root}/{f}" + for f in [ + "foo.cpp", + "foo1.cpp", + "foo1Child.cpp", + "foo1Parent.cpp", + "foo2.cpp", + "fooChild.cpp", + "fooParent.cpp", + ] + ) + ], + ), + ): + backend_path = mozpath.join(env.topobjdir, dir, "backend.mk") + lines = [l.strip() for l in open(backend_path, "rt").readlines()] + + found = [str for str in lines if str.startswith("CPPSRCS")] + self.assertEqual(found, expected) + + def test_defines(self): + """Test that DEFINES are written to backend.mk correctly.""" + env = self._consume("defines", RecursiveMakeBackend) + + backend_path = mozpath.join(env.topobjdir, "backend.mk") + lines = [l.strip() for l in open(backend_path, "rt").readlines()[2:]] + + var = "DEFINES" + defines = [val for val in lines if val.startswith(var)] + + expected = ["DEFINES += -DFOO '-DBAZ=\"ab'\\''cd\"' -UQUX -DBAR=7 -DVALUE=xyz"] + self.assertEqual(defines, expected) + + def test_local_includes(self): + """Test that LOCAL_INCLUDES are written to backend.mk correctly.""" + env = self._consume("local_includes", RecursiveMakeBackend) + + backend_path = mozpath.join(env.topobjdir, "backend.mk") + lines = [l.strip() for l in open(backend_path, "rt").readlines()[2:]] + + expected = [ + "LOCAL_INCLUDES += -I$(srcdir)/bar/baz", + "LOCAL_INCLUDES += -I$(srcdir)/foo", + ] + + found = [str for str in lines if str.startswith("LOCAL_INCLUDES")] + self.assertEqual(found, expected) + + def test_generated_includes(self): + """Test that GENERATED_INCLUDES are written to backend.mk correctly.""" + env = self._consume("generated_includes", RecursiveMakeBackend) + + backend_path = mozpath.join(env.topobjdir, "backend.mk") + lines = [l.strip() for l in open(backend_path, "rt").readlines()[2:]] + + expected = [ + "LOCAL_INCLUDES += -I$(CURDIR)/bar/baz", + "LOCAL_INCLUDES += -I$(CURDIR)/foo", + ] + + found = [str for str in lines if str.startswith("LOCAL_INCLUDES")] + self.assertEqual(found, expected) + + def test_rust_library(self): + """Test that a Rust library is written to backend.mk correctly.""" + env = self._consume("rust-library", RecursiveMakeBackend) + + backend_path = mozpath.join(env.topobjdir, "backend.mk") + lines = [ + l.strip() + for l in open(backend_path, "rt").readlines()[2:] + # Strip out computed flags, they're a PITA to test. + if not l.startswith("COMPUTED_") + ] + + expected = [ + "RUST_LIBRARY_FILE := %s/x86_64-unknown-linux-gnu/release/libtest_library.a" + % env.topobjdir, # noqa + "CARGO_FILE := $(srcdir)/Cargo.toml", + "CARGO_TARGET_DIR := %s" % env.topobjdir, + ] + + self.assertEqual(lines, expected) + + def test_host_rust_library(self): + """Test that a Rust library is written to backend.mk correctly.""" + env = self._consume("host-rust-library", RecursiveMakeBackend) + + backend_path = mozpath.join(env.topobjdir, "backend.mk") + lines = [ + l.strip() + for l in open(backend_path, "rt").readlines()[2:] + # Strip out computed flags, they're a PITA to test. + if not l.startswith("COMPUTED_") + ] + + expected = [ + "HOST_RUST_LIBRARY_FILE := %s/x86_64-unknown-linux-gnu/release/libhostrusttool.a" + % env.topobjdir, # noqa + "CARGO_FILE := $(srcdir)/Cargo.toml", + "CARGO_TARGET_DIR := %s" % env.topobjdir, + ] + + self.assertEqual(lines, expected) + + def test_host_rust_library_with_features(self): + """Test that a host Rust library with features is written to backend.mk correctly.""" + env = self._consume("host-rust-library-features", RecursiveMakeBackend) + + backend_path = mozpath.join(env.topobjdir, "backend.mk") + lines = [ + l.strip() + for l in open(backend_path, "rt").readlines()[2:] + # Strip out computed flags, they're a PITA to test. + if not l.startswith("COMPUTED_") + ] + + expected = [ + "HOST_RUST_LIBRARY_FILE := %s/x86_64-unknown-linux-gnu/release/libhostrusttool.a" + % env.topobjdir, # noqa + "CARGO_FILE := $(srcdir)/Cargo.toml", + "CARGO_TARGET_DIR := %s" % env.topobjdir, + "HOST_RUST_LIBRARY_FEATURES := musthave cantlivewithout", + ] + + self.assertEqual(lines, expected) + + def test_rust_library_with_features(self): + """Test that a Rust library with features is written to backend.mk correctly.""" + env = self._consume("rust-library-features", RecursiveMakeBackend) + + backend_path = mozpath.join(env.topobjdir, "backend.mk") + lines = [ + l.strip() + for l in open(backend_path, "rt").readlines()[2:] + # Strip out computed flags, they're a PITA to test. + if not l.startswith("COMPUTED_") + ] + + expected = [ + "RUST_LIBRARY_FILE := %s/x86_64-unknown-linux-gnu/release/libfeature_library.a" + % env.topobjdir, # noqa + "CARGO_FILE := $(srcdir)/Cargo.toml", + "CARGO_TARGET_DIR := %s" % env.topobjdir, + "RUST_LIBRARY_FEATURES := musthave cantlivewithout", + ] + + self.assertEqual(lines, expected) + + def test_rust_programs(self): + """Test that `{HOST_,}RUST_PROGRAMS` are written to backend.mk correctly.""" + env = self._consume("rust-programs", RecursiveMakeBackend) + + backend_path = mozpath.join(env.topobjdir, "code/backend.mk") + lines = [ + l.strip() + for l in open(backend_path, "rt").readlines()[2:] + # Strip out computed flags, they're a PITA to test. + if not l.startswith("COMPUTED_") + ] + + expected = [ + "CARGO_FILE := %s/code/Cargo.toml" % env.topsrcdir, + "CARGO_TARGET_DIR := %s" % env.topobjdir, + "RUST_PROGRAMS += $(DEPTH)/i686-pc-windows-msvc/release/target.exe", + "RUST_CARGO_PROGRAMS += target", + "HOST_RUST_PROGRAMS += $(DEPTH)/i686-pc-windows-msvc/release/host.exe", + "HOST_RUST_CARGO_PROGRAMS += host", + ] + + self.assertEqual(lines, expected) + + root_deps_path = mozpath.join(env.topobjdir, "root-deps.mk") + lines = [l.strip() for l in open(root_deps_path, "rt").readlines()] + + self.assertTrue( + any(l == "recurse_compile: code/host code/target" for l in lines) + ) + + def test_final_target(self): + """Test that FINAL_TARGET is written to backend.mk correctly.""" + env = self._consume("final_target", RecursiveMakeBackend) + + final_target_rule = "FINAL_TARGET = $(if $(XPI_NAME),$(DIST)/xpi-stage/$(XPI_NAME),$(DIST)/bin)$(DIST_SUBDIR:%=/%)" # noqa + expected = dict() + expected[env.topobjdir] = [] + expected[mozpath.join(env.topobjdir, "both")] = [ + "XPI_NAME = mycrazyxpi", + "DIST_SUBDIR = asubdir", + final_target_rule, + ] + expected[mozpath.join(env.topobjdir, "dist-subdir")] = [ + "DIST_SUBDIR = asubdir", + final_target_rule, + ] + expected[mozpath.join(env.topobjdir, "xpi-name")] = [ + "XPI_NAME = mycrazyxpi", + final_target_rule, + ] + expected[mozpath.join(env.topobjdir, "final-target")] = [ + "FINAL_TARGET = $(DEPTH)/random-final-target" + ] + for key, expected_rules in six.iteritems(expected): + backend_path = mozpath.join(key, "backend.mk") + lines = [l.strip() for l in open(backend_path, "rt").readlines()[2:]] + found = [ + str + for str in lines + if str.startswith("FINAL_TARGET") + or str.startswith("XPI_NAME") + or str.startswith("DIST_SUBDIR") + ] + self.assertEqual(found, expected_rules) + + def test_final_target_pp_files(self): + """Test that FINAL_TARGET_PP_FILES is written to backend.mk correctly.""" + env = self._consume("dist-files", RecursiveMakeBackend) + + backend_path = mozpath.join(env.topobjdir, "backend.mk") + lines = [l.strip() for l in open(backend_path, "rt").readlines()[2:]] + + expected = [ + "DIST_FILES_0 += $(srcdir)/install.rdf", + "DIST_FILES_0 += $(srcdir)/main.js", + "DIST_FILES_0_PATH := $(DEPTH)/dist/bin/", + "DIST_FILES_0_TARGET := misc", + "PP_TARGETS += DIST_FILES_0", + ] + + found = [str for str in lines if "DIST_FILES" in str] + self.assertEqual(found, expected) + + def test_localized_files(self): + """Test that LOCALIZED_FILES is written to backend.mk correctly.""" + env = self._consume("localized-files", RecursiveMakeBackend) + + backend_path = mozpath.join(env.topobjdir, "backend.mk") + lines = [l.strip() for l in open(backend_path, "rt").readlines()[2:]] + + expected = [ + "LOCALIZED_FILES_0_FILES += $(wildcard $(LOCALE_SRCDIR)/abc/*.abc)", + "LOCALIZED_FILES_0_FILES += $(call MERGE_FILE,bar.ini)", + "LOCALIZED_FILES_0_FILES += $(call MERGE_FILE,foo.js)", + "LOCALIZED_FILES_0_DEST = $(FINAL_TARGET)/", + "LOCALIZED_FILES_0_TARGET := misc", + "INSTALL_TARGETS += LOCALIZED_FILES_0", + ] + + found = [str for str in lines if "LOCALIZED_FILES" in str] + self.assertEqual(found, expected) + + def test_localized_pp_files(self): + """Test that LOCALIZED_PP_FILES is written to backend.mk correctly.""" + env = self._consume("localized-pp-files", RecursiveMakeBackend) + + backend_path = mozpath.join(env.topobjdir, "backend.mk") + lines = [l.strip() for l in open(backend_path, "rt").readlines()[2:]] + + expected = [ + "LOCALIZED_PP_FILES_0 += $(call MERGE_FILE,bar.ini)", + "LOCALIZED_PP_FILES_0 += $(call MERGE_FILE,foo.js)", + "LOCALIZED_PP_FILES_0_PATH = $(FINAL_TARGET)/", + "LOCALIZED_PP_FILES_0_TARGET := misc", + "LOCALIZED_PP_FILES_0_FLAGS := --silence-missing-directive-warnings", + "PP_TARGETS += LOCALIZED_PP_FILES_0", + ] + + found = [str for str in lines if "LOCALIZED_PP_FILES" in str] + self.assertEqual(found, expected) + + def test_config(self): + """Test that CONFIGURE_SUBST_FILES are properly handled.""" + env = self._consume("test_config", RecursiveMakeBackend) + + self.assertEqual( + open(os.path.join(env.topobjdir, "file"), "r").readlines(), + ["#ifdef foo\n", "bar baz\n", "@bar@\n"], + ) + + def test_prog_lib_c_only(self): + """Test that C-only binary artifacts are marked as such.""" + env = self._consume("prog-lib-c-only", RecursiveMakeBackend) + + # PROGRAM C-onlyness. + with open(os.path.join(env.topobjdir, "c-program", "backend.mk"), "r") as fh: + lines = fh.readlines() + lines = [line.rstrip() for line in lines] + + self.assertIn("PROG_IS_C_ONLY_c_test_program := 1", lines) + + with open(os.path.join(env.topobjdir, "cxx-program", "backend.mk"), "r") as fh: + lines = fh.readlines() + lines = [line.rstrip() for line in lines] + + # Test for only the absence of the variable, not the precise + # form of the variable assignment. + for line in lines: + self.assertNotIn("PROG_IS_C_ONLY_cxx_test_program", line) + + # SIMPLE_PROGRAMS C-onlyness. + with open( + os.path.join(env.topobjdir, "c-simple-programs", "backend.mk"), "r" + ) as fh: + lines = fh.readlines() + lines = [line.rstrip() for line in lines] + + self.assertIn("PROG_IS_C_ONLY_c_simple_program := 1", lines) + + with open( + os.path.join(env.topobjdir, "cxx-simple-programs", "backend.mk"), "r" + ) as fh: + lines = fh.readlines() + lines = [line.rstrip() for line in lines] + + for line in lines: + self.assertNotIn("PROG_IS_C_ONLY_cxx_simple_program", line) + + # Libraries C-onlyness. + with open(os.path.join(env.topobjdir, "c-library", "backend.mk"), "r") as fh: + lines = fh.readlines() + lines = [line.rstrip() for line in lines] + + self.assertIn("LIB_IS_C_ONLY := 1", lines) + + with open(os.path.join(env.topobjdir, "cxx-library", "backend.mk"), "r") as fh: + lines = fh.readlines() + lines = [line.rstrip() for line in lines] + + for line in lines: + self.assertNotIn("LIB_IS_C_ONLY", line) + + def test_linkage(self): + env = self._consume("linkage", RecursiveMakeBackend) + expected_linkage = { + "prog": { + "SHARED_LIBS": ["qux/qux.so", "../shared/baz.so"], + "STATIC_LIBS": ["../real/foo.a"], + "OS_LIBS": ["-lfoo", "-lbaz", "-lbar"], + }, + "shared": { + "OS_LIBS": ["-lfoo"], + "SHARED_LIBS": ["../prog/qux/qux.so"], + "STATIC_LIBS": [], + }, + "static": { + "STATIC_LIBS": ["../real/foo.a"], + "OS_LIBS": ["-lbar"], + "SHARED_LIBS": ["../prog/qux/qux.so"], + }, + "real": { + "STATIC_LIBS": [], + "SHARED_LIBS": ["../prog/qux/qux.so"], + "OS_LIBS": ["-lbaz"], + }, + } + actual_linkage = {} + for name in expected_linkage.keys(): + with open(os.path.join(env.topobjdir, name, "backend.mk"), "r") as fh: + actual_linkage[name] = [line.rstrip() for line in fh.readlines()] + for name in expected_linkage: + for var in expected_linkage[name]: + for val in expected_linkage[name][var]: + val = os.path.normpath(val) + line = "%s += %s" % (var, val) + self.assertIn(line, actual_linkage[name]) + actual_linkage[name].remove(line) + for line in actual_linkage[name]: + self.assertNotIn("%s +=" % var, line) + + def test_list_files(self): + env = self._consume("linkage", RecursiveMakeBackend) + expected_list_files = { + "prog/MyProgram_exe.list": [ + "../static/bar/bar1.o", + "../static/bar/bar2.o", + "../static/bar/bar_helper/bar_helper1.o", + ], + "shared/baz_so.list": ["baz/baz1.o"], + } + actual_list_files = {} + for name in expected_list_files.keys(): + with open(os.path.join(env.topobjdir, name), "r") as fh: + actual_list_files[name] = [line.rstrip() for line in fh.readlines()] + for name in expected_list_files: + self.assertEqual( + actual_list_files[name], + [os.path.normpath(f) for f in expected_list_files[name]], + ) + + # We don't produce a list file for a shared library composed only of + # object files in its directory, but instead list them in a variable. + with open(os.path.join(env.topobjdir, "prog", "qux", "backend.mk"), "r") as fh: + lines = [line.rstrip() for line in fh.readlines()] + + self.assertIn("qux.so_OBJS := qux1.o", lines) + + def test_jar_manifests(self): + env = self._consume("jar-manifests", RecursiveMakeBackend) + + with open(os.path.join(env.topobjdir, "backend.mk"), "r") as fh: + lines = fh.readlines() + + lines = [line.rstrip() for line in lines] + + self.assertIn("JAR_MANIFEST := %s/jar.mn" % env.topsrcdir, lines) + + def test_test_manifests_duplicate_support_files(self): + """Ensure duplicate support-files in test manifests work.""" + env = self._consume( + "test-manifests-duplicate-support-files", RecursiveMakeBackend + ) + + p = os.path.join(env.topobjdir, "_build_manifests", "install", "_test_files") + m = InstallManifest(p) + self.assertIn("testing/mochitest/tests/support-file.txt", m) + + def test_install_manifests_package_tests(self): + """Ensure test suites honor package_tests=False.""" + env = self._consume("test-manifests-package-tests", RecursiveMakeBackend) + + man_dir = mozpath.join(env.topobjdir, "_build_manifests", "install") + self.assertTrue(os.path.isdir(man_dir)) + + full = mozpath.join(man_dir, "_test_files") + self.assertTrue(os.path.exists(full)) + + m = InstallManifest(path=full) + + # Only mochitest.js should be in the install manifest. + self.assertTrue("testing/mochitest/tests/mochitest.js" in m) + + # The path is odd here because we do not normalize at test manifest + # processing time. This is a fragile test because there's currently no + # way to iterate the manifest. + self.assertFalse("instrumentation/./not_packaged.java" in m) + + def test_program_paths(self): + """PROGRAMs with various moz.build settings that change the destination should produce + the expected paths in backend.mk.""" + env = self._consume("program-paths", RecursiveMakeBackend) + + expected = [ + ("dist-bin", "$(DEPTH)/dist/bin/dist-bin.prog"), + ("dist-subdir", "$(DEPTH)/dist/bin/foo/dist-subdir.prog"), + ("final-target", "$(DEPTH)/final/target/final-target.prog"), + ("not-installed", "not-installed.prog"), + ] + prefix = "PROGRAM = " + for subdir, expected_program in expected: + with io.open(os.path.join(env.topobjdir, subdir, "backend.mk"), "r") as fh: + lines = fh.readlines() + program = [ + line.rstrip().split(prefix, 1)[1] + for line in lines + if line.startswith(prefix) + ][0] + self.assertEqual(program, expected_program) + + +if __name__ == "__main__": + main() diff --git a/python/mozbuild/mozbuild/test/backend/test_test_manifest.py b/python/mozbuild/mozbuild/test/backend/test_test_manifest.py new file mode 100644 index 0000000000..15ef7dbaec --- /dev/null +++ b/python/mozbuild/mozbuild/test/backend/test_test_manifest.py @@ -0,0 +1,94 @@ +# 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/. + +import os + +import mozpack.path as mozpath +import six.moves.cPickle as pickle +from mozunit import main + +from mozbuild.backend.test_manifest import TestManifestBackend +from mozbuild.test.backend.common import BackendTester + + +class TestTestManifestBackend(BackendTester): + def test_all_tests_metadata_file_written(self): + """Ensure all-tests.pkl is generated.""" + env = self._consume("test-manifests-written", TestManifestBackend) + + all_tests_path = mozpath.join(env.topobjdir, "all-tests.pkl") + self.assertTrue(os.path.exists(all_tests_path)) + + with open(all_tests_path, "rb") as fh: + o = pickle.load(fh) + + self.assertIn("xpcshell.js", o) + self.assertIn("dir1/test_bar.js", o) + + self.assertEqual(len(o["xpcshell.js"]), 1) + + def test_test_installs_metadata_file_written(self): + """Ensure test-installs.pkl is generated.""" + env = self._consume("test-manifest-shared-support", TestManifestBackend) + all_tests_path = mozpath.join(env.topobjdir, "all-tests.pkl") + self.assertTrue(os.path.exists(all_tests_path)) + test_installs_path = mozpath.join(env.topobjdir, "test-installs.pkl") + + with open(test_installs_path, "rb") as fh: + test_installs = pickle.load(fh) + + self.assertEqual( + set(test_installs.keys()), + set(["child/test_sub.js", "child/data/**", "child/another-file.sjs"]), + ) + + for key in test_installs.keys(): + self.assertIn(key, test_installs) + + def test_test_defaults_metadata_file_written(self): + """Ensure test-defaults.pkl is generated.""" + env = self._consume("test-manifests-written", TestManifestBackend) + + test_defaults_path = mozpath.join(env.topobjdir, "test-defaults.pkl") + self.assertTrue(os.path.exists(test_defaults_path)) + + with open(test_defaults_path, "rb") as fh: + o = {mozpath.normpath(k): v for k, v in pickle.load(fh).items()} + + self.assertEqual( + set(mozpath.relpath(k, env.topsrcdir) for k in o.keys()), + set(["dir1/xpcshell.toml", "xpcshell.toml", "mochitest.toml"]), + ) + + manifest_path = mozpath.join(env.topsrcdir, "xpcshell.toml") + self.assertIn("here", o[manifest_path]) + self.assertIn("support-files", o[manifest_path]) + + def test_test_manifest_sources(self): + """Ensure that backend sources are generated correctly.""" + env = self._consume("test-manifests-backend-sources", TestManifestBackend) + + backend_path = mozpath.join(env.topobjdir, "backend.TestManifestBackend.in") + self.assertTrue(os.path.exists(backend_path)) + + status_path = mozpath.join(env.topobjdir, "config.status") + + with open(backend_path, "r") as fh: + sources = set(source.strip() for source in fh) + + self.assertEqual( + sources, + set( + [ + mozpath.join(env.topsrcdir, "mochitest.toml"), + mozpath.join(env.topsrcdir, "mochitest-common.toml"), + mozpath.join(env.topsrcdir, "moz.build"), + status_path, + ] + ), + ) + + +if __name__ == "__main__": + main() diff --git a/python/mozbuild/mozbuild/test/backend/test_visualstudio.py b/python/mozbuild/mozbuild/test/backend/test_visualstudio.py new file mode 100644 index 0000000000..14cccb484b --- /dev/null +++ b/python/mozbuild/mozbuild/test/backend/test_visualstudio.py @@ -0,0 +1,63 @@ +# 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/. + +import os +import unittest +from xml.dom.minidom import parse + +from mozunit import main + +from mozbuild.backend.visualstudio import VisualStudioBackend +from mozbuild.test.backend.common import BackendTester + + +class TestVisualStudioBackend(BackendTester): + @unittest.skip("Failing inconsistently in automation.") + def test_basic(self): + """Ensure we can consume our stub project.""" + + env = self._consume("visual-studio", VisualStudioBackend) + + msvc = os.path.join(env.topobjdir, "msvc") + self.assertTrue(os.path.isdir(msvc)) + + self.assertTrue(os.path.isfile(os.path.join(msvc, "mozilla.sln"))) + self.assertTrue(os.path.isfile(os.path.join(msvc, "mozilla.props"))) + self.assertTrue(os.path.isfile(os.path.join(msvc, "mach.bat"))) + self.assertTrue(os.path.isfile(os.path.join(msvc, "binary_my_app.vcxproj"))) + self.assertTrue(os.path.isfile(os.path.join(msvc, "target_full.vcxproj"))) + self.assertTrue(os.path.isfile(os.path.join(msvc, "library_dir1.vcxproj"))) + self.assertTrue(os.path.isfile(os.path.join(msvc, "library_dir1.vcxproj.user"))) + + d = parse(os.path.join(msvc, "library_dir1.vcxproj")) + self.assertEqual(d.documentElement.tagName, "Project") + els = d.getElementsByTagName("ClCompile") + self.assertEqual(len(els), 2) + + # mozilla-config.h should be explicitly listed as an include. + els = d.getElementsByTagName("NMakeForcedIncludes") + self.assertEqual(len(els), 1) + self.assertEqual( + els[0].firstChild.nodeValue, "$(TopObjDir)\\dist\\include\\mozilla-config.h" + ) + + # LOCAL_INCLUDES get added to the include search path. + els = d.getElementsByTagName("NMakeIncludeSearchPath") + self.assertEqual(len(els), 1) + includes = els[0].firstChild.nodeValue.split(";") + self.assertIn(os.path.normpath("$(TopSrcDir)/includeA/foo"), includes) + self.assertIn(os.path.normpath("$(TopSrcDir)/dir1"), includes) + self.assertIn(os.path.normpath("$(TopObjDir)/dir1"), includes) + self.assertIn(os.path.normpath("$(TopObjDir)\\dist\\include"), includes) + + # DEFINES get added to the project. + els = d.getElementsByTagName("NMakePreprocessorDefinitions") + self.assertEqual(len(els), 1) + defines = els[0].firstChild.nodeValue.split(";") + self.assertIn("DEFINEFOO", defines) + self.assertIn("DEFINEBAR=bar", defines) + + +if __name__ == "__main__": + main() diff --git a/python/mozbuild/mozbuild/test/code_analysis/test_mach_commands.py b/python/mozbuild/mozbuild/test/code_analysis/test_mach_commands.py new file mode 100644 index 0000000000..774688c62f --- /dev/null +++ b/python/mozbuild/mozbuild/test/code_analysis/test_mach_commands.py @@ -0,0 +1,90 @@ +# 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/. +import os +import unittest +from unittest import mock + +import mozpack.path as mozpath +from mach.registrar import Registrar +from mozunit import main + +from mozbuild.base import MozbuildObject + + +class TestStaticAnalysis(unittest.TestCase): + def setUp(self): + self.remove_cats = [] + for cat in ("build", "post-build", "misc", "testing", "devenv"): + if cat in Registrar.categories: + continue + Registrar.register_category(cat, cat, cat) + self.remove_cats.append(cat) + + def tearDown(self): + for cat in self.remove_cats: + del Registrar.categories[cat] + del Registrar.commands_by_category[cat] + + def test_bug_1615884(self): + # TODO: cleaner test + # we're testing the `_is_ignored_path` but in an ideal + # world we should test the clang_analysis mach command + # since that small function is an internal detail. + # But there is zero test infra for that mach command + from mozbuild.code_analysis.mach_commands import _is_ignored_path + + config = MozbuildObject.from_environment() + context = mock.MagicMock() + context.cwd = config.topsrcdir + + command_context = mock.MagicMock() + command_context.topsrcdir = os.path.join("/root", "dir") + path = os.path.join("/root", "dir", "path1") + + ignored_dirs_re = r"path1|path2/here|path3\there" + self.assertTrue( + _is_ignored_path(command_context, ignored_dirs_re, path) is not None + ) + + # simulating a win32 env + win32_path = "\\root\\dir\\path1" + command_context.topsrcdir = "\\root\\dir" + old_sep = os.sep + os.sep = "\\" + try: + self.assertTrue( + _is_ignored_path(command_context, ignored_dirs_re, win32_path) + is not None + ) + finally: + os.sep = old_sep + + self.assertTrue( + _is_ignored_path(command_context, ignored_dirs_re, "path2") is None + ) + + def test_get_files(self): + from mozbuild.code_analysis.mach_commands import get_abspath_files + + config = MozbuildObject.from_environment() + context = mock.MagicMock() + context.cwd = config.topsrcdir + + command_context = mock.MagicMock() + command_context.topsrcdir = mozpath.join("/root", "dir") + source = get_abspath_files( + command_context, ["file1", mozpath.join("directory", "file2")] + ) + + self.assertTrue( + source + == [ + mozpath.join(command_context.topsrcdir, "file1"), + mozpath.join(command_context.topsrcdir, "directory", "file2"), + ] + ) + + +if __name__ == "__main__": + main() diff --git a/python/mozbuild/mozbuild/test/codecoverage/sample_lcov.info b/python/mozbuild/mozbuild/test/codecoverage/sample_lcov.info new file mode 100644 index 0000000000..996ccac215 --- /dev/null +++ b/python/mozbuild/mozbuild/test/codecoverage/sample_lcov.info @@ -0,0 +1,1895 @@ +SF:lcov_test_newTab.js +FN:1,top-level +FN:31,top-level +FN:232,Transformation_rearrangeSites/</< +FN:259,Transformation_whenTransitionEnded +FN:262,onEnd +FN:275,Transformation_getNodeOpacity +FN:287,Transformation_setNodeOpacity +FN:307,Transformation_moveSite +FN:317,Transformation_isFrozen +FN:334,Page_init +FN:363,Page_observe +FN:399,update +FN:417,update/this._scheduleUpdateTimeout< +FN:431,Page_init +FN:443,Page_init/< +FN:459,Page_updateAttributes +FN:482,Page_handleUnloadEvent +FN:499,Page_handleEvent +FN:538,Page_handleEvent/< +FN:544,gPage.onPageFirstVisible +FN:567,onPageVisibleAndLoaded +FN:575,reportLastVisibleTileIndex +FN:619,gGrid.node +FN:630,gGrid.cells +FN:635,gGrid.sites +FN:638,gGrid.ready +FN:641,gGrid.isDocumentLoaded +FN:647,Grid_init +FN:651,Grid_init/< +FN:676,Grid_createSite +FN:685,Grid_handleEvent +FN:697,Grid_lock +FN:704,Grid_unlock +FN:714,refresh +FN:722,_refreshGrid +FN:755,Grid_computeHeight +FN:764,Grid_createSiteFragment +FN:791,Grid_isHistoricalTile +FN:799,Grid_resizeGrid +FN:870,Cell +FN:876,Cell/< +FN:890,Cell.prototype.node +FN:895,Cell.prototype.index +FN:907,Cell.prototype.previousSibling +FN:920,Cell.prototype.nextSibling +FN:933,Cell.prototype.site +FN:942,Cell_containsPinnedSite +FN:951,Cell_isEmpty +FN:958,Cell_handleEvent +FN:991,Site +FN:1005,Site.prototype.node +FN:1010,Site.prototype.link +FN:1015,Site.prototype.url +FN:1020,Site.prototype.title +FN:1025,Site.prototype.cell +FN:1035,Site_pin +FN:1051,Site_unpin +FN:1063,Site_isPinned +FN:1071,Site_block +FN:1084,Site_querySelector +FN:1093,Site.prototype._updateAttributes +FN:1105,Site.prototype._newTabString +FN:1116,Site.prototype._getSuggestedTileExplanation +FN:1128,Site_checkLinkEndTime +FN:1147,Site_render +FN:1199,Site_onFirstVisible +FN:1213,Site_captureIfMissing +FN:1222,Site_refreshThumbnail +FN:1246,Site.prototype._ignoreHoverEvents +FN:1247,Site.prototype._ignoreHoverEvents/< +FN:1250,Site.prototype._ignoreHoverEvents/< +FN:1258,Site_addEventHandlers +FN:1275,Site_speculativeConnect +FN:1284,Site_recordSiteClicked +FN:1296,Site.prototype._toggleLegalText +FN:1324,Site_onClick +FN:1386,Site_handleEvent +FN:1417,gDrag.draggedSite +FN:1424,gDrag.cellWidth +FN:1425,gDrag.cellHeight +FN:1432,Drag_start +FN:1465,Drag_drag +FN:1489,Drag_end +FN:1505,Drag_isValid +FN:1524,Drag_setDragData +FN:1545,Drag_setDragData/< +FN:1551,gDragDataHelper.mimeType +FN:1555,DragDataHelper_getLinkFromDragEvent +FN:1585,Drop_enter +FN:1594,Drop_exit +FN:1609,Drop_drop +FN:1628,Drop_repinSitesAfterDrop +FN:1632,Drop_repinSitesAfterDrop/pinnedSites< +FN:1637,Drop_repinSitesAfterDrop/< +FN:1645,Drop_pinDraggedSite +FN:1669,Drop_delayedRearrange +FN:1676,callback +FN:1691,Drop_cancelDelayedArrange +FN:1702,Drop_rearrange +FN:1733,gDropTargetShim.init +FN:1740,gDropTargetShim._addEventListeners +FN:1752,gDropTargetShim._removeEventListeners +FN:1764,gDropTargetShim.handleEvent +FN:1788,gDropTargetShim._dragstart +FN:1799,gDropTargetShim._dragover +FN:1819,gDropTargetShim._drop +FN:1839,gDropTargetShim._dragend +FN:1862,gDropTargetShim._updateDropTarget +FN:1888,gDropTargetShim._findDropTarget +FN:1914,DropTargetShim_getCellPositions +FN:1918,DropTargetShim_getCellPositions/this._cellPositions< +FN:1929,gDropTargetShim._dispatchEvent +FN:1954,DropPreview_rearrange +FN:1972,DropPreview_insertDraggedSite +FN:1999,DropPreview_repositionPinnedSites +FN:2005,DropPreview_repositionPinnedSites/< +FN:2023,DropPreview_filterPinnedSites +FN:2030,DropPreview_filterPinnedSites/< +FN:2047,DropPreview_getPinnedRange +FN:2077,DropPreview_hasOverflowedPinnedSite +FN:2104,DropPreview_repositionOverflowedPinnedSite +FN:2135,DropPreview_indexOfLowerPrioritySite +FN:2170,Updater_updateGrid +FN:2177,Updater_updateGrid/< +FN:2189,Updater_updateGrid/</< +FN:2206,Updater_findRemainingSites +FN:2210,Updater_findRemainingSites/< +FN:2216,Updater_findRemainingSites/< +FN:2225,Updater_freezeSitePositions +FN:2226,Updater_freezeSitePositions/< +FN:2236,Updater_moveSiteNodes +FN:2244,Updater_moveSiteNodes/< +FN:2268,Updater_rearrangeSites +FN:2279,Updater_removeLegacySites +FN:2283,Updater_removeLegacySites/< +FN:2288,Updater_removeLegacySites/</< +FN:2290,Updater_removeLegacySites/</</< +FN:2308,Updater_fillEmptyCells +FN:2312,Updater_fillEmptyCells/< +FN:2316,Updater_fillEmptyCells/</< +FN:2351,UndoDialog_init +FN:2363,UndoDialog_show +FN:2383,UndoDialog_hide +FN:2399,UndoDialog_handleEvent +FN:2416,UndoDialog_undo +FN:2434,UndoDialog_undoAll +FN:2435,UndoDialog_undoAll/< +FN:2446,gSearch.init +FN:2448,gSearch.init/< +FN:2469,gCustomize.init +FN:2474,gCustomize.init/< +FN:2483,gCustomize.hidePanel +FN:2484,onTransitionEnd +FN:2495,gCustomize.showPanel +FN:2504,gCustomize.showPanel/< +FN:2517,gCustomize.handleEvent +FN:2528,gCustomize.onClick +FN:2554,gCustomize.onKeyDown +FN:2560,gCustomize.showLearn +FN:2565,gCustomize.updateSelected +FN:2568,gCustomize.updateSelected/< +FN:2602,gIntro.init +FN:2608,gIntro._showMessage +FN:2612,gIntro._showMessage/< +FN:2621,gIntro._bold +FN:2625,gIntro._link +FN:2629,gIntro._exitIntro +FN:2631,gIntro._exitIntro/< +FN:2636,gIntro._generateParagraphs +FN:2646,gIntro.showIfNecessary +FN:2656,gIntro.showPanel +FN:36,newTabString +FN:44,inPrivateBrowsingMode +FN:67,gTransformation._cellBorderWidths +FN:86,Transformation_getNodePosition +FN:96,Transformation_fadeNodeIn +FN:97,Transformation_fadeNodeIn/< +FN:111,Transformation_fadeNodeOut +FN:120,Transformation_showSite +FN:129,Transformation_hideSite +FN:138,Transformation_setSitePosition +FN:150,Transformation_freezeSitePosition +FN:167,Transformation_unfreezeSitePosition +FN:184,Transformation_slideSiteTo +FN:191,finish +FN:221,Transformation_rearrangeSites +FN:227,Transformation_rearrangeSites/< +FNDA:1,top-level +FNDA:1,top-level +FNDA:1,Page_init +FNDA:2,update +FNDA:1,Page_init +FNDA:1,Page_init/< +FNDA:1,Page_updateAttributes +FNDA:1,Page_handleUnloadEvent +FNDA:2,Page_handleEvent +FNDA:1,gPage.onPageFirstVisible +FNDA:1,onPageVisibleAndLoaded +FNDA:1,reportLastVisibleTileIndex +FNDA:2,gGrid.node +FNDA:13,gGrid.cells +FNDA:12,gGrid.sites +FNDA:1,gGrid.ready +FNDA:4,gGrid.isDocumentLoaded +FNDA:1,Grid_init +FNDA:1,Grid_init/< +FNDA:2,Grid_createSite +FNDA:1,Grid_handleEvent +FNDA:1,refresh +FNDA:2,_refreshGrid +FNDA:4,Grid_computeHeight +FNDA:1,Grid_createSiteFragment +FNDA:8,Grid_isHistoricalTile +FNDA:3,Grid_resizeGrid +FNDA:30,Cell +FNDA:120,Cell/< +FNDA:183,Cell.prototype.node +FNDA:180,Cell.prototype.site +FNDA:2,Site +FNDA:16,Site.prototype.node +FNDA:33,Site.prototype.link +FNDA:7,Site.prototype.url +FNDA:4,Site.prototype.title +FNDA:2,Site_isPinned +FNDA:12,Site_querySelector +FNDA:2,Site_checkLinkEndTime +FNDA:2,Site_render +FNDA:1,Site_onFirstVisible +FNDA:3,Site_captureIfMissing +FNDA:2,Site_refreshThumbnail +FNDA:4,Site.prototype._ignoreHoverEvents +FNDA:2,Site_addEventHandlers +FNDA:1,gDropTargetShim.init +FNDA:1,UndoDialog_init +FNDA:1,gSearch.init +FNDA:1,gCustomize.init +FNDA:1,gCustomize.updateSelected +FNDA:3,gCustomize.updateSelected/< +FNDA:1,gIntro.init +FNDA:1,gIntro.showIfNecessary +FNDA:3,newTabString +FNF:187 +FNH:54 +BRDA:233,0,0,- +BRDA:233,0,1,- +BRDA:236,1,0,- +BRDA:236,1,1,- +BRDA:263,0,0,- +BRDA:263,0,1,- +BRDA:289,0,0,- +BRDA:289,0,1,- +BRDA:290,1,0,- +BRDA:290,1,1,- +BRDA:293,2,0,- +BRDA:293,2,1,- +BRDA:348,0,0,- +BRDA:348,0,1,1 +BRDA:364,0,0,- +BRDA:364,0,1,- +BRDA:371,1,0,- +BRDA:371,1,1,- +BRDA:377,2,0,- +BRDA:377,2,1,- +BRDA:382,3,0,- +BRDA:382,3,1,- +BRDA:382,4,0,- +BRDA:382,4,1,- +BRDA:384,5,0,- +BRDA:384,5,1,- +BRDA:384,6,0,- +BRDA:384,6,1,- +BRDA:383,7,0,- +BRDA:383,7,1,- +BRDA:399,0,0,2 +BRDA:399,0,1,- +BRDA:401,1,0,- +BRDA:401,1,1,2 +BRDA:405,2,0,1 +BRDA:405,2,1,1 +BRDA:405,3,0,1 +BRDA:405,3,1,1 +BRDA:413,4,0,- +BRDA:413,4,1,- +BRDA:419,0,0,- +BRDA:419,0,1,- +BRDA:432,0,0,1 +BRDA:432,0,1,- +BRDA:440,1,0,1 +BRDA:440,1,1,- +BRDA:463,0,0,- +BRDA:463,0,1,2 +BRDA:462,1,0,2 +BRDA:462,1,1,1 +BRDA:472,2,0,- +BRDA:472,2,1,- +BRDA:471,3,0,- +BRDA:471,3,1,1 +BRDA:488,0,0,1 +BRDA:488,0,1,- +BRDA:500,0,0,1 +BRDA:500,0,1,1 +BRDA:500,1,0,1 +BRDA:500,1,1,- +BRDA:500,2,0,- +BRDA:500,2,1,- +BRDA:500,3,0,- +BRDA:500,3,1,- +BRDA:500,4,0,- +BRDA:500,4,1,- +BRDA:500,5,0,- +BRDA:500,5,1,- +BRDA:511,6,0,- +BRDA:511,6,1,- +BRDA:510,7,0,- +BRDA:510,7,1,- +BRDA:519,8,0,- +BRDA:519,8,1,- +BRDA:519,9,0,- +BRDA:519,9,1,- +BRDA:523,10,0,- +BRDA:523,10,1,- +BRDA:523,11,0,- +BRDA:523,11,1,- +BRDA:530,12,0,- +BRDA:530,12,1,- +BRDA:549,0,0,14 +BRDA:549,0,1,1 +BRDA:548,1,0,16 +BRDA:548,1,1,1 +BRDA:560,2,0,1 +BRDA:560,2,1,- +BRDA:588,0,0,1 +BRDA:588,0,1,22 +BRDA:588,1,0,15 +BRDA:588,1,1,8 +BRDA:589,2,0,7 +BRDA:589,2,1,1 +BRDA:591,3,0,1 +BRDA:591,3,1,- +BRDA:587,4,0,24 +BRDA:587,4,1,1 +BRDA:635,0,0,181 +BRDA:635,0,1,12 +BRDA:665,0,0,- +BRDA:665,0,1,1 +BRDA:686,0,0,1 +BRDA:686,0,1,- +BRDA:686,1,0,- +BRDA:686,1,1,- +BRDA:728,0,0,31 +BRDA:728,0,1,2 +BRDA:733,1,0,30 +BRDA:733,1,1,2 +BRDA:741,2,0,- +BRDA:741,2,1,2 +BRDA:740,3,0,2 +BRDA:740,3,1,2 +BRDA:757,0,0,2 +BRDA:757,0,1,2 +BRDA:793,0,0,8 +BRDA:793,0,1,- +BRDA:793,1,0,- +BRDA:793,1,1,- +BRDA:793,2,0,- +BRDA:793,2,1,- +BRDA:804,0,0,1 +BRDA:804,0,1,2 +BRDA:804,1,0,2 +BRDA:804,1,1,1 +BRDA:809,2,0,1 +BRDA:809,2,1,1 +BRDA:819,3,0,1 +BRDA:819,3,1,1 +BRDA:839,4,0,8 +BRDA:839,4,1,- +BRDA:838,5,0,9 +BRDA:838,5,1,2 +BRDA:909,0,0,- +BRDA:909,0,1,- +BRDA:922,0,0,- +BRDA:922,0,1,- +BRDA:935,0,0,168 +BRDA:935,0,1,12 +BRDA:944,0,0,- +BRDA:944,0,1,- +BRDA:961,0,0,- +BRDA:961,0,1,- +BRDA:961,1,0,- +BRDA:961,1,1,- +BRDA:964,2,0,- +BRDA:964,2,1,- +BRDA:964,3,0,- +BRDA:964,3,1,- +BRDA:967,4,0,- +BRDA:967,4,1,- +BRDA:967,5,0,- +BRDA:967,5,1,- +BRDA:967,6,0,- +BRDA:967,6,1,- +BRDA:967,7,0,- +BRDA:967,7,1,- +BRDA:1020,0,0,4 +BRDA:1020,0,1,- +BRDA:1027,0,0,- +BRDA:1027,0,1,- +BRDA:1036,0,0,- +BRDA:1036,0,1,- +BRDA:1041,1,0,- +BRDA:1041,1,1,- +BRDA:1052,0,0,- +BRDA:1052,0,1,- +BRDA:1072,0,0,- +BRDA:1072,0,1,- +BRDA:1096,0,0,- +BRDA:1096,0,1,- +BRDA:1108,0,0,- +BRDA:1108,0,1,- +BRDA:1119,0,0,- +BRDA:1119,0,1,- +BRDA:1129,0,0,2 +BRDA:1129,0,1,- +BRDA:1129,1,0,2 +BRDA:1129,1,1,- +BRDA:1151,0,0,- +BRDA:1151,0,1,2 +BRDA:1153,1,0,- +BRDA:1153,1,1,2 +BRDA:1153,2,0,- +BRDA:1153,2,1,2 +BRDA:1154,3,0,- +BRDA:1154,3,1,- +BRDA:1156,4,0,2 +BRDA:1156,4,1,- +BRDA:1165,5,0,- +BRDA:1165,5,1,2 +BRDA:1173,6,0,2 +BRDA:1173,6,1,- +BRDA:1174,7,0,- +BRDA:1174,7,1,- +BRDA:1185,8,0,2 +BRDA:1185,8,1,- +BRDA:1200,0,0,1 +BRDA:1200,0,1,- +BRDA:1200,1,0,1 +BRDA:1200,1,1,- +BRDA:1214,0,0,- +BRDA:1214,0,1,3 +BRDA:1214,1,0,- +BRDA:1214,1,1,3 +BRDA:1224,0,0,- +BRDA:1224,0,1,2 +BRDA:1224,1,0,2 +BRDA:1224,1,1,- +BRDA:1228,2,0,2 +BRDA:1228,2,1,- +BRDA:1232,3,0,- +BRDA:1232,3,1,2 +BRDA:1235,4,0,- +BRDA:1235,4,1,2 +BRDA:1239,5,0,2 +BRDA:1239,5,1,- +BRDA:1285,0,0,- +BRDA:1285,0,1,- +BRDA:1286,1,0,- +BRDA:1286,1,1,- +BRDA:1287,2,0,- +BRDA:1287,2,1,- +BRDA:1298,0,0,- +BRDA:1298,0,1,- +BRDA:1311,1,0,- +BRDA:1311,1,1,- +BRDA:1311,2,0,- +BRDA:1311,2,1,- +BRDA:1314,3,0,- +BRDA:1314,3,1,- +BRDA:1315,4,0,- +BRDA:1315,4,1,- +BRDA:1331,0,0,- +BRDA:1331,0,1,- +BRDA:1332,1,0,- +BRDA:1332,1,1,- +BRDA:1334,2,0,- +BRDA:1334,2,1,- +BRDA:1334,3,0,- +BRDA:1334,3,1,- +BRDA:1340,4,0,- +BRDA:1340,4,1,- +BRDA:1343,5,0,- +BRDA:1343,5,1,- +BRDA:1347,6,0,- +BRDA:1347,6,1,- +BRDA:1349,7,0,- +BRDA:1349,7,1,- +BRDA:1353,8,0,- +BRDA:1353,8,1,- +BRDA:1359,9,0,- +BRDA:1359,9,1,- +BRDA:1360,10,0,- +BRDA:1360,10,1,- +BRDA:1364,11,0,- +BRDA:1364,11,1,- +BRDA:1364,12,0,- +BRDA:1364,12,1,- +BRDA:1368,13,0,- +BRDA:1368,13,1,- +BRDA:1368,14,0,- +BRDA:1368,14,1,- +BRDA:1369,15,0,- +BRDA:1369,15,1,- +BRDA:1378,16,0,- +BRDA:1378,16,1,- +BRDA:1387,0,0,- +BRDA:1387,0,1,- +BRDA:1387,1,0,- +BRDA:1387,1,1,- +BRDA:1387,2,0,- +BRDA:1387,2,1,- +BRDA:1439,0,0,- +BRDA:1439,0,1,- +BRDA:1491,0,0,- +BRDA:1491,0,1,- +BRDA:1510,0,0,- +BRDA:1510,0,1,- +BRDA:1510,1,0,- +BRDA:1510,1,1,- +BRDA:1557,0,0,- +BRDA:1557,0,1,- +BRDA:1557,1,0,- +BRDA:1557,1,1,- +BRDA:1561,2,0,- +BRDA:1561,2,1,- +BRDA:1562,3,0,- +BRDA:1562,3,1,- +BRDA:1562,4,0,- +BRDA:1562,4,1,- +BRDA:1595,0,0,- +BRDA:1595,0,1,- +BRDA:1595,1,0,- +BRDA:1595,1,1,- +BRDA:1612,0,0,- +BRDA:1612,0,1,- +BRDA:1633,0,0,- +BRDA:1633,0,1,- +BRDA:1649,0,0,- +BRDA:1649,0,1,- +BRDA:1651,1,0,- +BRDA:1651,1,1,- +BRDA:1655,2,0,- +BRDA:1655,2,1,- +BRDA:1671,0,0,- +BRDA:1671,0,1,- +BRDA:1692,0,0,- +BRDA:1692,0,1,- +BRDA:1706,0,0,- +BRDA:1706,0,1,- +BRDA:1765,0,0,- +BRDA:1765,0,1,- +BRDA:1765,1,0,- +BRDA:1765,1,1,- +BRDA:1765,2,0,- +BRDA:1765,2,1,- +BRDA:1765,3,0,- +BRDA:1765,3,1,- +BRDA:1765,4,0,- +BRDA:1765,4,1,- +BRDA:1789,0,0,- +BRDA:1789,0,1,- +BRDA:1810,0,0,- +BRDA:1810,0,1,- +BRDA:1840,0,0,- +BRDA:1840,0,1,- +BRDA:1841,1,0,- +BRDA:1841,1,1,- +BRDA:1841,2,0,- +BRDA:1841,2,1,- +BRDA:1866,0,0,- +BRDA:1866,0,1,- +BRDA:1867,1,0,- +BRDA:1867,1,1,- +BRDA:1871,2,0,- +BRDA:1871,2,1,- +BRDA:1875,3,0,- +BRDA:1875,3,1,- +BRDA:1902,0,0,- +BRDA:1902,0,1,- +BRDA:1902,1,0,- +BRDA:1902,1,1,- +BRDA:1898,2,0,- +BRDA:1898,2,1,- +BRDA:1915,0,0,- +BRDA:1915,0,1,- +BRDA:1977,0,0,- +BRDA:1977,0,1,- +BRDA:1982,1,0,- +BRDA:1982,1,1,- +BRDA:2012,0,0,- +BRDA:2012,0,1,- +BRDA:2032,0,0,- +BRDA:2032,0,1,- +BRDA:2032,1,0,- +BRDA:2032,1,1,- +BRDA:2032,2,0,- +BRDA:2032,2,1,- +BRDA:2038,3,0,- +BRDA:2038,3,1,- +BRDA:2052,0,0,- +BRDA:2052,0,1,- +BRDA:2056,1,0,- +BRDA:2056,1,1,- +BRDA:2056,2,0,- +BRDA:2056,2,1,- +BRDA:2062,3,0,- +BRDA:2062,3,1,- +BRDA:2062,4,0,- +BRDA:2062,4,1,- +BRDA:2081,0,0,- +BRDA:2081,0,1,- +BRDA:2087,1,0,- +BRDA:2087,1,1,- +BRDA:2093,2,0,- +BRDA:2093,2,1,- +BRDA:2109,0,0,- +BRDA:2109,0,1,- +BRDA:2116,1,0,- +BRDA:2116,1,1,- +BRDA:2115,2,0,- +BRDA:2115,2,1,- +BRDA:2145,0,0,- +BRDA:2145,0,1,- +BRDA:2151,1,0,- +BRDA:2151,1,1,- +BRDA:2151,2,0,- +BRDA:2151,2,1,- +BRDA:2143,3,0,- +BRDA:2143,3,1,- +BRDA:2211,0,0,- +BRDA:2211,0,1,- +BRDA:2217,0,0,- +BRDA:2217,0,1,- +BRDA:2217,1,0,- +BRDA:2217,1,1,- +BRDA:2227,0,0,- +BRDA:2227,0,1,- +BRDA:2249,0,0,- +BRDA:2249,0,1,- +BRDA:2249,1,0,- +BRDA:2249,1,1,- +BRDA:2253,2,0,- +BRDA:2253,2,1,- +BRDA:2257,3,0,- +BRDA:2257,3,1,- +BRDA:2285,0,0,- +BRDA:2285,0,1,- +BRDA:2285,1,0,- +BRDA:2285,1,1,- +BRDA:2313,0,0,- +BRDA:2313,0,1,- +BRDA:2313,1,0,- +BRDA:2313,1,1,- +BRDA:2364,0,0,- +BRDA:2364,0,1,- +BRDA:2384,0,0,- +BRDA:2384,0,1,- +BRDA:2400,0,0,- +BRDA:2400,0,1,- +BRDA:2400,1,0,- +BRDA:2400,1,1,- +BRDA:2400,2,0,- +BRDA:2400,2,1,- +BRDA:2417,0,0,- +BRDA:2417,0,1,- +BRDA:2423,1,0,- +BRDA:2423,1,1,- +BRDA:2470,0,0,7 +BRDA:2470,0,1,1 +BRDA:2496,0,0,- +BRDA:2496,0,1,- +BRDA:2518,0,0,- +BRDA:2518,0,1,- +BRDA:2518,1,0,- +BRDA:2518,1,1,- +BRDA:2529,0,0,- +BRDA:2529,0,1,- +BRDA:2530,1,0,- +BRDA:2530,1,1,- +BRDA:2534,2,0,- +BRDA:2534,2,1,- +BRDA:2534,3,0,- +BRDA:2534,3,1,- +BRDA:2534,4,0,- +BRDA:2534,4,1,- +BRDA:2534,5,0,- +BRDA:2534,5,1,- +BRDA:2539,6,0,- +BRDA:2539,6,1,- +BRDA:2555,0,0,- +BRDA:2555,0,1,- +BRDA:2567,0,0,- +BRDA:2567,0,1,1 +BRDA:2567,1,0,- +BRDA:2567,1,1,1 +BRDA:2577,2,0,- +BRDA:2577,2,1,1 +BRDA:2570,0,0,2 +BRDA:2570,0,1,1 +BRDA:2603,0,0,6 +BRDA:2603,0,1,1 +BRDA:2647,0,0,1 +BRDA:2647,0,1,- +BRDA:2650,1,0,1 +BRDA:2650,1,1,- +BRDA:2660,0,0,- +BRDA:2660,0,1,- +BRDA:38,0,0,- +BRDA:38,0,1,3 +BRDA:101,0,0,- +BRDA:101,0,1,- +BRDA:151,0,0,- +BRDA:151,0,1,- +BRDA:168,0,0,- +BRDA:168,0,1,- +BRDA:187,0,0,- +BRDA:187,0,1,- +BRDA:204,1,0,- +BRDA:204,1,1,- +BRDA:205,2,0,- +BRDA:205,2,1,- +BRDA:192,0,0,- +BRDA:192,0,1,- +BRDA:192,1,0,- +BRDA:192,1,1,- +BRDA:195,2,0,- +BRDA:195,2,1,- +BRDA:224,0,0,- +BRDA:224,0,1,- +BRDA:225,1,0,- +BRDA:225,1,1,- +BRDA:246,2,0,- +BRDA:246,2,1,- +BRDA:229,0,0,- +BRDA:229,0,1,- +BRDA:229,1,0,- +BRDA:229,1,1,- +BRF:500 +BRH:90 +DA:7,1 +DA:8,1 +DA:23,1 +DA:24,1 +DA:25,1 +DA:26,1 +DA:27,1 +DA:28,1 +DA:36,1 +DA:48,1 +DA:49,1 +DA:51,1 +DA:52,1 +DA:53,1 +DA:62,1 +DA:324,1 +DA:330,1 +DA:607,1 +DA:608,1 +DA:609,1 +DA:614,1 +DA:870,1 +DA:1406,1 +DA:1550,1 +DA:1570,1 +DA:1575,1 +DA:1719,1 +DA:1947,1 +DA:2164,1 +DA:2337,1 +DA:2445,1 +DA:2456,1 +DA:2585,1 +DA:2586,1 +DA:2588,1 +DA:7,1 +DA:8,1 +DA:10,1 +DA:11,1 +DA:12,1 +DA:13,1 +DA:14,1 +DA:15,1 +DA:17,1 +DA:18,1 +DA:17,1 +DA:19,1 +DA:20,1 +DA:19,1 +DA:29,1 +DA:31,1 +DA:48,1 +DA:49,1 +DA:51,1 +DA:52,1 +DA:53,1 +DA:62,1 +DA:67,1 +DA:86,1 +DA:96,1 +DA:111,1 +DA:120,1 +DA:129,1 +DA:138,1 +DA:150,1 +DA:167,1 +DA:184,1 +DA:221,1 +DA:259,1 +DA:275,1 +DA:287,1 +DA:307,1 +DA:317,1 +DA:324,1 +DA:330,1 +DA:334,1 +DA:363,1 +DA:399,1 +DA:431,1 +DA:459,1 +DA:482,1 +DA:499,1 +DA:544,1 +DA:567,1 +DA:575,1 +DA:607,1 +DA:608,1 +DA:609,1 +DA:614,1 +DA:618,1 +DA:619,1 +DA:624,1 +DA:629,1 +DA:630,1 +DA:635,1 +DA:638,1 +DA:641,1 +DA:647,1 +DA:676,1 +DA:685,1 +DA:697,1 +DA:704,1 +DA:714,1 +DA:722,1 +DA:755,1 +DA:764,1 +DA:791,1 +DA:799,1 +DA:881,1 +DA:885,1 +DA:890,1 +DA:895,1 +DA:907,1 +DA:920,1 +DA:933,1 +DA:942,1 +DA:951,1 +DA:958,1 +DA:1001,1 +DA:1005,1 +DA:1010,1 +DA:1015,1 +DA:1020,1 +DA:1025,1 +DA:1035,1 +DA:1051,1 +DA:1063,1 +DA:1071,1 +DA:1084,1 +DA:1093,1 +DA:1105,1 +DA:1116,1 +DA:1128,1 +DA:1147,1 +DA:1199,1 +DA:1213,1 +DA:1222,1 +DA:1246,1 +DA:1258,1 +DA:1275,1 +DA:1284,1 +DA:1296,1 +DA:1324,1 +DA:1386,1 +DA:1406,1 +DA:1410,1 +DA:1411,1 +DA:1416,1 +DA:1417,1 +DA:1422,1 +DA:1423,1 +DA:1424,1 +DA:1425,1 +DA:1432,1 +DA:1465,1 +DA:1489,1 +DA:1505,1 +DA:1524,1 +DA:1550,1 +DA:1551,1 +DA:1555,1 +DA:1570,1 +DA:1575,1 +DA:1579,1 +DA:1585,1 +DA:1594,1 +DA:1609,1 +DA:1628,1 +DA:1645,1 +DA:1669,1 +DA:1691,1 +DA:1702,1 +DA:1719,1 +DA:1723,1 +DA:1728,1 +DA:1733,1 +DA:1740,1 +DA:1752,1 +DA:1764,1 +DA:1788,1 +DA:1799,1 +DA:1819,1 +DA:1839,1 +DA:1862,1 +DA:1888,1 +DA:1914,1 +DA:1929,1 +DA:1947,1 +DA:1954,1 +DA:1972,1 +DA:1999,1 +DA:2023,1 +DA:2047,1 +DA:2077,1 +DA:2104,1 +DA:2135,1 +DA:2164,1 +DA:2170,1 +DA:2206,1 +DA:2225,1 +DA:2236,1 +DA:2268,1 +DA:2279,1 +DA:2308,1 +DA:2337,1 +DA:2341,1 +DA:2346,1 +DA:2351,1 +DA:2363,1 +DA:2383,1 +DA:2399,1 +DA:2416,1 +DA:2434,1 +DA:2442,1 +DA:2445,1 +DA:2446,1 +DA:2456,1 +DA:2457,1 +DA:2467,1 +DA:2469,1 +DA:2483,1 +DA:2495,1 +DA:2517,1 +DA:2528,1 +DA:2554,1 +DA:2560,1 +DA:2565,1 +DA:2585,1 +DA:2586,1 +DA:2588,1 +DA:2589,1 +DA:2598,1 +DA:2600,1 +DA:2602,1 +DA:2608,1 +DA:2621,1 +DA:2625,1 +DA:2629,1 +DA:2636,1 +DA:2646,1 +DA:2656,1 +DA:2677,1 +DA:32,1 +DA:33,1 +DA:32,1 +DA:233,0 +DA:235,0 +DA:236,0 +DA:238,0 +DA:241,0 +DA:261,0 +DA:262,0 +DA:263,0 +DA:264,0 +DA:265,0 +DA:276,0 +DA:277,0 +DA:289,0 +DA:290,0 +DA:291,0 +DA:293,0 +DA:294,0 +DA:297,0 +DA:308,0 +DA:309,0 +DA:318,0 +DA:336,1 +DA:339,1 +DA:344,1 +DA:347,1 +DA:348,1 +DA:349,1 +DA:351,1 +DA:354,1 +DA:357,1 +DA:364,0 +DA:365,0 +DA:367,0 +DA:368,0 +DA:371,0 +DA:372,0 +DA:373,0 +DA:377,0 +DA:378,0 +DA:380,0 +DA:382,0 +DA:383,0 +DA:384,0 +DA:385,0 +DA:383,0 +DA:401,2 +DA:405,2 +DA:406,1 +DA:409,2 +DA:413,0 +DA:414,0 +DA:417,0 +DA:424,0 +DA:417,0 +DA:419,0 +DA:420,0 +DA:423,0 +DA:432,1 +DA:433,0 +DA:435,1 +DA:438,1 +DA:440,1 +DA:441,0 +DA:443,1 +DA:447,1 +DA:450,1 +DA:461,1 +DA:462,1 +DA:463,2 +DA:464,2 +DA:466,0 +DA:462,3 +DA:470,1 +DA:471,1 +DA:472,0 +DA:473,0 +DA:475,0 +DA:471,1 +DA:483,1 +DA:487,1 +DA:488,1 +DA:489,0 +DA:492,1 +DA:500,2 +DA:502,1 +DA:505,1 +DA:508,0 +DA:511,0 +DA:512,0 +DA:515,0 +DA:510,0 +DA:519,0 +DA:520,0 +DA:523,0 +DA:524,0 +DA:525,0 +DA:530,0 +DA:531,0 +DA:532,0 +DA:535,0 +DA:538,0 +DA:539,0 +DA:546,1 +DA:548,1 +DA:549,15 +DA:553,1 +DA:548,17 +DA:558,1 +DA:560,1 +DA:561,0 +DA:563,1 +DA:569,1 +DA:572,1 +DA:576,1 +DA:577,1 +DA:576,1 +DA:579,1 +DA:580,1 +DA:581,1 +DA:580,1 +DA:583,1 +DA:584,1 +DA:585,1 +DA:587,1 +DA:588,23 +DA:589,8 +DA:590,1 +DA:591,1 +DA:593,0 +DA:587,25 +DA:599,1 +DA:648,1 +DA:649,1 +DA:651,1 +DA:665,1 +DA:666,1 +DA:652,1 +DA:653,1 +DA:659,1 +DA:661,1 +DA:677,2 +DA:678,2 +DA:679,2 +DA:686,1 +DA:689,1 +DA:698,0 +DA:705,0 +DA:715,1 +DA:716,1 +DA:723,2 +DA:724,2 +DA:727,2 +DA:728,2 +DA:729,30 +DA:728,30 +DA:733,2 +DA:736,2 +DA:739,2 +DA:740,2 +DA:741,2 +DA:742,2 +DA:740,2 +DA:746,2 +DA:747,2 +DA:748,2 +DA:756,4 +DA:757,4 +DA:758,4 +DA:765,1 +DA:766,1 +DA:767,1 +DA:770,1 +DA:771,1 +DA:777,1 +DA:779,1 +DA:783,1 +DA:784,1 +DA:792,8 +DA:793,8 +DA:804,3 +DA:805,1 +DA:809,2 +DA:810,1 +DA:811,1 +DA:812,1 +DA:813,1 +DA:814,1 +DA:817,2 +DA:819,2 +DA:820,1 +DA:821,1 +DA:825,2 +DA:826,2 +DA:827,2 +DA:830,2 +DA:832,2 +DA:833,2 +DA:832,2 +DA:835,2 +DA:837,2 +DA:839,8 +DA:842,8 +DA:838,11 +DA:849,2 +DA:856,2 +DA:857,2 +DA:858,2 +DA:859,2 +DA:860,2 +DA:871,30 +DA:872,30 +DA:873,30 +DA:876,30 +DA:878,30 +DA:876,30 +DA:877,120 +DA:896,0 +DA:899,0 +DA:901,0 +DA:908,0 +DA:909,0 +DA:912,0 +DA:914,0 +DA:921,0 +DA:922,0 +DA:925,0 +DA:927,0 +DA:934,180 +DA:935,180 +DA:943,0 +DA:944,0 +DA:952,0 +DA:961,0 +DA:962,0 +DA:964,0 +DA:965,0 +DA:967,0 +DA:969,0 +DA:970,0 +DA:973,0 +DA:976,0 +DA:979,0 +DA:980,0 +DA:992,2 +DA:993,2 +DA:995,2 +DA:997,2 +DA:998,2 +DA:1026,0 +DA:1027,0 +DA:1036,0 +DA:1037,0 +DA:1039,0 +DA:1040,0 +DA:1041,0 +DA:1043,0 +DA:1045,0 +DA:1052,0 +DA:1053,0 +DA:1054,0 +DA:1055,0 +DA:1064,2 +DA:1072,0 +DA:1073,0 +DA:1074,0 +DA:1075,0 +DA:1085,12 +DA:1094,0 +DA:1096,0 +DA:1097,0 +DA:1098,0 +DA:1100,0 +DA:1101,0 +DA:1106,0 +DA:1107,0 +DA:1109,0 +DA:1110,0 +DA:1111,0 +DA:1108,0 +DA:1113,0 +DA:1117,0 +DA:1118,0 +DA:1119,0 +DA:1120,0 +DA:1122,0 +DA:1129,2 +DA:1130,0 +DA:1132,0 +DA:1134,0 +DA:1135,0 +DA:1137,0 +DA:1139,0 +DA:1140,0 +DA:1149,2 +DA:1151,2 +DA:1152,2 +DA:1153,2 +DA:1154,0 +DA:1155,0 +DA:1156,2 +DA:1158,2 +DA:1159,2 +DA:1160,2 +DA:1161,2 +DA:1163,2 +DA:1164,2 +DA:1165,2 +DA:1166,2 +DA:1171,2 +DA:1173,2 +DA:1174,0 +DA:1175,0 +DA:1176,0 +DA:1179,0 +DA:1180,0 +DA:1181,0 +DA:1182,0 +DA:1185,2 +DA:1186,0 +DA:1189,2 +DA:1191,2 +DA:1200,1 +DA:1202,0 +DA:1205,1 +DA:1214,3 +DA:1215,3 +DA:1224,2 +DA:1225,0 +DA:1227,2 +DA:1228,2 +DA:1229,0 +DA:1232,2 +DA:1233,2 +DA:1235,2 +DA:1236,2 +DA:1237,2 +DA:1239,2 +DA:1240,0 +DA:1241,0 +DA:1247,4 +DA:1250,4 +DA:1248,0 +DA:1251,0 +DA:1260,2 +DA:1261,2 +DA:1262,2 +DA:1266,2 +DA:1267,2 +DA:1268,2 +DA:1269,2 +DA:1276,0 +DA:1277,0 +DA:1278,0 +DA:1285,0 +DA:1286,0 +DA:1287,0 +DA:1290,0 +DA:1292,0 +DA:1293,0 +DA:1292,0 +DA:1297,0 +DA:1298,0 +DA:1299,0 +DA:1300,0 +DA:1302,0 +DA:1304,0 +DA:1305,0 +DA:1306,0 +DA:1307,0 +DA:1309,0 +DA:1310,0 +DA:1311,0 +DA:1312,0 +DA:1313,0 +DA:1314,0 +DA:1315,0 +DA:1317,0 +DA:1325,0 +DA:1326,0 +DA:1327,0 +DA:1328,0 +DA:1331,0 +DA:1332,0 +DA:1334,0 +DA:1335,0 +DA:1336,0 +DA:1340,0 +DA:1341,0 +DA:1343,0 +DA:1344,0 +DA:1347,0 +DA:1348,0 +DA:1349,0 +DA:1353,0 +DA:1354,0 +DA:1356,0 +DA:1357,0 +DA:1359,0 +DA:1360,0 +DA:1361,0 +DA:1362,0 +DA:1364,0 +DA:1365,0 +DA:1366,0 +DA:1368,0 +DA:1369,0 +DA:1371,0 +DA:1373,0 +DA:1378,0 +DA:1379,0 +DA:1387,0 +DA:1389,0 +DA:1390,0 +DA:1393,0 +DA:1396,0 +DA:1433,0 +DA:1436,0 +DA:1437,0 +DA:1438,0 +DA:1439,0 +DA:1440,0 +DA:1439,0 +DA:1442,0 +DA:1444,0 +DA:1447,0 +DA:1448,0 +DA:1449,0 +DA:1450,0 +DA:1453,0 +DA:1454,0 +DA:1455,0 +DA:1457,0 +DA:1467,0 +DA:1470,0 +DA:1473,0 +DA:1474,0 +DA:1477,0 +DA:1478,0 +DA:1481,0 +DA:1490,0 +DA:1491,0 +DA:1492,0 +DA:1491,0 +DA:1495,0 +DA:1497,0 +DA:1506,0 +DA:1510,0 +DA:1511,0 +DA:1516,0 +DA:1525,0 +DA:1527,0 +DA:1528,0 +DA:1529,0 +DA:1530,0 +DA:1531,0 +DA:1532,0 +DA:1533,0 +DA:1537,0 +DA:1538,0 +DA:1539,0 +DA:1540,0 +DA:1541,0 +DA:1545,0 +DA:1552,0 +DA:1556,0 +DA:1557,0 +DA:1558,0 +DA:1561,0 +DA:1562,0 +DA:1563,0 +DA:1586,0 +DA:1595,0 +DA:1596,0 +DA:1599,0 +DA:1600,0 +DA:1612,0 +DA:1613,0 +DA:1616,0 +DA:1618,0 +DA:1621,0 +DA:1629,0 +DA:1632,0 +DA:1637,0 +DA:1633,0 +DA:1646,0 +DA:1647,0 +DA:1649,0 +DA:1651,0 +DA:1652,0 +DA:1653,0 +DA:1654,0 +DA:1655,0 +DA:1657,0 +DA:1660,0 +DA:1676,0 +DA:1671,0 +DA:1672,0 +DA:1674,0 +DA:1681,0 +DA:1682,0 +DA:1685,0 +DA:1677,0 +DA:1678,0 +DA:1692,0 +DA:1693,0 +DA:1694,0 +DA:1703,0 +DA:1706,0 +DA:1707,0 +DA:1709,0 +DA:1734,1 +DA:1741,0 +DA:1743,0 +DA:1744,0 +DA:1745,0 +DA:1746,0 +DA:1753,0 +DA:1755,0 +DA:1756,0 +DA:1757,0 +DA:1758,0 +DA:1765,0 +DA:1767,0 +DA:1770,0 +DA:1773,0 +DA:1776,0 +DA:1779,0 +DA:1789,0 +DA:1790,0 +DA:1791,0 +DA:1802,0 +DA:1803,0 +DA:1806,0 +DA:1810,0 +DA:1811,0 +DA:1821,0 +DA:1825,0 +DA:1829,0 +DA:1832,0 +DA:1840,0 +DA:1841,0 +DA:1843,0 +DA:1844,0 +DA:1848,0 +DA:1849,0 +DA:1852,0 +DA:1853,0 +DA:1854,0 +DA:1864,0 +DA:1866,0 +DA:1867,0 +DA:1869,0 +DA:1871,0 +DA:1873,0 +DA:1875,0 +DA:1877,0 +DA:1879,0 +DA:1891,0 +DA:1892,0 +DA:1894,0 +DA:1895,0 +DA:1898,0 +DA:1899,0 +DA:1902,0 +DA:1903,0 +DA:1898,0 +DA:1907,0 +DA:1915,0 +DA:1916,0 +DA:1918,0 +DA:1919,0 +DA:1930,0 +DA:1931,0 +DA:1934,0 +DA:1935,0 +DA:1934,0 +DA:1937,0 +DA:1955,0 +DA:1958,0 +DA:1962,0 +DA:1964,0 +DA:1973,0 +DA:1974,0 +DA:1977,0 +DA:1978,0 +DA:1979,0 +DA:1982,0 +DA:1983,0 +DA:1984,0 +DA:1988,0 +DA:2002,0 +DA:2005,0 +DA:2008,0 +DA:2005,0 +DA:2012,0 +DA:2013,0 +DA:2006,0 +DA:2007,0 +DA:2024,0 +DA:2028,0 +DA:2030,0 +DA:2032,0 +DA:2033,0 +DA:2035,0 +DA:2038,0 +DA:2048,0 +DA:2049,0 +DA:2052,0 +DA:2053,0 +DA:2057,0 +DA:2056,0 +DA:2059,0 +DA:2063,0 +DA:2062,0 +DA:2066,0 +DA:2081,0 +DA:2082,0 +DA:2084,0 +DA:2087,0 +DA:2088,0 +DA:2090,0 +DA:2093,0 +DA:2107,0 +DA:2109,0 +DA:2110,0 +DA:2111,0 +DA:2115,0 +DA:2116,0 +DA:2117,0 +DA:2118,0 +DA:2115,0 +DA:2123,0 +DA:2137,0 +DA:2138,0 +DA:2143,0 +DA:2145,0 +DA:2146,0 +DA:2148,0 +DA:2151,0 +DA:2152,0 +DA:2143,0 +DA:2155,0 +DA:2171,0 +DA:2174,0 +DA:2177,0 +DA:2180,0 +DA:2185,0 +DA:2189,0 +DA:2191,0 +DA:2194,0 +DA:2207,0 +DA:2210,0 +DA:2216,0 +DA:2211,0 +DA:2212,0 +DA:2217,0 +DA:2226,0 +DA:2227,0 +DA:2228,0 +DA:2237,0 +DA:2242,0 +DA:2244,0 +DA:2260,0 +DA:2244,0 +DA:2245,0 +DA:2246,0 +DA:2249,0 +DA:2250,0 +DA:2253,0 +DA:2254,0 +DA:2257,0 +DA:2258,0 +DA:2269,0 +DA:2270,0 +DA:2280,0 +DA:2283,0 +DA:2300,0 +DA:2285,0 +DA:2286,0 +DA:2288,0 +DA:2290,0 +DA:2291,0 +DA:2294,0 +DA:2295,0 +DA:2309,0 +DA:2312,0 +DA:2328,0 +DA:2312,0 +DA:2328,0 +DA:2312,0 +DA:2313,0 +DA:2314,0 +DA:2316,0 +DA:2318,0 +DA:2321,0 +DA:2325,0 +DA:2326,0 +DA:2352,1 +DA:2353,1 +DA:2354,1 +DA:2355,1 +DA:2356,1 +DA:2364,0 +DA:2365,0 +DA:2367,0 +DA:2368,0 +DA:2369,0 +DA:2370,0 +DA:2371,0 +DA:2374,0 +DA:2375,0 +DA:2376,0 +DA:2377,0 +DA:2384,0 +DA:2385,0 +DA:2387,0 +DA:2388,0 +DA:2389,0 +DA:2390,0 +DA:2391,0 +DA:2392,0 +DA:2400,0 +DA:2402,0 +DA:2405,0 +DA:2408,0 +DA:2417,0 +DA:2418,0 +DA:2420,0 +DA:2421,0 +DA:2423,0 +DA:2424,0 +DA:2427,0 +DA:2428,0 +DA:2435,0 +DA:2438,0 +DA:2435,0 +DA:2436,0 +DA:2437,0 +DA:2447,1 +DA:2448,1 +DA:2447,1 +DA:2449,1 +DA:2450,1 +DA:2451,1 +DA:2470,1 +DA:2471,7 +DA:2470,8 +DA:2474,1 +DA:2475,1 +DA:2476,1 +DA:2477,1 +DA:2478,1 +DA:2480,1 +DA:2484,0 +DA:2488,0 +DA:2489,0 +DA:2490,0 +DA:2491,0 +DA:2492,0 +DA:2485,0 +DA:2486,0 +DA:2496,0 +DA:2497,0 +DA:2500,0 +DA:2501,0 +DA:2502,0 +DA:2503,0 +DA:2504,0 +DA:2507,0 +DA:2504,0 +DA:2509,0 +DA:2510,0 +DA:2514,0 +DA:2506,0 +DA:2518,0 +DA:2520,0 +DA:2523,0 +DA:2529,0 +DA:2530,0 +DA:2531,0 +DA:2534,0 +DA:2536,0 +DA:2539,0 +DA:2540,0 +DA:2542,0 +DA:2546,0 +DA:2549,0 +DA:2555,0 +DA:2556,0 +DA:2561,0 +DA:2562,0 +DA:2566,1 +DA:2567,1 +DA:2568,1 +DA:2577,1 +DA:2579,1 +DA:2569,3 +DA:2570,3 +DA:2571,1 +DA:2574,2 +DA:2603,1 +DA:2604,6 +DA:2603,7 +DA:2610,0 +DA:2612,0 +DA:2617,0 +DA:2618,0 +DA:2617,0 +DA:2613,0 +DA:2622,0 +DA:2626,0 +DA:2630,0 +DA:2631,0 +DA:2632,0 +DA:2637,0 +DA:2638,0 +DA:2639,0 +DA:2640,0 +DA:2641,0 +DA:2642,0 +DA:2639,0 +DA:2647,1 +DA:2648,0 +DA:2650,1 +DA:2651,0 +DA:2652,0 +DA:2657,0 +DA:2658,0 +DA:2660,0 +DA:2662,0 +DA:2664,0 +DA:2667,0 +DA:2670,0 +DA:2671,0 +DA:37,3 +DA:38,3 +DA:39,3 +DA:41,0 +DA:45,0 +DA:68,0 +DA:69,0 +DA:70,0 +DA:71,0 +DA:75,0 +DA:76,0 +DA:75,0 +DA:78,0 +DA:87,0 +DA:88,0 +DA:97,0 +DA:99,0 +DA:101,0 +DA:102,0 +DA:112,0 +DA:121,0 +DA:130,0 +DA:139,0 +DA:140,0 +DA:142,0 +DA:143,0 +DA:151,0 +DA:152,0 +DA:154,0 +DA:155,0 +DA:156,0 +DA:157,0 +DA:159,0 +DA:160,0 +DA:168,0 +DA:169,0 +DA:171,0 +DA:172,0 +DA:173,0 +DA:191,0 +DA:185,0 +DA:186,0 +DA:187,0 +DA:189,0 +DA:200,0 +DA:201,0 +DA:204,0 +DA:205,0 +DA:206,0 +DA:208,0 +DA:209,0 +DA:192,0 +DA:193,0 +DA:195,0 +DA:196,0 +DA:222,0 +DA:223,0 +DA:224,0 +DA:225,0 +DA:227,0 +DA:244,0 +DA:227,0 +DA:246,0 +DA:247,0 +DA:229,0 +DA:230,0 +DA:232,0 +LF:1146 +LH:478 +end_of_record diff --git a/python/mozbuild/mozbuild/test/codecoverage/test_lcov_rewrite.py b/python/mozbuild/mozbuild/test/codecoverage/test_lcov_rewrite.py new file mode 100644 index 0000000000..533e9960c5 --- /dev/null +++ b/python/mozbuild/mozbuild/test/codecoverage/test_lcov_rewrite.py @@ -0,0 +1,444 @@ +# 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/. + +import json +import os +import shutil +import unittest +from io import StringIO +from tempfile import NamedTemporaryFile + +import buildconfig +import mozunit + +from mozbuild.codecoverage import chrome_map, lcov_rewriter + +here = os.path.dirname(__file__) + +BUILDCONFIG = { + "topobjdir": buildconfig.topobjdir, + "MOZ_APP_NAME": buildconfig.substs.get("MOZ_APP_NAME", "nightly"), + "OMNIJAR_NAME": buildconfig.substs.get("OMNIJAR_NAME", "omni.ja"), + "MOZ_MACBUNDLE_NAME": buildconfig.substs.get("MOZ_MACBUNDLE_NAME", "Nightly.app"), +} + +basic_file = """TN:Compartment_5f7f5c30251800 +SF:resource://gre/modules/osfile.jsm +FN:1,top-level +FNDA:1,top-level +FNF:1 +FNH:1 +BRDA:9,0,61,1 +BRF:1 +BRH:1 +DA:9,1 +DA:24,1 +LF:2 +LH:2 +end_of_record +""" + +# These line numbers are (synthetically) sorted. +multiple_records = """SF:resource://gre/modules/workers/require.js +FN:1,top-level +FN:80,.get +FN:95,require +FNDA:1,top-level +FNF:3 +FNH:1 +BRDA:46,0,16,- +BRDA:135,225,446,- +BRF:2 +BRH:0 +DA:43,1 +DA:46,1 +DA:152,0 +DA:163,1 +LF:4 +LH:3 +end_of_record +SF:resource://gre/modules/osfile/osfile_async_worker.js +FN:12,top-level +FN:30,worker.dispatch +FN:34,worker.postMessage +FN:387,do_close +FN:392,exists +FN:394,do_exists +FN:400,unixSymLink +FNDA:1,do_exists +FNDA:1,exists +FNDA:1,top-level +FNDA:594,worker.dispatch +FNF:7 +FNH:4 +BRDA:6,0,30,1 +BRDA:365,0,103,- +BRF:2 +BRH:1 +DA:6,1 +DA:7,0 +DA:12,1 +DA:18,1 +DA:19,1 +DA:20,1 +DA:23,1 +DA:25,1 +DA:401,0 +DA:407,1 +LF:10 +LH:8 +end_of_record +""" + +fn_with_multiple_commas = """TN:Compartment_5f7f5c30251800 +SF:resource://gre/modules/osfile.jsm +FN:1,function,name,with,commas +FNDA:1,function,name,with,commas +FNF:1 +FNH:1 +BRDA:9,0,61,1 +BRF:1 +BRH:1 +DA:9,1 +DA:24,1 +LF:2 +LH:2 +end_of_record +""" + + +class TempFile: + def __init__(self, content): + self.file = NamedTemporaryFile(mode="w", delete=False, encoding="utf-8") + self.file.write(content) + self.file.close() + + def __enter__(self): + return self.file.name + + def __exit__(self, *args): + os.remove(self.file.name) + + +class TestLcovParser(unittest.TestCase): + def parser_roundtrip(self, lcov_string): + with TempFile(lcov_string) as fname: + file_obj = lcov_rewriter.LcovFile([fname]) + out = StringIO() + file_obj.print_file(out, lambda s: (s, None), lambda x, pp: x) + + return out.getvalue() + + def test_basic_parse(self): + output = self.parser_roundtrip(basic_file) + self.assertEqual(basic_file, output) + + output = self.parser_roundtrip(multiple_records) + self.assertEqual(multiple_records, output) + + def test_multiple_commas(self): + output = self.parser_roundtrip(fn_with_multiple_commas) + self.assertEqual(fn_with_multiple_commas, output) + + +multiple_included_files = """//@line 1 "/src/dir/foo.js" +bazfoobar +//@line 2 "/src/dir/path/bar.js" +@foo@ +//@line 3 "/src/dir/foo.js" +bazbarfoo +//@line 2 "/src/dir/path/bar.js" +foobarbaz +//@line 3 "/src/dir/path2/test.js" +barfoobaz +//@line 1 "/src/dir/path/baz.js" +baz +//@line 6 "/src/dir/f.js" +fin +""" + +srcdir_prefix_files = """//@line 1 "/src/dir/foo.js" +bazfoobar +//@line 2 "$SRCDIR/path/file.js" +@foo@ +//@line 3 "/src/dir/foo.js" +bazbarfoo +""" + + +class TestLineRemapping(unittest.TestCase): + def setUp(self): + chrome_map_file = os.path.join(buildconfig.topobjdir, "chrome-map.json") + self._old_chrome_info_file = None + if os.path.isfile(chrome_map_file): + backup_file = os.path.join(buildconfig.topobjdir, "chrome-map-backup.json") + self._old_chrome_info_file = backup_file + self._chrome_map_file = chrome_map_file + shutil.move(chrome_map_file, backup_file) + + empty_chrome_info = [ + {}, + {}, + {}, + BUILDCONFIG, + ] + with open(chrome_map_file, "w") as fh: + json.dump(empty_chrome_info, fh) + + self.lcov_rewriter = lcov_rewriter.LcovFileRewriter(chrome_map_file, "", "", []) + self.pp_rewriter = self.lcov_rewriter.pp_rewriter + + def tearDown(self): + if self._old_chrome_info_file: + shutil.move(self._old_chrome_info_file, self._chrome_map_file) + + def test_map_multiple_included(self): + with TempFile(multiple_included_files) as fname: + actual = chrome_map.generate_pp_info(fname, "/src/dir") + expected = { + "2,3": ("foo.js", 1), + "4,5": ("path/bar.js", 2), + "6,7": ("foo.js", 3), + "8,9": ("path/bar.js", 2), + "10,11": ("path2/test.js", 3), + "12,13": ("path/baz.js", 1), + "14,15": ("f.js", 6), + } + + self.assertEqual(actual, expected) + + def test_map_srcdir_prefix(self): + with TempFile(srcdir_prefix_files) as fname: + actual = chrome_map.generate_pp_info(fname, "/src/dir") + expected = { + "2,3": ("foo.js", 1), + "4,5": ("path/file.js", 2), + "6,7": ("foo.js", 3), + } + + self.assertEqual(actual, expected) + + def test_remap_lcov(self): + pp_remap = { + "1941,2158": ("dropPreview.js", 6), + "2159,2331": ("updater.js", 6), + "2584,2674": ("intro.js", 6), + "2332,2443": ("undo.js", 6), + "864,985": ("cells.js", 6), + "2444,2454": ("search.js", 6), + "1567,1712": ("drop.js", 6), + "2455,2583": ("customize.js", 6), + "1713,1940": ("dropTargetShim.js", 6), + "1402,1548": ("drag.js", 6), + "1549,1566": ("dragDataHelper.js", 6), + "453,602": ("page.js", 141), + "2675,2678": ("newTab.js", 70), + "56,321": ("transformations.js", 6), + "603,863": ("grid.js", 6), + "322,452": ("page.js", 6), + "986,1401": ("sites.js", 6), + } + + fpath = os.path.join(here, "sample_lcov.info") + + # Read original records + lcov_file = lcov_rewriter.LcovFile([fpath]) + records = [lcov_file.parse_record(r) for _, _, r in lcov_file.iterate_records()] + + # This summarization changes values due multiple reports per line coming + # from the JS engine (bug 1198356). + for r in records: + r.resummarize() + original_line_count = r.line_count + original_covered_line_count = r.covered_line_count + original_function_count = r.function_count + original_covered_function_count = r.covered_function_count + + self.assertEqual(len(records), 1) + + # Rewrite preprocessed entries. + lcov_file = lcov_rewriter.LcovFile([fpath]) + r_num = [] + + def rewrite_source(s): + r_num.append(1) + return s, pp_remap + + out = StringIO() + lcov_file.print_file(out, rewrite_source, self.pp_rewriter.rewrite_record) + self.assertEqual(len(r_num), 1) + + # Read rewritten lcov. + with TempFile(out.getvalue()) as fname: + lcov_file = lcov_rewriter.LcovFile([fname]) + records = [ + lcov_file.parse_record(r) for _, _, r in lcov_file.iterate_records() + ] + + self.assertEqual(len(records), 17) + + # Lines/functions are only "moved" between records, not duplicated or omited. + self.assertEqual(original_line_count, sum(r.line_count for r in records)) + self.assertEqual( + original_covered_line_count, sum(r.covered_line_count for r in records) + ) + self.assertEqual( + original_function_count, sum(r.function_count for r in records) + ) + self.assertEqual( + original_covered_function_count, + sum(r.covered_function_count for r in records), + ) + + +class TestUrlFinder(unittest.TestCase): + def setUp(self): + chrome_map_file = os.path.join(buildconfig.topobjdir, "chrome-map.json") + self._old_chrome_info_file = None + if os.path.isfile(chrome_map_file): + backup_file = os.path.join(buildconfig.topobjdir, "chrome-map-backup.json") + self._old_chrome_info_file = backup_file + self._chrome_map_file = chrome_map_file + shutil.move(chrome_map_file, backup_file) + + dummy_chrome_info = [ + { + "resource://activity-stream/": [ + "dist/bin/browser/chrome/browser/res/activity-stream", + ], + "chrome://browser/content/": [ + "dist/bin/browser/chrome/browser/content/browser", + ], + }, + { + "chrome://global/content/license.html": "chrome://browser/content/license.html", + }, + { + "dist/bin/components/MainProcessSingleton.js": ["path1", None], + "dist/bin/browser/features/firefox@getpocket.com/bootstrap.js": [ + "path4", + None, + ], + "dist/bin/modules/osfile/osfile_async_worker.js": [ + "toolkit/components/osfile/modules/osfile_async_worker.js", + None, + ], + "dist/bin/browser/chrome/browser/res/activity-stream/lib/": [ + "browser/components/newtab/lib/*", + None, + ], + "dist/bin/browser/chrome/browser/content/browser/license.html": [ + "browser/base/content/license.html", + None, + ], + "dist/bin/modules/AppConstants.sys.mjs": [ + "toolkit/modules/AppConstants.sys.mjs", + { + "101,102": ["toolkit/modules/AppConstants.sys.mjs", 135], + }, + ], + }, + BUILDCONFIG, + ] + with open(chrome_map_file, "w") as fh: + json.dump(dummy_chrome_info, fh) + + def tearDown(self): + if self._old_chrome_info_file: + shutil.move(self._old_chrome_info_file, self._chrome_map_file) + + def test_jar_paths(self): + app_name = BUILDCONFIG["MOZ_APP_NAME"] + omnijar_name = BUILDCONFIG["OMNIJAR_NAME"] + + paths = [ + ( + "jar:file:///home/worker/workspace/build/application/" + + app_name + + "/" + + omnijar_name + + "!/components/MainProcessSingleton.js", + "path1", + ), + ( + "jar:file:///home/worker/workspace/build/application/" + + app_name + + "/browser/features/firefox@getpocket.com.xpi!/bootstrap.js", + "path4", + ), + ] + + url_finder = lcov_rewriter.UrlFinder(self._chrome_map_file, "", "", []) + for path, expected in paths: + self.assertEqual(url_finder.rewrite_url(path)[0], expected) + + def test_wrong_scheme_paths(self): + paths = [ + "http://www.mozilla.org/aFile.js", + "https://www.mozilla.org/aFile.js", + "data:something", + "about:newtab", + "javascript:something", + ] + + url_finder = lcov_rewriter.UrlFinder(self._chrome_map_file, "", "", []) + for path in paths: + self.assertIsNone(url_finder.rewrite_url(path)) + + def test_chrome_resource_paths(self): + paths = [ + # Path with default url prefix + ( + "resource://gre/modules/osfile/osfile_async_worker.js", + ("toolkit/components/osfile/modules/osfile_async_worker.js", None), + ), + # Path with url prefix that is in chrome map + ( + "resource://activity-stream/lib/PrefsFeed.sys.mjs", + ("browser/components/newtab/lib/PrefsFeed.sys.mjs", None), + ), + # Path which is in url overrides + ( + "chrome://global/content/license.html", + ("browser/base/content/license.html", None), + ), + # Path which ends with > eval + ( + "resource://gre/modules/osfile/osfile_async_worker.js line 3 > eval", + None, + ), + # Path which ends with > Function + ( + "resource://gre/modules/osfile/osfile_async_worker.js line 3 > Function", + None, + ), + # Path which contains "->" + ( + "resource://gre/modules/addons/XPIProvider.sys.mjs -> resource://gre/modules/osfile/osfile_async_worker.js", # noqa + ("toolkit/components/osfile/modules/osfile_async_worker.js", None), + ), + # Path with pp_info + ( + "resource://gre/modules/AppConstants.sys.mjs", + ( + "toolkit/modules/AppConstants.sys.mjs", + { + "101,102": ["toolkit/modules/AppConstants.sys.mjs", 135], + }, + ), + ), + # Path with query + ( + "resource://activity-stream/lib/PrefsFeed.jsm?q=0.9098419174803978", + ("browser/components/newtab/lib/PrefsFeed.jsm", None), + ), + ] + + url_finder = lcov_rewriter.UrlFinder(self._chrome_map_file, "", "dist/bin/", []) + for path, expected in paths: + self.assertEqual(url_finder.rewrite_url(path), expected) + + +if __name__ == "__main__": + mozunit.main() diff --git a/python/mozbuild/mozbuild/test/common.py b/python/mozbuild/mozbuild/test/common.py new file mode 100644 index 0000000000..47f04a8dd3 --- /dev/null +++ b/python/mozbuild/mozbuild/test/common.py @@ -0,0 +1,69 @@ +# 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/. + +import errno +import os +import shutil + +import mozpack.path as mozpath +from buildconfig import topsrcdir +from mach.logging import LoggingManager + +from mozbuild.util import ReadOnlyDict + +# By including this module, tests get structured logging. +log_manager = LoggingManager() +log_manager.add_terminal_logging() + + +def prepare_tmp_topsrcdir(path): + for p in ( + "build/autoconf/config.guess", + "build/autoconf/config.sub", + "build/moz.configure/checks.configure", + "build/moz.configure/init.configure", + "build/moz.configure/util.configure", + ): + file_path = os.path.join(path, p) + try: + os.makedirs(os.path.dirname(file_path)) + except OSError as e: + if e.errno != errno.EEXIST: + raise + shutil.copy(os.path.join(topsrcdir, p), file_path) + + +# mozconfig is not a reusable type (it's actually a module) so, we +# have to mock it. +class MockConfig(object): + def __init__( + self, + topsrcdir="/path/to/topsrcdir", + extra_substs={}, + error_is_fatal=True, + ): + self.topsrcdir = mozpath.abspath(topsrcdir) + self.topobjdir = mozpath.abspath("/path/to/topobjdir") + + self.substs = ReadOnlyDict( + { + "MOZ_FOO": "foo", + "MOZ_BAR": "bar", + "MOZ_TRUE": "1", + "MOZ_FALSE": "", + "DLL_PREFIX": "lib", + "DLL_SUFFIX": ".so", + }, + **extra_substs + ) + + self.defines = self.substs + + self.lib_prefix = "lib" + self.lib_suffix = ".a" + self.import_prefix = "lib" + self.import_suffix = ".so" + self.dll_prefix = "lib" + self.dll_suffix = ".so" + self.error_is_fatal = error_is_fatal diff --git a/python/mozbuild/mozbuild/test/compilation/__init__.py b/python/mozbuild/mozbuild/test/compilation/__init__.py new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/python/mozbuild/mozbuild/test/compilation/__init__.py diff --git a/python/mozbuild/mozbuild/test/compilation/test_warnings.py b/python/mozbuild/mozbuild/test/compilation/test_warnings.py new file mode 100644 index 0000000000..1769e2e333 --- /dev/null +++ b/python/mozbuild/mozbuild/test/compilation/test_warnings.py @@ -0,0 +1,240 @@ +# 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/. + +import os +import unittest + +from mozfile.mozfile import NamedTemporaryFile +from mozunit import main + +from mozbuild.compilation.warnings import ( + CompilerWarning, + WarningsCollector, + WarningsDatabase, +) + +CLANG_TESTS = [ + ( + "foobar.cpp:123:10: warning: you messed up [-Wfoo]", + "foobar.cpp", + 123, + 10, + "warning", + "you messed up", + "-Wfoo", + ), + ( + "c_locale_dummy.c:457:1: error: (near initialization for " + "'full_wmonthname[0]') [clang-diagnostic-error]", + "c_locale_dummy.c", + 457, + 1, + "error", + "(near initialization for 'full_wmonthname[0]')", + "clang-diagnostic-error", + ), +] + +CURRENT_LINE = 1 + + +def get_warning(): + global CURRENT_LINE + + w = CompilerWarning() + w["filename"] = "/foo/bar/baz.cpp" + w["line"] = CURRENT_LINE + w["column"] = 12 + w["message"] = "This is irrelevant" + + CURRENT_LINE += 1 + + return w + + +class TestCompilerWarning(unittest.TestCase): + def test_equivalence(self): + w1 = CompilerWarning() + w2 = CompilerWarning() + + s = set() + + # Empty warnings should be equal. + self.assertEqual(w1, w2) + + s.add(w1) + s.add(w2) + + self.assertEqual(len(s), 1) + + w1["filename"] = "/foo.c" + w2["filename"] = "/bar.c" + + self.assertNotEqual(w1, w2) + + s = set() + s.add(w1) + s.add(w2) + + self.assertEqual(len(s), 2) + + w1["filename"] = "/foo.c" + w1["line"] = 5 + w2["line"] = 5 + + w2["filename"] = "/foo.c" + w1["column"] = 3 + w2["column"] = 3 + + self.assertEqual(w1, w2) + + def test_comparison(self): + w1 = CompilerWarning() + w2 = CompilerWarning() + + w1["filename"] = "/aaa.c" + w1["line"] = 5 + w1["column"] = 5 + + w2["filename"] = "/bbb.c" + w2["line"] = 5 + w2["column"] = 5 + + self.assertLess(w1, w2) + self.assertGreater(w2, w1) + self.assertGreaterEqual(w2, w1) + + w2["filename"] = "/aaa.c" + w2["line"] = 4 + w2["column"] = 6 + + self.assertLess(w2, w1) + self.assertGreater(w1, w2) + self.assertGreaterEqual(w1, w2) + + w2["filename"] = "/aaa.c" + w2["line"] = 5 + w2["column"] = 10 + + self.assertLess(w1, w2) + self.assertGreater(w2, w1) + self.assertGreaterEqual(w2, w1) + + w2["filename"] = "/aaa.c" + w2["line"] = 5 + w2["column"] = 5 + + self.assertLessEqual(w1, w2) + self.assertLessEqual(w2, w1) + self.assertGreaterEqual(w2, w1) + self.assertGreaterEqual(w1, w2) + + +class TestWarningsAndErrorsParsing(unittest.TestCase): + def test_clang_parsing(self): + for source, filename, line, column, diag_type, message, flag in CLANG_TESTS: + collector = WarningsCollector(lambda w: None) + warning = collector.process_line(source) + + self.assertIsNotNone(warning) + + self.assertEqual(warning["filename"], filename) + self.assertEqual(warning["line"], line) + self.assertEqual(warning["column"], column) + self.assertEqual(warning["type"], diag_type) + self.assertEqual(warning["message"], message) + self.assertEqual(warning["flag"], flag) + + +class TestWarningsDatabase(unittest.TestCase): + def test_basic(self): + db = WarningsDatabase() + + self.assertEqual(len(db), 0) + + for i in range(10): + db.insert(get_warning(), compute_hash=False) + + self.assertEqual(len(db), 10) + + warnings = list(db) + self.assertEqual(len(warnings), 10) + + def test_hashing(self): + """Ensure that hashing files on insert works.""" + db = WarningsDatabase() + + temp = NamedTemporaryFile(mode="wt") + temp.write("x" * 100) + temp.flush() + + w = CompilerWarning() + w["filename"] = temp.name + w["line"] = 1 + w["column"] = 4 + w["message"] = "foo bar" + + # Should not throw. + db.insert(w) + + w["filename"] = "DOES_NOT_EXIST" + + with self.assertRaises(Exception): + db.insert(w) + + def test_pruning(self): + """Ensure old warnings are removed from database appropriately.""" + db = WarningsDatabase() + + source_files = [] + for i in range(1, 21): + temp = NamedTemporaryFile(mode="wt") + temp.write("x" * (100 * i)) + temp.flush() + + # Keep reference so it doesn't get GC'd and deleted. + source_files.append(temp) + + w = CompilerWarning() + w["filename"] = temp.name + w["line"] = 1 + w["column"] = i * 10 + w["message"] = "irrelevant" + + db.insert(w) + + self.assertEqual(len(db), 20) + + # If we change a source file, inserting a new warning should nuke the + # old one. + source_files[0].write("extra") + source_files[0].flush() + + w = CompilerWarning() + w["filename"] = source_files[0].name + w["line"] = 1 + w["column"] = 50 + w["message"] = "replaced" + + db.insert(w) + + self.assertEqual(len(db), 20) + + warnings = list(db.warnings_for_file(source_files[0].name)) + self.assertEqual(len(warnings), 1) + self.assertEqual(warnings[0]["column"], w["column"]) + + # If we delete the source file, calling prune should cause the warnings + # to go away. + old_filename = source_files[0].name + del source_files[0] + + self.assertFalse(os.path.exists(old_filename)) + + db.prune() + self.assertEqual(len(db), 19) + + +if __name__ == "__main__": + main() diff --git a/python/mozbuild/mozbuild/test/configure/common.py b/python/mozbuild/mozbuild/test/configure/common.py new file mode 100644 index 0000000000..d04021d7e5 --- /dev/null +++ b/python/mozbuild/mozbuild/test/configure/common.py @@ -0,0 +1,325 @@ +# 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/. + +import copy +import errno +import os +import subprocess +import sys +import tempfile +import unittest + +import six +from buildconfig import topobjdir, topsrcdir +from mozpack import path as mozpath +from six import StringIO, string_types + +from mozbuild.configure import ConfigureSandbox +from mozbuild.util import ReadOnlyNamespace, memoized_property + + +def fake_short_path(path): + if sys.platform.startswith("win"): + return "/".join( + p.split(" ", 1)[0] + "~1" if " " in p else p for p in mozpath.split(path) + ) + return path + + +def ensure_exe_extension(path): + if sys.platform.startswith("win"): + return path + ".exe" + return path + + +class ConfigureTestVFS(object): + def __init__(self, paths): + self._paths = set(mozpath.abspath(p) for p in paths) + + def _real_file(self, path): + return mozpath.basedir(path, [topsrcdir, topobjdir, tempfile.gettempdir()]) + + def exists(self, path): + if path in self._paths: + return True + if self._real_file(path): + return os.path.exists(path) + return False + + def isfile(self, path): + path = mozpath.abspath(path) + if path in self._paths: + return True + if self._real_file(path): + return os.path.isfile(path) + return False + + def expanduser(self, path): + return os.path.expanduser(path) + + def isdir(self, path): + path = mozpath.abspath(path) + if any(mozpath.basedir(mozpath.dirname(p), [path]) for p in self._paths): + return True + if self._real_file(path): + return os.path.isdir(path) + return False + + def getsize(self, path): + if not self._real_file(path): + raise FileNotFoundError(path) + return os.path.getsize(path) + + +class ConfigureTestSandbox(ConfigureSandbox): + """Wrapper around the ConfigureSandbox for testing purposes. + + Its arguments are the same as ConfigureSandbox, except for the additional + `paths` argument, which is a dict where the keys are file paths and the + values are either None or a function that will be called when the sandbox + calls an implemented function from subprocess with the key as command. + When the command is CONFIG_SHELL, the function for the path of the script + that follows will be called. + + The API for those functions is: + retcode, stdout, stderr = func(stdin, args) + + This class is only meant to implement the minimal things to make + moz.configure testing possible. As such, it takes shortcuts. + """ + + def __init__(self, paths, config, environ, *args, **kwargs): + self._search_path = environ.get("PATH", "").split(os.pathsep) + + self._subprocess_paths = { + mozpath.abspath(k): v for k, v in six.iteritems(paths) if v + } + + paths = list(paths) + + environ = copy.copy(environ) + if "CONFIG_SHELL" not in environ: + environ["CONFIG_SHELL"] = mozpath.abspath("/bin/sh") + self._subprocess_paths[environ["CONFIG_SHELL"]] = self.shell + paths.append(environ["CONFIG_SHELL"]) + self._subprocess_paths[ + mozpath.join(topsrcdir, "build/win32/vswhere.exe") + ] = self.vswhere + + vfs = ConfigureTestVFS(paths) + + os_path = {k: getattr(vfs, k) for k in dir(vfs) if not k.startswith("_")} + + os_path.update(self.OS.path.__dict__) + + os_contents = {} + exec("from os import *", {}, os_contents) + os_contents["path"] = ReadOnlyNamespace(**os_path) + os_contents["environ"] = dict(environ) + self.imported_os = ReadOnlyNamespace(**os_contents) + + super(ConfigureTestSandbox, self).__init__(config, environ, *args, **kwargs) + + @memoized_property + def _wrapped_mozfile(self): + return ReadOnlyNamespace(which=self.which) + + @memoized_property + def _wrapped_os(self): + return self.imported_os + + @memoized_property + def _wrapped_subprocess(self): + return ReadOnlyNamespace( + CalledProcessError=subprocess.CalledProcessError, + check_output=self.check_output, + run=self.subprocess_run, + PIPE=subprocess.PIPE, + STDOUT=subprocess.STDOUT, + Popen=self.Popen, + ) + + @memoized_property + def _wrapped_ctypes(self): + class CTypesFunc(object): + def __init__(self, func): + self._func = func + + def __call__(self, *args, **kwargs): + return self._func(*args, **kwargs) + + return ReadOnlyNamespace( + create_unicode_buffer=self.create_unicode_buffer, + windll=ReadOnlyNamespace( + kernel32=ReadOnlyNamespace( + GetShortPathNameW=CTypesFunc(self.GetShortPathNameW) + ) + ), + wintypes=ReadOnlyNamespace(LPCWSTR=0, LPWSTR=1, DWORD=2), + ) + + @memoized_property + def _wrapped__winreg(self): + def OpenKey(*args, **kwargs): + raise WindowsError() + + return ReadOnlyNamespace(HKEY_LOCAL_MACHINE=0, OpenKey=OpenKey) + + def create_unicode_buffer(self, *args, **kwargs): + class Buffer(object): + def __init__(self): + self.value = "" + + return Buffer() + + def GetShortPathNameW(self, path_in, path_out, length): + path_out.value = fake_short_path(path_in) + return length + + def which(self, command, mode=None, path=None, exts=None): + if isinstance(path, string_types): + path = path.split(os.pathsep) + + for parent in path or self._search_path: + c = mozpath.abspath(mozpath.join(parent, command)) + for candidate in (c, ensure_exe_extension(c)): + if self.imported_os.path.exists(candidate): + return candidate + return None + + def Popen(self, args, stdin=None, stdout=None, stderr=None, **kargs): + program = self.which(args[0]) + if not program: + raise OSError(errno.ENOENT, "File not found") + + func = self._subprocess_paths.get(program) + cwd = kargs.get("cwd") + if cwd and func.__code__.co_argcount == 3: + retcode, stdout, stderr = func(stdin, args[1:], cwd) + else: + retcode, stdout, stderr = func(stdin, args[1:]) + + class Process(object): + def communicate(self, stdin=None): + return stdout, stderr + + def wait(self): + return retcode + + return Process() + + def check_output(self, args, **kwargs): + proc = self.Popen(args, **kwargs) + stdout, stderr = proc.communicate() + retcode = proc.wait() + if retcode: + raise subprocess.CalledProcessError(retcode, args, stdout) + return stdout + + def subprocess_run(self, args, **kwargs): + proc = self.Popen(args, **kwargs) + stdout, stderr = proc.communicate() + retcode = proc.wait() + if kwargs.get("check") and retcode: + raise subprocess.CalledProcessError(retcode, args, stdout) + return ReadOnlyNamespace( + args=args, + returncode=retcode, + stdout=stdout, + stderr=stderr, + ) + + def shell(self, stdin, args): + script = mozpath.abspath(args[0]) + if script in self._subprocess_paths: + return self._subprocess_paths[script](stdin, args[1:]) + return 127, "", "File not found" + + def vswhere(self, stdin, args): + return 0, "[]", "" + + def get_config(self, name): + # Like the loop in ConfigureSandbox.run, but only execute the code + # associated with the given config item. + for func, args in self._execution_queue: + if ( + func == self._resolve_and_set + and args[0] is self._config + and args[1] == name + ): + func(*args) + return self._config.get(name) + + +class BaseConfigureTest(unittest.TestCase): + HOST = "x86_64-pc-linux-gnu" + + def setUp(self): + self._cwd = os.getcwd() + os.chdir(topobjdir) + + def tearDown(self): + os.chdir(self._cwd) + + def config_guess(self, stdin, args): + return 0, self.HOST, "" + + def config_sub(self, stdin, args): + return 0, args[0], "" + + def get_sandbox( + self, + paths, + config, + args=[], + environ={}, + mozconfig="", + out=None, + logger=None, + cls=ConfigureTestSandbox, + ): + kwargs = {} + if logger: + kwargs["logger"] = logger + else: + if not out: + out = StringIO() + kwargs["stdout"] = out + kwargs["stderr"] = out + + if hasattr(self, "TARGET"): + target = ["--target=%s" % self.TARGET] + else: + target = [] + + if mozconfig: + fh, mozconfig_path = tempfile.mkstemp(text=True) + os.write(fh, six.ensure_binary(mozconfig)) + os.close(fh) + else: + mozconfig_path = os.path.join( + os.path.dirname(__file__), "data", "empty_mozconfig" + ) + + try: + environ = dict( + environ, + OLD_CONFIGURE=os.path.join(topsrcdir, "old-configure"), + MOZCONFIG=mozconfig_path, + ) + + paths = dict(paths) + autoconf_dir = mozpath.join(topsrcdir, "build", "autoconf") + paths[mozpath.join(autoconf_dir, "config.guess")] = self.config_guess + paths[mozpath.join(autoconf_dir, "config.sub")] = self.config_sub + + sandbox = cls( + paths, config, environ, ["configure"] + target + args, **kwargs + ) + sandbox.include_file(os.path.join(topsrcdir, "moz.configure")) + + return sandbox + finally: + if mozconfig: + os.remove(mozconfig_path) diff --git a/python/mozbuild/mozbuild/test/configure/data/decorators.configure b/python/mozbuild/mozbuild/test/configure/data/decorators.configure new file mode 100644 index 0000000000..b98eb26f3f --- /dev/null +++ b/python/mozbuild/mozbuild/test/configure/data/decorators.configure @@ -0,0 +1,53 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# 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/. + + +@template +def simple_decorator(func): + return func + + +@template +def wrapper_decorator(func): + def wrapper(*args, **kwargs): + return func(*args, **kwargs) + + return wrapper + + +@template +def function_decorator(*args, **kwargs): + # We could return wrapper_decorator from above here, but then we wouldn't + # know if this works as expected because wrapper_decorator itself was + # modified or because the right thing happened here. + def wrapper_decorator(func): + def wrapper(*args, **kwargs): + return func(*args, **kwargs) + + return wrapper + + return wrapper_decorator + + +@depends("--help") +@simple_decorator +def foo(help): + global FOO + FOO = 1 + + +@depends("--help") +@wrapper_decorator +def bar(help): + global BAR + BAR = 1 + + +@depends("--help") +@function_decorator("a", "b", "c") +def qux(help): + global QUX + QUX = 1 diff --git a/python/mozbuild/mozbuild/test/configure/data/empty_mozconfig b/python/mozbuild/mozbuild/test/configure/data/empty_mozconfig new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/python/mozbuild/mozbuild/test/configure/data/empty_mozconfig diff --git a/python/mozbuild/mozbuild/test/configure/data/extra.configure b/python/mozbuild/mozbuild/test/configure/data/extra.configure new file mode 100644 index 0000000000..e54a93dbc3 --- /dev/null +++ b/python/mozbuild/mozbuild/test/configure/data/extra.configure @@ -0,0 +1,15 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# 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/. + +option("--extra", help="Extra") + + +@depends("--extra") +def extra(extra): + return extra + + +set_config("EXTRA", extra) diff --git a/python/mozbuild/mozbuild/test/configure/data/imply_option/imm.configure b/python/mozbuild/mozbuild/test/configure/data/imply_option/imm.configure new file mode 100644 index 0000000000..f20a4a7149 --- /dev/null +++ b/python/mozbuild/mozbuild/test/configure/data/imply_option/imm.configure @@ -0,0 +1,37 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# 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/. + +imply_option("--enable-foo", True) + +option("--enable-foo", help="enable foo") + + +@depends("--enable-foo", "--help") +def foo(value, help): + if value: + return True + + +imply_option("--enable-bar", ("foo", "bar")) + +option("--enable-bar", nargs="*", help="enable bar") + + +@depends("--enable-bar") +def bar(value): + if value: + return value + + +imply_option("--enable-baz", "BAZ") + +option("--enable-baz", nargs=1, help="enable baz") + + +@depends("--enable-baz") +def bar(value): + if value: + return value diff --git a/python/mozbuild/mozbuild/test/configure/data/imply_option/infer.configure b/python/mozbuild/mozbuild/test/configure/data/imply_option/infer.configure new file mode 100644 index 0000000000..b73be9a720 --- /dev/null +++ b/python/mozbuild/mozbuild/test/configure/data/imply_option/infer.configure @@ -0,0 +1,28 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# 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/. + +option("--enable-foo", help="enable foo") + + +@depends("--enable-foo", "--help") +def foo(value, help): + if value: + return True + + +imply_option("--enable-bar", foo) + + +option("--enable-bar", help="enable bar") + + +@depends("--enable-bar") +def bar(value): + if value: + return value + + +set_config("BAR", bar) diff --git a/python/mozbuild/mozbuild/test/configure/data/imply_option/infer_ko.configure b/python/mozbuild/mozbuild/test/configure/data/imply_option/infer_ko.configure new file mode 100644 index 0000000000..9b3761c3c3 --- /dev/null +++ b/python/mozbuild/mozbuild/test/configure/data/imply_option/infer_ko.configure @@ -0,0 +1,36 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# 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/. + +option("--enable-hoge", help="enable hoge") + + +@depends("--enable-hoge") +def hoge(value): + return value + + +option("--enable-foo", help="enable foo") + + +@depends("--enable-foo", hoge) +def foo(value, hoge): + if value: + return True + + +imply_option("--enable-bar", foo) + + +option("--enable-bar", help="enable bar") + + +@depends("--enable-bar") +def bar(value): + if value: + return value + + +set_config("BAR", bar) diff --git a/python/mozbuild/mozbuild/test/configure/data/imply_option/negative.configure b/python/mozbuild/mozbuild/test/configure/data/imply_option/negative.configure new file mode 100644 index 0000000000..e953231f5e --- /dev/null +++ b/python/mozbuild/mozbuild/test/configure/data/imply_option/negative.configure @@ -0,0 +1,40 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# 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/. + +option("--enable-foo", help="enable foo") + + +@depends("--enable-foo") +def foo(value): + if value: + return False + + +imply_option("--enable-bar", foo) + + +option("--disable-hoge", help="enable hoge") + + +@depends("--disable-hoge") +def hoge(value): + if not value: + return False + + +imply_option("--enable-bar", hoge) + + +option("--enable-bar", default=True, help="enable bar") + + +@depends("--enable-bar") +def bar(value): + if not value: + return value + + +set_config("BAR", bar) diff --git a/python/mozbuild/mozbuild/test/configure/data/imply_option/simple.configure b/python/mozbuild/mozbuild/test/configure/data/imply_option/simple.configure new file mode 100644 index 0000000000..6aa225cc45 --- /dev/null +++ b/python/mozbuild/mozbuild/test/configure/data/imply_option/simple.configure @@ -0,0 +1,28 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# 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/. + +option("--enable-foo", help="enable foo") + + +@depends("--enable-foo") +def foo(value): + if value: + return True + + +imply_option("--enable-bar", foo) + + +option("--enable-bar", help="enable bar") + + +@depends("--enable-bar") +def bar(value): + if value: + return value + + +set_config("BAR", bar) diff --git a/python/mozbuild/mozbuild/test/configure/data/imply_option/values.configure b/python/mozbuild/mozbuild/test/configure/data/imply_option/values.configure new file mode 100644 index 0000000000..93198a8295 --- /dev/null +++ b/python/mozbuild/mozbuild/test/configure/data/imply_option/values.configure @@ -0,0 +1,28 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# 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/. + +option("--enable-foo", nargs="*", help="enable foo") + + +@depends("--enable-foo") +def foo(value): + if value: + return value + + +imply_option("--enable-bar", foo) + + +option("--enable-bar", nargs="*", help="enable bar") + + +@depends("--enable-bar") +def bar(value): + if value: + return value + + +set_config("BAR", bar) diff --git a/python/mozbuild/mozbuild/test/configure/data/included.configure b/python/mozbuild/mozbuild/test/configure/data/included.configure new file mode 100644 index 0000000000..355d41c31a --- /dev/null +++ b/python/mozbuild/mozbuild/test/configure/data/included.configure @@ -0,0 +1,71 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# 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/. + + +# For more complex and repetitive things, we can create templates +@template +def check_compiler_flag(flag): + @depends(is_gcc) + def check(value): + if value: + return [flag] + + set_config("CFLAGS", check) + return check + + +check_compiler_flag("-Werror=foobar") + + +# Normal functions can be used in @depends functions. +def fortytwo(): + return 42 + + +def twentyone(): + yield 21 + + +@depends(is_gcc) +def check(value): + if value: + return fortytwo() + + +set_config("TEMPLATE_VALUE", check) + + +@depends(is_gcc) +def check(value): + if value: + for val in twentyone(): + return val + + +set_config("TEMPLATE_VALUE_2", check) + + +# Normal functions can use @imports too to import modules. +@imports("sys") +def platform(): + return sys.platform + + +option("--enable-imports-in-template", help="Imports in template") + + +@depends("--enable-imports-in-template") +def check(value): + if value: + return platform() + + +set_config("PLATFORM", check) + + +@template +def indirectly_define_option(*args, **kwargs): + option(*args, **kwargs) diff --git a/python/mozbuild/mozbuild/test/configure/data/moz.configure b/python/mozbuild/mozbuild/test/configure/data/moz.configure new file mode 100644 index 0000000000..c7fd454eeb --- /dev/null +++ b/python/mozbuild/mozbuild/test/configure/data/moz.configure @@ -0,0 +1,243 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# 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/. + +option("--enable-simple", help="Enable simple") + +# Setting MOZ_WITH_ENV in the environment has the same effect as passing +# --enable-with-env. +option("--enable-with-env", env="MOZ_WITH_ENV", help="Enable with env") + +# Optional values +option("--enable-values", nargs="*", help="Enable values") + +# Optional values from a set of choices +option("--enable-choices", nargs="*", choices=("a", "b", "c"), help="Enable choices") + +# Everything supported in the Option class is supported in option(). Assume +# the tests of the Option class are extensive about this. + +# Alternatively to --enable/--disable, there also is --with/--without. The +# difference is semantic only. Behavior is the same as --enable/--disable. + +# When the option name starts with --disable/--without, the default is for +# the option to be enabled. +option("--without-thing", help="Build without thing") + +# A --enable/--with option with a default of False is equivalent to a +# --disable/--without option. This can be used to change the defaults +# depending on e.g. the target or the built application. +option("--with-stuff", default=False, help="Build with stuff") + +# Other kinds of arbitrary options are also allowed. This is effectively +# equivalent to --enable/--with, with no possibility of --disable/--without. +option("--option", env="MOZ_OPTION", help="Option") + +# It is also possible to pass options through the environment only. +option(env="CC", nargs=1, help="C Compiler") + + +# Call the function when the --enable-simple option is processed, with its +# OptionValue as argument. +@depends("--enable-simple") +def simple(simple): + if simple: + return simple + + +set_config("ENABLED_SIMPLE", simple) + + +# There can be multiple functions depending on the same option. +@depends("--enable-simple") +def simple(simple): + return simple + + +set_config("SIMPLE", simple) + + +@depends("--enable-with-env") +def with_env(with_env): + return with_env + + +set_config("WITH_ENV", with_env) + + +# It doesn't matter if the dependency is on --enable or --disable +@depends("--disable-values") +def with_env2(values): + return values + + +set_config("VALUES", with_env2) + + +# It is possible to @depends on environment-only options. +@depends("CC") +def is_gcc(cc): + return cc and "gcc" in cc[0] + + +set_config("IS_GCC", is_gcc) + + +# It is possible to depend on the result from another function. +@depends(with_env2) +def with_env3(values): + return values + + +set_config("VALUES2", with_env3) + + +# @depends functions can also return results for use as input to another +# @depends. +@depends(with_env3) +def with_env4(values): + return values + + +@depends(with_env4) +def with_env5(values): + return values + + +set_config("VALUES3", with_env5) + + +# The result from @depends functions can also be used as input to options. +# The result must be returned, not implied. +@depends("--enable-simple") +def simple(simple): + return "simple" if simple else "not-simple" + + +option("--with-returned-default", default=simple, help="Returned default") + + +@depends("--with-returned-default") +def default(value): + return value + + +set_config("DEFAULTED", default) + + +@depends("--enable-simple") +def other_default(simple): + return bool(simple) + + +# When the default comes from the result of a @depends function, the help +# string can be picked between two variants automatically. +option( + "--with-other-default", default=other_default, help="{With|Without} other default" +) + + +@depends("--enable-values") +def choices(values): + if len(values): + return { + "alpha": ("a", "b", "c"), + "numeric": ("0", "1", "2"), + }.get(values[0]) + + +option("--returned-choices", choices=choices, help="Choices") + + +@depends("--returned-choices") +def returned_choices(values): + return values + + +set_config("CHOICES", returned_choices) + +# Unusual case: an option can default to enabled, but still allow for optional +# values. In that case, both --disable and --enable options will be shown in +# configure --help, and the help message needs to include both variants. +option("--disable-foo", nargs="*", choices=("x", "y"), help="{Enable|Disable} Foo") + + +# All options must be referenced by some @depends function. +# It is possible to depend on multiple options/functions +@depends( + "--without-thing", + "--with-stuff", + with_env4, + "--option", + "--enable-choices", + "--with-other-default", + "--disable-foo", +) +def remainder(*args): + return args + + +set_config("REMAINDER", remainder) + +# It is possible to include other files to extend the configuration script. +include("included.configure") + +# It is also possible for the include file path to come from the result of a +# @depends function. +option("--enable-include", nargs=1, help="Include") + + +@depends("--enable-include") +def include_path(path): + return path[0] if path else None + + +include(include_path) + +# Sandboxed functions can import from modules through the use of the @imports +# decorator. +# The order of the decorators matter: @imports needs to appear after other +# decorators. +option("--with-imports", nargs="?", help="Imports") + + +# A limited set of functions from os.path are exposed by default. +@depends("--with-imports") +def with_imports(value): + if len(value): + return hasattr(os.path, "abspath") + + +set_config("HAS_ABSPATH", with_imports) + + +# It is still possible to import the full set from os.path. +# It is also possible to cherry-pick builtins. +@depends("--with-imports") +@imports("os.path") +def with_imports(value): + if len(value): + return hasattr(os.path, "getatime") + + +set_config("HAS_GETATIME", with_imports) + + +@depends("--with-imports") +def with_imports(value): + if len(value): + return hasattr(os.path, "getatime") + + +set_config("HAS_GETATIME2", with_imports) + +# This option should be attributed to this file in the --help output even though +# included.configure is the actual file that defines the option. +indirectly_define_option("--indirect-option", help="Indirectly defined option") + + +@depends("--indirect-option") +def indirect_option(option): + return option diff --git a/python/mozbuild/mozbuild/test/configure/data/set_config.configure b/python/mozbuild/mozbuild/test/configure/data/set_config.configure new file mode 100644 index 0000000000..0ae5fef6d6 --- /dev/null +++ b/python/mozbuild/mozbuild/test/configure/data/set_config.configure @@ -0,0 +1,51 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# 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/. + +option("--set-foo", help="set foo") + + +@depends("--set-foo") +def foo(value): + if value: + return True + + +set_config("FOO", foo) + + +option("--set-bar", help="set bar") + + +@depends("--set-bar") +def bar(value): + return bool(value) + + +set_config("BAR", bar) + + +option("--set-value", nargs=1, help="set value") + + +@depends("--set-value") +def set_value(value): + if value: + return value[0] + + +set_config("VALUE", set_value) + + +option("--set-name", nargs=1, help="set name") + + +@depends("--set-name") +def set_name(value): + if value: + return value[0] + + +set_config(set_name, True) diff --git a/python/mozbuild/mozbuild/test/configure/data/set_define.configure b/python/mozbuild/mozbuild/test/configure/data/set_define.configure new file mode 100644 index 0000000000..ce9a60d7f1 --- /dev/null +++ b/python/mozbuild/mozbuild/test/configure/data/set_define.configure @@ -0,0 +1,51 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# 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/. + +option("--set-foo", help="set foo") + + +@depends("--set-foo") +def foo(value): + if value: + return True + + +set_define("FOO", foo) + + +option("--set-bar", help="set bar") + + +@depends("--set-bar") +def bar(value): + return bool(value) + + +set_define("BAR", bar) + + +option("--set-value", nargs=1, help="set value") + + +@depends("--set-value") +def set_value(value): + if value: + return value[0] + + +set_define("VALUE", set_value) + + +option("--set-name", nargs=1, help="set name") + + +@depends("--set-name") +def set_name(value): + if value: + return value[0] + + +set_define(set_name, True) diff --git a/python/mozbuild/mozbuild/test/configure/data/subprocess.configure b/python/mozbuild/mozbuild/test/configure/data/subprocess.configure new file mode 100644 index 0000000000..3316fee087 --- /dev/null +++ b/python/mozbuild/mozbuild/test/configure/data/subprocess.configure @@ -0,0 +1,24 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# 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/. + + +@depends("--help") +@imports("codecs") +@imports(_from="mozbuild.configure.util", _import="getpreferredencoding") +@imports("os") +@imports(_from="__builtin__", _import="open") +def dies_when_logging(_): + test_file = "test.txt" + quote_char = "'" + if getpreferredencoding().lower() == "utf-8": + quote_char = "\u00B4" + try: + with open(test_file, "w+") as fh: + fh.write(quote_char) + out = check_cmd_output("cat", "test.txt") + log.info(out) + finally: + os.remove(test_file) diff --git a/python/mozbuild/mozbuild/test/configure/lint.py b/python/mozbuild/mozbuild/test/configure/lint.py new file mode 100644 index 0000000000..59d41da264 --- /dev/null +++ b/python/mozbuild/mozbuild/test/configure/lint.py @@ -0,0 +1,62 @@ +# 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/. + +import os +import unittest + +import six +from buildconfig import topobjdir, topsrcdir +from mozunit import main + +from mozbuild.configure.lint import LintSandbox + +test_path = os.path.abspath(__file__) + + +class LintMeta(type): + def __new__(mcs, name, bases, attrs): + def create_test(project, func): + def test(self): + return func(self, project) + + return test + + for project in ( + "browser", + "js", + "memory", + "mobile/android", + ): + attrs["test_%s" % project.replace("/", "_")] = create_test( + project, attrs["lint"] + ) + + return type.__new__(mcs, name, bases, attrs) + + +# We don't actually need python2 compat, but this makes flake8 happy. +@six.add_metaclass(LintMeta) +class Lint(unittest.TestCase): + def setUp(self): + self._curdir = os.getcwd() + os.chdir(topobjdir) + + def tearDown(self): + os.chdir(self._curdir) + + def lint(self, project): + sandbox = LintSandbox( + { + "OLD_CONFIGURE": os.path.join(topsrcdir, "old-configure"), + "MOZCONFIG": os.path.join( + os.path.dirname(test_path), "data", "empty_mozconfig" + ), + }, + ["configure", "--enable-project=%s" % project, "--help"], + ) + sandbox.run(os.path.join(topsrcdir, "moz.configure")) + + +if __name__ == "__main__": + main() diff --git a/python/mozbuild/mozbuild/test/configure/macos_fake_sdk/SDKSettings.plist b/python/mozbuild/mozbuild/test/configure/macos_fake_sdk/SDKSettings.plist new file mode 100644 index 0000000000..786746f103 --- /dev/null +++ b/python/mozbuild/mozbuild/test/configure/macos_fake_sdk/SDKSettings.plist @@ -0,0 +1,8 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> +<plist version="1.0"> +<dict> + <key>Version</key> + <string>14.2</string> +</dict> +</plist> diff --git a/python/mozbuild/mozbuild/test/configure/test_bootstrap.py b/python/mozbuild/mozbuild/test/configure/test_bootstrap.py new file mode 100644 index 0000000000..758ddd5632 --- /dev/null +++ b/python/mozbuild/mozbuild/test/configure/test_bootstrap.py @@ -0,0 +1,193 @@ +# 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/. + +import os +import sys +from tempfile import TemporaryDirectory + +import mozpack.path as mozpath +from buildconfig import topsrcdir +from mozunit import MockedOpen, main + +from common import BaseConfigureTest +from mozbuild.util import ReadOnlyNamespace + + +class IndexSearch: + def should_replace_task(self, task, *args): + return f'fake-task-id-for-{task["index"][0]}' + + +class TestBootstrap(BaseConfigureTest): + @staticmethod + def import_module(module): + if module == "taskgraph.optimize.strategies": + return ReadOnlyNamespace(IndexSearch=IndexSearch) + + # This method asserts the expected result of bootstrapping for the given + # argument (`arg`) to configure. + # - `states` is a 3-tuple of the initial state of each of the 3 fake toolchains + # bootstrapped by the test. Valid states are: + # - `True`: the toolchain was already installed and up-to-date. + # - `"old"`: an old version of the toolchain was already installed. + # - `False`: the toolchain was not already installed. + # - `bootstrapped` is a 3-tuple representing whether each toolchain is expected + # to have been actively bootstrapped by configure. + # - `in_path` is a 3-tuple representing whether the path for each toolchain is + # expected to have been added to the `bootstrap_search_path`. Valid values are: + # - `True`: the toolchain path was prepended to `bootstrap_search_path`. + # - `"append"`: the toolchain path was appended to `bootstrap_search_path`. + # - `False`: the toolchain path is not in `bootstrap_search_path`. + def assertBootstrap(self, arg, states, bootstrapped, in_path): + called_for = [] + + def mock_mach(input, args, cwd): + assert os.path.basename(args[0]) == "mach" + assert args[1] == "--log-no-times" + assert args[2] == "artifact" + assert args[3] == "toolchain" + assert args[4] == "--from-task" + toolchain_dir = os.path.basename(args[-1].rpartition(".artifact")[0]) + try: + os.mkdir(os.path.join(cwd, toolchain_dir)) + except FileExistsError: + pass + called_for.append(toolchain_dir) + return "", "", 0 + + sandbox = self.get_sandbox( + { + sys.executable: mock_mach, + }, + {}, + [arg] if arg else [], + {}, + ) + dep = sandbox._depends[sandbox["vcs_checkout_type"]] + getattr(sandbox, "__value_for_depends")[(dep,)] = None + dep = sandbox._depends[sandbox["original_path"]] + getattr(sandbox, "__value_for_depends")[(dep,)] = ["dummy"] + + tmp_dir = TemporaryDirectory() + dep = sandbox._depends[sandbox["toolchains_base_dir"]] + getattr(sandbox, "__value_for_depends")[(dep,)] = tmp_dir.name + + dep = sandbox._depends[sandbox["bootstrap_toolchain_tasks"]] + getattr(sandbox, "__value_for_depends")[(dep,)] = ReadOnlyNamespace( + prefix="linux64", + tasks={ + "toolchain-foo": { + "index": ["fake.index.foo"], + "artifact": "public/foo.artifact", + }, + "toolchain-linux64-bar": { + "index": ["fake.index.bar"], + "artifact": "public/bar.artifact", + }, + "toolchain-linux64-qux": { + "index": ["fake.index.qux"], + "artifact": "public/qux.artifact", + }, + }, + ) + toolchains = ["foo", "bar", "qux"] + for t in toolchains: + exec(f'{t} = bootstrap_search_path("{t}")', sandbox) + sandbox._wrapped_importlib = ReadOnlyNamespace(import_module=self.import_module) + for t, in_path, b, state in zip(toolchains, in_path, bootstrapped, states): + if in_path == "append": + expected = ["dummy", mozpath.join(tmp_dir.name, t)] + elif in_path: + expected = [mozpath.join(tmp_dir.name, t), "dummy"] + else: + expected = ["dummy"] + if state: + os.mkdir(os.path.join(tmp_dir.name, t)) + indices = os.path.join(tmp_dir.name, "indices") + os.makedirs(indices, exist_ok=True) + with open(os.path.join(indices, t), "w") as fh: + fh.write("old" if state == "old" else t) + self.assertEqual(sandbox._value_for(sandbox[t]), expected, msg=t) + self.assertEqual(t in called_for, bool(b), msg=t) + + def test_bootstrap(self): + milestone_path = os.path.join(topsrcdir, "config", "milestone.txt") + + with MockedOpen({milestone_path: "124.0a1"}): + self.assertBootstrap( + "--disable-bootstrap", + (True, "old", False), + (False, False, False), + (True, True, False), + ) + self.assertBootstrap( + None, + (True, "old", False), + (False, True, True), + (True, True, True), + ) + + with MockedOpen({milestone_path: "124.0"}): + self.assertBootstrap( + "--disable-bootstrap", + (True, "old", False), + (False, False, False), + ("append", "append", False), + ) + self.assertBootstrap( + None, + (True, "old", False), + (False, False, False), + ("append", "append", False), + ) + + for milestone in ("124.0a1", "124.0"): + with MockedOpen({milestone_path: milestone}): + # With `--enable-bootstrap`, anything is bootstrappable + self.assertBootstrap( + "--enable-bootstrap", + (True, "old", False), + (False, True, True), + (True, True, True), + ) + + # With `--enable-bootstrap=foo,bar`, only foo and bar are bootstrappable + self.assertBootstrap( + "--enable-bootstrap=foo,bar", + (False, "old", False), + (True, True, False), + (True, True, False), + ) + self.assertBootstrap( + "--enable-bootstrap=foo", + (True, "old", True), + (False, False, False), + (True, "append", "append"), + ) + + # With `--enable-bootstrap=-foo`, anything is bootstrappable, except foo + self.assertBootstrap( + "--enable-bootstrap=-foo", + (True, False, "old"), + (False, True, True), + ("append", True, True), + ) + self.assertBootstrap( + "--enable-bootstrap=-foo", + (False, False, "old"), + (False, True, True), + (False, True, True), + ) + + # Corner case. + self.assertBootstrap( + "--enable-bootstrap=-foo,foo,bar", + (False, False, "old"), + (False, True, False), + (False, True, "append"), + ) + + +if __name__ == "__main__": + main() diff --git a/python/mozbuild/mozbuild/test/configure/test_checks_configure.py b/python/mozbuild/mozbuild/test/configure/test_checks_configure.py new file mode 100644 index 0000000000..3482f82f3d --- /dev/null +++ b/python/mozbuild/mozbuild/test/configure/test_checks_configure.py @@ -0,0 +1,1168 @@ +# 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/. + +import os +import sys +import textwrap +import unittest + +from buildconfig import topsrcdir +from mozpack import path as mozpath +from mozunit import MockedOpen, main +from six import StringIO + +from common import ConfigureTestSandbox, ensure_exe_extension, fake_short_path +from mozbuild.configure import ConfigureError, ConfigureSandbox +from mozbuild.shellutil import quote as shell_quote + + +class TestChecksConfigure(unittest.TestCase): + def test_checking(self): + def make_test(to_exec): + def test(val, msg): + out = StringIO() + sandbox = ConfigureSandbox({}, stdout=out, stderr=out) + base_dir = os.path.join(topsrcdir, "build", "moz.configure") + sandbox.include_file(os.path.join(base_dir, "checks.configure")) + exec(to_exec, sandbox) + sandbox["foo"](val) + self.assertEqual(out.getvalue(), msg) + + return test + + test = make_test( + textwrap.dedent( + """ + @checking('for a thing') + def foo(value): + return value + """ + ) + ) + test(True, "checking for a thing... yes\n") + test(False, "checking for a thing... no\n") + test(42, "checking for a thing... 42\n") + test("foo", "checking for a thing... foo\n") + data = ["foo", "bar"] + test(data, "checking for a thing... %r\n" % data) + + # When the function given to checking does nothing interesting, the + # behavior is not altered + test = make_test( + textwrap.dedent( + """ + @checking('for a thing', lambda x: x) + def foo(value): + return value + """ + ) + ) + test(True, "checking for a thing... yes\n") + test(False, "checking for a thing... no\n") + test(42, "checking for a thing... 42\n") + test("foo", "checking for a thing... foo\n") + data = ["foo", "bar"] + test(data, "checking for a thing... %r\n" % data) + + test = make_test( + textwrap.dedent( + """ + def munge(x): + if not x: + return 'not found' + if isinstance(x, (str, bool, int)): + return x + return ' '.join(x) + + @checking('for a thing', munge) + def foo(value): + return value + """ + ) + ) + test(True, "checking for a thing... yes\n") + test(False, "checking for a thing... not found\n") + test(42, "checking for a thing... 42\n") + test("foo", "checking for a thing... foo\n") + data = ["foo", "bar"] + test(data, "checking for a thing... foo bar\n") + + KNOWN_A = ensure_exe_extension(mozpath.abspath("/usr/bin/known-a")) + KNOWN_B = ensure_exe_extension(mozpath.abspath("/usr/local/bin/known-b")) + KNOWN_C = ensure_exe_extension(mozpath.abspath("/home/user/bin/known c")) + OTHER_A = ensure_exe_extension(mozpath.abspath("/lib/other/known-a")) + + def get_result( + self, + command="", + args=[], + environ={}, + prog="/bin/configure", + extra_paths=None, + includes=("util.configure", "checks.configure"), + ): + config = {} + out = StringIO() + paths = {self.KNOWN_A: None, self.KNOWN_B: None, self.KNOWN_C: None} + if extra_paths: + paths.update(extra_paths) + environ = dict(environ) + if "PATH" not in environ: + environ["PATH"] = os.pathsep.join(os.path.dirname(p) for p in paths) + paths[self.OTHER_A] = None + sandbox = ConfigureTestSandbox(paths, config, environ, [prog] + args, out, out) + base_dir = os.path.join(topsrcdir, "build", "moz.configure") + for f in includes: + sandbox.include_file(os.path.join(base_dir, f)) + + status = 0 + try: + exec(command, sandbox) + sandbox.run() + except SystemExit as e: + status = e.code + + return config, out.getvalue(), status + + def test_check_prog(self): + config, out, status = self.get_result('check_prog("FOO", ("known-a",))') + self.assertEqual(status, 0) + self.assertEqual(config, {"FOO": self.KNOWN_A}) + self.assertEqual(out, "checking for foo... %s\n" % self.KNOWN_A) + + config, out, status = self.get_result( + 'check_prog("FOO", ("unknown", "known-b", "known c"))' + ) + self.assertEqual(status, 0) + self.assertEqual(config, {"FOO": self.KNOWN_B}) + self.assertEqual(out, "checking for foo... %s\n" % self.KNOWN_B) + + config, out, status = self.get_result( + 'check_prog("FOO", ("unknown", "unknown-2", "known c"))' + ) + self.assertEqual(status, 0) + self.assertEqual(config, {"FOO": fake_short_path(self.KNOWN_C)}) + self.assertEqual( + out, "checking for foo... %s\n" % shell_quote(fake_short_path(self.KNOWN_C)) + ) + + config, out, status = self.get_result('check_prog("FOO", ("unknown",))') + self.assertEqual(status, 1) + self.assertEqual(config, {}) + self.assertEqual( + out, + textwrap.dedent( + """\ + checking for foo... not found + DEBUG: foo: Looking for unknown + ERROR: Cannot find foo + """ + ), + ) + + config, out, status = self.get_result( + 'check_prog("FOO", ("unknown", "unknown-2", "unknown 3"))' + ) + self.assertEqual(status, 1) + self.assertEqual(config, {}) + self.assertEqual( + out, + textwrap.dedent( + """\ + checking for foo... not found + DEBUG: foo: Looking for unknown + DEBUG: foo: Looking for unknown-2 + DEBUG: foo: Looking for 'unknown 3' + ERROR: Cannot find foo + """ + ), + ) + + config, out, status = self.get_result( + 'check_prog("FOO", ("unknown", "unknown-2", "unknown 3"), ' + "allow_missing=True)" + ) + self.assertEqual(status, 0) + self.assertEqual(config, {}) + self.assertEqual(out, "checking for foo... not found\n") + + @unittest.skipIf(not sys.platform.startswith("win"), "Windows-only test") + def test_check_prog_exe(self): + config, out, status = self.get_result( + 'check_prog("FOO", ("unknown", "known-b", "known c"))', ["FOO=known-a.exe"] + ) + self.assertEqual(status, 0) + self.assertEqual(config, {"FOO": self.KNOWN_A}) + self.assertEqual(out, "checking for foo... %s\n" % self.KNOWN_A) + + config, out, status = self.get_result( + 'check_prog("FOO", ("unknown", "known-b", "known c"))', + ["FOO=%s" % os.path.splitext(self.KNOWN_A)[0]], + ) + self.assertEqual(status, 0) + self.assertEqual(config, {"FOO": self.KNOWN_A}) + self.assertEqual(out, "checking for foo... %s\n" % self.KNOWN_A) + + def test_check_prog_with_args(self): + config, out, status = self.get_result( + 'check_prog("FOO", ("unknown", "known-b", "known c"))', ["FOO=known-a"] + ) + self.assertEqual(status, 0) + self.assertEqual(config, {"FOO": self.KNOWN_A}) + self.assertEqual(out, "checking for foo... %s\n" % self.KNOWN_A) + + config, out, status = self.get_result( + 'check_prog("FOO", ("unknown", "known-b", "known c"))', + ["FOO=%s" % self.KNOWN_A], + ) + self.assertEqual(status, 0) + self.assertEqual(config, {"FOO": self.KNOWN_A}) + self.assertEqual(out, "checking for foo... %s\n" % self.KNOWN_A) + + path = self.KNOWN_B.replace("known-b", "known-a") + config, out, status = self.get_result( + 'check_prog("FOO", ("unknown", "known-b", "known c"))', ["FOO=%s" % path] + ) + self.assertEqual(status, 1) + self.assertEqual(config, {}) + self.assertEqual( + out, + textwrap.dedent( + """\ + checking for foo... not found + DEBUG: foo: Looking for %s + ERROR: Cannot find foo + """ + ) + % path, + ) + + config, out, status = self.get_result( + 'check_prog("FOO", ("unknown",))', ["FOO=known c"] + ) + self.assertEqual(status, 0) + self.assertEqual(config, {"FOO": fake_short_path(self.KNOWN_C)}) + self.assertEqual( + out, "checking for foo... %s\n" % shell_quote(fake_short_path(self.KNOWN_C)) + ) + + config, out, status = self.get_result( + 'check_prog("FOO", ("unknown", "unknown-2", "unknown 3"), ' + "allow_missing=True)", + ["FOO=unknown"], + ) + self.assertEqual(status, 1) + self.assertEqual(config, {}) + self.assertEqual( + out, + textwrap.dedent( + """\ + checking for foo... not found + DEBUG: foo: Looking for unknown + ERROR: Cannot find foo + """ + ), + ) + + def test_check_prog_what(self): + config, out, status = self.get_result( + 'check_prog("CC", ("known-a",), what="the target C compiler")' + ) + self.assertEqual(status, 0) + self.assertEqual(config, {"CC": self.KNOWN_A}) + self.assertEqual( + out, "checking for the target C compiler... %s\n" % self.KNOWN_A + ) + + config, out, status = self.get_result( + 'check_prog("CC", ("unknown", "unknown-2", "unknown 3"),' + ' what="the target C compiler")' + ) + self.assertEqual(status, 1) + self.assertEqual(config, {}) + self.assertEqual( + out, + textwrap.dedent( + """\ + checking for the target C compiler... not found + DEBUG: cc: Looking for unknown + DEBUG: cc: Looking for unknown-2 + DEBUG: cc: Looking for 'unknown 3' + ERROR: Cannot find the target C compiler + """ + ), + ) + + def test_check_prog_input(self): + config, out, status = self.get_result( + textwrap.dedent( + """ + option("--with-ccache", nargs=1, help="ccache") + check_prog("CCACHE", ("known-a",), input="--with-ccache") + """ + ), + ["--with-ccache=known-b"], + ) + self.assertEqual(status, 0) + self.assertEqual(config, {"CCACHE": self.KNOWN_B}) + self.assertEqual(out, "checking for ccache... %s\n" % self.KNOWN_B) + + script = textwrap.dedent( + """ + option(env="CC", nargs=1, help="compiler") + @depends("CC") + def compiler(value): + return value[0].split()[0] if value else None + check_prog("CC", ("known-a",), input=compiler) + """ + ) + config, out, status = self.get_result(script) + self.assertEqual(status, 0) + self.assertEqual(config, {"CC": self.KNOWN_A}) + self.assertEqual(out, "checking for cc... %s\n" % self.KNOWN_A) + + config, out, status = self.get_result(script, ["CC=known-b"]) + self.assertEqual(status, 0) + self.assertEqual(config, {"CC": self.KNOWN_B}) + self.assertEqual(out, "checking for cc... %s\n" % self.KNOWN_B) + + config, out, status = self.get_result(script, ["CC=known-b -m32"]) + self.assertEqual(status, 0) + self.assertEqual(config, {"CC": self.KNOWN_B}) + self.assertEqual(out, "checking for cc... %s\n" % self.KNOWN_B) + + def test_check_prog_progs(self): + config, out, status = self.get_result('check_prog("FOO", ())') + self.assertEqual(status, 0) + self.assertEqual(config, {}) + self.assertEqual(out, "") + + config, out, status = self.get_result('check_prog("FOO", ())', ["FOO=known-a"]) + self.assertEqual(status, 0) + self.assertEqual(config, {"FOO": self.KNOWN_A}) + self.assertEqual(out, "checking for foo... %s\n" % self.KNOWN_A) + + script = textwrap.dedent( + """ + option(env="TARGET", nargs=1, default="linux", help="target") + @depends("TARGET") + def compiler(value): + if value: + if value[0] == "linux": + return ("gcc", "clang") + if value[0] == "winnt": + return ("cl", "clang-cl") + check_prog("CC", compiler) + """ + ) + config, out, status = self.get_result(script) + self.assertEqual(status, 1) + self.assertEqual(config, {}) + self.assertEqual( + out, + textwrap.dedent( + """\ + checking for cc... not found + DEBUG: cc: Looking for gcc + DEBUG: cc: Looking for clang + ERROR: Cannot find cc + """ + ), + ) + + config, out, status = self.get_result(script, ["TARGET=linux"]) + self.assertEqual(status, 1) + self.assertEqual(config, {}) + self.assertEqual( + out, + textwrap.dedent( + """\ + checking for cc... not found + DEBUG: cc: Looking for gcc + DEBUG: cc: Looking for clang + ERROR: Cannot find cc + """ + ), + ) + + config, out, status = self.get_result(script, ["TARGET=winnt"]) + self.assertEqual(status, 1) + self.assertEqual(config, {}) + self.assertEqual( + out, + textwrap.dedent( + """\ + checking for cc... not found + DEBUG: cc: Looking for cl + DEBUG: cc: Looking for clang-cl + ERROR: Cannot find cc + """ + ), + ) + + config, out, status = self.get_result(script, ["TARGET=none"]) + self.assertEqual(status, 0) + self.assertEqual(config, {}) + self.assertEqual(out, "") + + config, out, status = self.get_result(script, ["TARGET=winnt", "CC=known-a"]) + self.assertEqual(status, 0) + self.assertEqual(config, {"CC": self.KNOWN_A}) + self.assertEqual(out, "checking for cc... %s\n" % self.KNOWN_A) + + config, out, status = self.get_result(script, ["TARGET=none", "CC=known-a"]) + self.assertEqual(status, 0) + self.assertEqual(config, {"CC": self.KNOWN_A}) + self.assertEqual(out, "checking for cc... %s\n" % self.KNOWN_A) + + def test_check_prog_configure_error(self): + with self.assertRaises(ConfigureError) as e: + self.get_result('check_prog("FOO", "foo")') + + self.assertEqual(str(e.exception), "progs must resolve to a list or tuple!") + + with self.assertRaises(ConfigureError) as e: + self.get_result( + 'foo = depends(when=True)(lambda: ("a", "b"))\n' + 'check_prog("FOO", ("known-a",), input=foo)' + ) + + self.assertEqual( + str(e.exception), + "input must resolve to a tuple or a list with a " + "single element, or a string", + ) + + with self.assertRaises(ConfigureError) as e: + self.get_result( + 'foo = depends(when=True)(lambda: {"a": "b"})\n' + 'check_prog("FOO", ("known-a",), input=foo)' + ) + + self.assertEqual( + str(e.exception), + "input must resolve to a tuple or a list with a " + "single element, or a string", + ) + + def test_check_prog_with_path(self): + config, out, status = self.get_result( + 'check_prog("A", ("known-a",), paths=["/some/path"])' + ) + self.assertEqual(status, 1) + self.assertEqual(config, {}) + self.assertEqual( + out, + textwrap.dedent( + """\ + checking for a... not found + DEBUG: a: Looking for known-a + ERROR: Cannot find a + """ + ), + ) + + config, out, status = self.get_result( + 'check_prog("A", ("known-a",), paths=["%s"])' + % os.path.dirname(self.OTHER_A) + ) + self.assertEqual(status, 0) + self.assertEqual(config, {"A": self.OTHER_A}) + self.assertEqual( + out, + textwrap.dedent( + """\ + checking for a... %s + """ + % self.OTHER_A + ), + ) + + dirs = map(mozpath.dirname, (self.OTHER_A, self.KNOWN_A)) + config, out, status = self.get_result( + textwrap.dedent( + """\ + check_prog("A", ("known-a",), paths=["%s"]) + """ + % os.pathsep.join(dirs) + ) + ) + self.assertEqual(status, 0) + self.assertEqual(config, {"A": self.OTHER_A}) + self.assertEqual( + out, + textwrap.dedent( + """\ + checking for a... %s + """ + % self.OTHER_A + ), + ) + + dirs = map(mozpath.dirname, (self.KNOWN_A, self.KNOWN_B)) + config, out, status = self.get_result( + textwrap.dedent( + """\ + check_prog("A", ("known-a",), paths=["%s", "%s"]) + """ + % (os.pathsep.join(dirs), self.OTHER_A) + ) + ) + self.assertEqual(status, 0) + self.assertEqual(config, {"A": self.KNOWN_A}) + self.assertEqual( + out, + textwrap.dedent( + """\ + checking for a... %s + """ + % self.KNOWN_A + ), + ) + + config, out, status = self.get_result( + 'check_prog("A", ("known-a",), paths="%s")' % os.path.dirname(self.OTHER_A) + ) + + self.assertEqual(status, 1) + self.assertEqual(config, {}) + self.assertEqual( + out, + textwrap.dedent( + """\ + checking for a... """ # noqa # trailing whitespace... + """ + DEBUG: a: Looking for known-a + ERROR: Paths provided to find_program must be a list of strings, not %r + """ + % mozpath.dirname(self.OTHER_A) + ), + ) + + @unittest.skipIf( + not sys.platform.startswith("linux"), + "Linux-only test, assumes Java is located from a $PATH", + ) + def test_java_tool_checks_linux(self): + def run_configure_java( + mock_fs_paths, mock_java_home=None, mock_path=None, args=[] + ): + script = textwrap.dedent( + """\ + @depends('--help') + def host(_): + return namespace(os='unknown', kernel='unknown') + toolchains_base_dir = depends(when=True)(lambda: '/mozbuild') + include('%(topsrcdir)s/build/moz.configure/java.configure') + """ + % {"topsrcdir": topsrcdir} + ) + + # Don't let system JAVA_HOME influence the test + original_java_home = os.environ.pop("JAVA_HOME", None) + configure_environ = {} + + if mock_java_home: + os.environ["JAVA_HOME"] = mock_java_home + configure_environ["JAVA_HOME"] = mock_java_home + + if mock_path: + configure_environ["PATH"] = mock_path + + # * Even if the real file sysphabtem has a symlink at the mocked path, don't let + # realpath follow it, as it may influence the test. + # * When finding a binary, check the mock paths rather than the real filesystem. + # Note: Python doesn't allow the different "with" bits to be put in parenthesis, + # because then it thinks it's an un-with-able tuple. Additionally, if this is cleanly + # lined up with "\", black removes them and autoformats them to the block that is + # below. + result = self.get_result( + args=args, + command=script, + extra_paths=paths, + environ=configure_environ, + ) + + if original_java_home: + os.environ["JAVA_HOME"] = original_java_home + return result + + java = mozpath.abspath("/usr/bin/java") + javac = mozpath.abspath("/usr/bin/javac") + paths = {java: None, javac: None} + expected_error_message = ( + "ERROR: Could not locate Java at /mozbuild/jdk/jdk-17.0.9+9/bin, " + "please run ./mach bootstrap --no-system-changes\n" + ) + + config, out, status = run_configure_java(paths) + self.assertEqual(status, 1) + self.assertEqual(config, {}) + self.assertEqual(out, expected_error_message) + + # An alternative valid set of tools referred to by JAVA_HOME. + alt_java = mozpath.abspath("/usr/local/bin/java") + alt_javac = mozpath.abspath("/usr/local/bin/javac") + alt_java_home = mozpath.dirname(mozpath.dirname(alt_java)) + paths = {alt_java: None, alt_javac: None, java: None, javac: None} + + alt_path = mozpath.dirname(java) + config, out, status = run_configure_java(paths, alt_java_home, alt_path) + self.assertEqual(status, 1) + self.assertEqual(config, {}) + self.assertEqual(out, expected_error_message) + + # We can use --with-java-bin-path instead of JAVA_HOME to similar + # effect. + config, out, status = run_configure_java( + paths, + mock_path=mozpath.dirname(java), + args=["--with-java-bin-path=%s" % mozpath.dirname(alt_java)], + ) + self.assertEqual(status, 0) + self.assertEqual(config, {"JAVA": alt_java, "MOZ_JAVA_CODE_COVERAGE": False}) + self.assertEqual( + out, + textwrap.dedent( + """\ + checking for java... %s + """ + % alt_java + ), + ) + + # If --with-java-bin-path and JAVA_HOME are both set, + # --with-java-bin-path takes precedence. + config, out, status = run_configure_java( + paths, + mock_java_home=mozpath.dirname(mozpath.dirname(java)), + mock_path=mozpath.dirname(java), + args=["--with-java-bin-path=%s" % mozpath.dirname(alt_java)], + ) + self.assertEqual(status, 0) + self.assertEqual(config, {"JAVA": alt_java, "MOZ_JAVA_CODE_COVERAGE": False}) + self.assertEqual( + out, + textwrap.dedent( + """\ + checking for java... %s + """ + % alt_java + ), + ) + + # --enable-java-coverage should set MOZ_JAVA_CODE_COVERAGE. + alt_java_home = mozpath.dirname(mozpath.dirname(java)) + config, out, status = run_configure_java( + paths, + mock_java_home=alt_java_home, + mock_path=mozpath.dirname(java), + args=["--enable-java-coverage"], + ) + self.assertEqual(status, 1) + self.assertEqual(config, {}) + + # Any missing tool is fatal when these checks run. + paths = {} + config, out, status = run_configure_java( + mock_fs_paths={}, + mock_path=mozpath.dirname(java), + args=["--enable-java-coverage"], + ) + self.assertEqual(status, 1) + self.assertEqual(config, {}) + self.assertEqual(out, expected_error_message) + + def test_pkg_check_modules(self): + mock_pkg_config_version = "0.10.0" + mock_pkg_config_path = mozpath.abspath("/usr/bin/pkg-config") + + seen_flags = set() + + def mock_pkg_config(_, args): + if "--dont-define-prefix" in args: + args = list(args) + seen_flags.add(args.pop(args.index("--dont-define-prefix"))) + args = tuple(args) + if args[0:2] == ("--errors-to-stdout", "--print-errors"): + assert len(args) == 3 + package = args[2] + if package == "unknown": + return ( + 1, + "Package unknown was not found in the pkg-config search path.\n" + "Perhaps you should add the directory containing `unknown.pc'\n" + "to the PKG_CONFIG_PATH environment variable\n" + "No package 'unknown' found", + "", + ) + if package == "valid": + return 0, "", "" + if package == "new > 1.1": + return 1, "Requested 'new > 1.1' but version of new is 1.1", "" + if args[0] == "--cflags": + assert len(args) == 2 + return 0, "-I/usr/include/%s" % args[1], "" + if args[0] == "--libs": + assert len(args) == 2 + return 0, "-l%s" % args[1], "" + if args[0] == "--version": + return 0, mock_pkg_config_version, "" + if args[0] == "--about": + return 1, "Unknown option --about", "" + self.fail("Unexpected arguments to mock_pkg_config: %s" % (args,)) + + def mock_pkgconf(_, args): + if args[0] == "--shared": + seen_flags.add(args[0]) + args = args[1:] + if args[0] == "--about": + return 0, "pkgconf {}".format(mock_pkg_config_version), "" + return mock_pkg_config(_, args) + + def get_result(cmd, args=[], bootstrapped_sysroot=False, extra_paths=None): + return self.get_result( + textwrap.dedent( + """\ + option('--disable-compile-environment', help='compile env') + compile_environment = depends(when='--enable-compile-environment')(lambda: True) + toolchain_prefix = depends(when=True)(lambda: None) + target_multiarch_dir = depends(when=True)(lambda: None) + target_sysroot = depends(when=True)(lambda: %(sysroot)s) + target = depends(when=True)(lambda: None) + include('%(topsrcdir)s/build/moz.configure/util.configure') + include('%(topsrcdir)s/build/moz.configure/checks.configure') + # Skip bootstrapping. + @template + def check_prog(*args, **kwargs): + del kwargs["bootstrap"] + return check_prog(*args, **kwargs) + include('%(topsrcdir)s/build/moz.configure/pkg.configure') + """ + % { + "topsrcdir": topsrcdir, + "sysroot": "namespace(bootstrapped=True)" + if bootstrapped_sysroot + else "None", + } + ) + + cmd, + args=args, + extra_paths=extra_paths, + includes=(), + ) + + extra_paths = {mock_pkg_config_path: mock_pkg_config} + + config, output, status = get_result("pkg_check_modules('MOZ_VALID', 'valid')") + self.assertEqual(status, 1) + self.assertEqual( + output, + textwrap.dedent( + """\ + checking for pkg_config... not found + ERROR: *** The pkg-config script could not be found. Make sure it is + *** in your path, or set the PKG_CONFIG environment variable + *** to the full path to pkg-config. + """ + ), + ) + + for pkg_config, version, bootstrapped_sysroot, is_pkgconf in ( + (mock_pkg_config, "0.10.0", False, False), + (mock_pkg_config, "0.30.0", False, False), + (mock_pkg_config, "0.30.0", True, False), + (mock_pkgconf, "1.1.0", True, True), + (mock_pkgconf, "1.6.0", False, True), + (mock_pkgconf, "1.8.0", False, True), + (mock_pkgconf, "1.8.0", True, True), + ): + seen_flags = set() + mock_pkg_config_version = version + config, output, status = get_result( + "pkg_check_modules('MOZ_VALID', 'valid')", + bootstrapped_sysroot=bootstrapped_sysroot, + extra_paths={mock_pkg_config_path: pkg_config}, + ) + self.assertEqual(status, 0) + self.assertEqual( + output, + textwrap.dedent( + """\ + checking for pkg_config... %s + checking for pkg-config version... %s + checking whether pkg-config is pkgconf... %s + checking for valid... yes + checking MOZ_VALID_CFLAGS... -I/usr/include/valid + checking MOZ_VALID_LIBS... -lvalid + """ + % ( + mock_pkg_config_path, + mock_pkg_config_version, + "yes" if is_pkgconf else "no", + ) + ), + ) + self.assertEqual( + config, + { + "PKG_CONFIG": mock_pkg_config_path, + "MOZ_VALID_CFLAGS": ("-I/usr/include/valid",), + "MOZ_VALID_LIBS": ("-lvalid",), + }, + ) + if version == "1.8.0" and bootstrapped_sysroot: + self.assertEqual(seen_flags, set(["--shared", "--dont-define-prefix"])) + elif version == "1.8.0": + self.assertEqual(seen_flags, set(["--shared"])) + elif version in ("1.6.0", "0.30.0") and bootstrapped_sysroot: + self.assertEqual(seen_flags, set(["--dont-define-prefix"])) + else: + self.assertEqual(seen_flags, set()) + + config, output, status = get_result( + "pkg_check_modules('MOZ_UKNOWN', 'unknown')", extra_paths=extra_paths + ) + self.assertEqual(status, 1) + self.assertEqual( + output, + textwrap.dedent( + """\ + checking for pkg_config... %s + checking for pkg-config version... %s + checking whether pkg-config is pkgconf... no + checking for unknown... no + ERROR: Package unknown was not found in the pkg-config search path. + ERROR: Perhaps you should add the directory containing `unknown.pc' + ERROR: to the PKG_CONFIG_PATH environment variable + ERROR: No package 'unknown' found + """ + % (mock_pkg_config_path, mock_pkg_config_version) + ), + ) + self.assertEqual(config, {"PKG_CONFIG": mock_pkg_config_path}) + + config, output, status = get_result( + "pkg_check_modules('MOZ_NEW', 'new > 1.1')", extra_paths=extra_paths + ) + self.assertEqual(status, 1) + self.assertEqual( + output, + textwrap.dedent( + """\ + checking for pkg_config... %s + checking for pkg-config version... %s + checking whether pkg-config is pkgconf... no + checking for new > 1.1... no + ERROR: Requested 'new > 1.1' but version of new is 1.1 + """ + % (mock_pkg_config_path, mock_pkg_config_version) + ), + ) + self.assertEqual(config, {"PKG_CONFIG": mock_pkg_config_path}) + + # allow_missing makes missing packages non-fatal. + cmd = textwrap.dedent( + """\ + have_new_module = pkg_check_modules('MOZ_NEW', 'new > 1.1', allow_missing=True) + @depends(have_new_module) + def log_new_module_error(mod): + if mod is not True: + log.info('Module not found.') + """ + ) + + config, output, status = get_result(cmd, extra_paths=extra_paths) + self.assertEqual(status, 0) + self.assertEqual( + output, + textwrap.dedent( + """\ + checking for pkg_config... %s + checking for pkg-config version... %s + checking whether pkg-config is pkgconf... no + checking for new > 1.1... no + WARNING: Requested 'new > 1.1' but version of new is 1.1 + Module not found. + """ + % (mock_pkg_config_path, mock_pkg_config_version) + ), + ) + self.assertEqual(config, {"PKG_CONFIG": mock_pkg_config_path}) + + config, output, status = get_result( + cmd, args=["--disable-compile-environment"], extra_paths=extra_paths + ) + self.assertEqual(status, 0) + self.assertEqual(output, "Module not found.\n") + self.assertEqual(config, {}) + + def mock_old_pkg_config(_, args): + if args[0] == "--version": + return 0, "0.8.10", "" + if args[0] == "--about": + return 1, "Unknown option --about", "" + self.fail("Unexpected arguments to mock_old_pkg_config: %s" % args) + + extra_paths = {mock_pkg_config_path: mock_old_pkg_config} + + config, output, status = get_result( + "pkg_check_modules('MOZ_VALID', 'valid')", extra_paths=extra_paths + ) + self.assertEqual(status, 1) + self.assertEqual( + output, + textwrap.dedent( + """\ + checking for pkg_config... %s + checking for pkg-config version... 0.8.10 + checking whether pkg-config is pkgconf... no + ERROR: *** Your version of pkg-config is too old. You need version 0.9.0 or newer. + """ + % mock_pkg_config_path + ), + ) + + def test_simple_keyfile(self): + includes = ("util.configure", "checks.configure", "keyfiles.configure") + + config, output, status = self.get_result( + "simple_keyfile('Mozilla API')", includes=includes + ) + self.assertEqual(status, 0) + self.assertEqual( + output, + textwrap.dedent( + """\ + checking for the Mozilla API key... no + """ + ), + ) + self.assertEqual(config, {"MOZ_MOZILLA_API_KEY": "no-mozilla-api-key"}) + + config, output, status = self.get_result( + "simple_keyfile('Mozilla API')", + args=["--with-mozilla-api-keyfile=/foo/bar/does/not/exist"], + includes=includes, + ) + self.assertEqual(status, 1) + self.assertEqual( + output, + textwrap.dedent( + """\ + checking for the Mozilla API key... no + ERROR: '/foo/bar/does/not/exist': No such file or directory. + """ + ), + ) + self.assertEqual(config, {}) + + with MockedOpen({"key": ""}): + config, output, status = self.get_result( + "simple_keyfile('Mozilla API')", + args=["--with-mozilla-api-keyfile=key"], + includes=includes, + ) + self.assertEqual(status, 1) + self.assertEqual( + output, + textwrap.dedent( + """\ + checking for the Mozilla API key... no + ERROR: 'key' is empty. + """ + ), + ) + self.assertEqual(config, {}) + + with MockedOpen({"key": "fake-key\n"}): + config, output, status = self.get_result( + "simple_keyfile('Mozilla API')", + args=["--with-mozilla-api-keyfile=key"], + includes=includes, + ) + self.assertEqual(status, 0) + self.assertEqual( + output, + textwrap.dedent( + """\ + checking for the Mozilla API key... yes + """ + ), + ) + self.assertEqual(config, {"MOZ_MOZILLA_API_KEY": "fake-key"}) + + with MockedOpen({"default": "default-key\n"}): + config, output, status = self.get_result( + "simple_keyfile('Mozilla API', default='default')", includes=includes + ) + self.assertEqual(status, 0) + self.assertEqual( + output, + textwrap.dedent( + """\ + checking for the Mozilla API key... yes + """ + ), + ) + self.assertEqual(config, {"MOZ_MOZILLA_API_KEY": "default-key"}) + + with MockedOpen({"default": "default-key\n", "key": "fake-key\n"}): + config, output, status = self.get_result( + "simple_keyfile('Mozilla API', default='key')", includes=includes + ) + self.assertEqual(status, 0) + self.assertEqual( + output, + textwrap.dedent( + """\ + checking for the Mozilla API key... yes + """ + ), + ) + self.assertEqual(config, {"MOZ_MOZILLA_API_KEY": "fake-key"}) + + def test_id_and_secret_keyfile(self): + includes = ("util.configure", "checks.configure", "keyfiles.configure") + + config, output, status = self.get_result( + "id_and_secret_keyfile('Bing API')", includes=includes + ) + self.assertEqual(status, 0) + self.assertEqual( + output, + textwrap.dedent( + """\ + checking for the Bing API key... no + """ + ), + ) + self.assertEqual( + config, + { + "MOZ_BING_API_CLIENTID": "no-bing-api-clientid", + "MOZ_BING_API_KEY": "no-bing-api-key", + }, + ) + + config, output, status = self.get_result( + "id_and_secret_keyfile('Bing API')", + args=["--with-bing-api-keyfile=/foo/bar/does/not/exist"], + includes=includes, + ) + self.assertEqual(status, 1) + self.assertEqual( + output, + textwrap.dedent( + """\ + checking for the Bing API key... no + ERROR: '/foo/bar/does/not/exist': No such file or directory. + """ + ), + ) + self.assertEqual(config, {}) + + with MockedOpen({"key": ""}): + config, output, status = self.get_result( + "id_and_secret_keyfile('Bing API')", + args=["--with-bing-api-keyfile=key"], + includes=includes, + ) + self.assertEqual(status, 1) + self.assertEqual( + output, + textwrap.dedent( + """\ + checking for the Bing API key... no + ERROR: 'key' is empty. + """ + ), + ) + self.assertEqual(config, {}) + + with MockedOpen({"key": "fake-id fake-key\n"}): + config, output, status = self.get_result( + "id_and_secret_keyfile('Bing API')", + args=["--with-bing-api-keyfile=key"], + includes=includes, + ) + self.assertEqual(status, 0) + self.assertEqual( + output, + textwrap.dedent( + """\ + checking for the Bing API key... yes + """ + ), + ) + self.assertEqual( + config, + {"MOZ_BING_API_CLIENTID": "fake-id", "MOZ_BING_API_KEY": "fake-key"}, + ) + + with MockedOpen({"key": "fake-key\n"}): + config, output, status = self.get_result( + "id_and_secret_keyfile('Bing API')", + args=["--with-bing-api-keyfile=key"], + includes=includes, + ) + self.assertEqual(status, 1) + self.assertEqual( + output, + textwrap.dedent( + """\ + checking for the Bing API key... no + ERROR: Bing API key file has an invalid format. + """ + ), + ) + self.assertEqual(config, {}) + + with MockedOpen({"default-key": "default-id default-key\n"}): + config, output, status = self.get_result( + "id_and_secret_keyfile('Bing API', default='default-key')", + includes=includes, + ) + self.assertEqual(status, 0) + self.assertEqual( + output, + textwrap.dedent( + """\ + checking for the Bing API key... yes + """ + ), + ) + self.assertEqual( + config, + { + "MOZ_BING_API_CLIENTID": "default-id", + "MOZ_BING_API_KEY": "default-key", + }, + ) + + with MockedOpen( + {"default-key": "default-id default-key\n", "key": "fake-id fake-key\n"} + ): + config, output, status = self.get_result( + "id_and_secret_keyfile('Bing API', default='default-key')", + args=["--with-bing-api-keyfile=key"], + includes=includes, + ) + self.assertEqual(status, 0) + self.assertEqual( + output, + textwrap.dedent( + """\ + checking for the Bing API key... yes + """ + ), + ) + self.assertEqual( + config, + {"MOZ_BING_API_CLIENTID": "fake-id", "MOZ_BING_API_KEY": "fake-key"}, + ) + + +if __name__ == "__main__": + main() diff --git a/python/mozbuild/mozbuild/test/configure/test_compile_checks.py b/python/mozbuild/mozbuild/test/configure/test_compile_checks.py new file mode 100644 index 0000000000..1e3fccd05c --- /dev/null +++ b/python/mozbuild/mozbuild/test/configure/test_compile_checks.py @@ -0,0 +1,596 @@ +# 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/. + +import os +import textwrap +import unittest + +import mozpack.path as mozpath +from buildconfig import topsrcdir +from mozunit import main +from six import StringIO +from test_toolchain_helpers import FakeCompiler + +from common import ConfigureTestSandbox + + +class BaseCompileChecks(unittest.TestCase): + def get_mock_compiler(self, expected_test_content=None, expected_flags=None): + expected_flags = expected_flags or [] + + def mock_compiler(stdin, args): + if args != ["--version"]: + test_file = [a for a in args if not a.startswith("-")] + self.assertEqual(len(test_file), 1) + test_file = test_file[0] + args = [a for a in args if a.startswith("-")] + self.assertIn("-c", args) + for flag in expected_flags: + self.assertIn(flag, args) + + if expected_test_content: + with open(test_file) as fh: + test_content = fh.read() + self.assertEqual(test_content, expected_test_content) + + return FakeCompiler()(None, args) + + return mock_compiler + + def do_compile_test(self, command, expected_test_content=None, expected_flags=None): + paths = { + os.path.abspath("/usr/bin/mockcc"): self.get_mock_compiler( + expected_test_content=expected_test_content, + expected_flags=expected_flags, + ), + } + + base_dir = os.path.join(topsrcdir, "build", "moz.configure") + + mock_compiler_defs = textwrap.dedent( + """\ + @depends(when=True) + def extra_toolchain_flags(): + return [] + + @depends(when=True) + def linker_ldflags(): + return [] + + target = depends(when=True)(lambda: True) + + @depends(when=True) + def configure_cache(): + + class ConfigureCache(dict): + pass + + cache_data = {} + + cache = ConfigureCache(cache_data) + cache.version_checked_compilers = set() + + return cache + + include('%s/compilers-util.configure') + + @template + def wrap_compiler(compiler): + return compiler_class(compiler, False) + + @wrap_compiler + @depends(when=True) + def c_compiler(): + return namespace( + flags=[], + type='gcc', + compiler=os.path.abspath('/usr/bin/mockcc'), + wrapper=[], + language='C', + ) + + @wrap_compiler + @depends(when=True) + def host_c_compiler(): + return namespace( + flags=[], + type='gcc', + compiler=os.path.abspath('/usr/bin/mockcc'), + wrapper=[], + language='C', + ) + + @wrap_compiler + @depends(when=True) + def cxx_compiler(): + return namespace( + flags=[], + type='gcc', + compiler=os.path.abspath('/usr/bin/mockcc'), + wrapper=[], + language='C++', + ) + + @wrap_compiler + @depends(when=True) + def host_cxx_compiler(): + return namespace( + flags=[], + type='gcc', + compiler=os.path.abspath('/usr/bin/mockcc'), + wrapper=[], + language='C++', + ) + """ + % mozpath.normsep(base_dir) + ) + + config = {} + out = StringIO() + sandbox = ConfigureTestSandbox(paths, config, {}, ["/bin/configure"], out, out) + sandbox.include_file(os.path.join(base_dir, "util.configure")) + sandbox.include_file(os.path.join(base_dir, "checks.configure")) + exec(mock_compiler_defs, sandbox) + sandbox.include_file(os.path.join(base_dir, "compile-checks.configure")) + + status = 0 + try: + exec(command, sandbox) + sandbox.run() + except SystemExit as e: + status = e.code + + return config, out.getvalue(), status + + +class TestHeaderChecks(BaseCompileChecks): + def test_try_compile_include(self): + expected_test_content = textwrap.dedent( + """\ + #include <foo.h> + #include <bar.h> + int + main(void) + { + + ; + return 0; + } + """ + ) + + cmd = textwrap.dedent( + """\ + try_compile(['foo.h', 'bar.h'], language='C') + """ + ) + + config, out, status = self.do_compile_test(cmd, expected_test_content) + self.assertEqual(status, 0) + self.assertEqual(config, {}) + + def test_try_compile_flags(self): + expected_flags = ["--extra", "--flags"] + + cmd = textwrap.dedent( + """\ + try_compile(language='C++', flags=['--flags', '--extra']) + """ + ) + + config, out, status = self.do_compile_test(cmd, expected_flags=expected_flags) + self.assertEqual(status, 0) + self.assertEqual(config, {}) + + def test_try_compile_failure(self): + cmd = textwrap.dedent( + """\ + have_fn = try_compile(body='somefn();', flags=['-funknown-flag']) + set_config('HAVE_SOMEFN', have_fn) + + have_another = try_compile(body='anotherfn();', language='C') + set_config('HAVE_ANOTHERFN', have_another) + """ + ) + + config, out, status = self.do_compile_test(cmd) + self.assertEqual(status, 0) + self.assertEqual( + config, + { + "HAVE_ANOTHERFN": True, + }, + ) + + def test_try_compile_msg(self): + cmd = textwrap.dedent( + """\ + known_flag = try_compile(language='C++', flags=['-fknown-flag'], + check_msg='whether -fknown-flag works') + set_config('HAVE_KNOWN_FLAG', known_flag) + """ + ) + config, out, status = self.do_compile_test(cmd) + self.assertEqual(status, 0) + self.assertEqual(config, {"HAVE_KNOWN_FLAG": True}) + self.assertEqual( + out, + textwrap.dedent( + """\ + checking whether -fknown-flag works... yes + """ + ), + ) + + def test_check_header(self): + expected_test_content = textwrap.dedent( + """\ + #include <foo.h> + int + main(void) + { + + ; + return 0; + } + """ + ) + + cmd = textwrap.dedent( + """\ + check_header('foo.h') + """ + ) + + config, out, status = self.do_compile_test( + cmd, expected_test_content=expected_test_content + ) + self.assertEqual(status, 0) + self.assertEqual(config, {"DEFINES": {"HAVE_FOO_H": True}}) + self.assertEqual( + out, + textwrap.dedent( + """\ + checking for foo.h... yes + """ + ), + ) + + def test_check_header_conditional(self): + cmd = textwrap.dedent( + """\ + check_headers('foo.h', 'bar.h', when=never) + """ + ) + + config, out, status = self.do_compile_test(cmd) + self.assertEqual(status, 0) + self.assertEqual(out, "") + self.assertEqual(config, {"DEFINES": {}}) + + def test_check_header_include(self): + expected_test_content = textwrap.dedent( + """\ + #include <std.h> + #include <bar.h> + #include <foo.h> + int + main(void) + { + + ; + return 0; + } + """ + ) + + cmd = textwrap.dedent( + """\ + have_foo = check_header('foo.h', includes=['std.h', 'bar.h']) + set_config('HAVE_FOO_H', have_foo) + """ + ) + + config, out, status = self.do_compile_test( + cmd, expected_test_content=expected_test_content + ) + + self.assertEqual(status, 0) + self.assertEqual( + config, + { + "HAVE_FOO_H": True, + "DEFINES": { + "HAVE_FOO_H": True, + }, + }, + ) + self.assertEqual( + out, + textwrap.dedent( + """\ + checking for foo.h... yes + """ + ), + ) + + def test_check_headers_multiple(self): + cmd = textwrap.dedent( + """\ + baz_bar, quux_bar = check_headers('baz/foo-bar.h', 'baz-quux/foo-bar.h') + set_config('HAVE_BAZ_BAR', baz_bar) + set_config('HAVE_QUUX_BAR', quux_bar) + """ + ) + + config, out, status = self.do_compile_test(cmd) + self.assertEqual(status, 0) + self.assertEqual( + config, + { + "HAVE_BAZ_BAR": True, + "HAVE_QUUX_BAR": True, + "DEFINES": { + "HAVE_BAZ_FOO_BAR_H": True, + "HAVE_BAZ_QUUX_FOO_BAR_H": True, + }, + }, + ) + self.assertEqual( + out, + textwrap.dedent( + """\ + checking for baz/foo-bar.h... yes + checking for baz-quux/foo-bar.h... yes + """ + ), + ) + + def test_check_headers_not_found(self): + cmd = textwrap.dedent( + """\ + baz_bar, quux_bar = check_headers('baz/foo-bar.h', 'baz-quux/foo-bar.h', + flags=['-funknown-flag']) + set_config('HAVE_BAZ_BAR', baz_bar) + set_config('HAVE_QUUX_BAR', quux_bar) + """ + ) + + config, out, status = self.do_compile_test(cmd) + self.assertEqual(status, 0) + self.assertEqual(config, {"DEFINES": {}}) + self.assertEqual( + out, + textwrap.dedent( + """\ + checking for baz/foo-bar.h... no + checking for baz-quux/foo-bar.h... no + """ + ), + ) + + +class TestWarningChecks(BaseCompileChecks): + def get_warnings(self): + return textwrap.dedent( + """\ + set_config('_WARNINGS_CFLAGS', warnings_flags.cflags) + set_config('_WARNINGS_CXXFLAGS', warnings_flags.cxxflags) + """ + ) + + def test_check_and_add_warning(self): + for flag, expected_flags in ( + ("-Wfoo", ["-Werror", "-Wfoo"]), + ("-Wno-foo", ["-Werror", "-Wfoo"]), + ("-Werror=foo", ["-Werror=foo"]), + ("-Wno-error=foo", ["-Wno-error=foo"]), + ): + cmd = ( + textwrap.dedent( + """\ + check_and_add_warning('%s') + """ + % flag + ) + + self.get_warnings() + ) + + config, out, status = self.do_compile_test( + cmd, expected_flags=expected_flags + ) + self.assertEqual(status, 0) + self.assertEqual( + config, + { + "_WARNINGS_CFLAGS": [flag], + "_WARNINGS_CXXFLAGS": [flag], + }, + ) + self.assertEqual( + out, + textwrap.dedent( + """\ + checking whether the C compiler supports {flag}... yes + checking whether the C++ compiler supports {flag}... yes + """.format( + flag=flag + ) + ), + ) + + def test_check_and_add_warning_one(self): + cmd = ( + textwrap.dedent( + """\ + check_and_add_warning('-Wfoo', cxx_compiler) + """ + ) + + self.get_warnings() + ) + + config, out, status = self.do_compile_test(cmd) + self.assertEqual(status, 0) + self.assertEqual( + config, + { + "_WARNINGS_CFLAGS": [], + "_WARNINGS_CXXFLAGS": ["-Wfoo"], + }, + ) + self.assertEqual( + out, + textwrap.dedent( + """\ + checking whether the C++ compiler supports -Wfoo... yes + """ + ), + ) + + def test_check_and_add_warning_when(self): + cmd = ( + textwrap.dedent( + """\ + @depends(when=True) + def never(): + return False + check_and_add_warning('-Wfoo', cxx_compiler, when=never) + """ + ) + + self.get_warnings() + ) + + config, out, status = self.do_compile_test(cmd) + self.assertEqual(status, 0) + self.assertEqual( + config, + { + "_WARNINGS_CFLAGS": [], + "_WARNINGS_CXXFLAGS": [], + }, + ) + self.assertEqual(out, "") + + cmd = ( + textwrap.dedent( + """\ + @depends(when=True) + def always(): + return True + check_and_add_warning('-Wfoo', cxx_compiler, when=always) + """ + ) + + self.get_warnings() + ) + + config, out, status = self.do_compile_test(cmd) + self.assertEqual(status, 0) + self.assertEqual( + config, + { + "_WARNINGS_CFLAGS": [], + "_WARNINGS_CXXFLAGS": ["-Wfoo"], + }, + ) + self.assertEqual( + out, + textwrap.dedent( + """\ + checking whether the C++ compiler supports -Wfoo... yes + """ + ), + ) + + def test_add_warning(self): + cmd = ( + textwrap.dedent( + """\ + add_warning('-Wfoo') + """ + ) + + self.get_warnings() + ) + + config, out, status = self.do_compile_test(cmd) + self.assertEqual(status, 0) + self.assertEqual( + config, + { + "_WARNINGS_CFLAGS": ["-Wfoo"], + "_WARNINGS_CXXFLAGS": ["-Wfoo"], + }, + ) + self.assertEqual(out, "") + + def test_add_warning_one(self): + cmd = ( + textwrap.dedent( + """\ + add_warning('-Wfoo', c_compiler) + """ + ) + + self.get_warnings() + ) + + config, out, status = self.do_compile_test(cmd) + self.assertEqual(status, 0) + self.assertEqual( + config, + { + "_WARNINGS_CFLAGS": ["-Wfoo"], + "_WARNINGS_CXXFLAGS": [], + }, + ) + self.assertEqual(out, "") + + def test_add_warning_when(self): + cmd = ( + textwrap.dedent( + """\ + @depends(when=True) + def never(): + return False + add_warning('-Wfoo', c_compiler, when=never) + """ + ) + + self.get_warnings() + ) + + config, out, status = self.do_compile_test(cmd) + self.assertEqual(status, 0) + self.assertEqual( + config, + { + "_WARNINGS_CFLAGS": [], + "_WARNINGS_CXXFLAGS": [], + }, + ) + self.assertEqual(out, "") + + cmd = ( + textwrap.dedent( + """\ + @depends(when=True) + def always(): + return True + add_warning('-Wfoo', c_compiler, when=always) + """ + ) + + self.get_warnings() + ) + + config, out, status = self.do_compile_test(cmd) + self.assertEqual(status, 0) + self.assertEqual( + config, + { + "_WARNINGS_CFLAGS": ["-Wfoo"], + "_WARNINGS_CXXFLAGS": [], + }, + ) + self.assertEqual(out, "") + + +if __name__ == "__main__": + main() diff --git a/python/mozbuild/mozbuild/test/configure/test_configure.py b/python/mozbuild/mozbuild/test/configure/test_configure.py new file mode 100644 index 0000000000..d075477a44 --- /dev/null +++ b/python/mozbuild/mozbuild/test/configure/test_configure.py @@ -0,0 +1,2033 @@ +# 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/. + +import os +import sys +import textwrap +import unittest + +import mozpack.path as mozpath +import six +from mozunit import MockedOpen, main +from six import StringIO + +from mozbuild.configure import ConfigureError, ConfigureSandbox +from mozbuild.configure.options import ( + InvalidOptionError, + NegativeOptionValue, + PositiveOptionValue, +) +from mozbuild.util import ReadOnlyNamespace, memoized_property + +test_data_path = mozpath.abspath(mozpath.dirname(__file__)) +test_data_path = mozpath.join(test_data_path, "data") + + +class TestConfigure(unittest.TestCase): + def get_config( + self, options=[], env={}, configure="moz.configure", prog="/bin/configure" + ): + config = {} + out = StringIO() + sandbox = ConfigureSandbox(config, env, [prog] + options, out, out) + + sandbox.run(mozpath.join(test_data_path, configure)) + + if "--help" in options: + return six.ensure_text(out.getvalue()), config + self.assertEqual("", out.getvalue()) + return config + + def moz_configure(self, source): + return MockedOpen( + {os.path.join(test_data_path, "moz.configure"): textwrap.dedent(source)} + ) + + def test_defaults(self): + config = self.get_config() + self.maxDiff = None + self.assertEqual( + { + "CHOICES": NegativeOptionValue(), + "DEFAULTED": PositiveOptionValue(("not-simple",)), + "IS_GCC": NegativeOptionValue(), + "REMAINDER": ( + PositiveOptionValue(), + NegativeOptionValue(), + NegativeOptionValue(), + NegativeOptionValue(), + NegativeOptionValue(), + NegativeOptionValue(), + PositiveOptionValue(), + ), + "SIMPLE": NegativeOptionValue(), + "VALUES": NegativeOptionValue(), + "VALUES2": NegativeOptionValue(), + "VALUES3": NegativeOptionValue(), + "WITH_ENV": NegativeOptionValue(), + }, + config, + ) + + def test_help(self): + help, config = self.get_config(["--help"], prog="configure") + + self.assertEqual({}, config) + self.maxDiff = None + self.assertEqual( + "Usage: configure [options]\n" + "\n" + "Options: [defaults in brackets after descriptions]\n" + " Help options:\n" + " --help print this message\n" + "\n" + " Options from python/mozbuild/mozbuild/test/configure/data/moz.configure:\n" + " --enable-simple Enable simple\n" + " --enable-with-env Enable with env\n" + " --enable-values Enable values\n" + " --enable-choices={a,b,c} Enable choices\n" + " --without-thing Build without thing\n" + " --with-stuff Build with stuff\n" + " --option Option\n" + " --with-returned-default Returned default [not-simple]\n" + " --with-other-default With other default\n" + " --returned-choices Choices\n" + " --enable-foo={x,y} Enable Foo\n" + " --disable-foo Disable Foo\n" + " --enable-include Include\n" + " --with-imports Imports\n" + " --indirect-option Indirectly defined option\n" + "\n" + " Options from python/mozbuild/mozbuild/test/configure/data/included.configure:\n" + " --enable-imports-in-template\n" + " Imports in template\n" + "\n" + "\n" + "Environment variables:\n" + " Options from python/mozbuild/mozbuild/test/configure/data/moz.configure:\n" + " CC C Compiler\n" + "\n", + help.replace("\\", "/"), + ) + + help, config = self.get_config( + ["--help", "--enable-simple", "--enable-values=numeric"], prog="configure" + ) + + self.assertEqual({}, config) + self.maxDiff = None + self.assertEqual( + "Usage: configure [options]\n" + "\n" + "Options: [defaults in brackets after descriptions]\n" + " Help options:\n" + " --help print this message\n" + "\n" + " Options from python/mozbuild/mozbuild/test/configure/data/moz.configure:\n" + " --enable-simple Enable simple\n" + " --enable-with-env Enable with env\n" + " --enable-values Enable values\n" + " --enable-choices={a,b,c} Enable choices\n" + " --without-thing Build without thing\n" + " --with-stuff Build with stuff\n" + " --option Option\n" + " --with-returned-default Returned default [simple]\n" + " --without-other-default Without other default\n" + " --returned-choices={0,1,2}\n" + " Choices\n" + " --enable-foo={x,y} Enable Foo\n" + " --disable-foo Disable Foo\n" + " --enable-include Include\n" + " --with-imports Imports\n" + " --indirect-option Indirectly defined option\n" + "\n" + " Options from python/mozbuild/mozbuild/test/configure/data/included.configure:\n" + " --enable-imports-in-template\n" + " Imports in template\n" + "\n" + "\n" + "Environment variables:\n" + " Options from python/mozbuild/mozbuild/test/configure/data/moz.configure:\n" + " CC C Compiler\n" + "\n", + help.replace("\\", "/"), + ) + + def test_unknown(self): + with self.assertRaises(InvalidOptionError): + self.get_config(["--unknown"]) + + def test_simple(self): + for config in ( + self.get_config(), + self.get_config(["--disable-simple"]), + # Last option wins. + self.get_config(["--enable-simple", "--disable-simple"]), + ): + self.assertNotIn("ENABLED_SIMPLE", config) + self.assertIn("SIMPLE", config) + self.assertEqual(NegativeOptionValue(), config["SIMPLE"]) + + for config in ( + self.get_config(["--enable-simple"]), + self.get_config(["--disable-simple", "--enable-simple"]), + ): + self.assertIn("ENABLED_SIMPLE", config) + self.assertIn("SIMPLE", config) + self.assertEqual(PositiveOptionValue(), config["SIMPLE"]) + self.assertIs(config["SIMPLE"], config["ENABLED_SIMPLE"]) + + # --enable-simple doesn't take values. + with self.assertRaises(InvalidOptionError): + self.get_config(["--enable-simple=value"]) + + def test_with_env(self): + for config in ( + self.get_config(), + self.get_config(["--disable-with-env"]), + self.get_config(["--enable-with-env", "--disable-with-env"]), + self.get_config(env={"MOZ_WITH_ENV": ""}), + # Options win over environment + self.get_config(["--disable-with-env"], env={"MOZ_WITH_ENV": "1"}), + ): + self.assertIn("WITH_ENV", config) + self.assertEqual(NegativeOptionValue(), config["WITH_ENV"]) + + for config in ( + self.get_config(["--enable-with-env"]), + self.get_config(["--disable-with-env", "--enable-with-env"]), + self.get_config(env={"MOZ_WITH_ENV": "1"}), + self.get_config(["--enable-with-env"], env={"MOZ_WITH_ENV": ""}), + ): + self.assertIn("WITH_ENV", config) + self.assertEqual(PositiveOptionValue(), config["WITH_ENV"]) + + with self.assertRaises(InvalidOptionError): + self.get_config(["--enable-with-env=value"]) + + with self.assertRaises(InvalidOptionError): + self.get_config(env={"MOZ_WITH_ENV": "value"}) + + def test_values(self, name="VALUES"): + for config in ( + self.get_config(), + self.get_config(["--disable-values"]), + self.get_config(["--enable-values", "--disable-values"]), + ): + self.assertIn(name, config) + self.assertEqual(NegativeOptionValue(), config[name]) + + for config in ( + self.get_config(["--enable-values"]), + self.get_config(["--disable-values", "--enable-values"]), + ): + self.assertIn(name, config) + self.assertEqual(PositiveOptionValue(), config[name]) + + config = self.get_config(["--enable-values=foo"]) + self.assertIn(name, config) + self.assertEqual(PositiveOptionValue(("foo",)), config[name]) + + config = self.get_config(["--enable-values=foo,bar"]) + self.assertIn(name, config) + self.assertTrue(config[name]) + self.assertEqual(PositiveOptionValue(("foo", "bar")), config[name]) + + def test_values2(self): + self.test_values("VALUES2") + + def test_values3(self): + self.test_values("VALUES3") + + def test_returned_default(self): + config = self.get_config(["--enable-simple"]) + self.assertIn("DEFAULTED", config) + self.assertEqual(PositiveOptionValue(("simple",)), config["DEFAULTED"]) + + config = self.get_config(["--disable-simple"]) + self.assertIn("DEFAULTED", config) + self.assertEqual(PositiveOptionValue(("not-simple",)), config["DEFAULTED"]) + + def test_returned_choices(self): + for val in ("a", "b", "c"): + config = self.get_config( + ["--enable-values=alpha", "--returned-choices=%s" % val] + ) + self.assertIn("CHOICES", config) + self.assertEqual(PositiveOptionValue((val,)), config["CHOICES"]) + + for val in ("0", "1", "2"): + config = self.get_config( + ["--enable-values=numeric", "--returned-choices=%s" % val] + ) + self.assertIn("CHOICES", config) + self.assertEqual(PositiveOptionValue((val,)), config["CHOICES"]) + + with self.assertRaises(InvalidOptionError): + self.get_config(["--enable-values=numeric", "--returned-choices=a"]) + + with self.assertRaises(InvalidOptionError): + self.get_config(["--enable-values=alpha", "--returned-choices=0"]) + + def test_included(self): + config = self.get_config(env={"CC": "gcc"}) + self.assertIn("IS_GCC", config) + self.assertEqual(config["IS_GCC"], True) + + config = self.get_config(["--enable-include=extra.configure", "--extra"]) + self.assertIn("EXTRA", config) + self.assertEqual(PositiveOptionValue(), config["EXTRA"]) + + with self.assertRaises(InvalidOptionError): + self.get_config(["--extra"]) + + def test_template(self): + config = self.get_config(env={"CC": "gcc"}) + self.assertIn("CFLAGS", config) + self.assertEqual(config["CFLAGS"], ["-Werror=foobar"]) + + config = self.get_config(env={"CC": "clang"}) + self.assertNotIn("CFLAGS", config) + + def test_imports(self): + config = {} + out = StringIO() + sandbox = ConfigureSandbox(config, {}, ["configure"], out, out) + + with self.assertRaises(ImportError): + exec( + textwrap.dedent( + """ + @template + def foo(): + import sys + foo()""" + ), + sandbox, + ) + + exec( + textwrap.dedent( + """ + @template + @imports('sys') + def foo(): + return sys""" + ), + sandbox, + ) + + self.assertIs(sandbox["foo"](), sys) + + # os.path after an import is a mix of vanilla os.path and sandbox os.path. + os_path = {} + exec("from os.path import *", {}, os_path) + os_path.update(sandbox.OS.path.__dict__) + os_path = ReadOnlyNamespace(**os_path) + + exec( + textwrap.dedent( + """ + @template + @imports(_from='os', _import='path') + def foo(): + return path""" + ), + sandbox, + ) + + self.assertEqual(sandbox["foo"](), os_path) + + exec( + textwrap.dedent( + """ + @template + @imports(_from='os', _import='path', _as='os_path') + def foo(): + return os_path""" + ), + sandbox, + ) + + self.assertEqual(sandbox["foo"](), os_path) + + exec( + textwrap.dedent( + """ + @template + @imports('__builtin__') + def foo(): + return __builtin__""" + ), + sandbox, + ) + + with self.assertRaises(Exception) as e: + sandbox["foo"]() + self.assertEqual(str(e.exception), "Importing __builtin__ is forbidden") + + exec( + textwrap.dedent( + """ + @template + @imports(_from='__builtin__', _import='open') + def foo(): + return open('%s')""" + % os.devnull + ), + sandbox, + ) + + f = sandbox["foo"]() + self.assertEqual(f.name, os.devnull) + f.close() + + # This used to unlock the sandbox + exec( + textwrap.dedent( + """ + @template + @imports(_import='__builtin__', _as='__builtins__') + def foo(): + import sys + return sys""" + ), + sandbox, + ) + + with self.assertRaises(Exception) as e: + sandbox["foo"]() + self.assertEqual(str(e.exception), "Importing __builtin__ is forbidden") + + exec( + textwrap.dedent( + """ + @template + @imports('__sandbox__') + def foo(): + return __sandbox__""" + ), + sandbox, + ) + + self.assertIs(sandbox["foo"](), sandbox) + + exec( + textwrap.dedent( + """ + @template + @imports(_import='__sandbox__', _as='s') + def foo(): + return s""" + ), + sandbox, + ) + + self.assertIs(sandbox["foo"](), sandbox) + + # Nothing leaked from the function being executed + self.assertEqual(list(sandbox), ["__builtins__", "foo"]) + self.assertEqual(sandbox["__builtins__"], ConfigureSandbox.BUILTINS) + + exec( + textwrap.dedent( + """ + @template + @imports('sys') + def foo(): + @depends(when=True) + def bar(): + return sys + return bar + bar = foo()""" + ), + sandbox, + ) + + with self.assertRaises(NameError) as e: + sandbox._depends[sandbox["bar"]].result() + + self.assertIn("name 'sys' is not defined", str(e.exception)) + + def test_apply_imports(self): + imports = [] + + class CountApplyImportsSandbox(ConfigureSandbox): + def _apply_imports(self, *args, **kwargs): + imports.append((args, kwargs)) + super(CountApplyImportsSandbox, self)._apply_imports(*args, **kwargs) + + config = {} + out = StringIO() + sandbox = CountApplyImportsSandbox(config, {}, ["configure"], out, out) + + exec( + textwrap.dedent( + """ + @template + @imports('sys') + def foo(): + return sys + foo() + foo()""" + ), + sandbox, + ) + + self.assertEqual(len(imports), 1) + + def test_import_wrapping(self): + bar = object() + foo = ReadOnlyNamespace(bar=bar) + + class BasicWrappingSandbox(ConfigureSandbox): + @memoized_property + def _wrapped_foo(self): + return foo + + config = {} + out = StringIO() + sandbox = BasicWrappingSandbox(config, {}, ["configure"], out, out) + + exec( + textwrap.dedent( + """ + @template + @imports('foo') + def toplevel(): + return foo + @template + @imports('foo.bar') + def bar(): + return foo.bar + @template + @imports('foo.bar') + def bar_upper(): + return foo + @template + @imports(_from='foo', _import='bar') + def from_import(): + return bar + @template + @imports(_from='foo', _import='bar', _as='custom_name') + def from_import_as(): + return custom_name + @template + @imports(_import='foo', _as='custom_name') + def import_as(): + return custom_name + """ + ), + sandbox, + ) + self.assertIs(sandbox["toplevel"](), foo) + self.assertIs(sandbox["bar"](), bar) + self.assertIs(sandbox["bar_upper"](), foo) + self.assertIs(sandbox["from_import"](), bar) + self.assertIs(sandbox["from_import_as"](), bar) + self.assertIs(sandbox["import_as"](), foo) + + def test_os_path(self): + config = self.get_config(["--with-imports=%s" % __file__]) + self.assertIn("HAS_ABSPATH", config) + self.assertEqual(config["HAS_ABSPATH"], True) + self.assertIn("HAS_GETATIME", config) + self.assertEqual(config["HAS_GETATIME"], True) + self.assertIn("HAS_GETATIME2", config) + self.assertEqual(config["HAS_GETATIME2"], False) + + def test_template_call(self): + config = self.get_config(env={"CC": "gcc"}) + self.assertIn("TEMPLATE_VALUE", config) + self.assertEqual(config["TEMPLATE_VALUE"], 42) + self.assertIn("TEMPLATE_VALUE_2", config) + self.assertEqual(config["TEMPLATE_VALUE_2"], 21) + + def test_template_imports(self): + config = self.get_config(["--enable-imports-in-template"]) + self.assertIn("PLATFORM", config) + self.assertEqual(config["PLATFORM"], sys.platform) + + def test_decorators(self): + config = {} + out = StringIO() + sandbox = ConfigureSandbox(config, {}, ["configure"], out, out) + + sandbox.include_file(mozpath.join(test_data_path, "decorators.configure")) + + self.assertNotIn("FOO", sandbox) + self.assertNotIn("BAR", sandbox) + self.assertNotIn("QUX", sandbox) + + def test_set_config(self): + def get_config(*args): + return self.get_config(*args, configure="set_config.configure") + + help, config = get_config(["--help"]) + self.assertEqual(config, {}) + + config = get_config(["--set-foo"]) + self.assertIn("FOO", config) + self.assertEqual(config["FOO"], True) + + config = get_config(["--set-bar"]) + self.assertNotIn("FOO", config) + self.assertIn("BAR", config) + self.assertEqual(config["BAR"], True) + + config = get_config(["--set-value=qux"]) + self.assertIn("VALUE", config) + self.assertEqual(config["VALUE"], "qux") + + config = get_config(["--set-name=hoge"]) + self.assertIn("hoge", config) + self.assertEqual(config["hoge"], True) + + config = get_config([]) + self.assertEqual(config, {"BAR": False}) + + with self.assertRaises(ConfigureError): + # Both --set-foo and --set-name=FOO are going to try to + # set_config('FOO'...) + get_config(["--set-foo", "--set-name=FOO"]) + + def test_set_config_when(self): + with self.moz_configure( + """ + option('--with-qux', help='qux') + set_config('FOO', 'foo', when=True) + set_config('BAR', 'bar', when=False) + set_config('QUX', 'qux', when='--with-qux') + """ + ): + config = self.get_config() + self.assertEqual( + config, + { + "FOO": "foo", + }, + ) + config = self.get_config(["--with-qux"]) + self.assertEqual( + config, + { + "FOO": "foo", + "QUX": "qux", + }, + ) + + def test_set_config_when_disable(self): + with self.moz_configure( + """ + option('--disable-baz', help='Disable baz') + set_config('BAZ', True, when='--enable-baz') + """ + ): + config = self.get_config() + self.assertEqual(config["BAZ"], True) + config = self.get_config(["--enable-baz"]) + self.assertEqual(config["BAZ"], True) + config = self.get_config(["--disable-baz"]) + self.assertEqual(config, {}) + + def test_set_define(self): + def get_config(*args): + return self.get_config(*args, configure="set_define.configure") + + help, config = get_config(["--help"]) + self.assertEqual(config, {"DEFINES": {}}) + + config = get_config(["--set-foo"]) + self.assertIn("FOO", config["DEFINES"]) + self.assertEqual(config["DEFINES"]["FOO"], True) + + config = get_config(["--set-bar"]) + self.assertNotIn("FOO", config["DEFINES"]) + self.assertIn("BAR", config["DEFINES"]) + self.assertEqual(config["DEFINES"]["BAR"], True) + + config = get_config(["--set-value=qux"]) + self.assertIn("VALUE", config["DEFINES"]) + self.assertEqual(config["DEFINES"]["VALUE"], "qux") + + config = get_config(["--set-name=hoge"]) + self.assertIn("hoge", config["DEFINES"]) + self.assertEqual(config["DEFINES"]["hoge"], True) + + config = get_config([]) + self.assertEqual(config["DEFINES"], {"BAR": False}) + + with self.assertRaises(ConfigureError): + # Both --set-foo and --set-name=FOO are going to try to + # set_define('FOO'...) + get_config(["--set-foo", "--set-name=FOO"]) + + def test_set_define_when(self): + with self.moz_configure( + """ + option('--with-qux', help='qux') + set_define('FOO', 'foo', when=True) + set_define('BAR', 'bar', when=False) + set_define('QUX', 'qux', when='--with-qux') + """ + ): + config = self.get_config() + self.assertEqual( + config["DEFINES"], + { + "FOO": "foo", + }, + ) + config = self.get_config(["--with-qux"]) + self.assertEqual( + config["DEFINES"], + { + "FOO": "foo", + "QUX": "qux", + }, + ) + + def test_set_define_when_disable(self): + with self.moz_configure( + """ + option('--disable-baz', help='Disable baz') + set_define('BAZ', True, when='--enable-baz') + """ + ): + config = self.get_config() + self.assertEqual(config["DEFINES"]["BAZ"], True) + config = self.get_config(["--enable-baz"]) + self.assertEqual(config["DEFINES"]["BAZ"], True) + config = self.get_config(["--disable-baz"]) + self.assertEqual(config["DEFINES"], {}) + + def test_imply_option_simple(self): + def get_config(*args): + return self.get_config(*args, configure="imply_option/simple.configure") + + help, config = get_config(["--help"]) + self.assertEqual(config, {}) + + config = get_config([]) + self.assertEqual(config, {}) + + config = get_config(["--enable-foo"]) + self.assertIn("BAR", config) + self.assertEqual(config["BAR"], PositiveOptionValue()) + + with self.assertRaises(InvalidOptionError) as e: + get_config(["--enable-foo", "--disable-bar"]) + + self.assertEqual( + str(e.exception), + "'--enable-bar' implied by '--enable-foo' conflicts with " + "'--disable-bar' from the command-line", + ) + + def test_imply_option_negative(self): + def get_config(*args): + return self.get_config(*args, configure="imply_option/negative.configure") + + help, config = get_config(["--help"]) + self.assertEqual(config, {}) + + config = get_config([]) + self.assertEqual(config, {}) + + config = get_config(["--enable-foo"]) + self.assertIn("BAR", config) + self.assertEqual(config["BAR"], NegativeOptionValue()) + + with self.assertRaises(InvalidOptionError) as e: + get_config(["--enable-foo", "--enable-bar"]) + + self.assertEqual( + str(e.exception), + "'--disable-bar' implied by '--enable-foo' conflicts with " + "'--enable-bar' from the command-line", + ) + + config = get_config(["--disable-hoge"]) + self.assertIn("BAR", config) + self.assertEqual(config["BAR"], NegativeOptionValue()) + + with self.assertRaises(InvalidOptionError) as e: + get_config(["--disable-hoge", "--enable-bar"]) + + self.assertEqual( + str(e.exception), + "'--disable-bar' implied by '--disable-hoge' conflicts with " + "'--enable-bar' from the command-line", + ) + + def test_imply_option_values(self): + def get_config(*args): + return self.get_config(*args, configure="imply_option/values.configure") + + help, config = get_config(["--help"]) + self.assertEqual(config, {}) + + config = get_config([]) + self.assertEqual(config, {}) + + config = get_config(["--enable-foo=a"]) + self.assertIn("BAR", config) + self.assertEqual(config["BAR"], PositiveOptionValue(("a",))) + + config = get_config(["--enable-foo=a,b"]) + self.assertIn("BAR", config) + self.assertEqual(config["BAR"], PositiveOptionValue(("a", "b"))) + + with self.assertRaises(InvalidOptionError) as e: + get_config(["--enable-foo=a,b", "--disable-bar"]) + + self.assertEqual( + str(e.exception), + "'--enable-bar=a,b' implied by '--enable-foo' conflicts with " + "'--disable-bar' from the command-line", + ) + + def test_imply_option_infer(self): + def get_config(*args): + return self.get_config(*args, configure="imply_option/infer.configure") + + help, config = get_config(["--help"]) + self.assertEqual(config, {}) + + config = get_config([]) + self.assertEqual(config, {}) + + with self.assertRaises(InvalidOptionError) as e: + get_config(["--enable-foo", "--disable-bar"]) + + self.assertEqual( + str(e.exception), + "'--enable-bar' implied by '--enable-foo' conflicts with " + "'--disable-bar' from the command-line", + ) + + with self.assertRaises(ConfigureError) as e: + self.get_config([], configure="imply_option/infer_ko.configure") + + self.assertEqual( + str(e.exception), + "Cannot infer what implies '--enable-bar'. Please add a `reason` " + "to the `imply_option` call.", + ) + + def test_imply_option_immediate_value(self): + def get_config(*args): + return self.get_config(*args, configure="imply_option/imm.configure") + + help, config = get_config(["--help"]) + self.assertEqual(config, {}) + + config = get_config([]) + self.assertEqual(config, {}) + + config_path = mozpath.abspath( + mozpath.join(test_data_path, "imply_option", "imm.configure") + ) + + with self.assertRaisesRegexp( + InvalidOptionError, + "--enable-foo' implied by 'imply_option at %s:7' conflicts " + "with '--disable-foo' from the command-line" % config_path, + ): + get_config(["--disable-foo"]) + + with self.assertRaisesRegexp( + InvalidOptionError, + "--enable-bar=foo,bar' implied by 'imply_option at %s:18' " + "conflicts with '--enable-bar=a,b,c' from the command-line" % config_path, + ): + get_config(["--enable-bar=a,b,c"]) + + with self.assertRaisesRegexp( + InvalidOptionError, + "--enable-baz=BAZ' implied by 'imply_option at %s:29' " + "conflicts with '--enable-baz=QUUX' from the command-line" % config_path, + ): + get_config(["--enable-baz=QUUX"]) + + def test_imply_option_failures(self): + with self.assertRaises(ConfigureError) as e: + with self.moz_configure( + """ + imply_option('--with-foo', ('a',), 'bar') + """ + ): + self.get_config() + + self.assertEqual( + str(e.exception), + "`--with-foo`, emitted from `%s` line 2, is unknown." + % mozpath.join(test_data_path, "moz.configure"), + ) + + with self.assertRaises(TypeError) as e: + with self.moz_configure( + """ + imply_option('--with-foo', 42, 'bar') + + option('--with-foo', help='foo') + @depends('--with-foo') + def foo(value): + return value + """ + ): + self.get_config() + + self.assertEqual(str(e.exception), "Unexpected type: 'int'") + + def test_imply_option_when(self): + with self.moz_configure( + """ + option('--with-foo', help='foo') + imply_option('--with-qux', True, when='--with-foo') + option('--with-qux', help='qux') + set_config('QUX', depends('--with-qux')(lambda x: x)) + """ + ): + config = self.get_config() + self.assertEqual( + config, + { + "QUX": NegativeOptionValue(), + }, + ) + + config = self.get_config(["--with-foo"]) + self.assertEqual( + config, + { + "QUX": PositiveOptionValue(), + }, + ) + + def test_imply_option_dependency_loop(self): + with self.moz_configure( + """ + option('--without-foo', help='foo') + + @depends('--with-foo') + def qux_default(foo): + return bool(foo) + + option('--with-qux', default=qux_default, help='qux') + + imply_option('--with-foo', depends('--with-qux')(lambda x: x or None)) + + set_config('FOO', depends('--with-foo')(lambda x: x)) + set_config('QUX', depends('--with-qux')(lambda x: x)) + """ + ): + config = self.get_config() + self.assertEqual( + config, + { + "FOO": PositiveOptionValue(), + "QUX": PositiveOptionValue(), + }, + ) + + config = self.get_config(["--without-foo"]) + self.assertEqual( + config, + { + "FOO": NegativeOptionValue(), + "QUX": NegativeOptionValue(), + }, + ) + + config = self.get_config(["--with-qux"]) + self.assertEqual( + config, + { + "FOO": PositiveOptionValue(), + "QUX": PositiveOptionValue(), + }, + ) + + with self.assertRaises(InvalidOptionError) as e: + config = self.get_config(["--without-foo", "--with-qux"]) + + self.assertEqual( + str(e.exception), + "'--with-foo' implied by '--with-qux' conflicts " + "with '--without-foo' from the command-line", + ) + + config = self.get_config(["--without-qux"]) + self.assertEqual( + config, + { + "FOO": PositiveOptionValue(), + "QUX": NegativeOptionValue(), + }, + ) + + with self.moz_configure( + """ + option('--with-foo', help='foo') + + @depends('--with-foo') + def qux_default(foo): + return bool(foo) + + option('--with-qux', default=qux_default, help='qux') + + imply_option('--with-foo', depends('--with-qux')(lambda x: x or None)) + + set_config('FOO', depends('--with-foo')(lambda x: x)) + set_config('QUX', depends('--with-qux')(lambda x: x)) + """ + ): + config = self.get_config() + self.assertEqual( + config, + { + "FOO": NegativeOptionValue(), + "QUX": NegativeOptionValue(), + }, + ) + + config = self.get_config(["--with-foo"]) + self.assertEqual( + config, + { + "FOO": PositiveOptionValue(), + "QUX": PositiveOptionValue(), + }, + ) + + with self.assertRaises(InvalidOptionError) as e: + config = self.get_config(["--with-qux"]) + + self.assertEqual( + str(e.exception), + "'--with-foo' implied by '--with-qux' conflicts " + "with '--without-foo' from the default", + ) + + with self.assertRaises(InvalidOptionError) as e: + config = self.get_config(["--without-foo", "--with-qux"]) + + self.assertEqual( + str(e.exception), + "'--with-foo' implied by '--with-qux' conflicts " + "with '--without-foo' from the command-line", + ) + + config = self.get_config(["--without-qux"]) + self.assertEqual( + config, + { + "FOO": NegativeOptionValue(), + "QUX": NegativeOptionValue(), + }, + ) + + config_path = mozpath.abspath(mozpath.join(test_data_path, "moz.configure")) + + # Same test as above, but using `when` in the `imply_option`. + with self.moz_configure( + """ + option('--with-foo', help='foo') + + @depends('--with-foo') + def qux_default(foo): + return bool(foo) + + option('--with-qux', default=qux_default, help='qux') + + imply_option('--with-foo', True, when='--with-qux') + + set_config('FOO', depends('--with-foo')(lambda x: x)) + set_config('QUX', depends('--with-qux')(lambda x: x)) + """ + ): + config = self.get_config() + self.assertEqual( + config, + { + "FOO": NegativeOptionValue(), + "QUX": NegativeOptionValue(), + }, + ) + + config = self.get_config(["--with-foo"]) + self.assertEqual( + config, + { + "FOO": PositiveOptionValue(), + "QUX": PositiveOptionValue(), + }, + ) + + with self.assertRaises(InvalidOptionError) as e: + config = self.get_config(["--with-qux"]) + + self.assertEqual( + str(e.exception), + "'--with-foo' implied by 'imply_option at %s:10' conflicts " + "with '--without-foo' from the default" % config_path, + ) + + with self.assertRaises(InvalidOptionError) as e: + config = self.get_config(["--without-foo", "--with-qux"]) + + self.assertEqual( + str(e.exception), + "'--with-foo' implied by 'imply_option at %s:10' conflicts " + "with '--without-foo' from the command-line" % config_path, + ) + + config = self.get_config(["--without-qux"]) + self.assertEqual( + config, + { + "FOO": NegativeOptionValue(), + "QUX": NegativeOptionValue(), + }, + ) + + def test_imply_option_recursion(self): + config_path = mozpath.abspath(mozpath.join(test_data_path, "moz.configure")) + + message = ( + "'--without-foo' appears somewhere in the direct or indirect dependencies " + "when resolving imply_option at %s:8" % config_path + ) + + with self.moz_configure( + """ + option('--without-foo', help='foo') + + imply_option('--with-qux', depends('--with-foo')(lambda x: x or None)) + + option('--with-qux', help='qux') + + imply_option('--with-foo', depends('--with-qux')(lambda x: x or None)) + + set_config('FOO', depends('--with-foo')(lambda x: x)) + set_config('QUX', depends('--with-qux')(lambda x: x)) + """ + ): + # Note: no error is detected when the depends function in the + # imply_options resolve to None, which disables the imply_option. + + with self.assertRaises(ConfigureError) as e: + self.get_config() + + self.assertEqual(str(e.exception), message) + + with self.assertRaises(ConfigureError) as e: + self.get_config(["--with-qux"]) + + self.assertEqual(str(e.exception), message) + + with self.assertRaises(ConfigureError) as e: + self.get_config(["--without-foo", "--with-qux"]) + + self.assertEqual(str(e.exception), message) + + def test_option_failures(self): + with self.assertRaises(ConfigureError) as e: + with self.moz_configure('option("--with-foo", help="foo")'): + self.get_config() + + self.assertEqual( + str(e.exception), + "Option `--with-foo` is not handled ; reference it with a @depends", + ) + + with self.assertRaises(ConfigureError) as e: + with self.moz_configure( + """ + option("--with-foo", help="foo") + option("--with-foo", help="foo") + """ + ): + self.get_config() + + self.assertEqual(str(e.exception), "Option `--with-foo` already defined") + + with self.assertRaises(ConfigureError) as e: + with self.moz_configure( + """ + option(env="MOZ_FOO", help="foo") + option(env="MOZ_FOO", help="foo") + """ + ): + self.get_config() + + self.assertEqual(str(e.exception), "Option `MOZ_FOO` already defined") + + with self.assertRaises(ConfigureError) as e: + with self.moz_configure( + """ + option('--with-foo', env="MOZ_FOO", help="foo") + option(env="MOZ_FOO", help="foo") + """ + ): + self.get_config() + + self.assertEqual(str(e.exception), "Option `MOZ_FOO` already defined") + + with self.assertRaises(ConfigureError) as e: + with self.moz_configure( + """ + option(env="MOZ_FOO", help="foo") + option('--with-foo', env="MOZ_FOO", help="foo") + """ + ): + self.get_config() + + self.assertEqual(str(e.exception), "Option `MOZ_FOO` already defined") + + with self.assertRaises(ConfigureError) as e: + with self.moz_configure( + """ + option('--with-foo', env="MOZ_FOO", help="foo") + option('--with-foo', help="foo") + """ + ): + self.get_config() + + self.assertEqual(str(e.exception), "Option `--with-foo` already defined") + + def test_option_when(self): + with self.moz_configure( + """ + option('--with-foo', help='foo', when=True) + option('--with-bar', help='bar', when=False) + option('--with-qux', env="QUX", help='qux', when='--with-foo') + + set_config('FOO', depends('--with-foo', when=True)(lambda x: x)) + set_config('BAR', depends('--with-bar', when=False)(lambda x: x)) + set_config('QUX', depends('--with-qux', when='--with-foo')(lambda x: x)) + """ + ): + config = self.get_config() + self.assertEqual( + config, + { + "FOO": NegativeOptionValue(), + }, + ) + + config = self.get_config(["--with-foo"]) + self.assertEqual( + config, + { + "FOO": PositiveOptionValue(), + "QUX": NegativeOptionValue(), + }, + ) + + config = self.get_config(["--with-foo", "--with-qux"]) + self.assertEqual( + config, + { + "FOO": PositiveOptionValue(), + "QUX": PositiveOptionValue(), + }, + ) + + with self.assertRaises(InvalidOptionError) as e: + self.get_config(["--with-bar"]) + + self.assertEqual( + str(e.exception), "--with-bar is not available in this configuration" + ) + + with self.assertRaises(InvalidOptionError) as e: + self.get_config(["--with-qux"]) + + self.assertEqual( + str(e.exception), "--with-qux is not available in this configuration" + ) + + with self.assertRaises(InvalidOptionError) as e: + self.get_config(["QUX=1"]) + + self.assertEqual( + str(e.exception), "QUX is not available in this configuration" + ) + + config = self.get_config(env={"QUX": "1"}) + self.assertEqual( + config, + { + "FOO": NegativeOptionValue(), + }, + ) + + help, config = self.get_config(["--help"]) + self.assertEqual( + help.replace("\\", "/"), + textwrap.dedent( + """\ + Usage: configure [options] + + Options: [defaults in brackets after descriptions] + Help options: + --help print this message + + Options from python/mozbuild/mozbuild/test/configure/data/moz.configure: + --with-foo foo + + """ + ), + ) + + help, config = self.get_config(["--help", "--with-foo"]) + self.assertEqual( + help.replace("\\", "/"), + textwrap.dedent( + """\ + Usage: configure [options] + + Options: [defaults in brackets after descriptions] + Help options: + --help print this message + + Options from python/mozbuild/mozbuild/test/configure/data/moz.configure: + --with-foo foo + --with-qux qux + + """ + ), + ) + + with self.moz_configure( + """ + option('--with-foo', help='foo', when=True) + set_config('FOO', depends('--with-foo')(lambda x: x)) + """ + ): + with self.assertRaises(ConfigureError) as e: + self.get_config() + + self.assertEqual( + str(e.exception), + "@depends function needs the same `when` as " "options it depends on", + ) + + with self.moz_configure( + """ + @depends(when=True) + def always(): + return True + @depends(when=True) + def always2(): + return True + option('--with-foo', help='foo', when=always) + set_config('FOO', depends('--with-foo', when=always2)(lambda x: x)) + """ + ): + with self.assertRaises(ConfigureError) as e: + self.get_config() + + self.assertEqual( + str(e.exception), + "@depends function needs the same `when` as " "options it depends on", + ) + + with self.moz_configure( + """ + @depends(when=True) + def always(): + return True + @depends(when=True) + def always2(): + return True + with only_when(always2): + option('--with-foo', help='foo', when=always) + # include() triggers resolution of its dependencies, and their + # side effects. + include(depends('--with-foo', when=always)(lambda x: x)) + # The sandbox should figure that the `when` here is + # appropriate. Bad behavior in CombinedDependsFunction.__eq__ + # made this fail in the past. + set_config('FOO', depends('--with-foo', when=always)(lambda x: x)) + """ + ): + self.get_config() + + with self.moz_configure( + """ + option('--with-foo', help='foo') + option('--without-bar', help='bar', when='--with-foo') + option('--with-qux', help='qux', when='--with-bar') + set_config('QUX', True, when='--with-qux') + """ + ): + # These are valid: + self.get_config(["--with-foo"]) + self.get_config(["--with-foo", "--with-bar"]) + self.get_config(["--with-foo", "--without-bar"]) + self.get_config(["--with-foo", "--with-bar", "--with-qux"]) + self.get_config(["--with-foo", "--with-bar", "--without-qux"]) + with self.assertRaises(InvalidOptionError) as e: + self.get_config(["--with-bar"]) + with self.assertRaises(InvalidOptionError) as e: + self.get_config(["--without-bar"]) + with self.assertRaises(InvalidOptionError) as e: + self.get_config(["--with-qux"]) + with self.assertRaises(InvalidOptionError) as e: + self.get_config(["--without-qux"]) + with self.assertRaises(InvalidOptionError) as e: + self.get_config(["--with-foo", "--without-bar", "--with-qux"]) + with self.assertRaises(InvalidOptionError) as e: + self.get_config(["--with-foo", "--without-bar", "--without-qux"]) + + def test_include_failures(self): + with self.assertRaises(ConfigureError) as e: + with self.moz_configure('include("../foo.configure")'): + self.get_config() + + self.assertEqual( + str(e.exception), + "Cannot include `%s` because it is not in a subdirectory of `%s`" + % ( + mozpath.normpath(mozpath.join(test_data_path, "..", "foo.configure")), + mozpath.normsep(test_data_path), + ), + ) + + with self.assertRaises(ConfigureError) as e: + with self.moz_configure( + """ + include('extra.configure') + include('extra.configure') + """ + ): + self.get_config() + + self.assertEqual( + str(e.exception), + "Cannot include `%s` because it was included already." + % mozpath.normpath(mozpath.join(test_data_path, "extra.configure")), + ) + + with self.assertRaises(TypeError) as e: + with self.moz_configure( + """ + include(42) + """ + ): + self.get_config() + + self.assertEqual(str(e.exception), "Unexpected type: 'int'") + + def test_include_when(self): + with MockedOpen( + { + os.path.join(test_data_path, "moz.configure"): textwrap.dedent( + """ + option('--with-foo', help='foo') + + include('always.configure', when=True) + include('never.configure', when=False) + include('foo.configure', when='--with-foo') + + set_config('FOO', foo) + set_config('BAR', bar) + set_config('QUX', qux) + """ + ), + os.path.join(test_data_path, "always.configure"): textwrap.dedent( + """ + option('--with-bar', help='bar') + @depends('--with-bar') + def bar(x): + if x: + return 'bar' + """ + ), + os.path.join(test_data_path, "never.configure"): textwrap.dedent( + """ + option('--with-qux', help='qux') + @depends('--with-qux') + def qux(x): + if x: + return 'qux' + """ + ), + os.path.join(test_data_path, "foo.configure"): textwrap.dedent( + """ + option('--with-foo-really', help='really foo') + @depends('--with-foo-really') + def foo(x): + if x: + return 'foo' + + include('foo2.configure', when='--with-foo-really') + """ + ), + os.path.join(test_data_path, "foo2.configure"): textwrap.dedent( + """ + set_config('FOO2', True) + """ + ), + } + ): + config = self.get_config() + self.assertEqual(config, {}) + + config = self.get_config(["--with-foo"]) + self.assertEqual(config, {}) + + config = self.get_config(["--with-bar"]) + self.assertEqual( + config, + { + "BAR": "bar", + }, + ) + + with self.assertRaises(InvalidOptionError) as e: + self.get_config(["--with-qux"]) + + self.assertEqual( + str(e.exception), "--with-qux is not available in this configuration" + ) + + config = self.get_config(["--with-foo", "--with-foo-really"]) + self.assertEqual( + config, + { + "FOO": "foo", + "FOO2": True, + }, + ) + + def test_sandbox_failures(self): + with self.assertRaises(KeyError) as e: + with self.moz_configure( + """ + include = 42 + """ + ): + self.get_config() + + self.assertIn("Cannot reassign builtins", str(e.exception)) + + with self.assertRaises(KeyError) as e: + with self.moz_configure( + """ + foo = 42 + """ + ): + self.get_config() + + self.assertIn( + "Cannot assign `foo` because it is neither a @depends nor a " "@template", + str(e.exception), + ) + + def test_depends_failures(self): + with self.assertRaises(ConfigureError) as e: + with self.moz_configure( + """ + @depends() + def foo(): + return + """ + ): + self.get_config() + + self.assertEqual(str(e.exception), "@depends needs at least one argument") + + with self.assertRaises(ConfigureError) as e: + with self.moz_configure( + """ + @depends('--with-foo') + def foo(value): + return value + """ + ): + self.get_config() + + self.assertEqual( + str(e.exception), + "'--with-foo' is not a known option. Maybe it's " "declared too late?", + ) + + with self.assertRaises(ConfigureError) as e: + with self.moz_configure( + """ + @depends('--with-foo=42') + def foo(value): + return value + """ + ): + self.get_config() + + self.assertEqual(str(e.exception), "Option must not contain an '='") + + with self.assertRaises(TypeError) as e: + with self.moz_configure( + """ + @depends(42) + def foo(value): + return value + """ + ): + self.get_config() + + self.assertEqual( + str(e.exception), + "Cannot use object of type 'int' as argument " "to @depends", + ) + + with self.assertRaises(ConfigureError) as e: + with self.moz_configure( + """ + @depends('--help') + def foo(value): + yield + """ + ): + self.get_config() + + self.assertEqual( + str(e.exception), "Cannot decorate generator functions with @depends" + ) + + with self.assertRaises(TypeError) as e: + with self.moz_configure( + """ + @depends('--help') + def foo(value): + return value + + depends('--help')(foo) + """ + ): + self.get_config() + + self.assertEqual(str(e.exception), "Cannot nest @depends functions") + + with self.assertRaises(TypeError) as e: + with self.moz_configure( + """ + @template + def foo(f): + pass + + depends('--help')(foo) + """ + ): + self.get_config() + + self.assertEqual(str(e.exception), "Cannot use a @template function here") + + with self.assertRaises(ConfigureError) as e: + with self.moz_configure( + """ + option('--foo', help='foo') + @depends('--foo') + def foo(value): + return value + + foo() + """ + ): + self.get_config() + + self.assertEqual(str(e.exception), "The `foo` function may not be called") + + with self.assertRaises(TypeError) as e: + with self.moz_configure( + """ + @depends('--help', foo=42) + def foo(_): + return + """ + ): + self.get_config() + + self.assertEqual( + str(e.exception), "depends_impl() got an unexpected keyword argument 'foo'" + ) + + def test_depends_when(self): + with self.moz_configure( + """ + @depends(when=True) + def foo(): + return 'foo' + + set_config('FOO', foo) + + @depends(when=False) + def bar(): + return 'bar' + + set_config('BAR', bar) + + option('--with-qux', help='qux') + @depends(when='--with-qux') + def qux(): + return 'qux' + + set_config('QUX', qux) + """ + ): + config = self.get_config() + self.assertEqual( + config, + { + "FOO": "foo", + }, + ) + + config = self.get_config(["--with-qux"]) + self.assertEqual( + config, + { + "FOO": "foo", + "QUX": "qux", + }, + ) + + def test_depends_value(self): + with self.moz_configure( + """ + foo = depends(when=True)('foo') + + set_config('FOO', foo) + + bar = depends(when=False)('bar') + + set_config('BAR', bar) + + option('--with-qux', help='qux') + @depends(when='--with-qux') + def qux(): + return 'qux' + + set_config('QUX', qux) + """ + ): + config = self.get_config() + self.assertEqual( + config, + { + "FOO": "foo", + }, + ) + + with self.assertRaises(TypeError) as e: + with self.moz_configure( + """ + option('--foo', help='foo') + + depends('--foo')('foo') + """ + ): + self.get_config() + + self.assertEqual( + str(e.exception), "Cannot wrap literal values in @depends with dependencies" + ) + + def test_imports_failures(self): + with self.assertRaises(ConfigureError) as e: + with self.moz_configure( + """ + @imports('os') + @template + def foo(value): + return value + """ + ): + self.get_config() + + self.assertEqual(str(e.exception), "@imports must appear after @template") + + with self.assertRaises(ConfigureError) as e: + with self.moz_configure( + """ + option('--foo', help='foo') + @imports('os') + @depends('--foo') + def foo(value): + return value + """ + ): + self.get_config() + + self.assertEqual(str(e.exception), "@imports must appear after @depends") + + for import_ in ( + "42", + "_from=42, _import='os'", + "_from='os', _import='path', _as=42", + ): + with self.assertRaises(TypeError) as e: + with self.moz_configure( + """ + @imports(%s) + @template + def foo(value): + return value + """ + % import_ + ): + self.get_config() + + self.assertEqual(str(e.exception), "Unexpected type: 'int'") + + with self.assertRaises(TypeError) as e: + with self.moz_configure( + """ + @imports('os', 42) + @template + def foo(value): + return value + """ + ): + self.get_config() + + self.assertEqual(str(e.exception), "Unexpected type: 'int'") + + with self.assertRaises(ValueError) as e: + with self.moz_configure( + """ + @imports('os*') + def foo(value): + return value + """ + ): + self.get_config() + + self.assertEqual(str(e.exception), "Invalid argument to @imports: 'os*'") + + def test_only_when(self): + moz_configure = """ + option('--enable-when', help='when') + @depends('--enable-when', '--help') + def when(value, _): + return bool(value) + + with only_when(when): + option('--foo', nargs='*', help='foo') + @depends('--foo') + def foo(value): + return value + + set_config('FOO', foo) + set_define('FOO', foo) + + # It is possible to depend on a function defined in a only_when + # block. It then resolves to `None`. + set_config('BAR', depends(foo)(lambda x: x)) + set_define('BAR', depends(foo)(lambda x: x)) + """ + + with self.moz_configure(moz_configure): + config = self.get_config() + self.assertEqual( + config, + { + "DEFINES": {}, + }, + ) + + config = self.get_config(["--enable-when"]) + self.assertEqual( + config, + { + "BAR": NegativeOptionValue(), + "FOO": NegativeOptionValue(), + "DEFINES": { + "BAR": NegativeOptionValue(), + "FOO": NegativeOptionValue(), + }, + }, + ) + + config = self.get_config(["--enable-when", "--foo=bar"]) + self.assertEqual( + config, + { + "BAR": PositiveOptionValue(["bar"]), + "FOO": PositiveOptionValue(["bar"]), + "DEFINES": { + "BAR": PositiveOptionValue(["bar"]), + "FOO": PositiveOptionValue(["bar"]), + }, + }, + ) + + # The --foo option doesn't exist when --enable-when is not given. + with self.assertRaises(InvalidOptionError) as e: + self.get_config(["--foo"]) + + self.assertEqual( + str(e.exception), "--foo is not available in this configuration" + ) + + # Cannot depend on an option defined in a only_when block, because we + # don't know what OptionValue would make sense. + with self.moz_configure( + moz_configure + + """ + set_config('QUX', depends('--foo')(lambda x: x)) + """ + ): + with self.assertRaises(ConfigureError) as e: + self.get_config() + + self.assertEqual( + str(e.exception), + "@depends function needs the same `when` as " "options it depends on", + ) + + with self.moz_configure( + moz_configure + + """ + set_config('QUX', depends('--foo', when=when)(lambda x: x)) + """ + ): + self.get_config(["--enable-when"]) + + # Using imply_option for an option defined in a only_when block fails + # similarly if the imply_option happens outside the block. + with self.moz_configure( + """ + imply_option('--foo', True) + """ + + moz_configure + ): + with self.assertRaises(InvalidOptionError) as e: + self.get_config() + + self.assertEqual( + str(e.exception), "--foo is not available in this configuration" + ) + + # And similarly doesn't fail when the condition is true. + with self.moz_configure( + """ + imply_option('--foo', True) + """ + + moz_configure + ): + self.get_config(["--enable-when"]) + + def test_depends_binary_ops(self): + with self.moz_configure( + """ + option('--foo', nargs=1, help='foo') + @depends('--foo') + def foo(value): + return value or 0 + + option('--bar', nargs=1, help='bar') + @depends('--bar') + def bar(value): + return value or '' + + option('--baz', nargs=1, help='baz') + @depends('--baz') + def baz(value): + return value + + set_config('FOOorBAR', foo | bar) + set_config('FOOorBARorBAZ', foo | bar | baz) + set_config('FOOandBAR', foo & bar) + set_config('FOOandBARandBAZ', foo & bar & baz) + """ + ): + for foo_opt, foo_value in ( + ("", 0), + ("--foo=foo", PositiveOptionValue(("foo",))), + ): + for bar_opt, bar_value in ( + ("", ""), + ("--bar=bar", PositiveOptionValue(("bar",))), + ): + for baz_opt, baz_value in ( + ("", NegativeOptionValue()), + ("--baz=baz", PositiveOptionValue(("baz",))), + ): + config = self.get_config( + [x for x in (foo_opt, bar_opt, baz_opt) if x] + ) + self.assertEqual( + config, + { + "FOOorBAR": foo_value or bar_value, + "FOOorBARorBAZ": foo_value or bar_value or baz_value, + "FOOandBAR": foo_value and bar_value, + "FOOandBARandBAZ": foo_value + and bar_value + and baz_value, + }, + ) + + def test_depends_getattr(self): + with self.moz_configure( + """ + option('--foo', nargs=1, help='foo') + @depends('--foo') + def foo(value): + return value + + option('--bar', nargs=1, help='bar') + @depends('--bar') + def bar(value): + return value or None + + @depends(foo, bar) + def foobar(foo, bar): + return namespace(foo=foo, bar=bar) + + set_config('FOO', foobar.foo) + set_config('BAR', foobar.bar) + set_config('BAZ', foobar.baz) + """ + ): + config = self.get_config() + self.assertEqual( + config, + { + "FOO": NegativeOptionValue(), + }, + ) + + config = self.get_config(["--foo=foo"]) + self.assertEqual( + config, + { + "FOO": PositiveOptionValue(("foo",)), + }, + ) + + config = self.get_config(["--bar=bar"]) + self.assertEqual( + config, + { + "FOO": NegativeOptionValue(), + "BAR": PositiveOptionValue(("bar",)), + }, + ) + + config = self.get_config(["--foo=foo", "--bar=bar"]) + self.assertEqual( + config, + { + "FOO": PositiveOptionValue(("foo",)), + "BAR": PositiveOptionValue(("bar",)), + }, + ) + + +if __name__ == "__main__": + main() diff --git a/python/mozbuild/mozbuild/test/configure/test_lint.py b/python/mozbuild/mozbuild/test/configure/test_lint.py new file mode 100644 index 0000000000..5c54af84f5 --- /dev/null +++ b/python/mozbuild/mozbuild/test/configure/test_lint.py @@ -0,0 +1,524 @@ +# 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/. + +import os +import textwrap +import traceback +import unittest + +import mozpack.path as mozpath +from mozunit import MockedOpen, main + +from mozbuild.configure import ConfigureError +from mozbuild.configure.lint import LintSandbox + +test_data_path = mozpath.abspath(mozpath.dirname(__file__)) +test_data_path = mozpath.join(test_data_path, "data") + + +class AssertRaisesFromLine: + def __init__(self, test_case, expected, path, line): + self.test_case = test_case + self.expected = expected + self.path = path + self.line = line + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_value, tb): + if exc_type is None: + raise Exception(f"{self.expected.__name__} not raised") + if not issubclass(exc_type, self.expected): + return False + self.exception = exc_value + self.test_case.assertEqual( + traceback.extract_tb(tb)[-1][:2], (self.path, self.line) + ) + return True + + +class TestLint(unittest.TestCase): + def lint_test(self, options=[], env={}): + sandbox = LintSandbox(env, ["configure"] + options) + + sandbox.run(mozpath.join(test_data_path, "moz.configure")) + + def moz_configure(self, source): + return MockedOpen( + {os.path.join(test_data_path, "moz.configure"): textwrap.dedent(source)} + ) + + def assertRaisesFromLine(self, exc_type, line): + return AssertRaisesFromLine( + self, exc_type, mozpath.join(test_data_path, "moz.configure"), line + ) + + def test_configure_testcase(self): + # Lint python/mozbuild/mozbuild/test/configure/data/moz.configure + self.lint_test() + + def test_depends_failures(self): + with self.moz_configure( + """ + option('--foo', help='foo') + @depends('--foo') + def foo(value): + return value + + @depends('--help', foo) + @imports('os') + def bar(help, foo): + return foo + """ + ): + self.lint_test() + + with self.assertRaisesFromLine(ConfigureError, 7) as e: + with self.moz_configure( + """ + option('--foo', help='foo') + @depends('--foo') + def foo(value): + return value + + @depends('--help', foo) + def bar(help, foo): + return foo + """ + ): + self.lint_test() + + self.assertEqual(str(e.exception), "The dependency on `--help` is unused") + + with self.assertRaisesFromLine(ConfigureError, 3) as e: + with self.moz_configure( + """ + option('--foo', help='foo') + @depends('--foo') + @imports('os') + def foo(value): + return value + + @depends('--help', foo) + @imports('os') + def bar(help, foo): + return foo + """ + ): + self.lint_test() + + self.assertEqual( + str(e.exception), + "Missing '--help' dependency because `bar` depends on '--help' and `foo`", + ) + + with self.assertRaisesFromLine(ConfigureError, 7) as e: + with self.moz_configure( + """ + @template + def tmpl(): + qux = 42 + + option('--foo', help='foo') + @depends('--foo') + def foo(value): + qux + return value + + @depends('--help', foo) + @imports('os') + def bar(help, foo): + return foo + tmpl() + """ + ): + self.lint_test() + + self.assertEqual( + str(e.exception), + "Missing '--help' dependency because `bar` depends on '--help' and `foo`", + ) + + with self.moz_configure( + """ + option('--foo', help='foo') + @depends('--foo') + def foo(value): + return value + + include(foo) + """ + ): + self.lint_test() + + with self.assertRaisesFromLine(ConfigureError, 3) as e: + with self.moz_configure( + """ + option('--foo', help='foo') + @depends('--foo') + @imports('os') + def foo(value): + return value + + include(foo) + """ + ): + self.lint_test() + + self.assertEqual(str(e.exception), "Missing '--help' dependency") + + with self.assertRaisesFromLine(ConfigureError, 3) as e: + with self.moz_configure( + """ + option('--foo', help='foo') + @depends('--foo') + @imports('os') + def foo(value): + return value + + @depends(foo) + def bar(value): + return value + + include(bar) + """ + ): + self.lint_test() + + self.assertEqual(str(e.exception), "Missing '--help' dependency") + + with self.assertRaisesFromLine(ConfigureError, 3) as e: + with self.moz_configure( + """ + option('--foo', help='foo') + @depends('--foo') + @imports('os') + def foo(value): + return value + + option('--bar', help='bar', when=foo) + """ + ): + self.lint_test() + + self.assertEqual(str(e.exception), "Missing '--help' dependency") + + # This would have failed with "Missing '--help' dependency" + # in the past, because of the reference to the builtin False. + with self.moz_configure( + """ + option('--foo', help='foo') + @depends('--foo') + def foo(value): + return False or value + + option('--bar', help='bar', when=foo) + """ + ): + self.lint_test() + + # However, when something that is normally a builtin is overridden, + # we should still want the dependency on --help. + with self.assertRaisesFromLine(ConfigureError, 7) as e: + with self.moz_configure( + """ + @template + def tmpl(): + sorted = 42 + + option('--foo', help='foo') + @depends('--foo') + def foo(value): + return sorted + + option('--bar', help='bar', when=foo) + tmpl() + """ + ): + self.lint_test() + + self.assertEqual(str(e.exception), "Missing '--help' dependency") + + # There is a default restricted `os` module when there is no explicit + # @imports, and it's fine to use it without a dependency on --help. + with self.moz_configure( + """ + option('--foo', help='foo') + @depends('--foo') + def foo(value): + os + return value + + include(foo) + """ + ): + self.lint_test() + + with self.assertRaisesFromLine(ConfigureError, 3) as e: + with self.moz_configure( + """ + option('--foo', help='foo') + @depends('--foo') + def foo(value): + return + + include(foo) + """ + ): + self.lint_test() + + self.assertEqual(str(e.exception), "The dependency on `--foo` is unused") + + with self.assertRaisesFromLine(ConfigureError, 5) as e: + with self.moz_configure( + """ + @depends(when=True) + def bar(): + return + @depends(bar) + def foo(value): + return + + include(foo) + """ + ): + self.lint_test() + + self.assertEqual(str(e.exception), "The dependency on `bar` is unused") + + with self.assertRaisesFromLine(ConfigureError, 2) as e: + with self.moz_configure( + """ + @depends(depends(when=True)(lambda: None)) + def foo(value): + return + + include(foo) + """ + ): + self.lint_test() + + self.assertEqual(str(e.exception), "The dependency on `<lambda>` is unused") + + with self.assertRaisesFromLine(ConfigureError, 9) as e: + with self.moz_configure( + """ + @template + def tmpl(): + @depends(when=True) + def bar(): + return + return bar + qux = tmpl() + @depends(qux) + def foo(value): + return + + include(foo) + """ + ): + self.lint_test() + + self.assertEqual(str(e.exception), "The dependency on `qux` is unused") + + def test_default_enable(self): + # --enable-* with default=True is not allowed. + with self.moz_configure( + """ + option('--enable-foo', default=False, help='foo') + """ + ): + self.lint_test() + with self.assertRaisesFromLine(ConfigureError, 2) as e: + with self.moz_configure( + """ + option('--enable-foo', default=True, help='foo') + """ + ): + self.lint_test() + self.assertEqual( + str(e.exception), + "--disable-foo should be used instead of " "--enable-foo with default=True", + ) + + def test_default_disable(self): + # --disable-* with default=False is not allowed. + with self.moz_configure( + """ + option('--disable-foo', default=True, help='foo') + """ + ): + self.lint_test() + with self.assertRaisesFromLine(ConfigureError, 2) as e: + with self.moz_configure( + """ + option('--disable-foo', default=False, help='foo') + """ + ): + self.lint_test() + self.assertEqual( + str(e.exception), + "--enable-foo should be used instead of " + "--disable-foo with default=False", + ) + + def test_default_with(self): + # --with-* with default=True is not allowed. + with self.moz_configure( + """ + option('--with-foo', default=False, help='foo') + """ + ): + self.lint_test() + with self.assertRaisesFromLine(ConfigureError, 2) as e: + with self.moz_configure( + """ + option('--with-foo', default=True, help='foo') + """ + ): + self.lint_test() + self.assertEqual( + str(e.exception), + "--without-foo should be used instead of " "--with-foo with default=True", + ) + + def test_default_without(self): + # --without-* with default=False is not allowed. + with self.moz_configure( + """ + option('--without-foo', default=True, help='foo') + """ + ): + self.lint_test() + with self.assertRaisesFromLine(ConfigureError, 2) as e: + with self.moz_configure( + """ + option('--without-foo', default=False, help='foo') + """ + ): + self.lint_test() + self.assertEqual( + str(e.exception), + "--with-foo should be used instead of " "--without-foo with default=False", + ) + + def test_default_func(self): + # Help text for an option with variable default should contain + # {enable|disable} rule. + with self.moz_configure( + """ + option(env='FOO', help='foo') + option('--enable-bar', default=depends('FOO')(lambda x: bool(x)), + help='{Enable|Disable} bar') + """ + ): + self.lint_test() + with self.assertRaisesFromLine(ConfigureError, 3) as e: + with self.moz_configure( + """ + option(env='FOO', help='foo') + option('--enable-bar', default=depends('FOO')(lambda x: bool(x)),\ + help='Enable bar') + """ + ): + self.lint_test() + self.assertEqual( + str(e.exception), + '`help` should contain "{Enable|Disable}" because of ' + "non-constant default", + ) + + def test_dual_help(self): + # Help text for an option that can be both disabled and enabled with an + # optional value should contain {enable|disable} rule. + with self.moz_configure( + """ + option('--disable-bar', nargs="*", choices=("a", "b"), + help='{Enable|Disable} bar') + """ + ): + self.lint_test() + with self.assertRaisesFromLine(ConfigureError, 2) as e: + with self.moz_configure( + """ + option('--disable-bar', nargs="*", choices=("a", "b"), help='Enable bar') + """ + ): + self.lint_test() + self.assertEqual( + str(e.exception), + '`help` should contain "{Enable|Disable}" because it ' + "can be both disabled and enabled with an optional value", + ) + + def test_large_offset(self): + with self.assertRaisesFromLine(ConfigureError, 375): + with self.moz_configure( + """ + option(env='FOO', help='foo') + """ + + "\n" * 371 + + """ + option('--enable-bar', default=depends('FOO')(lambda x: bool(x)),\ + help='Enable bar') + """ + ): + self.lint_test() + + def test_undefined_global(self): + with self.assertRaisesFromLine(NameError, 6) as e: + with self.moz_configure( + """ + option(env='FOO', help='foo') + @depends('FOO') + def foo(value): + if value: + return unknown + return value + """ + ): + self.lint_test() + + self.assertEqual(str(e.exception), "global name 'unknown' is not defined") + + # Ideally, this would raise on line 4, where `unknown` is used, but + # python disassembly doesn't give use the information. + with self.assertRaisesFromLine(NameError, 2) as e: + with self.moz_configure( + """ + @template + def tmpl(): + @depends(unknown) + def foo(value): + if value: + return True + return foo + tmpl() + """ + ): + self.lint_test() + + self.assertEqual(str(e.exception), "global name 'unknown' is not defined") + + def test_unnecessary_imports(self): + with self.assertRaisesFromLine(NameError, 3) as e: + with self.moz_configure( + """ + option(env='FOO', help='foo') + @depends('FOO') + @imports(_from='__builtin__', _import='list') + def foo(value): + if value: + return list() + return value + """ + ): + self.lint_test() + + self.assertEqual(str(e.exception), "builtin 'list' doesn't need to be imported") + + +if __name__ == "__main__": + main() diff --git a/python/mozbuild/mozbuild/test/configure/test_moz_configure.py b/python/mozbuild/mozbuild/test/configure/test_moz_configure.py new file mode 100644 index 0000000000..7bb1b927e0 --- /dev/null +++ b/python/mozbuild/mozbuild/test/configure/test_moz_configure.py @@ -0,0 +1,185 @@ +# 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 mozunit import main + +from common import BaseConfigureTest, ConfigureTestSandbox +from mozbuild.util import ReadOnlyNamespace, memoized_property + + +def sandbox_class(platform): + class ConfigureTestSandboxOverridingPlatform(ConfigureTestSandbox): + @memoized_property + def _wrapped_sys(self): + sys = {} + exec("from sys import *", sys) + sys["platform"] = platform + return ReadOnlyNamespace(**sys) + + return ConfigureTestSandboxOverridingPlatform + + +class TargetTest(BaseConfigureTest): + def get_target(self, args, env={}): + if "linux" in self.HOST: + platform = "linux2" + elif "mingw" in self.HOST or "windows" in self.HOST: + platform = "win32" + elif "openbsd6" in self.HOST: + platform = "openbsd6" + else: + raise Exception("Missing platform for HOST {}".format(self.HOST)) + sandbox = self.get_sandbox({}, {}, args, env, cls=sandbox_class(platform)) + return sandbox._value_for(sandbox["target"]).alias + + +class TestTargetLinux(TargetTest): + def test_target(self): + self.assertEqual(self.get_target([]), self.HOST) + self.assertEqual(self.get_target(["--target=i686"]), "i686-pc-linux-gnu") + self.assertEqual( + self.get_target(["--target=i686-unknown-linux-gnu"]), + "i686-unknown-linux-gnu", + ) + self.assertEqual( + self.get_target(["--target=i686-pc-windows-msvc"]), "i686-pc-windows-msvc" + ) + + +class TestTargetWindows(TargetTest): + # BaseConfigureTest uses this as the return value for config.guess + HOST = "i686-pc-windows-msvc" + + def test_target(self): + self.assertEqual(self.get_target([]), self.HOST) + self.assertEqual( + self.get_target(["--target=x86_64-pc-windows-msvc"]), + "x86_64-pc-windows-msvc", + ) + self.assertEqual(self.get_target(["--target=x86_64"]), "x86_64-pc-windows-msvc") + + # The tests above are actually not realistic, because most Windows + # machines will have a few environment variables that make us not + # use config.guess. + + # 32-bits process on x86_64 host. + env = { + "PROCESSOR_ARCHITECTURE": "x86", + "PROCESSOR_ARCHITEW6432": "AMD64", + } + self.assertEqual(self.get_target([], env), "x86_64-pc-windows-msvc") + self.assertEqual( + self.get_target(["--target=i686-pc-windows-msvc"]), "i686-pc-windows-msvc" + ) + self.assertEqual(self.get_target(["--target=i686"]), "i686-pc-windows-msvc") + + # 64-bits process on x86_64 host. + env = { + "PROCESSOR_ARCHITECTURE": "AMD64", + } + self.assertEqual(self.get_target([], env), "x86_64-pc-windows-msvc") + self.assertEqual( + self.get_target(["--target=i686-pc-windows-msvc"]), "i686-pc-windows-msvc" + ) + self.assertEqual(self.get_target(["--target=i686"]), "i686-pc-windows-msvc") + + # 32-bits process on x86 host. + env = { + "PROCESSOR_ARCHITECTURE": "x86", + } + self.assertEqual(self.get_target([], env), "i686-pc-windows-msvc") + self.assertEqual( + self.get_target(["--target=x86_64-pc-windows-msvc"]), + "x86_64-pc-windows-msvc", + ) + self.assertEqual(self.get_target(["--target=x86_64"]), "x86_64-pc-windows-msvc") + + # While host autodection will give us a -windows-msvc triplet, setting host + # is expecting to implicitly set the target. + self.assertEqual( + self.get_target(["--host=x86_64-pc-windows-gnu"]), "x86_64-pc-windows-gnu" + ) + self.assertEqual( + self.get_target(["--host=x86_64-pc-mingw32"]), "x86_64-pc-mingw32" + ) + + +class TestTargetAndroid(TargetTest): + HOST = "x86_64-pc-linux-gnu" + + def test_target(self): + self.assertEqual( + self.get_target(["--enable-project=mobile/android"]), + "arm-unknown-linux-androideabi", + ) + self.assertEqual( + self.get_target(["--enable-project=mobile/android", "--target=i686"]), + "i686-unknown-linux-android", + ) + self.assertEqual( + self.get_target(["--enable-project=mobile/android", "--target=x86_64"]), + "x86_64-unknown-linux-android", + ) + self.assertEqual( + self.get_target(["--enable-project=mobile/android", "--target=aarch64"]), + "aarch64-unknown-linux-android", + ) + self.assertEqual( + self.get_target(["--enable-project=mobile/android", "--target=arm"]), + "arm-unknown-linux-androideabi", + ) + + +class TestTargetOpenBSD(TargetTest): + # config.guess returns amd64 on OpenBSD, which we need to pass through to + # config.sub so that it canonicalizes to x86_64. + HOST = "amd64-unknown-openbsd6.4" + + def test_target(self): + self.assertEqual(self.get_target([]), "x86_64-unknown-openbsd6.4") + + def config_sub(self, stdin, args): + if args[0] == "amd64-unknown-openbsd6.4": + return 0, "x86_64-unknown-openbsd6.4", "" + return super(TestTargetOpenBSD, self).config_sub(stdin, args) + + +class TestMozConfigure(BaseConfigureTest): + def test_nsis_version(self): + this = self + + class FakeNSIS(object): + def __init__(self, version): + self.version = version + + def __call__(self, stdin, args): + this.assertEqual(args, ("-version",)) + return 0, self.version, "" + + def check_nsis_version(version): + sandbox = self.get_sandbox( + {"/usr/bin/makensis": FakeNSIS(version)}, + {}, + ["--target=x86_64-pc-windows-msvc", "--disable-bootstrap"], + {"PATH": "/usr/bin", "MAKENSISU": "/usr/bin/makensis"}, + ) + return sandbox._value_for(sandbox["nsis_version"]) + + with self.assertRaises(SystemExit): + check_nsis_version("v2.5") + + with self.assertRaises(SystemExit): + check_nsis_version("v3.0a2") + + self.assertEqual(check_nsis_version("v3.0b1"), "3.0b1") + self.assertEqual(check_nsis_version("v3.0b2"), "3.0b2") + self.assertEqual(check_nsis_version("v3.0rc1"), "3.0rc1") + self.assertEqual(check_nsis_version("v3.0"), "3.0") + self.assertEqual(check_nsis_version("v3.0-2"), "3.0") + self.assertEqual(check_nsis_version("v3.0.1"), "3.0") + self.assertEqual(check_nsis_version("v3.1"), "3.1") + + +if __name__ == "__main__": + main() diff --git a/python/mozbuild/mozbuild/test/configure/test_options.py b/python/mozbuild/mozbuild/test/configure/test_options.py new file mode 100644 index 0000000000..91f2968023 --- /dev/null +++ b/python/mozbuild/mozbuild/test/configure/test_options.py @@ -0,0 +1,916 @@ +# 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/. + +import unittest + +from mozunit import main + +from mozbuild.configure.options import ( + CommandLineHelper, + ConflictingOptionError, + InvalidOptionError, + NegativeOptionValue, + Option, + OptionValue, + PositiveOptionValue, +) + + +class Option(Option): + def __init__(self, *args, **kwargs): + kwargs["help"] = "Dummy help" + super(Option, self).__init__(*args, **kwargs) + + +class TestOption(unittest.TestCase): + def test_option(self): + option = Option("--option") + self.assertEqual(option.prefix, "") + self.assertEqual(option.name, "option") + self.assertEqual(option.env, None) + self.assertFalse(option.default) + + option = Option("--enable-option") + self.assertEqual(option.prefix, "enable") + self.assertEqual(option.name, "option") + self.assertEqual(option.env, None) + self.assertFalse(option.default) + + option = Option("--disable-option") + self.assertEqual(option.prefix, "disable") + self.assertEqual(option.name, "option") + self.assertEqual(option.env, None) + self.assertTrue(option.default) + + option = Option("--with-option") + self.assertEqual(option.prefix, "with") + self.assertEqual(option.name, "option") + self.assertEqual(option.env, None) + self.assertFalse(option.default) + + option = Option("--without-option") + self.assertEqual(option.prefix, "without") + self.assertEqual(option.name, "option") + self.assertEqual(option.env, None) + self.assertTrue(option.default) + + option = Option("--without-option-foo", env="MOZ_OPTION") + self.assertEqual(option.env, "MOZ_OPTION") + + option = Option(env="MOZ_OPTION") + self.assertEqual(option.prefix, "") + self.assertEqual(option.name, None) + self.assertEqual(option.env, "MOZ_OPTION") + self.assertFalse(option.default) + + with self.assertRaises(InvalidOptionError) as e: + Option("--option", nargs=0, default=("a",)) + self.assertEqual( + str(e.exception), "The given `default` doesn't satisfy `nargs`" + ) + + with self.assertRaises(InvalidOptionError) as e: + Option("--option", nargs=1, default=()) + self.assertEqual( + str(e.exception), "default must be a bool, a string or a tuple of strings" + ) + + with self.assertRaises(InvalidOptionError) as e: + Option("--option", nargs=1, default=True) + self.assertEqual( + str(e.exception), "The given `default` doesn't satisfy `nargs`" + ) + + with self.assertRaises(InvalidOptionError) as e: + Option("--option", nargs=1, default=("a", "b")) + self.assertEqual( + str(e.exception), "The given `default` doesn't satisfy `nargs`" + ) + + with self.assertRaises(InvalidOptionError) as e: + Option("--option", nargs=2, default=()) + self.assertEqual( + str(e.exception), "default must be a bool, a string or a tuple of strings" + ) + + with self.assertRaises(InvalidOptionError) as e: + Option("--option", nargs=2, default=True) + self.assertEqual( + str(e.exception), "The given `default` doesn't satisfy `nargs`" + ) + + with self.assertRaises(InvalidOptionError) as e: + Option("--option", nargs=2, default=("a",)) + self.assertEqual( + str(e.exception), "The given `default` doesn't satisfy `nargs`" + ) + + with self.assertRaises(InvalidOptionError) as e: + Option("--option", nargs="?", default=("a", "b")) + self.assertEqual( + str(e.exception), "The given `default` doesn't satisfy `nargs`" + ) + + with self.assertRaises(InvalidOptionError) as e: + Option("--option", nargs="+", default=()) + self.assertEqual( + str(e.exception), "default must be a bool, a string or a tuple of strings" + ) + + with self.assertRaises(InvalidOptionError) as e: + Option("--option", nargs="+", default=True) + self.assertEqual( + str(e.exception), "The given `default` doesn't satisfy `nargs`" + ) + + # --disable options with a nargs value that requires at least one + # argument need to be given a default. + with self.assertRaises(InvalidOptionError) as e: + Option("--disable-option", nargs=1) + self.assertEqual( + str(e.exception), "The given `default` doesn't satisfy `nargs`" + ) + + with self.assertRaises(InvalidOptionError) as e: + Option("--disable-option", nargs="+") + self.assertEqual( + str(e.exception), "The given `default` doesn't satisfy `nargs`" + ) + + # Test nargs inference from default value + option = Option("--with-foo", default=True) + self.assertEqual(option.nargs, 0) + + option = Option("--with-foo", default=False) + self.assertEqual(option.nargs, 0) + + option = Option("--with-foo", default="a") + self.assertEqual(option.nargs, "?") + + option = Option("--with-foo", default=("a",)) + self.assertEqual(option.nargs, "?") + + option = Option("--with-foo", default=("a", "b")) + self.assertEqual(option.nargs, "*") + + option = Option(env="FOO", default=True) + self.assertEqual(option.nargs, 0) + + option = Option(env="FOO", default=False) + self.assertEqual(option.nargs, 0) + + option = Option(env="FOO", default="a") + self.assertEqual(option.nargs, "?") + + option = Option(env="FOO", default=("a",)) + self.assertEqual(option.nargs, "?") + + option = Option(env="FOO", default=("a", "b")) + self.assertEqual(option.nargs, "*") + + def test_option_option(self): + for option in ( + "--option", + "--enable-option", + "--disable-option", + "--with-option", + "--without-option", + ): + self.assertEqual(Option(option).option, option) + self.assertEqual(Option(option, env="FOO").option, option) + + opt = Option(option, default=False) + self.assertEqual( + opt.option, + option.replace("-disable-", "-enable-").replace("-without-", "-with-"), + ) + + opt = Option(option, default=True) + self.assertEqual( + opt.option, + option.replace("-enable-", "-disable-").replace("-with-", "-without-"), + ) + + self.assertEqual(Option(env="FOO").option, "FOO") + + def test_option_choices(self): + with self.assertRaises(InvalidOptionError) as e: + Option("--option", nargs=3, choices=("a", "b")) + self.assertEqual(str(e.exception), "Not enough `choices` for `nargs`") + + with self.assertRaises(InvalidOptionError) as e: + Option("--without-option", nargs=1, choices=("a", "b")) + self.assertEqual( + str(e.exception), "A `default` must be given along with `choices`" + ) + + with self.assertRaises(InvalidOptionError) as e: + Option("--without-option", nargs="+", choices=("a", "b")) + self.assertEqual( + str(e.exception), "A `default` must be given along with `choices`" + ) + + with self.assertRaises(InvalidOptionError) as e: + Option("--without-option", default="c", choices=("a", "b")) + self.assertEqual( + str(e.exception), "The `default` value must be one of 'a', 'b'" + ) + + with self.assertRaises(InvalidOptionError) as e: + Option( + "--without-option", + default=( + "a", + "c", + ), + choices=("a", "b"), + ) + self.assertEqual( + str(e.exception), "The `default` value must be one of 'a', 'b'" + ) + + with self.assertRaises(InvalidOptionError) as e: + Option("--without-option", default=("c",), choices=("a", "b")) + self.assertEqual( + str(e.exception), "The `default` value must be one of 'a', 'b'" + ) + + option = Option("--with-option", nargs="+", choices=("a", "b")) + with self.assertRaises(InvalidOptionError) as e: + option.get_value("--with-option=c") + self.assertEqual(str(e.exception), "'c' is not one of 'a', 'b'") + + value = option.get_value("--with-option=b,a") + self.assertTrue(value) + self.assertEqual(PositiveOptionValue(("b", "a")), value) + + option = Option("--without-option", nargs="*", default="a", choices=("a", "b")) + with self.assertRaises(InvalidOptionError) as e: + option.get_value("--with-option=c") + self.assertEqual(str(e.exception), "'c' is not one of 'a', 'b'") + + value = option.get_value("--with-option=b,a") + self.assertTrue(value) + self.assertEqual(PositiveOptionValue(("b", "a")), value) + + # Default is enabled without a value, but the option can be also be disabled or + # used with a value. + option = Option("--without-option", nargs="*", choices=("a", "b")) + value = option.get_value("--with-option") + self.assertEqual(PositiveOptionValue(), value) + value = option.get_value("--with-option=a") + self.assertEqual(PositiveOptionValue(("a",)), value) + with self.assertRaises(InvalidOptionError) as e: + option.get_value("--with-option=c") + self.assertEqual(str(e.exception), "'c' is not one of 'a', 'b'") + + # Test nargs inference from choices + option = Option("--with-option", choices=("a", "b")) + self.assertEqual(option.nargs, 1) + + # Test "relative" values + option = Option( + "--with-option", nargs="*", default=("b", "c"), choices=("a", "b", "c", "d") + ) + + value = option.get_value("--with-option=+d") + self.assertEqual(PositiveOptionValue(("b", "c", "d")), value) + + value = option.get_value("--with-option=-b") + self.assertEqual(PositiveOptionValue(("c",)), value) + + value = option.get_value("--with-option=-b,+d") + self.assertEqual(PositiveOptionValue(("c", "d")), value) + + # Adding something that is in the default is fine + value = option.get_value("--with-option=+b") + self.assertEqual(PositiveOptionValue(("b", "c")), value) + + # Removing something that is not in the default is fine, as long as it + # is one of the choices + value = option.get_value("--with-option=-a") + self.assertEqual(PositiveOptionValue(("b", "c")), value) + + with self.assertRaises(InvalidOptionError) as e: + option.get_value("--with-option=-e") + self.assertEqual(str(e.exception), "'e' is not one of 'a', 'b', 'c', 'd'") + + # Other "not a choice" errors. + with self.assertRaises(InvalidOptionError) as e: + option.get_value("--with-option=+e") + self.assertEqual(str(e.exception), "'e' is not one of 'a', 'b', 'c', 'd'") + + with self.assertRaises(InvalidOptionError) as e: + option.get_value("--with-option=e") + self.assertEqual(str(e.exception), "'e' is not one of 'a', 'b', 'c', 'd'") + + def test_option_value_compare(self): + # OptionValue are tuple and equivalence should compare as tuples. + val = PositiveOptionValue(("foo",)) + + self.assertEqual(val[0], "foo") + self.assertEqual(val, PositiveOptionValue(("foo",))) + self.assertNotEqual(val, PositiveOptionValue(("foo", "bar"))) + + # Can compare a tuple to an OptionValue. + self.assertEqual(val, ("foo",)) + self.assertNotEqual(val, ("foo", "bar")) + + # Different OptionValue types are never equal. + self.assertNotEqual(val, OptionValue(("foo",))) + + # For usability reasons, we raise TypeError when attempting to compare + # against a non-tuple. + with self.assertRaisesRegexp(TypeError, "cannot compare a"): + val == "foo" + + # But we allow empty option values to compare otherwise we can't + # easily compare value-less types like PositiveOptionValue and + # NegativeOptionValue. + empty_positive = PositiveOptionValue() + empty_negative = NegativeOptionValue() + self.assertEqual(empty_positive, ()) + self.assertEqual(empty_positive, PositiveOptionValue()) + self.assertEqual(empty_negative, ()) + self.assertEqual(empty_negative, NegativeOptionValue()) + self.assertNotEqual(empty_positive, "foo") + self.assertNotEqual(empty_positive, ("foo",)) + self.assertNotEqual(empty_negative, "foo") + self.assertNotEqual(empty_negative, ("foo",)) + + def test_option_value_format(self): + val = PositiveOptionValue() + self.assertEqual("--with-value", val.format("--with-value")) + self.assertEqual("--with-value", val.format("--without-value")) + self.assertEqual("--enable-value", val.format("--enable-value")) + self.assertEqual("--enable-value", val.format("--disable-value")) + self.assertEqual("--value", val.format("--value")) + self.assertEqual("VALUE=1", val.format("VALUE")) + + val = PositiveOptionValue(("a",)) + self.assertEqual("--with-value=a", val.format("--with-value")) + self.assertEqual("--with-value=a", val.format("--without-value")) + self.assertEqual("--enable-value=a", val.format("--enable-value")) + self.assertEqual("--enable-value=a", val.format("--disable-value")) + self.assertEqual("--value=a", val.format("--value")) + self.assertEqual("VALUE=a", val.format("VALUE")) + + val = PositiveOptionValue(("a", "b")) + self.assertEqual("--with-value=a,b", val.format("--with-value")) + self.assertEqual("--with-value=a,b", val.format("--without-value")) + self.assertEqual("--enable-value=a,b", val.format("--enable-value")) + self.assertEqual("--enable-value=a,b", val.format("--disable-value")) + self.assertEqual("--value=a,b", val.format("--value")) + self.assertEqual("VALUE=a,b", val.format("VALUE")) + + val = NegativeOptionValue() + self.assertEqual("--without-value", val.format("--with-value")) + self.assertEqual("--without-value", val.format("--without-value")) + self.assertEqual("--disable-value", val.format("--enable-value")) + self.assertEqual("--disable-value", val.format("--disable-value")) + self.assertEqual("", val.format("--value")) + self.assertEqual("VALUE=", val.format("VALUE")) + + def test_option_value(self, name="option", nargs=0, default=None): + disabled = name.startswith(("disable-", "without-")) + if disabled: + negOptionValue = PositiveOptionValue + posOptionValue = NegativeOptionValue + else: + posOptionValue = PositiveOptionValue + negOptionValue = NegativeOptionValue + defaultValue = PositiveOptionValue(default) if default else negOptionValue() + + option = Option("--%s" % name, nargs=nargs, default=default) + + if nargs in (0, "?", "*") or disabled: + value = option.get_value("--%s" % name, "option") + self.assertEqual(value, posOptionValue()) + self.assertEqual(value.origin, "option") + else: + with self.assertRaises(InvalidOptionError) as e: + option.get_value("--%s" % name) + if nargs == 1: + self.assertEqual(str(e.exception), "--%s takes 1 value" % name) + elif nargs == "+": + self.assertEqual(str(e.exception), "--%s takes 1 or more values" % name) + else: + self.assertEqual(str(e.exception), "--%s takes 2 values" % name) + + value = option.get_value("") + self.assertEqual(value, defaultValue) + self.assertEqual(value.origin, "default") + + value = option.get_value(None) + self.assertEqual(value, defaultValue) + self.assertEqual(value.origin, "default") + + with self.assertRaises(AssertionError): + value = option.get_value("MOZ_OPTION=", "environment") + + with self.assertRaises(AssertionError): + value = option.get_value("MOZ_OPTION=1", "environment") + + with self.assertRaises(AssertionError): + value = option.get_value("--foo") + + if nargs in (1, "?", "*", "+") and not disabled: + value = option.get_value("--%s=" % name, "option") + self.assertEqual(value, PositiveOptionValue(("",))) + self.assertEqual(value.origin, "option") + else: + with self.assertRaises(InvalidOptionError) as e: + option.get_value("--%s=" % name) + if disabled: + self.assertEqual(str(e.exception), "Cannot pass a value to --%s" % name) + else: + self.assertEqual( + str(e.exception), "--%s takes %d values" % (name, nargs) + ) + + if nargs in (1, "?", "*", "+") and not disabled: + value = option.get_value("--%s=foo" % name, "option") + self.assertEqual(value, PositiveOptionValue(("foo",))) + self.assertEqual(value.origin, "option") + else: + with self.assertRaises(InvalidOptionError) as e: + option.get_value("--%s=foo" % name) + if disabled: + self.assertEqual(str(e.exception), "Cannot pass a value to --%s" % name) + else: + self.assertEqual( + str(e.exception), "--%s takes %d values" % (name, nargs) + ) + + if nargs in (2, "*", "+") and not disabled: + value = option.get_value("--%s=foo,bar" % name, "option") + self.assertEqual(value, PositiveOptionValue(("foo", "bar"))) + self.assertEqual(value.origin, "option") + else: + with self.assertRaises(InvalidOptionError) as e: + option.get_value("--%s=foo,bar" % name, "option") + if disabled: + self.assertEqual(str(e.exception), "Cannot pass a value to --%s" % name) + elif nargs == "?": + self.assertEqual(str(e.exception), "--%s takes 0 or 1 values" % name) + else: + self.assertEqual( + str(e.exception), + "--%s takes %d value%s" % (name, nargs, "s" if nargs != 1 else ""), + ) + + option = Option("--%s" % name, env="MOZ_OPTION", nargs=nargs, default=default) + if nargs in (0, "?", "*") or disabled: + value = option.get_value("--%s" % name, "option") + self.assertEqual(value, posOptionValue()) + self.assertEqual(value.origin, "option") + else: + with self.assertRaises(InvalidOptionError) as e: + option.get_value("--%s" % name) + if disabled: + self.assertEqual(str(e.exception), "Cannot pass a value to --%s" % name) + elif nargs == "+": + self.assertEqual(str(e.exception), "--%s takes 1 or more values" % name) + else: + self.assertEqual( + str(e.exception), + "--%s takes %d value%s" % (name, nargs, "s" if nargs != 1 else ""), + ) + + value = option.get_value("") + self.assertEqual(value, defaultValue) + self.assertEqual(value.origin, "default") + + value = option.get_value(None) + self.assertEqual(value, defaultValue) + self.assertEqual(value.origin, "default") + + value = option.get_value("MOZ_OPTION=", "environment") + self.assertEqual(value, NegativeOptionValue()) + self.assertEqual(value.origin, "environment") + + if nargs in (0, "?", "*"): + value = option.get_value("MOZ_OPTION=1", "environment") + self.assertEqual(value, PositiveOptionValue()) + self.assertEqual(value.origin, "environment") + elif nargs in (1, "+"): + value = option.get_value("MOZ_OPTION=1", "environment") + self.assertEqual(value, PositiveOptionValue(("1",))) + self.assertEqual(value.origin, "environment") + else: + with self.assertRaises(InvalidOptionError) as e: + option.get_value("MOZ_OPTION=1", "environment") + self.assertEqual(str(e.exception), "MOZ_OPTION takes 2 values") + + if nargs in (1, "?", "*", "+") and not disabled: + value = option.get_value("--%s=" % name, "option") + self.assertEqual(value, PositiveOptionValue(("",))) + self.assertEqual(value.origin, "option") + else: + with self.assertRaises(InvalidOptionError) as e: + option.get_value("--%s=" % name, "option") + if disabled: + self.assertEqual(str(e.exception), "Cannot pass a value to --%s" % name) + else: + self.assertEqual( + str(e.exception), "--%s takes %d values" % (name, nargs) + ) + + with self.assertRaises(AssertionError): + value = option.get_value("--foo", "option") + + if nargs in (1, "?", "*", "+"): + value = option.get_value("MOZ_OPTION=foo", "environment") + self.assertEqual(value, PositiveOptionValue(("foo",))) + self.assertEqual(value.origin, "environment") + else: + with self.assertRaises(InvalidOptionError) as e: + option.get_value("MOZ_OPTION=foo", "environment") + self.assertEqual(str(e.exception), "MOZ_OPTION takes %d values" % nargs) + + if nargs in (2, "*", "+"): + value = option.get_value("MOZ_OPTION=foo,bar", "environment") + self.assertEqual(value, PositiveOptionValue(("foo", "bar"))) + self.assertEqual(value.origin, "environment") + else: + with self.assertRaises(InvalidOptionError) as e: + option.get_value("MOZ_OPTION=foo,bar", "environment") + if nargs == "?": + self.assertEqual(str(e.exception), "MOZ_OPTION takes 0 or 1 values") + else: + self.assertEqual( + str(e.exception), + "MOZ_OPTION takes %d value%s" % (nargs, "s" if nargs != 1 else ""), + ) + + if disabled: + return option + + env_option = Option(env="MOZ_OPTION", nargs=nargs, default=default) + with self.assertRaises(AssertionError): + env_option.get_value("--%s" % name) + + value = env_option.get_value("") + self.assertEqual(value, defaultValue) + self.assertEqual(value.origin, "default") + + value = env_option.get_value("MOZ_OPTION=", "environment") + self.assertEqual(value, negOptionValue()) + self.assertEqual(value.origin, "environment") + + if nargs in (0, "?", "*"): + value = env_option.get_value("MOZ_OPTION=1", "environment") + self.assertEqual(value, posOptionValue()) + self.assertTrue(value) + self.assertEqual(value.origin, "environment") + elif nargs in (1, "+"): + value = env_option.get_value("MOZ_OPTION=1", "environment") + self.assertEqual(value, PositiveOptionValue(("1",))) + self.assertEqual(value.origin, "environment") + else: + with self.assertRaises(InvalidOptionError) as e: + env_option.get_value("MOZ_OPTION=1", "environment") + self.assertEqual(str(e.exception), "MOZ_OPTION takes 2 values") + + with self.assertRaises(AssertionError) as e: + env_option.get_value("--%s" % name) + + with self.assertRaises(AssertionError) as e: + env_option.get_value("--foo") + + if nargs in (1, "?", "*", "+"): + value = env_option.get_value("MOZ_OPTION=foo", "environment") + self.assertEqual(value, PositiveOptionValue(("foo",))) + self.assertEqual(value.origin, "environment") + else: + with self.assertRaises(InvalidOptionError) as e: + env_option.get_value("MOZ_OPTION=foo", "environment") + self.assertEqual(str(e.exception), "MOZ_OPTION takes %d values" % nargs) + + if nargs in (2, "*", "+"): + value = env_option.get_value("MOZ_OPTION=foo,bar", "environment") + self.assertEqual(value, PositiveOptionValue(("foo", "bar"))) + self.assertEqual(value.origin, "environment") + else: + with self.assertRaises(InvalidOptionError) as e: + env_option.get_value("MOZ_OPTION=foo,bar", "environment") + if nargs == "?": + self.assertEqual(str(e.exception), "MOZ_OPTION takes 0 or 1 values") + else: + self.assertEqual( + str(e.exception), + "MOZ_OPTION takes %d value%s" % (nargs, "s" if nargs != 1 else ""), + ) + + return option + + def test_option_value_enable( + self, enable="enable", disable="disable", nargs=0, default=None + ): + option = self.test_option_value( + "%s-option" % enable, nargs=nargs, default=default + ) + + value = option.get_value("--%s-option" % disable, "option") + self.assertEqual(value, NegativeOptionValue()) + self.assertEqual(value.origin, "option") + + option = self.test_option_value( + "%s-option" % disable, nargs=nargs, default=default + ) + + if nargs in (0, "?", "*"): + value = option.get_value("--%s-option" % enable, "option") + self.assertEqual(value, PositiveOptionValue()) + self.assertEqual(value.origin, "option") + else: + with self.assertRaises(InvalidOptionError) as e: + option.get_value("--%s-option" % enable, "option") + if nargs == 1: + self.assertEqual(str(e.exception), "--%s-option takes 1 value" % enable) + elif nargs == "+": + self.assertEqual( + str(e.exception), "--%s-option takes 1 or more values" % enable + ) + else: + self.assertEqual( + str(e.exception), "--%s-option takes 2 values" % enable + ) + + def test_option_value_with(self): + self.test_option_value_enable("with", "without") + + def test_option_value_invalid_nargs(self): + with self.assertRaises(InvalidOptionError) as e: + Option("--option", nargs="foo") + self.assertEqual( + str(e.exception), "nargs must be a positive integer, '?', '*' or '+'" + ) + + with self.assertRaises(InvalidOptionError) as e: + Option("--option", nargs=-2) + self.assertEqual( + str(e.exception), "nargs must be a positive integer, '?', '*' or '+'" + ) + + def test_option_value_nargs_1(self): + self.test_option_value(nargs=1) + self.test_option_value(nargs=1, default=("a",)) + self.test_option_value_enable(nargs=1, default=("a",)) + + # A default is required + with self.assertRaises(InvalidOptionError) as e: + Option("--disable-option", nargs=1) + self.assertEqual( + str(e.exception), "The given `default` doesn't satisfy `nargs`" + ) + + def test_option_value_nargs_2(self): + self.test_option_value(nargs=2) + self.test_option_value(nargs=2, default=("a", "b")) + self.test_option_value_enable(nargs=2, default=("a", "b")) + + # A default is required + with self.assertRaises(InvalidOptionError) as e: + Option("--disable-option", nargs=2) + self.assertEqual( + str(e.exception), "The given `default` doesn't satisfy `nargs`" + ) + + def test_option_value_nargs_0_or_1(self): + self.test_option_value(nargs="?") + self.test_option_value(nargs="?", default=("a",)) + self.test_option_value_enable(nargs="?") + self.test_option_value_enable(nargs="?", default=("a",)) + + def test_option_value_nargs_0_or_more(self): + self.test_option_value(nargs="*") + self.test_option_value(nargs="*", default=("a",)) + self.test_option_value(nargs="*", default=("a", "b")) + self.test_option_value_enable(nargs="*") + self.test_option_value_enable(nargs="*", default=("a",)) + self.test_option_value_enable(nargs="*", default=("a", "b")) + + def test_option_value_nargs_1_or_more(self): + self.test_option_value(nargs="+") + self.test_option_value(nargs="+", default=("a",)) + self.test_option_value(nargs="+", default=("a", "b")) + self.test_option_value_enable(nargs="+", default=("a",)) + self.test_option_value_enable(nargs="+", default=("a", "b")) + + # A default is required + with self.assertRaises(InvalidOptionError) as e: + Option("--disable-option", nargs="+") + self.assertEqual( + str(e.exception), "The given `default` doesn't satisfy `nargs`" + ) + + +class TestCommandLineHelper(unittest.TestCase): + def test_basic(self): + helper = CommandLineHelper({}, ["cmd", "--foo", "--bar"]) + + self.assertEqual(["--foo", "--bar"], list(helper)) + + helper.add("--enable-qux") + + self.assertEqual(["--foo", "--bar", "--enable-qux"], list(helper)) + + value, option = helper.handle(Option("--bar")) + self.assertEqual(["--foo", "--enable-qux"], list(helper)) + self.assertEqual(PositiveOptionValue(), value) + self.assertEqual("--bar", option) + + value, option = helper.handle(Option("--baz")) + self.assertEqual(["--foo", "--enable-qux"], list(helper)) + self.assertEqual(NegativeOptionValue(), value) + self.assertEqual(None, option) + + with self.assertRaises(AssertionError): + CommandLineHelper({}, ["--foo", "--bar"]) + + def test_precedence(self): + foo = Option("--with-foo", nargs="*") + helper = CommandLineHelper({}, ["cmd", "--with-foo=a,b"]) + value, option = helper.handle(foo) + self.assertEqual(PositiveOptionValue(("a", "b")), value) + self.assertEqual("command-line", value.origin) + self.assertEqual("--with-foo=a,b", option) + + helper = CommandLineHelper({}, ["cmd", "--with-foo=a,b", "--without-foo"]) + value, option = helper.handle(foo) + self.assertEqual(NegativeOptionValue(), value) + self.assertEqual("command-line", value.origin) + self.assertEqual("--without-foo", option) + + helper = CommandLineHelper({}, ["cmd", "--without-foo", "--with-foo=a,b"]) + value, option = helper.handle(foo) + self.assertEqual(PositiveOptionValue(("a", "b")), value) + self.assertEqual("command-line", value.origin) + self.assertEqual("--with-foo=a,b", option) + + foo = Option("--with-foo", env="FOO", nargs="*") + helper = CommandLineHelper({"FOO": ""}, ["cmd", "--with-foo=a,b"]) + value, option = helper.handle(foo) + self.assertEqual(PositiveOptionValue(("a", "b")), value) + self.assertEqual("command-line", value.origin) + self.assertEqual("--with-foo=a,b", option) + + helper = CommandLineHelper({"FOO": "a,b"}, ["cmd", "--without-foo"]) + value, option = helper.handle(foo) + self.assertEqual(NegativeOptionValue(), value) + self.assertEqual("command-line", value.origin) + self.assertEqual("--without-foo", option) + + helper = CommandLineHelper({"FOO": ""}, ["cmd", "--with-bar=a,b"]) + value, option = helper.handle(foo) + self.assertEqual(NegativeOptionValue(), value) + self.assertEqual("environment", value.origin) + self.assertEqual("FOO=", option) + + helper = CommandLineHelper({"FOO": "a,b"}, ["cmd", "--without-bar"]) + value, option = helper.handle(foo) + self.assertEqual(PositiveOptionValue(("a", "b")), value) + self.assertEqual("environment", value.origin) + self.assertEqual("FOO=a,b", option) + + helper = CommandLineHelper({}, ["cmd", "--with-foo=a,b", "FOO="]) + value, option = helper.handle(foo) + self.assertEqual(NegativeOptionValue(), value) + self.assertEqual("command-line", value.origin) + self.assertEqual("FOO=", option) + + helper = CommandLineHelper({}, ["cmd", "--without-foo", "FOO=a,b"]) + value, option = helper.handle(foo) + self.assertEqual(PositiveOptionValue(("a", "b")), value) + self.assertEqual("command-line", value.origin) + self.assertEqual("FOO=a,b", option) + + helper = CommandLineHelper({}, ["cmd", "FOO=", "--with-foo=a,b"]) + value, option = helper.handle(foo) + self.assertEqual(PositiveOptionValue(("a", "b")), value) + self.assertEqual("command-line", value.origin) + self.assertEqual("--with-foo=a,b", option) + + helper = CommandLineHelper({}, ["cmd", "FOO=a,b", "--without-foo"]) + value, option = helper.handle(foo) + self.assertEqual(NegativeOptionValue(), value) + self.assertEqual("command-line", value.origin) + self.assertEqual("--without-foo", option) + + def test_extra_args(self): + foo = Option("--with-foo", env="FOO", nargs="*") + helper = CommandLineHelper({}, ["cmd"]) + helper.add("FOO=a,b,c", "other-origin") + value, option = helper.handle(foo) + self.assertEqual(PositiveOptionValue(("a", "b", "c")), value) + self.assertEqual("other-origin", value.origin) + self.assertEqual("FOO=a,b,c", option) + + helper = CommandLineHelper({}, ["cmd"]) + helper.add("FOO=a,b,c", "other-origin") + helper.add("--with-foo=a,b,c", "other-origin") + value, option = helper.handle(foo) + self.assertEqual(PositiveOptionValue(("a", "b", "c")), value) + self.assertEqual("other-origin", value.origin) + self.assertEqual("--with-foo=a,b,c", option) + + # Adding conflicting options is not allowed. + helper = CommandLineHelper({}, ["cmd"]) + helper.add("FOO=a,b,c", "other-origin") + with self.assertRaises(ConflictingOptionError) as cm: + helper.add("FOO=", "other-origin") + self.assertEqual("FOO=", cm.exception.arg) + self.assertEqual("other-origin", cm.exception.origin) + self.assertEqual("FOO=a,b,c", cm.exception.old_arg) + self.assertEqual("other-origin", cm.exception.old_origin) + with self.assertRaises(ConflictingOptionError) as cm: + helper.add("FOO=a,b", "other-origin") + self.assertEqual("FOO=a,b", cm.exception.arg) + self.assertEqual("other-origin", cm.exception.origin) + self.assertEqual("FOO=a,b,c", cm.exception.old_arg) + self.assertEqual("other-origin", cm.exception.old_origin) + # But adding the same is allowed. + helper.add("FOO=a,b,c", "other-origin") + value, option = helper.handle(foo) + self.assertEqual(PositiveOptionValue(("a", "b", "c")), value) + self.assertEqual("other-origin", value.origin) + self.assertEqual("FOO=a,b,c", option) + + # The same rule as above applies when using the option form vs. the + # variable form. But we can't detect it when .add is called. + helper = CommandLineHelper({}, ["cmd"]) + helper.add("FOO=a,b,c", "other-origin") + helper.add("--without-foo", "other-origin") + with self.assertRaises(ConflictingOptionError) as cm: + helper.handle(foo) + self.assertEqual("--without-foo", cm.exception.arg) + self.assertEqual("other-origin", cm.exception.origin) + self.assertEqual("FOO=a,b,c", cm.exception.old_arg) + self.assertEqual("other-origin", cm.exception.old_origin) + helper = CommandLineHelper({}, ["cmd"]) + helper.add("FOO=a,b,c", "other-origin") + helper.add("--with-foo=a,b", "other-origin") + with self.assertRaises(ConflictingOptionError) as cm: + helper.handle(foo) + self.assertEqual("--with-foo=a,b", cm.exception.arg) + self.assertEqual("other-origin", cm.exception.origin) + self.assertEqual("FOO=a,b,c", cm.exception.old_arg) + self.assertEqual("other-origin", cm.exception.old_origin) + helper = CommandLineHelper({}, ["cmd"]) + helper.add("FOO=a,b,c", "other-origin") + helper.add("--with-foo=a,b,c", "other-origin") + value, option = helper.handle(foo) + self.assertEqual(PositiveOptionValue(("a", "b", "c")), value) + self.assertEqual("other-origin", value.origin) + self.assertEqual("--with-foo=a,b,c", option) + + # Conflicts are also not allowed against what is in the + # environment/on the command line. + helper = CommandLineHelper({}, ["cmd", "--with-foo=a,b"]) + helper.add("FOO=a,b,c", "other-origin") + with self.assertRaises(ConflictingOptionError) as cm: + helper.handle(foo) + self.assertEqual("FOO=a,b,c", cm.exception.arg) + self.assertEqual("other-origin", cm.exception.origin) + self.assertEqual("--with-foo=a,b", cm.exception.old_arg) + self.assertEqual("command-line", cm.exception.old_origin) + + helper = CommandLineHelper({}, ["cmd", "--with-foo=a,b"]) + helper.add("--without-foo", "other-origin") + with self.assertRaises(ConflictingOptionError) as cm: + helper.handle(foo) + self.assertEqual("--without-foo", cm.exception.arg) + self.assertEqual("other-origin", cm.exception.origin) + self.assertEqual("--with-foo=a,b", cm.exception.old_arg) + self.assertEqual("command-line", cm.exception.old_origin) + + def test_possible_origins(self): + with self.assertRaises(InvalidOptionError): + Option("--foo", possible_origins="command-line") + + helper = CommandLineHelper({"BAZ": "1"}, ["cmd", "--foo", "--bar"]) + foo = Option("--foo", possible_origins=("command-line",)) + value, option = helper.handle(foo) + self.assertEqual(PositiveOptionValue(), value) + self.assertEqual("command-line", value.origin) + self.assertEqual("--foo", option) + + bar = Option("--bar", possible_origins=("mozconfig",)) + with self.assertRaisesRegexp( + InvalidOptionError, + "--bar can not be set by command-line. Values are accepted from: mozconfig", + ): + helper.handle(bar) + + baz = Option(env="BAZ", possible_origins=("implied",)) + with self.assertRaisesRegexp( + InvalidOptionError, + "BAZ=1 can not be set by environment. Values are accepted from: implied", + ): + helper.handle(baz) + + +if __name__ == "__main__": + main() diff --git a/python/mozbuild/mozbuild/test/configure/test_toolchain_configure.py b/python/mozbuild/mozbuild/test/configure/test_toolchain_configure.py new file mode 100644 index 0000000000..d438b68eb8 --- /dev/null +++ b/python/mozbuild/mozbuild/test/configure/test_toolchain_configure.py @@ -0,0 +1,2158 @@ +# 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/. + +import logging +import os + +import six +from mozboot.util import MINIMUM_RUST_VERSION +from mozpack import path as mozpath +from mozunit import main +from six import StringIO +from test_toolchain_helpers import CompilerResult, FakeCompiler, PrependFlags + +from common import BaseConfigureTest +from mozbuild.configure.util import Version +from mozbuild.util import ReadOnlyNamespace, memoize + +DEFAULT_C99 = {"__STDC_VERSION__": "199901L"} + +DEFAULT_C11 = {"__STDC_VERSION__": "201112L"} + +DEFAULT_C17 = {"__STDC_VERSION__": "201710L"} + +DEFAULT_CXX_97 = {"__cplusplus": "199711L"} + +DEFAULT_CXX_11 = {"__cplusplus": "201103L"} + +DRAFT_CXX_14 = {"__cplusplus": "201300L"} + +DEFAULT_CXX_14 = {"__cplusplus": "201402L"} + +DRAFT_CXX17_201500 = {"__cplusplus": "201500L"} + +DRAFT_CXX17_201406 = {"__cplusplus": "201406L"} + +DEFAULT_CXX_17 = {"__cplusplus": "201703L"} + +SUPPORTS_GNU99 = {"-std=gnu99": DEFAULT_C99} + +SUPPORTS_GNUXX11 = {"-std=gnu++11": DEFAULT_CXX_11} + +SUPPORTS_GNUXX14 = {"-std=gnu++14": DEFAULT_CXX_14} + +SUPPORTS_CXX14 = {"-std=c++14": DEFAULT_CXX_14} + +SUPPORTS_GNUXX17 = {"-std=gnu++17": DEFAULT_CXX_17} + +SUPPORTS_CXX17 = {"-std=c++17": DEFAULT_CXX_17} + + +@memoize +def GCC_BASE(version): + version = Version(version) + return FakeCompiler( + { + "__GNUC__": version.major, + "__GNUC_MINOR__": version.minor, + "__GNUC_PATCHLEVEL__": version.patch, + "__STDC__": 1, + } + ) + + +@memoize +def GCC(version): + return GCC_BASE(version) + SUPPORTS_GNU99 + + +@memoize +def GXX(version): + return GCC_BASE(version) + DEFAULT_CXX_97 + SUPPORTS_GNUXX11 + + +SUPPORTS_DRAFT_CXX14_VERSION = {"-std=gnu++14": DRAFT_CXX_14} + +SUPPORTS_GNUXX1Z = {"-std=gnu++1z": DRAFT_CXX17_201406} + +SUPPORTS_DRAFT_CXX17_201500_VERSION = {"-std=gnu++17": DRAFT_CXX17_201500} + +GCC_4_9 = GCC("4.9.3") +GXX_4_9 = GXX("4.9.3") + SUPPORTS_DRAFT_CXX14_VERSION +GCC_5 = GCC("5.2.1") + DEFAULT_C11 +GXX_5 = GXX("5.2.1") + SUPPORTS_GNUXX14 +GCC_6 = GCC("6.4.0") + DEFAULT_C11 +GXX_6 = ( + GXX("6.4.0") + + DEFAULT_CXX_14 + + SUPPORTS_GNUXX17 + + SUPPORTS_DRAFT_CXX17_201500_VERSION +) +GCC_7 = GCC("7.3.0") + DEFAULT_C11 +GXX_7 = GXX("7.3.0") + DEFAULT_CXX_14 + SUPPORTS_GNUXX17 + SUPPORTS_CXX17 +GCC_8 = GCC("8.3.0") + DEFAULT_C11 +GXX_8 = GXX("8.3.0") + DEFAULT_CXX_14 + SUPPORTS_GNUXX17 + SUPPORTS_CXX17 +GCC_10 = GCC("10.2.1") + DEFAULT_C17 +GXX_10 = GXX("10.2.1") + DEFAULT_CXX_14 + SUPPORTS_GNUXX17 + SUPPORTS_CXX17 + +DEFAULT_GCC = GCC_8 +DEFAULT_GXX = GXX_8 + +GCC_PLATFORM_LITTLE_ENDIAN = { + "__ORDER_LITTLE_ENDIAN__": 1234, + "__ORDER_BIG_ENDIAN__": 4321, + "__BYTE_ORDER__": 1234, +} + +GCC_PLATFORM_BIG_ENDIAN = { + "__ORDER_LITTLE_ENDIAN__": 1234, + "__ORDER_BIG_ENDIAN__": 4321, + "__BYTE_ORDER__": 4321, +} + +GCC_PLATFORM_X86 = FakeCompiler(GCC_PLATFORM_LITTLE_ENDIAN) + { + None: {"__i386__": 1}, + "-m64": {"__i386__": False, "__x86_64__": 1}, +} + +GCC_PLATFORM_X86_64 = FakeCompiler(GCC_PLATFORM_LITTLE_ENDIAN) + { + None: {"__x86_64__": 1}, + "-m32": {"__x86_64__": False, "__i386__": 1}, +} + +GCC_PLATFORM_ARM = FakeCompiler(GCC_PLATFORM_LITTLE_ENDIAN) + {"__arm__": 1} + +GCC_PLATFORM_LINUX = {"__linux__": 1} + +GCC_PLATFORM_DARWIN = {"__APPLE__": 1} + +GCC_PLATFORM_WIN = {"_WIN32": 1, "WINNT": 1} + +GCC_PLATFORM_OPENBSD = {"__OpenBSD__": 1} + +GCC_PLATFORM_X86_LINUX = FakeCompiler(GCC_PLATFORM_X86, GCC_PLATFORM_LINUX) +GCC_PLATFORM_X86_64_LINUX = FakeCompiler(GCC_PLATFORM_X86_64, GCC_PLATFORM_LINUX) +GCC_PLATFORM_ARM_LINUX = FakeCompiler(GCC_PLATFORM_ARM, GCC_PLATFORM_LINUX) +GCC_PLATFORM_X86_OSX = FakeCompiler(GCC_PLATFORM_X86, GCC_PLATFORM_DARWIN) +GCC_PLATFORM_X86_64_OSX = FakeCompiler(GCC_PLATFORM_X86_64, GCC_PLATFORM_DARWIN) +GCC_PLATFORM_X86_WIN = FakeCompiler(GCC_PLATFORM_X86, GCC_PLATFORM_WIN) +GCC_PLATFORM_X86_64_WIN = FakeCompiler(GCC_PLATFORM_X86_64, GCC_PLATFORM_WIN) + + +@memoize +def CLANG_BASE(version): + version = Version(version) + return FakeCompiler( + { + "__clang__": 1, + "__clang_major__": version.major, + "__clang_minor__": version.minor, + "__clang_patchlevel__": version.patch, + } + ) + + +@memoize +def CLANG(version): + return GCC_BASE("4.2.1") + CLANG_BASE(version) + SUPPORTS_GNU99 + + +@memoize +def CLANGXX(version): + return ( + GCC_BASE("4.2.1") + + CLANG_BASE(version) + + DEFAULT_CXX_97 + + SUPPORTS_GNUXX11 + + SUPPORTS_GNUXX14 + ) + + +CLANG_3_3 = CLANG("3.3.0") + DEFAULT_C99 +CLANGXX_3_3 = CLANGXX("3.3.0") +CLANG_4_0 = CLANG("4.0.2") + DEFAULT_C11 +CLANGXX_4_0 = CLANGXX("4.0.2") + SUPPORTS_GNUXX1Z +CLANG_8_0 = CLANG("8.0.0") + DEFAULT_C11 +CLANGXX_8_0 = CLANGXX("8.0.0") + DEFAULT_CXX_14 + SUPPORTS_GNUXX17 +XCODE_CLANG_3_3 = ( + CLANG("5.0") + + DEFAULT_C99 + + { + # Real Xcode clang has a full version here, but we don't care about it. + "__apple_build_version__": "1" + } +) +XCODE_CLANGXX_3_3 = CLANGXX("5.0") + {"__apple_build_version__": "1"} +XCODE_CLANG_4_0 = CLANG("9.0.0") + DEFAULT_C11 + {"__apple_build_version__": "1"} +XCODE_CLANGXX_4_0 = ( + CLANGXX("9.0.0") + SUPPORTS_GNUXX1Z + {"__apple_build_version__": "1"} +) +XCODE_CLANG_8_0 = CLANG("11.0.1") + DEFAULT_C11 + {"__apple_build_version__": "1"} +XCODE_CLANGXX_8_0 = ( + CLANGXX("11.0.1") + SUPPORTS_GNUXX17 + {"__apple_build_version__": "1"} +) +DEFAULT_CLANG = CLANG_8_0 +DEFAULT_CLANGXX = CLANGXX_8_0 + + +def CLANG_PLATFORM(gcc_platform): + base = { + "--target=x86_64-linux-gnu": GCC_PLATFORM_X86_64_LINUX[None], + "--target=x86_64-apple-darwin11.2.0": GCC_PLATFORM_X86_64_OSX[None], + "--target=i686-linux-gnu": GCC_PLATFORM_X86_LINUX[None], + "--target=i686-apple-darwin11.2.0": GCC_PLATFORM_X86_OSX[None], + "--target=arm-linux-gnu": GCC_PLATFORM_ARM_LINUX[None], + } + undo_gcc_platform = { + k: {symbol: False for symbol in gcc_platform[None]} for k in base + } + return FakeCompiler(gcc_platform, undo_gcc_platform, base) + + +CLANG_PLATFORM_X86_LINUX = CLANG_PLATFORM(GCC_PLATFORM_X86_LINUX) +CLANG_PLATFORM_X86_64_LINUX = CLANG_PLATFORM(GCC_PLATFORM_X86_64_LINUX) +CLANG_PLATFORM_X86_OSX = CLANG_PLATFORM(GCC_PLATFORM_X86_OSX) +CLANG_PLATFORM_X86_64_OSX = CLANG_PLATFORM(GCC_PLATFORM_X86_64_OSX) +CLANG_PLATFORM_X86_WIN = CLANG_PLATFORM(GCC_PLATFORM_X86_WIN) +CLANG_PLATFORM_X86_64_WIN = CLANG_PLATFORM(GCC_PLATFORM_X86_64_WIN) + + +@memoize +def VS(version): + version = Version(version) + return FakeCompiler( + { + None: { + "_MSC_VER": "%02d%02d" % (version.major, version.minor), + "_MSC_FULL_VER": "%02d%02d%05d" + % (version.major, version.minor, version.patch), + "_MT": "1", + }, + "*.cpp": DEFAULT_CXX_97, + } + ) + + +VS_2017u8 = VS("19.15.26726") + +VS_PLATFORM_X86 = {"_M_IX86": 600, "_WIN32": 1} + +VS_PLATFORM_X86_64 = {"_M_X64": 100, "_WIN32": 1, "_WIN64": 1} + +# Despite the 32 in the name, this macro is defined for 32- and 64-bit. +MINGW32 = {"__MINGW32__": True} + +# Note: In reality, the -std=gnu* options are only supported when preceded by +# -Xclang. +CLANG_CL_3_9 = ( + CLANG_BASE("3.9.0") + + VS("18.00.00000") + + DEFAULT_C11 + + SUPPORTS_GNU99 + + SUPPORTS_GNUXX11 + + SUPPORTS_CXX14 +) + {"*.cpp": {"__STDC_VERSION__": False, "__cplusplus": "201103L"}} +CLANG_CL_9_0 = ( + CLANG_BASE("9.0.0") + + VS("18.00.00000") + + DEFAULT_C11 + + SUPPORTS_GNU99 + + SUPPORTS_GNUXX11 + + SUPPORTS_CXX14 + + SUPPORTS_CXX17 +) + {"*.cpp": {"__STDC_VERSION__": False, "__cplusplus": "201103L"}} + +CLANG_CL_PLATFORM_X86 = FakeCompiler( + VS_PLATFORM_X86, GCC_PLATFORM_X86[None], GCC_PLATFORM_LITTLE_ENDIAN +) +CLANG_CL_PLATFORM_X86_64 = FakeCompiler( + VS_PLATFORM_X86_64, GCC_PLATFORM_X86_64[None], GCC_PLATFORM_LITTLE_ENDIAN +) + +LIBRARY_NAME_INFOS = { + "linux-gnu": { + "DLL_PREFIX": "lib", + "DLL_SUFFIX": ".so", + "LIB_PREFIX": "lib", + "LIB_SUFFIX": "a", + "IMPORT_LIB_SUFFIX": "", + "OBJ_SUFFIX": "o", + }, + "darwin11.2.0": { + "DLL_PREFIX": "lib", + "DLL_SUFFIX": ".dylib", + "LIB_PREFIX": "lib", + "LIB_SUFFIX": "a", + "IMPORT_LIB_SUFFIX": "", + "OBJ_SUFFIX": "o", + }, + "mingw32": { + "DLL_PREFIX": "", + "DLL_SUFFIX": ".dll", + "LIB_PREFIX": "lib", + "LIB_SUFFIX": "a", + "IMPORT_LIB_SUFFIX": "a", + "OBJ_SUFFIX": "o", + }, + "windows-msvc": { + "DLL_PREFIX": "", + "DLL_SUFFIX": ".dll", + "LIB_PREFIX": "", + "LIB_SUFFIX": "lib", + "IMPORT_LIB_SUFFIX": "lib", + "OBJ_SUFFIX": "obj", + }, + "windows-gnu": { + "DLL_PREFIX": "", + "DLL_SUFFIX": ".dll", + "LIB_PREFIX": "lib", + "LIB_SUFFIX": "a", + "IMPORT_LIB_SUFFIX": "a", + "OBJ_SUFFIX": "o", + }, + "openbsd6.1": { + "DLL_PREFIX": "lib", + "DLL_SUFFIX": ".so.1.0", + "LIB_PREFIX": "lib", + "LIB_SUFFIX": "a", + "IMPORT_LIB_SUFFIX": "", + "OBJ_SUFFIX": "o", + }, +} + + +class BaseToolchainTest(BaseConfigureTest): + def setUp(self): + super(BaseToolchainTest, self).setUp() + self.out = StringIO() + self.logger = logging.getLogger("BaseToolchainTest") + self.logger.setLevel(logging.ERROR) + self.handler = logging.StreamHandler(self.out) + self.logger.addHandler(self.handler) + + def tearDown(self): + self.logger.removeHandler(self.handler) + del self.handler + del self.out + super(BaseToolchainTest, self).tearDown() + + def do_toolchain_test(self, paths, results, args=[], environ={}): + """Helper to test the toolchain checks from toolchain.configure. + + - `paths` is a dict associating compiler paths to FakeCompiler + definitions from above. + - `results` is a dict associating result variable names from + toolchain.configure (c_compiler, cxx_compiler, host_c_compiler, + host_cxx_compiler) with a result. + The result can either be an error string, or a CompilerResult + corresponding to the object returned by toolchain.configure checks. + When the results for host_c_compiler are identical to c_compiler, + they can be omitted. Likewise for host_cxx_compiler vs. + cxx_compiler. + """ + environ = dict(environ) + if "PATH" not in environ: + environ["PATH"] = os.pathsep.join( + mozpath.abspath(p) for p in ("/bin", "/usr/bin") + ) + + args = args + ["--enable-release", "--disable-bootstrap"] + + sandbox = self.get_sandbox(paths, {}, args, environ, logger=self.logger) + + for var in ( + "c_compiler", + "cxx_compiler", + "host_c_compiler", + "host_cxx_compiler", + ): + if var in results: + result = results[var] + elif var.startswith("host_"): + result = results.get(var[5:], {}) + else: + result = {} + try: + self.out.truncate(0) + self.out.seek(0) + compiler = sandbox._value_for(sandbox[var]) + # Add var on both ends to make it clear which of the + # variables is failing the test when that happens. + self.assertEqual((var, compiler), (var, result)) + except SystemExit: + self.assertEqual((var, result), (var, self.out.getvalue().strip())) + return + + # Normalize the target os to match what we have as keys in + # LIBRARY_NAME_INFOS. + target_os = getattr(self, "TARGET", self.HOST).split("-", 2)[2] + if target_os == "mingw32": + compiler_type = sandbox._value_for(sandbox["c_compiler"]).type + if compiler_type == "clang-cl": + target_os = "windows-msvc" + elif target_os == "linux-gnuabi64": + target_os = "linux-gnu" + + self.do_library_name_info_test(target_os, sandbox) + + # Try again on artifact builds. In that case, we always get library + # name info for msvc on Windows + if target_os == "mingw32": + target_os = "windows-msvc" + + sandbox = self.get_sandbox( + paths, {}, args + ["--enable-artifact-builds"], environ, logger=self.logger + ) + + self.do_library_name_info_test(target_os, sandbox) + + def do_library_name_info_test(self, target_os, sandbox): + library_name_info = LIBRARY_NAME_INFOS[target_os] + for k in ( + "DLL_PREFIX", + "DLL_SUFFIX", + "LIB_PREFIX", + "LIB_SUFFIX", + "IMPORT_LIB_SUFFIX", + "OBJ_SUFFIX", + ): + self.assertEqual( + "%s=%s" % (k, sandbox.get_config(k)), + "%s=%s" % (k, library_name_info[k]), + ) + + +def old_gcc_message(old_ver): + return "Only GCC 8.1 or newer is supported (found version {}).".format(old_ver) + + +class LinuxToolchainTest(BaseToolchainTest): + PATHS = { + "/usr/bin/gcc": DEFAULT_GCC + GCC_PLATFORM_X86_64_LINUX, + "/usr/bin/g++": DEFAULT_GXX + GCC_PLATFORM_X86_64_LINUX, + "/usr/bin/gcc-4.9": GCC_4_9 + GCC_PLATFORM_X86_64_LINUX, + "/usr/bin/g++-4.9": GXX_4_9 + GCC_PLATFORM_X86_64_LINUX, + "/usr/bin/gcc-5": GCC_5 + GCC_PLATFORM_X86_64_LINUX, + "/usr/bin/g++-5": GXX_5 + GCC_PLATFORM_X86_64_LINUX, + "/usr/bin/gcc-6": GCC_6 + GCC_PLATFORM_X86_64_LINUX, + "/usr/bin/g++-6": GXX_6 + GCC_PLATFORM_X86_64_LINUX, + "/usr/bin/gcc-7": GCC_7 + GCC_PLATFORM_X86_64_LINUX, + "/usr/bin/g++-7": GXX_7 + GCC_PLATFORM_X86_64_LINUX, + "/usr/bin/gcc-8": GCC_8 + GCC_PLATFORM_X86_64_LINUX, + "/usr/bin/g++-8": GXX_8 + GCC_PLATFORM_X86_64_LINUX, + "/usr/bin/gcc-10": GCC_10 + GCC_PLATFORM_X86_64_LINUX, + "/usr/bin/g++-10": GXX_10 + GCC_PLATFORM_X86_64_LINUX, + "/usr/bin/clang": DEFAULT_CLANG + CLANG_PLATFORM_X86_64_LINUX, + "/usr/bin/clang++": DEFAULT_CLANGXX + CLANG_PLATFORM_X86_64_LINUX, + "/usr/bin/clang-8.0": CLANG_8_0 + CLANG_PLATFORM_X86_64_LINUX, + "/usr/bin/clang++-8.0": CLANGXX_8_0 + CLANG_PLATFORM_X86_64_LINUX, + "/usr/bin/clang-4.0": CLANG_4_0 + CLANG_PLATFORM_X86_64_LINUX, + "/usr/bin/clang++-4.0": CLANGXX_4_0 + CLANG_PLATFORM_X86_64_LINUX, + "/usr/bin/clang-3.3": CLANG_3_3 + CLANG_PLATFORM_X86_64_LINUX, + "/usr/bin/clang++-3.3": CLANGXX_3_3 + CLANG_PLATFORM_X86_64_LINUX, + } + + GCC_4_7_RESULT = old_gcc_message("4.7.3") + GXX_4_7_RESULT = GCC_4_7_RESULT + GCC_4_9_RESULT = old_gcc_message("4.9.3") + GXX_4_9_RESULT = GCC_4_9_RESULT + GCC_5_RESULT = old_gcc_message("5.2.1") + GXX_5_RESULT = GCC_5_RESULT + GCC_6_RESULT = old_gcc_message("6.4.0") + GXX_6_RESULT = GCC_6_RESULT + GCC_7_RESULT = old_gcc_message("7.3.0") + GXX_7_RESULT = GCC_7_RESULT + GCC_8_RESULT = CompilerResult( + flags=["-std=gnu99"], + version="8.3.0", + type="gcc", + compiler="/usr/bin/gcc-8", + language="C", + ) + GXX_8_RESULT = CompilerResult( + flags=["-std=gnu++17"], + version="8.3.0", + type="gcc", + compiler="/usr/bin/g++-8", + language="C++", + ) + DEFAULT_GCC_RESULT = GCC_8_RESULT + {"compiler": "/usr/bin/gcc"} + DEFAULT_GXX_RESULT = GXX_8_RESULT + {"compiler": "/usr/bin/g++"} + + CLANG_3_3_RESULT = ( + "Only clang/llvm 8.0 or newer is supported (found version 3.3.0)." + ) + CLANGXX_3_3_RESULT = ( + "Only clang/llvm 8.0 or newer is supported (found version 3.3.0)." + ) + CLANG_4_0_RESULT = ( + "Only clang/llvm 8.0 or newer is supported (found version 4.0.2)." + ) + CLANGXX_4_0_RESULT = ( + "Only clang/llvm 8.0 or newer is supported (found version 4.0.2)." + ) + CLANG_8_0_RESULT = CompilerResult( + flags=["-std=gnu99"], + version="8.0.0", + type="clang", + compiler="/usr/bin/clang-8.0", + language="C", + ) + CLANGXX_8_0_RESULT = CompilerResult( + flags=["-std=gnu++17"], + version="8.0.0", + type="clang", + compiler="/usr/bin/clang++-8.0", + language="C++", + ) + DEFAULT_CLANG_RESULT = CLANG_8_0_RESULT + {"compiler": "/usr/bin/clang"} + DEFAULT_CLANGXX_RESULT = CLANGXX_8_0_RESULT + {"compiler": "/usr/bin/clang++"} + + def test_default(self): + # We'll try clang and gcc, and find clang first. + self.do_toolchain_test( + self.PATHS, + { + "c_compiler": self.DEFAULT_CLANG_RESULT, + "cxx_compiler": self.DEFAULT_CLANGXX_RESULT, + }, + ) + + def test_gcc(self): + self.do_toolchain_test( + self.PATHS, + { + "c_compiler": self.DEFAULT_GCC_RESULT, + "cxx_compiler": self.DEFAULT_GXX_RESULT, + }, + environ={"CC": "gcc", "CXX": "g++"}, + ) + + def test_unsupported_gcc(self): + self.do_toolchain_test( + self.PATHS, + {"c_compiler": self.GCC_4_9_RESULT}, + environ={"CC": "gcc-4.9", "CXX": "g++-4.9"}, + ) + + # Maybe this should be reporting the mismatched version instead. + self.do_toolchain_test( + self.PATHS, + { + "c_compiler": self.DEFAULT_GCC_RESULT, + "cxx_compiler": self.GXX_4_9_RESULT, + }, + environ={"CC": "gcc", "CXX": "g++-4.9"}, + ) + + def test_overridden_gcc(self): + self.do_toolchain_test( + self.PATHS, + {"c_compiler": self.GCC_7_RESULT, "cxx_compiler": self.GXX_7_RESULT}, + environ={"CC": "gcc-7", "CXX": "g++-7"}, + ) + + def test_guess_cxx(self): + # When CXX is not set, we guess it from CC. + self.do_toolchain_test( + self.PATHS, + {"c_compiler": self.GCC_7_RESULT, "cxx_compiler": self.GXX_7_RESULT}, + environ={"CC": "gcc-7"}, + ) + + def test_mismatched_gcc(self): + self.do_toolchain_test( + self.PATHS, + { + "c_compiler": self.DEFAULT_GCC_RESULT, + "cxx_compiler": ( + "The target C compiler is version 8.3.0, while the target " + "C++ compiler is version 10.2.1. Need to use the same compiler " + "version." + ), + }, + environ={"CC": "gcc", "CXX": "g++-10"}, + ) + + self.do_toolchain_test( + self.PATHS, + { + "c_compiler": self.DEFAULT_GCC_RESULT, + "cxx_compiler": self.DEFAULT_GXX_RESULT, + "host_c_compiler": self.DEFAULT_GCC_RESULT, + "host_cxx_compiler": ( + "The host C compiler is version 8.3.0, while the host " + "C++ compiler is version 10.2.1. Need to use the same compiler " + "version." + ), + }, + environ={"CC": "gcc", "HOST_CXX": "g++-10"}, + ) + + def test_mismatched_compiler(self): + self.do_toolchain_test( + self.PATHS, + { + "c_compiler": self.DEFAULT_CLANG_RESULT, + "cxx_compiler": ( + "The target C compiler is clang, while the target C++ compiler " + "is gcc. Need to use the same compiler suite." + ), + }, + environ={"CXX": "g++"}, + ) + + self.do_toolchain_test( + self.PATHS, + { + "c_compiler": self.DEFAULT_CLANG_RESULT, + "cxx_compiler": self.DEFAULT_CLANGXX_RESULT, + "host_c_compiler": self.DEFAULT_CLANG_RESULT, + "host_cxx_compiler": ( + "The host C compiler is clang, while the host C++ compiler " + "is gcc. Need to use the same compiler suite." + ), + }, + environ={"HOST_CXX": "g++"}, + ) + + self.do_toolchain_test( + self.PATHS, + { + "c_compiler": "`%s` is not a C compiler." + % mozpath.abspath("/usr/bin/g++") + }, + environ={"CC": "g++"}, + ) + + self.do_toolchain_test( + self.PATHS, + { + "c_compiler": self.DEFAULT_CLANG_RESULT, + "cxx_compiler": "`%s` is not a C++ compiler." + % mozpath.abspath("/usr/bin/clang"), + }, + environ={"CXX": "clang"}, + ) + + def test_clang(self): + # We'll try gcc and clang, but since there is no gcc (gcc-x.y doesn't + # count), find clang. + paths = { + k: v + for k, v in six.iteritems(self.PATHS) + if os.path.basename(k) not in ("gcc", "g++") + } + self.do_toolchain_test( + paths, + { + "c_compiler": self.DEFAULT_CLANG_RESULT, + "cxx_compiler": self.DEFAULT_CLANGXX_RESULT, + }, + ) + + def test_guess_cxx_clang(self): + # When CXX is not set, we guess it from CC. + self.do_toolchain_test( + self.PATHS, + { + "c_compiler": self.CLANG_8_0_RESULT, + "cxx_compiler": self.CLANGXX_8_0_RESULT, + }, + environ={"CC": "clang-8.0"}, + ) + + def test_unsupported_clang(self): + self.do_toolchain_test( + self.PATHS, + { + "c_compiler": self.CLANG_3_3_RESULT, + "cxx_compiler": self.CLANGXX_3_3_RESULT, + }, + environ={"CC": "clang-3.3", "CXX": "clang++-3.3"}, + ) + self.do_toolchain_test( + self.PATHS, + { + "c_compiler": self.CLANG_4_0_RESULT, + "cxx_compiler": self.CLANGXX_4_0_RESULT, + }, + environ={"CC": "clang-4.0", "CXX": "clang++-4.0"}, + ) + + def test_no_supported_compiler(self): + # Even if there are gcc-x.y or clang-x.y compilers available, we + # don't try them. This could be considered something to improve. + paths = { + k: v + for k, v in six.iteritems(self.PATHS) + if os.path.basename(k) not in ("gcc", "g++", "clang", "clang++") + } + self.do_toolchain_test( + paths, {"c_compiler": "Cannot find the target C compiler"} + ) + + def test_absolute_path(self): + paths = dict(self.PATHS) + paths.update( + { + "/opt/clang/bin/clang": paths["/usr/bin/clang"], + "/opt/clang/bin/clang++": paths["/usr/bin/clang++"], + } + ) + result = { + "c_compiler": self.DEFAULT_CLANG_RESULT + + {"compiler": "/opt/clang/bin/clang"}, + "cxx_compiler": self.DEFAULT_CLANGXX_RESULT + + {"compiler": "/opt/clang/bin/clang++"}, + } + self.do_toolchain_test( + paths, + result, + environ={"CC": "/opt/clang/bin/clang", "CXX": "/opt/clang/bin/clang++"}, + ) + # With CXX guess too. + self.do_toolchain_test(paths, result, environ={"CC": "/opt/clang/bin/clang"}) + + def test_atypical_name(self): + paths = dict(self.PATHS) + paths.update( + { + "/usr/bin/afl-clang-fast": paths["/usr/bin/clang"], + "/usr/bin/afl-clang-fast++": paths["/usr/bin/clang++"], + } + ) + self.do_toolchain_test( + paths, + { + "c_compiler": self.DEFAULT_CLANG_RESULT + + {"compiler": "/usr/bin/afl-clang-fast"}, + "cxx_compiler": self.DEFAULT_CLANGXX_RESULT + + {"compiler": "/usr/bin/afl-clang-fast++"}, + }, + environ={"CC": "afl-clang-fast", "CXX": "afl-clang-fast++"}, + ) + + def test_mixed_compilers(self): + self.do_toolchain_test( + self.PATHS, + { + "c_compiler": self.DEFAULT_CLANG_RESULT, + "cxx_compiler": self.DEFAULT_CLANGXX_RESULT, + "host_c_compiler": self.DEFAULT_GCC_RESULT, + "host_cxx_compiler": self.DEFAULT_GXX_RESULT, + }, + environ={"CC": "clang", "HOST_CC": "gcc"}, + ) + + self.do_toolchain_test( + self.PATHS, + { + "c_compiler": self.DEFAULT_CLANG_RESULT, + "cxx_compiler": self.DEFAULT_CLANGXX_RESULT, + "host_c_compiler": self.DEFAULT_GCC_RESULT, + "host_cxx_compiler": self.DEFAULT_GXX_RESULT, + }, + environ={"CC": "clang", "CXX": "clang++", "HOST_CC": "gcc"}, + ) + + +class LinuxSimpleCrossToolchainTest(BaseToolchainTest): + TARGET = "i686-pc-linux-gnu" + PATHS = LinuxToolchainTest.PATHS + DEFAULT_GCC_RESULT = LinuxToolchainTest.DEFAULT_GCC_RESULT + DEFAULT_GXX_RESULT = LinuxToolchainTest.DEFAULT_GXX_RESULT + DEFAULT_CLANG_RESULT = LinuxToolchainTest.DEFAULT_CLANG_RESULT + DEFAULT_CLANGXX_RESULT = LinuxToolchainTest.DEFAULT_CLANGXX_RESULT + + def test_cross_gcc(self): + self.do_toolchain_test( + self.PATHS, + { + "c_compiler": self.DEFAULT_GCC_RESULT + {"flags": ["-m32"]}, + "cxx_compiler": self.DEFAULT_GXX_RESULT + {"flags": ["-m32"]}, + "host_c_compiler": self.DEFAULT_GCC_RESULT, + "host_cxx_compiler": self.DEFAULT_GXX_RESULT, + }, + environ={"CC": "gcc"}, + ) + + def test_cross_clang(self): + self.do_toolchain_test( + self.PATHS, + { + "c_compiler": self.DEFAULT_CLANG_RESULT + {"flags": ["-m32"]}, + "cxx_compiler": self.DEFAULT_CLANGXX_RESULT + {"flags": ["-m32"]}, + "host_c_compiler": self.DEFAULT_CLANG_RESULT, + "host_cxx_compiler": self.DEFAULT_CLANGXX_RESULT, + }, + ) + + +class LinuxX86_64CrossToolchainTest(BaseToolchainTest): + HOST = "i686-pc-linux-gnu" + TARGET = "x86_64-pc-linux-gnu" + PATHS = { + "/usr/bin/gcc": DEFAULT_GCC + GCC_PLATFORM_X86_LINUX, + "/usr/bin/g++": DEFAULT_GXX + GCC_PLATFORM_X86_LINUX, + "/usr/bin/clang": DEFAULT_CLANG + CLANG_PLATFORM_X86_LINUX, + "/usr/bin/clang++": DEFAULT_CLANGXX + CLANG_PLATFORM_X86_LINUX, + } + DEFAULT_GCC_RESULT = LinuxToolchainTest.DEFAULT_GCC_RESULT + DEFAULT_GXX_RESULT = LinuxToolchainTest.DEFAULT_GXX_RESULT + DEFAULT_CLANG_RESULT = LinuxToolchainTest.DEFAULT_CLANG_RESULT + DEFAULT_CLANGXX_RESULT = LinuxToolchainTest.DEFAULT_CLANGXX_RESULT + + def test_cross_gcc(self): + self.do_toolchain_test( + self.PATHS, + { + "c_compiler": self.DEFAULT_GCC_RESULT + {"flags": ["-m64"]}, + "cxx_compiler": self.DEFAULT_GXX_RESULT + {"flags": ["-m64"]}, + "host_c_compiler": self.DEFAULT_GCC_RESULT, + "host_cxx_compiler": self.DEFAULT_GXX_RESULT, + }, + environ={"CC": "gcc"}, + ) + + def test_cross_clang(self): + self.do_toolchain_test( + self.PATHS, + { + "c_compiler": self.DEFAULT_CLANG_RESULT + {"flags": ["-m64"]}, + "cxx_compiler": self.DEFAULT_CLANGXX_RESULT + {"flags": ["-m64"]}, + "host_c_compiler": self.DEFAULT_CLANG_RESULT, + "host_cxx_compiler": self.DEFAULT_CLANGXX_RESULT, + }, + ) + + +def xcrun(stdin, args): + if args == ("--show-sdk-path",): + return ( + 0, + mozpath.join(os.path.abspath(os.path.dirname(__file__)), "macos_fake_sdk"), + "", + ) + raise NotImplementedError() + + +class OSXToolchainTest(BaseToolchainTest): + HOST = "x86_64-apple-darwin11.2.0" + PATHS = { + "/usr/bin/gcc-5": GCC_5 + GCC_PLATFORM_X86_64_OSX, + "/usr/bin/g++-5": GXX_5 + GCC_PLATFORM_X86_64_OSX, + "/usr/bin/gcc-8": GCC_8 + GCC_PLATFORM_X86_64_OSX, + "/usr/bin/g++-8": GXX_8 + GCC_PLATFORM_X86_64_OSX, + "/usr/bin/clang": XCODE_CLANG_8_0 + CLANG_PLATFORM_X86_64_OSX, + "/usr/bin/clang++": XCODE_CLANGXX_8_0 + CLANG_PLATFORM_X86_64_OSX, + "/usr/bin/clang-4.0": XCODE_CLANG_4_0 + CLANG_PLATFORM_X86_64_OSX, + "/usr/bin/clang++-4.0": XCODE_CLANGXX_4_0 + CLANG_PLATFORM_X86_64_OSX, + "/usr/bin/clang-3.3": XCODE_CLANG_3_3 + CLANG_PLATFORM_X86_64_OSX, + "/usr/bin/clang++-3.3": XCODE_CLANGXX_3_3 + CLANG_PLATFORM_X86_64_OSX, + "/usr/bin/xcrun": xcrun, + } + CLANG_3_3_RESULT = ( + "Only clang/llvm 8.0 or newer is supported (found version 4.0.0.or.less)." + ) + CLANGXX_3_3_RESULT = ( + "Only clang/llvm 8.0 or newer is supported (found version 4.0.0.or.less)." + ) + CLANG_4_0_RESULT = ( + "Only clang/llvm 8.0 or newer is supported (found version 4.0.0.or.less)." + ) + CLANGXX_4_0_RESULT = ( + "Only clang/llvm 8.0 or newer is supported (found version 4.0.0.or.less)." + ) + DEFAULT_CLANG_RESULT = CompilerResult( + flags=["-std=gnu99"], + version="8.0.0", + type="clang", + compiler="/usr/bin/clang", + language="C", + ) + DEFAULT_CLANGXX_RESULT = CompilerResult( + flags=["-stdlib=libc++", "-std=gnu++17"], + version="8.0.0", + type="clang", + compiler="/usr/bin/clang++", + language="C++", + ) + GCC_5_RESULT = LinuxToolchainTest.GCC_5_RESULT + GXX_5_RESULT = LinuxToolchainTest.GXX_5_RESULT + GCC_8_RESULT = LinuxToolchainTest.GCC_8_RESULT + GXX_8_RESULT = LinuxToolchainTest.GXX_8_RESULT + SYSROOT_FLAGS = { + "flags": PrependFlags( + [ + "-isysroot", + xcrun("", ("--show-sdk-path",))[1], + "-mmacosx-version-min=10.15", + ] + ) + } + + def test_clang(self): + # We only try clang because gcc is known not to work. + self.do_toolchain_test( + self.PATHS, + { + "c_compiler": self.DEFAULT_CLANG_RESULT + self.SYSROOT_FLAGS, + "cxx_compiler": self.DEFAULT_CLANGXX_RESULT + self.SYSROOT_FLAGS, + }, + ) + + def test_not_gcc(self): + # We won't pick GCC if it's the only thing available. + paths = { + k: v + for k, v in six.iteritems(self.PATHS) + if os.path.basename(k) not in ("clang", "clang++") + } + self.do_toolchain_test( + paths, {"c_compiler": "Cannot find the target C compiler"} + ) + + def test_unsupported_clang(self): + self.do_toolchain_test( + self.PATHS, + { + "c_compiler": self.CLANG_3_3_RESULT, + "cxx_compiler": self.CLANGXX_3_3_RESULT, + }, + environ={"CC": "clang-3.3", "CXX": "clang++-3.3"}, + ) + # When targeting mac, we require at least version 5. + self.do_toolchain_test( + self.PATHS, + { + "c_compiler": self.CLANG_4_0_RESULT, + "cxx_compiler": self.CLANGXX_4_0_RESULT, + }, + environ={"CC": "clang-4.0", "CXX": "clang++-4.0"}, + ) + + def test_forced_gcc(self): + # GCC can still be forced if the user really wants it. + self.do_toolchain_test( + self.PATHS, + { + "c_compiler": self.GCC_8_RESULT + self.SYSROOT_FLAGS, + "cxx_compiler": self.GXX_8_RESULT + self.SYSROOT_FLAGS, + }, + environ={"CC": "gcc-8", "CXX": "g++-8"}, + ) + + def test_forced_unsupported_gcc(self): + self.do_toolchain_test( + self.PATHS, + {"c_compiler": self.GCC_5_RESULT}, + environ={"CC": "gcc-5", "CXX": "g++-5"}, + ) + + +class MingwToolchainTest(BaseToolchainTest): + HOST = "i686-pc-mingw32" + + # For the purpose of this test, it doesn't matter that the paths are not + # real Windows paths. + PATHS = { + "/usr/bin/cl": VS_2017u8 + VS_PLATFORM_X86, + "/usr/bin/clang-cl-3.9": CLANG_CL_3_9 + CLANG_CL_PLATFORM_X86, + "/usr/bin/clang-cl": CLANG_CL_9_0 + CLANG_CL_PLATFORM_X86, + "/usr/bin/gcc": DEFAULT_GCC + GCC_PLATFORM_X86_WIN + MINGW32, + "/usr/bin/g++": DEFAULT_GXX + GCC_PLATFORM_X86_WIN + MINGW32, + "/usr/bin/gcc-4.9": GCC_4_9 + GCC_PLATFORM_X86_WIN + MINGW32, + "/usr/bin/g++-4.9": GXX_4_9 + GCC_PLATFORM_X86_WIN + MINGW32, + "/usr/bin/gcc-5": GCC_5 + GCC_PLATFORM_X86_WIN + MINGW32, + "/usr/bin/g++-5": GXX_5 + GCC_PLATFORM_X86_WIN + MINGW32, + "/usr/bin/gcc-6": GCC_6 + GCC_PLATFORM_X86_WIN + MINGW32, + "/usr/bin/g++-6": GXX_6 + GCC_PLATFORM_X86_WIN + MINGW32, + "/usr/bin/gcc-7": GCC_7 + GCC_PLATFORM_X86_WIN + MINGW32, + "/usr/bin/g++-7": GXX_7 + GCC_PLATFORM_X86_WIN + MINGW32, + "/usr/bin/clang": DEFAULT_CLANG + CLANG_PLATFORM_X86_WIN, + "/usr/bin/clang++": DEFAULT_CLANGXX + CLANG_PLATFORM_X86_WIN, + "/usr/bin/clang-8.0": CLANG_8_0 + CLANG_PLATFORM_X86_WIN, + "/usr/bin/clang++-8.0": CLANGXX_8_0 + CLANG_PLATFORM_X86_WIN, + "/usr/bin/clang-4.0": CLANG_4_0 + CLANG_PLATFORM_X86_WIN, + "/usr/bin/clang++-4.0": CLANGXX_4_0 + CLANG_PLATFORM_X86_WIN, + "/usr/bin/clang-3.3": CLANG_3_3 + CLANG_PLATFORM_X86_WIN, + "/usr/bin/clang++-3.3": CLANGXX_3_3 + CLANG_PLATFORM_X86_WIN, + } + + CLANG_CL_3_9_RESULT = ( + "Only clang-cl 9.0 or newer is supported (found version 3.9.0)" + ) + CLANG_CL_9_0_RESULT = CompilerResult( + version="9.0.0", + flags=["-Xclang", "-std=gnu99"], + type="clang-cl", + compiler="/usr/bin/clang-cl", + language="C", + ) + CLANGXX_CL_3_9_RESULT = ( + "Only clang-cl 9.0 or newer is supported (found version 3.9.0)" + ) + CLANGXX_CL_9_0_RESULT = CompilerResult( + version="9.0.0", + flags=["-Xclang", "-std=c++17"], + type="clang-cl", + compiler="/usr/bin/clang-cl", + language="C++", + ) + CLANG_3_3_RESULT = LinuxToolchainTest.CLANG_3_3_RESULT + CLANGXX_3_3_RESULT = LinuxToolchainTest.CLANGXX_3_3_RESULT + CLANG_4_0_RESULT = LinuxToolchainTest.CLANG_4_0_RESULT + CLANGXX_4_0_RESULT = LinuxToolchainTest.CLANGXX_4_0_RESULT + DEFAULT_CLANG_RESULT = LinuxToolchainTest.DEFAULT_CLANG_RESULT + DEFAULT_CLANGXX_RESULT = LinuxToolchainTest.DEFAULT_CLANGXX_RESULT + + def test_unsupported_msvc(self): + self.do_toolchain_test( + self.PATHS, + {"c_compiler": "Unknown compiler or compiler not supported."}, + environ={"CC": "/usr/bin/cl"}, + ) + + def test_unsupported_clang_cl(self): + self.do_toolchain_test( + self.PATHS, + {"c_compiler": self.CLANG_CL_3_9_RESULT}, + environ={"CC": "/usr/bin/clang-cl-3.9"}, + ) + + def test_clang_cl(self): + self.do_toolchain_test( + self.PATHS, + { + "c_compiler": self.CLANG_CL_9_0_RESULT, + "cxx_compiler": self.CLANGXX_CL_9_0_RESULT, + }, + ) + + def test_gcc(self): + # GCC is unsupported, if you try it should find clang. + paths = { + k: v + for k, v in six.iteritems(self.PATHS) + if os.path.basename(k) != "clang-cl" + } + self.do_toolchain_test( + paths, + { + "c_compiler": self.DEFAULT_CLANG_RESULT, + "cxx_compiler": self.DEFAULT_CLANGXX_RESULT, + }, + ) + + # This test is not perfect, as the GCC version needs to be updated when we + # bump the minimum GCC version, but the idea is that even supported GCC + # on other platforms should not be supported on Windows. + def test_overridden_supported_elsewhere_gcc(self): + self.do_toolchain_test( + self.PATHS, + {"c_compiler": "Unknown compiler or compiler not supported."}, + environ={"CC": "gcc-7", "CXX": "g++-7"}, + ) + + def test_overridden_unsupported_gcc(self): + self.do_toolchain_test( + self.PATHS, + {"c_compiler": "Unknown compiler or compiler not supported."}, + environ={"CC": "gcc-5", "CXX": "g++-5"}, + ) + + def test_clang(self): + # We'll pick clang if nothing else is found. + paths = { + k: v + for k, v in six.iteritems(self.PATHS) + if os.path.basename(k) not in ("clang-cl", "gcc") + } + self.do_toolchain_test( + paths, + { + "c_compiler": self.DEFAULT_CLANG_RESULT, + "cxx_compiler": self.DEFAULT_CLANGXX_RESULT, + }, + ) + + def test_overridden_unsupported_clang(self): + # clang 3.3 C compiler is perfectly fine, but we need more for C++. + self.do_toolchain_test( + self.PATHS, + { + "c_compiler": self.CLANG_3_3_RESULT, + "cxx_compiler": self.CLANGXX_3_3_RESULT, + }, + environ={"CC": "clang-3.3", "CXX": "clang++-3.3"}, + ) + + +class Mingw64ToolchainTest(MingwToolchainTest): + HOST = "x86_64-pc-mingw32" + + # For the purpose of this test, it doesn't matter that the paths are not + # real Windows paths. + PATHS = { + "/usr/bin/cl": VS_2017u8 + VS_PLATFORM_X86_64, + "/usr/bin/clang-cl": CLANG_CL_9_0 + CLANG_CL_PLATFORM_X86_64, + "/usr/bin/clang-cl-3.9": CLANG_CL_3_9 + CLANG_CL_PLATFORM_X86_64, + "/usr/bin/gcc": DEFAULT_GCC + GCC_PLATFORM_X86_64_WIN + MINGW32, + "/usr/bin/g++": DEFAULT_GXX + GCC_PLATFORM_X86_64_WIN + MINGW32, + "/usr/bin/gcc-4.9": GCC_4_9 + GCC_PLATFORM_X86_64_WIN + MINGW32, + "/usr/bin/g++-4.9": GXX_4_9 + GCC_PLATFORM_X86_64_WIN + MINGW32, + "/usr/bin/gcc-5": GCC_5 + GCC_PLATFORM_X86_64_WIN + MINGW32, + "/usr/bin/g++-5": GXX_5 + GCC_PLATFORM_X86_64_WIN + MINGW32, + "/usr/bin/gcc-6": GCC_6 + GCC_PLATFORM_X86_64_WIN + MINGW32, + "/usr/bin/g++-6": GXX_6 + GCC_PLATFORM_X86_64_WIN + MINGW32, + "/usr/bin/gcc-7": GCC_7 + GCC_PLATFORM_X86_64_WIN + MINGW32, + "/usr/bin/g++-7": GXX_7 + GCC_PLATFORM_X86_64_WIN + MINGW32, + "/usr/bin/clang": DEFAULT_CLANG + CLANG_PLATFORM_X86_64_WIN, + "/usr/bin/clang++": DEFAULT_CLANGXX + CLANG_PLATFORM_X86_64_WIN, + "/usr/bin/clang-8.0": CLANG_8_0 + CLANG_PLATFORM_X86_64_WIN, + "/usr/bin/clang++-8.0": CLANGXX_8_0 + CLANG_PLATFORM_X86_64_WIN, + "/usr/bin/clang-4.0": CLANG_4_0 + CLANG_PLATFORM_X86_64_WIN, + "/usr/bin/clang++-4.0": CLANGXX_4_0 + CLANG_PLATFORM_X86_64_WIN, + "/usr/bin/clang-3.3": CLANG_3_3 + CLANG_PLATFORM_X86_64_WIN, + "/usr/bin/clang++-3.3": CLANGXX_3_3 + CLANG_PLATFORM_X86_64_WIN, + } + + +class WindowsToolchainTest(BaseToolchainTest): + HOST = "i686-pc-windows-msvc" + + PATHS = MingwToolchainTest.PATHS + + def test_unsupported_msvc(self): + self.do_toolchain_test( + self.PATHS, + {"c_compiler": "Unknown compiler or compiler not supported."}, + environ={"CC": "/usr/bin/cl"}, + ) + + def test_unsupported_clang_cl(self): + self.do_toolchain_test( + self.PATHS, + {"c_compiler": MingwToolchainTest.CLANG_CL_3_9_RESULT}, + environ={"CC": "/usr/bin/clang-cl-3.9"}, + ) + + def test_clang_cl(self): + self.do_toolchain_test( + self.PATHS, + { + "c_compiler": MingwToolchainTest.CLANG_CL_9_0_RESULT, + "cxx_compiler": MingwToolchainTest.CLANGXX_CL_9_0_RESULT, + }, + ) + + def test_unsupported_gcc(self): + paths = { + k: v + for k, v in six.iteritems(self.PATHS) + if os.path.basename(k) != "clang-cl" + } + self.do_toolchain_test( + paths, + {"c_compiler": "Cannot find the target C compiler"}, + ) + + def test_overridden_unsupported_gcc(self): + self.do_toolchain_test( + self.PATHS, + {"c_compiler": "Unknown compiler or compiler not supported."}, + environ={"CC": "gcc-5", "CXX": "g++-5"}, + ) + + def test_unsupported_clang(self): + paths = { + k: v + for k, v in six.iteritems(self.PATHS) + if os.path.basename(k) not in ("clang-cl", "gcc") + } + self.do_toolchain_test( + paths, + {"c_compiler": "Cannot find the target C compiler"}, + ) + + def test_overridden_unsupported_clang(self): + self.do_toolchain_test( + self.PATHS, + {"c_compiler": "Unknown compiler or compiler not supported."}, + environ={"CC": "clang-3.3", "CXX": "clang++-3.3"}, + ) + + +class Windows64ToolchainTest(WindowsToolchainTest): + HOST = "x86_64-pc-windows-msvc" + + PATHS = Mingw64ToolchainTest.PATHS + + +class WindowsGnuToolchainTest(BaseToolchainTest): + HOST = "i686-pc-windows-gnu" + + PATHS = MingwToolchainTest.PATHS + + def test_unsupported_msvc(self): + self.do_toolchain_test( + self.PATHS, + {"c_compiler": "Unknown compiler or compiler not supported."}, + environ={"CC": "/usr/bin/cl"}, + ) + + def test_unsupported_clang_cl(self): + paths = { + k: v + for k, v in six.iteritems(self.PATHS) + if os.path.basename(k) == "clang-cl" + } + self.do_toolchain_test( + paths, + {"c_compiler": "Cannot find the target C compiler"}, + ) + + def test_overridden_unsupported_clang_cl(self): + self.do_toolchain_test( + self.PATHS, + {"c_compiler": "Unknown compiler or compiler not supported."}, + environ={"CC": "clang-cl", "CXX": "clang-cl"}, + ) + + def test_unsupported_gcc(self): + paths = { + k: v for k, v in six.iteritems(self.PATHS) if os.path.basename(k) == "gcc" + } + self.do_toolchain_test( + paths, + {"c_compiler": "Cannot find the target C compiler"}, + ) + + def test_overridden_unsupported_gcc(self): + self.do_toolchain_test( + self.PATHS, + {"c_compiler": "Unknown compiler or compiler not supported."}, + environ={"CC": "gcc-5", "CXX": "g++-5"}, + ) + + def test_clang(self): + paths = { + k: v + for k, v in six.iteritems(self.PATHS) + if os.path.basename(k) not in ("clang-cl", "gcc") + } + self.do_toolchain_test( + paths, + { + "c_compiler": MingwToolchainTest.DEFAULT_CLANG_RESULT, + "cxx_compiler": MingwToolchainTest.DEFAULT_CLANGXX_RESULT, + }, + ) + + def test_overridden_unsupported_clang(self): + self.do_toolchain_test( + self.PATHS, + { + "c_compiler": MingwToolchainTest.CLANG_3_3_RESULT, + "cxx_compiler": MingwToolchainTest.CLANGXX_3_3_RESULT, + }, + environ={"CC": "clang-3.3", "CXX": "clang++-3.3"}, + ) + + +class WindowsGnu64ToolchainTest(WindowsGnuToolchainTest): + HOST = "x86_64-pc-windows-gnu" + + PATHS = Mingw64ToolchainTest.PATHS + + +class LinuxCrossCompileToolchainTest(BaseToolchainTest): + TARGET = "arm-unknown-linux-gnu" + PATHS = { + "/usr/bin/arm-linux-gnu-gcc-4.9": GCC_4_9 + GCC_PLATFORM_ARM_LINUX, + "/usr/bin/arm-linux-gnu-g++-4.9": GXX_4_9 + GCC_PLATFORM_ARM_LINUX, + "/usr/bin/arm-linux-gnu-gcc-5": GCC_5 + GCC_PLATFORM_ARM_LINUX, + "/usr/bin/arm-linux-gnu-g++-5": GXX_5 + GCC_PLATFORM_ARM_LINUX, + "/usr/bin/arm-linux-gnu-gcc": DEFAULT_GCC + GCC_PLATFORM_ARM_LINUX, + "/usr/bin/arm-linux-gnu-g++": DEFAULT_GXX + GCC_PLATFORM_ARM_LINUX, + "/usr/bin/arm-linux-gnu-gcc-7": GCC_7 + GCC_PLATFORM_ARM_LINUX, + "/usr/bin/arm-linux-gnu-g++-7": GXX_7 + GCC_PLATFORM_ARM_LINUX, + } + PATHS.update(LinuxToolchainTest.PATHS) + ARM_GCC_4_9_RESULT = LinuxToolchainTest.GCC_4_9_RESULT + ARM_GXX_4_9_RESULT = LinuxToolchainTest.GXX_4_9_RESULT + ARM_GCC_5_RESULT = LinuxToolchainTest.GCC_5_RESULT + ARM_GXX_5_RESULT = LinuxToolchainTest.GXX_5_RESULT + ARM_DEFAULT_GCC_RESULT = LinuxToolchainTest.DEFAULT_GCC_RESULT + { + "compiler": "/usr/bin/arm-linux-gnu-gcc" + } + ARM_DEFAULT_GXX_RESULT = LinuxToolchainTest.DEFAULT_GXX_RESULT + { + "compiler": "/usr/bin/arm-linux-gnu-g++" + } + ARM_GCC_7_RESULT = LinuxToolchainTest.GCC_7_RESULT + ARM_GXX_7_RESULT = LinuxToolchainTest.GXX_7_RESULT + DEFAULT_CLANG_RESULT = LinuxToolchainTest.DEFAULT_CLANG_RESULT + DEFAULT_CLANGXX_RESULT = LinuxToolchainTest.DEFAULT_CLANGXX_RESULT + DEFAULT_GCC_RESULT = LinuxToolchainTest.DEFAULT_GCC_RESULT + DEFAULT_GXX_RESULT = LinuxToolchainTest.DEFAULT_GXX_RESULT + + little_endian = FakeCompiler(GCC_PLATFORM_LINUX, GCC_PLATFORM_LITTLE_ENDIAN) + big_endian = FakeCompiler(GCC_PLATFORM_LINUX, GCC_PLATFORM_BIG_ENDIAN) + + PLATFORMS = { + "i686-pc-linux-gnu": GCC_PLATFORM_X86_LINUX, + "x86_64-pc-linux-gnu": GCC_PLATFORM_X86_64_LINUX, + "arm-unknown-linux-gnu": GCC_PLATFORM_ARM_LINUX, + "aarch64-unknown-linux-gnu": little_endian + {"__aarch64__": 1}, + "ia64-unknown-linux-gnu": little_endian + {"__ia64__": 1}, + "s390x-unknown-linux-gnu": big_endian + {"__s390x__": 1, "__s390__": 1}, + "s390-unknown-linux-gnu": big_endian + {"__s390__": 1}, + "powerpc64-unknown-linux-gnu": big_endian + + { + None: {"__powerpc64__": 1, "__powerpc__": 1}, + "-m32": {"__powerpc64__": False}, + }, + "powerpc-unknown-linux-gnu": big_endian + + {None: {"__powerpc__": 1}, "-m64": {"__powerpc64__": 1}}, + "alpha-unknown-linux-gnu": little_endian + {"__alpha__": 1}, + "hppa-unknown-linux-gnu": big_endian + {"__hppa__": 1}, + "sparc64-unknown-linux-gnu": big_endian + + {None: {"__arch64__": 1, "__sparc__": 1}, "-m32": {"__arch64__": False}}, + "sparc-unknown-linux-gnu": big_endian + + {None: {"__sparc__": 1}, "-m64": {"__arch64__": 1}}, + "m68k-unknown-linux-gnu": big_endian + {"__m68k__": 1}, + "mips64-unknown-linux-gnuabi64": big_endian + {"__mips64": 1, "__mips__": 1}, + "mips-unknown-linux-gnu": big_endian + {"__mips__": 1}, + "riscv64-unknown-linux-gnu": little_endian + {"__riscv": 1, "__riscv_xlen": 64}, + "sh4-unknown-linux-gnu": little_endian + {"__sh__": 1}, + } + + PLATFORMS["powerpc64le-unknown-linux-gnu"] = ( + PLATFORMS["powerpc64-unknown-linux-gnu"] + GCC_PLATFORM_LITTLE_ENDIAN + ) + PLATFORMS["mips64el-unknown-linux-gnuabi64"] = ( + PLATFORMS["mips64-unknown-linux-gnuabi64"] + GCC_PLATFORM_LITTLE_ENDIAN + ) + PLATFORMS["mipsel-unknown-linux-gnu"] = ( + PLATFORMS["mips-unknown-linux-gnu"] + GCC_PLATFORM_LITTLE_ENDIAN + ) + + def do_test_cross_gcc_32_64(self, host, target): + self.HOST = host + self.TARGET = target + paths = { + "/usr/bin/gcc": DEFAULT_GCC + self.PLATFORMS[host], + "/usr/bin/g++": DEFAULT_GXX + self.PLATFORMS[host], + } + cross_flags = {"flags": ["-m64" if "64" in target else "-m32"]} + self.do_toolchain_test( + paths, + { + "c_compiler": self.DEFAULT_GCC_RESULT + cross_flags, + "cxx_compiler": self.DEFAULT_GXX_RESULT + cross_flags, + "host_c_compiler": self.DEFAULT_GCC_RESULT, + "host_cxx_compiler": self.DEFAULT_GXX_RESULT, + }, + ) + self.HOST = LinuxCrossCompileToolchainTest.HOST + self.TARGET = LinuxCrossCompileToolchainTest.TARGET + + def test_cross_x86_x64(self): + self.do_test_cross_gcc_32_64("i686-pc-linux-gnu", "x86_64-pc-linux-gnu") + self.do_test_cross_gcc_32_64("x86_64-pc-linux-gnu", "i686-pc-linux-gnu") + + def test_cross_sparc_sparc64(self): + self.do_test_cross_gcc_32_64( + "sparc-unknown-linux-gnu", "sparc64-unknown-linux-gnu" + ) + self.do_test_cross_gcc_32_64( + "sparc64-unknown-linux-gnu", "sparc-unknown-linux-gnu" + ) + + def test_cross_ppc_ppc64(self): + self.do_test_cross_gcc_32_64( + "powerpc-unknown-linux-gnu", "powerpc64-unknown-linux-gnu" + ) + self.do_test_cross_gcc_32_64( + "powerpc64-unknown-linux-gnu", "powerpc-unknown-linux-gnu" + ) + + def do_test_cross_gcc(self, host, target): + self.HOST = host + self.TARGET = target + host_cpu = host.split("-")[0] + cpu, manufacturer, os = target.split("-", 2) + toolchain_prefix = "/usr/bin/%s-%s" % (cpu, os) + paths = { + "/usr/bin/gcc": DEFAULT_GCC + self.PLATFORMS[host], + "/usr/bin/g++": DEFAULT_GXX + self.PLATFORMS[host], + } + self.do_toolchain_test( + paths, + { + "c_compiler": ( + "Target C compiler target CPU (%s) " + "does not match --target CPU (%s)" % (host_cpu, cpu) + ) + }, + ) + + paths.update( + { + "%s-gcc" % toolchain_prefix: DEFAULT_GCC + self.PLATFORMS[target], + "%s-g++" % toolchain_prefix: DEFAULT_GXX + self.PLATFORMS[target], + } + ) + self.do_toolchain_test( + paths, + { + "c_compiler": self.DEFAULT_GCC_RESULT + + {"compiler": "%s-gcc" % toolchain_prefix}, + "cxx_compiler": self.DEFAULT_GXX_RESULT + + {"compiler": "%s-g++" % toolchain_prefix}, + "host_c_compiler": self.DEFAULT_GCC_RESULT, + "host_cxx_compiler": self.DEFAULT_GXX_RESULT, + }, + ) + self.HOST = LinuxCrossCompileToolchainTest.HOST + self.TARGET = LinuxCrossCompileToolchainTest.TARGET + + def test_cross_gcc_misc(self): + for target in self.PLATFORMS: + if not target.endswith("-pc-linux-gnu"): + self.do_test_cross_gcc("x86_64-pc-linux-gnu", target) + + def test_cannot_cross(self): + self.TARGET = "mipsel-unknown-linux-gnu" + + paths = { + "/usr/bin/gcc": DEFAULT_GCC + self.PLATFORMS["mips-unknown-linux-gnu"], + "/usr/bin/g++": DEFAULT_GXX + self.PLATFORMS["mips-unknown-linux-gnu"], + } + self.do_toolchain_test( + paths, + { + "c_compiler": ( + "Target C compiler target endianness (big) " + "does not match --target endianness (little)" + ) + }, + ) + self.TARGET = LinuxCrossCompileToolchainTest.TARGET + + def test_overridden_cross_gcc(self): + self.do_toolchain_test( + self.PATHS, + { + "c_compiler": self.ARM_GCC_7_RESULT, + "cxx_compiler": self.ARM_GXX_7_RESULT, + "host_c_compiler": self.DEFAULT_GCC_RESULT, + "host_cxx_compiler": self.DEFAULT_GXX_RESULT, + }, + environ={"CC": "arm-linux-gnu-gcc-7", "CXX": "arm-linux-gnu-g++-7"}, + ) + + def test_overridden_unsupported_cross_gcc(self): + self.do_toolchain_test( + self.PATHS, + {"c_compiler": self.ARM_GCC_4_9_RESULT}, + environ={"CC": "arm-linux-gnu-gcc-4.9", "CXX": "arm-linux-gnu-g++-4.9"}, + ) + + def test_guess_cross_cxx(self): + # When CXX is not set, we guess it from CC. + self.do_toolchain_test( + self.PATHS, + { + "c_compiler": self.ARM_GCC_7_RESULT, + "cxx_compiler": self.ARM_GXX_7_RESULT, + "host_c_compiler": self.DEFAULT_GCC_RESULT, + "host_cxx_compiler": self.DEFAULT_GXX_RESULT, + }, + environ={"CC": "arm-linux-gnu-gcc-7"}, + ) + + self.do_toolchain_test( + self.PATHS, + { + "c_compiler": self.ARM_DEFAULT_GCC_RESULT, + "cxx_compiler": self.ARM_DEFAULT_GXX_RESULT, + "host_c_compiler": self.DEFAULT_CLANG_RESULT, + "host_cxx_compiler": self.DEFAULT_CLANGXX_RESULT, + }, + environ={"CC": "arm-linux-gnu-gcc", "HOST_CC": "clang"}, + ) + + self.do_toolchain_test( + self.PATHS, + { + "c_compiler": self.ARM_DEFAULT_GCC_RESULT, + "cxx_compiler": self.ARM_DEFAULT_GXX_RESULT, + "host_c_compiler": self.DEFAULT_CLANG_RESULT, + "host_cxx_compiler": self.DEFAULT_CLANGXX_RESULT, + }, + environ={ + "CC": "arm-linux-gnu-gcc", + "CXX": "arm-linux-gnu-g++", + "HOST_CC": "clang", + }, + ) + + def test_cross_clang(self): + cross_clang_result = self.DEFAULT_CLANG_RESULT + { + "flags": ["--target=arm-linux-gnu"] + } + cross_clangxx_result = self.DEFAULT_CLANGXX_RESULT + { + "flags": ["--target=arm-linux-gnu"] + } + self.do_toolchain_test( + self.PATHS, + { + "c_compiler": cross_clang_result, + "cxx_compiler": cross_clangxx_result, + "host_c_compiler": self.DEFAULT_CLANG_RESULT, + "host_cxx_compiler": self.DEFAULT_CLANGXX_RESULT, + }, + environ={"CC": "clang", "HOST_CC": "clang"}, + ) + + self.do_toolchain_test( + self.PATHS, + { + "c_compiler": cross_clang_result, + "cxx_compiler": cross_clangxx_result, + "host_c_compiler": self.DEFAULT_CLANG_RESULT, + "host_cxx_compiler": self.DEFAULT_CLANGXX_RESULT, + }, + environ={"CC": "clang"}, + ) + + def test_cross_atypical_clang(self): + paths = dict(self.PATHS) + paths.update( + { + "/usr/bin/afl-clang-fast": paths["/usr/bin/clang"], + "/usr/bin/afl-clang-fast++": paths["/usr/bin/clang++"], + } + ) + afl_clang_result = self.DEFAULT_CLANG_RESULT + { + "compiler": "/usr/bin/afl-clang-fast" + } + afl_clangxx_result = self.DEFAULT_CLANGXX_RESULT + { + "compiler": "/usr/bin/afl-clang-fast++" + } + self.do_toolchain_test( + paths, + { + "c_compiler": afl_clang_result + {"flags": ["--target=arm-linux-gnu"]}, + "cxx_compiler": afl_clangxx_result + + {"flags": ["--target=arm-linux-gnu"]}, + "host_c_compiler": afl_clang_result, + "host_cxx_compiler": afl_clangxx_result, + }, + environ={"CC": "afl-clang-fast", "CXX": "afl-clang-fast++"}, + ) + + +class OSXCrossToolchainTest(BaseToolchainTest): + TARGET = "i686-apple-darwin11.2.0" + PATHS = dict(LinuxToolchainTest.PATHS) + PATHS.update( + { + "/usr/bin/clang": CLANG_8_0 + CLANG_PLATFORM_X86_64_LINUX, + "/usr/bin/clang++": CLANGXX_8_0 + CLANG_PLATFORM_X86_64_LINUX, + } + ) + DEFAULT_CLANG_RESULT = CompilerResult( + flags=["-std=gnu99"], + version="8.0.0", + type="clang", + compiler="/usr/bin/clang", + language="C", + ) + DEFAULT_CLANGXX_RESULT = CompilerResult( + flags=["-std=gnu++17"], + version="8.0.0", + type="clang", + compiler="/usr/bin/clang++", + language="C++", + ) + + def test_osx_cross(self): + self.do_toolchain_test( + self.PATHS, + { + "c_compiler": self.DEFAULT_CLANG_RESULT + + OSXToolchainTest.SYSROOT_FLAGS + + {"flags": ["--target=i686-apple-darwin11.2.0"]}, + "cxx_compiler": self.DEFAULT_CLANGXX_RESULT + + {"flags": PrependFlags(["-stdlib=libc++"])} + + OSXToolchainTest.SYSROOT_FLAGS + + {"flags": ["--target=i686-apple-darwin11.2.0"]}, + "host_c_compiler": self.DEFAULT_CLANG_RESULT, + "host_cxx_compiler": self.DEFAULT_CLANGXX_RESULT, + }, + environ={"CC": "clang"}, + args=["--with-macos-sdk=%s" % OSXToolchainTest.SYSROOT_FLAGS["flags"][1]], + ) + + def test_cannot_osx_cross(self): + self.do_toolchain_test( + self.PATHS, + { + "c_compiler": "Target C compiler target kernel (Linux) does not " + "match --target kernel (Darwin)" + }, + environ={"CC": "gcc"}, + args=["--with-macos-sdk=%s" % OSXToolchainTest.SYSROOT_FLAGS["flags"][1]], + ) + + +class WindowsCrossToolchainTest(BaseToolchainTest): + TARGET = "x86_64-pc-windows-msvc" + DEFAULT_CLANG_RESULT = LinuxToolchainTest.DEFAULT_CLANG_RESULT + DEFAULT_CLANGXX_RESULT = LinuxToolchainTest.DEFAULT_CLANGXX_RESULT + + def test_clang_cl_cross(self): + paths = {"/usr/bin/clang-cl": CLANG_CL_9_0 + CLANG_CL_PLATFORM_X86_64} + paths.update(LinuxToolchainTest.PATHS) + self.do_toolchain_test( + paths, + { + "c_compiler": MingwToolchainTest.CLANG_CL_9_0_RESULT, + "cxx_compiler": MingwToolchainTest.CLANGXX_CL_9_0_RESULT, + "host_c_compiler": self.DEFAULT_CLANG_RESULT, + "host_cxx_compiler": self.DEFAULT_CLANGXX_RESULT, + }, + ) + + +class OpenBSDToolchainTest(BaseToolchainTest): + HOST = "x86_64-unknown-openbsd6.1" + TARGET = "x86_64-unknown-openbsd6.1" + PATHS = { + "/usr/bin/gcc": DEFAULT_GCC + GCC_PLATFORM_X86_64 + GCC_PLATFORM_OPENBSD, + "/usr/bin/g++": DEFAULT_GXX + GCC_PLATFORM_X86_64 + GCC_PLATFORM_OPENBSD, + } + DEFAULT_GCC_RESULT = LinuxToolchainTest.DEFAULT_GCC_RESULT + DEFAULT_GXX_RESULT = LinuxToolchainTest.DEFAULT_GXX_RESULT + + def test_gcc(self): + self.do_toolchain_test( + self.PATHS, + { + "c_compiler": self.DEFAULT_GCC_RESULT, + "cxx_compiler": self.DEFAULT_GXX_RESULT, + }, + ) + + +@memoize +def gen_invoke_cargo(version, rustup_wrapper=False): + def invoke_cargo(stdin, args): + args = tuple(args) + if not rustup_wrapper and args == ("+stable",): + return (101, "", "we are the real thing") + if args == ("--version", "--verbose"): + return 0, "cargo %s\nrelease: %s" % (version, version), "" + raise NotImplementedError("unsupported arguments") + + return invoke_cargo + + +@memoize +def gen_invoke_rustc(version, rustup_wrapper=False): + def invoke_rustc(stdin, args): + args = tuple(args) + # TODO: we don't have enough machinery set up to test the `rustup which` + # fallback yet. + if not rustup_wrapper and args == ("+stable",): + return (1, "", "error: couldn't read +stable: No such file or directory") + if args == ("--version", "--verbose"): + return ( + 0, + "rustc %s\nrelease: %s\nhost: x86_64-unknown-linux-gnu" + % (version, version), + "", + ) + if args == ("--print", "target-list"): + # Raw list returned by rustc version 1.70 + rust_targets = [ + "aarch64-apple-darwin", + "aarch64-apple-ios", + "aarch64-apple-ios-macabi", + "aarch64-apple-ios-sim", + "aarch64-apple-tvos", + "aarch64-apple-watchos-sim", + "aarch64-fuchsia", + "aarch64-kmc-solid_asp3", + "aarch64-linux-android", + "aarch64-nintendo-switch-freestanding", + "aarch64-pc-windows-gnullvm", + "aarch64-pc-windows-msvc", + "aarch64-unknown-freebsd", + "aarch64-unknown-fuchsia", + "aarch64-unknown-hermit", + "aarch64-unknown-linux-gnu", + "aarch64-unknown-linux-gnu_ilp32", + "aarch64-unknown-linux-musl", + "aarch64-unknown-linux-ohos", + "aarch64-unknown-netbsd", + "aarch64-unknown-none", + "aarch64-unknown-none-softfloat", + "aarch64-unknown-nto-qnx710", + "aarch64-unknown-openbsd", + "aarch64-unknown-redox", + "aarch64-unknown-uefi", + "aarch64-uwp-windows-msvc", + "aarch64-wrs-vxworks", + "aarch64_be-unknown-linux-gnu", + "aarch64_be-unknown-linux-gnu_ilp32", + "arm-linux-androideabi", + "arm-unknown-linux-gnueabi", + "arm-unknown-linux-gnueabihf", + "arm-unknown-linux-musleabi", + "arm-unknown-linux-musleabihf", + "arm64_32-apple-watchos", + "armeb-unknown-linux-gnueabi", + "armebv7r-none-eabi", + "armebv7r-none-eabihf", + "armv4t-none-eabi", + "armv4t-unknown-linux-gnueabi", + "armv5te-none-eabi", + "armv5te-unknown-linux-gnueabi", + "armv5te-unknown-linux-musleabi", + "armv5te-unknown-linux-uclibceabi", + "armv6-unknown-freebsd", + "armv6-unknown-netbsd-eabihf", + "armv6k-nintendo-3ds", + "armv7-apple-ios", + "armv7-linux-androideabi", + "armv7-sony-vita-newlibeabihf", + "armv7-unknown-freebsd", + "armv7-unknown-linux-gnueabi", + "armv7-unknown-linux-gnueabihf", + "armv7-unknown-linux-musleabi", + "armv7-unknown-linux-musleabihf", + "armv7-unknown-linux-ohos", + "armv7-unknown-linux-uclibceabi", + "armv7-unknown-linux-uclibceabihf", + "armv7-unknown-netbsd-eabihf", + "armv7-wrs-vxworks-eabihf", + "armv7a-kmc-solid_asp3-eabi", + "armv7a-kmc-solid_asp3-eabihf", + "armv7a-none-eabi", + "armv7a-none-eabihf", + "armv7k-apple-watchos", + "armv7r-none-eabi", + "armv7r-none-eabihf", + "armv7s-apple-ios", + "asmjs-unknown-emscripten", + "avr-unknown-gnu-atmega328", + "bpfeb-unknown-none", + "bpfel-unknown-none", + "hexagon-unknown-linux-musl", + "i386-apple-ios", + "i586-pc-nto-qnx700", + "i586-pc-windows-msvc", + "i586-unknown-linux-gnu", + "i586-unknown-linux-musl", + "i686-apple-darwin", + "i686-linux-android", + "i686-pc-windows-gnu", + "i686-pc-windows-msvc", + "i686-unknown-freebsd", + "i686-unknown-haiku", + "i686-unknown-linux-gnu", + "i686-unknown-linux-musl", + "i686-unknown-netbsd", + "i686-unknown-openbsd", + "i686-unknown-uefi", + "i686-uwp-windows-gnu", + "i686-uwp-windows-msvc", + "i686-wrs-vxworks", + "loongarch64-unknown-linux-gnu", + "m68k-unknown-linux-gnu", + "mips-unknown-linux-gnu", + "mips-unknown-linux-musl", + "mips-unknown-linux-uclibc", + "mips64-openwrt-linux-musl", + "mips64-unknown-linux-gnuabi64", + "mips64-unknown-linux-muslabi64", + "mips64el-unknown-linux-gnuabi64", + "mips64el-unknown-linux-muslabi64", + "mipsel-sony-psp", + "mipsel-sony-psx", + "mipsel-unknown-linux-gnu", + "mipsel-unknown-linux-musl", + "mipsel-unknown-linux-uclibc", + "mipsel-unknown-none", + "mipsisa32r6-unknown-linux-gnu", + "mipsisa32r6el-unknown-linux-gnu", + "mipsisa64r6-unknown-linux-gnuabi64", + "mipsisa64r6el-unknown-linux-gnuabi64", + "msp430-none-elf", + "nvptx64-nvidia-cuda", + "powerpc-unknown-freebsd", + "powerpc-unknown-linux-gnu", + "powerpc-unknown-linux-gnuspe", + "powerpc-unknown-linux-musl", + "powerpc-unknown-netbsd", + "powerpc-unknown-openbsd", + "powerpc-wrs-vxworks", + "powerpc-wrs-vxworks-spe", + "powerpc64-ibm-aix", + "powerpc64-unknown-freebsd", + "powerpc64-unknown-linux-gnu", + "powerpc64-unknown-linux-musl", + "powerpc64-unknown-openbsd", + "powerpc64-wrs-vxworks", + "powerpc64le-unknown-freebsd", + "powerpc64le-unknown-linux-gnu", + "powerpc64le-unknown-linux-musl", + "riscv32gc-unknown-linux-gnu", + "riscv32gc-unknown-linux-musl", + "riscv32i-unknown-none-elf", + "riscv32im-unknown-none-elf", + "riscv32imac-unknown-none-elf", + "riscv32imac-unknown-xous-elf", + "riscv32imc-esp-espidf", + "riscv32imc-unknown-none-elf", + "riscv64gc-unknown-freebsd", + "riscv64gc-unknown-fuchsia", + "riscv64gc-unknown-linux-gnu", + "riscv64gc-unknown-linux-musl", + "riscv64gc-unknown-none-elf", + "riscv64gc-unknown-openbsd", + "riscv64imac-unknown-none-elf", + "s390x-unknown-linux-gnu", + "s390x-unknown-linux-musl", + "sparc-unknown-linux-gnu", + "sparc64-unknown-linux-gnu", + "sparc64-unknown-netbsd", + "sparc64-unknown-openbsd", + "sparcv9-sun-solaris", + "thumbv4t-none-eabi", + "thumbv5te-none-eabi", + "thumbv6m-none-eabi", + "thumbv7a-pc-windows-msvc", + "thumbv7a-uwp-windows-msvc", + "thumbv7em-none-eabi", + "thumbv7em-none-eabihf", + "thumbv7m-none-eabi", + "thumbv7neon-linux-androideabi", + "thumbv7neon-unknown-linux-gnueabihf", + "thumbv7neon-unknown-linux-musleabihf", + "thumbv8m.base-none-eabi", + "thumbv8m.main-none-eabi", + "thumbv8m.main-none-eabihf", + "wasm32-unknown-emscripten", + "wasm32-unknown-unknown", + "wasm32-wasi", + "wasm64-unknown-unknown", + "x86_64-apple-darwin", + "x86_64-apple-ios", + "x86_64-apple-ios-macabi", + "x86_64-apple-tvos", + "x86_64-apple-watchos-sim", + "x86_64-fortanix-unknown-sgx", + "x86_64-fuchsia", + "x86_64-linux-android", + "x86_64-pc-nto-qnx710", + "x86_64-pc-solaris", + "x86_64-pc-windows-gnu", + "x86_64-pc-windows-gnullvm", + "x86_64-pc-windows-msvc", + "x86_64-sun-solaris", + "x86_64-unknown-dragonfly", + "x86_64-unknown-freebsd", + "x86_64-unknown-fuchsia", + "x86_64-unknown-haiku", + "x86_64-unknown-hermit", + "x86_64-unknown-illumos", + "x86_64-unknown-l4re-uclibc", + "x86_64-unknown-linux-gnu", + "x86_64-unknown-linux-gnux32", + "x86_64-unknown-linux-musl", + "x86_64-unknown-netbsd", + "x86_64-unknown-none", + "x86_64-unknown-openbsd", + "x86_64-unknown-redox", + "x86_64-unknown-uefi", + "x86_64-uwp-windows-gnu", + "x86_64-uwp-windows-msvc", + "x86_64-wrs-vxworks", + ] + # Additional targets from 1.71 + if Version(version) >= "1.71.0": + rust_targets += [ + "x86_64h-apple-darwin", + ] + # Additional targets from 1.72 + if Version(version) >= "1.72.0": + rust_targets += [ + "aarch64_be-unknown-netbsd", + "loongarch64-unknown-none", + "loongarch64-unknown-none-softfloat", + "riscv32imac-esp-espidf", + "riscv64gc-unknown-netbsd", + ] + # Additional targets from 1.73 + if Version(version) >= "1.73.0": + rust_targets += [ + "aarch64-unknown-teeos", + "csky-unknown-linux-gnuabiv2", + "riscv64-linux-android", + "riscv64gc-unknown-hermit", + "sparc-unknown-none-elf", + "wasm32-wasi-preview1-threads", + "x86_64-unikraft-linux-musl", + "x86_64-unknown-linux-ohos", + ] + # Additional targets from 1.74 + if Version(version) >= "1.74.0": + rust_targets += [ + "i686-pc-windows-gnullvm", + "i686-unknown-hurd-gnu", + ] + rust_targets.remove("armv7-apple-ios") + # Additional targets from 1.75 + if Version(version) >= "1.75.0": + rust_targets += [ + "aarch64-apple-tvos-sim", + "csky-unknown-linux-gnuabiv2hf", + "i586-unknown-netbsd", + "mipsel-unknown-netbsd", + ] + # Additional targets from 1.76 + if Version(version) >= "1.76.0": + rust_targets += [ + "aarch64-apple-watchos", + "arm64e-apple-darwin", + "arm64e-apple-ios", + "i686-win7-windows-msvc", + "riscv32imafc-unknown-none-elf", + "x86_64-win7-windows-msvc", + ] + rust_targets.remove("asmjs-unknown-emscripten") + rust_targets.remove("x86_64-sun-solaris") + # Additional targets from 1.77 + if Version(version) >= "1.77.0": + rust_targets += [ + "aarch64-unknown-illumos", + "hexagon-unknown-none-elf", + ] + + return 0, "\n".join(sorted(rust_targets)), "" + if ( + len(args) == 6 + and args[:2] == ("--crate-type", "staticlib") + and args[2].startswith("--target=") + and args[3] == "-o" + ): + with open(args[4], "w") as fh: + fh.write("foo") + return 0, "", "" + raise NotImplementedError("unsupported arguments") + + return invoke_rustc + + +class RustTest(BaseConfigureTest): + VERSION = MINIMUM_RUST_VERSION + + def get_rust_target(self, target, compiler_type="gcc", arm_target=None): + environ = { + "PATH": os.pathsep.join(mozpath.abspath(p) for p in ("/bin", "/usr/bin")) + } + + paths = { + mozpath.abspath("/usr/bin/cargo"): gen_invoke_cargo(self.VERSION), + mozpath.abspath("/usr/bin/rustc"): gen_invoke_rustc(self.VERSION), + } + + self.TARGET = target + sandbox = self.get_sandbox(paths, {}, [], environ) + + # Trick the sandbox into not running the target compiler check + dep = sandbox._depends[sandbox["c_compiler"]] + getattr(sandbox, "__value_for_depends")[(dep,)] = CompilerResult( + type=compiler_type + ) + # Same for the arm_target checks. + dep = sandbox._depends[sandbox["arm_target"]] + getattr(sandbox, "__value_for_depends")[ + (dep,) + ] = arm_target or ReadOnlyNamespace( + arm_arch=7, thumb2=False, fpu="vfpv2", float_abi="softfp" + ) + return sandbox._value_for(sandbox["rust_target_triple"]) + + def test_rust_target(self): + # Cases where the output of config.sub matches a rust target + for straightforward in ( + "x86_64-unknown-dragonfly", + "aarch64-unknown-freebsd", + "i686-unknown-freebsd", + "x86_64-unknown-freebsd", + "sparc64-unknown-netbsd", + "i686-unknown-netbsd", + "x86_64-unknown-netbsd", + "i686-unknown-openbsd", + "x86_64-unknown-openbsd", + "aarch64-unknown-linux-gnu", + "sparc64-unknown-linux-gnu", + "i686-unknown-linux-gnu", + "i686-apple-darwin", + "x86_64-apple-darwin", + "mips-unknown-linux-gnu", + "mipsel-unknown-linux-gnu", + "mips64-unknown-linux-gnuabi64", + "mips64el-unknown-linux-gnuabi64", + "powerpc64-unknown-linux-gnu", + "powerpc64le-unknown-linux-gnu", + "i686-pc-windows-msvc", + "x86_64-pc-windows-msvc", + "aarch64-pc-windows-msvc", + "i686-pc-windows-gnu", + "x86_64-pc-windows-gnu", + ): + self.assertEqual(self.get_rust_target(straightforward), straightforward) + + # Cases where the output of config.sub is different + for autoconf, rust in ( + ("aarch64-unknown-linux-android", "aarch64-linux-android"), + ("aarch64-unknown-linux-android21", "aarch64-linux-android"), + ("aarch64-apple-darwin23.3.0", "aarch64-apple-darwin"), + ("arm-unknown-linux-androideabi", "armv7-linux-androideabi"), + ("armv7-unknown-linux-androideabi", "armv7-linux-androideabi"), + ("armv7-unknown-linux-androideabi21", "armv7-linux-androideabi"), + ("i386-unknown-linux-android", "i686-linux-android"), + ("i686-unknown-linux-android21", "i686-linux-android"), + ("i686-pc-linux-gnu", "i686-unknown-linux-gnu"), + ("x86_64-unknown-linux-android", "x86_64-linux-android"), + ("x86_64-unknown-linux-android21", "x86_64-linux-android"), + ("x86_64-pc-linux-gnu", "x86_64-unknown-linux-gnu"), + ("sparcv9-sun-solaris2", "sparcv9-sun-solaris"), + ( + "x86_64-sun-solaris2", + "x86_64-sun-solaris" + if Version(self.VERSION) < "1.76.0" + else "x86_64-pc-solaris", + ), + ("x86_64-apple-darwin23.3.0", "x86_64-apple-darwin"), + ): + self.assertEqual(self.get_rust_target(autoconf), rust) + + # Windows + for autoconf, building_with_gcc, rust in ( + ("i686-pc-mingw32", "clang-cl", "i686-pc-windows-msvc"), + ("x86_64-pc-mingw32", "clang-cl", "x86_64-pc-windows-msvc"), + ("i686-pc-mingw32", "clang", "i686-pc-windows-gnu"), + ("x86_64-pc-mingw32", "clang", "x86_64-pc-windows-gnu"), + ("i686-w64-mingw32", "clang", "i686-pc-windows-gnu"), + ("x86_64-w64-mingw32", "clang", "x86_64-pc-windows-gnu"), + ("aarch64-windows-mingw32", "clang-cl", "aarch64-pc-windows-msvc"), + ): + self.assertEqual(self.get_rust_target(autoconf, building_with_gcc), rust) + + # Arm special cases + self.assertEqual( + self.get_rust_target( + "arm-unknown-linux-androideabi", + arm_target=ReadOnlyNamespace( + arm_arch=7, fpu="neon", thumb2=True, float_abi="softfp" + ), + ), + "thumbv7neon-linux-androideabi", + ) + + self.assertEqual( + self.get_rust_target( + "arm-unknown-linux-androideabi", + arm_target=ReadOnlyNamespace( + arm_arch=7, fpu="neon", thumb2=False, float_abi="softfp" + ), + ), + "armv7-linux-androideabi", + ) + + self.assertEqual( + self.get_rust_target( + "arm-unknown-linux-androideabi", + arm_target=ReadOnlyNamespace( + arm_arch=7, fpu="vfpv2", thumb2=True, float_abi="softfp" + ), + ), + "armv7-linux-androideabi", + ) + + self.assertEqual( + self.get_rust_target( + "armv7-unknown-linux-gnueabihf", + arm_target=ReadOnlyNamespace( + arm_arch=7, fpu="neon", thumb2=True, float_abi="hard" + ), + ), + "thumbv7neon-unknown-linux-gnueabihf", + ) + + self.assertEqual( + self.get_rust_target( + "armv7-unknown-linux-gnueabihf", + arm_target=ReadOnlyNamespace( + arm_arch=7, fpu="neon", thumb2=False, float_abi="hard" + ), + ), + "armv7-unknown-linux-gnueabihf", + ) + + self.assertEqual( + self.get_rust_target( + "armv7-unknown-linux-gnueabihf", + arm_target=ReadOnlyNamespace( + arm_arch=7, fpu="vfpv2", thumb2=True, float_abi="hard" + ), + ), + "armv7-unknown-linux-gnueabihf", + ) + + self.assertEqual( + self.get_rust_target( + "arm-unknown-freebsd13.0-gnueabihf", + arm_target=ReadOnlyNamespace( + arm_arch=7, fpu="vfpv2", thumb2=True, float_abi="hard" + ), + ), + "armv7-unknown-freebsd", + ) + + self.assertEqual( + self.get_rust_target( + "arm-unknown-freebsd13.0-gnueabihf", + arm_target=ReadOnlyNamespace( + arm_arch=6, fpu=None, thumb2=False, float_abi="hard" + ), + ), + "armv6-unknown-freebsd", + ) + + self.assertEqual( + self.get_rust_target( + "arm-unknown-linux-gnueabi", + arm_target=ReadOnlyNamespace( + arm_arch=4, fpu=None, thumb2=False, float_abi="softfp" + ), + ), + "armv4t-unknown-linux-gnueabi", + ) + + +class Rust177Test(RustTest): + VERSION = "1.77.0" + + +if __name__ == "__main__": + main() diff --git a/python/mozbuild/mozbuild/test/configure/test_toolchain_helpers.py b/python/mozbuild/mozbuild/test/configure/test_toolchain_helpers.py new file mode 100644 index 0000000000..f42778215b --- /dev/null +++ b/python/mozbuild/mozbuild/test/configure/test_toolchain_helpers.py @@ -0,0 +1,433 @@ +# 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/. + +import copy +import re +import unittest +from fnmatch import fnmatch +from textwrap import dedent + +import six +from mozpack import path as mozpath +from mozunit import MockedOpen, main +from six import StringIO + +from mozbuild.preprocessor import Preprocessor +from mozbuild.util import ReadOnlyNamespace + + +class CompilerPreprocessor(Preprocessor): + # The C preprocessor only expands macros when they are not in C strings. + # For now, we don't look very hard for C strings because they don't matter + # that much for our unit tests, but we at least avoid expanding in the + # simple "FOO" case. + VARSUBST = re.compile('(?<!")(?P<VAR>\w+)(?!")', re.U) + NON_WHITESPACE = re.compile("\S") + HAS_FEATURE_OR_BUILTIN = re.compile( + '(__has_(?:feature|builtin|attribute|warning))\("?([^"\)]*)"?\)' + ) + + def __init__(self, *args, **kwargs): + Preprocessor.__init__(self, *args, **kwargs) + self.do_filter("c_substitution") + self.setMarker("#\s*") + + def do_if(self, expression, **kwargs): + # The C preprocessor handles numbers following C rules, which is a + # different handling than what our Preprocessor does out of the box. + # Hack around it enough that the configure tests work properly. + context = self.context + + def normalize_numbers(value): + if isinstance(value, six.string_types): + if value[-1:] == "L" and value[:-1].isdigit(): + value = int(value[:-1]) + return value + + # Our Preprocessor doesn't handle macros with parameters, so we hack + # around that for __has_feature()-like things. + + def normalize_has_feature_or_builtin(expr): + return ( + self.HAS_FEATURE_OR_BUILTIN.sub(r"\1\2", expr) + .replace("-", "_") + .replace("+", "_") + ) + + self.context = self.Context( + (normalize_has_feature_or_builtin(k), normalize_numbers(v)) + for k, v in six.iteritems(context) + ) + try: + return Preprocessor.do_if( + self, normalize_has_feature_or_builtin(expression), **kwargs + ) + finally: + self.context = context + + class Context(dict): + def __missing__(self, key): + return None + + def filter_c_substitution(self, line): + def repl(matchobj): + varname = matchobj.group("VAR") + if varname in self.context: + result = six.text_type(self.context[varname]) + # The C preprocessor inserts whitespaces around expanded + # symbols. + start, end = matchobj.span("VAR") + if self.NON_WHITESPACE.match(line[start - 1 : start]): + result = " " + result + if self.NON_WHITESPACE.match(line[end : end + 1]): + result = result + " " + return result + return matchobj.group(0) + + return self.VARSUBST.sub(repl, line) + + +class TestCompilerPreprocessor(unittest.TestCase): + def test_expansion(self): + pp = CompilerPreprocessor({"A": 1, "B": "2", "C": "c", "D": "d"}) + pp.out = StringIO() + input = StringIO('A.B.C "D"') + input.name = "foo" + pp.do_include(input) + + self.assertEqual(pp.out.getvalue(), '1 . 2 . c "D"') + + def test_normalization(self): + pp = CompilerPreprocessor( + {"__has_attribute(bar)": 1, '__has_warning("-Wc++98-foo")': 1} + ) + pp.out = StringIO() + input = StringIO( + dedent( + """\ + #if __has_warning("-Wbar") + WBAR + #endif + #if __has_warning("-Wc++98-foo") + WFOO + #endif + #if !__has_warning("-Wc++98-foo") + NO_WFOO + #endif + #if __has_attribute(bar) + BAR + #else + NO_BAR + #endif + #if !__has_attribute(foo) + NO_FOO + #endif + """ + ) + ) + + input.name = "foo" + pp.do_include(input) + + self.assertEqual(pp.out.getvalue(), "WFOO\nBAR\nNO_FOO\n") + + def test_condition(self): + pp = CompilerPreprocessor({"A": 1, "B": "2", "C": "0L"}) + pp.out = StringIO() + input = StringIO( + dedent( + """\ + #ifdef A + IFDEF_A + #endif + #if A + IF_A + #endif + # if B + IF_B + # else + IF_NOT_B + # endif + #if !C + IF_NOT_C + #else + IF_C + #endif + """ + ) + ) + input.name = "foo" + pp.do_include(input) + + self.assertEqual("IFDEF_A\nIF_A\nIF_NOT_B\nIF_NOT_C\n", pp.out.getvalue()) + + +class FakeCompiler(dict): + """Defines a fake compiler for use in toolchain tests below. + + The definitions given when creating an instance can have one of two + forms: + - a dict giving preprocessor symbols and their respective value, e.g. + { '__GNUC__': 4, '__STDC__': 1 } + - a dict associating flags to preprocessor symbols. An entry for `None` + is required in this case. Those are the baseline preprocessor symbols. + Additional entries describe additional flags to set or existing flags + to unset (with a value of `False`). + { + None: { '__GNUC__': 4, '__STDC__': 1, '__STRICT_ANSI__': 1 }, + '-std=gnu99': { '__STDC_VERSION__': '199901L', + '__STRICT_ANSI__': False }, + } + With the dict above, invoking the preprocessor with no additional flags + would define __GNUC__, __STDC__ and __STRICT_ANSI__, and with -std=gnu99, + __GNUC__, __STDC__, and __STDC_VERSION__ (__STRICT_ANSI__ would be + unset). + It is also possible to have different symbols depending on the source + file extension. In this case, the key is '*.ext'. e.g. + { + '*.c': { '__STDC__': 1 }, + '*.cpp': { '__cplusplus': '199711L' }, + } + + All the given definitions are merged together. + + A FakeCompiler instance itself can be used as a definition to create + another FakeCompiler. + + For convenience, FakeCompiler instances can be added (+) to one another. + """ + + def __init__(self, *definitions): + for definition in definitions: + if all(not isinstance(d, dict) for d in six.itervalues(definition)): + definition = {None: definition} + for key, value in six.iteritems(definition): + self.setdefault(key, {}).update(value) + + def __call__(self, stdin, args): + files = [] + flags = [] + args = iter(args) + while True: + arg = next(args, None) + if arg is None: + break + if arg.startswith("-"): + # Ignore -isysroot/--sysroot and the argument that follows it. + if arg in ("-isysroot", "--sysroot"): + next(args, None) + else: + flags.append(arg) + else: + files.append(arg) + + if "-E" in flags: + assert len(files) == 1 + file = files[0] + pp = CompilerPreprocessor(self[None]) + + def apply_defn(defn): + for k, v in six.iteritems(defn): + if v is False: + if k in pp.context: + del pp.context[k] + else: + pp.context[k] = v + + for glob, defn in six.iteritems(self): + if glob and not glob.startswith("-") and fnmatch(file, glob): + apply_defn(defn) + + for flag in flags: + apply_defn(self.get(flag, {})) + + pp.out = StringIO() + pp.do_include(file) + return 0, pp.out.getvalue(), "" + elif "-c" in flags: + if "-funknown-flag" in flags: + return 1, "", "" + return 0, "", "" + + return 1, "", "" + + def __add__(self, other): + return FakeCompiler(self, other) + + +class TestFakeCompiler(unittest.TestCase): + def test_fake_compiler(self): + with MockedOpen({"file": "A B C", "file.c": "A B C"}): + compiler = FakeCompiler({"A": "1", "B": "2"}) + self.assertEqual(compiler(None, ["-E", "file"]), (0, "1 2 C", "")) + + compiler = FakeCompiler( + { + None: {"A": "1", "B": "2"}, + "-foo": {"C": "foo"}, + "-bar": {"B": "bar", "C": "bar"}, + "-qux": {"B": False}, + "*.c": {"B": "42"}, + } + ) + self.assertEqual(compiler(None, ["-E", "file"]), (0, "1 2 C", "")) + self.assertEqual(compiler(None, ["-E", "-foo", "file"]), (0, "1 2 foo", "")) + self.assertEqual( + compiler(None, ["-E", "-bar", "file"]), (0, "1 bar bar", "") + ) + self.assertEqual(compiler(None, ["-E", "-qux", "file"]), (0, "1 B C", "")) + self.assertEqual( + compiler(None, ["-E", "-foo", "-bar", "file"]), (0, "1 bar bar", "") + ) + self.assertEqual( + compiler(None, ["-E", "-bar", "-foo", "file"]), (0, "1 bar foo", "") + ) + self.assertEqual( + compiler(None, ["-E", "-bar", "-qux", "file"]), (0, "1 B bar", "") + ) + self.assertEqual( + compiler(None, ["-E", "-qux", "-bar", "file"]), (0, "1 bar bar", "") + ) + self.assertEqual(compiler(None, ["-E", "file.c"]), (0, "1 42 C", "")) + self.assertEqual( + compiler(None, ["-E", "-bar", "file.c"]), (0, "1 bar bar", "") + ) + + def test_multiple_definitions(self): + compiler = FakeCompiler({"A": 1, "B": 2}, {"C": 3}) + + self.assertEqual(compiler, {None: {"A": 1, "B": 2, "C": 3}}) + compiler = FakeCompiler({"A": 1, "B": 2}, {"B": 4, "C": 3}) + + self.assertEqual(compiler, {None: {"A": 1, "B": 4, "C": 3}}) + compiler = FakeCompiler( + {"A": 1, "B": 2}, {None: {"B": 4, "C": 3}, "-foo": {"D": 5}} + ) + + self.assertEqual(compiler, {None: {"A": 1, "B": 4, "C": 3}, "-foo": {"D": 5}}) + + compiler = FakeCompiler( + {None: {"A": 1, "B": 2}, "-foo": {"D": 5}}, + {"-foo": {"D": 5}, "-bar": {"E": 6}}, + ) + + self.assertEqual( + compiler, {None: {"A": 1, "B": 2}, "-foo": {"D": 5}, "-bar": {"E": 6}} + ) + + +class PrependFlags(list): + """Wrapper to allow to Prepend to flags instead of appending, in + CompilerResult. + """ + + +class CompilerResult(ReadOnlyNamespace): + """Helper of convenience to manipulate toolchain results in unit tests + + When adding a dict, the result is a new CompilerResult with the values + from the dict replacing those from the CompilerResult, except for `flags`, + where the value from the dict extends the `flags` in `self`. + """ + + def __init__( + self, wrapper=None, compiler="", version="", type="", language="", flags=None + ): + if flags is None: + flags = [] + if wrapper is None: + wrapper = [] + super(CompilerResult, self).__init__( + flags=flags, + version=version, + type=type, + compiler=mozpath.abspath(compiler), + wrapper=wrapper, + language=language, + ) + + def __add__(self, other): + assert isinstance(other, dict) + result = copy.deepcopy(self.__dict__) + for k, v in six.iteritems(other): + if k == "flags": + flags = result.setdefault(k, []) + if isinstance(v, PrependFlags): + flags[:0] = v + else: + flags.extend(v) + else: + result[k] = v + return CompilerResult(**result) + + +class TestCompilerResult(unittest.TestCase): + def test_compiler_result(self): + result = CompilerResult() + self.assertEqual( + result.__dict__, + { + "wrapper": [], + "compiler": mozpath.abspath(""), + "version": "", + "type": "", + "language": "", + "flags": [], + }, + ) + + result = CompilerResult( + compiler="/usr/bin/gcc", + version="4.2.1", + type="gcc", + language="C", + flags=["-std=gnu99"], + ) + self.assertEqual( + result.__dict__, + { + "wrapper": [], + "compiler": mozpath.abspath("/usr/bin/gcc"), + "version": "4.2.1", + "type": "gcc", + "language": "C", + "flags": ["-std=gnu99"], + }, + ) + + result2 = result + {"flags": ["-m32"]} + self.assertEqual( + result2.__dict__, + { + "wrapper": [], + "compiler": mozpath.abspath("/usr/bin/gcc"), + "version": "4.2.1", + "type": "gcc", + "language": "C", + "flags": ["-std=gnu99", "-m32"], + }, + ) + # Original flags are untouched. + self.assertEqual(result.flags, ["-std=gnu99"]) + + result3 = result + { + "compiler": "/usr/bin/gcc-4.7", + "version": "4.7.3", + "flags": ["-m32"], + } + self.assertEqual( + result3.__dict__, + { + "wrapper": [], + "compiler": mozpath.abspath("/usr/bin/gcc-4.7"), + "version": "4.7.3", + "type": "gcc", + "language": "C", + "flags": ["-std=gnu99", "-m32"], + }, + ) + + +if __name__ == "__main__": + main() diff --git a/python/mozbuild/mozbuild/test/configure/test_toolkit_moz_configure.py b/python/mozbuild/mozbuild/test/configure/test_toolkit_moz_configure.py new file mode 100644 index 0000000000..7d20c0a49e --- /dev/null +++ b/python/mozbuild/mozbuild/test/configure/test_toolkit_moz_configure.py @@ -0,0 +1,268 @@ +# 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/. + +import os + +from buildconfig import topsrcdir +from mozpack import path as mozpath +from mozunit import MockedOpen, main +from test_toolchain_helpers import CompilerResult + +from common import BaseConfigureTest +from mozbuild.configure.options import InvalidOptionError +from mozbuild.configure.util import Version + + +class TestToolkitMozConfigure(BaseConfigureTest): + def test_moz_configure_options(self): + def get_value_for(args=[], environ={}, mozconfig=""): + sandbox = self.get_sandbox({}, {}, args, environ, mozconfig) + + # Add a fake old-configure option + sandbox.option_impl( + "--with-foo", nargs="*", help="Help missing for old configure options" + ) + + # Remove all implied options, otherwise, getting + # all_configure_options below triggers them, and that triggers + # configure parts that aren't expected to run during this test. + del sandbox._implied_options[:] + result = sandbox._value_for(sandbox["all_configure_options"]) + shell = mozpath.abspath("/bin/sh") + return result.replace("CONFIG_SHELL=%s " % shell, "") + + self.assertEqual( + "--enable-application=browser", + get_value_for(["--enable-application=browser"]), + ) + + self.assertEqual( + "--enable-application=browser " "MOZ_VTUNE=1", + get_value_for(["--enable-application=browser", "MOZ_VTUNE=1"]), + ) + + value = get_value_for( + environ={"MOZ_VTUNE": "1"}, + mozconfig="ac_add_options --enable-application=browser", + ) + + self.assertEqual("--enable-application=browser MOZ_VTUNE=1", value) + + # --disable-js-shell is the default, so it's filtered out. + self.assertEqual( + "--enable-application=browser", + get_value_for(["--enable-application=browser", "--disable-js-shell"]), + ) + + # Normally, --without-foo would be filtered out because that's the + # default, but since it is a (fake) old-configure option, it always + # appears. + self.assertEqual( + "--enable-application=browser --without-foo", + get_value_for(["--enable-application=browser", "--without-foo"]), + ) + self.assertEqual( + "--enable-application=browser --with-foo", + get_value_for(["--enable-application=browser", "--with-foo"]), + ) + + self.assertEqual( + "--enable-application=browser '--with-foo=foo bar'", + get_value_for(["--enable-application=browser", "--with-foo=foo bar"]), + ) + + def test_developer_options(self, milestone="42.0a1"): + def get_value(args=[], environ={}): + sandbox = self.get_sandbox({}, {}, args, environ) + return sandbox._value_for(sandbox["developer_options"]) + + milestone_path = os.path.join(topsrcdir, "config", "milestone.txt") + with MockedOpen({milestone_path: milestone}): + # developer options are enabled by default on "nightly" milestone + # only + self.assertEqual(get_value(), "a" in milestone or None) + + self.assertEqual(get_value(["--enable-release"]), None) + + self.assertEqual(get_value(environ={"MOZILLA_OFFICIAL": 1}), None) + + self.assertEqual( + get_value(["--enable-release"], environ={"MOZILLA_OFFICIAL": 1}), None + ) + + with self.assertRaises(InvalidOptionError): + get_value(["--disable-release"], environ={"MOZILLA_OFFICIAL": 1}) + + self.assertEqual(get_value(environ={"MOZ_AUTOMATION": 1}), None) + + def test_developer_options_release(self): + self.test_developer_options("42.0") + + def test_elfhack(self): + class ReadElf: + def __init__(self, with_relr): + self.with_relr = with_relr + + def __call__(self, stdin, args): + assert len(args) == 2 and args[0] == "-d" + if self.with_relr: + return 0, " 0x0000023 (DT_RELR) 0x1000\n", "" + return 0, "", "" + + class MockCC: + def __init__(self, have_pack_relative_relocs, have_arc4random): + self.have_pack_relative_relocs = have_pack_relative_relocs + self.have_arc4random = have_arc4random + + def __call__(self, stdin, args): + if args == ["--version"]: + return 0, "mockcc", "" + if "-Wl,--version" in args: + assert len(args) <= 2 + if len(args) == 1 or args[0] == "-fuse-ld=bfd": + return 0, "GNU ld", "" + if args[0] == "-fuse-ld=lld": + return 0, "LLD", "" + if args[0] == "-fuse-ld=gold": + return 0, "GNU gold", "" + elif "-Wl,-z,pack-relative-relocs" in args: + if self.have_pack_relative_relocs: + return 0, "", "" + else: + return 1, "", "Unknown flag" + + # Assume the first argument is an input file. + with open(args[0]) as fh: + if "arc4random()" in fh.read(): + if self.have_arc4random: + return 0, "", "" + else: + return 1, "", "Undefined symbol" + assert False + + def get_values(mockcc, readelf, args=[]): + sandbox = self.get_sandbox( + { + "/usr/bin/mockcc": mockcc, + "/usr/bin/readelf": readelf, + }, + {}, + ["--disable-bootstrap", "--disable-release"] + args, + ) + value_for_depends = getattr(sandbox, "__value_for_depends") + # Trick the sandbox into not running too much + dep = sandbox._depends[sandbox["c_compiler"]] + value_for_depends[(dep,)] = CompilerResult( + compiler="/usr/bin/mockcc", + language="C", + type="clang", + version=Version("16.0"), + flags=[], + ) + dep = sandbox._depends[sandbox["readelf"]] + value_for_depends[(dep,)] = "/usr/bin/readelf" + + return ( + sandbox._value_for(sandbox["select_linker"]).KIND, + sandbox._value_for(sandbox["pack_relative_relocs_flags"]), + sandbox._value_for(sandbox["which_elf_hack"]), + ) + + PACK = ["-Wl,-z,pack-relative-relocs"] + # The typical case with a bootstrap build: linker supports pack-relative-relocs, + # but glibc is old and doesn't. + mockcc = MockCC(True, False) + readelf = ReadElf(True) + self.assertEqual(get_values(mockcc, readelf), ("lld", None, "relr")) + self.assertEqual( + get_values(mockcc, readelf, ["--enable-release"]), ("lld", None, "relr") + ) + self.assertEqual( + get_values(mockcc, readelf, ["--enable-elf-hack"]), ("lld", None, "relr") + ) + self.assertEqual( + get_values(mockcc, readelf, ["--enable-elf-hack=relr"]), + ("lld", None, "relr"), + ) + # LLD is picked by default and enabling elfhack fails because of that. + with self.assertRaises(SystemExit): + get_values(mockcc, readelf, ["--enable-elf-hack=legacy"]) + # If we force to use BFD ld, it works. + self.assertEqual( + get_values( + mockcc, readelf, ["--enable-elf-hack=legacy", "--enable-linker=bfd"] + ), + ("bfd", None, "legacy"), + ) + + for mockcc, readelf in ( + # Linker doesn't support pack-relative-relocs. Glibc is old. + (MockCC(False, False), ReadElf(False)), + # Linker doesn't support pack-relative-relocs. Glibc is new. + (MockCC(False, True), ReadElf(False)), + # Linker doesn't error out for unknown flags. Glibc is old. + (MockCC(True, False), ReadElf(False)), + # Linker doesn't error out for unknown flags. Glibc is new. + (MockCC(True, True), ReadElf(False)), + ): + self.assertEqual(get_values(mockcc, readelf), ("lld", None, None)) + self.assertEqual( + get_values(mockcc, readelf, ["--enable-release"]), + ("lld", None, None), + ) + with self.assertRaises(SystemExit): + get_values(mockcc, readelf, ["--enable-elf-hack"]) + with self.assertRaises(SystemExit): + get_values(mockcc, readelf, ["--enable-elf-hack=relr"]) + # LLD is picked by default and enabling elfhack fails because of that. + with self.assertRaises(SystemExit): + get_values(mockcc, readelf, ["--enable-elf-hack=legacy"]) + # If we force to use BFD ld, it works. + self.assertEqual( + get_values( + mockcc, readelf, ["--enable-elf-hack", "--enable-linker=bfd"] + ), + ("bfd", None, "legacy"), + ) + self.assertEqual( + get_values( + mockcc, readelf, ["--enable-elf-hack=legacy", "--enable-linker=bfd"] + ), + ("bfd", None, "legacy"), + ) + + # Linker supports pack-relative-relocs, and glibc too. We use pack-relative-relocs + # unless elfhack is explicitly enabled. + mockcc = MockCC(True, True) + readelf = ReadElf(True) + self.assertEqual(get_values(mockcc, readelf), ("lld", PACK, None)) + self.assertEqual( + get_values(mockcc, readelf, ["--enable-release"]), ("lld", PACK, None) + ) + self.assertEqual( + get_values(mockcc, readelf, ["--enable-elf-hack"]), + ("lld", None, "relr"), + ) + self.assertEqual( + get_values(mockcc, readelf, ["--enable-elf-hack=relr"]), + ("lld", None, "relr"), + ) + # LLD is picked by default and enabling elfhack fails because of that. + with self.assertRaises(SystemExit): + get_values(mockcc, readelf, ["--enable-elf-hack=legacy"]) + # If we force to use BFD ld, it works. + self.assertEqual( + get_values(mockcc, readelf, ["--enable-elf-hack", "--enable-linker=bfd"]), + ("bfd", None, "relr"), + ) + self.assertEqual( + get_values( + mockcc, readelf, ["--enable-elf-hack=legacy", "--enable-linker=bfd"] + ), + ("bfd", None, "legacy"), + ) + + +if __name__ == "__main__": + main() diff --git a/python/mozbuild/mozbuild/test/configure/test_util.py b/python/mozbuild/mozbuild/test/configure/test_util.py new file mode 100644 index 0000000000..7bbe9b7680 --- /dev/null +++ b/python/mozbuild/mozbuild/test/configure/test_util.py @@ -0,0 +1,538 @@ +# 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/. + +import logging +import os +import sys +import tempfile +import textwrap +import unittest + +import six +from buildconfig import topsrcdir +from mozpack import path as mozpath +from mozunit import main +from six import StringIO + +from common import ConfigureTestSandbox +from mozbuild.configure import ConfigureSandbox +from mozbuild.configure.util import ( + ConfigureOutputHandler, + LineIO, + Version, + getpreferredencoding, +) + + +class TestConfigureOutputHandler(unittest.TestCase): + def test_separation(self): + out = StringIO() + err = StringIO() + name = "%s.test_separation" % self.__class__.__name__ + logger = logging.getLogger(name) + logger.setLevel(logging.DEBUG) + logger.addHandler(ConfigureOutputHandler(out, err)) + + logger.error("foo") + logger.warning("bar") + logger.info("baz") + # DEBUG level is not printed out by this handler + logger.debug("qux") + + self.assertEqual(out.getvalue(), "baz\n") + self.assertEqual(err.getvalue(), "foo\nbar\n") + + def test_format(self): + out = StringIO() + err = StringIO() + name = "%s.test_format" % self.__class__.__name__ + logger = logging.getLogger(name) + logger.setLevel(logging.DEBUG) + handler = ConfigureOutputHandler(out, err) + handler.setFormatter(logging.Formatter("%(levelname)s:%(message)s")) + logger.addHandler(handler) + + logger.error("foo") + logger.warning("bar") + logger.info("baz") + # DEBUG level is not printed out by this handler + logger.debug("qux") + + self.assertEqual(out.getvalue(), "baz\n") + self.assertEqual(err.getvalue(), "ERROR:foo\n" "WARNING:bar\n") + + def test_continuation(self): + out = StringIO() + name = "%s.test_continuation" % self.__class__.__name__ + logger = logging.getLogger(name) + logger.setLevel(logging.DEBUG) + handler = ConfigureOutputHandler(out, out) + handler.setFormatter(logging.Formatter("%(levelname)s:%(message)s")) + logger.addHandler(handler) + + logger.info("foo") + logger.info("checking bar... ") + logger.info("yes") + logger.info("qux") + + self.assertEqual(out.getvalue(), "foo\n" "checking bar... yes\n" "qux\n") + + out.seek(0) + out.truncate() + + logger.info("foo") + logger.info("checking bar... ") + logger.warning("hoge") + logger.info("no") + logger.info("qux") + + self.assertEqual( + out.getvalue(), + "foo\n" "checking bar... \n" "WARNING:hoge\n" " ... no\n" "qux\n", + ) + + out.seek(0) + out.truncate() + + logger.info("foo") + logger.info("checking bar... ") + logger.warning("hoge") + logger.warning("fuga") + logger.info("no") + logger.info("qux") + + self.assertEqual( + out.getvalue(), + "foo\n" + "checking bar... \n" + "WARNING:hoge\n" + "WARNING:fuga\n" + " ... no\n" + "qux\n", + ) + + out.seek(0) + out.truncate() + err = StringIO() + + logger.removeHandler(handler) + handler = ConfigureOutputHandler(out, err) + handler.setFormatter(logging.Formatter("%(levelname)s:%(message)s")) + logger.addHandler(handler) + + logger.info("foo") + logger.info("checking bar... ") + logger.warning("hoge") + logger.warning("fuga") + logger.info("no") + logger.info("qux") + + self.assertEqual(out.getvalue(), "foo\n" "checking bar... no\n" "qux\n") + + self.assertEqual(err.getvalue(), "WARNING:hoge\n" "WARNING:fuga\n") + + def test_queue_debug(self): + out = StringIO() + name = "%s.test_queue_debug" % self.__class__.__name__ + logger = logging.getLogger(name) + logger.setLevel(logging.DEBUG) + handler = ConfigureOutputHandler(out, out, maxlen=3) + handler.setFormatter(logging.Formatter("%(levelname)s:%(message)s")) + logger.addHandler(handler) + + with handler.queue_debug(): + logger.info("checking bar... ") + logger.debug("do foo") + logger.info("yes") + logger.info("qux") + + self.assertEqual(out.getvalue(), "checking bar... yes\n" "qux\n") + + out.seek(0) + out.truncate() + + with handler.queue_debug(): + logger.info("checking bar... ") + logger.debug("do foo") + logger.info("no") + logger.error("fail") + + self.assertEqual( + out.getvalue(), "checking bar... no\n" "DEBUG:do foo\n" "ERROR:fail\n" + ) + + out.seek(0) + out.truncate() + + with handler.queue_debug(): + logger.info("checking bar... ") + logger.debug("do foo") + logger.debug("do bar") + logger.debug("do baz") + logger.info("no") + logger.error("fail") + + self.assertEqual( + out.getvalue(), + "checking bar... no\n" + "DEBUG:do foo\n" + "DEBUG:do bar\n" + "DEBUG:do baz\n" + "ERROR:fail\n", + ) + + out.seek(0) + out.truncate() + + with handler.queue_debug(): + logger.info("checking bar... ") + logger.debug("do foo") + logger.debug("do bar") + logger.debug("do baz") + logger.debug("do qux") + logger.debug("do hoge") + logger.info("no") + logger.error("fail") + + self.assertEqual( + out.getvalue(), + "checking bar... no\n" + "DEBUG:<truncated - see config.log for full output>\n" + "DEBUG:do baz\n" + "DEBUG:do qux\n" + "DEBUG:do hoge\n" + "ERROR:fail\n", + ) + + out.seek(0) + out.truncate() + + try: + with handler.queue_debug(): + logger.info("checking bar... ") + logger.debug("do foo") + logger.debug("do bar") + logger.info("no") + e = Exception("fail") + raise e + except Exception as caught: + self.assertIs(caught, e) + + self.assertEqual( + out.getvalue(), "checking bar... no\n" "DEBUG:do foo\n" "DEBUG:do bar\n" + ) + + def test_queue_debug_reentrant(self): + out = StringIO() + name = "%s.test_queue_debug_reentrant" % self.__class__.__name__ + logger = logging.getLogger(name) + logger.setLevel(logging.DEBUG) + handler = ConfigureOutputHandler(out, out, maxlen=10) + handler.setFormatter(logging.Formatter("%(levelname)s| %(message)s")) + logger.addHandler(handler) + + try: + with handler.queue_debug(): + logger.info("outer info") + logger.debug("outer debug") + with handler.queue_debug(): + logger.info("inner info") + logger.debug("inner debug") + e = Exception("inner exception") + raise e + except Exception as caught: + self.assertIs(caught, e) + + self.assertEqual( + out.getvalue(), + "outer info\n" "inner info\n" "DEBUG| outer debug\n" "DEBUG| inner debug\n", + ) + + out.seek(0) + out.truncate() + + try: + with handler.queue_debug(): + logger.info("outer info") + logger.debug("outer debug") + with handler.queue_debug(): + logger.info("inner info") + logger.debug("inner debug") + e = Exception("outer exception") + raise e + except Exception as caught: + self.assertIs(caught, e) + + self.assertEqual( + out.getvalue(), + "outer info\n" "inner info\n" "DEBUG| outer debug\n" "DEBUG| inner debug\n", + ) + + out.seek(0) + out.truncate() + + with handler.queue_debug(): + logger.info("outer info") + logger.debug("outer debug") + with handler.queue_debug(): + logger.info("inner info") + logger.debug("inner debug") + logger.error("inner error") + self.assertEqual( + out.getvalue(), + "outer info\n" + "inner info\n" + "DEBUG| outer debug\n" + "DEBUG| inner debug\n" + "ERROR| inner error\n", + ) + + out.seek(0) + out.truncate() + + with handler.queue_debug(): + logger.info("outer info") + logger.debug("outer debug") + with handler.queue_debug(): + logger.info("inner info") + logger.debug("inner debug") + logger.error("outer error") + self.assertEqual( + out.getvalue(), + "outer info\n" + "inner info\n" + "DEBUG| outer debug\n" + "DEBUG| inner debug\n" + "ERROR| outer error\n", + ) + + def test_is_same_output(self): + fd1 = sys.stderr.fileno() + fd2 = os.dup(fd1) + try: + self.assertTrue(ConfigureOutputHandler._is_same_output(fd1, fd2)) + finally: + os.close(fd2) + + fd2, path = tempfile.mkstemp() + try: + self.assertFalse(ConfigureOutputHandler._is_same_output(fd1, fd2)) + + fd3 = os.dup(fd2) + try: + self.assertTrue(ConfigureOutputHandler._is_same_output(fd2, fd3)) + finally: + os.close(fd3) + + with open(path, "a") as fh: + fd3 = fh.fileno() + self.assertTrue(ConfigureOutputHandler._is_same_output(fd2, fd3)) + + finally: + os.close(fd2) + os.remove(path) + + +class TestLineIO(unittest.TestCase): + def test_lineio(self): + lines = [] + l = LineIO(lambda l: lines.append(l)) + + l.write("a") + self.assertEqual(lines, []) + + l.write("b") + self.assertEqual(lines, []) + + l.write("\n") + self.assertEqual(lines, ["ab"]) + + l.write("cdef") + self.assertEqual(lines, ["ab"]) + + l.write("\n") + self.assertEqual(lines, ["ab", "cdef"]) + + l.write("ghi\njklm") + self.assertEqual(lines, ["ab", "cdef", "ghi"]) + + l.write("nop\nqrst\nuv\n") + self.assertEqual(lines, ["ab", "cdef", "ghi", "jklmnop", "qrst", "uv"]) + + l.write("wx\nyz") + self.assertEqual(lines, ["ab", "cdef", "ghi", "jklmnop", "qrst", "uv", "wx"]) + + l.close() + self.assertEqual( + lines, ["ab", "cdef", "ghi", "jklmnop", "qrst", "uv", "wx", "yz"] + ) + + def test_lineio_contextmanager(self): + lines = [] + with LineIO(lambda l: lines.append(l)) as l: + l.write("a\nb\nc") + + self.assertEqual(lines, ["a", "b"]) + + self.assertEqual(lines, ["a", "b", "c"]) + + +class TestLogSubprocessOutput(unittest.TestCase): + def test_non_ascii_subprocess_output(self): + out = StringIO() + sandbox = ConfigureSandbox({}, {}, ["configure"], out, out) + + sandbox.include_file( + mozpath.join(topsrcdir, "build", "moz.configure", "util.configure") + ) + sandbox.include_file( + mozpath.join( + topsrcdir, + "python", + "mozbuild", + "mozbuild", + "test", + "configure", + "data", + "subprocess.configure", + ) + ) + status = 0 + try: + sandbox.run() + except SystemExit as e: + status = e.code + + self.assertEqual(status, 0) + quote_char = "'" + if getpreferredencoding().lower() == "utf-8": + quote_char = "\u00B4" + self.assertEqual(six.ensure_text(out.getvalue().strip()), quote_char) + + +class TestVersion(unittest.TestCase): + def test_version_simple(self): + v = Version("1") + self.assertEqual(v, "1") + self.assertLess(v, "2") + self.assertGreater(v, "0.5") + self.assertEqual(v.major, 1) + self.assertEqual(v.minor, 0) + self.assertEqual(v.patch, 0) + + def test_version_more(self): + v = Version("1.2.3b") + self.assertLess(v, "2") + self.assertEqual(v.major, 1) + self.assertEqual(v.minor, 2) + self.assertEqual(v.patch, 3) + + def test_version_bad(self): + # A version with a letter in the middle doesn't really make sense, + # so everything after it should be ignored. + v = Version("1.2b.3") + self.assertLess(v, "2") + self.assertEqual(v.major, 1) + self.assertEqual(v.minor, 2) + self.assertEqual(v.patch, 0) + + def test_version_badder(self): + v = Version("1b.2.3") + self.assertLess(v, "2") + self.assertEqual(v.major, 1) + self.assertEqual(v.minor, 0) + self.assertEqual(v.patch, 0) + + +class TestCheckCmdOutput(unittest.TestCase): + def get_result(self, command="", paths=None): + paths = paths or {} + config = {} + out = StringIO() + sandbox = ConfigureTestSandbox(paths, config, {}, ["/bin/configure"], out, out) + sandbox.include_file( + mozpath.join(topsrcdir, "build", "moz.configure", "util.configure") + ) + status = 0 + try: + exec(command, sandbox) + sandbox.run() + except SystemExit as e: + status = e.code + return config, out.getvalue(), status + + def test_simple_program(self): + def mock_simple_prog(_, args): + if len(args) == 1 and args[0] == "--help": + return 0, "simple program help...", "" + self.fail("Unexpected arguments to mock_simple_program: %s" % args) + + prog_path = mozpath.abspath("/simple/prog") + cmd = "log.info(check_cmd_output('%s', '--help'))" % prog_path + config, out, status = self.get_result(cmd, paths={prog_path: mock_simple_prog}) + self.assertEqual(config, {}) + self.assertEqual(status, 0) + self.assertEqual(out, "simple program help...\n") + + def test_failing_program(self): + def mock_error_prog(_, args): + if len(args) == 1 and args[0] == "--error": + return (127, "simple program output", "simple program error output") + self.fail("Unexpected arguments to mock_error_program: %s" % args) + + prog_path = mozpath.abspath("/simple/prog") + cmd = "log.info(check_cmd_output('%s', '--error'))" % prog_path + config, out, status = self.get_result(cmd, paths={prog_path: mock_error_prog}) + self.assertEqual(config, {}) + self.assertEqual(status, 1) + self.assertEqual( + out, + textwrap.dedent( + """\ + DEBUG: Executing: `%s --error` + DEBUG: The command returned non-zero exit status 127. + DEBUG: Its output was: + DEBUG: | simple program output + DEBUG: Its error output was: + DEBUG: | simple program error output + ERROR: Command `%s --error` failed with exit status 127. + """ + % (prog_path, prog_path) + ), + ) + + def test_error_callback(self): + def mock_error_prog(_, args): + if len(args) == 1 and args[0] == "--error": + return 127, "simple program error...", "" + self.fail("Unexpected arguments to mock_error_program: %s" % args) + + prog_path = mozpath.abspath("/simple/prog") + cmd = textwrap.dedent( + """\ + check_cmd_output('%s', '--error', + onerror=lambda: die('`prog` produced an error')) + """ + % prog_path + ) + config, out, status = self.get_result(cmd, paths={prog_path: mock_error_prog}) + self.assertEqual(config, {}) + self.assertEqual(status, 1) + self.assertEqual( + out, + textwrap.dedent( + """\ + DEBUG: Executing: `%s --error` + DEBUG: The command returned non-zero exit status 127. + DEBUG: Its output was: + DEBUG: | simple program error... + ERROR: `prog` produced an error + """ + % prog_path + ), + ) + + +if __name__ == "__main__": + main() diff --git a/python/mozbuild/mozbuild/test/controller/__init__.py b/python/mozbuild/mozbuild/test/controller/__init__.py new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/python/mozbuild/mozbuild/test/controller/__init__.py diff --git a/python/mozbuild/mozbuild/test/controller/test_ccachestats.py b/python/mozbuild/mozbuild/test/controller/test_ccachestats.py new file mode 100644 index 0000000000..f1efa78c3a --- /dev/null +++ b/python/mozbuild/mozbuild/test/controller/test_ccachestats.py @@ -0,0 +1,866 @@ +# 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/. + +import time +import unittest + +from mozunit import main + +from mozbuild.controller.building import CCacheStats + +TIMESTAMP = time.time() +TIMESTAMP2 = time.time() + 10 +TIMESTAMP_STR = time.strftime("%c", time.localtime(TIMESTAMP)) +TIMESTAMP2_STR = time.strftime("%c", time.localtime(TIMESTAMP2)) + + +class TestCcacheStats(unittest.TestCase): + STAT_GARBAGE = """A garbage line which should be failed to parse""" + + STAT0 = """ + cache directory /home/tlin/.ccache + cache hit (direct) 0 + cache hit (preprocessed) 0 + cache miss 0 + files in cache 0 + cache size 0 Kbytes + max cache size 16.0 Gbytes""" + + STAT1 = """ + cache directory /home/tlin/.ccache + cache hit (direct) 100 + cache hit (preprocessed) 200 + cache miss 2500 + called for link 180 + called for preprocessing 6 + compile failed 11 + preprocessor error 3 + bad compiler arguments 6 + unsupported source language 9 + autoconf compile/link 60 + unsupported compiler option 2 + no input file 21 + files in cache 7344 + cache size 1.9 Gbytes + max cache size 16.0 Gbytes""" + + STAT2 = """ + cache directory /home/tlin/.ccache + cache hit (direct) 1900 + cache hit (preprocessed) 300 + cache miss 2600 + called for link 361 + called for preprocessing 12 + compile failed 22 + preprocessor error 6 + bad compiler arguments 12 + unsupported source language 18 + autoconf compile/link 120 + unsupported compiler option 4 + no input file 48 + files in cache 7392 + cache size 2.0 Gbytes + max cache size 16.0 Gbytes""" + + STAT3 = """ + cache directory /Users/tlin/.ccache + primary config /Users/tlin/.ccache/ccache.conf + secondary config (readonly) /usr/local/Cellar/ccache/3.2/etc/ccache.conf + cache hit (direct) 12004 + cache hit (preprocessed) 1786 + cache miss 26348 + called for link 2338 + called for preprocessing 6313 + compile failed 399 + preprocessor error 390 + bad compiler arguments 86 + unsupported source language 66 + autoconf compile/link 2439 + unsupported compiler option 187 + no input file 1068 + files in cache 18044 + cache size 7.5 GB + max cache size 8.6 GB + """ + + STAT4 = """ + cache directory /Users/tlin/.ccache + primary config /Users/tlin/.ccache/ccache.conf + secondary config (readonly) /usr/local/Cellar/ccache/3.2.1/etc/ccache.conf + cache hit (direct) 21039 + cache hit (preprocessed) 2315 + cache miss 39370 + called for link 3651 + called for preprocessing 6693 + compile failed 723 + ccache internal error 1 + preprocessor error 588 + bad compiler arguments 128 + unsupported source language 99 + autoconf compile/link 3669 + unsupported compiler option 187 + no input file 1711 + files in cache 18313 + cache size 6.3 GB + max cache size 6.0 GB + """ + + STAT5 = """ + cache directory /Users/tlin/.ccache + primary config /Users/tlin/.ccache/ccache.conf + secondary config (readonly) /usr/local/Cellar/ccache/3.2.1/etc/ccache.conf + cache hit (direct) 21039 + cache hit (preprocessed) 2315 + cache miss 39372 + called for link 3653 + called for preprocessing 6693 + compile failed 723 + ccache internal error 1 + preprocessor error 588 + bad compiler arguments 128 + unsupported source language 99 + autoconf compile/link 3669 + unsupported compiler option 187 + no input file 1711 + files in cache 17411 + cache size 6.0 GB + max cache size 6.0 GB + """ + + STAT6 = """ + cache directory /Users/tlin/.ccache + primary config /Users/tlin/.ccache/ccache.conf + secondary config (readonly) /usr/local/Cellar/ccache/3.3.2/etc/ccache.conf + cache hit (direct) 319287 + cache hit (preprocessed) 125987 + cache miss 749959 + cache hit rate 37.25 % + called for link 87978 + called for preprocessing 418591 + multiple source files 1861 + compiler produced no output 122 + compiler produced empty output 174 + compile failed 14330 + ccache internal error 1 + preprocessor error 9459 + can't use precompiled header 4 + bad compiler arguments 2077 + unsupported source language 18195 + autoconf compile/link 51485 + unsupported compiler option 322 + no input file 309538 + cleanups performed 1 + files in cache 17358 + cache size 15.4 GB + max cache size 17.2 GB + """ + + STAT7 = """ + cache directory /Users/tlin/.ccache + primary config /Users/tlin/.ccache/ccache.conf + secondary config (readonly) /usr/local/Cellar/ccache/3.3.3/etc/ccache.conf + cache hit (direct) 27035 + cache hit (preprocessed) 13939 + cache miss 62630 + cache hit rate 39.55 % + called for link 1280 + called for preprocessing 736 + compile failed 550 + preprocessor error 638 + bad compiler arguments 20 + autoconf compile/link 1751 + unsupported code directive 2 + no input file 2378 + cleanups performed 1792 + files in cache 3479 + cache size 4.4 GB + max cache size 5.0 GB + """ + + # Substitute a locally-generated timestamp because the timestamp format is + # locale-dependent. + STAT8 = f""" + cache directory /home/psimonyi/.ccache + primary config /home/psimonyi/.ccache/ccache.conf + secondary config (readonly) /etc/ccache.conf + stats zero time {TIMESTAMP_STR} + cache hit (direct) 571 + cache hit (preprocessed) 1203 + cache miss 11747 + cache hit rate 13.12 % + called for link 623 + called for preprocessing 7194 + compile failed 32 + preprocessor error 137 + bad compiler arguments 4 + autoconf compile/link 348 + no input file 162 + cleanups performed 77 + files in cache 13464 + cache size 6.2 GB + max cache size 7.0 GB + """ + + STAT9 = f""" + cache directory /Users/tlin/.ccache + primary config /Users/tlin/.ccache/ccache.conf + secondary config (readonly) /usr/local/Cellar/ccache/3.5/etc/ccache.conf + stats updated {TIMESTAMP2_STR} + stats zeroed {TIMESTAMP_STR} + cache hit (direct) 80147 + cache hit (preprocessed) 21413 + cache miss 191128 + cache hit rate 34.70 % + called for link 5194 + called for preprocessing 1721 + compile failed 825 + preprocessor error 3838 + cache file missing 4863 + bad compiler arguments 32 + autoconf compile/link 3554 + unsupported code directive 4 + no input file 5545 + cleanups performed 3154 + files in cache 18525 + cache size 13.4 GB + max cache size 15.0 GB + """ + + VERSION_3_5_GIT = """ + ccache version 3.5.1+2_gf5309092_dirty + + Copyright (C) 2002-2007 Andrew Tridgell + Copyright (C) 2009-2019 Joel Rosdahl + + This program is free software; you can redistribute it and/or modify it under + the terms of the GNU General Public License as published by the Free Software + Foundation; either version 3 of the License, or (at your option) any later + version. + """ + + VERSION_4_2 = """ + ccache version 4.2.1 + + Copyright (C) 2002-2007 Andrew Tridgell + Copyright (C) 2009-2021 Joel Rosdahl and other contributors + + See <https://ccache.dev/credits.html> for a complete list of contributors. + + This program is free software; you can redistribute it and/or modify it under + the terms of the GNU General Public License as published by the Free Software + Foundation; either version 3 of the License, or (at your option) any later + version. + """ + + VERSION_4_4 = """ + ccache version 4.4 + Features: file-storage http-storage + + Copyright (C) 2002-2007 Andrew Tridgell + Copyright (C) 2009-2021 Joel Rosdahl and other contributors + + See <https://ccache.dev/credits.html> for a complete list of contributors. + + This program is free software; you can redistribute it and/or modify it under + the terms of the GNU General Public License as published by the Free Software + Foundation; either version 3 of the License, or (at your option) any later + version. + """ + + VERSION_4_4_2 = """ + ccache version 4.4.2 + Features: file-storage http-storage + + Copyright (C) 2002-2007 Andrew Tridgell + Copyright (C) 2009-2021 Joel Rosdahl and other contributors + + See <https://ccache.dev/credits.html> for a complete list of contributors. + + This program is free software; you can redistribute it and/or modify it under + the terms of the GNU General Public License as published by the Free Software + Foundation; either version 3 of the License, or (at your option) any later + version. + """ + + VERSION_4_5 = """ + ccache version 4.5.1 + Features: file-storage http-storage redis-storage + + Copyright (C) 2002-2007 Andrew Tridgell + Copyright (C) 2009-2021 Joel Rosdahl and other contributors + + See <https://ccache.dev/credits.html> for a complete list of contributors. + + This program is free software; you can redistribute it and/or modify it under + the terms of the GNU General Public License as published by the Free Software + Foundation; either version 3 of the License, or (at your option) any later + version. + """ + + STAT10 = f"""\ +stats_updated_timestamp\t{int(TIMESTAMP)} +stats_zeroed_timestamp\t0 +direct_cache_hit\t197 +preprocessed_cache_hit\t719 +cache_miss\t8427 +called_for_link\t569 +called_for_preprocessing\t110 +multiple_source_files\t0 +compiler_produced_stdout\t0 +compiler_produced_no_output\t0 +compiler_produced_empty_output\t0 +compile_failed\t49 +internal_error\t1 +preprocessor_error\t90 +could_not_use_precompiled_header\t0 +could_not_use_modules\t0 +could_not_find_compiler\t0 +missing_cache_file\t1 +bad_compiler_arguments\t6 +unsupported_source_language\t0 +compiler_check_failed\t0 +autoconf_test\t418 +unsupported_compiler_option\t0 +unsupported_code_directive\t1 +output_to_stdout\t0 +bad_output_file\t0 +no_input_file\t9 +error_hashing_extra_file\t0 +cleanups_performed\t161 +files_in_cache\t4425 +cache_size_kibibyte\t4624220 +""" + + STAT11 = f"""\ +stats_updated_timestamp\t{int(TIMESTAMP)} +stats_zeroed_timestamp\t{int(TIMESTAMP2)} +direct_cache_hit\t0 +preprocessed_cache_hit\t0 +cache_miss\t0 +called_for_link\t0 +called_for_preprocessing\t0 +multiple_source_files\t0 +compiler_produced_stdout\t0 +compiler_produced_no_output\t0 +compiler_produced_empty_output\t0 +compile_failed\t0 +internal_error\t0 +preprocessor_error\t0 +could_not_use_precompiled_header\t0 +could_not_use_modules\t0 +could_not_find_compiler\t0 +missing_cache_file\t0 +bad_compiler_arguments\t0 +unsupported_source_language\t0 +compiler_check_failed\t0 +autoconf_test\t0 +unsupported_compiler_option\t0 +unsupported_code_directive\t0 +output_to_stdout\t0 +bad_output_file\t0 +no_input_file\t0 +error_hashing_extra_file\t0 +cleanups_performed\t16 +files_in_cache\t0 +cache_size_kibibyte\t0 +""" + + STAT12 = """\ +stats_updated_timestamp\t0 +stats_zeroed_timestamp\t0 +direct_cache_hit\t0 +preprocessed_cache_hit\t0 +cache_miss\t0 +called_for_link\t0 +called_for_preprocessing\t0 +multiple_source_files\t0 +compiler_produced_stdout\t0 +compiler_produced_no_output\t0 +compiler_produced_empty_output\t0 +compile_failed\t0 +internal_error\t0 +preprocessor_error\t0 +could_not_use_precompiled_header\t0 +could_not_use_modules\t0 +could_not_find_compiler\t0 +missing_cache_file\t0 +bad_compiler_arguments\t0 +unsupported_source_language\t0 +compiler_check_failed\t0 +autoconf_test\t0 +unsupported_compiler_option\t0 +unsupported_code_directive\t0 +output_to_stdout\t0 +bad_output_file\t0 +no_input_file\t0 +error_hashing_extra_file\t0 +cleanups_performed\t16 +files_in_cache\t0 +cache_size_kibibyte\t0 +""" + + STAT13 = f"""\ +stats_updated_timestamp\t{int(TIMESTAMP)} +stats_zeroed_timestamp\t{int(TIMESTAMP2)} +direct_cache_hit\t280542 +preprocessed_cache_hit\t0 +cache_miss\t387653 +called_for_link\t0 +called_for_preprocessing\t0 +multiple_source_files\t0 +compiler_produced_stdout\t0 +compiler_produced_no_output\t0 +compiler_produced_empty_output\t0 +compile_failed\t1665 +internal_error\t1 +preprocessor_error\t0 +could_not_use_precompiled_header\t0 +could_not_use_modules\t0 +could_not_find_compiler\t0 +missing_cache_file\t0 +bad_compiler_arguments\t0 +unsupported_source_language\t0 +compiler_check_failed\t0 +autoconf_test\t0 +unsupported_compiler_option\t0 +unsupported_code_directive\t0 +output_to_stdout\t0 +bad_output_file\t0 +no_input_file\t2 +error_hashing_extra_file\t0 +cleanups_performed\t364 +files_in_cache\t335104 +cache_size_kibibyte\t18224250 +""" + + maxDiff = None + + def test_parse_garbage_stats_message(self): + self.assertRaises(ValueError, CCacheStats, self.STAT_GARBAGE) + + def test_parse_zero_stats_message(self): + stats = CCacheStats(self.STAT0) + self.assertEqual(stats.hit_rates(), (0, 0, 0)) + + def test_hit_rate_of_diff_stats(self): + stats1 = CCacheStats(self.STAT1) + stats2 = CCacheStats(self.STAT2) + stats_diff = stats2 - stats1 + self.assertEqual(stats_diff.hit_rates(), (0.9, 0.05, 0.05)) + + def test_stats_contains_data(self): + stats0 = CCacheStats(self.STAT0) + stats1 = CCacheStats(self.STAT1) + stats2 = CCacheStats(self.STAT2) + stats_diff_zero = stats1 - stats1 + stats_diff_negative1 = stats0 - stats1 + stats_diff_negative2 = stats1 - stats2 + + self.assertFalse(stats0) + self.assertTrue(stats1) + self.assertTrue(stats2) + self.assertFalse(stats_diff_zero) + self.assertFalse(stats_diff_negative1) + self.assertFalse(stats_diff_negative2) + + def test_stats_version32(self): + stat2 = CCacheStats(self.STAT2) + stat3 = CCacheStats(self.STAT3) + stats_diff = stat3 - stat2 + self.assertEqual( + str(stat3), + "cache hit (direct) 12004\n" + "cache hit (preprocessed) 1786\n" + "cache miss 26348\n" + "called for link 2338\n" + "called for preprocessing 6313\n" + "compile failed 399\n" + "preprocessor error 390\n" + "bad compiler arguments 86\n" + "unsupported source language 66\n" + "autoconf compile/link 2439\n" + "unsupported compiler option 187\n" + "no input file 1068\n" + "files in cache 18044\n" + "cache size 7.5 Gbytes\n" + "max cache size 8.6 Gbytes", + ) + self.assertEqual( + str(stats_diff), + "cache hit (direct) 10104\n" + "cache hit (preprocessed) 1486\n" + "cache miss 23748\n" + "called for link 1977\n" + "called for preprocessing 6301\n" + "compile failed 377\n" + "preprocessor error 384\n" + "bad compiler arguments 74\n" + "unsupported source language 48\n" + "autoconf compile/link 2319\n" + "unsupported compiler option 183\n" + "no input file 1020\n" + "files in cache 18044\n" + "cache size 7.5 Gbytes\n" + "max cache size 8.6 Gbytes", + ) + + def test_cache_size_shrinking(self): + stat4 = CCacheStats(self.STAT4) + stat5 = CCacheStats(self.STAT5) + stats_diff = stat5 - stat4 + self.assertEqual( + str(stat4), + "cache hit (direct) 21039\n" + "cache hit (preprocessed) 2315\n" + "cache miss 39370\n" + "called for link 3651\n" + "called for preprocessing 6693\n" + "compile failed 723\n" + "ccache internal error 1\n" + "preprocessor error 588\n" + "bad compiler arguments 128\n" + "unsupported source language 99\n" + "autoconf compile/link 3669\n" + "unsupported compiler option 187\n" + "no input file 1711\n" + "files in cache 18313\n" + "cache size 6.3 Gbytes\n" + "max cache size 6.0 Gbytes", + ) + self.assertEqual( + str(stat5), + "cache hit (direct) 21039\n" + "cache hit (preprocessed) 2315\n" + "cache miss 39372\n" + "called for link 3653\n" + "called for preprocessing 6693\n" + "compile failed 723\n" + "ccache internal error 1\n" + "preprocessor error 588\n" + "bad compiler arguments 128\n" + "unsupported source language 99\n" + "autoconf compile/link 3669\n" + "unsupported compiler option 187\n" + "no input file 1711\n" + "files in cache 17411\n" + "cache size 6.0 Gbytes\n" + "max cache size 6.0 Gbytes", + ) + self.assertEqual( + str(stats_diff), + "cache hit (direct) 0\n" + "cache hit (preprocessed) 0\n" + "cache miss 2\n" + "called for link 2\n" + "called for preprocessing 0\n" + "compile failed 0\n" + "ccache internal error 0\n" + "preprocessor error 0\n" + "bad compiler arguments 0\n" + "unsupported source language 0\n" + "autoconf compile/link 0\n" + "unsupported compiler option 0\n" + "no input file 0\n" + "files in cache 17411\n" + "cache size 6.0 Gbytes\n" + "max cache size 6.0 Gbytes", + ) + + def test_stats_version33(self): + # Test stats for 3.3.2. + stat3 = CCacheStats(self.STAT3) + stat6 = CCacheStats(self.STAT6) + stats_diff = stat6 - stat3 + self.assertEqual( + str(stat6), + "cache hit (direct) 319287\n" + "cache hit (preprocessed) 125987\n" + "cache hit rate 37\n" + "cache miss 749959\n" + "called for link 87978\n" + "called for preprocessing 418591\n" + "multiple source files 1861\n" + "compiler produced no output 122\n" + "compiler produced empty output 174\n" + "compile failed 14330\n" + "ccache internal error 1\n" + "preprocessor error 9459\n" + "can't use precompiled header 4\n" + "bad compiler arguments 2077\n" + "unsupported source language 18195\n" + "autoconf compile/link 51485\n" + "unsupported compiler option 322\n" + "no input file 309538\n" + "cleanups performed 1\n" + "files in cache 17358\n" + "cache size 15.4 Gbytes\n" + "max cache size 17.2 Gbytes", + ) + self.assertEqual( + str(stat3), + "cache hit (direct) 12004\n" + "cache hit (preprocessed) 1786\n" + "cache miss 26348\n" + "called for link 2338\n" + "called for preprocessing 6313\n" + "compile failed 399\n" + "preprocessor error 390\n" + "bad compiler arguments 86\n" + "unsupported source language 66\n" + "autoconf compile/link 2439\n" + "unsupported compiler option 187\n" + "no input file 1068\n" + "files in cache 18044\n" + "cache size 7.5 Gbytes\n" + "max cache size 8.6 Gbytes", + ) + self.assertEqual( + str(stats_diff), + "cache hit (direct) 307283\n" + "cache hit (preprocessed) 124201\n" + "cache hit rate 37\n" + "cache miss 723611\n" + "called for link 85640\n" + "called for preprocessing 412278\n" + "multiple source files 1861\n" + "compiler produced no output 122\n" + "compiler produced empty output 174\n" + "compile failed 13931\n" + "ccache internal error 1\n" + "preprocessor error 9069\n" + "can't use precompiled header 4\n" + "bad compiler arguments 1991\n" + "unsupported source language 18129\n" + "autoconf compile/link 49046\n" + "unsupported compiler option 135\n" + "no input file 308470\n" + "cleanups performed 1\n" + "files in cache 17358\n" + "cache size 15.4 Gbytes\n" + "max cache size 17.2 Gbytes", + ) + + # Test stats for 3.3.3. + stat7 = CCacheStats(self.STAT7) + self.assertEqual( + str(stat7), + "cache hit (direct) 27035\n" + "cache hit (preprocessed) 13939\n" + "cache hit rate 39\n" + "cache miss 62630\n" + "called for link 1280\n" + "called for preprocessing 736\n" + "compile failed 550\n" + "preprocessor error 638\n" + "bad compiler arguments 20\n" + "autoconf compile/link 1751\n" + "unsupported code directive 2\n" + "no input file 2378\n" + "cleanups performed 1792\n" + "files in cache 3479\n" + "cache size 4.4 Gbytes\n" + "max cache size 5.0 Gbytes", + ) + + def test_stats_version34(self): + # Test parsing 3.4 output. + stat8 = CCacheStats(self.STAT8) + self.assertEqual( + str(stat8), + f"stats zeroed {int(TIMESTAMP)}\n" + "cache hit (direct) 571\n" + "cache hit (preprocessed) 1203\n" + "cache hit rate 13\n" + "cache miss 11747\n" + "called for link 623\n" + "called for preprocessing 7194\n" + "compile failed 32\n" + "preprocessor error 137\n" + "bad compiler arguments 4\n" + "autoconf compile/link 348\n" + "no input file 162\n" + "cleanups performed 77\n" + "files in cache 13464\n" + "cache size 6.2 Gbytes\n" + "max cache size 7.0 Gbytes", + ) + + def test_stats_version35(self): + # Test parsing 3.5 output. + stat9 = CCacheStats(self.STAT9) + self.assertEqual( + str(stat9), + f"stats zeroed {int(TIMESTAMP)}\n" + f"stats updated {int(TIMESTAMP2)}\n" + "cache hit (direct) 80147\n" + "cache hit (preprocessed) 21413\n" + "cache hit rate 34\n" + "cache miss 191128\n" + "called for link 5194\n" + "called for preprocessing 1721\n" + "compile failed 825\n" + "preprocessor error 3838\n" + "cache file missing 4863\n" + "bad compiler arguments 32\n" + "autoconf compile/link 3554\n" + "unsupported code directive 4\n" + "no input file 5545\n" + "cleanups performed 3154\n" + "files in cache 18525\n" + "cache size 13.4 Gbytes\n" + "max cache size 15.0 Gbytes", + ) + + def test_stats_version37(self): + # verify version checks + self.assertFalse(CCacheStats._is_version_3_7_or_newer(self.VERSION_3_5_GIT)) + self.assertTrue(CCacheStats._is_version_3_7_or_newer(self.VERSION_4_2)) + self.assertTrue(CCacheStats._is_version_3_7_or_newer(self.VERSION_4_4)) + self.assertTrue(CCacheStats._is_version_3_7_or_newer(self.VERSION_4_4_2)) + self.assertTrue(CCacheStats._is_version_3_7_or_newer(self.VERSION_4_5)) + + # Test parsing 3.7+ output. + stat10 = CCacheStats(self.STAT10, True) + self.assertEqual( + str(stat10), + "stats zeroed 0\n" + f"stats updated {int(TIMESTAMP)}\n" + "cache hit (direct) 197\n" + "cache hit (preprocessed) 719\n" + "cache hit rate 9\n" + "cache miss 8427\n" + "called for link 569\n" + "called for preprocessing 110\n" + "multiple source files 0\n" + "compiler produced stdout 0\n" + "compiler produced no output 0\n" + "compiler produced empty output 0\n" + "compile failed 49\n" + "ccache internal error 1\n" + "preprocessor error 90\n" + "can't use precompiled header 0\n" + "couldn't find the compiler 0\n" + "cache file missing 1\n" + "bad compiler arguments 6\n" + "unsupported source language 0\n" + "compiler check failed 0\n" + "autoconf compile/link 418\n" + "unsupported code directive 1\n" + "unsupported compiler option 0\n" + "output to stdout 0\n" + "no input file 9\n" + "error hashing extra file 0\n" + "cleanups performed 161\n" + "files in cache 4425\n" + "cache size 4.4 Gbytes", + ) + + stat11 = CCacheStats(self.STAT11, True) + self.assertEqual( + str(stat11), + f"stats zeroed {int(TIMESTAMP2)}\n" + f"stats updated {int(TIMESTAMP)}\n" + "cache hit (direct) 0\n" + "cache hit (preprocessed) 0\n" + "cache hit rate 0\n" + "cache miss 0\n" + "called for link 0\n" + "called for preprocessing 0\n" + "multiple source files 0\n" + "compiler produced stdout 0\n" + "compiler produced no output 0\n" + "compiler produced empty output 0\n" + "compile failed 0\n" + "ccache internal error 0\n" + "preprocessor error 0\n" + "can't use precompiled header 0\n" + "couldn't find the compiler 0\n" + "cache file missing 0\n" + "bad compiler arguments 0\n" + "unsupported source language 0\n" + "compiler check failed 0\n" + "autoconf compile/link 0\n" + "unsupported code directive 0\n" + "unsupported compiler option 0\n" + "output to stdout 0\n" + "no input file 0\n" + "error hashing extra file 0\n" + "cleanups performed 16\n" + "files in cache 0\n" + "cache size 0.0 Kbytes", + ) + + stat12 = CCacheStats(self.STAT12, True) + self.assertEqual( + str(stat12), + "stats zeroed 0\n" + "stats updated 0\n" + "cache hit (direct) 0\n" + "cache hit (preprocessed) 0\n" + "cache hit rate 0\n" + "cache miss 0\n" + "called for link 0\n" + "called for preprocessing 0\n" + "multiple source files 0\n" + "compiler produced stdout 0\n" + "compiler produced no output 0\n" + "compiler produced empty output 0\n" + "compile failed 0\n" + "ccache internal error 0\n" + "preprocessor error 0\n" + "can't use precompiled header 0\n" + "couldn't find the compiler 0\n" + "cache file missing 0\n" + "bad compiler arguments 0\n" + "unsupported source language 0\n" + "compiler check failed 0\n" + "autoconf compile/link 0\n" + "unsupported code directive 0\n" + "unsupported compiler option 0\n" + "output to stdout 0\n" + "no input file 0\n" + "error hashing extra file 0\n" + "cleanups performed 16\n" + "files in cache 0\n" + "cache size 0.0 Kbytes", + ) + + stat13 = CCacheStats(self.STAT13, True) + self.assertEqual( + str(stat13), + f"stats zeroed {int(TIMESTAMP2)}\n" + f"stats updated {int(TIMESTAMP)}\n" + "cache hit (direct) 280542\n" + "cache hit (preprocessed) 0\n" + "cache hit rate 41\n" + "cache miss 387653\n" + "called for link 0\n" + "called for preprocessing 0\n" + "multiple source files 0\n" + "compiler produced stdout 0\n" + "compiler produced no output 0\n" + "compiler produced empty output 0\n" + "compile failed 1665\n" + "ccache internal error 1\n" + "preprocessor error 0\n" + "can't use precompiled header 0\n" + "couldn't find the compiler 0\n" + "cache file missing 0\n" + "bad compiler arguments 0\n" + "unsupported source language 0\n" + "compiler check failed 0\n" + "autoconf compile/link 0\n" + "unsupported code directive 0\n" + "unsupported compiler option 0\n" + "output to stdout 0\n" + "no input file 2\n" + "error hashing extra file 0\n" + "cleanups performed 364\n" + "files in cache 335104\n" + "cache size 17.4 Gbytes", + ) + + +if __name__ == "__main__": + main() diff --git a/python/mozbuild/mozbuild/test/controller/test_clobber.py b/python/mozbuild/mozbuild/test/controller/test_clobber.py new file mode 100644 index 0000000000..fff3c5a438 --- /dev/null +++ b/python/mozbuild/mozbuild/test/controller/test_clobber.py @@ -0,0 +1,214 @@ +# 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/. + +import os +import shutil +import tempfile +import unittest + +from mozunit import main + +from mozbuild.base import MozbuildObject +from mozbuild.controller.building import BuildDriver +from mozbuild.controller.clobber import Clobberer +from mozbuild.test.common import prepare_tmp_topsrcdir + + +class TestClobberer(unittest.TestCase): + def setUp(self): + self._temp_dirs = [] + self._old_env = dict(os.environ) + os.environ.pop("MOZCONFIG", None) + os.environ.pop("MOZ_OBJDIR", None) + + return unittest.TestCase.setUp(self) + + def tearDown(self): + os.environ.clear() + os.environ.update(self._old_env) + + for d in self._temp_dirs: + shutil.rmtree(d, ignore_errors=True) + + return unittest.TestCase.tearDown(self) + + def get_tempdir(self): + t = tempfile.mkdtemp() + self._temp_dirs.append(t) + return t + + def get_topsrcdir(self): + t = self.get_tempdir() + prepare_tmp_topsrcdir(t) + p = os.path.join(t, "CLOBBER") + with open(p, "a"): + pass + + return t + + def test_no_objdir(self): + """If topobjdir does not exist, no clobber is needed.""" + + tmp = os.path.join(self.get_tempdir(), "topobjdir") + self.assertFalse(os.path.exists(tmp)) + + c = Clobberer(self.get_topsrcdir(), tmp) + self.assertFalse(c.clobber_needed()) + + required, performed, reason = c.maybe_do_clobber(os.getcwd(), True) + self.assertFalse(required) + self.assertFalse(performed) + self.assertIsNone(reason) + + self.assertFalse(os.path.isdir(tmp)) + self.assertFalse(os.path.exists(os.path.join(tmp, "CLOBBER"))) + + def test_objdir_no_clobber_file(self): + """If CLOBBER does not exist in topobjdir, treat as empty.""" + + c = Clobberer(self.get_topsrcdir(), self.get_tempdir()) + self.assertFalse(c.clobber_needed()) + + required, performed, reason = c.maybe_do_clobber(os.getcwd(), True) + self.assertFalse(required) + self.assertFalse(performed) + self.assertIsNone(reason) + + self.assertFalse(os.path.exists(os.path.join(c.topobjdir, "CLOBBER"))) + + def test_objdir_clobber_newer(self): + """If CLOBBER in topobjdir is newer, do nothing.""" + + c = Clobberer(self.get_topsrcdir(), self.get_tempdir()) + with open(c.obj_clobber, "a"): + pass + + required, performed, reason = c.maybe_do_clobber(os.getcwd(), True) + self.assertFalse(required) + self.assertFalse(performed) + self.assertIsNone(reason) + + def test_objdir_clobber_older(self): + """If CLOBBER in topobjdir is older, we clobber.""" + + c = Clobberer(self.get_topsrcdir(), self.get_tempdir()) + with open(c.obj_clobber, "a"): + pass + + dummy_path = os.path.join(c.topobjdir, "foo") + with open(dummy_path, "a"): + pass + + self.assertTrue(os.path.exists(dummy_path)) + + old_time = os.path.getmtime(c.src_clobber) - 60 + os.utime(c.obj_clobber, (old_time, old_time)) + + self.assertTrue(c.clobber_needed()) + + required, performed, reason = c.maybe_do_clobber(os.getcwd(), True) + self.assertTrue(required) + self.assertTrue(performed) + + self.assertFalse(os.path.exists(dummy_path)) + self.assertFalse(os.path.exists(c.obj_clobber)) + + def test_objdir_is_srcdir(self): + """If topobjdir is the topsrcdir, refuse to clobber.""" + + tmp = self.get_topsrcdir() + c = Clobberer(tmp, tmp) + + self.assertFalse(c.clobber_needed()) + + def test_cwd_is_topobjdir(self): + """If cwd is topobjdir, we can still clobber.""" + c = Clobberer(self.get_topsrcdir(), self.get_tempdir()) + + with open(c.obj_clobber, "a"): + pass + + dummy_file = os.path.join(c.topobjdir, "dummy_file") + with open(dummy_file, "a"): + pass + + dummy_dir = os.path.join(c.topobjdir, "dummy_dir") + os.mkdir(dummy_dir) + + self.assertTrue(os.path.exists(dummy_file)) + self.assertTrue(os.path.isdir(dummy_dir)) + + old_time = os.path.getmtime(c.src_clobber) - 60 + os.utime(c.obj_clobber, (old_time, old_time)) + + self.assertTrue(c.clobber_needed()) + + required, performed, reason = c.maybe_do_clobber(c.topobjdir, True) + self.assertTrue(required) + self.assertTrue(performed) + + self.assertFalse(os.path.exists(dummy_file)) + self.assertFalse(os.path.exists(dummy_dir)) + + def test_cwd_under_topobjdir(self): + """If cwd is under topobjdir, we can't clobber.""" + + c = Clobberer(self.get_topsrcdir(), self.get_tempdir()) + + with open(c.obj_clobber, "a"): + pass + + old_time = os.path.getmtime(c.src_clobber) - 60 + os.utime(c.obj_clobber, (old_time, old_time)) + + d = os.path.join(c.topobjdir, "dummy_dir") + os.mkdir(d) + + required, performed, reason = c.maybe_do_clobber(d, True) + self.assertTrue(required) + self.assertFalse(performed) + self.assertIn("Cannot clobber while the shell is inside", reason) + + def test_mozconfig_opt_in(self): + """Auto clobber iff AUTOCLOBBER is in the environment.""" + + topsrcdir = self.get_topsrcdir() + topobjdir = self.get_tempdir() + + obj_clobber = os.path.join(topobjdir, "CLOBBER") + with open(obj_clobber, "a"): + pass + + dummy_file = os.path.join(topobjdir, "dummy_file") + with open(dummy_file, "a"): + pass + + self.assertTrue(os.path.exists(dummy_file)) + + old_time = os.path.getmtime(os.path.join(topsrcdir, "CLOBBER")) - 60 + os.utime(obj_clobber, (old_time, old_time)) + + # Check auto clobber is off by default + env = dict(os.environ) + if env.get("AUTOCLOBBER", False): + del env["AUTOCLOBBER"] + + mbo = MozbuildObject(topsrcdir, None, None, topobjdir) + build = mbo._spawn(BuildDriver) + + status = build._check_clobber(build.mozconfig, env) + + self.assertEqual(status, True) + self.assertTrue(os.path.exists(dummy_file)) + + # Check auto clobber opt-in works + env["AUTOCLOBBER"] = "1" + + status = build._check_clobber(build.mozconfig, env) + self.assertFalse(status) + self.assertFalse(os.path.exists(dummy_file)) + + +if __name__ == "__main__": + main() diff --git a/python/mozbuild/mozbuild/test/data/Makefile b/python/mozbuild/mozbuild/test/data/Makefile new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/python/mozbuild/mozbuild/test/data/Makefile diff --git a/python/mozbuild/mozbuild/test/data/bad.properties b/python/mozbuild/mozbuild/test/data/bad.properties new file mode 100644 index 0000000000..d4d8109b69 --- /dev/null +++ b/python/mozbuild/mozbuild/test/data/bad.properties @@ -0,0 +1,12 @@ +# A region.properties file with invalid unicode byte sequences. The +# sequences were cribbed from Markus Kuhn's "UTF-8 decoder capability +# and stress test", available at +# http://www.cl.cam.ac.uk/~mgk25/ucs/examples/UTF-8-test.txt + +# 3.5 Impossible bytes | +# | +# The following two bytes cannot appear in a correct UTF-8 string | +# | +# 3.5.1 fe = "þ" | +# 3.5.2 ff = "ÿ" | +# 3.5.3 fe fe ff ff = "þþÿÿ" | diff --git a/python/mozbuild/mozbuild/test/data/test-dir/Makefile b/python/mozbuild/mozbuild/test/data/test-dir/Makefile new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/python/mozbuild/mozbuild/test/data/test-dir/Makefile diff --git a/python/mozbuild/mozbuild/test/data/test-dir/with/Makefile b/python/mozbuild/mozbuild/test/data/test-dir/with/Makefile new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/python/mozbuild/mozbuild/test/data/test-dir/with/Makefile diff --git a/python/mozbuild/mozbuild/test/data/test-dir/with/without/with/Makefile b/python/mozbuild/mozbuild/test/data/test-dir/with/without/with/Makefile new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/python/mozbuild/mozbuild/test/data/test-dir/with/without/with/Makefile diff --git a/python/mozbuild/mozbuild/test/data/test-dir/without/with/Makefile b/python/mozbuild/mozbuild/test/data/test-dir/without/with/Makefile new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/python/mozbuild/mozbuild/test/data/test-dir/without/with/Makefile diff --git a/python/mozbuild/mozbuild/test/data/valid.properties b/python/mozbuild/mozbuild/test/data/valid.properties new file mode 100644 index 0000000000..db64bf2eed --- /dev/null +++ b/python/mozbuild/mozbuild/test/data/valid.properties @@ -0,0 +1,11 @@ +# A region.properties file with unicode characters. + +# Danish. +# #### ~~ Søren Munk Skrøder, sskroeder - 2009-05-30 @ #mozmae + +# Korean. +A.title=í•œë©”ì¼ + +# Russian. +list.0 = test +list.1 = Ð¯Ð½Ð´ÐµÐºÑ diff --git a/python/mozbuild/mozbuild/test/frontend/__init__.py b/python/mozbuild/mozbuild/test/frontend/__init__.py new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/__init__.py diff --git a/python/mozbuild/mozbuild/test/frontend/data/allow-compiler-warnings/moz.build b/python/mozbuild/mozbuild/test/frontend/data/allow-compiler-warnings/moz.build new file mode 100644 index 0000000000..0bf5b55ecb --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/allow-compiler-warnings/moz.build @@ -0,0 +1,20 @@ +# Any copyright is dedicated to the Public Domain. +# http://creativecommons.org/publicdomain/zero/1.0/ + + +@template +def AllowCompilerWarnings(): + COMPILE_FLAGS["WARNINGS_AS_ERRORS"] = [] + + +@template +def Library(name): + """Template for libraries.""" + LIBRARY_NAME = name + + +Library("dummy") + +UNIFIED_SOURCES += ["test1.c"] + +AllowCompilerWarnings() diff --git a/python/mozbuild/mozbuild/test/frontend/data/allow-compiler-warnings/test1.c b/python/mozbuild/mozbuild/test/frontend/data/allow-compiler-warnings/test1.c new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/allow-compiler-warnings/test1.c diff --git a/python/mozbuild/mozbuild/test/frontend/data/asflags/moz.build b/python/mozbuild/mozbuild/test/frontend/data/asflags/moz.build new file mode 100644 index 0000000000..80f48a7d81 --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/asflags/moz.build @@ -0,0 +1,15 @@ +# Any copyright is dedicated to the Public Domain. +# http://creativecommons.org/publicdomain/zero/1.0/ + + +@template +def Library(name): + """Template for libraries.""" + LIBRARY_NAME = name + + +Library("dummy") + +SOURCES += ["test1.c", "test2.S"] + +ASFLAGS += ["-no-integrated-as"] diff --git a/python/mozbuild/mozbuild/test/frontend/data/asflags/test1.c b/python/mozbuild/mozbuild/test/frontend/data/asflags/test1.c new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/asflags/test1.c diff --git a/python/mozbuild/mozbuild/test/frontend/data/asflags/test2.S b/python/mozbuild/mozbuild/test/frontend/data/asflags/test2.S new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/asflags/test2.S diff --git a/python/mozbuild/mozbuild/test/frontend/data/branding-files/bar.ico b/python/mozbuild/mozbuild/test/frontend/data/branding-files/bar.ico new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/branding-files/bar.ico diff --git a/python/mozbuild/mozbuild/test/frontend/data/branding-files/baz.png b/python/mozbuild/mozbuild/test/frontend/data/branding-files/baz.png new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/branding-files/baz.png diff --git a/python/mozbuild/mozbuild/test/frontend/data/branding-files/foo.xpm b/python/mozbuild/mozbuild/test/frontend/data/branding-files/foo.xpm new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/branding-files/foo.xpm diff --git a/python/mozbuild/mozbuild/test/frontend/data/branding-files/moz.build b/python/mozbuild/mozbuild/test/frontend/data/branding-files/moz.build new file mode 100644 index 0000000000..65f22d578b --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/branding-files/moz.build @@ -0,0 +1,12 @@ +# Any copyright is dedicated to the Public Domain. +# http://creativecommons.org/publicdomain/zero/1.0/ + +BRANDING_FILES += [ + "bar.ico", + "baz.png", + "foo.xpm", +] + +BRANDING_FILES.icons += [ + "quux.icns", +] diff --git a/python/mozbuild/mozbuild/test/frontend/data/branding-files/quux.icns b/python/mozbuild/mozbuild/test/frontend/data/branding-files/quux.icns new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/branding-files/quux.icns diff --git a/python/mozbuild/mozbuild/test/frontend/data/compile-defines/moz.build b/python/mozbuild/mozbuild/test/frontend/data/compile-defines/moz.build new file mode 100644 index 0000000000..65d71dae2b --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/compile-defines/moz.build @@ -0,0 +1,16 @@ +# Any copyright is dedicated to the Public Domain. +# http://creativecommons.org/publicdomain/zero/1.0/ + + +@template +def Library(name): + """Template for libraries.""" + LIBRARY_NAME = name + + +Library("dummy") + +UNIFIED_SOURCES += ["test1.c"] + +DEFINES["MOZ_TEST_DEFINE"] = True +LIBRARY_DEFINES["MOZ_LIBRARY_DEFINE"] = "MOZ_TEST" diff --git a/python/mozbuild/mozbuild/test/frontend/data/compile-defines/test1.c b/python/mozbuild/mozbuild/test/frontend/data/compile-defines/test1.c new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/compile-defines/test1.c diff --git a/python/mozbuild/mozbuild/test/frontend/data/compile-flags-field-validation/moz.build b/python/mozbuild/mozbuild/test/frontend/data/compile-flags-field-validation/moz.build new file mode 100644 index 0000000000..70622bc4e1 --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/compile-flags-field-validation/moz.build @@ -0,0 +1,15 @@ +# Any copyright is dedicated to the Public Domain. +# http://creativecommons.org/publicdomain/zero/1.0/ + + +@template +def Library(name): + """Template for libraries.""" + LIBRARY_NAME = name + + +Library("dummy") + +COMPILE_FLAGS["STL_FLAGS"] = [] + +UNIFIED_SOURCES += ["test1.c"] diff --git a/python/mozbuild/mozbuild/test/frontend/data/compile-flags-field-validation/test1.c b/python/mozbuild/mozbuild/test/frontend/data/compile-flags-field-validation/test1.c new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/compile-flags-field-validation/test1.c diff --git a/python/mozbuild/mozbuild/test/frontend/data/compile-flags-templates/moz.build b/python/mozbuild/mozbuild/test/frontend/data/compile-flags-templates/moz.build new file mode 100644 index 0000000000..6e611fc598 --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/compile-flags-templates/moz.build @@ -0,0 +1,27 @@ +# Any copyright is dedicated to the Public Domain. +# http://creativecommons.org/publicdomain/zero/1.0/ + + +@template +def Library(name): + """Template for libraries.""" + LIBRARY_NAME = name + + +Library("dummy") + + +@template +def DisableStlWrapping(): + COMPILE_FLAGS["STL"] = [] + + +@template +def NoVisibilityFlags(): + COMPILE_FLAGS["VISIBILITY"] = [] + + +UNIFIED_SOURCES += ["test1.c"] + +DisableStlWrapping() +NoVisibilityFlags() diff --git a/python/mozbuild/mozbuild/test/frontend/data/compile-flags-templates/test1.c b/python/mozbuild/mozbuild/test/frontend/data/compile-flags-templates/test1.c new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/compile-flags-templates/test1.c diff --git a/python/mozbuild/mozbuild/test/frontend/data/compile-flags-type-validation/moz.build b/python/mozbuild/mozbuild/test/frontend/data/compile-flags-type-validation/moz.build new file mode 100644 index 0000000000..31094736a7 --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/compile-flags-type-validation/moz.build @@ -0,0 +1,15 @@ +# Any copyright is dedicated to the Public Domain. +# http://creativecommons.org/publicdomain/zero/1.0/ + + +@template +def Library(name): + """Template for libraries.""" + LIBRARY_NAME = name + + +Library("dummy") + +COMPILE_FLAGS["STL"] = [None, 123] + +UNIFIED_SOURCES += ["test1.c"] diff --git a/python/mozbuild/mozbuild/test/frontend/data/compile-flags-type-validation/test1.c b/python/mozbuild/mozbuild/test/frontend/data/compile-flags-type-validation/test1.c new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/compile-flags-type-validation/test1.c diff --git a/python/mozbuild/mozbuild/test/frontend/data/compile-flags/moz.build b/python/mozbuild/mozbuild/test/frontend/data/compile-flags/moz.build new file mode 100644 index 0000000000..0e6f75cfa1 --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/compile-flags/moz.build @@ -0,0 +1,22 @@ +# Any copyright is dedicated to the Public Domain. +# http://creativecommons.org/publicdomain/zero/1.0/ + + +@template +def Library(name): + """Template for libraries.""" + LIBRARY_NAME = name + + +Library("dummy") + + +@template +def DisableStlWrapping(): + COMPILE_FLAGS["STL"] = [] + + +UNIFIED_SOURCES += ["test1.c"] + +CXXFLAGS += ["-funroll-loops", "-Wall"] +CFLAGS += ["-Wall", "-funroll-loops"] diff --git a/python/mozbuild/mozbuild/test/frontend/data/compile-flags/test1.c b/python/mozbuild/mozbuild/test/frontend/data/compile-flags/test1.c new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/compile-flags/test1.c diff --git a/python/mozbuild/mozbuild/test/frontend/data/compile-includes/moz.build b/python/mozbuild/mozbuild/test/frontend/data/compile-includes/moz.build new file mode 100644 index 0000000000..10c28e2833 --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/compile-includes/moz.build @@ -0,0 +1,15 @@ +# Any copyright is dedicated to the Public Domain. +# http://creativecommons.org/publicdomain/zero/1.0/ + + +@template +def Library(name): + """Template for libraries.""" + LIBRARY_NAME = name + + +Library("dummy") + +UNIFIED_SOURCES += ["test1.c"] + +LOCAL_INCLUDES += ["subdir"] diff --git a/python/mozbuild/mozbuild/test/frontend/data/compile-includes/subdir/header.h b/python/mozbuild/mozbuild/test/frontend/data/compile-includes/subdir/header.h new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/compile-includes/subdir/header.h diff --git a/python/mozbuild/mozbuild/test/frontend/data/compile-includes/test1.c b/python/mozbuild/mozbuild/test/frontend/data/compile-includes/test1.c new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/compile-includes/test1.c diff --git a/python/mozbuild/mozbuild/test/frontend/data/config-file-substitution/moz.build b/python/mozbuild/mozbuild/test/frontend/data/config-file-substitution/moz.build new file mode 100644 index 0000000000..f42dc0a517 --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/config-file-substitution/moz.build @@ -0,0 +1,6 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# Any copyright is dedicated to the Public Domain. +# http://creativecommons.org/publicdomain/zero/1.0/ + +CONFIGURE_SUBST_FILES += ["foo"] +CONFIGURE_SUBST_FILES += ["bar"] diff --git a/python/mozbuild/mozbuild/test/frontend/data/crate-dependency-path-resolution/Cargo.toml b/python/mozbuild/mozbuild/test/frontend/data/crate-dependency-path-resolution/Cargo.toml new file mode 100644 index 0000000000..3bbf79e0c3 --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/crate-dependency-path-resolution/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "random-crate" +version = "0.1.0" +authors = [ + "The Mozilla Project Developers", +] + +[lib] +crate-type = ["staticlib"] + +[dependencies] +deep-crate = { version = "0.1.0", path = "the/depths" } +mozilla-central-workspace-hack = { version = "0.1", features = ["random-crate"], optional = true } + +[profile.dev] +panic = "abort" + +[profile.release] +panic = "abort" diff --git a/python/mozbuild/mozbuild/test/frontend/data/crate-dependency-path-resolution/moz.build b/python/mozbuild/mozbuild/test/frontend/data/crate-dependency-path-resolution/moz.build new file mode 100644 index 0000000000..de1967c519 --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/crate-dependency-path-resolution/moz.build @@ -0,0 +1,19 @@ +# Any copyright is dedicated to the Public Domain. +# http://creativecommons.org/publicdomain/zero/1.0/ + + +@template +def Library(name): + """Template for libraries.""" + LIBRARY_NAME = name + + +@template +def RustLibrary(name): + """Template for Rust libraries.""" + Library(name) + + IS_RUST_LIBRARY = True + + +RustLibrary("random-crate") diff --git a/python/mozbuild/mozbuild/test/frontend/data/crate-dependency-path-resolution/shallow/Cargo.toml b/python/mozbuild/mozbuild/test/frontend/data/crate-dependency-path-resolution/shallow/Cargo.toml new file mode 100644 index 0000000000..e918f9228d --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/crate-dependency-path-resolution/shallow/Cargo.toml @@ -0,0 +1,6 @@ +[package] +name = "shallow-crate" +version = "0.1.0" +authors = [ + "The Mozilla Project Developers", +] diff --git a/python/mozbuild/mozbuild/test/frontend/data/crate-dependency-path-resolution/the/depths/Cargo.toml b/python/mozbuild/mozbuild/test/frontend/data/crate-dependency-path-resolution/the/depths/Cargo.toml new file mode 100644 index 0000000000..cebcb38ab7 --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/crate-dependency-path-resolution/the/depths/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "deep-crate" +version = "0.1.0" +authors = [ + "The Mozilla Project Developers", +] + +[dependencies] +shallow-crate = { path = "../../shallow" } diff --git a/python/mozbuild/mozbuild/test/frontend/data/defines/moz.build b/python/mozbuild/mozbuild/test/frontend/data/defines/moz.build new file mode 100644 index 0000000000..6085619c58 --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/defines/moz.build @@ -0,0 +1,9 @@ +# Any copyright is dedicated to the Public Domain. +# http://creativecommons.org/publicdomain/zero/1.0/ + +value = "xyz" +DEFINES["FOO"] = True +DEFINES["BAZ"] = '"abcd"' +DEFINES["BAR"] = 7 +DEFINES["VALUE"] = value +DEFINES["QUX"] = False diff --git a/python/mozbuild/mozbuild/test/frontend/data/disable-compiler-warnings/moz.build b/python/mozbuild/mozbuild/test/frontend/data/disable-compiler-warnings/moz.build new file mode 100644 index 0000000000..064fa09893 --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/disable-compiler-warnings/moz.build @@ -0,0 +1,20 @@ +# Any copyright is dedicated to the Public Domain. +# http://creativecommons.org/publicdomain/zero/1.0/ + + +@template +def DisableCompilerWarnings(): + COMPILE_FLAGS["WARNINGS_CFLAGS"] = [] + + +@template +def Library(name): + """Template for libraries.""" + LIBRARY_NAME = name + + +Library("dummy") + +UNIFIED_SOURCES += ["test1.c"] + +DisableCompilerWarnings() diff --git a/python/mozbuild/mozbuild/test/frontend/data/disable-compiler-warnings/test1.c b/python/mozbuild/mozbuild/test/frontend/data/disable-compiler-warnings/test1.c new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/disable-compiler-warnings/test1.c diff --git a/python/mozbuild/mozbuild/test/frontend/data/disable-stl-wrapping/moz.build b/python/mozbuild/mozbuild/test/frontend/data/disable-stl-wrapping/moz.build new file mode 100644 index 0000000000..40cb3e7781 --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/disable-stl-wrapping/moz.build @@ -0,0 +1,21 @@ +# Any copyright is dedicated to the Public Domain. +# http://creativecommons.org/publicdomain/zero/1.0/ + + +@template +def Library(name): + """Template for libraries.""" + LIBRARY_NAME = name + + +Library("dummy") + + +@template +def DisableStlWrapping(): + COMPILE_FLAGS["STL"] = [] + + +UNIFIED_SOURCES += ["test1.c"] + +DisableStlWrapping() diff --git a/python/mozbuild/mozbuild/test/frontend/data/disable-stl-wrapping/test1.c b/python/mozbuild/mozbuild/test/frontend/data/disable-stl-wrapping/test1.c new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/disable-stl-wrapping/test1.c diff --git a/python/mozbuild/mozbuild/test/frontend/data/dist-files-missing/install.rdf b/python/mozbuild/mozbuild/test/frontend/data/dist-files-missing/install.rdf new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/dist-files-missing/install.rdf diff --git a/python/mozbuild/mozbuild/test/frontend/data/dist-files-missing/moz.build b/python/mozbuild/mozbuild/test/frontend/data/dist-files-missing/moz.build new file mode 100644 index 0000000000..25961f149f --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/dist-files-missing/moz.build @@ -0,0 +1,8 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# Any copyright is dedicated to the Public Domain. +# http://creativecommons.org/publicdomain/zero/1.0/ + +FINAL_TARGET_PP_FILES += [ + "install.rdf", + "main.js", +] diff --git a/python/mozbuild/mozbuild/test/frontend/data/dist-files/install.rdf b/python/mozbuild/mozbuild/test/frontend/data/dist-files/install.rdf new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/dist-files/install.rdf diff --git a/python/mozbuild/mozbuild/test/frontend/data/dist-files/main.js b/python/mozbuild/mozbuild/test/frontend/data/dist-files/main.js new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/dist-files/main.js diff --git a/python/mozbuild/mozbuild/test/frontend/data/dist-files/moz.build b/python/mozbuild/mozbuild/test/frontend/data/dist-files/moz.build new file mode 100644 index 0000000000..25961f149f --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/dist-files/moz.build @@ -0,0 +1,8 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# Any copyright is dedicated to the Public Domain. +# http://creativecommons.org/publicdomain/zero/1.0/ + +FINAL_TARGET_PP_FILES += [ + "install.rdf", + "main.js", +] diff --git a/python/mozbuild/mozbuild/test/frontend/data/exports-generated/foo.h b/python/mozbuild/mozbuild/test/frontend/data/exports-generated/foo.h new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/exports-generated/foo.h diff --git a/python/mozbuild/mozbuild/test/frontend/data/exports-generated/moz.build b/python/mozbuild/mozbuild/test/frontend/data/exports-generated/moz.build new file mode 100644 index 0000000000..bd3507c97b --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/exports-generated/moz.build @@ -0,0 +1,8 @@ +# Any copyright is dedicated to the Public Domain. +# http://creativecommons.org/publicdomain/zero/1.0/ + +EXPORTS += ["foo.h"] +EXPORTS.mozilla += ["mozilla1.h"] +EXPORTS.mozilla += ["!mozilla2.h"] + +GENERATED_FILES += ["mozilla2.h"] diff --git a/python/mozbuild/mozbuild/test/frontend/data/exports-generated/mozilla1.h b/python/mozbuild/mozbuild/test/frontend/data/exports-generated/mozilla1.h new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/exports-generated/mozilla1.h diff --git a/python/mozbuild/mozbuild/test/frontend/data/exports-missing-generated/foo.h b/python/mozbuild/mozbuild/test/frontend/data/exports-missing-generated/foo.h new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/exports-missing-generated/foo.h diff --git a/python/mozbuild/mozbuild/test/frontend/data/exports-missing-generated/moz.build b/python/mozbuild/mozbuild/test/frontend/data/exports-missing-generated/moz.build new file mode 100644 index 0000000000..d81109d37d --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/exports-missing-generated/moz.build @@ -0,0 +1,5 @@ +# Any copyright is dedicated to the Public Domain. +# http://creativecommons.org/publicdomain/zero/1.0/ + +EXPORTS += ["foo.h"] +EXPORTS += ["!bar.h"] diff --git a/python/mozbuild/mozbuild/test/frontend/data/exports-missing/foo.h b/python/mozbuild/mozbuild/test/frontend/data/exports-missing/foo.h new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/exports-missing/foo.h diff --git a/python/mozbuild/mozbuild/test/frontend/data/exports-missing/moz.build b/python/mozbuild/mozbuild/test/frontend/data/exports-missing/moz.build new file mode 100644 index 0000000000..3f94fbdccd --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/exports-missing/moz.build @@ -0,0 +1,6 @@ +# Any copyright is dedicated to the Public Domain. +# http://creativecommons.org/publicdomain/zero/1.0/ + +EXPORTS += ["foo.h"] +EXPORTS.mozilla += ["mozilla1.h"] +EXPORTS.mozilla += ["mozilla2.h"] diff --git a/python/mozbuild/mozbuild/test/frontend/data/exports-missing/mozilla1.h b/python/mozbuild/mozbuild/test/frontend/data/exports-missing/mozilla1.h new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/exports-missing/mozilla1.h diff --git a/python/mozbuild/mozbuild/test/frontend/data/exports/bar.h b/python/mozbuild/mozbuild/test/frontend/data/exports/bar.h new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/exports/bar.h diff --git a/python/mozbuild/mozbuild/test/frontend/data/exports/baz.h b/python/mozbuild/mozbuild/test/frontend/data/exports/baz.h new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/exports/baz.h diff --git a/python/mozbuild/mozbuild/test/frontend/data/exports/dom1.h b/python/mozbuild/mozbuild/test/frontend/data/exports/dom1.h new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/exports/dom1.h diff --git a/python/mozbuild/mozbuild/test/frontend/data/exports/dom2.h b/python/mozbuild/mozbuild/test/frontend/data/exports/dom2.h new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/exports/dom2.h diff --git a/python/mozbuild/mozbuild/test/frontend/data/exports/dom3.h b/python/mozbuild/mozbuild/test/frontend/data/exports/dom3.h new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/exports/dom3.h diff --git a/python/mozbuild/mozbuild/test/frontend/data/exports/foo.h b/python/mozbuild/mozbuild/test/frontend/data/exports/foo.h new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/exports/foo.h diff --git a/python/mozbuild/mozbuild/test/frontend/data/exports/gfx.h b/python/mozbuild/mozbuild/test/frontend/data/exports/gfx.h new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/exports/gfx.h diff --git a/python/mozbuild/mozbuild/test/frontend/data/exports/mem.h b/python/mozbuild/mozbuild/test/frontend/data/exports/mem.h new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/exports/mem.h diff --git a/python/mozbuild/mozbuild/test/frontend/data/exports/mem2.h b/python/mozbuild/mozbuild/test/frontend/data/exports/mem2.h new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/exports/mem2.h diff --git a/python/mozbuild/mozbuild/test/frontend/data/exports/moz.build b/python/mozbuild/mozbuild/test/frontend/data/exports/moz.build new file mode 100644 index 0000000000..64253b1cf0 --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/exports/moz.build @@ -0,0 +1,13 @@ +# Any copyright is dedicated to the Public Domain. +# http://creativecommons.org/publicdomain/zero/1.0/ + +EXPORTS += ["foo.h"] +EXPORTS += ["bar.h", "baz.h"] +EXPORTS.mozilla += ["mozilla1.h"] +EXPORTS.mozilla += ["mozilla2.h"] +EXPORTS.mozilla.dom += ["dom1.h"] +EXPORTS.mozilla.dom += ["dom2.h", "dom3.h"] +EXPORTS.mozilla.gfx += ["gfx.h"] +EXPORTS.vpx = ["mem.h"] +EXPORTS.vpx += ["mem2.h"] +EXPORTS.nspr.private = ["pprio.h", "pprthred.h"] diff --git a/python/mozbuild/mozbuild/test/frontend/data/exports/mozilla1.h b/python/mozbuild/mozbuild/test/frontend/data/exports/mozilla1.h new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/exports/mozilla1.h diff --git a/python/mozbuild/mozbuild/test/frontend/data/exports/mozilla2.h b/python/mozbuild/mozbuild/test/frontend/data/exports/mozilla2.h new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/exports/mozilla2.h diff --git a/python/mozbuild/mozbuild/test/frontend/data/exports/pprio.h b/python/mozbuild/mozbuild/test/frontend/data/exports/pprio.h new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/exports/pprio.h diff --git a/python/mozbuild/mozbuild/test/frontend/data/exports/pprthred.h b/python/mozbuild/mozbuild/test/frontend/data/exports/pprthred.h new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/exports/pprthred.h diff --git a/python/mozbuild/mozbuild/test/frontend/data/files-info/bug_component/bad-assignment/moz.build b/python/mozbuild/mozbuild/test/frontend/data/files-info/bug_component/bad-assignment/moz.build new file mode 100644 index 0000000000..693b6cc962 --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/files-info/bug_component/bad-assignment/moz.build @@ -0,0 +1,2 @@ +with Files("*"): + BUG_COMPONENT = "bad value" diff --git a/python/mozbuild/mozbuild/test/frontend/data/files-info/bug_component/different-matchers/moz.build b/python/mozbuild/mozbuild/test/frontend/data/files-info/bug_component/different-matchers/moz.build new file mode 100644 index 0000000000..ca5c74fd6a --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/files-info/bug_component/different-matchers/moz.build @@ -0,0 +1,4 @@ +with Files("*.jsm"): + BUG_COMPONENT = ("Firefox", "JS") +with Files("*.cpp"): + BUG_COMPONENT = ("Firefox", "C++") diff --git a/python/mozbuild/mozbuild/test/frontend/data/files-info/bug_component/final/moz.build b/python/mozbuild/mozbuild/test/frontend/data/files-info/bug_component/final/moz.build new file mode 100644 index 0000000000..9b1d05a9b0 --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/files-info/bug_component/final/moz.build @@ -0,0 +1,3 @@ +with Files("**/Makefile.in"): + BUG_COMPONENT = ("Firefox Build System", "General") + FINAL = True diff --git a/python/mozbuild/mozbuild/test/frontend/data/files-info/bug_component/final/subcomponent/moz.build b/python/mozbuild/mozbuild/test/frontend/data/files-info/bug_component/final/subcomponent/moz.build new file mode 100644 index 0000000000..9b21529812 --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/files-info/bug_component/final/subcomponent/moz.build @@ -0,0 +1,2 @@ +with Files("**"): + BUG_COMPONENT = ("Another", "Component") diff --git a/python/mozbuild/mozbuild/test/frontend/data/files-info/bug_component/moz.build b/python/mozbuild/mozbuild/test/frontend/data/files-info/bug_component/moz.build new file mode 100644 index 0000000000..4bbca3dc09 --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/files-info/bug_component/moz.build @@ -0,0 +1,2 @@ +with Files("**"): + BUG_COMPONENT = ("default_product", "default_component") diff --git a/python/mozbuild/mozbuild/test/frontend/data/files-info/bug_component/simple/moz.build b/python/mozbuild/mozbuild/test/frontend/data/files-info/bug_component/simple/moz.build new file mode 100644 index 0000000000..e8b99df68d --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/files-info/bug_component/simple/moz.build @@ -0,0 +1,2 @@ +with Files("*"): + BUG_COMPONENT = ("Firefox Build System", "General") diff --git a/python/mozbuild/mozbuild/test/frontend/data/files-info/bug_component/static/moz.build b/python/mozbuild/mozbuild/test/frontend/data/files-info/bug_component/static/moz.build new file mode 100644 index 0000000000..49acf29196 --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/files-info/bug_component/static/moz.build @@ -0,0 +1,5 @@ +with Files("foo"): + BUG_COMPONENT = ("FooProduct", "FooComponent") + +with Files("bar"): + BUG_COMPONENT = ("BarProduct", "BarComponent") diff --git a/python/mozbuild/mozbuild/test/frontend/data/files-info/moz.build b/python/mozbuild/mozbuild/test/frontend/data/files-info/moz.build new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/files-info/moz.build diff --git a/python/mozbuild/mozbuild/test/frontend/data/final-target-pp-files-non-srcdir/moz.build b/python/mozbuild/mozbuild/test/frontend/data/final-target-pp-files-non-srcdir/moz.build new file mode 100644 index 0000000000..67e5fb5dce --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/final-target-pp-files-non-srcdir/moz.build @@ -0,0 +1,7 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# Any copyright is dedicated to the Public Domain. +# http://creativecommons.org/publicdomain/zero/1.0/ + +FINAL_TARGET_PP_FILES += [ + "!foo.js", +] diff --git a/python/mozbuild/mozbuild/test/frontend/data/generated-files-absolute-script/moz.build b/python/mozbuild/mozbuild/test/frontend/data/generated-files-absolute-script/moz.build new file mode 100644 index 0000000000..860f025eac --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/generated-files-absolute-script/moz.build @@ -0,0 +1,9 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# Any copyright is dedicated to the Public Domain. +# http://creativecommons.org/publicdomain/zero/1.0/ + +GENERATED_FILES += ["bar.c"] + +bar = GENERATED_FILES["bar.c"] +bar.script = "/script.py:make_bar" +bar.inputs = [] diff --git a/python/mozbuild/mozbuild/test/frontend/data/generated-files-absolute-script/script.py b/python/mozbuild/mozbuild/test/frontend/data/generated-files-absolute-script/script.py new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/generated-files-absolute-script/script.py diff --git a/python/mozbuild/mozbuild/test/frontend/data/generated-files-force/moz.build b/python/mozbuild/mozbuild/test/frontend/data/generated-files-force/moz.build new file mode 100644 index 0000000000..33f54a17e8 --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/generated-files-force/moz.build @@ -0,0 +1,11 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# Any copyright is dedicated to the Public Domain. +# http://creativecommons.org/publicdomain/zero/1.0/ + +GENERATED_FILES += [ + "bar.c", + "foo.c", + ("xpidllex.py", "xpidlyacc.py"), +] +GENERATED_FILES["bar.c"].force = True +GENERATED_FILES["foo.c"].force = False diff --git a/python/mozbuild/mozbuild/test/frontend/data/generated-files-method-names/moz.build b/python/mozbuild/mozbuild/test/frontend/data/generated-files-method-names/moz.build new file mode 100644 index 0000000000..298513383b --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/generated-files-method-names/moz.build @@ -0,0 +1,13 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# Any copyright is dedicated to the Public Domain. +# http://creativecommons.org/publicdomain/zero/1.0/ + +GENERATED_FILES += ["bar.c", "foo.c"] + +bar = GENERATED_FILES["bar.c"] +bar.script = "script.py:make_bar" +bar.inputs = [] + +foo = GENERATED_FILES["foo.c"] +foo.script = "script.py" +foo.inputs = [] diff --git a/python/mozbuild/mozbuild/test/frontend/data/generated-files-method-names/script.py b/python/mozbuild/mozbuild/test/frontend/data/generated-files-method-names/script.py new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/generated-files-method-names/script.py diff --git a/python/mozbuild/mozbuild/test/frontend/data/generated-files-no-inputs/moz.build b/python/mozbuild/mozbuild/test/frontend/data/generated-files-no-inputs/moz.build new file mode 100644 index 0000000000..50f703c696 --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/generated-files-no-inputs/moz.build @@ -0,0 +1,9 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# Any copyright is dedicated to the Public Domain. +# http://creativecommons.org/publicdomain/zero/1.0/ + +GENERATED_FILES += ["bar.c", "foo.c"] + +foo = GENERATED_FILES["foo.c"] +foo.script = "script.py" +foo.inputs = ["datafile"] diff --git a/python/mozbuild/mozbuild/test/frontend/data/generated-files-no-inputs/script.py b/python/mozbuild/mozbuild/test/frontend/data/generated-files-no-inputs/script.py new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/generated-files-no-inputs/script.py diff --git a/python/mozbuild/mozbuild/test/frontend/data/generated-files-no-python-script/moz.build b/python/mozbuild/mozbuild/test/frontend/data/generated-files-no-python-script/moz.build new file mode 100644 index 0000000000..ebdb7bfaf5 --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/generated-files-no-python-script/moz.build @@ -0,0 +1,8 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# Any copyright is dedicated to the Public Domain. +# http://creativecommons.org/publicdomain/zero/1.0/ + +GENERATED_FILES += ["bar.c", "foo.c"] + +bar = GENERATED_FILES["bar.c"] +bar.script = "script.rb" diff --git a/python/mozbuild/mozbuild/test/frontend/data/generated-files-no-python-script/script.rb b/python/mozbuild/mozbuild/test/frontend/data/generated-files-no-python-script/script.rb new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/generated-files-no-python-script/script.rb diff --git a/python/mozbuild/mozbuild/test/frontend/data/generated-files-no-script/moz.build b/python/mozbuild/mozbuild/test/frontend/data/generated-files-no-script/moz.build new file mode 100644 index 0000000000..258a0f2325 --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/generated-files-no-script/moz.build @@ -0,0 +1,8 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# Any copyright is dedicated to the Public Domain. +# http://creativecommons.org/publicdomain/zero/1.0/ + +GENERATED_FILES += ["bar.c", "foo.c"] + +bar = GENERATED_FILES["bar.c"] +bar.script = "nonexistent-script.py" diff --git a/python/mozbuild/mozbuild/test/frontend/data/generated-files/moz.build b/python/mozbuild/mozbuild/test/frontend/data/generated-files/moz.build new file mode 100644 index 0000000000..97267c5d26 --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/generated-files/moz.build @@ -0,0 +1,9 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# Any copyright is dedicated to the Public Domain. +# http://creativecommons.org/publicdomain/zero/1.0/ + +GENERATED_FILES += [ + "bar.c", + "foo.c", + ("xpidllex.py", "xpidlyacc.py"), +] diff --git a/python/mozbuild/mozbuild/test/frontend/data/generated-sources/a.cpp b/python/mozbuild/mozbuild/test/frontend/data/generated-sources/a.cpp new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/generated-sources/a.cpp diff --git a/python/mozbuild/mozbuild/test/frontend/data/generated-sources/b.cc b/python/mozbuild/mozbuild/test/frontend/data/generated-sources/b.cc new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/generated-sources/b.cc diff --git a/python/mozbuild/mozbuild/test/frontend/data/generated-sources/c.cxx b/python/mozbuild/mozbuild/test/frontend/data/generated-sources/c.cxx new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/generated-sources/c.cxx diff --git a/python/mozbuild/mozbuild/test/frontend/data/generated-sources/d.c b/python/mozbuild/mozbuild/test/frontend/data/generated-sources/d.c new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/generated-sources/d.c diff --git a/python/mozbuild/mozbuild/test/frontend/data/generated-sources/e.m b/python/mozbuild/mozbuild/test/frontend/data/generated-sources/e.m new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/generated-sources/e.m diff --git a/python/mozbuild/mozbuild/test/frontend/data/generated-sources/f.mm b/python/mozbuild/mozbuild/test/frontend/data/generated-sources/f.mm new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/generated-sources/f.mm diff --git a/python/mozbuild/mozbuild/test/frontend/data/generated-sources/g.S b/python/mozbuild/mozbuild/test/frontend/data/generated-sources/g.S new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/generated-sources/g.S diff --git a/python/mozbuild/mozbuild/test/frontend/data/generated-sources/h.s b/python/mozbuild/mozbuild/test/frontend/data/generated-sources/h.s new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/generated-sources/h.s diff --git a/python/mozbuild/mozbuild/test/frontend/data/generated-sources/i.asm b/python/mozbuild/mozbuild/test/frontend/data/generated-sources/i.asm new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/generated-sources/i.asm diff --git a/python/mozbuild/mozbuild/test/frontend/data/generated-sources/moz.build b/python/mozbuild/mozbuild/test/frontend/data/generated-sources/moz.build new file mode 100644 index 0000000000..e305d9d32f --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/generated-sources/moz.build @@ -0,0 +1,39 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# Any copyright is dedicated to the Public Domain. +# http://creativecommons.org/publicdomain/zero/1.0/ + + +@template +def Library(name): + """Template for libraries.""" + LIBRARY_NAME = name + + +Library("dummy") + +SOURCES += [ + "!a.cpp", + "!b.cc", + "!c.cxx", +] + +SOURCES += [ + "!d.c", +] + +SOURCES += [ + "!e.m", +] + +SOURCES += [ + "!f.mm", +] + +SOURCES += [ + "!g.S", +] + +SOURCES += [ + "!h.s", + "!i.asm", +] diff --git a/python/mozbuild/mozbuild/test/frontend/data/generated_includes/moz.build b/python/mozbuild/mozbuild/test/frontend/data/generated_includes/moz.build new file mode 100644 index 0000000000..31f9042c0a --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/generated_includes/moz.build @@ -0,0 +1,5 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# Any copyright is dedicated to the Public Domain. +# http://creativecommons.org/publicdomain/zero/1.0/ + +LOCAL_INCLUDES += ["!/bar/baz", "!foo"] diff --git a/python/mozbuild/mozbuild/test/frontend/data/host-compile-flags/moz.build b/python/mozbuild/mozbuild/test/frontend/data/host-compile-flags/moz.build new file mode 100644 index 0000000000..4225234c65 --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/host-compile-flags/moz.build @@ -0,0 +1,22 @@ +# Any copyright is dedicated to the Public Domain. +# http://creativecommons.org/publicdomain/zero/1.0/ + + +@template +def HostLibrary(name): + """Template for libraries.""" + HOST_LIBRARY_NAME = name + + +HostLibrary("dummy") + +HOST_SOURCES += ["test1.c"] + +value = "xyz" +HOST_DEFINES["FOO"] = True +HOST_DEFINES["BAZ"] = '"abcd"' +HOST_DEFINES["BAR"] = 7 +HOST_DEFINES["VALUE"] = value +HOST_DEFINES["QUX"] = False + +HOST_CFLAGS += ["-funroll-loops", "-host-arg"] diff --git a/python/mozbuild/mozbuild/test/frontend/data/host-compile-flags/test1.c b/python/mozbuild/mozbuild/test/frontend/data/host-compile-flags/test1.c new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/host-compile-flags/test1.c diff --git a/python/mozbuild/mozbuild/test/frontend/data/host-program-paths/final-target/moz.build b/python/mozbuild/mozbuild/test/frontend/data/host-program-paths/final-target/moz.build new file mode 100644 index 0000000000..a2136749dc --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/host-program-paths/final-target/moz.build @@ -0,0 +1,5 @@ +# Any copyright is dedicated to the Public Domain. +# http://creativecommons.org/publicdomain/zero/1.0/ + +FINAL_TARGET = "final/target" +HostProgram("final-target") diff --git a/python/mozbuild/mozbuild/test/frontend/data/host-program-paths/installed/moz.build b/python/mozbuild/mozbuild/test/frontend/data/host-program-paths/installed/moz.build new file mode 100644 index 0000000000..0d10d35508 --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/host-program-paths/installed/moz.build @@ -0,0 +1,4 @@ +# Any copyright is dedicated to the Public Domain. +# http://creativecommons.org/publicdomain/zero/1.0/ + +HostProgram("dist-host-bin") diff --git a/python/mozbuild/mozbuild/test/frontend/data/host-program-paths/moz.build b/python/mozbuild/mozbuild/test/frontend/data/host-program-paths/moz.build new file mode 100644 index 0000000000..ef9175fa54 --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/host-program-paths/moz.build @@ -0,0 +1,14 @@ +# Any copyright is dedicated to the Public Domain. +# http://creativecommons.org/publicdomain/zero/1.0/ + + +@template +def HostProgram(name): + HOST_PROGRAM = name + + +DIRS += [ + "final-target", + "installed", + "not-installed", +] diff --git a/python/mozbuild/mozbuild/test/frontend/data/host-program-paths/not-installed/moz.build b/python/mozbuild/mozbuild/test/frontend/data/host-program-paths/not-installed/moz.build new file mode 100644 index 0000000000..4a8451bc8f --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/host-program-paths/not-installed/moz.build @@ -0,0 +1,5 @@ +# Any copyright is dedicated to the Public Domain. +# http://creativecommons.org/publicdomain/zero/1.0/ + +DIST_INSTALL = False +HostProgram("not-installed") diff --git a/python/mozbuild/mozbuild/test/frontend/data/host-rust-libraries/Cargo.toml b/python/mozbuild/mozbuild/test/frontend/data/host-rust-libraries/Cargo.toml new file mode 100644 index 0000000000..49b7ef6497 --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/host-rust-libraries/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "host-lib" +version = "0.1.0" +authors = [ + "The Mozilla Project Developers", +] + +[lib] +crate-type = ["staticlib"] + +[profile.dev] +panic = "abort" + +[profile.release] +panic = "abort" + +[dependencies] +mozilla-central-workspace-hack = { version = "0.1", features = ["host-lib"], optional = true } diff --git a/python/mozbuild/mozbuild/test/frontend/data/host-rust-libraries/moz.build b/python/mozbuild/mozbuild/test/frontend/data/host-rust-libraries/moz.build new file mode 100644 index 0000000000..37b6728ae3 --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/host-rust-libraries/moz.build @@ -0,0 +1,22 @@ +# Any copyright is dedicated to the Public Domain. +# http://creativecommons.org/publicdomain/zero/1.0/ + + +@template +def HostLibrary(name): + """Template for libraries.""" + HOST_LIBRARY_NAME = name + + +@template +def HostRustLibrary(name, features=None): + """Template for Rust libraries.""" + HostLibrary(name) + + IS_RUST_LIBRARY = True + + if features: + RUST_LIBRARY_FEATURES = features + + +HostRustLibrary("host-lib") diff --git a/python/mozbuild/mozbuild/test/frontend/data/host-rust-program-no-cargo-toml/moz.build b/python/mozbuild/mozbuild/test/frontend/data/host-rust-program-no-cargo-toml/moz.build new file mode 100644 index 0000000000..c60e731d99 --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/host-rust-program-no-cargo-toml/moz.build @@ -0,0 +1 @@ +HOST_RUST_PROGRAMS += ["none"] diff --git a/python/mozbuild/mozbuild/test/frontend/data/host-rust-program-nonexistent-name/Cargo.toml b/python/mozbuild/mozbuild/test/frontend/data/host-rust-program-nonexistent-name/Cargo.toml new file mode 100644 index 0000000000..e5ea7b26a6 --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/host-rust-program-nonexistent-name/Cargo.toml @@ -0,0 +1,10 @@ +[package] +authors = ["The Mozilla Project Developers"] +name = "testing" +version = "0.0.1" + +[[bin]] +name = "some" + +[dependencies] +mozilla-central-workspace-hack = { version = "0.1", features = ["testing"], optional = true } diff --git a/python/mozbuild/mozbuild/test/frontend/data/host-rust-program-nonexistent-name/moz.build b/python/mozbuild/mozbuild/test/frontend/data/host-rust-program-nonexistent-name/moz.build new file mode 100644 index 0000000000..c60e731d99 --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/host-rust-program-nonexistent-name/moz.build @@ -0,0 +1 @@ +HOST_RUST_PROGRAMS += ["none"] diff --git a/python/mozbuild/mozbuild/test/frontend/data/host-rust-programs/Cargo.toml b/python/mozbuild/mozbuild/test/frontend/data/host-rust-programs/Cargo.toml new file mode 100644 index 0000000000..e5ea7b26a6 --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/host-rust-programs/Cargo.toml @@ -0,0 +1,10 @@ +[package] +authors = ["The Mozilla Project Developers"] +name = "testing" +version = "0.0.1" + +[[bin]] +name = "some" + +[dependencies] +mozilla-central-workspace-hack = { version = "0.1", features = ["testing"], optional = true } diff --git a/python/mozbuild/mozbuild/test/frontend/data/host-rust-programs/moz.build b/python/mozbuild/mozbuild/test/frontend/data/host-rust-programs/moz.build new file mode 100644 index 0000000000..2d75958b07 --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/host-rust-programs/moz.build @@ -0,0 +1 @@ +HOST_RUST_PROGRAMS += ["some"] diff --git a/python/mozbuild/mozbuild/test/frontend/data/host-sources/a.cpp b/python/mozbuild/mozbuild/test/frontend/data/host-sources/a.cpp new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/host-sources/a.cpp diff --git a/python/mozbuild/mozbuild/test/frontend/data/host-sources/b.cc b/python/mozbuild/mozbuild/test/frontend/data/host-sources/b.cc new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/host-sources/b.cc diff --git a/python/mozbuild/mozbuild/test/frontend/data/host-sources/c.cxx b/python/mozbuild/mozbuild/test/frontend/data/host-sources/c.cxx new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/host-sources/c.cxx diff --git a/python/mozbuild/mozbuild/test/frontend/data/host-sources/d.c b/python/mozbuild/mozbuild/test/frontend/data/host-sources/d.c new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/host-sources/d.c diff --git a/python/mozbuild/mozbuild/test/frontend/data/host-sources/e.mm b/python/mozbuild/mozbuild/test/frontend/data/host-sources/e.mm new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/host-sources/e.mm diff --git a/python/mozbuild/mozbuild/test/frontend/data/host-sources/f.mm b/python/mozbuild/mozbuild/test/frontend/data/host-sources/f.mm new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/host-sources/f.mm diff --git a/python/mozbuild/mozbuild/test/frontend/data/host-sources/moz.build b/python/mozbuild/mozbuild/test/frontend/data/host-sources/moz.build new file mode 100644 index 0000000000..b1f5b98039 --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/host-sources/moz.build @@ -0,0 +1,27 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# Any copyright is dedicated to the Public Domain. +# http://creativecommons.org/publicdomain/zero/1.0/ + + +@template +def HostLibrary(name): + """Template for libraries.""" + HOST_LIBRARY_NAME = name + + +HostLibrary("dummy") + +HOST_SOURCES += [ + "a.cpp", + "b.cc", + "c.cxx", +] + +HOST_SOURCES += [ + "d.c", +] + +HOST_SOURCES += [ + "e.mm", + "f.mm", +] diff --git a/python/mozbuild/mozbuild/test/frontend/data/include-basic/included.build b/python/mozbuild/mozbuild/test/frontend/data/include-basic/included.build new file mode 100644 index 0000000000..3532347e27 --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/include-basic/included.build @@ -0,0 +1,4 @@ +# Any copyright is dedicated to the Public Domain. +# http://creativecommons.org/publicdomain/zero/1.0/ + +DIRS += ["bar"] diff --git a/python/mozbuild/mozbuild/test/frontend/data/include-basic/moz.build b/python/mozbuild/mozbuild/test/frontend/data/include-basic/moz.build new file mode 100644 index 0000000000..b8e37c69ea --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/include-basic/moz.build @@ -0,0 +1,7 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# Any copyright is dedicated to the Public Domain. +# http://creativecommons.org/publicdomain/zero/1.0/ + +DIRS = ["foo"] + +include("included.build") diff --git a/python/mozbuild/mozbuild/test/frontend/data/include-file-stack/included-1.build b/python/mozbuild/mozbuild/test/frontend/data/include-file-stack/included-1.build new file mode 100644 index 0000000000..b5dc2728c6 --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/include-file-stack/included-1.build @@ -0,0 +1,4 @@ +# Any copyright is dedicated to the Public Domain. +# http://creativecommons.org/publicdomain/zero/1.0/ + +include("included-2.build") diff --git a/python/mozbuild/mozbuild/test/frontend/data/include-file-stack/included-2.build b/python/mozbuild/mozbuild/test/frontend/data/include-file-stack/included-2.build new file mode 100644 index 0000000000..9bfc65481d --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/include-file-stack/included-2.build @@ -0,0 +1,4 @@ +# Any copyright is dedicated to the Public Domain. +# http://creativecommons.org/publicdomain/zero/1.0/ + +ILLEGAL = True diff --git a/python/mozbuild/mozbuild/test/frontend/data/include-file-stack/moz.build b/python/mozbuild/mozbuild/test/frontend/data/include-file-stack/moz.build new file mode 100644 index 0000000000..def43513c7 --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/include-file-stack/moz.build @@ -0,0 +1,5 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# Any copyright is dedicated to the Public Domain. +# http://creativecommons.org/publicdomain/zero/1.0/ + +include("included-1.build") diff --git a/python/mozbuild/mozbuild/test/frontend/data/include-missing/moz.build b/python/mozbuild/mozbuild/test/frontend/data/include-missing/moz.build new file mode 100644 index 0000000000..34129f7c93 --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/include-missing/moz.build @@ -0,0 +1,5 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# Any copyright is dedicated to the Public Domain. +# http://creativecommons.org/publicdomain/zero/1.0/ + +include("missing.build") diff --git a/python/mozbuild/mozbuild/test/frontend/data/include-outside-topsrcdir/relative.build b/python/mozbuild/mozbuild/test/frontend/data/include-outside-topsrcdir/relative.build new file mode 100644 index 0000000000..714a044436 --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/include-outside-topsrcdir/relative.build @@ -0,0 +1,4 @@ +# Any copyright is dedicated to the Public Domain. +# http://creativecommons.org/publicdomain/zero/1.0/ + +include("../moz.build") diff --git a/python/mozbuild/mozbuild/test/frontend/data/include-relative-from-child/child/child.build b/python/mozbuild/mozbuild/test/frontend/data/include-relative-from-child/child/child.build new file mode 100644 index 0000000000..ecae03ca7d --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/include-relative-from-child/child/child.build @@ -0,0 +1,4 @@ +# Any copyright is dedicated to the Public Domain. +# http://creativecommons.org/publicdomain/zero/1.0/ + +include("../parent.build") diff --git a/python/mozbuild/mozbuild/test/frontend/data/include-relative-from-child/child/child2.build b/python/mozbuild/mozbuild/test/frontend/data/include-relative-from-child/child/child2.build new file mode 100644 index 0000000000..36210ba96b --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/include-relative-from-child/child/child2.build @@ -0,0 +1,4 @@ +# Any copyright is dedicated to the Public Domain. +# http://creativecommons.org/publicdomain/zero/1.0/ + +include("grandchild/grandchild.build") diff --git a/python/mozbuild/mozbuild/test/frontend/data/include-relative-from-child/child/grandchild/grandchild.build b/python/mozbuild/mozbuild/test/frontend/data/include-relative-from-child/child/grandchild/grandchild.build new file mode 100644 index 0000000000..76dcdb899f --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/include-relative-from-child/child/grandchild/grandchild.build @@ -0,0 +1,4 @@ +# Any copyright is dedicated to the Public Domain. +# http://creativecommons.org/publicdomain/zero/1.0/ + +include("../../parent.build") diff --git a/python/mozbuild/mozbuild/test/frontend/data/include-relative-from-child/parent.build b/python/mozbuild/mozbuild/test/frontend/data/include-relative-from-child/parent.build new file mode 100644 index 0000000000..eb1477d0df --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/include-relative-from-child/parent.build @@ -0,0 +1,4 @@ +# Any copyright is dedicated to the Public Domain. +# http://creativecommons.org/publicdomain/zero/1.0/ + +DIRS = ["foo"] diff --git a/python/mozbuild/mozbuild/test/frontend/data/include-topsrcdir-relative/moz.build b/python/mozbuild/mozbuild/test/frontend/data/include-topsrcdir-relative/moz.build new file mode 100644 index 0000000000..879b832ed8 --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/include-topsrcdir-relative/moz.build @@ -0,0 +1,5 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# Any copyright is dedicated to the Public Domain. +# http://creativecommons.org/publicdomain/zero/1.0/ + +include("/sibling.build") diff --git a/python/mozbuild/mozbuild/test/frontend/data/include-topsrcdir-relative/sibling.build b/python/mozbuild/mozbuild/test/frontend/data/include-topsrcdir-relative/sibling.build new file mode 100644 index 0000000000..eb1477d0df --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/include-topsrcdir-relative/sibling.build @@ -0,0 +1,4 @@ +# Any copyright is dedicated to the Public Domain. +# http://creativecommons.org/publicdomain/zero/1.0/ + +DIRS = ["foo"] diff --git a/python/mozbuild/mozbuild/test/frontend/data/inheriting-variables/bar/moz.build b/python/mozbuild/mozbuild/test/frontend/data/inheriting-variables/bar/moz.build new file mode 100644 index 0000000000..568f361a54 --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/inheriting-variables/bar/moz.build @@ -0,0 +1,5 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# 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/. diff --git a/python/mozbuild/mozbuild/test/frontend/data/inheriting-variables/foo/baz/moz.build b/python/mozbuild/mozbuild/test/frontend/data/inheriting-variables/foo/baz/moz.build new file mode 100644 index 0000000000..9c392681c7 --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/inheriting-variables/foo/baz/moz.build @@ -0,0 +1,7 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# 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/. + +XPIDL_MODULE = "baz" diff --git a/python/mozbuild/mozbuild/test/frontend/data/inheriting-variables/foo/moz.build b/python/mozbuild/mozbuild/test/frontend/data/inheriting-variables/foo/moz.build new file mode 100644 index 0000000000..f3368867ad --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/inheriting-variables/foo/moz.build @@ -0,0 +1,7 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# 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/. + +DIRS += ["baz"] diff --git a/python/mozbuild/mozbuild/test/frontend/data/inheriting-variables/moz.build b/python/mozbuild/mozbuild/test/frontend/data/inheriting-variables/moz.build new file mode 100644 index 0000000000..169e9d1554 --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/inheriting-variables/moz.build @@ -0,0 +1,10 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# 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/. + +XPIDL_MODULE = "foobar" +export("XPIDL_MODULE") + +DIRS += ["foo", "bar"] diff --git a/python/mozbuild/mozbuild/test/frontend/data/ipdl_sources/bar/moz.build b/python/mozbuild/mozbuild/test/frontend/data/ipdl_sources/bar/moz.build new file mode 100644 index 0000000000..b49ec1216b --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/ipdl_sources/bar/moz.build @@ -0,0 +1,14 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# 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/. + +PREPROCESSED_IPDL_SOURCES += [ + "bar1.ipdl", +] + +IPDL_SOURCES += [ + "bar.ipdl", + "bar2.ipdlh", +] diff --git a/python/mozbuild/mozbuild/test/frontend/data/ipdl_sources/foo/moz.build b/python/mozbuild/mozbuild/test/frontend/data/ipdl_sources/foo/moz.build new file mode 100644 index 0000000000..c2e891572b --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/ipdl_sources/foo/moz.build @@ -0,0 +1,14 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# 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/. + +PREPROCESSED_IPDL_SOURCES += [ + "foo1.ipdl", +] + +IPDL_SOURCES += [ + "foo.ipdl", + "foo2.ipdlh", +] diff --git a/python/mozbuild/mozbuild/test/frontend/data/ipdl_sources/moz.build b/python/mozbuild/mozbuild/test/frontend/data/ipdl_sources/moz.build new file mode 100644 index 0000000000..9fe7699519 --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/ipdl_sources/moz.build @@ -0,0 +1,10 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# 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/. + +DIRS += [ + "bar", + "foo", +] diff --git a/python/mozbuild/mozbuild/test/frontend/data/jar-manifests-multiple-files/moz.build b/python/mozbuild/mozbuild/test/frontend/data/jar-manifests-multiple-files/moz.build new file mode 100644 index 0000000000..fa61c94006 --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/jar-manifests-multiple-files/moz.build @@ -0,0 +1,7 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# 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/. + +JAR_MANIFESTS += ["jar.mn", "other.jar"] diff --git a/python/mozbuild/mozbuild/test/frontend/data/jar-manifests/moz.build b/python/mozbuild/mozbuild/test/frontend/data/jar-manifests/moz.build new file mode 100644 index 0000000000..d988c0ff9b --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/jar-manifests/moz.build @@ -0,0 +1,7 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# 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/. + +JAR_MANIFESTS += ["jar.mn"] diff --git a/python/mozbuild/mozbuild/test/frontend/data/library-defines/liba/moz.build b/python/mozbuild/mozbuild/test/frontend/data/library-defines/liba/moz.build new file mode 100644 index 0000000000..65fcc6d08e --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/library-defines/liba/moz.build @@ -0,0 +1,5 @@ +# Any copyright is dedicated to the Public Domain. +# http://creativecommons.org/publicdomain/zero/1.0/ + +Library("liba") +LIBRARY_DEFINES["IN_LIBA"] = True diff --git a/python/mozbuild/mozbuild/test/frontend/data/library-defines/libb/moz.build b/python/mozbuild/mozbuild/test/frontend/data/library-defines/libb/moz.build new file mode 100644 index 0000000000..f4cf7b31a0 --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/library-defines/libb/moz.build @@ -0,0 +1,7 @@ +# Any copyright is dedicated to the Public Domain. +# http://creativecommons.org/publicdomain/zero/1.0/ + +Library("libb") +FINAL_LIBRARY = "liba" +LIBRARY_DEFINES["IN_LIBB"] = True +USE_LIBS += ["libd"] diff --git a/python/mozbuild/mozbuild/test/frontend/data/library-defines/libc/moz.build b/python/mozbuild/mozbuild/test/frontend/data/library-defines/libc/moz.build new file mode 100644 index 0000000000..022a67559d --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/library-defines/libc/moz.build @@ -0,0 +1,5 @@ +# Any copyright is dedicated to the Public Domain. +# http://creativecommons.org/publicdomain/zero/1.0/ + +Library("libc") +FINAL_LIBRARY = "libb" diff --git a/python/mozbuild/mozbuild/test/frontend/data/library-defines/libd/moz.build b/python/mozbuild/mozbuild/test/frontend/data/library-defines/libd/moz.build new file mode 100644 index 0000000000..0bd94be069 --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/library-defines/libd/moz.build @@ -0,0 +1,5 @@ +# Any copyright is dedicated to the Public Domain. +# http://creativecommons.org/publicdomain/zero/1.0/ + +Library("libd") +FORCE_STATIC_LIB = True diff --git a/python/mozbuild/mozbuild/test/frontend/data/library-defines/moz.build b/python/mozbuild/mozbuild/test/frontend/data/library-defines/moz.build new file mode 100644 index 0000000000..dcc955cf28 --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/library-defines/moz.build @@ -0,0 +1,11 @@ +# Any copyright is dedicated to the Public Domain. +# http://creativecommons.org/publicdomain/zero/1.0/ + + +@template +def Library(name): + """Template for libraries.""" + LIBRARY_NAME = name + + +DIRS = ["liba", "libb", "libc", "libd"] diff --git a/python/mozbuild/mozbuild/test/frontend/data/link-flags/moz.build b/python/mozbuild/mozbuild/test/frontend/data/link-flags/moz.build new file mode 100644 index 0000000000..9e25efdcbf --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/link-flags/moz.build @@ -0,0 +1,16 @@ +# Any copyright is dedicated to the Public Domain. +# http://creativecommons.org/publicdomain/zero/1.0/ + + +@template +def Library(name): + """Template for libraries.""" + LIBRARY_NAME = name + + +Library("dummy") + +UNIFIED_SOURCES += ["test1.c"] + +LDFLAGS += ["-Wl,-U_foo"] +LDFLAGS += ["-framework Foo", "-x"] diff --git a/python/mozbuild/mozbuild/test/frontend/data/link-flags/test1.c b/python/mozbuild/mozbuild/test/frontend/data/link-flags/test1.c new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/link-flags/test1.c diff --git a/python/mozbuild/mozbuild/test/frontend/data/local_includes-filename/foo.h b/python/mozbuild/mozbuild/test/frontend/data/local_includes-filename/foo.h new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/local_includes-filename/foo.h diff --git a/python/mozbuild/mozbuild/test/frontend/data/local_includes-filename/moz.build b/python/mozbuild/mozbuild/test/frontend/data/local_includes-filename/moz.build new file mode 100644 index 0000000000..70259db75b --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/local_includes-filename/moz.build @@ -0,0 +1,5 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# Any copyright is dedicated to the Public Domain. +# http://creativecommons.org/publicdomain/zero/1.0/ + +LOCAL_INCLUDES += ["foo.h"] diff --git a/python/mozbuild/mozbuild/test/frontend/data/local_includes-invalid/objdir/moz.build b/python/mozbuild/mozbuild/test/frontend/data/local_includes-invalid/objdir/moz.build new file mode 100644 index 0000000000..6dcbab537d --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/local_includes-invalid/objdir/moz.build @@ -0,0 +1,5 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# Any copyright is dedicated to the Public Domain. +# http://creativecommons.org/publicdomain/zero/1.0/ + +LOCAL_INCLUDES += ["!/"] diff --git a/python/mozbuild/mozbuild/test/frontend/data/local_includes-invalid/srcdir/moz.build b/python/mozbuild/mozbuild/test/frontend/data/local_includes-invalid/srcdir/moz.build new file mode 100644 index 0000000000..6d8f6cd2af --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/local_includes-invalid/srcdir/moz.build @@ -0,0 +1,5 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# Any copyright is dedicated to the Public Domain. +# http://creativecommons.org/publicdomain/zero/1.0/ + +LOCAL_INCLUDES += ["/"] diff --git a/python/mozbuild/mozbuild/test/frontend/data/local_includes/bar/baz/dummy_file_for_nonempty_directory b/python/mozbuild/mozbuild/test/frontend/data/local_includes/bar/baz/dummy_file_for_nonempty_directory new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/local_includes/bar/baz/dummy_file_for_nonempty_directory diff --git a/python/mozbuild/mozbuild/test/frontend/data/local_includes/foo/dummy_file_for_nonempty_directory b/python/mozbuild/mozbuild/test/frontend/data/local_includes/foo/dummy_file_for_nonempty_directory new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/local_includes/foo/dummy_file_for_nonempty_directory diff --git a/python/mozbuild/mozbuild/test/frontend/data/local_includes/moz.build b/python/mozbuild/mozbuild/test/frontend/data/local_includes/moz.build new file mode 100644 index 0000000000..1c29ac2ea2 --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/local_includes/moz.build @@ -0,0 +1,5 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# Any copyright is dedicated to the Public Domain. +# http://creativecommons.org/publicdomain/zero/1.0/ + +LOCAL_INCLUDES += ["/bar/baz", "foo"] diff --git a/python/mozbuild/mozbuild/test/frontend/data/localized-files-from-generated/moz.build b/python/mozbuild/mozbuild/test/frontend/data/localized-files-from-generated/moz.build new file mode 100644 index 0000000000..491a026419 --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/localized-files-from-generated/moz.build @@ -0,0 +1,6 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# Any copyright is dedicated to the Public Domain. +# http://creativecommons.org/publicdomain/zero/1.0/ + +LOCALIZED_GENERATED_FILES += ["abc.ini"] +LOCALIZED_FILES += ["!abc.ini"] diff --git a/python/mozbuild/mozbuild/test/frontend/data/localized-files-no-en-us/en-US/bar.ini b/python/mozbuild/mozbuild/test/frontend/data/localized-files-no-en-us/en-US/bar.ini new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/localized-files-no-en-us/en-US/bar.ini diff --git a/python/mozbuild/mozbuild/test/frontend/data/localized-files-no-en-us/foo.js b/python/mozbuild/mozbuild/test/frontend/data/localized-files-no-en-us/foo.js new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/localized-files-no-en-us/foo.js diff --git a/python/mozbuild/mozbuild/test/frontend/data/localized-files-no-en-us/inner/locales/en-US/bar.ini b/python/mozbuild/mozbuild/test/frontend/data/localized-files-no-en-us/inner/locales/en-US/bar.ini new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/localized-files-no-en-us/inner/locales/en-US/bar.ini diff --git a/python/mozbuild/mozbuild/test/frontend/data/localized-files-no-en-us/moz.build b/python/mozbuild/mozbuild/test/frontend/data/localized-files-no-en-us/moz.build new file mode 100644 index 0000000000..5c3efc8117 --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/localized-files-no-en-us/moz.build @@ -0,0 +1,9 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# Any copyright is dedicated to the Public Domain. +# http://creativecommons.org/publicdomain/zero/1.0/ + +LOCALIZED_FILES.foo += [ + "en-US/bar.ini", + "foo.js", + "inner/locales/en-US/bar.ini", +] diff --git a/python/mozbuild/mozbuild/test/frontend/data/localized-files-not-localized-generated/moz.build b/python/mozbuild/mozbuild/test/frontend/data/localized-files-not-localized-generated/moz.build new file mode 100644 index 0000000000..678f503174 --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/localized-files-not-localized-generated/moz.build @@ -0,0 +1,6 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# Any copyright is dedicated to the Public Domain. +# http://creativecommons.org/publicdomain/zero/1.0/ + +GENERATED_FILES += ["abc.ini"] +LOCALIZED_FILES += ["!abc.ini"] diff --git a/python/mozbuild/mozbuild/test/frontend/data/localized-files/en-US/bar.ini b/python/mozbuild/mozbuild/test/frontend/data/localized-files/en-US/bar.ini new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/localized-files/en-US/bar.ini diff --git a/python/mozbuild/mozbuild/test/frontend/data/localized-files/en-US/foo.js b/python/mozbuild/mozbuild/test/frontend/data/localized-files/en-US/foo.js new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/localized-files/en-US/foo.js diff --git a/python/mozbuild/mozbuild/test/frontend/data/localized-files/moz.build b/python/mozbuild/mozbuild/test/frontend/data/localized-files/moz.build new file mode 100644 index 0000000000..25a9030881 --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/localized-files/moz.build @@ -0,0 +1,9 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# Any copyright is dedicated to the Public Domain. +# http://creativecommons.org/publicdomain/zero/1.0/ + +LOCALIZED_FILES.foo += [ + "en-US/bar.ini", + "en-US/code/*.js", + "en-US/foo.js", +] diff --git a/python/mozbuild/mozbuild/test/frontend/data/localized-generated-files-final-target-files/moz.build b/python/mozbuild/mozbuild/test/frontend/data/localized-generated-files-final-target-files/moz.build new file mode 100644 index 0000000000..48acff1447 --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/localized-generated-files-final-target-files/moz.build @@ -0,0 +1,6 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# Any copyright is dedicated to the Public Domain. +# http://creativecommons.org/publicdomain/zero/1.0/ + +LOCALIZED_GENERATED_FILES += ["abc.ini"] +FINAL_TARGET_FILES += ["!abc.ini"] diff --git a/python/mozbuild/mozbuild/test/frontend/data/localized-generated-files-force/moz.build b/python/mozbuild/mozbuild/test/frontend/data/localized-generated-files-force/moz.build new file mode 100644 index 0000000000..73685545de --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/localized-generated-files-force/moz.build @@ -0,0 +1,6 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# Any copyright is dedicated to the Public Domain. +# http://creativecommons.org/publicdomain/zero/1.0/ + +LOCALIZED_GENERATED_FILES += ["abc.ini", ("bar", "baz")] +LOCALIZED_GENERATED_FILES["abc.ini"].force = True diff --git a/python/mozbuild/mozbuild/test/frontend/data/localized-generated-files/moz.build b/python/mozbuild/mozbuild/test/frontend/data/localized-generated-files/moz.build new file mode 100644 index 0000000000..cc306d5991 --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/localized-generated-files/moz.build @@ -0,0 +1,5 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# Any copyright is dedicated to the Public Domain. +# http://creativecommons.org/publicdomain/zero/1.0/ + +LOCALIZED_GENERATED_FILES += ["abc.ini", ("bar", "baz")] diff --git a/python/mozbuild/mozbuild/test/frontend/data/localized-pp-files/en-US/bar.ini b/python/mozbuild/mozbuild/test/frontend/data/localized-pp-files/en-US/bar.ini new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/localized-pp-files/en-US/bar.ini diff --git a/python/mozbuild/mozbuild/test/frontend/data/localized-pp-files/en-US/foo.js b/python/mozbuild/mozbuild/test/frontend/data/localized-pp-files/en-US/foo.js new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/localized-pp-files/en-US/foo.js diff --git a/python/mozbuild/mozbuild/test/frontend/data/localized-pp-files/moz.build b/python/mozbuild/mozbuild/test/frontend/data/localized-pp-files/moz.build new file mode 100644 index 0000000000..b2916a1226 --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/localized-pp-files/moz.build @@ -0,0 +1,8 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# Any copyright is dedicated to the Public Domain. +# http://creativecommons.org/publicdomain/zero/1.0/ + +LOCALIZED_PP_FILES.foo += [ + "en-US/bar.ini", + "en-US/foo.js", +] diff --git a/python/mozbuild/mozbuild/test/frontend/data/missing-local-includes/moz.build b/python/mozbuild/mozbuild/test/frontend/data/missing-local-includes/moz.build new file mode 100644 index 0000000000..1c29ac2ea2 --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/missing-local-includes/moz.build @@ -0,0 +1,5 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# Any copyright is dedicated to the Public Domain. +# http://creativecommons.org/publicdomain/zero/1.0/ + +LOCAL_INCLUDES += ["/bar/baz", "foo"] diff --git a/python/mozbuild/mozbuild/test/frontend/data/missing-xpidl/moz.build b/python/mozbuild/mozbuild/test/frontend/data/missing-xpidl/moz.build new file mode 100644 index 0000000000..e3a2a69d07 --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/missing-xpidl/moz.build @@ -0,0 +1,6 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# Any copyright is dedicated to the Public Domain. +# http://creativecommons.org/publicdomain/zero/1.0/ + +XPIDL_MODULE = "my_module" +XPIDL_SOURCES = ["nonexistant.idl"] diff --git a/python/mozbuild/mozbuild/test/frontend/data/multiple-rust-libraries/moz.build b/python/mozbuild/mozbuild/test/frontend/data/multiple-rust-libraries/moz.build new file mode 100644 index 0000000000..7956580d14 --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/multiple-rust-libraries/moz.build @@ -0,0 +1,29 @@ +# Any copyright is dedicated to the Public Domain. +# http://creativecommons.org/publicdomain/zero/1.0/ + + +@template +def Library(name): + """Template for libraries.""" + LIBRARY_NAME = name + + +@template +def RustLibrary(name): + """Template for Rust libraries.""" + Library(name) + + IS_RUST_LIBRARY = True + + +Library("test") + +DIRS += [ + "rust1", + "rust2", +] + +USE_LIBS += [ + "rust1", + "rust2", +] diff --git a/python/mozbuild/mozbuild/test/frontend/data/multiple-rust-libraries/rust1/Cargo.toml b/python/mozbuild/mozbuild/test/frontend/data/multiple-rust-libraries/rust1/Cargo.toml new file mode 100644 index 0000000000..419e7907ad --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/multiple-rust-libraries/rust1/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "rust1" +version = "0.1.0" +authors = [ + "The Mozilla Project Developers", +] + +[lib] +crate-type = ["staticlib"] + +[profile.dev] +panic = "abort" + +[profile.release] +panic = "abort" + +[dependencies] +mozilla-central-workspace-hack = { version = "0.1", features = ["rust1"], optional = true } diff --git a/python/mozbuild/mozbuild/test/frontend/data/multiple-rust-libraries/rust1/moz.build b/python/mozbuild/mozbuild/test/frontend/data/multiple-rust-libraries/rust1/moz.build new file mode 100644 index 0000000000..0cc01e1e24 --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/multiple-rust-libraries/rust1/moz.build @@ -0,0 +1,4 @@ +# Any copyright is dedicated to the Public Domain. +# http://creativecommons.org/publicdomain/zero/1.0/ + +RustLibrary("rust1") diff --git a/python/mozbuild/mozbuild/test/frontend/data/multiple-rust-libraries/rust2/Cargo.toml b/python/mozbuild/mozbuild/test/frontend/data/multiple-rust-libraries/rust2/Cargo.toml new file mode 100644 index 0000000000..a8872803aa --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/multiple-rust-libraries/rust2/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "rust2" +version = "0.1.0" +authors = [ + "The Mozilla Project Developers", +] + +[lib] +crate-type = ["staticlib"] + +[profile.dev] +panic = "abort" + +[profile.release] +panic = "abort" + +[dependencies] +mozilla-central-workspace-hack = { version = "0.1", features = ["rust2"], optional = true } diff --git a/python/mozbuild/mozbuild/test/frontend/data/multiple-rust-libraries/rust2/moz.build b/python/mozbuild/mozbuild/test/frontend/data/multiple-rust-libraries/rust2/moz.build new file mode 100644 index 0000000000..4ec4ea9c79 --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/multiple-rust-libraries/rust2/moz.build @@ -0,0 +1,4 @@ +# Any copyright is dedicated to the Public Domain. +# http://creativecommons.org/publicdomain/zero/1.0/ + +RustLibrary("rust2") diff --git a/python/mozbuild/mozbuild/test/frontend/data/object-conflicts/1/Test.c b/python/mozbuild/mozbuild/test/frontend/data/object-conflicts/1/Test.c new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/object-conflicts/1/Test.c diff --git a/python/mozbuild/mozbuild/test/frontend/data/object-conflicts/1/Test.cpp b/python/mozbuild/mozbuild/test/frontend/data/object-conflicts/1/Test.cpp new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/object-conflicts/1/Test.cpp diff --git a/python/mozbuild/mozbuild/test/frontend/data/object-conflicts/1/moz.build b/python/mozbuild/mozbuild/test/frontend/data/object-conflicts/1/moz.build new file mode 100644 index 0000000000..44610a781c --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/object-conflicts/1/moz.build @@ -0,0 +1,4 @@ +SOURCES += [ + "Test.c", + "Test.cpp", +] diff --git a/python/mozbuild/mozbuild/test/frontend/data/object-conflicts/2/Test.cpp b/python/mozbuild/mozbuild/test/frontend/data/object-conflicts/2/Test.cpp new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/object-conflicts/2/Test.cpp diff --git a/python/mozbuild/mozbuild/test/frontend/data/object-conflicts/2/moz.build b/python/mozbuild/mozbuild/test/frontend/data/object-conflicts/2/moz.build new file mode 100644 index 0000000000..b1064ae0c0 --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/object-conflicts/2/moz.build @@ -0,0 +1,4 @@ +SOURCES += [ + "subdir/Test.cpp", + "Test.cpp", +] diff --git a/python/mozbuild/mozbuild/test/frontend/data/object-conflicts/2/subdir/Test.cpp b/python/mozbuild/mozbuild/test/frontend/data/object-conflicts/2/subdir/Test.cpp new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/object-conflicts/2/subdir/Test.cpp diff --git a/python/mozbuild/mozbuild/test/frontend/data/object-conflicts/3/Test.c b/python/mozbuild/mozbuild/test/frontend/data/object-conflicts/3/Test.c new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/object-conflicts/3/Test.c diff --git a/python/mozbuild/mozbuild/test/frontend/data/object-conflicts/3/Test.cpp b/python/mozbuild/mozbuild/test/frontend/data/object-conflicts/3/Test.cpp new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/object-conflicts/3/Test.cpp diff --git a/python/mozbuild/mozbuild/test/frontend/data/object-conflicts/3/moz.build b/python/mozbuild/mozbuild/test/frontend/data/object-conflicts/3/moz.build new file mode 100644 index 0000000000..a225907cae --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/object-conflicts/3/moz.build @@ -0,0 +1,7 @@ +SOURCES += [ + "Test.c", +] + +UNIFIED_SOURCES += [ + "Test.cpp", +] diff --git a/python/mozbuild/mozbuild/test/frontend/data/object-conflicts/4/Test.c b/python/mozbuild/mozbuild/test/frontend/data/object-conflicts/4/Test.c new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/object-conflicts/4/Test.c diff --git a/python/mozbuild/mozbuild/test/frontend/data/object-conflicts/4/Test.cpp b/python/mozbuild/mozbuild/test/frontend/data/object-conflicts/4/Test.cpp new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/object-conflicts/4/Test.cpp diff --git a/python/mozbuild/mozbuild/test/frontend/data/object-conflicts/4/moz.build b/python/mozbuild/mozbuild/test/frontend/data/object-conflicts/4/moz.build new file mode 100644 index 0000000000..ea5da28d88 --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/object-conflicts/4/moz.build @@ -0,0 +1,4 @@ +UNIFIED_SOURCES += [ + "Test.c", + "Test.cpp", +] diff --git a/python/mozbuild/mozbuild/test/frontend/data/program-paths/dist-bin/moz.build b/python/mozbuild/mozbuild/test/frontend/data/program-paths/dist-bin/moz.build new file mode 100644 index 0000000000..d8b952c014 --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/program-paths/dist-bin/moz.build @@ -0,0 +1,4 @@ +# Any copyright is dedicated to the Public Domain. +# http://creativecommons.org/publicdomain/zero/1.0/ + +Program("dist-bin") diff --git a/python/mozbuild/mozbuild/test/frontend/data/program-paths/dist-subdir/moz.build b/python/mozbuild/mozbuild/test/frontend/data/program-paths/dist-subdir/moz.build new file mode 100644 index 0000000000..fc2f664c01 --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/program-paths/dist-subdir/moz.build @@ -0,0 +1,5 @@ +# Any copyright is dedicated to the Public Domain. +# http://creativecommons.org/publicdomain/zero/1.0/ + +DIST_SUBDIR = "foo" +Program("dist-subdir") diff --git a/python/mozbuild/mozbuild/test/frontend/data/program-paths/final-target/moz.build b/python/mozbuild/mozbuild/test/frontend/data/program-paths/final-target/moz.build new file mode 100644 index 0000000000..a0d5805262 --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/program-paths/final-target/moz.build @@ -0,0 +1,5 @@ +# Any copyright is dedicated to the Public Domain. +# http://creativecommons.org/publicdomain/zero/1.0/ + +FINAL_TARGET = "final/target" +Program("final-target") diff --git a/python/mozbuild/mozbuild/test/frontend/data/program-paths/moz.build b/python/mozbuild/mozbuild/test/frontend/data/program-paths/moz.build new file mode 100644 index 0000000000..d1d087fd45 --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/program-paths/moz.build @@ -0,0 +1,15 @@ +# Any copyright is dedicated to the Public Domain. +# http://creativecommons.org/publicdomain/zero/1.0/ + + +@template +def Program(name): + PROGRAM = name + + +DIRS += [ + "dist-bin", + "dist-subdir", + "final-target", + "not-installed", +] diff --git a/python/mozbuild/mozbuild/test/frontend/data/program-paths/not-installed/moz.build b/python/mozbuild/mozbuild/test/frontend/data/program-paths/not-installed/moz.build new file mode 100644 index 0000000000..c725ab7326 --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/program-paths/not-installed/moz.build @@ -0,0 +1,5 @@ +# Any copyright is dedicated to the Public Domain. +# http://creativecommons.org/publicdomain/zero/1.0/ + +DIST_INSTALL = False +Program("not-installed") diff --git a/python/mozbuild/mozbuild/test/frontend/data/program/moz.build b/python/mozbuild/mozbuild/test/frontend/data/program/moz.build new file mode 100644 index 0000000000..b3f7062732 --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/program/moz.build @@ -0,0 +1,18 @@ +# Any copyright is dedicated to the Public Domain. +# http://creativecommons.org/publicdomain/zero/1.0/ + + +@template +def Program(name): + PROGRAM = name + + +@template +def SimplePrograms(names, ext=".cpp"): + SIMPLE_PROGRAMS += names + SOURCES += ["%s%s" % (name, ext) for name in names] + + +Program("test_program") + +SimplePrograms(["test_program1", "test_program2"]) diff --git a/python/mozbuild/mozbuild/test/frontend/data/program/test_program1.cpp b/python/mozbuild/mozbuild/test/frontend/data/program/test_program1.cpp new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/program/test_program1.cpp diff --git a/python/mozbuild/mozbuild/test/frontend/data/program/test_program2.cpp b/python/mozbuild/mozbuild/test/frontend/data/program/test_program2.cpp new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/program/test_program2.cpp diff --git a/python/mozbuild/mozbuild/test/frontend/data/reader-error-bad-dir/moz.build b/python/mozbuild/mozbuild/test/frontend/data/reader-error-bad-dir/moz.build new file mode 100644 index 0000000000..68581574b1 --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/reader-error-bad-dir/moz.build @@ -0,0 +1,5 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# Any copyright is dedicated to the Public Domain. +# http://creativecommons.org/publicdomain/zero/1.0/ + +DIRS = ["foo"] diff --git a/python/mozbuild/mozbuild/test/frontend/data/reader-error-basic/moz.build b/python/mozbuild/mozbuild/test/frontend/data/reader-error-basic/moz.build new file mode 100644 index 0000000000..0a91c4692b --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/reader-error-basic/moz.build @@ -0,0 +1,5 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# Any copyright is dedicated to the Public Domain. +# http://creativecommons.org/publicdomain/zero/1.0/ + +ILLEGAL = True diff --git a/python/mozbuild/mozbuild/test/frontend/data/reader-error-empty-list/moz.build b/python/mozbuild/mozbuild/test/frontend/data/reader-error-empty-list/moz.build new file mode 100644 index 0000000000..4dfba1c60f --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/reader-error-empty-list/moz.build @@ -0,0 +1,5 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# Any copyright is dedicated to the Public Domain. +# http://creativecommons.org/publicdomain/zero/1.0/ + +DIRS = [] diff --git a/python/mozbuild/mozbuild/test/frontend/data/reader-error-error-func/moz.build b/python/mozbuild/mozbuild/test/frontend/data/reader-error-error-func/moz.build new file mode 100644 index 0000000000..d0f35c4c1d --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/reader-error-error-func/moz.build @@ -0,0 +1,5 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# Any copyright is dedicated to the Public Domain. +# http://creativecommons.org/publicdomain/zero/1.0/ + +error("Some error.") diff --git a/python/mozbuild/mozbuild/test/frontend/data/reader-error-included-from/child.build b/python/mozbuild/mozbuild/test/frontend/data/reader-error-included-from/child.build new file mode 100644 index 0000000000..9bfc65481d --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/reader-error-included-from/child.build @@ -0,0 +1,4 @@ +# Any copyright is dedicated to the Public Domain. +# http://creativecommons.org/publicdomain/zero/1.0/ + +ILLEGAL = True diff --git a/python/mozbuild/mozbuild/test/frontend/data/reader-error-included-from/moz.build b/python/mozbuild/mozbuild/test/frontend/data/reader-error-included-from/moz.build new file mode 100644 index 0000000000..603f3a7204 --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/reader-error-included-from/moz.build @@ -0,0 +1,5 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# Any copyright is dedicated to the Public Domain. +# http://creativecommons.org/publicdomain/zero/1.0/ + +include("child.build") diff --git a/python/mozbuild/mozbuild/test/frontend/data/reader-error-missing-include/moz.build b/python/mozbuild/mozbuild/test/frontend/data/reader-error-missing-include/moz.build new file mode 100644 index 0000000000..34129f7c93 --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/reader-error-missing-include/moz.build @@ -0,0 +1,5 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# Any copyright is dedicated to the Public Domain. +# http://creativecommons.org/publicdomain/zero/1.0/ + +include("missing.build") diff --git a/python/mozbuild/mozbuild/test/frontend/data/reader-error-outside-topsrcdir/moz.build b/python/mozbuild/mozbuild/test/frontend/data/reader-error-outside-topsrcdir/moz.build new file mode 100644 index 0000000000..040c1f5df1 --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/reader-error-outside-topsrcdir/moz.build @@ -0,0 +1,5 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# Any copyright is dedicated to the Public Domain. +# http://creativecommons.org/publicdomain/zero/1.0/ + +include("../include-basic/moz.build") diff --git a/python/mozbuild/mozbuild/test/frontend/data/reader-error-read-unknown-global/moz.build b/python/mozbuild/mozbuild/test/frontend/data/reader-error-read-unknown-global/moz.build new file mode 100644 index 0000000000..6fc10f766a --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/reader-error-read-unknown-global/moz.build @@ -0,0 +1,5 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# Any copyright is dedicated to the Public Domain. +# http://creativecommons.org/publicdomain/zero/1.0/ + +l = FOO diff --git a/python/mozbuild/mozbuild/test/frontend/data/reader-error-repeated-dir/moz.build b/python/mozbuild/mozbuild/test/frontend/data/reader-error-repeated-dir/moz.build new file mode 100644 index 0000000000..91845b337f --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/reader-error-repeated-dir/moz.build @@ -0,0 +1,7 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# Any copyright is dedicated to the Public Domain. +# http://creativecommons.org/publicdomain/zero/1.0/ + +DIRS = ["foo"] + +DIRS += ["foo"] diff --git a/python/mozbuild/mozbuild/test/frontend/data/reader-error-script-error/moz.build b/python/mozbuild/mozbuild/test/frontend/data/reader-error-script-error/moz.build new file mode 100644 index 0000000000..a91d38b415 --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/reader-error-script-error/moz.build @@ -0,0 +1,5 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# Any copyright is dedicated to the Public Domain. +# http://creativecommons.org/publicdomain/zero/1.0/ + +foo = True + None diff --git a/python/mozbuild/mozbuild/test/frontend/data/reader-error-syntax/moz.build b/python/mozbuild/mozbuild/test/frontend/data/reader-error-syntax/moz.build new file mode 100644 index 0000000000..70a0d2c066 --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/reader-error-syntax/moz.build @@ -0,0 +1,5 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# Any copyright is dedicated to the Public Domain. +# http://creativecommons.org/publicdomain/zero/1.0/ + +foo = diff --git a/python/mozbuild/mozbuild/test/frontend/data/reader-error-write-bad-value/moz.build b/python/mozbuild/mozbuild/test/frontend/data/reader-error-write-bad-value/moz.build new file mode 100644 index 0000000000..2e8194b223 --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/reader-error-write-bad-value/moz.build @@ -0,0 +1,5 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# Any copyright is dedicated to the Public Domain. +# http://creativecommons.org/publicdomain/zero/1.0/ + +DIRS = "dir" diff --git a/python/mozbuild/mozbuild/test/frontend/data/reader-error-write-unknown-global/moz.build b/python/mozbuild/mozbuild/test/frontend/data/reader-error-write-unknown-global/moz.build new file mode 100644 index 0000000000..5675031753 --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/reader-error-write-unknown-global/moz.build @@ -0,0 +1,7 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# Any copyright is dedicated to the Public Domain. +# http://creativecommons.org/publicdomain/zero/1.0/ + +DIRS = ["dir1", "dir2"] + +FOO = "bar" diff --git a/python/mozbuild/mozbuild/test/frontend/data/reader-relevant-mozbuild/d1/every-level/a/file b/python/mozbuild/mozbuild/test/frontend/data/reader-relevant-mozbuild/d1/every-level/a/file new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/reader-relevant-mozbuild/d1/every-level/a/file diff --git a/python/mozbuild/mozbuild/test/frontend/data/reader-relevant-mozbuild/d1/every-level/a/moz.build b/python/mozbuild/mozbuild/test/frontend/data/reader-relevant-mozbuild/d1/every-level/a/moz.build new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/reader-relevant-mozbuild/d1/every-level/a/moz.build diff --git a/python/mozbuild/mozbuild/test/frontend/data/reader-relevant-mozbuild/d1/every-level/b/file b/python/mozbuild/mozbuild/test/frontend/data/reader-relevant-mozbuild/d1/every-level/b/file new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/reader-relevant-mozbuild/d1/every-level/b/file diff --git a/python/mozbuild/mozbuild/test/frontend/data/reader-relevant-mozbuild/d1/every-level/b/moz.build b/python/mozbuild/mozbuild/test/frontend/data/reader-relevant-mozbuild/d1/every-level/b/moz.build new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/reader-relevant-mozbuild/d1/every-level/b/moz.build diff --git a/python/mozbuild/mozbuild/test/frontend/data/reader-relevant-mozbuild/d1/every-level/moz.build b/python/mozbuild/mozbuild/test/frontend/data/reader-relevant-mozbuild/d1/every-level/moz.build new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/reader-relevant-mozbuild/d1/every-level/moz.build diff --git a/python/mozbuild/mozbuild/test/frontend/data/reader-relevant-mozbuild/d1/file1 b/python/mozbuild/mozbuild/test/frontend/data/reader-relevant-mozbuild/d1/file1 new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/reader-relevant-mozbuild/d1/file1 diff --git a/python/mozbuild/mozbuild/test/frontend/data/reader-relevant-mozbuild/d1/file2 b/python/mozbuild/mozbuild/test/frontend/data/reader-relevant-mozbuild/d1/file2 new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/reader-relevant-mozbuild/d1/file2 diff --git a/python/mozbuild/mozbuild/test/frontend/data/reader-relevant-mozbuild/d1/moz.build b/python/mozbuild/mozbuild/test/frontend/data/reader-relevant-mozbuild/d1/moz.build new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/reader-relevant-mozbuild/d1/moz.build diff --git a/python/mozbuild/mozbuild/test/frontend/data/reader-relevant-mozbuild/d1/no-intermediate-moz-build/child/file b/python/mozbuild/mozbuild/test/frontend/data/reader-relevant-mozbuild/d1/no-intermediate-moz-build/child/file new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/reader-relevant-mozbuild/d1/no-intermediate-moz-build/child/file diff --git a/python/mozbuild/mozbuild/test/frontend/data/reader-relevant-mozbuild/d1/no-intermediate-moz-build/child/moz.build b/python/mozbuild/mozbuild/test/frontend/data/reader-relevant-mozbuild/d1/no-intermediate-moz-build/child/moz.build new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/reader-relevant-mozbuild/d1/no-intermediate-moz-build/child/moz.build diff --git a/python/mozbuild/mozbuild/test/frontend/data/reader-relevant-mozbuild/d1/parent-is-far/dir1/dir2/dir3/file b/python/mozbuild/mozbuild/test/frontend/data/reader-relevant-mozbuild/d1/parent-is-far/dir1/dir2/dir3/file new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/reader-relevant-mozbuild/d1/parent-is-far/dir1/dir2/dir3/file diff --git a/python/mozbuild/mozbuild/test/frontend/data/reader-relevant-mozbuild/d1/parent-is-far/moz.build b/python/mozbuild/mozbuild/test/frontend/data/reader-relevant-mozbuild/d1/parent-is-far/moz.build new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/reader-relevant-mozbuild/d1/parent-is-far/moz.build diff --git a/python/mozbuild/mozbuild/test/frontend/data/reader-relevant-mozbuild/d2/dir1/file b/python/mozbuild/mozbuild/test/frontend/data/reader-relevant-mozbuild/d2/dir1/file new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/reader-relevant-mozbuild/d2/dir1/file diff --git a/python/mozbuild/mozbuild/test/frontend/data/reader-relevant-mozbuild/d2/dir1/moz.build b/python/mozbuild/mozbuild/test/frontend/data/reader-relevant-mozbuild/d2/dir1/moz.build new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/reader-relevant-mozbuild/d2/dir1/moz.build diff --git a/python/mozbuild/mozbuild/test/frontend/data/reader-relevant-mozbuild/d2/dir2/file b/python/mozbuild/mozbuild/test/frontend/data/reader-relevant-mozbuild/d2/dir2/file new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/reader-relevant-mozbuild/d2/dir2/file diff --git a/python/mozbuild/mozbuild/test/frontend/data/reader-relevant-mozbuild/d2/dir2/moz.build b/python/mozbuild/mozbuild/test/frontend/data/reader-relevant-mozbuild/d2/dir2/moz.build new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/reader-relevant-mozbuild/d2/dir2/moz.build diff --git a/python/mozbuild/mozbuild/test/frontend/data/reader-relevant-mozbuild/d2/moz.build b/python/mozbuild/mozbuild/test/frontend/data/reader-relevant-mozbuild/d2/moz.build new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/reader-relevant-mozbuild/d2/moz.build diff --git a/python/mozbuild/mozbuild/test/frontend/data/reader-relevant-mozbuild/file b/python/mozbuild/mozbuild/test/frontend/data/reader-relevant-mozbuild/file new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/reader-relevant-mozbuild/file diff --git a/python/mozbuild/mozbuild/test/frontend/data/reader-relevant-mozbuild/moz.build b/python/mozbuild/mozbuild/test/frontend/data/reader-relevant-mozbuild/moz.build new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/reader-relevant-mozbuild/moz.build diff --git a/python/mozbuild/mozbuild/test/frontend/data/resolved-flags-error/moz.build b/python/mozbuild/mozbuild/test/frontend/data/resolved-flags-error/moz.build new file mode 100644 index 0000000000..d4b9a3075d --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/resolved-flags-error/moz.build @@ -0,0 +1,17 @@ +# Any copyright is dedicated to the Public Domain. +# http://creativecommons.org/publicdomain/zero/1.0/ + + +@template +def Library(name): + """Template for libraries.""" + LIBRARY_NAME = name + + +Library("dummy") + +UNIFIED_SOURCES += ["test1.c"] + +DEFINES["MOZ_TEST_DEFINE"] = True +LIBRARY_DEFINES["MOZ_LIBRARY_DEFINE"] = "MOZ_TEST" +COMPILE_FLAGS["DEFINES"] = ["-DFOO"] diff --git a/python/mozbuild/mozbuild/test/frontend/data/resolved-flags-error/test1.c b/python/mozbuild/mozbuild/test/frontend/data/resolved-flags-error/test1.c new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/resolved-flags-error/test1.c diff --git a/python/mozbuild/mozbuild/test/frontend/data/rust-library-dash-folding/Cargo.toml b/python/mozbuild/mozbuild/test/frontend/data/rust-library-dash-folding/Cargo.toml new file mode 100644 index 0000000000..d0b62a3a4f --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/rust-library-dash-folding/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "random-crate" +version = "0.1.0" +authors = [ + "The Mozilla Project Developers", +] + +[lib] +crate-type = ["staticlib"] + +[profile.dev] +panic = "abort" + +[profile.release] +panic = "abort" + +[dependencies] +mozilla-central-workspace-hack = { version = "0.1", features = ["random-crate"], optional = true } diff --git a/python/mozbuild/mozbuild/test/frontend/data/rust-library-dash-folding/moz.build b/python/mozbuild/mozbuild/test/frontend/data/rust-library-dash-folding/moz.build new file mode 100644 index 0000000000..de1967c519 --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/rust-library-dash-folding/moz.build @@ -0,0 +1,19 @@ +# Any copyright is dedicated to the Public Domain. +# http://creativecommons.org/publicdomain/zero/1.0/ + + +@template +def Library(name): + """Template for libraries.""" + LIBRARY_NAME = name + + +@template +def RustLibrary(name): + """Template for Rust libraries.""" + Library(name) + + IS_RUST_LIBRARY = True + + +RustLibrary("random-crate") diff --git a/python/mozbuild/mozbuild/test/frontend/data/rust-library-duplicate-features/Cargo.toml b/python/mozbuild/mozbuild/test/frontend/data/rust-library-duplicate-features/Cargo.toml new file mode 100644 index 0000000000..d0b62a3a4f --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/rust-library-duplicate-features/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "random-crate" +version = "0.1.0" +authors = [ + "The Mozilla Project Developers", +] + +[lib] +crate-type = ["staticlib"] + +[profile.dev] +panic = "abort" + +[profile.release] +panic = "abort" + +[dependencies] +mozilla-central-workspace-hack = { version = "0.1", features = ["random-crate"], optional = true } diff --git a/python/mozbuild/mozbuild/test/frontend/data/rust-library-duplicate-features/moz.build b/python/mozbuild/mozbuild/test/frontend/data/rust-library-duplicate-features/moz.build new file mode 100644 index 0000000000..ccd8ede3c0 --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/rust-library-duplicate-features/moz.build @@ -0,0 +1,20 @@ +# Any copyright is dedicated to the Public Domain. +# http://creativecommons.org/publicdomain/zero/1.0/ + + +@template +def Library(name): + """Template for libraries.""" + LIBRARY_NAME = name + + +@template +def RustLibrary(name, features): + """Template for Rust libraries.""" + Library(name) + + IS_RUST_LIBRARY = True + RUST_LIBRARY_FEATURES = features + + +RustLibrary("random-crate", ["musthave", "cantlivewithout", "musthave"]) diff --git a/python/mozbuild/mozbuild/test/frontend/data/rust-library-features/Cargo.toml b/python/mozbuild/mozbuild/test/frontend/data/rust-library-features/Cargo.toml new file mode 100644 index 0000000000..d0b62a3a4f --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/rust-library-features/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "random-crate" +version = "0.1.0" +authors = [ + "The Mozilla Project Developers", +] + +[lib] +crate-type = ["staticlib"] + +[profile.dev] +panic = "abort" + +[profile.release] +panic = "abort" + +[dependencies] +mozilla-central-workspace-hack = { version = "0.1", features = ["random-crate"], optional = true } diff --git a/python/mozbuild/mozbuild/test/frontend/data/rust-library-features/moz.build b/python/mozbuild/mozbuild/test/frontend/data/rust-library-features/moz.build new file mode 100644 index 0000000000..9d88bdea08 --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/rust-library-features/moz.build @@ -0,0 +1,20 @@ +# Any copyright is dedicated to the Public Domain. +# http://creativecommons.org/publicdomain/zero/1.0/ + + +@template +def Library(name): + """Template for libraries.""" + LIBRARY_NAME = name + + +@template +def RustLibrary(name, features): + """Template for Rust libraries.""" + Library(name) + + IS_RUST_LIBRARY = True + RUST_LIBRARY_FEATURES = features + + +RustLibrary("random-crate", ["musthave", "cantlivewithout"]) diff --git a/python/mozbuild/mozbuild/test/frontend/data/rust-library-invalid-crate-type/Cargo.toml b/python/mozbuild/mozbuild/test/frontend/data/rust-library-invalid-crate-type/Cargo.toml new file mode 100644 index 0000000000..b0da98887d --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/rust-library-invalid-crate-type/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "random-crate" +version = "0.1.0" +authors = [ + "The Mozilla Project Developers", +] + +[lib] +crate-type = ["dylib"] + +[profile.dev] +panic = "abort" + +[profile.release] +panic = "abort" + +[dependencies] +mozilla-central-workspace-hack = { version = "0.1", features = ["random-crate"], optional = true } diff --git a/python/mozbuild/mozbuild/test/frontend/data/rust-library-invalid-crate-type/moz.build b/python/mozbuild/mozbuild/test/frontend/data/rust-library-invalid-crate-type/moz.build new file mode 100644 index 0000000000..de1967c519 --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/rust-library-invalid-crate-type/moz.build @@ -0,0 +1,19 @@ +# Any copyright is dedicated to the Public Domain. +# http://creativecommons.org/publicdomain/zero/1.0/ + + +@template +def Library(name): + """Template for libraries.""" + LIBRARY_NAME = name + + +@template +def RustLibrary(name): + """Template for Rust libraries.""" + Library(name) + + IS_RUST_LIBRARY = True + + +RustLibrary("random-crate") diff --git a/python/mozbuild/mozbuild/test/frontend/data/rust-library-name-mismatch/Cargo.toml b/python/mozbuild/mozbuild/test/frontend/data/rust-library-name-mismatch/Cargo.toml new file mode 100644 index 0000000000..127509a3e6 --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/rust-library-name-mismatch/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "deterministic-crate" +version = "0.1.0" +authors = [ + "The Mozilla Project Developers", +] + +[profile.dev] +panic = "abort" + +[profile.release] +panic = "abort" + +[dependencies] +mozilla-central-workspace-hack = { version = "0.1", features = ["deterministic-crate"], optional = true } diff --git a/python/mozbuild/mozbuild/test/frontend/data/rust-library-name-mismatch/moz.build b/python/mozbuild/mozbuild/test/frontend/data/rust-library-name-mismatch/moz.build new file mode 100644 index 0000000000..de1967c519 --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/rust-library-name-mismatch/moz.build @@ -0,0 +1,19 @@ +# Any copyright is dedicated to the Public Domain. +# http://creativecommons.org/publicdomain/zero/1.0/ + + +@template +def Library(name): + """Template for libraries.""" + LIBRARY_NAME = name + + +@template +def RustLibrary(name): + """Template for Rust libraries.""" + Library(name) + + IS_RUST_LIBRARY = True + + +RustLibrary("random-crate") diff --git a/python/mozbuild/mozbuild/test/frontend/data/rust-library-no-cargo-toml/moz.build b/python/mozbuild/mozbuild/test/frontend/data/rust-library-no-cargo-toml/moz.build new file mode 100644 index 0000000000..de1967c519 --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/rust-library-no-cargo-toml/moz.build @@ -0,0 +1,19 @@ +# Any copyright is dedicated to the Public Domain. +# http://creativecommons.org/publicdomain/zero/1.0/ + + +@template +def Library(name): + """Template for libraries.""" + LIBRARY_NAME = name + + +@template +def RustLibrary(name): + """Template for Rust libraries.""" + Library(name) + + IS_RUST_LIBRARY = True + + +RustLibrary("random-crate") diff --git a/python/mozbuild/mozbuild/test/frontend/data/rust-library-no-lib-section/Cargo.toml b/python/mozbuild/mozbuild/test/frontend/data/rust-library-no-lib-section/Cargo.toml new file mode 100644 index 0000000000..bca0070ce2 --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/rust-library-no-lib-section/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "random-crate" +version = "0.1.0" +authors = [ + "The Mozilla Project Developers", +] + +[profile.dev] +panic = "abort" + +[profile.release] +panic = "abort" + +[dependencies] +mozilla-central-workspace-hack = { version = "0.1", features = ["random-crate"], optional = true } diff --git a/python/mozbuild/mozbuild/test/frontend/data/rust-library-no-lib-section/moz.build b/python/mozbuild/mozbuild/test/frontend/data/rust-library-no-lib-section/moz.build new file mode 100644 index 0000000000..de1967c519 --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/rust-library-no-lib-section/moz.build @@ -0,0 +1,19 @@ +# Any copyright is dedicated to the Public Domain. +# http://creativecommons.org/publicdomain/zero/1.0/ + + +@template +def Library(name): + """Template for libraries.""" + LIBRARY_NAME = name + + +@template +def RustLibrary(name): + """Template for Rust libraries.""" + Library(name) + + IS_RUST_LIBRARY = True + + +RustLibrary("random-crate") diff --git a/python/mozbuild/mozbuild/test/frontend/data/rust-no-workspace-hack/Cargo.toml b/python/mozbuild/mozbuild/test/frontend/data/rust-no-workspace-hack/Cargo.toml new file mode 100644 index 0000000000..fbb4ae087d --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/rust-no-workspace-hack/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "random-crate" +version = "0.1.0" +authors = [ + "The Mozilla Project Developers", +] + +[lib] +crate-type = ["staticlib"] + +[profile.dev] +panic = "abort" + +[profile.release] +panic = "abort" diff --git a/python/mozbuild/mozbuild/test/frontend/data/rust-no-workspace-hack/moz.build b/python/mozbuild/mozbuild/test/frontend/data/rust-no-workspace-hack/moz.build new file mode 100644 index 0000000000..c82c03205c --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/rust-no-workspace-hack/moz.build @@ -0,0 +1,9 @@ +@template +def RustLibrary(name): + """Template for Rust libraries.""" + LIBRARY_NAME = name + + IS_RUST_LIBRARY = True + + +RustLibrary("random-crate") diff --git a/python/mozbuild/mozbuild/test/frontend/data/rust-old-workspace-hack/Cargo.toml b/python/mozbuild/mozbuild/test/frontend/data/rust-old-workspace-hack/Cargo.toml new file mode 100644 index 0000000000..e915c9a783 --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/rust-old-workspace-hack/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "random-crate" +version = "0.1.0" +authors = [ + "The Mozilla Project Developers", +] + +[lib] +crate-type = ["staticlib"] + +[profile.dev] +panic = "abort" + +[profile.release] +panic = "abort" + +[dependencies] +mozilla-central-workspace-hack = { path = "../../../../../../../build/workspace-hack" } diff --git a/python/mozbuild/mozbuild/test/frontend/data/rust-old-workspace-hack/moz.build b/python/mozbuild/mozbuild/test/frontend/data/rust-old-workspace-hack/moz.build new file mode 100644 index 0000000000..c82c03205c --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/rust-old-workspace-hack/moz.build @@ -0,0 +1,9 @@ +@template +def RustLibrary(name): + """Template for Rust libraries.""" + LIBRARY_NAME = name + + IS_RUST_LIBRARY = True + + +RustLibrary("random-crate") diff --git a/python/mozbuild/mozbuild/test/frontend/data/rust-program-no-cargo-toml/moz.build b/python/mozbuild/mozbuild/test/frontend/data/rust-program-no-cargo-toml/moz.build new file mode 100644 index 0000000000..56601854f9 --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/rust-program-no-cargo-toml/moz.build @@ -0,0 +1 @@ +RUST_PROGRAMS += ["none"] diff --git a/python/mozbuild/mozbuild/test/frontend/data/rust-program-nonexistent-name/Cargo.toml b/python/mozbuild/mozbuild/test/frontend/data/rust-program-nonexistent-name/Cargo.toml new file mode 100644 index 0000000000..e5ea7b26a6 --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/rust-program-nonexistent-name/Cargo.toml @@ -0,0 +1,10 @@ +[package] +authors = ["The Mozilla Project Developers"] +name = "testing" +version = "0.0.1" + +[[bin]] +name = "some" + +[dependencies] +mozilla-central-workspace-hack = { version = "0.1", features = ["testing"], optional = true } diff --git a/python/mozbuild/mozbuild/test/frontend/data/rust-program-nonexistent-name/moz.build b/python/mozbuild/mozbuild/test/frontend/data/rust-program-nonexistent-name/moz.build new file mode 100644 index 0000000000..56601854f9 --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/rust-program-nonexistent-name/moz.build @@ -0,0 +1 @@ +RUST_PROGRAMS += ["none"] diff --git a/python/mozbuild/mozbuild/test/frontend/data/rust-programs/Cargo.toml b/python/mozbuild/mozbuild/test/frontend/data/rust-programs/Cargo.toml new file mode 100644 index 0000000000..e5ea7b26a6 --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/rust-programs/Cargo.toml @@ -0,0 +1,10 @@ +[package] +authors = ["The Mozilla Project Developers"] +name = "testing" +version = "0.0.1" + +[[bin]] +name = "some" + +[dependencies] +mozilla-central-workspace-hack = { version = "0.1", features = ["testing"], optional = true } diff --git a/python/mozbuild/mozbuild/test/frontend/data/rust-programs/moz.build b/python/mozbuild/mozbuild/test/frontend/data/rust-programs/moz.build new file mode 100644 index 0000000000..80dc15120a --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/rust-programs/moz.build @@ -0,0 +1 @@ +RUST_PROGRAMS += ["some"] diff --git a/python/mozbuild/mozbuild/test/frontend/data/schedules/moz.build b/python/mozbuild/mozbuild/test/frontend/data/schedules/moz.build new file mode 100644 index 0000000000..3f4f450d37 --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/schedules/moz.build @@ -0,0 +1,19 @@ +# Any copyright is dedicated to the Public Domain. +# http://creativecommons.org/publicdomain/zero/1.0/ + +with Files("*.win"): + SCHEDULES.exclusive = ["windows"] + +with Files("*.osx"): + SCHEDULES.exclusive = ["macosx"] + +with Files("win.and.osx"): + # this conflicts with the previous clause and will cause an error + # when read + SCHEDULES.exclusive = ["macosx", "windows"] + +with Files("subd/**.py"): + SCHEDULES.inclusive += ["py-lint"] + +with Files("**/*.js"): + SCHEDULES.inclusive += ["js-lint"] diff --git a/python/mozbuild/mozbuild/test/frontend/data/schedules/subd/moz.build b/python/mozbuild/mozbuild/test/frontend/data/schedules/subd/moz.build new file mode 100644 index 0000000000..b9c3bf6c74 --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/schedules/subd/moz.build @@ -0,0 +1,5 @@ +with Files("yaml.py"): + SCHEDULES.inclusive += ["yaml-lint"] + +with Files("win.js"): + SCHEDULES.exclusive = ["windows"] diff --git a/python/mozbuild/mozbuild/test/frontend/data/sources-just-c/d.c b/python/mozbuild/mozbuild/test/frontend/data/sources-just-c/d.c new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/sources-just-c/d.c diff --git a/python/mozbuild/mozbuild/test/frontend/data/sources-just-c/e.m b/python/mozbuild/mozbuild/test/frontend/data/sources-just-c/e.m new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/sources-just-c/e.m diff --git a/python/mozbuild/mozbuild/test/frontend/data/sources-just-c/g.S b/python/mozbuild/mozbuild/test/frontend/data/sources-just-c/g.S new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/sources-just-c/g.S diff --git a/python/mozbuild/mozbuild/test/frontend/data/sources-just-c/h.s b/python/mozbuild/mozbuild/test/frontend/data/sources-just-c/h.s new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/sources-just-c/h.s diff --git a/python/mozbuild/mozbuild/test/frontend/data/sources-just-c/i.asm b/python/mozbuild/mozbuild/test/frontend/data/sources-just-c/i.asm new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/sources-just-c/i.asm diff --git a/python/mozbuild/mozbuild/test/frontend/data/sources-just-c/moz.build b/python/mozbuild/mozbuild/test/frontend/data/sources-just-c/moz.build new file mode 100644 index 0000000000..29abd6de5d --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/sources-just-c/moz.build @@ -0,0 +1,29 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# Any copyright is dedicated to the Public Domain. +# http://creativecommons.org/publicdomain/zero/1.0/ + + +@template +def Library(name): + """Template for libraries.""" + LIBRARY_NAME = name + + +Library("dummy") + +SOURCES += [ + "d.c", +] + +SOURCES += [ + "e.m", +] + +SOURCES += [ + "g.S", +] + +SOURCES += [ + "h.s", + "i.asm", +] diff --git a/python/mozbuild/mozbuild/test/frontend/data/sources/a.cpp b/python/mozbuild/mozbuild/test/frontend/data/sources/a.cpp new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/sources/a.cpp diff --git a/python/mozbuild/mozbuild/test/frontend/data/sources/b.cc b/python/mozbuild/mozbuild/test/frontend/data/sources/b.cc new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/sources/b.cc diff --git a/python/mozbuild/mozbuild/test/frontend/data/sources/c.cxx b/python/mozbuild/mozbuild/test/frontend/data/sources/c.cxx new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/sources/c.cxx diff --git a/python/mozbuild/mozbuild/test/frontend/data/sources/d.c b/python/mozbuild/mozbuild/test/frontend/data/sources/d.c new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/sources/d.c diff --git a/python/mozbuild/mozbuild/test/frontend/data/sources/e.m b/python/mozbuild/mozbuild/test/frontend/data/sources/e.m new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/sources/e.m diff --git a/python/mozbuild/mozbuild/test/frontend/data/sources/f.mm b/python/mozbuild/mozbuild/test/frontend/data/sources/f.mm new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/sources/f.mm diff --git a/python/mozbuild/mozbuild/test/frontend/data/sources/g.S b/python/mozbuild/mozbuild/test/frontend/data/sources/g.S new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/sources/g.S diff --git a/python/mozbuild/mozbuild/test/frontend/data/sources/h.s b/python/mozbuild/mozbuild/test/frontend/data/sources/h.s new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/sources/h.s diff --git a/python/mozbuild/mozbuild/test/frontend/data/sources/i.asm b/python/mozbuild/mozbuild/test/frontend/data/sources/i.asm new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/sources/i.asm diff --git a/python/mozbuild/mozbuild/test/frontend/data/sources/moz.build b/python/mozbuild/mozbuild/test/frontend/data/sources/moz.build new file mode 100644 index 0000000000..e25f865f72 --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/sources/moz.build @@ -0,0 +1,39 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# Any copyright is dedicated to the Public Domain. +# http://creativecommons.org/publicdomain/zero/1.0/ + + +@template +def Library(name): + """Template for libraries.""" + LIBRARY_NAME = name + + +Library("dummy") + +SOURCES += [ + "a.cpp", + "b.cc", + "c.cxx", +] + +SOURCES += [ + "d.c", +] + +SOURCES += [ + "e.m", +] + +SOURCES += [ + "f.mm", +] + +SOURCES += [ + "g.S", +] + +SOURCES += [ + "h.s", + "i.asm", +] diff --git a/python/mozbuild/mozbuild/test/frontend/data/templates/templates.mozbuild b/python/mozbuild/mozbuild/test/frontend/data/templates/templates.mozbuild new file mode 100644 index 0000000000..33b4346dfe --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/templates/templates.mozbuild @@ -0,0 +1,25 @@ +@template +def Template(foo, bar=[]): + SOURCES += foo + DIRS += bar + + +@template +def TemplateError(foo): + ILLEGAL = foo + + +@template +def TemplateGlobalVariable(): + SOURCES += illegal + + +@template +def TemplateGlobalUPPERVariable(): + SOURCES += DIRS + + +@template +def TemplateInherit(foo): + USE_LIBS += ["foo"] + Template(foo) diff --git a/python/mozbuild/mozbuild/test/frontend/data/test-harness-files-root/moz.build b/python/mozbuild/mozbuild/test/frontend/data/test-harness-files-root/moz.build new file mode 100644 index 0000000000..d7f6377d0d --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/test-harness-files-root/moz.build @@ -0,0 +1,4 @@ +# Any copyright is dedicated to the Public Domain. +# http://creativecommons.org/publicdomain/zero/1.0/ + +TEST_HARNESS_FILES += ["foo.py"] diff --git a/python/mozbuild/mozbuild/test/frontend/data/test-harness-files/mochitest.py b/python/mozbuild/mozbuild/test/frontend/data/test-harness-files/mochitest.py new file mode 100644 index 0000000000..d87114ac7d --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/test-harness-files/mochitest.py @@ -0,0 +1 @@ +# dummy file so the existence checks for TEST_HARNESS_FILES succeed diff --git a/python/mozbuild/mozbuild/test/frontend/data/test-harness-files/mochitest.toml b/python/mozbuild/mozbuild/test/frontend/data/test-harness-files/mochitest.toml new file mode 100644 index 0000000000..db4574b581 --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/test-harness-files/mochitest.toml @@ -0,0 +1 @@ +[DEFAULT] diff --git a/python/mozbuild/mozbuild/test/frontend/data/test-harness-files/moz.build b/python/mozbuild/mozbuild/test/frontend/data/test-harness-files/moz.build new file mode 100644 index 0000000000..8e2acab145 --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/test-harness-files/moz.build @@ -0,0 +1,7 @@ +# Any copyright is dedicated to the Public Domain. +# http://creativecommons.org/publicdomain/zero/1.0/ + +TEST_HARNESS_FILES.mochitest += ["runtests.py"] +TEST_HARNESS_FILES.mochitest += ["utils.py"] +TEST_HARNESS_FILES.testing.mochitest += ["mochitest.py"] +TEST_HARNESS_FILES.testing.mochitest += ["mochitest.toml"] diff --git a/python/mozbuild/mozbuild/test/frontend/data/test-harness-files/runtests.py b/python/mozbuild/mozbuild/test/frontend/data/test-harness-files/runtests.py new file mode 100644 index 0000000000..d87114ac7d --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/test-harness-files/runtests.py @@ -0,0 +1 @@ +# dummy file so the existence checks for TEST_HARNESS_FILES succeed diff --git a/python/mozbuild/mozbuild/test/frontend/data/test-harness-files/utils.py b/python/mozbuild/mozbuild/test/frontend/data/test-harness-files/utils.py new file mode 100644 index 0000000000..d87114ac7d --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/test-harness-files/utils.py @@ -0,0 +1 @@ +# dummy file so the existence checks for TEST_HARNESS_FILES succeed diff --git a/python/mozbuild/mozbuild/test/frontend/data/test-install-shared-lib/moz.build b/python/mozbuild/mozbuild/test/frontend/data/test-install-shared-lib/moz.build new file mode 100644 index 0000000000..fa592c72a3 --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/test-install-shared-lib/moz.build @@ -0,0 +1,16 @@ +# Any copyright is dedicated to the Public Domain. +# http://creativecommons.org/publicdomain/zero/1.0/ + + +@template +def SharedLibrary(name): + LIBRARY_NAME = name + FORCE_SHARED_LIB = True + + +DIST_INSTALL = False +SharedLibrary("foo") + +TEST_HARNESS_FILES.foo.bar += [ + "!%sfoo%s" % (CONFIG["DLL_PREFIX"], CONFIG["DLL_SUFFIX"]) +] diff --git a/python/mozbuild/mozbuild/test/frontend/data/test-linkables-cxx-link/moz.build b/python/mozbuild/mozbuild/test/frontend/data/test-linkables-cxx-link/moz.build new file mode 100644 index 0000000000..0f84eb5554 --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/test-linkables-cxx-link/moz.build @@ -0,0 +1,14 @@ +# Any copyright is dedicated to the Public Domain. +# http://creativecommons.org/publicdomain/zero/1.0/ + +DIRS = ["one", "two", "three"] + + +@template +def SharedLibrary(name): + LIBRARY_NAME = name + FORCE_SHARED_LIB = True + + +SharedLibrary("cxx_shared") +USE_LIBS += ["cxx_static"] diff --git a/python/mozbuild/mozbuild/test/frontend/data/test-linkables-cxx-link/one/foo.cpp b/python/mozbuild/mozbuild/test/frontend/data/test-linkables-cxx-link/one/foo.cpp new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/test-linkables-cxx-link/one/foo.cpp diff --git a/python/mozbuild/mozbuild/test/frontend/data/test-linkables-cxx-link/one/moz.build b/python/mozbuild/mozbuild/test/frontend/data/test-linkables-cxx-link/one/moz.build new file mode 100644 index 0000000000..f03a34c33f --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/test-linkables-cxx-link/one/moz.build @@ -0,0 +1,11 @@ +# Any copyright is dedicated to the Public Domain. +# http://creativecommons.org/publicdomain/zero/1.0/ + + +@template +def Library(name): + LIBRARY_NAME = name + + +Library("cxx_static") +SOURCES += ["foo.cpp"] diff --git a/python/mozbuild/mozbuild/test/frontend/data/test-linkables-cxx-link/three/moz.build b/python/mozbuild/mozbuild/test/frontend/data/test-linkables-cxx-link/three/moz.build new file mode 100644 index 0000000000..08e26c4eb3 --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/test-linkables-cxx-link/three/moz.build @@ -0,0 +1,5 @@ +# Any copyright is dedicated to the Public Domain. +# http://creativecommons.org/publicdomain/zero/1.0/ + +SharedLibrary("just_c_shared") +USE_LIBS += ["just_c_static"] diff --git a/python/mozbuild/mozbuild/test/frontend/data/test-linkables-cxx-link/two/foo.c b/python/mozbuild/mozbuild/test/frontend/data/test-linkables-cxx-link/two/foo.c new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/test-linkables-cxx-link/two/foo.c diff --git a/python/mozbuild/mozbuild/test/frontend/data/test-linkables-cxx-link/two/moz.build b/python/mozbuild/mozbuild/test/frontend/data/test-linkables-cxx-link/two/moz.build new file mode 100644 index 0000000000..d3bb738ba4 --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/test-linkables-cxx-link/two/moz.build @@ -0,0 +1,11 @@ +# Any copyright is dedicated to the Public Domain. +# http://creativecommons.org/publicdomain/zero/1.0/ + + +@template +def Library(name): + LIBRARY_NAME = name + + +Library("just_c_static") +SOURCES += ["foo.c"] diff --git a/python/mozbuild/mozbuild/test/frontend/data/test-manifest-absolute-support/absolute-support.toml b/python/mozbuild/mozbuild/test/frontend/data/test-manifest-absolute-support/absolute-support.toml new file mode 100644 index 0000000000..e1dc490e00 --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/test-manifest-absolute-support/absolute-support.toml @@ -0,0 +1,4 @@ +[DEFAULT] +support-files = ["/.well-known/foo.txt"] + +["test_file.js"] diff --git a/python/mozbuild/mozbuild/test/frontend/data/test-manifest-absolute-support/foo.txt b/python/mozbuild/mozbuild/test/frontend/data/test-manifest-absolute-support/foo.txt new file mode 100644 index 0000000000..ce01362503 --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/test-manifest-absolute-support/foo.txt @@ -0,0 +1 @@ +hello diff --git a/python/mozbuild/mozbuild/test/frontend/data/test-manifest-absolute-support/moz.build b/python/mozbuild/mozbuild/test/frontend/data/test-manifest-absolute-support/moz.build new file mode 100644 index 0000000000..17ac7234a1 --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/test-manifest-absolute-support/moz.build @@ -0,0 +1,4 @@ +# Any copyright is dedicated to the Public Domain. +# http://creativecommons.org/publicdomain/zero/1.0/ + +MOCHITEST_MANIFESTS += ["absolute-support.toml"] diff --git a/python/mozbuild/mozbuild/test/frontend/data/test-manifest-absolute-support/test_file.js b/python/mozbuild/mozbuild/test/frontend/data/test-manifest-absolute-support/test_file.js new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/test-manifest-absolute-support/test_file.js diff --git a/python/mozbuild/mozbuild/test/frontend/data/test-manifest-dupes/bar.js b/python/mozbuild/mozbuild/test/frontend/data/test-manifest-dupes/bar.js new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/test-manifest-dupes/bar.js diff --git a/python/mozbuild/mozbuild/test/frontend/data/test-manifest-dupes/foo.js b/python/mozbuild/mozbuild/test/frontend/data/test-manifest-dupes/foo.js new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/test-manifest-dupes/foo.js diff --git a/python/mozbuild/mozbuild/test/frontend/data/test-manifest-dupes/mochitest.toml b/python/mozbuild/mozbuild/test/frontend/data/test-manifest-dupes/mochitest.toml new file mode 100644 index 0000000000..57669864d0 --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/test-manifest-dupes/mochitest.toml @@ -0,0 +1,8 @@ +[DEFAULT] +support-files = [ + "bar.js", + "foo.js", + "bar.js", +] + +["test_baz.js"] diff --git a/python/mozbuild/mozbuild/test/frontend/data/test-manifest-dupes/moz.build b/python/mozbuild/mozbuild/test/frontend/data/test-manifest-dupes/moz.build new file mode 100644 index 0000000000..d2b33added --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/test-manifest-dupes/moz.build @@ -0,0 +1,4 @@ +# Any copyright is dedicated to the Public Domain. +# http://creativecommons.org/publicdomain/zero/1.0/ + +MOCHITEST_MANIFESTS += ["mochitest.toml"] diff --git a/python/mozbuild/mozbuild/test/frontend/data/test-manifest-dupes/test_baz.js b/python/mozbuild/mozbuild/test/frontend/data/test-manifest-dupes/test_baz.js new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/test-manifest-dupes/test_baz.js diff --git a/python/mozbuild/mozbuild/test/frontend/data/test-manifest-emitted-includes/included-reftest.list b/python/mozbuild/mozbuild/test/frontend/data/test-manifest-emitted-includes/included-reftest.list new file mode 100644 index 0000000000..1caf9cc391 --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/test-manifest-emitted-includes/included-reftest.list @@ -0,0 +1 @@ +!= reftest2.html reftest2-ref.html
\ No newline at end of file diff --git a/python/mozbuild/mozbuild/test/frontend/data/test-manifest-emitted-includes/moz.build b/python/mozbuild/mozbuild/test/frontend/data/test-manifest-emitted-includes/moz.build new file mode 100644 index 0000000000..8f321387af --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/test-manifest-emitted-includes/moz.build @@ -0,0 +1 @@ +REFTEST_MANIFESTS += ["reftest.list"] diff --git a/python/mozbuild/mozbuild/test/frontend/data/test-manifest-emitted-includes/reftest.list b/python/mozbuild/mozbuild/test/frontend/data/test-manifest-emitted-includes/reftest.list new file mode 100644 index 0000000000..80caf8ffa4 --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/test-manifest-emitted-includes/reftest.list @@ -0,0 +1,2 @@ +== reftest1.html reftest1-ref.html +include included-reftest.list diff --git a/python/mozbuild/mozbuild/test/frontend/data/test-manifest-empty/empty.toml b/python/mozbuild/mozbuild/test/frontend/data/test-manifest-empty/empty.toml new file mode 100644 index 0000000000..cbf1eb1927 --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/test-manifest-empty/empty.toml @@ -0,0 +1,2 @@ +[DEFAULT] +foo = "bar" diff --git a/python/mozbuild/mozbuild/test/frontend/data/test-manifest-empty/moz.build b/python/mozbuild/mozbuild/test/frontend/data/test-manifest-empty/moz.build new file mode 100644 index 0000000000..c6adbbead9 --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/test-manifest-empty/moz.build @@ -0,0 +1,4 @@ +# Any copyright is dedicated to the Public Domain. +# http://creativecommons.org/publicdomain/zero/1.0/ + +MOCHITEST_MANIFESTS += ["empty.toml"] diff --git a/python/mozbuild/mozbuild/test/frontend/data/test-manifest-inactive-ignored/test_inactive.html b/python/mozbuild/mozbuild/test/frontend/data/test-manifest-inactive-ignored/test_inactive.html new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/test-manifest-inactive-ignored/test_inactive.html diff --git a/python/mozbuild/mozbuild/test/frontend/data/test-manifest-install-includes/common.toml b/python/mozbuild/mozbuild/test/frontend/data/test-manifest-install-includes/common.toml new file mode 100644 index 0000000000..6c1ad745ec --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/test-manifest-install-includes/common.toml @@ -0,0 +1,3 @@ +[DEFAULT] + +["test_foo.html"] diff --git a/python/mozbuild/mozbuild/test/frontend/data/test-manifest-install-includes/mochitest.toml b/python/mozbuild/mozbuild/test/frontend/data/test-manifest-install-includes/mochitest.toml new file mode 100644 index 0000000000..f7810d34c4 --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/test-manifest-install-includes/mochitest.toml @@ -0,0 +1,3 @@ +[DEFAULT] + +["include:common.ini"] diff --git a/python/mozbuild/mozbuild/test/frontend/data/test-manifest-install-includes/moz.build b/python/mozbuild/mozbuild/test/frontend/data/test-manifest-install-includes/moz.build new file mode 100644 index 0000000000..d2b33added --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/test-manifest-install-includes/moz.build @@ -0,0 +1,4 @@ +# Any copyright is dedicated to the Public Domain. +# http://creativecommons.org/publicdomain/zero/1.0/ + +MOCHITEST_MANIFESTS += ["mochitest.toml"] diff --git a/python/mozbuild/mozbuild/test/frontend/data/test-manifest-install-includes/test_foo.html b/python/mozbuild/mozbuild/test/frontend/data/test-manifest-install-includes/test_foo.html new file mode 100644 index 0000000000..18ecdcb795 --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/test-manifest-install-includes/test_foo.html @@ -0,0 +1 @@ +<html></html> diff --git a/python/mozbuild/mozbuild/test/frontend/data/test-manifest-just-support/foo.txt b/python/mozbuild/mozbuild/test/frontend/data/test-manifest-just-support/foo.txt new file mode 100644 index 0000000000..ce01362503 --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/test-manifest-just-support/foo.txt @@ -0,0 +1 @@ +hello diff --git a/python/mozbuild/mozbuild/test/frontend/data/test-manifest-just-support/just-support.toml b/python/mozbuild/mozbuild/test/frontend/data/test-manifest-just-support/just-support.toml new file mode 100644 index 0000000000..0703748ded --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/test-manifest-just-support/just-support.toml @@ -0,0 +1,2 @@ +[DEFAULT] +support-files = ["foo.txt"] diff --git a/python/mozbuild/mozbuild/test/frontend/data/test-manifest-just-support/moz.build b/python/mozbuild/mozbuild/test/frontend/data/test-manifest-just-support/moz.build new file mode 100644 index 0000000000..5ce3aa387e --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/test-manifest-just-support/moz.build @@ -0,0 +1,4 @@ +# Any copyright is dedicated to the Public Domain. +# http://creativecommons.org/publicdomain/zero/1.0/ + +MOCHITEST_MANIFESTS += ["just-support.toml"] diff --git a/python/mozbuild/mozbuild/test/frontend/data/test-manifest-keys-extracted/a11y-support/dir1/bar b/python/mozbuild/mozbuild/test/frontend/data/test-manifest-keys-extracted/a11y-support/dir1/bar new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/test-manifest-keys-extracted/a11y-support/dir1/bar diff --git a/python/mozbuild/mozbuild/test/frontend/data/test-manifest-keys-extracted/a11y-support/foo b/python/mozbuild/mozbuild/test/frontend/data/test-manifest-keys-extracted/a11y-support/foo new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/test-manifest-keys-extracted/a11y-support/foo diff --git a/python/mozbuild/mozbuild/test/frontend/data/test-manifest-keys-extracted/a11y.toml b/python/mozbuild/mozbuild/test/frontend/data/test-manifest-keys-extracted/a11y.toml new file mode 100644 index 0000000000..0cfc5d044d --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/test-manifest-keys-extracted/a11y.toml @@ -0,0 +1,4 @@ +[DEFAULT] +support-files = "a11y-support/**" + +['test_a11y.js'] diff --git a/python/mozbuild/mozbuild/test/frontend/data/test-manifest-keys-extracted/browser.toml b/python/mozbuild/mozbuild/test/frontend/data/test-manifest-keys-extracted/browser.toml new file mode 100644 index 0000000000..5cf86c9722 --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/test-manifest-keys-extracted/browser.toml @@ -0,0 +1,7 @@ +[DEFAULT] +support-files = [ + "support1", + "support2", +] + +["test_browser.js"] diff --git a/python/mozbuild/mozbuild/test/frontend/data/test-manifest-keys-extracted/chrome.toml b/python/mozbuild/mozbuild/test/frontend/data/test-manifest-keys-extracted/chrome.toml new file mode 100644 index 0000000000..bca2bc7308 --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/test-manifest-keys-extracted/chrome.toml @@ -0,0 +1,3 @@ +[DEFAULT] + +["test_chrome.js"] diff --git a/python/mozbuild/mozbuild/test/frontend/data/test-manifest-keys-extracted/crashtest.list b/python/mozbuild/mozbuild/test/frontend/data/test-manifest-keys-extracted/crashtest.list new file mode 100644 index 0000000000..b9d7f2685a --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/test-manifest-keys-extracted/crashtest.list @@ -0,0 +1 @@ +== crashtest1.html crashtest1-ref.html diff --git a/python/mozbuild/mozbuild/test/frontend/data/test-manifest-keys-extracted/metro.toml b/python/mozbuild/mozbuild/test/frontend/data/test-manifest-keys-extracted/metro.toml new file mode 100644 index 0000000000..c8fca14c0a --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/test-manifest-keys-extracted/metro.toml @@ -0,0 +1,3 @@ +[DEFAULT] + +["test_metro.js"] diff --git a/python/mozbuild/mozbuild/test/frontend/data/test-manifest-keys-extracted/mochitest.toml b/python/mozbuild/mozbuild/test/frontend/data/test-manifest-keys-extracted/mochitest.toml new file mode 100644 index 0000000000..ad11d2a0b7 --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/test-manifest-keys-extracted/mochitest.toml @@ -0,0 +1,11 @@ +[DEFAULT] +support-files = [ + "external1", + "external2", +] +generated-files = [ + "external1", + "external2", +] + +["test_mochitest.js"] diff --git a/python/mozbuild/mozbuild/test/frontend/data/test-manifest-keys-extracted/moz.build b/python/mozbuild/mozbuild/test/frontend/data/test-manifest-keys-extracted/moz.build new file mode 100644 index 0000000000..0a3f851d84 --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/test-manifest-keys-extracted/moz.build @@ -0,0 +1,12 @@ +# Any copyright is dedicated to the Public Domain. +# http://creativecommons.org/publicdomain/zero/1.0/ + +A11Y_MANIFESTS += ["a11y.toml"] +BROWSER_CHROME_MANIFESTS += ["browser.toml"] +METRO_CHROME_MANIFESTS += ["metro.toml"] +MOCHITEST_MANIFESTS += ["mochitest.toml"] +MOCHITEST_CHROME_MANIFESTS += ["chrome.toml"] +XPCSHELL_TESTS_MANIFESTS += ["xpcshell.toml"] +REFTEST_MANIFESTS += ["reftest.list"] +CRASHTEST_MANIFESTS += ["crashtest.list"] +PYTHON_UNITTEST_MANIFESTS += ["python.toml"] diff --git a/python/mozbuild/mozbuild/test/frontend/data/test-manifest-keys-extracted/python.toml b/python/mozbuild/mozbuild/test/frontend/data/test-manifest-keys-extracted/python.toml new file mode 100644 index 0000000000..bf2bf9103d --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/test-manifest-keys-extracted/python.toml @@ -0,0 +1,3 @@ +[DEFAULT] + +["test_foo.py"] diff --git a/python/mozbuild/mozbuild/test/frontend/data/test-manifest-keys-extracted/reftest.list b/python/mozbuild/mozbuild/test/frontend/data/test-manifest-keys-extracted/reftest.list new file mode 100644 index 0000000000..3fc25b2966 --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/test-manifest-keys-extracted/reftest.list @@ -0,0 +1 @@ +== reftest1.html reftest1-ref.html diff --git a/python/mozbuild/mozbuild/test/frontend/data/test-manifest-keys-extracted/test_a11y.js b/python/mozbuild/mozbuild/test/frontend/data/test-manifest-keys-extracted/test_a11y.js new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/test-manifest-keys-extracted/test_a11y.js diff --git a/python/mozbuild/mozbuild/test/frontend/data/test-manifest-keys-extracted/test_browser.js b/python/mozbuild/mozbuild/test/frontend/data/test-manifest-keys-extracted/test_browser.js new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/test-manifest-keys-extracted/test_browser.js diff --git a/python/mozbuild/mozbuild/test/frontend/data/test-manifest-keys-extracted/test_chrome.js b/python/mozbuild/mozbuild/test/frontend/data/test-manifest-keys-extracted/test_chrome.js new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/test-manifest-keys-extracted/test_chrome.js diff --git a/python/mozbuild/mozbuild/test/frontend/data/test-manifest-keys-extracted/test_foo.py b/python/mozbuild/mozbuild/test/frontend/data/test-manifest-keys-extracted/test_foo.py new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/test-manifest-keys-extracted/test_foo.py diff --git a/python/mozbuild/mozbuild/test/frontend/data/test-manifest-keys-extracted/test_metro.js b/python/mozbuild/mozbuild/test/frontend/data/test-manifest-keys-extracted/test_metro.js new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/test-manifest-keys-extracted/test_metro.js diff --git a/python/mozbuild/mozbuild/test/frontend/data/test-manifest-keys-extracted/test_mochitest.js b/python/mozbuild/mozbuild/test/frontend/data/test-manifest-keys-extracted/test_mochitest.js new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/test-manifest-keys-extracted/test_mochitest.js diff --git a/python/mozbuild/mozbuild/test/frontend/data/test-manifest-keys-extracted/test_xpcshell.js b/python/mozbuild/mozbuild/test/frontend/data/test-manifest-keys-extracted/test_xpcshell.js new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/test-manifest-keys-extracted/test_xpcshell.js diff --git a/python/mozbuild/mozbuild/test/frontend/data/test-manifest-keys-extracted/xpcshell.toml b/python/mozbuild/mozbuild/test/frontend/data/test-manifest-keys-extracted/xpcshell.toml new file mode 100644 index 0000000000..10314f7677 --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/test-manifest-keys-extracted/xpcshell.toml @@ -0,0 +1,5 @@ +[DEFAULT] +head = "head1 head2" +dupe-manifest = true + +["test_xpcshell.js"] diff --git a/python/mozbuild/mozbuild/test/frontend/data/test-manifest-missing-manifest/moz.build b/python/mozbuild/mozbuild/test/frontend/data/test-manifest-missing-manifest/moz.build new file mode 100644 index 0000000000..9986156310 --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/test-manifest-missing-manifest/moz.build @@ -0,0 +1,4 @@ +# Any copyright is dedicated to the Public Domain. +# http://creativecommons.org/publicdomain/zero/1.0/ + +XPCSHELL_TESTS_MANIFESTS += ["does_not_exist.toml"] diff --git a/python/mozbuild/mozbuild/test/frontend/data/test-manifest-missing-test-file-unfiltered/moz.build b/python/mozbuild/mozbuild/test/frontend/data/test-manifest-missing-test-file-unfiltered/moz.build new file mode 100644 index 0000000000..827201f8e9 --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/test-manifest-missing-test-file-unfiltered/moz.build @@ -0,0 +1,4 @@ +# Any copyright is dedicated to the Public Domain. +# http://creativecommons.org/publicdomain/zero/1.0/ + +XPCSHELL_TESTS_MANIFESTS += ["xpcshell.toml"] diff --git a/python/mozbuild/mozbuild/test/frontend/data/test-manifest-missing-test-file-unfiltered/xpcshell.toml b/python/mozbuild/mozbuild/test/frontend/data/test-manifest-missing-test-file-unfiltered/xpcshell.toml new file mode 100644 index 0000000000..a445a40307 --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/test-manifest-missing-test-file-unfiltered/xpcshell.toml @@ -0,0 +1,4 @@ +[DEFAULT] +support-files = ["support/**"] + +["missing.js"] diff --git a/python/mozbuild/mozbuild/test/frontend/data/test-manifest-missing-test-file/mochitest.toml b/python/mozbuild/mozbuild/test/frontend/data/test-manifest-missing-test-file/mochitest.toml new file mode 100644 index 0000000000..ccef589e4f --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/test-manifest-missing-test-file/mochitest.toml @@ -0,0 +1,3 @@ +[DEFAULT] + +["test_missing.html"] diff --git a/python/mozbuild/mozbuild/test/frontend/data/test-manifest-missing-test-file/moz.build b/python/mozbuild/mozbuild/test/frontend/data/test-manifest-missing-test-file/moz.build new file mode 100644 index 0000000000..d2b33added --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/test-manifest-missing-test-file/moz.build @@ -0,0 +1,4 @@ +# Any copyright is dedicated to the Public Domain. +# http://creativecommons.org/publicdomain/zero/1.0/ + +MOCHITEST_MANIFESTS += ["mochitest.toml"] diff --git a/python/mozbuild/mozbuild/test/frontend/data/test-manifest-parent-support-files-dir/child/mochitest.toml b/python/mozbuild/mozbuild/test/frontend/data/test-manifest-parent-support-files-dir/child/mochitest.toml new file mode 100644 index 0000000000..02f231a677 --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/test-manifest-parent-support-files-dir/child/mochitest.toml @@ -0,0 +1,4 @@ +[DEFAULT] +support-files = ["../support-file.txt"] + +["test_foo.js"] diff --git a/python/mozbuild/mozbuild/test/frontend/data/test-manifest-parent-support-files-dir/child/test_foo.js b/python/mozbuild/mozbuild/test/frontend/data/test-manifest-parent-support-files-dir/child/test_foo.js new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/test-manifest-parent-support-files-dir/child/test_foo.js diff --git a/python/mozbuild/mozbuild/test/frontend/data/test-manifest-parent-support-files-dir/moz.build b/python/mozbuild/mozbuild/test/frontend/data/test-manifest-parent-support-files-dir/moz.build new file mode 100644 index 0000000000..f4f4be5414 --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/test-manifest-parent-support-files-dir/moz.build @@ -0,0 +1,4 @@ +# Any copyright is dedicated to the Public Domain. +# http://creativecommons.org/publicdomain/zero/1.0/ + +MOCHITEST_MANIFESTS += ["child/mochitest.toml"] diff --git a/python/mozbuild/mozbuild/test/frontend/data/test-manifest-parent-support-files-dir/support-file.txt b/python/mozbuild/mozbuild/test/frontend/data/test-manifest-parent-support-files-dir/support-file.txt new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/test-manifest-parent-support-files-dir/support-file.txt diff --git a/python/mozbuild/mozbuild/test/frontend/data/test-manifest-shared-missing/child/another-file.sjs b/python/mozbuild/mozbuild/test/frontend/data/test-manifest-shared-missing/child/another-file.sjs new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/test-manifest-shared-missing/child/another-file.sjs diff --git a/python/mozbuild/mozbuild/test/frontend/data/test-manifest-shared-missing/child/browser.toml b/python/mozbuild/mozbuild/test/frontend/data/test-manifest-shared-missing/child/browser.toml new file mode 100644 index 0000000000..98d02421df --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/test-manifest-shared-missing/child/browser.toml @@ -0,0 +1,7 @@ +[DEFAULT] +support-files = [ + "another-file.sjs", + "data/**" +] + +["test_sub.js"] diff --git a/python/mozbuild/mozbuild/test/frontend/data/test-manifest-shared-missing/child/data/one.txt b/python/mozbuild/mozbuild/test/frontend/data/test-manifest-shared-missing/child/data/one.txt new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/test-manifest-shared-missing/child/data/one.txt diff --git a/python/mozbuild/mozbuild/test/frontend/data/test-manifest-shared-missing/child/data/two.txt b/python/mozbuild/mozbuild/test/frontend/data/test-manifest-shared-missing/child/data/two.txt new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/test-manifest-shared-missing/child/data/two.txt diff --git a/python/mozbuild/mozbuild/test/frontend/data/test-manifest-shared-missing/child/test_sub.js b/python/mozbuild/mozbuild/test/frontend/data/test-manifest-shared-missing/child/test_sub.js new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/test-manifest-shared-missing/child/test_sub.js diff --git a/python/mozbuild/mozbuild/test/frontend/data/test-manifest-shared-missing/mochitest.toml b/python/mozbuild/mozbuild/test/frontend/data/test-manifest-shared-missing/mochitest.toml new file mode 100644 index 0000000000..f4bf8864d7 --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/test-manifest-shared-missing/mochitest.toml @@ -0,0 +1,10 @@ +[DEFAULT] +support-files = [ + "support-file.txt", + "!/child/test_sub.js", + "!/child/another-file.sjs", + "!/child/data/**", + "!/does/not/exist.sjs", +] + +["test_foo.js"] diff --git a/python/mozbuild/mozbuild/test/frontend/data/test-manifest-shared-missing/moz.build b/python/mozbuild/mozbuild/test/frontend/data/test-manifest-shared-missing/moz.build new file mode 100644 index 0000000000..d28a244978 --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/test-manifest-shared-missing/moz.build @@ -0,0 +1,5 @@ +# Any copyright is dedicated to the Public Domain. +# http://creativecommons.org/publicdomain/zero/1.0/ + +MOCHITEST_MANIFESTS += ["mochitest.toml"] +BROWSER_CHROME_MANIFESTS += ["child/browser.toml"] diff --git a/python/mozbuild/mozbuild/test/frontend/data/test-manifest-shared-missing/support-file.txt b/python/mozbuild/mozbuild/test/frontend/data/test-manifest-shared-missing/support-file.txt new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/test-manifest-shared-missing/support-file.txt diff --git a/python/mozbuild/mozbuild/test/frontend/data/test-manifest-shared-missing/test_foo.js b/python/mozbuild/mozbuild/test/frontend/data/test-manifest-shared-missing/test_foo.js new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/test-manifest-shared-missing/test_foo.js diff --git a/python/mozbuild/mozbuild/test/frontend/data/test-manifest-shared-support/child/another-file.sjs b/python/mozbuild/mozbuild/test/frontend/data/test-manifest-shared-support/child/another-file.sjs new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/test-manifest-shared-support/child/another-file.sjs diff --git a/python/mozbuild/mozbuild/test/frontend/data/test-manifest-shared-support/child/browser.toml b/python/mozbuild/mozbuild/test/frontend/data/test-manifest-shared-support/child/browser.toml new file mode 100644 index 0000000000..98d02421df --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/test-manifest-shared-support/child/browser.toml @@ -0,0 +1,7 @@ +[DEFAULT] +support-files = [ + "another-file.sjs", + "data/**" +] + +["test_sub.js"] diff --git a/python/mozbuild/mozbuild/test/frontend/data/test-manifest-shared-support/child/data/one.txt b/python/mozbuild/mozbuild/test/frontend/data/test-manifest-shared-support/child/data/one.txt new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/test-manifest-shared-support/child/data/one.txt diff --git a/python/mozbuild/mozbuild/test/frontend/data/test-manifest-shared-support/child/data/two.txt b/python/mozbuild/mozbuild/test/frontend/data/test-manifest-shared-support/child/data/two.txt new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/test-manifest-shared-support/child/data/two.txt diff --git a/python/mozbuild/mozbuild/test/frontend/data/test-manifest-shared-support/child/test_sub.js b/python/mozbuild/mozbuild/test/frontend/data/test-manifest-shared-support/child/test_sub.js new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/test-manifest-shared-support/child/test_sub.js diff --git a/python/mozbuild/mozbuild/test/frontend/data/test-manifest-shared-support/mochitest.toml b/python/mozbuild/mozbuild/test/frontend/data/test-manifest-shared-support/mochitest.toml new file mode 100644 index 0000000000..ab9ea26da7 --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/test-manifest-shared-support/mochitest.toml @@ -0,0 +1,9 @@ +[DEFAULT] +support-files = [ + "support-file.txt", + "!/child/test_sub.js", + "!/child/another-file.sjs", + "!/child/data/**", +] + +["test_foo.js"] diff --git a/python/mozbuild/mozbuild/test/frontend/data/test-manifest-shared-support/moz.build b/python/mozbuild/mozbuild/test/frontend/data/test-manifest-shared-support/moz.build new file mode 100644 index 0000000000..d28a244978 --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/test-manifest-shared-support/moz.build @@ -0,0 +1,5 @@ +# Any copyright is dedicated to the Public Domain. +# http://creativecommons.org/publicdomain/zero/1.0/ + +MOCHITEST_MANIFESTS += ["mochitest.toml"] +BROWSER_CHROME_MANIFESTS += ["child/browser.toml"] diff --git a/python/mozbuild/mozbuild/test/frontend/data/test-manifest-shared-support/support-file.txt b/python/mozbuild/mozbuild/test/frontend/data/test-manifest-shared-support/support-file.txt new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/test-manifest-shared-support/support-file.txt diff --git a/python/mozbuild/mozbuild/test/frontend/data/test-manifest-shared-support/test_foo.js b/python/mozbuild/mozbuild/test/frontend/data/test-manifest-shared-support/test_foo.js new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/test-manifest-shared-support/test_foo.js diff --git a/python/mozbuild/mozbuild/test/frontend/data/test-manifest-unmatched-generated/moz.build b/python/mozbuild/mozbuild/test/frontend/data/test-manifest-unmatched-generated/moz.build new file mode 100644 index 0000000000..d1500b8965 --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/test-manifest-unmatched-generated/moz.build @@ -0,0 +1,4 @@ +# Any copyright is dedicated to the Public Domain. +# http://creativecommons.org/publicdomain/zero/1.0/ + +MOCHITEST_MANIFESTS += ["test.toml"] diff --git a/python/mozbuild/mozbuild/test/frontend/data/test-manifest-unmatched-generated/test.toml b/python/mozbuild/mozbuild/test/frontend/data/test-manifest-unmatched-generated/test.toml new file mode 100644 index 0000000000..759a8f5721 --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/test-manifest-unmatched-generated/test.toml @@ -0,0 +1,4 @@ +[DEFAULT] +generated-files = "does_not_exist" + +["test_foo"] diff --git a/python/mozbuild/mozbuild/test/frontend/data/test-manifest-unmatched-generated/test_foo b/python/mozbuild/mozbuild/test/frontend/data/test-manifest-unmatched-generated/test_foo new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/test-manifest-unmatched-generated/test_foo diff --git a/python/mozbuild/mozbuild/test/frontend/data/test-symbols-file-objdir-missing-generated/moz.build b/python/mozbuild/mozbuild/test/frontend/data/test-symbols-file-objdir-missing-generated/moz.build new file mode 100644 index 0000000000..450af01d9a --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/test-symbols-file-objdir-missing-generated/moz.build @@ -0,0 +1,12 @@ +# Any copyright is dedicated to the Public Domain. +# http://creativecommons.org/publicdomain/zero/1.0/ + + +@template +def SharedLibrary(name): + LIBRARY_NAME = name + FORCE_SHARED_LIB = True + + +SharedLibrary("foo") +SYMBOLS_FILE = "!foo.symbols" diff --git a/python/mozbuild/mozbuild/test/frontend/data/test-symbols-file-objdir/foo.py b/python/mozbuild/mozbuild/test/frontend/data/test-symbols-file-objdir/foo.py new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/test-symbols-file-objdir/foo.py diff --git a/python/mozbuild/mozbuild/test/frontend/data/test-symbols-file-objdir/moz.build b/python/mozbuild/mozbuild/test/frontend/data/test-symbols-file-objdir/moz.build new file mode 100644 index 0000000000..7ea07b4ee9 --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/test-symbols-file-objdir/moz.build @@ -0,0 +1,15 @@ +# Any copyright is dedicated to the Public Domain. +# http://creativecommons.org/publicdomain/zero/1.0/ + + +@template +def SharedLibrary(name): + LIBRARY_NAME = name + FORCE_SHARED_LIB = True + + +SharedLibrary("foo") +SYMBOLS_FILE = "!foo.symbols" + +GENERATED_FILES += ["foo.symbols"] +GENERATED_FILES["foo.symbols"].script = "foo.py" diff --git a/python/mozbuild/mozbuild/test/frontend/data/test-symbols-file/foo.symbols b/python/mozbuild/mozbuild/test/frontend/data/test-symbols-file/foo.symbols new file mode 100644 index 0000000000..257cc5642c --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/test-symbols-file/foo.symbols @@ -0,0 +1 @@ +foo diff --git a/python/mozbuild/mozbuild/test/frontend/data/test-symbols-file/moz.build b/python/mozbuild/mozbuild/test/frontend/data/test-symbols-file/moz.build new file mode 100644 index 0000000000..47e435dbf5 --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/test-symbols-file/moz.build @@ -0,0 +1,12 @@ +# Any copyright is dedicated to the Public Domain. +# http://creativecommons.org/publicdomain/zero/1.0/ + + +@template +def SharedLibrary(name): + LIBRARY_NAME = name + FORCE_SHARED_LIB = True + + +SharedLibrary("foo") +SYMBOLS_FILE = "foo.symbols" diff --git a/python/mozbuild/mozbuild/test/frontend/data/traversal-all-vars/moz.build b/python/mozbuild/mozbuild/test/frontend/data/traversal-all-vars/moz.build new file mode 100644 index 0000000000..480808eb8a --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/traversal-all-vars/moz.build @@ -0,0 +1,6 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# Any copyright is dedicated to the Public Domain. +# http://creativecommons.org/publicdomain/zero/1.0/ + +DIRS += ["regular"] +TEST_DIRS += ["test"] diff --git a/python/mozbuild/mozbuild/test/frontend/data/traversal-all-vars/parallel/moz.build b/python/mozbuild/mozbuild/test/frontend/data/traversal-all-vars/parallel/moz.build new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/traversal-all-vars/parallel/moz.build diff --git a/python/mozbuild/mozbuild/test/frontend/data/traversal-all-vars/regular/moz.build b/python/mozbuild/mozbuild/test/frontend/data/traversal-all-vars/regular/moz.build new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/traversal-all-vars/regular/moz.build diff --git a/python/mozbuild/mozbuild/test/frontend/data/traversal-all-vars/test/moz.build b/python/mozbuild/mozbuild/test/frontend/data/traversal-all-vars/test/moz.build new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/traversal-all-vars/test/moz.build diff --git a/python/mozbuild/mozbuild/test/frontend/data/traversal-outside-topsrcdir/moz.build b/python/mozbuild/mozbuild/test/frontend/data/traversal-outside-topsrcdir/moz.build new file mode 100644 index 0000000000..dbdc694a6a --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/traversal-outside-topsrcdir/moz.build @@ -0,0 +1,5 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# Any copyright is dedicated to the Public Domain. +# http://creativecommons.org/publicdomain/zero/1.0/ + +DIRS = ["../../foo"] diff --git a/python/mozbuild/mozbuild/test/frontend/data/traversal-relative-dirs/bar/moz.build b/python/mozbuild/mozbuild/test/frontend/data/traversal-relative-dirs/bar/moz.build new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/traversal-relative-dirs/bar/moz.build diff --git a/python/mozbuild/mozbuild/test/frontend/data/traversal-relative-dirs/foo/moz.build b/python/mozbuild/mozbuild/test/frontend/data/traversal-relative-dirs/foo/moz.build new file mode 100644 index 0000000000..4b42bbc5ab --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/traversal-relative-dirs/foo/moz.build @@ -0,0 +1,5 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# Any copyright is dedicated to the Public Domain. +# http://creativecommons.org/publicdomain/zero/1.0/ + +DIRS = ["../bar"] diff --git a/python/mozbuild/mozbuild/test/frontend/data/traversal-relative-dirs/moz.build b/python/mozbuild/mozbuild/test/frontend/data/traversal-relative-dirs/moz.build new file mode 100644 index 0000000000..68581574b1 --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/traversal-relative-dirs/moz.build @@ -0,0 +1,5 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# Any copyright is dedicated to the Public Domain. +# http://creativecommons.org/publicdomain/zero/1.0/ + +DIRS = ["foo"] diff --git a/python/mozbuild/mozbuild/test/frontend/data/traversal-repeated-dirs/bar/moz.build b/python/mozbuild/mozbuild/test/frontend/data/traversal-repeated-dirs/bar/moz.build new file mode 100644 index 0000000000..f204e245b4 --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/traversal-repeated-dirs/bar/moz.build @@ -0,0 +1,5 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# Any copyright is dedicated to the Public Domain. +# http://creativecommons.org/publicdomain/zero/1.0/ + +DIRS = ["../foo"] diff --git a/python/mozbuild/mozbuild/test/frontend/data/traversal-repeated-dirs/foo/moz.build b/python/mozbuild/mozbuild/test/frontend/data/traversal-repeated-dirs/foo/moz.build new file mode 100644 index 0000000000..4b42bbc5ab --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/traversal-repeated-dirs/foo/moz.build @@ -0,0 +1,5 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# Any copyright is dedicated to the Public Domain. +# http://creativecommons.org/publicdomain/zero/1.0/ + +DIRS = ["../bar"] diff --git a/python/mozbuild/mozbuild/test/frontend/data/traversal-repeated-dirs/moz.build b/python/mozbuild/mozbuild/test/frontend/data/traversal-repeated-dirs/moz.build new file mode 100644 index 0000000000..5a9445a6e6 --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/traversal-repeated-dirs/moz.build @@ -0,0 +1,5 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# Any copyright is dedicated to the Public Domain. +# http://creativecommons.org/publicdomain/zero/1.0/ + +DIRS = ["foo", "bar"] diff --git a/python/mozbuild/mozbuild/test/frontend/data/traversal-simple/bar/moz.build b/python/mozbuild/mozbuild/test/frontend/data/traversal-simple/bar/moz.build new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/traversal-simple/bar/moz.build diff --git a/python/mozbuild/mozbuild/test/frontend/data/traversal-simple/foo/biz/moz.build b/python/mozbuild/mozbuild/test/frontend/data/traversal-simple/foo/biz/moz.build new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/traversal-simple/foo/biz/moz.build diff --git a/python/mozbuild/mozbuild/test/frontend/data/traversal-simple/foo/moz.build b/python/mozbuild/mozbuild/test/frontend/data/traversal-simple/foo/moz.build new file mode 100644 index 0000000000..3ad8a1501d --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/traversal-simple/foo/moz.build @@ -0,0 +1,2 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +DIRS = ["biz"] diff --git a/python/mozbuild/mozbuild/test/frontend/data/traversal-simple/moz.build b/python/mozbuild/mozbuild/test/frontend/data/traversal-simple/moz.build new file mode 100644 index 0000000000..5a9445a6e6 --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/traversal-simple/moz.build @@ -0,0 +1,5 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# Any copyright is dedicated to the Public Domain. +# http://creativecommons.org/publicdomain/zero/1.0/ + +DIRS = ["foo", "bar"] diff --git a/python/mozbuild/mozbuild/test/frontend/data/unified-sources-non-unified/bar.cxx b/python/mozbuild/mozbuild/test/frontend/data/unified-sources-non-unified/bar.cxx new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/unified-sources-non-unified/bar.cxx diff --git a/python/mozbuild/mozbuild/test/frontend/data/unified-sources-non-unified/c1.c b/python/mozbuild/mozbuild/test/frontend/data/unified-sources-non-unified/c1.c new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/unified-sources-non-unified/c1.c diff --git a/python/mozbuild/mozbuild/test/frontend/data/unified-sources-non-unified/c2.c b/python/mozbuild/mozbuild/test/frontend/data/unified-sources-non-unified/c2.c new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/unified-sources-non-unified/c2.c diff --git a/python/mozbuild/mozbuild/test/frontend/data/unified-sources-non-unified/foo.cpp b/python/mozbuild/mozbuild/test/frontend/data/unified-sources-non-unified/foo.cpp new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/unified-sources-non-unified/foo.cpp diff --git a/python/mozbuild/mozbuild/test/frontend/data/unified-sources-non-unified/moz.build b/python/mozbuild/mozbuild/test/frontend/data/unified-sources-non-unified/moz.build new file mode 100644 index 0000000000..217e43831f --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/unified-sources-non-unified/moz.build @@ -0,0 +1,30 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# Any copyright is dedicated to the Public Domain. +# http://creativecommons.org/publicdomain/zero/1.0/ + + +@template +def Library(name): + """Template for libraries.""" + LIBRARY_NAME = name + + +Library("dummy") + +UNIFIED_SOURCES += [ + "bar.cxx", + "foo.cpp", + "quux.cc", +] + +UNIFIED_SOURCES += [ + "objc1.mm", + "objc2.mm", +] + +UNIFIED_SOURCES += [ + "c1.c", + "c2.c", +] + +FILES_PER_UNIFIED_FILE = 1 diff --git a/python/mozbuild/mozbuild/test/frontend/data/unified-sources-non-unified/objc1.mm b/python/mozbuild/mozbuild/test/frontend/data/unified-sources-non-unified/objc1.mm new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/unified-sources-non-unified/objc1.mm diff --git a/python/mozbuild/mozbuild/test/frontend/data/unified-sources-non-unified/objc2.mm b/python/mozbuild/mozbuild/test/frontend/data/unified-sources-non-unified/objc2.mm new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/unified-sources-non-unified/objc2.mm diff --git a/python/mozbuild/mozbuild/test/frontend/data/unified-sources-non-unified/quux.cc b/python/mozbuild/mozbuild/test/frontend/data/unified-sources-non-unified/quux.cc new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/unified-sources-non-unified/quux.cc diff --git a/python/mozbuild/mozbuild/test/frontend/data/unified-sources/bar.cxx b/python/mozbuild/mozbuild/test/frontend/data/unified-sources/bar.cxx new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/unified-sources/bar.cxx diff --git a/python/mozbuild/mozbuild/test/frontend/data/unified-sources/c1.c b/python/mozbuild/mozbuild/test/frontend/data/unified-sources/c1.c new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/unified-sources/c1.c diff --git a/python/mozbuild/mozbuild/test/frontend/data/unified-sources/c2.c b/python/mozbuild/mozbuild/test/frontend/data/unified-sources/c2.c new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/unified-sources/c2.c diff --git a/python/mozbuild/mozbuild/test/frontend/data/unified-sources/foo.cpp b/python/mozbuild/mozbuild/test/frontend/data/unified-sources/foo.cpp new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/unified-sources/foo.cpp diff --git a/python/mozbuild/mozbuild/test/frontend/data/unified-sources/moz.build b/python/mozbuild/mozbuild/test/frontend/data/unified-sources/moz.build new file mode 100644 index 0000000000..8a86e055da --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/unified-sources/moz.build @@ -0,0 +1,30 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# Any copyright is dedicated to the Public Domain. +# http://creativecommons.org/publicdomain/zero/1.0/ + + +@template +def Library(name): + """Template for libraries.""" + LIBRARY_NAME = name + + +Library("dummy") + +UNIFIED_SOURCES += [ + "bar.cxx", + "foo.cpp", + "quux.cc", +] + +UNIFIED_SOURCES += [ + "objc1.mm", + "objc2.mm", +] + +UNIFIED_SOURCES += [ + "c1.c", + "c2.c", +] + +FILES_PER_UNIFIED_FILE = 32 diff --git a/python/mozbuild/mozbuild/test/frontend/data/unified-sources/objc1.mm b/python/mozbuild/mozbuild/test/frontend/data/unified-sources/objc1.mm new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/unified-sources/objc1.mm diff --git a/python/mozbuild/mozbuild/test/frontend/data/unified-sources/objc2.mm b/python/mozbuild/mozbuild/test/frontend/data/unified-sources/objc2.mm new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/unified-sources/objc2.mm diff --git a/python/mozbuild/mozbuild/test/frontend/data/unified-sources/quux.cc b/python/mozbuild/mozbuild/test/frontend/data/unified-sources/quux.cc new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/unified-sources/quux.cc diff --git a/python/mozbuild/mozbuild/test/frontend/data/use-nasm/moz.build b/python/mozbuild/mozbuild/test/frontend/data/use-nasm/moz.build new file mode 100644 index 0000000000..63ac5283f6 --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/use-nasm/moz.build @@ -0,0 +1,15 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# Any copyright is dedicated to the Public Domain. +# http://creativecommons.org/publicdomain/zero/1.0/ + + +@template +def Library(name): + LIBRARY_NAME = name + + +Library("dummy") + +USE_NASM = True + +SOURCES += ["test1.S"] diff --git a/python/mozbuild/mozbuild/test/frontend/data/use-nasm/test1.S b/python/mozbuild/mozbuild/test/frontend/data/use-nasm/test1.S new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/use-nasm/test1.S diff --git a/python/mozbuild/mozbuild/test/frontend/data/variable-passthru/bans.S b/python/mozbuild/mozbuild/test/frontend/data/variable-passthru/bans.S new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/variable-passthru/bans.S diff --git a/python/mozbuild/mozbuild/test/frontend/data/variable-passthru/baz.def b/python/mozbuild/mozbuild/test/frontend/data/variable-passthru/baz.def new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/variable-passthru/baz.def diff --git a/python/mozbuild/mozbuild/test/frontend/data/variable-passthru/moz.build b/python/mozbuild/mozbuild/test/frontend/data/variable-passthru/moz.build new file mode 100644 index 0000000000..d080b00c92 --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/variable-passthru/moz.build @@ -0,0 +1,13 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# Any copyright is dedicated to the Public Domain. +# http://creativecommons.org/publicdomain/zero/1.0/ + +DIST_INSTALL = False + +DELAYLOAD_DLLS = ["foo.dll", "bar.dll"] + +RCFILE = "foo.rc" +RCINCLUDE = "bar.rc" +DEFFILE = "baz.def" + +WIN32_EXE_LDFLAGS += ["-subsystem:console"] diff --git a/python/mozbuild/mozbuild/test/frontend/data/variable-passthru/test1.c b/python/mozbuild/mozbuild/test/frontend/data/variable-passthru/test1.c new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/variable-passthru/test1.c diff --git a/python/mozbuild/mozbuild/test/frontend/data/variable-passthru/test1.cpp b/python/mozbuild/mozbuild/test/frontend/data/variable-passthru/test1.cpp new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/variable-passthru/test1.cpp diff --git a/python/mozbuild/mozbuild/test/frontend/data/variable-passthru/test1.mm b/python/mozbuild/mozbuild/test/frontend/data/variable-passthru/test1.mm new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/variable-passthru/test1.mm diff --git a/python/mozbuild/mozbuild/test/frontend/data/variable-passthru/test2.c b/python/mozbuild/mozbuild/test/frontend/data/variable-passthru/test2.c new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/variable-passthru/test2.c diff --git a/python/mozbuild/mozbuild/test/frontend/data/variable-passthru/test2.cpp b/python/mozbuild/mozbuild/test/frontend/data/variable-passthru/test2.cpp new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/variable-passthru/test2.cpp diff --git a/python/mozbuild/mozbuild/test/frontend/data/variable-passthru/test2.mm b/python/mozbuild/mozbuild/test/frontend/data/variable-passthru/test2.mm new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/variable-passthru/test2.mm diff --git a/python/mozbuild/mozbuild/test/frontend/data/visibility-flags/moz.build b/python/mozbuild/mozbuild/test/frontend/data/visibility-flags/moz.build new file mode 100644 index 0000000000..630a3afd80 --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/visibility-flags/moz.build @@ -0,0 +1,21 @@ +# Any copyright is dedicated to the Public Domain. +# http://creativecommons.org/publicdomain/zero/1.0/ + + +@template +def Library(name): + """Template for libraries.""" + LIBRARY_NAME = name + + +Library("dummy") + + +@template +def NoVisibilityFlags(): + COMPILE_FLAGS["VISIBILITY"] = [] + + +UNIFIED_SOURCES += ["test1.c"] + +NoVisibilityFlags() diff --git a/python/mozbuild/mozbuild/test/frontend/data/visibility-flags/test1.c b/python/mozbuild/mozbuild/test/frontend/data/visibility-flags/test1.c new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/visibility-flags/test1.c diff --git a/python/mozbuild/mozbuild/test/frontend/data/wasm-compile-flags/moz.build b/python/mozbuild/mozbuild/test/frontend/data/wasm-compile-flags/moz.build new file mode 100644 index 0000000000..e7cf13088f --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/wasm-compile-flags/moz.build @@ -0,0 +1,14 @@ +# Any copyright is dedicated to the Public Domain. +# http://creativecommons.org/publicdomain/zero/1.0/ + +SANDBOXED_WASM_LIBRARY_NAME = "dummy" + +WASM_SOURCES += ["test1.c"] + +value = "xyz" +WASM_DEFINES["FOO"] = True +WASM_DEFINES["BAZ"] = '"abcd"' +WASM_DEFINES["BAR"] = 7 +WASM_DEFINES["VALUE"] = value +WASM_DEFINES["QUX"] = False +WASM_CFLAGS += ["-funroll-loops", "-wasm-arg"] diff --git a/python/mozbuild/mozbuild/test/frontend/data/wasm-compile-flags/test1.c b/python/mozbuild/mozbuild/test/frontend/data/wasm-compile-flags/test1.c new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/wasm-compile-flags/test1.c diff --git a/python/mozbuild/mozbuild/test/frontend/data/wasm-sources/a.cpp b/python/mozbuild/mozbuild/test/frontend/data/wasm-sources/a.cpp new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/wasm-sources/a.cpp diff --git a/python/mozbuild/mozbuild/test/frontend/data/wasm-sources/b.cc b/python/mozbuild/mozbuild/test/frontend/data/wasm-sources/b.cc new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/wasm-sources/b.cc diff --git a/python/mozbuild/mozbuild/test/frontend/data/wasm-sources/c.cxx b/python/mozbuild/mozbuild/test/frontend/data/wasm-sources/c.cxx new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/wasm-sources/c.cxx diff --git a/python/mozbuild/mozbuild/test/frontend/data/wasm-sources/d.c b/python/mozbuild/mozbuild/test/frontend/data/wasm-sources/d.c new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/wasm-sources/d.c diff --git a/python/mozbuild/mozbuild/test/frontend/data/wasm-sources/moz.build b/python/mozbuild/mozbuild/test/frontend/data/wasm-sources/moz.build new file mode 100644 index 0000000000..e266bcb0dd --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/wasm-sources/moz.build @@ -0,0 +1,15 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# Any copyright is dedicated to the Public Domain. +# http://creativecommons.org/publicdomain/zero/1.0/ + +SANDBOXED_WASM_LIBRARY_NAME = "wasmSources" + +WASM_SOURCES += [ + "a.cpp", + "b.cc", + "c.cxx", +] + +WASM_SOURCES += [ + "d.c", +] diff --git a/python/mozbuild/mozbuild/test/frontend/data/xpidl-module-no-sources/moz.build b/python/mozbuild/mozbuild/test/frontend/data/xpidl-module-no-sources/moz.build new file mode 100644 index 0000000000..f0abd45382 --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/data/xpidl-module-no-sources/moz.build @@ -0,0 +1,5 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# Any copyright is dedicated to the Public Domain. +# http://creativecommons.org/publicdomain/zero/1.0/ + +XPIDL_MODULE = "xpidl_module" diff --git a/python/mozbuild/mozbuild/test/frontend/test_context.py b/python/mozbuild/mozbuild/test/frontend/test_context.py new file mode 100644 index 0000000000..fbf35e1c8c --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/test_context.py @@ -0,0 +1,736 @@ +# 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/. + +import os +import unittest + +import six +from mozpack import path as mozpath +from mozunit import main + +from mozbuild.frontend.context import ( + FUNCTIONS, + SPECIAL_VARIABLES, + SUBCONTEXTS, + VARIABLES, + AbsolutePath, + Context, + ContextDerivedTypedHierarchicalStringList, + ContextDerivedTypedList, + ContextDerivedTypedListWithItems, + ContextDerivedTypedRecord, + Files, + ObjDirPath, + Path, + SourcePath, +) +from mozbuild.util import StrictOrderingOnAppendListWithFlagsFactory + + +class TestContext(unittest.TestCase): + def test_defaults(self): + test = Context( + { + "foo": (int, int, ""), + "bar": (bool, bool, ""), + "baz": (dict, dict, ""), + } + ) + + self.assertEqual(list(test), []) + + self.assertEqual(test["foo"], 0) + + self.assertEqual(set(test.keys()), {"foo"}) + + self.assertEqual(test["bar"], False) + + self.assertEqual(set(test.keys()), {"foo", "bar"}) + + self.assertEqual(test["baz"], {}) + + self.assertEqual(set(test.keys()), {"foo", "bar", "baz"}) + + with self.assertRaises(KeyError): + test["qux"] + + self.assertEqual(set(test.keys()), {"foo", "bar", "baz"}) + + def test_type_check(self): + test = Context( + { + "foo": (int, int, ""), + "baz": (dict, list, ""), + } + ) + + test["foo"] = 5 + + self.assertEqual(test["foo"], 5) + + with self.assertRaises(ValueError): + test["foo"] = {} + + self.assertEqual(test["foo"], 5) + + with self.assertRaises(KeyError): + test["bar"] = True + + test["baz"] = [("a", 1), ("b", 2)] + + self.assertEqual(test["baz"], {"a": 1, "b": 2}) + + def test_update(self): + test = Context( + { + "foo": (int, int, ""), + "bar": (bool, bool, ""), + "baz": (dict, list, ""), + } + ) + + self.assertEqual(list(test), []) + + with self.assertRaises(ValueError): + test.update(bar=True, foo={}) + + self.assertEqual(list(test), []) + + test.update(bar=True, foo=1) + + self.assertEqual(set(test.keys()), {"foo", "bar"}) + self.assertEqual(test["foo"], 1) + self.assertEqual(test["bar"], True) + + test.update([("bar", False), ("foo", 2)]) + self.assertEqual(test["foo"], 2) + self.assertEqual(test["bar"], False) + + test.update([("foo", 0), ("baz", {"a": 1, "b": 2})]) + self.assertEqual(test["foo"], 0) + self.assertEqual(test["baz"], {"a": 1, "b": 2}) + + test.update([("foo", 42), ("baz", [("c", 3), ("d", 4)])]) + self.assertEqual(test["foo"], 42) + self.assertEqual(test["baz"], {"c": 3, "d": 4}) + + def test_context_paths(self): + test = Context() + + # Newly created context has no paths. + self.assertIsNone(test.main_path) + self.assertIsNone(test.current_path) + self.assertEqual(test.all_paths, set()) + self.assertEqual(test.source_stack, []) + + foo = os.path.abspath("foo") + test.add_source(foo) + + # Adding the first source makes it the main and current path. + self.assertEqual(test.main_path, foo) + self.assertEqual(test.current_path, foo) + self.assertEqual(test.all_paths, set([foo])) + self.assertEqual(test.source_stack, [foo]) + + bar = os.path.abspath("bar") + test.add_source(bar) + + # Adding the second source makes leaves main and current paths alone. + self.assertEqual(test.main_path, foo) + self.assertEqual(test.current_path, foo) + self.assertEqual(test.all_paths, set([bar, foo])) + self.assertEqual(test.source_stack, [foo]) + + qux = os.path.abspath("qux") + test.push_source(qux) + + # Pushing a source makes it the current path + self.assertEqual(test.main_path, foo) + self.assertEqual(test.current_path, qux) + self.assertEqual(test.all_paths, set([bar, foo, qux])) + self.assertEqual(test.source_stack, [foo, qux]) + + hoge = os.path.abspath("hoge") + test.push_source(hoge) + self.assertEqual(test.main_path, foo) + self.assertEqual(test.current_path, hoge) + self.assertEqual(test.all_paths, set([bar, foo, hoge, qux])) + self.assertEqual(test.source_stack, [foo, qux, hoge]) + + fuga = os.path.abspath("fuga") + + # Adding a source after pushing doesn't change the source stack + test.add_source(fuga) + self.assertEqual(test.main_path, foo) + self.assertEqual(test.current_path, hoge) + self.assertEqual(test.all_paths, set([bar, foo, fuga, hoge, qux])) + self.assertEqual(test.source_stack, [foo, qux, hoge]) + + # Adding a source twice doesn't change anything + test.add_source(qux) + self.assertEqual(test.main_path, foo) + self.assertEqual(test.current_path, hoge) + self.assertEqual(test.all_paths, set([bar, foo, fuga, hoge, qux])) + self.assertEqual(test.source_stack, [foo, qux, hoge]) + + last = test.pop_source() + + # Popping a source returns the last pushed one, not the last added one. + self.assertEqual(last, hoge) + self.assertEqual(test.main_path, foo) + self.assertEqual(test.current_path, qux) + self.assertEqual(test.all_paths, set([bar, foo, fuga, hoge, qux])) + self.assertEqual(test.source_stack, [foo, qux]) + + last = test.pop_source() + self.assertEqual(last, qux) + self.assertEqual(test.main_path, foo) + self.assertEqual(test.current_path, foo) + self.assertEqual(test.all_paths, set([bar, foo, fuga, hoge, qux])) + self.assertEqual(test.source_stack, [foo]) + + # Popping the main path is allowed. + last = test.pop_source() + self.assertEqual(last, foo) + self.assertEqual(test.main_path, foo) + self.assertIsNone(test.current_path) + self.assertEqual(test.all_paths, set([bar, foo, fuga, hoge, qux])) + self.assertEqual(test.source_stack, []) + + # Popping past the main path asserts. + with self.assertRaises(AssertionError): + test.pop_source() + + # Pushing after the main path was popped asserts. + with self.assertRaises(AssertionError): + test.push_source(foo) + + test = Context() + test.push_source(foo) + test.push_source(bar) + + # Pushing the same file twice is allowed. + test.push_source(bar) + test.push_source(foo) + self.assertEqual(last, foo) + self.assertEqual(test.main_path, foo) + self.assertEqual(test.current_path, foo) + self.assertEqual(test.all_paths, set([bar, foo])) + self.assertEqual(test.source_stack, [foo, bar, bar, foo]) + + def test_context_dirs(self): + class Config(object): + pass + + config = Config() + config.topsrcdir = mozpath.abspath(os.curdir) + config.topobjdir = mozpath.abspath("obj") + test = Context(config=config) + foo = mozpath.abspath("foo") + test.push_source(foo) + + self.assertEqual(test.srcdir, config.topsrcdir) + self.assertEqual(test.relsrcdir, "") + self.assertEqual(test.objdir, config.topobjdir) + self.assertEqual(test.relobjdir, "") + + foobar = os.path.abspath("foo/bar") + test.push_source(foobar) + self.assertEqual(test.srcdir, mozpath.join(config.topsrcdir, "foo")) + self.assertEqual(test.relsrcdir, "foo") + self.assertEqual(test.objdir, config.topobjdir) + self.assertEqual(test.relobjdir, "") + + +class TestSymbols(unittest.TestCase): + def _verify_doc(self, doc): + # Documentation should be of the format: + # """SUMMARY LINE + # + # EXTRA PARAGRAPHS + # """ + + self.assertNotIn("\r", doc) + + lines = doc.split("\n") + + # No trailing whitespace. + for line in lines[0:-1]: + self.assertEqual(line, line.rstrip()) + + self.assertGreater(len(lines), 0) + self.assertGreater(len(lines[0].strip()), 0) + + # Last line should be empty. + self.assertEqual(lines[-1].strip(), "") + + def test_documentation_formatting(self): + for typ, inp, doc in VARIABLES.values(): + self._verify_doc(doc) + + for attr, args, doc in FUNCTIONS.values(): + self._verify_doc(doc) + + for func, typ, doc in SPECIAL_VARIABLES.values(): + self._verify_doc(doc) + + for name, cls in SUBCONTEXTS.items(): + self._verify_doc(cls.__doc__) + + for name, v in cls.VARIABLES.items(): + self._verify_doc(v[2]) + + +class TestPaths(unittest.TestCase): + @classmethod + def setUpClass(cls): + class Config(object): + pass + + cls.config = config = Config() + config.topsrcdir = mozpath.abspath(os.curdir) + config.topobjdir = mozpath.abspath("obj") + + def test_path(self): + config = self.config + ctxt1 = Context(config=config) + ctxt1.push_source(mozpath.join(config.topsrcdir, "foo", "moz.build")) + ctxt2 = Context(config=config) + ctxt2.push_source(mozpath.join(config.topsrcdir, "bar", "moz.build")) + + path1 = Path(ctxt1, "qux") + self.assertIsInstance(path1, SourcePath) + self.assertEqual(path1, "qux") + self.assertEqual(path1.full_path, mozpath.join(config.topsrcdir, "foo", "qux")) + + path2 = Path(ctxt2, "../foo/qux") + self.assertIsInstance(path2, SourcePath) + self.assertEqual(path2, "../foo/qux") + self.assertEqual(path2.full_path, mozpath.join(config.topsrcdir, "foo", "qux")) + + self.assertEqual(path1, path2) + + self.assertEqual( + path1.join("../../bar/qux").full_path, + mozpath.join(config.topsrcdir, "bar", "qux"), + ) + + path1 = Path(ctxt1, "/qux/qux") + self.assertIsInstance(path1, SourcePath) + self.assertEqual(path1, "/qux/qux") + self.assertEqual(path1.full_path, mozpath.join(config.topsrcdir, "qux", "qux")) + + path2 = Path(ctxt2, "/qux/qux") + self.assertIsInstance(path2, SourcePath) + self.assertEqual(path2, "/qux/qux") + self.assertEqual(path2.full_path, mozpath.join(config.topsrcdir, "qux", "qux")) + + self.assertEqual(path1, path2) + + path1 = Path(ctxt1, "!qux") + self.assertIsInstance(path1, ObjDirPath) + self.assertEqual(path1, "!qux") + self.assertEqual(path1.full_path, mozpath.join(config.topobjdir, "foo", "qux")) + + path2 = Path(ctxt2, "!../foo/qux") + self.assertIsInstance(path2, ObjDirPath) + self.assertEqual(path2, "!../foo/qux") + self.assertEqual(path2.full_path, mozpath.join(config.topobjdir, "foo", "qux")) + + self.assertEqual(path1, path2) + + path1 = Path(ctxt1, "!/qux/qux") + self.assertIsInstance(path1, ObjDirPath) + self.assertEqual(path1, "!/qux/qux") + self.assertEqual(path1.full_path, mozpath.join(config.topobjdir, "qux", "qux")) + + path2 = Path(ctxt2, "!/qux/qux") + self.assertIsInstance(path2, ObjDirPath) + self.assertEqual(path2, "!/qux/qux") + self.assertEqual(path2.full_path, mozpath.join(config.topobjdir, "qux", "qux")) + + self.assertEqual(path1, path2) + + path1 = Path(ctxt1, path1) + self.assertIsInstance(path1, ObjDirPath) + self.assertEqual(path1, "!/qux/qux") + self.assertEqual(path1.full_path, mozpath.join(config.topobjdir, "qux", "qux")) + + path2 = Path(ctxt2, path2) + self.assertIsInstance(path2, ObjDirPath) + self.assertEqual(path2, "!/qux/qux") + self.assertEqual(path2.full_path, mozpath.join(config.topobjdir, "qux", "qux")) + + self.assertEqual(path1, path2) + + path1 = Path(path1) + self.assertIsInstance(path1, ObjDirPath) + self.assertEqual(path1, "!/qux/qux") + self.assertEqual(path1.full_path, mozpath.join(config.topobjdir, "qux", "qux")) + + self.assertEqual(path1, path2) + + path2 = Path(path2) + self.assertIsInstance(path2, ObjDirPath) + self.assertEqual(path2, "!/qux/qux") + self.assertEqual(path2.full_path, mozpath.join(config.topobjdir, "qux", "qux")) + + self.assertEqual(path1, path2) + + def test_source_path(self): + config = self.config + ctxt = Context(config=config) + ctxt.push_source(mozpath.join(config.topsrcdir, "foo", "moz.build")) + + path = SourcePath(ctxt, "qux") + self.assertEqual(path, "qux") + self.assertEqual(path.full_path, mozpath.join(config.topsrcdir, "foo", "qux")) + self.assertEqual(path.translated, mozpath.join(config.topobjdir, "foo", "qux")) + + path = SourcePath(ctxt, "../bar/qux") + self.assertEqual(path, "../bar/qux") + self.assertEqual(path.full_path, mozpath.join(config.topsrcdir, "bar", "qux")) + self.assertEqual(path.translated, mozpath.join(config.topobjdir, "bar", "qux")) + + path = SourcePath(ctxt, "/qux/qux") + self.assertEqual(path, "/qux/qux") + self.assertEqual(path.full_path, mozpath.join(config.topsrcdir, "qux", "qux")) + self.assertEqual(path.translated, mozpath.join(config.topobjdir, "qux", "qux")) + + with self.assertRaises(ValueError): + SourcePath(ctxt, "!../bar/qux") + + with self.assertRaises(ValueError): + SourcePath(ctxt, "!/qux/qux") + + path = SourcePath(path) + self.assertIsInstance(path, SourcePath) + self.assertEqual(path, "/qux/qux") + self.assertEqual(path.full_path, mozpath.join(config.topsrcdir, "qux", "qux")) + self.assertEqual(path.translated, mozpath.join(config.topobjdir, "qux", "qux")) + + path = Path(path) + self.assertIsInstance(path, SourcePath) + + def test_objdir_path(self): + config = self.config + ctxt = Context(config=config) + ctxt.push_source(mozpath.join(config.topsrcdir, "foo", "moz.build")) + + path = ObjDirPath(ctxt, "!qux") + self.assertEqual(path, "!qux") + self.assertEqual(path.full_path, mozpath.join(config.topobjdir, "foo", "qux")) + + path = ObjDirPath(ctxt, "!../bar/qux") + self.assertEqual(path, "!../bar/qux") + self.assertEqual(path.full_path, mozpath.join(config.topobjdir, "bar", "qux")) + + path = ObjDirPath(ctxt, "!/qux/qux") + self.assertEqual(path, "!/qux/qux") + self.assertEqual(path.full_path, mozpath.join(config.topobjdir, "qux", "qux")) + + with self.assertRaises(ValueError): + path = ObjDirPath(ctxt, "../bar/qux") + + with self.assertRaises(ValueError): + path = ObjDirPath(ctxt, "/qux/qux") + + path = ObjDirPath(path) + self.assertIsInstance(path, ObjDirPath) + self.assertEqual(path, "!/qux/qux") + self.assertEqual(path.full_path, mozpath.join(config.topobjdir, "qux", "qux")) + + path = Path(path) + self.assertIsInstance(path, ObjDirPath) + + def test_absolute_path(self): + config = self.config + ctxt = Context(config=config) + ctxt.push_source(mozpath.join(config.topsrcdir, "foo", "moz.build")) + + path = AbsolutePath(ctxt, "%/qux") + self.assertEqual(path, "%/qux") + self.assertEqual(path.full_path, "/qux") + + with self.assertRaises(ValueError): + path = AbsolutePath(ctxt, "%qux") + + def test_path_with_mixed_contexts(self): + config = self.config + ctxt1 = Context(config=config) + ctxt1.push_source(mozpath.join(config.topsrcdir, "foo", "moz.build")) + ctxt2 = Context(config=config) + ctxt2.push_source(mozpath.join(config.topsrcdir, "bar", "moz.build")) + + path1 = Path(ctxt1, "qux") + path2 = Path(ctxt2, path1) + self.assertEqual(path2, path1) + self.assertEqual(path2, "qux") + self.assertEqual(path2.context, ctxt1) + self.assertEqual(path2.full_path, mozpath.join(config.topsrcdir, "foo", "qux")) + + path1 = Path(ctxt1, "../bar/qux") + path2 = Path(ctxt2, path1) + self.assertEqual(path2, path1) + self.assertEqual(path2, "../bar/qux") + self.assertEqual(path2.context, ctxt1) + self.assertEqual(path2.full_path, mozpath.join(config.topsrcdir, "bar", "qux")) + + path1 = Path(ctxt1, "/qux/qux") + path2 = Path(ctxt2, path1) + self.assertEqual(path2, path1) + self.assertEqual(path2, "/qux/qux") + self.assertEqual(path2.context, ctxt1) + self.assertEqual(path2.full_path, mozpath.join(config.topsrcdir, "qux", "qux")) + + path1 = Path(ctxt1, "!qux") + path2 = Path(ctxt2, path1) + self.assertEqual(path2, path1) + self.assertEqual(path2, "!qux") + self.assertEqual(path2.context, ctxt1) + self.assertEqual(path2.full_path, mozpath.join(config.topobjdir, "foo", "qux")) + + path1 = Path(ctxt1, "!../bar/qux") + path2 = Path(ctxt2, path1) + self.assertEqual(path2, path1) + self.assertEqual(path2, "!../bar/qux") + self.assertEqual(path2.context, ctxt1) + self.assertEqual(path2.full_path, mozpath.join(config.topobjdir, "bar", "qux")) + + path1 = Path(ctxt1, "!/qux/qux") + path2 = Path(ctxt2, path1) + self.assertEqual(path2, path1) + self.assertEqual(path2, "!/qux/qux") + self.assertEqual(path2.context, ctxt1) + self.assertEqual(path2.full_path, mozpath.join(config.topobjdir, "qux", "qux")) + + def test_path_typed_list(self): + config = self.config + ctxt1 = Context(config=config) + ctxt1.push_source(mozpath.join(config.topsrcdir, "foo", "moz.build")) + ctxt2 = Context(config=config) + ctxt2.push_source(mozpath.join(config.topsrcdir, "bar", "moz.build")) + + paths = [ + "!../bar/qux", + "!/qux/qux", + "!qux", + "../bar/qux", + "/qux/qux", + "qux", + ] + + MyList = ContextDerivedTypedList(Path) + l = MyList(ctxt1) + l += paths + + for p_str, p_path in zip(paths, l): + self.assertEqual(p_str, p_path) + self.assertEqual(p_path, Path(ctxt1, p_str)) + self.assertEqual( + p_path.join("foo"), Path(ctxt1, mozpath.join(p_str, "foo")) + ) + + l2 = MyList(ctxt2) + l2 += paths + + for p_str, p_path in zip(paths, l2): + self.assertEqual(p_str, p_path) + self.assertEqual(p_path, Path(ctxt2, p_str)) + + # Assigning with Paths from another context doesn't rebase them + l2 = MyList(ctxt2) + l2 += l + + for p_str, p_path in zip(paths, l2): + self.assertEqual(p_str, p_path) + self.assertEqual(p_path, Path(ctxt1, p_str)) + + MyListWithFlags = ContextDerivedTypedListWithItems( + Path, + StrictOrderingOnAppendListWithFlagsFactory( + { + "foo": bool, + } + ), + ) + l = MyListWithFlags(ctxt1) + l += paths + + for p in paths: + l[p].foo = True + + for p_str, p_path in zip(paths, l): + self.assertEqual(p_str, p_path) + self.assertEqual(p_path, Path(ctxt1, p_str)) + self.assertEqual(l[p_str].foo, True) + self.assertEqual(l[p_path].foo, True) + + def test_path_typed_hierarchy_list(self): + config = self.config + ctxt1 = Context(config=config) + ctxt1.push_source(mozpath.join(config.topsrcdir, "foo", "moz.build")) + ctxt2 = Context(config=config) + ctxt2.push_source(mozpath.join(config.topsrcdir, "bar", "moz.build")) + + paths = [ + "!../bar/qux", + "!/qux/qux", + "!qux", + "../bar/qux", + "/qux/qux", + "qux", + ] + + MyList = ContextDerivedTypedHierarchicalStringList(Path) + l = MyList(ctxt1) + l += paths + l.subdir += paths + + for _, files in l.walk(): + for p_str, p_path in zip(paths, files): + self.assertEqual(p_str, p_path) + self.assertEqual(p_path, Path(ctxt1, p_str)) + self.assertEqual( + p_path.join("foo"), Path(ctxt1, mozpath.join(p_str, "foo")) + ) + + l2 = MyList(ctxt2) + l2 += paths + l2.subdir += paths + + for _, files in l2.walk(): + for p_str, p_path in zip(paths, files): + self.assertEqual(p_str, p_path) + self.assertEqual(p_path, Path(ctxt2, p_str)) + + # Assigning with Paths from another context doesn't rebase them + l2 = MyList(ctxt2) + l2 += l + + for _, files in l2.walk(): + for p_str, p_path in zip(paths, files): + self.assertEqual(p_str, p_path) + self.assertEqual(p_path, Path(ctxt1, p_str)) + + +class TestTypedRecord(unittest.TestCase): + def test_fields(self): + T = ContextDerivedTypedRecord(("field1", six.text_type), ("field2", list)) + inst = T(None) + self.assertEqual(inst.field1, "") + self.assertEqual(inst.field2, []) + + inst.field1 = "foo" + inst.field2 += ["bar"] + + self.assertEqual(inst.field1, "foo") + self.assertEqual(inst.field2, ["bar"]) + + with self.assertRaises(AttributeError): + inst.field3 = [] + + def test_coercion(self): + T = ContextDerivedTypedRecord(("field1", six.text_type), ("field2", list)) + inst = T(None) + inst.field1 = 3 + inst.field2 += ("bar",) + self.assertEqual(inst.field1, "3") + self.assertEqual(inst.field2, ["bar"]) + + with self.assertRaises(TypeError): + inst.field2 = object() + + +class TestFiles(unittest.TestCase): + def test_aggregate_empty(self): + c = Context({}) + + files = {"moz.build": Files(c, "**")} + + self.assertEqual( + Files.aggregate(files), + { + "bug_component_counts": [], + "recommended_bug_component": None, + }, + ) + + def test_single_bug_component(self): + c = Context({}) + f = Files(c, "**") + f["BUG_COMPONENT"] = ("Product1", "Component1") + + files = {"moz.build": f} + self.assertEqual( + Files.aggregate(files), + { + "bug_component_counts": [(("Product1", "Component1"), 1)], + "recommended_bug_component": ("Product1", "Component1"), + }, + ) + + def test_multiple_bug_components(self): + c = Context({}) + f1 = Files(c, "**") + f1["BUG_COMPONENT"] = ("Product1", "Component1") + + f2 = Files(c, "**") + f2["BUG_COMPONENT"] = ("Product2", "Component2") + + files = {"a": f1, "b": f2, "c": f1} + self.assertEqual( + Files.aggregate(files), + { + "bug_component_counts": [ + (("Product1", "Component1"), 2), + (("Product2", "Component2"), 1), + ], + "recommended_bug_component": ("Product1", "Component1"), + }, + ) + + def test_no_recommended_bug_component(self): + """If there is no clear count winner, we don't recommend a bug component.""" + c = Context({}) + f1 = Files(c, "**") + f1["BUG_COMPONENT"] = ("Product1", "Component1") + + f2 = Files(c, "**") + f2["BUG_COMPONENT"] = ("Product2", "Component2") + + files = {"a": f1, "b": f2} + self.assertEqual( + Files.aggregate(files), + { + "bug_component_counts": [ + (("Product1", "Component1"), 1), + (("Product2", "Component2"), 1), + ], + "recommended_bug_component": None, + }, + ) + + def test_multiple_patterns(self): + c = Context({}) + f1 = Files(c, "a/**") + f1["BUG_COMPONENT"] = ("Product1", "Component1") + f2 = Files(c, "b/**", "a/bar") + f2["BUG_COMPONENT"] = ("Product2", "Component2") + + files = {"a/foo": f1, "a/bar": f2, "b/foo": f2} + self.assertEqual( + Files.aggregate(files), + { + "bug_component_counts": [ + (("Product2", "Component2"), 2), + (("Product1", "Component1"), 1), + ], + "recommended_bug_component": ("Product2", "Component2"), + }, + ) + + +if __name__ == "__main__": + main() diff --git a/python/mozbuild/mozbuild/test/frontend/test_emitter.py b/python/mozbuild/mozbuild/test/frontend/test_emitter.py new file mode 100644 index 0000000000..13018ba5b2 --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/test_emitter.py @@ -0,0 +1,1894 @@ +# 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/. + +import os +import unittest + +import mozpack.path as mozpath +import six +from mozunit import main + +from mozbuild.frontend.context import ObjDirPath, Path +from mozbuild.frontend.data import ( + ComputedFlags, + ConfigFileSubstitution, + Defines, + DirectoryTraversal, + Exports, + FinalTargetPreprocessedFiles, + GeneratedFile, + HostProgram, + HostRustLibrary, + HostRustProgram, + HostSources, + IPDLCollection, + JARManifest, + LocalInclude, + LocalizedFiles, + LocalizedPreprocessedFiles, + Program, + RustLibrary, + RustProgram, + SharedLibrary, + SimpleProgram, + Sources, + StaticLibrary, + TestHarnessFiles, + TestManifest, + UnifiedSources, + VariablePassthru, + WasmSources, +) +from mozbuild.frontend.emitter import TreeMetadataEmitter +from mozbuild.frontend.reader import ( + BuildReader, + BuildReaderError, + SandboxValidationError, +) +from mozbuild.test.common import MockConfig + +data_path = mozpath.abspath(mozpath.dirname(__file__)) +data_path = mozpath.join(data_path, "data") + + +class TestEmitterBasic(unittest.TestCase): + def setUp(self): + self._old_env = dict(os.environ) + os.environ.pop("MOZ_OBJDIR", None) + + def tearDown(self): + os.environ.clear() + os.environ.update(self._old_env) + + def reader(self, name, enable_tests=False, extra_substs=None): + substs = dict( + ENABLE_TESTS="1" if enable_tests else "", + BIN_SUFFIX=".prog", + HOST_BIN_SUFFIX=".hostprog", + OS_TARGET="WINNT", + COMPILE_ENVIRONMENT="1", + STL_FLAGS=["-I/path/to/topobjdir/dist/stl_wrappers"], + VISIBILITY_FLAGS=["-include", "$(topsrcdir)/config/gcc_hidden.h"], + OBJ_SUFFIX="obj", + WASM_OBJ_SUFFIX="wasm", + WASM_CFLAGS=["-foo"], + ) + if extra_substs: + substs.update(extra_substs) + config = MockConfig(mozpath.join(data_path, name), extra_substs=substs) + + return BuildReader(config) + + def read_topsrcdir(self, reader, filter_common=True): + emitter = TreeMetadataEmitter(reader.config) + objs = list(emitter.emit(reader.read_topsrcdir())) + self.assertGreater(len(objs), 0) + + filtered = [] + for obj in objs: + if filter_common and isinstance(obj, DirectoryTraversal): + continue + + filtered.append(obj) + + return filtered + + def test_dirs_traversal_simple(self): + reader = self.reader("traversal-simple") + objs = self.read_topsrcdir(reader, filter_common=False) + self.assertEqual(len(objs), 4) + + for o in objs: + self.assertIsInstance(o, DirectoryTraversal) + self.assertTrue(os.path.isabs(o.context_main_path)) + self.assertEqual(len(o.context_all_paths), 1) + + reldirs = [o.relsrcdir for o in objs] + self.assertEqual(reldirs, ["", "foo", "foo/biz", "bar"]) + + dirs = [[d.full_path for d in o.dirs] for o in objs] + self.assertEqual( + dirs, + [ + [ + mozpath.join(reader.config.topsrcdir, "foo"), + mozpath.join(reader.config.topsrcdir, "bar"), + ], + [mozpath.join(reader.config.topsrcdir, "foo", "biz")], + [], + [], + ], + ) + + def test_traversal_all_vars(self): + reader = self.reader("traversal-all-vars") + objs = self.read_topsrcdir(reader, filter_common=False) + self.assertEqual(len(objs), 2) + + for o in objs: + self.assertIsInstance(o, DirectoryTraversal) + + reldirs = set([o.relsrcdir for o in objs]) + self.assertEqual(reldirs, set(["", "regular"])) + + for o in objs: + reldir = o.relsrcdir + + if reldir == "": + self.assertEqual( + [d.full_path for d in o.dirs], + [mozpath.join(reader.config.topsrcdir, "regular")], + ) + + def test_traversal_all_vars_enable_tests(self): + reader = self.reader("traversal-all-vars", enable_tests=True) + objs = self.read_topsrcdir(reader, filter_common=False) + self.assertEqual(len(objs), 3) + + for o in objs: + self.assertIsInstance(o, DirectoryTraversal) + + reldirs = set([o.relsrcdir for o in objs]) + self.assertEqual(reldirs, set(["", "regular", "test"])) + + for o in objs: + reldir = o.relsrcdir + + if reldir == "": + self.assertEqual( + [d.full_path for d in o.dirs], + [ + mozpath.join(reader.config.topsrcdir, "regular"), + mozpath.join(reader.config.topsrcdir, "test"), + ], + ) + + def test_config_file_substitution(self): + reader = self.reader("config-file-substitution") + objs = self.read_topsrcdir(reader) + self.assertEqual(len(objs), 2) + + self.assertIsInstance(objs[0], ConfigFileSubstitution) + self.assertIsInstance(objs[1], ConfigFileSubstitution) + + topobjdir = mozpath.abspath(reader.config.topobjdir) + self.assertEqual(objs[0].relpath, "foo") + self.assertEqual( + mozpath.normpath(objs[0].output_path), + mozpath.normpath(mozpath.join(topobjdir, "foo")), + ) + self.assertEqual( + mozpath.normpath(objs[1].output_path), + mozpath.normpath(mozpath.join(topobjdir, "bar")), + ) + + def test_variable_passthru(self): + reader = self.reader("variable-passthru") + objs = self.read_topsrcdir(reader) + + self.assertEqual(len(objs), 1) + self.assertIsInstance(objs[0], VariablePassthru) + + wanted = { + "NO_DIST_INSTALL": True, + "RCFILE": "foo.rc", + "RCINCLUDE": "bar.rc", + "WIN32_EXE_LDFLAGS": ["-subsystem:console"], + } + + variables = objs[0].variables + maxDiff = self.maxDiff + self.maxDiff = None + self.assertEqual(wanted, variables) + self.maxDiff = maxDiff + + def test_compile_flags(self): + reader = self.reader( + "compile-flags", extra_substs={"WARNINGS_AS_ERRORS": "-Werror"} + ) + sources, ldflags, lib, flags = self.read_topsrcdir(reader) + self.assertIsInstance(flags, ComputedFlags) + self.assertEqual(flags.flags["STL"], reader.config.substs["STL_FLAGS"]) + self.assertEqual( + flags.flags["VISIBILITY"], reader.config.substs["VISIBILITY_FLAGS"] + ) + self.assertEqual(flags.flags["WARNINGS_AS_ERRORS"], ["-Werror"]) + self.assertEqual(flags.flags["MOZBUILD_CFLAGS"], ["-Wall", "-funroll-loops"]) + self.assertEqual(flags.flags["MOZBUILD_CXXFLAGS"], ["-funroll-loops", "-Wall"]) + + def test_asflags(self): + reader = self.reader("asflags", extra_substs={"ASFLAGS": ["-safeseh"]}) + as_sources, sources, ldflags, lib, flags, asflags = self.read_topsrcdir(reader) + self.assertIsInstance(asflags, ComputedFlags) + self.assertEqual(asflags.flags["OS"], reader.config.substs["ASFLAGS"]) + self.assertEqual(asflags.flags["MOZBUILD"], ["-no-integrated-as"]) + + def test_debug_flags(self): + reader = self.reader( + "compile-flags", + extra_substs={"MOZ_DEBUG_FLAGS": "-g", "MOZ_DEBUG_SYMBOLS": "1"}, + ) + sources, ldflags, lib, flags = self.read_topsrcdir(reader) + self.assertIsInstance(flags, ComputedFlags) + self.assertEqual(flags.flags["DEBUG"], ["-g"]) + + def test_disable_debug_flags(self): + reader = self.reader( + "compile-flags", + extra_substs={"MOZ_DEBUG_FLAGS": "-g", "MOZ_DEBUG_SYMBOLS": ""}, + ) + sources, ldflags, lib, flags = self.read_topsrcdir(reader) + self.assertIsInstance(flags, ComputedFlags) + self.assertEqual(flags.flags["DEBUG"], []) + + def test_link_flags(self): + reader = self.reader( + "link-flags", + extra_substs={ + "OS_LDFLAGS": ["-Wl,rpath-link=/usr/lib"], + "MOZ_OPTIMIZE": "", + "MOZ_OPTIMIZE_LDFLAGS": ["-Wl,-dead_strip"], + "MOZ_DEBUG_LDFLAGS": ["-framework ExceptionHandling"], + }, + ) + sources, ldflags, lib, compile_flags = self.read_topsrcdir(reader) + self.assertIsInstance(ldflags, ComputedFlags) + self.assertEqual(ldflags.flags["OS"], reader.config.substs["OS_LDFLAGS"]) + self.assertEqual( + ldflags.flags["MOZBUILD"], ["-Wl,-U_foo", "-framework Foo", "-x"] + ) + self.assertEqual(ldflags.flags["OPTIMIZE"], []) + + def test_debug_ldflags(self): + reader = self.reader( + "link-flags", + extra_substs={ + "MOZ_DEBUG_SYMBOLS": "1", + "MOZ_DEBUG_LDFLAGS": ["-framework ExceptionHandling"], + }, + ) + sources, ldflags, lib, compile_flags = self.read_topsrcdir(reader) + self.assertIsInstance(ldflags, ComputedFlags) + self.assertEqual(ldflags.flags["OS"], reader.config.substs["MOZ_DEBUG_LDFLAGS"]) + + def test_windows_opt_link_flags(self): + reader = self.reader( + "link-flags", + extra_substs={ + "OS_ARCH": "WINNT", + "GNU_CC": "", + "MOZ_OPTIMIZE": "1", + "MOZ_DEBUG_LDFLAGS": ["-DEBUG"], + "MOZ_DEBUG_SYMBOLS": "1", + "MOZ_OPTIMIZE_FLAGS": [], + "MOZ_OPTIMIZE_LDFLAGS": [], + }, + ) + sources, ldflags, lib, compile_flags = self.read_topsrcdir(reader) + self.assertIsInstance(ldflags, ComputedFlags) + self.assertIn("-DEBUG", ldflags.flags["OS"]) + self.assertIn("-OPT:REF,ICF", ldflags.flags["OS"]) + + def test_windows_dmd_link_flags(self): + reader = self.reader( + "link-flags", + extra_substs={ + "OS_ARCH": "WINNT", + "GNU_CC": "", + "MOZ_DMD": "1", + "MOZ_DEBUG_LDFLAGS": ["-DEBUG"], + "MOZ_DEBUG_SYMBOLS": "1", + "MOZ_OPTIMIZE": "1", + "MOZ_OPTIMIZE_FLAGS": [], + }, + ) + sources, ldflags, lib, compile_flags = self.read_topsrcdir(reader) + self.assertIsInstance(ldflags, ComputedFlags) + self.assertEqual(ldflags.flags["OS"], ["-DEBUG", "-OPT:REF,ICF"]) + + def test_host_compile_flags(self): + reader = self.reader( + "host-compile-flags", + extra_substs={ + "HOST_CXXFLAGS": ["-Wall", "-Werror"], + "HOST_CFLAGS": ["-Werror", "-Wall"], + }, + ) + sources, ldflags, flags, lib, target_flags = self.read_topsrcdir(reader) + self.assertIsInstance(flags, ComputedFlags) + self.assertEqual( + flags.flags["HOST_CXXFLAGS"], reader.config.substs["HOST_CXXFLAGS"] + ) + self.assertEqual( + flags.flags["HOST_CFLAGS"], reader.config.substs["HOST_CFLAGS"] + ) + self.assertEqual( + set(flags.flags["HOST_DEFINES"]), + set(["-DFOO", '-DBAZ="abcd"', "-UQUX", "-DBAR=7", "-DVALUE=xyz"]), + ) + self.assertEqual( + flags.flags["MOZBUILD_HOST_CFLAGS"], ["-funroll-loops", "-host-arg"] + ) + self.assertEqual(flags.flags["MOZBUILD_HOST_CXXFLAGS"], []) + + def test_host_no_optimize_flags(self): + reader = self.reader( + "host-compile-flags", + extra_substs={"MOZ_OPTIMIZE": "1", "MOZ_OPTIMIZE_FLAGS": ["-O2"]}, + ) + sources, ldflags, flags, lib, target_flags = self.read_topsrcdir(reader) + self.assertIsInstance(flags, ComputedFlags) + self.assertEqual(flags.flags["HOST_OPTIMIZE"], []) + + def test_host_optimize_flags(self): + reader = self.reader( + "host-compile-flags", + extra_substs={"HOST_OPTIMIZE_FLAGS": ["-O2"]}, + ) + sources, ldflags, flags, lib, target_flags = self.read_topsrcdir(reader) + self.assertIsInstance(flags, ComputedFlags) + self.assertEqual(flags.flags["HOST_OPTIMIZE"], ["-O2"]) + + def test_cross_optimize_flags(self): + reader = self.reader( + "host-compile-flags", + extra_substs={ + "MOZ_OPTIMIZE": "1", + "MOZ_OPTIMIZE_FLAGS": ["-O2"], + "HOST_OPTIMIZE_FLAGS": ["-O3"], + "CROSS_COMPILE": "1", + }, + ) + sources, ldflags, flags, lib, target_flags = self.read_topsrcdir(reader) + self.assertIsInstance(flags, ComputedFlags) + self.assertEqual(flags.flags["HOST_OPTIMIZE"], ["-O3"]) + + def test_host_rtl_flag(self): + reader = self.reader( + "host-compile-flags", extra_substs={"OS_ARCH": "WINNT", "MOZ_DEBUG": "1"} + ) + sources, ldflags, flags, lib, target_flags = self.read_topsrcdir(reader) + self.assertIsInstance(flags, ComputedFlags) + self.assertEqual(flags.flags["RTL"], ["-MDd"]) + + def test_compile_flags_validation(self): + reader = self.reader("compile-flags-field-validation") + + with six.assertRaisesRegex(self, BuildReaderError, "Invalid value."): + self.read_topsrcdir(reader) + + reader = self.reader("compile-flags-type-validation") + with six.assertRaisesRegex( + self, BuildReaderError, "A list of strings must be provided" + ): + self.read_topsrcdir(reader) + + def test_compile_flags_templates(self): + reader = self.reader( + "compile-flags-templates", + extra_substs={ + "NSPR_CFLAGS": ["-I/nspr/path"], + "NSS_CFLAGS": ["-I/nss/path"], + "MOZ_JPEG_CFLAGS": ["-I/jpeg/path"], + "MOZ_PNG_CFLAGS": ["-I/png/path"], + "MOZ_ZLIB_CFLAGS": ["-I/zlib/path"], + "MOZ_PIXMAN_CFLAGS": ["-I/pixman/path"], + }, + ) + sources, ldflags, lib, flags = self.read_topsrcdir(reader) + self.assertIsInstance(flags, ComputedFlags) + self.assertEqual(flags.flags["STL"], []) + self.assertEqual(flags.flags["VISIBILITY"], []) + self.assertEqual( + flags.flags["OS_INCLUDES"], + [ + "-I/nspr/path", + "-I/nss/path", + "-I/jpeg/path", + "-I/png/path", + "-I/zlib/path", + "-I/pixman/path", + ], + ) + + def test_disable_stl_wrapping(self): + reader = self.reader("disable-stl-wrapping") + sources, ldflags, lib, flags = self.read_topsrcdir(reader) + self.assertIsInstance(flags, ComputedFlags) + self.assertEqual(flags.flags["STL"], []) + + def test_visibility_flags(self): + reader = self.reader("visibility-flags") + sources, ldflags, lib, flags = self.read_topsrcdir(reader) + self.assertIsInstance(flags, ComputedFlags) + self.assertEqual(flags.flags["VISIBILITY"], []) + + def test_defines_in_flags(self): + reader = self.reader("compile-defines") + defines, sources, ldflags, lib, flags = self.read_topsrcdir(reader) + self.assertIsInstance(flags, ComputedFlags) + self.assertEqual( + flags.flags["LIBRARY_DEFINES"], ["-DMOZ_LIBRARY_DEFINE=MOZ_TEST"] + ) + self.assertEqual(flags.flags["DEFINES"], ["-DMOZ_TEST_DEFINE"]) + + def test_resolved_flags_error(self): + reader = self.reader("resolved-flags-error") + with six.assertRaisesRegex( + self, + BuildReaderError, + "`DEFINES` may not be set in COMPILE_FLAGS from moz.build", + ): + self.read_topsrcdir(reader) + + def test_includes_in_flags(self): + reader = self.reader("compile-includes") + defines, sources, ldflags, lib, flags = self.read_topsrcdir(reader) + self.assertIsInstance(flags, ComputedFlags) + self.assertEqual( + flags.flags["BASE_INCLUDES"], + ["-I%s" % reader.config.topsrcdir, "-I%s" % reader.config.topobjdir], + ) + self.assertEqual( + flags.flags["EXTRA_INCLUDES"], + ["-I%s/dist/include" % reader.config.topobjdir], + ) + self.assertEqual( + flags.flags["LOCAL_INCLUDES"], ["-I%s/subdir" % reader.config.topsrcdir] + ) + + def test_allow_compiler_warnings(self): + reader = self.reader( + "allow-compiler-warnings", extra_substs={"WARNINGS_AS_ERRORS": "-Werror"} + ) + sources, ldflags, lib, flags = self.read_topsrcdir(reader) + self.assertEqual(flags.flags["WARNINGS_AS_ERRORS"], []) + + def test_disable_compiler_warnings(self): + reader = self.reader( + "disable-compiler-warnings", extra_substs={"WARNINGS_CFLAGS": "-Wall"} + ) + sources, ldflags, lib, flags = self.read_topsrcdir(reader) + self.assertEqual(flags.flags["WARNINGS_CFLAGS"], []) + + def test_use_nasm(self): + # When nasm is not available, this should raise. + reader = self.reader("use-nasm") + with six.assertRaisesRegex( + self, SandboxValidationError, "nasm is not available" + ): + self.read_topsrcdir(reader) + + # When nasm is available, this should work. + reader = self.reader( + "use-nasm", extra_substs=dict(NASM="nasm", NASM_ASFLAGS="-foo") + ) + + sources, passthru, ldflags, lib, flags, asflags = self.read_topsrcdir(reader) + + self.assertIsInstance(passthru, VariablePassthru) + self.assertIsInstance(ldflags, ComputedFlags) + self.assertIsInstance(flags, ComputedFlags) + self.assertIsInstance(asflags, ComputedFlags) + + self.assertEqual(asflags.flags["OS"], reader.config.substs["NASM_ASFLAGS"]) + + maxDiff = self.maxDiff + self.maxDiff = None + self.assertEqual( + passthru.variables, + {"AS": "nasm", "AS_DASH_C_FLAG": "", "ASOUTOPTION": "-o "}, + ) + self.maxDiff = maxDiff + + def test_generated_files(self): + reader = self.reader("generated-files") + objs = self.read_topsrcdir(reader) + + self.assertEqual(len(objs), 3) + for o in objs: + self.assertIsInstance(o, GeneratedFile) + self.assertFalse(o.localized) + self.assertFalse(o.force) + + expected = ["bar.c", "foo.c", ("xpidllex.py", "xpidlyacc.py")] + for o, f in zip(objs, expected): + expected_filename = f if isinstance(f, tuple) else (f,) + self.assertEqual(o.outputs, expected_filename) + self.assertEqual(o.script, None) + self.assertEqual(o.method, None) + self.assertEqual(o.inputs, []) + + def test_generated_files_force(self): + reader = self.reader("generated-files-force") + objs = self.read_topsrcdir(reader) + + self.assertEqual(len(objs), 3) + for o in objs: + self.assertIsInstance(o, GeneratedFile) + self.assertEqual(o.force, "bar.c" in o.outputs) + + def test_localized_generated_files(self): + reader = self.reader("localized-generated-files") + objs = self.read_topsrcdir(reader) + + self.assertEqual(len(objs), 2) + for o in objs: + self.assertIsInstance(o, GeneratedFile) + self.assertTrue(o.localized) + + expected = ["abc.ini", ("bar", "baz")] + for o, f in zip(objs, expected): + expected_filename = f if isinstance(f, tuple) else (f,) + self.assertEqual(o.outputs, expected_filename) + self.assertEqual(o.script, None) + self.assertEqual(o.method, None) + self.assertEqual(o.inputs, []) + + def test_localized_generated_files_force(self): + reader = self.reader("localized-generated-files-force") + objs = self.read_topsrcdir(reader) + + self.assertEqual(len(objs), 2) + for o in objs: + self.assertIsInstance(o, GeneratedFile) + self.assertTrue(o.localized) + self.assertEqual(o.force, "abc.ini" in o.outputs) + + def test_localized_files_from_generated(self): + """Test that using LOCALIZED_GENERATED_FILES and then putting the output in + LOCALIZED_FILES as an objdir path works. + """ + reader = self.reader("localized-files-from-generated") + objs = self.read_topsrcdir(reader) + + self.assertEqual(len(objs), 2) + self.assertIsInstance(objs[0], GeneratedFile) + self.assertIsInstance(objs[1], LocalizedFiles) + + def test_localized_files_not_localized_generated(self): + """Test that using GENERATED_FILES and then putting the output in + LOCALIZED_FILES as an objdir path produces an error. + """ + reader = self.reader("localized-files-not-localized-generated") + with six.assertRaisesRegex( + self, + SandboxValidationError, + "Objdir file listed in LOCALIZED_FILES not in LOCALIZED_GENERATED_FILES:", + ): + self.read_topsrcdir(reader) + + def test_localized_generated_files_final_target_files(self): + """Test that using LOCALIZED_GENERATED_FILES and then putting the output in + FINAL_TARGET_FILES as an objdir path produces an error. + """ + reader = self.reader("localized-generated-files-final-target-files") + with six.assertRaisesRegex( + self, + SandboxValidationError, + "Outputs of LOCALIZED_GENERATED_FILES cannot be used in FINAL_TARGET_FILES:", + ): + self.read_topsrcdir(reader) + + def test_generated_files_method_names(self): + reader = self.reader("generated-files-method-names") + objs = self.read_topsrcdir(reader) + + self.assertEqual(len(objs), 2) + for o in objs: + self.assertIsInstance(o, GeneratedFile) + + expected = ["bar.c", "foo.c"] + expected_method_names = ["make_bar", "main"] + for o, expected_filename, expected_method in zip( + objs, expected, expected_method_names + ): + self.assertEqual(o.outputs, (expected_filename,)) + self.assertEqual(o.method, expected_method) + self.assertEqual(o.inputs, []) + + def test_generated_files_absolute_script(self): + reader = self.reader("generated-files-absolute-script") + objs = self.read_topsrcdir(reader) + + self.assertEqual(len(objs), 1) + + o = objs[0] + self.assertIsInstance(o, GeneratedFile) + self.assertEqual(o.outputs, ("bar.c",)) + self.assertRegex(o.script, "script.py$") + self.assertEqual(o.method, "make_bar") + self.assertEqual(o.inputs, []) + + def test_generated_files_no_script(self): + reader = self.reader("generated-files-no-script") + with six.assertRaisesRegex( + self, SandboxValidationError, "Script for generating bar.c does not exist" + ): + self.read_topsrcdir(reader) + + def test_generated_files_no_inputs(self): + reader = self.reader("generated-files-no-inputs") + with six.assertRaisesRegex( + self, SandboxValidationError, "Input for generating foo.c does not exist" + ): + self.read_topsrcdir(reader) + + def test_generated_files_no_python_script(self): + reader = self.reader("generated-files-no-python-script") + with six.assertRaisesRegex( + self, + SandboxValidationError, + "Script for generating bar.c does not end in .py", + ): + self.read_topsrcdir(reader) + + def test_exports(self): + reader = self.reader("exports") + objs = self.read_topsrcdir(reader) + + self.assertEqual(len(objs), 1) + self.assertIsInstance(objs[0], Exports) + + expected = [ + ("", ["foo.h", "bar.h", "baz.h"]), + ("mozilla", ["mozilla1.h", "mozilla2.h"]), + ("mozilla/dom", ["dom1.h", "dom2.h", "dom3.h"]), + ("mozilla/gfx", ["gfx.h"]), + ("nspr/private", ["pprio.h", "pprthred.h"]), + ("vpx", ["mem.h", "mem2.h"]), + ] + for (expect_path, expect_headers), (actual_path, actual_headers) in zip( + expected, [(path, list(seq)) for path, seq in objs[0].files.walk()] + ): + self.assertEqual(expect_path, actual_path) + self.assertEqual(expect_headers, actual_headers) + + def test_exports_missing(self): + """ + Missing files in EXPORTS is an error. + """ + reader = self.reader("exports-missing") + with six.assertRaisesRegex( + self, SandboxValidationError, "File listed in EXPORTS does not exist:" + ): + self.read_topsrcdir(reader) + + def test_exports_missing_generated(self): + """ + An objdir file in EXPORTS that is not in GENERATED_FILES is an error. + """ + reader = self.reader("exports-missing-generated") + with six.assertRaisesRegex( + self, + SandboxValidationError, + "Objdir file listed in EXPORTS not in GENERATED_FILES:", + ): + self.read_topsrcdir(reader) + + def test_exports_generated(self): + reader = self.reader("exports-generated") + objs = self.read_topsrcdir(reader) + + self.assertEqual(len(objs), 2) + self.assertIsInstance(objs[0], GeneratedFile) + self.assertIsInstance(objs[1], Exports) + exports = [(path, list(seq)) for path, seq in objs[1].files.walk()] + self.assertEqual( + exports, [("", ["foo.h"]), ("mozilla", ["mozilla1.h", "!mozilla2.h"])] + ) + path, files = exports[1] + self.assertIsInstance(files[1], ObjDirPath) + + def test_test_harness_files(self): + reader = self.reader("test-harness-files") + objs = self.read_topsrcdir(reader) + + self.assertEqual(len(objs), 1) + self.assertIsInstance(objs[0], TestHarnessFiles) + + expected = { + "mochitest": ["runtests.py", "utils.py"], + "testing/mochitest": ["mochitest.py", "mochitest.toml"], + } + + for path, strings in objs[0].files.walk(): + self.assertTrue(path in expected) + basenames = sorted(mozpath.basename(s) for s in strings) + self.assertEqual(sorted(expected[path]), basenames) + + def test_test_harness_files_root(self): + reader = self.reader("test-harness-files-root") + with six.assertRaisesRegex( + self, + SandboxValidationError, + "Cannot install files to the root of TEST_HARNESS_FILES", + ): + self.read_topsrcdir(reader) + + def test_program(self): + reader = self.reader("program") + objs = self.read_topsrcdir(reader) + + self.assertEqual(len(objs), 6) + self.assertIsInstance(objs[0], Sources) + self.assertIsInstance(objs[1], ComputedFlags) + self.assertIsInstance(objs[2], ComputedFlags) + self.assertIsInstance(objs[3], Program) + self.assertIsInstance(objs[4], SimpleProgram) + self.assertIsInstance(objs[5], SimpleProgram) + + self.assertEqual(objs[3].program, "test_program.prog") + self.assertEqual(objs[4].program, "test_program1.prog") + self.assertEqual(objs[5].program, "test_program2.prog") + + self.assertEqual(objs[3].name, "test_program.prog") + self.assertEqual(objs[4].name, "test_program1.prog") + self.assertEqual(objs[5].name, "test_program2.prog") + + self.assertEqual( + objs[4].objs, + [ + mozpath.join( + reader.config.topobjdir, + "test_program1.%s" % reader.config.substs["OBJ_SUFFIX"], + ) + ], + ) + self.assertEqual( + objs[5].objs, + [ + mozpath.join( + reader.config.topobjdir, + "test_program2.%s" % reader.config.substs["OBJ_SUFFIX"], + ) + ], + ) + + def test_program_paths(self): + """Various moz.build settings that change the destination of PROGRAM should be + accurately reflected in Program.output_path.""" + reader = self.reader("program-paths") + objs = self.read_topsrcdir(reader) + prog_paths = [o.output_path for o in objs if isinstance(o, Program)] + self.assertEqual( + prog_paths, + [ + "!/dist/bin/dist-bin.prog", + "!/dist/bin/foo/dist-subdir.prog", + "!/final/target/final-target.prog", + "!not-installed.prog", + ], + ) + + def test_host_program_paths(self): + """The destination of a HOST_PROGRAM (almost always dist/host/bin) + should be accurately reflected in Program.output_path.""" + reader = self.reader("host-program-paths") + objs = self.read_topsrcdir(reader) + prog_paths = [o.output_path for o in objs if isinstance(o, HostProgram)] + self.assertEqual( + prog_paths, + [ + "!/dist/host/bin/final-target.hostprog", + "!/dist/host/bin/dist-host-bin.hostprog", + "!not-installed.hostprog", + ], + ) + + def test_test_manifest_missing_manifest(self): + """A missing manifest file should result in an error.""" + reader = self.reader("test-manifest-missing-manifest") + + with six.assertRaisesRegex(self, BuildReaderError, "Missing files"): + self.read_topsrcdir(reader) + + def test_empty_test_manifest_rejected(self): + """A test manifest without any entries is rejected.""" + reader = self.reader("test-manifest-empty") + + with six.assertRaisesRegex(self, SandboxValidationError, "Empty test manifest"): + self.read_topsrcdir(reader) + + def test_test_manifest_just_support_files(self): + """A test manifest with no tests but support-files is not supported.""" + reader = self.reader("test-manifest-just-support") + + with six.assertRaisesRegex(self, SandboxValidationError, "Empty test manifest"): + self.read_topsrcdir(reader) + + def test_test_manifest_dupe_support_files(self): + """A test manifest with dupe support-files in a single test is not + supported. + """ + reader = self.reader("test-manifest-dupes") + + with six.assertRaisesRegex( + self, + SandboxValidationError, + "bar.js appears multiple times " + "in a test manifest under a support-files field, please omit the duplicate entry.", + ): + self.read_topsrcdir(reader) + + def test_test_manifest_absolute_support_files(self): + """Support files starting with '/' are placed relative to the install root""" + reader = self.reader("test-manifest-absolute-support") + + objs = self.read_topsrcdir(reader) + self.assertEqual(len(objs), 1) + o = objs[0] + self.assertEqual(len(o.installs), 3) + expected = [ + mozpath.normpath(mozpath.join(o.install_prefix, "../.well-known/foo.txt")), + mozpath.join(o.install_prefix, "absolute-support.toml"), + mozpath.join(o.install_prefix, "test_file.js"), + ] + paths = sorted([v[0] for v in o.installs.values()]) + self.assertEqual(paths, expected) + + @unittest.skip("Bug 1304316 - Items in the second set but not the first") + def test_test_manifest_shared_support_files(self): + """Support files starting with '!' are given separate treatment, so their + installation can be resolved when running tests. + """ + reader = self.reader("test-manifest-shared-support") + supported, child = self.read_topsrcdir(reader) + + expected_deferred_installs = { + "!/child/test_sub.js", + "!/child/another-file.sjs", + "!/child/data/**", + } + + self.assertEqual(len(supported.installs), 3) + self.assertEqual(set(supported.deferred_installs), expected_deferred_installs) + self.assertEqual(len(child.installs), 3) + self.assertEqual(len(child.pattern_installs), 1) + + def test_test_manifest_deffered_install_missing(self): + """A non-existent shared support file reference produces an error.""" + reader = self.reader("test-manifest-shared-missing") + + with six.assertRaisesRegex( + self, + SandboxValidationError, + "entry in support-files not present in the srcdir", + ): + self.read_topsrcdir(reader) + + def test_test_manifest_install_includes(self): + """Ensure that any [include:foo.toml] are copied to the objdir.""" + reader = self.reader("test-manifest-install-includes") + + objs = self.read_topsrcdir(reader) + self.assertEqual(len(objs), 1) + o = objs[0] + self.assertEqual(len(o.installs), 3) + self.assertEqual(o.manifest_relpath, "mochitest.toml") + self.assertEqual(o.manifest_obj_relpath, "mochitest.toml") + expected = [ + mozpath.normpath(mozpath.join(o.install_prefix, "common.toml")), + mozpath.normpath(mozpath.join(o.install_prefix, "mochitest.toml")), + mozpath.normpath(mozpath.join(o.install_prefix, "test_foo.html")), + ] + paths = sorted([v[0] for v in o.installs.values()]) + self.assertEqual(paths, expected) + + def test_test_manifest_includes(self): + """Ensure that manifest objects from the emitter list a correct manifest.""" + reader = self.reader("test-manifest-emitted-includes") + [obj] = self.read_topsrcdir(reader) + + # Expected manifest leafs for our tests. + expected_manifests = { + "reftest1.html": "reftest.list", + "reftest1-ref.html": "reftest.list", + "reftest2.html": "included-reftest.list", + "reftest2-ref.html": "included-reftest.list", + } + + for t in obj.tests: + self.assertTrue(t["manifest"].endswith(expected_manifests[t["name"]])) + + def test_test_manifest_keys_extracted(self): + """Ensure all metadata from test manifests is extracted.""" + reader = self.reader("test-manifest-keys-extracted") + + objs = [o for o in self.read_topsrcdir(reader) if isinstance(o, TestManifest)] + + self.assertEqual(len(objs), 8) + + metadata = { + "a11y.toml": { + "flavor": "a11y", + "installs": {"a11y.toml": False, "test_a11y.js": True}, + "pattern-installs": 1, + }, + "browser.toml": { + "flavor": "browser-chrome", + "installs": { + "browser.toml": False, + "test_browser.js": True, + "support1": False, + "support2": False, + }, + }, + "mochitest.toml": { + "flavor": "mochitest", + "installs": {"mochitest.toml": False, "test_mochitest.js": True}, + "external": {"external1", "external2"}, + }, + "chrome.toml": { + "flavor": "chrome", + "installs": {"chrome.toml": False, "test_chrome.js": True}, + }, + "xpcshell.toml": { + "flavor": "xpcshell", + "dupe": True, + "installs": { + "xpcshell.toml": False, + "test_xpcshell.js": True, + "head1": False, + "head2": False, + }, + }, + "reftest.list": {"flavor": "reftest", "installs": {}}, + "crashtest.list": {"flavor": "crashtest", "installs": {}}, + "python.toml": {"flavor": "python", "installs": {"python.toml": False}}, + } + + for o in objs: + m = metadata[mozpath.basename(o.manifest_relpath)] + + self.assertTrue(o.path.startswith(o.directory)) + self.assertEqual(o.flavor, m["flavor"]) + self.assertEqual(o.dupe_manifest, m.get("dupe", False)) + + external_normalized = set(mozpath.basename(p) for p in o.external_installs) + self.assertEqual(external_normalized, m.get("external", set())) + + self.assertEqual(len(o.installs), len(m["installs"])) + for path in o.installs.keys(): + self.assertTrue(path.startswith(o.directory)) + relpath = path[len(o.directory) + 1 :] + + self.assertIn(relpath, m["installs"]) + self.assertEqual(o.installs[path][1], m["installs"][relpath]) + + if "pattern-installs" in m: + self.assertEqual(len(o.pattern_installs), m["pattern-installs"]) + + def test_test_manifest_unmatched_generated(self): + reader = self.reader("test-manifest-unmatched-generated") + + with six.assertRaisesRegex( + self, + SandboxValidationError, + "entry in generated-files not present elsewhere", + ): + self.read_topsrcdir(reader), + + def test_test_manifest_parent_support_files_dir(self): + """support-files referencing a file in a parent directory works.""" + reader = self.reader("test-manifest-parent-support-files-dir") + + objs = [o for o in self.read_topsrcdir(reader) if isinstance(o, TestManifest)] + + self.assertEqual(len(objs), 1) + + o = objs[0] + + expected = mozpath.join(o.srcdir, "support-file.txt") + self.assertIn(expected, o.installs) + self.assertEqual( + o.installs[expected], + ("testing/mochitest/tests/child/support-file.txt", False), + ) + + def test_test_manifest_missing_test_error(self): + """Missing test files should result in error.""" + reader = self.reader("test-manifest-missing-test-file") + + with six.assertRaisesRegex( + self, + SandboxValidationError, + "lists test that does not exist: test_missing.html", + ): + self.read_topsrcdir(reader) + + def test_test_manifest_missing_test_error_unfiltered(self): + """Missing test files should result in error, even when the test list is not filtered.""" + reader = self.reader("test-manifest-missing-test-file-unfiltered") + + with six.assertRaisesRegex( + self, SandboxValidationError, "lists test that does not exist: missing.js" + ): + self.read_topsrcdir(reader) + + def test_ipdl_sources(self): + reader = self.reader( + "ipdl_sources", + extra_substs={"IPDL_ROOT": mozpath.abspath("/path/to/topobjdir")}, + ) + objs = self.read_topsrcdir(reader) + ipdl_collection = objs[0] + self.assertIsInstance(ipdl_collection, IPDLCollection) + + ipdls = set( + mozpath.relpath(p, ipdl_collection.topsrcdir) + for p in ipdl_collection.all_regular_sources() + ) + expected = set( + ["bar/bar.ipdl", "bar/bar2.ipdlh", "foo/foo.ipdl", "foo/foo2.ipdlh"] + ) + + self.assertEqual(ipdls, expected) + + pp_ipdls = set( + mozpath.relpath(p, ipdl_collection.topsrcdir) + for p in ipdl_collection.all_preprocessed_sources() + ) + expected = set(["bar/bar1.ipdl", "foo/foo1.ipdl"]) + self.assertEqual(pp_ipdls, expected) + + def test_local_includes(self): + """Test that LOCAL_INCLUDES is emitted correctly.""" + reader = self.reader("local_includes") + objs = self.read_topsrcdir(reader) + + local_includes = [o.path for o in objs if isinstance(o, LocalInclude)] + expected = ["/bar/baz", "foo"] + + self.assertEqual(local_includes, expected) + + local_includes = [o.path.full_path for o in objs if isinstance(o, LocalInclude)] + expected = [ + mozpath.join(reader.config.topsrcdir, "bar/baz"), + mozpath.join(reader.config.topsrcdir, "foo"), + ] + + self.assertEqual(local_includes, expected) + + def test_local_includes_invalid(self): + """Test that invalid LOCAL_INCLUDES are properly detected.""" + reader = self.reader("local_includes-invalid/srcdir") + + with six.assertRaisesRegex( + self, + SandboxValidationError, + "Path specified in LOCAL_INCLUDES.*resolves to the " + "topsrcdir or topobjdir", + ): + self.read_topsrcdir(reader) + + reader = self.reader("local_includes-invalid/objdir") + + with six.assertRaisesRegex( + self, + SandboxValidationError, + "Path specified in LOCAL_INCLUDES.*resolves to the " + "topsrcdir or topobjdir", + ): + self.read_topsrcdir(reader) + + def test_local_includes_file(self): + """Test that a filename can't be used in LOCAL_INCLUDES.""" + reader = self.reader("local_includes-filename") + + with six.assertRaisesRegex( + self, + SandboxValidationError, + "Path specified in LOCAL_INCLUDES is a filename", + ): + self.read_topsrcdir(reader) + + def test_generated_includes(self): + """Test that GENERATED_INCLUDES is emitted correctly.""" + reader = self.reader("generated_includes") + objs = self.read_topsrcdir(reader) + + generated_includes = [o.path for o in objs if isinstance(o, LocalInclude)] + expected = ["!/bar/baz", "!foo"] + + self.assertEqual(generated_includes, expected) + + generated_includes = [ + o.path.full_path for o in objs if isinstance(o, LocalInclude) + ] + expected = [ + mozpath.join(reader.config.topobjdir, "bar/baz"), + mozpath.join(reader.config.topobjdir, "foo"), + ] + + self.assertEqual(generated_includes, expected) + + def test_defines(self): + reader = self.reader("defines") + objs = self.read_topsrcdir(reader) + + defines = {} + for o in objs: + if isinstance(o, Defines): + defines = o.defines + + expected = { + "BAR": 7, + "BAZ": '"abcd"', + "FOO": True, + "VALUE": "xyz", + "QUX": False, + } + + self.assertEqual(defines, expected) + + def test_jar_manifests(self): + reader = self.reader("jar-manifests") + objs = self.read_topsrcdir(reader) + + self.assertEqual(len(objs), 1) + for obj in objs: + self.assertIsInstance(obj, JARManifest) + self.assertIsInstance(obj.path, Path) + + def test_jar_manifests_multiple_files(self): + with six.assertRaisesRegex( + self, SandboxValidationError, "limited to one value" + ): + reader = self.reader("jar-manifests-multiple-files") + self.read_topsrcdir(reader) + + def test_xpidl_module_no_sources(self): + """XPIDL_MODULE without XPIDL_SOURCES should be rejected.""" + with six.assertRaisesRegex( + self, SandboxValidationError, "XPIDL_MODULE " "cannot be defined" + ): + reader = self.reader("xpidl-module-no-sources") + self.read_topsrcdir(reader) + + def test_xpidl_module_missing_sources(self): + """Missing XPIDL_SOURCES should be rejected.""" + with six.assertRaisesRegex( + self, SandboxValidationError, "File .* " "from XPIDL_SOURCES does not exist" + ): + reader = self.reader("missing-xpidl") + self.read_topsrcdir(reader) + + def test_missing_local_includes(self): + """LOCAL_INCLUDES containing non-existent directories should be rejected.""" + with six.assertRaisesRegex( + self, + SandboxValidationError, + "Path specified in " "LOCAL_INCLUDES does not exist", + ): + reader = self.reader("missing-local-includes") + self.read_topsrcdir(reader) + + def test_library_defines(self): + """Test that LIBRARY_DEFINES is propagated properly.""" + reader = self.reader("library-defines") + objs = self.read_topsrcdir(reader) + + libraries = [o for o in objs if isinstance(o, StaticLibrary)] + library_flags = [ + o + for o in objs + if isinstance(o, ComputedFlags) and "LIBRARY_DEFINES" in o.flags + ] + expected = { + "liba": "-DIN_LIBA", + "libb": "-DIN_LIBB -DIN_LIBA", + "libc": "-DIN_LIBA -DIN_LIBB", + "libd": "", + } + defines = {} + for lib in libraries: + defines[lib.basename] = " ".join(lib.lib_defines.get_defines()) + self.assertEqual(expected, defines) + defines_in_flags = {} + for flags in library_flags: + defines_in_flags[flags.relobjdir] = " ".join( + flags.flags["LIBRARY_DEFINES"] or [] + ) + self.assertEqual(expected, defines_in_flags) + + def test_sources(self): + """Test that SOURCES works properly.""" + reader = self.reader("sources") + objs = self.read_topsrcdir(reader) + + as_flags = objs.pop() + self.assertIsInstance(as_flags, ComputedFlags) + computed_flags = objs.pop() + self.assertIsInstance(computed_flags, ComputedFlags) + # The third to last object is a Linkable. + linkable = objs.pop() + self.assertTrue(linkable.cxx_link) + ld_flags = objs.pop() + self.assertIsInstance(ld_flags, ComputedFlags) + self.assertEqual(len(objs), 6) + for o in objs: + self.assertIsInstance(o, Sources) + + suffix_map = {obj.canonical_suffix: obj for obj in objs} + self.assertEqual(len(suffix_map), 6) + + expected = { + ".cpp": ["a.cpp", "b.cc", "c.cxx"], + ".c": ["d.c"], + ".m": ["e.m"], + ".mm": ["f.mm"], + ".S": ["g.S"], + ".s": ["h.s", "i.asm"], + } + for suffix, files in expected.items(): + sources = suffix_map[suffix] + self.assertEqual( + sources.files, [mozpath.join(reader.config.topsrcdir, f) for f in files] + ) + + for f in files: + self.assertIn( + mozpath.join( + reader.config.topobjdir, + "%s.%s" + % (mozpath.splitext(f)[0], reader.config.substs["OBJ_SUFFIX"]), + ), + linkable.objs, + ) + + def test_sources_just_c(self): + """Test that a linkable with no C++ sources doesn't have cxx_link set.""" + reader = self.reader("sources-just-c") + objs = self.read_topsrcdir(reader) + + as_flags = objs.pop() + self.assertIsInstance(as_flags, ComputedFlags) + flags = objs.pop() + self.assertIsInstance(flags, ComputedFlags) + # The third to last object is a Linkable. + linkable = objs.pop() + self.assertFalse(linkable.cxx_link) + + def test_linkables_cxx_link(self): + """Test that linkables transitively set cxx_link properly.""" + reader = self.reader("test-linkables-cxx-link") + got_results = 0 + for obj in self.read_topsrcdir(reader): + if isinstance(obj, SharedLibrary): + if obj.basename == "cxx_shared": + self.assertEqual( + obj.name, + "%scxx_shared%s" + % (reader.config.dll_prefix, reader.config.dll_suffix), + ) + self.assertTrue(obj.cxx_link) + got_results += 1 + elif obj.basename == "just_c_shared": + self.assertEqual( + obj.name, + "%sjust_c_shared%s" + % (reader.config.dll_prefix, reader.config.dll_suffix), + ) + self.assertFalse(obj.cxx_link) + got_results += 1 + self.assertEqual(got_results, 2) + + def test_generated_sources(self): + """Test that GENERATED_SOURCES works properly.""" + reader = self.reader("generated-sources") + objs = self.read_topsrcdir(reader) + + as_flags = objs.pop() + self.assertIsInstance(as_flags, ComputedFlags) + flags = objs.pop() + self.assertIsInstance(flags, ComputedFlags) + # The third to last object is a Linkable. + linkable = objs.pop() + self.assertTrue(linkable.cxx_link) + flags = objs.pop() + self.assertIsInstance(flags, ComputedFlags) + self.assertEqual(len(objs), 6) + + generated_sources = [ + o for o in objs if isinstance(o, Sources) and o.generated_files + ] + self.assertEqual(len(generated_sources), 6) + + suffix_map = {obj.canonical_suffix: obj for obj in generated_sources} + self.assertEqual(len(suffix_map), 6) + + expected = { + ".cpp": ["a.cpp", "b.cc", "c.cxx"], + ".c": ["d.c"], + ".m": ["e.m"], + ".mm": ["f.mm"], + ".S": ["g.S"], + ".s": ["h.s", "i.asm"], + } + for suffix, files in expected.items(): + sources = suffix_map[suffix] + self.assertEqual( + sources.generated_files, + [mozpath.join(reader.config.topobjdir, f) for f in files], + ) + + for f in files: + self.assertIn( + mozpath.join( + reader.config.topobjdir, + "%s.%s" + % (mozpath.splitext(f)[0], reader.config.substs["OBJ_SUFFIX"]), + ), + linkable.objs, + ) + + def test_host_sources(self): + """Test that HOST_SOURCES works properly.""" + reader = self.reader("host-sources") + objs = self.read_topsrcdir(reader) + + # This objdir will generate target flags. + flags = objs.pop() + self.assertIsInstance(flags, ComputedFlags) + # The second to last object is a Linkable + linkable = objs.pop() + self.assertTrue(linkable.cxx_link) + # This objdir will also generate host flags. + host_flags = objs.pop() + self.assertIsInstance(host_flags, ComputedFlags) + # ...and ldflags. + ldflags = objs.pop() + self.assertIsInstance(ldflags, ComputedFlags) + self.assertEqual(len(objs), 3) + for o in objs: + self.assertIsInstance(o, HostSources) + + suffix_map = {obj.canonical_suffix: obj for obj in objs} + self.assertEqual(len(suffix_map), 3) + + expected = { + ".cpp": ["a.cpp", "b.cc", "c.cxx"], + ".c": ["d.c"], + ".mm": ["e.mm", "f.mm"], + } + for suffix, files in expected.items(): + sources = suffix_map[suffix] + self.assertEqual( + sources.files, [mozpath.join(reader.config.topsrcdir, f) for f in files] + ) + + for f in files: + self.assertIn( + mozpath.join( + reader.config.topobjdir, + "host_%s.%s" + % (mozpath.splitext(f)[0], reader.config.substs["OBJ_SUFFIX"]), + ), + linkable.objs, + ) + + def test_wasm_sources(self): + """Test that WASM_SOURCES works properly.""" + reader = self.reader( + "wasm-sources", extra_substs={"WASM_CC": "clang", "WASM_CXX": "clang++"} + ) + objs = list(self.read_topsrcdir(reader)) + + # The second to last object is a linkable. + linkable = objs[-2] + # Other than that, we only care about the WasmSources objects. + objs = objs[:2] + for o in objs: + self.assertIsInstance(o, WasmSources) + + suffix_map = {obj.canonical_suffix: obj for obj in objs} + self.assertEqual(len(suffix_map), 2) + + expected = {".cpp": ["a.cpp", "b.cc", "c.cxx"], ".c": ["d.c"]} + for suffix, files in expected.items(): + sources = suffix_map[suffix] + self.assertEqual( + sources.files, [mozpath.join(reader.config.topsrcdir, f) for f in files] + ) + for f in files: + self.assertIn( + mozpath.join( + reader.config.topobjdir, + "%s.%s" + % ( + mozpath.splitext(f)[0], + reader.config.substs["WASM_OBJ_SUFFIX"], + ), + ), + linkable.objs, + ) + + def test_unified_sources(self): + """Test that UNIFIED_SOURCES works properly.""" + reader = self.reader("unified-sources") + objs = self.read_topsrcdir(reader) + + # The last object is a ComputedFlags, the second to last a Linkable, + # followed by ldflags, ignore them. + linkable = objs[-2] + objs = objs[:-3] + self.assertEqual(len(objs), 3) + for o in objs: + self.assertIsInstance(o, UnifiedSources) + + suffix_map = {obj.canonical_suffix: obj for obj in objs} + self.assertEqual(len(suffix_map), 3) + + expected = { + ".cpp": ["bar.cxx", "foo.cpp", "quux.cc"], + ".mm": ["objc1.mm", "objc2.mm"], + ".c": ["c1.c", "c2.c"], + } + for suffix, files in expected.items(): + sources = suffix_map[suffix] + self.assertEqual( + sources.files, [mozpath.join(reader.config.topsrcdir, f) for f in files] + ) + + # Unified sources are not required + if sources.have_unified_mapping: + for f in dict(sources.unified_source_mapping).keys(): + self.assertIn( + mozpath.join( + reader.config.topobjdir, + "%s.%s" + % ( + mozpath.splitext(f)[0], + reader.config.substs["OBJ_SUFFIX"], + ), + ), + linkable.objs, + ) + + def test_unified_sources_non_unified(self): + """Test that UNIFIED_SOURCES with FILES_PER_UNIFIED_FILE=1 works properly.""" + reader = self.reader("unified-sources-non-unified") + objs = self.read_topsrcdir(reader) + + # The last object is a Linkable, the second to last ComputedFlags, + # followed by ldflags, ignore them. + objs = objs[:-3] + self.assertEqual(len(objs), 3) + for o in objs: + self.assertIsInstance(o, UnifiedSources) + + suffix_map = {obj.canonical_suffix: obj for obj in objs} + self.assertEqual(len(suffix_map), 3) + + expected = { + ".cpp": ["bar.cxx", "foo.cpp", "quux.cc"], + ".mm": ["objc1.mm", "objc2.mm"], + ".c": ["c1.c", "c2.c"], + } + for suffix, files in expected.items(): + sources = suffix_map[suffix] + self.assertEqual( + sources.files, [mozpath.join(reader.config.topsrcdir, f) for f in files] + ) + self.assertFalse(sources.have_unified_mapping) + + def test_object_conflicts(self): + """Test that object name conflicts are detected.""" + reader = self.reader("object-conflicts/1") + with self.assertRaisesRegex( + SandboxValidationError, + "Test.cpp from SOURCES would have the same object name as" + " Test.c from SOURCES\\.", + ): + self.read_topsrcdir(reader) + + reader = self.reader("object-conflicts/2") + with self.assertRaisesRegex( + SandboxValidationError, + "Test.cpp from SOURCES would have the same object name as" + " subdir/Test.cpp from SOURCES\\.", + ): + self.read_topsrcdir(reader) + + reader = self.reader("object-conflicts/3") + with self.assertRaisesRegex( + SandboxValidationError, + "Test.cpp from UNIFIED_SOURCES would have the same object name as" + " Test.c from SOURCES in non-unified builds\\.", + ): + self.read_topsrcdir(reader) + + reader = self.reader("object-conflicts/4") + with self.assertRaisesRegex( + SandboxValidationError, + "Test.cpp from UNIFIED_SOURCES would have the same object name as" + " Test.c from UNIFIED_SOURCES in non-unified builds\\.", + ): + self.read_topsrcdir(reader) + + def test_final_target_pp_files(self): + """Test that FINAL_TARGET_PP_FILES works properly.""" + reader = self.reader("dist-files") + objs = self.read_topsrcdir(reader) + + self.assertEqual(len(objs), 1) + self.assertIsInstance(objs[0], FinalTargetPreprocessedFiles) + + # Ideally we'd test hierarchies, but that would just be testing + # the HierarchicalStringList class, which we test separately. + for path, files in objs[0].files.walk(): + self.assertEqual(path, "") + self.assertEqual(len(files), 2) + + expected = {"install.rdf", "main.js"} + for f in files: + self.assertTrue(six.text_type(f) in expected) + + def test_missing_final_target_pp_files(self): + """Test that FINAL_TARGET_PP_FILES with missing files throws errors.""" + with six.assertRaisesRegex( + self, + SandboxValidationError, + "File listed in " "FINAL_TARGET_PP_FILES does not exist", + ): + reader = self.reader("dist-files-missing") + self.read_topsrcdir(reader) + + def test_final_target_pp_files_non_srcdir(self): + """Test that non-srcdir paths in FINAL_TARGET_PP_FILES throws errors.""" + reader = self.reader("final-target-pp-files-non-srcdir") + with six.assertRaisesRegex( + self, + SandboxValidationError, + "Only source directory paths allowed in FINAL_TARGET_PP_FILES:", + ): + self.read_topsrcdir(reader) + + def test_localized_files(self): + """Test that LOCALIZED_FILES works properly.""" + reader = self.reader("localized-files") + objs = self.read_topsrcdir(reader) + + self.assertEqual(len(objs), 1) + self.assertIsInstance(objs[0], LocalizedFiles) + + for path, files in objs[0].files.walk(): + self.assertEqual(path, "foo") + self.assertEqual(len(files), 3) + + expected = {"en-US/bar.ini", "en-US/code/*.js", "en-US/foo.js"} + for f in files: + self.assertTrue(six.text_type(f) in expected) + + def test_localized_files_no_en_us(self): + """Test that LOCALIZED_FILES errors if a path does not start with + `en-US/` or contain `locales/en-US/`.""" + reader = self.reader("localized-files-no-en-us") + with six.assertRaisesRegex( + self, + SandboxValidationError, + "LOCALIZED_FILES paths must start with `en-US/` or contain `locales/en-US/`: " + "foo.js", + ): + self.read_topsrcdir(reader) + + def test_localized_pp_files(self): + """Test that LOCALIZED_PP_FILES works properly.""" + reader = self.reader("localized-pp-files") + objs = self.read_topsrcdir(reader) + + self.assertEqual(len(objs), 1) + self.assertIsInstance(objs[0], LocalizedPreprocessedFiles) + + for path, files in objs[0].files.walk(): + self.assertEqual(path, "foo") + self.assertEqual(len(files), 2) + + expected = {"en-US/bar.ini", "en-US/foo.js"} + for f in files: + self.assertTrue(six.text_type(f) in expected) + + def test_rust_library_no_cargo_toml(self): + """Test that defining a RustLibrary without a Cargo.toml fails.""" + reader = self.reader("rust-library-no-cargo-toml") + with six.assertRaisesRegex( + self, SandboxValidationError, "No Cargo.toml file found" + ): + self.read_topsrcdir(reader) + + def test_rust_library_name_mismatch(self): + """Test that defining a RustLibrary that doesn't match Cargo.toml fails.""" + reader = self.reader("rust-library-name-mismatch") + with six.assertRaisesRegex( + self, + SandboxValidationError, + "library.*does not match Cargo.toml-defined package", + ): + self.read_topsrcdir(reader) + + def test_rust_library_no_lib_section(self): + """Test that a RustLibrary Cargo.toml with no [lib] section fails.""" + reader = self.reader("rust-library-no-lib-section") + with six.assertRaisesRegex( + self, SandboxValidationError, "Cargo.toml for.* has no \\[lib\\] section" + ): + self.read_topsrcdir(reader) + + def test_rust_library_invalid_crate_type(self): + """Test that a RustLibrary Cargo.toml has a permitted crate-type.""" + reader = self.reader("rust-library-invalid-crate-type") + with six.assertRaisesRegex( + self, SandboxValidationError, "crate-type.* is not permitted" + ): + self.read_topsrcdir(reader) + + def test_rust_library_dash_folding(self): + """Test that on-disk names of RustLibrary objects convert dashes to underscores.""" + reader = self.reader( + "rust-library-dash-folding", + extra_substs=dict(RUST_TARGET="i686-pc-windows-msvc"), + ) + objs = self.read_topsrcdir(reader) + + self.assertEqual(len(objs), 4) + ldflags, host_cflags, lib, cflags = objs + self.assertIsInstance(ldflags, ComputedFlags) + self.assertIsInstance(cflags, ComputedFlags) + self.assertIsInstance(host_cflags, ComputedFlags) + self.assertIsInstance(lib, RustLibrary) + self.assertRegex(lib.lib_name, "random_crate") + self.assertRegex(lib.import_name, "random_crate") + self.assertRegex(lib.basename, "random-crate") + + def test_multiple_rust_libraries(self): + """Test that linking multiple Rust libraries throws an error""" + reader = self.reader( + "multiple-rust-libraries", + extra_substs=dict(RUST_TARGET="i686-pc-windows-msvc"), + ) + with six.assertRaisesRegex( + self, SandboxValidationError, "Cannot link the following Rust libraries" + ): + self.read_topsrcdir(reader) + + def test_rust_library_features(self): + """Test that RustLibrary features are correctly emitted.""" + reader = self.reader( + "rust-library-features", + extra_substs=dict(RUST_TARGET="i686-pc-windows-msvc"), + ) + objs = self.read_topsrcdir(reader) + + self.assertEqual(len(objs), 4) + ldflags, host_cflags, lib, cflags = objs + self.assertIsInstance(ldflags, ComputedFlags) + self.assertIsInstance(cflags, ComputedFlags) + self.assertIsInstance(host_cflags, ComputedFlags) + self.assertIsInstance(lib, RustLibrary) + self.assertEqual(lib.features, ["musthave", "cantlivewithout"]) + + def test_rust_library_duplicate_features(self): + """Test that duplicate RustLibrary features are rejected.""" + reader = self.reader("rust-library-duplicate-features") + with six.assertRaisesRegex( + self, + SandboxValidationError, + "features for .* should not contain duplicates", + ): + self.read_topsrcdir(reader) + + def test_rust_program_no_cargo_toml(self): + """Test that specifying RUST_PROGRAMS without a Cargo.toml fails.""" + reader = self.reader("rust-program-no-cargo-toml") + with six.assertRaisesRegex( + self, SandboxValidationError, "No Cargo.toml file found" + ): + self.read_topsrcdir(reader) + + def test_host_rust_program_no_cargo_toml(self): + """Test that specifying HOST_RUST_PROGRAMS without a Cargo.toml fails.""" + reader = self.reader("host-rust-program-no-cargo-toml") + with six.assertRaisesRegex( + self, SandboxValidationError, "No Cargo.toml file found" + ): + self.read_topsrcdir(reader) + + def test_rust_program_nonexistent_name(self): + """Test that specifying RUST_PROGRAMS that don't exist in Cargo.toml + correctly throws an error.""" + reader = self.reader("rust-program-nonexistent-name") + with six.assertRaisesRegex( + self, SandboxValidationError, "Cannot find Cargo.toml definition for" + ): + self.read_topsrcdir(reader) + + def test_host_rust_program_nonexistent_name(self): + """Test that specifying HOST_RUST_PROGRAMS that don't exist in + Cargo.toml correctly throws an error.""" + reader = self.reader("host-rust-program-nonexistent-name") + with six.assertRaisesRegex( + self, SandboxValidationError, "Cannot find Cargo.toml definition for" + ): + self.read_topsrcdir(reader) + + def test_rust_programs(self): + """Test RUST_PROGRAMS emission.""" + reader = self.reader( + "rust-programs", + extra_substs=dict(RUST_TARGET="i686-pc-windows-msvc", BIN_SUFFIX=".exe"), + ) + objs = self.read_topsrcdir(reader) + + self.assertEqual(len(objs), 4) + ldflags, host_cflags, cflags, prog = objs + self.assertIsInstance(ldflags, ComputedFlags) + self.assertIsInstance(cflags, ComputedFlags) + self.assertIsInstance(host_cflags, ComputedFlags) + self.assertIsInstance(prog, RustProgram) + self.assertEqual(prog.name, "some") + + def test_host_rust_programs(self): + """Test HOST_RUST_PROGRAMS emission.""" + reader = self.reader( + "host-rust-programs", + extra_substs=dict( + RUST_HOST_TARGET="i686-pc-windows-msvc", HOST_BIN_SUFFIX=".exe" + ), + ) + objs = self.read_topsrcdir(reader) + + self.assertEqual(len(objs), 4) + print(objs) + ldflags, cflags, hostflags, prog = objs + self.assertIsInstance(ldflags, ComputedFlags) + self.assertIsInstance(cflags, ComputedFlags) + self.assertIsInstance(hostflags, ComputedFlags) + self.assertIsInstance(prog, HostRustProgram) + self.assertEqual(prog.name, "some") + + def test_host_rust_libraries(self): + """Test HOST_RUST_LIBRARIES emission.""" + reader = self.reader( + "host-rust-libraries", + extra_substs=dict( + RUST_HOST_TARGET="i686-pc-windows-msvc", HOST_BIN_SUFFIX=".exe" + ), + ) + objs = self.read_topsrcdir(reader) + + self.assertEqual(len(objs), 4) + ldflags, host_cflags, lib, cflags = objs + self.assertIsInstance(ldflags, ComputedFlags) + self.assertIsInstance(cflags, ComputedFlags) + self.assertIsInstance(host_cflags, ComputedFlags) + self.assertIsInstance(lib, HostRustLibrary) + self.assertRegex(lib.lib_name, "host_lib") + self.assertRegex(lib.import_name, "host_lib") + + def test_crate_dependency_path_resolution(self): + """Test recursive dependencies resolve with the correct paths.""" + reader = self.reader( + "crate-dependency-path-resolution", + extra_substs=dict(RUST_TARGET="i686-pc-windows-msvc"), + ) + objs = self.read_topsrcdir(reader) + + self.assertEqual(len(objs), 4) + ldflags, host_cflags, lib, cflags = objs + self.assertIsInstance(ldflags, ComputedFlags) + self.assertIsInstance(cflags, ComputedFlags) + self.assertIsInstance(host_cflags, ComputedFlags) + self.assertIsInstance(lib, RustLibrary) + + def test_missing_workspace_hack(self): + """Test detection of a missing workspace hack.""" + reader = self.reader("rust-no-workspace-hack") + with six.assertRaisesRegex( + self, SandboxValidationError, "doesn't contain the workspace hack" + ): + self.read_topsrcdir(reader) + + def test_old_workspace_hack(self): + """Test detection of an old workspace hack.""" + reader = self.reader("rust-old-workspace-hack") + with six.assertRaisesRegex( + self, + SandboxValidationError, + "needs an update to its mozilla-central-workspace-hack dependency", + ): + self.read_topsrcdir(reader) + + def test_install_shared_lib(self): + """Test that we can install a shared library with TEST_HARNESS_FILES""" + reader = self.reader("test-install-shared-lib") + objs = self.read_topsrcdir(reader) + self.assertIsInstance(objs[0], TestHarnessFiles) + self.assertIsInstance(objs[1], VariablePassthru) + self.assertIsInstance(objs[2], ComputedFlags) + self.assertIsInstance(objs[3], SharedLibrary) + self.assertIsInstance(objs[4], ComputedFlags) + for path, files in objs[0].files.walk(): + for f in files: + self.assertEqual(str(f), "!libfoo.so") + self.assertEqual(path, "foo/bar") + + def test_symbols_file(self): + """Test that SYMBOLS_FILE works""" + reader = self.reader("test-symbols-file") + genfile, ldflags, shlib, flags = self.read_topsrcdir(reader) + self.assertIsInstance(genfile, GeneratedFile) + self.assertIsInstance(flags, ComputedFlags) + self.assertIsInstance(ldflags, ComputedFlags) + self.assertIsInstance(shlib, SharedLibrary) + # This looks weird but MockConfig sets DLL_{PREFIX,SUFFIX} and + # the reader method in this class sets OS_TARGET=WINNT. + self.assertEqual(shlib.symbols_file, "libfoo.so.def") + + def test_symbols_file_objdir(self): + """Test that a SYMBOLS_FILE in the objdir works""" + reader = self.reader("test-symbols-file-objdir") + genfile, ldflags, shlib, flags = self.read_topsrcdir(reader) + self.assertIsInstance(genfile, GeneratedFile) + self.assertEqual( + genfile.script, mozpath.join(reader.config.topsrcdir, "foo.py") + ) + self.assertIsInstance(flags, ComputedFlags) + self.assertIsInstance(ldflags, ComputedFlags) + self.assertIsInstance(shlib, SharedLibrary) + self.assertEqual(shlib.symbols_file, "foo.symbols") + + def test_symbols_file_objdir_missing_generated(self): + """Test that a SYMBOLS_FILE in the objdir that's missing + from GENERATED_FILES is an error. + """ + reader = self.reader("test-symbols-file-objdir-missing-generated") + with six.assertRaisesRegex( + self, + SandboxValidationError, + "Objdir file specified in SYMBOLS_FILE not in GENERATED_FILES:", + ): + self.read_topsrcdir(reader) + + def test_wasm_compile_flags(self): + reader = self.reader( + "wasm-compile-flags", + extra_substs={"WASM_CC": "clang", "WASM_CXX": "clang++"}, + ) + flags = list(self.read_topsrcdir(reader))[2] + self.assertIsInstance(flags, ComputedFlags) + self.assertEqual( + flags.flags["WASM_CFLAGS"], reader.config.substs["WASM_CFLAGS"] + ) + self.assertEqual( + flags.flags["MOZBUILD_WASM_CFLAGS"], ["-funroll-loops", "-wasm-arg"] + ) + self.assertEqual( + set(flags.flags["WASM_DEFINES"]), + set(["-DFOO", '-DBAZ="abcd"', "-UQUX", "-DBAR=7", "-DVALUE=xyz"]), + ) + + +if __name__ == "__main__": + main() diff --git a/python/mozbuild/mozbuild/test/frontend/test_namespaces.py b/python/mozbuild/mozbuild/test/frontend/test_namespaces.py new file mode 100644 index 0000000000..e8c1a3eb00 --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/test_namespaces.py @@ -0,0 +1,225 @@ +# 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/. + +import unittest + +import six +from mozunit import main + +from mozbuild.frontend.context import ( + Context, + ContextDerivedTypedList, + ContextDerivedTypedListWithItems, + ContextDerivedValue, +) +from mozbuild.util import ( + StrictOrderingOnAppendList, + StrictOrderingOnAppendListWithFlagsFactory, + UnsortedError, +) + + +class Fuga(object): + def __init__(self, value): + self.value = value + + +class Piyo(ContextDerivedValue): + def __init__(self, context, value): + if not isinstance(value, six.text_type): + raise ValueError + self.context = context + self.value = value + + def lower(self): + return self.value.lower() + + def __str__(self): + return self.value + + def __eq__(self, other): + return self.value == six.text_type(other) + + def __lt__(self, other): + return self.value < six.text_type(other) + + def __le__(self, other): + return self.value <= six.text_type(other) + + def __gt__(self, other): + return self.value > six.text_type(other) + + def __ge__(self, other): + return self.value >= six.text_type(other) + + def __hash__(self): + return hash(self.value) + + +VARIABLES = { + "HOGE": (six.text_type, six.text_type, None), + "FUGA": (Fuga, six.text_type, None), + "PIYO": (Piyo, six.text_type, None), + "HOGERA": (ContextDerivedTypedList(Piyo, StrictOrderingOnAppendList), list, None), + "HOGEHOGE": ( + ContextDerivedTypedListWithItems( + Piyo, + StrictOrderingOnAppendListWithFlagsFactory( + { + "foo": bool, + } + ), + ), + list, + None, + ), +} + + +class TestContext(unittest.TestCase): + def test_key_rejection(self): + # Lowercase keys should be rejected during normal operation. + ns = Context(allowed_variables=VARIABLES) + + with self.assertRaises(KeyError) as ke: + ns["foo"] = True + + e = ke.exception.args + self.assertEqual(e[0], "global_ns") + self.assertEqual(e[1], "set_unknown") + self.assertEqual(e[2], "foo") + self.assertTrue(e[3]) + + # Unknown uppercase keys should be rejected. + with self.assertRaises(KeyError) as ke: + ns["FOO"] = True + + e = ke.exception.args + self.assertEqual(e[0], "global_ns") + self.assertEqual(e[1], "set_unknown") + self.assertEqual(e[2], "FOO") + self.assertTrue(e[3]) + + def test_allowed_set(self): + self.assertIn("HOGE", VARIABLES) + + ns = Context(allowed_variables=VARIABLES) + + ns["HOGE"] = "foo" + self.assertEqual(ns["HOGE"], "foo") + + def test_value_checking(self): + ns = Context(allowed_variables=VARIABLES) + + # Setting to a non-allowed type should not work. + with self.assertRaises(ValueError) as ve: + ns["HOGE"] = True + + e = ve.exception.args + self.assertEqual(e[0], "global_ns") + self.assertEqual(e[1], "set_type") + self.assertEqual(e[2], "HOGE") + self.assertEqual(e[3], True) + self.assertEqual(e[4], six.text_type) + + def test_key_checking(self): + # Checking for existence of a key should not populate the key if it + # doesn't exist. + g = Context(allowed_variables=VARIABLES) + + self.assertFalse("HOGE" in g) + self.assertFalse("HOGE" in g) + + def test_coercion(self): + ns = Context(allowed_variables=VARIABLES) + + # Setting to a type different from the allowed input type should not + # work. + with self.assertRaises(ValueError) as ve: + ns["FUGA"] = False + + e = ve.exception.args + self.assertEqual(e[0], "global_ns") + self.assertEqual(e[1], "set_type") + self.assertEqual(e[2], "FUGA") + self.assertEqual(e[3], False) + self.assertEqual(e[4], six.text_type) + + ns["FUGA"] = "fuga" + self.assertIsInstance(ns["FUGA"], Fuga) + self.assertEqual(ns["FUGA"].value, "fuga") + + ns["FUGA"] = Fuga("hoge") + self.assertIsInstance(ns["FUGA"], Fuga) + self.assertEqual(ns["FUGA"].value, "hoge") + + def test_context_derived_coercion(self): + ns = Context(allowed_variables=VARIABLES) + + # Setting to a type different from the allowed input type should not + # work. + with self.assertRaises(ValueError) as ve: + ns["PIYO"] = False + + e = ve.exception.args + self.assertEqual(e[0], "global_ns") + self.assertEqual(e[1], "set_type") + self.assertEqual(e[2], "PIYO") + self.assertEqual(e[3], False) + self.assertEqual(e[4], six.text_type) + + ns["PIYO"] = "piyo" + self.assertIsInstance(ns["PIYO"], Piyo) + self.assertEqual(ns["PIYO"].value, "piyo") + self.assertEqual(ns["PIYO"].context, ns) + + ns["PIYO"] = Piyo(ns, "fuga") + self.assertIsInstance(ns["PIYO"], Piyo) + self.assertEqual(ns["PIYO"].value, "fuga") + self.assertEqual(ns["PIYO"].context, ns) + + def test_context_derived_typed_list(self): + ns = Context(allowed_variables=VARIABLES) + + # Setting to a type that's rejected by coercion should not work. + with self.assertRaises(ValueError): + ns["HOGERA"] = [False] + + ns["HOGERA"] += ["a", "b", "c"] + + self.assertIsInstance(ns["HOGERA"], VARIABLES["HOGERA"][0]) + for n in range(0, 3): + self.assertIsInstance(ns["HOGERA"][n], Piyo) + self.assertEqual(ns["HOGERA"][n].value, ["a", "b", "c"][n]) + self.assertEqual(ns["HOGERA"][n].context, ns) + + with self.assertRaises(UnsortedError): + ns["HOGERA"] += ["f", "e", "d"] + + def test_context_derived_typed_list_with_items(self): + ns = Context(allowed_variables=VARIABLES) + + # Setting to a type that's rejected by coercion should not work. + with self.assertRaises(ValueError): + ns["HOGEHOGE"] = [False] + + values = ["a", "b", "c"] + ns["HOGEHOGE"] += values + + self.assertIsInstance(ns["HOGEHOGE"], VARIABLES["HOGEHOGE"][0]) + for v in values: + ns["HOGEHOGE"][v].foo = True + + for v, item in zip(values, ns["HOGEHOGE"]): + self.assertIsInstance(item, Piyo) + self.assertEqual(v, item) + self.assertEqual(ns["HOGEHOGE"][v].foo, True) + self.assertEqual(ns["HOGEHOGE"][item].foo, True) + + with self.assertRaises(UnsortedError): + ns["HOGEHOGE"] += ["f", "e", "d"] + + +if __name__ == "__main__": + main() diff --git a/python/mozbuild/mozbuild/test/frontend/test_reader.py b/python/mozbuild/mozbuild/test/frontend/test_reader.py new file mode 100644 index 0000000000..a15bb15d7e --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/test_reader.py @@ -0,0 +1,531 @@ +# 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/. + +import os +import sys +import unittest + +import mozpack.path as mozpath +from mozunit import main + +from mozbuild import schedules +from mozbuild.frontend.context import BugzillaComponent +from mozbuild.frontend.reader import BuildReader, BuildReaderError +from mozbuild.test.common import MockConfig + +if sys.version_info.major == 2: + text_type = "unicode" +else: + text_type = "str" + +data_path = mozpath.abspath(mozpath.dirname(__file__)) +data_path = mozpath.join(data_path, "data") + + +class TestBuildReader(unittest.TestCase): + def setUp(self): + self._old_env = dict(os.environ) + os.environ.pop("MOZ_OBJDIR", None) + + def tearDown(self): + os.environ.clear() + os.environ.update(self._old_env) + + def config(self, name, **kwargs): + path = mozpath.join(data_path, name) + + return MockConfig(path, **kwargs) + + def reader(self, name, enable_tests=False, error_is_fatal=True, **kwargs): + extra = {} + if enable_tests: + extra["ENABLE_TESTS"] = "1" + config = self.config(name, extra_substs=extra, error_is_fatal=error_is_fatal) + + return BuildReader(config, **kwargs) + + def file_path(self, name, *args): + return mozpath.join(data_path, name, *args) + + def test_dirs_traversal_simple(self): + reader = self.reader("traversal-simple") + + contexts = list(reader.read_topsrcdir()) + + self.assertEqual(len(contexts), 4) + + def test_dirs_traversal_no_descend(self): + reader = self.reader("traversal-simple") + + path = mozpath.join(reader.config.topsrcdir, "moz.build") + self.assertTrue(os.path.exists(path)) + + contexts = list(reader.read_mozbuild(path, reader.config, descend=False)) + + self.assertEqual(len(contexts), 1) + + def test_dirs_traversal_all_variables(self): + reader = self.reader("traversal-all-vars") + + contexts = list(reader.read_topsrcdir()) + self.assertEqual(len(contexts), 2) + + reader = self.reader("traversal-all-vars", enable_tests=True) + + contexts = list(reader.read_topsrcdir()) + self.assertEqual(len(contexts), 3) + + def test_relative_dirs(self): + # Ensure relative directories are traversed. + reader = self.reader("traversal-relative-dirs") + + contexts = list(reader.read_topsrcdir()) + self.assertEqual(len(contexts), 3) + + def test_repeated_dirs_ignored(self): + # Ensure repeated directories are ignored. + reader = self.reader("traversal-repeated-dirs") + + contexts = list(reader.read_topsrcdir()) + self.assertEqual(len(contexts), 3) + + def test_outside_topsrcdir(self): + # References to directories outside the topsrcdir should fail. + reader = self.reader("traversal-outside-topsrcdir") + + with self.assertRaises(Exception): + list(reader.read_topsrcdir()) + + def test_error_basic(self): + reader = self.reader("reader-error-basic") + + with self.assertRaises(BuildReaderError) as bre: + list(reader.read_topsrcdir()) + + e = bre.exception + self.assertEqual( + e.actual_file, self.file_path("reader-error-basic", "moz.build") + ) + + self.assertIn("The error occurred while processing the", str(e)) + + def test_error_included_from(self): + reader = self.reader("reader-error-included-from") + + with self.assertRaises(BuildReaderError) as bre: + list(reader.read_topsrcdir()) + + e = bre.exception + self.assertEqual( + e.actual_file, self.file_path("reader-error-included-from", "child.build") + ) + self.assertEqual( + e.main_file, self.file_path("reader-error-included-from", "moz.build") + ) + + self.assertIn("This file was included as part of processing", str(e)) + + def test_error_syntax_error(self): + reader = self.reader("reader-error-syntax") + + with self.assertRaises(BuildReaderError) as bre: + list(reader.read_topsrcdir()) + + e = bre.exception + self.assertIn("Python syntax error on line 5", str(e)) + self.assertIn(" foo =", str(e)) + self.assertIn(" ^", str(e)) + + def test_error_read_unknown_global(self): + reader = self.reader("reader-error-read-unknown-global") + + with self.assertRaises(BuildReaderError) as bre: + list(reader.read_topsrcdir()) + + e = bre.exception + self.assertIn("The error was triggered on line 5", str(e)) + self.assertIn("The underlying problem is an attempt to read", str(e)) + self.assertIn(" FOO", str(e)) + + def test_error_write_unknown_global(self): + reader = self.reader("reader-error-write-unknown-global") + + with self.assertRaises(BuildReaderError) as bre: + list(reader.read_topsrcdir()) + + e = bre.exception + self.assertIn("The error was triggered on line 7", str(e)) + self.assertIn("The underlying problem is an attempt to write", str(e)) + self.assertIn(" FOO", str(e)) + + def test_error_write_bad_value(self): + reader = self.reader("reader-error-write-bad-value") + + with self.assertRaises(BuildReaderError) as bre: + list(reader.read_topsrcdir()) + + e = bre.exception + self.assertIn("The error was triggered on line 5", str(e)) + self.assertIn("is an attempt to write an illegal value to a special", str(e)) + + self.assertIn("variable whose value was rejected is:\n\n DIRS", str(e)) + + self.assertIn( + "written to it was of the following type:\n\n %s" % text_type, str(e) + ) + + self.assertIn("expects the following type(s):\n\n list", str(e)) + + def test_error_illegal_path(self): + reader = self.reader("reader-error-outside-topsrcdir") + + with self.assertRaises(BuildReaderError) as bre: + list(reader.read_topsrcdir()) + + e = bre.exception + self.assertIn("The underlying problem is an illegal file access", str(e)) + + def test_error_missing_include_path(self): + reader = self.reader("reader-error-missing-include") + + with self.assertRaises(BuildReaderError) as bre: + list(reader.read_topsrcdir()) + + e = bre.exception + self.assertIn("we referenced a path that does not exist", str(e)) + + def test_error_script_error(self): + reader = self.reader("reader-error-script-error") + + with self.assertRaises(BuildReaderError) as bre: + list(reader.read_topsrcdir()) + + e = bre.exception + self.assertIn("The error appears to be the fault of the script", str(e)) + self.assertIn(' ["TypeError: unsupported operand', str(e)) + + def test_error_bad_dir(self): + reader = self.reader("reader-error-bad-dir") + + with self.assertRaises(BuildReaderError) as bre: + list(reader.read_topsrcdir()) + + e = bre.exception + self.assertIn("we referenced a path that does not exist", str(e)) + + def test_error_repeated_dir(self): + reader = self.reader("reader-error-repeated-dir") + + with self.assertRaises(BuildReaderError) as bre: + list(reader.read_topsrcdir()) + + e = bre.exception + self.assertIn("Directory (foo) registered multiple times", str(e)) + + def test_error_error_func(self): + reader = self.reader("reader-error-error-func") + + with self.assertRaises(BuildReaderError) as bre: + list(reader.read_topsrcdir()) + + e = bre.exception + self.assertIn("A moz.build file called the error() function.", str(e)) + self.assertIn(" Some error.", str(e)) + + def test_error_error_func_ok(self): + reader = self.reader("reader-error-error-func", error_is_fatal=False) + + list(reader.read_topsrcdir()) + + def test_error_empty_list(self): + reader = self.reader("reader-error-empty-list") + + with self.assertRaises(BuildReaderError) as bre: + list(reader.read_topsrcdir()) + + e = bre.exception + self.assertIn("Variable DIRS assigned an empty value.", str(e)) + + def test_inheriting_variables(self): + reader = self.reader("inheriting-variables") + + contexts = list(reader.read_topsrcdir()) + + self.assertEqual(len(contexts), 4) + self.assertEqual( + [context.relsrcdir for context in contexts], ["", "foo", "foo/baz", "bar"] + ) + self.assertEqual( + [context["XPIDL_MODULE"] for context in contexts], + ["foobar", "foobar", "baz", "foobar"], + ) + + def test_find_relevant_mozbuilds(self): + reader = self.reader("reader-relevant-mozbuild") + + # Absolute paths outside topsrcdir are rejected. + with self.assertRaises(Exception): + reader._find_relevant_mozbuilds(["/foo"]) + + # File in root directory. + paths = reader._find_relevant_mozbuilds(["file"]) + self.assertEqual(paths, {"file": ["moz.build"]}) + + # File in child directory. + paths = reader._find_relevant_mozbuilds(["d1/file1"]) + self.assertEqual(paths, {"d1/file1": ["moz.build", "d1/moz.build"]}) + + # Multiple files in same directory. + paths = reader._find_relevant_mozbuilds(["d1/file1", "d1/file2"]) + self.assertEqual( + paths, + { + "d1/file1": ["moz.build", "d1/moz.build"], + "d1/file2": ["moz.build", "d1/moz.build"], + }, + ) + + # Missing moz.build from missing intermediate directory. + paths = reader._find_relevant_mozbuilds( + ["d1/no-intermediate-moz-build/child/file"] + ) + self.assertEqual( + paths, + { + "d1/no-intermediate-moz-build/child/file": [ + "moz.build", + "d1/moz.build", + "d1/no-intermediate-moz-build/child/moz.build", + ] + }, + ) + + # Lots of empty directories. + paths = reader._find_relevant_mozbuilds( + ["d1/parent-is-far/dir1/dir2/dir3/file"] + ) + self.assertEqual( + paths, + { + "d1/parent-is-far/dir1/dir2/dir3/file": [ + "moz.build", + "d1/moz.build", + "d1/parent-is-far/moz.build", + ] + }, + ) + + # Lots of levels. + paths = reader._find_relevant_mozbuilds( + ["d1/every-level/a/file", "d1/every-level/b/file"] + ) + self.assertEqual( + paths, + { + "d1/every-level/a/file": [ + "moz.build", + "d1/moz.build", + "d1/every-level/moz.build", + "d1/every-level/a/moz.build", + ], + "d1/every-level/b/file": [ + "moz.build", + "d1/moz.build", + "d1/every-level/moz.build", + "d1/every-level/b/moz.build", + ], + }, + ) + + # Different root directories. + paths = reader._find_relevant_mozbuilds(["d1/file", "d2/file", "file"]) + self.assertEqual( + paths, + { + "file": ["moz.build"], + "d1/file": ["moz.build", "d1/moz.build"], + "d2/file": ["moz.build", "d2/moz.build"], + }, + ) + + def test_read_relevant_mozbuilds(self): + reader = self.reader("reader-relevant-mozbuild") + + paths, contexts = reader.read_relevant_mozbuilds( + ["d1/every-level/a/file", "d1/every-level/b/file", "d2/file"] + ) + self.assertEqual(len(paths), 3) + self.assertEqual(len(contexts), 6) + + self.assertEqual( + [ctx.relsrcdir for ctx in paths["d1/every-level/a/file"]], + ["", "d1", "d1/every-level", "d1/every-level/a"], + ) + self.assertEqual( + [ctx.relsrcdir for ctx in paths["d1/every-level/b/file"]], + ["", "d1", "d1/every-level", "d1/every-level/b"], + ) + self.assertEqual([ctx.relsrcdir for ctx in paths["d2/file"]], ["", "d2"]) + + def test_all_mozbuild_paths(self): + reader = self.reader("reader-relevant-mozbuild") + + paths = list(reader.all_mozbuild_paths()) + # Ensure no duplicate paths. + self.assertEqual(sorted(paths), sorted(set(paths))) + self.assertEqual(len(paths), 10) + + def test_files_bad_bug_component(self): + reader = self.reader("files-info") + + with self.assertRaises(BuildReaderError): + reader.files_info(["bug_component/bad-assignment/moz.build"]) + + def test_files_bug_component_static(self): + reader = self.reader("files-info") + + v = reader.files_info( + [ + "bug_component/static/foo", + "bug_component/static/bar", + "bug_component/static/foo/baz", + ] + ) + self.assertEqual(len(v), 3) + self.assertEqual( + v["bug_component/static/foo"]["BUG_COMPONENT"], + BugzillaComponent("FooProduct", "FooComponent"), + ) + self.assertEqual( + v["bug_component/static/bar"]["BUG_COMPONENT"], + BugzillaComponent("BarProduct", "BarComponent"), + ) + self.assertEqual( + v["bug_component/static/foo/baz"]["BUG_COMPONENT"], + BugzillaComponent("default_product", "default_component"), + ) + + def test_files_bug_component_simple(self): + reader = self.reader("files-info") + + v = reader.files_info(["bug_component/simple/moz.build"]) + self.assertEqual(len(v), 1) + flags = v["bug_component/simple/moz.build"] + self.assertEqual(flags["BUG_COMPONENT"].product, "Firefox Build System") + self.assertEqual(flags["BUG_COMPONENT"].component, "General") + + def test_files_bug_component_different_matchers(self): + reader = self.reader("files-info") + + v = reader.files_info( + [ + "bug_component/different-matchers/foo.jsm", + "bug_component/different-matchers/bar.cpp", + "bug_component/different-matchers/baz.misc", + ] + ) + self.assertEqual(len(v), 3) + + js_flags = v["bug_component/different-matchers/foo.jsm"] + cpp_flags = v["bug_component/different-matchers/bar.cpp"] + misc_flags = v["bug_component/different-matchers/baz.misc"] + + self.assertEqual(js_flags["BUG_COMPONENT"], BugzillaComponent("Firefox", "JS")) + self.assertEqual( + cpp_flags["BUG_COMPONENT"], BugzillaComponent("Firefox", "C++") + ) + self.assertEqual( + misc_flags["BUG_COMPONENT"], + BugzillaComponent("default_product", "default_component"), + ) + + def test_files_bug_component_final(self): + reader = self.reader("files-info") + + v = reader.files_info( + [ + "bug_component/final/foo", + "bug_component/final/Makefile.in", + "bug_component/final/subcomponent/Makefile.in", + "bug_component/final/subcomponent/bar", + ] + ) + + self.assertEqual( + v["bug_component/final/foo"]["BUG_COMPONENT"], + BugzillaComponent("default_product", "default_component"), + ) + self.assertEqual( + v["bug_component/final/Makefile.in"]["BUG_COMPONENT"], + BugzillaComponent("Firefox Build System", "General"), + ) + self.assertEqual( + v["bug_component/final/subcomponent/Makefile.in"]["BUG_COMPONENT"], + BugzillaComponent("Firefox Build System", "General"), + ) + self.assertEqual( + v["bug_component/final/subcomponent/bar"]["BUG_COMPONENT"], + BugzillaComponent("Another", "Component"), + ) + + def test_invalid_flavor(self): + reader = self.reader("invalid-files-flavor") + + with self.assertRaises(BuildReaderError): + reader.files_info(["foo.js"]) + + def test_schedules(self): + reader = self.reader("schedules") + info = reader.files_info( + [ + "win.and.osx", + "somefile", + "foo.win", + "foo.osx", + "subd/aa.py", + "subd/yaml.py", + "subd/win.js", + ] + ) + # default: all exclusive, no inclusive + self.assertEqual(info["somefile"]["SCHEDULES"].inclusive, []) + self.assertEqual( + info["somefile"]["SCHEDULES"].exclusive, schedules.EXCLUSIVE_COMPONENTS + ) + # windows-only + self.assertEqual(info["foo.win"]["SCHEDULES"].inclusive, []) + self.assertEqual(info["foo.win"]["SCHEDULES"].exclusive, ["windows"]) + # osx-only + self.assertEqual(info["foo.osx"]["SCHEDULES"].inclusive, []) + self.assertEqual(info["foo.osx"]["SCHEDULES"].exclusive, ["macosx"]) + # top-level moz.build specifies subd/**.py with an inclusive option + self.assertEqual(info["subd/aa.py"]["SCHEDULES"].inclusive, ["py-lint"]) + self.assertEqual( + info["subd/aa.py"]["SCHEDULES"].exclusive, schedules.EXCLUSIVE_COMPONENTS + ) + # Files('yaml.py') in subd/moz.build combines with Files('subdir/**.py') + self.assertEqual( + info["subd/yaml.py"]["SCHEDULES"].inclusive, ["py-lint", "yaml-lint"] + ) + self.assertEqual( + info["subd/yaml.py"]["SCHEDULES"].exclusive, schedules.EXCLUSIVE_COMPONENTS + ) + # .. but exlusive does not override inclusive + self.assertEqual(info["subd/win.js"]["SCHEDULES"].inclusive, ["js-lint"]) + self.assertEqual(info["subd/win.js"]["SCHEDULES"].exclusive, ["windows"]) + + self.assertEqual( + set(info["subd/yaml.py"]["SCHEDULES"].components), + set(schedules.EXCLUSIVE_COMPONENTS + ["py-lint", "yaml-lint"]), + ) + + # win.and.osx is defined explicitly, and matches *.osx, and the two have + # conflicting SCHEDULES.exclusive settings, so the later one is used + self.assertEqual( + set(info["win.and.osx"]["SCHEDULES"].exclusive), set(["macosx", "windows"]) + ) + + +if __name__ == "__main__": + main() diff --git a/python/mozbuild/mozbuild/test/frontend/test_sandbox.py b/python/mozbuild/mozbuild/test/frontend/test_sandbox.py new file mode 100644 index 0000000000..017de1ce9c --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/test_sandbox.py @@ -0,0 +1,536 @@ +# 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/. + +import unittest + +import mozpack.path as mozpath +from mozunit import main + +from mozbuild.frontend.context import ( + FUNCTIONS, + SPECIAL_VARIABLES, + VARIABLES, + Context, + SourcePath, +) +from mozbuild.frontend.reader import MozbuildSandbox, SandboxCalledError +from mozbuild.frontend.sandbox import Sandbox, SandboxExecutionError, SandboxLoadError +from mozbuild.test.common import MockConfig + +test_data_path = mozpath.abspath(mozpath.dirname(__file__)) +test_data_path = mozpath.join(test_data_path, "data") + + +class TestSandbox(unittest.TestCase): + def sandbox(self): + return Sandbox( + Context( + { + "DIRS": (list, list, None), + } + ) + ) + + def test_exec_source_success(self): + sandbox = self.sandbox() + context = sandbox._context + + sandbox.exec_source("foo = True", mozpath.abspath("foo.py")) + + self.assertNotIn("foo", context) + self.assertEqual(context.main_path, mozpath.abspath("foo.py")) + self.assertEqual(context.all_paths, set([mozpath.abspath("foo.py")])) + + def test_exec_compile_error(self): + sandbox = self.sandbox() + + with self.assertRaises(SandboxExecutionError) as se: + sandbox.exec_source("2f23;k;asfj", mozpath.abspath("foo.py")) + + self.assertEqual(se.exception.file_stack, [mozpath.abspath("foo.py")]) + self.assertIsInstance(se.exception.exc_value, SyntaxError) + self.assertEqual(sandbox._context.main_path, mozpath.abspath("foo.py")) + + def test_exec_import_denied(self): + sandbox = self.sandbox() + + with self.assertRaises(SandboxExecutionError) as se: + sandbox.exec_source("import sys") + + self.assertIsInstance(se.exception, SandboxExecutionError) + self.assertEqual(se.exception.exc_type, ImportError) + + def test_exec_source_multiple(self): + sandbox = self.sandbox() + + sandbox.exec_source('DIRS = ["foo"]') + sandbox.exec_source('DIRS += ["bar"]') + + self.assertEqual(sandbox["DIRS"], ["foo", "bar"]) + + def test_exec_source_illegal_key_set(self): + sandbox = self.sandbox() + + with self.assertRaises(SandboxExecutionError) as se: + sandbox.exec_source("ILLEGAL = True") + + e = se.exception + self.assertIsInstance(e.exc_value, KeyError) + + e = se.exception.exc_value + self.assertEqual(e.args[0], "global_ns") + self.assertEqual(e.args[1], "set_unknown") + + def test_exec_source_reassign(self): + sandbox = self.sandbox() + + sandbox.exec_source('DIRS = ["foo"]') + with self.assertRaises(SandboxExecutionError) as se: + sandbox.exec_source('DIRS = ["bar"]') + + self.assertEqual(sandbox["DIRS"], ["foo"]) + e = se.exception + self.assertIsInstance(e.exc_value, KeyError) + + e = se.exception.exc_value + self.assertEqual(e.args[0], "global_ns") + self.assertEqual(e.args[1], "reassign") + self.assertEqual(e.args[2], "DIRS") + + def test_exec_source_reassign_builtin(self): + sandbox = self.sandbox() + + with self.assertRaises(SandboxExecutionError) as se: + sandbox.exec_source("sorted = 1") + + e = se.exception + self.assertIsInstance(e.exc_value, KeyError) + + e = se.exception.exc_value + self.assertEqual(e.args[0], "Cannot reassign builtins") + + +class TestedSandbox(MozbuildSandbox): + """Version of MozbuildSandbox with a little more convenience for testing. + + It automatically normalizes paths given to exec_file and exec_source. This + helps simplify the test code. + """ + + def normalize_path(self, path): + return mozpath.normpath(mozpath.join(self._context.config.topsrcdir, path)) + + def source_path(self, path): + return SourcePath(self._context, path) + + def exec_file(self, path): + super(TestedSandbox, self).exec_file(self.normalize_path(path)) + + def exec_source(self, source, path=""): + super(TestedSandbox, self).exec_source( + source, self.normalize_path(path) if path else "" + ) + + +class TestMozbuildSandbox(unittest.TestCase): + def sandbox(self, data_path=None, metadata={}): + config = None + + if data_path is not None: + config = MockConfig(mozpath.join(test_data_path, data_path)) + else: + config = MockConfig() + + return TestedSandbox(Context(VARIABLES, config), metadata) + + def test_default_state(self): + sandbox = self.sandbox() + sandbox._context.add_source(sandbox.normalize_path("moz.build")) + config = sandbox._context.config + + self.assertEqual(sandbox["TOPSRCDIR"], config.topsrcdir) + self.assertEqual(sandbox["TOPOBJDIR"], config.topobjdir) + self.assertEqual(sandbox["RELATIVEDIR"], "") + self.assertEqual(sandbox["SRCDIR"], config.topsrcdir) + self.assertEqual(sandbox["OBJDIR"], config.topobjdir) + + def test_symbol_presence(self): + # Ensure no discrepancies between the master symbol table and what's in + # the sandbox. + sandbox = self.sandbox() + sandbox._context.add_source(sandbox.normalize_path("moz.build")) + + all_symbols = set() + all_symbols |= set(FUNCTIONS.keys()) + all_symbols |= set(SPECIAL_VARIABLES.keys()) + + for symbol in all_symbols: + self.assertIsNotNone(sandbox[symbol]) + + def test_path_calculation(self): + sandbox = self.sandbox() + sandbox._context.add_source(sandbox.normalize_path("foo/bar/moz.build")) + config = sandbox._context.config + + self.assertEqual(sandbox["TOPSRCDIR"], config.topsrcdir) + self.assertEqual(sandbox["TOPOBJDIR"], config.topobjdir) + self.assertEqual(sandbox["RELATIVEDIR"], "foo/bar") + self.assertEqual(sandbox["SRCDIR"], mozpath.join(config.topsrcdir, "foo/bar")) + self.assertEqual(sandbox["OBJDIR"], mozpath.join(config.topobjdir, "foo/bar")) + + def test_config_access(self): + sandbox = self.sandbox() + config = sandbox._context.config + + self.assertEqual(sandbox["CONFIG"]["MOZ_TRUE"], "1") + self.assertEqual(sandbox["CONFIG"]["MOZ_FOO"], config.substs["MOZ_FOO"]) + + # Access to an undefined substitution should return None. + self.assertNotIn("MISSING", sandbox["CONFIG"]) + self.assertIsNone(sandbox["CONFIG"]["MISSING"]) + + # Should shouldn't be allowed to assign to the config. + with self.assertRaises(Exception): + sandbox["CONFIG"]["FOO"] = "" + + def test_special_variables(self): + sandbox = self.sandbox() + sandbox._context.add_source(sandbox.normalize_path("moz.build")) + + for k in SPECIAL_VARIABLES: + with self.assertRaises(KeyError): + sandbox[k] = 0 + + def test_exec_source_reassign_exported(self): + template_sandbox = self.sandbox(data_path="templates") + + # Templates need to be defined in actual files because of + # inspect.getsourcelines. + template_sandbox.exec_file("templates.mozbuild") + + config = MockConfig() + + exports = {"DIST_SUBDIR": "browser"} + + sandbox = TestedSandbox( + Context(VARIABLES, config), + metadata={ + "exports": exports, + "templates": template_sandbox.templates, + }, + ) + + self.assertEqual(sandbox["DIST_SUBDIR"], "browser") + + # Templates should not interfere + sandbox.exec_source("Template([])", "foo.mozbuild") + + sandbox.exec_source('DIST_SUBDIR = "foo"') + with self.assertRaises(SandboxExecutionError) as se: + sandbox.exec_source('DIST_SUBDIR = "bar"') + + self.assertEqual(sandbox["DIST_SUBDIR"], "foo") + e = se.exception + self.assertIsInstance(e.exc_value, KeyError) + + e = se.exception.exc_value + self.assertEqual(e.args[0], "global_ns") + self.assertEqual(e.args[1], "reassign") + self.assertEqual(e.args[2], "DIST_SUBDIR") + + def test_include_basic(self): + sandbox = self.sandbox(data_path="include-basic") + + sandbox.exec_file("moz.build") + + self.assertEqual( + sandbox["DIRS"], + [ + sandbox.source_path("foo"), + sandbox.source_path("bar"), + ], + ) + self.assertEqual( + sandbox._context.main_path, sandbox.normalize_path("moz.build") + ) + self.assertEqual(len(sandbox._context.all_paths), 2) + + def test_include_outside_topsrcdir(self): + sandbox = self.sandbox(data_path="include-outside-topsrcdir") + + with self.assertRaises(SandboxLoadError) as se: + sandbox.exec_file("relative.build") + + self.assertEqual( + se.exception.illegal_path, sandbox.normalize_path("../moz.build") + ) + + def test_include_error_stack(self): + # Ensure the path stack is reported properly in exceptions. + sandbox = self.sandbox(data_path="include-file-stack") + + with self.assertRaises(SandboxExecutionError) as se: + sandbox.exec_file("moz.build") + + e = se.exception + self.assertIsInstance(e.exc_value, KeyError) + + args = e.exc_value.args + self.assertEqual(args[0], "global_ns") + self.assertEqual(args[1], "set_unknown") + self.assertEqual(args[2], "ILLEGAL") + + expected_stack = [ + mozpath.join(sandbox._context.config.topsrcdir, p) + for p in ["moz.build", "included-1.build", "included-2.build"] + ] + + self.assertEqual(e.file_stack, expected_stack) + + def test_include_missing(self): + sandbox = self.sandbox(data_path="include-missing") + + with self.assertRaises(SandboxLoadError) as sle: + sandbox.exec_file("moz.build") + + self.assertIsNotNone(sle.exception.read_error) + + def test_include_relative_from_child_dir(self): + # A relative path from a subdirectory should be relative from that + # child directory. + sandbox = self.sandbox(data_path="include-relative-from-child") + sandbox.exec_file("child/child.build") + self.assertEqual(sandbox["DIRS"], [sandbox.source_path("../foo")]) + + sandbox = self.sandbox(data_path="include-relative-from-child") + sandbox.exec_file("child/child2.build") + self.assertEqual(sandbox["DIRS"], [sandbox.source_path("../foo")]) + + def test_include_topsrcdir_relative(self): + # An absolute path for include() is relative to topsrcdir. + + sandbox = self.sandbox(data_path="include-topsrcdir-relative") + sandbox.exec_file("moz.build") + + self.assertEqual(sandbox["DIRS"], [sandbox.source_path("foo")]) + + def test_error(self): + sandbox = self.sandbox() + + with self.assertRaises(SandboxCalledError) as sce: + sandbox.exec_source('error("This is an error.")') + + e = sce.exception.message + self.assertIn("This is an error.", str(e)) + + def test_substitute_config_files(self): + sandbox = self.sandbox() + sandbox._context.add_source(sandbox.normalize_path("moz.build")) + + sandbox.exec_source('CONFIGURE_SUBST_FILES += ["bar", "foo"]') + self.assertEqual(sandbox["CONFIGURE_SUBST_FILES"], ["bar", "foo"]) + for item in sandbox["CONFIGURE_SUBST_FILES"]: + self.assertIsInstance(item, SourcePath) + + def test_invalid_exports_set_base(self): + sandbox = self.sandbox() + + with self.assertRaises(SandboxExecutionError) as se: + sandbox.exec_source('EXPORTS = "foo.h"') + + self.assertEqual(se.exception.exc_type, ValueError) + + def test_templates(self): + sandbox = self.sandbox(data_path="templates") + + # Templates need to be defined in actual files because of + # inspect.getsourcelines. + sandbox.exec_file("templates.mozbuild") + + sandbox2 = self.sandbox(metadata={"templates": sandbox.templates}) + source = """ +Template([ + 'foo.cpp', +]) +""" + sandbox2.exec_source(source, "foo.mozbuild") + + self.assertEqual( + sandbox2._context, + { + "SOURCES": ["foo.cpp"], + "DIRS": [], + }, + ) + + sandbox2 = self.sandbox(metadata={"templates": sandbox.templates}) + source = """ +SOURCES += ['qux.cpp'] +Template([ + 'bar.cpp', + 'foo.cpp', +],[ + 'foo', +]) +SOURCES += ['hoge.cpp'] +""" + sandbox2.exec_source(source, "foo.mozbuild") + + self.assertEqual( + sandbox2._context, + { + "SOURCES": ["qux.cpp", "bar.cpp", "foo.cpp", "hoge.cpp"], + "DIRS": [sandbox2.source_path("foo")], + }, + ) + + sandbox2 = self.sandbox(metadata={"templates": sandbox.templates}) + source = """ +TemplateError([ + 'foo.cpp', +]) +""" + with self.assertRaises(SandboxExecutionError) as se: + sandbox2.exec_source(source, "foo.mozbuild") + + e = se.exception + self.assertIsInstance(e.exc_value, KeyError) + + e = se.exception.exc_value + self.assertEqual(e.args[0], "global_ns") + self.assertEqual(e.args[1], "set_unknown") + + # TemplateGlobalVariable tries to access 'illegal' but that is expected + # to throw. + sandbox2 = self.sandbox(metadata={"templates": sandbox.templates}) + source = """ +illegal = True +TemplateGlobalVariable() +""" + with self.assertRaises(SandboxExecutionError) as se: + sandbox2.exec_source(source, "foo.mozbuild") + + e = se.exception + self.assertIsInstance(e.exc_value, NameError) + + # TemplateGlobalUPPERVariable sets SOURCES with DIRS, but the context + # used when running the template is not expected to access variables + # from the global context. + sandbox2 = self.sandbox(metadata={"templates": sandbox.templates}) + source = """ +DIRS += ['foo'] +TemplateGlobalUPPERVariable() +""" + sandbox2.exec_source(source, "foo.mozbuild") + self.assertEqual( + sandbox2._context, + { + "SOURCES": [], + "DIRS": [sandbox2.source_path("foo")], + }, + ) + + # However, the result of the template is mixed with the global + # context. + sandbox2 = self.sandbox(metadata={"templates": sandbox.templates}) + source = """ +SOURCES += ['qux.cpp'] +TemplateInherit([ + 'bar.cpp', + 'foo.cpp', +]) +SOURCES += ['hoge.cpp'] +""" + sandbox2.exec_source(source, "foo.mozbuild") + + self.assertEqual( + sandbox2._context, + { + "SOURCES": ["qux.cpp", "bar.cpp", "foo.cpp", "hoge.cpp"], + "USE_LIBS": ["foo"], + "DIRS": [], + }, + ) + + # Template names must be CamelCase. Here, we can define the template + # inline because the error happens before inspect.getsourcelines. + sandbox2 = self.sandbox(metadata={"templates": sandbox.templates}) + source = """ +@template +def foo(): + pass +""" + + with self.assertRaises(SandboxExecutionError) as se: + sandbox2.exec_source(source, "foo.mozbuild") + + e = se.exception + self.assertIsInstance(e.exc_value, NameError) + + e = se.exception.exc_value + self.assertIn("Template function names must be CamelCase.", str(e)) + + # Template names must not already be registered. + sandbox2 = self.sandbox(metadata={"templates": sandbox.templates}) + source = """ +@template +def Template(): + pass +""" + with self.assertRaises(SandboxExecutionError) as se: + sandbox2.exec_source(source, "foo.mozbuild") + + e = se.exception + self.assertIsInstance(e.exc_value, KeyError) + + e = se.exception.exc_value + self.assertIn( + 'A template named "Template" was already declared in %s.' + % sandbox.normalize_path("templates.mozbuild"), + str(e), + ) + + def test_function_args(self): + class Foo(int): + pass + + def foo(a, b): + return type(a), type(b) + + FUNCTIONS.update( + { + "foo": (lambda self: foo, (Foo, int), ""), + } + ) + + try: + sandbox = self.sandbox() + source = 'foo("a", "b")' + + with self.assertRaises(SandboxExecutionError) as se: + sandbox.exec_source(source, "foo.mozbuild") + + e = se.exception + self.assertIsInstance(e.exc_value, ValueError) + + sandbox = self.sandbox() + source = 'foo(1, "b")' + + with self.assertRaises(SandboxExecutionError) as se: + sandbox.exec_source(source, "foo.mozbuild") + + e = se.exception + self.assertIsInstance(e.exc_value, ValueError) + + sandbox = self.sandbox() + source = "a = foo(1, 2)" + sandbox.exec_source(source, "foo.mozbuild") + + self.assertEqual(sandbox["a"], (Foo, int)) + finally: + del FUNCTIONS["foo"] + + +if __name__ == "__main__": + main() diff --git a/python/mozbuild/mozbuild/test/python.toml b/python/mozbuild/mozbuild/test/python.toml new file mode 100644 index 0000000000..e3471b47f0 --- /dev/null +++ b/python/mozbuild/mozbuild/test/python.toml @@ -0,0 +1,123 @@ +[DEFAULT] +subsuite = "mozbuild" + +["action/test_buildlist.py"] + +["action/test_html_fragment_preprocessor.py"] + +["action/test_langpack_manifest.py"] + +["action/test_node.py"] + +["action/test_process_install_manifest.py"] + +["backend/test_build.py"] + +["backend/test_configenvironment.py"] + +["backend/test_database.py"] + +["backend/test_fastermake.py"] + +["backend/test_partialconfigenvironment.py"] + +["backend/test_recursivemake.py"] + +["backend/test_test_manifest.py"] + +["backend/test_visualstudio.py"] + +["code_analysis/test_mach_commands.py"] + +["codecoverage/test_lcov_rewrite.py"] + +["compilation/test_warnings.py"] + +["configure/lint.py"] + +["configure/test_bootstrap.py"] + +["configure/test_checks_configure.py"] + +["configure/test_compile_checks.py"] + +["configure/test_configure.py"] + +["configure/test_lint.py"] + +["configure/test_moz_configure.py"] + +["configure/test_options.py"] + +["configure/test_toolchain_configure.py"] + +["configure/test_toolchain_helpers.py"] + +["configure/test_toolkit_moz_configure.py"] + +["configure/test_util.py"] + +["controller/test_ccachestats.py"] + +["controller/test_clobber.py"] + +["frontend/test_context.py"] + +["frontend/test_emitter.py"] + +["frontend/test_namespaces.py"] + +["frontend/test_reader.py"] + +["frontend/test_sandbox.py"] + +["repackaging/test_deb.py"] + +["test_artifact_cache.py"] + +["test_artifacts.py"] + +["test_base.py"] + +["test_containers.py"] + +["test_dotproperties.py"] + +["test_expression.py"] + +["test_jarmaker.py"] + +["test_licenses.py"] + +["test_line_endings.py"] + +["test_makeutil.py"] + +["test_manifest.py"] + +["test_mozconfig.py"] + +["test_mozinfo.py"] + +["test_preprocessor.py"] + +["test_pythonutil.py"] + +["test_rewrite_mozbuild.py"] + +["test_telemetry.py"] + +["test_telemetry_settings.py"] + +["test_util.py"] + +["test_util_fileavoidwrite.py"] + +["test_vendor.py"] +skip-if = ["true"] # Bug 1765416 +requirements = "python/mozbuild/mozbuild/test/vendor_requirements.txt" + +["test_vendor_tools.py"] +skip-if = ["os == 'win'"] # Windows doesn't have the same path seperator as linux, and we just don't need to run it there + +["vendor/test_vendor_manifest.py"] diff --git a/python/mozbuild/mozbuild/test/repackaging/test_deb.py b/python/mozbuild/mozbuild/test/repackaging/test_deb.py new file mode 100644 index 0000000000..d6ecbf62ab --- /dev/null +++ b/python/mozbuild/mozbuild/test/repackaging/test_deb.py @@ -0,0 +1,805 @@ +# 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/. + +import datetime +import json +import logging +import os +import tarfile +import tempfile +import zipfile +from contextlib import nullcontext as does_not_raise +from io import StringIO +from unittest.mock import MagicMock, Mock, call + +import mozpack.path as mozpath +import mozunit +import pytest + +from mozbuild.repackaging import deb + +_APPLICATION_INI_CONTENT = """[App] +Vendor=Mozilla +Name=Firefox +RemotingName=firefox-nightly-try +CodeName=Firefox Nightly +BuildID=20230222000000 +""" + +_APPLICATION_INI_CONTENT_DATA = { + "name": "Firefox", + "display_name": "Firefox Nightly", + "vendor": "Mozilla", + "remoting_name": "firefox-nightly-try", + "build_id": "20230222000000", +} + + +@pytest.mark.parametrize( + "number_of_application_ini_files, expectaction, expected_result", + ( + (0, pytest.raises(ValueError), None), + (1, does_not_raise(), _APPLICATION_INI_CONTENT_DATA), + (2, pytest.raises(ValueError), None), + ), +) +def test_extract_application_ini_data( + number_of_application_ini_files, expectaction, expected_result +): + with tempfile.TemporaryDirectory() as d: + tar_path = os.path.join(d, "input.tar") + with tarfile.open(tar_path, "w") as tar: + application_ini_path = os.path.join(d, "application.ini") + with open(application_ini_path, "w") as application_ini_file: + application_ini_file.write(_APPLICATION_INI_CONTENT) + + for i in range(number_of_application_ini_files): + tar.add(application_ini_path, f"{i}/application.ini") + + with expectaction: + assert deb._extract_application_ini_data(tar_path) == expected_result + + +def test_extract_application_ini_data_from_directory(): + with tempfile.TemporaryDirectory() as d: + with open(os.path.join(d, "application.ini"), "w") as f: + f.write(_APPLICATION_INI_CONTENT) + + assert ( + deb._extract_application_ini_data_from_directory(d) + == _APPLICATION_INI_CONTENT_DATA + ) + + +@pytest.mark.parametrize( + "version, build_number, package_name_suffix, description_suffix, release_product, application_ini_data, expected, raises", + ( + ( + "112.0a1", + 1, + "", + "", + "firefox", + { + "name": "Firefox", + "display_name": "Firefox", + "vendor": "Mozilla", + "remoting_name": "firefox-nightly-try", + "build_id": "20230222000000", + }, + { + "DEB_DESCRIPTION": "Mozilla Firefox", + "DEB_PKG_INSTALL_PATH": "usr/lib/firefox-nightly-try", + "DEB_PKG_NAME": "firefox-nightly-try", + "DEB_PKG_VERSION": "112.0a1~20230222000000", + }, + does_not_raise(), + ), + ( + "112.0a1", + 1, + "-l10n-fr", + " - Language pack for Firefox Nightly for fr", + "firefox", + { + "name": "Firefox", + "display_name": "Firefox", + "vendor": "Mozilla", + "remoting_name": "firefox-nightly-try", + "build_id": "20230222000000", + }, + { + "DEB_DESCRIPTION": "Mozilla Firefox - Language pack for Firefox Nightly for fr", + "DEB_PKG_INSTALL_PATH": "usr/lib/firefox-nightly-try", + "DEB_PKG_NAME": "firefox-nightly-try-l10n-fr", + "DEB_PKG_VERSION": "112.0a1~20230222000000", + }, + does_not_raise(), + ), + ( + "112.0b1", + 1, + "", + "", + "firefox", + { + "name": "Firefox", + "display_name": "Firefox", + "vendor": "Mozilla", + "remoting_name": "firefox-nightly-try", + "build_id": "20230222000000", + }, + { + "DEB_DESCRIPTION": "Mozilla Firefox", + "DEB_PKG_INSTALL_PATH": "usr/lib/firefox-nightly-try", + "DEB_PKG_NAME": "firefox-nightly-try", + "DEB_PKG_VERSION": "112.0b1~build1", + }, + does_not_raise(), + ), + ( + "112.0", + 2, + "", + "", + "firefox", + { + "name": "Firefox", + "display_name": "Firefox", + "vendor": "Mozilla", + "remoting_name": "firefox-nightly-try", + "build_id": "20230222000000", + }, + { + "DEB_DESCRIPTION": "Mozilla Firefox", + "DEB_PKG_INSTALL_PATH": "usr/lib/firefox-nightly-try", + "DEB_PKG_NAME": "firefox-nightly-try", + "DEB_PKG_VERSION": "112.0~build2", + }, + does_not_raise(), + ), + ( + "120.0b9", + 1, + "", + "", + "devedition", + { + "name": "Firefox", + "display_name": "Firefox Developer Edition", + "vendor": "Mozilla", + "remoting_name": "firefox-aurora", + "build_id": "20230222000000", + }, + { + "DEB_DESCRIPTION": "Mozilla Firefox Developer Edition", + "DEB_PKG_INSTALL_PATH": "usr/lib/firefox-devedition", + "DEB_PKG_NAME": "firefox-devedition", + "DEB_PKG_VERSION": "120.0b9~build1", + }, + does_not_raise(), + ), + ( + "120.0b9", + 1, + "-l10n-ach", + " - Firefox Developer Edition Language Pack for Acholi (ach) – Acoli", + "devedition", + { + "name": "Firefox", + "display_name": "Firefox Developer Edition", + "vendor": "Mozilla", + "remoting_name": "firefox-aurora", + "build_id": "20230222000000", + }, + { + "DEB_DESCRIPTION": "Mozilla Firefox Developer Edition - Firefox Developer Edition Language Pack for Acholi (ach) – Acoli", + "DEB_PKG_INSTALL_PATH": "usr/lib/firefox-devedition", + "DEB_PKG_NAME": "firefox-devedition-l10n-ach", + "DEB_PKG_VERSION": "120.0b9~build1", + }, + does_not_raise(), + ), + ( + "120.0b9", + 1, + "-l10n-ach", + " - Firefox Developer Edition Language Pack for Acholi (ach) – Acoli", + "devedition", + { + "name": "Firefox", + "display_name": "Firefox Developer Edition", + "vendor": "Mozilla", + "remoting_name": "firefox-aurora", + "build_id": "20230222000000", + }, + { + "DEB_DESCRIPTION": "Mozilla Firefox Developer Edition - Firefox Developer Edition Language Pack for Acholi (ach) – Acoli", + "DEB_PKG_INSTALL_PATH": "usr/lib/firefox-devedition", + "DEB_PKG_NAME": "firefox-devedition-l10n-ach", + "DEB_PKG_VERSION": "120.0b9~build1", + }, + does_not_raise(), + ), + ( + "120.0b9", + 1, + "-l10n-ach", + " - Firefox Developer Edition Language Pack for Acholi (ach) – Acoli", + "devedition", + { + "name": "Firefox", + "display_name": "Firefox Developer Edition", + "vendor": "Mozilla", + "remoting_name": "firefox-aurora", + "build_id": "20230222000000", + }, + { + "DEB_DESCRIPTION": "Mozilla Firefox Developer Edition - Firefox Developer Edition Language Pack for Acholi (ach) – Acoli", + "DEB_PKG_INSTALL_PATH": "usr/lib/firefox-aurora", + "DEB_PKG_NAME": "firefox-aurora-l10n-ach", + "DEB_PKG_VERSION": "120.0b9~build1", + }, + pytest.raises(AssertionError), + ), + ), +) +def test_get_build_variables( + version, + build_number, + package_name_suffix, + description_suffix, + release_product, + application_ini_data, + expected, + raises, +): + application_ini_data = deb._parse_application_ini_data( + application_ini_data, + version, + build_number, + ) + with raises: + if not package_name_suffix: + depends = "${shlibs:Depends}," + elif release_product == "devedition": + depends = ( + f"firefox-devedition (= {application_ini_data['deb_pkg_version']})" + ) + else: + depends = f"{application_ini_data['remoting_name']} (= {application_ini_data['deb_pkg_version']})" + + build_variables = deb._get_build_variables( + application_ini_data, + "x86", + depends=depends, + package_name_suffix=package_name_suffix, + description_suffix=description_suffix, + release_product=release_product, + ) + + assert build_variables == { + **{ + "DEB_CHANGELOG_DATE": "Wed, 22 Feb 2023 00:00:00 -0000", + "DEB_ARCH_NAME": "i386", + "DEB_DEPENDS": depends, + }, + **expected, + } + + +def test_copy_plain_deb_config(monkeypatch): + def mock_listdir(dir): + assert dir == "/template_dir" + return [ + "/template_dir/debian_file1.in", + "/template_dir/debian_file2.in", + "/template_dir/debian_file3", + "/template_dir/debian_file4", + ] + + monkeypatch.setattr(deb.os, "listdir", mock_listdir) + + def mock_makedirs(dir, exist_ok): + assert dir == "/source_dir/debian" + assert exist_ok is True + + monkeypatch.setattr(deb.os, "makedirs", mock_makedirs) + + mock_copy = MagicMock() + monkeypatch.setattr(deb.shutil, "copy", mock_copy) + + deb._copy_plain_deb_config("/template_dir", "/source_dir") + assert mock_copy.call_args_list == [ + call("/template_dir/debian_file3", "/source_dir/debian/debian_file3"), + call("/template_dir/debian_file4", "/source_dir/debian/debian_file4"), + ] + + +def test_render_deb_templates(): + with tempfile.TemporaryDirectory() as template_dir, tempfile.TemporaryDirectory() as source_dir: + with open(os.path.join(template_dir, "debian_file1.in"), "w") as f: + f.write("${some_build_variable}") + + with open(os.path.join(template_dir, "debian_file2.in"), "w") as f: + f.write("Some hardcoded value") + + with open(os.path.join(template_dir, "ignored_file.in"), "w") as f: + f.write("Must not be copied") + + deb._render_deb_templates( + template_dir, + source_dir, + {"some_build_variable": "some_value"}, + exclude_file_names=["ignored_file.in"], + ) + + with open(os.path.join(source_dir, "debian", "debian_file1")) as f: + assert f.read() == "some_value" + + with open(os.path.join(source_dir, "debian", "debian_file2")) as f: + assert f.read() == "Some hardcoded value" + + assert not os.path.exists(os.path.join(source_dir, "debian", "ignored_file")) + assert not os.path.exists(os.path.join(source_dir, "debian", "ignored_file.in")) + + +def test_inject_deb_distribution_folder(monkeypatch): + def mock_check_call(command): + global clone_dir + clone_dir = command[-1] + os.makedirs(os.path.join(clone_dir, "desktop/deb/distribution")) + + monkeypatch.setattr(deb.subprocess, "check_call", mock_check_call) + + def mock_copytree(source_tree, destination_tree): + global clone_dir + assert source_tree == mozpath.join(clone_dir, "desktop/deb/distribution") + assert destination_tree == "/source_dir/firefox/distribution" + + monkeypatch.setattr(deb.shutil, "copytree", mock_copytree) + + deb._inject_deb_distribution_folder("/source_dir", "Firefox") + + +ZH_TW_FTL = """\ +# 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/. + + +# These messages are used by the Firefox ".desktop" file on Linux. +# https://specifications.freedesktop.org/desktop-entry-spec/desktop-entry-spec-latest.html + +# The entry name is the label on the desktop icon, among other things. +desktop-entry-name = { -brand-shortcut-name } +# The comment usually appears as a tooltip when hovering over application menu entry. +desktop-entry-comment = ç€è¦½å…¨çƒè³‡è¨Šç¶² +desktop-entry-generic-name = 網é ç€è¦½å™¨ +# Keywords are search terms used to find this application. +# The string is a list of keywords separated by semicolons: +# - Do NOT replace semicolons with other punctuation signs. +# - The list MUST end with a semicolon. +desktop-entry-keywords = 網際網路;網路;ç€è¦½å™¨;網é ;上網;Internet;WWW;Browser;Web;Explorer; + +## Actions are visible in a context menu after right clicking the +## taskbar icon, possibly other places depending on the environment. + +desktop-action-new-window-name = 開新視窗 +desktop-action-new-private-window-name = é–‹æ–°éš±ç§è¦–窗 +""" + +NIGHTLY_DESKTOP_ENTRY_FILE_TEXT = """\ +[Desktop Entry] +Version=1.0 +Type=Application +Exec=firefox-nightly %u +Terminal=false +X-MultipleArgs=false +Icon=firefox-nightly +StartupWMClass=firefox-nightly +Categories=GNOME;GTK;Network;WebBrowser; +MimeType=application/json;application/pdf;application/rdf+xml;application/rss+xml;application/x-xpinstall;application/xhtml+xml;application/xml;audio/flac;audio/ogg;audio/webm;image/avif;image/gif;image/jpeg;image/png;image/svg+xml;image/webp;text/html;text/xml;video/ogg;video/webm;x-scheme-handler/chrome;x-scheme-handler/http;x-scheme-handler/https;x-scheme-handler/mailto; +StartupNotify=true +Actions=new-window;new-private-window;open-profile-manager; +Name=en-US-desktop-entry-name +Name[zh_TW]=zh-TW-desktop-entry-name +Comment=en-US-desktop-entry-comment +Comment[zh_TW]=zh-TW-desktop-entry-comment +GenericName=en-US-desktop-entry-generic-name +GenericName[zh_TW]=zh-TW-desktop-entry-generic-name +Keywords=en-US-desktop-entry-keywords +Keywords[zh_TW]=zh-TW-desktop-entry-keywords +X-GNOME-FullName=en-US-desktop-entry-x-gnome-full-name +X-GNOME-FullName[zh_TW]=zh-TW-desktop-entry-x-gnome-full-name + +[Desktop Action new-window] +Exec=firefox-nightly --new-window %u +Name=en-US-desktop-action-new-window-name +Name[zh_TW]=zh-TW-desktop-action-new-window-name + +[Desktop Action new-private-window] +Exec=firefox-nightly --private-window %u +Name=en-US-desktop-action-new-private-window-name +Name[zh_TW]=zh-TW-desktop-action-new-private-window-name + +[Desktop Action open-profile-manager] +Exec=firefox-nightly --ProfileManager +Name=en-US-desktop-action-open-profile-manager +Name[zh_TW]=zh-TW-desktop-action-open-profile-manager +""" + +DEVEDITION_DESKTOP_ENTRY_FILE_TEXT = """\ +[Desktop Entry] +Version=1.0 +Type=Application +Exec=firefox-devedition %u +Terminal=false +X-MultipleArgs=false +Icon=firefox-devedition +StartupWMClass=firefox-aurora +Categories=GNOME;GTK;Network;WebBrowser; +MimeType=application/json;application/pdf;application/rdf+xml;application/rss+xml;application/x-xpinstall;application/xhtml+xml;application/xml;audio/flac;audio/ogg;audio/webm;image/avif;image/gif;image/jpeg;image/png;image/svg+xml;image/webp;text/html;text/xml;video/ogg;video/webm;x-scheme-handler/chrome;x-scheme-handler/http;x-scheme-handler/https;x-scheme-handler/mailto; +StartupNotify=true +Actions=new-window;new-private-window;open-profile-manager; +Name=en-US-desktop-entry-name +Name[zh_TW]=zh-TW-desktop-entry-name +Comment=en-US-desktop-entry-comment +Comment[zh_TW]=zh-TW-desktop-entry-comment +GenericName=en-US-desktop-entry-generic-name +GenericName[zh_TW]=zh-TW-desktop-entry-generic-name +Keywords=en-US-desktop-entry-keywords +Keywords[zh_TW]=zh-TW-desktop-entry-keywords +X-GNOME-FullName=en-US-desktop-entry-x-gnome-full-name +X-GNOME-FullName[zh_TW]=zh-TW-desktop-entry-x-gnome-full-name + +[Desktop Action new-window] +Exec=firefox-devedition --new-window %u +Name=en-US-desktop-action-new-window-name +Name[zh_TW]=zh-TW-desktop-action-new-window-name + +[Desktop Action new-private-window] +Exec=firefox-devedition --private-window %u +Name=en-US-desktop-action-new-private-window-name +Name[zh_TW]=zh-TW-desktop-action-new-private-window-name + +[Desktop Action open-profile-manager] +Exec=firefox-devedition --ProfileManager +Name=en-US-desktop-action-open-profile-manager +Name[zh_TW]=zh-TW-desktop-action-open-profile-manager +""" + + +def test_generate_deb_desktop_entry_file_text(monkeypatch): + def responsive(url): + assert "zh-TW" in url + return Mock( + **{ + "status_code": 200, + "text": ZH_TW_FTL, + } + ) + + monkeypatch.setattr(deb.requests, "get", responsive) + + output_stream = StringIO() + logger = logging.getLogger("mozbuild:test:repackaging") + logger.setLevel(logging.DEBUG) + stream_handler = logging.StreamHandler(output_stream) + logger.addHandler(stream_handler) + + def log(level, action, params, format_str): + logger.log( + level, + format_str.format(**params), + extra={"action": action, "params": params}, + ) + + def fluent_localization(locales, resources, loader): + def format_value(resource): + return f"{locales[0]}-{resource}" + + return Mock(**{"format_value": format_value}) + + fluent_resource_loader = Mock() + + monkeypatch.setattr( + deb.json, + "load", + lambda f: {"zh-TW": {"platforms": ["linux"], "revision": "default"}}, + ) + + build_variables = { + "DEB_PKG_NAME": "firefox-nightly", + } + release_product = "firefox" + release_type = "nightly" + + desktop_entry_file_text = deb._generate_browser_desktop_entry_file_text( + log, + build_variables, + release_product, + release_type, + fluent_localization, + fluent_resource_loader, + ) + + assert desktop_entry_file_text == NIGHTLY_DESKTOP_ENTRY_FILE_TEXT + + build_variables = { + "DEB_PKG_NAME": "firefox-devedition", + } + release_product = "devedition" + release_type = "beta" + + desktop_entry_file_text = deb._generate_browser_desktop_entry_file_text( + log, + build_variables, + release_product, + release_type, + fluent_localization, + fluent_resource_loader, + ) + + assert desktop_entry_file_text == DEVEDITION_DESKTOP_ENTRY_FILE_TEXT + + def outage(url): + return Mock(**{"status_code": 500}) + + monkeypatch.setattr(deb.requests, "get", outage) + + with pytest.raises(deb.HgServerError): + desktop_entry_file_text = deb._generate_browser_desktop_entry_file_text( + log, + build_variables, + release_product, + release_type, + fluent_localization, + fluent_resource_loader, + ) + + +@pytest.mark.parametrize( + "does_path_exits, expectation", + ( + (True, does_not_raise()), + (False, pytest.raises(deb.NoDebPackageFound)), + ), +) +def test_generate_deb_archive( + monkeypatch, + does_path_exits, + expectation, +): + monkeypatch.setattr(deb, "_get_command", lambda _: ["mock_command"]) + monkeypatch.setattr(deb.subprocess, "check_call", lambda *_, **__: None) + + def mock_exists(path): + assert path == "/target_dir/firefox_111.0_amd64.deb" + return does_path_exits + + monkeypatch.setattr(deb.os.path, "exists", mock_exists) + + def mock_move(source_path, destination_path): + assert source_path == "/target_dir/firefox_111.0_amd64.deb" + assert destination_path == "/output/target.deb" + + monkeypatch.setattr(deb.shutil, "move", mock_move) + + with expectation: + deb._generate_deb_archive( + source_dir="/source_dir", + target_dir="/target_dir", + output_file_path="/output/target.deb", + build_variables={ + "DEB_PKG_NAME": "firefox", + "DEB_PKG_VERSION": "111.0", + }, + arch="x86_64", + ) + + +@pytest.mark.parametrize( + "arch, is_chroot_available, expected", + ( + ( + "all", + True, + [ + "chroot", + "/srv/jessie-amd64", + "bash", + "-c", + "cd /tmp/*/source; dpkg-buildpackage -us -uc -b", + ], + ), + ("all", False, ["dpkg-buildpackage", "-us", "-uc", "-b"]), + ( + "x86", + True, + [ + "chroot", + "/srv/jessie-i386", + "bash", + "-c", + "cd /tmp/*/source; dpkg-buildpackage -us -uc -b --host-arch=i386", + ], + ), + ("x86", False, ["dpkg-buildpackage", "-us", "-uc", "-b", "--host-arch=i386"]), + ( + "x86_64", + True, + [ + "chroot", + "/srv/jessie-amd64", + "bash", + "-c", + "cd /tmp/*/source; dpkg-buildpackage -us -uc -b --host-arch=amd64", + ], + ), + ( + "x86_64", + False, + ["dpkg-buildpackage", "-us", "-uc", "-b", "--host-arch=amd64"], + ), + ), +) +def test_get_command(monkeypatch, arch, is_chroot_available, expected): + monkeypatch.setattr(deb, "_is_chroot_available", lambda _: is_chroot_available) + assert deb._get_command(arch) == expected + + +@pytest.mark.parametrize( + "arch, does_dir_exist, expected_path, expected_result", + ( + ("all", False, "/srv/jessie-amd64", False), + ("all", True, "/srv/jessie-amd64", True), + ("x86", False, "/srv/jessie-i386", False), + ("x86_64", False, "/srv/jessie-amd64", False), + ("x86", True, "/srv/jessie-i386", True), + ("x86_64", True, "/srv/jessie-amd64", True), + ), +) +def test_is_chroot_available( + monkeypatch, arch, does_dir_exist, expected_path, expected_result +): + def _mock_is_dir(path): + assert path == expected_path + return does_dir_exist + + monkeypatch.setattr(deb.os.path, "isdir", _mock_is_dir) + assert deb._is_chroot_available(arch) == expected_result + + +@pytest.mark.parametrize( + "arch, expected", + ( + ("all", "/srv/jessie-amd64"), + ("x86", "/srv/jessie-i386"), + ("x86_64", "/srv/jessie-amd64"), + ), +) +def test_get_chroot_path(arch, expected): + assert deb._get_chroot_path(arch) == expected + + +_MANIFEST_JSON_DATA = { + "langpack_id": "fr", + "manifest_version": 2, + "browser_specific_settings": { + "gecko": { + "id": "langpack-fr@devedition.mozilla.org", + "strict_min_version": "112.0a1", + "strict_max_version": "112.0a1", + } + }, + "name": "Language: Français (French)", + "description": "Firefox Developer Edition Language Pack for Français (fr) – French", + "version": "112.0.20230227.181253", + "languages": { + "fr": { + "version": "20230223164410", + "chrome_resources": { + "app-marketplace-icons": "browser/chrome/browser/locale/fr/app-marketplace-icons/", + "branding": "browser/chrome/fr/locale/branding/", + "browser": "browser/chrome/fr/locale/browser/", + "browser-region": "browser/chrome/fr/locale/browser-region/", + "devtools": "browser/chrome/fr/locale/fr/devtools/client/", + "devtools-shared": "browser/chrome/fr/locale/fr/devtools/shared/", + "formautofill": "browser/features/formautofill@mozilla.org/fr/locale/fr/", + "report-site-issue": "browser/features/webcompat-reporter@mozilla.org/fr/locale/fr/", + "alerts": "chrome/fr/locale/fr/alerts/", + "autoconfig": "chrome/fr/locale/fr/autoconfig/", + "global": "chrome/fr/locale/fr/global/", + "global-platform": { + "macosx": "chrome/fr/locale/fr/global-platform/mac/", + "linux": "chrome/fr/locale/fr/global-platform/unix/", + "android": "chrome/fr/locale/fr/global-platform/unix/", + "win": "chrome/fr/locale/fr/global-platform/win/", + }, + "mozapps": "chrome/fr/locale/fr/mozapps/", + "necko": "chrome/fr/locale/fr/necko/", + "passwordmgr": "chrome/fr/locale/fr/passwordmgr/", + "pdf.js": "chrome/fr/locale/pdfviewer/", + "pipnss": "chrome/fr/locale/fr/pipnss/", + "pippki": "chrome/fr/locale/fr/pippki/", + "places": "chrome/fr/locale/fr/places/", + "weave": "chrome/fr/locale/fr/services/", + }, + } + }, + "sources": {"browser": {"base_path": "browser/"}}, + "author": "mozfr.org (contributors: L’équipe francophone)", +} + + +def test_extract_langpack_metadata(): + with tempfile.TemporaryDirectory() as d: + langpack_path = os.path.join(d, "langpack.xpi") + with zipfile.ZipFile(langpack_path, "w") as zip: + zip.writestr("manifest.json", json.dumps(_MANIFEST_JSON_DATA)) + + assert deb._extract_langpack_metadata(langpack_path) == _MANIFEST_JSON_DATA + + +@pytest.mark.parametrize( + "version, build_number, expected", + ( + ( + "112.0a1", + 1, + { + "build_id": "20230222000000", + "deb_pkg_version": "112.0a1~20230222000000", + "display_name": "Firefox Nightly", + "name": "Firefox", + "remoting_name": "firefox-nightly-try", + "timestamp": datetime.datetime(2023, 2, 22, 0, 0), + "vendor": "Mozilla", + }, + ), + ( + "112.0b1", + 1, + { + "build_id": "20230222000000", + "deb_pkg_version": "112.0b1~build1", + "display_name": "Firefox Nightly", + "name": "Firefox", + "remoting_name": "firefox-nightly-try", + "timestamp": datetime.datetime(2023, 2, 22, 0, 0), + "vendor": "Mozilla", + }, + ), + ( + "112.0", + 2, + { + "build_id": "20230222000000", + "deb_pkg_version": "112.0~build2", + "display_name": "Firefox Nightly", + "name": "Firefox", + "remoting_name": "firefox-nightly-try", + "timestamp": datetime.datetime(2023, 2, 22, 0, 0), + "vendor": "Mozilla", + }, + ), + ), +) +def test_load_application_ini_data(version, build_number, expected): + with tempfile.TemporaryDirectory() as d: + tar_path = os.path.join(d, "input.tar") + with tarfile.open(tar_path, "w") as tar: + application_ini_path = os.path.join(d, "application.ini") + with open(application_ini_path, "w") as application_ini_file: + application_ini_file.write(_APPLICATION_INI_CONTENT) + tar.add(application_ini_path) + application_ini_data = deb._load_application_ini_data( + tar_path, version, build_number + ) + assert application_ini_data == expected + + +if __name__ == "__main__": + mozunit.main() diff --git a/python/mozbuild/mozbuild/test/test_android_version_code.py b/python/mozbuild/mozbuild/test/test_android_version_code.py new file mode 100644 index 0000000000..7600ebe0d8 --- /dev/null +++ b/python/mozbuild/mozbuild/test/test_android_version_code.py @@ -0,0 +1,111 @@ +# 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/. + +import unittest + +from mozunit import main + +from mozbuild.android_version_code import ( + android_version_code_v0, + android_version_code_v1, +) + + +class TestAndroidVersionCode(unittest.TestCase): + def test_android_version_code_v0(self): + # From https://treeherder.mozilla.org/#/jobs?repo=mozilla-central&revision=e25de9972a77. + buildid = "20150708104620" + arm_api9 = 2015070819 + arm_api11 = 2015070821 + x86_api9 = 2015070822 + self.assertEqual( + android_version_code_v0( + buildid, cpu_arch="armeabi", min_sdk=9, max_sdk=None + ), + arm_api9, + ) + self.assertEqual( + android_version_code_v0( + buildid, cpu_arch="armeabi-v7a", min_sdk=11, max_sdk=None + ), + arm_api11, + ) + self.assertEqual( + android_version_code_v0(buildid, cpu_arch="x86", min_sdk=9, max_sdk=None), + x86_api9, + ) + + def test_android_version_code_v1(self): + buildid = "20150825141628" + arm_api16 = 0b01111000001000000001001001110001 + arm64_api21 = 0b01111000001000000001001001110100 + x86_api9 = 0b01111000001000000001001001110100 + self.assertEqual( + android_version_code_v1( + buildid, cpu_arch="armeabi-v7a", min_sdk=16, max_sdk=None + ), + arm_api16, + ) + self.assertEqual( + android_version_code_v1( + buildid, cpu_arch="arm64-v8a", min_sdk=21, max_sdk=None + ), + arm64_api21, + ) + self.assertEqual( + android_version_code_v1(buildid, cpu_arch="x86", min_sdk=9, max_sdk=None), + x86_api9, + ) + + def test_android_version_code_v1_underflow(self): + """Verify that it is an error to ask for v1 codes predating the cutoff.""" + buildid = "201508010000" # Earliest possible. + arm_api9 = 0b01111000001000000000000000000000 + self.assertEqual( + android_version_code_v1( + buildid, cpu_arch="armeabi", min_sdk=9, max_sdk=None + ), + arm_api9, + ) + with self.assertRaises(ValueError) as cm: + underflow = "201507310000" # Latest possible (valid) underflowing date. + android_version_code_v1( + underflow, cpu_arch="armeabi", min_sdk=9, max_sdk=None + ) + self.assertTrue("underflow" in cm.exception.message) + + def test_android_version_code_v1_running_low(self): + """Verify there is an informative message if one asks for v1 + codes that are close to overflow.""" + with self.assertRaises(ValueError) as cm: + overflow = "20290801000000" + android_version_code_v1( + overflow, cpu_arch="armeabi", min_sdk=9, max_sdk=None + ) + self.assertTrue("Running out of low order bits" in cm.exception.message) + + def test_android_version_code_v1_overflow(self): + """Verify that it is an error to ask for v1 codes that actually does overflow.""" + with self.assertRaises(ValueError) as cm: + overflow = "20310801000000" + android_version_code_v1( + overflow, cpu_arch="armeabi", min_sdk=9, max_sdk=None + ) + self.assertTrue("overflow" in cm.exception.message) + + def test_android_version_code_v0_relative_v1(self): + """Verify that the first v1 code is greater than the equivalent v0 code.""" + buildid = "20150801000000" + self.assertGreater( + android_version_code_v1( + buildid, cpu_arch="armeabi", min_sdk=9, max_sdk=None + ), + android_version_code_v0( + buildid, cpu_arch="armeabi", min_sdk=9, max_sdk=None + ), + ) + + +if __name__ == "__main__": + main() diff --git a/python/mozbuild/mozbuild/test/test_artifact_cache.py b/python/mozbuild/mozbuild/test/test_artifact_cache.py new file mode 100644 index 0000000000..d12d150183 --- /dev/null +++ b/python/mozbuild/mozbuild/test/test_artifact_cache.py @@ -0,0 +1,145 @@ +# 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/. + +import os +import time +import unittest +from shutil import rmtree +from tempfile import mkdtemp + +import mozunit + +from mozbuild import artifact_cache +from mozbuild.artifact_cache import ArtifactCache + +CONTENTS = { + "http://server/foo": b"foo", + "http://server/bar": b"bar" * 400, + "http://server/qux": b"qux" * 400, + "http://server/fuga": b"fuga" * 300, + "http://server/hoge": b"hoge" * 300, + "http://server/larger": b"larger" * 3000, +} + + +class FakeResponse(object): + def __init__(self, content): + self._content = content + + @property + def headers(self): + return {"Content-length": str(len(self._content))} + + def iter_content(self, chunk_size): + content = memoryview(self._content) + while content: + yield content[:chunk_size] + content = content[chunk_size:] + + def raise_for_status(self): + pass + + def close(self): + pass + + +class FakeSession(object): + def get(self, url, stream=True): + assert stream is True + return FakeResponse(CONTENTS[url]) + + +class TestArtifactCache(unittest.TestCase): + def setUp(self): + self.min_cached_artifacts = artifact_cache.MIN_CACHED_ARTIFACTS + self.max_cached_artifacts_size = artifact_cache.MAX_CACHED_ARTIFACTS_SIZE + artifact_cache.MIN_CACHED_ARTIFACTS = 2 + artifact_cache.MAX_CACHED_ARTIFACTS_SIZE = 4096 + + self._real_utime = os.utime + os.utime = self.utime + self.timestamp = time.time() - 86400 + + self.tmpdir = mkdtemp() + + def tearDown(self): + rmtree(self.tmpdir) + artifact_cache.MIN_CACHED_ARTIFACTS = self.min_cached_artifacts + artifact_cache.MAX_CACHED_ARTIFACTS_SIZE = self.max_cached_artifacts_size + os.utime = self._real_utime + + def utime(self, path, times): + if times is None: + # Ensure all downloaded files have a different timestamp + times = (self.timestamp, self.timestamp) + self.timestamp += 2 + self._real_utime(path, times) + + def listtmpdir(self): + return [p for p in os.listdir(self.tmpdir) if p != ".metadata_never_index"] + + def test_artifact_cache_persistence(self): + cache = ArtifactCache(self.tmpdir) + cache._download_manager.session = FakeSession() + + path = cache.fetch("http://server/foo") + expected = [os.path.basename(path)] + self.assertEqual(self.listtmpdir(), expected) + + path = cache.fetch("http://server/bar") + expected.append(os.path.basename(path)) + self.assertEqual(sorted(self.listtmpdir()), sorted(expected)) + + # We're downloading more than the cache allows us, but since it's all + # in the same session, no purge happens. + path = cache.fetch("http://server/qux") + expected.append(os.path.basename(path)) + self.assertEqual(sorted(self.listtmpdir()), sorted(expected)) + + path = cache.fetch("http://server/fuga") + expected.append(os.path.basename(path)) + self.assertEqual(sorted(self.listtmpdir()), sorted(expected)) + + cache = ArtifactCache(self.tmpdir) + cache._download_manager.session = FakeSession() + + # Downloading a new file in a new session purges the oldest files in + # the cache. + path = cache.fetch("http://server/hoge") + expected.append(os.path.basename(path)) + expected = expected[2:] + self.assertEqual(sorted(self.listtmpdir()), sorted(expected)) + + # Downloading a file already in the cache leaves the cache untouched + cache = ArtifactCache(self.tmpdir) + cache._download_manager.session = FakeSession() + + path = cache.fetch("http://server/qux") + self.assertEqual(sorted(self.listtmpdir()), sorted(expected)) + + # bar was purged earlier, re-downloading it should purge the oldest + # downloaded file, which at this point would be qux, but we also + # re-downloaded it in the mean time, so the next one (fuga) should be + # the purged one. + cache = ArtifactCache(self.tmpdir) + cache._download_manager.session = FakeSession() + + path = cache.fetch("http://server/bar") + expected.append(os.path.basename(path)) + expected = [p for p in expected if "fuga" not in p] + self.assertEqual(sorted(self.listtmpdir()), sorted(expected)) + + # Downloading one file larger than the cache size should still leave + # MIN_CACHED_ARTIFACTS files. + cache = ArtifactCache(self.tmpdir) + cache._download_manager.session = FakeSession() + + path = cache.fetch("http://server/larger") + expected.append(os.path.basename(path)) + expected = expected[-2:] + self.assertEqual(sorted(self.listtmpdir()), sorted(expected)) + + +if __name__ == "__main__": + mozunit.main() diff --git a/python/mozbuild/mozbuild/test/test_artifacts.py b/python/mozbuild/mozbuild/test/test_artifacts.py new file mode 100644 index 0000000000..6897136d4a --- /dev/null +++ b/python/mozbuild/mozbuild/test/test_artifacts.py @@ -0,0 +1,122 @@ +# 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 unittest import TestCase + +import buildconfig +import mozunit + +from mozbuild.artifacts import ArtifactJob, ThunderbirdMixin + + +class FakeArtifactJob(ArtifactJob): + package_re = r"" + + +class TestArtifactJob(TestCase): + def _assert_candidate_trees(self, version_display, expected_trees): + buildconfig.substs["MOZ_APP_VERSION_DISPLAY"] = version_display + + job = FakeArtifactJob() + self.assertGreater(len(job.candidate_trees), 0) + self.assertEqual(job.candidate_trees, expected_trees) + + def test_candidate_trees_with_empty_file(self): + self._assert_candidate_trees( + version_display="", expected_trees=ArtifactJob.default_candidate_trees + ) + + def test_candidate_trees_with_beta_version(self): + self._assert_candidate_trees( + version_display="92.1b2", expected_trees=ArtifactJob.beta_candidate_trees + ) + + def test_candidate_trees_with_esr_version(self): + self._assert_candidate_trees( + version_display="91.3.0esr", expected_trees=ArtifactJob.esr_candidate_trees + ) + + def test_candidate_trees_with_nightly_version(self): + self._assert_candidate_trees( + version_display="95.0a1", expected_trees=ArtifactJob.nightly_candidate_trees + ) + + def test_candidate_trees_with_release_version(self): + self._assert_candidate_trees( + version_display="93.0.1", expected_trees=ArtifactJob.default_candidate_trees + ) + + def test_candidate_trees_with_newline_before_version(self): + self._assert_candidate_trees( + version_display="\n\n91.3.0esr", + expected_trees=ArtifactJob.esr_candidate_trees, + ) + + def test_property_is_cached(self): + job = FakeArtifactJob() + expected_trees = ArtifactJob.esr_candidate_trees + + buildconfig.substs["MOZ_APP_VERSION_DISPLAY"] = "91.3.0.esr" + self.assertEqual(job.candidate_trees, expected_trees) + # Because the property is cached, changing the + # `MOZ_APP_VERSION_DISPLAY` won't have any impact. + buildconfig.substs["MOZ_APP_VERSION_DISPLAY"] = "" + self.assertEqual(job.candidate_trees, expected_trees) + + +class FakeThunderbirdJob(ThunderbirdMixin, FakeArtifactJob): + pass + + +class TestThunderbirdMixin(TestCase): + def _assert_candidate_trees(self, version_display, source_repo, expected_trees): + buildconfig.substs["MOZ_APP_VERSION_DISPLAY"] = version_display + buildconfig.substs["MOZ_SOURCE_REPO"] = source_repo + + job = FakeThunderbirdJob() + self.assertGreater(len(job.candidate_trees), 0) + self.assertEqual(job.candidate_trees, expected_trees) + + def test_candidate_trees_with_beta_version(self): + self._assert_candidate_trees( + version_display="92.1b2", + source_repo="https://hg.mozilla.org/releases/comm-beta", + expected_trees=ThunderbirdMixin.beta_candidate_trees, + ) + + def test_candidate_trees_with_esr_version(self): + self._assert_candidate_trees( + version_display="91.3.0esr", + source_repo="https://hg.mozilla.org/releases/comm-esr91", + expected_trees=ThunderbirdMixin.esr_candidate_trees, + ) + + def test_candidate_trees_with_nightly_version(self): + self._assert_candidate_trees( + version_display="95.0a1", + source_repo="https://hg.mozilla.org/comm-central", + expected_trees=ThunderbirdMixin.nightly_candidate_trees, + ) + + def test_candidate_trees_with_release_version(self): + self._assert_candidate_trees( + version_display="93.0.1", + source_repo="https://hg.mozilla.org/releases/comm-release", + expected_trees=ThunderbirdMixin.default_candidate_trees, + ) + + def test_property_is_cached(self): + job = FakeThunderbirdJob() + expected_trees = ThunderbirdMixin.esr_candidate_trees + + buildconfig.substs["MOZ_APP_VERSION_DISPLAY"] = "91.3.0.esr" + self.assertEqual(job.candidate_trees, expected_trees) + # Because the property is cached, changing the + # `MOZ_APP_VERSION_DISPLAY` won't have any impact. + buildconfig.substs["MOZ_APP_VERSION_DISPLAY"] = "" + self.assertEqual(job.candidate_trees, expected_trees) + + +if __name__ == "__main__": + mozunit.main() diff --git a/python/mozbuild/mozbuild/test/test_base.py b/python/mozbuild/mozbuild/test/test_base.py new file mode 100644 index 0000000000..c75a71ef5d --- /dev/null +++ b/python/mozbuild/mozbuild/test/test_base.py @@ -0,0 +1,446 @@ +# 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/. + +import json +import os +import shutil +import sys +import tempfile +import unittest + +import mozpack.path as mozpath +from buildconfig import topobjdir, topsrcdir +from mach.logging import LoggingManager +from mozfile.mozfile import NamedTemporaryFile +from mozunit import main +from six import StringIO + +from mozbuild.backend.configenvironment import ConfigEnvironment +from mozbuild.base import ( + BadEnvironmentException, + MachCommandBase, + MozbuildObject, + PathArgument, +) +from mozbuild.test.common import prepare_tmp_topsrcdir + +curdir = os.path.dirname(__file__) +log_manager = LoggingManager() + + +class TestMozbuildObject(unittest.TestCase): + def setUp(self): + self._old_cwd = os.getcwd() + self._old_env = dict(os.environ) + os.environ.pop("MOZCONFIG", None) + os.environ.pop("MOZ_OBJDIR", None) + + def tearDown(self): + os.chdir(self._old_cwd) + os.environ.clear() + os.environ.update(self._old_env) + + def get_base(self, topobjdir=None): + return MozbuildObject(topsrcdir, None, log_manager, topobjdir=topobjdir) + + def test_objdir_config_guess(self): + base = self.get_base() + + with NamedTemporaryFile(mode="wt") as mozconfig: + os.environ["MOZCONFIG"] = mozconfig.name + + self.assertIsNotNone(base.topobjdir) + self.assertEqual(len(base.topobjdir.split()), 1) + config_guess = base.resolve_config_guess() + self.assertTrue(base.topobjdir.endswith(config_guess)) + self.assertTrue(os.path.isabs(base.topobjdir)) + self.assertTrue(base.topobjdir.startswith(base.topsrcdir)) + + def test_objdir_trailing_slash(self): + """Trailing slashes in topobjdir should be removed.""" + base = self.get_base() + + with NamedTemporaryFile(mode="wt") as mozconfig: + mozconfig.write("mk_add_options MOZ_OBJDIR=@TOPSRCDIR@/foo/") + mozconfig.flush() + os.environ["MOZCONFIG"] = mozconfig.name + + self.assertEqual(base.topobjdir, mozpath.join(base.topsrcdir, "foo")) + self.assertTrue(base.topobjdir.endswith("foo")) + + def test_objdir_config_status(self): + """Ensure @CONFIG_GUESS@ is handled when loading mozconfig.""" + base = self.get_base() + guess = base.resolve_config_guess() + + # There may be symlinks involved, so we use real paths to ensure + # path consistency. + d = os.path.realpath(tempfile.mkdtemp()) + try: + mozconfig = os.path.join(d, "mozconfig") + with open(mozconfig, "wt") as fh: + fh.write("mk_add_options MOZ_OBJDIR=@TOPSRCDIR@/foo/@CONFIG_GUESS@") + print("Wrote mozconfig %s" % mozconfig) + + topobjdir = os.path.join(d, "foo", guess) + os.makedirs(topobjdir) + + # Create a fake topsrcdir. + prepare_tmp_topsrcdir(d) + + mozinfo = os.path.join(topobjdir, "mozinfo.json") + with open(mozinfo, "wt") as fh: + json.dump( + dict( + topsrcdir=d, + mozconfig=mozconfig, + ), + fh, + ) + + os.environ["MOZCONFIG"] = mozconfig + os.chdir(topobjdir) + + obj = MozbuildObject.from_environment(detect_virtualenv_mozinfo=False) + + self.assertEqual(obj.topobjdir, mozpath.normsep(topobjdir)) + finally: + os.chdir(self._old_cwd) + shutil.rmtree(d) + + def test_relative_objdir(self): + """Relative defined objdirs are loaded properly.""" + d = os.path.realpath(tempfile.mkdtemp()) + try: + mozconfig = os.path.join(d, "mozconfig") + with open(mozconfig, "wt") as fh: + fh.write("mk_add_options MOZ_OBJDIR=./objdir") + + topobjdir = mozpath.join(d, "objdir") + os.mkdir(topobjdir) + + mozinfo = os.path.join(topobjdir, "mozinfo.json") + with open(mozinfo, "wt") as fh: + json.dump( + dict( + topsrcdir=d, + mozconfig=mozconfig, + ), + fh, + ) + + os.environ["MOZCONFIG"] = mozconfig + child = os.path.join(topobjdir, "foo", "bar") + os.makedirs(child) + os.chdir(child) + + obj = MozbuildObject.from_environment(detect_virtualenv_mozinfo=False) + + self.assertEqual(obj.topobjdir, topobjdir) + + finally: + os.chdir(self._old_cwd) + shutil.rmtree(d) + + @unittest.skipIf( + not hasattr(os, "symlink") or os.name == "nt", "symlinks not available." + ) + def test_symlink_objdir(self): + """Objdir that is a symlink is loaded properly.""" + d = os.path.realpath(tempfile.mkdtemp()) + try: + topobjdir_real = os.path.join(d, "objdir") + topobjdir_link = os.path.join(d, "objlink") + + os.mkdir(topobjdir_real) + os.symlink(topobjdir_real, topobjdir_link) + + mozconfig = os.path.join(d, "mozconfig") + with open(mozconfig, "wt") as fh: + fh.write("mk_add_options MOZ_OBJDIR=%s" % topobjdir_link) + + mozinfo = os.path.join(topobjdir_real, "mozinfo.json") + with open(mozinfo, "wt") as fh: + json.dump( + dict( + topsrcdir=d, + mozconfig=mozconfig, + ), + fh, + ) + + os.chdir(topobjdir_link) + obj = MozbuildObject.from_environment(detect_virtualenv_mozinfo=False) + self.assertEqual(obj.topobjdir, topobjdir_real) + + os.chdir(topobjdir_real) + obj = MozbuildObject.from_environment(detect_virtualenv_mozinfo=False) + self.assertEqual(obj.topobjdir, topobjdir_real) + + finally: + os.chdir(self._old_cwd) + shutil.rmtree(d) + + def test_mach_command_base_inside_objdir(self): + """Ensure a MachCommandBase constructed from inside the objdir works.""" + + d = os.path.realpath(tempfile.mkdtemp()) + + try: + topobjdir = os.path.join(d, "objdir") + os.makedirs(topobjdir) + + topsrcdir = os.path.join(d, "srcdir") + prepare_tmp_topsrcdir(topsrcdir) + + mozinfo = os.path.join(topobjdir, "mozinfo.json") + with open(mozinfo, "wt") as fh: + json.dump( + dict( + topsrcdir=topsrcdir, + ), + fh, + ) + + os.chdir(topobjdir) + + class MockMachContext(object): + pass + + context = MockMachContext() + context.cwd = topobjdir + context.topdir = topsrcdir + context.settings = None + context.log_manager = None + context.detect_virtualenv_mozinfo = False + + o = MachCommandBase(context, None) + + self.assertEqual(o.topobjdir, mozpath.normsep(topobjdir)) + self.assertEqual(o.topsrcdir, mozpath.normsep(topsrcdir)) + + finally: + os.chdir(self._old_cwd) + shutil.rmtree(d) + + def test_objdir_is_srcdir_rejected(self): + """Ensure the srcdir configurations are rejected.""" + d = os.path.realpath(tempfile.mkdtemp()) + + try: + # The easiest way to do this is to create a mozinfo.json with data + # that will never happen. + mozinfo = os.path.join(d, "mozinfo.json") + with open(mozinfo, "wt") as fh: + json.dump({"topsrcdir": d}, fh) + + os.chdir(d) + + with self.assertRaises(BadEnvironmentException): + MozbuildObject.from_environment(detect_virtualenv_mozinfo=False) + + finally: + os.chdir(self._old_cwd) + shutil.rmtree(d) + + def test_objdir_mismatch(self): + """Ensure MachCommandBase throwing on objdir mismatch.""" + d = os.path.realpath(tempfile.mkdtemp()) + + try: + real_topobjdir = os.path.join(d, "real-objdir") + os.makedirs(real_topobjdir) + + topobjdir = os.path.join(d, "objdir") + os.makedirs(topobjdir) + + topsrcdir = os.path.join(d, "srcdir") + prepare_tmp_topsrcdir(topsrcdir) + + mozconfig = os.path.join(d, "mozconfig") + with open(mozconfig, "wt") as fh: + fh.write( + "mk_add_options MOZ_OBJDIR=%s" % real_topobjdir.replace("\\", "/") + ) + + mozinfo = os.path.join(topobjdir, "mozinfo.json") + with open(mozinfo, "wt") as fh: + json.dump( + dict( + topsrcdir=topsrcdir, + mozconfig=mozconfig, + ), + fh, + ) + + os.chdir(topobjdir) + + class MockMachContext(object): + pass + + context = MockMachContext() + context.cwd = topobjdir + context.topdir = topsrcdir + context.settings = None + context.log_manager = None + context.detect_virtualenv_mozinfo = False + + stdout = sys.stdout + sys.stdout = StringIO() + try: + with self.assertRaises(SystemExit): + MachCommandBase(context, None) + + self.assertTrue( + sys.stdout.getvalue().startswith( + "Ambiguous object directory detected." + ) + ) + finally: + sys.stdout = stdout + + finally: + os.chdir(self._old_cwd) + shutil.rmtree(d) + + def test_config_environment(self): + d = os.path.realpath(tempfile.mkdtemp()) + + try: + with open(os.path.join(d, "config.status"), "w") as fh: + fh.write("# coding=utf-8\n") + fh.write("from __future__ import unicode_literals\n") + fh.write("topobjdir = '%s'\n" % mozpath.normsep(d)) + fh.write("topsrcdir = '%s'\n" % topsrcdir) + fh.write("mozconfig = None\n") + fh.write("defines = { 'FOO': 'foo' }\n") + fh.write("substs = { 'QUX': 'qux' }\n") + fh.write( + "__all__ = ['topobjdir', 'topsrcdir', 'defines', " + "'substs', 'mozconfig']" + ) + + base = self.get_base(topobjdir=d) + + ce = base.config_environment + self.assertIsInstance(ce, ConfigEnvironment) + + self.assertEqual(base.defines, ce.defines) + self.assertEqual(base.substs, ce.substs) + + self.assertEqual(base.defines, {"FOO": "foo"}) + self.assertEqual( + base.substs, + { + "ACDEFINES": "-DFOO=foo", + "ALLEMPTYSUBSTS": "", + "ALLSUBSTS": "ACDEFINES = -DFOO=foo\nQUX = qux", + "QUX": "qux", + }, + ) + finally: + shutil.rmtree(d) + + def test_get_binary_path(self): + base = self.get_base(topobjdir=topobjdir) + + platform = sys.platform + + # We should ideally use the config.status from the build. Let's install + # a fake one. + substs = [ + ("MOZ_APP_NAME", "awesomeapp"), + ("MOZ_BUILD_APP", "awesomeapp"), + ] + if sys.platform.startswith("darwin"): + substs.append(("OS_ARCH", "Darwin")) + substs.append(("BIN_SUFFIX", "")) + substs.append(("MOZ_MACBUNDLE_NAME", "Nightly.app")) + elif sys.platform.startswith(("win32", "cygwin")): + substs.append(("OS_ARCH", "WINNT")) + substs.append(("BIN_SUFFIX", ".exe")) + else: + substs.append(("OS_ARCH", "something")) + substs.append(("BIN_SUFFIX", "")) + + base._config_environment = ConfigEnvironment( + base.topsrcdir, base.topobjdir, substs=substs + ) + + p = base.get_binary_path("xpcshell", False) + if platform.startswith("darwin"): + self.assertTrue(p.endswith("Contents/MacOS/xpcshell")) + elif platform.startswith(("win32", "cygwin")): + self.assertTrue(p.endswith("xpcshell.exe")) + else: + self.assertTrue(p.endswith("dist/bin/xpcshell")) + + p = base.get_binary_path(validate_exists=False) + if platform.startswith("darwin"): + self.assertTrue(p.endswith("Contents/MacOS/awesomeapp")) + elif platform.startswith(("win32", "cygwin")): + self.assertTrue(p.endswith("awesomeapp.exe")) + else: + self.assertTrue(p.endswith("dist/bin/awesomeapp")) + + p = base.get_binary_path(validate_exists=False, where="staged-package") + if platform.startswith("darwin"): + self.assertTrue( + p.endswith("awesomeapp/Nightly.app/Contents/MacOS/awesomeapp") + ) + elif platform.startswith(("win32", "cygwin")): + self.assertTrue(p.endswith("awesomeapp\\awesomeapp.exe")) + else: + self.assertTrue(p.endswith("awesomeapp/awesomeapp")) + + self.assertRaises(Exception, base.get_binary_path, where="somewhere") + + p = base.get_binary_path("foobar", validate_exists=False) + if platform.startswith("win32"): + self.assertTrue(p.endswith("foobar.exe")) + else: + self.assertTrue(p.endswith("foobar")) + + +class TestPathArgument(unittest.TestCase): + def test_path_argument(self): + # Absolute path + p = PathArgument("/obj/foo", "/src", "/obj", "/src") + self.assertEqual(p.relpath(), "foo") + self.assertEqual(p.srcdir_path(), "/src/foo") + self.assertEqual(p.objdir_path(), "/obj/foo") + + # Relative path within srcdir + p = PathArgument("foo", "/src", "/obj", "/src") + self.assertEqual(p.relpath(), "foo") + self.assertEqual(p.srcdir_path(), "/src/foo") + self.assertEqual(p.objdir_path(), "/obj/foo") + + # Relative path within subdirectory + p = PathArgument("bar", "/src", "/obj", "/src/foo") + self.assertEqual(p.relpath(), "foo/bar") + self.assertEqual(p.srcdir_path(), "/src/foo/bar") + self.assertEqual(p.objdir_path(), "/obj/foo/bar") + + # Relative path within objdir + p = PathArgument("foo", "/src", "/obj", "/obj") + self.assertEqual(p.relpath(), "foo") + self.assertEqual(p.srcdir_path(), "/src/foo") + self.assertEqual(p.objdir_path(), "/obj/foo") + + # "." path + p = PathArgument(".", "/src", "/obj", "/src/foo") + self.assertEqual(p.relpath(), "foo") + self.assertEqual(p.srcdir_path(), "/src/foo") + self.assertEqual(p.objdir_path(), "/obj/foo") + + # Nested src/obj directories + p = PathArgument("bar", "/src", "/src/obj", "/src/obj/foo") + self.assertEqual(p.relpath(), "foo/bar") + self.assertEqual(p.srcdir_path(), "/src/foo/bar") + self.assertEqual(p.objdir_path(), "/src/obj/foo/bar") + + +if __name__ == "__main__": + main() diff --git a/python/mozbuild/mozbuild/test/test_containers.py b/python/mozbuild/mozbuild/test/test_containers.py new file mode 100644 index 0000000000..50dd0a4088 --- /dev/null +++ b/python/mozbuild/mozbuild/test/test_containers.py @@ -0,0 +1,224 @@ +# 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/. + +import unittest +from collections import OrderedDict + +from mozunit import main + +from mozbuild.util import ( + KeyedDefaultDict, + List, + OrderedDefaultDict, + ReadOnlyDefaultDict, + ReadOnlyDict, + ReadOnlyKeyedDefaultDict, + ReadOnlyNamespace, +) + + +class TestReadOnlyNamespace(unittest.TestCase): + def test_basic(self): + test = ReadOnlyNamespace(foo=1, bar=2) + + self.assertEqual(test.foo, 1) + self.assertEqual(test.bar, 2) + self.assertEqual( + sorted(i for i in dir(test) if not i.startswith("__")), ["bar", "foo"] + ) + + with self.assertRaises(AttributeError): + test.missing + + with self.assertRaises(Exception): + test.foo = 2 + + with self.assertRaises(Exception): + del test.foo + + self.assertEqual(test, test) + self.assertEqual(test, ReadOnlyNamespace(foo=1, bar=2)) + self.assertNotEqual(test, ReadOnlyNamespace(foo="1", bar=2)) + self.assertNotEqual(test, ReadOnlyNamespace(foo=1, bar=2, qux=3)) + self.assertNotEqual(test, ReadOnlyNamespace(foo=1, qux=3)) + self.assertNotEqual(test, ReadOnlyNamespace(foo=3, bar="42")) + + +class TestReadOnlyDict(unittest.TestCase): + def test_basic(self): + original = {"foo": 1, "bar": 2} + + test = ReadOnlyDict(original) + + self.assertEqual(original, test) + self.assertEqual(test["foo"], 1) + + with self.assertRaises(KeyError): + test["missing"] + + with self.assertRaises(Exception): + test["baz"] = True + + def test_update(self): + original = {"foo": 1, "bar": 2} + + test = ReadOnlyDict(original) + + with self.assertRaises(Exception): + test.update(foo=2) + + self.assertEqual(original, test) + + def test_del(self): + original = {"foo": 1, "bar": 2} + + test = ReadOnlyDict(original) + + with self.assertRaises(Exception): + del test["foo"] + + self.assertEqual(original, test) + + +class TestReadOnlyDefaultDict(unittest.TestCase): + def test_simple(self): + original = {"foo": 1, "bar": 2} + + test = ReadOnlyDefaultDict(bool, original) + + self.assertEqual(original, test) + + self.assertEqual(test["foo"], 1) + + def test_assignment(self): + test = ReadOnlyDefaultDict(bool, {}) + + with self.assertRaises(Exception): + test["foo"] = True + + def test_defaults(self): + test = ReadOnlyDefaultDict(bool, {"foo": 1}) + + self.assertEqual(test["foo"], 1) + + self.assertEqual(test["qux"], False) + + +class TestList(unittest.TestCase): + def test_add_list(self): + test = List([1, 2, 3]) + + test += [4, 5, 6] + self.assertIsInstance(test, List) + self.assertEqual(test, [1, 2, 3, 4, 5, 6]) + + test = test + [7, 8] + self.assertIsInstance(test, List) + self.assertEqual(test, [1, 2, 3, 4, 5, 6, 7, 8]) + + def test_add_string(self): + test = List([1, 2, 3]) + + with self.assertRaises(ValueError): + test += "string" + + def test_none(self): + """As a special exception, we allow None to be treated as an empty + list.""" + test = List([1, 2, 3]) + + test += None + self.assertEqual(test, [1, 2, 3]) + + test = test + None + self.assertIsInstance(test, List) + self.assertEqual(test, [1, 2, 3]) + + with self.assertRaises(ValueError): + test += False + + with self.assertRaises(ValueError): + test = test + False + + +class TestOrderedDefaultDict(unittest.TestCase): + def test_simple(self): + original = OrderedDict(foo=1, bar=2) + + test = OrderedDefaultDict(bool, original) + + self.assertEqual(original, test) + + self.assertEqual(test["foo"], 1) + + self.assertEqual(list(test), ["foo", "bar"]) + + def test_defaults(self): + test = OrderedDefaultDict(bool, {"foo": 1}) + + self.assertEqual(test["foo"], 1) + + self.assertEqual(test["qux"], False) + + self.assertEqual(list(test), ["foo", "qux"]) + + +class TestKeyedDefaultDict(unittest.TestCase): + def test_simple(self): + original = {"foo": 1, "bar": 2} + + test = KeyedDefaultDict(lambda x: x, original) + + self.assertEqual(original, test) + + self.assertEqual(test["foo"], 1) + + def test_defaults(self): + test = KeyedDefaultDict(lambda x: x, {"foo": 1}) + + self.assertEqual(test["foo"], 1) + + self.assertEqual(test["qux"], "qux") + + self.assertEqual(test["bar"], "bar") + + test["foo"] = 2 + test["qux"] = None + test["baz"] = "foo" + + self.assertEqual(test["foo"], 2) + + self.assertEqual(test["qux"], None) + + self.assertEqual(test["baz"], "foo") + + +class TestReadOnlyKeyedDefaultDict(unittest.TestCase): + def test_defaults(self): + test = ReadOnlyKeyedDefaultDict(lambda x: x, {"foo": 1}) + + self.assertEqual(test["foo"], 1) + + self.assertEqual(test["qux"], "qux") + + self.assertEqual(test["bar"], "bar") + + copy = dict(test) + + with self.assertRaises(Exception): + test["foo"] = 2 + + with self.assertRaises(Exception): + test["qux"] = None + + with self.assertRaises(Exception): + test["baz"] = "foo" + + self.assertEqual(test, copy) + + self.assertEqual(len(test), 3) + + +if __name__ == "__main__": + main() diff --git a/python/mozbuild/mozbuild/test/test_dotproperties.py b/python/mozbuild/mozbuild/test/test_dotproperties.py new file mode 100644 index 0000000000..4e7a437799 --- /dev/null +++ b/python/mozbuild/mozbuild/test/test_dotproperties.py @@ -0,0 +1,183 @@ +# -*- coding: utf-8 -*- + +import os +import unittest + +import mozpack.path as mozpath +from mozunit import main +from six import StringIO + +from mozbuild.dotproperties import DotProperties + +test_data_path = mozpath.abspath(mozpath.dirname(__file__)) +test_data_path = mozpath.join(test_data_path, "data") + + +class TestDotProperties(unittest.TestCase): + def test_get(self): + contents = StringIO( + """ +key=value +""" + ) + p = DotProperties(contents) + self.assertEqual(p.get("missing"), None) + self.assertEqual(p.get("missing", "default"), "default") + self.assertEqual(p.get("key"), "value") + + def test_update(self): + contents = StringIO( + """ +old=old value +key=value +""" + ) + p = DotProperties(contents) + self.assertEqual(p.get("old"), "old value") + self.assertEqual(p.get("key"), "value") + + new_contents = StringIO( + """ +key=new value +""" + ) + p.update(new_contents) + self.assertEqual(p.get("old"), "old value") + self.assertEqual(p.get("key"), "new value") + + def test_get_list(self): + contents = StringIO( + """ +list.0=A +list.1=B +list.2=C + +order.1=B +order.0=A +order.2=C +""" + ) + p = DotProperties(contents) + self.assertEqual(p.get_list("missing"), []) + self.assertEqual(p.get_list("list"), ["A", "B", "C"]) + self.assertEqual(p.get_list("order"), ["A", "B", "C"]) + + def test_get_list_with_shared_prefix(self): + contents = StringIO( + """ +list.0=A +list.1=B +list.2=C + +list.sublist.1=E +list.sublist.0=D +list.sublist.2=F + +list.sublist.second.0=G + +list.other.0=H +""" + ) + p = DotProperties(contents) + self.assertEqual(p.get_list("list"), ["A", "B", "C"]) + self.assertEqual(p.get_list("list.sublist"), ["D", "E", "F"]) + self.assertEqual(p.get_list("list.sublist.second"), ["G"]) + self.assertEqual(p.get_list("list.other"), ["H"]) + + def test_get_dict(self): + contents = StringIO( + """ +A.title=title A + +B.title=title B +B.url=url B + +C=value +""" + ) + p = DotProperties(contents) + self.assertEqual(p.get_dict("missing"), {}) + self.assertEqual(p.get_dict("A"), {"title": "title A"}) + self.assertEqual(p.get_dict("B"), {"title": "title B", "url": "url B"}) + with self.assertRaises(ValueError): + p.get_dict("A", required_keys=["title", "url"]) + with self.assertRaises(ValueError): + p.get_dict("missing", required_keys=["key"]) + # A key=value pair is considered to root an empty dict. + self.assertEqual(p.get_dict("C"), {}) + with self.assertRaises(ValueError): + p.get_dict("C", required_keys=["missing_key"]) + + def test_get_dict_with_shared_prefix(self): + contents = StringIO( + """ +A.title=title A +A.subdict.title=title A subdict + +B.title=title B +B.url=url B +B.subdict.title=title B subdict +B.subdict.url=url B subdict +""" + ) + p = DotProperties(contents) + self.assertEqual(p.get_dict("A"), {"title": "title A"}) + self.assertEqual(p.get_dict("B"), {"title": "title B", "url": "url B"}) + self.assertEqual(p.get_dict("A.subdict"), {"title": "title A subdict"}) + self.assertEqual( + p.get_dict("B.subdict"), + {"title": "title B subdict", "url": "url B subdict"}, + ) + + def test_get_dict_with_value_prefix(self): + contents = StringIO( + """ +A.default=A +A.default.B=B +A.default.B.ignored=B ignored +A.default.C=C +A.default.C.ignored=C ignored +""" + ) + p = DotProperties(contents) + self.assertEqual(p.get("A.default"), "A") + # This enumerates the properties. + self.assertEqual(p.get_dict("A.default"), {"B": "B", "C": "C"}) + # They can still be fetched directly. + self.assertEqual(p.get("A.default.B"), "B") + self.assertEqual(p.get("A.default.C"), "C") + + def test_unicode(self): + contents = StringIO( + """ +# Danish. +# #### ~~ Søren Munk Skrøder, sskroeder - 2009-05-30 @ #mozmae + +# Korean. +A.title=í•œë©”ì¼ + +# Russian. +list.0 = test +list.1 = Ð¯Ð½Ð´ÐµÐºÑ +""" + ) + p = DotProperties(contents) + self.assertEqual(p.get_dict("A"), {"title": "한메ì¼"}) + self.assertEqual(p.get_list("list"), ["test", "ЯндекÑ"]) + + def test_valid_unicode_from_file(self): + # The contents of valid.properties is identical to the contents of the + # test above. This specifically exercises reading from a file. + p = DotProperties(os.path.join(test_data_path, "valid.properties")) + self.assertEqual(p.get_dict("A"), {"title": "한메ì¼"}) + self.assertEqual(p.get_list("list"), ["test", "ЯндекÑ"]) + + def test_bad_unicode_from_file(self): + # The contents of bad.properties is not valid Unicode; see the comments + # in the file itself for details. + with self.assertRaises(UnicodeDecodeError): + DotProperties(os.path.join(test_data_path, "bad.properties")) + + +if __name__ == "__main__": + main() diff --git a/python/mozbuild/mozbuild/test/test_expression.py b/python/mozbuild/mozbuild/test/test_expression.py new file mode 100644 index 0000000000..535e62bf43 --- /dev/null +++ b/python/mozbuild/mozbuild/test/test_expression.py @@ -0,0 +1,88 @@ +import unittest + +import mozunit + +from mozbuild.preprocessor import Context, Expression + + +class TestContext(unittest.TestCase): + """ + Unit tests for the Context class + """ + + def setUp(self): + self.c = Context() + self.c["FAIL"] = "PASS" + + def test_string_literal(self): + """test string literal, fall-through for undefined var in a Context""" + self.assertEqual(self.c["PASS"], "PASS") + + def test_variable(self): + """test value for defined var in the Context class""" + self.assertEqual(self.c["FAIL"], "PASS") + + def test_in(self): + """test 'var in context' to not fall for fallback""" + self.assertTrue("FAIL" in self.c) + self.assertTrue("PASS" not in self.c) + + +class TestExpression(unittest.TestCase): + """ + Unit tests for the Expression class + evaluate() is called with a context {FAIL: 'PASS'} + """ + + def setUp(self): + self.c = Context() + self.c["FAIL"] = "PASS" + + def test_string_literal(self): + """Test for a string literal in an Expression""" + self.assertEqual(Expression("PASS").evaluate(self.c), "PASS") + + def test_variable(self): + """Test for variable value in an Expression""" + self.assertEqual(Expression("FAIL").evaluate(self.c), "PASS") + + def test_not(self): + """Test for the ! operator""" + self.assertTrue(Expression("!0").evaluate(self.c)) + self.assertTrue(not Expression("!1").evaluate(self.c)) + + def test_equals(self): + """Test for the == operator""" + self.assertTrue(Expression("FAIL == PASS").evaluate(self.c)) + + def test_notequals(self): + """Test for the != operator""" + self.assertTrue(Expression("FAIL != 1").evaluate(self.c)) + + def test_logical_and(self): + """Test for the && operator""" + self.assertTrue(Expression("PASS == PASS && PASS != NOTPASS").evaluate(self.c)) + + def test_logical_or(self): + """Test for the || operator""" + self.assertTrue( + Expression("PASS == NOTPASS || PASS != NOTPASS").evaluate(self.c) + ) + + def test_logical_ops(self): + """Test for the && and || operators precedence""" + # Would evaluate to false if precedence was wrong + self.assertTrue( + Expression("PASS == PASS || PASS != NOTPASS && PASS == NOTPASS").evaluate( + self.c + ) + ) + + def test_defined(self): + """Test for the defined() value""" + self.assertTrue(Expression("defined(FAIL)").evaluate(self.c)) + self.assertTrue(Expression("!defined(PASS)").evaluate(self.c)) + + +if __name__ == "__main__": + mozunit.main() diff --git a/python/mozbuild/mozbuild/test/test_jarmaker.py b/python/mozbuild/mozbuild/test/test_jarmaker.py new file mode 100644 index 0000000000..24a8c7694a --- /dev/null +++ b/python/mozbuild/mozbuild/test/test_jarmaker.py @@ -0,0 +1,493 @@ +# 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/. + +import os +import os.path +import sys +import unittest +from filecmp import dircmp +from shutil import copy2, rmtree +from tempfile import mkdtemp +from zipfile import ZipFile + +import mozunit +import six +from six import StringIO + +from mozbuild.jar import JarMaker + +if sys.platform == "win32": + import ctypes + from ctypes import POINTER, WinError + + DWORD = ctypes.c_ulong + LPDWORD = POINTER(DWORD) + HANDLE = ctypes.c_void_p + GENERIC_READ = 0x80000000 + FILE_SHARE_READ = 0x00000001 + OPEN_EXISTING = 3 + MAX_PATH = 260 + + class FILETIME(ctypes.Structure): + _fields_ = [("dwLowDateTime", DWORD), ("dwHighDateTime", DWORD)] + + class BY_HANDLE_FILE_INFORMATION(ctypes.Structure): + _fields_ = [ + ("dwFileAttributes", DWORD), + ("ftCreationTime", FILETIME), + ("ftLastAccessTime", FILETIME), + ("ftLastWriteTime", FILETIME), + ("dwVolumeSerialNumber", DWORD), + ("nFileSizeHigh", DWORD), + ("nFileSizeLow", DWORD), + ("nNumberOfLinks", DWORD), + ("nFileIndexHigh", DWORD), + ("nFileIndexLow", DWORD), + ] + + # http://msdn.microsoft.com/en-us/library/aa363858 + CreateFile = ctypes.windll.kernel32.CreateFileA + CreateFile.argtypes = [ + ctypes.c_char_p, + DWORD, + DWORD, + ctypes.c_void_p, + DWORD, + DWORD, + HANDLE, + ] + CreateFile.restype = HANDLE + + # http://msdn.microsoft.com/en-us/library/aa364952 + GetFileInformationByHandle = ctypes.windll.kernel32.GetFileInformationByHandle + GetFileInformationByHandle.argtypes = [HANDLE, POINTER(BY_HANDLE_FILE_INFORMATION)] + GetFileInformationByHandle.restype = ctypes.c_int + + # http://msdn.microsoft.com/en-us/library/aa364996 + GetVolumePathName = ctypes.windll.kernel32.GetVolumePathNameA + GetVolumePathName.argtypes = [ctypes.c_char_p, ctypes.c_char_p, DWORD] + GetVolumePathName.restype = ctypes.c_int + + # http://msdn.microsoft.com/en-us/library/aa364993 + GetVolumeInformation = ctypes.windll.kernel32.GetVolumeInformationA + GetVolumeInformation.argtypes = [ + ctypes.c_char_p, + ctypes.c_char_p, + DWORD, + LPDWORD, + LPDWORD, + LPDWORD, + ctypes.c_char_p, + DWORD, + ] + GetVolumeInformation.restype = ctypes.c_int + + +def symlinks_supported(path): + if sys.platform == "win32": + # Add 1 for a trailing backslash if necessary, and 1 for the terminating + # null character. + volpath = ctypes.create_string_buffer(len(path) + 2) + rv = GetVolumePathName(six.ensure_binary(path), volpath, len(volpath)) + if rv == 0: + raise WinError() + + fsname = ctypes.create_string_buffer(MAX_PATH + 1) + rv = GetVolumeInformation( + volpath, None, 0, None, None, None, fsname, len(fsname) + ) + if rv == 0: + raise WinError() + + # Return true only if the fsname is NTFS + return fsname.value == "NTFS" + else: + return True + + +def _getfileinfo(path): + """Return information for the given file. This only works on Windows.""" + fh = CreateFile( + six.ensure_binary(path), + GENERIC_READ, + FILE_SHARE_READ, + None, + OPEN_EXISTING, + 0, + None, + ) + if fh is None: + raise WinError() + info = BY_HANDLE_FILE_INFORMATION() + rv = GetFileInformationByHandle(fh, info) + if rv == 0: + raise WinError() + return info + + +def is_symlink_to(dest, src): + if sys.platform == "win32": + # Check if both are on the same volume and have the same file ID + destinfo = _getfileinfo(dest) + srcinfo = _getfileinfo(src) + return ( + destinfo.dwVolumeSerialNumber == srcinfo.dwVolumeSerialNumber + and destinfo.nFileIndexHigh == srcinfo.nFileIndexHigh + and destinfo.nFileIndexLow == srcinfo.nFileIndexLow + ) + else: + # Read the link and check if it is correct + if not os.path.islink(dest): + return False + target = os.path.abspath(os.readlink(dest)) + abssrc = os.path.abspath(src) + return target == abssrc + + +class _TreeDiff(dircmp): + """Helper to report rich results on difference between two directories.""" + + def _fillDiff(self, dc, rv, basepath="{0}"): + rv["right_only"] += map(lambda l: basepath.format(l), dc.right_only) + rv["left_only"] += map(lambda l: basepath.format(l), dc.left_only) + rv["diff_files"] += map(lambda l: basepath.format(l), dc.diff_files) + rv["funny"] += map(lambda l: basepath.format(l), dc.common_funny) + rv["funny"] += map(lambda l: basepath.format(l), dc.funny_files) + for subdir, _dc in six.iteritems(dc.subdirs): + self._fillDiff(_dc, rv, basepath.format(subdir + "/{0}")) + + def allResults(self, left, right): + rv = {"right_only": [], "left_only": [], "diff_files": [], "funny": []} + self._fillDiff(self, rv) + chunks = [] + if rv["right_only"]: + chunks.append("{0} only in {1}".format(", ".join(rv["right_only"]), right)) + if rv["left_only"]: + chunks.append("{0} only in {1}".format(", ".join(rv["left_only"]), left)) + if rv["diff_files"]: + chunks.append("{0} differ".format(", ".join(rv["diff_files"]))) + if rv["funny"]: + chunks.append("{0} don't compare".format(", ".join(rv["funny"]))) + return "; ".join(chunks) + + +class TestJarMaker(unittest.TestCase): + """ + Unit tests for JarMaker.py + """ + + debug = False # set to True to debug failing tests on disk + + def setUp(self): + self.tmpdir = mkdtemp() + self.srcdir = os.path.join(self.tmpdir, "src") + os.mkdir(self.srcdir) + self.builddir = os.path.join(self.tmpdir, "build") + os.mkdir(self.builddir) + self.refdir = os.path.join(self.tmpdir, "ref") + os.mkdir(self.refdir) + self.stagedir = os.path.join(self.tmpdir, "stage") + os.mkdir(self.stagedir) + + def tearDown(self): + if self.debug: + print(self.tmpdir) + elif sys.platform != "win32": + # can't clean up on windows + rmtree(self.tmpdir) + + def _jar_and_compare(self, infile, **kwargs): + jm = JarMaker(outputFormat="jar") + if "topsourcedir" not in kwargs: + kwargs["topsourcedir"] = self.srcdir + for attr in ("topsourcedir", "sourcedirs"): + if attr in kwargs: + setattr(jm, attr, kwargs[attr]) + jm.makeJar(infile, self.builddir) + cwd = os.getcwd() + os.chdir(self.builddir) + try: + # expand build to stage + for path, dirs, files in os.walk("."): + stagedir = os.path.join(self.stagedir, path) + if not os.path.isdir(stagedir): + os.mkdir(stagedir) + for file in files: + if file.endswith(".jar"): + # expand jar + stagepath = os.path.join(stagedir, file) + os.mkdir(stagepath) + zf = ZipFile(os.path.join(path, file)) + # extractall is only in 2.6, do this manually :-( + for entry_name in zf.namelist(): + segs = entry_name.split("/") + fname = segs.pop() + dname = os.path.join(stagepath, *segs) + if not os.path.isdir(dname): + os.makedirs(dname) + if not fname: + # directory, we're done + continue + _c = zf.read(entry_name) + open(os.path.join(dname, fname), "wb").write(_c) + zf.close() + else: + copy2(os.path.join(path, file), stagedir) + # compare both dirs + os.chdir("..") + td = _TreeDiff("ref", "stage") + return td.allResults("reference", "build") + finally: + os.chdir(cwd) + + def _create_simple_setup(self): + # create src content + jarf = open(os.path.join(self.srcdir, "jar.mn"), "w") + jarf.write( + """test.jar: + dir/foo (bar) +""" + ) + jarf.close() + open(os.path.join(self.srcdir, "bar"), "w").write("content\n") + # create reference + refpath = os.path.join(self.refdir, "chrome", "test.jar", "dir") + os.makedirs(refpath) + open(os.path.join(refpath, "foo"), "w").write("content\n") + + def test_a_simple_jar(self): + """Test a simple jar.mn""" + self._create_simple_setup() + # call JarMaker + rv = self._jar_and_compare( + os.path.join(self.srcdir, "jar.mn"), sourcedirs=[self.srcdir] + ) + self.assertTrue(not rv, rv) + + def test_a_simple_symlink(self): + """Test a simple jar.mn with a symlink""" + if not symlinks_supported(self.srcdir): + raise unittest.SkipTest("symlinks not supported") + + self._create_simple_setup() + jm = JarMaker(outputFormat="symlink") + jm.sourcedirs = [self.srcdir] + jm.topsourcedir = self.srcdir + jm.makeJar(os.path.join(self.srcdir, "jar.mn"), self.builddir) + # All we do is check that srcdir/bar points to builddir/chrome/test/dir/foo + srcbar = os.path.join(self.srcdir, "bar") + destfoo = os.path.join(self.builddir, "chrome", "test", "dir", "foo") + self.assertTrue( + is_symlink_to(destfoo, srcbar), + "{0} is not a symlink to {1}".format(destfoo, srcbar), + ) + + def _create_wildcard_setup(self): + # create src content + jarf = open(os.path.join(self.srcdir, "jar.mn"), "w") + jarf.write( + """test.jar: + dir/bar (*.js) + dir/hoge (qux/*) +""" + ) + jarf.close() + open(os.path.join(self.srcdir, "foo.js"), "w").write("foo.js\n") + open(os.path.join(self.srcdir, "bar.js"), "w").write("bar.js\n") + os.makedirs(os.path.join(self.srcdir, "qux", "foo")) + open(os.path.join(self.srcdir, "qux", "foo", "1"), "w").write("1\n") + open(os.path.join(self.srcdir, "qux", "foo", "2"), "w").write("2\n") + open(os.path.join(self.srcdir, "qux", "baz"), "w").write("baz\n") + # create reference + refpath = os.path.join(self.refdir, "chrome", "test.jar", "dir") + os.makedirs(os.path.join(refpath, "bar")) + os.makedirs(os.path.join(refpath, "hoge", "foo")) + open(os.path.join(refpath, "bar", "foo.js"), "w").write("foo.js\n") + open(os.path.join(refpath, "bar", "bar.js"), "w").write("bar.js\n") + open(os.path.join(refpath, "hoge", "foo", "1"), "w").write("1\n") + open(os.path.join(refpath, "hoge", "foo", "2"), "w").write("2\n") + open(os.path.join(refpath, "hoge", "baz"), "w").write("baz\n") + + def test_a_wildcard_jar(self): + """Test a wildcard in jar.mn""" + self._create_wildcard_setup() + # call JarMaker + rv = self._jar_and_compare( + os.path.join(self.srcdir, "jar.mn"), sourcedirs=[self.srcdir] + ) + self.assertTrue(not rv, rv) + + def test_a_wildcard_symlink(self): + """Test a wildcard in jar.mn with symlinks""" + if not symlinks_supported(self.srcdir): + raise unittest.SkipTest("symlinks not supported") + + self._create_wildcard_setup() + jm = JarMaker(outputFormat="symlink") + jm.sourcedirs = [self.srcdir] + jm.topsourcedir = self.srcdir + jm.makeJar(os.path.join(self.srcdir, "jar.mn"), self.builddir) + + expected_symlinks = { + ("bar", "foo.js"): ("foo.js",), + ("bar", "bar.js"): ("bar.js",), + ("hoge", "foo", "1"): ("qux", "foo", "1"), + ("hoge", "foo", "2"): ("qux", "foo", "2"), + ("hoge", "baz"): ("qux", "baz"), + } + for dest, src in six.iteritems(expected_symlinks): + srcpath = os.path.join(self.srcdir, *src) + destpath = os.path.join(self.builddir, "chrome", "test", "dir", *dest) + self.assertTrue( + is_symlink_to(destpath, srcpath), + "{0} is not a symlink to {1}".format(destpath, srcpath), + ) + + +class Test_relativesrcdir(unittest.TestCase): + def setUp(self): + self.jm = JarMaker() + self.jm.topsourcedir = "/TOPSOURCEDIR" + self.jm.relativesrcdir = "browser/locales" + self.fake_empty_file = StringIO() + self.fake_empty_file.name = "fake_empty_file" + + def tearDown(self): + del self.jm + del self.fake_empty_file + + def test_en_US(self): + jm = self.jm + jm.makeJar(self.fake_empty_file, "/NO_OUTPUT_REQUIRED") + self.assertEqual( + jm.localedirs, + [ + os.path.join( + os.path.abspath("/TOPSOURCEDIR"), "browser/locales", "en-US" + ) + ], + ) + + def test_l10n_no_merge(self): + jm = self.jm + jm.l10nbase = "/L10N_BASE" + jm.makeJar(self.fake_empty_file, "/NO_OUTPUT_REQUIRED") + self.assertEqual(jm.localedirs, [os.path.join("/L10N_BASE", "browser")]) + + def test_l10n_merge(self): + jm = self.jm + jm.l10nbase = "/L10N_MERGE" + jm.makeJar(self.fake_empty_file, "/NO_OUTPUT_REQUIRED") + self.assertEqual( + jm.localedirs, + [ + os.path.join("/L10N_MERGE", "browser"), + ], + ) + + def test_override(self): + jm = self.jm + jm.outputFormat = "flat" # doesn't touch chrome dir without files + jarcontents = StringIO( + """en-US.jar: +relativesrcdir dom/locales: +""" + ) + jarcontents.name = "override.mn" + jm.makeJar(jarcontents, "/NO_OUTPUT_REQUIRED") + self.assertEqual( + jm.localedirs, + [os.path.join(os.path.abspath("/TOPSOURCEDIR"), "dom/locales", "en-US")], + ) + + def test_override_l10n(self): + jm = self.jm + jm.l10nbase = "/L10N_BASE" + jm.outputFormat = "flat" # doesn't touch chrome dir without files + jarcontents = StringIO( + """en-US.jar: +relativesrcdir dom/locales: +""" + ) + jarcontents.name = "override.mn" + jm.makeJar(jarcontents, "/NO_OUTPUT_REQUIRED") + self.assertEqual(jm.localedirs, [os.path.join("/L10N_BASE", "dom")]) + + +class Test_fluent(unittest.TestCase): + """ + Unit tests for JarMaker interaction with Fluent + """ + + debug = False # set to True to debug failing tests on disk + + def setUp(self): + self.tmpdir = mkdtemp() + self.srcdir = os.path.join(self.tmpdir, "src") + os.mkdir(self.srcdir) + self.builddir = os.path.join(self.tmpdir, "build") + os.mkdir(self.builddir) + self.l10nbase = os.path.join(self.tmpdir, "l10n-base") + os.mkdir(self.l10nbase) + self.l10nmerge = os.path.join(self.tmpdir, "l10n-merge") + os.mkdir(self.l10nmerge) + + def tearDown(self): + if self.debug: + print(self.tmpdir) + elif sys.platform != "win32": + # can't clean up on windows + rmtree(self.tmpdir) + + def _create_fluent_setup(self): + # create src content + jarf = open(os.path.join(self.srcdir, "jar.mn"), "w") + jarf.write( + """[localization] test.jar: + app (%app/**/*.ftl) +""" + ) + jarf.close() + appdir = os.path.join(self.srcdir, "app", "locales", "en-US", "app") + os.makedirs(appdir) + open(os.path.join(appdir, "test.ftl"), "w").write("id = Value") + open(os.path.join(appdir, "test2.ftl"), "w").write("id2 = Value 2") + + l10ndir = os.path.join(self.l10nbase, "app", "app") + os.makedirs(l10ndir) + open(os.path.join(l10ndir, "test.ftl"), "w").write("id = L10n Value") + + def test_l10n_not_merge_ftl(self): + """Test that JarMaker doesn't merge source .ftl files""" + self._create_fluent_setup() + jm = JarMaker(outputFormat="symlink") + jm.sourcedirs = [self.srcdir] + jm.topsourcedir = self.srcdir + jm.l10nbase = self.l10nbase + jm.l10nmerge = self.l10nmerge + jm.relativesrcdir = "app/locales" + jm.makeJar(os.path.join(self.srcdir, "jar.mn"), self.builddir) + + # test.ftl should be taken from the l10ndir, since it is present there + destpath = os.path.join( + self.builddir, "localization", "test", "app", "test.ftl" + ) + srcpath = os.path.join(self.l10nbase, "app", "app", "test.ftl") + self.assertTrue( + is_symlink_to(destpath, srcpath), + "{0} should be a symlink to {1}".format(destpath, srcpath), + ) + + # test2.ftl on the other hand, is only present in en-US dir, and should + # not be linked from the build dir + destpath = os.path.join( + self.builddir, "localization", "test", "app", "test2.ftl" + ) + self.assertFalse( + os.path.isfile(destpath), "test2.ftl should not be taken from en-US" + ) + + +if __name__ == "__main__": + mozunit.main() diff --git a/python/mozbuild/mozbuild/test/test_licenses.py b/python/mozbuild/mozbuild/test/test_licenses.py new file mode 100644 index 0000000000..9f3f12d423 --- /dev/null +++ b/python/mozbuild/mozbuild/test/test_licenses.py @@ -0,0 +1,33 @@ +import unittest + +import mozunit + +from mozbuild.vendor.vendor_rust import VendorRust + + +class TestLicenses(unittest.TestCase): + """ + Unit tests for the Rust Vendoring stuff + """ + + def setUp(self): + pass + + def tearDown(self): + pass + + def testLicense(self): + self.assertEqual(VendorRust.runtime_license("", "Apache-2.0"), True) + self.assertEqual(VendorRust.runtime_license("", "MIT"), True) + self.assertEqual(VendorRust.runtime_license("", "GPL"), False) + self.assertEqual(VendorRust.runtime_license("", "MIT /GPL"), True) + self.assertEqual(VendorRust.runtime_license("", "GPL/ Proprietary"), False) + self.assertEqual(VendorRust.runtime_license("", "GPL AND MIT"), False) + self.assertEqual(VendorRust.runtime_license("", "ISC\tAND\tMIT"), False) + self.assertEqual(VendorRust.runtime_license("", "GPL OR MIT"), True) + self.assertEqual(VendorRust.runtime_license("", "ALLIGATOR MIT"), False) + pass + + +if __name__ == "__main__": + mozunit.main() diff --git a/python/mozbuild/mozbuild/test/test_line_endings.py b/python/mozbuild/mozbuild/test/test_line_endings.py new file mode 100644 index 0000000000..f8cdd89174 --- /dev/null +++ b/python/mozbuild/mozbuild/test/test_line_endings.py @@ -0,0 +1,45 @@ +import unittest + +import mozunit +from mozfile import NamedTemporaryFile +from six import StringIO + +from mozbuild.preprocessor import Preprocessor + + +class TestLineEndings(unittest.TestCase): + """ + Unit tests for the Context class + """ + + def setUp(self): + self.pp = Preprocessor() + self.pp.out = StringIO() + self.f = NamedTemporaryFile(mode="wb") + + def tearDown(self): + self.f.close() + + def createFile(self, lineendings): + for line, ending in zip([b"a", b"#literal b", b"c"], lineendings): + self.f.write(line + ending) + self.f.flush() + + def testMac(self): + self.createFile([b"\x0D"] * 3) + self.pp.do_include(self.f.name) + self.assertEqual(self.pp.out.getvalue(), "a\nb\nc\n") + + def testUnix(self): + self.createFile([b"\x0A"] * 3) + self.pp.do_include(self.f.name) + self.assertEqual(self.pp.out.getvalue(), "a\nb\nc\n") + + def testWindows(self): + self.createFile([b"\x0D\x0A"] * 3) + self.pp.do_include(self.f.name) + self.assertEqual(self.pp.out.getvalue(), "a\nb\nc\n") + + +if __name__ == "__main__": + mozunit.main() diff --git a/python/mozbuild/mozbuild/test/test_makeutil.py b/python/mozbuild/mozbuild/test/test_makeutil.py new file mode 100644 index 0000000000..524851bfbd --- /dev/null +++ b/python/mozbuild/mozbuild/test/test_makeutil.py @@ -0,0 +1,164 @@ +# 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/. + +import os +import unittest + +from mozunit import main +from six import StringIO + +from mozbuild.makeutil import Makefile, Rule, read_dep_makefile, write_dep_makefile + + +class TestMakefile(unittest.TestCase): + def test_rule(self): + out = StringIO() + rule = Rule() + rule.dump(out) + self.assertEqual(out.getvalue(), "") + + out = StringIO() + rule.add_targets(["foo", "bar"]) + rule.dump(out) + self.assertEqual(out.getvalue(), "foo bar:\n") + + out = StringIO() + rule.add_targets(["baz"]) + rule.add_dependencies(["qux", "hoge", "piyo"]) + rule.dump(out) + self.assertEqual(out.getvalue(), "foo bar baz: qux hoge piyo\n") + + out = StringIO() + rule = Rule(["foo", "bar"]) + rule.add_dependencies(["baz"]) + rule.add_commands(["echo $@"]) + rule.add_commands(["$(BAZ) -o $@ $<", "$(TOUCH) $@"]) + rule.dump(out) + self.assertEqual( + out.getvalue(), + "foo bar: baz\n" + + "\techo $@\n" + + "\t$(BAZ) -o $@ $<\n" + + "\t$(TOUCH) $@\n", + ) + + out = StringIO() + rule = Rule(["foo"]) + rule.add_dependencies(["bar", "foo", "baz"]) + rule.dump(out) + self.assertEqual(out.getvalue(), "foo: bar baz\n") + + out = StringIO() + rule.add_targets(["bar"]) + rule.dump(out) + self.assertEqual(out.getvalue(), "foo bar: baz\n") + + out = StringIO() + rule.add_targets(["bar"]) + rule.dump(out) + self.assertEqual(out.getvalue(), "foo bar: baz\n") + + out = StringIO() + rule.add_dependencies(["bar"]) + rule.dump(out) + self.assertEqual(out.getvalue(), "foo bar: baz\n") + + out = StringIO() + rule.add_dependencies(["qux"]) + rule.dump(out) + self.assertEqual(out.getvalue(), "foo bar: baz qux\n") + + out = StringIO() + rule.add_dependencies(["qux"]) + rule.dump(out) + self.assertEqual(out.getvalue(), "foo bar: baz qux\n") + + out = StringIO() + rule.add_dependencies(["hoge", "hoge"]) + rule.dump(out) + self.assertEqual(out.getvalue(), "foo bar: baz qux hoge\n") + + out = StringIO() + rule.add_targets(["fuga", "fuga"]) + rule.dump(out) + self.assertEqual(out.getvalue(), "foo bar fuga: baz qux hoge\n") + + def test_makefile(self): + out = StringIO() + mk = Makefile() + rule = mk.create_rule(["foo"]) + rule.add_dependencies(["bar", "baz", "qux"]) + rule.add_commands(["echo foo"]) + rule = mk.create_rule().add_targets(["bar", "baz"]) + rule.add_dependencies(["hoge"]) + rule.add_commands(["echo $@"]) + mk.dump(out, removal_guard=False) + self.assertEqual( + out.getvalue(), + "foo: bar baz qux\n" + "\techo foo\n" + "bar baz: hoge\n" + "\techo $@\n", + ) + + out = StringIO() + mk.dump(out) + self.assertEqual( + out.getvalue(), + "foo: bar baz qux\n" + + "\techo foo\n" + + "bar baz: hoge\n" + + "\techo $@\n" + + "hoge qux:\n", + ) + + def test_statement(self): + out = StringIO() + mk = Makefile() + mk.create_rule(["foo"]).add_dependencies(["bar"]).add_commands(["echo foo"]) + mk.add_statement("BAR = bar") + mk.create_rule(["$(BAR)"]).add_commands(["echo $@"]) + mk.dump(out, removal_guard=False) + self.assertEqual( + out.getvalue(), + "foo: bar\n" + "\techo foo\n" + "BAR = bar\n" + "$(BAR):\n" + "\techo $@\n", + ) + + @unittest.skipIf(os.name != "nt", "Test only applicable on Windows.") + def test_path_normalization(self): + out = StringIO() + mk = Makefile() + rule = mk.create_rule(["c:\\foo"]) + rule.add_dependencies(["c:\\bar", "c:\\baz\\qux"]) + rule.add_commands(["echo c:\\foo"]) + mk.dump(out) + self.assertEqual( + out.getvalue(), + "c:/foo: c:/bar c:/baz/qux\n" + "\techo c:\\foo\n" + "c:/bar c:/baz/qux:\n", + ) + + def test_read_dep_makefile(self): + input = StringIO( + os.path.abspath("foo") + + ": bar\n" + + "baz qux: \\ \n" + + "hoge \\\n" + + "piyo \\\n" + + "fuga\n" + + "fuga:\n" + ) + result = list(read_dep_makefile(input)) + self.assertEqual(len(result), 2) + self.assertEqual( + list(result[0].targets()), [os.path.abspath("foo").replace(os.sep, "/")] + ) + self.assertEqual(list(result[0].dependencies()), ["bar"]) + self.assertEqual(list(result[1].targets()), ["baz", "qux"]) + self.assertEqual(list(result[1].dependencies()), ["hoge", "piyo", "fuga"]) + + def test_write_dep_makefile(self): + out = StringIO() + write_dep_makefile(out, "target", ["b", "c", "a"]) + self.assertEqual(out.getvalue(), "target: b c a\n" + "a b c:\n") + + +if __name__ == "__main__": + main() diff --git a/python/mozbuild/mozbuild/test/test_manifest.py b/python/mozbuild/mozbuild/test/test_manifest.py new file mode 100644 index 0000000000..e5675aba36 --- /dev/null +++ b/python/mozbuild/mozbuild/test/test_manifest.py @@ -0,0 +1,2081 @@ +# 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/. + +import unittest + +import mozfile +from mozunit import main + +from mozbuild.vendor.moz_yaml import MozYamlVerifyError, load_moz_yaml + + +class TestManifest(unittest.TestCase): + def process_test_vectors(self, test_vectors): + index = 0 + for vector in test_vectors: + print("Testing index", index) + expected, yaml = vector + with mozfile.NamedTemporaryFile() as tf: + tf.write(yaml) + tf.flush() + if expected == "exception": + with self.assertRaises(MozYamlVerifyError): + load_moz_yaml(tf.name, require_license_file=False) + else: + self.assertDictEqual( + load_moz_yaml(tf.name, require_license_file=False), expected + ) + index += 1 + + # =========================================================================================== + def test_simple(self): + simple_dict = { + "schema": "1", + "origin": { + "description": "2D Graphics Library", + "license": ["MPL-1.1", "LGPL-2.1"], + "name": "cairo", + "release": "version 1.6.4", + "revision": "AA001122334455", + "url": "https://www.cairographics.org/", + }, + "bugzilla": {"component": "Graphics", "product": "Core"}, + } + + self.process_test_vectors( + [ + ( + simple_dict, + b""" +--- +schema: 1 +origin: + name: cairo + description: 2D Graphics Library + url: https://www.cairographics.org/ + release: version 1.6.4 + license: + - MPL-1.1 + - LGPL-2.1 + revision: AA001122334455 +bugzilla: + product: Core + component: Graphics + """.strip(), + ), + ( + simple_dict, + b""" +schema: 1 +origin: + name: cairo + description: 2D Graphics Library + url: https://www.cairographics.org/ + release: version 1.6.4 + license: + - MPL-1.1 + - LGPL-2.1 + revision: AA001122334455 +bugzilla: + product: Core + component: Graphics + """.strip(), + ), + ] + ) + + # =========================================================================================== + def test_updatebot(self): + self.process_test_vectors( + [ + ( + { + "schema": "1", + "origin": { + "description": "2D Graphics Library", + "license": ["MPL-1.1", "LGPL-2.1"], + "name": "cairo", + "release": "version 1.6.4", + "revision": "AA001122334455", + "url": "https://www.cairographics.org/", + }, + "bugzilla": {"component": "Graphics", "product": "Core"}, + "updatebot": { + "maintainer-phab": "tjr", + "maintainer-bz": "a@example.com", + }, + }, + b""" +--- +schema: 1 +origin: + name: cairo + description: 2D Graphics Library + url: https://www.cairographics.org/ + release: version 1.6.4 + license: + - MPL-1.1 + - LGPL-2.1 + revision: AA001122334455 +bugzilla: + product: Core + component: Graphics +updatebot: + maintainer-phab: tjr + maintainer-bz: a@example.com + """.strip(), + ), + # ------------------------------------------------- + ( + { + "schema": "1", + "origin": { + "description": "2D Graphics Library", + "license": ["MPL-1.1", "LGPL-2.1"], + "name": "cairo", + "release": "version 1.6.4", + "revision": "001122334455", + "url": "https://www.cairographics.org/", + }, + "bugzilla": {"component": "Graphics", "product": "Core"}, + "updatebot": { + "maintainer-phab": "tjr", + "maintainer-bz": "a@example.com", + }, + }, + b""" +--- +schema: 1 +origin: + name: cairo + description: 2D Graphics Library + url: https://www.cairographics.org/ + release: version 1.6.4 + license: + - MPL-1.1 + - LGPL-2.1 + revision: 001122334455 +bugzilla: + product: Core + component: Graphics +updatebot: + maintainer-phab: tjr + maintainer-bz: a@example.com + """.strip(), + ), + # ------------------------------------------------- + ( + { + "schema": "1", + "origin": { + "description": "2D Graphics Library", + "license": ["MPL-1.1", "LGPL-2.1"], + "name": "cairo", + "release": "version 1.6.4", + "revision": "001122334455", + "url": "https://www.cairographics.org/", + }, + "bugzilla": {"component": "Graphics", "product": "Core"}, + "updatebot": { + "try-preset": "foo", + "maintainer-phab": "tjr", + "maintainer-bz": "a@example.com", + }, + }, + b""" +--- +schema: 1 +origin: + name: cairo + description: 2D Graphics Library + url: https://www.cairographics.org/ + release: version 1.6.4 + license: + - MPL-1.1 + - LGPL-2.1 + revision: 001122334455 +bugzilla: + product: Core + component: Graphics +updatebot: + try-preset: foo + maintainer-phab: tjr + maintainer-bz: a@example.com + """.strip(), + ), + # ------------------------------------------------- + ( + { + "schema": "1", + "origin": { + "description": "2D Graphics Library", + "license": ["MPL-1.1", "LGPL-2.1"], + "name": "cairo", + "release": "version 1.6.4", + "revision": "AA001122334455", + "url": "https://www.cairographics.org/", + }, + "bugzilla": {"component": "Graphics", "product": "Core"}, + "vendoring": { + "url": "https://example.com", + "source-hosting": "gitlab", + }, + "updatebot": { + "maintainer-phab": "tjr", + "maintainer-bz": "a@example.com", + "fuzzy-query": "!linux64", + "tasks": [{"type": "commit-alert"}], + }, + }, + b""" +--- +schema: 1 +origin: + name: cairo + description: 2D Graphics Library + url: https://www.cairographics.org/ + release: version 1.6.4 + license: + - MPL-1.1 + - LGPL-2.1 + revision: AA001122334455 +vendoring: + url: https://example.com + source-hosting: gitlab +bugzilla: + product: Core + component: Graphics +updatebot: + fuzzy-query: "!linux64" + maintainer-phab: tjr + maintainer-bz: a@example.com + tasks: + - type: commit-alert + """.strip(), + ), + # ------------------------------------------------- + ( + "exception", + b""" +--- +schema: 1 +origin: + name: cairo + description: 2D Graphics Library + url: https://www.cairographics.org/ + release: version 1.6.4 + license: + - MPL-1.1 + - LGPL-2.1 + revision: AA001122334455 +vendoring: + url: https://example.com + source-hosting: gitlab +bugzilla: + product: Core + component: Graphics +updatebot: + try-preset: foo + fuzzy-query: "!linux64" + maintainer-phab: tjr + maintainer-bz: a@example.com + tasks: + - type: commit-alert + """.strip(), + ), + # ------------------------------------------------- + ( + { + "schema": "1", + "origin": { + "description": "2D Graphics Library", + "license": ["MPL-1.1", "LGPL-2.1"], + "name": "cairo", + "release": "version 1.6.4", + "revision": "AA001122334455", + "url": "https://www.cairographics.org/", + }, + "bugzilla": {"component": "Graphics", "product": "Core"}, + "vendoring": { + "url": "https://example.com", + "source-hosting": "gitlab", + }, + "updatebot": { + "maintainer-phab": "tjr", + "maintainer-bz": "a@example.com", + "fuzzy-paths": ["dir1/", "dir2"], + "tasks": [{"type": "commit-alert"}], + }, + }, + b""" +--- +schema: 1 +origin: + name: cairo + description: 2D Graphics Library + url: https://www.cairographics.org/ + release: version 1.6.4 + license: + - MPL-1.1 + - LGPL-2.1 + revision: AA001122334455 +vendoring: + url: https://example.com + source-hosting: gitlab +bugzilla: + product: Core + component: Graphics +updatebot: + fuzzy-paths: + - dir1/ + - dir2 + maintainer-phab: tjr + maintainer-bz: a@example.com + tasks: + - type: commit-alert + """.strip(), + ), + # ------------------------------------------------- + ( + { + "schema": "1", + "origin": { + "description": "2D Graphics Library", + "license": ["MPL-1.1", "LGPL-2.1"], + "name": "cairo", + "release": "version 1.6.4", + "revision": "AA001122334455", + "url": "https://www.cairographics.org/", + }, + "bugzilla": {"component": "Graphics", "product": "Core"}, + "vendoring": { + "url": "https://example.com", + "source-hosting": "gitlab", + }, + "updatebot": { + "maintainer-phab": "tjr", + "maintainer-bz": "a@example.com", + "fuzzy-paths": ["dir1/"], + "tasks": [{"type": "commit-alert"}], + }, + }, + b""" +--- +schema: 1 +origin: + name: cairo + description: 2D Graphics Library + url: https://www.cairographics.org/ + release: version 1.6.4 + license: + - MPL-1.1 + - LGPL-2.1 + revision: AA001122334455 +vendoring: + url: https://example.com + source-hosting: gitlab +bugzilla: + product: Core + component: Graphics +updatebot: + fuzzy-paths: ['dir1/'] + maintainer-phab: tjr + maintainer-bz: a@example.com + tasks: + - type: commit-alert + """.strip(), + ), + # ------------------------------------------------- + ( + { + "schema": "1", + "origin": { + "description": "2D Graphics Library", + "license": ["MPL-1.1", "LGPL-2.1"], + "name": "cairo", + "release": "version 1.6.4", + "revision": "AA001122334455", + "url": "https://www.cairographics.org/", + }, + "bugzilla": {"component": "Graphics", "product": "Core"}, + "vendoring": { + "url": "https://example.com", + "source-hosting": "gitlab", + "tracking": "commit", + "flavor": "rust", + }, + "updatebot": { + "maintainer-phab": "tjr", + "maintainer-bz": "a@example.com", + "tasks": [ + {"type": "commit-alert", "frequency": "release"}, + { + "type": "vendoring", + "enabled": False, + "cc": ["b@example.com"], + "needinfo": ["c@example.com"], + "frequency": "1 weeks", + "platform": "windows", + }, + ], + }, + }, + b""" +--- +schema: 1 +origin: + name: cairo + description: 2D Graphics Library + url: https://www.cairographics.org/ + release: version 1.6.4 + license: + - MPL-1.1 + - LGPL-2.1 + revision: AA001122334455 +vendoring: + url: https://example.com + tracking: commit + source-hosting: gitlab + flavor: rust +bugzilla: + product: Core + component: Graphics +updatebot: + maintainer-phab: tjr + maintainer-bz: a@example.com + tasks: + - type: commit-alert + frequency: release + - type: vendoring + enabled: False + cc: ["b@example.com"] + needinfo: ["c@example.com"] + frequency: 1 weeks + platform: windows + """.strip(), + ), + # ------------------------------------------------- + ( + { + "schema": "1", + "origin": { + "description": "2D Graphics Library", + "license": ["MPL-1.1", "LGPL-2.1"], + "name": "cairo", + "release": "version 1.6.4", + "revision": "AA001122334455", + "url": "https://www.cairographics.org/", + }, + "bugzilla": {"component": "Graphics", "product": "Core"}, + "vendoring": { + "url": "https://example.com", + "source-hosting": "gitlab", + "tracking": "tag", + "flavor": "rust", + }, + "updatebot": { + "maintainer-phab": "tjr", + "maintainer-bz": "a@example.com", + "tasks": [ + {"type": "commit-alert", "frequency": "release"}, + { + "type": "vendoring", + "enabled": False, + "cc": ["b@example.com"], + "needinfo": ["c@example.com"], + "frequency": "1 weeks, 4 commits", + "platform": "windows", + }, + ], + }, + }, + b""" +--- +schema: 1 +origin: + name: cairo + description: 2D Graphics Library + url: https://www.cairographics.org/ + release: version 1.6.4 + license: + - MPL-1.1 + - LGPL-2.1 + revision: AA001122334455 +vendoring: + url: https://example.com + tracking: tag + source-hosting: gitlab + flavor: rust +bugzilla: + product: Core + component: Graphics +updatebot: + maintainer-phab: tjr + maintainer-bz: a@example.com + tasks: + - type: commit-alert + frequency: release + - type: vendoring + enabled: False + cc: ["b@example.com"] + needinfo: ["c@example.com"] + frequency: 1 weeks, 4 commits + platform: windows + """.strip(), + ), + # ------------------------------------------------- + ( + "exception", # rust flavor cannot use update-actions + b""" +--- +schema: 1 +origin: + name: cairo + description: 2D Graphics Library + url: https://www.cairographics.org/ + release: version 1.6.4 + license: + - MPL-1.1 + - LGPL-2.1 + revision: AA001122334455 +vendoring: + url: https://example.com + tracking: tag + source-hosting: gitlab + flavor: rust + update-actions: + - action: move-file + from: foo + to: bar +bugzilla: + product: Core + component: Graphics +updatebot: + maintainer-phab: tjr + maintainer-bz: a@example.com + tasks: + - type: commit-alert + frequency: release + - type: vendoring + enabled: False + cc: ["b@example.com"] + needinfo: ["c@example.com"] + frequency: 1 weeks, 4 commits + platform: windows + """.strip(), + ), + # ------------------------------------------------- + ( + { + "schema": "1", + "origin": { + "description": "2D Graphics Library", + "license": ["MPL-1.1", "LGPL-2.1"], + "name": "cairo", + "release": "version 1.6.4", + "revision": "AA001122334455", + "url": "https://www.cairographics.org/", + }, + "bugzilla": {"component": "Graphics", "product": "Core"}, + "vendoring": { + "url": "https://example.com", + "source-hosting": "gitlab", + }, + "updatebot": { + "maintainer-phab": "tjr", + "maintainer-bz": "a@example.com", + "tasks": [ + { + "type": "vendoring", + "enabled": False, + "cc": ["b@example.com", "c@example.com"], + "needinfo": ["d@example.com", "e@example.com"], + "frequency": "every", + }, + { + "type": "commit-alert", + "filter": "none", + "source-extensions": [".c", ".cpp"], + "frequency": "2 weeks", + "platform": "linux", + }, + ], + }, + }, + b""" +--- +schema: 1 +origin: + name: cairo + description: 2D Graphics Library + url: https://www.cairographics.org/ + release: version 1.6.4 + license: + - MPL-1.1 + - LGPL-2.1 + revision: AA001122334455 +vendoring: + url: https://example.com + source-hosting: gitlab +bugzilla: + product: Core + component: Graphics +updatebot: + maintainer-phab: tjr + maintainer-bz: a@example.com + tasks: + - type: vendoring + enabled: False + cc: + - b@example.com + - c@example.com + needinfo: + - d@example.com + - e@example.com + frequency: every + - type: commit-alert + filter: none + frequency: 2 weeks + platform: linux + source-extensions: + - .c + - .cpp + """.strip(), + ), + # ------------------------------------------------- + ( + { + "schema": "1", + "origin": { + "description": "2D Graphics Library", + "license": ["MPL-1.1", "LGPL-2.1"], + "name": "cairo", + "release": "version 1.6.4", + "revision": "AA001122334455", + "url": "https://www.cairographics.org/", + }, + "bugzilla": {"component": "Graphics", "product": "Core"}, + "vendoring": { + "url": "https://example.com", + "source-hosting": "gitlab", + }, + "updatebot": { + "maintainer-phab": "tjr", + "maintainer-bz": "a@example.com", + "tasks": [ + { + "type": "vendoring", + "enabled": False, + "cc": ["b@example.com", "c@example.com"], + "needinfo": ["d@example.com", "e@example.com"], + "frequency": "every", + }, + { + "type": "commit-alert", + "filter": "none", + "source-extensions": [".c", ".cpp"], + "frequency": "2 commits", + "platform": "linux", + }, + ], + }, + }, + b""" +--- +schema: 1 +origin: + name: cairo + description: 2D Graphics Library + url: https://www.cairographics.org/ + release: version 1.6.4 + license: + - MPL-1.1 + - LGPL-2.1 + revision: AA001122334455 +vendoring: + url: https://example.com + source-hosting: gitlab +bugzilla: + product: Core + component: Graphics +updatebot: + maintainer-phab: tjr + maintainer-bz: a@example.com + tasks: + - type: vendoring + enabled: False + cc: + - b@example.com + - c@example.com + needinfo: + - d@example.com + - e@example.com + frequency: every + - type: commit-alert + filter: none + frequency: 2 commits + platform: linux + source-extensions: + - .c + - .cpp + """.strip(), + ), + # ------------------------------------------------- + ( + { + "schema": "1", + "origin": { + "description": "2D Graphics Library", + "license": ["MPL-1.1", "LGPL-2.1"], + "name": "cairo", + "release": "version 1.6.4", + "revision": "AA001122334455", + "url": "https://www.cairographics.org/", + }, + "bugzilla": {"component": "Graphics", "product": "Core"}, + "vendoring": { + "url": "https://example.com", + "source-hosting": "gitlab", + }, + "updatebot": { + "maintainer-phab": "tjr", + "maintainer-bz": "a@example.com", + "tasks": [ + { + "type": "vendoring", + "enabled": False, + "cc": ["b@example.com", "c@example.com"], + "needinfo": ["d@example.com", "e@example.com"], + "frequency": "every", + "blocking": "1234", + }, + { + "type": "commit-alert", + "filter": "none", + "source-extensions": [".c", ".cpp"], + "frequency": "2 commits", + "platform": "linux", + }, + ], + }, + }, + b""" +--- +schema: 1 +origin: + name: cairo + description: 2D Graphics Library + url: https://www.cairographics.org/ + release: version 1.6.4 + license: + - MPL-1.1 + - LGPL-2.1 + revision: AA001122334455 +vendoring: + url: https://example.com + source-hosting: gitlab +bugzilla: + product: Core + component: Graphics +updatebot: + maintainer-phab: tjr + maintainer-bz: a@example.com + tasks: + - type: vendoring + enabled: False + cc: + - b@example.com + - c@example.com + needinfo: + - d@example.com + - e@example.com + frequency: every + blocking: 1234 + - type: commit-alert + filter: none + frequency: 2 commits + platform: linux + source-extensions: + - .c + - .cpp + """.strip(), + ), + # ------------------------------------------------- + ( + "exception", + b""" +--- +schema: 1 +origin: + name: cairo + description: 2D Graphics Library + url: https://www.cairographics.org/ + release: version 1.6.4 + license: + - MPL-1.1 + - LGPL-2.1 + revision: AA001122334455 +vendoring: + url: https://example.com + source-hosting: gitlab +bugzilla: + product: Core + component: Graphics +updatebot: + maintainer-phab: tjr + maintainer-bz: a@example.com + tasks: + - type: vendoring + branch: foo + enabled: False + cc: + - b@example.com + - c@example.com + needinfo: + - d@example.com + - e@example.com + frequency: every + blocking: 1234 + - type: commit-alert + filter: none + frequency: 2 commits + platform: linux + source-extensions: + - .c + - .cpp + """.strip(), + ), + # ------------------------------------------------- + ( + { + "schema": "1", + "origin": { + "license": ["MPL-1.1", "LGPL-2.1"], + "name": "cairo", + "description": "2D Graphics Library", + "url": "https://www.cairographics.org/", + "release": "version 1.6.4", + "revision": "AA001122334455", + }, + "bugzilla": {"component": "Graphics", "product": "Core"}, + "vendoring": { + "url": "https://example.com", + "source-hosting": "gitlab", + "flavor": "individual-files", + "individual-files": [ + {"upstream": "foo", "destination": "bar"} + ], + }, + }, + b""" +--- +schema: 1 +origin: + name: cairo + description: 2D Graphics Library + url: https://www.cairographics.org/ + release: version 1.6.4 + license: + - MPL-1.1 + - LGPL-2.1 + revision: AA001122334455 +vendoring: + url: https://example.com + source-hosting: gitlab + flavor: individual-files + individual-files: + - upstream: foo + destination: bar +bugzilla: + product: Core + component: Graphics + """.strip(), + ), + # ------------------------------------------------- + ( + { + "schema": "1", + "origin": { + "license": ["MPL-1.1", "LGPL-2.1"], + "name": "cairo", + "description": "2D Graphics Library", + "url": "https://www.cairographics.org/", + "release": "version 1.6.4", + "revision": "AA001122334455", + }, + "bugzilla": {"component": "Graphics", "product": "Core"}, + "vendoring": { + "url": "https://example.com", + "source-hosting": "gitlab", + "flavor": "individual-files", + "individual-files": [ + {"upstream": "foo", "destination": "bar"} + ], + "update-actions": [ + {"action": "move-file", "from": "foo", "to": "bar"} + ], + }, + }, + b""" +--- +schema: 1 +origin: + name: cairo + description: 2D Graphics Library + url: https://www.cairographics.org/ + release: version 1.6.4 + license: + - MPL-1.1 + - LGPL-2.1 + revision: AA001122334455 +vendoring: + url: https://example.com + source-hosting: gitlab + flavor: individual-files + individual-files: + - upstream: foo + destination: bar + update-actions: + - action: move-file + from: foo + to: bar +bugzilla: + product: Core + component: Graphics + """.strip(), + ), + # ------------------------------------------------- + ( + { + "schema": "1", + "origin": { + "license": ["MPL-1.1", "LGPL-2.1"], + "name": "cairo", + "description": "2D Graphics Library", + "url": "https://www.cairographics.org/", + "release": "version 1.6.4", + "revision": "AA001122334455", + }, + "bugzilla": {"component": "Graphics", "product": "Core"}, + "vendoring": { + "url": "https://example.com", + "source-hosting": "gitlab", + "flavor": "individual-files", + "individual-files-default-destination": "bar", + "individual-files-default-upstream": "foo", + "individual-files-list": ["foo", "bar"], + "update-actions": [ + {"action": "move-file", "from": "foo", "to": "bar"} + ], + }, + }, + b""" +--- +schema: 1 +origin: + name: cairo + description: 2D Graphics Library + url: https://www.cairographics.org/ + release: version 1.6.4 + license: + - MPL-1.1 + - LGPL-2.1 + revision: AA001122334455 +vendoring: + url: https://example.com + source-hosting: gitlab + flavor: individual-files + individual-files-default-upstream: foo + individual-files-default-destination: bar + individual-files-list: + - foo + - bar + update-actions: + - action: move-file + from: foo + to: bar +bugzilla: + product: Core + component: Graphics + """.strip(), + ), + # ------------------------------------------------- + ( + "exception", # can't have both types of indidivudal-files list + b""" +--- +schema: 1 +origin: + name: cairo + description: 2D Graphics Library + url: https://www.cairographics.org/ + release: version 1.6.4 + license: + - MPL-1.1 + - LGPL-2.1 + revision: AA001122334455 +vendoring: + url: https://example.com + source-hosting: gitlab + flavor: individual-files + individual-files-list: + - foo + individual-files: + - upstream: foo + destination: bar + update-actions: + - action: move-file + from: foo + to: bar +bugzilla: + product: Core + component: Graphics + """.strip(), + ), + # ------------------------------------------------- + ( + "exception", # can't have indidivudal-files-default-upstream + b""" +--- +schema: 1 +origin: + name: cairo + description: 2D Graphics Library + url: https://www.cairographics.org/ + release: version 1.6.4 + license: + - MPL-1.1 + - LGPL-2.1 + revision: AA001122334455 +vendoring: + url: https://example.com + source-hosting: gitlab + flavor: individual-files + indidivudal-files-default-upstream: foo + individual-files: + - upstream: foo + destination: bar + update-actions: + - action: move-file + from: foo + to: bar +bugzilla: + product: Core + component: Graphics + """.strip(), + ), + # ------------------------------------------------- + ( + "exception", # must have indidivudal-files-default-upstream + b""" +--- +schema: 1 +origin: + name: cairo + description: 2D Graphics Library + url: https://www.cairographics.org/ + release: version 1.6.4 + license: + - MPL-1.1 + - LGPL-2.1 + revision: AA001122334455 +vendoring: + url: https://example.com + source-hosting: gitlab + flavor: individual-files + indidivudal-files-default-destination: foo + individual-files-list: + - foo + - bar + update-actions: + - action: move-file + from: foo + to: bar +bugzilla: + product: Core + component: Graphics + """.strip(), + ), + # ------------------------------------------------- + ( + "exception", + b""" +--- +schema: 1 +origin: + name: cairo + description: 2D Graphics Library + url: https://www.cairographics.org/ + release: version 1.6.4 + license: + - MPL-1.1 + - LGPL-2.1 + revision: AA001122334455 +vendoring: + url: https://example.com + source-hosting: gitlab + tracking: tag + flavor: individual-files + individual-files: + - upstream-src: foo + dst: bar +bugzilla: + product: Core + component: Graphics + """.strip(), + ), + # ------------------------------------------------- + ( + "exception", + b""" +--- +schema: 1 +origin: + name: cairo + description: 2D Graphics Library + url: https://www.cairographics.org/ + release: version 1.6.4 + license: + - MPL-1.1 + - LGPL-2.1 + revision: AA001122334455 +vendoring: + url: https://example.com + source-hosting: gitlab + flavor: individual-files +bugzilla: + product: Core + component: Graphics + """.strip(), + ), + # ------------------------------------------------- + ( + "exception", + b""" +--- +schema: 1 +origin: + name: cairo + description: 2D Graphics Library + url: https://www.cairographics.org/ + release: version 1.6.4 + license: + - MPL-1.1 + - LGPL-2.1 + revision: AA001122334455 +vendoring: + url: https://example.com + source-hosting: gitlab + flavor: rust + individual-files: + - upstream: foo + destination: bar +bugzilla: + product: Core + component: Graphics + """.strip(), + ), + # ------------------------------------------------- + ( + "exception", + b""" +--- +schema: 1 +origin: + name: cairo + description: 2D Graphics Library + url: https://www.cairographics.org/ + release: version 1.6.4 + license: + - MPL-1.1 + - LGPL-2.1 + revision: AA001122334455 +vendoring: + url: https://example.com + source-hosting: gitlab + flavor: rust + include: + - foo +bugzilla: + product: Core + component: Graphics + """.strip(), + ), + # ------------------------------------------------- + ( + "exception", + b""" +--- +schema: 1 +origin: + name: cairo + description: 2D Graphics Library + url: https://www.cairographics.org/ + release: version 1.6.4 + license: + - MPL-1.1 + - LGPL-2.1 + revision: AA001122334455 +vendoring: + url: https://example.com + source-hosting: gitlab +bugzilla: + product: Core + component: Graphics +updatebot: + maintainer-phab: tjr + maintainer-bz: a@example.com + tasks: + - type: vendoring + enabled: False + cc: + - b@example.com + - c@example.com + needinfo: + - d@example.com + - e@example.com + frequency: every + blocking: foo + - type: commit-alert + filter: none + frequency: 2 commits + platform: linux + source-extensions: + - .c + - .cpp + """.strip(), + ), + # ------------------------------------------------- + ( + "exception", + b""" +--- +schema: 1 +origin: + name: cairo + description: 2D Graphics Library + url: https://www.cairographics.org/ + release: version 1.6.4 + license: + - MPL-1.1 + - LGPL-2.1 + revision: AA001122334455 +vendoring: + url: https://example.com + source-hosting: gitlab +bugzilla: + product: Core + component: Graphics +updatebot: + maintainer-phab: tjr + maintainer-bz: a@example.com + fuzzy-paths: "must-be-array" + tasks: + - type: vendoring + enabled: False + cc: + - b@example.com + - c@example.com + needinfo: + - d@example.com + - e@example.com + frequency: every + - type: commit-alert + filter: none + frequency: 2 commits + platform: linux + source-extensions: + - .c + - .cpp + """.strip(), + ), + # ------------------------------------------------- + ( + "exception", + b""" +--- +schema: 1 +origin: + name: cairo + description: 2D Graphics Library + url: https://www.cairographics.org/ + release: version 1.6.4 + license: + - MPL-1.1 + - LGPL-2.1 + revision: AA001122334455 +vendoring: + url: https://example.com + source-hosting: gitlab +bugzilla: + product: Core + component: Graphics +updatebot: + maintainer-phab: tjr + maintainer-bz: a@example.com + tasks: + - type: vendoring + enabled: False + cc: + - b@example.com + - c@example.com + needinfo: + - d@example.com + - e@example.com + frequency: every + - type: commit-alert + filter: none + frequency: 2 commits, 4 weeks + platform: linux + source-extensions: + - .c + - .cpp + """.strip(), + ), + # ------------------------------------------------- + ( + "exception", + b""" +--- +schema: 1 +origin: + name: cairo + description: 2D Graphics Library + url: https://www.cairographics.org/ + release: version 1.6.4 + license: + - MPL-1.1 + - LGPL-2.1 + revision: AA001122334455 +vendoring: + url: https://example.com + source-hosting: gitlab +bugzilla: + product: Core + component: Graphics +updatebot: + maintainer-phab: tjr + maintainer-bz: a@example.com + tasks: + - type: vendoring + enabled: False + cc: + - b@example.com + - c@example.com + needinfo: + - d@example.com + - e@example.com + frequency: every + - type: commit-alert + filter: none + frequency: 4 weeks, 2 commits, 3 weeks + platform: linux + source-extensions: + - .c + - .cpp + """.strip(), + ), + # ------------------------------------------------- + ( + "exception", + b""" +--- +schema: 1 +origin: + name: cairo + description: 2D Graphics Library + url: https://www.cairographics.org/ + release: version 1.6.4 + license: + - MPL-1.1 + - LGPL-2.1 + revision: AA001122334455 +vendoring: + url: https://example.com + source-hosting: gitlab + flavor: chocolate +bugzilla: + product: Core + component: Graphics +updatebot: + maintainer-phab: tjr + maintainer-bz: a@example.com + tasks: + - type: vendoring + enabled: False + cc: + - b@example.com + - c@example.com + needinfo: + - d@example.com + - e@example.com + frequency: every + - type: commit-alert + filter: none + frequency: 2 weeks + platform: linux + source-extensions: + - .c + - .cpp + """.strip(), + ), + # ------------------------------------------------- + ( + "exception", + b""" +--- +schema: 1 +origin: + name: cairo + description: 2D Graphics Library + url: https://www.cairographics.org/ + release: version 1.6.4 + license: + - MPL-1.1 + - LGPL-2.1 + revision: AA001122334455 +vendoring: + url: https://example.com + source-hosting: gitlab + flavor: chocolate +bugzilla: + product: Core + component: Graphics +updatebot: + maintainer-phab: tjr + maintainer-bz: a@example.com + tasks: + - type: vendoring + enabled: False + cc: + - b@example.com + - c@example.com + needinfo: + - d@example.com + - e@example.com + frequency: every + - type: commit-alert + filter: none + frequency: 01 commits + platform: linux + source-extensions: + - .c + - .cpp + """.strip(), + ), + # ------------------------------------------------- + ( + "exception", + b""" +--- +schema: 1 +origin: + name: cairo + description: 2D Graphics Library + url: https://www.cairographics.org/ + release: version 1.6.4 + license: + - MPL-1.1 + - LGPL-2.1 + revision: AA001122334455 +vendoring: + url: https://example.com + source-hosting: gitlab +bugzilla: + product: Core + component: Graphics +updatebot: + maintainer-phab: tjr + maintainer-bz: a@example.com + tasks: + - type: vendoring + enabled: False + cc: + - b@example.com + - c@example.com + needinfo: + - d@example.com + - e@example.com + frequency: every + - type: commit-alert + filter: none + frequency: 2 weeks + platform: mac + source-extensions: + - .c + - .cpp + """.strip(), + ), + # ------------------------------------------------- + ( + "exception", + b""" +--- +schema: 1 +origin: + name: cairo + description: 2D Graphics Library + url: https://www.cairographics.org/ + release: version 1.6.4 + license: + - MPL-1.1 + - LGPL-2.1 + revision: AA001122334455 +bugzilla: + product: Core + component: Graphics +updatebot: + maintainer-phab: tjr + maintainer-bz: a@example.com + tasks: + - type: vendoring + enabled: False + cc: + - b@example.com + - c@example.com + - type: commit-alert + filter: none + source-extensions: + - .c + - .cpp + """.strip(), + ), + # ------------------------------------------------- + ( + "exception", + b""" +--- +schema: 1 +origin: + name: cairo + description: 2D Graphics Library + url: https://www.cairographics.org/ + release: version 1.6.4 + license: + - MPL-1.1 + - LGPL-2.1 +bugzilla: + product: Core + component: Graphics +vendoring: + url: https://example.com + source-hosting: gitlab +updatebot: + maintainer-phab: tjr + maintainer-bz: a@example.com + tasks: + - type: vendoring + enabled: False + cc: + - b@example.com + - c@example.com + - type: commit-alert + filter: none + source-extensions: + - .c + - .cpp + """.strip(), + ), + # ------------------------------------------------- + ( + "exception", + b""" +--- +schema: 1 +origin: + name: cairo + description: 2D Graphics Library + url: https://www.cairographics.org/ + release: version 1.6.4 + license: + - MPL-1.1 + - LGPL-2.1 + revision: AA001122334455 +vendoring: + url: https://example.com + source-hosting: gitlab +bugzilla: + product: Core + component: Graphics +updatebot: + maintainer-phab: tjr + maintainer-bz: a@example.com + tasks: + - type: vendoring + filter: none + """.strip(), + ), + # ------------------------------------------------- + ( + "exception", + b""" +--- +schema: 1 +origin: + name: cairo + description: 2D Graphics Library + url: https://www.cairographics.org/ + release: version 1.6.4 + license: + - MPL-1.1 + - LGPL-2.1 + revision: AA001122334455 +vendoring: + url: https://example.com + source-hosting: gitlab +bugzilla: + product: Core + component: Graphics +updatebot: + maintainer-phab: tjr + maintainer-bz: a@example.com + tasks: + - type: foo + """.strip(), + ), + # ------------------------------------------------- + ( + "exception", + b""" +--- +schema: 1 +origin: + name: cairo + description: 2D Graphics Library + url: https://www.cairographics.org/ + release: version 1.6.4 + license: + - MPL-1.1 + - LGPL-2.1 + revision: AA001122334455 +vendoring: + url: https://example.com + source-hosting: gitlab +bugzilla: + product: Core + component: Graphics +updatebot: + maintainer-phab: tjr + maintainer-bz: a@example.com + tasks: + - type: vendoring + source-extensions: + - .c + - .cpp + """.strip(), + ), + # ------------------------------------------------- + ( + "exception", + b""" +--- +schema: 1 +origin: + name: cairo + description: 2D Graphics Library + url: https://www.cairographics.org/ + release: version 1.6.4 + license: + - MPL-1.1 + - LGPL-2.1 + revision: AA001122334455 +vendoring: + url: https://example.com + source-hosting: gitlab +bugzilla: + product: Core + component: Graphics +updatebot: + maintainer-phab: tjr + maintainer-bz: a@example.com + tasks: + - type: commit-alert + filter: hogwash + """.strip(), + ), + ( + "exception", + b""" +--- +schema: 1 +origin: + name: cairo + description: 2D Graphics Library + url: https://www.cairographics.org/ + release: version 1.6.4 + license: + - MPL-1.1 + - LGPL-2.1 + revision: AA001122334455 +bugzilla: + product: Core + component: Graphics +vendoring: + url: https://example.com + source-hosting: gitlab +updatebot: + maintainer-phab: tjr + maintainer-bz: a@example.com + tasks: + - type: vendoring + enabled: False + cc: + - b@example.com + - c@example.com + - type: commit-alert + - type: commit-alert + filter: none + source-extensions: + - .c + - .cpp""".strip(), + ), + ( + "exception", + b""" +--- +schema: 1 +origin: + name: cairo + description: 2D Graphics Library + url: https://www.cairographics.org/ + release: version 1.6.4 + license: + - MPL-1.1 + - LGPL-2.1 + revision: AA001122334455 +bugzilla: + product: Core + component: Graphics +vendoring: + url: https://example.com + source-hosting: gitlab +updatebot: + maintainer-phab: tjr + maintainer-bz: a@example.com + tasks: + - type: vendoring + enabled: False + cc: + - b@example.com + - c@example.com + - type: vendoring + - type: commit-alert + filter: none + source-extensions: + - .c + - .cpp""".strip(), + ), + ( + "exception", + b""" +--- +schema: 1 +origin: + name: cairo + description: 2D Graphics Library + url: https://www.cairographics.org/ + release: version 1.6.4 + license: + - MPL-1.1 + - LGPL-2.1 + revision: AA001122334455 +bugzilla: + product: Core + component: Graphics +vendoring: + url: https://example.com + source-hosting: gitlab +updatebot: + maintainer-phab: tjr + maintainer-bz: a@example.com + tasks: + - type: vendoring + enabled: False + cc: + - b@example.com + - c@example.com + - type: commit-alert + frequency: every-release + filter: none + source-extensions: + - .c + - .cpp""".strip(), + ), + ( + "exception", + b""" +--- +schema: 1 +origin: + name: cairo + description: 2D Graphics Library + url: https://www.cairographics.org/ + release: version 1.6.4 + license: + - MPL-1.1 + - LGPL-2.1 + revision: AA001122334455 +bugzilla: + product: Core + component: Graphics +vendoring: + url: https://example.com + source-hosting: gitlab +updatebot: + maintainer-phab: tjr + maintainer-bz: a@example.com + tasks: + - type: vendoring + enabled: False + cc: + - b@example.com + - c@example.com + frequency: 2 months + - type: commit-alert + filter: none + source-extensions: + - .c + - .cpp""".strip(), + ), + ( + "exception", + b""" +--- +schema: 1 +origin: + name: cairo + description: 2D Graphics Library + url: https://www.cairographics.org/ + release: version 1.6.4 + license: + - MPL-1.1 + - LGPL-2.1 + revision: AA001122334455 +vendoring: + url: https://example.com + source-hosting: gitlab +bugzilla: + product: Core + component: Graphics +updatebot: + maintainer-phab: tjr + maintainer-bz: a@example.com + tasks: + - type: commit-alert + frequency: 0 weeks + """.strip(), + ), + ] + ) + + # =========================================================================================== + def test_malformed(self): + with mozfile.NamedTemporaryFile() as tf: + tf.write(b"blah") + tf.flush() + with self.assertRaises(MozYamlVerifyError): + load_moz_yaml(tf.name, require_license_file=False) + + def test_schema(self): + with mozfile.NamedTemporaryFile() as tf: + tf.write(b"schema: 99") + tf.flush() + with self.assertRaises(MozYamlVerifyError): + load_moz_yaml(tf.name, require_license_file=False) + + def test_json(self): + with mozfile.NamedTemporaryFile() as tf: + tf.write( + b'{"origin": {"release": "version 1.6.4", "url": "https://w' + b'ww.cairographics.org/", "description": "2D Graphics Libra' + b'ry", "license": ["MPL-1.1", "LGPL-2.1"], "name": "cairo"}' + b', "bugzilla": {"product": "Core", "component": "Graphics"' + b'}, "schema": 1}' + ) + tf.flush() + with self.assertRaises(MozYamlVerifyError): + load_moz_yaml(tf.name, require_license_file=False) + + def test_revision(self): + self.process_test_vectors( + [ + ( + { + "schema": "1", + "origin": { + "description": "2D Graphics Library", + "license": ["MPL-1.1", "LGPL-2.1"], + "name": "cairo", + "release": "version 1.6.4", + "revision": "v1.6.37", + "url": "https://www.cairographics.org/", + }, + "bugzilla": {"component": "Graphics", "product": "Core"}, + }, + b""" +--- +schema: 1 +origin: + name: cairo + description: 2D Graphics Library + url: https://www.cairographics.org/ + release: version 1.6.4 + license: + - MPL-1.1 + - LGPL-2.1 + revision: v1.6.37 +bugzilla: + product: Core + component: Graphics""".strip(), + ), + ( + "exception", + b""" +--- +schema: 1 +origin: + name: cairo + description: 2D Graphics Library + url: https://www.cairographics.org/ + release: version 1.6.4 + license: + - MPL-1.1 + - LGPL-2.1 + revision: 4.0.0. +bugzilla: + product: Core + component: Graphics""".strip(), + ), + ( + "exception", + b""" +--- +schema: 1 +origin: + name: cairo + description: 2D Graphics Library + url: https://www.cairographics.org/ + release: version 1.6.4 + license: + - MPL-1.1 + - LGPL-2.1 + revision: 4.^.0 +bugzilla: + product: Core + component: Graphics""".strip(), + ), + ( + "exception", + b""" +--- +schema: 1 +origin: + name: cairo + description: 2D Graphics Library + url: https://www.cairographics.org/ + release: version 1.6.4 + license: + - MPL-1.1 + - LGPL-2.1 + revision: " " +bugzilla: + product: Core + component: Graphics""".strip(), + ), + ( + "exception", + b""" +--- +schema: 1 +origin: + name: cairo + description: 2D Graphics Library + url: https://www.cairographics.org/ + release: version 1.6.4 + license: + - MPL-1.1 + - LGPL-2.1 + revision: ??? +bugzilla: + product: Core + component: Graphics""".strip(), + ), + ( + "exception", + b""" +--- +schema: 1 +origin: + name: cairo + description: 2D Graphics Library + url: https://www.cairographics.org/ + release: version 1.6.4 + license: + - MPL-1.1 + - LGPL-2.1 + revision: ] +bugzilla: + product: Core + component: Graphics""".strip(), + ), + ( + "exception", + b""" +--- +schema: 1 +origin: + name: cairo + description: 2D Graphics Library + url: https://www.cairographics.org/ + release: version 1.6.4 + license: + - MPL-1.1 + - LGPL-2.1 + revision: AA001122334455 +bugzilla: + product: Core + component: Graphics +vendoring: + url: https://example.com + source-hosting: gitlab + update-actions: + - action: run-script + cwd: '{cwd}' + script: 'script.py' + args: ['hi'] + pattern: 'hi' +""".strip(), + ), + ( + "exception", + b""" +--- +schema: 1 +origin: + name: cairo + description: 2D Graphics Library + url: https://www.cairographics.org/ + release: version 1.6.4 + license: + - MPL-1.1 + - LGPL-2.1 + revision: AA001122334455 +bugzilla: + product: Core + component: Graphics +vendoring: + url: https://example.com + source-hosting: gitlab + update-actions: + - action: run-script + cwd: '{cwd}' + args: ['hi'] +""".strip(), + ), + ] + ) + + +if __name__ == "__main__": + main() diff --git a/python/mozbuild/mozbuild/test/test_mozconfig.py b/python/mozbuild/mozbuild/test/test_mozconfig.py new file mode 100644 index 0000000000..20827d7f29 --- /dev/null +++ b/python/mozbuild/mozbuild/test/test_mozconfig.py @@ -0,0 +1,275 @@ +# 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/. + +import os +import unittest +from shutil import rmtree +from tempfile import mkdtemp + +from mozfile.mozfile import NamedTemporaryFile +from mozunit import main + +from mozbuild.mozconfig import MozconfigLoader, MozconfigLoadException + + +class TestMozconfigLoader(unittest.TestCase): + def setUp(self): + self._old_env = dict(os.environ) + os.environ.pop("MOZCONFIG", None) + os.environ.pop("MOZ_OBJDIR", None) + os.environ.pop("CC", None) + os.environ.pop("CXX", None) + self._temp_dirs = set() + + def tearDown(self): + os.environ.clear() + os.environ.update(self._old_env) + + for d in self._temp_dirs: + rmtree(d) + + def get_loader(self): + return MozconfigLoader(self.get_temp_dir()) + + def get_temp_dir(self): + d = mkdtemp() + self._temp_dirs.add(d) + + return d + + def test_read_no_mozconfig(self): + # This is basically to ensure changes to defaults incur a test failure. + result = self.get_loader().read_mozconfig() + + self.assertEqual( + result, + { + "path": None, + "topobjdir": None, + "configure_args": None, + "make_flags": None, + "make_extra": None, + "env": None, + "vars": None, + }, + ) + + def test_read_empty_mozconfig(self): + with NamedTemporaryFile(mode="w") as mozconfig: + result = self.get_loader().read_mozconfig(mozconfig.name) + + self.assertEqual(result["path"], mozconfig.name) + self.assertIsNone(result["topobjdir"]) + self.assertEqual(result["configure_args"], []) + self.assertEqual(result["make_flags"], []) + self.assertEqual(result["make_extra"], []) + + for f in ("added", "removed", "modified"): + self.assertEqual(len(result["vars"][f]), 0) + self.assertEqual(len(result["env"][f]), 0) + + self.assertEqual(result["env"]["unmodified"], {}) + + def test_read_capture_ac_options(self): + """Ensures ac_add_options calls are captured.""" + with NamedTemporaryFile(mode="w") as mozconfig: + mozconfig.write("ac_add_options --enable-debug\n") + mozconfig.write("ac_add_options --disable-tests --enable-foo\n") + mozconfig.write('ac_add_options --foo="bar baz"\n') + mozconfig.flush() + + result = self.get_loader().read_mozconfig(mozconfig.name) + self.assertEqual( + result["configure_args"], + ["--enable-debug", "--disable-tests", "--enable-foo", "--foo=bar baz"], + ) + + def test_read_ac_options_substitution(self): + """Ensure ac_add_options values are substituted.""" + with NamedTemporaryFile(mode="w") as mozconfig: + mozconfig.write("ac_add_options --foo=@TOPSRCDIR@\n") + mozconfig.flush() + + loader = self.get_loader() + result = loader.read_mozconfig(mozconfig.name) + self.assertEqual(result["configure_args"], ["--foo=%s" % loader.topsrcdir]) + + def test_read_capture_mk_options(self): + """Ensures mk_add_options calls are captured.""" + with NamedTemporaryFile(mode="w") as mozconfig: + mozconfig.write("mk_add_options MOZ_OBJDIR=/foo/bar\n") + mozconfig.write('mk_add_options MOZ_MAKE_FLAGS="-j8 -s"\n') + mozconfig.write('mk_add_options FOO="BAR BAZ"\n') + mozconfig.write("mk_add_options BIZ=1\n") + mozconfig.flush() + + result = self.get_loader().read_mozconfig(mozconfig.name) + self.assertEqual(result["topobjdir"], "/foo/bar") + self.assertEqual(result["make_flags"], ["-j8", "-s"]) + self.assertEqual(result["make_extra"], ["FOO=BAR BAZ", "BIZ=1"]) + + def test_read_no_mozconfig_objdir_environ(self): + os.environ["MOZ_OBJDIR"] = "obj-firefox" + result = self.get_loader().read_mozconfig() + self.assertEqual(result["topobjdir"], "obj-firefox") + + def test_read_empty_mozconfig_objdir_environ(self): + os.environ["MOZ_OBJDIR"] = "obj-firefox" + with NamedTemporaryFile(mode="w") as mozconfig: + result = self.get_loader().read_mozconfig(mozconfig.name) + self.assertEqual(result["topobjdir"], "obj-firefox") + + def test_read_capture_mk_options_objdir_environ(self): + """Ensures mk_add_options calls are captured and override the environ.""" + os.environ["MOZ_OBJDIR"] = "obj-firefox" + with NamedTemporaryFile(mode="w") as mozconfig: + mozconfig.write("mk_add_options MOZ_OBJDIR=/foo/bar\n") + mozconfig.flush() + + result = self.get_loader().read_mozconfig(mozconfig.name) + self.assertEqual(result["topobjdir"], "/foo/bar") + + def test_read_moz_objdir_substitution(self): + """Ensure @TOPSRCDIR@ substitution is recognized in MOZ_OBJDIR.""" + with NamedTemporaryFile(mode="w") as mozconfig: + mozconfig.write("mk_add_options MOZ_OBJDIR=@TOPSRCDIR@/some-objdir") + mozconfig.flush() + + loader = self.get_loader() + result = loader.read_mozconfig(mozconfig.name) + + self.assertEqual(result["topobjdir"], "%s/some-objdir" % loader.topsrcdir) + + def test_read_new_variables(self): + """New variables declared in mozconfig file are detected.""" + with NamedTemporaryFile(mode="w") as mozconfig: + mozconfig.write("CC=/usr/local/bin/clang\n") + mozconfig.write("CXX=/usr/local/bin/clang++\n") + mozconfig.flush() + + result = self.get_loader().read_mozconfig(mozconfig.name) + + self.assertEqual( + result["vars"]["added"], + {"CC": "/usr/local/bin/clang", "CXX": "/usr/local/bin/clang++"}, + ) + self.assertEqual(result["env"]["added"], {}) + + def test_read_exported_variables(self): + """Exported variables are caught as new variables.""" + with NamedTemporaryFile(mode="w") as mozconfig: + mozconfig.write("export MY_EXPORTED=woot\n") + mozconfig.flush() + + result = self.get_loader().read_mozconfig(mozconfig.name) + + self.assertEqual(result["vars"]["added"], {}) + self.assertEqual(result["env"]["added"], {"MY_EXPORTED": "woot"}) + + def test_read_modify_variables(self): + """Variables modified by mozconfig are detected.""" + old_path = os.path.realpath("/usr/bin/gcc") + new_path = os.path.realpath("/usr/local/bin/clang") + os.environ["CC"] = old_path + + with NamedTemporaryFile(mode="w") as mozconfig: + mozconfig.write('CC="%s"\n' % new_path) + mozconfig.flush() + + result = self.get_loader().read_mozconfig(mozconfig.name) + + self.assertEqual(result["vars"]["modified"], {}) + self.assertEqual(result["env"]["modified"], {"CC": (old_path, new_path)}) + + def test_read_unmodified_variables(self): + """Variables modified by mozconfig are detected.""" + cc_path = os.path.realpath("/usr/bin/gcc") + os.environ["CC"] = cc_path + + with NamedTemporaryFile(mode="w") as mozconfig: + mozconfig.flush() + + result = self.get_loader().read_mozconfig(mozconfig.name) + + self.assertEqual(result["vars"]["unmodified"], {}) + self.assertEqual(result["env"]["unmodified"], {"CC": cc_path}) + + def test_read_removed_variables(self): + """Variables unset by the mozconfig are detected.""" + cc_path = os.path.realpath("/usr/bin/clang") + os.environ["CC"] = cc_path + + with NamedTemporaryFile(mode="w") as mozconfig: + mozconfig.write("unset CC\n") + mozconfig.flush() + + result = self.get_loader().read_mozconfig(mozconfig.name) + + self.assertEqual(result["vars"]["removed"], {}) + self.assertEqual(result["env"]["removed"], {"CC": cc_path}) + + def test_read_multiline_variables(self): + """Ensure multi-line variables are captured properly.""" + with NamedTemporaryFile(mode="w") as mozconfig: + mozconfig.write('multi="foo\nbar"\n') + mozconfig.write("single=1\n") + mozconfig.flush() + + result = self.get_loader().read_mozconfig(mozconfig.name) + + self.assertEqual( + result["vars"]["added"], {"multi": "foo\nbar", "single": "1"} + ) + self.assertEqual(result["env"]["added"], {}) + + def test_read_topsrcdir_defined(self): + """Ensure $topsrcdir references work as expected.""" + with NamedTemporaryFile(mode="w") as mozconfig: + mozconfig.write("TEST=$topsrcdir") + mozconfig.flush() + + loader = self.get_loader() + result = loader.read_mozconfig(mozconfig.name) + + self.assertEqual( + result["vars"]["added"]["TEST"], loader.topsrcdir.replace(os.sep, "/") + ) + self.assertEqual(result["env"]["added"], {}) + + def test_read_empty_variable_value(self): + """Ensure empty variable values are parsed properly.""" + with NamedTemporaryFile(mode="w") as mozconfig: + mozconfig.write("EMPTY=\n") + mozconfig.write("export EXPORT_EMPTY=\n") + mozconfig.flush() + + result = self.get_loader().read_mozconfig(mozconfig.name) + + self.assertEqual( + result["vars"]["added"], + { + "EMPTY": "", + }, + ) + self.assertEqual(result["env"]["added"], {"EXPORT_EMPTY": ""}) + + def test_read_load_exception(self): + """Ensure non-0 exit codes in mozconfigs are handled properly.""" + with NamedTemporaryFile(mode="w") as mozconfig: + mozconfig.write('echo "hello world"\n') + mozconfig.write("exit 1\n") + mozconfig.flush() + + with self.assertRaises(MozconfigLoadException) as e: + self.get_loader().read_mozconfig(mozconfig.name) + + self.assertIn( + "Evaluation of your mozconfig exited with an error", str(e.exception) + ) + self.assertEqual(e.exception.path, mozconfig.name.replace(os.sep, "/")) + self.assertEqual(e.exception.output, ["hello world"]) + + +if __name__ == "__main__": + main() diff --git a/python/mozbuild/mozbuild/test/test_mozinfo.py b/python/mozbuild/mozbuild/test/test_mozinfo.py new file mode 100755 index 0000000000..0d966b3dcc --- /dev/null +++ b/python/mozbuild/mozbuild/test/test_mozinfo.py @@ -0,0 +1,318 @@ +#!/usr/bin/env python +# 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/. + +import json +import os +import tempfile +import unittest + +import mozunit +import six +from mozfile.mozfile import NamedTemporaryFile +from six import StringIO + +from mozbuild.backend.configenvironment import ConfigEnvironment +from mozbuild.mozinfo import build_dict, write_mozinfo + + +class Base(object): + def _config(self, substs={}): + d = os.path.dirname(__file__) + return ConfigEnvironment(d, d, substs=substs) + + +class TestBuildDict(unittest.TestCase, Base): + def test_missing(self): + """ + Test that missing required values raises. + """ + + with self.assertRaises(Exception): + build_dict(self._config(substs=dict(OS_TARGET="foo"))) + + with self.assertRaises(Exception): + build_dict(self._config(substs=dict(TARGET_CPU="foo"))) + + with self.assertRaises(Exception): + build_dict(self._config(substs=dict(MOZ_WIDGET_TOOLKIT="foo"))) + + def test_win(self): + d = build_dict( + self._config( + dict( + OS_TARGET="WINNT", + TARGET_CPU="i386", + MOZ_WIDGET_TOOLKIT="windows", + ) + ) + ) + self.assertEqual("win", d["os"]) + self.assertEqual("x86", d["processor"]) + self.assertEqual("windows", d["toolkit"]) + self.assertEqual(32, d["bits"]) + + def test_linux(self): + d = build_dict( + self._config( + dict( + OS_TARGET="Linux", + TARGET_CPU="i386", + MOZ_WIDGET_TOOLKIT="gtk", + ) + ) + ) + self.assertEqual("linux", d["os"]) + self.assertEqual("x86", d["processor"]) + self.assertEqual("gtk", d["toolkit"]) + self.assertEqual(32, d["bits"]) + + d = build_dict( + self._config( + dict( + OS_TARGET="Linux", + TARGET_CPU="x86_64", + MOZ_WIDGET_TOOLKIT="gtk", + ) + ) + ) + self.assertEqual("linux", d["os"]) + self.assertEqual("x86_64", d["processor"]) + self.assertEqual("gtk", d["toolkit"]) + self.assertEqual(64, d["bits"]) + + def test_mac(self): + d = build_dict( + self._config( + dict( + OS_TARGET="Darwin", + TARGET_CPU="i386", + MOZ_WIDGET_TOOLKIT="cocoa", + ) + ) + ) + self.assertEqual("mac", d["os"]) + self.assertEqual("x86", d["processor"]) + self.assertEqual("cocoa", d["toolkit"]) + self.assertEqual(32, d["bits"]) + + d = build_dict( + self._config( + dict( + OS_TARGET="Darwin", + TARGET_CPU="x86_64", + MOZ_WIDGET_TOOLKIT="cocoa", + ) + ) + ) + self.assertEqual("mac", d["os"]) + self.assertEqual("x86_64", d["processor"]) + self.assertEqual("cocoa", d["toolkit"]) + self.assertEqual(64, d["bits"]) + + def test_android(self): + d = build_dict( + self._config( + dict( + OS_TARGET="Android", + TARGET_CPU="arm", + MOZ_WIDGET_TOOLKIT="android", + ) + ) + ) + self.assertEqual("android", d["os"]) + self.assertEqual("arm", d["processor"]) + self.assertEqual("android", d["toolkit"]) + self.assertEqual(32, d["bits"]) + + def test_x86(self): + """ + Test that various i?86 values => x86. + """ + d = build_dict( + self._config( + dict( + OS_TARGET="WINNT", + TARGET_CPU="i486", + MOZ_WIDGET_TOOLKIT="windows", + ) + ) + ) + self.assertEqual("x86", d["processor"]) + + d = build_dict( + self._config( + dict( + OS_TARGET="WINNT", + TARGET_CPU="i686", + MOZ_WIDGET_TOOLKIT="windows", + ) + ) + ) + self.assertEqual("x86", d["processor"]) + + def test_arm(self): + """ + Test that all arm CPU architectures => arm. + """ + d = build_dict( + self._config( + dict( + OS_TARGET="Linux", + TARGET_CPU="arm", + MOZ_WIDGET_TOOLKIT="gtk", + ) + ) + ) + self.assertEqual("arm", d["processor"]) + + d = build_dict( + self._config( + dict( + OS_TARGET="Linux", + TARGET_CPU="armv7", + MOZ_WIDGET_TOOLKIT="gtk", + ) + ) + ) + self.assertEqual("arm", d["processor"]) + + def test_unknown(self): + """ + Test that unknown values pass through okay. + """ + d = build_dict( + self._config( + dict( + OS_TARGET="RandOS", + TARGET_CPU="cptwo", + MOZ_WIDGET_TOOLKIT="foobar", + ) + ) + ) + self.assertEqual("randos", d["os"]) + self.assertEqual("cptwo", d["processor"]) + self.assertEqual("foobar", d["toolkit"]) + # unknown CPUs should not get a bits value + self.assertFalse("bits" in d) + + def test_debug(self): + """ + Test that debug values are properly detected. + """ + d = build_dict( + self._config( + dict( + OS_TARGET="Linux", + TARGET_CPU="i386", + MOZ_WIDGET_TOOLKIT="gtk", + ) + ) + ) + self.assertEqual(False, d["debug"]) + + d = build_dict( + self._config( + dict( + OS_TARGET="Linux", + TARGET_CPU="i386", + MOZ_WIDGET_TOOLKIT="gtk", + MOZ_DEBUG="1", + ) + ) + ) + self.assertEqual(True, d["debug"]) + + def test_crashreporter(self): + """ + Test that crashreporter values are properly detected. + """ + d = build_dict( + self._config( + dict( + OS_TARGET="Linux", + TARGET_CPU="i386", + MOZ_WIDGET_TOOLKIT="gtk", + ) + ) + ) + self.assertEqual(False, d["crashreporter"]) + + d = build_dict( + self._config( + dict( + OS_TARGET="Linux", + TARGET_CPU="i386", + MOZ_WIDGET_TOOLKIT="gtk", + MOZ_CRASHREPORTER="1", + ) + ) + ) + self.assertEqual(True, d["crashreporter"]) + + +class TestWriteMozinfo(unittest.TestCase, Base): + """ + Test the write_mozinfo function. + """ + + def setUp(self): + fd, f = tempfile.mkstemp() + self.f = six.ensure_text(f) + os.close(fd) + + def tearDown(self): + os.unlink(self.f) + + def test_basic(self): + """ + Test that writing to a file produces correct output. + """ + c = self._config( + dict( + OS_TARGET="WINNT", + TARGET_CPU="i386", + MOZ_WIDGET_TOOLKIT="windows", + ) + ) + tempdir = tempfile.gettempdir() + c.topsrcdir = tempdir + with NamedTemporaryFile( + dir=os.path.normpath(c.topsrcdir), mode="wt" + ) as mozconfig: + mozconfig.write("unused contents") + mozconfig.flush() + c.mozconfig = mozconfig.name + write_mozinfo(self.f, c) + with open(self.f) as f: + d = json.load(f) + self.assertEqual("win", d["os"]) + self.assertEqual("x86", d["processor"]) + self.assertEqual("windows", d["toolkit"]) + self.assertEqual(tempdir, d["topsrcdir"]) + self.assertEqual(mozconfig.name, d["mozconfig"]) + self.assertEqual(32, d["bits"]) + + def test_fileobj(self): + """ + Test that writing to a file-like object produces correct output. + """ + s = StringIO() + c = self._config( + dict( + OS_TARGET="WINNT", + TARGET_CPU="i386", + MOZ_WIDGET_TOOLKIT="windows", + ) + ) + write_mozinfo(s, c) + d = json.loads(s.getvalue()) + self.assertEqual("win", d["os"]) + self.assertEqual("x86", d["processor"]) + self.assertEqual("windows", d["toolkit"]) + self.assertEqual(32, d["bits"]) + + +if __name__ == "__main__": + mozunit.main() diff --git a/python/mozbuild/mozbuild/test/test_preprocessor.py b/python/mozbuild/mozbuild/test/test_preprocessor.py new file mode 100644 index 0000000000..82039c2bd7 --- /dev/null +++ b/python/mozbuild/mozbuild/test/test_preprocessor.py @@ -0,0 +1,832 @@ +# 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/. + +import os +import shutil +import unittest +from tempfile import mkdtemp + +from mozunit import MockedOpen, main +from six import StringIO + +from mozbuild.preprocessor import Preprocessor + + +class TestPreprocessor(unittest.TestCase): + """ + Unit tests for the Context class + """ + + def setUp(self): + self.pp = Preprocessor() + self.pp.out = StringIO() + + def do_include_compare(self, content_lines, expected_lines): + content = "%s" % "\n".join(content_lines) + expected = "%s".rstrip() % "\n".join(expected_lines) + + with MockedOpen({"dummy": content}): + self.pp.do_include("dummy") + self.assertEqual(self.pp.out.getvalue().rstrip("\n"), expected) + + def do_include_pass(self, content_lines): + self.do_include_compare(content_lines, ["PASS"]) + + def test_conditional_if_0(self): + self.do_include_pass( + [ + "#if 0", + "FAIL", + "#else", + "PASS", + "#endif", + ] + ) + + def test_no_marker(self): + lines = [ + "#if 0", + "PASS", + "#endif", + ] + self.pp.setMarker(None) + self.do_include_compare(lines, lines) + + def test_string_value(self): + self.do_include_compare( + [ + "#define FOO STRING", + "#if FOO", + "string value is true", + "#else", + "string value is false", + "#endif", + ], + ["string value is false"], + ) + + def test_number_value(self): + self.do_include_compare( + [ + "#define FOO 1", + "#if FOO", + "number value is true", + "#else", + "number value is false", + "#endif", + ], + ["number value is true"], + ) + + def test_conditional_if_0_elif_1(self): + self.do_include_pass( + [ + "#if 0", + "#elif 1", + "PASS", + "#else", + "FAIL", + "#endif", + ] + ) + + def test_conditional_if_1(self): + self.do_include_pass( + [ + "#if 1", + "PASS", + "#else", + "FAIL", + "#endif", + ] + ) + + def test_conditional_if_0_or_1(self): + self.do_include_pass( + [ + "#if 0 || 1", + "PASS", + "#else", + "FAIL", + "#endif", + ] + ) + + def test_conditional_if_1_elif_1_else(self): + self.do_include_pass( + [ + "#if 1", + "PASS", + "#elif 1", + "FAIL", + "#else", + "FAIL", + "#endif", + ] + ) + + def test_conditional_if_1_if_1(self): + self.do_include_pass( + [ + "#if 1", + "#if 1", + "PASS", + "#else", + "FAIL", + "#endif", + "#else", + "FAIL", + "#endif", + ] + ) + + def test_conditional_not_0(self): + self.do_include_pass( + [ + "#if !0", + "PASS", + "#else", + "FAIL", + "#endif", + ] + ) + + def test_conditional_not_0_and_1(self): + self.do_include_pass( + [ + "#if !0 && !1", + "FAIL", + "#else", + "PASS", + "#endif", + ] + ) + + def test_conditional_not_1(self): + self.do_include_pass( + [ + "#if !1", + "FAIL", + "#else", + "PASS", + "#endif", + ] + ) + + def test_conditional_not_emptyval(self): + self.do_include_compare( + [ + "#define EMPTYVAL", + "#ifndef EMPTYVAL", + "FAIL", + "#else", + "PASS", + "#endif", + "#ifdef EMPTYVAL", + "PASS", + "#else", + "FAIL", + "#endif", + ], + ["PASS", "PASS"], + ) + + def test_conditional_not_nullval(self): + self.do_include_pass( + [ + "#define NULLVAL 0", + "#if !NULLVAL", + "PASS", + "#else", + "FAIL", + "#endif", + ] + ) + + def test_indentation(self): + self.do_include_pass( + [ + " #define NULLVAL 0", + " #if !NULLVAL", + "PASS", + " #else", + "FAIL", + " #endif", + ] + ) + + def test_expand(self): + self.do_include_pass( + [ + "#define ASVAR AS", + "#expand P__ASVAR__S", + ] + ) + + def test_undef_defined(self): + self.do_include_compare( + [ + "#define BAR", + "#undef BAR", + "BAR", + ], + ["BAR"], + ) + + def test_undef_undefined(self): + self.do_include_compare( + [ + "#undef BAR", + ], + [], + ) + + def test_filter_attemptSubstitution(self): + self.do_include_compare( + [ + "#filter attemptSubstitution", + "@PASS@", + "#unfilter attemptSubstitution", + ], + ["@PASS@"], + ) + + def test_filter_emptyLines(self): + self.do_include_compare( + [ + "lines with a", + "", + "blank line", + "#filter emptyLines", + "lines with", + "", + "no blank lines", + "#unfilter emptyLines", + "yet more lines with", + "", + "blank lines", + ], + [ + "lines with a", + "", + "blank line", + "lines with", + "no blank lines", + "yet more lines with", + "", + "blank lines", + ], + ) + + def test_filter_dumbComments(self): + self.do_include_compare( + [ + "#filter dumbComments", + "PASS//PASS // PASS", + " //FAIL", + "// FAIL", + "PASS //", + "PASS // FAIL", + "//", + "", + "#unfilter dumbComments", + "// PASS", + ], + [ + "PASS//PASS // PASS", + "", + "", + "PASS //", + "PASS // FAIL", + "", + "", + "// PASS", + ], + ) + + def test_filter_dumbComments_and_emptyLines(self): + self.do_include_compare( + [ + "#filter dumbComments emptyLines", + "PASS//PASS // PASS", + " //FAIL", + "// FAIL", + "PASS //", + "PASS // FAIL", + "//", + "", + "#unfilter dumbComments emptyLines", + "", + "// PASS", + ], + [ + "PASS//PASS // PASS", + "PASS //", + "PASS // FAIL", + "", + "// PASS", + ], + ) + + def test_filter_substitution(self): + self.do_include_pass( + [ + "#define VAR ASS", + "#filter substitution", + "P@VAR@", + "#unfilter substitution", + ] + ) + + def test_error(self): + with MockedOpen({"f": "#error spit this message out\n"}): + with self.assertRaises(Preprocessor.Error) as e: + self.pp.do_include("f") + self.assertEqual(e.args[0][-1], "spit this message out") + + def test_ambigous_command(self): + comment = "# if I tell you a joke\n" + with MockedOpen({"f": comment}): + with self.assertRaises(Preprocessor.Error) as e: + self.pp.do_include("f") + the_exception = e.exception + self.assertEqual(the_exception.args[0][-1], comment) + + def test_javascript_line(self): + # The preprocessor is reading the filename from somewhere not caught + # by MockedOpen. + tmpdir = mkdtemp() + try: + full = os.path.join(tmpdir, "javascript_line.js.in") + with open(full, "w") as fh: + fh.write( + "\n".join( + [ + "// Line 1", + "#if 0", + "// line 3", + "#endif", + "// line 5", + "# comment", + "// line 7", + "// line 8", + "// line 9", + "# another comment", + "// line 11", + "#define LINE 1", + "// line 13, given line number overwritten with 2", + "", + ] + ) + ) + + self.pp.do_include(full) + out = "\n".join( + [ + "// Line 1", + '//@line 5 "CWDjavascript_line.js.in"', + "// line 5", + '//@line 7 "CWDjavascript_line.js.in"', + "// line 7", + "// line 8", + "// line 9", + '//@line 11 "CWDjavascript_line.js.in"', + "// line 11", + '//@line 2 "CWDjavascript_line.js.in"', + "// line 13, given line number overwritten with 2", + "", + ] + ) + out = out.replace("CWD", tmpdir + os.path.sep) + self.assertEqual(self.pp.out.getvalue(), out) + finally: + shutil.rmtree(tmpdir) + + def test_literal(self): + self.do_include_pass( + [ + "#literal PASS", + ] + ) + + def test_var_directory(self): + self.do_include_pass( + [ + "#ifdef DIRECTORY", + "PASS", + "#else", + "FAIL", + "#endif", + ] + ) + + def test_var_file(self): + self.do_include_pass( + [ + "#ifdef FILE", + "PASS", + "#else", + "FAIL", + "#endif", + ] + ) + + def test_var_if_0(self): + self.do_include_pass( + [ + "#define VAR 0", + "#if VAR", + "FAIL", + "#else", + "PASS", + "#endif", + ] + ) + + def test_var_if_0_elifdef(self): + self.do_include_pass( + [ + "#if 0", + "#elifdef FILE", + "PASS", + "#else", + "FAIL", + "#endif", + ] + ) + + def test_var_if_0_elifndef(self): + self.do_include_pass( + [ + "#if 0", + "#elifndef VAR", + "PASS", + "#else", + "FAIL", + "#endif", + ] + ) + + def test_var_ifdef_0(self): + self.do_include_pass( + [ + "#define VAR 0", + "#ifdef VAR", + "PASS", + "#else", + "FAIL", + "#endif", + ] + ) + + def test_var_ifdef_1_or_undef(self): + self.do_include_pass( + [ + "#define FOO 1", + "#if defined(FOO) || defined(BAR)", + "PASS", + "#else", + "FAIL", + "#endif", + ] + ) + + def test_var_ifdef_undef(self): + self.do_include_pass( + [ + "#define VAR 0", + "#undef VAR", + "#ifdef VAR", + "FAIL", + "#else", + "PASS", + "#endif", + ] + ) + + def test_var_ifndef_0(self): + self.do_include_pass( + [ + "#define VAR 0", + "#ifndef VAR", + "FAIL", + "#else", + "PASS", + "#endif", + ] + ) + + def test_var_ifndef_0_and_undef(self): + self.do_include_pass( + [ + "#define FOO 0", + "#if !defined(FOO) && !defined(BAR)", + "FAIL", + "#else", + "PASS", + "#endif", + ] + ) + + def test_var_ifndef_undef(self): + self.do_include_pass( + [ + "#define VAR 0", + "#undef VAR", + "#ifndef VAR", + "PASS", + "#else", + "FAIL", + "#endif", + ] + ) + + def test_var_line(self): + self.do_include_pass( + [ + "#ifdef LINE", + "PASS", + "#else", + "FAIL", + "#endif", + ] + ) + + def test_filterDefine(self): + self.do_include_pass( + [ + "#filter substitution", + "#define VAR AS", + "#define VAR2 P@VAR@", + "@VAR2@S", + ] + ) + + def test_number_value_equals(self): + self.do_include_pass( + [ + "#define FOO 1000", + "#if FOO == 1000", + "PASS", + "#else", + "FAIL", + "#endif", + ] + ) + + def test_default_defines(self): + self.pp.handleCommandLine(["-DFOO"]) + self.do_include_pass( + [ + "#if FOO == 1", + "PASS", + "#else", + "FAIL", + ] + ) + + def test_number_value_equals_defines(self): + self.pp.handleCommandLine(["-DFOO=1000"]) + self.do_include_pass( + [ + "#if FOO == 1000", + "PASS", + "#else", + "FAIL", + ] + ) + + def test_octal_value_equals(self): + self.do_include_pass( + [ + "#define FOO 0100", + "#if FOO == 0100", + "PASS", + "#else", + "FAIL", + "#endif", + ] + ) + + def test_octal_value_equals_defines(self): + self.pp.handleCommandLine(["-DFOO=0100"]) + self.do_include_pass( + [ + "#if FOO == 0100", + "PASS", + "#else", + "FAIL", + "#endif", + ] + ) + + def test_value_quoted_expansion(self): + """ + Quoted values on the commandline don't currently have quotes stripped. + Pike says this is for compat reasons. + """ + self.pp.handleCommandLine(['-DFOO="ABCD"']) + self.do_include_compare( + [ + "#filter substitution", + "@FOO@", + ], + ['"ABCD"'], + ) + + def test_octal_value_quoted_expansion(self): + self.pp.handleCommandLine(['-DFOO="0100"']) + self.do_include_compare( + [ + "#filter substitution", + "@FOO@", + ], + ['"0100"'], + ) + + def test_number_value_not_equals_quoted_defines(self): + self.pp.handleCommandLine(['-DFOO="1000"']) + self.do_include_pass( + [ + "#if FOO == 1000", + "FAIL", + "#else", + "PASS", + "#endif", + ] + ) + + def test_octal_value_not_equals_quoted_defines(self): + self.pp.handleCommandLine(['-DFOO="0100"']) + self.do_include_pass( + [ + "#if FOO == 0100", + "FAIL", + "#else", + "PASS", + "#endif", + ] + ) + + def test_undefined_variable(self): + with MockedOpen({"f": "#filter substitution\n@foo@"}): + with self.assertRaises(Preprocessor.Error) as e: + self.pp.do_include("f") + self.assertEqual(e.key, "UNDEFINED_VAR") + + def test_include(self): + files = { + "foo/test": "\n".join( + [ + "#define foo foobarbaz", + "#include @inc@", + "@bar@", + "", + ] + ), + "bar": "\n".join( + [ + "#define bar barfoobaz", + "@foo@", + "", + ] + ), + "f": "\n".join( + [ + "#filter substitution", + "#define inc ../bar", + "#include foo/test", + "", + ] + ), + } + + with MockedOpen(files): + self.pp.do_include("f") + self.assertEqual(self.pp.out.getvalue(), "foobarbaz\nbarfoobaz\n") + + def test_include_line(self): + files = { + "srcdir/test.js": "\n".join( + [ + "#define foo foobarbaz", + "#include @inc@", + "@bar@", + "", + ] + ), + "srcdir/bar.js": "\n".join( + [ + "#define bar barfoobaz", + "@foo@", + "", + ] + ), + "srcdir/foo.js": "\n".join( + [ + "bazfoobar", + "#include bar.js", + "bazbarfoo", + "", + ] + ), + "objdir/baz.js": "baz\n", + "srcdir/f.js": "\n".join( + [ + "#include foo.js", + "#filter substitution", + "#define inc bar.js", + "#include test.js", + "#include ../objdir/baz.js", + "fin", + "", + ] + ), + } + + preprocessed = ( + '//@line 1 "$SRCDIR/foo.js"\n' + "bazfoobar\n" + '//@line 2 "$SRCDIR/bar.js"\n' + "@foo@\n" + '//@line 3 "$SRCDIR/foo.js"\n' + "bazbarfoo\n" + '//@line 2 "$SRCDIR/bar.js"\n' + "foobarbaz\n" + '//@line 3 "$SRCDIR/test.js"\n' + "barfoobaz\n" + '//@line 1 "$OBJDIR/baz.js"\n' + "baz\n" + '//@line 6 "$SRCDIR/f.js"\n' + "fin\n" + ) + + # Try with separate srcdir/objdir + with MockedOpen(files): + self.pp.topsrcdir = os.path.abspath("srcdir") + self.pp.topobjdir = os.path.abspath("objdir") + self.pp.do_include("srcdir/f.js") + self.assertEqual(self.pp.out.getvalue(), preprocessed) + + # Try again with relative objdir + self.setUp() + files["srcdir/objdir/baz.js"] = files["objdir/baz.js"] + del files["objdir/baz.js"] + files["srcdir/f.js"] = files["srcdir/f.js"].replace("../", "") + with MockedOpen(files): + self.pp.topsrcdir = os.path.abspath("srcdir") + self.pp.topobjdir = os.path.abspath("srcdir/objdir") + self.pp.do_include("srcdir/f.js") + self.assertEqual(self.pp.out.getvalue(), preprocessed) + + def test_include_missing_file(self): + with MockedOpen({"f": "#include foo\n"}): + with self.assertRaises(Preprocessor.Error) as e: + self.pp.do_include("f") + self.assertEqual(e.exception.key, "FILE_NOT_FOUND") + + def test_include_undefined_variable(self): + with MockedOpen({"f": "#filter substitution\n#include @foo@\n"}): + with self.assertRaises(Preprocessor.Error) as e: + self.pp.do_include("f") + self.assertEqual(e.exception.key, "UNDEFINED_VAR") + + def test_include_literal_at(self): + files = { + "@foo@": "#define foo foobarbaz\n", + "f": "#include @foo@\n#filter substitution\n@foo@\n", + } + + with MockedOpen(files): + self.pp.do_include("f") + self.assertEqual(self.pp.out.getvalue(), "foobarbaz\n") + + def test_command_line_literal_at(self): + with MockedOpen({"@foo@.in": "@foo@\n"}): + self.pp.handleCommandLine(["-Fsubstitution", "-Dfoo=foobarbaz", "@foo@.in"]) + self.assertEqual(self.pp.out.getvalue(), "foobarbaz\n") + + def test_invalid_ifdef(self): + with MockedOpen({"dummy": "#ifdef FOO == BAR\nPASS\n#endif"}): + with self.assertRaises(Preprocessor.Error) as e: + self.pp.do_include("dummy") + self.assertEqual(e.exception.key, "INVALID_VAR") + + with MockedOpen({"dummy": "#ifndef FOO == BAR\nPASS\n#endif"}): + with self.assertRaises(Preprocessor.Error) as e: + self.pp.do_include("dummy") + self.assertEqual(e.exception.key, "INVALID_VAR") + + # Trailing whitespaces, while not nice, shouldn't be an error. + self.do_include_pass( + [ + "#ifndef FOO ", + "PASS", + "#endif", + ] + ) + + +if __name__ == "__main__": + main() diff --git a/python/mozbuild/mozbuild/test/test_pythonutil.py b/python/mozbuild/mozbuild/test/test_pythonutil.py new file mode 100644 index 0000000000..6ebb5cc46e --- /dev/null +++ b/python/mozbuild/mozbuild/test/test_pythonutil.py @@ -0,0 +1,24 @@ +# 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/. + +import os + +from mozunit import main + +from mozbuild.pythonutil import iter_modules_in_path + + +def test_iter_modules_in_path(): + tests_path = os.path.normcase(os.path.dirname(__file__)) + paths = list(iter_modules_in_path(tests_path)) + assert set(paths) == set( + [ + os.path.join(os.path.abspath(tests_path), "__init__.py"), + os.path.join(os.path.abspath(tests_path), "test_pythonutil.py"), + ] + ) + + +if __name__ == "__main__": + main() diff --git a/python/mozbuild/mozbuild/test/test_rewrite_mozbuild.py b/python/mozbuild/mozbuild/test/test_rewrite_mozbuild.py new file mode 100644 index 0000000000..467295c9e9 --- /dev/null +++ b/python/mozbuild/mozbuild/test/test_rewrite_mozbuild.py @@ -0,0 +1,515 @@ +# coding: utf-8 +# 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/. + +import os +import tempfile +import unittest + +from mozunit import main + +import mozbuild.vendor.rewrite_mozbuild as mu + +SAMPLE_PIXMAN_MOZBUILD = """ +if CONFIG['OS_ARCH'] != 'Darwin' and CONFIG['CC_TYPE'] in ('clang', 'gcc'): + if CONFIG['HAVE_ARM_NEON']: + SOURCES += [ + "pixman-arm-neon-asm-bilinear.S", + "pixman-arm-neon-asm.S", + ] + if CONFIG['HAVE_ARM_SIMD']: + SOURCES += [ + 'pixman-arm-simd-asm-scaled.S', + 'pixman-arm-simd-asm.S'] + +SOURCES += ['pixman-region32.c', + 'pixman-solid-fill.c', + 'pixman-trap.c', + 'pixman-utils.c', + 'pixman-x86.c', + 'pixman.c', +] + +if use_sse2: + DEFINES['USE_SSE'] = True + DEFINES['USE_SSE2'] = True + SOURCES += ['pixman-sse2.c'] + SOURCES['pixman-sse2.c'].flags += CONFIG['SSE_FLAGS'] + CONFIG['SSE2_FLAGS'] + if CONFIG['CC_TYPE'] in ('clang', 'gcc'): + SOURCES['pixman-sse2.c'].flags += ['-Winline'] +""" + +SAMPLE_DAV1D_MOZBUILD = """ +SOURCES += [ + '../../third_party/dav1d/src/cdf.c', + '../../third_party/dav1d/src/cpu.c', + ] +EXPORTS = [ + '../../third_party/dav1d/src/header1.h', + '../../third_party/dav1d/src/header2.h', + ] +""" + + +SAMPLE_JPEGXL_MOZBUILD = """ +SOURCES += [ + "/third_party/jpeg-xl/lib/jxl/ac_strategy.cc", + "/third_party/jpeg-xl/lib/jxl/alpha.cc", + "/third_party/jpeg-xl/lib/jxl/ans_common.cc", + "/third_party/jpeg-xl/lib/jxl/aux_out.cc", + ] +EXPORTS.bob.carol = [ + "/third_party/jpeg-xl/lib/jxl/header1.hpp", + "/third_party/jpeg-xl/lib/jxl/header2.h", +] +""" + + +def _make_mozbuild_directory_structure(mozbuild_path, contents): + d = tempfile.TemporaryDirectory() + os.makedirs(os.path.join(d.name, os.path.split(mozbuild_path)[0])) + + arcconfig = open(os.path.join(d.name, ".arcconfig"), mode="w") + arcconfig.close() + + mozbuild = open(os.path.join(d.name, mozbuild_path), mode="w") + mozbuild.write(contents) + mozbuild.close() + + return d + + +class TestUtils(unittest.TestCase): + def test_normalize_filename(self): + self.assertEqual(mu.normalize_filename("foo/bar/moz.build", "/"), "/") + self.assertEqual( + mu.normalize_filename("foo/bar/moz.build", "a.c"), "foo/bar/a.c" + ) + self.assertEqual( + mu.normalize_filename("foo/bar/moz.build", "baz/a.c"), "foo/bar/baz/a.c" + ) + self.assertEqual(mu.normalize_filename("foo/bar/moz.build", "/a.c"), "/a.c") + + def test_unnormalize_filename(self): + test_vectors = [ + ("foo/bar/moz.build", "/"), + ("foo/bar/moz.build", "a.c"), + ("foo/bar/moz.build", "baz/a.c"), + ("foo/bar/moz.build", "/a.c"), + ] + + for vector in test_vectors: + mozbuild, file = vector + self.assertEqual( + mu.unnormalize_filename( + mozbuild, mu.normalize_filename(mozbuild, file) + ), + file, + ) + + def test_find_all_posible_assignments_from_filename(self): + test_vectors = [ + # ( + # target_filename_normalized + # source_assignments + # expected + # ) + ( + "root/dir/asm/blah.S", + { + "> SOURCES": ["root/dir/main.c"], + "> if conditional > SOURCES": ["root/dir/asm/blah.S"], + }, + {"> if conditional > SOURCES": ["root/dir/asm/blah.S"]}, + ), + ( + "root/dir/dostuff.c", + { + "> SOURCES": ["root/dir/main.c"], + "> if conditional > SOURCES": ["root/dir/asm/blah.S"], + }, + {"> SOURCES": ["root/dir/main.c"]}, + ), + ] + + for vector in test_vectors: + target_filename_normalized, source_assignments, expected = vector + actual = mu.find_all_posible_assignments_from_filename( + source_assignments, target_filename_normalized + ) + self.assertEqual(actual, expected) + + def test_filenames_directory_is_in_filename_list(self): + test_vectors = [ + # ( + # normalized filename + # list of normalized_filenames + # expected + # ) + ("foo/bar/a.c", ["foo/b.c"], False), + ("foo/bar/a.c", ["foo/b.c", "foo/bar/c.c"], True), + ("foo/bar/a.c", ["foo/b.c", "foo/bar/baz/d.c"], False), + ] + for vector in test_vectors: + normalized_filename, list_of_normalized_filesnames, expected = vector + actual = mu.filenames_directory_is_in_filename_list( + normalized_filename, list_of_normalized_filesnames + ) + self.assertEqual(actual, expected) + + def test_guess_best_assignment(self): + test_vectors = [ + # ( + # filename_normalized + # source_assignments + # expected + # ) + ( + "foo/asm_arm.c", + { + "> SOURCES": ["foo/main.c", "foo/all_utility.c"], + "> if ASM > SOURCES": ["foo/asm_x86.c"], + }, + "> if ASM > SOURCES", + ) + ] + for vector in test_vectors: + normalized_filename, source_assignments, expected = vector + actual, _ = mu.guess_best_assignment( + source_assignments, normalized_filename + ) + self.assertEqual(actual, expected) + + def test_mozbuild_removing(self): + test_vectors = [ + ( + "media/dav1d/moz.build", + SAMPLE_DAV1D_MOZBUILD, + "third_party/dav1d/src/cdf.c", + "media/dav1d/", + "third-party/dav1d/", + " '../../third_party/dav1d/src/cdf.c',\n", + ), + ( + "media/dav1d/moz.build", + SAMPLE_DAV1D_MOZBUILD, + "third_party/dav1d/src/header1.h", + "media/dav1d/", + "third-party/dav1d/", + " '../../third_party/dav1d/src/header1.h',\n", + ), + ( + "media/jxl/moz.build", + SAMPLE_JPEGXL_MOZBUILD, + "third_party/jpeg-xl/lib/jxl/alpha.cc", + "media/jxl/", + "third-party/jpeg-xl/", + ' "/third_party/jpeg-xl/lib/jxl/alpha.cc",\n', + ), + ( + "media/jxl/moz.build", + SAMPLE_JPEGXL_MOZBUILD, + "third_party/jpeg-xl/lib/jxl/header1.hpp", + "media/jxl/", + "third-party/jpeg-xl/", + ' "/third_party/jpeg-xl/lib/jxl/header1.hpp",\n', + ), + ] + + for vector in test_vectors: + ( + mozbuild_path, + mozbuild_contents, + file_to_remove, + moz_yaml_dir, + vendoring_dir, + replace_str, + ) = vector + + startdir = os.getcwd() + try: + mozbuild_dir = _make_mozbuild_directory_structure( + mozbuild_path, mozbuild_contents + ) + os.chdir(mozbuild_dir.name) + + mu.remove_file_from_moz_build_file( + file_to_remove, + moz_yaml_dir=moz_yaml_dir, + vendoring_dir=vendoring_dir, + ) + + with open(os.path.join(mozbuild_dir.name, mozbuild_path)) as file: + contents = file.read() + + expected_output = mozbuild_contents.replace(replace_str, "") + if contents != expected_output: + print("File to remove:", file_to_remove) + print("Contents:") + print("-------------------") + print(contents) + print("-------------------") + print("Expected:") + print("-------------------") + print(expected_output) + print("-------------------") + self.assertEqual(contents, expected_output) + finally: + os.chdir(startdir) + + def test_mozbuild_adding(self): + test_vectors = [ + ( + "media/dav1d/moz.build", + SAMPLE_DAV1D_MOZBUILD, + "third_party/dav1d/src/cdf2.c", + "media/dav1d/", + "third-party/dav1d/", + "cdf.c',\n", + "cdf.c',\n '../../third_party/dav1d/src/cdf2.c',\n", + ), + ( + "media/dav1d/moz.build", + SAMPLE_DAV1D_MOZBUILD, + "third_party/dav1d/src/header3.h", + "media/dav1d/", + "third-party/dav1d/", + "header2.h',\n", + "header2.h',\n '../../third_party/dav1d/src/header3.h',\n", + ), + ( + "media/jxl/moz.build", + SAMPLE_JPEGXL_MOZBUILD, + "third_party/jpeg-xl/lib/jxl/alpha2.cc", + "media/jxl/", + "third-party/jpeg-xl/", + 'alpha.cc",\n', + 'alpha.cc",\n "/third_party/jpeg-xl/lib/jxl/alpha2.cc",\n', + ), + ( + "media/jxl/moz.build", + SAMPLE_JPEGXL_MOZBUILD, + "third_party/jpeg-xl/lib/jxl/header3.hpp", + "media/jxl/", + "third-party/jpeg-xl/", + 'header2.h",\n', + 'header2.h",\n "/third_party/jpeg-xl/lib/jxl/header3.hpp",\n', + ), + ] + + for vector in test_vectors: + ( + mozbuild_path, + mozbuild_contents, + file_to_add, + moz_yaml_dir, + vendoring_dir, + search_str, + replace_str, + ) = vector + + startdir = os.getcwd() + try: + mozbuild_dir = _make_mozbuild_directory_structure( + mozbuild_path, mozbuild_contents + ) + os.chdir(mozbuild_dir.name) + + mu.add_file_to_moz_build_file( + file_to_add, moz_yaml_dir=moz_yaml_dir, vendoring_dir=vendoring_dir + ) + + with open(os.path.join(mozbuild_dir.name, mozbuild_path)) as file: + contents = file.read() + + expected_output = mozbuild_contents.replace(search_str, replace_str) + if contents != expected_output: + print("File to add:", file_to_add) + print("Contents:") + print("-------------------") + print(contents) + print("-------------------") + print("Expected:") + print("-------------------") + print(expected_output) + print("-------------------") + self.assertEqual(contents, expected_output) + finally: + os.chdir(startdir) + + # This test is legacy. I'm keeping it around, but new test vectors should be added to the + # non-internal test to exercise the public API. + def test_mozbuild_adding_internal(self): + test_vectors = [ + # ( + # mozbuild_contents + # unnormalized_filename_to_add, + # unnormalized_list_of_files + # expected_output + # ) + ( + SAMPLE_PIXMAN_MOZBUILD, + "pixman-sse2-more.c", + ["pixman-sse2.c"], + SAMPLE_PIXMAN_MOZBUILD.replace( + "SOURCES += ['pixman-sse2.c']", + "SOURCES += ['pixman-sse2-more.c','pixman-sse2.c']", + ), + ), + ( + SAMPLE_PIXMAN_MOZBUILD, + "pixman-trap-more.c", + [ + "pixman-region32.c", + "pixman-solid-fill.c", + "pixman-trap.c", + "pixman-utils.c", + "pixman-x86.c", + "pixman.c", + ], + SAMPLE_PIXMAN_MOZBUILD.replace( + "'pixman-trap.c',", "'pixman-trap-more.c',\n 'pixman-trap.c'," + ), + ), + ( + SAMPLE_PIXMAN_MOZBUILD, + "pixman-arm-neon-asm-more.S", + ["pixman-arm-neon-asm-bilinear.S", "pixman-arm-neon-asm.S"], + SAMPLE_PIXMAN_MOZBUILD.replace( + '"pixman-arm-neon-asm.S"', + '"pixman-arm-neon-asm-more.S",\n "pixman-arm-neon-asm.S"', + ), + ), + ( + SAMPLE_PIXMAN_MOZBUILD, + "pixman-arm-simd-asm-smore.S", + ["pixman-arm-simd-asm-scaled.S", "pixman-arm-simd-asm.S"], + SAMPLE_PIXMAN_MOZBUILD.replace( + "'pixman-arm-simd-asm.S'", + "'pixman-arm-simd-asm-smore.S',\n 'pixman-arm-simd-asm.S'", + ), + ), + ( + SAMPLE_PIXMAN_MOZBUILD, + "pixman-arm-simd-asn.S", + ["pixman-arm-simd-asm-scaled.S", "pixman-arm-simd-asm.S"], + SAMPLE_PIXMAN_MOZBUILD.replace( + "'pixman-arm-simd-asm.S'", + "'pixman-arm-simd-asm.S',\n 'pixman-arm-simd-asn.S'", + ), + ), + ] + + for vector in test_vectors: + ( + mozbuild_contents, + unnormalized_filename_to_add, + unnormalized_list_of_files, + expected_output, + ) = vector + + fd, filename = tempfile.mkstemp(text=True) + os.close(fd) + file = open(filename, mode="w") + file.write(mozbuild_contents) + file.close() + + mu.edit_moz_build_file_to_add_file( + filename, unnormalized_filename_to_add, unnormalized_list_of_files + ) + + with open(filename) as file: + contents = file.read() + os.remove(filename) + + if contents != expected_output: + print("File to add:", unnormalized_filename_to_add) + print("Contents:") + print("-------------------") + print(contents) + print("-------------------") + print("Expected:") + print("-------------------") + print(expected_output) + print("-------------------") + self.assertEqual(contents, expected_output) + + # This test is legacy. I'm keeping it around, but new test vectors should be added to the + # non-internal test to exercise the public API. + def test_mozbuild_removing_internal(self): + test_vectors = [ + # ( + # mozbuild_contents + # unnormalized_filename_to_add + # expected_output + # ) + ( + SAMPLE_PIXMAN_MOZBUILD, + "pixman-sse2.c", + SAMPLE_PIXMAN_MOZBUILD.replace( + "SOURCES += ['pixman-sse2.c']", "SOURCES += []" + ), + ), + ( + SAMPLE_PIXMAN_MOZBUILD, + "pixman-trap.c", + SAMPLE_PIXMAN_MOZBUILD.replace(" 'pixman-trap.c',\n", ""), + ), + ( + SAMPLE_PIXMAN_MOZBUILD, + "pixman-arm-neon-asm.S", + SAMPLE_PIXMAN_MOZBUILD.replace( + ' "pixman-arm-neon-asm.S",\n', "" + ), + ), + ( + SAMPLE_PIXMAN_MOZBUILD, + "pixman-arm-simd-asm.S", + SAMPLE_PIXMAN_MOZBUILD.replace( + " 'pixman-arm-simd-asm.S'", " " + ), + ), + ( + SAMPLE_PIXMAN_MOZBUILD, + "pixman-region32.c", + SAMPLE_PIXMAN_MOZBUILD.replace("'pixman-region32.c',", ""), + ), + ] + + for vector in test_vectors: + ( + mozbuild_contents, + unnormalized_filename_to_remove, + expected_output, + ) = vector + + fd, filename = tempfile.mkstemp(text=True) + os.close(fd) + file = open(filename, mode="w") + file.write(mozbuild_contents) + file.close() + + mu.edit_moz_build_file_to_remove_file( + filename, unnormalized_filename_to_remove + ) + + with open(filename) as file: + contents = file.read() + os.remove(filename) + + if contents != expected_output: + print("File to remove:", unnormalized_filename_to_remove) + print("Contents:") + print("-------------------") + print(contents) + print("-------------------") + print("Expected:") + print("-------------------") + print(expected_output) + print("-------------------") + self.assertEqual(contents, expected_output) + + +if __name__ == "__main__": + main() diff --git a/python/mozbuild/mozbuild/test/test_telemetry.py b/python/mozbuild/mozbuild/test/test_telemetry.py new file mode 100644 index 0000000000..894e32ee2d --- /dev/null +++ b/python/mozbuild/mozbuild/test/test_telemetry.py @@ -0,0 +1,102 @@ +# Any copyright is dedicated to the Public Domain. +# http://creativecommons.org/publicdomain/zero/1.0/ + +import os + +import buildconfig +import mozunit + +from mozbuild.telemetry import filter_args + +TELEMETRY_LOAD_ERROR = """ +Error loading telemetry. mach output: +========================================================= +%s +========================================================= +""" + + +def test_path_filtering(): + srcdir_path = os.path.join(buildconfig.topsrcdir, "a") + srcdir_path_2 = os.path.join(buildconfig.topsrcdir, "a/b/c") + objdir_path = os.path.join(buildconfig.topobjdir, "x") + objdir_path_2 = os.path.join(buildconfig.topobjdir, "x/y/z") + home_path = os.path.join(os.path.expanduser("~"), "something_in_home") + other_path = "/other/path" + args = filter_args( + "pass", + [ + "python", + "-c", + "pass", + srcdir_path, + srcdir_path_2, + objdir_path, + objdir_path_2, + home_path, + other_path, + ], + buildconfig.topsrcdir, + buildconfig.topobjdir, + cwd=buildconfig.topsrcdir, + ) + + expected = [ + "a", + "a/b/c", + "$topobjdir/x", + "$topobjdir/x/y/z", + "$HOME/something_in_home", + "<path omitted>", + ] + assert args == expected + + +def test_path_filtering_in_objdir(): + srcdir_path = os.path.join(buildconfig.topsrcdir, "a") + srcdir_path_2 = os.path.join(buildconfig.topsrcdir, "a/b/c") + objdir_path = os.path.join(buildconfig.topobjdir, "x") + objdir_path_2 = os.path.join(buildconfig.topobjdir, "x/y/z") + other_path = "/other/path" + args = filter_args( + "pass", + [ + "python", + "-c", + "pass", + srcdir_path, + srcdir_path_2, + objdir_path, + objdir_path_2, + other_path, + ], + buildconfig.topsrcdir, + buildconfig.topobjdir, + cwd=buildconfig.topobjdir, + ) + expected = ["$topsrcdir/a", "$topsrcdir/a/b/c", "x", "x/y/z", "<path omitted>"] + assert args == expected + + +def test_path_filtering_other_cwd(tmpdir): + srcdir_path = os.path.join(buildconfig.topsrcdir, "a") + srcdir_path_2 = os.path.join(buildconfig.topsrcdir, "a/b/c") + other_path = str(tmpdir.join("other")) + args = filter_args( + "pass", + ["python", "-c", "pass", srcdir_path, srcdir_path_2, other_path], + buildconfig.topsrcdir, + buildconfig.topobjdir, + cwd=str(tmpdir), + ) + expected = [ + "$topsrcdir/a", + "$topsrcdir/a/b/c", + # cwd-relative paths should be relativized + "other", + ] + assert args == expected + + +if __name__ == "__main__": + mozunit.main() diff --git a/python/mozbuild/mozbuild/test/test_telemetry_settings.py b/python/mozbuild/mozbuild/test/test_telemetry_settings.py new file mode 100644 index 0000000000..edd293ecf4 --- /dev/null +++ b/python/mozbuild/mozbuild/test/test_telemetry_settings.py @@ -0,0 +1,173 @@ +# 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/. + +import os +from unittest import mock +from unittest.mock import Mock + +import mozunit +import pytest +import requests +from mach.config import ConfigSettings +from mach.decorators import SettingsProvider +from mach.settings import MachSettings +from mach.telemetry import ( + initialize_telemetry_setting, + record_telemetry_settings, + resolve_is_employee, +) + + +@SettingsProvider +class OtherSettings: + config_settings = [("foo.bar", "int", "", 1), ("build.abc", "string", "", "")] + + +def record_enabled_telemetry(mozbuild_path, settings): + record_telemetry_settings(settings, mozbuild_path, True) + + +@pytest.fixture +def settings(): + s = ConfigSettings() + s.register_provider(MachSettings) + s.register_provider(OtherSettings) + return s + + +def load_settings_file(mozbuild_path, settings): + settings.load_file(os.path.join(mozbuild_path, "machrc")) + + +def write_config(mozbuild_path, contents): + with open(os.path.join(mozbuild_path, "machrc"), "w") as f: + f.write(contents) + + +def test_nonexistent(tmpdir, settings): + record_enabled_telemetry(tmpdir, settings) + load_settings_file(tmpdir, settings) + assert settings.mach_telemetry.is_enabled + + +def test_file_exists_no_build_section(tmpdir, settings): + write_config( + tmpdir, + """[foo] +bar = 2 +""", + ) + record_enabled_telemetry(tmpdir, settings) + load_settings_file(tmpdir, settings) + assert settings.mach_telemetry.is_enabled + assert settings.foo.bar == 2 + + +def test_existing_build_section(tmpdir, settings): + write_config( + tmpdir, + """[foo] +bar = 2 + +[build] +abc = xyz +""", + ) + record_enabled_telemetry(tmpdir, settings) + load_settings_file(tmpdir, settings) + assert settings.mach_telemetry.is_enabled + assert settings.build.abc == "xyz" + assert settings.foo.bar == 2 + + +def test_malformed_file(tmpdir, settings): + """Ensure that a malformed config file doesn't cause breakage.""" + write_config( + tmpdir, + """[foo +bar = 1 +""", + ) + record_enabled_telemetry(tmpdir, settings) + # Can't load_settings config, it will not have been written! + + +def _initialize_telemetry(settings, is_employee, contributor_prompt_response=None): + with mock.patch( + "mach.telemetry.resolve_is_employee", return_value=is_employee + ), mock.patch( + "mach.telemetry.prompt_telemetry_message_contributor", + return_value=contributor_prompt_response, + ) as prompt_mock, mock.patch( + "subprocess.run", return_value=Mock(returncode=0) + ), mock.patch( + "mach.config.ConfigSettings" + ): + initialize_telemetry_setting(settings, "", "") + return prompt_mock.call_count == 1 + + +def test_initialize_new_contributor_deny_telemetry(settings): + did_prompt = _initialize_telemetry(settings, False, False) + assert did_prompt + assert not settings.mach_telemetry.is_enabled + assert settings.mach_telemetry.is_set_up + assert settings.mach_telemetry.is_done_first_time_setup + + +def test_initialize_new_contributor_allow_telemetry(settings): + did_prompt = _initialize_telemetry(settings, False, True) + assert did_prompt + assert settings.mach_telemetry.is_enabled + assert settings.mach_telemetry.is_set_up + assert settings.mach_telemetry.is_done_first_time_setup + + +def test_initialize_new_employee(settings): + did_prompt = _initialize_telemetry(settings, True) + assert not did_prompt + assert settings.mach_telemetry.is_enabled + assert settings.mach_telemetry.is_set_up + assert settings.mach_telemetry.is_done_first_time_setup + + +def test_initialize_noop_when_telemetry_disabled_env(monkeypatch): + monkeypatch.setenv("DISABLE_TELEMETRY", "1") + with mock.patch("mach.telemetry.record_telemetry_settings") as record_mock: + did_prompt = _initialize_telemetry(None, False) + assert record_mock.call_count == 0 + assert not did_prompt + + +def test_initialize_noop_when_request_error(settings): + with mock.patch( + "mach.telemetry.resolve_is_employee", + side_effect=requests.exceptions.RequestException("Unlucky"), + ), mock.patch("mach.telemetry.record_telemetry_settings") as record_mock: + initialize_telemetry_setting(None, None, None) + assert record_mock.call_count == 0 + + +def test_resolve_is_employee(): + def mock_and_run(is_employee_bugzilla, is_employee_vcs): + with mock.patch( + "mach.telemetry.resolve_is_employee_by_credentials", + return_value=is_employee_bugzilla, + ), mock.patch( + "mach.telemetry.resolve_is_employee_by_vcs", return_value=is_employee_vcs + ): + return resolve_is_employee(None) + + assert not mock_and_run(False, False) + assert not mock_and_run(False, True) + assert not mock_and_run(False, None) + assert mock_and_run(True, False) + assert mock_and_run(True, True) + assert mock_and_run(True, None) + assert not mock_and_run(None, False) + assert mock_and_run(None, True) + + +if __name__ == "__main__": + mozunit.main() diff --git a/python/mozbuild/mozbuild/test/test_util.py b/python/mozbuild/mozbuild/test/test_util.py new file mode 100644 index 0000000000..4a37172ca4 --- /dev/null +++ b/python/mozbuild/mozbuild/test/test_util.py @@ -0,0 +1,890 @@ +# coding: utf-8 +# 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/. + +import copy +import hashlib +import itertools +import os +import string +import sys +import unittest + +import pytest +import six +from mozfile.mozfile import NamedTemporaryFile +from mozunit import main + +from mozbuild.util import ( + EnumString, + EnumStringComparisonError, + HierarchicalStringList, + MozbuildDeletionError, + ReadOnlyDict, + StrictOrderingOnAppendList, + StrictOrderingOnAppendListWithAction, + StrictOrderingOnAppendListWithFlagsFactory, + TypedList, + TypedNamedTuple, + UnsortedError, + expand_variables, + group_unified_files, + hash_file, + hexdump, + memoize, + memoized_property, + pair, + resolve_target_to_make, +) + +if sys.version_info[0] == 3: + str_type = "str" +else: + str_type = "unicode" + +data_path = os.path.abspath(os.path.dirname(__file__)) +data_path = os.path.join(data_path, "data") + + +class TestHashing(unittest.TestCase): + def test_hash_file_known_hash(self): + """Ensure a known hash value is recreated.""" + data = b"The quick brown fox jumps over the lazy cog" + expected = "de9f2c7fd25e1b3afad3e85a0bd17d9b100db4b3" + + temp = NamedTemporaryFile() + temp.write(data) + temp.flush() + + actual = hash_file(temp.name) + + self.assertEqual(actual, expected) + + def test_hash_file_large(self): + """Ensure that hash_file seems to work with a large file.""" + data = b"x" * 1048576 + + hasher = hashlib.sha1() + hasher.update(data) + expected = hasher.hexdigest() + + temp = NamedTemporaryFile() + temp.write(data) + temp.flush() + + actual = hash_file(temp.name) + + self.assertEqual(actual, expected) + + +class TestResolveTargetToMake(unittest.TestCase): + def setUp(self): + self.topobjdir = data_path + + def assertResolve(self, path, expected): + # Handle Windows path separators. + (reldir, target) = resolve_target_to_make(self.topobjdir, path) + if reldir is not None: + reldir = reldir.replace(os.sep, "/") + if target is not None: + target = target.replace(os.sep, "/") + self.assertEqual((reldir, target), expected) + + def test_root_path(self): + self.assertResolve("/test-dir", ("test-dir", None)) + self.assertResolve("/test-dir/with", ("test-dir/with", None)) + self.assertResolve("/test-dir/without", ("test-dir", None)) + self.assertResolve("/test-dir/without/with", ("test-dir/without/with", None)) + + def test_dir(self): + self.assertResolve("test-dir", ("test-dir", None)) + self.assertResolve("test-dir/with", ("test-dir/with", None)) + self.assertResolve("test-dir/with", ("test-dir/with", None)) + self.assertResolve("test-dir/without", ("test-dir", None)) + self.assertResolve("test-dir/without/with", ("test-dir/without/with", None)) + + def test_top_level(self): + self.assertResolve("package", (None, "package")) + # Makefile handling shouldn't affect top-level targets. + self.assertResolve("Makefile", (None, "Makefile")) + + def test_regular_file(self): + self.assertResolve("test-dir/with/file", ("test-dir/with", "file")) + self.assertResolve( + "test-dir/with/without/file", ("test-dir/with", "without/file") + ) + self.assertResolve( + "test-dir/with/without/with/file", ("test-dir/with/without/with", "file") + ) + + self.assertResolve("test-dir/without/file", ("test-dir", "without/file")) + self.assertResolve( + "test-dir/without/with/file", ("test-dir/without/with", "file") + ) + self.assertResolve( + "test-dir/without/with/without/file", + ("test-dir/without/with", "without/file"), + ) + + def test_Makefile(self): + self.assertResolve("test-dir/with/Makefile", ("test-dir", "with/Makefile")) + self.assertResolve( + "test-dir/with/without/Makefile", ("test-dir/with", "without/Makefile") + ) + self.assertResolve( + "test-dir/with/without/with/Makefile", + ("test-dir/with", "without/with/Makefile"), + ) + + self.assertResolve( + "test-dir/without/Makefile", ("test-dir", "without/Makefile") + ) + self.assertResolve( + "test-dir/without/with/Makefile", ("test-dir", "without/with/Makefile") + ) + self.assertResolve( + "test-dir/without/with/without/Makefile", + ("test-dir/without/with", "without/Makefile"), + ) + + +class TestHierarchicalStringList(unittest.TestCase): + def setUp(self): + self.EXPORTS = HierarchicalStringList() + + def test_exports_append(self): + self.assertEqual(self.EXPORTS._strings, []) + self.EXPORTS += ["foo.h"] + self.assertEqual(self.EXPORTS._strings, ["foo.h"]) + self.EXPORTS += ["bar.h"] + self.assertEqual(self.EXPORTS._strings, ["foo.h", "bar.h"]) + + def test_exports_subdir(self): + self.assertEqual(self.EXPORTS._children, {}) + self.EXPORTS.foo += ["foo.h"] + six.assertCountEqual(self, self.EXPORTS._children, {"foo": True}) + self.assertEqual(self.EXPORTS.foo._strings, ["foo.h"]) + self.EXPORTS.bar += ["bar.h"] + six.assertCountEqual(self, self.EXPORTS._children, {"foo": True, "bar": True}) + self.assertEqual(self.EXPORTS.foo._strings, ["foo.h"]) + self.assertEqual(self.EXPORTS.bar._strings, ["bar.h"]) + + def test_exports_multiple_subdir(self): + self.EXPORTS.foo.bar = ["foobar.h"] + six.assertCountEqual(self, self.EXPORTS._children, {"foo": True}) + six.assertCountEqual(self, self.EXPORTS.foo._children, {"bar": True}) + six.assertCountEqual(self, self.EXPORTS.foo.bar._children, {}) + self.assertEqual(self.EXPORTS._strings, []) + self.assertEqual(self.EXPORTS.foo._strings, []) + self.assertEqual(self.EXPORTS.foo.bar._strings, ["foobar.h"]) + + def test_invalid_exports_append(self): + with self.assertRaises(ValueError) as ve: + self.EXPORTS += "foo.h" + six.assertRegex( + self, + str(ve.exception), + "Expected a list of strings, not <(?:type|class) '%s'>" % str_type, + ) + + def test_invalid_exports_set(self): + with self.assertRaises(ValueError) as ve: + self.EXPORTS.foo = "foo.h" + + six.assertRegex( + self, + str(ve.exception), + "Expected a list of strings, not <(?:type|class) '%s'>" % str_type, + ) + + def test_invalid_exports_append_base(self): + with self.assertRaises(ValueError) as ve: + self.EXPORTS += "foo.h" + + six.assertRegex( + self, + str(ve.exception), + "Expected a list of strings, not <(?:type|class) '%s'>" % str_type, + ) + + def test_invalid_exports_bool(self): + with self.assertRaises(ValueError) as ve: + self.EXPORTS += [True] + + six.assertRegex( + self, + str(ve.exception), + "Expected a list of strings, not an element of " "<(?:type|class) 'bool'>", + ) + + def test_del_exports(self): + with self.assertRaises(MozbuildDeletionError): + self.EXPORTS.foo += ["bar.h"] + del self.EXPORTS.foo + + def test_unsorted(self): + with self.assertRaises(UnsortedError): + self.EXPORTS += ["foo.h", "bar.h"] + + with self.assertRaises(UnsortedError): + self.EXPORTS.foo = ["foo.h", "bar.h"] + + with self.assertRaises(UnsortedError): + self.EXPORTS.foo += ["foo.h", "bar.h"] + + def test_reassign(self): + self.EXPORTS.foo = ["foo.h"] + + with self.assertRaises(KeyError): + self.EXPORTS.foo = ["bar.h"] + + def test_walk(self): + l = HierarchicalStringList() + l += ["root1", "root2", "root3"] + l.child1 += ["child11", "child12", "child13"] + l.child1.grandchild1 += ["grandchild111", "grandchild112"] + l.child1.grandchild2 += ["grandchild121", "grandchild122"] + l.child2.grandchild1 += ["grandchild211", "grandchild212"] + l.child2.grandchild1 += ["grandchild213", "grandchild214"] + + els = list((path, list(seq)) for path, seq in l.walk()) + self.assertEqual( + els, + [ + ("", ["root1", "root2", "root3"]), + ("child1", ["child11", "child12", "child13"]), + ("child1/grandchild1", ["grandchild111", "grandchild112"]), + ("child1/grandchild2", ["grandchild121", "grandchild122"]), + ( + "child2/grandchild1", + [ + "grandchild211", + "grandchild212", + "grandchild213", + "grandchild214", + ], + ), + ], + ) + + def test_merge(self): + l1 = HierarchicalStringList() + l1 += ["root1", "root2", "root3"] + l1.child1 += ["child11", "child12", "child13"] + l1.child1.grandchild1 += ["grandchild111", "grandchild112"] + l1.child1.grandchild2 += ["grandchild121", "grandchild122"] + l1.child2.grandchild1 += ["grandchild211", "grandchild212"] + l1.child2.grandchild1 += ["grandchild213", "grandchild214"] + l2 = HierarchicalStringList() + l2.child1 += ["child14", "child15"] + l2.child1.grandchild2 += ["grandchild123"] + l2.child3 += ["child31", "child32"] + + l1 += l2 + els = list((path, list(seq)) for path, seq in l1.walk()) + self.assertEqual( + els, + [ + ("", ["root1", "root2", "root3"]), + ("child1", ["child11", "child12", "child13", "child14", "child15"]), + ("child1/grandchild1", ["grandchild111", "grandchild112"]), + ( + "child1/grandchild2", + ["grandchild121", "grandchild122", "grandchild123"], + ), + ( + "child2/grandchild1", + [ + "grandchild211", + "grandchild212", + "grandchild213", + "grandchild214", + ], + ), + ("child3", ["child31", "child32"]), + ], + ) + + +class TestStrictOrderingOnAppendList(unittest.TestCase): + def test_init(self): + l = StrictOrderingOnAppendList() + self.assertEqual(len(l), 0) + + l = StrictOrderingOnAppendList(["a", "b", "c"]) + self.assertEqual(len(l), 3) + + with self.assertRaises(UnsortedError): + StrictOrderingOnAppendList(["c", "b", "a"]) + + self.assertEqual(len(l), 3) + + def test_extend(self): + l = StrictOrderingOnAppendList() + l.extend(["a", "b"]) + self.assertEqual(len(l), 2) + self.assertIsInstance(l, StrictOrderingOnAppendList) + + with self.assertRaises(UnsortedError): + l.extend(["d", "c"]) + + self.assertEqual(len(l), 2) + + def test_slicing(self): + l = StrictOrderingOnAppendList() + l[:] = ["a", "b"] + self.assertEqual(len(l), 2) + self.assertIsInstance(l, StrictOrderingOnAppendList) + + with self.assertRaises(UnsortedError): + l[:] = ["b", "a"] + + self.assertEqual(len(l), 2) + + def test_add(self): + l = StrictOrderingOnAppendList() + l2 = l + ["a", "b"] + self.assertEqual(len(l), 0) + self.assertEqual(len(l2), 2) + self.assertIsInstance(l2, StrictOrderingOnAppendList) + + with self.assertRaises(UnsortedError): + l2 = l + ["b", "a"] + + self.assertEqual(len(l), 0) + + def test_iadd(self): + l = StrictOrderingOnAppendList() + l += ["a", "b"] + self.assertEqual(len(l), 2) + self.assertIsInstance(l, StrictOrderingOnAppendList) + + with self.assertRaises(UnsortedError): + l += ["b", "a"] + + self.assertEqual(len(l), 2) + + def test_add_after_iadd(self): + l = StrictOrderingOnAppendList(["b"]) + l += ["a"] + l2 = l + ["c", "d"] + self.assertEqual(len(l), 2) + self.assertEqual(len(l2), 4) + self.assertIsInstance(l2, StrictOrderingOnAppendList) + with self.assertRaises(UnsortedError): + l2 = l + ["d", "c"] + + self.assertEqual(len(l), 2) + + def test_add_StrictOrderingOnAppendList(self): + l = StrictOrderingOnAppendList() + l += ["c", "d"] + l += ["a", "b"] + l2 = StrictOrderingOnAppendList() + with self.assertRaises(UnsortedError): + l2 += list(l) + # Adding a StrictOrderingOnAppendList to another shouldn't throw + l2 += l + + +class TestStrictOrderingOnAppendListWithAction(unittest.TestCase): + def setUp(self): + self.action = lambda a: (a, id(a)) + + def assertSameList(self, expected, actual): + self.assertEqual(len(expected), len(actual)) + for idx, item in enumerate(actual): + self.assertEqual(item, expected[idx]) + + def test_init(self): + l = StrictOrderingOnAppendListWithAction(action=self.action) + self.assertEqual(len(l), 0) + original = ["a", "b", "c"] + l = StrictOrderingOnAppendListWithAction(["a", "b", "c"], action=self.action) + expected = [self.action(i) for i in original] + self.assertSameList(expected, l) + + with self.assertRaises(ValueError): + StrictOrderingOnAppendListWithAction("abc", action=self.action) + + with self.assertRaises(ValueError): + StrictOrderingOnAppendListWithAction() + + def test_extend(self): + l = StrictOrderingOnAppendListWithAction(action=self.action) + original = ["a", "b"] + l.extend(original) + expected = [self.action(i) for i in original] + self.assertSameList(expected, l) + + with self.assertRaises(ValueError): + l.extend("ab") + + def test_slicing(self): + l = StrictOrderingOnAppendListWithAction(action=self.action) + original = ["a", "b"] + l[:] = original + expected = [self.action(i) for i in original] + self.assertSameList(expected, l) + + with self.assertRaises(ValueError): + l[:] = "ab" + + def test_add(self): + l = StrictOrderingOnAppendListWithAction(action=self.action) + original = ["a", "b"] + l2 = l + original + expected = [self.action(i) for i in original] + self.assertSameList(expected, l2) + + with self.assertRaises(ValueError): + l + "abc" + + def test_iadd(self): + l = StrictOrderingOnAppendListWithAction(action=self.action) + original = ["a", "b"] + l += original + expected = [self.action(i) for i in original] + self.assertSameList(expected, l) + + with self.assertRaises(ValueError): + l += "abc" + + +class TestStrictOrderingOnAppendListWithFlagsFactory(unittest.TestCase): + def test_strict_ordering_on_append_list_with_flags_factory(self): + cls = StrictOrderingOnAppendListWithFlagsFactory( + { + "foo": bool, + "bar": int, + } + ) + + l = cls() + l += ["a", "b"] + + with self.assertRaises(Exception): + l["a"] = "foo" + + with self.assertRaises(Exception): + l["c"] + + self.assertEqual(l["a"].foo, False) + l["a"].foo = True + self.assertEqual(l["a"].foo, True) + + with self.assertRaises(TypeError): + l["a"].bar = "bar" + + self.assertEqual(l["a"].bar, 0) + l["a"].bar = 42 + self.assertEqual(l["a"].bar, 42) + + l["b"].foo = True + self.assertEqual(l["b"].foo, True) + + with self.assertRaises(AttributeError): + l["b"].baz = False + + l["b"].update(foo=False, bar=12) + self.assertEqual(l["b"].foo, False) + self.assertEqual(l["b"].bar, 12) + + with self.assertRaises(AttributeError): + l["b"].update(xyz=1) + + def test_strict_ordering_on_append_list_with_flags_factory_extend(self): + FooList = StrictOrderingOnAppendListWithFlagsFactory( + {"foo": bool, "bar": six.text_type} + ) + foo = FooList(["a", "b", "c"]) + foo["a"].foo = True + foo["b"].bar = "bar" + + # Don't allow extending lists with different flag definitions. + BarList = StrictOrderingOnAppendListWithFlagsFactory( + {"foo": six.text_type, "baz": bool} + ) + bar = BarList(["d", "e", "f"]) + bar["d"].foo = "foo" + bar["e"].baz = True + with self.assertRaises(ValueError): + foo + bar + with self.assertRaises(ValueError): + bar + foo + + # It's not obvious what to do with duplicate list items with possibly + # different flag values, so don't allow that case. + with self.assertRaises(ValueError): + foo + foo + + def assertExtended(l): + self.assertEqual(len(l), 6) + self.assertEqual(l["a"].foo, True) + self.assertEqual(l["b"].bar, "bar") + self.assertTrue("c" in l) + self.assertEqual(l["d"].foo, True) + self.assertEqual(l["e"].bar, "bar") + self.assertTrue("f" in l) + + # Test extend. + zot = FooList(["d", "e", "f"]) + zot["d"].foo = True + zot["e"].bar = "bar" + zot.extend(foo) + assertExtended(zot) + + # Test __add__. + zot = FooList(["d", "e", "f"]) + zot["d"].foo = True + zot["e"].bar = "bar" + assertExtended(foo + zot) + assertExtended(zot + foo) + + # Test __iadd__. + foo += zot + assertExtended(foo) + + # Test __setitem__. + foo[3:] = [] + self.assertEqual(len(foo), 3) + foo[3:] = zot + assertExtended(foo) + + +class TestMemoize(unittest.TestCase): + def test_memoize(self): + self._count = 0 + + @memoize + def wrapped(a, b): + self._count += 1 + return a + b + + self.assertEqual(self._count, 0) + self.assertEqual(wrapped(1, 1), 2) + self.assertEqual(self._count, 1) + self.assertEqual(wrapped(1, 1), 2) + self.assertEqual(self._count, 1) + self.assertEqual(wrapped(2, 1), 3) + self.assertEqual(self._count, 2) + self.assertEqual(wrapped(1, 2), 3) + self.assertEqual(self._count, 3) + self.assertEqual(wrapped(1, 2), 3) + self.assertEqual(self._count, 3) + self.assertEqual(wrapped(1, 1), 2) + self.assertEqual(self._count, 3) + + def test_memoize_method(self): + class foo(object): + def __init__(self): + self._count = 0 + + @memoize + def wrapped(self, a, b): + self._count += 1 + return a + b + + instance = foo() + refcount = sys.getrefcount(instance) + self.assertEqual(instance._count, 0) + self.assertEqual(instance.wrapped(1, 1), 2) + self.assertEqual(instance._count, 1) + self.assertEqual(instance.wrapped(1, 1), 2) + self.assertEqual(instance._count, 1) + self.assertEqual(instance.wrapped(2, 1), 3) + self.assertEqual(instance._count, 2) + self.assertEqual(instance.wrapped(1, 2), 3) + self.assertEqual(instance._count, 3) + self.assertEqual(instance.wrapped(1, 2), 3) + self.assertEqual(instance._count, 3) + self.assertEqual(instance.wrapped(1, 1), 2) + self.assertEqual(instance._count, 3) + + # Memoization of methods is expected to not keep references to + # instances, so the refcount shouldn't have changed after executing the + # memoized method. + self.assertEqual(refcount, sys.getrefcount(instance)) + + def test_memoized_property(self): + class foo(object): + def __init__(self): + self._count = 0 + + @memoized_property + def wrapped(self): + self._count += 1 + return 42 + + instance = foo() + self.assertEqual(instance._count, 0) + self.assertEqual(instance.wrapped, 42) + self.assertEqual(instance._count, 1) + self.assertEqual(instance.wrapped, 42) + self.assertEqual(instance._count, 1) + + +class TestTypedList(unittest.TestCase): + def test_init(self): + cls = TypedList(int) + l = cls() + self.assertEqual(len(l), 0) + + l = cls([1, 2, 3]) + self.assertEqual(len(l), 3) + + with self.assertRaises(ValueError): + cls([1, 2, "c"]) + + def test_extend(self): + cls = TypedList(int) + l = cls() + l.extend([1, 2]) + self.assertEqual(len(l), 2) + self.assertIsInstance(l, cls) + + with self.assertRaises(ValueError): + l.extend([3, "c"]) + + self.assertEqual(len(l), 2) + + def test_slicing(self): + cls = TypedList(int) + l = cls() + l[:] = [1, 2] + self.assertEqual(len(l), 2) + self.assertIsInstance(l, cls) + + with self.assertRaises(ValueError): + l[:] = [3, "c"] + + self.assertEqual(len(l), 2) + + def test_add(self): + cls = TypedList(int) + l = cls() + l2 = l + [1, 2] + self.assertEqual(len(l), 0) + self.assertEqual(len(l2), 2) + self.assertIsInstance(l2, cls) + + with self.assertRaises(ValueError): + l2 = l + [3, "c"] + + self.assertEqual(len(l), 0) + + def test_iadd(self): + cls = TypedList(int) + l = cls() + l += [1, 2] + self.assertEqual(len(l), 2) + self.assertIsInstance(l, cls) + + with self.assertRaises(ValueError): + l += [3, "c"] + + self.assertEqual(len(l), 2) + + def test_add_coercion(self): + objs = [] + + class Foo(object): + def __init__(self, obj): + objs.append(obj) + + cls = TypedList(Foo) + l = cls() + l += [1, 2] + self.assertEqual(len(objs), 2) + self.assertEqual(type(l[0]), Foo) + self.assertEqual(type(l[1]), Foo) + + # Adding a TypedList to a TypedList shouldn't trigger coercion again + l2 = cls() + l2 += l + self.assertEqual(len(objs), 2) + self.assertEqual(type(l2[0]), Foo) + self.assertEqual(type(l2[1]), Foo) + + # Adding a TypedList to a TypedList shouldn't even trigger the code + # that does coercion at all. + l2 = cls() + list.__setitem__(l, slice(0, -1), [1, 2]) + l2 += l + self.assertEqual(len(objs), 2) + self.assertEqual(type(l2[0]), int) + self.assertEqual(type(l2[1]), int) + + def test_memoized(self): + cls = TypedList(int) + cls2 = TypedList(str) + self.assertEqual(TypedList(int), cls) + self.assertNotEqual(cls, cls2) + + +class TypedTestStrictOrderingOnAppendList(unittest.TestCase): + def test_init(self): + class Unicode(six.text_type): + def __new__(cls, other): + if not isinstance(other, six.text_type): + raise ValueError() + return six.text_type.__new__(cls, other) + + cls = TypedList(Unicode, StrictOrderingOnAppendList) + l = cls() + self.assertEqual(len(l), 0) + + l = cls(["a", "b", "c"]) + self.assertEqual(len(l), 3) + + with self.assertRaises(UnsortedError): + cls(["c", "b", "a"]) + + with self.assertRaises(ValueError): + cls(["a", "b", 3]) + + self.assertEqual(len(l), 3) + + +class TestTypedNamedTuple(unittest.TestCase): + def test_simple(self): + FooBar = TypedNamedTuple("FooBar", [("foo", six.text_type), ("bar", int)]) + + t = FooBar(foo="foo", bar=2) + self.assertEqual(type(t), FooBar) + self.assertEqual(t.foo, "foo") + self.assertEqual(t.bar, 2) + self.assertEqual(t[0], "foo") + self.assertEqual(t[1], 2) + + FooBar("foo", 2) + + with self.assertRaises(TypeError): + FooBar("foo", "not integer") + with self.assertRaises(TypeError): + FooBar(2, 4) + + # Passing a tuple as the first argument is the same as passing multiple + # arguments. + t1 = ("foo", 3) + t2 = FooBar(t1) + self.assertEqual(type(t2), FooBar) + self.assertEqual(FooBar(t1), FooBar("foo", 3)) + + +class TestGroupUnifiedFiles(unittest.TestCase): + FILES = ["%s.cpp" % letter for letter in string.ascii_lowercase] + + def test_multiple_files(self): + mapping = list(group_unified_files(self.FILES, "Unified", "cpp", 5)) + + def check_mapping(index, expected_num_source_files): + (unified_file, source_files) = mapping[index] + + self.assertEqual(unified_file, "Unified%d.cpp" % index) + self.assertEqual(len(source_files), expected_num_source_files) + + all_files = list(itertools.chain(*[files for (_, files) in mapping])) + self.assertEqual(len(all_files), len(self.FILES)) + self.assertEqual(set(all_files), set(self.FILES)) + + expected_amounts = [5, 5, 5, 5, 5, 1] + for i, amount in enumerate(expected_amounts): + check_mapping(i, amount) + + +class TestMisc(unittest.TestCase): + def test_pair(self): + self.assertEqual(list(pair([1, 2, 3, 4, 5, 6])), [(1, 2), (3, 4), (5, 6)]) + + self.assertEqual( + list(pair([1, 2, 3, 4, 5, 6, 7])), [(1, 2), (3, 4), (5, 6), (7, None)] + ) + + def test_expand_variables(self): + self.assertEqual(expand_variables("$(var)", {"var": "value"}), "value") + + self.assertEqual( + expand_variables("$(a) and $(b)", {"a": "1", "b": "2"}), "1 and 2" + ) + + self.assertEqual( + expand_variables("$(a) and $(undefined)", {"a": "1", "b": "2"}), "1 and " + ) + + self.assertEqual( + expand_variables( + "before $(string) between $(list) after", + {"string": "abc", "list": ["a", "b", "c"]}, + ), + "before abc between a b c after", + ) + + +class TestEnumString(unittest.TestCase): + def test_string(self): + class CompilerType(EnumString): + POSSIBLE_VALUES = ("gcc", "clang", "clang-cl") + + type = CompilerType("gcc") + self.assertEqual(type, "gcc") + self.assertNotEqual(type, "clang") + self.assertNotEqual(type, "clang-cl") + self.assertIn(type, ("gcc", "clang-cl")) + self.assertNotIn(type, ("clang", "clang-cl")) + + with self.assertRaises(EnumStringComparisonError): + self.assertEqual(type, "foo") + + with self.assertRaises(EnumStringComparisonError): + self.assertNotEqual(type, "foo") + + with self.assertRaises(EnumStringComparisonError): + self.assertIn(type, ("foo", "gcc")) + + with self.assertRaises(ValueError): + type = CompilerType("foo") + + +class TestHexDump(unittest.TestCase): + @unittest.skipUnless(six.PY3, "requires Python 3") + def test_hexdump(self): + self.assertEqual( + hexdump("abcdef123💩ZYXWVU".encode("utf-8")), + [ + "00 61 62 63 64 65 66 31 32 33 f0 9f 92 a9 5a 59 58 |abcdef123....ZYX|\n", + "10 57 56 55 |WVU |\n", + ], + ) + + +def test_read_only_dict(): + d = ReadOnlyDict(foo="bar") + with pytest.raises(Exception): + d["foo"] = "baz" + + with pytest.raises(Exception): + d.update({"foo": "baz"}) + + with pytest.raises(Exception): + del d["foo"] + + # ensure copy still works + d_copy = d.copy() + assert d == d_copy + # TODO Returning a dict here feels like a bug, but there are places in-tree + # relying on this behaviour. + assert isinstance(d_copy, dict) + + d_copy = copy.copy(d) + assert d == d_copy + assert isinstance(d_copy, ReadOnlyDict) + + d_copy = copy.deepcopy(d) + assert d == d_copy + assert isinstance(d_copy, ReadOnlyDict) + + +if __name__ == "__main__": + main() diff --git a/python/mozbuild/mozbuild/test/test_util_fileavoidwrite.py b/python/mozbuild/mozbuild/test/test_util_fileavoidwrite.py new file mode 100644 index 0000000000..38c8941562 --- /dev/null +++ b/python/mozbuild/mozbuild/test/test_util_fileavoidwrite.py @@ -0,0 +1,110 @@ +# 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/. +"""Tests for the FileAvoidWrite object.""" + +import locale +import pathlib + +import pytest +from mozunit import main + +from mozbuild.util import FileAvoidWrite + + +@pytest.fixture +def tmp_path(tmpdir): + """Backport of the tmp_path fixture from pytest 3.9.1.""" + return pathlib.Path(str(tmpdir)) + + +def test_overwrite_contents(tmp_path): + file = tmp_path / "file.txt" + file.write_text("abc") + + faw = FileAvoidWrite(str(file)) + faw.write("bazqux") + + assert faw.close() == (True, True) + assert file.read_text() == "bazqux" + + +def test_store_new_contents(tmp_path): + file = tmp_path / "file.txt" + + faw = FileAvoidWrite(str(file)) + faw.write("content") + + assert faw.close() == (False, True) + assert file.read_text() == "content" + + +def test_change_binary_file_contents(tmp_path): + file = tmp_path / "file.dat" + file.write_bytes(b"\0") + + faw = FileAvoidWrite(str(file), readmode="rb") + faw.write(b"\0\0\0") + + assert faw.close() == (True, True) + assert file.read_bytes() == b"\0\0\0" + + +def test_obj_as_context_manager(tmp_path): + file = tmp_path / "file.txt" + + with FileAvoidWrite(str(file)) as fh: + fh.write("foobar") + + assert file.read_text() == "foobar" + + +def test_no_write_happens_if_file_contents_same(tmp_path): + file = tmp_path / "file.txt" + file.write_text("content") + original_write_time = file.stat().st_mtime + + faw = FileAvoidWrite(str(file)) + faw.write("content") + + assert faw.close() == (True, False) + assert file.stat().st_mtime == original_write_time + + +def test_diff_not_created_by_default(tmp_path): + file = tmp_path / "file.txt" + faw = FileAvoidWrite(str(file)) + faw.write("dummy") + faw.close() + assert faw.diff is None + + +def test_diff_update(tmp_path): + file = tmp_path / "diffable.txt" + file.write_text("old") + + faw = FileAvoidWrite(str(file), capture_diff=True) + faw.write("new") + faw.close() + + diff = "\n".join(faw.diff) + assert "-old" in diff + assert "+new" in diff + + +@pytest.mark.skipif( + locale.getdefaultlocale()[1] == "cp1252", + reason="Fails on win32 terminals with cp1252 encoding", +) +def test_write_unicode(tmp_path): + # Unicode grinning face :D + binary_emoji = b"\xf0\x9f\x98\x80" + + file = tmp_path / "file.dat" + faw = FileAvoidWrite(str(file)) + faw.write(binary_emoji) + faw.close() + + +if __name__ == "__main__": + main() diff --git a/python/mozbuild/mozbuild/test/test_vendor.py b/python/mozbuild/mozbuild/test/test_vendor.py new file mode 100644 index 0000000000..07ba088337 --- /dev/null +++ b/python/mozbuild/mozbuild/test/test_vendor.py @@ -0,0 +1,48 @@ +# 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/. + +import os +import shutil +import subprocess +import tempfile +from unittest.mock import Mock + +import mozunit +from buildconfig import topsrcdir + +from mozbuild.vendor.vendor_python import VendorPython + + +def test_up_to_date_vendor(): + with tempfile.TemporaryDirectory() as work_dir: + subprocess.check_call(["hg", "init", work_dir]) + os.makedirs(os.path.join(work_dir, "third_party")) + shutil.copytree( + os.path.join(topsrcdir, os.path.join("third_party", "python")), + os.path.join(work_dir, os.path.join("third_party", "python")), + ) + + # Run the vendoring process + vendor = VendorPython( + work_dir, None, Mock(), topobjdir=os.path.join(work_dir, "obj") + ) + vendor.vendor() + + # Verify that re-vendoring did not cause file changes. + # Note that we don't want hg-ignored generated files + # to bust the diff, so we exclude them (pycache, egg-info). + subprocess.check_call( + [ + "diff", + "-r", + os.path.join(topsrcdir, os.path.join("third_party", "python")), + os.path.join(work_dir, os.path.join("third_party", "python")), + "--exclude=__pycache__", + "--strip-trailing-cr", + ] + ) + + +if __name__ == "__main__": + mozunit.main() diff --git a/python/mozbuild/mozbuild/test/test_vendor_tools.py b/python/mozbuild/mozbuild/test/test_vendor_tools.py new file mode 100644 index 0000000000..271be6d7da --- /dev/null +++ b/python/mozbuild/mozbuild/test/test_vendor_tools.py @@ -0,0 +1,90 @@ +# 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/. + +import mozunit + +from mozbuild.vendor.vendor_manifest import list_of_paths_to_readable_string + + +def test_list_of_paths_to_readable_string(): + paths = ["/tmp/a", "/tmp/b"] + s = list_of_paths_to_readable_string(paths) + assert not s.endswith(", ]") + assert s.endswith("]") + assert "/tmp/a" in s + assert "/tmp/b" in s + + paths = ["/tmp/a", "/tmp/b", "/tmp/c", "/tmp/d"] + s = list_of_paths_to_readable_string(paths) + assert not s.endswith(", ") + assert s.endswith("]") + assert "/tmp/a" not in s + assert "/tmp/b" not in s + assert "4 items in /tmp" in s + + paths = [ + "/tmp/a", + "/tmp/b", + "/tmp/c", + "/tmp/d", + "/tmp/d", + "/tmp/d", + "/tmp/d", + "/tmp/d", + "/tmp/d", + "/tmp/d", + ] + s = list_of_paths_to_readable_string(paths) + assert not s.endswith(", ") + assert s.endswith("]") + assert "/tmp/a" not in s + assert " a" not in s + assert "/tmp/b" not in s + assert "10 (omitted) items in /tmp" in s + + paths = ["/tmp", "/foo"] + s = list_of_paths_to_readable_string(paths) + assert not s.endswith(", ") + assert s.endswith("]") + assert "/tmp" in s + assert "/foo" in s + + paths = [ + "/tmp/a", + "/tmp/b", + "/tmp/c", + "/tmp/d", + "/tmp/d", + "/tmp/d", + "/tmp/d", + "/tmp/d", + "/tmp/d", + "/tmp/d", + ] + paths.extend(["/foo/w", "/foo/x", "/foo/y", "/foo/z"]) + paths.extend(["/bar/m", "/bar/n"]) + paths.extend(["/etc"]) + s = list_of_paths_to_readable_string(paths) + assert not s.endswith(", ") + assert s.endswith("]") + assert "/tmp/a" not in s + assert " d" not in s + assert "/tmp/b" not in s + assert "10 (omitted) items in /tmp" in s + + assert "/foo/w" not in s + assert "/foo/x" not in s + assert "4 items in /foo" in s + assert " w" in s + + assert "/bar/m" in s + assert "/bar/n" in s + + assert "/etc" in s + + assert len(s) < len(str(paths)) + + +if __name__ == "__main__": + mozunit.main() diff --git a/python/mozbuild/mozbuild/test/vendor/__init__.py b/python/mozbuild/mozbuild/test/vendor/__init__.py new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/python/mozbuild/mozbuild/test/vendor/__init__.py diff --git a/python/mozbuild/mozbuild/test/vendor/test_vendor_manifest.py b/python/mozbuild/mozbuild/test/vendor/test_vendor_manifest.py new file mode 100644 index 0000000000..6023ede58e --- /dev/null +++ b/python/mozbuild/mozbuild/test/vendor/test_vendor_manifest.py @@ -0,0 +1,134 @@ +# 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 https://mozilla.org/MPL/2.0/. + +# 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 contextlib import nullcontext as does_not_raise + +import pytest +from mozunit import main + +from mozbuild.vendor.vendor_manifest import _replace_in_file + + +@pytest.mark.parametrize( + "pattern,replacement,input_data,expected,exception", + [ + ( + "lookup.c", + "vorbis_lookup.c", + '#include "lookup.c"\n', + '#include "vorbis_lookup.c"\n', + does_not_raise(), + ), + ( + "lookup.c", + "vorbis_lookup.c", + '#include "vorbis_lookup.c"\n', + '#include "vorbis_lookup.c"\n', + pytest.raises(Exception), + ), + ( + "@VCS_TAG@", + "616bfd1506a8a75c6a358e578cbec9ca11931502", + '#define DAV1D_VERSION "@VCS_TAG@"\n', + '#define DAV1D_VERSION "616bfd1506a8a75c6a358e578cbec9ca11931502"\n', + does_not_raise(), + ), + ( + "@VCS_TAG@", + "616bfd1506a8a75c6a358e578cbec9ca11931502", + '#define DAV1D_VERSION "8a651f3a1f40fe73847423fb6a5776103cb42218"\n', + '#define DAV1D_VERSION "616bfd1506a8a75c6a358e578cbec9ca11931502"\n', + pytest.raises(Exception), + ), + ( + "[regexy pattern]", + "replaced", + "here\nis [regexy pattern]\n", + "here\nis replaced\n", + does_not_raise(), + ), + ( + "some (other) regex.", + "replaced,", + "This can be some (other) regex. Yo!\n", + "This can be replaced, Yo!\n", + does_not_raise(), + ), + ], +) +def test_replace_in_file( + tmp_path, pattern, replacement, input_data, expected, exception +): + file = tmp_path / "file.txt" + file.write_text(input_data) + + with exception: + _replace_in_file(file, pattern, replacement, regex=False) + assert file.read_text() == expected + + +@pytest.mark.parametrize( + "pattern,replacement,input_data,expected,exception", + [ + ( + r"\[tag v[1-9\.]+\]", + "[tag v1.2.13]", + "Version:\n#[tag v1.2.12]\n", + "Version:\n#[tag v1.2.13]\n", + does_not_raise(), + ), + ( + r"\[tag v[1-9\.]+\]", + "[tag v1.2.13]", + "Version:\n#[tag v1.2.13]\n", + "Version:\n#[tag v1.2.13]\n", + does_not_raise(), + ), + ( + r"\[tag v[1-9\.]+\]", + "[tag v0.17.0]", + "Version:\n#[tag v0.16.3]\n", + "Version:\n#[tag v0.17.0]\n", + pytest.raises(Exception), + ), + ( + r"\[tag v[1-9\.]+\]", + "[tag v0.17.0]", + "Version:\n#[tag v0.16.3]\n", + "Version:\n#[tag v0.17.0]\n", + pytest.raises(Exception), + ), + ( + r'DEFINES\["OPUS_VERSION"\] = "(.+)"', + 'DEFINES["OPUS_VERSION"] = "5023249b5c935545fb02dbfe845cae996ecfc8bb"', + 'DEFINES["OPUS_BUILD"] = True\nDEFINES["OPUS_VERSION"] = "8a651f3a1f40fe73847423fb6a5776103cb42218"\nDEFINES["USE_ALLOCA"] = True\n', + 'DEFINES["OPUS_BUILD"] = True\nDEFINES["OPUS_VERSION"] = "5023249b5c935545fb02dbfe845cae996ecfc8bb"\nDEFINES["USE_ALLOCA"] = True\n', + does_not_raise(), + ), + ( + r'DEFINES\["OPUS_VERSION"\] = "(.+)"', + 'DEFINES["OPUS_VERSION"] = "5023249b5c935545fb02dbfe845cae996ecfc8bb"', + 'DEFINES["OPUS_BUILD"] = True\nDEFINES["OPUS_TAG"] = "8a651f3a1f40fe73847423fb6a5776103cb42218"\nDEFINES["USE_ALLOCA"] = True\n', + 'DEFINES["OPUS_BUILD"] = True\nDEFINES["OPUS_VERSION"] = "5023249b5c935545fb02dbfe845cae996ecfc8bb"\nDEFINES["USE_ALLOCA"] = True\n', + pytest.raises(Exception), + ), + ], +) +def test_replace_in_file_regex( + tmp_path, pattern, replacement, input_data, expected, exception +): + file = tmp_path / "file.txt" + file.write_text(input_data) + + with exception: + _replace_in_file(file, pattern, replacement, regex=True) + assert file.read_text() == expected + + +if __name__ == "__main__": + main() diff --git a/python/mozbuild/mozbuild/test/vendor_requirements.in b/python/mozbuild/mozbuild/test/vendor_requirements.in new file mode 100644 index 0000000000..2b04a38b1b --- /dev/null +++ b/python/mozbuild/mozbuild/test/vendor_requirements.in @@ -0,0 +1,6 @@ +# Until bug 1724273 lands, python-testing code that uses a site is not possible. Work around
+# this by representing the "vendor" site's dependency as a separate "requirements.txt" file,
+# which can be used by python-test's "requirements" feature.
+poetry==1.4
+poetry-core==1.5.1
+tomlkit==0.12.3
diff --git a/python/mozbuild/mozbuild/test/vendor_requirements.txt b/python/mozbuild/mozbuild/test/vendor_requirements.txt new file mode 100644 index 0000000000..592f081ce6 --- /dev/null +++ b/python/mozbuild/mozbuild/test/vendor_requirements.txt @@ -0,0 +1,512 @@ +# +# This file is autogenerated by pip-compile with Python 3.10 +# by the following command: +# +# pip-compile --generate-hashes --output-file=python/mozbuild/mozbuild/test/vendor_requirements.txt python/mozbuild/mozbuild/test/vendor_requirements.in +# +appdirs==1.4.4 \ + --hash=sha256:7d5d0167b2b1ba821647616af46a749d1c653740dd0d2415100fe26e27afdf41 \ + --hash=sha256:a841dacd6b99318a741b166adb07e19ee71a274450e68237b4650ca1055ab128 + # via virtualenv +attrs==22.2.0 \ + --hash=sha256:29e95c7f6778868dbd49170f98f8818f78f3dc5e0e37c0b1f474e3561b240836 \ + --hash=sha256:c9227bfc2f01993c03f68db37d1d15c9690188323c067c641f1a35ca58185f99 + # via jsonschema +build==0.10.0 \ + --hash=sha256:af266720050a66c893a6096a2f410989eeac74ff9a68ba194b3f6473e8e26171 \ + --hash=sha256:d5b71264afdb5951d6704482aac78de887c80691c52b88a9ad195983ca2c9269 + # via poetry +cachecontrol[filecache]==0.12.10 \ + --hash=sha256:b0d43d8f71948ef5ebdee5fe236b86c6ffc7799370453dccb0e894c20dfa487c \ + --hash=sha256:d8aca75b82eec92d84b5d6eb8c8f66ea16f09d2adb09dbca27fe2d5fc8d3732d + # via + # cachecontrol + # poetry +certifi==2021.10.8 \ + --hash=sha256:78884e7c1d4b00ce3cea67b44566851c4343c120abd683433ce934a68ea58872 \ + --hash=sha256:d62a0163eb4c2344ac042ab2bdf75399a71a2d8c7d47eac2e2ee91b9d6339569 + # via requests +cffi==1.16.0 \ + --hash=sha256:0c9ef6ff37e974b73c25eecc13952c55bceed9112be2d9d938ded8e856138bcc \ + --hash=sha256:131fd094d1065b19540c3d72594260f118b231090295d8c34e19a7bbcf2e860a \ + --hash=sha256:1b8ebc27c014c59692bb2664c7d13ce7a6e9a629be20e54e7271fa696ff2b417 \ + --hash=sha256:2c56b361916f390cd758a57f2e16233eb4f64bcbeee88a4881ea90fca14dc6ab \ + --hash=sha256:2d92b25dbf6cae33f65005baf472d2c245c050b1ce709cc4588cdcdd5495b520 \ + --hash=sha256:31d13b0f99e0836b7ff893d37af07366ebc90b678b6664c955b54561fc36ef36 \ + --hash=sha256:32c68ef735dbe5857c810328cb2481e24722a59a2003018885514d4c09af9743 \ + --hash=sha256:3686dffb02459559c74dd3d81748269ffb0eb027c39a6fc99502de37d501faa8 \ + --hash=sha256:582215a0e9adbe0e379761260553ba11c58943e4bbe9c36430c4ca6ac74b15ed \ + --hash=sha256:5b50bf3f55561dac5438f8e70bfcdfd74543fd60df5fa5f62d94e5867deca684 \ + --hash=sha256:5bf44d66cdf9e893637896c7faa22298baebcd18d1ddb6d2626a6e39793a1d56 \ + --hash=sha256:6602bc8dc6f3a9e02b6c22c4fc1e47aa50f8f8e6d3f78a5e16ac33ef5fefa324 \ + --hash=sha256:673739cb539f8cdaa07d92d02efa93c9ccf87e345b9a0b556e3ecc666718468d \ + --hash=sha256:68678abf380b42ce21a5f2abde8efee05c114c2fdb2e9eef2efdb0257fba1235 \ + --hash=sha256:68e7c44931cc171c54ccb702482e9fc723192e88d25a0e133edd7aff8fcd1f6e \ + --hash=sha256:6b3d6606d369fc1da4fd8c357d026317fbb9c9b75d36dc16e90e84c26854b088 \ + --hash=sha256:748dcd1e3d3d7cd5443ef03ce8685043294ad6bd7c02a38d1bd367cfd968e000 \ + --hash=sha256:7651c50c8c5ef7bdb41108b7b8c5a83013bfaa8a935590c5d74627c047a583c7 \ + --hash=sha256:7b78010e7b97fef4bee1e896df8a4bbb6712b7f05b7ef630f9d1da00f6444d2e \ + --hash=sha256:7e61e3e4fa664a8588aa25c883eab612a188c725755afff6289454d6362b9673 \ + --hash=sha256:80876338e19c951fdfed6198e70bc88f1c9758b94578d5a7c4c91a87af3cf31c \ + --hash=sha256:8895613bcc094d4a1b2dbe179d88d7fb4a15cee43c052e8885783fac397d91fe \ + --hash=sha256:88e2b3c14bdb32e440be531ade29d3c50a1a59cd4e51b1dd8b0865c54ea5d2e2 \ + --hash=sha256:8f8e709127c6c77446a8c0a8c8bf3c8ee706a06cd44b1e827c3e6a2ee6b8c098 \ + --hash=sha256:9cb4a35b3642fc5c005a6755a5d17c6c8b6bcb6981baf81cea8bfbc8903e8ba8 \ + --hash=sha256:9f90389693731ff1f659e55c7d1640e2ec43ff725cc61b04b2f9c6d8d017df6a \ + --hash=sha256:a09582f178759ee8128d9270cd1344154fd473bb77d94ce0aeb2a93ebf0feaf0 \ + --hash=sha256:a6a14b17d7e17fa0d207ac08642c8820f84f25ce17a442fd15e27ea18d67c59b \ + --hash=sha256:a72e8961a86d19bdb45851d8f1f08b041ea37d2bd8d4fd19903bc3083d80c896 \ + --hash=sha256:abd808f9c129ba2beda4cfc53bde801e5bcf9d6e0f22f095e45327c038bfe68e \ + --hash=sha256:ac0f5edd2360eea2f1daa9e26a41db02dd4b0451b48f7c318e217ee092a213e9 \ + --hash=sha256:b29ebffcf550f9da55bec9e02ad430c992a87e5f512cd63388abb76f1036d8d2 \ + --hash=sha256:b2ca4e77f9f47c55c194982e10f058db063937845bb2b7a86c84a6cfe0aefa8b \ + --hash=sha256:b7be2d771cdba2942e13215c4e340bfd76398e9227ad10402a8767ab1865d2e6 \ + --hash=sha256:b84834d0cf97e7d27dd5b7f3aca7b6e9263c56308ab9dc8aae9784abb774d404 \ + --hash=sha256:b86851a328eedc692acf81fb05444bdf1891747c25af7529e39ddafaf68a4f3f \ + --hash=sha256:bcb3ef43e58665bbda2fb198698fcae6776483e0c4a631aa5647806c25e02cc0 \ + --hash=sha256:c0f31130ebc2d37cdd8e44605fb5fa7ad59049298b3f745c74fa74c62fbfcfc4 \ + --hash=sha256:c6a164aa47843fb1b01e941d385aab7215563bb8816d80ff3a363a9f8448a8dc \ + --hash=sha256:d8a9d3ebe49f084ad71f9269834ceccbf398253c9fac910c4fd7053ff1386936 \ + --hash=sha256:db8e577c19c0fda0beb7e0d4e09e0ba74b1e4c092e0e40bfa12fe05b6f6d75ba \ + --hash=sha256:dc9b18bf40cc75f66f40a7379f6a9513244fe33c0e8aa72e2d56b0196a7ef872 \ + --hash=sha256:e09f3ff613345df5e8c3667da1d918f9149bd623cd9070c983c013792a9a62eb \ + --hash=sha256:e4108df7fe9b707191e55f33efbcb2d81928e10cea45527879a4749cbe472614 \ + --hash=sha256:e6024675e67af929088fda399b2094574609396b1decb609c55fa58b028a32a1 \ + --hash=sha256:e70f54f1796669ef691ca07d046cd81a29cb4deb1e5f942003f401c0c4a2695d \ + --hash=sha256:e715596e683d2ce000574bae5d07bd522c781a822866c20495e52520564f0969 \ + --hash=sha256:e760191dd42581e023a68b758769e2da259b5d52e3103c6060ddc02c9edb8d7b \ + --hash=sha256:ed86a35631f7bfbb28e108dd96773b9d5a6ce4811cf6ea468bb6a359b256b1e4 \ + --hash=sha256:ee07e47c12890ef248766a6e55bd38ebfb2bb8edd4142d56db91b21ea68b7627 \ + --hash=sha256:fa3a0128b152627161ce47201262d3140edb5a5c3da88d73a1b790a959126956 \ + --hash=sha256:fcc8eb6d5902bb1cf6dc4f187ee3ea80a1eba0a89aba40a5cb20a5087d961357 + # via cryptography +charset-normalizer==2.0.12 \ + --hash=sha256:2857e29ff0d34db842cd7ca3230549d1a697f96ee6d3fb071cfa6c7393832597 \ + --hash=sha256:6881edbebdb17b39b4eaaa821b438bf6eddffb4468cf344f09f89def34a8b1df + # via requests +cleo==2.0.1 \ + --hash=sha256:6eb133670a3ed1f3b052d53789017b6e50fca66d1287e6e6696285f4cb8ea448 \ + --hash=sha256:eb4b2e1f3063c11085cebe489a6e9124163c226575a3c3be69b2e51af4a15ec5 + # via poetry +crashtest==0.4.1 \ + --hash=sha256:80d7b1f316ebfbd429f648076d6275c877ba30ba48979de4191714a75266f0ce \ + --hash=sha256:8d23eac5fa660409f57472e3851dab7ac18aba459a8d19cbbba86d3d5aecd2a5 + # via + # cleo + # poetry +cryptography==42.0.2 \ + --hash=sha256:087887e55e0b9c8724cf05361357875adb5c20dec27e5816b653492980d20380 \ + --hash=sha256:09a77e5b2e8ca732a19a90c5bca2d124621a1edb5438c5daa2d2738bfeb02589 \ + --hash=sha256:130c0f77022b2b9c99d8cebcdd834d81705f61c68e91ddd614ce74c657f8b3ea \ + --hash=sha256:141e2aa5ba100d3788c0ad7919b288f89d1fe015878b9659b307c9ef867d3a65 \ + --hash=sha256:28cb2c41f131a5758d6ba6a0504150d644054fd9f3203a1e8e8d7ac3aea7f73a \ + --hash=sha256:2f9f14185962e6a04ab32d1abe34eae8a9001569ee4edb64d2304bf0d65c53f3 \ + --hash=sha256:320948ab49883557a256eab46149df79435a22d2fefd6a66fe6946f1b9d9d008 \ + --hash=sha256:36d4b7c4be6411f58f60d9ce555a73df8406d484ba12a63549c88bd64f7967f1 \ + --hash=sha256:3b15c678f27d66d247132cbf13df2f75255627bcc9b6a570f7d2fd08e8c081d2 \ + --hash=sha256:3dbd37e14ce795b4af61b89b037d4bc157f2cb23e676fa16932185a04dfbf635 \ + --hash=sha256:4383b47f45b14459cab66048d384614019965ba6c1a1a141f11b5a551cace1b2 \ + --hash=sha256:44c95c0e96b3cb628e8452ec060413a49002a247b2b9938989e23a2c8291fc90 \ + --hash=sha256:4b063d3413f853e056161eb0c7724822a9740ad3caa24b8424d776cebf98e7ee \ + --hash=sha256:52ed9ebf8ac602385126c9a2fe951db36f2cb0c2538d22971487f89d0de4065a \ + --hash=sha256:55d1580e2d7e17f45d19d3b12098e352f3a37fe86d380bf45846ef257054b242 \ + --hash=sha256:5ef9bc3d046ce83c4bbf4c25e1e0547b9c441c01d30922d812e887dc5f125c12 \ + --hash=sha256:5fa82a26f92871eca593b53359c12ad7949772462f887c35edaf36f87953c0e2 \ + --hash=sha256:61321672b3ac7aade25c40449ccedbc6db72c7f5f0fdf34def5e2f8b51ca530d \ + --hash=sha256:701171f825dcab90969596ce2af253143b93b08f1a716d4b2a9d2db5084ef7be \ + --hash=sha256:841ec8af7a8491ac76ec5a9522226e287187a3107e12b7d686ad354bb78facee \ + --hash=sha256:8a06641fb07d4e8f6c7dda4fc3f8871d327803ab6542e33831c7ccfdcb4d0ad6 \ + --hash=sha256:8e88bb9eafbf6a4014d55fb222e7360eef53e613215085e65a13290577394529 \ + --hash=sha256:a00aee5d1b6c20620161984f8ab2ab69134466c51f58c052c11b076715e72929 \ + --hash=sha256:a047682d324ba56e61b7ea7c7299d51e61fd3bca7dad2ccc39b72bd0118d60a1 \ + --hash=sha256:a7ef8dd0bf2e1d0a27042b231a3baac6883cdd5557036f5e8df7139255feaac6 \ + --hash=sha256:ad28cff53f60d99a928dfcf1e861e0b2ceb2bc1f08a074fdd601b314e1cc9e0a \ + --hash=sha256:b9097a208875fc7bbeb1286d0125d90bdfed961f61f214d3f5be62cd4ed8a446 \ + --hash=sha256:b97fe7d7991c25e6a31e5d5e795986b18fbbb3107b873d5f3ae6dc9a103278e9 \ + --hash=sha256:e0ec52ba3c7f1b7d813cd52649a5b3ef1fc0d433219dc8c93827c57eab6cf888 \ + --hash=sha256:ea2c3ffb662fec8bbbfce5602e2c159ff097a4631d96235fcf0fb00e59e3ece4 \ + --hash=sha256:fa3dec4ba8fb6e662770b74f62f1a0c7d4e37e25b58b2bf2c1be4c95372b4a33 \ + --hash=sha256:fbeb725c9dc799a574518109336acccaf1303c30d45c075c665c0793c2f79a7f + # via secretstorage +distlib==0.3.4 \ + --hash=sha256:6564fe0a8f51e734df6333d08b8b94d4ea8ee6b99b5ed50613f731fd4089f34b \ + --hash=sha256:e4b58818180336dc9c529bfb9a0b58728ffc09ad92027a3f30b7cd91e3458579 + # via virtualenv +dulwich==0.21.3 \ + --hash=sha256:026427b5ef0f1fe138ed22078e49b00175b58b11e5c18e2be00f06ee0782603b \ + --hash=sha256:03ed9448f2944166e28aa8d3f4c8feeceb5c6880e9ffe5ab274869d45abd9589 \ + --hash=sha256:058aaba18aefe18fcd84b216fd34d032ad453967dcf3dee263278951cd43e2d4 \ + --hash=sha256:075c8e9d2694ff16fc6e8a5ec0c771b7c33be12e4ebecc346fd74315d3d84605 \ + --hash=sha256:08ee426b609dab552839b5c7394ae9af2112c164bb727b7f85a69980eced9251 \ + --hash=sha256:092829f27a2c87cdf6b6523216822859ecf01d281ddfae0e58cad1f44adafff6 \ + --hash=sha256:0b541bd58426a30753ab12cc024ba29b6699d197d9d0d9f130b9768ab20e0e6a \ + --hash=sha256:0cd83f84e58aa59fb9d85cf15e74be83a5be876ac5876d5030f60fcce7ab36f1 \ + --hash=sha256:1799c04bd53ec404ebd2c82c1d66197a31e5f0549c95348bb7d3f57a28c94241 \ + --hash=sha256:1cf246530b8d574b33a9614da76881b96c190c0fe78f76ab016c88082c0da051 \ + --hash=sha256:208d01a9cda1bae16c92e8c54e806701a16969346aba44b8d6921c6c227277a9 \ + --hash=sha256:21ee962211839bb6e52d41f363ce9dbb0638d341a1c02263e163d69012f58b25 \ + --hash=sha256:250ec581682af846cb85844f8032b7642dd278006b1c3abd5e8e718eba0b1b00 \ + --hash=sha256:25376efc6ea2ee9daa868a120d4f9c905dcb7774f68931be921fba41a657f58a \ + --hash=sha256:2bf2be68fddfc0adfe43be99ab31f6b0f16b9ef1e40464679ba831ff615ad4a3 \ + --hash=sha256:33f73e8f902c6397cc73a727db1f6e75add8ce894bfbb1a15daa2f7a4138a744 \ + --hash=sha256:3b048f84c94c3284f29bf228f1094ccc48763d76ede5c35632153bd7f697b846 \ + --hash=sha256:40f8f461eba87ef2e8ce0005ca2c12f1b4fdbbafd3a717b8570060d7cd35ee0c \ + --hash=sha256:512bb4b04e403a38860f7eb22abeeaefba3c4a9c08bc7beec8885494c5828034 \ + --hash=sha256:5a1137177b62eec949c0f1564eef73920f842af5ebfc260c20d9cd47e8ecd519 \ + --hash=sha256:6618e35268d116bffddd6dbec360a40c54b3164f8af0513d95d8698f36e2eacc \ + --hash=sha256:67dbf4dd7586b2d437f539d5dc930ebceaf74a4150720644d6ea7e5ffc1cb2ff \ + --hash=sha256:6f8d45f5fcdb52c60c902a951f549faad9979314e7e069f4fa3d14eb409b16a0 \ + --hash=sha256:73f9feba3da1ae66f0b521d7c2727db7f5025a83facdc73f4f39abe2b6d4f00d \ + --hash=sha256:7aaf5c4528e83e3176e7dbb01dcec34fb41c93279a8f8527cf33e5df88bfb910 \ + --hash=sha256:7c69c95d5242171d07396761f759a8a4d566e9a01bf99612f9b9e309e70a80fc \ + --hash=sha256:7ca3b453d767eb83b3ec58f0cfcdc934875a341cdfdb0dc55c1431c96608cf83 \ + --hash=sha256:7f2cb11fe789b72feeae7cdf6e27375c33ed6915f8ca5ea7ce81b5e234c75a9e \ + --hash=sha256:89af4ee347f361338bad5c27b023f9d19e7aed17aa75cb519f28e6cf1658a0ba \ + --hash=sha256:8ad7de37c9ff817bc5d26f89100f87b7f1a5cc25e5eaaa54f11dc66cca9652e4 \ + --hash=sha256:8ba1fe3fb415fd34cae5ca090fb82030b6e8423d6eb2c4c9c4fbf50b15c7664c \ + --hash=sha256:9213a114dd19cfca19715088f12f143e918c5e1b4e26f7acf1a823d7da9e1413 \ + --hash=sha256:9f08e5cc10143d3da2a2cf735d8b932ef4e4e1d74b0c74ce66c52eab02068be8 \ + --hash=sha256:a275b3a579dfd923d6330f6e5c2886dbdb5da4e004c5abecb107eb347d301412 \ + --hash=sha256:a2e6270923bf5ec0e9f720d689579a904f401c62193222d000d8cb8e880684e9 \ + --hash=sha256:a98989ff1ed20825728495ffb859cd700a120850074184d2e1ec08a0b1ab8ab3 \ + --hash=sha256:ae38c6d24d7aff003a241c8f1dd268eb1c6f7625d91e3435836ff5a5eed05ce5 \ + --hash=sha256:af7a417e19068b1abeb9addd3c045a2d6e40d15365af6aa3cbe2d47305b5bb11 \ + --hash=sha256:b09b6166876d2cba8f331a548932b09e11c9386db0525c9ca15c399b666746fc \ + --hash=sha256:b9fc609a3d4009ee31212f435f5a75720ef24280f6d23edfd53f77b562a79c5b \ + --hash=sha256:ba3d42cd83d7f89b9c1b2f76df971e8ab58815f8060da4dc67b9ae9dba1b34cc \ + --hash=sha256:baf5b3b901272837bee2311ecbd28fdbe960d288a070dc72bdfdf48cfcbb8090 \ + --hash=sha256:bb54fe45deb55e4caae4ea2c1dba93ee79fb5c377287b14056d4c30fb156920e \ + --hash=sha256:be0801ae3f9017c6437bcd23a4bf2b2aa88e465f7efeed4b079944d07e3df994 \ + --hash=sha256:c349431f5c8aa99b8744550d0bb4615f63e73450584202ac5db0e5d7da4d82ff \ + --hash=sha256:c80ade5cdb0ea447e7f43b32abc2f4a628dcdfa64dc8ee5ab4262987e5e0814f \ + --hash=sha256:c8d1837c3d2d8e56aacc13a91ec7540b3baadc1b254fbdf225a2d15b72b654c3 \ + --hash=sha256:c97561c22fc05d0f6ba370d9bd67f86c313c38f31a1793e0ee9acb78ee28e4b8 \ + --hash=sha256:cf1f6edc968619a4355481c29d5571726723bc12924e2b25bd3348919f9bc992 \ + --hash=sha256:cf7af6458cf6343a2a0632ae2fc5f04821b2ffefc7b8a27f4eacb726ef89c682 \ + --hash=sha256:d0ac29adf468a838884e1507d81e872096238c76fe7da7f3325507e4390b6867 \ + --hash=sha256:d7ad871d044a96f794170f2434e832c6b42804d0b53721377d03f865245cd273 \ + --hash=sha256:ddb790f2fdc22984fba643866b21d04733c5cf7c3ace2a1e99e0c1c1d2336aab \ + --hash=sha256:e3b686b49adeb7fc45791dfae96ffcffeba1038e8b7603f369d6661f59e479fc \ + --hash=sha256:e7b8cb38a93de87b980f882f0dcd19f2e3ad43216f34e06916315cb3a03e6964 \ + --hash=sha256:f4f8ff776ca38ce272d9c164a7f77db8a54a8cad6d9468124317adf8732be07d + # via poetry +filelock==3.10.0 \ + --hash=sha256:3199fd0d3faea8b911be52b663dfccceb84c95949dd13179aa21436d1a79c4ce \ + --hash=sha256:e90b34656470756edf8b19656785c5fea73afa1953f3e1b0d645cef11cab3182 + # via + # poetry + # virtualenv +html5lib==1.1 \ + --hash=sha256:0d78f8fde1c230e99fe37986a60526d7049ed4bf8a9fadbad5f00e22e58e041d \ + --hash=sha256:b2e5b40261e20f354d198eae92afc10d750afb487ed5e50f9c4eaf07c184146f + # via poetry +idna==3.3 \ + --hash=sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff \ + --hash=sha256:9d643ff0a55b762d5cdb124b8eaa99c66322e2157b69160bc32796e824360e6d + # via requests +importlib-metadata==6.1.0 \ + --hash=sha256:43ce9281e097583d758c2c708c4376371261a02c34682491a8e98352365aad20 \ + --hash=sha256:ff80f3b5394912eb1b108fcfd444dc78b7f1f3e16b16188054bd01cb9cb86f09 + # via keyring +installer==0.6.0 \ + --hash=sha256:ae7c62d1d6158b5c096419102ad0d01fdccebf857e784cee57f94165635fe038 \ + --hash=sha256:f3bd36cd261b440a88a1190b1becca0578fee90b4b62decc796932fdd5ae8839 + # via poetry +jaraco-classes==3.2.3 \ + --hash=sha256:2353de3288bc6b82120752201c6b1c1a14b058267fa424ed5ce5984e3b922158 \ + --hash=sha256:89559fa5c1d3c34eff6f631ad80bb21f378dbcbb35dd161fd2c6b93f5be2f98a + # via keyring +jeepney==0.8.0 \ + --hash=sha256:5efe48d255973902f6badc3ce55e2aa6c5c3b3bc642059ef3a91247bcfcc5806 \ + --hash=sha256:c0a454ad016ca575060802ee4d590dd912e35c122fa04e70306de3d076cce755 + # via + # keyring + # secretstorage +jsonschema==4.17.3 \ + --hash=sha256:0f864437ab8b6076ba6707453ef8f98a6a0d512a80e93f8abdb676f737ecb60d \ + --hash=sha256:a870ad254da1a8ca84b6a2905cac29d265f805acc57af304784962a2aa6508f6 + # via poetry +keyring==23.13.1 \ + --hash=sha256:771ed2a91909389ed6148631de678f82ddc73737d85a927f382a8a1b157898cd \ + --hash=sha256:ba2e15a9b35e21908d0aaf4e0a47acc52d6ae33444df0da2b49d41a46ef6d678 + # via poetry +lockfile==0.12.2 \ + --hash=sha256:6aed02de03cba24efabcd600b30540140634fc06cfa603822d508d5361e9f799 \ + --hash=sha256:6c3cb24f344923d30b2785d5ad75182c8ea7ac1b6171b08657258ec7429d50fa + # via + # cachecontrol + # poetry +more-itertools==9.1.0 \ + --hash=sha256:cabaa341ad0389ea83c17a94566a53ae4c9d07349861ecb14dc6d0345cf9ac5d \ + --hash=sha256:d2bc7f02446e86a68911e58ded76d6561eea00cddfb2a91e7019bbb586c799f3 + # via jaraco-classes +msgpack==1.0.3 \ + --hash=sha256:0d8c332f53ffff01953ad25131272506500b14750c1d0ce8614b17d098252fbc \ + --hash=sha256:1c58cdec1cb5fcea8c2f1771d7b5fec79307d056874f746690bd2bdd609ab147 \ + --hash=sha256:2c3ca57c96c8e69c1a0d2926a6acf2d9a522b41dc4253a8945c4c6cd4981a4e3 \ + --hash=sha256:2f30dd0dc4dfe6231ad253b6f9f7128ac3202ae49edd3f10d311adc358772dba \ + --hash=sha256:2f97c0f35b3b096a330bb4a1a9247d0bd7e1f3a2eba7ab69795501504b1c2c39 \ + --hash=sha256:36a64a10b16c2ab31dcd5f32d9787ed41fe68ab23dd66957ca2826c7f10d0b85 \ + --hash=sha256:3d875631ecab42f65f9dce6f55ce6d736696ced240f2634633188de2f5f21af9 \ + --hash=sha256:40fb89b4625d12d6027a19f4df18a4de5c64f6f3314325049f219683e07e678a \ + --hash=sha256:47d733a15ade190540c703de209ffbc42a3367600421b62ac0c09fde594da6ec \ + --hash=sha256:494471d65b25a8751d19c83f1a482fd411d7ca7a3b9e17d25980a74075ba0e88 \ + --hash=sha256:51fdc7fb93615286428ee7758cecc2f374d5ff363bdd884c7ea622a7a327a81e \ + --hash=sha256:6eef0cf8db3857b2b556213d97dd82de76e28a6524853a9beb3264983391dc1a \ + --hash=sha256:6f4c22717c74d44bcd7af353024ce71c6b55346dad5e2cc1ddc17ce8c4507c6b \ + --hash=sha256:73a80bd6eb6bcb338c1ec0da273f87420829c266379c8c82fa14c23fb586cfa1 \ + --hash=sha256:89908aea5f46ee1474cc37fbc146677f8529ac99201bc2faf4ef8edc023c2bf3 \ + --hash=sha256:8a3a5c4b16e9d0edb823fe54b59b5660cc8d4782d7bf2c214cb4b91a1940a8ef \ + --hash=sha256:96acc674bb9c9be63fa8b6dabc3248fdc575c4adc005c440ad02f87ca7edd079 \ + --hash=sha256:973ad69fd7e31159eae8f580f3f707b718b61141838321c6fa4d891c4a2cca52 \ + --hash=sha256:9b6f2d714c506e79cbead331de9aae6837c8dd36190d02da74cb409b36162e8a \ + --hash=sha256:9c0903bd93cbd34653dd63bbfcb99d7539c372795201f39d16fdfde4418de43a \ + --hash=sha256:9fce00156e79af37bb6db4e7587b30d11e7ac6a02cb5bac387f023808cd7d7f4 \ + --hash=sha256:a598d0685e4ae07a0672b59792d2cc767d09d7a7f39fd9bd37ff84e060b1a996 \ + --hash=sha256:b0a792c091bac433dfe0a70ac17fc2087d4595ab835b47b89defc8bbabcf5c73 \ + --hash=sha256:bb87f23ae7d14b7b3c21009c4b1705ec107cb21ee71975992f6aca571fb4a42a \ + --hash=sha256:bf1e6bfed4860d72106f4e0a1ab519546982b45689937b40257cfd820650b920 \ + --hash=sha256:c1ba333b4024c17c7591f0f372e2daa3c31db495a9b2af3cf664aef3c14354f7 \ + --hash=sha256:c2140cf7a3ec475ef0938edb6eb363fa704159e0bf71dde15d953bacc1cf9d7d \ + --hash=sha256:c7e03b06f2982aa98d4ddd082a210c3db200471da523f9ac197f2828e80e7770 \ + --hash=sha256:d02cea2252abc3756b2ac31f781f7a98e89ff9759b2e7450a1c7a0d13302ff50 \ + --hash=sha256:da24375ab4c50e5b7486c115a3198d207954fe10aaa5708f7b65105df09109b2 \ + --hash=sha256:e4c309a68cb5d6bbd0c50d5c71a25ae81f268c2dc675c6f4ea8ab2feec2ac4e2 \ + --hash=sha256:f01b26c2290cbd74316990ba84a14ac3d599af9cebefc543d241a66e785cf17d \ + --hash=sha256:f201d34dc89342fabb2a10ed7c9a9aaaed9b7af0f16a5923f1ae562b31258dea \ + --hash=sha256:f74da1e5fcf20ade12c6bf1baa17a2dc3604958922de8dc83cbe3eff22e8b611 + # via cachecontrol +packaging==20.9 \ + --hash=sha256:5b327ac1320dc863dca72f4514ecc086f31186744b84a230374cc1fd776feae5 \ + --hash=sha256:67714da7f7bc052e064859c05c595155bd1ee9f69f76557e21f051443c20947a + # via + # build + # poetry +pexpect==4.8.0 \ + --hash=sha256:0b48a55dcb3c05f3329815901ea4fc1537514d6ba867a152b581d69ae3710937 \ + --hash=sha256:fc65a43959d153d0114afe13997d439c22823a27cefceb5ff35c2178c6784c0c + # via poetry +pkginfo==1.9.6 \ + --hash=sha256:4b7a555a6d5a22169fcc9cf7bfd78d296b0361adad412a346c1226849af5e546 \ + --hash=sha256:8fd5896e8718a4372f0ea9cc9d96f6417c9b986e23a4d116dda26b62cc29d046 + # via poetry +platformdirs==2.6.2 \ + --hash=sha256:83c8f6d04389165de7c9b6f0c682439697887bca0aa2f1c87ef1826be3584490 \ + --hash=sha256:e1fea1fe471b9ff8332e229df3cb7de4f53eeea4998d3b6bfff542115e998bd2 + # via poetry +poetry==1.4.0 \ + --hash=sha256:151ad741e163a329c8b13ea602dde979b7616fc350cfcff74b604e93263934a8 \ + --hash=sha256:f88a7a812a5d8c1f5a378e0924f898926b2ac10c3b5c03f7282f2182f90d8507 + # via + # -r python/mozbuild/mozbuild/test/vendor_requirements.in + # poetry-plugin-export +poetry-core==1.5.1 \ + --hash=sha256:41887261358863f25831fa0ad1fe7e451fc32d1c81fcf7710ba5174cc0047c6d \ + --hash=sha256:b1900dea81eb18feb7323d404e5f10430205541a4a683a912893f9d2b5807797 + # via + # -r python/mozbuild/mozbuild/test/vendor_requirements.in + # poetry + # poetry-plugin-export +poetry-plugin-export==1.3.0 \ + --hash=sha256:61ae5ec1db233aba947a48e1ce54c6ff66afd0e1c87195d6bce64c73a5ae658c \ + --hash=sha256:6e5919bf84afcb08cdd419a03f909f490d8671f00633a3c6df8ba09b0820dc2f + # via poetry +ptyprocess==0.7.0 \ + --hash=sha256:4b41f3967fce3af57cc7e94b888626c18bf37a083e3651ca8feeb66d492fef35 \ + --hash=sha256:5c5d0a3b48ceee0b48485e0c26037c0acd7d29765ca3fbb5cb3831d347423220 + # via pexpect +pycparser==2.21 \ + --hash=sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9 \ + --hash=sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206 + # via cffi +pyparsing==3.0.8 \ + --hash=sha256:7bf433498c016c4314268d95df76c81b842a4cb2b276fa3312cfb1e1d85f6954 \ + --hash=sha256:ef7b523f6356f763771559412c0d7134753f037822dad1b16945b7b846f7ad06 + # via packaging +pyproject-hooks==1.0.0 \ + --hash=sha256:283c11acd6b928d2f6a7c73fa0d01cb2bdc5f07c57a2eeb6e83d5e56b97976f8 \ + --hash=sha256:f271b298b97f5955d53fb12b72c1fb1948c22c1a6b70b315c54cedaca0264ef5 + # via + # build + # poetry +pyrsistent==0.19.3 \ + --hash=sha256:016ad1afadf318eb7911baa24b049909f7f3bb2c5b1ed7b6a8f21db21ea3faa8 \ + --hash=sha256:1a2994773706bbb4995c31a97bc94f1418314923bd1048c6d964837040376440 \ + --hash=sha256:20460ac0ea439a3e79caa1dbd560344b64ed75e85d8703943e0b66c2a6150e4a \ + --hash=sha256:3311cb4237a341aa52ab8448c27e3a9931e2ee09561ad150ba94e4cfd3fc888c \ + --hash=sha256:3a8cb235fa6d3fd7aae6a4f1429bbb1fec1577d978098da1252f0489937786f3 \ + --hash=sha256:3ab2204234c0ecd8b9368dbd6a53e83c3d4f3cab10ecaf6d0e772f456c442393 \ + --hash=sha256:42ac0b2f44607eb92ae88609eda931a4f0dfa03038c44c772e07f43e738bcac9 \ + --hash=sha256:49c32f216c17148695ca0e02a5c521e28a4ee6c5089f97e34fe24163113722da \ + --hash=sha256:4b774f9288dda8d425adb6544e5903f1fb6c273ab3128a355c6b972b7df39dcf \ + --hash=sha256:4c18264cb84b5e68e7085a43723f9e4c1fd1d935ab240ce02c0324a8e01ccb64 \ + --hash=sha256:5a474fb80f5e0d6c9394d8db0fc19e90fa540b82ee52dba7d246a7791712f74a \ + --hash=sha256:64220c429e42a7150f4bfd280f6f4bb2850f95956bde93c6fda1b70507af6ef3 \ + --hash=sha256:878433581fc23e906d947a6814336eee031a00e6defba224234169ae3d3d6a98 \ + --hash=sha256:99abb85579e2165bd8522f0c0138864da97847875ecbd45f3e7e2af569bfc6f2 \ + --hash=sha256:a2471f3f8693101975b1ff85ffd19bb7ca7dd7c38f8a81701f67d6b4f97b87d8 \ + --hash=sha256:aeda827381f5e5d65cced3024126529ddc4289d944f75e090572c77ceb19adbf \ + --hash=sha256:b735e538f74ec31378f5a1e3886a26d2ca6351106b4dfde376a26fc32a044edc \ + --hash=sha256:c147257a92374fde8498491f53ffa8f4822cd70c0d85037e09028e478cababb7 \ + --hash=sha256:c4db1bd596fefd66b296a3d5d943c94f4fac5bcd13e99bffe2ba6a759d959a28 \ + --hash=sha256:c74bed51f9b41c48366a286395c67f4e894374306b197e62810e0fdaf2364da2 \ + --hash=sha256:c9bb60a40a0ab9aba40a59f68214eed5a29c6274c83b2cc206a359c4a89fa41b \ + --hash=sha256:cc5d149f31706762c1f8bda2e8c4f8fead6e80312e3692619a75301d3dbb819a \ + --hash=sha256:ccf0d6bd208f8111179f0c26fdf84ed7c3891982f2edaeae7422575f47e66b64 \ + --hash=sha256:e42296a09e83028b3476f7073fcb69ffebac0e66dbbfd1bd847d61f74db30f19 \ + --hash=sha256:e8f2b814a3dc6225964fa03d8582c6e0b6650d68a232df41e3cc1b66a5d2f8d1 \ + --hash=sha256:f0774bf48631f3a20471dd7c5989657b639fd2d285b861237ea9e82c36a415a9 \ + --hash=sha256:f0e7c4b2f77593871e918be000b96c8107da48444d57005b6a6bc61fb4331b2c + # via jsonschema +rapidfuzz==2.13.7 \ + --hash=sha256:020858dd89b60ce38811cd6e37875c4c3c8d7fcd8bc20a0ad2ed1f464b34dc4e \ + --hash=sha256:042644133244bfa7b20de635d500eb9f46af7097f3d90b1724f94866f17cb55e \ + --hash=sha256:08590905a95ccfa43f4df353dcc5d28c15d70664299c64abcad8721d89adce4f \ + --hash=sha256:114810491efb25464016fd554fdf1e20d390309cecef62587494fc474d4b926f \ + --hash=sha256:1333fb3d603d6b1040e365dca4892ba72c7e896df77a54eae27dc07db90906e3 \ + --hash=sha256:16080c05a63d6042643ae9b6cfec1aefd3e61cef53d0abe0df3069b9d4b72077 \ + --hash=sha256:16ffad751f43ab61001187b3fb4a9447ec2d1aedeff7c5bac86d3b95f9980cc3 \ + --hash=sha256:1f50d1227e6e2a0e3ae1fb1c9a2e1c59577d3051af72c7cab2bcc430cb5e18da \ + --hash=sha256:1fbad8fb28d98980f5bff33c7842efef0315d42f0cd59082108482a7e6b61410 \ + --hash=sha256:23524635840500ce6f4d25005c9529a97621689c85d2f727c52eed1782839a6a \ + --hash=sha256:24d3fea10680d085fd0a4d76e581bfb2b1074e66e78fd5964d4559e1fcd2a2d4 \ + --hash=sha256:24eb6b843492bdc63c79ee4b2f104059b7a2201fef17f25177f585d3be03405a \ + --hash=sha256:25b4cedf2aa19fb7212894ce5f5219010cce611b60350e9a0a4d492122e7b351 \ + --hash=sha256:27be9c63215d302ede7d654142a2e21f0d34ea6acba512a4ae4cfd52bbaa5b59 \ + --hash=sha256:2c836f0f2d33d4614c3fbaf9a1eb5407c0fe23f8876f47fd15b90f78daa64c34 \ + --hash=sha256:3a9bd02e1679c0fd2ecf69b72d0652dbe2a9844eaf04a36ddf4adfbd70010e95 \ + --hash=sha256:3d8b081988d0a49c486e4e845a547565fee7c6e7ad8be57ff29c3d7c14c6894c \ + --hash=sha256:3dcffe1f3cbda0dc32133a2ae2255526561ca594f15f9644384549037b355245 \ + --hash=sha256:3f11a7eff7bc6301cd6a5d43f309e22a815af07e1f08eeb2182892fca04c86cb \ + --hash=sha256:42085d4b154a8232767de8296ac39c8af5bccee6b823b0507de35f51c9cbc2d7 \ + --hash=sha256:424f82c35dbe4f83bdc3b490d7d696a1dc6423b3d911460f5493b7ffae999fd2 \ + --hash=sha256:43fb8cb030f888c3f076d40d428ed5eb4331f5dd6cf1796cfa39c67bf0f0fc1e \ + --hash=sha256:460853983ab88f873173e27cc601c5276d469388e6ad6e08c4fd57b2a86f1064 \ + --hash=sha256:467c1505362823a5af12b10234cb1c4771ccf124c00e3fc9a43696512bd52293 \ + --hash=sha256:46b9b8aa09998bc48dd800854e8d9b74bc534d7922c1d6e1bbf783e7fa6ac29c \ + --hash=sha256:53dcae85956853b787c27c1cb06f18bb450e22cf57a4ad3444cf03b8ff31724a \ + --hash=sha256:585206112c294e335d84de5d5f179c0f932837752d7420e3de21db7fdc476278 \ + --hash=sha256:5ada0a14c67452358c1ee52ad14b80517a87b944897aaec3e875279371a9cb96 \ + --hash=sha256:5e2b3d020219baa75f82a4e24b7c8adcb598c62f0e54e763c39361a9e5bad510 \ + --hash=sha256:6120f2995f5154057454c5de99d86b4ef3b38397899b5da1265467e8980b2f60 \ + --hash=sha256:68a89bb06d5a331511961f4d3fa7606f8e21237467ba9997cae6f67a1c2c2b9e \ + --hash=sha256:7496e8779905b02abc0ab4ba2a848e802ab99a6e20756ffc967a0de4900bd3da \ + --hash=sha256:759a3361711586a29bc753d3d1bdb862983bd9b9f37fbd7f6216c24f7c972554 \ + --hash=sha256:75c45dcd595f8178412367e302fd022860ea025dc4a78b197b35428081ed33d5 \ + --hash=sha256:7d005e058d86f2a968a8d28ca6f2052fab1f124a39035aa0523261d6baf21e1f \ + --hash=sha256:7f7930adf84301797c3f09c94b9c5a9ed90a9e8b8ed19b41d2384937e0f9f5bd \ + --hash=sha256:8109e0324d21993d5b2d111742bf5958f3516bf8c59f297c5d1cc25a2342eb66 \ + --hash=sha256:81642a24798851b118f82884205fc1bd9ff70b655c04018c467824b6ecc1fabc \ + --hash=sha256:8450d15f7765482e86ef9be2ad1a05683cd826f59ad236ef7b9fb606464a56aa \ + --hash=sha256:875d51b3497439a72e2d76183e1cb5468f3f979ab2ddfc1d1f7dde3b1ecfb42f \ + --hash=sha256:8b477b43ced896301665183a5e0faec0f5aea2373005648da8bdcb3c4b73f280 \ + --hash=sha256:8d3e252d4127c79b4d7c2ae47271636cbaca905c8bb46d80c7930ab906cf4b5c \ + --hash=sha256:916bc2e6cf492c77ad6deb7bcd088f0ce9c607aaeabc543edeb703e1fbc43e31 \ + --hash=sha256:988f8f6abfba7ee79449f8b50687c174733b079521c3cc121d65ad2d38831846 \ + --hash=sha256:99a84ab9ac9a823e7e93b4414f86344052a5f3e23b23aa365cda01393ad895bd \ + --hash=sha256:9be02162af0376d64b840f2fc8ee3366794fc149f1e06d095a6a1d42447d97c5 \ + --hash=sha256:a5585189b3d90d81ccd62d4f18530d5ac8972021f0aaaa1ffc6af387ff1dce75 \ + --hash=sha256:ae33a72336059213996fe4baca4e0e4860913905c2efb7c991eab33b95a98a0a \ + --hash=sha256:af4f7c3c904ca709493eb66ca9080b44190c38e9ecb3b48b96d38825d5672559 \ + --hash=sha256:b20141fa6cee041917801de0bab503447196d372d4c7ee9a03721b0a8edf5337 \ + --hash=sha256:b3210869161a864f3831635bb13d24f4708c0aa7208ef5baac1ac4d46e9b4208 \ + --hash=sha256:b34e8c0e492949ecdd5da46a1cfc856a342e2f0389b379b1a45a3cdcd3176a6e \ + --hash=sha256:b52ac2626945cd21a2487aeefed794c14ee31514c8ae69b7599170418211e6f6 \ + --hash=sha256:b5dd713a1734574c2850c566ac4286594bacbc2d60b9170b795bee4b68656625 \ + --hash=sha256:b5f705652360d520c2de52bee11100c92f59b3e3daca308ebb150cbc58aecdad \ + --hash=sha256:b6389c50d8d214c9cd11a77f6d501529cb23279a9c9cafe519a3a4b503b5f72a \ + --hash=sha256:b6bad92de071cbffa2acd4239c1779f66851b60ffbbda0e4f4e8a2e9b17e7eef \ + --hash=sha256:b75dd0928ce8e216f88660ab3d5c5ffe990f4dd682fd1709dba29d5dafdde6de \ + --hash=sha256:c2523f8180ebd9796c18d809e9a19075a1060b1a170fde3799e83db940c1b6d5 \ + --hash=sha256:c31022d9970177f6affc6d5dd757ed22e44a10890212032fabab903fdee3bfe7 \ + --hash=sha256:c36fd260084bb636b9400bb92016c6bd81fd80e59ed47f2466f85eda1fc9f782 \ + --hash=sha256:c3741cb0bf9794783028e8b0cf23dab917fa5e37a6093b94c4c2f805f8e36b9f \ + --hash=sha256:c3fbe449d869ea4d0909fc9d862007fb39a584fb0b73349a6aab336f0d90eaed \ + --hash=sha256:c66546e30addb04a16cd864f10f5821272a1bfe6462ee5605613b4f1cb6f7b48 \ + --hash=sha256:c71d9d512b76f05fa00282227c2ae884abb60e09f08b5ca3132b7e7431ac7f0d \ + --hash=sha256:c8601a66fbfc0052bb7860d2eacd303fcde3c14e87fdde409eceff516d659e77 \ + --hash=sha256:c88adbcb933f6b8612f6c593384bf824e562bb35fc8a0f55fac690ab5b3486e5 \ + --hash=sha256:ca00fafd2756bc9649bf80f1cf72c647dce38635f0695d7ce804bc0f759aa756 \ + --hash=sha256:ca8a23097c1f50e0fdb4de9e427537ca122a18df2eead06ed39c3a0bef6d9d3a \ + --hash=sha256:cda1e2f66bb4ba7261a0f4c2d052d5d909798fca557cbff68f8a79a87d66a18f \ + --hash=sha256:cdfc04f7647c29fb48da7a04082c34cdb16f878d3c6d098d62d5715c0ad3000c \ + --hash=sha256:cf62dacb3f9234f3fddd74e178e6d25c68f2067fde765f1d95f87b1381248f58 \ + --hash=sha256:d00df2e4a81ffa56a6b1ec4d2bc29afdcb7f565e0b8cd3092fece2290c4c7a79 \ + --hash=sha256:d248a109699ce9992304e79c1f8735c82cc4c1386cd8e27027329c0549f248a2 \ + --hash=sha256:d63def9bbc6b35aef4d76dc740301a4185867e8870cbb8719ec9de672212fca8 \ + --hash=sha256:d82f20c0060ffdaadaf642b88ab0aa52365b56dffae812e188e5bdb998043588 \ + --hash=sha256:dbcf5371ea704759fcce772c66a07647751d1f5dbdec7818331c9b31ae996c77 \ + --hash=sha256:e8914dad106dacb0775718e54bf15e528055c4e92fb2677842996f2d52da5069 \ + --hash=sha256:ebe303cd9839af69dd1f7942acaa80b1ba90bacef2e7ded9347fbed4f1654672 \ + --hash=sha256:ec55a81ac2b0f41b8d6fb29aad16e55417036c7563bad5568686931aa4ff08f7 \ + --hash=sha256:effe182767d102cb65dfbbf74192237dbd22d4191928d59415aa7d7c861d8c88 \ + --hash=sha256:f42b82f268689f429def9ecfb86fa65ceea0eaf3fed408b570fe113311bf5ce7 \ + --hash=sha256:f6fe570e20e293eb50491ae14ddeef71a6a7e5f59d7e791393ffa99b13f1f8c2 \ + --hash=sha256:f799d1d6c33d81e983d3682571cc7d993ae7ff772c19b3aabb767039c33f6d1e \ + --hash=sha256:f891b98f8bc6c9d521785816085e9657212621e93f223917fb8e32f318b2957e \ + --hash=sha256:fa263135b892686e11d5b84f6a1892523123a00b7e5882eff4fbdabb38667347 \ + --hash=sha256:fa4c598ed77f74ec973247ca776341200b0f93ec3883e34c222907ce72cb92a4 \ + --hash=sha256:fe56659ccadbee97908132135de4b875543353351e0c92e736b7c57aee298b5a \ + --hash=sha256:fe59a0c21a032024edb0c8e43f5dee5623fef0b65a1e3c1281836d9ce199af3b + # via cleo +requests==2.27.1 \ + --hash=sha256:68d7c56fd5a8999887728ef304a6d12edc7be74f1cfa47714fc8b414525c9a61 \ + --hash=sha256:f22fa1e554c9ddfd16e6e41ac79759e17be9e492b3587efa038054674760e72d + # via + # cachecontrol + # poetry + # requests-toolbelt +requests-toolbelt==0.9.1 \ + --hash=sha256:380606e1d10dc85c3bd47bf5a6095f815ec007be7a8b69c878507068df059e6f \ + --hash=sha256:968089d4584ad4ad7c171454f0a5c6dac23971e9472521ea3b6d49d610aa6fc0 + # via poetry +secretstorage==3.3.3 \ + --hash=sha256:2403533ef369eca6d2ba81718576c5e0f564d5cca1b58f73a8b23e7d4eeebd77 \ + --hash=sha256:f356e6628222568e3af06f2eba8df495efa13b3b63081dafd4f7d9a7b7bc9f99 + # via keyring +shellingham==1.5.0.post1 \ + --hash=sha256:368bf8c00754fd4f55afb7bbb86e272df77e4dc76ac29dbcbb81a59e9fc15744 \ + --hash=sha256:823bc5fb5c34d60f285b624e7264f4dda254bc803a3774a147bf99c0e3004a28 + # via poetry +six==1.16.0 \ + --hash=sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926 \ + --hash=sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254 + # via + # html5lib + # virtualenv +tomli==2.0.1 \ + --hash=sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc \ + --hash=sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f + # via + # build + # poetry + # pyproject-hooks +tomlkit==0.12.3 \ + --hash=sha256:75baf5012d06501f07bee5bf8e801b9f343e7aac5a92581f20f80ce632e6b5a4 \ + --hash=sha256:b0a645a9156dc7cb5d3a1f0d4bab66db287fcb8e0430bdd4664a095ea16414ba + # via + # -r python/mozbuild/mozbuild/test/vendor_requirements.in + # poetry +trove-classifiers==2023.3.9 \ + --hash=sha256:06fd10c95d285e7ddebd59e6a4ba299f03d7417d38d369248a4a40c9754a68fa \ + --hash=sha256:ee42f2f8c1d4bcfe35f746e472f07633570d485fab45407effc0379270a3bb03 + # via poetry +urllib3==1.26.9 \ + --hash=sha256:44ece4d53fb1706f667c9bd1c648f5469a2ec925fcf3a776667042d645472c14 \ + --hash=sha256:aabaf16477806a5e1dd19aa41f8c2b7950dd3c746362d7e3223dbe6de6ac448e + # via + # dulwich + # poetry + # requests +virtualenv==20.4.4 \ + --hash=sha256:09c61377ef072f43568207dc8e46ddeac6bcdcaf288d49011bda0e7f4d38c4a2 \ + --hash=sha256:a935126db63128861987a7d5d30e23e8ec045a73840eeccb467c148514e29535 + # via poetry +webencodings==0.5.1 \ + --hash=sha256:a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78 \ + --hash=sha256:b36a1c245f2d304965eb4e0a82848379241dc04b865afcc4aab16748587e1923 + # via html5lib +zipp==3.6.0 \ + --hash=sha256:71c644c5369f4a6e07636f0aa966270449561fcea2e3d6747b8d23efaa9d7832 \ + --hash=sha256:9fe5ea21568a0a70e50f273397638d39b03353731e6cbbb3fd8502a33fec40bc + # via importlib-metadata diff --git a/python/mozbuild/mozbuild/testing.py b/python/mozbuild/mozbuild/testing.py new file mode 100644 index 0000000000..c0b52cd2a0 --- /dev/null +++ b/python/mozbuild/mozbuild/testing.py @@ -0,0 +1,261 @@ +# 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/. + +import os +import sys + +import manifestparser +import mozpack.path as mozpath +from mozpack.copier import FileCopier +from mozpack.manifests import InstallManifest + +# These definitions provide a single source of truth for modules attempting +# to get a view of all tests for a build. Used by the emitter to figure out +# how to read/install manifests and by test dependency annotations in Files() +# entries to enumerate test flavors. + +# While there are multiple test manifests, the behavior is very similar +# across them. We enforce this by having common handling of all +# manifests and outputting a single class type with the differences +# described inside the instance. +# +# Keys are variable prefixes and values are tuples describing how these +# manifests should be handled: +# +# (flavor, install_root, install_subdir, package_tests) +# +# flavor identifies the flavor of this test. +# install_root is the path prefix to install the files starting from the root +# directory and not as specified by the manifest location. (bug 972168) +# install_subdir is the path of where to install the files in +# the tests directory. +# package_tests indicates whether to package test files into the test +# package; suites that compile the test files should not install +# them into the test package. +# +TEST_MANIFESTS = dict( + A11Y=("a11y", "testing/mochitest", "a11y", True), + BROWSER_CHROME=("browser-chrome", "testing/mochitest", "browser", True), + ANDROID_INSTRUMENTATION=("instrumentation", "instrumentation", ".", False), + FIREFOX_UI_FUNCTIONAL=("firefox-ui-functional", "firefox-ui", ".", False), + FIREFOX_UI_UPDATE=("firefox-ui-update", "firefox-ui", ".", False), + PYTHON_UNITTEST=("python", "python", ".", False), + CRAMTEST=("cram", "cram", ".", False), + TELEMETRY_TESTS_CLIENT=( + "telemetry-tests-client", + "toolkit/components/telemetry/tests/marionette/", + ".", + False, + ), + MARIONETTE=("marionette", "marionette", ".", False), + MOCHITEST=("mochitest", "testing/mochitest", "tests", True), + MOCHITEST_CHROME=("chrome", "testing/mochitest", "chrome", True), + WEBRTC_SIGNALLING_TEST=("steeplechase", "steeplechase", ".", True), + XPCSHELL_TESTS=("xpcshell", "xpcshell", ".", True), + PERFTESTS=("perftest", "testing/perf", "perf", True), +) + +# reftests, wpt, and puppeteer all have their own manifest formats +# and are processed separately +REFTEST_FLAVORS = ("crashtest", "reftest") +PUPPETEER_FLAVORS = ("puppeteer",) +WEB_PLATFORM_TESTS_FLAVORS = ("web-platform-tests",) + + +def all_test_flavors(): + return ( + [v[0] for v in TEST_MANIFESTS.values()] + + list(REFTEST_FLAVORS) + + list(PUPPETEER_FLAVORS) + + list(WEB_PLATFORM_TESTS_FLAVORS) + ) + + +class TestInstallInfo(object): + def __init__(self): + self.seen = set() + self.pattern_installs = [] + self.installs = [] + self.external_installs = set() + self.deferred_installs = set() + + def __ior__(self, other): + self.pattern_installs.extend(other.pattern_installs) + self.installs.extend(other.installs) + self.external_installs |= other.external_installs + self.deferred_installs |= other.deferred_installs + return self + + +class SupportFilesConverter(object): + """Processes a "support-files" entry from a test object, either from + a parsed object from a test manifests or its representation in + moz.build and returns the installs to perform for this test object. + + Processing the same support files multiple times will not have any further + effect, and the structure of the parsed objects from manifests will have a + lot of repeated entries, so this class takes care of memoizing. + """ + + def __init__(self): + self._fields = ( + ("head", set()), + ("support-files", set()), + ("generated-files", set()), + ) + + def convert_support_files(self, test, install_root, manifest_dir, out_dir): + # Arguments: + # test - The test object to process. + # install_root - The directory under $objdir/_tests that will contain + # the tests for this harness (examples are "testing/mochitest", + # "xpcshell"). + # manifest_dir - Absoulute path to the (srcdir) directory containing the + # manifest that included this test + # out_dir - The path relative to $objdir/_tests used as the destination for the + # test, based on the relative path to the manifest in the srcdir and + # the install_root. + info = TestInstallInfo() + for field, seen in self._fields: + value = test.get(field, "") + for pattern in value.split(): + # We track uniqueness locally (per test) where duplicates are forbidden, + # and globally, where they are permitted. If a support file appears multiple + # times for a single test, there are unnecessary entries in the manifest. But + # many entries will be shared across tests that share defaults. + key = field, pattern, out_dir + if key in info.seen: + raise ValueError( + "%s appears multiple times in a test manifest under a %s field," + " please omit the duplicate entry." % (pattern, field) + ) + info.seen.add(key) + if key in seen: + continue + seen.add(key) + + if field == "generated-files": + info.external_installs.add( + mozpath.normpath(mozpath.join(out_dir, pattern)) + ) + # '!' indicates our syntax for inter-directory support file + # dependencies. These receive special handling in the backend. + elif pattern[0] == "!": + info.deferred_installs.add(pattern) + # We only support globbing on support-files because + # the harness doesn't support * for head. + elif "*" in pattern and field == "support-files": + info.pattern_installs.append((manifest_dir, pattern, out_dir)) + # "absolute" paths identify files that are to be + # placed in the install_root directory (no globs) + elif pattern[0] == "/": + full = mozpath.normpath( + mozpath.join(manifest_dir, mozpath.basename(pattern)) + ) + info.installs.append( + (full, mozpath.join(install_root, pattern[1:])) + ) + else: + full = mozpath.normpath(mozpath.join(manifest_dir, pattern)) + dest_path = mozpath.join(out_dir, pattern) + + # If the path resolves to a different directory + # tree, we take special behavior depending on the + # entry type. + if not full.startswith(manifest_dir): + # If it's a support file, we install the file + # into the current destination directory. + # This implementation makes installing things + # with custom prefixes impossible. If this is + # needed, we can add support for that via a + # special syntax later. + if field == "support-files": + dest_path = mozpath.join(out_dir, os.path.basename(pattern)) + # If it's not a support file, we ignore it. + # This preserves old behavior so things like + # head files doesn't get installed multiple + # times. + else: + continue + info.installs.append((full, mozpath.normpath(dest_path))) + return info + + +def install_test_files(topsrcdir, topobjdir, tests_root): + """Installs the requested test files to the objdir. This is invoked by + test runners to avoid installing tens of thousands of test files when + only a few tests need to be run. + """ + + manifest = InstallManifest( + mozpath.join(topobjdir, "_build_manifests", "install", "_test_files") + ) + + harness_files_manifest = mozpath.join( + topobjdir, "_build_manifests", "install", tests_root + ) + + if os.path.isfile(harness_files_manifest): + # If the backend has generated an install manifest for test harness + # files they are treated as a monolith and installed each time we + # run tests. Fortunately there are not very many. + manifest |= InstallManifest(harness_files_manifest) + + copier = FileCopier() + manifest.populate_registry(copier) + copier.copy(mozpath.join(topobjdir, tests_root), remove_unaccounted=False) + + +# Convenience methods for test manifest reading. +def read_manifestparser_manifest(context, manifest_path): + path = manifest_path.full_path + return manifestparser.TestManifest( + manifests=[path], + strict=True, + rootdir=context.config.topsrcdir, + finder=context._finder, + handle_defaults=False, + ) + + +def read_reftest_manifest(context, manifest_path): + import reftest + + path = manifest_path.full_path + manifest = reftest.ReftestManifest(finder=context._finder) + manifest.load(path) + return manifest + + +def read_wpt_manifest(context, paths): + manifest_path, tests_root = paths + full_path = mozpath.normpath(mozpath.join(context.srcdir, manifest_path)) + old_path = sys.path[:] + try: + # Setup sys.path to include all the dependencies required to import + # the web-platform-tests manifest parser. web-platform-tests provides + # a the localpaths.py to do the path manipulation, which we load, + # providing the __file__ variable so it can resolve the relative + # paths correctly. + paths_file = os.path.join( + context.config.topsrcdir, + "testing", + "web-platform", + "tests", + "tools", + "localpaths.py", + ) + _globals = {"__file__": paths_file} + execfile(paths_file, _globals) + import manifest as wptmanifest + finally: + sys.path = old_path + f = context._finder.get(full_path) + try: + rv = wptmanifest.manifest.load(tests_root, f) + except wptmanifest.manifest.ManifestVersionMismatch: + # If we accidentially end up with a committed manifest that's the wrong + # version, then return an empty manifest here just to not break the build + rv = wptmanifest.manifest.Manifest() + return rv diff --git a/python/mozbuild/mozbuild/toolchains.py b/python/mozbuild/mozbuild/toolchains.py new file mode 100644 index 0000000000..c5418089bb --- /dev/null +++ b/python/mozbuild/mozbuild/toolchains.py @@ -0,0 +1,32 @@ +# 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/. + +import os + +import six + + +def toolchain_task_definitions(): + import gecko_taskgraph # noqa: triggers override of the `graph_config_schema` + from taskgraph.generator import load_tasks_for_kind + + # Don't import globally to allow this module being imported without + # the taskgraph module being available (e.g. standalone js) + params = {"level": os.environ.get("MOZ_SCM_LEVEL", "3")} + root_dir = os.path.join( + os.path.dirname(__file__), "..", "..", "..", "taskcluster", "ci" + ) + toolchains = load_tasks_for_kind(params, "toolchain", root_dir=root_dir) + aliased = {} + for t in toolchains.values(): + aliases = t.attributes.get("toolchain-alias") + if not aliases: + aliases = [] + if isinstance(aliases, six.text_type): + aliases = [aliases] + for alias in aliases: + aliased["toolchain-{}".format(alias)] = t + toolchains.update(aliased) + + return toolchains diff --git a/python/mozbuild/mozbuild/util.py b/python/mozbuild/mozbuild/util.py new file mode 100644 index 0000000000..576bbe3f43 --- /dev/null +++ b/python/mozbuild/mozbuild/util.py @@ -0,0 +1,1394 @@ +# 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/. + +# This file contains miscellaneous utility functions that don't belong anywhere +# in particular. + +import argparse +import collections +import collections.abc +import copy +import ctypes +import difflib +import errno +import functools +import hashlib +import io +import itertools +import os +import re +import stat +import sys +import time +from collections import OrderedDict +from io import BytesIO, StringIO +from pathlib import Path + +import six +from packaging.version import Version + +MOZBUILD_METRICS_PATH = os.path.abspath( + os.path.join(__file__, "..", "..", "metrics.yaml") +) + +if sys.platform == "win32": + _kernel32 = ctypes.windll.kernel32 + _FILE_ATTRIBUTE_NOT_CONTENT_INDEXED = 0x2000 + system_encoding = "mbcs" +else: + system_encoding = "utf-8" + + +def _open(path, mode): + if "b" in mode: + return io.open(path, mode) + return io.open(path, mode, encoding="utf-8", newline="\n") + + +def hash_file(path, hasher=None): + """Hashes a file specified by the path given and returns the hex digest.""" + + # If the default hashing function changes, this may invalidate + # lots of cached data. Don't change it lightly. + h = hasher or hashlib.sha1() + + with open(path, "rb") as fh: + while True: + data = fh.read(8192) + + if not len(data): + break + + h.update(data) + + return h.hexdigest() + + +class EmptyValue(six.text_type): + """A dummy type that behaves like an empty string and sequence. + + This type exists in order to support + :py:class:`mozbuild.frontend.reader.EmptyConfig`. It should likely not be + used elsewhere. + """ + + def __init__(self): + super(EmptyValue, self).__init__() + + +class ReadOnlyNamespace(object): + """A class for objects with immutable attributes set at initialization.""" + + def __init__(self, **kwargs): + for k, v in six.iteritems(kwargs): + super(ReadOnlyNamespace, self).__setattr__(k, v) + + def __delattr__(self, key): + raise Exception("Object does not support deletion.") + + def __setattr__(self, key, value): + raise Exception("Object does not support assignment.") + + def __ne__(self, other): + return not (self == other) + + def __eq__(self, other): + return self is other or ( + hasattr(other, "__dict__") and self.__dict__ == other.__dict__ + ) + + def __repr__(self): + return "<%s %r>" % (self.__class__.__name__, self.__dict__) + + +class ReadOnlyDict(dict): + """A read-only dictionary.""" + + def __init__(self, *args, **kwargs): + dict.__init__(self, *args, **kwargs) + + def __delitem__(self, key): + raise Exception("Object does not support deletion.") + + def __setitem__(self, key, value): + raise Exception("Object does not support assignment.") + + def update(self, *args, **kwargs): + raise Exception("Object does not support update.") + + def __copy__(self, *args, **kwargs): + return ReadOnlyDict(**dict.copy(self, *args, **kwargs)) + + def __deepcopy__(self, memo): + result = {} + for k, v in self.items(): + result[k] = copy.deepcopy(v, memo) + + return ReadOnlyDict(**result) + + def __reduce__(self, *args, **kwargs): + """ + Support for `pickle`. + """ + + return (self.__class__, (dict(self),)) + + +class undefined_default(object): + """Represents an undefined argument value that isn't None.""" + + +undefined = undefined_default() + + +class ReadOnlyDefaultDict(ReadOnlyDict): + """A read-only dictionary that supports default values on retrieval.""" + + def __init__(self, default_factory, *args, **kwargs): + ReadOnlyDict.__init__(self, *args, **kwargs) + self._default_factory = default_factory + + def __missing__(self, key): + value = self._default_factory() + dict.__setitem__(self, key, value) + return value + + +def ensureParentDir(path): + """Ensures the directory parent to the given file exists.""" + d = os.path.dirname(path) + if d and not os.path.exists(path): + try: + os.makedirs(d) + except OSError as error: + if error.errno != errno.EEXIST: + raise + + +def mkdir(path, not_indexed=False): + """Ensure a directory exists. + + If ``not_indexed`` is True, an attribute is set that disables content + indexing on the directory. + """ + try: + os.makedirs(path) + except OSError as e: + if e.errno != errno.EEXIST: + raise + + if not_indexed: + if sys.platform == "win32": + if isinstance(path, six.string_types): + fn = _kernel32.SetFileAttributesW + else: + fn = _kernel32.SetFileAttributesA + + fn(path, _FILE_ATTRIBUTE_NOT_CONTENT_INDEXED) + elif sys.platform == "darwin": + with open(os.path.join(path, ".metadata_never_index"), "a"): + pass + + +def simple_diff(filename, old_lines, new_lines): + """Returns the diff between old_lines and new_lines, in unified diff form, + as a list of lines. + + old_lines and new_lines are lists of non-newline terminated lines to + compare. + old_lines can be None, indicating a file creation. + new_lines can be None, indicating a file deletion. + """ + + old_name = "/dev/null" if old_lines is None else filename + new_name = "/dev/null" if new_lines is None else filename + + return difflib.unified_diff( + old_lines or [], new_lines or [], old_name, new_name, n=4, lineterm="" + ) + + +class FileAvoidWrite(BytesIO): + """File-like object that buffers output and only writes if content changed. + + We create an instance from an existing filename. New content is written to + it. When we close the file object, if the content in the in-memory buffer + differs from what is on disk, then we write out the new content. Otherwise, + the original file is untouched. + + Instances can optionally capture diffs of file changes. This feature is not + enabled by default because it a) doesn't make sense for binary files b) + could add unwanted overhead to calls. + + Additionally, there is dry run mode where the file is not actually written + out, but reports whether the file was existing and would have been updated + still occur, as well as diff capture if requested. + """ + + def __init__(self, filename, capture_diff=False, dry_run=False, readmode="r"): + BytesIO.__init__(self) + self.name = filename + assert type(capture_diff) == bool + assert type(dry_run) == bool + assert "r" in readmode + self._capture_diff = capture_diff + self._write_to_file = not dry_run + self.diff = None + self.mode = readmode + self._binary_mode = "b" in readmode + + def write(self, buf): + BytesIO.write(self, six.ensure_binary(buf)) + + def avoid_writing_to_file(self): + self._write_to_file = False + + def close(self): + """Stop accepting writes, compare file contents, and rewrite if needed. + + Returns a tuple of bools indicating what action was performed: + + (file existed, file updated) + + If ``capture_diff`` was specified at construction time and the + underlying file was changed, ``.diff`` will be populated with the diff + of the result. + """ + # Use binary data if the caller explicitly asked for it. + ensure = six.ensure_binary if self._binary_mode else six.ensure_text + buf = ensure(self.getvalue()) + + BytesIO.close(self) + existed = False + old_content = None + + try: + existing = _open(self.name, self.mode) + existed = True + except IOError: + pass + else: + try: + old_content = existing.read() + if old_content == buf: + return True, False + except IOError: + pass + finally: + existing.close() + + if self._write_to_file: + ensureParentDir(self.name) + # Maintain 'b' if specified. 'U' only applies to modes starting with + # 'r', so it is dropped. + writemode = "w" + if self._binary_mode: + writemode += "b" + buf = six.ensure_binary(buf) + else: + buf = six.ensure_text(buf) + with _open(self.name, writemode) as file: + file.write(buf) + + self._generate_diff(buf, old_content) + + return existed, True + + def _generate_diff(self, new_content, old_content): + """Generate a diff for the changed contents if `capture_diff` is True. + + If the changed contents could not be decoded as utf-8 then generate a + placeholder message instead of a diff. + + Args: + new_content: Str or bytes holding the new file contents. + old_content: Str or bytes holding the original file contents. Should be + None if no old content is being overwritten. + """ + if not self._capture_diff: + return + + try: + if old_content is None: + old_lines = None + else: + if self._binary_mode: + # difflib doesn't work with bytes. + old_content = old_content.decode("utf-8") + + old_lines = old_content.splitlines() + + if self._binary_mode: + # difflib doesn't work with bytes. + new_content = new_content.decode("utf-8") + + new_lines = new_content.splitlines() + + self.diff = simple_diff(self.name, old_lines, new_lines) + # FileAvoidWrite isn't unicode/bytes safe. So, files with non-ascii + # content or opened and written in different modes may involve + # implicit conversion and this will make Python unhappy. Since + # diffing isn't a critical feature, we just ignore the failure. + # This can go away once FileAvoidWrite uses io.BytesIO and + # io.StringIO. But that will require a lot of work. + except (UnicodeDecodeError, UnicodeEncodeError): + self.diff = ["Binary or non-ascii file changed: %s" % self.name] + + def __enter__(self): + return self + + def __exit__(self, type, value, traceback): + if not self.closed: + self.close() + + +def resolve_target_to_make(topobjdir, target): + r""" + Resolve `target` (a target, directory, or file) to a make target. + + `topobjdir` is the object directory; all make targets will be + rooted at or below the top-level Makefile in this directory. + + Returns a pair `(reldir, target)` where `reldir` is a directory + relative to `topobjdir` containing a Makefile and `target` is a + make target (possibly `None`). + + A directory resolves to the nearest directory at or above + containing a Makefile, and target `None`. + + A regular (non-Makefile) file resolves to the nearest directory at + or above the file containing a Makefile, and an appropriate + target. + + A Makefile resolves to the nearest parent strictly above the + Makefile containing a different Makefile, and an appropriate + target. + """ + + target = target.replace(os.sep, "/").lstrip("/") + abs_target = os.path.join(topobjdir, target) + + # For directories, run |make -C dir|. If the directory does not + # contain a Makefile, check parents until we find one. At worst, + # this will terminate at the root. + if os.path.isdir(abs_target): + current = abs_target + + while True: + make_path = os.path.join(current, "Makefile") + if os.path.exists(make_path): + return (current[len(topobjdir) + 1 :], None) + + current = os.path.dirname(current) + + # If it's not in a directory, this is probably a top-level make + # target. Treat it as such. + if "/" not in target: + return (None, target) + + # We have a relative path within the tree. We look for a Makefile + # as far into the path as possible. Then, we compute the make + # target as relative to that directory. + reldir = os.path.dirname(target) + target = os.path.basename(target) + + while True: + make_path = os.path.join(topobjdir, reldir, "Makefile") + + # We append to target every iteration, so the check below + # happens exactly once. + if target != "Makefile" and os.path.exists(make_path): + return (reldir, target) + + target = os.path.join(os.path.basename(reldir), target) + reldir = os.path.dirname(reldir) + + +class List(list): + """A list specialized for moz.build environments. + + We overload the assignment and append operations to require that the + appended thing is a list. This avoids bad surprises coming from appending + a string to a list, which would just add each letter of the string. + """ + + def __init__(self, iterable=None, **kwargs): + if iterable is None: + iterable = [] + if not isinstance(iterable, list): + raise ValueError("List can only be created from other list instances.") + + self._kwargs = kwargs + super(List, self).__init__(iterable) + + def extend(self, l): + if not isinstance(l, list): + raise ValueError("List can only be extended with other list instances.") + + return super(List, self).extend(l) + + def __setitem__(self, key, val): + if isinstance(key, slice): + if not isinstance(val, list): + raise ValueError( + "List can only be sliced with other list " "instances." + ) + if key.step: + raise ValueError("List cannot be sliced with a nonzero step " "value") + # Python 2 and Python 3 do this differently for some reason. + if six.PY2: + return super(List, self).__setslice__(key.start, key.stop, val) + else: + return super(List, self).__setitem__(key, val) + return super(List, self).__setitem__(key, val) + + def __setslice__(self, i, j, sequence): + return self.__setitem__(slice(i, j), sequence) + + def __add__(self, other): + # Allow None and EmptyValue is a special case because it makes undefined + # variable references in moz.build behave better. + other = [] if isinstance(other, (type(None), EmptyValue)) else other + if not isinstance(other, list): + raise ValueError("Only lists can be appended to lists.") + + new_list = self.__class__(self, **self._kwargs) + new_list.extend(other) + return new_list + + def __iadd__(self, other): + other = [] if isinstance(other, (type(None), EmptyValue)) else other + if not isinstance(other, list): + raise ValueError("Only lists can be appended to lists.") + + return super(List, self).__iadd__(other) + + +class UnsortedError(Exception): + def __init__(self, srtd, original): + assert len(srtd) == len(original) + + self.sorted = srtd + self.original = original + + for i, orig in enumerate(original): + s = srtd[i] + + if orig != s: + self.i = i + break + + def __str__(self): + s = StringIO() + + s.write("An attempt was made to add an unsorted sequence to a list. ") + s.write("The incoming list is unsorted starting at element %d. " % self.i) + s.write( + 'We expected "%s" but got "%s"' + % (self.sorted[self.i], self.original[self.i]) + ) + + return s.getvalue() + + +class StrictOrderingOnAppendList(List): + """A list specialized for moz.build environments. + + We overload the assignment and append operations to require that incoming + elements be ordered. This enforces cleaner style in moz.build files. + """ + + @staticmethod + def ensure_sorted(l): + if isinstance(l, StrictOrderingOnAppendList): + return + + def _first_element(e): + # If the list entry is a tuple, we sort based on the first element + # in the tuple. + return e[0] if isinstance(e, tuple) else e + + srtd = sorted(l, key=lambda x: _first_element(x).lower()) + + if srtd != l: + raise UnsortedError(srtd, l) + + def __init__(self, iterable=None, **kwargs): + if iterable is None: + iterable = [] + + StrictOrderingOnAppendList.ensure_sorted(iterable) + + super(StrictOrderingOnAppendList, self).__init__(iterable, **kwargs) + + def extend(self, l): + StrictOrderingOnAppendList.ensure_sorted(l) + + return super(StrictOrderingOnAppendList, self).extend(l) + + def __setitem__(self, key, val): + if isinstance(key, slice): + StrictOrderingOnAppendList.ensure_sorted(val) + return super(StrictOrderingOnAppendList, self).__setitem__(key, val) + + def __add__(self, other): + StrictOrderingOnAppendList.ensure_sorted(other) + + return super(StrictOrderingOnAppendList, self).__add__(other) + + def __iadd__(self, other): + StrictOrderingOnAppendList.ensure_sorted(other) + + return super(StrictOrderingOnAppendList, self).__iadd__(other) + + +class ImmutableStrictOrderingOnAppendList(StrictOrderingOnAppendList): + """Like StrictOrderingOnAppendList, but not allowing mutations of the value.""" + + def append(self, elt): + raise Exception("cannot use append on this type") + + def extend(self, iterable): + raise Exception("cannot use extend on this type") + + def __setslice__(self, i, j, iterable): + raise Exception("cannot assign to slices on this type") + + def __setitem__(self, i, elt): + raise Exception("cannot assign to indexes on this type") + + def __iadd__(self, other): + raise Exception("cannot use += on this type") + + +class StrictOrderingOnAppendListWithAction(StrictOrderingOnAppendList): + """An ordered list that accepts a callable to be applied to each item. + + A callable (action) passed to the constructor is run on each item of input. + The result of running the callable on each item will be stored in place of + the original input, but the original item must be used to enforce sortedness. + """ + + def __init__(self, iterable=(), action=None): + if not callable(action): + raise ValueError( + "A callable action is required to construct " + "a StrictOrderingOnAppendListWithAction" + ) + + self._action = action + if not isinstance(iterable, (tuple, list)): + raise ValueError( + "StrictOrderingOnAppendListWithAction can only be initialized " + "with another list" + ) + iterable = [self._action(i) for i in iterable] + super(StrictOrderingOnAppendListWithAction, self).__init__( + iterable, action=action + ) + + def extend(self, l): + if not isinstance(l, list): + raise ValueError( + "StrictOrderingOnAppendListWithAction can only be extended " + "with another list" + ) + l = [self._action(i) for i in l] + return super(StrictOrderingOnAppendListWithAction, self).extend(l) + + def __setitem__(self, key, val): + if isinstance(key, slice): + if not isinstance(val, list): + raise ValueError( + "StrictOrderingOnAppendListWithAction can only be sliced " + "with another list" + ) + val = [self._action(item) for item in val] + return super(StrictOrderingOnAppendListWithAction, self).__setitem__(key, val) + + def __add__(self, other): + if not isinstance(other, list): + raise ValueError( + "StrictOrderingOnAppendListWithAction can only be added with " + "another list" + ) + return super(StrictOrderingOnAppendListWithAction, self).__add__(other) + + def __iadd__(self, other): + if not isinstance(other, list): + raise ValueError( + "StrictOrderingOnAppendListWithAction can only be added with " + "another list" + ) + other = [self._action(i) for i in other] + return super(StrictOrderingOnAppendListWithAction, self).__iadd__(other) + + +class MozbuildDeletionError(Exception): + pass + + +def FlagsFactory(flags): + """Returns a class which holds optional flags for an item in a list. + + The flags are defined in the dict given as argument, where keys are + the flag names, and values the type used for the value of that flag. + + The resulting class is used by the various <TypeName>WithFlagsFactory + functions below. + """ + assert isinstance(flags, dict) + assert all(isinstance(v, type) for v in flags.values()) + + class Flags(object): + __slots__ = flags.keys() + _flags = flags + + def update(self, **kwargs): + for k, v in six.iteritems(kwargs): + setattr(self, k, v) + + def __getattr__(self, name): + if name not in self.__slots__: + raise AttributeError( + "'%s' object has no attribute '%s'" + % (self.__class__.__name__, name) + ) + try: + return object.__getattr__(self, name) + except AttributeError: + value = self._flags[name]() + self.__setattr__(name, value) + return value + + def __setattr__(self, name, value): + if name not in self.__slots__: + raise AttributeError( + "'%s' object has no attribute '%s'" + % (self.__class__.__name__, name) + ) + if not isinstance(value, self._flags[name]): + raise TypeError( + "'%s' attribute of class '%s' must be '%s'" + % (name, self.__class__.__name__, self._flags[name].__name__) + ) + return object.__setattr__(self, name, value) + + def __delattr__(self, name): + raise MozbuildDeletionError("Unable to delete attributes for this object") + + return Flags + + +class StrictOrderingOnAppendListWithFlags(StrictOrderingOnAppendList): + """A list with flags specialized for moz.build environments. + + Each subclass has a set of typed flags; this class lets us use `isinstance` + for natural testing. + """ + + +def StrictOrderingOnAppendListWithFlagsFactory(flags): + """Returns a StrictOrderingOnAppendList-like object, with optional + flags on each item. + + The flags are defined in the dict given as argument, where keys are + the flag names, and values the type used for the value of that flag. + + Example: + + .. code-block:: python + + FooList = StrictOrderingOnAppendListWithFlagsFactory({ + 'foo': bool, 'bar': unicode + }) + foo = FooList(['a', 'b', 'c']) + foo['a'].foo = True + foo['b'].bar = 'bar' + """ + + class StrictOrderingOnAppendListWithFlagsSpecialization( + StrictOrderingOnAppendListWithFlags + ): + def __init__(self, iterable=None): + if iterable is None: + iterable = [] + StrictOrderingOnAppendListWithFlags.__init__(self, iterable) + self._flags_type = FlagsFactory(flags) + self._flags = dict() + + def __getitem__(self, name): + if name not in self._flags: + if name not in self: + raise KeyError("'%s'" % name) + self._flags[name] = self._flags_type() + return self._flags[name] + + def __setitem__(self, name, value): + if not isinstance(name, slice): + raise TypeError( + "'%s' object does not support item assignment" + % self.__class__.__name__ + ) + result = super( + StrictOrderingOnAppendListWithFlagsSpecialization, self + ).__setitem__(name, value) + # We may have removed items. + for k in set(self._flags.keys()) - set(self): + del self._flags[k] + if isinstance(value, StrictOrderingOnAppendListWithFlags): + self._update_flags(value) + return result + + def _update_flags(self, other): + if self._flags_type._flags != other._flags_type._flags: + raise ValueError( + "Expected a list of strings with flags like %s, not like %s" + % (self._flags_type._flags, other._flags_type._flags) + ) + intersection = set(self._flags.keys()) & set(other._flags.keys()) + if intersection: + raise ValueError( + "Cannot update flags: both lists of strings with flags configure %s" + % intersection + ) + self._flags.update(other._flags) + + def extend(self, l): + result = super( + StrictOrderingOnAppendListWithFlagsSpecialization, self + ).extend(l) + if isinstance(l, StrictOrderingOnAppendListWithFlags): + self._update_flags(l) + return result + + def __add__(self, other): + result = super( + StrictOrderingOnAppendListWithFlagsSpecialization, self + ).__add__(other) + if isinstance(other, StrictOrderingOnAppendListWithFlags): + # Result has flags from other but not from self, since + # internally we duplicate self and then extend with other, and + # only extend knows about flags. Since we don't allow updating + # when the set of flag keys intersect, which we instance we pass + # to _update_flags here matters. This needs to be correct but + # is an implementation detail. + result._update_flags(self) + return result + + def __iadd__(self, other): + result = super( + StrictOrderingOnAppendListWithFlagsSpecialization, self + ).__iadd__(other) + if isinstance(other, StrictOrderingOnAppendListWithFlags): + self._update_flags(other) + return result + + return StrictOrderingOnAppendListWithFlagsSpecialization + + +class HierarchicalStringList(object): + """A hierarchy of lists of strings. + + Each instance of this object contains a list of strings, which can be set or + appended to. A sub-level of the hierarchy is also an instance of this class, + can be added by appending to an attribute instead. + + For example, the moz.build variable EXPORTS is an instance of this class. We + can do: + + EXPORTS += ['foo.h'] + EXPORTS.mozilla.dom += ['bar.h'] + + In this case, we have 3 instances (EXPORTS, EXPORTS.mozilla, and + EXPORTS.mozilla.dom), and the first and last each have one element in their + list. + """ + + __slots__ = ("_strings", "_children") + + def __init__(self): + # Please change ContextDerivedTypedHierarchicalStringList in context.py + # if you make changes here. + self._strings = StrictOrderingOnAppendList() + self._children = {} + + class StringListAdaptor(collections.abc.Sequence): + def __init__(self, hsl): + self._hsl = hsl + + def __getitem__(self, index): + return self._hsl._strings[index] + + def __len__(self): + return len(self._hsl._strings) + + def walk(self): + """Walk over all HierarchicalStringLists in the hierarchy. + + This is a generator of (path, sequence). + + The path is '' for the root level and '/'-delimited strings for + any descendants. The sequence is a read-only sequence of the + strings contained at that level. + """ + + if self._strings: + path_to_here = "" + yield path_to_here, self.StringListAdaptor(self) + + for k, l in sorted(self._children.items()): + for p, v in l.walk(): + path_to_there = "%s/%s" % (k, p) + yield path_to_there.strip("/"), v + + def __setattr__(self, name, value): + if name in self.__slots__: + return object.__setattr__(self, name, value) + + # __setattr__ can be called with a list when a simple assignment is + # used: + # + # EXPORTS.foo = ['file.h'] + # + # In this case, we need to overwrite foo's current list of strings. + # + # However, __setattr__ is also called with a HierarchicalStringList + # to try to actually set the attribute. We want to ignore this case, + # since we don't actually create an attribute called 'foo', but just add + # it to our list of children (using _get_exportvariable()). + self._set_exportvariable(name, value) + + def __getattr__(self, name): + if name.startswith("__"): + return object.__getattr__(self, name) + return self._get_exportvariable(name) + + def __delattr__(self, name): + raise MozbuildDeletionError("Unable to delete attributes for this object") + + def __iadd__(self, other): + if isinstance(other, HierarchicalStringList): + self._strings += other._strings + for c in other._children: + self[c] += other[c] + else: + self._check_list(other) + self._strings += other + return self + + def __getitem__(self, name): + return self._get_exportvariable(name) + + def __setitem__(self, name, value): + self._set_exportvariable(name, value) + + def _get_exportvariable(self, name): + # Please change ContextDerivedTypedHierarchicalStringList in context.py + # if you make changes here. + child = self._children.get(name) + if not child: + child = self._children[name] = HierarchicalStringList() + return child + + def _set_exportvariable(self, name, value): + if name in self._children: + if value is self._get_exportvariable(name): + return + raise KeyError("global_ns", "reassign", "<some variable>.%s" % name) + + exports = self._get_exportvariable(name) + exports._check_list(value) + exports._strings += value + + def _check_list(self, value): + if not isinstance(value, list): + raise ValueError("Expected a list of strings, not %s" % type(value)) + for v in value: + if not isinstance(v, six.string_types): + raise ValueError( + "Expected a list of strings, not an element of %s" % type(v) + ) + + +class LockFile(object): + """LockFile is used by the lock_file method to hold the lock. + + This object should not be used directly, but only through + the lock_file method below. + """ + + def __init__(self, lockfile): + self.lockfile = lockfile + + def __del__(self): + while True: + try: + os.remove(self.lockfile) + break + except OSError as e: + if e.errno == errno.EACCES: + # Another process probably has the file open, we'll retry. + # Just a short sleep since we want to drop the lock ASAP + # (but we need to let some other process close the file + # first). + time.sleep(0.1) + else: + # Re-raise unknown errors + raise + + +def lock_file(lockfile, max_wait=600): + """Create and hold a lockfile of the given name, with the given timeout. + + To release the lock, delete the returned object. + """ + + # FUTURE This function and object could be written as a context manager. + + while True: + try: + fd = os.open(lockfile, os.O_EXCL | os.O_RDWR | os.O_CREAT) + # We created the lockfile, so we're the owner + break + except OSError as e: + if e.errno == errno.EEXIST or ( + sys.platform == "win32" and e.errno == errno.EACCES + ): + pass + else: + # Should not occur + raise + + try: + # The lock file exists, try to stat it to get its age + # and read its contents to report the owner PID + f = open(lockfile, "r") + s = os.stat(lockfile) + except EnvironmentError as e: + if e.errno == errno.ENOENT or e.errno == errno.EACCES: + # We didn't create the lockfile, so it did exist, but it's + # gone now. Just try again + continue + + raise Exception( + "{0} exists but stat() failed: {1}".format(lockfile, e.strerror) + ) + + # We didn't create the lockfile and it's still there, check + # its age + now = int(time.time()) + if now - s[stat.ST_MTIME] > max_wait: + pid = f.readline().rstrip() + raise Exception( + "{0} has been locked for more than " + "{1} seconds (PID {2})".format(lockfile, max_wait, pid) + ) + + # It's not been locked too long, wait a while and retry + f.close() + time.sleep(1) + + # if we get here. we have the lockfile. Convert the os.open file + # descriptor into a Python file object and record our PID in it + f = os.fdopen(fd, "w") + f.write("{0}\n".format(os.getpid())) + f.close() + + return LockFile(lockfile) + + +class OrderedDefaultDict(OrderedDict): + """A combination of OrderedDict and defaultdict.""" + + def __init__(self, default_factory, *args, **kwargs): + OrderedDict.__init__(self, *args, **kwargs) + self._default_factory = default_factory + + def __missing__(self, key): + value = self[key] = self._default_factory() + return value + + +class KeyedDefaultDict(dict): + """Like a defaultdict, but the default_factory function takes the key as + argument""" + + def __init__(self, default_factory, *args, **kwargs): + dict.__init__(self, *args, **kwargs) + self._default_factory = default_factory + + def __missing__(self, key): + value = self._default_factory(key) + dict.__setitem__(self, key, value) + return value + + +class ReadOnlyKeyedDefaultDict(KeyedDefaultDict, ReadOnlyDict): + """Like KeyedDefaultDict, but read-only.""" + + +class memoize(dict): + """A decorator to memoize the results of function calls depending + on its arguments. + Both functions and instance methods are handled, although in the + instance method case, the results are cache in the instance itself. + """ + + def __init__(self, func): + self.func = func + functools.update_wrapper(self, func) + + def __call__(self, *args): + if args not in self: + self[args] = self.func(*args) + return self[args] + + def method_call(self, instance, *args): + name = "_%s" % self.func.__name__ + if not hasattr(instance, name): + setattr(instance, name, {}) + cache = getattr(instance, name) + if args not in cache: + cache[args] = self.func(instance, *args) + return cache[args] + + def __get__(self, instance, cls): + return functools.update_wrapper( + functools.partial(self.method_call, instance), self.func + ) + + +class memoized_property(object): + """A specialized version of the memoize decorator that works for + class instance properties. + """ + + def __init__(self, func): + self.func = func + + def __get__(self, instance, cls): + name = "_%s" % self.func.__name__ + if not hasattr(instance, name): + setattr(instance, name, self.func(instance)) + return getattr(instance, name) + + +def TypedNamedTuple(name, fields): + """Factory for named tuple types with strong typing. + + Arguments are an iterable of 2-tuples. The first member is the + the field name. The second member is a type the field will be validated + to be. + + Construction of instances varies from ``collections.namedtuple``. + + First, if a single tuple argument is given to the constructor, this is + treated as the equivalent of passing each tuple value as a separate + argument into __init__. e.g.:: + + t = (1, 2) + TypedTuple(t) == TypedTuple(1, 2) + + This behavior is meant for moz.build files, so vanilla tuples are + automatically cast to typed tuple instances. + + Second, fields in the tuple are validated to be instances of the specified + type. This is done via an ``isinstance()`` check. To allow multiple types, + pass a tuple as the allowed types field. + """ + cls = collections.namedtuple(name, (name for name, typ in fields)) + + class TypedTuple(cls): + __slots__ = () + + def __new__(klass, *args, **kwargs): + if len(args) == 1 and not kwargs and isinstance(args[0], tuple): + args = args[0] + + return super(TypedTuple, klass).__new__(klass, *args, **kwargs) + + def __init__(self, *args, **kwargs): + for i, (fname, ftype) in enumerate(self._fields): + value = self[i] + + if not isinstance(value, ftype): + raise TypeError( + "field in tuple not of proper type: %s; " + "got %s, expected %s" % (fname, type(value), ftype) + ) + + TypedTuple._fields = fields + + return TypedTuple + + +@memoize +def TypedList(type, base_class=List): + """A list with type coercion. + + The given ``type`` is what list elements are being coerced to. It may do + strict validation, throwing ValueError exceptions. + + A ``base_class`` type can be given for more specific uses than a List. For + example, a Typed StrictOrderingOnAppendList can be created with: + + TypedList(unicode, StrictOrderingOnAppendList) + """ + + class _TypedList(base_class): + @staticmethod + def normalize(e): + if not isinstance(e, type): + e = type(e) + return e + + def _ensure_type(self, l): + if isinstance(l, self.__class__): + return l + + return [self.normalize(e) for e in l] + + def __init__(self, iterable=None, **kwargs): + if iterable is None: + iterable = [] + iterable = self._ensure_type(iterable) + + super(_TypedList, self).__init__(iterable, **kwargs) + + def extend(self, l): + l = self._ensure_type(l) + + return super(_TypedList, self).extend(l) + + def __setitem__(self, key, val): + val = self._ensure_type(val) + + return super(_TypedList, self).__setitem__(key, val) + + def __add__(self, other): + other = self._ensure_type(other) + + return super(_TypedList, self).__add__(other) + + def __iadd__(self, other): + other = self._ensure_type(other) + + return super(_TypedList, self).__iadd__(other) + + def append(self, other): + self += [other] + + return _TypedList + + +def group_unified_files(files, unified_prefix, unified_suffix, files_per_unified_file): + """Return an iterator of (unified_filename, source_filenames) tuples. + + We compile most C and C++ files in "unified mode"; instead of compiling + ``a.cpp``, ``b.cpp``, and ``c.cpp`` separately, we compile a single file + that looks approximately like:: + + #include "a.cpp" + #include "b.cpp" + #include "c.cpp" + + This function handles the details of generating names for the unified + files, and determining which original source files go in which unified + file.""" + + # Our last returned list of source filenames may be short, and we + # don't want the fill value inserted by zip_longest to be an + # issue. So we do a little dance to filter it out ourselves. + dummy_fill_value = ("dummy",) + + def filter_out_dummy(iterable): + return six.moves.filter(lambda x: x != dummy_fill_value, iterable) + + # From the itertools documentation, slightly modified: + def grouper(n, iterable): + "grouper(3, 'ABCDEFG', 'x') --> ABC DEF Gxx" + args = [iter(iterable)] * n + return six.moves.zip_longest(fillvalue=dummy_fill_value, *args) + + for i, unified_group in enumerate(grouper(files_per_unified_file, files)): + just_the_filenames = list(filter_out_dummy(unified_group)) + yield "%s%d.%s" % (unified_prefix, i, unified_suffix), just_the_filenames + + +def pair(iterable): + """Given an iterable, returns an iterable pairing its items. + + For example, + list(pair([1,2,3,4,5,6])) + returns + [(1,2), (3,4), (5,6)] + """ + i = iter(iterable) + return six.moves.zip_longest(i, i) + + +def pairwise(iterable): + """Given an iterable, returns an iterable of overlapped pairs of + its items. Based on the Python itertools documentation. + + For example, + list(pairwise([1,2,3,4,5,6])) + returns + [(1,2), (2,3), (3,4), (4,5), (5,6)] + """ + a, b = itertools.tee(iterable) + next(b, None) + return zip(a, b) + + +VARIABLES_RE = re.compile(r"\$\((\w+)\)") + + +def expand_variables(s, variables): + """Given a string with $(var) variable references, replace those references + with the corresponding entries from the given `variables` dict. + + If a variable value is not a string, it is iterated and its items are + joined with a whitespace.""" + result = "" + for s, name in pair(VARIABLES_RE.split(s)): + result += s + value = variables.get(name) + if not value: + continue + if not isinstance(value, six.string_types): + value = " ".join(value) + result += value + return result + + +class DefinesAction(argparse.Action): + """An ArgumentParser action to handle -Dvar[=value] type of arguments.""" + + def __call__(self, parser, namespace, values, option_string): + defines = getattr(namespace, self.dest) + if defines is None: + defines = {} + values = values.split("=", 1) + if len(values) == 1: + name, value = values[0], 1 + else: + name, value = values + if value.isdigit(): + value = int(value) + defines[name] = value + setattr(namespace, self.dest, defines) + + +class EnumStringComparisonError(Exception): + pass + + +class EnumString(six.text_type): + """A string type that only can have a limited set of values, similarly to + an Enum, and can only be compared against that set of values. + + The class is meant to be subclassed, where the subclass defines + POSSIBLE_VALUES. The `subclass` method is a helper to create such + subclasses. + """ + + POSSIBLE_VALUES = () + + def __init__(self, value): + if value not in self.POSSIBLE_VALUES: + raise ValueError( + "'%s' is not a valid value for %s" % (value, self.__class__.__name__) + ) + + def __eq__(self, other): + if other not in self.POSSIBLE_VALUES: + raise EnumStringComparisonError( + "Can only compare with %s" + % ", ".join("'%s'" % v for v in self.POSSIBLE_VALUES) + ) + return super(EnumString, self).__eq__(other) + + def __ne__(self, other): + return not (self == other) + + def __hash__(self): + return super(EnumString, self).__hash__() + + def __repr__(self): + return f"{self.__class__.__name__}({str(self)!r})" + + +def _escape_char(c): + # str.encode('unicode_espace') doesn't escape quotes, presumably because + # quoting could be done with either ' or ". + if c == "'": + return "\\'" + return six.text_type(c.encode("unicode_escape")) + + +def ensure_bytes(value, encoding="utf-8"): + if isinstance(value, six.text_type): + return value.encode(encoding) + return value + + +def ensure_unicode(value, encoding="utf-8"): + if isinstance(value, six.binary_type): + return value.decode(encoding) + return value + + +def process_time(): + if six.PY2: + return time.clock() + else: + return time.process_time() + + +def hexdump(buf): + """ + Returns a list of hexdump-like lines corresponding to the given input buffer. + """ + assert six.PY3 + off_format = "%0{}x ".format(len(str(len(buf)))) + lines = [] + for off in range(0, len(buf), 16): + line = off_format % off + chunk = buf[off : min(off + 16, len(buf))] + for n, byte in enumerate(chunk): + line += " %02x" % byte + if n == 7: + line += " " + for n in range(len(chunk), 16): + line += " " + if n == 7: + line += " " + line += " |" + for byte in chunk: + if byte < 127 and byte >= 32: + line += chr(byte) + else: + line += "." + for n in range(len(chunk), 16): + line += " " + line += "|\n" + lines.append(line) + return lines + + +def mozilla_build_version(): + mozilla_build = os.environ.get("MOZILLABUILD") + + version_file = Path(mozilla_build) / "VERSION" + + assert version_file.exists(), ( + f'The MozillaBuild VERSION file was not found at "{version_file}".\n' + "Please check if MozillaBuild is installed correctly and that the" + "`MOZILLABUILD` environment variable is to the correct path." + ) + + with version_file.open() as file: + return Version(file.readline().rstrip("\n")) diff --git a/python/mozbuild/mozbuild/vendor/__init__.py b/python/mozbuild/mozbuild/vendor/__init__.py new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/python/mozbuild/mozbuild/vendor/__init__.py diff --git a/python/mozbuild/mozbuild/vendor/docs/index.rst b/python/mozbuild/mozbuild/vendor/docs/index.rst new file mode 100644 index 0000000000..b67cf991cb --- /dev/null +++ b/python/mozbuild/mozbuild/vendor/docs/index.rst @@ -0,0 +1,200 @@ +================================ +Vendoring Third Party Components +================================ + +The firefox source tree vendors many third party dependencies. The build system +provides a normalized way to keep track of: + +1. The upstream source license, location and revision + +2. (Optionally) The upstream source modification, including + + 1. Mozilla-specific patches + + 2. Custom update actions, such as excluding some files, moving files around + etc. + +This is done through a descriptive ``moz.yaml`` file added to the third +party sources, and the use of: + +.. code-block:: sh + + ./mach vendor [options] ./path/to/moz.yaml + +to interact with it. + + +Template ``moz.yaml`` file +========================== + +.. code-block:: yaml + + + # All fields are mandatory unless otherwise noted + + schema: 1 + # Version of this schema + + bugzilla: + # Bugzilla product and component for this directory and subdirectories. + product: product name + component: component name + + origin: + + name: name of the package + # eg. nestegg + + description: short (one line) description + + url: package's homepage url + # Usually different from repository url + + release: identifier + # Human-readable identifier for this version/release + # Generally "version NNN", "tag SSS", "bookmark SSS" + + revision: sha + # Revision to pull in + # Must be a long or short commit SHA (long preferred) + + license: MPL-2.0 + # Multiple licenses can be specified (as a YAML list) + # Where possible using the mnemonic from https://spdx.org/licenses/ + # From a list of approved licenses (to be determined) + # A "LICENSE" file must exist in the destination directory after patches are applied + + license-file: COPYING + # optional + # explicit name of the License file if it's not one of the supported + # hard-coded value. + + vendoring: + # optional + # Information needed to update the library automatically. + + url: source url (generally repository clone url) + # eg. https://github.com/kinetiknz/nestegg.git + # Any repository host can be specified here, however initially we'll only support + # automated vendoring from github.com and googlesource.com, to be extended later. + + source-hosting: gitlab + # name of the infrastructure used to host the sources. Can be one of + # gitlab, angle, googlesource, codeberg, github or git. The later is + # more generic but less efficient. + + patches: + - file + - path/to/file + - path/*.patch + # optional + # List of patch files to apply after vendoring. Applied in the order specified, and + # alphabetically if globbing is used. Patches must apply cleanly before changes are + # pushed + # All patch files are implicitly added to the keep file list. + + keep: + - file + - path/to/file + - another/path + - *.mozilla + # optional + # List of files in mozilla-central that are not deleted while vendoring + # Implicitly contains "moz.yaml", any files referenced as patches + + exclude: + - file + - path/to/file + - another/path + - docs + - src/*.test + # optional + # Files/paths that will not be vendored from source repository + # Implicitly contains ".git", and ".gitignore" + + include: + - file + - path/to/file + - another/path + - docs/LICENSE.* + # optional + # Files/paths that will always be vendored, even if they would + # otherwise be excluded by "exclude". + + # If neither "exclude" or "include" are set, all files will be vendored + # Files/paths in "include" will always be vendored, even if excluded + # eg. excluding "docs/" then including "docs/LICENSE" will vendor just the LICENSE file + # from the docs directory + + # All three file/path parameters ("keep", "exclude", and "include") support filenames, + # directory names, and globs/wildcards. + + update-actions: + - action: move-file + from: '{vendor_dir}/origin' + to: '{vendor_dir}/dest' + + - action: move-dir + from: '{vendor_dir}/origin' + to: '{vendor_dir}/dest' + + - action: copy-file + from: '{vendor_dir}/origin' + to: '{vendor_dir}/dest' + + - action: delete-path + path: "src/unused" + + - action: replace-in-file + pattern: '@REVISION@' + with: '{revision}' + file: '{yaml_dir}/vcs_version.h' + + - action: replace-in-file-regex + file: '{vendor_dir}/lib/arm/armopts.s' + pattern: '@HAVE_ARM_ASM_((EDSP)|(MEDIA)|(NEON))@' + with: '1' + + - action: run-script + script: '{yaml_dir}/update.sh' + args: ['{revision}'] + cwd: '{cwd}' + + # optional + # In-tree actions to be executed after vendoring but before pushing. + + +Common Vendoring Operations +=========================== + + +Update to the latest upstream revision: + +.. code-block:: sh + + ./mach vendor /path/to/moz.yaml + + +Check for latest revision, returning no output if it is up-to-date, and a +version identifier if it needs to be updated: + +.. code-block:: sh + + ./mach vendor /path/to/moz.yaml --check-for-update + +Vendor a specific revision: + +.. code-block:: sh + + ./mach vendor /path/to/moz.yaml -r $REVISION --force + + +In the presence of patches, two steps are needed: + +1. Vendor without applying patches (patches are applied *after* + ``update-actions``) through ``--patch-mode none`` + +2. Apply patches on updated sources through ``--patch -mode only`` + +In the absence of patches, a single step is needed, and no extra argument is +required. diff --git a/python/mozbuild/mozbuild/vendor/host_angle.py b/python/mozbuild/mozbuild/vendor/host_angle.py new file mode 100644 index 0000000000..9716c76a24 --- /dev/null +++ b/python/mozbuild/mozbuild/vendor/host_angle.py @@ -0,0 +1,37 @@ +# 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/. + +import requests + +from mozbuild.vendor.host_base import BaseHost + + +class AngleHost(BaseHost): + def upstream_commit(self, revision): + raise Exception("Should not be called") + + def upstream_tag(self, revision): + data = requests.get("https://omahaproxy.appspot.com/all.json").json() + + for row in data: + if row["os"] == "win64": + for version in row["versions"]: + if version["channel"] == "beta": + branch = "chromium/" + version["true_branch"] + + if revision != "HEAD" and revision != branch: + raise Exception( + "Passing a --revision for Angle that is not HEAD " + + "or the true branch is not supported." + ) + + return ( + branch, + version["current_reldate"], + ) + + raise Exception("Could not find win64 beta version in the JSON response") + + def upstream_snapshot(self, revision): + raise Exception("Not supported for Angle") diff --git a/python/mozbuild/mozbuild/vendor/host_base.py b/python/mozbuild/mozbuild/vendor/host_base.py new file mode 100644 index 0000000000..8ed4144aa6 --- /dev/null +++ b/python/mozbuild/mozbuild/vendor/host_base.py @@ -0,0 +1,79 @@ +# 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/. + +import os +import subprocess +import tempfile +import urllib + + +class BaseHost: + def __init__(self, manifest): + self.manifest = manifest + self.repo_url = urllib.parse.urlparse(self.manifest["vendoring"]["url"]) + + def upstream_tag(self, revision): + """Temporarily clone the repo to get the latest tag and timestamp""" + with tempfile.TemporaryDirectory() as temp_repo_clone: + starting_directory = os.getcwd() + os.chdir(temp_repo_clone) + subprocess.run( + [ + "git", + "clone", + "-c", + "core.autocrlf=input", + self.manifest["vendoring"]["url"], + self.manifest["origin"]["name"], + ], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + universal_newlines=True, + check=True, + ) + os.chdir("/".join([temp_repo_clone, self.manifest["origin"]["name"]])) + revision_arg = [] + if revision and revision != "HEAD": + revision_arg = [revision] + + try: + tag = subprocess.run( + ["git", "--no-pager", "tag", "-l", "--sort=creatordate"] + + revision_arg, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + universal_newlines=True, + check=True, + ).stdout.splitlines()[-1] + except IndexError: # 0 lines of output, the tag does not exist + if revision: + raise Exception(f"Requested tag {revision} not found in source.") + else: + raise Exception("No tags found in source.") + + tag_timestamp = subprocess.run( + [ + "git", + "log", + "-1", + "--date=iso8601-strict", + "--format=%cd", + tag, + ], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + universal_newlines=True, + check=True, + ).stdout.splitlines()[-1] + os.chdir(starting_directory) + return tag, tag_timestamp + + def upstream_snapshot(self, revision): + raise Exception("Unimplemented for this subclass...") + + def upstream_path_to_file(self, revision, filepath): + raise Exception("Unimplemented for this subclass...") + + def upstream_release_artifact(self, revision, release_artifact): + raise Exception("Unimplemented for this subclass...") diff --git a/python/mozbuild/mozbuild/vendor/host_codeberg.py b/python/mozbuild/mozbuild/vendor/host_codeberg.py new file mode 100644 index 0000000000..158dd0472d --- /dev/null +++ b/python/mozbuild/mozbuild/vendor/host_codeberg.py @@ -0,0 +1,28 @@ +# 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/. + +import requests + +from mozbuild.vendor.host_base import BaseHost + + +class CodebergHost(BaseHost): + def upstream_commit(self, revision): + """Query the codeberg api for a git commit id and timestamp.""" + codeberg_api = ( + self.repo_url.scheme + "://" + self.repo_url.netloc + "/api/v1/repos/" + ) + codeberg_api += self.repo_url.path[1:] + codeberg_api += "/git/commits" + req = requests.get("/".join([codeberg_api, revision])) + req.raise_for_status() + info = req.json() + return (info["sha"], info["created"]) + + def upstream_snapshot(self, revision): + codeberg_api = ( + self.repo_url.scheme + "://" + self.repo_url.netloc + "/api/v1/repos/" + ) + codeberg_api += self.repo_url.path[1:] + return "/".join([codeberg_api, "archive", revision + ".tar.gz"]) diff --git a/python/mozbuild/mozbuild/vendor/host_git.py b/python/mozbuild/mozbuild/vendor/host_git.py new file mode 100644 index 0000000000..90f5125422 --- /dev/null +++ b/python/mozbuild/mozbuild/vendor/host_git.py @@ -0,0 +1,36 @@ +# 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/. + +import os +import subprocess +import tempfile + +from mozbuild.vendor.host_base import BaseHost + + +class GitHost(BaseHost): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.workdir = tempfile.TemporaryDirectory(suffix="." + self.repo_url.netloc) + subprocess.check_call( + ["git", "clone", self.repo_url.geturl(), self.workdir.name] + ) + + def upstream_commit(self, revision): + sha = subprocess.check_output( + ["git", "rev-parse", revision], cwd=self.workdir.name + ) + created = subprocess.check_output( + ["git", "show", "--no-patch", "--format=%ci", revision], + cwd=self.workdir.name, + ) + return sha.strip(), created.strip() + + def upstream_snapshot(self, revision): + tarball = os.path.join(self.workdir.name, revision.decode() + ".tar") + subprocess.check_call( + ["git", "archive", "--format=tar", "-o", tarball, revision], + cwd=self.workdir.name, + ) + return "file://" + tarball diff --git a/python/mozbuild/mozbuild/vendor/host_github.py b/python/mozbuild/mozbuild/vendor/host_github.py new file mode 100644 index 0000000000..9300585f71 --- /dev/null +++ b/python/mozbuild/mozbuild/vendor/host_github.py @@ -0,0 +1,38 @@ +# 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/. + +import requests + +from mozbuild.vendor.host_base import BaseHost + + +class GitHubHost(BaseHost): + def api_get(self, path): + """Generic Github API get.""" + repo = self.repo_url.path[1:].strip("/") + github_api = f"https://api.github.com/repos/{repo}/{path}" + req = requests.get(github_api) + req.raise_for_status() + return req.json() + + def upstream_commit(self, revision): + """Query the github api for a git commit id and timestamp.""" + info = self.api_get(f"commits/{revision}") + return (info["sha"], info["commit"]["committer"]["date"]) + + def upstream_snapshot(self, revision): + return "/".join( + [self.manifest["vendoring"]["url"], "archive", revision + ".tar.gz"] + ) + + def upstream_path_to_file(self, revision, filepath): + repo = self.repo_url.path[1:] + return "/".join(["https://raw.githubusercontent.com", repo, revision, filepath]) + + def upstream_release_artifact(self, revision, release_artifact): + repo = self.repo_url.path[1:] + release_artifact = release_artifact.format(tag=revision) + return ( + f"https://github.com/{repo}/releases/download/{revision}/{release_artifact}" + ) diff --git a/python/mozbuild/mozbuild/vendor/host_gitlab.py b/python/mozbuild/mozbuild/vendor/host_gitlab.py new file mode 100644 index 0000000000..8bfc3ddc79 --- /dev/null +++ b/python/mozbuild/mozbuild/vendor/host_gitlab.py @@ -0,0 +1,26 @@ +# 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/. + +import requests + +from mozbuild.vendor.host_base import BaseHost + + +class GitLabHost(BaseHost): + def upstream_commit(self, revision): + """Query the gitlab api for a git commit id and timestamp.""" + gitlab_api = ( + self.repo_url.scheme + "://" + self.repo_url.netloc + "/api/v4/projects/" + ) + gitlab_api += self.repo_url.path[1:].replace("/", "%2F") + gitlab_api += "/repository/commits" + req = requests.get("/".join([gitlab_api, revision])) + req.raise_for_status() + info = req.json() + return (info["id"], info["committed_date"]) + + def upstream_snapshot(self, revision): + return "/".join( + [self.manifest["vendoring"]["url"], "-", "archive", revision + ".tar.gz"] + ) diff --git a/python/mozbuild/mozbuild/vendor/host_googlesource.py b/python/mozbuild/mozbuild/vendor/host_googlesource.py new file mode 100644 index 0000000000..c903bd99b5 --- /dev/null +++ b/python/mozbuild/mozbuild/vendor/host_googlesource.py @@ -0,0 +1,32 @@ +# 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/. + +import requests + +from mozbuild.vendor.host_base import BaseHost + + +class GoogleSourceHost(BaseHost): + def upstream_commit(self, revision): + """Query for a git commit and timestamp.""" + url = "/".join( + [self.manifest["vendoring"]["url"], "+", revision + "?format=JSON"] + ) + req = requests.get(url) + req.raise_for_status() + try: + info = req.json() + except ValueError: + # As of 2017 May, googlesource sends 4 garbage characters + # at the beginning of the json response. Work around this. + # https://bugs.chromium.org/p/chromium/issues/detail?id=718550 + import json + + info = json.loads(req.text[4:]) + return (info["commit"], info["committer"]["time"]) + + def upstream_snapshot(self, revision): + return "/".join( + [self.manifest["vendoring"]["url"], "+archive", revision + ".tar.gz"] + ) diff --git a/python/mozbuild/mozbuild/vendor/mach_commands.py b/python/mozbuild/mozbuild/vendor/mach_commands.py new file mode 100644 index 0000000000..5e8c4664ae --- /dev/null +++ b/python/mozbuild/mozbuild/vendor/mach_commands.py @@ -0,0 +1,231 @@ +# 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/. + +import logging +import sys + +from mach.decorators import Command, CommandArgument, SubCommand + +from mozbuild.vendor.moz_yaml import MozYamlVerifyError, load_moz_yaml + + +# Fun quirk of ./mach - you can specify a default argument as well as subcommands. +# If the default argument matches a subcommand, the subcommand gets called. If it +# doesn't, we wind up in the default command. +@Command( + "vendor", + category="misc", + description="Vendor third-party dependencies into the source repository.", +) +@CommandArgument( + "--check-for-update", + action="store_true", + help="For scripted use, prints the new commit to update to, or nothing if up to date.", + default=False, +) +@CommandArgument( + "--add-to-exports", + action="store_true", + help="Will attempt to add new header files into any relevant EXPORTS block.", + default=False, +) +@CommandArgument( + "--ignore-modified", + action="store_true", + help="Ignore modified files in current checkout.", + default=False, +) +@CommandArgument("-r", "--revision", help="Repository tag or commit to update to.") +@CommandArgument( + "-f", + "--force", + action="store_true", + help="Force a re-vendor even if we're up to date", +) +@CommandArgument( + "--verify", "-v", action="store_true", help="(Only) verify the manifest." +) +@CommandArgument( + "--patch-mode", + help="Select how vendored patches will be imported. 'none' skips patch import, and" + "'only' imports patches and skips library vendoring.", + default="", +) +@CommandArgument("library", nargs=1, help="The moz.yaml file of the library to vendor.") +def vendor( + command_context, + library, + revision, + ignore_modified=False, + check_for_update=False, + add_to_exports=False, + force=False, + verify=False, + patch_mode=None, +): + """ + Vendor third-party dependencies into the source repository. + + Vendoring rust and python can be done with ./mach vendor [rust/python]. + Vendoring other libraries can be done with ./mach vendor [arguments] path/to/file.yaml + """ + library = library[0] + assert library not in ["rust", "python"] + + command_context.populate_logger() + command_context.log_manager.enable_unstructured() + if check_for_update: + logging.disable(level=logging.CRITICAL) + + try: + manifest = load_moz_yaml(library) + if verify: + print("%s: OK" % library) + sys.exit(0) + except MozYamlVerifyError as e: + print(e) + sys.exit(1) + + if "vendoring" not in manifest: + raise Exception( + "Cannot perform update actions if we don't have a 'vendoring' section in the moz.yaml" + ) + + patch_modes = "none", "only", "check" + if patch_mode and patch_mode not in patch_modes: + print( + "Unknown patch mode given '%s'. Please use one of: 'none' or 'only'." + % patch_mode + ) + sys.exit(1) + + patches = manifest["vendoring"].get("patches") + if patches and not patch_mode and not check_for_update: + print( + "Patch mode was not given when required. Please use one of: 'none' or 'only'" + ) + sys.exit(1) + if patch_mode == "only" and not patches: + print( + "Patch import was specified for %s but there are no vendored patches defined." + % library + ) + sys.exit(1) + + if not ignore_modified and not check_for_update: + check_modified_files(command_context) + elif ignore_modified and not check_for_update: + print( + "Because you passed --ignore-modified we will not be " + + "able to detect spurious upstream updates." + ) + + if not revision: + revision = "HEAD" + + from mozbuild.vendor.vendor_manifest import VendorManifest + + vendor_command = command_context._spawn(VendorManifest) + vendor_command.vendor( + command_context, + library, + manifest, + revision, + ignore_modified, + check_for_update, + force, + add_to_exports, + patch_mode, + ) + + sys.exit(0) + + +def check_modified_files(command_context): + """ + Ensure that there aren't any uncommitted changes to files + in the working copy, since we're going to change some state + on the user. + """ + modified = command_context.repository.get_changed_files("M") + if modified: + command_context.log( + logging.ERROR, + "modified_files", + {}, + """You have uncommitted changes to the following files: + +{files} + +Please commit or stash these changes before vendoring, or re-run with `--ignore-modified`. +""".format( + files="\n".join(sorted(modified)) + ), + ) + sys.exit(1) + + +# ===================================================================== + + +@SubCommand( + "vendor", + "rust", + description="Vendor rust crates from crates.io into third_party/rust", +) +@CommandArgument( + "--ignore-modified", + action="store_true", + help="Ignore modified files in current checkout", + default=False, +) +@CommandArgument( + "--force", + action="store_true", + help=("Ignore any kind of error that happens during vendoring"), + default=False, +) +@CommandArgument( + "--issues-json", + help="Path to a code-review issues.json file to write out", +) +def vendor_rust(command_context, **kwargs): + from mozbuild.vendor.vendor_rust import VendorRust + + vendor_command = command_context._spawn(VendorRust) + issues_json = kwargs.pop("issues_json", None) + ok = vendor_command.vendor(**kwargs) + if issues_json: + with open(issues_json, "w") as fh: + fh.write(vendor_command.serialize_issues_json()) + if ok: + sys.exit(0) + else: + print("Errors occured; new rust crates were not vendored.") + sys.exit(1) + + +# ===================================================================== + + +@SubCommand( + "vendor", + "python", + description="Vendor Python packages from pypi.org into third_party/python. " + "Some extra files like docs and tests will automatically be excluded." + "Installs the packages listed in third_party/python/requirements.in and " + "their dependencies.", + virtualenv_name="vendor", +) +@CommandArgument( + "--keep-extra-files", + action="store_true", + default=False, + help="Keep all files, including tests and documentation.", +) +def vendor_python(command_context, keep_extra_files): + from mozbuild.vendor.vendor_python import VendorPython + + vendor_command = command_context._spawn(VendorPython) + vendor_command.vendor(keep_extra_files) diff --git a/python/mozbuild/mozbuild/vendor/moz.build b/python/mozbuild/mozbuild/vendor/moz.build new file mode 100644 index 0000000000..315dc32600 --- /dev/null +++ b/python/mozbuild/mozbuild/vendor/moz.build @@ -0,0 +1,8 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# 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/. + +with Files("**"): + BUG_COMPONENT = ("Developer Infrastructure", "Mach Vendor & Updatebot") diff --git a/python/mozbuild/mozbuild/vendor/moz_yaml.py b/python/mozbuild/mozbuild/vendor/moz_yaml.py new file mode 100644 index 0000000000..c094d22a2b --- /dev/null +++ b/python/mozbuild/mozbuild/vendor/moz_yaml.py @@ -0,0 +1,791 @@ +# 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/. + +# Utility package for working with moz.yaml files. +# +# Requires `pyyaml` and `voluptuous` +# (both are in-tree under third_party/python) + +import errno +import os +import re + +import voluptuous +import yaml +from voluptuous import ( + All, + Boolean, + FqdnUrl, + In, + Invalid, + Length, + Match, + Msg, + Required, + Schema, + Unique, +) +from yaml.error import MarkedYAMLError + +# TODO ensure this matches the approved list of licenses +VALID_LICENSES = [ + # Standard Licenses (as per https://spdx.org/licenses/) + "Apache-2.0", + "BSD-2-Clause", + "BSD-3-Clause", + "BSD-3-Clause-Clear", + "BSL-1.0", + "CC0-1.0", + "ISC", + "ICU", + "LGPL-2.1", + "LGPL-3.0", + "MIT", + "MPL-1.1", + "MPL-2.0", + "Public Domain", + "Unlicense", + "WTFPL", + "Zlib", + # Unique Licenses + "ACE", # http://www.cs.wustl.edu/~schmidt/ACE-copying.html + "Anti-Grain-Geometry", # http://www.antigrain.com/license/index.html + "JPNIC", # https://www.nic.ad.jp/ja/idn/idnkit/download/index.html + "Khronos", # https://www.khronos.org/openmaxdl + "libpng", # http://www.libpng.org/pub/png/src/libpng-LICENSE.txt + "Unicode", # http://www.unicode.org/copyright.html +] + +VALID_SOURCE_HOSTS = ["gitlab", "googlesource", "github", "angle", "codeberg", "git"] + +""" +--- +# Third-Party Library Template +# All fields are mandatory unless otherwise noted + +# Version of this schema +schema: 1 + +bugzilla: + # Bugzilla product and component for this directory and subdirectories + product: product name + component: component name + +# Document the source of externally hosted code +origin: + + # Short name of the package/library + name: name of the package + + description: short (one line) description + + # Full URL for the package's homepage/etc + # Usually different from repository url + url: package's homepage url + + # Human-readable identifier for this version/release + # Generally "version NNN", "tag SSS", "bookmark SSS" + release: identifier + + # Revision to pull in + # Must be a long or short commit SHA (long preferred) + revision: sha + + # The package's license, where possible using the mnemonic from + # https://spdx.org/licenses/ + # Multiple licenses can be specified (as a YAML list) + # A "LICENSE" file must exist containing the full license text + license: MPL-2.0 + + # If the package's license is specified in a particular file, + # this is the name of the file. + # optional + license-file: COPYING + + # If there are any mozilla-specific notes you want to put + # about a library, they can be put here. + notes: Notes about the library + +# Configuration for the automated vendoring system. +# optional +vendoring: + + # Repository URL to vendor from + # eg. https://github.com/kinetiknz/nestegg + # Any repository host can be specified here, however initially we'll only + # support automated vendoring from selected sources. + url: source url (generally repository clone url) + + # Type of hosting for the upstream repository + # Valid values are 'gitlab', 'github', googlesource + source-hosting: gitlab + + # Type of Vendoring + # This is either 'regular', 'individual-files', or 'rust' + # If omitted, will default to 'regular' + flavor: rust + + # Type of git reference (commit, tag) to track updates from. + # You cannot use tag tracking with the individual-files flavor + # If omitted, will default to tracking commits. + tracking: commit + + # When using tag tracking (only on Github currently) use a release artifact + # for the source code instead of the automatically built git-archive exports. + # The source repository must build these artifacts with consistent filenames + # for every tag. This is useful when the Github repository uses submodules + # since they are not included in the git-archives. + # Substitution is performed on the filename, {tag} is replaced with the tag name. + # optional + release-artifact: "rnp-{tag}.tar.gz" + + # Base directory of the location where the source files will live in-tree. + # If omitted, will default to the location the moz.yaml file is in. + vendor-directory: third_party/directory + + # Allows skipping certain steps of the vendoring process. + # Most useful if e.g. vendoring upstream is complicated and should be done by a script + # The valid steps that can be skipped are listed below + skip-vendoring-steps: + - fetch + - keep + - include + - exclude + - move-contents + - hg-add + - spurious-check + - update-moz-yaml + - update-moz-build + + # List of patch files to apply after vendoring. Applied in the order + # specified, and alphabetically if globbing is used. Patches must apply + # cleanly before changes are pushed. + # Patch files should be relative to the vendor-directory rather than the gecko + # root directory. + # All patch files are implicitly added to the keep file list. + # optional + patches: + - file + - path/to/file + - path/*.patch + - path/** # Captures all files and subdirectories below path + - path/* # Captures all files but _not_ subdirectories below path. Equivalent to `path/` + + # List of files that are not removed from the destination directory while vendoring + # in a new version of the library. Intended for mozilla files not present in upstream. + # Implicitly contains "moz.yaml", "moz.build", and any files referenced in + # "patches" + # optional + keep: + - file + - path/to/file + - another/path + - *.mozilla + + # Files/paths that will not be vendored from the upstream repository + # Implicitly contains ".git", and ".gitignore" + # optional + exclude: + - file + - path/to/file + - another/path + - docs + - src/*.test + + # Files/paths that will always be vendored from source repository, even if + # they would otherwise be excluded by "exclude". + # optional + include: + - file + - path/to/file + - another/path + - docs/LICENSE.* + + # Files that are modified as part of the update process. + # To avoid creating updates that don't update anything, ./mach vendor will detect + # if any in-tree files have changed. If there are files that are always changed + # during an update process (e.g. version numbers or source revisions), list them + # here to avoid having them counted as substative changes. + # This field does NOT support directories or globbing + # optional + generated: + - '{yaml_dir}/vcs_version.h' + + # If neither "exclude" or "include" are set, all files will be vendored + # Files/paths in "include" will always be vendored, even if excluded + # eg. excluding "docs/" then including "docs/LICENSE" will vendor just the + # LICENSE file from the docs directory + + # All three file/path parameters ("keep", "exclude", and "include") support + # filenames, directory names, and globs/wildcards. + + # Actions to take after updating. Applied in order. + # The action subfield is required. It must be one of: + # - copy-file + # - move-file + # - move-dir + # - replace-in-file + # - replace-in-file-regex + # - delete-path + # - run-script + # Unless otherwise noted, all subfields of action are required. + # + # If the action is copy-file, move-file, or move-dir: + # from is the source file + # to is the destination + # + # If the action is replace-in-file or replace-in-file-regex: + # pattern is what in the file to search for. It is an exact strng match. + # with is the string to replace it with. Accepts the special keyword + # '{revision}' for the commit we are updating to. + # File is the file to replace it in. + # + # If the action is delete-path + # path is the file or directory to recursively delete + # + # If the action is run-script: + # script is the script to run + # cwd is the directory the script should run with as its cwd + # args is a list of arguments to pass to the script + # + # If the action is run-command: + # command is the command to run + # Unlike run-script, `command` is _not_ processed to be relative + # to the vendor directory, and is passed directly to python's + # execution code without any path substitution or manipulation + # cwd is the directory the command should run with as its cwd + # args is a list of arguments to pass to the command + # + # + # Unless specified otherwise, all files/directories are relative to the + # vendor-directory. If the vendor-directory is different from the + # directory of the yaml file, the keyword '{yaml_dir}' may be used + # to make the path relative to that directory. + # 'run-script' supports the addictional keyword {cwd} which, if used, + # must only be used at the beginning of the path. + # + # optional + update-actions: + - action: copy-file + from: include/vcs_version.h.in + to: '{yaml_dir}/vcs_version.h' + + - action: replace-in-file + pattern: '@VCS_TAG@' + with: '{revision}' + file: '{yaml_dir}/vcs_version.h' + + - action: delete-path + path: '{yaml_dir}/config' + + - action: run-script + script: '{cwd}/generate_sources.sh' + cwd: '{yaml_dir}' + + +# Configuration for automatic updating system. +# optional +updatebot: + + # TODO: allow multiple users to be specified + # Phabricator username for a maintainer of the library, used for assigning + # reviewers. For a review group, preface with #, such as "#build"" + maintainer-phab: tjr + + # Bugzilla email address for a maintainer of the library, used for needinfos + maintainer-bz: tom@mozilla.com + + # Optional: A preset for ./mach try to use. If present, fuzzy-query and fuzzy-paths will + # be ignored. If it, fuzzy-query, and fuzzy-path are omitted, ./mach try auto will be used + try-preset: media + + # Optional: A query string for ./mach try fuzzy. If try-preset, it and fuzzy-paths are omitted + # then ./mach try auto will be used + fuzzy-query: media + + # Optional: An array of test paths for ./mach try fuzzy. If try-preset, it and fuzzy-query are + # omitted then ./mach try auto will be used + fuzzy-paths: ['media'] + + # The tasks that Updatebot can run. Only one of each task is currently permitted + # optional + tasks: + - type: commit-alert + branch: upstream-branch-name + cc: ["bugzilla@email.address", "another@example.com"] + needinfo: ["bugzilla@email.address", "another@example.com"] + enabled: True + filter: security + frequency: every + platform: windows + blocking: 1234 + - type: vendoring + branch: master + enabled: False + + # frequency can be 'every', 'release', 'N weeks', 'N commits' + # or 'N weeks, M commits' requiring satisfying both constraints. + frequency: 2 weeks +""" + +RE_SECTION = re.compile(r"^(\S[^:]*):").search +RE_FIELD = re.compile(r"^\s\s([^:]+):\s+(\S+)$").search + + +class MozYamlVerifyError(Exception): + def __init__(self, filename, error): + self.filename = filename + self.error = error + + def __str__(self): + return "%s: %s" % (self.filename, self.error) + + +def load_moz_yaml(filename, verify=True, require_license_file=True): + """Loads and verifies the specified manifest.""" + + # Load and parse YAML. + try: + with open(filename, "r") as f: + manifest = yaml.load(f, Loader=yaml.BaseLoader) + except IOError as e: + if e.errno == errno.ENOENT: + raise MozYamlVerifyError(filename, "Failed to find manifest: %s" % filename) + raise + except MarkedYAMLError as e: + raise MozYamlVerifyError(filename, e) + + if not verify: + return manifest + + # Verify schema. + if "schema" not in manifest: + raise MozYamlVerifyError(filename, 'Missing manifest "schema"') + if manifest["schema"] == "1": + schema = _schema_1() + schema_additional = _schema_1_additional + schema_transform = _schema_1_transform + else: + raise MozYamlVerifyError(filename, "Unsupported manifest schema") + + try: + schema(manifest) + schema_additional(filename, manifest, require_license_file=require_license_file) + manifest = schema_transform(manifest) + except (voluptuous.Error, ValueError) as e: + raise MozYamlVerifyError(filename, e) + + return manifest + + +def _schema_1(): + """Returns Voluptuous Schema object.""" + return Schema( + { + Required("schema"): "1", + Required("bugzilla"): { + Required("product"): All(str, Length(min=1)), + Required("component"): All(str, Length(min=1)), + }, + "origin": { + Required("name"): All(str, Length(min=1)), + Required("description"): All(str, Length(min=1)), + "notes": All(str, Length(min=1)), + Required("url"): FqdnUrl(), + Required("license"): Msg(License(), msg="Unsupported License"), + "license-file": All(str, Length(min=1)), + Required("release"): All(str, Length(min=1)), + # The following regex defines a valid git reference + # The first group [^ ~^:?*[\]] matches 0 or more times anything + # that isn't a Space, ~, ^, :, ?, *, or ] + # The second group [^ ~^:?*[\]\.]+ matches 1 or more times + # anything that isn't a Space, ~, ^, :, ?, *, [, ], or . + "revision": Match(r"^[^ ~^:?*[\]]*[^ ~^:?*[\]\.]+$"), + }, + "updatebot": { + Required("maintainer-phab"): All(str, Length(min=1)), + Required("maintainer-bz"): All(str, Length(min=1)), + "try-preset": All(str, Length(min=1)), + "fuzzy-query": All(str, Length(min=1)), + "fuzzy-paths": All([str], Length(min=1)), + "tasks": All( + UpdatebotTasks(), + [ + { + Required("type"): In( + ["vendoring", "commit-alert"], + msg="Invalid type specified in tasks", + ), + "branch": All(str, Length(min=1)), + "enabled": Boolean(), + "cc": Unique([str]), + "needinfo": Unique([str]), + "filter": In( + ["none", "security", "source-extensions"], + msg="Invalid filter value specified in tasks", + ), + "source-extensions": Unique([str]), + "blocking": Match(r"^[0-9]+$"), + "frequency": Match( + r"^(every|release|[1-9][0-9]* weeks?|[1-9][0-9]* commits?|" + + r"[1-9][0-9]* weeks?, ?[1-9][0-9]* commits?)$" + ), + "platform": Match(r"^(windows|linux)$"), + } + ], + ), + }, + "vendoring": { + Required("url"): FqdnUrl(), + Required("source-hosting"): All( + str, + Length(min=1), + In(VALID_SOURCE_HOSTS, msg="Unsupported Source Hosting"), + ), + "tracking": Match(r"^(commit|tag)$"), + "release-artifact": All(str, Length(min=1)), + "flavor": Match(r"^(regular|rust|individual-files)$"), + "skip-vendoring-steps": Unique([str]), + "vendor-directory": All(str, Length(min=1)), + "patches": Unique([str]), + "keep": Unique([str]), + "exclude": Unique([str]), + "include": Unique([str]), + "generated": Unique([str]), + "individual-files": [ + { + Required("upstream"): All(str, Length(min=1)), + Required("destination"): All(str, Length(min=1)), + } + ], + "individual-files-default-upstream": All(str, Length(min=1)), + "individual-files-default-destination": All(str, Length(min=1)), + "individual-files-list": Unique([str]), + "update-actions": All( + UpdateActions(), + [ + { + Required("action"): In( + [ + "copy-file", + "move-file", + "move-dir", + "replace-in-file", + "replace-in-file-regex", + "run-script", + "run-command", + "delete-path", + ], + msg="Invalid action specified in update-actions", + ), + "from": All(str, Length(min=1)), + "to": All(str, Length(min=1)), + "pattern": All(str, Length(min=1)), + "with": All(str, Length(min=1)), + "file": All(str, Length(min=1)), + "script": All(str, Length(min=1)), + "command": All(str, Length(min=1)), + "args": All([All(str, Length(min=1))]), + "cwd": All(str, Length(min=1)), + "path": All(str, Length(min=1)), + } + ], + ), + }, + } + ) + + +def _schema_1_additional(filename, manifest, require_license_file=True): + """Additional schema/validity checks""" + + vendor_directory = os.path.dirname(filename) + if "vendoring" in manifest and "vendor-directory" in manifest["vendoring"]: + vendor_directory = manifest["vendoring"]["vendor-directory"] + + # LICENSE file must exist, except for Rust crates which are exempted + # because the license is required to be specified in the Cargo.toml file + if require_license_file and "origin" in manifest: + files = [f.lower() for f in os.listdir(vendor_directory)] + if ( + not ( + "license-file" in manifest["origin"] + and manifest["origin"]["license-file"].lower() in files + ) + and not ( + "license" in files + or "license.txt" in files + or "license.rst" in files + or "license.html" in files + or "license.md" in files + ) + and not ( + "vendoring" in manifest + and manifest["vendoring"].get("flavor", "regular") == "rust" + ) + ): + license = manifest["origin"]["license"] + if isinstance(license, list): + license = "/".join(license) + raise ValueError("Failed to find %s LICENSE file" % license) + + # Cannot vendor without an origin. + if "vendoring" in manifest and "origin" not in manifest: + raise ValueError('"vendoring" requires an "origin"') + + # Cannot vendor without a computer-readable revision. + if "vendoring" in manifest and "revision" not in manifest["origin"]: + raise ValueError( + 'If "vendoring" is present, "revision" must be present in "origin"' + ) + + # The Rust and Individual Flavor type precludes a lot of options + # individual-files could, in theory, use several of these, but until we have a use case let's + # disallow them so we're not worrying about whether they work. When we need them we can make + # sure they do. + if ( + "vendoring" in manifest + and manifest["vendoring"].get("flavor", "regular") != "regular" + ): + for i in [ + "skip-vendoring-steps", + "keep", + "exclude", + "include", + "generated", + ]: + if i in manifest["vendoring"]: + raise ValueError("A non-regular flavor of update cannot use '%s'" % i) + + if manifest["vendoring"].get("flavor", "regular") == "rust": + for i in [ + "update-actions", + ]: + if i in manifest["vendoring"]: + raise ValueError("A rust flavor of update cannot use '%s'" % i) + + # Ensure that only individual-files flavor uses those options + if ( + "vendoring" in manifest + and manifest["vendoring"].get("flavor", "regular") != "individual-files" + ): + if ( + "individual-files" in manifest["vendoring"] + or "individual-files-list" in manifest["vendoring"] + ): + raise ValueError( + "Only individual-files flavor of update can use 'individual-files'" + ) + + # Ensure that release-artifact is only used with tag tracking + if "vendoring" in manifest and "release-artifact" in manifest["vendoring"]: + if ( + manifest["vendoring"].get("source-hosting") != "github" + or manifest["vendoring"].get("tracking", "commit") != "tag" + ): + raise ValueError( + "You can only use release-artifact with tag tracking from Github." + ) + + # Ensure that the individual-files flavor has all the correct options + if ( + "vendoring" in manifest + and manifest["vendoring"].get("flavor", "regular") == "individual-files" + ): + # Because the only way we can determine the latest tag is by doing a local clone, + # we don't want to do that for individual-files flavors because those flavors are + # usually on gigantic repos we don't want to clone for such a simple thing. + if manifest["vendoring"].get("tracking", "commit") == "tag": + raise ValueError( + "You cannot use tag tracking with the individual-files flavor. (Sorry.)" + ) + + # We need either individual-files or individual-files-list + if ( + "individual-files" not in manifest["vendoring"] + and "individual-files-list" not in manifest["vendoring"] + ): + raise ValueError( + "The individual-files flavor must include either " + + "'individual-files' or 'individual-files-list'" + ) + # For whichever we have, make sure we don't have the other and we don't have + # options we shouldn't or lack ones we should. + if "individual-files" in manifest["vendoring"]: + if "individual-files-list" in manifest["vendoring"]: + raise ValueError( + "individual-files-list is mutually exclusive with individual-files" + ) + if "individual-files-default-upstream" in manifest["vendoring"]: + raise ValueError( + "individual-files-default-upstream can only be used with individual-files-list" + ) + if "individual-files-default-destination" in manifest["vendoring"]: + raise ValueError( + "individual-files-default-destination can only be used " + + "with individual-files-list" + ) + if "individual-files-list" in manifest["vendoring"]: + if "individual-files" in manifest["vendoring"]: + raise ValueError( + "individual-files is mutually exclusive with individual-files-list" + ) + if "individual-files-default-upstream" not in manifest["vendoring"]: + raise ValueError( + "individual-files-default-upstream must be used with individual-files-list" + ) + if "individual-files-default-destination" not in manifest["vendoring"]: + raise ValueError( + "individual-files-default-destination must be used with individual-files-list" + ) + + if "updatebot" in manifest: + # If there are Updatebot tasks, then certain fields must be present and + # defaults need to be set. + if "tasks" in manifest["updatebot"]: + if "vendoring" not in manifest or "url" not in manifest["vendoring"]: + raise ValueError( + "If Updatebot tasks are specified, a vendoring url must be included." + ) + + if "try-preset" in manifest["updatebot"]: + for f in ["fuzzy-query", "fuzzy-paths"]: + if f in manifest["updatebot"]: + raise ValueError( + "If 'try-preset' is specified, then %s cannot be" % f + ) + + # Check for a simple YAML file + with open(filename, "r") as f: + has_schema = False + for line in f.readlines(): + m = RE_SECTION(line) + if m: + if m.group(1) == "schema": + has_schema = True + break + if not has_schema: + raise ValueError("Not simple YAML") + + +# Do type conversion for the few things that need it. +# Everythig is parsed as a string to (a) not cause problems with revisions that +# are only numerals and (b) not strip leading zeros from the numbers if we just +# converted them to string +def _schema_1_transform(manifest): + if "updatebot" in manifest: + if "tasks" in manifest["updatebot"]: + for i in range(len(manifest["updatebot"]["tasks"])): + if "enabled" in manifest["updatebot"]["tasks"][i]: + val = manifest["updatebot"]["tasks"][i]["enabled"] + manifest["updatebot"]["tasks"][i]["enabled"] = ( + val.lower() == "true" or val.lower() == "yes" + ) + return manifest + + +class UpdateActions(object): + """Voluptuous validator which verifies the update actions(s) are valid.""" + + def __call__(self, values): + for v in values: + if "action" not in v: + raise Invalid("All file-update entries must specify a valid action") + if v["action"] in ["copy-file", "move-file", "move-dir"]: + if "from" not in v or "to" not in v or len(v.keys()) != 3: + raise Invalid( + "%s action must (only) specify 'from' and 'to' keys" + % v["action"] + ) + elif v["action"] in ["replace-in-file", "replace-in-file-regex"]: + if ( + "pattern" not in v + or "with" not in v + or "file" not in v + or len(v.keys()) != 4 + ): + raise Invalid( + "replace-in-file action must (only) specify " + + "'pattern', 'with', and 'file' keys" + ) + elif v["action"] == "delete-path": + if "path" not in v or len(v.keys()) != 2: + raise Invalid( + "delete-path action must (only) specify the 'path' key" + ) + elif v["action"] == "run-script": + if "script" not in v or "cwd" not in v: + raise Invalid( + "run-script action must specify 'script' and 'cwd' keys" + ) + if set(v.keys()) - set(["args", "cwd", "script", "action"]) != set(): + raise Invalid( + "run-script action may only specify 'script', 'cwd', and 'args' keys" + ) + elif v["action"] == "run-command": + if "command" not in v or "cwd" not in v: + raise Invalid( + "run-command action must specify 'command' and 'cwd' keys" + ) + if set(v.keys()) - set(["args", "cwd", "command", "action"]) != set(): + raise Invalid( + "run-command action may only specify 'command', 'cwd', and 'args' keys" + ) + else: + # This check occurs before the validator above, so the above is + # redundant but we leave it to be verbose. + raise Invalid("Supplied action " + v["action"] + " is invalid.") + return values + + def __repr__(self): + return "UpdateActions" + + +class UpdatebotTasks(object): + """Voluptuous validator which verifies the updatebot task(s) are valid.""" + + def __call__(self, values): + seenTaskTypes = set() + for v in values: + if "type" not in v: + raise Invalid("All updatebot tasks must specify a valid type") + + if v["type"] in seenTaskTypes: + raise Invalid("Only one type of each task is currently supported") + seenTaskTypes.add(v["type"]) + + if v["type"] == "vendoring": + for i in ["filter", "branch", "source-extensions"]: + if i in v: + raise Invalid( + "'%s' is only valid for commit-alert task types" % i + ) + elif v["type"] == "commit-alert": + pass + else: + # This check occurs before the validator above, so the above is + # redundant but we leave it to be verbose. + raise Invalid("Supplied type " + v["type"] + " is invalid.") + return values + + def __repr__(self): + return "UpdatebotTasks" + + +class License(object): + """Voluptuous validator which verifies the license(s) are valid as per our + allow list.""" + + def __call__(self, values): + if isinstance(values, str): + values = [values] + elif not isinstance(values, list): + raise Invalid("Must be string or list") + for v in values: + if v not in VALID_LICENSES: + raise Invalid("Bad License") + return values + + def __repr__(self): + return "License" diff --git a/python/mozbuild/mozbuild/vendor/rewrite_mozbuild.py b/python/mozbuild/mozbuild/vendor/rewrite_mozbuild.py new file mode 100644 index 0000000000..8163c05dc3 --- /dev/null +++ b/python/mozbuild/mozbuild/vendor/rewrite_mozbuild.py @@ -0,0 +1,1286 @@ +# 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/. + +# Utility package for working with moz.yaml files. +# +# Requires `pyyaml` and `voluptuous` +# (both are in-tree under third_party/python) + +""" +Problem: + ./mach vendor needs to be able to add or remove files from moz.build files automatically to + be able to effectively update a library automatically and send useful try runs in. + + So far, it has been difficult to do that. + + Why: + - Some files need to go into UNIFIED_SOURCES vs SOURCES + - Some files are os-specific, and need to go into per-OS conditionals + - Some files are both UNIFIED_SOURCES/SOURCES sensitive and OS-specific. + +Proposal: + Design an algorithm that maps a third party library file to a suspected moz.build location. + Run the algorithm on all files specified in all third party libraries' moz.build files. + See if the proposed place in the moz.build file matches the actual place. + +Initial Algorithm + Given a file, which includes the filename and the path from gecko root, we want to find the + correct moz.build file and location within that file. + Take the path of the file, and iterate up the directory tree, looking for moz.build files as + we go. + Consider each of these moz.build files, starting with the one closest to the file. + Within a moz.build file, identify the SOURCES or UNIFIED_SOURCES block(s) that contains a file + in the same directory path as the file to be added. + If there is only one such block, use that one. + If there are multiple blocks, look at the files within each block and note the longest length + of a common prefix (including partial filenames - if we just did full directories the + result would be the same as the prior step and we would not narrow the results down). Use + the block containing the longest prefix. (We call this 'guessing'.) + +Result of the proposal: + The initial implementation works on 1675 of 1977 elligible files. + The files it does not work on include: + - general failures. Such as when we find that avutil.cpp wants to be next to adler32.cpp + but avutil.cpp is in SOURCES and adler32.cpp is in UNIFIED_SOURCES. (And many similar + cases.) + - per-cpu-feature files, where only a single file is added under a conditional + - When guessing, because of a len(...) > longest_so_far comparison, we would prefer the + first block we found. + - Changing this to prefer UNIFIED_SOURCES in the event of a tie + yielded 17 additional correct assignments (about a 1% improvement) + - As a result of the change immediately above, when guessing, because given equal + prefixes, we would prefer a UNIFIED_SOURCES block over other blocks, even if the other + blocks are longer + - Changing this (again) to prefer the block containing more files yielded 49 additional + correct assignments (about a 2.5% improvement) + + The files that are ineligible for consideration are: + - Those in libwebrtc + - Those specified in source assignments composed of generators (e.g. [f for f in '%.c']) + - Those specified in source assignments to subscripted variables + (e.g. SOURCES += foo['x86_files']) + + We needed to iterate up the directory and look at a different moz.build file _zero_ times. + This indicates this code is probably not needed, and therefore we will remove it from the + algorithm. + We needed to guess base on the longest prefix 944 times, indicating that this code is + absolutely crucial and should be double-checked. (And indeed, upon double-checking it, + bugs were identified.) + + After some initial testing, it was determined that this code completely fell down when the + vendoring directory differed from the moz.yaml directory (definitions below.) The code was + slightly refactored to handle this case, primarily by (a) re-inserting the logic to check + multiple moz.build files instead of the first and (b) handling some complicated normalization + notions (details in comments). + +Slightly Improved Algorithm Changes: + Don't bother iterating up the directory tree looking for moz.build files, just take the first. + When guessing, in the event of a common-prefix tie, prefer the block containing more files + + With these changes, we now Successfully Matched 1724 of 1977 files + +CODE CONCEPTS + +source-assignment + An assignment of files to a SOURCES or UNIFIED_SOURCES variable, such as + SOURCES += ['ffpvx.cpp'] + + We specifically look only for these two variable names to avoid identifying things + such as CXX_FLAGS. + + Sometimes; however, there is an intermediary variable, such as `SOURCES += celt_filenames` + In this situation we find the celt_filenames assignment, and treat it as a 'source-assignment' + +source-assignment-location + source-assignment-location is a human readable string that identifies where in the moz.build + file the source-assignment is. It can used to visually match the location upon manual + inspection; and given a source-assignment-location, re-identify it when iterating over all + source-assignments in a file. + + The actual string consists of the path from the root of the moz.build file to the + source-assignment, plus a suffix number. + + We suffix the final value with an incrementing counter. This is to support moz.build files + that, for whatever reason, use multiple SOURCES += [] list in the same basic block. This index + is per-file, so no two assignments in the same file (even if they have separate locations) + should have the same suffix. + + For example: + + When `SOURCES += ['ffpvx.xpp']` appears as the first line of the file (or any other + unindented-location) its source-assignment-location will be `> SOURCES 1`. + + When `SOURCES += ['ffpvx.xpp']` appears inside a conditional such as + `CONFIG['OS_TARGET'] == 'WINNT'` then its source-assignment-location will be + `> if CONFIG['OS_TARGET'] == 'WINNT' > SOURCES 1` + + When SOURCES += ['ffpvx.xpp'] appears as the second line of the file, and a different + SOURCES += [] was the first line, then its source-assignment-location will be "> SOURCES 2". + + No two source-assignments may have the same source-assignment-location. If they do, we raise + an assert. + +file vs filename + a 'filename' is a string specifing the name and sometimes the path of a file. + a 'file' is an object you get from open()-ing a filename + + A variable that is a string should always use 'filename' + +vendoring directory vs moz.yaml directory + In many cases, a library's moz.yaml file, moz.build file(s), and sources files will all live + under a single directory. e.g. libjpeg + + In other cases, a library's source files are in one directory (we call this the 'vendoring + directory') and the moz.yaml file and moz.build file(s) are in another directory (we call this + the moz.yaml directory). e.g. libdav1d + +normalized-filename + A filename is 'normalized' if it has been expanded to the full path from the gecko root. This + requires a moz.build file. + + For example a filename `lib/opus.c` may be specified inside the `media/libopus/moz.build` + file. The filename is normalized by os.path.join()-ing the dirname of the moz.build file + (i.e. `media/libopus`) to the filename, resulting in `media/libopus/lib/opus.c` + + A filename that begins with '/' is presumed to already be specified relative to the gecko + root, and therefore is not modified. + + Normalization gets more complicated when dealing with separate vendoring and moz.yaml + directories. This is because a file can be considered normalized when it looks like + third_party/libdav1d/src/a.cpp + _or_ when it looks like + media/libdav1d/../../third_party/libdav1d/src/a.cpp + This is because in the moz.build file, it will be specified as + `../../third_party/libdav1d/src/a.cpp` and we 'normalize' it by prepending the path to the + moz.build file. + + Normalization is not just about having an 'absolute' path from gecko_root to file. In fact + it's not really about that at all - it's about matching filenames. Therefore when we are + dealing with separate vendoring and moz.yaml directories we will very quickly 're-normalize' + a normalized filename to get it into one of those foo/bar/../../third_party/... paths that + will make sense for the moz.build file we are interested in. + + Whenever a filename is normalized, it should be specified as such in the variable name, + either as a prefix (normalized_filename) or a suffix (target_filename_normalized) + +statistic + Using some hacky stuff, we report statistics about how many times we hit certain branches of + the code. + e.g. + - "How many times did we refine a guess based on prefix length" + - "How many times did we refine a guess based on the number of files in the block" + - "What is the histogram of guess candidates" + + We do this to identify how frequently certain code paths were taken, allowing us to identify + strange behavior and investigate outliers. This process lead to identifying bugs and small + improvements. +""" + +import ast +import copy +import os +import re +import shutil +import subprocess +import sys +from pprint import pprint + +try: + from mozbuild.frontend.sandbox import alphabetical_sorted +except Exception: + + def alphabetical_sorted(iterable, key=lambda x: x.lower(), reverse=False): + return sorted(iterable, key=key, reverse=reverse) + + +# This can be edited to enable better Python 3.8 behavior, but is set so that +# everything is consistent by default so errors can be detected more easily. +FORCE_DOWNGRADE_BEHAVIOR = True + +statistics = { + "guess_candidates": {}, + "number_refinements": {}, + "needed_to_guess": 0, + "length_logic": {}, +} + + +def log(*args, **kwargs): + # If is helpful to keep some logging statements around, but we don't want to print them + # unless we are debugging + # print(*args, **kwargs) + pass + + +############################################## + +import inspect + + +def node_to_name(code, node): + if ( + not FORCE_DOWNGRADE_BEHAVIOR + and sys.version_info[0] >= 3 + and sys.version_info[1] >= 8 + ): + return ast.get_source_segment(code, node) + + return node.__class__.__name__ + + +def get_attribute_label(node): + assert isinstance(node, ast.Attribute) + + label = "" + subtarget = node + while isinstance(subtarget, ast.Attribute): + label = subtarget.attr + ("." if label else "") + label + subtarget = subtarget.value + + if isinstance(subtarget, ast.Name): + label = subtarget.id + "." + label + elif isinstance(subtarget, ast.Subscript) and isinstance(subtarget.value, ast.Name): + label = subtarget.value.id + "." + label + else: + raise Exception( + "Unxpected subtarget of type %s found in get_attribute_label. label=%s" + % (subtarget, label) + ) + + return label + + +def ast_get_source_segment(code, node): + caller = inspect.stack()[1] + + if "sphinx" in caller.filename or ( + not FORCE_DOWNGRADE_BEHAVIOR + and sys.version_info[0] >= 3 + and sys.version_info[1] >= 8 + ): + return ast.original_get_source_segment(code, node) + + if caller.function == "assignment_node_to_source_filename_list": + return "" + + raise Exception( + "ast_get_source_segment is not available with this Python version. (ver=%s.%s, caller=%s)" + % (sys.version_info.major, sys.version_info.minor, caller.function) + ) + + +# Overwrite it so we don't accidently use it +if sys.version_info[0] >= 3 and sys.version_info[1] >= 8: + ast.original_get_source_segment = ast.get_source_segment + ast.get_source_segment = ast_get_source_segment + + +############################################## + + +def node_to_readable_file_location(code, node, child_node=None): + location = "" + + if isinstance(node.parent, ast.Module): + # The next node up is the root, don't go higher. + pass + else: + location += node_to_readable_file_location(code, node.parent, node) + + location += " > " + if isinstance(node, ast.Module): + raise Exception("We shouldn't see a Module") + elif isinstance(node, ast.If): + assert child_node + if child_node in node.body: + location += "if " + node_to_name(code, node.test) + else: + location += "else-of-if " + node_to_name(code, node.test) + elif isinstance(node, ast.For): + location += ( + "for " + + node_to_name(code, node.target) + + " in " + + node_to_name(code, node.iter) + ) + elif isinstance(node, ast.AugAssign): + if isinstance(node.target, ast.Name): + location += node.target.id + else: + location += node_to_name(code, node.target) + elif isinstance(node, ast.Assign): + # This assert would fire if we did e.g. some_sources = all_sources = [ ... ] + assert len(node.targets) == 1, "Assignment node contains more than one target" + if isinstance(node.targets[0], ast.Name): + location += node.targets[0].id + else: + location += node_to_name(code, node.targets[0]) + else: + raise Exception("Got a node type I don't know how to handle: " + str(node)) + + return location + + +def assignment_node_to_source_filename_list(code, node): + """ + If the list of filenames is not a list of constants (e.g. it's a generated list) + it's (probably) infeasible to try and figure it out. At least we're not going to try + right now. Maybe in the future? + + If this happens, we'll return an empty list. The consequence of this is that we + won't be able to match a file against this list, so we may not be able to add it. + + (But if the file matches a generated list, perhaps it will be included in the + Sources list automatically?) + """ + if isinstance(node.value, ast.List) and "elts" in node.value._fields: + for f in node.value.elts: + if not isinstance(f, ast.Constant) and not isinstance(f, ast.Str): + log( + "Found non-constant source file name in list: ", + ast_get_source_segment(code, f), + ) + return [] + return [ + f.value if isinstance(f, ast.Constant) else f.s for f in node.value.elts + ] + elif isinstance(node.value, ast.ListComp): + # SOURCES += [f for f in foo if blah] + log("Could not find the files for " + ast_get_source_segment(code, node.value)) + elif isinstance(node.value, ast.Name) or isinstance(node.value, ast.Subscript): + # SOURCES += other_var + # SOURCES += files['X64_SOURCES'] + log("Could not find the files for " + ast_get_source_segment(code, node)) + elif isinstance(node.value, ast.Call): + # SOURCES += sorted(...) + log("Could not find the files for " + ast_get_source_segment(code, node)) + else: + raise Exception( + "Unexpected node received in assignment_node_to_source_filename_list: " + + str(node) + ) + return [] + + +def mozbuild_file_to_source_assignments(normalized_mozbuild_filename, assignment_type): + """ + Returns a dictionary of 'source-assignment-location' -> 'normalized source filename list' + contained in the moz.build file specified + + normalized_mozbuild_filename: the moz.build file to read + """ + source_assignments = {} + + if assignment_type == "source-files": + targets = ["SOURCES", "UNIFIED_SOURCES"] + else: + targets = ["EXPORTS"] + + # Parse the AST of the moz.build file + code = open(normalized_mozbuild_filename).read() + root = ast.parse(code) + + # Populate node parents. This allows us to walk up from a node to the root. + # (Really I think python's ast class should do this, but it doesn't, so we monkey-patch it) + for node in ast.walk(root): + for child in ast.iter_child_nodes(node): + child.parent = node + + # Find all the assignments of SOURCES or UNIFIED_SOURCES + if assignment_type == "source-files": + source_assignment_nodes = [ + node + for node in ast.walk(root) + if isinstance(node, ast.AugAssign) + and isinstance(node.target, ast.Name) + and node.target.id in targets + ] + assert ( + len([n for n in source_assignment_nodes if not isinstance(n.op, ast.Add)]) + == 0 + ), "We got a Source assignment that wasn't +=" + + # Recurse and find nodes where we do SOURCES += other_var or SOURCES += FILES['foo'] + recursive_assignment_nodes = [ + node + for node in source_assignment_nodes + if isinstance(node.value, ast.Name) or isinstance(node.value, ast.Subscript) + ] + + recursive_assignment_nodes_names = [ + node.value.id + for node in recursive_assignment_nodes + if isinstance(node.value, ast.Name) + ] + + # TODO: We do not dig into subscript variables. These are currently only used by two + # libraries that use external sources.mozbuild files. + # recursive_assignment_nodes_names.extend([something<node> for node in + # recursive_assignment_nodes if isinstance(node.value, ast.Subscript)] + + additional_assignment_nodes = [ + node + for node in ast.walk(root) + if isinstance(node, ast.Assign) + and isinstance(node.targets[0], ast.Name) + and node.targets[0].id in recursive_assignment_nodes_names + ] + + # Remove the original, useless assignment node (the SOURCES += other_var) + for node in recursive_assignment_nodes: + source_assignment_nodes.remove(node) + # Add the other_var += [''] source-assignment + source_assignment_nodes.extend(additional_assignment_nodes) + else: + source_assignment_nodes = [ + node + for node in ast.walk(root) + if isinstance(node, ast.AugAssign) + and ( + (isinstance(node.target, ast.Name) and node.target.id == "EXPORTS") + or ( + isinstance(node.target, ast.Attribute) + and get_attribute_label(node.target).startswith("EXPORTS") + ) + ) + ] + source_assignment_nodes.extend( + [ + node + for node in ast.walk(root) + if isinstance(node, ast.Assign) + and ( + ( + isinstance(node.targets[0], ast.Name) + and node.targets[0].id == "EXPORTS" + ) + or ( + isinstance(node.targets[0], ast.Attribute) + and get_attribute_label(node.targets[0]).startswith("EXPORTS") + ) + ) + ] + ) + + # Get the source-assignment-location for the node: + assignment_index = 1 + for a in source_assignment_nodes: + source_assignment_location = ( + node_to_readable_file_location(code, a) + " " + str(assignment_index) + ) + source_filename_list = assignment_node_to_source_filename_list(code, a) + + if not source_filename_list: + # In some cases (like generated source file lists) we will have an empty list. + # If that is the case, just omit the source assignment + continue + + normalized_source_filename_list = [ + normalize_filename(normalized_mozbuild_filename, f) + for f in source_filename_list + ] + + if source_assignment_location in source_assignments: + source_assignment_location = node_to_readable_file_location(code, a) + + assert ( + source_assignment_location not in source_assignments + ), "In %s, two assignments have the same key ('%s')" % ( + normalized_mozbuild_filename, + source_assignment_location, + ) + source_assignments[source_assignment_location] = normalized_source_filename_list + assignment_index += 1 + + return (source_assignments, root, code) + + +def unnormalize_filename(normalized_mozbuild_filename, normalized_filename): + if normalized_filename[0] == "/": + return normalized_filename + + mozbuild_path = ( + os.path.dirname(normalized_mozbuild_filename).replace(os.path.sep, "/") + "/" + ) + return normalized_filename.replace(mozbuild_path, "") + + +def normalize_filename(normalized_mozbuild_filename, filename): + if filename[0] == "/": + return filename + + mozbuild_path = os.path.dirname(normalized_mozbuild_filename).replace( + os.path.sep, "/" + ) + return os.path.join(mozbuild_path, filename).replace(os.path.sep, "/") + + +def get_mozbuild_file_search_order( + normalized_filename, + moz_yaml_dir=None, + vendoring_dir=None, + all_mozbuild_filenames_normalized=None, +): + """ + Returns an ordered list of normalized moz.build filenames to consider for a given filename + + normalized_filename: a source filename normalized to the gecko root + + moz_yaml_dir: the path from gecko_root to the moz.yaml file (which is the root of the + moz.build files) + + moz_yaml_dir: the path to where the library's source files are + + all_mozbuild_filenames_normalized: (optional) the list of all third-party moz.build files + If all_mozbuild_filenames_normalized is not specified, we look in the filesystem. + + The list is built out of two distinct steps. + + In Step 1 we will walk up a directory tree, looking for moz.build files. We append moz.build + files in this order, preferring the lowest moz.build we find, then moving on to one in a + higher directory. + The directory we start in is a little complicated. We take the series of subdirectories + between vendoring_dir and the file in question, and then append them to the moz.yaml + directory. + + Example: + + .. code-block:: python + + When moz_yaml directory != vendoring_directory: + moz_yaml_dir = foo/bar/ + vendoring_dir = third_party/baz/ + normalized_filename = third_party/baz/asm/arm/a.S + starting_directory: foo/bar/asm/arm/ + When moz_yaml directory == vendoring_directory + (In this case, these variables will actually be 'None' but the algorthm is the same) + moz_yaml_dir = foo/bar/ + vendoring_dir = foo/bar/ + normalized_filename = foo/bar/asm/arm/a.S + starting_directory: foo/bar/asm/arm/ + + In Step 2 we get a bit desparate. When the vendoring directory and the moz_yaml directory are + not the same, there is no guarentee that the moz_yaml directory will adhere to the same + directory structure as the vendoring directory. And indeed it doesn't in some cases + (e.g. libdav1d.) + So in this situation we start at the root of the moz_yaml directory and walk downwards, adding + _any_ moz.build file we encounter to the list. Later on (in all cases, not just + moz_yaml_dir != vendoring_dir) we only consider a moz.build file if it has source files whose + directory matches the normalized_filename, so this step, though desparate, is safe-ish and + believe it or not has worked for some file additions. + """ + ordered_list = [] + + if all_mozbuild_filenames_normalized is None: + assert os.path.isfile( + ".arcconfig" + ), "We do not seem to be running from the gecko root" + + # The first time around, this variable name is incorrect. + # It's actually the full path+filename, not a directory. + test_directory = None + if (moz_yaml_dir, vendoring_dir) == (None, None): + # In this situation, the library is vendored into the same directory as + # the moz.build files. We can start traversing directories up from the file to + # add to find the correct moz.build file + test_directory = normalized_filename + elif moz_yaml_dir and vendoring_dir: + # In this situation, the library is vendored in a different place (typically + # third_party/foo) from the moz.build files. + subdirectory_path = normalized_filename.replace(vendoring_dir, "") + test_directory = os.path.join(moz_yaml_dir, subdirectory_path) + else: + raise Exception("If moz_yaml_dir or vendoring_dir are specified, both must be") + + # Step 1 + while ( + len(os.path.dirname(test_directory).replace(os.path.sep, "/")) > 1 + ): # While we are not at '/' + containing_directory = os.path.dirname(test_directory) + + possible_normalized_mozbuild_filename = os.path.join( + containing_directory, "moz.build" + ) + + if not all_mozbuild_filenames_normalized: + if os.path.isfile(possible_normalized_mozbuild_filename): + ordered_list.append(possible_normalized_mozbuild_filename) + elif possible_normalized_mozbuild_filename in all_mozbuild_filenames_normalized: + ordered_list.append(possible_normalized_mozbuild_filename) + + test_directory = containing_directory + + # Step 2 + if moz_yaml_dir: + for root, dirs, files in os.walk(moz_yaml_dir): + for f in files: + if f == "moz.build": + ordered_list.append(os.path.join(root, f)) + + return ordered_list + + +def get_closest_mozbuild_file( + normalized_filename, + moz_yaml_dir=None, + vendoring_dir=None, + all_mozbuild_filenames_normalized=None, +): + """ + Returns the closest moz.build file in the directory tree to a normalized filename + """ + r = get_mozbuild_file_search_order( + normalized_filename, + moz_yaml_dir, + vendoring_dir, + all_mozbuild_filenames_normalized, + ) + return r[0] if r else None + + +def filenames_directory_is_in_filename_list( + filename_normalized, list_of_normalized_filenames +): + """ + Given a normalized filename and a list of normalized filenames, first turn them into a + containing directory, and a list of containing directories. Then test if the containing + directory of the filename is in the list. + + ex: + f = filenames_directory_is_in_filename_list + f("foo/bar/a.c", ["foo/b.c"]) -> false + f("foo/bar/a.c", ["foo/b.c", "foo/bar/c.c"]) -> true + f("foo/bar/a.c", ["foo/b.c", "foo/bar/baz/d.c"]) -> false + """ + path_list = set( + [ + os.path.dirname(f).replace(os.path.sep, "/") + for f in list_of_normalized_filenames + ] + ) + return os.path.dirname(filename_normalized).replace(os.path.sep, "/") in path_list + + +def find_all_posible_assignments_from_filename(source_assignments, filename_normalized): + """ + Given a list of source assignments and a normalized filename, narrow the list to assignments + that contain a file whose directory matches the filename's directory. + """ + possible_assignments = {} + for key, list_of_normalized_filenames in source_assignments.items(): + if not list_of_normalized_filenames: + continue + if filenames_directory_is_in_filename_list( + filename_normalized, list_of_normalized_filenames + ): + possible_assignments[key] = list_of_normalized_filenames + return possible_assignments + + +def guess_best_assignment(source_assignments, filename_normalized): + """ + Given several assignments, all of which contain the same directory as the filename, pick one + we think is best and return its source-assignment-location. + + We do this by looking at the filename itself (not just its directory) and picking the + assignment which contains a filename with the longest matching prefix. + + e.g: "foo/asm_neon.c" compared to ["foo/main.c", "foo/all_utility.c"], ["foo/asm_arm.c"] + -> ["foo/asm_arm.c"] (match of `foo/asm_`) + """ + length_of_longest_match = 0 + source_assignment_location_of_longest_match = None + statistic_number_refinements = 0 + statistic_length_logic = 0 + + for key, list_of_normalized_filenames in source_assignments.items(): + for f in list_of_normalized_filenames: + if filename_normalized == f: + # Do not cheat by matching the prefix of the exact file + continue + + prefix = os.path.commonprefix([filename_normalized, f]) + if len(prefix) > length_of_longest_match: + statistic_number_refinements += 1 + length_of_longest_match = len(prefix) + source_assignment_location_of_longest_match = key + elif len(prefix) == length_of_longest_match and len( + source_assignments[key] + ) > len(source_assignments[source_assignment_location_of_longest_match]): + statistic_number_refinements += 1 + statistic_length_logic += 1 + length_of_longest_match = len(prefix) + source_assignment_location_of_longest_match = key + return ( + source_assignment_location_of_longest_match, + (statistic_number_refinements, statistic_length_logic), + ) + + +def edit_moz_build_file_to_add_file( + normalized_mozbuild_filename, + unnormalized_filename_to_add, + unnormalized_list_of_files, +): + """ + This function edits the moz.build file in-place + + I had _really_ hoped to replace this whole damn thing with something that adds a + node to the AST, dumps the AST out, and then runs black on the file but there are + some issues: + - third party moz.build files (or maybe all moz.build files) aren't always run through black + - dumping the ast out losing comments + + """ + + # Make sure that we only write in forward slashes + if "\\" in unnormalized_filename_to_add: + unnormalized_filename_to_add = unnormalized_filename_to_add.replace("\\", "/") + + # add the file into the list, and then sort it in the same way the moz.build validator + # expects + unnormalized_list_of_files.append(unnormalized_filename_to_add) + unnormalized_list_of_files = alphabetical_sorted(unnormalized_list_of_files) + + # we're going to add our file by doing a find/replace of an adjacent file in the list + indx_of_addition = unnormalized_list_of_files.index(unnormalized_filename_to_add) + indx_of_addition + if indx_of_addition == 0: + target_indx = 1 + replace_before = False + else: + target_indx = indx_of_addition - 1 + replace_before = True + + find_str = unnormalized_list_of_files[target_indx] + + # We will only perform the first replacement. This is because sometimes there's moz.build + # code like: + # SOURCES += ['file.cpp'] + # SOURCES['file.cpp'].flags += ['-Winline'] + # If we replaced every time we found the target, we would be inserting into that second + # line. + did_replace = False + + with open(normalized_mozbuild_filename, mode="r") as file: + with open(normalized_mozbuild_filename + ".new", mode="wb") as output: + for line in file: + if not did_replace and find_str in line: + did_replace = True + + # Okay, we found the line we need to edit, now we need to be ugly about it + # Grab the type of quote used in this moz.build file: single or double + quote_type = line[line.index(find_str) - 1] + + if "[" not in line: + # We'll want to put our new file onto its own line + newline_to_add = "\n" + # And copy the indentation of the line we're adding adjacent to + indent_value = line[0 : line.index(quote_type)] + else: + # This is frustrating, we have the start of the array here. We aren't + # going to be able to indent things onto a newline properly. We're just + # going to have to stick it in on the same line. + newline_to_add = "" + indent_value = "" + + find_str = "%s%s%s" % (quote_type, find_str, quote_type) + if replace_before: + replacement_tuple = ( + find_str, + newline_to_add, + indent_value, + quote_type, + unnormalized_filename_to_add, + quote_type, + ) + replace_str = "%s,%s%s%s%s%s" % replacement_tuple + else: + replacement_tuple = ( + quote_type, + unnormalized_filename_to_add, + quote_type, + newline_to_add, + indent_value, + find_str, + ) + replace_str = "%s%s%s,%s%s%s" % replacement_tuple + + line = line.replace(find_str, replace_str) + + output.write((line.rstrip() + "\n").encode("utf-8")) + + shutil.move(normalized_mozbuild_filename + ".new", normalized_mozbuild_filename) + + +def edit_moz_build_file_to_remove_file( + normalized_mozbuild_filename, unnormalized_filename_to_remove +): + """ + This function edits the moz.build file in-place + """ + + simple_file_line = re.compile( + "^\s*['\"]" + unnormalized_filename_to_remove + "['\"],*$" + ) + did_replace = False + + with open(normalized_mozbuild_filename, mode="r") as file: + with open(normalized_mozbuild_filename + ".new", mode="wb") as output: + for line in file: + if not did_replace and unnormalized_filename_to_remove in line: + did_replace = True + + # If the line consists of just a single source file on it, then we're in the + # clear - we can just skip this line. + if simple_file_line.match(line): + # Do not output anything, just keep going. + continue + + # Okay, so the line is a little more complicated. + quote_type = line[line.index(unnormalized_filename_to_remove) - 1] + + if "[" in line or "]" in line: + find_str = "%s%s%s,*" % ( + quote_type, + unnormalized_filename_to_remove, + quote_type, + ) + line = re.sub(find_str, "", line) + else: + raise Exception( + "Got an unusual type of line we're trying to remove a file from:", + line, + ) + + output.write((line.rstrip() + "\n").encode("utf-8")) + + shutil.move(normalized_mozbuild_filename + ".new", normalized_mozbuild_filename) + + +def validate_directory_parameters(moz_yaml_dir, vendoring_dir): + # Validate the parameters + assert (moz_yaml_dir, vendoring_dir) == (None, None) or ( + moz_yaml_dir and vendoring_dir + ), "If either moz_yaml_dir or vendoring_dir are specified, they both must be" + + if moz_yaml_dir is not None and vendoring_dir is not None: + # Ensure they are provided with trailing slashes + moz_yaml_dir += "/" if moz_yaml_dir[-1] != "/" else "" + vendoring_dir += "/" if vendoring_dir[-1] != "/" else "" + + return (moz_yaml_dir, vendoring_dir) + + +HAS_ABSOLUTE = 1 +HAS_TRAVERSE_CHILD = 2 +HAS_RELATIVE_CHILD = 2 # behaves the same as above + + +def get_file_reference_modes(source_assignments): + """ + Given a set of source assignments, this function traverses through the + files references in those assignments to see if the files are referenced + using absolute paths (relative to gecko root) or relative paths. + + It will return all the modes that are seen. + """ + modes = set() + + for key, list_of_normalized_filenames in source_assignments.items(): + if not list_of_normalized_filenames: + continue + for file in list_of_normalized_filenames: + if file[0] == "/": + modes.add(HAS_ABSOLUTE) + elif file[0:2] == "../": + modes.add(HAS_TRAVERSE_CHILD) + else: + modes.add(HAS_RELATIVE_CHILD) + return modes + + +def renormalize_filename( + mode, + moz_yaml_dir, + vendoring_dir, + normalized_mozbuild_filename, + normalized_filename_to_act_on, +): + """ + Edit the normalized_filename_to_act_on to either + - Make it an absolute path from gecko root (if we're in that mode) + - Get a relative path from the vendoring directory to the yaml directory where the + moz.build file is (If they are in separate directories) + """ + if mode == HAS_ABSOLUTE: + # If the moz.build file uses absolute paths from the gecko root, this is easy, + # all we need to do is prepend a '/' to indicate that + normalized_filename_to_act_on = "/" + normalized_filename_to_act_on + elif moz_yaml_dir and vendoring_dir: + # To re-normalize it in this case, we: + # (a) get the path from gecko_root to the moz.build file we are considering + # (b) compute a relative path from that directory to the file we want + # (c) because (b) started at the moz.build file's directory, it is not + # normalized to the gecko_root. Therefore we need to normalize it by + # prepending (a) + a = os.path.dirname(normalized_mozbuild_filename).replace(os.path.sep, "/") + b = os.path.relpath(normalized_filename_to_act_on, start=a).replace( + os.path.sep, "/" + ) + c = os.path.join(a, b).replace(os.path.sep, "/") + normalized_filename_to_act_on = c + + return normalized_filename_to_act_on + + +######################################################### +# PUBLIC API +######################################################### + + +class MozBuildRewriteException(Exception): + pass + + +def remove_file_from_moz_build_file( + normalized_filename_to_remove, moz_yaml_dir=None, vendoring_dir=None +): + """ + Given a filename, relative to the gecko root (aka normalized), we look for the nearest + moz.build file, look in that file for the file, and then edit that moz.build file in-place. + """ + moz_yaml_dir, vendoring_dir = validate_directory_parameters( + moz_yaml_dir, vendoring_dir + ) + + all_possible_normalized_mozbuild_filenames = get_mozbuild_file_search_order( + normalized_filename_to_remove, moz_yaml_dir, vendoring_dir, None + ) + + # normalized_filename_to_remove is the path from gecko_root to the file. However, if we vendor + # separate from moz.yaml; then 'normalization' gets more complicated as explained above. + # We will need to re-normalize the filename for each moz.build file we want to test, so we + # save the original normalized filename for this purpose + original_normalized_filename_to_remove = normalized_filename_to_remove + + # These are the two header file types specified in vendor_manifest.py > source_suffixes + if normalized_filename_to_remove.endswith( + ".h" + ) or normalized_filename_to_remove.endswith(".hpp"): + assignment_type = "header-files" + else: + assignment_type = "source-files" + + for normalized_mozbuild_filename in all_possible_normalized_mozbuild_filenames: + source_assignments, root, code = mozbuild_file_to_source_assignments( + normalized_mozbuild_filename, assignment_type + ) + + modes = get_file_reference_modes(source_assignments) + + for mode in modes: + normalized_filename_to_remove = renormalize_filename( + mode, + moz_yaml_dir, + vendoring_dir, + normalized_mozbuild_filename, + normalized_filename_to_remove, + ) + + for key in source_assignments: + normalized_source_filename_list = source_assignments[key] + if normalized_filename_to_remove in normalized_source_filename_list: + unnormalized_filename_to_remove = unnormalize_filename( + normalized_mozbuild_filename, normalized_filename_to_remove + ) + edit_moz_build_file_to_remove_file( + normalized_mozbuild_filename, unnormalized_filename_to_remove + ) + return + + normalized_filename_to_remove = original_normalized_filename_to_remove + raise MozBuildRewriteException("Could not remove " + normalized_filename_to_remove) + + +def add_file_to_moz_build_file( + normalized_filename_to_add, moz_yaml_dir=None, vendoring_dir=None +): + """ + This is the overall function. Given a filename, relative to the gecko root (aka normalized), + we look for a moz.build file to add it to, look for the place in the moz.build file to add it, + and then edit that moz.build file in-place. + + It accepted two optional parameters. If one is specified they both must be. If a library is + vendored in a separate place from the moz.yaml file, these parameters specify those two + directories. + """ + moz_yaml_dir, vendoring_dir = validate_directory_parameters( + moz_yaml_dir, vendoring_dir + ) + + all_possible_normalized_mozbuild_filenames = get_mozbuild_file_search_order( + normalized_filename_to_add, moz_yaml_dir, vendoring_dir, None + ) + + # normalized_filename_to_add is the path from gecko_root to the file. However, if we vendor + # separate from moz.yaml; then 'normalization' gets more complicated as explained above. + # We will need to re-normalize the filename for each moz.build file we want to test, so we + # save the original normalized filename for this purpose + original_normalized_filename_to_add = normalized_filename_to_add + + if normalized_filename_to_add.endswith(".h") or normalized_filename_to_add.endswith( + ".hpp" + ): + assignment_type = "header-files" + else: + assignment_type = "source-files" + + for normalized_mozbuild_filename in all_possible_normalized_mozbuild_filenames: + source_assignments, root, code = mozbuild_file_to_source_assignments( + normalized_mozbuild_filename, assignment_type + ) + + modes = get_file_reference_modes(source_assignments) + + for mode in modes: + normalized_filename_to_add = renormalize_filename( + mode, + moz_yaml_dir, + vendoring_dir, + normalized_mozbuild_filename, + normalized_filename_to_add, + ) + + possible_assignments = find_all_posible_assignments_from_filename( + source_assignments, normalized_filename_to_add + ) + + if len(possible_assignments) == 0: + normalized_filename_to_add = original_normalized_filename_to_add + continue + + assert ( + len(possible_assignments) > 0 + ), "Could not find a single possible source assignment" + if len(possible_assignments) > 1: + best_guess, _ = guess_best_assignment( + possible_assignments, normalized_filename_to_add + ) + chosen_source_assignment_location = best_guess + else: + chosen_source_assignment_location = list(possible_assignments.keys())[0] + + guessed_list_containing_normalized_filenames = possible_assignments[ + chosen_source_assignment_location + ] + + # unnormalize filenames so we can edit the moz.build file. They rarely use full paths. + unnormalized_filename_to_add = unnormalize_filename( + normalized_mozbuild_filename, normalized_filename_to_add + ) + unnormalized_list_of_files = [ + unnormalize_filename(normalized_mozbuild_filename, f) + for f in guessed_list_containing_normalized_filenames + ] + + edit_moz_build_file_to_add_file( + normalized_mozbuild_filename, + unnormalized_filename_to_add, + unnormalized_list_of_files, + ) + return + + raise MozBuildRewriteException( + "Could not find a single moz.build file to add " + normalized_filename_to_add + ) + + +######################################################### +# TESTING CODE +######################################################### + + +def get_all_target_filenames_normalized(all_mozbuild_filenames_normalized): + """ + Given a list of moz.build files, returns all the files listed in all the souce assignments + in the file. + + This function is only used for debug/testing purposes - there is no reason to call this + as part of 'the algorithm' + """ + all_target_filenames_normalized = [] + for normalized_mozbuild_filename in all_mozbuild_filenames_normalized: + source_assignments, root, code = mozbuild_file_to_source_assignments( + normalized_mozbuild_filename + ) + for key in source_assignments: + list_of_normalized_filenames = source_assignments[key] + all_target_filenames_normalized.extend(list_of_normalized_filenames) + + return all_target_filenames_normalized + + +def try_to_match_target_file( + all_mozbuild_filenames_normalized, target_filename_normalized +): + """ + Runs 'the algorithm' on a target file, and returns if the algorithm was successful + + all_mozbuild_filenames_normalized: the list of all third-party moz.build files + target_filename_normalized - the target filename, normalized to the gecko root + """ + + # We do not update the statistics for failed matches, so save a copy + global statistics + backup_statistics = copy.deepcopy(statistics) + + if "" == target_filename_normalized: + raise Exception("Received an empty target_filename_normalized") + + normalized_mozbuild_filename = get_closest_mozbuild_file( + target_filename_normalized, None, None, all_mozbuild_filenames_normalized + ) + if not normalized_mozbuild_filename: + return (False, "No moz.build file found") + + source_assignments, root, code = mozbuild_file_to_source_assignments( + normalized_mozbuild_filename + ) + possible_assignments = find_all_posible_assignments_from_filename( + source_assignments, target_filename_normalized + ) + + if len(possible_assignments) == 0: + raise Exception("No possible assignments were found") + elif len(possible_assignments) > 1: + ( + best_guess, + (statistic_number_refinements, statistic_length_logic), + ) = guess_best_assignment(possible_assignments, target_filename_normalized) + chosen_source_assignment_location = best_guess + + statistics["needed_to_guess"] += 1 + + if len(possible_assignments) not in statistics["guess_candidates"]: + statistics["guess_candidates"][len(possible_assignments)] = 0 + statistics["guess_candidates"][len(possible_assignments)] += 1 + + if statistic_number_refinements not in statistics["number_refinements"]: + statistics["number_refinements"][statistic_number_refinements] = 0 + statistics["number_refinements"][statistic_number_refinements] += 1 + + if statistic_length_logic not in statistics["length_logic"]: + statistics["length_logic"][statistic_length_logic] = 0 + statistics["length_logic"][statistic_length_logic] += 1 + + else: + chosen_source_assignment_location = list(possible_assignments.keys())[0] + + guessed_list_containing_normalized_filenames = possible_assignments[ + chosen_source_assignment_location + ] + + if target_filename_normalized in guessed_list_containing_normalized_filenames: + return (True, None) + + # Restore the copy of the statistics so we don't alter it for failed matches + statistics = backup_statistics + return (False, chosen_source_assignment_location) + + +def get_gecko_root(): + """ + Using __file__ as a base, find the gecko root + """ + gecko_root = None + directory_to_check = os.path.dirname(os.path.abspath(__file__)) + while not os.path.isfile(os.path.join(directory_to_check, ".arcconfig")): + directory_to_check = os.path.dirname(directory_to_check) + if directory_to_check == "/": + print("Could not find gecko root") + sys.exit(1) + + gecko_root = directory_to_check + return gecko_root + + +def get_all_mozbuild_filenames(gecko_root): + """ + Find all the third party moz.build files in the gecko repo + """ + third_party_paths = open( + os.path.join(gecko_root, "tools", "rewriting", "ThirdPartyPaths.txt") + ).readlines() + all_mozbuild_filenames_normalized = [] + for path in third_party_paths: + # We need shell=True because some paths are specified as globs + # We need an exception handler because sometimes the directory doesn't exist and find barfs + try: + output = subprocess.check_output( + "find %s -name moz.build" % os.path.join(gecko_root, path.strip()), + shell=True, + ).decode("utf-8") + for f in output.split("\n"): + f = f.replace("//", "/").strip().replace(gecko_root, "")[1:] + if f: + all_mozbuild_filenames_normalized.append(f) + except Exception: + pass + + return all_mozbuild_filenames_normalized + + +def test_all_third_party_files(gecko_root, all_mozbuild_filenames_normalized): + """ + Run the algorithm on every source file in a third party moz.build file and output the results + """ + all_mozbuild_filenames_normalized = [ + f for f in all_mozbuild_filenames_normalized if "webrtc" not in f + ] + all_target_filenames_normalized = get_all_target_filenames_normalized( + all_mozbuild_filenames_normalized + ) + + total_attempted = 0 + failed_matched = [] + successfully_matched = 0 + + print("Going to try to match %i files..." % len(all_target_filenames_normalized)) + for target_filename_normalized in all_target_filenames_normalized: + result, wrong_guess = try_to_match_target_file( + all_mozbuild_filenames_normalized, target_filename_normalized + ) + + total_attempted += 1 + if result: + successfully_matched += 1 + else: + failed_matched.append((target_filename_normalized, wrong_guess)) + if total_attempted % 100 == 0: + print("Progress:", total_attempted) + + print( + "Successfully Matched %i of %i files" % (successfully_matched, total_attempted) + ) + if failed_matched: + print("Failed files:") + for f in failed_matched: + print("\t", f[0], f[1]) + print("Statistics:") + pprint(statistics) + + +if __name__ == "__main__": + gecko_root = get_gecko_root() + os.chdir(gecko_root) + + add_file_to_moz_build_file( + "third_party/jpeg-xl/lib/include/jxl/resizable_parallel_runner.h", + "media/libjxl", + "third_party/jpeg-xl", + ) + + # all_mozbuild_filenames_normalized = get_all_mozbuild_filenames(gecko_root) + # test_all_third_party_files(gecko_root, all_mozbuild_filenames_normalized) diff --git a/python/mozbuild/mozbuild/vendor/test_vendor_changes.sh b/python/mozbuild/mozbuild/vendor/test_vendor_changes.sh new file mode 100755 index 0000000000..3d0e390f7f --- /dev/null +++ b/python/mozbuild/mozbuild/vendor/test_vendor_changes.sh @@ -0,0 +1,65 @@ +#!/bin/bash + +if [[ ! -f "CLOBBER" ]]; then + echo "Script should be run from mozilla-central root" + exit 1 +fi + +echo "THIS SCRIPT WILL REVERT AND PURGE UNCOMMIT LOCAL CHANGES" +echo "TYPE ok TO CONTINUE" +read CONFIRMATION +if [[ $CONFIRMATION != "ok" ]]; then + echo "Did not get 'ok', exiting" + exit 0 +fi + +ALL_MOZ_YAML_FILES=$(find . -name moz.yaml) + +for f in $ALL_MOZ_YAML_FILES; do + IFS='' read -r -d '' INPUT <<"EOF" +import sys +import yaml +enabled = False +with open(sys.argv[1]) as yaml_in: + o = yaml.safe_load(yaml_in) + if "updatebot" in o: + if 'tasks' in o["updatebot"]: + for t in o["updatebot"]["tasks"]: + if t["type"] == "vendoring": + if t.get("enabled", True) and t.get("platform", "Linux").lower() == "linux": + enabled = True +if enabled: + print(sys.argv[1]) +EOF + + FILE=$(python3 -c "$INPUT" $f) + + if [[ ! -z $FILE ]]; then + UPDATEBOT_YAML_FILES+=("$FILE") + fi +done + + +for FILE in "${UPDATEBOT_YAML_FILES[@]}"; do + REVISION=$(yq eval ".origin.revision" $FILE) + HAS_PATCHES=$(yq eval ".vendoring.patches | (. != null)" $FILE) + + echo "$FILE - $REVISION" + if [[ $HAS_PATCHES == "false" ]]; then + ./mach vendor $FILE --force --revision $REVISION + if [[ $? == 1 ]]; then + exit 1 + fi + else + ./mach vendor $FILE --force --revision $REVISION --patch-mode=none + if [[ $? == 1 ]]; then + exit 1 + fi + ./mach vendor $FILE --force --revision $REVISION --patch-mode=only --ignore-modified + if [[ $? == 1 ]]; then + exit 1 + fi + fi + hg revert . + hg purge +done diff --git a/python/mozbuild/mozbuild/vendor/vendor_manifest.py b/python/mozbuild/mozbuild/vendor/vendor_manifest.py new file mode 100644 index 0000000000..65ee161348 --- /dev/null +++ b/python/mozbuild/mozbuild/vendor/vendor_manifest.py @@ -0,0 +1,872 @@ +# 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/. + +import functools +import glob +import logging +import os +import re +import shutil +import stat +import sys +import tarfile +import tempfile +from collections import defaultdict + +import mozfile +import mozpack.path as mozpath +import requests + +from mozbuild.base import MozbuildObject +from mozbuild.vendor.rewrite_mozbuild import ( + MozBuildRewriteException, + add_file_to_moz_build_file, + remove_file_from_moz_build_file, +) + +DEFAULT_EXCLUDE_FILES = [".git*", ".git*/**"] +DEFAULT_KEEP_FILES = ["**/moz.build", "**/moz.yaml"] +DEFAULT_INCLUDE_FILES = [] + + +def iglob_hidden(*args, **kwargs): + # glob._ishidden exists from 3.5 up to 3.12 (and beyond?) + old_ishidden = glob._ishidden + glob._ishidden = lambda x: False + try: + yield from glob.iglob(*args, **kwargs) + finally: + glob._ishidden = old_ishidden + + +def throwe(): + raise Exception + + +def _replace_in_file(file, pattern, replacement, regex=False): + def replacer(matchobj: re.Match): + if matchobj.group(0) == replacement: + print(f"WARNING: {action} replaced '{matchobj.group(0)}' with same.") + return replacement + + with open(file) as f: + contents = f.read() + + action = "replace-in-file-regex" + if not regex: + pattern = re.escape(pattern) + action = "replace-in-file" + + newcontents, count = re.subn(pattern, replacer, contents) + if count < 1: + raise Exception( + f"{action} could not find '{pattern}' in {file} to replace with '{replacement}'." + ) + + with open(file, "w") as f: + f.write(newcontents) + + +def list_of_paths_to_readable_string(paths): + # From https://stackoverflow.com/a/41578071 + dic = defaultdict(list) + for item in paths: + if os.path.isdir(item): # To check path is a directory + _ = dic[item] # will set default value as empty list + else: + path, file = os.path.split(item) + dic[path].append(file) + + final_string = "[" + for key, val in dic.items(): + if len(val) == 0: + final_string += key + ", " + elif len(val) < 3: + final_string += ", ".join([os.path.join(key, v) for v in val]) + ", " + elif len(val) < 10: + final_string += "%s items in %s: %s and %s, " % ( + len(val), + key, + ", ".join(val[0:-1]), + val[-1], + ) + else: + final_string += "%s (omitted) items in %s, " % (len(val), key) + + if final_string[-2:] == ", ": + final_string = final_string[:-2] + + return final_string + "]" + + +class VendorManifest(MozbuildObject): + def should_perform_step(self, step): + return step not in self.manifest["vendoring"].get("skip-vendoring-steps", []) + + def vendor( + self, + command_context, + yaml_file, + manifest, + revision, + ignore_modified, + check_for_update, + force, + add_to_exports, + patch_mode, + ): + self.manifest = manifest + self.yaml_file = yaml_file + self._extract_directory = throwe + self.logInfo = functools.partial(self.log, logging.INFO, "vendor") + self.patch_mode = patch_mode + if "vendor-directory" not in self.manifest["vendoring"]: + self.manifest["vendoring"]["vendor-directory"] = os.path.dirname( + self.yaml_file + ) + + # ========================================================== + # If we're only patching; do that + if "patches" in self.manifest["vendoring"] and patch_mode == "only": + self.import_local_patches( + self.manifest["vendoring"]["patches"], + os.path.dirname(self.yaml_file), + self.manifest["vendoring"]["vendor-directory"], + ) + return + + # ========================================================== + self.source_host = self.get_source_host() + + ref_type = self.manifest["vendoring"].get("tracking", "commit") + flavor = self.manifest["vendoring"].get("flavor", "regular") + # Individiual files are special + + if revision == "tip": + # This case allows us to force-update a tag-tracking library to master + new_revision, timestamp = self.source_host.upstream_commit("HEAD") + elif ref_type == "tag": + new_revision, timestamp = self.source_host.upstream_tag(revision) + else: + new_revision, timestamp = self.source_host.upstream_commit(revision) + + self.logInfo( + {"ref_type": ref_type, "ref": new_revision, "timestamp": timestamp}, + "Latest {ref_type} is {ref} from {timestamp}", + ) + + # ========================================================== + if not force and self.manifest["origin"]["revision"] == new_revision: + # We're up to date, don't do anything + self.logInfo({}, "Latest upstream matches in-tree.") + return + elif flavor != "individual-file" and check_for_update: + # Only print the new revision to stdout + print("%s %s" % (new_revision, timestamp)) + return + + # ========================================================== + if flavor == "regular": + self.process_regular( + new_revision, timestamp, ignore_modified, add_to_exports + ) + elif flavor == "individual-files": + self.process_individual( + new_revision, timestamp, ignore_modified, add_to_exports + ) + elif flavor == "rust": + self.process_rust( + command_context, + self.manifest["origin"]["revision"], + new_revision, + timestamp, + ignore_modified, + ) + else: + raise Exception("Unknown flavor") + + def process_rust( + self, command_context, old_revision, new_revision, timestamp, ignore_modified + ): + # First update the Cargo.toml + cargo_file = os.path.join(os.path.dirname(self.yaml_file), "Cargo.toml") + try: + _replace_in_file(cargo_file, old_revision, new_revision) + except Exception: + # If we can't find it the first time, try again with a short hash + _replace_in_file(cargo_file, old_revision[:8], new_revision) + + # Then call ./mach vendor rust + from mozbuild.vendor.vendor_rust import VendorRust + + vendor_command = command_context._spawn(VendorRust) + vendor_command.vendor(ignore_modified=True) + + self.update_yaml(new_revision, timestamp) + + def fetch_individual(self, new_revision): + # This design is used because there is no github API to query + # for the last commit that modified a file; nor a way to get file + # blame. So really all we can do is just download and replace the + # files and see if they changed... + + def download_and_write_file(url, destination): + self.logInfo( + {"local_file": destination, "url": url}, + "Downloading {local_file} from {url}...", + ) + + with mozfile.NamedTemporaryFile() as tmpfile: + try: + req = requests.get(url, stream=True) + for data in req.iter_content(4096): + tmpfile.write(data) + tmpfile.seek(0) + + shutil.copy2(tmpfile.name, destination) + except Exception as e: + raise (e) + + # Only one of these loops will have content, so just do them both + for f in self.manifest["vendoring"].get("individual-files", []): + url = self.source_host.upstream_path_to_file(new_revision, f["upstream"]) + destination = self.get_full_path(f["destination"]) + download_and_write_file(url, destination) + + for f in self.manifest["vendoring"].get("individual-files-list", []): + url = self.source_host.upstream_path_to_file( + new_revision, + self.manifest["vendoring"]["individual-files-default-upstream"] + f, + ) + destination = self.get_full_path( + self.manifest["vendoring"]["individual-files-default-destination"] + f + ) + download_and_write_file(url, destination) + + def process_regular_or_individual( + self, is_individual, new_revision, timestamp, ignore_modified, add_to_exports + ): + if self.should_perform_step("fetch"): + if is_individual: + self.fetch_individual(new_revision) + else: + self.fetch_and_unpack(new_revision) + else: + self.logInfo({}, "Skipping fetching upstream source.") + + self.logInfo({}, "Checking for update actions") + self.update_files(new_revision) + + if self.patch_mode == "check": + self.import_local_patches( + self.manifest["vendoring"].get("patches", []), + os.path.dirname(self.yaml_file), + self.manifest["vendoring"]["vendor-directory"], + ) + elif "patches" in self.manifest["vendoring"]: + # Remind the user + self.log( + logging.CRITICAL, + "vendor", + {}, + "Patches present in manifest!!! Please run " + "'./mach vendor --patch-mode only' after commiting changes.", + ) + + if self.should_perform_step("hg-add"): + self.logInfo({}, "Registering changes with version control.") + self.repository.add_remove_files( + self.manifest["vendoring"]["vendor-directory"], + os.path.dirname(self.yaml_file), + ) + else: + self.logInfo({}, "Skipping registering changes.") + + if self.should_perform_step("spurious-check"): + self.logInfo({}, "Checking for a spurious update.") + self.spurious_check(new_revision, ignore_modified) + else: + self.logInfo({}, "Skipping the spurious update check.") + + if self.should_perform_step("update-moz-yaml"): + self.logInfo({}, "Updating moz.yaml.") + self.update_yaml(new_revision, timestamp) + else: + self.logInfo({}, "Skipping updating the moz.yaml file.") + + # individual flavor does not need this step, but performing it should + # always be a no-op + if self.should_perform_step("update-moz-build"): + self.logInfo({}, "Updating moz.build files") + self.update_moz_build( + self.manifest["vendoring"]["vendor-directory"], + os.path.dirname(self.yaml_file), + add_to_exports, + ) + else: + self.logInfo({}, "Skipping update of moz.build files") + + self.logInfo({"rev": new_revision}, "Updated to '{rev}'.") + + def process_regular(self, new_revision, timestamp, ignore_modified, add_to_exports): + is_individual = False + self.process_regular_or_individual( + is_individual, new_revision, timestamp, ignore_modified, add_to_exports + ) + + def process_individual( + self, new_revision, timestamp, ignore_modified, add_to_exports + ): + is_individual = True + self.process_regular_or_individual( + is_individual, new_revision, timestamp, ignore_modified, add_to_exports + ) + + def get_source_host(self): + if self.manifest["vendoring"]["source-hosting"] == "gitlab": + from mozbuild.vendor.host_gitlab import GitLabHost + + return GitLabHost(self.manifest) + elif self.manifest["vendoring"]["source-hosting"] == "github": + from mozbuild.vendor.host_github import GitHubHost + + return GitHubHost(self.manifest) + elif self.manifest["vendoring"]["source-hosting"] == "git": + from mozbuild.vendor.host_git import GitHost + + return GitHost(self.manifest) + elif self.manifest["vendoring"]["source-hosting"] == "googlesource": + from mozbuild.vendor.host_googlesource import GoogleSourceHost + + return GoogleSourceHost(self.manifest) + elif self.manifest["vendoring"]["source-hosting"] == "angle": + from mozbuild.vendor.host_angle import AngleHost + + return AngleHost(self.manifest) + elif self.manifest["vendoring"]["source-hosting"] == "codeberg": + from mozbuild.vendor.host_codeberg import CodebergHost + + return CodebergHost(self.manifest) + else: + raise Exception( + "Unknown source host: " + self.manifest["vendoring"]["source-hosting"] + ) + + def get_full_path(self, path, support_cwd=False): + if support_cwd and path[0:5] == "{cwd}": + path = path.replace("{cwd}", ".") + elif "{tmpextractdir}" in path: + # _extract_directory() will throw an exception if it is invalid to use it + path = path.replace("{tmpextractdir}", self._extract_directory()) + elif "{yaml_dir}" in path: + path = path.replace("{yaml_dir}", os.path.dirname(self.yaml_file)) + elif "{vendor_dir}" in path: + path = path.replace( + "{vendor_dir}", self.manifest["vendoring"]["vendor-directory"] + ) + else: + path = mozpath.join(self.manifest["vendoring"]["vendor-directory"], path) + return os.path.abspath(path) + + def convert_patterns_to_paths(self, directory, patterns): + # glob.iglob uses shell-style wildcards for path name completion. + # "recursive=True" enables the double asterisk "**" wildcard which matches + # for nested directories as well as the directory we're searching in. + paths = [] + for pattern in patterns: + pattern_full_path = mozpath.join(directory, pattern) + # If pattern is a directory recursively add contents of directory + if os.path.isdir(pattern_full_path): + # Append double asterisk to the end to make glob.iglob recursively match + # contents of directory + paths.extend( + iglob_hidden(mozpath.join(pattern_full_path, "**"), recursive=True) + ) + # Otherwise pattern is a file or wildcard expression so add it without altering it + else: + paths.extend(iglob_hidden(pattern_full_path, recursive=True)) + # Remove folder names from list of paths in order to avoid prematurely + # truncating directories elsewhere + # Sort the final list to ensure we preserve 01_, 02_ ordering for e.g. *.patch globs + final_paths = sorted( + [mozpath.normsep(path) for path in paths if not os.path.isdir(path)] + ) + return final_paths + + def fetch_and_unpack(self, revision): + """Fetch and unpack upstream source""" + + def validate_tar_member(member, path): + def is_within_directory(directory, target): + real_directory = os.path.realpath(directory) + real_target = os.path.realpath(target) + prefix = os.path.commonprefix([real_directory, real_target]) + return prefix == real_directory + + member_path = os.path.join(path, member.name) + if not is_within_directory(path, member_path): + raise Exception("Attempted path traversal in tar file: " + member.name) + if member.issym(): + link_path = os.path.join(os.path.dirname(member_path), member.linkname) + if not is_within_directory(path, link_path): + raise Exception( + "Attempted link path traversal in tar file: " + member.name + ) + if member.mode & (stat.S_ISUID | stat.S_ISGID): + raise Exception( + "Attempted setuid or setgid in tar file: " + member.name + ) + + def safe_extract(tar, path=".", *, numeric_owner=False): + def _files(tar, path): + for member in tar: + validate_tar_member(member, path) + yield member + + tar.extractall(path, members=_files(tar, path), numeric_owner=numeric_owner) + + release_artifact = self.manifest["vendoring"].get("release-artifact", False) + + if release_artifact: + url = self.source_host.upstream_release_artifact(revision, release_artifact) + else: + url = self.source_host.upstream_snapshot(revision) + + self.logInfo({"url": url}, "Fetching code archive from {url}") + + with mozfile.NamedTemporaryFile() as tmptarfile: + tmpextractdir = tempfile.TemporaryDirectory() + try: + if url.startswith("file://"): + with open(url[len("file://") :], "rb") as tarinput: + tmptarfile.write(tarinput.read()) + else: + req = requests.get(url, stream=True) + for data in req.iter_content(4096): + tmptarfile.write(data) + tmptarfile.seek(0) + + vendor_dir = mozpath.normsep( + self.manifest["vendoring"]["vendor-directory"] + ) + if self.should_perform_step("keep"): + self.logInfo({}, "Retaining wanted in-tree files.") + to_keep = self.convert_patterns_to_paths( + vendor_dir, + self.manifest["vendoring"].get("keep", []) + + DEFAULT_KEEP_FILES + + self.manifest["vendoring"].get("patches", []), + ) + else: + self.logInfo({}, "Skipping retention of in-tree files.") + to_keep = [] + + self.logInfo({"vd": vendor_dir}, "Cleaning {vd} to import changes.") + # We use double asterisk wildcard here to get complete list of recursive contents + for file in self.convert_patterns_to_paths(vendor_dir, ["**"]): + file = mozpath.normsep(file) + if file not in to_keep: + mozfile.remove(file) + + self.logInfo({"vd": vendor_dir}, "Unpacking upstream files for {vd}.") + with tarfile.open(tmptarfile.name) as tar: + safe_extract(tar, tmpextractdir.name) + + def get_first_dir(p): + halves = os.path.split(p) + return get_first_dir(halves[0]) if halves[0] else halves[1] + + one_prefix = get_first_dir(tar.getnames()[0]) + has_prefix = all( + map(lambda name: name.startswith(one_prefix), tar.getnames()) + ) + + # GitLab puts everything down a directory; move it up. + if has_prefix: + tardir = mozpath.join(tmpextractdir.name, one_prefix) + mozfile.copy_contents( + tardir, tmpextractdir.name, ignore_dangling_symlinks=True + ) + mozfile.remove(tardir) + + if self.should_perform_step("include"): + self.logInfo({}, "Retaining wanted files from upstream changes.") + to_include = self.convert_patterns_to_paths( + tmpextractdir.name, + self.manifest["vendoring"].get("include", []) + + DEFAULT_INCLUDE_FILES, + ) + else: + self.logInfo({}, "Skipping retention of included files.") + to_include = [] + + if self.should_perform_step("exclude"): + self.logInfo({}, "Removing excluded files from upstream changes.") + to_exclude = self.convert_patterns_to_paths( + tmpextractdir.name, + self.manifest["vendoring"].get("exclude", []) + + DEFAULT_EXCLUDE_FILES, + ) + else: + self.logInfo({}, "Skipping removing excluded files.") + to_exclude = [] + + # If we have files that match both patterns, figure out the _longer_ + # pattern that it matches. (We hope this will be the more precise/stricter one) + conflicts = list(set(to_exclude).intersection(set(to_include))) + if conflicts: + remove_from_include = [] + remove_from_exclude = [] + + for c in conflicts: + longest_exclude = "" + longest_include = "" + + for pattern in ( + self.manifest["vendoring"].get("exclude", []) + + DEFAULT_EXCLUDE_FILES + ): + if c in self.convert_patterns_to_paths( + tmpextractdir.name, + [pattern], + ): + if len(pattern) > len(longest_exclude): + longest_exclude = pattern + + for pattern in ( + self.manifest["vendoring"].get("include", []) + + DEFAULT_INCLUDE_FILES + ): + if c in self.convert_patterns_to_paths( + tmpextractdir.name, + [pattern], + ): + if len(pattern) > len(longest_include): + longest_include = pattern + + if len(longest_include) == len(longest_exclude): + # If it's a tie, give 'include' precedence' + remove_from_exclude.append(c) + elif len(longest_include) == 0 or len(longest_exclude) == 0: + raise Exception("Pattern didn't match both.") + elif len(longest_include) > len(longest_exclude): + remove_from_exclude.append(c) + else: + remove_from_include.append(c) + + to_exclude = list(set(to_exclude) - set(remove_from_exclude)) + to_include = list(set(to_include) - set(remove_from_include)) + + if to_exclude: + self.logInfo( + {"files": list_of_paths_to_readable_string(to_exclude)}, + "Removing: {files}", + ) + for exclusion in to_exclude: + mozfile.remove(exclusion) + + # Clear out empty directories + # removeEmpty() won't remove directories containing only empty directories + # so just keep callign it as long as it's doing something + def removeEmpty(tmpextractdir): + removed = False + folders = list(os.walk(tmpextractdir))[1:] + for folder in folders: + if not folder[2]: + try: + os.rmdir(folder[0]) + removed = True + except Exception: + pass + return removed + + while removeEmpty(tmpextractdir.name): + pass + + # Then copy over the directories + if self.should_perform_step("move-contents"): + self.logInfo({"d": vendor_dir}, "Copying to {d}.") + mozfile.copy_contents(tmpextractdir.name, vendor_dir) + else: + self.logInfo({}, "Skipping copying contents into tree.") + self._extract_directory = lambda: tmpextractdir.name + except Exception as e: + tmpextractdir.cleanup() + raise e + + def update_yaml(self, revision, timestamp): + with open(self.yaml_file) as f: + yaml = f.readlines() + + replaced = 0 + replacements = [ + [" release:", " %s (%s)." % (revision, timestamp)], + [" revision:", " %s" % (revision)], + ] + + for i in range(0, len(yaml)): + l = yaml[i] + + for r in replacements: + if r[0] in l: + print("Found " + l) + replaced += 1 + yaml[i] = re.sub(r[0] + " [v\.a-f0-9]+.*$", r[0] + r[1], yaml[i]) + + assert len(replacements) == replaced + + with open(self.yaml_file, "wb") as f: + f.write(("".join(yaml)).encode("utf-8")) + + def spurious_check(self, revision, ignore_modified): + changed_files = set( + [ + os.path.abspath(f) + for f in self.repository.get_changed_files(mode="staged") + ] + ) + generated_files = set( + [ + self.get_full_path(f) + for f in self.manifest["vendoring"].get("generated", []) + ] + ) + changed_files = set(changed_files) - generated_files + if not changed_files: + self.logInfo({"r": revision}, "Upstream {r} hasn't modified files locally.") + # We almost certainly won't be here if ignore_modified was passed, because a modified + # local file will show up as a changed_file, but we'll be safe anyway. + if not ignore_modified and generated_files: + for g in generated_files: + self.repository.clean_directory(g) + elif generated_files: + self.log( + logging.CRITICAL, + "vendor", + {"files": generated_files}, + "Because you passed --ignore-modified we are not cleaning your" + + " working directory, but the following files were probably" + + " spuriously edited and can be reverted: {files}", + ) + sys.exit(-2) + + self.logInfo( + {"rev": revision, "num": len(changed_files)}, + "Version '{rev}' has changed {num} files.", + ) + + def update_files(self, revision): + if "update-actions" not in self.manifest["vendoring"]: + return + + for update in self.manifest["vendoring"]["update-actions"]: + if update["action"] == "copy-file": + src = self.get_full_path(update["from"]) + dst = self.get_full_path(update["to"]) + + self.logInfo( + {"s": src, "d": dst}, "action: copy-file src: {s} dst: {d}" + ) + + with open(src) as f: + contents = f.read() + with open(dst, "w") as f: + f.write(contents) + elif update["action"] == "move-file": + src = self.get_full_path(update["from"]) + dst = self.get_full_path(update["to"]) + + self.logInfo( + {"s": src, "d": dst}, "action: move-file src: {s} dst: {d}" + ) + + shutil.move(src, dst) + elif update["action"] == "move-dir": + src = self.get_full_path(update["from"]) + dst = self.get_full_path(update["to"]) + + self.logInfo( + {"src": src, "dst": dst}, "action: move-dir src: {src} dst: {dst}" + ) + + if not os.path.isdir(src): + raise Exception( + "Cannot move from a source directory %s that is not a directory" + % src + ) + os.makedirs(dst, exist_ok=True) + + def copy_tree(src, dst): + names = os.listdir(src) + os.makedirs(dst, exist_ok=True) + + for name in names: + srcname = os.path.join(src, name) + dstname = os.path.join(dst, name) + + if os.path.isdir(srcname): + copy_tree(srcname, dstname) + else: + shutil.copy2(srcname, dstname) + + copy_tree(src, dst) + shutil.rmtree(src) + + elif update["action"] in ["replace-in-file", "replace-in-file-regex"]: + file = self.get_full_path(update["file"]) + + self.logInfo({"file": file}, "action: replace-in-file file: {file}") + + replacement = update["with"].replace("{revision}", revision) + _replace_in_file( + file, + update["pattern"], + replacement, + regex=update["action"] == "replace-in-file-regex", + ) + elif update["action"] == "delete-path": + path = self.get_full_path(update["path"]) + self.logInfo({"path": path}, "action: delete-path path: {path}") + mozfile.remove(path) + elif update["action"] in ["run-script", "run-command"]: + if update["action"] == "run-script": + command = self.get_full_path(update["script"], support_cwd=True) + else: + command = update["command"] + + run_dir = self.get_full_path(update["cwd"], support_cwd=True) + + args = [] + for a in update.get("args", []): + if a == "{revision}": + args.append(revision) + elif any( + s in a + for s in [ + "{cwd}", + "{vendor_dir}", + "{yaml_dir}", + "{tmpextractdir}", + ] + ): + args.append(self.get_full_path(a, support_cwd=True)) + else: + args.append(a) + + self.logInfo( + { + "command": command, + "run_dir": run_dir, + "args": args, + "type": update["action"], + }, + "action: {type} command: {command} working dir: {run_dir} args: {args}", + ) + extra_env = ( + {"GECKO_PATH": os.getcwd()} + if "GECKO_PATH" not in os.environ + else {} + ) + # We also add a signal to scripts that they are running under mach vendor + extra_env["MACH_VENDOR"] = "1" + self.run_process( + args=[command] + args, + cwd=run_dir, + log_name=command, + require_unix_environment=True, + append_env=extra_env, + ) + else: + assert False, "Unknown action supplied (how did this pass validation?)" + + def update_moz_build(self, vendoring_dir, moz_yaml_dir, add_to_exports): + if vendoring_dir == moz_yaml_dir: + vendoring_dir = moz_yaml_dir = None + + # If you edit this (especially for header files) you should double check + # rewrite_mozbuild.py around 'assignment_type' + source_suffixes = [".cc", ".c", ".cpp", ".S", ".asm"] + header_suffixes = [".h", ".hpp"] + + files_removed = self.repository.get_changed_files(diff_filter="D") + files_added = self.repository.get_changed_files(diff_filter="A") + + # Filter the files added to just source files we track in moz.build files. + files_added = [ + f for f in files_added if any([f.endswith(s) for s in source_suffixes]) + ] + header_files_to_add = [ + f for f in files_added if any([f.endswith(s) for s in header_suffixes]) + ] + if add_to_exports: + files_added += header_files_to_add + elif header_files_to_add: + self.log( + logging.WARNING, + "header_files_warning", + {}, + ( + "We found %s header files in the update, pass --add-to-exports if you want" + + " to attempt to include them in EXPORTS blocks: %s" + ) + % (len(header_files_to_add), header_files_to_add), + ) + + self.logInfo( + {"added": len(files_added), "removed": len(files_removed)}, + "Found {added} files added and {removed} files removed.", + ) + + should_abort = False + for f in files_added: + try: + add_file_to_moz_build_file(f, moz_yaml_dir, vendoring_dir) + except MozBuildRewriteException: + self.log( + logging.ERROR, + "vendor", + {}, + "Could not add %s to the appropriate moz.build file" % f, + ) + should_abort = True + + for f in files_removed: + try: + remove_file_from_moz_build_file(f, moz_yaml_dir, vendoring_dir) + except MozBuildRewriteException: + self.log( + logging.ERROR, + "vendor", + {}, + "Could not remove %s from the appropriate moz.build file" % f, + ) + should_abort = True + + if should_abort: + self.log( + logging.ERROR, + "vendor", + {}, + "This is a deficiency in ./mach vendor . " + + "Please review the affected files before committing.", + ) + # Exit with -1 to distinguish this from the Exception case of exiting with 1 + sys.exit(-1) + + def import_local_patches(self, patches, yaml_dir, vendor_dir): + self.logInfo({}, "Importing local patches...") + for patch in self.convert_patterns_to_paths(yaml_dir, patches): + script = [ + "patch", + "-p1", + "--directory", + vendor_dir, + "--input", + os.path.abspath(patch), + "--no-backup-if-mismatch", + ] + self.run_process( + args=script, + log_name=script, + ) diff --git a/python/mozbuild/mozbuild/vendor/vendor_python.py b/python/mozbuild/mozbuild/vendor/vendor_python.py new file mode 100644 index 0000000000..c969a3a157 --- /dev/null +++ b/python/mozbuild/mozbuild/vendor/vendor_python.py @@ -0,0 +1,232 @@ +# 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/. + +import os +import shutil +import subprocess +import sys +from pathlib import Path + +import mozfile +from mozfile import TemporaryDirectory +from mozpack.files import FileFinder + +from mozbuild.base import MozbuildObject + +EXCLUDED_PACKAGES = { + # dlmanager's package on PyPI only has metadata, but is missing the code. + # https://github.com/parkouss/dlmanager/issues/1 + "dlmanager", + # gyp's package on PyPI doesn't have any downloadable files. + "gyp", + # We keep some wheels vendored in "_venv" for use in Mozharness + "_venv", + # We manage vendoring "vsdownload" with a moz.yaml file (there is no module + # on PyPI). + "vsdownload", + # The moz.build file isn't a vendored module, so don't delete it. + "moz.build", + "requirements.in", + # The ansicon package contains DLLs and we don't want to arbitrarily vendor + # them since they could be unsafe. This module should rarely be used in practice + # (it's a fallback for old versions of windows). We've intentionally vendored a + # modified 'dummy' version of it so that the dependency checks still succeed, but + # if it ever is attempted to be used, it will fail gracefully. + "ansicon", + # Non-permanent exclusion of pip to avoid vendoring removing a solution to + # pkgutil's deprecation. See https://bugzilla.mozilla.org/show_bug.cgi?id=1857470 + # for more information. + "pip", +} + + +class VendorPython(MozbuildObject): + def __init__(self, *args, **kwargs): + MozbuildObject.__init__(self, *args, virtualenv_name="vendor", **kwargs) + + def vendor(self, keep_extra_files=False): + from mach.python_lockfile import PoetryHandle + + self.populate_logger() + self.log_manager.enable_unstructured() + + vendor_dir = Path(self.topsrcdir) / "third_party" / "python" + requirements_in = vendor_dir / "requirements.in" + poetry_lockfile = vendor_dir / "poetry.lock" + _sort_requirements_in(requirements_in) + + with TemporaryDirectory() as work_dir: + work_dir = Path(work_dir) + poetry = PoetryHandle(work_dir) + poetry.add_requirements_in_file(requirements_in) + poetry.reuse_existing_lockfile(poetry_lockfile) + lockfiles = poetry.generate_lockfiles(do_update=False) + + # Vendoring packages is only viable if it's possible to have a single + # set of packages that work regardless of which environment they're used in. + # So, we scrub environment markers, so that we essentially ask pip to + # download "all dependencies for all environments". Pip will then either + # fetch them as requested, or intelligently raise an error if that's not + # possible (e.g.: if different versions of Python would result in different + # packages/package versions). + pip_lockfile_without_markers = work_dir / "requirements.no-markers.txt" + shutil.copy(str(lockfiles.pip_lockfile), str(pip_lockfile_without_markers)) + remove_environment_markers_from_requirements_txt( + pip_lockfile_without_markers + ) + + with TemporaryDirectory() as tmp: + # use requirements.txt to download archived source distributions of all + # packages + subprocess.check_call( + [ + sys.executable, + "-m", + "pip", + "download", + "-r", + str(pip_lockfile_without_markers), + "--no-deps", + "--dest", + tmp, + "--abi", + "none", + "--platform", + "any", + ] + ) + _purge_vendor_dir(vendor_dir) + self._extract(tmp, vendor_dir, keep_extra_files) + + requirements_out = vendor_dir / "requirements.txt" + + # since requirements.out and poetry.lockfile are both outputs from + # third party code, they may contain carriage returns on Windows. We + # should strip the carriage returns to maintain consistency in our output + # regardless of which platform is doing the vendoring. We can do this and + # the copying at the same time to minimize reads and writes. + _copy_file_strip_carriage_return(lockfiles.pip_lockfile, requirements_out) + _copy_file_strip_carriage_return(lockfiles.poetry_lockfile, poetry_lockfile) + self.repository.add_remove_files(vendor_dir) + + def _extract(self, src, dest, keep_extra_files=False): + """extract source distribution into vendor directory""" + + ignore = () + if not keep_extra_files: + ignore = ("*/doc", "*/docs", "*/test", "*/tests", "**/.git") + finder = FileFinder(src) + for archive, _ in finder.find("*"): + _, ext = os.path.splitext(archive) + archive_path = os.path.join(finder.base, archive) + if ext == ".whl": + # Archive is named like "$package-name-1.0-py2.py3-none-any.whl", and should + # have four dashes that aren't part of the package name. + package_name, version, spec, abi, platform_and_suffix = archive.rsplit( + "-", 4 + ) + + if package_name in EXCLUDED_PACKAGES: + print( + f"'{package_name}' is on the exclusion list and will not be vendored." + ) + continue + + target_package_dir = os.path.join(dest, package_name) + os.mkdir(target_package_dir) + + # Extract all the contents of the wheel into the package subdirectory. + # We're expecting at least a code directory and a ".dist-info" directory, + # though there may be a ".data" directory as well. + mozfile.extract(archive_path, target_package_dir, ignore=ignore) + _denormalize_symlinks(target_package_dir) + else: + # Archive is named like "$package-name-1.0.tar.gz", and the rightmost + # dash should separate the package name from the rest of the archive + # specifier. + package_name, archive_postfix = archive.rsplit("-", 1) + package_dir = os.path.join(dest, package_name) + + if package_name in EXCLUDED_PACKAGES: + print( + f"'{package_name}' is on the exclusion list and will not be vendored." + ) + continue + + # The archive should only contain one top-level directory, which has + # the source files. We extract this directory directly to + # the vendor directory. + extracted_files = mozfile.extract(archive_path, dest, ignore=ignore) + assert len(extracted_files) == 1 + extracted_package_dir = extracted_files[0] + + # The extracted package dir includes the version in the name, + # which we don't we don't want. + mozfile.move(extracted_package_dir, package_dir) + _denormalize_symlinks(package_dir) + + +def _sort_requirements_in(requirements_in: Path): + requirements = {} + with requirements_in.open(mode="r", newline="\n") as f: + comments = [] + for line in f.readlines(): + line = line.strip() + if not line or line.startswith("#"): + comments.append(line) + continue + name, version = line.split("==") + requirements[name] = version, comments + comments = [] + + with requirements_in.open(mode="w", newline="\n") as f: + for name, (version, comments) in sorted(requirements.items()): + if comments: + f.write("{}\n".format("\n".join(comments))) + f.write("{}=={}\n".format(name, version)) + + +def remove_environment_markers_from_requirements_txt(requirements_txt: Path): + with requirements_txt.open(mode="r", newline="\n") as f: + lines = f.readlines() + markerless_lines = [] + continuation_token = " \\" + for line in lines: + line = line.rstrip() + + if not line.startswith(" ") and not line.startswith("#") and ";" in line: + has_continuation_token = line.endswith(continuation_token) + # The first line of each requirement looks something like: + # package-name==X.Y; python_version>=3.7 + # We can scrub the environment marker by splitting on the semicolon + line = line.split(";")[0] + if has_continuation_token: + line += continuation_token + markerless_lines.append(line) + else: + markerless_lines.append(line) + + with requirements_txt.open(mode="w", newline="\n") as f: + f.write("\n".join(markerless_lines)) + + +def _purge_vendor_dir(vendor_dir): + for child in Path(vendor_dir).iterdir(): + if child.name not in EXCLUDED_PACKAGES: + mozfile.remove(str(child)) + + +def _denormalize_symlinks(target): + # If any files inside the vendored package were symlinks, turn them into normal files + # because hg.mozilla.org forbids symlinks in the repository. + link_finder = FileFinder(target) + for _, f in link_finder.find("**"): + if os.path.islink(f.path): + link_target = os.path.realpath(f.path) + os.unlink(f.path) + shutil.copyfile(link_target, f.path) + + +def _copy_file_strip_carriage_return(file_src: Path, file_dst): + shutil.copyfileobj(file_src.open(mode="r"), file_dst.open(mode="w", newline="\n")) diff --git a/python/mozbuild/mozbuild/vendor/vendor_rust.py b/python/mozbuild/mozbuild/vendor/vendor_rust.py new file mode 100644 index 0000000000..15b644cf99 --- /dev/null +++ b/python/mozbuild/mozbuild/vendor/vendor_rust.py @@ -0,0 +1,976 @@ +# 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/. + +import errno +import hashlib +import json +import logging +import os +import re +import subprocess +import typing +from collections import defaultdict +from itertools import dropwhile +from pathlib import Path + +import mozpack.path as mozpath +import toml +from looseversion import LooseVersion +from mozboot.util import MINIMUM_RUST_VERSION + +from mozbuild.base import BuildEnvironmentNotFoundException, MozbuildObject + +if typing.TYPE_CHECKING: + import datetime + +# Type of a TOML value. +TomlItem = typing.Union[ + str, + typing.List["TomlItem"], + typing.Dict[str, "TomlItem"], + bool, + int, + float, + "datetime.datetime", + "datetime.date", + "datetime.time", +] + + +CARGO_CONFIG_TEMPLATE = """\ +# This file contains vendoring instructions for cargo. +# It was generated by `mach vendor rust`. +# Please do not edit. + +{config} + +# Take advantage of the fact that cargo will treat lines starting with # +# as comments to add preprocessing directives. This file can thus by copied +# as-is to $topsrcdir/.cargo/config with no preprocessing to be used there +# (for e.g. independent tasks building rust code), or be preprocessed by +# the build system to produce a .cargo/config with the right content. +#define REPLACE_NAME {replace_name} +#define VENDORED_DIRECTORY {directory} +# We explicitly exclude the following section when preprocessing because +# it would overlap with the preprocessed [source."@REPLACE_NAME@"], and +# cargo would fail. +#ifndef REPLACE_NAME +[source.{replace_name}] +directory = "{directory}" +#endif + +# Thankfully, @REPLACE_NAME@ is unlikely to be a legitimate source, so +# cargo will ignore it when it's here verbatim. +#filter substitution +[source."@REPLACE_NAME@"] +directory = "@top_srcdir@/@VENDORED_DIRECTORY@" +""" + + +CARGO_LOCK_NOTICE = """ +NOTE: `cargo vendor` may have made changes to your Cargo.lock. To restore your +Cargo.lock to the HEAD version, run `git checkout -- Cargo.lock` or +`hg revert Cargo.lock`. +""" + + +PACKAGES_WE_DONT_WANT = {} + +PACKAGES_WE_ALWAYS_WANT_AN_OVERRIDE_OF = [ + "autocfg", + "cmake", + "vcpkg", + "windows", + "windows-targets", +] + + +# Historically duplicated crates. Eventually we want this list to be empty. +# If you do need to make changes increasing the number of duplicates, please +# add a comment as to why. +TOLERATED_DUPES = { + "mio": 2, + # Transition from time 0.1 to 0.3 underway, but chrono is stuck on 0.1 + # and hasn't been updated in 1.5 years (an hypothetical update is + # expected to remove the dependency on time altogether). + "time": 2, +} + + +class VendorRust(MozbuildObject): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self._issues = [] + + def serialize_issues_json(self): + return json.dumps( + { + "Cargo.lock": [ + { + "path": "Cargo.lock", + "column": None, + "line": None, + "level": "error" if level == logging.ERROR else "warning", + "message": msg, + } + for (level, msg) in self._issues + ] + } + ) + + def log(self, level, action, params, format_str): + if level >= logging.WARNING: + self._issues.append((level, format_str.format(**params))) + super().log(level, action, params, format_str) + + def get_cargo_path(self): + try: + return self.substs["CARGO"] + except (BuildEnvironmentNotFoundException, KeyError): + if "MOZ_AUTOMATION" in os.environ: + cargo = os.path.join( + os.environ["MOZ_FETCHES_DIR"], "rustc", "bin", "cargo" + ) + assert os.path.exists(cargo) + return cargo + # Default if this tree isn't configured. + from mozfile import which + + cargo = which("cargo") + if not cargo: + raise OSError( + errno.ENOENT, + ( + "Could not find 'cargo' on your $PATH. " + "Hint: have you run `mach build` or `mach configure`?" + ), + ) + return cargo + + def check_cargo_version(self, cargo): + """ + Ensure that Cargo is new enough. + """ + out = ( + subprocess.check_output([cargo, "--version"]) + .splitlines()[0] + .decode("UTF-8") + ) + if not out.startswith("cargo"): + return False + version = LooseVersion(out.split()[1]) + # Cargo 1.71.0 changed vendoring in a way that creates a lot of noise + # if we go back and forth between vendoring with an older version and + # a newer version. Only allow the newer versions. + minimum_rust_version = MINIMUM_RUST_VERSION + if LooseVersion("1.71.0") >= MINIMUM_RUST_VERSION: + minimum_rust_version = "1.71.0" + if version < minimum_rust_version: + self.log( + logging.ERROR, + "cargo_version", + {}, + "Cargo >= {0} required (install Rust {0} or newer)".format( + minimum_rust_version + ), + ) + return False + self.log(logging.DEBUG, "cargo_version", {}, "cargo is new enough") + return True + + def has_modified_files(self): + """ + Ensure that there aren't any uncommitted changes to files + in the working copy, since we're going to change some state + on the user. Allow changes to Cargo.{toml,lock} since that's + likely to be a common use case. + """ + modified = [ + f + for f in self.repository.get_changed_files("M") + if os.path.basename(f) not in ("Cargo.toml", "Cargo.lock") + and not f.startswith("supply-chain/") + ] + if modified: + self.log( + logging.ERROR, + "modified_files", + {}, + """You have uncommitted changes to the following files: + +{files} + +Please commit or stash these changes before vendoring, or re-run with `--ignore-modified`. +""".format( + files="\n".join(sorted(modified)) + ), + ) + return modified + + def check_openssl(self): + """ + Set environment flags for building with openssl. + + MacOS doesn't include openssl, but the openssl-sys crate used by + mach-vendor expects one of the system. It's common to have one + installed in /usr/local/opt/openssl by homebrew, but custom link + flags are necessary to build against it. + """ + + test_paths = ["/usr/include", "/usr/local/include"] + if any( + [os.path.exists(os.path.join(path, "openssl/ssl.h")) for path in test_paths] + ): + # Assume we can use one of these system headers. + return None + + if os.path.exists("/usr/local/opt/openssl/include/openssl/ssl.h"): + # Found a likely homebrew install. + self.log( + logging.INFO, "openssl", {}, "Using OpenSSL in /usr/local/opt/openssl" + ) + return { + "OPENSSL_INCLUDE_DIR": "/usr/local/opt/openssl/include", + "OPENSSL_LIB_DIR": "/usr/local/opt/openssl/lib", + } + + self.log(logging.ERROR, "openssl", {}, "OpenSSL not found!") + return None + + def _ensure_cargo(self): + """ + Ensures all the necessary cargo bits are installed. + + Returns the path to cargo if successful, None otherwise. + """ + cargo = self.get_cargo_path() + if not self.check_cargo_version(cargo): + return None + return cargo + + # A whitelist of acceptable license identifiers for the + # packages.license field from https://spdx.org/licenses/. Cargo + # documentation claims that values are checked against the above + # list and that multiple entries can be separated by '/'. We + # choose to list all combinations instead for the sake of + # completeness and because some entries below obviously do not + # conform to the format prescribed in the documentation. + # + # It is insufficient to have additions to this whitelist reviewed + # solely by a build peer; any additions must be checked by somebody + # competent to review licensing minutiae. + + # Licenses for code used at runtime. Please see the above comment before + # adding anything to this list. + RUNTIME_LICENSE_WHITELIST = [ + "Apache-2.0", + "Apache-2.0 WITH LLVM-exception", + # BSD-2-Clause and BSD-3-Clause are ok, but packages using them + # must be added to the appropriate section of about:licenses. + # To encourage people to remember to do that, we do not whitelist + # the licenses themselves, and we require the packages to be added + # to RUNTIME_LICENSE_PACKAGE_WHITELIST below. + "CC0-1.0", + "ISC", + "MIT", + "MPL-2.0", + "Unicode-DFS-2016", + "Unlicense", + "Zlib", + ] + + # Licenses for code used at build time (e.g. code generators). Please see the above + # comments before adding anything to this list. + BUILDTIME_LICENSE_WHITELIST = { + "BSD-3-Clause": [ + "bindgen", + "fuchsia-zircon", + "fuchsia-zircon-sys", + "fuchsia-cprng", + "glsl", + "instant", + ] + } + + # This whitelist should only be used for packages that use an acceptable + # license, but that also need to explicitly mentioned in about:license. + RUNTIME_LICENSE_PACKAGE_WHITELIST = { + "BSD-2-Clause": [ + "arrayref", + "mach", + "qlog", + ], + "BSD-3-Clause": [ + "subtle", + ], + } + + # ICU4X is distributed as individual crates that all share the same LICENSE + # that will need to be individually added to the allow list below. We'll + # define the SHA256 once here, to make the review process easier as new + # ICU4X crates are vendored into the tree. + ICU4X_LICENSE_SHA256 = ( + "853f87c96f3d249f200fec6db1114427bc8bdf4afddc93c576956d78152ce978" + ) + + # This whitelist should only be used for packages that use a + # license-file and for which the license-file entry has been + # reviewed. The table is keyed by package names and maps to the + # sha256 hash of the license file that we reviewed. + # + # As above, it is insufficient to have additions to this whitelist + # reviewed solely by a build peer; any additions must be checked by + # somebody competent to review licensing minutiae. + RUNTIME_LICENSE_FILE_PACKAGE_WHITELIST = { + # MIT + "deque": "6485b8ed310d3f0340bf1ad1f47645069ce4069dcc6bb46c7d5c6faf41de1fdb", + # we're whitelisting this fuchsia crate because it doesn't get built in the final + # product but has a license-file that needs ignoring + "fuchsia-cprng": "03b114f53e6587a398931762ee11e2395bfdba252a329940e2c8c9e81813845b", + # ICU4X uses Unicode v3 license + "icu_collections": ICU4X_LICENSE_SHA256, + "icu_locid": ICU4X_LICENSE_SHA256, + "icu_locid_transform": ICU4X_LICENSE_SHA256, + "icu_locid_transform_data": ICU4X_LICENSE_SHA256, + "icu_properties": ICU4X_LICENSE_SHA256, + "icu_properties_data": ICU4X_LICENSE_SHA256, + "icu_provider": ICU4X_LICENSE_SHA256, + "icu_provider_adapters": ICU4X_LICENSE_SHA256, + "icu_provider_macros": ICU4X_LICENSE_SHA256, + "icu_segmenter": ICU4X_LICENSE_SHA256, + "litemap": ICU4X_LICENSE_SHA256, + "tinystr": ICU4X_LICENSE_SHA256, + "writeable": ICU4X_LICENSE_SHA256, + "yoke": ICU4X_LICENSE_SHA256, + "yoke-derive": ICU4X_LICENSE_SHA256, + "zerofrom": ICU4X_LICENSE_SHA256, + "zerofrom-derive": ICU4X_LICENSE_SHA256, + "zerovec": ICU4X_LICENSE_SHA256, + "zerovec-derive": ICU4X_LICENSE_SHA256, + } + + @staticmethod + def runtime_license(package, license_string): + """Cargo docs say: + --- + https://doc.rust-lang.org/cargo/reference/manifest.html + + This is an SPDX 2.1 license expression for this package. Currently + crates.io will validate the license provided against a whitelist of + known license and exception identifiers from the SPDX license list + 2.4. Parentheses are not currently supported. + + Multiple licenses can be separated with a `/`, although that usage + is deprecated. Instead, use a license expression with AND and OR + operators to get more explicit semantics. + --- + But I have no idea how you can meaningfully AND licenses, so + we will abort if that is detected. We'll handle `/` and OR as + equivalent and approve is any is in our approved list.""" + + # This specific AND combination has been reviewed for encoding_rs. + if ( + license_string == "(Apache-2.0 OR MIT) AND BSD-3-Clause" + and package == "encoding_rs" + ): + return True + + # This specific AND combination has been reviewed for unicode-ident. + if ( + license_string == "(MIT OR Apache-2.0) AND Unicode-DFS-2016" + and package == "unicode-ident" + ): + return True + + if re.search(r"\s+AND", license_string): + return False + + license_list = re.split(r"\s*/\s*|\s+OR\s+", license_string) + for license in license_list: + if license in VendorRust.RUNTIME_LICENSE_WHITELIST: + return True + if package in VendorRust.RUNTIME_LICENSE_PACKAGE_WHITELIST.get(license, []): + return True + return False + + def _check_licenses(self, vendor_dir: str) -> bool: + def verify_acceptable_license(package: str, license: str) -> bool: + self.log( + logging.DEBUG, "package_license", {}, "has license {}".format(license) + ) + + if not self.runtime_license(package, license): + if license not in self.BUILDTIME_LICENSE_WHITELIST: + self.log( + logging.ERROR, + "package_license_error", + {}, + """Package {} has a non-approved license: {}. + + Please request license review on the package's license. If the package's license + is approved, please add it to the whitelist of suitable licenses. + """.format( + package, license + ), + ) + return False + elif package not in self.BUILDTIME_LICENSE_WHITELIST[license]: + self.log( + logging.ERROR, + "package_license_error", + {}, + """Package {} has a license that is approved for build-time dependencies: + {} + but the package itself is not whitelisted as being a build-time only package. + + If your package is build-time only, please add it to the whitelist of build-time + only packages. Otherwise, you need to request license review on the package's license. + If the package's license is approved, please add it to the whitelist of suitable licenses. + """.format( + package, license + ), + ) + return False + return True + + def check_package(package_name: str) -> bool: + self.log( + logging.DEBUG, + "package_check", + {}, + "Checking license for {}".format(package_name), + ) + + toml_file = os.path.join(vendor_dir, package_name, "Cargo.toml") + with open(toml_file, encoding="utf-8") as fh: + toml_data = toml.load(fh) + + package_entry: typing.Dict[str, TomlItem] = toml_data["package"] + license = package_entry.get("license", None) + license_file = package_entry.get("license-file", None) + + if license is not None and type(license) is not str: + self.log( + logging.ERROR, + "package_invalid_license_format", + {}, + "package {} has an invalid `license` field (expected a string)".format( + package_name + ), + ) + return False + + if license_file is not None and type(license_file) is not str: + self.log( + logging.ERROR, + "package_invalid_license_format", + {}, + "package {} has an invalid `license-file` field (expected a string)".format( + package_name + ), + ) + return False + + # License information is optional for crates to provide, but + # we require it. + if not license and not license_file: + self.log( + logging.ERROR, + "package_no_license", + {}, + "package {} does not provide a license".format(package_name), + ) + return False + + # The Cargo.toml spec suggests that crates should either have + # `license` or `license-file`, but not both. We might as well + # be defensive about that, though. + if license and license_file: + self.log( + logging.ERROR, + "package_many_licenses", + {}, + "package {} provides too many licenses".format(package_name), + ) + return False + + if license: + return verify_acceptable_license(package_name, license) + + # otherwise, it's a custom license in a separate file + assert license_file is not None + self.log( + logging.DEBUG, + "package_license_file", + {}, + "package has license-file {}".format(license_file), + ) + + if package_name not in self.RUNTIME_LICENSE_FILE_PACKAGE_WHITELIST: + self.log( + logging.ERROR, + "package_license_file_unknown", + {}, + """Package {} has an unreviewed license file: {}. + +Please request review on the provided license; if approved, the package can be added +to the whitelist of packages whose licenses are suitable. +""".format( + package_name, license_file + ), + ) + return False + + approved_hash = self.RUNTIME_LICENSE_FILE_PACKAGE_WHITELIST[package_name] + + with open( + os.path.join(vendor_dir, package_name, license_file), "rb" + ) as license_buf: + current_hash = hashlib.sha256(license_buf.read()).hexdigest() + + if current_hash != approved_hash: + self.log( + logging.ERROR, + "package_license_file_mismatch", + {}, + """Package {} has changed its license file: {} (hash {}). + +Please request review on the provided license; if approved, please update the +license file's hash. +""".format( + package_name, license_file, current_hash + ), + ) + return False + return True + + # Force all of the packages to be checked for license information + # before reducing via `all`, so all license issues are found in a + # single `mach vendor rust` invocation. + results = [ + check_package(p) + for p in os.listdir(vendor_dir) + if os.path.isdir(os.path.join(vendor_dir, p)) + ] + return all(results) + + def _check_build_rust(self, cargo_lock): + ret = True + crates = {} + for path in Path(self.topsrcdir).glob("build/rust/**/Cargo.toml"): + with open(path) as fh: + cargo_toml = toml.load(fh) + relative_path = path.relative_to(self.topsrcdir) + package = cargo_toml["package"] + key = (package["name"], package["version"]) + if key in crates: + self.log( + logging.ERROR, + "build_rust", + { + "path": crates[key], + "path2": relative_path, + "crate": key[0], + "version": key[1], + }, + "{path} and {path2} both contain {crate} {version}", + ) + ret = False + crates[key] = relative_path + + for package in cargo_lock["package"]: + key = (package["name"], package["version"]) + if key in crates and "source" not in package: + crates.pop(key) + + for (name, version), path in crates.items(): + self.log( + logging.ERROR, + "build_rust", + {"path": path, "crate": name, "version": version}, + "{crate} {version} has an override in {path} that is not used", + ) + ret = False + return ret + + def vendor(self, ignore_modified=False, force=False): + from mozbuild.mach_commands import cargo_vet + + self.populate_logger() + self.log_manager.enable_unstructured() + if not ignore_modified and self.has_modified_files(): + return False + + cargo = self._ensure_cargo() + if not cargo: + self.log(logging.ERROR, "cargo_not_found", {}, "Cargo was not found.") + return False + + relative_vendor_dir = "third_party/rust" + vendor_dir = mozpath.join(self.topsrcdir, relative_vendor_dir) + + # We use check_call instead of mozprocess to ensure errors are displayed. + # We do an |update -p| here to regenerate the Cargo.lock file with minimal + # changes. See bug 1324462 + res = subprocess.run([cargo, "update", "-p", "gkrust"], cwd=self.topsrcdir) + if res.returncode: + self.log(logging.ERROR, "cargo_update_failed", {}, "Cargo update failed.") + return False + + with open(os.path.join(self.topsrcdir, "Cargo.lock")) as fh: + cargo_lock = toml.load(fh) + failed = False + for package in cargo_lock.get("patch", {}).get("unused", []): + self.log( + logging.ERROR, + "unused_patch", + {"crate": package["name"]}, + """Unused patch in top-level Cargo.toml for {crate}.""", + ) + failed = True + + if not self._check_build_rust(cargo_lock): + failed = True + + grouped = defaultdict(list) + for package in cargo_lock["package"]: + if package["name"] in PACKAGES_WE_ALWAYS_WANT_AN_OVERRIDE_OF: + # When the in-tree version is used, there is `source` for + # it in Cargo.lock, which is what we expect. + if package.get("source"): + self.log( + logging.ERROR, + "non_overridden", + { + "crate": package["name"], + "version": package["version"], + "source": package["source"], + }, + "Crate {crate} v{version} must be overridden but isn't " + "and comes from {source}.", + ) + failed = True + elif package["name"] in PACKAGES_WE_DONT_WANT: + self.log( + logging.ERROR, + "undesirable", + { + "crate": package["name"], + "version": package["version"], + "reason": PACKAGES_WE_DONT_WANT[package["name"]], + }, + "Crate {crate} is not desirable: {reason}", + ) + failed = True + grouped[package["name"]].append(package) + + for name, packages in grouped.items(): + # Allow to have crates of the same name when one depends on the other. + num = len( + [ + p + for p in packages + if all(d.split()[0] != name for d in p.get("dependencies", [])) + ] + ) + expected = TOLERATED_DUPES.get(name, 1) + if num > expected: + self.log( + logging.ERROR, + "duplicate_crate", + { + "crate": name, + "num": num, + "expected": expected, + "file": Path(__file__).relative_to(self.topsrcdir), + }, + "There are {num} different versions of crate {crate} " + "(expected {expected}). Please avoid the extra duplication " + "or adjust TOLERATED_DUPES in {file} if not possible " + "(but we'd prefer the former).", + ) + failed = True + elif num < expected and num > 1: + self.log( + logging.ERROR, + "less_duplicate_crate", + { + "crate": name, + "num": num, + "expected": expected, + "file": Path(__file__).relative_to(self.topsrcdir), + }, + "There are {num} different versions of crate {crate} " + "(expected {expected}). Please adjust TOLERATED_DUPES in " + "{file} to reflect this improvement.", + ) + failed = True + elif num < expected and num > 0: + self.log( + logging.ERROR, + "less_duplicate_crate", + { + "crate": name, + "file": Path(__file__).relative_to(self.topsrcdir), + }, + "Crate {crate} is not duplicated anymore. " + "Please adjust TOLERATED_DUPES in {file} to reflect this improvement.", + ) + failed = True + elif name in TOLERATED_DUPES and expected <= 1: + self.log( + logging.ERROR, + "broken_allowed_dupes", + { + "crate": name, + "file": Path(__file__).relative_to(self.topsrcdir), + }, + "Crate {crate} is not duplicated. Remove it from " + "TOLERATED_DUPES in {file}.", + ) + failed = True + + for name in TOLERATED_DUPES: + if name not in grouped: + self.log( + logging.ERROR, + "outdated_allowed_dupes", + { + "crate": name, + "file": Path(__file__).relative_to(self.topsrcdir), + }, + "Crate {crate} is not in Cargo.lock anymore. Remove it from " + "TOLERATED_DUPES in {file}.", + ) + failed = True + + # Only emit warnings for cargo-vet for now. + env = os.environ.copy() + env["PATH"] = os.pathsep.join( + ( + str(Path(cargo).parent), + os.environ["PATH"], + ) + ) + flags = ["--output-format=json"] + if "MOZ_AUTOMATION" in os.environ: + flags.append("--locked") + flags.append("--frozen") + res = cargo_vet( + self, + flags, + stdout=subprocess.PIPE, + env=env, + ) + if res.returncode: + vet = json.loads(res.stdout) + logged_error = False + for failure in vet.get("failures", []): + failure["crate"] = failure.pop("name") + self.log( + logging.ERROR, + "cargo_vet_failed", + failure, + "Missing audit for {crate}:{version} (requires {missing_criteria})." + " Run `./mach cargo vet` for more information.", + ) + logged_error = True + # NOTE: This could log more information, but the violation JSON + # output isn't super stable yet, so it's probably simpler to tell + # the caller to run `./mach cargo vet` directly. + for key in vet.get("violations", {}).keys(): + self.log( + logging.ERROR, + "cargo_vet_failed", + {"key": key}, + "Violation conflict for {key}. Run `./mach cargo vet` for more information.", + ) + logged_error = True + if "error" in vet: + # NOTE: The error format produced by cargo-vet is from the + # `miette` crate, and can include a lot of metadata and context. + # If we want to show more details in the future, we can expand + # this rendering to also include things like source labels and + # related error metadata. + error = vet["error"] + self.log( + logging.ERROR, + "cargo_vet_failed", + error, + "Vet {severity}: {message}", + ) + if "help" in error: + self.log(logging.INFO, "cargo_vet_failed", error, " help: {help}") + for cause in error.get("causes", []): + self.log( + logging.INFO, + "cargo_vet_failed", + {"cause": cause}, + " cause: {cause}", + ) + for related in error.get("related", []): + self.log( + logging.INFO, + "cargo_vet_failed", + related, + " related {severity}: {message}", + ) + self.log( + logging.INFO, + "cargo_vet_failed", + {}, + "Run `./mach cargo vet` for more information.", + ) + logged_error = True + if not logged_error: + self.log( + logging.ERROR, + "cargo_vet_failed", + {}, + "Unknown vet error. Run `./mach cargo vet` for more information.", + ) + failed = True + + # If we failed when checking the crates list and/or running `cargo vet`, + # stop before invoking `cargo vendor`. + if failed and not force: + return False + + res = subprocess.run( + [cargo, "vendor", vendor_dir], cwd=self.topsrcdir, stdout=subprocess.PIPE + ) + if res.returncode: + self.log(logging.ERROR, "cargo_vendor_failed", {}, "Cargo vendor failed.") + return False + output = res.stdout.decode("UTF-8") + + # Get the snippet of configuration that cargo vendor outputs, and + # update .cargo/config with it. + # XXX(bug 1576765): Hopefully do something better after + # https://github.com/rust-lang/cargo/issues/7280 is addressed. + config = "\n".join( + dropwhile(lambda l: not l.startswith("["), output.splitlines()) + ) + + # The config is toml; parse it as such. + config = toml.loads(config) + + # For each replace-with, extract their configuration and update the + # corresponding directory to be relative to topsrcdir. + replaces = { + v["replace-with"] for v in config["source"].values() if "replace-with" in v + } + + # We only really expect one replace-with + if len(replaces) != 1: + self.log( + logging.ERROR, + "vendor_failed", + {}, + """cargo vendor didn't output a unique replace-with. Found: %s.""" + % replaces, + ) + return False + + replace_name = replaces.pop() + replace = config["source"].pop(replace_name) + replace["directory"] = mozpath.relpath( + mozpath.normsep(os.path.normcase(replace["directory"])), + mozpath.normsep(os.path.normcase(self.topsrcdir)), + ) + + cargo_config = os.path.join(self.topsrcdir, ".cargo", "config.in") + with open(cargo_config, "w", encoding="utf-8", newline="\n") as fh: + fh.write( + CARGO_CONFIG_TEMPLATE.format( + config=toml.dumps(config), + replace_name=replace_name, + directory=replace["directory"], + ) + ) + + if not self._check_licenses(vendor_dir) and not force: + self.log( + logging.ERROR, + "license_check_failed", + {}, + """The changes from `mach vendor rust` will NOT be added to version control. + +{notice}""".format( + notice=CARGO_LOCK_NOTICE + ), + ) + self.repository.clean_directory(vendor_dir) + return False + + self.repository.add_remove_files(vendor_dir) + + # 100k is a reasonable upper bound on source file size. + FILESIZE_LIMIT = 100 * 1024 + large_files = set() + cumulative_added_size = 0 + for f in self.repository.get_changed_files("A"): + path = mozpath.join(self.topsrcdir, f) + size = os.stat(path).st_size + cumulative_added_size += size + if size > FILESIZE_LIMIT: + large_files.add(f) + + # Forcefully complain about large files being added, as history has + # shown that large-ish files typically are not needed. + if large_files: + self.log( + logging.ERROR, + "filesize_check", + {}, + """The following files exceed the filesize limit of {size}: + +{files} + +If you can't reduce the size of these files, talk to a build peer (on the #build +channel at https://chat.mozilla.org) about the particular large files you are +adding. + +The changes from `mach vendor rust` will NOT be added to version control. + +{notice}""".format( + files="\n".join(sorted(large_files)), + size=FILESIZE_LIMIT, + notice=CARGO_LOCK_NOTICE, + ), + ) + self.repository.forget_add_remove_files(vendor_dir) + self.repository.clean_directory(vendor_dir) + if not force: + return False + + # Only warn for large imports, since we may just have large code + # drops from time to time (e.g. importing features into m-c). + SIZE_WARN_THRESHOLD = 5 * 1024 * 1024 + if cumulative_added_size >= SIZE_WARN_THRESHOLD: + self.log( + logging.WARN, + "filesize_check", + {}, + """Your changes add {size} bytes of added files. + +Please consider finding ways to reduce the size of the vendored packages. +For instance, check the vendored packages for unusually large test or +benchmark files that don't need to be published to crates.io and submit +a pull request upstream to ignore those files when publishing.""".format( + size=cumulative_added_size + ), + ) + if "MOZ_AUTOMATION" in os.environ: + changed = self.repository.get_changed_files(mode="staged") + for file in changed: + self.log( + logging.ERROR, + "vendor-change", + {"file": file}, + "File was modified by vendor: {file}", + ) + if changed: + return False + return True diff --git a/python/mozbuild/mozpack/__init__.py b/python/mozbuild/mozpack/__init__.py new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/python/mozbuild/mozpack/__init__.py diff --git a/python/mozbuild/mozpack/apple_pkg/Distribution.template b/python/mozbuild/mozpack/apple_pkg/Distribution.template new file mode 100644 index 0000000000..2f4b9484d9 --- /dev/null +++ b/python/mozbuild/mozpack/apple_pkg/Distribution.template @@ -0,0 +1,19 @@ +<?xml version="1.0" encoding="utf-8"?> +<installer-gui-script minSpecVersion="1"> + <pkg-ref id="${CFBundleIdentifier}"> + <bundle-version> + <bundle CFBundleShortVersionString="${CFBundleShortVersionString}" CFBundleVersion="${CFBundleVersion}" id="${CFBundleIdentifier}" path="${app_name}.app"/> + </bundle-version> + </pkg-ref> + <options customize="never" require-scripts="false" hostArchitectures="x86_64,arm64"/> + <choices-outline> + <line choice="default"> + <line choice="${CFBundleIdentifier}"/> + </line> + </choices-outline> + <choice id="default"/> + <choice id="${CFBundleIdentifier}" visible="false"> + <pkg-ref id="${CFBundleIdentifier}"/> + </choice> + <pkg-ref id="${CFBundleIdentifier}" version="${simple_version}" installKBytes="${installKBytes}">#${app_name_url_encoded}.pkg</pkg-ref> +</installer-gui-script>
\ No newline at end of file diff --git a/python/mozbuild/mozpack/apple_pkg/PackageInfo.template b/python/mozbuild/mozpack/apple_pkg/PackageInfo.template new file mode 100644 index 0000000000..74d47e396c --- /dev/null +++ b/python/mozbuild/mozpack/apple_pkg/PackageInfo.template @@ -0,0 +1,19 @@ +<?xml version="1.0" encoding="utf-8"?> +<pkg-info overwrite-permissions="true" relocatable="false" identifier="${CFBundleIdentifier}" postinstall-action="none" version="${simple_version}" format-version="2" generator-version="InstallCmds-681 (18F132)" install-location="/Applications" auth="root"> + <payload numberOfFiles="${numberOfFiles}" installKBytes="${installKBytes}"/> + <bundle path="./${app_name}.app" id="${CFBundleIdentifier}" CFBundleShortVersionString="${CFBundleShortVersionString}" CFBundleVersion="${CFBundleVersion}"/> + <bundle-version> + <bundle id="${CFBundleIdentifier}"/> + </bundle-version> + <upgrade-bundle> + <bundle id="${CFBundleIdentifier}"/> + </upgrade-bundle> + <update-bundle/> + <atomic-update-bundle/> + <strict-identifier> + <bundle id="${CFBundleIdentifier}"/> + </strict-identifier> + <relocate> + <bundle id="${CFBundleIdentifier}"/> + </relocate> +</pkg-info>
\ No newline at end of file diff --git a/python/mozbuild/mozpack/archive.py b/python/mozbuild/mozpack/archive.py new file mode 100644 index 0000000000..89bf14b179 --- /dev/null +++ b/python/mozbuild/mozpack/archive.py @@ -0,0 +1,153 @@ +# 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/. + +import bz2 +import gzip +import stat +import tarfile + +from .files import BaseFile, File + +# 2016-01-01T00:00:00+0000 +DEFAULT_MTIME = 1451606400 + + +# Python 3.9 contains this change: +# https://github.com/python/cpython/commit/674935b8caf33e47c78f1b8e197b1b77a04992d2 +# which changes the output of tar creation compared to earlier versions. +# As this code is used to generate tar files that are meant to be deterministic +# across versions of python (specifically, it's used as part of computing the hash +# of docker images, which needs to be identical between CI (which uses python 3.8), +# and developer environments (using arbitrary versions of python, at this point, +# most probably more recent than 3.9)). +# What we do is subblass TarInfo so that if used on python >= 3.9, it reproduces the +# behavior from python < 3.9. +# Here's how it goes: +# - the behavior in python >= 3.9 is the same as python < 3.9 when the type encoded +# in the tarinfo is CHRTYPE or BLKTYPE. +# - the value of the type is only compared in the context of choosing which behavior +# to take +# - we replace the type with the same value (so that using the value has no changes) +# but that pretends to be the same as CHRTYPE so that the condition that enables the +# old behavior is taken. +class HackedType(bytes): + def __eq__(self, other): + if other == tarfile.CHRTYPE: + return True + return self == other + + +class TarInfo(tarfile.TarInfo): + @staticmethod + def _create_header(info, format, encoding, errors): + info["type"] = HackedType(info["type"]) + return tarfile.TarInfo._create_header(info, format, encoding, errors) + + +def create_tar_from_files(fp, files): + """Create a tar file deterministically. + + Receives a dict mapping names of files in the archive to local filesystem + paths or ``mozpack.files.BaseFile`` instances. + + The files will be archived and written to the passed file handle opened + for writing. + + Only regular files can be written. + + FUTURE accept a filename argument (or create APIs to write files) + """ + # The format is explicitly set to tarfile.GNU_FORMAT, because this default format + # has been changed in Python 3.8. + with tarfile.open( + name="", mode="w", fileobj=fp, dereference=True, format=tarfile.GNU_FORMAT + ) as tf: + for archive_path, f in sorted(files.items()): + if not isinstance(f, BaseFile): + f = File(f) + + ti = TarInfo(archive_path) + ti.mode = f.mode or 0o0644 + ti.type = tarfile.REGTYPE + + if not ti.isreg(): + raise ValueError("not a regular file: %s" % f) + + # Disallow setuid and setgid bits. This is an arbitrary restriction. + # However, since we set uid/gid to root:root, setuid and setgid + # would be a glaring security hole if the archive were + # uncompressed as root. + if ti.mode & (stat.S_ISUID | stat.S_ISGID): + raise ValueError("cannot add file with setuid or setgid set: " "%s" % f) + + # Set uid, gid, username, and group as deterministic values. + ti.uid = 0 + ti.gid = 0 + ti.uname = "" + ti.gname = "" + + # Set mtime to a constant value. + ti.mtime = DEFAULT_MTIME + + ti.size = f.size() + # tarfile wants to pass a size argument to read(). So just + # wrap/buffer in a proper file object interface. + tf.addfile(ti, f.open()) + + +def create_tar_gz_from_files(fp, files, filename=None, compresslevel=9): + """Create a tar.gz file deterministically from files. + + This is a glorified wrapper around ``create_tar_from_files`` that + adds gzip compression. + + The passed file handle should be opened for writing in binary mode. + When the function returns, all data has been written to the handle. + """ + # Offset 3-7 in the gzip header contains an mtime. Pin it to a known + # value so output is deterministic. + gf = gzip.GzipFile( + filename=filename or "", + mode="wb", + fileobj=fp, + compresslevel=compresslevel, + mtime=DEFAULT_MTIME, + ) + with gf: + create_tar_from_files(gf, files) + + +class _BZ2Proxy(object): + """File object that proxies writes to a bz2 compressor.""" + + def __init__(self, fp, compresslevel=9): + self.fp = fp + self.compressor = bz2.BZ2Compressor(compresslevel) + self.pos = 0 + + def tell(self): + return self.pos + + def write(self, data): + data = self.compressor.compress(data) + self.pos += len(data) + self.fp.write(data) + + def close(self): + data = self.compressor.flush() + self.pos += len(data) + self.fp.write(data) + + +def create_tar_bz2_from_files(fp, files, compresslevel=9): + """Create a tar.bz2 file deterministically from files. + + This is a glorified wrapper around ``create_tar_from_files`` that + adds bzip2 compression. + + This function is similar to ``create_tar_gzip_from_files()``. + """ + proxy = _BZ2Proxy(fp, compresslevel=compresslevel) + create_tar_from_files(proxy, files) + proxy.close() diff --git a/python/mozbuild/mozpack/chrome/__init__.py b/python/mozbuild/mozpack/chrome/__init__.py new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/python/mozbuild/mozpack/chrome/__init__.py diff --git a/python/mozbuild/mozpack/chrome/flags.py b/python/mozbuild/mozpack/chrome/flags.py new file mode 100644 index 0000000000..6b096c862a --- /dev/null +++ b/python/mozbuild/mozpack/chrome/flags.py @@ -0,0 +1,278 @@ +# 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/. + +import re +from collections import OrderedDict + +import six +from packaging.version import Version + +from mozpack.errors import errors + + +class Flag(object): + """ + Class for flags in manifest entries in the form: + "flag" (same as "flag=true") + "flag=yes|true|1" + "flag=no|false|0" + """ + + def __init__(self, name): + """ + Initialize a Flag with the given name. + """ + self.name = name + self.value = None + + def add_definition(self, definition): + """ + Add a flag value definition. Replaces any previously set value. + """ + if definition == self.name: + self.value = True + return + assert definition.startswith(self.name) + if definition[len(self.name)] != "=": + return errors.fatal("Malformed flag: %s" % definition) + value = definition[len(self.name) + 1 :] + if value in ("yes", "true", "1", "no", "false", "0"): + self.value = value + else: + return errors.fatal("Unknown value in: %s" % definition) + + def matches(self, value): + """ + Return whether the flag value matches the given value. The values + are canonicalized for comparison. + """ + if value in ("yes", "true", "1", True): + return self.value in ("yes", "true", "1", True) + if value in ("no", "false", "0", False): + return self.value in ("no", "false", "0", False, None) + raise RuntimeError("Invalid value: %s" % value) + + def __str__(self): + """ + Serialize the flag value in the same form given to the last + add_definition() call. + """ + if self.value is None: + return "" + if self.value is True: + return self.name + return "%s=%s" % (self.name, self.value) + + def __eq__(self, other): + return str(self) == other + + +class StringFlag(object): + """ + Class for string flags in manifest entries in the form: + "flag=string" + "flag!=string" + """ + + def __init__(self, name): + """ + Initialize a StringFlag with the given name. + """ + self.name = name + self.values = [] + + def add_definition(self, definition): + """ + Add a string flag definition. + """ + assert definition.startswith(self.name) + value = definition[len(self.name) :] + if value.startswith("="): + self.values.append(("==", value[1:])) + elif value.startswith("!="): + self.values.append(("!=", value[2:])) + else: + return errors.fatal("Malformed flag: %s" % definition) + + def matches(self, value): + """ + Return whether one of the string flag definitions matches the given + value. + For example, + + flag = StringFlag('foo') + flag.add_definition('foo!=bar') + flag.matches('bar') returns False + flag.matches('qux') returns True + flag = StringFlag('foo') + flag.add_definition('foo=bar') + flag.add_definition('foo=baz') + flag.matches('bar') returns True + flag.matches('baz') returns True + flag.matches('qux') returns False + """ + if not self.values: + return True + for comparison, val in self.values: + if eval("value %s val" % comparison): + return True + return False + + def __str__(self): + """ + Serialize the flag definitions in the same form given to each + add_definition() call. + """ + res = [] + for comparison, val in self.values: + if comparison == "==": + res.append("%s=%s" % (self.name, val)) + else: + res.append("%s!=%s" % (self.name, val)) + return " ".join(res) + + def __eq__(self, other): + return str(self) == other + + +class VersionFlag(object): + """ + Class for version flags in manifest entries in the form: + "flag=version" + "flag<=version" + "flag<version" + "flag>=version" + "flag>version" + """ + + def __init__(self, name): + """ + Initialize a VersionFlag with the given name. + """ + self.name = name + self.values = [] + + def add_definition(self, definition): + """ + Add a version flag definition. + """ + assert definition.startswith(self.name) + value = definition[len(self.name) :] + if value.startswith("="): + self.values.append(("==", Version(value[1:]))) + elif len(value) > 1 and value[0] in ["<", ">"]: + if value[1] == "=": + if len(value) < 3: + return errors.fatal("Malformed flag: %s" % definition) + self.values.append((value[0:2], Version(value[2:]))) + else: + self.values.append((value[0], Version(value[1:]))) + else: + return errors.fatal("Malformed flag: %s" % definition) + + def matches(self, value): + """ + Return whether one of the version flag definitions matches the given + value. + For example, + + flag = VersionFlag('foo') + flag.add_definition('foo>=1.0') + flag.matches('1.0') returns True + flag.matches('1.1') returns True + flag.matches('0.9') returns False + flag = VersionFlag('foo') + flag.add_definition('foo>=1.0') + flag.add_definition('foo<0.5') + flag.matches('0.4') returns True + flag.matches('1.0') returns True + flag.matches('0.6') returns False + """ + value = Version(value) + if not self.values: + return True + for comparison, val in self.values: + if eval("value %s val" % comparison): + return True + return False + + def __str__(self): + """ + Serialize the flag definitions in the same form given to each + add_definition() call. + """ + res = [] + for comparison, val in self.values: + if comparison == "==": + res.append("%s=%s" % (self.name, val)) + else: + res.append("%s%s%s" % (self.name, comparison, val)) + return " ".join(res) + + def __eq__(self, other): + return str(self) == other + + +class Flags(OrderedDict): + """ + Class to handle a set of flags definitions given on a single manifest + entry. + + """ + + FLAGS = { + "application": StringFlag, + "appversion": VersionFlag, + "platformversion": VersionFlag, + "contentaccessible": Flag, + "os": StringFlag, + "osversion": VersionFlag, + "abi": StringFlag, + "platform": Flag, + "xpcnativewrappers": Flag, + "tablet": Flag, + "process": StringFlag, + "backgroundtask": StringFlag, + } + RE = re.compile(r"([!<>=]+)") + + def __init__(self, *flags): + """ + Initialize a set of flags given in string form. + flags = Flags('contentaccessible=yes', 'appversion>=3.5') + """ + OrderedDict.__init__(self) + for f in flags: + name = self.RE.split(f) + name = name[0] + if name not in self.FLAGS: + errors.fatal("Unknown flag: %s" % name) + continue + if name not in self: + self[name] = self.FLAGS[name](name) + self[name].add_definition(f) + + def __str__(self): + """ + Serialize the set of flags. + """ + return " ".join(str(self[k]) for k in self) + + def match(self, **filter): + """ + Return whether the set of flags match the set of given filters. + flags = Flags('contentaccessible=yes', 'appversion>=3.5', + 'application=foo') + + flags.match(application='foo') returns True + flags.match(application='foo', appversion='3.5') returns True + flags.match(application='foo', appversion='3.0') returns False + + """ + for name, value in six.iteritems(filter): + if name not in self: + continue + if not self[name].matches(value): + return False + return True diff --git a/python/mozbuild/mozpack/chrome/manifest.py b/python/mozbuild/mozpack/chrome/manifest.py new file mode 100644 index 0000000000..14c11d4c1d --- /dev/null +++ b/python/mozbuild/mozpack/chrome/manifest.py @@ -0,0 +1,400 @@ +# 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/. + +import os +import re + +import six +from six.moves.urllib.parse import urlparse + +import mozpack.path as mozpath +from mozpack.chrome.flags import Flags +from mozpack.errors import errors + + +class ManifestEntry(object): + """ + Base class for all manifest entry types. + Subclasses may define the following class or member variables: + + - localized: indicates whether the manifest entry is used for localized + data. + - type: the manifest entry type (e.g. 'content' in + 'content global content/global/') + - allowed_flags: a set of flags allowed to be defined for the given + manifest entry type. + + A manifest entry is attached to a base path, defining where the manifest + entry is bound to, and that is used to find relative paths defined in + entries. + """ + + localized = False + type = None + allowed_flags = [ + "application", + "platformversion", + "os", + "osversion", + "abi", + "xpcnativewrappers", + "tablet", + "process", + "contentaccessible", + "backgroundtask", + ] + + def __init__(self, base, *flags): + """ + Initialize a manifest entry with the given base path and flags. + """ + self.base = base + self.flags = Flags(*flags) + if not all(f in self.allowed_flags for f in self.flags): + errors.fatal( + "%s unsupported for %s manifest entries" + % ( + ",".join(f for f in self.flags if f not in self.allowed_flags), + self.type, + ) + ) + + def serialize(self, *args): + """ + Serialize the manifest entry. + """ + entry = [self.type] + list(args) + flags = str(self.flags) + if flags: + entry.append(flags) + return " ".join(entry) + + def __eq__(self, other): + return self.base == other.base and str(self) == str(other) + + def __ne__(self, other): + return not self.__eq__(other) + + def __repr__(self): + return "<%s@%s>" % (str(self), self.base) + + def move(self, base): + """ + Return a new manifest entry with a different base path. + """ + return parse_manifest_line(base, str(self)) + + def rebase(self, base): + """ + Return a new manifest entry with all relative paths defined in the + entry relative to a new base directory. + The base class doesn't define relative paths, so it is equivalent to + move(). + """ + return self.move(base) + + +class ManifestEntryWithRelPath(ManifestEntry): + """ + Abstract manifest entry type with a relative path definition. + """ + + def __init__(self, base, relpath, *flags): + ManifestEntry.__init__(self, base, *flags) + self.relpath = relpath + + def __str__(self): + return self.serialize(self.relpath) + + def rebase(self, base): + """ + Return a new manifest entry with all relative paths defined in the + entry relative to a new base directory. + """ + clone = ManifestEntry.rebase(self, base) + clone.relpath = mozpath.rebase(self.base, base, self.relpath) + return clone + + @property + def path(self): + return mozpath.normpath(mozpath.join(self.base, self.relpath)) + + +class Manifest(ManifestEntryWithRelPath): + """ + Class for 'manifest' entries. + manifest some/path/to/another.manifest + """ + + type = "manifest" + + +class ManifestChrome(ManifestEntryWithRelPath): + """ + Abstract class for chrome entries. + """ + + def __init__(self, base, name, relpath, *flags): + ManifestEntryWithRelPath.__init__(self, base, relpath, *flags) + self.name = name + + @property + def location(self): + return mozpath.join(self.base, self.relpath) + + +class ManifestContent(ManifestChrome): + """ + Class for 'content' entries. + content global content/global/ + """ + + type = "content" + allowed_flags = ManifestChrome.allowed_flags + [ + "contentaccessible", + "platform", + ] + + def __str__(self): + return self.serialize(self.name, self.relpath) + + +class ManifestMultiContent(ManifestChrome): + """ + Abstract class for chrome entries with multiple definitions. + Used for locale and skin entries. + """ + + type = None + + def __init__(self, base, name, id, relpath, *flags): + ManifestChrome.__init__(self, base, name, relpath, *flags) + self.id = id + + def __str__(self): + return self.serialize(self.name, self.id, self.relpath) + + +class ManifestLocale(ManifestMultiContent): + """ + Class for 'locale' entries. + locale global en-US content/en-US/ + locale global fr content/fr/ + """ + + localized = True + type = "locale" + + +class ManifestSkin(ManifestMultiContent): + """ + Class for 'skin' entries. + skin global classic/1.0 content/skin/classic/ + """ + + type = "skin" + + +class ManifestOverload(ManifestEntry): + """ + Abstract class for chrome entries defining some kind of overloading. + Used for overlay, override or style entries. + """ + + type = None + + def __init__(self, base, overloaded, overload, *flags): + ManifestEntry.__init__(self, base, *flags) + self.overloaded = overloaded + self.overload = overload + + def __str__(self): + return self.serialize(self.overloaded, self.overload) + + +class ManifestOverlay(ManifestOverload): + """ + Class for 'overlay' entries. + overlay chrome://global/content/viewSource.xul \ + chrome://browser/content/viewSourceOverlay.xul + """ + + type = "overlay" + + +class ManifestStyle(ManifestOverload): + """ + Class for 'style' entries. + style chrome://global/content/viewSource.xul \ + chrome://browser/skin/ + """ + + type = "style" + + +class ManifestOverride(ManifestOverload): + """ + Class for 'override' entries. + override chrome://global/locale/netError.dtd \ + chrome://browser/locale/netError.dtd + """ + + type = "override" + + +class ManifestResource(ManifestEntry): + """ + Class for 'resource' entries. + resource gre-resources toolkit/res/ + resource services-sync resource://gre/modules/services-sync/ + + The target may be a relative path or a resource or chrome url. + """ + + type = "resource" + + def __init__(self, base, name, target, *flags): + ManifestEntry.__init__(self, base, *flags) + self.name = name + self.target = target + + def __str__(self): + return self.serialize(self.name, self.target) + + def rebase(self, base): + u = urlparse(self.target) + if u.scheme and u.scheme != "jar": + return ManifestEntry.rebase(self, base) + clone = ManifestEntry.rebase(self, base) + clone.target = mozpath.rebase(self.base, base, self.target) + return clone + + +class ManifestBinaryComponent(ManifestEntryWithRelPath): + """ + Class for 'binary-component' entries. + binary-component some/path/to/a/component.dll + """ + + type = "binary-component" + + +class ManifestComponent(ManifestEntryWithRelPath): + """ + Class for 'component' entries. + component {b2bba4df-057d-41ea-b6b1-94a10a8ede68} foo.js + """ + + type = "component" + + def __init__(self, base, cid, file, *flags): + ManifestEntryWithRelPath.__init__(self, base, file, *flags) + self.cid = cid + + def __str__(self): + return self.serialize(self.cid, self.relpath) + + +class ManifestInterfaces(ManifestEntryWithRelPath): + """ + Class for 'interfaces' entries. + interfaces foo.xpt + """ + + type = "interfaces" + + +class ManifestCategory(ManifestEntry): + """ + Class for 'category' entries. + category command-line-handler m-browser @mozilla.org/browser/clh; + """ + + type = "category" + + def __init__(self, base, category, name, value, *flags): + ManifestEntry.__init__(self, base, *flags) + self.category = category + self.name = name + self.value = value + + def __str__(self): + return self.serialize(self.category, self.name, self.value) + + +class ManifestContract(ManifestEntry): + """ + Class for 'contract' entries. + contract @mozilla.org/foo;1 {b2bba4df-057d-41ea-b6b1-94a10a8ede68} + """ + + type = "contract" + + def __init__(self, base, contractID, cid, *flags): + ManifestEntry.__init__(self, base, *flags) + self.contractID = contractID + self.cid = cid + + def __str__(self): + return self.serialize(self.contractID, self.cid) + + +# All manifest classes by their type name. +MANIFESTS_TYPES = dict( + [ + (c.type, c) + for c in globals().values() + if type(c) == type + and issubclass(c, ManifestEntry) + and hasattr(c, "type") + and c.type + ] +) + +MANIFEST_RE = re.compile(r"^#.*$") + + +def parse_manifest_line(base, line): + """ + Parse a line from a manifest file with the given base directory and + return the corresponding ManifestEntry instance. + """ + # Remove comments + cmd = MANIFEST_RE.sub("", line).strip().split() + if not cmd: + return None + if not cmd[0] in MANIFESTS_TYPES: + return errors.fatal("Unknown manifest directive: %s" % cmd[0]) + return MANIFESTS_TYPES[cmd[0]](base, *cmd[1:]) + + +def parse_manifest(root, path, fileobj=None): + """ + Parse a manifest file. + """ + base = mozpath.dirname(path) + if root: + path = os.path.normpath(os.path.abspath(os.path.join(root, path))) + if not fileobj: + fileobj = open(path) + linenum = 0 + for line in fileobj: + line = six.ensure_text(line) + linenum += 1 + with errors.context(path, linenum): + e = parse_manifest_line(base, line) + if e: + yield e + + +def is_manifest(path): + """ + Return whether the given path is that of a manifest file. + """ + return ( + path.endswith(".manifest") + and not path.endswith(".CRT.manifest") + and not path.endswith(".exe.manifest") + and os.path.basename(path) != "cose.manifest" + ) diff --git a/python/mozbuild/mozpack/copier.py b/python/mozbuild/mozpack/copier.py new file mode 100644 index 0000000000..c042e5432f --- /dev/null +++ b/python/mozbuild/mozpack/copier.py @@ -0,0 +1,605 @@ +# 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/. + +import concurrent.futures as futures +import errno +import os +import stat +import sys +from collections import Counter, OrderedDict, defaultdict + +import six + +import mozpack.path as mozpath +from mozpack.errors import errors +from mozpack.files import BaseFile, Dest + + +class FileRegistry(object): + """ + Generic container to keep track of a set of BaseFile instances. It + preserves the order under which the files are added, but doesn't keep + track of empty directories (directories are not stored at all). + The paths associated with the BaseFile instances are relative to an + unspecified (virtual) root directory. + + registry = FileRegistry() + registry.add('foo/bar', file_instance) + """ + + def __init__(self): + self._files = OrderedDict() + self._required_directories = Counter() + self._partial_paths_cache = {} + + def _partial_paths(self, path): + """ + Turn "foo/bar/baz/zot" into ["foo/bar/baz", "foo/bar", "foo"]. + """ + dir_name = path.rpartition("/")[0] + if not dir_name: + return [] + + partial_paths = self._partial_paths_cache.get(dir_name) + if partial_paths: + return partial_paths + + partial_paths = [dir_name] + self._partial_paths(dir_name) + + self._partial_paths_cache[dir_name] = partial_paths + return partial_paths + + def add(self, path, content): + """ + Add a BaseFile instance to the container, under the given path. + """ + assert isinstance(content, BaseFile) + if path in self._files: + return errors.error("%s already added" % path) + if self._required_directories[path] > 0: + return errors.error("Can't add %s: it is a required directory" % path) + # Check whether any parent of the given path is already stored + partial_paths = self._partial_paths(path) + for partial_path in partial_paths: + if partial_path in self._files: + return errors.error("Can't add %s: %s is a file" % (path, partial_path)) + self._files[path] = content + self._required_directories.update(partial_paths) + + def match(self, pattern): + """ + Return the list of paths, stored in the container, matching the + given pattern. See the mozpack.path.match documentation for a + description of the handled patterns. + """ + if "*" in pattern: + return [p for p in self.paths() if mozpath.match(p, pattern)] + if pattern == "": + return self.paths() + if pattern in self._files: + return [pattern] + return [p for p in self.paths() if mozpath.basedir(p, [pattern]) == pattern] + + def remove(self, pattern): + """ + Remove paths matching the given pattern from the container. See the + mozpack.path.match documentation for a description of the handled + patterns. + """ + items = self.match(pattern) + if not items: + return errors.error( + "Can't remove %s: %s" + % (pattern, "not matching anything previously added") + ) + for i in items: + del self._files[i] + self._required_directories.subtract(self._partial_paths(i)) + + def paths(self): + """ + Return all paths stored in the container, in the order they were added. + """ + return list(self._files) + + def __len__(self): + """ + Return number of paths stored in the container. + """ + return len(self._files) + + def __contains__(self, pattern): + raise RuntimeError( + "'in' operator forbidden for %s. Use contains()." % self.__class__.__name__ + ) + + def contains(self, pattern): + """ + Return whether the container contains paths matching the given + pattern. See the mozpack.path.match documentation for a description of + the handled patterns. + """ + return len(self.match(pattern)) > 0 + + def __getitem__(self, path): + """ + Return the BaseFile instance stored in the container for the given + path. + """ + return self._files[path] + + def __iter__(self): + """ + Iterate over all (path, BaseFile instance) pairs from the container. + for path, file in registry: + (...) + """ + return six.iteritems(self._files) + + def required_directories(self): + """ + Return the set of directories required by the paths in the container, + in no particular order. The returned directories are relative to an + unspecified (virtual) root directory (and do not include said root + directory). + """ + return set(k for k, v in self._required_directories.items() if v > 0) + + def output_to_inputs_tree(self): + """ + Return a dictionary mapping each output path to the set of its + required input paths. + + All paths are normalized. + """ + tree = {} + for output, file in self: + output = mozpath.normpath(output) + tree[output] = set(mozpath.normpath(f) for f in file.inputs()) + return tree + + def input_to_outputs_tree(self): + """ + Return a dictionary mapping each input path to the set of + impacted output paths. + + All paths are normalized. + """ + tree = defaultdict(set) + for output, file in self: + output = mozpath.normpath(output) + for input in file.inputs(): + input = mozpath.normpath(input) + tree[input].add(output) + return dict(tree) + + +class FileRegistrySubtree(object): + """A proxy class to give access to a subtree of an existing FileRegistry. + + Note this doesn't implement the whole FileRegistry interface.""" + + def __new__(cls, base, registry): + if not base: + return registry + return object.__new__(cls) + + def __init__(self, base, registry): + self._base = base + self._registry = registry + + def _get_path(self, path): + # mozpath.join will return a trailing slash if path is empty, and we + # don't want that. + return mozpath.join(self._base, path) if path else self._base + + def add(self, path, content): + return self._registry.add(self._get_path(path), content) + + def match(self, pattern): + return [ + mozpath.relpath(p, self._base) + for p in self._registry.match(self._get_path(pattern)) + ] + + def remove(self, pattern): + return self._registry.remove(self._get_path(pattern)) + + def paths(self): + return [p for p, f in self] + + def __len__(self): + return len(self.paths()) + + def contains(self, pattern): + return self._registry.contains(self._get_path(pattern)) + + def __getitem__(self, path): + return self._registry[self._get_path(path)] + + def __iter__(self): + for p, f in self._registry: + if mozpath.basedir(p, [self._base]): + yield mozpath.relpath(p, self._base), f + + +class FileCopyResult(object): + """Represents results of a FileCopier.copy operation.""" + + def __init__(self): + self.updated_files = set() + self.existing_files = set() + self.removed_files = set() + self.removed_directories = set() + + @property + def updated_files_count(self): + return len(self.updated_files) + + @property + def existing_files_count(self): + return len(self.existing_files) + + @property + def removed_files_count(self): + return len(self.removed_files) + + @property + def removed_directories_count(self): + return len(self.removed_directories) + + +class FileCopier(FileRegistry): + """ + FileRegistry with the ability to copy the registered files to a separate + directory. + """ + + def copy( + self, + destination, + skip_if_older=True, + remove_unaccounted=True, + remove_all_directory_symlinks=True, + remove_empty_directories=True, + ): + """ + Copy all registered files to the given destination path. The given + destination can be an existing directory, or not exist at all. It + can't be e.g. a file. + The copy process acts a bit like rsync: files are not copied when they + don't need to (see mozpack.files for details on file.copy). + + By default, files in the destination directory that aren't + registered are removed and empty directories are deleted. In + addition, all directory symlinks in the destination directory + are deleted: this is a conservative approach to ensure that we + never accidently write files into a directory that is not the + destination directory. In the worst case, we might have a + directory symlink in the object directory to the source + directory. + + To disable removing of unregistered files, pass + remove_unaccounted=False. To disable removing empty + directories, pass remove_empty_directories=False. In rare + cases, you might want to maintain directory symlinks in the + destination directory (at least those that are not required to + be regular directories): pass + remove_all_directory_symlinks=False. Exercise caution with + this flag: you almost certainly do not want to preserve + directory symlinks. + + Returns a FileCopyResult that details what changed. + """ + assert isinstance(destination, six.string_types) + assert not os.path.exists(destination) or os.path.isdir(destination) + + result = FileCopyResult() + have_symlinks = hasattr(os, "symlink") + destination = os.path.normpath(destination) + + # We create the destination directory specially. We can't do this as + # part of the loop doing mkdir() below because that loop munges + # symlinks and permissions and parent directories of the destination + # directory may have their own weird schema. The contract is we only + # manage children of destination, not its parents. + try: + os.makedirs(destination) + except OSError as e: + if e.errno != errno.EEXIST: + raise + + # Because we could be handling thousands of files, code in this + # function is optimized to minimize system calls. We prefer CPU time + # in Python over possibly I/O bound filesystem calls to stat() and + # friends. + + required_dirs = set([destination]) + required_dirs |= set( + os.path.normpath(os.path.join(destination, d)) + for d in self.required_directories() + ) + + # Ensure destination directories are in place and proper. + # + # The "proper" bit is important. We need to ensure that directories + # have appropriate permissions or we will be unable to discover + # and write files. Furthermore, we need to verify directories aren't + # symlinks. + # + # Symlinked directories (a symlink whose target is a directory) are + # incompatible with us because our manifest talks in terms of files, + # not directories. If we leave symlinked directories unchecked, we + # would blindly follow symlinks and this might confuse file + # installation. For example, if an existing directory is a symlink + # to directory X and we attempt to install a symlink in this directory + # to a file in directory X, we may create a recursive symlink! + for d in sorted(required_dirs, key=len): + try: + os.mkdir(d) + except OSError as error: + if error.errno != errno.EEXIST: + raise + + # We allow the destination to be a symlink because the caller + # is responsible for managing the destination and we assume + # they know what they are doing. + if have_symlinks and d != destination: + st = os.lstat(d) + if stat.S_ISLNK(st.st_mode): + # While we have remove_unaccounted, it doesn't apply + # to directory symlinks because if it did, our behavior + # could be very wrong. + os.remove(d) + os.mkdir(d) + + if not os.access(d, os.W_OK): + umask = os.umask(0o077) + os.umask(umask) + os.chmod(d, 0o777 & ~umask) + + if isinstance(remove_unaccounted, FileRegistry): + existing_files = set( + os.path.normpath(os.path.join(destination, p)) + for p in remove_unaccounted.paths() + ) + existing_dirs = set( + os.path.normpath(os.path.join(destination, p)) + for p in remove_unaccounted.required_directories() + ) + existing_dirs |= {os.path.normpath(destination)} + else: + # While we have remove_unaccounted, it doesn't apply to empty + # directories because it wouldn't make sense: an empty directory + # is empty, so removing it should have no effect. + existing_dirs = set() + existing_files = set() + for root, dirs, files in os.walk(destination): + # We need to perform the same symlink detection as above. + # os.walk() doesn't follow symlinks into directories by + # default, so we need to check dirs (we can't wait for root). + if have_symlinks: + filtered = [] + for d in dirs: + full = os.path.join(root, d) + st = os.lstat(full) + if stat.S_ISLNK(st.st_mode): + # This directory symlink is not a required + # directory: any such symlink would have been + # removed and a directory created above. + if remove_all_directory_symlinks: + os.remove(full) + result.removed_files.add(os.path.normpath(full)) + else: + existing_files.add(os.path.normpath(full)) + else: + filtered.append(d) + + dirs[:] = filtered + + existing_dirs.add(os.path.normpath(root)) + + for d in dirs: + existing_dirs.add(os.path.normpath(os.path.join(root, d))) + + for f in files: + existing_files.add(os.path.normpath(os.path.join(root, f))) + + # Now we reconcile the state of the world against what we want. + dest_files = set() + + # Install files. + # + # Creating/appending new files on Windows/NTFS is slow. So we use a + # thread pool to speed it up significantly. The performance of this + # loop is so critical to common build operations on Linux that the + # overhead of the thread pool is worth avoiding, so we have 2 code + # paths. We also employ a low water mark to prevent thread pool + # creation if number of files is too small to benefit. + copy_results = [] + if sys.platform == "win32" and len(self) > 100: + with futures.ThreadPoolExecutor(4) as e: + fs = [] + for p, f in self: + destfile = os.path.normpath(os.path.join(destination, p)) + fs.append((destfile, e.submit(f.copy, destfile, skip_if_older))) + + copy_results = [(path, f.result) for path, f in fs] + else: + for p, f in self: + destfile = os.path.normpath(os.path.join(destination, p)) + copy_results.append((destfile, f.copy(destfile, skip_if_older))) + + for destfile, copy_result in copy_results: + dest_files.add(destfile) + if copy_result: + result.updated_files.add(destfile) + else: + result.existing_files.add(destfile) + + # Remove files no longer accounted for. + if remove_unaccounted: + for f in existing_files - dest_files: + # Windows requires write access to remove files. + if os.name == "nt" and not os.access(f, os.W_OK): + # It doesn't matter what we set permissions to since we + # will remove this file shortly. + os.chmod(f, 0o600) + + os.remove(f) + result.removed_files.add(f) + + if not remove_empty_directories: + return result + + # Figure out which directories can be removed. This is complicated + # by the fact we optionally remove existing files. This would be easy + # if we walked the directory tree after installing files. But, we're + # trying to minimize system calls. + + # Start with the ideal set. + remove_dirs = existing_dirs - required_dirs + + # Then don't remove directories if we didn't remove unaccounted files + # and one of those files exists. + if not remove_unaccounted: + parents = set() + pathsep = os.path.sep + for f in existing_files: + path = f + while True: + # All the paths are normalized and relative by this point, + # so os.path.dirname would only do extra work. + dirname = path.rpartition(pathsep)[0] + if dirname in parents: + break + parents.add(dirname) + path = dirname + remove_dirs -= parents + + # Remove empty directories that aren't required. + for d in sorted(remove_dirs, key=len, reverse=True): + try: + try: + os.rmdir(d) + except OSError as e: + if e.errno in (errno.EPERM, errno.EACCES): + # Permissions may not allow deletion. So ensure write + # access is in place before attempting to rmdir again. + os.chmod(d, 0o700) + os.rmdir(d) + else: + raise + except OSError as e: + # If remove_unaccounted is a # FileRegistry, then we have a + # list of directories that may not be empty, so ignore rmdir + # ENOTEMPTY errors for them. + if ( + isinstance(remove_unaccounted, FileRegistry) + and e.errno == errno.ENOTEMPTY + ): + continue + raise + result.removed_directories.add(d) + + return result + + +class Jarrer(FileRegistry, BaseFile): + """ + FileRegistry with the ability to copy and pack the registered files as a + jar file. Also acts as a BaseFile instance, to be copied with a FileCopier. + """ + + def __init__(self, compress=True): + """ + Create a Jarrer instance. See mozpack.mozjar.JarWriter documentation + for details on the compress argument. + """ + self.compress = compress + self._preload = [] + self._compress_options = {} # Map path to compress boolean option. + FileRegistry.__init__(self) + + def add(self, path, content, compress=None): + FileRegistry.add(self, path, content) + if compress is not None: + self._compress_options[path] = compress + + def copy(self, dest, skip_if_older=True): + """ + Pack all registered files in the given destination jar. The given + destination jar may be a path to jar file, or a Dest instance for + a jar file. + If the destination jar file exists, its (compressed) contents are used + instead of the registered BaseFile instances when appropriate. + """ + + class DeflaterDest(Dest): + """ + Dest-like class, reading from a file-like object initially, but + switching to a Deflater object if written to. + + dest = DeflaterDest(original_file) + dest.read() # Reads original_file + dest.write(data) # Creates a Deflater and write data there + dest.read() # Re-opens the Deflater and reads from it + """ + + def __init__(self, orig=None, compress=True): + self.mode = None + self.deflater = orig + self.compress = compress + + def read(self, length=-1): + if self.mode != "r": + assert self.mode is None + self.mode = "r" + return self.deflater.read(length) + + def write(self, data): + if self.mode != "w": + from mozpack.mozjar import Deflater + + self.deflater = Deflater(self.compress) + self.mode = "w" + self.deflater.write(data) + + def exists(self): + return self.deflater is not None + + if isinstance(dest, six.string_types): + dest = Dest(dest) + assert isinstance(dest, Dest) + + from mozpack.mozjar import JarReader, JarWriter + + try: + old_jar = JarReader(fileobj=dest) + except Exception: + old_jar = [] + + old_contents = dict([(f.filename, f) for f in old_jar]) + + with JarWriter(fileobj=dest, compress=self.compress) as jar: + for path, file in self: + compress = self._compress_options.get(path, self.compress) + if path in old_contents: + deflater = DeflaterDest(old_contents[path], compress) + else: + deflater = DeflaterDest(compress=compress) + file.copy(deflater, skip_if_older) + jar.add(path, deflater.deflater, mode=file.mode, compress=compress) + if self._preload: + jar.preload(self._preload) + + def open(self): + raise RuntimeError("unsupported") + + def preload(self, paths): + """ + Add the given set of paths to the list of preloaded files. See + mozpack.mozjar.JarWriter documentation for details on jar preloading. + """ + self._preload.extend(paths) diff --git a/python/mozbuild/mozpack/dmg.py b/python/mozbuild/mozpack/dmg.py new file mode 100644 index 0000000000..4e094648fe --- /dev/null +++ b/python/mozbuild/mozpack/dmg.py @@ -0,0 +1,258 @@ +# 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/. + +import os +import platform +import shutil +import subprocess +from pathlib import Path +from typing import List + +import mozfile + +from mozbuild.util import ensureParentDir + +is_linux = platform.system() == "Linux" +is_osx = platform.system() == "Darwin" + + +def chmod(dir): + "Set permissions of DMG contents correctly" + subprocess.check_call(["chmod", "-R", "a+rX,a-st,u+w,go-w", dir]) + + +def rsync(source: Path, dest: Path): + "rsync the contents of directory source into directory dest" + # Ensure a trailing slash on directories so rsync copies the *contents* of source. + raw_source = str(source) + if source.is_dir(): + raw_source = str(source) + "/" + subprocess.check_call(["rsync", "-a", "--copy-unsafe-links", raw_source, dest]) + + +def set_folder_icon(dir: Path, tmpdir: Path, hfs_tool: Path = None): + "Set HFS attributes of dir to use a custom icon" + if is_linux: + hfs = tmpdir / "staged.hfs" + subprocess.check_call([hfs_tool, hfs, "attr", "/", "C"]) + elif is_osx: + subprocess.check_call(["SetFile", "-a", "C", dir]) + + +def generate_hfs_file( + stagedir: Path, tmpdir: Path, volume_name: str, mkfshfs_tool: Path +): + """ + When cross compiling, we zero fill an hfs file, that we will turn into + a DMG. To do so we test the size of the staged dir, and add some slight + padding to that. + """ + hfs = tmpdir / "staged.hfs" + output = subprocess.check_output(["du", "-s", stagedir]) + size = int(output.split()[0]) / 1000 # Get in MB + size = int(size * 1.02) # Bump the used size slightly larger. + # Setup a proper file sized out with zero's + subprocess.check_call( + [ + "dd", + "if=/dev/zero", + "of={}".format(hfs), + "bs=1M", + "count={}".format(size), + ] + ) + subprocess.check_call([mkfshfs_tool, "-v", volume_name, hfs]) + + +def create_app_symlink(stagedir: Path, tmpdir: Path, hfs_tool: Path = None): + """ + Make a symlink to /Applications. The symlink name is a space + so we don't have to localize it. The Applications folder icon + will be shown in Finder, which should be clear enough for users. + """ + if is_linux: + hfs = os.path.join(tmpdir, "staged.hfs") + subprocess.check_call([hfs_tool, hfs, "symlink", "/ ", "/Applications"]) + elif is_osx: + os.symlink("/Applications", stagedir / " ") + + +def create_dmg_from_staged( + stagedir: Path, + output_dmg: Path, + tmpdir: Path, + volume_name: str, + hfs_tool: Path = None, + dmg_tool: Path = None, + mkfshfs_tool: Path = None, + attribution_sentinel: str = None, +): + "Given a prepared directory stagedir, produce a DMG at output_dmg." + + if is_linux: + # The dmg tool doesn't create the destination directories, and silently + # returns success if the parent directory doesn't exist. + ensureParentDir(output_dmg) + hfs = os.path.join(tmpdir, "staged.hfs") + subprocess.check_call([hfs_tool, hfs, "addall", stagedir]) + + dmg_cmd = [dmg_tool, "build", hfs, output_dmg] + if attribution_sentinel: + while len(attribution_sentinel) < 1024: + attribution_sentinel += "\t" + subprocess.check_call( + [ + hfs_tool, + hfs, + "setattr", + f"{volume_name}.app", + "com.apple.application-instance", + attribution_sentinel, + ] + ) + subprocess.check_call(["cp", hfs, str(Path(output_dmg).parent)]) + dmg_cmd.append(attribution_sentinel) + + subprocess.check_call( + dmg_cmd, + # dmg is seriously chatty + stdout=subprocess.DEVNULL, + ) + elif is_osx: + hybrid = tmpdir / "hybrid.dmg" + subprocess.check_call( + [ + "hdiutil", + "makehybrid", + "-hfs", + "-hfs-volume-name", + volume_name, + "-hfs-openfolder", + stagedir, + "-ov", + stagedir, + "-o", + hybrid, + ] + ) + subprocess.check_call( + [ + "hdiutil", + "convert", + "-format", + "UDBZ", + "-imagekey", + "bzip2-level=9", + "-ov", + hybrid, + "-o", + output_dmg, + ] + ) + + +def create_dmg( + source_directory: Path, + output_dmg: Path, + volume_name: str, + extra_files: List[tuple], + dmg_tool: Path, + hfs_tool: Path, + mkfshfs_tool: Path, + attribution_sentinel: str = None, +): + """ + Create a DMG disk image at the path output_dmg from source_directory. + + Use volume_name as the disk image volume name, and + use extra_files as a list of tuples of (filename, relative path) to copy + into the disk image. + """ + if platform.system() not in ("Darwin", "Linux"): + raise Exception("Don't know how to build a DMG on '%s'" % platform.system()) + + with mozfile.TemporaryDirectory() as tmp: + tmpdir = Path(tmp) + stagedir = tmpdir / "stage" + stagedir.mkdir() + + # Copy the app bundle over using rsync + rsync(source_directory, stagedir) + # Copy extra files + for source, target in extra_files: + full_target = stagedir / target + full_target.parent.mkdir(parents=True, exist_ok=True) + shutil.copyfile(source, full_target) + if is_linux: + # Not needed in osx + generate_hfs_file(stagedir, tmpdir, volume_name, mkfshfs_tool) + create_app_symlink(stagedir, tmpdir, hfs_tool) + # Set the folder attributes to use a custom icon + set_folder_icon(stagedir, tmpdir, hfs_tool) + chmod(stagedir) + create_dmg_from_staged( + stagedir, + output_dmg, + tmpdir, + volume_name, + hfs_tool, + dmg_tool, + mkfshfs_tool, + attribution_sentinel, + ) + + +def extract_dmg_contents( + dmgfile: Path, + destdir: Path, + dmg_tool: Path = None, + hfs_tool: Path = None, +): + if is_linux: + with mozfile.TemporaryDirectory() as tmpdir: + hfs_file = os.path.join(tmpdir, "firefox.hfs") + subprocess.check_call( + [dmg_tool, "extract", dmgfile, hfs_file], + # dmg is seriously chatty + stdout=subprocess.DEVNULL, + ) + subprocess.check_call([hfs_tool, hfs_file, "extractall", "/", destdir]) + else: + # TODO: find better way to resolve topsrcdir (checkout directory) + topsrcdir = Path(__file__).parent.parent.parent.parent.resolve() + unpack_diskimage = topsrcdir / "build/package/mac_osx/unpack-diskimage" + unpack_mountpoint = Path("/tmp/app-unpack") + subprocess.check_call([unpack_diskimage, dmgfile, unpack_mountpoint, destdir]) + + +def extract_dmg( + dmgfile: Path, + output: Path, + dmg_tool: Path = None, + hfs_tool: Path = None, + dsstore: Path = None, + icon: Path = None, + background: Path = None, +): + if platform.system() not in ("Darwin", "Linux"): + raise Exception("Don't know how to extract a DMG on '%s'" % platform.system()) + + with mozfile.TemporaryDirectory() as tmp: + tmpdir = Path(tmp) + extract_dmg_contents(dmgfile, tmpdir, dmg_tool, hfs_tool) + applications_symlink = tmpdir / " " + if applications_symlink.is_symlink(): + # Rsync will fail on the presence of this symlink + applications_symlink.unlink() + rsync(tmpdir, output) + + if dsstore: + dsstore.parent.mkdir(parents=True, exist_ok=True) + rsync(tmpdir / ".DS_Store", dsstore) + if background: + background.parent.mkdir(parents=True, exist_ok=True) + rsync(tmpdir / ".background" / background.name, background) + if icon: + icon.parent.mkdir(parents=True, exist_ok=True) + rsync(tmpdir / ".VolumeIcon.icns", icon) diff --git a/python/mozbuild/mozpack/errors.py b/python/mozbuild/mozpack/errors.py new file mode 100644 index 0000000000..25c0e8549c --- /dev/null +++ b/python/mozbuild/mozpack/errors.py @@ -0,0 +1,151 @@ +# 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/. + +import sys +from contextlib import contextmanager + + +class ErrorMessage(Exception): + """Exception type raised from errors.error() and errors.fatal()""" + + +class AccumulatedErrors(Exception): + """Exception type raised from errors.accumulate()""" + + +class ErrorCollector(object): + """ + Error handling/logging class. A global instance, errors, is provided for + convenience. + + Warnings, errors and fatal errors may be logged by calls to the following + functions: + - errors.warn(message) + - errors.error(message) + - errors.fatal(message) + + Warnings only send the message on the logging output, while errors and + fatal errors send the message and throw an ErrorMessage exception. The + exception, however, may be deferred. See further below. + + Errors may be ignored by calling: + - errors.ignore_errors() + + After calling that function, only fatal errors throw an exception. + + The warnings, errors or fatal errors messages may be augmented with context + information when a context is provided. Context is defined by a pair + (filename, linenumber), and may be set with errors.context() used as a + + context manager: + + .. code-block:: python + + with errors.context(filename, linenumber): + errors.warn(message) + + Arbitrary nesting is supported, both for errors.context calls: + + .. code-block:: python + + with errors.context(filename1, linenumber1): + errors.warn(message) + with errors.context(filename2, linenumber2): + errors.warn(message) + + as well as for function calls: + + .. code-block:: python + + def func(): + errors.warn(message) + with errors.context(filename, linenumber): + func() + + Errors and fatal errors can have their exception thrown at a later time, + allowing for several different errors to be reported at once before + throwing. This is achieved with errors.accumulate() as a context manager: + + .. code-block:: python + + with errors.accumulate(): + if test1: + errors.error(message1) + if test2: + errors.error(message2) + + In such cases, a single AccumulatedErrors exception is thrown, but doesn't + contain information about the exceptions. The logged messages do. + """ + + out = sys.stderr + WARN = 1 + ERROR = 2 + FATAL = 3 + _level = ERROR + _context = [] + _count = None + + def ignore_errors(self, ignore=True): + if ignore: + self._level = self.FATAL + else: + self._level = self.ERROR + + def _full_message(self, level, msg): + if level >= self._level: + level = "error" + else: + level = "warning" + if self._context: + file, line = self._context[-1] + return "%s: %s:%d: %s" % (level, file, line, msg) + return "%s: %s" % (level, msg) + + def _handle(self, level, msg): + msg = self._full_message(level, msg) + if level >= self._level: + if self._count is None: + raise ErrorMessage(msg) + self._count += 1 + print(msg, file=self.out) + + def fatal(self, msg): + self._handle(self.FATAL, msg) + + def error(self, msg): + self._handle(self.ERROR, msg) + + def warn(self, msg): + self._handle(self.WARN, msg) + + def get_context(self): + if self._context: + return self._context[-1] + + @contextmanager + def context(self, file, line): + if file and line: + self._context.append((file, line)) + yield + if file and line: + self._context.pop() + + @contextmanager + def accumulate(self): + assert self._count is None + self._count = 0 + yield + count = self._count + self._count = None + if count: + raise AccumulatedErrors() + + @property + def count(self): + # _count can be None. + return self._count if self._count else 0 + + +errors = ErrorCollector() diff --git a/python/mozbuild/mozpack/executables.py b/python/mozbuild/mozpack/executables.py new file mode 100644 index 0000000000..38f0902826 --- /dev/null +++ b/python/mozbuild/mozpack/executables.py @@ -0,0 +1,135 @@ +# 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/. + +import os +import struct +import subprocess +from io import BytesIO + +from mozpack.errors import errors + +MACHO_SIGNATURES = [ + 0xFEEDFACE, # mach-o 32-bits big endian + 0xCEFAEDFE, # mach-o 32-bits little endian + 0xFEEDFACF, # mach-o 64-bits big endian + 0xCFFAEDFE, # mach-o 64-bits little endian +] + +FAT_SIGNATURE = 0xCAFEBABE # mach-o FAT binary + +ELF_SIGNATURE = 0x7F454C46 # Elf binary + +UNKNOWN = 0 +MACHO = 1 +ELF = 2 + + +def get_type(path_or_fileobj): + """ + Check the signature of the give file and returns what kind of executable + matches. + """ + if hasattr(path_or_fileobj, "peek"): + f = BytesIO(path_or_fileobj.peek(8)) + elif hasattr(path_or_fileobj, "read"): + f = path_or_fileobj + else: + f = open(path_or_fileobj, "rb") + signature = f.read(4) + if len(signature) < 4: + return UNKNOWN + signature = struct.unpack(">L", signature)[0] + if signature == ELF_SIGNATURE: + return ELF + if signature in MACHO_SIGNATURES: + return MACHO + if signature != FAT_SIGNATURE: + return UNKNOWN + # We have to sanity check the second four bytes, because Java class + # files use the same magic number as Mach-O fat binaries. + # This logic is adapted from file(1), which says that Mach-O uses + # these bytes to count the number of architectures within, while + # Java uses it for a version number. Conveniently, there are only + # 18 labelled Mach-O architectures, and Java's first released + # class format used the version 43.0. + num = f.read(4) + if len(num) < 4: + return UNKNOWN + num = struct.unpack(">L", num)[0] + if num < 20: + return MACHO + return UNKNOWN + + +def is_executable(path): + """ + Return whether a given file path points to an executable or a library, + where an executable or library is identified by: + - the file extension on OS/2 and WINNT + - the file signature on OS/X and ELF systems (GNU/Linux, Android, BSD, Solaris) + + As this function is intended for use to choose between the ExecutableFile + and File classes in FileFinder, and choosing ExecutableFile only matters + on OS/2, OS/X, ELF and WINNT (in GCC build) systems, we don't bother + detecting other kind of executables. + """ + from buildconfig import substs + + if not os.path.exists(path): + return False + + if substs["OS_ARCH"] == "WINNT": + return path.lower().endswith((substs["DLL_SUFFIX"], substs["BIN_SUFFIX"])) + + return get_type(path) != UNKNOWN + + +def may_strip(path): + """ + Return whether strip() should be called + """ + from buildconfig import substs + + return bool(substs.get("PKG_STRIP")) + + +def strip(path): + """ + Execute the STRIP command with STRIP_FLAGS on the given path. + """ + from buildconfig import substs + + strip = substs["STRIP"] + flags = substs.get("STRIP_FLAGS", []) + cmd = [strip] + flags + [path] + if subprocess.call(cmd) != 0: + errors.fatal("Error executing " + " ".join(cmd)) + + +def may_elfhack(path): + """ + Return whether elfhack() should be called + """ + # elfhack only supports libraries. We should check the ELF header for + # the right flag, but checking the file extension works too. + from buildconfig import substs + + return ( + "USE_ELF_HACK" in substs + and substs["USE_ELF_HACK"] + and path.endswith(substs["DLL_SUFFIX"]) + and "COMPILE_ENVIRONMENT" in substs + and substs["COMPILE_ENVIRONMENT"] + ) + + +def elfhack(path): + """ + Execute the elfhack command on the given path. + """ + from buildconfig import topobjdir + + cmd = [os.path.join(topobjdir, "build/unix/elfhack/elfhack"), path] + if subprocess.call(cmd) != 0: + errors.fatal("Error executing " + " ".join(cmd)) diff --git a/python/mozbuild/mozpack/files.py b/python/mozbuild/mozpack/files.py new file mode 100644 index 0000000000..a320e1f4b4 --- /dev/null +++ b/python/mozbuild/mozpack/files.py @@ -0,0 +1,1271 @@ +# 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/. + +import bisect +import codecs +import errno +import inspect +import os +import platform +import shutil +import stat +import subprocess +import uuid +from collections import OrderedDict +from io import BytesIO +from itertools import chain, takewhile +from tarfile import TarFile, TarInfo +from tempfile import NamedTemporaryFile, mkstemp + +import six +from jsmin import JavascriptMinify + +import mozbuild.makeutil as makeutil +import mozpack.path as mozpath +from mozbuild.preprocessor import Preprocessor +from mozbuild.util import FileAvoidWrite, ensure_unicode, memoize +from mozpack.chrome.manifest import ManifestEntry, ManifestInterfaces +from mozpack.errors import ErrorMessage, errors +from mozpack.executables import elfhack, is_executable, may_elfhack, may_strip, strip +from mozpack.mozjar import JarReader + +try: + import hglib +except ImportError: + hglib = None + + +# For clean builds, copying files on win32 using CopyFile through ctypes is +# ~2x as fast as using shutil.copyfile. +if platform.system() != "Windows": + _copyfile = shutil.copyfile +else: + import ctypes + + _kernel32 = ctypes.windll.kernel32 + _CopyFileA = _kernel32.CopyFileA + _CopyFileW = _kernel32.CopyFileW + + def _copyfile(src, dest): + # False indicates `dest` should be overwritten if it exists already. + if isinstance(src, six.text_type) and isinstance(dest, six.text_type): + _CopyFileW(src, dest, False) + elif isinstance(src, str) and isinstance(dest, str): + _CopyFileA(src, dest, False) + else: + raise TypeError("mismatched path types!") + + +# Helper function; ensures we always open files with the correct encoding when +# opening them in text mode. +def _open(path, mode="r"): + if six.PY3 and "b" not in mode: + return open(path, mode, encoding="utf-8") + return open(path, mode) + + +class Dest(object): + """ + Helper interface for BaseFile.copy. The interface works as follows: + - read() and write() can be used to sequentially read/write from the underlying file. + - a call to read() after a write() will re-open the underlying file and read from it. + - a call to write() after a read() will re-open the underlying file, emptying it, and write to it. + """ + + def __init__(self, path): + self.file = None + self.mode = None + self.path = ensure_unicode(path) + + @property + def name(self): + return self.path + + def read(self, length=-1): + if self.mode != "r": + self.file = _open(self.path, mode="rb") + self.mode = "r" + return self.file.read(length) + + def write(self, data): + if self.mode != "w": + self.file = _open(self.path, mode="wb") + self.mode = "w" + to_write = six.ensure_binary(data) + return self.file.write(to_write) + + def exists(self): + return os.path.exists(self.path) + + def close(self): + if self.mode: + self.mode = None + self.file.close() + self.file = None + + +class BaseFile(object): + """ + Base interface and helper for file copying. Derived class may implement + their own copy function, or rely on BaseFile.copy using the open() member + function and/or the path property. + """ + + @staticmethod + def is_older(first, second): + """ + Compares the modification time of two files, and returns whether the + ``first`` file is older than the ``second`` file. + """ + # 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. + return int(os.path.getmtime(first) * 1000) <= int( + os.path.getmtime(second) * 1000 + ) + + @staticmethod + def any_newer(dest, inputs): + """ + Compares the modification time of ``dest`` to multiple input files, and + returns whether any of the ``inputs`` is newer (has a later mtime) than + ``dest``. + """ + # 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. + dest_mtime = int(os.path.getmtime(dest) * 1000) + for input in inputs: + try: + src_mtime = int(os.path.getmtime(input) * 1000) + except OSError as e: + if e.errno == errno.ENOENT: + # If an input file was removed, we should update. + return True + raise + if dest_mtime < src_mtime: + return True + return False + + @staticmethod + def normalize_mode(mode): + # Normalize file mode: + # - keep file type (e.g. S_IFREG) + ret = stat.S_IFMT(mode) + # - expand user read and execute permissions to everyone + if mode & 0o0400: + ret |= 0o0444 + if mode & 0o0100: + ret |= 0o0111 + # - keep user write permissions + if mode & 0o0200: + ret |= 0o0200 + # - leave away sticky bit, setuid, setgid + return ret + + def copy(self, dest, skip_if_older=True): + """ + Copy the BaseFile content to the destination given as a string or a + Dest instance. Avoids replacing existing files if the BaseFile content + matches that of the destination, or in case of plain files, if the + destination is newer than the original file. This latter behaviour is + disabled when skip_if_older is False. + Returns whether a copy was actually performed (True) or not (False). + """ + if isinstance(dest, six.string_types): + dest = Dest(dest) + else: + assert isinstance(dest, Dest) + + can_skip_content_check = False + if not dest.exists(): + can_skip_content_check = True + elif getattr(self, "path", None) and getattr(dest, "path", None): + if skip_if_older and BaseFile.is_older(self.path, dest.path): + return False + elif os.path.getsize(self.path) != os.path.getsize(dest.path): + can_skip_content_check = True + + if can_skip_content_check: + if getattr(self, "path", None) and getattr(dest, "path", None): + # The destination directory must exist, or CopyFile will fail. + destdir = os.path.dirname(dest.path) + try: + os.makedirs(destdir) + except OSError as e: + if e.errno != errno.EEXIST: + raise + _copyfile(self.path, dest.path) + shutil.copystat(self.path, dest.path) + else: + # Ensure the file is always created + if not dest.exists(): + dest.write(b"") + shutil.copyfileobj(self.open(), dest) + return True + + src = self.open() + accumulated_src_content = [] + while True: + dest_content = dest.read(32768) + src_content = src.read(32768) + accumulated_src_content.append(src_content) + if len(dest_content) == len(src_content) == 0: + break + # If the read content differs between origin and destination, + # write what was read up to now, and copy the remainder. + if six.ensure_binary(dest_content) != six.ensure_binary(src_content): + dest.write(b"".join(accumulated_src_content)) + shutil.copyfileobj(src, dest) + break + if hasattr(self, "path") and hasattr(dest, "path"): + shutil.copystat(self.path, dest.path) + return True + + def open(self): + """ + Return a file-like object allowing to read() the content of the + associated file. This is meant to be overloaded in subclasses to return + a custom file-like object. + """ + assert self.path is not None + return open(self.path, "rb") + + def read(self): + raise NotImplementedError("BaseFile.read() not implemented. Bug 1170329.") + + def size(self): + """Returns size of the entry. + + Derived classes are highly encouraged to override this with a more + optimal implementation. + """ + return len(self.read()) + + @property + def mode(self): + """ + Return the file's unix mode, or None if it has no meaning. + """ + return None + + def inputs(self): + """ + Return an iterable of the input file paths that impact this output file. + """ + raise NotImplementedError("BaseFile.inputs() not implemented.") + + +class File(BaseFile): + """ + File class for plain files. + """ + + def __init__(self, path): + self.path = ensure_unicode(path) + + @property + def mode(self): + """ + Return the file's unix mode, as returned by os.stat().st_mode. + """ + if platform.system() == "Windows": + return None + assert self.path is not None + mode = os.stat(self.path).st_mode + return self.normalize_mode(mode) + + def read(self): + """Return the contents of the file.""" + with open(self.path, "rb") as fh: + return fh.read() + + def size(self): + return os.stat(self.path).st_size + + def inputs(self): + return (self.path,) + + +class ExecutableFile(File): + """ + File class for executable and library files on OS/2, OS/X and ELF systems. + (see mozpack.executables.is_executable documentation). + """ + + def __init__(self, path): + File.__init__(self, path) + + def copy(self, dest, skip_if_older=True): + real_dest = dest + if not isinstance(dest, six.string_types): + fd, dest = mkstemp() + os.close(fd) + os.remove(dest) + assert isinstance(dest, six.string_types) + # If File.copy didn't actually copy because dest is newer, check the + # file sizes. If dest is smaller, it means it is already stripped and + # elfhacked, so we can skip. + if not File.copy(self, dest, skip_if_older) and os.path.getsize( + self.path + ) > os.path.getsize(dest): + return False + try: + if may_strip(dest): + strip(dest) + if may_elfhack(dest): + elfhack(dest) + except ErrorMessage: + os.remove(dest) + raise + + if real_dest != dest: + f = File(dest) + ret = f.copy(real_dest, skip_if_older) + os.remove(dest) + return ret + return True + + +class AbsoluteSymlinkFile(File): + """File class that is copied by symlinking (if available). + + This class only works if the target path is absolute. + """ + + def __init__(self, path): + if not os.path.isabs(path): + raise ValueError("Symlink target not absolute: %s" % path) + + File.__init__(self, path) + + def copy(self, dest, skip_if_older=True): + assert isinstance(dest, six.string_types) + + # The logic in this function is complicated by the fact that symlinks + # aren't universally supported. So, where symlinks aren't supported, we + # fall back to file copying. Keep in mind that symlink support is + # per-filesystem, not per-OS. + + # Handle the simple case where symlinks are definitely not supported by + # falling back to file copy. + if not hasattr(os, "symlink"): + return File.copy(self, dest, skip_if_older=skip_if_older) + + # Always verify the symlink target path exists. + if not os.path.exists(self.path): + errors.fatal("Symlink target path does not exist: %s" % self.path) + + st = None + + try: + st = os.lstat(dest) + except OSError as ose: + if ose.errno != errno.ENOENT: + raise + + # If the dest is a symlink pointing to us, we have nothing to do. + # If it's the wrong symlink, the filesystem must support symlinks, + # so we replace with a proper symlink. + if st and stat.S_ISLNK(st.st_mode): + link = os.readlink(dest) + if link == self.path: + return False + + os.remove(dest) + os.symlink(self.path, dest) + return True + + # If the destination doesn't exist, we try to create a symlink. If that + # fails, we fall back to copy code. + if not st: + try: + os.symlink(self.path, dest) + return True + except OSError: + return File.copy(self, dest, skip_if_older=skip_if_older) + + # Now the complicated part. If the destination exists, we could be + # replacing a file with a symlink. Or, the filesystem may not support + # symlinks. We want to minimize I/O overhead for performance reasons, + # so we keep the existing destination file around as long as possible. + # A lot of the system calls would be eliminated if we cached whether + # symlinks are supported. However, even if we performed a single + # up-front test of whether the root of the destination directory + # supports symlinks, there's no guarantee that all operations for that + # dest (or source) would be on the same filesystem and would support + # symlinks. + # + # Our strategy is to attempt to create a new symlink with a random + # name. If that fails, we fall back to copy mode. If that works, we + # remove the old destination and move the newly-created symlink into + # its place. + + temp_dest = os.path.join(os.path.dirname(dest), str(uuid.uuid4())) + try: + os.symlink(self.path, temp_dest) + # TODO Figure out exactly how symlink creation fails and only trap + # that. + except EnvironmentError: + return File.copy(self, dest, skip_if_older=skip_if_older) + + # If removing the original file fails, don't forget to clean up the + # temporary symlink. + try: + os.remove(dest) + except EnvironmentError: + os.remove(temp_dest) + raise + + os.rename(temp_dest, dest) + return True + + +class HardlinkFile(File): + """File class that is copied by hard linking (if available) + + This is similar to the AbsoluteSymlinkFile, but with hard links. The symlink + implementation requires paths to be absolute, because they are resolved at + read time, which makes relative paths messy. Hard links resolve paths at + link-creation time, so relative paths are fine. + """ + + def copy(self, dest, skip_if_older=True): + assert isinstance(dest, six.string_types) + + if not hasattr(os, "link"): + return super(HardlinkFile, self).copy(dest, skip_if_older=skip_if_older) + + try: + path_st = os.stat(self.path) + except OSError as e: + if e.errno == errno.ENOENT: + errors.fatal("Hard link target path does not exist: %s" % self.path) + else: + raise + + st = None + try: + st = os.lstat(dest) + except OSError as e: + if e.errno != errno.ENOENT: + raise + + if st: + # The dest already points to the right place. + if st.st_dev == path_st.st_dev and st.st_ino == path_st.st_ino: + return False + # The dest exists and it points to the wrong place + os.remove(dest) + + # At this point, either the dest used to exist and we just deleted it, + # or it never existed. We can now safely create the hard link. + try: + os.link(self.path, dest) + except OSError: + # If we can't hard link, fall back to copying + return super(HardlinkFile, self).copy(dest, skip_if_older=skip_if_older) + return True + + +class ExistingFile(BaseFile): + """ + File class that represents a file that may exist but whose content comes + from elsewhere. + + This purpose of this class is to account for files that are installed via + external means. It is typically only used in manifests or in registries to + account for files. + + When asked to copy, this class does nothing because nothing is known about + the source file/data. + + Instances of this class come in two flavors: required and optional. If an + existing file is required, it must exist during copy() or an error is + raised. + """ + + def __init__(self, required): + self.required = required + + def copy(self, dest, skip_if_older=True): + if isinstance(dest, six.string_types): + dest = Dest(dest) + else: + assert isinstance(dest, Dest) + + if not self.required: + return + + if not dest.exists(): + errors.fatal("Required existing file doesn't exist: %s" % dest.path) + + def inputs(self): + return () + + +class PreprocessedFile(BaseFile): + """ + File class for a file that is preprocessed. PreprocessedFile.copy() runs + the preprocessor on the file to create the output. + """ + + def __init__( + self, + path, + depfile_path, + marker, + defines, + extra_depends=None, + silence_missing_directive_warnings=False, + ): + self.path = ensure_unicode(path) + self.depfile = ensure_unicode(depfile_path) + self.marker = marker + self.defines = defines + self.extra_depends = list(extra_depends or []) + self.silence_missing_directive_warnings = silence_missing_directive_warnings + + def inputs(self): + pp = Preprocessor(defines=self.defines, marker=self.marker) + pp.setSilenceDirectiveWarnings(self.silence_missing_directive_warnings) + + with _open(self.path, "r") as input: + with _open(os.devnull, "w") as output: + pp.processFile(input=input, output=output) + + # This always yields at least self.path. + return pp.includes + + def copy(self, dest, skip_if_older=True): + """ + Invokes the preprocessor to create the destination file. + """ + if isinstance(dest, six.string_types): + dest = Dest(dest) + else: + assert isinstance(dest, Dest) + + # We have to account for the case where the destination exists and is a + # symlink to something. Since we know the preprocessor is certainly not + # going to create a symlink, we can just remove the existing one. If the + # destination is not a symlink, we leave it alone, since we're going to + # overwrite its contents anyway. + # If symlinks aren't supported at all, we can skip this step. + # See comment in AbsoluteSymlinkFile about Windows. + if hasattr(os, "symlink") and platform.system() != "Windows": + if os.path.islink(dest.path): + os.remove(dest.path) + + pp_deps = set(self.extra_depends) + + # If a dependency file was specified, and it exists, add any + # dependencies from that file to our list. + if self.depfile and os.path.exists(self.depfile): + target = mozpath.normpath(dest.name) + with _open(self.depfile, "rt") as fileobj: + for rule in makeutil.read_dep_makefile(fileobj): + if target in rule.targets(): + pp_deps.update(rule.dependencies()) + + skip = False + if dest.exists() and skip_if_older: + # If a dependency file was specified, and it doesn't exist, + # assume that the preprocessor needs to be rerun. That will + # regenerate the dependency file. + if self.depfile and not os.path.exists(self.depfile): + skip = False + else: + skip = not BaseFile.any_newer(dest.path, pp_deps) + + if skip: + return False + + deps_out = None + if self.depfile: + deps_out = FileAvoidWrite(self.depfile) + pp = Preprocessor(defines=self.defines, marker=self.marker) + pp.setSilenceDirectiveWarnings(self.silence_missing_directive_warnings) + + with _open(self.path, "r") as input: + pp.processFile(input=input, output=dest, depfile=deps_out) + + dest.close() + if self.depfile: + deps_out.close() + + return True + + +class GeneratedFile(BaseFile): + """ + File class for content with no previous existence on the filesystem. + """ + + def __init__(self, content): + self._content = content + + @property + def content(self): + if inspect.isfunction(self._content): + self._content = self._content() + return six.ensure_binary(self._content) + + @content.setter + def content(self, content): + self._content = content + + def open(self): + return BytesIO(self.content) + + def read(self): + return self.content + + def size(self): + return len(self.content) + + def inputs(self): + return () + + +class DeflatedFile(BaseFile): + """ + File class for members of a jar archive. DeflatedFile.copy() effectively + extracts the file from the jar archive. + """ + + def __init__(self, file): + from mozpack.mozjar import JarFileReader + + assert isinstance(file, JarFileReader) + self.file = file + + def open(self): + self.file.seek(0) + return self.file + + +class ExtractedTarFile(GeneratedFile): + """ + File class for members of a tar archive. Contents of the underlying file + are extracted immediately and stored in memory. + """ + + def __init__(self, tar, info): + assert isinstance(info, TarInfo) + assert isinstance(tar, TarFile) + GeneratedFile.__init__(self, tar.extractfile(info).read()) + self._unix_mode = self.normalize_mode(info.mode) + + @property + def mode(self): + return self._unix_mode + + def read(self): + return self.content + + +class ManifestFile(BaseFile): + """ + File class for a manifest file. It takes individual manifest entries (using + the add() and remove() member functions), and adjusts them to be relative + to the base path for the manifest, given at creation. + Example: + There is a manifest entry "content foobar foobar/content/" relative + to "foobar/chrome". When packaging, the entry will be stored in + jar:foobar/omni.ja!/chrome/chrome.manifest, which means the entry + will have to be relative to "chrome" instead of "foobar/chrome". This + doesn't really matter when serializing the entry, since this base path + is not written out, but it matters when moving the entry at the same + time, e.g. to jar:foobar/omni.ja!/chrome.manifest, which we don't do + currently but could in the future. + """ + + def __init__(self, base, entries=None): + self._base = base + self._entries = [] + self._interfaces = [] + for e in entries or []: + self.add(e) + + def add(self, entry): + """ + Add the given entry to the manifest. Entries are rebased at open() time + instead of add() time so that they can be more easily remove()d. + """ + assert isinstance(entry, ManifestEntry) + if isinstance(entry, ManifestInterfaces): + self._interfaces.append(entry) + else: + self._entries.append(entry) + + def remove(self, entry): + """ + Remove the given entry from the manifest. + """ + assert isinstance(entry, ManifestEntry) + if isinstance(entry, ManifestInterfaces): + self._interfaces.remove(entry) + else: + self._entries.remove(entry) + + def open(self): + """ + Return a file-like object allowing to read() the serialized content of + the manifest. + """ + content = "".join( + "%s\n" % e.rebase(self._base) + for e in chain(self._entries, self._interfaces) + ) + return BytesIO(six.ensure_binary(content)) + + def __iter__(self): + """ + Iterate over entries in the manifest file. + """ + return chain(self._entries, self._interfaces) + + def isempty(self): + """ + Return whether there are manifest entries to write + """ + return len(self._entries) + len(self._interfaces) == 0 + + +class MinifiedCommentStripped(BaseFile): + """ + File class for content minified by stripping comments. This wraps around a + BaseFile instance, and removes lines starting with a # from its content. + """ + + def __init__(self, file): + assert isinstance(file, BaseFile) + self._file = file + + def open(self): + """ + Return a file-like object allowing to read() the minified content of + the underlying file. + """ + content = "".join( + l + for l in [six.ensure_text(s) for s in self._file.open().readlines()] + if not l.startswith("#") + ) + return BytesIO(six.ensure_binary(content)) + + +class MinifiedJavaScript(BaseFile): + """ + File class for minifying JavaScript files. + """ + + def __init__(self, file, verify_command=None): + assert isinstance(file, BaseFile) + self._file = file + self._verify_command = verify_command + + def open(self): + output = six.StringIO() + minify = JavascriptMinify( + codecs.getreader("utf-8")(self._file.open()), output, quote_chars="'\"`" + ) + minify.minify() + output.seek(0) + output_source = six.ensure_binary(output.getvalue()) + output = BytesIO(output_source) + + if not self._verify_command: + return output + + input_source = self._file.open().read() + + with NamedTemporaryFile("wb+") as fh1, NamedTemporaryFile("wb+") as fh2: + fh1.write(input_source) + fh2.write(output_source) + fh1.flush() + fh2.flush() + + try: + args = list(self._verify_command) + args.extend([fh1.name, fh2.name]) + subprocess.check_output( + args, stderr=subprocess.STDOUT, universal_newlines=True + ) + except subprocess.CalledProcessError as e: + errors.warn( + "JS minification verification failed for %s:" + % (getattr(self._file, "path", "<unknown>")) + ) + # Prefix each line with "Warning:" so mozharness doesn't + # think these error messages are real errors. + for line in e.output.splitlines(): + errors.warn(line) + + return self._file.open() + + return output + + +class BaseFinder(object): + def __init__( + self, base, minify=False, minify_js=False, minify_js_verify_command=None + ): + """ + Initializes the instance with a reference base directory. + + The optional minify argument specifies whether minification of code + should occur. minify_js is an additional option to control minification + of JavaScript. It requires minify to be True. + + minify_js_verify_command can be used to optionally verify the results + of JavaScript minification. If defined, it is expected to be an iterable + that will constitute the first arguments to a called process which will + receive the filenames of the original and minified JavaScript files. + The invoked process can then verify the results. If minification is + rejected, the process exits with a non-0 exit code and the original + JavaScript source is used. An example value for this argument is + ('/path/to/js', '/path/to/verify/script.js'). + """ + if minify_js and not minify: + raise ValueError("minify_js requires minify.") + + self.base = mozpath.normsep(base) + self._minify = minify + self._minify_js = minify_js + self._minify_js_verify_command = minify_js_verify_command + + def find(self, pattern): + """ + Yield path, BaseFile_instance pairs for all files under the base + directory and its subdirectories that match the given pattern. See the + mozpack.path.match documentation for a description of the handled + patterns. + """ + while pattern.startswith("/"): + pattern = pattern[1:] + for p, f in self._find(pattern): + yield p, self._minify_file(p, f) + + def get(self, path): + """Obtain a single file. + + Where ``find`` is tailored towards matching multiple files, this method + is used for retrieving a single file. Use this method when performance + is critical. + + Returns a ``BaseFile`` if at most one file exists or ``None`` otherwise. + """ + files = list(self.find(path)) + if len(files) != 1: + return None + return files[0][1] + + def __iter__(self): + """ + Iterates over all files under the base directory (excluding files + starting with a '.' and files at any level under a directory starting + with a '.'). + for path, file in finder: + ... + """ + return self.find("") + + def __contains__(self, pattern): + raise RuntimeError( + "'in' operator forbidden for %s. Use contains()." % self.__class__.__name__ + ) + + def contains(self, pattern): + """ + Return whether some files under the base directory match the given + pattern. See the mozpack.path.match documentation for a description of + the handled patterns. + """ + return any(self.find(pattern)) + + def _minify_file(self, path, file): + """ + Return an appropriate MinifiedSomething wrapper for the given BaseFile + instance (file), according to the file type (determined by the given + path), if the FileFinder was created with minification enabled. + Otherwise, just return the given BaseFile instance. + """ + if not self._minify or isinstance(file, ExecutableFile): + return file + + if path.endswith((".ftl", ".properties")): + return MinifiedCommentStripped(file) + + if self._minify_js and path.endswith((".js", ".jsm")): + return MinifiedJavaScript(file, self._minify_js_verify_command) + + return file + + def _find_helper(self, pattern, files, file_getter): + """Generic implementation of _find. + + A few *Finder implementations share logic for returning results. + This function implements the custom logic. + + The ``file_getter`` argument is a callable that receives a path + that is known to exist. The callable should return a ``BaseFile`` + instance. + """ + if "*" in pattern: + for p in files: + if mozpath.match(p, pattern): + yield p, file_getter(p) + elif pattern == "": + for p in files: + yield p, file_getter(p) + elif pattern in files: + yield pattern, file_getter(pattern) + else: + for p in files: + if mozpath.basedir(p, [pattern]) == pattern: + yield p, file_getter(p) + + +class FileFinder(BaseFinder): + """ + Helper to get appropriate BaseFile instances from the file system. + """ + + def __init__( + self, + base, + find_executables=False, + ignore=(), + ignore_broken_symlinks=False, + find_dotfiles=False, + **kargs + ): + """ + Create a FileFinder for files under the given base directory. + + The find_executables argument determines whether the finder needs to + try to guess whether files are executables. Disabling this guessing + when not necessary can speed up the finder significantly. + + ``ignore`` accepts an iterable of patterns to ignore. Entries are + strings that match paths relative to ``base`` using + ``mozpath.match()``. This means if an entry corresponds + to a directory, all files under that directory will be ignored. If + an entry corresponds to a file, that particular file will be ignored. + ``ignore_broken_symlinks`` is passed by the packager to work around an + issue with the build system not cleaning up stale files in some common + cases. See bug 1297381. + """ + BaseFinder.__init__(self, base, **kargs) + self.find_dotfiles = find_dotfiles + self.find_executables = find_executables + self.ignore = tuple(mozpath.normsep(path) for path in ignore) + self.ignore_broken_symlinks = ignore_broken_symlinks + + def _find(self, pattern): + """ + Actual implementation of FileFinder.find(), dispatching to specialized + member functions depending on what kind of pattern was given. + Note all files with a name starting with a '.' are ignored when + scanning directories, but are not ignored when explicitely requested. + """ + if "*" in pattern: + return self._find_glob("", mozpath.split(pattern)) + elif os.path.isdir(os.path.join(self.base, pattern)): + return self._find_dir(pattern) + else: + f = self.get(pattern) + return ((pattern, f),) if f else () + + def _find_dir(self, path): + """ + Actual implementation of FileFinder.find() when the given pattern + corresponds to an existing directory under the base directory. + Ignores file names starting with a '.' under the given path. If the + path itself has leafs starting with a '.', they are not ignored. + """ + for p in self.ignore: + if mozpath.match(path, p): + return + + # The sorted makes the output idempotent. Otherwise, we are + # likely dependent on filesystem implementation details, such as + # inode ordering. + for p in sorted(os.listdir(os.path.join(self.base, path))): + if p.startswith("."): + if p in (".", ".."): + continue + if not self.find_dotfiles: + continue + for p_, f in self._find(mozpath.join(path, p)): + yield p_, f + + def get(self, path): + srcpath = os.path.join(self.base, path) + if not os.path.lexists(srcpath): + return None + + if self.ignore_broken_symlinks and not os.path.exists(srcpath): + return None + + for p in self.ignore: + if mozpath.match(path, p): + return None + + if self.find_executables and is_executable(srcpath): + return ExecutableFile(srcpath) + else: + return File(srcpath) + + def _find_glob(self, base, pattern): + """ + Actual implementation of FileFinder.find() when the given pattern + contains globbing patterns ('*' or '**'). This is meant to be an + equivalent of: + for p, f in self: + if mozpath.match(p, pattern): + yield p, f + but avoids scanning the entire tree. + """ + if not pattern: + for p, f in self._find(base): + yield p, f + elif pattern[0] == "**": + for p, f in self._find(base): + if mozpath.match(p, mozpath.join(*pattern)): + yield p, f + elif "*" in pattern[0]: + if not os.path.exists(os.path.join(self.base, base)): + return + + for p in self.ignore: + if mozpath.match(base, p): + return + + # See above comment w.r.t. sorted() and idempotent behavior. + for p in sorted(os.listdir(os.path.join(self.base, base))): + if p.startswith(".") and not pattern[0].startswith("."): + continue + if mozpath.match(p, pattern[0]): + for p_, f in self._find_glob(mozpath.join(base, p), pattern[1:]): + yield p_, f + else: + for p, f in self._find_glob(mozpath.join(base, pattern[0]), pattern[1:]): + yield p, f + + +class JarFinder(BaseFinder): + """ + Helper to get appropriate DeflatedFile instances from a JarReader. + """ + + def __init__(self, base, reader, **kargs): + """ + Create a JarFinder for files in the given JarReader. The base argument + is used as an indication of the Jar file location. + """ + assert isinstance(reader, JarReader) + BaseFinder.__init__(self, base, **kargs) + self._files = OrderedDict((f.filename, f) for f in reader) + + def _find(self, pattern): + """ + Actual implementation of JarFinder.find(), dispatching to specialized + member functions depending on what kind of pattern was given. + """ + return self._find_helper( + pattern, self._files, lambda x: DeflatedFile(self._files[x]) + ) + + +class TarFinder(BaseFinder): + """ + Helper to get files from a TarFile. + """ + + def __init__(self, base, tar, **kargs): + """ + Create a TarFinder for files in the given TarFile. The base argument + is used as an indication of the Tar file location. + """ + assert isinstance(tar, TarFile) + self._tar = tar + BaseFinder.__init__(self, base, **kargs) + self._files = OrderedDict((f.name, f) for f in tar if f.isfile()) + + def _find(self, pattern): + """ + Actual implementation of TarFinder.find(), dispatching to specialized + member functions depending on what kind of pattern was given. + """ + return self._find_helper( + pattern, self._files, lambda x: ExtractedTarFile(self._tar, self._files[x]) + ) + + +class ComposedFinder(BaseFinder): + """ + Composes multiple File Finders in some sort of virtual file system. + + A ComposedFinder is initialized from a dictionary associating paths + to `*Finder instances.` + + Note this could be optimized to be smarter than getting all the files + in advance. + """ + + def __init__(self, finders): + # Can't import globally, because of the dependency of mozpack.copier + # on this module. + from mozpack.copier import FileRegistry + + self.files = FileRegistry() + + for base, finder in sorted(six.iteritems(finders)): + if self.files.contains(base): + self.files.remove(base) + for p, f in finder.find(""): + self.files.add(mozpath.join(base, p), f) + + def find(self, pattern): + for p in self.files.match(pattern): + yield p, self.files[p] + + +class MercurialFile(BaseFile): + """File class for holding data from Mercurial.""" + + def __init__(self, client, rev, path): + self._content = client.cat( + [six.ensure_binary(path)], rev=six.ensure_binary(rev) + ) + + def open(self): + return BytesIO(six.ensure_binary(self._content)) + + def read(self): + return self._content + + +class MercurialRevisionFinder(BaseFinder): + """A finder that operates on a specific Mercurial revision.""" + + def __init__(self, repo, rev=".", recognize_repo_paths=False, **kwargs): + """Create a finder attached to a specific revision in a repository. + + If no revision is given, open the parent of the working directory. + + ``recognize_repo_paths`` will enable a mode where ``.get()`` will + recognize full paths that include the repo's path. Typically Finder + instances are "bound" to a base directory and paths are relative to + that directory. This mode changes that. When this mode is activated, + ``.find()`` will not work! This mode exists to support the moz.build + reader, which uses absolute paths instead of relative paths. The reader + should eventually be rewritten to use relative paths and this hack + should be removed (TODO bug 1171069). + """ + if not hglib: + raise Exception("hglib package not found") + + super(MercurialRevisionFinder, self).__init__(base=repo, **kwargs) + + self._root = mozpath.normpath(repo).rstrip("/") + self._recognize_repo_paths = recognize_repo_paths + + # We change directories here otherwise we have to deal with relative + # paths. + oldcwd = os.getcwd() + os.chdir(self._root) + try: + self._client = hglib.open(path=repo, encoding=b"utf-8") + finally: + os.chdir(oldcwd) + self._rev = rev if rev is not None else "." + self._files = OrderedDict() + + # Immediately populate the list of files in the repo since nearly every + # operation requires this list. + out = self._client.rawcommand( + [ + b"files", + b"--rev", + six.ensure_binary(self._rev), + ] + ) + for relpath in out.splitlines(): + # Mercurial may use \ as path separator on Windows. So use + # normpath(). + self._files[six.ensure_text(mozpath.normpath(relpath))] = None + + def _find(self, pattern): + if self._recognize_repo_paths: + raise NotImplementedError("cannot use find with recognize_repo_path") + + return self._find_helper(pattern, self._files, self._get) + + def get(self, path): + path = mozpath.normpath(path) + if self._recognize_repo_paths: + if not path.startswith(self._root): + raise ValueError( + "lookups in recognize_repo_paths mode must be " + "prefixed with repo path: %s" % path + ) + path = path[len(self._root) + 1 :] + + try: + return self._get(path) + except KeyError: + return None + + def _get(self, path): + # We lazy populate self._files because potentially creating tens of + # thousands of MercurialFile instances for every file in the repo is + # inefficient. + f = self._files[path] + if not f: + f = MercurialFile(self._client, self._rev, path) + self._files[path] = f + + return f + + +class FileListFinder(BaseFinder): + """Finder for a literal list of file names.""" + + def __init__(self, files): + """files must be a sorted list.""" + self._files = files + + @memoize + def _match(self, pattern): + """Return a sorted list of all files matching the given pattern.""" + # We don't use the utility _find_helper method because it's not tuned + # for performance in the way that we would like this class to be. That's + # a possible avenue for refactoring here. + ret = [] + # We do this as an optimization to figure out where in the sorted list + # to search and where to stop searching. + components = pattern.split("/") + prefix = "/".join(takewhile(lambda s: "*" not in s, components)) + start = bisect.bisect_left(self._files, prefix) + for i in six.moves.range(start, len(self._files)): + f = self._files[i] + if not f.startswith(prefix): + break + # Skip hidden files while scanning. + if "/." in f[len(prefix) :]: + continue + if mozpath.match(f, pattern): + ret.append(f) + return ret + + def find(self, pattern): + pattern = pattern.strip("/") + for path in self._match(pattern): + yield path, File(path) diff --git a/python/mozbuild/mozpack/macpkg.py b/python/mozbuild/mozpack/macpkg.py new file mode 100644 index 0000000000..367f38fd05 --- /dev/null +++ b/python/mozbuild/mozpack/macpkg.py @@ -0,0 +1,222 @@ +# 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/. + +# TODO: Eventually consolidate with mozpack.pkg module. This is kept separate +# for now because of the vast difference in API, and to avoid churn for the +# users of this module (docker images, macos SDK artifacts) when changes are +# necessary in mozpack.pkg +import bz2 +import concurrent.futures +import io +import lzma +import os +import struct +import zlib +from collections import deque, namedtuple +from xml.etree.ElementTree import XML + + +class ZlibFile(object): + def __init__(self, fileobj): + self.fileobj = fileobj + self.decompressor = zlib.decompressobj() + self.buf = b"" + + def read(self, length): + cutoff = min(length, len(self.buf)) + result = self.buf[:cutoff] + self.buf = self.buf[cutoff:] + while len(result) < length: + buf = self.fileobj.read(io.DEFAULT_BUFFER_SIZE) + if not buf: + break + buf = self.decompressor.decompress(buf) + cutoff = min(length - len(result), len(buf)) + result += buf[:cutoff] + self.buf += buf[cutoff:] + return result + + +def unxar(fileobj): + magic = fileobj.read(4) + if magic != b"xar!": + raise Exception("Not a XAR?") + + header_size = fileobj.read(2) + header_size = struct.unpack(">H", header_size)[0] + if header_size > 64: + raise Exception( + f"Don't know how to handle a {header_size} bytes XAR header size" + ) + header_size -= 6 # what we've read so far. + header = fileobj.read(header_size) + if len(header) != header_size: + raise Exception("Failed to read XAR header") + ( + version, + compressed_toc_len, + uncompressed_toc_len, + checksum_type, + ) = struct.unpack(">HQQL", header[:22]) + if version != 1: + raise Exception(f"XAR version {version} not supported") + toc = fileobj.read(compressed_toc_len) + base = fileobj.tell() + if len(toc) != compressed_toc_len: + raise Exception("Failed to read XAR TOC") + toc = zlib.decompress(toc) + if len(toc) != uncompressed_toc_len: + raise Exception("Corrupted XAR?") + toc = XML(toc).find("toc") + queue = deque(toc.findall("file")) + while queue: + f = queue.pop() + queue.extend(f.iterfind("file")) + if f.find("type").text != "file": + continue + filename = f.find("name").text + data = f.find("data") + length = int(data.find("length").text) + size = int(data.find("size").text) + offset = int(data.find("offset").text) + encoding = data.find("encoding").get("style") + fileobj.seek(base + offset, os.SEEK_SET) + content = Take(fileobj, length) + if encoding == "application/octet-stream": + if length != size: + raise Exception(f"{length} != {size}") + elif encoding == "application/x-bzip2": + content = bz2.BZ2File(content) + elif encoding == "application/x-gzip": + # Despite the encoding saying gzip, it is in fact, a raw zlib stream. + content = ZlibFile(content) + else: + raise Exception(f"XAR encoding {encoding} not supported") + + yield filename, content + + +class Pbzx(object): + def __init__(self, fileobj): + magic = fileobj.read(4) + if magic != b"pbzx": + raise Exception("Not a PBZX payload?") + # The first thing in the file looks like the size of each + # decompressed chunk except the last one. It should match + # decompressed_size in all cases except last, but we don't + # check. + chunk_size = fileobj.read(8) + chunk_size = struct.unpack(">Q", chunk_size)[0] + executor = concurrent.futures.ThreadPoolExecutor(max_workers=os.cpu_count()) + self.chunk_getter = executor.map(self._uncompress_chunk, self._chunker(fileobj)) + self._init_one_chunk() + + @staticmethod + def _chunker(fileobj): + while True: + header = fileobj.read(16) + if header == b"": + break + if len(header) != 16: + raise Exception("Corrupted PBZX payload?") + decompressed_size, compressed_size = struct.unpack(">QQ", header) + chunk = fileobj.read(compressed_size) + yield decompressed_size, compressed_size, chunk + + @staticmethod + def _uncompress_chunk(data): + decompressed_size, compressed_size, chunk = data + if compressed_size != decompressed_size: + chunk = lzma.decompress(chunk) + if len(chunk) != decompressed_size: + raise Exception("Corrupted PBZX payload?") + return chunk + + def _init_one_chunk(self): + self.offset = 0 + self.chunk = next(self.chunk_getter, "") + + def read(self, length=None): + if length == 0: + return b"" + if length and len(self.chunk) >= self.offset + length: + start = self.offset + self.offset += length + return self.chunk[start : self.offset] + else: + result = self.chunk[self.offset :] + self._init_one_chunk() + if self.chunk: + # XXX: suboptimal if length is larger than the chunk size + result += self.read(None if length is None else length - len(result)) + return result + + +class Take(object): + """ + File object wrapper that allows to read at most a certain length. + """ + + def __init__(self, fileobj, limit): + self.fileobj = fileobj + self.limit = limit + + def read(self, length=None): + if length is None: + length = self.limit + else: + length = min(length, self.limit) + result = self.fileobj.read(length) + self.limit -= len(result) + return result + + +CpioInfo = namedtuple("CpioInfo", ["mode", "nlink", "dev", "ino"]) + + +def uncpio(fileobj): + while True: + magic = fileobj.read(6) + # CPIO payloads in mac pkg files are using the portable ASCII format. + if magic != b"070707": + if magic.startswith(b"0707"): + raise Exception("Unsupported CPIO format") + raise Exception("Not a CPIO header") + header = fileobj.read(70) + ( + dev, + ino, + mode, + uid, + gid, + nlink, + rdev, + mtime, + namesize, + filesize, + ) = struct.unpack(">6s6s6s6s6s6s6s11s6s11s", header) + dev = int(dev, 8) + ino = int(ino, 8) + mode = int(mode, 8) + nlink = int(nlink, 8) + namesize = int(namesize, 8) + filesize = int(filesize, 8) + name = fileobj.read(namesize) + if name[-1] != 0: + raise Exception("File name is not NUL terminated") + name = name[:-1] + if name == b"TRAILER!!!": + break + + if b"/../" in name or name.startswith(b"../") or name == b"..": + raise Exception(".. is forbidden in file name") + if name.startswith(b"."): + name = name[1:] + if name.startswith(b"/"): + name = name[1:] + content = Take(fileobj, filesize) + yield name, CpioInfo(mode=mode, nlink=nlink, dev=dev, ino=ino), content + # Ensure the content is totally consumed + while content.read(4096): + pass diff --git a/python/mozbuild/mozpack/manifests.py b/python/mozbuild/mozpack/manifests.py new file mode 100644 index 0000000000..2df6c729ea --- /dev/null +++ b/python/mozbuild/mozpack/manifests.py @@ -0,0 +1,483 @@ +# 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/. + +import json +from contextlib import contextmanager + +import six + +import mozpack.path as mozpath + +from .files import ( + AbsoluteSymlinkFile, + ExistingFile, + File, + FileFinder, + GeneratedFile, + HardlinkFile, + PreprocessedFile, +) + + +# This probably belongs in a more generic module. Where? +@contextmanager +def _auto_fileobj(path, fileobj, mode="r"): + if path and fileobj: + raise AssertionError("Only 1 of path or fileobj may be defined.") + + if not path and not fileobj: + raise AssertionError("Must specified 1 of path or fileobj.") + + if path: + fileobj = open(path, mode) + + try: + yield fileobj + finally: + if path: + fileobj.close() + + +class UnreadableInstallManifest(Exception): + """Raised when an invalid install manifest is parsed.""" + + +class InstallManifest(object): + """Describes actions to be used with a copier.FileCopier instance. + + This class facilitates serialization and deserialization of data used to + construct a copier.FileCopier and to perform copy operations. + + The manifest defines source paths, destination paths, and a mechanism by + which the destination file should come into existence. + + Entries in the manifest correspond to the following types: + + copy -- The file specified as the source path will be copied to the + destination path. + + link -- The destination path will be a symlink or hardlink to the source + path. If symlinks are not supported, a copy will be performed. + + exists -- The destination path is accounted for and won't be deleted by + the FileCopier. If the destination path doesn't exist, an error is + raised. + + optional -- The destination path is accounted for and won't be deleted by + the FileCopier. No error is raised if the destination path does not + exist. + + patternlink -- Paths matched by the expression in the source path + will be symlinked or hardlinked to the destination directory. + + patterncopy -- Similar to patternlink except files are copied, not + symlinked/hardlinked. + + preprocess -- The file specified at the source path will be run through + the preprocessor, and the output will be written to the destination + path. + + content -- The destination file will be created with the given content. + + Version 1 of the manifest was the initial version. + Version 2 added optional path support + Version 3 added support for pattern entries. + Version 4 added preprocessed file support. + Version 5 added content support. + """ + + CURRENT_VERSION = 5 + + FIELD_SEPARATOR = "\x1f" + + # Negative values are reserved for non-actionable items, that is, metadata + # that doesn't describe files in the destination. + LINK = 1 + COPY = 2 + REQUIRED_EXISTS = 3 + OPTIONAL_EXISTS = 4 + PATTERN_LINK = 5 + PATTERN_COPY = 6 + PREPROCESS = 7 + CONTENT = 8 + + def __init__(self, path=None, fileobj=None): + """Create a new InstallManifest entry. + + If path is defined, the manifest will be populated with data from the + file path. + + If fileobj is defined, the manifest will be populated with data read + from the specified file object. + + Both path and fileobj cannot be defined. + """ + self._dests = {} + self._source_files = set() + + if path or fileobj: + with _auto_fileobj(path, fileobj, "r") as fh: + self._source_files.add(fh.name) + self._load_from_fileobj(fh) + + def _load_from_fileobj(self, fileobj): + version = fileobj.readline().rstrip() + if version not in ("1", "2", "3", "4", "5"): + raise UnreadableInstallManifest("Unknown manifest version: %s" % version) + + for line in fileobj: + # Explicitly strip on \n so we don't strip out the FIELD_SEPARATOR + # as well. + line = line.rstrip("\n") + + fields = line.split(self.FIELD_SEPARATOR) + + record_type = int(fields[0]) + + if record_type == self.LINK: + dest, source = fields[1:] + self.add_link(source, dest) + continue + + if record_type == self.COPY: + dest, source = fields[1:] + self.add_copy(source, dest) + continue + + if record_type == self.REQUIRED_EXISTS: + _, path = fields + self.add_required_exists(path) + continue + + if record_type == self.OPTIONAL_EXISTS: + _, path = fields + self.add_optional_exists(path) + continue + + if record_type == self.PATTERN_LINK: + _, base, pattern, dest = fields[1:] + self.add_pattern_link(base, pattern, dest) + continue + + if record_type == self.PATTERN_COPY: + _, base, pattern, dest = fields[1:] + self.add_pattern_copy(base, pattern, dest) + continue + + if record_type == self.PREPROCESS: + dest, source, deps, marker, defines, warnings = fields[1:] + + self.add_preprocess( + source, + dest, + deps, + marker, + self._decode_field_entry(defines), + silence_missing_directive_warnings=bool(int(warnings)), + ) + continue + + if record_type == self.CONTENT: + dest, content = fields[1:] + + self.add_content( + six.ensure_text(self._decode_field_entry(content)), dest + ) + continue + + # Don't fail for non-actionable items, allowing + # forward-compatibility with those we will add in the future. + if record_type >= 0: + raise UnreadableInstallManifest("Unknown record type: %d" % record_type) + + def __len__(self): + return len(self._dests) + + def __contains__(self, item): + return item in self._dests + + def __eq__(self, other): + return isinstance(other, InstallManifest) and self._dests == other._dests + + def __neq__(self, other): + return not self.__eq__(other) + + def __ior__(self, other): + if not isinstance(other, InstallManifest): + raise ValueError("Can only | with another instance of InstallManifest.") + + self.add_entries_from(other) + + return self + + def _encode_field_entry(self, data): + """Converts an object into a format that can be stored in the manifest file. + + Complex data types, such as ``dict``, need to be converted into a text + representation before they can be written to a file. + """ + return json.dumps(data, sort_keys=True) + + def _decode_field_entry(self, data): + """Restores an object from a format that can be stored in the manifest file. + + Complex data types, such as ``dict``, need to be converted into a text + representation before they can be written to a file. + """ + return json.loads(data) + + def write(self, path=None, fileobj=None, expand_pattern=False): + """Serialize this manifest to a file or file object. + + If path is specified, that file will be written to. If fileobj is specified, + the serialized content will be written to that file object. + + It is an error if both are specified. + """ + with _auto_fileobj(path, fileobj, "wt") as fh: + fh.write("%d\n" % self.CURRENT_VERSION) + + for dest in sorted(self._dests): + entry = self._dests[dest] + + if expand_pattern and entry[0] in ( + self.PATTERN_LINK, + self.PATTERN_COPY, + ): + type, base, pattern, dest = entry + type = self.LINK if type == self.PATTERN_LINK else self.COPY + finder = FileFinder(base) + paths = [f[0] for f in finder.find(pattern)] + for path in paths: + source = mozpath.join(base, path) + parts = ["%d" % type, mozpath.join(dest, path), source] + fh.write( + "%s\n" + % self.FIELD_SEPARATOR.join( + six.ensure_text(p) for p in parts + ) + ) + else: + parts = ["%d" % entry[0], dest] + parts.extend(entry[1:]) + fh.write( + "%s\n" + % self.FIELD_SEPARATOR.join(six.ensure_text(p) for p in parts) + ) + + def add_link(self, source, dest): + """Add a link to this manifest. + + dest will be either a symlink or hardlink to source. + """ + self._add_entry(dest, (self.LINK, source)) + + def add_copy(self, source, dest): + """Add a copy to this manifest. + + source will be copied to dest. + """ + self._add_entry(dest, (self.COPY, source)) + + def add_required_exists(self, dest): + """Record that a destination file must exist. + + This effectively prevents the listed file from being deleted. + """ + self._add_entry(dest, (self.REQUIRED_EXISTS,)) + + def add_optional_exists(self, dest): + """Record that a destination file may exist. + + This effectively prevents the listed file from being deleted. Unlike a + "required exists" file, files of this type do not raise errors if the + destination file does not exist. + """ + self._add_entry(dest, (self.OPTIONAL_EXISTS,)) + + def add_pattern_link(self, base, pattern, dest): + """Add a pattern match that results in links being created. + + A ``FileFinder`` will be created with its base set to ``base`` + and ``FileFinder.find()`` will be called with ``pattern`` to discover + source files. Each source file will be either symlinked or hardlinked + under ``dest``. + + Filenames under ``dest`` are constructed by taking the path fragment + after ``base`` and concatenating it with ``dest``. e.g. + + <base>/foo/bar.h -> <dest>/foo/bar.h + """ + self._add_entry( + mozpath.join(dest, pattern), (self.PATTERN_LINK, base, pattern, dest) + ) + + def add_pattern_copy(self, base, pattern, dest): + """Add a pattern match that results in copies. + + See ``add_pattern_link()`` for usage. + """ + self._add_entry( + mozpath.join(dest, pattern), (self.PATTERN_COPY, base, pattern, dest) + ) + + def add_preprocess( + self, + source, + dest, + deps, + marker="#", + defines={}, + silence_missing_directive_warnings=False, + ): + """Add a preprocessed file to this manifest. + + ``source`` will be passed through preprocessor.py, and the output will be + written to ``dest``. + """ + self._add_entry( + dest, + ( + self.PREPROCESS, + source, + deps, + marker, + self._encode_field_entry(defines), + "1" if silence_missing_directive_warnings else "0", + ), + ) + + def add_content(self, content, dest): + """Add a file with the given content.""" + self._add_entry( + dest, + ( + self.CONTENT, + self._encode_field_entry(content), + ), + ) + + def _add_entry(self, dest, entry): + if dest in self._dests: + raise ValueError("Item already in manifest: %s" % dest) + + self._dests[dest] = entry + + def add_entries_from(self, other, base=""): + """ + Copy data from another mozpack.copier.InstallManifest + instance, adding an optional base prefix to the destination. + + This allows to merge two manifests into a single manifest, or + two take the tagged union of two manifests. + """ + # We must copy source files to ourselves so extra dependencies from + # the preprocessor are taken into account. Ideally, we would track + # which source file each entry came from. However, this is more + # complicated and not yet implemented. The current implementation + # will result in over invalidation, possibly leading to performance + # loss. + self._source_files |= other._source_files + + for dest in sorted(other._dests): + new_dest = mozpath.join(base, dest) if base else dest + entry = other._dests[dest] + if entry[0] in (self.PATTERN_LINK, self.PATTERN_COPY): + entry_type, entry_base, entry_pattern, entry_dest = entry + new_entry_dest = mozpath.join(base, entry_dest) if base else entry_dest + new_entry = (entry_type, entry_base, entry_pattern, new_entry_dest) + else: + new_entry = tuple(entry) + + self._add_entry(new_dest, new_entry) + + def populate_registry(self, registry, defines_override={}, link_policy="symlink"): + """Populate a mozpack.copier.FileRegistry instance with data from us. + + The caller supplied a FileRegistry instance (or at least something that + conforms to its interface) and that instance is populated with data + from this manifest. + + Defines can be given to override the ones in the manifest for + preprocessing. + + The caller can set a link policy. This determines whether symlinks, + hardlinks, or copies are used for LINK and PATTERN_LINK. + """ + assert link_policy in ("symlink", "hardlink", "copy") + for dest in sorted(self._dests): + entry = self._dests[dest] + install_type = entry[0] + + if install_type == self.LINK: + if link_policy == "symlink": + cls = AbsoluteSymlinkFile + elif link_policy == "hardlink": + cls = HardlinkFile + else: + cls = File + registry.add(dest, cls(entry[1])) + continue + + if install_type == self.COPY: + registry.add(dest, File(entry[1])) + continue + + if install_type == self.REQUIRED_EXISTS: + registry.add(dest, ExistingFile(required=True)) + continue + + if install_type == self.OPTIONAL_EXISTS: + registry.add(dest, ExistingFile(required=False)) + continue + + if install_type in (self.PATTERN_LINK, self.PATTERN_COPY): + _, base, pattern, dest = entry + finder = FileFinder(base) + paths = [f[0] for f in finder.find(pattern)] + + if install_type == self.PATTERN_LINK: + if link_policy == "symlink": + cls = AbsoluteSymlinkFile + elif link_policy == "hardlink": + cls = HardlinkFile + else: + cls = File + else: + cls = File + + for path in paths: + source = mozpath.join(base, path) + registry.add(mozpath.join(dest, path), cls(source)) + + continue + + if install_type == self.PREPROCESS: + defines = self._decode_field_entry(entry[4]) + if defines_override: + defines.update(defines_override) + registry.add( + dest, + PreprocessedFile( + entry[1], + depfile_path=entry[2], + marker=entry[3], + defines=defines, + extra_depends=self._source_files, + silence_missing_directive_warnings=bool(int(entry[5])), + ), + ) + + continue + + if install_type == self.CONTENT: + # GeneratedFile expect the buffer interface, which the unicode + # type doesn't have, so encode to a str. + content = self._decode_field_entry(entry[1]).encode("utf-8") + registry.add(dest, GeneratedFile(content)) + continue + + raise Exception( + "Unknown install type defined in manifest: %d" % install_type + ) diff --git a/python/mozbuild/mozpack/mozjar.py b/python/mozbuild/mozpack/mozjar.py new file mode 100644 index 0000000000..6500ebfcec --- /dev/null +++ b/python/mozbuild/mozpack/mozjar.py @@ -0,0 +1,842 @@ +# 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/. + +import os +import struct +import zlib +from collections import OrderedDict +from io import BytesIO, UnsupportedOperation +from zipfile import ZIP_DEFLATED, ZIP_STORED + +import six + +import mozpack.path as mozpath +from mozbuild.util import ensure_bytes + +JAR_STORED = ZIP_STORED +JAR_DEFLATED = ZIP_DEFLATED +MAX_WBITS = 15 + + +class JarReaderError(Exception): + """Error type for Jar reader errors.""" + + +class JarWriterError(Exception): + """Error type for Jar writer errors.""" + + +class JarStruct(object): + """ + Helper used to define ZIP archive raw data structures. Data structures + handled by this helper all start with a magic number, defined in + subclasses MAGIC field as a 32-bits unsigned integer, followed by data + structured as described in subclasses STRUCT field. + + The STRUCT field contains a list of (name, type) pairs where name is a + field name, and the type can be one of 'uint32', 'uint16' or one of the + field names. In the latter case, the field is considered to be a string + buffer with a length given in that field. + For example, + + .. code-block:: python + + STRUCT = [ + ('version', 'uint32'), + ('filename_size', 'uint16'), + ('filename', 'filename_size') + ] + + describes a structure with a 'version' 32-bits unsigned integer field, + followed by a 'filename_size' 16-bits unsigned integer field, followed by a + filename_size-long string buffer 'filename'. + + Fields that are used as other fields size are not stored in objects. In the + above example, an instance of such subclass would only have two attributes: + - obj['version'] + - obj['filename'] + + filename_size would be obtained with len(obj['filename']). + + JarStruct subclasses instances can be either initialized from existing data + (deserialized), or with empty fields. + """ + + TYPE_MAPPING = {"uint32": (b"I", 4), "uint16": (b"H", 2)} + + def __init__(self, data=None): + """ + Create an instance from the given data. Data may be omitted to create + an instance with empty fields. + """ + assert self.MAGIC and isinstance(self.STRUCT, OrderedDict) + self.size_fields = set( + t for t in six.itervalues(self.STRUCT) if t not in JarStruct.TYPE_MAPPING + ) + self._values = {} + if data: + self._init_data(data) + else: + self._init_empty() + + def _init_data(self, data): + """ + Initialize an instance from data, following the data structure + described in self.STRUCT. The self.MAGIC signature is expected at + data[:4]. + """ + assert data is not None + self.signature, size = JarStruct.get_data("uint32", data) + if self.signature != self.MAGIC: + raise JarReaderError("Bad magic") + offset = size + # For all fields used as other fields sizes, keep track of their value + # separately. + sizes = dict((t, 0) for t in self.size_fields) + for name, t in six.iteritems(self.STRUCT): + if t in JarStruct.TYPE_MAPPING: + value, size = JarStruct.get_data(t, data[offset:]) + else: + size = sizes[t] + value = data[offset : offset + size] + if isinstance(value, memoryview): + value = value.tobytes() + if name not in sizes: + self._values[name] = value + else: + sizes[name] = value + offset += size + + def _init_empty(self): + """ + Initialize an instance with empty fields. + """ + self.signature = self.MAGIC + for name, t in six.iteritems(self.STRUCT): + if name in self.size_fields: + continue + self._values[name] = 0 if t in JarStruct.TYPE_MAPPING else "" + + @staticmethod + def get_data(type, data): + """ + Deserialize a single field of given type (must be one of + JarStruct.TYPE_MAPPING) at the given offset in the given data. + """ + assert type in JarStruct.TYPE_MAPPING + assert data is not None + format, size = JarStruct.TYPE_MAPPING[type] + data = data[:size] + if isinstance(data, memoryview): + data = data.tobytes() + return struct.unpack(b"<" + format, data)[0], size + + def serialize(self): + """ + Serialize the data structure according to the data structure definition + from self.STRUCT. + """ + serialized = struct.pack(b"<I", self.signature) + sizes = dict( + (t, name) + for name, t in six.iteritems(self.STRUCT) + if t not in JarStruct.TYPE_MAPPING + ) + for name, t in six.iteritems(self.STRUCT): + if t in JarStruct.TYPE_MAPPING: + format, size = JarStruct.TYPE_MAPPING[t] + if name in sizes: + value = len(self[sizes[name]]) + else: + value = self[name] + serialized += struct.pack(b"<" + format, value) + else: + serialized += ensure_bytes(self[name]) + return serialized + + @property + def size(self): + """ + Return the size of the data structure, given the current values of all + variable length fields. + """ + size = JarStruct.TYPE_MAPPING["uint32"][1] + for name, type in six.iteritems(self.STRUCT): + if type in JarStruct.TYPE_MAPPING: + size += JarStruct.TYPE_MAPPING[type][1] + else: + size += len(self[name]) + return size + + def __getitem__(self, key): + return self._values[key] + + def __setitem__(self, key, value): + if key not in self.STRUCT: + raise KeyError(key) + if key in self.size_fields: + raise AttributeError("can't set attribute") + self._values[key] = value + + def __contains__(self, key): + return key in self._values + + def __iter__(self): + return six.iteritems(self._values) + + def __repr__(self): + return "<%s %s>" % ( + self.__class__.__name__, + " ".join("%s=%s" % (n, v) for n, v in self), + ) + + +class JarCdirEnd(JarStruct): + """ + End of central directory record. + """ + + MAGIC = 0x06054B50 + STRUCT = OrderedDict( + [ + ("disk_num", "uint16"), + ("cdir_disk", "uint16"), + ("disk_entries", "uint16"), + ("cdir_entries", "uint16"), + ("cdir_size", "uint32"), + ("cdir_offset", "uint32"), + ("comment_size", "uint16"), + ("comment", "comment_size"), + ] + ) + + +CDIR_END_SIZE = JarCdirEnd().size + + +class JarCdirEntry(JarStruct): + """ + Central directory file header + """ + + MAGIC = 0x02014B50 + STRUCT = OrderedDict( + [ + ("creator_version", "uint16"), + ("min_version", "uint16"), + ("general_flag", "uint16"), + ("compression", "uint16"), + ("lastmod_time", "uint16"), + ("lastmod_date", "uint16"), + ("crc32", "uint32"), + ("compressed_size", "uint32"), + ("uncompressed_size", "uint32"), + ("filename_size", "uint16"), + ("extrafield_size", "uint16"), + ("filecomment_size", "uint16"), + ("disknum", "uint16"), + ("internal_attr", "uint16"), + ("external_attr", "uint32"), + ("offset", "uint32"), + ("filename", "filename_size"), + ("extrafield", "extrafield_size"), + ("filecomment", "filecomment_size"), + ] + ) + + +class JarLocalFileHeader(JarStruct): + """ + Local file header + """ + + MAGIC = 0x04034B50 + STRUCT = OrderedDict( + [ + ("min_version", "uint16"), + ("general_flag", "uint16"), + ("compression", "uint16"), + ("lastmod_time", "uint16"), + ("lastmod_date", "uint16"), + ("crc32", "uint32"), + ("compressed_size", "uint32"), + ("uncompressed_size", "uint32"), + ("filename_size", "uint16"), + ("extra_field_size", "uint16"), + ("filename", "filename_size"), + ("extra_field", "extra_field_size"), + ] + ) + + +class JarFileReader(object): + """ + File-like class for use by JarReader to give access to individual files + within a Jar archive. + """ + + def __init__(self, header, data): + """ + Initialize a JarFileReader. header is the local file header + corresponding to the file in the jar archive, data a buffer containing + the file data. + """ + assert header["compression"] in [JAR_DEFLATED, JAR_STORED] + self._data = data + # Copy some local file header fields. + for name in ["compressed_size", "uncompressed_size", "crc32"]: + setattr(self, name, header[name]) + self.filename = six.ensure_text(header["filename"]) + self.compressed = header["compression"] != JAR_STORED + self.compress = header["compression"] + + def readable(self): + return True + + def read(self, length=-1): + """ + Read some amount of uncompressed data. + """ + return self.uncompressed_data.read(length) + + def readinto(self, b): + """ + Read bytes into a pre-allocated, writable bytes-like object `b` and return + the number of bytes read. + """ + return self.uncompressed_data.readinto(b) + + def readlines(self): + """ + Return a list containing all the lines of data in the uncompressed + data. + """ + return self.read().splitlines(True) + + def __iter__(self): + """ + Iterator, to support the "for line in fileobj" constructs. + """ + return iter(self.readlines()) + + def seek(self, pos, whence=os.SEEK_SET): + """ + Change the current position in the uncompressed data. Subsequent reads + will start from there. + """ + return self.uncompressed_data.seek(pos, whence) + + def close(self): + """ + Free the uncompressed data buffer. + """ + self.uncompressed_data.close() + + @property + def closed(self): + return self.uncompressed_data.closed + + @property + def compressed_data(self): + """ + Return the raw compressed data. + """ + return self._data[: self.compressed_size] + + @property + def uncompressed_data(self): + """ + Return the uncompressed data. + """ + if hasattr(self, "_uncompressed_data"): + return self._uncompressed_data + data = self.compressed_data + if self.compress == JAR_STORED: + data = data.tobytes() + elif self.compress == JAR_DEFLATED: + data = zlib.decompress(data.tobytes(), -MAX_WBITS) + else: + assert False # Can't be another value per __init__ + if len(data) != self.uncompressed_size: + raise JarReaderError("Corrupted file? %s" % self.filename) + self._uncompressed_data = BytesIO(data) + return self._uncompressed_data + + +class JarReader(object): + """ + Class with methods to read Jar files. Can open standard jar files as well + as Mozilla jar files (see further details in the JarWriter documentation). + """ + + def __init__(self, file=None, fileobj=None, data=None): + """ + Opens the given file as a Jar archive. Use the given file-like object + if one is given instead of opening the given file name. + """ + if fileobj: + data = fileobj.read() + elif file: + data = open(file, "rb").read() + self._data = memoryview(data) + # The End of Central Directory Record has a variable size because of + # comments it may contain, so scan for it from the end of the file. + offset = -CDIR_END_SIZE + while True: + signature = JarStruct.get_data("uint32", self._data[offset:])[0] + if signature == JarCdirEnd.MAGIC: + break + if offset == -len(self._data): + raise JarReaderError("Not a jar?") + offset -= 1 + self._cdir_end = JarCdirEnd(self._data[offset:]) + + def close(self): + """ + Free some resources associated with the Jar. + """ + del self._data + + @property + def compression(self): + entries = self.entries + if not entries: + return JAR_STORED + return max(f["compression"] for f in six.itervalues(entries)) + + @property + def entries(self): + """ + Return an ordered dict of central directory entries, indexed by + filename, in the order they appear in the Jar archive central + directory. Directory entries are skipped. + """ + if hasattr(self, "_entries"): + return self._entries + preload = 0 + if self.is_optimized: + preload = JarStruct.get_data("uint32", self._data)[0] + entries = OrderedDict() + offset = self._cdir_end["cdir_offset"] + for e in six.moves.xrange(self._cdir_end["cdir_entries"]): + entry = JarCdirEntry(self._data[offset:]) + offset += entry.size + # Creator host system. 0 is MSDOS, 3 is Unix + host = entry["creator_version"] >> 8 + # External attributes values depend on host above. On Unix the + # higher bits are the stat.st_mode value. On MSDOS, the lower bits + # are the FAT attributes. + xattr = entry["external_attr"] + # Skip directories + if (host == 0 and xattr & 0x10) or (host == 3 and xattr & (0o040000 << 16)): + continue + entries[six.ensure_text(entry["filename"])] = entry + if entry["offset"] < preload: + self._last_preloaded = six.ensure_text(entry["filename"]) + self._entries = entries + return entries + + @property + def is_optimized(self): + """ + Return whether the jar archive is optimized. + """ + # In optimized jars, the central directory is at the beginning of the + # file, after a single 32-bits value, which is the length of data + # preloaded. + return self._cdir_end["cdir_offset"] == JarStruct.TYPE_MAPPING["uint32"][1] + + @property + def last_preloaded(self): + """ + Return the name of the last file that is set to be preloaded. + See JarWriter documentation for more details on preloading. + """ + if hasattr(self, "_last_preloaded"): + return self._last_preloaded + self._last_preloaded = None + self.entries + return self._last_preloaded + + def _getreader(self, entry): + """ + Helper to create a JarFileReader corresponding to the given central + directory entry. + """ + header = JarLocalFileHeader(self._data[entry["offset"] :]) + for key, value in entry: + if key in header and header[key] != value: + raise JarReaderError( + "Central directory and file header " + + "mismatch. Corrupted archive?" + ) + return JarFileReader(header, self._data[entry["offset"] + header.size :]) + + def __iter__(self): + """ + Iterate over all files in the Jar archive, in the form of + JarFileReaders. + for file in jarReader: + ... + """ + for entry in six.itervalues(self.entries): + yield self._getreader(entry) + + def __getitem__(self, name): + """ + Get a JarFileReader for the given file name. + """ + return self._getreader(self.entries[name]) + + def __contains__(self, name): + """ + Return whether the given file name appears in the Jar archive. + """ + return name in self.entries + + +class JarWriter(object): + """ + Class with methods to write Jar files. Can write more-or-less standard jar + archives as well as jar archives optimized for Gecko. See the documentation + for the close() member function for a description of both layouts. + """ + + def __init__(self, file=None, fileobj=None, compress=True, compress_level=9): + """ + Initialize a Jar archive in the given file. Use the given file-like + object if one is given instead of opening the given file name. + The compress option determines the default behavior for storing data + in the jar archive. The optimize options determines whether the jar + archive should be optimized for Gecko or not. ``compress_level`` + defines the zlib compression level. It must be a value between 0 and 9 + and defaults to 9, the highest and slowest level of compression. + """ + if fileobj: + self._data = fileobj + else: + self._data = open(file, "wb") + if compress is True: + compress = JAR_DEFLATED + self._compress = compress + self._compress_level = compress_level + self._contents = OrderedDict() + self._last_preloaded = None + + def __enter__(self): + """ + Context manager __enter__ method for JarWriter. + """ + return self + + def __exit__(self, type, value, tb): + """ + Context manager __exit__ method for JarWriter. + """ + self.finish() + + def finish(self): + """ + Flush and close the Jar archive. + + Standard jar archives are laid out like the following: + - Local file header 1 + - File data 1 + - Local file header 2 + - File data 2 + - (...) + - Central directory entry pointing at Local file header 1 + - Central directory entry pointing at Local file header 2 + - (...) + - End of central directory, pointing at first central directory + entry. + + Jar archives optimized for Gecko are laid out like the following: + - 32-bits unsigned integer giving the amount of data to preload. + - Central directory entry pointing at Local file header 1 + - Central directory entry pointing at Local file header 2 + - (...) + - End of central directory, pointing at first central directory + entry. + - Local file header 1 + - File data 1 + - Local file header 2 + - File data 2 + - (...) + - End of central directory, pointing at first central directory + entry. + + The duplication of the End of central directory is to accomodate some + Zip reading tools that want an end of central directory structure to + follow the central directory entries. + """ + offset = 0 + headers = {} + preload_size = 0 + # Prepare central directory entries + for entry, content in six.itervalues(self._contents): + header = JarLocalFileHeader() + for name in entry.STRUCT: + if name in header: + header[name] = entry[name] + entry["offset"] = offset + offset += len(content) + header.size + if six.ensure_text(entry["filename"]) == self._last_preloaded: + preload_size = offset + headers[entry] = header + # Prepare end of central directory + end = JarCdirEnd() + end["disk_entries"] = len(self._contents) + end["cdir_entries"] = end["disk_entries"] + end["cdir_size"] = six.moves.reduce( + lambda x, y: x + y[0].size, self._contents.values(), 0 + ) + # On optimized archives, store the preloaded size and the central + # directory entries, followed by the first end of central directory. + if preload_size: + end["cdir_offset"] = 4 + offset = end["cdir_size"] + end["cdir_offset"] + end.size + preload_size += offset + self._data.write(struct.pack("<I", preload_size)) + for entry, _ in six.itervalues(self._contents): + entry["offset"] += offset + self._data.write(entry.serialize()) + self._data.write(end.serialize()) + # Store local file entries followed by compressed data + for entry, content in six.itervalues(self._contents): + self._data.write(headers[entry].serialize()) + if isinstance(content, memoryview): + self._data.write(content.tobytes()) + else: + self._data.write(content) + # On non optimized archives, store the central directory entries. + if not preload_size: + end["cdir_offset"] = offset + for entry, _ in six.itervalues(self._contents): + self._data.write(entry.serialize()) + # Store the end of central directory. + self._data.write(end.serialize()) + self._data.close() + + def add(self, name, data, compress=None, mode=None, skip_duplicates=False): + """ + Add a new member to the jar archive, with the given name and the given + data. + The compress option indicates how the given data should be compressed + (one of JAR_STORED or JAR_DEFLATE), or compressed according + to the default defined when creating the JarWriter (None). True and + False are allowed values for backwards compatibility, mapping, + respectively, to JAR_DEFLATE and JAR_STORED. + When the data should be compressed, it is only really compressed if + the compressed size is smaller than the uncompressed size. + The mode option gives the unix permissions that should be stored for the + jar entry, which defaults to 0o100644 (regular file, u+rw, g+r, o+r) if + not specified. + If a duplicated member is found skip_duplicates will prevent raising + an exception if set to True. + The given data may be a buffer, a file-like instance, a Deflater or a + JarFileReader instance. The latter two allow to avoid uncompressing + data to recompress it. + """ + name = mozpath.normsep(six.ensure_text(name)) + + if name in self._contents and not skip_duplicates: + raise JarWriterError("File %s already in JarWriter" % name) + if compress is None: + compress = self._compress + if compress is True: + compress = JAR_DEFLATED + if compress is False: + compress = JAR_STORED + if isinstance(data, (JarFileReader, Deflater)) and data.compress == compress: + deflater = data + else: + deflater = Deflater(compress, compress_level=self._compress_level) + if isinstance(data, (six.binary_type, six.string_types)): + deflater.write(data) + elif hasattr(data, "read"): + try: + data.seek(0) + except (UnsupportedOperation, AttributeError): + pass + deflater.write(data.read()) + else: + raise JarWriterError("Don't know how to handle %s" % type(data)) + # Fill a central directory entry for this new member. + entry = JarCdirEntry() + entry["creator_version"] = 20 + if mode is None: + # If no mode is given, default to u+rw, g+r, o+r. + mode = 0o000644 + if not mode & 0o777000: + # If no file type is given, default to regular file. + mode |= 0o100000 + # Set creator host system (upper byte of creator_version) to 3 (Unix) so + # mode is honored when there is one. + entry["creator_version"] |= 3 << 8 + entry["external_attr"] = (mode & 0xFFFF) << 16 + if deflater.compressed: + entry["min_version"] = 20 # Version 2.0 supports deflated streams + entry["general_flag"] = 2 # Max compression + entry["compression"] = deflater.compress + else: + entry["min_version"] = 10 # Version 1.0 for stored streams + entry["general_flag"] = 0 + entry["compression"] = JAR_STORED + # January 1st, 2010. See bug 592369. + entry["lastmod_date"] = ((2010 - 1980) << 9) | (1 << 5) | 1 + entry["lastmod_time"] = 0 + entry["crc32"] = deflater.crc32 + entry["compressed_size"] = deflater.compressed_size + entry["uncompressed_size"] = deflater.uncompressed_size + entry["filename"] = six.ensure_binary(name) + self._contents[name] = entry, deflater.compressed_data + + def preload(self, files): + """ + Set which members of the jar archive should be preloaded when opening + the archive in Gecko. This reorders the members according to the order + of given list. + """ + new_contents = OrderedDict() + for f in files: + if f not in self._contents: + continue + new_contents[f] = self._contents[f] + self._last_preloaded = f + for f in self._contents: + if f not in new_contents: + new_contents[f] = self._contents[f] + self._contents = new_contents + + +class Deflater(object): + """ + File-like interface to zlib compression. The data is actually not + compressed unless the compressed form is smaller than the uncompressed + data. + """ + + def __init__(self, compress=True, compress_level=9): + """ + Initialize a Deflater. The compress argument determines how to + compress. + """ + self._data = BytesIO() + if compress is True: + compress = JAR_DEFLATED + elif compress is False: + compress = JAR_STORED + self.compress = compress + if compress == JAR_DEFLATED: + self._deflater = zlib.compressobj(compress_level, zlib.DEFLATED, -MAX_WBITS) + self._deflated = BytesIO() + else: + assert compress == JAR_STORED + self._deflater = None + self.crc32 = 0 + + def write(self, data): + """ + Append a buffer to the Deflater. + """ + if isinstance(data, memoryview): + data = data.tobytes() + data = six.ensure_binary(data) + self._data.write(data) + + if self.compress: + if self._deflater: + self._deflated.write(self._deflater.compress(data)) + else: + raise JarWriterError("Can't write after flush") + + self.crc32 = zlib.crc32(data, self.crc32) & 0xFFFFFFFF + + def close(self): + """ + Close the Deflater. + """ + self._data.close() + if self.compress: + self._deflated.close() + + def _flush(self): + """ + Flush the underlying zlib compression object. + """ + if self.compress and self._deflater: + self._deflated.write(self._deflater.flush()) + self._deflater = None + + @property + def compressed(self): + """ + Return whether the data should be compressed. + """ + return self._compressed_size < self.uncompressed_size + + @property + def _compressed_size(self): + """ + Return the real compressed size of the data written to the Deflater. If + the Deflater is set not to compress, the uncompressed size is returned. + Otherwise, the actual compressed size is returned, whether or not it is + a win over the uncompressed size. + """ + if self.compress: + self._flush() + return self._deflated.tell() + return self.uncompressed_size + + @property + def compressed_size(self): + """ + Return the compressed size of the data written to the Deflater. If the + Deflater is set not to compress, the uncompressed size is returned. + Otherwise, if the data should not be compressed (the real compressed + size is bigger than the uncompressed size), return the uncompressed + size. + """ + if self.compressed: + return self._compressed_size + return self.uncompressed_size + + @property + def uncompressed_size(self): + """ + Return the size of the data written to the Deflater. + """ + return self._data.tell() + + @property + def compressed_data(self): + """ + Return the compressed data, if the data should be compressed (real + compressed size smaller than the uncompressed size), or the + uncompressed data otherwise. + """ + if self.compressed: + return self._deflated.getvalue() + return self._data.getvalue() + + +class JarLog(dict): + """ + Helper to read the file Gecko generates when setting MOZ_JAR_LOG_FILE. + The jar log is then available as a dict with the jar path as key, and + the corresponding access log as a list value. Only the first access to + a given member of a jar is stored. + """ + + def __init__(self, file=None, fileobj=None): + if not fileobj: + fileobj = open(file, "r") + for line in fileobj: + jar, path = line.strip().split(None, 1) + if not jar or not path: + continue + entry = self.setdefault(jar, []) + if path not in entry: + entry.append(path) diff --git a/python/mozbuild/mozpack/packager/__init__.py b/python/mozbuild/mozpack/packager/__init__.py new file mode 100644 index 0000000000..a2f763c855 --- /dev/null +++ b/python/mozbuild/mozpack/packager/__init__.py @@ -0,0 +1,445 @@ +# 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/. + +import codecs +import json +import os +import re +from collections import deque + +import six + +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(object): + """ + Class that represents a component in a package manifest. + """ + + 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): + """ + Split [1, 2, 3, 4, 5, 6, 7] into [(1, 2, 3), (4, 5, 6)]. + """ + return zip(*[iter(lst)] * 3) + + KEY_VALUE_RE = re.compile( + r""" + \s* # optional whitespace. + ([a-zA-Z0-9_]+) # key. + \s*=\s* # optional space around =. + "([^"]*)" # value without surrounding quotes. + (?:\s+|$) + """, + re.VERBOSE, + ) + + @staticmethod + def _split_options(string): + """ + Split 'key1="value1" key2="value2"' into + {'key1':'value1', 'key2':'value2'}. + + Returned keys and values are all strings. + + Throws ValueError if the input is malformed. + """ + options = {} + splits = Component.KEY_VALUE_RE.split(string) + if len(splits) % 3 != 1: + # This should never happen -- we expect to always split + # into ['', ('key', 'val', '')*]. + 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): + """ + Split 'name key1="value1" key2="value2"' into + ('name', {'key1':'value1', 'key2':'value2'}). + + Returned name, keys and values are all strings. + + Raises ValueError if the input is malformed. + """ + 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): + """ + Create a component from a 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(object): + """ + Class for parsing of a package manifest, after preprocessing. + + A package manifest is a list of file paths, with some syntaxic sugar: + [] designates a toplevel component. Example: [xpcom] + - in front of a file specifies it to be removed + * wildcard support + ** expands to all files and zero or more directories + ; file comment + + The parser takes input from the preprocessor line by line, and pushes + parsed information to a sink object. + + The add and remove methods of the sink object are called with the + current Component instance and a path. + """ + + def __init__(self, sink): + """ + Initialize the package manifest parser with the given sink. + """ + self._component = Component("") + self._sink = sink + + def handle_line(self, str): + """ + Handle a line of input and push the parsed information to the sink + object. + """ + # Remove comments. + 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): + """ + File-like helper to handle the preprocessor output and send it to a parser. + The parser's handle_line method is called in the relevant errors.context. + """ + + 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={}): + """ + Preprocess the file-like input with the given defines, and send the + preprocessed output line by line to the given parser. + """ + 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 the given file-like manifest with the given defines, and push + the parsed information to a sink. See PackageManifestParser documentation + for more details on the sink. + """ + preprocess(manifest, PackageManifestParser(sink), defines) + + +class CallDeque(deque): + """ + Queue of function calls to make. + """ + + 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): + """ + Helper used to translate and buffer instructions from the + SimpleManifestSink to a formatter. Formatters expect some information to be + given first that the simple manifest contents can't guarantee before the + end of the input. + """ + + def __init__(self, formatter): + self.formatter = formatter + # Queue for formatter.add_interfaces()/add_manifest() calls. + self._queue = CallDeque() + # Queue for formatter.add_manifest() calls for ManifestChrome. + self._chrome_queue = CallDeque() + # Queue for formatter.add() calls. + self._file_queue = CallDeque() + # All paths containing addons. (key is path, value is whether it + # should be packed or unpacked) + self._addons = {} + # All manifest paths imported. + self._manifests = set() + # All manifest paths included from some other manifest. + self._included_manifests = {} + self._closed = False + + # Parsing RDF is complex, and would require an external library to do + # properly. Just go with some hackish but probably sufficient regexp + 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): + """ + Add the given BaseFile instance with the given path. + """ + 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 = six.ensure_text(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 = six.ensure_text(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): + """ + Add the given BaseFile to the collection of addons if a parent + directory is not already in the collection. + """ + 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): + """ + Add the given BaseFile with manifest file contents with the given path. + """ + self._manifests.add(path) + base = "" + if hasattr(file, "path"): + # Find the directory the given path is relative to. + 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())): + # ManifestResources need to be given after ManifestChrome, so just + # put all ManifestChrome in a separate queue to make them first. + if isinstance(e, ManifestChrome): + # e.move(e.base) just returns a clone of the entry. + 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 a binary component is added to an addon, prevent the addon + # from being packed. + 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): + """ + Return all paths under which root manifests have been found. Root + manifests are manifests that are included in no other manifest. + `addons` indicates whether to include addon bases as well. + """ + all_bases = set( + mozpath.dirname(m) for m in self._manifests - set(self._included_manifests) + ) + if not addons: + all_bases -= set(self._addons) + else: + # If for some reason some detected addon doesn't have a + # non-included manifest. + all_bases |= set(self._addons) + return all_bases + + def close(self): + """ + Push all instructions to the formatter. + """ + self._closed = True + + bases = self.get_bases() + broken_bases = sorted( + m + for m, includer in six.iteritems(self._included_manifests) + 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): + """ + Parser sink for "simple" package manifests. Simple package manifests use + the format described in the PackageManifestParser documentation, but don't + support file removals, and require manifests, interfaces and chrome data to + be explicitely listed. + Entries starting with bin/ are searched under bin/ in the FileFinder, but + are packaged without the bin/ prefix. + """ + + def __init__(self, finder, formatter): + """ + Initialize the SimpleManifestSink. The given FileFinder is used to + get files matching the patterns given in the manifest. The given + formatter does the packaging job. + """ + self._finder = finder + self.packager = SimplePackager(formatter) + self._closed = False + self._manifests = set() + + @staticmethod + def normalize_path(path): + """ + Remove any bin/ prefix. + """ + if mozpath.basedir(path, ["bin"]) == "bin": + return mozpath.relpath(path, "bin") + return path + + def add(self, component, pattern): + """ + Add files with the given pattern in the given component. + """ + 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): + """ + Remove files with the given pattern in the given component. + """ + assert not self._closed + errors.fatal("Removal is unsupported") + + def close(self, auto_root_manifest=True): + """ + Add possibly missing bits and push all instructions to the formatter. + """ + if auto_root_manifest: + # Simple package manifests don't contain the root manifests, so + # find and add them. + 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() diff --git a/python/mozbuild/mozpack/packager/formats.py b/python/mozbuild/mozpack/packager/formats.py new file mode 100644 index 0000000000..95a6dee2f6 --- /dev/null +++ b/python/mozbuild/mozpack/packager/formats.py @@ -0,0 +1,354 @@ +# 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 six.moves.urllib.parse import urlparse + +import mozpack.path as mozpath +from mozpack.chrome.manifest import ( + Manifest, + ManifestBinaryComponent, + ManifestChrome, + ManifestInterfaces, + ManifestMultiContent, + ManifestResource, +) +from mozpack.copier import FileRegistry, FileRegistrySubtree, Jarrer +from mozpack.errors import errors +from mozpack.files import ManifestFile + +""" +Formatters are classes receiving packaging instructions and creating the +appropriate package layout. + +There are three distinct formatters, each handling one of the different chrome +formats: + - flat: essentially, copies files from the source with the same file system + layout. Manifests entries are grouped in a single manifest per directory, + as well as XPT interfaces. + - jar: chrome content is packaged in jar files. + - omni: chrome content, modules, non-binary components, and many other + elements are packaged in an omnijar file for each base directory. + +The base interface provides the following methods: + - add_base(path [, addon]) + Register a base directory for an application or GRE, or an addon. + Base directories usually contain a root manifest (manifests not + included in any other manifest) named chrome.manifest. + The optional addon argument tells whether the base directory + is that of a packed addon (True), unpacked addon ('unpacked') or + otherwise (False). + The method may only be called in sorted order of `path` (alphanumeric + order, parents before children). + - add(path, content) + Add the given content (BaseFile instance) at the given virtual path + - add_interfaces(path, content) + Add the given content (BaseFile instance) as an interface. Equivalent + to add(path, content) with the right add_manifest(). + - add_manifest(entry) + Add a ManifestEntry. + - contains(path) + Returns whether the given virtual path is known of the formatter. + +The virtual paths mentioned above are paths as they would be with a flat +chrome. + +Formatters all take a FileCopier instance they will fill with the packaged +data. +""" + + +class PiecemealFormatter(object): + """ + Generic formatter that dispatches across different sub-formatters + according to paths. + """ + + def __init__(self, copier): + assert isinstance(copier, (FileRegistry, FileRegistrySubtree)) + self.copier = copier + self._sub_formatter = {} + self._frozen_bases = False + + def add_base(self, base, addon=False): + # Only allow to add a base directory before calls to _get_base() + assert not self._frozen_bases + assert base not in self._sub_formatter + assert all(base > b for b in self._sub_formatter) + self._add_base(base, addon) + + def _get_base(self, path): + """ + Return the deepest base directory containing the given path. + """ + self._frozen_bases = True + base = mozpath.basedir(path, self._sub_formatter.keys()) + relpath = mozpath.relpath(path, base) if base else path + return base, relpath + + def add(self, path, content): + base, relpath = self._get_base(path) + if base is None: + return self.copier.add(relpath, content) + return self._sub_formatter[base].add(relpath, content) + + def add_manifest(self, entry): + base, relpath = self._get_base(entry.base) + assert base is not None + return self._sub_formatter[base].add_manifest(entry.move(relpath)) + + def add_interfaces(self, path, content): + base, relpath = self._get_base(path) + assert base is not None + return self._sub_formatter[base].add_interfaces(relpath, content) + + def contains(self, path): + assert "*" not in path + base, relpath = self._get_base(path) + if base is None: + return self.copier.contains(relpath) + return self._sub_formatter[base].contains(relpath) + + +class FlatFormatter(PiecemealFormatter): + """ + Formatter for the flat package format. + """ + + def _add_base(self, base, addon=False): + self._sub_formatter[base] = FlatSubFormatter( + FileRegistrySubtree(base, self.copier) + ) + + +class FlatSubFormatter(object): + """ + Sub-formatter for the flat package format. + """ + + def __init__(self, copier): + assert isinstance(copier, (FileRegistry, FileRegistrySubtree)) + self.copier = copier + self._chrome_db = {} + + def add(self, path, content): + self.copier.add(path, content) + + def add_manifest(self, entry): + # Store manifest entries in a single manifest per directory, named + # after their parent directory, except for root manifests, all named + # chrome.manifest. + if entry.base: + name = mozpath.basename(entry.base) + else: + name = "chrome" + path = mozpath.normpath(mozpath.join(entry.base, "%s.manifest" % name)) + if not self.copier.contains(path): + # Add a reference to the manifest file in the parent manifest, if + # the manifest file is not a root manifest. + if entry.base: + parent = mozpath.dirname(entry.base) + relbase = mozpath.basename(entry.base) + relpath = mozpath.join(relbase, mozpath.basename(path)) + self.add_manifest(Manifest(parent, relpath)) + self.copier.add(path, ManifestFile(entry.base)) + + if isinstance(entry, ManifestChrome): + data = self._chrome_db.setdefault(entry.name, {}) + if isinstance(entry, ManifestMultiContent): + entries = data.setdefault(entry.type, {}).setdefault(entry.id, []) + else: + entries = data.setdefault(entry.type, []) + for e in entries: + # Ideally, we'd actually check whether entry.flags are more + # specific than e.flags, but in practice the following test + # is enough for now. + if entry == e: + errors.warn('"%s" is duplicated. Skipping.' % entry) + return + if not entry.flags or e.flags and entry.flags == e.flags: + errors.fatal('"%s" overrides "%s"' % (entry, e)) + entries.append(entry) + + self.copier[path].add(entry) + + def add_interfaces(self, path, content): + self.copier.add(path, content) + self.add_manifest( + ManifestInterfaces(mozpath.dirname(path), mozpath.basename(path)) + ) + + def contains(self, path): + assert "*" not in path + return self.copier.contains(path) + + +class JarFormatter(PiecemealFormatter): + """ + Formatter for the jar package format. Assumes manifest entries related to + chrome are registered before the chrome data files are added. Also assumes + manifest entries for resources are registered after chrome manifest + entries. + """ + + def __init__(self, copier, compress=True): + PiecemealFormatter.__init__(self, copier) + self._compress = compress + + def _add_base(self, base, addon=False): + if addon is True: + jarrer = Jarrer(self._compress) + self.copier.add(base + ".xpi", jarrer) + self._sub_formatter[base] = FlatSubFormatter(jarrer) + else: + self._sub_formatter[base] = JarSubFormatter( + FileRegistrySubtree(base, self.copier), self._compress + ) + + +class JarSubFormatter(PiecemealFormatter): + """ + Sub-formatter for the jar package format. It is a PiecemealFormatter that + dispatches between further sub-formatter for each of the jar files it + dispatches the chrome data to, and a FlatSubFormatter for the non-chrome + files. + """ + + def __init__(self, copier, compress=True): + PiecemealFormatter.__init__(self, copier) + self._frozen_chrome = False + self._compress = compress + self._sub_formatter[""] = FlatSubFormatter(copier) + + def _jarize(self, entry, relpath): + """ + Transform a manifest entry in one pointing to chrome data in a jar. + Return the corresponding chrome path and the new entry. + """ + base = entry.base + basepath = mozpath.split(relpath)[0] + chromepath = mozpath.join(base, basepath) + entry = ( + entry.rebase(chromepath) + .move(mozpath.join(base, "jar:%s.jar!" % basepath)) + .rebase(base) + ) + return chromepath, entry + + def add_manifest(self, entry): + if isinstance(entry, ManifestChrome) and not urlparse(entry.relpath).scheme: + chromepath, entry = self._jarize(entry, entry.relpath) + assert not self._frozen_chrome + if chromepath not in self._sub_formatter: + jarrer = Jarrer(self._compress) + self.copier.add(chromepath + ".jar", jarrer) + self._sub_formatter[chromepath] = FlatSubFormatter(jarrer) + elif isinstance(entry, ManifestResource) and not urlparse(entry.target).scheme: + chromepath, new_entry = self._jarize(entry, entry.target) + if chromepath in self._sub_formatter: + entry = new_entry + PiecemealFormatter.add_manifest(self, entry) + + +class OmniJarFormatter(JarFormatter): + """ + Formatter for the omnijar package format. + """ + + def __init__(self, copier, omnijar_name, compress=True, non_resources=()): + JarFormatter.__init__(self, copier, compress) + self._omnijar_name = omnijar_name + self._non_resources = non_resources + + def _add_base(self, base, addon=False): + if addon: + # Because add_base is always called with parents before children, + # all the possible ancestry of `base` is already present in + # `_sub_formatter`. + parent_base = mozpath.basedir(base, self._sub_formatter.keys()) + rel_base = mozpath.relpath(base, parent_base) + # If the addon is under a resource directory, package it in the + # omnijar. + parent_sub_formatter = self._sub_formatter[parent_base] + if parent_sub_formatter.is_resource(rel_base): + omnijar_sub_formatter = parent_sub_formatter._sub_formatter[ + self._omnijar_name + ] + self._sub_formatter[base] = FlatSubFormatter( + FileRegistrySubtree(rel_base, omnijar_sub_formatter.copier) + ) + return + JarFormatter._add_base(self, base, addon) + else: + self._sub_formatter[base] = OmniJarSubFormatter( + FileRegistrySubtree(base, self.copier), + self._omnijar_name, + self._compress, + self._non_resources, + ) + + +class OmniJarSubFormatter(PiecemealFormatter): + """ + Sub-formatter for the omnijar package format. It is a PiecemealFormatter + that dispatches between a FlatSubFormatter for the resources data and + another FlatSubFormatter for the other files. + """ + + def __init__(self, copier, omnijar_name, compress=True, non_resources=()): + PiecemealFormatter.__init__(self, copier) + self._omnijar_name = omnijar_name + self._compress = compress + self._non_resources = non_resources + self._sub_formatter[""] = FlatSubFormatter(copier) + jarrer = Jarrer(self._compress) + self._sub_formatter[omnijar_name] = FlatSubFormatter(jarrer) + + def _get_base(self, path): + base = self._omnijar_name if self.is_resource(path) else "" + # Only add the omnijar file if something ends up in it. + if base and not self.copier.contains(base): + self.copier.add(base, self._sub_formatter[base].copier) + return base, path + + def add_manifest(self, entry): + base = "" + if not isinstance(entry, ManifestBinaryComponent): + base = self._omnijar_name + formatter = self._sub_formatter[base] + return formatter.add_manifest(entry) + + def is_resource(self, path): + """ + Return whether the given path corresponds to a resource to be put in an + omnijar archive. + """ + if any(mozpath.match(path, p.replace("*", "**")) for p in self._non_resources): + return False + path = mozpath.split(path) + if path[0] == "chrome": + return len(path) == 1 or path[1] != "icons" + if path[0] == "components": + return path[-1].endswith((".js", ".xpt")) + if path[0] == "res": + return len(path) == 1 or ( + path[1] != "cursors" + and path[1] != "touchbar" + and path[1] != "MainMenu.nib" + ) + if path[0] == "defaults": + return len(path) != 3 or not ( + path[2] == "channel-prefs.js" and path[1] in ["pref", "preferences"] + ) + if len(path) <= 2 and path[-1] == "greprefs.js": + # Accommodate `greprefs.js` and `$ANDROID_CPU_ARCH/greprefs.js`. + return True + return path[0] in [ + "modules", + "actors", + "dictionaries", + "hyphenation", + "localization", + "update.locale", + "contentaccessible", + ] diff --git a/python/mozbuild/mozpack/packager/l10n.py b/python/mozbuild/mozpack/packager/l10n.py new file mode 100644 index 0000000000..76871e15cd --- /dev/null +++ b/python/mozbuild/mozpack/packager/l10n.py @@ -0,0 +1,304 @@ +# 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/. + +""" +Replace localized parts of a packaged directory with data from a langpack +directory. +""" + +import json +import os + +import six +from createprecomplete import generate_precomplete + +import mozpack.path as mozpath +from mozpack.chrome.manifest import ( + Manifest, + ManifestChrome, + ManifestEntryWithRelPath, + ManifestLocale, + is_manifest, +) +from mozpack.copier import FileCopier, Jarrer +from mozpack.errors import errors +from mozpack.files import ComposedFinder, GeneratedFile, ManifestFile +from mozpack.mozjar import JAR_DEFLATED +from mozpack.packager import Component, SimpleManifestSink, SimplePackager +from mozpack.packager.formats import FlatFormatter, JarFormatter, OmniJarFormatter +from mozpack.packager.unpack import UnpackFinder + + +class LocaleManifestFinder(object): + def __init__(self, finder): + entries = self.entries = [] + bases = self.bases = [] + + class MockFormatter(object): + def add_interfaces(self, path, content): + pass + + def add(self, path, content): + pass + + def add_manifest(self, entry): + if entry.localized: + entries.append(entry) + + def add_base(self, base, addon=False): + bases.append(base) + + # SimplePackager rejects "manifest foo.manifest" entries with + # additional flags (such as "manifest foo.manifest application=bar"). + # Those type of entries are used by language packs to work as addons, + # but are not necessary for the purpose of l10n repacking. So we wrap + # the finder in order to remove those entries. + class WrapFinder(object): + def __init__(self, finder): + self._finder = finder + + def find(self, pattern): + for p, f in self._finder.find(pattern): + if isinstance(f, ManifestFile): + unwanted = [ + e for e in f._entries if isinstance(e, Manifest) and e.flags + ] + if unwanted: + f = ManifestFile( + f._base, [e for e in f._entries if e not in unwanted] + ) + yield p, f + + sink = SimpleManifestSink(WrapFinder(finder), MockFormatter()) + sink.add(Component(""), "*") + sink.close(False) + + # Find unique locales used in these manifest entries. + self.locales = list( + set(e.id for e in self.entries if isinstance(e, ManifestLocale)) + ) + + +class L10NRepackFormatterMixin(object): + def __init__(self, *args, **kwargs): + super(L10NRepackFormatterMixin, self).__init__(*args, **kwargs) + self._dictionaries = {} + + def add(self, path, file): + base, relpath = self._get_base(path) + if path.endswith(".dic"): + if relpath.startswith("dictionaries/"): + root, ext = mozpath.splitext(mozpath.basename(path)) + self._dictionaries[root] = path + elif path.endswith("/built_in_addons.json"): + data = json.loads(six.ensure_text(file.open().read())) + data["dictionaries"] = self._dictionaries + # The GeneratedFile content is only really generated after + # all calls to formatter.add. + file = GeneratedFile(lambda: json.dumps(data)) + elif relpath.startswith("META-INF/"): + # Ignore signatures inside omnijars. We drop these items: if we + # don't treat them as omnijar resources, they will be included in + # the top-level package, and that's not how omnijars are signed (Bug + # 1750676). If we treat them as omnijar resources, they will stay + # in the omnijar, as expected -- but the signatures won't be valid + # after repacking. Therefore, drop them. + return + super(L10NRepackFormatterMixin, self).add(path, file) + + +def L10NRepackFormatter(klass): + class L10NRepackFormatter(L10NRepackFormatterMixin, klass): + pass + + return L10NRepackFormatter + + +FlatFormatter = L10NRepackFormatter(FlatFormatter) +JarFormatter = L10NRepackFormatter(JarFormatter) +OmniJarFormatter = L10NRepackFormatter(OmniJarFormatter) + + +def _repack(app_finder, l10n_finder, copier, formatter, non_chrome=set()): + app = LocaleManifestFinder(app_finder) + l10n = LocaleManifestFinder(l10n_finder) + + # The code further below assumes there's only one locale replaced with + # another one. + if len(app.locales) > 1: + errors.fatal("Multiple app locales aren't supported: " + ",".join(app.locales)) + if len(l10n.locales) > 1: + errors.fatal( + "Multiple l10n locales aren't supported: " + ",".join(l10n.locales) + ) + locale = app.locales[0] + l10n_locale = l10n.locales[0] + + # For each base directory, store what path a locale chrome package name + # corresponds to. + # e.g., for the following entry under app/chrome: + # locale foo en-US path/to/files + # keep track that the locale path for foo in app is + # app/chrome/path/to/files. + # As there may be multiple locale entries with the same base, but with + # different flags, that tracking takes the flags into account when there + # are some. Example: + # locale foo en-US path/to/files/win os=Win + # locale foo en-US path/to/files/mac os=Darwin + def key(entry): + if entry.flags: + return "%s %s" % (entry.name, entry.flags) + return entry.name + + l10n_paths = {} + for e in l10n.entries: + if isinstance(e, ManifestChrome): + base = mozpath.basedir(e.path, app.bases) + l10n_paths.setdefault(base, {}) + l10n_paths[base][key(e)] = e.path + + # For chrome and non chrome files or directories, store what langpack path + # corresponds to a package path. + paths = {} + for e in app.entries: + if isinstance(e, ManifestEntryWithRelPath): + base = mozpath.basedir(e.path, app.bases) + if base not in l10n_paths: + errors.fatal("Locale doesn't contain %s/" % base) + # Allow errors to accumulate + continue + if key(e) not in l10n_paths[base]: + errors.fatal("Locale doesn't have a manifest entry for '%s'" % e.name) + # Allow errors to accumulate + continue + paths[e.path] = l10n_paths[base][key(e)] + + for pattern in non_chrome: + for base in app.bases: + path = mozpath.join(base, pattern) + left = set(p for p, f in app_finder.find(path)) + right = set(p for p, f in l10n_finder.find(path)) + for p in right: + paths[p] = p + for p in left - right: + paths[p] = None + + # Create a new package, with non localized bits coming from the original + # package, and localized bits coming from the langpack. + packager = SimplePackager(formatter) + for p, f in app_finder: + if is_manifest(p): + # Remove localized manifest entries. + for e in [e for e in f if e.localized]: + f.remove(e) + # If the path is one that needs a locale replacement, use the + # corresponding file from the langpack. + path = None + if p in paths: + path = paths[p] + if not path: + continue + else: + base = mozpath.basedir(p, paths.keys()) + if base: + subpath = mozpath.relpath(p, base) + path = mozpath.normpath(mozpath.join(paths[base], subpath)) + + if path: + files = [f for p, f in l10n_finder.find(path)] + if not len(files): + if base not in non_chrome: + finderBase = "" + if hasattr(l10n_finder, "base"): + finderBase = l10n_finder.base + errors.error("Missing file: %s" % os.path.join(finderBase, path)) + else: + packager.add(path, files[0]) + else: + packager.add(p, f) + + # Add localized manifest entries from the langpack. + l10n_manifests = [] + for base in set(e.base for e in l10n.entries): + m = ManifestFile(base, [e for e in l10n.entries if e.base == base]) + path = mozpath.join(base, "chrome.%s.manifest" % l10n_locale) + l10n_manifests.append((path, m)) + bases = packager.get_bases() + for path, m in l10n_manifests: + base = mozpath.basedir(path, bases) + packager.add(path, m) + # Add a "manifest $path" entry in the top manifest under that base. + m = ManifestFile(base) + m.add(Manifest(base, mozpath.relpath(path, base))) + packager.add(mozpath.join(base, "chrome.manifest"), m) + + packager.close() + + # Add any remaining non chrome files. + for pattern in non_chrome: + for base in bases: + for p, f in l10n_finder.find(mozpath.join(base, pattern)): + if not formatter.contains(p): + formatter.add(p, f) + + # Resources in `localization` directories are packaged from the source and then + # if localized versions are present in the l10n dir, we package them as well + # keeping the source dir resources as a runtime fallback. + for p, f in l10n_finder.find("**/localization"): + if not formatter.contains(p): + formatter.add(p, f) + + # Transplant jar preloading information. + for path, log in six.iteritems(app_finder.jarlogs): + assert isinstance(copier[path], Jarrer) + copier[path].preload([l.replace(locale, l10n_locale) for l in log]) + + +def repack( + source, l10n, extra_l10n={}, non_resources=[], non_chrome=set(), minify=False +): + """ + Replace localized data from the `source` directory with localized data + from `l10n` and `extra_l10n`. + + The `source` argument points to a directory containing a packaged + application (in omnijar, jar or flat form). + The `l10n` argument points to a directory containing the main localized + data (usually in the form of a language pack addon) to use to replace + in the packaged application. + The `extra_l10n` argument contains a dict associating relative paths in + the source to separate directories containing localized data for them. + This can be used to point at different language pack addons for different + parts of the package application. + The `non_resources` argument gives a list of relative paths in the source + that should not be added in an omnijar in case the packaged application + is in that format. + The `non_chrome` argument gives a list of file/directory patterns for + localized files that are not listed in a chrome.manifest. + If `minify`, `.properties` files are minified. + """ + app_finder = UnpackFinder(source, minify=minify) + l10n_finder = UnpackFinder(l10n, minify=minify) + if extra_l10n: + finders = { + "": l10n_finder, + } + for base, path in six.iteritems(extra_l10n): + finders[base] = UnpackFinder(path, minify=minify) + l10n_finder = ComposedFinder(finders) + copier = FileCopier() + compress = min(app_finder.compressed, JAR_DEFLATED) + if app_finder.kind == "flat": + formatter = FlatFormatter(copier) + elif app_finder.kind == "jar": + formatter = JarFormatter(copier, compress=compress) + elif app_finder.kind == "omni": + formatter = OmniJarFormatter( + copier, app_finder.omnijar, compress=compress, non_resources=non_resources + ) + + with errors.accumulate(): + _repack(app_finder, l10n_finder, copier, formatter, non_chrome) + copier.copy(source, skip_if_older=False) + generate_precomplete(source) diff --git a/python/mozbuild/mozpack/packager/unpack.py b/python/mozbuild/mozpack/packager/unpack.py new file mode 100644 index 0000000000..dff295eb9b --- /dev/null +++ b/python/mozbuild/mozpack/packager/unpack.py @@ -0,0 +1,200 @@ +# 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/. + +import codecs + +from six.moves.urllib.parse import urlparse + +import mozpack.path as mozpath +from mozpack.chrome.manifest import ( + ManifestEntryWithRelPath, + ManifestResource, + is_manifest, + parse_manifest, +) +from mozpack.copier import FileCopier, FileRegistry +from mozpack.files import BaseFinder, DeflatedFile, FileFinder, ManifestFile +from mozpack.mozjar import JarReader +from mozpack.packager import SimplePackager +from mozpack.packager.formats import FlatFormatter + + +class UnpackFinder(BaseFinder): + """ + Special Finder object that treats the source package directory as if it + were in the flat chrome format, whatever chrome format it actually is in. + + This means that for example, paths like chrome/browser/content/... match + files under jar:chrome/browser.jar!/content/... in case of jar chrome + format. + + The only argument to the constructor is a Finder instance or a path. + The UnpackFinder is populated with files from this Finder instance, + or with files from a FileFinder using the given path as its root. + """ + + def __init__(self, source, omnijar_name=None, unpack_xpi=True, **kwargs): + if isinstance(source, BaseFinder): + assert not kwargs + self._finder = source + else: + self._finder = FileFinder(source, **kwargs) + self.base = self._finder.base + self.files = FileRegistry() + self.kind = "flat" + if omnijar_name: + self.omnijar = omnijar_name + else: + # Can't include globally because of bootstrapping issues. + from buildconfig import substs + + self.omnijar = substs.get("OMNIJAR_NAME", "omni.ja") + self.jarlogs = {} + self.compressed = False + self._unpack_xpi = unpack_xpi + + jars = set() + + for p, f in self._finder.find("*"): + # Skip the precomplete file, which is generated at packaging time. + if p == "precomplete": + continue + base = mozpath.dirname(p) + # If the file matches the omnijar pattern, it is an omnijar. + # All the files it contains go in the directory containing the full + # pattern. Manifests are merged if there is a corresponding manifest + # in the directory. + if self._maybe_zip(f) and mozpath.match(p, "**/%s" % self.omnijar): + jar = self._open_jar(p, f) + if "chrome.manifest" in jar: + self.kind = "omni" + self._fill_with_jar(p[: -len(self.omnijar) - 1], jar) + continue + # If the file is a manifest, scan its entries for some referencing + # jar: urls. If there are some, the files contained in the jar they + # point to, go under a directory named after the jar. + if is_manifest(p): + m = self.files[p] if self.files.contains(p) else ManifestFile(base) + for e in parse_manifest( + self.base, p, codecs.getreader("utf-8")(f.open()) + ): + m.add(self._handle_manifest_entry(e, jars)) + if self.files.contains(p): + continue + f = m + # If we're unpacking packed addons and the file is a packed addon, + # unpack it under a directory named after the xpi. + if self._unpack_xpi and p.endswith(".xpi") and self._maybe_zip(f): + self._fill_with_jar(p[:-4], self._open_jar(p, f)) + continue + if p not in jars: + self.files.add(p, f) + + def _fill_with_jar(self, base, jar): + for j in jar: + path = mozpath.join(base, j.filename) + if is_manifest(j.filename): + m = ( + self.files[path] + if self.files.contains(path) + else ManifestFile(mozpath.dirname(path)) + ) + for e in parse_manifest(None, path, j): + m.add(e) + if not self.files.contains(path): + self.files.add(path, m) + continue + else: + self.files.add(path, DeflatedFile(j)) + + def _handle_manifest_entry(self, entry, jars): + jarpath = None + if ( + isinstance(entry, ManifestEntryWithRelPath) + and urlparse(entry.relpath).scheme == "jar" + ): + jarpath, entry = self._unjarize(entry, entry.relpath) + elif ( + isinstance(entry, ManifestResource) + and urlparse(entry.target).scheme == "jar" + ): + jarpath, entry = self._unjarize(entry, entry.target) + if jarpath: + # Don't defer unpacking the jar file. If we already saw + # it, take (and remove) it from the registry. If we + # haven't, try to find it now. + if self.files.contains(jarpath): + jar = self.files[jarpath] + self.files.remove(jarpath) + else: + jar = [f for p, f in self._finder.find(jarpath)] + assert len(jar) == 1 + jar = jar[0] + if jarpath not in jars: + base = mozpath.splitext(jarpath)[0] + for j in self._open_jar(jarpath, jar): + self.files.add(mozpath.join(base, j.filename), DeflatedFile(j)) + jars.add(jarpath) + self.kind = "jar" + return entry + + def _open_jar(self, path, file): + """ + Return a JarReader for the given BaseFile instance, keeping a log of + the preloaded entries it has. + """ + jar = JarReader(fileobj=file.open()) + self.compressed = max(self.compressed, jar.compression) + if jar.last_preloaded: + jarlog = list(jar.entries.keys()) + self.jarlogs[path] = jarlog[: jarlog.index(jar.last_preloaded) + 1] + return jar + + def find(self, path): + for p in self.files.match(path): + yield p, self.files[p] + + def _maybe_zip(self, file): + """ + Return whether the given BaseFile looks like a ZIP/Jar. + """ + header = file.open().read(8) + return len(header) == 8 and (header[0:2] == b"PK" or header[4:6] == b"PK") + + def _unjarize(self, entry, relpath): + """ + Transform a manifest entry pointing to chrome data in a jar in one + pointing to the corresponding unpacked path. Return the jar path and + the new entry. + """ + base = entry.base + jar, relpath = urlparse(relpath).path.split("!", 1) + entry = ( + entry.rebase(mozpath.join(base, "jar:%s!" % jar)) + .move(mozpath.join(base, mozpath.splitext(jar)[0])) + .rebase(base) + ) + return mozpath.join(base, jar), entry + + +def unpack_to_registry(source, registry, omnijar_name=None): + """ + Transform a jar chrome or omnijar packaged directory into a flat package. + + The given registry is filled with the flat package. + """ + finder = UnpackFinder(source, omnijar_name) + packager = SimplePackager(FlatFormatter(registry)) + for p, f in finder.find("*"): + packager.add(p, f) + packager.close() + + +def unpack(source, omnijar_name=None): + """ + Transform a jar chrome or omnijar packaged directory into a flat package. + """ + copier = FileCopier() + unpack_to_registry(source, copier, omnijar_name) + copier.copy(source, skip_if_older=False) diff --git a/python/mozbuild/mozpack/path.py b/python/mozbuild/mozpack/path.py new file mode 100644 index 0000000000..3e5af0a06b --- /dev/null +++ b/python/mozbuild/mozpack/path.py @@ -0,0 +1,246 @@ +# 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/. + +""" +Like :py:mod:`os.path`, with a reduced set of functions, and with normalized path +separators (always use forward slashes). +Also contains a few additional utilities not found in :py:mod:`os.path`. +""" + +import ctypes +import os +import posixpath +import re +import sys + + +def normsep(path): + """ + Normalize path separators, by using forward slashes instead of whatever + :py:const:`os.sep` is. + """ + if os.sep != "/": + # Python 2 is happy to do things like byte_string.replace(u'foo', + # u'bar'), but not Python 3. + if isinstance(path, bytes): + path = path.replace(os.sep.encode("ascii"), b"/") + else: + path = path.replace(os.sep, "/") + if os.altsep and os.altsep != "/": + if isinstance(path, bytes): + path = path.replace(os.altsep.encode("ascii"), b"/") + else: + path = path.replace(os.altsep, "/") + return path + + +def cargo_workaround(path): + unc = "//?/" + if path.startswith(unc): + return path[len(unc) :] + return path + + +def relpath(path, start): + path = normsep(path) + start = normsep(start) + if sys.platform == "win32": + # os.path.relpath can't handle relative paths between UNC and non-UNC + # paths, so strip a //?/ prefix if present (bug 1581248) + path = cargo_workaround(path) + start = cargo_workaround(start) + try: + rel = os.path.relpath(path, start) + except ValueError: + # On Windows this can throw a ValueError if the two paths are on + # different drives. In that case, just return the path. + return abspath(path) + rel = normsep(rel) + return "" if rel == "." else rel + + +def realpath(path): + return normsep(os.path.realpath(path)) + + +def abspath(path): + return normsep(os.path.abspath(path)) + + +def join(*paths): + return normsep(os.path.join(*paths)) + + +def normpath(path): + return posixpath.normpath(normsep(path)) + + +def dirname(path): + return posixpath.dirname(normsep(path)) + + +def commonprefix(paths): + return posixpath.commonprefix([normsep(path) for path in paths]) + + +def basename(path): + return os.path.basename(path) + + +def splitext(path): + return posixpath.splitext(normsep(path)) + + +def split(path): + """ + Return the normalized path as a list of its components. + + ``split('foo/bar/baz')`` returns ``['foo', 'bar', 'baz']`` + """ + return normsep(path).split("/") + + +def basedir(path, bases): + """ + Given a list of directories (`bases`), return which one contains the given + path. If several matches are found, the deepest base directory is returned. + + ``basedir('foo/bar/baz', ['foo', 'baz', 'foo/bar'])`` returns ``'foo/bar'`` + (`'foo'` and `'foo/bar'` both match, but `'foo/bar'` is the deepest match) + """ + path = normsep(path) + bases = [normsep(b) for b in bases] + if path in bases: + return path + for b in sorted(bases, reverse=True): + if b == "" or path.startswith(b + "/"): + return b + + +re_cache = {} +# Python versions < 3.7 return r'\/' for re.escape('/'). +if re.escape("/") == "/": + MATCH_STAR_STAR_RE = re.compile(r"(^|/)\\\*\\\*/") + MATCH_STAR_STAR_END_RE = re.compile(r"(^|/)\\\*\\\*$") +else: + MATCH_STAR_STAR_RE = re.compile(r"(^|\\\/)\\\*\\\*\\\/") + MATCH_STAR_STAR_END_RE = re.compile(r"(^|\\\/)\\\*\\\*$") + + +def match(path, pattern): + """ + Return whether the given path matches the given pattern. + An asterisk can be used to match any string, including the null string, in + one part of the path: + + ``foo`` matches ``*``, ``f*`` or ``fo*o`` + + However, an asterisk matching a subdirectory may not match the null string: + + ``foo/bar`` does *not* match ``foo/*/bar`` + + If the pattern matches one of the ancestor directories of the path, the + patch is considered matching: + + ``foo/bar`` matches ``foo`` + + Two adjacent asterisks can be used to match files and zero or more + directories and subdirectories. + + ``foo/bar`` matches ``foo/**/bar``, or ``**/bar`` + """ + if not pattern: + return True + if pattern not in re_cache: + p = re.escape(pattern) + p = MATCH_STAR_STAR_RE.sub(r"\1(?:.+/)?", p) + p = MATCH_STAR_STAR_END_RE.sub(r"(?:\1.+)?", p) + p = p.replace(r"\*", "[^/]*") + "(?:/.*)?$" + re_cache[pattern] = re.compile(p) + return re_cache[pattern].match(path) is not None + + +def rebase(oldbase, base, relativepath): + """ + Return `relativepath` relative to `base` instead of `oldbase`. + """ + if base == oldbase: + return relativepath + if len(base) < len(oldbase): + assert basedir(oldbase, [base]) == base + relbase = relpath(oldbase, base) + result = join(relbase, relativepath) + else: + assert basedir(base, [oldbase]) == oldbase + relbase = relpath(base, oldbase) + result = relpath(relativepath, relbase) + result = normpath(result) + if relativepath.endswith("/") and not result.endswith("/"): + result += "/" + return result + + +def readlink(path): + if hasattr(os, "readlink"): + return normsep(os.readlink(path)) + + # Unfortunately os.path.realpath doesn't support symlinks on Windows, and os.readlink + # is only available on Windows with Python 3.2+. We have to resort to ctypes... + + assert sys.platform == "win32" + + CreateFileW = ctypes.windll.kernel32.CreateFileW + CreateFileW.argtypes = [ + ctypes.wintypes.LPCWSTR, + ctypes.wintypes.DWORD, + ctypes.wintypes.DWORD, + ctypes.wintypes.LPVOID, + ctypes.wintypes.DWORD, + ctypes.wintypes.DWORD, + ctypes.wintypes.HANDLE, + ] + CreateFileW.restype = ctypes.wintypes.HANDLE + + GENERIC_READ = 0x80000000 + FILE_SHARE_READ = 0x00000001 + OPEN_EXISTING = 3 + FILE_FLAG_BACKUP_SEMANTICS = 0x02000000 + + handle = CreateFileW( + path, + GENERIC_READ, + FILE_SHARE_READ, + 0, + OPEN_EXISTING, + FILE_FLAG_BACKUP_SEMANTICS, + 0, + ) + assert handle != 1, "Failed getting a handle to: {}".format(path) + + MAX_PATH = 260 + + buf = ctypes.create_unicode_buffer(MAX_PATH) + GetFinalPathNameByHandleW = ctypes.windll.kernel32.GetFinalPathNameByHandleW + GetFinalPathNameByHandleW.argtypes = [ + ctypes.wintypes.HANDLE, + ctypes.wintypes.LPWSTR, + ctypes.wintypes.DWORD, + ctypes.wintypes.DWORD, + ] + GetFinalPathNameByHandleW.restype = ctypes.wintypes.DWORD + + FILE_NAME_NORMALIZED = 0x0 + + rv = GetFinalPathNameByHandleW(handle, buf, MAX_PATH, FILE_NAME_NORMALIZED) + assert rv != 0 and rv <= MAX_PATH, "Failed getting final path for: {}".format(path) + + CloseHandle = ctypes.windll.kernel32.CloseHandle + CloseHandle.argtypes = [ctypes.wintypes.HANDLE] + CloseHandle.restype = ctypes.wintypes.BOOL + + rv = CloseHandle(handle) + assert rv != 0, "Failed closing handle" + + # Remove leading '\\?\' from the result. + return normsep(buf.value[4:]) diff --git a/python/mozbuild/mozpack/pkg.py b/python/mozbuild/mozpack/pkg.py new file mode 100644 index 0000000000..75a63b9746 --- /dev/null +++ b/python/mozbuild/mozpack/pkg.py @@ -0,0 +1,299 @@ +# 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/. + +import concurrent.futures +import lzma +import os +import plistlib +import struct +import subprocess +from pathlib import Path +from string import Template +from typing import List +from urllib.parse import quote + +import mozfile + +TEMPLATE_DIRECTORY = Path(__file__).parent / "apple_pkg" +PBZX_CHUNK_SIZE = 16 * 1024 * 1024 # 16MB chunks + + +def get_apple_template(name: str) -> Template: + """ + Given <name>, open file at <TEMPLATE_DIRECTORY>/<name>, read contents and + return as a Template + + Args: + name: str, Filename for the template + + Returns: + Template, loaded from file + """ + tmpl_path = TEMPLATE_DIRECTORY / name + if not tmpl_path.is_file(): + raise Exception(f"Could not find template: {tmpl_path}") + with tmpl_path.open("r") as tmpl: + contents = tmpl.read() + return Template(contents) + + +def save_text_file(content: str, destination: Path): + """ + Saves a text file to <destination> with provided <content> + Note: Overwrites contents + + Args: + content: str, The desired contents of the file + destination: Path, The file path + """ + with destination.open("w") as out_fd: + out_fd.write(content) + print(f"Created text file at {destination}") + print(f"Created text file size: {destination.stat().st_size} bytes") + + +def get_app_info_plist(app_path: Path) -> dict: + """ + Retrieve most information from Info.plist file of an app. + The Info.plist file should be located in ?.app/Contents/Info.plist + + Note: Ignores properties that are not <string> type + + Args: + app_path: Path, the .app file/directory path + + Returns: + dict, the dictionary of properties found in Info.plist + """ + info_plist = app_path / "Contents/Info.plist" + if not info_plist.is_file(): + raise Exception(f"Could not find Info.plist in {info_plist}") + + print(f"Reading app Info.plist from: {info_plist}") + + with info_plist.open("rb") as plist_fd: + data = plistlib.load(plist_fd) + + return data + + +def create_payload(destination: Path, root_path: Path, cpio_tool: str): + """ + Creates a payload at <destination> based on <root_path> + + Args: + destination: Path, the destination Path + root_path: Path, the root directory Path + cpio_tool: str, + """ + # Files to be cpio'd are root folder + contents + file_list = ["./"] + get_relative_glob_list(root_path, "**/*") + + with mozfile.TemporaryDirectory() as tmp_dir: + tmp_payload_path = Path(tmp_dir) / "Payload" + print(f"Creating Payload with cpio from {root_path} to {tmp_payload_path}") + print(f"Found {len(file_list)} files") + with tmp_payload_path.open("wb") as tmp_payload: + process = subprocess.run( + [ + cpio_tool, + "-o", # copy-out mode + "--format", + "odc", # old POSIX .1 portable format + "--owner", + "0:80", # clean ownership + ], + stdout=tmp_payload, + stderr=subprocess.PIPE, + input="\n".join(file_list) + "\n", + encoding="ascii", + cwd=root_path, + ) + # cpio outputs number of blocks to stderr + print(f"[CPIO]: {process.stderr}") + if process.returncode: + raise Exception(f"CPIO error {process.returncode}") + + tmp_payload_size = tmp_payload_path.stat().st_size + print(f"Uncompressed Payload size: {tmp_payload_size // 1024}kb") + + def compress_chunk(chunk): + compressed_chunk = lzma.compress(chunk) + return len(chunk), compressed_chunk + + def chunker(fileobj, chunk_size): + while True: + chunk = fileobj.read(chunk_size) + if not chunk: + break + yield chunk + + with tmp_payload_path.open("rb") as f_in, destination.open( + "wb" + ) as f_out, concurrent.futures.ThreadPoolExecutor( + max_workers=os.cpu_count() + ) as executor: + f_out.write(b"pbzx") + f_out.write(struct.pack(">Q", PBZX_CHUNK_SIZE)) + chunks = chunker(f_in, PBZX_CHUNK_SIZE) + for uncompressed_size, compressed_chunk in executor.map( + compress_chunk, chunks + ): + f_out.write(struct.pack(">Q", uncompressed_size)) + if len(compressed_chunk) < uncompressed_size: + f_out.write(struct.pack(">Q", len(compressed_chunk))) + f_out.write(compressed_chunk) + else: + # Considering how unlikely this is, we prefer to just decompress + # here than to keep the original uncompressed chunk around + f_out.write(struct.pack(">Q", uncompressed_size)) + f_out.write(lzma.decompress(compressed_chunk)) + + print(f"Compressed Payload file to {destination}") + print(f"Compressed Payload size: {destination.stat().st_size // 1024}kb") + + +def create_bom(bom_path: Path, root_path: Path, mkbom_tool: Path): + """ + Creates a Bill Of Materials file at <bom_path> based on <root_path> + + Args: + bom_path: Path, destination Path for the BOM file + root_path: Path, root directory Path + mkbom_tool: Path, mkbom tool Path + """ + print(f"Creating BOM file from {root_path} to {bom_path}") + subprocess.check_call( + [ + mkbom_tool, + "-u", + "0", + "-g", + "80", + str(root_path), + str(bom_path), + ] + ) + print(f"Created BOM File size: {bom_path.stat().st_size // 1024}kb") + + +def get_relative_glob_list(source: Path, glob: str) -> List[str]: + """ + Given a source path, return a list of relative path based on glob + + Args: + source: Path, source directory Path + glob: str, unix style glob + + Returns: + list[str], paths found in source directory + """ + return [f"./{c.relative_to(source)}" for c in source.glob(glob)] + + +def xar_package_folder(source_path: Path, destination: Path, xar_tool: Path): + """ + Create a pkg from <source_path> to <destination> + The command is issued with <source_path> as cwd + + Args: + source_path: Path, source absolute Path + destination: Path, destination absolute Path + xar_tool: Path, xar tool Path + """ + if not source_path.is_absolute() or not destination.is_absolute(): + raise Exception("Source and destination should be absolute.") + + print(f"Creating pkg from {source_path} to {destination}") + # Create a list of ./<file> - noting xar takes care of <file>/** + file_list = get_relative_glob_list(source_path, "*") + + subprocess.check_call( + [ + xar_tool, + "--compression", + "none", + "-vcf", + destination, + *file_list, + ], + cwd=source_path, + ) + print(f"Created PKG file to {destination}") + print(f"Created PKG size: {destination.stat().st_size // 1024}kb") + + +def create_pkg( + source_app: Path, + output_pkg: Path, + mkbom_tool: Path, + xar_tool: Path, + cpio_tool: Path, +): + """ + Create a mac PKG installer from <source_app> to <output_pkg> + + Args: + source_app: Path, source .app file/directory Path + output_pkg: Path, destination .pkg file + mkbom_tool: Path, mkbom tool Path + xar_tool: Path, xar tool Path + cpio: Path, cpio tool Path + """ + + app_name = source_app.name.rsplit(".", maxsplit=1)[0] + + with mozfile.TemporaryDirectory() as tmpdir: + root_path = Path(tmpdir) / "darwin/root" + flat_path = Path(tmpdir) / "darwin/flat" + + # Create required directories + # TODO: Investigate Resources folder contents for other lproj? + (flat_path / "Resources/en.lproj").mkdir(parents=True, exist_ok=True) + (flat_path / f"{app_name}.pkg").mkdir(parents=True, exist_ok=True) + root_path.mkdir(parents=True, exist_ok=True) + + # Copy files over + subprocess.check_call( + [ + "cp", + "-R", + str(source_app), + str(root_path), + ] + ) + + # Count all files (innards + itself) + file_count = len(list(source_app.glob("**/*"))) + 1 + print(f"Calculated source files count: {file_count}") + # Get package contents size + package_size = sum(f.stat().st_size for f in source_app.glob("**/*")) // 1024 + print(f"Calculated source package size: {package_size}kb") + + app_info = get_app_info_plist(source_app) + app_info["numberOfFiles"] = file_count + app_info["installKBytes"] = package_size + app_info["app_name"] = app_name + app_info["app_name_url_encoded"] = quote(app_name) + + # This seems arbitrary, there might be another way of doing it, + # but Info.plist doesn't provide the simple version we need + major_version = app_info["CFBundleShortVersionString"].split(".")[0] + app_info["simple_version"] = f"{major_version}.0.0" + + pkg_info_tmpl = get_apple_template("PackageInfo.template") + pkg_info = pkg_info_tmpl.substitute(app_info) + save_text_file(pkg_info, flat_path / f"{app_name}.pkg/PackageInfo") + + distribution_tmp = get_apple_template("Distribution.template") + distribution = distribution_tmp.substitute(app_info) + save_text_file(distribution, flat_path / "Distribution") + + payload_path = flat_path / f"{app_name}.pkg/Payload" + create_payload(payload_path, root_path, cpio_tool) + + bom_path = flat_path / f"{app_name}.pkg/Bom" + create_bom(bom_path, root_path, mkbom_tool) + + xar_package_folder(flat_path, output_pkg, xar_tool) diff --git a/python/mozbuild/mozpack/test/__init__.py b/python/mozbuild/mozpack/test/__init__.py new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/python/mozbuild/mozpack/test/__init__.py diff --git a/python/mozbuild/mozpack/test/data/test_data b/python/mozbuild/mozpack/test/data/test_data new file mode 100644 index 0000000000..fb7f0c4fc2 --- /dev/null +++ b/python/mozbuild/mozpack/test/data/test_data @@ -0,0 +1 @@ +test_data
\ No newline at end of file diff --git a/python/mozbuild/mozpack/test/python.toml b/python/mozbuild/mozpack/test/python.toml new file mode 100644 index 0000000000..c886e78cf1 --- /dev/null +++ b/python/mozbuild/mozpack/test/python.toml @@ -0,0 +1,32 @@ +[DEFAULT] +subsuite = "mozbuild" + +["test_archive.py"] + +["test_chrome_flags.py"] + +["test_chrome_manifest.py"] + +["test_copier.py"] + +["test_errors.py"] + +["test_files.py"] + +["test_manifests.py"] + +["test_mozjar.py"] + +["test_packager.py"] + +["test_packager_formats.py"] + +["test_packager_l10n.py"] + +["test_packager_unpack.py"] + +["test_path.py"] + +["test_pkg.py"] + +["test_unify.py"] diff --git a/python/mozbuild/mozpack/test/support/minify_js_verify.py b/python/mozbuild/mozpack/test/support/minify_js_verify.py new file mode 100644 index 0000000000..88cc0ece0c --- /dev/null +++ b/python/mozbuild/mozpack/test/support/minify_js_verify.py @@ -0,0 +1,15 @@ +# 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/. + +import sys + +if len(sys.argv) != 4: + raise Exception("Usage: minify_js_verify <exitcode> <orig> <minified>") + +retcode = int(sys.argv[1]) + +if retcode: + print("Error message", file=sys.stderr) + +sys.exit(retcode) diff --git a/python/mozbuild/mozpack/test/test_archive.py b/python/mozbuild/mozpack/test/test_archive.py new file mode 100644 index 0000000000..3417f279df --- /dev/null +++ b/python/mozbuild/mozpack/test/test_archive.py @@ -0,0 +1,197 @@ +# 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/. + +import hashlib +import os +import shutil +import stat +import tarfile +import tempfile +import unittest + +import pytest +from mozunit import main + +from mozpack.archive import ( + DEFAULT_MTIME, + create_tar_bz2_from_files, + create_tar_from_files, + create_tar_gz_from_files, +) +from mozpack.files import GeneratedFile + +MODE_STANDARD = stat.S_IRUSR | stat.S_IWUSR | stat.S_IRGRP | stat.S_IROTH + + +def file_hash(path): + h = hashlib.sha1() + with open(path, "rb") as fh: + while True: + data = fh.read(8192) + if not data: + break + h.update(data) + + return h.hexdigest() + + +class TestArchive(unittest.TestCase): + def _create_files(self, root): + files = {} + for i in range(10): + p = os.path.join(root, "file%02d" % i) + with open(p, "wb") as fh: + fh.write(b"file%02d" % i) + # Need to set permissions or umask may influence testing. + os.chmod(p, MODE_STANDARD) + files["file%02d" % i] = p + + for i in range(10): + files["file%02d" % (i + 10)] = GeneratedFile(b"file%02d" % (i + 10)) + + return files + + def _verify_basic_tarfile(self, tf): + self.assertEqual(len(tf.getmembers()), 20) + + names = ["file%02d" % i for i in range(20)] + self.assertEqual(tf.getnames(), names) + + for ti in tf.getmembers(): + self.assertEqual(ti.uid, 0) + self.assertEqual(ti.gid, 0) + self.assertEqual(ti.uname, "") + self.assertEqual(ti.gname, "") + self.assertEqual(ti.mode, MODE_STANDARD) + self.assertEqual(ti.mtime, DEFAULT_MTIME) + + @pytest.mark.xfail( + reason="ValueError is not thrown despite being provided directory." + ) + def test_dirs_refused(self): + d = tempfile.mkdtemp() + try: + tp = os.path.join(d, "test.tar") + with open(tp, "wb") as fh: + with self.assertRaisesRegexp(ValueError, "not a regular"): + create_tar_from_files(fh, {"test": d}) + finally: + shutil.rmtree(d) + + @pytest.mark.xfail(reason="ValueError is not thrown despite uid/gid being set.") + def test_setuid_setgid_refused(self): + d = tempfile.mkdtemp() + try: + uid = os.path.join(d, "setuid") + gid = os.path.join(d, "setgid") + with open(uid, "a"): + pass + with open(gid, "a"): + pass + + os.chmod(uid, MODE_STANDARD | stat.S_ISUID) + os.chmod(gid, MODE_STANDARD | stat.S_ISGID) + + tp = os.path.join(d, "test.tar") + with open(tp, "wb") as fh: + with self.assertRaisesRegexp(ValueError, "cannot add file with setuid"): + create_tar_from_files(fh, {"test": uid}) + with self.assertRaisesRegexp(ValueError, "cannot add file with setuid"): + create_tar_from_files(fh, {"test": gid}) + finally: + shutil.rmtree(d) + + def test_create_tar_basic(self): + d = tempfile.mkdtemp() + try: + files = self._create_files(d) + + tp = os.path.join(d, "test.tar") + with open(tp, "wb") as fh: + create_tar_from_files(fh, files) + + # Output should be deterministic. + self.assertEqual(file_hash(tp), "01cd314e277f060e98c7de6c8ea57f96b3a2065c") + + with tarfile.open(tp, "r") as tf: + self._verify_basic_tarfile(tf) + + finally: + shutil.rmtree(d) + + @pytest.mark.xfail(reason="hash mismatch") + def test_executable_preserved(self): + d = tempfile.mkdtemp() + try: + p = os.path.join(d, "exec") + with open(p, "wb") as fh: + fh.write("#!/bin/bash\n") + os.chmod(p, MODE_STANDARD | stat.S_IXUSR) + + tp = os.path.join(d, "test.tar") + with open(tp, "wb") as fh: + create_tar_from_files(fh, {"exec": p}) + + self.assertEqual(file_hash(tp), "357e1b81c0b6cfdfa5d2d118d420025c3c76ee93") + + with tarfile.open(tp, "r") as tf: + m = tf.getmember("exec") + self.assertEqual(m.mode, MODE_STANDARD | stat.S_IXUSR) + + finally: + shutil.rmtree(d) + + def test_create_tar_gz_basic(self): + d = tempfile.mkdtemp() + try: + files = self._create_files(d) + + gp = os.path.join(d, "test.tar.gz") + with open(gp, "wb") as fh: + create_tar_gz_from_files(fh, files) + + self.assertEqual(file_hash(gp), "7c4da5adc5088cdf00911d5daf9a67b15de714b7") + + with tarfile.open(gp, "r:gz") as tf: + self._verify_basic_tarfile(tf) + + finally: + shutil.rmtree(d) + + def test_tar_gz_name(self): + d = tempfile.mkdtemp() + try: + files = self._create_files(d) + + gp = os.path.join(d, "test.tar.gz") + with open(gp, "wb") as fh: + create_tar_gz_from_files(fh, files, filename="foobar") + + self.assertEqual(file_hash(gp), "721e00083c17d16df2edbddf40136298c06d0c49") + + with tarfile.open(gp, "r:gz") as tf: + self._verify_basic_tarfile(tf) + + finally: + shutil.rmtree(d) + + def test_create_tar_bz2_basic(self): + d = tempfile.mkdtemp() + try: + files = self._create_files(d) + + bp = os.path.join(d, "test.tar.bz2") + with open(bp, "wb") as fh: + create_tar_bz2_from_files(fh, files) + + self.assertEqual(file_hash(bp), "eb5096d2fbb71df7b3d690001a6f2e82a5aad6a7") + + with tarfile.open(bp, "r:bz2") as tf: + self._verify_basic_tarfile(tf) + finally: + shutil.rmtree(d) + + +if __name__ == "__main__": + main() diff --git a/python/mozbuild/mozpack/test/test_chrome_flags.py b/python/mozbuild/mozpack/test/test_chrome_flags.py new file mode 100644 index 0000000000..4f1a968dc2 --- /dev/null +++ b/python/mozbuild/mozpack/test/test_chrome_flags.py @@ -0,0 +1,150 @@ +# 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/. + +import unittest + +import mozunit + +from mozpack.chrome.flags import Flag, Flags, StringFlag, VersionFlag +from mozpack.errors import ErrorMessage + + +class TestFlag(unittest.TestCase): + def test_flag(self): + flag = Flag("flag") + self.assertEqual(str(flag), "") + self.assertTrue(flag.matches(False)) + self.assertTrue(flag.matches("false")) + self.assertFalse(flag.matches("true")) + self.assertRaises(ErrorMessage, flag.add_definition, "flag=") + self.assertRaises(ErrorMessage, flag.add_definition, "flag=42") + self.assertRaises(ErrorMessage, flag.add_definition, "flag!=false") + + flag.add_definition("flag=1") + self.assertEqual(str(flag), "flag=1") + self.assertTrue(flag.matches(True)) + self.assertTrue(flag.matches("1")) + self.assertFalse(flag.matches("no")) + + flag.add_definition("flag=true") + self.assertEqual(str(flag), "flag=true") + self.assertTrue(flag.matches(True)) + self.assertTrue(flag.matches("true")) + self.assertFalse(flag.matches("0")) + + flag.add_definition("flag=no") + self.assertEqual(str(flag), "flag=no") + self.assertTrue(flag.matches("false")) + self.assertFalse(flag.matches("1")) + + flag.add_definition("flag") + self.assertEqual(str(flag), "flag") + self.assertFalse(flag.matches("false")) + self.assertTrue(flag.matches("true")) + self.assertFalse(flag.matches(False)) + + def test_string_flag(self): + flag = StringFlag("flag") + self.assertEqual(str(flag), "") + self.assertTrue(flag.matches("foo")) + self.assertRaises(ErrorMessage, flag.add_definition, "flag>=2") + + flag.add_definition("flag=foo") + self.assertEqual(str(flag), "flag=foo") + self.assertTrue(flag.matches("foo")) + self.assertFalse(flag.matches("bar")) + + flag.add_definition("flag=bar") + self.assertEqual(str(flag), "flag=foo flag=bar") + self.assertTrue(flag.matches("foo")) + self.assertTrue(flag.matches("bar")) + self.assertFalse(flag.matches("baz")) + + flag = StringFlag("flag") + flag.add_definition("flag!=bar") + self.assertEqual(str(flag), "flag!=bar") + self.assertTrue(flag.matches("foo")) + self.assertFalse(flag.matches("bar")) + + def test_version_flag(self): + flag = VersionFlag("flag") + self.assertEqual(str(flag), "") + self.assertTrue(flag.matches("1.0")) + self.assertRaises(ErrorMessage, flag.add_definition, "flag!=2") + + flag.add_definition("flag=1.0") + self.assertEqual(str(flag), "flag=1.0") + self.assertTrue(flag.matches("1.0")) + self.assertFalse(flag.matches("2.0")) + + flag.add_definition("flag=2.0") + self.assertEqual(str(flag), "flag=1.0 flag=2.0") + self.assertTrue(flag.matches("1.0")) + self.assertTrue(flag.matches("2.0")) + self.assertFalse(flag.matches("3.0")) + + flag = VersionFlag("flag") + flag.add_definition("flag>=2.0") + self.assertEqual(str(flag), "flag>=2.0") + self.assertFalse(flag.matches("1.0")) + self.assertTrue(flag.matches("2.0")) + self.assertTrue(flag.matches("3.0")) + + flag.add_definition("flag<1.10") + self.assertEqual(str(flag), "flag>=2.0 flag<1.10") + self.assertTrue(flag.matches("1.0")) + self.assertTrue(flag.matches("1.9")) + self.assertFalse(flag.matches("1.10")) + self.assertFalse(flag.matches("1.20")) + self.assertTrue(flag.matches("2.0")) + self.assertTrue(flag.matches("3.0")) + self.assertRaises(Exception, flag.add_definition, "flag<") + self.assertRaises(Exception, flag.add_definition, "flag>") + self.assertRaises(Exception, flag.add_definition, "flag>=") + self.assertRaises(Exception, flag.add_definition, "flag<=") + self.assertRaises(Exception, flag.add_definition, "flag!=1.0") + + +class TestFlags(unittest.TestCase): + def setUp(self): + self.flags = Flags( + "contentaccessible=yes", + "appversion>=3.5", + "application=foo", + "application=bar", + "appversion<2.0", + "platform", + "abi!=Linux_x86-gcc3", + ) + + def test_flags_str(self): + self.assertEqual( + str(self.flags), + "contentaccessible=yes " + + "appversion>=3.5 appversion<2.0 application=foo " + + "application=bar platform abi!=Linux_x86-gcc3", + ) + + def test_flags_match_unset(self): + self.assertTrue(self.flags.match(os="WINNT")) + + def test_flags_match(self): + self.assertTrue(self.flags.match(application="foo")) + self.assertFalse(self.flags.match(application="qux")) + + def test_flags_match_different(self): + self.assertTrue(self.flags.match(abi="WINNT_x86-MSVC")) + self.assertFalse(self.flags.match(abi="Linux_x86-gcc3")) + + def test_flags_match_version(self): + self.assertTrue(self.flags.match(appversion="1.0")) + self.assertTrue(self.flags.match(appversion="1.5")) + self.assertFalse(self.flags.match(appversion="2.0")) + self.assertFalse(self.flags.match(appversion="3.0")) + self.assertTrue(self.flags.match(appversion="3.5")) + self.assertTrue(self.flags.match(appversion="3.10")) + + +if __name__ == "__main__": + mozunit.main() diff --git a/python/mozbuild/mozpack/test/test_chrome_manifest.py b/python/mozbuild/mozpack/test/test_chrome_manifest.py new file mode 100644 index 0000000000..c1d5826bbc --- /dev/null +++ b/python/mozbuild/mozpack/test/test_chrome_manifest.py @@ -0,0 +1,176 @@ +# 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/. + +import os +import unittest + +import mozunit + +from mozpack.chrome.manifest import ( + MANIFESTS_TYPES, + Manifest, + ManifestBinaryComponent, + ManifestCategory, + ManifestComponent, + ManifestContent, + ManifestContract, + ManifestInterfaces, + ManifestLocale, + ManifestOverlay, + ManifestOverride, + ManifestResource, + ManifestSkin, + ManifestStyle, + parse_manifest, + parse_manifest_line, +) +from mozpack.errors import AccumulatedErrors, errors +from test_errors import TestErrors + + +class TestManifest(unittest.TestCase): + def test_parse_manifest(self): + manifest = [ + "content global content/global/", + "content global content/global/ application=foo application=bar" + + " platform", + "locale global en-US content/en-US/", + "locale global en-US content/en-US/ application=foo", + "skin global classic/1.0 content/skin/classic/", + "skin global classic/1.0 content/skin/classic/ application=foo" + + " os=WINNT", + "", + "manifest pdfjs/chrome.manifest", + "resource gre-resources toolkit/res/", + "override chrome://global/locale/netError.dtd" + + " chrome://browser/locale/netError.dtd", + "# Comment", + "component {b2bba4df-057d-41ea-b6b1-94a10a8ede68} foo.js", + "contract @mozilla.org/foo;1" + " {b2bba4df-057d-41ea-b6b1-94a10a8ede68}", + "interfaces foo.xpt", + "binary-component bar.so", + "category command-line-handler m-browser" + + " @mozilla.org/browser/clh;1" + + " application={ec8030f7-c20a-464f-9b0e-13a3a9e97384}", + "style chrome://global/content/viewSource.xul" + " chrome://browser/skin/", + "overlay chrome://global/content/viewSource.xul" + + " chrome://browser/content/viewSourceOverlay.xul", + ] + other_manifest = ["content global content/global/"] + expected_result = [ + ManifestContent("", "global", "content/global/"), + ManifestContent( + "", + "global", + "content/global/", + "application=foo", + "application=bar", + "platform", + ), + ManifestLocale("", "global", "en-US", "content/en-US/"), + ManifestLocale("", "global", "en-US", "content/en-US/", "application=foo"), + ManifestSkin("", "global", "classic/1.0", "content/skin/classic/"), + ManifestSkin( + "", + "global", + "classic/1.0", + "content/skin/classic/", + "application=foo", + "os=WINNT", + ), + Manifest("", "pdfjs/chrome.manifest"), + ManifestResource("", "gre-resources", "toolkit/res/"), + ManifestOverride( + "", + "chrome://global/locale/netError.dtd", + "chrome://browser/locale/netError.dtd", + ), + ManifestComponent("", "{b2bba4df-057d-41ea-b6b1-94a10a8ede68}", "foo.js"), + ManifestContract( + "", "@mozilla.org/foo;1", "{b2bba4df-057d-41ea-b6b1-94a10a8ede68}" + ), + ManifestInterfaces("", "foo.xpt"), + ManifestBinaryComponent("", "bar.so"), + ManifestCategory( + "", + "command-line-handler", + "m-browser", + "@mozilla.org/browser/clh;1", + "application=" + "{ec8030f7-c20a-464f-9b0e-13a3a9e97384}", + ), + ManifestStyle( + "", "chrome://global/content/viewSource.xul", "chrome://browser/skin/" + ), + ManifestOverlay( + "", + "chrome://global/content/viewSource.xul", + "chrome://browser/content/viewSourceOverlay.xul", + ), + ] + with mozunit.MockedOpen( + { + "manifest": "\n".join(manifest), + "other/manifest": "\n".join(other_manifest), + } + ): + # Ensure we have tests for all types of manifests. + self.assertEqual( + set(type(e) for e in expected_result), set(MANIFESTS_TYPES.values()) + ) + self.assertEqual( + list(parse_manifest(os.curdir, "manifest")), expected_result + ) + self.assertEqual( + list(parse_manifest(os.curdir, "other/manifest")), + [ManifestContent("other", "global", "content/global/")], + ) + + def test_manifest_rebase(self): + m = parse_manifest_line("chrome", "content global content/global/") + m = m.rebase("") + self.assertEqual(str(m), "content global chrome/content/global/") + m = m.rebase("chrome") + self.assertEqual(str(m), "content global content/global/") + + m = parse_manifest_line("chrome/foo", "content global content/global/") + m = m.rebase("chrome") + self.assertEqual(str(m), "content global foo/content/global/") + m = m.rebase("chrome/foo") + self.assertEqual(str(m), "content global content/global/") + + m = parse_manifest_line("modules/foo", "resource foo ./") + m = m.rebase("modules") + self.assertEqual(str(m), "resource foo foo/") + m = m.rebase("modules/foo") + self.assertEqual(str(m), "resource foo ./") + + m = parse_manifest_line("chrome", "content browser browser/content/") + m = m.rebase("chrome/browser").move("jar:browser.jar!").rebase("") + self.assertEqual(str(m), "content browser jar:browser.jar!/content/") + + +class TestManifestErrors(TestErrors, unittest.TestCase): + def test_parse_manifest_errors(self): + manifest = [ + "skin global classic/1.0 content/skin/classic/ platform", + "", + "binary-component bar.so", + "unsupported foo", + ] + with mozunit.MockedOpen({"manifest": "\n".join(manifest)}): + with self.assertRaises(AccumulatedErrors): + with errors.accumulate(): + list(parse_manifest(os.curdir, "manifest")) + out = self.get_output() + # Expecting 2 errors + self.assertEqual(len(out), 2) + path = os.path.abspath("manifest") + # First on line 1 + self.assertTrue(out[0].startswith("error: %s:1: " % path)) + # Second on line 4 + self.assertTrue(out[1].startswith("error: %s:4: " % path)) + + +if __name__ == "__main__": + mozunit.main() diff --git a/python/mozbuild/mozpack/test/test_copier.py b/python/mozbuild/mozpack/test/test_copier.py new file mode 100644 index 0000000000..60ebd2c1e9 --- /dev/null +++ b/python/mozbuild/mozpack/test/test_copier.py @@ -0,0 +1,548 @@ +# 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/. + +import os +import stat +import unittest + +import mozunit +import six + +import mozpack.path as mozpath +from mozpack.copier import FileCopier, FileRegistry, FileRegistrySubtree, Jarrer +from mozpack.errors import ErrorMessage +from mozpack.files import ExistingFile, GeneratedFile +from mozpack.mozjar import JarReader +from mozpack.test.test_files import MatchTestTemplate, MockDest, TestWithTmpDir + + +class BaseTestFileRegistry(MatchTestTemplate): + def add(self, path): + self.registry.add(path, GeneratedFile(path)) + + def do_check(self, pattern, result): + self.checked = True + if result: + self.assertTrue(self.registry.contains(pattern)) + else: + self.assertFalse(self.registry.contains(pattern)) + self.assertEqual(self.registry.match(pattern), result) + + def do_test_file_registry(self, registry): + self.registry = registry + self.registry.add("foo", GeneratedFile(b"foo")) + bar = GeneratedFile(b"bar") + self.registry.add("bar", bar) + self.assertEqual(self.registry.paths(), ["foo", "bar"]) + self.assertEqual(self.registry["bar"], bar) + + self.assertRaises( + ErrorMessage, self.registry.add, "foo", GeneratedFile(b"foo2") + ) + + self.assertRaises(ErrorMessage, self.registry.remove, "qux") + + self.assertRaises( + ErrorMessage, self.registry.add, "foo/bar", GeneratedFile(b"foobar") + ) + self.assertRaises( + ErrorMessage, self.registry.add, "foo/bar/baz", GeneratedFile(b"foobar") + ) + + self.assertEqual(self.registry.paths(), ["foo", "bar"]) + + self.registry.remove("foo") + self.assertEqual(self.registry.paths(), ["bar"]) + self.registry.remove("bar") + self.assertEqual(self.registry.paths(), []) + + self.prepare_match_test() + self.do_match_test() + self.assertTrue(self.checked) + self.assertEqual( + self.registry.paths(), + [ + "bar", + "foo/bar", + "foo/baz", + "foo/qux/1", + "foo/qux/bar", + "foo/qux/2/test", + "foo/qux/2/test2", + ], + ) + + self.registry.remove("foo/qux") + self.assertEqual(self.registry.paths(), ["bar", "foo/bar", "foo/baz"]) + + self.registry.add("foo/qux", GeneratedFile(b"fooqux")) + self.assertEqual( + self.registry.paths(), ["bar", "foo/bar", "foo/baz", "foo/qux"] + ) + self.registry.remove("foo/b*") + self.assertEqual(self.registry.paths(), ["bar", "foo/qux"]) + + self.assertEqual([f for f, c in self.registry], ["bar", "foo/qux"]) + self.assertEqual(len(self.registry), 2) + + self.add("foo/.foo") + self.assertTrue(self.registry.contains("foo/.foo")) + + def do_test_registry_paths(self, registry): + self.registry = registry + + # Can't add a file if it requires a directory in place of a + # file we also require. + self.registry.add("foo", GeneratedFile(b"foo")) + self.assertRaises( + ErrorMessage, self.registry.add, "foo/bar", GeneratedFile(b"foobar") + ) + + # Can't add a file if we already have a directory there. + self.registry.add("bar/baz", GeneratedFile(b"barbaz")) + self.assertRaises(ErrorMessage, self.registry.add, "bar", GeneratedFile(b"bar")) + + # Bump the count of things that require bar/ to 2. + self.registry.add("bar/zot", GeneratedFile(b"barzot")) + self.assertRaises(ErrorMessage, self.registry.add, "bar", GeneratedFile(b"bar")) + + # Drop the count of things that require bar/ to 1. + self.registry.remove("bar/baz") + self.assertRaises(ErrorMessage, self.registry.add, "bar", GeneratedFile(b"bar")) + + # Drop the count of things that require bar/ to 0. + self.registry.remove("bar/zot") + self.registry.add("bar/zot", GeneratedFile(b"barzot")) + + +class TestFileRegistry(BaseTestFileRegistry, unittest.TestCase): + def test_partial_paths(self): + cases = { + "foo/bar/baz/zot": ["foo/bar/baz", "foo/bar", "foo"], + "foo/bar": ["foo"], + "bar": [], + } + reg = FileRegistry() + for path, parts in six.iteritems(cases): + self.assertEqual(reg._partial_paths(path), parts) + + def test_file_registry(self): + self.do_test_file_registry(FileRegistry()) + + def test_registry_paths(self): + self.do_test_registry_paths(FileRegistry()) + + def test_required_directories(self): + self.registry = FileRegistry() + + self.registry.add("foo", GeneratedFile(b"foo")) + self.assertEqual(self.registry.required_directories(), set()) + + self.registry.add("bar/baz", GeneratedFile(b"barbaz")) + self.assertEqual(self.registry.required_directories(), {"bar"}) + + self.registry.add("bar/zot", GeneratedFile(b"barzot")) + self.assertEqual(self.registry.required_directories(), {"bar"}) + + self.registry.add("bar/zap/zot", GeneratedFile(b"barzapzot")) + self.assertEqual(self.registry.required_directories(), {"bar", "bar/zap"}) + + self.registry.remove("bar/zap/zot") + self.assertEqual(self.registry.required_directories(), {"bar"}) + + self.registry.remove("bar/baz") + self.assertEqual(self.registry.required_directories(), {"bar"}) + + self.registry.remove("bar/zot") + self.assertEqual(self.registry.required_directories(), set()) + + self.registry.add("x/y/z", GeneratedFile(b"xyz")) + self.assertEqual(self.registry.required_directories(), {"x", "x/y"}) + + +class TestFileRegistrySubtree(BaseTestFileRegistry, unittest.TestCase): + def test_file_registry_subtree_base(self): + registry = FileRegistry() + self.assertEqual(registry, FileRegistrySubtree("", registry)) + self.assertNotEqual(registry, FileRegistrySubtree("base", registry)) + + def create_registry(self): + registry = FileRegistry() + registry.add("foo/bar", GeneratedFile(b"foo/bar")) + registry.add("baz/qux", GeneratedFile(b"baz/qux")) + return FileRegistrySubtree("base/root", registry) + + def test_file_registry_subtree(self): + self.do_test_file_registry(self.create_registry()) + + def test_registry_paths_subtree(self): + FileRegistry() + self.do_test_registry_paths(self.create_registry()) + + +class TestFileCopier(TestWithTmpDir): + def all_dirs(self, base): + all_dirs = set() + for root, dirs, files in os.walk(base): + if not dirs: + all_dirs.add(mozpath.relpath(root, base)) + return all_dirs + + def all_files(self, base): + all_files = set() + for root, dirs, files in os.walk(base): + for f in files: + all_files.add(mozpath.join(mozpath.relpath(root, base), f)) + return all_files + + def test_file_copier(self): + copier = FileCopier() + copier.add("foo/bar", GeneratedFile(b"foobar")) + copier.add("foo/qux", GeneratedFile(b"fooqux")) + copier.add("foo/deep/nested/directory/file", GeneratedFile(b"fooz")) + copier.add("bar", GeneratedFile(b"bar")) + copier.add("qux/foo", GeneratedFile(b"quxfoo")) + copier.add("qux/bar", GeneratedFile(b"")) + + result = copier.copy(self.tmpdir) + self.assertEqual(self.all_files(self.tmpdir), set(copier.paths())) + self.assertEqual( + self.all_dirs(self.tmpdir), set(["foo/deep/nested/directory", "qux"]) + ) + + self.assertEqual( + result.updated_files, + set(self.tmppath(p) for p in self.all_files(self.tmpdir)), + ) + self.assertEqual(result.existing_files, set()) + self.assertEqual(result.removed_files, set()) + self.assertEqual(result.removed_directories, set()) + + copier.remove("foo") + copier.add("test", GeneratedFile(b"test")) + result = copier.copy(self.tmpdir) + self.assertEqual(self.all_files(self.tmpdir), set(copier.paths())) + self.assertEqual(self.all_dirs(self.tmpdir), set(["qux"])) + self.assertEqual( + result.removed_files, + set( + self.tmppath(p) + for p in ("foo/bar", "foo/qux", "foo/deep/nested/directory/file") + ), + ) + + def test_symlink_directory_replaced(self): + """Directory symlinks in destination are replaced if they need to be + real directories.""" + if not self.symlink_supported: + return + + dest = self.tmppath("dest") + + copier = FileCopier() + copier.add("foo/bar/baz", GeneratedFile(b"foobarbaz")) + + os.makedirs(self.tmppath("dest/foo")) + dummy = self.tmppath("dummy") + os.mkdir(dummy) + link = self.tmppath("dest/foo/bar") + os.symlink(dummy, link) + + result = copier.copy(dest) + + st = os.lstat(link) + self.assertFalse(stat.S_ISLNK(st.st_mode)) + self.assertTrue(stat.S_ISDIR(st.st_mode)) + + self.assertEqual(self.all_files(dest), set(copier.paths())) + + self.assertEqual(result.removed_directories, set()) + self.assertEqual(len(result.updated_files), 1) + + def test_remove_unaccounted_directory_symlinks(self): + """Directory symlinks in destination that are not in the way are + deleted according to remove_unaccounted and + remove_all_directory_symlinks. + """ + if not self.symlink_supported: + return + + dest = self.tmppath("dest") + + copier = FileCopier() + copier.add("foo/bar/baz", GeneratedFile(b"foobarbaz")) + + os.makedirs(self.tmppath("dest/foo")) + dummy = self.tmppath("dummy") + os.mkdir(dummy) + + os.mkdir(self.tmppath("dest/zot")) + link = self.tmppath("dest/zot/zap") + os.symlink(dummy, link) + + # If not remove_unaccounted but remove_empty_directories, then + # the symlinked directory remains (as does its containing + # directory). + result = copier.copy( + dest, + remove_unaccounted=False, + remove_empty_directories=True, + remove_all_directory_symlinks=False, + ) + + st = os.lstat(link) + self.assertTrue(stat.S_ISLNK(st.st_mode)) + self.assertFalse(stat.S_ISDIR(st.st_mode)) + + self.assertEqual(self.all_files(dest), set(copier.paths())) + self.assertEqual(self.all_dirs(dest), set(["foo/bar"])) + + self.assertEqual(result.removed_directories, set()) + self.assertEqual(len(result.updated_files), 1) + + # If remove_unaccounted but not remove_empty_directories, then + # only the symlinked directory is removed. + result = copier.copy( + dest, + remove_unaccounted=True, + remove_empty_directories=False, + remove_all_directory_symlinks=False, + ) + + st = os.lstat(self.tmppath("dest/zot")) + self.assertFalse(stat.S_ISLNK(st.st_mode)) + self.assertTrue(stat.S_ISDIR(st.st_mode)) + + self.assertEqual(result.removed_files, set([link])) + self.assertEqual(result.removed_directories, set()) + + self.assertEqual(self.all_files(dest), set(copier.paths())) + self.assertEqual(self.all_dirs(dest), set(["foo/bar", "zot"])) + + # If remove_unaccounted and remove_empty_directories, then + # both the symlink and its containing directory are removed. + link = self.tmppath("dest/zot/zap") + os.symlink(dummy, link) + + result = copier.copy( + dest, + remove_unaccounted=True, + remove_empty_directories=True, + remove_all_directory_symlinks=False, + ) + + self.assertEqual(result.removed_files, set([link])) + self.assertEqual(result.removed_directories, set([self.tmppath("dest/zot")])) + + self.assertEqual(self.all_files(dest), set(copier.paths())) + self.assertEqual(self.all_dirs(dest), set(["foo/bar"])) + + def test_permissions(self): + """Ensure files without write permission can be deleted.""" + with open(self.tmppath("dummy"), "a"): + pass + + p = self.tmppath("no_perms") + with open(p, "a"): + pass + + # Make file and directory unwritable. Reminder: making a directory + # unwritable prevents modifications (including deletes) from the list + # of files in that directory. + os.chmod(p, 0o400) + os.chmod(self.tmpdir, 0o400) + + copier = FileCopier() + copier.add("dummy", GeneratedFile(b"content")) + result = copier.copy(self.tmpdir) + self.assertEqual(result.removed_files_count, 1) + self.assertFalse(os.path.exists(p)) + + def test_no_remove(self): + copier = FileCopier() + copier.add("foo", GeneratedFile(b"foo")) + + with open(self.tmppath("bar"), "a"): + pass + + os.mkdir(self.tmppath("emptydir")) + d = self.tmppath("populateddir") + os.mkdir(d) + + with open(self.tmppath("populateddir/foo"), "a"): + pass + + result = copier.copy(self.tmpdir, remove_unaccounted=False) + + self.assertEqual( + self.all_files(self.tmpdir), set(["foo", "bar", "populateddir/foo"]) + ) + self.assertEqual(self.all_dirs(self.tmpdir), set(["populateddir"])) + self.assertEqual(result.removed_files, set()) + self.assertEqual(result.removed_directories, set([self.tmppath("emptydir")])) + + def test_no_remove_empty_directories(self): + copier = FileCopier() + copier.add("foo", GeneratedFile(b"foo")) + + with open(self.tmppath("bar"), "a"): + pass + + os.mkdir(self.tmppath("emptydir")) + d = self.tmppath("populateddir") + os.mkdir(d) + + with open(self.tmppath("populateddir/foo"), "a"): + pass + + result = copier.copy( + self.tmpdir, remove_unaccounted=False, remove_empty_directories=False + ) + + self.assertEqual( + self.all_files(self.tmpdir), set(["foo", "bar", "populateddir/foo"]) + ) + self.assertEqual(self.all_dirs(self.tmpdir), set(["emptydir", "populateddir"])) + self.assertEqual(result.removed_files, set()) + self.assertEqual(result.removed_directories, set()) + + def test_optional_exists_creates_unneeded_directory(self): + """Demonstrate that a directory not strictly required, but specified + as the path to an optional file, will be unnecessarily created. + + This behaviour is wrong; fixing it is tracked by Bug 972432; + and this test exists to guard against unexpected changes in + behaviour. + """ + + dest = self.tmppath("dest") + + copier = FileCopier() + copier.add("foo/bar", ExistingFile(required=False)) + + result = copier.copy(dest) + + st = os.lstat(self.tmppath("dest/foo")) + self.assertFalse(stat.S_ISLNK(st.st_mode)) + self.assertTrue(stat.S_ISDIR(st.st_mode)) + + # What's worse, we have no record that dest was created. + self.assertEqual(len(result.updated_files), 0) + + # But we do have an erroneous record of an optional file + # existing when it does not. + self.assertIn(self.tmppath("dest/foo/bar"), result.existing_files) + + def test_remove_unaccounted_file_registry(self): + """Test FileCopier.copy(remove_unaccounted=FileRegistry())""" + + dest = self.tmppath("dest") + + copier = FileCopier() + copier.add("foo/bar/baz", GeneratedFile(b"foobarbaz")) + copier.add("foo/bar/qux", GeneratedFile(b"foobarqux")) + copier.add("foo/hoge/fuga", GeneratedFile(b"foohogefuga")) + copier.add("foo/toto/tata", GeneratedFile(b"footototata")) + + os.makedirs(os.path.join(dest, "bar")) + with open(os.path.join(dest, "bar", "bar"), "w") as fh: + fh.write("barbar") + os.makedirs(os.path.join(dest, "foo", "toto")) + with open(os.path.join(dest, "foo", "toto", "toto"), "w") as fh: + fh.write("foototototo") + + result = copier.copy(dest, remove_unaccounted=False) + + self.assertEqual( + self.all_files(dest), set(copier.paths()) | {"foo/toto/toto", "bar/bar"} + ) + self.assertEqual( + self.all_dirs(dest), {"foo/bar", "foo/hoge", "foo/toto", "bar"} + ) + + copier2 = FileCopier() + copier2.add("foo/hoge/fuga", GeneratedFile(b"foohogefuga")) + + # We expect only files copied from the first copier to be removed, + # not the extra file that was there beforehand. + result = copier2.copy(dest, remove_unaccounted=copier) + + self.assertEqual( + self.all_files(dest), set(copier2.paths()) | {"foo/toto/toto", "bar/bar"} + ) + self.assertEqual(self.all_dirs(dest), {"foo/hoge", "foo/toto", "bar"}) + self.assertEqual(result.updated_files, {self.tmppath("dest/foo/hoge/fuga")}) + self.assertEqual(result.existing_files, set()) + self.assertEqual( + result.removed_files, + { + self.tmppath(p) + for p in ("dest/foo/bar/baz", "dest/foo/bar/qux", "dest/foo/toto/tata") + }, + ) + self.assertEqual(result.removed_directories, {self.tmppath("dest/foo/bar")}) + + +class TestJarrer(unittest.TestCase): + def check_jar(self, dest, copier): + jar = JarReader(fileobj=dest) + self.assertEqual([f.filename for f in jar], copier.paths()) + for f in jar: + self.assertEqual(f.uncompressed_data.read(), copier[f.filename].content) + + def test_jarrer(self): + copier = Jarrer() + copier.add("foo/bar", GeneratedFile(b"foobar")) + copier.add("foo/qux", GeneratedFile(b"fooqux")) + copier.add("foo/deep/nested/directory/file", GeneratedFile(b"fooz")) + copier.add("bar", GeneratedFile(b"bar")) + copier.add("qux/foo", GeneratedFile(b"quxfoo")) + copier.add("qux/bar", GeneratedFile(b"")) + + dest = MockDest() + copier.copy(dest) + self.check_jar(dest, copier) + + copier.remove("foo") + copier.add("test", GeneratedFile(b"test")) + copier.copy(dest) + self.check_jar(dest, copier) + + copier.remove("test") + copier.add("test", GeneratedFile(b"replaced-content")) + copier.copy(dest) + self.check_jar(dest, copier) + + copier.copy(dest) + self.check_jar(dest, copier) + + preloaded = ["qux/bar", "bar"] + copier.preload(preloaded) + copier.copy(dest) + + dest.seek(0) + jar = JarReader(fileobj=dest) + self.assertEqual( + [f.filename for f in jar], + preloaded + [p for p in copier.paths() if p not in preloaded], + ) + self.assertEqual(jar.last_preloaded, preloaded[-1]) + + def test_jarrer_compress(self): + copier = Jarrer() + copier.add("foo/bar", GeneratedFile(b"ffffff")) + copier.add("foo/qux", GeneratedFile(b"ffffff"), compress=False) + + dest = MockDest() + copier.copy(dest) + self.check_jar(dest, copier) + + dest.seek(0) + jar = JarReader(fileobj=dest) + self.assertTrue(jar["foo/bar"].compressed) + self.assertFalse(jar["foo/qux"].compressed) + + +if __name__ == "__main__": + mozunit.main() diff --git a/python/mozbuild/mozpack/test/test_errors.py b/python/mozbuild/mozpack/test/test_errors.py new file mode 100644 index 0000000000..411b1b54c3 --- /dev/null +++ b/python/mozbuild/mozpack/test/test_errors.py @@ -0,0 +1,95 @@ +# 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/. + +import sys +import unittest + +import mozunit +import six + +from mozpack.errors import AccumulatedErrors, ErrorMessage, errors + + +class TestErrors(object): + def setUp(self): + errors.out = six.moves.cStringIO() + errors.ignore_errors(False) + + def tearDown(self): + errors.out = sys.stderr + + def get_output(self): + return [l.strip() for l in errors.out.getvalue().splitlines()] + + +class TestErrorsImpl(TestErrors, unittest.TestCase): + def test_plain_error(self): + errors.warn("foo") + self.assertRaises(ErrorMessage, errors.error, "foo") + self.assertRaises(ErrorMessage, errors.fatal, "foo") + self.assertEqual(self.get_output(), ["warning: foo"]) + + def test_ignore_errors(self): + errors.ignore_errors() + errors.warn("foo") + errors.error("bar") + self.assertRaises(ErrorMessage, errors.fatal, "foo") + self.assertEqual(self.get_output(), ["warning: foo", "warning: bar"]) + + def test_no_error(self): + with errors.accumulate(): + errors.warn("1") + + def test_simple_error(self): + with self.assertRaises(AccumulatedErrors): + with errors.accumulate(): + errors.error("1") + self.assertEqual(self.get_output(), ["error: 1"]) + + def test_error_loop(self): + with self.assertRaises(AccumulatedErrors): + with errors.accumulate(): + for i in range(3): + errors.error("%d" % i) + self.assertEqual(self.get_output(), ["error: 0", "error: 1", "error: 2"]) + + def test_multiple_errors(self): + with self.assertRaises(AccumulatedErrors): + with errors.accumulate(): + errors.error("foo") + for i in range(3): + if i == 2: + errors.warn("%d" % i) + else: + errors.error("%d" % i) + errors.error("bar") + self.assertEqual( + self.get_output(), + ["error: foo", "error: 0", "error: 1", "warning: 2", "error: bar"], + ) + + def test_errors_context(self): + with self.assertRaises(AccumulatedErrors): + with errors.accumulate(): + self.assertEqual(errors.get_context(), None) + with errors.context("foo", 1): + self.assertEqual(errors.get_context(), ("foo", 1)) + errors.error("a") + with errors.context("bar", 2): + self.assertEqual(errors.get_context(), ("bar", 2)) + errors.error("b") + self.assertEqual(errors.get_context(), ("foo", 1)) + errors.error("c") + self.assertEqual( + self.get_output(), + [ + "error: foo:1: a", + "error: bar:2: b", + "error: foo:1: c", + ], + ) + + +if __name__ == "__main__": + mozunit.main() diff --git a/python/mozbuild/mozpack/test/test_files.py b/python/mozbuild/mozpack/test/test_files.py new file mode 100644 index 0000000000..1c86f2e0cc --- /dev/null +++ b/python/mozbuild/mozpack/test/test_files.py @@ -0,0 +1,1362 @@ +# 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 mozbuild.util import ensure_bytes, ensureParentDir +from mozpack.errors import ErrorMessage, errors +from mozpack.files import ( + AbsoluteSymlinkFile, + ComposedFinder, + DeflatedFile, + Dest, + ExistingFile, + ExtractedTarFile, + File, + FileFinder, + GeneratedFile, + HardlinkFile, + JarFinder, + ManifestFile, + MercurialFile, + MercurialRevisionFinder, + MinifiedCommentStripped, + MinifiedJavaScript, + PreprocessedFile, + TarFinder, +) + +# We don't have hglib installed everywhere. +try: + import hglib +except ImportError: + hglib = None + +import os +import platform +import random +import sys +import tarfile +import unittest +from io import BytesIO +from tempfile import mkdtemp + +import mozfile +import mozunit +import six + +import mozpack.path as mozpath +from mozpack.chrome.manifest import ( + ManifestContent, + ManifestLocale, + ManifestOverride, + ManifestResource, +) +from mozpack.mozjar import JarReader, JarWriter + + +class TestWithTmpDir(unittest.TestCase): + def setUp(self): + self.tmpdir = mkdtemp() + + self.symlink_supported = False + self.hardlink_supported = False + + # See comment in mozpack.files.AbsoluteSymlinkFile + if hasattr(os, "symlink") and platform.system() != "Windows": + dummy_path = self.tmppath("dummy_file") + with open(dummy_path, "a"): + pass + + try: + os.symlink(dummy_path, self.tmppath("dummy_symlink")) + os.remove(self.tmppath("dummy_symlink")) + except EnvironmentError: + pass + finally: + os.remove(dummy_path) + + self.symlink_supported = True + + if hasattr(os, "link"): + dummy_path = self.tmppath("dummy_file") + with open(dummy_path, "a"): + pass + + try: + os.link(dummy_path, self.tmppath("dummy_hardlink")) + os.remove(self.tmppath("dummy_hardlink")) + except EnvironmentError: + pass + finally: + os.remove(dummy_path) + + self.hardlink_supported = True + + def tearDown(self): + mozfile.rmtree(self.tmpdir) + + def tmppath(self, relpath): + return os.path.normpath(os.path.join(self.tmpdir, relpath)) + + +class MockDest(BytesIO, Dest): + def __init__(self): + BytesIO.__init__(self) + self.mode = None + + def read(self, length=-1): + if self.mode != "r": + self.seek(0) + self.mode = "r" + return BytesIO.read(self, length) + + def write(self, data): + if self.mode != "w": + self.seek(0) + self.truncate(0) + self.mode = "w" + return BytesIO.write(self, data) + + def exists(self): + return True + + def close(self): + if self.mode: + self.mode = None + + +class DestNoWrite(Dest): + def write(self, data): + raise RuntimeError + + +class TestDest(TestWithTmpDir): + def test_dest(self): + dest = Dest(self.tmppath("dest")) + self.assertFalse(dest.exists()) + dest.write(b"foo") + self.assertTrue(dest.exists()) + dest.write(b"foo") + self.assertEqual(dest.read(4), b"foof") + self.assertEqual(dest.read(), b"oo") + self.assertEqual(dest.read(), b"") + dest.write(b"bar") + self.assertEqual(dest.read(4), b"bar") + dest.close() + self.assertEqual(dest.read(), b"bar") + dest.write(b"foo") + dest.close() + dest.write(b"qux") + self.assertEqual(dest.read(), b"qux") + + +rand = bytes( + random.choice(b"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ") + for i in six.moves.xrange(131597) +) +samples = [ + b"", + b"test", + b"fooo", + b"same", + b"same", + b"Different and longer", + rand, + rand, + rand[:-1] + b"_", + b"test", +] + + +class TestFile(TestWithTmpDir): + def test_file(self): + """ + Check that File.copy yields the proper content in the destination file + in all situations that trigger different code paths: + - different content + - different content of the same size + - same content + - long content + """ + src = self.tmppath("src") + dest = self.tmppath("dest") + + for content in samples: + with open(src, "wb") as tmp: + tmp.write(content) + # Ensure the destination file, when it exists, is older than the + # source + if os.path.exists(dest): + time = os.path.getmtime(src) - 1 + os.utime(dest, (time, time)) + f = File(src) + f.copy(dest) + self.assertEqual(content, open(dest, "rb").read()) + self.assertEqual(content, f.open().read()) + self.assertEqual(content, f.open().read()) + + def test_file_dest(self): + """ + Similar to test_file, but for a destination object instead of + a destination file. This ensures the destination object is being + used properly by File.copy, ensuring that other subclasses of Dest + will work. + """ + src = self.tmppath("src") + dest = MockDest() + + for content in samples: + with open(src, "wb") as tmp: + tmp.write(content) + f = File(src) + f.copy(dest) + self.assertEqual(content, dest.getvalue()) + + def test_file_open(self): + """ + Test whether File.open returns an appropriately reset file object. + """ + src = self.tmppath("src") + content = b"".join(samples) + with open(src, "wb") as tmp: + tmp.write(content) + + f = File(src) + self.assertEqual(content[:42], f.open().read(42)) + self.assertEqual(content, f.open().read()) + + def test_file_no_write(self): + """ + Test various conditions where File.copy is expected not to write + in the destination file. + """ + src = self.tmppath("src") + dest = self.tmppath("dest") + + with open(src, "wb") as tmp: + tmp.write(b"test") + + # Initial copy + f = File(src) + f.copy(dest) + + # Ensure subsequent copies won't trigger writes + f.copy(DestNoWrite(dest)) + self.assertEqual(b"test", open(dest, "rb").read()) + + # When the source file is newer, but with the same content, no copy + # should occur + time = os.path.getmtime(src) - 1 + os.utime(dest, (time, time)) + f.copy(DestNoWrite(dest)) + self.assertEqual(b"test", open(dest, "rb").read()) + + # When the source file is older than the destination file, even with + # different content, no copy should occur. + with open(src, "wb") as tmp: + tmp.write(b"fooo") + time = os.path.getmtime(dest) - 1 + os.utime(src, (time, time)) + f.copy(DestNoWrite(dest)) + self.assertEqual(b"test", open(dest, "rb").read()) + + # Double check that under conditions where a copy occurs, we would get + # an exception. + time = os.path.getmtime(src) - 1 + os.utime(dest, (time, time)) + self.assertRaises(RuntimeError, f.copy, DestNoWrite(dest)) + + # skip_if_older=False is expected to force a copy in this situation. + f.copy(dest, skip_if_older=False) + self.assertEqual(b"fooo", open(dest, "rb").read()) + + +class TestAbsoluteSymlinkFile(TestWithTmpDir): + def test_absolute_relative(self): + AbsoluteSymlinkFile("/foo") + + with self.assertRaisesRegexp(ValueError, "Symlink target not absolute"): + AbsoluteSymlinkFile("./foo") + + def test_symlink_file(self): + source = self.tmppath("test_path") + with open(source, "wt") as fh: + fh.write("Hello world") + + s = AbsoluteSymlinkFile(source) + dest = self.tmppath("symlink") + self.assertTrue(s.copy(dest)) + + if self.symlink_supported: + self.assertTrue(os.path.islink(dest)) + link = os.readlink(dest) + self.assertEqual(link, source) + else: + self.assertTrue(os.path.isfile(dest)) + content = open(dest).read() + self.assertEqual(content, "Hello world") + + def test_replace_file_with_symlink(self): + # If symlinks are supported, an existing file should be replaced by a + # symlink. + source = self.tmppath("test_path") + with open(source, "wt") as fh: + fh.write("source") + + dest = self.tmppath("dest") + with open(dest, "a"): + pass + + s = AbsoluteSymlinkFile(source) + s.copy(dest, skip_if_older=False) + + if self.symlink_supported: + self.assertTrue(os.path.islink(dest)) + link = os.readlink(dest) + self.assertEqual(link, source) + else: + self.assertTrue(os.path.isfile(dest)) + content = open(dest).read() + self.assertEqual(content, "source") + + def test_replace_symlink(self): + if not self.symlink_supported: + return + + source = self.tmppath("source") + with open(source, "a"): + pass + + dest = self.tmppath("dest") + + os.symlink(self.tmppath("bad"), dest) + self.assertTrue(os.path.islink(dest)) + + s = AbsoluteSymlinkFile(source) + self.assertTrue(s.copy(dest)) + + self.assertTrue(os.path.islink(dest)) + link = os.readlink(dest) + self.assertEqual(link, source) + + def test_noop(self): + if not hasattr(os, "symlink") or sys.platform == "win32": + return + + source = self.tmppath("source") + dest = self.tmppath("dest") + + with open(source, "a"): + pass + + os.symlink(source, dest) + link = os.readlink(dest) + self.assertEqual(link, source) + + s = AbsoluteSymlinkFile(source) + self.assertFalse(s.copy(dest)) + + link = os.readlink(dest) + self.assertEqual(link, source) + + +class TestHardlinkFile(TestWithTmpDir): + def test_absolute_relative(self): + HardlinkFile("/foo") + HardlinkFile("./foo") + + def test_hardlink_file(self): + source = self.tmppath("test_path") + with open(source, "wt") as fh: + fh.write("Hello world") + + s = HardlinkFile(source) + dest = self.tmppath("hardlink") + self.assertTrue(s.copy(dest)) + + if self.hardlink_supported: + source_stat = os.stat(source) + dest_stat = os.stat(dest) + self.assertEqual(source_stat.st_dev, dest_stat.st_dev) + self.assertEqual(source_stat.st_ino, dest_stat.st_ino) + else: + self.assertTrue(os.path.isfile(dest)) + with open(dest) as f: + content = f.read() + self.assertEqual(content, "Hello world") + + def test_replace_file_with_hardlink(self): + # If hardlink are supported, an existing file should be replaced by a + # symlink. + source = self.tmppath("test_path") + with open(source, "wt") as fh: + fh.write("source") + + dest = self.tmppath("dest") + with open(dest, "a"): + pass + + s = HardlinkFile(source) + s.copy(dest, skip_if_older=False) + + if self.hardlink_supported: + source_stat = os.stat(source) + dest_stat = os.stat(dest) + self.assertEqual(source_stat.st_dev, dest_stat.st_dev) + self.assertEqual(source_stat.st_ino, dest_stat.st_ino) + else: + self.assertTrue(os.path.isfile(dest)) + with open(dest) as f: + content = f.read() + self.assertEqual(content, "source") + + def test_replace_hardlink(self): + if not self.hardlink_supported: + raise unittest.SkipTest("hardlink not supported") + + source = self.tmppath("source") + with open(source, "a"): + pass + + dest = self.tmppath("dest") + + os.link(source, dest) + + s = HardlinkFile(source) + self.assertFalse(s.copy(dest)) + + source_stat = os.lstat(source) + dest_stat = os.lstat(dest) + self.assertEqual(source_stat.st_dev, dest_stat.st_dev) + self.assertEqual(source_stat.st_ino, dest_stat.st_ino) + + def test_noop(self): + if not self.hardlink_supported: + raise unittest.SkipTest("hardlink not supported") + + source = self.tmppath("source") + dest = self.tmppath("dest") + + with open(source, "a"): + pass + + os.link(source, dest) + + s = HardlinkFile(source) + self.assertFalse(s.copy(dest)) + + source_stat = os.lstat(source) + dest_stat = os.lstat(dest) + self.assertEqual(source_stat.st_dev, dest_stat.st_dev) + self.assertEqual(source_stat.st_ino, dest_stat.st_ino) + + +class TestPreprocessedFile(TestWithTmpDir): + def test_preprocess(self): + """ + Test that copying the file invokes the preprocessor + """ + src = self.tmppath("src") + dest = self.tmppath("dest") + + with open(src, "wb") as tmp: + tmp.write(b"#ifdef FOO\ntest\n#endif") + + f = PreprocessedFile(src, depfile_path=None, marker="#", defines={"FOO": True}) + self.assertTrue(f.copy(dest)) + + self.assertEqual(b"test\n", open(dest, "rb").read()) + + def test_preprocess_file_no_write(self): + """ + Test various conditions where PreprocessedFile.copy is expected not to + write in the destination file. + """ + src = self.tmppath("src") + dest = self.tmppath("dest") + depfile = self.tmppath("depfile") + + with open(src, "wb") as tmp: + tmp.write(b"#ifdef FOO\ntest\n#endif") + + # Initial copy + f = PreprocessedFile( + src, depfile_path=depfile, marker="#", defines={"FOO": True} + ) + self.assertTrue(f.copy(dest)) + + # Ensure subsequent copies won't trigger writes + self.assertFalse(f.copy(DestNoWrite(dest))) + self.assertEqual(b"test\n", open(dest, "rb").read()) + + # When the source file is older than the destination file, even with + # different content, no copy should occur. + with open(src, "wb") as tmp: + tmp.write(b"#ifdef FOO\nfooo\n#endif") + time = os.path.getmtime(dest) - 1 + os.utime(src, (time, time)) + self.assertFalse(f.copy(DestNoWrite(dest))) + self.assertEqual(b"test\n", open(dest, "rb").read()) + + # skip_if_older=False is expected to force a copy in this situation. + self.assertTrue(f.copy(dest, skip_if_older=False)) + self.assertEqual(b"fooo\n", open(dest, "rb").read()) + + def test_preprocess_file_dependencies(self): + """ + Test that the preprocess runs if the dependencies of the source change + """ + src = self.tmppath("src") + dest = self.tmppath("dest") + incl = self.tmppath("incl") + deps = self.tmppath("src.pp") + + with open(src, "wb") as tmp: + tmp.write(b"#ifdef FOO\ntest\n#endif") + + with open(incl, "wb") as tmp: + tmp.write(b"foo bar") + + # Initial copy + f = PreprocessedFile(src, depfile_path=deps, marker="#", defines={"FOO": True}) + self.assertTrue(f.copy(dest)) + + # Update the source so it #includes the include file. + with open(src, "wb") as tmp: + tmp.write(b"#include incl\n") + time = os.path.getmtime(dest) + 1 + os.utime(src, (time, time)) + self.assertTrue(f.copy(dest)) + self.assertEqual(b"foo bar", open(dest, "rb").read()) + + # If one of the dependencies changes, the file should be updated. The + # mtime of the dependency is set after the destination file, to avoid + # both files having the same time. + with open(incl, "wb") as tmp: + tmp.write(b"quux") + time = os.path.getmtime(dest) + 1 + os.utime(incl, (time, time)) + self.assertTrue(f.copy(dest)) + self.assertEqual(b"quux", open(dest, "rb").read()) + + # Perform one final copy to confirm that we don't run the preprocessor + # again. We update the mtime of the destination so it's newer than the + # input files. This would "just work" if we weren't changing + time = os.path.getmtime(incl) + 1 + os.utime(dest, (time, time)) + self.assertFalse(f.copy(DestNoWrite(dest))) + + def test_replace_symlink(self): + """ + Test that if the destination exists, and is a symlink, the target of + the symlink is not overwritten by the preprocessor output. + """ + if not self.symlink_supported: + return + + source = self.tmppath("source") + dest = self.tmppath("dest") + pp_source = self.tmppath("pp_in") + deps = self.tmppath("deps") + + with open(source, "a"): + pass + + os.symlink(source, dest) + self.assertTrue(os.path.islink(dest)) + + with open(pp_source, "wb") as tmp: + tmp.write(b"#define FOO\nPREPROCESSED") + + f = PreprocessedFile( + pp_source, depfile_path=deps, marker="#", defines={"FOO": True} + ) + self.assertTrue(f.copy(dest)) + + self.assertEqual(b"PREPROCESSED", open(dest, "rb").read()) + self.assertFalse(os.path.islink(dest)) + self.assertEqual(b"", open(source, "rb").read()) + + +class TestExistingFile(TestWithTmpDir): + def test_required_missing_dest(self): + with self.assertRaisesRegexp(ErrorMessage, "Required existing file"): + f = ExistingFile(required=True) + f.copy(self.tmppath("dest")) + + def test_required_existing_dest(self): + p = self.tmppath("dest") + with open(p, "a"): + pass + + f = ExistingFile(required=True) + f.copy(p) + + def test_optional_missing_dest(self): + f = ExistingFile(required=False) + f.copy(self.tmppath("dest")) + + def test_optional_existing_dest(self): + p = self.tmppath("dest") + with open(p, "a"): + pass + + f = ExistingFile(required=False) + f.copy(p) + + +class TestGeneratedFile(TestWithTmpDir): + def test_generated_file(self): + """ + Check that GeneratedFile.copy yields the proper content in the + destination file in all situations that trigger different code paths + (see TestFile.test_file) + """ + dest = self.tmppath("dest") + + for content in samples: + f = GeneratedFile(content) + f.copy(dest) + self.assertEqual(content, open(dest, "rb").read()) + + def test_generated_file_open(self): + """ + Test whether GeneratedFile.open returns an appropriately reset file + object. + """ + content = b"".join(samples) + f = GeneratedFile(content) + self.assertEqual(content[:42], f.open().read(42)) + self.assertEqual(content, f.open().read()) + + def test_generated_file_no_write(self): + """ + Test various conditions where GeneratedFile.copy is expected not to + write in the destination file. + """ + dest = self.tmppath("dest") + + # Initial copy + f = GeneratedFile(b"test") + f.copy(dest) + + # Ensure subsequent copies won't trigger writes + f.copy(DestNoWrite(dest)) + self.assertEqual(b"test", open(dest, "rb").read()) + + # When using a new instance with the same content, no copy should occur + f = GeneratedFile(b"test") + f.copy(DestNoWrite(dest)) + self.assertEqual(b"test", open(dest, "rb").read()) + + # Double check that under conditions where a copy occurs, we would get + # an exception. + f = GeneratedFile(b"fooo") + self.assertRaises(RuntimeError, f.copy, DestNoWrite(dest)) + + def test_generated_file_function(self): + """ + Test GeneratedFile behavior with functions. + """ + dest = self.tmppath("dest") + data = { + "num_calls": 0, + } + + def content(): + data["num_calls"] += 1 + return b"content" + + f = GeneratedFile(content) + self.assertEqual(data["num_calls"], 0) + f.copy(dest) + self.assertEqual(data["num_calls"], 1) + self.assertEqual(b"content", open(dest, "rb").read()) + self.assertEqual(b"content", f.open().read()) + self.assertEqual(b"content", f.read()) + self.assertEqual(len(b"content"), f.size()) + self.assertEqual(data["num_calls"], 1) + + f.content = b"modified" + f.copy(dest) + self.assertEqual(data["num_calls"], 1) + self.assertEqual(b"modified", open(dest, "rb").read()) + self.assertEqual(b"modified", f.open().read()) + self.assertEqual(b"modified", f.read()) + self.assertEqual(len(b"modified"), f.size()) + + f.content = content + self.assertEqual(data["num_calls"], 1) + self.assertEqual(b"content", f.read()) + self.assertEqual(data["num_calls"], 2) + + +class TestDeflatedFile(TestWithTmpDir): + def test_deflated_file(self): + """ + Check that DeflatedFile.copy yields the proper content in the + destination file in all situations that trigger different code paths + (see TestFile.test_file) + """ + src = self.tmppath("src.jar") + dest = self.tmppath("dest") + + contents = {} + with JarWriter(src) as jar: + for content in samples: + name = "".join( + random.choice( + "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" + ) + for i in range(8) + ) + jar.add(name, content, compress=True) + contents[name] = content + + for j in JarReader(src): + f = DeflatedFile(j) + f.copy(dest) + self.assertEqual(contents[j.filename], open(dest, "rb").read()) + + def test_deflated_file_open(self): + """ + Test whether DeflatedFile.open returns an appropriately reset file + object. + """ + src = self.tmppath("src.jar") + content = b"".join(samples) + with JarWriter(src) as jar: + jar.add("content", content) + + f = DeflatedFile(JarReader(src)["content"]) + self.assertEqual(content[:42], f.open().read(42)) + self.assertEqual(content, f.open().read()) + + def test_deflated_file_no_write(self): + """ + Test various conditions where DeflatedFile.copy is expected not to + write in the destination file. + """ + src = self.tmppath("src.jar") + dest = self.tmppath("dest") + + with JarWriter(src) as jar: + jar.add("test", b"test") + jar.add("test2", b"test") + jar.add("fooo", b"fooo") + + jar = JarReader(src) + # Initial copy + f = DeflatedFile(jar["test"]) + f.copy(dest) + + # Ensure subsequent copies won't trigger writes + f.copy(DestNoWrite(dest)) + self.assertEqual(b"test", open(dest, "rb").read()) + + # When using a different file with the same content, no copy should + # occur + f = DeflatedFile(jar["test2"]) + f.copy(DestNoWrite(dest)) + self.assertEqual(b"test", open(dest, "rb").read()) + + # Double check that under conditions where a copy occurs, we would get + # an exception. + f = DeflatedFile(jar["fooo"]) + self.assertRaises(RuntimeError, f.copy, DestNoWrite(dest)) + + +class TestManifestFile(TestWithTmpDir): + def test_manifest_file(self): + f = ManifestFile("chrome") + f.add(ManifestContent("chrome", "global", "toolkit/content/global/")) + f.add(ManifestResource("chrome", "gre-resources", "toolkit/res/")) + f.add(ManifestResource("chrome/pdfjs", "pdfjs", "./")) + f.add(ManifestContent("chrome/pdfjs", "pdfjs", "pdfjs")) + f.add(ManifestLocale("chrome", "browser", "en-US", "en-US/locale/browser/")) + + f.copy(self.tmppath("chrome.manifest")) + self.assertEqual( + open(self.tmppath("chrome.manifest")).readlines(), + [ + "content global toolkit/content/global/\n", + "resource gre-resources toolkit/res/\n", + "resource pdfjs pdfjs/\n", + "content pdfjs pdfjs/pdfjs\n", + "locale browser en-US en-US/locale/browser/\n", + ], + ) + + self.assertRaises( + ValueError, + f.remove, + ManifestContent("", "global", "toolkit/content/global/"), + ) + self.assertRaises( + ValueError, + f.remove, + ManifestOverride( + "chrome", + "chrome://global/locale/netError.dtd", + "chrome://browser/locale/netError.dtd", + ), + ) + + f.remove(ManifestContent("chrome", "global", "toolkit/content/global/")) + self.assertRaises( + ValueError, + f.remove, + ManifestContent("chrome", "global", "toolkit/content/global/"), + ) + + f.copy(self.tmppath("chrome.manifest")) + content = open(self.tmppath("chrome.manifest"), "rb").read() + self.assertEqual(content[:42], f.open().read(42)) + self.assertEqual(content, f.open().read()) + + +# Compiled typelib for the following IDL: +# interface foo; +# [scriptable, uuid(5f70da76-519c-4858-b71e-e3c92333e2d6)] +# interface bar { +# void bar(in foo f); +# }; +# We need to make this [scriptable] so it doesn't get deleted from the +# typelib. We don't need to make the foo interfaces below [scriptable], +# because they will be automatically included by virtue of being an +# argument to a method of |bar|. +bar_xpt = GeneratedFile( + b"\x58\x50\x43\x4F\x4D\x0A\x54\x79\x70\x65\x4C\x69\x62\x0D\x0A\x1A" + + b"\x01\x02\x00\x02\x00\x00\x00\x7B\x00\x00\x00\x24\x00\x00\x00\x5C" + + b"\x80\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + + b"\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x5F" + + b"\x70\xDA\x76\x51\x9C\x48\x58\xB7\x1E\xE3\xC9\x23\x33\xE2\xD6\x00" + + b"\x00\x00\x05\x00\x00\x00\x00\x00\x00\x00\x0D\x00\x66\x6F\x6F\x00" + + b"\x62\x61\x72\x00\x62\x61\x72\x00\x00\x00\x00\x01\x00\x00\x00\x00" + + b"\x09\x01\x80\x92\x00\x01\x80\x06\x00\x00\x80" +) + +# Compiled typelib for the following IDL: +# [uuid(3271bebc-927e-4bef-935e-44e0aaf3c1e5)] +# interface foo { +# void foo(); +# }; +foo_xpt = GeneratedFile( + b"\x58\x50\x43\x4F\x4D\x0A\x54\x79\x70\x65\x4C\x69\x62\x0D\x0A\x1A" + + b"\x01\x02\x00\x01\x00\x00\x00\x57\x00\x00\x00\x24\x00\x00\x00\x40" + + b"\x80\x00\x00\x32\x71\xBE\xBC\x92\x7E\x4B\xEF\x93\x5E\x44\xE0\xAA" + + b"\xF3\xC1\xE5\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x09\x00" + + b"\x66\x6F\x6F\x00\x66\x6F\x6F\x00\x00\x00\x00\x01\x00\x00\x00\x00" + + b"\x05\x00\x80\x06\x00\x00\x00" +) + +# Compiled typelib for the following IDL: +# [uuid(7057f2aa-fdc2-4559-abde-08d939f7e80d)] +# interface foo { +# void foo(); +# }; +foo2_xpt = GeneratedFile( + b"\x58\x50\x43\x4F\x4D\x0A\x54\x79\x70\x65\x4C\x69\x62\x0D\x0A\x1A" + + b"\x01\x02\x00\x01\x00\x00\x00\x57\x00\x00\x00\x24\x00\x00\x00\x40" + + b"\x80\x00\x00\x70\x57\xF2\xAA\xFD\xC2\x45\x59\xAB\xDE\x08\xD9\x39" + + b"\xF7\xE8\x0D\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x09\x00" + + b"\x66\x6F\x6F\x00\x66\x6F\x6F\x00\x00\x00\x00\x01\x00\x00\x00\x00" + + b"\x05\x00\x80\x06\x00\x00\x00" +) + + +class TestMinifiedCommentStripped(TestWithTmpDir): + def test_minified_comment_stripped(self): + propLines = [ + "# Comments are removed", + "foo = bar", + "", + "# Another comment", + ] + prop = GeneratedFile("\n".join(propLines)) + self.assertEqual( + MinifiedCommentStripped(prop).open().readlines(), [b"foo = bar\n", b"\n"] + ) + open(self.tmppath("prop"), "w").write("\n".join(propLines)) + MinifiedCommentStripped(File(self.tmppath("prop"))).copy(self.tmppath("prop2")) + self.assertEqual(open(self.tmppath("prop2")).readlines(), ["foo = bar\n", "\n"]) + + +class TestMinifiedJavaScript(TestWithTmpDir): + orig_lines = [ + "// Comment line", + 'let foo = "bar";', + "var bar = true;", + "", + "// Another comment", + ] + + def test_minified_javascript(self): + orig_f = GeneratedFile("\n".join(self.orig_lines)) + min_f = MinifiedJavaScript(orig_f) + + mini_lines = min_f.open().readlines() + self.assertTrue(mini_lines) + self.assertTrue(len(mini_lines) < len(self.orig_lines)) + + def _verify_command(self, code): + our_dir = os.path.abspath(os.path.dirname(__file__)) + return [ + sys.executable, + os.path.join(our_dir, "support", "minify_js_verify.py"), + code, + ] + + def test_minified_verify_success(self): + orig_f = GeneratedFile("\n".join(self.orig_lines)) + min_f = MinifiedJavaScript(orig_f, verify_command=self._verify_command("0")) + + mini_lines = [six.ensure_text(s) for s in min_f.open().readlines()] + self.assertTrue(mini_lines) + self.assertTrue(len(mini_lines) < len(self.orig_lines)) + + def test_minified_verify_failure(self): + orig_f = GeneratedFile("\n".join(self.orig_lines)) + errors.out = six.StringIO() + min_f = MinifiedJavaScript(orig_f, verify_command=self._verify_command("1")) + + mini_lines = min_f.open().readlines() + output = errors.out.getvalue() + errors.out = sys.stderr + self.assertEqual( + output, + "warning: JS minification verification failed for <unknown>:\n" + "warning: Error message\n", + ) + self.assertEqual(mini_lines, orig_f.open().readlines()) + + +class MatchTestTemplate(object): + def prepare_match_test(self, with_dotfiles=False): + self.add("bar") + self.add("foo/bar") + self.add("foo/baz") + self.add("foo/qux/1") + self.add("foo/qux/bar") + self.add("foo/qux/2/test") + self.add("foo/qux/2/test2") + if with_dotfiles: + self.add("foo/.foo") + self.add("foo/.bar/foo") + + def do_match_test(self): + self.do_check( + "", + [ + "bar", + "foo/bar", + "foo/baz", + "foo/qux/1", + "foo/qux/bar", + "foo/qux/2/test", + "foo/qux/2/test2", + ], + ) + self.do_check( + "*", + [ + "bar", + "foo/bar", + "foo/baz", + "foo/qux/1", + "foo/qux/bar", + "foo/qux/2/test", + "foo/qux/2/test2", + ], + ) + self.do_check( + "foo/qux", ["foo/qux/1", "foo/qux/bar", "foo/qux/2/test", "foo/qux/2/test2"] + ) + self.do_check("foo/b*", ["foo/bar", "foo/baz"]) + self.do_check("baz", []) + self.do_check("foo/foo", []) + self.do_check("foo/*ar", ["foo/bar"]) + self.do_check("*ar", ["bar"]) + self.do_check("*/bar", ["foo/bar"]) + self.do_check( + "foo/*ux", ["foo/qux/1", "foo/qux/bar", "foo/qux/2/test", "foo/qux/2/test2"] + ) + self.do_check( + "foo/q*ux", + ["foo/qux/1", "foo/qux/bar", "foo/qux/2/test", "foo/qux/2/test2"], + ) + self.do_check("foo/*/2/test*", ["foo/qux/2/test", "foo/qux/2/test2"]) + self.do_check("**/bar", ["bar", "foo/bar", "foo/qux/bar"]) + self.do_check("foo/**/test", ["foo/qux/2/test"]) + self.do_check( + "foo", + [ + "foo/bar", + "foo/baz", + "foo/qux/1", + "foo/qux/bar", + "foo/qux/2/test", + "foo/qux/2/test2", + ], + ) + self.do_check( + "foo/**", + [ + "foo/bar", + "foo/baz", + "foo/qux/1", + "foo/qux/bar", + "foo/qux/2/test", + "foo/qux/2/test2", + ], + ) + self.do_check("**/2/test*", ["foo/qux/2/test", "foo/qux/2/test2"]) + self.do_check( + "**/foo", + [ + "foo/bar", + "foo/baz", + "foo/qux/1", + "foo/qux/bar", + "foo/qux/2/test", + "foo/qux/2/test2", + ], + ) + self.do_check("**/barbaz", []) + self.do_check("f**/bar", ["foo/bar"]) + + def do_finder_test(self, finder): + self.assertTrue(finder.contains("foo/.foo")) + self.assertTrue(finder.contains("foo/.bar")) + self.assertTrue("foo/.foo" in [f for f, c in finder.find("foo/.foo")]) + self.assertTrue("foo/.bar/foo" in [f for f, c in finder.find("foo/.bar")]) + self.assertEqual( + sorted([f for f, c in finder.find("foo/.*")]), ["foo/.bar/foo", "foo/.foo"] + ) + for pattern in ["foo", "**", "**/*", "**/foo", "foo/*"]: + self.assertFalse("foo/.foo" in [f for f, c in finder.find(pattern)]) + self.assertFalse("foo/.bar/foo" in [f for f, c in finder.find(pattern)]) + self.assertEqual( + sorted([f for f, c in finder.find(pattern)]), + sorted([f for f, c in finder if mozpath.match(f, pattern)]), + ) + + +def do_check(test, finder, pattern, result): + if result: + test.assertTrue(finder.contains(pattern)) + else: + test.assertFalse(finder.contains(pattern)) + test.assertEqual(sorted(list(f for f, c in finder.find(pattern))), sorted(result)) + + +class TestFileFinder(MatchTestTemplate, TestWithTmpDir): + def add(self, path): + ensureParentDir(self.tmppath(path)) + open(self.tmppath(path), "wb").write(six.ensure_binary(path)) + + def do_check(self, pattern, result): + do_check(self, self.finder, pattern, result) + + def test_file_finder(self): + self.prepare_match_test(with_dotfiles=True) + self.finder = FileFinder(self.tmpdir) + self.do_match_test() + self.do_finder_test(self.finder) + + def test_get(self): + self.prepare_match_test() + finder = FileFinder(self.tmpdir) + + self.assertIsNone(finder.get("does-not-exist")) + res = finder.get("bar") + self.assertIsInstance(res, File) + self.assertEqual(mozpath.normpath(res.path), mozpath.join(self.tmpdir, "bar")) + + def test_ignored_dirs(self): + """Ignored directories should not have results returned.""" + self.prepare_match_test() + self.add("fooz") + + # Present to ensure prefix matching doesn't exclude. + self.add("foo/quxz") + + self.finder = FileFinder(self.tmpdir, ignore=["foo/qux"]) + + self.do_check("**", ["bar", "foo/bar", "foo/baz", "foo/quxz", "fooz"]) + self.do_check("foo/*", ["foo/bar", "foo/baz", "foo/quxz"]) + self.do_check("foo/**", ["foo/bar", "foo/baz", "foo/quxz"]) + self.do_check("foo/qux/**", []) + self.do_check("foo/qux/*", []) + self.do_check("foo/qux/bar", []) + self.do_check("foo/quxz", ["foo/quxz"]) + self.do_check("fooz", ["fooz"]) + + def test_ignored_files(self): + """Ignored files should not have results returned.""" + self.prepare_match_test() + + # Be sure prefix match doesn't get ignored. + self.add("barz") + + self.finder = FileFinder(self.tmpdir, ignore=["foo/bar", "bar"]) + self.do_check( + "**", + [ + "barz", + "foo/baz", + "foo/qux/1", + "foo/qux/2/test", + "foo/qux/2/test2", + "foo/qux/bar", + ], + ) + self.do_check( + "foo/**", + [ + "foo/baz", + "foo/qux/1", + "foo/qux/2/test", + "foo/qux/2/test2", + "foo/qux/bar", + ], + ) + + def test_ignored_patterns(self): + """Ignore entries with patterns should be honored.""" + self.prepare_match_test() + + self.add("foo/quxz") + + self.finder = FileFinder(self.tmpdir, ignore=["foo/qux/*"]) + self.do_check("**", ["foo/bar", "foo/baz", "foo/quxz", "bar"]) + self.do_check("foo/**", ["foo/bar", "foo/baz", "foo/quxz"]) + + def test_dotfiles(self): + """Finder can find files beginning with . is configured.""" + self.prepare_match_test(with_dotfiles=True) + self.finder = FileFinder(self.tmpdir, find_dotfiles=True) + self.do_check( + "**", + [ + "bar", + "foo/.foo", + "foo/.bar/foo", + "foo/bar", + "foo/baz", + "foo/qux/1", + "foo/qux/bar", + "foo/qux/2/test", + "foo/qux/2/test2", + ], + ) + + def test_dotfiles_plus_ignore(self): + self.prepare_match_test(with_dotfiles=True) + self.finder = FileFinder( + self.tmpdir, find_dotfiles=True, ignore=["foo/.bar/**"] + ) + self.do_check( + "foo/**", + [ + "foo/.foo", + "foo/bar", + "foo/baz", + "foo/qux/1", + "foo/qux/bar", + "foo/qux/2/test", + "foo/qux/2/test2", + ], + ) + + +class TestJarFinder(MatchTestTemplate, TestWithTmpDir): + def add(self, path): + self.jar.add(path, ensure_bytes(path), compress=True) + + def do_check(self, pattern, result): + do_check(self, self.finder, pattern, result) + + def test_jar_finder(self): + self.jar = JarWriter(file=self.tmppath("test.jar")) + self.prepare_match_test() + self.jar.finish() + reader = JarReader(file=self.tmppath("test.jar")) + self.finder = JarFinder(self.tmppath("test.jar"), reader) + self.do_match_test() + + self.assertIsNone(self.finder.get("does-not-exist")) + self.assertIsInstance(self.finder.get("bar"), DeflatedFile) + + +class TestTarFinder(MatchTestTemplate, TestWithTmpDir): + def add(self, path): + self.tar.addfile(tarfile.TarInfo(name=path)) + + def do_check(self, pattern, result): + do_check(self, self.finder, pattern, result) + + def test_tar_finder(self): + self.tar = tarfile.open(name=self.tmppath("test.tar.bz2"), mode="w:bz2") + self.prepare_match_test() + self.tar.close() + with tarfile.open(name=self.tmppath("test.tar.bz2"), mode="r:bz2") as tarreader: + self.finder = TarFinder(self.tmppath("test.tar.bz2"), tarreader) + self.do_match_test() + + self.assertIsNone(self.finder.get("does-not-exist")) + self.assertIsInstance(self.finder.get("bar"), ExtractedTarFile) + + +class TestComposedFinder(MatchTestTemplate, TestWithTmpDir): + def add(self, path, content=None): + # Put foo/qux files under $tmp/b. + if path.startswith("foo/qux/"): + real_path = mozpath.join("b", path[8:]) + else: + real_path = mozpath.join("a", path) + ensureParentDir(self.tmppath(real_path)) + if not content: + content = six.ensure_binary(path) + open(self.tmppath(real_path), "wb").write(content) + + def do_check(self, pattern, result): + if "*" in pattern: + return + do_check(self, self.finder, pattern, result) + + def test_composed_finder(self): + self.prepare_match_test() + # Also add files in $tmp/a/foo/qux because ComposedFinder is + # expected to mask foo/qux entirely with content from $tmp/b. + ensureParentDir(self.tmppath("a/foo/qux/hoge")) + open(self.tmppath("a/foo/qux/hoge"), "wb").write(b"hoge") + open(self.tmppath("a/foo/qux/bar"), "wb").write(b"not the right content") + self.finder = ComposedFinder( + { + "": FileFinder(self.tmppath("a")), + "foo/qux": FileFinder(self.tmppath("b")), + } + ) + self.do_match_test() + + self.assertIsNone(self.finder.get("does-not-exist")) + self.assertIsInstance(self.finder.get("bar"), File) + + +@unittest.skipUnless(hglib, "hglib not available") +@unittest.skipIf( + six.PY3 and os.name == "nt", "Does not currently work in Python3 on Windows" +) +class TestMercurialRevisionFinder(MatchTestTemplate, TestWithTmpDir): + def setUp(self): + super(TestMercurialRevisionFinder, self).setUp() + hglib.init(self.tmpdir) + self._clients = [] + + def tearDown(self): + # Ensure the hg client process is closed. Otherwise, Windows + # may have trouble removing the repo directory because the process + # has an open handle on it. + for client in getattr(self, "_clients", []): + if client.server: + client.close() + + self._clients[:] = [] + + super(TestMercurialRevisionFinder, self).tearDown() + + def _client(self): + configs = ( + # b'' because py2 needs !unicode + b'ui.username="Dummy User <dummy@example.com>"', + ) + client = hglib.open( + six.ensure_binary(self.tmpdir), + encoding=b"UTF-8", # b'' because py2 needs !unicode + configs=configs, + ) + self._clients.append(client) + return client + + def add(self, path): + with self._client() as c: + ensureParentDir(self.tmppath(path)) + with open(self.tmppath(path), "wb") as fh: + fh.write(six.ensure_binary(path)) + c.add(six.ensure_binary(self.tmppath(path))) + + def do_check(self, pattern, result): + do_check(self, self.finder, pattern, result) + + def _get_finder(self, *args, **kwargs): + f = MercurialRevisionFinder(*args, **kwargs) + self._clients.append(f._client) + return f + + def test_default_revision(self): + self.prepare_match_test() + with self._client() as c: + c.commit("initial commit") + + self.finder = self._get_finder(self.tmpdir) + self.do_match_test() + + self.assertIsNone(self.finder.get("does-not-exist")) + self.assertIsInstance(self.finder.get("bar"), MercurialFile) + + def test_old_revision(self): + with self._client() as c: + with open(self.tmppath("foo"), "wb") as fh: + fh.write(b"foo initial") + c.add(six.ensure_binary(self.tmppath("foo"))) + c.commit("initial") + + with open(self.tmppath("foo"), "wb") as fh: + fh.write(b"foo second") + with open(self.tmppath("bar"), "wb") as fh: + fh.write(b"bar second") + c.add(six.ensure_binary(self.tmppath("bar"))) + c.commit("second") + # This wipes out the working directory, ensuring the finder isn't + # finding anything from the filesystem. + c.rawcommand([b"update", b"null"]) + + finder = self._get_finder(self.tmpdir, "0") + f = finder.get("foo") + self.assertEqual(f.read(), b"foo initial") + self.assertEqual(f.read(), b"foo initial", "read again for good measure") + self.assertIsNone(finder.get("bar")) + + finder = self._get_finder(self.tmpdir, rev="1") + f = finder.get("foo") + self.assertEqual(f.read(), b"foo second") + f = finder.get("bar") + self.assertEqual(f.read(), b"bar second") + f = None + + def test_recognize_repo_paths(self): + with self._client() as c: + with open(self.tmppath("foo"), "wb") as fh: + fh.write(b"initial") + c.add(six.ensure_binary(self.tmppath("foo"))) + c.commit("initial") + c.rawcommand([b"update", b"null"]) + + finder = self._get_finder(self.tmpdir, "0", recognize_repo_paths=True) + with self.assertRaises(NotImplementedError): + list(finder.find("")) + + with self.assertRaises(ValueError): + finder.get("foo") + with self.assertRaises(ValueError): + finder.get("") + + f = finder.get(self.tmppath("foo")) + self.assertIsInstance(f, MercurialFile) + self.assertEqual(f.read(), b"initial") + f = None + + +if __name__ == "__main__": + mozunit.main() diff --git a/python/mozbuild/mozpack/test/test_manifests.py b/python/mozbuild/mozpack/test/test_manifests.py new file mode 100644 index 0000000000..a5db53b58c --- /dev/null +++ b/python/mozbuild/mozpack/test/test_manifests.py @@ -0,0 +1,465 @@ +# 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/. + +import os + +import mozunit + +from mozpack.copier import FileCopier, FileRegistry +from mozpack.manifests import InstallManifest, UnreadableInstallManifest +from mozpack.test.test_files import TestWithTmpDir + + +class TestInstallManifest(TestWithTmpDir): + def test_construct(self): + m = InstallManifest() + self.assertEqual(len(m), 0) + + def test_malformed(self): + f = self.tmppath("manifest") + open(f, "wt").write("junk\n") + with self.assertRaises(UnreadableInstallManifest): + InstallManifest(f) + + def test_adds(self): + m = InstallManifest() + m.add_link("s_source", "s_dest") + m.add_copy("c_source", "c_dest") + m.add_required_exists("e_dest") + m.add_optional_exists("o_dest") + m.add_pattern_link("ps_base", "ps/*", "ps_dest") + m.add_pattern_copy("pc_base", "pc/**", "pc_dest") + m.add_preprocess("p_source", "p_dest", "p_source.pp") + m.add_content("content", "content") + + self.assertEqual(len(m), 8) + self.assertIn("s_dest", m) + self.assertIn("c_dest", m) + self.assertIn("p_dest", m) + self.assertIn("e_dest", m) + self.assertIn("o_dest", m) + self.assertIn("content", m) + + with self.assertRaises(ValueError): + m.add_link("s_other", "s_dest") + + with self.assertRaises(ValueError): + m.add_copy("c_other", "c_dest") + + with self.assertRaises(ValueError): + m.add_preprocess("p_other", "p_dest", "p_other.pp") + + with self.assertRaises(ValueError): + m.add_required_exists("e_dest") + + with self.assertRaises(ValueError): + m.add_optional_exists("o_dest") + + with self.assertRaises(ValueError): + m.add_pattern_link("ps_base", "ps/*", "ps_dest") + + with self.assertRaises(ValueError): + m.add_pattern_copy("pc_base", "pc/**", "pc_dest") + + with self.assertRaises(ValueError): + m.add_content("content", "content") + + def _get_test_manifest(self): + m = InstallManifest() + m.add_link(self.tmppath("s_source"), "s_dest") + m.add_copy(self.tmppath("c_source"), "c_dest") + m.add_preprocess( + self.tmppath("p_source"), + "p_dest", + self.tmppath("p_source.pp"), + "#", + {"FOO": "BAR", "BAZ": "QUX"}, + ) + m.add_required_exists("e_dest") + m.add_optional_exists("o_dest") + m.add_pattern_link("ps_base", "*", "ps_dest") + m.add_pattern_copy("pc_base", "**", "pc_dest") + m.add_content("the content\non\nmultiple lines", "content") + + return m + + def test_serialization(self): + m = self._get_test_manifest() + + p = self.tmppath("m") + m.write(path=p) + self.assertTrue(os.path.isfile(p)) + + with open(p, "r") as fh: + c = fh.read() + + self.assertEqual(c.count("\n"), 9) + + lines = c.splitlines() + self.assertEqual(len(lines), 9) + + self.assertEqual(lines[0], "5") + + m2 = InstallManifest(path=p) + self.assertEqual(m, m2) + p2 = self.tmppath("m2") + m2.write(path=p2) + + with open(p2, "r") as fh: + c2 = fh.read() + + self.assertEqual(c, c2) + + def test_populate_registry(self): + m = self._get_test_manifest() + r = FileRegistry() + m.populate_registry(r) + + self.assertEqual(len(r), 6) + self.assertEqual( + r.paths(), ["c_dest", "content", "e_dest", "o_dest", "p_dest", "s_dest"] + ) + + def test_pattern_expansion(self): + source = self.tmppath("source") + os.mkdir(source) + os.mkdir("%s/base" % source) + os.mkdir("%s/base/foo" % source) + + with open("%s/base/foo/file1" % source, "a"): + pass + + with open("%s/base/foo/file2" % source, "a"): + pass + + m = InstallManifest() + m.add_pattern_link("%s/base" % source, "**", "dest") + + c = FileCopier() + m.populate_registry(c) + self.assertEqual(c.paths(), ["dest/foo/file1", "dest/foo/file2"]) + + def test_write_expand_pattern(self): + source = self.tmppath("source") + os.mkdir(source) + os.mkdir("%s/base" % source) + os.mkdir("%s/base/foo" % source) + + with open("%s/base/foo/file1" % source, "a"): + pass + + with open("%s/base/foo/file2" % source, "a"): + pass + + m = InstallManifest() + m.add_pattern_link("%s/base" % source, "**", "dest") + + track = self.tmppath("track") + m.write(path=track, expand_pattern=True) + + m = InstallManifest(path=track) + self.assertEqual( + sorted(dest for dest in m._dests), ["dest/foo/file1", "dest/foo/file2"] + ) + + def test_or(self): + m1 = self._get_test_manifest() + orig_length = len(m1) + m2 = InstallManifest() + m2.add_link("s_source2", "s_dest2") + m2.add_copy("c_source2", "c_dest2") + + m1 |= m2 + + self.assertEqual(len(m2), 2) + self.assertEqual(len(m1), orig_length + 2) + + self.assertIn("s_dest2", m1) + self.assertIn("c_dest2", m1) + + def test_copier_application(self): + dest = self.tmppath("dest") + os.mkdir(dest) + + to_delete = self.tmppath("dest/to_delete") + with open(to_delete, "a"): + pass + + with open(self.tmppath("s_source"), "wt") as fh: + fh.write("symlink!") + + with open(self.tmppath("c_source"), "wt") as fh: + fh.write("copy!") + + with open(self.tmppath("p_source"), "wt") as fh: + fh.write("#define FOO 1\npreprocess!") + + with open(self.tmppath("dest/e_dest"), "a"): + pass + + with open(self.tmppath("dest/o_dest"), "a"): + pass + + m = self._get_test_manifest() + c = FileCopier() + m.populate_registry(c) + result = c.copy(dest) + + self.assertTrue(os.path.exists(self.tmppath("dest/s_dest"))) + self.assertTrue(os.path.exists(self.tmppath("dest/c_dest"))) + self.assertTrue(os.path.exists(self.tmppath("dest/p_dest"))) + self.assertTrue(os.path.exists(self.tmppath("dest/e_dest"))) + self.assertTrue(os.path.exists(self.tmppath("dest/o_dest"))) + self.assertTrue(os.path.exists(self.tmppath("dest/content"))) + self.assertFalse(os.path.exists(to_delete)) + + with open(self.tmppath("dest/s_dest"), "rt") as fh: + self.assertEqual(fh.read(), "symlink!") + + with open(self.tmppath("dest/c_dest"), "rt") as fh: + self.assertEqual(fh.read(), "copy!") + + with open(self.tmppath("dest/p_dest"), "rt") as fh: + self.assertEqual(fh.read(), "preprocess!") + + self.assertEqual( + result.updated_files, + set( + self.tmppath(p) + for p in ("dest/s_dest", "dest/c_dest", "dest/p_dest", "dest/content") + ), + ) + self.assertEqual( + result.existing_files, + set([self.tmppath("dest/e_dest"), self.tmppath("dest/o_dest")]), + ) + self.assertEqual(result.removed_files, {to_delete}) + self.assertEqual(result.removed_directories, set()) + + def test_preprocessor(self): + manifest = self.tmppath("m") + deps = self.tmppath("m.pp") + dest = self.tmppath("dest") + include = self.tmppath("p_incl") + + with open(include, "wt") as fh: + fh.write("#define INCL\n") + time = os.path.getmtime(include) - 3 + os.utime(include, (time, time)) + + with open(self.tmppath("p_source"), "wt") as fh: + fh.write("#ifdef FOO\n#if BAZ == QUX\nPASS1\n#endif\n#endif\n") + fh.write("#ifdef DEPTEST\nPASS2\n#endif\n") + fh.write("#include p_incl\n#ifdef INCLTEST\nPASS3\n#endif\n") + time = os.path.getmtime(self.tmppath("p_source")) - 3 + os.utime(self.tmppath("p_source"), (time, time)) + + # Create and write a manifest with the preprocessed file, then apply it. + # This should write out our preprocessed file. + m = InstallManifest() + m.add_preprocess( + self.tmppath("p_source"), "p_dest", deps, "#", {"FOO": "BAR", "BAZ": "QUX"} + ) + m.write(path=manifest) + + m = InstallManifest(path=manifest) + c = FileCopier() + m.populate_registry(c) + c.copy(dest) + + self.assertTrue(os.path.exists(self.tmppath("dest/p_dest"))) + + with open(self.tmppath("dest/p_dest"), "rt") as fh: + self.assertEqual(fh.read(), "PASS1\n") + + # Create a second manifest with the preprocessed file, then apply it. + # Since this manifest does not exist on the disk, there should not be a + # dependency on it, and the preprocessed file should not be modified. + m2 = InstallManifest() + m2.add_preprocess( + self.tmppath("p_source"), "p_dest", deps, "#", {"DEPTEST": True} + ) + c = FileCopier() + m2.populate_registry(c) + result = c.copy(dest) + + self.assertFalse(self.tmppath("dest/p_dest") in result.updated_files) + self.assertTrue(self.tmppath("dest/p_dest") in result.existing_files) + + # Write out the second manifest, then load it back in from the disk. + # This should add the dependency on the manifest file, so our + # preprocessed file should be regenerated with the new defines. + # We also set the mtime on the destination file back, so it will be + # older than the manifest file. + m2.write(path=manifest) + time = os.path.getmtime(manifest) - 1 + os.utime(self.tmppath("dest/p_dest"), (time, time)) + m2 = InstallManifest(path=manifest) + c = FileCopier() + m2.populate_registry(c) + self.assertTrue(c.copy(dest)) + + with open(self.tmppath("dest/p_dest"), "rt") as fh: + self.assertEqual(fh.read(), "PASS2\n") + + # Set the time on the manifest back, so it won't be picked up as + # modified in the next test + time = os.path.getmtime(manifest) - 1 + os.utime(manifest, (time, time)) + + # Update the contents of a file included by the source file. This should + # cause the destination to be regenerated. + with open(include, "wt") as fh: + fh.write("#define INCLTEST\n") + + time = os.path.getmtime(include) - 1 + os.utime(self.tmppath("dest/p_dest"), (time, time)) + c = FileCopier() + m2.populate_registry(c) + self.assertTrue(c.copy(dest)) + + with open(self.tmppath("dest/p_dest"), "rt") as fh: + self.assertEqual(fh.read(), "PASS2\nPASS3\n") + + def test_preprocessor_dependencies(self): + manifest = self.tmppath("m") + deps = self.tmppath("m.pp") + dest = self.tmppath("dest") + source = self.tmppath("p_source") + destfile = self.tmppath("dest/p_dest") + include = self.tmppath("p_incl") + os.mkdir(dest) + + with open(source, "wt") as fh: + fh.write("#define SRC\nSOURCE\n") + time = os.path.getmtime(source) - 3 + os.utime(source, (time, time)) + + with open(include, "wt") as fh: + fh.write("INCLUDE\n") + time = os.path.getmtime(source) - 3 + os.utime(include, (time, time)) + + # Create and write a manifest with the preprocessed file. + m = InstallManifest() + m.add_preprocess(source, "p_dest", deps, "#", {"FOO": "BAR", "BAZ": "QUX"}) + m.write(path=manifest) + + time = os.path.getmtime(source) - 5 + os.utime(manifest, (time, time)) + + # Now read the manifest back in, and apply it. This should write out + # our preprocessed file. + m = InstallManifest(path=manifest) + c = FileCopier() + m.populate_registry(c) + self.assertTrue(c.copy(dest)) + + with open(destfile, "rt") as fh: + self.assertEqual(fh.read(), "SOURCE\n") + + # Next, modify the source to #INCLUDE another file. + with open(source, "wt") as fh: + fh.write("SOURCE\n#include p_incl\n") + time = os.path.getmtime(source) - 1 + os.utime(destfile, (time, time)) + + # Apply the manifest, and confirm that it also reads the newly included + # file. + m = InstallManifest(path=manifest) + c = FileCopier() + m.populate_registry(c) + c.copy(dest) + + with open(destfile, "rt") as fh: + self.assertEqual(fh.read(), "SOURCE\nINCLUDE\n") + + # Set the time on the source file back, so it won't be picked up as + # modified in the next test. + time = os.path.getmtime(source) - 1 + os.utime(source, (time, time)) + + # Now, modify the include file (but not the original source). + with open(include, "wt") as fh: + fh.write("INCLUDE MODIFIED\n") + time = os.path.getmtime(include) - 1 + os.utime(destfile, (time, time)) + + # Apply the manifest, and confirm that the change to the include file + # is detected. That should cause the preprocessor to run again. + m = InstallManifest(path=manifest) + c = FileCopier() + m.populate_registry(c) + c.copy(dest) + + with open(destfile, "rt") as fh: + self.assertEqual(fh.read(), "SOURCE\nINCLUDE MODIFIED\n") + + # ORing an InstallManifest should copy file dependencies + m = InstallManifest() + m |= InstallManifest(path=manifest) + c = FileCopier() + m.populate_registry(c) + e = c._files["p_dest"] + self.assertEqual(e.extra_depends, [manifest]) + + def test_add_entries_from(self): + source = self.tmppath("source") + os.mkdir(source) + os.mkdir("%s/base" % source) + os.mkdir("%s/base/foo" % source) + + with open("%s/base/foo/file1" % source, "a"): + pass + + with open("%s/base/foo/file2" % source, "a"): + pass + + m = InstallManifest() + m.add_pattern_link("%s/base" % source, "**", "dest") + + p = InstallManifest() + p.add_entries_from(m) + self.assertEqual(len(p), 1) + + c = FileCopier() + p.populate_registry(c) + self.assertEqual(c.paths(), ["dest/foo/file1", "dest/foo/file2"]) + + q = InstallManifest() + q.add_entries_from(m, base="target") + self.assertEqual(len(q), 1) + + d = FileCopier() + q.populate_registry(d) + self.assertEqual(d.paths(), ["target/dest/foo/file1", "target/dest/foo/file2"]) + + # Some of the values in an InstallManifest include destination + # information that is present in the keys. Verify that we can + # round-trip serialization. + r = InstallManifest() + r.add_entries_from(m) + r.add_entries_from(m, base="target") + self.assertEqual(len(r), 2) + + temp_path = self.tmppath("temp_path") + r.write(path=temp_path) + + s = InstallManifest(path=temp_path) + e = FileCopier() + s.populate_registry(e) + + self.assertEqual( + e.paths(), + [ + "dest/foo/file1", + "dest/foo/file2", + "target/dest/foo/file1", + "target/dest/foo/file2", + ], + ) + + +if __name__ == "__main__": + mozunit.main() diff --git a/python/mozbuild/mozpack/test/test_mozjar.py b/python/mozbuild/mozpack/test/test_mozjar.py new file mode 100644 index 0000000000..e96c59238f --- /dev/null +++ b/python/mozbuild/mozpack/test/test_mozjar.py @@ -0,0 +1,350 @@ +# 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/. + +import os +import unittest +from collections import OrderedDict + +import mozunit +import six + +import mozpack.path as mozpath +from mozpack.files import FileFinder +from mozpack.mozjar import ( + Deflater, + JarLog, + JarReader, + JarReaderError, + JarStruct, + JarWriter, + JarWriterError, +) +from mozpack.test.test_files import MockDest + +test_data_path = mozpath.abspath(mozpath.dirname(__file__)) +test_data_path = mozpath.join(test_data_path, "data") + + +class TestJarStruct(unittest.TestCase): + class Foo(JarStruct): + MAGIC = 0x01020304 + STRUCT = OrderedDict( + [ + ("foo", "uint32"), + ("bar", "uint16"), + ("qux", "uint16"), + ("length", "uint16"), + ("length2", "uint16"), + ("string", "length"), + ("string2", "length2"), + ] + ) + + def test_jar_struct(self): + foo = TestJarStruct.Foo() + self.assertEqual(foo.signature, TestJarStruct.Foo.MAGIC) + self.assertEqual(foo["foo"], 0) + self.assertEqual(foo["bar"], 0) + self.assertEqual(foo["qux"], 0) + self.assertFalse("length" in foo) + self.assertFalse("length2" in foo) + self.assertEqual(foo["string"], "") + self.assertEqual(foo["string2"], "") + + self.assertEqual(foo.size, 16) + + foo["foo"] = 0x42434445 + foo["bar"] = 0xABCD + foo["qux"] = 0xEF01 + foo["string"] = "abcde" + foo["string2"] = "Arbitrarily long string" + + serialized = ( + b"\x04\x03\x02\x01\x45\x44\x43\x42\xcd\xab\x01\xef" + + b"\x05\x00\x17\x00abcdeArbitrarily long string" + ) + self.assertEqual(foo.size, len(serialized)) + foo_serialized = foo.serialize() + self.assertEqual(foo_serialized, serialized) + + def do_test_read_jar_struct(self, data): + self.assertRaises(JarReaderError, TestJarStruct.Foo, data) + self.assertRaises(JarReaderError, TestJarStruct.Foo, data[2:]) + + foo = TestJarStruct.Foo(data[1:]) + self.assertEqual(foo["foo"], 0x45444342) + self.assertEqual(foo["bar"], 0xCDAB) + self.assertEqual(foo["qux"], 0x01EF) + self.assertFalse("length" in foo) + self.assertFalse("length2" in foo) + self.assertEqual(foo["string"], b"012345") + self.assertEqual(foo["string2"], b"67") + + def test_read_jar_struct(self): + data = ( + b"\x00\x04\x03\x02\x01\x42\x43\x44\x45\xab\xcd\xef" + + b"\x01\x06\x00\x02\x0001234567890" + ) + self.do_test_read_jar_struct(data) + + def test_read_jar_struct_memoryview(self): + data = ( + b"\x00\x04\x03\x02\x01\x42\x43\x44\x45\xab\xcd\xef" + + b"\x01\x06\x00\x02\x0001234567890" + ) + self.do_test_read_jar_struct(memoryview(data)) + + +class TestDeflater(unittest.TestCase): + def wrap(self, data): + return data + + def test_deflater_no_compress(self): + deflater = Deflater(False) + deflater.write(self.wrap(b"abc")) + self.assertFalse(deflater.compressed) + self.assertEqual(deflater.uncompressed_size, 3) + self.assertEqual(deflater.compressed_size, deflater.uncompressed_size) + self.assertEqual(deflater.compressed_data, b"abc") + self.assertEqual(deflater.crc32, 0x352441C2) + + def test_deflater_compress_no_gain(self): + deflater = Deflater(True) + deflater.write(self.wrap(b"abc")) + self.assertFalse(deflater.compressed) + self.assertEqual(deflater.uncompressed_size, 3) + self.assertEqual(deflater.compressed_size, deflater.uncompressed_size) + self.assertEqual(deflater.compressed_data, b"abc") + self.assertEqual(deflater.crc32, 0x352441C2) + + def test_deflater_compress(self): + deflater = Deflater(True) + deflater.write(self.wrap(b"aaaaaaaaaaaaanopqrstuvwxyz")) + self.assertTrue(deflater.compressed) + self.assertEqual(deflater.uncompressed_size, 26) + self.assertNotEqual(deflater.compressed_size, deflater.uncompressed_size) + self.assertEqual(deflater.crc32, 0xD46B97ED) + # The CRC is the same as when not compressed + deflater = Deflater(False) + self.assertFalse(deflater.compressed) + deflater.write(self.wrap(b"aaaaaaaaaaaaanopqrstuvwxyz")) + self.assertEqual(deflater.crc32, 0xD46B97ED) + + def test_deflater_empty(self): + deflater = Deflater(False) + self.assertFalse(deflater.compressed) + self.assertEqual(deflater.uncompressed_size, 0) + self.assertEqual(deflater.compressed_size, deflater.uncompressed_size) + self.assertEqual(deflater.compressed_data, b"") + self.assertEqual(deflater.crc32, 0) + + +class TestDeflaterMemoryView(TestDeflater): + def wrap(self, data): + return memoryview(data) + + +class TestJar(unittest.TestCase): + def test_jar(self): + s = MockDest() + with JarWriter(fileobj=s) as jar: + jar.add("foo", b"foo") + self.assertRaises(JarWriterError, jar.add, "foo", b"bar") + jar.add("bar", b"aaaaaaaaaaaaanopqrstuvwxyz") + jar.add("baz/qux", b"aaaaaaaaaaaaanopqrstuvwxyz", False) + jar.add("baz\\backslash", b"aaaaaaaaaaaaaaa") + + files = [j for j in JarReader(fileobj=s)] + + self.assertEqual(files[0].filename, "foo") + self.assertFalse(files[0].compressed) + self.assertEqual(files[0].read(), b"foo") + + self.assertEqual(files[1].filename, "bar") + self.assertTrue(files[1].compressed) + self.assertEqual(files[1].read(), b"aaaaaaaaaaaaanopqrstuvwxyz") + + self.assertEqual(files[2].filename, "baz/qux") + self.assertFalse(files[2].compressed) + self.assertEqual(files[2].read(), b"aaaaaaaaaaaaanopqrstuvwxyz") + + if os.sep == "\\": + self.assertEqual( + files[3].filename, + "baz/backslash", + "backslashes in filenames on Windows should get normalized", + ) + else: + self.assertEqual( + files[3].filename, + "baz\\backslash", + "backslashes in filenames on POSIX platform are untouched", + ) + + s = MockDest() + with JarWriter(fileobj=s, compress=False) as jar: + jar.add("bar", b"aaaaaaaaaaaaanopqrstuvwxyz") + jar.add("foo", b"foo") + jar.add("baz/qux", b"aaaaaaaaaaaaanopqrstuvwxyz", True) + + jar = JarReader(fileobj=s) + files = [j for j in jar] + + self.assertEqual(files[0].filename, "bar") + self.assertFalse(files[0].compressed) + self.assertEqual(files[0].read(), b"aaaaaaaaaaaaanopqrstuvwxyz") + + self.assertEqual(files[1].filename, "foo") + self.assertFalse(files[1].compressed) + self.assertEqual(files[1].read(), b"foo") + + self.assertEqual(files[2].filename, "baz/qux") + self.assertTrue(files[2].compressed) + self.assertEqual(files[2].read(), b"aaaaaaaaaaaaanopqrstuvwxyz") + + self.assertTrue("bar" in jar) + self.assertTrue("foo" in jar) + self.assertFalse("baz" in jar) + self.assertTrue("baz/qux" in jar) + self.assertTrue(jar["bar"], files[1]) + self.assertTrue(jar["foo"], files[0]) + self.assertTrue(jar["baz/qux"], files[2]) + + s.seek(0) + jar = JarReader(fileobj=s) + self.assertTrue("bar" in jar) + self.assertTrue("foo" in jar) + self.assertFalse("baz" in jar) + self.assertTrue("baz/qux" in jar) + + files[0].seek(0) + self.assertEqual(jar["bar"].filename, files[0].filename) + self.assertEqual(jar["bar"].compressed, files[0].compressed) + self.assertEqual(jar["bar"].read(), files[0].read()) + + files[1].seek(0) + self.assertEqual(jar["foo"].filename, files[1].filename) + self.assertEqual(jar["foo"].compressed, files[1].compressed) + self.assertEqual(jar["foo"].read(), files[1].read()) + + files[2].seek(0) + self.assertEqual(jar["baz/qux"].filename, files[2].filename) + self.assertEqual(jar["baz/qux"].compressed, files[2].compressed) + self.assertEqual(jar["baz/qux"].read(), files[2].read()) + + def test_rejar(self): + s = MockDest() + with JarWriter(fileobj=s) as jar: + jar.add("foo", b"foo") + jar.add("bar", b"aaaaaaaaaaaaanopqrstuvwxyz") + jar.add("baz/qux", b"aaaaaaaaaaaaanopqrstuvwxyz", False) + + new = MockDest() + with JarWriter(fileobj=new) as jar: + for j in JarReader(fileobj=s): + jar.add(j.filename, j) + + jar = JarReader(fileobj=new) + files = [j for j in jar] + + self.assertEqual(files[0].filename, "foo") + self.assertFalse(files[0].compressed) + self.assertEqual(files[0].read(), b"foo") + + self.assertEqual(files[1].filename, "bar") + self.assertTrue(files[1].compressed) + self.assertEqual(files[1].read(), b"aaaaaaaaaaaaanopqrstuvwxyz") + + self.assertEqual(files[2].filename, "baz/qux") + self.assertTrue(files[2].compressed) + self.assertEqual(files[2].read(), b"aaaaaaaaaaaaanopqrstuvwxyz") + + def test_add_from_finder(self): + s = MockDest() + with JarWriter(fileobj=s) as jar: + finder = FileFinder(test_data_path) + for p, f in finder.find("test_data"): + jar.add("test_data", f) + + jar = JarReader(fileobj=s) + files = [j for j in jar] + + self.assertEqual(files[0].filename, "test_data") + self.assertFalse(files[0].compressed) + self.assertEqual(files[0].read(), b"test_data") + + +class TestPreload(unittest.TestCase): + def test_preload(self): + s = MockDest() + with JarWriter(fileobj=s) as jar: + jar.add("foo", b"foo") + jar.add("bar", b"abcdefghijklmnopqrstuvwxyz") + jar.add("baz/qux", b"aaaaaaaaaaaaanopqrstuvwxyz") + + jar = JarReader(fileobj=s) + self.assertEqual(jar.last_preloaded, None) + + with JarWriter(fileobj=s) as jar: + jar.add("foo", b"foo") + jar.add("bar", b"abcdefghijklmnopqrstuvwxyz") + jar.add("baz/qux", b"aaaaaaaaaaaaanopqrstuvwxyz") + jar.preload(["baz/qux", "bar"]) + + jar = JarReader(fileobj=s) + self.assertEqual(jar.last_preloaded, "bar") + files = [j for j in jar] + + self.assertEqual(files[0].filename, "baz/qux") + self.assertEqual(files[1].filename, "bar") + self.assertEqual(files[2].filename, "foo") + + +class TestJarLog(unittest.TestCase): + def test_jarlog(self): + s = six.moves.cStringIO( + "\n".join( + [ + "bar/baz.jar first", + "bar/baz.jar second", + "bar/baz.jar third", + "bar/baz.jar second", + "bar/baz.jar second", + "omni.ja stuff", + "bar/baz.jar first", + "omni.ja other/stuff", + "omni.ja stuff", + "bar/baz.jar third", + ] + ) + ) + log = JarLog(fileobj=s) + self.assertEqual( + set(log.keys()), + set( + [ + "bar/baz.jar", + "omni.ja", + ] + ), + ) + self.assertEqual( + log["bar/baz.jar"], + [ + "first", + "second", + "third", + ], + ) + self.assertEqual( + log["omni.ja"], + [ + "stuff", + "other/stuff", + ], + ) + + +if __name__ == "__main__": + mozunit.main() diff --git a/python/mozbuild/mozpack/test/test_packager.py b/python/mozbuild/mozpack/test/test_packager.py new file mode 100644 index 0000000000..266902ebb2 --- /dev/null +++ b/python/mozbuild/mozpack/test/test_packager.py @@ -0,0 +1,630 @@ +# 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/. + +import os +import unittest + +import mozunit +from buildconfig import topobjdir +from mozunit import MockedOpen + +import mozpack.path as mozpath +from mozbuild.preprocessor import Preprocessor +from mozpack.chrome.manifest import ( + ManifestBinaryComponent, + ManifestContent, + ManifestResource, +) +from mozpack.errors import ErrorMessage, errors +from mozpack.files import GeneratedFile +from mozpack.packager import ( + CallDeque, + Component, + SimpleManifestSink, + SimplePackager, + preprocess_manifest, +) + +MANIFEST = """ +bar/* +[foo] +foo/* +-foo/bar +chrome.manifest +[zot destdir="destdir"] +foo/zot +; comment +#ifdef baz +[baz] +baz@SUFFIX@ +#endif +""" + + +class TestPreprocessManifest(unittest.TestCase): + MANIFEST_PATH = mozpath.join("$OBJDIR", "manifest") + + EXPECTED_LOG = [ + ((MANIFEST_PATH, 2), "add", "", "bar/*"), + ((MANIFEST_PATH, 4), "add", "foo", "foo/*"), + ((MANIFEST_PATH, 5), "remove", "foo", "foo/bar"), + ((MANIFEST_PATH, 6), "add", "foo", "chrome.manifest"), + ((MANIFEST_PATH, 8), "add", 'zot destdir="destdir"', "foo/zot"), + ] + + def setUp(self): + class MockSink(object): + def __init__(self): + self.log = [] + + def add(self, component, path): + self._log(errors.get_context(), "add", repr(component), path) + + def remove(self, component, path): + self._log(errors.get_context(), "remove", repr(component), path) + + def _log(self, *args): + self.log.append(args) + + self.sink = MockSink() + self.cwd = os.getcwd() + os.chdir(topobjdir) + + def tearDown(self): + os.chdir(self.cwd) + + def test_preprocess_manifest(self): + with MockedOpen({"manifest": MANIFEST}): + preprocess_manifest(self.sink, "manifest") + self.assertEqual(self.sink.log, self.EXPECTED_LOG) + + def test_preprocess_manifest_missing_define(self): + with MockedOpen({"manifest": MANIFEST}): + self.assertRaises( + Preprocessor.Error, + preprocess_manifest, + self.sink, + "manifest", + {"baz": 1}, + ) + + def test_preprocess_manifest_defines(self): + with MockedOpen({"manifest": MANIFEST}): + preprocess_manifest(self.sink, "manifest", {"baz": 1, "SUFFIX": ".exe"}) + self.assertEqual( + self.sink.log, + self.EXPECTED_LOG + [((self.MANIFEST_PATH, 12), "add", "baz", "baz.exe")], + ) + + +class MockFinder(object): + def __init__(self, files): + self.files = files + self.log = [] + + def find(self, path): + self.log.append(path) + for f in sorted(self.files): + if mozpath.match(f, path): + yield f, self.files[f] + + def __iter__(self): + return self.find("") + + +class MockFormatter(object): + def __init__(self): + self.log = [] + + def add_base(self, *args): + self._log(errors.get_context(), "add_base", *args) + + def add_manifest(self, *args): + self._log(errors.get_context(), "add_manifest", *args) + + def add_interfaces(self, *args): + self._log(errors.get_context(), "add_interfaces", *args) + + def add(self, *args): + self._log(errors.get_context(), "add", *args) + + def _log(self, *args): + self.log.append(args) + + +class TestSimplePackager(unittest.TestCase): + def test_simple_packager(self): + class GeneratedFileWithPath(GeneratedFile): + def __init__(self, path, content): + GeneratedFile.__init__(self, content) + self.path = path + + formatter = MockFormatter() + packager = SimplePackager(formatter) + curdir = os.path.abspath(os.curdir) + file = GeneratedFileWithPath( + os.path.join(curdir, "foo", "bar.manifest"), + b"resource bar bar/\ncontent bar bar/", + ) + with errors.context("manifest", 1): + packager.add("foo/bar.manifest", file) + + file = GeneratedFileWithPath( + os.path.join(curdir, "foo", "baz.manifest"), b"resource baz baz/" + ) + with errors.context("manifest", 2): + packager.add("bar/baz.manifest", file) + + with errors.context("manifest", 3): + packager.add( + "qux/qux.manifest", + GeneratedFile( + b"".join( + [ + b"resource qux qux/\n", + b"binary-component qux.so\n", + ] + ) + ), + ) + bar_xpt = GeneratedFile(b"bar.xpt") + qux_xpt = GeneratedFile(b"qux.xpt") + foo_html = GeneratedFile(b"foo_html") + bar_html = GeneratedFile(b"bar_html") + with errors.context("manifest", 4): + packager.add("foo/bar.xpt", bar_xpt) + with errors.context("manifest", 5): + packager.add("foo/bar/foo.html", foo_html) + packager.add("foo/bar/bar.html", bar_html) + + file = GeneratedFileWithPath( + os.path.join(curdir, "foo.manifest"), + b"".join( + [ + b"manifest foo/bar.manifest\n", + b"manifest bar/baz.manifest\n", + ] + ), + ) + with errors.context("manifest", 6): + packager.add("foo.manifest", file) + with errors.context("manifest", 7): + packager.add("foo/qux.xpt", qux_xpt) + + file = GeneratedFileWithPath( + os.path.join(curdir, "addon", "chrome.manifest"), b"resource hoge hoge/" + ) + with errors.context("manifest", 8): + packager.add("addon/chrome.manifest", file) + + install_rdf = GeneratedFile(b"<RDF></RDF>") + with errors.context("manifest", 9): + packager.add("addon/install.rdf", install_rdf) + + with errors.context("manifest", 10): + packager.add("addon2/install.rdf", install_rdf) + packager.add( + "addon2/chrome.manifest", GeneratedFile(b"binary-component addon2.so") + ) + + with errors.context("manifest", 11): + packager.add("addon3/install.rdf", install_rdf) + packager.add( + "addon3/chrome.manifest", + GeneratedFile(b"manifest components/components.manifest"), + ) + packager.add( + "addon3/components/components.manifest", + GeneratedFile(b"binary-component addon3.so"), + ) + + with errors.context("manifest", 12): + install_rdf_addon4 = GeneratedFile( + b"<RDF>\n<...>\n<em:unpack>true</em:unpack>\n<...>\n</RDF>" + ) + packager.add("addon4/install.rdf", install_rdf_addon4) + + with errors.context("manifest", 13): + install_rdf_addon5 = GeneratedFile( + b"<RDF>\n<...>\n<em:unpack>false</em:unpack>\n<...>\n</RDF>" + ) + packager.add("addon5/install.rdf", install_rdf_addon5) + + with errors.context("manifest", 14): + install_rdf_addon6 = GeneratedFile( + b"<RDF>\n<... em:unpack=true>\n<...>\n</RDF>" + ) + packager.add("addon6/install.rdf", install_rdf_addon6) + + with errors.context("manifest", 15): + install_rdf_addon7 = GeneratedFile( + b"<RDF>\n<... em:unpack=false>\n<...>\n</RDF>" + ) + packager.add("addon7/install.rdf", install_rdf_addon7) + + with errors.context("manifest", 16): + install_rdf_addon8 = GeneratedFile( + b'<RDF>\n<... em:unpack="true">\n<...>\n</RDF>' + ) + packager.add("addon8/install.rdf", install_rdf_addon8) + + with errors.context("manifest", 17): + install_rdf_addon9 = GeneratedFile( + b'<RDF>\n<... em:unpack="false">\n<...>\n</RDF>' + ) + packager.add("addon9/install.rdf", install_rdf_addon9) + + with errors.context("manifest", 18): + install_rdf_addon10 = GeneratedFile( + b"<RDF>\n<... em:unpack='true'>\n<...>\n</RDF>" + ) + packager.add("addon10/install.rdf", install_rdf_addon10) + + with errors.context("manifest", 19): + install_rdf_addon11 = GeneratedFile( + b"<RDF>\n<... em:unpack='false'>\n<...>\n</RDF>" + ) + packager.add("addon11/install.rdf", install_rdf_addon11) + + we_manifest = GeneratedFile( + b'{"manifest_version": 2, "name": "Test WebExtension", "version": "1.0"}' + ) + # hybrid and hybrid2 are both bootstrapped extensions with + # embedded webextensions, they differ in the order in which + # the manifests are added to the packager. + with errors.context("manifest", 20): + packager.add("hybrid/install.rdf", install_rdf) + + with errors.context("manifest", 21): + packager.add("hybrid/webextension/manifest.json", we_manifest) + + with errors.context("manifest", 22): + packager.add("hybrid2/webextension/manifest.json", we_manifest) + + with errors.context("manifest", 23): + packager.add("hybrid2/install.rdf", install_rdf) + + with errors.context("manifest", 24): + packager.add("webextension/manifest.json", we_manifest) + + non_we_manifest = GeneratedFile(b'{"not a webextension": true}') + with errors.context("manifest", 25): + packager.add("nonwebextension/manifest.json", non_we_manifest) + + self.assertEqual(formatter.log, []) + + with errors.context("dummy", 1): + packager.close() + self.maxDiff = None + # The formatter is expected to reorder the manifest entries so that + # chrome entries appear before the others. + self.assertEqual( + formatter.log, + [ + (("dummy", 1), "add_base", "", False), + (("dummy", 1), "add_base", "addon", True), + (("dummy", 1), "add_base", "addon10", "unpacked"), + (("dummy", 1), "add_base", "addon11", True), + (("dummy", 1), "add_base", "addon2", "unpacked"), + (("dummy", 1), "add_base", "addon3", "unpacked"), + (("dummy", 1), "add_base", "addon4", "unpacked"), + (("dummy", 1), "add_base", "addon5", True), + (("dummy", 1), "add_base", "addon6", "unpacked"), + (("dummy", 1), "add_base", "addon7", True), + (("dummy", 1), "add_base", "addon8", "unpacked"), + (("dummy", 1), "add_base", "addon9", True), + (("dummy", 1), "add_base", "hybrid", True), + (("dummy", 1), "add_base", "hybrid2", True), + (("dummy", 1), "add_base", "qux", False), + (("dummy", 1), "add_base", "webextension", True), + ( + (os.path.join(curdir, "foo", "bar.manifest"), 2), + "add_manifest", + ManifestContent("foo", "bar", "bar/"), + ), + ( + (os.path.join(curdir, "foo", "bar.manifest"), 1), + "add_manifest", + ManifestResource("foo", "bar", "bar/"), + ), + ( + ("bar/baz.manifest", 1), + "add_manifest", + ManifestResource("bar", "baz", "baz/"), + ), + ( + ("qux/qux.manifest", 1), + "add_manifest", + ManifestResource("qux", "qux", "qux/"), + ), + ( + ("qux/qux.manifest", 2), + "add_manifest", + ManifestBinaryComponent("qux", "qux.so"), + ), + (("manifest", 4), "add_interfaces", "foo/bar.xpt", bar_xpt), + (("manifest", 7), "add_interfaces", "foo/qux.xpt", qux_xpt), + ( + (os.path.join(curdir, "addon", "chrome.manifest"), 1), + "add_manifest", + ManifestResource("addon", "hoge", "hoge/"), + ), + ( + ("addon2/chrome.manifest", 1), + "add_manifest", + ManifestBinaryComponent("addon2", "addon2.so"), + ), + ( + ("addon3/components/components.manifest", 1), + "add_manifest", + ManifestBinaryComponent("addon3/components", "addon3.so"), + ), + (("manifest", 5), "add", "foo/bar/foo.html", foo_html), + (("manifest", 5), "add", "foo/bar/bar.html", bar_html), + (("manifest", 9), "add", "addon/install.rdf", install_rdf), + (("manifest", 10), "add", "addon2/install.rdf", install_rdf), + (("manifest", 11), "add", "addon3/install.rdf", install_rdf), + (("manifest", 12), "add", "addon4/install.rdf", install_rdf_addon4), + (("manifest", 13), "add", "addon5/install.rdf", install_rdf_addon5), + (("manifest", 14), "add", "addon6/install.rdf", install_rdf_addon6), + (("manifest", 15), "add", "addon7/install.rdf", install_rdf_addon7), + (("manifest", 16), "add", "addon8/install.rdf", install_rdf_addon8), + (("manifest", 17), "add", "addon9/install.rdf", install_rdf_addon9), + (("manifest", 18), "add", "addon10/install.rdf", install_rdf_addon10), + (("manifest", 19), "add", "addon11/install.rdf", install_rdf_addon11), + (("manifest", 20), "add", "hybrid/install.rdf", install_rdf), + ( + ("manifest", 21), + "add", + "hybrid/webextension/manifest.json", + we_manifest, + ), + ( + ("manifest", 22), + "add", + "hybrid2/webextension/manifest.json", + we_manifest, + ), + (("manifest", 23), "add", "hybrid2/install.rdf", install_rdf), + (("manifest", 24), "add", "webextension/manifest.json", we_manifest), + ( + ("manifest", 25), + "add", + "nonwebextension/manifest.json", + non_we_manifest, + ), + ], + ) + + self.assertEqual( + packager.get_bases(), + set( + [ + "", + "addon", + "addon2", + "addon3", + "addon4", + "addon5", + "addon6", + "addon7", + "addon8", + "addon9", + "addon10", + "addon11", + "qux", + "hybrid", + "hybrid2", + "webextension", + ] + ), + ) + self.assertEqual(packager.get_bases(addons=False), set(["", "qux"])) + + def test_simple_packager_manifest_consistency(self): + formatter = MockFormatter() + # bar/ is detected as an addon because of install.rdf, but top-level + # includes a manifest inside bar/. + packager = SimplePackager(formatter) + packager.add( + "base.manifest", + GeneratedFile( + b"manifest foo/bar.manifest\n" b"manifest bar/baz.manifest\n" + ), + ) + packager.add("foo/bar.manifest", GeneratedFile(b"resource bar bar")) + packager.add("bar/baz.manifest", GeneratedFile(b"resource baz baz")) + packager.add("bar/install.rdf", GeneratedFile(b"")) + + with self.assertRaises(ErrorMessage) as e: + packager.close() + + self.assertEqual( + str(e.exception), + 'error: "bar/baz.manifest" is included from "base.manifest", ' + 'which is outside "bar"', + ) + + # bar/ is detected as a separate base because of chrome.manifest that + # is included nowhere, but top-level includes another manifest inside + # bar/. + packager = SimplePackager(formatter) + packager.add( + "base.manifest", + GeneratedFile( + b"manifest foo/bar.manifest\n" b"manifest bar/baz.manifest\n" + ), + ) + packager.add("foo/bar.manifest", GeneratedFile(b"resource bar bar")) + packager.add("bar/baz.manifest", GeneratedFile(b"resource baz baz")) + packager.add("bar/chrome.manifest", GeneratedFile(b"resource baz baz")) + + with self.assertRaises(ErrorMessage) as e: + packager.close() + + self.assertEqual( + str(e.exception), + 'error: "bar/baz.manifest" is included from "base.manifest", ' + 'which is outside "bar"', + ) + + # bar/ is detected as a separate base because of chrome.manifest that + # is included nowhere, but chrome.manifest includes baz.manifest from + # the same directory. This shouldn't error out. + packager = SimplePackager(formatter) + packager.add("base.manifest", GeneratedFile(b"manifest foo/bar.manifest\n")) + packager.add("foo/bar.manifest", GeneratedFile(b"resource bar bar")) + packager.add("bar/baz.manifest", GeneratedFile(b"resource baz baz")) + packager.add("bar/chrome.manifest", GeneratedFile(b"manifest baz.manifest")) + packager.close() + + +class TestSimpleManifestSink(unittest.TestCase): + def test_simple_manifest_parser(self): + formatter = MockFormatter() + foobar = GeneratedFile(b"foobar") + foobaz = GeneratedFile(b"foobaz") + fooqux = GeneratedFile(b"fooqux") + foozot = GeneratedFile(b"foozot") + finder = MockFinder( + { + "bin/foo/bar": foobar, + "bin/foo/baz": foobaz, + "bin/foo/qux": fooqux, + "bin/foo/zot": foozot, + "bin/foo/chrome.manifest": GeneratedFile(b"resource foo foo/"), + "bin/chrome.manifest": GeneratedFile(b"manifest foo/chrome.manifest"), + } + ) + parser = SimpleManifestSink(finder, formatter) + component0 = Component("component0") + component1 = Component("component1") + component2 = Component("component2", destdir="destdir") + parser.add(component0, "bin/foo/b*") + parser.add(component1, "bin/foo/qux") + parser.add(component1, "bin/foo/chrome.manifest") + parser.add(component2, "bin/foo/zot") + self.assertRaises(ErrorMessage, parser.add, "component1", "bin/bar") + + self.assertEqual(formatter.log, []) + parser.close() + self.assertEqual( + formatter.log, + [ + (None, "add_base", "", False), + ( + ("foo/chrome.manifest", 1), + "add_manifest", + ManifestResource("foo", "foo", "foo/"), + ), + (None, "add", "foo/bar", foobar), + (None, "add", "foo/baz", foobaz), + (None, "add", "foo/qux", fooqux), + (None, "add", "destdir/foo/zot", foozot), + ], + ) + + self.assertEqual( + finder.log, + [ + "bin/foo/b*", + "bin/foo/qux", + "bin/foo/chrome.manifest", + "bin/foo/zot", + "bin/bar", + "bin/chrome.manifest", + ], + ) + + +class TestCallDeque(unittest.TestCase): + def test_call_deque(self): + class Logger(object): + def __init__(self): + self._log = [] + + def log(self, str): + self._log.append(str) + + @staticmethod + def staticlog(logger, str): + logger.log(str) + + def do_log(logger, str): + logger.log(str) + + logger = Logger() + d = CallDeque() + d.append(logger.log, "foo") + d.append(logger.log, "bar") + d.append(logger.staticlog, logger, "baz") + d.append(do_log, logger, "qux") + self.assertEqual(logger._log, []) + d.execute() + self.assertEqual(logger._log, ["foo", "bar", "baz", "qux"]) + + +class TestComponent(unittest.TestCase): + def do_split(self, string, name, options): + n, o = Component._split_component_and_options(string) + self.assertEqual(name, n) + self.assertEqual(options, o) + + def test_component_split_component_and_options(self): + self.do_split("component", "component", {}) + self.do_split("trailingspace ", "trailingspace", {}) + self.do_split(" leadingspace", "leadingspace", {}) + self.do_split(" trim ", "trim", {}) + self.do_split(' trim key="value"', "trim", {"key": "value"}) + self.do_split(' trim empty=""', "trim", {"empty": ""}) + self.do_split(' trim space=" "', "trim", {"space": " "}) + self.do_split( + 'component key="value" key2="second" ', + "component", + {"key": "value", "key2": "second"}, + ) + self.do_split( + 'trim key=" value with spaces " key2="spaces again"', + "trim", + {"key": " value with spaces ", "key2": "spaces again"}, + ) + + def do_split_error(self, string): + self.assertRaises(ValueError, Component._split_component_and_options, string) + + def test_component_split_component_and_options_errors(self): + self.do_split_error('"component') + self.do_split_error('comp"onent') + self.do_split_error('component"') + self.do_split_error('"component"') + self.do_split_error("=component") + self.do_split_error("comp=onent") + self.do_split_error("component=") + self.do_split_error('key="val"') + self.do_split_error("component key=") + self.do_split_error('component key="val') + self.do_split_error('component key=val"') + self.do_split_error('component key="val" x') + self.do_split_error('component x key="val"') + self.do_split_error('component key1="val" x key2="val"') + + def do_from_string(self, string, name, destdir=""): + component = Component.from_string(string) + self.assertEqual(name, component.name) + self.assertEqual(destdir, component.destdir) + + def test_component_from_string(self): + self.do_from_string("component", "component") + self.do_from_string("component-with-hyphen", "component-with-hyphen") + self.do_from_string('component destdir="foo/bar"', "component", "foo/bar") + self.do_from_string('component destdir="bar spc"', "component", "bar spc") + self.assertRaises(ErrorMessage, Component.from_string, "") + self.assertRaises(ErrorMessage, Component.from_string, "component novalue=") + self.assertRaises( + ErrorMessage, Component.from_string, "component badoption=badvalue" + ) + + +if __name__ == "__main__": + mozunit.main() diff --git a/python/mozbuild/mozpack/test/test_packager_formats.py b/python/mozbuild/mozpack/test/test_packager_formats.py new file mode 100644 index 0000000000..b09971a102 --- /dev/null +++ b/python/mozbuild/mozpack/test/test_packager_formats.py @@ -0,0 +1,537 @@ +# 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/. + +import unittest +from itertools import chain + +import mozunit +import six + +import mozpack.path as mozpath +from mozpack.chrome.manifest import ( + ManifestBinaryComponent, + ManifestComponent, + ManifestContent, + ManifestLocale, + ManifestResource, + ManifestSkin, +) +from mozpack.copier import FileRegistry +from mozpack.errors import ErrorMessage +from mozpack.files import GeneratedFile, ManifestFile +from mozpack.packager.formats import FlatFormatter, JarFormatter, OmniJarFormatter +from mozpack.test.test_files import bar_xpt, foo2_xpt, foo_xpt +from test_errors import TestErrors + +CONTENTS = { + "bases": { + # base_path: is_addon? + "": False, + "app": False, + "addon0": "unpacked", + "addon1": True, + "app/chrome/addons/addon2": True, + }, + "manifests": [ + ManifestContent("chrome/f", "oo", "oo/"), + ManifestContent("chrome/f", "bar", "oo/bar/"), + ManifestResource("chrome/f", "foo", "resource://bar/"), + ManifestBinaryComponent("components", "foo.so"), + ManifestContent("app/chrome", "content", "foo/"), + ManifestComponent("app/components", "{foo-id}", "foo.js"), + ManifestContent("addon0/chrome", "addon0", "foo/bar/"), + ManifestContent("addon1/chrome", "addon1", "foo/bar/"), + ManifestContent("app/chrome/addons/addon2/chrome", "addon2", "foo/bar/"), + ], + "files": { + "chrome/f/oo/bar/baz": GeneratedFile(b"foobarbaz"), + "chrome/f/oo/baz": GeneratedFile(b"foobaz"), + "chrome/f/oo/qux": GeneratedFile(b"fooqux"), + "components/foo.so": GeneratedFile(b"foo.so"), + "components/foo.xpt": foo_xpt, + "components/bar.xpt": bar_xpt, + "foo": GeneratedFile(b"foo"), + "app/chrome/foo/foo": GeneratedFile(b"appfoo"), + "app/components/foo.js": GeneratedFile(b"foo.js"), + "addon0/chrome/foo/bar/baz": GeneratedFile(b"foobarbaz"), + "addon0/components/foo.xpt": foo2_xpt, + "addon0/components/bar.xpt": bar_xpt, + "addon1/chrome/foo/bar/baz": GeneratedFile(b"foobarbaz"), + "addon1/components/foo.xpt": foo2_xpt, + "addon1/components/bar.xpt": bar_xpt, + "app/chrome/addons/addon2/chrome/foo/bar/baz": GeneratedFile(b"foobarbaz"), + "app/chrome/addons/addon2/components/foo.xpt": foo2_xpt, + "app/chrome/addons/addon2/components/bar.xpt": bar_xpt, + }, +} + +FILES = CONTENTS["files"] + +RESULT_FLAT = { + "chrome.manifest": [ + "manifest chrome/chrome.manifest", + "manifest components/components.manifest", + ], + "chrome/chrome.manifest": [ + "manifest f/f.manifest", + ], + "chrome/f/f.manifest": [ + "content oo oo/", + "content bar oo/bar/", + "resource foo resource://bar/", + ], + "chrome/f/oo/bar/baz": FILES["chrome/f/oo/bar/baz"], + "chrome/f/oo/baz": FILES["chrome/f/oo/baz"], + "chrome/f/oo/qux": FILES["chrome/f/oo/qux"], + "components/components.manifest": [ + "binary-component foo.so", + "interfaces bar.xpt", + "interfaces foo.xpt", + ], + "components/foo.so": FILES["components/foo.so"], + "components/foo.xpt": foo_xpt, + "components/bar.xpt": bar_xpt, + "foo": FILES["foo"], + "app/chrome.manifest": [ + "manifest chrome/chrome.manifest", + "manifest components/components.manifest", + ], + "app/chrome/chrome.manifest": [ + "content content foo/", + ], + "app/chrome/foo/foo": FILES["app/chrome/foo/foo"], + "app/components/components.manifest": [ + "component {foo-id} foo.js", + ], + "app/components/foo.js": FILES["app/components/foo.js"], +} + +for addon in ("addon0", "addon1", "app/chrome/addons/addon2"): + RESULT_FLAT.update( + { + mozpath.join(addon, p): f + for p, f in six.iteritems( + { + "chrome.manifest": [ + "manifest chrome/chrome.manifest", + "manifest components/components.manifest", + ], + "chrome/chrome.manifest": [ + "content %s foo/bar/" % mozpath.basename(addon), + ], + "chrome/foo/bar/baz": FILES[ + mozpath.join(addon, "chrome/foo/bar/baz") + ], + "components/components.manifest": [ + "interfaces bar.xpt", + "interfaces foo.xpt", + ], + "components/bar.xpt": bar_xpt, + "components/foo.xpt": foo2_xpt, + } + ) + } + ) + +RESULT_JAR = { + p: RESULT_FLAT[p] + for p in ( + "chrome.manifest", + "chrome/chrome.manifest", + "components/components.manifest", + "components/foo.so", + "components/foo.xpt", + "components/bar.xpt", + "foo", + "app/chrome.manifest", + "app/components/components.manifest", + "app/components/foo.js", + "addon0/chrome.manifest", + "addon0/components/components.manifest", + "addon0/components/foo.xpt", + "addon0/components/bar.xpt", + ) +} + +RESULT_JAR.update( + { + "chrome/f/f.manifest": [ + "content oo jar:oo.jar!/", + "content bar jar:oo.jar!/bar/", + "resource foo resource://bar/", + ], + "chrome/f/oo.jar": { + "bar/baz": FILES["chrome/f/oo/bar/baz"], + "baz": FILES["chrome/f/oo/baz"], + "qux": FILES["chrome/f/oo/qux"], + }, + "app/chrome/chrome.manifest": [ + "content content jar:foo.jar!/", + ], + "app/chrome/foo.jar": { + "foo": FILES["app/chrome/foo/foo"], + }, + "addon0/chrome/chrome.manifest": [ + "content addon0 jar:foo.jar!/bar/", + ], + "addon0/chrome/foo.jar": { + "bar/baz": FILES["addon0/chrome/foo/bar/baz"], + }, + "addon1.xpi": { + mozpath.relpath(p, "addon1"): f + for p, f in six.iteritems(RESULT_FLAT) + if p.startswith("addon1/") + }, + "app/chrome/addons/addon2.xpi": { + mozpath.relpath(p, "app/chrome/addons/addon2"): f + for p, f in six.iteritems(RESULT_FLAT) + if p.startswith("app/chrome/addons/addon2/") + }, + } +) + +RESULT_OMNIJAR = { + p: RESULT_FLAT[p] + for p in ( + "components/foo.so", + "foo", + ) +} + +RESULT_OMNIJAR.update({p: RESULT_JAR[p] for p in RESULT_JAR if p.startswith("addon")}) + +RESULT_OMNIJAR.update( + { + "omni.foo": { + "components/components.manifest": [ + "interfaces bar.xpt", + "interfaces foo.xpt", + ], + }, + "chrome.manifest": [ + "manifest components/components.manifest", + ], + "components/components.manifest": [ + "binary-component foo.so", + ], + "app/omni.foo": { + p: RESULT_FLAT["app/" + p] + for p in chain( + ( + "chrome.manifest", + "chrome/chrome.manifest", + "chrome/foo/foo", + "components/components.manifest", + "components/foo.js", + ), + ( + mozpath.relpath(p, "app") + for p in six.iterkeys(RESULT_FLAT) + if p.startswith("app/chrome/addons/addon2/") + ), + ) + }, + } +) + +RESULT_OMNIJAR["omni.foo"].update( + { + p: RESULT_FLAT[p] + for p in ( + "chrome.manifest", + "chrome/chrome.manifest", + "chrome/f/f.manifest", + "chrome/f/oo/bar/baz", + "chrome/f/oo/baz", + "chrome/f/oo/qux", + "components/foo.xpt", + "components/bar.xpt", + ) + } +) + +RESULT_OMNIJAR_WITH_SUBPATH = { + k.replace("omni.foo", "bar/omni.foo"): v for k, v in RESULT_OMNIJAR.items() +} + +CONTENTS_WITH_BASE = { + "bases": { + mozpath.join("base/root", b) if b else "base/root": a + for b, a in six.iteritems(CONTENTS["bases"]) + }, + "manifests": [ + m.move(mozpath.join("base/root", m.base)) for m in CONTENTS["manifests"] + ], + "files": { + mozpath.join("base/root", p): f for p, f in six.iteritems(CONTENTS["files"]) + }, +} + +EXTRA_CONTENTS = { + "extra/file": GeneratedFile(b"extra file"), +} + +CONTENTS_WITH_BASE["files"].update(EXTRA_CONTENTS) + + +def result_with_base(results): + result = {mozpath.join("base/root", p): v for p, v in six.iteritems(results)} + result.update(EXTRA_CONTENTS) + return result + + +RESULT_FLAT_WITH_BASE = result_with_base(RESULT_FLAT) +RESULT_JAR_WITH_BASE = result_with_base(RESULT_JAR) +RESULT_OMNIJAR_WITH_BASE = result_with_base(RESULT_OMNIJAR) + + +def fill_formatter(formatter, contents): + for base, is_addon in sorted(contents["bases"].items()): + formatter.add_base(base, is_addon) + + for manifest in contents["manifests"]: + formatter.add_manifest(manifest) + + for k, v in sorted(six.iteritems(contents["files"])): + if k.endswith(".xpt"): + formatter.add_interfaces(k, v) + else: + formatter.add(k, v) + + +def get_contents(registry, read_all=False, mode="rt"): + result = {} + for k, v in registry: + if isinstance(v, FileRegistry): + result[k] = get_contents(v) + elif isinstance(v, ManifestFile) or read_all: + if "b" in mode: + result[k] = v.open().read() + else: + result[k] = six.ensure_text(v.open().read()).splitlines() + else: + result[k] = v + return result + + +class TestFormatters(TestErrors, unittest.TestCase): + maxDiff = None + + def test_bases(self): + formatter = FlatFormatter(FileRegistry()) + formatter.add_base("") + formatter.add_base("addon0", addon=True) + formatter.add_base("browser") + self.assertEqual(formatter._get_base("platform.ini"), ("", "platform.ini")) + self.assertEqual( + formatter._get_base("browser/application.ini"), + ("browser", "application.ini"), + ) + self.assertEqual( + formatter._get_base("addon0/install.rdf"), ("addon0", "install.rdf") + ) + + def do_test_contents(self, formatter, contents): + for f in contents["files"]: + # .xpt files are merged, so skip them. + if not f.endswith(".xpt"): + self.assertTrue(formatter.contains(f)) + + def test_flat_formatter(self): + registry = FileRegistry() + formatter = FlatFormatter(registry) + + fill_formatter(formatter, CONTENTS) + self.assertEqual(get_contents(registry), RESULT_FLAT) + self.do_test_contents(formatter, CONTENTS) + + def test_jar_formatter(self): + registry = FileRegistry() + formatter = JarFormatter(registry) + + fill_formatter(formatter, CONTENTS) + self.assertEqual(get_contents(registry), RESULT_JAR) + self.do_test_contents(formatter, CONTENTS) + + def test_omnijar_formatter(self): + registry = FileRegistry() + formatter = OmniJarFormatter(registry, "omni.foo") + + fill_formatter(formatter, CONTENTS) + self.assertEqual(get_contents(registry), RESULT_OMNIJAR) + self.do_test_contents(formatter, CONTENTS) + + def test_flat_formatter_with_base(self): + registry = FileRegistry() + formatter = FlatFormatter(registry) + + fill_formatter(formatter, CONTENTS_WITH_BASE) + self.assertEqual(get_contents(registry), RESULT_FLAT_WITH_BASE) + self.do_test_contents(formatter, CONTENTS_WITH_BASE) + + def test_jar_formatter_with_base(self): + registry = FileRegistry() + formatter = JarFormatter(registry) + + fill_formatter(formatter, CONTENTS_WITH_BASE) + self.assertEqual(get_contents(registry), RESULT_JAR_WITH_BASE) + self.do_test_contents(formatter, CONTENTS_WITH_BASE) + + def test_omnijar_formatter_with_base(self): + registry = FileRegistry() + formatter = OmniJarFormatter(registry, "omni.foo") + + fill_formatter(formatter, CONTENTS_WITH_BASE) + self.assertEqual(get_contents(registry), RESULT_OMNIJAR_WITH_BASE) + self.do_test_contents(formatter, CONTENTS_WITH_BASE) + + def test_omnijar_formatter_with_subpath(self): + registry = FileRegistry() + formatter = OmniJarFormatter(registry, "bar/omni.foo") + + fill_formatter(formatter, CONTENTS) + self.assertEqual(get_contents(registry), RESULT_OMNIJAR_WITH_SUBPATH) + self.do_test_contents(formatter, CONTENTS) + + def test_omnijar_is_resource(self): + def is_resource(base, path): + registry = FileRegistry() + f = OmniJarFormatter( + registry, + "omni.foo", + non_resources=[ + "defaults/messenger/mailViews.dat", + "defaults/foo/*", + "*/dummy", + ], + ) + f.add_base("") + f.add_base("app") + f.add(mozpath.join(base, path), GeneratedFile(b"")) + if f.copier.contains(mozpath.join(base, path)): + return False + self.assertTrue(f.copier.contains(mozpath.join(base, "omni.foo"))) + self.assertTrue(f.copier[mozpath.join(base, "omni.foo")].contains(path)) + return True + + for base in ["", "app/"]: + self.assertTrue(is_resource(base, "chrome")) + self.assertTrue(is_resource(base, "chrome/foo/bar/baz.properties")) + self.assertFalse(is_resource(base, "chrome/icons/foo.png")) + self.assertTrue(is_resource(base, "components/foo.js")) + self.assertFalse(is_resource(base, "components/foo.so")) + self.assertTrue(is_resource(base, "res/foo.css")) + self.assertFalse(is_resource(base, "res/cursors/foo.png")) + self.assertFalse(is_resource(base, "res/MainMenu.nib/foo")) + self.assertTrue(is_resource(base, "defaults/pref/foo.js")) + self.assertFalse(is_resource(base, "defaults/pref/channel-prefs.js")) + self.assertTrue(is_resource(base, "defaults/preferences/foo.js")) + self.assertFalse(is_resource(base, "defaults/preferences/channel-prefs.js")) + self.assertTrue(is_resource(base, "modules/foo.jsm")) + self.assertTrue(is_resource(base, "greprefs.js")) + self.assertTrue(is_resource(base, "hyphenation/foo")) + self.assertTrue(is_resource(base, "update.locale")) + self.assertFalse(is_resource(base, "foo")) + self.assertFalse(is_resource(base, "foo/bar/greprefs.js")) + self.assertTrue(is_resource(base, "defaults/messenger/foo.dat")) + self.assertFalse(is_resource(base, "defaults/messenger/mailViews.dat")) + self.assertTrue(is_resource(base, "defaults/pref/foo.js")) + self.assertFalse(is_resource(base, "defaults/foo/bar.dat")) + self.assertFalse(is_resource(base, "defaults/foo/bar/baz.dat")) + self.assertTrue(is_resource(base, "chrome/foo/bar/baz/dummy_")) + self.assertFalse(is_resource(base, "chrome/foo/bar/baz/dummy")) + self.assertTrue(is_resource(base, "chrome/foo/bar/dummy_")) + self.assertFalse(is_resource(base, "chrome/foo/bar/dummy")) + + def test_chrome_override(self): + registry = FileRegistry() + f = FlatFormatter(registry) + f.add_base("") + f.add_manifest(ManifestContent("chrome", "foo", "foo/unix")) + # A more specific entry for a given chrome name can override a more + # generic one. + f.add_manifest(ManifestContent("chrome", "foo", "foo/win", "os=WINNT")) + f.add_manifest(ManifestContent("chrome", "foo", "foo/osx", "os=Darwin")) + + # Chrome with the same name overrides the previous registration. + with self.assertRaises(ErrorMessage) as e: + f.add_manifest(ManifestContent("chrome", "foo", "foo/")) + + self.assertEqual( + str(e.exception), + 'error: "content foo foo/" overrides ' '"content foo foo/unix"', + ) + + # Chrome with the same name and same flags overrides the previous + # registration. + with self.assertRaises(ErrorMessage) as e: + f.add_manifest(ManifestContent("chrome", "foo", "foo/", "os=WINNT")) + + self.assertEqual( + str(e.exception), + 'error: "content foo foo/ os=WINNT" overrides ' + '"content foo foo/win os=WINNT"', + ) + + # We may start with the more specific entry first + f.add_manifest(ManifestContent("chrome", "bar", "bar/win", "os=WINNT")) + # Then adding a more generic one overrides it. + with self.assertRaises(ErrorMessage) as e: + f.add_manifest(ManifestContent("chrome", "bar", "bar/unix")) + + self.assertEqual( + str(e.exception), + 'error: "content bar bar/unix" overrides ' '"content bar bar/win os=WINNT"', + ) + + # Adding something more specific still works. + f.add_manifest( + ManifestContent("chrome", "bar", "bar/win", "os=WINNT osversion>=7.0") + ) + + # Variations of skin/locales are allowed. + f.add_manifest( + ManifestSkin("chrome", "foo", "classic/1.0", "foo/skin/classic/") + ) + f.add_manifest(ManifestSkin("chrome", "foo", "modern/1.0", "foo/skin/modern/")) + + f.add_manifest(ManifestLocale("chrome", "foo", "en-US", "foo/locale/en-US/")) + f.add_manifest(ManifestLocale("chrome", "foo", "ja-JP", "foo/locale/ja-JP/")) + + # But same-skin/locale still error out. + with self.assertRaises(ErrorMessage) as e: + f.add_manifest( + ManifestSkin("chrome", "foo", "classic/1.0", "foo/skin/classic/foo") + ) + + self.assertEqual( + str(e.exception), + 'error: "skin foo classic/1.0 foo/skin/classic/foo" overrides ' + '"skin foo classic/1.0 foo/skin/classic/"', + ) + + with self.assertRaises(ErrorMessage) as e: + f.add_manifest( + ManifestLocale("chrome", "foo", "en-US", "foo/locale/en-US/foo") + ) + + self.assertEqual( + str(e.exception), + 'error: "locale foo en-US foo/locale/en-US/foo" overrides ' + '"locale foo en-US foo/locale/en-US/"', + ) + + # Duplicating existing manifest entries is not an error. + f.add_manifest(ManifestContent("chrome", "foo", "foo/unix")) + + self.assertEqual( + self.get_output(), + [ + 'warning: "content foo foo/unix" is duplicated. Skipping.', + ], + ) + + +if __name__ == "__main__": + mozunit.main() diff --git a/python/mozbuild/mozpack/test/test_packager_l10n.py b/python/mozbuild/mozpack/test/test_packager_l10n.py new file mode 100644 index 0000000000..0714ae3252 --- /dev/null +++ b/python/mozbuild/mozpack/test/test_packager_l10n.py @@ -0,0 +1,153 @@ +# 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/. + +import unittest + +import mozunit +import six + +from mozpack.chrome.manifest import Manifest, ManifestContent, ManifestLocale +from mozpack.copier import FileRegistry +from mozpack.files import GeneratedFile, ManifestFile +from mozpack.packager import l10n +from test_packager import MockFinder + + +class TestL10NRepack(unittest.TestCase): + def test_l10n_repack(self): + foo = GeneratedFile(b"foo") + foobar = GeneratedFile(b"foobar") + qux = GeneratedFile(b"qux") + bar = GeneratedFile(b"bar") + baz = GeneratedFile(b"baz") + dict_aa = GeneratedFile(b"dict_aa") + dict_bb = GeneratedFile(b"dict_bb") + dict_cc = GeneratedFile(b"dict_cc") + barbaz = GeneratedFile(b"barbaz") + lst = GeneratedFile(b"foo\nbar") + app_finder = MockFinder( + { + "bar/foo": foo, + "chrome/foo/foobar": foobar, + "chrome/qux/qux.properties": qux, + "chrome/qux/baz/baz.properties": baz, + "chrome/chrome.manifest": ManifestFile( + "chrome", + [ + ManifestContent("chrome", "foo", "foo/"), + ManifestLocale("chrome", "qux", "en-US", "qux/"), + ], + ), + "chrome.manifest": ManifestFile( + "", [Manifest("", "chrome/chrome.manifest")] + ), + "dict/aa": dict_aa, + "app/chrome/bar/barbaz.dtd": barbaz, + "app/chrome/chrome.manifest": ManifestFile( + "app/chrome", [ManifestLocale("app/chrome", "bar", "en-US", "bar/")] + ), + "app/chrome.manifest": ManifestFile( + "app", [Manifest("app", "chrome/chrome.manifest")] + ), + "app/dict/bb": dict_bb, + "app/dict/cc": dict_cc, + "app/chrome/bar/search/foo.xml": foo, + "app/chrome/bar/search/bar.xml": bar, + "app/chrome/bar/search/lst.txt": lst, + "META-INF/foo": foo, # Stripped. + "inner/META-INF/foo": foo, # Not stripped. + "app/META-INF/foo": foo, # Stripped. + "app/inner/META-INF/foo": foo, # Not stripped. + } + ) + app_finder.jarlogs = {} + app_finder.base = "app" + foo_l10n = GeneratedFile(b"foo_l10n") + qux_l10n = GeneratedFile(b"qux_l10n") + baz_l10n = GeneratedFile(b"baz_l10n") + barbaz_l10n = GeneratedFile(b"barbaz_l10n") + lst_l10n = GeneratedFile(b"foo\nqux") + l10n_finder = MockFinder( + { + "chrome/qux-l10n/qux.properties": qux_l10n, + "chrome/qux-l10n/baz/baz.properties": baz_l10n, + "chrome/chrome.manifest": ManifestFile( + "chrome", + [ + ManifestLocale("chrome", "qux", "x-test", "qux-l10n/"), + ], + ), + "chrome.manifest": ManifestFile( + "", [Manifest("", "chrome/chrome.manifest")] + ), + "dict/bb": dict_bb, + "dict/cc": dict_cc, + "app/chrome/bar-l10n/barbaz.dtd": barbaz_l10n, + "app/chrome/chrome.manifest": ManifestFile( + "app/chrome", + [ManifestLocale("app/chrome", "bar", "x-test", "bar-l10n/")], + ), + "app/chrome.manifest": ManifestFile( + "app", [Manifest("app", "chrome/chrome.manifest")] + ), + "app/dict/aa": dict_aa, + "app/chrome/bar-l10n/search/foo.xml": foo_l10n, + "app/chrome/bar-l10n/search/qux.xml": qux_l10n, + "app/chrome/bar-l10n/search/lst.txt": lst_l10n, + } + ) + l10n_finder.base = "l10n" + copier = FileRegistry() + formatter = l10n.FlatFormatter(copier) + + l10n._repack( + app_finder, + l10n_finder, + copier, + formatter, + ["dict", "chrome/**/search/*.xml"], + ) + self.maxDiff = None + + repacked = { + "bar/foo": foo, + "chrome/foo/foobar": foobar, + "chrome/qux-l10n/qux.properties": qux_l10n, + "chrome/qux-l10n/baz/baz.properties": baz_l10n, + "chrome/chrome.manifest": ManifestFile( + "chrome", + [ + ManifestContent("chrome", "foo", "foo/"), + ManifestLocale("chrome", "qux", "x-test", "qux-l10n/"), + ], + ), + "chrome.manifest": ManifestFile( + "", [Manifest("", "chrome/chrome.manifest")] + ), + "dict/bb": dict_bb, + "dict/cc": dict_cc, + "app/chrome/bar-l10n/barbaz.dtd": barbaz_l10n, + "app/chrome/chrome.manifest": ManifestFile( + "app/chrome", + [ManifestLocale("app/chrome", "bar", "x-test", "bar-l10n/")], + ), + "app/chrome.manifest": ManifestFile( + "app", [Manifest("app", "chrome/chrome.manifest")] + ), + "app/dict/aa": dict_aa, + "app/chrome/bar-l10n/search/foo.xml": foo_l10n, + "app/chrome/bar-l10n/search/qux.xml": qux_l10n, + "app/chrome/bar-l10n/search/lst.txt": lst_l10n, + "inner/META-INF/foo": foo, + "app/inner/META-INF/foo": foo, + } + + self.assertEqual( + dict((p, f.open().read()) for p, f in copier), + dict((p, f.open().read()) for p, f in six.iteritems(repacked)), + ) + + +if __name__ == "__main__": + mozunit.main() diff --git a/python/mozbuild/mozpack/test/test_packager_unpack.py b/python/mozbuild/mozpack/test/test_packager_unpack.py new file mode 100644 index 0000000000..57a2d71eda --- /dev/null +++ b/python/mozbuild/mozpack/test/test_packager_unpack.py @@ -0,0 +1,67 @@ +# 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/. + +import mozunit + +from mozpack.copier import FileCopier, FileRegistry +from mozpack.packager.formats import FlatFormatter, JarFormatter, OmniJarFormatter +from mozpack.packager.unpack import unpack_to_registry +from mozpack.test.test_files import TestWithTmpDir +from mozpack.test.test_packager_formats import CONTENTS, fill_formatter, get_contents + + +class TestUnpack(TestWithTmpDir): + maxDiff = None + + @staticmethod + def _get_copier(cls): + copier = FileCopier() + formatter = cls(copier) + fill_formatter(formatter, CONTENTS) + return copier + + @classmethod + def setUpClass(cls): + cls.contents = get_contents( + cls._get_copier(FlatFormatter), read_all=True, mode="rb" + ) + + def _unpack_test(self, cls): + # Format a package with the given formatter class + copier = self._get_copier(cls) + copier.copy(self.tmpdir) + + # Unpack that package. Its content is expected to match that of a Flat + # formatted package. + registry = FileRegistry() + unpack_to_registry(self.tmpdir, registry, getattr(cls, "OMNIJAR_NAME", None)) + self.assertEqual( + get_contents(registry, read_all=True, mode="rb"), self.contents + ) + + def test_flat_unpack(self): + self._unpack_test(FlatFormatter) + + def test_jar_unpack(self): + self._unpack_test(JarFormatter) + + @staticmethod + def _omni_foo_formatter(name): + class OmniFooFormatter(OmniJarFormatter): + OMNIJAR_NAME = name + + def __init__(self, registry): + super(OmniFooFormatter, self).__init__(registry, name) + + return OmniFooFormatter + + def test_omnijar_unpack(self): + self._unpack_test(self._omni_foo_formatter("omni.foo")) + + def test_omnijar_subpath_unpack(self): + self._unpack_test(self._omni_foo_formatter("bar/omni.foo")) + + +if __name__ == "__main__": + mozunit.main() diff --git a/python/mozbuild/mozpack/test/test_path.py b/python/mozbuild/mozpack/test/test_path.py new file mode 100644 index 0000000000..6c7aeb5400 --- /dev/null +++ b/python/mozbuild/mozpack/test/test_path.py @@ -0,0 +1,152 @@ +# 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/. + +import os +import unittest + +import mozunit + +from mozpack.path import ( + basedir, + basename, + commonprefix, + dirname, + join, + match, + normpath, + rebase, + relpath, + split, + splitext, +) + + +class TestPath(unittest.TestCase): + SEP = os.sep + + def test_relpath(self): + self.assertEqual(relpath("foo", "foo"), "") + self.assertEqual(relpath(self.SEP.join(("foo", "bar")), "foo/bar"), "") + self.assertEqual(relpath(self.SEP.join(("foo", "bar")), "foo"), "bar") + self.assertEqual( + relpath(self.SEP.join(("foo", "bar", "baz")), "foo"), "bar/baz" + ) + self.assertEqual(relpath(self.SEP.join(("foo", "bar")), "foo/bar/baz"), "..") + self.assertEqual(relpath(self.SEP.join(("foo", "bar")), "foo/baz"), "../bar") + self.assertEqual(relpath("foo/", "foo"), "") + self.assertEqual(relpath("foo/bar/", "foo"), "bar") + + def test_join(self): + self.assertEqual(join("foo", "bar", "baz"), "foo/bar/baz") + self.assertEqual(join("foo", "", "bar"), "foo/bar") + self.assertEqual(join("", "foo", "bar"), "foo/bar") + self.assertEqual(join("", "foo", "/bar"), "/bar") + + def test_normpath(self): + self.assertEqual( + normpath(self.SEP.join(("foo", "bar", "baz", "..", "qux"))), "foo/bar/qux" + ) + + def test_dirname(self): + self.assertEqual(dirname("foo/bar/baz"), "foo/bar") + self.assertEqual(dirname("foo/bar"), "foo") + self.assertEqual(dirname("foo"), "") + self.assertEqual(dirname("foo/bar/"), "foo/bar") + + def test_commonprefix(self): + self.assertEqual( + commonprefix( + [self.SEP.join(("foo", "bar", "baz")), "foo/qux", "foo/baz/qux"] + ), + "foo/", + ) + self.assertEqual( + commonprefix([self.SEP.join(("foo", "bar", "baz")), "foo/qux", "baz/qux"]), + "", + ) + + def test_basename(self): + self.assertEqual(basename("foo/bar/baz"), "baz") + self.assertEqual(basename("foo/bar"), "bar") + self.assertEqual(basename("foo"), "foo") + self.assertEqual(basename("foo/bar/"), "") + + def test_split(self): + self.assertEqual( + split(self.SEP.join(("foo", "bar", "baz"))), ["foo", "bar", "baz"] + ) + + def test_splitext(self): + self.assertEqual( + splitext(self.SEP.join(("foo", "bar", "baz.qux"))), ("foo/bar/baz", ".qux") + ) + + def test_basedir(self): + foobarbaz = self.SEP.join(("foo", "bar", "baz")) + self.assertEqual(basedir(foobarbaz, ["foo", "bar", "baz"]), "foo") + self.assertEqual(basedir(foobarbaz, ["foo", "foo/bar", "baz"]), "foo/bar") + self.assertEqual(basedir(foobarbaz, ["foo/bar", "foo", "baz"]), "foo/bar") + self.assertEqual(basedir(foobarbaz, ["foo", "bar", ""]), "foo") + self.assertEqual(basedir(foobarbaz, ["bar", "baz", ""]), "") + + def test_match(self): + self.assertTrue(match("foo", "")) + self.assertTrue(match("foo/bar/baz.qux", "foo/bar")) + self.assertTrue(match("foo/bar/baz.qux", "foo")) + self.assertTrue(match("foo", "*")) + self.assertTrue(match("foo/bar/baz.qux", "foo/bar/*")) + self.assertTrue(match("foo/bar/baz.qux", "foo/bar/*")) + self.assertTrue(match("foo/bar/baz.qux", "foo/bar/*")) + self.assertTrue(match("foo/bar/baz.qux", "foo/bar/*")) + self.assertTrue(match("foo/bar/baz.qux", "foo/*/baz.qux")) + self.assertTrue(match("foo/bar/baz.qux", "*/bar/baz.qux")) + self.assertTrue(match("foo/bar/baz.qux", "*/*/baz.qux")) + self.assertTrue(match("foo/bar/baz.qux", "*/*/*")) + self.assertTrue(match("foo/bar/baz.qux", "foo/*/*")) + self.assertTrue(match("foo/bar/baz.qux", "foo/*/*.qux")) + self.assertTrue(match("foo/bar/baz.qux", "foo/b*/*z.qux")) + self.assertTrue(match("foo/bar/baz.qux", "foo/b*r/ba*z.qux")) + self.assertFalse(match("foo/bar/baz.qux", "foo/b*z/ba*r.qux")) + self.assertTrue(match("foo/bar/baz.qux", "**")) + self.assertTrue(match("foo/bar/baz.qux", "**/baz.qux")) + self.assertTrue(match("foo/bar/baz.qux", "**/bar/baz.qux")) + self.assertTrue(match("foo/bar/baz.qux", "foo/**/baz.qux")) + self.assertTrue(match("foo/bar/baz.qux", "foo/**/*.qux")) + self.assertTrue(match("foo/bar/baz.qux", "**/foo/bar/baz.qux")) + self.assertTrue(match("foo/bar/baz.qux", "foo/**/bar/baz.qux")) + self.assertTrue(match("foo/bar/baz.qux", "foo/**/bar/*.qux")) + self.assertTrue(match("foo/bar/baz.qux", "foo/**/*.qux")) + self.assertTrue(match("foo/bar/baz.qux", "**/*.qux")) + self.assertFalse(match("foo/bar/baz.qux", "**.qux")) + self.assertFalse(match("foo/bar", "foo/*/bar")) + self.assertTrue(match("foo/bar/baz.qux", "foo/**/bar/**")) + self.assertFalse(match("foo/nobar/baz.qux", "foo/**/bar/**")) + self.assertTrue(match("foo/bar", "foo/**/bar/**")) + + def test_rebase(self): + self.assertEqual(rebase("foo", "foo/bar", "bar/baz"), "baz") + self.assertEqual(rebase("foo", "foo", "bar/baz"), "bar/baz") + self.assertEqual(rebase("foo/bar", "foo", "baz"), "bar/baz") + + +if os.altsep: + + class TestAltPath(TestPath): + SEP = os.altsep + + class TestReverseAltPath(TestPath): + def setUp(self): + sep = os.sep + os.sep = os.altsep + os.altsep = sep + + def tearDown(self): + self.setUp() + + class TestAltReverseAltPath(TestReverseAltPath): + SEP = os.altsep + + +if __name__ == "__main__": + mozunit.main() diff --git a/python/mozbuild/mozpack/test/test_pkg.py b/python/mozbuild/mozpack/test/test_pkg.py new file mode 100644 index 0000000000..f1febbbae0 --- /dev/null +++ b/python/mozbuild/mozpack/test/test_pkg.py @@ -0,0 +1,138 @@ +# 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 pathlib import Path +from string import Template +from unittest.mock import patch + +import mozunit + +import mozpack.pkg +from mozpack.pkg import ( + create_bom, + create_payload, + create_pkg, + get_app_info_plist, + get_apple_template, + get_relative_glob_list, + save_text_file, + xar_package_folder, +) +from mozpack.test.test_files import TestWithTmpDir + + +class TestPkg(TestWithTmpDir): + maxDiff = None + + class MockSubprocessRun: + stderr = "" + stdout = "" + returncode = 0 + + def __init__(self, returncode=0): + self.returncode = returncode + + def _mk_test_file(self, name, mode=0o777): + tool = Path(self.tmpdir) / f"{name}" + tool.touch() + tool.chmod(mode) + return tool + + def test_get_apple_template(self): + tmpl = get_apple_template("Distribution.template") + assert type(tmpl) == Template + + def test_get_apple_template_not_file(self): + with self.assertRaises(Exception): + get_apple_template("tmpl-should-not-exist") + + def test_save_text_file(self): + content = "Hello" + destination = Path(self.tmpdir) / "test_save_text_file" + save_text_file(content, destination) + with destination.open("r") as file: + assert content == file.read() + + def test_get_app_info_plist(self): + app_path = Path(self.tmpdir) / "app" + (app_path / "Contents").mkdir(parents=True) + (app_path / "Contents/Info.plist").touch() + data = {"foo": "bar"} + with patch.object(mozpack.pkg.plistlib, "load", lambda x: data): + assert data == get_app_info_plist(app_path) + + def test_get_app_info_plist_not_file(self): + app_path = Path(self.tmpdir) / "app-does-not-exist" + with self.assertRaises(Exception): + get_app_info_plist(app_path) + + def _mock_payload(self, returncode): + def _mock_run(*args, **kwargs): + return self.MockSubprocessRun(returncode) + + return _mock_run + + def test_create_payload(self): + destination = Path(self.tmpdir) / "mockPayload" + with patch.object(mozpack.pkg.subprocess, "run", self._mock_payload(0)): + create_payload(destination, Path(self.tmpdir), "cpio") + + def test_create_bom(self): + bom_path = Path(self.tmpdir) / "Bom" + bom_path.touch() + root_path = Path(self.tmpdir) + tool_path = Path(self.tmpdir) / "not-really-used-during-test" + with patch.object(mozpack.pkg.subprocess, "check_call", lambda *x: None): + create_bom(bom_path, root_path, tool_path) + + def get_relative_glob_list(self): + source = Path(self.tmpdir) + (source / "testfile").touch() + glob = "*" + assert len(get_relative_glob_list(source, glob)) == 1 + + def test_xar_package_folder(self): + source = Path(self.tmpdir) + dest = source / "fakedestination" + dest.touch() + tool = source / "faketool" + with patch.object(mozpack.pkg.subprocess, "check_call", lambda *x, **y: None): + xar_package_folder(source, dest, tool) + + def test_xar_package_folder_not_absolute(self): + source = Path("./some/relative/path") + dest = Path("./some/other/relative/path") + tool = source / "faketool" + with patch.object(mozpack.pkg.subprocess, "check_call", lambda: None): + with self.assertRaises(Exception): + xar_package_folder(source, dest, tool) + + def test_create_pkg(self): + def noop(*x, **y): + pass + + def mock_get_app_info_plist(*args): + return {"CFBundleShortVersionString": "1.0.0"} + + def mock_get_apple_template(*args): + return Template("fake template") + + source = Path(self.tmpdir) / "FakeApp.app" + source.mkdir() + output = Path(self.tmpdir) / "output.pkg" + fake_tool = Path(self.tmpdir) / "faketool" + with patch.multiple( + mozpack.pkg, + get_app_info_plist=mock_get_app_info_plist, + get_apple_template=mock_get_apple_template, + save_text_file=noop, + create_payload=noop, + create_bom=noop, + xar_package_folder=noop, + ): + create_pkg(source, output, fake_tool, fake_tool, fake_tool) + + +if __name__ == "__main__": + mozunit.main() diff --git a/python/mozbuild/mozpack/test/test_unify.py b/python/mozbuild/mozpack/test/test_unify.py new file mode 100644 index 0000000000..15de50dccc --- /dev/null +++ b/python/mozbuild/mozpack/test/test_unify.py @@ -0,0 +1,250 @@ +# 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/. + +import os +import sys +from io import StringIO + +import mozunit + +from mozbuild.util import ensureParentDir +from mozpack.errors import AccumulatedErrors, ErrorMessage, errors +from mozpack.files import FileFinder +from mozpack.mozjar import JarWriter +from mozpack.test.test_files import MockDest, TestWithTmpDir +from mozpack.unify import UnifiedBuildFinder, UnifiedFinder + + +class TestUnified(TestWithTmpDir): + def create_one(self, which, path, content): + file = self.tmppath(os.path.join(which, path)) + ensureParentDir(file) + if isinstance(content, str): + content = content.encode("utf-8") + open(file, "wb").write(content) + + def create_both(self, path, content): + for p in ["a", "b"]: + self.create_one(p, path, content) + + +class TestUnifiedFinder(TestUnified): + def test_unified_finder(self): + self.create_both("foo/bar", "foobar") + self.create_both("foo/baz", "foobaz") + self.create_one("a", "bar", "bar") + self.create_one("b", "baz", "baz") + self.create_one("a", "qux", "foobar") + self.create_one("b", "qux", "baz") + self.create_one("a", "test/foo", "a\nb\nc\n") + self.create_one("b", "test/foo", "b\nc\na\n") + self.create_both("test/bar", "a\nb\nc\n") + + finder = UnifiedFinder( + FileFinder(self.tmppath("a")), + FileFinder(self.tmppath("b")), + sorted=["test"], + ) + self.assertEqual( + sorted( + [(f, c.open().read().decode("utf-8")) for f, c in finder.find("foo")] + ), + [("foo/bar", "foobar"), ("foo/baz", "foobaz")], + ) + self.assertRaises(ErrorMessage, any, finder.find("bar")) + self.assertRaises(ErrorMessage, any, finder.find("baz")) + self.assertRaises(ErrorMessage, any, finder.find("qux")) + self.assertEqual( + sorted( + [(f, c.open().read().decode("utf-8")) for f, c in finder.find("test")] + ), + [("test/bar", "a\nb\nc\n"), ("test/foo", "a\nb\nc\n")], + ) + + +class TestUnifiedBuildFinder(TestUnified): + def test_unified_build_finder(self): + finder = UnifiedBuildFinder( + FileFinder(self.tmppath("a")), FileFinder(self.tmppath("b")) + ) + + # Test chrome.manifest unification + self.create_both("chrome.manifest", "a\nb\nc\n") + self.create_one("a", "chrome/chrome.manifest", "a\nb\nc\n") + self.create_one("b", "chrome/chrome.manifest", "b\nc\na\n") + self.assertEqual( + sorted( + [ + (f, c.open().read().decode("utf-8")) + for f, c in finder.find("**/chrome.manifest") + ] + ), + [("chrome.manifest", "a\nb\nc\n"), ("chrome/chrome.manifest", "a\nb\nc\n")], + ) + + # Test buildconfig.html unification + self.create_one( + "a", + "chrome/browser/foo/buildconfig.html", + "\n".join( + [ + "<html>", + " <body>", + " <div>", + " <h1>Build Configuration</h1>", + " <div>foo</div>", + " </div>", + " </body>", + "</html>", + ] + ), + ) + self.create_one( + "b", + "chrome/browser/foo/buildconfig.html", + "\n".join( + [ + "<html>", + " <body>", + " <div>", + " <h1>Build Configuration</h1>", + " <div>bar</div>", + " </div>", + " </body>", + "</html>", + ] + ), + ) + self.assertEqual( + sorted( + [ + (f, c.open().read().decode("utf-8")) + for f, c in finder.find("**/buildconfig.html") + ] + ), + [ + ( + "chrome/browser/foo/buildconfig.html", + "\n".join( + [ + "<html>", + " <body>", + " <div>", + " <h1>Build Configuration</h1>", + " <div>foo</div>", + " <hr> </hr>", + " <div>bar</div>", + " </div>", + " </body>", + "</html>", + ] + ), + ) + ], + ) + + # Test xpi file unification + xpi = MockDest() + with JarWriter(fileobj=xpi, compress=True) as jar: + jar.add("foo", "foo") + jar.add("bar", "bar") + foo_xpi = xpi.read() + self.create_both("foo.xpi", foo_xpi) + + with JarWriter(fileobj=xpi, compress=True) as jar: + jar.add("foo", "bar") + self.create_one("a", "bar.xpi", foo_xpi) + self.create_one("b", "bar.xpi", xpi.read()) + + errors.out = StringIO() + with self.assertRaises(AccumulatedErrors), errors.accumulate(): + self.assertEqual( + [(f, c.open().read()) for f, c in finder.find("*.xpi")], + [("foo.xpi", foo_xpi)], + ) + errors.out = sys.stderr + + # Test install.rdf unification + x86_64 = "Darwin_x86_64-gcc3" + x86 = "Darwin_x86-gcc3" + target_tag = "<{em}targetPlatform>{platform}</{em}targetPlatform>" + target_attr = '{em}targetPlatform="{platform}" ' + + rdf_tag = "".join( + [ + '<{RDF}Description {em}bar="bar" {em}qux="qux">', + "<{em}foo>foo</{em}foo>", + "{targets}", + "<{em}baz>baz</{em}baz>", + "</{RDF}Description>", + ] + ) + rdf_attr = "".join( + [ + '<{RDF}Description {em}bar="bar" {attr}{em}qux="qux">', + "{targets}", + "<{em}foo>foo</{em}foo><{em}baz>baz</{em}baz>", + "</{RDF}Description>", + ] + ) + + for descr_ns, target_ns in (("RDF:", ""), ("", "em:"), ("RDF:", "em:")): + # First we need to infuse the above strings with our namespaces and + # platform values. + ns = {"RDF": descr_ns, "em": target_ns} + target_tag_x86_64 = target_tag.format(platform=x86_64, **ns) + target_tag_x86 = target_tag.format(platform=x86, **ns) + target_attr_x86_64 = target_attr.format(platform=x86_64, **ns) + target_attr_x86 = target_attr.format(platform=x86, **ns) + + tag_x86_64 = rdf_tag.format(targets=target_tag_x86_64, **ns) + tag_x86 = rdf_tag.format(targets=target_tag_x86, **ns) + tag_merged = rdf_tag.format( + targets=target_tag_x86_64 + target_tag_x86, **ns + ) + tag_empty = rdf_tag.format(targets="", **ns) + + attr_x86_64 = rdf_attr.format(attr=target_attr_x86_64, targets="", **ns) + attr_x86 = rdf_attr.format(attr=target_attr_x86, targets="", **ns) + attr_merged = rdf_attr.format( + attr="", targets=target_tag_x86_64 + target_tag_x86, **ns + ) + + # This table defines the test cases, columns "a" and "b" being the + # contents of the install.rdf of the respective platform and + # "result" the exepected merged content after unification. + testcases = ( + # _____a_____ _____b_____ ___result___# + (tag_x86_64, tag_x86, tag_merged), + (tag_x86_64, tag_empty, tag_empty), + (tag_empty, tag_x86, tag_empty), + (tag_empty, tag_empty, tag_empty), + (attr_x86_64, attr_x86, attr_merged), + (tag_x86_64, attr_x86, tag_merged), + (attr_x86_64, tag_x86, attr_merged), + (attr_x86_64, tag_empty, tag_empty), + (tag_empty, attr_x86, tag_empty), + ) + + # Now create the files from the above table and compare + results = [] + for emid, (rdf_a, rdf_b, result) in enumerate(testcases): + filename = "ext/id{0}/install.rdf".format(emid) + self.create_one("a", filename, rdf_a) + self.create_one("b", filename, rdf_b) + results.append((filename, result)) + + self.assertEqual( + sorted( + [ + (f, c.open().read().decode("utf-8")) + for f, c in finder.find("**/install.rdf") + ] + ), + results, + ) + + +if __name__ == "__main__": + mozunit.main() diff --git a/python/mozbuild/mozpack/unify.py b/python/mozbuild/mozpack/unify.py new file mode 100644 index 0000000000..ca4d0017a9 --- /dev/null +++ b/python/mozbuild/mozpack/unify.py @@ -0,0 +1,265 @@ +# 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/. + +import os +import re +import struct +import subprocess +from collections import OrderedDict +from tempfile import mkstemp + +import buildconfig + +import mozpack.path as mozpath +from mozbuild.util import hexdump +from mozpack.errors import errors +from mozpack.executables import MACHO_SIGNATURES +from mozpack.files import BaseFile, BaseFinder, ExecutableFile, GeneratedFile + +# Regular expressions for unifying install.rdf +FIND_TARGET_PLATFORM = re.compile( + r""" + <(?P<ns>[-._0-9A-Za-z]+:)?targetPlatform> # The targetPlatform tag, with any namespace + (?P<platform>[^<]*) # The actual platform value + </(?P=ns)?targetPlatform> # The closing tag + """, + re.X, +) +FIND_TARGET_PLATFORM_ATTR = re.compile( + r""" + (?P<tag><(?:[-._0-9A-Za-z]+:)?Description) # The opening part of the <Description> tag + (?P<attrs>[^>]*?)\s+ # The initial attributes + (?P<ns>[-._0-9A-Za-z]+:)?targetPlatform= # The targetPlatform attribute, with any namespace + [\'"](?P<platform>[^\'"]+)[\'"] # The actual platform value + (?P<otherattrs>[^>]*?>) # The remaining attributes and closing angle bracket + """, + re.X, +) + + +def may_unify_binary(file): + """ + Return whether the given BaseFile instance is an ExecutableFile that + may be unified. Only non-fat Mach-O binaries are to be unified. + """ + if isinstance(file, ExecutableFile): + signature = file.open().read(4) + if len(signature) < 4: + return False + signature = struct.unpack(">L", signature)[0] + if signature in MACHO_SIGNATURES: + return True + return False + + +class UnifiedExecutableFile(BaseFile): + """ + File class for executable and library files that to be unified with 'lipo'. + """ + + def __init__(self, executable1, executable2): + """ + Initialize a UnifiedExecutableFile with a pair of ExecutableFiles to + be unified. They are expected to be non-fat Mach-O executables. + """ + assert isinstance(executable1, ExecutableFile) + assert isinstance(executable2, ExecutableFile) + self._executables = (executable1, executable2) + + def copy(self, dest, skip_if_older=True): + """ + Create a fat executable from the two Mach-O executable given when + creating the instance. + skip_if_older is ignored. + """ + assert isinstance(dest, str) + tmpfiles = [] + try: + for e in self._executables: + fd, f = mkstemp() + os.close(fd) + tmpfiles.append(f) + e.copy(f, skip_if_older=False) + lipo = buildconfig.substs.get("LIPO") or "lipo" + subprocess.check_call([lipo, "-create"] + tmpfiles + ["-output", dest]) + except Exception as e: + errors.error( + "Failed to unify %s and %s: %s" + % (self._executables[0].path, self._executables[1].path, str(e)) + ) + finally: + for f in tmpfiles: + os.unlink(f) + + +class UnifiedFinder(BaseFinder): + """ + Helper to get unified BaseFile instances from two distinct trees on the + file system. + """ + + def __init__(self, finder1, finder2, sorted=[], **kargs): + """ + Initialize a UnifiedFinder. finder1 and finder2 are BaseFinder + instances from which files are picked. UnifiedFinder.find() will act as + FileFinder.find() but will error out when matches can only be found in + one of the two trees and not the other. It will also error out if + matches can be found on both ends but their contents are not identical. + + The sorted argument gives a list of mozpath.match patterns. File + paths matching one of these patterns will have their contents compared + with their lines sorted. + """ + assert isinstance(finder1, BaseFinder) + assert isinstance(finder2, BaseFinder) + self._finder1 = finder1 + self._finder2 = finder2 + self._sorted = sorted + BaseFinder.__init__(self, finder1.base, **kargs) + + def _find(self, path): + """ + UnifiedFinder.find() implementation. + """ + # There is no `OrderedSet`. Operator `|` was added only in + # Python 3.9, so we merge by hand. + all_paths = OrderedDict() + + files1 = OrderedDict() + for p, f in self._finder1.find(path): + files1[p] = f + all_paths[p] = True + files2 = OrderedDict() + for p, f in self._finder2.find(path): + files2[p] = f + all_paths[p] = True + + for p in all_paths: + err = errors.count + unified = self.unify_file(p, files1.get(p), files2.get(p)) + if unified: + yield p, unified + elif err == errors.count: # No errors have already been reported. + self._report_difference(p, files1.get(p), files2.get(p)) + + def _report_difference(self, path, file1, file2): + """ + Report differences between files in both trees. + """ + if not file1: + errors.error("File missing in %s: %s" % (self._finder1.base, path)) + return + if not file2: + errors.error("File missing in %s: %s" % (self._finder2.base, path)) + return + + errors.error( + "Can't unify %s: file differs between %s and %s" + % (path, self._finder1.base, self._finder2.base) + ) + if not isinstance(file1, ExecutableFile) and not isinstance( + file2, ExecutableFile + ): + from difflib import unified_diff + + try: + lines1 = [l.decode("utf-8") for l in file1.open().readlines()] + lines2 = [l.decode("utf-8") for l in file2.open().readlines()] + except UnicodeDecodeError: + lines1 = hexdump(file1.open().read()) + lines2 = hexdump(file2.open().read()) + + for line in unified_diff( + lines1, + lines2, + os.path.join(self._finder1.base, path), + os.path.join(self._finder2.base, path), + ): + errors.out.write(line) + + def unify_file(self, path, file1, file2): + """ + Given two BaseFiles and the path they were found at, return a + unified version of the files. If the files match, the first BaseFile + may be returned. + If the files don't match or one of them is `None`, the method returns + `None`. + Subclasses may decide to unify by using one of the files in that case. + """ + if not file1 or not file2: + return None + + if may_unify_binary(file1) and may_unify_binary(file2): + return UnifiedExecutableFile(file1, file2) + + content1 = file1.open().readlines() + content2 = file2.open().readlines() + if content1 == content2: + return file1 + for pattern in self._sorted: + if mozpath.match(path, pattern): + if sorted(content1) == sorted(content2): + return file1 + break + return None + + +class UnifiedBuildFinder(UnifiedFinder): + """ + Specialized UnifiedFinder for Mozilla applications packaging. It allows + ``*.manifest`` files to differ in their order, and unifies ``buildconfig.html`` + files by merging their content. + """ + + def __init__(self, finder1, finder2, **kargs): + UnifiedFinder.__init__( + self, finder1, finder2, sorted=["**/*.manifest"], **kargs + ) + + def unify_file(self, path, file1, file2): + """ + Unify files taking Mozilla application special cases into account. + Otherwise defer to UnifiedFinder.unify_file. + """ + basename = mozpath.basename(path) + if file1 and file2 and basename == "buildconfig.html": + content1 = file1.open().readlines() + content2 = file2.open().readlines() + # Copy everything from the first file up to the end of its <div>, + # insert a <hr> between the two files and copy the second file's + # content beginning after its leading <h1>. + return GeneratedFile( + b"".join( + content1[: content1.index(b" </div>\n")] + + [b" <hr> </hr>\n"] + + content2[ + content2.index(b" <h1>Build Configuration</h1>\n") + 1 : + ] + ) + ) + elif file1 and file2 and basename == "install.rdf": + # install.rdf files often have em:targetPlatform (either as + # attribute or as tag) that will differ between platforms. The + # unified install.rdf should contain both em:targetPlatforms if + # they exist, or strip them if only one file has a target platform. + content1, content2 = ( + FIND_TARGET_PLATFORM_ATTR.sub( + lambda m: m.group("tag") + + m.group("attrs") + + m.group("otherattrs") + + "<%stargetPlatform>%s</%stargetPlatform>" + % (m.group("ns") or "", m.group("platform"), m.group("ns") or ""), + f.open().read().decode("utf-8"), + ) + for f in (file1, file2) + ) + + platform2 = FIND_TARGET_PLATFORM.search(content2) + return GeneratedFile( + FIND_TARGET_PLATFORM.sub( + lambda m: m.group(0) + platform2.group(0) if platform2 else "", + content1, + ) + ) + return UnifiedFinder.unify_file(self, path, file1, file2) diff --git a/python/mozbuild/setup.py b/python/mozbuild/setup.py new file mode 100644 index 0000000000..30785493b0 --- /dev/null +++ b/python/mozbuild/setup.py @@ -0,0 +1,29 @@ +# 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 setuptools import find_packages, setup + +VERSION = "0.2" + +setup( + author="Mozilla Foundation", + author_email="dev-builds@lists.mozilla.org", + name="mozbuild", + description="Mozilla build system functionality.", + license="MPL 2.0", + packages=find_packages(), + version=VERSION, + install_requires=[ + "jsmin", + "mozfile", + ], + classifiers=[ + "Development Status :: 3 - Alpha", + "Topic :: Software Development :: Build Tools", + "License :: OSI Approved :: Mozilla Public License 2.0 (MPL 2.0)", + "Programming Language :: Python :: 2.7", + "Programming Language :: Python :: Implementation :: CPython", + ], + keywords="mozilla build", +) |