app

Local-first trade for farms and co-ops
git clone https://radroots.dev/git/app.git
Log | Files | Refs | README | LICENSE

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:
M.gitignore | 2+-
MAGENTS.md | 20++++++++++----------
MMakefile | 157+++++++++++++++++++++++++++++++++++++++++++++++++------------------------------
MREADME.md | 5+++++
Mbuild-logic/plugins/src/main/kotlin/org/harvestcircle/buildlogic/plugins/tasks/VerificationLanes.kt | 25+++++++++++++++----------
Mconfig/verification/lanes-v2.properties | 14++++++++++----
Atools/test-build-modes.sh | 39+++++++++++++++++++++++++++++++++++++++
Mtools/xtask/src/lib.rs | 16+++++++++++++++-
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)