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)
|
/// 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");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user