From d9e8f3c203ffd57ac93a467c03e2a07254b39747 Mon Sep 17 00:00:00 2001 From: Ralf Jung Date: Sun, 30 Jul 2023 17:28:23 +0200 Subject: [PATCH] miri-script refactor --- miri | 4 +- miri-script/miri | 4 + miri-script/src/arg.rs | 104 ------ miri-script/src/commands.rs | 669 ++++++++++++------------------------ miri-script/src/main.rs | 156 ++++++--- miri-script/src/util.rs | 154 +++++++++ 6 files changed, 494 insertions(+), 597 deletions(-) create mode 100755 miri-script/miri delete mode 100644 miri-script/src/arg.rs create mode 100644 miri-script/src/util.rs diff --git a/miri b/miri index 7412df69bd..c816a4bb06 100755 --- a/miri +++ b/miri @@ -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 $@ \ No newline at end of file +cargo build -q --manifest-path "$(dirname "$0")"/miri-script/Cargo.toml +"$(dirname "$0")"/miri-script/target/debug/miri-script "$@" diff --git a/miri-script/miri b/miri-script/miri new file mode 100755 index 0000000000..cf3ad06788 --- /dev/null +++ b/miri-script/miri @@ -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 . +exec "$(dirname "$0")"/../miri "$@" diff --git a/miri-script/src/arg.rs b/miri-script/src/arg.rs deleted file mode 100644 index 24a5204e04..0000000000 --- a/miri-script/src/arg.rs +++ /dev/null @@ -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, - }, - /// Just build miri. - Build { - /// Flags that are passed through to `cargo build`. - #[arg(trailing_var_arg = true, allow_hyphen_values = true)] - flags: Vec, - }, - /// Just check miri. - Check { - /// Flags that are passed through to `cargo check`. - #[arg(trailing_var_arg = true, allow_hyphen_values = true)] - flags: Vec, - }, - /// 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, - }, - /// Build miri, set up a sysroot and then run the driver with the given . - /// (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, - }, - /// Format all sources and tests. - Fmt { - /// Flags that are passed through to `rustfmt`. - #[arg(trailing_var_arg = true, allow_hyphen_values = true)] - flags: Vec, - }, - /// 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, - }, - /// Runs just `cargo ` 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, - }, - /// Runs 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, - }, - /// 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, - }, - /// 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, - }, - /// 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 }, - /// 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 }, -} diff --git a/miri-script/src/commands.rs b/miri-script/src/commands.rs index 121b506780..53f9cfcd55 100644 --- a/miri-script/src/commands.rs +++ b/miri-script/src/commands.rs @@ -1,307 +1,98 @@ -use std::collections::BTreeMap; - -use std::ffi::{OsStr, OsString}; -use std::path::{Path, PathBuf}; +use std::env; +use std::ffi::OsString; 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 { - 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 { - 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, - command: &'a super::Subcommands, - /// Environment variables passed to child processes. - env: BTreeMap, - /// Additional variables used by environment-altering commands. - /// These should be accessed by corresponding methods (e.g. `sysroot()`) and not directly. - sysroot: Option, -} - -fn shell_with_parent_env() -> Result { - 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, - value: impl Into, - ) -> Option { - 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", ¤t_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); + let manifest_path = path!(self.miri_dir / "cargo-miri" / "Cargo.toml"); + 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(()) } +} - /// 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 { - 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")?; +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![])?; } - - Ok(output?) - } - fn build_package( - // Path to Cargo.toml file of a package to build. - path: &OsStr, - toolchain: impl AsRef, - extra_flags: &[String], - args: impl IntoIterator>, - ) -> Result<()> { - let sh = Shell::new()?; - cmd!(sh, "cargo +{toolchain} build {extra_flags...} --manifest-path {path} {args...}") - .run()?; - Ok(()) - } - fn shell(&self) -> Result { - let sh = shell_with_parent_env()?; - for (k, v) in &self.env { - sh.set_var(k, v); + if auto_fmt { + Self::fmt(vec![])?; } - - Ok(sh) - } - - fn libdir(&self) -> Result { - 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 { - 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) + if auto_clippy { + Self::clippy(vec![])?; } - } - fn install_to_dir( - &mut self, - sh: &Shell, - path: PathBuf, - args: impl IntoIterator>, - ) -> 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()?; + 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()?; + 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), } - Ok(()) } - fn toolchain(&self, flags: &[OsString]) -> Result<()> { + fn toolchain(flags: Vec) -> 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 +109,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 +129,10 @@ impl MiriRunner<'_> { Ok(()) } - fn rustc_pull(&self, commit: Option) -> 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) -> 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 @@ -351,7 +143,7 @@ impl MiriRunner<'_> { })?; // 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)?; + 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}") .run() @@ -367,34 +159,31 @@ impl MiriRunner<'_> { 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 + fn rustc_push(rustc_git: Option, github_user: String, branch: String) -> Result<()> { + let sh = shell()?; + if let Some(rustc_git) = rustc_git { + sh.change_dir(rustc_git); } else { // If rustc_git is `Some`, we'll use an existing fork for the branch updates. // Otherwise, do this in the local Miri repo. + sh.change_dir(miri_dir()?); 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] " + "To avoid that, abort now and set the `--rustc-git` flag to an existing rustc checkout. Proceed? [y/N] " ); 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(); + let base = sh.read_file("rust-version")?.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!( @@ -409,7 +198,7 @@ impl MiriRunner<'_> { .run()?; println!(); // Do the actual push. - sh.change_dir(&self.miri_dir); + sh.change_dir(miri_dir()?); println!("Pushing miri changes..."); cmd!( sh, @@ -426,8 +215,7 @@ impl MiriRunner<'_> { 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,194 +224,187 @@ 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)?; + fn many_seeds(command: Vec, seed_start: u64, seed_count: u64) -> Result<()> { + let seed_end = seed_start + seed_count; + 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 = 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(); + if status.is_err() { + println!("Failing seed: {seed}"); + break; + } + } + Ok(()) + } + + fn bench(benches: Vec) -> 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_dir = path!(benches_dir / bench / "Cargo.toml"); + // (ab)use the `cmd!` macro to format this argument for us + let run_cmd = + cmd!(sh, "cargo miri run --manifest-path {current_bench_dir}").to_string(); + cmd!(sh, "{program_name} {args...} {run_cmd}").run()?; + } 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, - )?; + fn install(flags: Vec) -> 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 check(&self, flags: &[OsString]) -> Result<()> { - fn check_package( - // Path to Cargo.toml file of a package to check. - path: &OsStr, - toolchain: impl AsRef, - extra_flags: &[String], - all_targets: bool, - args: impl IntoIterator>, - ) -> 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, - )?; + fn build(flags: Vec) -> 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) -> 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) -> 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)?; + Ok(()) + } + + fn cargo(flags: Vec) -> 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(&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::(), - )?; - self.find_miri_sysroot()?; - let extra_flags = &self.cargo_extra_flags; + fn test(bless: bool, flags: Vec) -> 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. - let sh = self.shell()?; if bless { - sh.set_var("MIRI_BLESS", "Gesundheit"); + e.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()?; + e.test(path!(e.miri_dir / "Cargo.toml"), &flags)?; Ok(()) } - fn run(&mut self, dep: bool, flags: &[OsString]) -> Result<()> { - use itertools::Itertools; + fn run(dep: bool, flags: Vec) -> 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! - self.set_env("MIRI_TEST_TARGET", target); - } else if let Some(var) = - std::env::var_os("MIRI_TEST_TARGET").filter(|target| !target.is_empty()) - { + 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 entry = self.env.entry("MIRIFLAGS".into()).or_default(); - entry.push(" --target "); - entry.push(var); + 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!(self.miri_dir / "Cargo.toml"); - Self::build_package( - miri_manifest.as_ref(), - &self.active_toolchain, - &self.cargo_extra_flags, - std::iter::empty::(), - )?; - self.find_miri_sysroot()?; + // 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 = 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(); + let miri_flags = e.sh.var("MIRIFLAGS").unwrap_or_default(); + let miri_flags = shell_words::split(&miri_flags)?; + let toolchain = &e.toolchain; + let extra_flags = &e.cargo_extra_flags; if dep { cmd!( - sh, - "cargo +{toolchain} --quiet test --test compiletest {extra_flags...} --manifest-path {miri_manifest} -- --miri-run-dep-mode {miri_flags} {flags...}" - ).run()?; + 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!( - sh, - "cargo +{toolchain} --quiet run {extra_flags...} --manifest-path {miri_manifest} -- {miri_flags} {flags...}" - ).run()?; + e.sh, + "cargo +{toolchain} --quiet run {extra_flags...} --manifest-path {miri_manifest} -- {miri_flags...} {flags...}" + ).quiet().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(); + fn fmt(flags: Vec) -> 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. + 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 - &name != "target" + // dir or symlink. skip `target` and `.git`. + &name != "target" && &name != ".git" } }) { - let item = item.unwrap(); // Should never panic as we've already filtered out failed entries. + let item = item?; if item.file_type().is_file() { - let path = item.path(); - cmd!(sh, "rustfmt +{toolchain} --edition=2021 --config-path {config_path} {flags...} {path}").quiet().run()?; + cmd = cmd.arg(item.into_path()); } } - 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<()> { - 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()?; - for seed in seed_start..seed_end { - println!("Trying seed: {seed}"); - let mut miriflags = std::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(); - if status.is_err() { - println!("Failing seed: {seed}"); - break; - } - } + cmd.quiet().run()?; Ok(()) } } diff --git a/miri-script/src/main.rs b/miri-script/src/main.rs index dbee923d48..ce00de5ac5 100644 --- a/miri-script/src/main.rs +++ b/miri-script/src/main.rs @@ -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 { - 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, + }, + /// Just build miri. + Build { + /// Flags that are passed through to `cargo build`. + #[arg(trailing_var_arg = true, allow_hyphen_values = true)] + flags: Vec, + }, + /// Just check miri. + Check { + /// Flags that are passed through to `cargo check`. + #[arg(trailing_var_arg = true, allow_hyphen_values = true)] + flags: Vec, + }, + /// 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, + }, + /// Build miri, set up a sysroot and then run the driver with the given . + /// (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, + }, + /// Format all sources and tests. + Fmt { + /// Flags that are passed through to `rustfmt`. + #[arg(trailing_var_arg = true, allow_hyphen_values = true)] + flags: Vec, + }, + /// 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, + }, + /// Runs just `cargo ` 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, + }, + /// Runs 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, + }, + /// 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, + }, + /// 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, + }, + /// 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 }, + /// 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, + 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(()) } diff --git a/miri-script/src/util.rs b/miri-script/src/util.rs new file mode 100644 index 0000000000..90a25f5c19 --- /dev/null +++ b/miri-script/src/util.rs @@ -0,0 +1,154 @@ +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 { + 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 { + 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 { + 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) +} + +/// 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, + /// The rustc sysroot + pub sysroot: PathBuf, + /// The shell we use. + pub sh: Shell, +} + +impl MiriEnv { + pub fn new() -> Result { + 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 = shell_words::split(&cargo_extra_flags)?; + + Ok(MiriEnv { miri_dir, toolchain, sh, sysroot, cargo_extra_flags }) + } + + pub fn install_to_sysroot( + &self, + path: impl AsRef, + args: impl IntoIterator>, + ) -> Result<()> { + let sysroot = &self.sysroot; + let toolchain = &self.toolchain; + let extra_flags = &self.cargo_extra_flags; + // Install binaries to the miri toolchain's `sysroot` so they do not interact with other toolchains. + cmd!(self.sh, "cargo +{toolchain} install {extra_flags...} --path {path} --force --root {sysroot} {args...}").run()?; + Ok(()) + } + + pub fn build( + &self, + manifest_path: impl AsRef, + args: &[OsString], + quiet: bool, + ) -> Result<()> { + let toolchain = &self.toolchain; + let extra_flags = &self.cargo_extra_flags; + let quiet_flag = if quiet { Some("--quiet") } else { None }; + let mut cmd = cmd!( + self.sh, + "cargo +{toolchain} build {extra_flags...} --manifest-path {manifest_path} {quiet_flag...} {args...}" + ); + cmd.set_quiet(quiet); + cmd.run()?; + Ok(()) + } + + pub fn check(&self, manifest_path: impl AsRef, args: &[OsString]) -> Result<()> { + let toolchain = &self.toolchain; + let extra_flags = &self.cargo_extra_flags; + cmd!(self.sh, "cargo +{toolchain} check {extra_flags...} --manifest-path {manifest_path} --all-targets {args...}") + .run()?; + Ok(()) + } + + pub fn clippy(&self, manifest_path: impl AsRef, args: &[OsString]) -> Result<()> { + let toolchain = &self.toolchain; + let extra_flags = &self.cargo_extra_flags; + cmd!(self.sh, "cargo +{toolchain} clippy {extra_flags...} --manifest-path {manifest_path} --all-targets {args...}") + .run()?; + Ok(()) + } + + pub fn test(&self, manifest_path: impl AsRef, args: &[OsString]) -> Result<()> { + let toolchain = &self.toolchain; + let extra_flags = &self.cargo_extra_flags; + cmd!( + self.sh, + "cargo +{toolchain} test {extra_flags...} --manifest-path {manifest_path} {args...}" + ) + .run()?; + Ok(()) + } +}