Skip to main content

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                if check_opts.fix {
49                    // `cargo fix` applies span_suggestion replacements written by flux-driver.
50                    //
51                    // Mechanism: cargo fix sets RUSTC_WORKSPACE_WRAPPER to a self-re-exec proxy
52                    // that runs flux-driver (our RUSTC) with --error-format=json, parses the
53                    // resulting diagnostic JSON for suggested_replacement spans, and applies them
54                    // via rustfix. This is the same pipeline Clippy uses, just with RUSTC= instead
55                    // of RUSTC_WORKSPACE_WRAPPER= on our side; the two env vars are independent.
56                    cmd.arg("fix")
57                        // Implies broken-code (since Flux errors are not
58                        // warnings, it wouldn't apply otherwise).
59                        .arg("--broken-code");
60                } else {
61                    cmd.arg("check");
62                }
63                check_opts.forward_args(cmd);
64            }
65            CargoFluxCommand::Build(build_opts) => {
66                cmd.arg("build");
67                build_opts.forward_args(cmd);
68            }
69            CargoFluxCommand::Clean(clean_opts) => {
70                cmd.arg("clean");
71                clean_opts.forward_args(cmd);
72            }
73        }
74        cmd.args(["--profile", "flux"]);
75        cmd.args(["--config".as_ref(), config_file.as_os_str()]);
76    }
77
78    pub fn metadata(&self) -> MetadataCommand {
79        let mut meta = cargo_metadata::MetadataCommand::new();
80        match self {
81            CargoFluxCommand::Check(check_options) | CargoFluxCommand::Build(check_options) => {
82                check_options.forward_to_metadata(&mut meta);
83            }
84            CargoFluxCommand::Clean(clean_options) => {
85                clean_options.forward_to_metadata(&mut meta);
86            }
87        }
88        meta
89    }
90
91    pub fn targeted_package_ids(&self, metadata: &Metadata) -> HashSet<PackageId> {
92        match self {
93            CargoFluxCommand::Check(opts) | CargoFluxCommand::Build(opts) => {
94                opts.targeted_package_ids(metadata)
95            }
96            CargoFluxCommand::Clean(opts) => opts.targeted_package_ids(metadata),
97        }
98    }
99
100    pub fn only_check(&self) -> Option<&str> {
101        match self {
102            CargoFluxCommand::Check(opts) | CargoFluxCommand::Build(opts) => {
103                opts.only_check.as_deref()
104            }
105            CargoFluxCommand::Clean(_) => None,
106        }
107    }
108}
109
110#[derive(clap::Args)]
111pub struct CompileOpts {
112    /// Error format [possible values: human, short, json, json-diagnostic-short, json-diagnostic-rendered-ansi, json-render-diagnostics]
113    #[arg(long, value_name = "FMT")]
114    message_format: Option<String>,
115
116    #[command(flatten)]
117    workspace: Workspace,
118    #[command(flatten)]
119    features: Features,
120    #[command(flatten)]
121    compilation: CompilationOptions,
122    #[command(flatten)]
123    manifest: ManifestOptions,
124    #[command(flatten)]
125    flux_flags: Flags,
126
127    /// Only check items matching PATTERN (overrides include patterns from cargo.toml or flux.toml).
128    ///
129    /// Supported patterns:
130    ///   def:<name>              — match items whose name contains <name>
131    ///   span:<file>:<line>:<col> — match the item at a source location
132    ///   glob:<pattern>          — match files by glob (e.g. "glob:src/ascii/*.rs")
133    ///   <pattern>               — bare string treated as a glob
134    #[arg(long, value_name = "PATTERN")]
135    pub only_check: Option<String>,
136
137    /// Automatically apply Flux's suggested annotations to the source code
138    /// using 'cargo fix'.
139    ///
140    /// Implies --broken-code.
141    #[arg(long)]
142    pub fix: bool,
143
144    #[command(flatten)]
145    fix_opts: FixOpts,
146}
147
148impl CompileOpts {
149    fn forward_args(&self, cmd: &mut Command) {
150        let CompileOpts {
151            message_format,
152            workspace,
153            features,
154            compilation,
155            manifest,
156            fix: _,
157            fix_opts,
158            ..
159        } = self;
160        if let Some(message_format) = &message_format {
161            cmd.args(["--message-format", message_format]);
162        }
163        workspace.forward_args(cmd);
164        features.forward_args(cmd);
165        compilation.forward_args(cmd);
166        manifest.forward_args(cmd);
167        fix_opts.forward_args(cmd);
168    }
169
170    fn forward_to_metadata(&self, meta: &mut MetadataCommand) {
171        let CompileOpts { features, manifest, fix: _, fix_opts: _, .. } = self;
172        features.forward_to_metadata(meta);
173        manifest.forward_to_metadata(meta);
174    }
175
176    pub fn targeted_package_ids(&self, metadata: &Metadata) -> HashSet<PackageId> {
177        targeted_package_ids(&self.workspace, metadata)
178    }
179}
180
181fn package_matches_spec(package: &CargoPackage, spec: &str) -> bool {
182    if package.id.repr == spec {
183        return true;
184    }
185
186    if let Some((name, version)) = spec.rsplit_once('@') {
187        return package.name == name && package.version.to_string() == version;
188    }
189
190    package.name == spec
191}
192
193fn select_packages_by_spec<'a>(package: &Package, metadata: &'a Metadata) -> Vec<&'a CargoPackage> {
194    let workspace_packages = metadata.workspace_packages();
195    if !package.package.is_empty() {
196        workspace_packages
197            .into_iter()
198            .filter(|p| {
199                package
200                    .package
201                    .iter()
202                    .any(|spec| package_matches_spec(p, spec))
203            })
204            .collect()
205    } else if metadata.workspace_default_members.is_available() {
206        metadata.workspace_default_packages()
207    } else if let Some(root) = metadata.root_package() {
208        vec![root]
209    } else {
210        workspace_packages
211    }
212}
213
214fn targeted_package_ids(workspace: &Workspace, metadata: &Metadata) -> HashSet<PackageId> {
215    let mut packages = if workspace.workspace {
216        metadata.workspace_packages()
217    } else {
218        select_packages_by_spec(&workspace.package, metadata)
219    };
220
221    packages.retain(|p| {
222        !workspace
223            .exclude
224            .iter()
225            .any(|spec| package_matches_spec(p, spec))
226    });
227
228    packages.into_iter().map(|p| p.id.clone()).collect()
229}
230
231#[derive(clap::Args)]
232pub struct CleanOpts {
233    #[command(flatten, next_help_heading = "Package Selection")]
234    package: Package,
235    #[command(flatten)]
236    features: Features,
237    #[command(flatten)]
238    manifest: ManifestOptions,
239}
240
241impl CleanOpts {
242    fn forward_args(&self, cmd: &mut Command) {
243        let CleanOpts { package, features, manifest } = self;
244        package.forward_args(cmd);
245        features.forward_args(cmd);
246        manifest.forward_args(cmd);
247    }
248
249    fn forward_to_metadata(&self, meta: &mut MetadataCommand) {
250        let CleanOpts { package: _, features, manifest } = self;
251        features.forward_to_metadata(meta);
252        manifest.forward_to_metadata(meta);
253    }
254
255    pub fn targeted_package_ids(&self, metadata: &Metadata) -> HashSet<PackageId> {
256        select_packages_by_spec(&self.package, metadata)
257            .into_iter()
258            .map(|p| p.id.clone())
259            .collect()
260    }
261}
262
263#[derive(Debug, clap::Args)]
264#[command(about = None, long_about = None, next_help_heading = "Package Selection")]
265pub struct Workspace {
266    #[command(flatten)]
267    pub package: Package,
268
269    #[arg(long)]
270    /// Process all packages in the workspace
271    pub workspace: bool,
272
273    #[arg(long, value_name = "SPEC")]
274    /// Exclude packages from being processed
275    pub exclude: Vec<String>,
276}
277
278impl Workspace {
279    fn forward_args(&self, cmd: &mut Command) {
280        let Workspace { package, workspace, exclude } = self;
281        package.forward_args(cmd);
282        if *workspace {
283            cmd.arg("--workspace");
284        }
285        if !exclude.is_empty() {
286            cmd.args(exclude.iter().flat_map(|package| ["--exclude", package]));
287        }
288    }
289}
290
291#[derive(Debug, clap::Args)]
292#[command(about = None, long_about = None)]
293pub struct Package {
294    #[arg(short, long, value_name = "SPEC")]
295    /// Package to process (see `cargo help pkgid`)
296    pub package: Vec<String>,
297}
298
299impl Package {
300    fn forward_args(&self, cmd: &mut Command) {
301        let Package { package } = self;
302        if !package.is_empty() {
303            cmd.args(package.iter().flat_map(|package| ["--package", package]));
304        }
305    }
306}
307
308#[derive(Default, Clone, Debug, PartialEq, Eq, clap::Args)]
309#[command(about = None, long_about = None, next_help_heading = "Feature Selection")]
310pub struct Features {
311    #[arg(short = 'F', long, value_delimiter = ' ')]
312    /// Space-separated list of features to activate
313    pub features: Vec<String>,
314    #[arg(long)]
315    /// Activate all available features
316    pub all_features: bool,
317    #[arg(long)]
318    /// Do not activate the `default` feature
319    pub no_default_features: bool,
320}
321
322impl Features {
323    fn forward_args(&self, cmd: &mut Command) {
324        let Features { features, all_features, no_default_features } = self;
325        if !features.is_empty() {
326            cmd.args(features.iter().flat_map(|feature| ["--features", feature]));
327        }
328        if *all_features {
329            cmd.arg("--all-features");
330        }
331        if *no_default_features {
332            cmd.arg("--no-default-features");
333        }
334    }
335
336    fn forward_to_metadata(&self, meta: &mut MetadataCommand) {
337        let Features { features, all_features, no_default_features } = self;
338        if *all_features {
339            meta.features(cargo_metadata::CargoOpt::AllFeatures);
340        }
341        if *no_default_features {
342            meta.features(cargo_metadata::CargoOpt::NoDefaultFeatures);
343        }
344        if !features.is_empty() {
345            meta.features(cargo_metadata::CargoOpt::SomeFeatures(features.clone()));
346        }
347    }
348}
349
350#[derive(Debug, clap::Args)]
351#[command(next_help_heading = "Compilation Options")]
352pub struct CompilationOptions {
353    #[arg(short = 'j', long, value_name = "N")]
354    /// Number of parallel jobs, defaults to # of CPUs.
355    pub jobs: Option<u32>,
356    #[arg(long)]
357    /// Do not abort the build as soon as there is an error
358    pub keep_going: bool,
359    #[arg(long, value_name = "TRIPLE")]
360    /// Check for the target triple
361    pub target: Vec<String>,
362}
363
364impl CompilationOptions {
365    fn forward_args(&self, cmd: &mut Command) {
366        let CompilationOptions { jobs, keep_going, target } = self;
367        if let Some(jobs) = jobs {
368            cmd.args(["--jobs", &format!("{jobs}")]);
369        }
370        if *keep_going {
371            cmd.arg("--keep-going");
372        }
373        for t in target {
374            cmd.args(["--target", t]);
375        }
376    }
377}
378
379#[derive(Debug, clap::Args)]
380#[command(next_help_heading = "Manifest Options")]
381pub struct ManifestOptions {
382    #[arg(long, name = "PATH")]
383    /// Path to Cargo.toml
384    manifest_path: Option<Utf8PathBuf>,
385    /// Run without accessing the network
386    #[arg(long)]
387    offline: bool,
388}
389
390impl ManifestOptions {
391    fn forward_args(&self, cmd: &mut Command) {
392        let ManifestOptions { manifest_path, offline } = self;
393        if let Some(manifest_path) = &manifest_path {
394            cmd.args(["--manifest-path", manifest_path.as_str()]);
395        }
396        if *offline {
397            cmd.arg("--offline");
398        }
399    }
400
401    fn forward_to_metadata(&self, meta: &mut MetadataCommand) {
402        // TODO(nilehmann) should we pass offline to metadata?
403        let ManifestOptions { manifest_path, offline: _ } = self;
404        if let Some(manifest_path) = &manifest_path {
405            meta.manifest_path(manifest_path);
406        }
407    }
408}
409
410/// Options forwarded verbatim to `cargo fix` when `--fix` is passed.
411/// These are ignored when running `cargo check`.
412#[derive(Default, Debug, clap::Args)]
413#[command(about = None, long_about = None, next_help_heading = "Fix Options")]
414pub struct FixOpts {
415    /// Don't require a clean git working directory when running `--fix`.
416    #[arg(long, requires = "fix")]
417    allow_dirty: bool,
418
419    /// Don't require a staged git index when running `--fix`.
420    #[arg(long, requires = "fix")]
421    allow_staged: bool,
422}
423
424impl FixOpts {
425    fn forward_args(&self, cmd: &mut Command) {
426        let FixOpts { allow_dirty, allow_staged } = self;
427        if *allow_dirty {
428            cmd.arg("--allow-dirty");
429        }
430        if *allow_staged {
431            cmd.arg("--allow-staged");
432        }
433    }
434}