1 from __future__ import absolute_import, division, print_function
5 import distutils.version
19 with tempfile.NamedTemporaryFile(delete=False) as temp_file:
20 temp_path = temp_file.name
21 with tarfile.open(temp_path, "w:xz"):
24 except tarfile.CompressionError:
27 def get(url, path, verbose=False, do_verify=True):
29 sha_url = url + suffix
30 with tempfile.NamedTemporaryFile(delete=False) as temp_file:
31 temp_path = temp_file.name
32 with tempfile.NamedTemporaryFile(suffix=suffix, delete=False) as sha_file:
33 sha_path = sha_file.name
37 download(sha_path, sha_url, False, verbose)
38 if os.path.exists(path):
39 if verify(path, sha_path, False):
41 print("using already-download file", path)
45 print("ignoring already-download file",
46 path, "due to failed verification")
48 download(temp_path, url, True, verbose)
49 if do_verify and not verify(temp_path, sha_path, verbose):
50 raise RuntimeError("failed verification")
52 print("moving {} to {}".format(temp_path, path))
53 shutil.move(temp_path, path)
55 delete_if_present(sha_path, verbose)
56 delete_if_present(temp_path, verbose)
59 def delete_if_present(path, verbose):
60 """Remove the given file if present"""
61 if os.path.isfile(path):
63 print("removing", path)
67 def download(path, url, probably_big, verbose):
70 _download(path, url, probably_big, verbose, True)
73 print("\nspurious failure, trying again")
74 _download(path, url, probably_big, verbose, False)
77 def _download(path, url, probably_big, verbose, exception):
78 if probably_big or verbose:
79 print("downloading {}".format(url))
80 # see https://serverfault.com/questions/301128/how-to-download
81 if sys.platform == 'win32':
82 run(["PowerShell.exe", "/nologo", "-Command",
83 "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12;",
84 "(New-Object System.Net.WebClient).DownloadFile('{}', '{}')".format(url, path)],
88 if probably_big or verbose:
92 require(["curl", "--version"])
94 "-y", "30", "-Y", "10", # timeout if speed is < 10 bytes/sec for > 30 seconds
95 "--connect-timeout", "30", # timeout if cannot connect within 30 seconds
96 "--retry", "3", "-Sf", "-o", path, url],
101 def verify(path, sha_path, verbose):
102 """Check if the sha256 sum of the given path is valid"""
104 print("verifying", path)
105 with open(path, "rb") as source:
106 found = hashlib.sha256(source.read()).hexdigest()
107 with open(sha_path, "r") as sha256sum:
108 expected = sha256sum.readline().split()[0]
109 verified = found == expected
111 print("invalid checksum:\n"
113 " expected: {}".format(found, expected))
117 def unpack(tarball, tarball_suffix, dst, verbose=False, match=None):
118 """Unpack the given tarball file"""
119 print("extracting", tarball)
120 fname = os.path.basename(tarball).replace(tarball_suffix, "")
121 with contextlib.closing(tarfile.open(tarball)) as tar:
122 for member in tar.getnames():
123 if "/" not in member:
125 name = member.replace(fname + "/", "", 1)
126 if match is not None and not name.startswith(match):
128 name = name[len(match) + 1:]
130 dst_path = os.path.join(dst, name)
132 print(" extracting", member)
133 tar.extract(member, dst)
134 src_path = os.path.join(dst, member)
135 if os.path.isdir(src_path) and os.path.exists(dst_path):
137 shutil.move(src_path, dst_path)
138 shutil.rmtree(os.path.join(dst, fname))
141 def run(args, verbose=False, exception=False, is_bootstrap=False, **kwargs):
142 """Run a child program in a new process"""
144 print("running: " + ' '.join(args))
146 # Use Popen here instead of call() as it apparently allows powershell on
147 # Windows to not lock up waiting for input presumably.
148 ret = subprocess.Popen(args, **kwargs)
151 err = "failed to run: " + ' '.join(args)
152 if verbose or exception:
153 raise RuntimeError(err)
154 # For most failures, we definitely do want to print this error, or the user will have no
155 # idea what went wrong. But when we've successfully built bootstrap and it failed, it will
156 # have already printed an error above, so there's no need to print the exact command we're
164 def require(cmd, exit=True):
165 '''Run a command, returning its output.
167 If `exit` is `True`, exit the process.
168 Otherwise, return None.'''
170 return subprocess.check_output(cmd).strip()
171 except (subprocess.CalledProcessError, OSError) as exc:
174 print("error: unable to run `{}`: {}".format(' '.join(cmd), exc))
175 print("Please make sure it's installed and in the path.")
179 def stage0_data(rust_root):
180 """Build a dictionary from stage0.txt"""
181 nightlies = os.path.join(rust_root, "src/stage0.txt")
182 with open(nightlies, 'r') as nightlies:
183 lines = [line.rstrip() for line in nightlies
184 if not line.startswith("#")]
185 return dict([line.split(": ", 1) for line in lines if line])
188 def format_build_time(duration):
189 """Return a nicer format for build time
191 >>> format_build_time('300')
194 return str(datetime.timedelta(seconds=int(duration)))
197 def default_build_triple(verbose):
198 """Build triple as in LLVM"""
199 # If the user already has a host build triple with an existing `rustc`
200 # install, use their preference. This fixes most issues with Windows builds
201 # being detected as GNU instead of MSVC.
202 default_encoding = sys.getdefaultencoding()
204 version = subprocess.check_output(["rustc", "--version", "--verbose"],
205 stderr=subprocess.DEVNULL)
206 version = version.decode(default_encoding)
207 host = next(x for x in version.split('\n') if x.startswith("host: "))
208 triple = host.split("host: ")[1]
210 print("detected default triple {}".format(triple))
212 except Exception as e:
214 print("rustup not detected: {}".format(e))
215 print("falling back to auto-detect")
217 required = sys.platform != 'win32'
218 ostype = require(["uname", "-s"], exit=required)
219 cputype = require(['uname', '-m'], exit=required)
221 # If we do not have `uname`, assume Windows.
222 if ostype is None or cputype is None:
223 return 'x86_64-pc-windows-msvc'
225 ostype = ostype.decode(default_encoding)
226 cputype = cputype.decode(default_encoding)
228 # The goal here is to come up with the same triple as LLVM would,
229 # at least for the subset of platforms we're willing to target.
231 'Darwin': 'apple-darwin',
232 'DragonFly': 'unknown-dragonfly',
233 'FreeBSD': 'unknown-freebsd',
234 'Haiku': 'unknown-haiku',
235 'NetBSD': 'unknown-netbsd',
236 'OpenBSD': 'unknown-openbsd'
239 # Consider the direct transformation first and then the special cases
240 if ostype in ostype_mapper:
241 ostype = ostype_mapper[ostype]
242 elif ostype == 'Linux':
243 os_from_sp = subprocess.check_output(
244 ['uname', '-o']).strip().decode(default_encoding)
245 if os_from_sp == 'Android':
246 ostype = 'linux-android'
248 ostype = 'unknown-linux-gnu'
249 elif ostype == 'SunOS':
250 ostype = 'pc-solaris'
251 # On Solaris, uname -m will return a machine classification instead
252 # of a cpu type, so uname -p is recommended instead. However, the
253 # output from that option is too generic for our purposes (it will
254 # always emit 'i386' on x86/amd64 systems). As such, isainfo -k
255 # must be used instead.
256 cputype = require(['isainfo', '-k']).decode(default_encoding)
257 # sparc cpus have sun as a target vendor
258 if 'sparc' in cputype:
259 ostype = 'sun-solaris'
260 elif ostype.startswith('MINGW'):
261 # msys' `uname` does not print gcc configuration, but prints msys
262 # configuration. so we cannot believe `uname -m`:
263 # msys1 is always i686 and msys2 is always x86_64.
264 # instead, msys defines $MSYSTEM which is MINGW32 on i686 and
266 ostype = 'pc-windows-gnu'
268 if os.environ.get('MSYSTEM') == 'MINGW64':
270 elif ostype.startswith('MSYS'):
271 ostype = 'pc-windows-gnu'
272 elif ostype.startswith('CYGWIN_NT'):
274 if ostype.endswith('WOW64'):
276 ostype = 'pc-windows-gnu'
277 elif sys.platform == 'win32':
278 # Some Windows platforms might have a `uname` command that returns a
279 # non-standard string (e.g. gnuwin32 tools returns `windows32`). In
280 # these cases, fall back to using sys.platform.
281 return 'x86_64-pc-windows-msvc'
283 err = "unknown OS type: {}".format(ostype)
286 if cputype == 'powerpc' and ostype == 'unknown-freebsd':
287 cputype = subprocess.check_output(
288 ['uname', '-p']).strip().decode(default_encoding)
291 'aarch64': 'aarch64',
298 'powerpc': 'powerpc',
299 'powerpc64': 'powerpc64',
300 'powerpc64le': 'powerpc64le',
302 'ppc64': 'powerpc64',
303 'ppc64le': 'powerpc64le',
311 # Consider the direct transformation first and then the special cases
312 if cputype in cputype_mapper:
313 cputype = cputype_mapper[cputype]
314 elif cputype in {'xscale', 'arm'}:
316 if ostype == 'linux-android':
317 ostype = 'linux-androideabi'
318 elif ostype == 'unknown-freebsd':
319 cputype = subprocess.check_output(
320 ['uname', '-p']).strip().decode(default_encoding)
321 ostype = 'unknown-freebsd'
322 elif cputype == 'armv6l':
324 if ostype == 'linux-android':
325 ostype = 'linux-androideabi'
328 elif cputype in {'armv7l', 'armv8l'}:
330 if ostype == 'linux-android':
331 ostype = 'linux-androideabi'
334 elif cputype == 'mips':
335 if sys.byteorder == 'big':
337 elif sys.byteorder == 'little':
340 raise ValueError("unknown byteorder: {}".format(sys.byteorder))
341 elif cputype == 'mips64':
342 if sys.byteorder == 'big':
344 elif sys.byteorder == 'little':
347 raise ValueError('unknown byteorder: {}'.format(sys.byteorder))
348 # only the n64 ABI is supported, indicate it
350 elif cputype == 'sparc' or cputype == 'sparcv9' or cputype == 'sparc64':
353 err = "unknown cpu type: {}".format(cputype)
356 return "{}-{}".format(cputype, ostype)
359 @contextlib.contextmanager
360 def output(filepath):
361 tmp = filepath + '.tmp'
362 with open(tmp, 'w') as f:
365 if os.path.exists(filepath):
366 os.remove(filepath) # PermissionError/OSError on Win32 if in use
368 shutil.copy2(tmp, filepath)
371 os.rename(tmp, filepath)
374 class RustBuild(object):
375 """Provide all the methods required to build Rust"""
378 self._download_url = ''
379 self.rustc_channel = ''
380 self.rustfmt_channel = ''
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.rustc_channel
405 rustfmt_channel = self.rustfmt_channel
406 bin_root = self.bin_root(stage0)
410 key += str(self.rustc_commit)
411 if self.rustc(stage0).startswith(bin_root) and \
412 (not os.path.exists(self.rustc(stage0)) or
413 self.program_out_of_date(self.rustc_stamp(stage0), key)):
414 if os.path.exists(bin_root):
415 shutil.rmtree(bin_root)
416 tarball_suffix = '.tar.xz' if support_xz() else '.tar.gz'
417 filename = "rust-std-{}-{}{}".format(
418 rustc_channel, self.build, tarball_suffix)
419 pattern = "rust-std-{}".format(self.build)
420 self._download_component_helper(filename, pattern, tarball_suffix, stage0)
421 filename = "rustc-{}-{}{}".format(rustc_channel, self.build,
423 self._download_component_helper(filename, "rustc", tarball_suffix, stage0)
424 # download-rustc doesn't need its own cargo, it can just use beta's.
426 filename = "cargo-{}-{}{}".format(rustc_channel, self.build,
428 self._download_component_helper(filename, "cargo", tarball_suffix)
429 self.fix_bin_or_dylib("{}/bin/cargo".format(bin_root))
431 filename = "rustc-dev-{}-{}{}".format(rustc_channel, self.build, tarball_suffix)
432 self._download_component_helper(
433 filename, "rustc-dev", tarball_suffix, stage0
436 self.fix_bin_or_dylib("{}/bin/rustc".format(bin_root))
437 self.fix_bin_or_dylib("{}/bin/rustdoc".format(bin_root))
438 lib_dir = "{}/lib".format(bin_root)
439 for lib in os.listdir(lib_dir):
440 if lib.endswith(".so"):
441 self.fix_bin_or_dylib(os.path.join(lib_dir, lib))
442 with output(self.rustc_stamp(stage0)) as rust_stamp:
443 rust_stamp.write(key)
445 if self.rustfmt() and self.rustfmt().startswith(bin_root) and (
446 not os.path.exists(self.rustfmt())
447 or self.program_out_of_date(self.rustfmt_stamp(), self.rustfmt_channel)
450 tarball_suffix = '.tar.xz' if support_xz() else '.tar.gz'
451 [channel, date] = rustfmt_channel.split('-', 1)
452 filename = "rustfmt-{}-{}{}".format(channel, self.build, tarball_suffix)
453 self._download_component_helper(
454 filename, "rustfmt-preview", tarball_suffix, key=date
456 self.fix_bin_or_dylib("{}/bin/rustfmt".format(bin_root))
457 self.fix_bin_or_dylib("{}/bin/cargo-fmt".format(bin_root))
458 with output(self.rustfmt_stamp()) as rustfmt_stamp:
459 rustfmt_stamp.write(self.rustfmt_channel)
461 # Avoid downloading LLVM twice (once for stage0 and once for the master rustc)
462 if self.downloading_llvm() and stage0:
463 # We want the most recent LLVM submodule update to avoid downloading
464 # LLVM more often than necessary.
466 # This git command finds that commit SHA, looking for bors-authored
467 # commits that modified src/llvm-project or other relevant version
470 # This works even in a repository that has not yet initialized
472 top_level = subprocess.check_output([
473 "git", "rev-parse", "--show-toplevel",
474 ]).decode(sys.getdefaultencoding()).strip()
475 llvm_sha = subprocess.check_output([
476 "git", "rev-list", "--author=bors@rust-lang.org", "-n1",
477 "--first-parent", "HEAD",
479 "{}/src/llvm-project".format(top_level),
480 "{}/src/bootstrap/download-ci-llvm-stamp".format(top_level),
481 # the LLVM shared object file is named `LLVM-12-rust-{version}-nightly`
482 "{}/src/version".format(top_level)
483 ]).decode(sys.getdefaultencoding()).strip()
484 llvm_assertions = self.get_toml('assertions', 'llvm') == 'true'
485 llvm_root = self.llvm_root()
486 llvm_lib = os.path.join(llvm_root, "lib")
487 if self.program_out_of_date(self.llvm_stamp(), llvm_sha + str(llvm_assertions)):
488 self._download_ci_llvm(llvm_sha, llvm_assertions)
489 for binary in ["llvm-config", "FileCheck"]:
490 self.fix_bin_or_dylib(os.path.join(llvm_root, "bin", binary))
491 for lib in os.listdir(llvm_lib):
492 if lib.endswith(".so"):
493 self.fix_bin_or_dylib(os.path.join(llvm_lib, lib))
494 with output(self.llvm_stamp()) as llvm_stamp:
495 llvm_stamp.write(llvm_sha + str(llvm_assertions))
497 def downloading_llvm(self):
498 opt = self.get_toml('download-ci-llvm', 'llvm')
499 # This is currently all tier 1 targets (since others may not have CI
501 # https://doc.rust-lang.org/rustc/platform-support.html#tier-1
502 supported_platforms = [
503 "aarch64-unknown-linux-gnu",
504 "i686-pc-windows-gnu",
505 "i686-pc-windows-msvc",
506 "i686-unknown-linux-gnu",
507 "x86_64-unknown-linux-gnu",
508 "x86_64-apple-darwin",
509 "x86_64-pc-windows-gnu",
510 "x86_64-pc-windows-msvc",
512 return opt == "true" \
513 or (opt == "if-available" and self.build in supported_platforms)
515 def _download_component_helper(
516 self, filename, pattern, tarball_suffix, stage0=True, key=None
522 key = self.rustc_commit
523 cache_dst = os.path.join(self.build_dir, "cache")
524 rustc_cache = os.path.join(cache_dst, key)
525 if not os.path.exists(rustc_cache):
526 os.makedirs(rustc_cache)
529 url = "{}/dist/{}".format(self._download_url, key)
531 url = "https://ci-artifacts.rust-lang.org/rustc-builds/{}".format(self.rustc_commit)
532 tarball = os.path.join(rustc_cache, filename)
533 if not os.path.exists(tarball):
534 get("{}/{}".format(url, filename), tarball, verbose=self.verbose, do_verify=stage0)
535 unpack(tarball, tarball_suffix, self.bin_root(stage0), match=pattern, verbose=self.verbose)
537 def _download_ci_llvm(self, llvm_sha, llvm_assertions):
538 cache_prefix = "llvm-{}-{}".format(llvm_sha, llvm_assertions)
539 cache_dst = os.path.join(self.build_dir, "cache")
540 rustc_cache = os.path.join(cache_dst, cache_prefix)
541 if not os.path.exists(rustc_cache):
542 os.makedirs(rustc_cache)
544 url = "https://ci-artifacts.rust-lang.org/rustc-builds/{}".format(llvm_sha)
546 url = url.replace('rustc-builds', 'rustc-builds-alt')
547 # ci-artifacts are only stored as .xz, not .gz
549 print("error: XZ support is required to download LLVM")
550 print("help: consider disabling `download-ci-llvm` or using python3")
552 tarball_suffix = '.tar.xz'
553 filename = "rust-dev-nightly-" + self.build + tarball_suffix
554 tarball = os.path.join(rustc_cache, filename)
555 if not os.path.exists(tarball):
556 get("{}/{}".format(url, filename), tarball, verbose=self.verbose, do_verify=False)
557 unpack(tarball, tarball_suffix, self.llvm_root(),
559 verbose=self.verbose)
561 def fix_bin_or_dylib(self, fname):
562 """Modifies the interpreter section of 'fname' to fix the dynamic linker,
563 or the RPATH section, to fix the dynamic library search path
565 This method is only required on NixOS and uses the PatchELF utility to
566 change the interpreter/RPATH of ELF executables.
568 Please see https://nixos.org/patchelf.html for more information
570 default_encoding = sys.getdefaultencoding()
572 ostype = subprocess.check_output(
573 ['uname', '-s']).strip().decode(default_encoding)
574 except subprocess.CalledProcessError:
576 except OSError as reason:
577 if getattr(reason, 'winerror', None) is not None:
581 if ostype != "Linux":
584 # Use `/etc/os-release` instead of `/etc/NIXOS`.
585 # The latter one does not exist on NixOS when using tmpfs as root.
587 with open("/etc/os-release", "r") as f:
588 if not any(line.strip() == "ID=nixos" for line in f):
590 except FileNotFoundError:
592 if os.path.exists("/lib"):
595 # At this point we're pretty sure the user is running NixOS
596 nix_os_msg = "info: you seem to be running NixOS. Attempting to patch"
597 print(nix_os_msg, fname)
599 # Only build `.nix-deps` once.
600 nix_deps_dir = self.nix_deps_dir
602 # Run `nix-build` to "build" each dependency (which will likely reuse
603 # the existing `/nix/store` copy, or at most download a pre-built copy).
605 # Importantly, we create a gc-root called `.nix-deps` in the `build/`
606 # directory, but still reference the actual `/nix/store` path in the rpath
607 # as it makes it significantly more robust against changes to the location of
608 # the `.nix-deps` location.
610 # bintools: Needed for the path of `ld-linux.so` (via `nix-support/dynamic-linker`).
611 # zlib: Needed as a system dependency of `libLLVM-*.so`.
612 # patchelf: Needed for patching ELF binaries (see doc comment above).
613 nix_deps_dir = "{}/{}".format(self.build_dir, ".nix-deps")
615 with (import <nixpkgs> {});
617 name = "rust-stage0-dependencies";
626 subprocess.check_output([
627 "nix-build", "-E", nix_expr, "-o", nix_deps_dir,
629 except subprocess.CalledProcessError as reason:
630 print("warning: failed to call nix-build:", reason)
632 self.nix_deps_dir = nix_deps_dir
634 patchelf = "{}/bin/patchelf".format(nix_deps_dir)
636 # Relative default, all binary and dynamic libraries we ship
637 # appear to have this (even when `../lib` is redundant).
639 os.path.join(os.path.realpath(nix_deps_dir), "lib")
641 patchelf_args = ["--set-rpath", ":".join(rpath_entries)]
642 if not fname.endswith(".so"):
643 # Finally, set the corret .interp for binaries
644 with open("{}/nix-support/dynamic-linker".format(nix_deps_dir)) as dynamic_linker:
645 patchelf_args += ["--set-interpreter", dynamic_linker.read().rstrip()]
648 subprocess.check_output([patchelf] + patchelf_args + [fname])
649 except subprocess.CalledProcessError as reason:
650 print("warning: failed to call patchelf:", reason)
653 # If `download-rustc` is set, download the most recent commit with CI artifacts
654 def maybe_download_ci_toolchain(self):
655 # If `download-rustc` is not set, default to rebuilding.
656 download_rustc = self.get_toml("download-rustc", section="rust")
657 if download_rustc is None or download_rustc == "false":
659 assert download_rustc == "true" or download_rustc == "if-unchanged", download_rustc
661 # Handle running from a directory other than the top level
662 rev_parse = ["git", "rev-parse", "--show-toplevel"]
663 top_level = subprocess.check_output(rev_parse, universal_newlines=True).strip()
664 compiler = "{}/compiler/".format(top_level)
665 library = "{}/library/".format(top_level)
667 # Look for a version to compare to based on the current commit.
668 # Only commits merged by bors will have CI artifacts.
669 merge_base = ["git", "rev-list", "--author=bors@rust-lang.org", "-n1", "HEAD"]
670 commit = subprocess.check_output(merge_base, universal_newlines=True).strip()
672 # Warn if there were changes to the compiler or standard library since the ancestor commit.
673 status = subprocess.call(["git", "diff-index", "--quiet", commit, "--", compiler, library])
675 if download_rustc == "if-unchanged":
677 print("warning: `download-rustc` is enabled, but there are changes to \
678 compiler/ or library/")
681 print("using downloaded stage1 artifacts from CI (commit {})".format(commit))
682 self.rustc_commit = commit
683 # FIXME: support downloading artifacts from the beta channel
684 self.download_toolchain(False, "nightly")
686 def rustc_stamp(self, stage0):
687 """Return the path for .rustc-stamp at the given stage
690 >>> rb.build_dir = "build"
691 >>> rb.rustc_stamp(True) == os.path.join("build", "stage0", ".rustc-stamp")
693 >>> rb.rustc_stamp(False) == os.path.join("build", "ci-rustc", ".rustc-stamp")
696 return os.path.join(self.bin_root(stage0), '.rustc-stamp')
698 def rustfmt_stamp(self):
699 """Return the path for .rustfmt-stamp
702 >>> rb.build_dir = "build"
703 >>> rb.rustfmt_stamp() == os.path.join("build", "stage0", ".rustfmt-stamp")
706 return os.path.join(self.bin_root(True), '.rustfmt-stamp')
708 def llvm_stamp(self):
709 """Return the path for .rustfmt-stamp
712 >>> rb.build_dir = "build"
713 >>> rb.llvm_stamp() == os.path.join("build", "ci-llvm", ".llvm-stamp")
716 return os.path.join(self.llvm_root(), '.llvm-stamp')
719 def program_out_of_date(self, stamp_path, key):
720 """Check if the given program stamp is out of date"""
721 if not os.path.exists(stamp_path) or self.clean:
723 with open(stamp_path, 'r') as stamp:
724 return key != stamp.read()
726 def bin_root(self, stage0):
727 """Return the binary root directory for the given stage
730 >>> rb.build_dir = "build"
731 >>> rb.bin_root(True) == os.path.join("build", "stage0")
733 >>> rb.bin_root(False) == os.path.join("build", "ci-rustc")
736 When the 'build' property is given should be a nested directory:
738 >>> rb.build = "devel"
739 >>> rb.bin_root(True) == os.path.join("build", "devel", "stage0")
746 return os.path.join(self.build_dir, self.build, subdir)
749 """Return the CI LLVM root directory
752 >>> rb.build_dir = "build"
753 >>> rb.llvm_root() == os.path.join("build", "ci-llvm")
756 When the 'build' property is given should be a nested directory:
758 >>> rb.build = "devel"
759 >>> rb.llvm_root() == os.path.join("build", "devel", "ci-llvm")
762 return os.path.join(self.build_dir, self.build, "ci-llvm")
764 def get_toml(self, key, section=None):
765 """Returns the value of the given key in config.toml, otherwise returns None
768 >>> rb.config_toml = 'key1 = "value1"\\nkey2 = "value2"'
769 >>> rb.get_toml("key2")
772 If the key does not exists, the result is None:
774 >>> rb.get_toml("key3") is None
777 Optionally also matches the section the key appears in
779 >>> rb.config_toml = '[a]\\nkey = "value1"\\n[b]\\nkey = "value2"'
780 >>> rb.get_toml('key', 'a')
782 >>> rb.get_toml('key', 'b')
784 >>> rb.get_toml('key', 'c') is None
787 >>> rb.config_toml = 'key1 = true'
788 >>> rb.get_toml("key1")
793 for line in self.config_toml.splitlines():
794 section_match = re.match(r'^\s*\[(.*)\]\s*$', line)
795 if section_match is not None:
796 cur_section = section_match.group(1)
798 match = re.match(r'^{}\s*=(.*)$'.format(key), line)
799 if match is not None:
800 value = match.group(1)
801 if section is None or section == cur_section:
802 return self.get_string(value) or value.strip()
806 """Return config path for cargo"""
807 return self.program_config('cargo')
809 def rustc(self, stage0):
810 """Return config path for rustc"""
811 return self.program_config('rustc', stage0)
814 """Return config path for rustfmt"""
815 if not self.rustfmt_channel:
817 return self.program_config('rustfmt')
819 def program_config(self, program, stage0=True):
820 """Return config path for the given program at the given stage
823 >>> rb.config_toml = 'rustc = "rustc"\\n'
824 >>> rb.program_config('rustc')
826 >>> rb.config_toml = ''
827 >>> cargo_path = rb.program_config('cargo', True)
828 >>> cargo_path.rstrip(".exe") == os.path.join(rb.bin_root(True),
831 >>> cargo_path = rb.program_config('cargo', False)
832 >>> cargo_path.rstrip(".exe") == os.path.join(rb.bin_root(False),
836 config = self.get_toml(program)
838 return os.path.expanduser(config)
839 return os.path.join(self.bin_root(stage0), "bin", "{}{}".format(
840 program, self.exe_suffix()))
843 def get_string(line):
844 """Return the value between double quotes
846 >>> RustBuild.get_string(' "devel" ')
848 >>> RustBuild.get_string(" 'devel' ")
850 >>> RustBuild.get_string('devel') is None
852 >>> RustBuild.get_string(' "devel ')
855 start = line.find('"')
857 end = start + 1 + line[start + 1:].find('"')
858 return line[start + 1:end]
859 start = line.find('\'')
861 end = start + 1 + line[start + 1:].find('\'')
862 return line[start + 1:end]
867 """Return a suffix for executables"""
868 if sys.platform == 'win32':
872 def bootstrap_binary(self):
873 """Return the path of the bootstrap binary
876 >>> rb.build_dir = "build"
877 >>> rb.bootstrap_binary() == os.path.join("build", "bootstrap",
878 ... "debug", "bootstrap")
881 return os.path.join(self.build_dir, "bootstrap", "debug", "bootstrap")
883 def build_bootstrap(self):
884 """Build bootstrap"""
885 build_dir = os.path.join(self.build_dir, "bootstrap")
886 if self.clean and os.path.exists(build_dir):
887 shutil.rmtree(build_dir)
888 env = os.environ.copy()
889 # `CARGO_BUILD_TARGET` breaks bootstrap build.
890 # See also: <https://github.com/rust-lang/rust/issues/70208>.
891 if "CARGO_BUILD_TARGET" in env:
892 del env["CARGO_BUILD_TARGET"]
893 env["CARGO_TARGET_DIR"] = build_dir
894 env["RUSTC"] = self.rustc(True)
895 env["LD_LIBRARY_PATH"] = os.path.join(self.bin_root(True), "lib") + \
896 (os.pathsep + env["LD_LIBRARY_PATH"]) \
897 if "LD_LIBRARY_PATH" in env else ""
898 env["DYLD_LIBRARY_PATH"] = os.path.join(self.bin_root(True), "lib") + \
899 (os.pathsep + env["DYLD_LIBRARY_PATH"]) \
900 if "DYLD_LIBRARY_PATH" in env else ""
901 env["LIBRARY_PATH"] = os.path.join(self.bin_root(True), "lib") + \
902 (os.pathsep + env["LIBRARY_PATH"]) \
903 if "LIBRARY_PATH" in env else ""
904 # preserve existing RUSTFLAGS
905 env.setdefault("RUSTFLAGS", "")
906 env["RUSTFLAGS"] += " -Cdebuginfo=2"
908 build_section = "target.{}".format(self.build)
910 if self.get_toml("crt-static", build_section) == "true":
911 target_features += ["+crt-static"]
912 elif self.get_toml("crt-static", build_section) == "false":
913 target_features += ["-crt-static"]
915 env["RUSTFLAGS"] += " -C target-feature=" + (",".join(target_features))
916 target_linker = self.get_toml("linker", build_section)
917 if target_linker is not None:
918 env["RUSTFLAGS"] += " -C linker=" + target_linker
919 env["RUSTFLAGS"] += " -Wrust_2018_idioms -Wunused_lifetimes"
920 env["RUSTFLAGS"] += " -Wsemicolon_in_expressions_from_macros"
921 if self.get_toml("deny-warnings", "rust") != "false":
922 env["RUSTFLAGS"] += " -Dwarnings"
924 env["PATH"] = os.path.join(self.bin_root(True), "bin") + \
925 os.pathsep + env["PATH"]
926 if not os.path.isfile(self.cargo()):
927 raise Exception("no cargo executable found at `{}`".format(
929 args = [self.cargo(), "build", "--manifest-path",
930 os.path.join(self.rust_root, "src/bootstrap/Cargo.toml")]
931 for _ in range(1, self.verbose):
932 args.append("--verbose")
933 if self.use_locked_deps:
934 args.append("--locked")
935 if self.use_vendored_sources:
936 args.append("--frozen")
937 run(args, env=env, verbose=self.verbose)
939 def build_triple(self):
940 """Build triple as in LLVM
942 Note that `default_build_triple` is moderately expensive,
943 so use `self.build` where possible.
945 config = self.get_toml('build')
948 return default_build_triple(self.verbose)
950 def check_submodule(self, module, slow_submodules):
951 if not slow_submodules:
952 checked_out = subprocess.Popen(["git", "rev-parse", "HEAD"],
953 cwd=os.path.join(self.rust_root, module),
954 stdout=subprocess.PIPE)
959 def update_submodule(self, module, checked_out, recorded_submodules):
960 module_path = os.path.join(self.rust_root, module)
962 if checked_out is not None:
963 default_encoding = sys.getdefaultencoding()
964 checked_out = checked_out.communicate()[0].decode(default_encoding).strip()
965 if recorded_submodules[module] == checked_out:
968 print("Updating submodule", module)
970 run(["git", "submodule", "-q", "sync", module],
971 cwd=self.rust_root, verbose=self.verbose)
973 update_args = ["git", "submodule", "update", "--init", "--recursive"]
974 if self.git_version >= distutils.version.LooseVersion("2.11.0"):
975 update_args.append("--progress")
976 update_args.append(module)
977 run(update_args, cwd=self.rust_root, verbose=self.verbose, exception=True)
979 run(["git", "reset", "-q", "--hard"],
980 cwd=module_path, verbose=self.verbose)
981 run(["git", "clean", "-qdfx"],
982 cwd=module_path, verbose=self.verbose)
984 def update_submodules(self):
985 """Update submodules"""
986 if (not os.path.exists(os.path.join(self.rust_root, ".git"))) or \
987 self.get_toml('submodules') == "false":
990 default_encoding = sys.getdefaultencoding()
992 # check the existence and version of 'git' command
993 git_version_str = require(['git', '--version']).split()[2].decode(default_encoding)
994 self.git_version = distutils.version.LooseVersion(git_version_str)
996 slow_submodules = self.get_toml('fast-submodules') == "false"
999 print('Unconditionally updating submodules')
1001 print('Updating only changed submodules')
1002 default_encoding = sys.getdefaultencoding()
1003 # Only update submodules that are needed to build bootstrap. These are needed because Cargo
1004 # currently requires everything in a workspace to be "locally present" when starting a
1005 # build, and will give a hard error if any Cargo.toml files are missing.
1006 # FIXME: Is there a way to avoid cloning these eagerly? Bootstrap itself doesn't need to
1007 # share a workspace with any tools - maybe it could be excluded from the workspace?
1008 # That will still require cloning the submodules the second you check the standard
1009 # library, though...
1010 # FIXME: Is there a way to avoid hard-coding the submodules required?
1011 # WARNING: keep this in sync with the submodules hard-coded in bootstrap/lib.rs
1013 "src/tools/rust-installer",
1017 "library/backtrace",
1020 filtered_submodules = []
1021 submodules_names = []
1022 for module in submodules:
1023 check = self.check_submodule(module, slow_submodules)
1024 filtered_submodules.append((module, check))
1025 submodules_names.append(module)
1026 recorded = subprocess.Popen(["git", "ls-tree", "HEAD"] + submodules_names,
1027 cwd=self.rust_root, stdout=subprocess.PIPE)
1028 recorded = recorded.communicate()[0].decode(default_encoding).strip().splitlines()
1029 # { filename: hash }
1030 recorded_submodules = {}
1031 for data in recorded:
1032 # [mode, kind, hash, filename]
1034 recorded_submodules[data[3]] = data[2]
1035 for module in filtered_submodules:
1036 self.update_submodule(module[0], module[1], recorded_submodules)
1037 print("Submodules updated in %.2f seconds" % (time() - start_time))
1039 def set_normal_environment(self):
1040 """Set download URL for normal environment"""
1041 if 'RUSTUP_DIST_SERVER' in os.environ:
1042 self._download_url = os.environ['RUSTUP_DIST_SERVER']
1044 self._download_url = 'https://static.rust-lang.org'
1046 def set_dev_environment(self):
1047 """Set download URL for development environment"""
1048 if 'RUSTUP_DEV_DIST_SERVER' in os.environ:
1049 self._download_url = os.environ['RUSTUP_DEV_DIST_SERVER']
1051 self._download_url = 'https://dev-static.rust-lang.org'
1053 def check_vendored_status(self):
1054 """Check that vendoring is configured properly"""
1055 vendor_dir = os.path.join(self.rust_root, 'vendor')
1056 if 'SUDO_USER' in os.environ and not self.use_vendored_sources:
1057 if os.environ.get('USER') != os.environ['SUDO_USER']:
1058 self.use_vendored_sources = True
1059 print('info: looks like you are running this command under `sudo`')
1060 print(' and so in order to preserve your $HOME this will now')
1061 print(' use vendored sources by default.')
1062 if not os.path.exists(vendor_dir):
1063 print('error: vendoring required, but vendor directory does not exist.')
1064 print(' Run `cargo vendor` without sudo to initialize the '
1065 'vendor directory.')
1066 raise Exception("{} not found".format(vendor_dir))
1068 if self.use_vendored_sources:
1069 if not os.path.exists('.cargo'):
1070 os.makedirs('.cargo')
1071 with output('.cargo/config') as cargo_config:
1073 "[source.crates-io]\n"
1074 "replace-with = 'vendored-sources'\n"
1075 "registry = 'https://example.com'\n"
1077 "[source.vendored-sources]\n"
1078 "directory = '{}/vendor'\n"
1079 .format(self.rust_root))
1081 if os.path.exists('.cargo'):
1082 shutil.rmtree('.cargo')
1084 def ensure_vendored(self):
1085 """Ensure that the vendored sources are available if needed"""
1086 vendor_dir = os.path.join(self.rust_root, 'vendor')
1087 # Note that this does not handle updating the vendored dependencies if
1088 # the rust git repository is updated. Normal development usually does
1089 # not use vendoring, so hopefully this isn't too much of a problem.
1090 if self.use_vendored_sources and not os.path.exists(vendor_dir):
1094 "--sync=./src/tools/rust-analyzer/Cargo.toml",
1095 "--sync=./compiler/rustc_codegen_cranelift/Cargo.toml",
1096 ], verbose=self.verbose, cwd=self.rust_root)
1099 def bootstrap(help_triggered):
1100 """Configure, fetch, build and run the initial bootstrap"""
1102 # If the user is asking for help, let them know that the whole download-and-build
1103 # process has to happen before anything is printed out.
1105 print("info: Downloading and building bootstrap before processing --help")
1106 print(" command. See src/bootstrap/README.md for help with common")
1109 parser = argparse.ArgumentParser(description='Build rust')
1110 parser.add_argument('--config')
1111 parser.add_argument('--build')
1112 parser.add_argument('--clean', action='store_true')
1113 parser.add_argument('-v', '--verbose', action='count', default=0)
1115 args = [a for a in sys.argv if a != '-h' and a != '--help']
1116 args, _ = parser.parse_known_args(args)
1118 # Configure initial bootstrap
1120 build.rust_root = os.path.abspath(os.path.join(__file__, '../../..'))
1121 build.verbose = args.verbose
1122 build.clean = args.clean
1124 # Read from `RUST_BOOTSTRAP_CONFIG`, then `--config`, then fallback to `config.toml` (if it
1126 toml_path = os.getenv('RUST_BOOTSTRAP_CONFIG') or args.config
1127 if not toml_path and os.path.exists('config.toml'):
1128 toml_path = 'config.toml'
1131 if not os.path.exists(toml_path):
1132 toml_path = os.path.join(build.rust_root, toml_path)
1134 with open(toml_path) as config:
1135 build.config_toml = config.read()
1137 profile = build.get_toml('profile')
1138 if profile is not None:
1139 include_file = 'config.{}.toml'.format(profile)
1140 include_dir = os.path.join(build.rust_root, 'src', 'bootstrap', 'defaults')
1141 include_path = os.path.join(include_dir, include_file)
1142 # HACK: This works because `build.get_toml()` returns the first match it finds for a
1143 # specific key, so appending our defaults at the end allows the user to override them
1144 with open(include_path) as included_toml:
1145 build.config_toml += os.linesep + included_toml.read()
1147 config_verbose = build.get_toml('verbose', 'build')
1148 if config_verbose is not None:
1149 build.verbose = max(build.verbose, int(config_verbose))
1151 build.use_vendored_sources = build.get_toml('vendor', 'build') == 'true'
1153 build.use_locked_deps = build.get_toml('locked-deps', 'build') == 'true'
1155 build.check_vendored_status()
1157 build_dir = build.get_toml('build-dir', 'build') or 'build'
1158 build.build_dir = os.path.abspath(build_dir.replace("$ROOT", build.rust_root))
1160 data = stage0_data(build.rust_root)
1161 build.date = data['date']
1162 build.rustc_channel = data['rustc']
1164 if "rustfmt" in data:
1165 build.rustfmt_channel = data['rustfmt']
1168 build.set_dev_environment()
1170 build.set_normal_environment()
1172 build.build = args.build or build.build_triple()
1173 build.update_submodules()
1175 # Fetch/build the bootstrap
1176 build.download_toolchain()
1177 # Download the master compiler if `download-rustc` is set
1178 build.maybe_download_ci_toolchain()
1180 build.ensure_vendored()
1181 build.build_bootstrap()
1185 args = [build.bootstrap_binary()]
1186 args.extend(sys.argv[1:])
1187 env = os.environ.copy()
1188 env["BOOTSTRAP_PARENT_ID"] = str(os.getpid())
1189 env["BOOTSTRAP_PYTHON"] = sys.executable
1190 env["BUILD_DIR"] = build.build_dir
1191 env["RUSTC_BOOTSTRAP"] = '1'
1193 env["BOOTSTRAP_CONFIG"] = toml_path
1194 if build.rustc_commit is not None:
1195 env["BOOTSTRAP_DOWNLOAD_RUSTC"] = '1'
1196 run(args, env=env, verbose=build.verbose, is_bootstrap=True)
1200 """Entry point for the bootstrap process"""
1203 # x.py help <cmd> ...
1204 if len(sys.argv) > 1 and sys.argv[1] == 'help':
1205 sys.argv = [sys.argv[0], '-h'] + sys.argv[2:]
1208 '-h' in sys.argv) or ('--help' in sys.argv) or (len(sys.argv) == 1)
1210 bootstrap(help_triggered)
1211 if not help_triggered:
1212 print("Build completed successfully in {}".format(
1213 format_build_time(time() - start_time)))
1214 except (SystemExit, KeyboardInterrupt) as error:
1215 if hasattr(error, 'code') and isinstance(error.code, int):
1216 exit_code = error.code
1220 if not help_triggered:
1221 print("Build completed unsuccessfully in {}".format(
1222 format_build_time(time() - start_time)))
1226 if __name__ == '__main__':