just/src/recipe.rs

294 lines
8.5 KiB
Rust
Raw Normal View History

2017-11-16 23:30:08 -08:00
use common::*;
use std::path::PathBuf;
2017-11-16 23:30:08 -08:00
use std::process::{ExitStatus, Command, Stdio};
use platform::{Platform, PlatformInterface};
/// Return a `RuntimeError::Signal` if the process was terminated by a signal,
/// otherwise return an `RuntimeError::UnknownFailure`
fn error_from_signal(
recipe: &str,
line_number: Option<usize>,
exit_status: ExitStatus
) -> RuntimeError {
match Platform::signal_from_exit_status(exit_status) {
2018-03-05 13:21:35 -08:00
Some(signal) => RuntimeError::Signal{recipe, line_number, signal},
None => RuntimeError::Unknown{recipe, line_number},
2017-11-16 23:30:08 -08:00
}
}
#[derive(PartialEq, Debug)]
pub struct Recipe<'a> {
pub dependencies: Vec<&'a str>,
pub dependency_tokens: Vec<Token<'a>>,
pub doc: Option<&'a str>,
pub line_number: usize,
pub lines: Vec<Vec<Fragment<'a>>>,
pub name: &'a str,
pub parameters: Vec<Parameter<'a>>,
pub private: bool,
pub quiet: bool,
pub shebang: bool,
}
impl<'a> Recipe<'a> {
pub fn argument_range(&self) -> Range<usize> {
self.min_arguments()..self.max_arguments() + 1
}
pub fn min_arguments(&self) -> usize {
self.parameters.iter().filter(|p| !p.default.is_some()).count()
}
pub fn max_arguments(&self) -> usize {
if self.parameters.iter().any(|p| p.variadic) {
usize::MAX - 1
} else {
self.parameters.len()
}
}
pub fn run(
&self,
invocation_directory: &Result<PathBuf, String>,
arguments: &[&'a str],
scope: &Map<&'a str, String>,
2018-03-05 13:21:35 -08:00
dotenv: &Map<String, String>,
exports: &Set<&'a str>,
configuration: &Configuration,
2017-11-17 17:28:06 -08:00
) -> RunResult<'a, ()> {
if configuration.verbose {
let color = configuration.color.stderr().banner();
2017-11-16 23:30:08 -08:00
eprintln!("{}===> Running recipe `{}`...{}", color.prefix(), self.name, color.suffix());
}
let mut argument_map = Map::new();
let mut rest = arguments;
for parameter in &self.parameters {
let value = if rest.is_empty() {
match parameter.default {
Some(ref default) => Cow::Borrowed(default.as_str()),
None => return Err(RuntimeError::Internal{
message: "missing parameter without default".to_string()
}),
}
} else if parameter.variadic {
let value = Cow::Owned(rest.to_vec().join(" "));
rest = &[];
value
} else {
let value = Cow::Borrowed(rest[0]);
rest = &rest[1..];
value
};
argument_map.insert(parameter.name, value);
}
let mut evaluator = AssignmentEvaluator {
assignments: &empty(),
invocation_directory,
2018-03-05 13:21:35 -08:00
dry_run: configuration.dry_run,
evaluated: empty(),
2017-11-16 23:30:08 -08:00
overrides: &empty(),
quiet: configuration.quiet,
shell: configuration.shell,
2018-03-05 13:21:35 -08:00
dotenv,
exports,
scope,
2017-11-16 23:30:08 -08:00
};
if self.shebang {
let mut evaluated_lines = vec![];
for line in &self.lines {
evaluated_lines.push(evaluator.evaluate_line(line, &argument_map)?);
}
if configuration.dry_run || self.quiet {
2017-11-16 23:30:08 -08:00
for line in &evaluated_lines {
eprintln!("{}", line);
}
}
if configuration.dry_run {
2017-11-16 23:30:08 -08:00
return Ok(());
}
let tmp = TempDir::new("just")
.map_err(|error| RuntimeError::TmpdirIoError{recipe: self.name, io_error: error})?;
let mut path = tmp.path().to_path_buf();
path.push(self.name);
{
let mut f = fs::File::create(&path)
.map_err(|error| RuntimeError::TmpdirIoError{recipe: self.name, io_error: error})?;
let mut text = String::new();
// add the shebang
text += &evaluated_lines[0];
text += "\n";
// add blank lines so that lines in the generated script
// have the same line number as the corresponding lines
// in the justfile
for _ in 1..(self.line_number + 2) {
text += "\n"
}
for line in &evaluated_lines[1..] {
text += line;
text += "\n";
}
f.write_all(text.as_bytes())
.map_err(|error| RuntimeError::TmpdirIoError{recipe: self.name, io_error: error})?;
}
// make the script executable
Platform::set_execute_permission(&path)
.map_err(|error| RuntimeError::TmpdirIoError{recipe: self.name, io_error: error})?;
let shebang_line = evaluated_lines.first()
.ok_or_else(|| RuntimeError::Internal {
message: "evaluated_lines was empty".to_string()
})?;
let Shebang{interpreter, argument} = Shebang::new(shebang_line)
.ok_or_else(|| RuntimeError::Internal {
message: format!("bad shebang line: {}", shebang_line)
})?;
// create a command to run the script
let mut command = Platform::make_shebang_command(&path, interpreter, argument)
2017-11-17 17:28:06 -08:00
.map_err(|output_error| RuntimeError::Cygpath{recipe: self.name, output_error})?;
2017-11-16 23:30:08 -08:00
2018-03-05 13:21:35 -08:00
command.export_environment_variables(scope, dotenv, exports)?;
2017-11-16 23:30:08 -08:00
// run it!
match command.status() {
Ok(exit_status) => if let Some(code) = exit_status.code() {
if code != 0 {
2018-03-05 13:21:35 -08:00
return Err(RuntimeError::Code{recipe: self.name, line_number: None, code})
2017-11-16 23:30:08 -08:00
}
} else {
return Err(error_from_signal(self.name, None, exit_status))
},
Err(io_error) => return Err(RuntimeError::Shebang {
recipe: self.name,
command: interpreter.to_string(),
argument: argument.map(String::from),
2018-03-05 13:21:35 -08:00
io_error,
2017-11-16 23:30:08 -08:00
})
};
} else {
let mut lines = self.lines.iter().peekable();
let mut line_number = self.line_number + 1;
loop {
if lines.peek().is_none() {
break;
}
let mut evaluated = String::new();
loop {
if lines.peek().is_none() {
break;
}
let line = lines.next().unwrap();
line_number += 1;
evaluated += &evaluator.evaluate_line(line, &argument_map)?;
if line.last().map(Fragment::continuation).unwrap_or(false) {
evaluated.pop();
} else {
break;
}
}
let mut command = evaluated.as_str();
let quiet_command = command.starts_with('@');
if quiet_command {
command = &command[1..];
}
if command == "" {
continue;
}
if configuration.dry_run ||
configuration.verbose ||
!((quiet_command ^ self.quiet) ||
configuration.quiet
) {
let color = if configuration.highlight {
configuration.color.command()
2017-11-16 23:30:08 -08:00
} else {
configuration.color
2017-11-16 23:30:08 -08:00
};
eprintln!("{}", color.stderr().paint(command));
}
if configuration.dry_run {
2017-11-16 23:30:08 -08:00
continue;
}
let mut cmd = Command::new(configuration.shell);
2017-11-16 23:30:08 -08:00
cmd.arg("-cu").arg(command);
if configuration.quiet {
2017-11-16 23:30:08 -08:00
cmd.stderr(Stdio::null());
cmd.stdout(Stdio::null());
}
2018-03-05 13:21:35 -08:00
cmd.export_environment_variables(scope, dotenv, exports)?;
2017-11-16 23:30:08 -08:00
match cmd.status() {
Ok(exit_status) => if let Some(code) = exit_status.code() {
if code != 0 {
return Err(RuntimeError::Code{
2018-03-05 13:21:35 -08:00
recipe: self.name, line_number: Some(line_number), code,
2017-11-16 23:30:08 -08:00
});
}
} else {
return Err(error_from_signal(self.name, Some(line_number), exit_status));
},
Err(io_error) => return Err(RuntimeError::IoError{
2018-03-05 13:21:35 -08:00
recipe: self.name,
io_error,
}),
2017-11-16 23:30:08 -08:00
};
}
}
Ok(())
}
}
impl<'a> Display for Recipe<'a> {
fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> {
if let Some(doc) = self.doc {
writeln!(f, "# {}", doc)?;
}
write!(f, "{}", self.name)?;
for parameter in &self.parameters {
write!(f, " {}", parameter)?;
}
write!(f, ":")?;
for dependency in &self.dependencies {
write!(f, " {}", dependency)?;
}
for (i, pieces) in self.lines.iter().enumerate() {
if i == 0 {
writeln!(f, "")?;
}
for (j, piece) in pieces.iter().enumerate() {
if j == 0 {
write!(f, " ")?;
}
match *piece {
Fragment::Text{ref text} => write!(f, "{}", text.lexeme)?,
Fragment::Expression{ref expression, ..} =>
write!(f, "{}{}{}", "{{", expression, "}}")?,
}
}
if i + 1 < self.lines.len() {
write!(f, "\n")?;
}
}
Ok(())
}
}