use crate::config::{self, GlobalConfig};
use crate::error::*;
use crate::package_definition::{CommandDefinition, InvokeTemplate};
use crate::receipt::Receipt;
use std::collections::{BTreeMap, BTreeSet};
use std::path::Path;
use std::process::Command;
#[allow(clippy::too_many_arguments)]
pub fn build_env_vars(
home: &Path,
package_name: &str,
command_path: &str,
version: &str,
flags: &BTreeMap<String, String>,
command: &CommandDefinition,
receipt: &Receipt,
global_config: &GlobalConfig,
definition_config: &BTreeMap<String, serde_yml::Value>,
project_root: Option<&Path>,
project_data: bool,
project_config: Option<&GlobalConfig>,
cwd: Option<&Path>,
) -> (BTreeMap<String, String>, BTreeMap<String, String>) {
let mut env_vars = BTreeMap::new();
let mut placeholders = BTreeMap::new();
env_vars.insert("ZR_PACKAGE".to_string(), package_name.to_string());
env_vars.insert("ZR_COMMAND".to_string(), command_path.to_string());
env_vars.insert("ZR_VERSION".to_string(), version.to_string());
env_vars.insert(
"ZR_HOME".to_string(),
home.to_string_lossy().into_owned(),
);
if let Some(root) = project_root {
env_vars.insert(
"ZR_PROJECT".to_string(),
root.to_string_lossy().into_owned(),
);
}
if project_data {
if let Some(root) = project_root.or(cwd) {
let data_dir = crate::paths::project_data_dir(root, package_name);
env_vars.insert(
"ZR_DATA".to_string(),
data_dir.to_string_lossy().into_owned(),
);
}
}
let all_keys: BTreeSet<&str> = command
.args
.keys()
.chain(definition_config.keys())
.map(|s| s.as_str())
.collect();
for key in &all_keys {
let value = config::resolve(
key,
package_name,
flags,
receipt,
global_config,
definition_config,
project_config,
);
if let Some(v) = value {
let env_name = config::config_env_var_name(package_name, key);
env_vars.insert(env_name, v.clone());
placeholders.insert(key.to_string(), v.clone());
placeholders.insert(format!("config.{}", key), v);
}
}
(env_vars, placeholders)
}
pub fn exec_invoke(
invoke: &InvokeTemplate,
env_vars: &BTreeMap<String, String>,
placeholders: &BTreeMap<String, String>,
) -> Result<i32> {
let tokens = match invoke {
InvokeTemplate::String(s) => {
shlex::split(s).ok_or_else(|| anyhow!("failed to tokenize invoke template: {}", s))?
}
InvokeTemplate::Array(arr) => arr.clone(),
};
if tokens.is_empty() {
bail!("invoke template is empty");
}
let mut argv: Vec<String> = Vec::new();
for token in &tokens {
if let Some(substituted) = substitute_token(token, placeholders) {
argv.push(substituted);
}
}
if argv.is_empty() {
bail!("invoke template produced empty command after substitution");
}
let program = &argv[0];
let args = &argv[1..];
let status = Command::new(program)
.args(args)
.envs(env_vars)
.stdin(std::process::Stdio::inherit())
.stdout(std::process::Stdio::inherit())
.stderr(std::process::Stdio::inherit())
.status()
.with_context(|| format!("failed to execute: {}", program))?;
Ok(status.code().unwrap_or(1))
}
pub fn substitute_token(
token: &str,
placeholders: &BTreeMap<String, String>,
) -> Option<String> {
let mut result = String::with_capacity(token.len());
let mut has_placeholder = false;
let mut all_missing = true;
let mut pos = 0;
while pos < token.len() {
if let Some(open) = token[pos..].find('{') {
let abs_open = pos + open;
if open > 0 {
result.push_str(&token[pos..abs_open]);
all_missing = false;
}
if let Some(close) = token[abs_open + 1..].find('}') {
let key = &token[abs_open + 1..abs_open + 1 + close];
has_placeholder = true;
if let Some(value) = placeholders.get(key) {
all_missing = false;
result.push_str(value);
}
pos = abs_open + 1 + close + 1;
} else {
result.push('{');
pos = abs_open + 1;
}
} else {
if !token[pos..].is_empty() {
result.push_str(&token[pos..]);
all_missing = false;
}
break;
}
}
if has_placeholder && all_missing {
return None;
}
Some(result)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_substitute_token_basic() {
let mut placeholders = BTreeMap::new();
placeholders.insert("file".to_string(), "/path/to/file".to_string());
let result = substitute_token("{file}", &placeholders);
assert_eq!(result, Some("/path/to/file".to_string()));
}
#[test]
fn test_substitute_token_missing_optional() {
let placeholders = BTreeMap::new();
let result = substitute_token("{optional-arg}", &placeholders);
assert_eq!(result, None); }
#[test]
fn test_substitute_token_mixed() {
let mut placeholders = BTreeMap::new();
placeholders.insert("format".to_string(), "mp3".to_string());
let result = substitute_token("-f {format}", &placeholders);
assert_eq!(result, Some("-f mp3".to_string()));
}
#[test]
fn test_substitute_token_config_prefix() {
let mut placeholders = BTreeMap::new();
placeholders.insert("model".to_string(), "base".to_string());
placeholders.insert("config.model".to_string(), "base".to_string());
let result = substitute_token("{config.model}", &placeholders);
assert_eq!(result, Some("base".to_string()));
}
fn make_receipt() -> crate::receipt::Receipt {
crate::receipt::Receipt {
schema: 1,
current: "1.0.0".to_string(),
active: true,
mode: None,
transport: None,
config: BTreeMap::new(),
versions: BTreeMap::new(),
}
}
#[test]
fn test_zr_data_set_without_project_root() {
let home = Path::new("/home/user/.zr");
let cwd = Path::new("/tmp/newproject");
let receipt = make_receipt();
let global_config = config::GlobalConfig::default();
let command = crate::package_definition::CommandDefinition::default();
let def_config = BTreeMap::new();
let (env_vars, _) = build_env_vars(
home,
"wf",
"init",
"1.0.0",
&BTreeMap::new(),
&command,
&receipt,
&global_config,
&def_config,
None, true, None,
Some(cwd), );
let expected = format!("{}", cwd.join(".zr").join("wf").display());
assert_eq!(env_vars.get("ZR_DATA").unwrap(), &expected);
assert!(!env_vars.contains_key("ZR_PROJECT"));
}
}