extern crate clap;
extern crate env_logger;
#[macro_use] extern crate lazy_static;
#[macro_use] extern crate log;
extern crate open;
extern crate regex;
extern crate rustc_serialize;
extern crate semver;
extern crate shaman;
extern crate toml;
#[cfg(feature="chan")]
#[macro_use] extern crate chan;
const STUB_HASHES: bool = false;
const ALLOW_AUTO_REMOVE: bool = true;
#[cfg(feature="suppress-cargo-output")]
const CARGO_OUTPUT_TIMEOUT: u64 = 2_000;
#[cfg(windows)]
macro_rules! if_windows {
(@as_expr $e:expr) => { $e };
($($tts:tt)*) => { if_windows! { @as_expr { $($tts)* } } };
}
#[cfg(not(windows))]
macro_rules! if_windows {
($($tts:tt)*) => { {} };
}
mod consts;
mod error;
mod manifest;
mod platform;
mod templates;
mod util;
#[cfg(windows)]
mod file_assoc;
#[cfg(not(windows))]
mod file_assoc {}
use std::borrow::Cow;
use std::error::Error;
use std::ffi::OsString;
use std::fs;
use std::io::{Read, Write};
use std::path::{Path, PathBuf};
use std::process::{self, Command};
use semver::Version;
use error::{Blame, MainError, Result, ResultExt};
use platform::MigrationKind;
use util::{ChainMap, Defer, PathExt};
#[derive(Debug)]
enum SubCommand {
Script(Args),
Templates(templates::Args),
#[cfg(windows)]
FileAssoc(file_assoc::Args),
}
#[derive(Debug)]
struct Args {
script: Option<String>,
args: Vec<String>,
features: Option<String>,
expr: bool,
loop_: bool,
count: bool,
pkg_path: Option<String>,
gen_pkg_only: bool,
build_only: bool,
clear_cache: bool,
debug: bool,
dep: Vec<String>,
dep_extern: Vec<String>,
extern_: Vec<String>,
force: bool,
unstable_features: Vec<String>,
use_bincache: Option<bool>,
migrate_data: Option<MigrationKind>,
build_kind: BuildKind,
template: Option<String>,
}
#[derive(Copy, Clone, Debug)]
enum BuildKind {
Normal,
Test,
Bench,
}
impl BuildKind {
fn can_exec_directly(&self) -> bool {
match *self {
BuildKind::Normal => true,
BuildKind::Test | BuildKind::Bench => false,
}
}
fn exec_command(&self) -> &'static str {
match *self {
BuildKind::Normal => panic!("asked for exec command for normal build"),
BuildKind::Test => "test",
BuildKind::Bench => "bench",
}
}
fn from_flags(test: bool, bench: bool) -> Self {
match (test, bench) {
(false, false) => BuildKind::Normal,
(true, false) => BuildKind::Test,
(false, true) => BuildKind::Bench,
_ => panic!("got both test and bench")
}
}
}
fn parse_args() -> SubCommand {
use clap::{App, Arg, ArgGroup, SubCommand, AppSettings};
let version = option_env!("CARGO_PKG_VERSION").unwrap_or("unknown");
let about = r#"Compiles and runs "Cargoified Rust scripts"."#;
macro_rules! csas {
($($es:expr),*) => {
{
const PIN: &'static [&'static str] = &[$($es),*];
PIN
}
}
}
let m = App::new("cargo")
.bin_name("cargo")
.version(version)
.about(about)
.setting(AppSettings::SubcommandRequiredElseHelp)
.subcommand(SubCommand::with_name("script")
.version(version)
.about(about)
.usage("cargo script [FLAGS OPTIONS] [--] <script> <args>...")
.arg(Arg::with_name("script")
.help("Script file (with or without extension) to execute.")
.index(1)
)
.arg(Arg::with_name("args")
.help("Additional arguments passed to the script.")
.index(2)
.multiple(true)
)
.arg(Arg::with_name("expr")
.help("Execute <script> as a literal expression and display the result.")
.long("expr")
.short("e")
.conflicts_with_all(csas!["loop"])
.requires("script")
)
.arg(Arg::with_name("loop")
.help("Execute <script> as a literal closure once for each line from stdin.")
.long("loop")
.short("l")
.conflicts_with_all(csas!["expr"])
.requires("script")
)
.group(ArgGroup::with_name("expr_or_loop")
.args(&["expr", "loop"])
)
.arg(Arg::with_name("count")
.help("Invoke the loop closure with two arguments: line, and line number.")
.long("count")
.requires("loop")
)
.arg(Arg::with_name("debug")
.help("Build a debug executable, not an optimised one.")
.long("debug")
.requires("script")
)
.arg(Arg::with_name("dep")
.help("Add an additional Cargo dependency. Each SPEC can be either just the package name (which will assume the latest version) or a full `name=version` spec.")
.long("dep")
.short("d")
.takes_value(true)
.multiple(true)
.number_of_values(1)
.requires("script")
)
.arg(Arg::with_name("dep_extern")
.help("Like `dep`, except that it *also* adds a `#[macro_use] extern crate name;` item for expression and loop scripts. Note that this only works if the name of the dependency and the name of the library it generates are exactly the same.")
.long("dep-extern")
.short("D")
.takes_value(true)
.multiple(true)
.requires("expr_or_loop")
)
.arg(Arg::with_name("extern")
.help("Adds an `#[macro_use] extern crate name;` item for expressions and loop scripts.")
.long("extern")
.short("x")
.takes_value(true)
.multiple(true)
.requires("expr_or_loop")
)
.arg(Arg::with_name("features")
.help("Cargo features to pass when building and running.")
.long("features")
.takes_value(true)
)
.arg(Arg::with_name("unstable_features")
.help("Add a #![feature] declaration to the crate.")
.long("unstable-feature")
.short("u")
.takes_value(true)
.multiple(true)
.requires("expr_or_loop")
)
.arg(Arg::with_name("build_only")
.help("Build the script, but don't run it.")
.long("build-only")
.requires("script")
.conflicts_with_all(csas!["args"])
)
.arg(Arg::with_name("clear_cache")
.help("Clears out the script cache.")
.long("clear-cache")
)
.arg(Arg::with_name("force")
.help("Force the script to be rebuilt.")
.long("force")
.requires("script")
)
.arg(Arg::with_name("gen_pkg_only")
.help("Generate the Cargo package, but don't compile or run it.")
.long("gen-pkg-only")
.requires("script")
.conflicts_with_all(csas!["args", "build_only", "debug", "force", "test", "bench"])
)
.arg(Arg::with_name("pkg_path")
.help("Specify where to place the generated Cargo package.")
.long("pkg-path")
.takes_value(true)
.requires("script")
.conflicts_with_all(csas!["clear_cache", "force"])
)
.arg(Arg::with_name("use_bincache")
.help("Override whether or not the shared binary cache will be used for compilation.")
.long("use-shared-binary-cache")
.takes_value(true)
.possible_values(csas!["no", "yes"])
)
.arg(Arg::with_name("migrate_data")
.help("Migrate data from older versions.")
.long("migrate-data")
.takes_value(true)
.possible_values(csas!["dry-run", "for-real"])
)
.arg(Arg::with_name("test")
.help("Compile and run tests.")
.long("test")
.conflicts_with_all(csas!["bench", "debug", "args", "force"])
)
.arg(Arg::with_name("bench")
.help("Compile and run benchmarks. Requires a nightly toolchain.")
.long("bench")
.conflicts_with_all(csas!["test", "debug", "args", "force"])
)
.arg(Arg::with_name("template")
.help("Specify a template to use for expression scripts.")
.long("template")
.short("t")
.takes_value(true)
.requires("expr")
)
)
.subcommand(templates::Args::subcommand())
.chain_map(|mut app| {
drop(&mut app); if_windows! {
app = app.subcommand(file_assoc::Args::subcommand());
}
app
})
.get_matches();
if let Some(m) = m.subcommand_matches("templates") {
return ::SubCommand::Templates(templates::Args::parse(m));
}
if_windows! {
if let Some(m) = m.subcommand_matches("file-association") {
return ::SubCommand::FileAssoc(file_assoc::Args::parse(m));
}
}
let m = m.subcommand_matches("script").unwrap();
fn owned_vec_string<'a, I>(v: Option<I>) -> Vec<String>
where I: ::std::iter::Iterator<Item=&'a str> {
v.map(|itr| itr.map(Into::into).collect()).unwrap_or(vec![])
}
fn yes_or_no(v: Option<&str>) -> Option<bool> {
v.map(|v| match v {
"yes" => true,
"no" => false,
_ => unreachable!()
})
}
fn run_kind(v: Option<&str>) -> Option<MigrationKind> {
v.map(|v| match v {
"dry-run" => MigrationKind::DryRun,
"for-real" => MigrationKind::ForReal,
_ => unreachable!()
})
}
::SubCommand::Script(Args {
script: m.value_of("script").map(Into::into),
args: owned_vec_string(m.values_of("args")),
features: m.value_of("features").map(Into::into),
expr: m.is_present("expr"),
loop_: m.is_present("loop"),
count: m.is_present("count"),
pkg_path: m.value_of("pkg_path").map(Into::into),
gen_pkg_only: m.is_present("gen_pkg_only"),
build_only: m.is_present("build_only"),
clear_cache: m.is_present("clear_cache"),
debug: m.is_present("debug"),
dep: owned_vec_string(m.values_of("dep")),
dep_extern: owned_vec_string(m.values_of("dep_extern")),
extern_: owned_vec_string(m.values_of("extern")),
force: m.is_present("force"),
unstable_features: owned_vec_string(m.values_of("unstable_features")),
use_bincache: yes_or_no(m.value_of("use_bincache")),
migrate_data: run_kind(m.value_of("migrate_data")),
build_kind: BuildKind::from_flags(m.is_present("test"), m.is_present("bench")),
template: m.value_of("template").map(Into::into),
})
}
fn main() {
env_logger::init().unwrap();
info!("starting");
info!("args: {:?}", std::env::args().collect::<Vec<_>>());
let stderr = &mut std::io::stderr();
match try_main() {
Ok(0) => (),
Ok(code) => {
std::process::exit(code);
},
Err(ref err) if err.is_human() => {
writeln!(stderr, "error: {}", err).unwrap();
std::process::exit(1);
},
Err(ref err) => {
writeln!(stderr, "internal error: {}", err).unwrap();
std::process::exit(1);
}
}
}
fn try_main() -> Result<i32> {
let args = parse_args();
info!("Arguments: {:?}", args);
let args = match args {
SubCommand::Script(args) => args,
SubCommand::Templates(args) => return templates::try_main(args),
#[cfg(windows)]
SubCommand::FileAssoc(args) => return file_assoc::try_main(args),
};
if let Some(run_kind) = args.migrate_data {
println!("Migrating data...");
let (log, res) = platform::migrate_old_data(run_kind);
match (log.len(), res) {
(0, Ok(())) => {
println!("Nothing to do.");
return Ok(0);
},
(_, Ok(())) => {
for entry in log {
println!("- {}", entry);
}
return Ok(0);
},
(_, Err(err)) => {
for entry in log {
println!("- {}", entry);
}
return Err(err);
}
}
}
if log_enabled!(log::LogLevel::Debug) {
let scp = try!(get_script_cache_path());
let bcp = try!(get_binary_cache_path());
debug!("script-cache path: {:?}", scp);
debug!("binary-cache path: {:?}", bcp);
}
if args.clear_cache {
try!(clean_cache(0));
if args.script.is_none() {
println!("cargo script cache cleared.");
return Ok(0);
}
}
let script_name: String;
let script_path: PathBuf;
let content: String;
let input = match (args.script, args.expr, args.loop_) {
(Some(script), false, false) => {
let (path, mut file) = try!(find_script(script).ok_or("could not find script"));
script_name = path.file_stem()
.map(|os| os.to_string_lossy().into_owned())
.unwrap_or("unknown".into());
let mut body = String::new();
try!(file.read_to_string(&mut body));
let mtime = platform::file_last_modified(&file);
script_path = try!(std::env::current_dir()).join(path);
content = body;
Input::File(&script_name, &script_path, &content, mtime)
},
(Some(expr), true, false) => {
content = expr;
Input::Expr(&content, args.template.as_ref().map(|s| &**s))
},
(Some(loop_), false, true) => {
content = loop_;
Input::Loop(&content, args.count)
},
(None, _, _) => try!(Err((Blame::Human, consts::NO_ARGS_MESSAGE))),
_ => try!(Err((Blame::Human,
"cannot specify both --expr and --loop")))
};
info!("input: {:?}", input);
let deps = {
use std::collections::HashMap;
use std::collections::hash_map::Entry::{Occupied, Vacant};
let mut deps: HashMap<String, String> = HashMap::new();
for dep in args.dep.iter().chain(args.dep_extern.iter()).cloned() {
let dep = match dep.find('=') {
Some(_) => dep,
None => dep + "=*"
};
let mut parts = dep.splitn(2, '=');
let name = parts.next().expect("dependency is missing name");
let version = parts.next().expect("dependency is missing version");
assert!(parts.next().is_none(), "dependency somehow has three parts?!");
if name == "" {
try!(Err((Blame::Human, "cannot have empty dependency package name")));
}
if version == "" {
try!(Err((Blame::Human, "cannot have empty dependency version")));
}
match deps.entry(name.into()) {
Vacant(ve) => {
ve.insert(version.into());
},
Occupied(oe) => {
let existing = oe.get();
if &version != existing {
try!(Err((Blame::Human,
format!("conflicting versions for dependency '{}': '{}', '{}'",
name, existing, version))));
}
}
}
}
let mut deps: Vec<(String, String)> = deps.into_iter().collect();
deps.sort();
deps
};
info!("deps: {:?}", deps);
let prelude_items = {
let unstable_features = args.unstable_features.iter()
.map(|uf| format!("#![feature({})]", uf));
let dep_externs = args.dep_extern.iter()
.map(|d| match d.find('=') {
Some(i) => &d[..i],
None => &d[..]
})
.map(|d| match d.contains('-') {
true => Cow::from(d.replace("-", "_")),
false => Cow::from(d)
})
.map(|d| format!("#[macro_use] extern crate {};", d));
let externs = args.extern_.iter()
.map(|n| format!("#[macro_use] extern crate {};", n));
let mut items: Vec<_> = unstable_features.chain(dep_externs).chain(externs).collect();
items.sort();
items
};
info!("prelude_items: {:?}", prelude_items);
let action = try!(decide_action_for(
&input,
deps,
prelude_items,
args.debug,
args.pkg_path,
args.gen_pkg_only,
args.build_only,
args.force,
args.features,
args.use_bincache,
args.build_kind,
));
info!("action: {:?}", action);
try!(gen_pkg_and_compile(&input, &action));
let _defer_clear = {
let cc = args.clear_cache;
Defer::<_, MainError>::defer(move || {
if !cc {
try!(clean_cache(consts::MAX_CACHE_AGE_MS));
}
Ok(())
})
};
if action.execute {
if action.build_kind.can_exec_directly() {
let exe_path = try!(get_exe_path(action.build_kind, &action.pkg_path));
info!("executing {:?}", exe_path);
match try!(Command::new(exe_path).args(&args.args).status()
.map(|st| st.code().unwrap_or(1)))
{
0 => (),
n => return Ok(n)
}
} else {
let cmd_name = action.build_kind.exec_command();
info!("running `cargo {}`", cmd_name);
let mut cmd = try!(action.cargo(cmd_name));
match try!(cmd.status().map(|st| st.code().unwrap_or(1))) {
0 => (),
n => return Ok(n)
}
}
}
Ok(0)
}
fn clean_cache(max_age: u64) -> Result<()> {
info!("cleaning cache with max_age: {:?}", max_age);
if max_age == 0 {
info!("max_age is 0, clearing binary cache...");
let cache_dir = try!(get_binary_cache_path());
if ALLOW_AUTO_REMOVE {
if let Err(err) = fs::remove_dir_all(&cache_dir) {
error!("failed to remove binary cache {:?}: {}", cache_dir, err);
}
}
}
let cutoff = platform::current_time() - max_age;
info!("cutoff: {:>20?} ms", cutoff);
let cache_dir = try!(get_script_cache_path());
for child in try!(fs::read_dir(cache_dir)) {
let child = try!(child);
let path = child.path();
if path.is_file_polyfill() { continue }
info!("checking: {:?}", path);
let remove_dir = || {
let meta_mtime = {
let meta_path = get_pkg_metadata_path(&path);
let meta_file = match fs::File::open(&meta_path) {
Ok(file) => file,
Err(..) => {
info!("couldn't open metadata for {:?}", path);
return true
}
};
platform::file_last_modified(&meta_file)
};
info!("meta_mtime: {:>20?} ms", meta_mtime);
(meta_mtime <= cutoff)
};
if remove_dir() {
info!("removing {:?}", path);
if ALLOW_AUTO_REMOVE {
if let Err(err) = fs::remove_dir_all(&path) {
error!("failed to remove {:?} from cache: {}", path, err);
}
} else {
info!("(suppressed remove)");
}
}
}
info!("done cleaning cache.");
Ok(())
}
fn gen_pkg_and_compile(
input: &Input,
action: &InputAction,
) -> Result<()> {
let pkg_path = &action.pkg_path;
let meta = &action.metadata;
let old_meta = action.old_metadata.as_ref();
let mani_str = &action.manifest;
let script_str = &action.script;
info!("creating pkg dir...");
try!(fs::create_dir_all(pkg_path));
let cleanup_dir: Defer<_, MainError> = Defer::defer(|| {
if action.using_cache {
info!("cleaning up cache directory {:?}", pkg_path);
if ALLOW_AUTO_REMOVE {
try!(fs::remove_dir_all(pkg_path));
} else {
info!("(suppressed remove)");
}
}
Ok(())
});
let mut meta = meta.clone();
info!("generating Cargo package...");
let mani_path = {
let mani_path = action.manifest_path();
let mani_hash = old_meta.map(|m| &*m.manifest_hash);
match try!(overwrite_file(&mani_path, mani_str, mani_hash)) {
FileOverwrite::Same => (),
FileOverwrite::Changed { new_hash } => {
meta.manifest_hash = new_hash;
},
}
mani_path
};
{
let script_path = pkg_path.join(format!("{}.rs", input.safe_name()));
let script_hash = if action.force_compile {
debug!("told to force compile, ignoring script hash");
None
} else {
old_meta.map(|m| &*m.script_hash)
};
match try!(overwrite_file(&script_path, script_str, script_hash)) {
FileOverwrite::Same => (),
FileOverwrite::Changed { new_hash } => {
meta.script_hash = new_hash;
},
}
}
let meta = meta;
let mut compile_err = Ok(());
if action.compile {
info!("compiling...");
let mut cmd = try!(cargo("build", &*mani_path.to_string_lossy(), action.use_bincache, &meta));
#[cfg(feature="suppress-cargo-output")]
macro_rules! get_status {
($cmd:expr) => {
(match util::suppress_child_output(
&mut $cmd,
::std::time::Duration::from_millis(CARGO_OUTPUT_TIMEOUT)
) {
Ok(v) => v,
Err(e) => return Err(e),
}).status()
}
}
#[cfg(not(feature="suppress-cargo-output"))]
macro_rules! get_status {
($cmd:expr) => {
$cmd.status()
}
}
compile_err = get_status!(cmd).map_err(|e| Into::<MainError>::into(e))
.and_then(|st|
match st.code() {
Some(0) => Ok(()),
Some(st) => Err(format!("cargo failed with status {}", st).into()),
None => Err("cargo failed".into())
});
let _ = try!(compile_err);
let _ = try!(cargo_target(input, pkg_path, &*mani_path.to_string_lossy(), action.use_bincache, &meta));
if action.use_bincache {
let meta_hash = action.metadata.sha1_hash();
info!("writing meta hash: {:?}...", meta_hash);
let exe_meta_hash_path = try!(get_meta_hash_path(action.use_bincache, pkg_path));
let mut f = try!(fs::File::create(&exe_meta_hash_path));
try!(write!(&mut f, "{}", meta_hash));
}
}
if action.emit_metadata {
info!("emitting metadata...");
try!(write_pkg_metadata(pkg_path, &meta));
}
info!("disarming pkg dir cleanup...");
cleanup_dir.disarm();
compile_err
}
#[derive(Debug)]
struct InputAction {
compile: bool,
force_compile: bool,
emit_metadata: bool,
execute: bool,
pkg_path: PathBuf,
using_cache: bool,
use_bincache: bool,
metadata: PackageMetadata,
old_metadata: Option<PackageMetadata>,
manifest: String,
script: String,
build_kind: BuildKind,
}
impl InputAction {
fn manifest_path(&self) -> PathBuf {
self.pkg_path.join("Cargo.toml")
}
fn cargo(&self, cmd: &str) -> Result<Command> {
cargo(cmd, &*self.manifest_path().to_string_lossy(), self.use_bincache, &self.metadata)
}
}
#[derive(Clone, Debug, Eq, PartialEq, RustcDecodable, RustcEncodable)]
struct PackageMetadata {
path: Option<String>,
modified: Option<u64>,
template: Option<String>,
debug: bool,
deps: Vec<(String, String)>,
prelude: Vec<String>,
features: Option<String>,
manifest_hash: String,
script_hash: String,
}
impl PackageMetadata {
pub fn sha1_hash(&self) -> String {
hash_str(&format!("{:?}", self))
}
}
fn decide_action_for(
input: &Input,
deps: Vec<(String, String)>,
prelude: Vec<String>,
debug: bool,
pkg_path: Option<String>,
gen_pkg_only: bool,
build_only: bool,
force: bool,
features: Option<String>,
use_bincache: Option<bool>,
build_kind: BuildKind,
) -> Result<InputAction> {
let (pkg_path, using_cache) = pkg_path.map(|p| (p.into(), false))
.unwrap_or_else(|| {
let cache_path = get_script_cache_path().unwrap();
info!("cache_path: {:?}", cache_path);
let id = {
let deps_iter = deps.iter()
.map(|&(ref n, ref v)| (n as &str, v as &str));
input.compute_id(deps_iter).unwrap()
};
info!("id: {:?}", id);
(cache_path.join(&id), true)
});
info!("pkg_path: {:?}", pkg_path);
info!("using_cache: {:?}", using_cache);
info!("splitting input...");
let (mani_str, script_str) = try!(manifest::split_input(input, &deps, &prelude));
let (debug, force, build_only) = match build_kind {
BuildKind::Normal => (debug, force, build_only),
BuildKind::Test => (true, false, false),
BuildKind::Bench => (false, false, false),
};
let input_meta = {
let (path, mtime, template) = match *input {
Input::File(_, path, _, mtime)
=> (Some(path.to_string_lossy().into_owned()), Some(mtime), None),
Input::Expr(_, template)
=> (None, None, template),
Input::Loop(..)
=> (None, None, None)
};
PackageMetadata {
path: path,
modified: mtime,
template: template.map(Into::into),
debug: debug,
deps: deps,
prelude: prelude,
features: features,
manifest_hash: hash_str(&mani_str),
script_hash: hash_str(&script_str),
}
};
info!("input_meta: {:?}", input_meta);
let mut action = InputAction {
compile: force,
force_compile: force,
emit_metadata: true,
execute: !build_only,
pkg_path: pkg_path,
using_cache: using_cache,
use_bincache: use_bincache.unwrap_or(using_cache),
metadata: input_meta,
old_metadata: None,
manifest: mani_str,
script: script_str,
build_kind: build_kind,
};
macro_rules! bail {
($($name:ident: $value:expr),*) => {
return Ok(InputAction {
$($name: $value,)*
..action
})
}
}
if gen_pkg_only {
bail!(compile: false, execute: false)
}
match action.build_kind {
BuildKind::Normal => (),
BuildKind::Test | BuildKind::Bench => {
info!("not recompiling because: user asked for test/bench");
bail!(compile: false, force_compile: false)
}
}
let cache_meta = match get_pkg_metadata(&action.pkg_path) {
Ok(meta) => meta,
Err(err) => {
info!("recompiling because: failed to load metadata");
debug!("get_pkg_metadata error: {}", err.description());
bail!(compile: true)
}
};
if cache_meta != action.metadata {
info!("recompiling because: metadata did not match");
debug!("input metadata: {:?}", action.metadata);
debug!("cache metadata: {:?}", cache_meta);
bail!(old_metadata: Some(cache_meta), compile: true)
}
action.old_metadata = Some(cache_meta);
let exe_exists = match get_exe_path(action.build_kind, &action.pkg_path) {
Ok(exe_path) => exe_path.is_file_polyfill(),
Err(_) => false,
};
if !exe_exists {
info!("recompiling because: executable doesn't exist or isn't a file");
bail!(compile: true)
}
if action.use_bincache {
let exe_meta_hash_path = get_meta_hash_path(action.use_bincache, &action.pkg_path).unwrap();
if !exe_meta_hash_path.is_file_polyfill() {
info!("recompiling because: meta hash doesn't exist or isn't a file");
bail!(compile: true, force_compile: true)
}
let exe_meta_hash = {
let mut f = try!(fs::File::open(&exe_meta_hash_path));
let mut s = String::new();
try!(f.read_to_string(&mut s));
s
};
let meta_hash = action.metadata.sha1_hash();
if meta_hash != exe_meta_hash {
info!("recompiling because: meta hash doesn't match");
bail!(compile: true, force_compile: true)
}
}
Ok(action)
}
fn get_exe_path<P>(build_kind: BuildKind, pkg_path: P) -> Result<PathBuf>
where P: AsRef<Path> {
use std::fs::File;
match build_kind {
BuildKind::Normal => (),
BuildKind::Test | BuildKind::Bench => {
return Err("tried to get executable path for test/bench build".into());
},
}
let package_path = pkg_path.as_ref();
let cache_path = package_path.join("target.exe_path");
let mut f = try!(File::open(&cache_path));
let exe_path = try!(platform::read_path(&mut f));
Ok(exe_path)
}
fn get_meta_hash_path<P>(use_bincache: bool, pkg_path: P) -> Result<PathBuf>
where P: AsRef<Path> {
if !use_bincache {
panic!("tried to get meta-hash path when not using binary cache");
}
Ok(pkg_path.as_ref().join("target.meta-hash"))
}
fn get_pkg_metadata<P>(pkg_path: P) -> Result<PackageMetadata>
where P: AsRef<Path> {
let meta_path = get_pkg_metadata_path(pkg_path);
debug!("meta_path: {:?}", meta_path);
let mut meta_file = try!(fs::File::open(&meta_path));
let meta_str = {
let mut s = String::new();
meta_file.read_to_string(&mut s).unwrap();
s
};
let meta: PackageMetadata = try!(rustc_serialize::json::decode(&meta_str)
.map_err(|err| err.to_string()));
Ok(meta)
}
fn get_pkg_metadata_path<P>(pkg_path: P) -> PathBuf
where P: AsRef<Path> {
pkg_path.as_ref().join(consts::METADATA_FILE)
}
fn write_pkg_metadata<P>(pkg_path: P, meta: &PackageMetadata) -> Result<()>
where P: AsRef<Path> {
let meta_path = get_pkg_metadata_path(pkg_path);
debug!("meta_path: {:?}", meta_path);
let mut meta_file = try!(fs::File::create(&meta_path));
let meta_str = try!(rustc_serialize::json::encode(meta)
.map_err(|err| err.to_string()));
try!(write!(&mut meta_file, "{}", meta_str));
try!(meta_file.flush());
Ok(())
}
fn get_script_cache_path() -> Result<PathBuf> {
let cache_path = try!(platform::get_cache_dir());
Ok(cache_path.join("script-cache"))
}
fn get_binary_cache_path() -> Result<PathBuf> {
let cache_path = try!(platform::get_cache_dir());
Ok(cache_path.join("binary-cache"))
}
fn find_script<P>(path: P) -> Option<(PathBuf, fs::File)>
where P: AsRef<Path> {
let path = path.as_ref();
if let Ok(file) = fs::File::open(path) {
return Some((path.into(), file));
}
if path.extension().is_some() {
return None;
}
for &ext in consts::SEARCH_EXTS {
let path = path.with_extension(ext);
if let Ok(file) = fs::File::open(&path) {
return Some((path, file));
}
}
None
}
#[derive(Clone, Debug)]
pub enum Input<'a> {
File(&'a str, &'a Path, &'a str, u64),
Expr(&'a str, Option<&'a str>),
Loop(&'a str, bool),
}
impl<'a> Input<'a> {
pub fn safe_name(&self) -> &str {
use Input::*;
match *self {
File(name, _, _, _) => name,
Expr(..) => "expr",
Loop(..) => "loop",
}
}
pub fn package_name(&self) -> String {
let name = self.safe_name();
let mut r = String::with_capacity(name.len());
for (i, c) in name.chars().enumerate() {
match (i, c) {
(0, '0'...'9') => {
r.push('_');
r.push(c);
},
(_, '0'...'9')
| (_, 'a'...'z')
| (_, 'A'...'Z')
| (_, '_')
| (_, '-')
=> {
r.push(c);
},
(_, _) => {
r.push('_');
}
}
}
r
}
pub fn base_path(&self) -> PathBuf {
match *self {
Input::File(_, path, _, _) => path.parent().expect("couldn't get parent directory for file input base path").into(),
Input::Expr(..) | Input::Loop(..) => std::env::current_dir().expect("couldn't get current directory for input base path"),
}
}
pub fn compute_id<'dep, DepIt>(&self, deps: DepIt) -> Result<OsString>
where DepIt: IntoIterator<Item=(&'dep str, &'dep str)> {
use shaman::digest::Digest;
use shaman::sha1::Sha1;
use Input::*;
let hash_deps = || {
let mut hasher = Sha1::new();
for dep in deps {
hasher.input_str("dep=");
hasher.input_str(dep.0);
hasher.input_str("=");
hasher.input_str(dep.1);
hasher.input_str(";");
}
hasher
};
match *self {
File(name, path, _, _) => {
let mut hasher = Sha1::new();
hasher.input_str(&path.to_string_lossy());
let mut digest = hasher.result_str();
digest.truncate(consts::ID_DIGEST_LEN_MAX);
let mut id = OsString::new();
id.push("file-");
id.push(name);
id.push("-");
id.push(if STUB_HASHES { "stub" } else { &*digest });
Ok(id)
},
Expr(content, template) => {
let mut hasher = hash_deps();
hasher.input_str("template:");
hasher.input_str(template.unwrap_or(""));
hasher.input_str(";");
hasher.input_str(&content);
let mut digest = hasher.result_str();
digest.truncate(consts::ID_DIGEST_LEN_MAX);
let mut id = OsString::new();
id.push("expr-");
id.push(if STUB_HASHES { "stub" } else { &*digest });
Ok(id)
},
Loop(content, count) => {
let mut hasher = hash_deps();
hasher.input_str("count:");
hasher.input_str(if count { "true;" } else { "false;" });
hasher.input_str(&content);
let mut digest = hasher.result_str();
digest.truncate(consts::ID_DIGEST_LEN_MAX);
let mut id = OsString::new();
id.push("loop-");
id.push(if STUB_HASHES { "stub" } else { &*digest });
Ok(id)
},
}
}
}
fn hash_str(s: &str) -> String {
use shaman::digest::Digest;
use shaman::sha1::Sha1;
let mut hasher = Sha1::new();
hasher.input_str(s);
hasher.result_str()
}
enum FileOverwrite {
Same,
Changed { new_hash: String },
}
fn overwrite_file<P>(path: P, content: &str, hash: Option<&str>) -> Result<FileOverwrite>
where P: AsRef<Path> {
debug!("overwrite_file({:?}, _, {:?})", path.as_ref(), hash);
let new_hash = hash_str(content);
if Some(&*new_hash) == hash {
debug!(".. hashes match");
return Ok(FileOverwrite::Same);
}
debug!(".. hashes differ; new_hash: {:?}", new_hash);
let mut file = try!(fs::File::create(path));
try!(write!(&mut file, "{}", content));
try!(file.flush());
Ok(FileOverwrite::Changed { new_hash: new_hash })
}
fn cargo(cmd_name: &str, manifest: &str, use_bincache: bool, meta: &PackageMetadata) -> Result<Command> {
let mut cmd = Command::new("cargo");
cmd.arg(cmd_name)
.arg("--manifest-path").arg(manifest);
if platform::force_cargo_color() {
cmd.arg("--color").arg("always");
}
if use_bincache {
cmd.env("CARGO_TARGET_DIR", try!(get_binary_cache_path()));
}
if !meta.debug && cmd_name != "bench" {
cmd.arg("--release");
}
if let Some(ref features) = meta.features {
cmd.arg("--features").arg(features);
}
Ok(cmd)
}
fn cargo_target<P>(input: &Input, pkg_path: P, manifest: &str, use_bincache: bool, meta: &PackageMetadata) -> Result<PathBuf>
where P: AsRef<Path> {
lazy_static! {
static ref VER_JSON_MSGS: Version = Version::parse("0.18.0").unwrap();
}
trace!("cargo_target(_, {:?}, {:?}, {:?}, _)", pkg_path.as_ref(), manifest, use_bincache);
let cargo_ver = try!(cargo_version()
.err_tag("could not determine target filename"));
let exe_path = if cargo_ver < *VER_JSON_MSGS {
try!(cargo_target_by_guess(input, use_bincache, pkg_path.as_ref(), meta))
} else {
try!(cargo_target_by_message(input, manifest, use_bincache, meta))
};
trace!(".. exe_path: {:?}", exe_path);
{
use std::fs::File;
let manifest_path = Path::new(manifest);
let package_path = manifest_path.parent().unwrap();
let cache_path = package_path.join("target.exe_path");
let mut f = try!(File::create(&cache_path));
try!(platform::write_path(&mut f, &exe_path));
}
Ok(exe_path)
}
fn cargo_target_by_guess(input: &Input, use_bincache: bool, pkg_path: &Path, meta: &PackageMetadata) -> Result<PathBuf> {
trace!("cargo_target_by_guess(_, {:?}, {:?}, _)", use_bincache, pkg_path);
let profile = match meta.debug {
true => "debug",
false => "release"
};
let target_path = if use_bincache {
try!(get_binary_cache_path())
} else {
pkg_path.join("target")
};
let mut exe_path = target_path.join(profile).join(&input.package_name()).into_os_string();
exe_path.push(std::env::consts::EXE_SUFFIX);
Ok(exe_path.into())
}
fn cargo_target_by_message(input: &Input, manifest: &str, use_bincache: bool, meta: &PackageMetadata) -> Result<PathBuf> {
use std::io::{BufRead, BufReader};
use rustc_serialize::json;
trace!("cargo_target_by_message(_, {:?}, {:?}, _)", manifest, use_bincache);
let mut cmd = try!(cargo("build", manifest, use_bincache, meta));
cmd.arg("--message-format=json");
cmd.stdout(process::Stdio::piped());
cmd.stderr(process::Stdio::null());
trace!(".. cmd: {:?}", cmd);
let mut child = try!(cmd.spawn());
match try!(child.wait()).code() {
Some(0) => (),
Some(st) => return Err(format!("could not determine target filename: cargo exited with status {}", st).into()),
None => return Err(format!("could not determine target filename: cargo exited abnormally").into()),
}
let mut line = String::with_capacity(1024);
let mut stdout = BufReader::new(child.stdout.take().unwrap());
let null = json::Json::Null;
let package_name = input.package_name();
let mut line_num = 0;
loop {
line_num += 1;
line.clear();
let bytes = try!(stdout.read_line(&mut line));
trace!(".. line {}, {}b: {:?}", line_num, bytes, line);
if bytes == 0 {
return Err("could not determine target filename: did not find appropriate cargo message".into());
}
let msg = try!(json::Json::from_str(line.trim())
.map_err(Box::new));
if msg.find("reason").unwrap_or(&null).as_string() != Some("compiler-artifact") {
trace!(" couldn't find `compiler-artifact`");
continue;
}
if msg.find_path(&["target", "name"]).unwrap_or(&null).as_string() != Some(&package_name) {
trace!(" couldn't find `target.name`, or it wasn't {:?}", package_name);
continue;
}
let exe_path = msg.find_path(&["filenames"])
.expect("could not find `filenames` in json message")
.as_array()
.expect("`filenames` in json message was not an array")
[0]
.as_string()
.expect("`filenames[0]` in json message was not a string");
return Ok(exe_path.into());
}
}
fn cargo_version() -> Result<Version> {
use regex::Regex;
lazy_static! {
static ref RE_VERSION: Regex = Regex::new(r#"^cargo (\S+)"#).unwrap();
}
let mut cmd = Command::new("cargo");
cmd.arg("-V");
let child = try!(cmd.output());
match child.status.code() {
Some(0) => (),
Some(st) => return Err(format!("could not determine cargo version: cargo exited with status {}", st).into()),
None => return Err(format!("could not determine cargo version: cargo exited abnormally").into()),
}
let stdout = String::from_utf8_lossy(&child.stdout);
let m = match RE_VERSION.captures(&stdout) {
Some(m) => m,
None => return Err(format!("could not determine cargo version: output did not match expected").into()),
};
let ver = m.get(1).unwrap();
Ok(try!(Version::parse(ver.as_str())
.map_err(Box::new)))
}