🐍

NixOSでのPython開発

に公開

NixOS上でプログラミング

NixOSを使う方々は、OSやソフトウェアの構成を宣言的にすることができる点に惹かれて入門したのだと思っています。最初は大変ですが、慣れてくると自分の導入したいアプリや設定をconfiguration.nixに記述するだけなので細かい依存などを考える必要が無くなります。

では、この力をプログラミングにも使いたいというのは自然な流れです。

わざわざNixOSでなくともNixは使用できる

私はNixを1ヶ月しか使わずにNixOSへ飛び込みましたが、別にNixOSでなくともNixを使いこなすことはできます。

では、Nixをプログラミングに使用するとどのようなメリットがあるでしょうか。ぱっと思いつくだけでも以下があります。

  • グローバル環境を汚染しない: 特定のバージョンのPythonやNodeJSなどが必要な時、作業する時だけ有効にすることでグローバル環境とは独立したバージョンなどを使用できます。
  • 環境の再現が楽になる: 上とほぼ同じですが、別マシンに開発環境を移すのも楽です。
  • 依存の管理が楽になるかも: 依存をNix管理することで、バージョンの変更や再現が楽にできます。ただ、uvやnpmなどで生成されるlockファイルに任せるので十分という意見もあります。

実際にNixで開発環境を構築する場合は、以下のページが参考になりました。

https://zenn.dev/asa1984/books/nix-hands-on/viewer/ch02-03-devshell

https://bombrary.github.io/blog/posts/nix-dev-env/

Nixを使うのではなく、NixOSを使いたいがために入門した私にとっては、Nixそのものの可能性を理解するうえで非常に助けになりました。

Nixを用いた開発環境の構築

別にflakeを使用せずとも環境構築はできるのですが、バージョン管理をやりたいのでflakeを使用することにします。

今回、Python用として開発環境は以下のようにします。

 .
├──  .gitignore
├──  src
│  └──  modulename
│     ├──  __init__.py
│     ├──  __main__.py
│     └──  program.py
├──  test
│     └──  test.py
├──  LICENSE
├──  pyproject.toml
├──  README.md
└──  flake.nix

プロジェクトルートに以下のflake.nixを作成します。

flake.nix
{
  description = "A very basic flake for develop python";

  inputs = {
    nixpkgs.url = "github:nixos/nixpkgs?ref=nixos-unstable";
    flake-utils.url = "github:numtide/flake-utils";
  };

  outputs = { self, nixpkgs, flake-utils }:
    flake-utils.lib.eachDefaultSystem (system:
      let
        pkgs = nixpkgs.legacyPackages.${system};
        lib = pkgs.lib;
      in {
        devShells.default = pkgs.mkShell {
          buildInputs = with pkgs;
            [ git docker ] ++ (with python313Packages; [
              python
              numpy
            ]);
          pure = true;
        };
      });
}

保存したら、flake.nixのあるディレクトリにてシェルでnix developとすると環境に入ることができます。環境に入ると、flake.nixで定めた通りの環境が有効化されたbashが起動します。このbashからexitすると元通りになります。別プロセス視点では元環境の状態のままなので、別環境の汚染もしていません。

ここで重要なのは、devShellsとその中身です。

devShellsはnix developで作られるデフォルトの環境を定義します。別の名前にすることで、nix developの引数に環境の名前として渡し、複数環境を使い分けることもできます。

pkgs.mkShellが環境を作る本体の関数です。引数としてAttrSetを渡します。

  • buildInputs: 環境で有効化されるnixpkgsのモジュール。

なお、mkShellによって、bashなどが有効化されることが事前に設定されています。

環境に入ると、グローバル環境にPython3.13が無くともPythonのREPLを起動できます。Numpyも無事Importできます。

問題

bash以外を使いたい

私は普段、zshを使っています。中でzshを起動すれば使えはしますが、直接zshが起動してほしいです。

幸い、nix developには初期化フックが用意されています。pkgs.mkShellの引数になっているAttrSetに以下を追加することで、環境起動時にbashでスクリプトが実行されます。ここでは、その場でzshを起動し、zsh終了時に環境からもexitします。

shellHook = ''
  exec zsh
'';

uvを使おうとすると

「モジュールをNixpkgsから探すのは面倒だからPython moduleのバージョン管理はuvに任せるか」と思って、buildInputsにuvを登録したとします。

このとき、uv自体を走らせるためにPythonが必要なので、この環境かグローバル環境にuvが指定するバージョンのPythonを入れる必要があります。非NixOSならそこまで気にする必要は無いかもしれません。しかし、NixOSでは他にも深刻な問題があります。

nix store外のアプリの制限

さて、uvを起動できました。uvはPythonのバージョン管理をすることができ、複数バージョンのPythonを扱うことができるパッケージマネージャーです。しかし、uvでPythonをInstallしてもNixOSの制限により、このPythonを使用することができません。NixOSでは全てのバイナリはnix storeに入れる必要があります。

NixOSはFHSに従っていない

uvでPython本体のバージョン管理を諦め、モジュールの管理に主眼を当てることにします。試しにnumpyを入れます。

uv add numpy

しかし、Pythonからnumpyをimportできません。

ImportError: libstdc++.so.6: cannot open shared object file: No such file or directory

これは、NixOSがFHSに従っていないことで、/libに必要な共有ライブラリのファイルが見当たらないためです。

NixOSでnumpyを使用したい場合、解決策は2つあります。

ひとつは、FHSに従うようにすることです。nix-community/nix-ldを使用することで、FHSに従うファイル構造を生成できます。ただし、万能ではないうえ、Nixらしくない解決法です。

もうひとつは、上に示した例のようにNixpkgsにあるモジュールをそのまま使用することです。必要な共有ライブラリはnix storeにあるものを参照するので、問題を起こすことなく使用できます。その代わり、uvの使用は難しいです。

そもそもnumpyを入れて問題が発生したのは、numpyは計算速度向上のためにバイナリを同封しており、それが標準モジュールを使用するも、FHS通りの場所を参照したことでファイルが見つからなかったためです。試していませんが、Pythonコードで書かれただけのモジュールであれば問題なくuv経由でも導入できると思われます。

自作プログラムをimportできない

Nixに入門するまで、pipやuvによってモジュールのimportの設定はされていました。自作モジュールも管理対象に含めるのは簡単で、pip install -e .などで追加できました。

しかし、現状はNixによってモジュールが設定されたものの、自作モジュールを認識させることはできていない状態です。これを認識させる必要があります。

なお、shellHookでpip install -e .をしようにも、pipのあるディレクトリ=nix store内へ情報を記入することになるので、nix storeの制限よりブロックされます。

実は、Pythonがimportできるモジュールの検索対象ディレクトリはsys.pathに格納されます。NixOSでは、これをカスタムするために$PYTHONPATHという環境変数を編集しています。これはモジュールの場所を指定するもので、PATHのようにコロン区切りで記述します。

というわけで、PYTHONPATHに追記します。今回のケースではpyproject.toml./src内を見るよう設定していますが、これはpipやuv用です。そのため、自分でNix側に反映させる必要があります。他モジュールが設定された後、最後に設定します。

shellHook = ''
  PYTHONPATH = $PYTHONPATH:$(pwd)/src
  export PYTHONPATH
  exec zsh
'';

最終形

flake.nix
{
  description = "A very basic flake for develop python";

  inputs = {
    nixpkgs.url = "github:nixos/nixpkgs?ref=nixos-unstable";
    flake-utils.url = "github:numtide/flake-utils";
  };

  outputs = { self, nixpkgs, flake-utils }:
    flake-utils.lib.eachDefaultSystem (system:
      let
        pkgs = nixpkgs.legacyPackages.${system};
        lib = pkgs.lib;
      in {
        devShells.default = pkgs.mkShell {
          buildInputs = with pkgs;
            [ git docker ] ++ (with python313Packages; [
              python
              numpy
            ]);
          pure = true;
          shellHook = ''
            PYTHONPATH=$PYTHONPATH:$(pwd)/src
            export PYTHONPATH
            exec zsh
          '';
        };
      });
}

uvの使用を諦めるのにまず時間がかかりました。もし他にも便利にできることがあれば追記します。

Discussion