#!/usr/bin/env python3 # 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/. import os import platform import sys import subprocess import traceback from textwrap import dedent, fill MIN_PYTHON_VERSION = (3, 8) MAX_PYTHON_VERSION_TO_CONSIDER = (3, 11) def load_mach(dir_path, mach_path, args): # Defer import of "importlib.util" until after Python version check has happened # so that Python 2 usages fail gracefully. import importlib.util spec = importlib.util.spec_from_file_location("mach_initialize", mach_path) mach_initialize = importlib.util.module_from_spec(spec) spec.loader.exec_module(mach_initialize) return mach_initialize.initialize(dir_path, args) def check_and_get_mach(dir_path, args): initialize_paths = ( # Run Thunderbird's mach_initialize.py if it exists "comm/build/mach_initialize.py", "build/mach_initialize.py", # test package initialize "tools/mach_initialize.py", ) for initialize_path in initialize_paths: mach_path = os.path.join(dir_path, initialize_path) if os.path.isfile(mach_path): return load_mach(dir_path, mach_path, args) return None def find_alternate_python3_executables(): for i in range(MIN_PYTHON_VERSION[1], MAX_PYTHON_VERSION_TO_CONSIDER[1] + 1): potential_python_binary = f"python3.{i}" if os.name == "nt": potential_python_binary += ".exe" try: out = subprocess.run( [potential_python_binary, "--version"], stdout=subprocess.PIPE, stderr=subprocess.PIPE, encoding="UTF-8", ) binary_minor_version = int(out.stdout[9:11].strip(".")) if binary_minor_version >= MIN_PYTHON_VERSION[1]: yield potential_python_binary except Exception: pass def try_alternate_python3_executables(args): for potential_python_binary in find_alternate_python3_executables(): try: print( f"We found '{potential_python_binary}' and will attempt to re-run Mach with it." ) os.execvp( potential_python_binary, [potential_python_binary] + ["mach"] + args ) except Exception: # We don't really care what goes wrong, just don't let it bubble up # If we can't successfully launch with a different python3 binary # we will just print the normal help messages. pass def main(args): # Ensure we are running Python 3.8+. We run this check as soon as # possible to avoid a cryptic import/usage error. if sys.version_info < MIN_PYTHON_VERSION: print( f"Python {MIN_PYTHON_VERSION[0]}.{MIN_PYTHON_VERSION[1]}+ is required to run mach." ) print("You are running Mach with Python {0}".format(platform.python_version())) try_alternate_python3_executables(args) if sys.platform.startswith("linux"): print( dedent( """ See https://firefox-source-docs.mozilla.org/setup/linux_build.html#installingpython for guidance on how to install Python on your system. """ ).strip() ) elif sys.platform.startswith("darwin"): print( dedent( """ See https://firefox-source-docs.mozilla.org/setup/macos_build.html for guidance on how to prepare your system to build Firefox. Perhaps you need to update Xcode, or install Python using brew? """ ).strip() ) elif "MOZILLABUILD" in os.environ and os.environ.get("TERM"): print( dedent( """ Python is provided by MozillaBuild; ensure your MozillaBuild installation is up to date. See https://firefox-source-docs.mozilla.org/setup/windows_build.html#install-mozillabuild for details. """ ).strip() ) elif sys.platform.startswith("win"): print( dedent( """ You probably want to be interacting with Mach from within MozillaBuild, see https://firefox-source-docs.mozilla.org/setup/windows_build.html for details. If you are deliberately using Mach from outside MozillaBuild, then see https://firefox-source-docs.mozilla.org/mach/windows-usage-outside-mozillabuild.html#install-python for guidance on installing native Python on your system. """ ).strip() ) else: print( dedent( """ We do not have specific instructions for your platform on how to install Python. You may find Pyenv (https://github.com/pyenv/pyenv) helpful, if your system package manager does not provide a way to install a recent enough Python 3. """ ).strip() ) sys.exit(1) # XCode python sets __PYVENV_LAUNCHER__, which overrides the executable # used when a python subprocess is created. This is an issue when we want # to run using our virtualenv python executables. # In future Python relases, __PYVENV_LAUNCHER__ will be cleared before # application code (mach) is started. # https://github.com/python/cpython/pull/9516 os.environ.pop("__PYVENV_LAUNCHER__", None) try: mach = check_and_get_mach(os.path.dirname(os.path.realpath(__file__)), args) if not mach: print("Could not run mach: No mach source directory found.") sys.exit(1) sys.exit(mach.run(args)) except (KeyboardInterrupt, SystemExit): raise except Exception as e: if sys.version_info >= ( MAX_PYTHON_VERSION_TO_CONSIDER[0], MAX_PYTHON_VERSION_TO_CONSIDER[1] + 1, ): traceback.print_exc() print() print("---") print() print( fill( dedent( f"""\ Note that you are running Mach with Python {platform.python_version()}, which is higher than the highest known working version of Python for Mach. Consider running Mach with Python {MAX_PYTHON_VERSION_TO_CONSIDER[0]}.{MAX_PYTHON_VERSION_TO_CONSIDER[1]} or lower.""" ) ) ) try: alternative = next(find_alternate_python3_executables()) print() print("Running the following command may solve your issue:") print() print(f" {alternative} {sys.argv[0]} {' '.join(args)}") print() except StopIteration: pass sys.exit(1) else: raise if __name__ == "__main__": main(sys.argv[1:])