fix(kernel): fall back to linux-lts where linux-virt is absent
Alpine does not build the linux-virt flavor for riscv64, so --kernel without a path failed there with 'linux-virt package not found'. Scan the APKINDEX once for both flavors and prefer linux-virt, taking linux-lts when the virt flavor is missing. Also store the cached kernel decompressed: riscv64 and aarch64 ship their image gzipped (Image.gz), and QEMU's riscv -kernel loader understands only ELF, uImage and raw images, so the gzipped image hung at boot after the OpenSBI banner.
This commit is contained in:
@@ -36,7 +36,8 @@ pub struct Args {
|
||||
/// Boot with QEMU system emulation (optionally specify kernel path with =PATH, or omit to download default)
|
||||
///
|
||||
/// 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
|
||||
///
|
||||
/// The kernel path must use `=` syntax: with `--kernel ./vmlinuz` the
|
||||
|
||||
+126
-27
@@ -1,22 +1,32 @@
|
||||
//! Default kernel download for QEMU VM mode.
|
||||
//!
|
||||
//! 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)
|
||||
//! - VM-optimized configuration
|
||||
//! - VM-optimized configuration (`linux-virt`)
|
||||
//! - Multi-architecture support
|
||||
//! - 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 anyhow::{anyhow, Context, Result};
|
||||
use indicatif::{ProgressBar, ProgressStyle};
|
||||
use std::io::Read;
|
||||
use std::io::{Read, Seek};
|
||||
use std::path::{Path, PathBuf};
|
||||
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
|
||||
fn alpine_kernel_arch(arch: &str) -> &'static str {
|
||||
match arch {
|
||||
@@ -91,8 +101,11 @@ fn fetch_alpine_branch_from_yaml() -> Result<String> {
|
||||
Err(anyhow!("Could not determine Alpine version from releases"))
|
||||
}
|
||||
|
||||
/// Fetch the latest linux-virt package version from Alpine's package index
|
||||
fn get_linux_virt_version(branch: &str, arch: &str) -> Result<String> {
|
||||
/// Find the newest kernel package among `KERNEL_PACKAGES` in Alpine's
|
||||
/// 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
|
||||
let url = format!(
|
||||
"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());
|
||||
|
||||
// Parse the APKINDEX to find linux-virt
|
||||
// Parse the APKINDEX to find the best kernel package
|
||||
// Format:
|
||||
// P:linux-virt
|
||||
// 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;
|
||||
// (preference index, package, version)
|
||||
let mut best: Option<(usize, &'static str, String)> = None;
|
||||
|
||||
for line in contents.lines() {
|
||||
if let Some(name) = line.strip_prefix("P:") {
|
||||
pkg_name = Some(name.trim().to_string());
|
||||
} else if let Some(version) = line.strip_prefix("V:") {
|
||||
if pkg_name.as_deref() == Some("linux-virt") {
|
||||
return Ok(version.trim().to_string());
|
||||
if let Some(name) = &pkg_name {
|
||||
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
|
||||
return Err(anyhow!("linux-virt package not found in APKINDEX. Available packages may vary by architecture."));
|
||||
match best {
|
||||
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<()> {
|
||||
let version = get_linux_virt_version(branch, arch)?;
|
||||
veprintln!("Found linux-virt version: {}", version);
|
||||
let (package, version) = find_kernel_package(branch, arch)?;
|
||||
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
|
||||
let url = format!(
|
||||
"https://dl-cdn.alpinelinux.org/alpine/{}/main/{}/linux-virt-{}.apk",
|
||||
branch, arch, version
|
||||
"https://dl-cdn.alpinelinux.org/alpine/{}/main/{}/{}-{}.apk",
|
||||
branch, arch, package, version
|
||||
);
|
||||
|
||||
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")?;
|
||||
rt.block_on(download_kernel_async(&url, dest))?;
|
||||
|
||||
// Extract vmlinuz-virt from the APK
|
||||
// APK files are gzip-compressed tar archives
|
||||
// Extract the kernel image from the APK, then store it decompressed.
|
||||
// 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");
|
||||
// Remove the ~45MB APK whatever the extraction outcome — don't leave
|
||||
// it behind in the cache directory on failure.
|
||||
let result = extract_kernel_from_apk(&temp_apk, dest);
|
||||
let result = extract_kernel_from_apk(&temp_apk, dest, package)
|
||||
.and_then(|()| decompress_kernel_if_gzipped(dest));
|
||||
std::fs::remove_file(&temp_apk).ok();
|
||||
result
|
||||
}
|
||||
@@ -294,8 +327,15 @@ async fn download_kernel_async(url: &str, dest: &Path) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Extract vmlinuz-virt from an Alpine APK file
|
||||
fn extract_kernel_from_apk(apk_path: &Path, dest: &Path) -> Result<()> {
|
||||
/// Extract the kernel image from an Alpine APK file
|
||||
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...");
|
||||
|
||||
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);
|
||||
|
||||
// Look for the kernel file: boot/vmlinuz-virt
|
||||
if path_str == "boot/vmlinuz-virt" || path_str == "./boot/vmlinuz-virt" {
|
||||
if path_str == kernel_name || path_str == format!("./{}", kernel_name) {
|
||||
// Extract to destination
|
||||
entry.unpack(dest).context("Failed to extract kernel")?;
|
||||
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.
|
||||
@@ -365,4 +434,34 @@ mod tests {
|
||||
assert_eq!(alpine_kernel_arch("ppc64le"), "ppc64le");
|
||||
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");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user