LoginSignup
3
0

More than 1 year has passed since last update.

nixで自前パッケージをビルドしてNixOSで使う

Last updated at Posted at 2022-10-30

概要

nixpkgsには現在80,000を超えるパッケージが登録されていますが、ArchLinuxと比べて特定の分野でかつ利用者が少ないソフトやAsusやRazerなどベンダー固有の設定をするユーティリティなどはまだ十分とは言い難い状況です。
このとき自分でbuildしてpathを通すことになりますが、nixではbuild手順の提供とnixpkgs、NixOSを拡張できるテンプレートが用意されているため比較的簡単に自前パッケージを取り組むことができます。
ここでは、ビルド手順の自動化からFlakesを用いるかつパッケージバージョンの自動追尾までを順に解説します。

ToDO

  • GitHub Action+nvfetcherでのパッケージ更新の自動追尾 (2022/10/29)
  • pythonやrustのパッケージの拡張の仕方 (2022/10/29)
    • buildPythonPackagesを使ってパッケージとして拡張するのでいい?
    • pythonPackagesのoverlaysはできる?(このnixpkgs(awscli2)とか参考になる???)

パッケージ作製前に

  • nix 2.7で廃止されたSchemeが多くある。(2022/03/07以前の書き方だと警告が出る)

    • ここのコードは新しいSchemeで書いているためnix 2.6以下では動かない
    • NixOS/Templatesも更新されていなくて新旧の書き方が混在している
  • Flakeを用いると

    • プロジェクトのテンプレートを提供する
    • 実行可能なプログラムであることを定義する
    • 外部のgitリポジトリから依存関係を取り組むことができる
    • パッケージのビルドと環境構築を同じリポジトリで管理できる
    • nixpkgs、NixOSの拡張を簡単にできる
  • nixは複数言語のためのビルドツール及び環境作製ツールである(nixos.orgでも例はどう環境をつくるかが多い)

  • nixでパッケージを作るということは、1. どこかしらのリポジトリやダウンロードページからソースを取ってきて 2. 手順にそってビルドして 3. そのパッケージが入った環境に入る みたいな認識

    • NixOS(nixパッケージマネージャー)はFilesystem Hierarchy Standard(FHS)に対応していない(/lib, /usr/binがない)
    • ビルド時に依存関係のパスを解決する必要がある→基本的に自動でやってくれる
      • まれにパスを自分で明示してやる必要がある(Tipsにて簡単に説明)
  • パッケージが作れればdockerコンテナのように自前の環境を作ることに応用できる(逆も然り)

  • nix build(Flake)を用いる方法だと自前のパッケージをモジュールとして組み込みやすい

  • コードはSumi-Sumi/nix-making-package-tutorial

nixを用いたbuildおよびbuild環境の提供

nix-buildのための記法

  • 書き方はnixpkgsのパッケージを見たほうがいい
    • (先頭を{pkgs ? (import <nixpkgs>) {} }: with pkgs; stdenv.mkDerivation{...}に置き換える)
  • リファレンスはNixpkgs ManualNixpkgs Users and Contributors Guide
  • nix-tutorialもわかりやすい
  • AURにパッケージがある場合、そのPKGBUILDをnixで読み替えれば大体書ける。(patchはいるかもしれないしいらないかもしれない)
  • sha256はtarball→nix-prefetch-url、git→nix-prefetch-gitで得られる
gnugrepのビルド
{ pkgs ? (import <nixpkgs> {}) }:
let 
  version = "3.7";
in
with pkgs; stdenv.mkDerivation {
  pname = "gnugrep";                      # パッケージ情報
  inherit version;

  src = fetchurl {                        # ソースコードの取得
    url = "mirror://gnu/grep/grep-${version}.tar.xz";
    sha256 = "0g42svbc1nq5bamxfj6x7320wli4dlj86padk0hwgbk04hqxl42w";
  };

  nativeBuildInputs = [ perl ];           # ビルド時に必要なもの(コンパイラとか)
  outputs = [ "out" "info" ];             # 分割ビルド、 $outはstore-path, $infoはmanとか
                                          # pkgs/build-support/setup-hooks/multiple-outputs.shや
                                          # Multiple-output packages: https://jtojnar.github.io/dumpling/#chap-multiple-outputを参照

  buildInputs = [ pcre libiconv ];        # コンパイラ以外で実行時に必要なもの(システムパッケージとしてインストールされる)

  doCheck = false;

  preConfigure = ''                       # ビルドは各Phasesがありその中でPrePhase, Phase, PostPhaseなどがある
    export MKDIR_P="mkdir -p"             # https://nixos.org/manual/nixpkgs/stable/#sec-stdenv-phases参照
  '';

  postInstall =
    ''
      rm $out/bin/egrep $out/bin/fgrep
      echo "#! /bin/sh" > $out/bin/egrep
      echo "exec $out/bin/grep -E \"\$@\"" >> $out/bin/egrep
      echo "#! /bin/sh" > $out/bin/fgrep
      echo "exec $out/bin/grep -F \"\$@\"" >> $out/bin/fgrep
      chmod +x $out/bin/egrep $out/bin/fgrep
    '';

  meta = with lib; {                      # meta情報、自前パッケージならhomepageとdescriptionとlicenseとplatformsがあればいい?
    homepage = "https://www.gnu.org/software/grep/";
    description = "GNU implementation of the Unix grep command";

    longDescription = ''
      The grep command searches one or more input files for lines
      containing a match to a specified pattern.  By default, grep
      prints the matching lines.
    '';

    license = licenses.gpl3Plus;

    platforms = platforms.all;
    mainProgram = "grep";
  };

  passthru = {inherit pcre;};
}

Flakeを用いた書き方

flake-utilsを使わない書き方

  • nix flake new --template templates/#c-hello ./c-helloでプロジェクトを作る
  • NixOS/Templetes:c-hellonix >= 2.7の書き方に直した。
コードと結果
{
  description = "Sample no using flake-utils";

  # Nixpkgs / NixOS version to use.
  inputs.nixpkgs.url = "nixpkgs/nixos-22.05";

  outputs = { self, nixpkgs }:
    let

      # to work with older version of flakes
      lastModifiedDate = self.lastModifiedDate or self.lastModified or "19700101";

      # Generate a user-friendly version number.
      version = builtins.substring 0 8 lastModifiedDate;

      # System types to support.
      supportedSystems = [ "x86_64-linux" "x86_64-darwin" "aarch64-linux" "aarch64-darwin" ];

      # Helper function to generate an attrset '{ x86_64-linux = f "x86_64-linux"; ... }'.
      forAllSystems = nixpkgs.lib.genAttrs supportedSystems;

      # Nixpkgs instantiated for supported system types.
      nixpkgsFor = forAllSystems (system: import nixpkgs { inherit system; });

    in
    {
      packages = forAllSystems (system:
      let
        pkgs = nixpkgsFor.${system};
      in
      with pkgs; {
        hello = stdenv.mkDerivation rec {
          name = "hello-${version}";
          src = ./.;

          nativeBuildInputs = [ autoreconfHook ];
        };
      });
    };
}
$ nix flake show
path:/home/<name/Templates/samplepkgs/c-hello?lastModified=1666067866&narHash=sha256-2x+v9Wl9PjeeraiBChPiSdfD89cIkbLHsSYJv4Udgpw=
└───packages
    ├───aarch64-darwin
    │   └───hello: package 'hello-20221018'
    ├───aarch64-linux
    │   └───hello: package 'hello-20221018'
    ├───x86_64-darwin
    │   └───hello: package 'hello-20221018'
    └───x86_64-linux
        └───hello: package 'hello-20221018'

複数パッケージある場合にデフォルトのパッケージを設定

  • 指定なしでnix buildnix shellを実行した際にビルドされる
コードと結果
  • recをつけて再帰的に変数を呼び出す
    • 自前パッケージは<nixpkgs>にないため、変数定義がされてないとなってエラーとなる
{
  description = "Multi-package sample";

  # Nixpkgs / NixOS version to use.
  inputs.nixpkgs.url = "nixpkgs/nixos-22.05";

  outputs = { self, nixpkgs }:
    let

      # to work with older version of flakes
      lastModifiedDate = self.lastModifiedDate or self.lastModified or "19700101";

      # Generate a user-friendly version number.
      version = builtins.substring 0 8 lastModifiedDate;

      # System types to support.
      supportedSystems = [ "x86_64-linux" "x86_64-darwin" "aarch64-linux" "aarch64-darwin" ];

      # Helper function to generate an attrset '{ x86_64-linux = f "x86_64-linux"; ... }'.
      forAllSystems = nixpkgs.lib.genAttrs supportedSystems;

      # Nixpkgs instantiated for supported system types.
      nixpkgsFor = forAllSystems (system: import nixpkgs { inherit system; });

    in
    {
      packages = forAllSystems (system:
        let
          pkgs = nixpkgsFor.${system};
        in
        with pkgs; rec {        # 再帰的に変数を呼び出す
          c-hello = stdenv.mkDerivation {
            name = "c-hello-${version}";
            src = ./c-hello;

            nativeBuildInputs = [ autoreconfHook ];
          };
          go-hello = buildGoModule {
            pname = "go-hello";
            inherit version;
            src = ./go-hello;
            vendorSha256 = "sha256-pQpattmS9VmO3ZIQUFn66az8GSmB4IvYhTTCFn6SUmo=";
          };
          default = c-hello;   # デフォルトパッケージの定義
        }
      );
    };
}jj
$ nix flake show
# 各アーキテクチャーでdefaultが定義されている
path:/home/<name>/Templates/samplepkgs/sample-flakes?lastModified=1666072389&narHash=sha256-WcPrywIo3N596bZ1A0ZpoIcKnGIaogAsnP%2fxcuHzdhY=
└───packages
    ├───aarch64-darwin
    │   ├───c-hello: package 'c-hello-20221018'
    │   ├───default: package 'c-hello-20221018'
    │   └───go-hello: package 'go-hello-20221018'
    ├───aarch64-linux
    │   ├───c-hello: package 'c-hello-20221018'
    │   ├───default: package 'c-hello-20221018'
    │   └───go-hello: package 'go-hello-20221018'
    ├───x86_64-darwin
    │   ├───c-hello: package 'c-hello-20221018'
    │   ├───default: package 'c-hello-20221018'
    │   └───go-hello: package 'go-hello-20221018'
    └───x86_64-linux
        ├───c-hello: package 'c-hello-20221018'
        ├───default: package 'c-hello-20221018'
        └───go-hello: package 'go-hello-20221018'

flake-utilsを使った方法

  • apps周りなどちょっとスッキリ書ける
  • アーキテクチャ・システムを指定しなくても主要なものは勝手に入れてくれる
    • flake-utils.lib.eachDefaultPackages ["x86_64-linux" "aarch64-linux"]のように指定することも可能
コードと結果
{
  description = "A sample with flake-utils";

  # Nixpkgs / NixOS version to use.
  inputs.nixpkgs.url = "nixpkgs/nixos-22.05";
  # Using flake-utils
  inputs.flake-utils.url = "github:numtide/flake-utils";

  outputs = { self, nixpkgs, flake-utils }:
  let
    # to work with older version of flakes
    lastModifiedDate = self.lastModifiedDate or self.lastModified or "19700101";

    # Generate a user-friendly version number.
    version = builtins.substring 0 8 lastModifiedDate;
  in
    flake-utils.lib.eachDefaultSystem (system:
    # 特定のシステムのみをサポートすることも可能
    # flake-utils.lib.eachSystem ["x86_64-linux" "aarch64-linux"] (system: 
      let pkgs = nixpkgs.legacyPackages.${system}; in
        with pkgs; {
          packages = flake-utils.lib.flattenTree rec{  # Flakesはネストされたパッケージは見えないのでそれの解消
            c-hello = stdenv.mkDerivation {
              name = "c-hello-${version}";
              src = ./c-hello;
              nativeBuildInputs = [ autoreconfHook ];
            };
            go-hello = buildGoModule {
              pname = "go-hello";
              inherit version;
              src = ./go-hello;
              vendorSha256 = "sha256-pQpattmS9VmO3ZIQUFn66az8GSmB4IvYhTTCFn6SUmo=";
            };
            default = c-hello;
          };
          apps = rec {
            c-hello = flake-utils.lib.mkApp { drv = packages.c-hello; };
            go-hello = flake-utils.lib.mkApp { drv = packages.go-hello; };
            default = c-hello;
          };
        }
  );
}
$ nix flake show
path:/home/<name>/Templates/samplepkgs/flake-templates/sample-with-flake-utils?lastModified=1666083014&narHash=sha256-dVeym+Lp4EgaVMzlXtmXqX61zSKoNbAkCvzX1Avwwdw=
├───apps
│   ├───aarch64-darwin
│   │   ├───c-hello: app
│   │   ├───default: app
│   │   └───go-hello: app
│   ├───aarch64-linux
│   │   ├───c-hello: app
│   │   ├───default: app
│   │   └───go-hello: app
│   ├───i686-linux
│   │   ├───c-hello: app
│   │   ├───default: app
│   │   └───go-hello: app
│   ├───x86_64-darwin
│   │   ├───c-hello: app
│   │   ├───default: app
│   │   └───go-hello: app
│   └───x86_64-linux
│       ├───c-hello: app
│       ├───default: app
│       └───go-hello: app
└───packages
    ├───aarch64-darwin
    │   ├───c-hello: package 'c-hello-20221018'
    │   ├───default: package 'c-hello-20221018'
    │   └───go-hello: package 'go-hello-20221018'
    ├───aarch64-linux
    │   ├───c-hello: package 'c-hello-20221018'
    │   ├───default: package 'c-hello-20221018'
    │   └───go-hello: package 'go-hello-20221018'
    ├───i686-linux
    │   ├───c-hello: package 'c-hello-20221018'
    │   ├───default: package 'c-hello-20221018'
    │   └───go-hello: package 'go-hello-20221018'
    ├───x86_64-darwin
    │   ├───c-hello: package 'c-hello-20221018'
    │   ├───default: package 'c-hello-20221018'
    │   └───go-hello: package 'go-hello-20221018'
    └───x86_64-linux
        ├───c-hello: package 'c-hello-20221018'
        ├───default: package 'c-hello-20221018'
        └───go-hello: package 'go-hello-20221018'

Overlays可能なパッケージの作製

  • ビルドを定義したファイルにpassthru = {runnable = (true or false);}を追記することでnix run .#<package>で実行できるように追加してくれる
    • passthruはビルド時には無視される任意の属性を定義できる。(meta情報の付与、ラベル付といった用途でつける)
  • ディレクトリ構造は以下を想定
    • packages/<package>/default.nixは各パッケージのビルド手順を定義
    • sources/sources.nixはパッケージバージョンやハッシュ、提供元を定義
.
├── flake.lock
├── flake.nix
├── packages
│   ├── c-hello
│   │   └── default.nix
│   └── go-hello
│       └── default.nix
└── sources
    └── sources.nix
コードと結果
  • flake.nix
{
  description = "A sample with flake-utils";

  # Nixpkgs / NixOS version to use.
  inputs.nixpkgs.url = "nixpkgs/nixos-unstable";
  # Using flake-utils
  inputs.flake-utils.url = "github:numtide/flake-utils";

  outputs = { self, nixpkgs, flake-utils } @ inputs:
    let
      lib = nixpkgs.lib;
      genPkg = func: name: {      # 別ファイルに記されたビルド定義とバッケージ情報などをまとめる
        inherit name;
        value = func name;
      };
      genApp = pkgs: name: {
        inherit name;
        value = flake-utils.lib.mkApp { drv = pkgs.${name}; };
        };
      # default.nixの属性にpassthru = { runnable = true }を追記するとnix runで実行できるように自動でappsに追加する
      isRunableApp = pkgs: name: if pkgs.${name}.passthru.runnable or false then name else null;
      runnableApps = pkgs: lib.remove null (map (isRunableApp pkgs) names);

      pkgDir = ./packages;        # ビルと手順が記されたパッケージディレクトリ
      sources = import ./sources/sources.nix;  # パッケージ情報が記されたディレクトリ

      ls = builtins.readDir pkgDir;
      names = with builtins; lib.remove null (map ((ts: t: if ts.${t} == "directory" then t else null) ls) (attrNames ls));  # ディレクトリのみを列挙
      withContents = func: with builtins; listToAttrs (map (genPkg func) names);

      mkApps = pkgs: appNames: with builtins; listToAttrs (map (genApp pkgs) appNames);
    in
      { # Overlaysの定義
        overlays.default = final: prev:      # final: prev:はpythonでいうself, super
        let
          sources' = sources {};
        in withContents (name:
          let
            pkgs = import (pkgDir + "/${name}");
            override = builtins.intersectAttrs (builtins.functionArgs pkgs) ({
              pythonPackages = final.python3.pkgs;
              mySource = sources'.${name};
            });
          in final.callPackage pkgs override
          ) // { sources = sources'; };
      } # ここから先はnix buildやnix runで使うための設定(このリポジトリ単体でも使えるようにする)
      // flake-utils.lib.eachSystem ["x86_64-linux" "aarch64-linux"] (system:
        let
          pkgs = import nixpkgs {
            system = "${system}";
            overlays = [ self.overlays.default ];
            config.allowUnfree = true;
          };
        in with pkgs.legacyPackages.${system}; rec {
          packages =  withContents (name: pkgs.${name});
          apps = mkApps pkgs (runnableApps pkgs);
        }
      );
}
├───apps
│   ├───aarch64-linux
│   └───x86_64-linux
├───overlays
│   └───default: Nixpkgs overlay
└───packages
    ├───aarch64-linux
    │   ├───c-hello: package 'c-hello-20221020'
    │   └───go-hello: package 'go-hello-20221020'
    └───x86_64-linux
        ├───c-hello: package 'c-hello-20221020'
        └───go-hello: package 'go-hello-20221020'

nvfetcherを用いて独自パッケージの自動追跡をする

  • nvfetcherは独自パッケージの更新を自動追跡してくれるツール

    • 自動追跡はGitHub Actionsを使うといい
    • GitHubのタグやブランチだけでなく、AURやweb・http header+正規表現を用いてバージョンを自動で取得してくれる
  • TOMLでパッケージのソース元、nix runで実行可能かなどを定義することができる

    • nvfetcher -c "/path/to/toml-file"_sources/以下に生成される
  • これまでの解説はこれをするための布石だった。

コードと結果
  • flake.nix
{
  description = "A sample with flake-utils";

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

  outputs = { self, nixpkgs, flake-utils, nvfetcher } @ inputs:
    let
      lib = nixpkgs.lib;
      genPkg = func: name: {      # 別ファイルに記されたビルド定義とバッケージ情報などをまとめる
        inherit name;
        value = func name;
      };
      genApp = pkgs: name: {
        inherit name;
        value = flake-utils.lib.mkApp { drv = pkgs.${name}; };
        };
      # default.nixの属性にpassthru = { runnable = true }を追記するとnix runで実行できるように自動でappsに追加する
      isRunableApp = pkgs: name: if pkgs.${name}.passthru.runnable or false then name else null;
      runnableApps = pkgs: lib.remove null (map (isRunableApp pkgs) names);

      pkgDir = ./packages;        # ビルと手順が記されたパッケージディレクトリ
      sources = import ./_sources/generated.nix;  # パッケージ情報が記されたディレクトリ
      broken = import ./packages/broken.nix;  # ビルドできないパッケージを列挙

      ls = builtins.readDir pkgDir;
      isDir = ts: t: if ts.${t} == "directory" then t else null; # ディレクトリのみを列挙
      names = with builtins; lib.subtractLists broken (lib.remove null (map (isDir ls) (attrNames ls)));
      withContents = func: with builtins; listToAttrs (map (genPkg func) names);

      mkApps = pkgs: appNames: with builtins; listToAttrs (map (genApp pkgs) appNames);
    in
      { # Overlaysの定義
        overlays.default = final: prev:      # final: prev:はpythonでいうself, super
        let
          sources' = sources { inherit (final) fetchgit fetchurl fetchFromGitHub ; };
        in withContents (name:
          let
            pkgs = import (pkgDir + "/${name}");
            override = builtins.intersectAttrs (builtins.functionArgs pkgs) ({
              pythonPackages = final.python3.pkgs;
              mySource = sources'.${name};
            });
          in final.callPackage pkgs override
          ) // { sources = sources'; };
      } # ここから先はnix buildやnix runで使うための設定(このリポジトリ単体でも使えるようにする)
      // flake-utils.lib.eachSystem ["x86_64-linux" "aarch64-linux"] (system:
        let
          pkgs = import nixpkgs {
            system = "${system}";
            overlays = [ self.overlays.default nvfetcher.overlay];  # nvfetcherもoverlayする
            config.allowUnfree = true;
          };
        in with pkgs.legacyPackages.${system}; rec {
          packages =  withContents (name: pkgs.${name});
          apps = mkApps pkgs (runnableApps pkgs);
          checks = packages;  # For `nix flake check`
          devShells.default = nvfetcher.packages.${system}.ghcWithNvfetcher;  # For `nix develop`
        }
      );
}
  • packages.toml
[c-hello]
src.manual = "2.12.1"
fetch.url = "mirror://gnu/hello/hello-2.12.1.tar.gz"

Tips

  • stdenv.mkDerivationはネットワーク接続のない環境でビルドを行う

    • あくまでソースのみを用いてビルドする場合に使える
    • cargopipが使えない
  • buildInputsはビルド時に必要なコマンドを提供する依存関係を示す(gccやcargoなど)

  • nativeBuildInputsはビルド時にコマンドとしては必要ないが、libなどにパスを通さなければいけない依存関係を示す。

    • 一般的な依存関係
  • nixpkgsは各々のパッケージが独立している

    • そのパッケージを実行するのに必要なPATH(環境)しか持たない
    • ホストのターミナルではPATHが通ってコマンドも実行できるのに、あるプログラムを実行するとPATHが通っていないと怒られるというのが多々ある
  • ビルド時に依存関係のパスは勝手に解釈してくれるが、シェルスクリプトプログラムの中からshellを叩く(pythonのsubprocessなど)場合は解釈してくれない

    • この場合、実行スクリプトにwrapProgramを適用してPATHを通さなければならない
    ...
    postFixup = ''
        wrapProgram $out/bin/hoge \  # プログラムはシェルスクリプトでもバイナリでもいい
            --prefix PATH : ${lib.makeBinPATH [ foo ]} \  # バイナリへのパスを通す
            --prefix LD_LIBRARY_PATH : ${lib.makeLibraryPATH [ bar ]  # ライブラリへのパスを通す
    ''
    ...
    
  • cargopipを用いて依存関係を得る場合はそれぞれ、rustPlatform.buildRustPackagepythonPackages.buildPythonPackageを使用する

    • nixpkgsのマニュアルに各言語のビルドの仕方が載っている

参考

3
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
0