Progress
CI / build (push) Successful in 14s

This commit is contained in:
2026-09-04 17:01:46 +02:00
parent 286def05c1
commit 7dd818db68
7 changed files with 170 additions and 24 deletions
+5 -3
View File
@@ -1,11 +1,13 @@
#!/bin/sh #!/bin/sh
# Mount special filesystems # Create console device
mkdir -p /dev /proc /sys # TODO: is this necessary?
[ -c /dev/console ] || mknod -m 600 /dev/console c 5 1 [ -c /dev/console ] || mknod -m 600 /dev/console c 5 1
# Mount special filesystems
mkdir -p /proc /sys
mount -t proc proc /proc mount -t proc proc /proc
mount -t sysfs sysfs /sys mount -t sysfs sysfs /sys
mount -t devtmpfs dev /dev
# Networking # Networking
echo slim > /proc/sys/kernel/hostname echo slim > /proc/sys/kernel/hostname
-1
View File
@@ -5,7 +5,6 @@ mkdir -p /dev /proc /sys
[ -c /dev/console ] || mknod -m 600 /dev/console c 5 1 [ -c /dev/console ] || mknod -m 600 /dev/console c 5 1
mount -t proc proc /proc mount -t proc proc /proc
mount -t sysfs sysfs /sys mount -t sysfs sysfs /sys
mount -t devtmpfs dev /dev
# Networking # Networking
echo slim > /proc/sys/kernel/hostname echo slim > /proc/sys/kernel/hostname
+6 -10
View File
@@ -43,10 +43,10 @@ pub(crate) fn build(BuildCmd { kind, name, init }: BuildCmd) -> Result<()> {
ImageKind::Qcow2 => build_qcow2(image, &mount_path), ImageKind::Qcow2 => build_qcow2(image, &mount_path),
}; };
let unmounted = Command::new("podman") let unmounted = cmd(&[
.args(["unshare", "--", "podman", "image", "unmount", image]) "podman", "unshare", "--", "podman", "image", "unmount", image,
.output() ])
.is_ok_and(|out| out.status.success()); .is_ok();
if unmounted { if unmounted {
println!("Unmounted image."); println!("Unmounted image.");
} else { } else {
@@ -137,19 +137,15 @@ fn build_initrd(image: &str, mount_path: &Path) -> Result<()> {
println!("Registry: {}", reg_dir.display()); println!("Registry: {}", reg_dir.display());
let vmlinuz_dst = reg_dir.join("vmlinuz"); let vmlinuz_dst = reg_dir.join("vmlinuz");
let cp_status = Command::new("podman") cmd(&[
.args([ "podman",
"unshare", "unshare",
"--", "--",
"cp", "cp",
&vmlinuz_src, &vmlinuz_src,
vmlinuz_dst.to_str().context("registry path is not UTF-8")?, vmlinuz_dst.to_str().context("registry path is not UTF-8")?,
]) ])
.status()
.context("Failed to copy vmlinuz out of the image mount")?; .context("Failed to copy vmlinuz out of the image mount")?;
if !cp_status.success() {
bail!("Failed to copy vmlinuz to registry");
}
println!("Copied vmlinuz -> {}", vmlinuz_dst.display()); println!("Copied vmlinuz -> {}", vmlinuz_dst.display());
// Pack the rootfs as a gzipped newc cpio archive; vmlinuz is excluded // Pack the rootfs as a gzipped newc cpio archive; vmlinuz is excluded
+4
View File
@@ -2,6 +2,10 @@ use std::process::Command;
use anyhow::{Context, anyhow, bail}; use anyhow::{Context, anyhow, bail};
/// Spawn a subprocess with arguments.
///
/// Collects `stdout` and returns it on success.
/// Returns `Err` if the exit code isn't 0, or if the process failed to spawn.
pub fn cmd(cmd: &[&str]) -> anyhow::Result<String> { pub fn cmd(cmd: &[&str]) -> anyhow::Result<String> {
let [cmd, args @ ..] = cmd else { let [cmd, args @ ..] = cmd else {
bail!("missing command"); bail!("missing command");
+121
View File
@@ -0,0 +1,121 @@
use std::{
fmt::{self, Display},
net::{Ipv4Addr, SocketAddr},
str::FromStr,
};
use anyhow::{Context, anyhow, bail};
use serde::{Deserialize, Serialize, de::Error};
#[derive(Clone, Copy, Debug)]
pub enum Protocol {
Tcp,
Udp,
}
#[derive(Clone, Copy, Debug)]
pub struct PortForward {
pub protocol: Protocol,
pub from: SocketAddr,
pub to_addr: Option<Ipv4Addr>,
pub to_port: u16,
}
impl Display for Protocol {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(match self {
Protocol::Tcp => "tcp",
Protocol::Udp => "udp",
})
}
}
impl Display for PortForward {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}:{}-", self.protocol, self.from)?;
if let Some(to_addr) = self.to_addr {
write!(f, ":{}", to_addr)?;
}
write!(f, ":{}", self.to_port)
}
}
impl FromStr for PortForward {
type Err = anyhow::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let result = || {
let (protocol, s) = s.split_once(':').context("Missing ':'")?;
let (from, to) = s.split_once('-').context("Missing '-'")?;
let (to_addr, to_port) = to.split_once(':').context("Missing ':'")?;
let from = if let Some(from) = from.strip_prefix(':') {
let port: u16 = from.parse().context("Invalid port number")?;
SocketAddr::new(Ipv4Addr::UNSPECIFIED.into(), port)
} else {
from.parse()?
};
let to_addr = if to_addr.is_empty() {
None
} else {
Some(to_addr.parse()?)
};
Ok(PortForward {
from,
to_addr,
to_port: to_port.parse()?,
protocol: match protocol {
"udp" => Protocol::Udp,
"tcp" => Protocol::Tcp,
_ => bail!("Invalid protocol, expected 'udp' or 'tcp'"),
},
})
};
result()
.with_context(|| anyhow!("Expected '(tcp|udp):<address>-<address>', got {s:?}"))
.context("Malformed PortForward string")
}
}
impl Serialize for PortForward {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
format_args!("{}", self).serialize(serializer)
}
}
impl<'de> Deserialize<'de> for PortForward {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let s: String = Deserialize::deserialize(deserializer)?;
s.parse().map_err(D::Error::custom)
}
}
#[cfg(test)]
mod test {
use std::str::FromStr;
use super::PortForward;
#[test]
fn parse_port_forwards() {
let valid = [
"tcp:0.0.0.0:80-:8080",
"udp::1234-:1234",
"tcp:0.0.0.0:80-1.2.3.4:8080",
"udp::1234-255.255.255.255:1234",
];
for valid in valid {
PortForward::from_str(valid).expect("Failed to parse a valid port forward string");
}
}
}
+1
View File
@@ -1,5 +1,6 @@
mod build; mod build;
mod command; mod command;
mod forward;
mod image; mod image;
mod meta; mod meta;
mod qemu; mod qemu;
+26 -3
View File
@@ -3,9 +3,10 @@
use anyhow::{Context, Result, bail}; use anyhow::{Context, Result, bail};
use clap::Args; use clap::Args;
use std::process::Command; use std::{fmt::Write as _, process::Command};
use crate::{ use crate::{
forward::PortForward,
meta::Meta, meta::Meta,
registry::{registry_base_dir, registry_dir}, registry::{registry_base_dir, registry_dir},
}; };
@@ -18,9 +19,19 @@ pub struct RunCmd {
/// Amount of memory to give the VM, in qemu's format. /// Amount of memory to give the VM, in qemu's format.
#[clap(short, long, default_value = "1024M")] #[clap(short, long, default_value = "1024M")]
memory: String, memory: String,
/// Forward ports from host to guest. Example: `tcp:0.0.0.0:80-:8080`
#[clap(long)]
forward: Vec<PortForward>,
} }
pub(crate) fn run(RunCmd { name, memory }: RunCmd) -> Result<()> { pub(crate) fn run(
RunCmd {
name,
memory,
forward,
}: RunCmd,
) -> Result<()> {
let reg_dir = registry_dir(&name)?; let reg_dir = registry_dir(&name)?;
let vmlinuz_path = registry_base_dir()?.join("vmlinuz"); let vmlinuz_path = registry_base_dir()?.join("vmlinuz");
let initrd_path = reg_dir.join("initrd"); let initrd_path = reg_dir.join("initrd");
@@ -41,6 +52,7 @@ pub(crate) fn run(RunCmd { name, memory }: RunCmd) -> Result<()> {
"earlyprintk=serial", "earlyprintk=serial",
"nokaslr", "nokaslr",
&init_arg, &init_arg,
"devtmpfs.mount=1", // Automatically mount /dev at boot
]; ];
let qcow2_arg = format!("file={},format=qcow2,if=virtio", qcow2_path.display()); let qcow2_arg = format!("file={},format=qcow2,if=virtio", qcow2_path.display());
@@ -63,15 +75,26 @@ pub(crate) fn run(RunCmd { name, memory }: RunCmd) -> Result<()> {
let cmdline = cmdline.join(" "); let cmdline = cmdline.join(" ");
// add NIC and forward any user specified ports
let mut network = "user,model=virtio-net-pci".to_string();
for forward in &forward {
_ = write!(&mut network, ",hostfwd={forward}");
}
let mut command = Command::new("qemu-system-x86_64"); let mut command = Command::new("qemu-system-x86_64");
// TODO: make a lot of this configurable. especially -cpus
command command
.args(["-accel", "kvm"])
.args(["-cpu", "host", "-smp", "cpus=8"])
.args(["-m", &memory]) .args(["-m", &memory])
.arg("-kernel") .arg("-kernel")
.arg(vmlinuz_path.as_os_str()) .arg(vmlinuz_path.as_os_str())
.args(fs_args) .args(fs_args)
.args(["-snapshot"])
.args(["-append", &cmdline]) .args(["-append", &cmdline])
.args(["-nographic"]) .args(["-nographic"])
.args(["-nic", "user,model=virtio-net-pci"]); .args(["-nic", &network]);
println!("{command:?}"); println!("{command:?}");