Nix for Haskell: 静态构建
摘要
本教程介绍如何使用 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
本文通过用不到100行Go代码重新实现nix-build,揭示了Nix构建过程,表明将派生转换为存储路径本质上就是一次执行。
使用Nix的开发环境:四个快速示例
本教程演示了使用Nix设置开发环境的四种方法,包括交互式一次性使用、配置文件以及密封的Nix Flakes,并以GoCV和OpenCV为例。
Nix 需要可重定位的二进制文件
文章指出了一个问题是 Nix 二进制文件不可重定位,当存储前缀改变时会导致哈希变化和重新编译,并提出使用带有 $ORIGIN 的相对路径在 RUNPATH 中实现可重定位,而不会使缓存失效。
后现代构建系统
一篇博客文章,探讨理想中的'后现代'构建系统的设计,该系统优先考虑可信的增量构建、最大化计算复用和分布式构建,并以Nix作为参考。
nixidy 简介 - 使用 Nix 进行 Kubernetes GitOps
nixidy 是一个基于 Nix 的工具,用于管理 Kubernetes GitOps 部署,它用类型化、可复现的 Nix 表达式替代了 Helm 值文件和 Kustomize 覆盖层。本教程将介绍如何使用 Argo CD 设置 nixidy 项目,并生成纯 YAML 以供审查。