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 {
17 #[command(flatten)]
18 check_opts: CompileOpts,
19
20 #[command(subcommand)]
21 command: Option<CargoFluxCommand>,
22
23 #[arg(short = 'V', long, action = clap::ArgAction::SetTrue)]
25 version: bool,
26
27 #[arg(short, long, action = clap::ArgAction::Count)]
29 verbose: u8,
30 },
31}
32
33#[derive(clap::Subcommand)]
34pub enum CargoFluxCommand {
35 Check(CompileOpts),
38 Build(CompileOpts),
40 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 cmd.arg("fix")
57 .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 #[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 #[arg(long, value_name = "PATTERN")]
144 pub only_check: Option<String>,
145
146 #[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 pub workspace: bool,
281
282 #[arg(long, value_name = "SPEC")]
283 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 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 pub features: Vec<String>,
323 #[arg(long)]
324 pub all_features: bool,
326 #[arg(long)]
327 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 pub jobs: Option<u32>,
365 #[arg(long)]
366 pub keep_going: bool,
368 #[arg(long, value_name = "TRIPLE")]
369 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 manifest_path: Option<Utf8PathBuf>,
394 #[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 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#[derive(Default, Debug, clap::Args)]
422#[command(about = None, long_about = None, next_help_heading = "Fix Options")]
423pub struct FixOpts {
424 #[arg(long, requires = "fix")]
426 allow_dirty: bool,
427
428 #[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}