From 7c73c5fec042207e08bd932b4f1a3cab22ae729f Mon Sep 17 00:00:00 2001 From: Fabian Boehm Date: Wed, 31 Jan 2024 15:50:13 +0100 Subject: [PATCH] Make fish installable When built with the default "installable" feature, the data files (share/) are included in the fish binary itself. Run `fish --install` or `fish --install=noconfirm` (for non-interactive use) to install fish's data files into ~/.local/share/fish/install To figure out if the data files are out of date, we write the current version to a file on install, and read it on start. CMake disables the default features so nothing changes for that, but this allows installing via `cargo install`, and even making a static binary that you can then just upload and have extract itself. We set $__fish_help_dir to empty for installable builds, because we do not have a way to generate html docs (because we need fish_indent for highlighting). The man pages are found via $__fish_data_dir/man --- CMakeLists.txt | 1 + Cargo.lock | 158 +++++++++++++++++++++++++++++++- Cargo.toml | 4 +- build.rs | 27 +++++- share/functions/help.fish | 2 +- src/bin/fish.rs | 188 +++++++++++++++++++++++++++++++++++++- src/env/environment.rs | 78 +++++++++++----- 7 files changed, 425 insertions(+), 33 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index a0ebe3223..4ae4f3714 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -58,6 +58,7 @@ function(CREATE_TARGET target) $<$:--release> $<$:--release> --target ${Rust_CARGO_TARGET} + --no-default-features ${CARGO_FLAGS} ${FEATURES_ARG} && diff --git a/Cargo.lock b/Cargo.lock index 7136ca176..c5d216c53 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -20,6 +20,15 @@ version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + [[package]] name = "cc" version = "1.1.30" @@ -43,6 +52,25 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" +[[package]] +name = "cpufeatures" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "608697df725056feaccfa42cffdaeeec3fccc4ffc38358ecd19b243e716a78e0" +dependencies = [ + "libc", +] + +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "typenum", +] + [[package]] name = "dashmap" version = "5.5.3" @@ -56,6 +84,16 @@ dependencies = [ "parking_lot_core", ] +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + [[package]] name = "equivalent" version = "1.0.1" @@ -69,7 +107,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "534c5cf6194dfab3db3242765c03bbe257cf92f22b38f6bc0c58d59108a820ba" dependencies = [ "libc", - "windows-sys", + "windows-sys 0.52.0", ] [[package]] @@ -90,6 +128,7 @@ dependencies = [ "portable-atomic", "rand", "rsconf", + "rust-embed", "serial_test", "terminfo", "widestring", @@ -115,6 +154,16 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f81ec6369c545a7d40e4589b5597581fa1c441fe1cce96dd1de43159910a36a2" +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + [[package]] name = "hashbrown" version = "0.14.5" @@ -371,6 +420,49 @@ dependencies = [ "cc", ] +[[package]] +name = "rust-embed" +version = "8.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa66af4a4fdd5e7ebc276f115e895611a34739a9c1c01028383d612d550953c0" +dependencies = [ + "rust-embed-impl", + "rust-embed-utils", + "walkdir", +] + +[[package]] +name = "rust-embed-impl" +version = "8.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6125dbc8867951125eec87294137f4e9c2c96566e61bf72c45095a7c77761478" +dependencies = [ + "proc-macro2", + "quote", + "rust-embed-utils", + "syn 2.0.79", + "walkdir", +] + +[[package]] +name = "rust-embed-utils" +version = "8.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e5347777e9aacb56039b0e1f28785929a8a3b709e87482e7442c72e7c12529d" +dependencies = [ + "sha2", + "walkdir", +] + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + [[package]] name = "scopeguard" version = "1.2.0" @@ -397,7 +489,18 @@ checksum = "079a83df15f85d89a68d64ae1238f142f172b1fa915d0d76b26a7cba1b659a69" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 1.0.109", +] + +[[package]] +name = "sha2" +version = "0.10.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", ] [[package]] @@ -429,6 +532,17 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "syn" +version = "2.0.79" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89132cd0bf050864e1d38dc3bbc07a0eb8e7530af26344d3d2bbbef83499f590" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + [[package]] name = "terminfo" version = "0.9.0" @@ -441,18 +555,49 @@ dependencies = [ "phf_codegen", ] +[[package]] +name = "typenum" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" + [[package]] name = "unicode-ident" version = "1.0.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e91b56cd4cadaeb79bbf1a5645f6b4f8dc5bde8834ad5894a8db35fda9efa1fe" +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + [[package]] name = "widestring" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7219d36b6eac893fa81e84ebe06485e7dcbb616177469b142df14f1f4deb1311" +[[package]] +name = "winapi-util" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" +dependencies = [ + "windows-sys 0.59.0", +] + [[package]] name = "windows-sys" version = "0.52.0" @@ -462,6 +607,15 @@ dependencies = [ "windows-targets", ] +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets", +] + [[package]] name = "windows-targets" version = "0.52.6" diff --git a/Cargo.toml b/Cargo.toml index 61785ad84..6adfdfbe7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -53,6 +53,7 @@ rand = { version = "0.8.5", default-features = false, features = ["small_rng"] } widestring = "1.1.0" # We need 0.9.0 specifically for some crash fixes. terminfo = "0.9.0" +rust-embed = { version = "8.2.0", optional = true } [target.'cfg(not(target_has_atomic = "64"))'.dependencies] portable-atomic = { version = "1", default-features = false, features = [ @@ -83,8 +84,9 @@ name = "fish_key_reader" path = "src/bin/fish_key_reader.rs" [features] -default = [] +default = ["installable"] benchmark = [] +installable = ["dep:rust-embed"] # The following features are auto-detected by the build-script and should not be enabled manually. asan = [] diff --git a/build.rs b/build.rs index 126a4a036..ecfc1d397 100644 --- a/build.rs +++ b/build.rs @@ -223,10 +223,18 @@ fn setup_paths() { var } - let prefix = PathBuf::from(env::var("PREFIX").unwrap_or("/usr/local".to_string())); - if prefix.is_relative() { + let (prefix_from_home, prefix) = if let Ok(pre) = env::var("PREFIX") { + (false, PathBuf::from(pre)) + } else { + (true, PathBuf::from(".local/")) + }; + + // If someone gives us a $PREFIX, we need it to be absolute. + // Otherwise we would try to get it from $HOME and that won't really work. + if !prefix_from_home && prefix.is_relative() { panic!("Can't have relative prefix"); } + rsconf::rebuild_if_env_changed("PREFIX"); rsconf::set_env_value("PREFIX", prefix.to_str().unwrap()); @@ -234,11 +242,24 @@ fn setup_paths() { rsconf::set_env_value("DATADIR", datadir.to_str().unwrap()); rsconf::rebuild_if_env_changed("DATADIR"); + let datadir_subdir = if prefix_from_home { + "fish/install" + } else { + "fish" + }; + rsconf::set_env_value("DATADIR_SUBDIR", datadir_subdir); + let bindir = get_path("BINDIR", "bin/", prefix.clone()); rsconf::set_env_value("BINDIR", bindir.to_str().unwrap()); rsconf::rebuild_if_env_changed("BINDIR"); - let sysconfdir = get_path("SYSCONFDIR", "etc/", datadir.clone()); + let sysconfdir = get_path( + "SYSCONFDIR", + // If we get our prefix from $HOME, we should use the system's /etc/ + // ~/.local/share/etc/ makes no sense + if prefix_from_home { "/etc/" } else { "etc/" }, + datadir.clone(), + ); rsconf::set_env_value("SYSCONFDIR", sysconfdir.to_str().unwrap()); rsconf::rebuild_if_env_changed("SYSCONFDIR"); diff --git a/share/functions/help.fish b/share/functions/help.fish index 031a7d0d0..942eecba7 100644 --- a/share/functions/help.fish +++ b/share/functions/help.fish @@ -181,7 +181,7 @@ function help --description 'Show help for the fish shell' set -l version_string (string split . -f 1,2 -- $version | string join .) set -l ext_url https://fishshell.com/docs/$version_string/$fish_help_page set -l page_url - if test -f $__fish_help_dir/index.html; and not set -lq chromeos_linux_garcon + if set -q __fish_help_dir[1]; and test -f $__fish_help_dir/index.html; and not set -lq chromeos_linux_garcon # Help is installed, use it set page_url file://$__fish_help_dir/$fish_help_page diff --git a/src/bin/fish.rs b/src/bin/fish.rs index 671e15496..a48e2af93 100644 --- a/src/bin/fish.rs +++ b/src/bin/fish.rs @@ -21,6 +21,8 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA #![allow(unstable_name_collisions)] #![allow(clippy::uninlined_format_args)] +#[allow(unused_imports)] +use fish::future::IsSomeAnd; use fish::{ ast::Ast, builtins::shared::{ @@ -71,9 +73,103 @@ 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")] +fn install(confirm: bool) { + use rust_embed::RustEmbed; + + #[derive(RustEmbed)] + #[folder = "share/"] + struct Asset; + + use std::fs; + use std::io::ErrorKind; + use std::io::Write; + use std::io::{stderr, stdin}; + let Some(home) = fish::env::get_home() else { + eprintln!("Can't find $HOME",); + std::process::exit(1); + }; + let dir = PathBuf::from(home).join(DATA_DIR).join(DATA_DIR_SUBDIR); + + // TODO: Translation, + // FLOG? + // - Install: Translations + // - Install: Manpages (build via build.rs) + // - Don't install: __fish_build_paths.fish.in + if confirm { + if isatty(libc::STDIN_FILENO) { + eprintln!( + "This will write fish's data files to '{}'.\n\ + Please enter 'yes' to continue.", + dir.display() + ); + eprint!("> "); + let _ = stderr().flush(); + } + + let mut input = String::new(); + if let Err(error) = stdin().read_line(&mut input) { + eprintln!("error: {error}") + } + + if input != "yes\n" { + eprintln!("Exiting without writing any files\n"); + std::process::exit(1); + } + } else { + eprintln!("Installing fish's data files to '{}'.", dir.display()); + } + + // Remove the install directory first, to clean out any removed files. + if let Err(err) = fs::remove_dir_all(dir.clone()) { + if err.kind() != ErrorKind::NotFound { + eprintln!("Removing '{}' failed: {}", dir.display(), err); + std::process::exit(1); + } + } + + for file in Asset::iter() { + let path = dir.join(file.as_ref()); + let Ok(_) = fs::create_dir_all(path.parent().unwrap()) else { + eprintln!( + "Creating directory '{}' failed", + path.parent().unwrap().display() + ); + std::process::exit(1); + }; + let res = File::create(&path); + let Ok(mut f) = res else { + eprintln!("Creating file '{}' failed", path.display()); + continue; + }; + // This should be impossible. + let d = Asset::get(&file).expect("File was somehow not included???"); + if let Err(error) = f.write_all(&d.data) { + eprintln!("error: {error}"); + std::process::exit(1); + } + } + let verfile = dir.join("fish-install-version"); + let res = File::create(&verfile); + if let Ok(mut f) = res { + f.write_all(fish::BUILD_VERSION.as_bytes()) + .expect("FAILED TO WRITE"); + } else { + eprintln!("Creating file '{}' failed", verfile.display()); + }; + std::process::exit(0); +} + +#[cfg(not(feature = "installable"))] +fn install(_confirm: bool) { + eprintln!("Fish was built without support for self-installation"); + std::process::exit(1); +} + /// container to hold the options specified within the command line #[derive(Default, Debug)] struct FishCmdOpts { @@ -207,12 +303,31 @@ fn determine_config_directory_paths(argv0: impl AsRef) -> ConfigPaths { 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" + ); + 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: PathBuf::from(DATA_DIR).join("fish"), + data, sysconf: PathBuf::from(SYSCONF_DIR).join("fish"), doc: DOC_DIR.into(), - bin: Some(BIN_DIR.into()), + bin, } } @@ -266,6 +381,49 @@ fn source_config_in_directory(parser: &Parser, dir: &wstr) -> bool { /// Parse init files. exec_path is the path of fish executable as determined by argv[0]. fn read_init(parser: &Parser, paths: &ConfigPaths) { let datapath = str2wcstring(paths.data.as_os_str().as_bytes()); + + #[cfg(feature = "installable")] + { + // (false-positive, is_none_or is a backport, this builds with 1.70) + #[allow(clippy::incompatible_msrv)] + if paths + .bin + .clone() + .is_none_or(|x| !x.starts_with(env!("CARGO_MANIFEST_DIR"))) + { + // 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 version = match std::fs::read_to_string(verfile) { + Ok(x) => x, + Err(err) => { + let escaped_pathname = escape(&datapath); + FLOGF!( + error, + "Fish cannot find its asset files in '%ls'.\n\ + Refusing to read configuration because of this.\n\ + The underlying error is: '%ls'", + escaped_pathname, + err.to_string() + ); + return; + } + }; + + if version != fish::BUILD_VERSION { + FLOGF!( + error, + "Asset files are version %s, this fish is version %s. Please run `fish --install` again", + version, + fish::BUILD_VERSION + ); + // We could refuse to read any config, + // but that seems a bit harsh. + // return; + } + } + } if !source_config_in_directory(parser, &datapath) { // If we cannot read share/config.fish, our internal configuration, // something is wrong. @@ -274,8 +432,14 @@ fn read_init(parser: &Parser, paths: &ConfigPaths) { let escaped_pathname = escape(&datapath); FLOGF!( error, - "Fish cannot find its asset files in '%ls'. Refusing to read configuration.", - escaped_pathname + "Fish cannot find its asset files in '%ls'.\n\ + Refusing to read configuration because of this.", + escaped_pathname, + ); + #[cfg(feature = "installable")] + FLOG!( + error, + "If you installed via `cargo install`, please run `fish --install` and restart fish." ); return; } @@ -336,6 +500,7 @@ fn fish_parse_opt(args: &mut [WString], opts: &mut FishCmdOpts) -> ControlFlow ControlFlow opts.features = w.woptarg.unwrap().to_owned(), 'h' => opts.batch_cmds.push("__fish_print_help fish".into()), 'i' => opts.is_interactive_session = true, + 'I' => { + let noconfirm = match w.woptarg { + None => false, + Some(n) if n == L!("noconfirm") => true, + _ => { + FLOGF!( + error, + "Unknown argument to --install: '%ls'", + w.woptarg.unwrap() + ); + std::process::exit(1); + } + }; + install(!noconfirm); + } 'l' => opts.is_login = true, 'N' => { opts.no_config = true; diff --git a/src/env/environment.rs b/src/env/environment.rs index 05fa97deb..725a17abd 100644 --- a/src/env/environment.rs +++ b/src/env/environment.rs @@ -452,6 +452,39 @@ fn get_hostname_identifier() -> Option { } } +/// Get values for $HOME via getpwuid, +/// without trusting $USER or $HOME. +pub fn get_home() -> Option { + let uid: uid_t = geteuid(); + + let mut userinfo: MaybeUninit = MaybeUninit::uninit(); + let mut result: *mut libc::passwd = std::ptr::null_mut(); + let mut buf = [0 as libc::c_char; 8192]; + + // We need to get the data via the uid and don't trust $USER. + let retval = unsafe { + libc::getpwuid_r( + uid, + userinfo.as_mut_ptr(), + buf.as_mut_ptr(), + buf.len(), + &mut result, + ) + }; + if retval != 0 || result.is_null() { + return None; + } + + let userinfo = unsafe { userinfo.assume_init() }; + if !userinfo.pw_dir.is_null() { + let home = unsafe { CStr::from_ptr(userinfo.pw_dir) }; + let home = home.to_str().ok().map(|x| x.to_owned()); + home + } else { + None + } +} + /// Set up the USER and HOME variable. fn setup_user(vars: &EnvStack) { let uid: uid_t = geteuid(); @@ -595,22 +628,30 @@ pub fn env_init(paths: Option<&ConfigPaths>, do_uvars: bool, default_paths: bool .set(inherited_vars) .expect("env_init is being called multiple times"); + // Set $USER, $HOME and $EUID + // This involves going to passwd and stuff. + vars.set_one(L!("EUID"), EnvMode::GLOBAL, geteuid().to_wstring()); + setup_user(vars); + if let Some(paths) = paths { - vars.set_one( - FISH_DATADIR_VAR, - EnvMode::GLOBAL, - str2wcstring(paths.data.as_os_str().as_bytes()), - ); + let datadir = str2wcstring(paths.data.as_os_str().as_bytes()); + + vars.set_one(FISH_DATADIR_VAR, EnvMode::GLOBAL, datadir.clone()); vars.set_one( FISH_SYSCONFDIR_VAR, EnvMode::GLOBAL, str2wcstring(paths.sysconf.as_os_str().as_bytes()), ); - vars.set_one( - FISH_HELPDIR_VAR, - EnvMode::GLOBAL, - str2wcstring(paths.doc.as_os_str().as_bytes()), - ); + + if !cfg!(feature = "installable") { + vars.set_one( + FISH_HELPDIR_VAR, + EnvMode::GLOBAL, + str2wcstring(paths.doc.as_os_str().as_bytes()), + ); + } else { + vars.set_empty(FISH_HELPDIR_VAR, EnvMode::GLOBAL); + } if let Some(bp) = &paths.bin { vars.set_one( FISH_BIN_DIR, @@ -622,21 +663,14 @@ pub fn env_init(paths: Option<&ConfigPaths>, do_uvars: bool, default_paths: bool }; if default_paths { - let mut scstr = paths.data.clone(); - scstr.push("functions"); - vars.set_one( - L!("fish_function_path"), - EnvMode::GLOBAL, - str2wcstring(scstr.as_os_str().as_bytes()), - ); + let mut scstr = datadir; + // This is generated by PathBuf.join() everywhere currently + assert!(!scstr.ends_with("/")); + scstr.push_str("/functions"); + vars.set_one(L!("fish_function_path"), EnvMode::GLOBAL, scstr); } } - // Set $USER, $HOME and $EUID - // This involves going to passwd and stuff. - vars.set_one(L!("EUID"), EnvMode::GLOBAL, geteuid().to_wstring()); - setup_user(vars); - let user_config_dir = path_get_config(); vars.set_one( FISH_CONFIG_DIR,