diff --git a/edge0-infer/tools/assets/asset_common.py b/edge0-infer/tools/assets/asset_common.py index a9553bcd..d2b372ab 100644 --- a/edge0-infer/tools/assets/asset_common.py +++ b/edge0-infer/tools/assets/asset_common.py @@ -33,12 +33,13 @@ def snapshot(root: Path) -> dict[str, str]: return {str(p.relative_to(root)): sha256(p) for p in sorted(root.rglob('*')) if p.is_file()} -def verify_source(source: Path, lock: dict, names=None) -> None: +def verify_source(source: Path, lock: dict, names=None, *, digests: dict[str, str] | None = None) -> None: for name in names if names is not None else lock['files']: path = source / name if not path.is_file(): raise ValueError(f'missing source file: {name}') - if sha256(path) != lock['files'][name]: + digest = sha256(path) if digests is None else digests.get(name) + if digest != lock['files'][name]: raise ValueError(f'source digest mismatch: {name}') diff --git a/edge0-infer/tools/assets/edge0_35b/make_assets.py b/edge0-infer/tools/assets/edge0_35b/make_assets.py index 9f9c26c8..393b32c7 100644 --- a/edge0-infer/tools/assets/edge0_35b/make_assets.py +++ b/edge0-infer/tools/assets/edge0_35b/make_assets.py @@ -53,8 +53,8 @@ def make_assets(source: Path, out: Path, lock: dict | None = None, converter: Pa if partial.exists() or partial.is_symlink(): raise ValueError(f'an earlier run left {partial}; delete it and rerun') lock = lock if lock is not None else read_json(Path(__file__).with_name('source.lock.json')) - verify_source(source, lock) before = snapshot(source) + verify_source(source, lock, digests=before) partial.mkdir(parents=True, exist_ok=False) try: _convert(source, out, partial, lock, converter, before) diff --git a/edge0-infer/tools/assets/tests/test_make_assets.py b/edge0-infer/tools/assets/tests/test_make_assets.py index 1faca3e9..f105ccb1 100644 --- a/edge0-infer/tools/assets/tests/test_make_assets.py +++ b/edge0-infer/tools/assets/tests/test_make_assets.py @@ -167,3 +167,26 @@ def test_repository_converter_is_recorded_relative(tmp_path): partial = tmp_path / 'out.partial' assert maker.recorded_path(maker.CONVERTER, partial) == 'tools/assets/edge0_35b/edge0_convert/convert_mlx_to_gguf.py' assert maker.recorded_path(partial / 'staging/edge0-35b', partial) == '/staging/edge0-35b' + + +def test_source_snapshot_is_reused_for_lock_verification(setup, monkeypatch): + import asset_common + from collections import Counter + + source, out, lock, converter, _ = setup + original = asset_common.sha256 + reads = Counter() + + def counted(path): + path = Path(path) + if path.parent == source: + reads[path.name] += 1 + return original(path) + + monkeypatch.setattr(asset_common, 'sha256', counted) + with pytest.raises(maker.MakeError): + maker.make_assets(source, out, lock, converter) + # One digest supplies both the initial snapshot and lock verification; + # the finally block independently verifies that the converter changed nothing. + assert dict(reads) == {name: 2 for name in lock['files']} + assert not out.exists()