Skip to content

Commit 8b41ea2

Browse files
donislawdevclaude
andcommitted
packaging: one name to Windows is one file, and the tagged tree goes whatever happens
Outside review of #147. Two archives holding LICENSE and License with different bytes put one over the other on a Windows disk without a word, because the names were compared as written - they are compared folded to one case now, and refused like any two copies that differ. The tree of the tag is exported for the check before the card and again for the installer, and removed after each whatever happened in between, so a refusal no longer leaves it beside the release's files. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 parent e9643eb commit 8b41ea2

3 files changed

Lines changed: 55 additions & 13 deletions

File tree

‎.github/scripts/build_msi.py‎

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,11 @@ def unpack(archives, version, into):
155155
A file both archives hold goes in once, and only when both hold the same
156156
bytes - the three documents, today. Two different copies of one file are a
157157
question about how the release was built, and the installer does not pick one.
158+
159+
One file means one name to Windows, where the installer puts it: LICENSE
160+
and License are the same file there, so the names are compared folded to
161+
one case. Compared as written, the second would have overwritten the first
162+
without a word (outside review of #147).
158163
"""
159164
came_from = {}
160165
for package in packages.PACKAGES:
@@ -173,23 +178,25 @@ def unpack(archives, version, into):
173178
"unpacked into. That is not an archive the release built"
174179
% (name, entry.filename))
175180
target = os.path.join(into, *entry.filename.split("/"))
176-
if entry.filename in came_from:
177-
with open(target, "rb") as held:
181+
key = entry.filename.casefold()
182+
if key in came_from:
183+
first, held_at, held_as = came_from[key]
184+
with open(held_at, "rb") as held:
178185
if held.read() != archive.read(entry):
179-
refuse("%s and %s both hold %s, and not the same bytes. One "
180-
"installer carries one copy - look at how the release "
181-
"built the two archives"
182-
% (came_from[entry.filename], name, entry.filename))
186+
refuse("%s holds %s and %s holds %s, one file on Windows, and "
187+
"not the same bytes. One installer carries one copy - "
188+
"look at how the release built the two archives"
189+
% (first, held_as, name, entry.filename))
183190
continue
184-
came_from[entry.filename] = name
191+
came_from[key] = (name, target, entry.filename)
185192
os.makedirs(os.path.dirname(target), exist_ok=True)
186193
with archive.open(entry) as src, open(target, "wb") as dst:
187194
shutil.copyfileobj(src, dst)
188195
except zipfile.BadZipFile as err:
189196
refuse("%s is not a zip archive it can read (%s). Download it again" % (path, err))
190197
for package in packages.PACKAGES:
191198
program = package.program + ".exe"
192-
if program not in came_from:
199+
if program.casefold() not in came_from:
193200
refuse("the archives hold no %s at the top, and the installer puts it on PATH. "
194201
"That is not the shape the release builds" % program)
195202
return came_from

‎.github/scripts/sign_release.py‎

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@
5151
presses the button.
5252
"""
5353
import argparse
54+
import contextlib
5455
import datetime
5556
import hashlib
5657
import io
@@ -515,6 +516,23 @@ def export_tree(tag, into):
515516
print(" the tree of %s: %d file(s) in %s" % (tag, len(files_under(into)), into))
516517

517518

519+
@contextlib.contextmanager
520+
def tagged_tree(tag, into):
521+
"""The tree of the tag for as long as it is needed, and gone afterwards.
522+
523+
Gone whatever happened inside: a refusal anywhere between the check
524+
before the card and the installer left the export behind until the next
525+
run cleared it (outside review of #147). Exported twice rather than kept
526+
between the two, because the steps in between are the card and the Mac,
527+
and either can stop the run.
528+
"""
529+
export_tree(tag, into)
530+
try:
531+
yield into
532+
finally:
533+
shutil.rmtree(into, ignore_errors=True)
534+
535+
518536
def installer_script(tree):
519537
"""build_msi.py as the tag has it, or a refusal for a tag from before it."""
520538
script = os.path.join(tree, ".github", "scripts", "build_msi.py")
@@ -709,8 +727,8 @@ def main(argv=None):
709727
if is_candidate(args.tag):
710728
print(" %s is a release candidate, so it gets no installer" % args.tag)
711729
else:
712-
export_tree(args.tag, tree)
713-
check_installer(args.tag, tree)
730+
with tagged_tree(args.tag, tree):
731+
check_installer(args.tag, tree)
714732

715733
print("\n[1/9] fetching the build for %s" % args.tag)
716734
fetch_build(args.tag, work)
@@ -733,8 +751,8 @@ def main(argv=None):
733751
if is_candidate(args.tag):
734752
print(" none for a release candidate")
735753
else:
736-
build_installer(args.tag, tree, work, thumbprint, pin, signtool, args.dry_run)
737-
shutil.rmtree(tree)
754+
with tagged_tree(args.tag, tree):
755+
build_installer(args.tag, tree, work, thumbprint, pin, signtool, args.dry_run)
738756

739757
print("\n[7/9] checksums over what will be published")
740758
name_for_publication(work, args.tag)

‎internal/guard/msi_test.go‎

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -347,7 +347,14 @@ func TestTheInstallerIsBuiltFromBothArchivesOrNotAtAll(t *testing.T) {
347347
"dotnet tool install --global wix --version 5.0.2"},
348348
{"a document differs between the two archives", func(_ *testing.T, a archives, _, _ string) {
349349
a[window]["LICENSE"] = "another licence"
350-
}, "both hold LICENSE, and not the same bytes"},
350+
}, "holds LICENSE, one file on Windows, and not the same bytes"},
351+
// One file to Windows, where the installer puts it. Compared as
352+
// written, the second name overwrote the first on a Windows disk
353+
// and nothing was said (outside review of #147).
354+
{"a document differs and its name only in letter case", func(_ *testing.T, a archives, _, _ string) {
355+
delete(a[cli], "LICENSE")
356+
a[cli]["License"] = "another licence"
357+
}, "holds License, one file on Windows, and not the same bytes"},
351358
{"the command line archive is missing", func(_ *testing.T, a archives, _, _ string) {
352359
delete(a, cli)
353360
}, "holds no " + cli},
@@ -489,6 +496,14 @@ try:
489496
print("missing tag: EXPORTED")
490497
except SystemExit as refusal:
491498
print("missing tag: " + ("REFUSED" if "git fetch --tags" in str(refusal) else str(refusal)))
499+
kept = os.path.join(base, "kept")
500+
try:
501+
with sr.tagged_tree("HEAD", kept):
502+
print("inside: %s" % os.path.isfile(os.path.join(kept, "go.mod")))
503+
raise SystemExit("a refusal inside")
504+
except SystemExit:
505+
pass
506+
print("left after a refusal: %s" % os.path.exists(kept))
492507
`
493508
dir := t.TempDir()
494509
file := filepath.Join(dir, "probe.py")
@@ -512,6 +527,8 @@ except SystemExit as refusal:
512527
"candidate v0.5.0-rc1: True",
513528
"candidate v1.0.0-beta.2: True",
514529
"missing tag: REFUSED",
530+
"inside: True",
531+
"left after a refusal: False",
515532
} {
516533
if !strings.Contains(string(said), want+"\n") && !strings.Contains(string(said), want+"\r\n") {
517534
t.Errorf("the probe did not say %q:\n%s", want, said)

0 commit comments

Comments
 (0)