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:
Ali jawwad
2026-07-23 17:03:29 +05:00
committed by GitHub
parent 38eb2fcc4b
commit 370551ea89
2 changed files with 24 additions and 1 deletions

View File

@@ -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())

View File

@@ -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"