cargo_files_core/
lib.rs

1// Inspired by  https://github.com/rust-lang/rustfmt
2pub mod parser;
3
4use crate::parser::extract_crate_files;
5pub use cargo_metadata::Edition;
6use std::cmp::Ordering;
7use std::collections::{BTreeSet, HashSet};
8use std::hash::{Hash, Hasher};
9use std::io::{self};
10use std::path::{Path, PathBuf};
11use cargo_metadata::TargetKind;
12use thiserror::Error;
13
14#[derive(Debug, Error)]
15pub enum Error {
16    #[error("the manifest-path must be a path to a Cargo.toml file")]
17    ManifestNotCargoToml,
18    #[error("no targets were found")]
19    NoTargets,
20    #[error("there was an error reading Cargo.toml: {0}")]
21    ManifestError(io::Error),
22    #[error("there was an error reading {0}: {1}")]
23    FileError(PathBuf, io::Error),
24    #[error("there was an error parsing a source file: {0}")]
25    ParseError(#[from] syn::Error),
26    #[error("could not find module")]
27    ModuleNotFound,
28    #[error("source file must have parent")]
29    NoParent,
30    #[error("source file must have a stem")]
31    NoStem,
32}
33
34/// Get all source files for the given target.
35pub fn get_target_files(target: &Target) -> Result<HashSet<PathBuf>, Error> {
36    let mut acc = HashSet::new();
37    extract_crate_files(&target.path, &target.path, &mut acc)?;
38    Ok(acc)
39}
40
41/// Get all targets within the given cargo workspace.
42pub fn get_targets(manifest_path: Option<&Path>) -> Result<BTreeSet<Target>, Error> {
43    if let Some(specified_manifest_path) = manifest_path {
44        if !specified_manifest_path.ends_with("Cargo.toml") {
45            return Err(Error::ManifestNotCargoToml);
46        }
47        _get_targets(Some(specified_manifest_path))
48    } else {
49        _get_targets(None)
50    }
51}
52
53/// Target uses a `path` field for equality and hashing.
54#[derive(Debug)]
55pub struct Target {
56    /// A path to the main source file of the target.
57    pub path: PathBuf,
58    /// A kind of target (e.g., lib, bin, example, ...).
59    pub kind: TargetKind,
60    /// Rust edition for this target.
61    pub edition: Edition,
62}
63
64impl Target {
65    pub fn from_target(target: &cargo_metadata::Target) -> Self {
66        let path = PathBuf::from(&target.src_path);
67        let canonicalized = dunce::canonicalize(&path).unwrap_or(path);
68
69        Target {
70            path: canonicalized,
71            kind: target.kind[0].clone(),
72            edition: target.edition,
73        }
74    }
75}
76
77impl PartialEq for Target {
78    fn eq(&self, other: &Target) -> bool {
79        self.path == other.path
80    }
81}
82
83impl PartialOrd for Target {
84    fn partial_cmp(&self, other: &Target) -> Option<Ordering> {
85        Some(self.cmp(other))
86    }
87}
88
89impl Ord for Target {
90    fn cmp(&self, other: &Target) -> Ordering {
91        self.path.cmp(&other.path)
92    }
93}
94
95impl Eq for Target {}
96
97impl Hash for Target {
98    fn hash<H: Hasher>(&self, state: &mut H) {
99        self.path.hash(state);
100    }
101}
102
103/// Get all targets from the specified manifest.
104fn _get_targets(manifest_path: Option<&Path>) -> Result<BTreeSet<Target>, Error> {
105    let mut targets = BTreeSet::new();
106    get_targets_recursive(manifest_path, &mut targets, &mut BTreeSet::new())?;
107
108    if targets.is_empty() {
109        Err(Error::NoTargets)
110    } else {
111        Ok(targets)
112    }
113}
114
115fn get_targets_recursive(
116    manifest_path: Option<&Path>,
117    targets: &mut BTreeSet<Target>,
118    visited: &mut BTreeSet<String>,
119) -> Result<(), Error> {
120    let metadata = get_cargo_metadata(manifest_path).map_err(Error::ManifestError)?;
121
122    for package in &metadata.packages {
123        add_targets(&package.targets, targets);
124
125        // Look for local dependencies using information available since cargo v1.51
126        for dependency in &package.dependencies {
127            if dependency.path.is_none() || visited.contains(&dependency.name) {
128                continue;
129            }
130
131            let manifest_path = PathBuf::from(dependency.path.as_ref().unwrap()).join("Cargo.toml");
132            if manifest_path.exists()
133                && !metadata
134                    .packages
135                    .iter()
136                    .any(|p| p.manifest_path.eq(&manifest_path))
137            {
138                visited.insert(dependency.name.to_owned());
139                get_targets_recursive(Some(&manifest_path), targets, visited)?;
140            }
141        }
142    }
143
144    Ok(())
145}
146
147fn add_targets(target_paths: &[cargo_metadata::Target], targets: &mut BTreeSet<Target>) {
148    for target in target_paths {
149        targets.insert(Target::from_target(target));
150    }
151}
152
153fn get_cargo_metadata(manifest_path: Option<&Path>) -> Result<cargo_metadata::Metadata, io::Error> {
154    let mut cmd = cargo_metadata::MetadataCommand::new();
155    cmd.no_deps();
156    if let Some(manifest_path) = manifest_path {
157        cmd.manifest_path(manifest_path);
158    }
159    cmd.other_options(vec![String::from("--offline")]);
160
161    match cmd.exec() {
162        Ok(metadata) => Ok(metadata),
163        Err(_) => {
164            cmd.other_options(vec![]);
165            match cmd.exec() {
166                Ok(metadata) => Ok(metadata),
167                Err(error) => Err(io::Error::new(io::ErrorKind::Other, error.to_string())),
168            }
169        }
170    }
171}