diff --git a/Cargo.lock b/Cargo.lock index df218c4..0271e70 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1310,7 +1310,7 @@ version = "3.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" dependencies = [ - "toml_edit", + "toml_edit 0.25.4+spec-1.1.0", ] [[package]] @@ -1595,6 +1595,15 @@ dependencies = [ "zmij", ] +[[package]] +name = "serde_spanned" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" +dependencies = [ + "serde", +] + [[package]] name = "serde_spanned" version = "1.0.4" @@ -1786,7 +1795,7 @@ dependencies = [ "cfg-expr", "heck", "pkg-config", - "toml", + "toml 0.9.12+spec-1.1.0", "version-compare", ] @@ -1897,6 +1906,18 @@ dependencies = [ "tungstenite", ] +[[package]] +name = "toml" +version = "0.8.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" +dependencies = [ + "serde", + "serde_spanned 0.6.9", + "toml_datetime 0.6.11", + "toml_edit 0.22.27", +] + [[package]] name = "toml" version = "0.9.12+spec-1.1.0" @@ -1905,13 +1926,22 @@ checksum = "cf92845e79fc2e2def6a5d828f0801e29a2f8acc037becc5ab08595c7d5e9863" dependencies = [ "indexmap", "serde_core", - "serde_spanned", + "serde_spanned 1.0.4", "toml_datetime 0.7.5+spec-1.1.0", "toml_parser", "toml_writer", "winnow", ] +[[package]] +name = "toml_datetime" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" +dependencies = [ + "serde", +] + [[package]] name = "toml_datetime" version = "0.7.5+spec-1.1.0" @@ -1930,6 +1960,20 @@ dependencies = [ "serde_core", ] +[[package]] +name = "toml_edit" +version = "0.22.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" +dependencies = [ + "indexmap", + "serde", + "serde_spanned 0.6.9", + "toml_datetime 0.6.11", + "toml_write", + "winnow", +] + [[package]] name = "toml_edit" version = "0.25.4+spec-1.1.0" @@ -1951,6 +1995,12 @@ dependencies = [ "winnow", ] +[[package]] +name = "toml_write" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" + [[package]] name = "toml_writer" version = "1.0.6+spec-1.1.0" @@ -2429,6 +2479,15 @@ dependencies = [ "wayland-server", ] +[[package]] +name = "weft-pack" +version = "0.1.0" +dependencies = [ + "anyhow", + "serde", + "toml 0.8.23", +] + [[package]] name = "weft-runtime" version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index b0755ad..3b22b5a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,6 +3,7 @@ members = [ "crates/weft-appd", "crates/weft-build-meta", "crates/weft-compositor", + "crates/weft-pack", "crates/weft-runtime", "crates/weft-servo-shell", ] diff --git a/crates/weft-pack/Cargo.toml b/crates/weft-pack/Cargo.toml new file mode 100644 index 0000000..a8e1c76 --- /dev/null +++ b/crates/weft-pack/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "weft-pack" +version.workspace = true +edition.workspace = true +rust-version.workspace = true + +[[bin]] +name = "weft-pack" +path = "src/main.rs" + +[dependencies] +anyhow = "1.0" +serde = { version = "1", features = ["derive"] } +toml = "0.8" diff --git a/crates/weft-pack/src/main.rs b/crates/weft-pack/src/main.rs new file mode 100644 index 0000000..3e3ef4c --- /dev/null +++ b/crates/weft-pack/src/main.rs @@ -0,0 +1,252 @@ +use std::path::{Path, PathBuf}; + +use anyhow::Context; +use serde::Deserialize; + +#[derive(Debug, Deserialize)] +struct Manifest { + package: PackageMeta, + runtime: RuntimeMeta, + ui: UiMeta, +} + +#[derive(Debug, Deserialize)] +struct PackageMeta { + id: String, + name: String, + version: String, + description: Option, + author: Option, +} + +#[derive(Debug, Deserialize)] +struct RuntimeMeta { + module: String, +} + +#[derive(Debug, Deserialize)] +struct UiMeta { + entry: String, +} + +fn main() -> anyhow::Result<()> { + let args: Vec = std::env::args().collect(); + + match args.get(1).map(String::as_str) { + Some("check") => { + let dir = args.get(2).context("usage: weft-pack check ")?; + let result = check_package(Path::new(dir))?; + println!("{result}"); + } + Some("info") => { + let dir = args.get(2).context("usage: weft-pack info ")?; + let manifest = load_manifest(Path::new(dir))?; + print_info(&manifest); + } + _ => { + eprintln!("usage:"); + eprintln!(" weft-pack check validate a package directory"); + eprintln!(" weft-pack info print package metadata"); + std::process::exit(1); + } + } + + Ok(()) +} + +fn check_package(dir: &Path) -> anyhow::Result { + let mut errors: Vec = Vec::new(); + + let manifest = match load_manifest(dir) { + Ok(m) => Some(m), + Err(e) => { + errors.push(format!("wapp.toml: {e}")); + None + } + }; + + if let Some(ref m) = manifest { + if !is_valid_app_id(&m.package.id) { + errors.push(format!( + "package.id '{}' does not match required pattern", + m.package.id + )); + } + if m.package.name.is_empty() { + errors.push("package.name is empty".into()); + } + if m.package.name.len() > 64 { + errors.push(format!( + "package.name exceeds 64 characters ({})", + m.package.name.len() + )); + } + + let wasm_path = dir.join(&m.runtime.module); + if !wasm_path.exists() { + errors.push(format!( + "runtime.module '{}' not found", + wasm_path.display() + )); + } + + let ui_path = dir.join(&m.ui.entry); + if !ui_path.exists() { + errors.push(format!("ui.entry '{}' not found", ui_path.display())); + } + } + + if errors.is_empty() { + Ok("OK".into()) + } else { + Err(anyhow::anyhow!("{}", errors.join("\n"))) + } +} + +fn load_manifest(dir: &Path) -> anyhow::Result { + let manifest_path = dir.join("wapp.toml"); + let text = std::fs::read_to_string(&manifest_path) + .with_context(|| format!("read {}", manifest_path.display()))?; + toml::from_str(&text).with_context(|| format!("parse {}", manifest_path.display())) +} + +fn print_info(m: &Manifest) { + println!("id: {}", m.package.id); + println!("name: {}", m.package.name); + println!("version: {}", m.package.version); + if let Some(ref d) = m.package.description { + println!("desc: {d}"); + } + if let Some(ref a) = m.package.author { + println!("author: {a}"); + } + println!("module: {}", m.runtime.module); + println!("ui: {}", m.ui.entry); +} + +fn is_valid_app_id(id: &str) -> bool { + let parts: Vec<&str> = id.split('.').collect(); + if parts.len() < 3 { + return false; + } + parts.iter().all(|p| { + !p.is_empty() + && p.chars() + .next() + .map(|c| c.is_ascii_lowercase()) + .unwrap_or(false) + && p.chars() + .all(|c| c.is_ascii_lowercase() || c.is_ascii_digit()) + }) +} + +fn _resolve_store_roots() -> Vec { + if let Ok(explicit) = std::env::var("WEFT_APP_STORE") { + return vec![PathBuf::from(explicit)]; + } + let mut roots = Vec::new(); + if let Ok(home) = std::env::var("HOME") { + roots.push( + PathBuf::from(home) + .join(".local") + .join("share") + .join("weft") + .join("apps"), + ); + } + roots.push(PathBuf::from("/usr/share/weft/apps")); + roots +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn app_id_valid() { + assert!(is_valid_app_id("com.example.notes")); + assert!(is_valid_app_id("org.weft.calculator")); + assert!(is_valid_app_id("io.github.username.app")); + } + + #[test] + fn app_id_invalid() { + assert!(!is_valid_app_id("com.example")); + assert!(!is_valid_app_id("Com.example.notes")); + assert!(!is_valid_app_id("com.example.notes-app")); + assert!(!is_valid_app_id("com..example.notes")); + assert!(!is_valid_app_id("")); + assert!(!is_valid_app_id("com.Example.notes")); + } + + #[test] + fn check_package_missing_manifest() { + let tmp = std::env::temp_dir().join("weft_pack_test_empty"); + let _ = std::fs::create_dir_all(&tmp); + let result = check_package(&tmp); + assert!(result.is_err()); + let _ = std::fs::remove_dir_all(&tmp); + } + + #[test] + fn check_package_valid() { + use std::fs; + let tmp = std::env::temp_dir().join("weft_pack_test_valid"); + let ui_dir = tmp.join("ui"); + let _ = fs::create_dir_all(&ui_dir); + fs::write(tmp.join("app.wasm"), b"\0asm\x01\0\0\0").unwrap(); + fs::write(ui_dir.join("index.html"), b"").unwrap(); + fs::write( + tmp.join("wapp.toml"), + r#" +[package] +id = "com.example.test" +name = "Test App" +version = "1.0.0" + +[runtime] +module = "app.wasm" + +[ui] +entry = "ui/index.html" +"#, + ) + .unwrap(); + + let result = check_package(&tmp); + assert!(result.is_ok(), "{result:?}"); + assert_eq!(result.unwrap(), "OK"); + + let _ = fs::remove_dir_all(&tmp); + } + + #[test] + fn check_package_invalid_app_id() { + use std::fs; + let tmp = std::env::temp_dir().join("weft_pack_test_invalid_id"); + let ui_dir = tmp.join("ui"); + let _ = fs::create_dir_all(&ui_dir); + fs::write(tmp.join("app.wasm"), b"\0asm").unwrap(); + fs::write(ui_dir.join("index.html"), b"").unwrap(); + fs::write( + tmp.join("wapp.toml"), + r#" +[package] +id = "bad-id" +name = "Bad" +version = "0.1.0" + +[runtime] +module = "app.wasm" + +[ui] +entry = "ui/index.html" +"#, + ) + .unwrap(); + + let result = check_package(&tmp); + assert!(result.is_err()); + let _ = fs::remove_dir_all(&tmp); + } +} diff --git a/scripts/wsl-test.sh b/scripts/wsl-test.sh index 0935536..77201e6 100644 --- a/scripts/wsl-test.sh +++ b/scripts/wsl-test.sh @@ -34,5 +34,9 @@ echo "" echo "==> cargo test -p weft-runtime" cargo test -p weft-runtime 2>&1 +echo "" +echo "==> cargo test -p weft-pack" +cargo test -p weft-pack 2>&1 + echo "" echo "ALL DONE"