1
0
Fork 0
fayalite/crates/fayalite/src/build/external.rs

1101 lines
36 KiB
Rust

// SPDX-License-Identifier: LGPL-3.0-or-later
// See Notices.txt for copyright information
use crate::{
build::{
ArgsWriter, CommandParams, GetBaseJob, JobAndDependencies, JobAndKind,
JobArgsAndDependencies, JobDependencies, JobItem, JobItemName, JobKind, JobKindAndArgs,
JobParams, ToArgs, WriteArgs, intern_known_utf8_path_buf,
},
intern::{Intern, Interned},
util::{job_server::AcquiredJob, streaming_read_utf8::streaming_read_utf8},
};
use base64::{Engine, prelude::BASE64_URL_SAFE_NO_PAD};
use clap::builder::OsStringValueParser;
use eyre::{Context, bail, ensure, eyre};
use serde::{
Deserialize, Deserializer, Serialize, Serializer,
de::{DeserializeOwned, Error},
};
use std::{
borrow::Cow,
collections::BTreeMap,
ffi::{OsStr, OsString},
fmt,
hash::{Hash, Hasher},
io::Write,
marker::PhantomData,
path::{Path, PathBuf},
sync::OnceLock,
};
#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Debug, Serialize, Deserialize)]
#[non_exhaustive]
pub enum ExternalJobCacheVersion {
/// not used, used to be for `FormalCacheVersion`
V1,
V2,
}
impl ExternalJobCacheVersion {
pub const CURRENT: Self = Self::V2;
}
#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)]
#[non_exhaustive]
pub enum MaybeUtf8 {
Utf8(String),
Binary(Vec<u8>),
}
impl MaybeUtf8 {
pub fn as_bytes(&self) -> &[u8] {
match self {
MaybeUtf8::Utf8(v) => v.as_bytes(),
MaybeUtf8::Binary(v) => v,
}
}
}
#[derive(Serialize, Deserialize)]
#[serde(rename = "MaybeUtf8")]
enum MaybeUtf8Serde<'a> {
Utf8(Cow<'a, str>),
Binary(String),
}
impl<'de> Deserialize<'de> for MaybeUtf8 {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
Ok(match MaybeUtf8Serde::deserialize(deserializer)? {
MaybeUtf8Serde::Utf8(v) => Self::Utf8(v.into_owned()),
MaybeUtf8Serde::Binary(v) => BASE64_URL_SAFE_NO_PAD
.decode(&*v)
.map_err(D::Error::custom)?
.into(),
})
}
}
impl Serialize for MaybeUtf8 {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
match self {
MaybeUtf8::Utf8(v) => MaybeUtf8Serde::Utf8(Cow::Borrowed(v)),
MaybeUtf8::Binary(v) => MaybeUtf8Serde::Binary(BASE64_URL_SAFE_NO_PAD.encode(v)),
}
.serialize(serializer)
}
}
impl From<Vec<u8>> for MaybeUtf8 {
fn from(value: Vec<u8>) -> Self {
match String::from_utf8(value) {
Ok(value) => Self::Utf8(value),
Err(e) => Self::Binary(e.into_bytes()),
}
}
}
impl From<String> for MaybeUtf8 {
fn from(value: String) -> Self {
Self::Utf8(value)
}
}
#[derive(Clone, PartialEq, Eq, Debug, Serialize, Deserialize)]
#[serde(rename = "ExternalJobCache")]
pub struct ExternalJobCacheV2 {
pub version: ExternalJobCacheVersion,
pub inputs_hash: blake3::Hash,
pub stdout_stderr: String,
pub result: Result<BTreeMap<String, MaybeUtf8>, String>,
}
impl ExternalJobCacheV2 {
fn read_from_file(cache_json_path: Interned<str>) -> eyre::Result<Self> {
let cache_str = std::fs::read_to_string(&*cache_json_path)
.wrap_err_with(|| format!("can't read {cache_json_path}"))?;
serde_json::from_str(&cache_str).wrap_err_with(|| format!("can't decode {cache_json_path}"))
}
fn write_to_file(&self, cache_json_path: Interned<str>) -> eyre::Result<()> {
let cache_str = serde_json::to_string_pretty(&self).expect("serialization can't fail");
std::fs::write(&*cache_json_path, cache_str)
.wrap_err_with(|| format!("can't write {cache_json_path}"))
}
}
#[derive(Copy, Clone, PartialEq, Eq, Hash, Debug)]
pub struct ExternalJobCaching {
cache_json_path: Interned<str>,
run_even_if_cached: bool,
}
#[derive(Default)]
struct JobCacheHasher(blake3::Hasher);
impl JobCacheHasher {
fn hash_size(&mut self, size: usize) {
self.0.update(&u64::to_le_bytes(
size.try_into().expect("size should fit in u64"),
));
}
fn hash_sized_bytes(&mut self, bytes: &[u8]) {
self.hash_size(bytes.len());
self.0.update(bytes);
}
fn hash_sized_str(&mut self, s: &str) {
self.hash_sized_bytes(s.as_bytes());
}
fn hash_iter<F: FnMut(&mut Self, I::Item), I: IntoIterator<IntoIter: ExactSizeIterator>>(
&mut self,
iter: I,
mut f: F,
) {
let iter = iter.into_iter();
self.hash_size(iter.len());
iter.for_each(|item| f(self, item));
}
fn try_hash_iter<
F: FnMut(&mut Self, I::Item) -> Result<(), E>,
E,
I: IntoIterator<IntoIter: ExactSizeIterator>,
>(
&mut self,
iter: I,
mut f: F,
) -> Result<(), E> {
let mut iter = iter.into_iter();
self.hash_size(iter.len());
iter.try_for_each(|item| f(self, item))
}
}
fn write_file_atomically_no_clobber<F: FnOnce() -> C, C: AsRef<[u8]>>(
path: impl AsRef<Path>,
containing_dir: impl AsRef<Path>,
contents: F,
) -> std::io::Result<()> {
let path = path.as_ref();
let containing_dir = containing_dir.as_ref();
if !matches!(std::fs::exists(&path), Ok(true)) {
// use File::create_new rather than tempfile's code to get normal file permissions rather than mode 600 on Unix.
let mut file = tempfile::Builder::new()
.make_in(containing_dir, |path| std::fs::File::create_new(path))?;
file.write_all(contents().as_ref())?; // write all in one operation to avoid a bunch of tiny writes
file.into_temp_path().persist_noclobber(path)?;
}
Ok(())
}
impl ExternalJobCaching {
pub fn get_cache_dir_from_output_dir(output_dir: &str) -> PathBuf {
Path::join(output_dir.as_ref(), ".fayalite-job-cache")
}
pub fn make_cache_dir(
cache_dir: impl AsRef<Path>,
application_name: &str,
) -> std::io::Result<()> {
let cache_dir = cache_dir.as_ref();
std::fs::create_dir_all(cache_dir)?;
write_file_atomically_no_clobber(cache_dir.join("CACHEDIR.TAG"), cache_dir, || {
format!(
"Signature: 8a477f597d28d172789f06886806bc55\n\
# This file is a cache directory tag created by {application_name}.\n\
# For information about cache directory tags see https://bford.info/cachedir/\n"
)
})?;
write_file_atomically_no_clobber(cache_dir.join(".gitignore"), cache_dir, || {
format!(
"# This is a cache directory created by {application_name}.\n\
# ignore all files\n\
*\n"
)
})
}
pub fn new(
output_dir: &str,
application_name: &str,
json_file_stem: &str,
run_even_if_cached: bool,
) -> std::io::Result<Self> {
let cache_dir = Self::get_cache_dir_from_output_dir(output_dir);
Self::make_cache_dir(&cache_dir, application_name)?;
let mut cache_json_path = cache_dir;
cache_json_path.push(json_file_stem);
cache_json_path.set_extension("json");
let cache_json_path = intern_known_utf8_path_buf(cache_json_path);
Ok(Self {
cache_json_path,
run_even_if_cached,
})
}
fn write_stdout_stderr(stdout_stderr: &str) {
if stdout_stderr == "" {
return;
}
// use print! so output goes to Rust test output capture
if stdout_stderr.ends_with('\n') {
print!("{stdout_stderr}");
} else {
println!("{stdout_stderr}");
}
}
/// returns `Err(_)` if reading the cache failed, otherwise returns `Ok(_)` with the results from the cache
fn run_from_cache(
self,
inputs_hash: blake3::Hash,
output_file_paths: impl IntoIterator<Item = Interned<str>>,
) -> Result<Result<(), String>, ()> {
if self.run_even_if_cached {
return Err(());
}
let Ok(ExternalJobCacheV2 {
version: ExternalJobCacheVersion::CURRENT,
inputs_hash: cached_inputs_hash,
stdout_stderr,
result,
}) = ExternalJobCacheV2::read_from_file(self.cache_json_path)
else {
return Err(());
};
if inputs_hash != cached_inputs_hash {
return Err(());
}
match result {
Ok(outputs) => {
for output_file_path in output_file_paths {
let Some(output_data) = outputs.get(&*output_file_path) else {
if let Ok(true) = std::fs::exists(&*output_file_path) {
// assume the existing file is the correct one
continue;
}
return Err(());
};
let Ok(()) = std::fs::write(&*output_file_path, output_data.as_bytes()) else {
return Err(());
};
}
Self::write_stdout_stderr(&stdout_stderr);
Ok(Ok(()))
}
Err(error) => {
Self::write_stdout_stderr(&stdout_stderr);
Ok(Err(error))
}
}
}
fn make_command(
command_line: Interned<[Interned<str>]>,
) -> eyre::Result<std::process::Command> {
ensure!(!command_line.is_empty(), "command line must not be empty");
let mut cmd = std::process::Command::new(&*command_line[0]);
cmd.args(command_line[1..].iter().map(|arg| &**arg))
.stdin(std::process::Stdio::null());
Ok(cmd)
}
pub fn run<F: FnOnce(std::process::Command) -> eyre::Result<()>>(
self,
command_line: Interned<[Interned<str>]>,
input_file_paths: impl IntoIterator<Item = Interned<str>>,
output_file_paths: impl IntoIterator<Item = Interned<str>> + Clone,
run_fn: F,
) -> eyre::Result<()> {
let mut hasher = JobCacheHasher::default();
hasher.hash_iter(command_line.iter(), |hasher, arg| {
hasher.hash_sized_str(arg)
});
let mut input_file_paths =
Vec::<&str>::from_iter(input_file_paths.into_iter().map(Interned::into_inner));
input_file_paths.sort_unstable();
input_file_paths.dedup();
hasher.try_hash_iter(
&input_file_paths,
|hasher, input_file_path| -> eyre::Result<()> {
hasher.hash_sized_str(input_file_path);
hasher.hash_sized_bytes(
&std::fs::read(input_file_path).wrap_err_with(|| {
format!("can't read job input file: {input_file_path}")
})?,
);
Ok(())
},
)?;
let inputs_hash = hasher.0.finalize();
match self.run_from_cache(inputs_hash, output_file_paths.clone()) {
Ok(result) => return result.map_err(|e| eyre!(e)),
Err(()) => {}
}
let (pipe_reader, stdout, stderr) = std::io::pipe()
.and_then(|(r, w)| Ok((r, w.try_clone()?, w)))
.wrap_err_with(|| format!("when trying to create a pipe to run: {command_line:?}"))?;
let mut cmd = Self::make_command(command_line)?;
cmd.stdout(stdout).stderr(stderr);
let mut stdout_stderr = String::new();
let result = std::thread::scope(|scope| {
std::thread::Builder::new()
.name(format!("stdout:{}", command_line[0]))
.spawn_scoped(scope, || {
let _ = streaming_read_utf8(std::io::BufReader::new(pipe_reader), |s| {
stdout_stderr.push_str(s);
// use print! so output goes to Rust test output capture
print!("{s}");
std::io::Result::Ok(())
});
if !stdout_stderr.is_empty() && !stdout_stderr.ends_with('\n') {
println!();
}
})
.expect("spawn shouldn't fail");
run_fn(cmd)
});
ExternalJobCacheV2 {
version: ExternalJobCacheVersion::CURRENT,
inputs_hash,
stdout_stderr,
result: match &result {
Ok(()) => Ok(Result::from_iter(output_file_paths.into_iter().map(
|output_file_path: Interned<str>| -> eyre::Result<_> {
let output_file_path = &*output_file_path;
Ok((
String::from(output_file_path),
MaybeUtf8::from(std::fs::read(output_file_path).wrap_err_with(
|| format!("can't read job output file: {output_file_path}"),
)?),
))
},
))?),
Err(e) => Err(format!("{e:#}")),
},
}
.write_to_file(self.cache_json_path)?;
result
}
pub fn run_maybe_cached<F: FnOnce(std::process::Command) -> eyre::Result<()>>(
this: Option<Self>,
command_line: Interned<[Interned<str>]>,
input_file_paths: impl IntoIterator<Item = Interned<str>>,
output_file_paths: impl IntoIterator<Item = Interned<str>> + Clone,
run_fn: F,
) -> eyre::Result<()> {
match this {
Some(this) => this.run(command_line, input_file_paths, output_file_paths, run_fn),
None => run_fn(Self::make_command(command_line)?),
}
}
}
#[derive(Clone, Eq, Hash)]
pub struct ExternalCommandJobKind<T: ExternalCommand>(PhantomData<T>);
impl<T: ExternalCommand> fmt::Debug for ExternalCommandJobKind<T> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "ExternalCommandJobKind<{}>", std::any::type_name::<T>())
}
}
impl<T: ExternalCommand> PartialEq for ExternalCommandJobKind<T> {
fn eq(&self, _other: &Self) -> bool {
true
}
}
impl<T: ExternalCommand> Ord for ExternalCommandJobKind<T> {
fn cmp(&self, _other: &Self) -> std::cmp::Ordering {
std::cmp::Ordering::Equal
}
}
impl<T: ExternalCommand> PartialOrd for ExternalCommandJobKind<T> {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(self.cmp(other))
}
}
impl<T: ExternalCommand> Default for ExternalCommandJobKind<T> {
fn default() -> Self {
Self(PhantomData)
}
}
impl<T: ExternalCommand> Copy for ExternalCommandJobKind<T> {}
impl<T: ExternalCommand> ExternalCommandJobKind<T> {
pub const fn new() -> Self {
Self(PhantomData)
}
}
#[derive(Copy, Clone)]
struct ExternalProgramPathValueParser(ExternalProgram);
fn parse_which_result(
which_result: which::Result<PathBuf>,
program_name: impl Into<OsString>,
program_path_arg_name: impl FnOnce() -> String,
) -> Result<Interned<str>, ResolveProgramPathError> {
let which_result = match which_result {
Ok(v) => v,
Err(e) => {
return Err(ResolveProgramPathError {
inner: ResolveProgramPathErrorInner::Which(e),
program_name: program_name.into(),
program_path_arg_name: program_path_arg_name(),
});
}
};
Ok(str::intern_owned(
which_result
.into_os_string()
.into_string()
.map_err(|_| ResolveProgramPathError {
inner: ResolveProgramPathErrorInner::NotValidUtf8,
program_name: program_name.into(),
program_path_arg_name: program_path_arg_name(),
})?,
))
}
impl clap::builder::TypedValueParser for ExternalProgramPathValueParser {
type Value = Interned<str>;
fn parse_ref(
&self,
cmd: &clap::Command,
arg: Option<&clap::Arg>,
value: &OsStr,
) -> clap::error::Result<Self::Value> {
let program_path_arg_name = self.0.program_path_arg_name;
OsStringValueParser::new()
.try_map(move |program_name| {
parse_which_result(which::which(&program_name), program_name, || {
program_path_arg_name.into()
})
})
.parse_ref(cmd, arg, value)
}
}
#[derive(Clone, PartialEq, Eq, Hash, Debug, clap::Args)]
#[group(id = T::args_group_id())]
#[non_exhaustive]
pub struct ExternalCommandArgs<T: ExternalCommand> {
#[command(flatten)]
pub program_path: ExternalProgramPath<T::ExternalProgram>,
#[arg(
name = Interned::into_inner(T::run_even_if_cached_arg_name()),
long = T::run_even_if_cached_arg_name(),
)]
pub run_even_if_cached: bool,
#[command(flatten)]
pub additional_args: T::AdditionalArgs,
}
#[derive(Clone)]
enum ResolveProgramPathErrorInner {
Which(which::Error),
NotValidUtf8,
}
impl fmt::Debug for ResolveProgramPathErrorInner {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Which(v) => v.fmt(f),
Self::NotValidUtf8 => f.write_str("NotValidUtf8"),
}
}
}
impl fmt::Display for ResolveProgramPathErrorInner {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Which(v) => v.fmt(f),
Self::NotValidUtf8 => f.write_str("path is not valid UTF-8"),
}
}
}
#[derive(Clone, Debug)]
pub struct ResolveProgramPathError {
inner: ResolveProgramPathErrorInner,
program_name: std::ffi::OsString,
program_path_arg_name: String,
}
impl fmt::Display for ResolveProgramPathError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let Self {
inner,
program_name,
program_path_arg_name,
} = self;
write!(
f,
"{program_path_arg_name}: failed to resolve {program_name:?} to a valid program: {inner}",
)
}
}
impl std::error::Error for ResolveProgramPathError {}
pub fn resolve_program_path(
program_name: Option<&OsStr>,
default_program_name: impl AsRef<OsStr>,
program_path_env_var_name: Option<&OsStr>,
) -> Result<Interned<str>, ResolveProgramPathError> {
let default_program_name = default_program_name.as_ref();
let owned_program_name;
let program_name = if let Some(program_name) = program_name {
program_name
} else if let Some(v) = program_path_env_var_name.and_then(std::env::var_os) {
owned_program_name = v;
&owned_program_name
} else {
default_program_name
};
parse_which_result(which::which(program_name), program_name, || {
default_program_name.display().to_string()
})
}
impl<T: ExternalCommand> ExternalCommandArgs<T> {
pub fn with_resolved_program_path(
program_path: Interned<str>,
additional_args: T::AdditionalArgs,
) -> Self {
Self::new(
ExternalProgramPath::with_resolved_program_path(program_path),
additional_args,
)
}
pub fn new(
program_path: ExternalProgramPath<T::ExternalProgram>,
additional_args: T::AdditionalArgs,
) -> Self {
Self {
program_path,
run_even_if_cached: false,
additional_args,
}
}
pub fn resolve_program_path(
program_name: Option<&OsStr>,
additional_args: T::AdditionalArgs,
) -> Result<Self, ResolveProgramPathError> {
Ok(Self::new(
ExternalProgramPath::resolve_program_path(program_name)?,
additional_args,
))
}
}
impl<T: ExternalCommand> ToArgs for ExternalCommandArgs<T> {
fn to_args(&self, args: &mut (impl WriteArgs + ?Sized)) {
let Self {
program_path,
run_even_if_cached,
ref additional_args,
} = *self;
program_path.to_args(args);
if run_even_if_cached {
args.write_arg(format_args!("--{}", T::run_even_if_cached_arg_name()));
}
additional_args.to_args(args);
}
}
#[derive(Copy, Clone)]
struct ExternalCommandJobParams {
command_params: CommandParams,
inputs: Interned<[JobItemName]>,
outputs: Interned<[JobItemName]>,
output_paths: Interned<[Interned<str>]>,
}
impl ExternalCommandJobParams {
fn new<T: ExternalCommand>(job: &ExternalCommandJob<T>) -> Self {
let output_paths = T::output_paths(job);
let mut command_line = ArgsWriter(vec![job.program_path]);
T::command_line_args(job, &mut command_line);
Self {
command_params: CommandParams {
command_line: Intern::intern_owned(command_line.0),
current_dir: T::current_dir(job),
},
inputs: T::inputs(job),
outputs: output_paths
.iter()
.map(|&path| JobItemName::Path { path })
.collect(),
output_paths,
}
}
}
#[derive(Deserialize, Serialize)]
pub struct ExternalCommandJob<T: ExternalCommand> {
additional_job_data: T::AdditionalJobData,
program_path: Interned<str>,
output_dir: Interned<str>,
run_even_if_cached: bool,
#[serde(skip)]
params_cache: OnceLock<ExternalCommandJobParams>,
}
impl<T: ExternalCommand> Eq for ExternalCommandJob<T> {}
impl<T: ExternalCommand<AdditionalJobData: Clone>> Clone for ExternalCommandJob<T> {
fn clone(&self) -> Self {
let Self {
ref additional_job_data,
program_path,
output_dir,
run_even_if_cached,
ref params_cache,
} = *self;
Self {
additional_job_data: additional_job_data.clone(),
program_path,
output_dir,
run_even_if_cached,
params_cache: params_cache.clone(),
}
}
}
impl<T: ExternalCommand> fmt::Debug for ExternalCommandJob<T> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let Self {
additional_job_data,
program_path,
output_dir,
run_even_if_cached,
params_cache: _,
} = self;
write!(f, "ExternalCommandJob<{}>", std::any::type_name::<T>())?;
f.debug_struct("")
.field("additional_job_data", additional_job_data)
.field("program_path", program_path)
.field("output_dir", output_dir)
.field("run_even_if_cached", run_even_if_cached)
.finish()
}
}
impl<T: ExternalCommand> PartialEq for ExternalCommandJob<T> {
fn eq(&self, other: &Self) -> bool {
let Self {
additional_job_data,
program_path,
output_dir,
run_even_if_cached,
params_cache: _,
} = self;
*additional_job_data == other.additional_job_data
&& *program_path == other.program_path
&& *output_dir == other.output_dir
&& *run_even_if_cached == other.run_even_if_cached
}
}
impl<T: ExternalCommand> Hash for ExternalCommandJob<T> {
fn hash<H: Hasher>(&self, state: &mut H) {
let Self {
additional_job_data,
program_path,
output_dir,
run_even_if_cached,
params_cache: _,
} = self;
additional_job_data.hash(state);
program_path.hash(state);
output_dir.hash(state);
run_even_if_cached.hash(state);
}
}
impl<T: ExternalCommand> ExternalCommandJob<T> {
pub fn additional_job_data(&self) -> &T::AdditionalJobData {
&self.additional_job_data
}
pub fn program_path(&self) -> Interned<str> {
self.program_path
}
pub fn output_dir(&self) -> Interned<str> {
self.output_dir
}
pub fn run_even_if_cached(&self) -> bool {
self.run_even_if_cached
}
fn params(&self) -> &ExternalCommandJobParams {
self.params_cache
.get_or_init(|| ExternalCommandJobParams::new(self))
}
pub fn command_params(&self) -> CommandParams {
self.params().command_params
}
pub fn inputs(&self) -> Interned<[JobItemName]> {
self.params().inputs
}
pub fn output_paths(&self) -> Interned<[Interned<str>]> {
self.params().output_paths
}
pub fn outputs(&self) -> Interned<[JobItemName]> {
self.params().outputs
}
}
#[derive(Copy, Clone, PartialEq, Eq, Hash)]
pub struct ExternalProgramPath<T: ExternalProgramTrait> {
program_path: Interned<str>,
_phantom: PhantomData<T>,
}
impl<T: ExternalProgramTrait> ExternalProgramPath<T> {
pub fn with_resolved_program_path(program_path: Interned<str>) -> Self {
Self {
program_path,
_phantom: PhantomData,
}
}
pub fn resolve_program_path(
program_name: Option<&OsStr>,
) -> Result<Self, ResolveProgramPathError> {
let ExternalProgram {
default_program_name,
program_path_arg_name: _,
program_path_arg_value_name: _,
program_path_env_var_name,
} = ExternalProgram::new::<T>();
Ok(Self {
program_path: resolve_program_path(
program_name,
default_program_name,
program_path_env_var_name.as_ref().map(OsStr::new),
)?,
_phantom: PhantomData,
})
}
pub fn program_path(&self) -> Interned<str> {
self.program_path
}
}
impl<T: ExternalProgramTrait> fmt::Debug for ExternalProgramPath<T> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let Self {
program_path,
_phantom: _,
} = self;
write!(f, "ExternalProgramPath<{}>", std::any::type_name::<T>())?;
f.debug_tuple("").field(program_path).finish()
}
}
impl<T: ExternalProgramTrait> clap::FromArgMatches for ExternalProgramPath<T> {
fn from_arg_matches(matches: &clap::ArgMatches) -> Result<Self, clap::Error> {
let id = Interned::into_inner(ExternalProgram::new::<T>().program_path_arg_name);
// don't remove argument so later instances of Self can use it too
let program_path = *matches.get_one(id).expect("arg should always be present");
Ok(Self {
program_path,
_phantom: PhantomData,
})
}
fn update_from_arg_matches(&mut self, matches: &clap::ArgMatches) -> Result<(), clap::Error> {
*self = Self::from_arg_matches(matches)?;
Ok(())
}
}
impl<T: ExternalProgramTrait> clap::Args for ExternalProgramPath<T> {
fn augment_args(cmd: clap::Command) -> clap::Command {
let external_program @ ExternalProgram {
default_program_name,
program_path_arg_name,
program_path_arg_value_name,
program_path_env_var_name,
} = ExternalProgram::new::<T>();
let arg = cmd
.get_arguments()
.find(|arg| *arg.get_id().as_str() == *program_path_arg_name);
if let Some(arg) = arg {
// don't insert duplicate arguments.
// check that the previous argument actually matches this argument:
assert!(!arg.is_required_set());
assert!(matches!(arg.get_action(), clap::ArgAction::Set));
assert_eq!(arg.get_long(), Some(&*program_path_arg_name));
assert_eq!(
arg.get_value_names(),
Some(&[clap::builder::Str::from(program_path_arg_value_name)][..])
);
assert_eq!(
arg.get_env(),
program_path_env_var_name.as_ref().map(OsStr::new)
);
assert_eq!(
arg.get_default_values(),
&[OsStr::new(&default_program_name)]
);
assert_eq!(arg.get_value_hint(), clap::ValueHint::CommandName);
cmd
} else {
cmd.arg(
clap::Arg::new(Interned::into_inner(program_path_arg_name))
.required(false)
.value_parser(ExternalProgramPathValueParser(external_program))
.action(clap::ArgAction::Set)
.long(program_path_arg_name)
.value_name(program_path_arg_value_name)
.env(program_path_env_var_name.map(Interned::into_inner))
.default_value(default_program_name)
.value_hint(clap::ValueHint::CommandName),
)
}
}
fn augment_args_for_update(cmd: clap::Command) -> clap::Command {
Self::augment_args(cmd)
}
}
impl<T: ExternalProgramTrait> ToArgs for ExternalProgramPath<T> {
fn to_args(&self, args: &mut (impl WriteArgs + ?Sized)) {
let ExternalProgram {
program_path_arg_name,
..
} = ExternalProgram::new::<T>();
let Self {
program_path,
_phantom: _,
} = self;
if args.get_long_option_eq(program_path_arg_name) != Some(&**program_path) {
args.write_arg(format_args!("--{program_path_arg_name}={program_path}"));
}
}
}
#[derive(Copy, Clone, PartialEq, Eq, Hash, Debug)]
#[non_exhaustive]
pub struct ExternalProgram {
default_program_name: Interned<str>,
program_path_arg_name: Interned<str>,
program_path_arg_value_name: Interned<str>,
program_path_env_var_name: Option<Interned<str>>,
}
impl ExternalProgram {
pub fn new<T: ExternalProgramTrait>() -> Self {
Self {
default_program_name: T::default_program_name(),
program_path_arg_name: T::program_path_arg_name(),
program_path_arg_value_name: T::program_path_arg_value_name(),
program_path_env_var_name: T::program_path_env_var_name(),
}
}
pub fn default_program_name(&self) -> Interned<str> {
self.default_program_name
}
pub fn program_path_arg_name(&self) -> Interned<str> {
self.program_path_arg_name
}
pub fn program_path_arg_value_name(&self) -> Interned<str> {
self.program_path_arg_value_name
}
pub fn program_path_env_var_name(&self) -> Option<Interned<str>> {
self.program_path_env_var_name
}
}
impl<T: ExternalProgramTrait> From<T> for ExternalProgram {
fn from(_value: T) -> Self {
Self::new::<T>()
}
}
impl<T: ExternalProgramTrait> From<T> for Interned<ExternalProgram> {
fn from(_value: T) -> Self {
ExternalProgram::new::<T>().intern_sized()
}
}
pub trait ExternalProgramTrait:
'static + Send + Sync + Hash + Ord + fmt::Debug + Default + Copy
{
fn program_path_arg_name() -> Interned<str> {
Self::default_program_name()
}
fn program_path_arg_value_name() -> Interned<str> {
Intern::intern_owned(Self::program_path_arg_name().to_uppercase())
}
fn default_program_name() -> Interned<str>;
fn program_path_env_var_name() -> Option<Interned<str>> {
Some(Intern::intern_owned(
Self::program_path_arg_name()
.to_uppercase()
.replace('-', "_"),
))
}
}
pub trait ExternalCommand: 'static + Send + Sync + Hash + Eq + fmt::Debug + Sized + Clone {
type AdditionalArgs: ToArgs;
type AdditionalJobData: 'static
+ Send
+ Sync
+ Hash
+ Eq
+ fmt::Debug
+ Serialize
+ DeserializeOwned;
type Dependencies: JobDependencies<JobsAndKinds: GetBaseJob>;
type ExternalProgram: ExternalProgramTrait;
fn dependencies() -> Self::Dependencies;
fn args_to_jobs(
args: JobArgsAndDependencies<ExternalCommandJobKind<Self>>,
params: &JobParams,
) -> eyre::Result<(
Self::AdditionalJobData,
<Self::Dependencies as JobDependencies>::JobsAndKinds,
)>;
fn inputs(job: &ExternalCommandJob<Self>) -> Interned<[JobItemName]>;
fn output_paths(job: &ExternalCommandJob<Self>) -> Interned<[Interned<str>]>;
fn command_line_args<W: ?Sized + WriteArgs>(job: &ExternalCommandJob<Self>, args: &mut W);
fn current_dir(job: &ExternalCommandJob<Self>) -> Option<Interned<str>>;
fn job_kind_name() -> Interned<str>;
fn args_group_id() -> clap::Id {
Interned::into_inner(Self::job_kind_name()).into()
}
fn run_even_if_cached_arg_name() -> Interned<str> {
Intern::intern_owned(format!("{}-run-even-if-cached", Self::job_kind_name()))
}
fn subcommand_hidden() -> bool {
false
}
}
impl<T: ExternalCommand> JobKind for ExternalCommandJobKind<T> {
type Args = ExternalCommandArgs<T>;
type Job = ExternalCommandJob<T>;
type Dependencies = T::Dependencies;
fn dependencies(self) -> Self::Dependencies {
T::dependencies()
}
fn args_to_jobs(
args: JobArgsAndDependencies<Self>,
params: &JobParams,
) -> eyre::Result<JobAndDependencies<Self>> {
let JobKindAndArgs {
kind,
args:
ExternalCommandArgs {
program_path:
ExternalProgramPath {
program_path,
_phantom: _,
},
run_even_if_cached,
additional_args: _,
},
} = args.args;
let (additional_job_data, dependencies) = T::args_to_jobs(args, params)?;
let base_job = dependencies.base_job();
let job = ExternalCommandJob {
additional_job_data,
program_path,
output_dir: base_job.output_dir(),
run_even_if_cached: base_job.run_even_if_cached() | run_even_if_cached,
params_cache: OnceLock::new(),
};
job.params(); // fill cache
Ok(JobAndDependencies {
job: JobAndKind { kind, job },
dependencies,
})
}
fn inputs(self, job: &Self::Job) -> Interned<[JobItemName]> {
job.inputs()
}
fn outputs(self, job: &Self::Job) -> Interned<[JobItemName]> {
job.outputs()
}
fn name(self) -> Interned<str> {
T::job_kind_name()
}
fn external_command_params(self, job: &Self::Job) -> Option<CommandParams> {
Some(job.command_params())
}
fn run(
self,
job: &Self::Job,
inputs: &[JobItem],
params: &JobParams,
acquired_job: &mut AcquiredJob,
) -> eyre::Result<Vec<JobItem>> {
assert!(
inputs.iter().map(JobItem::name).eq(job.inputs()),
"{}\ninputs:\n{inputs:?}\njob.inputs():\n{:?}",
std::any::type_name::<Self>(),
job.inputs(),
);
let CommandParams {
command_line,
current_dir,
} = job.command_params();
ExternalJobCaching::new(
&job.output_dir,
&params.application_name(),
&T::job_kind_name(),
job.run_even_if_cached,
)?
.run(
command_line,
inputs
.iter()
.flat_map(|item| match item {
JobItem::Path { path } => std::slice::from_ref(path),
JobItem::DynamicPaths {
paths,
source_job_name: _,
} => paths,
})
.copied(),
job.output_paths(),
|mut cmd| {
if let Some(current_dir) = current_dir {
cmd.current_dir(current_dir);
}
let status = acquired_job.run_command(cmd, |cmd| cmd.status())?;
if !status.success() {
bail!("running {command_line:?} failed: {status}")
}
Ok(())
},
)?;
Ok(job
.output_paths()
.iter()
.map(|&path| JobItem::Path { path })
.collect())
}
fn subcommand_hidden(self) -> bool {
T::subcommand_hidden()
}
fn external_program(self) -> Option<Interned<ExternalProgram>> {
Some(ExternalProgram::new::<T::ExternalProgram>().intern_sized())
}
}