再現性のある 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 における以下の変更が影響している可能性が高そうだった。
あくまでも意図があっての修正なので、普段からバージョンアップするたびに再現性が崩れるということではない、と願いたい。
このあたりを踏まえて 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.htmlconst 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")}`;