pub struct PackOptions {
pub profile: String,
pub formats: Option<Vec<String>>,
pub no_sign: bool,
pub no_notarize: bool,
pub no_wait: bool,
}
impl Default for PackOptions {
fn default() -> Self {
PackOptions {
profile: "release".into(),
formats: None,
no_sign: false,
no_notarize: false,
no_wait: false,
}
}
}
pub enum InterpolateError {
MissingEnv(String),
Malformed(String),
}
impl InterpolateError {
pub fn message(&self) -> String {
match self {
InterpolateError::MissingEnv(name) => format!(
"environment variable {name} is not set (referenced as ${{{name}}} in Day.toml signing config)"
),
InterpolateError::Malformed(m) => m.clone(),
}
}
}
pub fn interpolate_full(raw: &str) -> Result<String, InterpolateError> {
let mut out = String::with_capacity(raw.len());
let mut rest = raw;
while let Some(start) = rest.find("${") {
out.push_str(&rest[..start]);
let after = &rest[start + 2..];
let Some(end) = after.find('}') else {
return Err(InterpolateError::Malformed(format!(
"unterminated ${{…}} reference in {raw:?}"
)));
};
let name = &after[..end];
if name.is_empty() || !name.chars().all(|c| c.is_ascii_alphanumeric() || c == '_') {
return Err(InterpolateError::Malformed(format!(
"invalid environment variable name ${{{name}}}"
)));
}
match std::env::var(name) {
Ok(v) if !v.is_empty() => out.push_str(&v),
_ => return Err(InterpolateError::MissingEnv(name.to_string())),
}
rest = &after[end + 1..];
}
out.push_str(rest);
Ok(out)
}
pub fn interpolate(raw: &str) -> Result<String, String> {
interpolate_full(raw).map_err(|e| e.message())
}
pub fn interpolate_opt(raw: Option<&String>) -> Result<Option<String>, String> {
raw.map(|s| interpolate(s)).transpose()
}
pub fn resolve_degradable(raw: &str, what: &str) -> Result<Option<String>, String> {
match interpolate_full(raw) {
Ok(v) => Ok(Some(v)),
Err(InterpolateError::MissingEnv(name)) => {
crate::ops::status(
"Warning",
&format!("{what}: ${{{name}}} is not set — degrading to the dev signing tier"),
);
Ok(None)
}
Err(e) => Err(e.message()),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn interpolate_passthrough_and_vars() {
assert_eq!(interpolate("plain").unwrap(), "plain");
unsafe { std::env::set_var("DAY_TEST_INTERP", "value") };
assert_eq!(interpolate("a-${DAY_TEST_INTERP}-b").unwrap(), "a-value-b");
assert_eq!(
interpolate("${DAY_TEST_INTERP}${DAY_TEST_INTERP}").unwrap(),
"valuevalue"
);
}
#[test]
fn interpolate_missing_names_variable_only() {
let err = interpolate("literal-${DAY_TEST_UNSET_VAR}-tail").unwrap_err();
assert!(err.contains("DAY_TEST_UNSET_VAR"));
assert!(!err.contains("literal-")); }
#[test]
fn interpolate_rejects_malformed() {
assert!(interpolate("${unclosed").is_err());
assert!(interpolate("${bad name}").is_err());
assert!(interpolate("${}").is_err());
}
#[test]
fn resolve_degradable_missing_and_empty_env() {
assert_eq!(
resolve_degradable("${DAY_TEST_UNSET_VAR2}", "test").unwrap(),
None
);
unsafe { std::env::set_var("DAY_TEST_EMPTY", "") };
assert_eq!(
resolve_degradable("${DAY_TEST_EMPTY}", "test").unwrap(),
None
);
assert!(resolve_degradable("${broken", "test").is_err());
}
}