]> git.lizzy.rs Git - rust.git/commitdiff
rustpkg: Finish all commands and declarative logic
authorZack Corr <zack@z0w0.me>
Sat, 19 Jan 2013 09:59:19 +0000 (19:59 +1000)
committerGraydon Hoare <graydon@mozilla.com>
Sat, 16 Feb 2013 02:04:10 +0000 (18:04 -0800)
src/librustpkg/rustpkg.rc
src/librustpkg/usage.rs
src/librustpkg/util.rs

index f95eabd584b4d6846bb890fe0a1e073f9e5c0bd9..aec3f21eb6f332e4417bb0cd09f532e54488aa68 100644 (file)
@@ -29,6 +29,7 @@ use core::*;
 use io::{ReaderUtil, WriterUtil};
 use std::getopts;
 use std::net::url;
+use send_map::linear::LinearMap;
 use rustc::driver::{driver, session};
 use rustc::metadata::{filesearch};
 use syntax::{ast, attr, codemap, diagnostic, parse, visit};
@@ -38,6 +39,8 @@ mod api;
 mod usage;
 mod util;
 
+use util::Package;
+
 struct PackageScript {
     id: ~str,
     name: ~str,
@@ -167,23 +170,26 @@ impl PackageScript {
         }
 
         let id = id.get();
-        let vers = vers.get();
+        let name = match util::parse_name(id) {
+            result::Ok(name) => name,
+            result::Err(err) => return result::Err(err)
+        };
+        let vers = match util::parse_vers(vers.get()) {
+            result::Ok(vers) => vers,
+            result::Err(err) => return result::Err(err)
+        };
 
         result::Ok(PackageScript {
             id: id,
-            name: util::parse_id(id),
-            vers: util::parse_vers(vers),
+            name: name,
+            vers: vers,
             crates: crates,
             deps: deps
         })
     }
 
     fn hash() -> ~str {
-        let hasher = hash::default_state();
-
-        hasher.write_str(self.id + self.vers.to_str());
-
-        fmt!("%s-%s-%s", self.name, hasher.result_str(), self.vers.to_str())
+        fmt!("%s-%s-%s", self.name, util::hash(self.id + self.vers.to_str()), self.vers.to_str())
     }
 
     fn work_dir() -> Path {
@@ -192,8 +198,8 @@ impl PackageScript {
 }
 
 struct Ctx {
-    cfgs: ~[~str],
-    prefer: bool
+    cfg: ast::crate_cfg,
+    mut dep_cache: LinearMap<~str, bool>
 }
 
 impl Ctx {
@@ -213,8 +219,10 @@ impl Ctx {
 
             if parts.len() >= 1 {
                 name = Some(parts[0]);
-            } else if parts.len() >= 2 {
-                vers = Some(parts[1]);
+
+                if parts.len() >= 2 {
+                    vers = Some(parts[1]);
+                }
             }
 
             (name, vers)
@@ -301,7 +309,8 @@ impl Ctx {
         }
 
         for script.crates.each |&crate| {
-            success = self.compile(&work_dir, &dir.push_rel(&Path(crate)), ~[]);
+            success = self.compile(&work_dir, &dir.push_rel(&Path(crate)), ~[],
+                                   false, false);
 
             if !success { break; }
         }
@@ -318,15 +327,20 @@ impl Ctx {
         true
     }
 
-    fn compile(dir: &Path, crate: &Path, flags: ~[~str]) -> bool {
+    fn compile(dir: &Path, crate: &Path, flags: ~[~str],
+               opt: bool, test: bool) -> bool {
         util::note(~"compiling " + crate.to_str());
 
         let lib_dir = dir.push(~"lib");
         let bin_dir = dir.push(~"bin");
+        let test_dir = dir.push(~"test");
         let binary = os::args()[0];
         let options: @session::options = @{
             binary: binary,
+            addl_lib_search_paths: ~[util::root().push(~"lib")],
             crate_type: session::unknown_crate,
+            optimize: if opt { session::Aggressive } else { session::No },
+            test: test,
             .. *session::basic_options()
         };
         let input = driver::file_input(*crate);
@@ -421,19 +435,33 @@ impl Ctx {
             }
         };
 
-        if is_bin {
-            let hasher = hash::default_state();
+        if test {
+            util::need_dir(&test_dir);
 
+            outputs = driver::build_output_filenames(input, &Some(test_dir),
+                                                     &None, sess)
+        }
+        else if is_bin {
             util::need_dir(&bin_dir);
-            hasher.write_str(name + uuid + vers);
 
-            let path = bin_dir.push(fmt!("%s-%s-%s", name, hasher.result_str(), vers));
+            #[cfg(windows)]
+            fn suffix() -> ~str { ~".exe" }
+
+            #[cfg(target_os = "linux")]
+            #[cfg(target_os = "android")]
+            #[cfg(target_os = "freebsd")]
+            #[cfg(target_os = "macos")]
+            fn suffix() -> ~str { ~"" }
+
+            let path = bin_dir.push(fmt!("%s-%s-%s%s", name,
+                                                       util::hash(name + uuid + vers),
+                                                       vers, suffix()));
             outputs = driver::build_output_filenames(input, &None, &Some(path), sess);
         } else {
             util::need_dir(&lib_dir);
 
-            outputs = driver::build_output_filenames(input, &Some(lib_dir), &None,
-                                                     sess)
+            outputs = driver::build_output_filenames(input, &Some(lib_dir),
+                                                     &None, sess)
         }
 
         driver::compile_upto(sess, cfg, input, driver::cu_everything,
@@ -456,19 +484,14 @@ impl Ctx {
         util::note(fmt!("cleaning %s v%s (%s)", script.name, script.vers.to_str(),
                                                 script.id));
 
-        if os::path_is_dir(&dir) {
-            if os::remove_dir(&dir) {
-                util::note(fmt!("cleaned %s v%s", script.name,
-                                                  script.vers.to_str()));
-            } else {
-                util::error(fmt!("cleaning %s v%s failed",
-                                 script.name, script.vers.to_str()));
-            }
-        } else {
-            util::note(fmt!("cleaned %s v%s", script.name,
-                                              script.vers.to_str()));
+        if os::path_exists(&dir) {
+            util::remove_dir_r(&dir);
+            util::note(fmt!("removed %s", dir.to_str()));
         }
 
+        util::note(fmt!("cleaned %s v%s", script.name,
+                                          script.vers.to_str()));
+
         true
     }
 
@@ -482,15 +505,17 @@ impl Ctx {
             dir = os::getcwd();
         } else {
             let url = url.get();
-            let hasher = hash::default_state();
+            let hash = util::hash(if !target.is_none() { url + target.get() } else { url });
 
-            hasher.write_str(url);
+            if self.dep_cache.contains_key(&hash) {
+                util::warn(~"already installed dep this run");
 
-            if !target.is_none() {
-                hasher.write_str(target.get());
+                return true;
             }
 
-            dir = util::root().push(~"tmp").push(hasher.result_str());
+            self.dep_cache.insert(hash, true);
+
+            dir = util::root().push(~"tmp").push(hash);
 
             if cache && os::path_exists(&dir) {
                 return true;
@@ -538,7 +563,8 @@ impl Ctx {
         }
 
         for script.crates.each |&crate| {
-            success = self.compile(&work_dir, &dir.push_rel(&Path(crate)), ~[]);
+            success = self.compile(&work_dir, &dir.push_rel(&Path(crate)), ~[],
+                                 true, false);
 
             if !success { break; }
         }
@@ -549,24 +575,37 @@ impl Ctx {
             return false;
         }
 
-        let from_bin_dir = dir.push(~"bin");
-        let from_lib_dir = dir.push(~"lib");
+        let from_bin_dir = work_dir.push(~"bin");
+        let from_lib_dir = work_dir.push(~"lib");
         let to_bin_dir = util::root().push(~"bin");
         let to_lib_dir = util::root().push(~"lib");
+        let mut bins = ~[];
+        let mut libs = ~[];
 
         for os::walk_dir(&from_bin_dir) |bin| {
             let to = to_bin_dir.push_rel(&bin.file_path());
 
             os::copy_file(bin, &to);
+            bins.push(to.to_str());
         }
+
         for os::walk_dir(&from_lib_dir) |lib| {
             let to = to_lib_dir.push_rel(&lib.file_path());
 
             os::copy_file(lib, &to);
+            libs.push(to.to_str());
         }
 
+        let package = Package {
+            id: script.id,
+            vers: script.vers,
+            bins: bins,
+            libs: libs
+        };
+
         util::note(fmt!("installed %s v%s", script.name,
                                             script.vers.to_str()));
+        util::add_pkg(&package);
 
         true
     }
@@ -626,21 +665,7 @@ impl Ctx {
         util::note(fmt!("fetching from %s using git", url));
 
         // Git can't clone into a non-empty directory
-        for os::walk_dir(dir) |&file| {
-            let mut cdir = file;
-
-            loop {
-                if os::path_is_dir(&cdir) {
-                    os::remove_dir(&cdir);
-                } else {
-                    os::remove_file(&cdir);
-                }
-
-                cdir = cdir.dir_path();
-
-                if cdir == *dir { break; }
-            }
-        }
+        util::remove_dir_r(dir);
 
         if run::program_output(~"git", ~[~"clone", url, dir.to_str()]).status != 0 {
             util::error(~"fetching failed: can't clone repository");
@@ -665,19 +690,190 @@ impl Ctx {
         true
     }
 
-    fn prefer(name: ~str, vers: Option<~str>) -> bool {
+    fn prefer(id: ~str, vers: Option<~str>) -> bool {
+        let package = match util::get_pkg(id, vers) {
+            result::Ok(package) => package,
+            result::Err(err) => {
+                util::error(err);
+
+                return false;
+            }
+        };
+        let name = match util::parse_name(package.id) {
+            result::Ok(name) => name,
+            result::Err(err) => {
+                util::error(err);
+
+                return false;
+            }
+        };
+
+        util::note(fmt!("preferring %s v%s (%s)", name, package.vers.to_str(),
+                                                  package.id));
+
+        let bin_dir = util::root().push(~"bin");
+
+        for package.bins.each |&bin| {
+            let path = Path(bin);
+            let name = str::split_char(path.file_path().to_str(), '-')[0];
+            let out = bin_dir.push(name);
+
+            util::link_exe(&path, &out);
+            util::note(fmt!("linked %s", out.to_str()));
+        }
+
+        util::note(fmt!("preferred %s v%s", name, package.vers.to_str()));
+
         true
     }
 
     fn test() -> bool {
+        let dir = os::getcwd();
+        let script = match PackageScript::parse(dir) {
+            result::Ok(script) => script,
+            result::Err(err) => {
+                util::error(err);
+
+                return false; 
+            }
+        };
+        let work_dir = script.work_dir();
+        let test_dir = work_dir.push(~"test");
+        let mut success = true;
+
+        util::need_dir(&work_dir);
+        util::note(fmt!("testing %s v%s (%s)", script.name, script.vers.to_str(),
+                                               script.id));
+
+        if script.deps.len() >= 1 {
+            util::note(~"installing dependencies");
+
+            for script.deps.each |&dep| {
+                let (url, target) = dep;
+
+                success = self.install(Some(url), target, true);
+
+                if !success { break; }
+            }
+
+
+            if !success {
+                util::error(fmt!("testing %s v%s failed: a dep wasn't installed",
+                                 script.name, script.vers.to_str()));
+
+                return false;
+            }
+
+            util::note(~"installed dependencies");
+        }
+
+        for script.crates.each |&crate| {
+            success = self.compile(&work_dir, &dir.push_rel(&Path(crate)), ~[],
+                                   false, true);
+
+            if !success { break; }
+        }
+
+        if !success {
+            util::error(fmt!("testing %s v%s failed: a crate failed to compile",
+                             script.name, script.vers.to_str()));
+
+            return false;
+        }
+
+        for os::walk_dir(&test_dir) |test| {
+            util::note(fmt!("running %s", test.to_str()));
+
+            let status = run::run_program(test.to_str(), ~[]);
+
+            if status != 0 {
+                os::set_exit_status(status);
+            }
+        }
+
+        util::note(fmt!("tested %s v%s", script.name, script.vers.to_str()));
+
         true
     }
 
-    fn uninstall(name: ~str, vers: Option<~str>) -> bool {
+    fn uninstall(id: ~str, vers: Option<~str>) -> bool {
+        let package = match util::get_pkg(id, vers) {
+            result::Ok(package) => package,
+            result::Err(err) => {
+                util::error(err);
+
+                return false;
+            }
+        };
+        let name = match util::parse_name(package.id) {
+            result::Ok(name) => name,
+            result::Err(err) => {
+                util::error(err);
+
+                return false;
+            }
+        };
+
+        util::note(fmt!("uninstalling %s v%s (%s)", name, package.vers.to_str(),
+                                                    package.id));
+
+        for vec::append(package.bins, package.libs).each |&file| {
+            let path = Path(file);
+
+            if os::path_exists(&path) {
+                if os::remove_file(&path) {
+                    util::note(fmt!("removed %s", path.to_str()));
+                } else {
+                    util::error(fmt!("could not remove %s", path.to_str()));
+                }
+            }
+        }
+
+        util::note(fmt!("uninstalled %s v%s", name, package.vers.to_str()));
+        util::remove_pkg(&package);
+
         true
     }
 
-    fn unprefer(name: ~str, vers: Option<~str>) -> bool {
+    fn unprefer(id: ~str, vers: Option<~str>) -> bool {
+        let package = match util::get_pkg(id, vers) {
+            result::Ok(package) => package,
+            result::Err(err) => {
+                util::error(err);
+
+                return false;
+            }
+        };
+        let name = match util::parse_name(package.id) {
+            result::Ok(name) => name,
+            result::Err(err) => {
+                util::error(err);
+
+                return false;
+            }
+        };
+
+        util::note(fmt!("unpreferring %s v%s (%s)", name, package.vers.to_str(),
+                                                    package.id));
+
+        let bin_dir = util::root().push(~"bin");
+
+        for package.bins.each |&bin| {
+            let path = Path(bin);
+            let name = str::split_char(path.file_path().to_str(), '-')[0];
+            let out = bin_dir.push(name);
+
+            if os::path_exists(&out) {
+                if os::remove_file(&out) {
+                    util::note(fmt!("unlinked %s", out.to_str()));
+                } else {
+                    util::error(fmt!("could not unlink %s", out.to_str()));
+                }
+            }
+        }
+
+        util::note(fmt!("unpreferred %s v%s", name, package.vers.to_str()));
+
         true
     }
 }
@@ -685,20 +881,19 @@ impl Ctx {
 pub fn main() {
     let args = os::args();
     let opts = ~[getopts::optflag(~"h"), getopts::optflag(~"help"),
-                 getopts::optmulti(~"c"), getopts::optmulti(~"cfg"),
-                 getopts::optmulti(~"p"), getopts::optmulti(~"prefer")];
+                 getopts::optmulti(~"c"), getopts::optmulti(~"cfg")];
     let matches = &match getopts::getopts(args, opts) {
         result::Ok(m) => m,
         result::Err(f) => {
-            fail fmt!("%s", getopts::fail_str(f));
+            util::error(fmt!("%s", getopts::fail_str(f)));
+
+            return;
         }
     };
     let help = getopts::opt_present(matches, ~"h") ||
                getopts::opt_present(matches, ~"help");
-    let cfgs = vec::append(getopts::opt_strs(matches, ~"cfg"),
+    let cfg = vec::append(getopts::opt_strs(matches, ~"cfg"),
                            getopts::opt_strs(matches, ~"c"));
-    let prefer = getopts::opt_present(matches, ~"p") ||
-                 getopts::opt_present(matches, ~"prefer");
     let mut args = copy matches.free;
 
     args.shift();
@@ -724,9 +919,15 @@ pub fn main() {
         };
     }
 
+    let mut cfg_specs = ~[];
+
+    for cfg.each |s| {
+        cfg_specs.push(attr::mk_word_item(/*bad*/copy *s));
+    }
+
     Ctx {
-        cfgs: cfgs,
-        prefer: prefer
+        cfg: cfg_specs,
+        mut dep_cache: LinearMap()
     }.run(cmd, args);
 }
 
index a46e918e3b7eb7f7098932c1b420452d9ed04aac..28694948511d6d9a4bc7cc2699e6cd0ad7d5717c 100644 (file)
@@ -1,5 +1,5 @@
 use core::io;
-
 pub fn general() {
     io::println(~"Usage: rustpkg [options] <cmd> [args..]
 
@@ -46,29 +46,22 @@ pub fn install() {
     rustpkg install http://rust-lang.org/servo-0.1.2.tar.gz
 
 Options:
-    -c, --cfg      Pass a cfg flag to the package script
-    -p, --prefer   Prefer the package after installing
-                   (see `rustpkg prefer -h`)");
+    -c, --cfg      Pass a cfg flag to the package script");
 }
 
 pub fn uninstall() {
-    io::println(~"rustpkg uninstall <name>[@version]
-
-Remove a package by name and/or version. If version is omitted then all
-versions of the package will be removed. If the package[s] is/are depended
-on by another package then they cannot be removed.  If the package is preferred
-(see `rustpkg prefer -h`), it will attempt to prefer the next latest
-version of the package if another version is installed, otherwise it'll remove
-the symlink.");
+    io::println(~"rustpkg uninstall <id|name>[@version]
+
+Remove a package by id or name and optionally version. If the package(s) is/are depended
+on by another package then they cannot be removed.");
 }
 
 pub fn prefer() {
-    io::println(~"rustpkg [options..] prefer <name>[@version]
+    io::println(~"rustpkg [options..] prefer <id|name>[@version]
 
 By default all binaries are given a unique name so that multiple versions can
 coexist. The prefer command will symlink the uniquely named binary to
-the binary directory under its bare name. The user will need to confirm
-if the symlink will overwrite another. If version is not supplied, the latest
+the binary directory under its bare name. If version is not supplied, the latest
 version of the package will be preferred.
 
 Example:
@@ -82,10 +75,12 @@ pub fn prefer() {
 }
 
 pub fn unprefer() {
-    io::println(~"rustpkg [options..] unprefer <name>
+    io::println(~"rustpkg [options..] unprefer <id|name>[@version]
 
 Remove all symlinks from the store to the binary directory for a package
-name. See `rustpkg prefer -h` for more information.");
+name and optionally version. If version is not supplied, the latest version
+of the package will be unpreferred. See `rustpkg prefer -h` for more
+information.");
 }
 
 pub fn test() {
index 67587ae93603666694ee9ad5ade8b93d465dd433..16ded3c640b6d1111e3d7e60bb5ec03054ac67f2 100644 (file)
@@ -1,7 +1,15 @@
 use core::*;
+use send_map::linear::LinearMap;
 use rustc::metadata::filesearch;
 use semver::Version;
-use std::term;
+use std::{json, term, sort};
+
+pub struct Package {
+    id: ~str,
+    vers: Version,
+    bins: ~[~str],
+    libs: ~[~str],
+}
 
 pub fn root() -> Path {
     match filesearch::get_rustpkg_root() {
@@ -17,26 +25,26 @@ pub fn is_cmd(cmd: ~str) -> bool {
     vec::contains(cmds, &cmd)
 }
 
-pub fn parse_id(id: ~str) -> ~str {
+pub fn parse_name(id: ~str) -> result::Result<~str, ~str> {
     let parts = str::split_char(id, '.');
 
     for parts.each |&part| {
         for str::chars(part).each |&char| {
             if char::is_whitespace(char) {
-                fail ~"could not parse id: contains whitespace";
+                return result::Err(~"could not parse id: contains whitespace");
             } else if char::is_uppercase(char) {
-                fail ~"could not parse id: should be all lowercase";
+                return result::Err(~"could not parse id: should be all lowercase");
             }
         }
     }
 
-    parts.last()
+    result::Ok(parts.last())
 }
 
-pub fn parse_vers(vers: ~str) -> Version {
+pub fn parse_vers(vers: ~str) -> result::Result<Version, ~str> {
     match semver::parse(vers) {
-        Some(vers) => vers,
-        None => fail ~"could not parse version: invalid"
+        Some(vers) => result::Ok(vers),
+        None => result::Err(~"could not parse version: invalid")
     }
 }
 
@@ -80,6 +88,13 @@ pub fn error(msg: ~str) {
     else { out.write_line(~"error: " + msg); }
 }
 
+pub fn hash(data: ~str) -> ~str {
+    let hasher = hash::default_state();
+
+    hasher.write_str(data);
+    hasher.result_str()
+}
+
 pub fn temp_change_dir<T>(dir: &Path, cb: fn() -> T) {
     let cwd = os::getcwd();
 
@@ -88,6 +103,342 @@ pub fn temp_change_dir<T>(dir: &Path, cb: fn() -> T) {
     os::change_dir(&cwd);
 }
 
+pub fn touch(path: &Path) {
+    match io::mk_file_writer(path, ~[io::Create]) {
+        result::Ok(writer) => writer.write_line(~""),
+        _ => {}
+    }
+}
+
+pub fn remove_dir_r(path: &Path) {
+    for os::walk_dir(path) |&file| {
+        let mut cdir = file;
+
+        loop {
+            if os::path_is_dir(&cdir) {
+                os::remove_dir(&cdir);
+            } else {
+                os::remove_file(&cdir);
+            }
+
+            cdir = cdir.dir_path();
+
+            if cdir == *path { break; }
+        }
+    }
+
+    os::remove_dir(path);
+}
+
+pub fn wait_for_lock(path: &Path) {
+    if os::path_exists(path) {
+        warn(fmt!("the database appears locked, please wait (or rm %s)",
+                        path.to_str()));
+
+        loop {
+            if !os::path_exists(path) { break; }
+        }
+    }
+}
+
+fn _add_pkg(packages: ~[json::Json], pkg: &Package) -> ~[json::Json] {
+    for packages.each |&package| {
+        match package {
+            json::Object(map) => {
+                let mut has_id = false;
+
+                match map.get(&~"id") {
+                    json::String(str) => {
+                        if pkg.id == str {
+                            has_id = true;
+                        }
+                    }
+                    _ => {}
+                }
+
+                match map.get(&~"vers") {
+                    json::String(str) => {
+                        if pkg.vers.to_str() == str {
+                            return packages;
+                        }
+                    }
+                    _ => {}
+                }
+            }
+            _ => {}
+        }
+    }
+
+    let mut map = ~LinearMap();
+
+    map.insert(~"id", json::String(pkg.id));
+    map.insert(~"vers", json::String(pkg.vers.to_str()));
+    map.insert(~"bins", json::List(do pkg.bins.map |&bin| {
+        json::String(bin)
+    }));
+    map.insert(~"libs", json::List(do pkg.libs.map |&lib| {
+        json::String(lib)
+    }));
+
+    vec::append(packages, ~[json::Object(map)])
+}
+
+fn _rm_pkg(packages: ~[json::Json], pkg: &Package) -> ~[json::Json] {
+    do packages.filter_map |&package| {
+        match package {
+            json::Object(map) => {
+                let mut has_id = false;
+
+                match map.get(&~"id") {
+                    json::String(str) => {
+                        if pkg.id == str {
+                            has_id = true;
+                        }
+                    }
+                    _ => {}
+                }
+
+                match map.get(&~"vers") {
+                    json::String(str) => {
+                        if pkg.vers.to_str() == str { None }
+                        else { Some(package) } 
+                    }
+                    _ => { Some(package) }
+                }
+            }
+            _ => { Some(package) }
+        }
+    }
+}
+
+pub fn load_pkgs() -> result::Result<~[json::Json], ~str> {
+    let root = root();
+    let db = root.push(~"db.json");
+    let db_lock = root.push(~"db.json.lck");
+
+    wait_for_lock(&db_lock);
+    touch(&db_lock);
+
+    let packages = if os::path_exists(&db) {
+        match io::read_whole_file_str(&db) {
+            result::Ok(str) => {
+                match json::from_str(str) {
+                    result::Ok(json) => {
+                        match json {
+                            json::List(list) => list,
+                            _ => {
+                                os::remove_file(&db_lock);
+
+                                return result::Err(~"package db's json is not a list");
+                            }
+                        }
+                    }
+                    result::Err(err) => {
+                        os::remove_file(&db_lock);
+
+                        return result::Err(fmt!("failed to parse package db: %s", err.to_str()));
+                    }
+                }
+            }
+            result::Err(err) => {
+                os::remove_file(&db_lock);
+
+                return result::Err(fmt!("failed to read package db: %s", err));
+            }
+        }
+    } else { ~[] };
+
+    os::remove_file(&db_lock);
+
+    result::Ok(packages)
+}
+
+pub fn get_pkg(id: ~str, vers: Option<~str>) -> result::Result<Package, ~str> {
+    let name = match parse_name(id) {
+        result::Ok(name) => name,
+        result::Err(err) => return result::Err(err)
+    };
+    let packages = match load_pkgs() {
+        result::Ok(packages) => packages,
+        result::Err(err) => return result::Err(err)
+    };
+    let mut sel = None;
+    let mut possibs = ~[];
+    let mut err = None;
+
+    for packages.each |&package| {
+        match package {
+            json::Object(map) => {
+                let pid = match map.get(&~"id") {
+                    json::String(str) => str,
+                    _ => loop
+                };
+                let pname = match parse_name(pid) {
+                    result::Ok(pname) => pname,
+                    result::Err(perr) => {
+                        err = Some(perr);
+
+                        break;
+                    }
+                };
+                let pvers = match map.get(&~"vers") {
+                    json::String(str) => str,
+                    _ => loop
+                };
+                if pid == id || pname == name {
+                    let bins = match map.get(&~"bins") {
+                        json::List(list) => {
+                            do list.map |&bin| {
+                                match bin {
+                                    json::String(str) => str,
+                                    _ => ~""
+                                }
+                            }
+                        }
+                        _ => ~[]
+                    };
+                    let libs = match map.get(&~"libs") {
+                        json::List(list) => {
+                            do list.map |&lib| {
+                                match lib {
+                                    json::String(str) => str,
+                                    _ => ~""
+                                }
+                            }
+                        }
+                        _ => ~[]
+                    };
+                    let package = Package {
+                        id: pid,
+                        vers: match parse_vers(pvers) {
+                            result::Ok(vers) => vers,
+                            result::Err(verr) => {
+                                err = Some(verr);
+
+                                break;
+                            }
+                        },
+                        bins: bins,
+                        libs: libs
+                    };
+
+                    if !vers.is_none() && vers.get() == pvers {
+                        sel = Some(package);
+                    }
+                    else {
+                        possibs.push(package);
+                    }
+                }
+            }
+            _ => {}
+        }
+    }
+
+    if !err.is_none() {
+        return result::Err(err.get());
+    }
+    if !sel.is_none() {
+        return result::Ok(sel.get());
+    }
+    if !vers.is_none() || possibs.len() < 1 {
+        return result::Err(~"package not found");
+    }
+
+    result::Ok(sort::merge_sort(possibs, |v1, v2| {
+        v1.vers <= v2.vers
+    }).last())
+}
+
+pub fn add_pkg(pkg: &Package) -> bool {
+    let root = root();
+    let db = root.push(~"db.json");
+    let db_lock = root.push(~"db.json.lck");
+    let packages = match load_pkgs() {
+        result::Ok(packages) => packages,
+        result::Err(err) => {
+            error(err);
+
+            return false;
+        }
+    };
+
+    wait_for_lock(&db_lock);
+    touch(&db_lock);
+    os::remove_file(&db);
+
+    match io::mk_file_writer(&db, ~[io::Create]) {
+        result::Ok(writer) => {
+            writer.write_line(json::to_pretty_str(&json::List(_add_pkg(packages, pkg))));
+        }
+        result::Err(err) => {
+            error(fmt!("failed to dump package db: %s", err));
+            os::remove_file(&db_lock);
+
+            return false;
+        }
+    }
+
+    os::remove_file(&db_lock);
+
+    true
+}
+
+pub fn remove_pkg(pkg: &Package) -> bool {
+    let root = root();
+    let db = root.push(~"db.json");
+    let db_lock = root.push(~"db.json.lck");
+    let packages = match load_pkgs() {
+        result::Ok(packages) => packages,
+        result::Err(err) => {
+            error(err);
+
+            return false;
+        }
+    };
+
+    wait_for_lock(&db_lock);
+    touch(&db_lock);
+    os::remove_file(&db);
+
+    match io::mk_file_writer(&db, ~[io::Create]) {
+        result::Ok(writer) => {
+            writer.write_line(json::to_pretty_str(&json::List(_rm_pkg(packages, pkg))));
+        }
+        result::Err(err) => {
+            error(fmt!("failed to dump package db: %s", err));
+            os::remove_file(&db_lock);
+
+            return false;
+        }
+    }
+
+    os::remove_file(&db_lock);
+
+    true
+}
+
+#[cfg(windows)]
+pub fn link_exe(_src: &Path, _dest: &Path) -> bool{
+    /* FIXME: Investigate how to do this on win32
+       Node wraps symlinks by having a .bat,
+       but that won't work with minGW. */
+
+    false
+}
+
+#[cfg(target_os = "linux")]
+#[cfg(target_os = "android")]
+#[cfg(target_os = "freebsd")]
+#[cfg(target_os = "macos")]
+pub fn link_exe(src: &Path, dest: &Path) -> bool unsafe {
+    do str::as_c_str(src.to_str()) |src_buf| {
+        do str::as_c_str(dest.to_str()) |dest_buf| {
+            libc::link(src_buf, dest_buf) == 0 as libc::c_int &&
+            libc::chmod(dest_buf, 755) == 0 as libc::c_int
+        }
+    }
+}
+
 #[test]
 fn test_is_cmd() {
     assert is_cmd(~"build");
@@ -100,7 +451,7 @@ fn test_is_cmd() {
 }
 
 #[test]
-fn test_parse_id() {
-    assert parse_id(~"org.mozilla.servo").get() == ~"servo";
-    assert parse_id(~"org. mozilla.servo 2131").is_err();
+fn test_parse_name() {
+    assert parse_name(~"org.mozilla.servo").get() == ~"servo";
+    assert parse_name(~"org. mozilla.servo 2131").is_err();
 }