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