WEFT_OS/crates/weft-appd/src/mount.rs
Marco Allegretti d2cb693c55 feat(appd): MountOrchestrator -- EROFS+dm-verity image mount on app launch
Add crates/weft-appd/src/mount.rs with MountOrchestrator.

On each app launch, supervise() calls MountOrchestrator::mount_if_needed
which looks for <app_id>.app.img, <app_id>.hash, and <app_id>.roothash
in the app store roots. If all three exist and weft-mount-helper is
available (WEFT_MOUNT_HELPER env or /usr/lib/weft/ default paths), it:

  - creates /tmp/weft-mnt-<session_id>/<app_id>/
  - invokes weft-mount-helper mount to set up dm-verity and EROFS mount
  - sets WEFT_APP_STORE=/tmp/weft-mnt-<session_id> in the child env so
    the runtime resolves the package from the mounted read-only image

After the process exits, umount() invokes weft-mount-helper umount and
removes the temporary directory.

Falls back to directory-based install silently if no image is found,
mount-helper is absent, or the mount fails.

Test: find_image_returns_none_when_absent.
2026-03-11 15:47:23 +01:00

116 lines
3.7 KiB
Rust

use std::path::{Path, PathBuf};
fn mount_helper_bin() -> Option<String> {
if let Ok(v) = std::env::var("WEFT_MOUNT_HELPER") {
return Some(v);
}
for candidate in [
"/usr/lib/weft/weft-mount-helper",
"/usr/local/lib/weft/weft-mount-helper",
] {
if Path::new(candidate).exists() {
return Some(candidate.to_string());
}
}
None
}
pub struct MountOrchestrator {
mountpoint: Option<PathBuf>,
}
impl MountOrchestrator {
pub fn mount_if_needed(app_id: &str, session_id: u64) -> (Self, Option<PathBuf>) {
let Some(helper) = mount_helper_bin() else {
return (Self { mountpoint: None }, None);
};
let (img, hash_dev, root_hash) = match find_image(app_id) {
Some(t) => t,
None => return (Self { mountpoint: None }, None),
};
let base = std::env::temp_dir().join(format!("weft-mnt-{session_id}"));
let mountpoint = base.join(app_id);
if let Err(e) = std::fs::create_dir_all(&mountpoint) {
tracing::warn!(session_id, %app_id, error=%e, "cannot create mount dir; skipping image mount");
return (Self { mountpoint: None }, None);
}
let status = std::process::Command::new(&helper)
.args([
"mount",
&img.to_string_lossy(),
&hash_dev.to_string_lossy(),
&root_hash,
&mountpoint.to_string_lossy(),
])
.status();
match status {
Ok(s) if s.success() => {
tracing::info!(session_id, %app_id, path=%mountpoint.display(), "EROFS image mounted");
(
Self {
mountpoint: Some(mountpoint),
},
Some(base),
)
}
Ok(s) => {
tracing::warn!(session_id, %app_id, status=%s, "mount-helper failed; using directory install");
let _ = std::fs::remove_dir_all(&base);
(Self { mountpoint: None }, None)
}
Err(e) => {
tracing::warn!(session_id, %app_id, error=%e, "spawn mount-helper failed; using directory install");
let _ = std::fs::remove_dir_all(&base);
(Self { mountpoint: None }, None)
}
}
}
pub fn umount(&self) {
let Some(ref mp) = self.mountpoint else {
return;
};
let Some(helper) = mount_helper_bin() else {
return;
};
let _ = std::process::Command::new(&helper)
.args(["umount", &mp.to_string_lossy()])
.status();
if let Some(parent) = mp.parent() {
let _ = std::fs::remove_dir_all(parent);
}
}
}
fn find_image(app_id: &str) -> Option<(PathBuf, PathBuf, String)> {
for root in crate::app_store_roots() {
let img = root.join(format!("{app_id}.app.img"));
let hash_dev = root.join(format!("{app_id}.hash"));
let roothash_file = root.join(format!("{app_id}.roothash"));
if img.exists() && hash_dev.exists() && roothash_file.exists() {
let Ok(root_hash) = std::fs::read_to_string(&roothash_file) else {
continue;
};
return Some((img, hash_dev, root_hash.trim().to_string()));
}
}
None
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn find_image_returns_none_when_absent() {
unsafe { std::env::set_var("WEFT_APP_STORE", "/tmp/nonexistent_weft_store_xyz") };
let result = find_image("com.example.missing");
unsafe { std::env::remove_var("WEFT_APP_STORE") };
assert!(result.is_none());
}
}