1#![doc = include_str!("../t/LIBRARY.md")]
2
3use {
6 anyhow::{Result, anyhow},
7 dirs::home_dir,
8 rayon::prelude::*,
9 regex::RegexSet,
10 reqwest::blocking::Client,
11 serde::{Deserialize, Serialize},
12 sprint::{Command, Pipe, Shell},
13 std::{
14 collections::BTreeMap,
15 fs::File,
16 path::{Path, PathBuf},
17 sync::LazyLock,
18 },
19};
20
21static CLIENT: LazyLock<Client> = LazyLock::new(|| {
24 Client::builder()
25 .user_agent("cargo-list")
26 .build()
27 .expect("create reqwest client")
28});
29
30#[derive(Debug, Default, Serialize, Eq, PartialEq, Hash, Clone)]
34pub enum Kind {
35 Local,
36 Git,
37
38 #[default]
39 External,
40}
41
42use Kind::{External, Git, Local};
43
44pub const ALL_KINDS: [Kind; 3] = [Local, Git, External];
46
47impl Kind {
48 fn from(source: &str) -> Kind {
49 if source.starts_with("git+") {
50 Git
51 } else if source.starts_with("path+") {
52 Local
53 } else {
54 External
55 }
56 }
57}
58
59#[derive(Debug, Serialize, Deserialize)]
63pub struct Crates {
64 installs: BTreeMap<String, Crate>,
65
66 #[serde(skip)]
67 pub active_toolchain: String,
68
69 #[serde(skip)]
70 pub active_version: String,
71}
72
73impl Crates {
74 pub fn from(path: &Path) -> Result<Crates> {
87 Crates::from_include(path, &[])
88 }
89
90 #[must_use]
94 pub fn is_empty(&self) -> bool {
95 self.installs.is_empty()
96 }
97
98 #[must_use]
100 pub fn crates(&self) -> BTreeMap<&str, &Crate> {
101 self.installs
102 .values()
103 .map(|x| (x.name.as_str(), x))
104 .collect()
105 }
106
107 #[allow(clippy::missing_panics_doc)]
117 pub fn from_include(path: &Path, patterns: &[&str]) -> Result<Crates> {
118 let mut crates: Crates = serde_json::from_reader(File::open(path)?)?;
119 if !patterns.is_empty() {
120 let set = RegexSet::new(patterns)?;
121 crates.installs = crates
122 .installs
123 .into_par_iter()
124 .filter_map(|(k, v)| {
125 if set.is_match(k.split_once(' ').unwrap().0) {
126 Some((k, v))
127 } else {
128 None
129 }
130 })
131 .collect();
132 }
133 crates.active_toolchain = active_toolchain();
134 crates.active_version = crates
135 .active_toolchain
136 .lines()
137 .filter_map(|line| {
138 line.split(' ')
139 .skip_while(|&word| word != "rustc")
140 .nth(1)
141 .map(ToString::to_string)
142 })
143 .nth(0)
144 .unwrap();
145 let errors = crates
146 .installs
147 .par_iter_mut()
148 .filter_map(|(k, v)| v.init(k, &crates.active_version).err())
149 .collect::<Vec<_>>();
150 if errors.is_empty() {
151 Ok(crates)
152 } else {
153 Err(anyhow!(format!(
154 "Errors: {}",
155 errors
156 .iter()
157 .map(ToString::to_string)
158 .collect::<Vec<_>>()
159 .join(", ")
160 )))
161 }
162 }
163}
164
165#[derive(Debug, Serialize, Deserialize)]
169#[allow(clippy::struct_excessive_bools)]
170pub struct Crate {
171 #[serde(skip_deserializing)]
172 pub name: String,
173
174 #[serde(skip_deserializing)]
175 pub kind: Kind,
176
177 #[serde(skip_deserializing)]
178 pub installed: String,
179
180 #[serde(skip_deserializing)]
181 pub available: String,
182
183 #[serde(skip_deserializing)]
184 pub newer: Vec<String>,
185
186 #[serde(skip_deserializing)]
187 pub rust_version: String,
188
189 #[serde(skip_deserializing)]
190 pub outdated: bool,
191
192 #[serde(skip_deserializing)]
193 pub outdated_rust: bool,
194
195 #[serde(skip_deserializing)]
196 source: String,
197
198 pub version_req: Option<String>,
199 bins: Vec<String>,
200 features: Vec<String>,
201 all_features: bool,
202 no_default_features: bool,
203 profile: String,
204 target: String,
205 rustc: String,
206}
207
208impl Crate {
209 fn init(&mut self, k: &str, active_version: &str) -> Result<()> {
211 let mut s = k.split(' ');
212 self.name = s.next().unwrap().to_string();
213 self.installed = s.next().unwrap().to_string();
214 self.source = s
215 .next()
216 .unwrap()
217 .strip_prefix('(')
218 .unwrap()
219 .strip_suffix(')')
220 .unwrap()
221 .to_string();
222
223 self.kind = Kind::from(&self.source);
224
225 self.rust_version = self
226 .rustc
227 .strip_prefix("rustc ")
228 .unwrap()
229 .split_once(' ')
230 .unwrap()
231 .0
232 .to_string();
233
234 self.outdated_rust = self.rust_version != active_version;
235
236 if self.kind == External {
237 (self.available, self.newer) = latest(&self.name, &self.version_req)?;
238 self.outdated = self.installed != self.available;
239 }
240
241 Ok(())
242 }
243
244 pub fn update_command(&self, pinned: bool) -> Vec<String> {
252 let mut r = vec!["cargo", "install"];
253
254 if self.no_default_features {
255 r.push("--no-default-features");
256 }
257
258 let features = if self.features.is_empty() {
259 None
260 } else {
261 Some(self.features.join(","))
262 };
263 if let Some(features) = &features {
264 r.push("-F");
265 r.push(features);
266 }
267
268 if !pinned && let Some(version) = &self.version_req {
269 r.push("--version");
270 r.push(version);
271 }
272
273 r.push("--profile");
274 r.push(&self.profile);
275
276 r.push("--target");
277 r.push(&self.target);
278
279 if self.outdated_rust {
280 r.push("--force");
281 }
282
283 if self.kind == Git {
284 r.push("--git");
285 r.push(&self.source[4..self.source.find('#').unwrap()]);
286 for bin in &self.bins {
287 r.push(bin);
288 }
289 } else {
290 r.push(&self.name);
291 }
292
293 r.into_iter().map(String::from).collect()
294 }
295}
296
297#[derive(Debug, Deserialize)]
304struct Versions {
305 versions: Vec<Version>,
306}
307
308impl Versions {
309 fn iter(&self) -> std::slice::Iter<'_, Version> {
310 self.versions.iter()
311 }
312
313 fn available(&self) -> Vec<&Version> {
314 self.iter().filter(|x| x.is_available()).collect()
315 }
316}
317
318#[derive(Debug, Deserialize)]
319struct Version {
320 num: semver::Version,
321 yanked: bool,
322}
323
324impl Version {
325 fn is_available(&self) -> bool {
326 self.num.pre.is_empty() && !self.yanked
327 }
328}
329
330pub fn latest(name: &str, version_req: &Option<String>) -> Result<(String, Vec<String>)> {
341 let url = format!("https://crates.io/api/v1/crates/{name}/versions");
342 let res = CLIENT.get(url).send()?;
343 let versions = res.json::<Versions>()?;
344 let available = versions.available();
345 if let Some(req) = version_req {
346 let req = semver::VersionReq::parse(req)?;
347 let mut newer = vec![];
348 for v in &available {
349 if req.matches(&v.num) {
350 return Ok((v.num.to_string(), newer));
351 }
352 newer.push(v.num.to_string());
353 }
354 Err(anyhow!(
355 "Failed to find an available version matching the requirement"
356 ))
357 } else if available.is_empty() {
358 Err(anyhow!("Failed to find any available version"))
359 } else {
360 Ok((available[0].num.to_string(), vec![]))
361 }
362}
363
364#[must_use]
366pub fn active_toolchain() -> String {
367 let r = Shell {
368 print: false,
369 ..Default::default()
370 }
371 .run(&[Command {
372 command: String::from("rustup show active-toolchain -v"),
373 stdout: Pipe::string(),
374 ..Default::default()
375 }]);
376 if let Pipe::String(Some(s)) = &r[0].stdout {
377 s.clone()
378 } else {
379 String::new()
380 }
381}
382
383#[must_use]
391pub fn expanduser(path: &str) -> PathBuf {
392 if path == "~" {
393 home_dir().unwrap().join(&path[1..])
394 } else if path.starts_with('~') {
395 home_dir().unwrap().join(&path[2..])
396 } else {
397 PathBuf::from(&path)
398 }
399}