rustpkg: Finish all commands and declarative logic

This commit is contained in:
Zack Corr 2013-01-19 19:59:19 +10:00 committed by Graydon Hoare
parent 321e3c4909
commit bd28fa4af5
3 changed files with 639 additions and 92 deletions

View 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);
}

View File

@ -1,5 +1,5 @@
use core::io;
pub fn general() {
io::println(~"Usage: rustpkg [options] <cmd> [args..]
@ -46,29 +46,22 @@ Examples:
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]
io::println(~"rustpkg uninstall <id|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.");
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 @@ Example:
}
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() {

View 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();
}