logical

package
v0.5.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 5, 2026 License: BSD-3-Clause Imports: 8 Imported by: 0

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

func Emit(o *Override) []byte

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

func Derive(before, after map[string]any, why string) []Op

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
)

func (Outcome) String

func (o Outcome) String() string

type Override

type Override struct {
	Project string
	Why     string
	Ops     []Op
}

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.

func Parse

func Parse(src []byte, filename string) (*Override, error)

Parse reads an override file.

Through bottle.HCLToMap, the same reader the recipes themselves go through. A second HCL front-end here would be two readings of one format, and the day they disagreed an override would mean one thing to the checker and another to the builder.

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.

func ParsePath

func ParsePath(s string) (Path, error)

ParsePath reads the form above. It is strict: an unterminated bracket, an empty step or a numeric index is an error at PARSE time, so a malformed override is refused before it can half-apply.

func (Path) String

func (p Path) String() string

String renders a Path back, bracketing a step that needs it, so an error message names the place in the same spelling the override used.

type Result

type Result struct {
	Op      Op
	Outcome Outcome
	Detail  string
}

Result reports one operation's outcome.

func Apply

func Apply(doc map[string]any, ops []Op) ([]Result, error)

Apply runs ops against doc, in order, and reports what each one did.

It returns an error on the FIRST premise that is gone, with nothing further attempted: a half-applied override is a recipe nobody wrote, and building from one is worse than not building.

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

func LoadDir(dir string) (*Set, error)

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

func (s *Set) ApplyTo(project string, doc map[string]any) ([]Result, error)

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.

func (*Set) For

func (s *Set) For(project string) *Override

For returns the override for a project, or nil.

func (*Set) Projects

func (s *Set) Projects() []string

Projects lists what the set overrides, sorted.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL