Nix flakeのフォーマッタを自分で書く
nix fmtあるいはnix formatter run
Nix flakeのプロジェクトでコードのフォーマッタを実行するコマンドです。
nix fmt(an alias fornix formatter run) calls the formatter specified in the flake.Flags can be forwarded to the formatter by using
--followed by the flags.Any arguments will be forwarded to the formatter. Typically these are the files to format.
...
Example
To use the official Nix formatter:
# flake.nix { outputs = { nixpkgs, self }: { formatter.x86_64-linux = nixpkgs.legacyPackages.${system}.nixfmt-tree; }; }
これはNixで書かれたファイルに限らず、何でもフォーマットできます。
もひとつ引用。Vladimir TimofeenkoさんのPractical Nix flake anatomy:
The formatter is a single derivation that is run when nix fmt is executed. Most often, one of
pkgs.alejandra,pkgs.nixfmtorpkgs.nixpkgs-fmtis used. ... this attribute is not limited to formatting only nix code. It is possible to use a tool like treefmt to format code in arbitrary language when nix fmt is called.
treefmt(およびtreefmt-nix)を利用しているNixユーザは多いと思います。しかしこういった便利なflakeを使いたくないシチュエーションもあるでしょう。たとえば、公開していて多くの人に配布したいプロジェクトで依存関係を極力減らしたい、など。
Flakeの解説記事はちらほら見かけますが、formatterについて深堀りしたものはあまりない気がするので、実例も交えて紹介しようとこの記事を書いています。
え?そんなのもう知ってるよ、バカにすんじゃねえって? お呼びでない、こりゃまた失礼しました。
先人に学ぶ
treefmt-nix
たとえばtreefmt-nixでformatterがどう定義されているかを見てみましょう。
module-options.nix (lines: 213–255):
wrapper = mkOption {
description = ''
The treefmt package, wrapped with the config file.
'';
type = types.package;
defaultText = lib.literalMD "wrapped `treefmt` command";
default =
let
code =
if builtins.compareVersions "2.0.0-rc4" config.package.version == 1 then
''
set -euo pipefail
find_up() {
ancestors=()
while true; do
if [[ -f $1 ]]; then
echo "$PWD"
exit 0
fi
ancestors+=("$PWD")
if [[ $PWD == / ]] || [[ $PWD == // ]]; then
echo "ERROR: Unable to locate the projectRootFile ($1) in any of: ''${ancestors[*]@Q}" >&2
exit 1
fi
cd ..
done
}
tree_root=$(find_up "${config.projectRootFile}")
exec ${config.package}/bin/treefmt --config-file ${config.build.configFile} "$@" --tree-root "$tree_root"
''
# treefmt-2.0.0-rc4 and later support the tree-root-file option
else
''
set -euo pipefail
unset PRJ_ROOT
exec ${config.package}/bin/treefmt \
--config-file=${config.build.configFile} \
--tree-root-file=${config.projectRootFile} \
"$@"
'';
x = pkgs.writeShellScriptBin "treefmt" code;
in
(x // { meta = config.package.meta // x.meta; });
};
writeShellScriptBinでシェルスクリプトを作成しています。何をやっているのかというと、プロジェクトのルートディレクトリを探し、そこをターゲットにtreefmtを実行しています。しかしtreefmtがブラックボックスなので、これだけではよく分からない。
flakelight
flakelightでも軽量版のtreefmtのようなフォーマッタを利用できます。どのように実装されているのか見てみましょ。
builtinModules/formatter.nix (lines: 30–65):
outputs.formatter = mkDefault (genSystems
({ pkgs, lib, fd, coreutils, ... }:
let
inherit (lib) attrValues makeBinPath;
formatters = config.formatters pkgs;
fullContext = all hasContext (attrValues formatters);
packages =
if config.devShell == null then [ ]
else (config.devShell pkgs).packages pkgs;
caseArms = toString (mapAttrsToList
(n: v: "\n ${n}) ${v} \"$f\" & ;;")
formatters);
in
pkgs.writeShellScriptBin "formatter" ''
PATH=${if fullContext then "" else makeBinPath packages}
if [ $# -eq 0 ]; then
flakedir=.
while [ "$(${coreutils}/bin/realpath "$flakedir")" != / ]; do
if [ -e "$flakedir/flake.nix" ]; then
exec "$0" "$flakedir"
fi
flakedir="$flakedir/.."
done
echo Failed to find flake root! >&2
exit 1
fi
for f in "$@"; do
if [ -d "$f" ]; then
${fd}/bin/fd "$f" -Htf -x "$0" &
else
case "$(${coreutils}/bin/basename "$f")" in${caseArms}
esac
fi
done &>/dev/null
wait
''));
プロジェクトのルートディレクトリ(flake.nixがある場所)を探し、fdでファイルを探して、case文でマッチしたファイル毎にそれぞれフォーマッタを適用しています。なんだ、簡単じゃん。
自分で書く
formatter.${system}
これは拙作のNeovimプラグインのCIで使っているflake.nixの抜粋ですが、
formatter = final.writeShellApplication {
name = "${pname}-formatter";
runtimeInputs = with final; [
stylua
selene
nixfmt-rfc-style
];
text = ''
mapfile -t files < <(git ls-files --exclude-standard)
for file in "''${files[@]}"; do
case "''${file##*.}" in
lua)
stylua "$file" &
selene -n "$file" &
;;
nix)
nixfmt -w80 "$file" &
;;
esac
done
'';
};
こんだけ。簡単でしょ?
checks.${system}.formatting
せっかくフォーマッタを用意できたし、nix flake checkでフォーマットをチェックしたいですよね。これはどう書いたらいいんでしょ。
また先人に学んでみましょう。
blueprint
formatter.nix (lines: 37–64):
check =
pkgs.runCommand "format-check"
{
nativeBuildInputs = [
formatter
pkgs.git
];
# only check on Linux
meta.platforms = pkgs.lib.platforms.linux;
}
''
export HOME=$NIX_BUILD_TOP/home
# keep timestamps so that treefmt is able to detect mtime changes
cp --no-preserve=mode --preserve=timestamps -r ${flake} source
cd source
git init --quiet
git add .
shopt -s globstar
${pname} **/*.nix
if ! git diff --exit-code; then
echo "-------------------------------"
echo "aborting due to above changes ^"
exit 1
fi
touch $out
'';
やっていることは、
- ソースコードを一時ディレクトリにコピー
- Gitリポジトリとして初期化(
git init)し、コードをインデックスに
登録(git add) - フォーマッタを実行(
${pname} **/*.nix) - Gitで変更があるかチェック(
git diff)し、変更があればフォーマット
されてなかったってことなので、exit 1
なるほどねえ。
自分で書く
再び拙作のflake.nixの抜粋ですが、
formatting =
pkgs.runCommandLocal "check-formatting"
{
buildInputs = [
pkgs.gitMinimal
pkgs.formatter
];
}
''
cp -r --no-preserve=mode ${self} source
cd source
git init --quiet && git add .
${pkgs.formatter.name}
test $? -ne 0 && exit 1
touch $out
'';
あとは、これをchecks.${system}の下に置いてあげればOK。ちなみにアトリビュート名はformattingでなくともなんでもよいです。
おわりに
Nix flakeのformatterの書き方、及びそのchecksへの登録の仕方の一例を紹介しました。
ここではシェルスクリプトで書きましたが、そもそも1つのフォーマッタで事足りるのなら単にそのフォーマッタをformatterに登録すればよいし、シェルスクリプトでない別の言語やツールを使ってもよいし、とにかくフォーマットを実行する何かを据えてあげればよいのです。
Perlではありませんが、NixはThere's More Than One Way To Do It.なところがあり、私は好きです。
Discussion