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.6.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-VUArtHymc52KuiXkF4CQW1zhvOCl4N0X3KkItbwLSbI=";
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
160 # tests/checks/cd.fish
161 sed -i 's|/bin/pwd|${coreutils}/bin/pwd|' tests/checks/cd.fish
162
163 # tests/checks/redirect.fish
164 sed -i 's|/bin/echo|${coreutils}/bin/echo|' tests/checks/redirect.fish
165
166 # tests/checks/vars_as_commands.fish
167 sed -i 's|/usr/bin|${coreutils}/bin|' tests/checks/vars_as_commands.fish
168
169 # tests/checks/jobs.fish
170 sed -i 's|ps -o stat|${procps}/bin/ps -o stat|' tests/checks/jobs.fish
171 sed -i 's|/bin/echo|${coreutils}/bin/echo|' tests/checks/jobs.fish
172
173 # tests/checks/job-control-noninteractive.fish
174 sed -i 's|/bin/echo|${coreutils}/bin/echo|' tests/checks/job-control-noninteractive.fish
175
176 # tests/checks/complete.fish
177 sed -i 's|/bin/ls|${coreutils}/bin/ls|' tests/checks/complete.fish
178 '' + lib.optionalString stdenv.isDarwin ''
179 # Tests use pkill/pgrep which are currently not built on Darwin
180 # See https://github.com/NixOS/nixpkgs/pull/103180
181 rm tests/pexpects/exit.py
182 rm tests/pexpects/job_summary.py
183 rm tests/pexpects/signals.py
184
185 # pexpect tests are flaky in general
186 # See https://github.com/fish-shell/fish-shell/issues/8789
187 rm tests/pexpects/bind.py
188 '' + lib.optionalString stdenv.isLinux ''
189 # pexpect tests are flaky on aarch64-linux (also x86_64-linux)
190 # See https://github.com/fish-shell/fish-shell/issues/8789
191 rm tests/pexpects/exit_handlers.py
192 '';
193
194 outputs = [ "out" "doc" ];
195 strictDeps = true;
196 nativeBuildInputs = [
197 cmake
198 gettext
199 ];
200
201 buildInputs = [
202 ncurses
203 libiconv
204 pcre2
205 ];
206
207 cmakeFlags = [
208 "-DCMAKE_INSTALL_DOCDIR=${placeholder "doc"}/share/doc/fish"
209 ] ++ lib.optionals stdenv.isDarwin [
210 "-DMAC_CODESIGN_ID=OFF"
211 ];
212
213 # The optional string is kind of an inelegant way to get fish to cross compile.
214 # Fish needs coreutils as a runtime dependency, and it gets put into
215 # CMAKE_PREFIX_PATH, which cmake uses to look up build time programs, so it
216 # was clobbering the PATH. It probably needs to be fixed at a lower level.
217 preConfigure = ''
218 patchShebangs ./build_tools/git_version_gen.sh
219 '' + lib.optionalString (stdenv.hostPlatform != stdenv.buildPlatform) ''
220 export CMAKE_PREFIX_PATH=
221 '';
222
223 # Required binaries during execution
224 propagatedBuildInputs = [
225 coreutils
226 gnugrep
227 gnused
228 groff
229 gettext
230 ] ++ lib.optional (!stdenv.isDarwin) man-db;
231
232 doCheck = true;
233
234 nativeCheckInputs = [
235 coreutils
236 (python3.withPackages (ps: [ ps.pexpect ]))
237 procps
238 ];
239
240 checkPhase = ''
241 make test
242 '';
243
244 postInstall = with lib; ''
245 sed -r "s|command grep|command ${gnugrep}/bin/grep|" \
246 -i "$out/share/fish/functions/grep.fish"
247 sed -e "s|\|cut|\|${coreutils}/bin/cut|" \
248 -i "$out/share/fish/functions/fish_prompt.fish"
249 sed -e "s|uname|${coreutils}/bin/uname|" \
250 -i "$out/share/fish/functions/__fish_pwd.fish" \
251 "$out/share/fish/functions/prompt_pwd.fish"
252 sed -e "s|sed |${gnused}/bin/sed |" \
253 -i "$out/share/fish/functions/alias.fish" \
254 "$out/share/fish/functions/prompt_pwd.fish"
255 sed -i "s|nroff|${groff}/bin/nroff|" \
256 "$out/share/fish/functions/__fish_print_help.fish"
257 sed -e "s|clear;|${getBin ncurses}/bin/clear;|" \
258 -i "$out/share/fish/functions/fish_default_key_bindings.fish"
259 sed -i "s|/usr/local/sbin /sbin /usr/sbin||" \
260 $out/share/fish/completions/{sudo.fish,doas.fish}
261 sed -e "s| awk | ${gawk}/bin/awk |" \
262 -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} \
263 $out/share/fish/completions/{cwebp,adb,ezjail-admin,grunt,helm,heroku,lsusb,make,p4,psql,rmmod,vim-addons}.fish
264
265 '' + optionalString usePython ''
266 cat > $out/share/fish/functions/__fish_anypython.fish <<EOF
267 function __fish_anypython
268 echo ${python3.interpreter}
269 return 0
270 end
271 EOF
272
273 '' + optionalString stdenv.isLinux ''
274 for cur in $out/share/fish/functions/*.fish; do
275 sed -e "s|/usr/bin/getent|${getent}/bin/getent|" \
276 -i "$cur"
277 done
278
279 '' + optionalString (!stdenv.isDarwin) ''
280 sed -i "s|Popen(\['manpath'|Popen(\['${man-db}/bin/manpath'|" \
281 "$out/share/fish/tools/create_manpage_completions.py"
282 sed -i "s|command manpath|command ${man-db}/bin/manpath|" \
283 "$out/share/fish/functions/man.fish"
284 '' + optionalString useOperatingSystemEtc ''
285 tee -a $out/etc/fish/config.fish < ${etcConfigAppendix}
286 '' + ''
287 tee -a $out/share/fish/__fish_build_paths.fish < ${fishPreInitHooks}
288 '';
289
290 meta = with lib; {
291 description = "Smart and user-friendly command line shell";
292 homepage = "https://fishshell.com/";
293 license = licenses.gpl2;
294 platforms = platforms.unix;
295 maintainers = with maintainers; [ cole-h winter srapenne ];
296 };
297
298 passthru = {
299 shellPath = "/bin/fish";
300 tests = {
301 nixos = nixosTests.fish;
302
303 # Test the fish_config tool by checking the generated splash page.
304 # Since the webserver requires a port to run, it is not started.
305 fishConfig =
306 let fishScript = writeText "test.fish" ''
307 set -x __fish_bin_dir ${fish}/bin
308 echo $__fish_bin_dir
309 cp -r ${fish}/share/fish/tools/web_config/* .
310 chmod -R +w *
311
312 # if we don't set `delete=False`, the file will get cleaned up
313 # automatically (leading the test to fail because there's no
314 # tempfile to check)
315 sed -e 's@, mode="w"@, mode="w", delete=False@' -i webconfig.py
316
317 # we delete everything after the fileurl is assigned
318 sed -e '/fileurl =/q' -i webconfig.py
319 echo "print(fileurl)" >> webconfig.py
320
321 # and check whether the message appears on the page
322 cat (${python3}/bin/python ./webconfig.py \
323 | tail -n1 | sed -ne 's|.*\(/build/.*\)|\1|p' \
324 ) | grep 'a href="http://localhost.*Start the Fish Web config'
325
326 # cannot test the http server because it needs a localhost port
327 '';
328 in
329 runCommand "test-web-config" { } ''
330 HOME=$(mktemp -d)
331 ${fish}/bin/fish ${fishScript} && touch $out
332 '';
333 };
334 updateScript = nix-update-script { };
335 };
336 };
337in
338fish