use std::fs;
use std::env;
use std::path::Path;
use std::path::PathBuf;
use std::collections::{HashSet, HashMap};
#[cfg(unix)]
use std::os::unix::fs::PermissionsExt;
use rand::{Rng, SeedableRng};
use rand::rngs::StdRng;
use std::time::{SystemTime, UNIX_EPOCH};
use color_eyre::Result;
use color_eyre::eyre;
use color_eyre::eyre::WrapErr;
use serde_json;
use serde_yaml;
#[cfg(unix)]
use nix::errno::Errno;
#[cfg(unix)]
use nix::unistd::chown;
use glob;
use crate::models::*;
use crate::dirs::*;
use crate::repo::sync_channel_metadata;
use crate::utils::{force_symlink_dir_for_virtiofs, force_symlink_file_for_native};
use crate::utils::force_symlink_file_for_virtiofs;
use crate::deinit::force_remove_dir_all;
#[cfg(unix)]
use crate::deb_triggers::ensure_triggers_dir;
use crate::plan::prepare_installation_plan;
use crate::install::execute_installation_plan;
use crate::history::record_history;
use crate::path::update_path;
use crate::shell_emit;
use crate::io;
use crate::lfs;
use log::warn;
fn push_env_var(
script: &mut String,
key: &str,
new_value: Option<String>,
original_value: Option<String>,
kind: shell_emit::ShellKind,
) {
if let Some(v) = &new_value {
println!("{}", shell_emit::emit_export(key, v, kind));
}
match kind {
shell_emit::ShellKind::Bash => match original_value {
Some(v) => script.push_str(&format!(
"export {}=\"{}\"\n",
key,
v.replace('\\', "\\\\").replace('"', "\\\"")
)),
None => script.push_str(&format!("unset {}\n", key)),
},
shell_emit::ShellKind::PowerShell => match original_value {
Some(v) => script.push_str(&format!(
"$env:{} = '{}'\n",
key,
shell_emit::ps_escape_single_quoted(&v)
)),
None => script.push_str(&format!(
"Remove-Item \"Env:{}\" -ErrorAction SilentlyContinue\n",
key
)),
},
}
}
fn next_prepend_path_order() -> Result<i32> {
let shared_store = config().init.shared_store;
let registered = registered_env_configs(shared_store);
let used: HashSet<i32> = registered.into_iter()
.filter(|cfg| cfg.register_path_order >= 0)
.map(|cfg| cfg.register_path_order)
.collect();
let mut priority = 100;
while used.contains(&priority) {
priority += 10;
}
Ok(priority)
}
pub fn get_all_env_names() -> Result<Vec<(String, bool)>> {
let mut my_envs = Vec::new();
let mut other_envs = Vec::new();
let current_user = get_username()?;
let shared_store = config().init.shared_store;
let user_envs = dirs_ref().user_envs.clone();
walk_environments(shared_store, &user_envs, |env_path, owner| {
let name = env_path.file_name()
.and_then(|n| n.to_str())
.unwrap_or_default()
.to_string();
if name != SELF_ENV && !name.starts_with('.') {
let is_public = if owner.is_none() {
false
} else {
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let mode = lfs::symlink_metadata(env_path)?
.permissions()
.mode() & 0o777;
let is_private = mode == 0o700;
!is_private
}
#[cfg(not(unix))]
{
true
}
};
let is_mine = match owner {
None => true,
Some(o) => o == current_user.as_str(),
};
let env_display_name = if is_mine {
name.clone()
} else {
format!("{}/{}", owner.unwrap(), name)
};
if is_mine {
my_envs.push((env_display_name, is_public));
} else if is_public {
other_envs.push((env_display_name, is_public));
}
}
Ok(())
})?;
my_envs.sort_by(|a, b| a.0.cmp(&b.0));
other_envs.sort_by(|a, b| a.0.cmp(&b.0));
my_envs.extend(other_envs.into_iter());
Ok(my_envs)
}
pub fn list_environments() -> Result<()> {
let all_envs = get_all_env_names()?;
let active_list: Vec<String> = env::var("EPKG_ACTIVE_ENV")
.ok()
.map(|active| active.split(':').map(String::from).collect())
.unwrap_or_default();
let shared_store = config().init.shared_store;
let registered_configs = registered_env_configs(shared_store);
let registered_map: HashMap<String, i32> = registered_configs
.into_iter()
.map(|cfg| (cfg.name, cfg.register_path_order))
.collect();
println!("{:<10} {:<25} {:<30} {:<20} {}",
"Type", "Status", "Environment", "Channel", "Root");
println!("{}", "-".repeat(130));
for (env, is_public) in all_envs {
let mut status_parts = Vec::new();
if let Some(pos) = active_list.iter().position(|e| e == &env) {
let order = pos + 1;
status_parts.push(format!("activated@{}", order));
}
if let Some(&order) = registered_map.get(&env) {
status_parts.push(format!("registered@{}", order));
}
let env_type = if is_public { "public" } else { "private" };
let status = status_parts.join(",");
let (env_root, channel) = match get_env_root(env.clone()) {
Ok(root) => {
let root_str = root.display().to_string();
match crate::io::deserialize_channel_config_from_root(&root) {
Ok(configs) => {
if let Some(cc) = configs.first() {
(root_str, cc.channel.clone())
} else {
(root_str, String::new())
}
}
Err(_) => (root_str, String::new()),
}
}
Err(_) => ("N/A".to_string(), String::new()),
};
println!("{:<10} {:<25} {:<30} {:<20} {}",
env_type,
status,
env,
channel,
env_root
);
}
Ok(())
}
fn setup_resolv_conf(env_root: &Path) -> Result<()> {
lfs::create_dir_all(env_root.join("etc"))?;
let resolv_conf_path = crate::dirs::path_join(env_root, &["etc", "resolv.conf"]);
if lfs::exists_in_env(&resolv_conf_path) {
return Ok(());
}
#[cfg(windows)]
{
let windows_stub = "# Managed by epkg on Windows\n# DNS resolution is provided by Windows networking.\n";
lfs::write(&resolv_conf_path, windows_stub)?;
}
#[cfg(not(windows))]
{
let host_resolv_conf = Path::new("/etc/resolv.conf");
if lfs::exists_on_host(host_resolv_conf) {
lfs::copy(host_resolv_conf, &resolv_conf_path)?;
} else {
warn!("/etc/resolv.conf does not exist on host. Creating default resolv.conf");
let default_resolv_conf = "nameserver 8.8.8.8\nnameserver 223.6.6.6\nnameserver 8.8.4.4\nnameserver 1.1.1.1\n";
lfs::write(&resolv_conf_path, default_resolv_conf)?;
}
}
Ok(())
}
fn setup_hosts(env_root: &Path) -> Result<()> {
lfs::create_dir_all(env_root.join("etc"))?;
let hosts_path = crate::dirs::path_join(env_root, &["etc", "hosts"]);
if lfs::exists_in_env(&hosts_path) {
return Ok(());
}
#[cfg(windows)]
{
let windows_stub = "# Managed by epkg on Windows\n127.0.0.1 localhost\n::1 localhost\n";
lfs::write(&hosts_path, windows_stub)?;
}
#[cfg(not(windows))]
{
let host_hosts = Path::new("/etc/hosts");
if lfs::exists_on_host(host_hosts) {
lfs::copy(host_hosts, &hosts_path)?;
} else {
let default_hosts = "127.0.0.1 localhost\n::1 localhost\n";
lfs::write(&hosts_path, default_hosts)?;
}
}
Ok(())
}
pub fn setup_ca_certificates_symlink(env_root: &Path) -> Result<()> {
let certs_dir = crate::dirs::path_join(env_root, &["etc", "ssl", "certs"]);
let ca_cert_path = certs_dir.join("ca-certificates.crt");
if lfs::exists_in_env(&ca_cert_path) {
return Ok(());
}
lfs::create_dir_all(&certs_dir)?;
let possible_paths = [
"etc/pki/ca-trust/extracted/pem/tls-ca-bundle.pem",
"etc/pki/tls/certs/ca-bundle.crt",
"etc/ca-certificates/extracted/tls-ca-bundle.pem",
"etc/ssl/certs/ca-certificates.crt",
"etc/ssl/ca-bundle.pem",
];
for rel_path in &possible_paths {
let full_path = env_root.join(rel_path);
if lfs::exists_in_env(&full_path) {
let target = PathBuf::from("/").join(rel_path);
log::debug!("Creating CA cert symlink: {} -> {}",
ca_cert_path.display(), target.display());
lfs::symlink_file_for_native(&target, &ca_cert_path)?;
return Ok(());
}
}
log::debug!("No CA certificate bundle found in env, skipping symlink creation");
Ok(())
}
fn create_environment_dirs_early(env_root: &Path) -> Result<()> {
let generations_root = env_root.join("generations");
let gen_0_dir = generations_root.join("0");
lfs::create_dir_all_with_case_sensitivity(env_root)?;
lfs::create_dir_all(&gen_0_dir)?;
lfs::create_dir_all(env_root.join("root"))?;
lfs::create_dir_all(env_root.join("ebin"))?;
lfs::create_dir_all(env_root.join("ebin"))?;
lfs::create_dir_all(crate::dirs::path_join(env_root, &["usr", "bin"]))?;
lfs::create_dir_all(crate::dirs::path_join(env_root, &["usr", "lib"]))?;
lfs::create_dir_all(crate::dirs::path_join(env_root, &["usr", "share"]))?;
lfs::create_dir_all(crate::dirs::path_join(env_root, &["usr", "include"]))?;
lfs::create_dir_all(crate::dirs::path_join(env_root, &["usr", "local", "bin"]))?;
lfs::create_dir_all(crate::dirs::path_join(env_root, &["usr", "bin"]))?;
lfs::create_dir_all(crate::dirs::path_join(env_root, &["usr", "sbin"]))?;
lfs::create_dir_all(env_root.join("var"))?;
lfs::create_dir_all(crate::dirs::path_join(env_root, &["opt", "epkg"]))?;
lfs::create_dir_all(env_root_etc_epkg(env_root))?;
force_symlink_dir_for_virtiofs("usr/sbin", env_root.join("sbin"))?;
force_symlink_dir_for_virtiofs("usr/bin", env_root.join("bin"))?;
#[cfg(not(windows))]
force_symlink_dir_for_virtiofs("usr/lib", env_root.join("lib"))?;
force_symlink_dir_for_virtiofs("usr/share", env_root.join("share"))?;
force_symlink_dir_for_virtiofs("usr/include", env_root.join("include"))?;
force_symlink_dir_for_virtiofs("0", generations_root.join("current"))?;
lfs::create_dir_all(env_root.join("dev"))?;
lfs::create_dir_all(env_root.join("proc"))?;
lfs::create_dir_all(env_root.join("sys"))?;
lfs::create_dir_all(env_root.join("run"))?;
lfs::create_dir_all(env_root.join("tmp"))?;
setup_resolv_conf(env_root)?;
setup_hosts(env_root)?;
Ok(())
}
fn epkg_symlink_name(pkg_format: &PackageFormat) -> &'static str {
let is_linux_target = matches!(pkg_format,
PackageFormat::Deb | PackageFormat::Rpm | PackageFormat::Apk
);
let is_arch_linux = *pkg_format == PackageFormat::Pacman &&
crate::models::channel_config().distro != "msys2";
if is_linux_target || is_arch_linux {
return "epkg";
}
crate::dirs::EPKG_USR_BIN_NAME
}
fn create_init_for_vm(env_root: &Path) -> Result<()> {
let init_path = crate::dirs::path_join(env_root, &["usr", "bin", "init"]);
log::debug!("Creating init {} -> epkg (VM)", init_path.display());
if lfs::exists_no_follow(&init_path) {
lfs::remove_file(&init_path)?;
}
let epkg_source = crate::dirs::path_join(env_root, &["usr", "bin", "epkg"]);
if lfs::hard_link(&epkg_source, &init_path).is_err() {
log::debug!("Hardlink failed, falling back to copy");
lfs::copy(&epkg_source, &init_path)?;
}
#[cfg(windows)]
{
const S_IFREG: u32 = 0o100000;
const MODE_755: u32 = S_IFREG | 0o755;
if let Err(e) = crate::ntfs_ea::set_posix_mode(&init_path, MODE_755, false) {
log::warn!("Failed to set execute permission on {}: {}", init_path.display(), e);
} else {
log::debug!("Set execute permission (100755) on {}", init_path.display());
}
}
Ok(())
}
fn needs_vm_for_pkg_format(pkg_format: &PackageFormat) -> bool {
let needs_vm = matches!(pkg_format,
PackageFormat::Deb | PackageFormat::Rpm | PackageFormat::Apk
);
let is_arch_linux = *pkg_format == PackageFormat::Pacman &&
crate::models::channel_config().distro != "msys2";
needs_vm || is_arch_linux
}
pub fn create_epkg_symlink(env_root: &Path, pkg_format: &PackageFormat) -> Result<()> {
if let Some(self_env_root) = find_env_root(SELF_ENV) {
let env_root_normalized = std::fs::canonicalize(env_root).unwrap_or_else(|_| env_root.to_path_buf());
let self_env_root_normalized = std::fs::canonicalize(&self_env_root).unwrap_or(self_env_root.clone());
if env_root_normalized == self_env_root_normalized {
log::debug!("Skipping epkg symlink creation in self environment (binary already installed)");
return Ok(());
}
let symlink_name = epkg_symlink_name(pkg_format);
let epkg_symlink = crate::dirs::path_join(env_root, &["usr", "bin", symlink_name]);
#[cfg(not(target_os = "linux"))]
if needs_vm_for_pkg_format(pkg_format) {
let arch = &crate::config().common.arch;
let self_epkg_linux = crate::dirs::path_join(&self_env_root, &["usr", "bin", &format!("epkg-linux-{}", arch)]);
if lfs::exists_in_env(&self_epkg_linux) {
log::debug!("Creating epkg binary for VM environment: {} from {}", epkg_symlink.display(), self_epkg_linux.display());
if lfs::exists_no_follow(&epkg_symlink) {
lfs::remove_file(&epkg_symlink)?;
}
let used_hardlink = lfs::hard_link(&self_epkg_linux, &epkg_symlink).is_ok();
if !used_hardlink {
log::debug!("Hardlink failed, falling back to copy");
lfs::copy(&self_epkg_linux, &epkg_symlink)?;
}
#[cfg(windows)]
{
const S_IFREG: u32 = 0o100000;
const MODE_755: u32 = S_IFREG | 0o755;
if let Err(e) = crate::ntfs_ea::set_posix_mode(&epkg_symlink, MODE_755, false) {
log::warn!("Failed to set execute permission on {}: {}", epkg_symlink.display(), e);
} else {
log::debug!("Set execute permission (100755) on {}", epkg_symlink.display());
}
}
create_init_for_vm(env_root)?;
return Ok(());
} else {
log::debug!("epkg-linux-{} not found in self env, skipping epkg binary", arch);
return Ok(());
}
}
#[cfg(target_os = "linux")]
if needs_vm_for_pkg_format(pkg_format) {
let self_epkg = crate::dirs::path_join(&self_env_root, &["usr", "bin", crate::dirs::EPKG_USR_BIN_NAME]);
if lfs::exists_in_env(&self_epkg) {
log::debug!("Creating epkg binary for VM environment: {} from {}", epkg_symlink.display(), self_epkg.display());
if lfs::exists_no_follow(&epkg_symlink) {
lfs::remove_file(&epkg_symlink)?;
}
let used_hardlink = lfs::hard_link(&self_epkg, &epkg_symlink).is_ok();
if !used_hardlink {
log::debug!("Hardlink failed, falling back to copy");
lfs::copy(&self_epkg, &epkg_symlink)?;
}
create_init_for_vm(env_root)?;
return Ok(());
}
}
let self_epkg = crate::dirs::path_join(&self_env_root, &["usr", "bin", crate::dirs::EPKG_USR_BIN_NAME]);
if lfs::exists_in_env(&self_epkg) {
log::debug!("Creating epkg symlink {} -> {} (native)", epkg_symlink.display(), self_epkg.display());
force_symlink_file_for_native(&self_epkg, &epkg_symlink)
.with_context(|| format!("Failed to create epkg symlink in {}", epkg_symlink.display()))?;
}
}
Ok(())
}
fn create_environment_dirs(env_root: &Path, pkg_format: &PackageFormat, env_config: &EnvConfig, channel_config: &ChannelConfig) -> Result<()> {
match pkg_format {
PackageFormat::Pacman => {
lfs::create_dir_all(env_root.join("usr"))?;
force_symlink_dir_for_virtiofs("lib", crate::dirs::path_join(env_root, &["usr", "lib64"]))?;
force_symlink_dir_for_virtiofs("usr/lib", env_root.join("lib64"))?;
},
_ => {
lfs::create_dir_all(crate::dirs::path_join(env_root, &["usr", "lib64"]))?;
force_symlink_dir_for_virtiofs("usr/lib64", env_root.join("lib64"))?;
if lfs::exists_on_host("/usr/lib32") {
lfs::create_dir_all(crate::dirs::path_join(env_root, &["usr", "lib32"]))?;
force_symlink_dir_for_virtiofs("usr/lib32", env_root.join("lib32"))?;
}
}
}
if *pkg_format == PackageFormat::Brew {
#[cfg(target_os = "macos")]
{
force_symlink_dir_for_virtiofs("../libexec", crate::dirs::path_join(env_root, &["usr", "libexec"]))?;
force_symlink_dir_for_virtiofs("../Frameworks", crate::dirs::path_join(env_root, &["usr", "Frameworks"]))?;
force_symlink_dir_for_virtiofs("../opt", crate::dirs::path_join(env_root, &["usr", "opt"]))?;
}
#[cfg(target_os = "linux")]
{
create_homebrew_symlinks(env_root);
}
lfs::create_dir_all(env_root.join("Caskroom"))?;
lfs::create_dir_all(env_root.join("var").join("homebrew").join("linked"))?;
lfs::create_dir_all(env_root.join("var").join("homebrew").join("locks"))?;
lfs::create_dir_all(env_root.join("var").join("homebrew").join("tmp"))?;
let sbin_path = env_root.join("sbin");
if lfs::symlink_metadata(&sbin_path).is_ok() {
lfs::remove_file(&sbin_path)?;
}
lfs::create_dir_all(&sbin_path)?;
}
if channel_config.distro == "fedora" {
force_symlink_dir_for_virtiofs("bin", crate::dirs::path_join(env_root, &["usr", "sbin"]))?;
} else {
lfs::create_dir_all(crate::dirs::path_join(env_root, &["usr", "sbin"]))?;
}
#[cfg(unix)]
if pkg_format == &PackageFormat::Deb {
ensure_triggers_dir(env_root)?;
}
create_epkg_symlink(env_root, pkg_format)?;
create_applet_symlinks(env_root, pkg_format)?;
#[cfg(unix)]
if !env_config.public {
let uid = nix::unistd::geteuid();
let gid = nix::unistd::getegid();
match chown(env_root, Some(uid), Some(gid)) {
Ok(()) => {}
Err(e) if e == Errno::EINVAL || e == Errno::EPERM || e == Errno::ENOTSUP => {
log::debug!(
"Could not chown private env at {} ({}); continuing",
env_root.display(),
e
);
}
Err(e) => {
return Err(e).wrap_err_with(|| format!("Failed to set owner for {}", env_root.display()));
}
}
crate::utils::set_permissions_from_mode(env_root, 0o700)
.wrap_err_with(|| format!("Failed to set permissions for {}", env_root.display()))?;
} else {
crate::utils::set_permissions_from_mode(env_root, 0o755)
.wrap_err_with(|| format!("Failed to set permissions for {}", env_root.display()))?;
}
#[cfg(windows)]
if !env_config.public {
log::debug!("private environment requested for '{}'; ACL hardening is not implemented on Windows yet", env_root.display());
}
Ok(())
}
fn create_applet_symlinks(env_root: &Path, pkg_format: &PackageFormat) -> Result<()> {
let systemctl_path = crate::dirs::path_join(env_root, &["usr", "bin", "systemctl"]);
if !lfs::exists_in_env(&systemctl_path) {
if let Err(e) = force_symlink_file_for_virtiofs("/usr/bin/true", &systemctl_path) {
log::debug!("Skipping systemctl symlink: {}", e);
}
}
crate::busybox::create_all_applet_symlinks(env_root, pkg_format)?;
Ok(())
}
fn create_default_world_json(env_root: &Path, pkg_format: &PackageFormat) -> Result<()> {
let mut world = std::collections::HashMap::new();
match pkg_format {
PackageFormat::Pacman | PackageFormat::Rpm | PackageFormat::Deb => {
let mut no_install_packages = vec!["systemd", "systemd-udev", "udev", "dbus",
"grubby", "grub2", "dracut", "kpartx",
"pam", "kbd", "kmod",
"cron", "cronie", "crontabs",
];
match pkg_format {
PackageFormat::Pacman => no_install_packages.push("pacman"),
PackageFormat::Rpm => no_install_packages.push("dnf"),
PackageFormat::Deb => no_install_packages.push("apt"),
_ => {}
}
world.insert("no-install".to_string(), no_install_packages.join(" "));
}
_ => {}
}
let world_path = crate::dirs::path_join(env_root, &["generations", "0", "world.json"]);
let world_json = serde_json::to_string_pretty(&world)?;
lfs::write(&world_path, world_json)?;
Ok(())
}
fn import_packages_and_create_metadata(env_root: &Path) -> Result<()> {
let gen_0_dir = crate::dirs::path_join(env_root, &["generations", "0"]);
let installed_packages_path = gen_0_dir.join("installed-packages.json");
let packages_to_import = if config().env.import_file.is_some() {
io::read_installed_packages_from_path(&installed_packages_path)?
} else {
InstalledPackagesMap::new()
};
if !packages_to_import.is_empty() {
sync_channel_metadata()?;
let plan = prepare_installation_plan(&packages_to_import, None)?;
execute_installation_plan(plan)?;
if let Err(e) = setup_ca_certificates_symlink(env_root) {
log::warn!("Failed to setup CA certificates symlink: {}", e);
}
} else {
lfs::write(installed_packages_path, "{\n}")?;
record_history(&gen_0_dir, None)?;
}
Ok(())
}
pub fn create_environment(env_name: &str) -> Result<()> {
let env_base = dirs().user_envs.join(env_name);
if env_name.starts_with("__") {
println!("# Note: environment name '{}' was auto-generated", env_name);
}
let (env_root, pkg_format) = setup_environment_paths(&env_base)?;
println!("Creating environment '{}' in {}", env_name, env_root.display());
create_environment_dirs_early(&env_root)?;
let (env_config, channel_config) = initialize_environment_config_after_setup(env_name, &env_root, &env_base)?;
create_environment_dirs(&env_root, &pkg_format, &env_config, &channel_config)?;
create_default_world_json(&env_root, &pkg_format)?;
import_packages_and_create_metadata(&env_root)?;
Ok(())
}
fn read_channel_config_content_from_source() -> Result<(String, String)> {
let sources_path = crate::dirs::path_join(get_epkg_src_path().as_path(), &["assets", "repos"]);
let (distro_name, distro_version) = parse_channel_option();
let src_yaml_path = sources_path.join(format!("{}.yaml", distro_name));
if !src_yaml_path.exists() {
return Err(eyre::eyre!(
"Channel configs source path does not exist: {}",
src_yaml_path.display()
));
}
let mut channel_content = fs::read_to_string(&src_yaml_path)?;
if let Some(version) = distro_version {
channel_content = update_version_in_contents(&channel_content, &version);
}
Ok((channel_content, distro_name))
}
fn determine_package_format() -> Result<PackageFormat> {
let (channel_content, _) = read_channel_config_content_from_source()?;
let channel_config: ChannelConfig = serde_yaml::from_str(&channel_content)
.wrap_err("Failed to parse channel config")?;
Ok(channel_config.format)
}
#[cfg(target_os = "linux")]
fn create_homebrew_symlinks(env_root: &Path) {
let homebrew_prefix = crate::brew_pkg::prefix::preferred();
let hb_prefix_path = Path::new(homebrew_prefix);
let host_lb = hb_prefix_path.parent().unwrap().join(".LB");
if !host_lb.exists() {
if let Err(e) = std::os::unix::fs::symlink(".linuxbrew", &host_lb) {
log::warn!("Failed to create host .LB symlink: {}", e);
} else {
log::info!("Created host symlink {} -> .linuxbrew", host_lb.display());
}
}
let hb_inside_env = env_root.join(homebrew_prefix.trim_start_matches('/'));
if let Some(parent) = hb_inside_env.parent() {
if let Err(e) = crate::lfs::create_dir_all(parent) {
log::warn!("Failed to create HOMEBREW_PREFIX parent in env: {}", e);
return;
}
}
if hb_inside_env.exists() {
log::debug!("Removing existing HOMEBREW_PREFIX at {}", hb_inside_env.display());
let _ = std::fs::remove_dir_all(&hb_inside_env);
let _ = std::fs::remove_file(&hb_inside_env);
}
if let Err(e) = std::os::unix::fs::symlink("../../", &hb_inside_env) {
log::warn!("Failed to create env .linuxbrew symlink: {}", e);
} else {
log::info!("Created env symlink {} -> ../../", hb_inside_env.display());
}
let env_lb = hb_inside_env.parent().unwrap().join(".LB");
if env_lb.exists() {
let _ = std::fs::remove_file(&env_lb);
}
if let Err(e) = std::os::unix::fs::symlink("../../", &env_lb) {
log::warn!("Failed to create env .LB symlink: {}", e);
} else {
log::info!("Created env symlink {} -> ../../", env_lb.display());
}
let host_ldso = hb_prefix_path.parent().unwrap().join(".ld.so");
if !host_ldso.exists() {
if let Err(e) = std::os::unix::fs::symlink(".linuxbrew/lib/ld.so", &host_ldso) {
log::warn!("Failed to create host .ld.so symlink: {}", e);
} else {
log::info!("Created host symlink {} -> .linuxbrew/lib/ld.so", host_ldso.display());
}
}
let env_ldso = hb_inside_env.parent().unwrap().join(".ld.so");
if env_ldso.exists() {
let _ = std::fs::remove_file(&env_ldso);
}
if let Err(e) = std::os::unix::fs::symlink("../../lib/ld.so", &env_ldso) {
log::warn!("Failed to create env .ld.so symlink: {}", e);
} else {
log::info!("Created env symlink {} -> ../../lib/ld.so", env_ldso.display());
}
}
#[cfg(not(unix))]
fn try_use_homebrew_prefix() -> Result<Option<PathBuf>> {
log::debug!("Brew package format on non-Unix platform, using regular env_root");
Ok(None)
}
#[cfg(unix)]
fn try_use_homebrew_prefix() -> Result<Option<PathBuf>> {
let homebrew_prefix = crate::brew_pkg::prefix::preferred();
let hb_path = Path::new(homebrew_prefix);
if !hb_path.exists() {
log::info!("HOMEBREW_PREFIX {} does not exist, attempting to create...", homebrew_prefix);
try_create_homebrew_prefix(homebrew_prefix)?;
if !hb_path.exists() {
log::info!("HOMEBREW_PREFIX {} was not created (sudo may have failed), using regular env_root", homebrew_prefix);
return Ok(None);
}
log::info!("Successfully created HOMEBREW_PREFIX: {}", homebrew_prefix);
return Ok(Some(hb_path.to_path_buf()));
}
#[cfg(target_os = "macos")]
{
if is_empty_directory_owned_by_user(hb_path)? {
log::info!("Reusing empty HOMEBREW_PREFIX {} owned by current user", homebrew_prefix);
return Ok(Some(hb_path.to_path_buf()));
}
return Err(eyre::eyre!(
"HOMEBREW_PREFIX {} already exists and is not empty or not owned by current user. \
On macOS, brew environments must use HOMEBREW_PREFIX as env_root. \
Please remove the directory first: epkg env remove <name>",
homebrew_prefix
));
}
#[cfg(not(target_os = "macos"))]
{
if is_empty_directory_owned_by_user(hb_path)? {
log::info!("Reusing empty HOMEBREW_PREFIX {} owned by current user", homebrew_prefix);
return Ok(Some(hb_path.to_path_buf()));
}
log::info!("HOMEBREW_PREFIX {} already exists (not empty or not owned by user), using regular env_root", homebrew_prefix);
Ok(None)
}
}
#[cfg(unix)]
fn is_empty_directory_owned_by_user(path: &Path) -> Result<bool> {
use std::os::unix::fs::MetadataExt;
use nix::unistd::{Uid, Gid};
if !path.exists() {
return Ok(false);
}
let metadata = std::fs::metadata(path)
.wrap_err_with(|| format!("Failed to get metadata for {}", path.display()))?;
let current_uid = Uid::current().as_raw();
let current_gid = Gid::current().as_raw();
if metadata.uid() != current_uid || metadata.gid() != current_gid {
log::debug!("HOMEBREW_PREFIX {} not owned by current user (uid={}, gid={}, expected uid={}, gid={})",
path.display(), metadata.uid(), metadata.gid(), current_uid, current_gid);
return Ok(false);
}
let entries = std::fs::read_dir(path)
.wrap_err_with(|| format!("Failed to read directory {}", path.display()))?;
let count = entries.count();
if count > 0 {
log::debug!("HOMEBREW_PREFIX {} not empty (contains {} entries)", path.display(), count);
return Ok(false);
}
Ok(true)
}
#[cfg(unix)]
fn try_create_homebrew_prefix(path: &str) -> Result<()> {
match std::fs::create_dir_all(path) {
Ok(_) => return Ok(()),
Err(e) if e.kind() == std::io::ErrorKind::PermissionDenied => {
println!("Need to create Homebrew prefix directory: {}", path);
println!("Requesting sudo permission to create it...");
let output = std::process::Command::new("sudo")
.args(&["sh", "-c", &format!("mkdir -p '{}' && chown $(id -u):$(id -g) '{}'", path, path)])
.status()
.wrap_err("Failed to run sudo command")?;
if !output.success() {
log::info!("sudo failed or was cancelled, will fallback to regular env_root");
return Ok(());
}
log::info!("Successfully created HOMEBREW_PREFIX: {}", path);
Ok(())
}
Err(e) => Err(e.into()),
}
}
fn setup_environment_paths(env_base: &Path) -> Result<(PathBuf, PackageFormat)> {
let pkg_format = determine_package_format()?;
let env_root = if !config().common.env_root.is_empty() {
PathBuf::from(&config().common.env_root)
} else if pkg_format == PackageFormat::Brew {
try_use_homebrew_prefix()?.unwrap_or(env_base.to_path_buf())
} else {
env_base.to_path_buf()
};
let env_channel_yaml = env_root_channel_yaml(&env_root);
if !config().common.force && lfs::exists_on_host(&env_channel_yaml) {
return Err(eyre::eyre!("Environment already exists at path: '{}'", env_root.display()));
}
if env_base != env_root {
if lfs::exists_no_follow(&env_base) && !lfs::is_symlink_or_junction(&env_base) {
return Err(eyre::eyre!("Environment base path '{}' already exists as a directory. Cannot create symlink.", env_base.display()));
}
if lfs::exists_no_follow(&env_base) {
lfs::remove_file(env_base)?;
}
if let Some(parent) = env_base.parent() {
lfs::create_dir_all(parent)?;
}
force_symlink_dir_for_virtiofs(&env_root, env_base)
.with_context(|| format!("Failed to create symlink from {} to {}", env_base.display(), env_root.display()))?;
}
create_environment_dirs_early(&env_root)?;
copy_channel_configs(&env_root)?;
Ok((env_root, pkg_format))
}
fn initialize_environment_config_after_setup(
env_name: &str,
env_root: &Path,
env_base: &Path
) -> Result<(EnvConfig, ChannelConfig)> {
let mut env_config = if let Some(import_file) = &config().env.import_file {
import_environment_from_file(env_root, import_file)?
} else {
EnvConfig::default()
};
override_env_config(&mut env_config, env_name, env_base, env_root);
io::serialize_env_config(env_config.clone())?;
let channel_configs = io::deserialize_channel_config_from_root(&env_root.to_path_buf())?;
let channel_config = channel_configs[0].clone();
Ok((env_config, channel_config))
}
* Import environment configuration from a YAML file.
*
* The file contains a single YAML document with EnvImport structure.
* Channel configs are stored in the 'files' field as ImportFile entries
* with paths like "etc/epkg/channel.yaml" or "etc/epkg/repos.d/debian-ceph.yaml".
*/
fn import_environment_from_file(env_root: &Path, import_file: &str) -> Result<EnvConfig> {
let env_export: EnvExport = io::read_yaml_file(Path::new(import_file))?;
for export_file in &env_export.files {
let file_path = env_root.join(lfs::host_path_from_manifest_rel_path(
export_file.path.trim_start_matches('/'),
));
if let Some(parent) = file_path.parent() {
lfs::create_dir_all(parent)?;
}
lfs::write(&file_path, &export_file.data)?;
}
Ok(env_export.env)
}
fn copy_main_channel_config(env_root: &Path, channel_content: &str) -> Result<()> {
let dest_channel_path = env_root_channel_yaml(env_root);
lfs::create_dir_all(dest_channel_path.parent().unwrap())?;
lfs::write(&dest_channel_path, channel_content)?;
Ok(())
}
fn copy_repo_configs(sources_path: &Path, env_root: &Path, distro_name: &str) -> Result<()> {
for repo in &config().env.repos {
let src_repo_yaml_path = sources_path.join(format!("{}-{}.yaml", distro_name, repo));
let repos_dir = env_root_repos_d(env_root);
lfs::create_dir_all(&repos_dir)?;
let dest_repo_path = repos_dir.join(format!("{}.yaml", repo));
lfs::copy(&src_repo_yaml_path, &dest_repo_path)?;
}
Ok(())
}
fn copy_channel_configs(env_root: &Path) -> Result<()> {
let (channel_content, distro_name) = read_channel_config_content_from_source()?;
let sources_path = crate::dirs::path_join(get_epkg_src_path().as_path(), &["assets", "repos"]);
copy_main_channel_config(env_root, &channel_content)?;
copy_repo_configs(&sources_path, env_root, &distro_name)?;
Ok(())
}
fn parse_channel_option() -> (String, Option<String>) {
let channel = config().env.channel.clone().unwrap_or(DEFAULT_CHANNEL.to_string());
if let Some((name, version)) = channel.split_once(io::CHANNEL_SEPARATOR) {
(name.to_string(), Some(version.to_string()))
} else {
(channel.clone(), None)
}
}
fn update_version_in_contents(contents: &str, version: &str) -> String {
let lines: Vec<&str> = contents.lines().collect();
let mut has_version_line = false;
let mut new_lines = Vec::new();
for line in lines {
if line.trim().starts_with("version:") {
new_lines.push(format!("version: {}", version));
has_version_line = true;
} else {
new_lines.push(line.to_string());
}
}
if !has_version_line {
new_lines.push(format!("version: {}", version));
}
new_lines.join("\n")
}
fn validate_environment_for_removal(name: &str) -> Result<PathBuf> {
if name == SELF_ENV || name == MAIN_ENV {
return Err(eyre::eyre!("Environment cannot be removed: '{}'", name));
}
let env_base = get_env_base_path(name);
if !lfs::exists_on_host(&env_base) {
return Err(eyre::eyre!("Environment does not exist: '{}'", name));
}
Ok(env_base)
}
fn handle_active_environment_for_removal(name: &str) -> Result<()> {
if let Ok(active_envs) = env::var("EPKG_ACTIVE_ENV") {
let env_stack: Vec<&str> = active_envs.split(':').collect();
if let Some(pos) = env_stack.iter().position(|&x| x == name) {
if pos == 0 {
deactivate_environment()?;
} else {
return Err(eyre::eyre!(
"Cannot remove environment '{}' as it is in the middle of active environment stack. \
Please deactivate environments in reverse order: {}",
name,
env_stack[..=pos].join(" -> ")
));
}
}
}
Ok(())
}
fn cleanup_env_root_if_needed(env_base: &Path, env_root: Option<&Path>) {
if let Some(env_root) = env_root {
if env_root != env_base {
log::debug!("Cleaning up env_root {} (different from env_base {})", env_root.display(), env_base.display());
#[cfg(unix)]
{
let homebrew_prefix = crate::brew_pkg::prefix::preferred();
let is_homebrew_prefix = env_root == Path::new(homebrew_prefix);
if is_homebrew_prefix {
log::info!("Cleaning HOMEBREW_PREFIX {} for reuse", env_root.display());
remove_contents_keep_directory(env_root);
return;
}
}
match lfs::remove_dir_all(env_root) {
Ok(()) => {
log::debug!("Successfully removed env_root {}", env_root.display());
}
Err(e) => {
log::debug!("Could not remove env_root {}: {}", env_root.display(), e);
}
}
}
}
}
#[cfg(unix)]
fn remove_contents_keep_directory(dir: &Path) {
if !dir.exists() || !dir.is_dir() {
return;
}
if let Ok(entries) = std::fs::read_dir(dir) {
for entry in entries.filter_map(|e| e.ok()) {
let path: PathBuf = entry.path();
let name = entry.file_name().to_string_lossy().to_string();
if name == ".LB" {
log::debug!("Keeping .LB symlink for reuse");
continue;
}
if path.is_symlink() {
if let Err(e) = lfs::remove_file(&path) {
log::warn!("Failed to remove symlink {}: {}", path.display(), e);
}
} else if path.is_dir() {
if let Err(e) = lfs::remove_dir_all(&path) {
log::warn!("Failed to remove directory {}: {}", path.display(), e);
}
} else {
if let Err(e) = lfs::remove_file(&path) {
log::warn!("Failed to remove file {}: {}", path.display(), e);
}
}
}
}
log::info!("Left empty HOMEBREW_PREFIX {} for reuse", dir.display());
}
pub fn remove_environment(name: &str) -> Result<()> {
let env_base = validate_environment_for_removal(name)?;
handle_active_environment_for_removal(name)?;
let env_root = crate::dirs::get_env_root(name.to_string()).ok();
unregister_environment(name)?;
force_remove_dir_all(&env_base)
.with_context(|| format!("Failed to remove environment directory '{}'", env_base.display()))?;
cleanup_env_root_if_needed(&env_base, env_root.as_deref());
println!("# Environment '{}' has been removed.", name);
Ok(())
}
pub fn activate_environment(name: &str) -> Result<()> {
if name == SELF_ENV {
return Err(eyre::eyre!("Environment 'self' cannot be activated"));
}
if !lfs::exists_on_host(&get_env_root(name.to_string())?) {
return Err(eyre::eyre!("Environment not exist: '{}'", name));
}
let original_active_envs = env::var("EPKG_ACTIVE_ENV").ok();
let original_session_path = env::var("EPKG_SESSION_PATH").ok();
if let Some(active_envs) = &original_active_envs {
if active_envs.split(':').any(|env| env == name) {
return Err(eyre::eyre!("Environment '{}' is already active", name));
}
if config().env.pure && config().env.stack {
return Err(eyre::eyre!("Cannot use pure mode with stack mode"));
}
if !config().env.stack && !active_envs.is_empty() {
return Err(eyre::eyre!("Cannot activate environment in non-stack mode when other environments are active. Please deactivate them first."));
}
}
let env_config = env_config();
let shell_kind = shell_emit::detect();
let mut script = String::new();
let session_path = original_session_path.unwrap_or_else(|| {
let seed = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_nanos() as u64 ^ (std::process::id() as u64);
let mut rng = StdRng::seed_from_u64(seed);
let path = std::env::temp_dir().join(format!(
"deactivate-{}-{:08x}",
std::process::id(),
rng.random::<u32>()
));
let path_str = path.to_string_lossy().into_owned();
println!(
"{}",
shell_emit::emit_export("EPKG_SESSION_PATH", &path_str, shell_kind)
);
match shell_kind {
shell_emit::ShellKind::Bash => script.push_str("unset EPKG_SESSION_PATH\n"),
shell_emit::ShellKind::PowerShell => {
script.push_str(
"Remove-Item \"Env:EPKG_SESSION_PATH\" -ErrorAction SilentlyContinue\n",
);
}
}
path_str
});
let name_with_pure_mark = if config().env.pure {
format!("{}{}", name, PURE_ENV_SUFFIX.to_string())
} else {
name.to_string()
};
let new_active_envs = if config().env.stack {
match &original_active_envs {
Some(envs) => format!("{}:{}", name_with_pure_mark, envs),
None => name_with_pure_mark.to_string(),
}
} else {
name_with_pure_mark.to_string()
};
println!("# Activate environment '{}'{}", name, if config().env.pure { " in pure mode" } else { "" });
push_env_var(
&mut script,
"EPKG_ACTIVE_ENV",
Some(new_active_envs.clone()),
original_active_envs,
shell_kind,
);
std::env::set_var("EPKG_ACTIVE_ENV", new_active_envs);
for (key, value) in &env_config.env_vars {
let original_value = env::var(key).ok();
push_env_var(
&mut script,
key,
Some(value.clone()),
original_value,
shell_kind,
);
}
update_path()?;
let deactivate_script = format!(
"{}-{}.{}",
session_path,
name,
shell_emit::deactivate_script_extension(shell_kind)
);
lfs::write(&deactivate_script, script)?;
Ok(())
}
pub fn deactivate_environment() -> Result<()> {
let active_env = match env::var("EPKG_ACTIVE_ENV") {
Ok(env) => env,
Err(_) => {
eprintln!("Warning: No environment is currently active");
return Ok(());
}
};
let session_path = match env::var("EPKG_SESSION_PATH") {
Ok(path) => path,
Err(_) => {
eprintln!("Warning: EPKG_SESSION_PATH not set");
return Ok(());
}
};
let mut active_envs: Vec<String> = active_env.split(':').map(String::from).collect();
if active_envs.is_empty() {
return Err(eyre::eyre!("No environment is currently active"));
}
let deactivated_env = active_envs.pop().unwrap();
let deactivated_env_name = deactivated_env.trim_end_matches(PURE_ENV_SUFFIX);
let deactivate_script = format!(
"{}-{}.{}",
session_path,
deactivated_env_name,
shell_emit::deactivate_script_extension(shell_emit::detect())
);
let script = fs::read_to_string(&deactivate_script)
.with_context(|| format!("Failed to read deactivate script: {}", deactivate_script))?;
println!("{}", script);
if let Err(e) = lfs::remove_file(&deactivate_script) {
eprintln!("Warning: Could not remove deactivate script: {}", e);
}
if active_envs.is_empty() {
env::remove_var("EPKG_ACTIVE_ENV");
} else {
env::set_var("EPKG_ACTIVE_ENV", active_envs.join(":"));
}
println!("# Deactivate environment '{}'", deactivated_env);
update_path()?;
Ok(())
}
pub fn register_environment_for(name: &str, mut env_config: EnvConfig) -> Result<()> {
if name == SELF_ENV {
return Err(eyre::eyre!("Environment 'self' cannot be registered"));
}
if env_config.register_to_path {
println!("# Environment '{}' is already registered.", name);
return Ok(());
}
let path_order = if let Some(order) = config().env.path_order {
order
} else {
next_prepend_path_order()?
};
println!("# Registering environment '{}' with PATH order {}", name, path_order);
env_config.register_to_path = true;
env_config.register_path_order = path_order;
io::serialize_env_config(env_config)?;
update_path()?;
Ok(())
}
pub fn register_environment(name: &str) -> Result<()> {
let env_config = io::deserialize_env_config_for(name.to_string())?;
register_environment_for(name, env_config)
}
pub fn unregister_environment(name: &str) -> Result<()> {
let mut env_config = io::deserialize_env_config_for(name.to_string())?;
if !env_config.register_to_path {
if config().subcommand == EpkgCommand::EnvUnregister {
println!("# Environment '{}' is not registered.", name);
}
return Ok(());
}
env_config.register_to_path = false;
env_config.register_path_order = 0;
io::serialize_env_config(env_config)?;
update_path()?;
println!("# Environment '{}' has been unregistered.", name);
Ok(())
}
pub fn export_environment(output: Option<String>) -> Result<()> {
let mut env_export = EnvExport {
env: env_config().clone(),
..EnvExport::default()
};
let env_root = PathBuf::from(&env_export.env.env_root);
collect_files_for_export(&mut env_export.files, &env_root, "etc/epkg/channel.yaml")?;
collect_files_for_export(&mut env_export.files, &env_root, "etc/epkg/repos.d/*.yaml")?;
collect_files_for_export(&mut env_export.files, &env_root, &format!("generations/current/world.json"))?;
collect_files_for_export(&mut env_export.files, &env_root, &format!("generations/current/installed-packages.json"))?;
let yaml_output = serde_yaml::to_string(&env_export)?;
if let Some(output_path) = output {
lfs::write(&output_path, yaml_output)?;
println!("Environment configuration exported to {}", output_path);
} else {
println!("{}", yaml_output);
}
Ok(())
}
fn override_env_config(env_config: &mut EnvConfig, name: &str, env_base: &Path, env_root: &Path) {
env_config.name = name.to_string();
env_config.env_base = env_base.to_string_lossy().to_string();
env_config.env_root = env_root.to_string_lossy().to_string();
if name == SELF_ENV {
env_config.public = true;
} else if name == MAIN_ENV {
env_config.public = false;
} else {
env_config.public = config().env.public;
}
env_config.register_to_path = false;
env_config.register_path_order = 0;
if let Some(link_type) = config().env.link {
env_config.link = link_type;
}
}
fn collect_files_for_export(files: &mut Vec<ExportFile>, base_dir: &Path, pattern: &str) -> Result<()> {
use glob::glob;
let full_pattern = base_dir.join(pattern);
let pattern_str = full_pattern.to_string_lossy();
for entry in glob(&pattern_str)
.with_context(|| format!("Failed to parse glob pattern: {}", pattern_str))?
{
match entry {
Ok(path) => {
if let Ok(contents) = fs::read_to_string(&path) {
if let Ok(relative_path) = path.strip_prefix(base_dir) {
let export_path = relative_path.display().to_string();
files.push(ExportFile {
path: export_path,
data: contents,
});
}
}
}
Err(e) => {
eprintln!("Warning: glob error for {}: {}", pattern_str, e);
}
}
}
Ok(())
}
pub fn get_environment_config(name: &str) -> Result<()> {
let config = env_config();
let parts: Vec<&str> = name.split('.').collect();
let mut current = serde_yaml::to_value(&config)?;
for part in parts {
current = current.get(part)
.ok_or_else(|| eyre::eyre!("Configuration key not found: {}", name))?
.clone();
}
println!("{:?}", current);
Ok(())
}
pub fn set_environment_config(name: &str, value: &str) -> Result<()> {
let config = env_config();
let mut config = config.clone();
let parts: Vec<&str> = name.split('.').collect();
match parts.as_slice() {
["name"] => config.name = value.to_string(),
["env_base"] => config.env_base = value.to_string(),
["env_root"] => config.env_root = value.to_string(),
["public"] => config.public = value.parse()?,
["register_to_path"] => config.register_to_path = value.parse()?,
["register_path_order"] => config.register_path_order = value.parse()?,
["env_vars", key] | ["env_var", key] => {
config.env_vars.insert((*key).to_string(), value.to_string());
}
["sandbox", "isolate_mode"] => {
config.sandbox.isolate_mode = Some(value.parse()?);
}
[top, ..] => {
return Err(eyre::eyre!("Unknown or unsupported configuration key: {}", top));
}
[] => {
return Err(eyre::eyre!("Configuration key cannot be empty"));
}
}
io::serialize_env_config(config)?;
Ok(())
}
pub fn registered_env_configs(shared_store: bool) -> Vec<EnvConfig> {
use std::fs;
let mut configs = Vec::new();
let current_user = get_username().unwrap_or_default();
let user_envs = match compute_user_envs(shared_store) {
Ok(path) => path,
Err(_) => return configs,
};
let _ = walk_environments(shared_store, &user_envs, |env_path, owner| {
let env_name = match env_path.file_name().and_then(|n| n.to_str()) {
Some(name) => name,
None => return Ok(()),
};
if env_name == SELF_ENV || env_name.starts_with('.') {
return Ok(());
}
if !shared_store && owner.is_some() {
return Ok(());
}
let is_public = if owner.is_none() {
false
} else {
match lfs::symlink_metadata(env_path) {
Ok(metadata) => {
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let mode = metadata.permissions().mode() & 0o777;
let is_private = mode == 0o700;
!is_private
}
#[cfg(not(unix))]
{
let _ = metadata;
true
}
}
Err(_) => false,
}
};
if let Some(owner_name) = owner {
if owner_name != current_user && !is_public {
return Ok(());
}
}
let config_path = env_root_env_yaml(env_path);
if !lfs::exists_in_env(&config_path) {
return Ok(());
}
let contents = match fs::read_to_string(&config_path) {
Ok(c) => c,
Err(err) => {
warn!("Failed to read {}: {}", config_path.display(), err);
return Ok(());
}
};
let mut config: EnvConfig = match serde_yaml::from_str(&contents) {
Ok(cfg) => cfg,
Err(err) => {
warn!("Failed to parse {}: {}", config_path.display(), err);
return Ok(());
}
};
if !config.register_to_path {
return Ok(());
}
if config.name.is_empty() {
config.name = env_name.to_string();
}
if let Some(owner_name) = owner {
config.name = format!("{}/{}", owner_name, config.name);
}
configs.push(config);
Ok(())
});
configs
}
pub fn find_command_in_registered_envs(cmd_name: &str, shared_store: bool) -> Result<Option<(String, PathBuf)>> {
let mut configs = registered_env_configs(shared_store);
configs.sort_by(|a, b| {
a.register_path_order.cmp(&b.register_path_order)
.then_with(|| a.name.cmp(&b.name))
});
let bin_dirs = ["usr/bin", "bin", "usr/local/bin", "usr/sbin", "sbin"];
#[cfg(windows)]
let command_candidates = {
let has_extension = Path::new(cmd_name).extension().is_some();
let mut candidates = vec![cmd_name.to_string()];
if !has_extension {
candidates.push(format!("{}.exe", cmd_name));
candidates.push(format!("{}.bat", cmd_name));
candidates.push(format!("{}.cmd", cmd_name));
}
candidates
};
#[cfg(not(windows))]
let command_candidates = vec![cmd_name.to_string()];
for env_cfg in configs {
let env_root = PathBuf::from(&env_cfg.env_root);
for bin_dir in &bin_dirs {
#[cfg(windows)]
let bin_path = PathBuf::from(bin_dir.replace('/', "\\"));
#[cfg(not(windows))]
let bin_path = Path::new(bin_dir);
for candidate in &command_candidates {
let cmd_path = env_root.join(&bin_path).join(candidate);
if !lfs::exists_in_env(&cmd_path) {
continue;
}
#[cfg(unix)]
{
if let Ok(metadata) = lfs::symlink_metadata(&cmd_path) {
let permissions = metadata.permissions();
if permissions.mode() & 0o111 != 0 {
log::debug!("found command '{}' at path '{}'", cmd_name, cmd_path.display());
return Ok(Some((env_cfg.name.clone(), env_root)));
}
}
}
#[cfg(not(unix))]
{
return Ok(Some((env_cfg.name.clone(), env_root)));
}
}
}
}
Ok(None)
}