diff --git a/src/bin/fish.rs b/src/bin/fish.rs index 5de92bd56..658ca8d79 100644 --- a/src/bin/fish.rs +++ b/src/bin/fish.rs @@ -40,7 +40,7 @@ }, env::{ environment::{env_init, EnvStack, Environment}, - ConfigPaths, EnvMode, Statuses, + ConfigPaths, EnvMode, Statuses, CONFIG_PATHS, }, eprintf, event::{self, Event}, @@ -76,16 +76,10 @@ use std::sync::Arc; use std::{env, ops::ControlFlow}; -const DOC_DIR: &str = env!("DOCDIR"); -const DATA_DIR: &str = env!("DATADIR"); -const DATA_DIR_SUBDIR: &str = env!("DATADIR_SUBDIR"); -const SYSCONF_DIR: &str = env!("SYSCONFDIR"); -const BIN_DIR: &str = env!("BINDIR"); - #[cfg(feature = "installable")] // Disable for clippy because otherwise it would require sphinx #[cfg(not(clippy))] -fn install(confirm: bool, dir: PathBuf) -> bool { +fn install(confirm: bool, dir: &PathBuf) -> bool { use rust_embed::RustEmbed; #[derive(RustEmbed)] @@ -185,7 +179,7 @@ fn extract_embed(dir: &Path) -> bool { } #[cfg(clippy)] -fn install(_confirm: bool, _dir: PathBuf) -> bool { +fn install(_confirm: bool, _dir: &PathBuf) -> bool { unreachable!() } @@ -250,128 +244,6 @@ fn print_rusage_self() { eprintln!(" signals: {signals}"); } -fn determine_config_directory_paths(argv0: impl AsRef) -> ConfigPaths { - // PORTING: why is this not just an associated method on ConfigPaths? - - let mut paths = ConfigPaths::default(); - let mut done = false; - let exec_path = get_executable_path(argv0.as_ref()); - if let Ok(exec_path) = exec_path.canonicalize() { - FLOG!( - config, - format!("exec_path: {:?}, argv[0]: {:?}", exec_path, argv0.as_ref()) - ); - // TODO: we should determine program_name from argv0 somewhere in this file - - // Detect if we're running right out of the CMAKE build directory - if exec_path.starts_with(env!("CARGO_MANIFEST_DIR")) { - let manifest_dir = Path::new(env!("CARGO_MANIFEST_DIR")); - FLOG!( - config, - "Running out of target directory, using paths relative to CARGO_MANIFEST_DIR:\n", - manifest_dir.display() - ); - done = true; - paths = ConfigPaths { - data: manifest_dir.join("share"), - sysconf: manifest_dir.join("etc"), - doc: manifest_dir.join("user_doc/html"), - bin: Some(exec_path.parent().unwrap().to_owned()), - } - } - - if !done { - // The next check is that we are in a relocatable directory tree - if exec_path.ends_with("bin/fish") { - let base_path = exec_path.parent().unwrap().parent().unwrap(); - paths = ConfigPaths { - // One obvious path is ~/.local (with fish in ~/.local/bin/). - // If we picked ~/.local/share/fish as our data path, - // we would install there and erase history. - // So let's isolate us a bit more. - #[cfg(feature = "installable")] - data: base_path.join("share/fish/install"), - #[cfg(not(feature = "installable"))] - data: base_path.join("share/fish"), - sysconf: base_path.join("etc/fish"), - doc: base_path.join("share/doc/fish"), - bin: Some(base_path.join("bin")), - } - } else if exec_path.ends_with("fish") { - FLOG!( - config, - "'fish' not in a 'bin/', trying paths relative to source tree" - ); - let base_path = exec_path.parent().unwrap(); - paths = ConfigPaths { - #[cfg(feature = "installable")] - data: base_path.join("share/install"), - #[cfg(not(feature = "installable"))] - data: base_path.join("share"), - sysconf: base_path.join("etc"), - doc: base_path.join("user_doc/html"), - bin: Some(base_path.to_path_buf()), - } - } - - if paths.data.exists() && paths.sysconf.exists() { - // The docs dir may not exist; in that case fall back to the compiled in path. - if !paths.doc.exists() { - paths.doc = PathBuf::from(DOC_DIR); - } - done = true; - } - } - } - - if !done { - // Fall back to what got compiled in. - let data = if cfg!(feature = "installable") { - let Some(home) = fish::env::get_home() else { - FLOG!( - error, - "Cannot find home directory and will refuse to read configuration.\n", - "Consider installing into a directory tree with `fish --install=PATH`." - ); - return paths; - }; - - PathBuf::from(home).join(DATA_DIR).join(DATA_DIR_SUBDIR) - } else { - Path::new(DATA_DIR).join(DATA_DIR_SUBDIR) - }; - let bin = if cfg!(feature = "installable") { - exec_path.parent().map(|x| x.to_path_buf()) - } else { - Some(PathBuf::from(BIN_DIR)) - }; - - FLOG!(config, "Using compiled in paths:"); - paths = ConfigPaths { - data, - sysconf: Path::new(SYSCONF_DIR).join("fish"), - doc: DOC_DIR.into(), - bin, - } - } - - FLOGF!( - config, - "determine_config_directory_paths() results:\npaths.data: %ls\npaths.sysconf: \ - %ls\npaths.doc: %ls\npaths.bin: %ls", - paths.data.display().to_string(), - paths.sysconf.display().to_string(), - paths.doc.display().to_string(), - paths - .bin - .clone() - .map(|x| x.display().to_string()) - .unwrap_or("|not found|".to_string()), - ); - - paths -} - // Source the file config.fish in the given directory. // Returns true if successful, false if not. fn source_config_in_directory(parser: &Parser, dir: &wstr) -> bool { @@ -449,7 +321,7 @@ fn read_init(parser: &Parser, paths: &ConfigPaths) { ); } - install(true, PathBuf::from(wcs2osstring(&datapath))); + install(true, &PathBuf::from(wcs2osstring(&datapath))); // We try to go on if installation failed (or was rejected) here // If the assets are missing, we will trigger a later error, // if they are outdated, things will probably (tm) work somewhat. @@ -579,7 +451,7 @@ fn fish_parse_opt(args: &mut [WString], opts: &mut FishCmdOpts) -> ControlFlow ControlFlow opts.is_login = true, @@ -812,13 +682,12 @@ fn throwing_main() -> i32 { save_term_foreground_process_group(); } + let mut paths: Option<&ConfigPaths> = None; // If we're not executing, there's no need to find the config. - let paths: Option = if !opts.no_exec { - let paths = Some(determine_config_directory_paths(OsString::from_vec( - wcs2string(&args[0]), - ))); + if !opts.no_exec { + paths = Some(&*CONFIG_PATHS); env_init( - paths.as_ref(), + paths, /* do uvars */ !opts.no_config, /* default paths */ opts.no_config, ); diff --git a/src/env/config_paths.rs b/src/env/config_paths.rs new file mode 100644 index 000000000..86688a6dc --- /dev/null +++ b/src/env/config_paths.rs @@ -0,0 +1,183 @@ +use std::{ + ffi::OsStr, + path::{Path, PathBuf}, +}; + +use once_cell::sync::Lazy; + +use crate::{FLOG, FLOGF}; + +use super::ConfigPaths; + +const DOC_DIR: &str = env!("DOCDIR"); +const DATA_DIR: &str = env!("DATADIR"); +const DATA_DIR_SUBDIR: &str = env!("DATADIR_SUBDIR"); +const SYSCONF_DIR: &str = env!("SYSCONFDIR"); +const BIN_DIR: &str = env!("BINDIR"); + +pub static CONFIG_PATHS: Lazy = Lazy::new(|| { + // Read the current executable and follow all symlinks to it. + // OpenBSD has issues with `std::env::current_exe`, see gh-9086 and + // https://github.com/rust-lang/rust/issues/60560 + let argv0 = PathBuf::from(std::env::args().next().unwrap()); + let argv0 = if argv0.exists() { + argv0 + } else { + std::env::current_exe().unwrap_or(argv0) + }; + let argv0 = argv0.canonicalize().unwrap_or(argv0); + determine_config_directory_paths(argv0) +}); + +fn determine_config_directory_paths(argv0: impl AsRef) -> ConfigPaths { + // PORTING: why is this not just an associated method on ConfigPaths? + + let mut paths = ConfigPaths::default(); + let mut done = false; + let exec_path = get_executable_path(argv0.as_ref()); + if let Ok(exec_path) = exec_path.canonicalize() { + FLOG!( + config, + format!("exec_path: {:?}, argv[0]: {:?}", exec_path, argv0.as_ref()) + ); + // TODO: we should determine program_name from argv0 somewhere in this file + + // Detect if we're running right out of the CMAKE build directory + if exec_path.starts_with(env!("CARGO_MANIFEST_DIR")) { + let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")); + FLOG!( + config, + "Running out of target directory, using paths relative to CARGO_MANIFEST_DIR:\n", + manifest_dir.display() + ); + done = true; + paths = ConfigPaths { + data: manifest_dir.join("share"), + sysconf: manifest_dir.join("etc"), + doc: manifest_dir.join("user_doc/html"), + bin: Some(exec_path.parent().unwrap().to_owned()), + locale: manifest_dir.join("share/locale"), + } + } + + if !done { + // The next check is that we are in a relocatable directory tree + if exec_path.ends_with("bin/fish") { + let base_path = exec_path.parent().unwrap().parent().unwrap(); + #[cfg(feature = "installable")] + let data_dir = base_path.join("share/fish/install"); + #[cfg(not(feature = "installable"))] + let data_dir = base_path.join("share/fish"); + paths = ConfigPaths { + // One obvious path is ~/.local (with fish in ~/.local/bin/). + // If we picked ~/.local/share/fish as our data path, + // we would install there and erase history. + // So let's isolate us a bit more. + data: data_dir.clone(), + sysconf: base_path.join("etc/fish"), + doc: base_path.join("share/doc/fish"), + bin: Some(base_path.join("bin")), + locale: data_dir.join("locale"), + } + } else if exec_path.ends_with("fish") { + FLOG!( + config, + "'fish' not in a 'bin/', trying paths relative to source tree" + ); + let base_path = exec_path.parent().unwrap(); + #[cfg(feature = "installable")] + let data_dir = base_path.join("share/install"); + #[cfg(not(feature = "installable"))] + let data_dir = base_path.join("share"); + paths = ConfigPaths { + data: data_dir.clone(), + sysconf: base_path.join("etc"), + doc: base_path.join("user_doc/html"), + bin: Some(base_path.to_path_buf()), + locale: data_dir.join("locale"), + } + } + + if paths.data.exists() && paths.sysconf.exists() { + // The docs dir may not exist; in that case fall back to the compiled in path. + if !paths.doc.exists() { + paths.doc = PathBuf::from(DOC_DIR); + } + done = true; + } + } + } + + if !done { + // Fall back to what got compiled in. + let data = if cfg!(feature = "installable") { + let Some(home) = crate::env::get_home() else { + FLOG!( + error, + "Cannot find home directory and will refuse to read configuration.\n", + "Consider installing into a directory tree with `fish --install=PATH`." + ); + return paths; + }; + + PathBuf::from(home).join(DATA_DIR).join(DATA_DIR_SUBDIR) + } else { + PathBuf::from(DATA_DIR).join(DATA_DIR_SUBDIR) + }; + let bin = if cfg!(feature = "installable") { + exec_path.parent().map(|x| x.to_path_buf()) + } else { + Some(PathBuf::from(BIN_DIR)) + }; + + FLOG!(config, "Using compiled in paths:"); + paths = ConfigPaths { + data: data.clone(), + sysconf: PathBuf::from(SYSCONF_DIR).join("fish"), + doc: DOC_DIR.into(), + bin, + locale: data.join("share"), + } + } + + FLOGF!( + config, + "determine_config_directory_paths() results:\npaths.data: %ls\npaths.sysconf: \ + %ls\npaths.doc: %ls\npaths.bin: %ls", + paths.data.display().to_string(), + paths.sysconf.display().to_string(), + paths.doc.display().to_string(), + paths + .bin + .clone() + .map(|x| x.display().to_string()) + .unwrap_or("|not found|".to_string()), + ); + + paths +} + +/// Get the absolute path to the fish executable itself +pub fn get_executable_path(argv0: impl AsRef) -> PathBuf { + if let Ok(path) = std::env::current_exe() { + if path.exists() { + return path; + } + + // When /proc/self/exe points to a file that was deleted (or overwritten on update!) + // then linux adds a " (deleted)" suffix. + // If that's not a valid path, let's remove that awkward suffix. + if !path.ends_with(" (deleted)") { + return path; + } + + if let (Some(filename), Some(parent)) = (path.file_name(), path.parent()) { + if let Some(filename) = filename.to_str() { + let corrected_filename = OsStr::new(filename.strip_suffix(" (deleted)").unwrap()); + return parent.join(corrected_filename); + } + } + return path; + } + argv0.as_ref().to_owned() +} diff --git a/src/env/mod.rs b/src/env/mod.rs index a27beba93..4af6b899b 100644 --- a/src/env/mod.rs +++ b/src/env/mod.rs @@ -1,6 +1,8 @@ +mod config_paths; pub mod environment; mod environment_impl; pub mod var; +pub use config_paths::CONFIG_PATHS; use crate::common::ToCString; pub use environment::*; diff --git a/src/env/var.rs b/src/env/var.rs index 62ec9920e..d14d6bce7 100644 --- a/src/env/var.rs +++ b/src/env/var.rs @@ -54,6 +54,7 @@ pub struct ConfigPaths { pub sysconf: PathBuf, // e.g., /usr/local/etc pub doc: PathBuf, // e.g., /usr/local/share/doc/fish pub bin: Option, // e.g., /usr/local/bin + pub locale: PathBuf, // e.g., /usr/local/share/locale } /// A collection of status and pipestatus. diff --git a/src/wutil/gettext.rs b/src/wutil/gettext.rs index ab968ed34..392bc6d6d 100644 --- a/src/wutil/gettext.rs +++ b/src/wutil/gettext.rs @@ -3,6 +3,7 @@ use std::sync::Mutex; use crate::common::{charptr2wcstring, truncate_at_nul, wcs2zstring, PACKAGE_NAME}; +use crate::env::CONFIG_PATHS; #[cfg(test)] use crate::tests::prelude::*; use crate::wchar::prelude::*; @@ -48,7 +49,9 @@ pub fn fish_textdomain(_domainname: &CStr) -> *mut c_char { // Really init wgettext. fn wgettext_really_init() { let package_name = CString::new(PACKAGE_NAME).unwrap(); - let localedir = CString::new(env!("LOCALEDIR")).unwrap(); + // This contains `datadir`; which when replaced to make the binary relocatable, + // causes null bytes at the end of the string. + let localedir = CString::new(CONFIG_PATHS.locale.display().to_string()).unwrap(); fish_bindtextdomain(&package_name, &localedir); fish_textdomain(&package_name); }