2 # This Source Code Form is subject to the terms of the Mozilla Public
3 # License, v. 2.0. If a copy of the MPL was not distributed with this file,
4 # You can obtain one at http://mozilla.org/MPL/2.0/.
6 # This script provides one-line bootstrap support to configure systems to build
7 # the tree. It does so by cloning the repo before calling directly into `mach
10 # Note that this script can't assume anything in particular about the host
11 # Python environment (except that it's run with a sufficiently recent version of
12 # Python 3), so we are restricted to stdlib modules.
16 major
, minor
= sys
.version_info
[:2]
17 if (major
< 3) or (major
== 3 and minor
< 7):
19 "Bootstrap currently only runs on Python 3.7+."
20 "Please try re-running with python3.7+."
29 from optparse
import OptionParser
30 from pathlib
import Path
32 CLONE_MERCURIAL_PULL_FAIL
= """
33 Failed to pull from hg.mozilla.org.
35 This is most likely because of unstable network connection.
36 Try running `cd %s && hg pull https://hg.mozilla.org/mozilla-unified` manually,
37 or download a mercurial bundle and use it:
38 https://firefox-source-docs.mozilla.org/contributing/vcs/mercurial_bundles.html"""
40 WINDOWS
= sys
.platform
.startswith("win32") or sys
.platform
.startswith("msys")
41 VCS_HUMAN_READABLE
= {
48 """Python implementation of which.
50 It returns the path of an executable or None if it couldn't be found.
52 search_dirs
= os
.environ
["PATH"].split(os
.pathsep
)
53 potential_names
= [name
]
55 potential_names
.insert(0, name
+ ".exe")
57 for path
in search_dirs
:
58 for executable_name
in potential_names
:
59 test
= Path(path
) / executable_name
60 if test
.is_file() and os
.access(test
, os
.X_OK
):
66 def validate_clone_dest(dest
: Path
):
73 print(f
"ERROR! Destination {dest} exists but is not a directory.")
76 if not any(dest
.iterdir()):
79 print(f
"ERROR! Destination directory {dest} exists but is nonempty.")
81 f
"To re-bootstrap the existing checkout, go into '{dest}' and run './mach bootstrap'."
86 def input_clone_dest(vcs
, no_interactive
):
87 repo_name
= "mozilla-unified"
88 print(f
"Cloning into {repo_name} using {VCS_HUMAN_READABLE[vcs]}...")
91 if not no_interactive
:
93 f
"Destination directory for clone (leave empty to use "
94 f
"default destination of {repo_name}): "
98 dest
= validate_clone_dest(Path(dest
).expanduser())
105 def hg_clone_firefox(hg
: Path
, dest
: Path
, head_repo
, head_rev
):
106 # We create an empty repo then modify the config before adding data.
107 # This is necessary to ensure storage settings are optimally
111 # The unified repo is generaldelta, so ensure the client is as
114 "format.generaldelta=true",
118 res
= subprocess
.call(args
)
120 print("unable to create destination repo; please try cloning manually")
123 # Strictly speaking, this could overwrite a config based on a template
124 # the user has installed. Let's pretend this problem doesn't exist
125 # unless someone complains about it.
126 with
open(dest
/ ".hg" / "hgrc", "a") as fh
:
127 fh
.write("[paths]\n")
128 fh
.write("default = https://hg.mozilla.org/mozilla-unified\n")
131 # The server uses aggressivemergedeltas which can blow up delta chain
132 # length. This can cause performance to tank due to delta chains being
133 # too long. Limit the delta chain length to something reasonable
134 # to bound revlog read time.
135 fh
.write("[format]\n")
136 fh
.write("# This is necessary to keep performance in check\n")
137 fh
.write("maxchainlen = 10000\n")
139 # Pulling a specific revision into an empty repository induces a lot of
140 # load on the Mercurial server, so we always pull from mozilla-unified (which,
141 # when done from an empty repository, is equivalent to a clone), and then pull
142 # the specific revision we want (if we want a specific one, otherwise we just
143 # use the "central" bookmark), at which point it will be an incremental pull,
144 # that the server can process more easily.
145 # This is the same thing that robustcheckout does on automation.
146 res
= subprocess
.call(
147 [str(hg
), "pull", "https://hg.mozilla.org/mozilla-unified"], cwd
=str(dest
)
149 if not res
and head_repo
:
150 res
= subprocess
.call(
151 [str(hg
), "pull", head_repo
, "-r", head_rev
], cwd
=str(dest
)
155 print(CLONE_MERCURIAL_PULL_FAIL
% dest
)
158 head_rev
= head_rev
or "central"
159 print(f
'updating to "{head_rev}" - the development head of Gecko and Firefox')
160 res
= subprocess
.call([str(hg
), "update", "-r", head_rev
], cwd
=str(dest
))
163 f
"error updating; you will need to `cd {dest} && hg update -r central` "
169 def git_clone_firefox(git
: Path
, dest
: Path
, watchman
: Path
, head_repo
, head_rev
):
172 env
= dict(os
.environ
)
174 cinnabar
= which("git-cinnabar")
176 from urllib
.request
import urlopen
178 cinnabar_url
= "https://github.com/glandium/git-cinnabar/"
179 # If git-cinnabar isn't installed already, that's fine; we can
180 # download a temporary copy. `mach bootstrap` will install a copy
181 # in the state dir; we don't want to copy all that logic to this
182 # tiny bootstrapping script.
183 tempdir
= Path(tempfile
.mkdtemp())
184 with
open(tempdir
/ "download.py", "wb") as fh
:
186 urlopen(f
"{cinnabar_url}/raw/master/download.py"), fh
189 subprocess
.check_call(
190 [sys
.executable
, str(tempdir
/ "download.py")],
193 env
["PATH"] = str(tempdir
) + os
.pathsep
+ env
["PATH"]
195 "WARNING! git-cinnabar is required for Firefox development "
196 "with git. After the clone is complete, the bootstrapper "
197 "will ask if you would like to configure git; answer yes, "
198 "and be sure to add git-cinnabar to your PATH according to "
199 "the bootstrapper output."
202 # We're guaranteed to have `git-cinnabar` installed now.
203 # Configure git per the git-cinnabar requirements.
204 subprocess
.check_call(
209 "hg::https://hg.mozilla.org/mozilla-unified",
214 subprocess
.check_call(
215 [str(git
), "config", "fetch.prune", "true"], cwd
=str(dest
), env
=env
217 subprocess
.check_call(
218 [str(git
), "config", "pull.ff", "only"], cwd
=str(dest
), env
=env
222 subprocess
.check_call(
223 [str(git
), "cinnabar", "fetch", f
"hg::{head_repo}", head_rev
],
228 subprocess
.check_call(
229 [str(git
), "checkout", "FETCH_HEAD" if head_rev
else "bookmarks/central"],
234 watchman_sample
= dest
/ ".git/hooks/fsmonitor-watchman.sample"
235 # Older versions of git didn't include fsmonitor-watchman.sample.
236 if watchman
and watchman_sample
.exists():
237 print("Configuring watchman")
238 watchman_config
= dest
/ ".git/hooks/query-watchman"
239 if not watchman_config
.exists():
240 print(f
"Copying {watchman_sample} to {watchman_config}")
243 ".git/hooks/fsmonitor-watchman.sample",
244 ".git/hooks/query-watchman",
246 subprocess
.check_call(copy_args
, cwd
=str(dest
))
252 ".git/hooks/query-watchman",
254 subprocess
.check_call(config_args
, cwd
=str(dest
), env
=env
)
258 shutil
.rmtree(str(tempdir
))
261 def add_microsoft_defender_antivirus_exclusions(dest
, no_system_changes
):
262 if no_system_changes
:
268 powershell_exe
= which("powershell")
270 if not powershell_exe
:
273 def print_attempt_exclusion(path
):
275 f
"Attempting to add exclusion path to Microsoft Defender Antivirus for: {path}"
278 powershell_exe
= str(powershell_exe
)
281 # mozilla-unified / clone dest
282 repo_dir
= Path
.cwd() / dest
283 paths
.append(repo_dir
)
284 print_attempt_exclusion(repo_dir
)
287 mozillabuild_dir
= os
.getenv("MOZILLABUILD")
289 paths
.append(mozillabuild_dir
)
290 print_attempt_exclusion(mozillabuild_dir
)
293 mozbuild_dir
= Path
.home() / ".mozbuild"
294 paths
.append(mozbuild_dir
)
295 print_attempt_exclusion(mozbuild_dir
)
297 args
= ";".join(f
"Add-MpPreference -ExclusionPath '{path}'" for path
in paths
)
298 command
= f
'-Command "{args}"'
300 # This will attempt to run as administrator by triggering a UAC prompt
301 # for admin credentials. If "No" is selected, no exclusions are added.
302 ctypes
.windll
.shell32
.ShellExecuteW(None, "runas", powershell_exe
, command
, None, 0)
307 no_interactive
= options
.no_interactive
308 no_system_changes
= options
.no_system_changes
313 print("Mercurial is not installed. Mercurial is required to clone Firefox.")
315 # We're going to recommend people install the Mercurial package with
316 # pip3. That will work if `pip3` installs binaries to a location
317 # that's in the PATH, but it might not be. To help out, if we CAN
318 # import "mercurial" (in which case it's already been installed),
319 # offer that as a solution.
320 import mercurial
# noqa: F401
323 "Hint: have you made sure that Mercurial is installed to a "
324 "location in your PATH?"
327 print("Try installing hg with `pip3 install Mercurial`.")
333 print("Git is not installed.")
334 print("Try installing git using your system package manager.")
337 dest
= input_clone_dest(vcs
, no_interactive
)
341 add_microsoft_defender_antivirus_exclusions(dest
, no_system_changes
)
343 print(f
"Cloning Firefox {VCS_HUMAN_READABLE[vcs]} repository to {dest}")
345 head_repo
= os
.environ
.get("GECKO_HEAD_REPOSITORY")
346 head_rev
= os
.environ
.get("GECKO_HEAD_REV")
349 return hg_clone_firefox(binary
, dest
, head_repo
, head_rev
)
351 watchman
= which("watchman")
352 return git_clone_firefox(binary
, dest
, watchman
, head_repo
, head_rev
)
355 def bootstrap(srcdir
: Path
, application_choice
, no_interactive
, no_system_changes
):
356 args
= [sys
.executable
, "mach"]
359 # --no-interactive is a global argument, not a command argument,
360 # so it needs to be specified before "bootstrap" is appended.
361 args
+= ["--no-interactive"]
363 args
+= ["bootstrap"]
365 if application_choice
:
366 args
+= ["--application-choice", application_choice
]
367 if no_system_changes
:
368 args
+= ["--no-system-changes"]
370 print("Running `%s`" % " ".join(args
))
371 return subprocess
.call(args
, cwd
=str(srcdir
))
375 parser
= OptionParser()
377 "--application-choice",
378 dest
="application_choice",
379 help='Pass in an application choice (see "APPLICATIONS" in '
380 "python/mozboot/mozboot/bootstrap.py) instead of using the "
381 "default interactive prompt.",
387 choices
=["git", "hg"],
388 help="VCS (hg or git) to use for downloading the source code, "
389 "instead of using the default interactive prompt.",
393 dest
="no_interactive",
395 help="Answer yes to any (Y/n) interactive prompts.",
398 "--no-system-changes",
399 dest
="no_system_changes",
401 help="Only executes actions that leave the system " "configuration alone.",
404 options
, leftover
= parser
.parse_args(args
)
406 srcdir
= clone(options
)
409 print("Clone complete.")
411 "If you need to run the tooling bootstrapping again, "
412 "then consider running './mach bootstrap' instead."
414 if not options
.no_interactive
:
415 remove_bootstrap_file
= input(
416 "Unless you are going to have more local copies of Firefox source code, "
417 "this 'bootstrap.py' file is no longer needed and can be deleted. "
418 "Clean up the bootstrap.py file? (Y/n)"
420 if not remove_bootstrap_file
:
421 remove_bootstrap_file
= "y"
422 if options
.no_interactive
or remove_bootstrap_file
== "y":
424 Path(sys
.argv
[0]).unlink()
425 except FileNotFoundError
:
426 print("File could not be found !")
429 options
.application_choice
,
430 options
.no_interactive
,
431 options
.no_system_changes
,
434 print("Could not bootstrap Firefox! Consider filing a bug.")
438 if __name__
== "__main__":
439 sys
.exit(main(sys
.argv
))