❄️

Nixでプロジェクトの環境構築をする

2024/12/11に公開

はじめに

この記事は、Wano Group Advent Calendar 2024の11日目の記事です。

Nixとは

Nixを簡潔に説明するのは難しいですが、以下のようないくつかの側面を持っています。

  • 再現性の高いビルドシステムとしてのNix
  • 上記のビルドシステム上でビルドしたパッケージを管理するパッケージマネージャとしてのNix
  • ビルド設定を宣言的に記述するための独自DSLとしてのNix言語
  • Nix言語で設定を記述し、Nixパッケージマネージャでパッケージ管理を行うLinuxディストリビューションとしてのNixOS

Nixとは何なのかについての詳細を知るには以下のBookがおすすめです。

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

本稿では、パッケージマネージャーとしてのNixを使用して1プロジェクトの開発環境を用意する流れの一例を説明したいと思います。

Nixを導入する

以下に従って、パッケージマネージャーとしてのNixをインストールします。

nixコマンドが使用できるようになればインストール完了です。

$ nix --version
nix (Nix) 2.24.6

Nix Flakeプロジェクトの作成

Nix FlakesとはNixで依存関係を宣言的に管理するための仕組みです。flake.nixというファイルで依存関係を宣言し、flake.lockというファイルで依存のバージョンを固定します。

以下のコマンドでNix Flakeプロジェクトを作成します。

# https://github.com/NixOS/templates で用意されているテンプレートの一覧
$ nix flake show templates

# 最小のflake.nixを生成
$ nix flake init --template 'templates#trivial'

ちなみに、Nix Flakesはまだ公式的には(インストール時の手順にもあったように)experimental featuresの扱いです。
以前はNix Channelsという仕組みで管理されていて、それらを利用するのに古いコマンド体系であるnix-<サブコマンド名>のようなコマンド(nix-build, nix-shell, nix-env等)で操作を行っていました。

Nix Flakesではコマンド体系も新しくなり、nix <サブコマンド名>のような形式になっています。
Nix関連の過去記事を読む際、それがどちらの管理方式前提で書かれたものなのか気をつけていただければと思います。

参考)Flakes - NixOS Wiki

flake.nixを記述する

flake.nixはNixで依存関係を管理するためのファイルです。種類としてはnpmのpackage.jsonや、cargoのCargo.tomlに近いものと言えるでしょう。

flake.nixではinputoutputという2つのattributeをメインに記載していきます。

  • inputs - プロジェクトに必要な依存を定義
  • outputs - ビルドして生成する出力を定義

ここでは以下のようにflake.nixを記述します。

例)

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

  outputs = { nixpkgs, flake-utils, ... }:
    flake-utils.lib.eachDefaultSystem (system:
      let pkgs = import nixpkgs { inherit system; };
      in {
        devShells.default = pkgs.mkShell {
          packages = with pkgs; [
            nixfmt
            dprint
            yamlfmt
            shellcheck
            shfmt
            treefmt
            nodejs
            yarn-berry
          ];
      });
}

Nixのビルドシステムでは暗黙的な依存(環境ごとの共有リンクに依存している等)が発生しないよう保証されていて、flake.nixをチーム間で共有しておけば環境によって動作が異なるような事態(おま環)を避けることができます。

また、Nix Flakesでは、Git管理下にあるファイルを評価するようになっているため、一度flake.nixをコミットしておくとよいでしょう。

開発環境でコマンドを実行する

先ほど記述した、outputs.devShells.<system>.defaultのattributeはnix developというコマンドに対応しています。
このattributeで必要なパッケージを宣言すると、nix developコマンド内でそのパッケージを利用することができます。

# Nix Flake環境に入る
$ nix develop

# Nix Flake環境の中でコマンドを実行
$ nix develop -c treefmt

nix developを実行すると、flake.lockというバージョンを固定するためのファイルが生成されます。flake.lockで固定されたバージョンはnix flake updateで更新することができます。

devShellでは、内から外は見えるが、外から内は見えないような状態になっており、元々の環境でパスが通っているコマンドはdevShell内でも実行できます。

また、devShellの設定で使用したpkgs.mkShellでは、shellHookでdevShellに入った時に実行するコマンドを指定することもできます。

        devShells.default = pkgs.mkShell {
          packages = with pkgs; [
            nixfmt
            dprint
            yamlfmt
            shfmt
            shellcheck
            treefmt
            nodejs
            yarn-berry
          ];
+         shellHook = ''
+           yarn
+         '';
        };

npmパッケージのインストールなどが必要な場合はこれを活用するとよさそうです。

direnvでプロジェクト配下に入った時にdevShellを起動する

環境変数の自動設定などに使用されるdirenvを、nix-direnvを用いて、devShellを起動するように設定することができます。

nix-direnvを使用するにはそれぞれの環境へのインストールが別途必要です。

.envrcを作成し、

use flake

と記述し、プロジェクト配下で

$ direnv allow

を一度実行しておくと、Nix Flakeプロジェクト配下に入った時に自動でdevShellが起動し、必要な依存がすべて用意されます。

treefmtでフォーマッターをまとめる

treefmtという色々なフォーマッターをまとめるツールがあります。

以下のようにtreefmt.tomlを作成し、

[formatter.nix]
command = "nixfmt"
includes = ["./**/*.nix"]

[formatter.dprint]
command = "dprint"
options = ["fmt"]
includes = ["./**/*.json", "./**/*.md", "./**/*.toml"]

[formatter.yaml]
command = "yamlfmt"
includes = ["./**/*.yaml", "./**/*.yml"]

[formatter.shellscript]
command = "shfmt"
includes = ["./**/*.sh"]

以下のようにコマンドを実行すると、それぞれのファイル形式ごとにまとめてフォーマットしてくれます。

$ nix develop -c treefmt

GitHub ActionsでのCI/CD

CI/CD構築にあたってもNixはとても便利です。例えば、GitHub Actionsで特定の言語・ツールをインストールするのにsetup系のaction([actions/setup-node](https://github.com/actions/setup-node))などを利用することは多々ありますが、ここでNixを活用することもできます。
Nixをインストールするactionには以下のようなものがあります。

また、以下のactionも入れておくと便利です。

例).github/workflows/ci.yaml等の設定例

name: CI

on:
  push:
    branches:
      - main
  pull_request:
  workflow_dispatch:

jobs:
  static-check:
    runs-on: ubuntu-latest
    timeout-minutes: 10
    steps:
      - uses: actions/checkout@v4
      - uses: cachix/install-nix-action@27
      - uses: DeterminateSystems/magic-nix-cache-action@main
      - uses: DeterminateSystems/flake-checker-action@main
      - name: Check
        run: nix develop -c just check

GitHub Actionsでflake.lockの定期的な更新を促す

Nix Flakeで管理しているパッケージの更新もGitHub Actionsで行うと便利です。

DeterminateSystems/update-flake-lockというflake.lockの更新PRを自動で作成してくれるactionがあるので、これを定期的に実行させるようにしておくとよさそうです。

例).github/workflows/update.yaml等の設定例

name: update

on:
  schedule:
    # 毎月5日/25日の00:00に実行
    - cron: '0 0 5,25 * *'
  workflow_dispatch:

jobs:
  update-flake-lock:
    runs-on: ubuntu-latest
    timeout-minutes: 5
    steps:
      - uses: actions/checkout@v4
      - uses: DeterminateSystems/nix-installer-action@main
      - name: Generate branch date
        run: |
          echo "CURRENT_DATE=$(date +'%Y%m%d%H%M%S')" >> $GITHUB_ENV
      - uses: DeterminateSystems/update-flake-lock@main
        with:
          pr-title: "Update flake.lock"
          pr-labels: |
            dependencies
          pr-reviewers: ${{ github.actor }}
          branch: "deps/update-flake-lock-${{ env.CURRENT_DATE }}"
          commit-msg: "dpes: Update flake.lock"
          path-to-flake-dir: ./

テンプレート化する

最初に登場した、nix flake initコマンドでは、テンプレートとしてGitHubやGitLab等のリポジトリ、任意のURLを指定することができます。

参考)nix flake - Nix Reference Manual

そのため、あるリポジトリにテンプレートを用意しておけば、nix flake init --template github:<user>/<repository>#<directory>でこれら全てを1コマンドで揃えることができます。

例えば、nokazn/nix-starterというリポジトリのrustディレクトリ配下に

|-- .envrc
|-- .gitattributes
|-- .github
|   └-- workflows
|       └-- ci.yaml
|-- .gitignore
|-- .vscode
|   └-- settings.json
|-- Cargo.lock
|-- Cargo.toml
|-- default.nix
|-- flake.nix
|-- justfile
|-- shell.nix
└-- src
    └-- main.rs

のようにファイルを用意しておくと、

$ nix flake init --template github:nokazn/nix-starter#rust

で全てのファイルがローカルにコピーされます。

まとめ

Nixを調べたときに出てくる「純粋関数型」のようなワードを見ると重厚なイメージを持ってしまうかもしれません。ただ、必要に応じて少しだけ利用するみたいな手軽な使い方も可能なので、ニーズに応じて活用していただけるとよいかなと思いました。

みなさんも良きNixライフを!

最後に

WanoグループではWebエンジニアを募集しています。ご興味のある方は是非、 https://group.wano.co.jp/jobs/ からご応募ください!

GitHubで編集を提案

Discussion