]> git.proxmox.com Git - rustc.git/blob - src/bootstrap/bootstrap.py
4111420e4745a17d2ffeaa0b618254bf0b5f323e
[rustc.git] / src / bootstrap / bootstrap.py
1 from __future__ import absolute_import, division, print_function
2 import argparse
3 import contextlib
4 import datetime
5 import distutils.version
6 import hashlib
7 import os
8 import re
9 import shutil
10 import subprocess
11 import sys
12 import tarfile
13 import tempfile
14
15 from time import time
16
17 def support_xz():
18 try:
19 with tempfile.NamedTemporaryFile(delete=False) as temp_file:
20 temp_path = temp_file.name
21 with tarfile.open(temp_path, "w:xz"):
22 pass
23 return True
24 except tarfile.CompressionError:
25 return False
26
27 def get(url, path, verbose=False, do_verify=True):
28 suffix = '.sha256'
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
34
35 try:
36 if do_verify:
37 download(sha_path, sha_url, False, verbose)
38 if os.path.exists(path):
39 if verify(path, sha_path, False):
40 if verbose:
41 print("using already-download file", path)
42 return
43 else:
44 if verbose:
45 print("ignoring already-download file",
46 path, "due to failed verification")
47 os.unlink(path)
48 download(temp_path, url, True, verbose)
49 if do_verify and not verify(temp_path, sha_path, verbose):
50 raise RuntimeError("failed verification")
51 if verbose:
52 print("moving {} to {}".format(temp_path, path))
53 shutil.move(temp_path, path)
54 finally:
55 delete_if_present(sha_path, verbose)
56 delete_if_present(temp_path, verbose)
57
58
59 def delete_if_present(path, verbose):
60 """Remove the given file if present"""
61 if os.path.isfile(path):
62 if verbose:
63 print("removing", path)
64 os.unlink(path)
65
66
67 def download(path, url, probably_big, verbose):
68 for _ in range(0, 4):
69 try:
70 _download(path, url, probably_big, verbose, True)
71 return
72 except RuntimeError:
73 print("\nspurious failure, trying again")
74 _download(path, url, probably_big, verbose, False)
75
76
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)],
85 verbose=verbose,
86 exception=exception)
87 else:
88 if probably_big or verbose:
89 option = "-#"
90 else:
91 option = "-s"
92 require(["curl", "--version"])
93 run(["curl", option,
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],
97 verbose=verbose,
98 exception=exception)
99
100
101 def verify(path, sha_path, verbose):
102 """Check if the sha256 sum of the given path is valid"""
103 if verbose:
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
110 if not verified:
111 print("invalid checksum:\n"
112 " found: {}\n"
113 " expected: {}".format(found, expected))
114 return verified
115
116
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:
124 continue
125 name = member.replace(fname + "/", "", 1)
126 if match is not None and not name.startswith(match):
127 continue
128 name = name[len(match) + 1:]
129
130 dst_path = os.path.join(dst, name)
131 if verbose:
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):
136 continue
137 shutil.move(src_path, dst_path)
138 shutil.rmtree(os.path.join(dst, fname))
139
140
141 def run(args, verbose=False, exception=False, **kwargs):
142 """Run a child program in a new process"""
143 if verbose:
144 print("running: " + ' '.join(args))
145 sys.stdout.flush()
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)
149 code = ret.wait()
150 if code != 0:
151 err = "failed to run: " + ' '.join(args)
152 if verbose or exception:
153 raise RuntimeError(err)
154 sys.exit(err)
155
156
157 def require(cmd, exit=True):
158 '''Run a command, returning its output.
159 On error,
160 If `exit` is `True`, exit the process.
161 Otherwise, return None.'''
162 try:
163 return subprocess.check_output(cmd).strip()
164 except (subprocess.CalledProcessError, OSError) as exc:
165 if not exit:
166 return None
167 print("error: unable to run `{}`: {}".format(' '.join(cmd), exc))
168 print("Please make sure it's installed and in the path.")
169 sys.exit(1)
170
171
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])
179
180
181 def format_build_time(duration):
182 """Return a nicer format for build time
183
184 >>> format_build_time('300')
185 '0:05:00'
186 """
187 return str(datetime.timedelta(seconds=int(duration)))
188
189
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()
196 try:
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]
202 if verbose:
203 print("detected default triple {}".format(triple))
204 return triple
205 except Exception as e:
206 if verbose:
207 print("rustup not detected: {}".format(e))
208 print("falling back to auto-detect")
209
210 required = sys.platform != 'win32'
211 ostype = require(["uname", "-s"], exit=required)
212 cputype = require(['uname', '-m'], exit=required)
213
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'
217
218 ostype = ostype.decode(default_encoding)
219 cputype = cputype.decode(default_encoding)
220
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.
223 ostype_mapper = {
224 'Darwin': 'apple-darwin',
225 'DragonFly': 'unknown-dragonfly',
226 'FreeBSD': 'unknown-freebsd',
227 'Haiku': 'unknown-haiku',
228 'NetBSD': 'unknown-netbsd',
229 'OpenBSD': 'unknown-openbsd'
230 }
231
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'
240 else:
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
258 # MINGW64 on x86_64.
259 ostype = 'pc-windows-gnu'
260 cputype = 'i686'
261 if os.environ.get('MSYSTEM') == 'MINGW64':
262 cputype = 'x86_64'
263 elif ostype.startswith('MSYS'):
264 ostype = 'pc-windows-gnu'
265 elif ostype.startswith('CYGWIN_NT'):
266 cputype = 'i686'
267 if ostype.endswith('WOW64'):
268 cputype = 'x86_64'
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'
275 else:
276 err = "unknown OS type: {}".format(ostype)
277 sys.exit(err)
278
279 if cputype == 'powerpc' and ostype == 'unknown-freebsd':
280 cputype = subprocess.check_output(
281 ['uname', '-p']).strip().decode(default_encoding)
282 cputype_mapper = {
283 'BePC': 'i686',
284 'aarch64': 'aarch64',
285 'amd64': 'x86_64',
286 'arm64': 'aarch64',
287 'i386': 'i686',
288 'i486': 'i686',
289 'i686': 'i686',
290 'i786': 'i686',
291 'powerpc': 'powerpc',
292 'powerpc64': 'powerpc64',
293 'powerpc64le': 'powerpc64le',
294 'ppc': 'powerpc',
295 'ppc64': 'powerpc64',
296 'ppc64le': 'powerpc64le',
297 's390x': 's390x',
298 'x64': 'x86_64',
299 'x86': 'i686',
300 'x86-64': 'x86_64',
301 'x86_64': 'x86_64'
302 }
303
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'}:
308 cputype = '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':
316 cputype = 'arm'
317 if ostype == 'linux-android':
318 ostype = 'linux-androideabi'
319 else:
320 ostype += 'eabihf'
321 elif cputype in {'armv7l', 'armv8l'}:
322 cputype = 'armv7'
323 if ostype == 'linux-android':
324 ostype = 'linux-androideabi'
325 else:
326 ostype += 'eabihf'
327 elif cputype == 'mips':
328 if sys.byteorder == 'big':
329 cputype = 'mips'
330 elif sys.byteorder == 'little':
331 cputype = 'mipsel'
332 else:
333 raise ValueError("unknown byteorder: {}".format(sys.byteorder))
334 elif cputype == 'mips64':
335 if sys.byteorder == 'big':
336 cputype = 'mips64'
337 elif sys.byteorder == 'little':
338 cputype = 'mips64el'
339 else:
340 raise ValueError('unknown byteorder: {}'.format(sys.byteorder))
341 # only the n64 ABI is supported, indicate it
342 ostype += 'abi64'
343 elif cputype == 'sparc' or cputype == 'sparcv9' or cputype == 'sparc64':
344 pass
345 else:
346 err = "unknown cpu type: {}".format(cputype)
347 sys.exit(err)
348
349 return "{}-{}".format(cputype, ostype)
350
351
352 @contextlib.contextmanager
353 def output(filepath):
354 tmp = filepath + '.tmp'
355 with open(tmp, 'w') as f:
356 yield f
357 try:
358 if os.path.exists(filepath):
359 os.remove(filepath) # PermissionError/OSError on Win32 if in use
360 except OSError:
361 shutil.copy2(tmp, filepath)
362 os.remove(tmp)
363 return
364 os.rename(tmp, filepath)
365
366
367 class RustBuild(object):
368 """Provide all the methods required to build Rust"""
369 def __init__(self):
370 self.date = ''
371 self._download_url = ''
372 self.rustc_channel = ''
373 self.rustfmt_channel = ''
374 self.build = ''
375 self.build_dir = ''
376 self.clean = False
377 self.config_toml = ''
378 self.rust_root = ''
379 self.use_locked_deps = ''
380 self.use_vendored_sources = ''
381 self.verbose = False
382 self.git_version = None
383 self.nix_deps_dir = None
384 self.rustc_commit = None
385
386 def download_toolchain(self, stage0=True, rustc_channel=None):
387 """Fetch the build system for Rust, written in Rust
388
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
391 compiler itself.
392
393 Each downloaded tarball is extracted, after that, the script
394 will move all the content to the right place.
395 """
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)
400
401 key = self.date
402 if not 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,
415 tarball_suffix)
416 self._download_component_helper(filename, "rustc", tarball_suffix, stage0)
417 filename = "cargo-{}-{}{}".format(rustc_channel, self.build,
418 tarball_suffix)
419 self._download_component_helper(filename, "cargo", tarball_suffix)
420 if not stage0:
421 filename = "rustc-dev-{}-{}{}".format(rustc_channel, self.build, tarball_suffix)
422 self._download_component_helper(
423 filename, "rustc-dev", tarball_suffix, stage0
424 )
425
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))
433 with output(self.rustc_stamp(stage0)) as rust_stamp:
434 rust_stamp.write(key)
435
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)
439 ):
440 if 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
446 )
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)
451
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.
456 #
457 # This git command finds that commit SHA, looking for bors-authored
458 # merges that modified src/llvm-project.
459 #
460 # This works even in a repository that has not yet initialized
461 # submodules.
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",
468 "--",
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))
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))
484 with output(self.llvm_stamp()) as llvm_stamp:
485 llvm_stamp.write(llvm_sha + str(llvm_assertions))
486
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
490 # artifacts)
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",
501 ]
502 return opt == "true" \
503 or (opt == "if-available" and self.build in supported_platforms)
504
505 def _download_component_helper(
506 self, filename, pattern, tarball_suffix, stage0=True, key=None
507 ):
508 if key is None:
509 if stage0:
510 key = self.date
511 else:
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)
517
518 if stage0:
519 url = "{}/dist/{}".format(self._download_url, key)
520 else:
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)
526
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)
533
534 url = "https://ci-artifacts.rust-lang.org/rustc-builds/{}".format(llvm_sha)
535 if llvm_assertions:
536 url = url.replace('rustc-builds', 'rustc-builds-alt')
537 # ci-artifacts are only stored as .xz, not .gz
538 if not support_xz():
539 print("error: XZ support is required to download LLVM")
540 print("help: consider disabling `download-ci-llvm` or using python3")
541 exit(1)
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(),
548 match="rust-dev",
549 verbose=self.verbose)
550
551 def fix_bin_or_dylib(self, fname):
552 """Modifies the interpreter section of 'fname' to fix the dynamic linker,
553 or the RPATH section, to fix the dynamic library search path
554
555 This method is only required on NixOS and uses the PatchELF utility to
556 change the interpreter/RPATH of ELF executables.
557
558 Please see https://nixos.org/patchelf.html for more information
559 """
560 default_encoding = sys.getdefaultencoding()
561 try:
562 ostype = subprocess.check_output(
563 ['uname', '-s']).strip().decode(default_encoding)
564 except subprocess.CalledProcessError:
565 return
566 except OSError as reason:
567 if getattr(reason, 'winerror', None) is not None:
568 return
569 raise reason
570
571 if ostype != "Linux":
572 return
573
574 if not os.path.exists("/etc/NIXOS"):
575 return
576 if os.path.exists("/lib"):
577 return
578
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)
582
583 # Only build `.nix-deps` once.
584 nix_deps_dir = self.nix_deps_dir
585 if not nix_deps_dir:
586 # Run `nix-build` to "build" each dependency (which will likely reuse
587 # the existing `/nix/store` copy, or at most download a pre-built copy).
588 #
589 # Importantly, we create a gc-root called `.nix-deps` in the `build/`
590 # directory, but still reference the actual `/nix/store` path in the rpath
591 # as it makes it significantly more robust against changes to the location of
592 # the `.nix-deps` location.
593 #
594 # bintools: Needed for the path of `ld-linux.so` (via `nix-support/dynamic-linker`).
595 # zlib: Needed as a system dependency of `libLLVM-*.so`.
596 # patchelf: Needed for patching ELF binaries (see doc comment above).
597 nix_deps_dir = "{}/{}".format(self.build_dir, ".nix-deps")
598 nix_expr = '''
599 with (import <nixpkgs> {});
600 symlinkJoin {
601 name = "rust-stage0-dependencies";
602 paths = [
603 zlib
604 patchelf
605 stdenv.cc.bintools
606 ];
607 }
608 '''
609 try:
610 subprocess.check_output([
611 "nix-build", "-E", nix_expr, "-o", nix_deps_dir,
612 ])
613 except subprocess.CalledProcessError as reason:
614 print("warning: failed to call nix-build:", reason)
615 return
616 self.nix_deps_dir = nix_deps_dir
617
618 patchelf = "{}/bin/patchelf".format(nix_deps_dir)
619 rpath_entries = [
620 # Relative default, all binary and dynamic libraries we ship
621 # appear to have this (even when `../lib` is redundant).
622 "$ORIGIN/../lib",
623 os.path.join(os.path.realpath(nix_deps_dir), "lib")
624 ]
625 patchelf_args = ["--set-rpath", ":".join(rpath_entries)]
626 if not fname.endswith(".so"):
627 # Finally, set the corret .interp for binaries
628 with open("{}/nix-support/dynamic-linker".format(nix_deps_dir)) as dynamic_linker:
629 patchelf_args += ["--set-interpreter", dynamic_linker.read().rstrip()]
630
631 try:
632 subprocess.check_output([patchelf] + patchelf_args + [fname])
633 except subprocess.CalledProcessError as reason:
634 print("warning: failed to call patchelf:", reason)
635 return
636
637 # If `download-rustc` is set, download the most recent commit with CI artifacts
638 def maybe_download_ci_toolchain(self):
639 # If `download-rustc` is not set, default to rebuilding.
640 download_rustc = self.get_toml("download-rustc", section="rust")
641 if download_rustc is None or download_rustc == "false":
642 return None
643 assert download_rustc == "true" or download_rustc == "if-unchanged", download_rustc
644
645 # Handle running from a directory other than the top level
646 rev_parse = ["git", "rev-parse", "--show-toplevel"]
647 top_level = subprocess.check_output(rev_parse, universal_newlines=True).strip()
648 compiler = "{}/compiler/".format(top_level)
649
650 # Look for a version to compare to based on the current commit.
651 # Only commits merged by bors will have CI artifacts.
652 merge_base = ["git", "log", "--author=bors", "--pretty=%H", "-n1"]
653 commit = subprocess.check_output(merge_base, universal_newlines=True).strip()
654
655 # Warn if there were changes to the compiler since the ancestor commit.
656 status = subprocess.call(["git", "diff-index", "--quiet", commit, "--", compiler])
657 if status != 0:
658 if download_rustc == "if-unchanged":
659 return None
660 print("warning: `download-rustc` is enabled, but there are changes to compiler/")
661
662 if self.verbose:
663 print("using downloaded stage1 artifacts from CI (commit {})".format(commit))
664 self.rustc_commit = commit
665 # FIXME: support downloading artifacts from the beta channel
666 self.download_toolchain(False, "nightly")
667
668 def rustc_stamp(self, stage0):
669 """Return the path for .rustc-stamp at the given stage
670
671 >>> rb = RustBuild()
672 >>> rb.build_dir = "build"
673 >>> rb.rustc_stamp(True) == os.path.join("build", "stage0", ".rustc-stamp")
674 True
675 >>> rb.rustc_stamp(False) == os.path.join("build", "ci-rustc", ".rustc-stamp")
676 True
677 """
678 return os.path.join(self.bin_root(stage0), '.rustc-stamp')
679
680 def rustfmt_stamp(self):
681 """Return the path for .rustfmt-stamp
682
683 >>> rb = RustBuild()
684 >>> rb.build_dir = "build"
685 >>> rb.rustfmt_stamp() == os.path.join("build", "stage0", ".rustfmt-stamp")
686 True
687 """
688 return os.path.join(self.bin_root(True), '.rustfmt-stamp')
689
690 def llvm_stamp(self):
691 """Return the path for .rustfmt-stamp
692
693 >>> rb = RustBuild()
694 >>> rb.build_dir = "build"
695 >>> rb.llvm_stamp() == os.path.join("build", "ci-llvm", ".llvm-stamp")
696 True
697 """
698 return os.path.join(self.llvm_root(), '.llvm-stamp')
699
700
701 def program_out_of_date(self, stamp_path, key):
702 """Check if the given program stamp is out of date"""
703 if not os.path.exists(stamp_path) or self.clean:
704 return True
705 with open(stamp_path, 'r') as stamp:
706 return key != stamp.read()
707
708 def bin_root(self, stage0):
709 """Return the binary root directory for the given stage
710
711 >>> rb = RustBuild()
712 >>> rb.build_dir = "build"
713 >>> rb.bin_root(True) == os.path.join("build", "stage0")
714 True
715 >>> rb.bin_root(False) == os.path.join("build", "ci-rustc")
716 True
717
718 When the 'build' property is given should be a nested directory:
719
720 >>> rb.build = "devel"
721 >>> rb.bin_root(True) == os.path.join("build", "devel", "stage0")
722 True
723 """
724 if stage0:
725 subdir = "stage0"
726 else:
727 subdir = "ci-rustc"
728 return os.path.join(self.build_dir, self.build, subdir)
729
730 def llvm_root(self):
731 """Return the CI LLVM root directory
732
733 >>> rb = RustBuild()
734 >>> rb.build_dir = "build"
735 >>> rb.llvm_root() == os.path.join("build", "ci-llvm")
736 True
737
738 When the 'build' property is given should be a nested directory:
739
740 >>> rb.build = "devel"
741 >>> rb.llvm_root() == os.path.join("build", "devel", "ci-llvm")
742 True
743 """
744 return os.path.join(self.build_dir, self.build, "ci-llvm")
745
746 def get_toml(self, key, section=None):
747 """Returns the value of the given key in config.toml, otherwise returns None
748
749 >>> rb = RustBuild()
750 >>> rb.config_toml = 'key1 = "value1"\\nkey2 = "value2"'
751 >>> rb.get_toml("key2")
752 'value2'
753
754 If the key does not exists, the result is None:
755
756 >>> rb.get_toml("key3") is None
757 True
758
759 Optionally also matches the section the key appears in
760
761 >>> rb.config_toml = '[a]\\nkey = "value1"\\n[b]\\nkey = "value2"'
762 >>> rb.get_toml('key', 'a')
763 'value1'
764 >>> rb.get_toml('key', 'b')
765 'value2'
766 >>> rb.get_toml('key', 'c') is None
767 True
768
769 >>> rb.config_toml = 'key1 = true'
770 >>> rb.get_toml("key1")
771 'true'
772 """
773
774 cur_section = None
775 for line in self.config_toml.splitlines():
776 section_match = re.match(r'^\s*\[(.*)\]\s*$', line)
777 if section_match is not None:
778 cur_section = section_match.group(1)
779
780 match = re.match(r'^{}\s*=(.*)$'.format(key), line)
781 if match is not None:
782 value = match.group(1)
783 if section is None or section == cur_section:
784 return self.get_string(value) or value.strip()
785 return None
786
787 def cargo(self):
788 """Return config path for cargo"""
789 return self.program_config('cargo')
790
791 def rustc(self, stage0):
792 """Return config path for rustc"""
793 return self.program_config('rustc', stage0)
794
795 def rustfmt(self):
796 """Return config path for rustfmt"""
797 if not self.rustfmt_channel:
798 return None
799 return self.program_config('rustfmt')
800
801 def program_config(self, program, stage0=True):
802 """Return config path for the given program at the given stage
803
804 >>> rb = RustBuild()
805 >>> rb.config_toml = 'rustc = "rustc"\\n'
806 >>> rb.program_config('rustc')
807 'rustc'
808 >>> rb.config_toml = ''
809 >>> cargo_path = rb.program_config('cargo', True)
810 >>> cargo_path.rstrip(".exe") == os.path.join(rb.bin_root(True),
811 ... "bin", "cargo")
812 True
813 >>> cargo_path = rb.program_config('cargo', False)
814 >>> cargo_path.rstrip(".exe") == os.path.join(rb.bin_root(False),
815 ... "bin", "cargo")
816 True
817 """
818 config = self.get_toml(program)
819 if config:
820 return os.path.expanduser(config)
821 return os.path.join(self.bin_root(stage0), "bin", "{}{}".format(
822 program, self.exe_suffix()))
823
824 @staticmethod
825 def get_string(line):
826 """Return the value between double quotes
827
828 >>> RustBuild.get_string(' "devel" ')
829 'devel'
830 >>> RustBuild.get_string(" 'devel' ")
831 'devel'
832 >>> RustBuild.get_string('devel') is None
833 True
834 >>> RustBuild.get_string(' "devel ')
835 ''
836 """
837 start = line.find('"')
838 if start != -1:
839 end = start + 1 + line[start + 1:].find('"')
840 return line[start + 1:end]
841 start = line.find('\'')
842 if start != -1:
843 end = start + 1 + line[start + 1:].find('\'')
844 return line[start + 1:end]
845 return None
846
847 @staticmethod
848 def exe_suffix():
849 """Return a suffix for executables"""
850 if sys.platform == 'win32':
851 return '.exe'
852 return ''
853
854 def bootstrap_binary(self):
855 """Return the path of the bootstrap binary
856
857 >>> rb = RustBuild()
858 >>> rb.build_dir = "build"
859 >>> rb.bootstrap_binary() == os.path.join("build", "bootstrap",
860 ... "debug", "bootstrap")
861 True
862 """
863 return os.path.join(self.build_dir, "bootstrap", "debug", "bootstrap")
864
865 def build_bootstrap(self):
866 """Build bootstrap"""
867 build_dir = os.path.join(self.build_dir, "bootstrap")
868 if self.clean and os.path.exists(build_dir):
869 shutil.rmtree(build_dir)
870 env = os.environ.copy()
871 # `CARGO_BUILD_TARGET` breaks bootstrap build.
872 # See also: <https://github.com/rust-lang/rust/issues/70208>.
873 if "CARGO_BUILD_TARGET" in env:
874 del env["CARGO_BUILD_TARGET"]
875 env["CARGO_TARGET_DIR"] = build_dir
876 env["RUSTC"] = self.rustc(True)
877 env["LD_LIBRARY_PATH"] = os.path.join(self.bin_root(True), "lib") + \
878 (os.pathsep + env["LD_LIBRARY_PATH"]) \
879 if "LD_LIBRARY_PATH" in env else ""
880 env["DYLD_LIBRARY_PATH"] = os.path.join(self.bin_root(True), "lib") + \
881 (os.pathsep + env["DYLD_LIBRARY_PATH"]) \
882 if "DYLD_LIBRARY_PATH" in env else ""
883 env["LIBRARY_PATH"] = os.path.join(self.bin_root(True), "lib") + \
884 (os.pathsep + env["LIBRARY_PATH"]) \
885 if "LIBRARY_PATH" in env else ""
886 # preserve existing RUSTFLAGS
887 env.setdefault("RUSTFLAGS", "")
888 env["RUSTFLAGS"] += " -Cdebuginfo=2"
889
890 build_section = "target.{}".format(self.build)
891 target_features = []
892 if self.get_toml("crt-static", build_section) == "true":
893 target_features += ["+crt-static"]
894 elif self.get_toml("crt-static", build_section) == "false":
895 target_features += ["-crt-static"]
896 if target_features:
897 env["RUSTFLAGS"] += " -C target-feature=" + (",".join(target_features))
898 target_linker = self.get_toml("linker", build_section)
899 if target_linker is not None:
900 env["RUSTFLAGS"] += " -C linker=" + target_linker
901 # cfg(bootstrap): Add `-Wsemicolon_in_expressions_from_macros` after the next beta bump
902 env["RUSTFLAGS"] += " -Wrust_2018_idioms -Wunused_lifetimes"
903 if self.get_toml("deny-warnings", "rust") != "false":
904 env["RUSTFLAGS"] += " -Dwarnings"
905
906 env["PATH"] = os.path.join(self.bin_root(True), "bin") + \
907 os.pathsep + env["PATH"]
908 if not os.path.isfile(self.cargo()):
909 raise Exception("no cargo executable found at `{}`".format(
910 self.cargo()))
911 args = [self.cargo(), "build", "--manifest-path",
912 os.path.join(self.rust_root, "src/bootstrap/Cargo.toml")]
913 for _ in range(1, self.verbose):
914 args.append("--verbose")
915 if self.use_locked_deps:
916 args.append("--locked")
917 if self.use_vendored_sources:
918 args.append("--frozen")
919 run(args, env=env, verbose=self.verbose)
920
921 def build_triple(self):
922 """Build triple as in LLVM
923
924 Note that `default_build_triple` is moderately expensive,
925 so use `self.build` where possible.
926 """
927 config = self.get_toml('build')
928 if config:
929 return config
930 return default_build_triple(self.verbose)
931
932 def check_submodule(self, module, slow_submodules):
933 if not slow_submodules:
934 checked_out = subprocess.Popen(["git", "rev-parse", "HEAD"],
935 cwd=os.path.join(self.rust_root, module),
936 stdout=subprocess.PIPE)
937 return checked_out
938 else:
939 return None
940
941 def update_submodule(self, module, checked_out, recorded_submodules):
942 module_path = os.path.join(self.rust_root, module)
943
944 if checked_out is not None:
945 default_encoding = sys.getdefaultencoding()
946 checked_out = checked_out.communicate()[0].decode(default_encoding).strip()
947 if recorded_submodules[module] == checked_out:
948 return
949
950 print("Updating submodule", module)
951
952 run(["git", "submodule", "-q", "sync", module],
953 cwd=self.rust_root, verbose=self.verbose)
954
955 update_args = ["git", "submodule", "update", "--init", "--recursive"]
956 if self.git_version >= distutils.version.LooseVersion("2.11.0"):
957 update_args.append("--progress")
958 update_args.append(module)
959 run(update_args, cwd=self.rust_root, verbose=self.verbose, exception=True)
960
961 run(["git", "reset", "-q", "--hard"],
962 cwd=module_path, verbose=self.verbose)
963 run(["git", "clean", "-qdfx"],
964 cwd=module_path, verbose=self.verbose)
965
966 def update_submodules(self):
967 """Update submodules"""
968 if (not os.path.exists(os.path.join(self.rust_root, ".git"))) or \
969 self.get_toml('submodules') == "false":
970 return
971
972 default_encoding = sys.getdefaultencoding()
973
974 # check the existence and version of 'git' command
975 git_version_str = require(['git', '--version']).split()[2].decode(default_encoding)
976 self.git_version = distutils.version.LooseVersion(git_version_str)
977
978 slow_submodules = self.get_toml('fast-submodules') == "false"
979 start_time = time()
980 if slow_submodules:
981 print('Unconditionally updating all submodules')
982 else:
983 print('Updating only changed submodules')
984 default_encoding = sys.getdefaultencoding()
985 submodules = [s.split(' ', 1)[1] for s in subprocess.check_output(
986 ["git", "config", "--file",
987 os.path.join(self.rust_root, ".gitmodules"),
988 "--get-regexp", "path"]
989 ).decode(default_encoding).splitlines()]
990 filtered_submodules = []
991 submodules_names = []
992 llvm_checked_out = os.path.exists(os.path.join(self.rust_root, "src/llvm-project/.git"))
993 external_llvm_provided = self.get_toml('llvm-config') or self.downloading_llvm()
994 llvm_needed = not self.get_toml('codegen-backends', 'rust') \
995 or "llvm" in self.get_toml('codegen-backends', 'rust')
996 for module in submodules:
997 if module.endswith("llvm-project"):
998 # Don't sync the llvm-project submodule if an external LLVM was
999 # provided, if we are downloading LLVM or if the LLVM backend is
1000 # not being built. Also, if the submodule has been initialized
1001 # already, sync it anyways so that it doesn't mess up contributor
1002 # pull requests.
1003 if external_llvm_provided or not llvm_needed:
1004 if self.get_toml('lld') != 'true' and not llvm_checked_out:
1005 continue
1006 check = self.check_submodule(module, slow_submodules)
1007 filtered_submodules.append((module, check))
1008 submodules_names.append(module)
1009 recorded = subprocess.Popen(["git", "ls-tree", "HEAD"] + submodules_names,
1010 cwd=self.rust_root, stdout=subprocess.PIPE)
1011 recorded = recorded.communicate()[0].decode(default_encoding).strip().splitlines()
1012 recorded_submodules = {}
1013 for data in recorded:
1014 data = data.split()
1015 recorded_submodules[data[3]] = data[2]
1016 for module in filtered_submodules:
1017 self.update_submodule(module[0], module[1], recorded_submodules)
1018 print("Submodules updated in %.2f seconds" % (time() - start_time))
1019
1020 def set_normal_environment(self):
1021 """Set download URL for normal environment"""
1022 if 'RUSTUP_DIST_SERVER' in os.environ:
1023 self._download_url = os.environ['RUSTUP_DIST_SERVER']
1024 else:
1025 self._download_url = 'https://static.rust-lang.org'
1026
1027 def set_dev_environment(self):
1028 """Set download URL for development environment"""
1029 if 'RUSTUP_DEV_DIST_SERVER' in os.environ:
1030 self._download_url = os.environ['RUSTUP_DEV_DIST_SERVER']
1031 else:
1032 self._download_url = 'https://dev-static.rust-lang.org'
1033
1034 def check_vendored_status(self):
1035 """Check that vendoring is configured properly"""
1036 vendor_dir = os.path.join(self.rust_root, 'vendor')
1037 if 'SUDO_USER' in os.environ and not self.use_vendored_sources:
1038 if os.environ.get('USER') != os.environ['SUDO_USER']:
1039 self.use_vendored_sources = True
1040 print('info: looks like you are running this command under `sudo`')
1041 print(' and so in order to preserve your $HOME this will now')
1042 print(' use vendored sources by default.')
1043 if not os.path.exists(vendor_dir):
1044 print('error: vendoring required, but vendor directory does not exist.')
1045 print(' Run `cargo vendor` without sudo to initialize the '
1046 'vendor directory.')
1047 raise Exception("{} not found".format(vendor_dir))
1048
1049 if self.use_vendored_sources:
1050 if not os.path.exists('.cargo'):
1051 os.makedirs('.cargo')
1052 with output('.cargo/config') as cargo_config:
1053 cargo_config.write(
1054 "[source.crates-io]\n"
1055 "replace-with = 'vendored-sources'\n"
1056 "registry = 'https://example.com'\n"
1057 "\n"
1058 "[source.vendored-sources]\n"
1059 "directory = '{}/vendor'\n"
1060 .format(self.rust_root))
1061 else:
1062 if os.path.exists('.cargo'):
1063 shutil.rmtree('.cargo')
1064
1065 def ensure_vendored(self):
1066 """Ensure that the vendored sources are available if needed"""
1067 vendor_dir = os.path.join(self.rust_root, 'vendor')
1068 # Note that this does not handle updating the vendored dependencies if
1069 # the rust git repository is updated. Normal development usually does
1070 # not use vendoring, so hopefully this isn't too much of a problem.
1071 if self.use_vendored_sources and not os.path.exists(vendor_dir):
1072 run([
1073 self.cargo(),
1074 "vendor",
1075 "--sync=./src/tools/rust-analyzer/Cargo.toml",
1076 "--sync=./compiler/rustc_codegen_cranelift/Cargo.toml",
1077 ], verbose=self.verbose, cwd=self.rust_root)
1078
1079
1080 def bootstrap(help_triggered):
1081 """Configure, fetch, build and run the initial bootstrap"""
1082
1083 # If the user is asking for help, let them know that the whole download-and-build
1084 # process has to happen before anything is printed out.
1085 if help_triggered:
1086 print("info: Downloading and building bootstrap before processing --help")
1087 print(" command. See src/bootstrap/README.md for help with common")
1088 print(" commands.")
1089
1090 parser = argparse.ArgumentParser(description='Build rust')
1091 parser.add_argument('--config')
1092 parser.add_argument('--build')
1093 parser.add_argument('--clean', action='store_true')
1094 parser.add_argument('-v', '--verbose', action='count', default=0)
1095
1096 args = [a for a in sys.argv if a != '-h' and a != '--help']
1097 args, _ = parser.parse_known_args(args)
1098
1099 # Configure initial bootstrap
1100 build = RustBuild()
1101 build.rust_root = os.path.abspath(os.path.join(__file__, '../../..'))
1102 build.verbose = args.verbose
1103 build.clean = args.clean
1104
1105 # Read from `RUST_BOOTSTRAP_CONFIG`, then `--config`, then fallback to `config.toml` (if it
1106 # exists).
1107 toml_path = os.getenv('RUST_BOOTSTRAP_CONFIG') or args.config
1108 if not toml_path and os.path.exists('config.toml'):
1109 toml_path = 'config.toml'
1110
1111 if toml_path:
1112 if not os.path.exists(toml_path):
1113 toml_path = os.path.join(build.rust_root, toml_path)
1114
1115 with open(toml_path) as config:
1116 build.config_toml = config.read()
1117
1118 profile = build.get_toml('profile')
1119 if profile is not None:
1120 include_file = 'config.{}.toml'.format(profile)
1121 include_dir = os.path.join(build.rust_root, 'src', 'bootstrap', 'defaults')
1122 include_path = os.path.join(include_dir, include_file)
1123 # HACK: This works because `build.get_toml()` returns the first match it finds for a
1124 # specific key, so appending our defaults at the end allows the user to override them
1125 with open(include_path) as included_toml:
1126 build.config_toml += os.linesep + included_toml.read()
1127
1128 config_verbose = build.get_toml('verbose', 'build')
1129 if config_verbose is not None:
1130 build.verbose = max(build.verbose, int(config_verbose))
1131
1132 build.use_vendored_sources = build.get_toml('vendor', 'build') == 'true'
1133
1134 build.use_locked_deps = build.get_toml('locked-deps', 'build') == 'true'
1135
1136 build.check_vendored_status()
1137
1138 build_dir = build.get_toml('build-dir', 'build') or 'build'
1139 build.build_dir = os.path.abspath(build_dir.replace("$ROOT", build.rust_root))
1140
1141 data = stage0_data(build.rust_root)
1142 build.date = data['date']
1143 build.rustc_channel = data['rustc']
1144
1145 if "rustfmt" in data:
1146 build.rustfmt_channel = data['rustfmt']
1147
1148 if 'dev' in data:
1149 build.set_dev_environment()
1150 else:
1151 build.set_normal_environment()
1152
1153 build.build = args.build or build.build_triple()
1154 build.update_submodules()
1155
1156 # Fetch/build the bootstrap
1157 build.download_toolchain()
1158 # Download the master compiler if `download-rustc` is set
1159 build.maybe_download_ci_toolchain()
1160 sys.stdout.flush()
1161 build.ensure_vendored()
1162 build.build_bootstrap()
1163 sys.stdout.flush()
1164
1165 # Run the bootstrap
1166 args = [build.bootstrap_binary()]
1167 args.extend(sys.argv[1:])
1168 env = os.environ.copy()
1169 env["BOOTSTRAP_PARENT_ID"] = str(os.getpid())
1170 env["BOOTSTRAP_PYTHON"] = sys.executable
1171 env["BUILD_DIR"] = build.build_dir
1172 env["RUSTC_BOOTSTRAP"] = '1'
1173 if toml_path:
1174 env["BOOTSTRAP_CONFIG"] = toml_path
1175 if build.rustc_commit is not None:
1176 env["BOOTSTRAP_DOWNLOAD_RUSTC"] = '1'
1177 run(args, env=env, verbose=build.verbose)
1178
1179
1180 def main():
1181 """Entry point for the bootstrap process"""
1182 start_time = time()
1183
1184 # x.py help <cmd> ...
1185 if len(sys.argv) > 1 and sys.argv[1] == 'help':
1186 sys.argv = [sys.argv[0], '-h'] + sys.argv[2:]
1187
1188 help_triggered = (
1189 '-h' in sys.argv) or ('--help' in sys.argv) or (len(sys.argv) == 1)
1190 try:
1191 bootstrap(help_triggered)
1192 if not help_triggered:
1193 print("Build completed successfully in {}".format(
1194 format_build_time(time() - start_time)))
1195 except (SystemExit, KeyboardInterrupt) as error:
1196 if hasattr(error, 'code') and isinstance(error.code, int):
1197 exit_code = error.code
1198 else:
1199 exit_code = 1
1200 print(error)
1201 if not help_triggered:
1202 print("Build completed unsuccessfully in {}".format(
1203 format_build_time(time() - start_time)))
1204 sys.exit(exit_code)
1205
1206
1207 if __name__ == '__main__':
1208 main()