Documentation
¶
Overview ¶
Package logical applies a recipe override that says WHAT it changes rather than WHERE the lines are.
Why not a unified diff ¶
go-pkgx/packages carries 242 overrides as `git diff` output against upstream's package.yml. A diff is anchored on the text AROUND the change, so it stops applying the moment upstream edits a neighbouring line — and nothing downstream says so. Measured on 2026-09-24: 3 of 231 had silently stopped, among them the -Werror switch mozilla.org/nss needs, whose absence cost two rebuilds and a duplicate fix for a defect that was already fixed. On 2026-09-27 a patch that `git apply --check` accepted was refused by the builder's own applier, because git searches for context with an offset and go-gitdiff does not; it merged broken.
A diff also cannot tell two very different situations apart. "Does not apply" means either *the thing I edit is gone* or *upstream already did what I was doing*. The first is a defect, the second is an override that should be deleted. This package reports them separately — see Outcome.
What the overrides actually do ¶
The verbs here are not invented. Every one of the 242 overrides was applied to a pristine pantry and the DOCUMENT compared before and after, with list elements matched by value:
change 153 a scalar became another scalar set 44 a key appeared change-substring 23 a string gained or lost a run list-append 15 list-replace-element-substring 9 project-created 8 remove 7 list-rewrite 7 list-replace-element 4 list-remove 1
171 of the 213 changed projects need only set/remove/change and whole-list operations; the other 42 are almost all one string edited inside one command. So: a merge, a removal, an insertion, and a substitution.
Semantics, and where each rule comes from ¶
Maps DEEP MERGE, as in JSON Merge Patch (RFC 7386): a key present in the override replaces that key, a key absent is inherited.
Lists are REPLACED, never merged. Kustomize merges some lists by a "merge key" and replaces others, and needs OpenAPI metadata to know which; there is no merge key for the lines of a shell script. Append and Prepend exist so that adding one element does not mean restating the list — the same reason Yocto has `:append` and `:prepend` rather than only assignment.
Removal is a VERB, not a sentinel. RFC 7386 deletes by writing null, and therefore cannot ever set a value to null; more to the point here, omission has to keep meaning "inherit".
Substitution matches a SUBSTRING of a value and never an index. Kustomize's own documentation warns that an index-based operation deletes the wrong element as soon as the base list is reordered — which is exactly as brittle as the line numbers this package exists to leave behind.
One HCL wrinkle ¶
A block header takes an identifier, so a map whose keys are project names has to be written as an object expression:
merge {
build {
dependencies = { "gnu.org/patch" = "*" } // = { }, not a block
}
}
This is not ours to change: it is how the recipes themselves are written, and the same reader parses both.
Nothing is written ¶
Apply works on a parsed document, in memory. Rewriting package.yml would drop every comment in it, and those comments carry the reasons: gnu.org/grep spends thirteen lines explaining why it names itself as a build dependency. A textual diff preserved them by only touching what it changed; a document rewrite would not.
Index ¶
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func Emit ¶
Emit writes an override back as HCL.
It exists for the conversion: 242 unified diffs become files nobody typed, and the only way that is a migration rather than a rewrite is if the output can be read back and checked. So the property this owes is a ROUND TRIP — Emit then Parse gives the same operations — and above it the real one, that applying them reproduces what the diff produced.
Derived operations come out as `edits`, one verb per entry, because that is faithfully what was derived and the `path` leaves no doubt what is touched. The `merge` block stays for files a person writes, where a recipe fragment reads better than a list of paths.
Types ¶
type Op ¶
type Op struct {
// Why this operation exists. Required: an override whose reason lives only
// in a commit message is an override nobody can decide to delete.
Why string
Path Path
Set any // set Path to this value (maps deep-merge, lists replace)
// Expect is what the operation believes is at Path before it runs, and
// HasExpect says the field means anything — nil is a value a document can
// legitimately hold.
//
// It exists for the one operation that can swallow an upstream change
// without saying so: replacing a whole list that was already there. A
// unified diff would at least have refused; a blind assignment would not,
// and quietly discarding somebody else's fix is worse than failing.
// RFC 6902 has this as `test`, and it is the verb JSON Merge Patch lacks.
Expect any
HasExpect bool
Remove bool // remove Path
Append []any // append these to the list at Path
Prepend []any // prepend these to the list at Path
From, To string // substitute: replace From with To inside the string(s) at Path
Substitute bool // distinguishes a substitution with an empty To
}
Op is one operation. Exactly one of the verbs is set.
func Derive ¶
Derive works out the operations that turn `before` into `after`.
It exists so that 242 unified diffs can be converted without anyone retyping them, and — more importantly — so the conversion can be CHECKED: apply what Derive returns to `before` and the result must equal `after`, document for document. A migration nobody can check is a rewrite.
Two rules matter and both come from the same place, that a position is not an identity:
- list elements are matched by VALUE, never by index;
- when a list differs by exactly one element out and one element in, and those two are the same string with one contiguous run changed, it is emitted as a SUBSTITUTION rather than a whole-list assignment. That is the "append a flag to this command" shape, 32 of the 42 hard cases, and writing the list out instead would fork it.
type Outcome ¶
type Outcome int
Outcome is what one operation did, and the three cases are the reason this package exists.
A unified diff has two: it applied, or it did not. "Did not" hides the difference between a defect and a job already done, and that difference is the whole maintenance story of an override directory.
const ( // Applied: the document changed. Applied Outcome = iota // Redundant: the document ALREADY said this. Upstream has caught up, and // the override should be deleted rather than carried. Never an error. Redundant // PremiseGone: what this operation edits is not there any more. It is an // error, and it is the only one of the three a diff could tell you about. PremiseGone )
type Override ¶
Override is one project's logical change, as a file states it.
project = "info-zip.org/zip"
why = "gcc 14 makes an implicit declaration an error, and info-zip's
memset probe runs $CC without CFLAGS"
merge {
build {
dependencies { "gnu.org/patch" = "*" }
}
}
edits = [
{
why = "unix/Makefile overwrites CFLAGS, so CC is the only channel"
path = "build.script"
from = "-std=gnu17\""
to = "-std=gnu17 -Wno-implicit-function-declaration\""
},
{ why = "…", path = "build.dependencies[\"crates.io/semverator\"]", remove = true },
]
`merge` is a single block because the shared HCL reader refuses a repeated one, and `edits` is a LIST because the order of two operations on the same path is a fact the file has to state rather than leave to a map's iteration. The merge runs first: it says what shape the recipe should have, and the edits adjust that shape.
type Path ¶
type Path []string
Path is a route to one place in a recipe document.
Dots separate steps, and a step whose own name contains a dot or a slash — which every project name does — is written in brackets:
build.dependencies["crates.io/semverator"] dependencies["openssl.org"] build.env.ARGS
Brackets take a quoted string, never a number. An index is a position, and a position is the thing this package refuses to depend on.
type Result ¶
Result reports one operation's outcome.
type Set ¶
type Set struct {
// Files records where each came from, so a report can name the file a
// reader has to open.
Files map[string]string
// contains filtered or unexported fields
}
Set is every logical override a directory holds, keyed by project.
One file per project, not one per change: a project's overrides are read together, applied together, and argued about together. The unified diffs they replace were one per FIX, which is why llvm.org had four of them and why answering "what do we do to llvm.org?" meant opening four files and working out what order they applied in.
func LoadDir ¶
LoadDir reads every *.hcl in dir.
A directory with none is not an error — during the migration most projects are still unified diffs, and a tree that has not been converted yet must keep building.
func (*Set) ApplyTo ¶
ApplyTo runs a project's override against a recipe document, if there is one. A project with no override is left exactly as it was.