Skip to main content

ic_build/cargo/selection/
mod.rs

1//! Typed recipe choices shared by Cargo metadata and build commands.
2
3use std::{
4    collections::BTreeSet,
5    ffi::OsString,
6    path::{Component, Path},
7};
8
9#[cfg(test)]
10mod tests;
11
12/// Caller-selected features for one Cargo graph.
13#[derive(Clone, Debug, Eq, PartialEq)]
14pub struct CargoFeatureSelection {
15    /// Features to activate, ordered and deduplicated before passing to Cargo.
16    pub features: BTreeSet<String>,
17    /// Activate every feature in the selected packages.
18    pub all_features: bool,
19    /// Whether Cargo activates the selected packages' default features.
20    pub default_features: bool,
21}
22
23impl Default for CargoFeatureSelection {
24    fn default() -> Self {
25        Self {
26            features: BTreeSet::new(),
27            default_features: true,
28            all_features: false,
29        }
30    }
31}
32
33impl CargoFeatureSelection {
34    pub(crate) fn arguments(&self) -> Vec<OsString> {
35        let mut arguments = Vec::new();
36        if self.all_features {
37            arguments.push("--all-features".into());
38        }
39        if !self.default_features {
40            arguments.push("--no-default-features".into());
41        }
42        if !self.features.is_empty() {
43            arguments.push(
44                format!(
45                    "--features={}",
46                    self.features
47                        .iter()
48                        .map(String::as_str)
49                        .collect::<Vec<_>>()
50                        .join(",")
51                )
52                .into(),
53            );
54        }
55        arguments
56    }
57}
58
59/// Caller-selected profile, features and network/lock policy for one recipe.
60///
61/// Package, target, manifest, output paths and message format belong to the
62/// enclosing build specification. Arbitrary Cargo arguments are not accepted.
63/// Use both locked and offline for Cargo's frozen policy.
64/// Defaults select Cargo's dev profile and default features, with network and
65/// lockfile policy inherited from the caller's environment/command factory.
66#[derive(Clone, Debug, Default, Eq, PartialEq)]
67pub struct CargoSelection {
68    /// Named Cargo profile; None selects dev. Release renders as --release.
69    pub profile: Option<String>,
70    /// Explicit features and whether default features are enabled.
71    pub features: CargoFeatureSelection,
72    /// Require Cargo to preserve the selected lockfile.
73    pub locked: bool,
74    /// Prevent Cargo from accessing the network.
75    pub offline: bool,
76}
77
78impl CargoSelection {
79    pub(crate) fn profile_target_dir(&self) -> Result<&str, &'static str> {
80        let profile = self.profile.as_deref().unwrap_or("dev");
81        let directory = match profile {
82            "dev" | "test" => "debug",
83            "bench" => "release",
84            custom => custom,
85        };
86        if !is_normal_component(Path::new(directory)) {
87            return Err("Cargo profile must select one normal output directory component");
88        }
89        Ok(directory)
90    }
91
92    pub(crate) fn metadata_arguments(&self) -> Vec<OsString> {
93        graph_arguments(&self.features, self.locked, self.offline)
94    }
95
96    pub(crate) fn build_arguments(&self) -> Vec<OsString> {
97        let mut arguments = Vec::new();
98        if let Some(profile) = &self.profile {
99            arguments.push(if profile == "release" {
100                "--release".into()
101            } else {
102                format!("--profile={profile}").into()
103            });
104        }
105        arguments.extend(self.metadata_arguments());
106        arguments
107    }
108}
109
110/// Encode graph choices identically for observations and build recipes.
111pub(crate) fn graph_arguments(
112    features: &CargoFeatureSelection,
113    locked: bool,
114    offline: bool,
115) -> Vec<OsString> {
116    let mut arguments = Vec::new();
117    for (enabled, flag) in [(locked, "--locked"), (offline, "--offline")] {
118        if enabled {
119            arguments.push(flag.into());
120        }
121    }
122    arguments.extend(features.arguments());
123    arguments
124}
125
126// Shared lexical test only. Callers retain Cargo selection and refusal policy,
127// including their existing treatment of normalized separators/current-dir parts.
128pub(crate) fn is_normal_component(path: &Path) -> bool {
129    let mut components = path.components();
130    matches!(
131        (components.next(), components.next()),
132        (Some(Component::Normal(_)), None)
133    )
134}