diff --git a/.changes/931.json b/.changes/931.json index 50d3d5065..b0a487844 100644 --- a/.changes/931.json +++ b/.changes/931.json @@ -1,4 +1,12 @@ -{ - "description": "deny installation of debian packages that conflict with our cross-compiler toolchains.", - "type": "fixed" -} +[ + { + "description": "add support for cargo aliases.", + "type": "added", + "issues": [562] + }, + { + "description": "allow users to ignore config files in the package.", + "type": "added", + "issues": [621] + } +] diff --git a/.changes/933.json b/.changes/933.json new file mode 100644 index 000000000..50d3d5065 --- /dev/null +++ b/.changes/933.json @@ -0,0 +1,4 @@ +{ + "description": "deny installation of debian packages that conflict with our cross-compiler toolchains.", + "type": "fixed" +} diff --git a/docs/cross_toml.md b/docs/cross_toml.md index 87020a792..5488d42ca 100644 --- a/docs/cross_toml.md +++ b/docs/cross_toml.md @@ -21,6 +21,7 @@ For example: ```toml [build.env] +cargo-config = "complete" volumes = ["VOL1_ARG", "VOL2_ARG"] passthrough = ["IMPORTANT_ENV_VARIABLES"] ``` @@ -73,6 +74,7 @@ This is similar to `build.env`, but allows you to be more specific per target. ```toml [target.x86_64-unknown-linux-gnu.env] +cargo-config = "ignore" volumes = ["VOL1_ARG", "VOL2_ARG"] passthrough = ["IMPORTANT_ENV_VARIABLES"] ``` diff --git a/src/bin/commands/containers.rs b/src/bin/commands/containers.rs index 973609d9e..cacab7f9b 100644 --- a/src/bin/commands/containers.rs +++ b/src/bin/commands/containers.rs @@ -392,7 +392,7 @@ pub fn create_persistent_volume( channel: Option<&Toolchain>, msg_info: &mut MessageInfo, ) -> cross::Result<()> { - let config = cross::config::Config::new(None); + let config = cross::CrossConfig::new(None); let toolchain_host: cross::Target = toolchain.into(); let mut toolchain = QualifiedToolchain::default(&config, msg_info)?; toolchain.replace_host(&ImagePlatform::from_target( @@ -483,7 +483,7 @@ pub fn remove_persistent_volume( channel: Option<&Toolchain>, msg_info: &mut MessageInfo, ) -> cross::Result<()> { - let config = cross::config::Config::new(None); + let config = cross::CrossConfig::new(None); let target_host: cross::Target = toolchain.into(); let mut toolchain = QualifiedToolchain::default(&config, msg_info)?; toolchain.replace_host(&ImagePlatform::from_target(target_host.target().clone())?); diff --git a/src/bin/cross.rs b/src/bin/cross.rs index 64536d296..de080f222 100644 --- a/src/bin/cross.rs +++ b/src/bin/cross.rs @@ -8,7 +8,7 @@ use std::{ use cross::{ cargo, cli, rustc, shell::{self, Verbosity}, - OutputExt, Subcommand, + CargoConfig, CargoToml, OutputExt, Subcommand, }; pub fn main() -> cross::Result<()> { @@ -16,10 +16,12 @@ pub fn main() -> cross::Result<()> { cross::install_termination_hook()?; let target_list = rustc::target_list(&mut Verbosity::Quiet.into())?; - let args = cli::parse(&target_list)?; + let cargo_toml = CargoToml::read()?; + let cargo_config = CargoConfig::new(cargo_toml); + let args = cli::parse(&target_list, &cargo_config)?; let subcommand = args.subcommand; let mut msg_info = shell::MessageInfo::create(args.verbose, args.quiet, args.color.as_deref())?; - let status = match cross::run(args, target_list, &mut msg_info)? { + let status = match cross::run(args, target_list, cargo_config, &mut msg_info)? { Some(status) => status, None => { // if we fallback to the host cargo, use the same invocation that was made to cross diff --git a/src/cargo_config.rs b/src/cargo_config.rs new file mode 100644 index 000000000..45ac34d52 --- /dev/null +++ b/src/cargo_config.rs @@ -0,0 +1,64 @@ +use std::collections::HashMap; + +use crate::cargo_toml::CargoToml; +use crate::config::{split_to_cloned_by_ws, Environment}; +use crate::errors::*; + +pub const CARGO_NO_PREFIX_ENVVARS: &[&str] = &[ + "http_proxy", + "TERM", + "RUSTDOCFLAGS", + "RUSTFLAGS", + "BROWSER", + "HTTPS_PROXY", + "HTTP_TIMEOUT", + "https_proxy", +]; + +#[derive(Debug)] +struct CargoEnvironment(Environment); + +impl CargoEnvironment { + fn new(map: Option>) -> Self { + CargoEnvironment(Environment::new("CARGO", map)) + } + + pub fn alias(&self, name: &str) -> Option> { + let key = format!("ALIAS_{name}"); + self.0 + .get_var(&self.0.var_name(&key)) + .map(|x| split_to_cloned_by_ws(&x)) + } +} + +#[derive(Debug)] +pub struct CargoConfig { + toml: Option, + env: CargoEnvironment, +} + +impl CargoConfig { + pub fn new(toml: Option) -> Self { + CargoConfig { + toml, + env: CargoEnvironment::new(None), + } + } + + pub fn alias(&self, name: &str) -> Result>> { + match self.env.alias(name) { + Some(alias) => Ok(Some(alias)), + None => match self.toml.as_ref() { + Some(t) => t.alias(name), + None => Ok(None), + }, + } + } + + pub fn to_toml(&self) -> Result> { + match self.toml.as_ref() { + Some(t) => Ok(Some(t.to_toml()?)), + None => Ok(None), + } + } +} diff --git a/src/cargo_toml.rs b/src/cargo_toml.rs new file mode 100644 index 000000000..fc9547a68 --- /dev/null +++ b/src/cargo_toml.rs @@ -0,0 +1,268 @@ +use std::collections::BTreeSet; +use std::env; +use std::path::Path; + +use crate::config::split_to_cloned_by_ws; +use crate::errors::*; +use crate::file; + +type Table = toml::value::Table; +type Value = toml::value::Value; + +// the strategy is to merge, with arrays merging together +// and the deeper the config file is, the higher its priority. +// arrays merge, numbers/strings get replaced, objects merge in. +// we don't want to make any assumptions about the cargo +// config data, in case we need to use it later. +#[derive(Debug, Clone, Default)] +pub struct CargoToml(Table); + +impl CargoToml { + fn parse(path: &Path) -> Result { + let contents = file::read(&path) + .wrap_err_with(|| format!("could not read cargo config file at `{path:?}`"))?; + Ok(CargoToml(toml::from_str(&contents)?)) + } + + pub fn to_toml(&self) -> Result { + toml::to_string(&self.0).map_err(Into::into) + } + + // finding cargo config files actually runs from the + // current working directory the command is invoked, + // not from the project root. same is true with work + // spaces: the project layout does not matter. + pub fn read() -> Result> { + // note: cargo supports both `config` and `config.toml` + // `config` exists for compatibility reasons, but if + // present, only it will be read. + let read = |dir: &Path| -> Result> { + let noext = dir.join("config"); + let ext = dir.join("config.toml"); + if noext.exists() { + Ok(Some(CargoToml::parse(&noext)?)) + } else if ext.exists() { + Ok(Some(CargoToml::parse(&ext)?)) + } else { + Ok(None) + } + }; + + let read_and_merge = |result: &mut Option, dir: &Path| -> Result<()> { + let parent = read(dir)?; + // can't use a match, since there's a use-after-move issue + match (result.as_mut(), parent) { + (Some(r), Some(p)) => r.merge(&p)?, + (None, Some(p)) => *result = Some(p), + (Some(_), None) | (None, None) => (), + } + + Ok(()) + }; + + let mut result = None; + let cwd = env::current_dir()?; + let mut dir: &Path = &cwd; + loop { + read_and_merge(&mut result, &dir.join(".cargo"))?; + let parent_dir = dir.parent(); + match parent_dir { + Some(path) => dir = path, + None => break, + } + } + + read_and_merge(&mut result, &home::cargo_home()?)?; + + Ok(result) + } + + fn merge(&mut self, parent: &CargoToml) -> Result<()> { + // can error on mismatched-data + + fn validate_types(x: &Value, y: &Value) -> Option<()> { + match x.same_type(y) { + true => Some(()), + false => None, + } + } + + // merge 2 tables. x has precedence over y. + fn merge_tables(x: &mut Table, y: &Table) -> Option<()> { + // we need to iterate over both keys, so we need a full deduplication + let keys: BTreeSet = x.keys().chain(y.keys()).cloned().collect(); + for key in keys { + let in_x = x.contains_key(&key); + let in_y = y.contains_key(&key); + match (in_x, in_y) { + (true, true) => { + // need to do our merge strategy + let xk = x.get_mut(&key)?; + let yk = y.get(&key)?; + validate_types(xk, yk)?; + + // now we've filtered out missing keys and optional values + // all key/value pairs should be same type. + if xk.is_table() { + merge_tables(xk.as_table_mut()?, yk.as_table()?)?; + } else if xk.is_array() { + xk.as_array_mut()?.extend_from_slice(yk.as_array()?); + } + } + (false, true) => { + // key in y is not in x: copy it over + let yk = y[&key].clone(); + x.insert(key, yk); + } + // key isn't present in y: can ignore it + (_, false) => (), + } + } + + Some(()) + } + + merge_tables(&mut self.0, &parent.0).ok_or_else(|| eyre::eyre!("could not merge")) + } + + pub fn alias(&self, name: &str) -> Result>> { + let parse_alias = |value: &Value| -> Result> { + if let Some(s) = value.as_str() { + Ok(split_to_cloned_by_ws(s)) + } else if let Some(a) = value.as_array() { + a.iter() + .map(|i| { + i.as_str() + .map(ToOwned::to_owned) + .ok_or_else(|| eyre::eyre!("invalid alias type, got {value}")) + }) + .collect() + } else { + eyre::bail!("invalid alias type, got {}", value.type_str()); + } + }; + + let alias = match self.0.get("alias") { + Some(a) => a, + None => return Ok(None), + }; + let table = match alias.as_table() { + Some(t) => t, + None => eyre::bail!("cargo config aliases must be a table"), + }; + + match table.get(name) { + Some(v) => Ok(Some(parse_alias(v)?)), + None => Ok(None), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + macro_rules! s { + ($s:literal) => { + $s.to_owned() + }; + } + + #[test] + fn test_parse() -> Result<()> { + let config1 = CargoToml(toml::from_str(CARGO_TOML1)?); + let config2 = CargoToml(toml::from_str(CARGO_TOML2)?); + assert_eq!(config1.alias("foo")?, Some(vec![s!("build"), s!("foo")])); + assert_eq!(config1.alias("bar")?, Some(vec![s!("check"), s!("bar")])); + assert_eq!(config2.alias("baz")?, Some(vec![s!("test"), s!("baz")])); + assert_eq!(config2.alias("bar")?, Some(vec![s!("init"), s!("bar")])); + assert_eq!(config1.alias("far")?, None); + assert_eq!(config2.alias("far")?, None); + + let mut merged = config1; + merged.merge(&config2)?; + assert_eq!(merged.alias("foo")?, Some(vec![s!("build"), s!("foo")])); + assert_eq!(merged.alias("baz")?, Some(vec![s!("test"), s!("baz")])); + assert_eq!(merged.alias("bar")?, Some(vec![s!("check"), s!("bar")])); + + // check our merge went well, with arrays, etc. + assert_eq!( + merged + .0 + .get("build") + .and_then(|x| x.get("jobs")) + .and_then(|x| x.as_integer()), + Some(2), + ); + assert_eq!( + merged + .0 + .get("build") + .and_then(|x| x.get("rustflags")) + .and_then(|x| x.as_array()) + .and_then(|x| x.iter().map(|i| i.as_str()).collect()), + Some(vec!["-C lto", "-Zbuild-std", "-Zdoctest-xcompile"]), + ); + + Ok(()) + } + + #[test] + fn test_read() -> Result<()> { + let config = CargoToml::read()?.expect("cross must have cargo config."); + assert_eq!( + config.alias("build-docker-image")?, + Some(vec![s!("xtask"), s!("build-docker-image")]) + ); + assert_eq!( + config.alias("xtask")?, + Some(vec![s!("run"), s!("-p"), s!("xtask"), s!("--")]) + ); + + Ok(()) + } + + const CARGO_TOML1: &str = r#" +[alias] +foo = "build foo" +bar = "check bar" + +[build] +jobs = 2 +rustc-wrapper = "sccache" +target = "x86_64-unknown-linux-gnu" +rustflags = ["-C lto", "-Zbuild-std"] +incremental = true + +[doc] +browser = "firefox" + +[env] +VAR1 = "VAL1" +VAR2 = { value = "VAL2", force = true } +VAR3 = { value = "relative/path", relative = true } +"#; + + const CARGO_TOML2: &str = r#" +# want to check tables merge +# want to check arrays concat +# want to check rest override +[alias] +baz = "test baz" +bar = "init bar" + +[build] +jobs = 4 +rustc-wrapper = "sccache" +target = "x86_64-unknown-linux-gnu" +rustflags = ["-Zdoctest-xcompile"] +incremental = true + +[doc] +browser = "chromium" + +[env] +VAR1 = "NEW1" +VAR2 = { value = "VAL2", force = false } +"#; +} diff --git a/src/cli.rs b/src/cli.rs index f5cf5986e..fa00e5b06 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -2,13 +2,14 @@ use std::env; use std::path::{Path, PathBuf}; use crate::cargo::Subcommand; +use crate::cargo_config::CargoConfig; use crate::errors::Result; use crate::file::{absolute_path, PathExt}; use crate::rustc::TargetList; use crate::shell::{self, MessageInfo}; use crate::Target; -#[derive(Debug)] +#[derive(Debug, Default)] pub struct Args { pub all: Vec, pub subcommand: Option, @@ -150,128 +151,174 @@ fn store_target_dir(_: String) -> Result { Ok("/target".to_owned()) } -pub fn parse(target_list: &TargetList) -> Result { - let mut channel = None; - let mut target = None; - let mut features = Vec::new(); - let mut manifest_path: Option = None; - let mut target_dir = None; - let mut sc = None; - let mut all: Vec = Vec::new(); - let mut version = false; - let mut quiet = false; - let mut verbose = false; - let mut color = None; - - { - let mut args = env::args().skip(1); - while let Some(arg) = args.next() { - if arg.is_empty() { - continue; +// we have an optional parent to check for unresolvable recursion +fn parse_subcommand( + arg: String, + result: &mut Args, + target_list: &TargetList, + config: &CargoConfig, + seen: &mut Vec, +) -> Result<()> { + if seen.iter().any(|x| x == &arg) { + let chain = seen.join(" -> "); + MessageInfo::default().fatal( + format_args!("alias {arg} has unresolvable recursive definition: {chain} -> {arg}"), + shell::ERROR_CODE, + ); + } + let subcommand = Subcommand::from(arg.as_ref()); + if subcommand == Subcommand::Other { + if let Some(alias) = config.alias(&arg)? { + seen.push(arg); + let mut iter = alias.iter().cloned(); + while let Some(subarg) = iter.next() { + parse_arg(subarg, result, target_list, config, seen, &mut iter)?; } - if is_verbose(arg.as_str()) { - verbose = true; - all.push(arg); - } else if matches!(arg.as_str(), "--version" | "-V") { - version = true; - } else if matches!(arg.as_str(), "--quiet" | "-q") { - quiet = true; - all.push(arg); - } else if let Some(kind) = is_value_arg(&arg, "--color") { - color = match kind { - ArgKind::Next => { - match parse_next_arg(arg, &mut all, str_to_owned, identity, &mut args)? { - Some(c) => Some(c), - None => shell::invalid_color(None), - } - } - ArgKind::Equal => Some(parse_equal_arg(arg, &mut all, str_to_owned, identity)?), - }; - } else if let Some(kind) = is_value_arg(&arg, "--manifest-path") { - manifest_path = match kind { - ArgKind::Next => parse_next_arg( - arg, - &mut all, - parse_manifest_path, - store_manifest_path, - &mut args, - )? - .flatten(), - ArgKind::Equal => { - parse_equal_arg(arg, &mut all, parse_manifest_path, store_manifest_path)? - } - }; - } else if let ("+", ch) = arg.split_at(1) { - channel = Some(ch.to_owned()); - } else if let Some(kind) = is_value_arg(&arg, "--target") { - let parse_target = |t: &str| Ok(Target::from(t, target_list)); - target = match kind { - ArgKind::Next => { - parse_next_arg(arg, &mut all, parse_target, identity, &mut args)? - } - ArgKind::Equal => Some(parse_equal_arg(arg, &mut all, parse_target, identity)?), - }; - } else if let Some(kind) = is_value_arg(&arg, "--features") { - match kind { - ArgKind::Next => { - let next = - parse_next_arg(arg, &mut all, str_to_owned, identity, &mut args)?; - if let Some(feature) = next { - features.push(feature); - } - } - ArgKind::Equal => { - features.push(parse_equal_arg(arg, &mut all, str_to_owned, identity)?); - } - } - } else if let Some(kind) = is_value_arg(&arg, "--target-dir") { - match kind { - ArgKind::Next => { - target_dir = parse_next_arg( - arg, - &mut all, - parse_target_dir, - store_target_dir, - &mut args, - )?; - } - ArgKind::Equal => { - target_dir = Some(parse_equal_arg( - arg, - &mut all, - parse_target_dir, - store_target_dir, - )?); - } + return Ok(()); + } + return Ok(()); + } + + // fallthrough + result.all.push(arg); + result.subcommand = Some(subcommand); + + Ok(()) +} + +fn parse_arg( + arg: String, + result: &mut Args, + target_list: &TargetList, + config: &CargoConfig, + seen: &mut Vec, + iter: &mut impl Iterator, +) -> Result<()> { + if arg.is_empty() { + return Ok(()); + } + if is_verbose(arg.as_str()) { + result.verbose = true; + result.all.push(arg); + } else if matches!(arg.as_str(), "--version" | "-V") { + result.version = true; + } else if matches!(arg.as_str(), "--quiet" | "-q") { + result.quiet = true; + result.all.push(arg); + } else if let Some(kind) = is_value_arg(&arg, "--color") { + result.color = match kind { + ArgKind::Next => { + match parse_next_arg(arg, &mut result.all, str_to_owned, identity, iter)? { + Some(c) => Some(c), + None => shell::invalid_color(None), } - } else { - if (!arg.starts_with('-') || arg == "--list") && sc.is_none() { - sc = Some(Subcommand::from(arg.as_ref())); + } + ArgKind::Equal => Some(parse_equal_arg( + arg, + &mut result.all, + str_to_owned, + identity, + )?), + }; + } else if let Some(kind) = is_value_arg(&arg, "--manifest-path") { + result.manifest_path = match kind { + ArgKind::Next => parse_next_arg( + arg, + &mut result.all, + parse_manifest_path, + store_manifest_path, + iter, + )? + .flatten(), + ArgKind::Equal => parse_equal_arg( + arg, + &mut result.all, + parse_manifest_path, + store_manifest_path, + )?, + }; + } else if let ("+", ch) = arg.split_at(1) { + result.channel = Some(ch.to_owned()); + } else if let Some(kind) = is_value_arg(&arg, "--target") { + println!("target={:?}", arg); + let parse_target = |t: &str| Ok(Target::from(t, target_list)); + result.target = match kind { + ArgKind::Next => parse_next_arg(arg, &mut result.all, parse_target, identity, iter)?, + ArgKind::Equal => Some(parse_equal_arg( + arg, + &mut result.all, + parse_target, + identity, + )?), + }; + } else if let Some(kind) = is_value_arg(&arg, "--features") { + match kind { + ArgKind::Next => { + let next = parse_next_arg(arg, &mut result.all, str_to_owned, identity, iter)?; + if let Some(feature) = next { + result.features.push(feature); } - - all.push(arg.clone()); + } + ArgKind::Equal => { + result.features.push(parse_equal_arg( + arg, + &mut result.all, + str_to_owned, + identity, + )?); + } + } + } else if let Some(kind) = is_value_arg(&arg, "--target-dir") { + match kind { + ArgKind::Next => { + result.target_dir = parse_next_arg( + arg, + &mut result.all, + parse_target_dir, + store_target_dir, + iter, + )?; + } + ArgKind::Equal => { + result.target_dir = Some(parse_equal_arg( + arg, + &mut result.all, + parse_target_dir, + store_target_dir, + )?); } } + } else if (!arg.starts_with('-') || arg == "--list") && result.subcommand.is_none() { + parse_subcommand(arg, result, target_list, config, seen)?; + } else { + result.all.push(arg.clone()); } - Ok(Args { - all, - subcommand: sc, - channel, - target, - features, - target_dir, - manifest_path, - version, - verbose, - quiet, - color, - }) + Ok(()) +} + +pub fn parse(target_list: &TargetList, config: &CargoConfig) -> Result { + let mut result = Args::default(); + let mut args = env::args().skip(1); + let mut seen = vec![]; + while let Some(arg) = args.next() { + parse_arg(arg, &mut result, target_list, config, &mut seen, &mut args)?; + } + + Ok(result) } #[cfg(test)] mod tests { use super::*; + use crate::rustc; + use crate::shell::Verbosity; + + macro_rules! s { + ($s:literal) => { + $s.to_owned() + }; + } #[test] fn is_verbose_test() { @@ -284,4 +331,57 @@ mod tests { assert!(is_verbose("-vvvv")); assert!(!is_verbose("-version")); } + + #[test] + fn test_nested_alias() { + let mut args = Args::default(); + let target_list = + rustc::target_list(&mut Verbosity::Quiet.into()).expect("failed to get target list"); + let config = CargoConfig::new(None); + + parse_subcommand( + "g".to_owned(), + &mut args, + &target_list, + &config, + &mut vec![s!("x"), s!("y"), s!("z"), s!("a"), s!("e"), s!("f")], + ) + .ok(); + } + + #[test] + #[should_panic] + fn test_recursive_alias() { + let mut args = Args::default(); + let target_list = + rustc::target_list(&mut Verbosity::Quiet.into()).expect("failed to get target list"); + let config = CargoConfig::new(None); + + parse_subcommand( + "recursive".to_owned(), + &mut args, + &target_list, + &config, + &mut vec![s!("recursive")], + ) + .ok(); + } + + #[test] + #[should_panic] + fn test_nested_recursive_alias() { + let mut args = Args::default(); + let target_list = + rustc::target_list(&mut Verbosity::Quiet.into()).expect("failed to get target list"); + let config = CargoConfig::new(None); + + parse_subcommand( + "y".to_owned(), + &mut args, + &target_list, + &config, + &mut vec![s!("x"), s!("y"), s!("z"), s!("a")], + ) + .ok(); + } } diff --git a/src/config.rs b/src/config.rs index 9cc4d13ef..2cffa03c2 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,150 +1,9 @@ -use crate::docker::custom::PreBuild; -use crate::docker::{ImagePlatform, PossibleImage}; -use crate::shell::MessageInfo; -use crate::{CrossToml, Result, Target, TargetList}; - use std::collections::HashMap; use std::env; use std::str::FromStr; -#[derive(Debug)] -struct Environment(&'static str, Option>); - -impl Environment { - fn new(map: Option>) -> Self { - Environment("CROSS", map) - } - - fn build_var_name(&self, name: &str) -> String { - format!("{}_{}", self.0, name.to_ascii_uppercase().replace('-', "_")) - } - - fn get_var(&self, name: &str) -> Option { - self.1 - .as_ref() - .and_then(|internal_map| internal_map.get(name).map(|v| (*v).to_owned())) - .or_else(|| env::var(name).ok()) - } - - fn get_values_for( - &self, - var: &str, - target: &Target, - convert: impl Fn(&str) -> T, - ) -> (Option, Option) { - let target_values = self.get_target_var(target, var).map(|ref s| convert(s)); - - let build_values = self.get_build_var(var).map(|ref s| convert(s)); - - (build_values, target_values) - } - - fn target_path(target: &Target, key: &str) -> String { - format!("TARGET_{target}_{key}") - } - - fn build_path(key: &str) -> String { - if !key.starts_with("BUILD_") { - format!("BUILD_{key}") - } else { - key.to_owned() - } - } - - fn get_build_var(&self, key: &str) -> Option { - self.get_var(&self.build_var_name(&Self::build_path(key))) - } - - fn get_target_var(&self, target: &Target, key: &str) -> Option { - self.get_var(&self.build_var_name(&Self::target_path(target, key))) - } - - fn xargo(&self, target: &Target) -> (Option, Option) { - self.get_values_for("XARGO", target, bool_from_envvar) - } - - fn build_std(&self, target: &Target) -> (Option, Option) { - self.get_values_for("BUILD_STD", target, bool_from_envvar) - } - - fn image(&self, target: &Target) -> Result> { - self.get_target_var(target, "IMAGE") - .map(Into::into) - .map(|mut i: PossibleImage| { - if let Some(toolchain) = self.get_target_var(target, "IMAGE_TOOLCHAIN") { - i.toolchain = toolchain - .split(',') - .map(|t| ImagePlatform::from_target(t.trim().into())) - .collect::>>()?; - Ok(i) - } else { - Ok(i) - } - }) - .transpose() - } - - fn dockerfile(&self, target: &Target) -> (Option, Option) { - self.get_values_for("DOCKERFILE", target, |s| s.to_owned()) - } - - fn dockerfile_context(&self, target: &Target) -> (Option, Option) { - self.get_values_for("DOCKERFILE_CONTEXT", target, |s| s.to_owned()) - } - - fn pre_build(&self, target: &Target) -> (Option, Option) { - self.get_values_for("PRE_BUILD", target, |v| { - let v: Vec<_> = v.split('\n').map(String::from).collect(); - if v.len() == 1 { - PreBuild::Single { - line: v.into_iter().next().expect("should contain one item"), - env: true, - } - } else { - PreBuild::Lines(v) - } - }) - } - - fn runner(&self, target: &Target) -> Option { - self.get_target_var(target, "RUNNER") - } - - fn passthrough(&self, target: &Target) -> (Option>, Option>) { - self.get_values_for("ENV_PASSTHROUGH", target, split_to_cloned_by_ws) - } - - fn volumes(&self, target: &Target) -> (Option>, Option>) { - self.get_values_for("ENV_VOLUMES", target, split_to_cloned_by_ws) - } - - fn target(&self) -> Option { - self.get_build_var("TARGET") - .or_else(|| std::env::var("CARGO_BUILD_TARGET").ok()) - } - - fn doctests(&self) -> Option { - self.get_var("CROSS_UNSTABLE_ENABLE_DOCTESTS") - .map(|s| bool_from_envvar(&s)) - } - - fn custom_toolchain(&self) -> bool { - self.get_var("CROSS_CUSTOM_TOOLCHAIN") - .map_or(false, |s| bool_from_envvar(&s)) - } - - fn custom_toolchain_compat(&self) -> Option { - self.get_var("CUSTOM_TOOLCHAIN_COMPAT") - } - - fn build_opts(&self) -> Option { - self.get_var("CROSS_BUILD_OPTS") - } -} - -fn split_to_cloned_by_ws(string: &str) -> Vec { - string.split_whitespace().map(String::from).collect() -} +use crate::cargo_config::CargoConfig; +use crate::cross_config::CrossConfig; pub fn bool_from_envvar(envvar: &str) -> bool { if let Ok(value) = bool::from_str(envvar) { @@ -156,630 +15,38 @@ pub fn bool_from_envvar(envvar: &str) -> bool { } } -#[derive(Debug)] -pub struct Config { - toml: Option, - env: Environment, +pub fn split_to_cloned_by_ws(string: &str) -> Vec { + string.split_whitespace().map(String::from).collect() } -impl Config { - pub fn new(toml: Option) -> Self { - Config { - toml, - env: Environment::new(None), - } - } - - pub fn confusable_target(&self, target: &Target, msg_info: &mut MessageInfo) -> Result<()> { - if let Some(keys) = self.toml.as_ref().map(|t| t.targets.keys()) { - for mentioned_target in keys { - let mentioned_target_norm = mentioned_target - .to_string() - .replace(|c| c == '-' || c == '_', "") - .to_lowercase(); - let target_norm = target - .to_string() - .replace(|c| c == '-' || c == '_', "") - .to_lowercase(); - if mentioned_target != target && mentioned_target_norm == target_norm { - msg_info.warn("a target named \"{mentioned_target}\" is mentioned in the Cross configuration, but the current specified target is \"{target}\".")?; - msg_info.status(" > Is the target misspelled in the Cross configuration?")?; - } - } - } - Ok(()) - } - - fn bool_from_config( - &self, - target: &Target, - env: impl Fn(&Environment, &Target) -> (Option, Option), - config: impl Fn(&CrossToml, &Target) -> (Option, Option), - ) -> Option { - let (env_build, env_target) = env(&self.env, target); - let (toml_build, toml_target) = if let Some(ref toml) = self.toml { - config(toml, target) - } else { - (None, None) - }; - - match (env_target, toml_target) { - (Some(value), _) => return Some(value), - (None, Some(value)) => return Some(value), - (None, None) => {} - }; - - match (env_build, toml_build) { - (Some(value), _) => return Some(value), - (None, Some(value)) => return Some(value), - (None, None) => {} - }; - - None - } - - fn vec_from_config( - &self, - target: &Target, - env: impl for<'a> Fn(&'a Environment, &Target) -> (Option>, Option>), - config: impl for<'a> Fn(&'a CrossToml, &Target) -> (Option<&'a [String]>, Option<&'a [String]>), - sum: bool, - ) -> Result>> { - if sum { - let (mut env_build, env_target) = env(&self.env, target); - env_build - .as_mut() - .map(|b| env_target.map(|mut t| b.append(&mut t))); - self.sum_of_env_toml_values(env_build, |t| config(t, target)) - } else { - self.get_from_ref(target, env, config) - } - } - - fn get_from_ref( - &self, - target: &Target, - env: impl for<'a> Fn(&'a Environment, &Target) -> (Option, Option), - config: impl for<'a> Fn(&'a CrossToml, &Target) -> (Option<&'a U>, Option<&'a U>), - ) -> Result> - where - U: ToOwned + ?Sized, - { - let (env_build, env_target) = env(&self.env, target); - - if let Some(env_target) = env_target { - return Ok(Some(env_target)); - } - - let (build, target) = self - .toml - .as_ref() - .map(|t| config(t, target)) - .unwrap_or_default(); - - // FIXME: let expression - if target.is_none() && env_build.is_some() { - return Ok(env_build); - } - - if target.is_none() { - Ok(build.map(ToOwned::to_owned)) - } else { - Ok(target.map(ToOwned::to_owned)) - } - } - - #[cfg(test)] - fn new_with(toml: Option, env: Environment) -> Self { - Config { toml, env } - } - - pub fn xargo(&self, target: &Target) -> Option { - self.bool_from_config(target, Environment::xargo, CrossToml::xargo) - } - - pub fn build_std(&self, target: &Target) -> Option { - self.bool_from_config(target, Environment::build_std, CrossToml::build_std) - } - - pub fn image(&self, target: &Target) -> Result> { - let env = self.env.image(target)?; - self.get_from_ref( - target, - move |_, _| (None, env.clone()), - |toml, target| (None, toml.image(target)), - ) - } - - pub fn runner(&self, target: &Target) -> Result> { - self.get_from_ref( - target, - |env, target| (None, env.runner(target)), - |toml, target| (None, toml.runner(target)), - ) - } - - pub fn doctests(&self) -> Option { - self.env.doctests() - } - - pub fn custom_toolchain(&self) -> bool { - self.env.custom_toolchain() - } - - pub fn custom_toolchain_compat(&self) -> Option { - self.env.custom_toolchain_compat() - } - - pub fn build_opts(&self) -> Option { - self.env.build_opts() - } - - pub fn env_passthrough(&self, target: &Target) -> Result>> { - self.vec_from_config( - target, - Environment::passthrough, - CrossToml::env_passthrough, - true, - ) - } - - pub fn env_volumes(&self, target: &Target) -> Result>> { - self.get_from_ref(target, Environment::volumes, CrossToml::env_volumes) - } - - pub fn target(&self, target_list: &TargetList) -> Option { - if let Some(env_value) = self.env.target() { - return Some(Target::from(&env_value, target_list)); - } - self.toml - .as_ref() - .and_then(|t| t.default_target(target_list)) - } +#[derive(Debug)] +pub struct Environment(&'static str, Option>); - pub fn dockerfile(&self, target: &Target) -> Result> { - self.get_from_ref(target, Environment::dockerfile, CrossToml::dockerfile) +impl Environment { + pub fn new(name: &'static str, map: Option>) -> Self { + Environment(name, map) } - pub fn dockerfile_context(&self, target: &Target) -> Result> { - self.get_from_ref( - target, - Environment::dockerfile_context, - CrossToml::dockerfile_context, - ) + pub fn var_name(&self, name: &str) -> String { + format!("{}_{}", self.0, name.to_ascii_uppercase().replace('-', "_")) } - pub fn dockerfile_build_args( - &self, - target: &Target, - ) -> Result>> { - // This value does not support env variables - self.toml + pub fn get_var(&self, name: &str) -> Option { + self.1 .as_ref() - .map_or(Ok(None), |t| Ok(t.dockerfile_build_args(target))) - } - - pub fn pre_build(&self, target: &Target) -> Result> { - self.get_from_ref(target, Environment::pre_build, CrossToml::pre_build) - } - - // FIXME: remove when we disable sums in 0.3.0. - fn sum_of_env_toml_values<'a>( - &'a self, - env_values: Option>, - toml_getter: impl FnOnce(&'a CrossToml) -> (Option<&'a [String]>, Option<&'a [String]>), - ) -> Result>> { - let mut defined = false; - let mut collect = vec![]; - if let Some(vars) = env_values { - collect.extend(vars.as_ref().iter().cloned()); - defined = true; - } else if let Some((build, target)) = self.toml.as_ref().map(toml_getter) { - if let Some(build) = build { - collect.extend(build.iter().cloned()); - defined = true; - } - if let Some(target) = target { - collect.extend(target.iter().cloned()); - defined = true; - } - } - if !defined { - Ok(None) - } else { - Ok(Some(collect)) - } + .and_then(|internal_map| internal_map.get(name).map(|v| (*v).to_owned())) + .or_else(|| env::var(name).ok()) } } -pub fn opt_merge + IntoIterator>( - opt1: Option, - opt2: Option, -) -> Option { - match (opt1, opt2) { - (None, None) => None, - (None, Some(opt2)) => Some(opt2), - (Some(opt1), None) => Some(opt1), - (Some(opt1), Some(opt2)) => { - let mut res = opt2; - res.extend(opt1); - Some(res) - } - } +#[derive(Debug)] +pub struct Config { + pub cargo: CargoConfig, + pub cross: CrossConfig, } -#[cfg(test)] -mod tests { - use super::*; - use crate::errors::*; - use crate::{Target, TargetList}; - - fn target_list() -> TargetList { - TargetList { - triples: vec![ - "aarch64-unknown-linux-gnu".to_owned(), - "armv7-unknown-linux-musleabihf".to_owned(), - ], - } - } - - fn target() -> Target { - let target_list = target_list(); - Target::from("aarch64-unknown-linux-gnu", &target_list) - } - - fn target2() -> Target { - let target_list = target_list(); - Target::from("armv7-unknown-linux-musleabihf", &target_list) - } - - mod test_environment { - - use super::*; - - #[test] - pub fn parse_error_in_env() { - let mut map = std::collections::HashMap::new(); - map.insert("CROSS_BUILD_XARGO", "tru"); - map.insert("CROSS_BUILD_STD", "false"); - - let env = Environment::new(Some(map)); - assert_eq!(env.xargo(&target()), (Some(true), None)); - assert_eq!(env.build_std(&target()), (Some(false), None)); - } - - #[test] - pub fn build_and_target_set_returns_tuple() { - let mut map = std::collections::HashMap::new(); - map.insert("CROSS_BUILD_XARGO", "true"); - map.insert("CROSS_TARGET_AARCH64_UNKNOWN_LINUX_GNU_XARGO", "false"); - - let env = Environment::new(Some(map)); - assert_eq!(env.xargo(&target()), (Some(true), Some(false))); - } - - #[test] - pub fn target_build_var_name() { - let map = std::collections::HashMap::new(); - - let env = Environment::new(Some(map)); - assert_eq!(env.build_var_name("build_xargo"), "CROSS_BUILD_XARGO"); - assert_eq!( - env.build_var_name("target_aarch64-unknown-linux-gnu_XARGO"), - "CROSS_TARGET_AARCH64_UNKNOWN_LINUX_GNU_XARGO" - ); - assert_eq!( - env.build_var_name("target-aarch64-unknown-linux-gnu_image"), - "CROSS_TARGET_AARCH64_UNKNOWN_LINUX_GNU_IMAGE" - ); - } - - #[test] - pub fn collect_passthrough() { - let mut map = std::collections::HashMap::new(); - map.insert("CROSS_BUILD_ENV_PASSTHROUGH", "TEST1 TEST2"); - map.insert( - "CROSS_TARGET_AARCH64_UNKNOWN_LINUX_GNU_ENV_PASSTHROUGH", - "PASS1 PASS2", - ); - - let env = Environment::new(Some(map)); - - let (build, target) = env.passthrough(&target()); - assert!(build.as_ref().unwrap().contains(&"TEST1".to_owned())); - assert!(build.as_ref().unwrap().contains(&"TEST2".to_owned())); - assert!(target.as_ref().unwrap().contains(&"PASS1".to_owned())); - assert!(target.as_ref().unwrap().contains(&"PASS2".to_owned())); - } - } - - #[cfg(test)] - mod test_config { - - use super::*; - - macro_rules! s { - ($x:literal) => { - $x.to_owned() - }; - } - - fn toml(content: &str) -> Result { - Ok( - CrossToml::parse_from_cross(content, &mut MessageInfo::default()) - .wrap_err("couldn't parse toml")? - .0, - ) - } - - #[test] - pub fn env_and_toml_build_xargo_then_use_env() -> Result<()> { - let mut map = HashMap::new(); - map.insert("CROSS_BUILD_XARGO", "true"); - map.insert( - "CROSS_BUILD_PRE_BUILD", - "apt-get update\napt-get install zlib-dev", - ); - - let env = Environment::new(Some(map)); - let config = Config::new_with(Some(toml(TOML_BUILD_XARGO_FALSE)?), env); - assert_eq!(config.xargo(&target()), Some(true)); - assert_eq!(config.build_std(&target()), None); - assert_eq!( - config.pre_build(&target())?, - Some(PreBuild::Lines(vec![ - s!("apt-get update"), - s!("apt-get install zlib-dev") - ])) - ); - - Ok(()) - } - - #[test] - pub fn env_target_and_toml_target_xargo_target_then_use_env() -> Result<()> { - let mut map = HashMap::new(); - map.insert("CROSS_TARGET_AARCH64_UNKNOWN_LINUX_GNU_XARGO", "true"); - map.insert("CROSS_TARGET_AARCH64_UNKNOWN_LINUX_GNU_BUILD_STD", "true"); - let env = Environment::new(Some(map)); - - let config = Config::new_with(Some(toml(TOML_TARGET_XARGO_FALSE)?), env); - assert_eq!(config.xargo(&target()), Some(true)); - assert_eq!(config.build_std(&target()), Some(true)); - assert_eq!(config.pre_build(&target())?, None); - - Ok(()) - } - - #[test] - pub fn env_target_and_toml_build_xargo_then_use_toml() -> Result<()> { - let mut map = HashMap::new(); - map.insert("CROSS_TARGET_AARCH64_UNKNOWN_LINUX_GNU_XARGO", "true"); - - let env = Environment::new(Some(map)); - let config = Config::new_with(Some(toml(TOML_BUILD_XARGO_FALSE)?), env); - assert_eq!(config.xargo(&target()), Some(true)); - assert_eq!(config.build_std(&target()), None); - assert_eq!(config.pre_build(&target())?, None); - - Ok(()) - } - - #[test] - pub fn env_target_and_toml_build_pre_build_then_use_env() -> Result<()> { - let mut map = HashMap::new(); - map.insert( - "CROSS_TARGET_AARCH64_UNKNOWN_LINUX_GNU_PRE_BUILD", - "dpkg --add-architecture arm64", - ); - - let env = Environment::new(Some(map)); - let config = Config::new_with(Some(toml(TOML_BUILD_PRE_BUILD)?), env); - assert_eq!( - config.pre_build(&target())?, - Some(PreBuild::Single { - line: s!("dpkg --add-architecture arm64"), - env: true - }) - ); - - Ok(()) - } - - #[test] - pub fn env_target_then_toml_target_then_env_build_then_toml_build() -> Result<()> { - let mut map = HashMap::new(); - map.insert("CROSS_BUILD_DOCKERFILE", "Dockerfile3"); - map.insert( - "CROSS_TARGET_AARCH64_UNKNOWN_LINUX_GNU_DOCKERFILE", - "Dockerfile4", - ); - - let env = Environment::new(Some(map)); - let config = Config::new_with(Some(toml(TOML_BUILD_DOCKERFILE)?), env); - assert_eq!(config.dockerfile(&target())?, Some(s!("Dockerfile4"))); - assert_eq!(config.dockerfile(&target2())?, Some(s!("Dockerfile3"))); - - let map = HashMap::new(); - let env = Environment::new(Some(map)); - let config = Config::new_with(Some(toml(TOML_BUILD_DOCKERFILE)?), env); - assert_eq!(config.dockerfile(&target())?, Some(s!("Dockerfile2"))); - assert_eq!(config.dockerfile(&target2())?, Some(s!("Dockerfile1"))); - - Ok(()) - } - - #[test] - pub fn toml_build_passthrough_then_use_target_passthrough_both() -> Result<()> { - let map = HashMap::new(); - let env = Environment::new(Some(map)); - let config = Config::new_with(Some(toml(TOML_ARRAYS_BOTH)?), env); - assert_eq!( - config.env_passthrough(&target())?, - Some(vec![s!("VAR1"), s!("VAR2"), s!("VAR3"), s!("VAR4")]) - ); - assert_eq!( - config.env_volumes(&target())?, - Some(vec![s!("VOLUME3"), s!("VOLUME4")]) - ); - - Ok(()) - } - - #[test] - pub fn toml_build_passthrough() -> Result<()> { - let map = HashMap::new(); - let env = Environment::new(Some(map)); - let config = Config::new_with(Some(toml(TOML_ARRAYS_BUILD)?), env); - assert_eq!( - config.env_passthrough(&target())?, - Some(vec![s!("VAR1"), s!("VAR2")]) - ); - assert_eq!( - config.env_volumes(&target())?, - Some(vec![s!("VOLUME1"), s!("VOLUME2")]) - ); - - Ok(()) - } - - #[test] - pub fn toml_target_passthrough() -> Result<()> { - let map = HashMap::new(); - let env = Environment::new(Some(map)); - let config = Config::new_with(Some(toml(TOML_ARRAYS_TARGET)?), env); - assert_eq!( - config.env_passthrough(&target())?, - Some(vec![s!("VAR3"), s!("VAR4")]) - ); - assert_eq!( - config.env_volumes(&target())?, - Some(vec![s!("VOLUME3"), s!("VOLUME4")]) - ); - - Ok(()) - } - - #[test] - pub fn volumes_use_env_over_toml() -> Result<()> { - let mut map = HashMap::new(); - map.insert("CROSS_BUILD_ENV_VOLUMES", "VOLUME1 VOLUME2"); - let env = Environment::new(Some(map)); - let config = Config::new_with(Some(toml(TOML_BUILD_VOLUMES)?), env); - let expected = vec![s!("VOLUME1"), s!("VOLUME2")]; - - let result = config.env_volumes(&target()).unwrap().unwrap_or_default(); - dbg!(&result); - assert!(result.len() == 2); - assert!(result.contains(&expected[0])); - assert!(result.contains(&expected[1])); - - Ok(()) - } - - #[test] - pub fn volumes_use_toml_when_no_env() -> Result<()> { - let map = HashMap::new(); - let env = Environment::new(Some(map)); - let config = Config::new_with(Some(toml(TOML_BUILD_VOLUMES)?), env); - let expected = vec![s!("VOLUME3"), s!("VOLUME4")]; - - let result = config.env_volumes(&target()).unwrap().unwrap_or_default(); - dbg!(&result); - assert!(result.len() == 2); - assert!(result.contains(&expected[0])); - assert!(result.contains(&expected[1])); - - Ok(()) - } - - #[test] - pub fn no_env_and_no_toml_default_target_then_none() -> Result<()> { - let config = Config::new_with(None, Environment::new(None)); - let config_target = config.target(&target_list()); - assert_eq!(config_target, None); - - Ok(()) - } - - #[test] - pub fn env_and_toml_default_target_then_use_env() -> Result<()> { - let mut map = HashMap::new(); - map.insert("CROSS_BUILD_TARGET", "armv7-unknown-linux-musleabihf"); - let env = Environment::new(Some(map)); - let config = Config::new_with(Some(toml(TOML_DEFAULT_TARGET)?), env); - - let config_target = config.target(&target_list()).unwrap(); - assert_eq!(config_target.triple(), "armv7-unknown-linux-musleabihf"); - - Ok(()) - } - - #[test] - pub fn no_env_but_toml_default_target_then_use_toml() -> Result<()> { - let env = Environment::new(None); - let config = Config::new_with(Some(toml(TOML_DEFAULT_TARGET)?), env); - - let config_target = config.target(&target_list()).unwrap(); - assert_eq!(config_target.triple(), "aarch64-unknown-linux-gnu"); - - Ok(()) - } - - static TOML_BUILD_XARGO_FALSE: &str = r#" - [build] - xargo = false - "#; - - static TOML_BUILD_PRE_BUILD: &str = r#" - [build] - pre-build = ["apt-get update && apt-get install zlib-dev"] - "#; - - static TOML_BUILD_DOCKERFILE: &str = r#" - [build] - dockerfile = "Dockerfile1" - [target.aarch64-unknown-linux-gnu] - dockerfile = "Dockerfile2" - "#; - - static TOML_TARGET_XARGO_FALSE: &str = r#" - [target.aarch64-unknown-linux-gnu] - xargo = false - "#; - - static TOML_BUILD_VOLUMES: &str = r#" - [build.env] - volumes = ["VOLUME3", "VOLUME4"] - [target.aarch64-unknown-linux-gnu] - xargo = false - "#; - - static TOML_ARRAYS_BOTH: &str = r#" - [build.env] - passthrough = ["VAR1", "VAR2"] - volumes = ["VOLUME1", "VOLUME2"] - - [target.aarch64-unknown-linux-gnu.env] - passthrough = ["VAR3", "VAR4"] - volumes = ["VOLUME3", "VOLUME4"] - "#; - - static TOML_ARRAYS_BUILD: &str = r#" - [build.env] - passthrough = ["VAR1", "VAR2"] - volumes = ["VOLUME1", "VOLUME2"] - "#; - - static TOML_ARRAYS_TARGET: &str = r#" - [target.aarch64-unknown-linux-gnu.env] - passthrough = ["VAR3", "VAR4"] - volumes = ["VOLUME3", "VOLUME4"] - "#; - - static TOML_DEFAULT_TARGET: &str = r#" - [build] - default-target = "aarch64-unknown-linux-gnu" - "#; +impl Config { + pub fn new(cargo: CargoConfig, cross: CrossConfig) -> Config { + Config { cargo, cross } } } diff --git a/src/cross_config.rs b/src/cross_config.rs new file mode 100644 index 000000000..783f218e8 --- /dev/null +++ b/src/cross_config.rs @@ -0,0 +1,789 @@ +use std::collections::HashMap; +use std::str::FromStr; + +use crate::config::{bool_from_envvar, split_to_cloned_by_ws, Environment}; +use crate::cross_toml::CargoConfigBehavior; +use crate::docker::custom::PreBuild; +use crate::docker::{ImagePlatform, PossibleImage}; +use crate::shell::MessageInfo; +use crate::{CrossToml, Result, Target, TargetList}; + +#[derive(Debug)] +struct CrossEnvironment(Environment); + +impl CrossEnvironment { + fn new(map: Option>) -> Self { + CrossEnvironment(Environment::new("CROSS", map)) + } + + fn get_values_for( + &self, + var: &str, + target: &Target, + convert: impl Fn(&str) -> T, + ) -> (Option, Option) { + let target_values = self.get_target_var(target, var).map(|ref s| convert(s)); + + let build_values = self.get_build_var(var).map(|ref s| convert(s)); + + (build_values, target_values) + } + + fn target_path(target: &Target, key: &str) -> String { + format!("TARGET_{target}_{key}") + } + + fn build_path(key: &str) -> String { + if !key.starts_with("BUILD_") { + format!("BUILD_{key}") + } else { + key.to_owned() + } + } + + fn get_build_var(&self, key: &str) -> Option { + self.0.get_var(&self.0.var_name(&Self::build_path(key))) + } + + fn get_target_var(&self, target: &Target, key: &str) -> Option { + self.0 + .get_var(&self.0.var_name(&Self::target_path(target, key))) + } + + fn xargo(&self, target: &Target) -> (Option, Option) { + self.get_values_for("XARGO", target, bool_from_envvar) + } + + fn build_std(&self, target: &Target) -> (Option, Option) { + self.get_values_for("BUILD_STD", target, bool_from_envvar) + } + + fn image(&self, target: &Target) -> Result> { + self.get_target_var(target, "IMAGE") + .map(Into::into) + .map(|mut i: PossibleImage| { + if let Some(toolchain) = self.get_target_var(target, "IMAGE_TOOLCHAIN") { + i.toolchain = toolchain + .split(',') + .map(|t| ImagePlatform::from_target(t.trim().into())) + .collect::>>()?; + Ok(i) + } else { + Ok(i) + } + }) + .transpose() + } + + fn dockerfile(&self, target: &Target) -> (Option, Option) { + self.get_values_for("DOCKERFILE", target, |s| s.to_owned()) + } + + fn dockerfile_context(&self, target: &Target) -> (Option, Option) { + self.get_values_for("DOCKERFILE_CONTEXT", target, |s| s.to_owned()) + } + + fn pre_build(&self, target: &Target) -> (Option, Option) { + self.get_values_for("PRE_BUILD", target, |v| { + let v: Vec<_> = v.split('\n').map(String::from).collect(); + if v.len() == 1 { + PreBuild::Single { + line: v.into_iter().next().expect("should contain one item"), + env: true, + } + } else { + PreBuild::Lines(v) + } + }) + } + + fn runner(&self, target: &Target) -> Option { + self.get_target_var(target, "RUNNER") + } + + fn cargo_config( + &self, + target: &Target, + ) -> Result<(Option, Option)> { + let (build, target) = + self.get_values_for("ENV_CARGO_CONFIG", target, CargoConfigBehavior::from_str); + Ok(match (build, target) { + (Some(b), Some(t)) => (Some(b?), Some(t?)), + (Some(b), None) => (Some(b?), None), + (None, Some(t)) => (None, Some(t?)), + (None, None) => (None, None), + }) + } + + fn passthrough(&self, target: &Target) -> (Option>, Option>) { + self.get_values_for("ENV_PASSTHROUGH", target, split_to_cloned_by_ws) + } + + fn volumes(&self, target: &Target) -> (Option>, Option>) { + self.get_values_for("ENV_VOLUMES", target, split_to_cloned_by_ws) + } + + fn target(&self) -> Option { + self.get_build_var("TARGET") + .or_else(|| std::env::var("CARGO_BUILD_TARGET").ok()) + } + + fn doctests(&self) -> Option { + self.0 + .get_var("CROSS_UNSTABLE_ENABLE_DOCTESTS") + .map(|s| bool_from_envvar(&s)) + } + + fn custom_toolchain(&self) -> bool { + self.0 + .get_var("CROSS_CUSTOM_TOOLCHAIN") + .map_or(false, |s| bool_from_envvar(&s)) + } + + fn custom_toolchain_compat(&self) -> Option { + self.0.get_var("CUSTOM_TOOLCHAIN_COMPAT") + } + + fn build_opts(&self) -> Option { + self.0.get_var("CROSS_BUILD_OPTS") + } +} + +#[derive(Debug)] +pub struct CrossConfig { + toml: Option, + env: CrossEnvironment, +} + +impl CrossConfig { + pub fn new(toml: Option) -> Self { + CrossConfig { + toml, + env: CrossEnvironment::new(None), + } + } + + pub fn confusable_target(&self, target: &Target, msg_info: &mut MessageInfo) -> Result<()> { + if let Some(keys) = self.toml.as_ref().map(|t| t.targets.keys()) { + for mentioned_target in keys { + let mentioned_target_norm = mentioned_target + .to_string() + .replace(|c| c == '-' || c == '_', "") + .to_lowercase(); + let target_norm = target + .to_string() + .replace(|c| c == '-' || c == '_', "") + .to_lowercase(); + if mentioned_target != target && mentioned_target_norm == target_norm { + msg_info.warn("a target named \"{mentioned_target}\" is mentioned in the Cross configuration, but the current specified target is \"{target}\".")?; + msg_info.status(" > Is the target misspelled in the Cross configuration?")?; + } + } + } + Ok(()) + } + + fn bool_from_config( + &self, + target: &Target, + env: impl Fn(&CrossEnvironment, &Target) -> (Option, Option), + config: impl Fn(&CrossToml, &Target) -> (Option, Option), + ) -> Option { + let (env_build, env_target) = env(&self.env, target); + let (toml_build, toml_target) = if let Some(ref toml) = self.toml { + config(toml, target) + } else { + (None, None) + }; + + match (env_target, toml_target) { + (Some(value), _) => return Some(value), + (None, Some(value)) => return Some(value), + (None, None) => {} + }; + + match (env_build, toml_build) { + (Some(value), _) => return Some(value), + (None, Some(value)) => return Some(value), + (None, None) => {} + }; + + None + } + + fn vec_from_config( + &self, + target: &Target, + env: impl for<'a> Fn( + &'a CrossEnvironment, + &Target, + ) -> (Option>, Option>), + config: impl for<'a> Fn(&'a CrossToml, &Target) -> (Option<&'a [String]>, Option<&'a [String]>), + sum: bool, + ) -> Result>> { + if sum { + let (mut env_build, env_target) = env(&self.env, target); + env_build + .as_mut() + .map(|b| env_target.map(|mut t| b.append(&mut t))); + self.sum_of_env_toml_values(env_build, |t| config(t, target)) + } else { + self.get_from_ref(target, env, config) + } + } + + fn get_from_ref( + &self, + target: &Target, + env: impl for<'a> Fn(&'a CrossEnvironment, &Target) -> (Option, Option), + config: impl for<'a> Fn(&'a CrossToml, &Target) -> (Option<&'a U>, Option<&'a U>), + ) -> Result> + where + U: ToOwned + ?Sized, + { + let (env_build, env_target) = env(&self.env, target); + + if let Some(env_target) = env_target { + return Ok(Some(env_target)); + } + + let (build, target) = self + .toml + .as_ref() + .map(|t| config(t, target)) + .unwrap_or_default(); + + // FIXME: let expression + if target.is_none() && env_build.is_some() { + return Ok(env_build); + } + + if target.is_none() { + Ok(build.map(ToOwned::to_owned)) + } else { + Ok(target.map(ToOwned::to_owned)) + } + } + + #[cfg(test)] + fn new_with(toml: Option, env: CrossEnvironment) -> Self { + CrossConfig { toml, env } + } + + pub fn xargo(&self, target: &Target) -> Option { + self.bool_from_config(target, CrossEnvironment::xargo, CrossToml::xargo) + } + + pub fn build_std(&self, target: &Target) -> Option { + self.bool_from_config(target, CrossEnvironment::build_std, CrossToml::build_std) + } + + pub fn image(&self, target: &Target) -> Result> { + let env = self.env.image(target)?; + self.get_from_ref( + target, + move |_, _| (None, env.clone()), + |toml, target| (None, toml.image(target)), + ) + } + + pub fn runner(&self, target: &Target) -> Result> { + self.get_from_ref( + target, + |env, target| (None, env.runner(target)), + |toml, target| (None, toml.runner(target)), + ) + } + + pub fn doctests(&self) -> Option { + self.env.doctests() + } + + pub fn custom_toolchain(&self) -> bool { + self.env.custom_toolchain() + } + + pub fn custom_toolchain_compat(&self) -> Option { + self.env.custom_toolchain_compat() + } + + pub fn build_opts(&self) -> Option { + self.env.build_opts() + } + + pub fn env_cargo_config(&self, target: &Target) -> Result> { + // TODO(ahuszagh) get_from_ref here + let (env_build, env_target) = self.env.cargo_config(target)?; + + if let Some(env_target) = env_target { + return Ok(Some(env_target)); + } + + let (build, target) = self + .toml + .as_ref() + .map(|t| t.env_cargo_config(target)) + .unwrap_or_default(); + + // FIXME: let expression + if target.is_none() && env_build.is_some() { + Ok(env_build) + } else if target.is_none() { + Ok(build) + } else { + Ok(target) + } + } + + pub fn env_passthrough(&self, target: &Target) -> Result>> { + self.vec_from_config( + target, + CrossEnvironment::passthrough, + CrossToml::env_passthrough, + true, + ) + } + + pub fn env_volumes(&self, target: &Target) -> Result>> { + self.get_from_ref(target, CrossEnvironment::volumes, CrossToml::env_volumes) + } + + pub fn target(&self, target_list: &TargetList) -> Option { + if let Some(env_value) = self.env.target() { + return Some(Target::from(&env_value, target_list)); + } + self.toml + .as_ref() + .and_then(|t| t.default_target(target_list)) + } + + pub fn dockerfile(&self, target: &Target) -> Result> { + self.get_from_ref(target, CrossEnvironment::dockerfile, CrossToml::dockerfile) + } + + pub fn dockerfile_context(&self, target: &Target) -> Result> { + self.get_from_ref( + target, + CrossEnvironment::dockerfile_context, + CrossToml::dockerfile_context, + ) + } + + pub fn dockerfile_build_args( + &self, + target: &Target, + ) -> Result>> { + // This value does not support env variables + self.toml + .as_ref() + .map_or(Ok(None), |t| Ok(t.dockerfile_build_args(target))) + } + + pub fn pre_build(&self, target: &Target) -> Result> { + self.get_from_ref(target, CrossEnvironment::pre_build, CrossToml::pre_build) + } + + // FIXME: remove when we disable sums in 0.3.0. + fn sum_of_env_toml_values<'a>( + &'a self, + env_values: Option>, + toml_getter: impl FnOnce(&'a CrossToml) -> (Option<&'a [String]>, Option<&'a [String]>), + ) -> Result>> { + let mut defined = false; + let mut collect = vec![]; + if let Some(vars) = env_values { + collect.extend(vars.as_ref().iter().cloned()); + defined = true; + } else if let Some((build, target)) = self.toml.as_ref().map(toml_getter) { + if let Some(build) = build { + collect.extend(build.iter().cloned()); + defined = true; + } + if let Some(target) = target { + collect.extend(target.iter().cloned()); + defined = true; + } + } + if !defined { + Ok(None) + } else { + Ok(Some(collect)) + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::errors::*; + use crate::{Target, TargetList}; + + fn target_list() -> TargetList { + TargetList { + triples: vec![ + "aarch64-unknown-linux-gnu".to_owned(), + "armv7-unknown-linux-musleabihf".to_owned(), + ], + } + } + + fn target() -> Target { + let target_list = target_list(); + Target::from("aarch64-unknown-linux-gnu", &target_list) + } + + fn target2() -> Target { + let target_list = target_list(); + Target::from("armv7-unknown-linux-musleabihf", &target_list) + } + + mod test_environment { + + use super::*; + + #[test] + pub fn parse_error_in_env() { + let mut map = std::collections::HashMap::new(); + map.insert("CROSS_BUILD_XARGO", "tru"); + map.insert("CROSS_BUILD_STD", "false"); + + let env = CrossEnvironment::new(Some(map)); + assert_eq!(env.xargo(&target()), (Some(true), None)); + assert_eq!(env.build_std(&target()), (Some(false), None)); + } + + #[test] + pub fn build_and_target_set_returns_tuple() { + let mut map = std::collections::HashMap::new(); + map.insert("CROSS_BUILD_XARGO", "true"); + map.insert("CROSS_TARGET_AARCH64_UNKNOWN_LINUX_GNU_XARGO", "false"); + + let env = CrossEnvironment::new(Some(map)); + assert_eq!(env.xargo(&target()), (Some(true), Some(false))); + } + + #[test] + pub fn target_build_var_name() { + let map = std::collections::HashMap::new(); + + let env = CrossEnvironment::new(Some(map)); + assert_eq!(env.0.var_name("build_xargo"), "CROSS_BUILD_XARGO"); + assert_eq!( + env.0.var_name("target_aarch64-unknown-linux-gnu_XARGO"), + "CROSS_TARGET_AARCH64_UNKNOWN_LINUX_GNU_XARGO" + ); + assert_eq!( + env.0.var_name("target-aarch64-unknown-linux-gnu_image"), + "CROSS_TARGET_AARCH64_UNKNOWN_LINUX_GNU_IMAGE" + ); + } + + #[test] + pub fn collect_passthrough() { + let mut map = std::collections::HashMap::new(); + map.insert("CROSS_BUILD_ENV_PASSTHROUGH", "TEST1 TEST2"); + map.insert( + "CROSS_TARGET_AARCH64_UNKNOWN_LINUX_GNU_ENV_PASSTHROUGH", + "PASS1 PASS2", + ); + + let env = CrossEnvironment::new(Some(map)); + + let (build, target) = env.passthrough(&target()); + assert!(build.as_ref().unwrap().contains(&"TEST1".to_owned())); + assert!(build.as_ref().unwrap().contains(&"TEST2".to_owned())); + assert!(target.as_ref().unwrap().contains(&"PASS1".to_owned())); + assert!(target.as_ref().unwrap().contains(&"PASS2".to_owned())); + } + } + + #[cfg(test)] + mod test_config { + + use super::*; + + macro_rules! s { + ($x:literal) => { + $x.to_owned() + }; + } + + fn toml(content: &str) -> Result { + Ok( + CrossToml::parse_from_cross(content, &mut MessageInfo::default()) + .wrap_err("couldn't parse toml")? + .0, + ) + } + + #[test] + pub fn env_and_toml_build_xargo_then_use_env() -> Result<()> { + let mut map = HashMap::new(); + map.insert("CROSS_BUILD_XARGO", "true"); + map.insert( + "CROSS_BUILD_PRE_BUILD", + "apt-get update\napt-get install zlib-dev", + ); + + let env = CrossEnvironment::new(Some(map)); + let config = CrossConfig::new_with(Some(toml(TOML_BUILD_XARGO_FALSE)?), env); + assert_eq!(config.xargo(&target()), Some(true)); + assert_eq!(config.build_std(&target()), None); + assert_eq!( + config.pre_build(&target())?, + Some(PreBuild::Lines(vec![ + s!("apt-get update"), + s!("apt-get install zlib-dev") + ])) + ); + + Ok(()) + } + + #[test] + pub fn env_target_and_toml_target_xargo_target_then_use_env() -> Result<()> { + let mut map = HashMap::new(); + map.insert("CROSS_TARGET_AARCH64_UNKNOWN_LINUX_GNU_XARGO", "true"); + map.insert("CROSS_TARGET_AARCH64_UNKNOWN_LINUX_GNU_BUILD_STD", "true"); + let env = CrossEnvironment::new(Some(map)); + + let config = CrossConfig::new_with(Some(toml(TOML_TARGET_XARGO_FALSE)?), env); + assert_eq!(config.xargo(&target()), Some(true)); + assert_eq!(config.build_std(&target()), Some(true)); + assert_eq!(config.pre_build(&target())?, None); + + Ok(()) + } + + #[test] + pub fn env_target_and_toml_build_xargo_then_use_toml() -> Result<()> { + let mut map = HashMap::new(); + map.insert("CROSS_TARGET_AARCH64_UNKNOWN_LINUX_GNU_XARGO", "true"); + + let env = CrossEnvironment::new(Some(map)); + let config = CrossConfig::new_with(Some(toml(TOML_BUILD_XARGO_FALSE)?), env); + assert_eq!(config.xargo(&target()), Some(true)); + assert_eq!(config.build_std(&target()), None); + assert_eq!(config.pre_build(&target())?, None); + + Ok(()) + } + + #[test] + pub fn env_target_and_toml_build_pre_build_then_use_env() -> Result<()> { + let mut map = HashMap::new(); + map.insert( + "CROSS_TARGET_AARCH64_UNKNOWN_LINUX_GNU_PRE_BUILD", + "dpkg --add-architecture arm64", + ); + + let env = CrossEnvironment::new(Some(map)); + let config = CrossConfig::new_with(Some(toml(TOML_BUILD_PRE_BUILD)?), env); + assert_eq!( + config.pre_build(&target())?, + Some(PreBuild::Single { + line: s!("dpkg --add-architecture arm64"), + env: true + }) + ); + + Ok(()) + } + + #[test] + pub fn env_target_then_toml_target_then_env_build_then_toml_build() -> Result<()> { + let mut map = HashMap::new(); + map.insert("CROSS_BUILD_DOCKERFILE", "Dockerfile3"); + map.insert( + "CROSS_TARGET_AARCH64_UNKNOWN_LINUX_GNU_DOCKERFILE", + "Dockerfile4", + ); + + let env = CrossEnvironment::new(Some(map)); + let config = CrossConfig::new_with(Some(toml(TOML_BUILD_DOCKERFILE)?), env); + assert_eq!(config.dockerfile(&target())?, Some(s!("Dockerfile4"))); + assert_eq!(config.dockerfile(&target2())?, Some(s!("Dockerfile3"))); + + let map = HashMap::new(); + let env = CrossEnvironment::new(Some(map)); + let config = CrossConfig::new_with(Some(toml(TOML_BUILD_DOCKERFILE)?), env); + assert_eq!(config.dockerfile(&target())?, Some(s!("Dockerfile2"))); + assert_eq!(config.dockerfile(&target2())?, Some(s!("Dockerfile1"))); + + Ok(()) + } + + #[test] + pub fn toml_build_passthrough_then_use_target_passthrough_both() -> Result<()> { + let map = HashMap::new(); + let env = CrossEnvironment::new(Some(map)); + let config = CrossConfig::new_with(Some(toml(TOML_ARRAYS_BOTH)?), env); + assert_eq!( + config.env_passthrough(&target())?, + Some(vec![s!("VAR1"), s!("VAR2"), s!("VAR3"), s!("VAR4")]) + ); + assert_eq!( + config.env_volumes(&target())?, + Some(vec![s!("VOLUME3"), s!("VOLUME4")]) + ); + + Ok(()) + } + + #[test] + pub fn toml_build_passthrough() -> Result<()> { + let map = HashMap::new(); + let env = CrossEnvironment::new(Some(map)); + let config = CrossConfig::new_with(Some(toml(TOML_ARRAYS_BUILD)?), env); + assert_eq!( + config.env_passthrough(&target())?, + Some(vec![s!("VAR1"), s!("VAR2")]) + ); + assert_eq!( + config.env_volumes(&target())?, + Some(vec![s!("VOLUME1"), s!("VOLUME2")]) + ); + + Ok(()) + } + + #[test] + pub fn toml_target_passthrough() -> Result<()> { + let map = HashMap::new(); + let env = CrossEnvironment::new(Some(map)); + let config = CrossConfig::new_with(Some(toml(TOML_ARRAYS_TARGET)?), env); + assert_eq!( + config.env_passthrough(&target())?, + Some(vec![s!("VAR3"), s!("VAR4")]) + ); + assert_eq!( + config.env_volumes(&target())?, + Some(vec![s!("VOLUME3"), s!("VOLUME4")]) + ); + + Ok(()) + } + + #[test] + pub fn volumes_use_env_over_toml() -> Result<()> { + let mut map = HashMap::new(); + map.insert("CROSS_BUILD_ENV_VOLUMES", "VOLUME1 VOLUME2"); + let env = CrossEnvironment::new(Some(map)); + let config = CrossConfig::new_with(Some(toml(TOML_BUILD_VOLUMES)?), env); + let expected = vec![s!("VOLUME1"), s!("VOLUME2")]; + + let result = config.env_volumes(&target()).unwrap().unwrap_or_default(); + dbg!(&result); + assert!(result.len() == 2); + assert!(result.contains(&expected[0])); + assert!(result.contains(&expected[1])); + + Ok(()) + } + + #[test] + pub fn volumes_use_toml_when_no_env() -> Result<()> { + let map = HashMap::new(); + let env = CrossEnvironment::new(Some(map)); + let config = CrossConfig::new_with(Some(toml(TOML_BUILD_VOLUMES)?), env); + let expected = vec![s!("VOLUME3"), s!("VOLUME4")]; + + let result = config.env_volumes(&target()).unwrap().unwrap_or_default(); + dbg!(&result); + assert!(result.len() == 2); + assert!(result.contains(&expected[0])); + assert!(result.contains(&expected[1])); + + Ok(()) + } + + #[test] + pub fn no_env_and_no_toml_default_target_then_none() -> Result<()> { + let config = CrossConfig::new_with(None, CrossEnvironment::new(None)); + let config_target = config.target(&target_list()); + assert_eq!(config_target, None); + + Ok(()) + } + + #[test] + pub fn env_and_toml_default_target_then_use_env() -> Result<()> { + let mut map = HashMap::new(); + map.insert("CROSS_BUILD_TARGET", "armv7-unknown-linux-musleabihf"); + let env = CrossEnvironment::new(Some(map)); + let config = CrossConfig::new_with(Some(toml(TOML_DEFAULT_TARGET)?), env); + + let config_target = config.target(&target_list()).unwrap(); + assert_eq!(config_target.triple(), "armv7-unknown-linux-musleabihf"); + + Ok(()) + } + + #[test] + pub fn no_env_but_toml_default_target_then_use_toml() -> Result<()> { + let env = CrossEnvironment::new(None); + let config = CrossConfig::new_with(Some(toml(TOML_DEFAULT_TARGET)?), env); + + let config_target = config.target(&target_list()).unwrap(); + assert_eq!(config_target.triple(), "aarch64-unknown-linux-gnu"); + + Ok(()) + } + + static TOML_BUILD_XARGO_FALSE: &str = r#" + [build] + xargo = false + "#; + + static TOML_BUILD_PRE_BUILD: &str = r#" + [build] + pre-build = ["apt-get update && apt-get install zlib-dev"] + "#; + + static TOML_BUILD_DOCKERFILE: &str = r#" + [build] + dockerfile = "Dockerfile1" + [target.aarch64-unknown-linux-gnu] + dockerfile = "Dockerfile2" + "#; + + static TOML_TARGET_XARGO_FALSE: &str = r#" + [target.aarch64-unknown-linux-gnu] + xargo = false + "#; + + static TOML_BUILD_VOLUMES: &str = r#" + [build.env] + volumes = ["VOLUME3", "VOLUME4"] + [target.aarch64-unknown-linux-gnu] + xargo = false + "#; + + static TOML_ARRAYS_BOTH: &str = r#" + [build.env] + passthrough = ["VAR1", "VAR2"] + volumes = ["VOLUME1", "VOLUME2"] + + [target.aarch64-unknown-linux-gnu.env] + passthrough = ["VAR3", "VAR4"] + volumes = ["VOLUME3", "VOLUME4"] + "#; + + static TOML_ARRAYS_BUILD: &str = r#" + [build.env] + passthrough = ["VAR1", "VAR2"] + volumes = ["VOLUME1", "VOLUME2"] + "#; + + static TOML_ARRAYS_TARGET: &str = r#" + [target.aarch64-unknown-linux-gnu.env] + passthrough = ["VAR3", "VAR4"] + volumes = ["VOLUME3", "VOLUME4"] + "#; + + static TOML_DEFAULT_TARGET: &str = r#" + [build] + default-target = "aarch64-unknown-linux-gnu" + "#; + } +} diff --git a/src/cross_toml.rs b/src/cross_toml.rs index 858faf8c2..69179497f 100644 --- a/src/cross_toml.rs +++ b/src/cross_toml.rs @@ -1,18 +1,24 @@ #![doc = include_str!("../docs/cross_toml.md")] +use std::collections::{BTreeSet, HashMap}; +use std::env; +use std::path::PathBuf; +use std::str::FromStr; + +use crate::cargo::CargoMetadata; use crate::docker::custom::PreBuild; use crate::docker::PossibleImage; use crate::shell::MessageInfo; -use crate::{config, errors::*}; +use crate::{errors::*, file}; use crate::{Target, TargetList}; use serde::de::DeserializeOwned; use serde::{Deserialize, Deserializer, Serialize}; -use std::collections::{BTreeSet, HashMap}; -use std::str::FromStr; /// Environment configuration #[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Default)] +#[serde(rename_all = "kebab-case")] pub struct CrossEnvConfig { + cargo_config: Option, volumes: Option>, passthrough: Option>, } @@ -70,8 +76,37 @@ impl FromStr for CrossTargetDockerfileConfig { } } +#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone, Copy)] +#[serde(rename_all = "kebab-case")] +pub enum CargoConfigBehavior { + Ignore, + #[serde(rename = "default")] + Normal, + Complete, +} + +impl Default for CargoConfigBehavior { + fn default() -> CargoConfigBehavior { + CargoConfigBehavior::Normal + } +} + +impl FromStr for CargoConfigBehavior { + type Err = eyre::Error; + + fn from_str(s: &str) -> Result { + match s { + "ignore" => Ok(CargoConfigBehavior::Ignore), + "default" => Ok(CargoConfigBehavior::Normal), + "complete" => Ok(CargoConfigBehavior::Complete), + _ => eyre::bail!("invalid cargo config behavior, got {s}"), + } + } +} + /// Cross configuration #[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Default)] +#[serde(rename_all = "kebab-case")] pub struct CrossToml { #[serde(default, rename = "target")] pub targets: HashMap, @@ -80,6 +115,50 @@ pub struct CrossToml { } impl CrossToml { + /// Obtains the [`CrossToml`] from one of the possible locations + /// + /// These locations are checked in the following order: + /// 1. If the `CROSS_CONFIG` variable is set, it tries to read the config from its value + /// 2. Otherwise, the `Cross.toml` in the project root is used + /// 3. Package metadata in the Cargo.toml + /// + /// The values from `CROSS_CONFIG` or `Cross.toml` are concatenated with the package + /// metadata in `Cargo.toml`, with `Cross.toml` having the highest priority. + pub fn read(metadata: &CargoMetadata, msg_info: &mut MessageInfo) -> Result> { + let root = &metadata.workspace_root; + let cross_config_path = match env::var("CROSS_CONFIG") { + Ok(var) => PathBuf::from(var), + Err(_) => root.join("Cross.toml"), + }; + + // Attempts to read the cross config from the Cargo.toml + let cargo_toml_str = + file::read(root.join("Cargo.toml")).wrap_err("failed to read Cargo.toml")?; + + if cross_config_path.exists() { + let cross_toml_str = file::read(&cross_config_path) + .wrap_err_with(|| format!("could not read file `{cross_config_path:?}`"))?; + + let (config, _) = CrossToml::parse(&cargo_toml_str, &cross_toml_str, msg_info) + .wrap_err_with(|| { + format!("failed to parse file `{cross_config_path:?}` as TOML",) + })?; + + Ok(Some(config)) + } else { + // Checks if there is a lowercase version of this file + if root.join("cross.toml").exists() { + msg_info.warn("There's a file named cross.toml, instead of Cross.toml. You may want to rename it, or it won't be considered.")?; + } + + if let Some((cfg, _)) = CrossToml::parse_from_cargo(&cargo_toml_str, msg_info)? { + Ok(Some(cfg)) + } else { + Ok(None) + } + } + } + /// Parses the [`CrossToml`] from all of the config sources pub fn parse( cargo_toml: &str, @@ -252,7 +331,7 @@ impl CrossToml { .as_ref() .and_then(|d| d.build_args.as_ref()); - config::opt_merge(target.cloned(), build.cloned()) + opt_merge(target.cloned(), build.cloned()) } /// Returns the `build.dockerfile.pre-build` and `target.{}.dockerfile.pre-build` part of `Cross.toml` @@ -275,6 +354,14 @@ impl CrossToml { self.get_value(target, |b| b.build_std, |t| t.build_std) } + /// Returns the cargo config behavior. + pub fn env_cargo_config( + &self, + target: &Target, + ) -> (Option, Option) { + self.get_value(target, |b| b.env.cargo_config, |t| t.env.cargo_config) + } + /// Returns the list of environment variables to pass through for `build` and `target` pub fn env_passthrough(&self, target: &Target) -> (Option<&[String]>, Option<&[String]>) { self.get_ref( @@ -442,6 +529,22 @@ where deserializer.deserialize_any(StringOrStringVec(PhantomData)) } +pub fn opt_merge + IntoIterator>( + opt1: Option, + opt2: Option, +) -> Option { + match (opt1, opt2) { + (None, None) => None, + (None, Some(opt2)) => Some(opt2), + (Some(opt1), None) => Some(opt1), + (Some(opt1), Some(opt2)) => { + let mut res = opt2; + res.extend(opt1); + Some(res) + } + } +} + #[cfg(test)] mod tests { use crate::docker::ImagePlatform; @@ -461,6 +564,21 @@ mod tests { }; } + #[test] + pub fn test_toml_cargo_config_behavior() -> Result<()> { + assert_eq!(CargoConfigBehavior::Normal, toml::from_str("\"default\"")?); + assert_eq!(CargoConfigBehavior::Ignore, toml::from_str("\"ignore\"")?); + assert_eq!( + CargoConfigBehavior::Complete, + toml::from_str("\"complete\"")? + ); + assert!(toml::from_str::("\"other\"").is_err()); + assert!(toml::from_str::("true").is_err()); + assert!(toml::from_str::("0").is_err()); + + Ok(()) + } + #[test] pub fn parse_empty_toml() -> Result<()> { let cfg = CrossToml { @@ -481,6 +599,7 @@ mod tests { targets: HashMap::new(), build: CrossBuildConfig { env: CrossEnvConfig { + cargo_config: Some(CargoConfigBehavior::Ignore), volumes: Some(vec![p!("VOL1_ARG"), p!("VOL2_ARG")]), passthrough: Some(vec![p!("VAR1"), p!("VAR2")]), }, @@ -498,6 +617,7 @@ mod tests { pre-build = ["echo 'Hello World!'"] [build.env] + cargo-config = "ignore" volumes = ["VOL1_ARG", "VOL2_ARG"] passthrough = ["VAR1", "VAR2"] "#; @@ -518,6 +638,7 @@ mod tests { }, CrossTargetConfig { env: CrossEnvConfig { + cargo_config: None, passthrough: Some(vec![p!("VAR1"), p!("VAR2")]), volumes: Some(vec![p!("VOL1_ARG"), p!("VOL2_ARG")]), }, @@ -577,6 +698,7 @@ mod tests { pre_build: Some(PreBuild::Lines(vec![p!("echo 'Hello'")])), runner: None, env: CrossEnvConfig { + cargo_config: None, passthrough: None, volumes: Some(vec![p!("VOL")]), }, @@ -587,6 +709,7 @@ mod tests { targets: target_map, build: CrossBuildConfig { env: CrossEnvConfig { + cargo_config: Some(CargoConfigBehavior::Complete), volumes: None, passthrough: Some(vec![]), }, @@ -604,6 +727,7 @@ mod tests { pre-build = [] [build.env] + cargo-config = "complete" passthrough = [] [target.aarch64-unknown-linux-gnu] @@ -647,6 +771,7 @@ mod tests { targets: HashMap::new(), build: CrossBuildConfig { env: CrossEnvConfig { + cargo_config: None, passthrough: None, volumes: None, }, diff --git a/src/docker/custom.rs b/src/docker/custom.rs index 847f3f0b3..643039b86 100644 --- a/src/docker/custom.rs +++ b/src/docker/custom.rs @@ -129,8 +129,9 @@ impl<'a> Dockerfile<'a> { } }; + let config = &options.config.cross; if matches!(self, Dockerfile::File { .. }) { - if let Ok(cross_base_image) = self::get_image_name(&options.config, &options.target) { + if let Ok(cross_base_image) = self::get_image_name(config, &options.target) { docker_build.args([ "--build-arg", &format!("CROSS_BASE_IMAGE={cross_base_image}"), @@ -140,11 +141,11 @@ impl<'a> Dockerfile<'a> { docker_build.args(["--file".into(), path]); - if let Some(build_opts) = options.config.build_opts() { + if let Some(build_opts) = config.build_opts() { docker_build.args(parse_docker_opts(&build_opts)?); } - let has_output = options.config.build_opts().map_or(false, |opts| { + let has_output = config.build_opts().map_or(false, |opts| { opts.contains("--load") || opts.contains("--output") }); if options.engine.kind.is_docker() && !has_output { diff --git a/src/docker/local.rs b/src/docker/local.rs index 05e1b3d37..221c7ae46 100644 --- a/src/docker/local.rs +++ b/src/docker/local.rs @@ -3,6 +3,7 @@ use std::path::Path; use std::process::{Command, ExitStatus}; use super::shared::*; +use crate::cross_toml::CargoConfigBehavior; use crate::errors::Result; use crate::extensions::CommandExt; use crate::file::{PathExt, ToUtf8}; @@ -40,7 +41,7 @@ pub(crate) fn run( .specify_platform(&options.engine, &mut docker); docker_envvars( &mut docker, - &options.config, + &options.config.cross, dirs, &options.target, msg_info, @@ -85,7 +86,7 @@ pub(crate) fn run( ), ]) .args(&["-v", &format!("{}:/target:Z", dirs.target.to_utf8()?)]); - docker_cwd(&mut docker, &paths)?; + docker_cwd(&mut docker, &paths, options.cargo_config_behavior)?; // When running inside NixOS or using Nix packaging we need to add the Nix // Store to the running container so it can load the needed binaries. @@ -100,6 +101,29 @@ pub(crate) fn run( ]); } + // If we're using all config settings, we need to mount all `.cargo` dirs. + // We've already mounted the CWD, so start at the parents. + let mut host_cwd = paths.cwd.parent(); + let mut mount_cwd = Path::new(&paths.directories.mount_cwd).parent(); + if let CargoConfigBehavior::Complete = options.cargo_config_behavior { + while let (Some(host), Some(mount)) = (host_cwd, mount_cwd) { + let host_cargo = host.join(".cargo"); + let mount_cargo = mount.join(".cargo"); + if host_cargo.exists() { + docker.args(&[ + "-v", + &format!( + "{}:{}:Z", + host_cargo.to_utf8()?, + mount_cargo.as_posix_absolute()? + ), + ]); + } + host_cwd = host.parent(); + mount_cwd = mount.parent(); + } + } + if io::Stdin::is_atty() { docker.arg("-i"); if io::Stdout::is_atty() && io::Stderr::is_atty() { diff --git a/src/docker/remote.rs b/src/docker/remote.rs index cfc5cf32c..bbdb46187 100644 --- a/src/docker/remote.rs +++ b/src/docker/remote.rs @@ -11,6 +11,7 @@ use super::engine::Engine; use super::shared::*; use crate::cargo::CargoMetadata; use crate::config::bool_from_envvar; +use crate::cross_toml::CargoConfigBehavior; use crate::errors::Result; use crate::extensions::CommandExt; use crate::file::{self, PathExt, ToUtf8}; @@ -1053,6 +1054,33 @@ pub(crate) fn run( ), (&dirs.host_root, mount_root.clone()), ]; + + // If we're using all config settings, write the combined + // config file to a fixed location (to avoid it becoming stale). + // SAFETY: safe, single-threaded execution. + let config_tempdir = unsafe { temp::TempDir::new()? }; + let config_temppath = config_tempdir.path(); + let config_src_dir = config_temppath.join(".cargo"); + if let CargoConfigBehavior::Complete = options.cargo_config_behavior { + let toml_opt = options.config.cargo.to_toml()?; + if let Some(toml_str) = toml_opt { + fs::create_dir_all(&config_src_dir)?; + file::write_file(config_src_dir.join("config.toml"), true)? + .write_all(toml_str.as_bytes())?; + + let dst_dir = mount_prefix_path.join(".cargo"); + copy_volume_files_nocache( + engine, + &container, + &config_src_dir, + &dst_dir, + false, + msg_info, + )?; + copied.push((&config_src_dir, dst_dir)); + } + } + let mut to_symlink = vec![]; let target_dir = file::canonicalize(&dirs.target)?; let target_dir = if let Ok(relpath) = target_dir.strip_prefix(&dirs.host_root) { @@ -1174,8 +1202,8 @@ symlink_recurse \"${{prefix}}\" // 6. execute our cargo command inside the container let mut docker = subcommand(engine, "exec"); docker_user_id(&mut docker, engine.kind); - docker_envvars(&mut docker, &options.config, dirs, target, msg_info)?; - docker_cwd(&mut docker, &paths)?; + docker_envvars(&mut docker, &options.config.cross, dirs, target, msg_info)?; + docker_cwd(&mut docker, &paths, options.cargo_config_behavior)?; docker.arg(&container); docker.args(&["sh", "-c", &build_command(dirs, &cmd)]); bail_container_exited!(); diff --git a/src/docker/shared.rs b/src/docker/shared.rs index af6d13d23..56fc5da63 100644 --- a/src/docker/shared.rs +++ b/src/docker/shared.rs @@ -9,7 +9,10 @@ use super::image::PossibleImage; use super::Image; use super::PROVIDED_IMAGES; use crate::cargo::{cargo_metadata_with_args, CargoMetadata}; +use crate::cargo_config::CARGO_NO_PREFIX_ENVVARS; use crate::config::{bool_from_envvar, Config}; +use crate::cross_config::CrossConfig; +use crate::cross_toml::CargoConfigBehavior; use crate::errors::*; use crate::extensions::{CommandExt, SafeCommand}; use crate::file::{self, write_file, PathExt, ToUtf8}; @@ -37,6 +40,7 @@ pub struct DockerOptions { pub config: Config, pub image: Image, pub uses_xargo: bool, + pub cargo_config_behavior: CargoConfigBehavior, } impl DockerOptions { @@ -46,6 +50,7 @@ impl DockerOptions { config: Config, image: Image, uses_xargo: bool, + cargo_config_behavior: CargoConfigBehavior, ) -> DockerOptions { DockerOptions { engine, @@ -53,6 +58,7 @@ impl DockerOptions { config, image, uses_xargo, + cargo_config_behavior, } } @@ -69,11 +75,13 @@ impl DockerOptions { #[must_use] pub fn needs_custom_image(&self) -> bool { self.config + .cross .dockerfile(&self.target) .unwrap_or_default() .is_some() || self .config + .cross .pre_build(&self.target) .unwrap_or_default() .is_some() @@ -85,11 +93,12 @@ impl DockerOptions { msg_info: &mut MessageInfo, ) -> Result { let mut image = self.image.clone(); + let config = &self.config.cross; - if let Some(path) = self.config.dockerfile(&self.target)? { - let context = self.config.dockerfile_context(&self.target)?; + if let Some(path) = config.dockerfile(&self.target)? { + let context = config.dockerfile_context(&self.target)?; - let is_custom_image = self.config.image(&self.target)?.is_some(); + let is_custom_image = config.image(&self.target)?.is_some(); let build = Dockerfile::File { path: &path, @@ -106,14 +115,14 @@ impl DockerOptions { .build( self, paths, - self.config + config .dockerfile_build_args(&self.target)? .unwrap_or_default(), msg_info, ) .wrap_err("when building dockerfile")?; } - let pre_build = self.config.pre_build(&self.target)?; + let pre_build = config.pre_build(&self.target)?; if let Some(pre_build) = pre_build { match pre_build { @@ -233,10 +242,18 @@ impl DockerPaths { self.workspace_from_cwd().is_ok() } + pub fn cargo_home(&self) -> &Path { + &self.directories.cargo + } + pub fn mount_cwd(&self) -> &str { &self.directories.mount_cwd } + pub fn mount_root(&self) -> &str { + &self.directories.mount_root + } + pub fn host_root(&self) -> &Path { &self.directories.host_root } @@ -467,16 +484,6 @@ pub(crate) fn cargo_safe_command(uses_xargo: bool) -> SafeCommand { } fn add_cargo_configuration_envvars(docker: &mut Command) { - let non_cargo_prefix = &[ - "http_proxy", - "TERM", - "RUSTDOCFLAGS", - "RUSTFLAGS", - "BROWSER", - "HTTPS_PROXY", - "HTTP_TIMEOUT", - "https_proxy", - ]; let cargo_prefix_skip = &[ "CARGO_HOME", "CARGO_TARGET_DIR", @@ -487,7 +494,7 @@ fn add_cargo_configuration_envvars(docker: &mut Command) { "CARGO_BUILD_RUSTDOC", ]; let is_cargo_passthrough = |key: &str| -> bool { - non_cargo_prefix.contains(&key) + CARGO_NO_PREFIX_ENVVARS.contains(&key) || key.starts_with("CARGO_") && !cargo_prefix_skip.contains(&key) }; @@ -502,7 +509,7 @@ fn add_cargo_configuration_envvars(docker: &mut Command) { pub(crate) fn docker_envvars( docker: &mut Command, - config: &Config, + config: &CrossConfig, dirs: &Directories, target: &Target, msg_info: &mut MessageInfo, @@ -558,8 +565,44 @@ pub(crate) fn build_command(dirs: &Directories, cmd: &SafeCommand) -> String { ) } -pub(crate) fn docker_cwd(docker: &mut Command, paths: &DockerPaths) -> Result<()> { +fn mount_to_ignore_cargo_config( + docker: &mut Command, + paths: &DockerPaths, + cargo_config_behavior: CargoConfigBehavior, +) -> Result<()> { + let check_mount = + |cmd: &mut Command, host: &Path, mount: &Path, relpath: &Path| -> Result<()> { + let cargo_dir = relpath.join(".cargo"); + if host.join(&cargo_dir).exists() { + // this is fine, since it has to be a POSIX path on the mount. + cmd.args(&["-v", &mount.join(&cargo_dir).as_posix_absolute()?]); + } + + Ok(()) + }; + if let CargoConfigBehavior::Ignore = cargo_config_behavior { + let mount_root = Path::new(paths.mount_root()); + let mount_cwd = Path::new(paths.mount_cwd()); + check_mount(docker, &paths.cwd, mount_cwd, Path::new(""))?; + // CWD isn't guaranteed to be a subdirectory of the mount root. + if let Ok(mut relpath) = mount_cwd.strip_prefix(mount_root) { + while let Some(parent) = relpath.parent() { + check_mount(docker, paths.host_root(), mount_root, parent)?; + relpath = parent; + } + } + } + + Ok(()) +} + +pub(crate) fn docker_cwd( + docker: &mut Command, + paths: &DockerPaths, + cargo_config_behavior: CargoConfigBehavior, +) -> Result<()> { docker.args(&["-w", paths.mount_cwd()]); + mount_to_ignore_cargo_config(docker, paths, cargo_config_behavior)?; Ok(()) } @@ -573,6 +616,7 @@ pub(crate) fn docker_mount( ) -> Result<()> { for ref var in options .config + .cross .env_volumes(&options.target)? .unwrap_or_default() { @@ -693,7 +737,7 @@ pub(crate) fn docker_seccomp( } /// Simpler version of [get_image] -pub fn get_image_name(config: &Config, target: &Target) -> Result { +pub fn get_image_name(config: &CrossConfig, target: &Target) -> Result { if let Some(image) = config.image(target)? { return Ok(image.name); } @@ -722,7 +766,7 @@ pub fn get_image_name(config: &Config, target: &Target) -> Result { .image_name(CROSS_IMAGE, version)) } -pub(crate) fn get_image(config: &Config, target: &Target) -> Result { +pub(crate) fn get_image(config: &CrossConfig, target: &Target) -> Result { if let Some(image) = config.image(target)? { return Ok(image); } diff --git a/src/lib.rs b/src/lib.rs index b147006b8..5a33be2da 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -31,8 +31,11 @@ mod tests; pub mod cargo; +mod cargo_config; +mod cargo_toml; pub mod cli; pub mod config; +mod cross_config; mod cross_toml; pub mod docker; pub mod errors; @@ -46,19 +49,21 @@ pub mod shell; pub mod temp; use std::env; -use std::path::PathBuf; use std::process::ExitStatus; use cli::Args; use color_eyre::owo_colors::OwoColorize; use color_eyre::{Help, SectionExt}; -use config::Config; use rustc::{QualifiedToolchain, Toolchain}; use rustc_version::Channel; use serde::{Deserialize, Serialize, Serializer}; pub use self::cargo::{cargo_command, cargo_metadata_with_args, CargoMetadata, Subcommand}; -use self::cross_toml::CrossToml; +pub use self::cargo_config::CargoConfig; +pub use self::cargo_toml::CargoToml; +pub use self::config::Config; +pub use self::cross_config::CrossConfig; +pub use self::cross_toml::CrossToml; use self::errors::Context; use self::shell::{MessageInfo, Verbosity}; @@ -456,6 +461,7 @@ fn warn_on_failure( pub fn run( args: Args, target_list: TargetList, + cargo_config: CargoConfig, msg_info: &mut MessageInfo, ) -> Result> { if args.version && args.subcommand.is_none() { @@ -471,18 +477,19 @@ pub fn run( let cwd = std::env::current_dir()?; if let Some(metadata) = cargo_metadata_with_args(None, Some(&args), msg_info)? { let host = host_version_meta.host(); - let toml = toml(&metadata, msg_info)?; - let config = Config::new(toml); + let cross_toml = CrossToml::read(&metadata, msg_info)?; + let cross_config = CrossConfig::new(cross_toml); + let config = Config::new(cargo_config, cross_config); let target = args .target - .or_else(|| config.target(&target_list)) + .or_else(|| config.cross.target(&target_list)) .unwrap_or_else(|| Target::from(host.triple(), &target_list)); - config.confusable_target(&target, msg_info)?; + config.cross.confusable_target(&target, msg_info)?; // Get the image we're supposed to base all our next actions on. // The image we actually run in might get changed with // `target.{{TARGET}}.dockerfile` or `target.{{TARGET}}.pre-build` - let image = match docker::get_image(&config, &target) { + let image = match docker::get_image(&config.cross, &target) { Ok(i) => i, Err(err) => { msg_info.warn(err)?; @@ -492,7 +499,7 @@ pub fn run( }; // Grab the current toolchain, this might be the one we mount in the image later - let default_toolchain = QualifiedToolchain::default(&config, msg_info)?; + let default_toolchain = QualifiedToolchain::default(&config.cross, msg_info)?; // `cross +channel`, where channel can be `+channel[-YYYY-MM-DD]` let mut toolchain = if let Some(channel) = args.channel { @@ -513,7 +520,7 @@ r#"Overriding the toolchain in cross is only possible in CLI by specifying a cha To override the toolchain mounted in the image, set `target.{}.image.toolchain = "{picked_host}"`"#, target).header("Note:".bright_cyan())); } - default_toolchain.with_picked(&config, picked_toolchain, msg_info)? + default_toolchain.with_picked(&config.cross, picked_toolchain, msg_info)? } else { default_toolchain }; @@ -565,9 +572,9 @@ To override the toolchain mounted in the image, set `target.{}.image.toolchain = is_nightly = channel == Channel::Nightly; } - let uses_build_std = config.build_std(&target).unwrap_or(false); + let uses_build_std = config.cross.build_std(&target).unwrap_or(false); let uses_xargo = - !uses_build_std && config.xargo(&target).unwrap_or(!target.is_builtin()); + !uses_build_std && config.cross.xargo(&target).unwrap_or(!target.is_builtin()); if !toolchain.is_custom { // build-std overrides xargo, but only use it if it's a built-in // tool but not an available target or doesn't have rust-std. @@ -623,7 +630,7 @@ To override the toolchain mounted in the image, set `target.{}.image.toolchain = }; let is_test = args.subcommand.map_or(false, |sc| sc == Subcommand::Test); - if is_test && config.doctests().unwrap_or_default() && is_nightly { + if is_test && config.cross.doctests().unwrap_or_default() && is_nightly { filtered_args.push("-Zdoctest-xcompile".to_owned()); } if uses_build_std { @@ -643,8 +650,16 @@ To override the toolchain mounted in the image, set `target.{}.image.toolchain = } let paths = docker::DockerPaths::create(&engine, metadata, cwd, toolchain.clone())?; - let options = - docker::DockerOptions::new(engine, target.clone(), config, image, uses_xargo); + let cargo_config_behavior = + config.cross.env_cargo_config(&target)?.unwrap_or_default(); + let options = docker::DockerOptions::new( + engine, + target.clone(), + config, + image, + uses_xargo, + cargo_config_behavior, + ); let status = docker::run(options, paths, &filtered_args, msg_info) .wrap_err("could not run container")?; let needs_host = args.subcommand.map_or(false, |sc| sc.needs_host(is_remote)); @@ -703,45 +718,3 @@ pub(crate) fn warn_host_version_mismatch( } Ok(VersionMatch::Same) } - -/// Obtains the [`CrossToml`] from one of the possible locations -/// -/// These locations are checked in the following order: -/// 1. If the `CROSS_CONFIG` variable is set, it tries to read the config from its value -/// 2. Otherwise, the `Cross.toml` in the project root is used -/// 3. Package metadata in the Cargo.toml -/// -/// The values from `CROSS_CONFIG` or `Cross.toml` are concatenated with the package -/// metadata in `Cargo.toml`, with `Cross.toml` having the highest priority. -fn toml(metadata: &CargoMetadata, msg_info: &mut MessageInfo) -> Result> { - let root = &metadata.workspace_root; - let cross_config_path = match env::var("CROSS_CONFIG") { - Ok(var) => PathBuf::from(var), - Err(_) => root.join("Cross.toml"), - }; - - // Attempts to read the cross config from the Cargo.toml - let cargo_toml_str = - file::read(root.join("Cargo.toml")).wrap_err("failed to read Cargo.toml")?; - - if cross_config_path.exists() { - let cross_toml_str = file::read(&cross_config_path) - .wrap_err_with(|| format!("could not read file `{cross_config_path:?}`"))?; - - let (config, _) = CrossToml::parse(&cargo_toml_str, &cross_toml_str, msg_info) - .wrap_err_with(|| format!("failed to parse file `{cross_config_path:?}` as TOML",))?; - - Ok(Some(config)) - } else { - // Checks if there is a lowercase version of this file - if root.join("cross.toml").exists() { - msg_info.warn("There's a file named cross.toml, instead of Cross.toml. You may want to rename it, or it won't be considered.")?; - } - - if let Some((cfg, _)) = CrossToml::parse_from_cargo(&cargo_toml_str, msg_info)? { - Ok(Some(cfg)) - } else { - Ok(None) - } - } -} diff --git a/src/rustc.rs b/src/rustc.rs index e36c947c0..ba36cf8a0 100644 --- a/src/rustc.rs +++ b/src/rustc.rs @@ -4,6 +4,7 @@ use std::process::Command; use rustc_version::{Version, VersionMeta}; use serde::Deserialize; +use crate::cross_config::CrossConfig; use crate::docker::ImagePlatform; use crate::errors::*; use crate::extensions::{env_program, CommandExt}; @@ -122,7 +123,7 @@ impl QualifiedToolchain { pub(crate) fn custom( name: &str, sysroot: &Path, - config: &crate::config::Config, + config: &CrossConfig, msg_info: &mut MessageInfo, ) -> Result { if let Some(compat) = config.custom_toolchain_compat() { @@ -188,7 +189,7 @@ impl QualifiedToolchain { } /// Grab the current default toolchain - pub fn default(config: &crate::config::Config, msg_info: &mut MessageInfo) -> Result { + pub fn default(config: &CrossConfig, msg_info: &mut MessageInfo) -> Result { let sysroot = sysroot(msg_info)?; let default_toolchain_name = sysroot @@ -207,7 +208,7 @@ impl QualifiedToolchain { /// Merge a "picked" toolchain, overriding set fields. pub fn with_picked( self, - config: &crate::config::Config, + config: &CrossConfig, picked: Toolchain, msg_info: &mut MessageInfo, ) -> Result { @@ -241,7 +242,7 @@ impl QualifiedToolchain { fn parse( sysroot: PathBuf, toolchain: &str, - config: &crate::config::Config, + config: &CrossConfig, msg_info: &mut MessageInfo, ) -> Result { match toolchain.parse::() { @@ -366,7 +367,7 @@ mod tests { QualifiedToolchain::custom( "bisector-nightly-2022-04-26-x86_64-unknown-linux-gnu", "/tmp/cross/sysroot".as_ref(), - &crate::config::Config::new(None), + &CrossConfig::new(None), &mut MessageInfo::create(true, false, None).unwrap(), ) .unwrap(); diff --git a/src/shell.rs b/src/shell.rs index 90171fed5..af29acc5e 100644 --- a/src/shell.rs +++ b/src/shell.rs @@ -9,6 +9,9 @@ use std::str::FromStr; use crate::errors::Result; use owo_colors::{self, OwoColorize}; +// the default error exit code for cargo. +pub const ERROR_CODE: i32 = 101; + // get the prefix for stderr messages macro_rules! cross_prefix { ($s:literal) => { @@ -82,9 +85,8 @@ impl Verbosity { fn create(color_choice: ColorChoice, verbose: bool, quiet: bool) -> Option { match (verbose, quiet) { - (true, true) => { - MessageInfo::from(color_choice).fatal("cannot set both --verbose and --quiet", 101) - } + (true, true) => MessageInfo::from(color_choice) + .fatal("cannot set both --verbose and --quiet", ERROR_CODE), (true, false) => Some(Verbosity::Verbose), (false, true) => Some(Verbosity::Quiet), (false, false) => None, @@ -206,7 +208,14 @@ impl MessageInfo { pub fn fatal(&mut self, message: T, code: i32) -> ! { self.error(message) .expect("could not display fatal message"); - std::process::exit(code); + + // need to catch panics in unittests, otherwise + // want the custom styled error message + if cfg!(test) { + panic!(""); + } else { + std::process::exit(code); + } } /// prints a red 'error' message.