diff --git a/docs/ci.md b/docs/ci.md index dbb48dad..e14e9602 100644 --- a/docs/ci.md +++ b/docs/ci.md @@ -276,12 +276,13 @@ also verified. Host builds that go through the common `make` / `cmake` / before the build tool runs. The fingerprint includes loaded modules, selected build environment variables, tool real paths, versions, and binary SHA-256 hashes. The app-owned build recipe portion of the build input hash is limited -to `programs//build.sh` and optional patch files under -`programs//patches/`; `run.sh`, `profile.sh`, `estimate.sh`, and app -documentation are not build cache inputs. Because of that boundary, -`profile.sh` and `estimate.sh` must not select build options or rebuild app -artifacts. Cache misses fall back to the normal `programs//build.sh` path -and store a fresh cache after a successful build. The generated child pipeline +to Git-tracked files under `programs//`. This is intentionally +conservative: a changed app-side file may cause an extra rebuild, but it should +not accidentally restore an old binary after app-local source or build recipe +changes. `profile.sh` and `estimate.sh` still must not select build options or +rebuild app artifacts. Cache misses fall back to the normal +`programs//build.sh` path and store a fresh cache after a successful build. +The generated child pipeline does not declare a GitLab `cache:` stanza; the cache directory must be a site-managed persistent path such as the custom runner's `CUSTOM_DIR`, not a per-job cleanup directory. @@ -289,8 +290,8 @@ per-job cleanup directory. Git source は記録済みの `ref_name` と `resolved_commit` を使って `git ls-remote` で再確認し、file/archive source は SHA-256 を再計算します。 container image hash が記録されている場合は image hash も確認します。 common の `make` / `cmake` / `ninja` wrapper を通る host build は、build tool 実行直前に収集した build environment fingerprint で照合します。この fingerprint には loaded modules、選択された build 環境変数、tool の real path、version、binary SHA-256 hash が含まれます。 -build input hash の app 側 build recipe は `programs//build.sh` と、任意のpatch file置き場である `programs//patches/` に限定します。`run.sh`、`profile.sh`、`estimate.sh`、app 文書は build cache input ではありません。 -この境界を守るため、`profile.sh` と `estimate.sh` では build option の選択や app artifact の再buildを行わないでください。 +build input hash の app 側 build recipe は `programs//` 配下の Git 管理ファイルを保守的に対象にします。app 側ファイルの変更で余分な rebuild が起きることはありますが、app-local source や build recipe の変更後に古い binary を誤って restore しないことを優先します。 +`profile.sh` と `estimate.sh` では build option の選択や app artifact の再buildを行わないでください。 cache miss の場合は通常の `programs//build.sh` 経路に戻り、成功後に新しい cache を保存します。 生成された child pipeline は GitLab の `cache:` stanza を出しません。cache directory は job ごとの cleanup 対象ではなく、custom runner の `CUSTOM_DIR` など site 側で管理する永続パスにしてください。 diff --git a/result_server/tests/test_result_detail_template.py b/result_server/tests/test_result_detail_template.py index b385621f..6962c8f6 100644 --- a/result_server/tests/test_result_detail_template.py +++ b/result_server/tests/test_result_detail_template.py @@ -173,11 +173,11 @@ def test_meta_info_section(self, app): assert "Cached Binary Created At" in html assert "2026-09-04T10:20:30Z" in html assert "Host Environment Fingerprint" in html - assert "host build environment is the same" in html - assert "build recipe inputs are the same" in html - assert "source metadata is the same" in html + assert "Matched current host build environment" in html + assert "Git-tracked files under programs/<code>/" in html + assert "Matched current source metadata" in html assert "Cached Artifacts Digest" in html - assert "restored build outputs are the same" in html + assert "Matched restored build outputs" in html assert "Hit Basis" in html assert "build inputs hash matched" in html assert "rccs-cloud" in html @@ -408,7 +408,12 @@ def test_build_cache_miss_shows_rejected_reason(self, app): "reason": "stored cache after build", "entry": { "created_at": "2026-09-05T01:02:03Z", - "digests": {"build_inputs": "sha256:new"}, + "host_environment_fingerprint": "sha256:host-new", + "digests": { + "build_inputs": "sha256:new", + "source_info": "sha256:source-new", + "artifacts": "sha256:artifacts-new", + }, }, "store_basis": ["build inputs hash recorded"], "restore": { @@ -416,7 +421,12 @@ def test_build_cache_miss_shows_rejected_reason(self, app): "reason": "build inputs changed: cached old, current new", "rejected_entry": { "created_at": "2026-09-04T01:02:03Z", - "digests": {"build_inputs": "sha256:old"}, + "host_environment_fingerprint": "sha256:host-old", + "digests": { + "build_inputs": "sha256:old", + "source_info": "sha256:source-old", + "artifacts": "sha256:artifacts-old", + }, }, }, }, @@ -431,6 +441,15 @@ def test_build_cache_miss_shows_rejected_reason(self, app): assert "build inputs changed: cached old, current new" in html assert "Rejected Cached Binary Created At" in html assert "2026-09-04T01:02:03Z" in html + assert "Recorded the host build environment" in html + assert "Recorded build recipe inputs" in html + assert "Recorded source_info.env" in html + assert "Recorded cached artifacts" in html + assert "Rejected Host Environment Fingerprint" in html + assert "Rejected Build Inputs Hash" in html + assert "Rejected candidate build inputs" in html + assert "Rejected candidate source metadata" in html + assert "Rejected candidate artifact digest" in html def test_quality_section(self, app): with app.test_request_context(): diff --git a/result_server/utils/evidence_packet.py b/result_server/utils/evidence_packet.py index 6faa2fba..4ce3e975 100644 --- a/result_server/utils/evidence_packet.py +++ b/result_server/utils/evidence_packet.py @@ -8,8 +8,8 @@ from urllib.parse import urlsplit from utils.result_detail_view import ( - BUILD_CACHE_DIGEST_HELP, - HOST_ENVIRONMENT_FINGERPRINT_HELP, + build_cache_digest_help, + build_cache_host_environment_help, ) from utils.result_records import format_numeric_value, summarize_input_info from utils.trigger_display import summarize_execution_trigger @@ -324,6 +324,9 @@ def _build_cache_rows(build_cache: Any) -> list[tuple[str, Any]]: entry = build_cache.get("entry") entry = entry if isinstance(entry, dict) else {} if entry: + context = ( + "matched" if str(build_cache.get("status") or "") == "hit" else "recorded" + ) rows.extend([ ("Cached binary created at", entry.get("created_at")), ("Source", _format_cache_source(entry.get("source"))), @@ -331,7 +334,7 @@ def _build_cache_rows(build_cache: Any) -> list[tuple[str, Any]]: "Host environment fingerprint", _digest_with_help( entry.get("host_environment_fingerprint"), - HOST_ENVIRONMENT_FINGERPRINT_HELP, + build_cache_host_environment_help(context), ), ), ]) @@ -340,15 +343,24 @@ def _build_cache_rows(build_cache: Any) -> list[tuple[str, Any]]: rows.extend([ ( "Build inputs hash", - _digest_with_help(digests.get("build_inputs"), BUILD_CACHE_DIGEST_HELP["build_inputs"]), + _digest_with_help( + digests.get("build_inputs"), + build_cache_digest_help("build_inputs", context), + ), ), ( "Source info digest", - _digest_with_help(digests.get("source_info"), BUILD_CACHE_DIGEST_HELP["source_info"]), + _digest_with_help( + digests.get("source_info"), + build_cache_digest_help("source_info", context), + ), ), ( "Cached artifacts digest", - _digest_with_help(digests.get("artifacts"), BUILD_CACHE_DIGEST_HELP["artifacts"]), + _digest_with_help( + digests.get("artifacts"), + build_cache_digest_help("artifacts", context), + ), ), ]) if build_cache.get("hit_basis"): diff --git a/result_server/utils/result_detail_view.py b/result_server/utils/result_detail_view.py index cc7ac8ac..1bd52c9b 100644 --- a/result_server/utils/result_detail_view.py +++ b/result_server/utils/result_detail_view.py @@ -7,31 +7,91 @@ from utils.trigger_display import summarize_execution_trigger -HOST_ENVIRONMENT_FINGERPRINT_HELP = ( - "Checks whether the host build environment is the same. Covers code, " - "system, loaded modules, selected build environment, and build tool real " - "paths, versions, and binary hashes." +HOST_ENVIRONMENT_FINGERPRINT_COVERAGE = ( + "Covers code, system, loaded modules, selected build environment, and build " + "tool real paths, versions, and binary hashes." ) -BUILD_CACHE_DIGEST_HELP = { - "build_inputs": ( - "Checks whether the build recipe inputs are the same. Covers app " - "build.sh, app patches, build-cache wrapper, build-tool wrappers, " - "environment snapshot helper, matrix generator, and any declared extra " - "cache inputs." +HOST_ENVIRONMENT_FINGERPRINT_HELP = { + "matched": ( + "Matched current host build environment against the restored cache " + f"entry. {HOST_ENVIRONMENT_FINGERPRINT_COVERAGE}" ), - "source_info": ( - "Checks whether the source metadata is the same. Uses source_info.env " - "saved with the cache entry, including source type and resolved source " - "identity." + "recorded": ( + "Recorded the host build environment for the newly stored cache entry. " + f"{HOST_ENVIRONMENT_FINGERPRINT_COVERAGE}" ), - "artifacts": ( - "Checks whether the restored build outputs are the same as the saved " - "cache entry. Covers artifacts/ relative paths, entry types, file " - "modes, file contents, and symlink targets." + "rejected": ( + "Recorded host build environment from a rejected cache candidate, shown " + f"for diagnosis. {HOST_ENVIRONMENT_FINGERPRINT_COVERAGE}" ), } +BUILD_CACHE_BUILD_INPUTS_COVERAGE = ( + "Covers Git-tracked files under programs//, the build-cache wrapper, " + "build-tool wrappers, environment snapshot helper, matrix generator, and " + "any site-provided extra cache inputs." +) + +BUILD_CACHE_DIGEST_HELP = { + "matched": { + "build_inputs": ( + "Matched current build recipe inputs against the restored cache " + f"entry. {BUILD_CACHE_BUILD_INPUTS_COVERAGE}" + ), + "source_info": ( + "Matched current source metadata against source_info.env saved with " + "the cache entry, including source type and resolved source identity." + ), + "artifacts": ( + "Matched restored build outputs against the saved cache entry before " + "and after restore. Covers artifacts/ relative paths, entry types, " + "file modes, file contents, and symlink targets." + ), + }, + "recorded": { + "build_inputs": ( + "Recorded build recipe inputs for the newly stored cache entry. " + f"{BUILD_CACHE_BUILD_INPUTS_COVERAGE}" + ), + "source_info": ( + "Recorded source_info.env for the newly stored cache entry, including " + "source type and resolved source identity." + ), + "artifacts": ( + "Recorded cached artifacts for the newly stored cache entry. Covers " + "artifacts/ relative paths, entry types, file modes, file contents, " + "and symlink targets." + ), + }, + "rejected": { + "build_inputs": ( + "Rejected candidate build inputs. The candidate was not restored; " + "compare this value with the rejection reason." + ), + "source_info": ( + "Rejected candidate source metadata. The candidate was not restored; " + "compare this value with the rejection reason." + ), + "artifacts": ( + "Rejected candidate artifact digest. The candidate was not restored; " + "this value describes the skipped cache entry." + ), + }, +} + + +def build_cache_host_environment_help(context="matched"): + return HOST_ENVIRONMENT_FINGERPRINT_HELP.get( + context, HOST_ENVIRONMENT_FINGERPRINT_HELP["matched"] + ) + + +def build_cache_digest_help(key, context="matched"): + return BUILD_CACHE_DIGEST_HELP.get(context, BUILD_CACHE_DIGEST_HELP["matched"]).get( + key, "" + ) + def build_result_detail_context( result, @@ -335,9 +395,10 @@ def _build_build_cache_rows(build_cache): if reason: rows.append({"label": "Reason", "value": reason}) + entry_context = "matched" if status == "hit" else "recorded" entry = build_cache.get("entry") entry = entry if isinstance(entry, dict) else {} - _append_cache_entry_rows(rows, entry, prefix="") + _append_cache_entry_rows(rows, entry, prefix="", context=entry_context) if status == "hit": hit_basis = build_cache.get("hit_basis") or [] @@ -355,12 +416,12 @@ def _build_build_cache_rows(build_cache): rows.append({"label": "Rejected Cache Reason", "value": restore_reason}) rejected_entry = restore.get("rejected_entry") rejected_entry = rejected_entry if isinstance(rejected_entry, dict) else {} - _append_cache_entry_rows(rows, rejected_entry, prefix="Rejected ") + _append_cache_entry_rows(rows, rejected_entry, prefix="Rejected ", context="rejected") return rows -def _append_cache_entry_rows(rows, entry, *, prefix): +def _append_cache_entry_rows(rows, entry, *, prefix, context): if not entry: return @@ -383,7 +444,7 @@ def _append_cache_entry_rows(rows, entry, *, prefix): rows.append({ "label": f"{prefix}Host Environment Fingerprint", "value": host_fingerprint, - "help": HOST_ENVIRONMENT_FINGERPRINT_HELP, + "help": build_cache_host_environment_help(context), }) elif entry.get("env_key_present") is True: rows.append({"label": f"{prefix}Host Environment", "value": "environment key matched"}) @@ -401,7 +462,7 @@ def _append_cache_entry_rows(rows, entry, *, prefix): rows.append({ "label": f"{prefix}{label}", "value": value, - "help": BUILD_CACHE_DIGEST_HELP[key], + "help": build_cache_digest_help(key, context), }) diff --git a/scripts/build_with_cache.sh b/scripts/build_with_cache.sh index 37e40a6c..35800173 100755 --- a/scripts/build_with_cache.sh +++ b/scripts/build_with_cache.sh @@ -250,8 +250,7 @@ build_inputs_hash() { hash_list=$(mktemp) if git -C "$repo_root" rev-parse --is-inside-work-tree >/dev/null 2>&1; then git -C "$repo_root" ls-files \ - "programs/${code}/build.sh" \ - "programs/${code}/patches" \ + "programs/${code}" \ "scripts/bk_functions.sh" \ "scripts/build_tool_wrappers" \ "scripts/build_with_cache.sh" \ @@ -261,12 +260,11 @@ build_inputs_hash() { else find "${repo_root}/scripts/build_tool_wrappers" \ -type f -print > "$file_list" 2>/dev/null || true - if [ -d "${repo_root}/programs/${code}/patches" ]; then - find "${repo_root}/programs/${code}/patches" \ + if [ -d "${repo_root}/programs/${code}" ]; then + find "${repo_root}/programs/${code}" \ -type f -print >> "$file_list" 2>/dev/null || true fi printf '%s\n' \ - "programs/${code}/build.sh" \ "scripts/bk_functions.sh" \ "scripts/build_with_cache.sh" \ "scripts/collect_environment_snapshot.sh" \ diff --git a/scripts/tests/test_build_cache.sh b/scripts/tests/test_build_cache.sh index 28182264..5c39c28b 100755 --- a/scripts/tests/test_build_cache.sh +++ b/scripts/tests/test_build_cache.sh @@ -366,7 +366,9 @@ printf '#!/bin/bash\n' > "${TMP_DIR}/git-project/programs/app/run.sh" printf '#!/bin/bash\n' > "${TMP_DIR}/git-project/programs/app/profile.sh" printf '# test app\n' > "${TMP_DIR}/git-project/programs/app/README.md" mkdir -p "${TMP_DIR}/git-project/programs/app/patches" +mkdir -p "${TMP_DIR}/git-project/programs/app/src" printf 'patch-v1\n' > "${TMP_DIR}/git-project/programs/app/patches/build.patch" +printf 'local-source-v1\n' > "${TMP_DIR}/git-project/programs/app/src/local-source.txt" pushd "${TMP_DIR}/git-project" >/dev/null git init --initial-branch=main >/dev/null git config user.email "benchkit@example.invalid" @@ -392,18 +394,26 @@ run_git_project_build_with_cache test "$(cat "${TMP_DIR}/git-build-count")" = "1" grep -q '^BK_BUILD_CACHE_STORED=true$' "${TMP_DIR}/git-project/results/build_cache.env" +printf 'local-source-v2\n' >> "${TMP_DIR}/git-project/programs/app/src/local-source.txt" +rm -rf "${TMP_DIR}/git-project/artifacts" "${TMP_DIR}/git-project/results" "${TMP_DIR}/git-project/appsrc" +run_git_project_build_with_cache +test "$(cat "${TMP_DIR}/git-build-count")" = "2" +grep -q '^BK_BUILD_CACHE_STORED=true$' "${TMP_DIR}/git-project/results/build_cache.env" +grep -q '^BK_BUILD_CACHE_RESTORE_STATUS=miss$' "${TMP_DIR}/git-project/results/build_cache.env" + printf '# run-only change\n' >> "${TMP_DIR}/git-project/programs/app/run.sh" printf '# profile-only change\n' >> "${TMP_DIR}/git-project/programs/app/profile.sh" printf '# docs-only change\n' >> "${TMP_DIR}/git-project/programs/app/README.md" rm -rf "${TMP_DIR}/git-project/artifacts" "${TMP_DIR}/git-project/results" "${TMP_DIR}/git-project/appsrc" run_git_project_build_with_cache -test "$(cat "${TMP_DIR}/git-build-count")" = "1" -grep -q '^BK_BUILD_CACHE_STATUS=hit$' "${TMP_DIR}/git-project/results/build_cache.env" +test "$(cat "${TMP_DIR}/git-build-count")" = "3" +grep -q '^BK_BUILD_CACHE_STORED=true$' "${TMP_DIR}/git-project/results/build_cache.env" +grep -q '^BK_BUILD_CACHE_RESTORE_STATUS=miss$' "${TMP_DIR}/git-project/results/build_cache.env" printf 'patch-v2\n' >> "${TMP_DIR}/git-project/programs/app/patches/build.patch" rm -rf "${TMP_DIR}/git-project/artifacts" "${TMP_DIR}/git-project/results" "${TMP_DIR}/git-project/appsrc" run_git_project_build_with_cache -test "$(cat "${TMP_DIR}/git-build-count")" = "2" +test "$(cat "${TMP_DIR}/git-build-count")" = "4" grep -q '^BK_BUILD_CACHE_STORED=true$' "${TMP_DIR}/git-project/results/build_cache.env" grep -q '^BK_BUILD_CACHE_RESTORE_STATUS=miss$' "${TMP_DIR}/git-project/results/build_cache.env" grep -q '^BK_BUILD_CACHE_REJECTED_CREATED_AT=' "${TMP_DIR}/git-project/results/build_cache.env" @@ -421,7 +431,7 @@ esac printf '\n# git build input change\n' >> "${TMP_DIR}/git-project/programs/app/build.sh" rm -rf "${TMP_DIR}/git-project/artifacts" "${TMP_DIR}/git-project/results" "${TMP_DIR}/git-project/appsrc" run_git_project_build_with_cache -test "$(cat "${TMP_DIR}/git-build-count")" = "3" +test "$(cat "${TMP_DIR}/git-build-count")" = "5" grep -q '^BK_BUILD_CACHE_STORED=true$' "${TMP_DIR}/git-project/results/build_cache.env" rm -rf "${TMP_DIR}/project/artifacts" "${TMP_DIR}/project/results" "${TMP_DIR}/project/tagsrc"