diff --git a/kani-driver/src/args/cargo.rs b/kani-driver/src/args/cargo.rs index 5850a601c704..67b04d7aa6a8 100644 --- a/kani-driver/src/args/cargo.rs +++ b/kani-driver/src/args/cargo.rs @@ -3,8 +3,8 @@ //! Module that define parsers that mimic Cargo options. use crate::args::ValidateArgs; +use crate::util::args::CargoArg; use clap::error::Error; -use std::ffi::OsString; use std::path::PathBuf; /// Arguments that Kani pass down into Cargo essentially uninterpreted. @@ -61,8 +61,8 @@ impl CargoCommonArgs { /// Convert the arguments back to a format that cargo can understand. /// Note that the `exclude` option requires special processing and it's not included here. - pub fn to_cargo_args(&self) -> Vec { - let mut cargo_args: Vec = vec![]; + pub fn to_cargo_args(&self) -> Vec { + let mut cargo_args: Vec = vec![]; if self.all_features { cargo_args.push("--all-features".into()); } @@ -117,12 +117,12 @@ pub struct CargoTargetArgs { impl CargoTargetArgs { /// Convert the arguments back to a format that cargo can understand. - pub fn to_cargo_args(&self) -> Vec { + pub fn to_cargo_args(&self) -> Vec { let mut cargo_args = self .bin .iter() .map(|binary| format!("--bin={binary}").into()) - .collect::>(); + .collect::>(); if self.bins { cargo_args.push("--bins".into()); @@ -168,7 +168,7 @@ pub struct CargoTestArgs { impl CargoTestArgs { /// Convert the arguments back to a format that cargo can understand. - pub fn to_cargo_args(&self) -> Vec { + pub fn to_cargo_args(&self) -> Vec { let mut cargo_args = self.common.to_cargo_args(); cargo_args.append(&mut self.target.to_cargo_args()); cargo_args diff --git a/kani-driver/src/call_cargo.rs b/kani-driver/src/call_cargo.rs index 9e32784f3fd4..fbcd09c4c835 100644 --- a/kani-driver/src/call_cargo.rs +++ b/kani-driver/src/call_cargo.rs @@ -2,13 +2,14 @@ // SPDX-License-Identifier: Apache-2.0 OR MIT use crate::args::VerificationArgs; -use crate::call_single_file::{LibConfig, to_rustc_arg}; +use crate::call_single_file::LibConfig; use crate::project::Artifact; use crate::session::{ KaniSession, get_cargo_path, lib_folder, lib_no_core_folder, setup_cargo_command, setup_cargo_command_inner, }; use crate::util; +use crate::util::args::{CargoArg, CommandWrapper as _, KaniArg, PassTo, encode_as_rustc_arg}; use anyhow::{Context, Result, bail}; use cargo_metadata::diagnostic::{Diagnostic, DiagnosticLevel}; use cargo_metadata::{ @@ -17,7 +18,6 @@ use cargo_metadata::{ }; use kani_metadata::{ArtifactType, CompilerArtifactStub}; use std::collections::HashMap; -use std::ffi::{OsStr, OsString}; use std::fmt::{self, Display}; use std::fs::{self, File}; use std::io::IsTerminal; @@ -77,13 +77,19 @@ crate-type = ["lib"] pub fn cargo_build_std(&self, std_path: &Path, krate_path: &Path) -> Result> { let lib_path = lib_no_core_folder().unwrap(); let mut rustc_args = self.kani_rustc_flags(LibConfig::new_no_core(lib_path)); - rustc_args.push(to_rustc_arg(self.kani_compiler_local_flags()).into()); + + // In theory, these could be passed just to the local crate rather than all crates, + // but the `cargo build` command we use for building `std` doesn't allow you to pass `rustc` + // arguments, so we have to pass them through the environment variable instead. + rustc_args.push(encode_as_rustc_arg(&self.kani_compiler_local_flags())); + // Ignore global assembly, since `compiler_builtins` has some. - rustc_args.push( - to_rustc_arg(vec!["--ignore-global-asm".to_string(), self.reachability_arg()]).into(), - ); + rustc_args.push(encode_as_rustc_arg(&[ + KaniArg::from("--ignore-global-asm"), + self.reachability_arg(), + ])); - let mut cargo_args: Vec = vec!["build".into()]; + let mut cargo_args: Vec = vec!["build".into()]; cargo_args.append(&mut cargo_config_args()); // Configuration needed to parse cargo compilation status. @@ -103,12 +109,10 @@ crate-type = ["lib"] // Since we are verifying the standard library, we set the reachability to all crates. let mut cmd = setup_cargo_command()?; - cmd.args(&cargo_args) + cmd.pass_cargo_args(&cargo_args) .current_dir(krate_path) .env("RUSTC", &self.kani_compiler) - // Use CARGO_ENCODED_RUSTFLAGS instead of RUSTFLAGS is preferred. See - // https://doc.rust-lang.org/cargo/reference/environment-variables.html - .env("CARGO_ENCODED_RUSTFLAGS", rustc_args.join(OsStr::new("\x1f"))) + .pass_rustc_args(&rustc_args, PassTo::AllCrates) .env("CARGO_TERM_PROGRESS_WHEN", "never") .env("__CARGO_TESTS_ONLY_SRC_ROOT", full_path.as_os_str()); @@ -146,9 +150,9 @@ crate-type = ["lib"] let lib_path = lib_folder().unwrap(); let mut rustc_args = self.kani_rustc_flags(LibConfig::new(lib_path)); - rustc_args.push(to_rustc_arg(self.kani_compiler_dependency_flags()).into()); + rustc_args.push(encode_as_rustc_arg(&self.kani_compiler_dependency_flags())); - let mut cargo_args: Vec = vec!["rustc".into()]; + let mut cargo_args: Vec = vec!["rustc".into()]; if let Some(path) = &self.args.cargo.manifest_path { cargo_args.push("--manifest-path".into()); cargo_args.push(path.into()); @@ -201,9 +205,6 @@ crate-type = ["lib"] let mut kani_pkg_args = vec![self.reachability_arg()]; kani_pkg_args.extend(self.kani_compiler_local_flags()); - // Convert package args to rustc args for passing - let pkg_args = vec!["--".into(), to_rustc_arg(kani_pkg_args)]; - let mut found_target = false; let packages = self.packages_to_verify(&self.args, &metadata)?; let mut artifacts = vec![]; @@ -212,14 +213,13 @@ crate-type = ["lib"] for verification_target in package_targets(&self.args, package) { let mut cmd = setup_cargo_command_inner(Some(verification_target.target().name.clone()))?; - cmd.args(&cargo_args) + cmd.pass_cargo_args(&cargo_args) .args(vec!["-p", &package.id.to_string()]) .args(verification_target.to_args()) - .args(&pkg_args) + .arg("--") // Add this delimiter so we start passing args to rustc and not Cargo .env("RUSTC", &self.kani_compiler) - // Use CARGO_ENCODED_RUSTFLAGS instead of RUSTFLAGS is preferred. See - // https://doc.rust-lang.org/cargo/reference/environment-variables.html - .env("CARGO_ENCODED_RUSTFLAGS", rustc_args.join(OsStr::new("\x1f"))) + .pass_rustc_args(&rustc_args, PassTo::AllCrates) + .pass_rustc_arg(encode_as_rustc_arg(&kani_pkg_args), PassTo::OnlyLocalCrate) // This is only required for stable but is a no-op for nightly channels .env("RUSTC_BOOTSTRAP", "1") .env("CARGO_TERM_PROGRESS_WHEN", "never"); @@ -472,7 +472,7 @@ crate-type = ["lib"] } } -pub fn cargo_config_args() -> Vec { +pub fn cargo_config_args() -> Vec { [ "--target", env!("TARGET"), @@ -481,7 +481,7 @@ pub fn cargo_config_args() -> Vec { "-Ztarget-applies-to-host", "--config=host.rustflags=[\"--cfg=kani_host\"]", ] - .map(OsString::from) + .map(CargoArg::from) .to_vec() } diff --git a/kani-driver/src/call_single_file.rs b/kani-driver/src/call_single_file.rs index 695be76f4c77..bdd9fdf52314 100644 --- a/kani-driver/src/call_single_file.rs +++ b/kani-driver/src/call_single_file.rs @@ -3,14 +3,14 @@ use anyhow::Result; use kani_metadata::UnstableFeature; -use std::ffi::OsString; use std::path::{Path, PathBuf}; use std::process::Command; use crate::session::{KaniSession, lib_folder}; +use crate::util::args::{CommandWrapper, KaniArg, PassTo, RustcArg, encode_as_rustc_arg}; pub struct LibConfig { - args: Vec, + args: Vec, } impl LibConfig { @@ -28,7 +28,7 @@ impl LibConfig { "--extern", kani_std_wrapper.as_str(), ] - .map(OsString::from) + .map(RustcArg::from) .to_vec(); LibConfig { args } } @@ -36,7 +36,7 @@ impl LibConfig { pub fn new_no_core(path: PathBuf) -> LibConfig { LibConfig { args: ["-L", path.to_str().unwrap(), "--extern", "kani_core"] - .map(OsString::from) + .map(RustcArg::from) .to_vec(), } } @@ -52,13 +52,13 @@ impl KaniSession { outdir: &Path, ) -> Result<()> { let mut kani_args = self.kani_compiler_local_flags(); - kani_args.push(format!("--reachability={}", self.reachability_mode())); + kani_args.push(format!("--reachability={}", self.reachability_mode()).into()); let lib_path = lib_folder().unwrap(); let mut rustc_args = self.kani_rustc_flags(LibConfig::new(lib_path)); rustc_args.push(file.into()); rustc_args.push("--out-dir".into()); - rustc_args.push(OsString::from(outdir.as_os_str())); + rustc_args.push(RustcArg::from(outdir.as_os_str())); rustc_args.push("--crate-name".into()); rustc_args.push(crate_name.into()); @@ -79,8 +79,9 @@ impl KaniSession { // Note that the order of arguments is important. Kani specific flags should precede // rustc ones. let mut cmd = Command::new(&self.kani_compiler); - let kani_compiler_args = to_rustc_arg(kani_args); - cmd.arg(kani_compiler_args).args(rustc_args); + + cmd.pass_rustc_arg(encode_as_rustc_arg(&kani_args), PassTo::OnlyLocalCrate) + .pass_rustc_args(&rustc_args, PassTo::OnlyLocalCrate); // This is only required for stable but is a no-op for nightly channels cmd.env("RUSTC_BOOTSTRAP", "1"); @@ -94,13 +95,13 @@ impl KaniSession { } /// Create a compiler option that represents the reachability mode. - pub fn reachability_arg(&self) -> String { - format!("--reachability={}", self.reachability_mode()) + pub fn reachability_arg(&self) -> KaniArg { + format!("--reachability={}", self.reachability_mode()).into() } /// The `kani-compiler`-specific arguments that should be passed when building all crates, /// including dependencies. - pub fn kani_compiler_dependency_flags(&self) -> Vec { + pub fn kani_compiler_dependency_flags(&self) -> Vec { let mut flags = vec![check_version()]; if self.args.ignore_global_asm { @@ -112,8 +113,8 @@ impl KaniSession { /// The `kani-compiler`-specific arguments that should be passed only to the local crate /// being compiled. - pub fn kani_compiler_local_flags(&self) -> Vec { - let mut flags = vec![]; + pub fn kani_compiler_local_flags(&self) -> Vec { + let mut flags: Vec = vec![]; if self.args.common_args.debug { flags.push("--log-level=debug".into()); @@ -163,7 +164,7 @@ impl KaniSession { } for harness in &self.args.harnesses { - flags.push(format!("--harness {harness}")); + flags.push(format!("--harness {harness}").into()); } if self.args.exact { @@ -171,21 +172,21 @@ impl KaniSession { } if let Some(args) = self.autoharness_compiler_flags.clone() { - flags.extend(args); + flags.extend(args.into_iter().map(KaniArg::from)); } - flags.extend(self.args.common_args.unstable_features.as_arguments().map(str::to_string)); + flags.extend(self.args.common_args.unstable_features.as_arguments().map(KaniArg::from)); flags } /// This function generates all rustc configurations required by our goto-c codegen. - pub fn kani_rustc_flags(&self, lib_config: LibConfig) -> Vec { + pub fn kani_rustc_flags(&self, lib_config: LibConfig) -> Vec { let mut flags: Vec<_> = base_rustc_flags(lib_config); // We only use panic abort strategy for verification since we cannot handle unwind logic. if self.args.coverage { flags.extend_from_slice( - &["-C", "instrument-coverage", "-Z", "no-profiler-runtime"].map(OsString::from), + &["-C", "instrument-coverage", "-Z", "no-profiler-runtime"].map(RustcArg::from), ); } flags.extend_from_slice( @@ -202,7 +203,7 @@ impl KaniSession { // Do not invoke the linker since the compiler will not generate real object files "-Clinker=echo", ] - .map(OsString::from), + .map(RustcArg::from), ); if self.args.no_codegen { @@ -232,7 +233,7 @@ impl KaniSession { } /// Common flags used for compiling user code for verification and playback flow. -pub fn base_rustc_flags(lib_config: LibConfig) -> Vec { +pub fn base_rustc_flags(lib_config: LibConfig) -> Vec { let mut flags = [ "-C", "overflow-checks=on", @@ -250,7 +251,7 @@ pub fn base_rustc_flags(lib_config: LibConfig) -> Vec { "-Z", "crate-attr=register_tool(kanitool)", ] - .map(OsString::from) + .map(RustcArg::from) .to_vec(); flags.extend(lib_config.args); @@ -258,30 +259,16 @@ pub fn base_rustc_flags(lib_config: LibConfig) -> Vec { // e.g. compiletest will set 'compile-flags' here and we should pass those down to rustc // and we fail in `tests/kani/Match/match_bool.rs` if let Ok(str) = std::env::var("RUSTFLAGS") { - flags.extend(str.split(' ').map(OsString::from)); + flags.extend(str.split(' ').map(RustcArg::from)); } flags } -/// This function can be used to convert Kani compiler specific arguments into a rustc one. -/// We currently pass Kani specific arguments using the `--llvm-args` structure which is the -/// hacky mechanism used by other rustc backend to receive arguments unknown to rustc. -/// -/// Note that Cargo caching mechanism takes the building context into consideration, which -/// includes the value of the rust flags. By using `--llvm-args`, we ensure that Cargo takes into -/// consideration all arguments that are used to configure Kani compiler. For example, enabling the -/// reachability checks will force recompilation if they were disabled in previous build. -/// For more details on this caching mechanism, see the -/// [fingerprint documentation](https://github.com/rust-lang/cargo/blob/82c3bb79e3a19a5164e33819ef81bfc2c984bc56/src/cargo/core/compiler/fingerprint/mod.rs) -pub fn to_rustc_arg(kani_args: Vec) -> String { - format!(r#"-Cllvm-args={}"#, kani_args.join(" ")) -} - /// Function that returns a `--check-version` argument to be added to the compiler flags. /// This is really just used to force the compiler to recompile everything from scratch when a user /// upgrades Kani. Cargo currently ignores the codegen backend version. /// See for more context. -fn check_version() -> String { - format!("--check-version={}", env!("CARGO_PKG_VERSION")) +fn check_version() -> KaniArg { + format!("--check-version={}", env!("CARGO_PKG_VERSION")).into() } diff --git a/kani-driver/src/concrete_playback/playback.rs b/kani-driver/src/concrete_playback/playback.rs index 833fdbc02384..6ee8398e6c91 100644 --- a/kani-driver/src/concrete_playback/playback.rs +++ b/kani-driver/src/concrete_playback/playback.rs @@ -8,9 +8,9 @@ use crate::args::playback_args::{CargoPlaybackArgs, KaniPlaybackArgs, MessageFor use crate::call_cargo::cargo_config_args; use crate::call_single_file::{LibConfig, base_rustc_flags}; use crate::session::{InstallType, lib_playback_folder, setup_cargo_command}; +use crate::util::args::{CargoArg, CommandWrapper, PassTo, RustcArg}; use crate::{session, util}; use anyhow::Result; -use std::ffi::OsString; use std::ops::Deref; use std::path::{Path, PathBuf}; use std::process::Command; @@ -72,7 +72,7 @@ fn build_test(install: &InstallType, args: &KaniPlaybackArgs) -> Result let mut rustc_args = base_rustc_flags(LibConfig::new(lib_playback_folder()?)); rustc_args.push("--test".into()); - rustc_args.push(OsString::from(&args.input)); + rustc_args.push(RustcArg::from(&args.input)); rustc_args.push(format!("--crate-name={TEST_BIN_NAME}").into()); if args.playback.common_opts.verbose() { @@ -84,7 +84,7 @@ fn build_test(install: &InstallType, args: &KaniPlaybackArgs) -> Result } let mut cmd = Command::new(install.kani_compiler()?); - cmd.args(rustc_args); + cmd.pass_rustc_args(&rustc_args, PassTo::OnlyLocalCrate); session::run_terminal(&args.playback.common_opts, cmd)?; @@ -97,7 +97,7 @@ fn cargo_test(args: CargoPlaybackArgs) -> Result<()> { let mut cmd = setup_cargo_command()?; let rustc_args = base_rustc_flags(LibConfig::new(lib_playback_folder()?)); - let mut cargo_args: Vec = vec!["test".into()]; + let mut cargo_args: Vec = vec!["test".into()]; if args.playback.common_opts.verbose() { cargo_args.push("-vv".into()); @@ -123,11 +123,9 @@ fn cargo_test(args: CargoPlaybackArgs) -> Result<()> { } // Arguments that will only be passed to the target package. - cmd.args(&cargo_args) + cmd.pass_cargo_args(&cargo_args) .env("RUSTC", &install.kani_compiler()?) - // Use CARGO_ENCODED_RUSTFLAGS instead of RUSTFLAGS is preferred. See - // https://doc.rust-lang.org/cargo/reference/environment-variables.html - .env("CARGO_ENCODED_RUSTFLAGS", rustc_args.join(&OsString::from("\x1f"))) + .pass_rustc_args(&rustc_args, PassTo::AllCrates) .env("CARGO_TERM_PROGRESS_WHEN", "never"); session::run_terminal(&args.playback.common_opts, cmd)?; diff --git a/kani-driver/src/util.rs b/kani-driver/src/util.rs index bfbf9e8d04e1..b4a3dd954b7e 100644 --- a/kani-driver/src/util.rs +++ b/kani-driver/src/util.rs @@ -88,6 +88,158 @@ pub fn info_operation(op: &str, msg: &str) { println!("{op_fmt} {msg_fmt}") } +/// # Kani Argument Types +/// +/// We have three different kinds of arguments we use to influence our compilation process. +/// This module provides specific types, conversions and methods for each to codify the differences betweeen them +/// and how they can be used. +/// +/// [`KaniArg`](args::KaniArg) -- First, there are Kani-specific arguments that the `kani-compiler` uses to direct its codegen. +/// (See the [`Arguments`] struct in `kani-compiler/src/args.rs` for how they are parsed.) +/// +/// [`RustcArg`](args::RustcArg) -- Before codegen, the Kani compiler invokes `rustc` to compile Rust source to MIR, +/// and we pass arguments to influence that compilation. +/// See [`LibConfig`](crate::call_single_file::LibConfig) for how this is used in a typical Kani invocation. +/// +/// [`CargoArg`](args::CargoArg) -- If we're calling Kani through `cargo kani`, we also want to pass arguments to Cargo itself. +/// +/// +/// ## Usage +/// * `CargoArg`s can be passed directly to a `&mut Command` using the `pass_cargo_args` method. +/// * `RustcArg`s can also be passed directly to a `&mut Command` using the `pass_rustc_arg{s}` methods, +/// with the added wrinkle that you have to specify whether they should be passed to all crates or just the local one. +/// If passing to `AllCrates`, this uses an environment variable, meaning that it can only be called once for each Command. +/// * Any `KaniArg`s must first be converted into a single `RustcArg` with the [`to_rustc_arg`](args::to_rustc_arg) function +/// before being passed to commands the same way as other `RustcArg`s. We do this (rather than having a separate +/// `pass_kani_args` function) because both kinds of arguments often have to share the same underlying `pass_rustc_args` +/// call to ensure the environment variables for one doesn't get overwritten as mentioned above. +pub(crate) mod args { + use std::{ + ffi::{OsStr, OsString}, + process::Command, + }; + + #[derive(Clone, PartialEq)] + /// Kani-specific arguments passed to `rustc` and then used by `kani-compiler`. + pub struct KaniArg(String); + + #[derive(Clone, PartialEq, Debug)] + /// Arguments passed to `rustc`. + pub struct RustcArg(OsString); + + #[derive(Clone, PartialEq)] + /// Arguments passed to Cargo. + pub struct CargoArg(OsString); + + macro_rules! from_impl { + ($type:tt, $inner:ty) => { + impl From for $type + where + T: Into<$inner>, + { + fn from(value: T) -> Self { + $type(value.into()) + } + } + + impl $type { + /// Get a reference to this argument's underlying type. + pub fn as_inner(&self) -> &$inner { + &self.0 + } + } + }; + } + + from_impl!(KaniArg, String); + from_impl!(RustcArg, OsString); + from_impl!(CargoArg, OsString); + + /// This function can be used to convert Kani compiler specific arguments into a rustc one. + /// We currently pass Kani specific arguments using the `--llvm-args` structure which is the + /// hacky mechanism used by other rustc backend to receive arguments unknown to rustc. + /// + /// Note that Cargo caching mechanism takes the building context into consideration, which + /// includes the value of the rust flags. By using `--llvm-args`, we ensure that Cargo takes into + /// consideration all arguments that are used to configure Kani compiler. For example, enabling the + /// reachability checks will force recompilation if they were disabled in previous build. + /// For more details on this caching mechanism, see the + /// [fingerprint documentation](https://github.com/rust-lang/cargo/blob/82c3bb79e3a19a5164e33819ef81bfc2c984bc56/src/cargo/core/compiler/fingerprint/mod.rs) + pub fn encode_as_rustc_arg<'a>(kani_args: impl IntoIterator) -> RustcArg { + format!( + r#"-Cllvm-args={}"#, + kani_args + .into_iter() + .map(KaniArg::as_inner) + .cloned() + .collect::>() + .join(" ") + ) + .into() + } + + pub enum PassTo { + /// Only pass arguments for use in the local crate. + /// This will just pass them directly as arguments to the command. + OnlyLocalCrate, + /// Pass arguments for use when compiling all dependencies using the + /// `CARGO_ENCODED_RUSTFLAGS` environment variable. + AllCrates, + } + + /// A wrapper trait that allows us to call these methods on a [Command]. + pub trait CommandWrapper { + fn pass_cargo_args(&mut self, args: &[CargoArg]) -> &mut Self; + fn pass_rustc_args(&mut self, args: &[RustcArg], to: PassTo) -> &mut Self; + fn pass_rustc_arg(&mut self, args: RustcArg, to: PassTo) -> &mut Self; + } + + impl CommandWrapper for Command { + /// Pass general arguments to cargo. + fn pass_cargo_args(&mut self, args: &[CargoArg]) -> &mut Self { + self.args(args.iter().map(CargoArg::as_inner)) + } + + fn pass_rustc_arg(&mut self, args: RustcArg, to: PassTo) -> &mut Self { + self.pass_rustc_args(&[args], to) + } + + /// Pass rustc arguments to the compiler for use in certain dependencies. + fn pass_rustc_args(&mut self, args: &[RustcArg], to: PassTo) -> &mut Self { + match to { + // Since we just want to pass to the local crate, just add them as arguments to the command. + PassTo::OnlyLocalCrate => self.args(args.iter().map(RustcArg::as_inner)), + + // Since we also want to recursively pass these args to all dependencies, + // use an environment variable that gets checked for each dependency. + PassTo::AllCrates => { + // Use of CARGO_ENCODED_RUSTFLAGS instead of RUSTFLAGS is preferred. See + // https://doc.rust-lang.org/cargo/reference/environment-variables.html + let env_var = OsString::from("CARGO_ENCODED_RUSTFLAGS"); + + // Ensure we wouldn't be overwriting an existing environment variable. + let env_var_exists = self.get_envs().any(|(var, _)| var == env_var); + assert!( + !env_var_exists, + "pass_rustc_args() uses an environment variable when called with `PassTo::AllCrates`, \ + so calling it multiple times in this way will overwrite all but the most recent call. \ + try combining the arguments you want to add and passing them to a single call instead." + ); + + self.env( + "CARGO_ENCODED_RUSTFLAGS", + args.iter() + .map(RustcArg::as_inner) + .cloned() + .collect::>() + .join(OsStr::new("\x1f")), + ) + } + } + } + } +} + #[cfg(test)] mod tests { use super::*;