tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

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    )