mirror of
https://github.com/marcoallegretti/WEFT_OS.git
synced 2026-03-27 01:13:09 +00:00
feat(pack): add weft-pack package validator tool
New crate: weft-pack — command-line tool for validating WEFT application package directories against the app-package-format spec. src/main.rs: - check <dir>: loads wapp.toml, validates app ID format, verifies package.name is non-empty and <=64 chars, confirms runtime.module and ui.entry files exist. Prints 'OK' on success or the list of errors. - info <dir>: prints all manifest fields to stdout. - load_manifest(): reads and parses wapp.toml with toml crate. - is_valid_app_id(): enforces reverse-domain convention (>=3 components, each starting with a lowercase letter, digits allowed, no hyphens or uppercase). Tests (5): - app_id_valid: accepts well-formed reverse-domain IDs. - app_id_invalid: rejects two-component, uppercase, hyphen, empty IDs. - check_package_missing_manifest: error when wapp.toml is absent. - check_package_valid: full happy-path with real temp files. - check_package_invalid_app_id: error on a hyphenated app ID. New deps: toml 0.8, serde 1 (derive). Added weft-pack to workspace Cargo.toml; wsl-test.sh extended.
This commit is contained in:
parent
1e4ced9a39
commit
ffae164747
5 changed files with 333 additions and 3 deletions
65
Cargo.lock
generated
65
Cargo.lock
generated
|
|
@ -1310,7 +1310,7 @@ version = "3.5.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f"
|
checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"toml_edit",
|
"toml_edit 0.25.4+spec-1.1.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -1595,6 +1595,15 @@ dependencies = [
|
||||||
"zmij",
|
"zmij",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde_spanned"
|
||||||
|
version = "0.6.9"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3"
|
||||||
|
dependencies = [
|
||||||
|
"serde",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde_spanned"
|
name = "serde_spanned"
|
||||||
version = "1.0.4"
|
version = "1.0.4"
|
||||||
|
|
@ -1786,7 +1795,7 @@ dependencies = [
|
||||||
"cfg-expr",
|
"cfg-expr",
|
||||||
"heck",
|
"heck",
|
||||||
"pkg-config",
|
"pkg-config",
|
||||||
"toml",
|
"toml 0.9.12+spec-1.1.0",
|
||||||
"version-compare",
|
"version-compare",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
@ -1897,6 +1906,18 @@ dependencies = [
|
||||||
"tungstenite",
|
"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]]
|
[[package]]
|
||||||
name = "toml"
|
name = "toml"
|
||||||
version = "0.9.12+spec-1.1.0"
|
version = "0.9.12+spec-1.1.0"
|
||||||
|
|
@ -1905,13 +1926,22 @@ checksum = "cf92845e79fc2e2def6a5d828f0801e29a2f8acc037becc5ab08595c7d5e9863"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"indexmap",
|
"indexmap",
|
||||||
"serde_core",
|
"serde_core",
|
||||||
"serde_spanned",
|
"serde_spanned 1.0.4",
|
||||||
"toml_datetime 0.7.5+spec-1.1.0",
|
"toml_datetime 0.7.5+spec-1.1.0",
|
||||||
"toml_parser",
|
"toml_parser",
|
||||||
"toml_writer",
|
"toml_writer",
|
||||||
"winnow",
|
"winnow",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "toml_datetime"
|
||||||
|
version = "0.6.11"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c"
|
||||||
|
dependencies = [
|
||||||
|
"serde",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "toml_datetime"
|
name = "toml_datetime"
|
||||||
version = "0.7.5+spec-1.1.0"
|
version = "0.7.5+spec-1.1.0"
|
||||||
|
|
@ -1930,6 +1960,20 @@ dependencies = [
|
||||||
"serde_core",
|
"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]]
|
[[package]]
|
||||||
name = "toml_edit"
|
name = "toml_edit"
|
||||||
version = "0.25.4+spec-1.1.0"
|
version = "0.25.4+spec-1.1.0"
|
||||||
|
|
@ -1951,6 +1995,12 @@ dependencies = [
|
||||||
"winnow",
|
"winnow",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "toml_write"
|
||||||
|
version = "0.1.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "toml_writer"
|
name = "toml_writer"
|
||||||
version = "1.0.6+spec-1.1.0"
|
version = "1.0.6+spec-1.1.0"
|
||||||
|
|
@ -2429,6 +2479,15 @@ dependencies = [
|
||||||
"wayland-server",
|
"wayland-server",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "weft-pack"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"anyhow",
|
||||||
|
"serde",
|
||||||
|
"toml 0.8.23",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "weft-runtime"
|
name = "weft-runtime"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ members = [
|
||||||
"crates/weft-appd",
|
"crates/weft-appd",
|
||||||
"crates/weft-build-meta",
|
"crates/weft-build-meta",
|
||||||
"crates/weft-compositor",
|
"crates/weft-compositor",
|
||||||
|
"crates/weft-pack",
|
||||||
"crates/weft-runtime",
|
"crates/weft-runtime",
|
||||||
"crates/weft-servo-shell",
|
"crates/weft-servo-shell",
|
||||||
]
|
]
|
||||||
|
|
|
||||||
14
crates/weft-pack/Cargo.toml
Normal file
14
crates/weft-pack/Cargo.toml
Normal file
|
|
@ -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"
|
||||||
252
crates/weft-pack/src/main.rs
Normal file
252
crates/weft-pack/src/main.rs
Normal file
|
|
@ -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<String>,
|
||||||
|
author: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
struct RuntimeMeta {
|
||||||
|
module: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
struct UiMeta {
|
||||||
|
entry: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() -> anyhow::Result<()> {
|
||||||
|
let args: Vec<String> = std::env::args().collect();
|
||||||
|
|
||||||
|
match args.get(1).map(String::as_str) {
|
||||||
|
Some("check") => {
|
||||||
|
let dir = args.get(2).context("usage: weft-pack check <dir>")?;
|
||||||
|
let result = check_package(Path::new(dir))?;
|
||||||
|
println!("{result}");
|
||||||
|
}
|
||||||
|
Some("info") => {
|
||||||
|
let dir = args.get(2).context("usage: weft-pack info <dir>")?;
|
||||||
|
let manifest = load_manifest(Path::new(dir))?;
|
||||||
|
print_info(&manifest);
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
eprintln!("usage:");
|
||||||
|
eprintln!(" weft-pack check <dir> validate a package directory");
|
||||||
|
eprintln!(" weft-pack info <dir> print package metadata");
|
||||||
|
std::process::exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn check_package(dir: &Path) -> anyhow::Result<String> {
|
||||||
|
let mut errors: Vec<String> = 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<Manifest> {
|
||||||
|
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<PathBuf> {
|
||||||
|
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"<!DOCTYPE html>").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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -34,5 +34,9 @@ echo ""
|
||||||
echo "==> cargo test -p weft-runtime"
|
echo "==> cargo test -p weft-runtime"
|
||||||
cargo test -p weft-runtime 2>&1
|
cargo test -p weft-runtime 2>&1
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "==> cargo test -p weft-pack"
|
||||||
|
cargo test -p weft-pack 2>&1
|
||||||
|
|
||||||
echo ""
|
echo ""
|
||||||
echo "ALL DONE"
|
echo "ALL DONE"
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue