#[cfg(feature = "libkrun")]
#[macro_export]
macro_rules! debug_epkg {
($($arg:tt)*) => {
if std::env::var("EPKG_DEBUG_LIBKRUN").is_ok_and(|v| v == "1") {
use std::time::Instant;
static START: std::sync::OnceLock<Instant> = std::sync::OnceLock::new();
let start = START.get_or_init(Instant::now);
let elapsed = start.elapsed();
eprintln!("[epkg {:5}.{:03}s] {}", elapsed.as_secs(), elapsed.subsec_millis(), format_args!($($arg)*));
}
};
}
#[cfg(not(feature = "libkrun"))]
#[macro_export]
macro_rules! debug_epkg {
($($arg:tt)*) => {};
}
mod dirs;
mod models;
mod io;
mod tar_extract;
mod lfs;
mod download;
mod depends;
mod resolve;
mod solver_tests;
mod parse_requires;
mod rpm_requires;
mod conda_requires;
mod parse_provides;
mod provides;
mod install;
mod upgrade;
mod remove;
mod hash;
#[cfg(unix)]
mod ipc;
mod store;
mod package_cache;
mod link;
mod expose;
#[cfg(target_os = "linux")]
mod xdesktop;
mod transaction;
mod world;
mod utils;
mod mtree;
#[cfg(unix)]
mod auto_idmap;
#[cfg(unix)]
mod posix;
mod history;
mod environment;
mod deinit;
#[cfg(target_os = "linux")]
mod apparmor;
mod init;
mod path;
mod shell_emit;
mod repo;
mod mmio;
mod mirror;
mod location;
mod package;
mod packages_stream;
mod index_html;
#[cfg(windows)]
pub mod ntfs_ea {
#![allow(dead_code)]
include!("../git/libkrun/src/devices/src/virtio/fs/windows/ntfs_ea.rs");
}
#[cfg(windows)]
#[allow(dead_code)]
mod krun_virtiofs_windows {
pub mod ntfs_ea {
include!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/git/libkrun/src/devices/src/virtio/fs/windows/ntfs_ea.rs"
));
}
pub mod reparse_point {
include!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/git/libkrun/src/devices/src/virtio/fs/windows/reparse_point.rs"
));
}
pub mod symlink {
include!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/git/libkrun/src/devices/src/virtio/fs/windows/symlink.rs"
));
}
}
mod deb_repo;
mod deb_pkg;
mod deb_sources;
mod rpm_repo;
mod rpm_pkg;
mod rpm_sources;
mod apk_repo;
mod apk_pkg;
mod arch_repo;
mod arch_pkg;
#[cfg(target_os = "linux")]
mod aur;
mod conda_repo;
mod conda_pkg;
mod conda_link;
mod brew_repo;
#[cfg(unix)]
mod brew_pkg;
#[cfg(unix)]
mod brew_service;
#[cfg(unix)]
mod brew_postinstall;
mod shebang;
mod version_constraint;
#[cfg(unix)]
mod epkg;
mod parse_version;
mod plan;
mod version_compare;
mod scriptlets;
mod hooks;
#[cfg(unix)]
mod userdb;
mod deb_triggers;
mod dpkg_db;
mod rpm_triggers;
#[cfg(target_os = "linux")]
mod lua;
#[cfg(any(unix, windows))]
mod risks;
mod run;
#[cfg(any(target_os = "linux", feature = "libkrun"))]
mod mount_specs;
#[cfg(target_os = "linux")]
mod namespace;
#[cfg(target_os = "linux")]
mod idmap;
#[cfg(target_os = "linux")]
mod mount;
#[cfg(target_os = "linux")]
mod qemu;
#[cfg(feature = "libkrun")]
mod libkrun;
mod vm;
mod vmm;
mod busybox;
mod info;
mod list;
mod search;
mod gc;
#[cfg(unix)]
mod service;
mod tool_wrapper;
#[cfg(debug_assertions)]
mod rpm_verify;
use std::env;
use std::path::{Path, PathBuf};
use std::process::exit;
use std::panic;
use time::OffsetDateTime;
use time::macros::format_description;
use crate::models::*;
use crate::dirs::*;
use crate::environment::*;
use crate::io::{edit_environment_config, read_yaml_file, CHANNEL_SEPARATOR};
use crate::path::update_path;
use crate::repo::sync_channel_metadata;
use crate::list::list_packages_with_scope;
use crate::install::install_packages;
use crate::upgrade::upgrade_packages;
use crate::remove::remove_packages;
use crate::history::{print_history, rollback_history};
use crate::init::{install_epkg_with_force, try_light_init, light_init, upgrade_epkg};
use crate::run::{command_run, command_busybox};
use color_eyre::Result;
use color_eyre::eyre;
use color_eyre::eyre::WrapErr;
use clap::{arg, Arg, ArgAction, Command};
use ctrlc;
use env_logger;
use log::LevelFilter;
use log;
use list::ListScope;
use std::io::IsTerminal;
#[cfg(not(test))]
fn main() -> Result<()> {
let mut builder = color_eyre::config::HookBuilder::default()
.display_env_section(false)
.display_location_section(true);
if std::io::stderr().is_terminal() {
builder = builder.theme(color_eyre::config::Theme::dark());
}
builder.install()?;
rustls::crypto::ring::default_provider()
.install_default()
.expect("Failed to install rustls crypto provider");
let argv: Vec<String> = std::env::args_os()
.map(|a| a.to_string_lossy().into_owned())
.collect();
#[cfg(target_os = "linux")]
let invoked_as_init = argv.first().map(|a| std::path::Path::new(a).file_name()) == Some(Some(std::ffi::OsStr::new("init")));
#[cfg(target_os = "linux")]
if invoked_as_init {
crate::busybox::init::init_logging_early();
}
#[cfg(target_os = "linux")]
if invoked_as_init {
use std::io::Write;
if let Ok(mut kmsg) = std::fs::OpenOptions::new().write(true).open("/dev/kmsg") {
let _ = write!(kmsg, "<6>main: before setup_logging\n");
}
}
#[cfg(target_os = "linux")]
let invoked_as_init_for_logging = invoked_as_init;
#[cfg(not(target_os = "linux"))]
let invoked_as_init_for_logging = false;
setup_logging(invoked_as_init_for_logging);
#[cfg(target_os = "linux")]
if invoked_as_init {
use std::io::Write;
if let Ok(mut kmsg) = std::fs::OpenOptions::new().write(true).open("/dev/kmsg") {
let _ = write!(kmsg, "<6>main: after setup_logging\n");
}
}
log::info!("{}", env!("EPKG_VERSION_INFO"));
if cfg!(debug_assertions) {
log::info!("debug build: {} exe={}",
env!("BUILD_TIME"),
std::env::current_exe().as_ref().map(|p| p.display().to_string()).unwrap_or_else(|_| "<unknown>".to_string()));
}
#[cfg(any(target_os = "macos", target_os = "linux"))]
{
#[cfg(target_os = "macos")]
const TARGET_FD_LIMIT: u64 = 81_920;
#[cfg(target_os = "linux")]
const TARGET_FD_LIMIT: u64 = 2_097_152;
let mut rlim: libc::rlimit = unsafe { std::mem::zeroed() };
if unsafe { libc::getrlimit(libc::RLIMIT_NOFILE, &mut rlim) } == 0 {
let soft = rlim.rlim_cur;
let hard = rlim.rlim_max;
log::debug!("Current RLIMIT_NOFILE: soft={}, hard={}", soft, hard);
if soft < TARGET_FD_LIMIT {
let target = std::cmp::min(hard, TARGET_FD_LIMIT);
if target > soft {
rlim.rlim_cur = target;
if unsafe { libc::setrlimit(libc::RLIMIT_NOFILE, &rlim) } != 0 {
let err = std::io::Error::last_os_error();
log::warn!("Failed to increase file descriptor limit from {} to {}: {}", soft, target, err);
} else {
log::debug!("Increased file descriptor limit from {} to {}", soft, target);
}
}
}
}
}
log::debug!("argv[{}]: {:?}", argv.len(), argv);
#[cfg(target_os = "linux")]
let invoked_as_applet = if invoked_as_init { false } else { crate::busybox::is_invoked_as_applet() };
#[cfg(not(target_os = "linux"))]
let invoked_as_applet = crate::busybox::is_invoked_as_applet();
#[cfg(target_os = "linux")]
if invoked_as_init {
use std::io::Write;
if let Ok(mut kmsg) = std::fs::OpenOptions::new().write(true).open("/dev/kmsg") {
let _ = write!(kmsg, "<6>main: before init_config applet={}\n", invoked_as_applet);
}
}
#[cfg(target_os = "linux")]
let invoked_as_init_for_config = invoked_as_init;
#[cfg(not(target_os = "linux"))]
let invoked_as_init_for_config = false;
init_config(invoked_as_applet, invoked_as_init_for_config)?;
#[cfg(target_os = "linux")]
if invoked_as_init {
use std::io::Write;
if let Ok(mut kmsg) = std::fs::OpenOptions::new().write(true).open("/dev/kmsg") {
let _ = write!(kmsg, "<6>main: after init_config, calling init::run\n");
}
use crate::busybox::init::run;
run(()).wrap_err("init: init::run failed")?;
return Ok(());
}
attach_session_log_under_epkg_cache();
{
if let Some(exit_code) = try_route_command_via_vm(crate::models::clap_matches())? {
std::process::exit(exit_code);
}
}
#[cfg(target_os = "linux")]
if invoked_as_init {
use std::io::Write;
if let Ok(mut kmsg) = std::fs::OpenOptions::new().write(true).open("/dev/kmsg") {
let _ = write!(kmsg, "<6>main: after attach_session_log\n");
}
}
setup_ctrlc();
#[cfg(target_os = "linux")]
if invoked_as_init {
use std::io::Write;
if let Ok(mut kmsg) = std::fs::OpenOptions::new().write(true).open("/dev/kmsg") {
let _ = write!(kmsg, "<6>main: after setup_ctrlc\n");
}
}
#[cfg(unix)]
unsafe {
libc::signal(libc::SIGPIPE, libc::SIG_DFL);
}
#[cfg(target_os = "linux")]
if invoked_as_init {
use std::io::Write;
if let Ok(mut kmsg) = std::fs::OpenOptions::new().write(true).open("/dev/kmsg") {
let _ = write!(kmsg, "<6>main: after SIGPIPE setup, applet={}\n", invoked_as_applet);
}
}
if invoked_as_applet {
match crate::busybox::handle_applet_invocation()? {
Some(_) => return Ok(()),
None => {}
}
}
log::trace!("Application starting with config: {:#?}", &*config());
try_light_init()?;
let matches = clap_matches();
match matches.subcommand() {
Some(("self", sub_matches)) => command_self(sub_matches)?,
Some(("env", sub_matches)) => command_env(sub_matches)?,
Some(("list", sub_matches)) => command_list(sub_matches)?,
Some(("info", sub_matches)) => command_info(sub_matches)?,
Some(("install", sub_matches)) => command_install(sub_matches)?,
Some(("upgrade", sub_matches)) => command_upgrade(sub_matches)?,
Some(("remove", sub_matches)) => command_remove(sub_matches)?,
Some(("history", sub_matches)) => command_history(sub_matches)?,
Some(("restore", sub_matches)) => command_restore(sub_matches)?,
Some(("update", sub_matches)) => command_update(sub_matches)?,
Some(("repo", sub_matches)) => command_repo(sub_matches)?,
Some(("hash", sub_matches)) => command_hash(sub_matches)?,
#[cfg(unix)]
Some(("build", sub_matches)) => command_build(sub_matches)?,
#[cfg(unix)]
Some(("unpack", sub_matches)) => command_unpack(&sub_matches)?,
#[cfg(unix)]
Some(("convert", sub_matches)) => command_convert(&sub_matches)?,
Some(("run", sub_matches)) => command_run(sub_matches)?,
Some(("busybox", sub_matches)) => command_busybox(sub_matches)?,
Some(("search", sub_matches)) => command_search(sub_matches)?,
Some(("gc", sub_matches)) => command_gc(sub_matches)?,
Some(("vm", sub_matches)) => command_vm(sub_matches)?,
#[cfg(unix)]
Some(("service", sub_matches)) => command_service(sub_matches)?,
_ => {}
}
Ok(())
}
#[cfg(not(test))]
use std::fs::{self, OpenOptions};
#[cfg(not(test))]
use std::sync::Mutex;
#[cfg(not(test))]
static LOG_FILE_WRITER: std::sync::OnceLock<Mutex<std::fs::File>> = std::sync::OnceLock::new();
#[cfg(not(test))]
fn setup_logging(invoked_as_init: bool) {
env_logger::Builder::from_default_env()
.filter_module("ureq_proto", LevelFilter::Warn)
.format(move |buf, record| {
use std::io::Write;
let timestamp = if invoked_as_init {
OffsetDateTime::now_utc().format(&format_description!(
"[year]-[month]-[day] [hour repr:24]:[minute]:[second].[subsecond digits:3] UTC"
)).unwrap_or_else(|_| "<utc_time_err>".to_string())
} else {
match OffsetDateTime::now_local() {
Ok(dt) => dt.format(&format_description!("[year]-[month]-[day] [hour repr:24]:[minute]:[second].[subsecond digits:3] [offset_hour sign:mandatory][offset_minute]")).unwrap_or_else(|_| "<time_fmt_err>".to_string()),
Err(_) => "<local_time_err>".to_string(),
}
};
let formatted = format!(
"[{} {} {}:{}] {}",
timestamp,
record.level(),
record.file().unwrap_or("unknown"),
record.line().unwrap_or(0),
record.args()
);
if let Some(writer) = LOG_FILE_WRITER.get() {
if let Ok(mut file) = writer.lock() {
if let Err(e) = writeln!(file, "{}", formatted) {
eprintln!("[epkg log error] write failed: {}", e);
} else if let Err(e) = file.flush() {
eprintln!("[epkg log error] flush failed: {}", e);
}
}
}
writeln!(buf, "{}", formatted)
})
.init();
}
#[cfg(not(test))]
fn session_log_basename() -> String {
let timestamp = match OffsetDateTime::now_local() {
Ok(dt) => dt.format(&format_description!(
"[year][month][day]_[hour][minute][second]"
)).unwrap_or_else(|_| {
OffsetDateTime::now_utc().format(&format_description!(
"[year][month][day]_[hour][minute][second]"
)).unwrap_or_else(|_| "unknown".to_string())
}),
Err(_) => {
OffsetDateTime::now_utc().format(&format_description!(
"[year][month][day]_[hour][minute][second]"
)).unwrap_or_else(|_| "unknown".to_string())
},
};
let pid = std::process::id();
format!("epkg_{}_{}.log", timestamp, pid)
}
#[cfg(not(test))]
fn try_open_session_log_in_dir(log_dir: &Path) {
if LOG_FILE_WRITER.get().is_some() {
return;
}
let path = log_dir.join(session_log_basename());
let _ = fs::create_dir_all(log_dir);
match OpenOptions::new()
.create(true)
.append(true)
.open(&path)
{
Ok(file) => {
let mut file = file;
let header = b"=== epkg log started ===\n";
let _ = std::io::Write::write_all(&mut file, header);
let _ = std::io::Write::flush(&mut file);
let _ = LOG_FILE_WRITER.set(Mutex::new(file));
log::debug!("Logging to: {}", path.display());
}
Err(e) => {
log::warn!("Failed to open log file {}: {}", path.display(), e);
}
}
}
#[cfg(not(test))]
fn attach_session_log_under_epkg_cache() {
if std::env::var_os("RUST_LOG").is_none() && std::env::var_os("EPKG_DEBUG_LIBKRUN").is_none() {
return;
}
let dir = crate::models::dirs().epkg_cache.join("logs");
try_open_session_log_in_dir(&dir);
}
#[cfg(not(test))]
fn setup_ctrlc() {
if !cfg!(debug_assertions) {
return;
}
if config().subcommand == EpkgCommand::Run {
return;
}
if std::env::var("RUST_BACKTRACE").is_err() {
return;
}
ctrlc::set_handler(move || {
eprintln!("\nReceived Ctrl-C! Cancelling downloads and collecting thread backtraces...");
crate::download::cancel_downloads();
let args: Vec<String> = std::env::args().collect();
eprintln!("Command: {}", args.join(" "));
eprintln!("Process ID: {}", std::process::id());
eprintln!("Current directory: {:?}", std::env::current_dir().unwrap_or_default());
eprintln!("Elapsed time: {:?}", std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap_or_default());
if let Ok(mirrors_guard) = crate::mirror::MIRRORS.try_lock() {
eprintln!("\nMirror performance statistics:");
crate::mirror::dump_mirror_performance_stats(&mirrors_guard, true);
} else {
eprintln!("\nCould not access mirror statistics (lock contention)");
}
crate::download::DOWNLOAD_MANAGER.dump_all_tasks();
print_all_thread_backtraces();
eprintln!("\nEnvironment variables of interest:");
for (key, value) in std::env::vars() {
if key.starts_with("RUST_") || key.starts_with("EPKG_") || key.starts_with("CARGO_") {
eprintln!(" {}={}", key, value);
}
}
eprintln!("\nExiting due to Ctrl-C...");
std::process::exit(130);
}).expect("Failed to set Ctrl-C handler");
}
fn parse_link_type(link_str: &str) -> Result<LinkType> {
match link_str {
"hardlink" => Ok(LinkType::Hardlink),
"symlink" => Ok(LinkType::Symlink),
"reflink" => Ok(LinkType::Reflink),
"move" => Ok(LinkType::Move),
"runpath" => Ok(LinkType::Runpath),
_ => Err(eyre::eyre!("Invalid link type: '{}'. Valid options are: hardlink, symlink, reflink, move, runpath", link_str)),
}
}
fn print_all_thread_backtraces() {
eprintln!("\n=== Runtime Information ===");
if let Ok(status) = std::fs::read_to_string("/proc/self/status") {
eprintln!("Process status:");
for line in status.lines() {
if line.starts_with("VmRSS:") || line.starts_with("VmSize:") ||
line.starts_with("Threads:") || line.starts_with("State:") ||
line.starts_with("PPid:") || line.starts_with("TracerPid:") {
eprintln!(" {}", line);
}
}
}
if let Ok(entries) = std::fs::read_dir("/proc/self/fd") {
let mut fds = Vec::new();
for entry in entries {
if let Ok(entry) = entry {
let fd_num = entry.file_name();
if let Ok(link) = std::fs::read_link(entry.path()) {
fds.push(format!("{}: {}", fd_num.to_string_lossy(), link.display()));
}
}
}
eprintln!("Open file descriptors ({}):", fds.len());
for fd in fds.iter().take(20) {
eprintln!(" {}", fd);
}
if fds.len() > 20 {
eprintln!(" ... and {} more", fds.len() - 20);
}
}
if let Ok(tcp) = std::fs::read_to_string("/proc/self/net/tcp") {
let mut lines = tcp.lines();
if let Some(header) = lines.next() {
let connections: Vec<_> = lines.take(10).collect();
if !connections.is_empty() {
eprintln!("Active TCP connections:");
eprintln!(" {}", header);
for conn in connections {
eprintln!(" {}", conn);
}
}
}
}
try_print_backtrace();
eprintln!("\n=== Debugging Tips ===");
eprintln!("To get more detailed debugging information:");
eprintln!("1. Attach gdb: gdb -p $(pidof epkg)");
eprintln!("2. Use strace: strace -p $(pidof epkg)");
eprintln!("3. Run with: RUST_LOG=debug RUST_BACKTRACE=full {}", std::env::args().collect::<Vec<_>>().join(" "));
eprintln!("4. Check system logs: journalctl --since '1 minute ago' --grep epkg");
}
fn try_print_backtrace() {
use std::process::Command;
if !cfg!(debug_assertions) {
return;
}
let pid = std::process::id();
eprintln!("=== Attempting to get userspace backtraces ===");
if let Ok(stack) = std::fs::read_to_string(format!("/proc/{}/stack", pid)) {
if !stack.trim().is_empty() {
eprintln!("Kernel stack trace:");
eprintln!("{}", stack);
}
}
if let Ok(output) = Command::new("eu-stack")
.args(["-p", &pid.to_string()])
.output()
{
if output.status.success() {
let stdout = String::from_utf8_lossy(&output.stdout);
if !stdout.trim().is_empty() {
eprintln!("eu-stack output:");
eprintln!("{}", stdout);
return;
}
}
}
if let Ok(output) = Command::new("pstack")
.arg(pid.to_string())
.output()
{
if output.status.success() {
let stdout = String::from_utf8_lossy(&output.stdout);
if !stdout.trim().is_empty() {
eprintln!("pstack output:");
eprintln!("{}", stdout);
return;
}
}
}
}
fn add_global_args_and_help(cmd: Command) -> Command {
cmd.author("Wu Fengguang <wfg@mail.ustc.edu.cn>")
.author("Duan Pengjie <pengjieduan@gmail.com>")
.author("Yingjiahui <ying_register@163.com>")
.about("epkg")
.version(env!("EPKG_VERSION_INFO"))
.arg_required_else_help(true)
.arg(arg!(--config <FILE> "Configuration file to use").hide(true).global(true))
.arg(arg!(-e --env <ENV_NAME> "Select the environment by name or owner/name").hide(true).global(true))
.arg(arg!(-r --root <DIR> "Select the environment by root dir").hide(true).global(true))
.arg(arg!(--arch <ARCH> "Select the CPU architecture").default_value(std::env::consts::ARCH).hide(true).global(true))
.arg(arg!(--"dry-run" "Simulated run without changing the system").hide(true).global(true))
.arg(arg!(--"download-only" "Download packages without installing").hide(true).global(true))
.arg(arg!(-q --quiet "Suppress output").hide(true).global(true))
.arg(arg!(-v --verbose "Verbose operation, show debug messages").hide(true).global(true))
.arg(arg!(-y --"assume-yes" "Automatically answer yes to all prompts").alias("yes").hide(true).global(true))
.arg(arg!(--"assume-no" "Automatically answer no to all prompts").hide(true).global(true))
.arg(arg!(--"ignore-missing" "Ignore missing packages").hide(true).global(true))
.arg(arg!(--"metadata-expire" <SECONDS> "Metadata expiration time in seconds (0=never, -1=always)").value_parser(clap::value_parser!(i32)).hide(true).global(true))
.arg(arg!(--proxy <URL> "HTTP proxy URL (e.g., http://proxy.example.com:8080)").hide(true).global(true))
.arg(arg!(--"retry" <NUMBER> "Number of retries for download tasks").value_parser(clap::value_parser!(usize)).hide(true).global(true))
.arg(arg!(--"parallel-download" <NUMBER> "Number of parallel download threads").value_parser(clap::value_parser!(usize)).hide(true).global(true))
.arg(arg!(--"parallel-processing" <NUMBER> "Number of parallel processing workers").value_parser(clap::value_parser!(usize)).hide(true).global(true))
.override_usage("epkg [OPTIONS] <COMMAND>")
.help_template(
r#"{about} {version}
USAGE: {usage}
COMMANDS:
Self Management:
self install [--store private|shared|auto] Install/upgrade epkg itself
self upgrade|remove Upgrade or remove epkg installation
Package Operations:
install Install packages
update Update package metadata
upgrade Upgrade packages
remove Uninstall packages
Environment Management:
env create [-c|--channel DISTRO] [-P|--public] [-i|--import FILE] <ENV_NAME|--root ENV_ROOT>
env <remove|register|unregister|activate|export> <ENV_NAME|--root ENV_ROOT>
env deactivate|path
env config <edit|get|set>
History & Rollback:
history Show environment history
restore <GEN_ID|-N> Restore environment to specific generation
Garbage Collection:
gc Clean up unused cache and store files
Info & Query:
list List packages: [--installed (default)|--available|--upgradable|--all] [PKGNAME_GLOB]
info Show package information
search Search for packages and files
repo list List repositories
Running Commands:
run Run command in environment namespace
service Service management: start/stop/restart/status/reload
busybox Run built-in command implementations
Package Utilities:
hash Compute binary package hash
unpack Unpack package file(s) into store directory
convert Convert rpm/deb/apk/... packages to epkg format (not ready)
Build:
build Build package from source (not ready)
OPTIONS:
--config <FILE> Configuration file to use
-e, --env <ENV_NAME> Select the environment by name or owner/name
-r, --root <DIR> Select the environment by root dir
--arch <ARCH> Select the CPU architecture
--dry-run Simulated run without changing the system
--download-only Download packages without installing
-q, --quiet Suppress output
-v, --verbose Verbose operation, show debug messages
-y, --assume-yes, --yes Automatically answer yes to all prompts
--assume-no Automatically answer no to all prompts
--ignore-missing Ignore missing packages
--metadata-expire <SECONDS> Metadata expiration time in seconds (0=never, -1=always)
--proxy <URL> HTTP proxy URL (e.g., http://proxy.example.com:8080)
--retry <NUMBER> Number of retries for download tasks
--parallel-download <NUMBER> Number of parallel download threads
--parallel-processing <NUMBER> Number of parallel processing workers
-h, --help Print help
-V, --version Print version
PATHS:
User private installation: (in data-flow order)
$HOME/.bashrc # sources $HOME/.epkg/envs/self/usr/src/epkg/assets/shell/epkg.sh for epkg() builtin func
$HOME/.cache/downloads/
$HOME/.cache/channels/
$HOME/.epkg/store/
$HOME/.epkg/envs/$env_name/
$HOME/.epkg/envs/$env_name/etc/epkg/ # per-env epkg config files
$HOME/.epkg/envs/self/usr/bin/epkg # epkg executable binary
$HOME/.epkg/envs/self/usr/src/epkg/ # epkg source code files
Root global installation:
$HOME/.bashrc
/opt/epkg/cache/downloads/
/opt/epkg/cache/channels/
/opt/epkg/store/
/opt/epkg/envs/root/$env_name/
"#)
}
fn add_self_subcommand(cmd: Command) -> Command {
cmd.subcommand(
Command::new("self")
.about("Manage epkg installation")
.arg_required_else_help(true)
.subcommand(
Command::new("install")
.about("Install epkg")
.arg(arg!(--commit <COMMIT>).help(format!("Source commit of epkg to install [default: {}]", DEFAULT_COMMIT)))
.arg(arg!(-c --channel <CHANNEL> "Set the channel, e.g. debian, debian-13, or msys2 (Windows)"))
.arg(arg!( --repo <REPO> "Add one or more repos separated by space, e.g. ceph postgresql").num_args(1..))
.arg(
arg!(--store <STORE> "Store mode: 'shared' (reused by all users), 'private' (current user only), or 'auto' (shared if installed by root)")
.default_value("auto")
.value_parser(["shared", "private", "auto"]),
)
.arg(arg!(--force "Force reinstall epkg source and binaries"))
)
.subcommand(
Command::new("upgrade")
.about("Upgrade epkg installation")
)
.subcommand(
Command::new("remove")
.about("Remove epkg installation")
.arg(
arg!(--scope <SCOPE> "Scope of removal: 'personal' (current user only) or 'global' (all users)")
.default_value("personal")
.value_parser(["personal", "global"]),
)
)
)
}
fn add_env_subcommand(cmd: Command) -> Command {
cmd.subcommand(
Command::new("env")
.about("Environment management")
.arg_required_else_help(true)
.subcommand(
Command::new("list")
.about("List all environments")
)
.subcommand(
Command::new("create")
.about("Create a new environment")
.arg(arg!([ENV_NAME] "Environment name or owner/name"))
.arg(arg!(-c --channel <CHANNEL> "Set the channel, e.g. debian, debian-13, or msys2 (Windows)"))
.arg(arg!( --repo <REPO> "Add one or more repos separated by space, e.g. ceph postgresql").num_args(1..))
.arg(arg!(-P --public "Usable by all users in the machine"))
.arg(arg!(-i --import <FILE> "Import from config file"))
.arg(arg!(--link <LINK> "Link type: hardlink, symlink, reflink, move, or runpath").value_parser(["hardlink", "symlink", "reflink", "move", "runpath"]))
)
.subcommand(
Command::new("remove")
.about("Remove an environment")
.arg(arg!([ENV_NAME] "Environment name or owner/name"))
)
.subcommand(
Command::new("register")
.about("Register an environment")
.arg(arg!([ENV_NAME] "Environment name or owner/name"))
.arg(
arg!(--"path-order" <PATH_ORDER> "Set the PATH order for the environment (lower number = earlier in PATH)")
.value_parser(clap::value_parser!(i32)),
)
)
.subcommand(
Command::new("unregister")
.about("Unregister an environment")
.arg(arg!([ENV_NAME] "Environment name or owner/name"))
)
.subcommand(
Command::new("activate")
.about("Activate an environment")
.arg(arg!([ENV_NAME] "Environment name or owner/name"))
.arg(arg!( --pure "Create a pure environment"))
.arg(arg!(-s --stack "Stack this environment on top of the current one"))
)
.subcommand(
Command::new("deactivate")
.about("Deactivate the current environment")
)
.subcommand(
Command::new("export")
.about("Export environment configuration")
.arg(arg!([ENV_NAME] "Environment name or owner/name"))
.arg(arg!(-o --output <FILE> "Output file path"))
)
.subcommand(
Command::new("path")
.about("Update PATH environment variable")
)
.subcommand(
Command::new("config")
.about("Configure environment settings")
.arg_required_else_help(true)
.subcommand(
Command::new("edit")
.about("Edit environment configuration file")
)
.subcommand(
Command::new("get")
.about("Get environment configuration value")
.arg(arg!(<NAME> "Configuration name to get"))
)
.subcommand(
Command::new("set")
.about("Set environment configuration value")
.arg(arg!(<NAME> "Configuration name to set"))
.arg(arg!(<VALUE> "Value to set"))
)
)
)
}
fn add_package_operation_subcommands(cmd: Command) -> Command {
cmd.subcommand(
Command::new("list")
.about("List packages")
.arg(arg!(--all "List all packages"))
.arg(arg!(--installed "List installed packages"))
.arg(arg!(--available "List available packages"))
.arg(arg!(--upgradable "List upgradable packages"))
.arg(arg!([GLOB_PATTERN] "Package name filtering"))
)
.subcommand(
Command::new("info")
.about("Show package information")
.arg(arg!(--files "Show filelist for installed packages"))
.arg(arg!(--scripts "Show install scriptlets for installed packages"))
.arg(arg!(--"store-path" "Show store path for installed packages"))
.arg(arg!(<PACKAGE_SPEC> ... "Package specifications to show info for").required(true))
.arg_required_else_help(true)
)
.subcommand(
Command::new("install")
.about("Install packages")
.arg(arg!(--"install-suggests" "Consider suggested packages as a dependency for installing"))
.arg(arg!(--"no-install-recommends" "Do not consider recommended packages as a dependency for installing"))
.arg(arg!(--"no-install-essentials" "Do not automatically install essential packages"))
.arg(arg!(--"no-install" <PACKAGES> "Packages to exclude from installation (comma-separated list, use -pkgname to remove from list)").value_delimiter(','))
.arg(arg!(--"prefer-low-version" "Prefer lower/older versions when multiple candidates are available"))
.arg(arg!(--"ignore-file-conflicts" "Ignore file conflicts and continue installation (dangerous, may break system)"))
.arg(arg!(<PACKAGE_SPEC> ... "Package specifications to install (can be package names, local .rpm/.deb files, or URLs to package files)"))
.arg_required_else_help(true)
)
.subcommand(
Command::new("upgrade")
.about("Upgrade packages")
.arg(arg!(--full "Full upgrade: upgrade all packages, not just those in world.json"))
.arg(arg!([PACKAGE_SPEC] ... "Package specifications to upgrade"))
)
.subcommand(
Command::new("remove")
.about("Remove packages")
.arg(arg!(<PACKAGE_SPEC> ... "Package specifications to remove"))
.arg_required_else_help(true)
)
}
fn add_history_and_utility_subcommands(cmd: Command) -> Command {
cmd.subcommand(
Command::new("history")
.about("Show environment history")
.arg(arg!([MAX_GENERATIONS] "Maximum number of generations to show").value_parser(clap::value_parser!(u32)))
)
.subcommand(
Command::new("restore")
.about("Restore environment to a specific generation")
.arg(arg!(<GEN_ID> "Generation ID to restore to (negative number for relative rollback)").value_parser(clap::value_parser!(i32)).allow_negative_numbers(true))
.arg_required_else_help(true)
)
.subcommand(
Command::new("update")
.about("Update package metadata")
.arg(arg!(--"need-files" "Download filelists (needed for file/path search)"))
)
.subcommand(
Command::new("repo")
.about("Repository management")
.subcommand(Command::new("list").about("List all available repositories"))
)
.subcommand(
Command::new("hash")
.about("Compute binary package hash")
.arg(arg!(<PACKAGE_STORE_DIR> ... "Package store dir to compute hash"))
.arg_required_else_help(true)
)
.subcommand(
Command::new("build")
.about("Build package from source (not ready)")
.arg(arg!(<PACKAGE_YAML> "Package YAML file to build"))
.arg_required_else_help(true)
)
.subcommand(
Command::new("unpack")
.about("Unpack package file(s) into a store directory")
.arg(arg!(<PACKAGE_FILE> ... "Package files to unpack").required(true))
.arg_required_else_help(true)
)
.subcommand(
Command::new("convert")
.about("Convert rpm/deb/apk/... packages to epkg format (not ready)")
.arg(arg!(--"out-dir" <OUTPUT_DIR> "Output directory").default_value("."))
.arg(arg!(--"origin-url" <ORIGIN_URL> "Where the package originated from").required(true))
.arg(arg!(<PACKAGE_FILE>... "Package files to convert (RPM, DEB, APK, etc.)").required(true))
.arg_required_else_help(true)
)
}
fn add_search_and_gc_subcommands(cmd: Command) -> Command {
cmd.subcommand(
Command::new("search")
.about("Search for packages and files")
.after_help(
r#"Examples:
epkg search --files ".desktop" # match literal substring
epkg search --files "*.desktop" # glob pattern, produces fewer results: file name must end with .desktop
epkg search --paths "**/*.desktop" # glob pattern, produces same results
epkg search --paths '\.desktop$' -x # regex pattern, produces same results
epkg search bash --in pkgname # match only in pkgname field
epkg search bash --in pkgname,summary # match in pkgname or summary
epkg search bash --format '${pkgname}\t${version}\t${summary}'
epkg search bash --format json # JSON output
epkg search bash --limit 10 # limit to 10 results
Note: Output order may vary between runs due to parallel optimization (results shown as found).
"#)
.arg(arg!(-f --files "Search in file names"))
.arg(arg!(-p --paths "Search in full paths"))
.arg(arg!(-x --regexp "Pattern is regular expression, refer to https://docs.rs/regex/latest/regex/#syntax"))
.arg(arg!(-i --"ignore-case" "Case-insensitive search"))
.arg(arg!(--in <FIELDS> "Fields to match in, comma-separated (e.g. pkgname,summary). Default: match in all fields"))
.arg(arg!(--format <FORMAT> "Output format: '${field[;width]}' syntax or 'json'. Default: '${pkgname} - ${summary}'"))
.arg(arg!(--limit <N> "Limit number of results").value_parser(clap::value_parser!(usize)))
.arg(arg!(<PATTERN> "Pattern to search for"))
.arg_required_else_help(true)
)
.subcommand(
Command::new("gc")
.about("Garbage collection - clean up unused cache and store files")
.arg(arg!(--"old-downloads" <DAYS> "Remove download files older than DAYS (0 = all files)")
.value_parser(clap::value_parser!(u64)))
)
.subcommand(
Command::new("vm")
.about("VM lifecycle management")
.subcommand_required(true)
.arg_required_else_help(true)
.subcommand(
Command::new("start")
.about("Start VM for environment")
.arg(arg!([ENV] "Environment name (or use --root)"))
.arg(arg!(-s --set <KV> "Set VM config: key=value (timeout, extend, cpus, memory)")
.action(ArgAction::Append))
.arg(arg!(--vmm <BACKEND> "VMM backend: libkrun or qemu")
.value_parser(["libkrun", "qemu"]))
)
.subcommand(
Command::new("stop")
.about("Stop VM")
.arg(arg!([ENV] "Environment name (or use --root)"))
)
.subcommand(
Command::new("list")
.about("List running VMs")
)
.subcommand(
Command::new("status")
.about("Show VM status (YAML)")
.arg(arg!([ENV] "Environment name (or use --root)"))
)
)
}
fn add_run_subcommand(cmd: Command) -> Command {
cmd.subcommand(
Command::new("run")
.about("Run command in environment namespace")
.arg_required_else_help(true)
.arg(arg!(-m --mount <SPEC> "Mount specification (JSON or Docker-like: [HOST_DIR|FS_TYPE:]SANDBOX_DIR[:OPTIONS])").value_name("SPEC").action(ArgAction::Append))
.arg(arg!(-u --user <USER> "Run as specified user (username or UID)"))
.arg(arg!(--isolate <MODE> "Sandbox mode: env (default), fs, or vm").value_parser(["env", "fs", "vm"]))
.arg(
arg!(--"namespace-strategy" <STRATEGY> "Namespace creation strategy: clone (default) or unshare (no extra child)")
.value_parser(["clone", "unshare"])
)
.arg(
arg!(--vmm <ORDER> "Preferred VMM backend order for --isolate=vm (comma-separated, e.g. 'libkrun,qemu' or 'qemu')")
.value_parser(clap::value_parser!(String))
)
.arg(
arg!(--kernel <KERNEL> "External kernel image to use for VM sandbox backends")
.value_parser(clap::value_parser!(String))
)
.arg(
arg!(--"kernel-args" <ARGS> "Extra kernel command line arguments for VM backends")
.value_parser(clap::value_parser!(String))
)
.arg(
arg!(--initrd <INITRD> "Initrd image to use for VM sandbox backends")
.value_parser(clap::value_parser!(String))
)
.arg(
arg!(--cpus <CPUS> "Number of virtual CPUs for --isolate=vm")
.value_parser(clap::value_parser!(String))
)
.arg(
arg!(--memory <SIZE> "Virtual memory size for --isolate=vm (e.g. 4096M, 4G)")
.value_parser(clap::value_parser!(String))
)
.arg(arg!(--timeout <SECONDS> "Timeout in seconds (0 = no timeout)").value_parser(clap::value_parser!(String)))
.arg(
Arg::new("vm-keep-timeout")
.long("vm-keep-timeout")
.value_name("SECS")
.help("With --isolate=vm: after each command exits, wait up to SECS seconds for another connection. None = one-shot VM (immediate shutdown). 0 = never timeout.")
.value_parser(clap::value_parser!(u32))
)
.arg(
Arg::new("translate-uid")
.long("translate-uid")
.value_name("SPEC")
.help("UID mapping for --isolate=vm (e.g., 'map:0:501:1' or 'squash-guest:0:501:65536', can be repeated)")
.action(ArgAction::Append)
)
.arg(
Arg::new("translate-gid")
.long("translate-gid")
.value_name("SPEC")
.help("GID mapping for --isolate=vm (e.g., 'map:0:20:1' or 'squash-guest:0:20:65536', can be repeated)")
.action(ArgAction::Append)
)
.arg(
Arg::new("io")
.short('i')
.long("io")
.value_name("MODE")
.help("I/O mode: auto (default), tty (PTY), stream (progressive), or batch (one-shot)")
.value_parser(["auto", "tty", "stream", "batch"])
)
.arg(arg!(<command> "Command to execute"))
.arg(arg!([args] ... "Arguments to pass to the command (use '--' to separate from epkg options)"))
.allow_hyphen_values(true)
.trailing_var_arg(true)
.after_long_help(
r#"ENVIRONMENT SELECTION (in order of precedence):
1. Explicit selection via command line flags:
• -e, --env <ENV_NAME> Select environment by name (e.g., "myenv" or "owner/myenv")
• -r, --root <DIR> Select the environment by root dir
2. If no command line flags are provided:
• EPKG_ACTIVE_ENV environment variable (if set)
• /etc/epkg/env.yaml configuration file (if exists)
3. Auto-detection (only for 'epkg run' when no environment selected above):
• Path detection: Command is treated as a path if it contains '/' or exists as a file
• If command is a path: Search upward for .eenv directory starting from command's parent
- .eenv with valid config → Use resolved environment name
- .eenv without config → Use .eenv directory path as environment
- No .eenv found → Use MAIN_ENV (default environment)
• If command is not a path and no .eenv found: Search registered environments for command
- Command found → Use environment containing the command
- Command not found → Use MAIN_ENV (default environment)
Use '--' to separate epkg options from command arguments when needed.
MOUNT SPECIFICATION:
Mount specification (JSON or Docker-like syntax):
• -m, --mount <SPEC> Mount specification (can be repeated)
Syntax: [<HOST_DIR|FS_TYPE>:]SANDBOX_DIR[:OPTIONS]
• HOST_DIR: Absolute host path (may start with '@' for env_root substitution)
• FS_TYPE: Pseudo filesystem type (tmpfs, proc, devtmpfs, devpts, mqueue, etc.) or "remount"
• SANDBOX_DIR: Absolute path inside sandbox (may start with '@' for env_root substitution)
• OPTIONS: Comma-separated key=value pairs and flags (ro, try, recursive, silent, etc.)
Path format rules:
• Leading '/' → absolute host path (e.g., /usr means the host's /usr)
• Leading '@' → substitute with environment root (e.g., @/tmp means $env_root/tmp)
• Paths must be absolute (start with '/' or '@'). Relative paths are not allowed.
Examples:
# Bind mount host /data and /config to sandbox /data, /config
epkg run -m /data -m /config node app.js
# Bind mount host /usr read‑only, skip if source missing
epkg run -m /usr:ro,try python
# Mount tmpfs on sandbox /tmp
epkg run -m tmpfs:/tmp:mode=0755 bash
# Mount proc filesystem on sandbox /proc
epkg run -m proc:/proc bash
# Remount existing mount as read-only
epkg run -m remount:/sys/kernel/security:ro bash
# Advanced mount specification (JSON)
epkg run -m '{"target":"/sys/kernel/security","flags":32768,"options":"ro"}' bash
EXAMPLES:
# Run command with auto-detected environment
epkg run ./script.sh # Searches for .eenv in script's directory
epkg run python # Searches registered environments for 'python'
epkg run /usr/local/bin/myapp # Searches for .eenv in /usr/local/bin
# Run command with explicit environment
epkg run -e myenv python
epkg run -r /path/to/env bash
# Run with additional mounts and user
epkg run -m /data:/data -m /config:/config -u appuser node server.js
# Separate epkg options from command arguments
epkg run -- jq --jq-option # Use '--' when command arguments start with '-'
"#)
)
}
fn add_busybox_subcommand(cmd: Command) -> Command {
let width = crate::busybox::terminal_width();
let width = if width == 0 { 80 } else { width };
let mut commands_list = String::new();
commands_list.push_str("Commands:\n");
commands_list.push_str(&crate::busybox::format_applet_list_compact(width));
cmd.subcommand(
Command::new("busybox")
.about("Run built-in command implementations")
.arg_required_else_help(true)
.allow_external_subcommands(true)
.arg(arg!(--list "List all available applets"))
.after_help(commands_list)
)
}
fn add_service_subcommand(cmd: Command) -> Command {
cmd.subcommand(
Command::new("service")
.about("Service management - start/stop/restart/status/reload services")
.arg_required_else_help(true)
.subcommand(
Command::new("start")
.about("Start a service")
.arg(arg!(<SERVICE_NAME> "Name of the service to start (without .service extension)"))
)
.subcommand(
Command::new("stop")
.about("Stop a service")
.arg(arg!(<SERVICE_NAME> "Name of the service to stop (without .service extension)"))
)
.subcommand(
Command::new("status")
.about("Show service status")
.arg(arg!(--all "Show status for all services across all environments"))
.arg(arg!([SERVICE_NAME] "Name of the service to check (without .service extension)"))
)
.subcommand(
Command::new("reload")
.about("Reload a service")
.arg(arg!(<SERVICE_NAME> "Name of the service to reload (without .service extension)"))
)
.subcommand(
Command::new("restart")
.about("Restart a service")
.arg(arg!(<SERVICE_NAME> "Name of the service to restart (without .service extension)"))
)
)
}
fn build_epkg_command() -> Command {
let cmd = Command::new("epkg");
let cmd = add_global_args_and_help(cmd);
let cmd = add_self_subcommand(cmd);
let cmd = add_env_subcommand(cmd);
let cmd = add_package_operation_subcommands(cmd);
let cmd = add_history_and_utility_subcommands(cmd);
let cmd = add_run_subcommand(cmd);
let cmd = add_busybox_subcommand(cmd);
let cmd = add_search_and_gc_subcommands(cmd);
let cmd = add_service_subcommand(cmd);
cmd
}
pub fn parse_cmdline() -> clap::ArgMatches {
let args: Vec<String> = env::args_os()
.map(|a| a.to_string_lossy().into_owned())
.collect();
match build_epkg_command().try_get_matches_from(args.clone()) {
Ok(matches) => matches,
Err(e) => crate::utils::handle_clap_error_with_cmdline(e, args.join(" ")),
}
}
pub fn parse_cmdline_from(args: Vec<String>) -> clap::ArgMatches {
#[cfg(target_os = "linux")]
let _ = crate::busybox::init::kmsg_write("<6>parse_cmdline_from: started\n");
#[cfg(target_os = "linux")]
let _ = crate::busybox::init::kmsg_write("<6>parse_cmdline_from: calling build_epkg_command\n");
let cmd = build_epkg_command();
#[cfg(target_os = "linux")]
let _ = crate::busybox::init::kmsg_write("<6>parse_cmdline_from: build_epkg_command returned\n");
#[cfg(target_os = "linux")]
let _ = crate::busybox::init::kmsg_write("<6>parse_cmdline_from: calling try_get_matches_from\n");
match cmd.try_get_matches_from(args.clone()) {
Ok(matches) => {
#[cfg(target_os = "linux")]
let _ = crate::busybox::init::kmsg_write("<6>parse_cmdline_from: try_get_matches_from OK\n");
matches
},
Err(e) => {
#[cfg(target_os = "linux")]
let _ = crate::busybox::init::kmsg_write("<6>parse_cmdline_from: try_get_matches_from ERR\n");
crate::utils::handle_clap_error_with_cmdline(e, args.join(" "))
},
}
}
fn load_config_from_matches(matches: &clap::ArgMatches) -> Result<EPKGConfig> {
let config = matches.get_one::<String>("config").map_or_else(
|| {
let default_config_path = crate::dirs::path_join(
&PathBuf::from(dirs::get_home()?),
&[".epkg", "config", "options.yaml"],
);
if default_config_path.exists() {
read_yaml_file(&default_config_path)
} else {
Ok(serde_yaml::from_str("{}")
.unwrap_or_else(|e| panic!("Failed to load default config from empty map: {:?}", e)))
}
},
|s| read_yaml_file(Path::new(s)),
)?;
Ok(config)
}
fn set_arch_and_validate(matches: &clap::ArgMatches, config: &mut EPKGConfig) -> Result<()> {
if let Some(arch) = matches.get_one::<String>("arch") {
config.common.arch = arch.to_string();
}
if config.common.arch.is_empty() {
config.common.arch = models::default_arch();
eprintln!("arch was configured to empty, using default architecture: {}", config.common.arch);
}
if !SUPPORT_ARCH_LIST.contains(&config.common.arch.as_str()) {
return Err(eyre::eyre!("Unsupported system architecture: {}", config.common.arch));
}
Ok(())
}
fn set_common_flags(matches: &clap::ArgMatches, config: &mut EPKGConfig) {
config.common.dry_run = matches.get_flag("dry-run");
config.common.download_only = matches.get_flag("download-only");
if matches.contains_id("quiet") {
config.common.quiet = matches.get_flag("quiet");
}
if matches.contains_id("verbose") {
config.common.verbose = matches.get_flag("verbose");
}
if matches.contains_id("assume-yes") {
config.common.assume_yes = matches.get_flag("assume-yes");
}
if matches.contains_id("assume-no") {
config.common.assume_no = matches.get_flag("assume-no");
}
if matches.contains_id("ignore-missing") {
config.common.ignore_missing = matches.get_flag("ignore-missing");
}
}
fn set_command_line_and_subcommand(matches: &clap::ArgMatches, config: &mut EPKGConfig) -> Result<()> {
let args: Vec<String> = std::env::args_os()
.map(|a| a.to_string_lossy().into_owned())
.collect();
let command_line = if args.len() > 1 {
"epkg ".to_owned() + &args[1..].join(" ")
} else {
String::new()
};
config.command_line = command_line;
config.subcommand = EpkgCommand::from(matches.subcommand_name().unwrap_or(""));
if config.subcommand != EpkgCommand::SelfInstall {
config.init.shared_store = utils::determine_shared_store()
.wrap_err("Failed to determine shared_store mode")?;
}
Ok(())
}
fn set_metadata_expire_and_proxy(matches: &clap::ArgMatches, config: &mut EPKGConfig) {
if config.subcommand == EpkgCommand::Remove ||
config.subcommand == EpkgCommand::Restore {
config.common.metadata_expire = 0;
} else if let Some(metadata_expire) = matches.get_one::<i32>("metadata-expire") {
config.common.metadata_expire = *metadata_expire;
}
if let Some(proxy) = matches.get_one::<String>("proxy") {
config.common.proxy = proxy.to_string();
}
}
fn setup_parallel_params(config: &mut EPKGConfig, matches: &clap::ArgMatches) {
if let Some(nr_retry) = matches.get_one::<usize>("retry") {
config.common.nr_retry = *nr_retry;
}
if let Some(nr) = matches.get_one::<usize>("parallel-download") {
config.common.nr_parallel_download = if *nr == 0 { 1 } else { *nr };
}
let nr_cpus = num_cpus::get().max(1);
if let Some(parallel_processing) = matches.get_one::<usize>("parallel-processing") {
config.common.parallel_processing = (*parallel_processing).clamp(1, nr_cpus);
} else {
config.common.parallel_processing = config.common.parallel_processing.clamp(1, nr_cpus);
}
}
pub fn parse_options_common(matches: &clap::ArgMatches) -> Result<EPKGConfig> {
let mut config = load_config_from_matches(matches)?;
determine_environment_explicit(matches, &mut config);
set_arch_and_validate(matches, &mut config)?;
set_common_flags(matches, &mut config);
set_command_line_and_subcommand(matches, &mut config)?;
set_metadata_expire_and_proxy(matches, &mut config);
setup_parallel_params(&mut config, matches);
Ok(config)
}
#[derive(Debug, PartialEq)]
enum EnvNameType {
Name,
OwnerName,
Path,
}
fn classify_env_name(env_name: &str) -> EnvNameType {
if env_name.starts_with('/') || env_name.starts_with("./") || env_name.starts_with("../") || env_name.ends_with('/') {
return EnvNameType::Path;
}
if let Some(slash_pos) = env_name.find('/') {
if env_name[slash_pos + 1..].contains('/') {
return EnvNameType::Path;
}
return EnvNameType::OwnerName;
}
EnvNameType::Name
}
fn validate_env_name(env_name: &str) -> Result<()> {
let classification = classify_env_name(env_name);
match classification {
EnvNameType::Path => {
return Err(eyre::eyre!(
"Environment name '{}' looks like a path. Use '-r {}' for root dir selection.",
env_name, env_name
));
}
EnvNameType::Name | EnvNameType::OwnerName => {}
}
if env_name.is_empty() {
return Err(eyre::eyre!("Environment name cannot be empty"));
}
if env_name.contains('\0') {
return Err(eyre::eyre!("Environment name cannot contain null character"));
}
if env_name.contains("..") {
return Err(eyre::eyre!("Environment name cannot contain .."));
}
let invalid_chars = [' ', '\t', '\\', ':', '*', '?', '"', '<', '>', '|'];
if let Some(ch) = env_name.chars().find(|c| invalid_chars.contains(c)) {
return Err(eyre::eyre!("Environment name contains invalid character '{}'", ch));
}
// Additional validation for owner/name format
if classification == EnvNameType::OwnerName {
let parts: Vec<&str> = env_name.split('/').collect();
if parts.len() != 2 {
return Err(eyre::eyre!("Owner/name format must be exactly 'owner/name'"));
}
let owner = parts[0];
let name = parts[1];
if owner.is_empty() || name.is_empty() {
return Err(eyre::eyre!("Owner and name parts cannot be empty"));
}
// Validate each part doesn't contain additional slashes (already ensured)
}
Ok(())
}
fn env_name_from_path(dir: &str) -> String {
let abs_dir = crate::utils::to_absolute_path(dir);
let trimmed = abs_dir.trim_matches(|c| c == '/' || c == '\\');
if trimmed.is_empty() {
return "sysroot".to_string();
}
// Replace both '/' and '\\' with "__", and ':' with "_" (for Windows drive letters)
let with_underscores = trimmed
.replace('/', "__")
.replace('\\', "__")
.replace(':', "_");
// Ensure name starts with '__' to mark as auto-generated
if with_underscores.starts_with("__") {
with_underscores
} else {
format!("__{}", with_underscores)
}
}
/// Try to infer channel from environment name.
///
/// This function checks if the env_name matches a valid channel pattern:
/// 1. Exact match: env_name is a channel name (e.g., "alpine", "conda")
/// 2. Channel-version: env_name is "{channel}-{version}" (e.g., "fedora-42", "ubuntu-noble")
///
/// Returns Some(channel_string) if successfully inferred, None otherwise.
fn infer_channel_from_env_name(env_name: &str) -> Option<String> {
// Get the assets/repos path
let repos_path = path_join(&get_epkg_src_path(), &["assets", "repos"]);
// Strategy 1: Check for exact channel match (e.g., "alpine", "fedora", "ubuntu")
let exact_channel_path = repos_path.join(format!("{}.yaml", env_name));
if exact_channel_path.exists() {
return Some(env_name.to_string());
}
// Strategy 2: Try to parse as "{distro}-{version}" format
// Split by '-' and try different split points from right to left
// This handles cases like "fedora-42", "ubuntu-noble", "debian-trixie"
let parts: Vec<&str> = env_name.split(CHANNEL_SEPARATOR).collect();
if parts.len() < 2 {
return None;
}
// Try each possible split point (from right to left to handle version with '-')
// For "fedora-42": try distro="fedora", version="42"
// For "debian-bookworm": try distro="debian", version="bookworm"
for split_idx in 1..parts.len() {
let distro = parts[..split_idx].join(&CHANNEL_SEPARATOR.to_string());
let version = parts[split_idx..].join(&CHANNEL_SEPARATOR.to_string());
// Check if distro.yaml exists
let distro_path = repos_path.join(format!("{}.yaml", distro));
if !distro_path.exists() {
continue;
}
// Load the channel config to check if version is valid
if let Ok(channel_config) = read_yaml_file::<ChannelConfig>(&distro_path) {
// Check if version matches any version or alias in versions list
// versions format: "noble 24.04" where first is standard name, rest are aliases
let version_matches = channel_config.versions.iter().any(|v| {
v.split_whitespace().any(|alias| alias == version)
});
if version_matches {
return Some(format!("{}{}{}", distro, CHANNEL_SEPARATOR, version));
}
}
}
None
}
/// Load env.yaml from a path (either env root dir, or "/" when config is at /etc/epkg/env.yaml)
/// and apply to config and ENV_CONFIG so get_env_config_path() and env_config() use it.
/// Set in_env_root only when loading from "/" (we're inside the env); when loading from -r PATH
fn apply_env_config_from_path(env_root_or_etc: &Path, config: &mut EPKGConfig) -> Result<()> {
if env_root_or_etc == Path::new("/") && std::env::consts::OS != "linux" {
log::debug!("apply_env_config_from_path: skipping '/' path on {} (runtime guard)", std::env::consts::OS);
return Err(eyre::eyre!(
"Cannot read /etc/epkg/env.yaml on {} platform. \
This indicates a bug in environment detection logic. \
On Windows/macOS, use -e <env_name> or -r <path> to select environment.",
std::env::consts::OS
));
}
let config_path = if env_root_or_etc == Path::new("/") {
env_root_env_yaml(Path::new("/"))
} else {
env_root_env_yaml(env_root_or_etc)
};
if !config_path.exists() {
return Err(eyre::eyre!("Environment config not found: {}", config_path.display()));
}
let env_config_data = read_yaml_file::<EnvConfig>(&config_path)?;
config.common.env_name = env_config_data.name.clone();
config.common.env_explicit = true;
config.common.in_env_root = env_root_or_etc == Path::new("/");
#[cfg(target_os = "linux")]
if env_root_or_etc == Path::new("/") && crate::busybox::is_inside_vm() {
log::debug!("apply_env_config_from_path: loading /etc/epkg/env.yaml inside VM, using '/' as env_root");
config.common.env_root = "/".to_string();
} else {
config.common.env_root = env_config_data.env_root.clone();
}
#[cfg(not(target_os = "linux"))]
{
config.common.env_root = env_config_data.env_root.clone();
}
let env_config = models::EnvConfig {
name: env_config_data.name.clone(),
env_root: config.common.env_root.clone(),
env_base: config.common.env_root.clone(),
..Default::default()
};
let _ = set_env_config(env_config);
Ok(())
}
pub fn resolve_env_root(env_root: &str) -> Result<String> {
let env_root = std::fs::canonicalize(env_root).map_err(|e| {
eyre::eyre!(
"Environment root '{}' does not exist or cannot be accessed: {}",
env_root,
e
)
})?;
if !env_root.is_dir() {
return Err(eyre::eyre!(
"Environment root '{}' is not a directory",
env_root.display()
));
}
let config_path = env_root_env_yaml(env_root.as_path());
if !config_path.exists() {
return Err(eyre::eyre!(
"Environment not found at path: {}\n (missing configuration file: {})",
env_root.display(),
config_path.display()
));
}
let env_config = io::read_yaml_file::<EnvConfig>(&config_path)?;
Ok(env_config.name)
}
fn determine_environment_explicit(matches: &clap::ArgMatches, config: &mut EPKGConfig) -> bool {
if let Some(env_arg) = matches.get_one::<String>("env") {
config.common.env_name = env_arg.to_string();
config.common.env_explicit = true;
config.common.env_name_explicit = true;
return true;
}
if let Some(dir) = matches.get_one::<String>("root") {
let abs_dir = crate::utils::to_absolute_path(dir);
config.common.env_root = abs_dir;
config.common.env_explicit = true;
return true;
}
false
}
fn try_detect_environment_from_env_yaml(_config: &mut EPKGConfig) -> Result<bool> {
#[cfg(not(target_os = "linux"))]
{
if std::env::consts::OS != "linux" {
log::debug!("try_detect_environment_from_env_yaml: skipping, not on Linux (runtime check)");
return Ok(false);
}
}
#[cfg(target_os = "linux")]
{
let root_env_yaml = env_root_env_yaml(Path::new("/"));
if !root_env_yaml.exists() {
return Ok(false);
}
apply_env_config_from_path(Path::new("/"), _config)?;
log::debug!("env: from /etc/epkg/env.yaml -> {}", _config.common.env_name);
Ok(true)
}
#[cfg(not(target_os = "linux"))]
{
Ok(false)
}
}
fn try_apply_explicit_env_root(config: &mut EPKGConfig) -> Result<bool> {
if config.common.env_root.is_empty() {
return Ok(false);
}
let env_root_path = config.common.env_root.clone();
let config_path = if env_root_path == "/" {
env_root_env_yaml(Path::new("/"))
} else {
env_root_env_yaml(Path::new(&env_root_path))
};
let is_env_create = config.subcommand == EpkgCommand::EnvCreate;
if config_path.exists() {
if is_env_create {
return Err(eyre::eyre!(
"Environment already exists at path: {}",
env_root_path
));
}
if config.common.env_name_explicit {
eprintln!("Both options '-e {}' and '-r {}' are given, using -r for selecting environment.",
config.common.env_name,
config.common.env_root);
}
apply_env_config_from_path(Path::new(&env_root_path), config)?;
log::debug!("env: explicit -r {} -> {}", env_root_path, config.common.env_name);
} else if is_env_create && !config.common.env_name.is_empty() {
log::debug!("env: explicit -r for create name={} root={}", config.common.env_name, env_root_path);
} else {
return Err(eyre::eyre!("Environment config not found: {}", config_path.display()));
}
Ok(true)
}
fn try_env_from_epkg_activenv(config: &mut EPKGConfig) -> bool {
let Ok(active_env) = env::var("EPKG_ACTIVE_ENV") else {
return false;
};
let env_name = active_env.split(':').next().unwrap_or(&active_env);
let env_name = env_name.trim_end_matches(PURE_ENV_SUFFIX);
if config.common.env_name_explicit && !config.common.env_name.is_empty() && config.common.env_name != env_name {
return false;
}
config.common.env_name = env_name.to_string();
config.common.env_explicit = true;
log::debug!("env: from EPKG_ACTIVE_ENV -> {}", config.common.env_name);
true
}
fn try_env_from_run_command(config: &mut EPKGConfig) -> Result<bool> {
if config.run.command.is_empty() || config.subcommand != EpkgCommand::Run {
return Ok(false);
}
let command = config.run.command.clone();
let (is_path, search_dir) = determine_command_path_info(&command);
if let Some(dot_eenv) = find_nearest_dot_eenv(&search_dir) {
set_env_name_by_path(&dot_eenv, config)?;
log::debug!("env: from run command .eenv at {} -> {}", dot_eenv.display(), config.common.env_name);
} else if !is_path {
search_registered_envs(&command, config);
if !config.common.env_name.is_empty() {
log::debug!("env: from run command (registered) -> {}", config.common.env_name);
}
}
Ok(!config.common.env_name.is_empty())
}
fn try_env_from_cwd_dot_eenv(config: &mut EPKGConfig) -> Result<bool> {
let Ok(cwd) = std::env::current_dir() else {
return Ok(false);
};
let Some(dot_eenv) = find_nearest_dot_eenv(&cwd) else {
return Ok(false);
};
set_env_name_by_path(&dot_eenv, config)?;
log::debug!("env: from cwd .eenv at {} -> {}", dot_eenv.display(), config.common.env_name);
Ok(true)
}
fn determine_environment_final(config: &mut EPKGConfig) -> Result<()> {
if try_apply_explicit_env_root(config)? {
return Ok(());
}
if config.subcommand == EpkgCommand::Run && !config.run.command.is_empty() {
let command = config.run.command.clone();
let (is_path, search_dir) = determine_command_path_info(&command);
if is_path {
if let Some(dot_eenv) = find_nearest_dot_eenv(&search_dir) {
set_env_name_by_path(&dot_eenv, config)?;
log::debug!("env: from run path .eenv at {} -> {}", dot_eenv.display(), config.common.env_name);
return Ok(());
}
} else if !config.common.env_name_explicit {
search_registered_envs(&command, config);
if !config.common.env_name.is_empty() {
log::debug!("env: from run command (registered) -> {}", config.common.env_name);
return Ok(());
}
}
}
if try_env_from_epkg_activenv(config) {
return Ok(());
}
if !config.common.env_name_explicit || config.common.env_name.is_empty() {
if try_detect_environment_from_env_yaml(config)? {
return Ok(());
}
}
if config.common.env_name_explicit && !config.common.env_name.is_empty() {
log::debug!(
"env: explicit -e {}, skipping further auto-detection",
config.common.env_name
);
return Ok(());
}
if !config.common.env_name.is_empty() {
log::debug!("env: using existing env_name -> {}", config.common.env_name);
return Ok(());
}
if try_env_from_run_command(config)? {
return Ok(());
}
if try_env_from_cwd_dot_eenv(config)? {
return Ok(());
}
log::debug!("env: fallback to MAIN_ENV -> {}", MAIN_ENV);
config.common.env_name = MAIN_ENV.to_string();
config.common.env_explicit = false;
Ok(())
}
fn determine_command_path_info(command: &str) -> (bool, PathBuf) {
let cwd = std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."));
if command.contains('/') || command.contains('\\') {
// Command contains slash: treat as path
let cmd_path = Path::new(command);
let parent = cmd_path.parent().unwrap_or(&cwd);
let search_dir = if parent.is_absolute() {
parent.to_path_buf()
} else {
// Join relative parent with current working directory to get absolute path
cwd.join(parent)
};
log::debug!("determine_command_path_info: command='{}', parent='{}', search_dir='{}'",
command, parent.display(), search_dir.display());
(true, search_dir)
} else if Path::new(command).exists() {
// Command exists as a file in current directory: treat as path
log::debug!("determine_command_path_info: command='{}' exists in cwd, search_dir='{}'",
command, cwd.display());
(true, cwd.clone())
} else {
// Command not a path and not a file: we're clueless
log::debug!("determine_command_path_info: command='{}' not a path, search_dir='{}'",
command, cwd.display());
(false, cwd.clone())
}
}
fn set_env_name_by_path(dot_eenv: &Path, options: &mut EPKGConfig) -> Result<()> {
let resolved_name = resolve_env_root(dot_eenv.to_string_lossy().as_ref())?;
options.common.env_name = resolved_name;
options.common.env_root = dot_eenv.to_string_lossy().to_string();
Ok(())
}
fn search_registered_envs(command: &str, options: &mut EPKGConfig) {
let shared_store = options.init.shared_store;
if let Ok(Some((env_name, env_root))) = find_command_in_registered_envs(command, shared_store) {
options.common.env_name = env_name;
options.common.env_root = env_root.to_string_lossy().to_string();
}
}
pub fn parse_options_subcommand(matches: &clap::ArgMatches, mut config: EPKGConfig) -> Result<EPKGConfig> {
match matches.subcommand() {
Some(("self", sub_matches)) => parse_options_self(&mut config, sub_matches).expect("Failed to parse self options"),
Some(("env", sub_matches)) => parse_options_env(&mut config, sub_matches).expect("Failed to parse env options"),
Some(("list", sub_matches)) => parse_options_list(&mut config, sub_matches).expect("Failed to parse list options"),
Some(("info", sub_matches)) => parse_options_info(&mut config, sub_matches).expect("Failed to parse info options"),
Some(("install", sub_matches)) => parse_options_install(&mut config, sub_matches).expect("Failed to parse install options"),
Some(("upgrade", sub_matches)) => parse_options_upgrade(&mut config, sub_matches).expect("Failed to parse upgrade options"),
Some(("remove", sub_matches)) => parse_options_remove(&mut config, sub_matches).expect("Failed to parse remove options"),
Some(("history", sub_matches)) => parse_options_history(&mut config, sub_matches).expect("Failed to parse history options"),
Some(("restore", sub_matches)) => parse_options_restore(&mut config, sub_matches).expect("Failed to parse restore options"),
Some(("update", sub_matches)) => parse_options_update(&mut config, sub_matches).expect("Failed to parse update options"),
Some(("repo", sub_matches)) => parse_options_repo(&mut config, sub_matches).expect("Failed to parse repo options"),
Some(("hash", sub_matches)) => parse_options_hash(&mut config, sub_matches).expect("Failed to parse hash options"),
Some(("build", sub_matches)) => parse_options_build(&mut config, sub_matches).expect("Failed to parse build options"),
Some(("unpack", sub_matches)) => parse_options_unpack(&mut config, sub_matches).expect("Failed to parse unpack options"),
Some(("convert", sub_matches)) => parse_options_convert(&mut config, sub_matches).expect("Failed to parse convert options"),
Some(("run", sub_matches)) => crate::run::parse_options_run(&mut config, sub_matches).expect("Failed to parse run options"),
Some(("search", sub_matches)) => parse_options_search(&mut config, sub_matches).expect("Failed to parse search options"),
Some(("service", sub_matches)) => parse_options_service(&mut config, sub_matches).expect("Failed to parse service options"),
Some(("vm", sub_matches)) => parse_options_vm(&mut config, sub_matches).expect("Failed to parse vm options"),
_ => {}
}
determine_environment_final(&mut config)?;
validate_env_name(&config.common.env_name)?;
crate::dirs::init_config_dirs(&mut config)?;
log::trace!("Configuration: {:#?}", config);
log::trace!("dirs: {:#?}", crate::dirs::dirs_ref());
Ok(config)
}
fn parse_options_self(config: &mut EPKGConfig, sub_matches: &clap::ArgMatches) -> Result<()> {
use crate::utils;
config.common.env_name = SELF_ENV.to_string();
config.common.env_explicit = true;
config.common.env_name_explicit = true;
match sub_matches.subcommand() {
Some(("install", sub_matches)) => {
config.subcommand = EpkgCommand::SelfInstall;
config.common.force = sub_matches.get_flag("force");
config.init.shared_store = sub_matches.get_one::<String>("store")
.map(|s| match s.as_str() {
"shared" => true,
"private" => false,
"auto" => utils::is_running_as_root(),
_ => false
})
.unwrap_or_else(|| utils::is_running_as_root());
if let Some(commit) = sub_matches.get_one::<String>("commit") {
config.init.commit = commit.to_string();
} else if config.init.commit.is_empty() {
config.init.commit = models::default_commit();
eprintln!("commit was configured to empty, using default commit: {}", config.init.commit);
}
if let Some(channel) = sub_matches.get_one::<String>("channel") {
config.env.channel = Some(channel.to_string());
}
if let Some(repos) = sub_matches.get_many::<String>("repo") {
config.env.repos = repos.map(|s| s.to_string()).collect();
}
}
Some(("upgrade", _sub_matches)) => {
config.subcommand = EpkgCommand::SelfUpgrade;
}
Some(("remove", _sub_matches)) => {
config.subcommand = EpkgCommand::SelfRemove;
}
_ => {}
}
Ok(())
}
fn parse_options_env(config: &mut EPKGConfig, matches: &clap::ArgMatches) -> Result<()> {
if let Some((subcommand_name, sub_matches)) = matches.subcommand() {
if matches!(subcommand_name, "create" | "remove" | "register" | "unregister" | "activate" | "export") {
if let Some(env_name) = sub_matches.get_one::<String>("ENV_NAME") {
config.common.env_name = env_name.to_string();
config.common.env_explicit = true;
config.common.env_name_explicit = true;
} else if !config.common.env_root.is_empty() &&
config.common.env_name.is_empty() {
config.common.env_name = env_name_from_path(&config.common.env_root);
config.common.env_explicit = true;
}
if !config.common.env_explicit {
eprintln!("error: environment name required");
eprintln!("usage: epkg env {} [ENV_NAME | --root DIR] [OPTIONS]", subcommand_name);
eprintln!("For more information, try 'epkg env {} --help'", subcommand_name);
exit(2);
}
}
match subcommand_name {
"list" => {
config.subcommand = EpkgCommand::EnvList;
}
"create" => {
config.subcommand = EpkgCommand::EnvCreate;
if let Some(channel) = sub_matches.get_one::<String>("channel") {
config.env.channel = Some(channel.to_string());
} else if !config.common.env_name.is_empty() {
if let Some(inferred_channel) = infer_channel_from_env_name(&config.common.env_name) {
config.env.channel = Some(inferred_channel);
}
}
if let Some(repos) = sub_matches.get_many::<String>("repo") {
config.env.repos = repos.map(|s| s.to_string()).collect();
}
if sub_matches.contains_id("public") {
config.env.public = sub_matches.get_flag("public");
}
config.env.import_file = sub_matches.get_one::<String>("import").cloned();
if let Some(link_str) = sub_matches.get_one::<String>("link") {
config.env.link = Some(parse_link_type(link_str.as_str())?);
}
}
"remove" => {
config.subcommand = EpkgCommand::EnvRemove;
}
"register" => {
config.subcommand = EpkgCommand::EnvRegister;
config.env.path_order = sub_matches.get_one::<i32>("path-order").cloned();
}
"unregister" => {
config.subcommand = EpkgCommand::EnvUnregister;
}
"activate" => {
config.subcommand = EpkgCommand::EnvActivate;
if sub_matches.contains_id("pure") {
config.env.pure = sub_matches.get_flag("pure");
}
if sub_matches.contains_id("stack") {
config.env.stack = sub_matches.get_flag("stack");
}
}
"deactivate" => {
config.subcommand = EpkgCommand::EnvDeactivate;
}
"export" => {
config.subcommand = EpkgCommand::EnvExport;
}
"path" => {
config.subcommand = EpkgCommand::EnvPath;
}
"config" => {
match sub_matches.subcommand() {
Some(("edit", _)) => {
config.subcommand = EpkgCommand::EnvConfigEdit;
}
Some(("get", _)) => {
config.subcommand = EpkgCommand::EnvConfigGet;
}
Some(("set", _)) => {
config.subcommand = EpkgCommand::EnvConfigSet;
}
_ => {}
}
}
_ => {}
}
}
Ok(())
}
fn parse_options_list(config: &mut EPKGConfig, sub_matches: &clap::ArgMatches) -> Result<()> {
if sub_matches.get_flag("all") {
config.list.list_all = true;
}
if sub_matches.get_flag("installed") {
config.list.list_installed = true;
}
if sub_matches.get_flag("available") {
config.list.list_available = true;
}
Ok(())
}
fn parse_options_info(_config: &mut EPKGConfig, _sub_matches: &clap::ArgMatches) -> Result<()> {
Ok(())
}
fn parse_options_install(config: &mut EPKGConfig, sub_matches: &clap::ArgMatches) -> Result<()> {
if sub_matches.contains_id("install-suggests") {
config.install.install_suggests = sub_matches.get_flag("install-suggests");
}
if sub_matches.contains_id("no-install-recommends") {
config.install.no_install_recommends = sub_matches.get_flag("no-install-recommends");
}
if sub_matches.contains_id("no-install-essentials") {
config.install.no_install_essentials = sub_matches.get_flag("no-install-essentials");
}
if let Some(no_install) = sub_matches.get_many::<String>("no-install") {
config.install.no_install = no_install.map(|s| s.to_string()).collect::<Vec<_>>().join(",");
}
if sub_matches.contains_id("prefer-low-version") {
config.install.prefer_low_version = sub_matches.get_flag("prefer-low-version");
}
if sub_matches.contains_id("ignore-file-conflicts") {
config.install.ignore_file_conflicts = sub_matches.get_flag("ignore-file-conflicts");
}
Ok(())
}
fn parse_options_upgrade(config: &mut EPKGConfig, sub_matches: &clap::ArgMatches) -> Result<()> {
config.upgrade.full_upgrade = sub_matches.get_flag("full");
Ok(())
}
fn parse_options_remove(_options: &mut EPKGConfig, _sub_matches: &clap::ArgMatches) -> Result<()> {
Ok(())
}
fn parse_options_history(config: &mut EPKGConfig, sub_matches: &clap::ArgMatches) -> Result<()> {
if let Some(max_generations) = sub_matches.get_one::<u32>("MAX_GENERATIONS") {
config.history.max_generations = Some(*max_generations);
}
Ok(())
}
fn parse_options_restore(_options: &mut EPKGConfig, _sub_matches: &clap::ArgMatches) -> Result<()> {
Ok(())
}
fn parse_options_update(config: &mut EPKGConfig, sub_matches: &clap::ArgMatches) -> Result<()> {
config.update.need_files = sub_matches.get_flag("need-files");
Ok(())
}
fn parse_options_repo(_options: &mut EPKGConfig, _sub_matches: &clap::ArgMatches) -> Result<()> {
Ok(())
}
fn parse_options_hash(_options: &mut EPKGConfig, _sub_matches: &clap::ArgMatches) -> Result<()> {
Ok(())
}
fn parse_options_build(_options: &mut EPKGConfig, _sub_matches: &clap::ArgMatches) -> Result<()> {
Ok(())
}
fn parse_options_unpack(_options: &mut EPKGConfig, _sub_matches: &clap::ArgMatches) -> Result<()> {
Ok(())
}
fn parse_options_convert(_config: &mut EPKGConfig, _sub_matches: &clap::ArgMatches) -> Result<()> {
Ok(())
}
fn parse_options_search(config: &mut EPKGConfig, sub_matches: &clap::ArgMatches) -> Result<()> {
let in_fields: Vec<String> = sub_matches
.get_one::<String>("in")
.map(|s| s.split(',').map(|f| f.trim().to_string()).collect())
.unwrap_or_default();
let options = search::SearchOptions {
files: sub_matches.get_flag("files"),
paths: sub_matches.get_flag("paths"),
regexp: sub_matches.get_flag("regexp"),
ignore_case: sub_matches.get_flag("ignore-case"),
origin_pattern: sub_matches.get_one::<String>("PATTERN").unwrap().to_string(),
in_fields,
format: sub_matches.get_one::<String>("format").cloned(),
limit: sub_matches.get_one::<usize>("limit").copied(),
..Default::default()
};
if options.files && options.origin_pattern.contains('/') {
eprintln!("Warning: Using -f|--files flag with pattern '{}' that contains '/'.\nConsider using -p|--paths flag instead for path-based searches.", options.origin_pattern);
exit(0);
}
config.search = options;
Ok(())
}
fn parse_options_service(config: &mut EPKGConfig, sub_matches: &clap::ArgMatches) -> Result<()> {
if let Some(("status", status_matches)) = sub_matches.subcommand() {
config.service.all = status_matches.get_flag("all");
}
Ok(())
}
fn parse_options_vm(config: &mut EPKGConfig, matches: &clap::ArgMatches) -> Result<()> {
if let Some((subcommand_name, sub_matches)) = matches.subcommand() {
if matches!(subcommand_name, "start" | "stop" | "status") {
if let Some(env_name) = sub_matches.get_one::<String>("ENV") {
config.common.env_name = env_name.to_string();
config.common.env_explicit = true;
config.common.env_name_explicit = true;
} else if !config.common.env_root.is_empty() &&
config.common.env_name.is_empty() {
config.common.env_name = env_name_from_path(&config.common.env_root);
config.common.env_explicit = true;
}
if !config.common.env_explicit {
eprintln!("error: environment name required");
eprintln!("usage: epkg vm {} [ENV_NAME | --root DIR]", subcommand_name);
eprintln!("For more information, try 'epkg vm {} --help'", subcommand_name);
exit(2);
}
}
}
Ok(())
}
fn command_env(sub_matches: &clap::ArgMatches) -> Result<()> {
let name = &config().common.env_name;
match sub_matches.subcommand() {
Some(("list", _)) => list_environments(),
Some(("create", _)) => create_environment(name),
Some(("remove", _)) => remove_environment(name),
Some(("register", _)) => register_environment(name),
Some(("unregister", _)) => unregister_environment(name),
Some(("activate", _)) => activate_environment(name),
Some(("deactivate", _)) => deactivate_environment(),
Some(("path", _)) => update_path(),
Some(("export", sub_matches)) => {
let output = sub_matches.get_one::<String>("output").cloned();
export_environment(output)
}
Some(("config", sub_matches)) => {
match sub_matches.subcommand() {
Some(("edit", _)) => edit_environment_config(),
Some(("get", sub_matches)) => {
if let Some(name) = sub_matches.get_one::<String>("NAME") {
get_environment_config(name)
} else {
Ok(())
}
}
Some(("set", sub_matches)) => {
if let (Some(name), Some(value)) = (sub_matches.get_one::<String>("NAME"), sub_matches.get_one::<String>("VALUE")) {
set_environment_config(name, value)
} else {
Ok(())
}
}
_ => Ok(()),
}
}
_ => Ok(()),
}
}
fn command_list(sub_matches: &clap::ArgMatches) -> Result<()> {
let scope = if sub_matches.get_flag("all") { ListScope::All
} else if sub_matches.get_flag("available") { ListScope::Available
} else if sub_matches.get_flag("upgradable") { ListScope::Upgradable
} else { ListScope::Installed
};
let pattern = sub_matches.get_one::<String>("GLOB_PATTERN")
.map(|s| s.as_str())
.unwrap_or("");
if scope != ListScope::Installed {
sync_channel_metadata()?;
}
list_packages_with_scope(scope, pattern)?;
Ok(())
}
fn command_info(sub_matches: &clap::ArgMatches) -> Result<()> {
let mut all_args: Vec<String> = Vec::new();
if let Some(package_specs) = sub_matches.get_many::<String>("PACKAGE_SPEC") {
all_args.extend(package_specs.cloned());
}
let show_files = sub_matches.get_flag("files");
let show_scripts = sub_matches.get_flag("scripts");
let show_store_path = sub_matches.get_flag("store-path");
crate::info::show_package_info(
&all_args,
show_files,
show_scripts,
show_store_path,
)?;
Ok(())
}
fn try_route_command_via_vm(matches: &clap::ArgMatches) -> Result<Option<i32>> {
#[cfg(feature = "libkrun")]
use std::collections::HashMap;
#[cfg(target_os = "linux")]
if crate::busybox::is_inside_vm() {
log::debug!("main: inside VM, skip VM session routing to avoid deadlock");
return Ok(None);
}
let (subcommand_name, sub_matches) = match matches.subcommand() {
Some(("install", sm)) => ("install", sm),
Some(("upgrade", sm)) => ("upgrade", sm),
Some(("remove", sm)) => ("remove", sm),
Some(("restore", sm)) => ("restore", sm),
#[cfg(not(target_os = "linux"))]
_ => return Ok(None),
#[cfg(target_os = "linux")]
_ => return Ok(None),
};
let env_name = &config().common.env_name;
if !crate::vm::session::is_vm_session_active(env_name) {
log::debug!("main: no active VM session for {}, proceeding with normal execution", env_name);
return Ok(None);
}
log::info!("main: routing '{}' command through existing VM session for {}",
subcommand_name, env_name);
let mut cmd_parts = vec![
"/usr/bin/epkg".to_string(),
"-e".to_string(),
config().common.env_name.clone(),
subcommand_name.to_string(),
];
if let Some(package_specs) = sub_matches.get_many::<String>("PACKAGE_SPEC") {
for spec in package_specs {
cmd_parts.push(spec.clone());
}
}
cmd_parts.push("--assume-yes".to_string());
#[cfg(feature = "libkrun")]
{
let env_root = crate::dirs::get_env_root(config().common.env_name.clone())?;
let mut env_vars: HashMap<String, String> = std::env::vars()
.filter(|(k, _)| k.starts_with("EPKG_") || k.starts_with("RUST_LOG") || k == "HOME")
.collect();
if !env_vars.contains_key("EPKG_CACHE") {
let dirs = crate::models::dirs();
env_vars.insert("EPKG_CACHE".to_string(), dirs.home_cache.to_string_lossy().to_string());
}
crate::libkrun::execute_via_existing_vm(
&env_root,
&cmd_parts,
crate::models::IoMode::Stream,
Some(&env_vars),
None,
None,
)
}
#[cfg(not(feature = "libkrun"))]
Err(eyre::eyre!("VM session routing requires libkrun feature"))
}
fn command_install(sub_matches: &clap::ArgMatches) -> Result<()> {
if let Some(package_specs) = sub_matches.get_many::<String>("PACKAGE_SPEC") {
let packages_vec: Vec<String> = package_specs.cloned().collect();
install_packages(packages_vec).map(|_| ())?;
}
Ok(())
}
fn command_upgrade(sub_matches: &clap::ArgMatches) -> Result<()> {
let package_names: Vec<String> = sub_matches
.get_many::<String>("PACKAGE_SPEC")
.map(|vals| vals.cloned().collect())
.unwrap_or_else(Vec::new);
upgrade_packages(package_names).map(|_| ())
}
fn command_remove(sub_matches: &clap::ArgMatches) -> Result<()> {
if let Some(package_specs) = sub_matches.get_many::<String>("PACKAGE_SPEC") {
let packages_vec: Vec<String> = package_specs.cloned().collect();
remove_packages(packages_vec).map(|_| ())?;
}
Ok(())
}
fn command_history(_sub_matches: &clap::ArgMatches) -> Result<()> {
print_history()
}
fn command_restore(sub_matches: &clap::ArgMatches) -> Result<()> {
if let Some(rollback_id) = sub_matches.get_one::<i32>("GEN_ID") {
rollback_history(*rollback_id)?;
}
Ok(())
}
fn command_update(_sub_matches: &clap::ArgMatches) -> Result<()> {
sync_channel_metadata()?;
Ok(())
}
fn command_repo(sub_matches: &clap::ArgMatches) -> Result<()> {
if let Some(_) = sub_matches.subcommand_matches("list") {
crate::repo::list_repos()?;
}
Ok(())
}
fn command_hash(sub_matches: &clap::ArgMatches) -> Result<()> {
if let Some(package_store_dirs) = sub_matches.get_many::<String>("PACKAGE_STORE_DIR") {
for dir in package_store_dirs {
let hash = crate::hash::epkg_store_hash(dir)?;
println!("{}", hash);
}
}
Ok(())
}
#[cfg(unix)]
fn command_build(sub_matches: &clap::ArgMatches) -> Result<()> {
if let Some(package_yaml) = sub_matches.get_one::<String>("PACKAGE_YAML") {
let epkg_src_path = get_epkg_src_path();
let build_script = crate::dirs::path_join(&epkg_src_path, &["build", "scripts", "generic-build.sh"]);
if !build_script.exists() {
return Err(eyre::eyre!("Build script not found"));
}
let mut command = std::process::Command::new("bash");
command.arg(build_script);
command.arg(package_yaml);
command.status()?;
}
Ok(())
}
#[cfg(unix)]
fn command_unpack(sub_matches: &clap::ArgMatches) -> Result<()> {
if let Some(package_files_iter) = sub_matches.get_many::<String>("PACKAGE_FILE") {
let files: Vec<String> = package_files_iter.cloned().collect();
match crate::store::unpack_packages(files) {
Ok(final_dirs) => {
if final_dirs.is_empty() {
eprintln!("No packages were unpacked by the store. This might indicate issues with the provided files or empty input.");
} else {
for final_dir in &final_dirs {
println!("{}", final_dir.display());
}
}
}
Err(e) => {
eprintln!("Error during store unpacking process: {}", e);
}
}
}
Ok(())
}
#[cfg(unix)]
fn command_convert(sub_matches: &clap::ArgMatches) -> Result<()> {
if let Some(package_files_iter) = sub_matches.get_many::<String>("PACKAGE_FILE") {
let files: Vec<String> = package_files_iter.cloned().collect();
let mut out_dir = sub_matches.get_one::<String>("out-dir").map(|s| s.as_str()).unwrap_or("");
if out_dir == "" {
out_dir = ".";
}
let origin_url = sub_matches.get_one::<String>("origin-url")
.map(|s| s.as_str())
.unwrap_or("default_url");
match crate::store::unpack_packages(files) {
Ok(final_dirs) => {
if final_dirs.is_empty() {
eprintln!("No packages were unpacked by the store. This might indicate issues with the provided files or empty input.");
} else {
for final_dir in &final_dirs {
epkg::compress_packages(final_dir, &out_dir, &origin_url)?;
}
}
}
Err(e) => {
eprintln!("Error during store unpacking process: {}", e);
}
}
}
Ok(())
}
fn command_search(_sub_matches: &clap::ArgMatches) -> Result<()> {
let mut options = config().search.clone();
search::search_repo_cache(&mut options)?;
Ok(())
}
fn command_gc(sub_matches: &clap::ArgMatches) -> Result<()> {
let old_downloads_days = sub_matches.get_one::<u64>("old-downloads").copied();
gc::gc_epkg(old_downloads_days)?;
Ok(())
}
fn command_vm(sub_matches: &clap::ArgMatches) -> Result<()> {
match sub_matches.subcommand() {
Some(("start", sm)) => vm::cmd_vm_start(sm)?,
Some(("stop", sm)) => vm::cmd_vm_stop(sm)?,
Some(("list", sm)) => vm::cmd_vm_list(sm)?,
Some(("status", sm)) => vm::cmd_vm_status(sm)?,
_ => {}
}
Ok(())
}
#[cfg(unix)]
fn command_service(sub_matches: &clap::ArgMatches) -> Result<()> {
service::command_service(sub_matches)
}
fn command_self(sub_matches: &clap::ArgMatches) -> Result<()> {
match sub_matches.subcommand() {
Some(("install", sub_matches)) => {
let force = sub_matches.get_flag("force");
if force || find_env_base(SELF_ENV).is_none() {
install_epkg_with_force(force)?;
}
if let Some(path) = find_env_base(MAIN_ENV) {
if !force {
eprintln!("epkg was already initialized for current user: {:?}", path);
}
} else {
light_init()?;
}
}
Some(("upgrade", _sub_matches)) => {
upgrade_epkg()?;
}
Some(("remove", sub_matches)) => {
if let Some(scope) = sub_matches.get_one::<String>("scope") {
deinit::deinit_epkg(scope)?;
}
}
_ => {}
}
Ok(())
}