// cargo.rs - Rust package manager import syntax::{ast, codemap, parse, visit, attr}; import syntax::diagnostic::span_handler; import codemap::span; import rustc::metadata::filesearch::{get_cargo_root, get_cargo_root_nearest, get_cargo_sysroot, libdir}; import syntax::diagnostic; import result::{ok, err}; import io::writer_util; import std::{map, json, tempfile, term, sort, getopts}; import map::hashmap; import json::to_str; import getopts::{optflag, optopt, opt_present}; type package = { name: str, uuid: str, url: str, method: str, description: str, ref: option, tags: ~[str], versions: ~[(str, str)] }; type local_package = { name: str, metaname: str, version: str, files: ~[str] }; type source = @{ name: str, mut url: str, mut method: str, mut key: option, mut keyfp: option, mut packages: ~[mut package] }; type cargo = { pgp: bool, root: str, installdir: str, bindir: str, libdir: str, workdir: str, sourcedir: str, sources: map::hashmap, mut current_install: str, dep_cache: map::hashmap, opts: options }; type crate = { name: str, vers: str, uuid: str, desc: option, sigs: option, crate_type: option, deps: ~[str] }; type options = { test: bool, mode: mode, free: ~[str], help: bool, }; enum mode { system_mode, user_mode, local_mode } 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) { ret false; } alt 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 { ret true; } } ret 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(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 alt str::find_str(u, "://") { option::some(i) { 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) { ret "git"; } if str::starts_with(url, "file://") || os::path_exists(url) { ret "file"; } "curl" } fn load_link(mis: ~[@ast::meta_item]) -> (option, option, option) { let mut name = none; let mut vers = none; let mut uuid = none; for mis.each |a| { alt a.node { ast::meta_name_value(v, {node: ast::lit_str(s), span: _}) { alt *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: str) -> 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| { alt a.node.value.node { ast::meta_name_value(v, {node: ast::lit_str(s), span: _}) { alt *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(e: env, i: @ast::view_item) { alt i.node { ast::view_item_use(ident, metas, id) { 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", *ident)] } else { metas }; let mut attr_name = ident; let mut attr_vers = ""; let mut attr_from = ""; for m.each |item| { alt attr::get_meta_item_value_str(item) { some(value) { let name = attr::get_meta_item_name(item); alt *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) { *attr_name + "@" + attr_vers } else { *attr_name } }; alt *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(e, a), visit_item: |a| goto_item(e, a), with *visit::default_simple_visitor() }); visit::visit_crate(*c, (), v); let deps = copy e.deps; alt (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 }) } _ { ret 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: str) { if os::path_is_dir(s) { ret; } if !os::make_dir(s, 493_i32 /* oct: 755 */) { fail #fmt["can't make_dir %s", s]; } } 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); } alt j { json::dict(j) { let mut url = alt j.find("url") { some(json::string(u)) { *u } _ { fail "needed 'url' field in source"; } }; let method = alt j.find("method") { some(json::string(u)) { *u } _ { assume_source_method(url) } }; let key = alt j.find("key") { some(json::string(u)) { some(*u) } _ { none } }; let keyfp = alt j.find("keyfp") { some(json::string(u)) { some(*u) } _ { none } }; if method == "file" { url = os::make_absolute(url); } ret @{ name: name, mut url: url, mut method: method, mut key: key, mut keyfp: keyfp, mut packages: ~[mut] }; } _ { fail "needed dict value in source"; } }; } fn try_parse_sources(filename: str, sources: map::hashmap) { if !os::path_exists(filename) { ret; } let c = io::read_whole_file_str(filename); alt 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, e.to_str()); } } } fn load_one_source_package(src: source, p: map::hashmap) { let name = alt 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)"); ret; } *n } _ { warn("malformed source json: " + src.name + " (missing name)"); ret; } }; let uuid = alt p.find("uuid") { some(json::string(n)) { if !is_uuid(*n) { warn("malformed source json: " + src.name + ", '" + *n + "'"+ " is an invalid uuid"); ret; } *n } _ { warn("malformed source json: " + src.name + " (missing uuid)"); ret; } }; let url = alt p.find("url") { some(json::string(n)) { *n } _ { warn("malformed source json: " + src.name + " (missing url)"); ret; } }; let method = alt p.find("method") { some(json::string(n)) { *n } _ { warn("malformed source json: " + src.name + " (missing method)"); ret; } }; let ref = alt p.find("ref") { some(json::string(n)) { some(*n) } _ { none } }; let mut tags = ~[]; alt p.find("tags") { some(json::list(js)) { for (*js).each |j| { alt j { json::string(j) { vec::grow(tags, 1u, *j); } _ { } } } } _ { } } let description = alt p.find("description") { some(json::string(n)) { *n } _ { warn("malformed source json: " + src.name + " (missing description)"); ret; } }; let newpkg = { name: name, uuid: uuid, url: url, method: method, description: description, ref: ref, tags: tags, versions: ~[] }; alt src.packages.position(|pkg| pkg.uuid == uuid ) { some(idx) { src.packages[idx] = newpkg; log(debug, " updated package: " + src.name + "/" + name); } none { vec::grow(src.packages, 1u, newpkg); } } log(debug, " loaded package: " + src.name + "/" + name); } fn load_source_info(c: cargo, src: source) { let dir = path::connect(c.sourcedir, src.name); let srcfile = path::connect(dir, "source.json"); if !os::path_exists(srcfile) { ret; } let srcstr = io::read_whole_file_str(srcfile); alt 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 = path::connect(c.sourcedir, src.name); let pkgfile = path::connect(dir, "packages.json"); if !os::path_exists(pkgfile) { ret; } let pkgstr = io::read_whole_file_str(pkgfile); alt json::from_str(result::get(pkgstr)) { ok(json::list(js)) { for (*js).each |j| { alt 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 match = alt getopts::getopts(argv, opts()) { result::ok(m) { m } result::err(f) { fail #fmt["%s", getopts::fail_str(f)]; } }; let test = opt_present(match, "test"); let G = opt_present(match, "G"); let g = opt_present(match, "g"); let help = opt_present(match, "h") || opt_present(match, "help"); let len = vec::len(match.free); let is_install = len > 1u && match.free[1] == "install"; let is_uninstall = len > 1u && match.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: match.free, help: help} } fn configure(opts: options) -> cargo { let home = alt get_cargo_root() { ok(home) { home } err(_err) { result::get(get_cargo_sysroot()) } }; let get_cargo_dir = alt 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::str_hash(); try_parse_sources(path::connect(home, "sources.json"), sources); try_parse_sources(path::connect(home, "local-sources.json"), sources); let dep_cache = map::str_hash(); let mut c = { pgp: pgp::supported(), root: home, installdir: p, bindir: path::connect(p, "bin"), libdir: path::connect(p, "lib"), workdir: path::connect(p, "work"), sourcedir: path::connect(home, "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| { // FIXME (#2280): this temporary shouldn't be // necessary, but seems to be, for borrowing. let pks = copy v.packages; for vec::each(pks) |p| { b(v, p); } } } // Runs all programs in directory fn run_programs(buildpath: str) { let newv = os::list_dir_path(buildpath); for newv.each |ct| { run::run_program(ct, ~[]); } } // Runs rustc in with the given flags // and returns fn run_in_buildpath(what: str, path: str, subdir: str, cf: str, extra_flags: ~[str]) -> option { let buildpath = path::connect(path, subdir); need_dir(buildpath); #debug("%s: %s -> %s", what, cf, buildpath); let p = run::program_output(rustc_sysroot(), ~["--out-dir", buildpath, cf] + extra_flags); if p.status != 0 { error(#fmt["rustc failed: %d\n%s\n%s", p.status, p.err, p.out]); ret none; } some(buildpath) } fn test_one_crate(_c: cargo, path: str, cf: str) { let buildpath = alt run_in_buildpath("testing", path, "/test", cf, ~[ "--test"]) { none { ret; } some(bp) { bp } }; run_programs(buildpath); } fn install_one_crate(c: cargo, path: str, cf: str) { let buildpath = alt run_in_buildpath("installing", path, "/build", cf, ~[]) { none { ret; } 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, exec_suffix)) || (exec_suffix == "" && !str::starts_with(path::basename(ct), "lib")) { #debug(" bin: %s", ct); install_to_dir(ct, c.bindir); if c.opts.mode == system_mode { // TODO: 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); install_to_dir(ct, c.libdir); } } } fn rustc_sysroot() -> str { alt os::self_exe_path() { some(path) { let path = ~[path, "..", "bin", "rustc"]; check vec::is_not_empty(path); let rustc = path::normalize(path::connect_many(path)); #debug(" rustc: %s", rustc); rustc } none { "rustc" } } } fn install_source(c: cargo, path: str) { #debug("source: %s", path); os::change_dir(path); let mut cratefiles = ~[]; for os::walk_dir(".") |p| { if str::ends_with(p, ".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| { alt load_crate(cf) { none { cont; } some(crate) { for crate.deps.each |query| { // TODO: handle cyclic dependencies let wd_base = c.workdir + path::path_sep(); let wd = alt tempfile::mkdtemp(wd_base, "") { some(wd) { wd } none { fail #fmt("needed temp dir: %s", wd_base); } }; 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: str, url: str, ref: option) { run::program_output("git", ~["clone", url, wd]); if option::is_some(ref) { let r = option::get(ref); os::change_dir(wd); run::run_program("git", ~["checkout", r]); } install_source(c, wd); } fn install_curl(c: cargo, wd: str, url: str) { let tarpath = path::connect(wd, "pkg.tar"); let p = run::program_output("curl", ~["-f", "-s", "-o", tarpath, 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, "-f", tarpath]); install_source(c, wd); } fn install_file(c: cargo, wd: str, path: str) { run::program_output("tar", ~["-x", "--strip-components=1", "-C", wd, "-f", path]); install_source(c, wd); } fn install_package(c: cargo, src: str, wd: str, pkg: package) { let url = copy pkg.url; let method = alt pkg.method { "git" { "git" } "file" { "file" } _ { "curl" } }; info(#fmt["installing %s/%s via %s...", src, pkg.name, method]); alt method { "git" { install_git(c, wd, url, copy pkg.ref); } "file" { install_file(c, wd, 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`"); ret; } fallback(); } fn install_uuid(c: cargo, wd: str, 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); ret; } else if vec::len(ps) == 0u { cargo_suggestion(c, || { error("can't find package: " + uuid); }); ret; } 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: str, 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); ret; } else if vec::len(ps) == 0u { cargo_suggestion(c, || { error("can't find package: " + name); }); ret; } 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: str, src: str, uuid: str) { alt c.sources.find(src) { some(s) { let packages = copy s.packages; if vec::any(packages, |p| { if p.uuid == uuid { install_package(c, src, wd, p); true } else { false } }) { ret; } } _ { } } error("can't find package: " + src + "/" + uuid); } fn install_named_specific(c: cargo, wd: str, src: str, name: str) { alt c.sources.find(src) { some(s) { let packages = copy s.packages; if vec::any(packages, |p| { if p.name == name { install_package(c, src, wd, p); true } else { false } }) { ret; } } _ { } } error("can't find package: " + src + "/" + name); } fn cmd_uninstall(c: cargo) { if vec::len(c.opts.free) < 3u { cmd_usage(); ret; } 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) if is_uuid(target) { for os::list_dir(lib).each |file| { alt str::find_str(file, "-" + target + "-") { some(idx) { let full = path::normalize(path::connect(lib, file)); if os::remove_file(full) { info("uninstalled: '" + full + "'"); } else { error("could not uninstall: '" + full + "'"); } ret; } none { cont; } } } error("can't find package with uuid: " + target); } else { for os::list_dir(lib).each |file| { alt str::find_str(file, "lib" + target + "-") { some(idx) { let full = path::normalize(path::connect(lib, file)); if os::remove_file(full) { info("uninstalled: '" + full + "'"); } else { error("could not uninstall: '" + full + "'"); } ret; } none { cont; } } } for os::list_dir(bin).each |file| { alt str::find_str(file, target) { some(idx) { let full = path::normalize(path::connect(bin, file)); if os::remove_file(full) { info("uninstalled: '" + full + "'"); } else { error("could not uninstall: '" + full + "'"); } ret; } none { cont; } } } error("can't find package with name: " + target); } } fn install_query(c: cargo, wd: str, target: str) { alt c.dep_cache.find(target) { some(inst) { if inst { ret; } } none {} } c.dep_cache.insert(target, true); if is_archive_path(target) { install_file(c, wd, target); ret; } else if is_git_url(target) { let ref = if c.opts.free.len() >= 4u { some(c.opts.free[3u]) } else { none }; install_git(c, wd, target, ref); } else if !valid_pkg_name(target) && has_archive_extension(target) { install_curl(c, wd, target); ret; } else { let mut ps = copy target; alt 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 cmd_install(c: cargo) unsafe { let wd_base = c.workdir + path::path_sep(); let wd = alt tempfile::mkdtemp(wd_base, "") { some(wd) { wd } none { fail #fmt("needed temp dir: %s", wd_base); } }; if vec::len(c.opts.free) == 2u { let cwd = os::getcwd(); let status = run::run_program("cp", ~["-R", cwd, wd]); if status != 0 { fail #fmt("could not copy directory: %s", cwd); } install_source(c, wd); ret; } sync(c); let query = c.opts.free[2]; c.current_install = copy query; install_query(c, wd, copy 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: str, src: source) -> bool { let name = src.name; let srcfile = path::connect(dir, "source.json.new"); let destsrcfile = path::connect(dir, "source.json"); let pkgfile = path::connect(dir, "packages.json.new"); let destpkgfile = path::connect(dir, "packages.json"); let keyfile = path::connect(dir, "key.gpg"); let srcsigfile = path::connect(dir, "source.json.sig"); let sigfile = path::connect(dir, "packages.json.sig"); let url = src.url; let mut has_src_file = false; if !os::copy_file(path::connect(url, "packages.json"), pkgfile) { error(#fmt["fetch for source %s (url %s) failed", name, url]); ret false; } if os::copy_file(path::connect(url, "source.json"), srcfile) { has_src_file = false; } os::copy_file(path::connect(url, "source.json.sig"), srcsigfile); os::copy_file(path::connect(url, "packages.json.sig"), sigfile); alt copy src.key { some(u) { let p = run::program_output("curl", ~["-f", "-s", "-o", keyfile, u]); if p.status != 0 { error(#fmt["fetch for source %s (key %s) failed", name, u]); ret false; } pgp::add(c.root, keyfile); } _ { } } alt (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]); ret 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]); ret 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]); ret true; } fn sync_one_git(c: cargo, dir: str, src: source) -> bool { let name = src.name; let srcfile = path::connect(dir, "source.json"); let pkgfile = path::connect(dir, "packages.json"); let keyfile = path::connect(dir, "key.gpg"); let srcsigfile = path::connect(dir, "source.json.sig"); let sigfile = path::connect(dir, "packages.json.sig"); let url = src.url; fn rollback(name: str, dir: str, 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(path::connect(dir, ".git")) { let p = run::program_output("git", ~["clone", url, dir]); if p.status != 0 { error(#fmt["fetch for source %s (url %s) failed", name, url]); ret false; } } else { if !os::change_dir(dir) { error(#fmt["fetch for source %s (url %s) failed", name, url]); ret false; } let p = run::program_output("git", ~["pull"]); if p.status != 0 { error(#fmt["fetch for source %s (url %s) failed", name, url]); ret false; } } let has_src_file = os::path_exists(srcfile); alt copy src.key { some(u) { let p = run::program_output("curl", ~["-f", "-s", "-o", keyfile, u]); if p.status != 0 { error(#fmt["fetch for source %s (key %s) failed", name, u]); rollback(name, dir, false); ret false; } pgp::add(c.root, keyfile); } _ { } } alt (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); ret 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); ret false; } } } _ {} } os::remove_file(keyfile); info(#fmt["synced source: %s", name]); ret true; } fn sync_one_curl(c: cargo, dir: str, src: source) -> bool { let name = src.name; let srcfile = path::connect(dir, "source.json.new"); let destsrcfile = path::connect(dir, "source.json"); let pkgfile = path::connect(dir, "packages.json.new"); let destpkgfile = path::connect(dir, "packages.json"); let keyfile = path::connect(dir, "key.gpg"); let srcsigfile = path::connect(dir, "source.json.sig"); let sigfile = path::connect(dir, "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, url]); if p.status != 0 { error(#fmt["fetch for source %s (url %s) failed", name, url]); ret false; } if smart { url = src.url + "/source.json"; let p = run::program_output("curl", ~["-f", "-s", "-o", srcfile, url]); if p.status == 0 { has_src_file = true; } } alt copy src.key { some(u) { let p = run::program_output("curl", ~["-f", "-s", "-o", keyfile, u]); if p.status != 0 { error(#fmt["fetch for source %s (key %s) failed", name, u]); ret false; } pgp::add(c.root, keyfile); } _ { } } alt (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, url]); if p.status != 0 { error(#fmt["fetch for source %s (sig %s) failed", name, url]); ret false; } let r = pgp::verify(c.root, pkgfile, sigfile, f); if !r { error(#fmt["signature verification failed for source %s", name]); ret false; } if smart && has_src_file { url = src.url + "/source.json.sig"; p = run::program_output("curl", ~["-f", "-s", "-o", srcsigfile, url]); if p.status != 0 { error(#fmt["fetch for source %s (sig %s) failed", name, url]); ret false; } let e = pgp::verify(c.root, srcfile, srcsigfile, f); if !e { error("signature verification failed for " + "source " + name); ret 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]); ret true; } fn sync_one(c: cargo, src: source) { let name = src.name; let dir = path::connect(c.sourcedir, name); info(#fmt["syncing source: %s...", name]); need_dir(dir); let result = alt 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 = path::connect(c.root, "sources.json.new"); let sigfile = path::connect(c.root, "sources.json.sig"); let destsrcfile = path::connect(c.root, "sources.json"); let p = run::program_output("curl", ~["-f", "-s", "-o", srcfile, srcurl]); if p.status != 0 { error(#fmt["fetch of sources.json failed: %s", p.out]); ret; } let p = run::program_output("curl", ~["-f", "-s", "-o", sigfile, sigurl]); if p.status != 0 { error(#fmt["fetch of sources.json.sig failed: %s", p.out]); ret; } let r = pgp::verify(c.root, srcfile, sigfile, pgp::signing_key_fp()); if !r { error(#fmt["signature verification failed for '%s'", srcfile]); ret; } copy_warn(srcfile, destsrcfile); os::remove_file(srcfile); os::remove_file(sigfile); info(#fmt["initialized .cargo in %s", c.root]); } 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(|a, b| a < b, copy s.packages); let l = vec::len(pks); print(io::with_str_writer(|writer| { let mut list = " >> "; do vec::iteri(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 { do vec::iter_between(c.opts.free, 2u, vec::len(c.opts.free)) |name| { if !valid_pkg_name(name) { error(#fmt("'%s' is an invalid source name", name)); } else { alt 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(); ret; } 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: str, destdir: str) { let newfile = path::connect(destdir, path::basename(srcfile)); let status = run::run_program("cp", ~["-r", srcfile, newfile]); if status == 0 { info(#fmt["installed: '%s'", newfile]); } else { error(#fmt["could not install: '%s'", newfile]); } } fn dump_cache(c: cargo) { need_dir(c.root); let out = path::connect(c.root, "cache.json"); let _root = json::dict(map::str_hash()); if os::path_exists(out) { copy_warn(out, path::connect(c.root, "cache.json.old")); } } fn dump_sources(c: cargo) { if c.sources.size() < 1u { ret; } need_dir(c.root); let out = path::connect(c.root, "sources.json"); if os::path_exists(out) { copy_warn(out, path::connect(c.root, "sources.json.old")); } alt io::buffered_file_writer(out) { result::ok(writer) { let hash = map::str_hash(); let root = json::dict(hash); for c.sources.each |k, v| { let chash = map::str_hash(); let child = json::dict(chash); chash.insert("url", json::string(@v.url)); chash.insert("method", json::string(@v.method)); alt copy v.key { some(key) { chash.insert("key", json::string(@key)); } _ {} } alt 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: str, destfile: str) { if !os::copy_file(srcfile, destfile) { warn(#fmt["copying %s to %s failed", srcfile, destfile]); } } fn cmd_sources(c: cargo) { if vec::len(c.opts.free) < 3u { for c.sources.each_value |v| { info(#fmt("%s (%s) via %s", copy v.name, copy v.url, copy v.method)); } ret; } let action = c.opts.free[2u]; alt 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(); ret; } 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)); ret; } alt c.sources.find(name) { some(source) { error(#fmt("source already exists: %s", name)); } none { c.sources.insert(name, @{ name: name, mut url: url, mut method: assume_source_method(url), mut key: none, mut keyfp: none, mut packages: ~[mut] }); info(#fmt("added source: %s", name)); } } } "remove" { if vec::len(c.opts.free) < 4u { cmd_usage(); ret; } let name = c.opts.free[3u]; if !valid_pkg_name(name) { error(#fmt("'%s' is an invalid source name", name)); ret; } alt c.sources.find(name) { some(source) { c.sources.remove(name); info(#fmt("removed source: %s", name)); } none { error(#fmt("no such source: %s", name)); } } } "set-url" { if vec::len(c.opts.free) < 5u { cmd_usage(); ret; } 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)); ret; } alt 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(); ret; } 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)); ret; } alt c.sources.find(name) { some(source) { let old = copy source.method; source.method = alt 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(); ret; } 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)); ret; } if !valid_pkg_name(newn) { error(#fmt("'%s' is an invalid source name", newn)); ret; } alt 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(); ret; } if o.help { alt 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(); } } ret; } if o.free[1] == "usage" { cmd_usage(); ret; } let mut c = configure(o); let home = c.root; let first_time = os::path_exists(path::connect(home, "sources.json")); if !first_time && o.free[1] != "init" { cmd_init(c); // FIXME (#2662): shouldn't need to reconfigure c = configure(o); } alt 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); }