use std::path::{Path, PathBuf};
use serde::Deserialize;
#[derive(Debug, Deserialize)]
pub struct CrepusManifest {
#[serde(default)]
pub ios: Option<IosTomlSection>,
#[serde(default)]
pub targets: Vec<ManifestTarget>,
#[serde(default)]
pub web: Option<WebShorthand>,
}
#[derive(Debug, Deserialize)]
pub struct IosTomlSection {
pub scheme: String,
#[serde(default = "default_xcodegen_spec")]
pub xcodegen_spec: String,
#[serde(default = "default_ios_destination")]
pub destination: String,
}
#[derive(Debug, Deserialize)]
pub struct ManifestTarget {
#[serde(rename = "type")]
pub target_type: String,
#[serde(default)]
pub id: Option<String>,
pub site: String,
#[serde(default)]
pub out: Option<String>,
#[serde(default)]
pub entry: Option<String>,
}
#[derive(Debug, Deserialize)]
pub struct WebShorthand {
#[serde(default = "default_dot")]
pub site: String,
#[serde(default)]
pub out: Option<String>,
#[serde(default)]
pub entry: Option<String>,
}
fn default_dot() -> String {
".".into()
}
pub(crate) fn default_xcodegen_spec() -> String {
"project.yml".into()
}
pub(crate) fn default_ios_destination() -> String {
"platform=iOS Simulator,name=iPhone 16,OS=latest".into()
}
#[derive(Debug, Clone)]
pub struct ResolvedWebTarget {
pub id: String,
pub site_dir: PathBuf,
pub out_dir: PathBuf,
pub entry: String,
}
type WebTargetRow = (Option<String>, String, Option<String>, Option<String>);
impl CrepusManifest {
pub fn parse(src: &str) -> Result<Self, String> {
toml::from_str(src).map_err(|e| e.to_string())
}
pub fn web_targets(&self, manifest_dir: &Path) -> Result<Vec<ResolvedWebTarget>, String> {
let mut raw: Vec<WebTargetRow> = Vec::new();
for t in &self.targets {
if !t.target_type.eq_ignore_ascii_case("web") {
continue;
}
raw.push((t.id.clone(), t.site.clone(), t.out.clone(), t.entry.clone()));
}
if raw.is_empty() {
if let Some(w) = &self.web {
raw.push((None, w.site.clone(), w.out.clone(), w.entry.clone()));
}
}
if raw.is_empty() {
return Ok(Vec::new());
}
let mut out = Vec::with_capacity(raw.len());
for (id, site, od, ent) in raw {
let site_dir = manifest_dir.join(&site);
let site_dir = std::fs::canonicalize(&site_dir).unwrap_or(site_dir);
let default_out = site_dir.join("dist");
let out_dir = od
.map(|r| {
let p = manifest_dir.join(r);
std::fs::canonicalize(&p).unwrap_or(p)
})
.unwrap_or(default_out);
let entry = ent
.filter(|s| !s.is_empty())
.unwrap_or_else(|| "index.crepus".into());
let id = id.unwrap_or_else(|| {
site_dir
.file_name()
.map(|s| s.to_string_lossy().into_owned())
.filter(|s| !s.is_empty())
.unwrap_or_else(|| "default".into())
});
out.push(ResolvedWebTarget {
id,
site_dir,
out_dir,
entry,
});
}
Ok(out)
}
}
pub fn find_manifest_upward(start: &Path) -> Option<PathBuf> {
let mut dir = if start.is_file() {
start.parent()?.to_path_buf()
} else {
start.to_path_buf()
};
loop {
let p = dir.join("crepus.toml");
if p.is_file() {
return Some(p);
}
if !dir.pop() {
break;
}
}
None
}
pub fn try_load_ios(manifest_path: &Path) -> Option<IosTomlSection> {
let raw = std::fs::read_to_string(manifest_path).ok()?;
let m: CrepusManifest = CrepusManifest::parse(&raw).ok()?;
m.ios
}
pub fn load_web_targets(manifest: Option<PathBuf>) -> Option<Vec<ResolvedWebTarget>> {
let cwd = std::env::current_dir().ok()?;
let mpath = manifest.or_else(|| find_manifest_upward(&cwd))?;
let md = mpath.parent()?.to_path_buf();
let raw = std::fs::read_to_string(&mpath).ok()?;
let man = CrepusManifest::parse(&raw).ok()?;
let targets = man.web_targets(&md).ok()?;
if targets.is_empty() {
None
} else {
Some(targets)
}
}
pub fn resolve_pick(
targets: &[ResolvedWebTarget],
id: Option<&str>,
) -> Result<ResolvedWebTarget, String> {
let ids: Vec<&str> = targets.iter().map(|t| t.id.as_str()).collect();
match (id, targets.len()) {
(Some(id), _) => targets
.iter()
.find(|t| t.id == id)
.cloned()
.ok_or_else(|| format!("no web target with id {id:?} (available: {ids:?})")),
(None, 1) => Ok(targets[0].clone()),
(None, n) => Err(format!(
"crepus.toml defines {n} web targets {ids:?}; pass --target ID"
)),
}
}