tor-browser

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

mach_commands.py (16736B)


      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 # Originally taken from /talos/mach_commands.py
      6 
      7 # Integrates raptor mozharness with mach
      8 
      9 import json
     10 import logging
     11 import os
     12 import socket
     13 import subprocess
     14 import sys
     15 
     16 from mach.decorators import Command
     17 from mach.util import get_state_dir
     18 from mozbuild.base import BinaryNotFoundException, MozbuildObject
     19 from mozbuild.base import MachCommandConditions as Conditions
     20 
     21 HERE = os.path.dirname(os.path.realpath(__file__))
     22 
     23 ANDROID_BROWSERS = ["geckoview", "refbrow", "fenix", "chrome-m", "cstm-car-m"]
     24 
     25 
     26 class RaptorRunner(MozbuildObject):
     27    def run_test(self, raptor_args, kwargs):
     28        """Setup and run mozharness.
     29 
     30        We want to do a few things before running Raptor:
     31 
     32        1. Clone mozharness
     33        2. Make the config for Raptor mozharness
     34        3. Run mozharness
     35        """
     36        # Validate that the user is using a supported python version before doing anything else
     37        max_py_major, max_py_minor = 3, 13
     38        sys_maj, sys_min = sys.version_info.major, sys.version_info.minor
     39        if sys_min > max_py_minor:
     40            raise PythonVersionException(
     41                print(
     42                    f"\tPlease downgrade your Python version as Raptor does not yet support Python "
     43                    f"versions greater than {max_py_major}.{max_py_minor}."
     44                    f"\n\tYou seem to currently be using Python {sys_maj}.{sys_min}."
     45                    f"\n\tSee here for a possible solution in debugging your python environment: "
     46                    f"https://firefox-source-docs.mozilla.org/testing/perfdocs/"
     47                    f"debugging.html#debugging-local-python-environment"
     48                )
     49            )
     50        self.init_variables(raptor_args, kwargs)
     51        self.make_config()
     52        self.write_config()
     53        self.make_args()
     54        return self.run_mozharness()
     55 
     56    def init_variables(self, raptor_args, kwargs):
     57        self.raptor_args = raptor_args
     58 
     59        if kwargs.get("host") == "HOST_IP":
     60            kwargs["host"] = os.environ["HOST_IP"]
     61        self.host = kwargs["host"]
     62        self.is_release_build = kwargs["is_release_build"]
     63        self.live_sites = kwargs["live_sites"]
     64        self.disable_perf_tuning = kwargs["disable_perf_tuning"]
     65        self.conditioned_profile = kwargs["conditioned_profile"]
     66        self.device_name = kwargs["device_name"]
     67        self.enable_marionette_trace = kwargs["enable_marionette_trace"]
     68        self.browsertime_visualmetrics = kwargs["browsertime_visualmetrics"]
     69        self.browsertime_node = kwargs["browsertime_node"]
     70        self.clean = kwargs["clean"]
     71        self.screenshot_on_failure = kwargs["screenshot_on_failure"]
     72 
     73        if Conditions.is_android(self) or kwargs["app"] in ANDROID_BROWSERS:
     74            self.binary_path = None
     75        else:
     76            self.binary_path = kwargs.get("binary") or self.get_binary_path()
     77 
     78        self.python = sys.executable
     79 
     80        self.raptor_dir = os.path.join(self.topsrcdir, "testing", "raptor")
     81        self.mozharness_dir = os.path.join(self.topsrcdir, "testing", "mozharness")
     82        self.config_file_path = os.path.join(
     83            self._topobjdir, "testing", "raptor-in_tree_conf.json"
     84        )
     85 
     86        self.virtualenv_script = os.path.join(
     87            self.topsrcdir, "third_party", "python", "virtualenv", "virtualenv.py"
     88        )
     89        self.virtualenv_path = os.path.join(self._topobjdir, "testing", "raptor-venv")
     90 
     91    def make_config(self):
     92        default_actions = [
     93            "populate-webroot",
     94            "create-virtualenv",
     95            "install-chromium-distribution",
     96            "run-tests",
     97        ]
     98        self.config = {
     99            "run_local": True,
    100            "binary_path": self.binary_path,
    101            "repo_path": self.topsrcdir,
    102            "raptor_path": self.raptor_dir,
    103            "obj_path": self.topobjdir,
    104            "log_name": "raptor",
    105            "virtualenv_path": self.virtualenv_path,
    106            "pypi_url": "http://pypi.org/simple",
    107            "base_work_dir": self.mozharness_dir,
    108            "exes": {
    109                "python": self.python,
    110                "virtualenv": [self.python, self.virtualenv_script],
    111            },
    112            "title": socket.gethostname(),
    113            "default_actions": default_actions,
    114            "raptor_cmd_line_args": self.raptor_args,
    115            "host": self.host,
    116            "live_sites": self.live_sites,
    117            "disable_perf_tuning": self.disable_perf_tuning,
    118            "conditioned_profile": self.conditioned_profile,
    119            "is_release_build": self.is_release_build,
    120            "device_name": self.device_name,
    121            "enable_marionette_trace": self.enable_marionette_trace,
    122            "browsertime_visualmetrics": self.browsertime_visualmetrics,
    123            "browsertime_node": self.browsertime_node,
    124            "mozbuild_path": get_state_dir(),
    125            "clean": self.clean,
    126            "screenshot_on_failure": self.screenshot_on_failure,
    127        }
    128 
    129        sys.path.insert(0, os.path.join(self.topsrcdir, "tools", "browsertime"))
    130        try:
    131            import platform
    132 
    133            import mach_commands as browsertime
    134 
    135            # We don't set `browsertime_{chromedriver,geckodriver} -- those will be found by
    136            # browsertime in its `node_modules` directory, which is appropriate for local builds.
    137            # We don't set `browsertime_ffmpeg` yet: it will need to be on the path.  There is code
    138            # to configure the environment including the path in
    139            # `tools/browsertime/mach_commands.py` but integrating it here will take more effort.
    140            self.config.update({
    141                "browsertime_browsertimejs": browsertime.browsertime_path(),
    142                "browsertime_vismet_script": browsertime.visualmetrics_path(),
    143            })
    144 
    145            def _get_browsertime_package():
    146                with open(
    147                    os.path.join(
    148                        self.topsrcdir,
    149                        "tools",
    150                        "browsertime",
    151                        "node_modules",
    152                        "browsertime",
    153                        "package.json",
    154                    )
    155                ) as package:
    156                    return json.load(package)
    157 
    158            def _get_browsertime_resolved():
    159                try:
    160                    with open(
    161                        os.path.join(
    162                            self.topsrcdir,
    163                            "tools",
    164                            "browsertime",
    165                            "node_modules",
    166                            ".package-lock.json",
    167                        )
    168                    ) as package_lock:
    169                        return json.load(package_lock)["packages"][
    170                            "node_modules/browsertime"
    171                        ]["resolved"]
    172                except FileNotFoundError:
    173                    # Older versions of node/npm add this metadata to package.json
    174                    return _get_browsertime_package()["_from"]
    175 
    176            def _should_install():
    177                # If ffmpeg doesn't exist in the .mozbuild directory,
    178                # then we should install
    179                btime_cache = os.path.join(self.config["mozbuild_path"], "browsertime")
    180                if not os.path.exists(btime_cache) or not any([
    181                    "ffmpeg" in cache_dir for cache_dir in os.listdir(btime_cache)
    182                ]):
    183                    return True
    184 
    185                # If browsertime doesn't exist, install it
    186                if not os.path.exists(
    187                    self.config["browsertime_browsertimejs"]
    188                ) or not os.path.exists(self.config["browsertime_vismet_script"]):
    189                    return True
    190 
    191                # Browsertime exists, check if it's outdated
    192                with open(
    193                    os.path.join(self.topsrcdir, "tools", "browsertime", "package.json")
    194                ) as new:
    195                    new_pkg = json.load(new)
    196 
    197                return not _get_browsertime_resolved().endswith(
    198                    new_pkg["devDependencies"]["browsertime"]
    199                )
    200 
    201            def _get_browsertime_version():
    202                # Returns the (version number, current commit) used
    203                return (
    204                    _get_browsertime_package()["version"],
    205                    _get_browsertime_resolved(),
    206                )
    207 
    208            # Check if browsertime scripts exist and try to install them if
    209            # they aren't
    210            if _should_install():
    211                # TODO: Make this "integration" nicer in the near future
    212                print("Missing browsertime files...attempting to install")
    213                subprocess.check_output(
    214                    [
    215                        os.path.join(self.topsrcdir, "mach"),
    216                        "browsertime",
    217                        "--setup",
    218                        "--clobber",
    219                    ],
    220                    shell="windows" in platform.system().lower(),
    221                )
    222                if _should_install():
    223                    raise Exception(
    224                        "Failed installation attempt. Cannot find browsertime scripts. "
    225                        "Run `./mach browsertime --setup --clobber` to set it up."
    226                    )
    227 
    228                # Bug 1766112 - For the time being, we need to trigger a
    229                # clean build to upgrade browsertime. This should be disabled
    230                # after some time.
    231                print(
    232                    "Setting --clean to True to rebuild Python "
    233                    "environment for Browsertime upgrade..."
    234                )
    235                self.config["clean"] = True
    236 
    237            print("Using browsertime version %s from %s" % _get_browsertime_version())
    238 
    239        finally:
    240            sys.path = sys.path[1:]
    241 
    242    def make_args(self):
    243        self.args = {
    244            "config": {},
    245            "initial_config_file": self.config_file_path,
    246        }
    247 
    248    def write_config(self):
    249        try:
    250            config_file = open(self.config_file_path, "w")
    251            config_file.write(json.dumps(self.config))
    252            config_file.close()
    253        except OSError as e:
    254            err_str = "Error writing to Raptor Mozharness config file {0}:{1}"
    255            print(err_str.format(self.config_file_path, str(e)))
    256            raise e
    257 
    258    def run_mozharness(self):
    259        sys.path.insert(0, self.mozharness_dir)
    260        from mozharness.mozilla.testing.raptor import Raptor
    261 
    262        raptor_mh = Raptor(
    263            config=self.args["config"],
    264            initial_config_file=self.args["initial_config_file"],
    265        )
    266        return raptor_mh.run()
    267 
    268 
    269 def setup_node(command_context):
    270    """Fetch the latest node-22 binary and install it into the .mozbuild directory."""
    271    import platform
    272 
    273    from mozbuild.artifact_commands import artifact_toolchain
    274    from mozbuild.nodeutil import find_node_executable
    275    from packaging.version import Version
    276 
    277    print("Setting up node for browsertime...")
    278    state_dir = get_state_dir()
    279    cache_path = os.path.join(state_dir, "browsertime", "node-22")
    280 
    281    def __check_for_node():
    282        # Check standard locations first
    283        node_exe = find_node_executable(min_version=Version("22.0.0"))
    284        if node_exe and (node_exe[0] is not None):
    285            return node_exe[0]
    286        if not os.path.exists(cache_path):
    287            return None
    288 
    289        # Check the browsertime-specific node location next
    290        node_name = "node"
    291        if platform.system() == "Windows":
    292            node_name = "node.exe"
    293            node_exe_path = os.path.join(
    294                state_dir,
    295                "browsertime",
    296                "node-22",
    297                "node",
    298            )
    299        else:
    300            node_exe_path = os.path.join(
    301                state_dir,
    302                "browsertime",
    303                "node-22",
    304                "node",
    305                "bin",
    306            )
    307 
    308        node_exe = os.path.join(node_exe_path, node_name)
    309        if not os.path.exists(node_exe):
    310            return None
    311 
    312        return node_exe
    313 
    314    node_exe = __check_for_node()
    315    if node_exe is None:
    316        toolchain_job = "{}-node-22"
    317        plat = platform.system()
    318        if plat == "Windows":
    319            toolchain_job = toolchain_job.format("win64")
    320        elif plat == "Darwin":
    321            if platform.processor() == "arm":
    322                toolchain_job = toolchain_job.format("macosx64-aarch64")
    323            else:
    324                toolchain_job = toolchain_job.format("macosx64")
    325        else:
    326            toolchain_job = toolchain_job.format("linux64")
    327 
    328        print(f"Downloading Node 22 from Taskcluster toolchain {toolchain_job}...")
    329 
    330        if not os.path.exists(cache_path):
    331            os.makedirs(cache_path, exist_ok=True)
    332 
    333        # Change directories to where node should be installed
    334        # before installing. Otherwise, it gets installed in the
    335        # top level of the repo (or the current working directory).
    336        cur_dir = os.getcwd()
    337        os.chdir(cache_path)
    338        artifact_toolchain(
    339            command_context,
    340            verbose=False,
    341            from_build=[toolchain_job],
    342            no_unpack=False,
    343            retry=0,
    344            cache_dir=cache_path,
    345        )
    346        os.chdir(cur_dir)
    347 
    348        node_exe = __check_for_node()
    349        if node_exe is None:
    350            raise Exception("Could not find Node v22 binary for Raptor-Browsertime")
    351 
    352        print("Finished downloading Node v22 from Taskcluster")
    353 
    354    print("Node v22+ found at: %s" % node_exe)
    355    return node_exe
    356 
    357 
    358 def create_parser():
    359    sys.path.insert(0, HERE)  # allow to import the raptor package
    360    from raptor.cmdline import create_parser
    361 
    362    return create_parser(mach_interface=True)
    363 
    364 
    365 @Command(
    366    "raptor",
    367    category="testing",
    368    description="Run Raptor performance tests.",
    369    parser=create_parser,
    370 )
    371 def run_raptor(command_context, **kwargs):
    372    build_obj = command_context
    373 
    374    # Setup node for browsertime
    375    kwargs["browsertime_node"] = setup_node(command_context)
    376 
    377    is_android = Conditions.is_android(build_obj) or kwargs["app"] in ANDROID_BROWSERS
    378 
    379    if is_android:
    380        from mozrunner.devices.android_device import (
    381            InstallIntent,
    382            verify_android_device,
    383        )
    384 
    385        install = (
    386            InstallIntent.NO if kwargs.pop("no_install", False) else InstallIntent.YES
    387        )
    388        verbose = False
    389        if (
    390            kwargs.get("log_mach_verbose")
    391            or kwargs.get("log_tbpl_level") == "debug"
    392            or kwargs.get("log_mach_level") == "debug"
    393            or kwargs.get("log_raw_level") == "debug"
    394        ):
    395            verbose = True
    396        if not verify_android_device(
    397            build_obj,
    398            install=install,
    399            app=kwargs["binary"],
    400            verbose=verbose,
    401            xre=True,
    402        ):  # Equivalent to 'run_local' = True.
    403            print(
    404                "****************************************************************************"
    405            )
    406            print(
    407                "Unable to verify device, please check your attached/connected android device"
    408            )
    409            print(
    410                "****************************************************************************"
    411            )
    412            return 1
    413        # Disable fission until geckoview supports fission by default.
    414        # Need fission on Android? Use '--setpref fission.autostart=true'
    415        kwargs["fission"] = False
    416 
    417    # Remove mach global arguments from sys.argv to prevent them
    418    # from being consumed by raptor. Treat any item in sys.argv
    419    # occuring before "raptor" as a mach global argument.
    420    argv = []
    421    in_mach = True
    422    for arg in sys.argv:
    423        if not in_mach:
    424            argv.append(arg)
    425        if arg.startswith("raptor"):
    426            in_mach = False
    427 
    428    raptor = command_context._spawn(RaptorRunner)
    429 
    430    try:
    431        return raptor.run_test(argv, kwargs)
    432    except BinaryNotFoundException as e:
    433        command_context.log(
    434            logging.ERROR, "raptor", {"error": str(e)}, "ERROR: {error}"
    435        )
    436        command_context.log(logging.INFO, "raptor", {"help": e.help()}, "{help}")
    437        return 1
    438    except Exception as e:
    439        print(repr(e))
    440        return 1
    441 
    442 
    443 @Command(
    444    "raptor-test",
    445    category="testing",
    446    description="Run Raptor performance tests.",
    447    parser=create_parser,
    448 )
    449 def run_raptor_test(command_context, **kwargs):
    450    return run_raptor(command_context, **kwargs)
    451 
    452 
    453 class PythonVersionException(Exception):
    454    pass