Skip to content

Conversation

@faho
Copy link
Member

@faho faho commented Mar 12, 2024

(update: since #10899, you can just do

cargo install --git https://github.com/fish-shell/fish-shell
~/.cargo/bin/fish

we now return you to your regular programming
)

Description

Here's the teaser:

cargo install --git https://github.com/faho/fish-shell --branch fish-installer
~/.cargo/bin/fish --install
~/.cargo/bin/fish

This is how you can get a running fish without root permissions.

You can also use it to build a 100% statically linked fish that you can then copy to another system (with the same architecture and operating system). I've been able to do that for x86_64 linux, and cross-compile one for aarch64 linux. (using musl because glibc doesn't do static linking)

I believe it would be possible to allow this as an option, and to add a static build to fishshell.com so people can just download a fish, without compilation.

I believe this is superior to appimages, which I have recently discovered require libfuse2, which I didn't have installed, along with other dependencies like a compatible libc (so you would have to build on an old glibc system, and additionally a musl one, and that for each architecture)

Supersedes #6475.


What this does is embed the entirety of share/ - all the functions, completions and fish_config, and then write it out when you run fish --install (or fish --install=noconfirm to skip the confirmation).

It writes them out to ~/.local/share/fish/install, so uninstallation is cargo uninstall fish; rm -r ~/.local/share/fish/install.

Technically, this is done by adding a Cargo feature, "installable", that's enabled by default (so cargo install will do it by default). When it is enabled, rust_embed will be added as a dependency. Default features are disabled in the cmake build so nothing changes there. (a slight wrinkle is that, if we did add another feature, we would have to turn it on in cmake)

This about doubles the fish binary size (6M to 12M unstripped, 4M to 8.7M stripped). I have run benchmarks where it had little impact (7% increase in --no-config startup time). I do not think this is important. Binaries of similar size on my system: ctest, emacs, agg, gdb, perf. Bigger binaries include: kitten, clang-tidy, qemu, clangd. I have never thought of the size of any of them.

TODO:

  • Documentation
  • Tests
  • The version currently only includes the commit hash (e.g. "4ec25d894"). I believe that's because cargo only does a shallow clone. This would have to find another source, e.g. the crate version, but even then it would look different because the number of commits is lost. Would something like "3.7.1-g4ec25d894" be an okay version string?

The following are more "future directions" than things I feel must be included now for this to be useful:

  • Stop including __fish_build_paths.fish.in (this is harmless)
  • Include man pages - these aren't currently included. It might be possible to get them in by generating them in build.rs.
    HTML isn't possible (it needs fish_indent to do the syntax highlighting), but help is always available and will simply open the online version.
  • Include translations
  • Make fish_indent/fish_key_reader available as builtins (and potentially by calling fish as them) so you really have a single-file fish - Builtin indent and key reader #10876
  • The data path is currently pretty set as ~/.local/share/fish/install. It would be nice to make this "relocatable" in some manner, so people can download a build and place it in e.g. /usr/local/bin/ or /opt/. (unfortunately cargo installs to ~/.cargo/bin/ by default but ~/.cargo/share/ isn't really a thing, so a "../share" feels off to me?)

Edit: The man pages are now included, fish_indent/key_reader is #10876.

The data path is computed at runtime in $HOME. So you can place this in /opt, but every user running it would have to --install separately. I believe that's fine for now, a future possibility is to install unconditionally.

The version is now CRATE_VERSION-gHASH. This could throw off some regexen because it doesn't match the VERSION-DISTANCE-gHASH format. We could add a fake DISTANCE, e.g. 9999, given that you can't rely on "number of commits" for anything anyway.

@faho faho added this to the fish next-3.x milestone Mar 12, 2024
@faho
Copy link
Member Author

faho commented Mar 13, 2024

One annoyance of this solution: You have to rerun fish --install on upgrade. I experimented with having it check the mtime of share/config.fish and printing a note if it was older than the fish binary. This generally works, but will trigger too often (e.g. if you move the fish binary after installing).

It might be possible to write a file with the version that was installed and check that on startup instead.

@matklad
Copy link

matklad commented Mar 20, 2024

I believe it would be possible to allow this as an option, and to add a static build to fishshell.com so people can just download a fish, without compilation.

Would be prudent to double-check performance of the statically linked version. Four-ish years ago, statically linking with musl could have tanked your performance due to musl's malloc being much slower than glibc one or jemalloc. This issue prevented rust-analyzer from going the fully statically linked way. But I remeber that musl wanted to rewrite the allocator, and, presumably, they had done that since.

@faho
Copy link
Member Author

faho commented Mar 22, 2024

Would be prudent to double-check performance of the statically linked version

Pure "--no-config" startup is faster, our other benchmarks are 10-70% slower (memory usage is ~25% less). Of course those are specifically written to mostly measure time spent in fish. Realistically, this is an entirely usable build and it's hard to notice a difference when just running it. Performance in shells is weird, very often most of the time is spent waiting for external commands.

It's "possible" to link glibc statically, but that will crash when you attempt to do echo ~foo (in libnss_something, via getpwnam_r). (this is why we used to forbid static linking in cmake)

The alternative is to offer a self-installable build dynamically linked against glibc, but that obviously requires a compatible (i.e. same version or newer) glibc to what it was built against. This would still be better than the appimage, because that needs libfuse2 and glibc on the host. These are also simple enough to build that we could do both.

This talk is of course secondary to whether having fish self-installable is useful - it would enable cargo install to do something usable, and it could then also be used to distribute cross-distro single-file binaries, whether they are statically or dynamically linked.

@eatyourbaby
Copy link

The alternative is to offer a self-installable build dynamically linked against glibc, but that obviously requires a compatible (i.e. same version or newer) glibc to what it was built against. This would still be better than the appimage, because that needs libfuse2 and glibc on the host. These are also simple enough to build that we could do both.

Binaries built with newer glibc isn't necessarily incompatible with older glibc. glibc uses symbol versioning to indicate compatibility. I pulled your branch and ran cargo build --release with glibc 2.37, which was released in 2023. readelf -V target/release/fish shows binary requires GLIBC_2.34, which was released in 2021. I don't know how many years of backwards compatibility is most appropriate, but I think building from Debian oldstable or the Ubuntu equivalent should suffice for most people.

@faho faho mentioned this pull request May 2, 2024
@Justinzobel
Copy link

So if I had a fleet of 100 servers on varying versions of Ubuntu LTS I could build it on the oldest server and it should be able to be copied to any of the same version server or higher and it would simply run?

@faho
Copy link
Member Author

faho commented May 2, 2024

I could build it on the oldest server and it should be able to be copied to any of the same version server or higher and it would simply run

If you build dynamically against glibc, yes. If you build statically against musl you can build it anywhere (probably even cross-compile it from macos), at the cost of some performance.

This will give you a per-user fish that needs to install its datafiles by running fish --install, after that fish will be fully capable.

@faho faho force-pushed the fish-installer branch from a67c86a to 94b051f Compare May 3, 2024 16:02
@faho
Copy link
Member Author

faho commented May 3, 2024

Alright, after a rebase this now crashes on startup - do cargo install --path ., run ~/.cargo/bin/fish, and you'll get a segfault.

Backtrace is:

#0  0x0000555555801744 in fish::fork_exec::blocked_signals_for_job ()
#1  0x00005555557ef2ee in fish::fork_exec::spawn::PosixSpawner::new ()
#2  0x00005555557ea0fc in fish::exec::exec_process_in_job ()
#3  0x00005555557e4bdf in fish::exec::exec_job ()
#4  0x00005555556804ea in fish::parse_execution::ParseExecutionContext::run_1_job ()
#5  0x000055555567f378 in fish::parse_execution::ParseExecutionContext::run_job_conjunction ()
#6  0x000055555567f20a in fish::parse_execution::ParseExecutionContext::run_job_list ()
#7  0x00005555556776a4 in fish::parse_execution::ParseExecutionContext::eval_node ()
#8  0x000055555576fd4c in fish::parser::Parser::eval_node ()
#9  0x000055555576d7fe in fish::parser::Parser::eval_with ()
#10 0x00005555557f6ba9 in fish::exec::exec_subshell_internal ()
#11 0x000055555572d2f3 in fish::expand::expand_cmdsubst ()

@krobelus
Copy link
Contributor

krobelus commented May 3, 2024

debug build might help. This is still pre-fork so that should rule out a class of problems

@faho
Copy link
Member Author

faho commented May 3, 2024

debug build might help.

Debug build gives an empty backtrace (only ??, no symbols).

Edit: Okay, I'm reasonably sure it's some dependency locking issue. If I run cargo build and run the resulting fish, it runs. But if I run cargo install, the resulting fish crashes and every cargo build after that also crashes until I cargo clean.

@faho faho force-pushed the fish-installer branch from 94b051f to 4ec25d8 Compare May 7, 2024 16:04
@faho
Copy link
Member Author

faho commented May 7, 2024

Okay I'm reasonably sure cargo install pulled in a newer libc. Pinning it to 0.2.151 (the last version compatible with rust < 1.71) seems to work.

@zanchey
Copy link
Member

zanchey commented May 25, 2024

The thing that bites people when using make install into their homedir for projects is that it does not delete files that are no longer installed. (This is why I worked on the development build packages - the only way to reliably manage your filesystem is with a package manager that uninstalls old files. make uninstall is not supported in CMake and in autotools it required a lot of care on the part of developers, which we did not always take.) cargo install will manage the binaries ok, but the fish --install step should possibly install to a versioned directory or similar.

@faho
Copy link
Member Author

faho commented May 25, 2024

fish --install step should possibly install to a versioned directory or similar.

It currently installs into ~/.local/share/fish/install, so there's the binaries in the cargo binary dir and exactly one directory.

That means uninstallation is cargo uninstall fish; rm -rf ~/.local/share/fish/install, which I feel is simple enough. (or if you want to entirely stop using fish you can just rm -rf ~/.local/share/fish)

A versioned directory could lead to multiple of these being left behind instead of simply overwritten on upgrade.

(plus the biggest issue left here is that installing via git like cargo install --git https://github.com/faho/fish-shell --branch fish-installer will not give you a version because cargo won't pull the tags, so you only get the commit hash)

@zanchey
Copy link
Member

zanchey commented May 26, 2024

A versioned directory could lead to multiple of these being left behind instead of simply overwritten on upgrade.

That's fine, but in that case the destination directory should be removed before the extraction starts - it doesn't look like this happens at present. Otherwise, files that are present in one version but not the next will be left behind.

@faho faho force-pushed the fish-installer branch from 6b8a22d to 0c7a915 Compare May 26, 2024 16:00
@faho
Copy link
Member Author

faho commented May 26, 2024

That's fine, but in that case the destination directory should be removed before the extraction starts

Good point, done.

@faho faho force-pushed the fish-installer branch from 0c7a915 to f3fc743 Compare June 10, 2024 17:19
@faho
Copy link
Member Author

faho commented Jun 10, 2024

Alright, those last two commits are a bit weird.

The immediate issue is that cargo install --git https://github.com/faho/fish-shell/ --branch fish-installer-test (note that's "fish-installer-test", which I left broken on purpose - this PR is "fish-installer") won't clone the tags. So if you do that right now, you'll get something in $FISH_VERSION (and fish --version) like

fish, version f3fc743fc

which will break a lot of uses of $FISH_VERSION - there is no version there, only the commit sha.

So this detects that case and turns it into

fish, version 3.7.95-g5b00071a7

instead, by using the crate version (in Cargo.toml).

I'm unsure if that is also needed for packages on crates.io.

I changed the crate version (which was still at 0.1.0!) for testing. The version I picked is arbitrary and preliminary, and I am fine with removing it again. Tho I suppose it's not harmful to keep it in?


I would like to try to get some more people to test this - and therefore the next fish version.

We still have more regressions remaining than I would like, especially in corners we don't visit as often and this is really easy to get going as a separate version of fish without breaking your main install.

It would also show if people like this way of installing it, and if anyone has an idea for getting the install/upgrade experience working.

@zanchey
Copy link
Member

zanchey commented Jun 11, 2024

Not sure how much work it would be but getting some form of CI running would be good.

We could publish the crates as pre-release versions. I think it makes sense to do that with a tagged tarball as well?

@faho faho force-pushed the fish-installer branch 2 times, most recently from 9a9cf1f to b6952f4 Compare June 12, 2024 15:34
@VuiMuich
Copy link

For the version TODO as a reference, leftwm does this:

    println!(
        "\x1b[0;94m::\x1b[0m LeftWM version: {}",
        env!("CARGO_PKG_VERSION")
    );
    println!(
        "\x1b[0;94m::\x1b[0m LeftWM git hash: {}",
        option_env!("GIT_HASH").unwrap_or(git_version::git_version!(fallback = "unknown"))

Looking so much forward when this PR gets merged 😍

@faho faho mentioned this pull request Nov 27, 2024
10 tasks
faho added 9 commits December 6, 2024 21:45
We don't want "fish --version" to print "unknown" or any other fake version
This was needed because we #included it in C++, but now it's easier to
have just the version in there
The next version is gonna be 4.0.0
It is possible we cannot acquire this
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
Makes it possible to generate the man pages without fish_indent
available.

(not the html docs because they highlight via fish_indent!)
This calls sphinx-build from build.rs to include the man pages in the binary.

We don't abort if sphinx doesn't exist, but we do if it failed.
For x86_64 and cross-compiled for aarch64, manually triggered

It *seems* to work, but I had to explicitly disable gettext for it (which is AFAICT currently non-functional under musl anyway).

Also it will create one .zip containing two .tar.xzs. It is about 8MB, which should be fine, tbh.
@faho faho removed the RFC label Dec 6, 2024
@faho faho modified the milestones: fish-future, fish 4.0 Dec 6, 2024
@faho
Copy link
Member Author

faho commented Dec 6, 2024

Alright, no use in waiting any longer on this, I'm pressing the big blue button (possibly green for y'all, I'm reasonably sure I have deuteranopia mode turned on).

@faho faho merged commit 74e0436 into fish-shell:master Dec 6, 2024
10 checks passed
@krobelus
Copy link
Contributor

krobelus commented Dec 6, 2024 via email

@faho
Copy link
Member Author

faho commented Dec 6, 2024

did you manage to fix HOME=$(mktemp -d) fish and man pages?

I did add the man pages, there's no way to fix HOME=$(mktemp -d) with this.

That's what --no-config is for.

(without resorting to auto-installing, which I am uncomfortable with)

@krobelus
Copy link
Contributor

krobelus commented Dec 6, 2024 via email

@faho
Copy link
Member Author

faho commented Dec 6, 2024

I think a fixed location is the wrong approach

I await your PR. The big problem is that ../share is a terrible directory to use.

Frankly I think our effort would be better invested loading the functions straight from the binary without unpacking, as well as incorporating the help text (that latter one also in non-installable builds).

@krobelus
Copy link
Contributor

krobelus commented Dec 6, 2024

The big problem is that ../share is a terrible directory to use.

Haven't tried it yet but I think everything would go into ~/.cache/fish, not sure where ../share comes into play.

Anyway, with the current approach, our stance should be that users should not rely on the default location of the support files, because we might change that in future. Probably this is more or less obvious.

Frankly I think our effort would be better invested loading the functions straight from the binary without unpacking, as well as incorporating the help text (that latter one also in non-installable builds).

yes that would be better (with the same interface). Having the raw files available in share/functions is nice but probably not a hard requirement. One can always look at the source

@faho
Copy link
Member Author

faho commented Dec 7, 2024

Haven't tried it yet but I think everything would go into ~/.cache/fish, not sure where ../share comes into play.

Whether it goes into ~/.cache/fish or ~/.local/share/fish isn't really important and doesn't solve your question of installing concurrent versions - it's still a fixed location.

You could of course make versioned directories (in either path), but that means you need to figure out how to clean old directories or you're gonna leave them around.

If instead the functions, completions and documentation are read from the binary, you'd only have fish_config and themes there, and those are just much less important to keep up to date.

(the trickiest part is the man pages, which we would have to write out to man for formatting)

@lukasbindreiter
Copy link

@faho - I just tried downloading the last staticbuild artifact for linux aarch64 - however the artifact has already expired - and since the build is only triggered manually there is no newer run. Could you maybe trigger a new one? And any chance the artifact lifetime could be increased a bit for that workflow?

@faho
Copy link
Member Author

faho commented Jan 29, 2025

@lukasbindreiter The 4.0b1 release contains copied artifacts at the bottom.

I've now triggered a new run for Integration_4.0.0.

And any chance the artifact lifetime could be increased a bit for that workflow?

We don't want to keep around arbitrary checkpoints for long. We're probably gonna keep the releases.

You can also always build it yourself with cargo build --release --git https://github.com/fish-shell/fish-shell.git --target aarch64-unknown-linux-musl (this requires a musl C toolchain, I've had some success with cargo-zigbuild for targets that zig supports - zig can be installed via pip, so pip install ziglang and then use cargo zigbuild instead of cargo build). If you don't need it static, cargo install just works.

@lukasbindreiter
Copy link

@faho Thank you very much, this resolved my issue! 🙏

Makes sense that you don't want to keep them around for too long - I wasn't aware they were part of the release and didn't check there unfortunately - thanks for the pointer.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.