Repository: cross-rs/cross (rust)

Issue:
Enable parsing config values in virtual manifests
### Checklist

- [X] I've looked through the [issues and pull requests](https://github.com/cross-rs/cross/issues?q=) for similar request
- [ ] This feature could be solved with a [custom docker image](https://github.com/cross-rs/cross#custom-docker-images) (optional)

### Describe your request

Cross should be able to parse config values from `[workspace.metadata.cross]` sections. 

### Describe why this would be a good inclusion for `cross`

It seems to be impossible to set per-workspace config values when using virtual manifests without editing `Cross.toml`, since `[package.metadata]` sections do not work in virtual manifests. 

An applicable scenario would be when the `CROSS_CONFIG` variable is set to a global, constant, `Cross.toml` file. 

Pull request:
metadata config
some minor refactoring and parse `[workspace.metadata.cross]`

fixes #1158


Patch:
diff --git a/.changes/1159.json b/.changes/1159.json
new file mode 100644
index 0000000..123fb49
--- /dev/null
+++ b/.changes/1159.json
@@ -0,0 +1,4 @@
+{
+    "type": "changed",
+    "description": "use `[workspace.metadata.cross]` as an alternative to `Cross.toml`"
+}
diff --git a/Cargo.toml b/Cargo.toml
index 442fab0..8d1ed0d 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -38,7 +38,7 @@ toml = "0.7.0"
 which = { version = "4.4.0", default_features = false }
 shell-escape = "0.1.5"
 serde = { version = "1.0.152", features = ["derive"] }
-serde_json = "1.0.91"
+serde_json = { version = "1.0.91", features = ["raw_value"] }
 serde_ignored = "0.1.7"
 shell-words = "1.1.0"
 const-sha1 = "0.2.0"
diff --git a/src/bin/commands/run.rs b/src/bin/commands/run.rs
index 13500a7..e6ad62a 100644
--- a/src/bin/commands/run.rs
+++ b/src/bin/commands/run.rs
@@ -61,7 +61,7 @@ impl Run {
                 };
 
             let toml = toml(&metadata, msg_info)?;
-            let config = Config::new(toml);
+            let config = Config::new(Some(toml));
 
             let image = match docker::get_image(&config, &target, false) {
                 Ok(i) => i,
diff --git a/src/cargo.rs b/src/cargo.rs
index 80d69fa..010c655 100644
--- a/src/cargo.rs
+++ b/src/cargo.rs
@@ -76,6 +76,7 @@ pub struct CargoMetadata {
     pub target_directory: PathBuf,
     pub packages: Vec<Package>,
     pub workspace_members: Vec<String>,
+    pub metadata: Option<Box<serde_json::value::RawValue>>,
 }
 
 impl CargoMetadata {
@@ -105,6 +106,7 @@ pub struct Package {
     pub source: Option<String>,
     pub version: String,
     pub license: Option<String>,
+    pub metadata: Option<Box<serde_json::value::RawValue>>,
 }
 
 impl Package {
diff --git a/src/config.rs b/src/config.rs
index 063c208..6b7ab8c 100644
--- a/src/config.rs
+++ b/src/config.rs
@@ -603,7 +603,7 @@ mod tests {
 
         fn toml(content: &str) -> Result<crate::CrossToml> {
             Ok(
-                CrossToml::parse_from_cross(content, &mut MessageInfo::default())
+                CrossToml::parse_from_cross_str(content, None, &mut MessageInfo::default())
                     .wrap_err("couldn't parse toml")?
                     .0,
             )
diff --git a/src/cross_toml.rs b/src/cross_toml.rs
index c3ebbd3..2884e9f 100644
--- a/src/cross_toml.rs
+++ b/src/cross_toml.rs
@@ -122,33 +122,18 @@ pub struct CrossToml {
 }
 
 impl CrossToml {
-    /// Parses the [`CrossToml`] from all of the config sources
-    pub fn parse(
-        cargo_toml: &str,
-        cross_toml: &str,
-        msg_info: &mut MessageInfo,
-    ) -> Result<(Self, BTreeSet<String>)> {
-        let (cross_toml, mut unused) = Self::parse_from_cross(cross_toml, msg_info)?;
-
-        if let Some((cargo_toml, u_cargo)) = Self::parse_from_cargo(cargo_toml, msg_info)? {
-            unused.extend(u_cargo);
-            Ok((cargo_toml.merge(cross_toml)?, unused))
-        } else {
-            Ok((cross_toml, unused))
-        }
-    }
-
     /// Parses the [`CrossToml`] from a string
-    pub fn parse_from_cross(
+    pub fn parse_from_cross_str(
         toml_str: &str,
+        source: Option<&str>,
         msg_info: &mut MessageInfo,
     ) -> Result<(Self, BTreeSet<String>)> {
         let tomld = toml::Deserializer::new(toml_str);
-        Self::parse_from_deserializer(tomld, msg_info)
+        Self::parse_from_deserializer(tomld, source, msg_info)
     }
 
     /// Parses the [`CrossToml`] from a string containing the Cargo.toml contents
-    pub fn parse_from_cargo(
+    pub fn parse_from_cargo_package_str(
         cargo_toml_str: &str,
         msg_info: &mut MessageInfo,
     ) -> Result<Option<(Self, BTreeSet<String>)>> {
@@ -161,6 +146,7 @@ impl CrossToml {
         if let Some(cross_meta) = cross_metadata_opt {
             Ok(Some(Self::parse_from_deserializer(
                 cross_meta.clone(),
+                None,
                 msg_info,
             )?))
         } else {
@@ -169,8 +155,9 @@ impl CrossToml {
     }
 
     /// Parses the [`CrossToml`] from a [`Deserializer`]
-    fn parse_from_deserializer<'de, D>(
+    pub fn parse_from_deserializer<'de, D>(
         deserializer: D,
+        source: Option<&str>,
         msg_info: &mut MessageInfo,
     ) -> Result<(Self, BTreeSet<String>)>
     where
@@ -184,7 +171,8 @@ impl CrossToml {
 
         if !unused.is_empty() {
             msg_info.warn(format_args!(
-                "found unused key(s) in Cross configuration:\n > {}",
+                "found unused key(s) in Cross configuration{}:\n > {}",
+                source.map(|s| format!(" at {s}")).unwrap_or_default(),
                 unused.clone().into_iter().collect::<Vec<_>>().join(", ")
             ))?;
         }
@@ -598,7 +586,7 @@ mod tests {
             targets: HashMap::new(),
             build: CrossBuildConfig::default(),
         };
-        let (parsed_cfg, unused) = CrossToml::parse_from_cross("", &mut m!())?;
+        let (parsed_cfg, unused) = CrossToml::parse_from_cross_str("", None, &mut m!())?;
 
         assert_eq!(parsed_cfg, cfg);
         assert!(unused.is_empty());
@@ -633,7 +621,7 @@ mod tests {
           volumes = ["VOL1_ARG", "VOL2_ARG"]
           passthrough = ["VAR1", "VAR2"]
         "#;
-        let (parsed_cfg, unused) = CrossToml::parse_from_cross(test_str, &mut m!())?;
+        let (parsed_cfg, unused) = CrossToml::parse_from_cross_str(test_str, None, &mut m!())?;
 
         assert_eq!(parsed_cfg, cfg);
         assert!(unused.is_empty());
@@ -705,7 +693,7 @@ mod tests {
             version = "2.17"
             image = "zig:local"
         "#;
-        let (parsed_cfg, unused) = CrossToml::parse_from_cross(test_str, &mut m!())?;
+        let (parsed_cfg, unused) = CrossToml::parse_from_cross_str(test_str, None, &mut m!())?;
 
         assert_eq!(parsed_cfg, cfg);
         assert!(unused.is_empty());
@@ -791,7 +779,7 @@ mod tests {
             [target.aarch64-unknown-linux-gnu.env]
             volumes = ["VOL"]
         "#;
-        let (parsed_cfg, unused) = CrossToml::parse_from_cross(test_str, &mut m!())?;
+        let (parsed_cfg, unused) = CrossToml::parse_from_cross_str(test_str, None, &mut m!())?;
 
         assert_eq!(parsed_cfg, cfg);
         assert!(unused.is_empty());
@@ -810,7 +798,7 @@ mod tests {
           cross = "1.2.3"
         "#;
 
-        let res = CrossToml::parse_from_cargo(test_str, &mut m!())?;
+        let res = CrossToml::parse_from_cargo_package_str(test_str, &mut m!())?;
         assert!(res.is_none());
 
         Ok(())
@@ -846,7 +834,9 @@ mod tests {
           xargo = true
         "#;
 
-        if let Some((parsed_cfg, _unused)) = CrossToml::parse_from_cargo(test_str, &mut m!())? {
+        if let Some((parsed_cfg, _unused)) =
+            CrossToml::parse_from_cargo_package_str(test_str, &mut m!())?
+        {
             assert_eq!(parsed_cfg, cfg);
         } else {
             panic!("Parsing result is None");
@@ -873,7 +863,7 @@ mod tests {
             zig = "2.17"
         "#;
 
-        let (cfg, _) = CrossToml::parse_from_cross(cfg, &mut m!())?;
+        let (cfg, _) = CrossToml::parse_from_cross_str(cfg, None, &mut m!())?;
         serde_json::from_value::<CrossToml>(serde_json::to_value(cfg)?)?;
         Ok(())
     }
@@ -977,9 +967,9 @@ mod tests {
         "#;
 
         // Parses configs
-        let (cfg1, _) = CrossToml::parse_from_cross(cfg1_str, &mut m!())?;
-        let (cfg2, _) = CrossToml::parse_from_cross(cfg2_str, &mut m!())?;
-        let (cfg_expected, _) = CrossToml::parse_from_cross(cfg_expected_str, &mut m!())?;
+        let (cfg1, _) = CrossToml::parse_from_cross_str(cfg1_str, None, &mut m!())?;
+        let (cfg2, _) = CrossToml::parse_from_cross_str(cfg2_str, None, &mut m!())?;
+        let (cfg_expected, _) = CrossToml::parse_from_cross_str(cfg_expected_str, None, &mut m!())?;
 
         // Merges config and compares
         let cfg_merged = cfg1.merge(cfg2)?;
@@ -1036,7 +1026,7 @@ mod tests {
             [build]
             pre-build = ["echo Hello World"]
         "#;
-        let (toml, unused) = CrossToml::parse_from_cross(toml_str, &mut m!())?;
+        let (toml, unused) = CrossToml::parse_from_cross_str(toml_str, None, &mut m!())?;
         assert!(unused.is_empty());
         assert!(matches!(
             toml.pre_build(&Target::new_built_in("aarch64-unknown-linux-gnu")),
diff --git a/src/lib.rs b/src/lib.rs
index 9e34d68..ecfd62f 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -752,7 +752,7 @@ pub fn setup(
 ) -> Result<Option<CrossSetup>, color_eyre::Report> {
     let host = host_version_meta.host();
     let toml = toml(metadata, msg_info)?;
-    let config = Config::new(toml);
+    let config = Config::new(Some(toml));
     let target = args
         .target
         .clone()
@@ -893,39 +893,78 @@ macro_rules! commit_info {
 /// 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
+/// 3. Package and workspace metadata in the Cargo.toml
 ///
-/// The values from `CROSS_CONFIG` or `Cross.toml` are concatenated with the package
+/// The values from `CROSS_CONFIG` or `Cross.toml` are concatenated with the
 /// metadata in `Cargo.toml`, with `Cross.toml` having the highest priority.
-pub fn toml(metadata: &CargoMetadata, msg_info: &mut MessageInfo) -> Result<Option<CrossToml>> {
+pub fn toml(metadata: &CargoMetadata, msg_info: &mut MessageInfo) -> Result<CrossToml> {
     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 mut config = 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",))?;
+        let (config, _) = CrossToml::parse_from_cross_str(
+            &cross_toml_str,
+            Some(cross_config_path.to_utf8()?),
+            msg_info,
+        )
+        .wrap_err_with(|| format!("failed to parse file `{cross_config_path:?}` as TOML",))?;
 
-        Ok(Some(config))
+        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.")?;
         }
+        CrossToml::default()
+    };
+    let mut found: Option<std::borrow::Cow<'_, str>> = None;
+
+    if let Some(workspace_metadata) = dbg!(&metadata.metadata) {
+        let workspace_metadata =
+            serde_json::de::from_str::<serde_json::Value>(workspace_metadata.get())?;
+        if let Some(cross) = dbg!(workspace_metadata.get("cross")) {
+            found = Some(
+                metadata
+                    .workspace_root
+                    .join("Cargo.toml")
+                    .to_utf8()?
+                    .to_owned()
+                    .into(),
+            );
+            let (workspace_config, _) =
+                CrossToml::parse_from_deserializer(cross, found.as_deref(), msg_info)?;
+            config = config.merge(workspace_config)?;
+        }
+    }
 
-        if let Some((cfg, _)) = CrossToml::parse_from_cargo(&cargo_toml_str, msg_info)? {
-            Ok(Some(cfg))
-        } else {
-            Ok(None)
+    for (package, package_metadata) in metadata
+        .packages
+        .iter()
+        .filter_map(|p| Some((p.manifest_path.as_path(), p.metadata.as_deref()?)))
+    {
+        let package_metadata =
+            serde_json::de::from_str::<serde_json::Value>(package_metadata.get())?;
+
+        if let Some(cross) = package_metadata.get("cross") {
+            if let Some(found) = &found {
+                msg_info.warn(format_args!("Found conflicting cross configuration in `{}`, use `[workspace.metadata.cross]` in the workspace manifest instead.\nCurrently only using configuration from `{}`", package.to_utf8()?, found))?;
+                continue;
+            }
+            let (workspace_config, _) = CrossToml::parse_from_deserializer(
+                cross,
+                Some(metadata.workspace_root.join("Cargo.toml").to_utf8()?),
+                msg_info,
+            )?;
+            config = config.merge(workspace_config)?;
+            found = Some(package.to_utf8()?.into());
         }
     }
+
+    Ok(config)
 }


Test patch:
diff --git a/src/tests/toml.rs b/src/tests/toml.rs
index 0cf013e..161b1e7 100644
--- a/src/tests/toml.rs
+++ b/src/tests/toml.rs
@@ -62,10 +62,17 @@ fn toml_check() -> Result<(), Box<dyn std::error::Error>> {
             );
             let mut msg_info = crate::shell::MessageInfo::default();
             let toml = if !cargo {
-                crate::cross_toml::CrossToml::parse_from_cross(&fence_content, &mut msg_info)?
+                crate::cross_toml::CrossToml::parse_from_cross_str(
+                    &fence_content,
+                    None,
+                    &mut msg_info,
+                )?
             } else {
-                crate::cross_toml::CrossToml::parse_from_cargo(&fence_content, &mut msg_info)?
-                    .unwrap_or_default()
+                crate::cross_toml::CrossToml::parse_from_cargo_package_str(
+                    &fence_content,
+                    &mut msg_info,
+                )?
+                .unwrap_or_default()
             };
             assert!(toml.1.is_empty());