Skip to content

build: the tool is built with Go 1.27 now, and six formats change bytes - #37

Merged
donislawdev merged 2 commits into
mainfrom
build/go-1-27
Sep 2, 2026
Merged

build: the tool is built with Go 1.27 now, and six formats change bytes#37
donislawdev merged 2 commits into
mainfrom
build/go-1-27

Conversation

@donislawdev

Copy link
Copy Markdown
Owner

🔴 This is a breaking change under D11. It needs a major version bump, which is the owner's call and is not in this branch.

Raises the toolchain from 1.26.7 to 1.27.0 in go.mod and in both workflows, rewrites the pinned hashes that moved, and bumps two tools that cannot read Go 1.27 at all.

What changes for someone using the tool

Sizes are unchanged. Every size that worked still works, the same sizes are reachable, and every reader that took these files still takes them. What moved is the compressed data inside them.

different bytes targz, png, docx, xlsx, pptx, ico when it holds a png, zip when compression is asked for
untouched zip left alone (its default stores), and the other 17 formats, byte for byte
pinned cases that moved 11 of 51 generator cases, 7 of 7 standard library paths

Two minimums moved and the site says so: png 73 → 74 B (75-82 and 85 are now the unreachable band) and targz 1052 → 1049 B (next reachable 1051, gap at 1050). Measured with tools/probes/reachable-sizes.py rather than derived - two numbers in that table were once wrong precisely from being derived.

Why move at all

Not because 1.27 is better. This machine builds other projects already on it, and GOTOOLCHAIN belongs to the account rather than to a project, so the two kept taking it in turns - a byte guard went red on formats nobody had touched, and go version settled it rather than reading the diff. Staying meant a check before every command, forever. Decision by the owner; written up in docs/GO-127-MIGRATION.md and O169.

TAR.GZ could not be produced at all under 1.27

Every size was refused: the generator came out three bytes short of its plan. The size of a .tar.gz is arithmetic rather than a measurement - compressing twice to learn a length would make --dry-run cost what a run costs - and that arithmetic carried the gzip framing as constants. Go 1.27 changed one: the block that closes a level zero stream went from five bytes to two.

It measures the framing now, at first use, from four compressions, and checks the model against a third block and against the empty stream before trusting it. A later release can move these bytes again but can no longer stop the format from being written.

Two properties made this safe to land as one commit:

  • The repair is byte neutral under 1.26.7 - nine of nine against a binary built from main. So every byte that moved in this branch belongs to the compiler, not to the repair, and the golden diff is auditable.
  • The measurement costs nothing measurable - startup medians 93.5 ms against 93.3 ms with overlapping spreads over 12 interleaved runs.

Two tools had to move, and neither was a finding about this code

  • staticcheck v0.7.0 → v0.8.1. The old one refuses the standard library outright: export data version 4 is greater than maximum supported version 2.
  • govulncheck v1.1.4 → v1.7.0. The old one panics (panic: Int inside golang.org/x/tools), which reads like a security result on a gate named for vulnerabilities. The new one is clean and finds no vulnerability in 1.27.0 - worth knowing for a release with no patches behind it yet.

gofmt from 1.27 reindents exactly one file, internal/gui/window/generate.go, and that is included.

What was checked

  • the full scope, finally countable because targz runs again: 11 of 51 and 7 of 7
  • targz produced at every size tried, exact to the byte; 312 shapes through the arithmetic guard
  • the framing repair byte neutral under the old compiler, 9 of 9
  • the golden writer proven to round trip both files byte for byte before any hash was rewritten, so the diff is hashes and provenance and nothing else
  • preflight non-test checks, 9 of 9, including the two new tool versions
  • the broad guard set green; three mutation entries added for the framing, and ten mutations caught in total including three the change had staled

Full analysis, and what it got wrong before the work started, in docs/GO-127-MIGRATION.md.

🤖 Generated with Claude Code

donislawdev and others added 2 commits September 2, 2026 07:12
Raises the toolchain in go.mod and GO_VERSION in both workflows from
1.26.7 to 1.27.0, rewrites the eleven pinned generator cases and all
seven pinned standard library paths that moved, and bumps two tools that
cannot read Go 1.27 at all.

This is a breaking change under D11. Sizes are unchanged, every size that
worked still works, and the same sizes are reachable - what moved is the
compressed data. Affected: targz, png, docx, xlsx, pptx, ico when it
holds a png, and zip when compression is asked for. zip left alone is
untouched because its default stores rather than compresses. The other
seventeen formats are byte for byte what they were.

The cause is compress/flate, measured with a probe that imports none of
this project: all thirty two combinations of input and level differ
between the two releases. At level zero the change is framing rather than
algorithm - the block closing a stream went from five bytes to two, a
constant three - and above it the compressor itself behaves differently.

The reason for moving is not that 1.27 is better. This machine builds
other projects that are already on it, and GOTOOLCHAIN belongs to the
account rather than to a project, so the two were taking it in turns and
a guard went red on formats nobody had touched. Staying meant a check
before every command, forever. Decision by the owner.

TAR.GZ could not be produced at all under 1.27 before this. Its size is
arithmetic rather than a measurement, because compressing twice to learn
a length would make a preview cost what a run costs, and that arithmetic
carried the gzip framing as three constants. It measures them now, at
first use, from four compressions, and checks the model against a third
block and against the empty stream before trusting it. A later release
can move these bytes again but can no longer stop the format being
written. That repair is byte neutral under 1.26.7 - nine of nine against
a binary from main - so every byte that moved here belongs to the
compiler and not to it.

Two tools had to move with the compiler, and neither was a finding about
this code. staticcheck v0.7.0 refuses the standard library outright
("export data version 4"), and govulncheck v1.1.4 PANICS, which reads
like a security result on a gate named for vulnerabilities. Both are at
their newest, v0.8.1 and v1.7.0, and both are clean - govulncheck finds
no vulnerability in 1.27.0, which was worth knowing for a release with no
patches behind it yet.

Two minimums moved and the site says so: png 73 to 74 B, with 75-82 and
85 now the sizes it cannot produce, and targz 1052 to 1049 B, next
reachable 1051, gap at 1050. Measured with the probe that exists for that
table rather than derived, because two numbers in it were once wrong from
being derived.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
CI found this, not the migration: three jobs went red on a change that
had passed a targeted local run, and one of the three was a latent fault
in compression rather than anything Go 1.27 did.

A 256 KiB archive at compression best would not settle. Traced: the
filler came down five or six bytes a round while the file sat at 262147
and 262148, wobbling by one. The filler is a tar entry and a tar entry is
padded to a whole 512 byte block, so handing back fewer than 512 bytes
changes WHICH bytes the archive carries and not HOW MANY - what comes out
of gzip then moves by a byte either way. The walk assumed a slope of one
and was stepping across a staircase.

That was true from the day compression landed. Go 1.27 only moved which
sizes sit on a step edge, which is why it looked like the compiler broke
it. So it gives back a whole block on an overshoot now: that lands under
the target, and the gzip extra field closes the rest exactly, because the
field adds precisely what it is given. Two rounds instead of never.
Checked over 90 consecutive sizes in four bands at all three levels.

The other two were mine and both were the guards being right.

sync.OnceValues in framing.go was new concurrency, which this project
asks to be declared. Declaring it would have been the wrong answer: the
registry works out this format's minimum during init, which asks for the
framing anyway, so nothing was ever lazy about it. A package level
variable is initialised once, before any goroutine exists, and needs no
lock. The guard found real over-engineering rather than a missing entry.

And a semicolon in a comment, which rule 13 does not take.

Adds tools/probes/targzsettle, which is how the staircase was seen, and a
mutation for the block step.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@donislawdev
donislawdev merged commit 0265f8e into main Sep 2, 2026
18 checks passed
@donislawdev
donislawdev deleted the build/go-1-27 branch September 2, 2026 06:48
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant