mirror of
https://github.com/github/spec-kit.git
synced 2026-08-03 06:26:30 +08:00
fix(bundler): order bundle members by canonical POSIX arcname (reproducible builds) (#3658)
_collect_files returned sorted(collected), i.e. pathlib.Path order, which is platform-dependent: on Windows PurePath compares case-folded with backslash separators, whereas the zip member NAMES are the canonical POSIX arcnames (build_bundle: file_path.relative_to(bundle_dir).as_posix()). So the same bundle built on Windows vs Linux/macOS produced archives whose members were laid out in different order — not byte-for-byte identical across build hosts, contradicting the packager's reproducible-build guarantee (fixed timestamps + canonical modes). Order by the same canonical POSIX-arcname key used to name members, so member order is host-independent. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -142,4 +142,10 @@ def _collect_files(
|
||||
# Skip symlinked files to avoid escaping the bundle directory.
|
||||
continue
|
||||
collected.append(path)
|
||||
return sorted(collected)
|
||||
# Order by the canonical POSIX arcname (the same key build_bundle uses to
|
||||
# NAME each member), not by pathlib.Path comparison. Path ordering is
|
||||
# platform-dependent (Windows folds case and uses backslash separators),
|
||||
# which would lay out zip members differently across build hosts and break
|
||||
# the byte-for-byte reproducible-build guarantee even though the member
|
||||
# names are identical.
|
||||
return sorted(collected, key=lambda p: p.relative_to(bundle_dir).as_posix())
|
||||
|
||||
@@ -73,6 +73,23 @@ def test_build_is_deterministic(tmp_path: Path):
|
||||
assert first.artifact_path.read_bytes() == second.artifact_path.read_bytes()
|
||||
|
||||
|
||||
def test_member_order_is_platform_independent(tmp_path: Path):
|
||||
# Members must be laid out in canonical POSIX-arcname order (the same key
|
||||
# build_bundle uses to NAME them), not pathlib.Path order — which folds case
|
||||
# on Windows and would otherwise reorder members across build hosts, breaking
|
||||
# the byte-for-byte reproducibility guarantee. Mixed-case names make the
|
||||
# difference observable: Path order on Windows groups differently than the
|
||||
# canonical string sort.
|
||||
bundle = _make_bundle(
|
||||
tmp_path / "b",
|
||||
extra_files={"Zeta.txt": "z", "apple.txt": "a", "Foo.txt": "f", "bar.txt": "b"},
|
||||
)
|
||||
result = build_bundle(bundle, output_dir=tmp_path / "out")
|
||||
with zipfile.ZipFile(result.artifact_path) as archive:
|
||||
names = archive.namelist()
|
||||
assert names == sorted(names)
|
||||
|
||||
|
||||
def test_output_dir_inside_bundle_excludes_prior_artifacts(tmp_path: Path):
|
||||
bundle = _make_bundle(tmp_path / "b", extra_files={"a.txt": "a"})
|
||||
out_dir = bundle / "dist"
|
||||
|
||||
Reference in New Issue
Block a user