ビルドの再現性とキャッシュ
成果物の差を入力・キャッシュ・包装時の情報へさかのぼり、再現に必要な条件を説明します。
説明
同じソースから同じ成果物を得るには、ソース本文だけでなく、依存する処理、設定、ツール、アーカイブの時刻情報なども条件に含める必要があります。キャッシュのキーから入力が漏れると、成功したように見えて古い出力を再利用する場合があります。
短い見出しをHTMLへ変換するPythonのビルドを使います。依存ファイルのハッシュ確認、入力不足のキャッシュ、依存と設定を含めたキャッシュを順に試します。最後は同じHTMLをZIPへ入れ、時刻の違いがアーカイブのバイト列を変えることを確認します。
成果物の差を入力・キャッシュ・包装時の情報へさかのぼり、再現に必要な条件を説明します。
1ソース・設定・依存の固定
見出し本文はhello、表示の接頭辞はLessonです。このビルドのdeps.lock.jsonは、依存ファイルのパスと内容のSHA-256を記録したものです。
cat src/title.txt見出し本文を読む
cat config.jsonビルド設定を読む
cat deps.lock.json固定する依存ファイルの識別子を読む依存はvendor/formatter.pyです。ロック記録を置くだけでなく、ビルド時に内容のハッシュと照合します。この演習のロック形式は、この小さなビルド専用です。
2ソースだけをキーにしたキャッシュ
まず不十分なキャッシュ方式でビルドします。source方式はsrc/title.txtだけからキーを作ります。
python3 build.py --cache sourceソース本文だけをキャッシュキーに使う
cat dist/index.html生成したHTMLの内容を読む初回はmissでHTMLを生成します。表示したSHA-256は成果物そのもののバイト列から求めた値です。キーが一致することと、成果物が要求どおり新しいことは別に確認します。
3同じ入力なら再利用する
入力を変えずにもう一度ビルドします。
python3 build.py --cache source同じキーで再ビルドするhitになり、成果物のSHA-256も同じです。この場合は入力が同じなので、再利用した内容と生成すべき内容が一致しています。
4依存の変更をハッシュ照合で検出
formatterを大文字化する版へ替えます。ロック記録はまだ元の内容を指しています。
cp formatter-v2.py vendor/formatter.py依存処理を変更する
python3 build.py --cache source固定したハッシュと照合するdependency hash mismatchで停止します。依存の内容が記録と違うため、キャッシュの有無を調べる前に拒否されました。
5依存を更新しても古いキャッシュが返る
新しい依存に対応するロック記録へ更新します。本文hello自体は変えていません。
cp deps-v2.lock.json deps.lock.json更新した依存をロック記録へ反映する
python3 build.py --cache source本文しか見ないキーで再利用する
cat dist/index.html依存は大文字化する版ですが、古いhelloのHTMLがhitで返ります。依存のハッシュ検証は通っても、キャッシュキーに依存の変更を含めなかったため、出力が古いままです。
6キャッシュを使わず結果を比較
現在の入力から作り直し、内容を比較します。
python3 build.py --no-cache保存済みの出力を使わず生成する
cat dist/index.html現在の依存で生成した内容を読む今度はHELLOです。成果物のSHA-256も変わります。依存を更新した後に本来得られる出力と、キャッシュが返した出力の不一致を確認できました。
7依存・設定・ツールを含むキーへ
full方式は本文、設定、依存とロック、ビルドスクリプト、Pythonの版をキーに含めます。続けて2回実行します。
python3 build.py --cache fullこのビルドが使う入力全体でキーを作る
python3 build.py --cache full初回miss、2回目hitで同じ成果物になります。必要な入力を含めたキーによって、現在の条件に対応する出力を再利用できます。別の環境でも再現するには、実際に出力へ影響する他の条件も調べる必要があります。
8設定変更でも再生成する
接頭辞をTopicへ変更します。本文と依存ファイルは同じです。
cp config-v2.json config.json表示の接頭辞を変更する
python3 build.py --cache full設定変更を含めたキーでビルドする
cat dist/index.html設定もキーに含むのでmissになり、Topic: HELLOへ変わります。ソースコードだけでなく、設定値も成果物を変える入力です。
9ZIPの時刻だけでバイト列が変わる
同じHTMLを、ZIP内の時刻だけ変えて二つのアーカイブにします。
python3 package.py --year 2025 --output a.zip2025年の時刻情報で格納する
python3 package.py --year 2026 --output b.zip2026年の時刻情報で格納する
python3 compare.py本文・時刻・全バイト列を比較する展開した本文は同じですが、アーカイブ全体は一致しません。ZIPには時刻などのメタデータも保存されるため、中身のHTMLが同じでもファイル全体のハッシュは変わります。
10アーカイブの作成条件もそろえる
a.zipと同じ時刻・名前・属性でc.zipを作ります。
python3 package.py --year 2025 --output c.zip同じ包装条件で再作成する
python3 -c "from pathlib import Path; print(Path('a.zip').read_bytes()==Path('c.zip').read_bytes()二つのアーカイブ全体を比較する)"Trueになり、a.zipとc.zipのSHA-256も一致します。入力の固定だけでなく、成果物へ含めるメタデータをそろえることも再現可能なビルドの条件です。
まとめ
依存のハッシュ確認とキャッシュの正しさは別です。キーから依存が漏れると古い出力を返します。
キャッシュキーには、このビルドのソース・設定・依存・スクリプト・ツール版など、出力を変える入力を含めます。
同じ本文でもZIPの時刻が異なるとファイル全体は変わります。包装条件も固定して再現性を確認します。
操作履歴と解説を振り返れます。
シェル / Python 3
cat src/title.txt
cat config.json
cat deps.lock.json hello
{"prefix":"Lesson: "}
{
"path": "vendor/formatter.py",
"sha256": "9d57f8f90a514713754ce616007c19c5f27f014203dc6c49dd803ba9743f3498"
}python3 build.py --cache source
cat dist/index.htmlcache=miss mode=source artifact_sha256=5abb02a5c64b0c55590b225c2dbe3ffca1b5150a770a4749b894ecf494401390 <!doctype html><html lang="en"><title>Lesson: hello</title><h1>Lesson: hello</h1></html>
python3 build.py --cache sourcecache=hit mode=source artifact_sha256=5abb02a5c64b0c55590b225c2dbe3ffca1b5150a770a4749b894ecf494401390
cp formatter-v2.py vendor/formatter.py
python3 build.py --cache sourcedependency hash mismatch: vendor/formatter.py
cp deps-v2.lock.json deps.lock.json
python3 build.py --cache source
cat dist/index.htmlcache=hit mode=source artifact_sha256=5abb02a5c64b0c55590b225c2dbe3ffca1b5150a770a4749b894ecf494401390 <!doctype html><html lang="en"><title>Lesson: hello</title><h1>Lesson: hello</h1></html>
python3 build.py --no-cache
cat dist/index.htmlcache=miss mode=disabled artifact_sha256=03d694e5e73ccb3333df413a0ec6b5ab32d82b3c389e4c856d55c83d798a979a <!doctype html><html lang="en"><title>Lesson: HELLO</title><h1>Lesson: HELLO</h1></html>
python3 build.py --cache full
python3 build.py --cache fullcache=miss mode=full artifact_sha256=03d694e5e73ccb3333df413a0ec6b5ab32d82b3c389e4c856d55c83d798a979a cache=hit mode=full artifact_sha256=03d694e5e73ccb3333df413a0ec6b5ab32d82b3c389e4c856d55c83d798a979a
cp config-v2.json config.json
python3 build.py --cache full
cat dist/index.htmlcache=miss mode=full artifact_sha256=998fb1e334cce4204b3d2f514fa28415b6c3cd66e38ab529ebb0c3998a7d2403 <!doctype html><html lang="en"><title>Topic: HELLO</title><h1>Topic: HELLO</h1></html>
python3 package.py --year 2025 --output a.zip
python3 package.py --year 2026 --output b.zip
python3 compare.pya.zip sha256=d74a6a92f241771131d77bbe306eb41360c0e551821a6586e273bb9ea83896fc b.zip sha256=da7ddee7462eef7b5cc8b42a9f39c68b42a417477391e549f53e97a343bf8306 payload_equal=True timestamps=(2025, 1, 1, 0, 0, 0),(2026, 1, 1, 0, 0, 0) archive_equal=False
python3 package.py --year 2025 --output c.zip
python3 -c "from pathlib import Path; print(Path('a.zip').read_bytes()==Path('c.zip').read_bytes())"c.zip sha256=d74a6a92f241771131d77bbe306eb41360c0e551821a6586e273bb9ea83896fc True
演習完了