app

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

commit f481cf7d57a6a68d46330a2bc96e274ba46b2ab2
parent 5ed105f04deff637c9f57cb0bcbc82565d7a0590
Author: triesap <tyson@radroots.org>
Date:   Tue,  4 Aug 2026 00:45:40 +0000

build: verify macOS release provenance

- source runtime and installer coordinates from their canonical contracts
- isolate release-native packaging from the debug and test classpaths
- verify bundle identity, versions, icon, and signed Mach-O provenance
- make the governed package target inspect the completed macOS disk image

Diffstat:
MMakefile | 2+-
Mapp/desktop/build.gradle.kts | 208++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----
Mapp/desktop/src/test/kotlin/org/radroots/studio/architecture/V5CompatibilityBaselineTest.kt | 11+++++++----
3 files changed, 204 insertions(+), 17 deletions(-)

diff --git a/Makefile b/Makefile @@ -47,7 +47,7 @@ run: doctor $(GRADLE) :app:desktop:run package: check - $(GRADLE) --no-daemon :app:desktop:packageDistributionForCurrentOS + $(GRADLE) --no-daemon :app:desktop:verifyMacOsPackage clean: doctor $(CARGO) clean --manifest-path $(CARGO_MANIFEST) diff --git a/app/desktop/build.gradle.kts b/app/desktop/build.gradle.kts @@ -17,8 +17,9 @@ import org.gradle.api.tasks.testing.Test import org.gradle.jvm.tasks.Jar import org.jetbrains.compose.desktop.application.dsl.TargetFormat import org.jetbrains.kotlin.gradle.dsl.JvmTarget - -version = "0.1.0-alpha" +import java.io.File +import java.util.Properties +import java.util.jar.JarFile plugins { alias(libs.plugins.kotlin.jvm) @@ -44,6 +45,44 @@ tasks.withType<org.jlleitschuh.gradle.ktlint.tasks.BaseKtLintCheckTask>().config } val rustManifest = rootProject.layout.projectDirectory.file("core/Cargo.toml") +val compatibilityBaseline = rootProject.layout.projectDirectory.file("core/compatibility/v5-baseline.properties") + +fun workspacePackageVersion(manifest: File): String { + val packageSection = + manifest + .readText() + .substringAfter("[workspace.package]", missingDelimiterValue = "") + .substringBefore("\n[") + return Regex("""(?m)^version\s*=\s*"([^"]+)"\s*$""") + .find(packageSection) + ?.groupValues + ?.get(1) + ?: throw GradleException("Cargo workspace package version is missing") +} + +val appVersion = workspacePackageVersion(rustManifest.asFile) +val macOsBuildVersion = "1" +val baseline = + Properties().apply { + compatibilityBaseline.asFile.inputStream().use(::load) + } + +fun baselineValue(key: String): String = + baseline.getProperty(key)?.takeIf(String::isNotBlank) + ?: throw GradleException("Compatibility baseline is missing $key") + +check(baselineValue("ffi.runtime.version") == appVersion) { + "Compatibility runtime version must match the Cargo workspace version" +} +val installableVersion = baselineValue("package.version") +check(Regex("""[1-9]\d*(\.\d+){0,2}""").matches(installableVersion)) { + "Compatibility package version must satisfy the macOS jpackage contract" +} +val applicationName = baselineValue("package.name") +val bundleId = baselineValue("package.bundle_id") +val applicationNamespace = baselineValue("package.namespace") +version = appVersion + val rustSources = rootProject.fileTree("core") { include( @@ -232,6 +271,128 @@ val releaseNativeResourcesJar by tasks.registering(Jar::class) { archiveClassifier.set("release-native-resources") from(generatedReleaseNativeResources) } +val releaseNativeRuntimeJar = + releaseNativeResourcesJar + .get() + .archiveFile + .get() + .asFile + +abstract class VerifyMacOsDistribution : DefaultTask() { + @get:InputDirectory + @get:PathSensitive(PathSensitivity.RELATIVE) + abstract val appDirectory: DirectoryProperty + + @get:InputFile + @get:PathSensitive(PathSensitivity.NONE) + abstract val releaseLibrary: RegularFileProperty + + @get:InputFile + @get:PathSensitive(PathSensitivity.NONE) + abstract val iconSource: RegularFileProperty + + @get:Input + abstract val expectedBundleId: Property<String> + + @get:Input + abstract val expectedPackageVersion: Property<String> + + @get:Input + abstract val expectedBuildVersion: Property<String> + + @get:Input + abstract val expectedNativeEntry: Property<String> + + @TaskAction + fun verify() { + val app = appDirectory.get().asFile + val plist = app.resolve("Contents/Info.plist") + require(plist.isFile) { "Packaged macOS Info.plist is missing" } + require(plistValue(plist, "CFBundleIdentifier") == expectedBundleId.get()) { + "Packaged macOS bundle identifier is incorrect" + } + require(plistValue(plist, "CFBundleShortVersionString") == expectedPackageVersion.get()) { + "Packaged macOS version is incorrect" + } + require(plistValue(plist, "CFBundleVersion") == expectedBuildVersion.get()) { + "Packaged macOS build version is incorrect" + } + + val sourceIcon = iconSource.get().asFile.readBytes() + val matchingIcons = + app + .walkTopDown() + .filter { it.isFile && it.extension == "icns" } + .count { it.readBytes().contentEquals(sourceIcon) } + require(matchingIcons == 1) { "Packaged macOS icon does not match the canonical icon" } + + val expectedEntry = expectedNativeEntry.get() + val packagedLibraries = mutableListOf<ByteArray>() + app + .walkTopDown() + .filter { it.isFile && it.extension == "jar" } + .forEach { jarFile -> + JarFile(jarFile).use { jar -> + jar.getJarEntry(expectedEntry)?.let { entry -> + packagedLibraries += jar.getInputStream(entry).use { it.readBytes() } + } + } + } + require(packagedLibraries.size == 1) { + "Packaged application must contain exactly one release native library" + } + val release = releaseLibrary.get().asFile + val packaged = temporaryDir.resolve(release.name).apply { writeBytes(packagedLibraries.single()) } + require(machOIdentity(packaged) == machOIdentity(release)) { + "Packaged native library identity does not match the Cargo release artifact" + } + commandOutput("/usr/bin/codesign", "--verify", "--strict", packaged.absolutePath) + } + + private fun plistValue( + plist: File, + key: String, + ): String = commandOutput("/usr/libexec/PlistBuddy", "-c", "Print :$key", plist.absolutePath) + + private fun machOIdentity(binary: File): String { + val output = commandOutput("/usr/bin/dwarfdump", "--uuid", binary.absolutePath) + return Regex("""UUID: ([0-9A-F-]+) \(([^)]+)\)""") + .find(output) + ?.value + ?: throw GradleException("Could not read the packaged native library identity") + } + + private fun commandOutput(vararg command: String): String { + val process = + ProcessBuilder(*command) + .redirectErrorStream(true) + .start() + val output = + process.inputStream + .bufferedReader() + .use { it.readText() } + .trim() + require(process.waitFor() == 0) { "External package inspection failed: $output" } + return output + } +} + +abstract class VerifyMacOsPackage : DefaultTask() { + @get:InputDirectory + @get:PathSensitive(PathSensitivity.RELATIVE) + abstract val packageDirectory: DirectoryProperty + + @get:Input + abstract val expectedFileName: Property<String> + + @TaskAction + fun verify() { + val packages = packageDirectory.asFileTree.files.filter { it.isFile && it.extension == "dmg" } + require(packages.size == 1) { "Expected exactly one macOS disk image" } + require(packages.single().name == expectedFileName.get()) { "Unexpected macOS disk image name" } + require(packages.single().length() > 0L) { "Packaged macOS disk image is empty" } + } +} dependencies { implementation(compose.desktop.currentOs) @@ -317,32 +478,55 @@ tasks.withType<JavaExec>().configureEach { compose.desktop { application { + disableDefaultConfiguration() dependsOn(releaseNativeResourcesJar.get()) - fromFiles(releaseNativeResourcesJar) - mainClass = "org.radroots.studio.desktop.MainKt" + dependsOn("jar") + val desktopJar = tasks.named<Jar>("jar").flatMap { it.archiveFile } + mainJar.set(desktopJar) + fromFiles(desktopJar, configurations.runtimeClasspath, releaseNativeRuntimeJar) + mainClass = "$applicationNamespace.desktop.MainKt" jvmArgs += listOf( - "-Dapple.awt.application.name=Radroots", + "-Dapple.awt.application.name=$applicationName", "-Dapple.awt.application.appearance=system", ) nativeDistributions { targetFormats(TargetFormat.Dmg) - packageName = "Radroots" - packageVersion = "1.0.0" - description = "Radroots Studio 0.1.0-alpha" + packageName = applicationName + packageVersion = installableVersion + description = "Radroots Studio $appVersion" copyright = "Copyright © 2024 Radroots, Inc." vendor = "Radroots, Inc" macOS { - bundleID = "org.radroots.studio" + bundleID = bundleId iconFile.set(project.file("src/main/resources/icons/radroots.icns")) - packageName = "Radroots" - dockName = "Radroots" - packageBuildVersion = "1" + packageName = applicationName + dockName = applicationName + packageBuildVersion = macOsBuildVersion } } } } + +val verifyMacOsDistribution by tasks.registering(VerifyMacOsDistribution::class) { + dependsOn("createDistributable") + appDirectory.set(layout.buildDirectory.dir("compose/binaries/main/app/$applicationName.app")) + releaseLibrary.set(rustReleaseLibrary) + iconSource.set(layout.projectDirectory.file("src/main/resources/icons/radroots.icns")) + expectedBundleId.set(bundleId) + expectedPackageVersion.set(installableVersion) + expectedBuildVersion.set(macOsBuildVersion) + expectedNativeEntry.set("$jnaPlatformPrefix/$rustLibraryName") +} +tasks.matching { it.name == "createDistributable" }.configureEach { + dependsOn(releaseNativeResourcesJar) +} +val verifyMacOsPackage by tasks.registering(VerifyMacOsPackage::class) { + dependsOn("packageDmg", verifyMacOsDistribution) + packageDirectory.set(layout.buildDirectory.dir("compose/binaries/main/dmg")) + expectedFileName.set("$applicationName-$installableVersion.dmg") +} diff --git a/app/desktop/src/test/kotlin/org/radroots/studio/architecture/V5CompatibilityBaselineTest.kt b/app/desktop/src/test/kotlin/org/radroots/studio/architecture/V5CompatibilityBaselineTest.kt @@ -34,11 +34,14 @@ class V5CompatibilityBaselineTest { baseline.getProperty("keyring.account"), ) + val manifest = root.resolve("core/Cargo.toml").readText() + val workspacePackage = manifest.substringAfter("[workspace.package]").substringBefore("\n[") + assertTrue(workspacePackage.contains("version = \"${baseline.getProperty("ffi.runtime.version")}\"")) val build = root.resolve("app/desktop/build.gradle.kts").readText() - assertTrue(build.contains("version = \"${baseline.getProperty("ffi.runtime.version")}\"")) - assertTrue(build.contains("packageName = \"${baseline.getProperty("package.name")}\"")) - assertTrue(build.contains("bundleID = \"${baseline.getProperty("package.bundle_id")}\"")) - assertTrue(build.contains("packageVersion = \"${baseline.getProperty("package.version")}\"")) + assertTrue(build.contains("version = appVersion")) + assertTrue(build.contains("packageName = applicationName")) + assertTrue(build.contains("bundleID = bundleId")) + assertTrue(build.contains("packageVersion = installableVersion")) } }