Bug 1700051: part 30) Narrow scope of `newOffset`. r=smaug
[gecko.git] / build / moz.configure / rust.configure
blob3333a3735dae71a3dbd81d2626146aa0d08295a7
1 # -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
2 # vim: set filetype=python:
3 # This Source Code Form is subject to the terms of the Mozilla Public
4 # License, v. 2.0. If a copy of the MPL was not distributed with this
5 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
8 # Rust is required by `rust_compiler` below. We allow_missing here
9 # to propagate failures to the better error message there.
10 option(env="RUSTC", nargs=1, help="Path to the rust compiler")
11 option(env="CARGO", nargs=1, help="Path to the Cargo package manager")
13 rustc = check_prog(
14     "_RUSTC",
15     ["rustc"],
16     what="rustc",
17     paths=rust_search_path,
18     input="RUSTC",
19     allow_missing=True,
21 cargo = check_prog(
22     "_CARGO",
23     ["cargo"],
24     what="cargo",
25     paths=rust_search_path,
26     input="CARGO",
27     allow_missing=True,
31 @template
32 def unwrap_rustup(prog, name):
33     # rustc and cargo can either be rustup wrappers, or they can be the actual,
34     # plain executables. For cargo, on OSX, rustup sets DYLD_LIBRARY_PATH (at
35     # least until https://github.com/rust-lang/rustup.rs/pull/1752 is merged
36     # and shipped) and that can wreak havoc (see bug 1536486). Similarly, for
37     # rustc, rustup silently honors toolchain overrides set by vendored crates
38     # (see bug 1547196).
39     #
40     # In either case, we need to find the plain executables.
41     #
42     # To achieve that, try to run `PROG +stable`. When the rustup wrapper is in
43     # use, it either prints PROG's help and exits with status 0, or prints
44     # an error message (error: toolchain 'stable' is not installed) and exits
45     # with status 1. In the cargo case, when plain cargo is in use, it exits
46     # with a different error message (e.g. "error: no such subcommand:
47     # `+stable`"), and exits with status 101.
48     #
49     # Unfortunately, in the rustc case, when plain rustc is in use,
50     # `rustc +stable` will exit with status 1, complaining about a missing
51     # "+stable" file. We'll examine the error output to try and distinguish
52     # between failing rustup and failing rustc.
53     @depends(prog, dependable(name))
54     @imports(_from="__builtin__", _import="open")
55     @imports("os")
56     def unwrap(prog, name):
57         if not prog:
58             return
60         def from_rustup_which():
61             out = check_cmd_output("rustup", "which", name, executable=prog).rstrip()
62             # If for some reason the above failed to return something, keep the
63             # PROG we found originally.
64             if out:
65                 log.info("Actually using '%s'", out)
66                 return out
68             log.info("No `rustup which` output, using '%s'", prog)
69             return prog
71         (retcode, stdout, stderr) = get_cmd_output(prog, "+stable")
73         if name == "cargo" and retcode != 101:
74             prog = from_rustup_which()
75         elif name == "rustc":
76             if retcode == 0:
77                 prog = from_rustup_which()
78             elif "+stable" in stderr:
79                 # PROG looks like plain `rustc`.
80                 pass
81             else:
82                 # Assume PROG looks like `rustup`. This case is a little weird,
83                 # insofar as the user doesn't have the "stable" toolchain
84                 # installed, but go ahead and unwrap anyway: the user might
85                 # have only certain versions, beta, or nightly installed, and
86                 # we'll catch invalid versions later.
87                 prog = from_rustup_which()
89         return prog
91     return unwrap
94 rustc = unwrap_rustup(rustc, "rustc")
95 cargo = unwrap_rustup(cargo, "cargo")
98 set_config("CARGO", cargo)
99 set_config("RUSTC", rustc)
102 @depends_if(rustc)
103 @checking("rustc version", lambda info: info.version)
104 def rustc_info(rustc):
105     if not rustc:
106         return
107     out = check_cmd_output(rustc, "--version", "--verbose").splitlines()
108     info = dict((s.strip() for s in line.split(":", 1)) for line in out[1:])
109     return namespace(
110         version=Version(info.get("release", "0")),
111         commit=info.get("commit-hash", "unknown"),
112         host=info["host"],
113         llvm_version=Version(info.get("LLVM version", "0")),
114     )
117 set_config(
118     "RUSTC_VERSION",
119     depends(rustc_info)(lambda info: str(info.version) if info else None),
123 @depends_if(cargo)
124 @checking("cargo version", lambda info: info.version)
125 @imports("re")
126 def cargo_info(cargo):
127     if not cargo:
128         return
129     out = check_cmd_output(cargo, "--version", "--verbose").splitlines()
130     info = dict((s.strip() for s in line.split(":", 1)) for line in out[1:])
131     version = info.get("release")
132     # Older versions of cargo didn't support --verbose, in which case, they
133     # only output a not-really-pleasant-to-parse output. Fortunately, they
134     # don't error out, so we can just try some regexp matching on the output
135     # we already got.
136     if version is None:
137         VERSION_FORMAT = r"^cargo (\d\.\d+\.\d+).*"
139         m = re.search(VERSION_FORMAT, out[0])
140         # Fail fast if cargo changes its output on us.
141         if not m:
142             die("Could not determine cargo version from output: %s", out)
143         version = m.group(1)
145     return namespace(
146         version=Version(version),
147     )
150 @depends(rustc_info, cargo_info)
151 @imports(_from="mozboot.util", _import="MINIMUM_RUST_VERSION")
152 @imports(_from="textwrap", _import="dedent")
153 def rust_compiler(rustc_info, cargo_info):
154     if not rustc_info:
155         die(
156             dedent(
157                 """\
158         Rust compiler not found.
159         To compile rust language sources, you must have 'rustc' in your path.
160         See https://www.rust-lang.org/ for more information.
162         You can install rust by running './mach bootstrap'
163         or by directly running the installer from https://rustup.rs/
164         """
165             )
166         )
167     rustc_min_version = Version(MINIMUM_RUST_VERSION)
168     cargo_min_version = rustc_min_version
170     version = rustc_info.version
171     is_nightly = "nightly" in version.version
172     is_version_number_match = (
173         version.major == rustc_min_version.major
174         and version.minor == rustc_min_version.minor
175         and version.patch == rustc_min_version.patch
176     )
178     if version < rustc_min_version or (is_version_number_match and is_nightly):
179         die(
180             dedent(
181                 """\
182         Rust compiler {} is too old.
184         To compile Rust language sources please install at least
185         version {} of the 'rustc' toolchain (or, if using nightly,
186         at least one version newer than {}) and make sure it is
187         first in your path.
189         You can verify this by typing 'rustc --version'.
191         If you have the 'rustup' tool installed you can upgrade
192         to the latest release by typing 'rustup update'. The
193         installer is available from https://rustup.rs/
194         """.format(
195                     version, rustc_min_version, rustc_min_version
196                 )
197             )
198         )
200     if not cargo_info:
201         die(
202             dedent(
203                 """\
204         Cargo package manager not found.
205         To compile Rust language sources, you must have 'cargo' in your path.
206         See https://www.rust-lang.org/ for more information.
208         You can install cargo by running './mach bootstrap'
209         or by directly running the installer from https://rustup.rs/
210         """
211             )
212         )
214     version = cargo_info.version
215     if version < cargo_min_version:
216         die(
217             dedent(
218                 """\
219         Cargo package manager {} is too old.
221         To compile Rust language sources please install at least
222         version {} of 'cargo' and make sure it is first in your path.
224         You can verify this by typing 'cargo --version'.
225         """
226             ).format(version, cargo_min_version)
227         )
229     return True
232 @depends(rustc, when=rust_compiler)
233 @imports(_from="__builtin__", _import="ValueError")
234 def rust_supported_targets(rustc):
235     out = check_cmd_output(rustc, "--print", "target-list").splitlines()
236     data = {}
237     for t in out:
238         try:
239             info = split_triplet(t, allow_wasi=True)
240         except ValueError:
241             if t.startswith("thumb"):
242                 cpu, rest = t.split("-", 1)
243                 retry = "-".join(("arm", rest))
244             elif t.endswith("-windows-msvc"):
245                 retry = t[: -len("windows-msvc")] + "mingw32"
246             elif t.endswith("-windows-gnu"):
247                 retry = t[: -len("windows-gnu")] + "mingw32"
248             else:
249                 continue
250             try:
251                 info = split_triplet(retry, allow_wasi=True)
252             except ValueError:
253                 continue
254         key = (info.cpu, info.endianness, info.os)
255         data.setdefault(key, []).append(namespace(rust_target=t, target=info))
256     return data
259 def detect_rustc_target(
260     host_or_target, compiler_info, arm_target, rust_supported_targets
262     # Rust's --target options are similar to, but not exactly the same
263     # as, the autoconf-derived targets we use.  An example would be that
264     # Rust uses distinct target triples for targetting the GNU C++ ABI
265     # and the MSVC C++ ABI on Win32, whereas autoconf has a single
266     # triple and relies on the user to ensure that everything is
267     # compiled for the appropriate ABI.  We need to perform appropriate
268     # munging to get the correct option to rustc.
269     # We correlate the autoconf-derived targets with the list of targets
270     # rustc gives us with --print target-list.
271     candidates = rust_supported_targets.get(
272         (host_or_target.cpu, host_or_target.endianness, host_or_target.os), []
273     )
275     def find_candidate(candidates):
276         if len(candidates) == 1:
277             return candidates[0].rust_target
278         elif not candidates:
279             return None
281         # We have multiple candidates. There are two cases where we can try to
282         # narrow further down using extra information from the build system.
283         # - For windows targets, correlate with the C compiler type
284         if host_or_target.kernel == "WINNT":
285             if compiler_info.type in ("gcc", "clang"):
286                 suffix = "windows-gnu"
287             else:
288                 suffix = "windows-msvc"
289             narrowed = [
290                 c for c in candidates if c.rust_target.endswith("-{}".format(suffix))
291             ]
292             if len(narrowed) == 1:
293                 return narrowed[0].rust_target
294             elif narrowed:
295                 candidates = narrowed
297             vendor_aliases = {"pc": ("w64", "windows")}
298             narrowed = [
299                 c
300                 for c in candidates
301                 if host_or_target.vendor in vendor_aliases.get(c.target.vendor, ())
302             ]
304             if len(narrowed) == 1:
305                 return narrowed[0].rust_target
307         # - For arm targets, correlate with arm_target
308         #   we could be more thorough with the supported rust targets, but they
309         #   don't support OSes that are supported to build Gecko anyways.
310         #   Also, sadly, the only interface to check the rust target cpu features
311         #   is --print target-spec-json, and it's unstable, so we have to rely on
312         #   our own knowledge of what each arm target means.
313         if host_or_target.cpu == "arm" and host_or_target.endianness == "little":
314             prefixes = []
315             if arm_target.arm_arch >= 7:
316                 if arm_target.thumb2 and arm_target.fpu == "neon":
317                     prefixes.append("thumbv7neon")
318                 if arm_target.thumb2:
319                     prefixes.append("thumbv7a")
320                 prefixes.append("armv7")
321             if arm_target.arm_arch >= 6:
322                 prefixes.append("armv6")
323                 if host_or_target.os != "Android":
324                     # arm-* rust targets are armv6... except arm-linux-androideabi
325                     prefixes.append("arm")
326             if arm_target.arm_arch >= 5:
327                 prefixes.append("armv5te")
328                 if host_or_target.os == "Android":
329                     # arm-* rust targets are armv6... except arm-linux-androideabi
330                     prefixes.append("arm")
331             if arm_target.arm_arch >= 4:
332                 prefixes.append("armv4t")
333             # rust freebsd targets are the only ones that don't have a 'hf' suffix
334             # for hard-float. Technically, that means if the float abi ever is not
335             # hard-float, this will pick a wrong target, but since rust only
336             # supports hard-float, let's assume that means freebsd only support
337             # hard-float.
338             if arm_target.float_abi == "hard" and host_or_target.os != "FreeBSD":
339                 suffix = "hf"
340             else:
341                 suffix = ""
342             for p in prefixes:
343                 for c in candidates:
344                     if c.rust_target.startswith(
345                         "{}-".format(p)
346                     ) and c.rust_target.endswith(suffix):
347                         return c.rust_target
349         # See if we can narrow down on the exact alias
350         narrowed = [c for c in candidates if c.target.alias == host_or_target.alias]
351         if len(narrowed) == 1:
352             return narrowed[0].rust_target
353         elif narrowed:
354             candidates = narrowed
356         # See if we can narrow down with the raw OS
357         narrowed = [c for c in candidates if c.target.raw_os == host_or_target.raw_os]
358         if len(narrowed) == 1:
359             return narrowed[0].rust_target
360         elif narrowed:
361             candidates = narrowed
363         # See if we can narrow down with the raw OS and raw CPU
364         narrowed = [
365             c
366             for c in candidates
367             if c.target.raw_os == host_or_target.raw_os
368             and c.target.raw_cpu == host_or_target.raw_cpu
369         ]
370         if len(narrowed) == 1:
371             return narrowed[0].rust_target
373         # Finally, see if the vendor can be used to disambiguate.
374         narrowed = [c for c in candidates if c.target.vendor == host_or_target.vendor]
375         if len(narrowed) == 1:
376             return narrowed[0].rust_target
378         return None
380     rustc_target = find_candidate(candidates)
382     if rustc_target is None:
383         die("Don't know how to translate {} for rustc".format(host_or_target.alias))
385     return rustc_target
388 @imports("os")
389 @imports(_from="six", _import="ensure_binary")
390 @imports(_from="tempfile", _import="mkstemp")
391 @imports(_from="textwrap", _import="dedent")
392 @imports(_from="mozbuild.configure.util", _import="LineIO")
393 def assert_rust_compile(host_or_target, rustc_target, rustc):
394     # Check to see whether our rustc has a reasonably functional stdlib
395     # for our chosen target.
396     target_arg = "--target=" + rustc_target
397     in_fd, in_path = mkstemp(prefix="conftest", suffix=".rs", text=True)
398     out_fd, out_path = mkstemp(prefix="conftest", suffix=".rlib")
399     os.close(out_fd)
400     try:
401         source = 'pub extern fn hello() { println!("Hello world"); }'
402         log.debug("Creating `%s` with content:", in_path)
403         with LineIO(lambda l: log.debug("| %s", l)) as out:
404             out.write(source)
406         os.write(in_fd, ensure_binary(source))
407         os.close(in_fd)
409         cmd = [
410             rustc,
411             "--crate-type",
412             "staticlib",
413             target_arg,
414             "-o",
415             out_path,
416             in_path,
417         ]
419         def failed():
420             die(
421                 dedent(
422                     """\
423             Cannot compile for {} with {}
424             The target may be unsupported, or you may not have
425             a rust std library for that target installed. Try:
427               rustup target add {}
428             """.format(
429                         host_or_target.alias, rustc, rustc_target
430                     )
431                 )
432             )
434         check_cmd_output(*cmd, onerror=failed)
435         if not os.path.exists(out_path) or os.path.getsize(out_path) == 0:
436             failed()
437     finally:
438         os.remove(in_path)
439         os.remove(out_path)
442 @depends(
443     rustc,
444     host,
445     host_c_compiler,
446     rustc_info.host,
447     rust_supported_targets,
448     arm_target,
449     when=rust_compiler,
451 @checking("for rust host triplet")
452 @imports(_from="textwrap", _import="dedent")
453 def rust_host_triple(
454     rustc, host, compiler_info, rustc_host, rust_supported_targets, arm_target
456     rustc_target = detect_rustc_target(
457         host, compiler_info, arm_target, rust_supported_targets
458     )
459     if rustc_target != rustc_host:
460         if host.alias == rustc_target:
461             configure_host = host.alias
462         else:
463             configure_host = "{}/{}".format(host.alias, rustc_target)
464         die(
465             dedent(
466                 """\
467         The rust compiler host ({rustc}) is not suitable for the configure host ({configure}).
469         You can solve this by:
470         * Set your configure host to match the rust compiler host by editing your
471         mozconfig and adding "ac_add_options --host={rustc}".
472         * Or, install the rust toolchain for {configure}, if supported, by running
473         "rustup default stable-{rustc_target}"
474         """.format(
475                     rustc=rustc_host,
476                     configure=configure_host,
477                     rustc_target=rustc_target,
478                 )
479             )
480         )
481     assert_rust_compile(host, rustc_target, rustc)
482     return rustc_target
485 @depends(
486     rustc, target, c_compiler, rust_supported_targets, arm_target, when=rust_compiler
488 @checking("for rust target triplet")
489 def rust_target_triple(
490     rustc, target, compiler_info, rust_supported_targets, arm_target
492     rustc_target = detect_rustc_target(
493         target, compiler_info, arm_target, rust_supported_targets
494     )
495     assert_rust_compile(target, rustc_target, rustc)
496     return rustc_target
499 set_config("RUST_TARGET", rust_target_triple)
500 set_config("RUST_HOST_TARGET", rust_host_triple)
503 # This is used for putting source info into symbol files.
504 set_config("RUSTC_COMMIT", depends(rustc_info)(lambda i: i.commit))
506 # Rustdoc is required by Rust tests below.
507 option(env="RUSTDOC", nargs=1, help="Path to the rustdoc program")
509 rustdoc = check_prog(
510     "RUSTDOC",
511     ["rustdoc"],
512     paths=rust_search_path,
513     input="RUSTDOC",
514     allow_missing=True,
517 # This option is separate from --enable-tests because Rust tests are particularly
518 # expensive in terms of compile time (especially for code in libxul).
519 option(
520     "--enable-rust-tests",
521     help="Enable building and running of Rust tests during `make check`",
525 @depends("--enable-rust-tests", rustdoc)
526 def rust_tests(enable_rust_tests, rustdoc):
527     if enable_rust_tests and not rustdoc:
528         die("--enable-rust-tests requires rustdoc")
529     return bool(enable_rust_tests)
532 set_config("MOZ_RUST_TESTS", rust_tests)
535 @depends(target, c_compiler, rustc)
536 @imports("os")
537 def rustc_natvis_ldflags(target, compiler_info, rustc):
538     if target.kernel == "WINNT" and compiler_info.type == "clang-cl":
539         sysroot = check_cmd_output(rustc, "--print", "sysroot").strip()
540         etc = os.path.join(sysroot, "lib/rustlib/etc")
541         ldflags = []
542         if os.path.isdir(etc):
543             for f in os.listdir(etc):
544                 if f.endswith(".natvis"):
545                     ldflags.append("-NATVIS:" + normsep(os.path.join(etc, f)))
546         return ldflags
549 set_config("RUSTC_NATVIS_LDFLAGS", rustc_natvis_ldflags)
552 option(
553     "--enable-rust-debug",
554     default=depends(when="--enable-debug")(lambda: True),
555     help="{Build|Do not build} Rust code with debug assertions turned " "on.",
559 @depends(when="--enable-rust-debug")
560 def debug_rust():
561     return True
564 set_config("MOZ_DEBUG_RUST", debug_rust)
565 set_define("MOZ_DEBUG_RUST", debug_rust)
567 # ==============================================================
569 option(env="RUSTFLAGS", nargs=1, help="Rust compiler flags")
570 set_config("RUSTFLAGS", depends("RUSTFLAGS")(lambda flags: flags))
573 # Rust compiler flags
574 # ==============================================================
577 @depends(moz_optimize)
578 def rustc_opt_level_default(moz_optimize):
579     return "2" if moz_optimize.optimize else "0"
582 option(
583     env="RUSTC_OPT_LEVEL",
584     default=rustc_opt_level_default,
585     nargs=1,
586     help="Rust compiler optimization level (-C opt-level=%s)",
590 @depends("RUSTC_OPT_LEVEL")
591 def rustc_opt_level(opt_level_option):
592     return opt_level_option[0]
595 set_config("CARGO_PROFILE_RELEASE_OPT_LEVEL", rustc_opt_level)
596 set_config("CARGO_PROFILE_DEV_OPT_LEVEL", rustc_opt_level)
599 @depends(
600     rustc_opt_level,
601     debug_rust,
602     target,
603     "--enable-debug-symbols",
604     "--enable-frame-pointers",
606 def rust_compile_flags(opt_level, debug_rust, target, debug_symbols, frame_pointers):
607     # Cargo currently supports only two interesting profiles for building:
608     # development and release. Those map (roughly) to --enable-debug and
609     # --disable-debug in Gecko, respectively.
610     #
611     # But we'd also like to support an additional axis of control for
612     # optimization level. Since Cargo only supports 2 profiles, we're in
613     # a bit of a bind.
614     #
615     # Code here derives various compiler options given other configure options.
616     # The options defined here effectively override defaults specified in
617     # Cargo.toml files.
619     debug_assertions = None
620     debug_info = None
622     # opt-level=0 implies -C debug-assertions, which may not be desired
623     # unless Rust debugging is enabled.
624     if opt_level == "0" and not debug_rust:
625         debug_assertions = False
627     if debug_symbols:
628         debug_info = "2"
630     opts = []
632     if debug_assertions is not None:
633         opts.append("debug-assertions=%s" % ("yes" if debug_assertions else "no"))
634     if debug_info is not None:
635         opts.append("debuginfo=%s" % debug_info)
636     if frame_pointers:
637         opts.append("force-frame-pointers=yes")
638     # CFG for arm64 is crashy, see `def security_hardening_cflags`.
639     if target.kernel == "WINNT" and target.cpu != "aarch64":
640         opts.append("control-flow-guard=yes")
642     flags = []
643     for opt in opts:
644         flags.extend(["-C", opt])
646     return flags
649 # Rust incremental compilation
650 # ==============================================================
653 option("--disable-cargo-incremental", help="Disable incremental rust compilation.")
656 @depends(
657     rustc_opt_level,
658     debug_rust,
659     "MOZ_AUTOMATION",
660     code_coverage,
661     "--disable-cargo-incremental",
662     using_sccache,
663     "RUSTC_WRAPPER",
665 @imports("os")
666 def cargo_incremental(
667     opt_level,
668     debug_rust,
669     automation,
670     code_coverage,
671     enabled,
672     using_sccache,
673     rustc_wrapper,
675     """Return a value for the CARGO_INCREMENTAL environment variable."""
677     if not enabled:
678         return "0"
680     # We never want to use incremental compilation in automation.  sccache
681     # handles our automation use case much better than incremental compilation
682     # would.
683     if automation:
684         return "0"
686     # Coverage instrumentation doesn't play well with incremental compilation
687     # https://github.com/rust-lang/rust/issues/50203.
688     if code_coverage:
689         return "0"
691     # Incremental compilation doesn't work as well as it should, and if we're
692     # using sccache, it's better to use sccache than incremental compilation.
693     if not using_sccache and rustc_wrapper:
694         rustc_wrapper = os.path.basename(rustc_wrapper[0])
695         if os.path.splitext(rustc_wrapper)[0].lower() == "sccache":
696             using_sccache = True
697     if using_sccache:
698         return "0"
700     # Incremental compilation is automatically turned on for debug builds, so
701     # we don't need to do anything special here.
702     if debug_rust:
703         return
705     # --enable-release automatically sets -O2 for Rust code, and people can
706     # set RUSTC_OPT_LEVEL to 2 or even 3 if they want to profile Rust code.
707     # Let's assume that if Rust code is using -O2 or higher, we shouldn't
708     # be using incremental compilation, because we'd be imposing a
709     # significant runtime cost.
710     if opt_level not in ("0", "1"):
711         return
713     # We're clear to use incremental compilation!
714     return "1"
717 set_config("CARGO_INCREMENTAL", cargo_incremental)
720 @depends(rust_compile_flags, rust_warning_flags)
721 def rust_flags(compile_flags, warning_flags):
722     return compile_flags + warning_flags
725 set_config("MOZ_RUST_DEFAULT_FLAGS", rust_flags)