1# This Source Code Form is subject to the terms of the Mozilla Public 2# License, v. 2.0. If a copy of the MPL was not distributed with this 3# file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 5from __future__ import absolute_import 6import argparse 7import hashlib 8import json 9import logging 10import os 11import shutil 12import six 13 14from collections import OrderedDict 15 16from mach.decorators import ( 17 CommandArgument, 18 CommandProvider, 19 Command, 20 SubCommand, 21) 22from mozbuild.artifact_builds import JOB_CHOICES 23from mozbuild.base import ( 24 MachCommandBase, 25 MachCommandConditions as conditions, 26) 27from mozbuild.util import ensureParentDir 28import mozversioncontrol 29 30 31_COULD_NOT_FIND_ARTIFACTS_TEMPLATE = ( 32 "ERROR!!!!!! Could not find artifacts for a toolchain build named " 33 "`{build}`. Local commits, dirty/stale files, and other changes in your " 34 "checkout may cause this error. Make sure you are on a fresh, current " 35 "checkout of mozilla-central. Beware that commands like `mach bootstrap` " 36 "and `mach artifact` are unlikely to work on any versions of the code " 37 "besides recent revisions of mozilla-central." 38) 39 40 41class SymbolsAction(argparse.Action): 42 def __call__(self, parser, namespace, values, option_string=None): 43 # If this function is called, it means the --symbols option was given, 44 # so we want to store the value `True` if no explicit value was given 45 # to the option. 46 setattr(namespace, self.dest, values or True) 47 48 49class ArtifactSubCommand(SubCommand): 50 def __call__(self, func): 51 after = SubCommand.__call__(self, func) 52 args = [ 53 CommandArgument("--tree", metavar="TREE", type=str, help="Firefox tree."), 54 CommandArgument( 55 "--job", metavar="JOB", choices=JOB_CHOICES, help="Build job." 56 ), 57 CommandArgument( 58 "--verbose", "-v", action="store_true", help="Print verbose output." 59 ), 60 ] 61 for arg in args: 62 after = arg(after) 63 return after 64 65 66@CommandProvider 67class PackageFrontend(MachCommandBase): 68 """Fetch and install binary artifacts from Mozilla automation.""" 69 70 @Command( 71 "artifact", 72 category="post-build", 73 description="Use pre-built artifacts to build Firefox.", 74 ) 75 def artifact(self, command_context): 76 """Download, cache, and install pre-built binary artifacts to build Firefox. 77 78 Use |mach build| as normal to freshen your installed binary libraries: 79 artifact builds automatically download, cache, and install binary 80 artifacts from Mozilla automation, replacing whatever may be in your 81 object directory. Use |mach artifact last| to see what binary artifacts 82 were last used. 83 84 Never build libxul again! 85 86 """ 87 pass 88 89 def _make_artifacts( 90 self, 91 tree=None, 92 job=None, 93 skip_cache=False, 94 download_tests=True, 95 download_symbols=False, 96 download_host_bins=False, 97 download_maven_zip=False, 98 no_process=False, 99 ): 100 state_dir = self._mach_context.state_dir 101 cache_dir = os.path.join(state_dir, "package-frontend") 102 103 hg = None 104 if conditions.is_hg(self): 105 hg = self.substs["HG"] 106 107 git = None 108 if conditions.is_git(self): 109 git = self.substs["GIT"] 110 111 # If we're building Thunderbird, we should be checking for comm-central artifacts. 112 topsrcdir = self.substs.get("commtopsrcdir", self.topsrcdir) 113 114 if download_maven_zip: 115 if download_tests: 116 raise ValueError("--maven-zip requires --no-tests") 117 if download_symbols: 118 raise ValueError("--maven-zip requires no --symbols") 119 if download_host_bins: 120 raise ValueError("--maven-zip requires no --host-bins") 121 if not no_process: 122 raise ValueError("--maven-zip requires --no-process") 123 124 from mozbuild.artifacts import Artifacts 125 126 artifacts = Artifacts( 127 tree, 128 self.substs, 129 self.defines, 130 job, 131 log=self.log, 132 cache_dir=cache_dir, 133 skip_cache=skip_cache, 134 hg=hg, 135 git=git, 136 topsrcdir=topsrcdir, 137 download_tests=download_tests, 138 download_symbols=download_symbols, 139 download_host_bins=download_host_bins, 140 download_maven_zip=download_maven_zip, 141 no_process=no_process, 142 mozbuild=self, 143 ) 144 return artifacts 145 146 @ArtifactSubCommand("artifact", "install", "Install a good pre-built artifact.") 147 @CommandArgument( 148 "source", 149 metavar="SRC", 150 nargs="?", 151 type=str, 152 help="Where to fetch and install artifacts from. Can be omitted, in " 153 "which case the current hg repository is inspected; an hg revision; " 154 "a remote URL; or a local file.", 155 default=None, 156 ) 157 @CommandArgument( 158 "--skip-cache", 159 action="store_true", 160 help="Skip all local caches to force re-fetching remote artifacts.", 161 default=False, 162 ) 163 @CommandArgument("--no-tests", action="store_true", help="Don't install tests.") 164 @CommandArgument( 165 "--symbols", nargs="?", action=SymbolsAction, help="Download symbols." 166 ) 167 @CommandArgument("--host-bins", action="store_true", help="Download host binaries.") 168 @CommandArgument("--distdir", help="Where to install artifacts to.") 169 @CommandArgument( 170 "--no-process", 171 action="store_true", 172 help="Don't process (unpack) artifact packages, just download them.", 173 ) 174 @CommandArgument( 175 "--maven-zip", action="store_true", help="Download Maven zip (Android-only)." 176 ) 177 def artifact_install( 178 self, 179 command_context, 180 source=None, 181 skip_cache=False, 182 tree=None, 183 job=None, 184 verbose=False, 185 no_tests=False, 186 symbols=False, 187 host_bins=False, 188 distdir=None, 189 no_process=False, 190 maven_zip=False, 191 ): 192 self._set_log_level(verbose) 193 artifacts = self._make_artifacts( 194 tree=tree, 195 job=job, 196 skip_cache=skip_cache, 197 download_tests=not no_tests, 198 download_symbols=symbols, 199 download_host_bins=host_bins, 200 download_maven_zip=maven_zip, 201 no_process=no_process, 202 ) 203 204 return artifacts.install_from(source, distdir or self.distdir) 205 206 @ArtifactSubCommand( 207 "artifact", 208 "clear-cache", 209 "Delete local artifacts and reset local artifact cache.", 210 ) 211 def artifact_clear_cache(self, command_context, tree=None, job=None, verbose=False): 212 self._set_log_level(verbose) 213 artifacts = self._make_artifacts(tree=tree, job=job) 214 artifacts.clear_cache() 215 return 0 216 217 @SubCommand("artifact", "toolchain") 218 @CommandArgument( 219 "--verbose", "-v", action="store_true", help="Print verbose output." 220 ) 221 @CommandArgument( 222 "--cache-dir", 223 metavar="DIR", 224 help="Directory where to store the artifacts cache", 225 ) 226 @CommandArgument( 227 "--skip-cache", 228 action="store_true", 229 help="Skip all local caches to force re-fetching remote artifacts.", 230 default=False, 231 ) 232 @CommandArgument( 233 "--from-build", 234 metavar="BUILD", 235 nargs="+", 236 help="Download toolchains resulting from the given build(s); " 237 "BUILD is a name of a toolchain task, e.g. linux64-clang", 238 ) 239 @CommandArgument( 240 "--tooltool-manifest", 241 metavar="MANIFEST", 242 help="Explicit tooltool manifest to process", 243 ) 244 @CommandArgument( 245 "--no-unpack", action="store_true", help="Do not unpack any downloaded file" 246 ) 247 @CommandArgument( 248 "--retry", type=int, default=4, help="Number of times to retry failed downloads" 249 ) 250 @CommandArgument( 251 "--bootstrap", 252 action="store_true", 253 help="Whether this is being called from bootstrap. " 254 "This verifies the toolchain is annotated as a toolchain used for local development.", 255 ) 256 @CommandArgument( 257 "--artifact-manifest", 258 metavar="FILE", 259 help="Store a manifest about the downloaded taskcluster artifacts", 260 ) 261 def artifact_toolchain( 262 self, 263 command_context, 264 verbose=False, 265 cache_dir=None, 266 skip_cache=False, 267 from_build=(), 268 tooltool_manifest=None, 269 no_unpack=False, 270 retry=0, 271 bootstrap=False, 272 artifact_manifest=None, 273 ): 274 """Download, cache and install pre-built toolchains.""" 275 from mozbuild.artifacts import ArtifactCache 276 from mozbuild.action.tooltool import ( 277 FileRecord, 278 open_manifest, 279 unpack_file, 280 ) 281 import redo 282 import requests 283 import time 284 285 from taskgraph.util.taskcluster import get_artifact_url 286 287 start = time.time() 288 self._set_log_level(verbose) 289 # Normally, we'd use self.log_manager.enable_unstructured(), 290 # but that enables all logging, while we only really want tooltool's 291 # and it also makes structured log output twice. 292 # So we manually do what it does, and limit that to the tooltool 293 # logger. 294 if self.log_manager.terminal_handler: 295 logging.getLogger("mozbuild.action.tooltool").addHandler( 296 self.log_manager.terminal_handler 297 ) 298 logging.getLogger("redo").addHandler(self.log_manager.terminal_handler) 299 self.log_manager.terminal_handler.addFilter( 300 self.log_manager.structured_filter 301 ) 302 if not cache_dir: 303 cache_dir = os.path.join(self._mach_context.state_dir, "toolchains") 304 305 tooltool_host = os.environ.get("TOOLTOOL_HOST", "tooltool.mozilla-releng.net") 306 taskcluster_proxy_url = os.environ.get("TASKCLUSTER_PROXY_URL") 307 if taskcluster_proxy_url: 308 tooltool_url = "{}/{}".format(taskcluster_proxy_url, tooltool_host) 309 else: 310 tooltool_url = "https://{}".format(tooltool_host) 311 312 cache = ArtifactCache(cache_dir=cache_dir, log=self.log, skip_cache=skip_cache) 313 314 class DownloadRecord(FileRecord): 315 def __init__(self, url, *args, **kwargs): 316 super(DownloadRecord, self).__init__(*args, **kwargs) 317 self.url = url 318 self.basename = self.filename 319 320 def fetch_with(self, cache): 321 self.filename = cache.fetch(self.url) 322 return self.filename 323 324 def validate(self): 325 if self.size is None and self.digest is None: 326 return True 327 return super(DownloadRecord, self).validate() 328 329 class ArtifactRecord(DownloadRecord): 330 def __init__(self, task_id, artifact_name): 331 for _ in redo.retrier(attempts=retry + 1, sleeptime=60): 332 cot = cache._download_manager.session.get( 333 get_artifact_url(task_id, "public/chain-of-trust.json") 334 ) 335 if cot.status_code >= 500: 336 continue 337 cot.raise_for_status() 338 break 339 else: 340 cot.raise_for_status() 341 342 digest = algorithm = None 343 data = json.loads(cot.text) 344 for algorithm, digest in ( 345 data.get("artifacts", {}).get(artifact_name, {}).items() 346 ): 347 pass 348 349 name = os.path.basename(artifact_name) 350 artifact_url = get_artifact_url( 351 task_id, 352 artifact_name, 353 use_proxy=not artifact_name.startswith("public/"), 354 ) 355 super(ArtifactRecord, self).__init__( 356 artifact_url, name, None, digest, algorithm, unpack=True 357 ) 358 359 records = OrderedDict() 360 downloaded = [] 361 362 if tooltool_manifest: 363 manifest = open_manifest(tooltool_manifest) 364 for record in manifest.file_records: 365 url = "{}/{}/{}".format(tooltool_url, record.algorithm, record.digest) 366 records[record.filename] = DownloadRecord( 367 url, 368 record.filename, 369 record.size, 370 record.digest, 371 record.algorithm, 372 unpack=record.unpack, 373 version=record.version, 374 visibility=record.visibility, 375 ) 376 377 if from_build: 378 if "MOZ_AUTOMATION" in os.environ: 379 self.log( 380 logging.ERROR, 381 "artifact", 382 {}, 383 "Do not use --from-build in automation; all dependencies " 384 "should be determined in the decision task.", 385 ) 386 return 1 387 from taskgraph.optimize.strategies import IndexSearch 388 from mozbuild.toolchains import toolchain_task_definitions 389 390 tasks = toolchain_task_definitions() 391 392 for b in from_build: 393 user_value = b 394 395 if not b.startswith("toolchain-"): 396 b = "toolchain-{}".format(b) 397 398 task = tasks.get(b) 399 if not task: 400 self.log( 401 logging.ERROR, 402 "artifact", 403 {"build": user_value}, 404 "Could not find a toolchain build named `{build}`", 405 ) 406 return 1 407 408 # Ensure that toolchains installed by `mach bootstrap` have the 409 # `local-toolchain attribute set. Taskgraph ensures that these 410 # are built on trunk projects, so the task will be available to 411 # install here. 412 if bootstrap and not task.attributes.get("local-toolchain"): 413 self.log( 414 logging.ERROR, 415 "artifact", 416 {"build": user_value}, 417 "Toolchain `{build}` is not annotated as used for local development.", 418 ) 419 return 1 420 421 artifact_name = task.attributes.get("toolchain-artifact") 422 self.log( 423 logging.DEBUG, 424 "artifact", 425 { 426 "name": artifact_name, 427 "index": task.optimization.get("index-search"), 428 }, 429 "Searching for {name} in {index}", 430 ) 431 deadline = None 432 task_id = IndexSearch().should_replace_task( 433 task, {}, deadline, task.optimization.get("index-search", []) 434 ) 435 if task_id in (True, False) or not artifact_name: 436 self.log( 437 logging.ERROR, 438 "artifact", 439 {"build": user_value}, 440 _COULD_NOT_FIND_ARTIFACTS_TEMPLATE, 441 ) 442 # Get and print some helpful info for diagnosis. 443 repo = mozversioncontrol.get_repository_object(self.topsrcdir) 444 changed_files = set(repo.get_outgoing_files()) | set( 445 repo.get_changed_files() 446 ) 447 if changed_files: 448 self.log( 449 logging.ERROR, 450 "artifact", 451 {}, 452 "Hint: consider reverting your local changes " 453 "to the following files: %s" % sorted(changed_files), 454 ) 455 if "TASKCLUSTER_ROOT_URL" in os.environ: 456 self.log( 457 logging.ERROR, 458 "artifact", 459 {"build": user_value}, 460 "Due to the environment variable TASKCLUSTER_ROOT_URL " 461 "being set, the artifacts were expected to be found " 462 "on {}. If this was unintended, unset " 463 "TASKCLUSTER_ROOT_URL and try again.".format( 464 os.environ["TASKCLUSTER_ROOT_URL"] 465 ), 466 ) 467 return 1 468 469 self.log( 470 logging.DEBUG, 471 "artifact", 472 {"name": artifact_name, "task_id": task_id}, 473 "Found {name} in {task_id}", 474 ) 475 476 record = ArtifactRecord(task_id, artifact_name) 477 records[record.filename] = record 478 479 for record in six.itervalues(records): 480 self.log( 481 logging.INFO, 482 "artifact", 483 {"name": record.basename}, 484 "Setting up artifact {name}", 485 ) 486 valid = False 487 # sleeptime is 60 per retry.py, used by tooltool_wrapper.sh 488 for attempt, _ in enumerate(redo.retrier(attempts=retry + 1, sleeptime=60)): 489 try: 490 record.fetch_with(cache) 491 except ( 492 requests.exceptions.HTTPError, 493 requests.exceptions.ChunkedEncodingError, 494 requests.exceptions.ConnectionError, 495 ) as e: 496 497 if isinstance(e, requests.exceptions.HTTPError): 498 # The relengapi proxy likes to return error 400 bad request 499 # which seems improbably to be due to our (simple) GET 500 # being borked. 501 status = e.response.status_code 502 should_retry = status >= 500 or status == 400 503 else: 504 should_retry = True 505 506 if should_retry or attempt < retry: 507 level = logging.WARN 508 else: 509 level = logging.ERROR 510 self.log(level, "artifact", {}, str(e)) 511 if not should_retry: 512 break 513 if attempt < retry: 514 self.log( 515 logging.INFO, "artifact", {}, "Will retry in a moment..." 516 ) 517 continue 518 try: 519 valid = record.validate() 520 except Exception: 521 pass 522 if not valid: 523 os.unlink(record.filename) 524 if attempt < retry: 525 self.log( 526 logging.INFO, 527 "artifact", 528 {}, 529 "Corrupt download. Will retry in a moment...", 530 ) 531 continue 532 533 downloaded.append(record) 534 break 535 536 if not valid: 537 self.log( 538 logging.ERROR, 539 "artifact", 540 {"name": record.basename}, 541 "Failed to download {name}", 542 ) 543 return 1 544 545 artifacts = {} if artifact_manifest else None 546 547 for record in downloaded: 548 local = os.path.join(os.getcwd(), record.basename) 549 if os.path.exists(local): 550 os.unlink(local) 551 # unpack_file needs the file with its final name to work 552 # (https://github.com/mozilla/build-tooltool/issues/38), so we 553 # need to copy it, even though we remove it later. Use hard links 554 # when possible. 555 try: 556 os.link(record.filename, local) 557 except Exception: 558 shutil.copy(record.filename, local) 559 # Keep a sha256 of each downloaded file, for the chain-of-trust 560 # validation. 561 if artifact_manifest is not None: 562 with open(local, "rb") as fh: 563 h = hashlib.sha256() 564 while True: 565 data = fh.read(1024 * 1024) 566 if not data: 567 break 568 h.update(data) 569 artifacts[record.url] = { 570 "sha256": h.hexdigest(), 571 } 572 if record.unpack and not no_unpack: 573 unpack_file(local) 574 os.unlink(local) 575 576 if not downloaded: 577 self.log(logging.ERROR, "artifact", {}, "Nothing to download") 578 579 if artifacts: 580 ensureParentDir(artifact_manifest) 581 with open(artifact_manifest, "w") as fh: 582 json.dump(artifacts, fh, indent=4, sort_keys=True) 583 584 if "MOZ_AUTOMATION" in os.environ: 585 end = time.time() 586 587 perfherder_data = { 588 "framework": {"name": "build_metrics"}, 589 "suites": [ 590 { 591 "name": "mach_artifact_toolchain", 592 "value": end - start, 593 "lowerIsBetter": True, 594 "shouldAlert": False, 595 "subtests": [], 596 } 597 ], 598 } 599 self.log( 600 logging.INFO, 601 "perfherder", 602 {"data": json.dumps(perfherder_data)}, 603 "PERFHERDER_DATA: {data}", 604 ) 605 606 return 0 607