refactor(verbose): replace the global flag and veprintln! with log

Diagnostics now go through the log facade: the library no longer
decides where output goes, and consumers plug in their own sink (the
CLI installs a stderr logger printing warnings always and ecr debug
diagnostics under -v; dependency records stay filtered out).  The
kernel 'quiet' flag moves from the global verbosity flag to an
explicit QemuConfig field.
This commit is contained in:
2026-10-03 00:13:34 +02:00
parent 031c6ba113
commit fe0b99ee6b
16 changed files with 129 additions and 106 deletions
Generated
+2
View File
@@ -307,6 +307,7 @@ dependencies = [
"flate2", "flate2",
"futures-util", "futures-util",
"libc", "libc",
"log",
"nix", "nix",
"reqwest", "reqwest",
"serde", "serde",
@@ -329,6 +330,7 @@ dependencies = [
"clap", "clap",
"ecr", "ecr",
"indicatif", "indicatif",
"log",
] ]
[[package]] [[package]]
+3
View File
@@ -22,5 +22,8 @@ clap = { version = "4", features = ["derive", "env"] }
# Error handling # Error handling
anyhow = "1" anyhow = "1"
# Logging facade; the stderr sink lives in src/logger.rs
log = { version = "0.4", features = ["std"] }
# Progress bar rendering of library progress events # Progress bar rendering of library progress events
indicatif = "0.18" indicatif = "0.18"
+47
View File
@@ -0,0 +1,47 @@
//! stderr logging sink: `Warn`/`Error` records always, `Debug`
//! diagnostics only with -v.
use log::{Level, LevelFilter, Log, Metadata, Record};
struct CliLogger {
max: LevelFilter,
}
impl Log for CliLogger {
fn enabled(&self, metadata: &Metadata) -> bool {
if metadata.level() > self.max {
return false;
}
match metadata.level() {
Level::Warn | Level::Error => true,
// Dependencies (hyper, rustls, …) share the log facade and are
// far noisier than we are; -v follows only our own diagnostics.
_ => metadata.target().starts_with("ecr"),
}
}
fn log(&self, record: &Record) {
if !self.enabled(record.metadata()) {
return;
}
match record.level() {
Level::Error => eprintln!("Error: {}", record.args()),
Level::Warn => eprintln!("Warning: {}", record.args()),
_ => eprintln!("{}", record.args()),
}
}
fn flush(&self) {}
}
/// Install the CLI logger: warnings and errors always, `ecr` debug
/// diagnostics only when `verbose` (-v).
pub fn init(verbose: bool) {
let max = if verbose {
LevelFilter::Debug
} else {
LevelFilter::Warn
};
log::set_boxed_logger(Box::new(CliLogger { max })).ok();
log::set_max_level(max);
}
+9 -7
View File
@@ -1,4 +1,5 @@
mod cli; mod cli;
mod logger;
mod progress; mod progress;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
@@ -11,14 +12,14 @@ use ecr::chroot;
use ecr::config::Config; use ecr::config::Config;
use ecr::exec::ExecOptions; use ecr::exec::ExecOptions;
use ecr::mount::BindTarget; use ecr::mount::BindTarget;
use ecr::{kernel, qemu, qemu_vm, utils, veprintln, verbose, PrepareRequest, RootfsCache}; use ecr::{kernel, qemu, qemu_vm, utils, PrepareRequest, RootfsCache};
use progress::TermProgress; use progress::TermProgress;
fn main() -> Result<()> { fn main() -> Result<()> {
let args = Args::parse(); let args = Args::parse();
// Initialise verbosity before anything else so all downstream code can use veprintln!. // Install the logger before anything else so all downstream code can log.
verbose::set(args.verbose); logger::init(args.verbose);
// Render library progress (bars/spinners) on the terminal // Render library progress (bars/spinners) on the terminal
let progress = TermProgress::new(); let progress = TermProgress::new();
@@ -63,11 +64,11 @@ fn main() -> Result<()> {
// QEMU system mode // QEMU system mode
let kernel_path = match kernel_opt { let kernel_path = match kernel_opt {
Some(path) => { Some(path) => {
veprintln!("QEMU mode: using provided kernel {}", path.display()); log::debug!("QEMU mode: using provided kernel {}", path.display());
path.clone() path.clone()
} }
None => { None => {
veprintln!("QEMU mode: downloading default kernel..."); log::debug!("QEMU mode: downloading default kernel...");
kernel::get_default_kernel(cache.dir(), &arch, &progress)? kernel::get_default_kernel(cache.dir(), &arch, &progress)?
} }
}; };
@@ -83,6 +84,7 @@ fn main() -> Result<()> {
kernel_path, kernel_path,
rootfs_path: rootfs.dir().to_path_buf(), rootfs_path: rootfs.dir().to_path_buf(),
memory: args.memory.clone(), memory: args.memory.clone(),
quiet: !args.verbose,
arch: arch.clone(), arch: arch.clone(),
command, command,
}, },
@@ -91,7 +93,7 @@ fn main() -> Result<()> {
// Cleanup happens automatically via tempfile // Cleanup happens automatically via tempfile
if result.is_ok() { if result.is_ok() {
veprintln!("Cleanup complete."); log::debug!("Cleanup complete.");
} }
result result
@@ -123,7 +125,7 @@ fn namespace_mode(args: &Args, rootfs: &Path, config: &Config, arch: &str) -> Re
// Cleanup happens automatically via tempfile // Cleanup happens automatically via tempfile
if exit_code.is_ok() { if exit_code.is_ok() {
veprintln!("Cleanup complete."); log::debug!("Cleanup complete.");
} }
exit_code exit_code
+3
View File
@@ -21,6 +21,9 @@ flate2 = "1"
xz2 = "0.1" xz2 = "0.1"
zstd = "0.13" zstd = "0.13"
# Logging facade (the CLI installs the sink)
log = "0.4"
# Unix syscall bindings # Unix syscall bindings
nix = { version = "0.31", features = ["fs", "mount", "sched", "signal", "user", "process", "hostname"] } nix = { version = "0.31", features = ["fs", "mount", "sched", "signal", "user", "process", "hostname"] }
+3 -4
View File
@@ -1,4 +1,3 @@
use crate::veprintln;
use anyhow::{anyhow, Context, Result}; use anyhow::{anyhow, Context, Result};
use nix::unistd::{chroot, execve}; use nix::unistd::{chroot, execve};
use std::os::unix::ffi::OsStrExt; use std::os::unix::ffi::OsStrExt;
@@ -39,7 +38,7 @@ pub fn run_chroot(
) -> Result<()> { ) -> Result<()> {
// Set hostname in UTS namespace // Set hostname in UTS namespace
if let Err(e) = crate::namespace::set_hostname("chroot") { if let Err(e) = crate::namespace::set_hostname("chroot") {
eprintln!("Warning: Failed to set hostname: {}", e); log::warn!("Failed to set hostname: {}", e);
} }
// Detect shell before chroot (we're still outside) // Detect shell before chroot (we're still outside)
@@ -90,8 +89,8 @@ pub fn run_chroot(
})?; })?;
// Print welcome message // Print welcome message
veprintln!("Entering chroot at {}", rootfs.display()); log::debug!("Entering chroot at {}", rootfs.display());
veprintln!("Working directory: {}", working_dir.display()); log::debug!("Working directory: {}", working_dir.display());
// execve(2) does not search PATH: resolve the program through the // execve(2) does not search PATH: resolve the program through the
// caller-composed environment like execvp(3) would. // caller-composed environment like execvp(3) would.
+13 -14
View File
@@ -1,5 +1,4 @@
use crate::progress::{Progress, Unit}; use crate::progress::{Progress, Unit};
use crate::veprintln;
use anyhow::{anyhow, Context, Result}; use anyhow::{anyhow, Context, Result};
use futures_util::StreamExt; use futures_util::StreamExt;
use reqwest::Client; use reqwest::Client;
@@ -88,7 +87,7 @@ pub fn download_image(
match source { match source {
ImageSource::DirectTarball { distro, version } => { ImageSource::DirectTarball { distro, version } => {
let url = crate::distro::resolve_distro_url(distro, version.as_deref(), arch)?; let url = crate::distro::resolve_distro_url(distro, version.as_deref(), arch)?;
veprintln!("Resolved URL: {}", url); log::debug!("Resolved URL: {}", url);
if !url.starts_with("http://") && !url.starts_with("https://") { if !url.starts_with("http://") && !url.starts_with("https://") {
return Err(anyhow!("Unsupported URL scheme: {}", url)); return Err(anyhow!("Unsupported URL scheme: {}", url));
} }
@@ -100,7 +99,7 @@ pub fn download_image(
tag, tag,
architecture, architecture,
} => { } => {
veprintln!("Pulling OCI image: {}/{}:{}", registry, repository, tag); log::debug!("Pulling OCI image: {}/{}:{}", registry, repository, tag);
rt.block_on(download_oci_image_async( rt.block_on(download_oci_image_async(
registry, registry,
repository, repository,
@@ -114,7 +113,7 @@ pub fn download_image(
} }
async fn download_file_async(url: &str, dest: &Path, progress: &dyn Progress) -> Result<()> { async fn download_file_async(url: &str, dest: &Path, progress: &dyn Progress) -> Result<()> {
veprintln!("Downloading: {}", url); log::debug!("Downloading: {}", url);
let client = Client::builder() let client = Client::builder()
.timeout(Duration::from_secs(300)) .timeout(Duration::from_secs(300))
@@ -168,7 +167,7 @@ async fn download_file_async(url: &str, dest: &Path, progress: &dyn Progress) ->
tokio::fs::rename(&temp_dest, dest) tokio::fs::rename(&temp_dest, dest)
.await .await
.context("Failed to move partial download to final destination")?; .context("Failed to move partial download to final destination")?;
veprintln!("Download complete: {}", dest.display()); log::debug!("Download complete: {}", dest.display());
Ok(()) Ok(())
} }
@@ -230,7 +229,7 @@ async fn try_download_oci_image(
format!("https://{}/v2/{}/manifests/{}", registry, repository, tag) format!("https://{}/v2/{}/manifests/{}", registry, repository, tag)
}; };
veprintln!("Fetching manifest from: {}", manifest_url); log::debug!("Fetching manifest from: {}", manifest_url);
// Request both manifest list and single manifest types (including OCI formats) // Request both manifest list and single manifest types (including OCI formats)
let response = client let response = client
@@ -265,7 +264,7 @@ async fn try_download_oci_image(
// Check if this is a manifest list (multi-arch) // Check if this is a manifest list (multi-arch)
let layers = if let Some(manifests) = manifest["manifests"].as_array() { let layers = if let Some(manifests) = manifest["manifests"].as_array() {
// This is a manifest list - find the right architecture // This is a manifest list - find the right architecture
veprintln!("Got manifest list with {} manifests", manifests.len()); log::debug!("Got manifest list with {} manifests", manifests.len());
let manifest_entry = manifests let manifest_entry = manifests
.iter() .iter()
@@ -291,7 +290,7 @@ async fn try_download_oci_image(
.as_str() .as_str()
.ok_or_else(|| anyhow!("No digest in manifest entry"))?; .ok_or_else(|| anyhow!("No digest in manifest entry"))?;
veprintln!("Found manifest digest: {}", manifest_digest); log::debug!("Found manifest digest: {}", manifest_digest);
// Now get the actual manifest for this architecture // Now get the actual manifest for this architecture
let arch_manifest_url = if registry == "docker.io" { let arch_manifest_url = if registry == "docker.io" {
@@ -338,7 +337,7 @@ async fn try_download_oci_image(
.clone() .clone()
}; };
veprintln!("Found {} layers to download", layers.len()); log::debug!("Found {} layers to download", layers.len());
// Download all layer blobs into a temp directory // Download all layer blobs into a temp directory
let temp_dir = tempfile::tempdir()?; let temp_dir = tempfile::tempdir()?;
@@ -358,7 +357,7 @@ async fn try_download_oci_image(
let ext = media_type_to_extension(media_type); let ext = media_type_to_extension(media_type);
let layer_name = format!("layer_{}.{}", i, ext); let layer_name = format!("layer_{}.{}", i, ext);
veprintln!( log::debug!(
"Fetching layer {}/{}: {}", "Fetching layer {}/{}: {}",
i + 1, i + 1,
layers.len(), layers.len(),
@@ -440,7 +439,7 @@ async fn get_auth_token(client: &Client, registry: &str, repository: &str) -> Re
"https://auth.docker.io/token?service=registry.docker.io&scope=repository:{}:pull", "https://auth.docker.io/token?service=registry.docker.io&scope=repository:{}:pull",
repository repository
); );
veprintln!("Getting auth token from: {}", url); log::debug!("Getting auth token from: {}", url);
return fetch_bearer_token(client, &url).await; return fetch_bearer_token(client, &url).await;
} }
@@ -464,8 +463,8 @@ async fn get_auth_token(client: &Client, registry: &str, repository: &str) -> Re
other => { other => {
// Unexpected status; proceed without a token and let the manifest // Unexpected status; proceed without a token and let the manifest
// request fail with a more descriptive error. // request fail with a more descriptive error.
eprintln!( log::warn!(
"Warning: registry probe returned HTTP {}; trying without auth", "registry probe returned HTTP {}; trying without auth",
other other
); );
return Ok(String::new()); return Ok(String::new());
@@ -492,7 +491,7 @@ async fn get_auth_token(client: &Client, registry: &str, repository: &str) -> Re
"{}?service={}&scope=repository:{}:pull", "{}?service={}&scope=repository:{}:pull",
realm, service, repository realm, service, repository
); );
veprintln!("Getting auth token from: {}", token_url); log::debug!("Getting auth token from: {}", token_url);
fetch_bearer_token(client, &token_url).await fetch_bearer_token(client, &token_url).await
} }
+1 -2
View File
@@ -17,7 +17,6 @@ use crate::mount::{self, BindTarget};
use crate::namespace; use crate::namespace;
use crate::qemu; use crate::qemu;
use crate::utils::{self, Arch}; use crate::utils::{self, Arch};
use crate::veprintln;
/// Options for [`exec`]. /// Options for [`exec`].
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
@@ -99,7 +98,7 @@ pub fn exec(rootfs: &Path, opts: &ExecOptions) -> Result<i32> {
for bind in &opts.binds { for bind in &opts.binds {
if !bind.read_only { if !bind.read_only {
veprintln!("Read-write mount: {}", bind.target.display()); log::debug!("Read-write mount: {}", bind.target.display());
} }
} }
+5 -6
View File
@@ -1,5 +1,4 @@
use crate::progress::{Progress, Unit}; use crate::progress::{Progress, Unit};
use crate::veprintln;
use anyhow::{Context, Result}; use anyhow::{Context, Result};
use std::fs::File; use std::fs::File;
use std::io::{BufReader, Read}; use std::io::{BufReader, Read};
@@ -15,7 +14,7 @@ pub fn extract_tarball(tarball: &Path, dest: &Path, progress: &dyn Progress) ->
// scanning the archive for a layers.manifest entry (which required // scanning the archive for a layers.manifest entry (which required
// reading the entire compressed archive twice for large OCI images). // reading the entire compressed archive twice for large OCI images).
if filename.starts_with("oci-") { if filename.starts_with("oci-") {
veprintln!("Detected multi-layer OCI image, extracting layers..."); log::debug!("Detected multi-layer OCI image, extracting layers...");
return extract_multi_layer_oci(tarball, dest, progress); return extract_multi_layer_oci(tarball, dest, progress);
} }
@@ -115,7 +114,7 @@ fn extract_multi_layer_oci(tarball: &Path, dest: &Path, progress: &dyn Progress)
// Not our layer-bundle layout: a provisioned rootfs persisted over // Not our layer-bundle layout: a provisioned rootfs persisted over
// an OCI cache entry (the dispatch above keys on the "oci-" filename // an OCI cache entry (the dispatch above keys on the "oci-" filename
// prefix). The outer archive is a plain rootfs tarball. // prefix). The outer archive is a plain rootfs tarball.
veprintln!("No layers.manifest in bundle; extracting as plain rootfs tarball"); log::debug!("No layers.manifest in bundle; extracting as plain rootfs tarball");
return extract_single_archive(tarball, dest, progress); return extract_single_archive(tarball, dest, progress);
} }
@@ -134,7 +133,7 @@ fn extract_multi_layer_oci(tarball: &Path, dest: &Path, progress: &dyn Progress)
if !layer_path.exists() { if !layer_path.exists() {
continue; continue;
} }
veprintln!("Extracting layer: {}", layer_name); log::debug!("Extracting layer: {}", layer_name);
extract_oci_layer(&layer_path, dest, &mut fixups, progress)?; extract_oci_layer(&layer_path, dest, &mut fixups, progress)?;
} }
@@ -324,7 +323,7 @@ fn apply_hard_link(dest: &Path, dst: &Path, target: &Path) -> Result<()> {
} }
std::os::unix::fs::symlink(target, dst) std::os::unix::fs::symlink(target, dst)
.with_context(|| format!("Failed to link {} -> {}", dst.display(), target.display()))?; .with_context(|| format!("Failed to link {} -> {}", dst.display(), target.display()))?;
veprintln!( log::debug!(
"Warning: hard link target {} absent; extracted {} as a symlink", "Warning: hard link target {} absent; extracted {} as a symlink",
target.display(), target.display(),
dst.display() dst.display()
@@ -421,7 +420,7 @@ fn extract_archive<R: std::io::Read>(
} }
progress.finish(); progress.finish();
veprintln!("Extracted {} files", count); log::debug!("Extracted {} files", count);
Ok(()) Ok(())
} }
+15 -16
View File
@@ -18,7 +18,6 @@
//! only ELF, uImage and raw images. //! only ELF, uImage and raw images.
use crate::progress::{Progress, Unit}; use crate::progress::{Progress, Unit};
use crate::veprintln;
use anyhow::{anyhow, Context, Result}; use anyhow::{anyhow, Context, Result};
use std::io::{Read, Seek}; use std::io::{Read, Seek};
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
@@ -112,7 +111,7 @@ fn find_kernel_package(branch: &str, arch: &str) -> Result<(&'static str, String
branch, arch branch, arch
); );
veprintln!("Fetching package index: {}", url); log::debug!("Fetching package index: {}", url);
let client = reqwest::blocking::Client::builder() let client = reqwest::blocking::Client::builder()
.timeout(Duration::from_secs(30)) .timeout(Duration::from_secs(30))
@@ -136,7 +135,7 @@ fn find_kernel_package(branch: &str, arch: &str) -> Result<(&'static str, String
// Extract APKINDEX from the tar.gz // Extract APKINDEX from the tar.gz
// We need to own the bytes to avoid lifetime issues // We need to own the bytes to avoid lifetime issues
let bytes_owned = bytes.to_vec(); let bytes_owned = bytes.to_vec();
veprintln!(" Downloaded {} bytes", bytes_owned.len()); log::debug!(" Downloaded {} bytes", bytes_owned.len());
// First decompress gzip to memory, then parse tar // First decompress gzip to memory, then parse tar
// Note: Alpine's APKINDEX.tar.gz uses concatenated gzip members (multi-member gzip) // Note: Alpine's APKINDEX.tar.gz uses concatenated gzip members (multi-member gzip)
@@ -148,7 +147,7 @@ fn find_kernel_package(branch: &str, arch: &str) -> Result<(&'static str, String
.read_to_end(&mut decompressed) .read_to_end(&mut decompressed)
.context("Failed to decompress gzip")?; .context("Failed to decompress gzip")?;
veprintln!(" Decompressed {} bytes", decompressed.len()); log::debug!(" Decompressed {} bytes", decompressed.len());
let tar_cursor = std::io::Cursor::new(decompressed); let tar_cursor = std::io::Cursor::new(decompressed);
let mut archive = tar::Archive::new(tar_cursor); let mut archive = tar::Archive::new(tar_cursor);
@@ -164,7 +163,7 @@ fn find_kernel_package(branch: &str, arch: &str) -> Result<(&'static str, String
let mut entry = match entry_result { let mut entry = match entry_result {
Ok(e) => e, Ok(e) => e,
Err(e) => { Err(e) => {
veprintln!( log::debug!(
" Warning: failed to read tar entry #{}: {}", " Warning: failed to read tar entry #{}: {}",
entry_count, entry_count,
e e
@@ -176,7 +175,7 @@ fn find_kernel_package(branch: &str, arch: &str) -> Result<(&'static str, String
let path = entry.path().context("Failed to get entry path")?; let path = entry.path().context("Failed to get entry path")?;
let path_str = path.to_string_lossy(); let path_str = path.to_string_lossy();
veprintln!(" Entry #{}: {}", entry_count, path_str); log::debug!(" Entry #{}: {}", entry_count, path_str);
if path_str == "APKINDEX" { if path_str == "APKINDEX" {
let mut contents = String::new(); let mut contents = String::new();
@@ -184,7 +183,7 @@ fn find_kernel_package(branch: &str, arch: &str) -> Result<(&'static str, String
.read_to_string(&mut contents) .read_to_string(&mut contents)
.context("Failed to read APKINDEX contents")?; .context("Failed to read APKINDEX contents")?;
veprintln!(" APKINDEX size: {} bytes", contents.len()); log::debug!(" APKINDEX size: {} bytes", contents.len());
// Parse the APKINDEX to find the best kernel package // Parse the APKINDEX to find the best kernel package
// Format: // Format:
@@ -228,7 +227,7 @@ fn find_kernel_package(branch: &str, arch: &str) -> Result<(&'static str, String
} }
} }
veprintln!(" Total entries processed: {}", entry_count); log::debug!(" Total entries processed: {}", entry_count);
Err(anyhow!( Err(anyhow!(
"APKINDEX file not found in tar.gz archive (processed {} entries)", "APKINDEX file not found in tar.gz archive (processed {} entries)",
@@ -244,7 +243,7 @@ fn download_alpine_kernel(
progress: &dyn Progress, progress: &dyn Progress,
) -> Result<()> { ) -> Result<()> {
let (package, version) = find_kernel_package(branch, arch)?; let (package, version) = find_kernel_package(branch, arch)?;
veprintln!("Found {} version: {}", package, version); log::debug!("Found {} version: {}", package, version);
// Construct the download URL for the kernel .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
@@ -253,7 +252,7 @@ fn download_alpine_kernel(
branch, arch, package, version branch, arch, package, version
); );
veprintln!("Downloading kernel: {}", url); log::debug!("Downloading kernel: {}", url);
// Use async download via the existing download module pattern // Use async download via the existing download module pattern
let rt = tokio::runtime::Runtime::new().context("Failed to create Tokio runtime")?; let rt = tokio::runtime::Runtime::new().context("Failed to create Tokio runtime")?;
@@ -336,7 +335,7 @@ fn extract_kernel_from_apk(apk_path: &Path, dest: &Path, package: &str) -> Resul
.ok_or_else(|| anyhow!("unexpected kernel package name: {}", package))?; .ok_or_else(|| anyhow!("unexpected kernel package name: {}", package))?;
let kernel_name = format!("boot/vmlinuz-{}", flavor); let kernel_name = format!("boot/vmlinuz-{}", flavor);
veprintln!("Extracting kernel from APK..."); log::debug!("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")?;
// Use MultiGzDecoder because Alpine APKs have concatenated gzip members // Use MultiGzDecoder because Alpine APKs have concatenated gzip members
@@ -348,12 +347,12 @@ fn extract_kernel_from_apk(apk_path: &Path, dest: &Path, package: &str) -> Resul
let path = entry.path().context("Failed to get entry path")?; let path = entry.path().context("Failed to get entry path")?;
let path_str = path.to_string_lossy(); let path_str = path.to_string_lossy();
veprintln!(" APK entry: {}", path_str); log::debug!(" APK entry: {}", path_str);
if path_str == kernel_name || path_str == format!("./{}", kernel_name) { if path_str == kernel_name || path_str == format!("./{}", kernel_name) {
// 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()); log::debug!(" Extracted kernel to: {}", dest.display());
return Ok(()); return Ok(());
} }
} }
@@ -379,7 +378,7 @@ fn decompress_kernel_if_gzipped(path: &Path) -> Result<()> {
return Ok(()); return Ok(());
} }
veprintln!("Kernel is gzip-compressed, decompressing..."); log::debug!("Kernel is gzip-compressed, decompressing...");
file.rewind().context("Failed to rewind kernel file")?; file.rewind().context("Failed to rewind kernel file")?;
let mut decompressed = Vec::new(); let mut decompressed = Vec::new();
flate2::read::MultiGzDecoder::new(file) flate2::read::MultiGzDecoder::new(file)
@@ -407,7 +406,7 @@ pub fn get_default_kernel(
// Check if already cached // Check if already cached
if kernel_path.exists() { if kernel_path.exists() {
veprintln!("Using cached default kernel: {}", kernel_path.display()); log::debug!("Using cached default kernel: {}", kernel_path.display());
return Ok(kernel_path); return Ok(kernel_path);
} }
@@ -416,7 +415,7 @@ pub fn get_default_kernel(
// Determine Alpine branch // Determine Alpine branch
let branch = get_alpine_branch()?; let branch = get_alpine_branch()?;
veprintln!("Using Alpine branch: {}", branch); log::debug!("Using Alpine branch: {}", branch);
// Download and extract the kernel // Download and extract the kernel
download_alpine_kernel(&branch, alpine_arch, &kernel_path, progress)?; download_alpine_kernel(&branch, alpine_arch, &kernel_path, progress)?;
-11
View File
@@ -25,17 +25,6 @@ pub mod qemu;
pub mod qemu_vm; pub mod qemu_vm;
pub mod rootfs; pub mod rootfs;
pub mod utils; pub mod utils;
pub mod verbose;
/// Print to stderr only when verbose mode is active (see [`verbose::set`]).
#[macro_export]
macro_rules! veprintln {
($($arg:tt)*) => {
if $crate::verbose::is_verbose() {
eprintln!($($arg)*);
}
};
}
pub use exec::{exec, ExecOptions}; pub use exec::{exec, ExecOptions};
pub use mount::BindTarget; pub use mount::BindTarget;
+2 -2
View File
@@ -67,7 +67,7 @@ pub fn setup_mounts(rootfs: &Path, binds: &[BindTarget]) -> Result<Vec<TempDir>>
MsFlags::MS_PRIVATE | MsFlags::MS_REC, MsFlags::MS_PRIVATE | MsFlags::MS_REC,
None::<&str>, None::<&str>,
) { ) {
eprintln!("Warning: Failed to make mounts private: {}", e); log::warn!("Failed to make mounts private: {}", e);
} }
// Mount /proc // Mount /proc
@@ -81,7 +81,7 @@ pub fn setup_mounts(rootfs: &Path, binds: &[BindTarget]) -> Result<Vec<TempDir>>
// Try to mount /sys (may fail in some environments) // Try to mount /sys (may fail in some environments)
if let Err(e) = mount_sys(rootfs) { if let Err(e) = mount_sys(rootfs) {
eprintln!("Warning: Could not mount /sys: {}", e); log::warn!("Could not mount /sys: {}", e);
} }
for bind in binds { for bind in binds {
+1 -2
View File
@@ -1,4 +1,3 @@
use crate::veprintln;
use anyhow::{anyhow, Result}; use anyhow::{anyhow, Result};
use std::path::Path; use std::path::Path;
@@ -19,6 +18,6 @@ pub fn check_binfmt(arch: &str) -> Result<()> {
)); ));
} }
veprintln!("QEMU binfmt_misc registered for {}", arch); log::debug!("QEMU binfmt_misc registered for {}", arch);
Ok(()) Ok(())
} }
+20 -22
View File
@@ -1,5 +1,4 @@
use crate::progress::{Progress, Unit}; use crate::progress::{Progress, Unit};
use crate::veprintln;
use anyhow::{anyhow, Context, Result}; use anyhow::{anyhow, Context, Result};
use cpio::{newc, NewcBuilder}; use cpio::{newc, NewcBuilder};
use std::io::Write; use std::io::Write;
@@ -15,6 +14,9 @@ pub struct QemuConfig {
pub rootfs_path: PathBuf, pub rootfs_path: PathBuf,
/// Memory size for VM (e.g., "2G", "512M") /// Memory size for VM (e.g., "2G", "512M")
pub memory: String, pub memory: String,
/// Append `quiet` to the kernel command line to suppress kernel log
/// messages (the CLI leaves this off under -v).
pub quiet: bool,
/// Target architecture /// Target architecture
pub arch: String, pub arch: String,
/// Optional command to run instead of default init /// Optional command to run instead of default init
@@ -65,9 +67,9 @@ pub fn launch_qemu(config: QemuConfig, progress: &dyn Progress) -> Result<()> {
// Check if we can use KVM acceleration // Check if we can use KVM acceleration
let use_kvm = can_use_kvm(&config.arch); let use_kvm = can_use_kvm(&config.arch);
if use_kvm { if use_kvm {
veprintln!(" KVM: enabled (native acceleration)"); log::debug!(" KVM: enabled (native acceleration)");
} else { } else {
veprintln!(" KVM: disabled (using software emulation)"); log::debug!(" KVM: disabled (using software emulation)");
} }
// Detect the best available shell in the rootfs // Detect the best available shell in the rootfs
@@ -89,13 +91,9 @@ pub fn launch_qemu(config: QemuConfig, progress: &dyn Progress) -> 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 (removed with -v) // 'quiet' suppresses kernel log messages for a cleaner console (see QemuConfig::quiet)
// 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() { let quiet_flag = if config.quiet { " quiet" } else { "" };
""
} else {
" quiet"
};
let kernel_append = if let Some(ref cmd) = config.command { let kernel_append = if let Some(ref cmd) = config.command {
// The argv travels through the kernel cmdline, where quotes and // The argv travels through the kernel cmdline, where quotes and
@@ -119,11 +117,11 @@ pub fn launch_qemu(config: QemuConfig, progress: &dyn Progress) -> Result<()> {
) )
}; };
veprintln!("Launching QEMU: {}", qemu_bin); log::debug!("Launching QEMU: {}", qemu_bin);
veprintln!(" Kernel: {}", config.kernel_path.display()); log::debug!(" Kernel: {}", config.kernel_path.display());
veprintln!(" Initramfs: {}", initramfs.display()); log::debug!(" Initramfs: {}", initramfs.display());
veprintln!(" Memory: {}", config.memory); log::debug!(" Memory: {}", config.memory);
veprintln!(" Kernel append: {}", kernel_append); log::debug!(" Kernel append: {}", kernel_append);
// Build QEMU arguments // Build QEMU arguments
// -machine virt is selected explicitly on riscv64 (see machine_for_arch) // -machine virt is selected explicitly on riscv64 (see machine_for_arch)
@@ -173,7 +171,7 @@ pub fn launch_qemu(config: QemuConfig, progress: &dyn Progress) -> Result<()> {
// Cleanup initramfs // Cleanup initramfs
if let Err(e) = std::fs::remove_file(&initramfs) { if let Err(e) = std::fs::remove_file(&initramfs) {
veprintln!("Warning: failed to cleanup initramfs: {}", e); log::debug!("Warning: failed to cleanup initramfs: {}", e);
} }
if !status.success() { if !status.success() {
@@ -308,7 +306,7 @@ fn create_initramfs(rootfs: &Path, progress: &dyn Progress) -> Result<PathBuf> {
let total_bytes = std::fs::metadata(&initramfs_path) let total_bytes = std::fs::metadata(&initramfs_path)
.map(|m| m.len()) .map(|m| m.len())
.unwrap_or(0); .unwrap_or(0);
veprintln!( log::debug!(
"Initramfs created: {} bytes, {} files", "Initramfs created: {} bytes, {} files",
total_bytes, total_bytes,
file_count file_count
@@ -345,7 +343,7 @@ fn write_cpio_archive<W: Write>(
&mut total_data, &mut total_data,
)?; )?;
veprintln!( log::debug!(
"Collected {} entries, {} bytes total data", "Collected {} entries, {} bytes total data",
written_names.len(), written_names.len(),
total_data total_data
@@ -508,7 +506,7 @@ fn write_dir_entries(
let dir_entries: Vec<_> = match std::fs::read_dir(current) { let dir_entries: Vec<_> = match std::fs::read_dir(current) {
Ok(entries) => entries.collect::<std::result::Result<_, _>>()?, Ok(entries) => entries.collect::<std::result::Result<_, _>>()?,
Err(e) => { Err(e) => {
veprintln!( log::debug!(
"Warning: cannot read directory {}: {}", "Warning: cannot read directory {}: {}",
current.display(), current.display(),
e e
@@ -526,7 +524,7 @@ fn write_dir_entries(
let metadata = match std::fs::symlink_metadata(&path) { let metadata = match std::fs::symlink_metadata(&path) {
Ok(m) => m, Ok(m) => m,
Err(e) => { Err(e) => {
veprintln!( log::debug!(
"Warning: skipping {} due to metadata error: {}", "Warning: skipping {} due to metadata error: {}",
path.display(), path.display(),
e e
@@ -577,7 +575,7 @@ fn write_dir_entries(
let data = match std::fs::read(&path) { let data = match std::fs::read(&path) {
Ok(data) => data, Ok(data) => data,
Err(e) => { Err(e) => {
veprintln!("Warning: cannot read file {}: {}", path.display(), e); log::debug!("Warning: cannot read file {}: {}", path.display(), e);
continue; continue;
} }
}; };
@@ -589,7 +587,7 @@ fn write_dir_entries(
let data = match std::fs::read(&path) { let data = match std::fs::read(&path) {
Ok(data) => data, Ok(data) => data,
Err(e) => { Err(e) => {
veprintln!("Warning: cannot read file {}: {}", path.display(), e); log::debug!("Warning: cannot read file {}: {}", path.display(), e);
continue; continue;
} }
}; };
@@ -598,7 +596,7 @@ fn write_dir_entries(
match std::fs::read_link(&path) { match std::fs::read_link(&path) {
Ok(target) => (target.to_string_lossy().into_owned().into_bytes(), 1, 0), Ok(target) => (target.to_string_lossy().into_owned().into_bytes(), 1, 0),
Err(e) => { Err(e) => {
veprintln!("Warning: cannot read symlink {}: {}", path.display(), e); log::debug!("Warning: cannot read symlink {}: {}", path.display(), e);
continue; continue;
} }
} }
+5 -9
View File
@@ -48,7 +48,6 @@ use crate::download::{digest_sidecar, download_image, fetch_oci_digest};
use crate::exec::ExecOptions; use crate::exec::ExecOptions;
use crate::extract::extract_tarball; use crate::extract::extract_tarball;
use crate::progress::Progress; use crate::progress::Progress;
use crate::veprintln;
/// Sidecar suffix marking a cache entry that holds a provisioned rootfs. /// Sidecar suffix marking a cache entry that holds a provisioned rootfs.
const PROVISIONED_SUFFIX: &str = "provisioned"; const PROVISIONED_SUFFIX: &str = "provisioned";
@@ -117,10 +116,7 @@ impl RootfsCache {
stored.as_deref() != Some(current.trim()) stored.as_deref() != Some(current.trim())
} }
Err(e) => { Err(e) => {
eprintln!( log::warn!("could not check image freshness ({}); using cache", e);
"Warning: could not check image freshness ({}); using cache",
e
);
false false
} }
} }
@@ -142,7 +138,7 @@ impl RootfsCache {
})?; })?;
download_image(&source, &cache_path, &arch, progress)?; download_image(&source, &cache_path, &arch, progress)?;
} else { } else {
veprintln!("Using cached tarball: {}", cache_path.display()); log::debug!("Using cached tarball: {}", cache_path.display());
} }
extract_prepared(cache_path, arch, progress) extract_prepared(cache_path, arch, progress)
@@ -175,7 +171,7 @@ impl RootfsCache {
let marker = sidecar(&cache_path, PROVISIONED_SUFFIX); let marker = sidecar(&cache_path, PROVISIONED_SUFFIX);
if !req.no_cache && cache_path.exists() && marker.exists() { if !req.no_cache && cache_path.exists() && marker.exists() {
veprintln!("Using provisioned cache entry: {}", cache_path.display()); log::debug!("Using provisioned cache entry: {}", cache_path.display());
return extract_prepared(cache_path, arch, progress); return extract_prepared(cache_path, arch, progress);
} }
@@ -278,7 +274,7 @@ impl PreparedRootfs {
self.cache_path.display() self.cache_path.display()
) )
})?; })?;
veprintln!("Persisted rootfs to cache: {}", self.cache_path.display()); log::debug!("Persisted rootfs to cache: {}", self.cache_path.display());
Ok(()) Ok(())
} }
} }
@@ -309,7 +305,7 @@ fn extract_prepared(
let temp = tempfile::tempdir().context("Failed to create temporary directory")?; let temp = tempfile::tempdir().context("Failed to create temporary directory")?;
let rootfs = temp.path().to_path_buf(); let rootfs = temp.path().to_path_buf();
veprintln!("Extracting to: {}", rootfs.display()); log::debug!("Extracting to: {}", rootfs.display());
extract_tarball(&cache_path, &rootfs, progress)?; extract_tarball(&cache_path, &rootfs, progress)?;
Ok(PreparedRootfs { Ok(PreparedRootfs {
-11
View File
@@ -1,11 +0,0 @@
use std::sync::atomic::{AtomicBool, Ordering};
static VERBOSE: AtomicBool = AtomicBool::new(false);
pub fn set(v: bool) {
VERBOSE.store(v, Ordering::Relaxed);
}
pub fn is_verbose() -> bool {
VERBOSE.load(Ordering::Relaxed)
}