miri-script refactor

This commit is contained in:
Ralf Jung 2023-07-30 17:28:23 +02:00
parent 7f2eca6a34
commit 3f952f4508
10 changed files with 589 additions and 650 deletions

View file

@ -130,7 +130,7 @@ jobs:
- name: clippy
run: ./miri clippy -- -D warnings
- name: rustdoc
run: RUSTDOCFLAGS="-Dwarnings" cargo doc --document-private-items
run: RUSTDOCFLAGS="-Dwarnings" ./miri cargo doc --document-private-items
# These jobs doesn't actually test anything, but they're only used to tell
# bors the build completed, as there is no practical way to detect when a

View file

@ -107,7 +107,7 @@ evaluation error was originally raised.
### UI testing
We use ui-testing in Miri, meaning we generate `.stderr` and `.stdout` files for the output
produced by Miri. You can use `./miri bless` to automatically (re)generate these files when
produced by Miri. You can use `./miri test --bless` to automatically (re)generate these files when
you add new tests or change how Miri presents certain output.
Note that when you also use `MIRIFLAGS` to change optimizations and similar, the ui output

View file

@ -538,8 +538,7 @@ pub fn phase_runner(mut binary_args: impl Iterator<Item = String>, phase: Runner
}
// Respect `MIRIFLAGS`.
if let Ok(a) = env::var("MIRIFLAGS") {
// This code is taken from `RUSTFLAGS` handling in cargo.
let args = a.split(' ').map(str::trim).filter(|s| !s.is_empty()).map(str::to_string);
let args = flagsplit(&a);
cmd.args(args);
}

View file

@ -114,6 +114,11 @@ pub fn cargo() -> Command {
Command::new(env::var_os("CARGO").unwrap_or_else(|| OsString::from("cargo")))
}
pub fn flagsplit(flags: &str) -> Vec<String> {
// This code is taken from `RUSTFLAGS` handling in cargo.
flags.split(' ').map(str::trim).filter(|s| !s.is_empty()).map(str::to_string).collect()
}
/// Execute the `Command`, where possible by replacing the current process with a new process
/// described by the `Command`. Then exit this process with the exit code of the new process.
pub fn exec(mut cmd: Command) -> ! {

View file

@ -2,5 +2,5 @@
set -e
# Instead of doing just `cargo run --manifest-path .. $@`, we invoke miri-script binary directly. Invoking `cargo run` goes through
# rustup (that sets it's own environmental variables), which is undesirable.
cargo build --manifest-path "$(dirname "$0")"/miri-script/Cargo.toml
"$(dirname "$0")"/miri-script/target/debug/miri-script $@
cargo build -q --manifest-path "$(dirname "$0")"/miri-script/Cargo.toml
"$(dirname "$0")"/miri-script/target/debug/miri-script "$@"

View file

@ -0,0 +1,4 @@
#!/bin/sh
# RA invokes `./miri cargo ...` for each workspace, so we need to forward that to the main `miri`
# script. See <https://github.com/rust-analyzer/rust-analyzer/issues/10793>.
exec "$(dirname "$0")"/../miri "$@"

View file

@ -1,104 +0,0 @@
use clap::{Parser, Subcommand};
use std::ffi::OsString;
#[derive(Parser, Clone, Debug)]
#[command(author, about, long_about = None)]
pub struct Cli {
#[command(subcommand)]
pub commands: Subcommands,
}
#[derive(Subcommand, Clone, Debug)]
pub enum Subcommands {
/// Installs the miri driver and cargo-miri.
/// Sets up the rpath such that the installed binary should work in any
/// working directory. Note that the binaries are placed in the `miri` toolchain
/// sysroot, to prevent conflicts with other toolchains.
Install {
/// Flags that are passed through to `cargo install`.
#[arg(trailing_var_arg = true, allow_hyphen_values = true)]
flags: Vec<OsString>,
},
/// Just build miri.
Build {
/// Flags that are passed through to `cargo build`.
#[arg(trailing_var_arg = true, allow_hyphen_values = true)]
flags: Vec<OsString>,
},
/// Just check miri.
Check {
/// Flags that are passed through to `cargo check`.
#[arg(trailing_var_arg = true, allow_hyphen_values = true)]
flags: Vec<OsString>,
},
/// Build miri, set up a sysroot and then run the test suite.
Test {
#[arg(long, default_value_t = false)]
bless: bool,
/// Flags that are passed through to `cargo test`.
#[arg(trailing_var_arg = true, allow_hyphen_values = true)]
flags: Vec<OsString>,
},
/// Build miri, set up a sysroot and then run the driver with the given <flags>.
/// (Also respects MIRIFLAGS environment variable.)
Run {
#[arg(long, default_value_t = false)]
dep: bool,
/// Flags that are passed through to `miri`
#[arg(trailing_var_arg = true, allow_hyphen_values = true)]
flags: Vec<OsString>,
},
/// Format all sources and tests.
Fmt {
/// Flags that are passed through to `rustfmt`.
#[arg(trailing_var_arg = true, allow_hyphen_values = true)]
flags: Vec<OsString>,
},
/// Runs clippy on all sources.
Clippy {
/// Flags that are passed through to `cargo clippy`.
#[arg(trailing_var_arg = true, allow_hyphen_values = true)]
flags: Vec<OsString>,
},
/// Runs just `cargo <flags>` with the Miri-specific environment variables.
/// Mainly meant to be invoked by rust-analyzer.
Cargo {
#[arg(trailing_var_arg = true, allow_hyphen_values = true)]
flags: Vec<OsString>,
},
/// Runs <command> over and over again with different seeds for Miri. The MIRIFLAGS
/// variable is set to its original value appended with ` -Zmiri-seed=$SEED` for
/// many different seeds.
ManySeeds {
/// Starting seed.
#[clap(long, env("MIRI_SEED_START"), default_value_t = 0)]
seed_start: u64,
#[clap(long, env("MIRI_SEEDS"), default_value_t = 256)]
/// Amount of seeds to try.
seeds: u64,
#[arg(trailing_var_arg = true, allow_hyphen_values = true)]
command: Vec<OsString>,
},
/// Runs the benchmarks from bench-cargo-miri in hyperfine. hyperfine needs to be installed.
Bench {
/// List of benchmarks to run. By default all benchmarks are run.
#[arg(trailing_var_arg = true, allow_hyphen_values = true)]
benches: Vec<OsString>,
},
/// Update and activate the rustup toolchain 'miri' to the commit given in the
/// `rust-version` file.
/// `rustup-toolchain-install-master` must be installed for this to work. Any extra
/// flags are passed to `rustup-toolchain-install-master`.
Toolchain {
#[arg(trailing_var_arg = true, allow_hyphen_values = true)]
flags: Vec<OsString>,
},
/// Pull and merge Miri changes from the rustc repo. Defaults to fetching the latest
/// rustc commit. The fetched commit is stored in the `rust-version` file, so the
/// next `./miri toolchain` will install the rustc that just got pulled.
RustcPull { commit: Option<String> },
/// Push Miri changes back to the rustc repo. This will pull a copy of the rustc
/// history into the Miri repo, unless you set the RUSTC_GIT env var to an existing
/// clone of the rustc repo.
RustcPush { github_user: String, branch: String },
}

View file

@ -1,307 +1,100 @@
use std::collections::BTreeMap;
use std::ffi::{OsStr, OsString};
use std::path::{Path, PathBuf};
use std::env;
use std::ffi::OsString;
use std::io::Write;
use std::ops::Not;
use anyhow::{anyhow, bail, Context, Result};
use dunce::canonicalize;
use path_macro::path;
use xshell::{cmd, Shell};
use walkdir::WalkDir;
use xshell::cmd;
use crate::arg::Subcommands;
use crate::util::*;
use crate::Command;
/// Used for rustc syncs.
const JOSH_FILTER: &str =
":rev(75dd959a3a40eb5b4574f8d2e23aa6efbeb33573:prefix=src/tools/miri):/src/tools/miri";
fn detect_miri_dir() -> std::io::Result<PathBuf> {
const MIRI_SCRIPT_ROOT_DIR: &str = env!("CARGO_MANIFEST_DIR");
Ok(canonicalize(MIRI_SCRIPT_ROOT_DIR)?.parent().unwrap().into())
}
/// Queries an active toolchain for `dir` via `rustup`.
fn get_active_toolchain(dir: &Path) -> Result<String> {
let sh = Shell::new()?;
sh.change_dir(dir);
let stdout = cmd!(sh, "rustup show active-toolchain").read()?;
Ok(stdout.split_whitespace().next().context("Could not obtain active Rust toolchain")?.into())
}
#[derive(Clone, Debug)]
pub(super) struct MiriRunner<'a> {
/// miri_dir is the root of the miri repository checkout we are working in.
miri_dir: PathBuf,
/// active_toolchain is passed as `+toolchain` argument to cargo/rustc invocations.
active_toolchain: String,
cargo_extra_flags: Vec<String>,
command: &'a super::Subcommands,
/// Environment variables passed to child processes.
env: BTreeMap<OsString, OsString>,
/// Additional variables used by environment-altering commands.
/// These should be accessed by corresponding methods (e.g. `sysroot()`) and not directly.
sysroot: Option<PathBuf>,
}
fn shell_with_parent_env() -> Result<Shell> {
let sh = Shell::new()?;
// xshell does not propagate parent's env variables by default.
for (k, v) in std::env::vars_os() {
sh.set_var(k, v);
}
Ok(sh)
}
impl MiriRunner<'_> {
pub(super) fn exec(command: &super::Subcommands) -> Result<()> {
Self::exec_inner(command, true)
}
fn exec_inner(command: &super::Subcommands, run_auto_things: bool) -> Result<()> {
let miri_dir = detect_miri_dir()?;
let active_toolchain = get_active_toolchain(&miri_dir)?;
let config = command.get_config(&miri_dir);
// CARGO_EXTRA_FLAGS do not have to be a valid UTF-8, but that's what shell_words' expects.
let cargo_extra_flags = std::env::var("CARGO_EXTRA_FLAGS").unwrap_or_default();
let cargo_extra_flags = shell_words::split(&cargo_extra_flags)?;
let env = BTreeMap::new();
let mut runner = MiriRunner {
miri_dir,
active_toolchain,
command,
env,
cargo_extra_flags,
sysroot: None,
};
if let Some(config) = config {
// Run the auto-things.
if run_auto_things {
if config.toolchain {
// Run this first, so that the toolchain doesn't change after
// other code has run.
let command = Subcommands::Toolchain { flags: vec![] };
Self::exec_inner(&command, false)?;
// Let's make sure to actually use that toolchain, too.
runner.active_toolchain = "miri".to_owned();
}
if config.fmt {
let command = Subcommands::Fmt { flags: vec![] };
Self::exec_inner(&command, false)?;
}
if config.clippy {
let command = Subcommands::Clippy {
flags: ["--", "-D", "warnings"].into_iter().map(OsString::from).collect(),
};
Self::exec_inner(&command, false)?;
}
}
// Prepare the environment
// Determine some toolchain properties
let libdir = runner.libdir()?;
if !libdir.exists() {
println!("Something went wrong determining the library dir.");
println!("I got {} but that does not exist.", libdir.display());
println!("Please report a bug at https://github.com/rust-lang/miri/issues.");
std::process::exit(2);
}
// Share target dir between `miri` and `cargo-miri`.
let target_dir = std::env::var_os("CARGO_TARGET_DIR")
.filter(|val| !val.is_empty())
.unwrap_or_else(|| {
let target_dir = path!(runner.miri_dir / "target");
target_dir.into()
});
runner.set_env("CARGO_TARGET_DIR", target_dir);
// We configure dev builds to not be unusably slow.
let devel_opt_level = std::env::var_os("CARGO_PROFILE_DEV_OPT_LEVEL")
.filter(|val| !val.is_empty())
.unwrap_or_else(|| "2".into());
runner.set_env("CARGO_PROFILE_DEV_OPT_LEVEL", devel_opt_level);
let rustflags = {
let env = std::env::var_os("RUSTFLAGS");
let mut flags_with_warnings = OsString::from(
"-Zunstable-options -Wrustc::internal -Wrust_2018_idioms -Wunused_lifetimes -Wsemicolon_in_expressions_from_macros ",
);
if let Some(value) = env {
flags_with_warnings.push(value);
}
// We set the rpath so that Miri finds the private rustc libraries it needs.
let mut flags_with_compiler_settings = OsString::from("-C link-args=-Wl,-rpath,");
flags_with_compiler_settings.push(&libdir);
flags_with_compiler_settings.push(flags_with_warnings);
flags_with_compiler_settings
};
runner.set_env("RUSTFLAGS", rustflags);
}
runner.execute()
}
fn execute(&mut self) -> Result<()> {
// Run command.
match self.command {
Subcommands::Install { flags } => self.install(flags),
Subcommands::Build { flags } => self.build(flags),
Subcommands::Check { flags } => self.check(flags),
Subcommands::Test { bless, flags } => self.test(*bless, flags),
Subcommands::Run { dep, flags } => self.run(*dep, flags),
Subcommands::Fmt { flags } => self.fmt(flags),
Subcommands::Clippy { flags } => self.clippy(flags),
Subcommands::Cargo { flags } => self.cargo(flags),
Subcommands::ManySeeds { command, seed_start, seeds } =>
self.many_seeds(command, *seed_start, *seeds),
Subcommands::Bench { benches } => self.bench(benches),
Subcommands::Toolchain { flags } => self.toolchain(flags),
Subcommands::RustcPull { commit } => self.rustc_pull(commit.clone()),
Subcommands::RustcPush { github_user, branch } => self.rustc_push(github_user, branch),
}
}
fn set_env(
&mut self,
key: impl Into<OsString>,
value: impl Into<OsString>,
) -> Option<OsString> {
self.env.insert(key.into(), value.into())
}
/// Prepare and set MIRI_SYSROOT. Respects `MIRI_TEST_TARGET` and takes into account
/// locally built vs. distributed rustc.
fn find_miri_sysroot(&mut self) -> Result<()> {
let current_sysroot = std::env::var_os("MIRI_SYSROOT").unwrap_or_default();
if !current_sysroot.is_empty() {
impl MiriEnv {
fn build_miri_sysroot(&mut self) -> Result<()> {
if self.sh.var("MIRI_SYSROOT").is_ok() {
// Sysroot already set, use that.
let current_value = self.set_env("MIRI_SYSROOT", &current_sysroot);
assert!(current_value.is_none() || current_value.unwrap() == current_sysroot);
return Ok(());
}
// We need to build a sysroot.
let target = std::env::var_os("MIRI_TEST_TARGET").filter(|target| !target.is_empty());
let sysroot = self.build_miri_sysroot(target.as_deref())?;
self.set_env("MIRI_SYSROOT", sysroot);
Ok(())
}
/// Build a sysroot and set MIRI_SYSROOT to use it. Arguments are passed to `cargo miri setup`.
fn build_miri_sysroot(&self, target: Option<&OsStr>) -> Result<String> {
let manifest_path = path!(self.miri_dir / "cargo-miri" / "Cargo.toml");
let Self { active_toolchain, cargo_extra_flags, .. } = &self;
let target_prefix: Option<&OsStr> = target.map(|_| "--target".as_ref());
let sh = self.shell()?;
let output = cmd!(sh, "cargo +{active_toolchain} --quiet run {cargo_extra_flags...} --manifest-path {manifest_path} -- miri setup --print-sysroot {target_prefix...} {target...}").read();
if output.is_err() {
// Run it again (without `--print-sysroot`) so the user can see the error.
cmd!(sh, "cargo +{active_toolchain} --quiet run {cargo_extra_flags...} --manifest-path {manifest_path} -- miri setup {target_prefix...} {target...}").run().with_context(|| "`cargo miri setup` failed")?;
}
Ok(output?)
}
fn build_package(
// Path to Cargo.toml file of a package to build.
path: &OsStr,
toolchain: impl AsRef<OsStr>,
extra_flags: &[String],
args: impl IntoIterator<Item = impl AsRef<OsStr>>,
) -> Result<()> {
let sh = Shell::new()?;
cmd!(sh, "cargo +{toolchain} build {extra_flags...} --manifest-path {path} {args...}")
.run()?;
Ok(())
}
fn shell(&self) -> Result<Shell> {
let sh = shell_with_parent_env()?;
for (k, v) in &self.env {
sh.set_var(k, v);
}
Ok(sh)
}
fn libdir(&self) -> Result<PathBuf> {
let sh = shell_with_parent_env()?;
let toolchain = &self.active_toolchain;
let target_output = cmd!(sh, "rustc +{toolchain} --version --verbose").read()?;
let rustc_meta = rustc_version::version_meta_for(&target_output)?;
let target = rustc_meta.host;
let sysroot = cmd!(sh, "rustc +{toolchain} --print sysroot").read()?;
let sysroot = PathBuf::from(sysroot);
let libdir = path!(sysroot / "lib" / "rustlib" / target / "lib");
Ok(libdir)
}
fn sysroot(&mut self) -> Result<PathBuf> {
if let Some(sysroot) = self.sysroot.as_ref() {
Ok(sysroot.clone())
} else {
let sh = shell_with_parent_env()?;
let toolchain = &self.active_toolchain;
let sysroot: PathBuf = cmd!(sh, "rustc +{toolchain} --print sysroot").read()?.into();
self.sysroot = Some(sysroot.clone());
Ok(sysroot)
}
}
fn install_to_dir(
&mut self,
sh: &Shell,
path: PathBuf,
args: impl IntoIterator<Item = impl AsRef<OsStr>>,
) -> Result<()> {
let sysroot = self.sysroot()?;
let toolchain = &self.active_toolchain;
let extra_flags = &self.cargo_extra_flags;
// "--locked" to respect the Cargo.lock file if it exists.
// Install binaries to the miri toolchain's sysroot so they do not interact with other toolchains.
cmd!(sh, "cargo +{toolchain} install {extra_flags...} --path {path} --force --root {sysroot} {args...}").run()?;
let Self { toolchain, cargo_extra_flags, .. } = &self;
let target = &match self.sh.var("MIRI_TEST_TARGET") {
Ok(target) => vec!["--target".into(), target],
Err(_) => vec![],
};
let output = cmd!(self.sh,
"cargo +{toolchain} --quiet run {cargo_extra_flags...} --manifest-path {manifest_path} --
miri setup --print-sysroot {target...}"
).read();
let Ok(output) = output else {
// Run it again (without `--print-sysroot` or `--quiet`) so the user can see the error.
cmd!(
self.sh,
"cargo +{toolchain} run {cargo_extra_flags...} --manifest-path {manifest_path} --
miri setup {target...}"
)
.run()
.with_context(|| "`cargo miri setup` failed")?;
panic!("`cargo miri setup` didn't fail again the 2nd time?");
};
self.sh.set_var("MIRI_SYSROOT", output);
Ok(())
}
}
impl MiriRunner<'_> {
fn bench(&self, benches: &[OsString]) -> Result<()> {
// The hyperfine to use
let hyperfine = std::env::var("HYPERFINE");
let hyperfine = hyperfine.as_deref().unwrap_or("hyperfine -w 1 -m 5 --shell=none");
let hyperfine = shell_words::split(hyperfine).unwrap();
let Some((program_name, args)) = hyperfine.split_first() else {
bail!("Expected HYPERFINE environment variable to be non-empty");
};
// Make sure we have an up-to-date Miri installed
Self::exec_inner(&Subcommands::Install { flags: vec![] }, false)?;
let benches_dir = path!(self.miri_dir / "bench-cargo-miri");
let benches = if benches.is_empty() {
std::fs::read_dir(&benches_dir)?
.filter_map(|path| {
path.ok()
.filter(|dir| dir.file_type().map(|t| t.is_dir()).unwrap_or(false))
.map(|p| p.file_name())
})
.collect()
} else {
benches.to_owned()
};
let sh = shell_with_parent_env()?;
let toolchain = &self.active_toolchain;
// Run the requested benchmarks
for bench in benches {
let current_bench_dir = path!(benches_dir / bench / "Cargo.toml");
cmd!(
sh,
"{program_name} {args...} 'cargo +'{toolchain}' miri run --manifest-path \"'{current_bench_dir}'\"'"
)
.run()?;
impl Command {
fn auto_actions() -> Result<()> {
let miri_dir = miri_dir()?;
let auto_everything = path!(miri_dir / ".auto_everything").exists();
let auto_toolchain = auto_everything || path!(miri_dir / ".auto-toolchain").exists();
let auto_fmt = auto_everything || path!(miri_dir / ".auto-fmt").exists();
let auto_clippy = auto_everything || path!(miri_dir / ".auto-clippy").exists();
// `toolchain` goes first as it could affect the others
if auto_toolchain {
Self::toolchain(vec![])?;
}
if auto_fmt {
Self::fmt(vec![])?;
}
if auto_clippy {
Self::clippy(vec![])?;
}
Ok(())
}
fn toolchain(&self, flags: &[OsString]) -> Result<()> {
pub fn exec(self) -> Result<()> {
match self {
Command::Install { flags } => Self::install(flags),
Command::Build { flags } => Self::build(flags),
Command::Check { flags } => Self::check(flags),
Command::Test { bless, flags } => Self::test(bless, flags),
Command::Run { dep, flags } => Self::run(dep, flags),
Command::Fmt { flags } => Self::fmt(flags),
Command::Clippy { flags } => Self::clippy(flags),
Command::Cargo { flags } => Self::cargo(flags),
Command::ManySeeds { command, seed_start, seeds } =>
Self::many_seeds(command, seed_start, seeds),
Command::Bench { benches } => Self::bench(benches),
Command::Toolchain { flags } => Self::toolchain(flags),
Command::RustcPull { commit } => Self::rustc_pull(commit.clone()),
Command::RustcPush { rustc_git, github_user, branch } =>
Self::rustc_push(rustc_git, github_user, branch),
}
}
fn toolchain(flags: Vec<OsString>) -> Result<()> {
// Make sure rustup-toolchain-install-master is installed.
which::which("rustup-toolchain-install-master").context("Please install rustup-toolchain-install-master by running 'cargo install rustup-toolchain-install-master'")?;
let sh = shell_with_parent_env()?;
sh.change_dir(&self.miri_dir);
which::which("rustup-toolchain-install-master")
.context("Please install rustup-toolchain-install-master by running 'cargo install rustup-toolchain-install-master'")?;
let sh = shell()?;
sh.change_dir(miri_dir()?);
let new_commit = Some(sh.read_file("rust-version")?.trim().to_owned());
let current_commit = {
let rustc_info = cmd!(sh, "rustc +miri --version -v").read();
@ -318,8 +111,9 @@ impl MiriRunner<'_> {
};
// Check if we already are at that commit.
if current_commit == new_commit {
println!("miri toolchain is already at commit {}.", current_commit.unwrap());
cmd!(sh, "rustup override set miri").run()?;
if active_toolchain()? != "miri" {
cmd!(sh, "rustup override set miri").run()?;
}
return Ok(());
}
// Install and setup new toolchain.
@ -337,10 +131,10 @@ impl MiriRunner<'_> {
Ok(())
}
fn rustc_pull(&self, commit: Option<String>) -> Result<()> {
let sh = shell_with_parent_env()?;
sh.change_dir(&self.miri_dir);
let commit: String = commit.map(Result::Ok).unwrap_or_else(|| {
fn rustc_pull(commit: Option<String>) -> Result<()> {
let sh = shell()?;
sh.change_dir(miri_dir()?);
let commit = commit.map(Result::Ok).unwrap_or_else(|| {
let rust_repo_head =
cmd!(sh, "git ls-remote https://github.com/rust-lang/rust/ HEAD").read()?;
rust_repo_head
@ -349,85 +143,109 @@ impl MiriRunner<'_> {
.map(|front| front.trim().to_owned())
.ok_or_else(|| anyhow!("Could not obtain Rust repo HEAD from remote."))
})?;
// Make sure the repo is clean.
if cmd!(sh, "git status --untracked-files=no --porcelain").read()?.is_empty().not() {
bail!("working directory must be clean before running `./miri rustc-pull`");
}
// Update rust-version file. As a separate commit, since making it part of
// the merge has confused the heck out of josh in the past.
sh.write_file(path!(self.miri_dir / "rust-version"), &commit)?;
// We pass `--no-verify` to avoid running git hooks like `./miri fmt` that could in turn
// trigger auto-actions.
sh.write_file("rust-version", &commit)?;
const PREPARING_COMMIT_MESSAGE: &str = "Preparing for merge from rustc";
cmd!(sh, "git commit rust-version -m {PREPARING_COMMIT_MESSAGE}")
cmd!(sh, "git commit rust-version --no-verify -m {PREPARING_COMMIT_MESSAGE}")
.run()
.context("FAILED to commit rust-version file, something went wrong")?;
// Fetch given rustc commit and note down which one that was
// Fetch given rustc commit.
cmd!(sh, "git fetch http://localhost:8000/rust-lang/rust.git@{commit}{JOSH_FILTER}.git")
.run()
.context("FAILED to fetch new commits, something went wrong")?;
.map_err(|e| {
// Try to un-do the previous `git commit`, to leave the repo in the state we found it it.
cmd!(sh, "git reset --hard HEAD^")
.run()
.expect("FAILED to clean up again after failed `git fetch`, sorry for that");
e
})
.context("FAILED to fetch new commits, something went wrong (committing the rust-version file has been undone)")?;
// Merge the fetched commit.
const MERGE_COMMIT_MESSAGE: &str = "Merge from rustc";
cmd!(sh, "git merge FETCH_HEAD --no-ff -m {MERGE_COMMIT_MESSAGE}")
cmd!(sh, "git merge FETCH_HEAD --no-verify --no-ff -m {MERGE_COMMIT_MESSAGE}")
.run()
.context("FAILED to merge new commits, something went wrong")?;
Ok(())
}
fn rustc_push(&self, github_user: &str, branch: &str) -> Result<()> {
let rustc_git = std::env::var_os("RUSTC_GIT");
let working_directory = if let Some(rustc_git) = rustc_git {
rustc_git
} else {
fn rustc_push(rustc_git: Option<String>, github_user: String, branch: String) -> Result<()> {
let sh = shell()?;
sh.change_dir(miri_dir()?);
let base = sh.read_file("rust-version")?.trim().to_owned();
// Make sure the repo is clean.
if cmd!(sh, "git status --untracked-files=no --porcelain").read()?.is_empty().not() {
bail!("working directory must be clean before running `./miri rustc-push`");
}
// Find a repo we can do our preparation in.
if let Some(rustc_git) = rustc_git {
// If rustc_git is `Some`, we'll use an existing fork for the branch updates.
sh.change_dir(rustc_git);
} else {
// Otherwise, do this in the local Miri repo.
println!(
"This will pull a copy of the rust-lang/rust history into this Miri checkout, growing it by about 1GB."
);
println!(
"To avoid that, abort now and set the RUSTC_GIT environment variable to an existing rustc checkout. Proceed? [y/N] "
print!(
"To avoid that, abort now and set the `--rustc-git` flag to an existing rustc checkout. Proceed? [y/N] "
);
std::io::stdout().flush()?;
let mut answer = String::new();
std::io::stdin().read_line(&mut answer)?;
if answer.trim().to_lowercase() != "y" {
std::process::exit(1);
}
self.miri_dir.clone().into()
};
// Prepare the branch. Pushing works much better if we use as base exactly
// the commit that we pulled from last time, so we use the `rust-version`
// file as a good approximation of that.
let rust_version_path = path!(self.miri_dir / "rust-version");
let base = std::fs::read_to_string(rust_version_path)?.trim().to_owned();
println!("Preparing {github_user}/rust (base: {base})...)");
let sh = shell_with_parent_env()?;
sh.change_dir(working_directory);
if cmd!(sh, "git fetch https://github.com/{github_user}").read().is_ok() {
println!("Preparing {github_user}/rust (base: {base})...");
if cmd!(sh, "git fetch https://github.com/{github_user}/rust {branch}")
.ignore_stderr()
.read()
.is_ok()
{
println!(
"The branch '{branch}' seems to already exist in 'https://github.com/{github_user}'. Please delete it and try again."
"The branch '{branch}' seems to already exist in 'https://github.com/{github_user}/rust'. Please delete it and try again."
);
std::process::exit(1);
}
cmd!(sh, "git fetch https://github.com/rust-lang/rust {base}").run()?;
cmd!(sh, "git push https://github.com/{github_user}/rust {base}:refs/heads/{branch}")
.run()?;
println!();
// Do the actual push.
sh.change_dir(&self.miri_dir);
sh.change_dir(miri_dir()?);
println!("Pushing miri changes...");
cmd!(
sh,
"git push http://localhost:8000/{github_user}/rust.git{JOSH_FILTER}.git HEAD:{branch}"
)
.run()?;
// Do a round-trip check to make sure the push worked as expected.
println!();
// Do a round-trip check to make sure the push worked as expected.
cmd!(
sh,
"git fetch http://localhost:8000/{github_user}/rust.git{JOSH_FILTER}.git {branch}"
)
.ignore_stderr()
.read()?;
let head = cmd!(sh, "git rev-parse HEAD").read()?;
let fetch_head = cmd!(sh, "git rev-parse FETCH_HEAD").read()?;
if head != fetch_head {
println!("ERROR: Josh created a non-roundtrip push! Do NOT merge this into rustc!");
std::process::exit(1);
bail!("Josh created a non-roundtrip push! Do NOT merge this into rustc!");
}
println!(
"Confirmed that the push round-trips back to Miri properly. Please create a rustc PR:"
@ -436,189 +254,20 @@ impl MiriRunner<'_> {
Ok(())
}
fn install(&mut self, flags: &[OsString]) -> Result<()> {
let sh = self.shell()?;
self.install_to_dir(&sh, self.miri_dir.clone(), flags)?;
let cargo_miri_dir = path!(self.miri_dir / "cargo-miri");
self.install_to_dir(&sh, cargo_miri_dir, flags)?;
Ok(())
}
fn build(&self, flags: &[OsString]) -> Result<()> {
// Build, and let caller control flags.
let miri_manifest = path!(self.miri_dir / "Cargo.toml");
let cargo_miri_manifest = path!(self.miri_dir / "cargo-miri" / "Cargo.toml");
Self::build_package(
miri_manifest.as_ref(),
&self.active_toolchain,
&self.cargo_extra_flags,
flags,
)?;
Self::build_package(
cargo_miri_manifest.as_ref(),
&self.active_toolchain,
&self.cargo_extra_flags,
flags,
)?;
Ok(())
}
fn check(&self, flags: &[OsString]) -> Result<()> {
fn check_package(
// Path to Cargo.toml file of a package to check.
path: &OsStr,
toolchain: impl AsRef<OsStr>,
extra_flags: &[String],
all_targets: bool,
args: impl IntoIterator<Item = impl AsRef<OsStr>>,
) -> Result<()> {
let all_targets: Option<&OsStr> = all_targets.then_some("--all-targets".as_ref());
let sh = Shell::new()?;
cmd!(sh, "cargo +{toolchain} check {extra_flags...} --manifest-path {path} {all_targets...} {args...}").run()?;
Ok(())
}
// Check, and let caller control flags.
let miri_manifest = path!(self.miri_dir / "Cargo.toml");
let cargo_miri_manifest = path!(self.miri_dir / "cargo-miri" / "Cargo.toml");
check_package(
miri_manifest.as_ref(),
&self.active_toolchain,
&self.cargo_extra_flags,
true,
flags,
)?;
check_package(
cargo_miri_manifest.as_ref(),
&self.active_toolchain,
&self.cargo_extra_flags,
false,
flags,
)?;
Ok(())
}
fn test(&mut self, bless: bool, flags: &[OsString]) -> Result<()> {
let miri_manifest = path!(self.miri_dir / "Cargo.toml");
// First build and get a sysroot.
Self::build_package(
miri_manifest.as_ref(),
&self.active_toolchain,
&self.cargo_extra_flags,
std::iter::empty::<OsString>(),
)?;
self.find_miri_sysroot()?;
let extra_flags = &self.cargo_extra_flags;
// Then test, and let caller control flags.
// Only in root project as `cargo-miri` has no tests.
let sh = self.shell()?;
if bless {
sh.set_var("MIRI_BLESS", "Gesundheit");
}
let toolchain: &OsStr = self.active_toolchain.as_ref();
cmd!(
sh,
"cargo +{toolchain} test {extra_flags...} --manifest-path {miri_manifest} -- {flags...}"
)
.run()?;
Ok(())
}
fn run(&mut self, dep: bool, flags: &[OsString]) -> Result<()> {
use itertools::Itertools;
// Scan for "--target" to overwrite the "MIRI_TEST_TARGET" env var so
// that we set the MIRI_SYSROOT up the right way.
let target = flags.iter().tuple_windows().find(|(first, _)| first == &"--target");
if let Some((_, target)) = target {
// Found it!
self.set_env("MIRI_TEST_TARGET", target);
} else if let Some(var) =
std::env::var_os("MIRI_TEST_TARGET").filter(|target| !target.is_empty())
{
// Make sure miri actually uses this target.
let entry = self.env.entry("MIRIFLAGS".into()).or_default();
entry.push(" --target ");
entry.push(var);
}
// First build and get a sysroot.
let miri_manifest = path!(self.miri_dir / "Cargo.toml");
Self::build_package(
miri_manifest.as_ref(),
&self.active_toolchain,
&self.cargo_extra_flags,
std::iter::empty::<OsString>(),
)?;
self.find_miri_sysroot()?;
// Then run the actual command.
let miri_flags = self.env.get(&OsString::from("MIRIFLAGS")).cloned().unwrap_or_default();
let miri_flags: &OsStr = miri_flags.as_ref();
let extra_flags = &self.cargo_extra_flags;
let sh = self.shell()?;
let toolchain: &OsStr = self.active_toolchain.as_ref();
if dep {
cmd!(
sh,
"cargo +{toolchain} --quiet test --test compiletest {extra_flags...} --manifest-path {miri_manifest} -- --miri-run-dep-mode {miri_flags} {flags...}"
).run()?;
} else {
cmd!(
sh,
"cargo +{toolchain} --quiet run {extra_flags...} --manifest-path {miri_manifest} -- {miri_flags} {flags...}"
).run()?;
}
Ok(())
}
fn fmt(&self, flags: &[OsString]) -> Result<()> {
let toolchain = &self.active_toolchain;
let config_path = path!(self.miri_dir / "rustfmt.toml");
let sh = self.shell()?;
for item in WalkDir::new(&self.miri_dir).into_iter().filter_entry(|entry| {
let name: String = entry.file_name().to_string_lossy().into();
let ty = entry.file_type();
if ty.is_file() {
name.ends_with(".rs")
} else {
// dir or symlink
&name != "target"
}
}) {
let item = item.unwrap(); // Should never panic as we've already filtered out failed entries.
if item.file_type().is_file() {
let path = item.path();
cmd!(sh, "rustfmt +{toolchain} --edition=2021 --config-path {config_path} {flags...} {path}").quiet().run()?;
}
}
Ok(())
}
fn clippy(&self, flags: &[OsString]) -> Result<()> {
let toolchain_modifier = &self.active_toolchain;
let extra_flags = &self.cargo_extra_flags;
let miri_manifest = path!(self.miri_dir / "Cargo.toml");
let sh = self.shell()?;
cmd!(sh, "cargo +{toolchain_modifier} clippy {extra_flags...} --manifest-path {miri_manifest} --all-targets -- {flags...}").run()?;
Ok(())
}
fn cargo(&self, flags: &[OsString]) -> Result<()> {
// We carefully kept the working dir intact, so this will run cargo *on the workspace in the
// current working dir*, not on the main Miri workspace. That is exactly what RA needs.
let toolchain_modifier = &self.active_toolchain;
let sh = self.shell()?;
cmd!(sh, "cargo +{toolchain_modifier} {flags...}").run()?;
Ok(())
}
fn many_seeds(&self, command: &[OsString], seed_start: u64, seed_count: u64) -> Result<()> {
fn many_seeds(command: Vec<OsString>, seed_start: u64, seed_count: u64) -> Result<()> {
let seed_end = seed_start + seed_count;
assert!(!command.is_empty());
let (command_name, trailing_args) = command.split_first().unwrap();
let sh = shell_with_parent_env()?;
let Some((command_name, trailing_args)) = command.split_first() else {
bail!("expected many-seeds command to be non-empty");
};
let sh = shell()?;
for seed in seed_start..seed_end {
println!("Trying seed: {seed}");
let mut miriflags = std::env::var_os("MIRIFLAGS").unwrap_or_default();
let mut miriflags = env::var_os("MIRIFLAGS").unwrap_or_default();
miriflags.push(format!(" -Zlayout-seed={seed} -Zmiri-seed={seed}"));
let status =
cmd!(sh, "{command_name} {trailing_args...}").env("MIRIFLAGS", miriflags).run();
let status = cmd!(sh, "{command_name} {trailing_args...}")
.env("MIRIFLAGS", miriflags)
.quiet()
.run();
if status.is_err() {
println!("Failing seed: {seed}");
break;
@ -626,4 +275,175 @@ impl MiriRunner<'_> {
}
Ok(())
}
fn bench(benches: Vec<OsString>) -> Result<()> {
// The hyperfine to use
let hyperfine = env::var("HYPERFINE");
let hyperfine = hyperfine.as_deref().unwrap_or("hyperfine -w 1 -m 5 --shell=none");
let hyperfine = shell_words::split(hyperfine)?;
let Some((program_name, args)) = hyperfine.split_first() else {
bail!("expected HYPERFINE environment variable to be non-empty");
};
// Make sure we have an up-to-date Miri installed and selected the right toolchain.
Self::install(vec![])?;
let sh = shell()?;
sh.change_dir(miri_dir()?);
let benches_dir = "bench-cargo-miri";
let benches = if benches.is_empty() {
sh.read_dir(benches_dir)?
.into_iter()
.filter(|path| path.is_dir())
.map(Into::into)
.collect()
} else {
benches.to_owned()
};
// Run the requested benchmarks
for bench in benches {
let current_bench = path!(benches_dir / bench / "Cargo.toml");
// We don't attempt to escape `current_bench`, but we wrap it in quotes.
// That seems to make Windows CI happy.
cmd!(
sh,
"{program_name} {args...} 'cargo miri run --manifest-path \"'{current_bench}'\"'"
)
.run()?;
}
Ok(())
}
fn install(flags: Vec<OsString>) -> Result<()> {
Self::auto_actions()?;
let e = MiriEnv::new()?;
e.install_to_sysroot(e.miri_dir.clone(), &flags)?;
e.install_to_sysroot(path!(e.miri_dir / "cargo-miri"), &flags)?;
Ok(())
}
fn build(flags: Vec<OsString>) -> Result<()> {
Self::auto_actions()?;
let e = MiriEnv::new()?;
e.build(path!(e.miri_dir / "Cargo.toml"), &flags, /* quiet */ false)?;
e.build(path!(e.miri_dir / "cargo-miri" / "Cargo.toml"), &flags, /* quiet */ false)?;
Ok(())
}
fn check(flags: Vec<OsString>) -> Result<()> {
Self::auto_actions()?;
let e = MiriEnv::new()?;
e.check(path!(e.miri_dir / "Cargo.toml"), &flags)?;
e.check(path!(e.miri_dir / "cargo-miri" / "Cargo.toml"), &flags)?;
Ok(())
}
fn clippy(flags: Vec<OsString>) -> Result<()> {
Self::auto_actions()?;
let e = MiriEnv::new()?;
e.clippy(path!(e.miri_dir / "Cargo.toml"), &flags)?;
e.clippy(path!(e.miri_dir / "cargo-miri" / "Cargo.toml"), &flags)?;
e.clippy(path!(e.miri_dir / "miri-script" / "Cargo.toml"), &flags)?;
Ok(())
}
fn cargo(flags: Vec<OsString>) -> Result<()> {
Self::auto_actions()?;
let e = MiriEnv::new()?;
let toolchain = &e.toolchain;
// We carefully kept the working dir intact, so this will run cargo *on the workspace in the
// current working dir*, not on the main Miri workspace. That is exactly what RA needs.
cmd!(e.sh, "cargo +{toolchain} {flags...}").run()?;
Ok(())
}
fn test(bless: bool, flags: Vec<OsString>) -> Result<()> {
Self::auto_actions()?;
let mut e = MiriEnv::new()?;
// First build, and get a sysroot.
e.build(path!(e.miri_dir / "Cargo.toml"), &[], /* quiet */ true)?;
e.build_miri_sysroot()?;
// Then test, and let caller control flags.
// Only in root project as `cargo-miri` has no tests.
if bless {
e.sh.set_var("RUSTC_BLESS", "Gesundheit");
}
e.test(path!(e.miri_dir / "Cargo.toml"), &flags)?;
Ok(())
}
fn run(dep: bool, flags: Vec<OsString>) -> Result<()> {
Self::auto_actions()?;
let mut e = MiriEnv::new()?;
// Scan for "--target" to overwrite the "MIRI_TEST_TARGET" env var so
// that we set the MIRI_SYSROOT up the right way.
use itertools::Itertools;
let target = flags.iter().tuple_windows().find(|(first, _)| first == &"--target");
if let Some((_, target)) = target {
// Found it!
e.sh.set_var("MIRI_TEST_TARGET", target);
} else if let Ok(target) = std::env::var("MIRI_TEST_TARGET") {
// Make sure miri actually uses this target.
let miriflags = e.sh.var("MIRIFLAGS").unwrap_or_default();
e.sh.set_var("MIRIFLAGS", format!("{miriflags} --target {target}"));
}
// First build, and get a sysroot.
let miri_manifest = path!(e.miri_dir / "Cargo.toml");
e.build(&miri_manifest, &[], /* quiet */ true)?;
e.build_miri_sysroot()?;
// Then run the actual command.
let miri_flags = e.sh.var("MIRIFLAGS").unwrap_or_default();
let miri_flags = flagsplit(&miri_flags);
let toolchain = &e.toolchain;
let extra_flags = &e.cargo_extra_flags;
if dep {
cmd!(
e.sh,
"cargo +{toolchain} --quiet test --test compiletest {extra_flags...} --manifest-path {miri_manifest} -- --miri-run-dep-mode {miri_flags...} {flags...}"
).quiet().run()?;
} else {
cmd!(
e.sh,
"cargo +{toolchain} --quiet run {extra_flags...} --manifest-path {miri_manifest} -- {miri_flags...} {flags...}"
).quiet().run()?;
}
Ok(())
}
fn fmt(flags: Vec<OsString>) -> Result<()> {
Self::auto_actions()?;
let e = MiriEnv::new()?;
let toolchain = &e.toolchain;
let config_path = path!(e.miri_dir / "rustfmt.toml");
let mut cmd = cmd!(
e.sh,
"rustfmt +{toolchain} --edition=2021 --config-path {config_path} {flags...}"
);
eprintln!("$ {cmd} ...");
// Add all the filenames to the command.
// FIXME: `rustfmt` will follow the `mod` statements in these files, so we get a bunch of
// duplicate diffs.
for item in WalkDir::new(&e.miri_dir).into_iter().filter_entry(|entry| {
let name = entry.file_name().to_string_lossy();
let ty = entry.file_type();
if ty.is_file() {
name.ends_with(".rs")
} else {
// dir or symlink. skip `target` and `.git`.
&name != "target" && &name != ".git"
}
}) {
let item = item?;
if item.file_type().is_file() {
cmd = cmd.arg(item.into_path());
}
}
// We want our own error message, repeating the command is too much.
cmd.quiet().run().map_err(|_| anyhow!("`rustfmt` failed"))?;
Ok(())
}
}

View file

@ -1,58 +1,120 @@
pub(crate) mod arg;
mod commands;
mod util;
use std::path::Path;
use std::ffi::OsString;
use anyhow::Result;
use clap::Parser;
use path_macro::path;
use clap::{Parser, Subcommand};
use arg::Subcommands;
struct AutoConfig {
toolchain: bool,
fmt: bool,
clippy: bool,
#[derive(Parser, Clone, Debug)]
#[command(author, about, long_about = None)]
pub struct Cli {
#[command(subcommand)]
pub command: Command,
}
impl Subcommands {
fn run_auto_things(&self) -> bool {
use Subcommands::*;
match self {
// Early commands, that don't do auto-things and don't want the environment-altering things happening below.
Toolchain { .. }
| RustcPull { .. }
| RustcPush { .. }
| ManySeeds { .. }
| Bench { .. } => false,
Install { .. }
| Check { .. }
| Build { .. }
| Test { .. }
| Run { .. }
| Fmt { .. }
| Clippy { .. }
| Cargo { .. } => true,
}
}
fn get_config(&self, miri_dir: &Path) -> Option<AutoConfig> {
let skip_auto_ops = std::env::var_os("MIRI_AUTO_OPS").is_some();
if !self.run_auto_things() {
return None;
}
if skip_auto_ops {
return Some(AutoConfig { toolchain: false, fmt: false, clippy: false });
}
let auto_everything = path!(miri_dir / ".auto_everything").exists();
let toolchain = auto_everything || path!(miri_dir / ".auto-toolchain").exists();
let fmt = auto_everything || path!(miri_dir / ".auto-fmt").exists();
let clippy = auto_everything || path!(miri_dir / ".auto-clippy").exists();
Some(AutoConfig { toolchain, fmt, clippy })
}
#[derive(Subcommand, Clone, Debug)]
pub enum Command {
/// Installs the miri driver and cargo-miri.
/// Sets up the rpath such that the installed binary should work in any
/// working directory. Note that the binaries are placed in the `miri` toolchain
/// sysroot, to prevent conflicts with other toolchains.
Install {
/// Flags that are passed through to `cargo install`.
#[arg(trailing_var_arg = true, allow_hyphen_values = true)]
flags: Vec<OsString>,
},
/// Just build miri.
Build {
/// Flags that are passed through to `cargo build`.
#[arg(trailing_var_arg = true, allow_hyphen_values = true)]
flags: Vec<OsString>,
},
/// Just check miri.
Check {
/// Flags that are passed through to `cargo check`.
#[arg(trailing_var_arg = true, allow_hyphen_values = true)]
flags: Vec<OsString>,
},
/// Build miri, set up a sysroot and then run the test suite.
Test {
#[arg(long, default_value_t = false)]
bless: bool,
/// Flags that are passed through to `cargo test`.
#[arg(trailing_var_arg = true, allow_hyphen_values = true)]
flags: Vec<OsString>,
},
/// Build miri, set up a sysroot and then run the driver with the given <flags>.
/// (Also respects MIRIFLAGS environment variable.)
Run {
#[arg(long, default_value_t = false)]
dep: bool,
/// Flags that are passed through to `miri`
#[arg(trailing_var_arg = true, allow_hyphen_values = true)]
flags: Vec<OsString>,
},
/// Format all sources and tests.
Fmt {
/// Flags that are passed through to `rustfmt`.
#[arg(trailing_var_arg = true, allow_hyphen_values = true)]
flags: Vec<OsString>,
},
/// Runs clippy on all sources.
Clippy {
/// Flags that are passed through to `cargo clippy`.
#[arg(trailing_var_arg = true, allow_hyphen_values = true)]
flags: Vec<OsString>,
},
/// Runs just `cargo <flags>` with the Miri-specific environment variables.
/// Mainly meant to be invoked by rust-analyzer.
Cargo {
#[arg(trailing_var_arg = true, allow_hyphen_values = true)]
flags: Vec<OsString>,
},
/// Runs <command> over and over again with different seeds for Miri. The MIRIFLAGS
/// variable is set to its original value appended with ` -Zmiri-seed=$SEED` for
/// many different seeds.
ManySeeds {
/// Starting seed.
#[arg(long, env = "MIRI_SEED_START", default_value_t = 0)]
seed_start: u64,
/// Amount of seeds to try.
#[arg(long, env = "MIRI_SEEDS", default_value_t = 256)]
seeds: u64,
#[arg(trailing_var_arg = true, allow_hyphen_values = true)]
command: Vec<OsString>,
},
/// Runs the benchmarks from bench-cargo-miri in hyperfine. hyperfine needs to be installed.
Bench {
/// List of benchmarks to run. By default all benchmarks are run.
#[arg(trailing_var_arg = true, allow_hyphen_values = true)]
benches: Vec<OsString>,
},
/// Update and activate the rustup toolchain 'miri' to the commit given in the
/// `rust-version` file.
/// `rustup-toolchain-install-master` must be installed for this to work. Any extra
/// flags are passed to `rustup-toolchain-install-master`.
Toolchain {
#[arg(trailing_var_arg = true, allow_hyphen_values = true)]
flags: Vec<OsString>,
},
/// Pull and merge Miri changes from the rustc repo. Defaults to fetching the latest
/// rustc commit. The fetched commit is stored in the `rust-version` file, so the
/// next `./miri toolchain` will install the rustc that just got pulled.
RustcPull { commit: Option<String> },
/// Push Miri changes back to the rustc repo. This will pull a copy of the rustc
/// history into the Miri repo, unless you set the RUSTC_GIT env var to an existing
/// clone of the rustc repo.
RustcPush {
#[arg(long, env = "RUSTC_GIT")]
rustc_git: Option<String>,
github_user: String,
branch: String,
},
}
fn main() -> Result<()> {
let args = arg::Cli::parse();
commands::MiriRunner::exec(&args.commands)?;
let args = Cli::parse();
args.command.exec()?;
Ok(())
}

View file

@ -0,0 +1,153 @@
use std::ffi::{OsStr, OsString};
use std::path::PathBuf;
use anyhow::{Context, Result};
use dunce::canonicalize;
use path_macro::path;
use xshell::{cmd, Shell};
pub fn miri_dir() -> std::io::Result<PathBuf> {
const MIRI_SCRIPT_ROOT_DIR: &str = env!("CARGO_MANIFEST_DIR");
Ok(canonicalize(MIRI_SCRIPT_ROOT_DIR)?.parent().unwrap().into())
}
/// Queries the active toolchain for the Miri dir.
pub fn active_toolchain() -> Result<String> {
let sh = shell()?;
sh.change_dir(miri_dir()?);
let stdout = cmd!(sh, "rustup show active-toolchain").read()?;
Ok(stdout.split_whitespace().next().context("Could not obtain active Rust toolchain")?.into())
}
pub fn shell() -> Result<Shell> {
let sh = Shell::new()?;
// xshell does not propagate parent's env variables by default.
for (k, v) in std::env::vars_os() {
sh.set_var(k, v);
}
Ok(sh)
}
pub fn flagsplit(flags: &str) -> Vec<String> {
// This code is taken from `RUSTFLAGS` handling in cargo.
flags.split(' ').map(str::trim).filter(|s| !s.is_empty()).map(str::to_string).collect()
}
/// Some extra state we track for building Miri, such as the right RUSTFLAGS.
pub struct MiriEnv {
/// miri_dir is the root of the miri repository checkout we are working in.
pub miri_dir: PathBuf,
/// active_toolchain is passed as `+toolchain` argument to cargo/rustc invocations.
pub toolchain: String,
/// Extra flags to pass to cargo.
pub cargo_extra_flags: Vec<String>,
/// The rustc sysroot
pub sysroot: PathBuf,
/// The shell we use.
pub sh: Shell,
}
impl MiriEnv {
pub fn new() -> Result<Self> {
let sh = shell()?;
let toolchain = active_toolchain()?;
let miri_dir = miri_dir()?;
let sysroot = cmd!(sh, "rustc +{toolchain} --print sysroot").read()?.into();
let target_output = cmd!(sh, "rustc +{toolchain} --version --verbose").read()?;
let rustc_meta = rustc_version::version_meta_for(&target_output)?;
let libdir = path!(sysroot / "lib" / "rustlib" / rustc_meta.host / "lib");
// Determine some toolchain properties
if !libdir.exists() {
println!("Something went wrong determining the library dir.");
println!("I got {} but that does not exist.", libdir.display());
println!("Please report a bug at https://github.com/rust-lang/miri/issues.");
std::process::exit(2);
}
// Share target dir between `miri` and `cargo-miri`.
let target_dir = std::env::var_os("CARGO_TARGET_DIR")
.unwrap_or_else(|| path!(miri_dir / "target").into());
sh.set_var("CARGO_TARGET_DIR", target_dir);
// We configure dev builds to not be unusably slow.
let devel_opt_level =
std::env::var_os("CARGO_PROFILE_DEV_OPT_LEVEL").unwrap_or_else(|| "2".into());
sh.set_var("CARGO_PROFILE_DEV_OPT_LEVEL", devel_opt_level);
// Compute rustflags.
let rustflags = {
let env = std::env::var_os("RUSTFLAGS");
let mut flags_with_warnings = OsString::from(
"-Zunstable-options -Wrustc::internal -Wrust_2018_idioms -Wunused_lifetimes -Wsemicolon_in_expressions_from_macros ",
);
if let Some(value) = env {
flags_with_warnings.push(value);
}
// We set the rpath so that Miri finds the private rustc libraries it needs.
let mut flags_with_compiler_settings = OsString::from("-C link-args=-Wl,-rpath,");
flags_with_compiler_settings.push(&libdir);
flags_with_compiler_settings.push(flags_with_warnings);
flags_with_compiler_settings
};
sh.set_var("RUSTFLAGS", rustflags);
// Get extra flags for cargo.
let cargo_extra_flags = std::env::var("CARGO_EXTRA_FLAGS").unwrap_or_default();
let cargo_extra_flags = flagsplit(&cargo_extra_flags);
Ok(MiriEnv { miri_dir, toolchain, sh, sysroot, cargo_extra_flags })
}
pub fn install_to_sysroot(
&self,
path: impl AsRef<OsStr>,
args: impl IntoIterator<Item = impl AsRef<OsStr>>,
) -> Result<()> {
let MiriEnv { sysroot, toolchain, cargo_extra_flags, .. } = self;
// Install binaries to the miri toolchain's `sysroot` so they do not interact with other toolchains.
cmd!(self.sh, "cargo +{toolchain} install {cargo_extra_flags...} --path {path} --force --root {sysroot} {args...}").run()?;
Ok(())
}
pub fn build(
&self,
manifest_path: impl AsRef<OsStr>,
args: &[OsString],
quiet: bool,
) -> Result<()> {
let MiriEnv { toolchain, cargo_extra_flags, .. } = self;
let quiet_flag = if quiet { Some("--quiet") } else { None };
let mut cmd = cmd!(
self.sh,
"cargo +{toolchain} build {cargo_extra_flags...} --manifest-path {manifest_path} {quiet_flag...} {args...}"
);
cmd.set_quiet(quiet);
cmd.run()?;
Ok(())
}
pub fn check(&self, manifest_path: impl AsRef<OsStr>, args: &[OsString]) -> Result<()> {
let MiriEnv { toolchain, cargo_extra_flags, .. } = self;
cmd!(self.sh, "cargo +{toolchain} check {cargo_extra_flags...} --manifest-path {manifest_path} --all-targets {args...}")
.run()?;
Ok(())
}
pub fn clippy(&self, manifest_path: impl AsRef<OsStr>, args: &[OsString]) -> Result<()> {
let MiriEnv { toolchain, cargo_extra_flags, .. } = self;
cmd!(self.sh, "cargo +{toolchain} clippy {cargo_extra_flags...} --manifest-path {manifest_path} --all-targets {args...}")
.run()?;
Ok(())
}
pub fn test(&self, manifest_path: impl AsRef<OsStr>, args: &[OsString]) -> Result<()> {
let MiriEnv { toolchain, cargo_extra_flags, .. } = self;
cmd!(
self.sh,
"cargo +{toolchain} test {cargo_extra_flags...} --manifest-path {manifest_path} {args...}"
)
.run()?;
Ok(())
}
}