1{ stdenv
2, lib
3, fetchurl
4, fetchpatch
5, coreutils
6, which
7, gnused
8, gnugrep
9, groff
10, gawk
11, man-db
12, getent
13, libiconv
14, pcre2
15, gettext
16, ncurses
17, python3
18, cmake
19, fishPlugins
20, procps
21
22# used to generate autocompletions from manpages and for configuration editing in the browser
23, usePython ? true
24
25, runCommand
26, writeText
27, nixosTests
28, nix-update-script
29, useOperatingSystemEtc ? true
30 # An optional string containing Fish code that initializes the environment.
31 # This is run at the very beginning of initialization. If it sets $NIX_PROFILES
32 # then Fish will use that to configure its function, completion, and conf.d paths.
33 # For example:
34 # fishEnvPreInit = "source /etc/fish/my-env-preinit.fish";
35 # It can also be a function that takes one argument, which is a function that
36 # takes a path to a bash file and converts it to fish. For example:
37 # fishEnvPreInit = source: source "${nix}/etc/profile.d/nix-daemon.sh";
38, fishEnvPreInit ? null
39}:
40let
41 etcConfigAppendix = writeText "config.fish.appendix" ''
42 ############### ↓ Nix hook for sourcing /etc/fish/config.fish ↓ ###############
43 # #
44 # Origin:
45 # This fish package was called with the attribute
46 # "useOperatingSystemEtc = true;".
47 #
48 # Purpose:
49 # Fish ordinarily sources /etc/fish/config.fish as
50 # $__fish_sysconfdir/config.fish,
51 # and $__fish_sysconfdir is defined at compile-time, baked into the C++
52 # component of fish. By default, it is set to "/etc/fish". When building
53 # through Nix, $__fish_sysconfdir gets set to $out/etc/fish. Here we may
54 # have included a custom $out/etc/config.fish in the fish package,
55 # as specified, but according to the value of useOperatingSystemEtc, we
56 # may want to further source the real "/etc/fish/config.fish" file.
57 #
58 # When this option is enabled, this segment should appear the very end of
59 # "$out/etc/config.fish". This is to emulate the behavior of fish itself
60 # with respect to /etc/fish/config.fish and ~/.config/fish/config.fish:
61 # source both, but source the more global configuration files earlier
62 # than the more local ones, so that more local configurations inherit
63 # from but override the more global locations.
64 #
65 # Special care needs to be taken, when fish is called from an FHS user env
66 # or similar setup, because this configuration file will then be relocated
67 # to /etc/fish/config.fish, so we test for this case to avoid nontermination.
68
69 if test -f /etc/fish/config.fish && test /etc/fish/config.fish != (status filename)
70 source /etc/fish/config.fish
71 end
72
73 # #
74 ############### ↑ Nix hook for sourcing /etc/fish/config.fish ↑ ###############
75 '';
76
77 fishPreInitHooks = writeText "__fish_build_paths_suffix.fish" ''
78 # source nixos environment
79 # note that this is required:
80 # 1. For all shells, not just login shells (mosh needs this as do some other command-line utilities)
81 # 2. Before the shell is initialized, so that config snippets can find the commands they use on the PATH
82 builtin status is-login
83 or test -z "$__fish_nixos_env_preinit_sourced" -a -z "$ETC_PROFILE_SOURCED" -a -z "$ETC_ZSHENV_SOURCED"
84 ${if fishEnvPreInit != null then ''
85 and begin
86 ${lib.removeSuffix "\n" (if lib.isFunction fishEnvPreInit then fishEnvPreInit sourceWithFenv else fishEnvPreInit)}
87 end'' else ''
88 and test -f /etc/fish/nixos-env-preinit.fish
89 and source /etc/fish/nixos-env-preinit.fish''}
90 and set -gx __fish_nixos_env_preinit_sourced 1
91
92 test -n "$NIX_PROFILES"
93 and begin
94 # We ensure that __extra_* variables are read in $__fish_datadir/config.fish
95 # with a preference for user-configured data by making sure the package-specific
96 # data comes last. Files are loaded/sourced in encounter order, duplicate
97 # basenames get skipped, so we assure this by prepending Nix profile paths
98 # (ordered in reverse of the $NIX_PROFILE variable)
99 #
100 # Note that at this point in evaluation, there is nothing whatsoever on the
101 # fish_function_path. That means we don't have most fish builtins, e.g., `eval`.
102
103
104 # additional profiles are expected in order of precedence, which means the reverse of the
105 # NIX_PROFILES variable (same as config.environment.profiles)
106 set -l __nix_profile_paths (string split ' ' $NIX_PROFILES)[-1..1]
107
108 set -p __extra_completionsdir \
109 $__nix_profile_paths"/etc/fish/completions" \
110 $__nix_profile_paths"/share/fish/vendor_completions.d"
111 set -p __extra_functionsdir \
112 $__nix_profile_paths"/etc/fish/functions" \
113 $__nix_profile_paths"/share/fish/vendor_functions.d"
114 set -p __extra_confdir \
115 $__nix_profile_paths"/etc/fish/conf.d" \
116 $__nix_profile_paths"/share/fish/vendor_conf.d"
117 end
118 '';
119
120 # This is wrapped in begin/end in case the user wants to apply redirections.
121 # This does mean the basic usage of sourcing a single file will produce
122 # `begin; begin; …; end; end` but that's ok.
123 sourceWithFenv = path: ''
124 begin # fenv
125 # This happens before $__fish_datadir/config.fish sets fish_function_path, so it is currently
126 # unset. We set it and then completely erase it, leaving its configuration to $__fish_datadir/config.fish
127 set fish_function_path ${fishPlugins.foreign-env}/share/fish/vendor_functions.d $__fish_datadir/functions
128 fenv source ${lib.escapeShellArg path}
129 set -l fenv_status $status
130 # clear fish_function_path so that it will be correctly set when we return to $__fish_datadir/config.fish
131 set -e fish_function_path
132 test $fenv_status -eq 0
133 end # fenv
134 '';
135
136 fish = stdenv.mkDerivation rec {
137 pname = "fish";
138 version = "3.7.1";
139
140 src = fetchurl {
141 # There are differences between the release tarball and the tarball GitHub
142 # packages from the tag. Specifically, it comes with a file containing its
143 # version, which is used in `build_tools/git_version_gen.sh` to determine
144 # the shell's actual version (and what it displays when running `fish
145 # --version`), as well as the local documentation for all builtins (and
146 # maybe other things).
147 url = "https://github.com/fish-shell/fish-shell/releases/download/${version}/${pname}-${version}.tar.xz";
148 hash = "sha256-YUyfVkPNB5nfOROV+mu8NklCe7g5cizjsRTTu8GjslA=";
149 };
150
151 # Fix FHS paths in tests
152 postPatch = ''
153 # src/fish_tests.cpp
154 sed -i 's|/bin/ls|${coreutils}/bin/ls|' src/fish_tests.cpp
155 sed -i 's|is_potential_path(L"/usr"|is_potential_path(L"/nix"|' src/fish_tests.cpp
156 sed -i 's|L"/bin/echo"|L"${coreutils}/bin/echo"|' src/fish_tests.cpp
157 sed -i 's|L"/bin/c"|L"${coreutils}/bin/c"|' src/fish_tests.cpp
158 sed -i 's|L"/bin/ca"|L"${coreutils}/bin/ca"|' src/fish_tests.cpp
159 # disable flakey test
160 sed -i '/{TEST_GROUP("history_races"), history_tests_t::test_history_races},/d' src/fish_tests.cpp
161
162 # tests/checks/cd.fish
163 sed -i 's|/bin/pwd|${coreutils}/bin/pwd|' tests/checks/cd.fish
164
165 # tests/checks/redirect.fish
166 sed -i 's|/bin/echo|${coreutils}/bin/echo|' tests/checks/redirect.fish
167
168 # tests/checks/vars_as_commands.fish
169 sed -i 's|/usr/bin|${coreutils}/bin|' tests/checks/vars_as_commands.fish
170
171 # tests/checks/jobs.fish
172 sed -i 's|ps -o stat|${procps}/bin/ps -o stat|' tests/checks/jobs.fish
173 sed -i 's|/bin/echo|${coreutils}/bin/echo|' tests/checks/jobs.fish
174
175 # tests/checks/job-control-noninteractive.fish
176 sed -i 's|/bin/echo|${coreutils}/bin/echo|' tests/checks/job-control-noninteractive.fish
177
178 # tests/checks/complete.fish
179 sed -i 's|/bin/ls|${coreutils}/bin/ls|' tests/checks/complete.fish
180 '' + lib.optionalString stdenv.isDarwin ''
181 # Tests use pkill/pgrep which are currently not built on Darwin
182 # See https://github.com/NixOS/nixpkgs/pull/103180
183 rm tests/pexpects/exit.py
184 rm tests/pexpects/job_summary.py
185 rm tests/pexpects/signals.py
186
187 # pexpect tests are flaky in general
188 # See https://github.com/fish-shell/fish-shell/issues/8789
189 rm tests/pexpects/bind.py
190 '' + lib.optionalString stdenv.isLinux ''
191 # pexpect tests are flaky on aarch64-linux (also x86_64-linux)
192 # See https://github.com/fish-shell/fish-shell/issues/8789
193 rm tests/pexpects/exit_handlers.py
194 '';
195
196 outputs = [ "out" "doc" ];
197 strictDeps = true;
198 nativeBuildInputs = [
199 cmake
200 gettext
201 ];
202
203 buildInputs = [
204 ncurses
205 libiconv
206 pcre2
207 ];
208
209 cmakeFlags = [
210 "-DCMAKE_INSTALL_DOCDIR=${placeholder "doc"}/share/doc/fish"
211 ] ++ lib.optionals stdenv.isDarwin [
212 "-DMAC_CODESIGN_ID=OFF"
213 ];
214
215 # Fish’s test suite needs to be able to look up process information and send signals.
216 sandboxProfile = lib.optionalString stdenv.isDarwin ''
217 (allow mach-lookup mach-task-name)
218 (allow signal (target children))
219 '';
220
221 # The optional string is kind of an inelegant way to get fish to cross compile.
222 # Fish needs coreutils as a runtime dependency, and it gets put into
223 # CMAKE_PREFIX_PATH, which cmake uses to look up build time programs, so it
224 # was clobbering the PATH. It probably needs to be fixed at a lower level.
225 preConfigure = ''
226 patchShebangs ./build_tools/git_version_gen.sh
227 '' + lib.optionalString (stdenv.hostPlatform != stdenv.buildPlatform) ''
228 export CMAKE_PREFIX_PATH=
229 '';
230
231 # Required binaries during execution
232 propagatedBuildInputs = [
233 coreutils
234 gnugrep
235 gnused
236 groff
237 gettext
238 ] ++ lib.optional (!stdenv.isDarwin) man-db;
239
240 doCheck = true;
241
242 nativeCheckInputs = [
243 coreutils
244 (python3.withPackages (ps: [ ps.pexpect ]))
245 procps
246 ];
247
248 checkPhase = ''
249 make test
250 '';
251
252 postInstall = with lib; ''
253 sed -r "s|command grep|command ${gnugrep}/bin/grep|" \
254 -i "$out/share/fish/functions/grep.fish"
255 sed -e "s|\|cut|\|${coreutils}/bin/cut|" \
256 -i "$out/share/fish/functions/fish_prompt.fish"
257 sed -e "s|uname|${coreutils}/bin/uname|" \
258 -i "$out/share/fish/functions/__fish_pwd.fish" \
259 "$out/share/fish/functions/prompt_pwd.fish"
260 sed -e "s|sed |${gnused}/bin/sed |" \
261 -i "$out/share/fish/functions/alias.fish" \
262 "$out/share/fish/functions/prompt_pwd.fish"
263 sed -i "s|nroff|${groff}/bin/nroff|" \
264 "$out/share/fish/functions/__fish_print_help.fish"
265 sed -e "s|clear;|${getBin ncurses}/bin/clear;|" \
266 -i "$out/share/fish/functions/fish_default_key_bindings.fish"
267 sed -i "s|/usr/local/sbin /sbin /usr/sbin||" \
268 $out/share/fish/completions/{sudo.fish,doas.fish}
269 sed -e "s| awk | ${gawk}/bin/awk |" \
270 -i $out/share/fish/functions/{__fish_print_packages.fish,__fish_print_addresses.fish,__fish_describe_command.fish,__fish_complete_man.fish,__fish_complete_convert_options.fish} \
271 $out/share/fish/completions/{cwebp,adb,ezjail-admin,grunt,helm,heroku,lsusb,make,p4,psql,rmmod,vim-addons}.fish
272
273 '' + optionalString usePython ''
274 cat > $out/share/fish/functions/__fish_anypython.fish <<EOF
275 function __fish_anypython
276 echo ${python3.interpreter}
277 return 0
278 end
279 EOF
280
281 '' + optionalString stdenv.isLinux ''
282 for cur in $out/share/fish/functions/*.fish; do
283 sed -e "s|/usr/bin/getent|${getent}/bin/getent|" \
284 -i "$cur"
285 done
286
287 '' + optionalString (!stdenv.isDarwin) ''
288 sed -i "s|Popen(\['manpath'|Popen(\['${man-db}/bin/manpath'|" \
289 "$out/share/fish/tools/create_manpage_completions.py"
290 sed -i "s|command manpath|command ${man-db}/bin/manpath|" \
291 "$out/share/fish/functions/man.fish"
292 '' + optionalString useOperatingSystemEtc ''
293 tee -a $out/etc/fish/config.fish < ${etcConfigAppendix}
294 '' + ''
295 tee -a $out/share/fish/__fish_build_paths.fish < ${fishPreInitHooks}
296 '';
297
298 meta = with lib; {
299 description = "Smart and user-friendly command line shell";
300 homepage = "https://fishshell.com/";
301 changelog = "https://github.com/fish-shell/fish-shell/releases/tag/${version}";
302 license = licenses.gpl2Only;
303 platforms = platforms.unix;
304 maintainers = with maintainers; [ adamcstephens cole-h winter ];
305 mainProgram = "fish";
306 };
307
308 passthru = {
309 shellPath = "/bin/fish";
310 tests = {
311 nixos = lib.optionalAttrs stdenv.isLinux nixosTests.fish;
312
313 # Test the fish_config tool by checking the generated splash page.
314 # Since the webserver requires a port to run, it is not started.
315 fishConfig =
316 let fishScript = writeText "test.fish" ''
317 set -x __fish_bin_dir ${fish}/bin
318 echo $__fish_bin_dir
319 cp -r ${fish}/share/fish/tools/web_config/* .
320 chmod -R +w *
321
322 # if we don't set `delete=False`, the file will get cleaned up
323 # automatically (leading the test to fail because there's no
324 # tempfile to check)
325 ${lib.getExe gnused} -e 's@, mode="w"@, mode="w", delete=False@' -i webconfig.py
326
327 # we delete everything after the fileurl is assigned
328 ${lib.getExe gnused} -e '/fileurl =/q' -i webconfig.py
329 echo "print(fileurl)" >> webconfig.py
330
331 # and check whether the message appears on the page
332 # cannot test the http server because it needs a localhost port
333 cat (${python3}/bin/python ./webconfig.py \
334 | tail -n1 | ${lib.getExe gnused} -e 's|file://||' \
335 ) | ${lib.getExe gnugrep} -q 'a href="http://localhost.*Start the Fish Web config'
336 '';
337 in
338 runCommand "test-web-config" { } ''
339 HOME=$(mktemp -d)
340 ${fish}/bin/fish ${fishScript} && touch $out
341 '';
342 };
343 updateScript = nix-update-script { };
344 };
345 };
346in
347fish