bootstrap.py (28471B)
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 file, 3 # You can obtain one at http://mozilla.org/MPL/2.0/. 4 5 import os 6 import platform 7 import re 8 import shutil 9 import subprocess 10 import sys 11 import time 12 from collections import OrderedDict 13 from pathlib import Path 14 from typing import Optional 15 16 # Use distro package to retrieve linux platform information 17 import distro 18 from mach.site import MachSiteManager 19 from mach.telemetry import initialize_telemetry_setting 20 from mach.util import ( 21 UserError, 22 get_state_dir, 23 to_optional_path, 24 ) 25 from mozbuild.base import MozbuildObject 26 from mozfile import which 27 from mozversioncontrol import get_repository_object 28 from packaging.version import Version 29 30 from mozboot.aerynos import AerynOsBootstrapper 31 from mozboot.archlinux import ArchlinuxBootstrapper 32 from mozboot.base import MODERN_RUST_VERSION 33 from mozboot.centosfedora import CentOSFedoraBootstrapper 34 from mozboot.debian import DebianBootstrapper 35 from mozboot.freebsd import FreeBSDBootstrapper 36 from mozboot.gentoo import GentooBootstrapper 37 from mozboot.mozconfig import MozconfigBuilder 38 from mozboot.mozillabuild import MozillaBuildBootstrapper 39 from mozboot.openbsd import OpenBSDBootstrapper 40 from mozboot.opensuse import OpenSUSEBootstrapper 41 from mozboot.osx import OSXBootstrapper, OSXBootstrapperLight 42 from mozboot.solus import SolusBootstrapper 43 from mozboot.void import VoidBootstrapper 44 from mozboot.windows import WindowsBootstrapper 45 46 APPLICATION_CHOICE = """ 47 Note on Artifact Mode: 48 49 Artifact builds download prebuilt C++ components rather than building 50 them locally. Artifact builds are faster! 51 52 Artifact builds are recommended for people working on Tor Browser or 53 Tor Browser for Android frontends, or the GeckoView Java API. They are unsuitable 54 for those working on C++ code. For more information see: 55 https://firefox-source-docs.mozilla.org/contributing/build/artifact_builds.html. 56 57 # Note to Tor Browser developers 58 59 This is still highly experimental. Expect bugs! 60 61 Please choose the version of Tor Browser you want to build (see note above): 62 %s 63 Your choice: """ 64 65 APPLICATIONS = OrderedDict([ 66 ("Tor Browser for Desktop Artifact Mode", "browser_artifact_mode"), 67 ("Tor Browser for Desktop", "browser"), 68 ( 69 "GeckoView/Tor Browser for Android Artifact Mode", 70 "mobile_android_artifact_mode", 71 ), 72 ("GeckoView/Tor Browser for Android", "mobile_android"), 73 ("SpiderMonkey JavaScript engine", "js"), 74 ]) 75 76 FINISHED = """ 77 Your system should be ready to build %s! 78 """ 79 80 MOZCONFIG_SUGGESTION_TEMPLATE = """ 81 Paste the lines between the chevrons (>>> and <<<) into 82 %s: 83 84 >>> 85 %s 86 <<< 87 """ 88 89 MOZCONFIG_MISMATCH_WARNING_TEMPLATE = """ 90 WARNING! Mismatch detected between the selected build target and the 91 mozconfig file %s: 92 93 Current config 94 >>> 95 %s 96 <<< 97 98 Expected config 99 >>> 100 %s 101 <<< 102 """ 103 104 CONFIGURE_MERCURIAL = """ 105 Mozilla recommends a number of changes to Mercurial to enhance your 106 experience with it. 107 108 Would you like to run a configuration wizard to ensure Mercurial is 109 optimally configured? (This will also ensure 'version-control-tools' is up-to-date)""" 110 111 CONFIGURE_GIT = """ 112 Would you like to run a few configuration steps to ensure Git is 113 optimally configured?""" 114 115 DEBIAN_DISTROS = ( 116 "debian", 117 "ubuntu", 118 "linuxmint", 119 "elementary", 120 "neon", 121 "pop", 122 "kali", 123 "devuan", 124 "pureos", 125 "deepin", 126 "tuxedo", 127 "zorin", 128 ) 129 130 FEDORA_DISTROS = ( 131 "centos", 132 "fedora", 133 "rocky", 134 "nobara", 135 "oracle", 136 "fedora-asahi-remix", 137 "ultramarine", 138 ) 139 140 141 OLD_REVISION_WARNING = """ 142 WARNING! You appear to be running `mach bootstrap` from an old revision. 143 bootstrap is meant primarily for getting developer environments up-to-date to 144 build the latest version of tree. Running bootstrap on old revisions may fail 145 and is not guaranteed to bring your machine to any working state in particular. 146 Proceed at your own peril. 147 """ 148 149 150 # Dev Drives were added in 22621.2338 and should be available in all subsequent versions 151 DEV_DRIVE_MINIMUM_VERSION = Version("10.0.22621.2338") 152 DEV_DRIVE_SUGGESTION = """ 153 Mach has detected that the Firefox source repository ({}) is located on an {} drive. 154 Your current version of Windows ({}) supports ReFS drives (Dev Drive). 155 156 It has been shown that Firefox builds are 5-10% faster on 157 ReFS, it is recommended that you create an ReFS drive and move the Firefox 158 source repository to it before proceeding. 159 160 The instructions for how to do that can be found here: https://learn.microsoft.com/en-us/windows/dev-drive/ 161 162 If you wish disregard this recommendation, you can hide this message by setting 163 'MACH_HIDE_DEV_DRIVE_SUGGESTION=1' in your environment variables (and restarting your shell).""" 164 DEV_DRIVE_DETECTION_ERROR = """ 165 Error encountered while checking for Dev Drive. 166 Reason: {} (skipping) 167 """ 168 169 170 def check_for_hgrc_state_dir_mismatch(state_dir): 171 ignore_hgrc_state_dir_mismatch = os.environ.get( 172 "MACH_IGNORE_HGRC_STATE_DIR_MISMATCH", "" 173 ) 174 if ignore_hgrc_state_dir_mismatch: 175 return 176 177 import subprocess 178 179 result = subprocess.run( 180 ["hg", "config", "--source", "-T", "json"], 181 check=False, 182 capture_output=True, 183 text=True, 184 ) 185 186 if result.returncode: 187 print("Failed to run 'hg config'. hg configuration checks will be skipped.") 188 return 189 190 from mozfile import json 191 192 try: 193 json_data = json.loads(result.stdout) 194 except json.JSONDecodeError as e: 195 print( 196 f"Error parsing 'hg config' JSON: {e}\n\n" 197 f"hg configuration checks will be skipped." 198 ) 199 return 200 201 mismatched_paths = [] 202 pattern = re.compile(r"(.*\.mozbuild)[\\/](.*)") 203 for entry in json_data: 204 if not entry["name"].startswith("extensions."): 205 continue 206 207 extension_path = entry["value"] 208 match = pattern.search(extension_path) 209 if match: 210 extension = entry["name"] 211 source_path = entry["source"] 212 state_dir_from_hgrc = Path(match.group(1)) 213 extension_suffix = match.group(2) 214 215 if state_dir != state_dir_from_hgrc.expanduser(): 216 expected_extension_path = state_dir / extension_suffix 217 218 mismatched_paths.append( 219 f"Extension: '{extension}' found in config file '{source_path}'\n" 220 f" Current: {extension_path}\n" 221 f" Expected: {expected_extension_path}\n" 222 ) 223 224 if mismatched_paths: 225 hgrc_state_dir_mismatch_error_message = ( 226 f"Paths for extensions in your hgrc file appear to be referencing paths that are not in " 227 f"the current '.mozbuild' state directory.\nYou may have set the `MOZBUILD_STATE_PATH` " 228 f"environment variable and/or moved the `.mozbuild` directory. You should update the " 229 f"paths for the following extensions manually to be inside '{state_dir}'\n" 230 f"(If you instead wish to hide this error, set 'MACH_IGNORE_HGRC_STATE_DIR_MISMATCH=1' " 231 f"in your environment variables and restart your shell before rerunning mach).\n\n" 232 f"You can either use the command 'hg config --edit' to make changes to your hg " 233 f"configuration or manually edit the 'config file' specified for each extension " 234 f"below:\n\n" 235 ) 236 hgrc_state_dir_mismatch_error_message += "".join(mismatched_paths) 237 238 raise Exception(hgrc_state_dir_mismatch_error_message) 239 240 241 class Bootstrapper: 242 """Main class that performs system bootstrap.""" 243 244 def __init__( 245 self, 246 choice=None, 247 no_interactive=False, 248 hg_configure=False, 249 no_system_changes=False, 250 exclude=[], 251 mach_context=None, 252 ): 253 self.instance = None 254 self.choice = choice 255 self.hg_configure = hg_configure 256 self.no_system_changes = no_system_changes 257 self.exclude = exclude 258 self.mach_context = mach_context 259 cls = None 260 args = { 261 "no_interactive": no_interactive, 262 "no_system_changes": no_system_changes, 263 } 264 265 if sys.platform.startswith("linux"): 266 # distro package provides reliable ids for popular distributions so 267 # we use those instead of the full distribution name 268 dist_id = distro.id() 269 version = distro.version() 270 271 if dist_id in FEDORA_DISTROS: 272 cls = CentOSFedoraBootstrapper 273 args["distro"] = dist_id 274 elif dist_id in DEBIAN_DISTROS: 275 cls = DebianBootstrapper 276 args["distro"] = dist_id 277 elif dist_id in ("gentoo", "funtoo"): 278 cls = GentooBootstrapper 279 elif dist_id in ("solus"): 280 cls = SolusBootstrapper 281 elif dist_id in ("arch", "kaos") or Path("/etc/arch-release").exists(): 282 cls = ArchlinuxBootstrapper 283 elif dist_id in ("aerynos"): 284 cls = AerynOsBootstrapper 285 elif dist_id in ("void"): 286 cls = VoidBootstrapper 287 elif dist_id in ( 288 "opensuse", 289 "opensuse-leap", 290 "opensuse-tumbleweed", 291 "suse", 292 ): 293 cls = OpenSUSEBootstrapper 294 else: 295 raise NotImplementedError( 296 "Bootstrap support for this Linux " 297 "distro not yet available: " + dist_id 298 ) 299 300 args["version"] = version 301 args["dist_id"] = dist_id 302 303 elif sys.platform.startswith("darwin"): 304 # TODO Support Darwin platforms that aren't OS X. 305 osx_version = platform.mac_ver()[0] 306 if platform.machine() == "arm64" or _macos_is_running_under_rosetta(): 307 cls = OSXBootstrapperLight 308 else: 309 cls = OSXBootstrapper 310 args["version"] = osx_version 311 312 elif sys.platform.startswith("openbsd"): 313 cls = OpenBSDBootstrapper 314 args["version"] = platform.uname()[2] 315 316 elif sys.platform.startswith(("dragonfly", "freebsd", "netbsd")): 317 cls = FreeBSDBootstrapper 318 args["version"] = platform.release() 319 args["flavor"] = platform.system() 320 321 elif sys.platform.startswith("win32") or sys.platform.startswith("msys"): 322 if "MOZILLABUILD" in os.environ: 323 cls = MozillaBuildBootstrapper 324 else: 325 cls = WindowsBootstrapper 326 if cls is None: 327 raise NotImplementedError( 328 "Bootstrap support is not yet available for your OS." 329 ) 330 331 self.instance = cls(**args) 332 333 def maybe_install_private_packages_or_exit(self, application, checkout_type): 334 # Install the clang packages needed for building the style system, as 335 # well as the version of NodeJS that we currently support. 336 # Also install the clang static-analysis package by default 337 # The best place to install our packages is in the state directory 338 # we have. We should have created one above in non-interactive mode. 339 self.instance.auto_bootstrap(application, self.exclude) 340 self.instance.install_toolchain_artifact("fix-stacks") 341 self.instance.install_toolchain_artifact("minidump-stackwalk") 342 if not self.instance.artifact_mode: 343 self.instance.install_toolchain_artifact("clang-tools/clang-tidy") 344 self.instance.ensure_sccache_packages() 345 # Like 'ensure_browser_packages' or 'ensure_mobile_android_packages' 346 getattr(self.instance, "ensure_%s_packages" % application)() 347 348 def check_code_submission(self, checkout_root: Path): 349 return 350 351 if self.instance.no_interactive or which("moz-phab"): 352 return 353 354 if not self.instance.prompt_yesno("Will you be submitting commits to Mozilla?"): 355 return 356 357 mach_binary = checkout_root / "mach" 358 subprocess.check_call((sys.executable, str(mach_binary), "install-moz-phab")) 359 360 def bootstrap(self, settings): 361 state_dir = Path(get_state_dir()) 362 363 hg = to_optional_path(which("hg")) 364 hg_installed = bool(hg) 365 366 if hg_installed: 367 check_for_hgrc_state_dir_mismatch(state_dir) 368 369 if self.choice is None: 370 applications = APPLICATIONS 371 # Like ['1. Firefox for Desktop', '2. Firefox for Android Artifact Mode', ...]. 372 labels = [ 373 "%s. %s" % (i, name) for i, name in enumerate(applications.keys(), 1) 374 ] 375 choices = [f" {labels[0]} [default]"] 376 choices += [f" {label}" for label in labels[1:]] 377 prompt = APPLICATION_CHOICE % "\n".join(choices) 378 prompt_choice = self.instance.prompt_int( 379 prompt=prompt, low=1, high=len(applications) 380 ) 381 name, application = list(applications.items())[prompt_choice - 1] 382 elif self.choice in APPLICATIONS.keys(): 383 name, application = self.choice, APPLICATIONS[self.choice] 384 elif self.choice in APPLICATIONS.values(): 385 name, application = next( 386 (k, v) for k, v in APPLICATIONS.items() if v == self.choice 387 ) 388 else: 389 raise Exception( 390 "Please pick a valid application choice: (%s)" 391 % "/".join(APPLICATIONS.keys()) 392 ) 393 394 mozconfig_builder = MozconfigBuilder() 395 self.instance.application = application 396 self.instance.artifact_mode = "artifact_mode" in application 397 398 self.instance.warn_if_pythonpath_is_set() 399 400 if sys.platform.startswith("darwin") and not os.environ.get( 401 "MACH_I_DO_WANT_TO_USE_ROSETTA" 402 ): 403 # If running on arm64 mac, check whether we're running under 404 # Rosetta and advise against it. 405 if _macos_is_running_under_rosetta(): 406 print( 407 "Python is being emulated under Rosetta. Please use a native " 408 "Python instead. If you still really want to go ahead, set " 409 "the MACH_I_DO_WANT_TO_USE_ROSETTA environment variable.", 410 file=sys.stderr, 411 ) 412 return 1 413 414 self.instance.state_dir = state_dir 415 416 # We need to enable the loading of hgrc in case extensions are 417 # required to open the repo. 418 (checkout_type, checkout_root) = current_firefox_checkout( 419 env=self.instance._hg_cleanenv(load_hgrc=True), 420 hg=hg, 421 ) 422 repo = get_repository_object(checkout_root) 423 self.instance.srcdir = checkout_root 424 self.instance.validate_environment() 425 self._validate_python_environment(checkout_root) 426 427 if sys.platform.startswith("win"): 428 self._check_for_dev_drive(checkout_root) 429 self._add_microsoft_defender_antivirus_exclusions(checkout_root, state_dir) 430 431 if self.instance.no_system_changes: 432 self.maybe_install_private_packages_or_exit(application, checkout_type) 433 self._output_mozconfig(application, mozconfig_builder) 434 sys.exit(0) 435 436 self.instance.install_system_packages() 437 438 # Like 'install_browser_packages' or 'install_mobile_android_packages'. 439 getattr(self.instance, "install_%s_packages" % application)(mozconfig_builder) 440 441 if not self.instance.artifact_mode: 442 self.instance.ensure_rust_modern() 443 444 git = to_optional_path(which("git")) 445 446 # Possibly configure Mercurial, but not if the current checkout or repo 447 # type is Git. 448 if checkout_type == "hg": 449 hg_installed, hg_modern = self.instance.ensure_mercurial_modern() 450 451 if hg_installed and checkout_type == "hg": 452 if not self.instance.no_interactive: 453 configure_hg = self.instance.prompt_yesno(prompt=CONFIGURE_MERCURIAL) 454 else: 455 configure_hg = self.hg_configure 456 457 if configure_hg: 458 repo.configure(state_dir) 459 460 # Offer to configure Git, if the current checkout or repo type is Git. 461 elif False and git and checkout_type == "git": 462 if not self.instance.no_interactive: 463 should_configure_git = self.instance.prompt_yesno(prompt=CONFIGURE_GIT) 464 else: 465 # Assuming default configuration setting applies to all VCS. 466 should_configure_git = self.hg_configure 467 468 if should_configure_git: 469 repo.configure(state_dir) 470 471 self.maybe_install_private_packages_or_exit(application, checkout_type) 472 self.check_code_submission(checkout_root) 473 # Wait until after moz-phab setup to check telemetry so that employees 474 # will be automatically opted-in. 475 if not self.instance.no_interactive and not settings.mach_telemetry.is_set_up: 476 initialize_telemetry_setting(settings, str(checkout_root), str(state_dir)) 477 478 self._output_mozconfig(application, mozconfig_builder) 479 480 print(FINISHED % name) 481 if not ( 482 which("rustc") 483 and self.instance._parse_version(Path("rustc")) >= MODERN_RUST_VERSION 484 ): 485 print( 486 "To build %s, please restart the shell (Start a new terminal window)" 487 % name 488 ) 489 490 def _check_for_dev_drive(self, topsrcdir): 491 def extract_windows_version_number(raw_ver_output): 492 pattern = re.compile(r"\bVersion (\d+(\.\d+)*)\b") 493 match = pattern.search(raw_ver_output) 494 495 if match: 496 windows_version_number = match.group(1) 497 return Version(windows_version_number) 498 499 return Version("0") 500 501 if os.environ.get("MACH_HIDE_DEV_DRIVE_SUGGESTION"): 502 return 503 504 print("Checking for Dev Drive...") 505 506 if not shutil.which("powershell"): 507 print( 508 "PowerShell is not available on the system path. Unable to check for Dev Drive." 509 ) 510 return 511 512 try: 513 ver_output = subprocess.check_output(["cmd.exe", "/c", "ver"], text=True) 514 current_windows_version = extract_windows_version_number(ver_output) 515 516 if current_windows_version < DEV_DRIVE_MINIMUM_VERSION: 517 return 518 519 file_system_info = subprocess.check_output( 520 [ 521 "powershell", 522 "Get-Item", 523 "-Path", 524 topsrcdir, 525 "|", 526 "Get-Volume", 527 "|", 528 "Select-Object", 529 "FileSystem", 530 ], 531 text=True, 532 ) 533 534 file_system_type = file_system_info.strip().split("\n")[2] 535 536 if file_system_type == "ReFS": 537 print(" The Firefox source repository is on a Dev Drive.") 538 else: 539 print( 540 DEV_DRIVE_SUGGESTION.format( 541 topsrcdir, file_system_type, current_windows_version 542 ) 543 ) 544 if self.instance.no_interactive: 545 pass 546 else: 547 input("\nPress enter to continue.") 548 549 except subprocess.CalledProcessError as error: 550 print( 551 DEV_DRIVE_DETECTION_ERROR.format(f"CalledProcessError: {error.stderr}") 552 ) 553 pass 554 555 def _add_microsoft_defender_antivirus_exclusions( 556 self, topsrcdir: Path, state_dir: Path 557 ): 558 if self.no_system_changes: 559 return 560 561 if os.environ.get("MOZ_AUTOMATION"): 562 return 563 564 # This will trigger a UAC prompt, and since it really only needs to be done 565 # once, we can put a flag_file in the state_dir once we've done it and check 566 # for its existence to prevent us from doing it again. 567 flag_file = state_dir / ".ANTIVIRUS_EXCLUSIONS_DONE" 568 if flag_file.exists(): 569 return 570 571 powershell_exe = which("powershell") 572 573 if not powershell_exe: 574 return 575 576 import ctypes 577 578 powershell_exe = str(powershell_exe) 579 paths = [] 580 581 # checkout root 582 paths.append(topsrcdir) 583 584 # MOZILLABUILD 585 mozillabuild_dir = os.getenv("MOZILLABUILD") 586 if mozillabuild_dir: 587 paths.append(mozillabuild_dir) 588 589 # .mozbuild 590 paths.append(state_dir) 591 592 joined_paths = "\n".join(f" '{p}'" for p in paths) 593 print( 594 "Attempting to add exclusion paths to Microsoft Defender Antivirus for:\n" 595 f"{joined_paths}" 596 ) 597 print( 598 "Note: This will trigger a UAC prompt. If you decline, no exclusions will be added." 599 ) 600 print( 601 f"This step will not run again unless you delete the following file: '{flag_file}'\n" 602 ) 603 604 args = ";".join(f"Add-MpPreference -ExclusionPath '{path}'" for path in paths) 605 command = f'-Command "{args}"' 606 607 # This will attempt to run as administrator by triggering a UAC prompt 608 # for admin credentials. If "No" is selected, no exclusions are added. 609 ctypes.windll.shell32.ShellExecuteW( 610 None, "runas", powershell_exe, command, None, 0 611 ) 612 613 try: 614 flag_file.touch(exist_ok=True) 615 except OSError as e: 616 print(f"Could not write flag_file '{flag_file}': {e}") 617 618 def _default_mozconfig_path(self): 619 return Path(self.mach_context.topdir) / "mozconfig" 620 621 def _read_default_mozconfig(self): 622 path = self._default_mozconfig_path() 623 with open(path) as mozconfig_file: 624 return mozconfig_file.read() 625 626 def _write_default_mozconfig(self, raw_mozconfig): 627 path = self._default_mozconfig_path() 628 with open(path, "w") as mozconfig_file: 629 mozconfig_file.write(raw_mozconfig) 630 print(f'Your requested configuration has been written to "{path}".') 631 632 def _show_mozconfig_suggestion(self, raw_mozconfig): 633 if raw_mozconfig: 634 suggestion = MOZCONFIG_SUGGESTION_TEMPLATE % ( 635 self._default_mozconfig_path(), 636 raw_mozconfig, 637 ) 638 print(suggestion, end="") 639 640 def _check_default_mozconfig_mismatch( 641 self, current_mozconfig_info, expected_application, expected_raw_mozconfig 642 ): 643 current_raw_mozconfig = self._read_default_mozconfig() 644 current_application = current_mozconfig_info["project"][0].replace("/", "_") 645 if current_mozconfig_info["artifact-builds"]: 646 current_application += "_artifact_mode" 647 648 if expected_application == current_application: 649 if expected_raw_mozconfig == current_raw_mozconfig: 650 return 651 652 # There's minor difference, show the suggestion. 653 self._show_mozconfig_suggestion(expected_raw_mozconfig) 654 return 655 656 warning = MOZCONFIG_MISMATCH_WARNING_TEMPLATE % ( 657 self._default_mozconfig_path(), 658 current_raw_mozconfig, 659 expected_raw_mozconfig, 660 ) 661 print(warning) 662 663 if not self.instance.prompt_yesno("Do you want to overwrite the config?"): 664 return 665 666 self._write_default_mozconfig(expected_raw_mozconfig) 667 668 def _output_mozconfig(self, application, mozconfig_builder): 669 # Like 'generate_browser_mozconfig' or 'generate_mobile_android_mozconfig'. 670 additional_mozconfig = getattr( 671 self.instance, "generate_%s_mozconfig" % application 672 )() 673 if additional_mozconfig: 674 mozconfig_builder.append(additional_mozconfig) 675 raw_mozconfig = mozconfig_builder.generate() 676 677 current_mozconfig_info = MozbuildObject.get_base_mozconfig_info( 678 self.mach_context.topdir, None, "" 679 ) 680 current_mozconfig_path = current_mozconfig_info["mozconfig"]["path"] 681 682 if current_mozconfig_path: 683 # mozconfig file exists 684 if self._default_mozconfig_path().exists() and Path.samefile( 685 Path(current_mozconfig_path), self._default_mozconfig_path() 686 ): 687 # This mozconfig file may be created by bootstrap. 688 self._check_default_mozconfig_mismatch( 689 current_mozconfig_info, application, raw_mozconfig 690 ) 691 elif raw_mozconfig: 692 # The mozconfig file is created by user. 693 self._show_mozconfig_suggestion(raw_mozconfig) 694 elif raw_mozconfig: 695 # No mozconfig file exists yet 696 self._write_default_mozconfig(raw_mozconfig) 697 698 def _validate_python_environment(self, topsrcdir): 699 valid = True 700 pip3 = to_optional_path(which("pip3")) 701 if not pip3: 702 print("ERROR: Could not find pip3.", file=sys.stderr) 703 self.instance.suggest_install_pip3() 704 valid = False 705 if not valid: 706 print( 707 "ERROR: Your Python installation will not be able to run " 708 "`mach bootstrap`. `mach bootstrap` cannot maintain your " 709 "Python environment for you; fix the errors shown here, and " 710 "then re-run `mach bootstrap`.", 711 file=sys.stderr, 712 ) 713 sys.exit(1) 714 715 mach_site = MachSiteManager.from_environment( 716 topsrcdir, 717 lambda: os.path.normpath(get_state_dir(True, topsrcdir=topsrcdir)), 718 ) 719 mach_site.attempt_populate_optional_packages() 720 721 722 def current_firefox_checkout(env, hg: Optional[Path] = None): 723 """Determine whether we're in a Firefox checkout. 724 725 Returns one of None, ``git``, or ``hg``. 726 """ 727 HG_ROOT_REVISIONS = set([ 728 # From mozilla-unified. 729 "8ba995b74e18334ab3707f27e9eb8f4e37ba3d29" 730 ]) 731 732 path = Path.cwd() 733 while path: 734 hg_dir = path / ".hg" 735 git_dir = path / ".git" 736 known_file = path / "config" / "milestone.txt" 737 if hg and hg_dir.exists(): 738 # Verify the hg repo is a Firefox repo by looking at rev 0. 739 try: 740 node = subprocess.check_output( 741 [str(hg), "log", "-r", "0", "--template", "{node}"], 742 cwd=str(path), 743 env=env, 744 universal_newlines=True, 745 ) 746 if node in HG_ROOT_REVISIONS: 747 _warn_if_risky_revision(path) 748 return "hg", path 749 # Else the root revision is different. There could be nested 750 # repos. So keep traversing the parents. 751 except subprocess.CalledProcessError: 752 pass 753 754 # Just check for known-good files in the checkout, to prevent attempted 755 # foot-shootings. Determining a canonical git checkout of mozilla-unified 756 # is...complicated 757 elif git_dir.exists() or hg_dir.exists(): 758 if known_file.exists(): 759 _warn_if_risky_revision(path) 760 return ("git" if git_dir.exists() else "hg"), path 761 elif known_file.exists(): 762 return "SOURCE", path 763 764 if not len(path.parents): 765 break 766 path = path.parent 767 768 raise UserError( 769 "Could not identify the root directory of your checkout! " 770 "Are you running `mach bootstrap` in an hg or git clone?" 771 ) 772 773 774 def _warn_if_risky_revision(path: Path): 775 # Warn the user if they're trying to bootstrap from an obviously old 776 # version of tree as reported by the version control system (a month in 777 # this case). This is an approximate calculation but is probably good 778 # enough for our purposes. 779 NUM_SECONDS_IN_MONTH = 60 * 60 * 24 * 30 780 781 repo = get_repository_object(path) 782 if (time.time() - repo.get_commit_time()) >= NUM_SECONDS_IN_MONTH: 783 print(OLD_REVISION_WARNING) 784 785 786 def _macos_is_running_under_rosetta(): 787 proc = subprocess.run( 788 ["sysctl", "-n", "sysctl.proc_translated"], 789 check=False, 790 stdout=subprocess.PIPE, 791 stderr=subprocess.DEVNULL, 792 ) 793 return ( 794 proc.returncode == 0 and proc.stdout.decode("ascii", "replace").strip() == "1" 795 )