just/src/recipe.rs

357 lines
9.4 KiB
Rust
Raw Normal View History

use crate::common::*;
2017-11-16 23:30:08 -08:00
use std::process::{ExitStatus, Stdio};
2017-11-16 23:30:08 -08:00
/// Return a `RuntimeError::Signal` if the process was terminated by a signal,
/// otherwise return an `RuntimeError::UnknownFailure`
2017-11-16 23:30:08 -08:00
fn error_from_signal(
recipe: &str,
2017-11-16 23:30:08 -08:00
line_number: Option<usize>,
exit_status: ExitStatus,
2017-11-16 23:30:08 -08:00
) -> RuntimeError {
match Platform::signal_from_exit_status(exit_status) {
Some(signal) => RuntimeError::Signal {
recipe,
line_number,
signal,
},
None => RuntimeError::Unknown {
recipe,
line_number,
},
2017-11-16 23:30:08 -08:00
}
}
/// A recipe, e.g. `foo: bar baz`
2021-06-08 01:01:27 -07:00
#[derive(PartialEq, Debug, Clone)]
pub(crate) struct Recipe<'src, D = Dependency<'src>> {
pub(crate) dependencies: Vec<D>,
pub(crate) doc: Option<&'src str>,
pub(crate) body: Vec<Line<'src>>,
pub(crate) name: Name<'src>,
pub(crate) parameters: Vec<Parameter<'src>>,
pub(crate) private: bool,
pub(crate) quiet: bool,
pub(crate) shebang: bool,
2017-11-16 23:30:08 -08:00
}
impl<'src, D> Recipe<'src, D> {
pub(crate) fn argument_range(&self) -> RangeInclusive<usize> {
self.min_arguments()..=self.max_arguments()
2017-11-16 23:30:08 -08:00
}
pub(crate) fn min_arguments(&self) -> usize {
self
.parameters
.iter()
.filter(|p| p.default.is_none() && p.kind != ParameterKind::Star)
.count()
2017-11-16 23:30:08 -08:00
}
pub(crate) fn max_arguments(&self) -> usize {
if self.parameters.iter().any(|p| p.kind.is_variadic()) {
usize::max_value() - 1
2017-11-16 23:30:08 -08:00
} else {
self.parameters.len()
}
}
pub(crate) fn name(&self) -> &'src str {
self.name.lexeme()
}
pub(crate) fn line_number(&self) -> usize {
self.name.line
}
pub(crate) fn public(&self) -> bool {
!self.private
}
pub(crate) fn run<'run>(
2017-11-16 23:30:08 -08:00
&self,
context: &RecipeContext<'src, 'run>,
dotenv: &BTreeMap<String, String>,
scope: Scope<'src, 'run>,
search: &'run Search,
positional: &[String],
) -> RunResult<'src, ()> {
let config = &context.config;
2018-08-27 18:36:40 -07:00
if config.verbosity.loquacious() {
let color = config.color.stderr().banner();
eprintln!(
"{}===> Running recipe `{}`...{}",
color.prefix(),
self.name,
color.suffix()
);
2017-11-16 23:30:08 -08:00
}
let mut evaluator =
Evaluator::recipe_evaluator(context.config, dotenv, &scope, context.settings, search);
2017-11-16 23:30:08 -08:00
if self.shebang {
let mut evaluated_lines = vec![];
for line in &self.body {
evaluated_lines.push(evaluator.evaluate_line(line, false)?);
2017-11-16 23:30:08 -08:00
}
if config.verbosity.loud() && (config.dry_run || self.quiet) {
2017-11-16 23:30:08 -08:00
for line in &evaluated_lines {
eprintln!("{}", line);
}
}
if config.dry_run {
2017-11-16 23:30:08 -08:00
return Ok(());
}
let shebang_line = evaluated_lines
.first()
.ok_or_else(|| RuntimeError::Internal {
message: "evaluated_lines was empty".to_owned(),
})?;
2021-05-17 20:44:12 -07:00
let shebang = Shebang::new(shebang_line).ok_or_else(|| RuntimeError::Internal {
message: format!("bad shebang line: {}", shebang_line),
})?;
2021-05-17 20:44:12 -07:00
let tmp = tempfile::Builder::new()
.prefix("just")
.tempdir()
.map_err(|error| RuntimeError::TmpdirIoError {
recipe: self.name(),
io_error: error,
})?;
2017-11-16 23:30:08 -08:00
let mut path = tmp.path().to_path_buf();
2021-05-17 20:44:12 -07:00
path.push(shebang.script_filename(self.name()));
2017-11-16 23:30:08 -08:00
{
let mut f = fs::File::create(&path).map_err(|error| RuntimeError::TmpdirIoError {
recipe: self.name(),
io_error: error,
})?;
2017-11-16 23:30:08 -08:00
let mut text = String::new();
2021-05-17 20:44:12 -07:00
if shebang.include_shebang_line() {
text += &evaluated_lines[0];
2021-05-17 20:44:12 -07:00
} else {
text += "\n";
}
2021-05-17 20:44:12 -07:00
2017-11-16 23:30:08 -08:00
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";
2017-11-16 23:30:08 -08:00
}
for line in &evaluated_lines[1..] {
text += line;
text += "\n";
}
if config.verbosity.grandiloquent() {
eprintln!("{}", config.color.doc().stderr().paint(&text));
}
2017-11-16 23:30:08 -08:00
f.write_all(text.as_bytes())
.map_err(|error| RuntimeError::TmpdirIoError {
recipe: self.name(),
io_error: error,
})?;
2017-11-16 23:30:08 -08:00
}
// make the script executable
Platform::set_execute_permission(&path).map_err(|error| RuntimeError::TmpdirIoError {
recipe: self.name(),
io_error: error,
})?;
2017-11-16 23:30:08 -08:00
// create a command to run the script
2021-05-17 20:44:12 -07:00
let mut command =
Platform::make_shebang_command(&path, &context.search.working_directory, shebang).map_err(
|output_error| RuntimeError::Cygpath {
recipe: self.name(),
output_error,
},
)?;
2017-11-16 23:30:08 -08:00
if context.settings.positional_arguments {
command.args(positional);
}
command.export(context.settings, dotenv, &scope);
2017-11-16 23:30:08 -08:00
// run it!
match InterruptHandler::guard(|| command.status()) {
Ok(exit_status) =>
if let Some(code) = exit_status.code() {
if code != 0 {
return Err(RuntimeError::Code {
recipe: self.name(),
line_number: None,
code,
});
}
} else {
return Err(error_from_signal(self.name(), None, exit_status));
},
Err(io_error) => {
return Err(RuntimeError::Shebang {
recipe: self.name(),
2021-05-17 20:44:12 -07:00
command: shebang.interpreter.to_owned(),
argument: shebang.argument.map(String::from),
io_error,
});
},
2017-11-16 23:30:08 -08:00
};
} else {
let mut lines = self.body.iter().peekable();
let mut line_number = self.line_number() + 1;
2017-11-16 23:30:08 -08:00
loop {
if lines.peek().is_none() {
break;
}
let mut evaluated = String::new();
let mut continued = false;
let quiet_command = lines.peek().map(|line| line.is_quiet()).unwrap_or(false);
let infallable_command = lines
.peek()
.map(|line| line.is_infallable())
.unwrap_or(false);
2017-11-16 23:30:08 -08:00
loop {
if lines.peek().is_none() {
break;
}
let line = lines.next().unwrap();
line_number += 1;
evaluated += &evaluator.evaluate_line(line, continued)?;
if line.is_continuation() {
continued = true;
2017-11-16 23:30:08 -08:00
evaluated.pop();
} else {
break;
}
}
let mut command = evaluated.as_str();
if quiet_command || infallable_command {
2017-11-16 23:30:08 -08:00
command = &command[1..];
}
if command.is_empty() {
2017-11-16 23:30:08 -08:00
continue;
}
if config.dry_run
|| config.verbosity.loquacious()
|| !((quiet_command ^ self.quiet) || config.verbosity.quiet())
{
let color = if config.highlight {
config.color.command()
2017-11-16 23:30:08 -08:00
} else {
config.color
2017-11-16 23:30:08 -08:00
};
eprintln!("{}", color.stderr().paint(command));
}
if config.dry_run {
2017-11-16 23:30:08 -08:00
continue;
}
let mut cmd = context.settings.shell_command(config);
cmd.current_dir(&context.search.working_directory);
2017-11-16 23:30:08 -08:00
cmd.arg(command);
2017-11-16 23:30:08 -08:00
if context.settings.positional_arguments {
cmd.arg(self.name.lexeme());
cmd.args(positional);
}
if config.verbosity.quiet() {
2017-11-16 23:30:08 -08:00
cmd.stderr(Stdio::null());
cmd.stdout(Stdio::null());
}
cmd.export(context.settings, dotenv, &scope);
2017-11-16 23:30:08 -08:00
match InterruptHandler::guard(|| cmd.status()) {
Ok(exit_status) =>
if let Some(code) = exit_status.code() {
if code != 0 && !infallable_command {
return Err(RuntimeError::Code {
recipe: self.name(),
line_number: Some(line_number),
code,
});
}
} else {
return Err(error_from_signal(
self.name(),
Some(line_number),
exit_status,
));
},
Err(io_error) => {
return Err(RuntimeError::IoError {
recipe: self.name(),
io_error,
});
},
2017-11-16 23:30:08 -08:00
};
}
}
Ok(())
}
}
impl<'src, D> Keyed<'src> for Recipe<'src, D> {
fn key(&self) -> &'src str {
self.name.lexeme()
}
}
2021-06-08 01:01:27 -07:00
impl<'src, D: Display> Display for Recipe<'src, D> {
fn fmt(&self, f: &mut Formatter) -> Result<(), fmt::Error> {
2017-11-16 23:30:08 -08:00
if let Some(doc) = self.doc {
writeln!(f, "# {}", doc)?;
}
if self.quiet {
write!(f, "@{}", self.name)?;
} else {
write!(f, "{}", self.name)?;
}
2017-11-16 23:30:08 -08:00
for parameter in &self.parameters {
write!(f, " {}", parameter)?;
}
write!(f, ":")?;
for dependency in &self.dependencies {
write!(f, " {}", dependency)?;
2017-11-16 23:30:08 -08:00
}
for (i, line) in self.body.iter().enumerate() {
2017-11-16 23:30:08 -08:00
if i == 0 {
2018-08-27 18:36:40 -07:00
writeln!(f)?;
2017-11-16 23:30:08 -08:00
}
for (j, fragment) in line.fragments.iter().enumerate() {
2017-11-16 23:30:08 -08:00
if j == 0 {
write!(f, " ")?;
}
match fragment {
Fragment::Text { token } => write!(f, "{}", token.lexeme())?,
2021-06-08 01:01:27 -07:00
Fragment::Interpolation { expression, .. } => write!(f, "{{{{ {} }}}}", expression)?,
2017-11-16 23:30:08 -08:00
}
}
if i + 1 < self.body.len() {
2018-08-27 18:36:40 -07:00
writeln!(f)?;
2017-11-16 23:30:08 -08:00
}
}
Ok(())
}
}