Compare commits
5
Commits
de507682c1
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f36897abc2 | ||
|
|
ba90e0f367 | ||
|
|
12f771d326 | ||
|
|
810bf50814 | ||
|
|
5dd9dff76a |
@@ -88,7 +88,7 @@ ecr [OPTIONS] <DISTRO[:VERSION]> [-- COMMAND...]
|
|||||||
| `--no-cache` | Force a fresh download, bypassing the cache |
|
| `--no-cache` | Force a fresh download, bypassing the cache |
|
||||||
| `-v, --verbose` | Print diagnostic output (URLs, layer info, extraction steps) |
|
| `-v, --verbose` | Print diagnostic output (URLs, layer info, extraction steps) |
|
||||||
| `-a, --arch <ARCH>` | Target architecture (`amd64`, `arm64`, `armhf`, `riscv64`, …) |
|
| `-a, --arch <ARCH>` | Target architecture (`amd64`, `arm64`, `armhf`, `riscv64`, …) |
|
||||||
| `--kernel[=PATH]` | Boot with QEMU system emulation. Downloads Alpine's `linux-virt` kernel if no `=PATH` given |
|
| `--kernel[=PATH]` | Boot with QEMU system emulation. Downloads Alpine's default kernel if no `=PATH` given (`linux-virt`, or `linux-lts` on riscv64 where the virt flavor is not built) |
|
||||||
| `-m, --memory <SIZE>` | Memory for QEMU VM (default: 2G, only with `--kernel`) |
|
| `-m, --memory <SIZE>` | Memory for QEMU VM (default: 2G, only with `--kernel`) |
|
||||||
|
|
||||||
## Examples
|
## Examples
|
||||||
@@ -124,7 +124,7 @@ ecr --kernel --memory 4G alpine
|
|||||||
When `--kernel` is specified, ecr boots the rootfs in a full QEMU virtual machine instead of using namespaces:
|
When `--kernel` is specified, ecr boots the rootfs in a full QEMU virtual machine instead of using namespaces:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
# Auto-download Alpine's linux-virt kernel (recommended)
|
# Auto-download Alpine's default kernel (recommended)
|
||||||
ecr --kernel alpine
|
ecr --kernel alpine
|
||||||
|
|
||||||
# Use your own kernel
|
# Use your own kernel
|
||||||
@@ -133,7 +133,7 @@ ecr --kernel=/boot/vmlinuz ubuntu
|
|||||||
|
|
||||||
This mode:
|
This mode:
|
||||||
- Creates an uncompressed CPIO initramfs from the rootfs (streamed to disk)
|
- Creates an uncompressed CPIO initramfs from the rootfs (streamed to disk)
|
||||||
- Boots QEMU with your kernel (or auto-downloads Alpine's `linux-virt` kernel)
|
- Boots QEMU with your kernel (or auto-downloads Alpine's default kernel: `linux-virt`, falling back to `linux-lts` on riscv64 where the virt flavor is not built; the downloaded image is stored decompressed so QEMU can load it)
|
||||||
- Provides full VM isolation
|
- Provides full VM isolation
|
||||||
- Works for any architecture (no binfmt_misc needed)
|
- Works for any architecture (no binfmt_misc needed)
|
||||||
- Caches the default kernel in `~/.cache/ecr/`
|
- Caches the default kernel in `~/.cache/ecr/`
|
||||||
@@ -143,6 +143,7 @@ Host bind mounts (`--bind`, `--bind-rw`) are not applied in this mode.
|
|||||||
Requirements:
|
Requirements:
|
||||||
- `qemu-system-<arch>` installed
|
- `qemu-system-<arch>` installed
|
||||||
- For custom kernels: kernel must have serial console support
|
- For custom kernels: kernel must have serial console support
|
||||||
|
- riscv64 VMs need QEMU ≥ 10.1 (`rva23s64` CPU, required by Ubuntu 25.10+ RVA23 userland)
|
||||||
|
|
||||||
## Supported distributions
|
## Supported distributions
|
||||||
|
|
||||||
|
|||||||
@@ -80,7 +80,7 @@ ecr [OPTIONS] <DISTRO[:VERSION]> -- [COMMAND]...
|
|||||||
| `--bind-rw <path>` | none | Read-write bind mount at `/mnt/<basename>` (can be specified multiple times, overrides `--bind` for same path) |
|
| `--bind-rw <path>` | none | Read-write bind mount at `/mnt/<basename>` (can be specified multiple times, overrides `--bind` for same path) |
|
||||||
| `--no-cache` | false | Download fresh tarball, ignore cache |
|
| `--no-cache` | false | Download fresh tarball, ignore cache |
|
||||||
| `--no-bind` | false | Skip mounting any directory |
|
| `--no-bind` | false | Skip mounting any directory |
|
||||||
| `--kernel[=PATH]` | none | Boot with QEMU system emulation; downloads the default Alpine `linux-virt` kernel when no `=PATH` is given |
|
| `--kernel[=PATH]` | none | Boot with QEMU system emulation; downloads the default Alpine kernel when no `=PATH` is given (`linux-virt`, falling back to `linux-lts` on architectures without a virt flavor, e.g. riscv64) |
|
||||||
| `-m, --memory <size>` | 2G | Memory size for QEMU VM (only used with `--kernel`) |
|
| `-m, --memory <size>` | 2G | Memory size for QEMU VM (only used with `--kernel`) |
|
||||||
| `-v, --verbose` | false | Print diagnostic messages |
|
| `-v, --verbose` | false | Print diagnostic messages |
|
||||||
| `-h, --help` | - | Show help |
|
| `-h, --help` | - | Show help |
|
||||||
@@ -274,6 +274,7 @@ ecr --kernel=/boot/vmlinuz debian -- /bin/sh -c "echo hello"
|
|||||||
2. Extract tarball to temporary directory
|
2. Extract tarball to temporary directory
|
||||||
3. Create uncompressed CPIO initramfs from rootfs (streamed to disk), including essential device nodes (/dev/ttyS0, /dev/null, /dev/tty) and an `/init` script that mounts proc/sys/dev, sets the hostname, execs the requested command argv verbatim (each argv element base64-encoded in the cmdline as `ECR_ARGV`), and powers off on exit
|
3. Create uncompressed CPIO initramfs from rootfs (streamed to disk), including essential device nodes (/dev/ttyS0, /dev/null, /dev/tty) and an `/init` script that mounts proc/sys/dev, sets the hostname, execs the requested command argv verbatim (each argv element base64-encoded in the cmdline as `ECR_ARGV`), and powers off on exit
|
||||||
4. Launch QEMU with:
|
4. Launch QEMU with:
|
||||||
|
- `-machine virt` on riscv64 only - qemu-system-riscv64's default machine is `spike`, which has neither a PCI bus (virtio-net-pci fails) nor a 16550 UART (console output is lost); `virt` has both plus bundled OpenSBI firmware
|
||||||
- `-kernel <path>` - provided (or downloaded) kernel
|
- `-kernel <path>` - provided (or downloaded) kernel
|
||||||
- `-initrd initramfs.cpio` - rootfs as initramfs
|
- `-initrd initramfs.cpio` - rootfs as initramfs
|
||||||
- `-append "console=ttyS0 [quiet] ECR_SHELL=... [ECR_ARGV=...] ECR_HOSTNAME=..."` - kernel command line (`quiet` unless `-v`)
|
- `-append "console=ttyS0 [quiet] ECR_SHELL=... [ECR_ARGV=...] ECR_HOSTNAME=..."` - kernel command line (`quiet` unless `-v`)
|
||||||
@@ -281,9 +282,14 @@ ecr --kernel=/boot/vmlinuz debian -- /bin/sh -c "echo hello"
|
|||||||
- `-display none -serial mon:stdio` - console on stdio
|
- `-display none -serial mon:stdio` - console on stdio
|
||||||
- `-netdev user,id=net0 -device virtio-net-pci,netdev=net0` - network NIC
|
- `-netdev user,id=net0 -device virtio-net-pci,netdev=net0` - network NIC
|
||||||
- `-enable-kvm -cpu host` - when the host supports KVM and the target matches the host architecture
|
- `-enable-kvm -cpu host` - when the host supports KVM and the target matches the host architecture
|
||||||
|
- `-cpu rva23s64` - riscv64 under TCG otherwise. Ubuntu builds its riscv64 port against the RVA23 profile (since 25.10); QEMU's default `rv64` CPU does not implement all profile extensions, so Ubuntu binaries die with SIGILL during init. Profiles are supersets, so baseline rv64gc rootfses (Alpine, older Ubuntu) run unchanged. Needs QEMU ≥ 10.1 (named profile CPUs).
|
||||||
5. Wait for QEMU to exit (init powers the VM off when the command/shell exits; `-no-reboot` makes QEMU terminate)
|
5. Wait for QEMU to exit (init powers the VM off when the command/shell exits; `-no-reboot` makes QEMU terminate)
|
||||||
6. Cleanup temporary files
|
6. Cleanup temporary files
|
||||||
|
|
||||||
|
### Default Kernel Download
|
||||||
|
|
||||||
|
The default kernel (`--kernel` without `=PATH`) comes from Alpine's `main` repository. `linux-virt` is preferred; where it is not built (riscv64), the index lookup falls back to `linux-lts`. Some architectures package their kernel image gzipped (riscv64, aarch64 ship `Image.gz`), and QEMU's riscv `-kernel` loader understands only ELF, uImage and raw images, so the cached kernel is stored decompressed (gzip magic `1f 8b` detected and gunzipped at download time).
|
||||||
|
|
||||||
### Initramfs Creation
|
### Initramfs Creation
|
||||||
|
|
||||||
The rootfs directory is converted to an uncompressed CPIO archive (newc format) using the `cpio` crate, streamed entry by entry so large rootfs images never need to fit in memory. Hard links are preserved: the first occurrence of a (device, inode) pair carries the data with a synthetic inode, subsequent occurrences are zero-size entries sharing that inode, which the kernel's initramfs loader turns into real hard links.
|
The rootfs directory is converted to an uncompressed CPIO archive (newc format) using the `cpio` crate, streamed entry by entry so large rootfs images never need to fit in memory. Hard links are preserved: the first occurrence of a (device, inode) pair carries the data with a synthetic inode, subsequent occurrences are zero-size entries sharing that inode, which the kernel's initramfs loader turns into real hard links.
|
||||||
|
|||||||
@@ -36,7 +36,8 @@ pub struct Args {
|
|||||||
/// Boot with QEMU system emulation (optionally specify kernel path with =PATH, or omit to download default)
|
/// Boot with QEMU system emulation (optionally specify kernel path with =PATH, or omit to download default)
|
||||||
///
|
///
|
||||||
/// Examples:
|
/// Examples:
|
||||||
/// --kernel Download and use the default Alpine linux-virt kernel
|
/// --kernel Download and use the default Alpine kernel
|
||||||
|
/// (linux-virt, or linux-lts on riscv64)
|
||||||
/// --kernel=./vmlinuz Use a specific kernel file
|
/// --kernel=./vmlinuz Use a specific kernel file
|
||||||
///
|
///
|
||||||
/// The kernel path must use `=` syntax: with `--kernel ./vmlinuz` the
|
/// The kernel path must use `=` syntax: with `--kernel ./vmlinuz` the
|
||||||
|
|||||||
+126
-27
@@ -1,22 +1,32 @@
|
|||||||
//! Default kernel download for QEMU VM mode.
|
//! Default kernel download for QEMU VM mode.
|
||||||
//!
|
//!
|
||||||
//! When `--kernel` is specified without a path, we download a default kernel
|
//! When `--kernel` is specified without a path, we download a default kernel
|
||||||
//! suitable for VM booting. We use Alpine's `linux-virt` package because:
|
//! suitable for VM booting from Alpine's kernel packages:
|
||||||
//!
|
//!
|
||||||
//! - Small size (~10-15MB compressed)
|
//! - Small size (~10-15MB compressed)
|
||||||
//! - VM-optimized configuration
|
//! - VM-optimized configuration (`linux-virt`)
|
||||||
//! - Multi-architecture support
|
//! - Multi-architecture support
|
||||||
//! - Simple direct download URLs
|
//! - Simple direct download URLs
|
||||||
//!
|
//!
|
||||||
//! The kernel is cached in the same cache directory as rootfs images.
|
//! `linux-virt` is preferred, but it is not built for every architecture
|
||||||
|
//! (riscv64 has no virt flavor in `main`), so we fall back to `linux-lts`
|
||||||
|
//! when the virt flavor is absent from the repository index.
|
||||||
|
//!
|
||||||
|
//! The kernel is cached in the same cache directory as rootfs images,
|
||||||
|
//! decompressed: some architectures (aarch64, riscv64) package their
|
||||||
|
//! kernel image gzipped, and QEMU's riscv `-kernel` loader understands
|
||||||
|
//! only ELF, uImage and raw images.
|
||||||
|
|
||||||
use crate::veprintln;
|
use crate::veprintln;
|
||||||
use anyhow::{anyhow, Context, Result};
|
use anyhow::{anyhow, Context, Result};
|
||||||
use indicatif::{ProgressBar, ProgressStyle};
|
use indicatif::{ProgressBar, ProgressStyle};
|
||||||
use std::io::Read;
|
use std::io::{Read, Seek};
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
|
/// Alpine kernel packages to consider, most preferred first
|
||||||
|
const KERNEL_PACKAGES: [&str; 2] = ["linux-virt", "linux-lts"];
|
||||||
|
|
||||||
/// Alpine architecture mapping for kernel packages
|
/// Alpine architecture mapping for kernel packages
|
||||||
fn alpine_kernel_arch(arch: &str) -> &'static str {
|
fn alpine_kernel_arch(arch: &str) -> &'static str {
|
||||||
match arch {
|
match arch {
|
||||||
@@ -91,8 +101,11 @@ fn fetch_alpine_branch_from_yaml() -> Result<String> {
|
|||||||
Err(anyhow!("Could not determine Alpine version from releases"))
|
Err(anyhow!("Could not determine Alpine version from releases"))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Fetch the latest linux-virt package version from Alpine's package index
|
/// Find the newest kernel package among `KERNEL_PACKAGES` in Alpine's
|
||||||
fn get_linux_virt_version(branch: &str, arch: &str) -> Result<String> {
|
/// package index. Returns the package name and version. The index is
|
||||||
|
/// scanned once; whichever candidate flavor appears with the highest
|
||||||
|
/// preference (lowest index) wins.
|
||||||
|
fn find_kernel_package(branch: &str, arch: &str) -> Result<(&'static str, String)> {
|
||||||
// Alpine package index URL
|
// Alpine package index URL
|
||||||
let url = format!(
|
let url = format!(
|
||||||
"https://dl-cdn.alpinelinux.org/alpine/{}/main/{}/APKINDEX.tar.gz",
|
"https://dl-cdn.alpinelinux.org/alpine/{}/main/{}/APKINDEX.tar.gz",
|
||||||
@@ -173,25 +186,45 @@ fn get_linux_virt_version(branch: &str, arch: &str) -> Result<String> {
|
|||||||
|
|
||||||
veprintln!(" APKINDEX size: {} bytes", contents.len());
|
veprintln!(" APKINDEX size: {} bytes", contents.len());
|
||||||
|
|
||||||
// Parse the APKINDEX to find linux-virt
|
// Parse the APKINDEX to find the best kernel package
|
||||||
// Format:
|
// Format:
|
||||||
// P:linux-virt
|
// P:linux-virt
|
||||||
// V:6.12.8-r0
|
// V:6.12.8-r0
|
||||||
// ...
|
// ...
|
||||||
|
// The first V: line of the highest-preference package wins; the
|
||||||
|
// earliest version entry in the index is the latest build.
|
||||||
let mut pkg_name: Option<String> = None;
|
let mut pkg_name: Option<String> = None;
|
||||||
|
// (preference index, package, version)
|
||||||
|
let mut best: Option<(usize, &'static str, String)> = None;
|
||||||
|
|
||||||
for line in contents.lines() {
|
for line in contents.lines() {
|
||||||
if let Some(name) = line.strip_prefix("P:") {
|
if let Some(name) = line.strip_prefix("P:") {
|
||||||
pkg_name = Some(name.trim().to_string());
|
pkg_name = Some(name.trim().to_string());
|
||||||
} else if let Some(version) = line.strip_prefix("V:") {
|
} else if let Some(version) = line.strip_prefix("V:") {
|
||||||
if pkg_name.as_deref() == Some("linux-virt") {
|
if let Some(name) = &pkg_name {
|
||||||
return Ok(version.trim().to_string());
|
if let Some(idx) = KERNEL_PACKAGES.iter().position(|p| *p == name) {
|
||||||
|
let preferred = best
|
||||||
|
.as_ref()
|
||||||
|
.map_or(true, |(best_idx, _, _)| idx < *best_idx);
|
||||||
|
if preferred {
|
||||||
|
best =
|
||||||
|
Some((idx, KERNEL_PACKAGES[idx], version.trim().to_string()));
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// If we got here, we found APKINDEX but not linux-virt
|
match best {
|
||||||
return Err(anyhow!("linux-virt package not found in APKINDEX. Available packages may vary by architecture."));
|
Some((_, pkg, version)) => return Ok((pkg, version)),
|
||||||
|
None => {
|
||||||
|
return Err(anyhow!(
|
||||||
|
"no kernel package found in APKINDEX (looked for: {}). \
|
||||||
|
Available packages may vary by architecture.",
|
||||||
|
KERNEL_PACKAGES.join(", ")
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -203,16 +236,16 @@ fn get_linux_virt_version(branch: &str, arch: &str) -> Result<String> {
|
|||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Download and extract the linux-virt kernel from Alpine's package repository
|
/// Download and extract the kernel from an Alpine package repository
|
||||||
fn download_alpine_kernel(branch: &str, arch: &str, dest: &Path) -> Result<()> {
|
fn download_alpine_kernel(branch: &str, arch: &str, dest: &Path) -> Result<()> {
|
||||||
let version = get_linux_virt_version(branch, arch)?;
|
let (package, version) = find_kernel_package(branch, arch)?;
|
||||||
veprintln!("Found linux-virt version: {}", version);
|
veprintln!("Found {} version: {}", package, version);
|
||||||
|
|
||||||
// Construct the download URL for the linux-virt .apk
|
// Construct the download URL for the kernel .apk
|
||||||
// Format: https://dl-cdn.alpinelinux.org/alpine/v3.23/main/x86_64/linux-virt-6.12.8-r0.apk
|
// Format: https://dl-cdn.alpinelinux.org/alpine/v3.23/main/x86_64/linux-virt-6.12.8-r0.apk
|
||||||
let url = format!(
|
let url = format!(
|
||||||
"https://dl-cdn.alpinelinux.org/alpine/{}/main/{}/linux-virt-{}.apk",
|
"https://dl-cdn.alpinelinux.org/alpine/{}/main/{}/{}-{}.apk",
|
||||||
branch, arch, version
|
branch, arch, package, version
|
||||||
);
|
);
|
||||||
|
|
||||||
veprintln!("Downloading kernel: {}", url);
|
veprintln!("Downloading kernel: {}", url);
|
||||||
@@ -221,12 +254,12 @@ fn download_alpine_kernel(branch: &str, arch: &str, dest: &Path) -> Result<()> {
|
|||||||
let rt = tokio::runtime::Runtime::new().context("Failed to create Tokio runtime")?;
|
let rt = tokio::runtime::Runtime::new().context("Failed to create Tokio runtime")?;
|
||||||
rt.block_on(download_kernel_async(&url, dest))?;
|
rt.block_on(download_kernel_async(&url, dest))?;
|
||||||
|
|
||||||
// Extract vmlinuz-virt from the APK
|
// Extract the kernel image from the APK, then store it decompressed.
|
||||||
// APK files are gzip-compressed tar archives
|
// Remove the ~45MB APK whatever the outcome — don't leave it behind
|
||||||
|
// in the cache directory on failure.
|
||||||
let temp_apk = dest.with_extension("apk");
|
let temp_apk = dest.with_extension("apk");
|
||||||
// Remove the ~45MB APK whatever the extraction outcome — don't leave
|
let result = extract_kernel_from_apk(&temp_apk, dest, package)
|
||||||
// it behind in the cache directory on failure.
|
.and_then(|()| decompress_kernel_if_gzipped(dest));
|
||||||
let result = extract_kernel_from_apk(&temp_apk, dest);
|
|
||||||
std::fs::remove_file(&temp_apk).ok();
|
std::fs::remove_file(&temp_apk).ok();
|
||||||
result
|
result
|
||||||
}
|
}
|
||||||
@@ -294,8 +327,15 @@ async fn download_kernel_async(url: &str, dest: &Path) -> Result<()> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Extract vmlinuz-virt from an Alpine APK file
|
/// Extract the kernel image from an Alpine APK file
|
||||||
fn extract_kernel_from_apk(apk_path: &Path, dest: &Path) -> Result<()> {
|
fn extract_kernel_from_apk(apk_path: &Path, dest: &Path, package: &str) -> Result<()> {
|
||||||
|
// The kernel binary is named after the flavor: boot/vmlinuz-virt for
|
||||||
|
// linux-virt, boot/vmlinuz-lts for linux-lts
|
||||||
|
let flavor = package
|
||||||
|
.strip_prefix("linux-")
|
||||||
|
.ok_or_else(|| anyhow!("unexpected kernel package name: {}", package))?;
|
||||||
|
let kernel_name = format!("boot/vmlinuz-{}", flavor);
|
||||||
|
|
||||||
veprintln!("Extracting kernel from APK...");
|
veprintln!("Extracting kernel from APK...");
|
||||||
|
|
||||||
let file = std::fs::File::open(apk_path).context("Failed to open APK file")?;
|
let file = std::fs::File::open(apk_path).context("Failed to open APK file")?;
|
||||||
@@ -310,8 +350,7 @@ fn extract_kernel_from_apk(apk_path: &Path, dest: &Path) -> Result<()> {
|
|||||||
|
|
||||||
veprintln!(" APK entry: {}", path_str);
|
veprintln!(" APK entry: {}", path_str);
|
||||||
|
|
||||||
// Look for the kernel file: boot/vmlinuz-virt
|
if path_str == kernel_name || path_str == format!("./{}", kernel_name) {
|
||||||
if path_str == "boot/vmlinuz-virt" || path_str == "./boot/vmlinuz-virt" {
|
|
||||||
// Extract to destination
|
// Extract to destination
|
||||||
entry.unpack(dest).context("Failed to extract kernel")?;
|
entry.unpack(dest).context("Failed to extract kernel")?;
|
||||||
veprintln!(" Extracted kernel to: {}", dest.display());
|
veprintln!(" Extracted kernel to: {}", dest.display());
|
||||||
@@ -319,7 +358,37 @@ fn extract_kernel_from_apk(apk_path: &Path, dest: &Path) -> Result<()> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Err(anyhow!("vmlinuz-virt not found in APK package"))
|
Err(anyhow!("{} not found in APK package", kernel_name))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Decompress the cached kernel if it is gzip-compressed, in place.
|
||||||
|
/// Alpine packages the kernel image gzipped on some architectures (the
|
||||||
|
/// riscv64 and aarch64 vmlinuz are Image.gz), but QEMU's riscv `-kernel`
|
||||||
|
/// loader understands only ELF, uImage and raw images — a gzipped Image
|
||||||
|
/// is loaded verbatim and never boots. Uncompressed formats (x86 bzImage,
|
||||||
|
/// ppc64le ELF, raw Image) are left untouched.
|
||||||
|
fn decompress_kernel_if_gzipped(path: &Path) -> Result<()> {
|
||||||
|
let mut file =
|
||||||
|
std::fs::File::open(path).with_context(|| format!("Failed to open {}", path.display()))?;
|
||||||
|
|
||||||
|
let mut magic = [0u8; 2];
|
||||||
|
let len = file
|
||||||
|
.read(&mut magic)
|
||||||
|
.context("Failed to read kernel magic bytes")?;
|
||||||
|
if len < magic.len() || magic != [0x1f, 0x8b] {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
veprintln!("Kernel is gzip-compressed, decompressing...");
|
||||||
|
file.rewind().context("Failed to rewind kernel file")?;
|
||||||
|
let mut decompressed = Vec::new();
|
||||||
|
flate2::read::MultiGzDecoder::new(file)
|
||||||
|
.read_to_end(&mut decompressed)
|
||||||
|
.context("Failed to decompress kernel")?;
|
||||||
|
|
||||||
|
std::fs::write(path, &decompressed)
|
||||||
|
.with_context(|| format!("Failed to write decompressed kernel to {}", path.display()))?;
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get the path to the cached default kernel for the given architecture.
|
/// Get the path to the cached default kernel for the given architecture.
|
||||||
@@ -365,4 +434,34 @@ mod tests {
|
|||||||
assert_eq!(alpine_kernel_arch("ppc64le"), "ppc64le");
|
assert_eq!(alpine_kernel_arch("ppc64le"), "ppc64le");
|
||||||
assert_eq!(alpine_kernel_arch("s390x"), "s390x");
|
assert_eq!(alpine_kernel_arch("s390x"), "s390x");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_decompress_kernel_if_gzipped() {
|
||||||
|
use flate2::write::GzEncoder;
|
||||||
|
use std::io::Write;
|
||||||
|
|
||||||
|
// A gzip-compressed kernel is decompressed in place
|
||||||
|
let dir = tempfile::tempdir().unwrap();
|
||||||
|
let path = dir.path().join("kernel");
|
||||||
|
let mut encoder = GzEncoder::new(
|
||||||
|
std::fs::File::create(&path).unwrap(),
|
||||||
|
flate2::Compression::default(),
|
||||||
|
);
|
||||||
|
encoder.write_all(b"fake kernel image").unwrap();
|
||||||
|
encoder.finish().unwrap();
|
||||||
|
|
||||||
|
decompress_kernel_if_gzipped(&path).unwrap();
|
||||||
|
assert_eq!(std::fs::read(&path).unwrap(), b"fake kernel image");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_decompress_leaves_plain_kernel_untouched() {
|
||||||
|
let dir = tempfile::tempdir().unwrap();
|
||||||
|
let path = dir.path().join("kernel");
|
||||||
|
// An ELF-ish header that is not the gzip magic
|
||||||
|
std::fs::write(&path, b"\x7fELFfake kernel image").unwrap();
|
||||||
|
|
||||||
|
decompress_kernel_if_gzipped(&path).unwrap();
|
||||||
|
assert_eq!(std::fs::read(&path).unwrap(), b"\x7fELFfake kernel image");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -125,10 +125,16 @@ pub fn launch_qemu(config: QemuConfig) -> Result<()> {
|
|||||||
veprintln!(" Kernel append: {}", kernel_append);
|
veprintln!(" Kernel append: {}", kernel_append);
|
||||||
|
|
||||||
// Build QEMU arguments
|
// Build QEMU arguments
|
||||||
|
// -machine virt is selected explicitly on riscv64 (see machine_for_arch)
|
||||||
// -display none suppresses VGA/BIOS output
|
// -display none suppresses VGA/BIOS output
|
||||||
// -serial mon:stdio connects serial console to terminal with QEMU monitor muxed
|
// -serial mon:stdio connects serial console to terminal with QEMU monitor muxed
|
||||||
// -no-reboot makes QEMU exit when the guest requests poweroff/reboot
|
// -no-reboot makes QEMU exit when the guest requests poweroff/reboot
|
||||||
let mut args = vec![
|
let mut args: Vec<String> = Vec::new();
|
||||||
|
if let Some(machine) = machine_for_arch(&config.arch) {
|
||||||
|
args.push("-machine".to_string());
|
||||||
|
args.push(machine.to_string());
|
||||||
|
}
|
||||||
|
args.extend(vec![
|
||||||
"-kernel".to_string(),
|
"-kernel".to_string(),
|
||||||
config.kernel_path.to_string_lossy().to_string(),
|
config.kernel_path.to_string_lossy().to_string(),
|
||||||
"-initrd".to_string(),
|
"-initrd".to_string(),
|
||||||
@@ -146,13 +152,16 @@ pub fn launch_qemu(config: QemuConfig) -> Result<()> {
|
|||||||
"user,id=net0".to_string(),
|
"user,id=net0".to_string(),
|
||||||
"-device".to_string(),
|
"-device".to_string(),
|
||||||
"virtio-net-pci,netdev=net0".to_string(),
|
"virtio-net-pci,netdev=net0".to_string(),
|
||||||
];
|
]);
|
||||||
|
|
||||||
// Add KVM acceleration if available
|
// Add KVM acceleration if available
|
||||||
if use_kvm {
|
if use_kvm {
|
||||||
args.push("-enable-kvm".to_string());
|
args.push("-enable-kvm".to_string());
|
||||||
args.push("-cpu".to_string());
|
args.push("-cpu".to_string());
|
||||||
args.push("host".to_string());
|
args.push("host".to_string());
|
||||||
|
} else if let Some(cpu) = cpu_for_arch(&config.arch) {
|
||||||
|
args.push("-cpu".to_string());
|
||||||
|
args.push(cpu.to_string());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Execute QEMU
|
// Execute QEMU
|
||||||
@@ -182,6 +191,35 @@ fn qemu_binary_for_arch(arch: &str) -> String {
|
|||||||
format!("qemu-system-{}", arch_enum.qemu_system_name())
|
format!("qemu-system-{}", arch_enum.qemu_system_name())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Get the QEMU machine to select for the target architecture, if any.
|
||||||
|
///
|
||||||
|
/// qemu-system-riscv64's default machine is `spike`, which has neither a
|
||||||
|
/// PCI bus (virtio-net-pci fails with "No 'PCI' bus found") nor a 16550
|
||||||
|
/// UART (console=ttyS0 output goes nowhere); the `virt` board has both,
|
||||||
|
/// plus bundled OpenSBI firmware for -kernel boot.
|
||||||
|
fn machine_for_arch(arch: &str) -> Option<&'static str> {
|
||||||
|
match crate::utils::Arch::from_str(arch) {
|
||||||
|
crate::utils::Arch::Riscv64 => Some("virt"),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the QEMU TCG CPU model to select for the target architecture, when
|
||||||
|
/// the emulator default is not sufficient.
|
||||||
|
///
|
||||||
|
/// Ubuntu builds its riscv64 port against the RVA23 profile (since 25.10),
|
||||||
|
/// so its binaries execute instructions QEMU's default `rv64` CPU does not
|
||||||
|
/// implement and die with SIGILL early in boot. `rva23s64` implements the
|
||||||
|
/// full supervisor profile; baseline rv64gc rootfses (Alpine, older
|
||||||
|
/// Ubuntu) run unchanged on it because profiles are supersets. The named
|
||||||
|
/// profile CPU needs QEMU 10.1+.
|
||||||
|
fn cpu_for_arch(arch: &str) -> Option<&'static str> {
|
||||||
|
match crate::utils::Arch::from_str(arch) {
|
||||||
|
crate::utils::Arch::Riscv64 => Some("rva23s64"),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Get architecture suffix for package names
|
/// Get architecture suffix for package names
|
||||||
fn get_arch_package_suffix(arch: &str) -> &'static str {
|
fn get_arch_package_suffix(arch: &str) -> &'static str {
|
||||||
crate::utils::Arch::from_str(arch).qemu_package_suffix()
|
crate::utils::Arch::from_str(arch).qemu_package_suffix()
|
||||||
@@ -601,6 +639,33 @@ mod tests {
|
|||||||
use super::*;
|
use super::*;
|
||||||
use std::io::Read as _;
|
use std::io::Read as _;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_machine_for_arch() {
|
||||||
|
// riscv64 must select the virt machine explicitly: the
|
||||||
|
// qemu-system-riscv64 default is `spike`, which has no PCI bus and
|
||||||
|
// no 16550 UART
|
||||||
|
assert_eq!(machine_for_arch("riscv64"), Some("virt"));
|
||||||
|
// Other architectures keep their emulator's default machine
|
||||||
|
assert_eq!(machine_for_arch("amd64"), None);
|
||||||
|
assert_eq!(machine_for_arch("x86_64"), None);
|
||||||
|
assert_eq!(machine_for_arch("arm64"), None);
|
||||||
|
assert_eq!(machine_for_arch("aarch64"), None);
|
||||||
|
assert_eq!(machine_for_arch("ppc64le"), None);
|
||||||
|
assert_eq!(machine_for_arch("s390x"), None);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_cpu_for_arch() {
|
||||||
|
// Ubuntu's riscv64 userland needs the RVA23 profile CPU; QEMU's
|
||||||
|
// default `rv64` CPU SIGILLs on it early in boot
|
||||||
|
assert_eq!(cpu_for_arch("riscv64"), Some("rva23s64"));
|
||||||
|
// Other architectures keep their emulator's default CPU
|
||||||
|
assert_eq!(cpu_for_arch("amd64"), None);
|
||||||
|
assert_eq!(cpu_for_arch("x86_64"), None);
|
||||||
|
assert_eq!(cpu_for_arch("arm64"), None);
|
||||||
|
assert_eq!(cpu_for_arch("aarch64"), None);
|
||||||
|
}
|
||||||
|
|
||||||
/// Parse a newc cpio archive into (name, ino, mode, nlink, file_size, data) tuples
|
/// Parse a newc cpio archive into (name, ino, mode, nlink, file_size, data) tuples
|
||||||
fn parse_cpio(archive: Vec<u8>) -> Vec<(String, u32, u32, u32, u32, Vec<u8>)> {
|
fn parse_cpio(archive: Vec<u8>) -> Vec<(String, u32, u32, u32, u32, Vec<u8>)> {
|
||||||
let mut cursor = std::io::Cursor::new(archive);
|
let mut cursor = std::io::Cursor::new(archive);
|
||||||
|
|||||||
Reference in New Issue
Block a user