1 use std::collections::HashMap;
2 use std::env;
3 use std::ffi::OsString;
4 use std::fmt::Write as _;
5 use std::fs::File;
6 use std::io::{self, BufWriter, Read, Write};
7 use std::ops::Not;
8 use std::path::{Path, PathBuf};
9 use std::process::Command;
10
11 use cargo_metadata::{Metadata, MetadataCommand};
12 use serde::{Deserialize, Serialize};
13
14 pub use crate::arg::*;
15
show_error(msg: &impl std::fmt::Display) -> !16 pub fn show_error(msg: &impl std::fmt::Display) -> ! {
17 eprintln!("fatal error: {msg}");
18 std::process::exit(1)
19 }
20
21 macro_rules! show_error {
22 ($($tt:tt)*) => { crate::util::show_error(&format_args!($($tt)*)) };
23 }
24
25 /// The information to run a crate with the given environment.
26 #[derive(Clone, Serialize, Deserialize)]
27 pub struct CrateRunEnv {
28 /// The command-line arguments.
29 pub args: Vec<String>,
30 /// The environment.
31 pub env: Vec<(OsString, OsString)>,
32 /// The current working directory.
33 pub current_dir: OsString,
34 /// The contents passed via standard input.
35 pub stdin: Vec<u8>,
36 }
37
38 impl CrateRunEnv {
39 /// Gather all the information we need.
collect(args: impl Iterator<Item = String>, capture_stdin: bool) -> Self40 pub fn collect(args: impl Iterator<Item = String>, capture_stdin: bool) -> Self {
41 let args = args.collect();
42 let env = env::vars_os().collect();
43 let current_dir = env::current_dir().unwrap().into_os_string();
44
45 let mut stdin = Vec::new();
46 if capture_stdin {
47 std::io::stdin().lock().read_to_end(&mut stdin).expect("cannot read stdin");
48 }
49
50 CrateRunEnv { args, env, current_dir, stdin }
51 }
52 }
53
54 /// The information Miri needs to run a crate. Stored as JSON when the crate is "compiled".
55 #[derive(Serialize, Deserialize)]
56 pub enum CrateRunInfo {
57 /// Run it with the given environment.
58 RunWith(CrateRunEnv),
59 /// Skip it as Miri does not support interpreting such kind of crates.
60 SkipProcMacroTest,
61 }
62
63 impl CrateRunInfo {
store(&self, filename: &Path)64 pub fn store(&self, filename: &Path) {
65 let file = File::create(filename)
66 .unwrap_or_else(|_| show_error!("cannot create `{}`", filename.display()));
67 let file = BufWriter::new(file);
68 serde_json::ser::to_writer(file, self)
69 .unwrap_or_else(|_| show_error!("cannot write to `{}`", filename.display()));
70 }
71 }
72
73 #[derive(Clone, Debug)]
74 pub enum MiriCommand {
75 /// Our own special 'setup' command.
76 Setup,
77 /// A command to be forwarded to cargo.
78 Forward(String),
79 }
80
81 /// Escapes `s` in a way that is suitable for using it as a string literal in TOML syntax.
escape_for_toml(s: &str) -> String82 pub fn escape_for_toml(s: &str) -> String {
83 // We want to surround this string in quotes `"`. So we first escape all quotes,
84 // and also all backslashes (that are used to escape quotes).
85 let s = s.replace('\\', r#"\\"#).replace('"', r#"\""#);
86 format!("\"{s}\"")
87 }
88
89 /// Returns the path to the `miri` binary
find_miri() -> PathBuf90 pub fn find_miri() -> PathBuf {
91 if let Some(path) = env::var_os("MIRI") {
92 return path.into();
93 }
94 let mut path = std::env::current_exe().expect("current executable path invalid");
95 if cfg!(windows) {
96 path.set_file_name("miri.exe");
97 } else {
98 path.set_file_name("miri");
99 }
100 path
101 }
102
miri() -> Command103 pub fn miri() -> Command {
104 Command::new(find_miri())
105 }
106
miri_for_host() -> Command107 pub fn miri_for_host() -> Command {
108 let mut cmd = miri();
109 cmd.env("MIRI_BE_RUSTC", "host");
110 cmd
111 }
112
cargo() -> Command113 pub fn cargo() -> Command {
114 Command::new(env::var_os("CARGO").unwrap_or_else(|| OsString::from("cargo")))
115 }
116
117 /// Execute the `Command`, where possible by replacing the current process with a new process
118 /// described by the `Command`. Then exit this process with the exit code of the new process.
exec(mut cmd: Command) -> !119 pub fn exec(mut cmd: Command) -> ! {
120 // On non-Unix imitate POSIX exec as closely as we can
121 #[cfg(not(unix))]
122 {
123 let exit_status = cmd.status().expect("failed to run command");
124 std::process::exit(exit_status.code().unwrap_or(-1))
125 }
126 // On Unix targets, actually exec.
127 // If exec returns, process setup has failed. This is the same error condition as the expect in
128 // the non-Unix case.
129 #[cfg(unix)]
130 {
131 use std::os::unix::process::CommandExt;
132 let error = cmd.exec();
133 Err(error).expect("failed to run command")
134 }
135 }
136
137 /// Execute the `Command`, where possible by replacing the current process with a new process
138 /// described by the `Command`. Then exit this process with the exit code of the new process.
139 /// `input` is also piped to the new process's stdin, on cfg(unix) platforms by writing its
140 /// contents to `path` first, then setting stdin to that file.
exec_with_pipe<P>(mut cmd: Command, input: &[u8], path: P) -> ! where P: AsRef<Path>,141 pub fn exec_with_pipe<P>(mut cmd: Command, input: &[u8], path: P) -> !
142 where
143 P: AsRef<Path>,
144 {
145 #[cfg(unix)]
146 {
147 // Write the bytes we want to send to stdin out to a file
148 std::fs::write(&path, input).unwrap();
149 // Open the file for reading, and set our new stdin to it
150 let stdin = File::open(&path).unwrap();
151 cmd.stdin(stdin);
152 // Unlink the file so that it is fully cleaned up as soon as the new process exits
153 std::fs::remove_file(&path).unwrap();
154 // Finally, we can hand off control.
155 exec(cmd)
156 }
157 #[cfg(not(unix))]
158 {
159 drop(path); // We don't need the path, we can pipe the bytes directly
160 cmd.stdin(std::process::Stdio::piped());
161 let mut child = cmd.spawn().expect("failed to spawn process");
162 {
163 let stdin = child.stdin.as_mut().expect("failed to open stdin");
164 stdin.write_all(input).expect("failed to write out test source");
165 }
166 let exit_status = child.wait().expect("failed to run command");
167 std::process::exit(exit_status.code().unwrap_or(-1))
168 }
169 }
170
ask_to_run(mut cmd: Command, ask: bool, text: &str)171 pub fn ask_to_run(mut cmd: Command, ask: bool, text: &str) {
172 // Disable interactive prompts in CI (GitHub Actions, Travis, AppVeyor, etc).
173 // Azure doesn't set `CI` though (nothing to see here, just Microsoft being Microsoft),
174 // so we also check their `TF_BUILD`.
175 let is_ci = env::var_os("CI").is_some() || env::var_os("TF_BUILD").is_some();
176 if ask && !is_ci {
177 let mut buf = String::new();
178 print!("I will run `{cmd:?}` to {text}. Proceed? [Y/n] ");
179 io::stdout().flush().unwrap();
180 io::stdin().read_line(&mut buf).unwrap();
181 match buf.trim().to_lowercase().as_ref() {
182 // Proceed.
183 "" | "y" | "yes" => {}
184 "n" | "no" => show_error!("aborting as per your request"),
185 a => show_error!("invalid answer `{}`", a),
186 };
187 } else {
188 eprintln!("Running `{cmd:?}` to {text}.");
189 }
190
191 if cmd.status().unwrap_or_else(|_| panic!("failed to execute {cmd:?}")).success().not() {
192 show_error!("failed to {}", text);
193 }
194 }
195
196 // Computes the extra flags that need to be passed to cargo to make it behave like the current
197 // cargo invocation.
cargo_extra_flags() -> Vec<String>198 fn cargo_extra_flags() -> Vec<String> {
199 let mut flags = Vec::new();
200 // `-Zunstable-options` is required by `--config`.
201 flags.push("-Zunstable-options".to_string());
202
203 // Forward `--config` flags.
204 let config_flag = "--config";
205 for arg in get_arg_flag_values(config_flag) {
206 flags.push(config_flag.to_string());
207 flags.push(arg);
208 }
209
210 // Forward `--manifest-path`.
211 let manifest_flag = "--manifest-path";
212 if let Some(manifest) = get_arg_flag_value(manifest_flag) {
213 flags.push(manifest_flag.to_string());
214 flags.push(manifest);
215 }
216
217 // Forwarding `--target-dir` would make sense, but `cargo metadata` does not support that flag.
218
219 flags
220 }
221
get_cargo_metadata() -> Metadata222 pub fn get_cargo_metadata() -> Metadata {
223 // This will honor the `CARGO` env var the same way our `cargo()` does.
224 MetadataCommand::new().no_deps().other_options(cargo_extra_flags()).exec().unwrap()
225 }
226
227 /// Pulls all the crates in this workspace from the cargo metadata.
228 /// Workspace members are emitted like "miri 0.1.0 (path+file:///path/to/miri)"
229 /// Additionally, somewhere between cargo metadata and TyCtxt, '-' gets replaced with '_' so we
230 /// make that same transformation here.
local_crates(metadata: &Metadata) -> String231 pub fn local_crates(metadata: &Metadata) -> String {
232 assert!(!metadata.workspace_members.is_empty());
233 let mut local_crates = String::new();
234 for member in &metadata.workspace_members {
235 let name = member.repr.split(' ').next().unwrap();
236 let name = name.replace('-', "_");
237 local_crates.push_str(&name);
238 local_crates.push(',');
239 }
240 local_crates.pop(); // Remove the trailing ','
241
242 local_crates
243 }
244
env_vars_from_cmd(cmd: &Command) -> Vec<(String, String)>245 fn env_vars_from_cmd(cmd: &Command) -> Vec<(String, String)> {
246 let mut envs = HashMap::new();
247 for (key, value) in std::env::vars() {
248 envs.insert(key, value);
249 }
250 for (key, value) in cmd.get_envs() {
251 if let Some(value) = value {
252 envs.insert(key.to_string_lossy().to_string(), value.to_string_lossy().to_string());
253 } else {
254 envs.remove(&key.to_string_lossy().to_string());
255 }
256 }
257 let mut envs: Vec<_> = envs.into_iter().collect();
258 envs.sort();
259 envs
260 }
261
262 /// Debug-print a command that is going to be run.
debug_cmd(prefix: &str, verbose: usize, cmd: &Command)263 pub fn debug_cmd(prefix: &str, verbose: usize, cmd: &Command) {
264 if verbose == 0 {
265 return;
266 }
267 // We only do a single `eprintln!` call to minimize concurrency interactions.
268 let mut out = prefix.to_string();
269 writeln!(out, " running command: env \\").unwrap();
270 if verbose > 1 {
271 // Print the full environment this will be called in.
272 for (key, value) in env_vars_from_cmd(cmd) {
273 writeln!(out, "{key}={value:?} \\").unwrap();
274 }
275 } else {
276 // Print only what has been changed for this `cmd`.
277 for (var, val) in cmd.get_envs() {
278 if let Some(val) = val {
279 writeln!(out, "{}={val:?} \\", var.to_string_lossy()).unwrap();
280 } else {
281 writeln!(out, "--unset={}", var.to_string_lossy()).unwrap();
282 }
283 }
284 }
285 write!(out, "{cmd:?}").unwrap();
286 eprintln!("{out}");
287 }
288