diff --git a/.changes/931.json b/.changes/931.json index 50d3d5065..7ae58a56d 100644 --- a/.changes/931.json +++ b/.changes/931.json @@ -1,4 +1,5 @@ { - "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], } 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/src/cargo_config.rs b/src/cargo_config.rs new file mode 100644 index 000000000..46b8ae2e9 --- /dev/null +++ b/src/cargo_config.rs @@ -0,0 +1,57 @@ +use std::collections::HashMap; + +use crate::cargo_toml::CargoToml; +use crate::config::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| x.split_whitespace().map(ToOwned::to_owned).collect()) + } +} + +#[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), + }, + } + } +} diff --git a/src/cargo_toml.rs b/src/cargo_toml.rs new file mode 100644 index 000000000..ac5184812 --- /dev/null +++ b/src/cargo_toml.rs @@ -0,0 +1,263 @@ +use std::collections::BTreeSet; +use std::env; +use std::path::Path; + +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)?)) + } + + // 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(s.split_whitespace().map(ToOwned::to_owned).collect()) + } 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.clone(); + 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 bc7743fbe..d2347a1fd 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::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, @@ -173,128 +174,165 @@ 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, + parent: Option<&str>, +) -> Result<()> { + if parent == Some(&arg) { + MessageInfo::default().fatal( + format_args!("alias {arg} has unresolvable recursive definition: {arg} -> {arg}"), + shell::ERROR_CODE, + ); + } + let subcommand = Subcommand::from(arg.as_ref()); + if subcommand == Subcommand::Other { + if let Some(alias) = config.alias(&arg)? { + let mut iter = alias.iter().cloned(); + while let Some(subarg) = iter.next() { + parse_arg(subarg, result, target_list, config, &mut iter, Some(&arg))?; } - 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.clone()); + result.subcommand = Some(subcommand); + + Ok(()) +} + +fn parse_arg( + arg: String, + result: &mut Args, + target_list: &TargetList, + config: &CargoConfig, + iter: &mut impl Iterator, + parent_subcommand: Option<&str>, +) -> 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, parent_subcommand)?; + } 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); + while let Some(arg) = args.next() { + parse_arg(arg, &mut result, target_list, config, &mut args, None)?; + } + + Ok(result) } #[cfg(test)] mod tests { use super::*; + use crate::rustc; + use crate::shell::Verbosity; #[test] fn is_verbose_test() { @@ -307,4 +345,22 @@ mod tests { assert!(is_verbose("-vvvv")); assert!(!is_verbose("-version")); } + + #[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, + Some("recursive"), + ) + .ok(); + } } diff --git a/src/config.rs b/src/config.rs index a5d583016..503254f43 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,128 +1,9 @@ -use crate::docker::custom::PreBuild; -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) -> Option { - self.get_target_var(target, "IMAGE") - } - - 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 { - env::var("CROSS_UNSTABLE_ENABLE_DOCTESTS") - .map(|s| bool_from_envvar(&s)) - .ok() - } - - fn custom_toolchain(&self) -> bool { - std::env::var("CROSS_CUSTOM_TOOLCHAIN").is_ok() - } -} - -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) { @@ -135,627 +16,33 @@ pub fn bool_from_envvar(envvar: &str) -> bool { } #[derive(Debug)] -pub struct Config { - toml: Option, - env: Environment, -} - -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 string_from_config( - &self, - target: &Target, - env: impl Fn(&Environment, &Target) -> Option, - config: impl Fn(&CrossToml, &Target) -> Option, - ) -> Result> { - let env_value = env(&self.env, target); - if let Some(env_value) = env_value { - return Ok(Some(env_value)); - } - self.toml - .as_ref() - .map_or(Ok(None), |t| Ok(config(t, target))) - } +pub struct Environment(&'static str, Option>); - 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> { - self.string_from_config(target, Environment::image, CrossToml::image) - } - - pub fn runner(&self, target: &Target) -> Result> { - self.string_from_config(target, Environment::runner, CrossToml::runner) - } - - pub fn doctests(&self) -> Option { - self.env.doctests() - } - - pub fn custom_toolchain(&self) -> bool { - self.env.custom_toolchain() - } - - 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)) - } - - 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..7c124f7a4 --- /dev/null +++ b/src/cross_config.rs @@ -0,0 +1,742 @@ +use std::collections::HashMap; + +use crate::config::{bool_from_envvar, Environment}; +use crate::docker::custom::PreBuild; +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) -> Option { + self.get_target_var(target, "IMAGE") + } + + 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.0 + .get_var("CROSS_UNSTABLE_ENABLE_DOCTESTS") + .map(|s| bool_from_envvar(&s)) + } + + fn custom_toolchain(&self) -> bool { + self.0.get_var("CROSS_UNSTABLE_ENABLE_DOCTESTS").is_some() + } +} + +fn split_to_cloned_by_ws(string: &str) -> Vec { + string.split_whitespace().map(String::from).collect() +} + +#[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 string_from_config( + &self, + target: &Target, + env: impl Fn(&CrossEnvironment, &Target) -> Option, + config: impl Fn(&CrossToml, &Target) -> Option, + ) -> Result> { + let env_value = env(&self.env, target); + if let Some(env_value) = env_value { + return Ok(Some(env_value)); + } + self.toml + .as_ref() + .map_or(Ok(None), |t| Ok(config(t, target))) + } + + 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> { + self.string_from_config(target, CrossEnvironment::image, CrossToml::image) + } + + pub fn runner(&self, target: &Target) -> Result> { + self.string_from_config(target, CrossEnvironment::runner, CrossToml::runner) + } + + pub fn doctests(&self) -> Option { + self.env.doctests() + } + + pub fn custom_toolchain(&self) -> bool { + self.env.custom_toolchain() + } + + 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)) + } + } +} + +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 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_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 819143f05..809c5baa9 100644 --- a/src/cross_toml.rs +++ b/src/cross_toml.rs @@ -1,13 +1,17 @@ #![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::shell::MessageInfo; -use crate::{config, errors::*}; +use crate::{cross_config, 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)] @@ -78,6 +82,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, @@ -250,7 +298,7 @@ impl CrossToml { .as_ref() .and_then(|d| d.build_args.as_ref()); - config::opt_merge(target.cloned(), build.cloned()) + cross_config::opt_merge(target.cloned(), build.cloned()) } /// Returns the `build.dockerfile.pre-build` and `target.{}.dockerfile.pre-build` part of `Cross.toml` diff --git a/src/docker/custom.rs b/src/docker/custom.rs index 31e2df6db..b40c1c19d 100644 --- a/src/docker/custom.rs +++ b/src/docker/custom.rs @@ -117,7 +117,7 @@ impl<'a> Dockerfile<'a> { }; if matches!(self, Dockerfile::File { .. }) { - if let Ok(cross_base_image) = self::image_name(&options.config, &options.target) { + if let Ok(cross_base_image) = self::image_name(&options.config.cross, &options.target) { docker_build.args([ "--build-arg", &format!("CROSS_BASE_IMAGE={cross_base_image}"), diff --git a/src/docker/local.rs b/src/docker/local.rs index 72ecd9c49..4d1a2da1f 100644 --- a/src/docker/local.rs +++ b/src/docker/local.rs @@ -22,7 +22,12 @@ pub(crate) fn run( let mut docker = subcommand(engine, "run"); docker_userns(&mut docker); - docker_envvars(&mut docker, &options.config, &options.target, msg_info)?; + docker_envvars( + &mut docker, + &options.config.cross, + &options.target, + msg_info, + )?; docker_mount( &mut docker, diff --git a/src/docker/remote.rs b/src/docker/remote.rs index 88f5c8a3a..2779686d7 100644 --- a/src/docker/remote.rs +++ b/src/docker/remote.rs @@ -1169,7 +1169,7 @@ 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, target, msg_info)?; + docker_envvars(&mut docker, &options.config.cross, target, msg_info)?; docker_cwd(&mut docker, &paths)?; docker.arg(&container); docker.args(&["sh", "-c", &format!("PATH=$PATH:/rust/bin {:?}", cmd)]); diff --git a/src/docker/shared.rs b/src/docker/shared.rs index 573ba2458..ac6618bf6 100644 --- a/src/docker/shared.rs +++ b/src/docker/shared.rs @@ -6,7 +6,9 @@ use std::{env, fs}; use super::custom::{Dockerfile, PreBuild}; use super::engine::*; 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::errors::*; use crate::extensions::{CommandExt, SafeCommand}; use crate::file::{self, write_file, ToUtf8}; @@ -62,11 +64,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() @@ -79,9 +83,9 @@ impl DockerOptions { ) -> Result { let mut image = self.image_name()?; - if let Some(path) = self.config.dockerfile(&self.target)? { - let context = self.config.dockerfile_context(&self.target)?; - let name = self.config.image(&self.target)?; + if let Some(path) = self.config.cross.dockerfile(&self.target)? { + let context = self.config.cross.dockerfile_context(&self.target)?; + let name = self.config.cross.image(&self.target)?; let build = Dockerfile::File { path: &path, @@ -94,13 +98,14 @@ impl DockerOptions { self, paths, self.config + .cross .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 = self.config.cross.pre_build(&self.target)?; if let Some(pre_build) = pre_build { match pre_build { @@ -170,7 +175,7 @@ impl DockerOptions { } pub(crate) fn image_name(&self) -> Result { - image_name(&self.config, &self.target) + image_name(&self.config.cross, &self.target) } } @@ -407,16 +412,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", @@ -427,7 +422,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) }; @@ -452,7 +447,7 @@ pub(crate) fn mount(docker: &mut Command, host_path: &Path, prefix: &str) -> Res pub(crate) fn docker_envvars( docker: &mut Command, - config: &Config, + config: &CrossConfig, target: &Target, msg_info: &mut MessageInfo, ) -> Result<()> { @@ -514,6 +509,7 @@ pub(crate) fn docker_mount( ) -> Result<()> { for ref var in options .config + .cross .env_volumes(&options.target)? .unwrap_or_default() { @@ -626,7 +622,7 @@ pub(crate) fn docker_seccomp( Ok(()) } -pub(crate) fn image_name(config: &Config, target: &Target) -> Result { +pub(crate) fn image_name(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 31f6d8f1f..0ad70a2e3 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -31,8 +31,11 @@ mod tests; mod cargo; +mod cargo_config; +mod cargo_toml; mod cli; mod config; +mod cross_config; mod cross_toml; pub mod docker; pub mod errors; @@ -47,14 +50,16 @@ pub mod temp; use std::env; use std::io::{self, Write}; -use std::path::PathBuf; use std::process::ExitStatus; -use config::Config; use rustc_version::Channel; use serde::{Deserialize, Serialize, Serializer}; pub use self::cargo::{cargo_command, cargo_metadata_with_args, CargoMetadata, Subcommand}; +use self::cargo_config::CargoConfig; +use self::cargo_toml::CargoToml; +use self::config::Config; +use self::cross_config::CrossConfig; use self::cross_toml::CrossToml; use self::errors::Context; use self::shell::{MessageInfo, Verbosity}; @@ -398,7 +403,9 @@ fn warn_on_failure(target: &Target, toolchain: &str, msg_info: &mut MessageInfo) pub fn run() -> Result { 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 mut msg_info = shell::MessageInfo::create(args.verbose, args.quiet, args.color.as_deref())?; if args.version && args.subcommand.is_none() { @@ -413,15 +420,15 @@ pub fn run() -> Result { let cwd = std::env::current_dir()?; if let Some(metadata) = cargo_metadata_with_args(None, Some(&args), &mut msg_info)? { let host = host_version_meta.host(); - let toml = toml(&metadata, &mut msg_info)?; - let config = Config::new(toml); + let toml = CrossToml::read(&metadata, &mut msg_info)?; + let cross_config = CrossConfig::new(toml); let target = args .target - .or_else(|| config.target(&target_list)) + .or_else(|| cross_config.target(&target_list)) .unwrap_or_else(|| Target::from(host.triple(), &target_list)); - config.confusable_target(&target, &mut msg_info)?; + cross_config.confusable_target(&target, &mut msg_info)?; - let image_exists = match docker::image_name(&config, &target) { + let image_exists = match docker::image_name(&cross_config, &target) { Ok(_) => true, Err(err) => { msg_info.warn(err)?; @@ -451,10 +458,10 @@ pub fn run() -> Result { is_nightly = channel == Channel::Nightly; } - let uses_build_std = config.build_std(&target).unwrap_or(false); + let uses_build_std = cross_config.build_std(&target).unwrap_or(false); let uses_xargo = - !uses_build_std && config.xargo(&target).unwrap_or(!target.is_builtin()); - if !config.custom_toolchain() { + !uses_build_std && cross_config.xargo(&target).unwrap_or(!target.is_builtin()); + if !cross_config.custom_toolchain() { // 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. let available_targets = rustup::available_targets(&toolchain, &mut msg_info)?; @@ -510,7 +517,7 @@ pub fn run() -> Result { }; 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 && cross_config.doctests().unwrap_or_default() && is_nightly { filtered_args.push("-Zdoctest-xcompile".to_owned()); } if uses_build_std { @@ -532,6 +539,7 @@ pub fn run() -> Result { } let paths = docker::DockerPaths::create(&engine, metadata, cwd, sysroot)?; + let config = Config::new(cargo_config, cross_config); let options = docker::DockerOptions::new(engine, target.clone(), config, uses_xargo); let status = docker::run(options, paths, &filtered_args, &mut msg_info) @@ -612,45 +620,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/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.