Skip to main content

cargo_zigbuild/zig/
cli_config.rs

1//! Rustflags extraction from cargo's `--config` CLI option.
2//!
3//! Cargo accepts arbitrary configuration overrides via `--config KEY=VALUE`
4//! (TOML syntax) or `--config <path>.toml`. cargo-zigbuild forwards these to
5//! the child cargo process, but `cargo_config2::Config::load()` only reads
6//! config files and environment variables, so `target-cpu` set via `--config`
7//! would silently not be reflected in the `-mcpu` passed to `zig cc`. This
8//! module parses the `--config` arguments so that rustflags provided this way
9//! participate in the resolution used for zig's `-mcpu`.
10//!
11//! Once cargo-config2 natively supports `--config` CLI overrides
12//! (<https://github.com/taiki-e/cargo-config2/issues/3>), most of this module
13//! (in particular the tier heuristic in [`CliConfig::overlay`]) can be
14//! replaced with that API.
15
16use std::collections::BTreeMap;
17use std::env;
18
19use anyhow::{Context, Result};
20use cargo_config2::Flags;
21
22/// Rustflags provided via cargo's `--config` CLI option.
23///
24/// Cargo merges `--config` values in left-to-right order with the same logic
25/// used for config files: arrays are joined with higher precedence items
26/// placed later. CLI values take precedence over config files and config
27/// environment variables (`CARGO_TARGET_<T>_RUSTFLAGS`, `CARGO_BUILD_RUSTFLAGS`),
28/// but not over the `RUSTFLAGS`/`CARGO_ENCODED_RUSTFLAGS` environment
29/// variables, which shortcut the rustflags resolution entirely.
30#[derive(Debug, Clone, Default, PartialEq)]
31pub struct CliConfig {
32    build_rustflags: Option<Flags>,
33    target_rustflags: BTreeMap<String, Flags>,
34}
35
36impl CliConfig {
37    /// Parses cargo `--config` arguments (`KEY=VALUE` in TOML syntax, or a
38    /// path to an extra config file ending in `.toml`).
39    ///
40    /// `target.'cfg(...)'` keys are ignored because evaluating cfg
41    /// expressions requires querying rustc; cargo still applies them to the
42    /// actual build, they just don't influence zig's `-mcpu`.
43    pub fn parse(config_args: &[String]) -> Result<Self> {
44        let mut parsed = Self::default();
45        for arg in config_args {
46            // Cargo treats an argument ending in `.toml` as a path to an
47            // extra config file; anything else must be TOML `KEY=VALUE`.
48            let config: cargo_config2::de::Config = if arg.ends_with(".toml") {
49                cargo_config2::de::Config::load_file(arg)?
50            } else {
51                toml::from_str(arg)
52                    .with_context(|| format!("failed to parse --config argument `{arg}`"))?
53            };
54            if let Some(flags) = &config.build.rustflags {
55                append_de_flags(parsed.build_rustflags.get_or_insert_default(), flags);
56            }
57            for (key, target_config) in &config.target {
58                if key.starts_with("cfg(") {
59                    continue;
60                }
61                if let Some(flags) = &target_config.rustflags {
62                    append_de_flags(
63                        parsed.target_rustflags.entry(key.clone()).or_default(),
64                        flags,
65                    );
66                }
67            }
68        }
69        Ok(parsed)
70    }
71
72    /// Resolves the effective rustflags for `rust_target`, overlaying the
73    /// CLI-provided values onto the config file/environment resolution.
74    ///
75    /// Cargo resolves rustflags from four mutually exclusive sources, in
76    /// order, using the first one that is set:
77    ///
78    /// 1. `CARGO_ENCODED_RUSTFLAGS` environment variable
79    /// 2. `RUSTFLAGS` environment variable
80    /// 3. all matching `target.<triple>.rustflags` and `target.<cfg>.rustflags`
81    ///    config entries joined together
82    /// 4. `build.rustflags` config value
83    ///
84    /// `--config` values participate in sources 3 and 4 with the highest
85    /// precedence within those sources (joined last).
86    pub fn rustflags(
87        &self,
88        cargo_config: &cargo_config2::Config,
89        rust_target: &str,
90    ) -> Result<Option<Flags>> {
91        let resolved = cargo_config.rustflags(rust_target)?;
92        if env::var_os("CARGO_ENCODED_RUSTFLAGS").is_some() || env::var_os("RUSTFLAGS").is_some() {
93            // Environment rustflags win over all config values, including CLI.
94            return Ok(resolved);
95        }
96        Ok(self.overlay(rust_target, resolved, cargo_config.build.rustflags.clone()))
97    }
98
99    /// Pure overlay of the CLI-provided rustflags onto the resolved config
100    /// values.
101    ///
102    /// `resolved` is the config file/environment resolution for the target
103    /// (source 3 if any target entries matched, source 4 otherwise) and
104    /// `build_flags` is the resolved `build.rustflags`. cargo_config2 does
105    /// not expose which source `resolved` came from, so when it equals
106    /// `build_flags` we assume it came from `build.rustflags`.
107    fn overlay(
108        &self,
109        rust_target: &str,
110        resolved: Option<Flags>,
111        build_flags: Option<Flags>,
112    ) -> Option<Flags> {
113        let from_build_tier = resolved == build_flags;
114        if let Some(cli_target) = self.target_rustflags.get(rust_target) {
115            // CLI target flags activate source 3: join file/env target
116            // entries (if any) with the CLI entries placed last;
117            // `build.rustflags` no longer applies.
118            let mut flags = if from_build_tier {
119                Flags::default()
120            } else {
121                resolved.unwrap_or_default()
122            };
123            flags.flags.extend(cli_target.flags.iter().cloned());
124            Some(flags)
125        } else if let Some(cli_build) = &self.build_rustflags {
126            if from_build_tier {
127                let mut flags = resolved.unwrap_or_default();
128                flags.flags.extend(cli_build.flags.iter().cloned());
129                Some(flags)
130            } else {
131                // A target.<triple>.rustflags entry matched; source 3 wins
132                // and build.rustflags (including the CLI value) is ignored.
133                resolved
134            }
135        } else {
136            resolved
137        }
138    }
139}
140
141fn append_de_flags(flags: &mut Flags, de_flags: &cargo_config2::de::Flags) {
142    flags
143        .flags
144        .extend(de_flags.flags.iter().map(|value| value.val.clone()));
145}
146
147#[cfg(test)]
148mod tests {
149    use super::*;
150    use std::io::Write;
151
152    const TARGET: &str = "x86_64-unknown-linux-gnu";
153
154    fn flags(s: &str) -> Flags {
155        Flags::from_space_separated(s)
156    }
157
158    #[test]
159    fn test_parse_target_rustflags_array() {
160        let config = CliConfig::parse(&[format!(
161            "target.{TARGET}.rustflags=['-C','target-cpu=x86-64-v4']"
162        )])
163        .unwrap();
164        assert_eq!(
165            config.target_rustflags[TARGET].flags,
166            vec!["-C", "target-cpu=x86-64-v4"]
167        );
168        assert!(config.build_rustflags.is_none());
169    }
170
171    #[test]
172    fn test_parse_target_rustflags_string() {
173        let config = CliConfig::parse(&[format!(
174            "target.{TARGET}.rustflags='-C target-cpu=x86-64-v4'"
175        )])
176        .unwrap();
177        assert_eq!(
178            config.target_rustflags[TARGET].flags,
179            vec!["-C", "target-cpu=x86-64-v4"]
180        );
181    }
182
183    #[test]
184    fn test_parse_build_rustflags() {
185        let config =
186            CliConfig::parse(&["build.rustflags=['-Ctarget-cpu=neoverse-n1']".to_string()])
187                .unwrap();
188        assert_eq!(
189            config.build_rustflags.unwrap().flags,
190            vec!["-Ctarget-cpu=neoverse-n1"]
191        );
192    }
193
194    #[test]
195    fn test_parse_multiple_args_join_left_to_right() {
196        let config = CliConfig::parse(&[
197            format!("target.{TARGET}.rustflags=['-Ctarget-cpu=x86-64-v2']"),
198            format!("target.{TARGET}.rustflags=['-Ctarget-cpu=x86-64-v4']"),
199        ])
200        .unwrap();
201        // Arrays are joined with later (higher precedence) items placed last.
202        assert_eq!(
203            config.target_rustflags[TARGET].flags,
204            vec!["-Ctarget-cpu=x86-64-v2", "-Ctarget-cpu=x86-64-v4"]
205        );
206    }
207
208    #[test]
209    fn test_parse_config_file() {
210        let mut file = tempfile::Builder::new().suffix(".toml").tempfile().unwrap();
211        writeln!(
212            file,
213            "[target.{TARGET}]\nrustflags = ['-C', 'target-cpu=x86-64-v3']"
214        )
215        .unwrap();
216        let config = CliConfig::parse(&[file.path().to_str().unwrap().to_string()]).unwrap();
217        assert_eq!(
218            config.target_rustflags[TARGET].flags,
219            vec!["-C", "target-cpu=x86-64-v3"]
220        );
221    }
222
223    #[test]
224    fn test_parse_ignores_unrelated_and_cfg_keys() {
225        let config = CliConfig::parse(&[
226            "net.git-fetch-with-cli=true".to_string(),
227            "profile.release.lto=true".to_string(),
228            "target.'cfg(target_arch = \"x86_64\")'.rustflags=['-Ctarget-cpu=x86-64-v4']"
229                .to_string(),
230        ])
231        .unwrap();
232        assert_eq!(config, CliConfig::default());
233    }
234
235    #[test]
236    fn test_overlay_cli_target_replaces_build_tier() {
237        let config = CliConfig::parse(&[format!(
238            "target.{TARGET}.rustflags=['-Ctarget-cpu=x86-64-v4']"
239        )])
240        .unwrap();
241        // `resolved` came from build.rustflags: CLI target flags activate
242        // source 3 and build.rustflags no longer applies.
243        let result = config.overlay(
244            TARGET,
245            Some(flags("-Ctarget-cpu=x86-64-v2")),
246            Some(flags("-Ctarget-cpu=x86-64-v2")),
247        );
248        assert_eq!(result.unwrap().flags, vec!["-Ctarget-cpu=x86-64-v4"]);
249    }
250
251    #[test]
252    fn test_overlay_cli_target_joins_file_target_tier() {
253        let config = CliConfig::parse(&[format!(
254            "target.{TARGET}.rustflags=['-Ctarget-cpu=x86-64-v4']"
255        )])
256        .unwrap();
257        // `resolved` came from target.<triple>.rustflags in a config file:
258        // entries are joined with CLI values last, so the CLI target-cpu wins.
259        let result = config.overlay(TARGET, Some(flags("-Ctarget-cpu=x86-64-v2")), None);
260        assert_eq!(
261            result.unwrap().flags,
262            vec!["-Ctarget-cpu=x86-64-v2", "-Ctarget-cpu=x86-64-v4"]
263        );
264    }
265
266    #[test]
267    fn test_overlay_cli_target_other_triple_is_ignored() {
268        let config = CliConfig::parse(&[
269            "target.aarch64-unknown-linux-gnu.rustflags=['-Ctarget-cpu=neoverse-n1']".to_string(),
270        ])
271        .unwrap();
272        let result = config.overlay(TARGET, None, None);
273        assert_eq!(result, None);
274    }
275
276    #[test]
277    fn test_overlay_cli_build_joins_build_tier() {
278        let config =
279            CliConfig::parse(&["build.rustflags=['-Ctarget-cpu=x86-64-v4']".to_string()]).unwrap();
280        let result = config.overlay(
281            TARGET,
282            Some(flags("-Ctarget-cpu=x86-64-v2")),
283            Some(flags("-Ctarget-cpu=x86-64-v2")),
284        );
285        assert_eq!(
286            result.unwrap().flags,
287            vec!["-Ctarget-cpu=x86-64-v2", "-Ctarget-cpu=x86-64-v4"]
288        );
289    }
290
291    #[test]
292    fn test_overlay_cli_build_ignored_when_target_tier_present() {
293        let config =
294            CliConfig::parse(&["build.rustflags=['-Ctarget-cpu=x86-64-v4']".to_string()]).unwrap();
295        // `resolved` differs from build.rustflags, so it came from a
296        // target.<triple>.rustflags entry, which wins over build.rustflags.
297        let result = config.overlay(TARGET, Some(flags("-Ctarget-cpu=x86-64-v2")), None);
298        assert_eq!(result.unwrap().flags, vec!["-Ctarget-cpu=x86-64-v2"]);
299    }
300
301    #[test]
302    fn test_overlay_no_cli_flags_keeps_resolved() {
303        let config = CliConfig::default();
304        let result = config.overlay(TARGET, Some(flags("-Ctarget-cpu=x86-64-v2")), None);
305        assert_eq!(result.unwrap().flags, vec!["-Ctarget-cpu=x86-64-v2"]);
306    }
307}