monolithic kernel

再現性のある tar アーカイブ作成

何もオプションを指定せずに tar コマンドでアーカイブを作成すると、同じファイルであってもアーカイブのハッシュ値が変化する。これは、ファイルのタイムスタンプや実行時の PID など、ファイルの内容以外に意図せず変化しうる要素がアーカイブに含まれていることに起因する。

同じ手順で作成したアーカイブが確実に同じハッシュ値を持つように再現性があれば、ハッシュ値によってアーカイブの正しさを確認できるようになる。

これ自体はそんなに変わった話ではなくて、セキュリティのためなどアーカイブの正しさを担保できる必要のある場面においてはよく行われている。GNU tar のドキュメントで手順が紹介されている (オプション一発でできるようにしてくれという気持ちにはなる)。

ただ、これはあくまでも GNU tar についてのものである。Windows や macOS に付属する tar は bsdtar なので同じ方法は使えない (できるかもしれないけどそこまで深追いしていない)。

今回は Linux、Windows、macOS で同じコマンドを動作させるため、すべての環境で GNU tar を使うことにした。

まず、macOS では Homebrew で gnu-tar をインストールすればよい。gtar としてインストールされるので、必要に応じてシェルでエイリアスを設定するなどする。

brew install gnu-tar

Windows では scoop を使っていれば tar をインストールするだけかと思いきや、どうも古いバージョンで止まっているらしくてオプションが一部使えなかった。git に付いてくる tar は新しいようだったので、そちらを使えばよいだろう。

scoop install git

Linux でも Windows や Mac 上のバージョン 1.35 (試した時点での最新バージョン) と結果を一致させるためには、Ubuntu 20.04 に入っていたバージョン 1.30 ではダメで、バージョン 1.35 で揃える必要があった。

brew install gnu-tar

出力される tar ファイルの差分を眺めてみたところ、差分の位置的にはバージョン 1.35 における以下の変更が影響している可能性が高そうだった。

Leave the devmajor and devminor fields empty (rather than zero) for non-special files, as this is more compatible with traditional tar.

あくまでも意図があっての修正なので、普段からバージョンアップするたびに再現性が崩れるということではない、と願いたい。

このあたりを踏まえて Deno でコードにすると以下のようになる。

import $ from "jsr:@david/dax@^0.41.0";
import * as datetime from "jsr:@std/datetime@^0.221.0";
import * as path from "jsr:@std/path@^0.221.0";

const tar = (() => {
  if (Deno.build.os === "windows") {
    return path.join(
      Deno.env.get("USERPROFILE")!,
      "scoop",
      "apps",
      "git",
      "current",
      "usr",
      "bin",
      "tar.exe",
    ).replace(/\\/g, "/"); // Workaround for dax: https://github.com/dsherret/dax/issues/273
  }
  if (Deno.build.os === "darwin") {
    return "gtar";
  }
  return "tar";
})();

const mtime = new Date(...);
// https://www.gnu.org/software/tar/manual/html_node/Reproducibility.html
const options = [
  "--sort=name",
  "--format=posix",
  "--pax-option=exthdr.name=%d/PaxHeaders/%f",
  "--pax-option=delete=atime,delete=ctime",
  `--mtime=${datetime.format(mtime, "yyyy-MM-ddTHH:mm:ssZ", { utc: true })}`,
  "--numeric-owner",
  "--owner=0",
  "--group=0",
  "--mode=go-rwx,u+rw-x",
];
const dir = "/path/to/dir";
const files = [
  "file1",
  "file2",
];
files.sort();

await $`${tar} -cf - ${options} -C ${dir} ${files} > ${$.path("/path/to/archive")}`;

Related articles