upm_lib/
lib.rs

1//! The universal package manager library (upm-lib) provides an abstraction to perform simple
2//! commands with any package manager. Currently there are no frontends implemented, but the
3//! functionality is exposed for frontends to utilize. Feel free to implement a frontend!
4//!
5//! If you want to do something with a particular package manager then this probably isn't the
6//! library for you. If you want to query multiple package managers at once to search for a package
7//! provided by multiple sources, then this is the library for you. This is common for language
8//! specific binaries that are provided by language package managers and system package managers.
9//!
10//! Since certain package managers such as NPM allow installation in a user's home directory or
11//! somewhere accessible for all users, there is a distinction between installation and removal of
12//! packages on a system-wide level and a local level.
13//!
14//! It is expected that the frontend would load in the different package managers from
15//! configuration files as discussed in [`PackageManager`](struct.PackageManager.html).
16//!
17//! Versioning is provided by the [Version] struct. [Version] is used in place of
18//! [semver](https://crates.io/crates/semver) due to the need to support non-semantic versions.
19//!
20//! [Version]: struct.Version.html
21
22#[macro_use] extern crate failure;
23extern crate regex;
24extern crate toml;
25
26use std::process::{Command,Child};
27use std::collections::HashSet;
28use std::hash::{Hash, Hasher};
29use std::fs::{File,read_dir};
30use std::io::prelude::*;
31use std::cmp::Ordering;
32use std::path::{PathBuf, Path};
33use failure::Error;
34use regex::Regex;
35use toml::Value;
36
37/// The representation of a package manager. Includes the name of the package manager, a path to
38/// reference scripts from, and commands in string form (or scripts to call package manager
39/// commands and properly format the output).
40#[derive(Eq,Clone,Default)]
41pub struct PackageManager {
42    pub name: String,
43    pub version: String,
44    pub config_dir: PathBuf,
45    pub install: Option<String>,
46    pub install_local: Option<String>,
47    pub remove: Option<String>,
48    pub remove_local: Option<String>,
49    pub search: Option<String>,
50}
51
52impl PackageManager {
53    //Concats a config_dir with a command that starts with ./ otherwise it returns the command str
54    fn fix_relative_path(config_dir: &PathBuf, command: &str) -> String {
55        if command.starts_with("./") {
56                let mut tmp = config_dir.as_os_str().to_str().unwrap().to_owned();
57                tmp.push_str(command);
58                tmp
59        } else {
60            command.to_owned()
61        }
62    }
63
64    /// Check if the PackageManager is installed by seeing if the version command exits with a
65    /// status code of 0.
66    pub fn exists(&self) -> bool {
67        let mut version_command = self.make_command("version").unwrap();
68        let status = version_command.status().expect("Failed to run version command");
69        status.success()
70    }
71
72    /// Check if the specified command field of the struct is some
73    pub fn has_command(&self, name: &str) -> bool {
74        match name {
75            "version" => true,
76            "install" => self.install.is_some(),
77            "install_local" => self.install_local.is_some(),
78            "remove" => self.remove.is_some(),
79            "remove_local" => self.remove_local.is_some(),
80            &_ => false,
81        }
82    }
83
84    /// Attempt to run the PackageManager command specified by name. Arguments can be supplied with
85    /// the args parameter.
86    pub fn run_command(&self, name: &str, args: &str) -> Result<Child,Error> {
87        let mut command = self.make_command(name).unwrap();
88        command.args(args.split_whitespace());
89        match command.spawn() {
90            Ok(child) => Ok(child),
91            Err(_) => bail!("Couldn't execute command")
92        }
93    }
94
95    /// Turns the String that describes a command into a std::process::Command struct.
96    /// # Panics
97    /// Panics if the name provided isn't one of the commands in the PackageManager struct
98    fn make_command(&self, name: &str) -> Option<Command> {
99        let tmp: Option<&String> = match name {
100            "version" => Some(&self.version),
101            "install" => self.install.as_ref(),
102            "install_local" => self.install_local.as_ref(),
103            "remove" => self.remove.as_ref(),
104            "remove_local" => self.remove_local.as_ref(),
105            _ => panic!("No such command"),
106        };
107        match tmp {
108            Some(s) => {
109                let s = PackageManager::fix_relative_path(&self.config_dir, s);
110                let mut s = s.split_whitespace();
111                let mut result = Command::new(s.nth(0).unwrap());
112                let args: Vec<&str> = s.collect();
113                result.args(args);
114                Some(result)
115            },
116            None => None,
117        }
118    }
119
120    /// Run the install command with the provided arguments
121    pub fn install(&self, args: &str) -> Result<Child,Error> {
122        self.run_command("install", args)
123    }
124
125    /// Run the uninstall command with the provided arguments
126    pub fn uninstall(&self, args: &str) -> Result<Child,Error> {
127        self.run_command("uninstall", args)
128    }
129
130    /// Run the search command with the provided arguments
131    pub fn search(&self, args: &str) -> Result<Child,Error> {
132        self.run_command("search", args)
133    }
134
135    /// Get the name of the package manager
136    pub fn get_name(&self) -> String {
137        self.name.to_owned()
138    }
139
140    /// Get the directory of the configuration file that describes the PackageManager
141    pub fn get_config_dir(self) -> PathBuf {
142        self.config_dir
143    }
144
145    /// Run the version command
146    pub fn version(self) -> Result<Child,Error> {
147        self.run_command("version", "")
148    }
149
150    /// Get the Version of the package manager
151    pub fn get_version(self) -> Result<Version,Error> {
152        let mut command = self.make_command("version").unwrap();
153        let output = command.output()?;
154        let version_string = String::from_utf8(output.stdout)?;
155        Ok(Version::from_str(&version_string))
156    }
157
158    /// Read a toml configuration file with a PackageManager description and create a
159    /// PackageManager from this info.
160    pub fn from_file<P: AsRef<Path>>(path: P) -> Result<PackageManager,Error> {
161        let mut file = File::open(&path)?;
162
163        let mut content = String::new();
164
165        file.read_to_string(&mut content)?;
166
167        let resource = content.as_str().parse::<Value>()?;
168
169        let name: String = String::from(path.as_ref().file_stem().unwrap().to_str().unwrap());
170
171        let version: String = match resource.get("version") {
172            Some(s) => s.as_str().unwrap().to_owned(),
173            None => bail!("Package manager version command not provided in config")
174        };
175
176        let install: Option<String> = match resource.get("install") {
177            Some(s) => Some(String::from(s.as_str().unwrap())),
178            None => None
179        };
180        let install_local: Option<String> = match resource.get("install_local") {
181            Some(s) => Some(String::from(s.as_str().unwrap())),
182            None => None
183        };
184        let remove: Option<String> = match resource.get("remove") {
185            Some(s) => Some(String::from(s.as_str().unwrap())),
186            None => None
187        };
188        let remove_local: Option<String> = match resource.get("remove_local") {
189            Some(s) => Some(String::from(s.as_str().unwrap())),
190            None => None
191        };
192        let search: Option<String> = match resource.get("search") {
193            Some(s) => Some(String::from(s.as_str().unwrap())),
194            None => None
195        };
196
197       let config_dir: PathBuf = match path.as_ref().parent() {
198           Some(dir) => dir.to_path_buf(),
199           None => PathBuf::new()
200       };
201
202        Ok(PackageManager {
203            name,
204            version,
205            config_dir,
206            install,
207            install_local,
208            remove,
209            remove_local,
210            search,
211        })
212    }
213}
214
215impl PartialEq for PackageManager {
216    fn eq(&self, other: &PackageManager) -> bool {
217        self.name == other.name
218    }
219}
220
221impl Ord for PackageManager {
222    fn cmp(&self, other: &PackageManager) -> Ordering {
223        self.name.cmp(&other.name)
224    }
225}
226
227impl PartialOrd for PackageManager {
228    fn partial_cmp(&self, other: &PackageManager) -> Option<Ordering> {
229        Some(self.cmp(other))
230    }
231}
232
233impl Hash for PackageManager {
234    fn hash<H: Hasher>(&self, state: &mut H) {
235        self.name.hash(state);
236    }
237}
238
239/// Information on a package from a particular package manager
240#[derive(Default)]
241pub struct Package {
242    pub name: String,
243    pub owner: PackageManager,
244    pub version: Version,
245    pub description: String,
246}
247
248impl Package {
249    /// Return whether the package has the specified name
250    pub fn is_called(&self, name: &str) -> bool {
251        self.name == name
252    }
253
254    /// Call install from the PackageManager pointed to by owner.
255    pub fn install(self) -> Result<Child,Error> {
256        self.owner.install(&self.name)
257    }
258
259    /// Call uninstall from the PackageManager pointed to by owner.
260    pub fn uninstall(self) -> Result<Child,Error> {
261        self.owner.uninstall(&self.name)
262    }
263
264    /// Return the package name
265    pub fn get_name(&self) -> String {
266        (&self.name).to_owned()
267    }
268
269    /// Return the package version
270    pub fn get_version(self) -> Version {
271        self.version
272    }
273
274    /// Return the description of the package
275    pub fn get_description(self) -> String {
276        self.description
277    }
278
279    /// Return the PackageManager that owns this
280    /// package
281    pub fn get_manager(self) -> PackageManager {
282        self.owner
283    }
284}
285
286/// A simple representation of a version string. For semantic versioning Steve Klabnik's semver
287/// crate is preferable. But non-semantic versioning is also permitted in this struct.
288#[derive(Debug,Default)]
289pub struct Version {
290    representation: String,
291    semantic: bool
292}
293
294impl Version {
295    /// Create a version from a string. Checks if the version fits with semantic versioning 2.0.0
296    /// and sets semantic to true if it does.
297    fn from_str(representation: &str) -> Version {
298        let semantic = Version::is_semantic(representation);
299        Version {
300            representation: String::from(representation),
301            semantic,
302        }
303    }
304
305    /// Get the string representation of the version
306    pub fn get_representation(self) -> String {
307        self.representation
308    }
309
310    /// Change the version along with checking if this new version appears to be semantic
311    pub fn set_representation(&mut self, val: String) {
312        self.representation = val;
313        self.semantic = Version::is_semantic(&self.representation);
314    }
315
316    /// Check if a representation appears to be semantic versioning
317    pub fn is_semantic(representation: &str) -> bool {
318        let re = Version::get_semantic_regex();
319        re.is_match(representation)
320    }
321
322    fn get_semantic_regex() -> Regex {
323        Regex::new(r"^(\d+)\.(\d+)\.(\d+)(?:-([\dA-Za-z-]+(?:\.[\dA-Za-z-]+)*))?(?:\+([\dA-Za-z-]+(?:\.[\dA-Za-z-]+)*))?$").unwrap()
324    }
325
326    /// Explicitly set whether the version is semantic. If the version string doesn't pass
327    /// is_semantic, then it won't set semantic to true and will return false.
328    pub fn set_semantic(&mut self, val: bool) -> Result<(),Error> {
329        if val && !Version::is_semantic(&self.representation) {
330            bail!("Version does not match semantic structure");
331        }
332        self.semantic = val;
333        Ok(())
334    }
335
336    /// Is this a semantic version?
337    pub fn get_semantic(self) -> bool {
338        self.semantic
339    }
340    
341}
342
343impl PartialEq for Version {
344    fn eq(&self, other: &Version) -> bool {
345        if self.semantic != other.semantic {
346            false
347        }
348        else if self.semantic && other.semantic {
349            let re = Version::get_semantic_regex();
350            let self_groups = re.captures(&self.representation).unwrap();
351            let other_groups = re.captures(&other.representation).unwrap();
352            self_groups.get(1)==other_groups.get(1) && self_groups.get(2)==
353                other_groups.get(2) && self_groups.get(3) == other_groups.get(3)
354        } else {
355            self.representation == other.representation
356        }
357    }
358}
359//TODO implement ordering for Versions
360
361//TODO Give info on what files couldn't be read
362/// Get a vector of any package managers specified in the given directory.
363pub fn get_managers<P: AsRef<Path>>(directory: P, names: &ManagerSpecifier) -> Result<Vec<PackageManager>, Error> {
364    let mut result = Vec::new();
365    if let Ok(entries) = read_dir(directory) {
366        for entry in entries {
367            if let Ok(entry) = entry {
368                let path = entry.path();
369                let name = entry.file_name();
370                if name.to_str().unwrap().ends_with(".toml") {
371                    if let Some(stem) = path.file_stem() {
372                        //Skip if the name shouldn't be collected
373                        match *names {
374                            ManagerSpecifier::Excludes(ref set) => {
375                                if set.contains(stem.to_str().unwrap()) {
376                                    continue;
377                                }
378                            },
379                            ManagerSpecifier::Includes(ref set) => {
380                                if !set.contains(stem.to_str().unwrap()) {
381                                    continue;
382                                }
383                            },
384                            _ => {}
385                        };
386                        //Add the package manager to the result
387                        let manager = PackageManager::from_file(&path);
388                        match manager {
389                            Ok(man) => result.push(man),
390                            Err(_e) => {}
391                        }
392                    }
393                }
394            }
395        }
396    }
397    Ok(result)
398}
399
400/// Provide a single type to exclude or solely include certain packagemanager names.
401pub enum ManagerSpecifier {
402    Excludes(HashSet<&'static str>),
403    Includes(HashSet<&'static str>),
404    Empty,
405}
406
407//TODO: provide info on what directories and files weren't read. This should probably be a new
408//struct for 1.0.0
409/// Read the configuration directories listed from highest precedence to lowest with the option to
410/// explicitly exclude or include certain package managers. If the include variant of
411/// `ManagerSpecifier` is used then only the specified packagemanager names will be returned if they
412/// exist.
413/// # Panics
414/// If one of the directories can't be read. This should be changed soon to avoid panicking and
415/// instead give feedback on what directories and files were and were not read.
416pub fn read_config_dirs<P: AsRef<Path>>(directories: Vec<P>, exceptions: &ManagerSpecifier) -> Vec<PackageManager> {
417    let mut result: HashSet<PackageManager> = HashSet::new();
418    for dir in directories {
419        let tmp = get_managers(dir, exceptions);
420        let tmp = match tmp {
421            Ok(s) => s,
422            Err(_e) => panic!("Couldn't get managers from directory"),
423        };
424        for manager in tmp {
425            if !result.contains(&manager) {
426                result.insert(manager);
427            }
428        }
429    }
430//    let global_dir = PathBuf::from(global_conf_dir());
431//    let secondary_dir = PathBuf::from(secondary_conf_dir());
432    let return_value: Vec<PackageManager> = result.into_iter().collect();
433    return_value
434}
435
436#[cfg(test)]
437mod tests {
438    use super::*;
439    #[test]
440    fn semantic_matching() {
441        let mut semantics: Vec<&str> = Vec::new();
442        semantics.push("0.1.1");
443        semantics.push("0.1.1-prerelease");
444        semantics.push("0.1.1-prerelease.x.3");
445        semantics.push("0.1.1-pre-pre-release");
446        semantics.push("0.1.1+builddata");
447        semantics.push("0.1.1+build-data");
448        semantics.push("0.1.1+builddata.3");
449        semantics.push("0.1.1-prerelease+builddata");
450        let mut jejune: Vec<&str> = Vec::new();
451        jejune.push("a.b.c");
452        jejune.push("1-1-1");
453        jejune.push("0.1.1-b@d");
454        jejune.push("0.1.1+b@d");
455        for string in &semantics {
456            assert!(Version::is_semantic(string), "{} was detected as not semantic", string);
457        }
458        for string in &jejune {
459            assert!(!Version::is_semantic(string), "{} was detected as semantic", string);
460        }
461    }
462
463    #[test]
464    fn creation_test() {
465        let blank_version = Version::new();
466        assert_eq!(blank_version.representation, String::new());
467        assert!(!blank_version.semantic);
468        let semantic_string = "0.1.2";
469        let non_semantic_string = "1.4rc2";
470        let semantic_version = Version::from_str(semantic_string);
471        assert!(semantic_version.get_semantic());
472        let non_semantic_version = Version::from_str(non_semantic_string);
473        assert!(!non_semantic_version.get_semantic());
474    }
475
476    #[test]
477    fn equality_test() {
478        let version1 = Version::from_str("0.1.2");
479        let version2 = Version::from_str("1.4rc2");
480        let mut version3 = Version::from_str("0.1.2");
481        assert_eq!(version1,version3);
482        assert_ne!(version1,version2);
483        let res = version3.set_semantic(false);
484        assert!(!res.is_err());
485        assert_ne!(version1,version3);
486    }
487
488    #[test]
489    fn read_toml() {
490        let path = PathBuf::from("./test-files");
491        let path_vec = vec!(&path);
492        let managers = read_config_dirs(path_vec, ManagerSpecifier::Empty);
493
494        let mut expected_managers = HashSet::new();
495        expected_managers.insert(PackageManager {
496            name: String::from("pacman"),
497            version: String::from("./pacman/version.sh"),
498            config_dir: PathBuf::from("./test-files"),
499            install: Some(String::from("pacman -S")),
500            install_local: None,
501            remove: Some(String::from("pacman -Rs")),
502            remove_local: None,
503            search: Some(String::from("pacman -Ss")),
504        });
505        for man in managers {
506            assert!(expected_managers.contains(&man));
507        }
508    }
509
510    #[test]
511    fn cargo_exists() {
512        let cargo = PackageManager {
513            name: String::from("cargo"),
514            version: String::from("./cargo/version.sh"),
515            config_dir: PathBuf::from("./test-files/"),
516            install: None,
517            install_local: Some(String::from("cargo install")),
518            remove: None,
519            remove_local: Some(String::from("cargo uninstall")),
520            search: Some(String::from("cargo search")),
521        };
522        assert!(cargo.exists(), "cargo apparently isn't installed here?");
523    }
524
525    #[test]
526    fn commands_fail_gracefully() {
527        let fake_manager = PackageManager {
528            name: String::from("fake"),
529            version: String::from("./fake/version.sh"), //this file is not executable
530            config_dir: PathBuf::from("./test-files/"),
531            install: Some(String::from("./fake/beelzebub")), //this is a directory
532            install_local: Some(String::from("./fake/baphomet")), //this file doesn't exist
533            remove: None,
534            remove_local: None,
535            search: None,
536        };
537        assert!(&fake_manager.run_command("version", "").is_err());
538        assert!(&fake_manager.run_command("install", "").is_err());
539        assert!(&fake_manager.run_command("install_local", "").is_err());
540    }
541}