mirror of
https://github.com/fish-shell/fish-shell.git
synced 2026-05-23 04:51:16 -03:00
Change gettext paths to be relocatable (#11195)
Move config path logic into its own module and use it for locale as well.
This commit is contained in:
153
src/bin/fish.rs
153
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<T: rust_embed::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<Path>) -> 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<i
|
||||
// path/etc/fish is sysconf????
|
||||
use std::fs;
|
||||
let dir = PathBuf::from(wcs2osstring(path));
|
||||
if install(true, dir.join("share/fish/install")) {
|
||||
if install(true, &dir.join("share/fish/install")) {
|
||||
for sub in &["share/fish/install", "etc/fish", "bin"] {
|
||||
let p = dir.join(sub);
|
||||
let Ok(_) = fs::create_dir_all(p.clone()) else {
|
||||
@@ -612,14 +484,12 @@ fn fish_parse_opt(args: &mut [WString], opts: &mut FishCmdOpts) -> ControlFlow<i
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let paths = Some(determine_config_directory_paths(OsString::from_vec(
|
||||
wcs2string(&args[0]),
|
||||
)));
|
||||
let paths = Some(&*CONFIG_PATHS);
|
||||
let Some(paths) = paths else {
|
||||
FLOG!(error, "Cannot find config paths");
|
||||
std::process::exit(1);
|
||||
};
|
||||
install(true, paths.data);
|
||||
install(true, &paths.data);
|
||||
}
|
||||
}
|
||||
'l' => 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<ConfigPaths> = 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,
|
||||
);
|
||||
|
||||
183
src/env/config_paths.rs
vendored
Normal file
183
src/env/config_paths.rs
vendored
Normal file
@@ -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<ConfigPaths> = 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<Path>) -> 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<Path>) -> 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()
|
||||
}
|
||||
2
src/env/mod.rs
vendored
2
src/env/mod.rs
vendored
@@ -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::*;
|
||||
|
||||
1
src/env/var.rs
vendored
1
src/env/var.rs
vendored
@@ -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<PathBuf>, // e.g., /usr/local/bin
|
||||
pub locale: PathBuf, // e.g., /usr/local/share/locale
|
||||
}
|
||||
|
||||
/// A collection of status and pipestatus.
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user