1#!/usr/bin/env vpython3 2 3# Copyright 2021 The Chromium Authors 4# Use of this source code is governed by a BSD-style license that can be 5# found in the LICENSE file. 6 7# This is a wrapper script which runs a Cargo build.rs build script 8# executable in a Cargo-like environment. Build scripts can do arbitrary 9# things and we can't support everything. Moreover, we do not WANT 10# to support everything because that means the build is not deterministic. 11# Code review processes must be applied to ensure that the build script 12# depends upon only these inputs: 13# 14# * The environment variables set by Cargo here: 15# https://doc.rust-lang.org/cargo/reference/environment-variables.html#environment-variables-cargo-sets-for-build-scripts 16# * Output from rustc commands, e.g. to figure out the Rust version. 17# 18# Similarly, the only allowable output from such a build script 19# is currently: 20# 21# * Generated .rs files 22# * cargo:rustc-cfg output. 23# 24# That's it. We don't even support the other standard cargo:rustc- 25# output messages. 26 27import argparse 28import io 29import os 30import platform 31import re 32import subprocess 33import sys 34import tempfile 35 36# Set up path to be able to import action_helpers 37sys.path.append( 38 os.path.join(os.path.dirname(os.path.abspath(__file__)), os.pardir, 39 os.pardir, 'build')) 40import action_helpers 41 42 43RUSTC_VERSION_LINE = re.compile(r"(\w+): (.*)") 44 45 46def rustc_name(): 47 if platform.system() == 'Windows': 48 return "rustc.exe" 49 else: 50 return "rustc" 51 52 53def host_triple(rustc_path): 54 """ Works out the host rustc target. """ 55 args = [rustc_path, "-vV"] 56 known_vars = dict() 57 proc = subprocess.Popen(args, stdout=subprocess.PIPE) 58 for line in io.TextIOWrapper(proc.stdout, encoding="utf-8"): 59 m = RUSTC_VERSION_LINE.match(line.rstrip()) 60 if m: 61 known_vars[m.group(1)] = m.group(2) 62 return known_vars["host"] 63 64 65def set_cargo_cfg_target_env_variables(rustc_path, env): 66 """ Sets CARGO_CFG_TARGET_... based on output from rustc. """ 67 target_triple = env["TARGET"] 68 assert target_triple 69 70 # TODO(lukasza): Check if command-line flags other `--target` may affect the 71 # output of `--print-cfg`. If so, then consider also passing extra `args` 72 # (derived from `rustflags` maybe?). 73 args = [rustc_path, "--print=cfg", f"--target={target_triple}"] 74 75 proc = subprocess.Popen(args, stdout=subprocess.PIPE) 76 for line in io.TextIOWrapper(proc.stdout, encoding="utf-8"): 77 line = line.strip() 78 if "=" not in line: continue 79 key, value = line.split("=") 80 if key.startswith("target_"): 81 key = "CARGO_CFG_" + key.upper() 82 value = value.strip('"') 83 if key in env: 84 env[key] = env[key] + f",{value}" 85 else: 86 env[key] = value 87 88 89# Before 1.77, the format was `cargo:rustc-cfg=`. As of 1.77 the format is now 90# `cargo::rustc-cfg=`. 91RUSTC_CFG_LINE = re.compile("cargo::?rustc-cfg=(.*)") 92 93 94def main(): 95 parser = argparse.ArgumentParser(description='Run Rust build script.') 96 parser.add_argument('--build-script', 97 required=True, 98 help='build script to run') 99 parser.add_argument('--output', 100 required=True, 101 help='where to write output rustc flags') 102 parser.add_argument('--target', help='rust target triple') 103 parser.add_argument('--target-abi', help='rust target_abi') 104 parser.add_argument('--features', help='features', nargs='+') 105 parser.add_argument('--env', help='environment variable', nargs='+') 106 parser.add_argument('--rustflags', 107 help=('path to a file of newline-separated command line ' 108 'flags for rustc')) 109 parser.add_argument('--rust-prefix', required=True, help='rust path prefix') 110 parser.add_argument('--generated-files', nargs='+', help='any generated file') 111 parser.add_argument('--out-dir', required=True, help='target out dir') 112 parser.add_argument('--src-dir', required=True, help='target source dir') 113 114 args = parser.parse_args() 115 116 rustc_path = os.path.join(args.rust_prefix, rustc_name()) 117 118 # We give the build script an OUT_DIR of a temporary directory, 119 # and copy out only any files which gn directives say that it 120 # should generate. Mostly this is to ensure we can atomically 121 # create those files, but it also serves to avoid side-effects 122 # from the build script. 123 # In the future, we could consider isolating this build script 124 # into a chroot jail or similar on some platforms, but ultimately 125 # we are always going to be reliant on code review to ensure the 126 # build script is deterministic and trustworthy, so this would 127 # really just be a backup to humans. 128 with tempfile.TemporaryDirectory() as tempdir: 129 env = {} # try to avoid build scripts depending on other things 130 env["RUSTC"] = os.path.abspath(rustc_path) 131 env["OUT_DIR"] = tempdir 132 env["CARGO_MANIFEST_DIR"] = os.path.abspath(args.src_dir) 133 env["HOST"] = host_triple(rustc_path) 134 if args.target is None: 135 env["TARGET"] = env["HOST"] 136 else: 137 env["TARGET"] = args.target 138 set_cargo_cfg_target_env_variables(rustc_path, env) 139 if args.features: 140 for f in args.features: 141 feature_name = f.upper().replace("-", "_") 142 env["CARGO_FEATURE_%s" % feature_name] = "1" 143 if args.rustflags: 144 with open(args.rustflags) as flags: 145 for flag in flags: 146 if "-Copt-level" in flag: 147 (_, opt) = flag.split("=") 148 env["OPT_LEVEL"] = opt.rstrip() 149 flags.seek(0) 150 env["CARGO_ENCODED_RUSTFLAGS"] = '\x1f'.join(flags.readlines()) 151 if args.env: 152 for e in args.env: 153 (k, v) = e.split("=") 154 env[k] = v 155 if "OPT_LEVEL" not in env: 156 env["OPT_LEVEL"] = "0" 157 158 # Pass through a couple which are useful for diagnostics 159 if os.environ.get("RUST_BACKTRACE"): 160 env["RUST_BACKTRACE"] = os.environ.get("RUST_BACKTRACE") 161 if os.environ.get("RUST_LOG"): 162 env["RUST_LOG"] = os.environ.get("RUST_LOG") 163 164 # In the future we should, set all the variables listed here: 165 # https://doc.rust-lang.org/cargo/reference/environment-variables.html#environment-variables-cargo-sets-for-build-scripts 166 167 proc = subprocess.run([os.path.abspath(args.build_script)], 168 env=env, 169 cwd=args.src_dir, 170 encoding='utf8', 171 stdout=subprocess.PIPE, 172 stderr=subprocess.PIPE) 173 174 if proc.stderr.rstrip(): 175 print(proc.stderr.rstrip(), file=sys.stderr) 176 proc.check_returncode() 177 178 flags = "" 179 for line in proc.stdout.split("\n"): 180 m = RUSTC_CFG_LINE.match(line.rstrip()) 181 if m: 182 flags = "%s--cfg\n%s\n" % (flags, m.group(1)) 183 184 # AtomicOutput will ensure we only write to the file on disk if what we 185 # give to write() is different than what's currently on disk. 186 with action_helpers.atomic_output(args.output) as output: 187 output.write(flags.encode("utf-8")) 188 189 # Copy any generated code out of the temporary directory, 190 # atomically. 191 if args.generated_files: 192 for generated_file in args.generated_files: 193 in_path = os.path.join(tempdir, generated_file) 194 out_path = os.path.join(args.out_dir, generated_file) 195 out_dir = os.path.dirname(out_path) 196 if not os.path.exists(out_dir): 197 os.makedirs(out_dir) 198 with open(in_path, 'rb') as input: 199 with action_helpers.atomic_output(out_path) as output: 200 content = input.read() 201 output.write(content) 202 203 204if __name__ == '__main__': 205 sys.exit(main()) 206