use std::collections::BTreeMap; use std::path::PathBuf; use serde::{Deserialize, Serialize}; use sha2::{Digest, Sha256}; use crate::{Error, Result}; /// A stable identity for a guest function used as a task action. `id` is /// meaningful only to the driver that registered the function; `key` is a /// content identity (e.g. a hash of the function source) for fingerprinting. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct CallbackRef { pub id: u32, pub key: String, } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub enum Action { None, /// Run through the platform shell (`sh -c` / `cmd /C`). Shell(String), /// Spawn directly, without a shell. Argv(Vec), /// Invoke a guest function; only drivers with a guest runtime can do this. Callback(CallbackRef), } impl Action { pub fn kind(&self) -> &'static str { match self { Action::None => "none", Action::Shell(_) => "shell", Action::Argv(_) => "argv", Action::Callback(_) => "callback", } } /// The text used for fingerprinting. Distinct kinds can never collide: /// only shell commands use the bare string form, for compatibility with /// fingerprints recorded by earlier avril versions. pub(crate) fn fingerprint_text(&self) -> String { match self { Action::None => String::new(), Action::Shell(command) => command.clone(), Action::Argv(argv) => format!("argv\0{}", serde_json::to_string(argv).unwrap()), Action::Callback(callback) => format!("callback\0{}", callback.key), } } pub fn display(&self) -> String { match self { Action::None => String::new(), Action::Shell(command) => command.clone(), Action::Argv(argv) => argv .iter() .map(|a| shell_quote(a)) .collect::>() .join(" "), Action::Callback(_) => "".into(), } } } #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct Task { pub description: Option, pub action: Action, pub dependencies: Vec, pub parallel: bool, pub env_file: Option, /// Declaring `files` opts the task into result caching. pub files: Option>, pub output: Vec, pub env: Vec, } impl Default for Task { fn default() -> Self { Task { description: None, action: Action::None, dependencies: Vec::new(), parallel: false, env_file: None, files: None, output: Vec::new(), env: Vec::new(), } } } #[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] pub struct Graph { pub tasks: BTreeMap, } impl Graph { pub fn get(&self, name: &str) -> Result<&Task> { self.tasks .get(name) .ok_or_else(|| Error::UnknownTask(name.to_owned())) } /// A content digest of the whole graph, for drivers that need to know /// whether a re-evaluated graph differs from the previous one. pub fn digest(&self) -> String { let bytes = serde_json::to_vec(self).unwrap(); format!("{:x}", Sha256::digest(&bytes)) } } #[cfg(not(windows))] pub fn shell_quote(arg: &str) -> String { if !arg.is_empty() && arg.bytes().all(|ch| { ch.is_ascii_alphanumeric() || matches!( ch, b'-' | b'_' | b'.' | b'/' | b':' | b'@' | b'%' | b'+' | b'=' | b',' ) }) { arg.to_owned() } else { format!("'{}'", arg.replace('\'', "'\\''")) } } #[cfg(windows)] pub fn shell_quote(arg: &str) -> String { if !arg.is_empty() && arg.bytes().all(|ch| { ch.is_ascii_alphanumeric() || matches!(ch, b'-' | b'_' | b'.' | b'/' | b':' | b'@') }) { arg.to_owned() } else { format!("\"{}\"", arg.replace('"', "\\\"")) } }