Automatically attempt to install

This is fairly subtle.

When installable, and we either can't find the version file or it is
outdated, we ask the user to confirm installation (just like `--install`).

We do that only if we are really truly interactive (with a tty!) to
avoid `fish -c` running into problems.
This check could be tightened even more, because currently:

```fish
fish -ic 'echo foo'
```

asks, while

```fish
fish -ic 'echo foo' < /dev/null
```

does not.

`fish -c` will still error out if it can't find the config, but it
will just run if it is out of date.
This commit is contained in:
Fabian Boehm 2024-12-10 18:49:34 +01:00
parent 99fa8aaaa7
commit 6d28845c2b
4 changed files with 60 additions and 54 deletions

View File

@ -55,10 +55,7 @@ Notable improvements and fixes
cargo install --path . # in a clone of the fish repository cargo install --path . # in a clone of the fish repository
# or `cargo build --release` and copy target/release/fish{,_indent,_key_reader} wherever you want # or `cargo build --release` and copy target/release/fish{,_indent,_key_reader} wherever you want
# and then, wherever you use it, run The first time it runs interactively, it will extract all the data files to (currently) ~/.local/share/fish/install/. To uninstall, remove the fish binaries and that directory.
/path/to/fish --install # or --install=noconfirm for non-interactive use
This will extract all the data files to (currently) ~/.local/share/fish/install/. To uninstall, remove the fish binaries and that directory.
This configuration is experimental. This configuration is experimental.
It does not affect the main configuration, which is a regular install via ``cmake``. It does not affect the main configuration, which is a regular install via ``cmake``.

View File

@ -171,10 +171,9 @@ Building fish as self-installable (experimental)
You can also build fish as a self-installing binary. You can also build fish as a self-installing binary.
This will include all the datafiles like the included functions or web configuration tool in the main ``fish`` binary, This will include all the datafiles like the included functions or web configuration tool in the main ``fish`` binary.
and you can unpack them to ~/.local/share/fish/install/ (currently, subject to change) by running ``fish --install`` (or ``fish --install=noconfirm`` to skip the confirmation).
You will have to use ``--install`` once per user and you will have to run it again when you upgrade fish. It will tell you to. On the first interactive run, and whenever it notices they are out of date, it will extract the datafiles to ~/.local/share/fish/install/ (currently, subject to change). You can do this manually by running ``fish --install`` (or ``fish --install=noconfirm`` to skip the confirmation).
To install fish as self-installable, just use ``cargo``, like:: To install fish as self-installable, just use ``cargo``, like::

View File

@ -43,6 +43,7 @@ The following options are available:
**--install[=noconfirm]** **--install[=noconfirm]**
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/.
Using ``--install=noconfirm`` will skip the confirmation step. Using ``--install=noconfirm`` will skip the confirmation step.
Fish will also do this automatically when run interactively.
**-l** or **--login** **-l** or **--login**
Act as if invoked as a login shell. Act as if invoked as a login shell.

View File

@ -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) { fn install(confirm: bool) -> bool {
use rust_embed::RustEmbed; use rust_embed::RustEmbed;
#[derive(RustEmbed)] #[derive(RustEmbed)]
@ -98,7 +98,7 @@ fn install(confirm: bool) {
use std::io::{stderr, stdin}; use std::io::{stderr, stdin};
let Some(home) = fish::env::get_home() else { let Some(home) = fish::env::get_home() else {
eprintln!("Can't find $HOME",); eprintln!("Can't find $HOME",);
std::process::exit(1); return false;
}; };
let dir = PathBuf::from(home).join(DATA_DIR).join(DATA_DIR_SUBDIR); let dir = PathBuf::from(home).join(DATA_DIR).join(DATA_DIR_SUBDIR);
@ -125,7 +125,7 @@ fn install(confirm: bool) {
if input != "yes\n" { if input != "yes\n" {
eprintln!("Exiting without writing any files\n"); eprintln!("Exiting without writing any files\n");
std::process::exit(1); return false;
} }
} else { } else {
eprintln!("Installing fish's data files to '{}'.", dir.display()); eprintln!("Installing fish's data files to '{}'.", dir.display());
@ -135,7 +135,7 @@ fn install(confirm: bool) {
if let Err(err) = fs::remove_dir_all(dir.clone()) { if let Err(err) = fs::remove_dir_all(dir.clone()) {
if err.kind() != ErrorKind::NotFound { if err.kind() != ErrorKind::NotFound {
eprintln!("Removing '{}' failed: {}", dir.display(), err); eprintln!("Removing '{}' failed: {}", dir.display(), err);
std::process::exit(1); return false;
} }
} }
@ -148,7 +148,7 @@ fn install(confirm: bool) {
"Creating directory '{}' failed", "Creating directory '{}' failed",
path.parent().unwrap().display() path.parent().unwrap().display()
); );
std::process::exit(1); return false;
}; };
let res = File::create(&path); let res = File::create(&path);
let Ok(mut f) = res else { let Ok(mut f) = res else {
@ -159,7 +159,7 @@ fn install(confirm: bool) {
let d = Asset::get(&file).expect("File was somehow not included???"); let d = Asset::get(&file).expect("File was somehow not included???");
if let Err(error) = f.write_all(&d.data) { if let Err(error) = f.write_all(&d.data) {
eprintln!("error: {error}"); eprintln!("error: {error}");
std::process::exit(1); return false;
} }
} }
@ -170,7 +170,7 @@ fn install(confirm: bool) {
"Creating directory '{}' failed", "Creating directory '{}' failed",
path.parent().unwrap().display() path.parent().unwrap().display()
); );
std::process::exit(1); return false;
}; };
let res = File::create(&path); let res = File::create(&path);
let Ok(mut f) = res else { let Ok(mut f) = res else {
@ -181,7 +181,7 @@ fn install(confirm: bool) {
let d = Docs::get(&file).expect("File was somehow not included???"); let d = Docs::get(&file).expect("File was somehow not included???");
if let Err(error) = f.write_all(&d.data) { if let Err(error) = f.write_all(&d.data) {
eprintln!("error: {error}"); eprintln!("error: {error}");
std::process::exit(1); return false;
} }
} }
@ -193,13 +193,13 @@ fn install(confirm: bool) {
} else { } else {
eprintln!("Creating file '{}' failed", verfile.display()); eprintln!("Creating file '{}' failed", verfile.display());
}; };
std::process::exit(0); return true;
} }
#[cfg(any(clippy, not(feature = "installable")))] #[cfg(any(clippy, not(feature = "installable")))]
fn install(_confirm: bool) { fn install(_confirm: bool) -> bool {
eprintln!("Fish was built without support for self-installation"); eprintln!("Fish was built without support for self-installation");
std::process::exit(1); return false;
} }
/// container to hold the options specified within the command line /// container to hold the options specified within the command line
@ -410,50 +410,58 @@ fn source_config_in_directory(parser: &Parser, dir: &wstr) -> bool {
return true; return true;
} }
#[cfg(feature = "installable")]
fn check_version_file(paths: &ConfigPaths, datapath: &wstr) -> Option<bool> {
// (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 = std::fs::read_to_string(verfile).ok()?;
return Some(version == fish::BUILD_VERSION);
}
// When running from the manifest dir, we'll just run.
return Some(true);
}
/// Parse init files. exec_path is the path of fish executable as determined by argv[0]. /// Parse init files. exec_path is the path of fish executable as determined by argv[0].
fn read_init(parser: &Parser, paths: &ConfigPaths) { fn read_init(parser: &Parser, paths: &ConfigPaths) {
let datapath = str2wcstring(paths.data.as_os_str().as_bytes()); let datapath = str2wcstring(paths.data.as_os_str().as_bytes());
#[cfg(feature = "installable")] #[cfg(feature = "installable")]
{ {
// (false-positive, is_none_or is a backport, this builds with 1.70) // If the version file is non-existent or out of date,
#[allow(clippy::incompatible_msrv)] // we try to install automatically, but only if we're interactive.
if paths // If we're not interactive, we still print an error later on pointing to `--install` if they don't exist,
.bin // but don't complain if they're merely out-of-date.
.clone() // We do specifically check for a tty because we want to read input to confirm.
.is_none_or(|x| !x.starts_with(env!("CARGO_MANIFEST_DIR"))) let v = check_version_file(paths, &datapath);
{
// 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 { #[allow(clippy::incompatible_msrv)]
FLOGF!( if v.is_none_or(|x| !x) && is_interactive_session() && isatty(libc::STDIN_FILENO) {
error, if v.is_none() {
"Asset files are version %s, this fish is version %s. Please run `fish --install` again", FLOG!(
version, warning,
fish::BUILD_VERSION "Fish's asset files are missing. Trying to install them."
);
} else {
FLOG!(
warning,
"Fish's asset files are out of date. Trying to install them."
); );
// We could refuse to read any config,
// but that seems a bit harsh.
// return;
} }
install(true);
// 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.
} }
} }
if !source_config_in_directory(parser, &datapath) { if !source_config_in_directory(parser, &datapath) {
@ -580,7 +588,8 @@ fn fish_parse_opt(args: &mut [WString], opts: &mut FishCmdOpts) -> ControlFlow<i
std::process::exit(1); std::process::exit(1);
} }
}; };
install(!noconfirm); let ret = install(!noconfirm);
std::process::exit(if ret { 0 } else { 1 });
} }
'l' => opts.is_login = true, 'l' => opts.is_login = true,
'N' => { 'N' => {