Support host:guest mount syntax with WORKINGDIR resolution
CI / build (pull_request) Successful in 12s
CI / build (pull_request) Successful in 12s
--mount now accepts <host-path>:<guest-path> (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=<tag>:<base64(guest_path)> - 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.
This commit is contained in:
+51
-9
@@ -107,9 +107,13 @@ test_mount() {
|
||||
echo "=== Testing --mount (9p shares) ==="
|
||||
|
||||
work="$(mktemp -d)"
|
||||
|
||||
# --- Image 1: no WORKINGDIR, test host:guest and multi-mount ---
|
||||
img="slim-test-mount"
|
||||
share_dir="$work/share"
|
||||
share_dir2="$work/share2"
|
||||
guest_dir="/mnt/guest"
|
||||
guest_dir2="/mnt/guest2"
|
||||
|
||||
mkdir -p "$share_dir" "$share_dir2"
|
||||
echo "hello from host" > "$share_dir/testfile.txt"
|
||||
@@ -120,7 +124,7 @@ FROM alpine:latest
|
||||
CMD ["/bin/sh", "-c", "poweroff -f"]
|
||||
EOF
|
||||
|
||||
echo "-- Building container image..."
|
||||
echo "-- Building container image (no WORKINGDIR)..."
|
||||
if ! podman build --network=none -t "$img" -f "$work/Containerfile" >/dev/null 2>&1; then
|
||||
report fail "--mount (podman build failed)"
|
||||
rm -rf "$work"
|
||||
@@ -135,25 +139,63 @@ EOF
|
||||
return
|
||||
fi
|
||||
|
||||
# The 9p share appears at the same path inside the VM as on the host.
|
||||
# Use --cmd to run the verification snippet with the canonical path.
|
||||
share_canon=$(readlink -f "$share_dir")
|
||||
share_canon2=$(readlink -f "$share_dir2")
|
||||
|
||||
echo "-- Booting VM with --mount $share_dir --mount $share_dir2 ..."
|
||||
echo "-- Test 1: --mount host:guest (absolute guest path, multi-mount)..."
|
||||
output=$(timeout "$TIMEOUT" "$SLIM_BIN" run "$img" \
|
||||
--mount "$share_dir" \
|
||||
--mount "$share_dir2" \
|
||||
--cmd "echo Available 9p shares:; for f in /sys/bus/virtio/drivers/9pnet_virtio/virtio*/mount_tag; do [ -f \"\$f\" ] && echo \"\$f: \$(tr -d '\\0' < \"\$f\")\"; done; echo Mounts:; mount -v; cat \"${share_canon}/testfile.txt\" 2>/dev/null || echo NO_FILE; cat \"${share_canon2}/testfile2.txt\" 2>/dev/null || echo NO_FILE2; echo MOUNT_VERIFY_DONE; poweroff -f" \
|
||||
--mount "${share_dir}:${guest_dir}" \
|
||||
--mount "${share_dir2}:${guest_dir2}" \
|
||||
--cmd "for f in /sys/bus/virtio/drivers/9pnet_virtio/virtio*/mount_tag; do [ -f \"\$f\" ] && echo \"\$f: \$(tr -d '\\0' < \"\$f\")\"; done; mount -v; cat ${guest_dir}/testfile.txt 2>/dev/null || echo NO_FILE; cat ${guest_dir2}/testfile2.txt 2>/dev/null || echo NO_FILE2; echo MOUNT_VERIFY_DONE; poweroff -f" \
|
||||
2>&1 || true)
|
||||
|
||||
check_output "$output" "mount_tag: slim0" "--mount 9p share detected"
|
||||
check_output "$output" "mount_tag: slim1" "--mount second 9p share detected"
|
||||
check_output "$output" "type 9p" "--mount 9p filesystem in mount list"
|
||||
check_output "$output" "hello from host" "--mount file accessible in VM"
|
||||
check_output "$output" "second share" "--mount second share file accessible in VM"
|
||||
check_output "$output" "hello from host" "--mount file accessible at guest path"
|
||||
check_output "$output" "second share" "--mount second file accessible at guest path"
|
||||
check_output "$output" "MOUNT_VERIFY_DONE" "--mount VM ran to completion"
|
||||
|
||||
cleanup "$img" "$img"
|
||||
|
||||
# --- Image 2: with WORKINGDIR, test relative guest path ---
|
||||
img="slim-test-mount-wd"
|
||||
share_dir3="$work/share3"
|
||||
|
||||
mkdir -p "$share_dir3"
|
||||
echo "relative share" > "$share_dir3/relfile.txt"
|
||||
|
||||
cat > "$work/Containerfile.wd" <<'EOF'
|
||||
FROM alpine:latest
|
||||
WORKDIR /app
|
||||
CMD ["/bin/sh", "-c", "poweroff -f"]
|
||||
EOF
|
||||
|
||||
echo "-- Building container image (WORKINGDIR /app)..."
|
||||
if ! podman build --network=none -t "$img" -f "$work/Containerfile.wd" >/dev/null 2>&1; then
|
||||
report fail "--mount relative (podman build failed)"
|
||||
rm -rf "$work"
|
||||
return
|
||||
fi
|
||||
|
||||
echo "-- Building slim VM..."
|
||||
if ! "$SLIM_BIN" build qcow2 "$img" >/dev/null 2>&1; then
|
||||
report fail "--mount relative (slim build failed)"
|
||||
cleanup "$img" "$img"
|
||||
rm -rf "$work"
|
||||
return
|
||||
fi
|
||||
|
||||
echo "-- Test 2: --mount host:relative-guest (resolved against WORKINGDIR)..."
|
||||
output=$(timeout "$TIMEOUT" "$SLIM_BIN" run "$img" \
|
||||
--mount "${share_dir3}:data" \
|
||||
--cmd "mount -v; cat /app/data/relfile.txt 2>/dev/null || echo NO_REL_FILE; echo REL_VERIFY_DONE; poweroff -f" \
|
||||
2>&1 || true)
|
||||
|
||||
check_output "$output" "type 9p" "--mount relative 9p filesystem in mount list"
|
||||
check_output "$output" "relative share" "--mount relative file accessible at WORKINGDIR/data"
|
||||
check_output "$output" "REL_VERIFY_DONE" "--mount relative VM ran to completion"
|
||||
|
||||
cleanup "$img" "$img"
|
||||
rm -rf "$work"
|
||||
}
|
||||
|
||||
+1
-1
@@ -78,7 +78,7 @@ 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)?;
|
||||
inject::inject(mount_path, &exec_script, config.working_dir.as_deref())?;
|
||||
println!("Injected /slim/ (init + exec)");
|
||||
|
||||
match kind {
|
||||
|
||||
+24
-2
@@ -85,8 +85,9 @@ pub fn build_exec_script(command: &str, env: &[String], working_dir: Option<&str
|
||||
lines
|
||||
}
|
||||
|
||||
/// Inject /slim/init and /slim/exec into a mounted container image rootfs.
|
||||
pub fn inject(mount_path: &Path, exec_script: &str) -> Result<()> {
|
||||
/// Inject /slim/init and /slim/exec (and optionally /slim/workdir) into a
|
||||
/// mounted container image rootfs.
|
||||
pub fn inject(mount_path: &Path, exec_script: &str, working_dir: Option<&str>) -> Result<()> {
|
||||
let mount_str = mount_path.to_str().context("mount path is not UTF-8")?;
|
||||
|
||||
let init_temp = tempfile::NamedTempFile::new()?;
|
||||
@@ -113,5 +114,26 @@ pub fn inject(mount_path: &Path, exec_script: &str) -> Result<()> {
|
||||
"podman", "unshare", "--", "install", "-D", "-m", "755", exec_src, &exec_dest,
|
||||
])?;
|
||||
|
||||
if let Some(dir) = working_dir.filter(|d| !d.is_empty()) {
|
||||
let workdir_temp = tempfile::NamedTempFile::new()?;
|
||||
fs::write(workdir_temp.path(), dir)?;
|
||||
let workdir_src = workdir_temp
|
||||
.path()
|
||||
.to_str()
|
||||
.context("temp path is not UTF-8")?;
|
||||
let workdir_dest = format!("{mount_str}/slim/workdir");
|
||||
cmd(&[
|
||||
"podman",
|
||||
"unshare",
|
||||
"--",
|
||||
"install",
|
||||
"-D",
|
||||
"-m",
|
||||
"644",
|
||||
workdir_src,
|
||||
&workdir_dest,
|
||||
])?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
+20
-13
@@ -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<String>,
|
||||
|
||||
/// 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:
|
||||
/// `<host-path>` or `<host-path>:<guest-path>`. 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<PathBuf>,
|
||||
mount: Vec<String>,
|
||||
}
|
||||
|
||||
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=<tag>:<base64(path)>.
|
||||
// because 9p mount_tag has a ~31-byte limit. The guest destination path
|
||||
// is passed on the kernel cmdline as slim.mount=<tag>:<base64(guest_path)>.
|
||||
// If no guest path is specified, the host path is used as the guest path.
|
||||
let mut virtfs_args: Vec<String> = 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}"));
|
||||
}
|
||||
|
||||
@@ -40,9 +40,13 @@ ip route add default via 10.0.2.2 2>/dev/null || echo "ip route add default fail
|
||||
echo nameserver 10.0.2.3 > /etc/resolv.conf
|
||||
|
||||
# === 9p shares (host directories shared via --mount) ===
|
||||
# Each share is passed as slim.mount=<tag>:<base64(dest_path)> on the
|
||||
# Each share is passed as slim.mount=<tag>:<base64(guest_path)> on the
|
||||
# kernel cmdline. The 9p tag is short (e.g. slim0) because mount_tag
|
||||
# has a ~31-byte limit; the full destination path is base64-encoded.
|
||||
# has a ~31-byte limit; the guest path is base64-encoded.
|
||||
# Non-absolute guest paths are resolved against /slim/workdir (the
|
||||
# image's WORKINGDIR).
|
||||
SLIM_WORKDIR=""
|
||||
[ -f /slim/workdir ] && SLIM_WORKDIR=$(cat /slim/workdir 2>/dev/null)
|
||||
# shellcheck disable=SC2013 # word-splitting is intentional: cmdline tokens are space-separated
|
||||
for tok in $(cat /proc/cmdline 2>/dev/null); do
|
||||
case "$tok" in
|
||||
@@ -52,6 +56,11 @@ for tok in $(cat /proc/cmdline 2>/dev/null); do
|
||||
dest_b64=${v#*:}
|
||||
dest=$(b64dec "$dest_b64") || echo "base64 decode failed"
|
||||
if [ -n "$tag" ] && [ -n "$dest" ]; then
|
||||
# Resolve relative paths against the image's WORKINGDIR
|
||||
case "$dest" in
|
||||
/*) ;;
|
||||
*) dest="${SLIM_WORKDIR:-/}/${dest}" ;;
|
||||
esac
|
||||
mkdir -p "$dest"
|
||||
mount -t 9p "$tag" "$dest" -o trans=virtio,version=9p2000.L 2>/dev/null \
|
||||
&& echo "mounted 9p '$tag' -> '$dest'" \
|
||||
|
||||
Reference in New Issue
Block a user