Documentation
¶
Overview ¶
Package validate answers one question about an ebuild without building it: does it still match the source it points at?
It reads the build options the upstream archive declares, reads the ones the ebuild passes, and subtracts. Every input comes from the local filesystem — the archive is already on disk, put there by the manifest step — so the package performs no network I/O at all, and it writes nothing anywhere.
Index ¶
- Constants
- Variables
- func AllowedBuildEnv(parent []string) []string
- func ArchiveBuildSystem(ctx context.Context, archive string) (string, error)
- func DependenciesSatisfied(ctx context.Context, stagedRoot, atom, version string, deps BuildDeps) (ok bool, missing []string, err error)
- func DigestBytes(body []byte) string
- func DistdirEvidenceNote(distdir string) string
- func FindDistfile(pkgDir, distdir, version string) (string, error)
- func GateForDepth(d Depth) (string, bool)
- func IsolationFidelityNote(isolated bool, probeReason string) string
- func PkgcheckFindings(ctx context.Context, pkgDir, atom string) ([]Finding, Outcome, string)
- func ProbeIsolation() (bool, string)
- func PromotionDecision(gates []GateResult, stageErr error) (bool, string)
- func SourcePrepareStarted(transcript string) bool
- func Stage(req StageRequest) (string, error)
- func StageRecordPath(stagedRoot string) string
- func StagedTreePath(stagingRoot, atom, version string) (string, error)
- func WriteStageRecord(stagedRoot string, rec StageRecord) error
- type BuildDeps
- type BuildRequest
- type Class
- type Declared
- type DeclineCause
- type Depth
- type DepthDecision
- type DepthOverride
- type DepthPolicy
- type DepthRequest
- type EbuildResult
- type Finding
- type GateResult
- type Option
- type Options
- type Outcome
- type Passed
- type PreparedBuild
- type PreparedBuildRequest
- type Report
- type Severity
- type StageRecord
- type StageRequest
- type StagedCandidate
- type StagedSweepPlan
- type SweepReport
- type SweepRequest
- type SweptEntry
- type Unresolved
Constants ¶
const ( GateOptions = "options" GateQA = "qa" GatePatches = "patches" GateConfigure = "configure" GateCompile = "compile" GateInstall = "install" GateReview = "review" )
Gate names which check produced a finding, and which check a GateResult answers for. It exists so Report.ExitCode can select on it: pkgcheck findings ride in the same report and must never move the exit code.
The names are the ladder's own names
patches, configure, compile and install are spelled exactly as Depth.String spells the rungs they belong to. That is deliberate: a report saying `depth=configure` and a gate named `configure` are talking about the same thing, and an operator should not have to learn two vocabularies for one ladder.
qa and review are the two that name no rung, because neither is one. pkgcheck runs beside the ladder and is advisory; the reviewer raises scrutiny and never decides. Their findings are reported like any other and are the only ones that cannot fail a bump.
const ( ProducedByApplier = "applier" ProducedByValidate = "validate" )
ProducedByApplier and ProducedByValidate name what wrote a stage record. They are the only two values StageRecord.ProducedBy is ever written with.
They are constants because these STRINGS are on-disk vocabulary rather than internal spelling: a record has to be readable by the version that wrote it AND by the next one, so renaming a value in the file is the one change that breaks reuse silently — nothing fails to compile, and a later run simply stops recognising the records an earlier release left beside its trees.
Why the producer decides anything at all
A stage record is a licence to publish WITHOUT running the gates again: a later `--apply` promotes a bump on the strength of what an earlier run recorded. The applier writes such a record having staged the very candidate it means to publish. A read-only validation run writes one having only MEASURED a tree — it promotes nothing, by contract — so its record is evidence about an outcome and NOT a licence, and the reuse path has to be able to tell the two apart from the bytes on disk alone.
const RepairBundleFile = ".bentoo-repair.json"
RepairBundleFile is the name of the repair bundle a failed apply leaves at the root of the staged tree it kept. It is spelled here, below the package that writes the bundle, so the sweep can keep such a tree without importing it.
Variables ¶
var ErrArchiveTooLarge = errors.New("archive output exceeds the read cap")
ErrArchiveTooLarge is returned when an invocation's output exceeds maxArchiveBytes. It is a named error so a caller can tell "this archive is hostile" apart from "this archive is broken" and report each honestly.
var ErrBuildSystemUndetermined = errors.New("no meson.build found: build system undetermined")
ErrBuildSystemUndetermined reports that the archive carries no meson.build at any project root, so nothing here can say what builds it.
This is a sentinel and not a guess on purpose. The caller turns it into a SKIPPED outcome that says the build system was undetermined; inferring "probably autotools" from the absence of one file would be unearned confidence.
var ErrDependenciesUndetermined = errors.New("whether the build dependencies are satisfied could not be determined")
ErrDependenciesUndetermined reports that this host could not be ASKED whether a candidate's build dependencies are satisfied — Portage is absent, the staged tree is not there, the resolve failed or timed out.
IT IS A DIFFERENT ANSWER FROM "a dependency is missing", and the difference is the reason this sentinel exists. Both make the caller skip the build gates but only one of them can name an atom to install. A caller that read a bare false as "something is missing" would print a skip blaming a dependency nobody has evidence for; errors.Is against this sentinel is how it tells the two apart, and it survives a reworded message the way a strings.Contains on the text would not.
var ErrNoStageRecord = errors.New("the staged tree carries no validation record")
ErrNoStageRecord reports that a staged tree carries no record at all.
It is a sentinel because a decision is made ON it — an unrecorded tree is REVALIDATED — and the caller must be able to tell "no claim" from "the record could not be read". Both revalidate today, but they are different facts about the operator's machine and only one of them is worth a warning.
var ErrStageUnpreparable = errors.New("the staged tree could not be prepared")
ErrStageUnpreparable reports that a staged tree could not be built, whatever the cause: a missing overlay, a malformed atom, a staging root nothing may write into, a disk that filled halfway through the copy.
IT IS A SENTINEL SO THE CALLER CAN RECOGNISE THE FAILURE BY TYPE, and that is not tidiness. What the caller does with it is withdraw the bump from promotion (see PromotionDecision) — so a caller that recognised it by matching message text would keep working exactly until somebody improved the wording, at which point unvalidated bumps would quietly start publishing again. errors.Is survives a reworded message; strings.Contains does not.
ONE SENTINEL COVERS EVERY CAUSE, on purpose. The cause is still named, in the wrapped error's own message, because the operator has to fix it — but no caller has to enumerate the ways staging can fail in order to react to staging having failed.
var ErrUnknownDepth = errors.New("unknown validation depth")
ErrUnknownDepth reports that a depth name came from a flag or a config key and does not name a rung of the ladder.
It is a sentinel so a caller can tell a bad depth apart from a validation failure: the first is a typo the operator must fix, the second is a bump that must not land, and they deserve different exit paths.
var ErrUnreadableVersion = errors.New("version cannot be read as a Gentoo version")
ErrUnreadableVersion reports that a version string could not be read as a Gentoo version, so the size of the move could not be measured.
It is a sentinel so a caller can tell "this bump really is major" apart from "we could not tell, so we charged the deepest class" — the two produce the same Class on purpose, and only the error distinguishes them.
Functions ¶
func AllowedBuildEnv ¶ added in v0.36.0
AllowedBuildEnv is the allow-listed subset of parent a Portage child gets, for callers outside this package that spawn `ebuild` themselves.
func ArchiveBuildSystem ¶ added in v0.36.0
ArchiveBuildSystem names the build system of the project an archive holds: "meson" when its root carries a meson.build, otherwise the system the shallowest marker file names, or "undetermined". It lists the archive and never unpacks it.
func DependenciesSatisfied ¶
func DependenciesSatisfied(ctx context.Context, stagedRoot, atom, version string, deps BuildDeps) (ok bool, missing []string, err error)
DependenciesSatisfied asks Portage whether this host could build the staged candidate at all, and reports every package it would have to install first.
`ebuild … configure` NEVER INSTALLS DEPENDENCIES, so a bump whose build dependencies are absent here would fail configure for a reason unrelated to the bump. Asking Portage first — a read-only `emerge -p` of the atom pinned with `=` and a `::` qualifier — separates "the bump is broken" from "this host cannot build it". Anything the pretend run lists BEYOND the package under test is a dependency this host lacks.
err == nil, ok == true — satisfied; the build gates may run.
err == nil, ok == false, missing — unsatisfied; the caller skips every build
gate and NAMES these atoms.
err != nil — UNDETERMINED; the caller skips but names
nothing (missing is nil on any error).
The staged repository is supplied as a PORTAGE_REPOSITORIES value, never as a repos.conf fragment (root, racing runs, a leftover after a kill): nothing is written anywhere.
func DigestBytes ¶
DigestBytes is the one spelling of "the content of this input", used for every digest a StageRecord carries.
It is one function rather than one per input so that the value WRITTEN by a proving run and the value COMPUTED by a promoting run cannot come from two implementations that drift apart — a mismatch there would not fail anything loudly, it would just revalidate everything forever.
func DistdirEvidenceNote ¶
DistdirEvidenceNote is the sentence a PASS carries about the distdir the run read from.
A pass asserts hermeticity, and a reader cannot tell a distdir this run enforced from the ambient one the host's own Portage configuration names unless the pass SAYS which one it exported. It states the ACT, not where the archives actually came from, so it can sit in front of the isolation note without contradicting it: this sentence says the gate set DISTDIR, that one says an unisolated build could have reached past it.
BOTH directions speak: if only the enforced case had a sentence, its absence would be ambiguous between "no distdir enforced" and "this reason predates the change", so the empty case says the build answered from the host's own configuration.
It is exported, like IsolationFidelityNote, so the applier's privileged `--compile` gate shares ONE copy of each sentence for the two states it shares with this path. Its third state, a directory the privilege tool could not carry, exists only there and is worded there.
func FindDistfile ¶ added in v0.36.0
FindDistfile returns the one archive of version listed in pkgDir's Manifest and present in distdir, by the same rules the options gate applies.
func GateForDepth ¶
GateForDepth names the gate whose OWN pass proves a run reached rung d. It is gateRungs read in the other direction, so the mapping stays in one table.
It is exported for `overlay autoupdate --check`, which must know whether the depth the POLICY SELECTED was measured; a table of its own in cmd/bentoo would be another spelling of this mapping, and could report a rung the runner never proved.
ok IS FALSE WHEN NO GATE PROVES d — today only DepthNone. The applier's similarly named gateForDepth (a repair target) falls back to the patch gate; this must not, or a depth-none bump would read as proved by a gate it never ran.
The loop returns the deepest gate AT OR BELOW d, which equals the gate for d only while gateRungs covers every selectable depth. Add a depth and its gate together, or a bump at that depth reads as proved on a SHALLOWER gate's pass. The three depth-gate tables (gateDepths, gateRungs, buildGates) stay separate on purpose: merging them would change the applier's repair-target fallback, which is behaviour.
func IsolationFidelityNote ¶
IsolationFidelityNote is the sentence a PASS carries when the run happened without a verified network namespace — the applier's `unverified isolation` label, applied to the build gates.
The run happens at all because the default does not move: `unshare --net` is denied to an ordinary user on the maintainer's host, so requiring isolation by default would turn every build gate there into a SKIPPED. The pass is kept AND qualified instead, which makes the weaker default honest rather than quiet. It returns "" on a verified run, so the label stays a signal.
It is exported because the applier's privileged `--compile` gate calls it directly, while the ladder reaches it through gateFor. Both paths must share the SENTENCE: one word — PASS — cannot describe two amounts of evidence, and a copy in the applier would drift the first time someone reworded one.
func PkgcheckFindings ¶
PkgcheckFindings collects the QA findings pkgcheck reports for one package, beside the option gate and without touching its verdict.
The reason is non-empty exactly when the outcome is SKIPPED: "pkgcheck found nothing" and "pkgcheck did not run" are different answers, and rendering them alike is a silent pass.
--cache=-git, because pkgcheck's GitAddon raises on this overlay's history: it prints a traceback to STDERR, writes nothing to stdout and EXITS 0, so every scan looks clean. Disabling it costs only the git-history checks (commit messages, dropped blockers), none of which say whether an ebuild matches its source.
The exit code is not the verdict: pkgcheck exits 0 with findings AND for an atom the repository does not hold. Records that did arrive are reported even after a non-zero exit, because findings on stdout are findings whatever the process did afterwards. Findings are carried at Gate "qa" so Report.ExitCode can exclude them.
func ProbeIsolation ¶
ProbeIsolation reports whether THIS PROCESS can create a network namespace, and — when it cannot — why.
It measures rather than infers. Portage reports `network-sandbox` in FEATURES but emits no warning when the unshare fails for lack of privilege, so a green build could mean "network cut off" or "full network access". Inferring from euid or capabilities would repeat that defect, so the probe starts a short-lived child with the namespace requested at fork time and reads the kernel's answer. A child, not unshare on this thread: Go cannot reliably retire an OS thread left in a foreign namespace, so in-process unsharing would contaminate whichever goroutines later land on it.
A probe that could not run has proved NOTHING, so it returns false with a reason exactly like a denial does: a failure of its own must never become evidence of isolation. reason is empty if and only if ok is true.
func PromotionDecision ¶
func PromotionDecision(gates []GateResult, stageErr error) (bool, string)
PromotionDecision answers the one question that must be settled before a bump is written into the published overlay: may this candidate be promoted, and if not, why not. It is pure, so the rule can be asserted directly.
A bump is promoted when every deciding gate reports PASS or SKIPPED — but a staging failure skips every build gate, so "nothing failed" alone would publish a candidate no gate ever read. A staging failure (stageErr) therefore WITHDRAWS the bump, and so does a list where nothing passed and a gate declined over the CANDIDATE (GateResult.Declined). A skip because THIS HOST could not answer — a missing dependency, no privilege to build — still promotes with the unreached depth named; refusing it would stop publishing on most workstations. A cause nobody recorded is not read as the candidate's (see DeclineUnrecorded).
It does not judge whether the gates COVER the selected depth: it is not given the depth. The QA gate never decides, as in Report.ExitCode, or pre-existing pkgcheck findings would stop every bump. The string explains the bool and is non-empty in BOTH directions, so no caller reads a promotion out of an empty reason.
func SourcePrepareStarted ¶
SourcePrepareStarted reports whether a build transcript shows that src_prepare had BEGUN — that the run got as far as the first phase the ebuild itself drives.
It is exported for the applier's build-fix gate, which must decide whether a failed build is the ebuild's fault before handing it to an agent. It asks HERE rather than grepping for the marker itself, so the marker is spelled in one package and cannot drift silently.
The predicate is the START of prepare, not `>>> Source prepared.`: a failure in setup or unpack is a host or distfile fault, from prepare onward it is the ebuild's. A patch that no longer applies fails BETWEEN the two markers and is the ebuild's fault (derive() reports the patches gate FAILED), so keying on the DONE marker would refuse a repair for the most common bump breakage.
The transcript is read with ANSI escapes removed (see stripANSI): Portage colours these markers whenever a terminal is attached, and the applier's compile gate attaches a real one.
func Stage ¶
func Stage(req StageRequest) (string, error)
Stage builds a self-consistent single-package Portage repository holding one candidate ebuild and returns its root, <StagingRoot>/<category>/<package>/ <version>: eclass/ and profiles/ copied from the overlay, metadata/layout.conf, its own profiles/repo_name, and <category>/<package>/<package>-<version>.ebuild.
- It refuses a StagingRoot inside the overlay: a staged candidate there is an unclaimed ebuild that `overlay autoupdate --clean` would delete.
- It masters onto gentoo with local COPIES of eclass/ and profiles/, never onto the overlay, whose deployed /var/db/repos copy lags by a sync cycle.
- Its repo_name is unique per package-version (two repositories with one name conflict) and written AFTER the profiles/ copy, which would clobber it.
- Restaging replaces the tree at the same path, which is why the applier's copyEbuild, refusing an existing destination, is not reused.
- No Manifest and no other version travels; Run writes Options.StagedManifest into the tree before the build gates, as only the caller knows its bytes.
Every failure wraps ErrStageUnpreparable and returns an EMPTY path, because a path handed back is a path somebody will build in.
func StageRecordPath ¶
StageRecordPath names the record of one staged tree. It is exported so an operator can be TOLD where the evidence is rather than having to know.
func StagedTreePath ¶
StagedTreePath is where Stage puts, and a later run looks for, the tree of one package and version: <stagingRoot>/<category>/<package>/<version>.
It is exported and shared with Stage itself so the layout is spelled ONCE. A promoting run has to find a tree it did not create, and a second spelling of the path would be a second spelling that can go stale — the failure being one where nothing is ever reused and no error is ever printed.
func WriteStageRecord ¶
func WriteStageRecord(stagedRoot string, rec StageRecord) error
WriteStageRecord puts rec beside the staged tree at stagedRoot.
stagedRoot must already be a directory: this writes a record ABOUT a tree, and creating the directory here would leave a proof standing where the thing it describes never was.
A failure is the caller's to decide about, and every caller in this repository treats it as a warning rather than as a failed bump: the cost of an unwritten record is one revalidation, which is the behaviour that predates records.
rec.ProducedBy is written exactly as the caller stated it and is never defaulted here. A writer is the one thing that KNOWS what it is, so filling a producer in on this side would let a future one inherit the applier's provenance by merely forgetting to name itself. Absence is given its meaning on the way back in, in ReadStageRecord, where the records that predate the field are.
Types ¶
type BuildDeps ¶
type BuildDeps struct {
// ExecCommand builds a command whose output this package CAPTURES, in the
// shape the rest of the repository already uses for an exec seam. It
// defaults to the package-level execCommand of archive.go.
ExecCommand func(ctx context.Context, name string, arg ...string) *exec.Cmd
// RunAttached executes a command and returns its combined output. It is the
// seam the compile gate needs rather than this one: a long build is streamed
// and the caller keeps the transcript, which is a different relationship with
// the child process than "collect stdout and parse it".
RunAttached func(cmd *exec.Cmd) ([]byte, error)
// LookPath answers "is this tool installed at all". It is separate from
// ExecCommand so the absent-binary branch stays reachable in a test on a host
// that does have Portage — which is every host this is developed on. It
// defaults to the package-level lookPath of pkgcheck.go.
LookPath func(name string) (string, error)
// IsolationProbe measures whether this process can create a network
// namespace, returning the answer and, when it is no, the reason. It defaults
// to ProbeIsolation and is consumed by the compile gate.
IsolationProbe func() (bool, string)
}
BuildDeps is the set of process- and host-level seams the build gates run through, injected as functions so that every branch — the tool is missing, the resolve failed, the host cannot isolate — stays reachable in a test on a host where the real thing behaves the other way. It is declared here, not beside RunBuildGates in build.go, because DependenciesSatisfied was its first consumer.
Every field may be nil, and nil means "use the real thing". Each consumer normalises what it reads (see commandFactory and binaryLookup), so a caller that needs no substitution passes BuildDeps{}, and a nil seam can never reach a call site and panic there.
type BuildRequest ¶
type BuildRequest struct {
// StagedRoot is the staged repository Stage produced — the repo root, the
// directory holding profiles/ and the candidate's category directory. It is
// NOT checked for existence here; see RunBuildGates.
StagedRoot string
// Key is the registry key as the operator asked about the package:
// category/package, possibly carrying ":slot" or "@label".
Key string
// Version is the candidate version being validated.
Version string
// Depth is how far up the ladder to run. It decides the single phase the one
// invocation runs, and therefore which gates can be derived at all.
Depth Depth
// RequireIsolation refuses to run a build this host cannot isolate. It is
// consumed by the isolation policy rather than by the phase derivation.
RequireIsolation bool
// LogDir is where a FAILED run's log is retained. An empty
// LogDir retains nothing and says so in the gate's reason, because a log
// nobody was told about is a log nobody will read.
LogDir string
// Distdir is the directory this run resolved for the build's archives. It is
// SET on the child as DISTDIR when it is non-empty.
//
// It is a COMPUTED input — resolved BY this run, from --distdir or from the
// private directory an apply's own manifest step filled — so it is assigned
// explicitly rather than admitted through allowedBuildEnv, which filters the
// PARENT's environment. When DISTDIR sat on that allow-list the gate ran
// against whatever distdir the operator's shell named, and --distdir never
// reached the fetch. The allow-list keeps meaning one thing: NOTHING crosses
// in from outside.
//
// EMPTY RESOLVES TO NOTHING. No DISTDIR is set and none is invented, so
// Portage answers from its own configuration — the honest answer for a run
// that resolved no directory of its own.
Distdir string
}
BuildRequest is one candidate's build-gate run: which staged tree to build, what to call it in the report, how deep to go, and where a failed run's log is kept.
Key and Version are the REPORT's names, not the build's: Portage stamps its errors with the STAGING repository's name, a repository the operator never created, and those errors are re-labelled to Key and Version before they reach a report.
RequireIsolation is read by RunBuildGates itself: with isolation required and unavailable, NOTHING IS SPAWNED and every covered gate reports SKIPPED naming why. A flag no code reads would leave the operator believing a build was refused when it in fact ran.
type Class ¶
type Class int
Class is how far a version bump moved, and therefore how much validation the bump earns.
The four rungs are ordered by DEPTH, cheapest first, so that a policy can say "at least ClassSeries" with a plain comparison and so that ClassMajor — the class every uncertain case falls back to — is the greatest. Do not reorder them: the ordering is the ladder, not a labelling.
const ( // ClassRevision is a bump where nothing upstream moved: only the Portage // revision suffix (-rN) differs, or the two versions are identical // (a revive, a retry). The tarball is the same one already validated. ClassRevision Class = iota // ClassPatch is a bump below the series component: the upstream release // changed but the series it belongs to did not. ClassPatch // ClassSeries is a crossing into another upstream series, the component // below the leading one. Build options and dependencies routinely change // here even though the leading component did not move. ClassSeries // ClassMajor is a move in the leading component — and the class every // version this package cannot read falls back to, because the greatest // depth is the only safe answer to "I do not know how far this moved". ClassMajor )
func Classify ¶
Classify reports how far a bump moved, from the two version strings and nothing else. It touches no filesystem, network, clock or subprocess: the class decides how much machine time a bump will cost, so finding it must cost nothing and give the same answer on every machine.
The class follows the first component that differs, with the Portage revision suffix set aside:
leading component -> ClassMajor the one below it -> ClassSeries anything deeper -> ClassPatch only the -rN suffix -> ClassRevision
The answer is symmetric (DISTANCE, not direction); refusing a downgrade is the caller's decision. If either version cannot be read, Classify returns ClassMajor AND an error naming it. Both, never one: the class alone would validate at maximum depth for a reason nobody could see, and the error alone would leave the caller with no class to act on.
func ClassifyForDepth ¶
ClassifyForDepth classifies a bump for ResolveDepth without offering the caller a way to throw the classification away.
Classify returns ClassMajor AND an error for a version it cannot read: both halves matter, the class because it is the safe answer and the error because it explains why the bump is about to be validated at the greatest depth. The reflexive `if err != nil { return }` at a call site discards the class this contract exists to provide, and the bump then gets no validation at all instead of the deepest — the exact inversion of what the fallback is for.
So this returns a NOTE rather than an error: there is nothing here to abort on. The note is empty when both versions read cleanly, and otherwise says what could not be read, for the caller to log or carry into the report beside the depth.
type Declared ¶
Declared is what the upstream archive says its build options are.
Sources records the archive members actually read. It is not bookkeeping: it is the gate's evidence, and the report prints it. A PASS whose sources are not shown cannot be audited — the operator has no way to tell "read the option file and found nothing wrong" from "found no option file to read".
func OptionsFromArchive ¶
OptionsFromArchive reads the build options an upstream archive declares.
It never unpacks the archive: one listing pass picks the members worth reading, and each is extracted to stdout (see extractArchiveMember). Only the members it parses are extracted.
The namespacing rule ¶
Meson puts one option file at each PROJECT root. What lives under subprojects/<name>/ is a separate project with its own root, and its options are addressed as -D<name>:<option>=. Filing a subproject's options under the root would compare them against declarations that never list them — every one would surface as an undeclared option, and a gate whose first finding is false is a gate someone switches off.
A subproject is recognised by its option file alone. Requiring a meson.build beside it would drop real subprojects that ship only options, and the cost of a false subproject is nil: nothing compares against a namespace the ebuild never addresses.
type DeclineCause ¶
type DeclineCause string
DeclineCause says WHY a SKIPPED gate declined, in the ONE dimension a promotion decision turns on: was the thing that stopped the gate a fact about the CANDIDATE, or a fact about THIS MACHINE.
It is a field and not a sentence because PromotionDecision REFUSES a bump on this distinction. Every skip's Reason already says which it is, in prose, but prose gets reworded, and reading the cause back out of it would let a wording change silently start (or stop) publishing unmeasured ebuilds. Nothing pattern-matches a Reason string.
It carries unbuildableHereReason's split one level up: a list that measured nothing ABOUT THE CANDIDATE is refused, while one that measured nothing because THIS MACHINE could not answer is promoted — refusing those would make `overlay autoupdate --apply` inert on an ordinary workstation.
const ( // DeclineUnrecorded is a skip whose producer has not named its cause. It // keeps today's answer — it does NOT refuse — and that fail-open is // DELIBERATE, not a gap waiting to be closed. // // The refusal names a KNOWN vacuity. A rule that refused on silence would // refuse every skip no producer has been taught to tag yet, which is the // flat "every deciding gate SKIPPED → refuse" reading that takes // `overlay autoupdate` down on any host that cannot build the package. // Each producer that learns to name its cause TIGHTENS the rule; nobody // should "fix" this into fail-closed in one edit. DeclineUnrecorded DeclineCause = "" // DeclineCandidate: something about THIS EBUILD stopped the gate — no // Manifest could be produced for it, its tree could not be staged. Nothing // read the candidate, and the bentoo overlay auto-commits and pushes within // minutes, so promoting on this publishes an unmeasured ebuild. DeclineCandidate DeclineCause = "candidate" // DeclineHost: something about THIS MACHINE stopped it — a build dependency // is not installed, no privilege to isolate, no `ebuild` on PATH. The // machine says nothing about the bump, so the bump is promoted with the // depth it did not reach named. DeclineHost DeclineCause = "host" )
type Depth ¶
type Depth int
Depth is how far validation runs for a given bump — the ladder a policy points at when it decides what a bump has earned.
THE LADDER IS CUMULATIVE. DepthConfigure means "run everything up to and including configure", not "run configure". A runner that reads the constants as a menu skips the patch gate on a configure request — a validation hole that still reports as a pass.
THE INTEGER ORDERING IS THE CONTRACT. The rungs are declared shallowest first so that `<` means "shallower than" and the policy resolver can combine a floor with a reviewer's proposal as max(floor, proposed). Reordering the constants silently changes which validation a bump gets.
QMERGE IS DELIBERATELY ABSENT. The ladder stops at DepthInstall, which assembles the package IMAGE under ${D} inside PORTAGE_TMPDIR. qmerge touches the running system and answers "do I want this version" — the package manager's question, not a validator's.
const ( // DepthNone runs no validation at all. It is the shallowest rung and the // zero value, but it is never reached by accident: ParseDepth refuses the // empty string rather than answering DepthNone, so a mistyped config key // cannot switch validation off in silence. DepthNone Depth = iota // DepthOptions compares the build options an ebuild declares against the // options the new upstream archive actually offers. Nothing is unpacked // beyond reading the archive, so this is the cheapest rung that can still // fail a bump. DepthOptions // DepthPatches additionally applies the ebuild's patches to the new // sources, catching the patch that stopped applying when upstream moved. // It includes DepthOptions. DepthPatches // DepthConfigure additionally runs the package's configure phase, which is // where a changed dependency or a dropped build option surfaces as an // error. It includes DepthPatches, and therefore DepthOptions. DepthConfigure // DepthCompile additionally compiles the sources. It includes // DepthConfigure, and therefore everything below it. It stops short of // src_install, and a compile pass SAYS so — the rung above is where that // phase runs. DepthCompile // DepthInstall additionally runs src_install, which assembles the package // image under ${D}: the phase where a `doins` of a file upstream renamed // dies, where an `emake DESTDIR="${D}" install` rule that stopped working // surfaces, and where most of Portage's QA notices are produced. It is the // deepest rung, and it includes DepthCompile and therefore everything below // it. // // It costs a compile plus src_install, not two compiles: the phases cascade // inside a single `ebuild` invocation. // // The gate additionally disables src_test, which runs between compile and // install, so that the verdict is a fact about the CANDIDATE rather than // about the host's FEATURES. A pass states both omissions. DepthInstall )
func Escalate ¶
Escalate combines the depth policy already resolved — the floor — with a depth the bump reviewer proposes, answering the depth to validate at and the case for it.
THE RULE IS max(floor, proposed), AND IT IS ONE-WAY. A reviewer reads a diff; it lacks an operator flag's authority, so it may buy more scrutiny and never sell any. The ladder's integer ordering (see depth.go) is what makes max mean "the deeper of the two".
IT TAKES PRIMITIVES, NOT autoupdate.BumpReviewReport: internal/autoupdate imports this package, so accepting that type is an import CYCLE. The caller calls this only when the report's ProposedDepth pointer is non-nil, keeping "proposed nothing" and "proposed none" different facts.
A PROPOSAL THAT RAISES DEPTH AND STATES NO REASON IS REFUSED, like a reasonless lowering override: a depth an operator cannot account for is one they will switch off. The floor is returned and the reason says why. The reason credits the reviewer ONLY when it actually added a gate.
func ParseDepth ¶
ParseDepth reads a depth by name, as written on the `--depth` flag or in a config key.
It matches against String() rather than a second table of literals, so the two directions cannot drift apart: any name a report can print is a name this accepts, and nothing else is.
The match is exact — no trimming, no case folding. Depth decides how much machine time a bump costs; accepting " Compile " as compile would mean the name in the config and the name in the report are no longer the same string, and the first surprise there is a config that reads as one depth and runs as another.
Every rejection names the offender AND lists the valid set. An error that said only "invalid depth" would send the operator to read the source for five short words.
func (Depth) String ¶
String is the name the ladder is known by outside this package. It is lower-case and unpadded because the very same spelling is what `--depth` and the config key accept — String and ParseDepth are exact inverses, and a report that printed "Configure" would name a depth nobody could request.
type DepthDecision ¶
type DepthDecision struct {
// Depth is how far this bump is validated.
Depth Depth
// Reason names the input that decided and, where one exists, quotes the
// stated justification. It is never empty.
Reason string
// SkippedByPolicy reports that this bump received LESS validation than
// policy chose for its class, and therefore must be counted as skipped
// rather than validated. An explicit operator flag never sets it:
// that is the operator's own instruction, not a package opting out.
SkippedByPolicy bool
}
DepthDecision is a depth and the case for it.
func ResolveDepth ¶
func ResolveDepth(req DepthRequest) DepthDecision
ResolveDepth answers how deep a bump is validated and why.
It applies the four levels of authority in order, and it is total: every request produces a depth and a non-empty reason, and no input other than an explicit operator flag can lower the depth without that being reported.
type DepthOverride ¶
type DepthOverride struct {
// Depth is the depth to use in place of the class default.
Depth Depth
// Reason is why this package is not validated the way its class says. It
// travels into the decision verbatim so the report quotes the operator
// rather than paraphrasing them.
Reason string
}
DepthOverride is a per-package instruction to validate at a depth other than the one policy chose, together with the reason it exists.
The reason is a field and not a comment because it is mandatory and consequential: an override that lowers depth removes a gate, and the report has to be able to say on whose authority. An override that lowers WITHOUT a reason is refused rather than applied — see ResolveDepth.
type DepthPolicy ¶
type DepthPolicy struct {
// ByClass is the depth each bump class earns. A class missing from
// the map does not mean "no validation": see classDepth.
ByClass map[Class]Depth
// Overrides is keyed by the full package atom ("category/package"), the
// same spelling the registry uses.
Overrides map[string]DepthOverride
}
DepthPolicy is the configured half of a depth decision: what each class earns, and which packages are exceptions.
It is expressed in this package's own types rather than in the config package's strings so that internal/autoupdate/validate stays free of a dependency on internal/common/config — the import already runs the other way (autoupdate imports validate), and a resolver that reads config would put the depth table behind a file format. Translating the configured strings into Depth values, and rejecting a typo by name through ParseDepth, is the job of whoever builds this struct.
func DefaultDepthPolicy ¶
func DefaultDepthPolicy() DepthPolicy
DefaultDepthPolicy is the depth table this toolkit SHIPS: a revision or patch bump is read but never built, and a series or major bump is taken as far as configure.
An empty policy must not mean this one's fallback. classDepth falls back to DepthCompile, which is right for a hole in a filled-in table but wrong for a table nobody filled in: then every revision bump would compile, a cost nobody agreed to. So a caller that supplies no table gets the shipped one.
config.DefaultDepthRevision and its three siblings hold the same values as strings, because the config layer answers GetDepthForClass for a key the operator did not write, and validate cannot import config. THE TWO MUST BE READ TOGETHER: a change to either is a change to both.
type DepthRequest ¶
type DepthRequest struct {
// Package is the full atom ("category/package"), used to find an override.
Package string
// Class is how far the bump moved, from Classify. Callers must pass the
// class Classify returns even when it also returns an error — see
// ClassifyForDepth.
Class Class
// ResolvedType is the checker's answer ("bin" or "source"), NOT the raw
// PackageConfig.Type field. That field is empty for most records and the
// type is auto-detected from the ebuild (RESTRICT=bindist, a -bin suffix, a
// binary SRC_URI); a resolver reading the raw field would see "" for almost
// every binary package in the registry and schedule a compile for a
// prebuilt blob. It arrives as a plain string because the resolved type
// lives on autoupdate.CheckResult, and this package cannot import
// autoupdate without an import cycle.
//
// Empty means nobody resolved it. That is NOT the same as "source": an
// unresolved record keeps every gate its class earns, because losing gates
// must never be something that happens by omission.
ResolvedType string
// Policy is the configured depth table and its per-package exceptions.
Policy DepthPolicy
// FlagDepth is `--depth`, nil when the operator did not give it. It is a
// pointer because DepthNone is a meaningful value an operator may ask for,
// and the zero value would otherwise be indistinguishable from "unset" —
// the one confusion that switches validation off in silence.
FlagDepth *Depth
// Compile is `--compile`, which is exactly `--depth=compile`.
Compile bool
}
DepthRequest is everything ResolveDepth is allowed to look at. It performs no I/O of its own, so a depth decision is reproducible from these fields alone and can be replayed in a report.
type EbuildResult ¶
type EbuildResult struct {
Package string `json:"package"`
Version string `json:"version"`
Depth string `json:"depth"`
DepthRequested string `json:"depth_requested"`
DepthReason string `json:"depth_reason,omitempty"`
Gates []GateResult `json:"gates"`
Sources []string `json:"sources"`
// Notes are facts about how the result was reached that no gate carries,
// rendered under the ebuild's row.
Notes []string `json:"notes,omitempty"`
}
EbuildResult is everything the run has to say about one ebuild version.
Gates is a list, not one field per gate: with per-gate fields, ExitCode once filtered on GateOptions alone, so an error from the gate that runs the build exited 0 while the report printed a failure. A new gate costs a constant.
Depth is how far validation actually got; DepthRequested is how far it was asked to go; DepthReason is the "because" between them, absent when they agree — an outcome names its own reach, ladder included. All three are strings, the spelling `--depth` accepts and Depth.String prints, because Depth is an int whose ORDERING is its contract and must not reach the wire.
The json tags are the contract of `overlay validate --json`: written out so a Go rename cannot silently rename a key under a consumer's jq expression. Only genuinely absent fields carry omitempty; Package, Version, Depth, DepthRequested, Gates and Sources are always present.
func (EbuildResult) WorstOutcome ¶
func (r EbuildResult) WorstOutcome() Outcome
WorstOutcome is the one outcome that stands for the whole ebuild, for a renderer that has one column and five gates to fit in it.
FAILED beats SKIPPED beats PASS, and PASS is only reached when EVERY deciding gate passed. A result carrying no deciding gate at all answers SKIPPED: it has said nothing about this ebuild, and "nothing" read as a pass is the defect this outcome exists to prevent.
pkgcheck is excluded, exactly as it is from ExitCode ¶
The QA gate skips whenever pkgcheck is not installed, which on such a host is every ebuild in the tree. Folding that into the headline would turn a clean whole-overlay run into "0 passed, 500 skipped" and put the summary line at odds with the exit code beside it — the same argument that keeps QA findings out of ExitCode, applied to the outcome instead of the finding. The QA gate is still rendered, still named, and still carries its own reason.
type Finding ¶
type Finding struct {
Gate string `json:"gate"`
Severity Severity `json:"severity"`
Detail string `json:"detail"`
}
Finding is one thing the gate has to say about an ebuild.
The json tags are the wire contract — see EbuildResult for why they are written out rather than inherited from the Go field names.
func Compare ¶
Compare subtracts the two option sets and returns the findings. It is pure — no I/O, no clock, no filesystem — so every case is a table row, not a fixture.
- A project option the archive does not declare is an `error`: it is what `-Daalib=` became when upstream removed the option at 1.29.
- A declared option the ebuild never passes is `info` — upstream gained something we have not adopted. Useful, never blocking.
- Each unresolved name is a `warning` quoting the text as written, because a name the gate could not read is a gap in its own answer.
Built-ins are never compared: Meson defines them and no option file declares them, so comparing one would manufacture an error out of a correct ebuild. Matching happens WITHIN a namespace: `sub:foo` is checked against `sub`'s declarations, never the root's, or every legitimate subproject option would surface as undeclared. Every Detail names its source (the ebuild line for a passed option, the archive member for a declared one) so a finding can be traced without grepping.
type GateResult ¶
type GateResult struct {
Gate string `json:"gate"`
Outcome Outcome `json:"outcome"`
Reason string `json:"reason,omitempty"`
Findings []Finding `json:"findings,omitempty"`
// Declined is why a SKIPPED gate declined. It is meaningless on PASS and
// FAILED, which measured something and therefore declined nothing.
Declined DeclineCause `json:"-"`
}
GateResult is what ONE gate has to say about one ebuild.
The reason lives per gate, not per ebuild: a single shared Reason let the last gate to write it overwrite another gate's cause.
Invariant: Outcome SKIPPED ALWAYS carries a non-empty Reason — a skip nobody can read is a pass. The converse does not hold: a PASS may carry a reason too, because an outcome names its own reach ("a configure pass does not cover compilation"). Each Finding also carries its own Gate so a renderer flattening every gate's findings into one list can still say which check spoke.
Declined is `json:"-"` as a requirement: this document is pinned BYTE-FOR-BYTE, and the same struct is StageRecord.Gates on disk, so a new key would change bytes already written by every installed copy of the tool. The price: a gate list ROUND-TRIPPED through a StageRecord comes back DeclineUnrecorded, so the vacuity rule decides only on a list held in memory by the run that produced it, and a reload fails open.
func RunBuildGates ¶
func RunBuildGates(ctx context.Context, req BuildRequest, deps BuildDeps) ([]GateResult, error)
RunBuildGates runs the staged candidate's build phases ONCE and reports every build gate the depth covers from that single run.
It invokes the DEEPEST phase the depth requires exactly once and derives every shallower gate from the markers `ebuild` prints: the patch gate passes at `>>> Source prepared.` (src_prepare applies the patches the way the build will), the configure gate at `>>> Source configured.`, the compile gate on a zero exit. A failure is attributed to the last phase that STARTED; a run that dies before prepare finished proved nothing about the patches and reports SKIPPED. Every covered gate carries its own outcome, never silence.
The build runs AS THE INVOKING USER, with an allow-listed environment (see buildEnvAllowed): escalation would stop a sweep at a password prompt. When isolation is REQUIRED and unavailable nothing is spawned and every gate reports SKIPPED; otherwise every PASS says `unverified isolation`.
The error is about the REQUEST (a malformed atom, no version, no staged tree), never the build: a failed build, or a host with no `ebuild`, returns gates and a nil error. No timeout: a legitimate compile can run for hours.
func SkippedGates ¶
func SkippedGates(d Depth, reason string) []GateResult
SkippedGates reports one SKIPPED gate, carrying reason, for every build gate depth d covers — and for no other gate.
It is EXPORTED because the applier's dependency pre-check is a second caller: the gates that owe an outcome when this host cannot answer must be the set RunBuildGates would have reported. Which gates those are is eachBuildGate's answer, shared with the runner, so the set a depth owes an outcome for does not depend on whether the run happened. THE LADDER IS CUMULATIVE: a compile-deep request reports patches, configure AND compile, each said out loud, because an unreported gate is indistinguishable from one that passed. A depth below DepthPatches yields nothing: an options-deep run needed no tree.
The reason is a required argument, as in report.go's skippedResult, so "SKIPPED always carries a reason" stays structural. The SIGNATURE IS FIXED because callers outside this package hold it: declinedGates is the sibling that names a cause, and this passes DeclineUnrecorded.
type Option ¶
type Option struct {
Name string // "aalib"
Subproject string // "" for the root project
Source string // archive member or ebuild line it was read from
}
Option is one build option name, on either side of the comparison.
Source is the only field that is not part of the option's identity, and it is the one the report cannot do without: it names the archive member a declared option was read from, or the ebuild line a passed one was written on. A finding that cannot be traced back to the line that caused it makes the operator grep for it, which is the difference between an actionable gate and an annoying one.
type Options ¶
type Options struct {
// Overlay is the tree to validate.
Overlay string
// Distdir names the directory to read archives from. Empty falls through to
// the host's own `portageq distdir` — see distfiles.Locate, which never
// creates the directory and never proves it writable.
//
// There is no configured rung between the two, because no configuration key
// exists for it; the command passes --distdir here or nothing.
Distdir string
// Selector is "", "<category>" or "<category>/<package>". Empty validates
// every ebuild in the overlay.
Selector string
// Depth is how far up the ladder this run goes, spelled the way `--depth`
// and the config key spell it: none, options, patches, configure, compile
// or install, each including every rung before it.
//
// EMPTY MEANS "options", AND THAT IS THE WHOLE COMPATIBILITY PROMISE.
// Every caller written before the ladder existed leaves this field
// zero, and each of them asked for exactly the static option gate. ParseDepth
// refuses "" rather than answering DepthNone, precisely so a mistyped key
// cannot switch validation off in silence — so the empty-to-options mapping
// is made HERE, once, instead of being left to each caller to remember.
//
// It is a string and not a Depth so the word the operator typed survives into
// the run and can be quoted back at them when it does not parse.
Depth string
// StagingRoot is the directory a run above DepthOptions prepares its staged
// trees under, <StagingRoot>/<category>/<package>/<version>.
//
// It is unused at or below DepthOptions, which is why a --depth-less run
// leaves it empty: the static gate reads files that are already on disk and
// writes nothing, and a scratch directory nothing uses is a directory that
// should not be created.
//
// IT MAY NOT RESOLVE INSIDE THE OVERLAY, and Stage refuses one that does
// rather than trusting its callers: ScanOverlay walks the overlay
// category/package deep and Reconcile turns every ebuild no registry pin
// claims into a deletion candidate, so a staged tree parked under the overlay
// root would be reported as a defect by this very command and deleted by
// `overlay autoupdate --clean`.
StagingRoot string
// RequireIsolation refuses to run a build this host cannot isolate, exactly
// as BuildRequest.RequireIsolation does for the applier — it IS that field,
// carried to the one caller that had no way to set it.
//
// IT EXISTS BECAUSE ITS ABSENCE WAS A POLICY BYPASS, not a missing feature.
// `autoupdate.validate.require_isolation` is honoured by the identical gates
// under `overlay autoupdate`, and those gates were once unreachable from
// `overlay validate`: every one of them SKIPPED, so nothing
// this command did could be unisolated. Wiring the seams made them run, and
// a build path that ignores the setting is not a command missing an option —
// it is the operator's decision that builds must be isolated, silently not
// applying to one of the two commands that build.
//
// The zero value is false, which is the shipped behaviour of both commands
// when the key is unset: an unisolated build still runs and its pass
// is labelled "unverified isolation" rather than refused, because creating
// the namespace needs privilege an ordinary user does not have.
RequireIsolation bool
// DistNames answers, for ONE package directory, which upstream archives the
// option gate may look for. It is the seam for a directory that cannot name
// them itself — a staged tree has no Manifest until a manifest step runs, so
// without it the gate would read nothing and report SKIPPED.
//
// NIL IS NOT AN EMPTY SLICE. Nil means nobody supplied anything: the gate
// parses pkgDir/Manifest and the report is byte-identical to the one before
// this field existed. A non-nil function returning no names is an ANSWER —
// the caller looked and there is nothing to read — and is never quietly
// replaced by a Manifest the caller has already spoken for.
//
// It is a func of pkgDir, never a bare []string, because one Run walks MANY
// packages: a flat list would answer each ebuild with whichever name matched,
// a confident verdict about the wrong tarball. It is per-CALL, never a field
// on an applier, because a seam stored once would hand package A's names to
// package B — a race under concurrent applies, wrong later under sequential
// ones.
DistNames func(pkgDir string) ([]string, error)
// Logger receives the run's diagnostics. Nil discards them.
Logger *slog.Logger
// StagedManifest answers, for ONE package directory, the Manifest content
// that package's STAGED tree must carry before a build gate can run in it.
//
// The caller owns it because this package cannot: Portage refuses an ebuild
// whose Manifest does not describe its archive, Stage deliberately does not
// copy the published Manifest (it describes the published versions), and
// the step that GENERATES one, `pkgdev manifest`, lives in package
// autoupdate, which imports this one — importing back would be a cycle.
//
// NIL MEANS NOTHING TRAVELS: nothing staged, nothing written, every build
// gate SKIPPED naming what stopped it. A non-nil function's bytes are written
// verbatim, because Portage VERIFIES their digests.
//
// A same-version caller (`overlay validate --depth`) feeds the PUBLISHED
// Manifest, which describes the archive on disk; a bump caller feeds the
// GENERATED one, since the published digests belong to the release being
// replaced. It is a per-CALL func of pkgDir for the reasons DistNames gives.
StagedManifest func(pkgDir string) ([]byte, error)
// LogDir is where a FAILED build gate's whole transcript is retained.
//
// The gate's findings quote only a SUMMARY; the transcript holds everything
// the phase printed — the compiler invocation, preceding warnings, the exact
// line of a generated file — and is often what turns "configure failed" into
// a diagnosis. It does NOT decide whether the findings name the cause: that
// is failureExcerpt's selection, over the in-memory transcript, so a gate
// names a removed upstream option with or without a LogDir.
//
// Empty retains nothing and the gate's reason says so, so the operator is
// told the log was not kept rather than left to wonder. It is a plain
// directory, not a func of pkgDir: one place for the whole run, and
// RunBuildGates already names each log after its atom and version.
LogDir string
}
Options is what a run needs, and a run is fully described by it: nothing here is discovered.
The plain values come from the command line or the config. The last two do not — they are SEAMS, functions the caller supplies so that a package directory which cannot answer for itself can still be validated: DistNames names the upstream archives the option gate may look for, StagedManifest supplies the Manifest a staged tree must carry before Portage will build in it. Their zero value is nil, and nil is exactly the behaviour every field of this struct had before they existed, which is why they are listed last: a caller written against the older struct is still describing the same run.
type Outcome ¶
type Outcome string
Outcome is what a gate managed to say about one ebuild.
SKIPPED is the reason this is three values and not a boolean. "The gate could not run" and "the gate ran and found nothing wrong" are different answers, and collapsing them is the defect this type exists to prevent: a clean report must never be readable as "we did not look".
const ( // OutcomePass means both sides were read and they agree. OutcomePass Outcome = "PASS" // OutcomeFailed means both sides were read and at least one error finding // came out of the comparison. OutcomeFailed Outcome = "FAILED" // OutcomeSkipped means the gate could not run. It ALWAYS carries a reason. OutcomeSkipped Outcome = "SKIPPED" )
type Passed ¶
type Passed struct {
Project []Option
BuiltIn []Option
Unresolved []Unresolved
}
Passed is what the ebuild hands the build.
The slices are exhaustive and disjoint: every `-D…=` an ebuild writes lands in exactly one of them. That is the property the comparison rests on — an unwritten fourth category ("names we could not make sense of, dropped") would be a silent hole, and a silent hole in a parity check reads as a pass.
func OptionsFromEbuild ¶
OptionsFromEbuild reads the option names an ebuild passes to the build.
Only the name side ¶
Just the text between `-D` and the first `=` is considered. Values are irrelevant to a parity check, and ignoring them is what makes the unresolved rule precise instead of noisy: `-Dexamples=$(usex test enabled disabled)` is an ordinary ebuild line whose NAME is perfectly static, while `-D${plugin}=` is a name assembled at runtime. A reader that looked at whole arguments would flag the first, and a gate that warns about every normal ebuild is a gate someone switches off.
`--enable-<name>` and `--with-<name>` are deliberately not extracted: with only Meson declarations on the upstream side there is nothing to compare them against, so the result would be an output no requirement consumes.
Source is set to "<ebuild file name>:<line>" so every finding can point at the line that caused it.
type PreparedBuild ¶
type PreparedBuild struct {
// StagedRoot is the tree Stage produced, and it is empty only when no tree
// was ever made. It travels even on the failing paths: the tree exists, and
// it is where anyone diagnosing this looks first.
StagedRoot string
Gates []GateResult
Reason string
// StageErr is non-nil when NO TREE WAS EVER PREPARED — the candidate's bytes
// could not be read, or Stage refused. It wraps ErrStageUnpreparable on
// Stage's own paths, which is why it is chained rather than restated: a
// caller reacts to staging having failed without enumerating the ways it
// can fail.
StageErr error
// GatesErr is non-nil when the gate ladder could not be started or was
// interrupted. Neither is a verdict on the ebuild — the first is a caller's
// bug about the REQUEST, the second is a build that ran and was killed — so
// a caller that records either as "it does not build" is recording something
// no gate said.
GatesErr error
}
PreparedBuild is what running one prepared build produced.
Gates and Reason are the report: the ladder's results in order, and the run-level reason the depth went unreached — empty when the gates themselves answered, because each then carries its own.
StageErr and GatesErr are the same two faults UNRENDERED, and they are here because the two callers owe their operators opposite things. Run turns every stopping condition into a reported skip and carries on with the rest of the overlay; realign.Prove must not, because a realignment reported as "does not build" when nothing was ever built is a change discarded over a fact about the disk. Both fields are non-nil ALONGSIDE the rendered skip, never instead of it, so a caller reading only Gates and Reason sees exactly what it saw before these fields existed.
func RunPreparedBuild ¶
func RunPreparedBuild(ctx context.Context, req PreparedBuildRequest) PreparedBuild
RunPreparedBuild is the prepared build, for a caller that selected its own candidate.
It is the SAME function buildDepthGates goes through — it calls the seam variable, not runPreparedBuildGates directly — and that is the whole point of it existing: one copy of the ladder, reached by both entry points, so a second caller cannot acquire a rule of its own about what gets gated.
It selects nothing and normalises nothing beyond the one rule it MUST NOT restate: the nil Manifest seam, read from normaliseStagedManifest so that both entry points take the same branch for the same reason.
type PreparedBuildRequest ¶
type PreparedBuildRequest struct {
// Overlay is the published tree Stage copies the eclasses and profiles out
// of; StagingRoot is where the single-package repository is built. Stage
// refuses a staging root that resolves inside the overlay, which is what
// keeps "never the overlay" a property of the code.
Overlay string
StagingRoot string
// Key is the registry key — category/package, possibly carrying ":slot" or
// "@label" — and Version is the version being built. They name the candidate
// to Portage; nothing here parses them back out of a filename.
Key string // registry key: category/package, possibly :slot or @label
Version string
// PackageDir is the PUBLISHED package directory the Manifest seam is asked
// about — not the staged copy. The staged tree is the thing that LACKS a
// Manifest, which is the whole reason the seam is being asked.
PackageDir string
// Ebuild is the candidate's bytes, written into the staged tree as they are.
//
// A caller that holds the bytes passes them; there is no path here for a
// caller that holds only a file, because the one entry point that reads from
// disk (buildDepthGates) reaches the core directly and keeps its read lazy.
Ebuild []byte
// Depth is how far up the ladder to go, passed through unchanged.
Depth Depth
// StagedManifest answers, for PackageDir, the Manifest content the staged
// tree must carry before a build gate can run in it. Nil means NOTHING
// TRAVELS — nothing is staged and nothing is built — which is exactly the
// rule Options.StagedManifest states, read from the one helper both go
// through rather than restated here.
StagedManifest func(pkgDir string) ([]byte, error)
// RequireIsolation is carried, not defaulted. The refusal inside
// RunBuildGates fires only when the request carries it, and a policy that
// applies to one of the two commands that build is not a policy.
RequireIsolation bool
// LogDir is where the whole transcript is kept for whoever has to go past
// the summary — the run that needs one is exactly the run that FAILED.
// Empty is still accepted and the gate's reason still says so.
LogDir string
// Distdir is the directory the build reads its archives from, set on the
// child as DISTDIR. It is the caller's answer and never one
// derived here — see BuildRequest.Distdir for why it travels as a field
// instead of through the environment.
//
// Empty sets nothing and invents nothing, which is the same
// answer a caller that never knew about this field gets.
Distdir string
// Deps is the command seam RunBuildGates and the host probe run through. Its
// zero value means the real commands, which is what every production caller
// passes.
Deps BuildDeps
}
PreparedBuildRequest is one already-chosen candidate plus everything the prepared build needs — the UPPER half of the two-half operation whose lower half (Stage, RunBuildGates) is exposed as a seam of its own.
Exporting only the lower half is what let a second entry point reach the build gates having prepared none of the upper one: no Manifest seam, no Manifest written into the staged tree, no host probe, and a BuildRequest whose two policy fields were left at their zero value. This type is the shape of what that caller was missing, so that asking for a build and asking for HALF a build stop being the same call.
Every field is an answer the caller already holds. Nothing here is re-derived by the core, because re-deriving the candidate would be SELECTING it, and selecting is the half the two callers legitimately do differently: Run walks the overlay, realign holds the one candidate it just built.
type Report ¶
type Report struct {
Overlay string `json:"overlay"`
Results []EbuildResult `json:"results"`
UnmatchedSelector string `json:"unmatched_selector,omitempty"`
}
Report is one whole run.
UnmatchedSelector carries the selector when it named a category or package the overlay does not hold. It is a field rather than an error because the run still produced a report — an empty one — and the command has to exit 2 while still rendering something.
func Run ¶
Run validates every ebuild the selector names and returns one outcome per ebuild version.
The governing rule: a condition that stops a gate becomes a REPORTED OUTCOME WITH A REASON — never a silent pass, never an aborted run. A missing distfile, a non-Meson build system, an unreadable ebuild: each is one ebuild reporting SKIPPED and why, while the run carries on. The only error returned is an overlay that could not be scanned, which leaves nothing to report.
Nothing here reaches the network; a test over this package's imports asserts it, because behaviour cannot prove a negative.
Every rung of Options.Depth gets an answer. At or below DepthOptions it is the static gate. Above it the build gates run against a staged tree when the caller supplied its Manifest (Options.StagedManifest), and are otherwise reported SKIPPED NAMING WHAT STOPPED THEM: omitting a gate the operator asked for would be exactly the silence the governing rule forbids.
func (Report) ExitCode ¶
ExitCode returns the process exit code for the run.
0 — every gate outcome was PASS or SKIPPED 1 — at least one finding of severity error, from any gate but pkgcheck's 2 — the selector named something the overlay does not hold
This is NOT internal/autoupdate.BatchResult.ExitCode (0 none, 2 TOTAL, 1 PARTIAL failure), and the two must not be unified. BatchResult counts ITEMS IT COULD NOT PRODUCE; this gate turns every would-be failure into a produced outcome that names why it was SKIPPED. Unified, `2` would carry two meanings in one binary — a bug that surfaces only in someone's CI script.
An error finding fails the run whatever gate produced it, with two structural exceptions. pkgcheck findings are excluded: the overlay carries pre-existing QA findings unrelated to any bump, and a metadata.xml typo must not fail `overlay validate` across the whole tree. The reviewer needs no exclusion: it never emits at error, so a model's opinion cannot fail a bump. The exclusion selects on the GATE THAT RAN, not on a finding's Gate label.
func (Report) HasErrors ¶
HasErrors reports whether any deciding gate recorded an error finding. It is what the text renderer keys its summary line on, so the summary and the exit code cannot disagree.
func (Report) Normalized ¶
Normalized returns a copy whose nil slices are empty ones, so the JSON document carries `[]` rather than `null`. A consumer piping this into `jq '.results[].gates[]'` should not have to special-case the difference between "no gates" and "the key was nil in Go".
A gate's own Findings are left alone, because they carry omitempty: nil and empty are both written as an absent key there, so neither can surface as a `null` for jq to trip over.
type Severity ¶
type Severity string
Severity ranks a finding. It is a string rather than an integer because it is rendered in a JSON document, where a number would force every consumer to carry the same mapping and get it right.
const ( // SeverityError is a passed option the upstream source does not declare — // the failure class issue #33 belongs to. It is the only severity that // reaches the exit code. SeverityError Severity = "error" // SeverityWarning is an option name that cannot be resolved statically. // Reported, because a name the gate could not read is a hole in its answer. SeverityWarning Severity = "warning" // SeverityInfo is an option upstream declares and the ebuild never passes. // Useful to know, blocks nothing. SeverityInfo Severity = "info" )
type StageRecord ¶
type StageRecord struct {
// Package and Version name the bump, for the human who opens the file. They
// are deliberately NOT what identifies the record: the path does that,
// so a tree moved into the wrong directory is already the wrong tree
// whatever these two say.
Package string `json:"package,omitempty"`
Version string `json:"version,omitempty"`
// ProducedBy names what wrote this record: ProducedByApplier or
// ProducedByValidate. A record written by a read-only validation run is
// evidence about a tree's outcome, NOT a licence to publish it.
//
// EMPTY MEANS ProducedByApplier, and the read path is where that is said —
// see ReadStageRecord. Every record already on an operator's disk was
// written before this field existed, and every one of them came from the
// applier.
ProducedBy string `json:"produced_by,omitempty"`
// Depth is the depth this run SELECTED, which is the depth the gates below
// were asked to cover — not the depth they reached. Reuse compares it
// against the depth the promoting run selects, and the comparison is "not
// below": a compile-deep proof answers a configure-deep question, a
// options-deep proof does not.
Depth Depth `json:"depth"`
// Gates is one entry per gate the run reported, with its outcome and its
// reason. A record with no gate in it proves nothing and is treated as no
// record at all — "something was checked" is not a statement anybody can act
// on.
Gates []GateResult `json:"gates"`
// EbuildDigest is the candidate's bytes AS THEY WERE STAGED — before the
// build fixer edited them.
//
// The pre-fixer bytes are the ones that matter, and getting this wrong is
// invisible: the build fixer edits the staged ebuild in place, so a digest
// taken from the tree as it stands after a repair would never again match
// what the next run computes from the overlay, and EVERY bump the fixer
// touched would revalidate — which are exactly the slow ones, the ones that
// needed a repair in the first place. The bump would still publish; it would
// just quietly pay for a second build every time.
EbuildDigest string `json:"ebuild_digest"`
// ValidatedDigest is the candidate's bytes AS THE GATES READ THEM, fixer
// and operator edits included, taken when the record is written for a run
// whose gates passed. It is empty on any other run. EbuildDigest says which
// bump the tree is about; this says which bytes were proved, so a staged
// ebuild edited after its proof no longer matches and is validated again.
ValidatedDigest string `json:"validated_digest,omitempty"`
// SubstitutionDigest covers the per-package rewrites the applier applies to
// the staged candidate after staging it (a tracked commit hash, an auxiliary
// variable). It is empty when the bump declares none, which is the ordinary
// case and the reason it is not folded into EbuildDigest: the two runs being
// compared read the same source ebuild, so only these values can make their
// candidates differ.
SubstitutionDigest string `json:"substitution_digest,omitempty"`
// DistfileDigest is the digest the published package Manifest records for
// the package's distfiles. It is the one input that is not in the overlay's
// text at all, and a tarball re-rolled upstream under the SAME name between
// staging and promotion moves it — at which point every gate's answer was
// about a source tree that is no longer the one being published.
DistfileDigest string `json:"distfile_digest,omitempty"`
// VendorUnpublished names the vendor tarball the proved run generated but did
// not publish, empty when it generated none or published the one it did. The
// tree's Manifest digests that tarball, so promoting the tree as it stands
// would publish an ebuild whose vendor distfile nobody serves; reuse refuses
// while it is set and the vendor step runs again. A record written before the
// field existed reads as empty.
VendorUnpublished string `json:"vendor_unpublished,omitempty"`
}
StageRecord is what one validation run leaves beside its staged tree so that a later run can promote it without paying for the same hours twice.
It is evidence, not a cache ¶
A cache answers "have I seen this key". This answers "was this exact candidate PROVED, how deeply, and over which inputs" — and it records failures just as faithfully as passes, because the staged tree of every bump that was NOT promoted is retained too. Feed retention straight into promotion with no record in between and yesterday's rejected bump publishes today; Gates is what stops that, and it is why the record stores each gate's own outcome rather than a single boolean.
func ReadStageRecord ¶
func ReadStageRecord(stagedRoot string) (StageRecord, error)
ReadStageRecord reads the record beside the staged tree at stagedRoot.
A missing record answers ErrNoStageRecord, and the reuse decision is: an unrecorded tree is revalidated. So is an unreadable or malformed one — a half-written record is not a weaker claim than a whole one, it is no claim, and the only safe reading of "I cannot tell what this tree proved" is to prove it again.
func (StageRecord) Proves ¶
func (r StageRecord) Proves(want Depth) (bool, string)
Proves answers the record's half of the promotion condition: does it show a candidate that was actually MEASURED, at a depth not below want. The string explains the answer either way: a promotion that cannot say why it skipped the gates is indistinguishable from a bump nobody validated.
At least one deciding gate must PASS. A run that stopped before any gate read the tree (no Manifest, an interrupt) records its requested depth beside a list of SKIPPED gates, and the reuse path consults neither refuseUnproved nor PromotionDecision, so accepting that would publish an unmeasured bump. A skip BESIDE a pass still proves: requiring every gate to PASS would revalidate, and re-skip, every bump whose host lacks a build dependency, on every run.
This keys on the OUTCOME where PromotionDecision keys on the CAUSE, and the two must not be unified: Declined is `json:"-"`, so a reloaded record reads DeclineUnrecorded and a cause-keyed refusal would never fire; and a wrong refusal here costs one revalidation, where there it stops an apply.
The QA gate never decides (pre-existing pkgcheck findings must not stop every bump), and an empty gate list proves nothing.
type StageRequest ¶
StageRequest is everything Stage needs to materialise one candidate.
Overlay is the published overlay the staged tree is built FROM. Staging only ever reads it; the candidate is never written there, which is the whole point of validating somewhere else.
StagingRoot is the directory the staged trees live under, chosen by the caller (in production <configDir>/staging) so a dry run can point it elsewhere. The path is the only place the retention rule is recorded — no index file, no lock — which lets several packages be staged concurrently.
Key is the registry key: category/package, possibly carrying ":slot" or "@label" — e.g. "app-editors/zed-bin@preview". Content paths derive from its suffix-stripped form (splitContentAtom); the stage root and the repository name keep the full key. Version is the PV; together they name the ebuild file.
EbuildBytes is the candidate's body, written verbatim: a reformatted body would make a gate result describe a file that never existed anywhere else.
type StagedCandidate ¶
type StagedCandidate struct {
Key string // registry key: category/package, possibly :slot or @label
Version string
}
StagedCandidate names one package/version the current run still needs.
type StagedSweepPlan ¶
type StagedSweepPlan struct {
StagingRoot string
Remove []string // trees this sweep would remove, sorted
Kept []SweptEntry // everything else, with the reason it stays, sorted
}
StagedSweepPlan is what one sweep WOULD do. Nothing in producing it removes anything.
The two lists partition everything the walk saw: a directory is a planned removal or a keep with a reason, never both and never neither. That is what makes a printed plan auditable — an entry in neither list is a tree the plan says nothing about, and an operator reads silence as "it will not be touched".
StagingRoot rides along because the executor acts on the root the PLAN names. A plan that did not carry it could only be executed against whatever root the caller happened to be holding, which is the one mistake this shape exists to make impossible.
Both lists are sorted, so two plans over one staging root diff cleanly.
func PlanStagedSweep ¶
func PlanStagedSweep(req SweepRequest) (StagedSweepPlan, error)
PlanStagedSweep walks one staging root, classifies every entry it finds, and leaves the filesystem exactly as it found it. Deciding apart from acting gives an operator a complete report before anything is removed — the same split as PlanOverlaySweep/ExecuteOverlaySweep, PlanPrune/ExecutePrune, PlanApply/Apply.
A tree is planned for removal when all three hold: this package RECOGNISES it as its own, it is not in req.InScope, and its record shows no deciding gate FAILED. Everything else is kept and reported. Not "keep the last N": a passed tree has served its purpose, while a failed one is the artifact an operator still needs next to the retained log. A tree with no readable record has an UNKNOWN outcome and is kept — a wrong keep costs disk, a wrong removal costs the artifact an operator was about to open.
ensureOutsideOverlay — the SAME check Stage uses — is the FIRST statement, so the published overlay is refused before it is ever listed. inScopeTreePaths comes next and propagates its error unchanged: a scope that cannot be named is refused, never silently swept. Both refusals return the zero plan, because a refusal carrying a populated plan is an invitation to execute it.
type SweepReport ¶
type SweepReport struct {
Removed []string
Kept []SweptEntry
}
SweepReport is everything one sweep did: the trees it removed, and every entry it left behind with the reason it left it.
Both lists are sorted, so two sweeps over one staging root produce a report an operator can diff.
func ExecuteStagedSweep ¶
func ExecuteStagedSweep(ctx context.Context, plan StagedSweepPlan) SweepReport
ExecuteStagedSweep removes exactly the trees plan.Remove names, and reports what it did and what it left.
It acts on the plan and never walks again: the plan is what an operator was shown and approved, and a second walk would reach its own verdicts on a root that has moved since — a tree staged between the prompt and the answer.
It returns no error, like ExecuteOverlaySweep: a tree that could not be removed is reported as a keep carrying the error, and a batch-level error would either repeat it or be read as "the sweep failed" and hide the removals that did succeed.
ctx is consulted BEFORE each removal, so a SIGINT lands between trees. Every tree not reached is reported as a keep naming the interruption, or the report would read as a completed sweep over a shorter plan. That a touched tree is wholly present or wholly absent comes from removeStagedTree, not from here.
func SweepStagedTrees ¶
func SweepStagedTrees(req SweepRequest) (SweepReport, error)
SweepStagedTrees removes the staged trees the current run no longer needs, and reports everything it kept with a reason each.
It is PlanStagedSweep followed by ExecuteStagedSweep and nothing else: the plan reaches every verdict, including the refusal of a staging root inside the overlay, and the executor is the only thing that acts on them.
It is RETAINED with NO production caller because its tests are the evidence that the plan/execute split preserved the behaviour — the same verdicts, in the same order, a failed removal still reported as a keep carrying the error. Rewriting those tests against the new pair would replace the evidence with assertions written by the change they check. The subcommand calls the pair directly, so it can print the plan and ask before executing.
It takes no context because its signature must not change; the subcommand passes its own context to ExecuteStagedSweep, so what is uncancellable is this test surface, not the path an operator interrupts.
type SweepRequest ¶
type SweepRequest struct {
Overlay string
StagingRoot string
InScope []StagedCandidate
}
SweepRequest is one sweep over one staging root.
InScope is what the CURRENT run still needs. A tree named there is never removed, whatever its record says, because a sweeper that eats the run that called it is worse than one that never runs.
type SweptEntry ¶
SweptEntry is one thing the sweep kept, and why.
The reason is not decoration. An operator looking at a staging root that did not shrink has to be able to read why it did not, and a sweeper that silently keeps things reads as a sweeper that swept.
type Unresolved ¶
type Unresolved struct {
Text string // the text as written, e.g. `-D${plugin}=`
Line int // 1-based, so the operator can go straight to it
}
Unresolved is an option name the ebuild builds at runtime instead of writing out, so no static reader can say what it will be.
It is REPORTED, never dropped. Dropping it is exactly the false-clean this project has already measured once and removed: the gate would say "nothing wrong here" about an ebuild it could not fully read.