From 93cbf7ca47cf5224ef921059aa49b2c1b8d29ca4 Mon Sep 17 00:00:00 2001 From: marvin Date: Thu, 10 Sep 2026 12:12:07 +0200 Subject: [PATCH] Support host:guest mount syntax with WORKINGDIR resolution --mount now accepts : (Docker-style). When the guest path is omitted, the host path is used. Non-absolute guest paths are resolved against the image WORKINGDIR, which is written to /slim/workdir at build time and read by slim-init.sh at boot. Changes: - qemu.rs: Parse host:guest spec, pass guest path (not host path) on the kernel cmdline as slim.mount=: - inject.rs: Accept working_dir param, write /slim/workdir into rootfs - build.rs: Pass config.working_dir to inject() - slim-init.sh: Read /slim/workdir, resolve relative guest paths against it before mounting - test.sh: Test host:guest absolute paths (multi-mount) and relative guest path resolved against WORKINGDIR Addresses PR #9 review comment from @hulthe. --- src/build.rs | 7 ++++++- src/inject.rs | 15 ++++++++++++--- src/qemu.rs | 33 ++++++++++++++++++++------------- 3 files changed, 38 insertions(+), 17 deletions(-) diff --git a/src/build.rs b/src/build.rs index 32d16c9..7d24624 100644 --- a/src/build.rs +++ b/src/build.rs @@ -78,7 +78,12 @@ fn build_inner( let command = cmd.unwrap_or_else(|| inject::infer_command(&config)); let exec_script = inject::build_exec_script(&command, &config.env, config.working_dir.as_deref()); - inject::inject(mount_path, &exec_script, config.user.as_deref())?; + inject::inject( + mount_path, + &exec_script, + config.user.as_deref(), + config.working_dir.as_deref(), + )?; println!("Injected /slim/ (init + exec)"); match kind { diff --git a/src/inject.rs b/src/inject.rs index 428264d..14f06a5 100644 --- a/src/inject.rs +++ b/src/inject.rs @@ -100,9 +100,14 @@ fn install_into_rootfs(mount: &str, name: &str, mode: &str, content: &str) -> Re Ok(()) } -/// Inject /slim/init, /slim/exec, and optionally /slim/user into a mounted -/// container image rootfs. -pub fn inject(mount_path: &Path, exec_script: &str, user: Option<&str>) -> Result<()> { +/// Inject /slim/init, /slim/exec, and optionally /slim/user and /slim/workdir +/// into a mounted container image rootfs. +pub fn inject( + mount_path: &Path, + exec_script: &str, + user: Option<&str>, + working_dir: Option<&str>, +) -> Result<()> { let mount_str = mount_path.to_str().context("mount path is not UTF-8")?; install_into_rootfs(mount_str, "init", "755", SLIM_INIT)?; @@ -112,5 +117,9 @@ pub fn inject(mount_path: &Path, exec_script: &str, user: Option<&str>) -> Resul install_into_rootfs(mount_str, "user", "644", user)?; } + if let Some(dir) = working_dir.filter(|d| !d.is_empty()) { + install_into_rootfs(mount_str, "workdir", "644", dir)?; + } + Ok(()) } diff --git a/src/qemu.rs b/src/qemu.rs index 7780e01..e5cc29d 100644 --- a/src/qemu.rs +++ b/src/qemu.rs @@ -8,7 +8,7 @@ use anyhow::{Context, Result, bail}; use base64::Engine; use clap::Args; -use std::{fmt::Write as _, path::PathBuf, process::Command}; +use std::{fmt::Write as _, process::Command}; use crate::{ forward::PortForward, @@ -33,10 +33,12 @@ pub struct RunCmd { #[clap(long)] cmd: Option, - /// Share a host directory into the VM via 9p. The directory appears at - /// the same path inside the VM. Can be repeated for multiple mounts. + /// Share a host directory into the VM via 9p. Format: + /// `` or `:`. When the guest path is + /// omitted, the host path is used. Non-absolute guest paths are + /// relative to the image's WORKINGDIR. Can be repeated. #[clap(long)] - mount: Vec, + mount: Vec, } pub(crate) fn run( @@ -88,25 +90,30 @@ pub(crate) fn run( } // Build 9p shares for each --mount. Tags are short (slim0, slim1, …) - // because 9p mount_tag has a ~31-byte limit. The full destination path - // is passed on the kernel cmdline as slim.mount=:. + // because 9p mount_tag has a ~31-byte limit. The guest destination path + // is passed on the kernel cmdline as slim.mount=:. + // If no guest path is specified, the host path is used as the guest path. let mut virtfs_args: Vec = Vec::new(); - for (i, host_path) in mount.iter().enumerate() { - let canonical = host_path + for (i, spec) in mount.iter().enumerate() { + let (host_path, guest_path) = match spec.split_once(':') { + Some((h, g)) => (h, g), + None => (spec.as_str(), spec.as_str()), + }; + let canonical = std::path::Path::new(host_path) .canonicalize() - .with_context(|| format!("Cannot resolve mount path '{}'", host_path.display()))?; - let path_str = canonical + .with_context(|| format!("Cannot resolve mount path '{host_path}'"))?; + let host_str = canonical .to_str() .context("Mount path is not valid UTF-8")?; let tag = format!("slim{i}"); - let dest_b64 = base64::engine::general_purpose::STANDARD.encode(path_str.as_bytes()); + let dest_b64 = base64::engine::general_purpose::STANDARD.encode(guest_path.as_bytes()); // QEMU's QemuOpts splits on commas — escape literal commas in the // path as ",," per QEMU convention. - let path_escaped = path_str.replace(',', ",,"); + let host_escaped = host_str.replace(',', ",,"); virtfs_args.push("-virtfs".into()); virtfs_args.push(format!( - "local,path={path_escaped},mount_tag={tag},security_model=mapped-xattr" + "local,path={host_escaped},mount_tag={tag},security_model=mapped-xattr" )); cmdline.push(format!("slim.mount={tag}:{dest_b64}")); }