cargo_whatfeatures/
registry.rs

1use anyhow::Context as _;
2use std::{collections::HashSet, path::PathBuf};
3
4use crate::features::Workspace;
5
6/// Local disk registry (cargo and our own)
7pub struct Registry {
8    cached: HashSet<Crate>,
9    local: HashSet<Crate>,
10}
11
12impl Registry {
13    /// Create a registry from the local cache (cargos and ours)
14    pub fn from_local() -> anyhow::Result<Self> {
15        use crate_version_parse::CrateVersion;
16
17        // TODO use jwalk here
18        let home = home::cargo_home()?
19            .join("registry")
20            .join("src")
21            .read_dir()
22            .with_context(|| "expected to have a local registry")?;
23
24        let (mut set, mut local) = (HashSet::new(), HashSet::new());
25
26        for path in home
27            .filter_map(|dir| dir.ok()?.path().read_dir().ok())
28            .flat_map(|dir| dir.flatten())
29            .map(|s| s.path())
30        {
31            if let Some(name) = path.file_name().and_then(|s| s.to_str()) {
32                let CrateVersion { name, version } = CrateVersion::try_parse(name)?;
33                set.insert(Crate {
34                    name: name.to_string(),
35                    version: version.to_string(),
36                    path,
37                    yanked: YankState::UnknownLocal, // TODO we can do an http request to figure this out
38                });
39            }
40        }
41
42        // TODO this should probably be a warning at the least
43        if let Ok(base) = crate::util::cache_dir() {
44            // TODO use jwalk here
45            for dir in base
46                .read_dir()
47                .into_iter()
48                .flat_map(|dir| dir.flatten())
49                .filter_map(|dir| {
50                    let path = dir.path();
51                    if !path.is_dir() {
52                        return None;
53                    }
54                    path.into()
55                })
56            {
57                let name = dir.strip_prefix(&base)?.to_str().expect("valid utf-8");
58                let CrateVersion { name, version } = CrateVersion::try_parse(name)?;
59                let crate_ = Crate {
60                    name: name.to_string(),
61                    version: version.to_string(),
62                    path: dir.clone(),
63                    yanked: YankState::UnknownLocal, // TODO we can do a http request to figure this out
64                };
65
66                if set.contains(&crate_) {
67                    // remove the cache directory (it already exists in the .cargo/registry)
68                    std::fs::remove_dir_all(dir)?;
69                } else {
70                    local.insert(crate_);
71                }
72            }
73        }
74
75        Ok(Self { cached: set, local })
76    }
77
78    /// Tries to get the crate/version from the registry
79    pub fn get(&self, crate_name: &str, crate_version: &str) -> Option<&Crate> {
80        self.cached
81            .iter()
82            .chain(self.local.iter())
83            .find(|Crate { name, version, .. }| name == crate_name && version == crate_version)
84    }
85
86    /// Tries to the the latest version from the cached registry
87    pub fn maybe_latest(&self, crate_name: &str) -> Option<&Crate> {
88        self.cached
89            .iter()
90            .chain(self.local.iter())
91            .filter(|Crate { name, .. }| name == crate_name)
92            .max_by(|Crate { version: left, .. }, Crate { version: right, .. }| left.cmp(&right))
93    }
94
95    /// Purge the local cache, returning how many crates it removed
96    pub fn purge_local_cache(&mut self) -> anyhow::Result<usize> {
97        let mut count = 0;
98        for crate_ in self.local.drain() {
99            std::fs::remove_dir_all(&crate_.path)?;
100            count += 1;
101        }
102        Ok(count)
103    }
104}
105
106/// Whether this crate was marked as yanked on crates.io
107#[derive(Copy, Clone, Debug, PartialEq, Eq)]
108pub enum YankState {
109    /// It was yanked
110    Yanked,
111    /// The its cached locally, so we can't know unless we do a http req.
112    // technically yanking only exists on crates.io, not other registries
113    UnknownLocal,
114    /// Its not been yanked
115    Available,
116}
117
118impl From<bool> for YankState {
119    fn from(yanked: bool) -> Self {
120        if yanked {
121            Self::Yanked
122        } else {
123            Self::Available
124        }
125    }
126}
127
128/// A crate stored on disk
129#[derive(Clone, Debug, Eq)]
130pub struct Crate {
131    /// Crate name
132    pub name: String,
133    /// Crate version
134    pub version: String,
135    /// Path to the crate directory
136    pub path: PathBuf,
137    /// Whether this crate was marked as yanked
138    pub yanked: YankState,
139}
140
141impl Crate {
142    /// Tries to get the features for the crate
143    pub fn get_features(&self) -> anyhow::Result<Workspace> {
144        cargo_metadata::MetadataCommand::new()
145            .no_deps()
146            .manifest_path(self.path.join("./Cargo.toml"))
147            .exec()
148            .map(|md| Workspace::parse(md, &self.name))
149            .map_err(Into::into)
150    }
151
152    /// Tries to get the features from a local crate -- without traversing workspace
153    pub fn from_local(path: impl Into<PathBuf>) -> anyhow::Result<Workspace> {
154        let path = path.into();
155
156        if let Some(file_name) = path.file_name() {
157            anyhow::ensure!(
158                file_name == "Cargo.toml",
159                "Path must be a directory or 'Cargo.toml'"
160            );
161            anyhow::ensure!(path.is_file(), "invalid manifest path");
162        } else {
163            anyhow::ensure!(path.join("Cargo.toml").is_file(), "invalid manifest path");
164        }
165
166        let name = path
167            .iter()
168            .last()
169            .unwrap_or_else(|| path.as_ref())
170            .to_string_lossy();
171
172        cargo_metadata::MetadataCommand::new()
173            .current_dir(&path)
174            .no_deps()
175            .exec()
176            .map(|md| Workspace::parse(md, &name))
177            .map(|mut ws| {
178                ws.map.retain(|k, _| {
179                    k.repr
180                        .splitn(2, ' ')
181                        .next()
182                        .filter(|&s| s == name)
183                        .is_some()
184                });
185                ws
186            })
187            .map_err(Into::into)
188    }
189
190    /// Tries to get the features from a local crate
191    pub fn from_path(path: impl Into<PathBuf>) -> anyhow::Result<Workspace> {
192        let path = path.into();
193
194        fn find_parent(mut path: PathBuf) -> PathBuf {
195            while !path.is_dir() {
196                if !path.pop() {
197                    break;
198                }
199            }
200
201            // if we do not have a directory, use '.' to mean the cwd
202            if !path.is_dir() {
203                let mut path = PathBuf::new();
204                path.push(".");
205                return path;
206            }
207
208            path
209        }
210
211        let name = find_parent(std::fs::canonicalize(path.clone())?);
212        let name = name
213            .iter()
214            .last()
215            .unwrap_or_else(|| path.as_ref())
216            .to_string_lossy();
217
218        let path = find_parent(path.clone());
219
220        cargo_metadata::MetadataCommand::new()
221            .current_dir(&path)
222            .no_deps()
223            .exec()
224            .map(|md| Workspace::parse(md, &name))
225            .map_err(Into::into)
226    }
227}
228
229impl PartialEq for Crate {
230    fn eq(&self, other: &Self) -> bool {
231        self.name == other.name && self.version == other.version
232    }
233}
234
235impl std::hash::Hash for Crate {
236    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
237        state.write(self.name.as_bytes());
238        state.write(self.version.as_bytes());
239    }
240}