cargo_install_latest/
lib.rs1use std::collections::{BTreeMap, HashMap};
2use std::io::{stderr, Write};
3use std::process::{Command, ExitStatus};
4
5extern crate tempdir;
6extern crate toml;
7
8pub fn installed_crates() -> Result<BTreeMap<String, Crate>, String> {
9 let mut cargo_list_installed = Command::new("cargo");
10 cargo_list_installed.arg("install");
11 cargo_list_installed.arg("--list");
12 let installed_output = cargo_list_installed
13 .output()
14 .map_err(|e| format!("I/O Error: {}", e))?;
15 let installed =
16 String::from_utf8(installed_output.stdout).map_err(|e| format!("UTF-8 Error: {}", e))?;
17
18 let mut crates: BTreeMap<String, Crate> = BTreeMap::new();
19 for line in installed.lines() {
20 let _crate = Crate::parse_list_output(line).map_err(|e| format!("Error: {:?}", e))?;
21 if let Some(_crate) = _crate {
22 if let Some(c) = crates.get(&_crate.name) {
23 if c.version > _crate.version {
28 continue;
29 }
30 }
31 crates.insert(_crate.name.clone(), _crate);
32 }
33 }
34 Ok(crates)
35}
36
37pub fn get_latest_versions(
38 required_crates: &HashMap<String, Crate>,
39) -> Result<HashMap<String, String>, String> {
40 use std::fs;
41 use tempdir::TempDir;
42
43 fn dependency_string(required_crates: &HashMap<String, Crate>) -> String {
44 let mut string = String::new();
45 for c in required_crates.values() {
46 match c.kind {
47 CrateKind::CratesIo => {
48 string.push_str(&format!(r#"{} = "{}"{}"#, c.name, c.version, '\n'));
49 }
50 }
51 }
52 string
53 }
54
55 fn create_dummy_crate(required_crates: &HashMap<String, Crate>) -> Result<TempDir, String> {
56 let tmpdir = TempDir::new("cargo-update-installed")
57 .map_err(|e| format!("I/O Error while creating temporary directory: {}", e))?;
58 let cargo_toml_path = tmpdir.path().join("Cargo.toml");
59 let src_dir_path = tmpdir.path().join("src");
60 let lib_rs_path = src_dir_path.join("lib.rs");
61
62 let cargo_toml_content = format!(
63 r#"[package]
64name = "cargo-update-installed-dummy"
65version = "0.1.0"
66authors = [""]
67
68[dependencies]
69{}
70"#,
71 dependency_string(required_crates)
72 );
73
74 fs::create_dir(src_dir_path)
75 .map_err(|e| format!("I/O Error while creating src dir in temp dir: {}", e))?;
76 fs::write(cargo_toml_path, cargo_toml_content)
77 .map_err(|e| format!("I/O Error while writing dummy Cargo.toml: {}", e))?;
78 fs::write(lib_rs_path, "")
79 .map_err(|e| format!("I/O Error while writing dummy lib.rs: {}", e))?;
80 Ok(tmpdir)
81 }
82
83 fn run_cargo_update(tmpdir: &TempDir) -> Result<ExitStatus, String> {
84 let mut cargo_update_command = Command::new("cargo");
85 cargo_update_command.arg("update");
86 cargo_update_command.arg("--manifest-path");
87 cargo_update_command.arg(tmpdir.path().join("Cargo.toml"));
88 cargo_update_command
89 .status()
90 .map_err(|e| format!("I/O Error while running `cargo update`: {}", e))
91 }
92
93 fn parse_cargo_lock(
94 tmpdir: &TempDir,
95 required_crates: &HashMap<String, Crate>,
96 ) -> Result<HashMap<String, String>, String> {
97 use std::fs;
98 use toml::Value;
99
100 let cargo_lock_path = tmpdir.path().join("Cargo.lock");
101 let cargo_lock = fs::read_to_string(cargo_lock_path)
102 .map_err(|e| format!("I/O Error while reading dummy Cargo.lock: {}", e))?;
103
104 let root_value: Value = cargo_lock
105 .parse()
106 .map_err(|e| format!("Error while parsing dummy Cargo.lock: {}", e))?;
107 let packages = root_value
108 .get("package")
109 .and_then(|v| v.as_array())
110 .ok_or("Error: package array not found in dummy Cargo.lock")?;
111
112 let mut latest_versions = HashMap::new();
113 for crate_name in required_crates.keys() {
114 let package = packages
115 .iter()
116 .find(|p| p.get("name").and_then(|v| v.as_str()) == Some(crate_name))
117 .ok_or(format!(
118 "Error: package {} not found in dummy Cargo.lock",
119 crate_name
120 ))?;
121 let version = package
122 .get("version")
123 .and_then(|v| v.as_str())
124 .ok_or(format!(
125 "Error: package {} has no version number in dummy Cargo.lock",
126 crate_name
127 ))?;
128 if latest_versions
129 .insert(crate_name.clone(), String::from(version))
130 .is_some()
131 {
132 writeln!(stderr(), "Warning: package {} is present multiple times in dummy Cargo.lock. Choosing version {}.", crate_name, version).expect("failed to write to stderr");
133 }
134 }
135 Ok(latest_versions)
136 }
137
138 let tmpdir = create_dummy_crate(required_crates)?;
139 if !run_cargo_update(&tmpdir)?.success() {
140 return Err("Error: `cargo update` failed".into());
141 }
142 parse_cargo_lock(&tmpdir, required_crates)
143}
144
145pub fn install_update(name: &str, version: &str) -> Result<ExitStatus, String> {
146 let mut cargo_install_command = Command::new("cargo");
147 cargo_install_command.arg("install");
148 cargo_install_command.arg("--force");
149 cargo_install_command.arg(name);
150 cargo_install_command.arg("--version");
151 cargo_install_command.arg(version);
152 cargo_install_command
153 .status()
154 .map_err(|e| format!("I/O Error while running `cargo install`: {}", e))
155}
156
157#[derive(Debug, Clone, PartialEq, Eq)]
158pub struct Crate {
159 pub name: String,
160 pub version: String,
161 pub kind: CrateKind,
162}
163
164#[derive(Debug, Clone, PartialEq, Eq)]
165pub enum CrateKind {
166 CratesIo,
167 }
175
176impl Crate {
177 pub fn parse_list_output(line: &str) -> Result<Option<Crate>, error::ParseListOutputError> {
179 use error::ParseListOutputError;
180
181 if line.starts_with(" ") {
182 return Ok(None);
183 }
184
185 let mut parts = line.split(" ");
186 let name = parts.next().ok_or(ParseListOutputError)?;
187
188 let version = parts.next().ok_or(ParseListOutputError)?;
189 if !version.starts_with("v") {
190 return Err(ParseListOutputError);
191 }
192 let version = version.trim_start_matches("v");
193
194 if version.ends_with(":") {
195 let version = version.trim_end_matches(":");
197 Ok(Some(Crate {
198 name: name.into(),
199 version: version.into(),
200 kind: CrateKind::CratesIo,
201 }))
202 } else {
203 let dependency_path = parts.next().ok_or(ParseListOutputError)?;
204 if !dependency_path.starts_with("(") || !dependency_path.ends_with("):") {
205 return Err(ParseListOutputError);
206 }
207 let dependency_path = dependency_path
208 .trim_start_matches("(")
209 .trim_end_matches("):");
210
211 if dependency_path.starts_with("http") {
212 writeln!(
214 stderr(),
215 "Warning: Git binaries are not supported. Ignoring `{}`.",
216 name
217 )
218 .expect("failed to write to stderr");
219 Ok(None)
220 } else {
221 writeln!(
223 stderr(),
224 "Warning: Local binaries are not supported. Ignoring `{}`.",
225 name
226 )
227 .expect("failed to write to stderr");
228 Ok(None)
229 }
230 }
231 }
232}
233
234pub mod error {
235 #[derive(Debug)]
236 pub struct ParseListOutputError;
237}