app

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

commit 7fdf93c81beeec1fc35ee38b537fea29886729b2
parent 2d008582eb1ae1f1282e33f0e6bf6b9782ae61a4
Author: triesap <tyson@radroots.org>
Date:   Thu,  6 Aug 2026 20:57:08 +0000

studio: qualify native product packaging

- remove ambient workspace dependencies from Cargo tasks
- select native installation formats for each desktop host
- inspect Linux and Windows packages for one canonical library
- keep macOS signing gates isolated to the macOS host

Diffstat:
MMakefile | 2+-
Mapp/desktop/build.gradle.kts | 156++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----------
2 files changed, 136 insertions(+), 22 deletions(-)

diff --git a/Makefile b/Makefile @@ -58,7 +58,7 @@ licenses: doctor $(EXTBUILD) $(GRADLE) --no-daemon --no-parallel --no-configuration-cache :app:desktop:checkLicense package: check - $(EXTBUILD) $(GRADLE) --no-daemon :app:desktop:verifyMacOsPackage + $(EXTBUILD) $(GRADLE) --no-daemon :app:desktop:verifyHostPackage release-check: doctor $(EXTBUILD) $(CARGO) audit --file core/Cargo.lock diff --git a/app/desktop/build.gradle.kts b/app/desktop/build.gradle.kts @@ -205,7 +205,7 @@ val syncRadrootsLibSource by tasks.registering(SyncRadrootsLibSource::class) { val validateRadrootsSourceLock by tasks.registering(Exec::class) { dependsOn(syncRadrootsLibSource) - workingDir(rootProject.projectDir) + workingDir(radrootsLibSource) commandLine( "cargo", "run", @@ -251,11 +251,12 @@ fun resolveNativeTarget( } } -val nativeTarget = - resolveNativeTarget( - providers.gradleProperty("nativeOs").getOrElse(System.getProperty("os.name")), - providers.gradleProperty("nativeArch").getOrElse(System.getProperty("os.arch")), - ) +val nativeOsName = providers.gradleProperty("nativeOs").getOrElse(System.getProperty("os.name")) +val nativeArchitecture = providers.gradleProperty("nativeArch").getOrElse(System.getProperty("os.arch")) +val nativeTarget = resolveNativeTarget(nativeOsName, nativeArchitecture) +val isMacOsHost = nativeOsName.lowercase().startsWith("mac") +val isLinuxHost = nativeOsName.lowercase().startsWith("linux") +val isWindowsHost = nativeOsName.lowercase().startsWith("windows") val rustLibraryName = nativeTarget.libraryName val rustDebugLibrary = file(cargoTargetRoot).resolve("debug/$rustLibraryName") val rustReleaseLibrary = file(cargoTargetRoot).resolve("release/$rustLibraryName") @@ -263,7 +264,7 @@ val jnaPlatformPrefix = nativeTarget.jnaPrefix val buildRustCoreDebug by tasks.registering(Exec::class) { dependsOn(validateRadrootsSourceLock) - workingDir(rootProject.projectDir) + workingDir(radrootsLibSource) commandLine( "cargo", "build", @@ -280,7 +281,7 @@ val buildRustCoreDebug by tasks.registering(Exec::class) { val buildRustCoreRelease by tasks.registering(Exec::class) { dependsOn(validateRadrootsSourceLock) - workingDir(rootProject.projectDir) + workingDir(radrootsLibSource) commandLine( "cargo", "build", @@ -303,7 +304,7 @@ val cleanGeneratedUniFfiKotlin by tasks.registering(Delete::class) { } val generateUniFfiKotlin by tasks.registering(Exec::class) { dependsOn(buildRustCoreDebug, cleanGeneratedUniFfiKotlin) - workingDir(rootProject.projectDir) + workingDir(radrootsLibSource) commandLine( "cargo", "run", @@ -525,6 +526,86 @@ abstract class VerifyMacOsPackage : DefaultTask() { } } +abstract class VerifyNativeInstallPackage : DefaultTask() { + @get:InputDirectory + @get:PathSensitive(PathSensitivity.RELATIVE) + abstract val packageDirectory: DirectoryProperty + + @get:InputFile + @get:PathSensitive(PathSensitivity.NONE) + abstract val releaseLibrary: RegularFileProperty + + @get:Input + abstract val packageExtension: Property<String> + + @get:Input + abstract val expectedVersion: Property<String> + + @get:Input + abstract val expectedNativeEntry: Property<String> + + @get:Input + abstract val hostFamily: Property<String> + + @TaskAction + fun verify() { + val extension = packageExtension.get() + val packages = packageDirectory.asFileTree.files.filter { it.isFile && it.extension == extension } + require(packages.size == 1) { "Expected exactly one .$extension installation package" } + val installPackage = packages.single() + require(installPackage.name.contains(expectedVersion.get())) { + "Installation package name does not contain the governed version" + } + require(installPackage.length() > 0L) { "Installation package is empty" } + + val extracted = temporaryDir.resolve("extracted").apply { mkdirs() } + when (hostFamily.get()) { + "linux" -> commandOutput("dpkg-deb", "--extract", installPackage.absolutePath, extracted.absolutePath) + "windows" -> + commandOutput( + "msiexec.exe", + "/a", + installPackage.absolutePath, + "/qn", + "TARGETDIR=${extracted.absolutePath}", + ) + else -> throw GradleException("Unsupported native package host") + } + + val expectedEntry = expectedNativeEntry.get() + val packagedLibraries = mutableListOf<ByteArray>() + extracted + .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) { + "Installation package must contain exactly one canonical native library" + } + require(packagedLibraries.single().contentEquals(releaseLibrary.get().asFile.readBytes())) { + "Installed native library does not match the canonical Cargo release artifact" + } + } + + private fun commandOutput(vararg command: String) { + val process = + ProcessBuilder(*command) + .redirectErrorStream(true) + .start() + val output = + process.inputStream + .bufferedReader() + .use { it.readText() } + .trim() + require(process.waitFor() == 0) { "Installation package extraction failed: $output" } + } +} + abstract class VerifyMacOsDeveloperIdSignature : DefaultTask() { @get:InputDirectory @get:PathSensitive(PathSensitivity.RELATIVE) @@ -697,14 +778,23 @@ compose.desktop { fromFiles(desktopJar, configurations.runtimeClasspath, releaseNativeRuntimeJar) mainClass = "$applicationNamespace.desktop.MainKt" - jvmArgs += - listOf( - "-Dapple.awt.application.name=$applicationName", - "-Dapple.awt.application.appearance=system", - ) + if (isMacOsHost) { + jvmArgs += + listOf( + "-Dapple.awt.application.name=$applicationName", + "-Dapple.awt.application.appearance=system", + ) + } nativeDistributions { - targetFormats(TargetFormat.Dmg) + targetFormats( + when { + isMacOsHost -> TargetFormat.Dmg + isLinuxHost -> TargetFormat.Deb + isWindowsHost -> TargetFormat.Msi + else -> throw GradleException("Unsupported desktop package host: $nativeOsName") + }, + ) packageName = applicationName packageVersion = installableVersion @@ -741,6 +831,32 @@ val verifyMacOsPackage by tasks.registering(VerifyMacOsPackage::class) { packageDirectory.set(layout.buildDirectory.dir("compose/binaries/main/dmg")) expectedFileName.set("$applicationName-$installableVersion.dmg") } +val verifyLinuxPackage by tasks.registering(VerifyNativeInstallPackage::class) { + dependsOn("packageDeb", verifyReleaseNativeLibrary) + packageDirectory.set(layout.buildDirectory.dir("compose/binaries/main/deb")) + releaseLibrary.set(rustReleaseLibrary) + packageExtension.set("deb") + expectedVersion.set(installableVersion) + expectedNativeEntry.set("$jnaPlatformPrefix/$rustLibraryName") + hostFamily.set("linux") +} +val verifyWindowsPackage by tasks.registering(VerifyNativeInstallPackage::class) { + dependsOn("packageMsi", verifyReleaseNativeLibrary) + packageDirectory.set(layout.buildDirectory.dir("compose/binaries/main/msi")) + releaseLibrary.set(rustReleaseLibrary) + packageExtension.set("msi") + expectedVersion.set(installableVersion) + expectedNativeEntry.set("$jnaPlatformPrefix/$rustLibraryName") + hostFamily.set("windows") +} +val verifyHostPackage by tasks.registering { + when { + isMacOsHost -> dependsOn(verifyMacOsPackage) + isLinuxHost -> dependsOn(verifyLinuxPackage) + isWindowsHost -> dependsOn(verifyWindowsPackage) + else -> throw GradleException("Unsupported desktop package host: $nativeOsName") + } +} val verifyMacOsDeveloperIdSignature by tasks.registering(VerifyMacOsDeveloperIdSignature::class) { dependsOn(verifyMacOsPackage) appDirectory.set(layout.buildDirectory.dir("compose/binaries/main/app/$applicationName.app")) @@ -750,10 +866,8 @@ val verifyMacOsNotarization by tasks.registering(VerifyMacOsNotarization::class) diskImage.set(layout.buildDirectory.file("compose/binaries/main/dmg/$applicationName-$installableVersion.dmg")) } tasks.register("releaseReadiness") { - dependsOn( - "checkLicense", - "dependencyCheckAnalyze", - verifyMacOsDeveloperIdSignature, - verifyMacOsNotarization, - ) + dependsOn("checkLicense", "dependencyCheckAnalyze", verifyHostPackage) + if (isMacOsHost) { + dependsOn(verifyMacOsDeveloperIdSignature, verifyMacOsNotarization) + } }