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