Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,19 @@ because it turns other people's test suites red.

### Changed

- **Building from source now needs Go 1.27.0.** It used to say 1.26.5, and that
sentence was true of compiling and false of the product. Go 1.27 changed
`compress/flate`, so a copy built on 1.26 answers the same version number,
writes that number into every manifest, and produces different bytes for PNG,
DOCX and TAR.GZ than the release of that name. The compiler now refuses that
build instead of producing one that quietly disagrees with everybody else's.

If you are pinned to Go 1.26 you can no longer build from source. The
downloadable binaries are unaffected, and so is `go install`.

A manifest already records which Go produced it, under `tool.go`, so two runs
that disagree can be told apart after the fact as well.

- **The `encryption` setting now says that a locked archive is a fixture rather
than protection.** Nothing about the files changes - the sentence `tfg formats
zip` prints, and the one the window shows beside the field, gained the two
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -172,7 +172,7 @@ Linux has no equivalent to sign them with.
go install -tags noasm github.com/donislawdev/TestingFilesGenerator/cmd/tfg@latest
```

**From source.** Needs Go 1.26.5 or newer, and nothing else:
**From source.** Needs Go 1.27.0 or newer, and nothing else:

```
git clone https://github.com/donislawdev/TestingFilesGenerator
Expand Down
52 changes: 36 additions & 16 deletions go.mod
Original file line number Diff line number Diff line change
@@ -1,15 +1,38 @@
module github.com/donislawdev/TestingFilesGenerator

// The compiler takes part in producing bytes, so its version is part of the
// byte stability contract (D11). This line is a minimum - the exact version
// used for tests and releases is pinned in the CI workflow, and the guard
// test in internal/guard reports any drift it causes. See docs/STACK.md.
go 1.26.5
// byte stability contract (D11). This line is the floor: the oldest compiler
// the module admits, and the only one of the two below it that the compiler
// itself enforces.
//
// It is held equal to the toolchain line rather than left lower, and that is
// the whole point of it. A floor below the pin is a hole, because the toolchain
// line is consulted only when GOTOOLCHAIN allows a switch. Measured on
// 2026-09-06 with the floor at 1.26.5: GOTOOLCHAIN=go1.26.8 built this module,
// the binary answered 0.3.0-rc1, wrote 0.3.0-rc1 into every manifest, and
// produced different bytes for png, docx and targz. Same source, same version
// number, different files - which is the failure D11 exists to prevent.
//
// The cost is stated rather than hidden: nobody on Go 1.26 can build this from
// source any more. That was the owner's decision on 2026-09-06, on the argument
// that the alternative was not a working build but a build that lies about
// which version it is. Kept honest by TestTheBuildFloorIsThePinnedToolchainAndTheReadmeSaysSo,
// which also holds the sentence in README.md against this number. See
// docs/STACK.md.
go 1.27.0

// What we actually build with, which is not the same statement as the line
// above. That one is a floor for anybody compiling this. This one says which
// toolchain produces our binaries, and Go fetches it rather than asking anyone
// to install it.
// How the line above got to the number it carries. There used to be a separate
// "toolchain" directive here saying which compiler produces our binaries, and
// it is gone rather than forgotten: measured on 2026-09-06, Go refuses to build
// a module whose toolchain directive is not newer than its go directive, and
// once the floor was raised to meet the pin the two were the same number. The
// build says "updates to go.mod needed" until the directive is taken out. The
// comment is kept because the directive was never the valuable part - these
// four decisions and what each of them cost were.
//
// What now carries the pin instead is GO_VERSION in the three CI workflows, and
// TestTheBuildFloorIsThePinnedToolchainAndTheReadmeSaysSo holds all of those
// against this line and against README.md.
//
// Raised to 1.26.6 on 2026-08-13 because govulncheck reported five standard
// library vulnerabilities reachable from the window binary under 1.26.5 - among
Expand Down Expand Up @@ -44,22 +67,21 @@ go 1.26.5
// closes a level zero stream went from five bytes to two. It measures the
// framing now, so the next release moves the bytes again but does not stop the
// format from being written.
toolchain go1.27.0

require github.com/goccy/go-yaml v1.19.2

require (
fyne.io/fyne/v2 v2.8.1
github.com/gen2brain/gav1d v0.2.5
github.com/gen2brain/jxl v0.2.0
github.com/goccy/go-yaml v1.19.2
github.com/nicksnyder/go-i18n/v2 v2.6.1
golang.org/x/image v0.45.0
golang.org/x/text v0.41.0
)

require github.com/FyshOS/fancyfs v0.0.1 // indirect

require (
fyne.io/fyne/v2 v2.8.1
fyne.io/systray v1.12.3-0.20260810170012-af4e8e793ec4 // indirect
github.com/BurntSushi/toml v1.6.0 // indirect
github.com/FyshOS/fancyfs v0.0.1 // indirect
github.com/anthonynsimon/bild v0.14.0 // indirect
github.com/clipperhouse/uax29/v2 v2.2.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
Expand All @@ -79,14 +101,12 @@ require (
github.com/jsummers/gobmp v0.0.0-20230614200233-a9de23ed2e25 // indirect
github.com/mattn/go-runewidth v0.0.24 // indirect
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 // indirect
github.com/nicksnyder/go-i18n/v2 v2.6.1
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/rymdport/portal v0.4.2 // indirect
github.com/srwiley/oksvg v0.0.0-20221011165216-be6e8873101c // indirect
github.com/srwiley/rasterx v0.0.0-20220730225603-2ab79fcdd4ef // indirect
github.com/stretchr/testify v1.11.1 // indirect
github.com/yuin/goldmark v1.8.2 // indirect
golang.org/x/image v0.45.0
golang.org/x/net v0.57.0 // indirect
golang.org/x/sys v0.47.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
Expand Down
160 changes: 160 additions & 0 deletions internal/guard/buildfloor_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
package guard

import (
"os"
"path/filepath"
"regexp"
"strings"
"testing"
)

// The oldest compiler this module admits is the one it is actually built with.
//
// The "go" line in go.mod is a FLOOR: the oldest compiler the module admits,
// and the only statement of the three below that the compiler itself enforces.
// A floor lower than what CI runs is a hole, and it is not a tidiness question
// here, because bytes are the contract. The comment above the go line records
// what moved when this project went to 1.27.0 on 2026-09-01: Go 1.27 changed
// compress/flate, so every format that puts bytes through deflate produces
// different ones - eleven of the fifty one pinned cases moved, and all seven of
// the pinned standard library paths.
//
// Measured on 2026-09-06 against a build of this tree, which is what made this
// worth a guard rather than a note. With the floor at 1.26.5 a build under Go
// 1.26 succeeded, answered "0.3.0-rc1" to "tfg version", wrote "0.3.0-rc1" into
// every manifest, and produced different bytes from the release of that name
// for png, docx and targz. Same source, same version number, different files -
// which is the exact failure D11 and the version number exist to make
// diagnosable. The diagnosis was already in place and works: manifest.tool.go
// records the toolchain, so the two runs read "go1.27.0" and "go1.26.8" and
// somebody who thinks to look can tell. Nothing made them look.
//
// THERE IS NO TOOLCHAIN DIRECTIVE TO COMPARE AGAINST, and that is a measurement
// rather than an oversight. Go refuses to build a module whose toolchain
// directive is not newer than its go directive - once the floor was raised to
// meet the pin, the build answered "updates to go.mod needed" until the
// directive came out. So the pin now lives in GO_VERSION in the workflows, and
// this holds the floor against every one of them.
//
// The third copy is the one a person reads before they have the repository.
// README.md states the minimum in prose, and prose is the copy that rots, so it
// is held here too. Same arrangement as the build tags in
// TestTheInstallInstructionsCarryTheBuildTags and for the same reason: one file
// holds the fact, everything inside a checkout reads it, and the copies that
// have to live outside get a guard instead.
//
// Equality rather than "the floor is at least the pin", deliberately. Comparing
// versions means arithmetic on version strings, and arithmetic on version
// strings is precisely where this class of defect hides. Two strings either
// match or they do not, and that is checkable without a comparison anybody has
// to be right about.
func TestTheBuildFloorIsThePinnedToolchainAndTheReadmeSaysSo(t *testing.T) {
root := repoRoot(t)
floor := goModFloor(t, root)

checked := 0
for _, wf := range workflowFiles(t, root) {
raw, err := os.ReadFile(wf)
if err != nil {
t.Fatalf("reading %s: %v", filepath.Base(wf), err)
}
pin := regexp.MustCompile(`(?m)^\s*GO_VERSION:\s*"?([0-9][0-9.]*)"?\s*$`)
for _, m := range pin.FindAllStringSubmatch(string(raw), -1) {
checked++
if m[1] == floor {
continue
}
t.Errorf("%s builds with Go %s and go.mod admits Go %s, so the compiler CI runs "+
"is not the oldest one this module accepts.\n"+
"What to do: make them the same number. A floor below what CI runs lets "+
"somebody build a copy that answers the same version and produces "+
"different bytes.", filepath.Base(wf), m[1], floor)
}
}
if checked == 0 {
t.Fatal("no workflow declared GO_VERSION, so this guard checked nothing against the " +
"floor. That is where the pin lives now that go.mod has no toolchain directive.")
}

stated, at, text := readmeMinimumGo(t, root)
if stated != floor {
t.Errorf("README.md line %d tells somebody they need Go %s and go.mod admits Go %s:\n"+
" %s\n"+
"What to do: make the sentence name %s. It is read before anybody has the "+
"repository, so it is the one copy nothing else can correct.",
at, stated, floor, strings.TrimSpace(text), floor)
}
}

// goModFloor returns the version on the go directive.
func goModFloor(t *testing.T, root string) string {
t.Helper()

raw, err := os.ReadFile(filepath.Join(root, "go.mod"))
if err != nil {
t.Fatalf("reading go.mod: %v", err)
}
for _, line := range strings.Split(string(raw), "\n") {
fields := strings.Fields(line)
if len(fields) == 2 && fields[0] == "go" {
return fields[1]
}
}
// Asserted rather than assumed. A guard that silently found no line would
// be green on a go.mod that lost it, which is the failure this file is
// about.
t.Fatal(`go.mod has no "go" line, so the build floor could not be read and this guard ` +
"checked nothing")
return ""
}

// workflowFiles lists the CI definitions, which are where the pin lives.
func workflowFiles(t *testing.T, root string) []string {
t.Helper()

found, err := filepath.Glob(filepath.Join(root, ".github", "workflows", "*.yml"))
if err != nil {
t.Fatalf("listing the workflows: %v", err)
}
if len(found) == 0 {
t.Fatal("no workflow files were found, so this guard could not read the pinned Go " +
"version from any of them")
}
return found
}

// readmeMinimumGo returns the version README.md names as the minimum, the line
// it sits on and that line's text.
//
// Matched on the sentence rather than on a position, because a position moves
// the first time somebody adds a paragraph above it.
func readmeMinimumGo(t *testing.T, root string) (string, int, string) {
t.Helper()

raw, err := os.ReadFile(filepath.Join(root, "README.md"))
if err != nil {
t.Fatalf("reading README.md: %v", err)
}

want := regexp.MustCompile(`Needs Go ([0-9][0-9.]*)`)
found, at, text := "", 0, ""
for i, line := range strings.Split(string(raw), "\n") {
m := want.FindStringSubmatch(line)
if m == nil {
continue
}
// A second sentence naming a version is two answers to one question,
// and the guard would then check whichever came last.
if found != "" {
t.Fatalf("README.md names a minimum Go version twice, on lines %d and %d, so "+
"there is no single sentence to hold against go.mod", at, i+1)
}
found, at, text = m[1], i+1, line
}
if found == "" {
t.Fatal(`README.md has no "Needs Go <version>" sentence, so this guard checked ` +
"nothing. It is the only statement of the requirement somebody sees before " +
"they clone.")
}
return found, at, text
}
Loading