summaryrefslogtreecommitdiffstats
path: root/mach
blob: 5ea05fb643c0cb53b17073b5c60a478b3c7a3e2c (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
#!/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:])