../nix-and-rust-dev

Nixで作るRustプロジェクト用の開発環境

この記事は Nix Advent Calendar の21日目の記事です。


はじめに

現在開発しているRustプロジェクトで使っている flake.nix は大体以下のようなものです。長いので今は細かくみる必要はありません。また、トップレベルに Cargo.tomlrust-toolchain.toml があることを想定しています。

{
  inputs = {
    nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";

    flake-parts = {
      url = "github:hercules-ci/flake-parts";
      inputs.nixpkgs-lib.follows = "nixpkgs";
    };

    rust-overlay = {
      url = "github:oxalica/rust-overlay";
      inputs.nixpkgs.follows = "nixpkgs";
    };

    git-hooks-nix = {
      url = "github:cachix/git-hooks.nix";
      inputs.nixpkgs.follows = "nixpkgs";
    };

    treefmt-nix.url = "github:numtide/treefmt-nix";

    crane.url = "github:ipetkov/crane";
  };

  outputs =
    inputs@{
      self,
      flake-parts,
      crane,
      ...
    }:
    flake-parts.lib.mkFlake { inherit inputs; } {
      flake = { };

      imports = [
        inputs.treefmt-nix.flakeModule
        inputs.git-hooks-nix.flakeModule
      ];

      systems = [
        "aarch64-linux"
        "x86_64-linux"
      ];

      perSystem =
        {
          config,
          system,
          pkgs,
          lib,
          self',
          ...
        }:
        let
          rust-bin = pkgs.rust-bin.fromRustupToolchainFile ./rust-toolchain.toml;
          craneLib = (crane.mkLib pkgs).overrideToolchain rust-bin;

          commonArgs = {
            src = craneLib.cleanCargoSource ./.;
            strictDeps = true;

            buildInputs = with pkgs; [ ];

            nativeBuildInputs = with pkgs; [ ];
          };

          cargoArtifacts = craneLib.buildDepsOnly (
            commonArgs
            // {
              pname = "deps";
            }
          );
        in
        {
          _module.args.pkgs = import inputs.nixpkgs {
            inherit system;
            overlays = [
              inputs.rust-overlay.overlays.default
            ];
          };

          treefmt = {
            projectRootFile = "flake.nix";

            programs.actionlint.enable = true;
            programs.nixfmt.enable = true;
            programs.rustfmt.enable = true;
            programs.taplo.enable = true;
            programs.yamlfmt.enable = true;
          };

          pre-commit = {
            settings = {
              hooks = {
                flake-treefmt = {
                  enable = true;
                  name = "flake-treefmt";
                  entry = lib.getExe config.treefmt.build.wrapper;
                  pass_filenames = false;
                };

                clippy.enable = true;
                cargo-check.enable = true;
              };

              settings.rust.check.cargoDeps = pkgs.rustPlatform.importCargoLock {
                lockFile = ./Cargo.lock;
              };
            };
          };

          packages.default = craneLib.buildPackage (
            commonArgs
            // {
              inherit cargoArtifacts;
              pname = "template"; # TODO: rename
              version = (builtins.fromTOML (builtins.readFile ./Cargo.toml)).package.version;
            }
          );

          devShells.default = pkgs.mkShell {
            inputsFrom = [ config.pre-commit.devShell ];

            buildInputs = with pkgs; [
              cargo-expand
              cargo-nextest

              rust-bin
            ];
          };
        };
    };
}

各部の説明

inputs

inputs(flakeの入力部分)で以下のようなものを取得しています。

outputs

どちらかというと重要なのは inputs よりもこちらです。

flake-parts

{
  outputs =
    inputs@{
      self,
      flake-parts,
      crane,
      ...
    }:
    flake-parts.lib.mkFlake { inherit inputs; } {

      flake = { };

      imports = [
        inputs.treefmt-nix.flakeModule
        inputs.git-hooks-nix.flakeModule
      ];

      systems = [
        "aarch64-linux"
        "x86_64-linux"
      ];

      perSystem = { ... }: { };#...
    };
}

systemsperSystem 部分に記述したAtribute Setの生成先を指定します。例として、以下のようなNix式は

{
  outputs =
    inputs@{
      self,
      flake-parts,
      ...
    }:
    flake-parts.lib.mkFlake { inherit inputs; } {
      systems = [
        "aarch64-linux"
        "x86_64-linux"
      ];

      perSystem = { ... }: {
        hello = "world";
      };
    };
}

このようになります(多分実際に試すと hello なんてないよみたいな感じで怒られます)。


{
  outputs = {
    hello.aarch64-linux = "world";
    hello.x86_64-linux = "world";
  };
}

imports では読み込む Flake Moduleを指定しています。また flake-parts.lib.mkFlake の引数、 flake には x86_64-linux などを追加しないもともとのflake要素をいれられますが今回のものではなにもいれていません。

perSystem

変数の定義

rust-toolchain.toml からRustのDerivation( rust-bin )を生成してそこから craneLib を作成します。 それを利用して、commonArgs(crane から生成するパッケージに共通で使う引数郡)とcommonArgscraneLib を使って cargoArtifacts を作成します。

cargoArtifacts はプロジェクトの依存だけのDerivationでこれを作成することで依存関係をキャッシュさせることが出来るようになります。

let
  rust-bin = pkgs.rust-bin.fromRustupToolchainFile ./rust-toolchain.toml;
  craneLib = (crane.mkLib pkgs).overrideToolchain rust-bin;

  commonArgs = {
    src = craneLib.cleanCargoSource ./.;
    strictDeps = true;

    buildInputs = with pkgs; [ ];

    nativeBuildInputs = with pkgs; [ ];
  };

  cargoArtifacts = craneLib.buildDepsOnly (
    commonArgs
    // {
      pname = "deps";
    }
  );
in
{
    # ...
}

以下のようにすることで inputsrust-overlay を読み込むことが出来ます。_module.args.pkgs はflake-partsの特殊な書き方で、通常は let の中で pkgs を定義します(system に自分のシステムをいれる)。

{
  _module.args.pkgs = import inputs.nixpkgs {
    inherit system;
    overlays = [
      inputs.rust-overlay.overlays.default
    ];
  };
}

treefmt-nixgit-hooks-nix の設定です。nix flake fmt でtreefmtが、nix flake check でclippyとcargo-checkが実行されます。コミット時にはどちらも実行されます。

{
  treefmt = {
    projectRootFile = "flake.nix";

    programs.actionlint.enable = true;
    programs.nixfmt.enable = true;
    programs.rustfmt.enable = true;
    programs.taplo.enable = true;
    programs.yamlfmt.enable = true;
  };

  pre-commit = {
    settings = {
      hooks = {
        flake-treefmt = {
          enable = true;
          name = "flake-treefmt";
          entry = lib.getExe config.treefmt.build.wrapper;
          pass_filenames = false;
        };

        clippy.enable = true;
        cargo-check.enable = true;
      };

      settings.rust.check.cargoDeps = pkgs.rustPlatform.importCargoLock {
        lockFile = ./Cargo.lock;
      };
    };
  };
}

RustのDerivationを作成しています。先に定義した、cargoArtifacts を使っています。また、versionには Cargo.toml からバージョンを取得してその値を入れています。

{
  packages.default = craneLib.buildPackage (
    commonArgs
    // {
        inherit cargoArtifacts;
        pname = "template"; # TODO: rename
        version = (builtins.fromTOML (builtins.readFile ./Cargo.toml)).package.version;
    }
  );
}

cargo-expandとcargo-nextest、そしてバージョンを指定した rustccargo 付きのdevShellを定義しています。nix develop するとdevShellが起動します。また、pre-commit を使えるようにしています。

{
  devShells.default = pkgs.mkShell {
    inputsFrom = [ config.pre-commit.devShell ];

    buildInputs = with pkgs; [
      cargo-expand
      cargo-nextest

      rust-bin
    ];
  };
}

おわりに

すぐに使える flake.nix ではありますが、いろいろもりもりで長めの記事になってしまいました。実際は部分的に導入していくことも出来ます。 最初の flake.nix は今作っている satler-git/etymora というプロジェクトの flake.nix に部分的に変更を加えたものです。実は他に satler-git/rust-template というリポジトリもありますが、なかなかフィードバックが出来ていません。

Tags /Nix/ /Rust/