feat(appd,pack): capability dispatch -- map wapp.toml capabilities to --preopen args

Add capabilities field to weft-pack PackageMeta (optional Vec<String>).
Print cap: lines in weft-pack info output when capabilities are declared.

In weft-appd:
- Make app_store_roots pub(crate) so runtime.rs can use it.
- Add resolve_preopens(app_id) in runtime.rs: reads wapp.toml from the
  package store, extracts capabilities, maps each to a (host, guest) pair:
    fs:rw:app-data / fs:read:app-data -> ~/.local/share/weft/apps/<id>/data :: /data
    fs:rw:xdg-documents / fs:read:xdg-documents -> ~/Documents :: /xdg/documents
  Unknown capabilities are logged at debug level and skipped.
- supervise() calls resolve_preopens() and appends --preopen HOST::GUEST
  flags before spawning the runtime binary.
This commit is contained in:
Marco Allegretti 2026-03-11 15:20:51 +01:00
parent 84eb39db96
commit c5a47a05b4
3 changed files with 77 additions and 1 deletions

View file

@ -286,7 +286,7 @@ pub(crate) async fn dispatch(req: Request, registry: &Registry) -> Response {
}
}
fn app_store_roots() -> Vec<std::path::PathBuf> {
pub(crate) fn app_store_roots() -> Vec<std::path::PathBuf> {
if let Ok(explicit) = std::env::var("WEFT_APP_STORE") {
return vec![std::path::PathBuf::from(explicit)];
}

View file

@ -1,3 +1,4 @@
use std::path::PathBuf;
use std::time::Duration;
use tokio::io::{AsyncBufReadExt, BufReader};
@ -9,6 +10,71 @@ use crate::ipc::{AppStateKind, Response};
const READY_TIMEOUT: Duration = Duration::from_secs(30);
fn resolve_preopens(app_id: &str) -> Vec<(String, String)> {
#[derive(serde::Deserialize)]
struct Pkg {
capabilities: Option<Vec<String>>,
}
#[derive(serde::Deserialize)]
struct M {
package: Pkg,
}
let pkg_dir = crate::app_store_roots().into_iter().find_map(|root| {
let dir = root.join(app_id);
if dir.join("wapp.toml").exists() {
Some(dir)
} else {
None
}
});
let caps = match pkg_dir {
None => return Vec::new(),
Some(dir) => {
let Ok(text) = std::fs::read_to_string(dir.join("wapp.toml")) else {
return Vec::new();
};
match toml::from_str::<M>(&text) {
Ok(m) => m.package.capabilities.unwrap_or_default(),
Err(_) => return Vec::new(),
}
}
};
let home = match std::env::var("HOME") {
Ok(h) => PathBuf::from(h),
Err(_) => return Vec::new(),
};
let mut preopens = Vec::new();
for cap in &caps {
match cap.as_str() {
"fs:rw:app-data" | "fs:read:app-data" => {
let data_dir = home
.join(".local/share/weft/apps")
.join(app_id)
.join("data");
let _ = std::fs::create_dir_all(&data_dir);
preopens.push((data_dir.to_string_lossy().into_owned(), "/data".to_string()));
}
"fs:rw:xdg-documents" | "fs:read:xdg-documents" => {
let docs = home.join("Documents");
if docs.exists() {
preopens.push((
docs.to_string_lossy().into_owned(),
"/xdg/documents".to_string(),
));
}
}
other => {
tracing::debug!(capability = other, "not mapped to preopen; skipped");
}
}
}
preopens
}
pub(crate) async fn supervise(
session_id: u64,
app_id: &str,
@ -37,6 +103,10 @@ pub(crate) async fn supervise(
cmd.arg("--ipc-socket").arg(sock);
}
for (host, guest) in resolve_preopens(app_id) {
cmd.arg("--preopen").arg(format!("{host}::{guest}"));
}
let mut child = match cmd.spawn() {
Ok(c) => c,
Err(e) => {

View file

@ -17,6 +17,7 @@ struct PackageMeta {
version: String,
description: Option<String>,
author: Option<String>,
capabilities: Option<Vec<String>>,
}
#[derive(Debug, Deserialize)]
@ -141,6 +142,11 @@ fn print_info(m: &Manifest) {
}
println!("module: {}", m.runtime.module);
println!("ui: {}", m.ui.entry);
if let Some(ref caps) = m.package.capabilities {
for cap in caps {
println!("cap: {cap}");
}
}
}
fn is_valid_app_id(id: &str) -> bool {