diff options
Diffstat (limited to 'python/mozboot/mozboot/osx.py')
-rw-r--r-- | python/mozboot/mozboot/osx.py | 310 |
1 files changed, 310 insertions, 0 deletions
diff --git a/python/mozboot/mozboot/osx.py b/python/mozboot/mozboot/osx.py new file mode 100644 index 0000000000..8cd180f4ab --- /dev/null +++ b/python/mozboot/mozboot/osx.py @@ -0,0 +1,310 @@ +# 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 platform +import subprocess +import sys +import tempfile +from urllib.request import urlopen + +import certifi +from mach.util import to_optional_path, to_optional_str +from mozfile import which +from packaging.version import Version + +from mozboot.base import BaseBootstrapper + +HOMEBREW_BOOTSTRAP = ( + "https://raw.githubusercontent.com/Homebrew/install/master/install.sh" +) + +BREW_INSTALL = """ +We will install the Homebrew package manager to install required packages. + +You will be prompted to install Homebrew with its default settings. If you +would prefer to do this manually, hit CTRL+c, install Homebrew yourself, ensure +"brew" is in your $PATH, and relaunch bootstrap. +""" + +BREW_PACKAGES = """ +We are now installing all required packages via Homebrew. You will see a lot of +output as packages are built. +""" + +NO_BREW_INSTALLED = "It seems you don't have Homebrew installed." + + +class OSXAndroidBootstrapper(object): + def install_mobile_android_packages(self, mozconfig_builder, artifact_mode=False): + os_arch = platform.machine() + if os_arch != "x86_64" and os_arch != "arm64": + raise Exception( + "You need a 64-bit version of Mac OS X to build " + "GeckoView/Firefox for Android." + ) + + from mozboot import android + + android.ensure_android( + "macosx", + os_arch, + artifact_mode=artifact_mode, + no_interactive=self.no_interactive, + ) + + if os_arch == "x86_64" or os_arch == "x86": + android.ensure_android( + "macosx", + os_arch, + system_images_only=True, + artifact_mode=artifact_mode, + no_interactive=self.no_interactive, + avd_manifest_path=android.AVD_MANIFEST_X86_64, + ) + android.ensure_android( + "macosx", + os_arch, + system_images_only=True, + artifact_mode=artifact_mode, + no_interactive=self.no_interactive, + avd_manifest_path=android.AVD_MANIFEST_ARM, + ) + else: + android.ensure_android( + "macosx", + os_arch, + system_images_only=True, + artifact_mode=artifact_mode, + no_interactive=self.no_interactive, + avd_manifest_path=android.AVD_MANIFEST_ARM64, + ) + + def ensure_mobile_android_packages(self): + from mozboot import android + + arch = platform.machine() + android.ensure_java("macosx", arch) + + if arch == "x86_64" or arch == "x86": + self.install_toolchain_artifact(android.MACOS_X86_64_ANDROID_AVD) + self.install_toolchain_artifact(android.MACOS_ARM_ANDROID_AVD) + elif arch == "arm64": + # The only emulator supported on Apple Silicon is the Arm64 one. + self.install_toolchain_artifact(android.MACOS_ARM64_ANDROID_AVD) + + def install_mobile_android_artifact_mode_packages(self, mozconfig_builder): + self.install_mobile_android_packages(mozconfig_builder, artifact_mode=True) + + def generate_mobile_android_mozconfig(self): + return self._generate_mobile_android_mozconfig() + + def generate_mobile_android_artifact_mode_mozconfig(self): + return self._generate_mobile_android_mozconfig(artifact_mode=True) + + def _generate_mobile_android_mozconfig(self, artifact_mode=False): + from mozboot import android + + return android.generate_mozconfig("macosx", artifact_mode=artifact_mode) + + +def ensure_command_line_tools(): + # We need either the command line tools or Xcode (one is sufficient). + # Python 3, required to run this code, is not installed by default on macos + # as of writing (macos <= 11.x). + # There are at least 5 different ways to obtain it: + # - macports + # - homebrew + # - command line tools + # - Xcode + # - python.org + # The first two require to install the command line tools. + # So only in the last case we may not have command line tools or xcode + # available. + # When the command line tools are installed, `xcode-select --print-path` + # prints their path. + # When Xcode is installed, `xcode-select --print-path` prints its path. + # When neither is installed, `xcode-select --print-path` prints an error + # to stderr and nothing to stdout. + # So in the rare case where we detect neither the command line tools or + # Xcode is installed, we trigger an intall of the command line tools + # (via `xcode-select --install`). + proc = subprocess.run( + ["xcode-select", "--print-path"], + stdout=subprocess.PIPE, + stderr=subprocess.DEVNULL, + ) + if not proc.stdout: + subprocess.run(["xcode-select", "--install"], check=True) + # xcode-select --install triggers a separate process to be started by + # launchd, and tracking its successful outcome would require something + # like figuring its pid and using kqueue to get a notification when it + # finishes. Considering how unlikely it is that someone would end up + # here in the first place, we just bail out. + print("Please follow the command line tools installer instructions") + print("and rerun `./mach bootstrap` when it's finished.") + sys.exit(1) + + +class OSXBootstrapperLight(OSXAndroidBootstrapper, BaseBootstrapper): + def __init__(self, version, **kwargs): + BaseBootstrapper.__init__(self, **kwargs) + + def install_system_packages(self): + ensure_command_line_tools() + + # All the installs below are assumed to be handled by mach configure/build by + # default, which is true for arm64. + def install_browser_packages(self, mozconfig_builder): + pass + + def install_browser_artifact_mode_packages(self, mozconfig_builder): + pass + + +class OSXBootstrapper(OSXAndroidBootstrapper, BaseBootstrapper): + def __init__(self, version, **kwargs): + BaseBootstrapper.__init__(self, **kwargs) + + self.os_version = Version(version) + + if self.os_version < Version("10.6"): + raise Exception("OS X 10.6 or above is required.") + + self.minor_version = version.split(".")[1] + + def install_system_packages(self): + ensure_command_line_tools() + + self.ensure_homebrew_installed() + _, hg_modern, _ = self.is_mercurial_modern() + if not hg_modern: + print( + "Mercurial wasn't found or is not sufficiently modern. " + "It will be installed with brew" + ) + + packages = ["git", "gnu-tar", "terminal-notifier", "watchman"] + if not hg_modern: + packages.append("mercurial") + self._ensure_homebrew_packages(packages) + + def install_browser_packages(self, mozconfig_builder): + pass + + def install_browser_artifact_mode_packages(self, mozconfig_builder): + pass + + def _ensure_homebrew_found(self): + self.brew = to_optional_path(which("brew")) + + return self.brew is not None + + def _ensure_homebrew_packages(self, packages, is_for_cask=False): + package_type_flag = "--cask" if is_for_cask else "--formula" + self.ensure_homebrew_installed() + + def create_homebrew_cmd(*parameters): + base_cmd = [to_optional_str(self.brew)] + base_cmd.extend(parameters) + return base_cmd + [package_type_flag] + + installed = set( + subprocess.check_output( + create_homebrew_cmd("list"), universal_newlines=True + ).split() + ) + outdated = set( + subprocess.check_output( + create_homebrew_cmd("outdated", "--quiet"), universal_newlines=True + ).split() + ) + + to_install = set(package for package in packages if package not in installed) + to_upgrade = set(package for package in packages if package in outdated) + + if to_install or to_upgrade: + print(BREW_PACKAGES) + if to_install: + subprocess.check_call(create_homebrew_cmd("install") + list(to_install)) + if to_upgrade: + subprocess.check_call(create_homebrew_cmd("upgrade") + list(to_upgrade)) + + def _ensure_homebrew_casks(self, casks): + self._ensure_homebrew_found() + + known_taps = subprocess.check_output([to_optional_str(self.brew), "tap"]) + + # Ensure that we can access old versions of packages. + if b"homebrew/cask-versions" not in known_taps: + subprocess.check_output( + [to_optional_str(self.brew), "tap", "homebrew/cask-versions"] + ) + + # "caskroom/versions" has been renamed to "homebrew/cask-versions", so + # it is safe to remove the old tap. Removing the old tap is necessary + # to avoid the error "Cask [name of cask] exists in multiple taps". + # See https://bugzilla.mozilla.org/show_bug.cgi?id=1544981 + if b"caskroom/versions" in known_taps: + subprocess.check_output( + [to_optional_str(self.brew), "untap", "caskroom/versions"] + ) + + self._ensure_homebrew_packages(casks, is_for_cask=True) + + def ensure_homebrew_browser_packages(self): + # TODO: Figure out what not to install for artifact mode + packages = ["yasm"] + self._ensure_homebrew_packages(packages) + + def ensure_homebrew_installed(self): + """ + Search for Homebrew in sys.path, if not found, prompt the user to install it. + Then assert our PATH ordering is correct. + """ + homebrew_found = self._ensure_homebrew_found() + if not homebrew_found: + self.install_homebrew() + + def ensure_sccache_packages(self): + from mozboot import sccache + + self.install_toolchain_artifact(sccache.RUSTC_DIST_TOOLCHAIN, no_unpack=True) + self.install_toolchain_artifact(sccache.CLANG_DIST_TOOLCHAIN, no_unpack=True) + + def install_homebrew(self): + print(BREW_INSTALL) + bootstrap = urlopen( + url=HOMEBREW_BOOTSTRAP, cafile=certifi.where(), timeout=20 + ).read() + with tempfile.NamedTemporaryFile() as tf: + tf.write(bootstrap) + tf.flush() + + subprocess.check_call(["bash", tf.name]) + + homebrew_found = self._ensure_homebrew_found() + if not homebrew_found: + print( + "Homebrew was just installed but can't be found on PATH. " + "Please file a bug." + ) + sys.exit(1) + + def _update_package_manager(self): + subprocess.check_call([to_optional_str(self.brew), "-v", "update"]) + + def _upgrade_package(self, package): + self._ensure_homebrew_installed() + + try: + subprocess.check_output( + [to_optional_str(self.brew), "-v", "upgrade", package], + stderr=subprocess.STDOUT, + ) + except subprocess.CalledProcessError as e: + if b"already installed" not in e.output: + raise + + def upgrade_mercurial(self, current): + self._upgrade_package("mercurial") |