Repository: cross-rs/cross
Pull request #842 (2022-06-23 09:25:51)

Allow `Cargo.toml` as configuration source
Due to a GitHub mishap in #754, this is a new PR containing the same commits (rebased by @Emilgardis). Just for reference, it still addresses #657.

Emilgardis: can you squash the commits also?

Patch:
diff --git a/.github/actions/setup-rust/action.yml b/.github/actions/setup-rust/action.yml
index 0dd4b00..ae1d8e2 100644
--- a/.github/actions/setup-rust/action.yml
+++ b/.github/actions/setup-rust/action.yml
@@ -3,7 +3,7 @@ description: Setup Rust with specified toolchain, target and components.
 inputs:
   toolchain:
     description: 'Toolchain'
-    default: stable
+    default: 1.58.1
     required: true
   target:
     description: 'Target'
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index b9b5118..83c4c50 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -55,7 +55,6 @@ jobs:
       - uses: ./.github/actions/setup-rust
         with:
           components: clippy
-          toolchain: 1.58.1 # MSRV, Minimally Supported Rust Version. Make sure to update README.md
 
       - name: Run clippy
         uses: actions-rs/cargo@v1
@@ -65,6 +64,7 @@ jobs:
 
   test:
     runs-on: ${{ matrix.os }}
+    needs: [fmt, clippy]
     strategy:
       matrix:
         os:
@@ -81,7 +81,7 @@ jobs:
         with:
           command: test
           args: --locked --all-targets --workspace --all-features
-        timeout-minutes: 10
+        timeout-minutes: 5
 
   generate-matrix:
     runs-on: ubuntu-latest
@@ -189,8 +189,9 @@ jobs:
             - { target: i686-unknown-freebsd,             os: ubuntu-latest,          dylib: 1, std: 1 }
             - { target: x86_64-unknown-freebsd,           os: ubuntu-latest,          dylib: 1, std: 1 }
             - { target: x86_64-unknown-netbsd,            os: ubuntu-latest,  cpp: 1, dylib: 1, std: 1 }
-            - { target: sparcv9-sun-solaris,              os: ubuntu-latest,  cpp: 1, dylib: 1, std: 1 }
-            - { target: x86_64-sun-solaris,               os: ubuntu-latest,  cpp: 1, dylib: 1, std: 1 }
+            # Disabled, see #534
+            #-{ target: sparcv9-sun-solaris,              os: ubuntu-latest,  cpp: 1, dylib: 1, std: 1 }
+            #-{ target: x86_64-sun-solaris,               os: ubuntu-latest,  cpp: 1, dylib: 1, std: 1 }
             - { target: thumbv6m-none-eabi,               os: ubuntu-latest,                    std: 1 }
             - { target: thumbv7em-none-eabi,              os: ubuntu-latest,                    std: 1 }
             - { target: thumbv7em-none-eabihf,            os: ubuntu-latest,                    std: 1 }
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 85bd1d3..bd97cde 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`.
@@ -31,7 +32,6 @@ This project adheres to [Semantic Versioning](http://semver.org/).
 
 ### Changed
 
-- #838 - re-enabled the solaris targets.
 - #807 - update Qemu to 6.1.0 on images using Ubuntu 18.04+ with python3.6+.
 - #775 - forward Cargo exit code to host
 - #762 - re-enabled `x86_64-unknown-dragonfly` target.
@@ -49,7 +49,6 @@ This project adheres to [Semantic Versioning](http://semver.org/).
 
 ### Fixed
 
-- #836 - write a `CACHEDIR.TAG` when creating the target directory, similar to `cargo`.
 - #804 - allow usage of env `CARGO_BUILD_TARGET` as an alias for `CROSS_BUILD_TARGET`
 - #792 - fixed container-in-container support when using podman.
 - #781 - ensure `target.$(...)` config options override `build` ones.
diff --git a/README.md b/README.md
index b453c9b..e4c070c 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/docker/solaris.sh b/docker/solaris.sh
index 7f88ec3..e3c10d9 100755
--- a/docker/solaris.sh
+++ b/docker/solaris.sh
@@ -10,7 +10,7 @@ main() {
     local arch="${1}"
 
     local binutils=2.28.1 \
-          gcc=8.4.0 \
+          gcc=6.4.0 \
           target="${arch}-sun-solaris2.10"
 
     install_packages bzip2 \
@@ -58,21 +58,18 @@ main() {
     add-apt-repository -y 'deb http://apt.dilos.org/dilos dilos2-testing main'
     dpkg --add-architecture "${apt_arch}"
     apt-get update
-    # shellcheck disable=SC2046
-    apt-get download $(apt-cache depends --recurse --no-replaces \
-      "libc:${apt_arch}"            \
-      "liblgrp-dev:${apt_arch}"     \
-      "liblgrp:${apt_arch}"         \
-      "libm-dev:${apt_arch}"        \
-      "libpthread:${apt_arch}"      \
-      "libresolv:${apt_arch}"       \
-      "librt:${apt_arch}"           \
-      "libsendfile-dev:${apt_arch}" \
-      "libsendfile:${apt_arch}"     \
-      "libsocket:${apt_arch}"       \
-      "system-crt:${apt_arch}"      \
-      "system-header:${apt_arch}"   \
-      | grep "^\w")
+    apt-cache depends --recurse --no-replaces \
+      "libc:${apt_arch}"           \
+      "libdl-dev:${apt_arch}"      \
+      "libm-dev:${apt_arch}"       \
+      "libnsl-dev:${apt_arch}"     \
+      "libpthread-dev:${apt_arch}" \
+      "libresolv-dev:${apt_arch}"  \
+      "librt:${apt_arch}"          \
+      "libsocket-dev:${apt_arch}"  \
+      "system-crt:${apt_arch}"     \
+      "system-header:${apt_arch}"  \
+      | grep "^\w" | xargs apt-get download
 
     for deb in *"${apt_arch}.deb"; do
       dpkg -x "${deb}" "${td}/solaris"
@@ -105,8 +102,6 @@ EOF
     ln -s usr/include "${destdir}/sys-include"
     ln -s usr/include "${destdir}/include"
 
-    # note: solaris2.10 is obsolete, so we can't upgrade to GCC 10 till then.
-    # for gcc 9.4.0, need `--enable-obsolete`
     cd gcc-build
     ../gcc/configure \
         --disable-libada \
diff --git a/docs/cross_toml.md b/docs/cross_toml.md
index bedfc49..51f8783 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.<KEY>]`.
 
 # `build`
 
diff --git a/src/bin/cross-util.rs b/src/bin/cross-util.rs
index b5746fc..2549d2e 100644
--- a/src/bin/cross-util.rs
+++ b/src/bin/cross-util.rs
@@ -1,6 +1,6 @@
 #![deny(missing_debug_implementations, rust_2018_idioms)]
 
-use clap::{CommandFactory, Parser, Subcommand};
+use clap::{Parser, Subcommand};
 
 mod commands;
 
@@ -14,13 +14,6 @@ struct Cli {
     command: Commands,
 }
 
-// hidden implied parser so we can get matches without recursion.
-#[derive(Parser, Debug)]
-struct CliHidden {
-    #[clap(subcommand)]
-    command: Commands,
-}
-
 #[derive(Subcommand, Debug)]
 enum Commands {
     /// List cross images in local storage.
@@ -32,8 +25,7 @@ fn is_toolchain(toolchain: &str) -> cross::Result<String> {
     if toolchain.starts_with('+') {
         Ok(toolchain.chars().skip(1).collect())
     } else {
-        let _ = <CliHidden as CommandFactory>::command().get_matches();
-        unreachable!();
+        eyre::bail!("not a toolchain")
     }
 }
 
diff --git a/src/config.rs b/src/config.rs
index dcc39ed..bcb3d49 100644
--- a/src/config.rs
+++ b/src/config.rs
@@ -431,7 +431,9 @@ mod tests {
         use std::matches;
 
         fn toml(content: &str) -> Result<crate::CrossToml> {
-            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 2d07300..8b0c5e4 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<Vec<String>>,
     passthrough: Option<Vec<String>>,
 }
 
 /// 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<bool>,
@@ -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<Target, CrossTargetConfig>,
@@ -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<String>)> {
+        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<String>)> {
-        let tomld = &mut toml::Deserializer::new(toml_str);
+    pub fn parse_from_cross(toml_str: &str) -> Result<(Self, BTreeSet<String>)> {
+        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<Option<(Self, BTreeSet<String>)>> {
+        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<String>)>
+    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<CrossToml> {
+        type ValueMap = serde_json::Map<String, serde_json::Value>;
+
+        fn to_map<S: Serialize>(s: S) -> Result<ValueMap> {
+            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<D: DeserializeOwned>(map: ValueMap) -> Result<D> {
+            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<String> = 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<String> {
         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<E>(self) -> Result<Self::Value, E>
+        where
+            E: de::Error,
+        {
+            Ok(None)
+        }
+
+        fn visit_unit<E>(self) -> Result<Self::Value, E>
+        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/docker/shared.rs b/src/docker/shared.rs
index ac3b51a..85b462a 100644
--- a/src/docker/shared.rs
+++ b/src/docker/shared.rs
@@ -62,7 +62,7 @@ impl Directories {
         // otherwise `docker` will create them but they will be owned by `root`
         fs::create_dir(&cargo).ok();
         fs::create_dir(&xargo).ok();
-        create_target_dir(target)?;
+        fs::create_dir(&target).ok();
 
         let cargo = mount_finder.find_mount_path(cargo);
         let xargo = mount_finder.find_mount_path(xargo);
@@ -111,24 +111,6 @@ impl Directories {
     }
 }
 
-const CACHEDIR_TAG: &str = "Signature: 8a477f597d28d172789f06886806bc55
-# This file is a cache directory tag created by cross.
-# For information about cache directory tags see https://bford.info/cachedir/";
-
-fn create_target_dir(path: &Path) -> Result<()> {
-    // cargo creates all paths to the target directory, and writes
-    // a cache dir tag only if the path doesn't previously exist.
-    if !path.exists() {
-        fs::create_dir_all(&path)?;
-        fs::OpenOptions::new()
-            .write(true)
-            .create_new(true)
-            .open(&path.join("CACHEDIR.TAG"))?
-            .write_all(CACHEDIR_TAG.as_bytes())?;
-    }
-    Ok(())
-}
-
 pub fn command(engine: &Engine) -> Command {
     Command::new(&engine.path)
 }
diff --git a/src/lib.rs b/src/lib.rs
index 55746e6..d793adf 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<Host> for Target {
     }
 }
 
-impl From<&str> for Target {
-    fn from(target_str: &str) -> Target {
-        let target_host: Host = target_str.into();
+impl From<String> 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<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
+        match self {
+            Target::BuiltIn { triple } => serializer.serialize_str(triple),
+            Target::Custom { triple } => serializer.serialize_str(triple),
+        }
+    }
+}
+
 pub fn run() -> Result<ExitStatus> {
     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<Option<CrossToml>> {
-    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/xtask/src/main.rs b/xtask/src/main.rs
index 74a7dac..77bca16 100644
--- a/xtask/src/main.rs
+++ b/xtask/src/main.rs
@@ -10,7 +10,7 @@ pub mod util;
 use std::path::PathBuf;
 
 use ci::CiJob;
-use clap::{CommandFactory, Parser, Subcommand};
+use clap::{Parser, Subcommand};
 use util::ImageTarget;
 
 use self::build_docker_image::BuildDockerImage;
@@ -28,13 +28,6 @@ struct Cli {
     command: Commands,
 }
 
-// hidden implied parser so we can get matches without recursion.
-#[derive(Parser, Debug)]
-struct CliHidden {
-    #[clap(subcommand)]
-    command: Commands,
-}
-
 #[derive(Subcommand, Debug)]
 enum Commands {
     /// Extract and print info for targets.
@@ -56,8 +49,7 @@ fn is_toolchain(toolchain: &str) -> cross::Result<String> {
     if toolchain.starts_with('+') {
         Ok(toolchain.chars().skip(1).collect())
     } else {
-        let _ = <CliHidden as CommandFactory>::command().get_matches();
-        unreachable!();
+        eyre::bail!("not a toolchain")
     }
 }
 


Test patch:
diff --git a/src/tests/toml.rs b/src/tests/toml.rs
index c7479f1..da5369a 100644
--- a/src/tests/toml.rs
+++ b/src/tests/toml.rs
@@ -41,9 +41,11 @@ fn toml_check() -> Result<(), Box<dyn std::error::Error>> {
                 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(())