use std::collections::{HashMap, HashSet, BTreeMap, BTreeSet};
use serde::{Deserialize, Serialize};
use std::path::PathBuf;
use std::sync::{LazyLock, OnceLock, RwLock};
#[cfg(test)]
use std::sync::Mutex;
use std::sync::Arc;
#[cfg(target_os = "linux")]
use nix::sched::CloneFlags;
#[cfg(target_os = "linux")]
use nix::unistd::{Uid, Gid};
#[cfg(target_os = "linux")]
use nix::mount::MsFlags;
#[cfg(target_os = "linux")]
use std::os::fd::OwnedFd;
#[cfg(not(test))]
use crate::parse_cmdline;
#[cfg(not(test))]
use crate::parse_cmdline_from;
use crate::parse_options_common;
use crate::parse_options_subcommand;
use crate::search::SearchOptions;
use crate::run::RunOptions;
use color_eyre::Result;
use color_eyre::eyre;
#[cfg(not(test))]
use color_eyre::eyre::WrapErr;
#[cfg(target_os = "linux")]
pub const DEFAULT_CHANNEL: &str = &"alpine";
#[cfg(target_os = "macos")]
pub const DEFAULT_CHANNEL: &str = &"conda";
#[cfg(target_os = "windows")]
pub const DEFAULT_CHANNEL: &str = &"conda";
pub const SUPPORT_ARCH_LIST: &[&str] = &["aarch64", "x86_64", "riscv64", "loongarch64"];
pub const PURE_ENV_SUFFIX: char = '!';
pub const DEFAULT_COMMIT: &str = &env!("EPKG_VERSION_TAG");
pub const SELF_ENV: &str = &"self";
pub const MAIN_ENV: &str = &"main";
#[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, Eq, Hash)]
pub enum LinkType {
#[serde(rename = "hardlink")]
Hardlink,
#[serde(rename = "symlink")]
Symlink,
#[serde(rename = "reflink")]
Reflink,
#[serde(rename = "move")]
Move,
#[serde(rename = "runpath")]
Runpath,
}
impl Default for LinkType {
fn default() -> Self {
LinkType::Hardlink
}
}
#[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, Eq, Hash)]
pub enum PackageFormat {
#[serde(rename = "epkg")]
Epkg,
#[serde(rename = "deb")]
Deb,
#[serde(rename = "rpm")]
Rpm,
#[serde(rename = "apk")]
Apk,
#[serde(rename = "pacman")]
Pacman,
#[serde(rename = "conda")]
Conda,
#[serde(rename = "brew")]
Brew,
#[serde(rename = "python")]
Python,
}
#[allow(dead_code)]
#[derive(Debug, Clone, Deserialize)]
pub struct Dependency {
pub pkgname: String,
pub ca_hash: String,
#[serde(default)]
pub version: String,
#[serde(default)]
pub arch: String,
}
#[derive(Debug, Clone)]
pub struct PackageRange {
pub begin: usize,
pub len: usize,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct Package {
pub pkgname: String,
pub version: String,
#[serde(default = "default_arch")]
pub arch: String,
#[serde(default)]
#[serde(skip_serializing_if = "is_zero_u32")]
pub size: u32,
#[serde(default)]
#[serde(rename = "installedSize")]
#[serde(skip_serializing_if = "is_zero_u32")]
pub installed_size: u32,
#[serde(default)]
#[serde(rename = "buildTime")]
pub build_time: Option<u32>,
#[serde(default)]
#[serde(rename = "source")]
pub source: Option<String>,
#[serde(default)]
pub location: String,
#[serde(default)]
#[serde(rename = "caHash")]
pub ca_hash: Option<String>,
#[serde(default)]
#[serde(rename = "sha256")]
pub sha256sum: Option<String>,
#[serde(default)]
#[serde(rename = "sha1")]
pub sha1sum: Option<String>,
#[serde(skip)]
#[allow(dead_code)]
pub depends: Vec<Dependency>,
#[serde(default)]
#[serde(rename = "requiresPre")]
pub requires_pre: Vec<String>,
#[serde(default)]
pub requires: Vec<String>,
#[serde(default)]
#[serde(rename = "buildRequires")]
pub build_requires: Vec<String>,
#[serde(default)]
#[serde(rename = "checkRequires")]
pub check_requires: Vec<String>,
#[serde(default)]
pub provides: Vec<String>,
#[serde(default)]
pub recommends: Vec<String>,
#[serde(default)]
pub suggests: Vec<String>,
#[serde(default)]
pub conflicts: Vec<String>,
#[serde(default)]
pub obsoletes: Vec<String>,
#[serde(default)]
pub enhances: Vec<String>,
#[serde(default)]
pub supplements: Vec<String>,
#[serde(default)]
pub files: Vec<String>,
#[serde(default)]
pub summary: String,
#[serde(default)]
pub description: Option<String>,
#[serde(default)]
pub homepage: String,
#[serde(default)]
pub caveats: Option<String>,
#[serde(default)]
pub section: Option<String>,
#[serde(default)]
pub priority: Option<String>,
#[serde(default)]
pub maintainer: String,
#[serde(default)]
pub license: Option<String>,
#[serde(default)]
pub vendor: Option<String>,
#[serde(default)]
#[serde(rename = "buildHost")]
pub build_host: Option<String>,
#[serde(default)]
pub signature: Option<String>,
#[serde(default)]
pub relocations: Option<String>,
#[serde(default)]
pub tag: Option<String>,
#[serde(default)]
#[serde(rename = "originUrl")]
pub origin_url: Option<String>,
#[serde(default)]
#[serde(rename = "multiArch")]
pub multi_arch: Option<String>,
#[serde(default)]
#[serde(rename = "serviceJson")]
pub service_json: Option<String>,
#[serde(default)]
pub format: PackageFormat,
#[serde(default)]
#[serde(rename = "repo")]
pub repodata_name: String,
#[serde(default)]
pub pkgkey: String,
#[serde(default)]
pub pkgline: Option<String>,
#[serde(skip)]
#[serde(rename = "packageBaseurl")]
pub package_baseurl: String,
}
#[allow(dead_code)]
#[derive(Debug, Deserialize)]
pub struct StorePathsIndex {
pub filename: String,
#[serde(default)]
pub sha256sum: Option<String>,
#[serde(default)]
pub datetime: Option<String>,
}
#[derive(Debug, Serialize, Deserialize, Default, Clone)]
pub struct FilelistsFileInfo {
pub filename: String,
pub sha256sum: String,
pub datetime: String,
pub size: u64,
}
#[derive(Debug, Serialize, Deserialize, Default, Clone)]
pub struct PackagesFileInfo {
pub filename: String,
pub sha256sum: String,
pub datetime: String,
pub size: u64,
pub nr_packages: usize,
pub nr_provides: usize,
pub nr_essentials: usize,
}
# ${HOME}/.epkg/envs/main/generations/current/installed-packages.json
{
"${pkgkey}": {
},
"jq__1.8.0-1__x86_64": {
"pkgline": "g5zo2bniyoyf3jwx4vo25qrf46al7ric__jq__1.8.0-1__x86_64",
"arch": "x86_64",
"depend_depth": 0,
"install_time": 1749433093,
"ebin_exposure": true,
"rdepends": []
},
"filesystem__2025.05.03-1__any": {
"pkgline": "7noavnmhiezcdzrjiv3tyhupsi2w4etw__filesystem__2025.05.03-1__any",
"arch": "any",
"depend_depth": 2,
"install_time": 1749437271,
"ebin_exposure": false,
"rdepends": [
"glibc__2.41+r48+g5cb575ca9a3d-1__x86_64"
]
},
}
*/
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct InstalledPackageInfo {
#[serde(default)]
pub pkgline: String,
#[serde(default = "default_arch")]
pub arch: String,
#[serde(default)]
pub depend_depth: u16,
#[serde(default)]
pub install_time: u64,
#[serde(default, skip_serializing_if = "is_false")]
pub ebin_exposure: bool,
#[serde(default, skip_serializing_if = "BTreeSet::is_empty")]
pub rdepends: BTreeSet<String>,
#[serde(default, skip_serializing_if = "BTreeSet::is_empty")]
pub depends: BTreeSet<String>,
#[serde(default, skip_serializing_if = "BTreeSet::is_empty")]
pub bdepends: BTreeSet<String>,
#[serde(default, skip_serializing_if = "BTreeSet::is_empty")]
pub rbdepends: BTreeSet<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub ebin_links: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
#[cfg(unix)]
pub xdesktop_links: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub pending_triggers: Vec<String>,
#[serde(default, skip_serializing_if = "is_false")]
pub triggers_awaited: bool,
#[serde(default, skip_serializing_if = "is_false")]
pub config_failed: bool,
}
impl Default for InstalledPackageInfo {
fn default() -> Self {
InstalledPackageInfo {
pkgline: String::new(),
arch: default_arch(),
depend_depth: 0,
install_time: 0,
ebin_exposure: false,
rdepends: BTreeSet::new(),
depends: BTreeSet::new(),
bdepends: BTreeSet::new(),
rbdepends: BTreeSet::new(),
ebin_links: Vec::new(),
#[cfg(unix)]
xdesktop_links: Vec::new(),
pending_triggers: Vec::new(),
triggers_awaited: false,
config_failed: false,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[derive(Default)]
pub struct GenerationCommand {
#[serde(default)]
pub timestamp: String,
#[serde(default)]
pub action: String,
#[serde(default)]
pub command_line: String,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub fresh_installs: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub upgrades_new: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub upgrades_old: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub old_removes: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub new_exposes: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub del_exposes: Vec<String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum IsolateMode {
#[default]
Env,
Fs,
Vm,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum NamespaceStrategy {
Unshare,
#[default]
Clone,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum IoMode {
#[default]
Auto,
Tty,
Stream,
Batch,
}
impl std::str::FromStr for IoMode {
type Err = eyre::Report;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.to_lowercase().as_str() {
"auto" => Ok(IoMode::Auto),
"tty" => Ok(IoMode::Tty),
"stream" => Ok(IoMode::Stream),
"batch" => Ok(IoMode::Batch),
_ => Err(eyre::eyre!(
"Invalid I/O mode '{}', expected auto, tty, stream, or batch",
s
)),
}
}
}
impl std::str::FromStr for IsolateMode {
type Err = eyre::Report;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.to_lowercase().as_str() {
"env" => Ok(IsolateMode::Env),
"fs" => Ok(IsolateMode::Fs),
"vm" => Ok(IsolateMode::Vm),
_ => Err(eyre::eyre!("Invalid sandbox mode '{}', expected env, fs, or vm", s)),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
#[cfg(target_os = "linux")]
pub struct MountSpec {
#[serde(default)]
pub source: String,
pub target: String,
#[serde(default)]
pub fs_type: String,
#[serde(default, skip_serializing_if = "is_false")]
pub try_only: bool,
#[serde(default)]
pub flags: u64,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub options: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub uid_map: Option<u32>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub euid_map: Option<u32>,
}
#[cfg(target_os = "linux")]
impl MountSpec {
pub fn ms_flags(&self) -> MsFlags {
MsFlags::from_bits_truncate(self.flags)
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct SandboxOptions {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub isolate_mode: Option<IsolateMode>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub namespace_strategy: Option<NamespaceStrategy>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub mount_specs: Vec<String>,
}
impl SandboxOptions {
fn is_empty(&self) -> bool {
self.isolate_mode.is_none()
&& self.namespace_strategy.is_none()
&& self.mount_specs.is_empty()
}
}
#[cfg(target_os = "linux")]
#[derive(Debug, Clone)]
pub struct ProcessCreationConfig {
pub namespace_strategy: NamespaceStrategy,
pub isolate_mode: IsolateMode,
pub namespace_flags: CloneFlags,
pub needs_uid_mapping: bool,
pub mount_spec_strings: Vec<String>,
pub working_dir: Option<PathBuf>,
}
#[cfg(target_os = "linux")]
#[derive(Debug)]
pub struct UnifiedChildContext {
pub env_root: PathBuf,
pub run_options: RunOptions,
pub command: PathBuf,
pub args: Vec<String>,
#[allow(dead_code)]
pub stdin_read_fd: Option<i32>,
pub isolate_mode: IsolateMode,
pub is_brew_env: bool,
pub sync_read_fd: Option<OwnedFd>,
#[allow(dead_code)]
pub mount_specs: Vec<MountSpec>,
pub uid: Uid,
pub gid: Gid,
pub euid: Uid,
pub user: Option<String>,
pub vm_socket_path: Option<PathBuf>,
pub vm_daemon_ready_fd: Option<OwnedFd>,
}
#[derive(Debug, Serialize, Deserialize)]
#[derive(Default)]
#[derive(Clone)]
pub struct EnvConfig {
pub name: String,
pub env_base: String,
pub env_root: String,
#[serde(default, skip_serializing_if = "is_false")]
pub public: bool,
#[serde(default, skip_serializing_if = "is_false")]
pub register_to_path: bool,
#[serde(default, skip_serializing_if = "is_zero")]
pub register_path_order: i32,
#[serde(default, skip_serializing_if = "HashMap::is_empty")]
pub env_vars: HashMap<String, String>,
#[serde(default)]
pub link: LinkType,
#[serde(default, skip_serializing_if = "SandboxOptions::is_empty")]
pub sandbox: SandboxOptions,
}
#[derive(Debug, Serialize, Deserialize, Default, Clone)]
pub struct ExportFile {
pub path: String,
pub data: String,
}
#[derive(Debug, Serialize, Deserialize, Default, Clone)]
pub struct EnvExport {
#[serde(default)]
pub env: EnvConfig,
#[serde(default)]
pub files: Vec<ExportFile>,
}
impl Default for PackageFormat {
fn default() -> Self {
PackageFormat::Epkg
}
}
impl PackageFormat {
pub fn to_str(self) -> &'static str {
match self {
PackageFormat::Epkg => "epkg",
PackageFormat::Deb => "deb",
PackageFormat::Rpm => "rpm",
PackageFormat::Apk => "apk",
PackageFormat::Pacman => "pacman",
PackageFormat::Conda => "conda",
PackageFormat::Python => "python",
PackageFormat::Brew => "brew",
}
}
pub fn from_str(s: &str) -> Result<Self> {
match s {
"epkg" => Ok(PackageFormat::Epkg),
"deb" => Ok(PackageFormat::Deb),
"rpm" => Ok(PackageFormat::Rpm),
"apk" => Ok(PackageFormat::Apk),
"pacman" => Ok(PackageFormat::Pacman),
"conda" => Ok(PackageFormat::Conda),
"python" => Ok(PackageFormat::Python),
"brew" => Ok(PackageFormat::Brew),
_ => Err(eyre::eyre!("Unknown format: {}", s)),
}
}
#[allow(unreachable_patterns)]
pub fn to_suffix(self) -> Result<&'static str> {
match self {
PackageFormat::Rpm => Ok("rpm"),
PackageFormat::Deb => Ok("deb"),
PackageFormat::Apk => Ok("apk"),
PackageFormat::Pacman => Ok("pkg.tar.zst"),
PackageFormat::Conda => Ok("conda"),
PackageFormat::Epkg => Ok("epkg"),
PackageFormat::Python => Ok("whl"),
_ => unreachable!("All PackageFormat variants are covered"),
}
}
pub fn from_suffix(suffix: &str) -> Result<Self> {
let suffix = suffix.strip_prefix('.').unwrap_or(suffix);
if suffix.ends_with("pkg.tar.zst") || suffix.ends_with("pkg.tar.xz") {
return Ok(PackageFormat::Pacman);
}
if suffix.ends_with("tar.bz2") {
return Ok(PackageFormat::Conda);
}
if suffix.ends_with("tar.gz") {
return Ok(PackageFormat::Python);
}
let ext = if let Some(dot_pos) = suffix.rfind('.') {
&suffix[dot_pos + 1..]
} else {
suffix
};
match ext {
"deb" => Ok(PackageFormat::Deb),
"rpm" => Ok(PackageFormat::Rpm),
"epkg" => Ok(PackageFormat::Epkg),
"apk" => Ok(PackageFormat::Apk),
"conda" => Ok(PackageFormat::Conda),
"whl" => Ok(PackageFormat::Python),
_ => Err(eyre::eyre!("Unknown package format suffix: {}", suffix)),
}
}
}
#[derive(Debug, Serialize, Deserialize)]
#[derive(Default)]
#[derive(Clone)]
pub struct ChannelConfig {
pub format: PackageFormat,
pub distro: String,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub distro_dirs: Vec<String>,
#[serde(default, skip_serializing_if = "String::is_empty")]
pub arch: String,
#[serde(default, skip_serializing_if = "String::is_empty")]
pub channel: String,
#[serde(default)]
pub repos: HashMap<String, RepoConfig>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub components: Vec<String>,
#[serde(default, skip_serializing_if = "String::is_empty")]
pub version: String,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub versions: Vec<String>,
#[serde(default, skip_serializing_if = "String::is_empty")]
pub app_version: String,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub app_versions: Vec<String>,
pub index_url: String,
#[serde(default, skip_serializing_if = "HashMap::is_empty")]
pub amend_index_urls: HashMap<String, String>,
#[serde(skip)]
pub file_path: String,
}
pub fn default_as_true() -> bool { true }
#[derive(Debug, Serialize, Deserialize)]
#[derive(Default)]
#[derive(Clone)]
pub struct RepoConfig {
#[serde(default = "default_as_true")]
pub enabled: bool,
#[serde(default)]
pub index_url: String,
#[serde(default)]
pub amend_index_urls: HashMap<String, String>,
#[serde(default)]
pub package_baseurl: String,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub alternative_baseurls: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub components: Vec<String>,
}
static REPODATA_INDICE: LazyLock<std::sync::RwLock<BTreeMap<String, RepoIndex>>> =
LazyLock::new(|| std::sync::RwLock::new(BTreeMap::new()));
static ENV_CONFIG: OnceLock<EnvConfig> = OnceLock::new();
static CHANNEL_CONFIGS: LazyLock<Vec<ChannelConfig>> = LazyLock::new(|| {
#[cfg(test)]
{
return Vec::new();
}
#[cfg(not(test))]
{
crate::io::deserialize_channel_config().expect("Failed to deserialize channel config")
}
});
pub fn env_config() -> &'static EnvConfig {
ENV_CONFIG.get_or_init(|| {
#[cfg(test)]
{
return EnvConfig::default();
}
#[cfg(not(test))]
{
crate::io::deserialize_env_config().expect("Failed to deserialize env config")
}
})
}
pub fn set_env_config(config: EnvConfig) -> Result<(), EnvConfig> {
ENV_CONFIG.set(config)
}
pub fn channel_configs() -> &'static Vec<ChannelConfig> {
&CHANNEL_CONFIGS
}
#[cfg(test)]
static DEFAULT_CHANNEL_CONFIG: LazyLock<Mutex<ChannelConfig>> = LazyLock::new(|| Mutex::new(ChannelConfig::default()));
pub fn channel_config() -> &'static ChannelConfig {
#[cfg(test)]
{
static CONFIG_PTR: OnceLock<usize> = OnceLock::new();
if CHANNEL_CONFIGS.is_empty() {
let mutex = LazyLock::force(&DEFAULT_CHANNEL_CONFIG);
let config = mutex.lock().unwrap();
let ptr_usize = *CONFIG_PTR.get_or_init(|| {
&*config as *const ChannelConfig as usize
});
return unsafe { &*(ptr_usize as *const ChannelConfig) };
}
}
&CHANNEL_CONFIGS[0]
}
#[cfg(test)]
pub fn channel_config_mut() -> std::sync::MutexGuard<'static, ChannelConfig> {
if CHANNEL_CONFIGS.is_empty() {
DEFAULT_CHANNEL_CONFIG.lock().unwrap_or_else(|e| e.into_inner())
} else {
panic!("channel_config_mut() called but CHANNEL_CONFIGS is not empty");
}
}
pub fn repodata_indice() -> std::sync::RwLockReadGuard<'static, BTreeMap<String, RepoIndex>> {
REPODATA_INDICE.read().unwrap_or_else(|e| e.into_inner())
}
pub fn repodata_indice_mut() -> std::sync::RwLockWriteGuard<'static, BTreeMap<String, RepoIndex>> {
REPODATA_INDICE.write().unwrap_or_else(|e| e.into_inner())
}
#[derive(Debug, Serialize, Deserialize, Default)]
pub struct RepoIndex {
pub repodata_name: String,
#[serde(skip)]
pub package_baseurl: String,
#[serde(skip)]
pub repo_dir_path: String,
#[serde(default)]
pub format: PackageFormat,
pub repo_shards: BTreeMap<String, RepoShard>,
}
#[derive(Debug, Serialize, Deserialize, Default)]
pub struct RepoShard {
#[serde(default)]
pub packages: PackagesFileInfo,
#[serde(default)]
pub filelists: Option<FilelistsFileInfo>,
#[serde(default)]
#[allow(dead_code)]
pub packages_metafile: String,
#[serde(default)]
#[allow(dead_code)]
pub filelists_metafile: Option<String>,
#[serde(skip)]
pub provide2pkgnames: Option<crate::mmio::Provide2PkgNamesMapper>,
#[serde(skip)]
pub essential_pkgnames: HashSet<String>,
#[serde(skip)]
pub pkgname2ranges: BTreeMap<String, Vec<PackageRange>>,
#[serde(skip)]
pub packages_mmap: Option<crate::mmio::FileMapper>,
#[serde(skip)]
pub pkgname2ranges_path: Option<std::path::PathBuf>,
}
#[derive(Debug, PartialEq, Eq, Clone, Copy, Default)]
pub enum EpkgCommand {
#[default]
None,
Env,
EnvList,
EnvCreate,
EnvRemove,
EnvRegister,
EnvUnregister,
EnvActivate,
EnvDeactivate,
EnvExport,
EnvPath,
EnvConfigEdit,
EnvConfigGet,
EnvConfigSet,
List,
Info,
Install,
Upgrade,
Remove,
History,
Restore,
Update,
Repo,
Hash,
Build,
Unpack,
Convert,
Run,
Busybox,
Search,
Gc,
Service,
SelfInstall,
SelfUpgrade,
SelfRemove,
}
impl From<&str> for EpkgCommand {
fn from(s: &str) -> Self {
match s.to_lowercase().as_str() {
"env" => EpkgCommand::Env,
"list" => EpkgCommand::List,
"info" => EpkgCommand::Info,
"install" => EpkgCommand::Install,
"upgrade" => EpkgCommand::Upgrade,
"remove" => EpkgCommand::Remove,
"history" => EpkgCommand::History,
"restore" => EpkgCommand::Restore,
"update" => EpkgCommand::Update,
"repo" => EpkgCommand::Repo,
"hash" => EpkgCommand::Hash,
"build" => EpkgCommand::Build,
"unpack" => EpkgCommand::Unpack,
"convert" => EpkgCommand::Convert,
"run" => EpkgCommand::Run,
"busybox" => EpkgCommand::Busybox,
"search" => EpkgCommand::Search,
"gc" => EpkgCommand::Gc,
"service" => EpkgCommand::Service,
"self" => EpkgCommand::None,
_ => EpkgCommand::None,
}
}
}
#[derive(Debug, Clone, Deserialize)]
pub struct EPKGConfig {
#[serde(default = "default_common_options")]
pub common: CommonOptions,
#[serde(default = "default_init_options")]
pub init: InitOptions,
#[serde(default)]
pub install: InstallOptions,
#[serde(default)]
pub upgrade: UpgradeOptions,
#[serde(default)]
pub update: UpdateOptions,
#[serde(default)]
pub list: ListOptions,
#[serde(skip)]
pub search: SearchOptions,
#[serde(default)]
pub env: EnvOptions,
#[serde(default)]
pub history: HistoryOptions,
#[serde(default)]
pub sandbox: SandboxOptions,
#[serde(skip)]
pub service: ServiceOptions,
#[serde(skip)]
pub run: RunOptions,
#[serde(skip)]
pub config_file: String,
#[serde(skip)]
pub command_line: String,
#[serde(skip)]
pub subcommand: EpkgCommand,
#[serde(default)]
pub dirs: EPKGDirs,
}
fn default_common_options() -> CommonOptions {
serde_yaml::from_str("{}").unwrap_or_else(|_| CommonOptions::default())
}
fn default_init_options() -> InitOptions {
serde_yaml::from_str("{}").unwrap_or_else(|_| InitOptions::default())
}
#[derive(Default, Debug, Clone, Deserialize)]
pub struct CommonOptions {
#[serde(skip)]
pub env_name: String,
#[serde(skip)]
pub env_root: String,
#[serde(skip)]
pub env_explicit: bool,
#[serde(skip)]
pub env_name_explicit: bool,
#[serde(skip)]
pub in_env_root: bool,
#[serde(skip)]
pub download_only: bool,
#[serde(skip)]
pub dry_run: bool,
#[serde(default = "default_arch")]
pub arch: String,
#[serde(default)]
pub quiet: bool,
#[serde(default)]
pub verbose: bool,
#[serde(default)]
pub assume_yes: bool,
#[serde(default)]
pub assume_no: bool,
#[serde(default)]
pub ignore_missing: bool,
#[serde(skip)]
pub force: bool,
#[serde(default = "default_metadata_expire")]
pub metadata_expire: i32,
#[serde(default)]
pub proxy: String,
#[serde(default = "default_nr_retry")]
pub nr_retry: usize,
#[serde(default = "default_nr_parallel_download")]
pub nr_parallel_download: usize,
#[serde(default = "default_parallel_processing")]
pub parallel_processing: usize,
}
pub fn default_arch() -> String {
std::env::consts::ARCH.to_string()
}
fn default_metadata_expire() -> i32 {
7 * 24 * 3600
}
fn default_nr_retry() -> usize {
6
}
fn default_nr_parallel_download() -> usize {
6
}
fn default_parallel_processing() -> usize {
let num_cpus = num_cpus::get();
let has_enough_cpus = num_cpus >= 4;
let has_enough_memory = match sys_info::mem_info() {
Ok(mem) => {
mem.total >= 1024 * 1024
},
Err(_) => false,
};
if has_enough_cpus && has_enough_memory {
num_cpus.max(1)
} else {
1
}
}
fn is_false(value: &bool) -> bool {
!*value
}
fn is_zero(value: &i32) -> bool {
*value == 0
}
fn is_zero_u32(value: &u32) -> bool {
*value == 0
}
#[derive(Default, Debug, Clone, Deserialize)]
pub struct InstallOptions {
#[serde(default)]
pub install_suggests: bool,
#[serde(default)]
pub no_install_recommends: bool,
#[serde(default)]
pub no_install_essentials: bool,
#[serde(skip)]
pub no_install: String,
#[serde(default)]
pub prefer_low_version: bool,
#[serde(default)]
pub ignore_file_conflicts: bool,
}
#[derive(Default, Debug, Clone, Deserialize)]
pub struct UpgradeOptions {
#[serde(skip)]
pub full_upgrade: bool,
}
#[derive(Default, Debug, Clone, Deserialize)]
pub struct UpdateOptions {
#[serde(skip)]
pub need_files: bool,
}
#[derive(Default, Debug, Clone, Deserialize)]
pub struct ListOptions {
#[serde(default)]
pub list_all: bool,
#[serde(default)]
pub list_installed: bool,
#[serde(default)]
pub list_available: bool,
}
#[derive(Default, Debug, Clone, Deserialize)]
pub struct EnvOptions {
#[serde(default)]
pub channel: Option<String>,
#[serde(default)]
pub path_order: Option<i32>,
#[serde(default)]
pub public: bool,
#[serde(default)]
pub pure: bool,
#[serde(default)]
pub stack: bool,
#[serde(default)]
pub link: Option<LinkType>,
#[serde(default)]
pub repos: Vec<String>,
#[serde(skip)]
pub import_file: Option<String>,
}
#[derive(Default, Debug, Clone, Deserialize)]
pub struct HistoryOptions {
#[serde(default)]
pub max_generations: Option<u32>,
}
#[derive(Default, Debug, Clone, Deserialize)]
pub struct InitOptions {
#[serde(skip)]
pub shared_store: bool,
#[serde(default = "default_commit")]
pub commit: String,
}
pub fn default_commit() -> String {
DEFAULT_COMMIT.to_string()
}
#[derive(Default, Debug, Clone, Deserialize)]
pub struct ServiceOptions {
#[serde(default)]
pub all: bool,
}
#[derive(Default, Debug, Clone, Deserialize)]
pub struct EPKGDirs {
#[serde(default)]
pub opt_epkg: PathBuf,
#[serde(default)]
#[allow(dead_code)]
pub home_epkg: PathBuf,
#[serde(default)]
#[allow(dead_code)]
pub home_cache: PathBuf,
#[serde(default)]
pub user_envs: PathBuf,
#[serde(default)]
#[allow(dead_code)]
pub user_aur_builds: PathBuf,
#[serde(default)]
pub epkg_store: PathBuf,
#[serde(default)]
pub epkg_cache: PathBuf,
#[serde(default)]
pub epkg_downloads_cache: PathBuf,
#[serde(default)]
pub epkg_channels_cache: PathBuf,
#[serde(default)]
pub epkg_run: PathBuf,
}
pub type InstalledPackagesMap = HashMap<String, Arc<InstalledPackageInfo>>;
pub type PackageMap = RwLock<HashMap<String, Arc<Package>>>;
pub type PackageNameMap = RwLock<HashMap<String, Vec<Arc<Package>>>>;
pub struct PackageCache {
pub pkgkey2package: PackageMap,
pub pkgline2package: PackageMap,
pub pkgname2packages: PackageNameMap,
pub provide2pkgnames: RwLock<HashMap<String, HashSet<String>>>,
pub installed_packages: RwLock<InstalledPackagesMap>,
pub pkgline2installed: RwLock<InstalledPackagesMap>,
pub world: RwLock<HashMap<String, String>>,
pub pkgline2filelist: RwLock<HashMap<String, Vec<String>>>,
pub installed_path_lookup_for_unpack: RwLock<Option<Arc<HashMap<String, String>>>>,
}
pub static PACKAGE_CACHE: LazyLock<PackageCache> = LazyLock::new(|| PackageCache::new());
static CLAP_MATCHES: OnceLock<clap::ArgMatches> = OnceLock::new();
pub fn clap_matches() -> &'static clap::ArgMatches {
#[cfg(test)]
{
CLAP_MATCHES.get_or_init(|| {
use clap::{Arg, ArgAction, Command};
Command::new("epkg")
.arg(Arg::new("arch").long("arch").default_value(std::env::consts::ARCH))
.arg(Arg::new("env").short('e').long("env"))
.arg(Arg::new("root").short('r').long("root").global(true))
.arg(Arg::new("config").short('C').long("config"))
.arg(Arg::new("dry-run").long("dry-run").action(ArgAction::SetTrue))
.arg(Arg::new("download-only").long("download-only").action(ArgAction::SetTrue))
.arg(Arg::new("quiet").short('q').long("quiet").action(ArgAction::SetTrue))
.arg(Arg::new("verbose").short('v').long("verbose").action(ArgAction::SetTrue))
.arg(Arg::new("assume-yes").short('y').long("assume-yes").action(ArgAction::SetTrue))
.arg(Arg::new("assume-no").long("assume-no").action(ArgAction::SetTrue))
.arg(Arg::new("ignore-missing").short('m').long("ignore-missing").action(ArgAction::SetTrue))
.arg(Arg::new("metadata-expire").long("metadata-expire"))
.arg(Arg::new("proxy").long("proxy"))
.arg(Arg::new("retry").long("retry").value_parser(clap::value_parser!(usize)))
.arg(Arg::new("parallel-download").long("parallel-download").value_parser(clap::value_parser!(usize)))
.arg(Arg::new("parallel-processing").long("parallel-processing").value_parser(clap::value_parser!(usize)))
.subcommand(Command::new("info").arg(Arg::new("PACKAGE_SPEC").num_args(0..)))
.arg_required_else_help(false)
.get_matches_from(vec!["epkg", "--dry-run", "info"])
})
}
#[cfg(not(test))]
{
CLAP_MATCHES.get().expect("init_config() must be called at startup")
}
}
#[cfg(not(test))]
pub fn init_config(invoked_as_applet: bool, invoked_as_init: bool) -> Result<()> {
#[cfg(target_os = "linux")]
if invoked_as_init {
let _ = crate::busybox::init::kmsg_write("<6>init_config: invoked_as_init=true, skipping config load\n");
}
if invoked_as_init {
#[cfg(target_os = "linux")]
let _ = crate::busybox::init::kmsg_write("<6>init_config: skipping clap parsing for init mode\n");
#[cfg(target_os = "linux")]
let _ = crate::busybox::init::kmsg_write("<6>init_config: parsing default config from YAML\n");
let default_config: EPKGConfig = serde_yaml::from_str("{}")
.unwrap_or_else(|e| {
#[cfg(target_os = "linux")]
let _ = crate::busybox::init::kmsg_write(&format!("<3>init_config: YAML parse FAILED: {:?}\n", e));
panic!("Failed to create default config: {:?}", e)
});
#[cfg(target_os = "linux")]
let _ = crate::busybox::init::kmsg_write("<6>init_config: YAML parse OK\n");
let matches = clap::ArgMatches::default();
#[cfg(target_os = "linux")]
let _ = crate::busybox::init::kmsg_write("<6>init_config: setting CLAP_MATCHES\n");
CLAP_MATCHES
.set(matches)
.map_err(|_| eyre::eyre!("init_config() must be called only once"))?;
#[cfg(target_os = "linux")]
let _ = crate::busybox::init::kmsg_write("<6>init_config: CLAP_MATCHES set, setting CONFIG\n");
*CONFIG.write().unwrap_or_else(|e| e.into_inner()) = Some(default_config);
#[cfg(target_os = "linux")]
let _ = crate::busybox::init::kmsg_write("<6>init_config: CONFIG set, returning Ok\n");
return Ok(());
}
let matches = if invoked_as_applet {
parse_cmdline_from(vec![
"epkg".to_string(),
"busybox".to_string(),
"true".to_string(),
])
} else {
parse_cmdline()
};
let common_cfg = parse_options_common(&matches).wrap_err("Failed to parse common options for CONFIG")?;
CLAP_MATCHES
.set(matches)
.map_err(|_| eyre::eyre!("init_config() must be called only once"))?;
let matches_ref = CLAP_MATCHES.get().unwrap();
let final_cfg = parse_options_subcommand(matches_ref, common_cfg)
.wrap_err("Failed to parse subcommand options for CONFIG")?;
*CONFIG.write().unwrap_or_else(|e| e.into_inner()) = Some(final_cfg);
Ok(())
}
static CONFIG: RwLock<Option<EPKGConfig>> = RwLock::new(None);
pub struct ConfigGuard(std::sync::RwLockReadGuard<'static, Option<EPKGConfig>>);
impl std::fmt::Debug for ConfigGuard {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
self.0.as_ref().unwrap().fmt(f)
}
}
impl std::ops::Deref for ConfigGuard {
type Target = EPKGConfig;
fn deref(&self) -> &EPKGConfig {
self.0.as_ref().unwrap()
}
}
pub fn config() -> ConfigGuard {
{
let guard = CONFIG.read().unwrap_or_else(|e| e.into_inner());
if guard.is_some() {
return ConfigGuard(guard);
}
}
{
let mut w = CONFIG.write().unwrap_or_else(|e| e.into_inner());
if w.is_none() {
let matches = clap_matches();
let cfg = parse_options_common(matches).expect("Failed to parse common options for CONFIG");
*w = Some(
parse_options_subcommand(matches, cfg).expect("Failed to parse subcommand options for CONFIG"),
);
}
}
ConfigGuard(CONFIG.read().unwrap_or_else(|e| e.into_inner()))
}
#[cfg(target_os = "linux")]
#[derive(Default)]
pub struct ConfigFlags {
pub assume_yes: bool,
pub quiet: bool,
pub dry_run: bool,
pub download_only: bool,
pub ignore_missing: bool,
}
#[cfg(target_os = "linux")]
pub fn apply_config_flags(flags: &ConfigFlags) {
if let Ok(mut guard) = CONFIG.write() {
if let Some(ref mut config) = *guard {
config.common.assume_yes = flags.assume_yes;
config.common.quiet = flags.quiet;
config.common.dry_run = flags.dry_run;
config.common.download_only = flags.download_only;
config.common.ignore_missing = flags.ignore_missing;
}
}
}
#[cfg(test)]
pub struct ConfigMutGuard(std::sync::RwLockWriteGuard<'static, Option<EPKGConfig>>);
#[cfg(test)]
impl std::ops::Deref for ConfigMutGuard {
type Target = EPKGConfig;
fn deref(&self) -> &EPKGConfig {
self.0.as_ref().unwrap()
}
}
#[cfg(test)]
impl std::ops::DerefMut for ConfigMutGuard {
fn deref_mut(&mut self) -> &mut EPKGConfig {
self.0.as_mut().unwrap()
}
}
#[cfg(test)]
pub fn config_mut() -> ConfigMutGuard {
let mut guard = CONFIG.write().unwrap_or_else(|e| e.into_inner());
if guard.is_none() {
let matches = clap_matches();
let cfg = parse_options_common(matches).expect("Failed to parse common options for CONFIG");
*guard = Some(
parse_options_subcommand(matches, cfg).expect("Failed to parse subcommand options for CONFIG"),
);
}
ConfigMutGuard(guard)
}
pub fn dirs() -> &'static EPKGDirs {
let _guard = config();
crate::dirs::dirs_ref()
}