auto merge of #6054 : catamorphism/rust/rustpkg, r=graydon

r? @graydon

Sorry, this pull request is a few different things at once, but I tried to make them separate commits.

First, as before, this should do file searching the way that's described in the doc now.

Second, there's also some preliminary work on the install command (really just tests for it).
This commit is contained in:
bors 2013-04-25 12:42:41 -07:00
commit 0604468fd5
18 changed files with 387 additions and 178 deletions

View File

@ -43,8 +43,6 @@ use vec;
pub use libc::fclose;
pub use os::consts::*;
// FIXME: move these to str perhaps? #2620
pub fn close(fd: c_int) -> c_int {
unsafe {
libc::close(fd)
@ -79,6 +77,8 @@ pub fn getcwd() -> Path {
}
}
// FIXME: move these to str perhaps? #2620
pub fn as_c_charp<T>(s: &str, f: &fn(*c_char) -> T) -> T {
str::as_c_str(s, |b| f(b as *c_char))
}

View File

@ -1,4 +1,24 @@
Right now (2013-04-11), only one package works, the branch of rust-sdl at:
https://github.com/catamorphism/rust-sdl/tree/new-rustpkg
Right now, commands that work are "build" and "clean".
and only one command works, "build".
`rustpkg build` and `rustpkg clean` should work
for example:
$ cd ~/rust/src/librustpkg/testsuite/pass
$ rustpkg build hello-world
... some output ...
$ rustpkg clean hello-world
-------------
the following test packages in librustpkg/testsuite/pass:
* hello-world
* install-paths
* simple-lib
* deeply/nested/path
* fancy-lib
It fails on the following test packages:
* external-crate (no support for `extern mod` inference yet)
and should fail with proper error messages
on all of the test packages in librustpkg/testsuite/fail
* no-inferred-crates

View File

@ -11,7 +11,12 @@
// Useful conditions
pub use core::path::Path;
pub use util::PkgId;
condition! {
bad_path: (super::Path, ~str) -> super::Path;
}
condition! {
nonexistent_package: (super::PkgId, ~str) -> super::Path;
}

21
src/librustpkg/context.rs Normal file
View File

@ -0,0 +1,21 @@
// Copyright 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 <LICENSE-APACHE or
// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
// option. This file may not be copied, modified, or distributed
// except according to those terms.
// Context data structure used by rustpkg
use core::hashmap::HashMap;
pub 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>,
}

View File

@ -15,39 +15,43 @@ use core::{os, str};
use core::option::*;
use util::PkgId;
/// Returns the output directory to use.
/// Right now is always the default, should
/// support changing it.
pub fn dest_dir(pkgid: PkgId) -> Path {
default_dest_dir(&pkgid.path)
#[deriving(Eq)]
pub enum OutputType { Main, Lib, Bench, Test }
/// Returns the value of RUST_PATH, as a list
/// of Paths. In general this should be read from the
/// environment; for now, it's hard-wired to just be "."
pub fn rust_path() -> ~[Path] {
~[Path(".")]
}
/// Returns the default output directory for compilation.
/// Creates that directory if it doesn't exist.
pub fn default_dest_dir(pkg_dir: &Path) -> Path {
/// Creates a directory that is readable, writeable,
/// and executable by the user. Returns true iff creation
/// succeeded.
pub fn make_dir_rwx(p: &Path) -> bool {
use core::libc::consts::os::posix88::{S_IRUSR, S_IWUSR, S_IXUSR};
use conditions::bad_path::cond;
// 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 {
rslt
}
else {
cond.raise((rslt, ~"Path names a file that isn't a directory"))
}
os::make_dir(p, (S_IRUSR | S_IWUSR | S_IXUSR) as i32)
}
/// Creates a directory that is readable, writeable,
/// and executable by the user. Returns true iff creation
/// succeeded. Also creates all intermediate subdirectories
/// if they don't already exist.
pub fn mkdir_recursive(p: &Path) -> bool {
if os::path_is_dir(p) {
return true;
}
let parent = p.dir_path();
debug!("mkdir_recursive: parent = %s",
parent.to_str());
if parent.to_str() == ~"."
|| parent.to_str() == ~"/" { // !!!
// No parent directories to create
os::path_is_dir(&parent) && make_dir_rwx(p)
}
else {
// Create it
if os::make_dir(&rslt, (S_IRUSR | S_IWUSR | S_IXUSR) as i32) {
rslt
}
else {
cond.raise((rslt, ~"Could not create directory"))
}
mkdir_recursive(&parent) && make_dir_rwx(p)
}
}
@ -69,34 +73,94 @@ pub fn normalize(p: ~Path) -> ~Path {
}
}
#[cfg(test)]
mod test {
use core::{os, rand};
use core::path::Path;
use path_util::*;
use core::rand::RngUtil;
// n.b. So far this only handles local workspaces
// n.b. The next three functions ignore the package version right
// now. Should fix that.
// Helper function to create a directory name that doesn't exist
pub fn mk_nonexistent(tmpdir: &Path, suffix: &str) -> Path {
let r = rand::rng();
for 1000.times {
let p = tmpdir.push(r.gen_str(16) + suffix);
if !os::path_exists(&p) {
return p;
}
}
fail!(~"Couldn't compute a non-existent path name; this is worrisome")
/// True if there's a directory in <workspace> with
/// pkgid's short name
pub fn workspace_contains_package_id(pkgid: PkgId, workspace: &Path) -> bool {
let pkgpath = workspace.push("src").push(pkgid.path.to_str());
os::path_is_dir(&pkgpath)
}
/// Return the directory for <pkgid>'s source files in <workspace>.
/// Doesn't check that it exists.
pub fn pkgid_src_in_workspace(pkgid: PkgId, workspace: &Path) -> Path {
let result = workspace.push("src");
result.push(pkgid.path.to_str())
}
/// Returns the executable that would be installed for <pkgid>
/// in <workspace>
pub fn target_executable_in_workspace(pkgid: PkgId, workspace: &Path) -> Path {
let result = workspace.push("bin");
// should use a target-specific subdirectory
mk_output_path(Main, pkgid.path.to_str(), result)
}
/// Returns the executable that would be installed for <pkgid>
/// in <workspace>
pub fn target_library_in_workspace(pkgid: PkgId, workspace: &Path) -> Path {
let result = workspace.push("lib");
mk_output_path(Lib, pkgid.path.to_str(), result)
}
/// Returns the test executable that would be installed for <pkgid>
/// in <workspace>
pub fn target_test_in_workspace(pkgid: PkgId, workspace: &Path) -> Path {
let result = workspace.push("build");
mk_output_path(Test, pkgid.path.to_str(), result)
}
/// Returns the bench executable that would be installed for <pkgid>
/// in <workspace>
pub fn target_bench_in_workspace(pkgid: PkgId, workspace: &Path) -> Path {
let result = workspace.push("build");
mk_output_path(Bench, pkgid.path.to_str(), result)
}
/// Return the directory for <pkgid>'s build artifacts in <workspace>.
/// Creates it if it doesn't exist.
pub fn build_pkg_id_in_workspace(pkgid: PkgId, workspace: &Path) -> Path {
use conditions::bad_path::cond;
let mut result = workspace.push("build");
// n.b. Should actually use a target-specific
// subdirectory of build/
result = result.push(normalize(~pkgid.path).to_str());
if os::path_exists(&result) || mkdir_recursive(&result) {
result
}
#[test]
fn default_dir_ok() {
let the_path = os::tmpdir();
let substitute_path = Path("xyzzy");
assert!(default_dest_dir(&the_path) == the_path.push(~"build"));
let nonexistent_path = mk_nonexistent(&the_path, "quux");
let bogus = do ::conditions::bad_path::cond.trap(|_| {
substitute_path
}).in { default_dest_dir(&nonexistent_path) };
assert!(bogus == substitute_path);
else {
cond.raise((result, fmt!("Could not create directory for package %s", pkgid.to_str())))
}
}
/// Return the output file for a given directory name,
/// given whether we're building a library and whether we're building tests
pub fn mk_output_path(what: OutputType, short_name: ~str, dir: Path) -> Path {
match what {
Lib => dir.push(os::dll_filename(short_name)),
_ => dir.push(fmt!("%s%s%s", short_name,
if what == Test { ~"test" } else { ~"" },
os::EXE_SUFFIX))
}
}
#[cfg(test)]
mod test {
use core::os;
#[test]
fn recursive_mkdir_ok() {
let root = os::tmpdir();
let path = "xy/z/zy";
let nested = root.push(path);
assert!(super::mkdir_recursive(&nested));
assert!(os::path_is_dir(&root.push("xy")));
assert!(os::path_is_dir(&root.push("xy/z")));
assert!(os::path_is_dir(&nested));
}
}

View File

@ -36,13 +36,19 @@ use rustc::metadata::filesearch;
use std::{getopts};
use syntax::{ast, diagnostic};
use util::*;
use path_util::{dest_dir, normalize};
use path_util::normalize;
use path_util::{build_pkg_id_in_workspace, pkgid_src_in_workspace};
use workspace::pkg_parent_workspaces;
use rustc::driver::session::{lib_crate, bin_crate, crate_type};
use context::Ctx;
mod conditions;
mod context;
mod usage;
mod path_util;
mod tests;
mod util;
mod workspace;
/// A PkgScript represents user-supplied custom logic for
/// special build hooks. This only exists for packages with
@ -70,7 +76,7 @@ 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 {
fn parse(script: Path, workspace: &Path, id: PkgId) -> PkgScript {
// Get the executable name that was invoked
let binary = os::args()[0];
// Build the rustc session data structures to pass
@ -85,7 +91,7 @@ impl PkgScript {
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);
let work_dir = build_pkg_id_in_workspace(id, workspace);
debug!("Returning package script with id %?", id);
@ -153,14 +159,6 @@ impl PkgScript {
}
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>,
}
impl Ctx {
fn run(&self, cmd: ~str, args: ~[~str]) {
@ -193,54 +191,50 @@ impl Ctx {
// 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 = %s", cwd.to_str());
for pkg_parent_workspaces(pkgid) |workspace| {
let src_dir = pkgid_src_in_workspace(pkgid, workspace);
let build_dir = build_pkg_id_in_workspace(pkgid, workspace);
debug!("Destination dir = %s", build_dir.to_str());
// Create the package source
let mut src = PkgSrc::new(&cwd, &dst_dir, &pkgid);
debug!("Package src = %?", src);
// Create the package source
let mut src = PkgSrc::new(&workspace.push("src"), &build_dir, &pkgid);
debug!("Package src = %?", src);
// Is there custom build logic? If so, use it
let pkg_src_dir = cwd.push_rel(&pkgid.path);
let mut custom = false;;
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"))
// Is there custom build logic? If so, use it
let pkg_src_dir = src_dir;
let mut custom = false;
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,
workspace,
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"))
}
custom = true;
// otherwise, the package script succeeded
cfgs
}
custom = true;
// otherwise, the package script succeeded
cfgs
}
None => {
debug!("No package script, continuing");
~[]
}
};
None => {
debug!("No package script, continuing");
~[]
}
};
// If there was a package script, it should have finished
// the build already. Otherwise...
if !custom {
// Find crates inside the workspace
src.find_crates();
// Build it!
src.build(&dst_dir, cfgs);
// If there was a package script, it should have finished
// the build already. Otherwise...
if !custom {
// Find crates inside the workspace
src.find_crates();
// Build it!
src.build(&build_dir, cfgs);
}
}
}
~"clean" => {
@ -250,8 +244,8 @@ impl Ctx {
// The package id is presumed to be the first command-line
// argument
let pkgid = PkgId::new(args[0]);
self.clean(pkgid);
let cwd = os::getcwd();
self.clean(&cwd, pkgid); // tjc: should use workspace, not cwd
}
~"do" => {
if args.len() < 2 {
@ -264,10 +258,16 @@ impl Ctx {
self.info();
}
~"install" => {
self.install(if args.len() >= 1 { Some(args[0]) }
else { None },
if args.len() >= 2 { Some(args[1]) }
else { None }, false);
if args.len() < 1 {
return usage::install();
}
// The package id is presumed to be the first command-line
// argument
let pkgid = PkgId::new(args[0]);
for pkg_parent_workspaces(pkgid) |workspace| {
self.install(workspace, pkgid);
}
}
~"prefer" => {
if args.len() < 1 {
@ -303,58 +303,17 @@ impl Ctx {
}
}
fn do_cmd(&self, cmd: ~str, pkgname: ~str) {
match cmd {
~"build" | ~"test" => {
util::error(~"that command cannot be manually called");
fail!(~"do_cmd");
}
_ => {}
}
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");
fail!(~"do_cmd");
}
}
None => {
util::error(fmt!("invoked `do`, but there is no package script in %s",
cwd.to_str()));
fail!(~"do_cmd");
}
}
fn do_cmd(&self, _cmd: ~str, _pkgname: ~str) {
// stub
fail!(~"`do` not yet implemented");
}
fn build(&self, _dir: &Path, _verbose: bool, _opt: bool,
_test: bool) -> Option<PkgScript> {
// 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) {
// What's the difference between build and compile?
fail!(~"compile not yet implemented");
}
fn clean(&self, id: PkgId) {
fn clean(&self, workspace: &Path, id: PkgId) {
// Could also support a custom build hook in the pkg
// script for cleaning files rustpkg doesn't know about.
// Do something reasonable for now
let dir = dest_dir(id);
let dir = build_pkg_id_in_workspace(id, workspace);
util::note(fmt!("Cleaning package %s (removing directory %s)",
id.to_str(), dir.to_str()));
if os::path_exists(&dir) {
@ -370,8 +329,7 @@ impl Ctx {
fail!(~"info not yet implemented");
}
fn install(&self, _url: Option<~str>,
_target: Option<~str>, _cache: bool) {
fn install(&self, _workspace: &Path, _id: PkgId) {
// stub
fail!(~"install not yet implemented");
}

View File

@ -9,3 +9,96 @@
// except according to those terms.
// rustpkg unit tests
use context::Ctx;
use core::hashmap::HashMap;
use core::path::Path;
use core::os;
use core::io;
use core::option::*;
use std::tempfile::mkdtemp;
use util::{PkgId, default_version};
use path_util::{target_executable_in_workspace, target_library_in_workspace,
target_test_in_workspace, target_bench_in_workspace,
make_dir_rwx};
fn fake_ctxt() -> Ctx {
Ctx {
json: false,
dep_cache: @mut HashMap::new()
}
}
fn fake_pkg() -> PkgId {
PkgId {
path: Path(~"bogus"),
version: default_version()
}
}
fn mk_temp_workspace() -> Path {
mkdtemp(&os::tmpdir(), "test").expect("couldn't create temp dir")
}
fn is_rwx(p: &Path) -> bool {
use core::libc::consts::os::posix88::{S_IRUSR, S_IWUSR, S_IXUSR};
match p.get_mode() {
None => return false,
Some(m) => {
((m & S_IRUSR as uint) == S_IRUSR as uint
&& (m & S_IWUSR as uint) == S_IWUSR as uint
&& (m & S_IXUSR as uint) == S_IXUSR as uint)
}
}
}
#[test]
fn test_make_dir_rwx() {
let temp = &os::tmpdir();
let dir = temp.push(~"quux");
let _ = os::remove_dir(&dir);
assert!(make_dir_rwx(&dir));
assert!(os::path_is_dir(&dir));
assert!(is_rwx(&dir));
assert!(os::remove_dir(&dir));
}
#[test]
#[ignore(reason = "install not yet implemented")]
fn test_install_valid() {
let ctxt = fake_ctxt();
let temp_pkg_id = fake_pkg();
let temp_workspace() = mk_temp_workspace();
// should have test, bench, lib, and main
ctxt.install(&temp_workspace, temp_pkg_id);
// Check that all files exist
let exec = target_executable_in_workspace(temp_pkg_id, &temp_workspace);
assert!(os::path_exists(&exec));
assert!(is_rwx(&exec));
let lib = target_library_in_workspace(temp_pkg_id, &temp_workspace);
assert!(os::path_exists(&lib));
assert!(is_rwx(&lib));
// And that the test and bench executables aren't installed
assert!(!os::path_exists(&target_test_in_workspace(temp_pkg_id, &temp_workspace)));
assert!(!os::path_exists(&target_bench_in_workspace(temp_pkg_id, &temp_workspace)));
}
#[test]
#[ignore(reason = "install not yet implemented")]
fn test_install_invalid() {
use conditions::nonexistent_package::cond;
let ctxt = fake_ctxt();
let pkgid = fake_pkg();
let temp_workspace = mk_temp_workspace();
let expected_path = Path(~"quux");
let substituted: Path = do cond.trap(|_| {
expected_path
}).in {
ctxt.install(&temp_workspace, pkgid);
// ok
fail!(~"test_install_invalid failed, should have raised a condition");
};
assert!(substituted == expected_path);
}

View File

@ -21,4 +21,4 @@ extern mod std;
pub mod foo;
pub mod bar;
#[path = "build/generated.rs"] pub mod generated;
#[path = "../../build/fancy_lib/generated.rs"] pub mod generated;

View File

@ -11,13 +11,18 @@
use core::run;
pub fn main() {
let cwd = os::getcwd();
debug!("cwd = %s", cwd.to_str());
let file = io::file_writer(&Path(~"fancy-lib/build/generated.rs"),
use core::libc::consts::os::posix88::{S_IRUSR, S_IWUSR, S_IXUSR};
let out_path = Path(~"build/fancy_lib");
if !os::path_exists(&out_path) {
assert!(os::make_dir(&out_path, (S_IRUSR | S_IWUSR | S_IXUSR) as i32));
}
let file = io::file_writer(&out_path.push("generated.rs"),
[io::Create]).get();
file.write_str("pub fn wheeeee() { for [1, 2, 3].each() |_| { assert!(true); } }");
// now compile the crate itself
run::run_program("rustc", ~[~"fancy-lib/fancy-lib.rs", ~"--lib",
~"-o", ~"fancy-lib/build/fancy_lib"]);
run::run_program("rustc", ~[~"src/fancy-lib/fancy-lib.rs", ~"--lib",
~"-o", out_path.push(~"fancy_lib").to_str()]);
}

View File

@ -78,7 +78,7 @@ impl ToStr for Version {
}
/// Placeholder
fn default_version() -> Version { ExactRevision(0.1) }
pub fn default_version() -> Version { ExactRevision(0.1) }
// Path-fragment identifier of a package such as
// 'github.com/graydon/test'; path must be a relative
@ -116,7 +116,14 @@ pub impl PkgId {
impl ToStr for PkgId {
fn to_str(&self) -> ~str {
// should probably use the filestem and not the whole path
fmt!("%s-%s", self.path.to_str(), self.version.to_str())
fmt!("%s-%s", self.path.to_str(),
// Replace dots with -s in the version
// this is because otherwise rustc will think
// that foo-0.1 has .1 as its extension
// (Temporary hack until I figure out how to
// get rustc to not name the object file
// foo-0.o if I pass in foo-0.1 to build_output_filenames)
str::replace(self.version.to_str(), ".", "-"))
}
}
@ -438,7 +445,9 @@ pub fn compile_input(sysroot: Option<Path>,
test: bool,
crate_type: session::crate_type) -> bool {
let short_name = pkg_id.to_str();
// Want just the directory component here
let pkg_filename = pkg_id.path.filename().expect(~"Weird pkg id");
let short_name = fmt!("%s-%s", pkg_filename, pkg_id.version.to_str());
assert!(in_file.components.len() > 1);
let input = driver::file_input(copy *in_file);
@ -515,7 +524,7 @@ pub fn compile_crate_from_input(input: driver::input,
out_file: Path,
binary: ~str,
what: driver::compile_upto) -> @ast::crate {
debug!("Calling build_output_filenames with %?", build_dir_opt);
debug!("Calling build_output_filenames with %? and %s", build_dir_opt, out_file.to_str());
let outputs = driver::build_output_filenames(&input, &build_dir_opt, &Some(out_file), sess);
debug!("Outputs are %? and output type = %?", outputs, sess.opts.output_type);
let cfg = driver::build_configuration(sess, @binary, &input);

View File

@ -0,0 +1,34 @@
// Copyright 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 <LICENSE-APACHE or
// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
// option. This file may not be copied, modified, or distributed
// except according to those terms.
// rustpkg utilities having to do with workspaces
use path_util::{rust_path, workspace_contains_package_id};
use util::PkgId;
use core::path::Path;
pub fn pkg_parent_workspaces(pkgid: PkgId, action: &fn(&Path) -> bool) {
// Using the RUST_PATH, find workspaces that contain
// this package ID
let workspaces = rust_path().filtered(|ws|
workspace_contains_package_id(pkgid, ws));
if workspaces.is_empty() {
// tjc: make this a condition
fail!(fmt!("Package %s not found in any of \
the following workspaces: %s",
pkgid.path.to_str(),
rust_path().to_str()));
}
for workspaces.each |ws| {
if action(ws) {
break;
}
}
}