1 from __future__ import absolute_import, division, print_function
5 import distutils.version
20 with tempfile.NamedTemporaryFile(delete=False) as temp_file:
21 temp_path = temp_file.name
22 with tarfile.open(temp_path, "w:xz"):
25 except tarfile.CompressionError:
28 def get(url, path, verbose=False, do_verify=True):
30 sha_url = url + suffix
31 with tempfile.NamedTemporaryFile(delete=False) as temp_file:
32 temp_path = temp_file.name
33 with tempfile.NamedTemporaryFile(suffix=suffix, delete=False) as sha_file:
34 sha_path = sha_file.name
38 download(sha_path, sha_url, False, verbose)
39 if os.path.exists(path):
40 if verify(path, sha_path, False):
42 print("using already-download file", path)
46 print("ignoring already-download file",
47 path, "due to failed verification")
49 download(temp_path, url, True, verbose)
50 if do_verify and not verify(temp_path, sha_path, verbose):
51 raise RuntimeError("failed verification")
53 print("moving {} to {}".format(temp_path, path))
54 shutil.move(temp_path, path)
56 delete_if_present(sha_path, verbose)
57 delete_if_present(temp_path, verbose)
60 def delete_if_present(path, verbose):
61 """Remove the given file if present"""
62 if os.path.isfile(path):
64 print("removing", path)
68 def download(path, url, probably_big, verbose):
71 _download(path, url, probably_big, verbose, True)
74 print("\nspurious failure, trying again")
75 _download(path, url, probably_big, verbose, False)
78 def _download(path, url, probably_big, verbose, exception):
79 if probably_big or verbose:
80 print("downloading {}".format(url))
81 # see https://serverfault.com/questions/301128/how-to-download
82 if sys.platform == 'win32':
83 run(["PowerShell.exe", "/nologo", "-Command",
84 "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12;",
85 "(New-Object System.Net.WebClient).DownloadFile('{}', '{}')".format(url, path)],
89 if probably_big or verbose:
93 require(["curl", "--version"])
95 "-y", "30", "-Y", "10", # timeout if speed is < 10 bytes/sec for > 30 seconds
96 "--connect-timeout", "30", # timeout if cannot connect within 30 seconds
97 "--retry", "3", "-Sf", "-o", path, url],
102 def verify(path, sha_path, verbose):
103 """Check if the sha256 sum of the given path is valid"""
105 print("verifying", path)
106 with open(path, "rb") as source:
107 found = hashlib.sha256(source.read()).hexdigest()
108 with open(sha_path, "r") as sha256sum:
109 expected = sha256sum.readline().split()[0]
110 verified = found == expected
112 print("invalid checksum:\n"
114 " expected: {}".format(found, expected))
118 def unpack(tarball, tarball_suffix, dst, verbose=False, match=None):
119 """Unpack the given tarball file"""
120 print("extracting", tarball)
121 fname = os.path.basename(tarball).replace(tarball_suffix, "")
122 with contextlib.closing(tarfile.open(tarball)) as tar:
123 for member in tar.getnames():
124 if "/" not in member:
126 name = member.replace(fname + "/", "", 1)
127 if match is not None and not name.startswith(match):
129 name = name[len(match) + 1:]
131 dst_path = os.path.join(dst, name)
133 print(" extracting", member)
134 tar.extract(member, dst)
135 src_path = os.path.join(dst, member)
136 if os.path.isdir(src_path) and os.path.exists(dst_path):
138 shutil.move(src_path, dst_path)
139 shutil.rmtree(os.path.join(dst, fname))
142 def run(args, verbose=False, exception=False, is_bootstrap=False, **kwargs):
143 """Run a child program in a new process"""
145 print("running: " + ' '.join(args))
147 # Use Popen here instead of call() as it apparently allows powershell on
148 # Windows to not lock up waiting for input presumably.
149 ret = subprocess.Popen(args, **kwargs)
152 err = "failed to run: " + ' '.join(args)
153 if verbose or exception:
154 raise RuntimeError(err)
155 # For most failures, we definitely do want to print this error, or the user will have no
156 # idea what went wrong. But when we've successfully built bootstrap and it failed, it will
157 # have already printed an error above, so there's no need to print the exact command we're
165 def require(cmd, exit=True):
166 '''Run a command, returning its output.
168 If `exit` is `True`, exit the process.
169 Otherwise, return None.'''
171 return subprocess.check_output(cmd).strip()
172 except (subprocess.CalledProcessError, OSError) as exc:
175 print("error: unable to run `{}`: {}".format(' '.join(cmd), exc))
176 print("Please make sure it's installed and in the path.")
180 def format_build_time(duration):
181 """Return a nicer format for build time
183 >>> format_build_time('300')
186 return str(datetime.timedelta(seconds=int(duration)))
189 def default_build_triple(verbose):
190 """Build triple as in LLVM"""
191 # If the user already has a host build triple with an existing `rustc`
192 # install, use their preference. This fixes most issues with Windows builds
193 # being detected as GNU instead of MSVC.
194 default_encoding = sys.getdefaultencoding()
196 version = subprocess.check_output(["rustc", "--version", "--verbose"],
197 stderr=subprocess.DEVNULL)
198 version = version.decode(default_encoding)
199 host = next(x for x in version.split('\n') if x.startswith("host: "))
200 triple = host.split("host: ")[1]
202 print("detected default triple {}".format(triple))
204 except Exception as e:
206 print("rustup not detected: {}".format(e))
207 print("falling back to auto-detect")
209 required = sys.platform != 'win32'
210 ostype = require(["uname", "-s"], exit=required)
211 cputype = require(['uname', '-m'], exit=required)
213 # If we do not have `uname`, assume Windows.
214 if ostype is None or cputype is None:
215 return 'x86_64-pc-windows-msvc'
217 ostype = ostype.decode(default_encoding)
218 cputype = cputype.decode(default_encoding)
220 # The goal here is to come up with the same triple as LLVM would,
221 # at least for the subset of platforms we're willing to target.
223 'Darwin': 'apple-darwin',
224 'DragonFly': 'unknown-dragonfly',
225 'FreeBSD': 'unknown-freebsd',
226 'Haiku': 'unknown-haiku',
227 'NetBSD': 'unknown-netbsd',
228 'OpenBSD': 'unknown-openbsd'
231 # Consider the direct transformation first and then the special cases
232 if ostype in ostype_mapper:
233 ostype = ostype_mapper[ostype]
234 elif ostype == 'Linux':
235 os_from_sp = subprocess.check_output(
236 ['uname', '-o']).strip().decode(default_encoding)
237 if os_from_sp == 'Android':
238 ostype = 'linux-android'
240 ostype = 'unknown-linux-gnu'
241 elif ostype == 'SunOS':
242 ostype = 'pc-solaris'
243 # On Solaris, uname -m will return a machine classification instead
244 # of a cpu type, so uname -p is recommended instead. However, the
245 # output from that option is too generic for our purposes (it will
246 # always emit 'i386' on x86/amd64 systems). As such, isainfo -k
247 # must be used instead.
248 cputype = require(['isainfo', '-k']).decode(default_encoding)
249 # sparc cpus have sun as a target vendor
250 if 'sparc' in cputype:
251 ostype = 'sun-solaris'
252 elif ostype.startswith('MINGW'):
253 # msys' `uname` does not print gcc configuration, but prints msys
254 # configuration. so we cannot believe `uname -m`:
255 # msys1 is always i686 and msys2 is always x86_64.
256 # instead, msys defines $MSYSTEM which is MINGW32 on i686 and
258 ostype = 'pc-windows-gnu'
260 if os.environ.get('MSYSTEM') == 'MINGW64':
262 elif ostype.startswith('MSYS'):
263 ostype = 'pc-windows-gnu'
264 elif ostype.startswith('CYGWIN_NT'):
266 if ostype.endswith('WOW64'):
268 ostype = 'pc-windows-gnu'
269 elif sys.platform == 'win32':
270 # Some Windows platforms might have a `uname` command that returns a
271 # non-standard string (e.g. gnuwin32 tools returns `windows32`). In
272 # these cases, fall back to using sys.platform.
273 return 'x86_64-pc-windows-msvc'
275 err = "unknown OS type: {}".format(ostype)
278 if cputype == 'powerpc' and ostype == 'unknown-freebsd':
279 cputype = subprocess.check_output(
280 ['uname', '-p']).strip().decode(default_encoding)
283 'aarch64': 'aarch64',
290 'powerpc': 'powerpc',
291 'powerpc64': 'powerpc64',
292 'powerpc64le': 'powerpc64le',
294 'ppc64': 'powerpc64',
295 'ppc64le': 'powerpc64le',
303 # Consider the direct transformation first and then the special cases
304 if cputype in cputype_mapper:
305 cputype = cputype_mapper[cputype]
306 elif cputype in {'xscale', 'arm'}:
308 if ostype == 'linux-android':
309 ostype = 'linux-androideabi'
310 elif ostype == 'unknown-freebsd':
311 cputype = subprocess.check_output(
312 ['uname', '-p']).strip().decode(default_encoding)
313 ostype = 'unknown-freebsd'
314 elif cputype == 'armv6l':
316 if ostype == 'linux-android':
317 ostype = 'linux-androideabi'
320 elif cputype in {'armv7l', 'armv8l'}:
322 if ostype == 'linux-android':
323 ostype = 'linux-androideabi'
326 elif cputype == 'mips':
327 if sys.byteorder == 'big':
329 elif sys.byteorder == 'little':
332 raise ValueError("unknown byteorder: {}".format(sys.byteorder))
333 elif cputype == 'mips64':
334 if sys.byteorder == 'big':
336 elif sys.byteorder == 'little':
339 raise ValueError('unknown byteorder: {}'.format(sys.byteorder))
340 # only the n64 ABI is supported, indicate it
342 elif cputype == 'sparc' or cputype == 'sparcv9' or cputype == 'sparc64':
345 err = "unknown cpu type: {}".format(cputype)
348 return "{}-{}".format(cputype, ostype)
351 @contextlib.contextmanager
352 def output(filepath):
353 tmp = filepath + '.tmp'
354 with open(tmp, 'w') as f:
357 if os.path.exists(filepath):
358 os.remove(filepath) # PermissionError/OSError on Win32 if in use
360 shutil.copy2(tmp, filepath)
363 os.rename(tmp, filepath)
366 class Stage0Toolchain:
367 def __init__(self, stage0_payload):
368 self.date = stage0_payload["date"]
369 self.version = stage0_payload["version"]
372 return self.version + "-" + self.date
375 class RustBuild(object):
376 """Provide all the methods required to build Rust"""
378 self.stage0_compiler = None
379 self.stage0_rustfmt = None
380 self._download_url = ''
384 self.config_toml = ''
386 self.use_locked_deps = ''
387 self.use_vendored_sources = ''
389 self.git_version = None
390 self.nix_deps_dir = None
391 self.rustc_commit = None
393 def download_toolchain(self, stage0=True, rustc_channel=None):
394 """Fetch the build system for Rust, written in Rust
396 This method will build a cache directory, then it will fetch the
397 tarball which has the stage0 compiler used to then bootstrap the Rust
400 Each downloaded tarball is extracted, after that, the script
401 will move all the content to the right place.
403 if rustc_channel is None:
404 rustc_channel = self.stage0_compiler.version
405 bin_root = self.bin_root(stage0)
407 key = self.stage0_compiler.date
409 key += str(self.rustc_commit)
410 if self.rustc(stage0).startswith(bin_root) and \
411 (not os.path.exists(self.rustc(stage0)) or
412 self.program_out_of_date(self.rustc_stamp(stage0), key)):
413 if os.path.exists(bin_root):
414 shutil.rmtree(bin_root)
415 tarball_suffix = '.tar.xz' if support_xz() else '.tar.gz'
416 filename = "rust-std-{}-{}{}".format(
417 rustc_channel, self.build, tarball_suffix)
418 pattern = "rust-std-{}".format(self.build)
419 self._download_component_helper(filename, pattern, tarball_suffix, stage0)
420 filename = "rustc-{}-{}{}".format(rustc_channel, self.build,
422 self._download_component_helper(filename, "rustc", tarball_suffix, stage0)
423 # download-rustc doesn't need its own cargo, it can just use beta's.
425 filename = "cargo-{}-{}{}".format(rustc_channel, self.build,
427 self._download_component_helper(filename, "cargo", tarball_suffix)
428 self.fix_bin_or_dylib("{}/bin/cargo".format(bin_root))
430 filename = "rustc-dev-{}-{}{}".format(rustc_channel, self.build, tarball_suffix)
431 self._download_component_helper(
432 filename, "rustc-dev", tarball_suffix, stage0
435 self.fix_bin_or_dylib("{}/bin/rustc".format(bin_root))
436 self.fix_bin_or_dylib("{}/bin/rustdoc".format(bin_root))
437 lib_dir = "{}/lib".format(bin_root)
438 for lib in os.listdir(lib_dir):
439 if lib.endswith(".so"):
440 self.fix_bin_or_dylib(os.path.join(lib_dir, lib))
441 with output(self.rustc_stamp(stage0)) as rust_stamp:
442 rust_stamp.write(key)
444 if self.rustfmt() and self.rustfmt().startswith(bin_root) and (
445 not os.path.exists(self.rustfmt())
446 or self.program_out_of_date(
447 self.rustfmt_stamp(),
448 "" if self.stage0_rustfmt is None else self.stage0_rustfmt.channel()
451 if self.stage0_rustfmt is not None:
452 tarball_suffix = '.tar.xz' if support_xz() else '.tar.gz'
453 filename = "rustfmt-{}-{}{}".format(
454 self.stage0_rustfmt.version, self.build, tarball_suffix,
456 self._download_component_helper(
457 filename, "rustfmt-preview", tarball_suffix, key=self.stage0_rustfmt.date
459 self.fix_bin_or_dylib("{}/bin/rustfmt".format(bin_root))
460 self.fix_bin_or_dylib("{}/bin/cargo-fmt".format(bin_root))
461 with output(self.rustfmt_stamp()) as rustfmt_stamp:
462 rustfmt_stamp.write(self.stage0_rustfmt.channel())
464 # Avoid downloading LLVM twice (once for stage0 and once for the master rustc)
465 if self.downloading_llvm() and stage0:
466 # We want the most recent LLVM submodule update to avoid downloading
467 # LLVM more often than necessary.
469 # This git command finds that commit SHA, looking for bors-authored
470 # merges that modified src/llvm-project or other relevant version
473 # This works even in a repository that has not yet initialized
475 top_level = subprocess.check_output([
476 "git", "rev-parse", "--show-toplevel",
477 ]).decode(sys.getdefaultencoding()).strip()
478 llvm_sha = subprocess.check_output([
479 "git", "rev-list", "--author=bors@rust-lang.org", "-n1",
480 "--merges", "--first-parent", "HEAD",
482 "{}/src/llvm-project".format(top_level),
483 "{}/src/bootstrap/download-ci-llvm-stamp".format(top_level),
484 # the LLVM shared object file is named `LLVM-12-rust-{version}-nightly`
485 "{}/src/version".format(top_level)
486 ]).decode(sys.getdefaultencoding()).strip()
487 llvm_assertions = self.get_toml('assertions', 'llvm') == 'true'
488 llvm_root = self.llvm_root()
489 llvm_lib = os.path.join(llvm_root, "lib")
490 if self.program_out_of_date(self.llvm_stamp(), llvm_sha + str(llvm_assertions)):
491 self._download_ci_llvm(llvm_sha, llvm_assertions)
492 for binary in ["llvm-config", "FileCheck"]:
493 self.fix_bin_or_dylib(os.path.join(llvm_root, "bin", binary))
494 for lib in os.listdir(llvm_lib):
495 if lib.endswith(".so"):
496 self.fix_bin_or_dylib(os.path.join(llvm_lib, lib))
497 with output(self.llvm_stamp()) as llvm_stamp:
498 llvm_stamp.write(llvm_sha + str(llvm_assertions))
500 def downloading_llvm(self):
501 opt = self.get_toml('download-ci-llvm', 'llvm')
502 # This is currently all tier 1 targets (since others may not have CI
504 # https://doc.rust-lang.org/rustc/platform-support.html#tier-1
505 supported_platforms = [
506 "aarch64-unknown-linux-gnu",
507 "i686-pc-windows-gnu",
508 "i686-pc-windows-msvc",
509 "i686-unknown-linux-gnu",
510 "x86_64-unknown-linux-gnu",
511 "x86_64-apple-darwin",
512 "x86_64-pc-windows-gnu",
513 "x86_64-pc-windows-msvc",
515 return opt == "true" \
516 or (opt == "if-available" and self.build in supported_platforms)
518 def _download_component_helper(
519 self, filename, pattern, tarball_suffix, stage0=True, key=None
523 key = self.stage0_compiler.date
525 key = self.rustc_commit
526 cache_dst = os.path.join(self.build_dir, "cache")
527 rustc_cache = os.path.join(cache_dst, key)
528 if not os.path.exists(rustc_cache):
529 os.makedirs(rustc_cache)
532 url = "{}/dist/{}".format(self._download_url, key)
534 url = "https://ci-artifacts.rust-lang.org/rustc-builds/{}".format(self.rustc_commit)
535 tarball = os.path.join(rustc_cache, filename)
536 if not os.path.exists(tarball):
537 get("{}/{}".format(url, filename), tarball, verbose=self.verbose, do_verify=stage0)
538 unpack(tarball, tarball_suffix, self.bin_root(stage0), match=pattern, verbose=self.verbose)
540 def _download_ci_llvm(self, llvm_sha, llvm_assertions):
541 cache_prefix = "llvm-{}-{}".format(llvm_sha, llvm_assertions)
542 cache_dst = os.path.join(self.build_dir, "cache")
543 rustc_cache = os.path.join(cache_dst, cache_prefix)
544 if not os.path.exists(rustc_cache):
545 os.makedirs(rustc_cache)
547 url = "https://ci-artifacts.rust-lang.org/rustc-builds/{}".format(llvm_sha)
549 url = url.replace('rustc-builds', 'rustc-builds-alt')
550 # ci-artifacts are only stored as .xz, not .gz
552 print("error: XZ support is required to download LLVM")
553 print("help: consider disabling `download-ci-llvm` or using python3")
555 tarball_suffix = '.tar.xz'
556 filename = "rust-dev-nightly-" + self.build + tarball_suffix
557 tarball = os.path.join(rustc_cache, filename)
558 if not os.path.exists(tarball):
559 get("{}/{}".format(url, filename), tarball, verbose=self.verbose, do_verify=False)
560 unpack(tarball, tarball_suffix, self.llvm_root(),
562 verbose=self.verbose)
564 def fix_bin_or_dylib(self, fname):
565 """Modifies the interpreter section of 'fname' to fix the dynamic linker,
566 or the RPATH section, to fix the dynamic library search path
568 This method is only required on NixOS and uses the PatchELF utility to
569 change the interpreter/RPATH of ELF executables.
571 Please see https://nixos.org/patchelf.html for more information
573 default_encoding = sys.getdefaultencoding()
575 ostype = subprocess.check_output(
576 ['uname', '-s']).strip().decode(default_encoding)
577 except subprocess.CalledProcessError:
579 except OSError as reason:
580 if getattr(reason, 'winerror', None) is not None:
584 if ostype != "Linux":
587 # Use `/etc/os-release` instead of `/etc/NIXOS`.
588 # The latter one does not exist on NixOS when using tmpfs as root.
590 with open("/etc/os-release", "r") as f:
591 if not any(line.strip() == "ID=nixos" for line in f):
593 except FileNotFoundError:
595 if os.path.exists("/lib"):
598 # At this point we're pretty sure the user is running NixOS
599 nix_os_msg = "info: you seem to be running NixOS. Attempting to patch"
600 print(nix_os_msg, fname)
602 # Only build `.nix-deps` once.
603 nix_deps_dir = self.nix_deps_dir
605 # Run `nix-build` to "build" each dependency (which will likely reuse
606 # the existing `/nix/store` copy, or at most download a pre-built copy).
608 # Importantly, we create a gc-root called `.nix-deps` in the `build/`
609 # directory, but still reference the actual `/nix/store` path in the rpath
610 # as it makes it significantly more robust against changes to the location of
611 # the `.nix-deps` location.
613 # bintools: Needed for the path of `ld-linux.so` (via `nix-support/dynamic-linker`).
614 # zlib: Needed as a system dependency of `libLLVM-*.so`.
615 # patchelf: Needed for patching ELF binaries (see doc comment above).
616 nix_deps_dir = "{}/{}".format(self.build_dir, ".nix-deps")
618 with (import <nixpkgs> {});
620 name = "rust-stage0-dependencies";
629 subprocess.check_output([
630 "nix-build", "-E", nix_expr, "-o", nix_deps_dir,
632 except subprocess.CalledProcessError as reason:
633 print("warning: failed to call nix-build:", reason)
635 self.nix_deps_dir = nix_deps_dir
637 patchelf = "{}/bin/patchelf".format(nix_deps_dir)
639 # Relative default, all binary and dynamic libraries we ship
640 # appear to have this (even when `../lib` is redundant).
642 os.path.join(os.path.realpath(nix_deps_dir), "lib")
644 patchelf_args = ["--set-rpath", ":".join(rpath_entries)]
645 if not fname.endswith(".so"):
646 # Finally, set the corret .interp for binaries
647 with open("{}/nix-support/dynamic-linker".format(nix_deps_dir)) as dynamic_linker:
648 patchelf_args += ["--set-interpreter", dynamic_linker.read().rstrip()]
651 subprocess.check_output([patchelf] + patchelf_args + [fname])
652 except subprocess.CalledProcessError as reason:
653 print("warning: failed to call patchelf:", reason)
656 # If `download-rustc` is set, download the most recent commit with CI artifacts
657 def maybe_download_ci_toolchain(self):
658 # If `download-rustc` is not set, default to rebuilding.
659 download_rustc = self.get_toml("download-rustc", section="rust")
660 if download_rustc is None or download_rustc == "false":
662 assert download_rustc == "true" or download_rustc == "if-unchanged", download_rustc
664 # Handle running from a directory other than the top level
665 rev_parse = ["git", "rev-parse", "--show-toplevel"]
666 top_level = subprocess.check_output(rev_parse, universal_newlines=True).strip()
667 compiler = "{}/compiler/".format(top_level)
668 library = "{}/library/".format(top_level)
670 # Look for a version to compare to based on the current commit.
671 # Only commits merged by bors will have CI artifacts.
673 "git", "rev-list", "--author=bors@rust-lang.org", "-n1",
674 "--merges", "--first-parent", "HEAD"
676 commit = subprocess.check_output(merge_base, universal_newlines=True).strip()
678 # Warn if there were changes to the compiler or standard library since the ancestor commit.
679 status = subprocess.call(["git", "diff-index", "--quiet", commit, "--", compiler, library])
681 if download_rustc == "if-unchanged":
683 print("warning: `download-rustc` is enabled, but there are changes to \
684 compiler/ or library/")
687 print("using downloaded stage1 artifacts from CI (commit {})".format(commit))
688 self.rustc_commit = commit
689 # FIXME: support downloading artifacts from the beta channel
690 self.download_toolchain(False, "nightly")
692 def rustc_stamp(self, stage0):
693 """Return the path for .rustc-stamp at the given stage
696 >>> rb.build_dir = "build"
697 >>> rb.rustc_stamp(True) == os.path.join("build", "stage0", ".rustc-stamp")
699 >>> rb.rustc_stamp(False) == os.path.join("build", "ci-rustc", ".rustc-stamp")
702 return os.path.join(self.bin_root(stage0), '.rustc-stamp')
704 def rustfmt_stamp(self):
705 """Return the path for .rustfmt-stamp
708 >>> rb.build_dir = "build"
709 >>> rb.rustfmt_stamp() == os.path.join("build", "stage0", ".rustfmt-stamp")
712 return os.path.join(self.bin_root(True), '.rustfmt-stamp')
714 def llvm_stamp(self):
715 """Return the path for .rustfmt-stamp
718 >>> rb.build_dir = "build"
719 >>> rb.llvm_stamp() == os.path.join("build", "ci-llvm", ".llvm-stamp")
722 return os.path.join(self.llvm_root(), '.llvm-stamp')
725 def program_out_of_date(self, stamp_path, key):
726 """Check if the given program stamp is out of date"""
727 if not os.path.exists(stamp_path) or self.clean:
729 with open(stamp_path, 'r') as stamp:
730 return key != stamp.read()
732 def bin_root(self, stage0):
733 """Return the binary root directory for the given stage
736 >>> rb.build_dir = "build"
737 >>> rb.bin_root(True) == os.path.join("build", "stage0")
739 >>> rb.bin_root(False) == os.path.join("build", "ci-rustc")
742 When the 'build' property is given should be a nested directory:
744 >>> rb.build = "devel"
745 >>> rb.bin_root(True) == os.path.join("build", "devel", "stage0")
752 return os.path.join(self.build_dir, self.build, subdir)
755 """Return the CI LLVM root directory
758 >>> rb.build_dir = "build"
759 >>> rb.llvm_root() == os.path.join("build", "ci-llvm")
762 When the 'build' property is given should be a nested directory:
764 >>> rb.build = "devel"
765 >>> rb.llvm_root() == os.path.join("build", "devel", "ci-llvm")
768 return os.path.join(self.build_dir, self.build, "ci-llvm")
770 def get_toml(self, key, section=None):
771 """Returns the value of the given key in config.toml, otherwise returns None
774 >>> rb.config_toml = 'key1 = "value1"\\nkey2 = "value2"'
775 >>> rb.get_toml("key2")
778 If the key does not exists, the result is None:
780 >>> rb.get_toml("key3") is None
783 Optionally also matches the section the key appears in
785 >>> rb.config_toml = '[a]\\nkey = "value1"\\n[b]\\nkey = "value2"'
786 >>> rb.get_toml('key', 'a')
788 >>> rb.get_toml('key', 'b')
790 >>> rb.get_toml('key', 'c') is None
793 >>> rb.config_toml = 'key1 = true'
794 >>> rb.get_toml("key1")
799 for line in self.config_toml.splitlines():
800 section_match = re.match(r'^\s*\[(.*)\]\s*$', line)
801 if section_match is not None:
802 cur_section = section_match.group(1)
804 match = re.match(r'^{}\s*=(.*)$'.format(key), line)
805 if match is not None:
806 value = match.group(1)
807 if section is None or section == cur_section:
808 return self.get_string(value) or value.strip()
812 """Return config path for cargo"""
813 return self.program_config('cargo')
815 def rustc(self, stage0):
816 """Return config path for rustc"""
817 return self.program_config('rustc', stage0)
820 """Return config path for rustfmt"""
821 if self.stage0_rustfmt is None:
823 return self.program_config('rustfmt')
825 def program_config(self, program, stage0=True):
826 """Return config path for the given program at the given stage
829 >>> rb.config_toml = 'rustc = "rustc"\\n'
830 >>> rb.program_config('rustc')
832 >>> rb.config_toml = ''
833 >>> cargo_path = rb.program_config('cargo', True)
834 >>> cargo_path.rstrip(".exe") == os.path.join(rb.bin_root(True),
837 >>> cargo_path = rb.program_config('cargo', False)
838 >>> cargo_path.rstrip(".exe") == os.path.join(rb.bin_root(False),
842 config = self.get_toml(program)
844 return os.path.expanduser(config)
845 return os.path.join(self.bin_root(stage0), "bin", "{}{}".format(
846 program, self.exe_suffix()))
849 def get_string(line):
850 """Return the value between double quotes
852 >>> RustBuild.get_string(' "devel" ')
854 >>> RustBuild.get_string(" 'devel' ")
856 >>> RustBuild.get_string('devel') is None
858 >>> RustBuild.get_string(' "devel ')
861 start = line.find('"')
863 end = start + 1 + line[start + 1:].find('"')
864 return line[start + 1:end]
865 start = line.find('\'')
867 end = start + 1 + line[start + 1:].find('\'')
868 return line[start + 1:end]
873 """Return a suffix for executables"""
874 if sys.platform == 'win32':
878 def bootstrap_binary(self):
879 """Return the path of the bootstrap binary
882 >>> rb.build_dir = "build"
883 >>> rb.bootstrap_binary() == os.path.join("build", "bootstrap",
884 ... "debug", "bootstrap")
887 return os.path.join(self.build_dir, "bootstrap", "debug", "bootstrap")
889 def build_bootstrap(self):
890 """Build bootstrap"""
891 build_dir = os.path.join(self.build_dir, "bootstrap")
892 if self.clean and os.path.exists(build_dir):
893 shutil.rmtree(build_dir)
894 env = os.environ.copy()
895 # `CARGO_BUILD_TARGET` breaks bootstrap build.
896 # See also: <https://github.com/rust-lang/rust/issues/70208>.
897 if "CARGO_BUILD_TARGET" in env:
898 del env["CARGO_BUILD_TARGET"]
899 env["CARGO_TARGET_DIR"] = build_dir
900 env["RUSTC"] = self.rustc(True)
901 env["LD_LIBRARY_PATH"] = os.path.join(self.bin_root(True), "lib") + \
902 (os.pathsep + env["LD_LIBRARY_PATH"]) \
903 if "LD_LIBRARY_PATH" in env else ""
904 env["DYLD_LIBRARY_PATH"] = os.path.join(self.bin_root(True), "lib") + \
905 (os.pathsep + env["DYLD_LIBRARY_PATH"]) \
906 if "DYLD_LIBRARY_PATH" in env else ""
907 env["LIBRARY_PATH"] = os.path.join(self.bin_root(True), "lib") + \
908 (os.pathsep + env["LIBRARY_PATH"]) \
909 if "LIBRARY_PATH" in env else ""
910 # preserve existing RUSTFLAGS
911 env.setdefault("RUSTFLAGS", "")
912 env["RUSTFLAGS"] += " -Cdebuginfo=2"
914 build_section = "target.{}".format(self.build)
916 if self.get_toml("crt-static", build_section) == "true":
917 target_features += ["+crt-static"]
918 elif self.get_toml("crt-static", build_section) == "false":
919 target_features += ["-crt-static"]
921 env["RUSTFLAGS"] += " -C target-feature=" + (",".join(target_features))
922 target_linker = self.get_toml("linker", build_section)
923 if target_linker is not None:
924 env["RUSTFLAGS"] += " -C linker=" + target_linker
925 env["RUSTFLAGS"] += " -Wrust_2018_idioms -Wunused_lifetimes"
926 env["RUSTFLAGS"] += " -Wsemicolon_in_expressions_from_macros"
927 if self.get_toml("deny-warnings", "rust") != "false":
928 env["RUSTFLAGS"] += " -Dwarnings"
930 env["PATH"] = os.path.join(self.bin_root(True), "bin") + \
931 os.pathsep + env["PATH"]
932 if not os.path.isfile(self.cargo()):
933 raise Exception("no cargo executable found at `{}`".format(
935 args = [self.cargo(), "build", "--manifest-path",
936 os.path.join(self.rust_root, "src/bootstrap/Cargo.toml")]
937 for _ in range(1, self.verbose):
938 args.append("--verbose")
939 if self.use_locked_deps:
940 args.append("--locked")
941 if self.use_vendored_sources:
942 args.append("--frozen")
943 run(args, env=env, verbose=self.verbose)
945 def build_triple(self):
946 """Build triple as in LLVM
948 Note that `default_build_triple` is moderately expensive,
949 so use `self.build` where possible.
951 config = self.get_toml('build')
954 return default_build_triple(self.verbose)
956 def check_submodule(self, module, slow_submodules):
957 if not slow_submodules:
958 checked_out = subprocess.Popen(["git", "rev-parse", "HEAD"],
959 cwd=os.path.join(self.rust_root, module),
960 stdout=subprocess.PIPE)
965 def update_submodule(self, module, checked_out, recorded_submodules):
966 module_path = os.path.join(self.rust_root, module)
968 if checked_out is not None:
969 default_encoding = sys.getdefaultencoding()
970 checked_out = checked_out.communicate()[0].decode(default_encoding).strip()
971 if recorded_submodules[module] == checked_out:
974 print("Updating submodule", module)
976 run(["git", "submodule", "-q", "sync", module],
977 cwd=self.rust_root, verbose=self.verbose)
979 update_args = ["git", "submodule", "update", "--init", "--recursive"]
980 if self.git_version >= distutils.version.LooseVersion("2.11.0"):
981 update_args.append("--progress")
982 update_args.append(module)
983 run(update_args, cwd=self.rust_root, verbose=self.verbose, exception=True)
985 run(["git", "reset", "-q", "--hard"],
986 cwd=module_path, verbose=self.verbose)
987 run(["git", "clean", "-qdfx"],
988 cwd=module_path, verbose=self.verbose)
990 def update_submodules(self):
991 """Update submodules"""
992 if (not os.path.exists(os.path.join(self.rust_root, ".git"))) or \
993 self.get_toml('submodules') == "false":
996 default_encoding = sys.getdefaultencoding()
998 # check the existence and version of 'git' command
999 git_version_str = require(['git', '--version']).split()[2].decode(default_encoding)
1000 self.git_version = distutils.version.LooseVersion(git_version_str)
1002 slow_submodules = self.get_toml('fast-submodules') == "false"
1005 print('Unconditionally updating submodules')
1007 print('Updating only changed submodules')
1008 default_encoding = sys.getdefaultencoding()
1009 # Only update submodules that are needed to build bootstrap. These are needed because Cargo
1010 # currently requires everything in a workspace to be "locally present" when starting a
1011 # build, and will give a hard error if any Cargo.toml files are missing.
1012 # FIXME: Is there a way to avoid cloning these eagerly? Bootstrap itself doesn't need to
1013 # share a workspace with any tools - maybe it could be excluded from the workspace?
1014 # That will still require cloning the submodules the second you check the standard
1015 # library, though...
1016 # FIXME: Is there a way to avoid hard-coding the submodules required?
1017 # WARNING: keep this in sync with the submodules hard-coded in bootstrap/lib.rs
1019 "src/tools/rust-installer",
1023 "library/backtrace",
1026 filtered_submodules = []
1027 submodules_names = []
1028 for module in submodules:
1029 check = self.check_submodule(module, slow_submodules)
1030 filtered_submodules.append((module, check))
1031 submodules_names.append(module)
1032 recorded = subprocess.Popen(["git", "ls-tree", "HEAD"] + submodules_names,
1033 cwd=self.rust_root, stdout=subprocess.PIPE)
1034 recorded = recorded.communicate()[0].decode(default_encoding).strip().splitlines()
1035 # { filename: hash }
1036 recorded_submodules = {}
1037 for data in recorded:
1038 # [mode, kind, hash, filename]
1040 recorded_submodules[data[3]] = data[2]
1041 for module in filtered_submodules:
1042 self.update_submodule(module[0], module[1], recorded_submodules)
1043 print("Submodules updated in %.2f seconds" % (time() - start_time))
1045 def set_dist_environment(self, url):
1046 """Set download URL for normal environment"""
1047 if 'RUSTUP_DIST_SERVER' in os.environ:
1048 self._download_url = os.environ['RUSTUP_DIST_SERVER']
1050 self._download_url = url
1052 def check_vendored_status(self):
1053 """Check that vendoring is configured properly"""
1054 vendor_dir = os.path.join(self.rust_root, 'vendor')
1055 if 'SUDO_USER' in os.environ and not self.use_vendored_sources:
1056 if os.environ.get('USER') != os.environ['SUDO_USER']:
1057 self.use_vendored_sources = True
1058 print('info: looks like you are running this command under `sudo`')
1059 print(' and so in order to preserve your $HOME this will now')
1060 print(' use vendored sources by default.')
1061 if not os.path.exists(vendor_dir):
1062 print('error: vendoring required, but vendor directory does not exist.')
1063 print(' Run `cargo vendor` without sudo to initialize the '
1064 'vendor directory.')
1065 raise Exception("{} not found".format(vendor_dir))
1067 if self.use_vendored_sources:
1068 if not os.path.exists('.cargo'):
1069 os.makedirs('.cargo')
1070 with output('.cargo/config') as cargo_config:
1072 "[source.crates-io]\n"
1073 "replace-with = 'vendored-sources'\n"
1074 "registry = 'https://example.com'\n"
1076 "[source.vendored-sources]\n"
1077 "directory = '{}/vendor'\n"
1078 .format(self.rust_root))
1080 if os.path.exists('.cargo'):
1081 shutil.rmtree('.cargo')
1083 def ensure_vendored(self):
1084 """Ensure that the vendored sources are available if needed"""
1085 vendor_dir = os.path.join(self.rust_root, 'vendor')
1086 # Note that this does not handle updating the vendored dependencies if
1087 # the rust git repository is updated. Normal development usually does
1088 # not use vendoring, so hopefully this isn't too much of a problem.
1089 if self.use_vendored_sources and not os.path.exists(vendor_dir):
1093 "--sync=./src/tools/rust-analyzer/Cargo.toml",
1094 "--sync=./compiler/rustc_codegen_cranelift/Cargo.toml",
1095 ], verbose=self.verbose, cwd=self.rust_root)
1098 def bootstrap(help_triggered):
1099 """Configure, fetch, build and run the initial bootstrap"""
1101 # If the user is asking for help, let them know that the whole download-and-build
1102 # process has to happen before anything is printed out.
1104 print("info: Downloading and building bootstrap before processing --help")
1105 print(" command. See src/bootstrap/README.md for help with common")
1108 parser = argparse.ArgumentParser(description='Build rust')
1109 parser.add_argument('--config')
1110 parser.add_argument('--build')
1111 parser.add_argument('--clean', action='store_true')
1112 parser.add_argument('-v', '--verbose', action='count', default=0)
1114 args = [a for a in sys.argv if a != '-h' and a != '--help']
1115 args, _ = parser.parse_known_args(args)
1117 # Configure initial bootstrap
1119 build.rust_root = os.path.abspath(os.path.join(__file__, '../../..'))
1120 build.verbose = args.verbose
1121 build.clean = args.clean
1123 # Read from `RUST_BOOTSTRAP_CONFIG`, then `--config`, then fallback to `config.toml` (if it
1125 toml_path = os.getenv('RUST_BOOTSTRAP_CONFIG') or args.config
1126 if not toml_path and os.path.exists('config.toml'):
1127 toml_path = 'config.toml'
1130 if not os.path.exists(toml_path):
1131 toml_path = os.path.join(build.rust_root, toml_path)
1133 with open(toml_path) as config:
1134 build.config_toml = config.read()
1136 profile = build.get_toml('profile')
1137 if profile is not None:
1138 include_file = 'config.{}.toml'.format(profile)
1139 include_dir = os.path.join(build.rust_root, 'src', 'bootstrap', 'defaults')
1140 include_path = os.path.join(include_dir, include_file)
1141 # HACK: This works because `build.get_toml()` returns the first match it finds for a
1142 # specific key, so appending our defaults at the end allows the user to override them
1143 with open(include_path) as included_toml:
1144 build.config_toml += os.linesep + included_toml.read()
1146 config_verbose = build.get_toml('verbose', 'build')
1147 if config_verbose is not None:
1148 build.verbose = max(build.verbose, int(config_verbose))
1150 build.use_vendored_sources = build.get_toml('vendor', 'build') == 'true'
1152 build.use_locked_deps = build.get_toml('locked-deps', 'build') == 'true'
1154 build.check_vendored_status()
1156 build_dir = build.get_toml('build-dir', 'build') or 'build'
1157 build.build_dir = os.path.abspath(build_dir.replace("$ROOT", build.rust_root))
1159 with open(os.path.join(build.rust_root, "src", "stage0.json")) as f:
1161 build.stage0_compiler = Stage0Toolchain(data["compiler"])
1162 if data.get("rustfmt") is not None:
1163 build.stage0_rustfmt = Stage0Toolchain(data["rustfmt"])
1165 build.set_dist_environment(data["dist_server"])
1167 build.build = args.build or build.build_triple()
1168 build.update_submodules()
1170 # Fetch/build the bootstrap
1171 build.download_toolchain()
1172 # Download the master compiler if `download-rustc` is set
1173 build.maybe_download_ci_toolchain()
1175 build.ensure_vendored()
1176 build.build_bootstrap()
1180 args = [build.bootstrap_binary()]
1181 args.extend(sys.argv[1:])
1182 env = os.environ.copy()
1183 env["BOOTSTRAP_PARENT_ID"] = str(os.getpid())
1184 env["BOOTSTRAP_PYTHON"] = sys.executable
1185 env["BUILD_DIR"] = build.build_dir
1186 env["RUSTC_BOOTSTRAP"] = '1'
1188 env["BOOTSTRAP_CONFIG"] = toml_path
1189 if build.rustc_commit is not None:
1190 env["BOOTSTRAP_DOWNLOAD_RUSTC"] = '1'
1191 run(args, env=env, verbose=build.verbose, is_bootstrap=True)
1195 """Entry point for the bootstrap process"""
1198 # x.py help <cmd> ...
1199 if len(sys.argv) > 1 and sys.argv[1] == 'help':
1200 sys.argv = [sys.argv[0], '-h'] + sys.argv[2:]
1203 '-h' in sys.argv) or ('--help' in sys.argv) or (len(sys.argv) == 1)
1205 bootstrap(help_triggered)
1206 if not help_triggered:
1207 print("Build completed successfully in {}".format(
1208 format_build_time(time() - start_time)))
1209 except (SystemExit, KeyboardInterrupt) as error:
1210 if hasattr(error, 'code') and isinstance(error.code, int):
1211 exit_code = error.code
1215 if not help_triggered:
1216 print("Build completed unsuccessfully in {}".format(
1217 format_build_time(time() - start_time)))
1221 if __name__ == '__main__':