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 http://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, **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)
157 def require(cmd, exit=True):
158 '''Run a command, returning its output.
160 If `exit` is `True`, exit the process.
161 Otherwise, return None.'''
163 return subprocess.check_output(cmd).strip()
164 except (subprocess.CalledProcessError, OSError) as exc:
167 print("error: unable to run `{}`: {}".format(' '.join(cmd), exc))
168 print("Please make sure it's installed and in the path.")
172 def stage0_data(rust_root):
173 """Build a dictionary from stage0.txt"""
174 nightlies = os.path.join(rust_root, "src/stage0.txt")
175 with open(nightlies, 'r') as nightlies:
176 lines = [line.rstrip() for line in nightlies
177 if not line.startswith("#")]
178 return dict([line.split(": ", 1) for line in lines if line])
181 def format_build_time(duration):
182 """Return a nicer format for build time
184 >>> format_build_time('300')
187 return str(datetime.timedelta(seconds=int(duration)))
190 def default_build_triple(verbose):
191 """Build triple as in LLVM"""
192 # If the user already has a host build triple with an existing `rustc`
193 # install, use their preference. This fixes most issues with Windows builds
194 # being detected as GNU instead of MSVC.
195 default_encoding = sys.getdefaultencoding()
197 version = subprocess.check_output(["rustc", "--version", "--verbose"],
198 stderr=subprocess.DEVNULL)
199 version = version.decode(default_encoding)
200 host = next(x for x in version.split('\n') if x.startswith("host: "))
201 triple = host.split("host: ")[1]
203 print("detected default triple {}".format(triple))
205 except Exception as e:
207 print("rustup not detected: {}".format(e))
208 print("falling back to auto-detect")
210 required = sys.platform != 'win32'
211 ostype = require(["uname", "-s"], exit=required)
212 cputype = require(['uname', '-m'], exit=required)
214 # If we do not have `uname`, assume Windows.
215 if ostype is None or cputype is None:
216 return 'x86_64-pc-windows-msvc'
218 ostype = ostype.decode(default_encoding)
219 cputype = cputype.decode(default_encoding)
221 # The goal here is to come up with the same triple as LLVM would,
222 # at least for the subset of platforms we're willing to target.
224 'Darwin': 'apple-darwin',
225 'DragonFly': 'unknown-dragonfly',
226 'FreeBSD': 'unknown-freebsd',
227 'Haiku': 'unknown-haiku',
228 'NetBSD': 'unknown-netbsd',
229 'OpenBSD': 'unknown-openbsd'
232 # Consider the direct transformation first and then the special cases
233 if ostype in ostype_mapper:
234 ostype = ostype_mapper[ostype]
235 elif ostype == 'Linux':
236 os_from_sp = subprocess.check_output(
237 ['uname', '-o']).strip().decode(default_encoding)
238 if os_from_sp == 'Android':
239 ostype = 'linux-android'
241 ostype = 'unknown-linux-gnu'
242 elif ostype == 'SunOS':
243 ostype = 'pc-solaris'
244 # On Solaris, uname -m will return a machine classification instead
245 # of a cpu type, so uname -p is recommended instead. However, the
246 # output from that option is too generic for our purposes (it will
247 # always emit 'i386' on x86/amd64 systems). As such, isainfo -k
248 # must be used instead.
249 cputype = require(['isainfo', '-k']).decode(default_encoding)
250 # sparc cpus have sun as a target vendor
251 if 'sparc' in cputype:
252 ostype = 'sun-solaris'
253 elif ostype.startswith('MINGW'):
254 # msys' `uname` does not print gcc configuration, but prints msys
255 # configuration. so we cannot believe `uname -m`:
256 # msys1 is always i686 and msys2 is always x86_64.
257 # instead, msys defines $MSYSTEM which is MINGW32 on i686 and
259 ostype = 'pc-windows-gnu'
261 if os.environ.get('MSYSTEM') == 'MINGW64':
263 elif ostype.startswith('MSYS'):
264 ostype = 'pc-windows-gnu'
265 elif ostype.startswith('CYGWIN_NT'):
267 if ostype.endswith('WOW64'):
269 ostype = 'pc-windows-gnu'
270 elif sys.platform == 'win32':
271 # Some Windows platforms might have a `uname` command that returns a
272 # non-standard string (e.g. gnuwin32 tools returns `windows32`). In
273 # these cases, fall back to using sys.platform.
274 return 'x86_64-pc-windows-msvc'
276 err = "unknown OS type: {}".format(ostype)
279 if cputype == 'powerpc' and ostype == 'unknown-freebsd':
280 cputype = subprocess.check_output(
281 ['uname', '-p']).strip().decode(default_encoding)
284 'aarch64': 'aarch64',
291 'powerpc': 'powerpc',
292 'powerpc64': 'powerpc64',
293 'powerpc64le': 'powerpc64le',
295 'ppc64': 'powerpc64',
296 'ppc64le': 'powerpc64le',
304 # Consider the direct transformation first and then the special cases
305 if cputype in cputype_mapper:
306 cputype = cputype_mapper[cputype]
307 elif cputype in {'xscale', 'arm'}:
309 if ostype == 'linux-android':
310 ostype = 'linux-androideabi'
311 elif ostype == 'unknown-freebsd':
312 cputype = subprocess.check_output(
313 ['uname', '-p']).strip().decode(default_encoding)
314 ostype = 'unknown-freebsd'
315 elif cputype == 'armv6l':
317 if ostype == 'linux-android':
318 ostype = 'linux-androideabi'
321 elif cputype in {'armv7l', 'armv8l'}:
323 if ostype == 'linux-android':
324 ostype = 'linux-androideabi'
327 elif cputype == 'mips':
328 if sys.byteorder == 'big':
330 elif sys.byteorder == 'little':
333 raise ValueError("unknown byteorder: {}".format(sys.byteorder))
334 elif cputype == 'mips64':
335 if sys.byteorder == 'big':
337 elif sys.byteorder == 'little':
340 raise ValueError('unknown byteorder: {}'.format(sys.byteorder))
341 # only the n64 ABI is supported, indicate it
343 elif cputype == 'sparc' or cputype == 'sparcv9' or cputype == 'sparc64':
346 err = "unknown cpu type: {}".format(cputype)
349 return "{}-{}".format(cputype, ostype)
352 @contextlib.contextmanager
353 def output(filepath):
354 tmp = filepath + '.tmp'
355 with open(tmp, 'w') as f:
358 if os.path.exists(filepath):
359 os.remove(filepath) # PermissionError/OSError on Win32 if in use
361 shutil.copy2(tmp, filepath)
364 os.rename(tmp, filepath)
367 class RustBuild(object):
368 """Provide all the methods required to build Rust"""
371 self._download_url = ''
372 self.rustc_channel = ''
373 self.rustfmt_channel = ''
377 self.config_toml = ''
379 self.use_locked_deps = ''
380 self.use_vendored_sources = ''
382 self.git_version = None
383 self.nix_deps_dir = None
384 self.rustc_commit = None
386 def download_toolchain(self, stage0=True, rustc_channel=None):
387 """Fetch the build system for Rust, written in Rust
389 This method will build a cache directory, then it will fetch the
390 tarball which has the stage0 compiler used to then bootstrap the Rust
393 Each downloaded tarball is extracted, after that, the script
394 will move all the content to the right place.
396 if rustc_channel is None:
397 rustc_channel = self.rustc_channel
398 rustfmt_channel = self.rustfmt_channel
399 bin_root = self.bin_root(stage0)
403 key += str(self.rustc_commit)
404 if self.rustc(stage0).startswith(bin_root) and \
405 (not os.path.exists(self.rustc(stage0)) or
406 self.program_out_of_date(self.rustc_stamp(stage0), key)):
407 if os.path.exists(bin_root):
408 shutil.rmtree(bin_root)
409 tarball_suffix = '.tar.xz' if support_xz() else '.tar.gz'
410 filename = "rust-std-{}-{}{}".format(
411 rustc_channel, self.build, tarball_suffix)
412 pattern = "rust-std-{}".format(self.build)
413 self._download_component_helper(filename, pattern, tarball_suffix, stage0)
414 filename = "rustc-{}-{}{}".format(rustc_channel, self.build,
416 self._download_component_helper(filename, "rustc", tarball_suffix, stage0)
417 filename = "cargo-{}-{}{}".format(rustc_channel, self.build,
419 self._download_component_helper(filename, "cargo", tarball_suffix)
421 filename = "rustc-dev-{}-{}{}".format(rustc_channel, self.build, tarball_suffix)
422 self._download_component_helper(
423 filename, "rustc-dev", tarball_suffix, stage0
426 self.fix_bin_or_dylib("{}/bin/rustc".format(bin_root))
427 self.fix_bin_or_dylib("{}/bin/rustdoc".format(bin_root))
428 self.fix_bin_or_dylib("{}/bin/cargo".format(bin_root))
429 lib_dir = "{}/lib".format(bin_root)
430 for lib in os.listdir(lib_dir):
431 if lib.endswith(".so"):
432 self.fix_bin_or_dylib(os.path.join(lib_dir, lib), rpath_libz=True)
433 with output(self.rustc_stamp(stage0)) as rust_stamp:
434 rust_stamp.write(key)
436 if self.rustfmt() and self.rustfmt().startswith(bin_root) and (
437 not os.path.exists(self.rustfmt())
438 or self.program_out_of_date(self.rustfmt_stamp(), self.rustfmt_channel)
441 tarball_suffix = '.tar.xz' if support_xz() else '.tar.gz'
442 [channel, date] = rustfmt_channel.split('-', 1)
443 filename = "rustfmt-{}-{}{}".format(channel, self.build, tarball_suffix)
444 self._download_component_helper(
445 filename, "rustfmt-preview", tarball_suffix, key=date
447 self.fix_bin_or_dylib("{}/bin/rustfmt".format(bin_root))
448 self.fix_bin_or_dylib("{}/bin/cargo-fmt".format(bin_root))
449 with output(self.rustfmt_stamp()) as rustfmt_stamp:
450 rustfmt_stamp.write(self.rustfmt_channel)
452 # Avoid downloading LLVM twice (once for stage0 and once for the master rustc)
453 if self.downloading_llvm() and stage0:
454 # We want the most recent LLVM submodule update to avoid downloading
455 # LLVM more often than necessary.
457 # This git command finds that commit SHA, looking for bors-authored
458 # merges that modified src/llvm-project.
460 # This works even in a repository that has not yet initialized
462 top_level = subprocess.check_output([
463 "git", "rev-parse", "--show-toplevel",
464 ]).decode(sys.getdefaultencoding()).strip()
465 llvm_sha = subprocess.check_output([
466 "git", "log", "--author=bors", "--format=%H", "-n1",
467 "-m", "--first-parent",
469 "{}/src/llvm-project".format(top_level),
470 "{}/src/bootstrap/download-ci-llvm-stamp".format(top_level),
471 # the LLVM shared object file is named `LLVM-12-rust-{version}-nightly`
472 "{}/src/version".format(top_level)
473 ]).decode(sys.getdefaultencoding()).strip()
474 llvm_assertions = self.get_toml('assertions', 'llvm') == 'true'
475 llvm_root = self.llvm_root()
476 llvm_lib = os.path.join(llvm_root, "lib")
477 if self.program_out_of_date(self.llvm_stamp(), llvm_sha + str(llvm_assertions)):
478 self._download_ci_llvm(llvm_sha, llvm_assertions)
479 for binary in ["llvm-config", "FileCheck"]:
480 self.fix_bin_or_dylib(os.path.join(llvm_root, "bin", binary), rpath_libz=True)
481 for lib in os.listdir(llvm_lib):
482 if lib.endswith(".so"):
483 self.fix_bin_or_dylib(os.path.join(llvm_lib, lib), rpath_libz=True)
484 with output(self.llvm_stamp()) as llvm_stamp:
485 llvm_stamp.write(llvm_sha + str(llvm_assertions))
487 def downloading_llvm(self):
488 opt = self.get_toml('download-ci-llvm', 'llvm')
489 # This is currently all tier 1 targets (since others may not have CI
491 # https://doc.rust-lang.org/rustc/platform-support.html#tier-1
492 supported_platforms = [
493 "aarch64-unknown-linux-gnu",
494 "i686-pc-windows-gnu",
495 "i686-pc-windows-msvc",
496 "i686-unknown-linux-gnu",
497 "x86_64-unknown-linux-gnu",
498 "x86_64-apple-darwin",
499 "x86_64-pc-windows-gnu",
500 "x86_64-pc-windows-msvc",
502 return opt == "true" \
503 or (opt == "if-available" and self.build in supported_platforms)
505 def _download_component_helper(
506 self, filename, pattern, tarball_suffix, stage0=True, key=None
512 key = self.rustc_commit
513 cache_dst = os.path.join(self.build_dir, "cache")
514 rustc_cache = os.path.join(cache_dst, key)
515 if not os.path.exists(rustc_cache):
516 os.makedirs(rustc_cache)
519 url = "{}/dist/{}".format(self._download_url, key)
521 url = "https://ci-artifacts.rust-lang.org/rustc-builds/{}".format(self.rustc_commit)
522 tarball = os.path.join(rustc_cache, filename)
523 if not os.path.exists(tarball):
524 get("{}/{}".format(url, filename), tarball, verbose=self.verbose, do_verify=stage0)
525 unpack(tarball, tarball_suffix, self.bin_root(stage0), match=pattern, verbose=self.verbose)
527 def _download_ci_llvm(self, llvm_sha, llvm_assertions):
528 cache_prefix = "llvm-{}-{}".format(llvm_sha, llvm_assertions)
529 cache_dst = os.path.join(self.build_dir, "cache")
530 rustc_cache = os.path.join(cache_dst, cache_prefix)
531 if not os.path.exists(rustc_cache):
532 os.makedirs(rustc_cache)
534 url = "https://ci-artifacts.rust-lang.org/rustc-builds/{}".format(llvm_sha)
536 url = url.replace('rustc-builds', 'rustc-builds-alt')
537 # ci-artifacts are only stored as .xz, not .gz
539 print("error: XZ support is required to download LLVM")
540 print("help: consider disabling `download-ci-llvm` or using python3")
542 tarball_suffix = '.tar.xz'
543 filename = "rust-dev-nightly-" + self.build + tarball_suffix
544 tarball = os.path.join(rustc_cache, filename)
545 if not os.path.exists(tarball):
546 get("{}/{}".format(url, filename), tarball, verbose=self.verbose, do_verify=False)
547 unpack(tarball, tarball_suffix, self.llvm_root(),
549 verbose=self.verbose)
551 def fix_bin_or_dylib(self, fname, rpath_libz=False):
552 """Modifies the interpreter section of 'fname' to fix the dynamic linker,
553 or the RPATH section, to fix the dynamic library search path
555 This method is only required on NixOS and uses the PatchELF utility to
556 change the interpreter/RPATH of ELF executables.
558 Please see https://nixos.org/patchelf.html for more information
560 default_encoding = sys.getdefaultencoding()
562 ostype = subprocess.check_output(
563 ['uname', '-s']).strip().decode(default_encoding)
564 except subprocess.CalledProcessError:
566 except OSError as reason:
567 if getattr(reason, 'winerror', None) is not None:
571 if ostype != "Linux":
574 if not os.path.exists("/etc/NIXOS"):
576 if os.path.exists("/lib"):
579 # At this point we're pretty sure the user is running NixOS
580 nix_os_msg = "info: you seem to be running NixOS. Attempting to patch"
581 print(nix_os_msg, fname)
583 # Only build `.nix-deps` once.
584 nix_deps_dir = self.nix_deps_dir
586 nix_deps_dir = ".nix-deps"
587 if not os.path.exists(nix_deps_dir):
588 os.makedirs(nix_deps_dir)
591 # Needed for the path of `ld-linux.so` (via `nix-support/dynamic-linker`).
592 "stdenv.cc.bintools",
594 # Needed as a system dependency of `libLLVM-*.so`.
597 # Needed for patching ELF binaries (see doc comment above).
601 # Run `nix-build` to "build" each dependency (which will likely reuse
602 # the existing `/nix/store` copy, or at most download a pre-built copy).
603 # Importantly, we don't rely on `nix-build` printing the `/nix/store`
604 # path on stdout, but use `-o` to symlink it into `stage0/.nix-deps/$dep`,
605 # ensuring garbage collection will never remove the `/nix/store` path
606 # (which would break our patched binaries that hardcode those paths).
609 subprocess.check_output([
610 "nix-build", "<nixpkgs>",
612 "-o", "{}/{}".format(nix_deps_dir, dep),
614 except subprocess.CalledProcessError as reason:
615 print("warning: failed to call nix-build:", reason)
618 self.nix_deps_dir = nix_deps_dir
620 patchelf = "{}/patchelf/bin/patchelf".format(nix_deps_dir)
624 # Patch RPATH to add `zlib` dependency that stems from LLVM
625 dylib_deps = ["zlib"]
627 # Relative default, all binary and dynamic libraries we ship
628 # appear to have this (even when `../lib` is redundant).
630 ] + ["{}/{}/lib".format(nix_deps_dir, dep) for dep in dylib_deps]
631 patchelf_args += ["--set-rpath", ":".join(rpath_entries)]
632 if not fname.endswith(".so"):
633 # Finally, set the corret .interp for binaries
634 bintools_dir = "{}/stdenv.cc.bintools".format(nix_deps_dir)
635 with open("{}/nix-support/dynamic-linker".format(bintools_dir)) as dynamic_linker:
636 patchelf_args += ["--set-interpreter", dynamic_linker.read().rstrip()]
639 subprocess.check_output([patchelf] + patchelf_args + [fname])
640 except subprocess.CalledProcessError as reason:
641 print("warning: failed to call patchelf:", reason)
644 # If `download-rustc` is set, download the most recent commit with CI artifacts
645 def maybe_download_ci_toolchain(self):
646 # If `download-rustc` is not set, default to rebuilding.
647 if self.get_toml("download-rustc", section="rust") != "true":
650 # Handle running from a directory other than the top level
651 rev_parse = ["git", "rev-parse", "--show-toplevel"]
652 top_level = subprocess.check_output(rev_parse, universal_newlines=True).strip()
653 compiler = "{}/compiler/".format(top_level)
655 # Look for a version to compare to based on the current commit.
656 # Only commits merged by bors will have CI artifacts.
657 merge_base = ["git", "log", "--author=bors", "--pretty=%H", "-n1"]
658 commit = subprocess.check_output(merge_base, universal_newlines=True).strip()
660 # Warn if there were changes to the compiler since the ancestor commit.
661 status = subprocess.call(["git", "diff-index", "--quiet", commit, "--", compiler])
663 print("warning: `download-rustc` is enabled, but there are changes to compiler/")
666 print("using downloaded stage1 artifacts from CI (commit {})".format(commit))
667 self.rustc_commit = commit
668 # FIXME: support downloading artifacts from the beta channel
669 self.download_toolchain(False, "nightly")
671 def rustc_stamp(self, stage0):
672 """Return the path for .rustc-stamp at the given stage
675 >>> rb.build_dir = "build"
676 >>> rb.rustc_stamp(True) == os.path.join("build", "stage0", ".rustc-stamp")
678 >>> rb.rustc_stamp(False) == os.path.join("build", "ci-rustc", ".rustc-stamp")
681 return os.path.join(self.bin_root(stage0), '.rustc-stamp')
683 def rustfmt_stamp(self):
684 """Return the path for .rustfmt-stamp
687 >>> rb.build_dir = "build"
688 >>> rb.rustfmt_stamp() == os.path.join("build", "stage0", ".rustfmt-stamp")
691 return os.path.join(self.bin_root(True), '.rustfmt-stamp')
693 def llvm_stamp(self):
694 """Return the path for .rustfmt-stamp
697 >>> rb.build_dir = "build"
698 >>> rb.llvm_stamp() == os.path.join("build", "ci-llvm", ".llvm-stamp")
701 return os.path.join(self.llvm_root(), '.llvm-stamp')
704 def program_out_of_date(self, stamp_path, key):
705 """Check if the given program stamp is out of date"""
706 if not os.path.exists(stamp_path) or self.clean:
708 with open(stamp_path, 'r') as stamp:
709 return key != stamp.read()
711 def bin_root(self, stage0):
712 """Return the binary root directory for the given stage
715 >>> rb.build_dir = "build"
716 >>> rb.bin_root(True) == os.path.join("build", "stage0")
718 >>> rb.bin_root(False) == os.path.join("build", "ci-rustc")
721 When the 'build' property is given should be a nested directory:
723 >>> rb.build = "devel"
724 >>> rb.bin_root(True) == os.path.join("build", "devel", "stage0")
731 return os.path.join(self.build_dir, self.build, subdir)
734 """Return the CI LLVM root directory
737 >>> rb.build_dir = "build"
738 >>> rb.llvm_root() == os.path.join("build", "ci-llvm")
741 When the 'build' property is given should be a nested directory:
743 >>> rb.build = "devel"
744 >>> rb.llvm_root() == os.path.join("build", "devel", "ci-llvm")
747 return os.path.join(self.build_dir, self.build, "ci-llvm")
749 def get_toml(self, key, section=None):
750 """Returns the value of the given key in config.toml, otherwise returns None
753 >>> rb.config_toml = 'key1 = "value1"\\nkey2 = "value2"'
754 >>> rb.get_toml("key2")
757 If the key does not exists, the result is None:
759 >>> rb.get_toml("key3") is None
762 Optionally also matches the section the key appears in
764 >>> rb.config_toml = '[a]\\nkey = "value1"\\n[b]\\nkey = "value2"'
765 >>> rb.get_toml('key', 'a')
767 >>> rb.get_toml('key', 'b')
769 >>> rb.get_toml('key', 'c') is None
772 >>> rb.config_toml = 'key1 = true'
773 >>> rb.get_toml("key1")
778 for line in self.config_toml.splitlines():
779 section_match = re.match(r'^\s*\[(.*)\]\s*$', line)
780 if section_match is not None:
781 cur_section = section_match.group(1)
783 match = re.match(r'^{}\s*=(.*)$'.format(key), line)
784 if match is not None:
785 value = match.group(1)
786 if section is None or section == cur_section:
787 return self.get_string(value) or value.strip()
791 """Return config path for cargo"""
792 return self.program_config('cargo')
794 def rustc(self, stage0):
795 """Return config path for rustc"""
796 return self.program_config('rustc', stage0)
799 """Return config path for rustfmt"""
800 if not self.rustfmt_channel:
802 return self.program_config('rustfmt')
804 def program_config(self, program, stage0=True):
805 """Return config path for the given program at the given stage
808 >>> rb.config_toml = 'rustc = "rustc"\\n'
809 >>> rb.program_config('rustc')
811 >>> rb.config_toml = ''
812 >>> cargo_path = rb.program_config('cargo', True)
813 >>> cargo_path.rstrip(".exe") == os.path.join(rb.bin_root(True),
816 >>> cargo_path = rb.program_config('cargo', False)
817 >>> cargo_path.rstrip(".exe") == os.path.join(rb.bin_root(False),
821 config = self.get_toml(program)
823 return os.path.expanduser(config)
824 return os.path.join(self.bin_root(stage0), "bin", "{}{}".format(
825 program, self.exe_suffix()))
828 def get_string(line):
829 """Return the value between double quotes
831 >>> RustBuild.get_string(' "devel" ')
833 >>> RustBuild.get_string(" 'devel' ")
835 >>> RustBuild.get_string('devel') is None
837 >>> RustBuild.get_string(' "devel ')
840 start = line.find('"')
842 end = start + 1 + line[start + 1:].find('"')
843 return line[start + 1:end]
844 start = line.find('\'')
846 end = start + 1 + line[start + 1:].find('\'')
847 return line[start + 1:end]
852 """Return a suffix for executables"""
853 if sys.platform == 'win32':
857 def bootstrap_binary(self):
858 """Return the path of the bootstrap binary
861 >>> rb.build_dir = "build"
862 >>> rb.bootstrap_binary() == os.path.join("build", "bootstrap",
863 ... "debug", "bootstrap")
866 return os.path.join(self.build_dir, "bootstrap", "debug", "bootstrap")
868 def build_bootstrap(self):
869 """Build bootstrap"""
870 build_dir = os.path.join(self.build_dir, "bootstrap")
871 if self.clean and os.path.exists(build_dir):
872 shutil.rmtree(build_dir)
873 env = os.environ.copy()
874 # `CARGO_BUILD_TARGET` breaks bootstrap build.
875 # See also: <https://github.com/rust-lang/rust/issues/70208>.
876 if "CARGO_BUILD_TARGET" in env:
877 del env["CARGO_BUILD_TARGET"]
878 env["CARGO_TARGET_DIR"] = build_dir
879 env["RUSTC"] = self.rustc(True)
880 env["LD_LIBRARY_PATH"] = os.path.join(self.bin_root(True), "lib") + \
881 (os.pathsep + env["LD_LIBRARY_PATH"]) \
882 if "LD_LIBRARY_PATH" in env else ""
883 env["DYLD_LIBRARY_PATH"] = os.path.join(self.bin_root(True), "lib") + \
884 (os.pathsep + env["DYLD_LIBRARY_PATH"]) \
885 if "DYLD_LIBRARY_PATH" in env else ""
886 env["LIBRARY_PATH"] = os.path.join(self.bin_root(True), "lib") + \
887 (os.pathsep + env["LIBRARY_PATH"]) \
888 if "LIBRARY_PATH" in env else ""
889 # preserve existing RUSTFLAGS
890 env.setdefault("RUSTFLAGS", "")
891 env["RUSTFLAGS"] += " -Cdebuginfo=2"
893 build_section = "target.{}".format(self.build)
895 if self.get_toml("crt-static", build_section) == "true":
896 target_features += ["+crt-static"]
897 elif self.get_toml("crt-static", build_section) == "false":
898 target_features += ["-crt-static"]
900 env["RUSTFLAGS"] += " -C target-feature=" + (",".join(target_features))
901 target_linker = self.get_toml("linker", build_section)
902 if target_linker is not None:
903 env["RUSTFLAGS"] += " -C linker=" + target_linker
904 # cfg(bootstrap): Add `-Wsemicolon_in_expressions_from_macros` after the next beta bump
905 env["RUSTFLAGS"] += " -Wrust_2018_idioms -Wunused_lifetimes"
906 if self.get_toml("deny-warnings", "rust") != "false":
907 env["RUSTFLAGS"] += " -Dwarnings"
909 env["PATH"] = os.path.join(self.bin_root(True), "bin") + \
910 os.pathsep + env["PATH"]
911 if not os.path.isfile(self.cargo()):
912 raise Exception("no cargo executable found at `{}`".format(
914 args = [self.cargo(), "build", "--manifest-path",
915 os.path.join(self.rust_root, "src/bootstrap/Cargo.toml")]
916 for _ in range(1, self.verbose):
917 args.append("--verbose")
918 if self.use_locked_deps:
919 args.append("--locked")
920 if self.use_vendored_sources:
921 args.append("--frozen")
922 run(args, env=env, verbose=self.verbose)
924 def build_triple(self):
925 """Build triple as in LLVM
927 Note that `default_build_triple` is moderately expensive,
928 so use `self.build` where possible.
930 config = self.get_toml('build')
933 return default_build_triple(self.verbose)
935 def check_submodule(self, module, slow_submodules):
936 if not slow_submodules:
937 checked_out = subprocess.Popen(["git", "rev-parse", "HEAD"],
938 cwd=os.path.join(self.rust_root, module),
939 stdout=subprocess.PIPE)
944 def update_submodule(self, module, checked_out, recorded_submodules):
945 module_path = os.path.join(self.rust_root, module)
947 if checked_out is not None:
948 default_encoding = sys.getdefaultencoding()
949 checked_out = checked_out.communicate()[0].decode(default_encoding).strip()
950 if recorded_submodules[module] == checked_out:
953 print("Updating submodule", module)
955 run(["git", "submodule", "-q", "sync", module],
956 cwd=self.rust_root, verbose=self.verbose)
958 update_args = ["git", "submodule", "update", "--init", "--recursive"]
959 if self.git_version >= distutils.version.LooseVersion("2.11.0"):
960 update_args.append("--progress")
961 update_args.append(module)
962 run(update_args, cwd=self.rust_root, verbose=self.verbose, exception=True)
964 run(["git", "reset", "-q", "--hard"],
965 cwd=module_path, verbose=self.verbose)
966 run(["git", "clean", "-qdfx"],
967 cwd=module_path, verbose=self.verbose)
969 def update_submodules(self):
970 """Update submodules"""
971 if (not os.path.exists(os.path.join(self.rust_root, ".git"))) or \
972 self.get_toml('submodules') == "false":
975 default_encoding = sys.getdefaultencoding()
977 # check the existence and version of 'git' command
978 git_version_str = require(['git', '--version']).split()[2].decode(default_encoding)
979 self.git_version = distutils.version.LooseVersion(git_version_str)
981 slow_submodules = self.get_toml('fast-submodules') == "false"
984 print('Unconditionally updating all submodules')
986 print('Updating only changed submodules')
987 default_encoding = sys.getdefaultencoding()
988 submodules = [s.split(' ', 1)[1] for s in subprocess.check_output(
989 ["git", "config", "--file",
990 os.path.join(self.rust_root, ".gitmodules"),
991 "--get-regexp", "path"]
992 ).decode(default_encoding).splitlines()]
993 filtered_submodules = []
994 submodules_names = []
995 llvm_checked_out = os.path.exists(os.path.join(self.rust_root, "src/llvm-project/.git"))
996 external_llvm_provided = self.get_toml('llvm-config') or self.downloading_llvm()
997 llvm_needed = not self.get_toml('codegen-backends', 'rust') \
998 or "llvm" in self.get_toml('codegen-backends', 'rust')
999 for module in submodules:
1000 if module.endswith("llvm-project"):
1001 # Don't sync the llvm-project submodule if an external LLVM was
1002 # provided, if we are downloading LLVM or if the LLVM backend is
1003 # not being built. Also, if the submodule has been initialized
1004 # already, sync it anyways so that it doesn't mess up contributor
1006 if external_llvm_provided or not llvm_needed:
1007 if self.get_toml('lld') != 'true' and not llvm_checked_out:
1009 check = self.check_submodule(module, slow_submodules)
1010 filtered_submodules.append((module, check))
1011 submodules_names.append(module)
1012 recorded = subprocess.Popen(["git", "ls-tree", "HEAD"] + submodules_names,
1013 cwd=self.rust_root, stdout=subprocess.PIPE)
1014 recorded = recorded.communicate()[0].decode(default_encoding).strip().splitlines()
1015 recorded_submodules = {}
1016 for data in recorded:
1018 recorded_submodules[data[3]] = data[2]
1019 for module in filtered_submodules:
1020 self.update_submodule(module[0], module[1], recorded_submodules)
1021 print("Submodules updated in %.2f seconds" % (time() - start_time))
1023 def set_normal_environment(self):
1024 """Set download URL for normal environment"""
1025 if 'RUSTUP_DIST_SERVER' in os.environ:
1026 self._download_url = os.environ['RUSTUP_DIST_SERVER']
1028 self._download_url = 'https://static.rust-lang.org'
1030 def set_dev_environment(self):
1031 """Set download URL for development environment"""
1032 if 'RUSTUP_DEV_DIST_SERVER' in os.environ:
1033 self._download_url = os.environ['RUSTUP_DEV_DIST_SERVER']
1035 self._download_url = 'https://dev-static.rust-lang.org'
1037 def check_vendored_status(self):
1038 """Check that vendoring is configured properly"""
1039 vendor_dir = os.path.join(self.rust_root, 'vendor')
1040 if 'SUDO_USER' in os.environ and not self.use_vendored_sources:
1041 if os.environ.get('USER') != os.environ['SUDO_USER']:
1042 self.use_vendored_sources = True
1043 print('info: looks like you are running this command under `sudo`')
1044 print(' and so in order to preserve your $HOME this will now')
1045 print(' use vendored sources by default.')
1046 if not os.path.exists(vendor_dir):
1047 print('error: vendoring required, but vendor directory does not exist.')
1048 print(' Run `cargo vendor` without sudo to initialize the '
1049 'vendor directory.')
1050 raise Exception("{} not found".format(vendor_dir))
1052 if self.use_vendored_sources:
1053 if not os.path.exists('.cargo'):
1054 os.makedirs('.cargo')
1055 with output('.cargo/config') as cargo_config:
1057 "[source.crates-io]\n"
1058 "replace-with = 'vendored-sources'\n"
1059 "registry = 'https://example.com'\n"
1061 "[source.vendored-sources]\n"
1062 "directory = '{}/vendor'\n"
1063 .format(self.rust_root))
1065 if os.path.exists('.cargo'):
1066 shutil.rmtree('.cargo')
1068 def ensure_vendored(self):
1069 """Ensure that the vendored sources are available if needed"""
1070 vendor_dir = os.path.join(self.rust_root, 'vendor')
1071 # Note that this does not handle updating the vendored dependencies if
1072 # the rust git repository is updated. Normal development usually does
1073 # not use vendoring, so hopefully this isn't too much of a problem.
1074 if self.use_vendored_sources and not os.path.exists(vendor_dir):
1078 "--sync=./src/tools/rust-analyzer/Cargo.toml",
1079 "--sync=./compiler/rustc_codegen_cranelift/Cargo.toml",
1080 ], verbose=self.verbose, cwd=self.rust_root)
1083 def bootstrap(help_triggered):
1084 """Configure, fetch, build and run the initial bootstrap"""
1086 # If the user is asking for help, let them know that the whole download-and-build
1087 # process has to happen before anything is printed out.
1089 print("info: Downloading and building bootstrap before processing --help")
1090 print(" command. See src/bootstrap/README.md for help with common")
1093 parser = argparse.ArgumentParser(description='Build rust')
1094 parser.add_argument('--config')
1095 parser.add_argument('--build')
1096 parser.add_argument('--clean', action='store_true')
1097 parser.add_argument('-v', '--verbose', action='count', default=0)
1099 args = [a for a in sys.argv if a != '-h' and a != '--help']
1100 args, _ = parser.parse_known_args(args)
1102 # Configure initial bootstrap
1104 build.rust_root = os.path.abspath(os.path.join(__file__, '../../..'))
1105 build.verbose = args.verbose
1106 build.clean = args.clean
1108 # Read from `RUST_BOOTSTRAP_CONFIG`, then `--config`, then fallback to `config.toml` (if it
1110 toml_path = os.getenv('RUST_BOOTSTRAP_CONFIG') or args.config
1111 if not toml_path and os.path.exists('config.toml'):
1112 toml_path = 'config.toml'
1115 if not os.path.exists(toml_path):
1116 toml_path = os.path.join(build.rust_root, toml_path)
1118 with open(toml_path) as config:
1119 build.config_toml = config.read()
1121 profile = build.get_toml('profile')
1122 if profile is not None:
1123 include_file = 'config.{}.toml'.format(profile)
1124 include_dir = os.path.join(build.rust_root, 'src', 'bootstrap', 'defaults')
1125 include_path = os.path.join(include_dir, include_file)
1126 # HACK: This works because `build.get_toml()` returns the first match it finds for a
1127 # specific key, so appending our defaults at the end allows the user to override them
1128 with open(include_path) as included_toml:
1129 build.config_toml += os.linesep + included_toml.read()
1131 config_verbose = build.get_toml('verbose', 'build')
1132 if config_verbose is not None:
1133 build.verbose = max(build.verbose, int(config_verbose))
1135 build.use_vendored_sources = build.get_toml('vendor', 'build') == 'true'
1137 build.use_locked_deps = build.get_toml('locked-deps', 'build') == 'true'
1139 build.check_vendored_status()
1141 build_dir = build.get_toml('build-dir', 'build') or 'build'
1142 build.build_dir = os.path.abspath(build_dir.replace("$ROOT", build.rust_root))
1144 data = stage0_data(build.rust_root)
1145 build.date = data['date']
1146 build.rustc_channel = data['rustc']
1148 if "rustfmt" in data:
1149 build.rustfmt_channel = data['rustfmt']
1152 build.set_dev_environment()
1154 build.set_normal_environment()
1156 build.build = args.build or build.build_triple()
1157 build.update_submodules()
1159 # Fetch/build the bootstrap
1160 build.download_toolchain()
1161 # Download the master compiler if `download-rustc` is set
1162 build.maybe_download_ci_toolchain()
1164 build.ensure_vendored()
1165 build.build_bootstrap()
1169 args = [build.bootstrap_binary()]
1170 args.extend(sys.argv[1:])
1171 env = os.environ.copy()
1172 env["BOOTSTRAP_PARENT_ID"] = str(os.getpid())
1173 env["BOOTSTRAP_PYTHON"] = sys.executable
1174 env["BUILD_DIR"] = build.build_dir
1175 env["RUSTC_BOOTSTRAP"] = '1'
1177 env["BOOTSTRAP_CONFIG"] = toml_path
1178 run(args, env=env, verbose=build.verbose)
1182 """Entry point for the bootstrap process"""
1185 # x.py help <cmd> ...
1186 if len(sys.argv) > 1 and sys.argv[1] == 'help':
1187 sys.argv = [sys.argv[0], '-h'] + sys.argv[2:]
1190 '-h' in sys.argv) or ('--help' in sys.argv) or (len(sys.argv) == 1)
1192 bootstrap(help_triggered)
1193 if not help_triggered:
1194 print("Build completed successfully in {}".format(
1195 format_build_time(time() - start_time)))
1196 except (SystemExit, KeyboardInterrupt) as error:
1197 if hasattr(error, 'code') and isinstance(error.code, int):
1198 exit_code = error.code
1202 if not help_triggered:
1203 print("Build completed unsuccessfully in {}".format(
1204 format_build_time(time() - start_time)))
1208 if __name__ == '__main__':