use std::collections::HashMap; use std::io; use std::path::{Path, PathBuf}; use std::sync::Arc; use crate::EnvMap; use crate::graph::Task; #[derive(Debug, thiserror::Error)] pub enum Error { #[error("Cannot load env file {}: {source}", .path.display())] Load { path: PathBuf, #[source] source: io::Error, }, } fn parse(source: &str) -> io::Result> { if source.contains('\0') { return Err(io::Error::new( io::ErrorKind::InvalidInput, "Environment files cannot contain NUL", )); } let source = source.replace('\r', ""); let mut rest = source.strip_prefix('\u{feff}').unwrap_or(&source); let mut values = Vec::new(); while !rest.is_empty() { rest = rest.trim_start_matches([' ', '\t', '\n']); let (line, next) = rest.split_once('\n').unwrap_or((rest, "")); let Some((raw_key, _)) = line.split_once('=') else { rest = next; continue; }; if line.starts_with('#') { rest = next; continue; } let mut key = raw_key.trim_matches([' ', '\t']); if let Some(exported) = key.strip_prefix("export ") { key = exported.trim_start_matches([' ', '\t']); } if key.is_empty() { rest = next; continue; } let value = rest[raw_key.len() + 1..].trim_start_matches([' ', '\t']); if let Some(quote @ (b'\'' | b'"' | b'`')) = value.as_bytes().first().copied() { let quote = quote as char; if let Some(end) = value[1..].find(quote).map(|index| index + 1) { let quoted = &value[1..end]; let parsed = if quote == '"' { quoted.replace("\\n", "\n") } else { quoted.to_owned() }; values.push((key.to_owned(), parsed)); rest = value[end + 1..] .split_once('\n') .map_or("", |(_, next)| next); continue; } let (value, next) = value.split_once('\n').unwrap_or((value, "")); values.push((key.to_owned(), value.to_owned())); rest = next; continue; } let (value, next) = value.split_once('\n').unwrap_or((value, "")); let value = value.split_once('#').map_or(value, |(value, _)| value); values.push((key.to_owned(), value.trim_matches([' ', '\t']).to_owned())); rest = next; } Ok(values) } pub fn load_values(paths: &[PathBuf]) -> Result, Error> { let mut values = Vec::new(); for path in paths { let source = std::fs::read_to_string(path) .map_err(|source| Error::Load { path: path.clone(), source })?; values.extend(parse(&source).map_err(|source| Error::Load { path: path.clone(), source })?); } Ok(values) } pub(crate) struct EnvLoader { root: PathBuf, cache: HashMap>>, } impl EnvLoader { pub(crate) fn new(root: PathBuf) -> Self { EnvLoader { root, cache: HashMap::new() } } fn load(&mut self, relative: &Path) -> crate::Result>> { let path = self.root.join(relative); if let Some(values) = self.cache.get(&path) { return Ok(values.clone()); } let values = Arc::new(load_values(std::slice::from_ref(&path))?); self.cache.insert(path, values.clone()); Ok(values) } /// The environment a task runs with: inherited values, overridden by /// its own env file when it declares one. pub(crate) fn effective(&mut self, task: &Task, inherited: &EnvMap) -> crate::Result { let mut env = inherited.clone(); let Some(file) = &task.env_file else { return Ok(env); }; for (key, value) in self.load(file)?.iter() { let key = avril_caps::env_name(key).map_err(|_| crate::Error::EnvName(key.clone()))?; env.insert(key.into_owned(), value.clone()); } Ok(env) } }