Allow installable builds to be installed into a specific path (#10923)

* Pass path to install()

It was dirty that it would re-get $HOME there anyway.

* Import wcs2osstring

* Allow installable builds to use a relocatable tree

If you give a path to `--install`, it will install fish into a
relocatable tree there, so

PATH/share/fish contains the datafiles
PATH/bin/fish contains the fish executable
PATH/etc/fish is sysconf

I am absolutely not sold on that last one - the way I always used
sysconfdir is that it is always /etc. This would be easy to fix but
should probably also be fixed for "regular" relocatable builds (no
idea who uses them).

An attempt at #10916

* Move install path into "install/" subdir

* Disable --install harder if not installable
This commit is contained in:
Fabian Boehm 2024-12-22 18:13:29 +01:00
parent b19a467ea6
commit 3dc49d9d93
2 changed files with 76 additions and 16 deletions

View file

@ -40,9 +40,11 @@ The following options are available:
**-i** or **--interactive** **-i** or **--interactive**
The shell is interactive. The shell is interactive.
**--install** **--install[=PATH]**
When built as self-installable (via cargo), this will unpack fish's datafiles and place them in ~/.local/share/fish/install/. When built as self-installable (via cargo), this will unpack fish's datafiles and place them in ~/.local/share/fish/install/.
Fish will also ask to do this automatically when run interactively. Fish will also ask to do this automatically when run interactively.
If PATH is given, fish will install itself into a relocatable directory tree rooted at that path.
That means it will install the datafiles to PATH/share/fish and copy itself to PATH/bin/fish.
**-l** or **--login** **-l** or **--login**
Act as if invoked as a login shell. Act as if invoked as a login shell.

View file

@ -30,7 +30,7 @@ use fish::{
}, },
common::{ common::{
escape, get_executable_path, save_term_foreground_process_group, scoped_push_replacer, escape, get_executable_path, save_term_foreground_process_group, scoped_push_replacer,
str2wcstring, wcs2string, PACKAGE_NAME, PROFILING_ACTIVE, PROGRAM_NAME, str2wcstring, wcs2osstring, wcs2string, PACKAGE_NAME, PROFILING_ACTIVE, PROGRAM_NAME,
}, },
env::{ env::{
environment::{env_init, EnvStack, Environment}, environment::{env_init, EnvStack, Environment},
@ -80,7 +80,7 @@ const BIN_DIR: &str = env!("BINDIR");
#[cfg(feature = "installable")] #[cfg(feature = "installable")]
// Disable for clippy because otherwise it would require sphinx // Disable for clippy because otherwise it would require sphinx
#[cfg(not(clippy))] #[cfg(not(clippy))]
fn install(confirm: bool) -> bool { fn install(confirm: bool, dir: PathBuf) -> bool {
use rust_embed::RustEmbed; use rust_embed::RustEmbed;
#[derive(RustEmbed)] #[derive(RustEmbed)]
@ -96,11 +96,6 @@ fn install(confirm: bool) -> bool {
use std::io::ErrorKind; use std::io::ErrorKind;
use std::io::Write; use std::io::Write;
use std::io::{stderr, stdin}; use std::io::{stderr, stdin};
let Some(home) = fish::env::get_home() else {
FLOG!(error, "Can't find home directory.");
return false;
};
let dir = PathBuf::from(home).join(DATA_DIR).join(DATA_DIR_SUBDIR);
// TODO: Translation, // TODO: Translation,
// FLOG? // FLOG?
@ -197,7 +192,7 @@ fn install(confirm: bool) -> bool {
} }
#[cfg(any(clippy, not(feature = "installable")))] #[cfg(any(clippy, not(feature = "installable")))]
fn install(_confirm: bool) -> bool { fn install(_confirm: bool, _dir: PathBuf) -> bool {
eprintln!("Fish was built without support for self-installation"); eprintln!("Fish was built without support for self-installation");
return false; return false;
} }
@ -300,10 +295,17 @@ fn determine_config_directory_paths(argv0: impl AsRef<Path>) -> ConfigPaths {
} }
if !done { if !done {
// The next check is that we are in a reloctable directory tree // The next check is that we are in a relocatable directory tree
if exec_path.ends_with("bin/fish") { if exec_path.ends_with("bin/fish") {
let base_path = exec_path.parent().unwrap().parent().unwrap(); let base_path = exec_path.parent().unwrap().parent().unwrap();
paths = ConfigPaths { 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"), data: base_path.join("share/fish"),
sysconf: base_path.join("etc/fish"), sysconf: base_path.join("etc/fish"),
doc: base_path.join("share/doc/fish"), doc: base_path.join("share/doc/fish"),
@ -316,6 +318,9 @@ fn determine_config_directory_paths(argv0: impl AsRef<Path>) -> ConfigPaths {
); );
let base_path = exec_path.parent().unwrap(); let base_path = exec_path.parent().unwrap();
paths = ConfigPaths { paths = ConfigPaths {
#[cfg(feature = "installable")]
data: base_path.join("share/install"),
#[cfg(not(feature = "installable"))]
data: base_path.join("share"), data: base_path.join("share"),
sysconf: base_path.join("etc"), sysconf: base_path.join("etc"),
doc: base_path.join("user_doc/html"), doc: base_path.join("user_doc/html"),
@ -339,7 +344,8 @@ fn determine_config_directory_paths(argv0: impl AsRef<Path>) -> ConfigPaths {
let Some(home) = fish::env::get_home() else { let Some(home) = fish::env::get_home() else {
FLOG!( FLOG!(
error, error,
"Cannot find home directory and will refuse to read configuration" "Cannot find home directory and will refuse to read configuration.\n",
"Consider installing into a directory tree with `fish --install=PATH`."
); );
return paths; return paths;
}; };
@ -421,8 +427,7 @@ fn check_version_file(paths: &ConfigPaths, datapath: &wstr) -> Option<bool> {
{ {
// When fish is installable, we write the version to a file, // When fish is installable, we write the version to a file,
// now we check it. // now we check it.
let verfile = let verfile = PathBuf::from(wcs2osstring(datapath)).join("fish-install-version");
PathBuf::from(fish::common::wcs2osstring(datapath)).join("fish-install-version");
let version = std::fs::read_to_string(verfile).ok()?; let version = std::fs::read_to_string(verfile).ok()?;
return Some(version == fish::BUILD_VERSION); return Some(version == fish::BUILD_VERSION);
@ -458,7 +463,7 @@ fn read_init(parser: &Parser, paths: &ConfigPaths) {
); );
} }
install(true); install(true, PathBuf::from(wcs2osstring(&datapath)));
// We try to go on if installation failed (or was rejected) here // We try to go on if installation failed (or was rejected) here
// If the assets are missing, we will trigger a later error, // If the assets are missing, we will trigger a later error,
// if they are outdated, things will probably (tm) work somewhat. // if they are outdated, things will probably (tm) work somewhat.
@ -540,7 +545,7 @@ fn fish_parse_opt(args: &mut [WString], opts: &mut FishCmdOpts) -> ControlFlow<i
wopt(L!("no-config"), NoArgument, 'N'), wopt(L!("no-config"), NoArgument, 'N'),
wopt(L!("no-execute"), NoArgument, 'n'), wopt(L!("no-execute"), NoArgument, 'n'),
wopt(L!("print-rusage-self"), NoArgument, RUSAGE_ARG), wopt(L!("print-rusage-self"), NoArgument, RUSAGE_ARG),
wopt(L!("install"), NoArgument, 'I'), wopt(L!("install"), OptionalArgument, 'I'),
wopt( wopt(
L!("print-debug-categories"), L!("print-debug-categories"),
NoArgument, NoArgument,
@ -576,7 +581,60 @@ fn fish_parse_opt(args: &mut [WString], opts: &mut FishCmdOpts) -> ControlFlow<i
'h' => opts.batch_cmds.push("__fish_print_help fish".into()), 'h' => opts.batch_cmds.push("__fish_print_help fish".into()),
'i' => opts.is_interactive_session = true, 'i' => opts.is_interactive_session = true,
'I' => { 'I' => {
install(false); #[cfg(not(feature = "installable"))]
eprintln!("Fish was built without support for self-installation");
#[cfg(feature = "installable")]
if let Some(path) = w.woptarg {
// We were given an explicit path.
// Install us there as a relocatable install.
// That means:
// path/bin/fish is the fish binary
// path/share/fish/ is the data directory
// path/etc/fish is sysconf????
use std::fs;
let dir = PathBuf::from(wcs2osstring(path));
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 {
eprintln!("Creating directory '{}' failed", p.display());
std::process::exit(1);
};
}
// Copy ourselves there.
let argv0 = OsString::from_vec(wcs2string(&args[0]));
let exec_path =
get_executable_path(<OsString as AsRef<Path>>::as_ref(&argv0));
let binpath = dir.join("bin/fish");
if let Ok(exec_path) = exec_path.canonicalize() {
if exec_path != binpath {
if let Err(err) = std::fs::copy(exec_path, binpath.clone()) {
FLOG!(error, "Cannot copy fish to", binpath.display());
FLOG!(error, err);
std::process::exit(1);
}
println!(
"Fish installed in '{}'. Start that from now on.",
binpath.display()
);
// TODO: Reexec fish?
std::process::exit(0);
}
} else {
FLOG!(error, "Cannot copy fish to '%ls'. Please copy the fish binary there manually", binpath.display());
}
}
} else {
let paths = Some(determine_config_directory_paths(OsString::from_vec(
wcs2string(&args[0]),
)));
let Some(paths) = paths else {
FLOG!(error, "Cannot find config paths");
std::process::exit(1);
};
install(true, paths.data);
}
} }
'l' => opts.is_login = true, 'l' => opts.is_login = true,
'N' => { 'N' => {