From 4665323bb4ba0931c690bf743b54a27ec493822a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Christian=20Gr=C3=BCnhage?= Date: Fri, 19 Jul 2024 12:47:07 +0200 Subject: [PATCH] Use directories for autoloading (#13382) fixes https://github.com/nushell/nushell/issues/13378 # Description This PR tries to improve usage of system APIs to determine the location of vendored autoload files. # User-Facing Changes The paths listed in #13180 and #13217 are changing. This has not been part of a release yet, so arguably the user facing changes are only to unreleased features anyway. # Tests + Formatting Haven't done, but if someone wants to help me here, I'm open to doing it. I just don't know how to properly test this. # After Submitting --- Cargo.lock | 3 + Cargo.toml | 2 + crates/nu-cli/tests/completions/mod.rs | 2 +- crates/nu-protocol/Cargo.toml | 5 + crates/nu-protocol/src/eval_const.rs | 143 ++++++++++++++++--------- src/config_files.rs | 3 +- 6 files changed, 107 insertions(+), 51 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 49770a9a0d..97f81d5185 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3344,6 +3344,8 @@ dependencies = [ "chrono", "chrono-humanize", "convert_case", + "dirs", + "dirs-sys", "fancy-regex", "indexmap", "log", @@ -3367,6 +3369,7 @@ dependencies = [ "tempfile", "thiserror", "typetag", + "windows-sys 0.48.0", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 335e2f0384..81602fb4b1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -84,6 +84,7 @@ deunicode = "1.6.0" dialoguer = { default-features = false, version = "0.11" } digest = { default-features = false, version = "0.10" } dirs = "5.0" +dirs-sys = "0.4" dtparse = "2.0" encoding_rs = "0.8" fancy-regex = "0.13" @@ -178,6 +179,7 @@ v_htmlescape = "0.15.0" wax = "0.6" which = "6.0.0" windows = "0.54" +windows-sys = "0.48" winreg = "0.52" [dependencies] diff --git a/crates/nu-cli/tests/completions/mod.rs b/crates/nu-cli/tests/completions/mod.rs index 590979251e..aa51e5c9bb 100644 --- a/crates/nu-cli/tests/completions/mod.rs +++ b/crates/nu-cli/tests/completions/mod.rs @@ -833,7 +833,7 @@ fn variables_completions() { "plugin-path".into(), "startup-time".into(), "temp-path".into(), - "vendor-autoload-dir".into(), + "vendor-autoload-dirs".into(), ]; // Match results diff --git a/crates/nu-protocol/Cargo.toml b/crates/nu-protocol/Cargo.toml index eaa861a073..ea900bf08d 100644 --- a/crates/nu-protocol/Cargo.toml +++ b/crates/nu-protocol/Cargo.toml @@ -23,6 +23,7 @@ byte-unit = { version = "5.1", features = [ "serde" ] } chrono = { workspace = true, features = [ "serde", "std", "unstable-locales" ], default-features = false } chrono-humanize = { workspace = true } convert_case = { workspace = true } +dirs = { workspace = true } fancy-regex = { workspace = true } indexmap = { workspace = true } lru = { workspace = true } @@ -38,6 +39,10 @@ log = { workspace = true } [target.'cfg(unix)'.dependencies] nix = { workspace = true, default-features = false, features = ["signal"] } +[target.'cfg(windows)'.dependencies] +dirs-sys = { workspace = true } +windows-sys = { workspace = true } + [features] plugin = [ "brotli", diff --git a/crates/nu-protocol/src/eval_const.rs b/crates/nu-protocol/src/eval_const.rs index cd4780773f..4cb5ccc0f5 100644 --- a/crates/nu-protocol/src/eval_const.rs +++ b/crates/nu-protocol/src/eval_const.rs @@ -185,24 +185,15 @@ pub(crate) fn create_nu_constant(engine_state: &EngineState, span: Span) -> Valu }, ); - // Create a system level directory for nushell scripts, modules, completions, etc - // that can be changed by setting the NU_VENDOR_AUTOLOAD_DIR env var on any platform - // before nushell is compiled OR if NU_VENDOR_AUTOLOAD_DIR is not set for non-windows - // systems, the PREFIX env var can be set before compile and used as PREFIX/nushell/vendor/autoload record.push( - "vendor-autoload-dir", - // pseudo code - // if env var NU_VENDOR_AUTOLOAD_DIR is set, in any platform, use it - // if not, if windows, use ALLUSERPROFILE\nushell\vendor\autoload - // if not, if non-windows, if env var PREFIX is set, use PREFIX/share/nushell/vendor/autoload - // if not, use the default /usr/share/nushell/vendor/autoload - - // check to see if NU_VENDOR_AUTOLOAD_DIR env var is set, if not, use the default - if let Some(path) = get_vendor_autoload_dir(engine_state) { - Value::string(path.to_string_lossy(), span) - } else { - Value::error(ShellError::ConfigDirNotFound { span: Some(span) }, span) - }, + "vendor-autoload-dirs", + Value::list( + get_vendor_autoload_dirs(engine_state) + .iter() + .map(|path| Value::string(path.to_string_lossy(), span)) + .collect(), + span, + ), ); record.push("temp-path", { @@ -259,39 +250,95 @@ pub(crate) fn create_nu_constant(engine_state: &EngineState, span: Span) -> Valu Value::record(record, span) } -pub fn get_vendor_autoload_dir(engine_state: &EngineState) -> Option { - // pseudo code - // if env var NU_VENDOR_AUTOLOAD_DIR is set, in any platform, use it - // if not, if windows, use ALLUSERPROFILE\nushell\vendor\autoload - // if not, if non-windows, if env var PREFIX is set, use PREFIX/share/nushell/vendor/autoload - // if not, use the default /usr/share/nushell/vendor/autoload +pub fn get_vendor_autoload_dirs(_engine_state: &EngineState) -> Vec { + // load order for autoload dirs + // /Library/Application Support/nushell/vendor/autoload on macOS + // /nushell/vendor/autoload for every dir in XDG_DATA_DIRS in reverse order on platforms other than windows. If XDG_DATA_DIRS is not set, it falls back to /share if PREFIX ends in local, or /local/share:/share otherwise. If PREFIX is not set, fall back to /usr/local/share:/usr/share. + // %ProgramData%\nushell\vendor\autoload on windows + // NU_VENDOR_AUTOLOAD_DIR from compile time, if env var is set at compile time + // if on macOS, additionally check XDG_DATA_HOME, which `dirs` is only doing on Linux + // /nushell/vendor/autoload of the current user according to the `dirs` crate + // NU_VENDOR_AUTOLOAD_DIR at runtime, if env var is set - // check to see if NU_VENDOR_AUTOLOAD_DIR env var is set, if not, use the default - Some( - option_env!("NU_VENDOR_AUTOLOAD_DIR") - .map(String::from) - .unwrap_or_else(|| { - if cfg!(windows) { - let all_user_profile = match engine_state.get_env_var("ALLUSERPROFILE") { - Some(v) => format!( - "{}\\nushell\\vendor\\autoload", - v.coerce_string().unwrap_or("C:\\ProgramData".into()) - ), - None => "C:\\ProgramData\\nushell\\vendor\\autoload".into(), - }; - all_user_profile - } else { - // In non-Windows environments, if NU_VENDOR_AUTOLOAD_DIR is not set - // check to see if PREFIX env var is set, and use it as PREFIX/nushell/vendor/autoload - // otherwise default to /usr/share/nushell/vendor/autoload - option_env!("PREFIX").map(String::from).map_or_else( - || "/usr/local/share/nushell/vendor/autoload".into(), - |prefix| format!("{}/share/nushell/vendor/autoload", prefix), - ) - } + let into_autoload_path_fn = |mut path: PathBuf| { + path.push("nushell"); + path.push("vendor"); + path.push("autoload"); + path + }; + + let mut dirs = Vec::new(); + + let mut append_fn = |path: PathBuf| { + if !dirs.contains(&path) { + dirs.push(path) + } + }; + + #[cfg(target_os = "macos")] + std::iter::once("/Library/Application Support") + .map(PathBuf::from) + .map(into_autoload_path_fn) + .for_each(&mut append_fn); + #[cfg(unix)] + { + use std::os::unix::ffi::OsStrExt; + + std::env::var_os("XDG_DATA_DIRS") + .or_else(|| { + option_env!("PREFIX").map(|prefix| { + if prefix.ends_with("local") { + std::ffi::OsString::from(format!("{prefix}/share")) + } else { + std::ffi::OsString::from(format!("{prefix}/local/share:{prefix}/share")) + } + }) }) - .into(), - ) + .unwrap_or_else(|| std::ffi::OsString::from("/usr/local/share/:/usr/share/")) + .as_encoded_bytes() + .split(|b| *b == b':') + .map(|split| into_autoload_path_fn(PathBuf::from(std::ffi::OsStr::from_bytes(split)))) + .rev() + .for_each(&mut append_fn); + } + + #[cfg(target_os = "windows")] + dirs_sys::known_folder(windows_sys::Win32::UI::Shell::FOLDERID_ProgramData) + .into_iter() + .map(into_autoload_path_fn) + .for_each(&mut append_fn); + + option_env!("NU_VENDOR_AUTOLOAD_DIR") + .into_iter() + .map(PathBuf::from) + .for_each(&mut append_fn); + + #[cfg(target_os = "macos")] + std::env::var("XDG_DATA_HOME") + .ok() + .map(PathBuf::from) + .or_else(|| { + dirs::home_dir().map(|mut home| { + home.push(".local"); + home.push("share"); + home + }) + }) + .map(into_autoload_path_fn) + .into_iter() + .for_each(&mut append_fn); + + dirs::data_dir() + .into_iter() + .map(into_autoload_path_fn) + .for_each(&mut append_fn); + + std::env::var_os("NU_VENDOR_AUTOLOAD_DIR") + .into_iter() + .map(PathBuf::from) + .for_each(&mut append_fn); + + dirs } fn eval_const_call( diff --git a/src/config_files.rs b/src/config_files.rs index 30977a6d0e..cf72819600 100644 --- a/src/config_files.rs +++ b/src/config_files.rs @@ -200,8 +200,7 @@ pub(crate) fn read_vendor_autoload_files(engine_state: &mut EngineState, stack: column!() ); - // read and source vendor_autoload_files file if exists - if let Some(autoload_dir) = nu_protocol::eval_const::get_vendor_autoload_dir(engine_state) { + for autoload_dir in nu_protocol::eval_const::get_vendor_autoload_dirs(engine_state) { warn!("read_vendor_autoload_files: {}", autoload_dir.display()); if autoload_dir.exists() {