diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 01:47:29 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 01:47:29 +0000 |
commit | 0ebf5bdf043a27fd3dfb7f92e0cb63d88954c44d (patch) | |
tree | a31f07c9bcca9d56ce61e9a1ffd30ef350d513aa /python/mozbuild/mozbuild/artifact_commands.py | |
parent | Initial commit. (diff) | |
download | firefox-esr-37a0381f8351b370577b65028ba1f6563ae23fdf.tar.xz firefox-esr-37a0381f8351b370577b65028ba1f6563ae23fdf.zip |
Adding upstream version 115.8.0esr.upstream/115.8.0esr
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'python/mozbuild/mozbuild/artifact_commands.py')
-rw-r--r-- | python/mozbuild/mozbuild/artifact_commands.py | 615 |
1 files changed, 615 insertions, 0 deletions
diff --git a/python/mozbuild/mozbuild/artifact_commands.py b/python/mozbuild/mozbuild/artifact_commands.py new file mode 100644 index 0000000000..12184ce0d9 --- /dev/null +++ b/python/mozbuild/mozbuild/artifact_commands.py @@ -0,0 +1,615 @@ +# This Source Code Form is subject to the terms of 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 + +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 gecko_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 |