// 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 result; import std::{map, json, tempfile, term, sort, getopts}; import map::hashmap; import str; import vec; import getopts::{optflag, optopt, opt_present}; type package = { mut name: str, mut uuid: str, mut url: str, mut method: str, mut description: str, mut ref: option, mut tags: [str], mut versions: [(str, str)] }; type local_package = { mut name: str, mut metaname: str, mut version: str, mut files: [str] }; type source = { mut name: str, mut url: str, mut method: str, mut key: option, mut keyfp: option, mut packages: [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 = { mut name: str, mut vers: str, mut uuid: str, mut desc: option, mut sigs: option, mut crate_type: option, mut 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: 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: 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 cm = codemap::new_codemap(); let handler = diagnostic::mk_handler(none); let sess = @{ cm: cm, mut next_id: 1, span_diagnostic: diagnostic::mk_span_handler(handler, cm), mut chpos: 0u, mut byte_pos: 0u }; 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" { } _ { e.deps += [query]; } } } _ { } } } fn goto_item(_e: env, _i: @ast::item) { } let e = @{ mut deps: [] }; let v = visit::mk_simple_visitor(@{ visit_view_item: bind goto_view_item(e, _), visit_item: bind goto_item(e, _), 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({ mut name: name0, mut vers: vers0, mut uuid: uuid0, mut desc: desc, mut sigs: sigs, mut crate_type: crate_type, mut 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 { mut name: name, mut url: url, mut method: method, mut key: key, mut keyfp: keyfp, mut packages: [] }; } _ { 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:%u:%u: %s", filename, e.line, e.col, e.msg); } } } 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 = { mut name: name, mut uuid: uuid, mut url: url, mut method: method, mut description: description, mut ref: ref, mut tags: tags, mut versions: [] }; for src.packages.each { |pkg| if pkg.uuid == uuid { pkg.name = newpkg.name; pkg.uuid = newpkg.uuid; pkg.url = newpkg.url; pkg.method = newpkg.method; pkg.description = newpkg.description; pkg.ref = newpkg.ref; pkg.tags = newpkg.tags; pkg.versions = newpkg.versions; log(debug, " updated package: " + src.name + "/" + name); ret; } } 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:%u:%u: %s", src.name, e.line, e.col, e.msg)); } }; } 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:%u:%u: %s", src.name, e.line, e.col, e.msg)); } }; } 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(home, "work"), sourcedir: path::connect(home, "sources"), sources: sources, mut current_install: "", dep_cache: dep_cache, opts: opts }; need_dir(c.root); need_dir(c.sourcedir); need_dir(c.workdir); need_dir(c.installdir); 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") { 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: needs stronger pattern matching // FIXME: 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: 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 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 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 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 = " >> "; 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 { 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", [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 v.key { some(key) { chash.insert("key", json::string(key)); } _ {} } alt 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", v.name, v.url, 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, { mut name: name, mut url: url, mut method: assume_source_method(url), mut key: none, mut keyfp: none, mut packages: [] }); 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: 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); }