app

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

commit 46514dd56c9fd6ed6c368fb521e84a0522599f2f
parent c38c24136d0af82f74ae1360a06c7e35489b2211
Author: triesap <tyson@radroots.org>
Date:   Mon, 10 Aug 2026 23:37:00 +0000

build: remove the transitional buildSrc implementation

Diffstat:
Mbuild-logic/plugins/src/functionalTest/kotlin/org/harvestcircle/buildlogic/plugins/ConventionPluginSmokeTest.kt | 6++----
DbuildSrc/build.gradle.kts | 8--------
DbuildSrc/settings.gradle.kts | 1-
DbuildSrc/src/main/kotlin/org/harvestcircle/gradle/CompatibilityExpectations.kt | 98-------------------------------------------------------------------------------
DbuildSrc/src/main/kotlin/org/harvestcircle/gradle/DesktopBuildMetadata.kt | 117-------------------------------------------------------------------------------
DbuildSrc/src/main/kotlin/org/harvestcircle/gradle/FfiCompatibilityBaseline.kt | 72------------------------------------------------------------------------
DbuildSrc/src/main/kotlin/org/harvestcircle/gradle/ProductCoordinates.kt | 293-------------------------------------------------------------------------------
DbuildSrc/src/main/kotlin/org/harvestcircle/gradle/SourceProvenance.kt | 108-------------------------------------------------------------------------------
DbuildSrc/src/main/kotlin/org/harvestcircle/gradle/VerificationLanes.kt | 103-------------------------------------------------------------------------------
DbuildSrc/src/main/kotlin/org/harvestcircle/gradle/VerifySharedBoundary.kt | 36------------------------------------
10 files changed, 2 insertions(+), 840 deletions(-)

diff --git a/build-logic/plugins/src/functionalTest/kotlin/org/harvestcircle/buildlogic/plugins/ConventionPluginSmokeTest.kt b/build-logic/plugins/src/functionalTest/kotlin/org/harvestcircle/buildlogic/plugins/ConventionPluginSmokeTest.kt @@ -4,7 +4,6 @@ import org.gradle.testkit.runner.GradleRunner import org.gradle.testkit.runner.UnexpectedBuildFailure import kotlin.io.path.createTempDirectory import kotlin.io.path.createDirectories -import kotlin.io.path.exists import kotlin.io.path.writeText import kotlin.test.Test import kotlin.test.assertTrue @@ -72,7 +71,6 @@ class ConventionPluginSmokeTest { val result = runner.build() assertTrue(result.output.contains("BUILD SUCCESSFUL"), pluginId) - assertTrue(!fixture.resolve("buildSrc").exists(), "$pluginId fixture must not provide buildSrc classes") if (pluginId == "org.harvestcircle.build.root") { assertTrue(result.output.contains("verifyProductCoordinates"), result.output) } @@ -160,7 +158,7 @@ class ConventionPluginSmokeTest { } @Test - fun desktopPluginPublishesTheApplicationContractWithoutClaimingIntegrationExecution() { + fun desktopPluginPublishesTheApplicationAndIntegrationContracts() { val fixture = createTempDirectory("harvestcircle-desktop-plugin-") prepareDesktopBuild(fixture, withUnitTest = true) @@ -178,8 +176,8 @@ class ConventionPluginSmokeTest { "verifyGeneratedDesktopBuildMetadata", "verifyTestInventory", "compileIntegrationTestKotlin", + "integrationTest", ).forEach { taskName -> assertTrue(result.output.contains(taskName), result.output) } - assertTrue(!result.output.lineSequence().any { it.startsWith("integrationTest -") }, result.output) } @Test diff --git a/buildSrc/build.gradle.kts b/buildSrc/build.gradle.kts @@ -1,8 +0,0 @@ -plugins { - `kotlin-dsl` -} - -repositories { - gradlePluginPortal() - mavenCentral() -} diff --git a/buildSrc/settings.gradle.kts b/buildSrc/settings.gradle.kts @@ -1 +0,0 @@ -rootProject.name = "harvestcircle-build-logic" diff --git a/buildSrc/src/main/kotlin/org/harvestcircle/gradle/CompatibilityExpectations.kt b/buildSrc/src/main/kotlin/org/harvestcircle/gradle/CompatibilityExpectations.kt @@ -1,98 +0,0 @@ -package org.harvestcircle.gradle - -import org.gradle.api.DefaultTask -import org.gradle.api.file.RegularFileProperty -import org.gradle.api.tasks.InputFile -import org.gradle.api.tasks.OutputFile -import org.gradle.api.tasks.PathSensitive -import org.gradle.api.tasks.PathSensitivity -import org.gradle.api.tasks.TaskAction - -object CompatibilityExpectationsSource { - fun render(baseline: FfiCompatibilityBaseline): String = - """ - |// @generated by the HarvestCircle Gradle build. Do not edit. - |package org.harvestcircle.application.generated - | - |internal object NativeCompatibilityExpectations { - | const val ffiContractId = ${baseline["contract.id"].quoted()} - | const val productVersion = ${baseline["product.version"].quoted()} - | const val cargoPackageVersion = ${baseline["product.version"].quoted()} - | const val distributionPackageVersion = ${baseline["package.version"].quoted()} - | const val productCoordinateDigest = ${baseline["product.coordinate_digest"].quoted()} - | const val sourceProvenanceDigest = ${baseline["source.provenance_digest"].quoted()} - | const val sourceFoundationBaseline = ${baseline["source.foundation_baseline"].quoted()} - | const val ffiContractHash = ${baseline["contract.hash"].quoted()} - | val ffiContractMajor: UShort = ${baseline.ushort("contract.major")}.toUShort() - | val minimumFfiContractMinor: UShort = ${baseline.ushort("contract.minor")}.toUShort() - | const val snapshotSchema: UInt = ${baseline.uint("snapshot.schema")}U - | const val minimumStorageSchema: UInt = ${baseline.uint("storage.schema.minimum")}U - | const val maximumStorageSchema: UInt = ${baseline.uint("storage.schema.current")}U - |} - """.trimMargin() + "\n" - - fun requireFresh( - actual: String?, - expected: String, - ) { - require(actual != null) { "Generated Kotlin compatibility expectations are missing" } - require(actual == expected) { "Generated Kotlin compatibility expectations are stale" } - } - - private fun String.quoted(): String = - buildString { - append('"') - this@quoted.forEach { character -> - when (character) { - '\\' -> append("\\\\") - '"' -> append("\\\"") - '\n' -> append("\\n") - '\r' -> append("\\r") - '\t' -> append("\\t") - else -> append(character) - } - } - append('"') - } - - private fun FfiCompatibilityBaseline.uint(key: String): UInt = - this[key].toUIntOrNull() ?: error("FFI baseline $key must be an unsigned integer") - - private fun FfiCompatibilityBaseline.ushort(key: String): UShort = - this[key].toUShortOrNull() ?: error("FFI baseline $key must fit an unsigned short") -} - -abstract class GenerateCompatibilityExpectations : DefaultTask() { - @get:InputFile - @get:PathSensitive(PathSensitivity.RELATIVE) - abstract val baselineFile: RegularFileProperty - - @get:OutputFile - abstract val outputFile: RegularFileProperty - - @TaskAction - fun generate() { - val source = CompatibilityExpectationsSource.render(FfiCompatibilityBaseline.load(baselineFile.get().asFile)) - val output = outputFile.get().asFile - output.parentFile.mkdirs() - output.writeText(source) - } -} - -abstract class VerifyGeneratedCompatibilityExpectations : DefaultTask() { - @get:InputFile - @get:PathSensitive(PathSensitivity.RELATIVE) - abstract val baselineFile: RegularFileProperty - - @get:InputFile - @get:PathSensitive(PathSensitivity.RELATIVE) - abstract val generatedFile: RegularFileProperty - - @TaskAction - fun verify() { - val expected = CompatibilityExpectationsSource.render(FfiCompatibilityBaseline.load(baselineFile.get().asFile)) - check(runCatching { CompatibilityExpectationsSource.requireFresh(null, expected) }.isFailure) - check(runCatching { CompatibilityExpectationsSource.requireFresh("$expected// stale\n", expected) }.isFailure) - CompatibilityExpectationsSource.requireFresh(generatedFile.get().asFile.readText(), expected) - } -} diff --git a/buildSrc/src/main/kotlin/org/harvestcircle/gradle/DesktopBuildMetadata.kt b/buildSrc/src/main/kotlin/org/harvestcircle/gradle/DesktopBuildMetadata.kt @@ -1,117 +0,0 @@ -package org.harvestcircle.gradle - -import org.gradle.api.DefaultTask -import org.gradle.api.file.RegularFileProperty -import org.gradle.api.provider.Property -import org.gradle.api.tasks.Input -import org.gradle.api.tasks.InputFile -import org.gradle.api.tasks.OutputFile -import org.gradle.api.tasks.PathSensitive -import org.gradle.api.tasks.PathSensitivity -import org.gradle.api.tasks.TaskAction - -data class DesktopBuildMetadataValues( - val productVersion: String, - val distributionPackageVersion: String, - val gradleToolchain: String, - val javaToolchain: String, - val kotlinToolchain: String, - val composeMultiplatformVersion: String, -) - -object DesktopBuildMetadataSource { - fun render(values: DesktopBuildMetadataValues): String = - """ - |// @generated by the HarvestCircle Gradle build. Do not edit. - |package org.harvestcircle.application.generated - | - |internal object DesktopBuildMetadata { - | const val productVersion = ${values.productVersion.quoted()} - | const val distributionPackageVersion = ${values.distributionPackageVersion.quoted()} - | const val gradleToolchain = ${values.gradleToolchain.quoted()} - | const val javaToolchain = ${values.javaToolchain.quoted()} - | const val kotlinToolchain = ${values.kotlinToolchain.quoted()} - | const val composeMultiplatformVersion = ${values.composeMultiplatformVersion.quoted()} - |} - """.trimMargin() + "\n" - - fun requireFresh( - actual: String?, - expected: String, - ) { - require(actual != null) { "Generated desktop build metadata is missing" } - require(actual == expected) { "Generated desktop build metadata is stale" } - } - - private fun String.quoted(): String = - buildString { - append('"') - this@quoted.forEach { character -> - when (character) { - '\\' -> append("\\\\") - '"' -> append("\\\"") - '\n' -> append("\\n") - '\r' -> append("\\r") - '\t' -> append("\\t") - else -> append(character) - } - } - append('"') - } -} - -abstract class DesktopBuildMetadataTask : DefaultTask() { - @get:Input - abstract val productVersion: Property<String> - - @get:Input - abstract val distributionPackageVersion: Property<String> - - @get:Input - abstract val gradleToolchain: Property<String> - - @get:Input - abstract val javaToolchain: Property<String> - - @get:Input - abstract val kotlinToolchain: Property<String> - - @get:Input - abstract val composeMultiplatformVersion: Property<String> - - protected fun values(): DesktopBuildMetadataValues = - DesktopBuildMetadataValues( - productVersion = productVersion.get(), - distributionPackageVersion = distributionPackageVersion.get(), - gradleToolchain = gradleToolchain.get(), - javaToolchain = javaToolchain.get(), - kotlinToolchain = kotlinToolchain.get(), - composeMultiplatformVersion = composeMultiplatformVersion.get(), - ) -} - -abstract class GenerateDesktopBuildMetadata : DesktopBuildMetadataTask() { - @get:OutputFile - abstract val outputFile: RegularFileProperty - - @TaskAction - fun generate() { - val output = outputFile.get().asFile - output.parentFile.mkdirs() - output.writeText(DesktopBuildMetadataSource.render(values())) - } -} - -abstract class VerifyGeneratedDesktopBuildMetadata : DesktopBuildMetadataTask() { - @get:InputFile - @get:PathSensitive(PathSensitivity.RELATIVE) - abstract val generatedFile: RegularFileProperty - - @TaskAction - fun verify() { - val expected = DesktopBuildMetadataSource.render(values()) - check(runCatching { DesktopBuildMetadataSource.requireFresh(null, expected) }.isFailure) - check(runCatching { DesktopBuildMetadataSource.requireFresh("$expected// stale\n", expected) }.isFailure) - DesktopBuildMetadataSource.requireFresh(generatedFile.get().asFile.readText(), expected) - } -} diff --git a/buildSrc/src/main/kotlin/org/harvestcircle/gradle/FfiCompatibilityBaseline.kt b/buildSrc/src/main/kotlin/org/harvestcircle/gradle/FfiCompatibilityBaseline.kt @@ -1,72 +0,0 @@ -package org.harvestcircle.gradle - -import java.io.File - -class FfiCompatibilityBaseline private constructor( - private val values: Map<String, String>, -) { - operator fun get(key: String): String = values.getValue(key) - - companion object { - private val requiredKeys = - linkedSetOf( - "schema", - "contract.id", - "contract.major", - "contract.minor", - "contract.hash", - "product.coordinate_digest", - "snapshot.schema", - "storage.schema.minimum", - "storage.schema.current", - "product.version", - "package.version", - "source.provenance_digest", - "source.foundation_baseline", - ) - - fun load(file: File): FfiCompatibilityBaseline = parse(file.readText()) - - fun parse(source: String): FfiCompatibilityBaseline { - require(!source.startsWith('\uFEFF')) { "FFI baseline must not contain a UTF-8 BOM" } - val values = linkedMapOf<String, String>() - source.lineSequence().forEachIndexed { index, raw -> - val line = raw.trim() - if (line.isEmpty() || line.startsWith('#')) return@forEachIndexed - val separator = line.indexOf('=') - require(separator > 0) { "FFI baseline line ${index + 1} is not key=value" } - val key = line.substring(0, separator).trim() - val value = line.substring(separator + 1).trim() - require(key in requiredKeys) { "Unknown FFI baseline key $key" } - require( - key.isNotEmpty() && - key.none(Char::isISOControl) && - value.isNotEmpty() && - value.none(Char::isISOControl), - ) { - "FFI baseline $key is empty or contains a control character" - } - require(values.put(key, value) == null) { "Duplicate FFI baseline key $key" } - } - require(values.keys == requiredKeys) { "FFI baseline keys do not match the v4 schema" } - require(values.getValue("schema") == "harvestcircle.ffi.v4") - require(values.getValue("contract.id") == "harvestcircle-desktop-ffi-v4") - require(values.getValue("contract.major") == "4") - require(values.getValue("contract.minor") == "1") - require(values.getValue("snapshot.schema") == "1") - require(values.getValue("storage.schema.minimum") == "5") - require(values.getValue("storage.schema.current") == "10") - listOf( - "contract.hash", - "product.coordinate_digest", - "source.provenance_digest", - ).forEach { key -> - require(values.getValue(key).matches(Regex("[0-9a-f]{64}"))) - } - require(values.getValue("source.foundation_baseline").matches(Regex("[0-9a-f]{40}"))) - require(values.getValue("product.version").matches(Regex("[0-9]+\\.[0-9]+\\.[0-9]+(?:-[0-9A-Za-z.-]+)?"))) - require(values.getValue("package.version").matches(Regex("[1-9][0-9]*(?:\\.[0-9]+){0,2}"))) - return FfiCompatibilityBaseline(values.toMap()) - } - } -} diff --git a/buildSrc/src/main/kotlin/org/harvestcircle/gradle/ProductCoordinates.kt b/buildSrc/src/main/kotlin/org/harvestcircle/gradle/ProductCoordinates.kt @@ -1,293 +0,0 @@ -package org.harvestcircle.gradle - -import java.io.File -import java.security.MessageDigest -import org.gradle.api.DefaultTask -import org.gradle.api.file.RegularFileProperty -import org.gradle.api.tasks.InputFile -import org.gradle.api.tasks.PathSensitive -import org.gradle.api.tasks.PathSensitivity -import org.gradle.api.tasks.TaskAction - -class ProductCoordinates private constructor( - private val values: Map<String, String>, - val digest: String, -) { - operator fun get(key: String): String = values.getValue(key) - - companion object { - const val schema = "harvestcircle.product.v1" - val requiredKeys: List<String> = - listOf( - "schema", - "product.name", - "product.slug", - "kotlin.root_namespace", - "desktop.application_id", - "desktop.bundle_id", - "desktop.main_class", - "ffi.kotlin_package", - "ffi.cdylib_name", - "database.qualifier", - "database.organization", - "database.application", - "database.filename", - "keyring.service", - "environment.prefix", - "vendor.name", - "copyright.notice", - ) - - fun load(file: File): ProductCoordinates = parse(file.readText()) - - fun parse(source: String): ProductCoordinates { - require(!source.startsWith('\uFEFF')) { "Product coordinates must not contain a UTF-8 BOM" } - val parsed = linkedMapOf<String, String>() - source.lineSequence().forEachIndexed { index, raw -> - val line = raw.trim() - if (line.isEmpty() || line.startsWith('#')) return@forEachIndexed - val separator = line.indexOf('=') - require(separator > 0) { "Product coordinate line ${index + 1} is not key=value" } - val key = line.substring(0, separator).trim() - val value = line.substring(separator + 1).trim() - require(key in requiredKeys) { "Unknown product coordinate $key" } - require(key.isNotEmpty() && key.none(Char::isISOControl) && value.isNotEmpty() && value.none(Char::isISOControl)) { - "Product coordinate $key is empty or contains a control character" - } - require(parsed.put(key, value) == null) { "Duplicate product coordinate $key" } - } - require(parsed.keys == requiredKeys.toSet()) { - "Product coordinate keys do not match the required schema" - } - parsed.forEach(::validateCoordinate) - val canonical = - buildString { - requiredKeys.forEach { key -> - append(key).append('=').append(parsed.getValue(key)).append('\n') - } - } - val digest = - MessageDigest - .getInstance("SHA-256") - .digest(canonical.toByteArray(Charsets.UTF_8)) - .joinToString("") { byte -> "%02x".format(byte) } - return ProductCoordinates(parsed.toMap(), digest) - } - - private fun validateCoordinate( - key: String, - value: String, - ) { - val valid = - when (key) { - "schema" -> value == schema - "product.name", "vendor.name", "copyright.notice" -> value.length <= 160 - "product.slug", - "ffi.cdylib_name", - "database.qualifier", - "database.organization", - "database.application", - -> value.isLowerIdentifier() - "kotlin.root_namespace", - "desktop.application_id", - "desktop.bundle_id", - "desktop.main_class", - "ffi.kotlin_package", - "keyring.service", - -> value.isDottedIdentifier() - "database.filename" -> - value.endsWith(".sqlite3") && - ".." !in value && - value.all { it.isAsciiLetterOrDigit() || it in "._-" } - "environment.prefix" -> - value.firstOrNull()?.let { it in 'A'..'Z' } == true && - value.endsWith('_') && - value.all { it in 'A'..'Z' || it.isDigit() || it == '_' } - else -> false - } - require(valid) { "Product coordinate $key has an invalid value" } - } - - private fun String.isLowerIdentifier(): Boolean = - firstOrNull()?.let { it in 'a'..'z' } == true && - all { it in 'a'..'z' || it.isDigit() || it == '_' } - - private fun String.isDottedIdentifier(): Boolean = - split('.').all { segment -> - segment.firstOrNull()?.let { it.isAsciiLetter() || it == '_' } == true && - segment.all { it.isAsciiLetterOrDigit() || it == '_' } - } - - private fun Char.isAsciiLetter(): Boolean = this in 'a'..'z' || this in 'A'..'Z' - - private fun Char.isAsciiLetterOrDigit(): Boolean = isAsciiLetter() || isDigit() - } -} - -abstract class VerifyProductCoordinates : DefaultTask() { - @get:InputFile - @get:PathSensitive(PathSensitivity.RELATIVE) - abstract val manifestFile: RegularFileProperty - - @get:InputFile - @get:PathSensitive(PathSensitivity.RELATIVE) - abstract val uniFfiConfigFile: RegularFileProperty - - @get:InputFile - @get:PathSensitive(PathSensitivity.RELATIVE) - abstract val ffiBaselineFile: RegularFileProperty - - @get:InputFile - @get:PathSensitive(PathSensitivity.RELATIVE) - abstract val sourceProvenanceFile: RegularFileProperty - - @get:InputFile - @get:PathSensitive(PathSensitivity.RELATIVE) - abstract val nativeCompatibilityFile: RegularFileProperty - - @TaskAction - fun verify() { - val source = manifestFile.get().asFile.readText() - val coordinates = ProductCoordinates.parse(source) - check(coordinates.digest.matches(Regex("[0-9a-f]{64}"))) - val equivalentSources = - listOf( - source.replace("\n", "\r\n"), - source.trimEnd(), - "# comment\n$source", - source.lineSequence().joinToString("\n") { line -> - if (line.isBlank() || line.startsWith('#')) line else line.replaceFirst("=", " = ") - }, - ) - equivalentSources.forEach { equivalent -> - check(ProductCoordinates.parse(equivalent).digest == coordinates.digest) - } - check(runCatching { ProductCoordinates.parse("\uFEFF$source") }.isFailure) - check(runCatching { ProductCoordinates.parse(source + "\nschema=${ProductCoordinates.schema}") }.isFailure) - check(runCatching { ProductCoordinates.parse(source + "\nunknown=value") }.isFailure) - check(runCatching { ProductCoordinates.parse(source.substringAfter('\n')) }.isFailure) - check(runCatching { ProductCoordinates.parse(source.replaceCoordinate("product.slug", "INVALID")) }.isFailure) - check( - runCatching { - ProductCoordinates.parse(source.replaceCoordinate("database.filename", "../other.sqlite3")) - }.isFailure, - ) - validCoordinateMutations.forEach { (key, replacement) -> - val mutated = ProductCoordinates.parse(source.replaceCoordinate(key, replacement)) - check(mutated[key] == replacement) - check(mutated.digest != coordinates.digest) - } - - val uniFfiConfig = uniFfiConfigFile.get().asFile.readText() - check( - uniFfiConfig.contains( - "package_name = \"${coordinates["ffi.kotlin_package"]}\"", - ), - ) - check( - uniFfiConfig.contains( - "cdylib_name = \"${coordinates["ffi.cdylib_name"]}\"", - ), - ) - - val baseline = FfiCompatibilityBaseline.load(ffiBaselineFile.get().asFile) - val baselineSource = ffiBaselineFile.get().asFile.readText() - check(runCatching { FfiCompatibilityBaseline.parse(baselineSource + "\nunknown=value") }.isFailure) - check(runCatching { FfiCompatibilityBaseline.parse(baselineSource.substringAfter('\n')) }.isFailure) - check(runCatching { FfiCompatibilityBaseline.parse("\uFEFF$baselineSource") }.isFailure) - check( - runCatching { - FfiCompatibilityBaseline.parse( - baselineSource.replace( - Regex("(?m)^contract\\.hash=.*$"), - "contract.hash=malformed", - ), - ) - }.isFailure, - ) - check( - runCatching { - FfiCompatibilityBaseline.parse( - baselineSource.replace( - Regex("(?m)^package\\.version=.*$"), - "package.version=invalid", - ), - ) - }.isFailure, - ) - check( - runCatching { - FfiCompatibilityBaseline.parse( - baselineSource + "\ncontract.id=harvestcircle-desktop-ffi-v4", - ) - }.isFailure, - ) - check(baseline["product.coordinate_digest"] == coordinates.digest) - val provenanceSource = sourceProvenanceFile.get().asFile.readText() - val provenance = SourceProvenance.parse(provenanceSource) - check(baseline["source.provenance_digest"] == provenance.digest) - check(provenance.foundationBaseline == baseline["source.foundation_baseline"]) - val equivalentProvenance = - listOf( - provenanceSource.replace("\n", "\r\n"), - provenanceSource.trimEnd(), - "# comment\n$provenanceSource", - provenanceSource.replace( - "component = \"domain\"\ncommit = \"a4d7deebec3e2ce2c1daa455de6d79857839aed0\"", - "commit = \"a4d7deebec3e2ce2c1daa455de6d79857839aed0\"\ncomponent = \"domain\"", - ), - ) - equivalentProvenance.forEach { equivalent -> - check(SourceProvenance.parse(equivalent).digest == provenance.digest) - } - check(runCatching { SourceProvenance.parse("\uFEFF$provenanceSource") }.isFailure) - check(runCatching { SourceProvenance.parse("unknown = \"value\"\n$provenanceSource") }.isFailure) - check( - SourceProvenance.parse( - provenanceSource.replace( - "a4d7deebec3e2ce2c1daa455de6d79857839aed0", - "b4d7deebec3e2ce2c1daa455de6d79857839aed0", - ), - ).digest != provenance.digest, - ) - val nativeCompatibility = nativeCompatibilityFile.get().asFile.readText() - check(nativeCompatibility.contains("NativeCompatibilityExpectations as Expected")) - listOf( - "contract.id", - "contract.hash", - "product.coordinate_digest", - "source.provenance_digest", - "source.foundation_baseline", - ).forEach { key -> check(!nativeCompatibility.contains(baseline[key])) } - } - - private fun String.replaceCoordinate( - key: String, - replacement: String, - ): String = - lineSequence().joinToString("\n") { line -> - if (line.substringBefore('=', missingDelimiterValue = "") == key) "$key=$replacement" else line - } - - private val validCoordinateMutations = - linkedMapOf( - "product.name" to "Harvest Circle Test", - "product.slug" to "harvestcircle_test", - "kotlin.root_namespace" to "org.example", - "desktop.application_id" to "org.example.desktop", - "desktop.bundle_id" to "org.example.bundle", - "desktop.main_class" to "org.example.MainKt", - "ffi.kotlin_package" to "org.example.ffi", - "ffi.cdylib_name" to "example_ffi", - "database.qualifier" to "com", - "database.organization" to "example", - "database.application" to "test", - "database.filename" to "example.sqlite3", - "keyring.service" to "org.example.desktop.nostr", - "environment.prefix" to "EXAMPLE_", - "vendor.name" to "Example Cooperative", - "copyright.notice" to "Copyright Example contributors", - ).also { mutations -> - check(mutations.size + 1 == ProductCoordinates.requiredKeys.size) - } -} diff --git a/buildSrc/src/main/kotlin/org/harvestcircle/gradle/SourceProvenance.kt b/buildSrc/src/main/kotlin/org/harvestcircle/gradle/SourceProvenance.kt @@ -1,108 +0,0 @@ -package org.harvestcircle.gradle - -import java.io.File -import java.security.MessageDigest - -class SourceProvenance private constructor( - private val root: Map<String, String>, - private val imports: List<Map<String, String>>, -) { - val foundationBaseline: String - get() = root.getValue("foundation_baseline") - - val canonical: String - get() = - buildString { - rootKeys.forEach { key -> - append(key).append('=').append(root.getValue(key)).append('\n') - } - imports.forEach { entry -> - append("import.component=").append(entry.getValue("component")).append('\n') - append("import.commit=").append(entry.getValue("commit")).append('\n') - } - } - - val digest: String - get() = canonical.sha256() - - companion object { - private val rootKeys = - linkedSetOf( - "schema", - "source_product", - "source_repository", - "foundation_baseline", - "canonical_radroots_repository", - "canonical_radroots_revision", - ) - private val importKeys = linkedSetOf("component", "commit") - private val assignment = Regex("^([A-Za-z0-9_]+)\\s*=\\s*\"([^\"]*)\"\\s*(?:#.*)?$") - - fun load(file: File): SourceProvenance = parse(file.readText()) - - fun parse(source: String): SourceProvenance { - require(!source.startsWith('\uFEFF')) { "Source provenance must not contain a UTF-8 BOM" } - val root = linkedMapOf<String, String>() - val imports = mutableListOf<Map<String, String>>() - var currentImport: LinkedHashMap<String, String>? = null - - fun completeImport() { - currentImport?.let { entry -> - require(entry.keys == importKeys) { "Source provenance import keys do not match the contract" } - imports += entry.toMap() - } - currentImport = null - } - - source.lineSequence().forEachIndexed { index, raw -> - val line = raw.trim() - if (line.isEmpty() || line.startsWith('#')) return@forEachIndexed - if (line == "[[import]]") { - completeImport() - currentImport = linkedMapOf() - return@forEachIndexed - } - val match = assignment.matchEntire(line) - require(match != null) { "Source provenance line ${index + 1} is not a supported TOML string assignment" } - val key = match.groupValues[1] - val value = match.groupValues[2] - require(value.isNotEmpty() && value.none(Char::isISOControl)) { - "Source provenance $key is empty or contains a control character" - } - val target = currentImport ?: root - val allowed = if (currentImport == null) rootKeys else importKeys - require(key in allowed) { "Unknown source provenance key $key" } - require(target.put(key, value) == null) { "Duplicate source provenance key $key" } - } - completeImport() - - require(root.keys == rootKeys) { "Source provenance root keys do not match the contract" } - require(root.getValue("schema") == "harvestcircle.source_provenance.v1") - listOf("foundation_baseline", "canonical_radroots_revision").forEach { key -> - require(root.getValue(key).isCanonicalHex(40)) { "Source provenance $key is not canonical" } - } - require(imports.isNotEmpty()) { "Source provenance imports must not be empty" } - imports.forEach { entry -> - require(entry.getValue("commit").isCanonicalHex(40)) { - "Source provenance import commit is not canonical" - } - } - require(imports.map { it.getValue("component") }.distinct().size == imports.size) { - "Source provenance components must be unique" - } - return SourceProvenance( - root.toMap(), - imports.sortedBy { it.getValue("component") }, - ) - } - } -} - -private fun String.sha256(): String = - MessageDigest - .getInstance("SHA-256") - .digest(toByteArray(Charsets.UTF_8)) - .joinToString("") { byte -> "%02x".format(byte) } - -private fun String.isCanonicalHex(width: Int): Boolean = - length == width && all { character -> character in '0'..'9' || character in 'a'..'f' } diff --git a/buildSrc/src/main/kotlin/org/harvestcircle/gradle/VerificationLanes.kt b/buildSrc/src/main/kotlin/org/harvestcircle/gradle/VerificationLanes.kt @@ -1,103 +0,0 @@ -package org.harvestcircle.gradle - -import org.gradle.api.DefaultTask -import org.gradle.api.file.DirectoryProperty -import org.gradle.api.file.RegularFileProperty -import org.gradle.api.tasks.InputFile -import org.gradle.api.tasks.Internal -import org.gradle.api.tasks.PathSensitive -import org.gradle.api.tasks.PathSensitivity -import org.gradle.api.tasks.TaskAction - -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", - "source.credentials" to "none", - "package.command" to "make package-check", - "package.runners" to "linux,macos,windows", - "package.credentials" to "none", - "provenance.commit" to environmentPrefix + "BUILD_SOURCE_COMMIT", - "provenance.dirty" to environmentPrefix + "BUILD_SOURCE_DIRTY", - "provenance.radroots" to environmentPrefix + "BUILD_RADROOTS_REVISION", - "provenance.epoch" to "SOURCE_DATE_EPOCH", - "signing.command" to "make signing-check", - "signing.runner" to "macos", - "signing.credentials" to "signing", - "notarization.command" to "make notarization-check", - "notarization.runner" to "macos", - "notarization.credentials" to "notarization", - ) - - fun parse( - source: String, - environmentPrefix: String, - ): Map<String, String> { - val expected = expected(environmentPrefix) - val parsed = linkedMapOf<String, String>() - source.trimEnd('\n', '\r').lineSequence().forEachIndexed { index, raw -> - val line = raw.trim() - require(line.isNotEmpty() && !line.startsWith('#')) { - "Verification lane policy contains an empty or comment line at ${index + 1}" - } - val separator = line.indexOf('=') - require(separator > 0 && separator < line.lastIndex && line.indexOf('=', separator + 1) == -1) { - "Verification lane policy contains malformed syntax at ${index + 1}" - } - val key = line.substring(0, separator) - val value = line.substring(separator + 1) - require(parsed.put(key, value) == null) { "Duplicate verification lane key: $key" } - } - require(parsed.keys == expected.keys) { "Verification lane keys do not match the authority" } - require(parsed == expected) { "Verification lane values do not match the authority" } - return parsed - } -} - -abstract class VerifyVerificationLanes : DefaultTask() { - @get:InputFile - @get:PathSensitive(PathSensitivity.RELATIVE) - abstract val policyFile: RegularFileProperty - - @get:InputFile - @get:PathSensitive(PathSensitivity.RELATIVE) - abstract val productManifestFile: RegularFileProperty - - @get:Internal - abstract val repositoryRoot: DirectoryProperty - - @TaskAction - fun verify() { - val source = policyFile.get().asFile.readText() - val environmentPrefix = - ProductCoordinates.load(productManifestFile.get().asFile)["environment.prefix"] - val policy = VerificationLanes.parse(source, environmentPrefix) - check(policy.size == 18) - check(runCatching { VerificationLanes.parse(source + "source.workflow=forbidden", environmentPrefix) }.isFailure) - check( - runCatching { - VerificationLanes.parse(source.replace("credentials=none", "credentials=all"), environmentPrefix) - }.isFailure, - ) - check( - runCatching { - VerificationLanes.parse(source.replace("source.runner=host", "source.runner=remote"), 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) - 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" - } - } - check(policy.values.none { ".github/" in it || ".act/" in it }) { - "Standalone verification policy must not reference an orchestration root" - } - } -} diff --git a/buildSrc/src/main/kotlin/org/harvestcircle/gradle/VerifySharedBoundary.kt b/buildSrc/src/main/kotlin/org/harvestcircle/gradle/VerifySharedBoundary.kt @@ -1,36 +0,0 @@ -package org.harvestcircle.gradle - -import org.gradle.api.DefaultTask -import org.gradle.api.file.ConfigurableFileCollection -import org.gradle.api.tasks.InputFiles -import org.gradle.api.tasks.PathSensitive -import org.gradle.api.tasks.PathSensitivity -import org.gradle.api.tasks.TaskAction - -abstract class VerifySharedBoundary : DefaultTask() { - @get:InputFiles - @get:PathSensitive(PathSensitivity.RELATIVE) - abstract val commonSources: ConfigurableFileCollection - - @TaskAction - fun verify() { - val forbidden = - listOf( - "org.harvestcircle." + "ffi", - "com.sun." + "jna", - "java." + "awt", - "javax." + "swing", - "java." + "io", - "java." + "nio", - ) - val findings = - commonSources.files - .filter { it.isFile && it.extension == "kt" } - .flatMap { file -> - forbidden - .filter(file.readText()::contains) - .map { token -> "${file.name}: prohibited common-source dependency $token" } - } - check(findings.isEmpty()) { findings.sorted().joinToString("\n") } - } -}