Compare commits
2 Commits
1d2031b3ca
...
e1d69eaed6
| Author | SHA1 | Date | |
|---|---|---|---|
| e1d69eaed6 | |||
| 3188566b6e |
@@ -38,7 +38,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 using specified kernel |
|
| `--kernel [PATH]` | Boot with QEMU system emulation. Downloads Alpine's `linux-virt` kernel if no path provided |
|
||||||
| `-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
|
||||||
@@ -59,11 +59,14 @@ ecr --arch arm64 alpine -- uname -m
|
|||||||
# Always pull a fresh image
|
# Always pull a fresh image
|
||||||
ecr --no-cache fedora
|
ecr --no-cache fedora
|
||||||
|
|
||||||
# Boot with QEMU system emulation (requires qemu-system-<arch>)
|
# Boot with QEMU system emulation (auto-downloads default kernel)
|
||||||
|
ecr --kernel alpine
|
||||||
|
|
||||||
|
# Boot with your own kernel
|
||||||
ecr --kernel /boot/vmlinuz ubuntu
|
ecr --kernel /boot/vmlinuz ubuntu
|
||||||
|
|
||||||
# Boot with custom memory
|
# Boot with custom memory
|
||||||
ecr --kernel /boot/vmlinuz --memory 4G alpine
|
ecr --kernel --memory 4G alpine
|
||||||
```
|
```
|
||||||
|
|
||||||
## QEMU System Mode
|
## QEMU System Mode
|
||||||
@@ -71,18 +74,23 @@ ecr --kernel /boot/vmlinuz --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
|
||||||
ecr --kernel /boot/vmlinuz alpine
|
# Auto-download Alpine's linux-virt kernel (recommended)
|
||||||
|
ecr --kernel alpine
|
||||||
|
|
||||||
|
# Use your own kernel
|
||||||
|
ecr --kernel /boot/vmlinuz ubuntu
|
||||||
```
|
```
|
||||||
|
|
||||||
This mode:
|
This mode:
|
||||||
- Creates a gzipped CPIO initramfs from the rootfs
|
- Creates a gzipped CPIO initramfs from the rootfs
|
||||||
- Boots QEMU with your kernel
|
- Boots QEMU with your kernel (or auto-downloads Alpine's `linux-virt` kernel)
|
||||||
- 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/`
|
||||||
|
|
||||||
Requirements:
|
Requirements:
|
||||||
- `qemu-system-<arch>` installed
|
- `qemu-system-<arch>` installed
|
||||||
- Kernel with serial console support
|
- For custom kernels: kernel must have serial console support
|
||||||
|
|
||||||
## Supported distributions
|
## Supported distributions
|
||||||
|
|
||||||
|
|||||||
+3
-3
@@ -170,9 +170,9 @@ mod tests {
|
|||||||
assert_eq!(env.len(), 5);
|
assert_eq!(env.len(), 5);
|
||||||
|
|
||||||
// Should NOT have any host-specific variables
|
// Should NOT have any host-specific variables
|
||||||
assert!(env.get("LANG").is_none());
|
assert!(!env.contains_key("LANG"));
|
||||||
assert!(env.get("DISPLAY").is_none());
|
assert!(!env.contains_key("DISPLAY"));
|
||||||
assert!(env.get("PWD").is_none());
|
assert!(!env.contains_key("PWD"));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
+7
-3
@@ -33,9 +33,13 @@ pub struct Args {
|
|||||||
#[arg(short = 'v', long)]
|
#[arg(short = 'v', long)]
|
||||||
pub verbose: bool,
|
pub verbose: bool,
|
||||||
|
|
||||||
/// Boot with QEMU system emulation using specified kernel (extracts rootfs as disk image)
|
/// Boot with QEMU system emulation (optionally specify kernel path, or omit to download default)
|
||||||
#[arg(long, value_name = "KERNEL_PATH")]
|
///
|
||||||
pub kernel: Option<PathBuf>,
|
/// Examples:
|
||||||
|
/// --kernel Download and use the default Alpine linux-virt kernel
|
||||||
|
/// --kernel ./vmlinuz Use a specific kernel file
|
||||||
|
#[arg(long, value_name = "KERNEL_PATH", num_args = 0..=1)]
|
||||||
|
pub kernel: Option<Option<PathBuf>>,
|
||||||
|
|
||||||
/// Memory size for QEMU VM (only used with --kernel, e.g., 512M, 2G)
|
/// Memory size for QEMU VM (only used with --kernel, e.g., 512M, 2G)
|
||||||
#[arg(short = 'm', long, default_value = "2G", value_name = "SIZE")]
|
#[arg(short = 'm', long, default_value = "2G", value_name = "SIZE")]
|
||||||
|
|||||||
+368
@@ -0,0 +1,368 @@
|
|||||||
|
//! 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:
|
||||||
|
//!
|
||||||
|
//! - Small size (~10-15MB compressed)
|
||||||
|
//! - VM-optimized configuration
|
||||||
|
//! - Multi-architecture support
|
||||||
|
//! - Simple direct download URLs
|
||||||
|
//!
|
||||||
|
//! The kernel is cached in the same cache directory as rootfs images.
|
||||||
|
|
||||||
|
use crate::veprintln;
|
||||||
|
use anyhow::{anyhow, Context, Result};
|
||||||
|
use indicatif::{ProgressBar, ProgressStyle};
|
||||||
|
use std::io::Read;
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
/// Alpine architecture mapping for kernel packages
|
||||||
|
fn alpine_kernel_arch(arch: &str) -> &'static str {
|
||||||
|
match arch {
|
||||||
|
"amd64" | "x86_64" => "x86_64",
|
||||||
|
"arm64" | "aarch64" => "aarch64",
|
||||||
|
"armhf" | "armv7l" | "arm" => "armv7",
|
||||||
|
"riscv64" => "riscv64",
|
||||||
|
"ppc64le" => "ppc64le",
|
||||||
|
"s390x" => "s390x",
|
||||||
|
"x86" | "i386" | "i686" => "x86",
|
||||||
|
_ => "x86_64",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the Alpine version branch for kernel downloads
|
||||||
|
/// We use the latest stable branch
|
||||||
|
fn get_alpine_branch() -> Result<String> {
|
||||||
|
// Fetch the latest-stable branch from Alpine CDN
|
||||||
|
// The URL redirects to the current stable version
|
||||||
|
let client = reqwest::blocking::Client::builder()
|
||||||
|
.timeout(Duration::from_secs(30))
|
||||||
|
.build()
|
||||||
|
.context("Failed to create HTTP client")?;
|
||||||
|
|
||||||
|
let response = client
|
||||||
|
.head("https://dl-cdn.alpinelinux.org/alpine/latest-stable/main/")
|
||||||
|
.send()
|
||||||
|
.context("Failed to check Alpine latest-stable")?;
|
||||||
|
|
||||||
|
// The final URL after redirect contains the version, e.g.:
|
||||||
|
// https://dl-cdn.alpinelinux.org/alpine/v3.23/main/
|
||||||
|
if let Some(final_url) = response
|
||||||
|
.url()
|
||||||
|
.as_str()
|
||||||
|
.strip_prefix("https://dl-cdn.alpinelinux.org/alpine/")
|
||||||
|
{
|
||||||
|
if let Some(branch) = final_url.split('/').next() {
|
||||||
|
return Ok(branch.to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback: parse from the releases YAML
|
||||||
|
fetch_alpine_branch_from_yaml()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn fetch_alpine_branch_from_yaml() -> Result<String> {
|
||||||
|
#[derive(serde::Deserialize)]
|
||||||
|
struct AlpineRelease {
|
||||||
|
version: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
let url =
|
||||||
|
"https://dl-cdn.alpinelinux.org/alpine/latest-stable/releases/x86_64/latest-releases.yaml";
|
||||||
|
let text = reqwest::blocking::get(url)
|
||||||
|
.context("Failed to fetch Alpine latest-releases.yaml")?
|
||||||
|
.text()
|
||||||
|
.context("Failed to read Alpine latest-releases.yaml")?;
|
||||||
|
|
||||||
|
let releases: Vec<AlpineRelease> =
|
||||||
|
serde_yaml::from_str(&text).context("Failed to parse Alpine latest-releases.yaml")?;
|
||||||
|
|
||||||
|
if let Some(release) = releases.first() {
|
||||||
|
if let Some(version) = &release.version {
|
||||||
|
// version is like "3.23.0", we want "v3.23"
|
||||||
|
let parts: Vec<&str> = version.split('.').collect();
|
||||||
|
if parts.len() >= 2 {
|
||||||
|
return Ok(format!("v{}.{}", parts[0], parts[1]));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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> {
|
||||||
|
// Alpine package index URL
|
||||||
|
let url = format!(
|
||||||
|
"https://dl-cdn.alpinelinux.org/alpine/{}/main/{}/APKINDEX.tar.gz",
|
||||||
|
branch, arch
|
||||||
|
);
|
||||||
|
|
||||||
|
veprintln!("Fetching package index: {}", url);
|
||||||
|
|
||||||
|
let client = reqwest::blocking::Client::builder()
|
||||||
|
.timeout(Duration::from_secs(30))
|
||||||
|
.build()
|
||||||
|
.context("Failed to create HTTP client")?;
|
||||||
|
|
||||||
|
let response = client
|
||||||
|
.get(&url)
|
||||||
|
.send()
|
||||||
|
.context("Failed to fetch Alpine APKINDEX")?;
|
||||||
|
|
||||||
|
if !response.status().is_success() {
|
||||||
|
return Err(anyhow!(
|
||||||
|
"Failed to fetch APKINDEX: HTTP {}",
|
||||||
|
response.status()
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
let bytes = response.bytes().context("Failed to read APKINDEX")?;
|
||||||
|
|
||||||
|
// Extract APKINDEX from the tar.gz
|
||||||
|
// We need to own the bytes to avoid lifetime issues
|
||||||
|
let bytes_owned = bytes.to_vec();
|
||||||
|
veprintln!(" Downloaded {} bytes", bytes_owned.len());
|
||||||
|
|
||||||
|
// First decompress gzip to memory, then parse tar
|
||||||
|
// Note: Alpine's APKINDEX.tar.gz uses concatenated gzip members (multi-member gzip)
|
||||||
|
// flate2::read::GzDecoder only reads the first member, so we use MultiGzDecoder
|
||||||
|
let cursor = std::io::Cursor::new(&bytes_owned);
|
||||||
|
let mut gz_decoder = flate2::read::MultiGzDecoder::new(cursor);
|
||||||
|
let mut decompressed = Vec::new();
|
||||||
|
gz_decoder
|
||||||
|
.read_to_end(&mut decompressed)
|
||||||
|
.context("Failed to decompress gzip")?;
|
||||||
|
|
||||||
|
veprintln!(" Decompressed {} bytes", decompressed.len());
|
||||||
|
|
||||||
|
let tar_cursor = std::io::Cursor::new(decompressed);
|
||||||
|
let mut archive = tar::Archive::new(tar_cursor);
|
||||||
|
|
||||||
|
// Iterate through entries directly
|
||||||
|
let entries_iter = archive
|
||||||
|
.entries()
|
||||||
|
.context("Failed to read APKINDEX tar entries")?;
|
||||||
|
let mut entry_count = 0;
|
||||||
|
|
||||||
|
for entry_result in entries_iter {
|
||||||
|
entry_count += 1;
|
||||||
|
let mut entry = match entry_result {
|
||||||
|
Ok(e) => e,
|
||||||
|
Err(e) => {
|
||||||
|
veprintln!(
|
||||||
|
" Warning: failed to read tar entry #{}: {}",
|
||||||
|
entry_count,
|
||||||
|
e
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let path = entry.path().context("Failed to get entry path")?;
|
||||||
|
let path_str = path.to_string_lossy();
|
||||||
|
|
||||||
|
veprintln!(" Entry #{}: {}", entry_count, path_str);
|
||||||
|
|
||||||
|
if path_str == "APKINDEX" {
|
||||||
|
let mut contents = String::new();
|
||||||
|
entry
|
||||||
|
.read_to_string(&mut contents)
|
||||||
|
.context("Failed to read APKINDEX contents")?;
|
||||||
|
|
||||||
|
veprintln!(" APKINDEX size: {} bytes", contents.len());
|
||||||
|
|
||||||
|
// Parse the APKINDEX to find linux-virt
|
||||||
|
// Format:
|
||||||
|
// P:linux-virt
|
||||||
|
// V:6.12.8-r0
|
||||||
|
// ...
|
||||||
|
let mut pkg_name: Option<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 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."));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
veprintln!(" Total entries processed: {}", entry_count);
|
||||||
|
|
||||||
|
Err(anyhow!(
|
||||||
|
"APKINDEX file not found in tar.gz archive (processed {} entries)",
|
||||||
|
entry_count
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Download and extract the linux-virt kernel from Alpine's 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);
|
||||||
|
|
||||||
|
// Construct the download URL for the linux-virt .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
|
||||||
|
);
|
||||||
|
|
||||||
|
veprintln!("Downloading kernel: {}", url);
|
||||||
|
|
||||||
|
// Use async download via the existing download module pattern
|
||||||
|
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
|
||||||
|
let temp_apk = dest.with_extension("apk");
|
||||||
|
extract_kernel_from_apk(&temp_apk, dest)?;
|
||||||
|
|
||||||
|
// Clean up the APK
|
||||||
|
std::fs::remove_file(&temp_apk).ok();
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn download_kernel_async(url: &str, dest: &Path) -> Result<()> {
|
||||||
|
use futures_util::StreamExt;
|
||||||
|
let client = reqwest::Client::builder()
|
||||||
|
.timeout(Duration::from_secs(300))
|
||||||
|
.build()
|
||||||
|
.context("Failed to create HTTP client")?;
|
||||||
|
|
||||||
|
let response = client
|
||||||
|
.get(url)
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.context("Failed to start kernel download")?;
|
||||||
|
|
||||||
|
if !response.status().is_success() {
|
||||||
|
return Err(anyhow!(
|
||||||
|
"Kernel download failed: HTTP {}",
|
||||||
|
response.status()
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
let total_size = response.content_length().unwrap_or(0);
|
||||||
|
|
||||||
|
// Setup progress bar
|
||||||
|
let pb = ProgressBar::new(total_size);
|
||||||
|
pb.set_style(
|
||||||
|
ProgressStyle::default_bar()
|
||||||
|
.template("{spinner:.green} [{elapsed_precise}] [{bar:40.cyan/blue}] {bytes}/{total_bytes} ({eta})")
|
||||||
|
.unwrap()
|
||||||
|
.progress_chars("#>-"),
|
||||||
|
);
|
||||||
|
|
||||||
|
let temp_apk = dest.with_extension("apk.partial");
|
||||||
|
let mut file = tokio::fs::File::create(&temp_apk)
|
||||||
|
.await
|
||||||
|
.context("Failed to create temp APK file")?;
|
||||||
|
|
||||||
|
let mut downloaded: u64 = 0;
|
||||||
|
let mut stream = response.bytes_stream();
|
||||||
|
|
||||||
|
while let Some(chunk) = stream.next().await {
|
||||||
|
let chunk = chunk.context("Failed to read chunk")?;
|
||||||
|
tokio::io::AsyncWriteExt::write_all(&mut file, &chunk)
|
||||||
|
.await
|
||||||
|
.context("Failed to write chunk")?;
|
||||||
|
downloaded += chunk.len() as u64;
|
||||||
|
pb.set_position(downloaded);
|
||||||
|
}
|
||||||
|
|
||||||
|
tokio::io::AsyncWriteExt::flush(&mut file)
|
||||||
|
.await
|
||||||
|
.context("Failed to flush file")?;
|
||||||
|
|
||||||
|
pb.finish_with_message("Download complete");
|
||||||
|
|
||||||
|
// Rename to final name
|
||||||
|
let final_apk = dest.with_extension("apk");
|
||||||
|
tokio::fs::rename(&temp_apk, &final_apk)
|
||||||
|
.await
|
||||||
|
.context("Failed to rename temp file")?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Extract vmlinuz-virt from an Alpine APK file
|
||||||
|
fn extract_kernel_from_apk(apk_path: &Path, dest: &Path) -> Result<()> {
|
||||||
|
veprintln!("Extracting kernel from APK...");
|
||||||
|
|
||||||
|
let file = std::fs::File::open(apk_path).context("Failed to open APK file")?;
|
||||||
|
// Use MultiGzDecoder because Alpine APKs have concatenated gzip members
|
||||||
|
let gz_decoder = flate2::read::MultiGzDecoder::new(file);
|
||||||
|
let mut archive = tar::Archive::new(gz_decoder);
|
||||||
|
|
||||||
|
for entry in archive.entries().context("Failed to read APK entries")? {
|
||||||
|
let mut entry = entry.context("Failed to read tar entry")?;
|
||||||
|
let path = entry.path().context("Failed to get entry path")?;
|
||||||
|
let path_str = path.to_string_lossy();
|
||||||
|
|
||||||
|
veprintln!(" APK entry: {}", path_str);
|
||||||
|
|
||||||
|
// Look for the kernel file: boot/vmlinuz-virt
|
||||||
|
if path_str == "boot/vmlinuz-virt" || path_str == "./boot/vmlinuz-virt" {
|
||||||
|
// Extract to destination
|
||||||
|
entry.unpack(dest).context("Failed to extract kernel")?;
|
||||||
|
veprintln!(" Extracted kernel to: {}", dest.display());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Err(anyhow!("vmlinuz-virt not found in APK package"))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the path to the cached default kernel for the given architecture.
|
||||||
|
/// Downloads and caches it if not present.
|
||||||
|
pub fn get_default_kernel(cache_dir: &Path, arch: &str) -> Result<PathBuf> {
|
||||||
|
let alpine_arch = alpine_kernel_arch(arch);
|
||||||
|
|
||||||
|
// Cache filename includes architecture
|
||||||
|
let kernel_filename = format!("ecr-default-kernel-{}.vmlinuz", alpine_arch);
|
||||||
|
let kernel_path = cache_dir.join(&kernel_filename);
|
||||||
|
|
||||||
|
// Check if already cached
|
||||||
|
if kernel_path.exists() {
|
||||||
|
veprintln!("Using cached default kernel: {}", kernel_path.display());
|
||||||
|
return Ok(kernel_path);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create cache directory if needed
|
||||||
|
std::fs::create_dir_all(cache_dir).context("Failed to create cache directory")?;
|
||||||
|
|
||||||
|
// Determine Alpine branch
|
||||||
|
let branch = get_alpine_branch()?;
|
||||||
|
veprintln!("Using Alpine branch: {}", branch);
|
||||||
|
|
||||||
|
// Download and extract the kernel
|
||||||
|
download_alpine_kernel(&branch, alpine_arch, &kernel_path)?;
|
||||||
|
|
||||||
|
Ok(kernel_path)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_alpine_kernel_arch() {
|
||||||
|
assert_eq!(alpine_kernel_arch("amd64"), "x86_64");
|
||||||
|
assert_eq!(alpine_kernel_arch("x86_64"), "x86_64");
|
||||||
|
assert_eq!(alpine_kernel_arch("arm64"), "aarch64");
|
||||||
|
assert_eq!(alpine_kernel_arch("aarch64"), "aarch64");
|
||||||
|
assert_eq!(alpine_kernel_arch("armhf"), "armv7");
|
||||||
|
assert_eq!(alpine_kernel_arch("riscv64"), "riscv64");
|
||||||
|
assert_eq!(alpine_kernel_arch("ppc64le"), "ppc64le");
|
||||||
|
assert_eq!(alpine_kernel_arch("s390x"), "s390x");
|
||||||
|
}
|
||||||
|
}
|
||||||
+17
-3
@@ -4,6 +4,7 @@ mod config;
|
|||||||
mod distro;
|
mod distro;
|
||||||
mod download;
|
mod download;
|
||||||
mod extract;
|
mod extract;
|
||||||
|
mod kernel;
|
||||||
mod mount;
|
mod mount;
|
||||||
mod namespace;
|
mod namespace;
|
||||||
mod qemu;
|
mod qemu;
|
||||||
@@ -129,9 +130,22 @@ fn main() -> Result<()> {
|
|||||||
extract_tarball(&cache_path, &rootfs)?;
|
extract_tarball(&cache_path, &rootfs)?;
|
||||||
|
|
||||||
// Branch based on --kernel flag
|
// Branch based on --kernel flag
|
||||||
if let Some(kernel_path) = &args.kernel {
|
// Option<Option<PathBuf>>:
|
||||||
|
// None -> --kernel not specified, use namespace mode
|
||||||
|
// Some(None) -> --kernel without path, download default kernel
|
||||||
|
// Some(Some(path)) -> --kernel /path/to/vmlinuz, use provided kernel
|
||||||
|
if let Some(kernel_opt) = &args.kernel {
|
||||||
// QEMU system mode
|
// QEMU system mode
|
||||||
veprintln!("QEMU mode: booting with kernel {}", kernel_path.display());
|
let kernel_path = match kernel_opt {
|
||||||
|
Some(path) => {
|
||||||
|
veprintln!("QEMU mode: using provided kernel {}", path.display());
|
||||||
|
path.clone()
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
veprintln!("QEMU mode: downloading default kernel...");
|
||||||
|
kernel::get_default_kernel(&cache_dir, &arch)?
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
let command = if args.command.is_empty() {
|
let command = if args.command.is_empty() {
|
||||||
None
|
None
|
||||||
@@ -140,7 +154,7 @@ fn main() -> Result<()> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
let result = qemu_vm::launch_qemu(qemu_vm::QemuConfig {
|
let result = qemu_vm::launch_qemu(qemu_vm::QemuConfig {
|
||||||
kernel_path: kernel_path.clone(),
|
kernel_path,
|
||||||
rootfs_path: rootfs,
|
rootfs_path: rootfs,
|
||||||
memory: args.memory.clone(),
|
memory: args.memory.clone(),
|
||||||
arch: arch.clone(),
|
arch: arch.clone(),
|
||||||
|
|||||||
+11
-5
@@ -88,18 +88,24 @@ pub fn launch_qemu(config: QemuConfig) -> Result<()> {
|
|||||||
// Build kernel command line
|
// Build kernel command line
|
||||||
// For initramfs boot, use rdinit= instead of init=
|
// For initramfs boot, use rdinit= instead of init=
|
||||||
// No root= needed as initramfs becomes the rootfs
|
// No root= needed as initramfs becomes the rootfs
|
||||||
// 'quiet' suppresses kernel log messages for a cleaner console
|
// 'quiet' suppresses kernel log messages for a cleaner console (removed with -v)
|
||||||
// The init script (added to initramfs) handles hostname, shell, and poweroff
|
// The init script (added to initramfs) handles hostname, shell, and poweroff
|
||||||
|
let quiet_flag = if crate::verbose::is_verbose() {
|
||||||
|
""
|
||||||
|
} else {
|
||||||
|
" quiet"
|
||||||
|
};
|
||||||
|
|
||||||
let kernel_append = if let Some(ref cmd) = config.command {
|
let kernel_append = if let Some(ref cmd) = config.command {
|
||||||
let cmd_str = cmd.join(" ");
|
let cmd_str = cmd.join(" ");
|
||||||
format!(
|
format!(
|
||||||
"console=ttyS0 quiet ECR_SHELL={} ECR_CMD=\"{}\" ECR_HOSTNAME={}",
|
"console=ttyS0{} ECR_SHELL={} ECR_CMD=\"{}\" ECR_HOSTNAME={}",
|
||||||
shell, cmd_str, hostname
|
quiet_flag, shell, cmd_str, hostname
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
format!(
|
format!(
|
||||||
"console=ttyS0 quiet ECR_SHELL={} ECR_HOSTNAME={}",
|
"console=ttyS0{} ECR_SHELL={} ECR_HOSTNAME={}",
|
||||||
shell, hostname
|
quiet_flag, shell, hostname
|
||||||
)
|
)
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
+4
-11
@@ -250,10 +250,10 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_arch_canonical_name() {
|
fn test_arch_alpine_name() {
|
||||||
assert_eq!(Arch::Amd64.canonical_name(), "x86_64");
|
assert_eq!(Arch::Amd64.alpine_name(), "x86_64");
|
||||||
assert_eq!(Arch::Arm64.canonical_name(), "aarch64");
|
assert_eq!(Arch::Arm64.alpine_name(), "aarch64");
|
||||||
assert_eq!(Arch::Armhf.canonical_name(), "armv7l");
|
assert_eq!(Arch::Armhf.alpine_name(), "armv7");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -263,13 +263,6 @@ mod tests {
|
|||||||
assert_eq!(Arch::Armhf.oci_name(), "arm");
|
assert_eq!(Arch::Armhf.oci_name(), "arm");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_arch_alpine_name() {
|
|
||||||
assert_eq!(Arch::Amd64.alpine_name(), "x86_64");
|
|
||||||
assert_eq!(Arch::Arm64.alpine_name(), "aarch64");
|
|
||||||
assert_eq!(Arch::Armhf.alpine_name(), "armv7");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_validate_memory_string_valid() {
|
fn test_validate_memory_string_valid() {
|
||||||
assert!(validate_memory_string("512M").is_ok());
|
assert!(validate_memory_string("512M").is_ok());
|
||||||
|
|||||||
Reference in New Issue
Block a user