背景

Nix Flakes 入門 nix build & nix run 編です。このブログはシェルスクリプトでビルドしていますが、 nix build コマンドでビルドできるように、簡単な flake.nix を作成しました。これで reproducible なブログ (html) になったかと思います。

実装

以下では Nix Flakes の環境構築を前提に、 flake.nix を作って行きます。

Nix Flakes の書き方

Nix Flakes を使って純粋な環境でビルドするためには、 Flake Outputspackages を定義します。 flake-utils のおかげで、 system 名 (x86_64_linux 等) に惑わされず、簡潔に flake.nix を作成できます:

{
  description = "A basic flake for my devlog";

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

  outputs =
    { nixpkgs, flake-utils, ... }:
    flake-utils.lib.eachDefaultSystem (
      system:
      let
        pkgs = import nixpkgs { inherit system; };
      in
      {
        # nix build の対象一覧
        packages = {
          # nix build .#devlog でビルドできる:
          devlog = pkgs.stdenvNoCC.mkDerivation {
            # 後述
          };
        };
      }
    );
}

参考:

Hello, nix build!

packages 中に devlog パッケージを記載してみます:

          # nix build .#devlog でビルドできる:
          devlog = pkgs.stdenvNoCC.mkDerivation {
            name = "devlog";
            src = ./.; # 1
            nativeBuildInputs = []; # 2
            buildPhase = '' # 3
              ls > ls.txt
              pwd > pwd.txt
            '';
            installPhase = '' # 4
              mkdir -p $out
              mv ls.txt $out/
              mv pwd.txt $out/
            '';
          };
        };
flake.nix 全文
{
  description = "A basic flake for my devlog";

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

  outputs =
    { nixpkgs, flake-utils, ... }:
    flake-utils.lib.eachDefaultSystem (
      system:
      let
        pkgs = import nixpkgs { inherit system; };
      in
      {
        # nix build の対象一覧
        packages = {
          # nix build .#devlog でビルドできる:
          devlog = pkgs.stdenvNoCC.mkDerivation {
            name = "devlog";
            src = ./.;
            nativeBuildInputs = [];
            buildPhase = ''
              ls > ls.txt
              pwd > pwd.txt
            '';
            installPhase = ''
              mkdir -p $out
              mv ls.txt $out/
              mv pwd.txt $out/
            '';
          };
        };
      }
    );
}

nix build は隔離された環境で実行されます。

devlog package を nix build <path>#<package> の形でビルドできます:

$ git add flake.nix
$ nix build .#devlog

現ディレクトリ下に install phase の $out ディレクトリへの symlink (result) が生成されており、 install phase で保存したファイルを確認できます:

$ ls -lA result
lrwxrwxrwx 1 tbm 50 May 10 06:33 result -> /nix/store/71c50cjbmairbhv20mar337i1jrg4iyg-devlog/
$ ls result/
ls.txt  pwd.txt
$ cat result/ls.txt # Git 管理化のファイルのみが見える
~~省略~~
$ cat result/pwd.txt
/build/2mxrczxdffffd75aj2wgz7mml9mzww8g-source

なお /build ディレクトリは存在せず、実際は /tmp の一時ディレクトリで build phase が実行されるようです。この辺りの sandbox 環境は、今はブラックボックスとしておきます:

$ bin/
bin/  boot/  d/  dev/  etc/  home/  lib/  lib64/  lost+found/  media/  nix/  opt/  proc/  root/  run/  srv/  sys/  tmp/  usr/  var/

Devlog をビルドする

仮置きの buildPhaseinstallPhase を書き換えて、実際に devlog をビルドします:

          devlog = pkgs.stdenvNoCC.mkDerivation {
            name = "devlog";
            src = ./.;
            nativeBuildInputs = with pkgs; [ # 1
              (emacs.pkgs.withPackages (epkgs: with epkgs; [ seq esxml ]))
              nodePackages.prettier
            ];
            buildPhase = ''
              export HOME="$(mktemp -d)" # 2
              emacs -Q --script "./build.el" -- "--release" # 3
              prettier --print-width 100 --write out/*.html out/diary/*.html
            '';
            installPhase = ''
              mkdir -p $out
              mv out $out/site
            '';
          };

nix build の使い道

nix build ではキャッシュが活かせないため、開発環境ではすべての記事をビルドすることになって無駄です。開発中は devShell を使ったり、ビルド用コマンドを flake.nix で定義して、 nix run でユーザ環境のファイルを直接読み書きしたほうが良いかもしれません。

追記: nix run 用の定義を作成しました。 packages の隣に置くと、 nix run .#build で実行できます:

        apps.build = flake-utils.lib.mkApp {
          drv = pkgs.writeShellApplication {
            name = "build";
            runtimeInputs = with pkgs; [
              (emacs.pkgs.withPackages (epkgs: with epkgs; [ seq esxml ]))
              nodePackages.prettier
            ];
            text = ''
              emacs -Q --script "./build.el" -- "--release"
              prettier --print-width 100 --write out/*.html out/diary/*.html
            '';
          };
        };

ビルドコマンドを apps.buildpackages.devlog の間でDRY すると、 flake.nix 全体はこうなります:

flake.nix
{
  description = "A basic flake for my devlog";

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

  outputs =
    { nixpkgs, flake-utils, ... }:
    flake-utils.lib.eachDefaultSystem (
      system:
      let
        pkgs = import nixpkgs { inherit system; };
        buildCommand = pkgs.writeShellApplication { # 1
          name = "buildCommand"; # 2
          runtimeInputs = with pkgs; [
            (emacs.pkgs.withPackages (epkgs: with epkgs; [ seq esxml ]))
            nodePackages.prettier
          ];
          text = ''
            emacs -Q --script "./build.el" -- "--release"
            prettier --print-width 100 --write out/*.html out/diary/*.html
          '';
        };
      in
      {
        apps.build = flake-utils.lib.mkApp {
          drv = buildCommand; # 3
        };
        packages = {
          devlog = pkgs.stdenvNoCC.mkDerivation {
            name = "devlog";
            src = ./.;
            nativeBuildInputs = with pkgs; [
              buildCommand # 4
            ];
            buildPhase = ''
              export HOME="$(mktemp -d)"
              buildCommand # 5
            '';
            installPhase = ''
              mkdir -p $out
              mv out $out/out
            '';
          };
        };
      }
    );
}

GitHub Actions のセットアップ

手元で nix build を使う意味が無かったので、 GitHub Actions で使うことにします。参考:

name: "Main"

# main branch への push 時に実行
on:
  push:
    branches: 'main'

jobs:
  build:
    name: Build
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v4
    - uses: cachix/install-nix-action@v31
    - name: Build the devlog
      run: nix build .#devlog
    - name: Upload devlog artifact # 1
      uses: actions/upload-pages-artifact@v3
      with:
        path: result # 2

  deploy:
    name: Deploy
    runs-on: ubuntu-latest
    needs: build

    # Grant GITHUB_TOKEN the permissions required to make a Pages deployment
    permissions:
      pages: write      # to deploy to Pages
      id-token: write   # to verify the deployment originates from an appropriate source

    environment:
      name: github-pages
      url: ${{ steps.deployment.outputs.page_url }}

    steps:
      - name: Deploy to GitHub Pages
        id: deployment
        uses: actions/deploy-pages@v4 # 3

まとめ

nix build を使い、隔離された環境で devlog をビルドできるようになりました。 nix build は、 PATH はもちろん、ファイルシステムとしても隔離された環境で実行されることが認識できました。

ローカルでは nix build の使い道が無かったため、 GitHub Actions に利用してみました。 html の diff はローカルで見ちゃえば良いかと思います。 cachix-action を使えば高速化できそうですが、今は導入していません。

この記事は GitHub Actions によってデプロイされる予定です。