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 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 #[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 #[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 pub workspace: bool,
238
239 #[arg(long, value_name = "SPEC")]
240 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 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 pub features: Vec<String>,
280 #[arg(long)]
281 pub all_features: bool,
283 #[arg(long)]
284 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 pub jobs: Option<u32>,
322 #[arg(long)]
323 pub keep_going: bool,
325 #[arg(long, value_name = "TRIPLE")]
326 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 manifest_path: Option<Utf8PathBuf>,
351 #[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 let ManifestOptions { manifest_path, offline: _ } = self;
370 if let Some(manifest_path) = &manifest_path {
371 meta.manifest_path(manifest_path);
372 }
373 }
374}