cargo_subcommand/
subcommand.rs

1use crate::args::Args;
2use crate::artifact::{Artifact, ArtifactType};
3use crate::error::{Error, Result};
4use crate::manifest::Manifest;
5use crate::profile::Profile;
6use crate::{utils, CrateType, LocalizedConfig};
7use std::collections::HashMap;
8use std::ffi::OsStr;
9use std::path::{Path, PathBuf};
10
11#[derive(Debug)]
12pub struct Subcommand {
13    args: Args,
14    package: String,
15    workspace_manifest: Option<PathBuf>,
16    manifest: PathBuf,
17    target_dir: PathBuf,
18    host_triple: String,
19    profile: Profile,
20    lib_artifact: Option<Artifact>,
21    bin_artifacts: Vec<Artifact>,
22    example_artifacts: Vec<Artifact>,
23    config: Option<LocalizedConfig>,
24}
25
26impl Subcommand {
27    pub fn new(args: Args) -> Result<Self> {
28        // TODO: support multiple packages properly
29        assert!(
30            args.package.len() < 2,
31            "Multiple packages are not supported yet by `cargo-subcommand`"
32        );
33        let package = args.package.get(0).map(|s| s.as_str());
34        assert!(
35            !args.workspace,
36            "`--workspace` is not supported yet by `cargo-subcommand`"
37        );
38        assert!(
39            args.exclude.is_empty(),
40            "`--exclude` is not supported yet by `cargo-subcommand`"
41        );
42
43        let manifest_path = args
44            .manifest_path
45            .clone()
46            .map(|path| {
47                if path.file_name() != Some(OsStr::new("Cargo.toml")) || !path.is_file() {
48                    Err(Error::ManifestPathNotFound)
49                } else {
50                    Ok(path)
51                }
52            })
53            .transpose()?;
54
55        let search_path = manifest_path.map_or_else(
56            || std::env::current_dir().map_err(|e| Error::Io(PathBuf::new(), e)),
57            |manifest_path| utils::canonicalize(manifest_path.parent().unwrap()),
58        )?;
59
60        // Scan up the directories based on --manifest-path and the working directory to find a Cargo.toml
61        let potential_manifest = utils::find_manifest(&search_path)?;
62        // Perform the same scan, but for a Cargo.toml containing [workspace]
63        let workspace_manifest = utils::find_workspace(&search_path)?;
64
65        let (manifest_path, manifest) = {
66            if let Some(workspace_manifest) = &workspace_manifest {
67                utils::find_package_manifest_in_workspace(
68                    workspace_manifest,
69                    potential_manifest,
70                    package,
71                )?
72            } else {
73                let (manifest_path, manifest) = potential_manifest;
74                manifest.map_nonvirtual_package(manifest_path, package)?
75            }
76        };
77
78        // The manifest is known to contain a package at this point
79        let package = &manifest.package.as_ref().unwrap().name;
80
81        let root_dir = manifest_path.parent().unwrap();
82
83        // TODO: Find, parse, and merge _all_ config files following the hierarchical structure:
84        // https://doc.rust-lang.org/cargo/reference/config.html#hierarchical-structure
85        let config = LocalizedConfig::find_cargo_config_for_workspace(root_dir)?;
86        if let Some(config) = &config {
87            config.set_env_vars().unwrap();
88        }
89
90        let parsed_manifest = Manifest::parse_from_toml(&manifest_path)?;
91
92        let target_dir = args
93            .target_dir
94            .clone()
95            .or_else(|| {
96                std::env::var_os("CARGO_BUILD_TARGET_DIR")
97                    .or_else(|| std::env::var_os("CARGO_TARGET_DIR"))
98                    .map(|os_str| os_str.into())
99            })
100            .map(|target_dir| {
101                if target_dir.is_relative() {
102                    std::env::current_dir().unwrap().join(target_dir)
103                } else {
104                    target_dir
105                }
106            });
107
108        let target_dir = target_dir.unwrap_or_else(|| {
109            workspace_manifest
110                .as_ref()
111                .map(|(path, _)| path)
112                .unwrap_or_else(|| &manifest_path)
113                .parent()
114                .unwrap()
115                .join(utils::get_target_dir_name(config.as_deref()).unwrap())
116        });
117
118        // https://doc.rust-lang.org/cargo/reference/cargo-targets.html#target-auto-discovery
119
120        let main_bin_path = Path::new("src/main.rs");
121        let main_lib_path = Path::new("src/lib.rs");
122
123        let mut bin_artifacts = HashMap::new();
124        let mut example_artifacts = HashMap::new();
125
126        fn find_main_file(dir: &Path, name: &str) -> Option<PathBuf> {
127            let alt_path = dir.join(format!("{}.rs", name));
128            alt_path.is_file().then_some(alt_path).or_else(|| {
129                let alt_path = dir.join(name).join("main.rs");
130                alt_path.is_file().then_some(alt_path)
131            })
132        }
133
134        // Add all explicitly configured binaries
135        for bin in &parsed_manifest.bins {
136            let path = bin
137                .path
138                .clone()
139                .or_else(|| find_main_file(&root_dir.join("src/bin"), &bin.name))
140                .ok_or_else(|| Error::BinNotFound(bin.name.clone()))?;
141
142            let prev = bin_artifacts.insert(
143                bin.name.clone(),
144                Artifact {
145                    name: bin.name.clone(),
146                    path,
147                    r#type: ArtifactType::Bin,
148                },
149            );
150            if prev.is_some() {
151                return Err(Error::DuplicateBin(bin.name.clone()));
152            }
153        }
154
155        // Add all explicitly configured examples
156        for example in &parsed_manifest.examples {
157            let path = example
158                .path
159                .clone()
160                .or_else(|| find_main_file(&root_dir.join("examples"), &example.name))
161                .ok_or_else(|| Error::ExampleNotFound(example.name.clone()))?;
162
163            let prev = example_artifacts.insert(
164                example.name.clone(),
165                Artifact {
166                    name: example.name.clone(),
167                    path,
168                    r#type: ArtifactType::Example,
169                },
170            );
171            if prev.is_some() {
172                return Err(Error::DuplicateExample(example.name.clone()));
173            }
174        }
175
176        /// Name is typically the [`Path::file_stem()`], except for `src/main.rs` where it is the package name
177        fn insert_if_unconfigured(
178            name: Option<String>,
179            path: &Path,
180            r#type: ArtifactType,
181            artifacts: &mut HashMap<String, Artifact>,
182        ) {
183            // Only insert the detected binary if there isn't another artifact already configuring this file path
184            if artifacts.values().any(|bin| bin.path == path) {
185                println!("Already configuring {path:?}");
186                return;
187            }
188
189            let name =
190                name.unwrap_or_else(|| path.file_stem().unwrap().to_str().unwrap().to_owned());
191
192            // Only insert the detected binary if an artifact with the same name wasn't yet configured
193            artifacts.entry(name.clone()).or_insert(Artifact {
194                name,
195                path: path.to_owned(),
196                r#type,
197            });
198        }
199
200        // Parse all autobins
201        if parsed_manifest
202            .package
203            .as_ref()
204            .map_or(true, |p| p.autobins)
205        {
206            // Special-case for the main binary of a package
207            if root_dir.join(main_bin_path).is_file() {
208                insert_if_unconfigured(
209                    Some(package.clone()),
210                    main_bin_path,
211                    ArtifactType::Bin,
212                    &mut bin_artifacts,
213                );
214            }
215
216            for file in utils::list_rust_files(&root_dir.join("src").join("bin"))? {
217                let file = file.strip_prefix(root_dir).unwrap();
218
219                insert_if_unconfigured(None, file, ArtifactType::Bin, &mut bin_artifacts);
220            }
221        }
222
223        // Parse all autoexamples
224        if parsed_manifest
225            .package
226            .as_ref()
227            .map_or(true, |p| p.autoexamples)
228        {
229            for file in utils::list_rust_files(&root_dir.join("examples"))? {
230                let file = file.strip_prefix(root_dir).unwrap();
231
232                insert_if_unconfigured(None, file, ArtifactType::Example, &mut example_artifacts);
233            }
234        }
235
236        let mut lib_artifact = parsed_manifest
237            .lib
238            .as_ref()
239            .map(|lib| Artifact {
240                // The library is either configured with sensible defaults
241                name: lib.name.as_ref().unwrap_or(package).clone(),
242                path: lib.path.as_deref().unwrap_or(main_lib_path).to_owned(),
243                r#type: ArtifactType::Lib,
244            })
245            .or_else(|| {
246                // Or autodetected with the same defaults, if that default path exists
247                root_dir.join(main_lib_path).is_file().then(|| Artifact {
248                    name: package.clone(),
249                    path: main_lib_path.to_owned(),
250                    r#type: ArtifactType::Lib,
251                })
252            });
253
254        // Filtering based on arguments
255        // https://doc.rust-lang.org/cargo/reference/cargo-targets.html#binaries
256
257        let specific_target_selected = args.specific_target_selected();
258
259        if specific_target_selected {
260            if !args.lib {
261                lib_artifact = None;
262            }
263
264            if !args.bins {
265                bin_artifacts.retain(|a, _| args.bin.contains(a));
266            }
267
268            if !args.examples {
269                example_artifacts.retain(|a, _| args.example.contains(a));
270            }
271        }
272
273        let host_triple = current_platform::CURRENT_PLATFORM.to_owned();
274        let profile = args.profile();
275        Ok(Self {
276            args,
277            package: package.clone(),
278            workspace_manifest: workspace_manifest.map(|(path, _)| path),
279            manifest: manifest_path,
280            target_dir,
281            host_triple,
282            profile,
283            lib_artifact,
284            bin_artifacts: bin_artifacts.into_values().collect(),
285            example_artifacts: example_artifacts.into_values().collect(),
286            config,
287        })
288    }
289
290    pub fn args(&self) -> &Args {
291        &self.args
292    }
293
294    pub fn package(&self) -> &str {
295        &self.package
296    }
297
298    pub fn workspace_manifest(&self) -> Option<&Path> {
299        self.workspace_manifest.as_deref()
300    }
301
302    pub fn manifest(&self) -> &Path {
303        &self.manifest
304    }
305
306    pub fn target(&self) -> Option<&str> {
307        self.args.target.as_deref()
308    }
309
310    pub fn profile(&self) -> &Profile {
311        &self.profile
312    }
313
314    pub fn artifacts(&self) -> impl Iterator<Item = &Artifact> {
315        self.lib_artifact
316            .iter()
317            .chain(&self.bin_artifacts)
318            .chain(&self.example_artifacts)
319    }
320
321    pub fn target_dir(&self) -> &Path {
322        &self.target_dir
323    }
324
325    pub fn host_triple(&self) -> &str {
326        &self.host_triple
327    }
328
329    pub fn quiet(&self) -> bool {
330        self.args.quiet
331    }
332
333    pub fn config(&self) -> Option<&LocalizedConfig> {
334        self.config.as_ref()
335    }
336
337    pub fn build_dir(&self, target: Option<&str>) -> PathBuf {
338        let target_dir = dunce::simplified(self.target_dir());
339        let arch_dir = if let Some(target) = target {
340            target_dir.join(target)
341        } else {
342            target_dir.to_path_buf()
343        };
344        arch_dir.join(self.profile())
345    }
346
347    pub fn artifact(
348        &self,
349        artifact: &Artifact,
350        target: Option<&str>,
351        crate_type: CrateType,
352    ) -> PathBuf {
353        let triple = target.unwrap_or_else(|| self.host_triple());
354        let file_name = artifact.file_name(crate_type, triple);
355        self.build_dir(target)
356            .join(artifact.build_dir())
357            .join(file_name)
358    }
359}