flux_bin/
cargo_flux_opts.rs

1use std::{collections::HashSet, path::Path, process::Command};
2
3use cargo_metadata::{
4    Metadata, MetadataCommand, Package as CargoPackage, PackageId, camino::Utf8PathBuf,
5};
6use flux_config::flags::Flags;
7
8use crate::cargo_style;
9
10#[derive(clap::Parser)]
11#[command(name = "cargo")]
12#[command(bin_name = "cargo")]
13#[command(styles = cargo_style::CLAP_STYLING)]
14pub enum Cli {
15    /// Flux's integration with Cargo
16    Flux {
17        #[command(flatten)]
18        check_opts: CompileOpts,
19
20        #[command(subcommand)]
21        command: Option<CargoFluxCommand>,
22
23        /// Print version information
24        #[arg(short = 'V', long, action = clap::ArgAction::SetTrue)]
25        version: bool,
26
27        /// Use verbose output (-Vv for more verbose output)
28        #[arg(short, long, action = clap::ArgAction::Count)]
29        verbose: u8,
30    },
31}
32
33#[derive(clap::Subcommand)]
34pub enum CargoFluxCommand {
35    /// Check a local package and its dependencies for errors using Flux.
36    /// This is the default command when no subcommand is provided.
37    Check(CompileOpts),
38    /// Compile a local package and its dependencies using Flux.
39    Build(CompileOpts),
40    /// Remove artifacts that cargo-flux has generated in the past
41    Clean(CleanOpts),
42}
43
44impl CargoFluxCommand {
45    pub fn forward_args(&self, cmd: &mut Command, config_file: &Path) {
46        match self {
47            CargoFluxCommand::Check(check_opts) => {
48                cmd.arg("check");
49                check_opts.forward_args(cmd);
50            }
51            CargoFluxCommand::Build(build_opts) => {
52                cmd.arg("build");
53                build_opts.forward_args(cmd);
54            }
55            CargoFluxCommand::Clean(clean_opts) => {
56                cmd.arg("clean");
57                clean_opts.forward_args(cmd);
58            }
59        }
60        cmd.args(["--profile", "flux"]);
61        cmd.args(["--config".as_ref(), config_file.as_os_str()]);
62    }
63
64    pub fn metadata(&self) -> MetadataCommand {
65        let mut meta = cargo_metadata::MetadataCommand::new();
66        match self {
67            CargoFluxCommand::Check(check_options) | CargoFluxCommand::Build(check_options) => {
68                check_options.forward_to_metadata(&mut meta);
69            }
70            CargoFluxCommand::Clean(clean_options) => {
71                clean_options.forward_to_metadata(&mut meta);
72            }
73        }
74        meta
75    }
76
77    pub fn targeted_package_ids(&self, metadata: &Metadata) -> HashSet<PackageId> {
78        match self {
79            CargoFluxCommand::Check(opts) | CargoFluxCommand::Build(opts) => {
80                opts.targeted_package_ids(metadata)
81            }
82            CargoFluxCommand::Clean(opts) => opts.targeted_package_ids(metadata),
83        }
84    }
85
86    pub fn only_check(&self) -> Option<&str> {
87        match self {
88            CargoFluxCommand::Check(opts) | CargoFluxCommand::Build(opts) => {
89                opts.only_check.as_deref()
90            }
91            CargoFluxCommand::Clean(_) => None,
92        }
93    }
94}
95
96#[derive(clap::Args)]
97pub struct CompileOpts {
98    /// Error format [possible values: human, short, json, json-diagnostic-short, json-diagnostic-rendered-ansi, json-render-diagnostics]
99    #[arg(long, value_name = "FMT")]
100    message_format: Option<String>,
101
102    #[command(flatten)]
103    workspace: Workspace,
104    #[command(flatten)]
105    features: Features,
106    #[command(flatten)]
107    compilation: CompilationOptions,
108    #[command(flatten)]
109    manifest: ManifestOptions,
110    #[command(flatten)]
111    flux_flags: Flags,
112
113    /// Only check items matching PATTERN (overrides include patterns from cargo.toml or flux.toml).
114    ///
115    /// Supported patterns:
116    ///   def:<name>              — match items whose name contains <name>
117    ///   span:<file>:<line>:<col> — match the item at a source location
118    ///   glob:<pattern>          — match files by glob (e.g. "glob:src/ascii/*.rs")
119    ///   <pattern>               — bare string treated as a glob
120    #[arg(long, value_name = "PATTERN")]
121    pub only_check: Option<String>,
122}
123
124impl CompileOpts {
125    fn forward_args(&self, cmd: &mut Command) {
126        let CompileOpts { message_format, workspace, features, compilation, manifest, .. } = self;
127        if let Some(message_format) = &message_format {
128            cmd.args(["--message-format", message_format]);
129        }
130        workspace.forward_args(cmd);
131        features.forward_args(cmd);
132        compilation.forward_args(cmd);
133        manifest.forward_args(cmd);
134    }
135
136    fn forward_to_metadata(&self, meta: &mut MetadataCommand) {
137        let CompileOpts { features, manifest, .. } = self;
138        features.forward_to_metadata(meta);
139        manifest.forward_to_metadata(meta);
140    }
141
142    pub fn targeted_package_ids(&self, metadata: &Metadata) -> HashSet<PackageId> {
143        targeted_package_ids(&self.workspace, metadata)
144    }
145}
146
147fn package_matches_spec(package: &CargoPackage, spec: &str) -> bool {
148    if package.id.repr == spec {
149        return true;
150    }
151
152    if let Some((name, version)) = spec.rsplit_once('@') {
153        return package.name == name && package.version.to_string() == version;
154    }
155
156    package.name == spec
157}
158
159fn select_packages_by_spec<'a>(package: &Package, metadata: &'a Metadata) -> Vec<&'a CargoPackage> {
160    let workspace_packages = metadata.workspace_packages();
161    if !package.package.is_empty() {
162        workspace_packages
163            .into_iter()
164            .filter(|p| {
165                package
166                    .package
167                    .iter()
168                    .any(|spec| package_matches_spec(p, spec))
169            })
170            .collect()
171    } else if metadata.workspace_default_members.is_available() {
172        metadata.workspace_default_packages()
173    } else if let Some(root) = metadata.root_package() {
174        vec![root]
175    } else {
176        workspace_packages
177    }
178}
179
180fn targeted_package_ids(workspace: &Workspace, metadata: &Metadata) -> HashSet<PackageId> {
181    let mut packages = if workspace.workspace {
182        metadata.workspace_packages()
183    } else {
184        select_packages_by_spec(&workspace.package, metadata)
185    };
186
187    packages.retain(|p| {
188        !workspace
189            .exclude
190            .iter()
191            .any(|spec| package_matches_spec(p, spec))
192    });
193
194    packages.into_iter().map(|p| p.id.clone()).collect()
195}
196
197#[derive(clap::Args)]
198pub struct CleanOpts {
199    #[command(flatten, next_help_heading = "Package Selection")]
200    package: Package,
201    #[command(flatten)]
202    features: Features,
203    #[command(flatten)]
204    manifest: ManifestOptions,
205}
206
207impl CleanOpts {
208    fn forward_args(&self, cmd: &mut Command) {
209        let CleanOpts { package, features, manifest } = self;
210        package.forward_args(cmd);
211        features.forward_args(cmd);
212        manifest.forward_args(cmd);
213    }
214
215    fn forward_to_metadata(&self, meta: &mut MetadataCommand) {
216        let CleanOpts { package: _, features, manifest } = self;
217        features.forward_to_metadata(meta);
218        manifest.forward_to_metadata(meta);
219    }
220
221    pub fn targeted_package_ids(&self, metadata: &Metadata) -> HashSet<PackageId> {
222        select_packages_by_spec(&self.package, metadata)
223            .into_iter()
224            .map(|p| p.id.clone())
225            .collect()
226    }
227}
228
229#[derive(Debug, clap::Args)]
230#[command(about = None, long_about = None, next_help_heading = "Package Selection")]
231pub struct Workspace {
232    #[command(flatten)]
233    pub package: Package,
234
235    #[arg(long)]
236    /// Process all packages in the workspace
237    pub workspace: bool,
238
239    #[arg(long, value_name = "SPEC")]
240    /// Exclude packages from being processed
241    pub exclude: Vec<String>,
242}
243
244impl Workspace {
245    fn forward_args(&self, cmd: &mut Command) {
246        let Workspace { package, workspace, exclude } = self;
247        package.forward_args(cmd);
248        if *workspace {
249            cmd.arg("--workspace");
250        }
251        if !exclude.is_empty() {
252            cmd.args(exclude.iter().flat_map(|package| ["--exclude", package]));
253        }
254    }
255}
256
257#[derive(Debug, clap::Args)]
258#[command(about = None, long_about = None)]
259pub struct Package {
260    #[arg(short, long, value_name = "SPEC")]
261    /// Package to process (see `cargo help pkgid`)
262    pub package: Vec<String>,
263}
264
265impl Package {
266    fn forward_args(&self, cmd: &mut Command) {
267        let Package { package } = self;
268        if !package.is_empty() {
269            cmd.args(package.iter().flat_map(|package| ["--package", package]));
270        }
271    }
272}
273
274#[derive(Default, Clone, Debug, PartialEq, Eq, clap::Args)]
275#[command(about = None, long_about = None, next_help_heading = "Feature Selection")]
276pub struct Features {
277    #[arg(short = 'F', long, value_delimiter = ' ')]
278    /// Space-separated list of features to activate
279    pub features: Vec<String>,
280    #[arg(long)]
281    /// Activate all available features
282    pub all_features: bool,
283    #[arg(long)]
284    /// Do not activate the `default` feature
285    pub no_default_features: bool,
286}
287
288impl Features {
289    fn forward_args(&self, cmd: &mut Command) {
290        let Features { features, all_features, no_default_features } = self;
291        if !features.is_empty() {
292            cmd.args(features.iter().flat_map(|feature| ["--features", feature]));
293        }
294        if *all_features {
295            cmd.arg("--all-features");
296        }
297        if *no_default_features {
298            cmd.arg("--no-default-features");
299        }
300    }
301
302    fn forward_to_metadata(&self, meta: &mut MetadataCommand) {
303        let Features { features, all_features, no_default_features } = self;
304        if *all_features {
305            meta.features(cargo_metadata::CargoOpt::AllFeatures);
306        }
307        if *no_default_features {
308            meta.features(cargo_metadata::CargoOpt::NoDefaultFeatures);
309        }
310        if !features.is_empty() {
311            meta.features(cargo_metadata::CargoOpt::SomeFeatures(features.clone()));
312        }
313    }
314}
315
316#[derive(Debug, clap::Args)]
317#[command(next_help_heading = "Compilation Options")]
318pub struct CompilationOptions {
319    #[arg(short = 'j', long, value_name = "N")]
320    /// Number of parallel jobs, defaults to # of CPUs.
321    pub jobs: Option<u32>,
322    #[arg(long)]
323    /// Do not abort the build as soon as there is an error
324    pub keep_going: bool,
325    #[arg(long, value_name = "TRIPLE")]
326    /// Check for the target triple
327    pub target: Vec<String>,
328}
329
330impl CompilationOptions {
331    fn forward_args(&self, cmd: &mut Command) {
332        let CompilationOptions { jobs, keep_going, target } = self;
333        if let Some(jobs) = jobs {
334            cmd.args(["--jobs", &format!("{jobs}")]);
335        }
336        if *keep_going {
337            cmd.arg("--keep-going");
338        }
339        for t in target {
340            cmd.args(["--target", t]);
341        }
342    }
343}
344
345#[derive(Debug, clap::Args)]
346#[command(next_help_heading = "Manifest Options")]
347pub struct ManifestOptions {
348    #[arg(long, name = "PATH")]
349    /// Path to Cargo.toml
350    manifest_path: Option<Utf8PathBuf>,
351    /// Run without accessing the network
352    #[arg(long)]
353    offline: bool,
354}
355
356impl ManifestOptions {
357    fn forward_args(&self, cmd: &mut Command) {
358        let ManifestOptions { manifest_path, offline } = self;
359        if let Some(manifest_path) = &manifest_path {
360            cmd.args(["--manifest-path", manifest_path.as_str()]);
361        }
362        if *offline {
363            cmd.arg("--offline");
364        }
365    }
366
367    fn forward_to_metadata(&self, meta: &mut MetadataCommand) {
368        // TODO(nilehmann) should we pass offline to metadata?
369        let ManifestOptions { manifest_path, offline: _ } = self;
370        if let Some(manifest_path) = &manifest_path {
371            meta.manifest_path(manifest_path);
372        }
373    }
374}