diff --git a/Cargo.lock b/Cargo.lock index 54cc0155..81769b76 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1471,6 +1471,12 @@ dependencies = [ "minimal-lexical", ] +[[package]] +name = "normalize-path" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f5438dd2b2ff4c6df6e1ce22d825ed2fa93ee2922235cc45186991717f0a892d" + [[package]] name = "num-bigint" version = "0.2.6" @@ -1547,6 +1553,7 @@ dependencies = [ "libz-sys", "machine-uid", "node-semver", + "normalize-path", "once_cell", "openssl", "package-json", diff --git a/Cargo.toml b/Cargo.toml index e57fb135..40e9d73a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -39,6 +39,7 @@ lazy_static = "1.4.0" libz-sys = { version = "1.1.12", features = ["static"] } machine-uid = "0.5.1" node-semver = "2.1.0" +normalize-path = "0.2.1" once_cell = "1.19.0" openssl = { version = "0.10", features = ["vendored"] } package-json = "0.4.0" diff --git a/src/internal/commands/utils.rs b/src/internal/commands/utils.rs index 8dc65d3c..48044f5d 100644 --- a/src/internal/commands/utils.rs +++ b/src/internal/commands/utils.rs @@ -3,8 +3,8 @@ use std::io; use std::io::Write; use std::path::{Path, PathBuf}; +use normalize_path::NormalizePath; use path_clean::PathClean; - use requestty::question::{completions, Completions}; use crate::internal::env::omni_cmd_file; @@ -16,7 +16,7 @@ pub fn split_name(string: &str, split_on: &str) -> Vec { pub fn abs_or_rel_path(path: &str) -> String { let current_dir = std::env::current_dir().unwrap(); - let path = std::path::PathBuf::from(&path); + let path = std::path::PathBuf::from(&path).normalize(); let path = if path.is_absolute() { path } else { @@ -44,7 +44,7 @@ pub fn abs_or_rel_path(path: &str) -> String { } pub fn abs_path(path: impl AsRef) -> PathBuf { - let path = path.as_ref(); + let path = path.as_ref().normalize(); let absolute_path = if path.is_absolute() { path.to_path_buf() diff --git a/src/internal/config/up/asdf_base.rs b/src/internal/config/up/asdf_base.rs index 33759b34..01506ba5 100644 --- a/src/internal/config/up/asdf_base.rs +++ b/src/internal/config/up/asdf_base.rs @@ -9,6 +9,7 @@ use itertools::any; use lazy_static::lazy_static; use node_semver::Range as semverRange; use node_semver::Version as semverVersion; +use normalize_path::NormalizePath; use once_cell::sync::OnceCell; use serde::Deserialize; use serde::Serialize; @@ -18,6 +19,7 @@ use walkdir::WalkDir; use crate::internal::cache::AsdfOperationCache; use crate::internal::cache::CacheObject; use crate::internal::cache::UpEnvironmentsCache; +use crate::internal::config::up::utils::force_remove_dir_all; use crate::internal::config::up::utils::run_progress; use crate::internal::config::up::utils::PrintProgressHandler; use crate::internal::config::up::utils::ProgressHandler; @@ -192,12 +194,12 @@ pub struct UpConfigAsdfBase { } impl UpConfigAsdfBase { - pub fn new(tool: &str, version: &str) -> Self { + pub fn new(tool: &str, version: &str, dirs: BTreeSet) -> Self { UpConfigAsdfBase { tool: tool.to_string(), tool_url: None, version: version.to_string(), - dirs: BTreeSet::new(), + dirs: dirs.clone(), detect_version_funcs: vec![], post_install_funcs: vec![], actual_version: OnceCell::new(), @@ -259,15 +261,27 @@ impl UpConfigAsdfBase { } else if let Some(value) = config_value.as_integer() { version = value.to_string(); } else { - if let Some(value) = config_value.get("version") { - version = value.as_str().unwrap().to_string(); + if let Some(value) = config_value.get_as_str_forced("version") { + version = value.to_string(); } if let Some(value) = config_value.get_as_str("dir") { - dirs.insert(value.to_string()); + dirs.insert( + PathBuf::from(value) + .normalize() + .to_string_lossy() + .to_string(), + ); } else if let Some(array) = config_value.get_as_array("dir") { for value in array { - dirs.insert(value.as_str().unwrap().to_string()); + if let Some(value) = value.as_str_forced() { + dirs.insert( + PathBuf::from(value) + .normalize() + .to_string_lossy() + .to_string(), + ); + } } } } @@ -780,7 +794,7 @@ impl UpConfigAsdfBase { // If any data path in the versions if !any(&env_tools, |tool| tool.data_path.is_some()) { - std::fs::remove_dir_all(wd_data_path).map_err(|err| { + force_remove_dir_all(wd_data_path).map_err(|err| { UpError::Exec(format!( "failed to remove workdir data path {}: {}", wd_data_path.display(), @@ -811,10 +825,10 @@ impl UpConfigAsdfBase { // Remove the tool directory if the tool is not expected if !expected_tools.contains(&tool_dir_name) { - std::fs::remove_dir_all(tool_dir.path()).map_err(|err| { + force_remove_dir_all(tool_dir.path()).map_err(|err| { UpError::Exec(format!( - "failed to remove workdir data path for workdir {}: {}", - workdir_id, err + "failed to remove workdir data path for tool {}: {}", + tool_dir_name, err )) })?; continue; @@ -847,7 +861,7 @@ impl UpConfigAsdfBase { // Remove the version directory if the version is not expected if !expected_versions.contains(&version_dir_name) { - std::fs::remove_dir_all(version_dir.path()).map_err(|err| { + force_remove_dir_all(version_dir.path()).map_err(|err| { UpError::Exec(format!( "failed to remove workdir data path for workdir {}, tool {} and version {}: {}", workdir_id, tool_dir_name, version_dir_name, err @@ -890,7 +904,7 @@ impl UpConfigAsdfBase { // Remove the path directory if the path is not expected if !expected_paths.contains(&path_dir_name) { - std::fs::remove_dir_all(path_dir.path()).map_err(|err| { + force_remove_dir_all(path_dir.path()).map_err(|err| { UpError::Exec(format!( "failed to remove workdir data path for workdir {}, tool {}, version {} and path {}: {}", workdir_id, tool_dir_name, version_dir_name, path_dir_name, err diff --git a/src/internal/config/up/golang.rs b/src/internal/config/up/golang.rs index 541765bb..b1d3f0a9 100644 --- a/src/internal/config/up/golang.rs +++ b/src/internal/config/up/golang.rs @@ -1,22 +1,32 @@ +use std::collections::BTreeSet; use std::fs::File; use std::io::BufRead; use std::io::BufReader; use std::path::Path; use std::path::PathBuf; +use normalize_path::NormalizePath; use once_cell::sync::OnceCell; use serde::{Deserialize, Serialize}; +use crate::internal::cache::utils::CacheObject; +use crate::internal::cache::UpEnvironmentsCache; use crate::internal::commands::utils::abs_path; +use crate::internal::config::up::utils::data_path_dir_hash; +use crate::internal::config::up::AsdfToolUpVersion; +use crate::internal::config::up::ProgressHandler; use crate::internal::config::up::UpConfigAsdfBase; use crate::internal::config::up::UpError; use crate::internal::config::up::UpOptions; +use crate::internal::env::current_dir; +use crate::internal::workdir; use crate::internal::ConfigValue; #[derive(Debug, Serialize, Deserialize, Clone)] pub struct UpConfigGolang { pub version: Option, pub version_file: Option, + pub dirs: BTreeSet, #[serde(skip)] pub asdf_base: OnceCell, } @@ -25,6 +35,7 @@ impl UpConfigGolang { pub fn from_config_value(config_value: Option<&ConfigValue>) -> Self { let mut version = None; let mut version_file = None; + let mut dirs = BTreeSet::new(); if let Some(config_value) = config_value { if let Some(value) = config_value.as_str() { @@ -33,11 +44,31 @@ impl UpConfigGolang { version = Some(value.to_string()); } else if let Some(value) = config_value.as_integer() { version = Some(value.to_string()); - } else if let Some(value) = config_value.as_table() { - if let Some(value) = value.get("version") { - version = Some(value.as_str().unwrap().to_string()); - } else if let Some(value) = value.get("version_file") { - version_file = Some(value.as_str().unwrap().to_string()); + } else { + if let Some(value) = config_value.get_as_str_forced("version") { + version = Some(value.to_string()); + } else if let Some(value) = config_value.get_as_str_forced("version_file") { + version_file = Some(value.to_string()); + } + + if let Some(value) = config_value.get_as_str("dir") { + dirs.insert( + PathBuf::from(value) + .normalize() + .to_string_lossy() + .to_string(), + ); + } else if let Some(array) = config_value.get_as_array("dir") { + for value in array { + if let Some(value) = value.as_str_forced() { + dirs.insert( + PathBuf::from(value) + .normalize() + .to_string_lossy() + .to_string(), + ); + } + } } } } @@ -46,6 +77,7 @@ impl UpConfigGolang { asdf_base: OnceCell::new(), version, version_file, + dirs, } } @@ -67,8 +99,10 @@ impl UpConfigGolang { "latest".to_string() }; - let mut asdf_base = UpConfigAsdfBase::new("golang", version.as_ref()); + let mut asdf_base = + UpConfigAsdfBase::new("golang", version.as_ref(), self.dirs.clone()); asdf_base.add_detect_version_func(detect_version_from_gomod); + asdf_base.add_post_install_func(setup_individual_gopath); Ok(asdf_base) }) @@ -140,3 +174,66 @@ fn extract_version_from_gomod_file( version_file.display(), ))) } + +fn setup_individual_gopath( + progress_handler: &dyn ProgressHandler, + tool: String, + versions: Vec, +) -> Result<(), UpError> { + if tool != "golang" { + panic!("setup_individual_gopath called with wrong tool: {}", tool); + } + + // Get the data path for the work directory + let workdir = workdir("."); + + let workdir_id = if let Some(workdir_id) = workdir.id() { + workdir_id + } else { + return Err(UpError::Exec(format!( + "failed to get workdir id for {}", + current_dir().display() + ))); + }; + + let data_path = if let Some(data_path) = workdir.data_path() { + data_path + } else { + return Err(UpError::Exec(format!( + "failed to get data path for {}", + current_dir().display() + ))); + }; + + // Handle each version individually + for version in &versions { + if let Err(err) = UpEnvironmentsCache::exclusive(|up_env| { + let mut any_changed = false; + for dir in &version.dirs { + let gopath_dir = data_path_dir_hash(dir); + + let gopath = data_path + .join(&tool) + .join(&version.version) + .join(&gopath_dir); + + any_changed = up_env.add_version_data_path( + &workdir_id, + &tool, + &version.version, + dir, + &gopath.to_string_lossy(), + ) || any_changed; + } + any_changed + }) { + progress_handler.progress(format!("failed to update tool cache: {}", err)); + return Err(UpError::Cache(format!( + "failed to update tool cache: {}", + err + ))); + } + } + + Ok(()) +} diff --git a/src/internal/config/up/python.rs b/src/internal/config/up/python.rs index e4b13ac6..535daa32 100644 --- a/src/internal/config/up/python.rs +++ b/src/internal/config/up/python.rs @@ -1,6 +1,5 @@ use std::path::PathBuf; -use blake3::Hasher; use semver::Version; use serde::{Deserialize, Serialize}; use tokio::process::Command as TokioCommand; @@ -9,6 +8,7 @@ use crate::internal::cache::utils::CacheObject; use crate::internal::cache::UpEnvironmentsCache; use crate::internal::config::up::asdf_tool_path; use crate::internal::config::up::run_progress; +use crate::internal::config::up::utils::data_path_dir_hash; use crate::internal::config::up::utils::RunConfig; use crate::internal::config::up::AsdfToolUpVersion; use crate::internal::config::up::ProgressHandler; @@ -120,15 +120,7 @@ fn setup_python_venv_per_dir( }; // Get the hash of the relative path - let venv_dir = if dir.is_empty() { - "root".to_string() - } else { - let mut hasher = Hasher::new(); - hasher.update(dir.as_bytes()); - let hash_bytes = hasher.finalize(); - let hash_b62 = base_62::encode(hash_bytes.as_bytes())[..20].to_string(); - hash_b62 - }; + let venv_dir = data_path_dir_hash(&dir); let venv_path = data_path .join("python") diff --git a/src/internal/config/up/utils.rs b/src/internal/config/up/utils.rs index 07e8a9e2..128753fa 100644 --- a/src/internal/config/up/utils.rs +++ b/src/internal/config/up/utils.rs @@ -1,10 +1,14 @@ use std::io::Write; +use std::path::Path; +use blake3::Hasher; use indicatif::MultiProgress; use indicatif::ProgressBar; use indicatif::ProgressDrawTarget; use indicatif::ProgressStyle; +use normalize_path::NormalizePath; use regex::Regex; +use std::os::unix::fs::PermissionsExt; use tempfile::NamedTempFile; use time::format_description::well_known::Rfc3339; use tokio::io::AsyncBufReadExt; @@ -511,3 +515,59 @@ impl ProgressHandler for PrintProgressHandler { // do nothing } } + +/// Return the name of the directory to use in the data path +/// for the given subdirectory of the work directory. +pub fn data_path_dir_hash(dir: &str) -> String { + let dir = Path::new(dir).normalize().to_string_lossy().to_string(); + + if dir.is_empty() { + "root".to_string() + } else { + let mut hasher = Hasher::new(); + hasher.update(dir.as_bytes()); + let hash_bytes = hasher.finalize(); + let hash_b62 = base_62::encode(hash_bytes.as_bytes())[..20].to_string(); + hash_b62 + } +} + +/// Remove the given directory, even if it contains read-only files. +/// This will first try to remove the directory normally, and if that +/// fails with a PermissionDenied error, it will make all files and +/// directories in the given path writeable, and then try again. +pub fn force_remove_dir_all>(path: P) -> std::io::Result<()> { + let path = path.as_ref(); + if path.exists() { + match std::fs::remove_dir_all(path) { + Ok(_) => {} + Err(err) => { + if err.kind() == std::io::ErrorKind::PermissionDenied { + set_writeable_recursive(path)?; + std::fs::remove_dir_all(path)?; + } else { + return Err(err); + } + } + } + } + Ok(()) +} + +/// Set all files and directories in the given path to be writeable. +/// This is useful when we want to remove a directory that contains +/// read-only files, which would otherwise fail. +pub fn set_writeable_recursive>(path: P) -> std::io::Result<()> { + for entry in walkdir::WalkDir::new(&path) + .into_iter() + .filter_map(|e| e.ok()) + { + let metadata = entry.metadata()?; + let mut permissions = metadata.permissions(); + if permissions.readonly() { + permissions.set_mode(0o775); + std::fs::set_permissions(entry.path(), permissions)?; + } + } + Ok(()) +} diff --git a/src/internal/dynenv.rs b/src/internal/dynenv.rs index 6d8a70b8..93b7a3e6 100644 --- a/src/internal/dynenv.rs +++ b/src/internal/dynenv.rs @@ -323,7 +323,14 @@ impl DynamicEnv { envsetter.set_value("GOROOT", &format!("{}/go", tool_prefix)); envsetter.set_value("GOVERSION", &version); + envsetter.prepend_to_list("GOPATH", &format!("{}/go", tool_prefix)); envsetter.prepend_to_list("PATH", &format!("{}/go/bin", tool_prefix)); + + // Handle the isolated GOPATH + if let Some(data_path) = &toolversion.data_path { + envsetter.prepend_to_list("GOPATH", data_path); + envsetter.prepend_to_list("PATH", &format!("{}/bin", data_path)); + }; } "python" => { let tool_prefix = if let Some(data_path) = &toolversion.data_path {