🦉

Nixとuvで磨くPython開発環境の再現性

に公開
2

LLM自作入門 という本の輪読会に参加しており、書籍のGitHubを用いて環境構築をしました。この記事ではLLM自作入門の第二章の環境再現を通じて、Nixとuvを利用した環境構築手法を紹介します。

本記事は requirements.txt が提供されているケースを紹介しますが、ゼロから作る場合でもほぼ同じです。(uv addが不要なだけ)

本記事の対象者はPythonの経験者です。Nixやuvの前提知識は問いません。

再現可能な環境を目指す

Python(特にCPython) はmacOSや一部のLinux Distributionに採用されているため、グローバルインストールすると思わぬ副作用が発生する可能性があります。また、ハッシュ無しのrequirements.txtのみで依存パッケージを管理している場合、再現に失敗することがあります。

再現性が担保されない環境では、実装者ごとに挙動が変わりかねません。また、実装してから月日が立った場合に挙動が変わる可能性もあります。実際、著者は数年前のPython本のコードが動かない経験をしたことがあります。

本記事では、Nixとuvを利用して「LLM自作入門の第二章の環境」の再現性を高めます。最終的にはNixとuvのロックファイル(プロジェクトが依存するすべてのパッケージおよび、そのバージョン・ハッシュ値を記録したファイル)が作成されます。

8月13日追記

また、requirements.txtはロックファイルのような機構を持たないため、完全な再現性が担保されません。

について誤りがあったため修正しました (@zhanpon 様ありがとうございました)

環境構築の流れ

次の流れで環境構築をします。

  1. 開発用シェルの作成
  2. Pythonプロジェクトの作成

開発用シェルの作成

Nixとnix developの紹介

Nixは再現可能なビルドを実現するパッケージマネージャです。ビルドが、(ストアになければ) ホスト内のサンドボックスで行なわれるので、いわゆるDependency Hell (依存関係地獄) を回避できます。また、依存関係やビルド手順を宣言的に記述できることも特徴です。

開発中に便利なのが、Nixのコマンドである nix develop です。flake.nixなどに定義された設定をもとに、一時的な開発用のシェルを立ち上げられます。開発用シェル内では宣言した依存パッケージが揃っており、ツールやライブラリが使えます。そしてセッションを終了すれば、その環境は破棄され、ホストシステムには変更が残りません。

Nixのインストール

公式の案内に従ってインストールしてください。有名どころのOSであれば1行実行するだけで入ります。

https://nixos.org/download/

Nixでuv環境を作るソースコード

実際のコードを見てみましょう。

{
  description = "Python Shell";

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

  outputs = { self, nixpkgs, flake-utils }:
    flake-utils.lib.eachDefaultSystem (system:
      let
        pkgs = import nixpkgs { inherit system; };
      in {
        devShell = pkgs.mkShell {
          buildInputs = [
            pkgs.python312
            pkgs.uv
          ];
          shellHook = ''
            echo "uv version: $(uv --version)"
            echo "python version: $(python --version)"
          '';
        };
      });
}

※何やら難しそうなコード が出てきましたが、読む部分は以下の部分だけで十分です。

buildInputs = [
  pkgs.python312
  pkgs.uv
];
shellHook = ''
  echo "uv version: $(uv --version)"
  echo "python version: $(python --version)"
'';
  • buildInputs にパッケージを記述します。今回はuvとPythonのバージョン3.12を入れています。パッケージ名称はNixOS Searchで検索すれば出てきます。
  • shellHook にシェル起動時に動くコマンドを記述できます。今回はそれぞれのバージョンを確認します。

※ Nixの設定ファイルは関数型言語であるNix言語で記述します。本記事は関数型言語に馴染みの無い方を想定しているため、このような説明をしました。

開発用シェルを起動

nix developで起動すると次のようにシェルが立ち上がります。

$ nix develop
uv version: uv 0.8.0
python version: Python 3.12.11

NixでPythonが管理できているか確認してみましょう。

bash-5.2$ which python
/nix/store/a97d77dxagnddkn5484kfc2gyj3dcdnq-python3-3.12.11/bin/python

ホストOSのPythonを利用していないことが分かりました。
(nix developで起動したシェルは既存のツールや環境変数を引き継ぐため、記載していないツールはHost OSのパスになります)

Pythonプロジェクトの作成

uvの紹介

uvは、Pythonのパッケージ管理と実行環境を高速に構築できるツールです。ロックファイルを作成するのでバージョンのブレを防げます。また、依存関係解決・インストール速度が非常に速いです。ここ1年ぐらいで利用が大きく広まったので、名前を聞いたことがある方もいるかもしれません。

プロジェクトを初期化

nix develop を実行したディレクトリで初期化します。毎回忘れるので --help で確認しています。今回は uv init chapter_2 --app としました。

bash-5.2$ uv init --help
Create a new project

Usage: uv init [OPTIONS] [PATH]
Options:
      --name <NAME>                    The name of the project
      --bare                           Only create a `pyproject.toml`
      --package                        Set up the project to be built as a Python package
      --no-package                     Do not set up the project to be built as a Python package
      --app                            Create a project for an application
      --lib                            Create a project for a library
      --script                         Create a script

Pythonパッケージインストール

LLM自作入門のrequirements.txtを確認すると次のようになっています。今回はch02の環境構築をしたかったので、ch04移行の依存関係はコメントアウトしました。

torch >= 2.3.0             # all
jupyterlab >= 4.0          # all
tiktoken >= 0.5.1          # ch02; ch04; ch05
# matplotlib >= 3.7.1      # ch04; ch06; ch07
# tensorflow >= 2.18.0     # ch05; ch06; ch07
# tqdm >= 4.66.1           # ch05; ch07
# numpy >= 1.26, < 2.1     # dependency of several other libraries like torch and pandas
# pandas >= 2.2.1          # ch06
# psutil >= 5.9.5          # ch07; already installed automatically as dependency of torch

ではインストールしてみましょう。requirements.txt経由でインストールする場合は次のコマンドを実行します。

bash-5.2$ uv add --requirements requirements.txt
Using CPython 3.12.11 interpreter at: /nix/store/a97d77dxagnddkn5484kfc2gyj3dcdnq-python3-3.12.11/bin/python3.12
Creating virtual environment at: .venv
Resolved 122 packages in 659ms
Prepared 102 packages in 11.85s
Installed 102 packages in 407ms
...
 + jupyterlab==4.4.5
...
 + tiktoken==0.11.0
...
 + torch==2.8.0
...

インストールが終わったら pyproject.toml を確認します。requirements.txt で記述した依存関係が記載されていればOKです。

[project]
name = "chapter-2"
version = "0.1.0"
description = "Add your description here"
readme = "README.md"
requires-python = ">=3.12"
dependencies = [
    "jupyterlab>=4.0",
    "tiktoken>=0.5.1",
    "torch>=2.3.0",
]

おわりに

再現性の担保といわれると、大量の複雑なコードが必要なのかなと感じるかもしれませんが、意外と簡単だったと思います。この記事をきっかけにNix + uvの利用者が増えれば幸いです。

参考

Nix

Nixについては次のZenn本がおすすめです。

https://zenn.dev/asa1984/books/nix-introduction

https://zenn.dev/asa1984/books/nix-hands-on

uv

uvは公式ドキュメントとGitHubのリリースノートを参考にしています。

https://docs.astral.sh/uv/

https://github.com/astral-sh/uv

補足

最新版のuvをインストールしたいという方もいると思います。その場合は cargoNix 経由で取得しBuildすると良いでしょう。

cargo install --git https://github.com/astral-sh/uv uv

Discussion

zhanponzhanpon

Nixに入門したいと思っていたので、この記事を参考にはじめてみようと思います!

requirements.txtはロックファイルのような機構を持たないため、完全な再現性が担保されません

実はrequirements.txtにもロックファイル相当の機能があるので少し補足させてください。

  1. ロック: pip-compileコマンドを利用すればハッシュ付きのrequirements.txtが生成できます
  2. インストール: python3 -m pip install --require-hashes --only-binary :all: -r requirements.txtのようにすればrequirements.txtを検証してからインストールします

参考資料:

しゅんそくしゅんそく

@zhanpon
補足いただきありがとうございます!! 記事修正致します。
requirements.txt 側にもLockfile相当の機能があるのは目から鱗でした。