1 from __future__ import absolute_import, division, print_function
5 import distutils.version
16 from time import time, sleep
18 # Acquire a lock on the build directory to make sure that
19 # we don't cause a race condition while building
20 # Lock is created in `build_dir/lock.db`
21 def acquire_lock(build_dir):
25 path = os.path.join(build_dir, "lock.db")
27 con = sqlite3.Connection(path, timeout=0)
29 curs.execute("BEGIN EXCLUSIVE")
30 # The lock is released when the cursor is dropped
32 # If the database is busy then lock has already been acquired
33 # so we wait for the lock.
34 # We retry every quarter second so that execution is passed back to python
35 # so that it can handle signals
36 except sqlite3.OperationalError:
39 print("Waiting for lock on build directory")
40 con = sqlite3.Connection(path, timeout=0.25)
44 curs.execute("BEGIN EXCLUSIVE")
46 except sqlite3.OperationalError:
51 print("warning: sqlite3 not available in python, skipping build directory lock")
52 print("please file an issue on rust-lang/rust")
53 print("this is not a problem for non-concurrent x.py invocations")
58 with tempfile.NamedTemporaryFile(delete=False) as temp_file:
59 temp_path = temp_file.name
60 with tarfile.open(temp_path, "w:xz"):
63 except tarfile.CompressionError:
66 def get(base, url, path, checksums, verbose=False, do_verify=True):
67 with tempfile.NamedTemporaryFile(delete=False) as temp_file:
68 temp_path = temp_file.name
72 if url not in checksums:
73 raise RuntimeError("src/stage0.json doesn't contain a checksum for {}".format(url))
74 sha256 = checksums[url]
75 if os.path.exists(path):
76 if verify(path, sha256, False):
78 print("using already-download file", path)
82 print("ignoring already-download file",
83 path, "due to failed verification")
85 download(temp_path, "{}/{}".format(base, url), True, verbose)
86 if do_verify and not verify(temp_path, sha256, verbose):
87 raise RuntimeError("failed verification")
89 print("moving {} to {}".format(temp_path, path))
90 shutil.move(temp_path, path)
92 if os.path.isfile(temp_path):
94 print("removing", temp_path)
98 def download(path, url, probably_big, verbose):
101 _download(path, url, probably_big, verbose, True)
104 print("\nspurious failure, trying again")
105 _download(path, url, probably_big, verbose, False)
108 def _download(path, url, probably_big, verbose, exception):
109 if probably_big or verbose:
110 print("downloading {}".format(url))
111 # see https://serverfault.com/questions/301128/how-to-download
112 if sys.platform == 'win32':
113 run(["PowerShell.exe", "/nologo", "-Command",
114 "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12;",
115 "(New-Object System.Net.WebClient).DownloadFile('{}', '{}')".format(url, path)],
119 if probably_big or verbose:
123 require(["curl", "--version"])
125 "-y", "30", "-Y", "10", # timeout if speed is < 10 bytes/sec for > 30 seconds
126 "--connect-timeout", "30", # timeout if cannot connect within 30 seconds
127 "--retry", "3", "-Sf", "-o", path, url],
132 def verify(path, expected, verbose):
133 """Check if the sha256 sum of the given path is valid"""
135 print("verifying", path)
136 with open(path, "rb") as source:
137 found = hashlib.sha256(source.read()).hexdigest()
138 verified = found == expected
140 print("invalid checksum:\n"
142 " expected: {}".format(found, expected))
146 def unpack(tarball, tarball_suffix, dst, verbose=False, match=None):
147 """Unpack the given tarball file"""
148 print("extracting", tarball)
149 fname = os.path.basename(tarball).replace(tarball_suffix, "")
150 with contextlib.closing(tarfile.open(tarball)) as tar:
151 for member in tar.getnames():
152 if "/" not in member:
154 name = member.replace(fname + "/", "", 1)
155 if match is not None and not name.startswith(match):
157 name = name[len(match) + 1:]
159 dst_path = os.path.join(dst, name)
161 print(" extracting", member)
162 tar.extract(member, dst)
163 src_path = os.path.join(dst, member)
164 if os.path.isdir(src_path) and os.path.exists(dst_path):
166 shutil.move(src_path, dst_path)
167 shutil.rmtree(os.path.join(dst, fname))
170 def run(args, verbose=False, exception=False, is_bootstrap=False, **kwargs):
171 """Run a child program in a new process"""
173 print("running: " + ' '.join(args))
175 # Use Popen here instead of call() as it apparently allows powershell on
176 # Windows to not lock up waiting for input presumably.
177 ret = subprocess.Popen(args, **kwargs)
180 err = "failed to run: " + ' '.join(args)
181 if verbose or exception:
182 raise RuntimeError(err)
183 # For most failures, we definitely do want to print this error, or the user will have no
184 # idea what went wrong. But when we've successfully built bootstrap and it failed, it will
185 # have already printed an error above, so there's no need to print the exact command we're
193 def require(cmd, exit=True):
194 '''Run a command, returning its output.
196 If `exit` is `True`, exit the process.
197 Otherwise, return None.'''
199 return subprocess.check_output(cmd).strip()
200 except (subprocess.CalledProcessError, OSError) as exc:
203 print("error: unable to run `{}`: {}".format(' '.join(cmd), exc))
204 print("Please make sure it's installed and in the path.")
208 def format_build_time(duration):
209 """Return a nicer format for build time
211 >>> format_build_time('300')
214 return str(datetime.timedelta(seconds=int(duration)))
217 def default_build_triple(verbose):
218 """Build triple as in LLVM"""
219 # If the user already has a host build triple with an existing `rustc`
220 # install, use their preference. This fixes most issues with Windows builds
221 # being detected as GNU instead of MSVC.
222 default_encoding = sys.getdefaultencoding()
224 version = subprocess.check_output(["rustc", "--version", "--verbose"],
225 stderr=subprocess.DEVNULL)
226 version = version.decode(default_encoding)
227 host = next(x for x in version.split('\n') if x.startswith("host: "))
228 triple = host.split("host: ")[1]
230 print("detected default triple {} from pre-installed rustc".format(triple))
232 except Exception as e:
234 print("pre-installed rustc not detected: {}".format(e))
235 print("falling back to auto-detect")
237 required = sys.platform != 'win32'
238 ostype = require(["uname", "-s"], exit=required)
239 cputype = require(['uname', '-m'], exit=required)
241 # If we do not have `uname`, assume Windows.
242 if ostype is None or cputype is None:
243 return 'x86_64-pc-windows-msvc'
245 ostype = ostype.decode(default_encoding)
246 cputype = cputype.decode(default_encoding)
248 # The goal here is to come up with the same triple as LLVM would,
249 # at least for the subset of platforms we're willing to target.
251 'Darwin': 'apple-darwin',
252 'DragonFly': 'unknown-dragonfly',
253 'FreeBSD': 'unknown-freebsd',
254 'Haiku': 'unknown-haiku',
255 'NetBSD': 'unknown-netbsd',
256 'OpenBSD': 'unknown-openbsd'
259 # Consider the direct transformation first and then the special cases
260 if ostype in ostype_mapper:
261 ostype = ostype_mapper[ostype]
262 elif ostype == 'Linux':
263 os_from_sp = subprocess.check_output(
264 ['uname', '-o']).strip().decode(default_encoding)
265 if os_from_sp == 'Android':
266 ostype = 'linux-android'
268 ostype = 'unknown-linux-gnu'
269 elif ostype == 'SunOS':
270 ostype = 'pc-solaris'
271 # On Solaris, uname -m will return a machine classification instead
272 # of a cpu type, so uname -p is recommended instead. However, the
273 # output from that option is too generic for our purposes (it will
274 # always emit 'i386' on x86/amd64 systems). As such, isainfo -k
275 # must be used instead.
276 cputype = require(['isainfo', '-k']).decode(default_encoding)
277 # sparc cpus have sun as a target vendor
278 if 'sparc' in cputype:
279 ostype = 'sun-solaris'
280 elif ostype.startswith('MINGW'):
281 # msys' `uname` does not print gcc configuration, but prints msys
282 # configuration. so we cannot believe `uname -m`:
283 # msys1 is always i686 and msys2 is always x86_64.
284 # instead, msys defines $MSYSTEM which is MINGW32 on i686 and
286 ostype = 'pc-windows-gnu'
288 if os.environ.get('MSYSTEM') == 'MINGW64':
290 elif ostype.startswith('MSYS'):
291 ostype = 'pc-windows-gnu'
292 elif ostype.startswith('CYGWIN_NT'):
294 if ostype.endswith('WOW64'):
296 ostype = 'pc-windows-gnu'
297 elif sys.platform == 'win32':
298 # Some Windows platforms might have a `uname` command that returns a
299 # non-standard string (e.g. gnuwin32 tools returns `windows32`). In
300 # these cases, fall back to using sys.platform.
301 return 'x86_64-pc-windows-msvc'
303 err = "unknown OS type: {}".format(ostype)
306 if cputype in ['powerpc', 'riscv'] and ostype == 'unknown-freebsd':
307 cputype = subprocess.check_output(
308 ['uname', '-p']).strip().decode(default_encoding)
311 'aarch64': 'aarch64',
319 'powerpc': 'powerpc',
320 'powerpc64': 'powerpc64',
321 'powerpc64le': 'powerpc64le',
323 'ppc64': 'powerpc64',
324 'ppc64le': 'powerpc64le',
325 'riscv64': 'riscv64gc',
333 # Consider the direct transformation first and then the special cases
334 if cputype in cputype_mapper:
335 cputype = cputype_mapper[cputype]
336 elif cputype in {'xscale', 'arm'}:
338 if ostype == 'linux-android':
339 ostype = 'linux-androideabi'
340 elif ostype == 'unknown-freebsd':
341 cputype = subprocess.check_output(
342 ['uname', '-p']).strip().decode(default_encoding)
343 ostype = 'unknown-freebsd'
344 elif cputype == 'armv6l':
346 if ostype == 'linux-android':
347 ostype = 'linux-androideabi'
350 elif cputype in {'armv7l', 'armv8l'}:
352 if ostype == 'linux-android':
353 ostype = 'linux-androideabi'
356 elif cputype == 'mips':
357 if sys.byteorder == 'big':
359 elif sys.byteorder == 'little':
362 raise ValueError("unknown byteorder: {}".format(sys.byteorder))
363 elif cputype == 'mips64':
364 if sys.byteorder == 'big':
366 elif sys.byteorder == 'little':
369 raise ValueError('unknown byteorder: {}'.format(sys.byteorder))
370 # only the n64 ABI is supported, indicate it
372 elif cputype == 'sparc' or cputype == 'sparcv9' or cputype == 'sparc64':
375 err = "unknown cpu type: {}".format(cputype)
378 return "{}-{}".format(cputype, ostype)
381 @contextlib.contextmanager
382 def output(filepath):
383 tmp = filepath + '.tmp'
384 with open(tmp, 'w') as f:
387 if os.path.exists(filepath):
388 os.remove(filepath) # PermissionError/OSError on Win32 if in use
390 shutil.copy2(tmp, filepath)
393 os.rename(tmp, filepath)
396 class Stage0Toolchain:
397 def __init__(self, stage0_payload):
398 self.date = stage0_payload["date"]
399 self.version = stage0_payload["version"]
402 return self.version + "-" + self.date
405 class RustBuild(object):
406 """Provide all the methods required to build Rust"""
408 self.checksums_sha256 = {}
409 self.stage0_compiler = None
410 self.stage0_rustfmt = None
411 self._download_url = ''
415 self.config_toml = ''
417 self.use_locked_deps = ''
418 self.use_vendored_sources = ''
420 self.git_version = None
421 self.nix_deps_dir = None
422 self.rustc_commit = None
424 def download_toolchain(self, stage0=True, rustc_channel=None):
425 """Fetch the build system for Rust, written in Rust
427 This method will build a cache directory, then it will fetch the
428 tarball which has the stage0 compiler used to then bootstrap the Rust
431 Each downloaded tarball is extracted, after that, the script
432 will move all the content to the right place.
434 if rustc_channel is None:
435 rustc_channel = self.stage0_compiler.version
436 bin_root = self.bin_root(stage0)
438 key = self.stage0_compiler.date
440 key += str(self.rustc_commit)
441 if self.rustc(stage0).startswith(bin_root) and \
442 (not os.path.exists(self.rustc(stage0)) or
443 self.program_out_of_date(self.rustc_stamp(stage0), key)):
444 if os.path.exists(bin_root):
445 shutil.rmtree(bin_root)
446 tarball_suffix = '.tar.xz' if support_xz() else '.tar.gz'
447 filename = "rust-std-{}-{}{}".format(
448 rustc_channel, self.build, tarball_suffix)
449 pattern = "rust-std-{}".format(self.build)
450 self._download_component_helper(filename, pattern, tarball_suffix, stage0)
451 filename = "rustc-{}-{}{}".format(rustc_channel, self.build,
453 self._download_component_helper(filename, "rustc", tarball_suffix, stage0)
454 # download-rustc doesn't need its own cargo, it can just use beta's.
456 filename = "cargo-{}-{}{}".format(rustc_channel, self.build,
458 self._download_component_helper(filename, "cargo", tarball_suffix)
459 self.fix_bin_or_dylib("{}/bin/cargo".format(bin_root))
461 filename = "rustc-dev-{}-{}{}".format(rustc_channel, self.build, tarball_suffix)
462 self._download_component_helper(
463 filename, "rustc-dev", tarball_suffix, stage0
466 self.fix_bin_or_dylib("{}/bin/rustc".format(bin_root))
467 self.fix_bin_or_dylib("{}/bin/rustdoc".format(bin_root))
468 lib_dir = "{}/lib".format(bin_root)
469 for lib in os.listdir(lib_dir):
470 if lib.endswith(".so"):
471 self.fix_bin_or_dylib(os.path.join(lib_dir, lib))
472 with output(self.rustc_stamp(stage0)) as rust_stamp:
473 rust_stamp.write(key)
475 if self.rustfmt() and self.rustfmt().startswith(bin_root) and (
476 not os.path.exists(self.rustfmt())
477 or self.program_out_of_date(
478 self.rustfmt_stamp(),
479 "" if self.stage0_rustfmt is None else self.stage0_rustfmt.channel()
482 if self.stage0_rustfmt is not None:
483 tarball_suffix = '.tar.xz' if support_xz() else '.tar.gz'
484 filename = "rustfmt-{}-{}{}".format(
485 self.stage0_rustfmt.version, self.build, tarball_suffix,
487 self._download_component_helper(
488 filename, "rustfmt-preview", tarball_suffix, key=self.stage0_rustfmt.date
490 self.fix_bin_or_dylib("{}/bin/rustfmt".format(bin_root))
491 self.fix_bin_or_dylib("{}/bin/cargo-fmt".format(bin_root))
492 with output(self.rustfmt_stamp()) as rustfmt_stamp:
493 rustfmt_stamp.write(self.stage0_rustfmt.channel())
495 # Avoid downloading LLVM twice (once for stage0 and once for the master rustc)
496 if self.downloading_llvm() and stage0:
497 # We want the most recent LLVM submodule update to avoid downloading
498 # LLVM more often than necessary.
500 # This git command finds that commit SHA, looking for bors-authored
501 # commits that modified src/llvm-project or other relevant version
504 # This works even in a repository that has not yet initialized
506 top_level = subprocess.check_output([
507 "git", "rev-parse", "--show-toplevel",
508 ]).decode(sys.getdefaultencoding()).strip()
509 llvm_sha = subprocess.check_output([
510 "git", "rev-list", "--author=bors@rust-lang.org", "-n1",
511 "--first-parent", "HEAD",
513 "{}/src/llvm-project".format(top_level),
514 "{}/src/bootstrap/download-ci-llvm-stamp".format(top_level),
515 # the LLVM shared object file is named `LLVM-12-rust-{version}-nightly`
516 "{}/src/version".format(top_level)
517 ]).decode(sys.getdefaultencoding()).strip()
518 llvm_assertions = self.get_toml('assertions', 'llvm') == 'true'
519 llvm_root = self.llvm_root()
520 llvm_lib = os.path.join(llvm_root, "lib")
521 if self.program_out_of_date(self.llvm_stamp(), llvm_sha + str(llvm_assertions)):
522 self._download_ci_llvm(llvm_sha, llvm_assertions)
523 for binary in ["llvm-config", "FileCheck"]:
524 self.fix_bin_or_dylib(os.path.join(llvm_root, "bin", binary))
525 for lib in os.listdir(llvm_lib):
526 if lib.endswith(".so"):
527 self.fix_bin_or_dylib(os.path.join(llvm_lib, lib))
528 with output(self.llvm_stamp()) as llvm_stamp:
529 llvm_stamp.write(llvm_sha + str(llvm_assertions))
531 def downloading_llvm(self):
532 opt = self.get_toml('download-ci-llvm', 'llvm')
533 # This is currently all tier 1 targets and tier 2 targets with host tools
534 # (since others may not have CI artifacts)
535 # https://doc.rust-lang.org/rustc/platform-support.html#tier-1
536 supported_platforms = [
538 "aarch64-unknown-linux-gnu",
539 "i686-pc-windows-gnu",
540 "i686-pc-windows-msvc",
541 "i686-unknown-linux-gnu",
542 "x86_64-unknown-linux-gnu",
543 "x86_64-apple-darwin",
544 "x86_64-pc-windows-gnu",
545 "x86_64-pc-windows-msvc",
546 # tier 2 with host tools
547 "aarch64-apple-darwin",
548 "aarch64-pc-windows-msvc",
549 "aarch64-unknown-linux-musl",
550 "arm-unknown-linux-gnueabi",
551 "arm-unknown-linux-gnueabihf",
552 "armv7-unknown-linux-gnueabihf",
553 "mips-unknown-linux-gnu",
554 "mips64-unknown-linux-gnuabi64",
555 "mips64el-unknown-linux-gnuabi64",
556 "mipsel-unknown-linux-gnu",
557 "powerpc-unknown-linux-gnu",
558 "powerpc64-unknown-linux-gnu",
559 "powerpc64le-unknown-linux-gnu",
560 "riscv64gc-unknown-linux-gnu",
561 "s390x-unknown-linux-gnu",
562 "x86_64-unknown-freebsd",
563 "x86_64-unknown-illumos",
564 "x86_64-unknown-linux-musl",
565 "x86_64-unknown-netbsd",
567 return opt == "true" \
568 or (opt == "if-available" and self.build in supported_platforms)
570 def _download_component_helper(
571 self, filename, pattern, tarball_suffix, stage0=True, key=None
575 key = self.stage0_compiler.date
577 key = self.rustc_commit
578 cache_dst = os.path.join(self.build_dir, "cache")
579 rustc_cache = os.path.join(cache_dst, key)
580 if not os.path.exists(rustc_cache):
581 os.makedirs(rustc_cache)
584 base = self._download_url
585 url = "dist/{}".format(key)
587 base = "https://ci-artifacts.rust-lang.org"
588 url = "rustc-builds/{}".format(self.rustc_commit)
589 tarball = os.path.join(rustc_cache, filename)
590 if not os.path.exists(tarball):
593 "{}/{}".format(url, filename),
595 self.checksums_sha256,
596 verbose=self.verbose,
599 unpack(tarball, tarball_suffix, self.bin_root(stage0), match=pattern, verbose=self.verbose)
601 def _download_ci_llvm(self, llvm_sha, llvm_assertions):
603 print("error: could not find commit hash for downloading LLVM")
604 print("help: maybe your repository history is too shallow?")
605 print("help: consider disabling `download-ci-llvm`")
606 print("help: or fetch enough history to include one upstream commit")
608 cache_prefix = "llvm-{}-{}".format(llvm_sha, llvm_assertions)
609 cache_dst = os.path.join(self.build_dir, "cache")
610 rustc_cache = os.path.join(cache_dst, cache_prefix)
611 if not os.path.exists(rustc_cache):
612 os.makedirs(rustc_cache)
614 base = "https://ci-artifacts.rust-lang.org"
615 url = "rustc-builds/{}".format(llvm_sha)
617 url = url.replace('rustc-builds', 'rustc-builds-alt')
618 # ci-artifacts are only stored as .xz, not .gz
620 print("error: XZ support is required to download LLVM")
621 print("help: consider disabling `download-ci-llvm` or using python3")
623 tarball_suffix = '.tar.xz'
624 filename = "rust-dev-nightly-" + self.build + tarball_suffix
625 tarball = os.path.join(rustc_cache, filename)
626 if not os.path.exists(tarball):
629 "{}/{}".format(url, filename),
631 self.checksums_sha256,
632 verbose=self.verbose,
635 unpack(tarball, tarball_suffix, self.llvm_root(),
637 verbose=self.verbose)
639 def fix_bin_or_dylib(self, fname):
640 """Modifies the interpreter section of 'fname' to fix the dynamic linker,
641 or the RPATH section, to fix the dynamic library search path
643 This method is only required on NixOS and uses the PatchELF utility to
644 change the interpreter/RPATH of ELF executables.
646 Please see https://nixos.org/patchelf.html for more information
648 default_encoding = sys.getdefaultencoding()
650 ostype = subprocess.check_output(
651 ['uname', '-s']).strip().decode(default_encoding)
652 except subprocess.CalledProcessError:
654 except OSError as reason:
655 if getattr(reason, 'winerror', None) is not None:
659 if ostype != "Linux":
662 # If the user has asked binaries to be patched for Nix, then
663 # don't check for NixOS or `/lib`, just continue to the patching.
664 if self.get_toml('patch-binaries-for-nix', 'build') != 'true':
665 # Use `/etc/os-release` instead of `/etc/NIXOS`.
666 # The latter one does not exist on NixOS when using tmpfs as root.
668 with open("/etc/os-release", "r") as f:
669 if not any(line.strip() == "ID=nixos" for line in f):
671 except FileNotFoundError:
673 if os.path.exists("/lib"):
676 # At this point we're pretty sure the user is running NixOS or
678 nix_os_msg = "info: you seem to be using Nix. Attempting to patch"
679 print(nix_os_msg, fname)
681 # Only build `.nix-deps` once.
682 nix_deps_dir = self.nix_deps_dir
684 # Run `nix-build` to "build" each dependency (which will likely reuse
685 # the existing `/nix/store` copy, or at most download a pre-built copy).
687 # Importantly, we create a gc-root called `.nix-deps` in the `build/`
688 # directory, but still reference the actual `/nix/store` path in the rpath
689 # as it makes it significantly more robust against changes to the location of
690 # the `.nix-deps` location.
692 # bintools: Needed for the path of `ld-linux.so` (via `nix-support/dynamic-linker`).
693 # zlib: Needed as a system dependency of `libLLVM-*.so`.
694 # patchelf: Needed for patching ELF binaries (see doc comment above).
695 nix_deps_dir = "{}/{}".format(self.build_dir, ".nix-deps")
697 with (import <nixpkgs> {});
699 name = "rust-stage0-dependencies";
708 subprocess.check_output([
709 "nix-build", "-E", nix_expr, "-o", nix_deps_dir,
711 except subprocess.CalledProcessError as reason:
712 print("warning: failed to call nix-build:", reason)
714 self.nix_deps_dir = nix_deps_dir
716 patchelf = "{}/bin/patchelf".format(nix_deps_dir)
718 # Relative default, all binary and dynamic libraries we ship
719 # appear to have this (even when `../lib` is redundant).
721 os.path.join(os.path.realpath(nix_deps_dir), "lib")
723 patchelf_args = ["--set-rpath", ":".join(rpath_entries)]
724 if not fname.endswith(".so"):
725 # Finally, set the corret .interp for binaries
726 with open("{}/nix-support/dynamic-linker".format(nix_deps_dir)) as dynamic_linker:
727 patchelf_args += ["--set-interpreter", dynamic_linker.read().rstrip()]
730 subprocess.check_output([patchelf] + patchelf_args + [fname])
731 except subprocess.CalledProcessError as reason:
732 print("warning: failed to call patchelf:", reason)
735 # If `download-rustc` is set, download the most recent commit with CI artifacts
736 def maybe_download_ci_toolchain(self):
737 # If `download-rustc` is not set, default to rebuilding.
738 download_rustc = self.get_toml("download-rustc", section="rust")
739 if download_rustc is None or download_rustc == "false":
741 assert download_rustc == "true" or download_rustc == "if-unchanged", download_rustc
743 # Handle running from a directory other than the top level
744 rev_parse = ["git", "rev-parse", "--show-toplevel"]
745 top_level = subprocess.check_output(rev_parse, universal_newlines=True).strip()
746 compiler = "{}/compiler/".format(top_level)
747 library = "{}/library/".format(top_level)
749 # Look for a version to compare to based on the current commit.
750 # Only commits merged by bors will have CI artifacts.
752 "git", "rev-list", "--author=bors@rust-lang.org", "-n1",
753 "--first-parent", "HEAD"
755 commit = subprocess.check_output(merge_base, universal_newlines=True).strip()
757 print("error: could not find commit hash for downloading rustc")
758 print("help: maybe your repository history is too shallow?")
759 print("help: consider disabling `download-rustc`")
760 print("help: or fetch enough history to include one upstream commit")
763 # Warn if there were changes to the compiler or standard library since the ancestor commit.
764 status = subprocess.call(["git", "diff-index", "--quiet", commit, "--", compiler, library])
766 if download_rustc == "if-unchanged":
768 print("warning: saw changes to compiler/ or library/ since {}; " \
769 "ignoring `download-rustc`".format(commit))
771 print("warning: `download-rustc` is enabled, but there are changes to " \
772 "compiler/ or library/")
775 print("using downloaded stage2 artifacts from CI (commit {})".format(commit))
776 self.rustc_commit = commit
777 # FIXME: support downloading artifacts from the beta channel
778 self.download_toolchain(False, "nightly")
780 def rustc_stamp(self, stage0):
781 """Return the path for .rustc-stamp at the given stage
784 >>> rb.build_dir = "build"
785 >>> rb.rustc_stamp(True) == os.path.join("build", "stage0", ".rustc-stamp")
787 >>> rb.rustc_stamp(False) == os.path.join("build", "ci-rustc", ".rustc-stamp")
790 return os.path.join(self.bin_root(stage0), '.rustc-stamp')
792 def rustfmt_stamp(self):
793 """Return the path for .rustfmt-stamp
796 >>> rb.build_dir = "build"
797 >>> rb.rustfmt_stamp() == os.path.join("build", "stage0", ".rustfmt-stamp")
800 return os.path.join(self.bin_root(True), '.rustfmt-stamp')
802 def llvm_stamp(self):
803 """Return the path for .rustfmt-stamp
806 >>> rb.build_dir = "build"
807 >>> rb.llvm_stamp() == os.path.join("build", "ci-llvm", ".llvm-stamp")
810 return os.path.join(self.llvm_root(), '.llvm-stamp')
813 def program_out_of_date(self, stamp_path, key):
814 """Check if the given program stamp is out of date"""
815 if not os.path.exists(stamp_path) or self.clean:
817 with open(stamp_path, 'r') as stamp:
818 return key != stamp.read()
820 def bin_root(self, stage0):
821 """Return the binary root directory for the given stage
824 >>> rb.build_dir = "build"
825 >>> rb.bin_root(True) == os.path.join("build", "stage0")
827 >>> rb.bin_root(False) == os.path.join("build", "ci-rustc")
830 When the 'build' property is given should be a nested directory:
832 >>> rb.build = "devel"
833 >>> rb.bin_root(True) == os.path.join("build", "devel", "stage0")
840 return os.path.join(self.build_dir, self.build, subdir)
843 """Return the CI LLVM root directory
846 >>> rb.build_dir = "build"
847 >>> rb.llvm_root() == os.path.join("build", "ci-llvm")
850 When the 'build' property is given should be a nested directory:
852 >>> rb.build = "devel"
853 >>> rb.llvm_root() == os.path.join("build", "devel", "ci-llvm")
856 return os.path.join(self.build_dir, self.build, "ci-llvm")
858 def get_toml(self, key, section=None):
859 """Returns the value of the given key in config.toml, otherwise returns None
862 >>> rb.config_toml = 'key1 = "value1"\\nkey2 = "value2"'
863 >>> rb.get_toml("key2")
866 If the key does not exist, the result is None:
868 >>> rb.get_toml("key3") is None
871 Optionally also matches the section the key appears in
873 >>> rb.config_toml = '[a]\\nkey = "value1"\\n[b]\\nkey = "value2"'
874 >>> rb.get_toml('key', 'a')
876 >>> rb.get_toml('key', 'b')
878 >>> rb.get_toml('key', 'c') is None
881 >>> rb.config_toml = 'key1 = true'
882 >>> rb.get_toml("key1")
887 for line in self.config_toml.splitlines():
888 section_match = re.match(r'^\s*\[(.*)\]\s*$', line)
889 if section_match is not None:
890 cur_section = section_match.group(1)
892 match = re.match(r'^{}\s*=(.*)$'.format(key), line)
893 if match is not None:
894 value = match.group(1)
895 if section is None or section == cur_section:
896 return self.get_string(value) or value.strip()
900 """Return config path for cargo"""
901 return self.program_config('cargo')
903 def rustc(self, stage0):
904 """Return config path for rustc"""
905 return self.program_config('rustc', stage0)
908 """Return config path for rustfmt"""
909 if self.stage0_rustfmt is None:
911 return self.program_config('rustfmt')
913 def program_config(self, program, stage0=True):
914 """Return config path for the given program at the given stage
917 >>> rb.config_toml = 'rustc = "rustc"\\n'
918 >>> rb.program_config('rustc')
920 >>> rb.config_toml = ''
921 >>> cargo_path = rb.program_config('cargo', True)
922 >>> cargo_path.rstrip(".exe") == os.path.join(rb.bin_root(True),
925 >>> cargo_path = rb.program_config('cargo', False)
926 >>> cargo_path.rstrip(".exe") == os.path.join(rb.bin_root(False),
930 config = self.get_toml(program)
932 return os.path.expanduser(config)
933 return os.path.join(self.bin_root(stage0), "bin", "{}{}".format(
934 program, self.exe_suffix()))
937 def get_string(line):
938 """Return the value between double quotes
940 >>> RustBuild.get_string(' "devel" ')
942 >>> RustBuild.get_string(" 'devel' ")
944 >>> RustBuild.get_string('devel') is None
946 >>> RustBuild.get_string(' "devel ')
949 start = line.find('"')
951 end = start + 1 + line[start + 1:].find('"')
952 return line[start + 1:end]
953 start = line.find('\'')
955 end = start + 1 + line[start + 1:].find('\'')
956 return line[start + 1:end]
961 """Return a suffix for executables"""
962 if sys.platform == 'win32':
966 def bootstrap_binary(self):
967 """Return the path of the bootstrap binary
970 >>> rb.build_dir = "build"
971 >>> rb.bootstrap_binary() == os.path.join("build", "bootstrap",
972 ... "debug", "bootstrap")
975 return os.path.join(self.build_dir, "bootstrap", "debug", "bootstrap")
977 def build_bootstrap(self):
978 """Build bootstrap"""
979 print("Building rustbuild")
980 build_dir = os.path.join(self.build_dir, "bootstrap")
981 if self.clean and os.path.exists(build_dir):
982 shutil.rmtree(build_dir)
983 env = os.environ.copy()
984 # `CARGO_BUILD_TARGET` breaks bootstrap build.
985 # See also: <https://github.com/rust-lang/rust/issues/70208>.
986 if "CARGO_BUILD_TARGET" in env:
987 del env["CARGO_BUILD_TARGET"]
988 env["CARGO_TARGET_DIR"] = build_dir
989 env["RUSTC"] = self.rustc(True)
990 env["LD_LIBRARY_PATH"] = os.path.join(self.bin_root(True), "lib") + \
991 (os.pathsep + env["LD_LIBRARY_PATH"]) \
992 if "LD_LIBRARY_PATH" in env else ""
993 env["DYLD_LIBRARY_PATH"] = os.path.join(self.bin_root(True), "lib") + \
994 (os.pathsep + env["DYLD_LIBRARY_PATH"]) \
995 if "DYLD_LIBRARY_PATH" in env else ""
996 env["LIBRARY_PATH"] = os.path.join(self.bin_root(True), "lib") + \
997 (os.pathsep + env["LIBRARY_PATH"]) \
998 if "LIBRARY_PATH" in env else ""
1000 # preserve existing RUSTFLAGS
1001 env.setdefault("RUSTFLAGS", "")
1002 build_section = "target.{}".format(self.build)
1003 target_features = []
1004 if self.get_toml("crt-static", build_section) == "true":
1005 target_features += ["+crt-static"]
1006 elif self.get_toml("crt-static", build_section) == "false":
1007 target_features += ["-crt-static"]
1009 env["RUSTFLAGS"] += " -C target-feature=" + (",".join(target_features))
1010 target_linker = self.get_toml("linker", build_section)
1011 if target_linker is not None:
1012 env["RUSTFLAGS"] += " -C linker=" + target_linker
1013 env["RUSTFLAGS"] += " -Wrust_2018_idioms -Wunused_lifetimes"
1014 env["RUSTFLAGS"] += " -Wsemicolon_in_expressions_from_macros"
1015 if self.get_toml("deny-warnings", "rust") != "false":
1016 env["RUSTFLAGS"] += " -Dwarnings"
1018 env["PATH"] = os.path.join(self.bin_root(True), "bin") + \
1019 os.pathsep + env["PATH"]
1020 if not os.path.isfile(self.cargo()):
1021 raise Exception("no cargo executable found at `{}`".format(
1023 args = [self.cargo(), "build", "--manifest-path",
1024 os.path.join(self.rust_root, "src/bootstrap/Cargo.toml")]
1025 for _ in range(0, self.verbose):
1026 args.append("--verbose")
1027 if self.use_locked_deps:
1028 args.append("--locked")
1029 if self.use_vendored_sources:
1030 args.append("--frozen")
1031 run(args, env=env, verbose=self.verbose)
1033 def build_triple(self):
1034 """Build triple as in LLVM
1036 Note that `default_build_triple` is moderately expensive,
1037 so use `self.build` where possible.
1039 config = self.get_toml('build')
1042 return default_build_triple(self.verbose)
1044 def check_submodule(self, module, slow_submodules):
1045 if not slow_submodules:
1046 checked_out = subprocess.Popen(["git", "rev-parse", "HEAD"],
1047 cwd=os.path.join(self.rust_root, module),
1048 stdout=subprocess.PIPE)
1053 def update_submodule(self, module, checked_out, recorded_submodules):
1054 module_path = os.path.join(self.rust_root, module)
1056 if checked_out is not None:
1057 default_encoding = sys.getdefaultencoding()
1058 checked_out = checked_out.communicate()[0].decode(default_encoding).strip()
1059 if recorded_submodules[module] == checked_out:
1062 print("Updating submodule", module)
1064 run(["git", "submodule", "-q", "sync", module],
1065 cwd=self.rust_root, verbose=self.verbose)
1067 update_args = ["git", "submodule", "update", "--init", "--recursive", "--depth=1"]
1068 if self.git_version >= distutils.version.LooseVersion("2.11.0"):
1069 update_args.append("--progress")
1070 update_args.append(module)
1072 run(update_args, cwd=self.rust_root, verbose=self.verbose, exception=True)
1073 except RuntimeError:
1074 print("Failed updating submodule. This is probably due to uncommitted local changes.")
1075 print('Either stash the changes by running "git stash" within the submodule\'s')
1076 print('directory, reset them by running "git reset --hard", or commit them.')
1077 print("To reset all submodules' changes run", end=" ")
1078 print('"git submodule foreach --recursive git reset --hard".')
1081 run(["git", "reset", "-q", "--hard"],
1082 cwd=module_path, verbose=self.verbose)
1083 run(["git", "clean", "-qdfx"],
1084 cwd=module_path, verbose=self.verbose)
1086 def update_submodules(self):
1087 """Update submodules"""
1088 if (not os.path.exists(os.path.join(self.rust_root, ".git"))) or \
1089 self.get_toml('submodules') == "false":
1092 default_encoding = sys.getdefaultencoding()
1094 # check the existence and version of 'git' command
1095 git_version_str = require(['git', '--version']).split()[2].decode(default_encoding)
1096 self.git_version = distutils.version.LooseVersion(git_version_str)
1098 slow_submodules = self.get_toml('fast-submodules') == "false"
1101 print('Unconditionally updating submodules')
1103 print('Updating only changed submodules')
1104 default_encoding = sys.getdefaultencoding()
1105 # Only update submodules that are needed to build bootstrap. These are needed because Cargo
1106 # currently requires everything in a workspace to be "locally present" when starting a
1107 # build, and will give a hard error if any Cargo.toml files are missing.
1108 # FIXME: Is there a way to avoid cloning these eagerly? Bootstrap itself doesn't need to
1109 # share a workspace with any tools - maybe it could be excluded from the workspace?
1110 # That will still require cloning the submodules the second you check the standard
1111 # library, though...
1112 # FIXME: Is there a way to avoid hard-coding the submodules required?
1113 # WARNING: keep this in sync with the submodules hard-coded in bootstrap/lib.rs
1115 "src/tools/rust-installer",
1119 "library/backtrace",
1122 filtered_submodules = []
1123 submodules_names = []
1124 for module in submodules:
1125 check = self.check_submodule(module, slow_submodules)
1126 filtered_submodules.append((module, check))
1127 submodules_names.append(module)
1128 recorded = subprocess.Popen(["git", "ls-tree", "HEAD"] + submodules_names,
1129 cwd=self.rust_root, stdout=subprocess.PIPE)
1130 recorded = recorded.communicate()[0].decode(default_encoding).strip().splitlines()
1131 # { filename: hash }
1132 recorded_submodules = {}
1133 for data in recorded:
1134 # [mode, kind, hash, filename]
1136 recorded_submodules[data[3]] = data[2]
1137 for module in filtered_submodules:
1138 self.update_submodule(module[0], module[1], recorded_submodules)
1139 print(" Submodules updated in %.2f seconds" % (time() - start_time))
1141 def set_dist_environment(self, url):
1142 """Set download URL for normal environment"""
1143 if 'RUSTUP_DIST_SERVER' in os.environ:
1144 self._download_url = os.environ['RUSTUP_DIST_SERVER']
1146 self._download_url = url
1148 def check_vendored_status(self):
1149 """Check that vendoring is configured properly"""
1150 vendor_dir = os.path.join(self.rust_root, 'vendor')
1151 if 'SUDO_USER' in os.environ and not self.use_vendored_sources:
1152 if os.environ.get('USER') != os.environ['SUDO_USER']:
1153 self.use_vendored_sources = True
1154 print('info: looks like you are running this command under `sudo`')
1155 print(' and so in order to preserve your $HOME this will now')
1156 print(' use vendored sources by default.')
1157 if not os.path.exists(vendor_dir):
1158 print('error: vendoring required, but vendor directory does not exist.')
1159 print(' Run `cargo vendor` without sudo to initialize the '
1160 'vendor directory.')
1161 raise Exception("{} not found".format(vendor_dir))
1163 if self.use_vendored_sources:
1164 config = ("[source.crates-io]\n"
1165 "replace-with = 'vendored-sources'\n"
1166 "registry = 'https://example.com'\n"
1168 "[source.vendored-sources]\n"
1169 "directory = '{}/vendor'\n"
1170 .format(self.rust_root))
1171 if not os.path.exists('.cargo'):
1172 os.makedirs('.cargo')
1173 with output('.cargo/config') as cargo_config:
1174 cargo_config.write(config)
1176 print('info: using vendored source, but .cargo/config is already present.')
1177 print(' Reusing the current configuration file. But you may want to '
1178 'configure vendoring like this:')
1181 if os.path.exists('.cargo'):
1182 shutil.rmtree('.cargo')
1184 def ensure_vendored(self):
1185 """Ensure that the vendored sources are available if needed"""
1186 vendor_dir = os.path.join(self.rust_root, 'vendor')
1187 # Note that this does not handle updating the vendored dependencies if
1188 # the rust git repository is updated. Normal development usually does
1189 # not use vendoring, so hopefully this isn't too much of a problem.
1190 if self.use_vendored_sources and not os.path.exists(vendor_dir):
1194 "--sync=./src/tools/rust-analyzer/Cargo.toml",
1195 "--sync=./compiler/rustc_codegen_cranelift/Cargo.toml",
1196 ], verbose=self.verbose, cwd=self.rust_root)
1199 def bootstrap(help_triggered):
1200 """Configure, fetch, build and run the initial bootstrap"""
1202 # If the user is asking for help, let them know that the whole download-and-build
1203 # process has to happen before anything is printed out.
1205 print("info: Downloading and building bootstrap before processing --help")
1206 print(" command. See src/bootstrap/README.md for help with common")
1209 parser = argparse.ArgumentParser(description='Build rust')
1210 parser.add_argument('--config')
1211 parser.add_argument('--build')
1212 parser.add_argument('--clean', action='store_true')
1213 parser.add_argument('-v', '--verbose', action='count', default=0)
1215 args = [a for a in sys.argv if a != '-h' and a != '--help']
1216 args, _ = parser.parse_known_args(args)
1218 # Configure initial bootstrap
1220 build.rust_root = os.path.abspath(os.path.join(__file__, '../../..'))
1221 build.verbose = args.verbose
1222 build.clean = args.clean
1224 # Read from `--config`, then `RUST_BOOTSTRAP_CONFIG`, then fallback to `config.toml` (if it
1226 toml_path = args.config or os.getenv('RUST_BOOTSTRAP_CONFIG')
1227 if not toml_path and os.path.exists('config.toml'):
1228 toml_path = 'config.toml'
1231 if not os.path.exists(toml_path):
1232 toml_path = os.path.join(build.rust_root, toml_path)
1234 with open(toml_path) as config:
1235 build.config_toml = config.read()
1237 profile = build.get_toml('profile')
1238 if profile is not None:
1239 include_file = 'config.{}.toml'.format(profile)
1240 include_dir = os.path.join(build.rust_root, 'src', 'bootstrap', 'defaults')
1241 include_path = os.path.join(include_dir, include_file)
1242 # HACK: This works because `build.get_toml()` returns the first match it finds for a
1243 # specific key, so appending our defaults at the end allows the user to override them
1244 with open(include_path) as included_toml:
1245 build.config_toml += os.linesep + included_toml.read()
1247 config_verbose = build.get_toml('verbose', 'build')
1248 if config_verbose is not None:
1249 build.verbose = max(build.verbose, int(config_verbose))
1251 build.use_vendored_sources = build.get_toml('vendor', 'build') == 'true'
1253 build.use_locked_deps = build.get_toml('locked-deps', 'build') == 'true'
1255 build.check_vendored_status()
1257 build_dir = build.get_toml('build-dir', 'build') or 'build'
1258 build.build_dir = os.path.abspath(build_dir.replace("$ROOT", build.rust_root))
1260 with open(os.path.join(build.rust_root, "src", "stage0.json")) as f:
1262 build.checksums_sha256 = data["checksums_sha256"]
1263 build.stage0_compiler = Stage0Toolchain(data["compiler"])
1264 if data.get("rustfmt") is not None:
1265 build.stage0_rustfmt = Stage0Toolchain(data["rustfmt"])
1267 build.set_dist_environment(data["dist_server"])
1269 build.build = args.build or build.build_triple()
1271 # Acquire the lock before doing any build actions
1272 # The lock is released when `lock` is dropped
1273 if not os.path.exists(build.build_dir):
1274 os.makedirs(build.build_dir)
1275 lock = acquire_lock(build.build_dir)
1276 build.update_submodules()
1278 # Fetch/build the bootstrap
1279 build.download_toolchain()
1280 # Download the master compiler if `download-rustc` is set
1281 build.maybe_download_ci_toolchain()
1283 build.ensure_vendored()
1284 build.build_bootstrap()
1288 args = [build.bootstrap_binary()]
1289 args.extend(sys.argv[1:])
1290 env = os.environ.copy()
1291 env["BOOTSTRAP_PARENT_ID"] = str(os.getpid())
1292 env["BOOTSTRAP_PYTHON"] = sys.executable
1293 env["BUILD_DIR"] = build.build_dir
1294 env["RUSTC_BOOTSTRAP"] = '1'
1296 env["BOOTSTRAP_CONFIG"] = toml_path
1297 if build.rustc_commit is not None:
1298 env["BOOTSTRAP_DOWNLOAD_RUSTC"] = '1'
1299 run(args, env=env, verbose=build.verbose, is_bootstrap=True)
1303 """Entry point for the bootstrap process"""
1306 # x.py help <cmd> ...
1307 if len(sys.argv) > 1 and sys.argv[1] == 'help':
1308 sys.argv = [sys.argv[0], '-h'] + sys.argv[2:]
1311 '-h' in sys.argv) or ('--help' in sys.argv) or (len(sys.argv) == 1)
1313 bootstrap(help_triggered)
1314 if not help_triggered:
1315 print("Build completed successfully in {}".format(
1316 format_build_time(time() - start_time)))
1317 except (SystemExit, KeyboardInterrupt) as error:
1318 if hasattr(error, 'code') and isinstance(error.code, int):
1319 exit_code = error.code
1323 if not help_triggered:
1324 print("Build completed unsuccessfully in {}".format(
1325 format_build_time(time() - start_time)))
1329 if __name__ == '__main__':