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 {
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
110#[derive(clap::Args)]
111pub struct CompileOpts {
112 #[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 #[arg(long, value_name = "PATTERN")]
135 pub only_check: Option<String>,
136
137 #[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 pub workspace: bool,
272
273 #[arg(long, value_name = "SPEC")]
274 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 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 pub features: Vec<String>,
314 #[arg(long)]
315 pub all_features: bool,
317 #[arg(long)]
318 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 pub jobs: Option<u32>,
356 #[arg(long)]
357 pub keep_going: bool,
359 #[arg(long, value_name = "TRIPLE")]
360 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 manifest_path: Option<Utf8PathBuf>,
385 #[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 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#[derive(Default, Debug, clap::Args)]
413#[command(about = None, long_about = None, next_help_heading = "Fix Options")]
414pub struct FixOpts {
415 #[arg(long, requires = "fix")]
417 allow_dirty: bool,
418
419 #[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}