From 3dc49d9d93fc18e8de79a27062493e3055d013a2 Mon Sep 17 00:00:00 2001 From: Fabian Boehm Date: Sun, 22 Dec 2024 18:13:29 +0100 Subject: [PATCH] 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 --- doc_src/cmds/fish.rst | 4 +- src/bin/fish.rs | 88 +++++++++++++++++++++++++++++++++++-------- 2 files changed, 76 insertions(+), 16 deletions(-) diff --git a/doc_src/cmds/fish.rst b/doc_src/cmds/fish.rst index 22c7e17c4..19889d964 100644 --- a/doc_src/cmds/fish.rst +++ b/doc_src/cmds/fish.rst @@ -40,9 +40,11 @@ The following options are available: **-i** or **--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/. 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** Act as if invoked as a login shell. diff --git a/src/bin/fish.rs b/src/bin/fish.rs index 6e4cf40e1..263b78f63 100644 --- a/src/bin/fish.rs +++ b/src/bin/fish.rs @@ -30,7 +30,7 @@ use fish::{ }, common::{ 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::{ environment::{env_init, EnvStack, Environment}, @@ -80,7 +80,7 @@ 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) -> bool { +fn install(confirm: bool, dir: PathBuf) -> bool { use rust_embed::RustEmbed; #[derive(RustEmbed)] @@ -96,11 +96,6 @@ fn install(confirm: bool) -> bool { use std::io::ErrorKind; use std::io::Write; 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, // FLOG? @@ -197,7 +192,7 @@ fn install(confirm: bool) -> bool { } #[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"); return false; } @@ -300,10 +295,17 @@ fn determine_config_directory_paths(argv0: impl AsRef) -> ConfigPaths { } 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") { 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"), @@ -316,6 +318,9 @@ fn determine_config_directory_paths(argv0: impl AsRef) -> ConfigPaths { ); 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"), @@ -339,7 +344,8 @@ fn determine_config_directory_paths(argv0: impl AsRef) -> ConfigPaths { let Some(home) = fish::env::get_home() else { FLOG!( 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; }; @@ -421,8 +427,7 @@ fn check_version_file(paths: &ConfigPaths, datapath: &wstr) -> Option { { // When fish is installable, we write the version to a file, // now we check it. - let verfile = - PathBuf::from(fish::common::wcs2osstring(datapath)).join("fish-install-version"); + let verfile = PathBuf::from(wcs2osstring(datapath)).join("fish-install-version"); let version = std::fs::read_to_string(verfile).ok()?; 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 // If the assets are missing, we will trigger a later error, // 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 ControlFlow opts.batch_cmds.push("__fish_print_help fish".into()), 'i' => opts.is_interactive_session = true, '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(>::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, 'N' => {