// cargo.rs - Rust package manager use syntax::{ast, codemap, parse, visit, attr}; use syntax::diagnostic::span_handler; use codemap::span; use rustc::metadata::filesearch::{get_cargo_root, get_cargo_root_nearest, get_cargo_sysroot, libdir}; use syntax::diagnostic; use result::{Ok, Err}; use io::WriterUtil; use std::{map, json, tempfile, term, sort, getopts}; use map::HashMap; use to_str::to_str; use getopts::{optflag, optopt, opt_present}; use dvec::DVec; type package = { name: ~str, uuid: ~str, url: ~str, method: ~str, description: ~str, reference: Option<~str>, tags: ~[~str], versions: ~[(~str, ~str)] }; #[cfg(stage0)] impl package : cmp::Ord { pure fn lt(&&other: package) -> bool { if self.name.lt(other.name) { return true; } if other.name.lt(self.name) { return false; } if self.uuid.lt(other.uuid) { return true; } if other.uuid.lt(self.uuid) { return false; } if self.url.lt(other.url) { return true; } if other.url.lt(self.url) { return false; } if self.method.lt(other.method) { return true; } if other.method.lt(self.method) { return false; } if self.description.lt(other.description) { return true; } if other.description.lt(self.description) { return false; } if self.tags.lt(other.tags) { return true; } if other.tags.lt(self.tags) { return false; } if self.versions.lt(other.versions) { return true; } return false; } pure fn le(&&other: package) -> bool { !other.lt(self) } pure fn ge(&&other: package) -> bool { !self.lt(other) } pure fn gt(&&other: package) -> bool { other.lt(self) } } #[cfg(stage1)] #[cfg(stage2)] impl package : cmp::Ord { pure fn lt(other: &package) -> bool { if self.name.lt(&(*other).name) { return true; } if (*other).name.lt(&self.name) { return false; } if self.uuid.lt(&(*other).uuid) { return true; } if (*other).uuid.lt(&self.uuid) { return false; } if self.url.lt(&(*other).url) { return true; } if (*other).url.lt(&self.url) { return false; } if self.method.lt(&(*other).method) { return true; } if (*other).method.lt(&self.method) { return false; } if self.description.lt(&(*other).description) { return true; } if (*other).description.lt(&self.description) { return false; } if self.tags.lt(&(*other).tags) { return true; } if (*other).tags.lt(&self.tags) { return false; } if self.versions.lt(&(*other).versions) { return true; } return false; } pure fn le(other: &package) -> bool { !(*other).lt(&self) } pure fn ge(other: &package) -> bool { !self.lt(other) } pure fn gt(other: &package) -> bool { (*other).lt(&self) } } type local_package = { name: ~str, metaname: ~str, version: ~str, files: ~[~str] }; type source = @{ name: ~str, mut url: ~str, mut method: ~str, mut key: Option<~str>, mut keyfp: Option<~str>, packages: DVec }; type cargo = { pgp: bool, root: Path, installdir: Path, bindir: Path, libdir: Path, workdir: Path, sourcedir: Path, sources: map::HashMap<~str, source>, mut current_install: ~str, dep_cache: map::HashMap<~str, bool>, opts: options }; type crate = { name: ~str, vers: ~str, uuid: ~str, desc: Option<~str>, sigs: Option<~str>, crate_type: Option<~str>, deps: ~[~str] }; type options = { test: bool, mode: mode, free: ~[~str], help: bool, }; enum mode { system_mode, user_mode, local_mode } #[cfg(stage0)] impl mode : cmp::Eq { pure fn eq(&&other: mode) -> bool { (self as uint) == (other as uint) } pure fn ne(&&other: mode) -> bool { !self.eq(other) } } #[cfg(stage1)] #[cfg(stage2)] impl mode : cmp::Eq { pure fn eq(other: &mode) -> bool { (self as uint) == ((*other) as uint) } pure fn ne(other: &mode) -> bool { !self.eq(other) } } fn opts() -> ~[getopts::Opt] { ~[optflag(~"g"), optflag(~"G"), optflag(~"test"), optflag(~"h"), optflag(~"help")] } fn info(msg: ~str) { let out = io::stdout(); if term::color_supported() { term::fg(out, term::color_green); out.write_str(~"info: "); term::reset(out); out.write_line(msg); } else { out.write_line(~"info: " + msg); } } fn warn(msg: ~str) { let out = io::stdout(); if term::color_supported() { term::fg(out, term::color_yellow); out.write_str(~"warning: "); term::reset(out); out.write_line(msg); }else { out.write_line(~"warning: " + msg); } } fn error(msg: ~str) { let out = io::stdout(); if term::color_supported() { term::fg(out, term::color_red); out.write_str(~"error: "); term::reset(out); out.write_line(msg); } else { out.write_line(~"error: " + msg); } } fn is_uuid(id: ~str) -> bool { let parts = str::split_str(id, ~"-"); if vec::len(parts) == 5u { let mut correct = 0u; for vec::eachi(parts) |i, part| { fn is_hex_digit(ch: char) -> bool { ('0' <= ch && ch <= '9') || ('a' <= ch && ch <= 'f') || ('A' <= ch && ch <= 'F') } if !part.all(is_hex_digit) { return false; } match i { 0u => { if str::len(part) == 8u { correct += 1u; } } 1u | 2u | 3u => { if str::len(part) == 4u { correct += 1u; } } 4u => { if str::len(part) == 12u { correct += 1u; } } _ => { } } } if correct >= 5u { return true; } } return false; } #[test] fn test_is_uuid() { assert is_uuid(~"aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaafAF09"); assert !is_uuid(~"aaaaaaaa-aaaa-aaaa-aaaaa-aaaaaaaaaaaa"); assert !is_uuid(~""); assert !is_uuid(~"aaaaaaaa-aaa -aaaa-aaaa-aaaaaaaaaaaa"); assert !is_uuid(~"aaaaaaaa-aaa!-aaaa-aaaa-aaaaaaaaaaaa"); assert !is_uuid(~"aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa-a"); assert !is_uuid(~"aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaป"); } // FIXME (#2661): implement url/URL parsing so we don't have to resort // to weak checks fn has_archive_extension(p: ~str) -> bool { str::ends_with(p, ~".tar") || str::ends_with(p, ~".tar.gz") || str::ends_with(p, ~".tar.bz2") || str::ends_with(p, ~".tar.Z") || str::ends_with(p, ~".tar.lz") || str::ends_with(p, ~".tar.xz") || str::ends_with(p, ~".tgz") || str::ends_with(p, ~".tbz") || str::ends_with(p, ~".tbz2") || str::ends_with(p, ~".tb2") || str::ends_with(p, ~".taz") || str::ends_with(p, ~".tlz") || str::ends_with(p, ~".txz") } fn is_archive_path(u: ~str) -> bool { has_archive_extension(u) && os::path_exists(&Path(u)) } fn is_archive_url(u: ~str) -> bool { // FIXME (#2661): this requires the protocol bit - if we had proper // url parsing, we wouldn't need it match str::find_str(u, ~"://") { option::Some(_) => has_archive_extension(u), _ => false } } fn is_git_url(url: ~str) -> bool { if str::ends_with(url, ~"/") { str::ends_with(url, ~".git/") } else { str::starts_with(url, ~"git://") || str::ends_with(url, ~".git") } } fn assume_source_method(url: ~str) -> ~str { if is_git_url(url) { return ~"git"; } if str::starts_with(url, ~"file://") || os::path_exists(&Path(url)) { return ~"file"; } ~"curl" } fn load_link(mis: ~[@ast::meta_item]) -> (Option<~str>, Option<~str>, Option<~str>) { let mut name = None; let mut vers = None; let mut uuid = None; for mis.each |a| { match a.node { ast::meta_name_value(v, {node: ast::lit_str(s), span: _}) => { match v { ~"name" => name = Some(*s), ~"vers" => vers = Some(*s), ~"uuid" => uuid = Some(*s), _ => { } } } _ => fail ~"load_link: meta items must be name-values" } } (name, vers, uuid) } fn load_crate(filename: &Path) -> Option { let sess = parse::new_parse_sess(None); let c = parse::parse_crate_from_crate_file(filename, ~[], sess); let mut name = None; let mut vers = None; let mut uuid = None; let mut desc = None; let mut sigs = None; let mut crate_type = None; for c.node.attrs.each |a| { match a.node.value.node { ast::meta_name_value(v, {node: ast::lit_str(_), span: _}) => { match v { ~"desc" => desc = Some(v), ~"sigs" => sigs = Some(v), ~"crate_type" => crate_type = Some(v), _ => { } } } ast::meta_list(v, mis) => { if v == ~"link" { let (n, v, u) = load_link(mis); name = n; vers = v; uuid = u; } } _ => { fail ~"crate attributes may not contain " + ~"meta_words"; } } } type env = @{ mut deps: ~[~str] }; fn goto_view_item(ps: syntax::parse::parse_sess, e: env, i: @ast::view_item) { match i.node { ast::view_item_use(ident, metas, _) => { let name_items = attr::find_meta_items_by_name(metas, ~"name"); let m = if name_items.is_empty() { metas + ~[attr::mk_name_value_item_str( ~"name", *ps.interner.get(ident))] } else { metas }; let mut attr_name = ident; let mut attr_vers = ~""; let mut attr_from = ~""; for m.each |item| { match attr::get_meta_item_value_str(*item) { Some(value) => { let name = attr::get_meta_item_name(*item); match name { ~"vers" => attr_vers = value, ~"from" => attr_from = value, _ => () } } None => () } } let query = if !str::is_empty(attr_from) { attr_from } else { if !str::is_empty(attr_vers) { ps.interner.get(attr_name) + ~"@" + attr_vers } else { *ps.interner.get(attr_name) } }; match *ps.interner.get(attr_name) { ~"std" | ~"core" => (), _ => vec::push(e.deps, query) } } _ => () } } fn goto_item(_e: env, _i: @ast::item) { } let e = @{ mut deps: ~[] }; let v = visit::mk_simple_visitor(@{ visit_view_item: |a| goto_view_item(sess, e, a), visit_item: |a| goto_item(e, a), .. *visit::default_simple_visitor() }); visit::visit_crate(*c, (), v); let deps = copy e.deps; match (name, vers, uuid) { (Some(name0), Some(vers0), Some(uuid0)) => { Some({ name: name0, vers: vers0, uuid: uuid0, desc: desc, sigs: sigs, crate_type: crate_type, deps: deps }) } _ => return None } } fn print(s: ~str) { io::stdout().write_line(s); } fn rest(s: ~str, start: uint) -> ~str { if (start >= str::len(s)) { ~"" } else { str::slice(s, start, str::len(s)) } } fn need_dir(s: &Path) { if os::path_is_dir(s) { return; } if !os::make_dir(s, 493_i32 /* oct: 755 */) { fail fmt!("can't make_dir %s", s.to_str()); } } fn valid_pkg_name(s: ~str) -> bool { fn is_valid_digit(c: char) -> bool { ('0' <= c && c <= '9') || ('a' <= c && c <= 'z') || ('A' <= c && c <= 'Z') || c == '-' || c == '_' } s.all(is_valid_digit) } fn parse_source(name: ~str, j: json::Json) -> source { if !valid_pkg_name(name) { fail fmt!("'%s' is an invalid source name", name); } match j { json::Dict(j) => { let mut url = match j.find(~"url") { Some(json::String(u)) => *u, _ => fail ~"needed 'url' field in source" }; let method = match j.find(~"method") { Some(json::String(u)) => *u, _ => assume_source_method(url) }; let key = match j.find(~"key") { Some(json::String(u)) => Some(*u), _ => None }; let keyfp = match j.find(~"keyfp") { Some(json::String(u)) => Some(*u), _ => None }; if method == ~"file" { url = os::make_absolute(&Path(url)).to_str(); } return @{ name: name, mut url: url, mut method: method, mut key: key, mut keyfp: keyfp, packages: DVec() }; } _ => fail ~"needed dict value in source" }; } fn try_parse_sources(filename: &Path, sources: map::HashMap<~str, source>) { if !os::path_exists(filename) { return; } let c = io::read_whole_file_str(filename); match json::from_str(result::get(c)) { Ok(json::Dict(j)) => { for j.each |k, v| { sources.insert(k, parse_source(k, v)); debug!("source: %s", k); } } Ok(_) => fail ~"malformed sources.json", Err(e) => fail fmt!("%s:%s", filename.to_str(), e.to_str()) } } fn load_one_source_package(src: source, p: map::HashMap<~str, json::Json>) { let name = match p.find(~"name") { Some(json::String(n)) => { if !valid_pkg_name(*n) { warn(~"malformed source json: " + src.name + ~", '" + *n + ~"'"+ ~" is an invalid name (alphanumeric, underscores and" + ~" dashes only)"); return; } *n } _ => { warn(~"malformed source json: " + src.name + ~" (missing name)"); return; } }; let uuid = match p.find(~"uuid") { Some(json::String(n)) => { if !is_uuid(*n) { warn(~"malformed source json: " + src.name + ~", '" + *n + ~"'"+ ~" is an invalid uuid"); return; } *n } _ => { warn(~"malformed source json: " + src.name + ~" (missing uuid)"); return; } }; let url = match p.find(~"url") { Some(json::String(n)) => *n, _ => { warn(~"malformed source json: " + src.name + ~" (missing url)"); return; } }; let method = match p.find(~"method") { Some(json::String(n)) => *n, _ => { warn(~"malformed source json: " + src.name + ~" (missing method)"); return; } }; let reference = match p.find(~"ref") { Some(json::String(n)) => Some(*n), _ => None }; let mut tags = ~[]; match p.find(~"tags") { Some(json::List(js)) => { for (*js).each |j| { match *j { json::String(j) => vec::grow(tags, 1u, *j), _ => () } } } _ => () } let description = match p.find(~"description") { Some(json::String(n)) => *n, _ => { warn(~"malformed source json: " + src.name + ~" (missing description)"); return; } }; let newpkg = { name: name, uuid: uuid, url: url, method: method, description: description, reference: reference, tags: tags, versions: ~[] }; match src.packages.position(|pkg| pkg.uuid == uuid) { Some(idx) => { src.packages.set_elt(idx, newpkg); log(debug, ~" updated package: " + src.name + ~"/" + name); } None => { src.packages.push(newpkg); } } log(debug, ~" loaded package: " + src.name + ~"/" + name); } fn load_source_info(c: &cargo, src: source) { let dir = c.sourcedir.push(src.name); let srcfile = dir.push("source.json"); if !os::path_exists(&srcfile) { return; } let srcstr = io::read_whole_file_str(&srcfile); match json::from_str(result::get(srcstr)) { Ok(json::Dict(s)) => { let o = parse_source(src.name, json::Dict(s)); src.key = o.key; src.keyfp = o.keyfp; } Ok(_) => { warn(~"malformed source.json: " + src.name + ~"(source info is not a dict)"); } Err(e) => { warn(fmt!("%s:%s", src.name, e.to_str())); } }; } fn load_source_packages(c: &cargo, src: source) { log(debug, ~"loading source: " + src.name); let dir = c.sourcedir.push(src.name); let pkgfile = dir.push("packages.json"); if !os::path_exists(&pkgfile) { return; } let pkgstr = io::read_whole_file_str(&pkgfile); match json::from_str(result::get(pkgstr)) { Ok(json::List(js)) => { for (*js).each |j| { match *j { json::Dict(p) => { load_one_source_package(src, p); } _ => { warn(~"malformed source json: " + src.name + ~" (non-dict pkg)"); } } } } Ok(_) => { warn(~"malformed packages.json: " + src.name + ~"(packages is not a list)"); } Err(e) => { warn(fmt!("%s:%s", src.name, e.to_str())); } }; } fn build_cargo_options(argv: ~[~str]) -> options { let matches = match getopts::getopts(argv, opts()) { result::Ok(m) => m, result::Err(f) => { fail fmt!("%s", getopts::fail_str(f)); } }; let test = opt_present(matches, ~"test"); let G = opt_present(matches, ~"G"); let g = opt_present(matches, ~"g"); let help = opt_present(matches, ~"h") || opt_present(matches, ~"help"); let len = vec::len(matches.free); let is_install = len > 1u && matches.free[1] == ~"install"; let is_uninstall = len > 1u && matches.free[1] == ~"uninstall"; if G && g { fail ~"-G and -g both provided"; } if !is_install && !is_uninstall && (g || G) { fail ~"-g and -G are only valid for `install` and `uninstall|rm`"; } let mode = if (!is_install && !is_uninstall) || g { user_mode } else if G { system_mode } else { local_mode }; {test: test, mode: mode, free: matches.free, help: help} } fn configure(opts: options) -> cargo { let home = match get_cargo_root() { Ok(home) => home, Err(_err) => result::get(get_cargo_sysroot()) }; let get_cargo_dir = match opts.mode { system_mode => get_cargo_sysroot, user_mode => get_cargo_root, local_mode => get_cargo_root_nearest }; let p = result::get(get_cargo_dir()); let sources = map::HashMap(); try_parse_sources(&home.push("sources.json"), sources); try_parse_sources(&home.push("local-sources.json"), sources); let dep_cache = map::HashMap(); let mut c = { pgp: pgp::supported(), root: home, installdir: p, bindir: p.push("bin"), libdir: p.push("lib"), workdir: p.push("work"), sourcedir: home.push("sources"), sources: sources, mut current_install: ~"", dep_cache: dep_cache, opts: opts }; need_dir(&c.root); need_dir(&c.installdir); need_dir(&c.sourcedir); need_dir(&c.workdir); need_dir(&c.libdir); need_dir(&c.bindir); for sources.each_key |k| { let mut s = sources.get(k); load_source_packages(&c, s); sources.insert(k, s); } if c.pgp { pgp::init(&c.root); } else { warn(~"command `gpg` was not found"); warn(~"you have to install gpg from source " + ~" or package manager to get it to work correctly"); } c } fn for_each_package(c: &cargo, b: fn(source, package)) { for c.sources.each_value |v| { for v.packages.each |p| { b(v, *p); } } } // Runs all programs in directory fn run_programs(buildpath: &Path) { let newv = os::list_dir_path(buildpath); for newv.each |ct| { run::run_program(ct.to_str(), ~[]); } } // Runs rustc in with the given flags // and returns fn run_in_buildpath(what: &str, path: &Path, subdir: &Path, cf: &Path, extra_flags: ~[~str]) -> Option { let buildpath = path.push_rel(subdir); need_dir(&buildpath); debug!("%s: %s -> %s", what, cf.to_str(), buildpath.to_str()); let p = run::program_output(rustc_sysroot(), ~[~"--out-dir", buildpath.to_str(), cf.to_str()] + extra_flags); if p.status != 0 { error(fmt!("rustc failed: %d\n%s\n%s", p.status, p.err, p.out)); return None; } Some(buildpath) } fn test_one_crate(_c: &cargo, path: &Path, cf: &Path) { let buildpath = match run_in_buildpath(~"testing", path, &Path("test"), cf, ~[ ~"--test"]) { None => return, Some(bp) => bp }; run_programs(&buildpath); } fn install_one_crate(c: &cargo, path: &Path, cf: &Path) { let buildpath = match run_in_buildpath(~"installing", path, &Path("build"), cf, ~[]) { None => return, Some(bp) => bp }; let newv = os::list_dir_path(&buildpath); let exec_suffix = os::exe_suffix(); for newv.each |ct| { if (exec_suffix != ~"" && str::ends_with(ct.to_str(), exec_suffix)) || (exec_suffix == ~"" && !str::starts_with(option::get(ct.filename()), ~"lib")) { debug!(" bin: %s", ct.to_str()); install_to_dir(*ct, &c.bindir); if c.opts.mode == system_mode { // FIXME (#2662): Put this file in PATH / symlink it so it can // be used as a generic executable // `cargo install -G rustray` and `rustray file.obj` } } else { debug!(" lib: %s", ct.to_str()); install_to_dir(*ct, &c.libdir); } } } fn rustc_sysroot() -> ~str { match os::self_exe_path() { Some(path) => { let rustc = path.push_many([~"..", ~"bin", ~"rustc"]); debug!(" rustc: %s", rustc.to_str()); rustc.to_str() } None => ~"rustc" } } fn install_source(c: &cargo, path: &Path) { debug!("source: %s", path.to_str()); os::change_dir(path); let mut cratefiles = ~[]; for os::walk_dir(&Path(".")) |p| { if p.filetype() == Some(~".rc") { vec::push(cratefiles, *p); } } if vec::is_empty(cratefiles) { fail ~"this doesn't look like a rust package (no .rc files)"; } for cratefiles.each |cf| { match load_crate(cf) { None => loop, Some(crate) => { for crate.deps.each |query| { // FIXME (#1356): handle cyclic dependencies // (n.b. #1356 says "Cyclic dependency is an error // condition") let wd = get_temp_workdir(c); install_query(c, &wd, *query); } os::change_dir(path); if c.opts.test { test_one_crate(c, path, cf); } install_one_crate(c, path, cf); } } } } fn install_git(c: &cargo, wd: &Path, url: ~str, reference: Option<~str>) { run::program_output(~"git", ~[~"clone", url, wd.to_str()]); if option::is_some(reference) { let r = option::get(reference); os::change_dir(wd); run::run_program(~"git", ~[~"checkout", r]); } install_source(c, wd); } fn install_curl(c: &cargo, wd: &Path, url: ~str) { let tarpath = wd.push("pkg.tar"); let p = run::program_output(~"curl", ~[~"-f", ~"-s", ~"-o", tarpath.to_str(), url]); if p.status != 0 { fail fmt!("fetch of %s failed: %s", url, p.err); } run::run_program(~"tar", ~[~"-x", ~"--strip-components=1", ~"-C", wd.to_str(), ~"-f", tarpath.to_str()]); install_source(c, wd); } fn install_file(c: &cargo, wd: &Path, path: &Path) { run::program_output(~"tar", ~[~"-x", ~"--strip-components=1", ~"-C", wd.to_str(), ~"-f", path.to_str()]); install_source(c, wd); } fn install_package(c: &cargo, src: ~str, wd: &Path, pkg: package) { let url = copy pkg.url; let method = match pkg.method { ~"git" => ~"git", ~"file" => ~"file", _ => ~"curl" }; info(fmt!("installing %s/%s via %s...", src, pkg.name, method)); match method { ~"git" => install_git(c, wd, url, copy pkg.reference), ~"file" => install_file(c, wd, &Path(url)), ~"curl" => install_curl(c, wd, copy url), _ => () } } fn cargo_suggestion(c: &cargo, fallback: fn()) { if c.sources.size() == 0u { error(~"no sources defined - you may wish to run " + ~"`cargo init`"); return; } fallback(); } fn install_uuid(c: &cargo, wd: &Path, uuid: ~str) { let mut ps = ~[]; for_each_package(c, |s, p| { if p.uuid == uuid { vec::grow(ps, 1u, (s.name, copy p)); } }); if vec::len(ps) == 1u { let (sname, p) = copy ps[0]; install_package(c, sname, wd, p); return; } else if vec::len(ps) == 0u { cargo_suggestion(c, || { error(~"can't find package: " + uuid); }); return; } error(~"found multiple packages:"); for ps.each |elt| { let (sname,p) = copy *elt; info(~" " + sname + ~"/" + p.uuid + ~" (" + p.name + ~")"); } } fn install_named(c: &cargo, wd: &Path, name: ~str) { let mut ps = ~[]; for_each_package(c, |s, p| { if p.name == name { vec::grow(ps, 1u, (s.name, copy p)); } }); if vec::len(ps) == 1u { let (sname, p) = copy ps[0]; install_package(c, sname, wd, p); return; } else if vec::len(ps) == 0u { cargo_suggestion(c, || { error(~"can't find package: " + name); }); return; } error(~"found multiple packages:"); for ps.each |elt| { let (sname,p) = copy *elt; info(~" " + sname + ~"/" + p.uuid + ~" (" + p.name + ~")"); } } fn install_uuid_specific(c: &cargo, wd: &Path, src: ~str, uuid: ~str) { match c.sources.find(src) { Some(s) => { for s.packages.each |p| { if p.uuid == uuid { install_package(c, src, wd, *p); return; } } } _ => () } error(~"can't find package: " + src + ~"/" + uuid); } fn install_named_specific(c: &cargo, wd: &Path, src: ~str, name: ~str) { match c.sources.find(src) { Some(s) => { for s.packages.each |p| { if p.name == name { install_package(c, src, wd, *p); return; } } } _ => () } error(~"can't find package: " + src + ~"/" + name); } fn cmd_uninstall(c: &cargo) { if vec::len(c.opts.free) < 3u { cmd_usage(); return; } let lib = &c.libdir; let bin = &c.bindir; let target = c.opts.free[2u]; // FIXME (#2662): needs stronger pattern matching // FIXME (#2662): needs to uninstall from a specified location in a // cache instead of looking for it (binaries can be uninstalled by // name only) fn try_uninstall(p: &Path) -> bool { if os::remove_file(p) { info(~"uninstalled: '" + p.to_str() + ~"'"); true } else { error(~"could not uninstall: '" + p.to_str() + ~"'"); false } } if is_uuid(target) { for os::list_dir(lib).each |file| { match str::find_str(*file, ~"-" + target + ~"-") { Some(_) => if !try_uninstall(&lib.push(*file)) { return }, None => () } } error(~"can't find package with uuid: " + target); } else { for os::list_dir(lib).each |file| { match str::find_str(*file, ~"lib" + target + ~"-") { Some(_) => if !try_uninstall(&lib.push(*file)) { return }, None => () } } for os::list_dir(bin).each |file| { match str::find_str(*file, target) { Some(_) => if !try_uninstall(&lib.push(*file)) { return }, None => () } } error(~"can't find package with name: " + target); } } fn install_query(c: &cargo, wd: &Path, target: ~str) { match c.dep_cache.find(target) { Some(inst) => { if inst { return; } } None => () } c.dep_cache.insert(target, true); if is_archive_path(target) { install_file(c, wd, &Path(target)); return; } else if is_git_url(target) { let reference = if c.opts.free.len() >= 4u { Some(c.opts.free[3u]) } else { None }; install_git(c, wd, target, reference); } else if !valid_pkg_name(target) && has_archive_extension(target) { install_curl(c, wd, target); return; } else { let mut ps = copy target; match str::find_char(ps, '/') { option::Some(idx) => { let source = str::slice(ps, 0u, idx); ps = str::slice(ps, idx + 1u, str::len(ps)); if is_uuid(ps) { install_uuid_specific(c, wd, source, ps); } else { install_named_specific(c, wd, source, ps); } } option::None => { if is_uuid(ps) { install_uuid(c, wd, ps); } else { install_named(c, wd, ps); } } } } // FIXME (#2662): This whole dep_cache and current_install thing is // a bit of a hack. It should be cleaned up in the future. if target == c.current_install { for c.dep_cache.each |k, _v| { c.dep_cache.remove(k); } c.current_install = ~""; } } fn get_temp_workdir(c: &cargo) -> Path { match tempfile::mkdtemp(&c.workdir, "cargo") { Some(wd) => wd, None => fail fmt!("needed temp dir: %s", c.workdir.to_str()) } } fn cmd_install(c: &cargo) unsafe { let wd = get_temp_workdir(c); if vec::len(c.opts.free) == 2u { let cwd = os::getcwd(); let status = run::run_program(~"cp", ~[~"-R", cwd.to_str(), wd.to_str()]); if status != 0 { fail fmt!("could not copy directory: %s", cwd.to_str()); } install_source(c, &wd); return; } sync(c); let query = c.opts.free[2]; c.current_install = query.to_str(); install_query(c, &wd, query); } fn sync(c: &cargo) { for c.sources.each_key |k| { let mut s = c.sources.get(k); sync_one(c, s); c.sources.insert(k, s); } } fn sync_one_file(c: &cargo, dir: &Path, src: source) -> bool { let name = src.name; let srcfile = dir.push("source.json.new"); let destsrcfile = dir.push("source.json"); let pkgfile = dir.push("packages.json.new"); let destpkgfile = dir.push("packages.json"); let keyfile = dir.push("key.gpg"); let srcsigfile = dir.push("source.json.sig"); let sigfile = dir.push("packages.json.sig"); let url = Path(src.url); let mut has_src_file = false; if !os::copy_file(&url.push("packages.json"), &pkgfile) { error(fmt!("fetch for source %s (url %s) failed", name, url.to_str())); return false; } if os::copy_file(&url.push("source.json"), &srcfile) { has_src_file = false; } os::copy_file(&url.push("source.json.sig"), &srcsigfile); os::copy_file(&url.push("packages.json.sig"), &sigfile); match copy src.key { Some(u) => { let p = run::program_output(~"curl", ~[~"-f", ~"-s", ~"-o", keyfile.to_str(), u]); if p.status != 0 { error(fmt!("fetch for source %s (key %s) failed", name, u)); return false; } pgp::add(&c.root, &keyfile); } _ => () } match (src.key, src.keyfp) { (Some(_), Some(f)) => { let r = pgp::verify(&c.root, &pkgfile, &sigfile, f); if !r { error(fmt!("signature verification failed for source %s", name)); return false; } if has_src_file { let e = pgp::verify(&c.root, &srcfile, &srcsigfile, f); if !e { error(fmt!("signature verification failed for source %s", name)); return false; } } } _ => () } copy_warn(&pkgfile, &destpkgfile); if has_src_file { copy_warn(&srcfile, &destsrcfile); } os::remove_file(&keyfile); os::remove_file(&srcfile); os::remove_file(&srcsigfile); os::remove_file(&pkgfile); os::remove_file(&sigfile); info(fmt!("synced source: %s", name)); return true; } fn sync_one_git(c: &cargo, dir: &Path, src: source) -> bool { let name = src.name; let srcfile = dir.push("source.json"); let pkgfile = dir.push("packages.json"); let keyfile = dir.push("key.gpg"); let srcsigfile = dir.push("source.json.sig"); let sigfile = dir.push("packages.json.sig"); let url = src.url; fn rollback(name: ~str, dir: &Path, insecure: bool) { fn msg(name: ~str, insecure: bool) { error(fmt!("could not rollback source: %s", name)); if insecure { warn(~"a past security check failed on source " + name + ~" and rolling back the source failed -" + ~" this source may be compromised"); } } if !os::change_dir(dir) { msg(name, insecure); } else { let p = run::program_output(~"git", ~[~"reset", ~"--hard", ~"HEAD@{1}"]); if p.status != 0 { msg(name, insecure); } } } if !os::path_exists(&dir.push(".git")) { let p = run::program_output(~"git", ~[~"clone", url, dir.to_str()]); if p.status != 0 { error(fmt!("fetch for source %s (url %s) failed", name, url)); return false; } } else { if !os::change_dir(dir) { error(fmt!("fetch for source %s (url %s) failed", name, url)); return false; } let p = run::program_output(~"git", ~[~"pull"]); if p.status != 0 { error(fmt!("fetch for source %s (url %s) failed", name, url)); return false; } } let has_src_file = os::path_exists(&srcfile); match copy src.key { Some(u) => { let p = run::program_output(~"curl", ~[~"-f", ~"-s", ~"-o", keyfile.to_str(), u]); if p.status != 0 { error(fmt!("fetch for source %s (key %s) failed", name, u)); rollback(name, dir, false); return false; } pgp::add(&c.root, &keyfile); } _ => () } match (src.key, src.keyfp) { (Some(_), Some(f)) => { let r = pgp::verify(&c.root, &pkgfile, &sigfile, f); if !r { error(fmt!("signature verification failed for source %s", name)); rollback(name, dir, false); return false; } if has_src_file { let e = pgp::verify(&c.root, &srcfile, &srcsigfile, f); if !e { error(fmt!("signature verification failed for source %s", name)); rollback(name, dir, false); return false; } } } _ => () } os::remove_file(&keyfile); info(fmt!("synced source: %s", name)); return true; } fn sync_one_curl(c: &cargo, dir: &Path, src: source) -> bool { let name = src.name; let srcfile = dir.push("source.json.new"); let destsrcfile = dir.push("source.json"); let pkgfile = dir.push("packages.json.new"); let destpkgfile = dir.push("packages.json"); let keyfile = dir.push("key.gpg"); let srcsigfile = dir.push("source.json.sig"); let sigfile = dir.push("packages.json.sig"); let mut url = src.url; let smart = !str::ends_with(src.url, ~"packages.json"); let mut has_src_file = false; if smart { url += ~"/packages.json"; } let p = run::program_output(~"curl", ~[~"-f", ~"-s", ~"-o", pkgfile.to_str(), url]); if p.status != 0 { error(fmt!("fetch for source %s (url %s) failed", name, url)); return false; } if smart { url = src.url + ~"/source.json"; let p = run::program_output(~"curl", ~[~"-f", ~"-s", ~"-o", srcfile.to_str(), url]); if p.status == 0 { has_src_file = true; } } match copy src.key { Some(u) => { let p = run::program_output(~"curl", ~[~"-f", ~"-s", ~"-o", keyfile.to_str(), u]); if p.status != 0 { error(fmt!("fetch for source %s (key %s) failed", name, u)); return false; } pgp::add(&c.root, &keyfile); } _ => () } match (src.key, src.keyfp) { (Some(_), Some(f)) => { if smart { url = src.url + ~"/packages.json.sig"; } else { url = src.url + ~".sig"; } let mut p = run::program_output(~"curl", ~[~"-f", ~"-s", ~"-o", sigfile.to_str(), url]); if p.status != 0 { error(fmt!("fetch for source %s (sig %s) failed", name, url)); return false; } let r = pgp::verify(&c.root, &pkgfile, &sigfile, f); if !r { error(fmt!("signature verification failed for source %s", name)); return false; } if smart && has_src_file { url = src.url + ~"/source.json.sig"; p = run::program_output(~"curl", ~[~"-f", ~"-s", ~"-o", srcsigfile.to_str(), url]); if p.status != 0 { error(fmt!("fetch for source %s (sig %s) failed", name, url)); return false; } let e = pgp::verify(&c.root, &srcfile, &srcsigfile, f); if !e { error(~"signature verification failed for " + ~"source " + name); return false; } } } _ => () } copy_warn(&pkgfile, &destpkgfile); if smart && has_src_file { copy_warn(&srcfile, &destsrcfile); } os::remove_file(&keyfile); os::remove_file(&srcfile); os::remove_file(&srcsigfile); os::remove_file(&pkgfile); os::remove_file(&sigfile); info(fmt!("synced source: %s", name)); return true; } fn sync_one(c: &cargo, src: source) { let name = src.name; let dir = c.sourcedir.push(name); info(fmt!("syncing source: %s...", name)); need_dir(&dir); let result = match src.method { ~"git" => sync_one_git(c, &dir, src), ~"file" => sync_one_file(c, &dir, src), _ => sync_one_curl(c, &dir, src) }; if result { load_source_info(c, src); load_source_packages(c, src); } } fn cmd_init(c: &cargo) { let srcurl = ~"http://www.rust-lang.org/cargo/sources.json"; let sigurl = ~"http://www.rust-lang.org/cargo/sources.json.sig"; let srcfile = c.root.push("sources.json.new"); let sigfile = c.root.push("sources.json.sig"); let destsrcfile = c.root.push("sources.json"); let p = run::program_output(~"curl", ~[~"-f", ~"-s", ~"-o", srcfile.to_str(), srcurl]); if p.status != 0 { error(fmt!("fetch of sources.json failed: %s", p.out)); return; } let p = run::program_output(~"curl", ~[~"-f", ~"-s", ~"-o", sigfile.to_str(), sigurl]); if p.status != 0 { error(fmt!("fetch of sources.json.sig failed: %s", p.out)); return; } let r = pgp::verify(&c.root, &srcfile, &sigfile, pgp::signing_key_fp()); if !r { error(fmt!("signature verification failed for '%s'", srcfile.to_str())); return; } copy_warn(&srcfile, &destsrcfile); os::remove_file(&srcfile); os::remove_file(&sigfile); info(fmt!("initialized .cargo in %s", c.root.to_str())); } fn print_pkg(s: source, p: package) { let mut m = s.name + ~"/" + p.name + ~" (" + p.uuid + ~")"; if vec::len(p.tags) > 0u { m = m + ~" [" + str::connect(p.tags, ~", ") + ~"]"; } info(m); if p.description != ~"" { print(~" >> " + p.description + ~"\n") } } fn print_source(s: source) { info(s.name + ~" (" + s.url + ~")"); let pks = sort::merge_sort(sys::shape_lt, s.packages.get()); let l = vec::len(pks); print(io::with_str_writer(|writer| { let mut list = ~" >> "; for vec::eachi(pks) |i, pk| { if str::len(list) > 78u { writer.write_line(list); list = ~" >> "; } list += pk.name + (if l - 1u == i { ~"" } else { ~", " }); } writer.write_line(list); })); } fn cmd_list(c: &cargo) { sync(c); if vec::len(c.opts.free) >= 3u { let v = vec::view(c.opts.free, 2u, vec::len(c.opts.free)); for vec::each(v) |name| { if !valid_pkg_name(*name) { error(fmt!("'%s' is an invalid source name", *name)); } else { match c.sources.find(*name) { Some(source) => { print_source(source); } None => { error(fmt!("no such source: %s", *name)); } } } } } else { for c.sources.each_value |v| { print_source(v); } } } fn cmd_search(c: &cargo) { if vec::len(c.opts.free) < 3u { cmd_usage(); return; } sync(c); let mut n = 0; let name = c.opts.free[2]; let tags = vec::slice(c.opts.free, 3u, vec::len(c.opts.free)); for_each_package(c, |s, p| { if (str::contains(p.name, name) || name == ~"*") && vec::all(tags, |t| vec::contains(p.tags, t) ) { print_pkg(s, p); n += 1; } }); info(fmt!("found %d packages", n)); } fn install_to_dir(srcfile: &Path, destdir: &Path) { let newfile = destdir.push(option::get(srcfile.filename())); let status = run::run_program(~"cp", ~[~"-r", srcfile.to_str(), newfile.to_str()]); if status == 0 { info(fmt!("installed: '%s'", newfile.to_str())); } else { error(fmt!("could not install: '%s'", newfile.to_str())); } } fn dump_cache(c: &cargo) { need_dir(&c.root); let out = c.root.push("cache.json"); let _root = json::Dict(map::HashMap()); if os::path_exists(&out) { copy_warn(&out, &c.root.push("cache.json.old")); } } fn dump_sources(c: &cargo) { if c.sources.size() < 1u { return; } need_dir(&c.root); let out = c.root.push("sources.json"); if os::path_exists(&out) { copy_warn(&out, &c.root.push("sources.json.old")); } match io::buffered_file_writer(&out) { result::Ok(writer) => { let hash = map::HashMap(); let root = json::Dict(hash); for c.sources.each |k, v| { let chash = map::HashMap(); let child = json::Dict(chash); chash.insert(~"url", json::String(@v.url)); chash.insert(~"method", json::String(@v.method)); match copy v.key { Some(key) => { chash.insert(~"key", json::String(@key)); } _ => () } match copy v.keyfp { Some(keyfp) => { chash.insert(~"keyfp", json::String(@keyfp)); } _ => () } hash.insert(k, child); } writer.write_str(json::to_str(root)); } result::Err(e) => { error(fmt!("could not dump sources: %s", e)); } } } fn copy_warn(srcfile: &Path, destfile: &Path) { if !os::copy_file(srcfile, destfile) { warn(fmt!("copying %s to %s failed", srcfile.to_str(), destfile.to_str())); } } fn cmd_sources(c: &cargo) { if vec::len(c.opts.free) < 3u { for c.sources.each_value |v| { info(fmt!("%s (%s) via %s", v.name, v.url, v.method)); } return; } let action = c.opts.free[2u]; match action { ~"clear" => { for c.sources.each_key |k| { c.sources.remove(k); } info(~"cleared sources"); } ~"add" => { if vec::len(c.opts.free) < 5u { cmd_usage(); return; } let name = c.opts.free[3u]; let url = c.opts.free[4u]; if !valid_pkg_name(name) { error(fmt!("'%s' is an invalid source name", name)); return; } if c.sources.contains_key(name) { error(fmt!("source already exists: %s", name)); } else { c.sources.insert(name, @{ name: name, mut url: url, mut method: assume_source_method(url), mut key: None, mut keyfp: None, packages: DVec() }); info(fmt!("added source: %s", name)); } } ~"remove" => { if vec::len(c.opts.free) < 4u { cmd_usage(); return; } let name = c.opts.free[3u]; if !valid_pkg_name(name) { error(fmt!("'%s' is an invalid source name", name)); return; } if c.sources.contains_key(name) { c.sources.remove(name); info(fmt!("removed source: %s", name)); } else { error(fmt!("no such source: %s", name)); } } ~"set-url" => { if vec::len(c.opts.free) < 5u { cmd_usage(); return; } let name = c.opts.free[3u]; let url = c.opts.free[4u]; if !valid_pkg_name(name) { error(fmt!("'%s' is an invalid source name", name)); return; } match c.sources.find(name) { Some(source) => { let old = copy source.url; let method = assume_source_method(url); source.url = url; source.method = method; c.sources.insert(name, source); info(fmt!("changed source url: '%s' to '%s'", old, url)); } None => { error(fmt!("no such source: %s", name)); } } } ~"set-method" => { if vec::len(c.opts.free) < 5u { cmd_usage(); return; } let name = c.opts.free[3u]; let method = c.opts.free[4u]; if !valid_pkg_name(name) { error(fmt!("'%s' is an invalid source name", name)); return; } match c.sources.find(name) { Some(source) => { let old = copy source.method; source.method = match method { ~"git" => ~"git", ~"file" => ~"file", _ => ~"curl" }; c.sources.insert(name, source); info(fmt!("changed source method: '%s' to '%s'", old, method)); } None => { error(fmt!("no such source: %s", name)); } } } ~"rename" => { if vec::len(c.opts.free) < 5u { cmd_usage(); return; } let name = c.opts.free[3u]; let newn = c.opts.free[4u]; if !valid_pkg_name(name) { error(fmt!("'%s' is an invalid source name", name)); return; } if !valid_pkg_name(newn) { error(fmt!("'%s' is an invalid source name", newn)); return; } match c.sources.find(name) { Some(source) => { c.sources.remove(name); c.sources.insert(newn, source); info(fmt!("renamed source: %s to %s", name, newn)); } None => { error(fmt!("no such source: %s", name)); } } } _ => cmd_usage() } } fn cmd_usage() { print(~"Usage: cargo [options] [args..] e.g. cargo install Where is one of: init, install, list, search, sources, uninstall, usage Options: -h, --help Display this message -h, --help Display help for "); } fn cmd_usage_init() { print(~"cargo init Re-initialize cargo in ~/.cargo. Clears all sources and then adds the default sources from ."); } fn cmd_usage_install() { print(~"cargo install cargo install [source/][@version] cargo install [source/][@version] cargo install [ref] cargo install cargo install Options: --test Run crate tests before installing -g Install to the user level (~/.cargo/bin/ instead of locally in ./.cargo/bin/ by default) -G Install to the system level (/usr/local/lib/cargo/bin/) Install a crate. If no arguments are supplied, it installs from the current working directory. If a source is provided, only install from that source, otherwise it installs from any source."); } fn cmd_usage_uninstall() { print(~"cargo uninstall [source/][@version] cargo uninstall [source/][@version] cargo uninstall [@version] cargo uninstall [@version] Options: -g Remove from the user level (~/.cargo/bin/ instead of locally in ./.cargo/bin/ by default) -G Remove from the system level (/usr/local/lib/cargo/bin/) Remove a crate. If a source is provided, only remove from that source, otherwise it removes from any source. If a crate was installed directly (git, tarball, etc.), you can remove it by metadata."); } fn cmd_usage_list() { print(~"cargo list [sources..] If no arguments are provided, list all sources and their packages. If source names are provided, list those sources and their packages. "); } fn cmd_usage_search() { print(~"cargo search [tags..] Search packages."); } fn cmd_usage_sources() { print(~"cargo sources cargo sources add cargo sources remove cargo sources rename cargo sources set-url cargo sources set-method If no arguments are supplied, list all sources (but not their packages). Commands: add Add a source. The source method will be guessed from the URL. remove Remove a source. rename Rename a source. set-url Change the URL for a source. set-method Change the method for a source."); } fn main(argv: ~[~str]) { let o = build_cargo_options(argv); if vec::len(o.free) < 2u { cmd_usage(); return; } if o.help { match o.free[1] { ~"init" => cmd_usage_init(), ~"install" => cmd_usage_install(), ~"uninstall" => cmd_usage_uninstall(), ~"list" => cmd_usage_list(), ~"search" => cmd_usage_search(), ~"sources" => cmd_usage_sources(), _ => cmd_usage() } return; } if o.free[1] == ~"usage" { cmd_usage(); return; } let mut c = configure(o); let home = c.root; let first_time = os::path_exists(&home.push("sources.json")); if !first_time && o.free[1] != ~"init" { cmd_init(&c); // FIXME (#2662): shouldn't need to reconfigure c = configure(o); } let c = &move c; match o.free[1] { ~"init" => cmd_init(c), ~"install" => cmd_install(c), ~"uninstall" => cmd_uninstall(c), ~"list" => cmd_list(c), ~"search" => cmd_search(c), ~"sources" => cmd_sources(c), _ => cmd_usage() } dump_cache(c); dump_sources(c); }