]> git.lizzy.rs Git - rust.git/blob - src/bootstrap/bootstrap.py
bootstrap: tweak verbosity settings
[rust.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 json
8 import os
9 import re
10 import shutil
11 import subprocess
12 import sys
13 import tarfile
14 import tempfile
15
16 from time import time
17
18 def support_xz():
19     try:
20         with tempfile.NamedTemporaryFile(delete=False) as temp_file:
21             temp_path = temp_file.name
22         with tarfile.open(temp_path, "w:xz"):
23             pass
24         return True
25     except tarfile.CompressionError:
26         return False
27
28 def get(base, url, path, checksums, verbose=False, do_verify=True):
29     with tempfile.NamedTemporaryFile(delete=False) as temp_file:
30         temp_path = temp_file.name
31
32     try:
33         if do_verify:
34             if url not in checksums:
35                 raise RuntimeError("src/stage0.json doesn't contain a checksum for {}".format(url))
36             sha256 = checksums[url]
37             if os.path.exists(path):
38                 if verify(path, sha256, False):
39                     if verbose:
40                         print("using already-download file", path)
41                     return
42                 else:
43                     if verbose:
44                         print("ignoring already-download file",
45                             path, "due to failed verification")
46                     os.unlink(path)
47         download(temp_path, "{}/{}".format(base, url), True, verbose)
48         if do_verify and not verify(temp_path, sha256, verbose):
49             raise RuntimeError("failed verification")
50         if verbose:
51             print("moving {} to {}".format(temp_path, path))
52         shutil.move(temp_path, path)
53     finally:
54         if os.path.isfile(temp_path):
55             if verbose:
56                 print("removing", temp_path)
57             os.unlink(temp_path)
58
59
60 def download(path, url, probably_big, verbose):
61     for _ in range(0, 4):
62         try:
63             _download(path, url, probably_big, verbose, True)
64             return
65         except RuntimeError:
66             print("\nspurious failure, trying again")
67     _download(path, url, probably_big, verbose, False)
68
69
70 def _download(path, url, probably_big, verbose, exception):
71     if probably_big or verbose:
72         print("downloading {}".format(url))
73     # see https://serverfault.com/questions/301128/how-to-download
74     if sys.platform == 'win32':
75         run(["PowerShell.exe", "/nologo", "-Command",
76              "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12;",
77              "(New-Object System.Net.WebClient).DownloadFile('{}', '{}')".format(url, path)],
78             verbose=verbose,
79             exception=exception)
80     else:
81         if probably_big or verbose:
82             option = "-#"
83         else:
84             option = "-s"
85         require(["curl", "--version"])
86         run(["curl", option,
87              "-y", "30", "-Y", "10",    # timeout if speed is < 10 bytes/sec for > 30 seconds
88              "--connect-timeout", "30",  # timeout if cannot connect within 30 seconds
89              "--retry", "3", "-Sf", "-o", path, url],
90             verbose=verbose,
91             exception=exception)
92
93
94 def verify(path, expected, verbose):
95     """Check if the sha256 sum of the given path is valid"""
96     if verbose:
97         print("verifying", path)
98     with open(path, "rb") as source:
99         found = hashlib.sha256(source.read()).hexdigest()
100     verified = found == expected
101     if not verified:
102         print("invalid checksum:\n"
103               "    found:    {}\n"
104               "    expected: {}".format(found, expected))
105     return verified
106
107
108 def unpack(tarball, tarball_suffix, dst, verbose=False, match=None):
109     """Unpack the given tarball file"""
110     print("extracting", tarball)
111     fname = os.path.basename(tarball).replace(tarball_suffix, "")
112     with contextlib.closing(tarfile.open(tarball)) as tar:
113         for member in tar.getnames():
114             if "/" not in member:
115                 continue
116             name = member.replace(fname + "/", "", 1)
117             if match is not None and not name.startswith(match):
118                 continue
119             name = name[len(match) + 1:]
120
121             dst_path = os.path.join(dst, name)
122             if verbose:
123                 print("  extracting", member)
124             tar.extract(member, dst)
125             src_path = os.path.join(dst, member)
126             if os.path.isdir(src_path) and os.path.exists(dst_path):
127                 continue
128             shutil.move(src_path, dst_path)
129     shutil.rmtree(os.path.join(dst, fname))
130
131
132 def run(args, verbose=False, exception=False, is_bootstrap=False, **kwargs):
133     """Run a child program in a new process"""
134     if verbose:
135         print("running: " + ' '.join(args))
136     sys.stdout.flush()
137     # Use Popen here instead of call() as it apparently allows powershell on
138     # Windows to not lock up waiting for input presumably.
139     ret = subprocess.Popen(args, **kwargs)
140     code = ret.wait()
141     if code != 0:
142         err = "failed to run: " + ' '.join(args)
143         if verbose or exception:
144             raise RuntimeError(err)
145         # For most failures, we definitely do want to print this error, or the user will have no
146         # idea what went wrong. But when we've successfully built bootstrap and it failed, it will
147         # have already printed an error above, so there's no need to print the exact command we're
148         # running.
149         if is_bootstrap:
150             sys.exit(1)
151         else:
152             sys.exit(err)
153
154
155 def require(cmd, exit=True):
156     '''Run a command, returning its output.
157     On error,
158         If `exit` is `True`, exit the process.
159         Otherwise, return None.'''
160     try:
161         return subprocess.check_output(cmd).strip()
162     except (subprocess.CalledProcessError, OSError) as exc:
163         if not exit:
164             return None
165         print("error: unable to run `{}`: {}".format(' '.join(cmd), exc))
166         print("Please make sure it's installed and in the path.")
167         sys.exit(1)
168
169
170 def format_build_time(duration):
171     """Return a nicer format for build time
172
173     >>> format_build_time('300')
174     '0:05:00'
175     """
176     return str(datetime.timedelta(seconds=int(duration)))
177
178
179 def default_build_triple(verbose):
180     """Build triple as in LLVM"""
181     # If the user already has a host build triple with an existing `rustc`
182     # install, use their preference. This fixes most issues with Windows builds
183     # being detected as GNU instead of MSVC.
184     default_encoding = sys.getdefaultencoding()
185     try:
186         version = subprocess.check_output(["rustc", "--version", "--verbose"],
187                 stderr=subprocess.DEVNULL)
188         version = version.decode(default_encoding)
189         host = next(x for x in version.split('\n') if x.startswith("host: "))
190         triple = host.split("host: ")[1]
191         if verbose:
192             print("detected default triple {}".format(triple))
193         return triple
194     except Exception as e:
195         if verbose:
196             print("rustup not detected: {}".format(e))
197             print("falling back to auto-detect")
198
199     required = sys.platform != 'win32'
200     ostype = require(["uname", "-s"], exit=required)
201     cputype = require(['uname', '-m'], exit=required)
202
203     # If we do not have `uname`, assume Windows.
204     if ostype is None or cputype is None:
205         return 'x86_64-pc-windows-msvc'
206
207     ostype = ostype.decode(default_encoding)
208     cputype = cputype.decode(default_encoding)
209
210     # The goal here is to come up with the same triple as LLVM would,
211     # at least for the subset of platforms we're willing to target.
212     ostype_mapper = {
213         'Darwin': 'apple-darwin',
214         'DragonFly': 'unknown-dragonfly',
215         'FreeBSD': 'unknown-freebsd',
216         'Haiku': 'unknown-haiku',
217         'NetBSD': 'unknown-netbsd',
218         'OpenBSD': 'unknown-openbsd'
219     }
220
221     # Consider the direct transformation first and then the special cases
222     if ostype in ostype_mapper:
223         ostype = ostype_mapper[ostype]
224     elif ostype == 'Linux':
225         os_from_sp = subprocess.check_output(
226             ['uname', '-o']).strip().decode(default_encoding)
227         if os_from_sp == 'Android':
228             ostype = 'linux-android'
229         else:
230             ostype = 'unknown-linux-gnu'
231     elif ostype == 'SunOS':
232         ostype = 'pc-solaris'
233         # On Solaris, uname -m will return a machine classification instead
234         # of a cpu type, so uname -p is recommended instead.  However, the
235         # output from that option is too generic for our purposes (it will
236         # always emit 'i386' on x86/amd64 systems).  As such, isainfo -k
237         # must be used instead.
238         cputype = require(['isainfo', '-k']).decode(default_encoding)
239         # sparc cpus have sun as a target vendor
240         if 'sparc' in cputype:
241             ostype = 'sun-solaris'
242     elif ostype.startswith('MINGW'):
243         # msys' `uname` does not print gcc configuration, but prints msys
244         # configuration. so we cannot believe `uname -m`:
245         # msys1 is always i686 and msys2 is always x86_64.
246         # instead, msys defines $MSYSTEM which is MINGW32 on i686 and
247         # MINGW64 on x86_64.
248         ostype = 'pc-windows-gnu'
249         cputype = 'i686'
250         if os.environ.get('MSYSTEM') == 'MINGW64':
251             cputype = 'x86_64'
252     elif ostype.startswith('MSYS'):
253         ostype = 'pc-windows-gnu'
254     elif ostype.startswith('CYGWIN_NT'):
255         cputype = 'i686'
256         if ostype.endswith('WOW64'):
257             cputype = 'x86_64'
258         ostype = 'pc-windows-gnu'
259     elif sys.platform == 'win32':
260         # Some Windows platforms might have a `uname` command that returns a
261         # non-standard string (e.g. gnuwin32 tools returns `windows32`). In
262         # these cases, fall back to using sys.platform.
263         return 'x86_64-pc-windows-msvc'
264     else:
265         err = "unknown OS type: {}".format(ostype)
266         sys.exit(err)
267
268     if cputype == 'powerpc' and ostype == 'unknown-freebsd':
269         cputype = subprocess.check_output(
270               ['uname', '-p']).strip().decode(default_encoding)
271     cputype_mapper = {
272         'BePC': 'i686',
273         'aarch64': 'aarch64',
274         'amd64': 'x86_64',
275         'arm64': 'aarch64',
276         'i386': 'i686',
277         'i486': 'i686',
278         'i686': 'i686',
279         'i786': 'i686',
280         'm68k': 'm68k',
281         'powerpc': 'powerpc',
282         'powerpc64': 'powerpc64',
283         'powerpc64le': 'powerpc64le',
284         'ppc': 'powerpc',
285         'ppc64': 'powerpc64',
286         'ppc64le': 'powerpc64le',
287         'riscv64': 'riscv64gc',
288         's390x': 's390x',
289         'x64': 'x86_64',
290         'x86': 'i686',
291         'x86-64': 'x86_64',
292         'x86_64': 'x86_64'
293     }
294
295     # Consider the direct transformation first and then the special cases
296     if cputype in cputype_mapper:
297         cputype = cputype_mapper[cputype]
298     elif cputype in {'xscale', 'arm'}:
299         cputype = 'arm'
300         if ostype == 'linux-android':
301             ostype = 'linux-androideabi'
302         elif ostype == 'unknown-freebsd':
303             cputype = subprocess.check_output(
304                 ['uname', '-p']).strip().decode(default_encoding)
305             ostype = 'unknown-freebsd'
306     elif cputype == 'armv6l':
307         cputype = 'arm'
308         if ostype == 'linux-android':
309             ostype = 'linux-androideabi'
310         else:
311             ostype += 'eabihf'
312     elif cputype in {'armv7l', 'armv8l'}:
313         cputype = 'armv7'
314         if ostype == 'linux-android':
315             ostype = 'linux-androideabi'
316         else:
317             ostype += 'eabihf'
318     elif cputype == 'mips':
319         if sys.byteorder == 'big':
320             cputype = 'mips'
321         elif sys.byteorder == 'little':
322             cputype = 'mipsel'
323         else:
324             raise ValueError("unknown byteorder: {}".format(sys.byteorder))
325     elif cputype == 'mips64':
326         if sys.byteorder == 'big':
327             cputype = 'mips64'
328         elif sys.byteorder == 'little':
329             cputype = 'mips64el'
330         else:
331             raise ValueError('unknown byteorder: {}'.format(sys.byteorder))
332         # only the n64 ABI is supported, indicate it
333         ostype += 'abi64'
334     elif cputype == 'sparc' or cputype == 'sparcv9' or cputype == 'sparc64':
335         pass
336     else:
337         err = "unknown cpu type: {}".format(cputype)
338         sys.exit(err)
339
340     return "{}-{}".format(cputype, ostype)
341
342
343 @contextlib.contextmanager
344 def output(filepath):
345     tmp = filepath + '.tmp'
346     with open(tmp, 'w') as f:
347         yield f
348     try:
349         if os.path.exists(filepath):
350             os.remove(filepath)  # PermissionError/OSError on Win32 if in use
351     except OSError:
352         shutil.copy2(tmp, filepath)
353         os.remove(tmp)
354         return
355     os.rename(tmp, filepath)
356
357
358 class Stage0Toolchain:
359     def __init__(self, stage0_payload):
360         self.date = stage0_payload["date"]
361         self.version = stage0_payload["version"]
362
363     def channel(self):
364         return self.version + "-" + self.date
365
366
367 class RustBuild(object):
368     """Provide all the methods required to build Rust"""
369     def __init__(self):
370         self.checksums_sha256 = {}
371         self.stage0_compiler = None
372         self.stage0_rustfmt = None
373         self._download_url = ''
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.stage0_compiler.version
398         bin_root = self.bin_root(stage0)
399
400         key = self.stage0_compiler.date
401         if not stage0:
402             key += str(self.rustc_commit)
403         if self.rustc(stage0).startswith(bin_root) and \
404                 (not os.path.exists(self.rustc(stage0)) or
405                  self.program_out_of_date(self.rustc_stamp(stage0), key)):
406             if os.path.exists(bin_root):
407                 shutil.rmtree(bin_root)
408             tarball_suffix = '.tar.xz' if support_xz() else '.tar.gz'
409             filename = "rust-std-{}-{}{}".format(
410                 rustc_channel, self.build, tarball_suffix)
411             pattern = "rust-std-{}".format(self.build)
412             self._download_component_helper(filename, pattern, tarball_suffix, stage0)
413             filename = "rustc-{}-{}{}".format(rustc_channel, self.build,
414                                               tarball_suffix)
415             self._download_component_helper(filename, "rustc", tarball_suffix, stage0)
416             # download-rustc doesn't need its own cargo, it can just use beta's.
417             if stage0:
418                 filename = "cargo-{}-{}{}".format(rustc_channel, self.build,
419                                                 tarball_suffix)
420                 self._download_component_helper(filename, "cargo", tarball_suffix)
421                 self.fix_bin_or_dylib("{}/bin/cargo".format(bin_root))
422             else:
423                 filename = "rustc-dev-{}-{}{}".format(rustc_channel, self.build, tarball_suffix)
424                 self._download_component_helper(
425                     filename, "rustc-dev", tarball_suffix, stage0
426                 )
427
428             self.fix_bin_or_dylib("{}/bin/rustc".format(bin_root))
429             self.fix_bin_or_dylib("{}/bin/rustdoc".format(bin_root))
430             lib_dir = "{}/lib".format(bin_root)
431             for lib in os.listdir(lib_dir):
432                 if lib.endswith(".so"):
433                     self.fix_bin_or_dylib(os.path.join(lib_dir, lib))
434             with output(self.rustc_stamp(stage0)) as rust_stamp:
435                 rust_stamp.write(key)
436
437         if self.rustfmt() and self.rustfmt().startswith(bin_root) and (
438             not os.path.exists(self.rustfmt())
439             or self.program_out_of_date(
440                 self.rustfmt_stamp(),
441                 "" if self.stage0_rustfmt is None else self.stage0_rustfmt.channel()
442             )
443         ):
444             if self.stage0_rustfmt is not None:
445                 tarball_suffix = '.tar.xz' if support_xz() else '.tar.gz'
446                 filename = "rustfmt-{}-{}{}".format(
447                     self.stage0_rustfmt.version, self.build, tarball_suffix,
448                 )
449                 self._download_component_helper(
450                     filename, "rustfmt-preview", tarball_suffix, key=self.stage0_rustfmt.date
451                 )
452                 self.fix_bin_or_dylib("{}/bin/rustfmt".format(bin_root))
453                 self.fix_bin_or_dylib("{}/bin/cargo-fmt".format(bin_root))
454                 with output(self.rustfmt_stamp()) as rustfmt_stamp:
455                     rustfmt_stamp.write(self.stage0_rustfmt.channel())
456
457         # Avoid downloading LLVM twice (once for stage0 and once for the master rustc)
458         if self.downloading_llvm() and stage0:
459             # We want the most recent LLVM submodule update to avoid downloading
460             # LLVM more often than necessary.
461             #
462             # This git command finds that commit SHA, looking for bors-authored
463             # commits that modified src/llvm-project or other relevant version
464             # stamp files.
465             #
466             # This works even in a repository that has not yet initialized
467             # submodules.
468             top_level = subprocess.check_output([
469                 "git", "rev-parse", "--show-toplevel",
470             ]).decode(sys.getdefaultencoding()).strip()
471             llvm_sha = subprocess.check_output([
472                 "git", "rev-list", "--author=bors@rust-lang.org", "-n1",
473                 "--first-parent", "HEAD",
474                 "--",
475                 "{}/src/llvm-project".format(top_level),
476                 "{}/src/bootstrap/download-ci-llvm-stamp".format(top_level),
477                 # the LLVM shared object file is named `LLVM-12-rust-{version}-nightly`
478                 "{}/src/version".format(top_level)
479             ]).decode(sys.getdefaultencoding()).strip()
480             llvm_assertions = self.get_toml('assertions', 'llvm') == 'true'
481             llvm_root = self.llvm_root()
482             llvm_lib = os.path.join(llvm_root, "lib")
483             if self.program_out_of_date(self.llvm_stamp(), llvm_sha + str(llvm_assertions)):
484                 self._download_ci_llvm(llvm_sha, llvm_assertions)
485                 for binary in ["llvm-config", "FileCheck"]:
486                     self.fix_bin_or_dylib(os.path.join(llvm_root, "bin", binary))
487                 for lib in os.listdir(llvm_lib):
488                     if lib.endswith(".so"):
489                         self.fix_bin_or_dylib(os.path.join(llvm_lib, lib))
490                 with output(self.llvm_stamp()) as llvm_stamp:
491                     llvm_stamp.write(llvm_sha + str(llvm_assertions))
492
493     def downloading_llvm(self):
494         opt = self.get_toml('download-ci-llvm', 'llvm')
495         # This is currently all tier 1 targets (since others may not have CI
496         # artifacts)
497         # https://doc.rust-lang.org/rustc/platform-support.html#tier-1
498         supported_platforms = [
499             "aarch64-unknown-linux-gnu",
500             "i686-pc-windows-gnu",
501             "i686-pc-windows-msvc",
502             "i686-unknown-linux-gnu",
503             "x86_64-unknown-linux-gnu",
504             "x86_64-apple-darwin",
505             "x86_64-pc-windows-gnu",
506             "x86_64-pc-windows-msvc",
507         ]
508         return opt == "true" \
509             or (opt == "if-available" and self.build in supported_platforms)
510
511     def _download_component_helper(
512         self, filename, pattern, tarball_suffix, stage0=True, key=None
513     ):
514         if key is None:
515             if stage0:
516                 key = self.stage0_compiler.date
517             else:
518                 key = self.rustc_commit
519         cache_dst = os.path.join(self.build_dir, "cache")
520         rustc_cache = os.path.join(cache_dst, key)
521         if not os.path.exists(rustc_cache):
522             os.makedirs(rustc_cache)
523
524         if stage0:
525             base = self._download_url
526             url = "dist/{}".format(key)
527         else:
528             base = "https://ci-artifacts.rust-lang.org"
529             url = "rustc-builds/{}".format(self.rustc_commit)
530         tarball = os.path.join(rustc_cache, filename)
531         if not os.path.exists(tarball):
532             get(
533                 base,
534                 "{}/{}".format(url, filename),
535                 tarball,
536                 self.checksums_sha256,
537                 verbose=self.verbose,
538                 do_verify=stage0,
539             )
540         unpack(tarball, tarball_suffix, self.bin_root(stage0), match=pattern, verbose=self.verbose)
541
542     def _download_ci_llvm(self, llvm_sha, llvm_assertions):
543         if not llvm_sha:
544             print("error: could not find commit hash for downloading LLVM")
545             print("help: maybe your repository history is too shallow?")
546             print("help: consider disabling `download-ci-llvm`")
547             print("help: or fetch enough history to include one upstream commit")
548             exit(1)
549         cache_prefix = "llvm-{}-{}".format(llvm_sha, llvm_assertions)
550         cache_dst = os.path.join(self.build_dir, "cache")
551         rustc_cache = os.path.join(cache_dst, cache_prefix)
552         if not os.path.exists(rustc_cache):
553             os.makedirs(rustc_cache)
554
555         base = "https://ci-artifacts.rust-lang.org"
556         url = "rustc-builds/{}".format(llvm_sha)
557         if llvm_assertions:
558             url = url.replace('rustc-builds', 'rustc-builds-alt')
559         # ci-artifacts are only stored as .xz, not .gz
560         if not support_xz():
561             print("error: XZ support is required to download LLVM")
562             print("help: consider disabling `download-ci-llvm` or using python3")
563             exit(1)
564         tarball_suffix = '.tar.xz'
565         filename = "rust-dev-nightly-" + self.build + tarball_suffix
566         tarball = os.path.join(rustc_cache, filename)
567         if not os.path.exists(tarball):
568             get(
569                 base,
570                 "{}/{}".format(url, filename),
571                 tarball,
572                 self.checksums_sha256,
573                 verbose=self.verbose,
574                 do_verify=False,
575             )
576         unpack(tarball, tarball_suffix, self.llvm_root(),
577                 match="rust-dev",
578                 verbose=self.verbose)
579
580     def fix_bin_or_dylib(self, fname):
581         """Modifies the interpreter section of 'fname' to fix the dynamic linker,
582         or the RPATH section, to fix the dynamic library search path
583
584         This method is only required on NixOS and uses the PatchELF utility to
585         change the interpreter/RPATH of ELF executables.
586
587         Please see https://nixos.org/patchelf.html for more information
588         """
589         default_encoding = sys.getdefaultencoding()
590         try:
591             ostype = subprocess.check_output(
592                 ['uname', '-s']).strip().decode(default_encoding)
593         except subprocess.CalledProcessError:
594             return
595         except OSError as reason:
596             if getattr(reason, 'winerror', None) is not None:
597                 return
598             raise reason
599
600         if ostype != "Linux":
601             return
602
603         # If the user has asked binaries to be patched for Nix, then
604         # don't check for NixOS or `/lib`, just continue to the patching.
605         if self.get_toml('patch-binaries-for-nix', 'build') != 'true':
606             # Use `/etc/os-release` instead of `/etc/NIXOS`.
607             # The latter one does not exist on NixOS when using tmpfs as root.
608             try:
609                 with open("/etc/os-release", "r") as f:
610                     if not any(line.strip() == "ID=nixos" for line in f):
611                         return
612             except FileNotFoundError:
613                 return
614             if os.path.exists("/lib"):
615                 return
616
617         # At this point we're pretty sure the user is running NixOS or
618         # using Nix
619         nix_os_msg = "info: you seem to be using Nix. Attempting to patch"
620         print(nix_os_msg, fname)
621
622         # Only build `.nix-deps` once.
623         nix_deps_dir = self.nix_deps_dir
624         if not nix_deps_dir:
625             # Run `nix-build` to "build" each dependency (which will likely reuse
626             # the existing `/nix/store` copy, or at most download a pre-built copy).
627             #
628             # Importantly, we create a gc-root called `.nix-deps` in the `build/`
629             # directory, but still reference the actual `/nix/store` path in the rpath
630             # as it makes it significantly more robust against changes to the location of
631             # the `.nix-deps` location.
632             #
633             # bintools: Needed for the path of `ld-linux.so` (via `nix-support/dynamic-linker`).
634             # zlib: Needed as a system dependency of `libLLVM-*.so`.
635             # patchelf: Needed for patching ELF binaries (see doc comment above).
636             nix_deps_dir = "{}/{}".format(self.build_dir, ".nix-deps")
637             nix_expr = '''
638             with (import <nixpkgs> {});
639             symlinkJoin {
640               name = "rust-stage0-dependencies";
641               paths = [
642                 zlib
643                 patchelf
644                 stdenv.cc.bintools
645               ];
646             }
647             '''
648             try:
649                 subprocess.check_output([
650                     "nix-build", "-E", nix_expr, "-o", nix_deps_dir,
651                 ])
652             except subprocess.CalledProcessError as reason:
653                 print("warning: failed to call nix-build:", reason)
654                 return
655             self.nix_deps_dir = nix_deps_dir
656
657         patchelf = "{}/bin/patchelf".format(nix_deps_dir)
658         rpath_entries = [
659             # Relative default, all binary and dynamic libraries we ship
660             # appear to have this (even when `../lib` is redundant).
661             "$ORIGIN/../lib",
662             os.path.join(os.path.realpath(nix_deps_dir), "lib")
663         ]
664         patchelf_args = ["--set-rpath", ":".join(rpath_entries)]
665         if not fname.endswith(".so"):
666             # Finally, set the corret .interp for binaries
667             with open("{}/nix-support/dynamic-linker".format(nix_deps_dir)) as dynamic_linker:
668                 patchelf_args += ["--set-interpreter", dynamic_linker.read().rstrip()]
669
670         try:
671             subprocess.check_output([patchelf] + patchelf_args + [fname])
672         except subprocess.CalledProcessError as reason:
673             print("warning: failed to call patchelf:", reason)
674             return
675
676     # If `download-rustc` is set, download the most recent commit with CI artifacts
677     def maybe_download_ci_toolchain(self):
678         # If `download-rustc` is not set, default to rebuilding.
679         download_rustc = self.get_toml("download-rustc", section="rust")
680         if download_rustc is None or download_rustc == "false":
681             return None
682         assert download_rustc == "true" or download_rustc == "if-unchanged", download_rustc
683
684         # Handle running from a directory other than the top level
685         rev_parse = ["git", "rev-parse", "--show-toplevel"]
686         top_level = subprocess.check_output(rev_parse, universal_newlines=True).strip()
687         compiler = "{}/compiler/".format(top_level)
688         library = "{}/library/".format(top_level)
689
690         # Look for a version to compare to based on the current commit.
691         # Only commits merged by bors will have CI artifacts.
692         merge_base = [
693             "git", "rev-list", "--author=bors@rust-lang.org", "-n1",
694             "--first-parent", "HEAD"
695         ]
696         commit = subprocess.check_output(merge_base, universal_newlines=True).strip()
697         if not commit:
698             print("error: could not find commit hash for downloading rustc")
699             print("help: maybe your repository history is too shallow?")
700             print("help: consider disabling `download-rustc`")
701             print("help: or fetch enough history to include one upstream commit")
702             exit(1)
703
704         # Warn if there were changes to the compiler or standard library since the ancestor commit.
705         status = subprocess.call(["git", "diff-index", "--quiet", commit, "--", compiler, library])
706         if status != 0:
707             if download_rustc == "if-unchanged":
708                 return None
709             print("warning: `download-rustc` is enabled, but there are changes to \
710                    compiler/ or library/")
711
712         if self.verbose:
713             print("using downloaded stage1 artifacts from CI (commit {})".format(commit))
714         self.rustc_commit = commit
715         # FIXME: support downloading artifacts from the beta channel
716         self.download_toolchain(False, "nightly")
717
718     def rustc_stamp(self, stage0):
719         """Return the path for .rustc-stamp at the given stage
720
721         >>> rb = RustBuild()
722         >>> rb.build_dir = "build"
723         >>> rb.rustc_stamp(True) == os.path.join("build", "stage0", ".rustc-stamp")
724         True
725         >>> rb.rustc_stamp(False) == os.path.join("build", "ci-rustc", ".rustc-stamp")
726         True
727         """
728         return os.path.join(self.bin_root(stage0), '.rustc-stamp')
729
730     def rustfmt_stamp(self):
731         """Return the path for .rustfmt-stamp
732
733         >>> rb = RustBuild()
734         >>> rb.build_dir = "build"
735         >>> rb.rustfmt_stamp() == os.path.join("build", "stage0", ".rustfmt-stamp")
736         True
737         """
738         return os.path.join(self.bin_root(True), '.rustfmt-stamp')
739
740     def llvm_stamp(self):
741         """Return the path for .rustfmt-stamp
742
743         >>> rb = RustBuild()
744         >>> rb.build_dir = "build"
745         >>> rb.llvm_stamp() == os.path.join("build", "ci-llvm", ".llvm-stamp")
746         True
747         """
748         return os.path.join(self.llvm_root(), '.llvm-stamp')
749
750
751     def program_out_of_date(self, stamp_path, key):
752         """Check if the given program stamp is out of date"""
753         if not os.path.exists(stamp_path) or self.clean:
754             return True
755         with open(stamp_path, 'r') as stamp:
756             return key != stamp.read()
757
758     def bin_root(self, stage0):
759         """Return the binary root directory for the given stage
760
761         >>> rb = RustBuild()
762         >>> rb.build_dir = "build"
763         >>> rb.bin_root(True) == os.path.join("build", "stage0")
764         True
765         >>> rb.bin_root(False) == os.path.join("build", "ci-rustc")
766         True
767
768         When the 'build' property is given should be a nested directory:
769
770         >>> rb.build = "devel"
771         >>> rb.bin_root(True) == os.path.join("build", "devel", "stage0")
772         True
773         """
774         if stage0:
775             subdir = "stage0"
776         else:
777             subdir = "ci-rustc"
778         return os.path.join(self.build_dir, self.build, subdir)
779
780     def llvm_root(self):
781         """Return the CI LLVM root directory
782
783         >>> rb = RustBuild()
784         >>> rb.build_dir = "build"
785         >>> rb.llvm_root() == os.path.join("build", "ci-llvm")
786         True
787
788         When the 'build' property is given should be a nested directory:
789
790         >>> rb.build = "devel"
791         >>> rb.llvm_root() == os.path.join("build", "devel", "ci-llvm")
792         True
793         """
794         return os.path.join(self.build_dir, self.build, "ci-llvm")
795
796     def get_toml(self, key, section=None):
797         """Returns the value of the given key in config.toml, otherwise returns None
798
799         >>> rb = RustBuild()
800         >>> rb.config_toml = 'key1 = "value1"\\nkey2 = "value2"'
801         >>> rb.get_toml("key2")
802         'value2'
803
804         If the key does not exists, the result is None:
805
806         >>> rb.get_toml("key3") is None
807         True
808
809         Optionally also matches the section the key appears in
810
811         >>> rb.config_toml = '[a]\\nkey = "value1"\\n[b]\\nkey = "value2"'
812         >>> rb.get_toml('key', 'a')
813         'value1'
814         >>> rb.get_toml('key', 'b')
815         'value2'
816         >>> rb.get_toml('key', 'c') is None
817         True
818
819         >>> rb.config_toml = 'key1 = true'
820         >>> rb.get_toml("key1")
821         'true'
822         """
823
824         cur_section = None
825         for line in self.config_toml.splitlines():
826             section_match = re.match(r'^\s*\[(.*)\]\s*$', line)
827             if section_match is not None:
828                 cur_section = section_match.group(1)
829
830             match = re.match(r'^{}\s*=(.*)$'.format(key), line)
831             if match is not None:
832                 value = match.group(1)
833                 if section is None or section == cur_section:
834                     return self.get_string(value) or value.strip()
835         return None
836
837     def cargo(self):
838         """Return config path for cargo"""
839         return self.program_config('cargo')
840
841     def rustc(self, stage0):
842         """Return config path for rustc"""
843         return self.program_config('rustc', stage0)
844
845     def rustfmt(self):
846         """Return config path for rustfmt"""
847         if self.stage0_rustfmt is None:
848             return None
849         return self.program_config('rustfmt')
850
851     def program_config(self, program, stage0=True):
852         """Return config path for the given program at the given stage
853
854         >>> rb = RustBuild()
855         >>> rb.config_toml = 'rustc = "rustc"\\n'
856         >>> rb.program_config('rustc')
857         'rustc'
858         >>> rb.config_toml = ''
859         >>> cargo_path = rb.program_config('cargo', True)
860         >>> cargo_path.rstrip(".exe") == os.path.join(rb.bin_root(True),
861         ... "bin", "cargo")
862         True
863         >>> cargo_path = rb.program_config('cargo', False)
864         >>> cargo_path.rstrip(".exe") == os.path.join(rb.bin_root(False),
865         ... "bin", "cargo")
866         True
867         """
868         config = self.get_toml(program)
869         if config:
870             return os.path.expanduser(config)
871         return os.path.join(self.bin_root(stage0), "bin", "{}{}".format(
872             program, self.exe_suffix()))
873
874     @staticmethod
875     def get_string(line):
876         """Return the value between double quotes
877
878         >>> RustBuild.get_string('    "devel"   ')
879         'devel'
880         >>> RustBuild.get_string("    'devel'   ")
881         'devel'
882         >>> RustBuild.get_string('devel') is None
883         True
884         >>> RustBuild.get_string('    "devel   ')
885         ''
886         """
887         start = line.find('"')
888         if start != -1:
889             end = start + 1 + line[start + 1:].find('"')
890             return line[start + 1:end]
891         start = line.find('\'')
892         if start != -1:
893             end = start + 1 + line[start + 1:].find('\'')
894             return line[start + 1:end]
895         return None
896
897     @staticmethod
898     def exe_suffix():
899         """Return a suffix for executables"""
900         if sys.platform == 'win32':
901             return '.exe'
902         return ''
903
904     def bootstrap_binary(self):
905         """Return the path of the bootstrap binary
906
907         >>> rb = RustBuild()
908         >>> rb.build_dir = "build"
909         >>> rb.bootstrap_binary() == os.path.join("build", "bootstrap",
910         ... "debug", "bootstrap")
911         True
912         """
913         return os.path.join(self.build_dir, "bootstrap", "debug", "bootstrap")
914
915     def build_bootstrap(self):
916         """Build bootstrap"""
917         build_dir = os.path.join(self.build_dir, "bootstrap")
918         if self.clean and os.path.exists(build_dir):
919             shutil.rmtree(build_dir)
920         env = os.environ.copy()
921         # `CARGO_BUILD_TARGET` breaks bootstrap build.
922         # See also: <https://github.com/rust-lang/rust/issues/70208>.
923         if "CARGO_BUILD_TARGET" in env:
924             del env["CARGO_BUILD_TARGET"]
925         env["CARGO_TARGET_DIR"] = build_dir
926         env["RUSTC"] = self.rustc(True)
927         env["LD_LIBRARY_PATH"] = os.path.join(self.bin_root(True), "lib") + \
928             (os.pathsep + env["LD_LIBRARY_PATH"]) \
929             if "LD_LIBRARY_PATH" in env else ""
930         env["DYLD_LIBRARY_PATH"] = os.path.join(self.bin_root(True), "lib") + \
931             (os.pathsep + env["DYLD_LIBRARY_PATH"]) \
932             if "DYLD_LIBRARY_PATH" in env else ""
933         env["LIBRARY_PATH"] = os.path.join(self.bin_root(True), "lib") + \
934             (os.pathsep + env["LIBRARY_PATH"]) \
935             if "LIBRARY_PATH" in env else ""
936
937         # preserve existing RUSTFLAGS
938         env.setdefault("RUSTFLAGS", "")
939         build_section = "target.{}".format(self.build)
940         target_features = []
941         if self.get_toml("crt-static", build_section) == "true":
942             target_features += ["+crt-static"]
943         elif self.get_toml("crt-static", build_section) == "false":
944             target_features += ["-crt-static"]
945         if target_features:
946             env["RUSTFLAGS"] += " -C target-feature=" + (",".join(target_features))
947         target_linker = self.get_toml("linker", build_section)
948         if target_linker is not None:
949             env["RUSTFLAGS"] += " -C linker=" + target_linker
950         env["RUSTFLAGS"] += " -Wrust_2018_idioms -Wunused_lifetimes"
951         env["RUSTFLAGS"] += " -Wsemicolon_in_expressions_from_macros"
952         if self.get_toml("deny-warnings", "rust") != "false":
953             env["RUSTFLAGS"] += " -Dwarnings"
954
955         env["PATH"] = os.path.join(self.bin_root(True), "bin") + \
956             os.pathsep + env["PATH"]
957         if not os.path.isfile(self.cargo()):
958             raise Exception("no cargo executable found at `{}`".format(
959                 self.cargo()))
960         args = [self.cargo(), "build", "--manifest-path",
961                 os.path.join(self.rust_root, "src/bootstrap/Cargo.toml")]
962         for _ in range(0, self.verbose):
963             args.append("--verbose")
964         if self.use_locked_deps:
965             args.append("--locked")
966         if self.use_vendored_sources:
967             args.append("--frozen")
968         run(args, env=env, verbose=self.verbose)
969
970     def build_triple(self):
971         """Build triple as in LLVM
972
973         Note that `default_build_triple` is moderately expensive,
974         so use `self.build` where possible.
975         """
976         config = self.get_toml('build')
977         if config:
978             return config
979         return default_build_triple(self.verbose)
980
981     def check_submodule(self, module, slow_submodules):
982         if not slow_submodules:
983             checked_out = subprocess.Popen(["git", "rev-parse", "HEAD"],
984                                            cwd=os.path.join(self.rust_root, module),
985                                            stdout=subprocess.PIPE)
986             return checked_out
987         else:
988             return None
989
990     def update_submodule(self, module, checked_out, recorded_submodules):
991         module_path = os.path.join(self.rust_root, module)
992
993         if checked_out is not None:
994             default_encoding = sys.getdefaultencoding()
995             checked_out = checked_out.communicate()[0].decode(default_encoding).strip()
996             if recorded_submodules[module] == checked_out:
997                 return
998
999         print("Updating submodule", module)
1000
1001         run(["git", "submodule", "-q", "sync", module],
1002             cwd=self.rust_root, verbose=self.verbose)
1003
1004         update_args = ["git", "submodule", "update", "--init", "--recursive", "--depth=1"]
1005         if self.git_version >= distutils.version.LooseVersion("2.11.0"):
1006             update_args.append("--progress")
1007         update_args.append(module)
1008         run(update_args, cwd=self.rust_root, verbose=self.verbose, exception=True)
1009
1010         run(["git", "reset", "-q", "--hard"],
1011             cwd=module_path, verbose=self.verbose)
1012         run(["git", "clean", "-qdfx"],
1013             cwd=module_path, verbose=self.verbose)
1014
1015     def update_submodules(self):
1016         """Update submodules"""
1017         if (not os.path.exists(os.path.join(self.rust_root, ".git"))) or \
1018                 self.get_toml('submodules') == "false":
1019             return
1020
1021         default_encoding = sys.getdefaultencoding()
1022
1023         # check the existence and version of 'git' command
1024         git_version_str = require(['git', '--version']).split()[2].decode(default_encoding)
1025         self.git_version = distutils.version.LooseVersion(git_version_str)
1026
1027         slow_submodules = self.get_toml('fast-submodules') == "false"
1028         start_time = time()
1029         if slow_submodules:
1030             print('Unconditionally updating submodules')
1031         else:
1032             print('Updating only changed submodules')
1033         default_encoding = sys.getdefaultencoding()
1034         # Only update submodules that are needed to build bootstrap.  These are needed because Cargo
1035         # currently requires everything in a workspace to be "locally present" when starting a
1036         # build, and will give a hard error if any Cargo.toml files are missing.
1037         # FIXME: Is there a way to avoid cloning these eagerly? Bootstrap itself doesn't need to
1038         #   share a workspace with any tools - maybe it could be excluded from the workspace?
1039         #   That will still require cloning the submodules the second you check the standard
1040         #   library, though...
1041         # FIXME: Is there a way to avoid hard-coding the submodules required?
1042         # WARNING: keep this in sync with the submodules hard-coded in bootstrap/lib.rs
1043         submodules = [
1044             "src/tools/rust-installer",
1045             "src/tools/cargo",
1046             "src/tools/rls",
1047             "src/tools/miri",
1048             "library/backtrace",
1049             "library/stdarch"
1050         ]
1051         filtered_submodules = []
1052         submodules_names = []
1053         for module in submodules:
1054             check = self.check_submodule(module, slow_submodules)
1055             filtered_submodules.append((module, check))
1056             submodules_names.append(module)
1057         recorded = subprocess.Popen(["git", "ls-tree", "HEAD"] + submodules_names,
1058                                     cwd=self.rust_root, stdout=subprocess.PIPE)
1059         recorded = recorded.communicate()[0].decode(default_encoding).strip().splitlines()
1060         # { filename: hash }
1061         recorded_submodules = {}
1062         for data in recorded:
1063             # [mode, kind, hash, filename]
1064             data = data.split()
1065             recorded_submodules[data[3]] = data[2]
1066         for module in filtered_submodules:
1067             self.update_submodule(module[0], module[1], recorded_submodules)
1068         print("Submodules updated in %.2f seconds" % (time() - start_time))
1069
1070     def set_dist_environment(self, url):
1071         """Set download URL for normal environment"""
1072         if 'RUSTUP_DIST_SERVER' in os.environ:
1073             self._download_url = os.environ['RUSTUP_DIST_SERVER']
1074         else:
1075             self._download_url = url
1076
1077     def check_vendored_status(self):
1078         """Check that vendoring is configured properly"""
1079         vendor_dir = os.path.join(self.rust_root, 'vendor')
1080         if 'SUDO_USER' in os.environ and not self.use_vendored_sources:
1081             if os.environ.get('USER') != os.environ['SUDO_USER']:
1082                 self.use_vendored_sources = True
1083                 print('info: looks like you are running this command under `sudo`')
1084                 print('      and so in order to preserve your $HOME this will now')
1085                 print('      use vendored sources by default.')
1086                 if not os.path.exists(vendor_dir):
1087                     print('error: vendoring required, but vendor directory does not exist.')
1088                     print('       Run `cargo vendor` without sudo to initialize the '
1089                           'vendor directory.')
1090                     raise Exception("{} not found".format(vendor_dir))
1091
1092         if self.use_vendored_sources:
1093             if not os.path.exists('.cargo'):
1094                 os.makedirs('.cargo')
1095             with output('.cargo/config') as cargo_config:
1096                 cargo_config.write(
1097                     "[source.crates-io]\n"
1098                     "replace-with = 'vendored-sources'\n"
1099                     "registry = 'https://example.com'\n"
1100                     "\n"
1101                     "[source.vendored-sources]\n"
1102                     "directory = '{}/vendor'\n"
1103                     .format(self.rust_root))
1104         else:
1105             if os.path.exists('.cargo'):
1106                 shutil.rmtree('.cargo')
1107
1108     def ensure_vendored(self):
1109         """Ensure that the vendored sources are available if needed"""
1110         vendor_dir = os.path.join(self.rust_root, 'vendor')
1111         # Note that this does not handle updating the vendored dependencies if
1112         # the rust git repository is updated. Normal development usually does
1113         # not use vendoring, so hopefully this isn't too much of a problem.
1114         if self.use_vendored_sources and not os.path.exists(vendor_dir):
1115             run([
1116                 self.cargo(),
1117                 "vendor",
1118                 "--sync=./src/tools/rust-analyzer/Cargo.toml",
1119                 "--sync=./compiler/rustc_codegen_cranelift/Cargo.toml",
1120             ], verbose=self.verbose, cwd=self.rust_root)
1121
1122
1123 def bootstrap(help_triggered):
1124     """Configure, fetch, build and run the initial bootstrap"""
1125
1126     # If the user is asking for help, let them know that the whole download-and-build
1127     # process has to happen before anything is printed out.
1128     if help_triggered:
1129         print("info: Downloading and building bootstrap before processing --help")
1130         print("      command. See src/bootstrap/README.md for help with common")
1131         print("      commands.")
1132
1133     parser = argparse.ArgumentParser(description='Build rust')
1134     parser.add_argument('--config')
1135     parser.add_argument('--build')
1136     parser.add_argument('--clean', action='store_true')
1137     parser.add_argument('-v', '--verbose', action='count', default=0)
1138
1139     args = [a for a in sys.argv if a != '-h' and a != '--help']
1140     args, _ = parser.parse_known_args(args)
1141
1142     # Configure initial bootstrap
1143     build = RustBuild()
1144     build.rust_root = os.path.abspath(os.path.join(__file__, '../../..'))
1145     build.verbose = args.verbose
1146     build.clean = args.clean
1147
1148     # Read from `RUST_BOOTSTRAP_CONFIG`, then `--config`, then fallback to `config.toml` (if it
1149     # exists).
1150     toml_path = os.getenv('RUST_BOOTSTRAP_CONFIG') or args.config
1151     if not toml_path and os.path.exists('config.toml'):
1152         toml_path = 'config.toml'
1153
1154     if toml_path:
1155         if not os.path.exists(toml_path):
1156             toml_path = os.path.join(build.rust_root, toml_path)
1157
1158         with open(toml_path) as config:
1159             build.config_toml = config.read()
1160
1161     profile = build.get_toml('profile')
1162     if profile is not None:
1163         include_file = 'config.{}.toml'.format(profile)
1164         include_dir = os.path.join(build.rust_root, 'src', 'bootstrap', 'defaults')
1165         include_path = os.path.join(include_dir, include_file)
1166         # HACK: This works because `build.get_toml()` returns the first match it finds for a
1167         # specific key, so appending our defaults at the end allows the user to override them
1168         with open(include_path) as included_toml:
1169             build.config_toml += os.linesep + included_toml.read()
1170
1171     config_verbose = build.get_toml('verbose', 'build')
1172     if config_verbose is not None:
1173         build.verbose = max(build.verbose, int(config_verbose))
1174
1175     build.use_vendored_sources = build.get_toml('vendor', 'build') == 'true'
1176
1177     build.use_locked_deps = build.get_toml('locked-deps', 'build') == 'true'
1178
1179     build.check_vendored_status()
1180
1181     build_dir = build.get_toml('build-dir', 'build') or 'build'
1182     build.build_dir = os.path.abspath(build_dir.replace("$ROOT", build.rust_root))
1183
1184     with open(os.path.join(build.rust_root, "src", "stage0.json")) as f:
1185         data = json.load(f)
1186     build.checksums_sha256 = data["checksums_sha256"]
1187     build.stage0_compiler = Stage0Toolchain(data["compiler"])
1188     if data.get("rustfmt") is not None:
1189         build.stage0_rustfmt = Stage0Toolchain(data["rustfmt"])
1190
1191     build.set_dist_environment(data["dist_server"])
1192
1193     build.build = args.build or build.build_triple()
1194     build.update_submodules()
1195
1196     # Fetch/build the bootstrap
1197     build.download_toolchain()
1198     # Download the master compiler if `download-rustc` is set
1199     build.maybe_download_ci_toolchain()
1200     sys.stdout.flush()
1201     build.ensure_vendored()
1202     build.build_bootstrap()
1203     sys.stdout.flush()
1204
1205     # Run the bootstrap
1206     args = [build.bootstrap_binary()]
1207     args.extend(sys.argv[1:])
1208     env = os.environ.copy()
1209     env["BOOTSTRAP_PARENT_ID"] = str(os.getpid())
1210     env["BOOTSTRAP_PYTHON"] = sys.executable
1211     env["BUILD_DIR"] = build.build_dir
1212     env["RUSTC_BOOTSTRAP"] = '1'
1213     if toml_path:
1214         env["BOOTSTRAP_CONFIG"] = toml_path
1215     if build.rustc_commit is not None:
1216         env["BOOTSTRAP_DOWNLOAD_RUSTC"] = '1'
1217     run(args, env=env, verbose=build.verbose, is_bootstrap=True)
1218
1219
1220 def main():
1221     """Entry point for the bootstrap process"""
1222     start_time = time()
1223
1224     # x.py help <cmd> ...
1225     if len(sys.argv) > 1 and sys.argv[1] == 'help':
1226         sys.argv = [sys.argv[0], '-h'] + sys.argv[2:]
1227
1228     help_triggered = (
1229         '-h' in sys.argv) or ('--help' in sys.argv) or (len(sys.argv) == 1)
1230     try:
1231         bootstrap(help_triggered)
1232         if not help_triggered:
1233             print("Build completed successfully in {}".format(
1234                 format_build_time(time() - start_time)))
1235     except (SystemExit, KeyboardInterrupt) as error:
1236         if hasattr(error, 'code') and isinstance(error.code, int):
1237             exit_code = error.code
1238         else:
1239             exit_code = 1
1240             print(error)
1241         if not help_triggered:
1242             print("Build completed unsuccessfully in {}".format(
1243                 format_build_time(time() - start_time)))
1244         sys.exit(exit_code)
1245
1246
1247 if __name__ == '__main__':
1248     main()