diff --git a/src/main.rs b/src/main.rs index 20e09b4..aca50d5 100644 --- a/src/main.rs +++ b/src/main.rs @@ -49,81 +49,109 @@ fn registry_dir(image: &str) -> PathBuf { } fn build(image: &str) -> Result<()> { - // Step 1: mount via podman unshare (verified in Slice 0) + let mount_path = mount_image(image)?; + println!("Mounted at: {}", mount_path.display()); + + // Keep the build result so the image is unmounted even when the build fails. + let result = build_artifacts(image, &mount_path); + + let unmounted = Command::new("podman") + .args(["unshare", "--", "podman", "image", "unmount", image]) + .output() + .is_ok_and(|out| out.status.success()); + if unmounted { + println!("Unmounted image."); + } else { + eprintln!("warning: failed to unmount image '{}'", image); + } + + result +} + +fn mount_image(image: &str) -> Result { let output = Command::new("podman") .args(["unshare", "--", "podman", "image", "mount", image]) .output() - .context("Failed to run podman unshare image mount")?; - let mount_path_str = String::from_utf8_lossy(&output.stdout).trim().to_string(); - if mount_path_str.is_empty() { - anyhow::bail!("podman image mount returned empty path"); + .context("Failed to run podman image mount")?; + let mount_path = String::from_utf8_lossy(&output.stdout).trim().to_string(); + if !output.status.success() || mount_path.is_empty() { + anyhow::bail!( + "podman image mount failed: {}", + String::from_utf8_lossy(&output.stderr).trim() + ); } - let mount_path = Path::new(&mount_path_str); - println!("Mounted at: {}", mount_path.display()); + Ok(PathBuf::from(mount_path)) +} - // Step 2: find vmlinuz - let vmlinuz_src = mount_path.join("vmlinuz"); - if !vmlinuz_src.exists() { +fn build_artifacts(image: &str, mount_path: &Path) -> Result<()> { + let mount_path = mount_path.to_str().context("mount path is not UTF-8")?; + let vmlinuz_src = format!("{mount_path}/vmlinuz"); + + // The rootless overlay mount only exists inside podman unshare's user + // namespace, so every read of the rootfs must run there; pipes still + // cross the namespace boundary. + let has_kernel = Command::new("podman") + .args(["unshare", "--", "test", "-f", &vmlinuz_src]) + .status() + .context("Failed to check for /vmlinuz inside the image mount")?; + if !has_kernel.success() { anyhow::bail!( "Image missing required /vmlinuz. Place kernel at /vmlinuz in Containerfile." ); } - // Step 3: prepare registry dir let reg_dir = registry_dir(image); fs::create_dir_all(®_dir)?; println!("Registry: {}", reg_dir.display()); - // Copy vmlinuz out let vmlinuz_dst = reg_dir.join("vmlinuz"); - fs::copy(&vmlinuz_src, &vmlinuz_dst).context("Copy vmlinuz to registry")?; + let cp_status = Command::new("podman") + .args([ + "unshare", + "--", + "cp", + &vmlinuz_src, + vmlinuz_dst.to_str().context("registry path is not UTF-8")?, + ]) + .status() + .context("Failed to copy vmlinuz out of the image mount")?; + if !cp_status.success() { + anyhow::bail!("Failed to copy vmlinuz to registry"); + } println!("Copied vmlinuz -> {}", vmlinuz_dst.display()); - // Step 4: create initrd via cpio binary (using system cpio for reliability) - let initrd_raw = reg_dir.join("initrd.tmp"); + // Pack the rootfs as a gzipped newc cpio archive; vmlinuz is excluded + // because QEMU loads the kernel separately via -kernel. The cpio + // pipeline runs inside the namespace, its stdout is compressed here. let initrd_path = reg_dir.join("initrd"); - - // Build cpio from rootfs (excluding vmlinuz since it is separate kernel) - let mut find_cmd = Command::new("find") - .arg(".") - .current_dir(mount_path) + let script = r#"cd "$1" && find . -not -path ./vmlinuz | cpio -o -H newc"#; + let mut cpio = Command::new("podman") + .args(["unshare", "--", "sh", "-c", script, "sh", mount_path]) .stdout(Stdio::piped()) .spawn() - .context("Failed to spawn find")?; - let find_stdout = find_cmd.stdout.take().unwrap(); + .context("Failed to spawn the cpio pipeline")?; - let mut cpio_cmd = Command::new("cpio") - .args(["-o", "-H", "newc"]) - .current_dir(mount_path) - .stdin(Stdio::from(find_stdout)) - .stdout(Stdio::from(File::create(&initrd_raw)?)) - .spawn() - .context("Failed to spawn cpio")?; + let initrd_file = File::create(&initrd_path).context("Failed to create initrd file")?; + let mut encoder = flate2::write::GzEncoder::new(initrd_file, flate2::Compression::default()); + let cpio_stdout = cpio.stdout.take().context("cpio stdout was not piped")?; + let copied = std::io::copy(&mut std::io::BufReader::new(cpio_stdout), &mut encoder); + let stream_result = copied + .and_then(move |_| encoder.finish().map(|_| ())) + .context("Failed to write the gzipped initrd"); + let cpio_status = cpio + .wait() + .context("Failed to wait for the cpio pipeline")?; - let find_status = find_cmd.wait()?; - let cpio_status = cpio_cmd.wait()?; - if !find_status.success() || !cpio_status.success() { - anyhow::bail!("find or cpio failed"); + if let Err(e) = stream_result { + let _ = fs::remove_file(&initrd_path); + return Err(e); + } + if !cpio_status.success() { + let _ = fs::remove_file(&initrd_path); + anyhow::bail!("find/cpio pipeline failed with status {cpio_status}"); } - - // Gzip initrd - let initrd_file = fs::File::open(&initrd_raw)?; - let mut initrd_writer = - flate2::write::GzEncoder::new(File::create(&initrd_path)?, flate2::Compression::default()); - std::io::copy( - &mut std::io::BufReader::new(initrd_file), - &mut initrd_writer, - )?; - initrd_writer.finish()?; - fs::remove_file(&initrd_raw)?; println!("Created initrd -> {}", initrd_path.display()); - // Unmount image - let _ = Command::new("podman") - .args(["unshare", "--", "podman", "image", "unmount", image]) - .output(); - println!("Unmounted image."); - Ok(()) } @@ -160,12 +188,17 @@ fn run(name: &str) -> Result<()> { ]) .status() .context("Failed to launch qemu-system-x86_64")?; - if status.success() { - println!("QEMU exited cleanly (timeout or normal exit)."); - } else { - println!( - "QEMU exited with non-zero status (timeout/expected for initrd boot without init)." - ); + + // `timeout` exits 124 when it kills QEMU after the 5s smoke-test + // window; any other non-zero status is a real failure. + match status.code() { + Some(0) => println!("QEMU exited cleanly."), + Some(124) => println!("QEMU timed out after 5s (expected during smoke boot)."), + Some(126) | Some(127) => { + anyhow::bail!("Failed to start qemu-system-x86_64 (is it installed and in PATH?)") + } + Some(code) => anyhow::bail!("QEMU exited with code {code}"), + None => anyhow::bail!("QEMU terminated by a signal"), } Ok(()) }