use std::path::{Path, PathBuf};
use std::sync::LazyLock;
use color_eyre::eyre::{self, Context, Result};
use log;
use crate::lfs;
use crate::dirs;
use crate::plan::InstallationPlan;
const SUPPORTED_TOOLS: &[&str] = &[
"pip", "pip3", "npm", "node", "npx", "gem", "bundle",
"go", "cargo", "mvn",
];
const TOOL_CONFIG_FILES: &[(&str, &[&str])] = &[
("pip", &["~/.pip/pip.conf", "~/.config/pip/pip.conf"]),
("npm", &["~/.npmrc"]),
("node", &[]),
("npx", &[]),
("gem", &["~/.gemrc"]),
("bundle", &["~/.bundle/config"]),
("go", &[]),
("cargo", &["~/.cargo/config.toml", "~/.cargo/config"]),
("mvn", &["~/.m2/settings.xml"]),
];
const TOOL_ENV_VARS: &[(&str, &[&str])] = &[
("pip", &["PIP_INDEX_URL", "PIP_INDEX_HOST"]),
("npm", &["npm_config_registry", "NPM_CONFIG_REGISTRY"]),
("node", &["npm_config_registry", "NODEJS_ORG_MIRROR"]),
("npx", &["npm_config_registry"]),
("gem", &["BUNDLE_MIRROR__HTTPS://RUBYGEMS__ORG/"]),
("bundle", &["BUNDLE_MIRROR__HTTPS://RUBYGEMS__ORG/", "BUNDLE_RUBYGEMS__ORG_MIRROR"]),
("go", &["GOPROXY"]),
("cargo", &["RUSTUP_DIST_SERVER", "CARGO_REGISTRIES_CRATES_INDEX"]),
("mvn", &["MAVEN_CENTRAL_MIRROR", "MAVEN_REPO_LOCAL"]),
];
static COUNTRY_TO_REGION: LazyLock<std::collections::HashMap<&'static str, &'static str>> = LazyLock::new(|| {
let mut map = std::collections::HashMap::new();
map.insert("CN", "cn");
let eu_countries = [
"AT", "BE", "BG", "HR", "CY", "CZ", "DK", "EE", "FI", "FR",
"DE", "GR", "HU", "IE", "IT", "LV", "LT", "LU", "MT", "NL",
"PL", "PT", "RO", "SK", "SI", "ES", "SE",
];
for cc in eu_countries {
map.insert(cc, "eu");
}
map.insert("US", "us");
map.insert("GB", "eu");
map.insert("JP", "us");
map.insert("KR", "us");
map.insert("AU", "us");
map.insert("CA", "us");
map.insert("NZ", "us");
map
});
pub fn country_to_region(country_code: &str) -> Option<&'static str> {
COUNTRY_TO_REGION.get(country_code).copied()
}
pub fn get_region_code() -> Option<String> {
crate::location::get_country_code()
.ok()
.and_then(|cc| country_to_region(&cc).map(|s| s.to_string()))
}
fn get_tool_config_dir() -> Result<PathBuf> {
let home = dirs::get_home()?;
Ok(crate::dirs::path_join(&PathBuf::from(home), &[".epkg", "config", "tool"]))
}
fn get_env_vars_dir() -> Result<PathBuf> {
let epkg_src = dirs::get_epkg_src_path();
Ok(crate::dirs::path_join(&epkg_src, &["assets", "tool", "env_vars"]))
}
pub fn setup_tool_config_symlinks() {
if let Err(e) = setup_tool_config_symlinks_inner() {
log::warn!("Failed to setup tool config symlinks: {}", e);
}
}
fn setup_tool_config_symlinks_inner() -> Result<()> {
let config_dir = get_tool_config_dir()?;
lfs::create_dir_all(&config_dir)?;
let env_vars_link = config_dir.join("env_vars");
let env_vars_target = get_env_vars_dir()?;
if lfs::exists_on_host(&env_vars_target) {
if !is_symlink_to(&env_vars_link, &env_vars_target) {
if lfs::exists_no_follow(&env_vars_link) {
lfs::remove_file(&env_vars_link)?;
}
lfs::symlink_dir_for_native(&env_vars_target, &env_vars_link)?;
log::info!("Created symlink: {} -> {}", env_vars_link.display(), env_vars_target.display());
}
}
let iploc_link = config_dir.join("my_region");
if let Some(region) = get_region_code() {
let iploc_target = config_dir.join("env_vars").join(®ion);
if lfs::exists_on_host(&iploc_target) {
if !is_symlink_to(&iploc_link, &iploc_target) {
if lfs::exists_no_follow(&iploc_link) {
lfs::remove_file(&iploc_link)?;
}
lfs::symlink_dir_for_native(&iploc_target, &iploc_link)?;
log::info!("Created my_region symlink: {} -> {} (region: {})",
iploc_link.display(), iploc_target.display(), region);
}
} else {
log::debug!("Region config dir {} does not exist, skipping my_region symlink", iploc_target.display());
}
} else {
log::debug!("Could not determine region, skipping my_region symlink");
}
Ok(())
}
fn is_symlink_to(link: &Path, target: &Path) -> bool {
if let Ok(resolved) = std::fs::read_link(link) {
resolved == target
} else {
false
}
}
fn check_env_var_set(tool: &str) -> bool {
for (t, vars) in TOOL_ENV_VARS {
if *t == tool {
for var in *vars {
if std::env::var(var).is_ok() {
log::debug!("Env var {} is already set for tool {}", var, tool);
return true;
}
}
}
}
false
}
fn expand_tilde(path: &str) -> PathBuf {
if path.starts_with("~/") {
if let Ok(home) = dirs::get_home() {
let rest = &path[2..];
#[cfg(windows)]
{
let rest = rest.replace('/', "\\");
return PathBuf::from(home).join(&rest);
}
#[cfg(not(windows))]
return PathBuf::from(home).join(rest);
}
}
PathBuf::from(path)
}
fn check_user_config_exists(tool: &str) -> bool {
for (t, paths) in TOOL_CONFIG_FILES {
if *t == tool {
for path in *paths {
let expanded = expand_tilde(path);
if lfs::exists_on_host(&expanded) {
log::debug!("User config file exists for tool {}: {}", tool, expanded.display());
return true;
}
}
}
}
false
}
fn should_create_wrapper(tool: &str, env_root: &Path) -> bool {
if !SUPPORTED_TOOLS.contains(&tool) {
return false;
}
if check_env_var_set(tool) {
log::debug!("Skipping wrapper for {}: env var already set", tool);
return false;
}
if check_user_config_exists(tool) {
log::debug!("Skipping wrapper for {}: user config exists", tool);
return false;
}
let wrapper_path = crate::dirs::path_join(env_root, &["usr", "local", "bin"]).join(tool);
if lfs::exists_in_env(&wrapper_path) {
log::debug!("Wrapper already exists for {}: {}", tool, wrapper_path.display());
return false;
}
true
}
fn detect_installed_tools(plan: &InstallationPlan) -> Vec<String> {
let mut tools = Vec::new();
const DEFAULT_PATHS: &[&str] = &["usr/bin/{}", "bin/{}"];
const TOOL_ALT_PATHS: &[(&str, &[&str])] = &[
("go", &["usr/lib/go/bin/go", "usr/lib/golang/bin/go"]),
("cargo", &["usr/lib/rust/bin/cargo"]),
("pip", &["usr/lib/python3/bin/pip"]),
("pip3", &["usr/lib/python3/bin/pip3"]),
("npm", &["usr/share/nodejs/bin/npm"]),
("node", &["usr/lib/nodejs/bin/node"]),
("npx", &["usr/share/nodejs/bin/npx"]),
("gem", &["usr/lib/ruby/bin/gem"]),
("bundle", &["usr/lib/ruby/bin/bundle"]),
("mvn", &["usr/share/maven/bin/mvn"]),
];
const BREW_TOOL_PATHS: &[(&str, &str)] = &[
("go", "Cellar/go/"),
("cargo", "Cellar/cargo/"),
("pip", "Cellar/pip/"),
("pip3", "Cellar/pip3/"),
("npm", "Cellar/npm/"),
("node", "Cellar/node/"),
("npx", "Cellar/npx/"),
("gem", "Cellar/gem/"),
("bundle", "Cellar/bundle/"),
("mvn", "Cellar/maven/"),
];
for file in &plan.batch.new_files {
let file_str = file.to_string_lossy().replace('\\', "/");
log::debug!("Checking new_file: {}", file_str);
// Check brew package paths (Cellar/TOOL/VERSION/...)
// Brew binaries are at Cellar/TOOL/VERSION/libexec/bin/TOOL or Cellar/TOOL/VERSION/bin/TOOL
for (tool, prefix) in BREW_TOOL_PATHS {
if file_str.starts_with(prefix) {
// Check if this is the main binary (libexec/bin/tool or bin/tool)
let bin_suffix = format!("libexec/bin/{}", tool);
let alt_bin_suffix = format!("bin/{}", tool);
if file_str.contains(&bin_suffix) || file_str.ends_with(&alt_bin_suffix) {
log::debug!("Detected brew tool: {} from path: {}", tool, file_str);
if !tools.contains(&tool.to_string()) {
tools.push(tool.to_string());
}
break;
}
}
}
// Check if file matches any tool's alternative paths
for (tool, alt_paths) in TOOL_ALT_PATHS {
for path in *alt_paths {
if file_str == *path {
log::debug!("Detected tool: {} from path: {}", tool, path);
if !tools.contains(&tool.to_string()) {
tools.push(tool.to_string());
}
break;
}
}
}
for tool in SUPPORTED_TOOLS {
for path_template in DEFAULT_PATHS {
let expected_path = path_template.replace("{}", tool);
if file_str == expected_path {
log::debug!("Detected tool: {} from default path: {}", tool, expected_path);
if !tools.contains(&tool.to_string()) {
tools.push(tool.to_string());
}
break;
}
}
}
}
log::debug!("Detected tools: {:?}", tools);
tools
}
fn get_wrapper_content(tool: &str) -> Result<String> {
let epkg_src = dirs::get_epkg_src_path();
let base = crate::dirs::path_join(&epkg_src, &["assets", "tool", "wrappers"]);
let wrapper_path = base.join(tool);
if lfs::exists_on_host(&wrapper_path) {
let content = std::fs::read_to_string(&wrapper_path)
.with_context(|| format!("Failed to read wrapper script: {}", wrapper_path.display()))?;
return Ok(content);
}
let generic = base.join("shell-wrapper.sh");
if lfs::exists_on_host(&generic) {
let content = std::fs::read_to_string(&generic)
.with_context(|| format!("Failed to read generic shell wrapper: {}", generic.display()))?;
return Ok(content);
}
Err(eyre::eyre!("No wrapper script found for tool: {}", tool))
}
#[cfg(windows)]
#[derive(Clone, Copy)]
enum WindowsShimKind {
Python,
Ruby,
PosixShell,
}
#[cfg(windows)]
fn detect_windows_shim_kind(content: &str) -> Option<WindowsShimKind> {
let shebang = content.lines().next()?.trim();
if shebang.contains("python") {
return Some(WindowsShimKind::Python);
}
if shebang.contains("ruby") {
return Some(WindowsShimKind::Ruby);
}
if shebang.contains("/bin/sh") || shebang.contains("/bin/bash") || shebang.contains("/usr/bin/env sh") {
return Some(WindowsShimKind::PosixShell);
}
None
}
#[cfg(windows)]
fn cmd_shim_template_filename(kind: WindowsShimKind) -> &'static str {
match kind {
WindowsShimKind::Python => "python.cmd",
WindowsShimKind::Ruby => "ruby.cmd",
WindowsShimKind::PosixShell => "posix_shell.cmd",
}
}
#[cfg(windows)]
fn load_windows_cmd_shim_template(kind: WindowsShimKind) -> Result<String> {
let epkg_src = dirs::get_epkg_src_path();
let path = crate::dirs::path_join(
&epkg_src,
&["assets", "tool", "cmd_shims", cmd_shim_template_filename(kind)],
);
if !lfs::exists_on_host(&path) {
return Err(eyre::eyre!(
"Windows CMD shim template missing: {} (expected under EPKG_SRC assets)",
path.display()
));
}
std::fs::read_to_string(&path)
.with_context(|| format!("Failed to read CMD shim template {}", path.display()))
}
#[cfg(windows)]
fn write_windows_cmd_shim(wrapper_dir: &Path, tool: &str, script_content: &str) -> Result<()> {
let Some(kind) = detect_windows_shim_kind(script_content) else {
log::debug!(
"No Windows .cmd shim for tool {} (unrecognized shebang); MSYS2/Git Bash can still run the extensionless script",
tool
);
return Ok(());
};
let body = load_windows_cmd_shim_template(kind)?;
let cmd_path = wrapper_dir.join(format!("{}.cmd", tool));
lfs::write(&cmd_path, body.as_bytes())
.with_context(|| format!("Failed to write {}", cmd_path.display()))?;
log::info!("Created Windows CMD launcher: {}", cmd_path.display());
Ok(())
}
fn create_tool_wrapper(tool: &str, env_root: &Path) -> Result<()> {
let wrapper_dir = crate::dirs::path_join(env_root, &["usr", "local", "bin"]);
lfs::create_dir_all(&wrapper_dir)?;
let wrapper_path = wrapper_dir.join(tool);
let content = get_wrapper_content(tool)?;
lfs::write(&wrapper_path, &content)?;
crate::utils::set_permissions_from_mode(&wrapper_path, 0o755)
.with_context(|| format!("Failed to set permissions for {}", wrapper_path.display()))?;
#[cfg(windows)]
write_windows_cmd_shim(&wrapper_dir, tool, &content)?;
log::info!("Created tool wrapper: {}", wrapper_path.display());
Ok(())
}
#[allow(dead_code)]
fn remove_tool_wrapper(tool: &str, env_root: &Path) -> Result<()> {
let wrapper_dir = crate::dirs::path_join(env_root, &["usr", "local", "bin"]);
let wrapper_path = wrapper_dir.join(tool);
if lfs::exists_in_env(&wrapper_path) {
lfs::remove_file(&wrapper_path)?;
log::info!("Removed tool wrapper: {}", wrapper_path.display());
}
#[cfg(windows)]
{
let cmd_path = wrapper_dir.join(format!("{}.cmd", tool));
if lfs::exists_in_env(&cmd_path) {
lfs::remove_file(&cmd_path)?;
log::info!("Removed Windows CMD launcher: {}", cmd_path.display());
}
}
Ok(())
}
pub fn setup_tool_wrappers(plan: &InstallationPlan) -> Result<()> {
log::debug!("setup_tool_wrappers: checking for newly installed tools");
log::debug!("setup_tool_wrappers: new_files count = {}", plan.batch.new_files.len());
for f in &plan.batch.new_files {
log::debug!("setup_tool_wrappers: new_file = {}", f.display());
}
let env_root = PathBuf::from(&plan.env_root);
setup_tool_config_symlinks();
let tools = detect_installed_tools(plan);
if tools.is_empty() {
log::debug!("setup_tool_wrappers: no supported tools detected in new files");
return Ok(());
}
log::debug!("Detected newly installed tools: {:?}", tools);
for tool in &tools {
if should_create_wrapper(tool, &env_root) {
create_tool_wrapper(tool, &env_root)?;
}
}
Ok(())
}
#[allow(dead_code)]
pub fn remove_tool_wrappers(_plan: &InstallationPlan) -> Result<()> {
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_country_to_region() {
assert_eq!(country_to_region("CN"), Some("cn"));
assert_eq!(country_to_region("US"), Some("us"));
assert_eq!(country_to_region("DE"), Some("eu"));
assert_eq!(country_to_region("FR"), Some("eu"));
assert_eq!(country_to_region("GB"), Some("eu"));
assert_eq!(country_to_region("JP"), Some("us"));
assert_eq!(country_to_region("XX"), None);
}
}