# This Source Code Form is subject to the terms of the Mozilla Public# License, v. 2.0. If a copy of the MPL was not distributed with this# file, You can obtain one at http://mozilla.org/MPL/2.0/.importmathimportosimportshutilimportsysfromimportlib.abcimportMetaPathFinderfrompathlibimportPathSTATE_DIR_FIRST_RUN="""Mach and the build system store shared state in a common directoryon the filesystem. The following directory will be created: {}If you would like to use a different directory, rename or move it to yourdesired location, and set the MOZBUILD_STATE_PATH environment variableaccordingly.""".strip()CATEGORIES={"build":{"short":"Build Commands","long":"Interact with the build system","priority":80,},"post-build":{"short":"Post-build Commands","long":"Common actions performed after completing a build.","priority":70,},"testing":{"short":"Testing","long":"Run tests.","priority":60,},"ci":{"short":"CI","long":"Taskcluster commands","priority":59,},"devenv":{"short":"Development Environment","long":"Set up and configure your development environment.","priority":50,},"build-dev":{"short":"Low-level Build System Interaction","long":"Interact with specific parts of the build system.","priority":20,},"misc":{"short":"Potpourri","long":"Potent potables and assorted snacks.","priority":10,},"release":{"short":"Release automation","long":"Commands for used in release automation.","priority":5,},"disabled":{"short":"Disabled","long":"The disabled commands are hidden by default. Use -v to display them. ""These commands are unavailable for your current context, "'run "mach <command>" to see why.',"priority":0,},}def_activate_python_environment(topsrcdir,get_state_dir,quiet):frommach.siteimportMachSiteManagermach_environment=MachSiteManager.from_environment(topsrcdir,get_state_dir,quiet=quiet)mach_environment.activate()def_maybe_activate_mozillabuild_environment():ifsys.platform!="win32":returnmozillabuild=Path(os.environ.get("MOZILLABUILD",r"C:\mozilla-build"))os.environ.setdefault("MOZILLABUILD",str(mozillabuild))assertmozillabuild.exists(),(f'MozillaBuild was not found at "{mozillabuild}".\n'"If it's installed in a different location, please "'set the "MOZILLABUILD" environment variable '"accordingly.")use_msys2=(mozillabuild/"msys2").exists()ifuse_msys2:mozillabuild_msys_tools_path=mozillabuild/"msys2"/"usr"/"bin"else:mozillabuild_msys_tools_path=mozillabuild/"msys"/"bin"paths_to_add=[mozillabuild_msys_tools_path,mozillabuild/"bin"]existing_paths=[Path(p)forpinos.environ.get("PATH","").split(os.pathsep)]fornew_pathinpaths_to_add:ifnew_pathnotinexisting_paths:os.environ["PATH"]+=f"{os.pathsep}{new_path}"defcheck_for_spaces(topsrcdir):if" "intopsrcdir:raiseException(f"Your checkout at path '{topsrcdir}' contains a space, which "f"is not supported. Please move it to somewhere that does not "f"have a space in the path before rerunning mach.")mozillabuild_dir=os.environ.get("MOZILLABUILD","")ifsys.platform=="win32"and" "inmozillabuild_dir:raiseException(f"Your installation of MozillaBuild appears to be installed on a path that "f"contains a space ('{mozillabuild_dir}') which is not supported. Please "f"reinstall MozillaBuild on a path without a space and restart your shell"f"from the new installation.")definitialize(topsrcdir,args=()):# This directory was deleted in bug 1666345, but there may be some ignored# files here. We can safely just delete it for the user so they don't have# to clean the repo themselves.deleted_dir=os.path.join(topsrcdir,"third_party","python","psutil")ifos.path.exists(deleted_dir):shutil.rmtree(deleted_dir,ignore_errors=True)# We need the "mach" module to access the logic to parse virtualenv# requirements. Since that depends on "packaging", we add it to the path too.# We need filelock for solving a virtualenv race conditionsys.path[0:0]=[os.path.join(topsrcdir,module)formodulein(os.path.join("python","mach"),os.path.join("testing","mozbase","mozfile"),os.path.join("third_party","python","packaging"),os.path.join("third_party","python","filelock"),)]frommach.utilimportget_state_dir,get_virtualenv_base_dir,setenvstate_dir=_create_state_dir()check_for_spaces(topsrcdir)# See bug 1874208:# Status messages from site.py break usages of `./mach environment`.# We pass `quiet` only for it to work around this, so that all other# commands can still write status messages.ifargsand(args[0]=="environment"or"--quiet"inargs):quiet=Trueelse:quiet=False# normpath state_dir to normalize msys-style slashes._activate_python_environment(topsrcdir,lambda:os.path.normpath(get_state_dir(True,topsrcdir=topsrcdir)),quiet=quiet,)_maybe_activate_mozillabuild_environment()importmach.mainfrommach.command_utilimport(MACH_COMMANDS,DetermineCommandVenvAction,load_commands_from_spec,)frommach.mainimportget_argument_parser# Set a reasonable limit to the number of open files.## Some linux systems set `ulimit -n` to a very high number, which works# well for systems that run servers, but this setting causes performance# problems when programs close file descriptors before forking, like# Python's `subprocess.Popen(..., close_fds=True)` (close_fds=True is the# default in Python 3), or Rust's stdlib. In some cases, Firefox does the# same thing when spawning processes. We would prefer to lower this limit# to avoid such performance problems; processes spawned by `mach` will# inherit the limit set here.## The Firefox build defaults the soft limit to 1024, except for builds that# do LTO, where the soft limit is 8192. We're going to default to the# latter, since people do occasionally do LTO builds on their local# machines, and requiring them to discover another magical setting after# setting up an LTO build in the first place doesn't seem good.## This code mimics the code in taskcluster/scripts/run-task.try:importresource# Keep the hard limit the same, though, allowing processes to change# their soft limit if they need to (Firefox does, for instance).(soft,hard)=resource.getrlimit(resource.RLIMIT_NOFILE)# Permit people to override our default limit if necessary via# MOZ_LIMIT_NOFILE, which is the same variable `run-task` uses.limit=os.environ.get("MOZ_LIMIT_NOFILE")iflimit:limit=int(limit)else:# If no explicit limit is given, use our default if it's less than# the current soft limit. For instance, the default on macOS is# 256, so we'd pick that rather than our default.limit=min(soft,8192)# Now apply the limit, if it's different from the original one.iflimit!=soft:resource.setrlimit(resource.RLIMIT_NOFILE,(limit,hard))exceptImportError:# The resource module is UNIX only.passdefresolve_repository():importmozversioncontroltry:# This API doesn't respect the vcs binary choices from configure.# If we ever need to use the VCS binary here, consider something# more robust.returnmozversioncontrol.get_repository_object(path=topsrcdir)except(mozversioncontrol.InvalidRepoPath,mozversioncontrol.MissingVCSTool):returnNonedefpre_dispatch_handler(context,handler,args):# If --disable-tests flag was enabled in the mozconfig used to compile# the build, tests will be disabled. Instead of trying to run# nonexistent tests then reporting a failure, this will prevent mach# from progressing beyond this point.ifhandler.category=="testing"andnothandler.ok_if_tests_disabled:frommozbuild.baseimportBuildEnvironmentNotFoundExceptiontry:frommozbuild.baseimportMozbuildObject# all environments should have an instance of build object.build=MozbuildObject.from_environment()ifbuildisnotNoneandnotgetattr(build,"substs",{"ENABLE_TESTS":True}).get("ENABLE_TESTS"):print("Tests have been disabled with --disable-tests.\n"+"Remove the flag, and re-compile to enable tests.")sys.exit(1)exceptBuildEnvironmentNotFoundException:# likely automation environment, so do nothing.passdefpost_dispatch_handler(context,handler,instance,success,start_time,end_time,depth,args):"""Perform global operations after command dispatch. For now, we will use this to handle build system telemetry. """# Don't finalize telemetry data if this mach command was invoked as part of# another mach command.ifdepth!=1:return_finalize_telemetry_glean(context.telemetry,handler.name=="bootstrap",success)defpopulate_context(key=None):ifkeyisNone:returnifkey=="state_dir":returnstate_dirifkey=="local_state_dir":returnget_state_dir(specific_to_topsrcdir=True)ifkey=="topdir":returntopsrcdirifkey=="pre_dispatch_handler":returnpre_dispatch_handlerifkey=="post_dispatch_handler":returnpost_dispatch_handlerifkey=="repository":returnresolve_repository()raiseAttributeError(key)# Note which process is top-level so that recursive mach invocations can avoid writing# telemetry data.if"MACH_MAIN_PID"notinos.environ:setenv("MACH_MAIN_PID",str(os.getpid()))driver=mach.main.Mach(os.getcwd())driver.populate_context_handler=populate_contextifnotdriver.settings_paths:# default global machrc locationdriver.settings_paths.append(state_dir)# always load local repository configurationdriver.settings_paths.append(topsrcdir)driver.load_settings()aliases=driver.settings.aliasparser=get_argument_parser(action=DetermineCommandVenvAction,topsrcdir=topsrcdir,)fromargparseimportNamespacefrommach.mainimport(SUGGESTED_COMMANDS_MESSAGE,UNKNOWN_COMMAND_ERROR,UnknownCommandError,)namespace_in=Namespace()setattr(namespace_in,"mach_command_aliases",aliases)try:namespace=parser.parse_args(args,namespace_in)exceptUnknownCommandErrorase:suggestion_message=(SUGGESTED_COMMANDS_MESSAGE%(e.verb,", ".join(e.suggested_commands))ife.suggested_commandselse"")print(UNKNOWN_COMMAND_ERROR%(e.verb,e.command,suggestion_message))sys.exit(1)command_name=getattr(namespace,"command_name",None)site_name=getattr(namespace,"site_name","common")command_site_manager=None# the 'clobber' command needs to run in the 'mach' venv, so we# don't want to activate any other virtualenv for it.ifcommand_name!="clobber":frommach.siteimportCommandSiteManagercommand_site_manager=CommandSiteManager.from_environment(topsrcdir,lambda:os.path.normpath(get_state_dir(True,topsrcdir=topsrcdir)),site_name,get_virtualenv_base_dir(topsrcdir),quiet=quiet,)command_site_manager.activate()forcategory,metainCATEGORIES.items():driver.define_category(category,meta["short"],meta["long"],meta["priority"])# Sparse checkouts may not have all mach_commands.py files. Ignore# errors from missing files. Same for spidermonkey tarballs.repo=resolve_repository()ifrepo!="SOURCE":missing_ok=(repoisnotNoneandrepo.sparse_checkout_present())oros.path.exists(os.path.join(topsrcdir,"INSTALL"))else:missing_ok=()commands_that_need_all_modules_loaded=["busted","help","mach-commands","mach-completion","mach-debug-commands",]defcommands_to_load(top_level_command:str):visited=set()deffind_downstream_commands_recursively(command:str):ifnotMACH_COMMANDS.get(command):returnifcommandinvisited:returnvisited.add(command)forcommand_dependencyinMACH_COMMANDS[command].command_dependencies:find_downstream_commands_recursively(command_dependency)find_downstream_commands_recursively(top_level_command)returnlist(visited)if(command_namenotinMACH_COMMANDSorcommand_nameincommands_that_need_all_modules_loaded):command_modules_to_load=MACH_COMMANDSelse:command_names_to_load=commands_to_load(command_name)command_modules_to_load={command_name:MACH_COMMANDS[command_name]forcommand_nameincommand_names_to_load}driver.command_site_manager=command_site_managerload_commands_from_spec(command_modules_to_load,topsrcdir,missing_ok=missing_ok)returndriverdef_finalize_telemetry_glean(telemetry,is_bootstrap,success):"""Submit telemetry collected by Glean. Finalizes some metrics (command success state and duration, system information) and requests Glean to send the collected data. """frommach.telemetryimportMACH_METRICS_PATHfrommozbuild.telemetryimport(get_cpu_brand,get_distro_and_version,get_psutil_stats,get_shell_info,get_vscode_running,)moz_automation=any(einos.environforein("MOZ_AUTOMATION","TASK_ID"))mach_metrics=telemetry.metrics(MACH_METRICS_PATH)mach_metrics.mach.duration.stop()mach_metrics.mach.success.set(success)mach_metrics.mach.moz_automation.set(moz_automation)system_metrics=mach_metrics.mach.systemcpu_brand=get_cpu_brand()ifcpu_brand:system_metrics.cpu_brand.set(cpu_brand)distro,version=get_distro_and_version()system_metrics.distro.set(distro)system_metrics.distro_version.set(version)vscode_terminal,ssh_connection=get_shell_info()system_metrics.vscode_terminal.set(vscode_terminal)system_metrics.ssh_connection.set(ssh_connection)system_metrics.vscode_running.set(get_vscode_running())has_psutil,logical_cores,physical_cores,memory_total=get_psutil_stats()ifhas_psutil:# psutil may not be available (we may not have been able to download# a wheel or build it from source).system_metrics.logical_cores.add(logical_cores)ifphysical_coresisnotNone:system_metrics.physical_cores.add(physical_cores)ifmemory_totalisnotNone:system_metrics.memory.accumulate(int(math.ceil(float(memory_total)/(1024*1024*1024))))telemetry.submit(is_bootstrap)def_create_state_dir():# Global build system and mach state is stored in a central directory. By# default, this is ~/.mozbuild. However, it can be defined via an# environment variable. We detect first run (by lack of this directory# existing) and notify the user that it will be created. The logic for# creation is much simpler for the "advanced" environment variable use# case. For default behavior, we educate users and give them an opportunity# to react.state_dir=os.environ.get("MOZBUILD_STATE_PATH")ifstate_dir:ifnotos.path.exists(state_dir):print(f"Creating global state directory from environment variable: {state_dir}")else:state_dir=os.path.expanduser("~/.mozbuild")ifnotos.path.exists(state_dir):ifnotos.environ.get("MOZ_AUTOMATION"):print(STATE_DIR_FIRST_RUN.format(state_dir))print(f"Creating default state directory: {state_dir}")os.makedirs(state_dir,mode=0o770,exist_ok=True)returnstate_dir# Hook import such that .pyc/.pyo files without a corresponding .py file in# the source directory are essentially ignored. See further below for details# and caveats.# Objdirs outside the source directory are ignored because in most cases, if# a .pyc/.pyo file exists there, a .py file will be next to it anyways.classFinderHook(MetaPathFinder):def__init__(self,klass):# Assume the source directory is the parent directory of the one# containing this file.self._source_dir=(os.path.normcase(os.path.abspath(os.path.dirname(os.path.dirname(__file__))))+os.sep)self.finder_class=klassdeffind_spec(self,full_name,paths=None,target=None):spec=self.finder_class.find_spec(full_name,paths,target)# Some modules don't have an origin.ifspecisNoneorspec.originisNone:returnspec# Normalize the origin path.path=os.path.normcase(os.path.abspath(spec.origin))# Note: we could avoid normcase and abspath above for non pyc/pyo# files, but those are actually rare, so it doesn't really matter.ifnotpath.endswith((".pyc",".pyo")):returnspec# Ignore modules outside our source directoryifnotpath.startswith(self._source_dir):returnspec# If there is no .py corresponding to the .pyc/.pyo module we're# resolving, remove the .pyc/.pyo file, and try again.ifnotos.path.exists(spec.origin[:-1]):ifos.path.exists(spec.origin):os.remove(spec.origin)spec=self.finder_class.find_spec(full_name,paths,target)returnspec# Additional hook for python >= 3.8's importlib.metadata.classMetadataHook(FinderHook):deffind_distributions(self,*args,**kwargs):returnself.finder_class.find_distributions(*args,**kwargs)defhook(finder):has_find_spec=hasattr(finder,"find_spec")has_find_distributions=hasattr(finder,"find_distributions")ifhas_find_specandhas_find_distributions:returnMetadataHook(finder)elifhas_find_spec:returnFinderHook(finder)returnfindersys.meta_path=[hook(c)forcinsys.meta_path]