Chapter 20

 §1. helloのビルド

いつまでHello, world!するんだと思ったあなたへ、こんにちは!
この本は入門書なので最後までハローワールドします。挨拶は大事ですからね。

と言っても、ここに至るまでの間に何度もGNU Helloをビルドしてきました。今回は、FlakeをGit込みでセットアップし、一から自作のhelloをNixパッケージ化します。プログラムの作成からビルドまでの一連の流れを確認しましょう。

1. Flakeのセットアップ

Flakeの作成
nix flake new hello-nix

コマンドを実行するとhello-nix/ディレクトリが作成され、その中に以下のflake.nixが配置されます。

flake.nix
{
  description = "A very basic flake";

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

  outputs = { self, nixpkgs }: {

    packages.x86_64-linux.hello = nixpkgs.legacyPackages.x86_64-linux.hello;

    packages.x86_64-linux.default = self.packages.x86_64-linux.hello;

  };
}

2. inputsの導入

初期のinputsにはgithub:nixos/nixpkgs?ref=nixos-unstablegithub:nixos/nixpkgs/nixos-unstableと等価)が設定されていますが、今回作るパッケージはNixOS以外のシステムでも利用したいので、nixpkgs-unstableを使います。また、flake-utilsで複数のプラットフォームに対応します。

flake.nix
{
- description = "A very basic flake";
+ description = "hello package written in Rust";

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

- outputs = { self, nixpkgs }: {
-
-   packages.x86_64-linux.hello = nixpkgs.legacyPackages.x86_64-linux.hello;
-
-   packages.x86_64-linux.default = self.packages.x86_64-linux.hello;
-
- };
+ outputs =
+   { nixpkgs, flake-utils, ... }:
+   flake-utils.lib.eachDefaultSystem (
+     system:
+     let
+       pkgs = nixpkgs.legacyPackages.${system};
+     in
+     {
+       packages = {
+         hello = pkgs.hello;
+         default = pkgs.hello;
+       };
+     }
+   );
}

今は初期状態と同じくNixpkgsのGNU Helloをエクスポートしていますが、これから私たちhelloに置き換えていきます。

3. helloを作る

今のコードはただNixpkgsが提供するGNU Helloを再エクスポートしているだけなので、自作のhelloに置き換えましょう。前回と同じではつまらないので、今回はRustで書きます。

helloの作成
mkdir src
touch ./src/hello.rs
src/hello.rs
fn main() {
    println!("Hello, world!");
}

ここで親切なRustaceanは「Hey you! Cargo(Rustのプロジェクト管理ツール)を使いなよ!」とアドバイスをくれると思いますが、諸事情により今回はrustc(Rustコンパイラ)のみを使います。

mkDerivationでビルド式を書きます。

flake.nix
{
  description = # (略)

  inputs = # (略)

  outputs =
    { nixpkgs, flake-utils, ... }:
    flake-utils.lib.eachDefaultSystem (
      system:
      let
        pkgs = nixpkgs.legacyPackages.${system};
+       hello = pkgs.stdenv.mkDerivation {
+         pname = "hello";
+         version = "0.0.1";
+         src = ./src;
+         nativeBuildInputs = with pkgs; [ rustc ];
+         buildPhase = ''
+           rustc ./hello.rs
+         '';
+         installPhase = ''
+           mkdir -p $out/bin
+           cp ./hello $out/bin/hello
+         '';
+       };
      in
      {
        packages = {
-         hello = pkgs.hello;
-         default = pkgs.hello;
+         inherit hello;
+         default = hello;
        };
      }
    );
}

nativeBuildInputsでビルド環境にrustcを導入し、buildPhaseでコンパイルします。生成された実行ファイルはinstallPhase$out/binにコピーします。

nix runで実行しましょう。

実行
$ nix run
# エラー発生!

「ファイルがないぞ!」というエラーが発生しました。今回はFlakeをGitリポジトリ化したため、NixはGit経由でファイルを探しますが、まだ追加したファイルをステージしていないのでエラーが発生します。git addしてからnix runしましょう。

ステージしてから実行
$ git add .

$ nix run
Hello, world!

4. リファクタリング

今回のような小さなプロジェクトならflake.nix1つで十分ですが、大きなプロジェクトならflake.nixに記述する内容は最小限に留めた方がいいので、ファイル分割してみましょう。また、パッケージのメタ情報が不足しているのでmkDerivationに設定を追加します。

リファクタリングに入る前にコミットしておきます。

コミットしておく
git commit --message="add hello-rs"

5.1. ファイル分割

importを使ってもいいですが、ここはNixpkgsが採用しているcallPackageパターン[1]を用います。nix/ディレクトリを作り、flake.nixに記述していたビルド式をnix/hello.nixに移します。

nix/hello.nix
{ stdenv, rustc }:
stdenv.mkDerivation {
  pname = "hello";
  version = "0.0.1";

  src = ../src; # 注意!nix/hello-rs.nixから見たsrc/への相対パス
  nativeBuildInputs = [ rustc ];
  buildPhase = ''
    rustc ./hello.rs
  '';
  installPhase = ''
    mkdir -p $out/bin
    cp ./hello $out/bin/hello
  '';
}

pkgs.callPackagenix/hello.nixをインポートします。

flake.nix
{
  description = # (略)

  inputs = # (略)

  outputs =
    { nixpkgs, flake-utils, ... }:
    flake-utils.lib.eachDefaultSystem (
      system:
      let
        pkgs = nixpkgs.legacyPackages.${system};
-       hello = # (略)
      in
      {
        packages = {
-         inherit hello;
-         default = hello;
+         hello = pkgs.callPackage ./nix/hello.nix { };
+         default = pkgs.callPackage ./nix/hello.nix { };
        };
      }
    );
}

実行して同じ結果が得られることを確認してください。

実行して確認
# nix/hello.nixを追加したのでステージング
$ git add .

$ nix run
Hello, world!

callPackage関数はPathとAttrSetを引数に取り、Pathで指定されたNixファイルの関数にpkgsを渡します。1.1. Nix言語の基本で扱ったように、AttrSetを引数にとる関数は受け取ったAttrSetからattributeを取り出して記述できるので、前述のhello.nixでは{ stdenv, rustc }というようにpkgs.stdenvpkgs.rustcを取り出しています。
また、今回はcallPackageの第二引数として空のAttrSetを渡していますが、このAttrSetはpkgsにマージされます。つまり、正確にはpkgs // {}hello.nixに与えられます。

callPackageの望ましい型
callPackage :: Path -> AttrSet -> Derivation

5.2. meta attributeの追加

mkDerivationではmetaというattributeで詳細なメタ情報を設定できます。

nix/hello.nix
-{ stdenv, rustc }:
+{
+  stdenv,
+  rustc,
+  lib,
+}:
stdenv.mkDerivation {
  pname = "hello";
  version = "0.0.1";

  src = ../src; # 注意!nix/hello-rs.nixから見たsrc/への相対パス
  nativeBuildInputs = [ rustc ];
  buildPhase = ''
    rustc ./hello.rs
  '';
  installPhase = ''
    mkdir -p $out/bin
    cp ./hello $out/bin/hello
  '';

+ meta = {
+   mainProgram = "hello";
+   description = "A hello world program written in Rust";
+   longDescription = ''
+     This is a demo package for the Nix-Hands-On, which is a hello world program written in Rust.
+   '';
+   license = lib.licenses.mit;
+   platforms = lib.platforms.all;
+ };
}

mainProgram

mainProgramnix runで実行されるプログラムを指します。複数の実行可能ファイルを含むパッケージやパッケージ名と実行可能ファイルの名前が異なるパッケージで利用します。nix runはデフォルトでは<ストアパス>/bin/<pname>を実行します。

descriptionとlongDescription

description/longDescriptionに記述された説明はsearch.nixos.orgで表示されます。
また、descriptionは、nix search <flake-url> <検索ワード>による検索の対象になります。

descriptionを利用した検索
# "rust"はパッケージ名には含まれないが、descriptionには含まれるためヒットする
❯ nix search . rust
* packages.x86_64-linux.default (0.0.1)
  A hello world program written in **Rust**

* packages.x86_64-linux.hello (0.0.1)
  A hello world program written in **Rust**

license

licenseにはパッケージのライセンスを指定します。lib.licensesは様々なライセンスを収録したAttrSetで、nixpkgs/lib/licenses.nixで定義されています。meta.licenseにunfreeとしてマークされているライセンスを指定すると、デフォルト設定では評価時にエラーが発生します。

Unfreeなライセンスを指定する
# 非営利・改変禁止なライセンスを指定
# https://creativecommons.org/licenses/by-nc-nd/4.0/deed.ja
license = lib.licenses.cc-by-nc-40;
Unfreeパッケージを評価
$ nix run
# エラー発生!

$NIXPKGS_ALLOW_UNFREE環境変数を設定したり、NixpkgsをインポートするときにallowUnfree = trueを指定することで、unfreeなライセンスのパッケージの評価を許可できます。

# Nix言語の非純粋な関数(getEnv)で環境変数を読み取るため、--impureオプションを付ける
$ NIXPKGS_ALLOW_UNFREE=1 nix run --impure
Hello, world!
Nixpkgsのインポート時にallowUnfreeを指定
pkgs = import nixpkgs {
  inherit system;
  config = {
    allowUnfree = true;
  };
};

platforms

platformsはパッケージがサポートするプラットフォームをListで指定します。nixpkgs/lib/systems/doubles.nixで定義されており、主に以下の区分でプラットフォームを指定できます。

  • OS(Linux, Darwin, Windows, FreeBSD, Cygwin, UNIXなど)
  • CPUアーキテクチャ(x86, ARM, RISC-Vなど)
  • etc...

platformsで指定されていないプラットフォームでパッケージを評価しようとするとエラーが発生します。例えば、platformslib.platforms.darwin(macOS)のみを指定し、Linuxで評価しようとするとエラーが発生します。

macOSのみをサポートする
platforms = lib.platforms.darwin;

今回はクロスプラットフォームなパッケージなので、lib.platforms.allを指定しています。

その他のattribute

基本的にmetaの情報はNixpkgsで利用されるものなので、これ以上は省略します。その他のattributeや詳細については以下の公式マニュアルを参照してください。

https://nixos.org/manual/nixpkgs/stable/#sec-standard-meta-attributes

脚注
  1. Callpackage Design Pattern - Nix Pills ↩︎