Allow custom precompile commands
authorAlex Crichton <alex@alexcrichton.com>
Wed, 18 Jun 2014 20:09:19 +0000 (13:09 -0700)
committerAlex Crichton <alex@alexcrichton.com>
Thu, 19 Jun 2014 18:52:51 +0000 (11:52 -0700)
This commit enables support for custom precompilation commands to be triggered
before a package builds via rustc. The current interface is to have
`build = "foo"` in the `[project]` section of Cargo.toml and cargo will just
execute the exact command given.

src/cargo/core/manifest.rs
src/cargo/ops/cargo_rustc.rs
src/cargo/util/errors.rs
src/cargo/util/process_builder.rs
src/cargo/util/toml.rs
tests/support/mod.rs
tests/test_cargo_compile.rs

index f4513b94bca2f043852b484d4ea02747adef0e7d..c147a563163eb5ec9147557b1e0389422a3fffd2 100644 (file)
@@ -18,12 +18,16 @@ pub struct Manifest {
     authors: Vec<String>,
     targets: Vec<Target>,
     target_dir: Path,
-    sources: Vec<SourceId>
+    sources: Vec<SourceId>,
+    build: Option<String>,
 }
 
 impl Show for Manifest {
     fn fmt(&self, f: &mut Formatter) -> fmt::Result {
-        write!(f, "Manifest({}, authors={}, targets={}, target_dir={})", self.summary, self.authors, self.targets, self.target_dir.display())
+        write!(f, "Manifest({}, authors={}, targets={}, target_dir={}, \
+                   build={})",
+               self.summary, self.authors, self.targets,
+               self.target_dir.display(), self.build)
     }
 }
 
@@ -34,7 +38,8 @@ pub struct SerializedManifest {
     dependencies: Vec<SerializedDependency>,
     authors: Vec<String>,
     targets: Vec<Target>,
-    target_dir: String
+    target_dir: String,
+    build: Option<String>,
 }
 
 impl<E, S: Encoder<E>> Encodable<S, E> for Manifest {
@@ -45,7 +50,8 @@ impl<E, S: Encoder<E>> Encodable<S, E> for Manifest {
             dependencies: self.summary.get_dependencies().iter().map(|d| SerializedDependency::from_dependency(d)).collect(),
             authors: self.authors.clone(),
             targets: self.targets.clone(),
-            target_dir: self.target_dir.display().to_str()
+            target_dir: self.target_dir.display().to_str(),
+            build: self.build.clone(),
         }.encode(s)
     }
 }
@@ -125,13 +131,16 @@ impl Show for Target {
 }
 
 impl Manifest {
-    pub fn new(summary: &Summary, targets: &[Target], target_dir: &Path, sources: Vec<SourceId>) -> Manifest {
+    pub fn new(summary: &Summary, targets: &[Target],
+               target_dir: &Path, sources: Vec<SourceId>,
+               build: Option<String>) -> Manifest {
         Manifest {
             summary: summary.clone(),
             authors: Vec::new(),
             targets: Vec::from_slice(targets),
             target_dir: target_dir.clone(),
-            sources: sources
+            sources: sources,
+            build: build,
         }
     }
 
@@ -170,6 +179,10 @@ impl Manifest {
     pub fn get_source_ids<'a>(&'a self) -> &'a [SourceId] {
         self.sources.as_slice()
     }
+
+    pub fn get_build<'a>(&'a self) -> Option<&'a str> {
+        self.build.as_ref().map(|s| s.as_slice())
+    }
 }
 
 impl Target {
index 0f6cee71522b5df51c3ebf62805826059c7afe4e..b86a837382fdf476aab3869bcf426d8a8d24577a 100644 (file)
@@ -3,7 +3,7 @@ use std::io;
 use std::path::Path;
 use core::{Package,PackageSet,Target};
 use util;
-use util::{CargoResult, ChainError, ProcessBuilder, internal, human};
+use util::{CargoResult, ChainError, ProcessBuilder, internal, human, CargoError};
 
 type Args = Vec<String>;
 
@@ -33,6 +33,11 @@ pub fn compile_packages(pkg: &Package, deps: &PackageSet) -> CargoResult<()> {
 fn compile_pkg(pkg: &Package, dest: &Path, deps_dir: &Path, primary: bool) -> CargoResult<()> {
     debug!("compile_pkg; pkg={}; targets={}", pkg, pkg.get_targets());
 
+    match pkg.get_manifest().get_build() {
+        Some(cmd) => try!(compile_custom(pkg, cmd, dest, deps_dir, primary)),
+        None => {}
+    }
+
     // compile
     for target in pkg.get_targets().iter() {
         // Only compile lib targets for dependencies
@@ -48,6 +53,20 @@ fn mk_target(target: &Path) -> CargoResult<()> {
     io::fs::mkdir_recursive(target, io::UserRWX).chain_error(|| internal("could not create target directory"))
 }
 
+fn compile_custom(pkg: &Package, cmd: &str, dest: &Path, deps_dir: &Path,
+                  _primary: bool) -> CargoResult<()> {
+    // FIXME: this needs to be smarter about splitting
+    let mut cmd = cmd.split(' ');
+    let mut p = util::process(cmd.next().unwrap())
+                     .cwd(pkg.get_root())
+                     .env("OUT_DIR", Some(dest.as_str().unwrap()))
+                     .env("DEPS_DIR", Some(dest.join(deps_dir).as_str().unwrap()));
+    for arg in cmd {
+        p = p.arg(arg);
+    }
+    p.exec_with_output().map(|_| ()).map_err(|e| e.mark_human())
+}
+
 fn rustc(root: &Path, target: &Target, dest: &Path, deps: &Path, verbose: bool) -> CargoResult<()> {
 
     let crate_types = target.rustc_crate_types();
@@ -72,7 +91,7 @@ fn prepare_rustc(root: &Path, target: &Target, crate_type: &'static str, dest: &
     let mut args = Vec::new();
 
     build_base_args(&mut args, target, crate_type, dest);
-    build_deps_args(&mut args, deps);
+    build_deps_args(&mut args, dest, deps);
 
     util::process("rustc")
         .cwd(root.clone())
@@ -89,9 +108,11 @@ fn build_base_args(into: &mut Args, target: &Target, crate_type: &'static str, d
     into.push(dest.display().to_str());
 }
 
-fn build_deps_args(dst: &mut Args, deps: &Path) {
+fn build_deps_args(dst: &mut Args, deps: &Path, dest: &Path) {
     dst.push("-L".to_str());
     dst.push(deps.display().to_str());
+    dst.push("-L".to_str());
+    dst.push(dest.display().to_str());
 }
 
 fn topsort(deps: &PackageSet) -> CargoResult<PackageSet> {
index 03c38753ee8d7b40116e4eaf66620c8291570fd4..649336664a6c90486afe94e5a437b814835ca8a5 100644 (file)
@@ -2,6 +2,7 @@ use std::io::process::{Command,ProcessOutput,ProcessExit,ExitStatus,ExitSignal};
 use std::io::IoError;
 use std::fmt;
 use std::fmt::{Show, Formatter};
+use std::str;
 
 use TomlError = toml::Error;
 
index 251cc97737dd6d5bc68cd2a51a162951111236e6..314095129874611ecacdf213ddabecf74814446a 100644 (file)
@@ -31,8 +31,13 @@ impl Show for ProcessBuilder {
 static PATH_SEP : &'static str = ":";
 
 impl ProcessBuilder {
-    pub fn args<T: Show>(mut self, arguments: &[T]) -> ProcessBuilder {
-        self.args = arguments.iter().map(|a| a.to_str()).collect();
+    pub fn arg<T: Str>(mut self, arg: T) -> ProcessBuilder {
+        self.args.push(arg.as_slice().to_str());
+        self
+    }
+
+    pub fn args<T: Str>(mut self, arguments: &[T]) -> ProcessBuilder {
+        self.args = arguments.iter().map(|a| a.as_slice().to_str()).collect();
         self
     }
 
@@ -98,14 +103,18 @@ impl ProcessBuilder {
         }
     }
 
-    fn build_command(&self) -> Command {
+    pub fn build_command(&self) -> Command {
         let mut command = Command::new(self.program.as_slice());
         command.args(self.args.as_slice()).cwd(&self.cwd);
         command
     }
 
     fn debug_string(&self) -> String {
-        format!("{} {}", self.program, self.args.connect(" "))
+        if self.args.len() == 0 {
+            self.program.to_str()
+        } else {
+            format!("{} {}", self.program, self.args.connect(" "))
+        }
     }
 
     fn build_env(&self) -> Vec<(String, String)> {
index 6312dc1a58cdc6f5aef566acfd76d27fb989e03e..3c6039551f69ec0a0527a4aac31b4a4b4f8f30df 100644 (file)
@@ -101,7 +101,8 @@ pub struct TomlManifest {
 pub struct TomlProject {
     pub name: String,
     pub version: String,
-    pub authors: Vec<String>
+    pub authors: Vec<String>,
+    build: Option<String>,
 }
 
 impl TomlProject {
@@ -159,10 +160,13 @@ impl TomlManifest {
         }
 
         Ok((Manifest::new(
-                &Summary::new(&self.project.to_package_id(source_id.get_url()), deps.as_slice()),
+                &Summary::new(&self.project.to_package_id(source_id.get_url()),
+                              deps.as_slice()),
                 targets.as_slice(),
                 &Path::new("target"),
-                sources), nested_paths))
+                sources,
+                self.project.build.clone()),
+           nested_paths))
     }
 }
 
index 621b4481f9093cdcc4829039e73f223df8413005..a976a158d3f85ffd4783d4e40ae871ea1130f9d0 100644 (file)
@@ -2,7 +2,7 @@
 use std;
 use std::io;
 use std::io::fs;
-use std::io::process::{ProcessOutput,ProcessExit};
+use std::io::process::{ProcessOutput};
 use std::os;
 use std::path::{Path,BytesContainer};
 use std::str;
@@ -80,7 +80,7 @@ impl ProjectBuilder {
             .extra_path(cargo_dir())
     }
 
-    pub fn file<B: BytesContainer, S: Str>(mut self, path: B, body: S) -> ProjectBuilder { 
+    pub fn file<B: BytesContainer, S: Str>(mut self, path: B, body: S) -> ProjectBuilder {
         self.files.push(FileBuilder::new(self.root.join(path), body.as_slice()));
         self
     }
@@ -202,18 +202,21 @@ impl Execs {
   }
 
   fn match_output(&self, actual: &ProcessOutput) -> ham::MatchResult {
-    self.match_status(actual.status)
+    self.match_status(actual)
       .and(self.match_stdout(actual))
       .and(self.match_stderr(actual))
   }
 
-  fn match_status(&self, actual: ProcessExit) -> ham::MatchResult {
+  fn match_status(&self, actual: &ProcessOutput) -> ham::MatchResult {
     match self.expect_exit_code {
       None => ham::success(),
       Some(code) => {
         ham::expect(
-          actual.matches_exit_status(code),
-          format!("exited with {}", actual))
+          actual.status.matches_exit_status(code),
+          format!("exited with {}\n--- stdout\n{}\n--- stderr\n{}",
+                  actual.status,
+                  str::from_utf8(actual.output.as_slice()),
+                  str::from_utf8(actual.error.as_slice())))
       }
     }
   }
index e096df9b221ddd04fbfe4ee1639e33d46e082861..adf992a6de89d130d1e584b033bd86ad7fcda8ad 100644 (file)
@@ -64,7 +64,15 @@ test!(cargo_compile_with_invalid_code {
     assert_that(p.cargo_process("cargo-compile"),
         execs()
         .with_status(101)
-        .with_stderr(format!("src/foo.rs:1:1: 1:8 error: expected item but found `invalid`\nsrc/foo.rs:1 invalid rust code!\n             ^~~~~~~\nCould not execute process `rustc src/foo.rs --crate-type bin --out-dir {} -L {}` (status=101)", target.display(), target.join("deps").display()).as_slice()));
+        .with_stderr(format!("\
+src/foo.rs:1:1: 1:8 error: expected item but found `invalid`
+src/foo.rs:1 invalid rust code!
+             ^~~~~~~
+Could not execute process \
+`rustc src/foo.rs --crate-type bin --out-dir {} -L {} -L {}` (status=101)",
+            target.display(),
+            target.display(),
+            target.join("deps").display()).as_slice()));
 })
 
 test!(cargo_compile_with_warnings_in_the_root_package {
@@ -285,3 +293,188 @@ test!(cargo_compile_with_nested_deps_longhand {
 })
 
 // test!(compiling_project_with_invalid_manifest)
+
+test!(custom_build {
+    let mut build = project("builder");
+    build = build
+        .file("Cargo.toml", r#"
+            [project]
+
+            name = "foo"
+            version = "0.5.0"
+            authors = ["wycats@example.com"]
+
+            [[bin]] name = "foo"
+        "#)
+        .file("src/foo.rs", r#"
+            fn main() { println!("Hello!"); }
+        "#);
+    assert_that(build.cargo_process("cargo-compile"),
+                execs().with_status(0));
+
+
+    let mut p = project("foo");
+    p = p
+        .file("Cargo.toml", format!(r#"
+            [project]
+
+            name = "foo"
+            version = "0.5.0"
+            authors = ["wycats@example.com"]
+            build = "{}"
+
+            [[bin]] name = "foo"
+        "#, build.root().join("target/foo").display()))
+        .file("src/foo.rs", r#"
+            fn main() {}
+        "#);
+    assert_that(p.cargo_process("cargo-compile"),
+                execs().with_status(0)
+                       .with_stdout(format!("Compiling foo v0.5.0 (file:{})\n",
+                                            p.root().display()))
+                       .with_stderr(""));
+})
+
+test!(custom_build_failure {
+    let mut build = project("builder");
+    build = build
+        .file("Cargo.toml", r#"
+            [project]
+
+            name = "foo"
+            version = "0.5.0"
+            authors = ["wycats@example.com"]
+
+            [[bin]] name = "foo"
+        "#)
+        .file("src/foo.rs", r#"
+            fn main() { fail!("nope") }
+        "#);
+    assert_that(build.cargo_process("cargo-compile"), execs().with_status(0));
+
+
+    let mut p = project("foo");
+    p = p
+        .file("Cargo.toml", format!(r#"
+            [project]
+
+            name = "foo"
+            version = "0.5.0"
+            authors = ["wycats@example.com"]
+            build = "{}"
+
+            [[bin]] name = "foo"
+        "#, build.root().join("target/foo").display()))
+        .file("src/foo.rs", r#"
+            fn main() {}
+        "#);
+    assert_that(p.cargo_process("cargo-compile"),
+                execs().with_status(101).with_stderr(format!("\
+Could not execute process `{}` (status=101)
+--- stderr
+task '<main>' failed at 'nope', src/foo.rs:2
+", build.root().join("target/foo").display())));
+})
+
+test!(custom_build_env_vars {
+    let mut p = project("foo");
+    let mut build = project("builder");
+    build = build
+        .file("Cargo.toml", r#"
+            [project]
+
+            name = "foo"
+            version = "0.5.0"
+            authors = ["wycats@example.com"]
+
+            [[bin]] name = "foo"
+        "#)
+        .file("src/foo.rs", format!(r#"
+            use std::os;
+            fn main() {{
+                assert_eq!(os::getenv("OUT_DIR").unwrap(), "{}".to_str());
+                assert_eq!(os::getenv("DEPS_DIR").unwrap(), "{}".to_str());
+            }}
+        "#,
+        p.root().join("target").display(),
+        p.root().join("target/deps").display()));
+    assert_that(build.cargo_process("cargo-compile"), execs().with_status(0));
+
+
+    p = p
+        .file("Cargo.toml", format!(r#"
+            [project]
+
+            name = "foo"
+            version = "0.5.0"
+            authors = ["wycats@example.com"]
+            build = "{}"
+
+            [[bin]] name = "foo"
+        "#, build.root().join("target/foo").display()))
+        .file("src/foo.rs", r#"
+            fn main() {}
+        "#);
+    assert_that(p.cargo_process("cargo-compile"), execs().with_status(0));
+})
+
+test!(custom_build_in_dependency {
+    let mut p = project("foo");
+    let bar = p.root().join("bar");
+    let mut build = project("builder");
+    build = build
+        .file("Cargo.toml", r#"
+            [project]
+
+            name = "foo"
+            version = "0.5.0"
+            authors = ["wycats@example.com"]
+
+            [[bin]] name = "foo"
+        "#)
+        .file("src/foo.rs", format!(r#"
+            use std::os;
+            fn main() {{
+                assert_eq!(os::getenv("OUT_DIR").unwrap(), "{}".to_str());
+                assert_eq!(os::getenv("DEPS_DIR").unwrap(), "{}".to_str());
+            }}
+        "#,
+        p.root().join("target/deps").display(),
+        p.root().join("target/deps").display()));
+    assert_that(build.cargo_process("cargo-compile"), execs().with_status(0));
+
+
+    p = p
+        .file(".cargo/config", format!(r#"
+            paths = ["{}"]
+        "#, bar.display()).as_slice())
+        .file("Cargo.toml", r#"
+            [project]
+
+            name = "foo"
+            version = "0.5.0"
+            authors = ["wycats@example.com"]
+
+            [[bin]] name = "foo"
+            [dependencies.bar] version = "0.5.0"
+        "#)
+        .file("src/foo.rs", r#"
+            extern crate bar;
+            fn main() { bar::bar() }
+        "#)
+        .file("bar/Cargo.toml", format!(r#"
+            [project]
+
+            name = "bar"
+            version = "0.5.0"
+            authors = ["wycats@example.com"]
+            build = "{}"
+
+            [[lib]] name = "bar"
+        "#, build.root().join("target/foo").display()))
+        .file("bar/src/bar.rs", r#"
+            pub fn bar() {}
+        "#);
+    assert_that(p.cargo_process("cargo-compile"),
+                execs().with_status(0));
+})