Skip to content

Generate the third-party notices from the module table a binary carries - #134

Merged
iderex merged 1 commit into
mainfrom
supply-chain/the-notices-the-binary-owes-its-recipient
Aug 13, 2026
Merged

Generate the third-party notices from the module table a binary carries#134
iderex merged 1 commit into
mainfrom
supply-chain/the-notices-the-binary-owes-its-recipient

Conversation

@iderex

@iderex iderex commented Aug 13, 2026

Copy link
Copy Markdown
Contributor

Refs #37

What this changes

A binary this repository publishes owes whoever downloads it the licence of
everything inside it, and most licences ask for the text rather than for a
name. Nothing here produced that text.

internal/notices renders the document from the module set the toolchain
records inside a binary, and reads each module's licence text out of the module
cache the build already populated. cmd/notices is the entry point: it takes
the binary and the cache root as two arguments, opens no connection and reads no
environment variable, so what it produced can be reproduced by hand from the two
paths in its command line. That is the shape cmd/contexts already argued for
keeping a live lookup out of a command whose output has to be checkable.

The means is Go, in this module, with nothing added to go.mod. The module
table is written by the toolchain and read by debug/buildinfo in the standard
library, so the data source is the build itself. Reaching for a generator from
outside would add the first dependency this tree has ever carried in order to
list the dependencies this tree carries, and the suites that already exist would
not reach it.

What failure it prevents

A notices file that names a module and does not carry its text. A module whose
licence cannot be read is refused under dependency-has-no-licence-text and
named in a section that says the document is incomplete by exactly those
entries, rather than dropped from a file that then reads as finished.

Drift is the second one. A maintained list is correct on the day it is written
and wrong once a dependency arrives, and wrong in the direction that matters: a
module in the binary and absent from the list. There is no second list here for
anything to drift against.

The render carries no clock and sorts by module path, so two runs from one build
produce one file. A document carrying the time it was made would differ between
two runs of the same tag, and a checksum over it could then no longer separate a
release built from different source from one built twice.

What was run

The gate CONTRIBUTING.md names, at 74762af04541029f0916fca904e1ccd6bdd35a92:

$ go build ./cmd/... ./internal/... && go vet ./cmd/... ./internal/...
$ gofmt -l cmd internal
$ go test -count=1 ./cmd/... ./internal/...
ok      github.com/Flowfin/lab/cmd/contexts     1.535s
ok      github.com/Flowfin/lab/cmd/lab          2.561s
ok      github.com/Flowfin/lab/cmd/notices      5.792s
ok      github.com/Flowfin/lab/cmd/pullrequest  1.272s
ok      github.com/Flowfin/lab/internal/check   1.352s
ok      github.com/Flowfin/lab/internal/contexts        0.793s
ok      github.com/Flowfin/lab/internal/hardware        1.093s
ok      github.com/Flowfin/lab/internal/invariants      1.576s
ok      github.com/Flowfin/lab/internal/notices 1.289s
ok      github.com/Flowfin/lab/internal/prose   1.049s
ok      github.com/Flowfin/lab/internal/pullrequest     1.834s

go build and go vet printed nothing, and gofmt -l printed nothing, which
is its passing result.

The runner over this tree:

$ go run ./cmd/lab check .
examined .
1 experiment directory walked, 1 record read
18 decision records read
the time this run read is 2026-08-13T12:04:09Z
0 refused

The guard was proved by deleting it. Replacing the refusing arm of Render with
a silent continue, so an unreadable module is dropped instead of refused, and
running the package:

$ go test ./internal/notices -count=1
--- FAIL: TestEveryCaseIsRenderedAsItsFilesDeclare/a-dependency-whose-licence-file-is-empty
        notices_test.go:73: refused [], and the case declares [dependency-has-no-licence-text]
--- FAIL: TestEveryCaseIsRenderedAsItsFilesDeclare/a-dependency-whose-licence-is-not-in-the-cache
        notices_test.go:73: refused [], and the case declares [dependency-has-no-licence-text]
--- FAIL: TestEveryCaseIsRenderedAsItsFilesDeclare/a-module-path-that-climbs-out-of-the-cache
        notices_test.go:73: refused [], and the case declares [dependency-has-no-licence-text]
FAIL    github.com/Flowfin/lab/internal/notices 0.573s

Exactly the three refusing cases went red and the neighbour they name stayed
green, so the rule bites for the reason it names rather than refusing whatever
it reads. The arm was restored and the suite is green above.

The size, and it is above the bound this repository notes at:

$ git diff --numstat origin/main...HEAD | awk '{a+=$1; d+=$2} END {print a+d}'
1240

Of that, 232 lines are the seven fixture cases and 1008 are Go, of which 585 are
statements and the rest are the comments this tree is written with. It was not
split, because the two halves a split would produce are the render with no caller
and the caller with no render, and neither is reviewable alone. The property a
reader can check instead of the diff is that every module the build records
either appears with the text it shipped or appears in the section saying it did
not.

What this does not do

It does not close #37. Two of that issue's three legs are that both artefacts are
generated by the release build and attached to the release, and this repository
has no release workflow and no release. Those legs belong to #41, which is where
the attaching happens, and the bill of materials belongs there with them, because
#37 places it where the artefact is built.

It does not identify a licence. It reproduces the file a module shipped under a
conventional licence filename and takes no view on which licence that is or on
whether reproducing it satisfies that licence. A label guessed by reading the
text would be a claim nothing here can support, and the document says so where a
reader will see it.

Nothing runs it yet. The command exists and its suite exercises it against a
binary it builds, and no workflow calls it, because the workflow that would is
#41.

The seven cases construct a module set rather than compiling one. Building a
module with a real dependency would need a fetch, and this suite runs with no
network. What compiles a binary and reads a real module table out of it is
cmd/notices' own test, and that binary has no third-party dependency, so the
two together prove the render and the reading rather than the two at once.

This board had no second reader for this change and the evidence above stands in
place of one.

A binary this repository publishes owes whoever downloads it the licence of
everything inside it, and most licences ask for the text rather than for a
name. Nothing here produced that text, so the obligation was met by a list
nobody had written yet.

internal/notices renders the document from the module set the toolchain records
inside the binary, and reads each module's licence text out of the module cache
the build already populated. cmd/notices is the entry point the release build
calls: it takes the binary and the cache root as arguments, opens no connection
and asks no environment variable, so what it produced can be reproduced by hand
from the two paths in its command line.

The failure it prevents is a notices file that names a module and does not carry
its text. A module whose licence cannot be read is refused under
dependency-has-no-licence-text and named in a section of the document that says
the document is incomplete by exactly those entries, rather than being dropped
from a file that then reads as finished. The second failure is drift: a
maintained list is correct on the day it is written and wrong once a dependency
arrives, so there is no second list here for anything to drift against.

The render carries no clock and sorts by module path, so two runs from one build
produce one file and a checksum over it still separates a release built from
different source from one built twice.

Refs #37

Signed-off-by: Nils Lehnen <30603423+iderex@users.noreply.github.com>
@iderex
iderex merged commit 466d63a into main Aug 13, 2026
25 checks passed
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.

Generate the third-party notices and the bill of materials

1 participant