commit b147de7289c925d0676b6a9079ca3c371016d0bc
parent ff21938c14afb3ffad4dabb429486a311ccc7380
Author: triesap <tyson@radroots.org>
Date: Mon, 10 Aug 2026 22:37:55 +0000
build: make HarvestCircle build modes explicit
- make standalone the invariant default without executable discovery or extbuild probes
- add governed source, integration, package, signing, notarization, and release routing
- validate exact mode-aware verification lanes and record mode in qualification output
- prove installed, missing, unknown, and release-only launcher behavior with fake-path tests
Diffstat:
8 files changed, 193 insertions(+), 85 deletions(-)
diff --git a/.gitignore b/.gitignore
@@ -3,7 +3,7 @@
.idea/
*.iml
**/build/
-core/target/
+**/target/
out/
local.properties
*.log
diff --git a/AGENTS.md b/AGENTS.md
@@ -106,16 +106,16 @@ keep implementation, tests, generated outputs, dependency evidence, and public
behavior aligned while preserving unrelated work.
The root `Makefile` is the standalone command surface and its durable behavior
-belongs in Gradle or public producer tools. When extbuild is installed, the
-Makefile routes commands through it. Without extbuild, Gradle uses its standard
-ignored `build/` directories and Cargo uses the ignored `core/target/` tree;
-the same checked-in tasks must remain functional. Set `EXTBUILD=` explicitly
-to exercise the standalone lane on a machine that also has extbuild. Run
-`make doctor` before the first mutating lane, use `make format`, `make lint`,
-and `make test` while iterating, and run `make check` for the complete source
-checkpoint. Use `make build`, `make bindings`, `make audit`, `make licenses`,
-`make package`, and `make release-check` when their affected artifact or
-release scope requires them.
+belongs in Gradle or public producer tools. Standalone targets never invoke or
+probe extbuild, even when it is installed. Explicit `governed-*` targets run a
+green extbuild doctor and route the same underlying commands through extbuild.
+Gradle otherwise uses its standard ignored `build/` directories and Cargo uses
+the ignored target trees. Run `make doctor` before the first standalone
+mutating lane and `make governed-doctor` before a governed lane. Use `make
+format`, `make lint`, and `make test` while iterating, and run `make check` or
+`make governed-check` for a complete source checkpoint. Signing, notarization,
+and release targets are governed-only. Unknown build modes fail before build
+mutation.
Shared public Git dependencies and package or advisory lanes may require
external services. Do not weaken immutable inputs or silently switch sources
diff --git a/Makefile b/Makefile
@@ -4,110 +4,149 @@ GRADLE ?= ./gradlew
CARGO ?= cargo
CARGO_MANIFEST := core/Cargo.toml
XTASK_MANIFEST := tools/xtask/Cargo.toml
-EXTBUILD ?= $(if $(shell cargo extbuild --version 2>/dev/null),cargo extbuild run --)
+BUILD_MODE ?= standalone
+VALID_BUILD_MODES := standalone governed
-.PHONY: help doctor lock metadata build-logic-check format format-fix lint test check build bindings dev run audit licenses foundation-check package host-package-check governed-package-check source-check package-check signing-check notarization-check release-check clean
+ifeq ($(filter $(BUILD_MODE),$(VALID_BUILD_MODES)),)
+$(error Unknown BUILD_MODE '$(BUILD_MODE)'; expected standalone or governed)
+endif
+
+ifeq ($(BUILD_MODE),governed)
+override BUILD_RUNNER := cargo extbuild run --
+else
+override BUILD_RUNNER :=
+endif
+
+.PHONY: help doctor governed-doctor lock metadata build-logic-check mode-check format format-fix lint test check governed-check build bindings dev run audit licenses foundation-check package host-package-check governed-package-check source-check governed-source-check package-check integration-check governed-integration-check acceptance-check signing-check _signing-check notarization-check _notarization-check release-check _release-check clean
help:
- @printf '%s\n' doctor lock metadata build-logic-check format format-fix lint test check build bindings dev run audit licenses foundation-check package host-package-check governed-package-check source-check package-check signing-check notarization-check release-check clean
+ @printf '%s\n' doctor governed-doctor lock metadata build-logic-check mode-check format format-fix lint test check governed-check build bindings dev run audit licenses foundation-check package host-package-check governed-package-check source-check governed-source-check package-check integration-check governed-integration-check acceptance-check signing-check notarization-check release-check clean
doctor:
- $(if $(strip $(EXTBUILD)),cargo extbuild doctor,@:)
- $(EXTBUILD) java -version
- $(EXTBUILD) $(CARGO) --version
- $(EXTBUILD) $(GRADLE) --version
+ @printf '%s\n' "harvestcircle.build.mode=$(BUILD_MODE)"
+ $(BUILD_RUNNER) java -version
+ $(BUILD_RUNNER) $(CARGO) --version
+ $(BUILD_RUNNER) $(GRADLE) --version
+
+governed-doctor:
+ $(CARGO) extbuild doctor
+
+ifeq ($(BUILD_MODE),governed)
+doctor: governed-doctor
+endif
lock: doctor
- $(EXTBUILD) $(CARGO) generate-lockfile --manifest-path $(CARGO_MANIFEST)
- $(EXTBUILD) $(CARGO) generate-lockfile --manifest-path $(XTASK_MANIFEST)
+ $(BUILD_RUNNER) $(CARGO) generate-lockfile --manifest-path $(CARGO_MANIFEST)
+ $(BUILD_RUNNER) $(CARGO) generate-lockfile --manifest-path $(XTASK_MANIFEST)
metadata: doctor
- $(EXTBUILD) $(CARGO) metadata --manifest-path $(CARGO_MANIFEST) --locked --format-version 1 --no-deps
+ $(BUILD_RUNNER) $(CARGO) metadata --manifest-path $(CARGO_MANIFEST) --locked --format-version 1 --no-deps
build-logic-check: doctor
- $(EXTBUILD) $(GRADLE) --no-daemon -p build-logic :contracts:check :plugins:check :plugins:functionalTest
+ $(BUILD_RUNNER) $(GRADLE) --no-daemon -p build-logic :contracts:check :plugins:check :plugins:functionalTest
+
+mode-check:
+ tools/test-build-modes.sh
format: doctor
- $(EXTBUILD) $(CARGO) fmt --manifest-path $(CARGO_MANIFEST) --all -- --check
- $(EXTBUILD) $(CARGO) fmt --manifest-path $(XTASK_MANIFEST) --all -- --check
- $(EXTBUILD) $(GRADLE) --no-daemon :app:shared:ktlintCheck :app:desktop:ktlintCheck
+ $(BUILD_RUNNER) $(CARGO) fmt --manifest-path $(CARGO_MANIFEST) --all -- --check
+ $(BUILD_RUNNER) $(CARGO) fmt --manifest-path $(XTASK_MANIFEST) --all -- --check
+ $(BUILD_RUNNER) $(GRADLE) --no-daemon :app:shared:ktlintCheck :app:desktop:ktlintCheck
format-fix: doctor
- $(EXTBUILD) $(CARGO) fmt --manifest-path $(CARGO_MANIFEST) --all
- $(EXTBUILD) $(CARGO) fmt --manifest-path $(XTASK_MANIFEST) --all
- $(EXTBUILD) $(GRADLE) --no-daemon :app:shared:ktlintFormat :app:desktop:ktlintFormat
+ $(BUILD_RUNNER) $(CARGO) fmt --manifest-path $(CARGO_MANIFEST) --all
+ $(BUILD_RUNNER) $(CARGO) fmt --manifest-path $(XTASK_MANIFEST) --all
+ $(BUILD_RUNNER) $(GRADLE) --no-daemon :app:shared:ktlintFormat :app:desktop:ktlintFormat
lint: doctor
- $(EXTBUILD) $(CARGO) clippy --manifest-path $(CARGO_MANIFEST) --workspace --all-targets --locked -- -D warnings
- $(EXTBUILD) $(CARGO) clippy --manifest-path $(XTASK_MANIFEST) --all-targets --locked -- -D warnings
- $(EXTBUILD) $(GRADLE) --no-daemon :app:shared:detektCommonMainSourceSet :app:shared:detektCommonTestSourceSet :app:desktop:detekt
+ $(BUILD_RUNNER) $(CARGO) clippy --manifest-path $(CARGO_MANIFEST) --workspace --all-targets --locked -- -D warnings
+ $(BUILD_RUNNER) $(CARGO) clippy --manifest-path $(XTASK_MANIFEST) --all-targets --locked -- -D warnings
+ $(BUILD_RUNNER) $(GRADLE) --no-daemon :app:shared:detektCommonMainSourceSet :app:shared:detektCommonTestSourceSet :app:desktop:detekt
test: doctor
- $(EXTBUILD) $(CARGO) test --manifest-path $(CARGO_MANIFEST) --workspace --locked
- $(EXTBUILD) $(CARGO) test --manifest-path $(XTASK_MANIFEST) --locked
- $(EXTBUILD) $(GRADLE) --no-daemon :app:shared:desktopTest :app:desktop:test
+ $(BUILD_RUNNER) $(CARGO) test --manifest-path $(CARGO_MANIFEST) --workspace --locked
+ $(BUILD_RUNNER) $(CARGO) test --manifest-path $(XTASK_MANIFEST) --locked
+ $(BUILD_RUNNER) $(GRADLE) --no-daemon :app:shared:desktopTest :app:desktop:test
+
+check: format lint test foundation-check mode-check
+ $(BUILD_RUNNER) $(GRADLE) --no-daemon :app:shared:check :app:desktop:check
-check: format lint test foundation-check
- $(EXTBUILD) $(GRADLE) --no-daemon :app:shared:check :app:desktop:check
+governed-check:
+ $(MAKE) --no-print-directory BUILD_MODE=governed check
build: doctor
- $(EXTBUILD) $(CARGO) build --manifest-path $(CARGO_MANIFEST) --workspace --locked
- $(EXTBUILD) $(GRADLE) --no-daemon :app:desktop:build
+ $(BUILD_RUNNER) $(CARGO) build --manifest-path $(CARGO_MANIFEST) --workspace --locked
+ $(BUILD_RUNNER) $(GRADLE) --no-daemon :app:desktop:build
bindings: doctor
- $(EXTBUILD) $(GRADLE) --no-daemon :app:desktop:verifyUniFfiBindings :app:desktop:verifyReleaseNativeLibrary
+ $(BUILD_RUNNER) $(GRADLE) --no-daemon :app:desktop:verifyUniFfiBindings :app:desktop:verifyReleaseNativeLibrary
dev: doctor
- $(EXTBUILD) $(GRADLE) :app:desktop:hotRun
+ $(BUILD_RUNNER) $(GRADLE) :app:desktop:hotRun
run: doctor
- $(EXTBUILD) $(GRADLE) :app:desktop:run
+ $(BUILD_RUNNER) $(GRADLE) :app:desktop:run
audit: doctor
- $(EXTBUILD) $(CARGO) audit --file core/Cargo.lock
- $(EXTBUILD) $(CARGO) deny --manifest-path $(CARGO_MANIFEST) check --config core/deny.toml advisories
- $(EXTBUILD) $(GRADLE) --no-daemon --no-configuration-cache :app:desktop:dependencyCheckAnalyze
+ $(BUILD_RUNNER) $(CARGO) audit --file core/Cargo.lock
+ $(BUILD_RUNNER) $(CARGO) deny --manifest-path $(CARGO_MANIFEST) check --config core/deny.toml advisories
+ $(BUILD_RUNNER) $(GRADLE) --no-daemon --no-configuration-cache :app:desktop:dependencyCheckAnalyze
licenses: doctor
- $(EXTBUILD) $(CARGO) deny --manifest-path $(CARGO_MANIFEST) check --config core/deny.toml licenses sources
- $(EXTBUILD) $(GRADLE) --no-daemon --no-parallel --no-configuration-cache :app:desktop:checkLicense
+ $(BUILD_RUNNER) $(CARGO) deny --manifest-path $(CARGO_MANIFEST) check --config core/deny.toml licenses sources
+ $(BUILD_RUNNER) $(GRADLE) --no-daemon --no-parallel --no-configuration-cache :app:desktop:checkLicense
foundation-check: doctor
- $(EXTBUILD) $(CARGO) run --manifest-path $(XTASK_MANIFEST) --locked -- qualification-report
+ HARVESTCIRCLE_BUILD_MODE=$(BUILD_MODE) $(BUILD_RUNNER) $(CARGO) run --manifest-path $(XTASK_MANIFEST) --locked -- qualification-report
package: check
- $(EXTBUILD) $(GRADLE) --no-daemon :app:desktop:verifyHostPackage
+ $(BUILD_RUNNER) $(GRADLE) --no-daemon :app:desktop:verifyHostPackage
-host-package-check:
- java -version
- $(CARGO) --version
- $(GRADLE) --version
- $(GRADLE) --no-daemon :app:desktop:verifyHostPackage
+host-package-check: doctor
+ $(BUILD_RUNNER) $(GRADLE) --no-daemon :app:desktop:verifyHostPackage
governed-package-check:
- cargo extbuild doctor
- cargo extbuild run -- java -version
- cargo extbuild run -- $(CARGO) --version
- cargo extbuild run -- $(GRADLE) --version
- cargo extbuild run -- $(GRADLE) --no-daemon :app:desktop:verifyHostPackage
+ $(MAKE) --no-print-directory BUILD_MODE=governed host-package-check
source-check: check bindings licenses
- $(EXTBUILD) $(GRADLE) --no-daemon :app:desktop:sourceReadiness
+ $(BUILD_RUNNER) $(GRADLE) --no-daemon :app:desktop:sourceReadiness
+
+governed-source-check:
+ $(MAKE) --no-print-directory BUILD_MODE=governed source-check
package-check: source-check
- $(EXTBUILD) $(GRADLE) --no-daemon :app:desktop:packageReadiness
+ $(BUILD_RUNNER) $(GRADLE) --no-daemon :app:desktop:packageReadiness
+
+integration-check: check
+ $(BUILD_RUNNER) $(GRADLE) --no-daemon :app:desktop:compileIntegrationTestKotlin
+
+governed-integration-check:
+ $(MAKE) --no-print-directory BUILD_MODE=governed integration-check
+
+acceptance-check: integration-check host-package-check
+
+signing-check:
+ $(MAKE) --no-print-directory BUILD_MODE=governed _signing-check
+
+_signing-check: doctor
+ $(BUILD_RUNNER) $(GRADLE) --no-daemon :app:desktop:signingReadiness
+
+notarization-check:
+ $(MAKE) --no-print-directory BUILD_MODE=governed _notarization-check
-signing-check: doctor
- $(EXTBUILD) $(GRADLE) --no-daemon :app:desktop:signingReadiness
+_notarization-check: doctor
+ $(BUILD_RUNNER) $(GRADLE) --no-daemon :app:desktop:notarizationReadiness
-notarization-check: doctor
- $(EXTBUILD) $(GRADLE) --no-daemon :app:desktop:notarizationReadiness
+release-check:
+ $(MAKE) --no-print-directory BUILD_MODE=governed _release-check
-release-check: doctor
- $(EXTBUILD) $(CARGO) audit --file core/Cargo.lock
- $(EXTBUILD) $(CARGO) deny --manifest-path $(CARGO_MANIFEST) check --config core/deny.toml advisories licenses sources
- $(EXTBUILD) $(GRADLE) --no-daemon --no-parallel --no-configuration-cache :app:desktop:releaseReadiness
+_release-check: doctor
+ @test "$(BUILD_MODE)" = governed || { printf '%s\n' 'release-check requires governed mode'; exit 2; }
+ $(BUILD_RUNNER) $(CARGO) audit --file core/Cargo.lock
+ $(BUILD_RUNNER) $(CARGO) deny --manifest-path $(CARGO_MANIFEST) check --config core/deny.toml advisories licenses sources
+ $(BUILD_RUNNER) $(GRADLE) --no-daemon --no-parallel --no-configuration-cache :app:desktop:releaseReadiness
clean: doctor
- $(EXTBUILD) $(CARGO) clean --manifest-path $(CARGO_MANIFEST)
- $(EXTBUILD) $(GRADLE) --no-daemon clean
+ $(BUILD_RUNNER) $(CARGO) clean --manifest-path $(CARGO_MANIFEST)
+ $(BUILD_RUNNER) $(CARGO) clean --manifest-path $(XTASK_MANIFEST)
+ $(BUILD_RUNNER) $(GRADLE) --no-daemon clean
diff --git a/README.md b/README.md
@@ -35,6 +35,11 @@ make build
make package
```
+These commands always use the standalone contributor lane. Use
+`make governed-check`, `make governed-integration-check`, or
+`make governed-package-check` when extbuild-governed output routing is
+required. Release, signing, and notarization checks are governed-only.
+
## Development branch
Active implementation currently proceeds on `dev`.
diff --git a/build-logic/plugins/src/main/kotlin/org/harvestcircle/buildlogic/plugins/tasks/VerificationLanes.kt b/build-logic/plugins/src/main/kotlin/org/harvestcircle/buildlogic/plugins/tasks/VerificationLanes.kt
@@ -14,11 +14,15 @@ object VerificationLanes {
private fun expected(environmentPrefix: String) =
linkedMapOf(
"schema" to "harvestcircle.verification-lanes.v2",
- "orchestration" to "standalone-make",
- "source.command" to "make source-check",
- "source.runner" to "host",
+ "orchestration" to "explicit-make-modes",
+ "source.standalone.command" to "make source-check",
+ "source.governed.command" to "make governed-source-check",
"source.credentials" to "none",
- "package.command" to "make package-check",
+ "integration.standalone.command" to "make integration-check",
+ "integration.governed.command" to "make governed-integration-check",
+ "integration.credentials" to "none",
+ "package.standalone.command" to "make host-package-check",
+ "package.governed.command" to "make governed-package-check",
"package.runners" to "linux,macos,windows",
"package.credentials" to "none",
"provenance.commit" to environmentPrefix + "BUILD_SOURCE_COMMIT",
@@ -31,6 +35,8 @@ object VerificationLanes {
"notarization.command" to "make notarization-check",
"notarization.runner" to "macos",
"notarization.credentials" to "notarization",
+ "release.command" to "make release-check",
+ "release.mode" to "governed",
)
fun parse(
@@ -76,7 +82,7 @@ abstract class VerifyVerificationLanes : DefaultTask() {
val environmentPrefix =
ProductCoordinates.load(productManifestFile.get().asFile)["environment.prefix"]
val policy = VerificationLanes.parse(source, environmentPrefix)
- check(policy.size == 18)
+ check(policy.size == 24)
check(runCatching { VerificationLanes.parse(source + "source.workflow=forbidden", environmentPrefix) }.isFailure)
check(
runCatching {
@@ -85,20 +91,19 @@ abstract class VerifyVerificationLanes : DefaultTask() {
)
check(
runCatching {
- VerificationLanes.parse(source.replace("source.runner=host", "source.runner=remote"), environmentPrefix)
+ VerificationLanes.parse(source.replace("release.mode=governed", "release.mode=standalone"), environmentPrefix)
}.isFailure,
)
val root = repositoryRoot.get().asFile.toPath()
val makefile = root.resolve("Makefile").toFile().readText()
- listOf("source.command", "package.command", "signing.command", "notarization.command").forEach { key ->
- val command = policy.getValue(key)
+ policy.filterKeys { it.endsWith(".command") }.forEach { (key, command) ->
val target = command.removePrefix("make ")
check(command == "make $target" && Regex("(?m)^${Regex.escape(target)}:").containsMatchIn(makefile)) {
- "Verification lane $key does not name a standalone Make target"
+ "Verification lane $key does not name a Make target"
}
}
check(policy.values.none { ".github/" in it || ".act/" in it }) {
- "Standalone verification policy must not reference an orchestration root"
+ "Verification policy must not reference an orchestration root"
}
}
}
diff --git a/config/verification/lanes-v2.properties b/config/verification/lanes-v2.properties
@@ -1,9 +1,13 @@
schema=harvestcircle.verification-lanes.v2
-orchestration=standalone-make
-source.command=make source-check
-source.runner=host
+orchestration=explicit-make-modes
+source.standalone.command=make source-check
+source.governed.command=make governed-source-check
source.credentials=none
-package.command=make package-check
+integration.standalone.command=make integration-check
+integration.governed.command=make governed-integration-check
+integration.credentials=none
+package.standalone.command=make host-package-check
+package.governed.command=make governed-package-check
package.runners=linux,macos,windows
package.credentials=none
provenance.commit=HARVESTCIRCLE_BUILD_SOURCE_COMMIT
@@ -16,3 +20,5 @@ signing.credentials=signing
notarization.command=make notarization-check
notarization.runner=macos
notarization.credentials=notarization
+release.command=make release-check
+release.mode=governed
diff --git a/tools/test-build-modes.sh b/tools/test-build-modes.sh
@@ -0,0 +1,39 @@
+#!/bin/sh
+set -eu
+
+repository_root=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)
+make_command=$(command -v make)
+fixture=$(mktemp -d "${TMPDIR:-/tmp}/harvestcircle-build-mode.XXXXXX")
+cleanup() {
+ find "$fixture" -depth -delete
+}
+trap cleanup EXIT HUP INT TERM
+
+printf '%s\n' '#!/bin/sh' 'if [ "${1:-}" = extbuild ]; then printf "%s\n" "cargo-extbuild unavailable" >&2; else printf "%s\n" "cargo must not be invoked in standalone dry-run" >&2; fi' 'exit 93' > "$fixture/cargo"
+chmod +x "$fixture/cargo"
+
+standalone_output=$(PATH="$fixture:$PATH" "$make_command" --no-print-directory -n BUILD_MODE=standalone -C "$repository_root" check)
+if printf '%s\n' "$standalone_output" | grep -q 'cargo extbuild'; then
+ printf '%s\n' 'standalone mode attempted to invoke extbuild' >&2
+ exit 1
+fi
+
+if "$make_command" --no-print-directory -C "$repository_root" BUILD_MODE=unsupported help > "$fixture/unknown.log" 2>&1; then
+ printf '%s\n' 'unknown build mode was accepted' >&2
+ exit 1
+fi
+grep -q "Unknown BUILD_MODE 'unsupported'" "$fixture/unknown.log"
+
+if PATH="$fixture:$PATH" "$make_command" --no-print-directory -C "$repository_root" governed-doctor > "$fixture/governed.log" 2>&1; then
+ printf '%s\n' 'governed mode succeeded without extbuild' >&2
+ exit 1
+fi
+grep -q 'cargo-extbuild unavailable' "$fixture/governed.log"
+
+if "$make_command" --no-print-directory -C "$repository_root" BUILD_MODE=standalone _release-check > "$fixture/release.log" 2>&1; then
+ printf '%s\n' 'release execution accepted standalone mode' >&2
+ exit 1
+fi
+grep -q 'release-check requires governed mode' "$fixture/release.log"
+
+printf '%s\n' 'harvestcircle.build-mode-contract=pass'
diff --git a/tools/xtask/src/lib.rs b/tools/xtask/src/lib.rs
@@ -27,6 +27,15 @@ impl FromStr for Command {
}
pub fn run(root: &Path, command: Command) -> Result<String, Vec<String>> {
+ let build_mode =
+ std::env::var("HARVESTCIRCLE_BUILD_MODE").unwrap_or_else(|_| "standalone".to_owned());
+ if command == Command::QualificationReport
+ && !matches!(build_mode.as_str(), "standalone" | "governed")
+ {
+ return Err(vec![format!(
+ "unknown qualification build mode: {build_mode}"
+ )]);
+ }
let inventory = Inventory::load(root).map_err(|finding| vec![finding])?;
let mut findings = Vec::new();
match command {
@@ -53,8 +62,13 @@ pub fn run(root: &Path, command: Command) -> Result<String, Vec<String>> {
Command::ProvenanceCheck => "provenance-check",
Command::QualificationReport => "qualification-report",
};
+ let mode = if command == Command::QualificationReport {
+ format!("harvestcircle.build.mode={build_mode}\n")
+ } else {
+ String::new()
+ };
Ok(format!(
- "harvestcircle.xtask.command={command_name}\nharvestcircle.xtask.inventory={inventory_kind}\nharvestcircle.xtask.result=pass\n"
+ "harvestcircle.xtask.command={command_name}\nharvestcircle.xtask.inventory={inventory_kind}\n{mode}harvestcircle.xtask.result=pass\n"
))
} else {
Err(findings)