NixOS に manylinux 互換環境を整えて Python を簡単に利用・開発する
0. TL;DR
nix-ld を設定で有効化し、
programs.nix-ld = {
enable = true;
libraries = [];
};
次のコマンドで manylinux 互換の開発シェルを起動。
nix --experimental-features "nix-command flakes" develop -L github:ymd-h/manylinux-nixos
# Pandas を利用するスクリプト my-program.py を実行
uv run --with pandas my-program.py
# hatch で dev 環境で my-program.py を実行
uvx hatch run dev:python my-program.py
1. 背景
1.1 NixOS
Nix は独自の Nix 言語による設定ファイルにより再現性のある環境を構築できる Linux・macOS 対応のパッケージマネージャーであり、NixOS は OS全体の環境設定まで Nix で管理する Linux ディストリビューションです。 (参考)
大きな特徴として、ハッシュ値レベルでビルド時の依存関係を管理していることが挙げられます。
ライブラリ・バイナリ等を各OSの標準的なパスではなく /nix/store
配下のハッシュ値を含むパスに配置しつつ、PATH環境変数や動的リンクを明示的に設定します。
これによって同じライブラリの異なるバージョンに依存するS/Wを混ぜて利用する事ができたり、一時的に異なるバージョンのプログラムを有効化したりする事ができたりします。
プロジェクト毎に様々な開発環境が必要な開発者(S/Wエンジニア)には非常に便利であり、私もその魅力に最近はまりつつあります。Zenn でも多くの記事が書かれており、注目を集めていると感じますね。
1.2 manylinux
manylinux は Linux 向けの標準的な Python バイナリ形式を定め、バイナリをビルドするための開発環境を提供しているプロジェクトです。
一言に Linux と言っても様々なディストリビューションが存在するため、主要なディストリビューションで EOL を迎えていないバージョンが共通して保有しているライブラリ (glibc 他) の最も古いバージョンをピックアップして、安全に依存できるライブラリと定めています。
PyPI ではこの manylinux 向けにビルドされた wheel だけが Linux 向けのバイナリとして配布されています。
(manylinux は最大公約数であり十分に最適化できないため、PyTorch は独自にバイナリをホスティングしていたり(参考)、Python 以外も含めて一緒に管理できる Anaconda の方が好まれたりもしますが、今回の主題からは外れますのでこの話はこのぐらいで。)
2. NixOS における Python 利用・開発の課題
NixOS は動的リンクのローダーも含めたライブラリを Linux の標準的なパスに配置しないため manylinux に準拠していません。そのため Nix 外で配布されている (manylinux 準拠などの) バイナリを実行しようとすると、ファイルが存在しないというちょっと親切ではない語弊のあるエラーが発生します。
Nix 向けのパッケージを一元的に管理している nixpkgs ではコミュニティーにより Python のパッケージもパッチをあてたりしながらメンテナンスされています。とはいえ限界があるため PyPI の多数のパッケージの全てが対応されているわけではありません。
3. nix-ld による manylinux 互換環境の構築
nix-ld は Python に限らず Nix 向けにパッチを当てられていない動的リンクを含むバイナリを NixOS で実行できるようにするリンカー (ローダー?) です。
Linux 標準のパスにシンボリックリンクを張る必要があるので、nix-ld 自体は OS 全体の環境に NixOS の設定を通じてインストールすることになります。
{
programs.nix-ld = {
enable = true;
libraries = [];
};
}
nix-ld は NIX_LD_LIBRARY_PATH
環境変数を用いて動的リンク先を指定するので、devShell 毎に指定して使い分ける事ができます。
manylinux に対応する Nix のパッケージ群は nixpkgs に定義されているものが見つかります。
外からどういうパスでこの定義にアクセスできるのか見つけられなかったので、今回は手で直接コピーします。
{
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
};
outputs = inputs@{ self, nixpkgs, ... }:
let
system = "x86_64-linux";
pkgs = nixpkgs.legacyPackages.${system};
libraries = with pkgs; [
glibc
stdenv.cc.cc
xorg.libX11
xorg.libXext
xorg.libXrender
xorg.libICE
xorg.libSM
libGL
glib
zlib
expat
];
in {
devShells.${system}.default = pkgs.mkShell {
NIX_LD_LIBRARY_PATH = pkgs.lib.makeLibraryPath libraries;
packages = libraries ++ [pkgs.uv];
};
}
}
あとは nix develop
で devShell (開発シェル) を起動して使うことができます。注意事項として Nix で管理された Python を利用することはできないので、uv から stand alone build の Python を利用します。
uv run --with pandas example.py
4. ライブラリ化
ここに書いた内容を再利用できるように公開しました。
とりあえず manylinux 互換環境を立ち上げるだけであれば、(nix-ld の設定をした上で) 以下のコマンドで開発シェルが立ち上がります。
nix --experimental-features "nix-command flakes" develop -L github:ymd-h/manylinux-nixos
オプションで -I nixpkgs=github:NixOS/nixpkgs/nixos-24.11
(参考) 等と指定する事で nixpkgs のバージョンを固定する事もできます。
毎回長いコマンドを実行するのは大変なので、通常は flake.nix
を書いて開発環境を定義する事が好まれます。また開発環境に追加でインストールするライブラリを指定する事もできます。
{
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
manylinux = {
url = "github:ymd-h/manylinux-nixos";
inputs.nixpkgs.follows = "nixpkgs";
};
};
outputs = inputs@{ self, nixpkgs, manylinux, ... }:
let
system = "x86_64-linux";
pkgs = nixpkgs.legacyPackages.${system};
lib = manylinux.lib.${system};
in
{
devShells.${system}.default = lib.manylinux2014.mkShell {
extraPackages = [ pkgs.actionlint ];
};
};
}
どちらの場合も uv をインストール済みであり、Python 本体やライブラリの管理実行を uv で行えます。
uv run --with pandas example.py
また HATCH_ENV_TYPE_VIRTUAL_UV_PATH
をインストールしている uv のパスに指定済みのため、 uv をインストーラーにして hatch を利用できるようにしてあります。
uvx hatch run my-env:python example.py
5. 長所と短所
5.1 長所
- manylinux のバイナリをパッチを当てることなくそのまま使える
- uv 等のパッケージマネージャーを使える
- 非 Python 製の uv を推奨
5.2 短所
- Nix ベースの再現性を活用できない
- uv のロック機構により問題を緩和できる
6. まとめ
nix-ld を利用して NixOS 上に manylinux 互換環境を構築する方法を調べてライブラリとして公開しました。
これにより Nix による非 Python の再現可能な環境構築を維持しながら、uv を用いて Python の開発環境を簡単にセットアップできるようになりました。
Discussion