use std::path::{Path, PathBuf};
use serde::Deserialize;
#[derive(Debug, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Manifest {
pub day: u32,
pub app: App,
#[serde(default)]
#[allow(dead_code)]
pub targets: Vec<String>,
#[serde(default)]
#[allow(dead_code)]
pub window: Window,
#[serde(default)]
pub signing: Option<Signing>,
}
#[derive(Debug, Default, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Signing {
#[serde(default)]
pub macos: Option<MacosSigning>,
#[serde(default)]
pub ios: Option<IosSigning>,
#[serde(default)]
pub android: Option<AndroidSigning>,
#[serde(default)]
pub windows: Option<WindowsSigning>,
#[serde(default)]
pub ohos: Option<OhosSigning>,
}
#[derive(Debug, Deserialize)]
#[serde(deny_unknown_fields, rename_all = "kebab-case")]
pub struct MacosSigning {
#[serde(default)]
pub identity: Option<String>,
#[serde(default)]
pub entitlements: Option<String>,
#[serde(default)]
pub notarize: Option<Notarize>,
}
#[derive(Debug, Deserialize)]
#[serde(deny_unknown_fields, rename_all = "kebab-case")]
pub struct Notarize {
pub key_id: String,
pub issuer: String,
pub key_path: String,
}
#[derive(Debug, Deserialize)]
#[serde(deny_unknown_fields, rename_all = "kebab-case")]
pub struct IosSigning {
pub team: String,
#[serde(default)]
pub export_method: Option<String>,
#[serde(default)]
pub key_id: Option<String>,
#[serde(default)]
pub issuer: Option<String>,
#[serde(default)]
pub key_path: Option<String>,
}
#[derive(Debug, Deserialize)]
#[serde(deny_unknown_fields, rename_all = "kebab-case")]
pub struct AndroidSigning {
pub keystore: String,
pub key_alias: String,
pub store_pass: String,
pub key_pass: String,
}
#[derive(Debug, Deserialize)]
#[serde(deny_unknown_fields, rename_all = "kebab-case")]
pub struct WindowsSigning {
pub provider: String,
#[serde(default)]
pub publisher: Option<String>,
#[serde(default)]
pub thumbprint: Option<String>,
#[serde(default)]
pub endpoint: Option<String>,
#[serde(default)]
pub account: Option<String>,
#[serde(default)]
pub profile: Option<String>,
#[serde(default)]
pub dlib: Option<String>,
#[serde(default)]
pub timestamp_url: Option<String>,
}
#[derive(Debug, Deserialize)]
#[serde(deny_unknown_fields, rename_all = "kebab-case")]
pub struct OhosSigning {
pub keystore: String,
pub key_alias: String,
pub store_pass: String,
pub key_pass: String,
pub cert: String,
pub profile: String,
}
#[derive(Debug, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct App {
pub name: String,
pub id: String,
#[serde(default)]
pub title: Option<String>,
#[serde(default = "default_version")]
pub version: String,
#[serde(default = "default_build")]
pub build: u64,
}
fn default_version() -> String {
"0.1.0".into()
}
fn default_build() -> u64 {
1
}
#[derive(Debug, Deserialize)]
#[serde(deny_unknown_fields)]
#[allow(dead_code)] pub struct Window {
#[serde(default = "default_w")]
pub width: f64,
#[serde(default = "default_h")]
pub height: f64,
}
impl Default for Window {
fn default() -> Self {
Window {
width: default_w(),
height: default_h(),
}
}
}
fn default_w() -> f64 {
480.0
}
fn default_h() -> f64 {
640.0
}
pub struct Project {
pub root: PathBuf,
pub manifest: Manifest,
}
fn strip_verbatim(p: PathBuf) -> PathBuf {
#[cfg(windows)]
if let Some(s) = p.to_str() {
if let Some(rest) = s.strip_prefix(r"\\?\UNC\") {
return PathBuf::from(format!(r"\\{rest}"));
}
if let Some(rest) = s.strip_prefix(r"\\?\") {
return PathBuf::from(rest);
}
}
p
}
pub fn find_project(start: Option<&Path>) -> Result<Project, String> {
let mut dir = match start {
Some(p) => p.to_path_buf(),
None => std::env::current_dir().map_err(|e| e.to_string())?,
};
loop {
let candidate = dir.join("day.yaml");
if candidate.exists() {
let text = std::fs::read_to_string(&candidate).map_err(|e| e.to_string())?;
let manifest: Manifest =
serde_norway::from_str(&text).map_err(|e| format!("day.yaml: {e}"))?;
if manifest.day != 1 {
return Err(format!(
"day.yaml: unsupported schema version {}",
manifest.day
));
}
let root = std::fs::canonicalize(&dir).unwrap_or_else(|_| {
std::env::current_dir()
.map(|cwd| cwd.join(&dir))
.unwrap_or_else(|_| dir.clone())
});
return Ok(Project {
root: strip_verbatim(root),
manifest,
});
}
if !dir.pop() {
return Err("no day.yaml found in this directory or any ancestor".into());
}
}
}
#[cfg(all(test, windows))]
mod tests {
use super::*;
#[test]
fn strip_verbatim_deverbatims_windows_paths() {
assert_eq!(
strip_verbatim(PathBuf::from(r"\\?\D:\a\day\day\apps\showcase")),
PathBuf::from(r"D:\a\day\day\apps\showcase")
);
assert_eq!(
strip_verbatim(PathBuf::from(r"\\?\UNC\server\share\proj")),
PathBuf::from(r"\\server\share\proj")
);
assert_eq!(
strip_verbatim(PathBuf::from(r"D:\a\proj")),
PathBuf::from(r"D:\a\proj")
);
let canon = std::fs::canonicalize(".").unwrap();
assert!(!strip_verbatim(canon).to_string_lossy().starts_with(r"\\?\"));
}
}