use std::path::{Path, PathBuf};
use std::fs;
use std::io::Write;
use crate::lfs;
use std::sync::Arc;
use color_eyre::Result;
use color_eyre::eyre::{self, WrapErr};
#[cfg(unix)]
use crate::models::SELF_ENV;
use crate::plan::InstallationPlan;
use crate::models::PACKAGE_CACHE;
use crate::package_cache::map_pkgline2filelist;
use crate::utils;
use crate::utils::FileType;
#[cfg(target_os = "linux")]
use crate::xdesktop;
#[cfg(unix)]
use crate::dirs;
#[cfg(unix)]
use crate::link::{hard_link_or_copy, bin_file_exists, create_symlink2};
use log;
fn expose_package_ebin(store_fs_dir: &PathBuf, env_root: &PathBuf) -> Result<Vec<String>> {
let fs_files = utils::list_package_files_with_info(store_fs_dir.to_str().ok_or_else(|| eyre::eyre!("Invalid store_fs_dir path"))?)?;
let absolute_ebin_paths = create_ebin_wrappers(env_root, store_fs_dir, &fs_files)?;
let mut relative_ebin_links: Vec<String> = Vec::new();
for abs_path in absolute_ebin_paths {
match abs_path.strip_prefix(env_root) {
Ok(rel_path) => {
relative_ebin_links.push(rel_path.to_string_lossy().into_owned());
}
Err(e) => {
log::warn!("Failed to strip prefix {} from path {} for store_fs_dir '{}': {}", env_root.display(), abs_path.display(), store_fs_dir.display(), e);
}
}
}
log::debug!("expose_package_ebin for store_fs_dir '{}': returning {} relative_ebin_links: {:?}", store_fs_dir.display(), relative_ebin_links.len(), relative_ebin_links);
Ok(relative_ebin_links)
}
fn unexpose_package_ebin(env_root: &Path, pkgkey: &str) -> Result<()> {
if let Some(pkg_info) = crate::plan::pkgkey2installed_pkg_info(pkgkey) {
if !pkg_info.ebin_links.is_empty() {
log::debug!("Unexposing ebin wrappers for package: {}", pkgkey);
for relative_ebin_path_str in &pkg_info.ebin_links {
let ebin_path = lfs::normalize_path_separators(&env_root.join(relative_ebin_path_str));
if lfs::symlink_metadata(&ebin_path).is_ok() {
log::debug!("Removing ebin wrapper: {}", ebin_path.display());
lfs::remove_file(&ebin_path)?;
} else {
log::warn!("Ebin wrapper listed in metadata not found for removal: {}", ebin_path.display());
}
}
}
if let Some(installed_package_info_mut) = PACKAGE_CACHE.installed_packages.write().unwrap().get_mut(pkgkey) {
let info_mut = Arc::make_mut(installed_package_info_mut);
info_mut.ebin_links.clear();
}
}
Ok(())
}
fn create_node_modules_symlink(env_root: &Path) -> Result<()> {
let node_modules_in_lib = crate::dirs::path_join(env_root, &["usr", "lib", "node_modules"]);
let node_modules_in_bin = crate::dirs::path_join(env_root, &["usr", "bin", "node_modules"]);
if !lfs::exists_in_env(&node_modules_in_lib) {
return Ok(());
}
if lfs::symlink_metadata(&node_modules_in_bin).is_ok() {
lfs::remove_file(&node_modules_in_bin)?;
}
lfs::symlink_dir_for_virtiofs("../lib/node_modules", &node_modules_in_bin)
.with_context(|| format!("Failed to create node_modules symlink at {}", node_modules_in_bin.display()))?;
log::debug!("Created symlink: {} -> ../lib/node_modules", node_modules_in_bin.display());
Ok(())
}
#[cfg(unix)]
fn handle_elf(target_path: &Path, env_root: &Path, fs_file: &Path) -> Result<()> {
log::info!("handle_elf: target_path={}, fs_file={}", target_path.display(), fs_file.display());
let is_brew = crate::run::is_brew_environment(env_root);
if is_brew {
let homebrew_prefix = crate::brew_pkg::prefix::preferred();
let hb_path = std::path::Path::new(homebrew_prefix);
let is_at_prefix = match env_root.canonicalize() {
Ok(canonical_env) => match hb_path.canonicalize() {
Ok(canonical_hb) => canonical_env == canonical_hb,
Err(_) => env_root == hb_path,
},
Err(_) => env_root == hb_path,
};
if is_at_prefix {
log::info!(" Brew at HOMEBREW_PREFIX: skipping elf-loader, creating symlink");
if target_path.exists() {
lfs::remove_file(target_path)?;
}
if let Some(parent) = target_path.parent() {
lfs::create_dir_all(parent)?;
}
lfs::symlink_file_for_virtiofs(fs_file, target_path)?;
log::debug!("Created symlink: {} -> {}", target_path.display(), fs_file.display());
return Ok(());
}
}
let self_env_root = dirs::find_env_root(SELF_ENV)
.ok_or_else(|| eyre::eyre!("Self environment not found"))?;
let elf_loader_path = crate::dirs::path_join(&self_env_root, &["usr", "bin", "elf-loader"]);
log::info!(" elf_loader_path={}", elf_loader_path.display());
if lfs::exists_in_env(target_path) {
log::info!(" Target exists, removing...");
if let Err(e) = lfs::remove_file(target_path) {
log::error!(" Failed to remove file: {}", e);
return Err(e);
}
log::info!(" Removed existing target");
}
if let Some(parent) = target_path.parent() {
lfs::create_dir_all(parent)?;
}
hard_link_or_copy(&elf_loader_path, target_path, true)
.with_context(|| format!(
"Failed to create hardlink or copy elf-loader from {} to {}",
elf_loader_path.display(),
target_path.display()
))?;
let has_bin_file = bin_file_exists(target_path, fs_file)
.with_context(|| format!("Failed to check bin file for {}", target_path.display()))?;
if !has_bin_file {
create_symlink2(target_path, fs_file)
.with_context(|| format!("Failed to ensure symlink2 for {}", target_path.display()))?;
}
log::debug!(
"handle_elf_with_loader target_path={}, env_root={}, fs_file={}",
target_path.display(),
env_root.display(),
fs_file.display()
);
Ok(())
}
fn create_ebin_wrappers(env_root: &Path, store_fs_dir: &Path, fs_files: &[crate::mtree::MtreeFileInfo]) -> Result<Vec<PathBuf>> {
let mut created_ebin_paths: Vec<PathBuf> = Vec::new();
for fs_file_info in fs_files {
let fs_file = &fs_file_info.path;
let path_str = fs_file.as_str();
let is_bin_path = path_str.starts_with("bin/") ||
path_str.contains("/bin/") ||
path_str.starts_with("sbin/") ||
path_str.contains("/sbin/");
let is_libexec_path = path_str.starts_with("libexec/") || path_str.contains("/libexec/");
if !is_bin_path && !is_libexec_path {
continue;
}
let lib_regex = regex::Regex::new(r"\.(so|so\.\d+)$").unwrap();
if lib_regex.is_match(&path_str) {
continue;
}
if fs_file_info.is_dir() {
continue;
}
if !fs_file_info.is_link() {
let mode = fs_file_info.mode;
let is_executable = if let Some(m) = mode {
m & 0o111 != 0
} else {
is_bin_path
};
if !is_executable {
continue;
}
}
let fs_file_relative = lfs::host_path_from_manifest_rel_path(fs_file);
let fs_file_absolute = store_fs_dir.join(&fs_file_relative);
if let Some(created_path) = create_ebin_wrapper(env_root, &fs_file_absolute, &fs_file_relative)
.with_context(|| format!("Failed to create ebin wrapper for {}", fs_file_absolute.display()))? {
created_ebin_paths.push(created_path);
}
}
let ruby_ebin_path = env_root.join("ebin").join("ruby");
let ruby_mri_ebin_path = env_root.join("ebin").join("ruby-mri");
if lfs::exists_in_env(&ruby_mri_ebin_path) {
if lfs::symlink_metadata(&ruby_ebin_path).is_ok() {
lfs::remove_file(&ruby_ebin_path)?;
}
let script_content = format!(
"#!/bin/sh\nexec {:?} \"$@\"\n",
ruby_mri_ebin_path
);
let ebin_dir = ruby_ebin_path.parent()
.ok_or_else(|| eyre::eyre!("Failed to get parent directory for {}", ruby_ebin_path.display()))?;
let temp_path = ebin_dir.join(".tmp-ruby-alias");
let mut wrapper = fs::OpenOptions::new()
.write(true)
.create(true)
.truncate(true)
.open(&temp_path)
.with_context(|| format!("Failed to open temp file {} for ruby alias wrapper", temp_path.display()))?;
wrapper.write_all(script_content.as_bytes())
.with_context(|| format!("Failed to write ruby alias wrapper to {}", temp_path.display()))?;
drop(wrapper);
set_wrapper_permissions(&temp_path)?;
fs::rename(&temp_path, &ruby_ebin_path)
.with_context(|| format!("Failed to rename temp file {} to {}", temp_path.display(), ruby_ebin_path.display()))?;
created_ebin_paths.push(ruby_ebin_path);
}
created_ebin_paths.extend(create_versioned_command_aliases(env_root)?);
log::debug!("create_ebin_wrappers: returning {} created paths: {:?}", created_ebin_paths.len(), created_ebin_paths);
Ok(created_ebin_paths)
}
fn create_versioned_command_aliases(env_root: &Path) -> Result<Vec<PathBuf>> {
let mut created_paths: Vec<PathBuf> = Vec::new();
let versioned_commands = [
("gcc", "gcc-"),
("g++", "g++-"),
("gfortran", "gfortran-"),
];
for (unversioned, versioned_prefix) in versioned_commands {
let unversioned_ebin = env_root.join("ebin").join(unversioned);
if lfs::exists_in_env(&unversioned_ebin) {
continue;
}
let ebin_dir = env_root.join("ebin");
if let Ok(entries) = fs::read_dir(&ebin_dir) {
let mut versioned_bins: Vec<(String, PathBuf)> = entries
.filter_map(|e| e.ok())
.filter_map(|e| {
let name = e.file_name().to_string_lossy().to_string();
if name.starts_with(versioned_prefix) {
let version_str = &name[versioned_prefix.len()..];
if let Ok(version) = version_str.parse::<u32>() {
Some((format!("{:03}", version), e.path()))
} else {
None
}
} else {
None
}
})
.collect();
versioned_bins.sort_by(|a, b| b.0.cmp(&a.0));
if let Some((_, versioned_path)) = versioned_bins.first() {
let script_content = format!(
"#!/bin/sh\nexec {:?} \"$@\"\n",
versioned_path
);
let temp_path = ebin_dir.join(format!(".tmp-{}-alias", unversioned));
let mut wrapper = fs::OpenOptions::new()
.write(true)
.create(true)
.truncate(true)
.open(&temp_path)
.with_context(|| format!("Failed to open temp file for {} alias wrapper", unversioned))?;
wrapper.write_all(script_content.as_bytes())
.with_context(|| format!("Failed to write {} alias wrapper", unversioned))?;
drop(wrapper);
set_wrapper_permissions(&temp_path)?;
fs::rename(&temp_path, &unversioned_ebin)
.with_context(|| format!("Failed to rename temp file to {}", unversioned_ebin.display()))?;
created_paths.push(unversioned_ebin);
}
}
}
let gcc_ebin = env_root.join("ebin").join("gcc");
let cc_ebin = env_root.join("ebin").join("cc");
if gcc_ebin.exists() && !cc_ebin.exists() {
crate::lfs::symlink_file_for_virtiofs(Path::new("gcc"), &cc_ebin)?;
created_paths.push(cc_ebin);
log::info!("Created cc -> gcc symlink for build systems using 'cc'");
}
Ok(created_paths)
}
pub fn create_libexec_bin_symlinks(env_root: &Path, store_fs_dir: &Path) -> Result<Vec<PathBuf>> {
let mut created_paths: Vec<PathBuf> = Vec::new();
let libexec_bin_candidates = [
crate::dirs::path_join(store_fs_dir, &["libexec", "bin"]),
crate::dirs::path_join(env_root, &["libexec", "bin"]),
];
let libexec_bin = libexec_bin_candidates.iter()
.find(|p| p.exists())
.map(|p| p.as_path())
.unwrap_or_else(|| &libexec_bin_candidates[0]);
if !libexec_bin.exists() {
return Ok(created_paths);
}
let target_bin_dirs = [
crate::dirs::path_join(env_root, &["usr", "bin"]),
env_root.join("bin"),
];
let entries = match fs::read_dir(&libexec_bin) {
Ok(e) => e,
Err(e) => {
log::debug!("Failed to read libexec/bin: {}", e);
return Ok(created_paths);
}
};
for entry in entries.flatten() {
let name = match entry.file_name().to_str() {
Some(n) => n.to_string(),
None => continue,
};
let file_type = match entry.file_type() {
Ok(ft) => ft,
Err(_) => continue,
};
if !lfs::is_symlink(&entry.path()) && !file_type.is_file() {
continue;
}
for target_bin in &target_bin_dirs {
if !target_bin.exists() {
continue;
}
let name_sanitized = lfs::sanitize_path_for_windows(std::path::Path::new(&name));
let target_path = target_bin.join(&name_sanitized);
if target_path.exists() || lfs::is_symlink(&target_path) {
continue;
}
let link_target = Path::new("../../libexec/bin").join(&name_sanitized);
match lfs::symlink_file_for_virtiofs(&link_target, &target_path) {
Ok(()) => {
log::info!("Created libexec symlink: {} -> {}",
target_path.display(), link_target.display());
created_paths.push(target_path);
}
Err(e) => {
log::debug!("Failed to create libexec symlink {}: {}",
target_path.display(), e);
}
}
}
}
Ok(created_paths)
}
fn resolve_ebin_target_path(env_root: &Path, env_path: &Path) -> PathBuf {
let mut current = env_path.to_path_buf();
let mut visited = std::collections::HashSet::new();
loop {
if !visited.insert(current.clone()) {
log::warn!("Symlink loop detected at: {}", current.display());
break;
}
if !lfs::exists_in_env(¤t) {
log::debug!("Path does not exist: {}", current.display());
break;
}
match fs::read_link(¤t) {
Ok(target) => {
if target.is_absolute() {
if target.starts_with(env_root) {
current = target.clone();
} else {
log::debug!("Absolute symlink outside env: {} -> {}", current.display(), target.display());
let target_rel = target.strip_prefix("/").unwrap_or(target.as_ref());
current = lfs::normalize_path_separators(&env_root.join(target_rel));
break;
}
} else {
if let Some(parent) = current.parent() {
current = lfs::normalize_path_separators(&parent.join(&target));
} else {
break;
}
}
}
Err(_) => {
break;
}
}
}
current
}
fn create_ebin_wrapper(env_root: &Path, fs_file_absolute: &Path, fs_file_relative: &Path) -> Result<Option<PathBuf>> {
let env_path = env_root.join(fs_file_relative);
let resolved_env_path = resolve_ebin_target_path(env_root, &env_path);
let (file_type, first_line) = if lfs::is_symlink(fs_file_absolute) {
match utils::get_file_type(&resolved_env_path) {
Ok(info) => info,
Err(e) => {
log::info!(
"Skipping ebin wrapper for {}: failed to determine file type for resolved path {}: {}",
fs_file_absolute.display(),
resolved_env_path.display(),
e
);
return Ok(None);
}
}
} else {
match utils::get_file_type(fs_file_absolute) {
Ok(info) => info,
Err(store_err) => {
match utils::get_file_type(&resolved_env_path) {
Ok(info) => info,
Err(env_err) => {
log::info!(
"Skipping ebin wrapper for {}: failed to determine file type from store path ({}) and resolved env path {} ({}).",
fs_file_absolute.display(),
store_err,
resolved_env_path.display(),
env_err
);
return Ok(None);
}
}
}
}
};
#[cfg(not(target_os = "linux"))]
if file_type == FileType::Elf {
#[cfg(unix)]
if !crate::run::is_brew_environment(env_root) {
log::debug!(
"Skipping ebin wrapper for {}: ELF binaries require VM execution on non-Linux host",
fs_file_absolute.display()
);
return Ok(None);
}
#[cfg(not(unix))]
{
return Ok(None);
}
}
let basename = fs_file_relative.file_name()
.ok_or_else(|| eyre::eyre!("Failed to get filename for {}", fs_file_relative.display()))?;
let ebin_path = env_root.join("ebin").join(basename);
if fs_file_relative.to_string_lossy().contains("/libexec/") && lfs::exists_in_env(&ebin_path) {
log::debug!(
"Skipping libexec internal binary {} - ebin wrapper {} already exists",
fs_file_relative.display(),
ebin_path.display()
);
return Ok(None);
}
log::debug!(
"Creating ebin wrapper: ebin_path={}, fs_file_absolute={}, fs_file_relative={}, resolved_env_path={}, file_type={:?}, first_line={:?}",
ebin_path.display(),
fs_file_absolute.display(),
fs_file_relative.display(),
resolved_env_path.display(),
file_type,
first_line
);
match file_type {
#[cfg(unix)]
FileType::Elf => {
handle_elf(&ebin_path, env_root, &resolved_env_path)
.with_context(|| format!("Failed to handle elf for {}", ebin_path.display()))?;
return Ok(Some(ebin_path));
}
#[cfg(unix)]
FileType::MachO => {
if crate::run::is_brew_environment(env_root) {
let homebrew_prefix = crate::brew_pkg::prefix::preferred();
let hb_path = std::path::Path::new(homebrew_prefix);
let is_at_prefix = match env_root.canonicalize() {
Ok(canonical_env) => match hb_path.canonicalize() {
Ok(canonical_hb) => canonical_env == canonical_hb,
Err(_) => env_root == hb_path,
},
Err(_) => env_root == hb_path,
};
if is_at_prefix {
if ebin_path.exists() {
lfs::remove_file(&ebin_path)?;
}
if let Some(parent) = ebin_path.parent() {
lfs::create_dir_all(parent)?;
}
lfs::symlink_file_for_virtiofs(&resolved_env_path, &ebin_path)?;
log::debug!("Created MachO symlink: {} -> {}", ebin_path.display(), resolved_env_path.display());
return Ok(Some(ebin_path));
}
}
create_binary_wrapper(&resolved_env_path, &ebin_path)
.with_context(|| format!("Failed to create MachO wrapper for {}", resolved_env_path.display()))?;
return Ok(Some(ebin_path));
}
#[cfg(not(unix))]
FileType::MachO => {
return Ok(None);
}
FileType::ShellScript
| FileType::PerlScript
| FileType::PythonScript
| FileType::RubyScript
| FileType::NodeScript
| FileType::LuaScript => {
create_script_wrapper(env_root, &resolved_env_path, &ebin_path, file_type, &first_line)
.with_context(|| format!("Failed to create script wrapper for {}", resolved_env_path.display()))?;
return Ok(Some(ebin_path));
}
_ => return Ok(None),
}
}
fn create_script_wrapper(
env_root: &Path,
fs_file: &Path,
ebin_path: &Path,
file_type: FileType,
first_line: &str,
) -> Result<()> {
let env_shell_bang_line = if file_type == FileType::NodeScript {
format!("#!/bin/sh\n")
} else if file_type == FileType::ShellScript && fs_file.file_name().map_or(false, |n| n == "npm" || n == "npx") {
format!("#!/bin/sh\n")
} else {
match create_shebang_line(env_root, first_line, fs_file) {
Ok(line) => line,
Err(e) => {
let root_cause = e.root_cause().to_string();
let path_str = fs_file.to_string_lossy();
let error_msg = format!(
"Cannot create script wrapper for {} at {}: failed to create shebang line for '{}': {}",
fs_file.display(),
ebin_path.display(),
first_line,
root_cause
);
if path_str.contains("/usr/bin") {
return Err(eyre::eyre!("{}", error_msg));
} else {
log::info!("{}", error_msg);
return Ok(());
}
}
}
};
let exec_cmd = get_exec_command(&file_type, fs_file, Some(env_root));
let ebin_dir = ebin_path.parent()
.ok_or_else(|| eyre::eyre!("Failed to get parent directory for {}", ebin_path.display()))?;
let temp_path = ebin_dir.join(format!(".tmp-{}", ebin_path.file_name().unwrap().to_string_lossy()));
let mut wrapper = fs::OpenOptions::new()
.write(true)
.create(true)
.truncate(true)
.open(&temp_path)
.with_context(|| format!("Failed to open temp file {} for create_script_wrapper", temp_path.display()))?;
if !env_shell_bang_line.is_empty() {
wrapper.write_all(env_shell_bang_line.as_bytes())
.with_context(|| format!("Failed to write shebang line to {}", temp_path.display()))?;
}
wrapper.write_all(exec_cmd.as_bytes())
.with_context(|| format!("Failed to write exec command to {}", temp_path.display()))?;
drop(wrapper);
set_wrapper_permissions(&temp_path)?;
fs::rename(&temp_path, ebin_path)
.with_context(|| format!("Failed to rename temp file {} to {}", temp_path.display(), ebin_path.display()))?;
log::debug!(
"Created script wrapper: ebin_path={}, fs_file={}, file_type={:?}, first_line={:?}",
ebin_path.display(),
fs_file.display(),
file_type,
first_line
);
Ok(())
}
#[cfg(unix)]
fn create_binary_wrapper(fs_file: &Path, ebin_path: &Path) -> Result<()> {
let ebin_dir = ebin_path.parent()
.ok_or_else(|| eyre::eyre!("Failed to get parent directory for {}", ebin_path.display()))?;
let temp_path = ebin_dir.join(format!(".tmp-{}", ebin_path.file_name().unwrap().to_string_lossy()));
let mut wrapper = fs::OpenOptions::new()
.write(true)
.create(true)
.truncate(true)
.open(&temp_path)
.with_context(|| format!("Failed to open temp file {} for create_binary_wrapper", temp_path.display()))?;
let script_content = format!("#!/bin/sh\nexec {:?} \"$@\"\n", fs_file);
wrapper.write_all(script_content.as_bytes())
.with_context(|| format!("Failed to write binary wrapper to {}", temp_path.display()))?;
drop(wrapper);
set_wrapper_permissions(&temp_path)?;
fs::rename(&temp_path, ebin_path)
.with_context(|| format!("Failed to rename temp file {} to {}", temp_path.display(), ebin_path.display()))?;
log::debug!(
"Created binary wrapper: ebin_path={}, fs_file={}",
ebin_path.display(),
fs_file.display()
);
Ok(())
}
fn create_shebang_line(env_root: &Path, first_line: &str, script_path: &Path) -> Result<String> {
let shebang_info = crate::shebang::parse_shebang_for_wrapper(first_line)?;
let env_interpreter_path = match create_interpreter_wrapper(env_root, &shebang_info.interpreter_path, &shebang_info.interpreter_basename, script_path)
.with_context(|| format!("Failed to create interpreter wrapper for {} with basename {}", shebang_info.interpreter_path, shebang_info.interpreter_basename))
{
Ok(path) => {
if path == "" {
return Ok(format!("{}\n", first_line));
}
path
},
Err(e) => return Err(e),
};
if shebang_info.remaining_params.is_empty() {
Ok(format!("#!{}\n", env_interpreter_path))
} else {
Ok(format!("#!{} {}\n", env_interpreter_path, shebang_info.remaining_params))
}
}
fn create_interpreter_wrapper(env_root: &Path, interpreter_path: &str, interpreter_basename: &str, script_path: &Path) -> Result<String> {
let env_interpreter = crate::dirs::path_join(env_root, &["ebin", interpreter_basename]);
if !lfs::exists_in_env(&env_interpreter) {
let interpreter_rel = interpreter_path.strip_prefix('/').unwrap_or(interpreter_path);
let interpreter_rel = lfs::normalize_path_separators(Path::new(interpreter_rel));
let interpreter_in_env = env_root.join(&interpreter_rel);
match find_link_interpreter(&interpreter_in_env, interpreter_basename, env_root) {
Ok(()) => {}
Err(e) => {
log::info!(
"Script interpreter not found. Please install '{}' to make below script work:\n script_path: {}\n env_path: {}, error: {}",
interpreter_path,
script_path.display(),
interpreter_in_env.display(),
e
);
return Ok("".to_string());
}
}
let path_to_canonicalize = lfs::resolve_symlink_in_env(&interpreter_in_env, env_root)
.unwrap_or_else(|| interpreter_in_env.clone());
let store_interpreter = fs::canonicalize(&path_to_canonicalize)
.with_context(|| format!("Failed to resolve interpreter path: {}", path_to_canonicalize.display()))?;
log::debug!("handle_elf params: env_interpreter={:?}, env_root={:?}, store_interpreter={:?}, interpreter_in_env={:?}",
env_interpreter, env_root, store_interpreter, interpreter_in_env);
#[cfg(unix)]
handle_elf(&env_interpreter, env_root, &store_interpreter)?;
}
Ok(env_interpreter.to_string_lossy().into_owned())
}
fn find_link_interpreter(interpreter_in_env: &Path, interpreter_basename: &str, env_root: &Path) -> Result<()> {
if lfs::resolve_symlink_in_env(interpreter_in_env, env_root).is_some() {
return Ok(());
}
if lfs::is_symlink(interpreter_in_env) {
let target = match fs::read_link(interpreter_in_env) {
Ok(t) => t.to_string_lossy().into_owned(),
Err(_) => "???".to_string(),
};
log::warn!(
"Removing broken symlink: {} -> {} (target not found in environment)",
interpreter_in_env.display(),
target
);
lfs::remove_file(interpreter_in_env)?;
} else if lfs::exists_on_host(interpreter_in_env) {
return Ok(());
}
find_and_link_alternative_interpreter(interpreter_in_env, interpreter_basename)?;
Ok(())
}
fn find_and_link_alternative_interpreter(interpreter_in_env: &Path, interpreter_basename: &str) -> Result<()> {
let parent = interpreter_in_env.parent()
.ok_or_else(|| eyre::eyre!("Failed to get parent directory of {}", interpreter_in_env.display()))?;
let targets = match interpreter_basename {
"sh" => glob::glob(&format!("{}/{{bash,dash,yash,busybox}}", parent.display()))
.with_context(|| "Failed to glob for shell interpreters")?,
_ => glob::glob(&format!("{}?*", interpreter_in_env.display()))
.with_context(|| format!("Failed to glob for {} interpreters", interpreter_basename))?
};
let target = targets
.filter_map(Result::ok)
.max_by(|a, b| {
let a_name = a.file_name().unwrap_or_default().to_string_lossy();
let b_name = b.file_name().unwrap_or_default().to_string_lossy();
a_name.cmp(&b_name)
})
.ok_or_else(|| eyre::eyre!("No suitable interpreter found for '{}'", interpreter_basename))?;
let target_sanitized = lfs::sanitize_path_for_windows(&target);
lfs::symlink_file_for_virtiofs(&target_sanitized, interpreter_in_env)?;
Ok(())
}
fn get_exec_command(file_type: &FileType, resolved_path: &Path, _env_root: Option<&Path>) -> String {
if *file_type == FileType::ShellScript {
let path_str = resolved_path.to_string_lossy();
if path_str.ends_with("/bin/npm") {
let cli_path = path_str.trim_end_matches("/bin/npm").to_string() + "/bin/npm-cli.js";
return format!("exec node {:?} \"$@\"\n", cli_path);
}
if path_str.ends_with("/bin/npx") {
let cli_path = path_str.trim_end_matches("/bin/npx").to_string() + "/bin/npx-cli.js";
return format!("exec node {:?} \"$@\"\n", cli_path);
}
}
match file_type {
FileType::ShellScript => format!("exec {:?} \"$@\"\n", resolved_path),
FileType::PythonScript => format!("exec(open({:?}).read())\n", resolved_path),
FileType::RubyScript => format!("load({:?})\n", resolved_path),
FileType::LuaScript => format!("dofile({:?})\n", resolved_path),
FileType::NodeScript => {
format!("exec node {:?} \"$@\"\n", resolved_path)
}
_ => format!("exec {:?} \"$@\"\n", resolved_path),
}
}
fn set_wrapper_permissions(ebin_path: &Path) -> Result<()> {
utils::set_executable_permissions(ebin_path, 0o755)
}
#[allow(unused)]
pub fn unexpose_package(plan: &mut InstallationPlan, env_root: &Path, pkgkey: &str) -> Result<()> {
unexpose_package_ebin(env_root, pkgkey)?;
if let Some(installed_package_info_mut) = PACKAGE_CACHE.installed_packages.write().unwrap().get_mut(pkgkey) {
let info_mut = Arc::make_mut(installed_package_info_mut);
#[cfg(target_os = "linux")]
{
xdesktop::unexpose_package_xdesktop(&info_mut.xdesktop_links, env_root, &mut plan.desktop_integration_occurred)?;
info_mut.xdesktop_links.clear();
}
info_mut.ebin_exposure = false;
}
Ok(())
}
pub fn expose_package(plan: &mut InstallationPlan, store_fs_dir: &Path, pkgkey: &str) -> Result<()> {
log::debug!("Exposing package: {} (store_fs_dir: {})", pkgkey, store_fs_dir.display());
if let Some(info) = crate::plan::pkgkey2new_pkg_info(plan, pkgkey) {
log::debug!(" Found in new_pkgs: ebin_exposure={}", info.ebin_exposure);
} else {
log::debug!(" NOT found in new_pkgs!");
}
let installed_pkg_info = PACKAGE_CACHE.installed_packages.read().unwrap().get(pkgkey)
.ok_or_else(|| eyre::eyre!("Package {} not found in installed_packages for exposure", pkgkey))?
.clone();
if installed_pkg_info.pkgline.is_empty() {
return Err(eyre::eyre!("Package {} has empty pkgline, cannot expose. This indicates the package wasn't properly downloaded and processed.", pkgkey));
}
let store_root = store_fs_dir.parent().unwrap().parent().unwrap();
#[allow(unused)]
let filelist = map_pkgline2filelist(store_root, &installed_pkg_info.pkgline)?;
let ebin_links = expose_package_ebin(&store_fs_dir.to_path_buf(), &plan.env_root)
.with_context(|| format!("Failed to expose package {}", pkgkey))?;
if let Ok(parsed) = crate::package::parse_pkgline(&installed_pkg_info.pkgline) {
if parsed.pkgname == "npm" || parsed.pkgname == "nodejs" {
create_node_modules_symlink(&plan.env_root)
.with_context(|| format!("Failed to create node_modules symlink for package {}", pkgkey))?;
}
}
#[cfg(target_os = "linux")]
let xdesktop_links = xdesktop::expose_package_xdesktop(&plan.env_root, &filelist, &mut plan.desktop_integration_occurred)?;
if let Some(installed_package_info_mut) = PACKAGE_CACHE.installed_packages.write().unwrap().get_mut(pkgkey) {
let info_mut = Arc::make_mut(installed_package_info_mut);
info_mut.ebin_links = ebin_links;
#[cfg(target_os = "linux")]
{
info_mut.xdesktop_links = xdesktop_links;
}
info_mut.ebin_exposure = true;
} else {
log::warn!("expose_package_operations: pkgkey '{}' not found in installed_packages. Links not stored.", pkgkey);
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
mod resolve_ebin_target_path_tests {
use super::*;
fn get_test_osroot() -> PathBuf {
crate::dirs::path_join(
&PathBuf::from(env!("CARGO_MANIFEST_DIR")),
&["tests", "osroot"],
)
}
#[test]
fn test_alpine_npm_symlink() {
let osroot = get_test_osroot().join("alpine");
let env_path = crate::dirs::path_join(&osroot, &["usr", "bin", "npm"]);
let resolved = resolve_ebin_target_path(&osroot, &env_path);
let expected = crate::dirs::path_join(&osroot, &["usr", "share", "nodejs", "npm", "bin", "npm-cli.js"]);
assert_eq!(resolved.canonicalize().unwrap(), expected.canonicalize().unwrap(), "Alpine npm symlink should resolve to npm-cli.js");
}
#[test]
fn test_alpine_npx_symlink() {
let osroot = get_test_osroot().join("alpine");
let env_path = crate::dirs::path_join(&osroot, &["usr", "bin", "npx"]);
let resolved = resolve_ebin_target_path(&osroot, &env_path);
let expected = crate::dirs::path_join(&osroot, &["usr", "share", "nodejs", "npm", "bin", "npx-cli.js"]);
assert_eq!(resolved.canonicalize().unwrap(), expected.canonicalize().unwrap(), "Alpine npx symlink should resolve to npx-cli.js");
}
#[test]
fn test_openeuler_npm_symlink() {
let osroot = get_test_osroot().join("openeuler");
let env_path = crate::dirs::path_join(&osroot, &["usr", "bin", "npm"]);
let resolved = resolve_ebin_target_path(&osroot, &env_path);
let expected = crate::dirs::path_join(&osroot, &["usr", "share", "nodejs", "npm", "bin", "npm"]);
assert_eq!(resolved.canonicalize().unwrap(), expected.canonicalize().unwrap(), "OpenEuler npm symlink should resolve to npm shell script");
}
#[test]
fn test_debian_npm_symlink() {
let osroot = get_test_osroot().join("debian");
let env_path = crate::dirs::path_join(&osroot, &["usr", "bin", "npm"]);
let resolved = resolve_ebin_target_path(&osroot, &env_path);
let expected = crate::dirs::path_join(&osroot, &["usr", "share", "nodejs", "npm", "bin", "npm-cli.js"]);
assert_eq!(resolved.canonicalize().unwrap(), expected.canonicalize().unwrap(), "Debian npm symlink should resolve to npm-cli.js");
}
#[test]
fn test_debian_nodejs_symlink() {
let osroot = get_test_osroot().join("debian");
let env_path = crate::dirs::path_join(&osroot, &["usr", "bin", "nodejs"]);
let resolved = resolve_ebin_target_path(&osroot, &env_path);
let expected = crate::dirs::path_join(&osroot, &["usr", "bin", "node"]);
assert_eq!(resolved.canonicalize().unwrap(), expected.canonicalize().unwrap(), "Debian nodejs symlink should resolve to node");
}
#[test]
fn test_regular_file_no_symlink() {
let osroot = get_test_osroot().join("debian");
let env_path = crate::dirs::path_join(&osroot, &["usr", "bin", "node"]);
let resolved = resolve_ebin_target_path(&osroot, &env_path);
assert_eq!(resolved, env_path, "Regular file should return same path");
}
}
}