Skip to content

Commit d1c173c

Browse files
committed
Try to read RUSTFLAGS from configs, etc.
1 parent 174ad0e commit d1c173c

File tree

5 files changed

+168
-10
lines changed

5 files changed

+168
-10
lines changed

Cargo.lock

Lines changed: 32 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ tracing-appender = "0.2"
3939
tracing-subscriber = "0.3"
4040
walkdir = "2.3"
4141
whoami = "1.2"
42+
dirs = "4.0.0"
4243

4344
[dependencies.regex]
4445
version = "1.5"

DESIGN.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -202,3 +202,15 @@ The initial baseline build is done in a single job, with no parallelism at the c
202202
If the baseline test completes successfully, its build directory is copied to make a total of one per parallel job. Unlike the initial copy from the source directory, this includes the `target` directory, since it should now be up to date for the compiler options that cargo-mutants will use.
203203

204204
We then launch the appropriate number of threads, each of which has its own build directory. They each build and test new mutants until everything is done, or until there's an error that stops all processing.
205+
206+
## `RUSTFLAGS`
207+
208+
Cargo has a somewhat complex system for controlling flags passed to `rustc`. cargo-mutants uses this to pass `--cap-lints` to avoid failures due to strict lints.
209+
210+
However, for other settings, we want to keep using whatever flags the user has configured in their environment, in the source tree, or in their per-user configuration. Unfortunately the Cargo `RUSTFLAGS` or `CARGO_ENCODED_RUSTFLAGS` variables entirely replace, instead of appending to, the user's flags.
211+
212+
This seems to require us to reimplement some of Cargo's logic from <https://doc.rust-lang.org/cargo/reference/config.html#buildrustflags> to first determine the user's flags, and then append our own.
213+
214+
Reading the configuration in turn requires knowing the right target triple.
215+
216+
In testing this we should probably override `CARGO_HOME` to avoid picking up any user configuration.

src/cargo.rs

Lines changed: 116 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,9 @@ use std::sync::Arc;
88
use std::thread::sleep;
99
use std::time::{Duration, Instant};
1010

11-
use anyhow::bail;
1211
#[allow(unused_imports)]
13-
use anyhow::{anyhow, Context, Result};
14-
use camino::Utf8Path;
12+
use anyhow::{anyhow, bail, Context, Result};
13+
use camino::{Utf8Path, Utf8PathBuf};
1514
use globset::GlobSet;
1615
use serde_json::Value;
1716
#[allow(unused_imports)]
@@ -28,24 +27,26 @@ const WAIT_POLL_INTERVAL: Duration = Duration::from_millis(50);
2827

2928
/// Run one `cargo` subprocess, with a timeout, and with appropriate handling of interrupts.
3029
pub fn run_cargo(
30+
build_dir: &BuildDir,
3131
argv: &[String],
32-
in_dir: &Utf8Path,
3332
log_file: &mut LogFile,
3433
timeout: Duration,
3534
console: &Console,
35+
rustflags: &str,
3636
) -> Result<ProcessStatus> {
3737
let start = Instant::now();
3838

39-
// See <https://doc.rust-lang.org/cargo/reference/environment-variables.html>
40-
// <https://doc.rust-lang.org/rustc/lints/levels.html#capping-lints>
41-
//
4239
// The tests might use Insta <https://insta.rs>, and we don't want it to write
4340
// updates to the source tree, and we *certainly* don't want it to write
4441
// updates and then let the test pass.
4542

46-
let env = [("RUSTFLAGS", "--cap-lints=allow"), ("INSTA_UPDATE", "no")];
43+
let env = [
44+
("CARGO_ENCODED_RUSTFLAGS", rustflags),
45+
("INSTA_UPDATE", "no"),
46+
];
47+
debug!(?env);
4748

48-
let mut child = Process::start(argv, &env, in_dir, timeout, log_file)?;
49+
let mut child = Process::start(argv, &env, build_dir.path(), timeout, log_file)?;
4950

5051
let process_status = loop {
5152
if let Some(exit_status) = child.poll()? {
@@ -113,11 +114,45 @@ impl CargoSourceTree {
113114
.expect("cargo_toml_path has a parent")
114115
.to_owned();
115116
assert!(root.is_dir());
117+
116118
Ok(CargoSourceTree {
117119
root,
118120
cargo_toml_path,
119121
})
120122
}
123+
124+
/// Return appropriate CARGO_ENCODED_RUSTFLAGS for building this tree, including any changes to cap-lints.
125+
///
126+
/// See <https://doc.rust-lang.org/cargo/reference/environment-variables.html>
127+
/// <https://doc.rust-lang.org/rustc/lints/levels.html#capping-lints>
128+
pub fn rustflags(&self) -> String {
129+
let mut rustflags: Vec<String> =
130+
if let Some(rustflags) = env::var_os("CARGO_ENCODED_RUSTFLAGS") {
131+
rustflags
132+
.to_str()
133+
.expect("CARGO_ENCODED_RUSTFLAGS is not valid UTF-8")
134+
.split(|c| c == '\x1f')
135+
.map(|s| s.to_owned())
136+
.collect()
137+
} else if let Some(rustflags) = env::var_os("RUSTFLAGS") {
138+
rustflags
139+
.to_str()
140+
.expect("RUSTFLAGS is not valid UTF-8")
141+
.split(' ')
142+
.map(|s| s.to_owned())
143+
.collect()
144+
} else {
145+
// TODO: Determine the right target triple and profile?
146+
let config_paths = enclosing_config_files(&self.root);
147+
debug!("search config files {config_paths:?}");
148+
// TODO: All matching target.<triple>.rustflags and target.<cfg>.rustflags config entries joined together.
149+
// TODO: build.rustflags config value.
150+
Vec::new()
151+
};
152+
rustflags.push("--cap-lints=allow".to_owned());
153+
debug!("adjusted rustflags: {:?}", rustflags);
154+
rustflags.join("\x1f")
155+
}
121156
}
122157

123158
/// Run `cargo locate-project` to find the path of the `Cargo.toml` enclosing this path.
@@ -261,6 +296,58 @@ fn should_mutate_target(target: &cargo_metadata::Target) -> bool {
261296
target.kind.iter().any(|k| k.ends_with("lib") || k == "bin")
262297
}
263298

299+
/// Return a list of cargo config.toml files enclosing a directory, and in the
300+
/// cargo home directory.
301+
///
302+
/// Only actually existing files are returned.
303+
fn enclosing_config_files(path: &Utf8Path) -> Result<Vec<Utf8PathBuf>> {
304+
// https://doc.rust-lang.org/cargo/reference/config.html#hierarchical-structure
305+
// NOTE: The docs are ambiguous on what order the arrays are joined; but it
306+
// seems to make sense to put the most-specific (first-searched) one *last*
307+
// so that it can override earlier values.
308+
// TODO: Unit test this walking up some directory tree?
309+
let mut path = path.canonicalize_utf8().context("canonicalize path")?;
310+
let mut r: Vec<Utf8PathBuf> = Vec::new();
311+
loop {
312+
for suffix in &[".cargo/config.toml", ".cargo/config"] {
313+
let config_path = path.join(suffix);
314+
if config_path.exists() {
315+
r.push(config_path);
316+
break;
317+
}
318+
}
319+
if let Some(parent) = path.parent() {
320+
path = parent.to_owned();
321+
} else {
322+
break;
323+
}
324+
}
325+
if let Some(cargo_home) = cargo_home() {
326+
for filename in ["config.toml", "config"] {
327+
let config_path = cargo_home.join(filename);
328+
if config_path.exists() {
329+
if !r.contains(&config_path) {
330+
r.push(config_path);
331+
}
332+
break;
333+
}
334+
}
335+
}
336+
Ok(r)
337+
}
338+
339+
fn cargo_home() -> Option<Utf8PathBuf> {
340+
if let Some(home) = env::var_os("CARGO_HOME") {
341+
let home = home.to_str().expect("CARGO_HOME is not valid UTF-8");
342+
Some(Utf8PathBuf::from(home))
343+
} else if let Some(home) = dirs::home_dir() {
344+
let home: Utf8PathBuf = home.try_into().expect("home_dir is not valid UTF-8");
345+
Some(home.join(".cargo"))
346+
} else {
347+
None
348+
}
349+
}
350+
264351
#[cfg(test)]
265352
mod test {
266353
use std::ffi::OsStr;
@@ -366,4 +453,24 @@ mod test {
366453
assert!(path.join("src/bin/factorial.rs").is_file());
367454
assert_eq!(path.file_name().unwrap(), OsStr::new("factorial"));
368455
}
456+
457+
/// Either CARGO_HOME is set, or at least it can be found in HOME.
458+
#[test]
459+
fn cargo_home_is_found_in_test_environment() {
460+
assert!(super::cargo_home().is_some());
461+
}
462+
463+
/// In the common case where the source is inside HOME, we still don't get duplicated config paths.
464+
#[test]
465+
fn enclosing_config_files_has_no_duplicates() {
466+
let paths = enclosing_config_files("testdata/tree/small_well_tested".into()).unwrap();
467+
for i in 0..paths.len() {
468+
for j in (i + 1)..(paths.len()) {
469+
assert_ne!(
470+
paths[i], paths[j],
471+
"duplicate config file found in {paths:?}"
472+
);
473+
}
474+
}
475+
}
369476
}

src/lab.rs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,8 @@ pub fn test_unmutated_then_all_mutants(
3636
let output_dir = OutputDir::new(output_in_dir)?;
3737
console.set_debug_log(output_dir.open_debug_log()?);
3838

39+
let rustflags = source_tree.rustflags();
40+
3941
let mut mutants = source_tree.mutants(&options)?;
4042
if options.shuffle {
4143
mutants.shuffle(&mut rand::thread_rng());
@@ -63,6 +65,7 @@ pub fn test_unmutated_then_all_mutants(
6365
phases,
6466
options.test_timeout.unwrap_or(Duration::MAX),
6567
console,
68+
&rustflags,
6669
)?
6770
};
6871
if !baseline_outcome.success() {
@@ -128,6 +131,7 @@ pub fn test_unmutated_then_all_mutants(
128131
phases,
129132
mutated_test_timeout,
130133
console,
134+
&rustflags,
131135
)
132136
.expect("scenario test");
133137
} else {
@@ -163,6 +167,7 @@ fn test_scenario(
163167
phases: &[Phase],
164168
test_timeout: Duration,
165169
console: &Console,
170+
rustflags: &str,
166171
) -> Result<ScenarioOutcome> {
167172
let mut log_file = output_mutex
168173
.lock()
@@ -185,11 +190,12 @@ fn test_scenario(
185190
_ => Duration::MAX,
186191
};
187192
let cargo_result = run_cargo(
193+
&build_dir,
188194
&cargo_argv,
189-
build_dir.path(),
190195
&mut log_file,
191196
timeout,
192197
console,
198+
rustflags,
193199
)?;
194200
outcome.add_phase_result(phase, phase_start.elapsed(), cargo_result, &cargo_argv);
195201
console.scenario_phase_finished(scenario, phase);

0 commit comments

Comments
 (0)