summaryrefslogtreecommitdiffstats
path: root/python/mozboot/mozboot/osx.py
blob: 8cd180f4ab08be5bd89d8484bd3d3219d3c6c744 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
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")