tor-browser

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

mach_initialize.py (20419B)


      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 
      5 import math
      6 import os
      7 import shutil
      8 import sys
      9 from importlib.abc import MetaPathFinder
     10 from pathlib import Path
     11 
     12 STATE_DIR_FIRST_RUN = """
     13 Mach and the build system store shared state in a common directory
     14 on the filesystem. The following directory will be created:
     15 
     16  {}
     17 
     18 If you would like to use a different directory, rename or move it to your
     19 desired location, and set the MOZBUILD_STATE_PATH environment variable
     20 accordingly.
     21 """.strip()
     22 
     23 
     24 CATEGORIES = {
     25    "build": {
     26        "short": "Build Commands",
     27        "long": "Interact with the build system",
     28        "priority": 80,
     29    },
     30    "post-build": {
     31        "short": "Post-build Commands",
     32        "long": "Common actions performed after completing a build.",
     33        "priority": 70,
     34    },
     35    "testing": {
     36        "short": "Testing",
     37        "long": "Run tests.",
     38        "priority": 60,
     39    },
     40    "ci": {
     41        "short": "CI",
     42        "long": "Taskcluster commands",
     43        "priority": 59,
     44    },
     45    "devenv": {
     46        "short": "Development Environment",
     47        "long": "Set up and configure your development environment.",
     48        "priority": 50,
     49    },
     50    "build-dev": {
     51        "short": "Low-level Build System Interaction",
     52        "long": "Interact with specific parts of the build system.",
     53        "priority": 20,
     54    },
     55    "misc": {
     56        "short": "Potpourri",
     57        "long": "Potent potables and assorted snacks.",
     58        "priority": 10,
     59    },
     60    "release": {
     61        "short": "Release automation",
     62        "long": "Commands for used in release automation.",
     63        "priority": 5,
     64    },
     65    "disabled": {
     66        "short": "Disabled",
     67        "long": "The disabled commands are hidden by default. Use -v to display them. "
     68        "These commands are unavailable for your current context, "
     69        'run "mach <command>" to see why.',
     70        "priority": 0,
     71    },
     72 }
     73 
     74 
     75 def _activate_python_environment(topsrcdir, get_state_dir, quiet):
     76    from mach.site import MachSiteManager
     77 
     78    mach_environment = MachSiteManager.from_environment(
     79        topsrcdir, get_state_dir, quiet=quiet
     80    )
     81    mach_environment.activate()
     82 
     83 
     84 def _maybe_activate_mozillabuild_environment():
     85    if sys.platform != "win32":
     86        return
     87 
     88    mozillabuild = Path(os.environ.get("MOZILLABUILD", r"C:\mozilla-build"))
     89    os.environ.setdefault("MOZILLABUILD", str(mozillabuild))
     90    assert mozillabuild.exists(), (
     91        f'MozillaBuild was not found at "{mozillabuild}".\n'
     92        "If it's installed in a different location, please "
     93        'set the "MOZILLABUILD" environment variable '
     94        "accordingly."
     95    )
     96 
     97    use_msys2 = (mozillabuild / "msys2").exists()
     98    if use_msys2:
     99        mozillabuild_msys_tools_path = mozillabuild / "msys2" / "usr" / "bin"
    100    else:
    101        mozillabuild_msys_tools_path = mozillabuild / "msys" / "bin"
    102 
    103    paths_to_add = [mozillabuild_msys_tools_path, mozillabuild / "bin"]
    104    existing_paths = [Path(p) for p in os.environ.get("PATH", "").split(os.pathsep)]
    105    for new_path in paths_to_add:
    106        if new_path not in existing_paths:
    107            os.environ["PATH"] += f"{os.pathsep}{new_path}"
    108 
    109 
    110 def check_for_spaces(topsrcdir):
    111    if " " in topsrcdir:
    112        raise Exception(
    113            f"Your checkout at path '{topsrcdir}' contains a space, which "
    114            f"is not supported. Please move it to somewhere that does not "
    115            f"have a space in the path before rerunning mach."
    116        )
    117 
    118    mozillabuild_dir = os.environ.get("MOZILLABUILD", "")
    119    if sys.platform == "win32" and " " in mozillabuild_dir:
    120        raise Exception(
    121            f"Your installation of MozillaBuild appears to be installed on a path that "
    122            f"contains a space ('{mozillabuild_dir}') which is not supported. Please "
    123            f"reinstall MozillaBuild on a path without a space and restart your shell"
    124            f"from the new installation."
    125        )
    126 
    127 
    128 def initialize(topsrcdir, args=()):
    129    # This directory was deleted in bug 1666345, but there may be some ignored
    130    # files here. We can safely just delete it for the user so they don't have
    131    # to clean the repo themselves.
    132    deleted_dir = os.path.join(topsrcdir, "third_party", "python", "psutil")
    133    if os.path.exists(deleted_dir):
    134        shutil.rmtree(deleted_dir, ignore_errors=True)
    135 
    136    # We need the "mach" module to access the logic to parse virtualenv
    137    # requirements. Since that depends on "packaging", we add it to the path too.
    138    # We need filelock for solving a virtualenv race condition
    139    sys.path[0:0] = [
    140        os.path.join(topsrcdir, module)
    141        for module in (
    142            os.path.join("python", "mach"),
    143            os.path.join("testing", "mozbase", "mozfile"),
    144            os.path.join("third_party", "python", "packaging"),
    145            os.path.join("third_party", "python", "filelock"),
    146        )
    147    ]
    148 
    149    from mach.util import get_state_dir, get_virtualenv_base_dir, setenv
    150 
    151    state_dir = _create_state_dir()
    152 
    153    check_for_spaces(topsrcdir)
    154 
    155    # See bug 1874208:
    156    # Status messages from site.py break usages of `./mach environment`.
    157    # We pass `quiet` only for it to work around this, so that all other
    158    # commands can still write status messages.
    159    if args and (args[0] == "environment" or "--quiet" in args):
    160        quiet = True
    161    else:
    162        quiet = False
    163 
    164    # normpath state_dir to normalize msys-style slashes.
    165    _activate_python_environment(
    166        topsrcdir,
    167        lambda: os.path.normpath(get_state_dir(True, topsrcdir=topsrcdir)),
    168        quiet=quiet,
    169    )
    170    _maybe_activate_mozillabuild_environment()
    171 
    172    import mach.main
    173    from mach.command_util import (
    174        MACH_COMMANDS,
    175        DetermineCommandVenvAction,
    176        load_commands_from_spec,
    177    )
    178    from mach.main import get_argument_parser
    179 
    180    # Set a reasonable limit to the number of open files.
    181    #
    182    # Some linux systems set `ulimit -n` to a very high number, which works
    183    # well for systems that run servers, but this setting causes performance
    184    # problems when programs close file descriptors before forking, like
    185    # Python's `subprocess.Popen(..., close_fds=True)` (close_fds=True is the
    186    # default in Python 3), or Rust's stdlib.  In some cases, Firefox does the
    187    # same thing when spawning processes.  We would prefer to lower this limit
    188    # to avoid such performance problems; processes spawned by `mach` will
    189    # inherit the limit set here.
    190    #
    191    # The Firefox build defaults the soft limit to 1024, except for builds that
    192    # do LTO, where the soft limit is 8192.  We're going to default to the
    193    # latter, since people do occasionally do LTO builds on their local
    194    # machines, and requiring them to discover another magical setting after
    195    # setting up an LTO build in the first place doesn't seem good.
    196    #
    197    # This code mimics the code in taskcluster/scripts/run-task.
    198    try:
    199        import resource
    200 
    201        # Keep the hard limit the same, though, allowing processes to change
    202        # their soft limit if they need to (Firefox does, for instance).
    203        (soft, hard) = resource.getrlimit(resource.RLIMIT_NOFILE)
    204        # Permit people to override our default limit if necessary via
    205        # MOZ_LIMIT_NOFILE, which is the same variable `run-task` uses.
    206        limit = os.environ.get("MOZ_LIMIT_NOFILE")
    207        if limit:
    208            limit = int(limit)
    209        else:
    210            # If no explicit limit is given, use our default if it's less than
    211            # the current soft limit.  For instance, the default on macOS is
    212            # 256, so we'd pick that rather than our default.
    213            limit = min(soft, 8192)
    214        # Now apply the limit, if it's different from the original one.
    215        if limit != soft:
    216            resource.setrlimit(resource.RLIMIT_NOFILE, (limit, hard))
    217    except ImportError:
    218        # The resource module is UNIX only.
    219        pass
    220 
    221    def resolve_repository():
    222        import mozversioncontrol
    223 
    224        try:
    225            # This API doesn't respect the vcs binary choices from configure.
    226            # If we ever need to use the VCS binary here, consider something
    227            # more robust.
    228            return mozversioncontrol.get_repository_object(path=topsrcdir)
    229        except (mozversioncontrol.InvalidRepoPath, mozversioncontrol.MissingVCSTool):
    230            return None
    231 
    232    def pre_dispatch_handler(context, handler, args):
    233        # If --disable-tests flag was enabled in the mozconfig used to compile
    234        # the build, tests will be disabled. Instead of trying to run
    235        # nonexistent tests then reporting a failure, this will prevent mach
    236        # from progressing beyond this point.
    237        if handler.category == "testing" and not handler.ok_if_tests_disabled:
    238            from mozbuild.base import BuildEnvironmentNotFoundException
    239 
    240            try:
    241                from mozbuild.base import MozbuildObject
    242 
    243                # all environments should have an instance of build object.
    244                build = MozbuildObject.from_environment()
    245                if build is not None and not getattr(
    246                    build, "substs", {"ENABLE_TESTS": True}
    247                ).get("ENABLE_TESTS"):
    248                    print(
    249                        "Tests have been disabled with --disable-tests.\n"
    250                        + "Remove the flag, and re-compile to enable tests."
    251                    )
    252                    sys.exit(1)
    253            except BuildEnvironmentNotFoundException:
    254                # likely automation environment, so do nothing.
    255                pass
    256 
    257    def post_dispatch_handler(
    258        context, handler, instance, success, start_time, end_time, depth, args
    259    ):
    260        """Perform global operations after command dispatch.
    261 
    262 
    263        For now,  we will use this to handle build system telemetry.
    264        """
    265 
    266        # Don't finalize telemetry data if this mach command was invoked as part of
    267        # another mach command.
    268        if depth != 1:
    269            return
    270 
    271        _finalize_telemetry_glean(
    272            context.telemetry,
    273            handler.name == "bootstrap",
    274            success,
    275            Path(topsrcdir),
    276            Path(state_dir),
    277            driver.settings,
    278        )
    279 
    280    def populate_context(key=None):
    281        if key is None:
    282            return
    283        if key == "state_dir":
    284            return state_dir
    285 
    286        if key == "local_state_dir":
    287            return get_state_dir(specific_to_topsrcdir=True)
    288 
    289        if key == "topdir":
    290            return topsrcdir
    291 
    292        if key == "pre_dispatch_handler":
    293            return pre_dispatch_handler
    294 
    295        if key == "post_dispatch_handler":
    296            return post_dispatch_handler
    297 
    298        if key == "repository":
    299            return resolve_repository()
    300 
    301        raise AttributeError(key)
    302 
    303    # Note which process is top-level so that recursive mach invocations can avoid writing
    304    # telemetry data.
    305    if "MACH_MAIN_PID" not in os.environ:
    306        setenv("MACH_MAIN_PID", str(os.getpid()))
    307 
    308    driver = mach.main.Mach(os.getcwd())
    309    driver.populate_context_handler = populate_context
    310 
    311    if not driver.settings_paths:
    312        # default global machrc location
    313        driver.settings_paths.append(state_dir)
    314    # always load local repository configuration
    315    driver.settings_paths.append(topsrcdir)
    316    driver.load_settings()
    317 
    318    aliases = driver.settings.alias
    319 
    320    parser = get_argument_parser(
    321        action=DetermineCommandVenvAction,
    322        topsrcdir=topsrcdir,
    323    )
    324    from argparse import Namespace
    325 
    326    from mach.main import (
    327        SUGGESTED_COMMANDS_MESSAGE,
    328        UNKNOWN_COMMAND_ERROR,
    329        UnknownCommandError,
    330    )
    331 
    332    namespace_in = Namespace()
    333    setattr(namespace_in, "mach_command_aliases", aliases)
    334 
    335    try:
    336        namespace = parser.parse_args(args, namespace_in)
    337    except UnknownCommandError as e:
    338        suggestion_message = (
    339            SUGGESTED_COMMANDS_MESSAGE % (e.verb, ", ".join(e.suggested_commands))
    340            if e.suggested_commands
    341            else ""
    342        )
    343        print(UNKNOWN_COMMAND_ERROR % (e.verb, e.command, suggestion_message))
    344        sys.exit(1)
    345 
    346    command_name = getattr(namespace, "command_name", None)
    347    site_name = getattr(namespace, "site_name", "common")
    348    command_site_manager = None
    349 
    350    # the 'clobber' command needs to run in the 'mach' venv, so we
    351    # don't want to activate any other virtualenv for it.
    352    if command_name != "clobber":
    353        from mach.site import CommandSiteManager
    354 
    355        command_site_manager = CommandSiteManager.from_environment(
    356            topsrcdir,
    357            lambda: os.path.normpath(get_state_dir(True, topsrcdir=topsrcdir)),
    358            site_name,
    359            get_virtualenv_base_dir(topsrcdir),
    360            quiet=quiet,
    361        )
    362 
    363        command_site_manager.activate()
    364 
    365    for category, meta in CATEGORIES.items():
    366        driver.define_category(category, meta["short"], meta["long"], meta["priority"])
    367 
    368    # Sparse checkouts may not have all mach_commands.py files. Ignore
    369    # errors from missing files. Same for spidermonkey tarballs.
    370    repo = resolve_repository()
    371    if repo != "SOURCE":
    372        missing_ok = (
    373            repo is not None and repo.sparse_checkout_present()
    374        ) or os.path.exists(os.path.join(topsrcdir, "INSTALL"))
    375    else:
    376        missing_ok = ()
    377 
    378    commands_that_need_all_modules_loaded = [
    379        "busted",
    380        "help",
    381        "mach-commands",
    382        "mach-completion",
    383        "mach-debug-commands",
    384    ]
    385 
    386    def commands_to_load(top_level_command: str):
    387        visited = set()
    388 
    389        def find_downstream_commands_recursively(command: str):
    390            if not MACH_COMMANDS.get(command):
    391                return
    392 
    393            if command in visited:
    394                return
    395 
    396            visited.add(command)
    397 
    398            for command_dependency in MACH_COMMANDS[command].command_dependencies:
    399                find_downstream_commands_recursively(command_dependency)
    400 
    401        find_downstream_commands_recursively(top_level_command)
    402 
    403        return list(visited)
    404 
    405    if (
    406        command_name not in MACH_COMMANDS
    407        or command_name in commands_that_need_all_modules_loaded
    408    ):
    409        command_modules_to_load = MACH_COMMANDS
    410    else:
    411        command_names_to_load = commands_to_load(command_name)
    412        command_modules_to_load = {
    413            command_name: MACH_COMMANDS[command_name]
    414            for command_name in command_names_to_load
    415        }
    416 
    417    driver.command_site_manager = command_site_manager
    418    load_commands_from_spec(command_modules_to_load, topsrcdir, missing_ok=missing_ok)
    419 
    420    return driver
    421 
    422 
    423 def _finalize_telemetry_glean(
    424    telemetry, is_bootstrap, success, topsrcdir, state_dir, settings
    425 ):
    426    """Submit telemetry collected by Glean.
    427 
    428    Finalizes some metrics (command success state and duration, system information) and
    429    requests Glean to send the collected data.
    430    """
    431 
    432    from mach.telemetry import MACH_METRICS_PATH, resolve_is_employee
    433    from mozbuild.telemetry import (
    434        get_cpu_brand,
    435        get_crowdstrike_running,
    436        get_distro_and_version,
    437        get_fleet_running,
    438        get_psutil_stats,
    439        get_shell_info,
    440        get_vscode_running,
    441    )
    442 
    443    moz_automation = any(e in os.environ for e in ("MOZ_AUTOMATION", "TASK_ID"))
    444 
    445    mach_metrics = telemetry.metrics(MACH_METRICS_PATH)
    446    mach_metrics.mach.duration.stop()
    447    mach_metrics.mach.success.set(success)
    448    mach_metrics.mach.moz_automation.set(moz_automation)
    449 
    450    system_metrics = mach_metrics.mach.system
    451    cpu_brand = get_cpu_brand()
    452    if cpu_brand:
    453        system_metrics.cpu_brand.set(cpu_brand)
    454    distro, version = get_distro_and_version()
    455    system_metrics.distro.set(distro)
    456    system_metrics.distro_version.set(version)
    457 
    458    vscode_terminal, ssh_connection = get_shell_info()
    459    system_metrics.vscode_terminal.set(vscode_terminal)
    460    system_metrics.ssh_connection.set(ssh_connection)
    461    system_metrics.vscode_running.set(get_vscode_running())
    462 
    463    # Only collect Fleet and CrowdStrike metrics for Mozilla employees
    464    if resolve_is_employee(topsrcdir, state_dir, settings):
    465        system_metrics.fleet_running.set(get_fleet_running())
    466        system_metrics.crowdstrike_running.set(get_crowdstrike_running())
    467 
    468    has_psutil, logical_cores, physical_cores, memory_total = get_psutil_stats()
    469    if has_psutil:
    470        # psutil may not be available (we may not have been able to download
    471        # a wheel or build it from source).
    472        system_metrics.logical_cores.add(logical_cores)
    473        if physical_cores is not None:
    474            system_metrics.physical_cores.add(physical_cores)
    475        if memory_total is not None:
    476            system_metrics.memory.accumulate(
    477                int(math.ceil(float(memory_total) / (1024 * 1024 * 1024)))
    478            )
    479    telemetry.submit(is_bootstrap)
    480 
    481 
    482 def _create_state_dir():
    483    # Global build system and mach state is stored in a central directory. By
    484    # default, this is ~/.mozbuild. However, it can be defined via an
    485    # environment variable. We detect first run (by lack of this directory
    486    # existing) and notify the user that it will be created. The logic for
    487    # creation is much simpler for the "advanced" environment variable use
    488    # case. For default behavior, we educate users and give them an opportunity
    489    # to react.
    490    state_dir = os.environ.get("MOZBUILD_STATE_PATH")
    491    if state_dir:
    492        if not os.path.exists(state_dir):
    493            print(
    494                f"Creating global state directory from environment variable: {state_dir}"
    495            )
    496    else:
    497        state_dir = os.path.expanduser("~/.mozbuild")
    498        if not os.path.exists(state_dir):
    499            if not os.environ.get("MOZ_AUTOMATION"):
    500                print(STATE_DIR_FIRST_RUN.format(state_dir))
    501 
    502            print(f"Creating default state directory: {state_dir}")
    503 
    504    os.makedirs(state_dir, mode=0o770, exist_ok=True)
    505    return state_dir
    506 
    507 
    508 # Hook import such that .pyc/.pyo files without a corresponding .py file in
    509 # the source directory are essentially ignored. See further below for details
    510 # and caveats.
    511 # Objdirs outside the source directory are ignored because in most cases, if
    512 # a .pyc/.pyo file exists there, a .py file will be next to it anyways.
    513 class FinderHook(MetaPathFinder):
    514    def __init__(self, klass):
    515        # Assume the source directory is the parent directory of the one
    516        # containing this file.
    517        self._source_dir = (
    518            os.path.normcase(
    519                os.path.abspath(os.path.dirname(os.path.dirname(__file__)))
    520            )
    521            + os.sep
    522        )
    523        self.finder_class = klass
    524 
    525    def find_spec(self, full_name, paths=None, target=None):
    526        spec = self.finder_class.find_spec(full_name, paths, target)
    527 
    528        # Some modules don't have an origin.
    529        if spec is None or spec.origin is None:
    530            return spec
    531 
    532        # Normalize the origin path.
    533        path = os.path.normcase(os.path.abspath(spec.origin))
    534        # Note: we could avoid normcase and abspath above for non pyc/pyo
    535        # files, but those are actually rare, so it doesn't really matter.
    536        if not path.endswith((".pyc", ".pyo")):
    537            return spec
    538 
    539        # Ignore modules outside our source directory
    540        if not path.startswith(self._source_dir):
    541            return spec
    542 
    543        # If there is no .py corresponding to the .pyc/.pyo module we're
    544        # resolving, remove the .pyc/.pyo file, and try again.
    545        if not os.path.exists(spec.origin[:-1]):
    546            if os.path.exists(spec.origin):
    547                os.remove(spec.origin)
    548            spec = self.finder_class.find_spec(full_name, paths, target)
    549 
    550        return spec
    551 
    552 
    553 # Additional hook for python >= 3.8's importlib.metadata.
    554 class MetadataHook(FinderHook):
    555    def find_distributions(self, *args, **kwargs):
    556        return self.finder_class.find_distributions(*args, **kwargs)
    557 
    558 
    559 def hook(finder):
    560    has_find_spec = hasattr(finder, "find_spec")
    561    has_find_distributions = hasattr(finder, "find_distributions")
    562    if has_find_spec and has_find_distributions:
    563        return MetadataHook(finder)
    564    elif has_find_spec:
    565        return FinderHook(finder)
    566    return finder
    567 
    568 
    569 sys.meta_path = [hook(c) for c in sys.meta_path]