Rustプロジェクト上で、Nixを使用してビルド、チェック、cargo-llvm-covでのテストカバレッジを行なう方法
TL;DR
craneというRustプロジェクトをビルドするシステムにある、cargoLlvmCov関数ってスゴいよなぁという話。
どんなflake.nixを書いたか
{
inputs = {
nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
flake-utils.url = "github:numtide/flake-utils";
treefmt-nix.url = "github:numtide/treefmt-nix";
rust-overlay.url = "github:oxalica/rust-overlay";
crane.url = "github:ipetkov/crane";
};
outputs = { self, nixpkgs, treefmt-nix, rust-overlay, flake-utils, crane }:
flake-utils.lib.eachSystem [ "x86_64-linux" "aarch64-linux" ] (system:
let
overlays = [ (import rust-overlay) ];
pkgs = import nixpkgs {
inherit overlays system;
};
lib = pkgs.lib;
treefmtEval = treefmt-nix.lib.evalModule pkgs ./treefmt.nix;
rust = pkgs.rust-bin.fromRustupToolchainFile ./rust-toolchain.toml;
craneLib = (crane.mkLib pkgs).overrideToolchain rust;
src = ./.;
cargoArtifacts = craneLib.buildDepsOnly {
inherit src;
buildInputs = bevyengine-dependencies;
nativeBuildInputs = [ pkgs.pkg-config ];
};
kosu = craneLib.buildPackage {
inherit src cargoArtifacts;
buildInputs = bevyengine-dependencies;
nativeBuildInputs = [ pkgs.pkg-config ];
strictDeps = true;
doCheck = true;
};
cargo-clippy = craneLib.cargoClippy {
inherit cargoArtifacts src;
buildInputs = bevyengine-dependencies;
nativeBuildInputs = [ pkgs.pkg-config ];
cargoClippyExtraArgs = "--verbose -- --deny warnings";
};
cargo-doc = craneLib.cargoDoc {
inherit cargoArtifacts src;
buildInputs = bevyengine-dependencies;
nativeBuildInputs = [ pkgs.pkg-config ];
};
bevyengine-dependencies = with pkgs; [
udev
alsa-lib
vulkan-loader
# To use the x11 feature
xorg.libX11
xorg.libXcursor
xorg.libXi
xorg.libXrandr
# To use the wayland feature
libxkbcommon
wayland
];
llvm-cov-text = craneLib.cargoLlvmCov {
inherit cargoArtifacts src;
buildInputs = bevyengine-dependencies;
nativeBuildInputs = [ pkgs.pkg-config ];
cargoExtraArgs = "--locked";
cargoLlvmCovCommand = "test";
cargoLlvmCovExtraArgs = "--text --output-dir $out";
};
llvm-cov = craneLib.cargoLlvmCov {
inherit cargoArtifacts src;
buildInputs = bevyengine-dependencies;
nativeBuildInputs = [ pkgs.pkg-config ];
cargoExtraArgs = "--locked";
cargoLlvmCovCommand = "test";
cargoLlvmCovExtraArgs = "--html --output-dir $out";
};
in
{
formatter = treefmtEval.config.build.wrapper;
packages.default = kosu;
packages.doc = cargo-doc;
packages.llvm-cov = llvm-cov;
packages.llvm-cov-text = llvm-cov-text;
apps.default = flake-utils.lib.mkApp {
drv = self.packages.${system}.default;
};
checks = {
inherit kosu cargo-clippy cargo-doc llvm-cov llvm-cov-text;
formatting = treefmtEval.config.build.check self;
};
devShells.default = pkgs.mkShell rec {
buildInputs = bevyengine-dependencies ++ [
rust
];
nativeBuildInputs = [
pkgs.pkg-config
];
LD_LIBRARY_PATH = lib.makeLibraryPath buildInputs;
shellHook = ''
export PS1="\n[nix-shell:\w]$ "
'';
};
});
}
以上のflake.nixの解説
flake.nix
というモノは、パッケージのビルド式(正確にはDerivationと言う)を宣言出来たり、チェックを作成して走らせたり、開発環境を整えたりできる代物である。他にもフォーマッタを指定して、nix fmt
と打つだけでプロジェクト全体のフォーマットを走らせたり、nix-overlayを作成する事だって出来る。
今回作成した部分は、let - in
文の部分に書かれている。llvm-cov
とllvm-cov-text
という名前で宣言されている。ここで使用されている関数、cargoLlvmCov
はcraneLib
を定義しないと使用出来ない点を留意して欲しい。
cargoLlvmCov
関数は、ココに定義されている。
以上の式を書くと、nix build .#llvm-cov
とCLI上で打つとHTML形式のテストカバレッジの結果がビルドされる。nix build .#llvm-cov-text
と打つと、packages.llvm-cov-text
にllvm-cov-text
の方の定義が紐付けられているため、テキスト形式のテストカバレッジの結果がビルドされる事となる。
この時点での注意点は、cargoLlvmCov
にbuildInputs
とnativeBuildInputs
を引数に含める事が可能であるという点だ。これらを含めないと、カバレッジの生成が失敗してしまうケースがある(例えると、pkg-config
をnativeBuildInputs
に使用しているケースなど)。
GitHub ActionsでのCI
Nixを書くという事は、手元でのビルドのみを書いている訳ではない。せっかくビルドの再現性が担保されているというのに、CIで使用しないという事は、宝の持ち腐れである(言いすぎかも…?)。そこで、今回はGitHub Actionsでcargo-llvm-cov
の結果を、Nixでビルドしてみようではないか。
今回書いたWorkflow
name: nix-checker
on:
push:
permissions: {}
jobs:
run-nix-check:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
- name: Install Nix
uses: cachix/install-nix-action@v26
with:
nix_path: nixpkgs=channel:nixos-unstable
- name: Run nix flake check
run: nix flake check --all-systems
run-nix-build:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
- name: Install Nix
uses: cachix/install-nix-action@v26
with:
nix_path: nixpkgs=channel:nixos-unstable
- name: Run nix-build with flakes
run: nix build .#default
run-nix-build-doc:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
- name: Install Nix
uses: cachix/install-nix-action@v26
with:
nix_path: nixpkgs=channel:nixos-unstable
- name: Run nix-build with flakes
run: nix build .#doc
build-cargo-llvm-cov-text:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
- name: Install Nix
uses: cachix/install-nix-action@v26
with:
nix_path: nixpkgs=channel:nixos-unstable
- name: Run nix-build with flakes
run: nix build .#llvm-cov-text
- name: Upload coverage
uses: actions/upload-artifact@v4
with:
name: coverage-text
path: result
build-cargo-llvm-cov:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
- name: Install Nix
uses: cachix/install-nix-action@v26
with:
nix_path: nixpkgs=channel:nixos-unstable
- name: Run nix-build with flakes
run: nix build .#llvm-cov
- name: Upload coverage
uses: actions/upload-artifact@v4
with:
name: coverage-html
path: result
Workflowの解説
まずは、run-nix-*
たちを解説する。ひとまず、これらの名前を列挙する。
- run-nix-check
- run-nix-build
- run-nix-build-doc
これらのWorkflowでやっている事は単純で、単にUbuntu上にNixをインストールして、nix build .#default
やnix build .#doc
を実行しているだけなのである。ちなみにドキュメントのホスティングはここでは行なっていない。これをすると何が嬉しいかというと、以下の点が挙げられる。
- 手元でも同様にビルドが出来る点
- 手元でビルドしても同様の結果になる点
- Workflowが単純になり、読みやすくなる点
次に、build-cargo-llvm-cov*
たちを解説する。といっても、先程話した点とあまり変わらない。単にUbuntu上にNixをインストールして、nix build .#llvm-cov
やnix build .#llvm-cov-text
を実行しているだけである。こちらでは、ビルドの成果物をアーティファクトとしてアップロードしている。
注意点
私の感覚ではあるため、はっきりとした事は言えないが、このCIの実行方法には、ある弱点がある。それは、ビルド時間が長くなるという点だ(但し、Nixありと無しとで正確に時間をはかった事はない…)。そのために、プライベートリポジトリだったりGitLab CI
を使用すると、時間制限に悩まされる。もしもプライベートリポジトリやGitLab CI
を使用している際には、別のビルド方法やビルド時間軽減を考えた方が良いかもしれない。
Discussion