cargo_install_latest/
lib.rs

1use 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                // only consider latest version
24                // (It is possible to have two different versions of the same crate installed,
25                // for example when an old version contained an executable that is no longer
26                // present in the newer version.)
27                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    /*
168        Git {
169            url: String,
170            branch: Option<String>,
171        },
172        Local,
173    */
174}
175
176impl Crate {
177    /// Parses a line from `cargo install --list`.
178    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            // crates.io dependency
196            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                // git dependency
213                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                // local dependency
222                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}