diff --git a/CHANGELOG.md b/CHANGELOG.md index 67752ebe9..bd97cde15 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,7 @@ This project adheres to [Semantic Versioning](http://semver.org/). - #678 - Add `target.{target}.pre-build` config for running commands before building the image. - #772 - added `CROSS_CONTAINER_OPTS` environment variable to replace `DOCKER_OPTS`. - #767, #788 - added the `cross-util` and `xtask` commands. +- #842 - Add `Cargo.toml` as configuration source - #745 - added `thumbv7neon-*` targets. - #741 - added `armv7-unknown-linux-gnueabi` and `armv7-unknown-linux-musleabi` targets. - #721 - add support for running doctests on nightly if `CROSS_UNSTABLE_ENABLE_DOCTESTS=true`. diff --git a/README.md b/README.md index b453c9bac..e4c070cec 100644 --- a/README.md +++ b/README.md @@ -84,9 +84,27 @@ Additional documentation can be found on the [wiki](https://github.com/cross-rs/ ## Configuration -You can place a `Cross.toml` file in the root of your Cargo project or use a -`CROSS_CONFIG` environment variable to tweak `cross`'s behavior. The format -of `Cross.toml` is documented in [docs/cross_toml.md](docs/cross_toml.md). +You have three options to configure `cross`. All of these options use the TOML format for configuration and the possible configuration values are documented [here](docs/cross_toml.md). + +### Option 1: Configuring `cross` directly in your `Cargo.toml` + +You can directly set [configuration values](docs/cross_toml.md) in your `Cargo.toml` file, under the `[package.metadata.cross]` table, i.e. key prefix. +An example config snippet would look like this: + +``` +[package.metadata.cross.target.aarch64-unknown-linux-gnu] +xargo = false +image = "test-image" +runner = "custom-runner" +``` + +### Option 2: Configuring `cross` via a `Cross.toml` file + +You can put your [configuration](docs/cross_toml.md) inside a `Cross.toml` file in your project root directory. + +### Option 3: Using `CROSS_CONFIG` to specify the location of your configuration + +By setting the `CROSS_CONFIG` environment variable, you can tell `cross` where it should search for the config file. This way you are not limited to a `Cross.toml` file in the project root. ### Custom Docker images diff --git a/docs/cross_toml.md b/docs/cross_toml.md index bedfc49d9..51f878302 100644 --- a/docs/cross_toml.md +++ b/docs/cross_toml.md @@ -1,4 +1,6 @@ -The `cross` configuration in the `Cross.toml` file, can contain the following elements: +The `cross` configuration in the `Cross.toml` file, can contain the elements described below. + +If the configuration is given in the `Cargo.toml`, these table headers must be of the form `[package.metadata.cross.]`. # `build` diff --git a/src/config.rs b/src/config.rs index dcc39eda4..bcb3d4977 100644 --- a/src/config.rs +++ b/src/config.rs @@ -431,7 +431,9 @@ mod tests { use std::matches; fn toml(content: &str) -> Result { - Ok(CrossToml::parse(content).wrap_err("couldn't parse toml")?.0) + Ok(CrossToml::parse_from_cross(content) + .wrap_err("couldn't parse toml")? + .0) } #[test] diff --git a/src/cross_toml.rs b/src/cross_toml.rs index 2d073002f..8b0c5e489 100644 --- a/src/cross_toml.rs +++ b/src/cross_toml.rs @@ -2,19 +2,20 @@ use crate::{config, errors::*}; use crate::{Target, TargetList}; -use serde::Deserialize; +use serde::de::DeserializeOwned; +use serde::{Deserialize, Deserializer, Serialize}; use std::collections::{BTreeSet, HashMap}; use std::str::FromStr; /// Environment configuration -#[derive(Debug, Deserialize, PartialEq, Eq, Default)] +#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Default)] pub struct CrossEnvConfig { volumes: Option>, passthrough: Option>, } /// Build configuration -#[derive(Debug, Deserialize, PartialEq, Eq, Default)] +#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Default)] #[serde(rename_all = "kebab-case")] pub struct CrossBuildConfig { #[serde(default)] @@ -28,7 +29,7 @@ pub struct CrossBuildConfig { } /// Target configuration -#[derive(Debug, Deserialize, PartialEq, Eq)] +#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)] #[serde(rename_all = "kebab-case")] pub struct CrossTargetConfig { xargo: Option, @@ -43,7 +44,7 @@ pub struct CrossTargetConfig { } /// Dockerfile configuration -#[derive(Debug, Deserialize, PartialEq, Eq)] +#[derive(Debug, Deserialize, Serialize, PartialEq, Eq)] #[serde(rename_all = "kebab-case")] pub struct CrossTargetDockerfileConfig { file: String, @@ -64,7 +65,7 @@ impl FromStr for CrossTargetDockerfileConfig { } /// Cross configuration -#[derive(Debug, Deserialize, PartialEq, Eq)] +#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Default)] pub struct CrossToml { #[serde(default, rename = "target")] pub targets: HashMap, @@ -73,13 +74,46 @@ pub struct CrossToml { } impl CrossToml { + /// Parses the [`CrossToml`] from all of the config sources + pub fn parse(cargo_toml: &str, cross_toml: &str) -> Result<(Self, BTreeSet)> { + let (cross_toml, unused) = Self::parse_from_cross(cross_toml)?; + + if let Some((cargo_toml, _)) = Self::parse_from_cargo(cargo_toml)? { + Ok((cargo_toml.merge(cross_toml)?, unused)) + } else { + Ok((cross_toml, unused)) + } + } + /// Parses the [`CrossToml`] from a string - pub fn parse(toml_str: &str) -> Result<(Self, BTreeSet)> { - let tomld = &mut toml::Deserializer::new(toml_str); + pub fn parse_from_cross(toml_str: &str) -> Result<(Self, BTreeSet)> { + let mut tomld = toml::Deserializer::new(toml_str); + Self::parse_from_deserializer(&mut tomld) + } - let mut unused = BTreeSet::new(); + /// Parses the [`CrossToml`] from a string containing the Cargo.toml contents + pub fn parse_from_cargo(cargo_toml_str: &str) -> Result)>> { + let cargo_toml: toml::Value = toml::from_str(cargo_toml_str)?; + let cross_metadata_opt = cargo_toml + .get("package") + .and_then(|p| p.get("metadata")) + .and_then(|m| m.get("cross")); + + if let Some(cross_meta) = cross_metadata_opt { + Ok(Some(Self::parse_from_deserializer(cross_meta.clone())?)) + } else { + Ok(None) + } + } - let cfg = serde_ignored::deserialize(tomld, |path| { + /// Parses the [`CrossToml`] from a [`Deserializer`] + fn parse_from_deserializer<'de, D>(deserializer: D) -> Result<(Self, BTreeSet)> + where + D: Deserializer<'de>, + D::Error: Send + Sync + 'static, + { + let mut unused = BTreeSet::new(); + let cfg = serde_ignored::deserialize(deserializer, |path| { unused.insert(path.to_string()); })?; @@ -93,6 +127,72 @@ impl CrossToml { Ok((cfg, unused)) } + /// Merges another [`CrossToml`] into `self` and returns a new merged one + pub fn merge(self, other: CrossToml) -> Result { + type ValueMap = serde_json::Map; + + fn to_map(s: S) -> Result { + if let Some(obj) = serde_json::to_value(s) + .wrap_err("could not convert CrossToml to serde_json::Value")? + .as_object() + { + Ok(obj.to_owned()) + } else { + eyre::bail!("failed to serialize CrossToml as object"); + } + } + + fn from_map(map: ValueMap) -> Result { + let value = serde_json::to_value(map) + .wrap_err("could not convert ValueMap to serde_json::Value")?; + serde_json::from_value(value) + .wrap_err("could not deserialize serde_json::Value to CrossToml") + } + + // merge 2 objects. y has precedence over x. + fn merge_objects(x: &mut ValueMap, y: &ValueMap) -> 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); + if !in_x && in_y { + let yk = y[&key].clone(); + x.insert(key, yk); + continue; + } else if !in_y { + continue; + } + + let xk = x.get_mut(&key)?; + let yk = y.get(&key)?; + if xk.is_null() && !yk.is_null() { + *xk = yk.clone(); + continue; + } else if yk.is_null() { + continue; + } + + // now we've filtered out missing keys and optional values + // all key/value pairs should be same type. + if xk.is_object() { + merge_objects(xk.as_object_mut()?, yk.as_object()?)?; + } else { + *xk = yk.clone(); + } + } + + Some(()) + } + + // Builds maps of objects + let mut self_map = to_map(&self)?; + let other_map = to_map(other)?; + + merge_objects(&mut self_map, &other_map).ok_or_else(|| eyre::eyre!("could not merge"))?; + from_map(self_map) + } + /// Returns the `target.{}.image` part of `Cross.toml` pub fn image(&self, target: &Target) -> Option { self.get_string(target, |_| None, |t| t.image.as_ref()) @@ -258,6 +358,20 @@ where Deserialize::deserialize(de::value::MapAccessDeserializer::new(map)); t.map(Some) } + + fn visit_none(self) -> Result + where + E: de::Error, + { + Ok(None) + } + + fn visit_unit(self) -> Result + where + E: de::Error, + { + Ok(None) + } } deserializer.deserialize_any(StringOrStruct(PhantomData)) @@ -273,7 +387,7 @@ mod tests { targets: HashMap::new(), build: CrossBuildConfig::default(), }; - let (parsed_cfg, unused) = CrossToml::parse("")?; + let (parsed_cfg, unused) = CrossToml::parse_from_cross("")?; assert_eq!(parsed_cfg, cfg); assert!(unused.is_empty()); @@ -307,7 +421,7 @@ mod tests { volumes = ["VOL1_ARG", "VOL2_ARG"] passthrough = ["VAR1", "VAR2"] "#; - let (parsed_cfg, unused) = CrossToml::parse(test_str)?; + let (parsed_cfg, unused) = CrossToml::parse_from_cross(test_str)?; assert_eq!(parsed_cfg, cfg); assert!(unused.is_empty()); @@ -351,7 +465,7 @@ mod tests { image = "test-image" pre-build = [] "#; - let (parsed_cfg, unused) = CrossToml::parse(test_str)?; + let (parsed_cfg, unused) = CrossToml::parse_from_cross(test_str)?; assert_eq!(parsed_cfg, cfg); assert!(unused.is_empty()); @@ -415,11 +529,176 @@ mod tests { [target.aarch64-unknown-linux-gnu.env] volumes = ["VOL"] "#; - let (parsed_cfg, unused) = CrossToml::parse(test_str)?; + let (parsed_cfg, unused) = CrossToml::parse_from_cross(test_str)?; assert_eq!(parsed_cfg, cfg); assert!(unused.is_empty()); Ok(()) } + + #[test] + pub fn parse_from_empty_cargo_toml() -> Result<()> { + let test_str = r#" + [package] + name = "cargo_toml_test_package" + version = "0.1.0" + + [dependencies] + cross = "1.2.3" + "#; + + let res = CrossToml::parse_from_cargo(test_str)?; + assert!(res.is_none()); + + Ok(()) + } + + #[test] + pub fn parse_from_cargo_toml() -> Result<()> { + let cfg = CrossToml { + targets: HashMap::new(), + build: CrossBuildConfig { + env: CrossEnvConfig { + passthrough: None, + volumes: None, + }, + build_std: None, + xargo: Some(true), + default_target: None, + pre_build: None, + dockerfile: None, + }, + }; + + let test_str = r#" + [package] + name = "cargo_toml_test_package" + version = "0.1.0" + + [dependencies] + cross = "1.2.3" + + [package.metadata.cross.build] + xargo = true + "#; + + if let Some((parsed_cfg, _unused)) = CrossToml::parse_from_cargo(test_str)? { + assert_eq!(parsed_cfg, cfg); + } else { + panic!("Parsing result is None"); + } + + Ok(()) + } + + #[test] + pub fn merge() -> Result<()> { + let cfg1_str = r#" + [target.aarch64-unknown-linux-gnu] + xargo = false + build-std = true + image = "test-image1" + + [target.aarch64-unknown-linux-gnu.env] + volumes = ["VOL1_ARG"] + passthrough = ["VAR1"] + + [target.target2] + xargo = false + build-std = true + image = "test-image2" + + [target.target2.env] + volumes = ["VOL2_ARG"] + passthrough = ["VAR2"] + + [build] + build-std = true + xargo = true + + [build.env] + volumes = [] + passthrough = ["VAR1", "VAR2"] + "#; + + let cfg2_str = r#" + [target.target2] + xargo = false + build-std = false + image = "test-image2-precedence" + + [target.target2.env] + volumes = ["VOL2_ARG_PRECEDENCE"] + passthrough = ["VAR2_PRECEDENCE"] + + [target.target3] + xargo = false + build-std = true + image = "test-image3" + + [target.target3.env] + volumes = ["VOL3_ARG"] + passthrough = ["VAR3"] + + [build] + build-std = true + xargo = false + default-target = "aarch64-unknown-linux-gnu" + + [build.env] + volumes = [] + passthrough = ["VAR3", "VAR4"] + + "#; + + let cfg_expected_str = r#" + [target.aarch64-unknown-linux-gnu] + xargo = false + build-std = true + image = "test-image1" + + [target.aarch64-unknown-linux-gnu.env] + volumes = ["VOL1_ARG"] + passthrough = ["VAR1"] + + [target.target2] + xargo = false + build-std = false + image = "test-image2-precedence" + + [target.target2.env] + volumes = ["VOL2_ARG_PRECEDENCE"] + passthrough = ["VAR2_PRECEDENCE"] + + [target.target3] + xargo = false + build-std = true + image = "test-image3" + + [target.target3.env] + volumes = ["VOL3_ARG"] + passthrough = ["VAR3"] + + [build] + build-std = true + xargo = false + default-target = "aarch64-unknown-linux-gnu" + + [build.env] + volumes = [] + passthrough = ["VAR3", "VAR4"] + "#; + + // Parses configs + let (cfg1, _) = CrossToml::parse_from_cross(cfg1_str)?; + let (cfg2, _) = CrossToml::parse_from_cross(cfg2_str)?; + let (cfg_expected, _) = CrossToml::parse_from_cross(cfg_expected_str)?; + + // Merges config and compares + let cfg_merged = cfg1.merge(cfg2)?; + assert_eq!(cfg_expected, cfg_merged); + + Ok(()) + } } diff --git a/src/lib.rs b/src/lib.rs index 55746e605..d793adf8a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -37,7 +37,7 @@ use std::process::ExitStatus; use config::Config; use rustc_version::Channel; -use serde::Deserialize; +use serde::{Deserialize, Serialize, Serializer}; pub use self::cargo::{cargo_command, cargo_metadata_with_args, CargoMetadata, Subcommand}; use self::cross_toml::CrossToml; @@ -145,7 +145,7 @@ impl<'a> From<&'a str> for Host { } #[derive(Debug, Clone, PartialEq, Eq, Hash, Deserialize)] -#[serde(from = "&str")] +#[serde(from = "String")] pub enum Target { BuiltIn { triple: String }, Custom { triple: String }, @@ -338,13 +338,22 @@ impl From for Target { } } -impl From<&str> for Target { - fn from(target_str: &str) -> Target { - let target_host: Host = target_str.into(); +impl From for Target { + fn from(target_str: String) -> Target { + let target_host: Host = target_str.as_str().into(); target_host.into() } } +impl Serialize for Target { + fn serialize(&self, serializer: S) -> Result { + match self { + Target::BuiltIn { triple } => serializer.serialize_str(triple), + Target::Custom { triple } => serializer.serialize_str(triple), + } + } +} + pub fn run() -> Result { let target_list = rustc::target_list(false)?; let args = cli::parse(&target_list)?; @@ -582,27 +591,44 @@ pub(crate) fn warn_host_version_mismatch( Ok(VersionMatch::Same) } -/// Parses the `Cross.toml` at the root of the Cargo project or from the -/// `CROSS_CONFIG` environment variable (if any exist in either location). +/// 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) -> Result> { - let path = match env::var("CROSS_CONFIG") { + let root = &metadata.workspace_root; + let cross_config_path = match env::var("CROSS_CONFIG") { Ok(var) => PathBuf::from(var), - Err(_) => metadata.workspace_root.join("Cross.toml"), + Err(_) => root.join("Cross.toml"), }; - if path.exists() { - let content = - file::read(&path).wrap_err_with(|| format!("could not read file `{path:?}`"))?; + // 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")?; - let (config, _) = CrossToml::parse(&content) - .wrap_err_with(|| format!("failed to parse file `{path:?}` as 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) + .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 metadata.workspace_root.join("cross.toml").exists() { + if root.join("cross.toml").exists() { eprintln!("There's a file named cross.toml, instead of Cross.toml. You may want to rename it, or it won't be considered."); } - Ok(None) + + if let Some((cfg, _)) = CrossToml::parse_from_cargo(&cargo_toml_str)? { + Ok(Some(cfg)) + } else { + Ok(None) + } } } diff --git a/src/tests/toml.rs b/src/tests/toml.rs index c7479f165..da5369a58 100644 --- a/src/tests/toml.rs +++ b/src/tests/toml.rs @@ -41,9 +41,11 @@ fn toml_check() -> Result<(), Box> { dir_entry.path(), text_line_no(&contents, fence.range().start), ); - assert!(crate::cross_toml::CrossToml::parse(fence.as_str())? - .1 - .is_empty()); + assert!( + crate::cross_toml::CrossToml::parse_from_cross(fence.as_str())? + .1 + .is_empty() + ); } } Ok(())