在 nixpkgs 中打包、分发与测试 Fish Shell 插件:vendor 机制、buildFishPlugin 与 wrapFish 实战指南
【免费下载链接】nixpkgsNix Packages collection & NixOS项目地址: https://gitcode.com/GitHub_Trending/ni/nixpkgs
Fish 是 nixpkgs 中一款"智能且用户友好"的命令行 Shell,官方文档以doc/packages/fish.section.md为核心,系统地介绍了 nixpkgs 如何以声明式方式打包 Fish 补全脚本、配置片段、函数与插件,并提供了buildFishPlugin、fishPlugins作用域和wrapFish包装器三套相互配合的工具链。读完本文,你将掌握:如何在任意 Nix 包中随包分发 Fish 资源(vendor 机制)、如何编写一个带单元测试的 Fish 插件、以及如何用wrapFish在隔离环境中调试 Fish 脚本而不污染系统配置。
从包到 Shell:Fish 的 vendor 资源分发机制
Fish 与大多数 Shell 的一个关键差异在于它的插件/扩展体系是"目录约定"驱动的:任何包只要把 Fish 脚本安装到约定的 vendor 目录,Fish 就会在启动时自动加载。nixpkgs 对这套约定做了完整支持,并把它上升为打包规范。
vendor 目录约定
根据 nixpkgs 文档(doc/packages/fish.section.md),任何包都可以随包携带 Fish 的补全(completions)、配置片段(configuration snippets)和函数(functions),它们应分别安装到以下三个目录:
$out/share/fish/vendor_completions.d:补全文件,挂载到fish_complete_path$out/share/fish/vendor_conf.d:配置片段,启动时被自动 source$out/share/fish/vendor_functions.d:函数文件,由 Fish 按需自动加载
NixOS 侧如何让 vendor 目录生效
在 NixOS 上,当模块选项programs.fish.enable以及programs.fish.vendor.{completions,config,functions}.enable被设置为 true 时,这些路径会被符号链接到当前系统环境中,并随 Fish 启动自动加载。
查看 nixos/modules/programs/fish.nix 第 109-131 行的选项定义,可以看到三个开关的默认值均为true:
vendor.config.enable # 是否 source 其他包提供的配置片段(默认 true) vendor.completions.enable # 是否使用其他包提供的补全文件(默认 true) vendor.functions.enable # 是否自动加载其他包提供的 fish 函数(默认 true)其底层实现是environment.pathsToLink:当开关打开时,NixOS 会把对应 vendor 子目录链接进/run/current-system/sw(见 nixos/modules/programs/fish.nix):
pathsToLink = lib.optional cfg.vendor.config.enable "/share/fish/vendor_conf.d" ++ lib.optional cfg.vendor.completions.enable "/share/fish/vendor_completions.d" ++ lib.optional cfg.vendor.functions.enable "/share/fish/vendor_functions.d";这意味着:任何通过environment.systemPackages安装、且按约定在$out/share/fish/vendor_*.d安装了资源的包,其 Fish 集成都会被自动启用,无需逐包手工配置。从源码结构看,这正是 nixpkgs 让"包自带 Shell 集成"成为惯例的关键——上游包(如 fzf、git、gh)发布的 vendor 目录内容,会在 Fish 启动时被无缝吸收。
插件从何而来:fishPlugins 作用域
vendor 机制解决的是"随包分发";而纯 Fish 插件则集中托管在fishPlugins作用域中。文档明确了两条归类原则:
- 提供独立可执行程序的包,属于 nixpkgs 顶层(top level);
- 唯一目的就是扩展 Fish 的包,属于
fishPlugins作用域,并应在 pkgs/shells/fish/plugins/default.nix 中注册。
fishPlugins是典型的lib.makeScope newScope作用域(见 pkgs/shells/fish/plugins/default.nix),内部通过callPackage逐个注册插件。截至当前仓库,它收录了 40+ 个插件,例如:
- 提示符类:
pure、tide、bobthefish、bobthefisher、hydro、gruvbox - 集成类:
fzf、fzf-fish、forgit、bass、foreign-env、nvm、aws - 工具类:
fishtape(TAP 测试运行器)、fishtape_3(3.x 版本线)、done(长命令完成通知)、git-abbr、wakatime-fish等
值得注意的细节:fishtape2.x 与 3.x 互不兼容,但不同插件的测试各自依赖不同版本,因此两者被同时保留(见 pkgs/shells/fish/plugins/default.nix 的注释)。这也是 nixpkgs 对生态现实的一种务实处理。此外,作用域还通过lib.optionalAttrs config.allowAliases维护了别名(如autopair-fish→autopair),用于兼容旧名。
buildFishPlugin:自动完成目录搬运与测试环境搭建
buildFishPlugin是打包 Fish 插件的核心工具函数,实现在 pkgs/shells/fish/plugins/build-fish-plugin.nix。文档对其职责的概括是:自动把$src/{completions,conf,conf.d,functions}下的 Fish 脚本复制到标准 vendor 安装路径,并搭建测试环境。
installPhase:四目录自动搬运
看 pkgs/shells/fish/plugins/build-fish-plugin.nix 的实现,installPhase 内定义了一个install_vendor_files辅助函数,依次执行四次搬运:
install_vendor_files completions completions # $src/completions -> $out/share/fish/vendor_completions.d install_vendor_files functions functions # $src/functions -> $out/share/fish/vendor_functions.d install_vendor_files conf conf # $src/conf -> $out/share/fish/vendor_conf.d install_vendor_files conf.d conf # $src/conf.d -> $out/share/fish/vendor_conf.d每个源目录若存在*.fish文件,就原样复制到对应 vendor 目录;源目录为空则跳过([ -n "$(shopt -s nullglob; echo $source/*.fish)" ] || return 0)。由此,上游仓库常见的completions/、functions/、conf.d/布局被统一映射到 Fish 的标准 vendor 路径,打包者无需手工编写安装脚本。
默认 phase 与扩展钩子
buildFishPlugin基于lib.extendMkDerivation对stdenv.mkDerivation做扩展,默认行为是:
unpackPhase ? ""、configurePhase ? ":"、buildPhase ? ":":即"无构建过程",纯粹搬运源码doCheck ? checkPhase != "":只要提供了checkPhase就自动启用测试
同时它允许像普通 derivation 一样叠加preInstall、postInstall、postPatch、nativeCheckInputs等标准钩子,因此对"目录布局不标准"的插件也能轻易适配。例如 pkgs/shells/fish/plugins/tide.nix 中,由于 tide 的tide configure需要额外的函数子目录,就在postInstall里补充复制:
postInstall = '' cp -R functions/tide $out/share/fish/vendor_functions.d/ '';又如 pkgs/shells/fish/plugins/foreign-env/default.nix 在preInstall阶段把脚本中硬编码的bash替换为 Nix store 中的 bash 绝对路径,保证运行时不依赖 PATH:
preInstall = '' sed -i -e "s|bash|${lib.getExe bash}|" functions/fenv.main.fish '';一个完整的插件打包范例:pure
文档推荐的参考示例是 pkgs/shells/fish/plugins/pure.nix,它同时演示了buildFishPlugin与 fishtape 单元测试的组合:
buildFishPlugin (finalAttrs: { pname = "pure"; version = "4.19.0"; src = fetchFromGitHub { owner = "pure-fish"; repo = "pure"; tag = "v${finalAttrs.version}"; hash = "sha256-8rxCmKu1pvBMm+/Ski7q3ikNnnX3gAqQ0jo0f2mIXrI="; }; nativeCheckInputs = [ git ]; checkPlugins = [ fishtape_3 ]; checkPhase = '' rm tests/pure_tools_installer.test.fish rm tests/_pure_uninstall.test.fish fishtape tests/*.test.fish ''; passthru.updateScript = nix-update-script { }; meta = { description = "Pretty, minimal and fast Fish prompt, ported from zsh"; homepage = "https://github.com/pure-fish/pure"; license = lib.licenses.mit; maintainers = with lib.maintainers; [ euxane ]; }; })要点拆解:
src直接指向上游 Git 仓库,buildFishPlugin负责把它解包并按 vendor 约定安装checkPhase中排除了两个依赖特殊环境的测试文件,然后用fishtape跑其余全部测试passthru.updateScript = nix-update-script { }接入 nixpkgs 的自动更新基础设施meta提供 description/homepage/license/maintainers,是入库的必要元数据
测试环境:checkPlugins 与 checkFunctionDirs
文档特别强调,buildFishPlugin会把可选的checkPhase放到一个 Fish shell 中执行,并且这个测试 Shell 内"其他已打包插件"与"包内 Fish 函数"分别由checkPlugins和checkFunctionDirs注入。看 pkgs/shells/fish/plugins/build-fish-plugin.nix 的实现:
nativeCheckInputs = [ writableTmpDirAsHomeHook (wrapFish { pluginPkgs = checkPlugins; functionDirs = checkFunctionDirs; }) ] ++ nativeCheckInputs; checkPhase = '' fish "${writeScript "${finalAttrs.name}-test" checkPhase}" '';机制拆解:
- 两个专有参数
checkPlugins、checkFunctionDirs通过excludeDrvArgNames从 mkDerivation 参数中剥离,不会泄漏为 derivation 属性 - 它们被喂给
wrapFish,生成一个预装了这些插件/函数目录的 fish 可执行文件,作为nativeCheckInputs checkPhase把用户写的脚本包成脚本文件后用fish执行,因此测试运行在 Fish 语法环境中,可直接使用被测试插件的函数
实际用法示例:
- pkgs/shells/fish/plugins/done.nix:
checkPlugins = [ fishtape ];然后在checkPhase里执行fishtape test/done.fish——测试依赖的 fishtape 正是通过 checkPlugins 注入的 - pkgs/shells/fish/plugins/fishtape.nix:
checkFunctionDirs = [ "./" ];,注释说明 "fishtape is introspective"——它把当前目录加入函数路径来自省测试自己;同时在preInstall里先把fishtape.fish挪进functions/子目录,以满足 buildFishPlugin 的目录约定
writableTmpDirAsHomeHook的存在则保证测试期间 Fish 的 user data 目录(如~/.cache/fish)可写,避免在只读 HOME 环境下测试失败。
wrapFish:不改环境、即插即用的隔离 Fish
wrapFish是一个围绕 Fish 的包装器,可创建一个预置了指定插件、补全、配置片段与函数的 Fish Shell,为测试 Fish 插件和脚本提供了"无需改动环境"的便捷途径。它的完整类型签名与全部参数文档见 pkgs/shells/fish/wrapper.nix,实现基于lib.makeOverridable。
参数一览
| 参数 | 类型 | 说明 |
|---|---|---|
pluginPkgs | [Derivation] | 通常来自fishPlugins的插件包,其 vendor 目录会被并入 |
completionDirs | [Path] | 追加到fish_complete_path的补全目录 |
functionDirs | [Path] | 追加到fish_function_path的函数目录 |
confDirs | [Path] | 由包装后的 fish 启动时 source 的配置目录 |
localConfig | String | 一段被 source 的 fish 脚本字符串 |
shellAliases | { N :: String } | 在包装后的 fish 中可用的别名 |
runtimeInputs | [Derivation] | 加入包装后 fish 的PATH的依赖包 |
除pluginPkgs外所有参数均有默认值,全部参数都可选(见 pkgs/shells/fish/wrapper.nix)。
文档中的最小示例
原文档给出的基础用法(doc/packages/fish.section.md):
wrapFish { pluginPkgs = with fishPlugins; [ pure foreign-env ]; completionDirs = [ ]; functionDirs = [ ]; confDirs = [ "/path/to/some/fish/init/dir/" ]; }完整参数示例与工作原理
wrapper.nix 自带的 doc 示例展示了全部参数的组合用法:
wrapFish { pluginPkgs = with fishPlugins; [ pure foreign-env ]; completionDirs = [ ]; functionDirs = [ ]; confDirs = [ "/path/to/some/fish/init/dir/" ]; shellAliases = { hello = "echo 'Hello World!'"; bye = "echo 'Bye World!'; exit"; }; runtimeInputs = with pkgs; [ curl w3m ]; }它的底层实现值得展开(pkgs/shells/fish/wrapper.nix):
- 路径合成:
vendorDir kind plugin = "${plugin}/share/fish/vendor_${kind}.d",把所有pluginPkgs的三个 vendor 目录分别并入补全路径、函数路径与配置路径;localConfig与shellAliases会被写成两个临时 vendor 配置片段(vendor_conf.d/aliases.fish与vendor_conf.d/config.local.fish),一并加入配置路径 - 别名生成:
shellAliases通过lib.mapAttrsToList转成alias key value行,并包裹在status is-interactive; and begin ... end中,保证只在交互式会话生效 - 启动脚本:最终产物是
writeShellApplication生成的fish可执行脚本,启动时执行:
${fish}/bin/fish --init-command " set --prepend fish_complete_path ${...complPath} set --prepend fish_function_path ${...funcPath} set --local fish_conf_source_path ${...confPath} for c in \$fish_conf_source_path/*; source \$c; end " "$@"即通过--init-command把补全目录/函数目录 prepend 到对应变量,再把所有配置片段 source 一遍,最后透传用户的参数("$@")。runtimeInputs则进入生成脚本的 PATH。从源码结构看,这种方式与 NixOS 模块的 vendor 机制原理一致,但发生在隔离的、用户自定义的集合内——非常适合 CI、测试夹具或临时演示环境。
面向 NixOS 用户的完整 Fish 配置
虽然本节在 fish.section.md 中着墨不多,但 nixos/modules/programs/fish.nix 是与 vendor 机制直接协同的落地模块,值得串联说明。
核心选项
programs.fish.enable(bool,默认 false):启用 fish 作为交互式 Shell,同时把fish与/run/current-system/sw/bin/fish加入shells(见 nixos/modules/programs/fish.nix)programs.fish.package:覆盖 fish 本体programs.fish.generateCompletions(默认 true):从系统包的 man page 自动生成补全。实现上会从 fish 二进制中提取内嵌的create_manpage_completions.py生成器,对systemPackages与extraCompletionPackages逐一生成补全并汇总到/etc/fish/generated_completions(见 nixos/modules/programs/fish.nix),并在交互式初始化时把它插入$fish_complete_path(nixos/modules/programs/fish.nix)programs.fish.extraCompletionPackages:在 generateCompletions 开启时,额外为其生成补全的包列表programs.fish.shellAliases(覆盖environment.shellAliases)、shellAbbrs(fish 特色缩写)、shellFunctions(含body与modifiers的 submodule)、shellInit/loginShellInit/interactiveShellInit/promptInit:分段注入初始化代码,生成的/etc/fish/config.fish会按"通用 / 登录 / 交互"三个阶段用__fish_nixos_*_config_sourced变量保证每个阶段只执行一次(见 nixos/modules/programs/fish.nix)
useBabelfish 与 foreign-env 二选一
NixOS 的shellInit等片段本质是 bash/POSIX 语法,fish 无法直接执行,模块提供了两条翻译路径(useBabelfish,默认 false):
useBabelfish = false(默认):使用fishPlugins.foreign-env的fenv source命令导入(nixos/modules/programs/fish.nix)useBabelfish = true:使用 pkgs/shells/fish/babelfish.nix(基于 Go 的 bash→fish 翻译器)把环境脚本翻译为原生 fish 文件后写入/etc/fish/*.fish(nixos/modules/programs/fish.nix)
回归测试验证
仓库用 NixOS 集成测试锁定了这些机制的行为(nixos/tests/fish.nix):
- 断言
programs.fish.enable = true后,/etc/fish/generated_completions/chmod.fish、kill.fish等生成补全确实存在 - 断言
$fish_complete_path按预期顺序包含/share/fish/vendor_completions.d、/etc/fish/generated_completions、~/.cache/fish/generated_completions - 断言
/etc/fish/nixos-env-preinit.fish、/etc/fish/config.fish均被生成,且 config 文件能被fish_indent合法格式化(machine.succeed("fish_indent -c /etc/fish/config.fish"))
这套测试同时验证了 vendor 链接、补全生成与配置生成三者的端到端正确性。
总结:三条路径,覆盖 Fish 生态全生命周期
回顾 nixpkgs 中 Fish 支持的整体设计,可用一句话概括:vendor 目录约定负责"随包分发",fishPlugins+buildFishPlugin负责"集中打包与测试",wrapFish负责"隔离试运行"。
- 想让已有包顺便带上 Fish 补全/函数 → 按
$out/share/fish/vendor_{completions,conf,functions}.d约定安装即可,NixOS 默认自动吸收 - 想为某个 Fish 插件建立正式包 → 放入
fishPlugins作用域(pkgs/shells/fish/plugins/default.nix),用buildFishPlugin自动搬运脚本,并用checkPlugins/checkFunctionDirs+ fishtape 补齐单元测试 - 想在开发中快速验证一组插件/脚本的组合 → 用
wrapFish声明pluginPkgs、completionDirs、functionDirs、confDirs、localConfig、shellAliases与runtimeInputs,得到一个开箱即用、不污染任何全局配置的 fish
这套从"打包规范 → 构建工具 → 测试设施 → 隔离运行"的完整链路,正是 nixpkgs 对 Fish 这一"插件友好型 Shell"生态的系统性支持。
【免费下载链接】nixpkgsNix Packages collection & NixOS项目地址: https://gitcode.com/GitHub_Trending/ni/nixpkgs
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考