// Copyright 2012-2013 The Rust Project Developers. See the COPYRIGHT // file at the top-level directory of this distribution and at // http://rust-lang.org/COPYRIGHT. // // Licensed under the Apache License, Version 2.0 or the MIT license // , at your // option. This file may not be copied, modified, or distributed // except according to those terms. // rustpkg - a purely function package manager and build system #[link(name = "rustpkg", vers = "0.6", uuid = "25de5e6e-279e-4a20-845c-4cabae92daaf", url = "https://github.com/mozilla/rust/tree/master/src/librustpkg")]; #[license = "MIT/ASL2"]; #[crate_type = "lib"]; #[no_core]; #[allow(vecs_implicitly_copyable, non_implicitly_copyable_typarams)]; extern mod core(vers = "0.6"); extern mod std(vers = "0.6"); extern mod rustc(vers = "0.6"); extern mod syntax(vers = "0.6"); use core::*; pub use core::path::Path; use core::container::Map; use core::hashmap::HashMap; use core::io::WriterUtil; use rustc::driver::{driver, session}; use rustc::metadata::filesearch; use std::net::url; use std::{getopts}; use syntax::{ast, diagnostic}; use util::{ExitCode, Pkg, PkgId}; mod usage; mod util; /// A PkgScript represents user-supplied custom logic for /// special build hooks. This only exists for packages with /// an explicit package script. struct PkgScript { /// Uniquely identifies this package id: PkgId, // Used to have this field: deps: ~[(~str, Option<~str>)] // but I think it shouldn't be stored here /// The contents of the package script: either a file path, /// or a string containing the text of the input input: driver::input, /// The session to use *only* for compiling the custom /// build script sess: session::Session, /// The config for compiling the custom build script cfg: ast::crate_cfg, /// The crate for the custom build script crate: @ast::crate, /// Directory in which to store build output build_dir: Path } impl PkgScript { /// Given the path name for a package script /// and a package ID, parse the package script into /// a PkgScript that we can then execute fn parse(script: Path, id: PkgId) -> PkgScript { // Get the executable name that was invoked let binary = os::args()[0]; // Build the rustc session data structures to pass // to the compiler let options = @session::options { binary: binary, crate_type: session::bin_crate, .. *session::basic_options() }; let input = driver::file_input(script); let sess = driver::build_session(options, diagnostic::emit); let cfg = driver::build_configuration(sess, binary, input); let (crate, _) = driver::compile_upto(sess, cfg, input, driver::cu_parse, None); let work_dir = dest_dir(id); debug!("Returning package script with id %?", id); PkgScript { id: id, input: input, sess: sess, cfg: cfg, crate: crate, build_dir: work_dir } } /// Run the contents of this package script, where /// is the command to pass to it (e.g., "build", "clean", "install") /// Returns a pair of an exit code and list of configs (obtained by /// calling the package script's configs() function if it exists // FIXME (#4432): Use workcache to only compile the script when changed fn run_custom(&self, what: ~str) -> (~[~str], ExitCode) { debug!("run_custom: %s", what); let sess = self.sess; debug!("Working directory = %s", self.build_dir.to_str()); // Collect together any user-defined commands in the package script let crate = util::ready_crate(sess, self.crate); debug!("Building output filenames with script name %s", driver::source_name(self.input)); match filesearch::get_rustpkg_sysroot() { Ok(r) => { let root = r.pop().pop().pop().pop(); // :-\ debug!("Root is %s, calling compile_rest", root.to_str()); util::compile_crate_from_input(self.input, Some(self.build_dir), sess, Some(crate), os::args()[0]); let exe = self.build_dir.push(~"pkg" + util::exe_suffix()); debug!("Running program: %s %s %s", exe.to_str(), root.to_str(), what); let status = run::run_program(exe.to_str(), ~[root.to_str(), what]); if status != 0 { return (~[], status); } else { debug!("Running program (configs): %s %s %s", exe.to_str(), root.to_str(), ~"configs"); let output = run::program_output(exe.to_str(), ~[root.to_str(), ~"configs"]); // Run the configs() function to get the configs let mut cfgs = ~[]; for str::each_word(output.out) |w| { cfgs.push(w.to_owned()); } (cfgs, output.status) } } Err(e) => { fail!(fmt!("Running package script, couldn't find rustpkg sysroot (%s)", e)) } } } fn hash(&self) -> ~str { self.id.hash() } } struct Ctx { // I'm not sure what this is for json: bool, // Cache of hashes of things already installed // though I'm not sure why the value is a bool dep_cache: @mut HashMap<~str, bool>, } /// Returns the output directory to use. /// Right now is always the default, should /// support changing it. fn dest_dir(pkgid: PkgId) -> Path { default_dest_dir(&pkgid.path).expect( ~"couldn't make default dir?!") } /// Returns the default output directory for compilation. /// Creates that directory if it doesn't exist. fn default_dest_dir(pkg_dir: &Path) -> Option { use core::libc::consts::os::posix88::{S_IRUSR, S_IWUSR, S_IXUSR}; // For now: assumes that pkg_dir exists and is relative // to the CWD. Change this later when we do path searching. let rslt = pkg_dir.push("build"); let is_dir = os::path_is_dir(&rslt); if os::path_exists(&rslt) { if is_dir { Some(rslt) } else { util::error(fmt!("%s is not a directory", rslt.to_str())); None } } else { // Create it if os::make_dir(&rslt, (S_IRUSR | S_IWUSR | S_IXUSR) as i32) { Some(rslt) } else { util::error(fmt!("Could not create directory %s", rslt.to_str())); None // ??? should probably use conditions } } } impl Ctx { fn run(&self, cmd: ~str, args: ~[~str]) { let root = util::root(); util::need_dir(&root); util::need_dir(&root.push(~"work")); util::need_dir(&root.push(~"lib")); util::need_dir(&root.push(~"bin")); util::need_dir(&root.push(~"tmp")); fn sep_name_vers(in: ~str) -> (Option<~str>, Option<~str>) { let mut name = None; let mut vers = None; for str::each_split_char(in, '@') |s| { if name.is_none() { name = Some(s.to_owned()); } else if vers.is_none() { vers = Some(s.to_owned()); } else { break; } } (name, vers) } match cmd { ~"build" => { if args.len() < 1 { return usage::build(); } // The package id is presumed to be the first command-line // argument let pkgid = PkgId::new(args[0]); // Should allow the build directory to be configured. // Right now it's always the "build" subdirectory in // the package directory let dst_dir = dest_dir(pkgid); debug!("Destination dir = %s", dst_dir.to_str()); // Right now, we assume the pkgid path is a valid dir // relative to the CWD. In the future, we should search // paths let cwd = os::getcwd().normalize(); debug!("Current working directory = %?", cwd); // Find crates inside the workspace let mut src = PkgSrc::new(&cwd, &dst_dir, &pkgid); debug!("Package src = %?", src); src.find_crates(); // Is there custom build logic? If so, use it let pkg_src_dir = cwd.push_rel(&pkgid.path); debug!("Package source directory = %s", pkg_src_dir.to_str()); let cfgs = match src.package_script_option(&pkg_src_dir) { Some(package_script_path) => { let pscript = PkgScript::parse(package_script_path, pkgid); // Limited right now -- we're only running the post_build // hook and probably fail otherwise // also post_build should be called pre_build let (cfgs, hook_result) = pscript.run_custom(~"post_build"); debug!("Command return code = %?", hook_result); if hook_result != 0 { fail!(fmt!("Error running custom build command")) } // otherwise, the package script succeeded cfgs } None => { debug!("No package script, continuing"); ~[] } }; src.build(&dst_dir, cfgs); } ~"clean" => { self.clean(); } ~"do" => { if args.len() < 2 { return usage::do_cmd(); } if !self.do_cmd(args[0], args[1]) { fail!(~"a command failed!"); } } ~"info" => { self.info(); } ~"install" => { self.install(if args.len() >= 1 { Some(args[0]) } else { None }, if args.len() >= 2 { Some(args[1]) } else { None }, false); } ~"prefer" => { if args.len() < 1 { return usage::uninstall(); } let (name, vers) = sep_name_vers(args[0]); self.prefer(name.get(), vers); } ~"test" => { self.test(); } ~"uninstall" => { if args.len() < 1 { return usage::uninstall(); } let (name, vers) = sep_name_vers(args[0]); self.uninstall(name.get(), vers); } ~"unprefer" => { if args.len() < 1 { return usage::uninstall(); } let (name, vers) = sep_name_vers(args[0]); self.unprefer(name.get(), vers); } _ => fail!(~"reached an unhandled command") } } fn do_cmd(&self, cmd: ~str, pkgname: ~str) -> bool { match cmd { ~"build" | ~"test" => { util::error(~"that command cannot be manually called"); return false; } _ => {} } let cwd = &os::getcwd(); let pkgid = PkgId::new(pkgname); // Always use the "build" subdirectory of the package dir, // but we should allow this to be configured let dst_dir = dest_dir(pkgid); let mut src = PkgSrc::new(cwd, &dst_dir, &pkgid); match src.package_script_option(cwd) { Some(script_path) => { let script = PkgScript::parse(script_path, pkgid); let (_, status) = script.run_custom(cmd); // Ignore cfgs? if status == 42 { // ??? util::error(~"no fns are listening for that cmd"); return false; } status == 0 } None => { util::error(fmt!("invoked `do`, but there is no package script in %s", cwd.to_str())); false } } } fn build(&self, _dir: &Path, _verbose: bool, _opt: bool, _test: bool) -> Option { // either not needed anymore, // or needed only when we don't have a package script. Not sure which one. fail!(); } fn compile(&self, _crate: &Path, _dir: &Path, _flags: ~[~str], _cfgs: ~[~str], _opt: bool, _test: bool) -> bool { // What's the difference between build and compile? fail!(~"compile not yet implemented"); } fn clean(&self) -> bool { // stub fail!(); } fn info(&self) { // stub fail!(); } fn install(&self, url: Option<~str>, target: Option<~str>, cache: bool) -> bool { let dir = match url { None => { util::note(~"installing from the cwd"); os::getcwd() } Some(url) => { let hash = util::hash(if !target.is_none() { url + target.get() } else { url }); if self.dep_cache.contains_key(&hash) { util::warn(~"already installed dep this run"); return true; } self.dep_cache.insert(hash, true); let dir = util::root().push(~"tmp").push(hash); if cache && os::path_exists(&dir) { return true; } if !self.fetch(&dir, url, target) { return false; } dir } }; let script = match self.build(&dir, false, true, false) { Some(script) => script, None => { return false; } }; let work_dir = script.build_dir; let from_bin_dir = work_dir.push(~"bin"); let from_lib_dir = work_dir.push(~"lib"); let to_bin_dir = util::root().push(~"bin"); let to_lib_dir = util::root().push(~"lib"); let mut bins = ~[]; let mut libs = ~[]; for os::walk_dir(&from_bin_dir) |bin| { let to = to_bin_dir.push_rel(&bin.file_path()); os::copy_file(bin, &to); bins.push(to.to_str()); } for os::walk_dir(&from_lib_dir) |lib| { let to = to_lib_dir.push_rel(&lib.file_path()); os::copy_file(lib, &to); libs.push(to.to_str()); } let package = Pkg { id: script.id, bins: bins, libs: libs }; util::note(fmt!("installed %s", script.id.to_str())); util::add_pkg(&package); true } fn fetch(&self, dir: &Path, url: ~str, target: Option<~str>) -> bool { let url = if str::find_str(url, "://").is_none() { ~"http://" + url } else { url }; let url = match url::from_str(url) { result::Ok(url) => url, result::Err(err) => { util::error(fmt!("failed parsing %s", err.to_lower())); return false; } }; let str = url.to_str(); match Path(url.path).filetype() { Some(ext) => { if ext == ~".git" { return self.fetch_git(dir, str, target); } } None => {} } match url.scheme { ~"git" => self.fetch_git(dir, str, target), ~"http" | ~"ftp" | ~"file" => self.fetch_curl(dir, str), _ => { util::warn(~"unknown url scheme to fetch, using curl"); self.fetch_curl(dir, str) } } } fn fetch_curl(&self, dir: &Path, url: ~str) -> bool { util::note(fmt!("fetching from %s using curl", url)); let tar = dir.dir_path().push(&dir.file_path().to_str() + ~".tar"); if run::program_output(~"curl", ~[~"-f", ~"-s", ~"-o", tar.to_str(), url]).status != 0 { util::error(~"fetching failed: downloading using curl failed"); return false; } if run::program_output(~"tar", ~[~"-x", ~"--strip-components=1", ~"-C", dir.to_str(), ~"-f", tar.to_str()]).status != 0 { util::error(~"fetching failed: extracting using tar failed" + ~"(is it a valid tar archive?)"); return false; } true } fn fetch_git(&self, dir: &Path, url: ~str, target: Option<~str>) -> bool { util::note(fmt!("fetching from %s using git", url)); // Git can't clone into a non-empty directory util::remove_dir_r(dir); if run::program_output(~"git", ~[~"clone", url, dir.to_str()]).status != 0 { util::error(~"fetching failed: can't clone repository"); return false; } if !target.is_none() { let mut success = true; do util::temp_change_dir(dir) { success = run::program_output(~"git", ~[~"checkout", target.get()]).status != 0 } if !success { util::error(~"fetching failed: can't checkout target"); return false; } } true } fn prefer(&self, id: ~str, vers: Option<~str>) -> bool { let package = match util::get_pkg(id, vers) { result::Ok(package) => package, result::Err(err) => { util::error(err); return false; } }; let name = package.id.path.to_str(); // ??? util::note(fmt!("preferring %s v%s", name, package.id.version.to_str())); let bin_dir = util::root().push(~"bin"); for package.bins.each |&bin| { let path = Path(bin); let mut name = None; for str::each_split_char(path.file_path().to_str(), '-') |s| { name = Some(s.to_owned()); break; } let out = bin_dir.push(name.unwrap()); util::link_exe(&path, &out); util::note(fmt!("linked %s", out.to_str())); } util::note(fmt!("preferred %s v%s", name, package.id.version.to_str())); true } fn test(&self) -> bool { let script = match self.build(&os::getcwd(), false, false, true) { Some(script) => script, None => { return false; } }; // To do util::note(fmt!("Would test %s, but this is a dry run", script.id.to_str())); false } fn uninstall(&self, _id: ~str, _vers: Option<~str>) -> bool { fail!(~"uninstall not yet implemented"); } fn unprefer(&self, _id: ~str, _vers: Option<~str>) -> bool { fail!(~"unprefer not yet implemented"); } } pub fn main() { io::println("WARNING: The Rust package manager is experimental and may be unstable"); let args = os::args(); let opts = ~[getopts::optflag(~"h"), getopts::optflag(~"help"), getopts::optflag(~"j"), getopts::optflag(~"json"), getopts::optmulti(~"c"), getopts::optmulti(~"cfg")]; let matches = &match getopts::getopts(args, opts) { result::Ok(m) => m, result::Err(f) => { util::error(fmt!("%s", getopts::fail_str(f))); return; } }; let help = getopts::opt_present(matches, ~"h") || getopts::opt_present(matches, ~"help"); let json = getopts::opt_present(matches, ~"j") || getopts::opt_present(matches, ~"json"); let mut args = copy matches.free; args.shift(); if (args.len() < 1) { return usage::general(); } let cmd = args.shift(); if !util::is_cmd(cmd) { return usage::general(); } else if help { return match cmd { ~"build" => usage::build(), ~"clean" => usage::clean(), ~"do" => usage::do_cmd(), ~"info" => usage::info(), ~"install" => usage::install(), ~"prefer" => usage::prefer(), ~"test" => usage::test(), ~"uninstall" => usage::uninstall(), ~"unprefer" => usage::unprefer(), _ => usage::general() }; } Ctx { json: json, dep_cache: @mut HashMap::new() }.run(cmd, args); } /// A crate is a unit of Rust code to be compiled into a binary or library pub struct Crate { file: Path, flags: ~[~str], cfgs: ~[~str] } pub struct Listener { cmds: ~[~str], cb: ~fn() } pub fn run(listeners: ~[Listener]) { let rcmd = os::args()[2]; let mut found = false; for listeners.each |listener| { for listener.cmds.each |&cmd| { if cmd == rcmd { (listener.cb)(); found = true; break; } } } if !found { os::set_exit_status(42); } } pub impl Crate { fn new(p: &Path) -> Crate { Crate { file: copy *p, flags: ~[], cfgs: ~[] } } fn flag(&self, flag: ~str) -> Crate { Crate { flags: vec::append(copy self.flags, ~[flag]), .. copy *self } } fn flags(&self, flags: ~[~str]) -> Crate { Crate { flags: vec::append(copy self.flags, flags), .. copy *self } } fn cfg(&self, cfg: ~str) -> Crate { Crate { cfgs: vec::append(copy self.cfgs, ~[cfg]), .. copy *self } } fn cfgs(&self, cfgs: ~[~str]) -> Crate { Crate { cfgs: vec::append(copy self.cfgs, cfgs), .. copy *self } } } /** * Get the working directory of the package script. * Assumes that the package script has been compiled * in is the working directory. */ pub fn work_dir() -> Path { os::self_exe_path().get() } /** * Get the source directory of the package (i.e. * where the crates are located). Assumes * that the cwd is changed to it before * running this executable. */ pub fn src_dir() -> Path { os::getcwd() } condition! { bad_pkg_id: (super::Path, ~str) -> ::util::PkgId; } // An enumeration of the unpacked source of a package workspace. // This contains a list of files found in the source workspace. pub struct PkgSrc { root: Path, // root of where the package source code lives dst_dir: Path, // directory where we will put the compiled output id: PkgId, libs: ~[Crate], mains: ~[Crate], tests: ~[Crate], benchs: ~[Crate], } condition! { bad_path: (super::Path, ~str) -> super::Path; } condition! { build_err: (~str) -> (); } impl PkgSrc { fn new(src_dir: &Path, dst_dir: &Path, id: &PkgId) -> PkgSrc { PkgSrc { root: copy *src_dir, dst_dir: copy *dst_dir, id: copy *id, libs: ~[], mains: ~[], tests: ~[], benchs: ~[] } } fn check_dir(&self) -> Path { use bad_path::cond; debug!("Pushing onto root: %s | %s", self.id.path.to_str(), self.root.to_str()); let dir = self.root.push_rel(&self.id.path).normalize(); debug!("Checking dir: %s", dir.to_str()); if !os::path_exists(&dir) { return cond.raise((dir, ~"missing package dir")); } if !os::path_is_dir(&dir) { return cond.raise((dir, ~"supplied path for package dir is a \ non-directory")); } dir } fn has_pkg_file(&self) -> bool { let dir = self.check_dir(); dir.push("pkg.rs").exists() } // If a file named "pkg.rs" in the current directory exists, // return the path for it. Otherwise, None fn package_script_option(&self, cwd: &Path) -> Option { let maybe_path = cwd.push("pkg.rs"); if os::path_exists(&maybe_path) { Some(maybe_path) } else { None } } /// True if the given path's stem is self's pkg ID's stem /// or if the pkg ID's stem is and the given path's /// stem is foo fn stem_matches(&self, p: &Path) -> bool { let self_id = self.id.path.filestem(); if self_id == p.filestem() { return true; } else { for self_id.each |pth| { if pth.starts_with("rust-") && match p.filestem() { Some(s) => str::eq_slice(s, pth.slice(5, pth.len())), None => false } { return true; } } } false } fn push_crate(cs: &mut ~[Crate], prefix: uint, p: &Path) { assert!(p.components.len() > prefix); let mut sub = Path(""); for vec::slice(p.components, prefix, p.components.len()).each |c| { sub = sub.push(*c); } debug!("found crate %s", sub.to_str()); cs.push(Crate::new(&sub)); } fn find_crates(&mut self) { use PkgSrc::push_crate; let dir = self.check_dir(); let prefix = dir.components.len(); // This is ugly, but can go away once we get rid // of .rc files let mut saw_rs = false; let mut saw_rc = false; debug!("Matching against %?", self.id.path.filestem()); for os::walk_dir(&dir) |pth| { match pth.filename() { Some(~"lib.rs") => push_crate(&mut self.libs, prefix, pth), Some(~"main.rs") => push_crate(&mut self.mains, prefix, pth), Some(~"test.rs") => push_crate(&mut self.tests, prefix, pth), Some(~"bench.rs") => push_crate(&mut self.benchs, prefix, pth), _ => { // If the file stem is the same as the // package ID, with an .rs or .rc extension, // consider it to be a crate let ext = pth.filetype(); let matches = |p: &Path| { self.stem_matches(p) && (ext == Some(~".rc") || ext == Some(~".rs")) }; debug!("Checking %? which %s and ext = %? %? %?", pth.filestem(), if matches(pth) { "matches" } else { "does not match" }, ext, saw_rs, saw_rc); if matches(pth) && // Avoid pushing foo.rc *and* foo.rs !((ext == Some(~".rc") && saw_rs) || (ext == Some(~".rs") && saw_rc)) { push_crate(&mut self.libs, // ???? prefix, pth); if ext == Some(~".rc") { saw_rc = true; } else if ext == Some(~".rs") { saw_rs = true; } } } } } debug!("found %u libs, %u mains, %u tests, %u benchs", self.libs.len(), self.mains.len(), self.tests.len(), self.benchs.len()) } fn build_crates(dst_dir: &Path, src_dir: &Path, crates: &[Crate], cfgs: ~[~str], test: bool) { for crates.each |&crate| { let path = &src_dir.push_rel(&crate.file).normalize(); util::note(fmt!("build_crates: compiling %s", path.to_str())); util::note(fmt!("build_crates: destination dir is %s", dst_dir.to_str())); let result = util::compile_crate(None, path, dst_dir, crate.flags, crate.cfgs + cfgs, false, test); if !result { build_err::cond.raise(fmt!("build failure on %s", path.to_str())); } debug!("Result of compiling %s was %?", path.to_str(), result); } } fn build(&self, dst_dir: &Path, cfgs: ~[~str]) { let dir = self.check_dir(); debug!("Building libs"); PkgSrc::build_crates(dst_dir, &dir, self.libs, cfgs, false); debug!("Building mains"); PkgSrc::build_crates(dst_dir, &dir, self.mains, cfgs, false); debug!("Building tests"); PkgSrc::build_crates(dst_dir, &dir, self.tests, cfgs, true); debug!("Building benches"); PkgSrc::build_crates(dst_dir, &dir, self.benchs, cfgs, true); } }