Nix for Haskell: 静态构建

Lobsters Hottest 工具

摘要

本教程介绍如何使用 Nix 为 Haskell 项目创建静态链接的可执行文件,涵盖 GHC 的静态构建配置以及与 Docker 的集成。

<p><a href="https://lobste.rs/s/medvuo/nix_for_haskell_static_builds">评论</a></p>
查看原文
查看缓存全文

缓存时间: 2026/06/18 14:03

# 使用 Nix 构建 Haskell 静态二进制 来源:https://abhinavsarkar.net/posts/nix-for-haskell-static-builds/ 在[上一篇](https://abhinavsarkar.net/posts/nix-for-haskell/)文章中,我们学习了如何使用 Nix 来管理和构建 Haskell 项目。在本篇文章中,我们将学习如何轻松地为 Haskell 项目创建静态链接的可执行文件。 本系列文章名为:**Nix for Haskell**。 1. [快速入门](https://abhinavsarkar.net/posts/nix-for-haskell/) 2. **静态构建** 👈 ### 目录 1. [静态构建](#静态构建) 2. [在 GHC 中启用静态构建](#在-ghc-中启用静态构建) 3. [配置应用程序](#配置应用程序) 4. [配置静态构建依赖](#配置静态构建依赖) 5. [最终步骤](#最终步骤) 6. [额外内容:构建 Docker 镜像](#额外内容构建-docker-镜像) 7. [结论](#结论) 建议先阅读[上一篇](https://abhinavsarkar.net/posts/nix-for-haskell/)文章,因为我们将从上次结束的地方继续(忽略 bonus 部分)。目前项目的目录结构如下: `Main.hs` 是默认生成的打印 "Hello, Haskell!" 的主文件。`ftr.cabal` 是默认生成的 [Cabal](https://www.haskell.org/cabal/) 文件。`sources.(json|nix)` 由 [Niv](https://github.com/nmattia/niv) 生成,用于将 [Nixpkgs](https://github.com/NixOS/nixpkgs/) 固定到特定版本。`nixpkgs.nix` 提供我们构建工具和依赖所使用的 nixpkgs。`package.nix` 和 `shell.nix` 分别用于构建包和管理 Nix shell。本篇文章不会改动这些文件。让我们开始吧。 ## 静态构建 [静态构建](https://en.wikipedia.org/wiki/Static_build)是指可执行文件与其所依赖的所有库静态链接。这与动态链接可执行文件相反,后者包含对依赖库的引用,并在运行时加载和链接这些库。虽然动态链接有其[优势](https://en.wikipedia.org/wiki/Static_build#Dynamic_linking),但静态链接的主要优点是可执行文件可以单独分发,无需附带或安装依赖库。这使得它在部署后端服务时非常有吸引力——只需下载并部署一个二进制可执行文件即可!无需关心安装和维护其依赖项。 许多编译器都支持静态构建,[Go](https://golang.org/) 和 [Rust](https://www.rust-lang.org/) 是其中非常容易的两门语言。Haskell 编译器 [GHC](https://www.haskell.org/ghc/) 也支持,但并非开箱即用。要静态链接 Haskell 可执行文件,我们需要先配置 GHC 本身,然后再配置可执行文件的构建。我们还需要配置 GHC 与 [musl](https://en.wikipedia.org/wiki/musl) libc 链接。这就是 Nix 发挥作用的地方,它简化了这一过程¹(https://abhinavsarkar.net/posts/nix-for-haskell-static-builds/#fn1)。 ## 在 GHC 中启用静态构建 如前所述,首先需要一个配置为进行静态构建的 GHC。我们创建一个独立的 nixpkgs derivation(与 `nixpkgs.nix` 分开),其中包含自定义配置的 GHC。 ```nix { arch, ghcVersion ? (import ./nixpkgs.nix { }).haskellPackages.ghc.version, }: let getGHCWithVersion = lib: "ghc" + lib.replaceStrings [ "." ] [ "" ] ghcVersion; sources = import ./sources.nix; in import sources.nixpkgs { system = arch + "-linux"; overlays = [ ( final: prev: let compiler = getGHCWithVersion prev.lib; prevHPackages = prev.haskell.packages.${compiler}; in prev.lib.attrsets.recursiveUpdate prev { haskell.packages.${compiler} = prevHPackages.override { ghc = prevHPackages.ghc.override { enableRelocatedStaticLibs = true; enableShared = false; enableDwarf = false; enableProfiledLibs = false; enableDocs = false; enableNativeBignum = true; }; buildHaskellPackages = prevHPackages.buildHaskellPackages.override (old: { ghc = final.haskell.packages.${compiler}.ghc; buildHaskellPackages = final.haskell.packages.${compiler}; }); }; } ) ( final: prev: let compiler = getGHCWithVersion prev.lib; in { haskellPackages = prev.haskell.packages.${compiler}; ghc = prev.haskell.packages.${compiler}.ghc; } ) (final: prev: { haskell = prev.haskell // { packageOverrides = prev.lib.composeExtensions prev.haskell.packageOverrides ( hfinal: hprev: { mkDerivation = args: hprev.mkDerivation ( args // { doCheck = false; doHaddock = false; enableLibraryProfiling = false; enableExecutableProfiling = false; } ); } ); }; }) ]; config = { }; } ``` `nix/nixpkgs-static-ghc.nix` 让我们逐部分解析。首先,我们接受 `arch` 和 `ghcVersion` 参数,允许我们为不同架构(X86-64 和 AArch64)以及不同 GHC 版本构建包。`ghcVersion` 默认为 nixpkgs 中的默认 GHC 版本。 该 derivation 与 `nixpkgs.nix` 相同,只是添加了一些 overlays。第一个 overlay 添加了为静态构建自定义配置的 GHC。我们为此启用了一些配置: - `enableRelocatedStaticLibs = true`:配置 GHC 运行时系统和核心包以生成位置无关代码,以便可以加载它们用于 Template Haskell。 - `enableShared = false`:禁用构建动态链接库,因此它们只构建为静态归档文件。 - `enableDwarf = false`:禁用基于 DWARF 的堆栈跟踪,因为它在 musl 目标上不可用。 - `enableProfiledLibs = false`:禁用构建性能分析启用的库。 - `enableDocs = false`:禁用生成文档。 - `enableNativeBignum = true`:使 GHC 使用纯 Haskell 的本地 bignum 后端 `ghc-bignum`,而不是 GMP,这样它创建的包可执行文件就不受 GPL 约束。如果你可以接受 GPL 的可执行文件,可以移除此设置。 `buildHaskellPackages` 相关的行将自定义 GHC 设置为 Nix 中使用的基于 Haskell 的工具的编译器²(https://abhinavsarkar.net/posts/nix-for-haskell-static-builds/#fn2)。 第二个 overlay 使 `cabal2nix`(用于将 `.cabal` 文件转换为 Nix derivation 的工具)使用自定义的 GHC。第三个 overlay 禁用了所有使用自定义 GHC 构建的 Haskell 库的文档生成、测试和性能分析。我们这样做是为了节省构建时间,假设静态构建仅用于发布,而文档、测试和分析则使用普通的 GHC 完成。 构建这个自定义的 GHC 可能需要几分钟到几个小时,具体取决于构建机器的配置³(https://abhinavsarkar.net/posts/nix-for-haskell-static-builds/#fn3)⁴(https://abhinavsarkar.net/posts/nix-for-haskell-static-builds/#fn4)。但只要保留 GHC 构建产物,这就是一次性成本。接下来,我们将包配置为构建为静态链接的可执行文件。 ## 配置应用程序 `package-static.nix` 文件相当于上一篇文章中的 `package.nix`,但用于构建静态可执行文件。 ```nix { arch, ghcVersion ? (import ./nix/nixpkgs.nix { }).haskellPackages.ghc.version, }: let pkgsOrig = import ./nix/nixpkgs-static-ghc.nix { inherit arch ghcVersion; }; pkgs = pkgsOrig.pkgsMusl; hlib = pkgs.haskell.lib.compose; staticDeps = import ./nix/static-deps.nix { inherit pkgs; }; inherit (staticDeps) libffi zlib numactl; in pkgs.lib.pipe (pkgs.haskellPackages.callCabal2nix "ftr" (pkgs.lib.cleanSource ./.) { }) [ hlib.dontHaddock hlib.dontHyperlinkSource hlib.dontCoverage hlib.disableExecutableProfiling hlib.disableLibraryProfiling hlib.disableSharedLibraries hlib.justStaticExecutables hlib.enableDeadCodeElimination (hlib.overrideCabal (old: { enableParallelBuilding = true; buildTools = (old.buildTools or [ ]) ++ [ pkgsOrig.buildPackages.lld ]; })) (hlib.appendConfigureFlags [ "-O2" "--ghc-option=-fPIC" "--ghc-option=-optl=-static" "--ghc-option=-split-sections" "--ghc-option=-optl-fuse-ld=lld" "--ld-option=-fuse-ld=lld" "--with-ld=ld.lld" "--ld-option=-Wl,--gc-sections,--build-id,--icf=all" "--extra-lib-dirs=${libffi}/lib" "--extra-lib-dirs=${zlib}/lib" "--extra-lib-dirs=${numactl}/lib" ]) ( src: pkgs.stdenv.mkDerivation { name = "${src.name}-compressed"; inherit src; nativeBuildInputs = [ pkgsOrig.upx ]; installPhase = '' mkdir -p $out cp -R $src/. $out chmod -R +w $out/bin upx -q --lzma -1 $out/bin/* chmod -R -w $out/bin ''; }) ] ``` `package-static.nix` 同样接受 `arch` 和 `ghcVersion` 参数,并将其传递给 `nix/nixpkgs-static-ghc.nix` 以创建包含上述自定义 GHC 的 nixpkgs。这给了我们 `pkgsOrig`,从中我们得到 `pkgsMusl` 版本。`pkgsMusl` 是相同的 nixpkgs,只是其中的每个可执行文件都链接到 musl libc。我们将其捕获为 `pkgs`,并用于构建我们的 Haskell 包。 在链接可执行文件时,我们需要将其与它所依赖的所有依赖库的静态版本链接。这就是 `nix/static-deps.nix` 文件提供的功能。我们将在下一节中查看它,但现在我们了解到它提供了 `libffi`、`zlib` 和 `numactl` 库⁵(https://abhinavsarkar.net/posts/nix-for-haskell-static-builds/#fn5)。 最后,我们进入包配置。它与 `package.nix` 一样开始,使用 `cabal2nix` 将 Haskell 项目连接到 Nix,但随后我们提供了一系列自定义配置。我们禁用了 Haddock 文档、超链接源文档、覆盖率测试、性能分析和共享库构建。我们启用了静态可执行文件构建和死代码消除。然后我们配置 cabal 使用多线程运行构建,并将 `lld` 添加到其构建工具列表中。 然后,我们添加了许多配置标志: - `-O2`:启用优化。 - `--ghc-option=-fPIC`:启用生成位置无关代码。 - `--ghc-option=-optl=-static`:启用静态链接。 - `--ghc-option=-split-sections`:启用分割段以生成更小的二进制文件。 - `--ghc-option=-optl-fuse-ld=lld`、`--ld-option=-fuse-ld=lld`、`--with-ld=ld.lld`:配置 GHC 使用 `lld` 作为链接器,它比默认链接器快得多。你可以省略这些行以使用默认链接器,或者将所有提到 `lld` 的地方替换为 `mold`,以使用 Mold 链接器,这取决于项目可能更快。 - `--ld-option=-Wl,--gc-sections,--build-id,--icf=all`:通过移除死代码来减小二进制文件大小。 - `--extra-lib-dirs=...`:这些行允许 GHC 将输出可执行文件与所提到的依赖库的静态版本链接。 最后,管道中的最后一个函数使用 UPX 压缩输出可执行文件。这通常会大幅减小二进制文件的大小⁶(https://abhinavsarkar.net/posts/nix-for-haskell-static-builds/#fn6)。 现在我们可以实际构建静态链接的可执行文件了: ```bash $ nix-build --argstr arch x86_64 package-static.nix $ nix-build --argstr arch aarch64 package-static.nix $ nix-build --argstr arch x86_64 --argstr ghcVersion "9.12" package-static.nix ``` 上面的第一行和第二行使用默认的 GHC 版本为 X86-64 和 AArch64 架构构建可执行文件。第三行指定了不同的 GHC 版本来构建。下面是第一个命令的清理后的输出日志: 输出日志 ``` $ nix-build --argstr arch x86_64 package-static.nix these 2 derivations will be built: /nix/store/42a291bq7ydvkdy3fdsyj82axrfsi6sy-ftr-0.1.0.0.drv /nix/store/2c2l50la8291q0jrqlc23bybaxwip8y2-ftr-0.1.0.0-compressed.drv building '/nix/store/42a291bq7ydvkdy3fdsyj82axrfsi6sy-ftr-0.1.0.0.drv' on 'ssh-ng://builder@linux-builder'... copying 1 paths... copying path '/nix/store/l9ls307kzxby72hqj4yl7ri7m8s3b3fk-source' to 'ssh-ng://builder@linux-builder'... building '/nix/store/42a291bq7ydvkdy3fdsyj82axrfsi6sy-ftr-0.1.0.0.drv'... Running phase: setupCompilerEnvironmentPhase Build with /nix/store/717lxds14ra0ndbnin2qhdhh91d3b69g-ghc-musl-native-bignum-9.10.3. Running phase: unpackPhase unpacking source archive /nix/store/l9ls307kzxby72hqj4yl7ri7m8s3b3fk-source source root is source Running phase: patchPhase Running phase: compileBuildDriverPhase setupCompileFlags: -package-db=/nix/var/nix/b/10kwxdphxvyy519y831ryji7fn/b/tmp.EPOEKNjT6Z/setup-package.conf.d -threaded [1 of 2] Compiling Main ( /nix/store/4mdp8nhyfddh7bllbi7xszz7k9955n79-Setup.hs, /nix/var/nix/b/10kwxdphxvyy519y831ryji7fn/b/tmp.EPOEKNjT6Z/Main.o ) [2 of 2] Linking Setup Running phase: updateAutotoolsGnuConfigScriptsPhase Running phase: configurePhase configureFlags: --verbose --prefix=/nix/store/pb2zay1k8b0vifhx7ghd5j6lbncq4b66-ftr-0.1.0.0 --libdir=$prefix/lib/$compiler/lib --libsubdir=$abi/$libname --with-gcc=gcc --package-db=/nix/var/nix/b/10kwxdphxvyy519y831ryji7fn/b/tmp.EPOEKNjT6Z/package.conf.d --ghc-option=-j4 --ghc-option=+RTS --ghc-option=-A64M --ghc-option=-RTS --disable-library-profiling --disable-profiling --disable-shared --disable-coverage --enable-static --disable-executable-dynamic --disable-tests --disable-benchmarks --enable-library-vanilla --disable-library-for-ghci --enable-split-sections --enable-library-stripping --enable-executable-stripping -O2 --ghc-option=-fPIC --ghc-option=-split-sections --ghc-option=-optl-fuse-

相似文章

不到100行代码实现nix-build

Lobsters Hottest

本文通过用不到100行Go代码重新实现nix-build,揭示了Nix构建过程,表明将派生转换为存储路径本质上就是一次执行。

使用Nix的开发环境:四个快速示例

Michael Stapelberg

本教程演示了使用Nix设置开发环境的四种方法,包括交互式一次性使用、配置文件以及密封的Nix Flakes,并以GoCV和OpenCV为例。

Nix 需要可重定位的二进制文件

Lobsters Hottest

文章指出了一个问题是 Nix 二进制文件不可重定位,当存储前缀改变时会导致哈希变化和重新编译,并提出使用带有 $ORIGIN 的相对路径在 RUNPATH 中实现可重定位,而不会使缓存失效。

后现代构建系统

Lobsters Hottest

一篇博客文章,探讨理想中的'后现代'构建系统的设计,该系统优先考虑可信的增量构建、最大化计算复用和分布式构建,并以Nix作为参考。

nixidy 简介 - 使用 Nix 进行 Kubernetes GitOps

Lobsters Hottest

nixidy 是一个基于 Nix 的工具,用于管理 Kubernetes GitOps 部署,它用类型化、可复现的 Nix 表达式替代了 Helm 值文件和 Kustomize 覆盖层。本教程将介绍如何使用 Argo CD 设置 nixidy 项目,并生成纯 YAML 以供审查。