use crate::prelude::*;
pub const FILE: &str = ".wsb/stamp.json";
#[derive(Debug, Clone, Default, PartialEq, Eq, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct StampConfig {
pub declared: Option<PathBuf>,
pub build_root: Option<PathBuf>,
pub local: Option<LocalRendering>,
}
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct LocalRendering {
pub variable: String,
#[serde(default)]
pub prepare: Option<Hook>,
#[serde(default)]
pub release: Option<Vec<String>>,
}
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
#[serde(untagged)]
pub enum Hook {
Command(Vec<String>),
Declared(DeclaredHook),
}
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct DeclaredHook {
pub run: Vec<String>,
#[serde(default)]
pub inputs: Option<HookInputs>,
#[serde(default)]
pub produces: Vec<PathBuf>,
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct HookInputs {
#[serde(default)]
pub repositories: Vec<PathBuf>,
#[serde(default)]
pub files: Vec<PathBuf>,
#[serde(default)]
pub programs: Vec<String>,
}
impl Hook {
pub fn argv(&self) -> &[String] {
match self {
Self::Command(argv) => argv,
Self::Declared(declared) => &declared.run,
}
}
pub fn inputs(&self) -> Option<&HookInputs> {
match self {
Self::Command(_) => None,
Self::Declared(declared) => declared.inputs.as_ref(),
}
}
pub fn produces(&self) -> &[PathBuf] {
match self {
Self::Command(_) => &[],
Self::Declared(declared) => &declared.produces,
}
}
}
impl StampConfig {
pub fn load(scan_root: &Path) -> Result<Option<Self>> {
let path = scan_root.join(FILE);
let text = match std::fs::read_to_string(&path) {
Ok(text) => text,
Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(None),
Err(error) => return Err(error).with_context(|| format!("cannot read {}", path.display())),
};
serde_json::from_str(&text)
.map(Some)
.with_context(|| format!("{} is not a valid stamp configuration", path.display()))
}
pub fn declared_file(scan_root: &Path) -> Result<PathBuf> {
Self::named(scan_root, "declared", |config| config.declared.as_ref())
}
pub fn build_root(scan_root: &Path) -> Result<PathBuf> {
let root = Self::named(scan_root, "build_root", |config| config.build_root.as_ref())?;
if root.starts_with(scan_root.join(wsb_prj::settings::DIR)) {
bail!(
"{} names {} as the build root, inside .wsb/, which is committed whole; \
name a folder outside it",
scan_root.join(FILE).display(),
root.display()
);
}
Ok(root)
}
pub fn local(scan_root: &Path) -> Result<LocalRendering> {
let path = scan_root.join(FILE);
match Self::load(scan_root)? {
None => bail!(
"a local render needs the variable that selects it, declared as \"local\" in {}, \
and there is no such file",
path.display()
),
Some(config) => config.local.ok_or_else(|| {
anyhow!(
"a local render needs the variable that selects it, and {} declares no \"local\"",
path.display()
)
}),
}
}
fn named(
scan_root: &Path,
key: &str,
pick: impl Fn(&Self) -> Option<&PathBuf>,
) -> Result<PathBuf> {
let path = scan_root.join(FILE);
match Self::load(scan_root)? {
None => bail!(
"a template asks for what {} would name as \"{key}\", and there is no {}",
FILE,
path.display()
),
Some(config) => match pick(&config) {
Some(relative) => Ok(scan_root.join(relative)),
None => bail!(
"a template asks for what {} names as \"{key}\", and it names none",
path.display()
),
},
}
}
}
pub fn hook_program(named: &str, scan_root: &Path, host_dir: Option<&Path>) -> Result<PathBuf> {
let is_path = named.starts_with("~/") || named.contains('/') || (cfg!(windows) && named.contains('\\'));
if is_path {
let path = match named.strip_prefix("~/") {
Some(rest) => std::env::var_os("HOME")
.or_else(|| std::env::var_os("USERPROFILE"))
.filter(|home| !home.is_empty())
.map(|home| PathBuf::from(home).join(rest))
.ok_or_else(|| anyhow!("{named} names the home folder, and HOME is not set"))?,
None => scan_root.join(named),
};
if !crate::platform::is_executable(&path) {
bail!("{named} is declared in {} and {} is no program", scan_root.join(FILE).display(), path.display());
}
return Ok(path);
}
if let Some(host_dir) = host_dir {
match crate::plugin::program(host_dir, named).map_err(|e| anyhow!(e))? {
Some(Some(program)) if crate::platform::is_executable(&program) => return Ok(program),
Some(Some(program)) => bail!(
"{named} is a plugin enabled on this host, and its program {} is not there; \
name it again: wsb plugin add {named} --program <path>",
program.display()
),
Some(None) => bail!(
"{named} is a plugin enabled on this host with no program recorded, so which {named} \
to run is not known; name it: wsb plugin add {named} --program <path>"
),
None => {}
}
}
crate::report::which(named)
.map(PathBuf::from)
.ok_or_else(|| anyhow!("no {named} is on the PATH (declared in {})", scan_root.join(FILE).display()))
}
#[cfg(test)]
#[path = "stamp_config_tests.rs"]
mod tests;