app

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

commit 1187de3e2407ead7565bb52237f9ffbb8f2d53cb
parent 5dfce7dea1e2e111955dbebff1655950aaf68b48
Author: triesap <tyson@radroots.org>
Date:   Thu, 13 Aug 2026 15:05:09 +0000

build: govern owned design modules

- add desktop-only design-system and catalog modules
- centralize Compose KMP convention setup and routed outputs
- expose complete design format lint and test lifecycle tasks
- cover both new convention plugins with isolated functional tests

Diffstat:
MMakefile | 8++++----
Aapp/design_system/build.gradle.kts | 7+++++++
Mbuild-logic/plugins/build.gradle.kts | 8++++++++
Mbuild-logic/plugins/src/functionalTest/kotlin/org/harvestcircle/buildlogic/plugins/ConventionPluginSmokeTest.kt | 23++++++++++++++++++++++-
Abuild-logic/plugins/src/main/kotlin/org/harvestcircle/buildlogic/plugins/DesktopDesignKmp.kt | 65+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Abuild-logic/plugins/src/main/kotlin/org/harvestcircle/buildlogic/plugins/HarvestCircleDesignCatalogPlugin.kt | 37+++++++++++++++++++++++++++++++++++++
Abuild-logic/plugins/src/main/kotlin/org/harvestcircle/buildlogic/plugins/HarvestCircleDesignSystemPlugin.kt | 33+++++++++++++++++++++++++++++++++
Mbuild-logic/plugins/src/main/kotlin/org/harvestcircle/buildlogic/plugins/HarvestCircleKmpSharedPlugin.kt | 51++-------------------------------------------------
Mbuild.gradle.kts | 35+++++++++++++++++++++++++++++++++++
Msettings.gradle.kts | 7++++++-
Atools/design_catalog/build.gradle.kts | 9+++++++++
11 files changed, 228 insertions(+), 55 deletions(-)

diff --git a/Makefile b/Makefile @@ -54,7 +54,7 @@ mode-check: format: doctor $(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 + $(BUILD_RUNNER) $(GRADLE) --no-daemon :app:shared:ktlintCheck :app:desktop:ktlintCheck designFormatCheck format-fix: doctor $(BUILD_RUNNER) $(CARGO) fmt --manifest-path $(CARGO_MANIFEST) --all @@ -64,15 +64,15 @@ format-fix: doctor lint: doctor $(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 + $(BUILD_RUNNER) $(GRADLE) --no-daemon :app:shared:detektCommonMainSourceSet :app:shared:detektCommonTestSourceSet :app:desktop:detekt designLint test: doctor $(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 + $(BUILD_RUNNER) $(GRADLE) --no-daemon :app:shared:desktopTest :app:desktop:test designTest check: format lint test foundation-check mode-check - $(BUILD_RUNNER) $(GRADLE) --no-daemon :app:shared:check :app:desktop:check + $(BUILD_RUNNER) $(GRADLE) --no-daemon :app:shared:check :app:desktop:check designCheck governed-check: $(MAKE) --no-print-directory BUILD_MODE=governed check diff --git a/app/design_system/build.gradle.kts b/app/design_system/build.gradle.kts @@ -0,0 +1,7 @@ +plugins { + id("org.harvestcircle.build.design-system") +} + +compose.resources { + packageOfResClass = "org.harvestcircle.designsystem.generated.resources" +} diff --git a/build-logic/plugins/build.gradle.kts b/build-logic/plugins/build.gradle.kts @@ -46,6 +46,14 @@ gradlePlugin { id = "org.harvestcircle.build.kmp-shared" implementationClass = "org.harvestcircle.buildlogic.plugins.HarvestCircleKmpSharedPlugin" } + create("harvestCircleDesignSystem") { + id = "org.harvestcircle.build.design-system" + implementationClass = "org.harvestcircle.buildlogic.plugins.HarvestCircleDesignSystemPlugin" + } + create("harvestCircleDesignCatalog") { + id = "org.harvestcircle.build.design-catalog" + implementationClass = "org.harvestcircle.buildlogic.plugins.HarvestCircleDesignCatalogPlugin" + } create("harvestCircleDesktopApp") { id = "org.harvestcircle.build.desktop-app" implementationClass = "org.harvestcircle.buildlogic.plugins.HarvestCircleDesktopAppPlugin" 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 @@ -16,6 +16,8 @@ class ConventionPluginSmokeTest { listOf( "org.harvestcircle.build.root", "org.harvestcircle.build.kmp-shared", + "org.harvestcircle.build.design-system", + "org.harvestcircle.build.design-catalog", "org.harvestcircle.build.desktop-app", "org.harvestcircle.build.rust-ffi", "org.harvestcircle.build.packaging", @@ -35,16 +37,35 @@ class ConventionPluginSmokeTest { append("pluginManagement { repositories { gradlePluginPortal(); mavenCentral() } }\n") append("dependencyResolutionManagement { repositories { mavenCentral() } }\n") append("rootProject.name = \"fixture\"\n") + if (pluginId == "org.harvestcircle.build.design-system") { + append("include(\":app:design_system\")\n") + } + if (pluginId == "org.harvestcircle.build.design-catalog") { + append("include(\":app:design_system\", \":tools:design_catalog\")\n") + } if (desktopFixture) append("include(\":app:shared\", \":app:desktop\")\n") }, ) - if (pluginId == "org.harvestcircle.build.kmp-shared" || desktopFixture) { + if ( + pluginId == "org.harvestcircle.build.kmp-shared" || + pluginId.startsWith("org.harvestcircle.build.design-") || + desktopFixture + ) { fixture.resolve("gradle").createDirectories().resolve("libs.versions.toml").writeText(kmpCatalog) } val buildFile = if (desktopFixture) { prepareDesktopFixture(fixture) fixture.resolve("app/desktop/build.gradle.kts") + } else if (pluginId == "org.harvestcircle.build.design-system") { + fixture.resolve("app/design_system").createDirectories() + .resolve("build.gradle.kts") + } else if (pluginId == "org.harvestcircle.build.design-catalog") { + fixture.resolve("app/design_system").createDirectories() + .resolve("build.gradle.kts") + .writeText("plugins { `java-library` }\n") + fixture.resolve("tools/design_catalog").createDirectories() + .resolve("build.gradle.kts") } else { fixture.resolve("build.gradle.kts") } diff --git a/build-logic/plugins/src/main/kotlin/org/harvestcircle/buildlogic/plugins/DesktopDesignKmp.kt b/build-logic/plugins/src/main/kotlin/org/harvestcircle/buildlogic/plugins/DesktopDesignKmp.kt @@ -0,0 +1,65 @@ +package org.harvestcircle.buildlogic.plugins + +import org.gradle.api.Project +import org.gradle.api.artifacts.VersionCatalog +import org.gradle.api.artifacts.VersionCatalogsExtension +import org.harvestcircle.buildlogic.contracts.resolveNativeTarget +import org.jlleitschuh.gradle.ktlint.KtlintExtension +import org.jetbrains.kotlin.gradle.dsl.JvmTarget +import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension +import org.jetbrains.kotlin.gradle.plugin.KotlinPlatformType + +internal fun Project.applyDesktopDesignKmp() { + pluginManager.apply("org.jetbrains.kotlin.multiplatform") + pluginManager.apply("org.jetbrains.compose") + pluginManager.apply("org.jetbrains.kotlin.plugin.compose") + pluginManager.apply("dev.detekt") + pluginManager.apply("org.jlleitschuh.gradle.ktlint") + + extensions.configure(KtlintExtension::class.java) { extension -> + extension.additionalEditorconfig.set( + mapOf("ktlint_function_naming_ignore_when_annotated_with" to "Composable"), + ) + } + extensions.configure(KotlinMultiplatformExtension::class.java) { kotlin -> + kotlin.jvm("desktop") { jvm -> + jvm.compilerOptions.jvmTarget.set(JvmTarget.JVM_21) + } + kotlin.jvmToolchain(21) + check( + kotlin.targets + .filter { it.platformType != KotlinPlatformType.common } + .map { it.name } == listOf("desktop"), + ) { + "$path must declare exactly one KMP platform target named desktop" + } + } +} + +internal fun Project.versionCatalog(): VersionCatalog = + extensions.getByType(VersionCatalogsExtension::class.java).named("libs") + +internal fun Project.routeDesignBuildDirectory(directoryName: String) { + providers.environmentVariable("EXT_BUILD_GRADLE_BUILD_DIR").orNull?.let { outputRoot -> + layout.buildDirectory.set(file(outputRoot).resolve(directoryName)) + } +} + +internal fun composeDesktopDependency( + target: Project, + composeVersion: String, +): String { + val osName = target.providers.gradleProperty("nativeOs").getOrElse(System.getProperty("os.name")) + val architecture = target.providers.gradleProperty("nativeArch").getOrElse(System.getProperty("os.arch")) + val suffix = + when (resolveNativeTarget(osName, architecture, "harvestcircle_ffi").jnaPrefix) { + "darwin-aarch64" -> "macos-arm64" + "darwin-x86-64" -> "macos-x64" + "linux-aarch64" -> "linux-arm64" + "linux-x86-64" -> "linux-x64" + "win32-aarch64" -> "windows-arm64" + "win32-x86-64" -> "windows-x64" + else -> error("Unsupported Compose desktop host: $osName/$architecture") + } + return "org.jetbrains.compose.desktop:desktop-jvm-$suffix:$composeVersion" +} diff --git a/build-logic/plugins/src/main/kotlin/org/harvestcircle/buildlogic/plugins/HarvestCircleDesignCatalogPlugin.kt b/build-logic/plugins/src/main/kotlin/org/harvestcircle/buildlogic/plugins/HarvestCircleDesignCatalogPlugin.kt @@ -0,0 +1,37 @@ +package org.harvestcircle.buildlogic.plugins + +import org.gradle.api.Plugin +import org.gradle.api.Project + +public class HarvestCircleDesignCatalogPlugin : Plugin<Project> { + override fun apply(target: Project) { + require(target.path == ":tools:design_catalog") { + "The HarvestCircle design-catalog plugin may only be applied to :tools:design_catalog" + } + target.applyDesktopDesignKmp() + val catalog = target.versionCatalog() + val kotlinVersion = catalog.findVersion("kotlin").get().requiredVersion + target.dependencies.add("commonMainImplementation", target.project(":app:design_system")) + target.dependencies.add( + "commonMainImplementation", + catalog.findLibrary("compose-foundation").get(), + ) + target.dependencies.add( + "commonMainImplementation", + catalog.findLibrary("compose-runtime").get(), + ) + target.dependencies.add( + "commonMainImplementation", + catalog.findLibrary("compose-ui").get(), + ) + target.dependencies.add( + "commonTestImplementation", + "org.jetbrains.kotlin:kotlin-test:$kotlinVersion", + ) + target.dependencies.add( + "desktopMainImplementation", + composeDesktopDependency(target, catalog.findVersion("compose").get().requiredVersion), + ) + target.routeDesignBuildDirectory("tools-design-catalog") + } +} diff --git a/build-logic/plugins/src/main/kotlin/org/harvestcircle/buildlogic/plugins/HarvestCircleDesignSystemPlugin.kt b/build-logic/plugins/src/main/kotlin/org/harvestcircle/buildlogic/plugins/HarvestCircleDesignSystemPlugin.kt @@ -0,0 +1,33 @@ +package org.harvestcircle.buildlogic.plugins + +import org.gradle.api.Plugin +import org.gradle.api.Project +import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension + +public class HarvestCircleDesignSystemPlugin : Plugin<Project> { + override fun apply(target: Project) { + require(target.path == ":app:design_system") { + "The HarvestCircle design-system plugin may only be applied to :app:design_system" + } + target.applyDesktopDesignKmp() + target.extensions.configure(KotlinMultiplatformExtension::class.java) { kotlin -> + kotlin.explicitApi() + } + val catalog = target.versionCatalog() + val kotlinVersion = catalog.findVersion("kotlin").get().requiredVersion + target.dependencies.add( + "commonMainApi", + catalog.findLibrary("compose-foundation").get(), + ) + target.dependencies.add( + "commonMainApi", + catalog.findLibrary("compose-runtime").get(), + ) + target.dependencies.add("commonMainApi", catalog.findLibrary("compose-ui").get()) + target.dependencies.add( + "commonTestImplementation", + "org.jetbrains.kotlin:kotlin-test:$kotlinVersion", + ) + target.routeDesignBuildDirectory("app-design-system") + } +} diff --git a/build-logic/plugins/src/main/kotlin/org/harvestcircle/buildlogic/plugins/HarvestCircleKmpSharedPlugin.kt b/build-logic/plugins/src/main/kotlin/org/harvestcircle/buildlogic/plugins/HarvestCircleKmpSharedPlugin.kt @@ -2,45 +2,16 @@ package org.harvestcircle.buildlogic.plugins import org.gradle.api.Plugin import org.gradle.api.Project -import org.gradle.api.artifacts.VersionCatalogsExtension -import org.harvestcircle.buildlogic.contracts.resolveNativeTarget import org.harvestcircle.buildlogic.plugins.tasks.VerifySharedBoundary -import org.jlleitschuh.gradle.ktlint.KtlintExtension -import org.jetbrains.kotlin.gradle.dsl.JvmTarget -import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension -import org.jetbrains.kotlin.gradle.plugin.KotlinPlatformType public class HarvestCircleKmpSharedPlugin : Plugin<Project> { override fun apply(target: Project) { - target.pluginManager.apply("org.jetbrains.kotlin.multiplatform") - target.pluginManager.apply("org.jetbrains.compose") - target.pluginManager.apply("org.jetbrains.kotlin.plugin.compose") - target.pluginManager.apply("dev.detekt") - target.pluginManager.apply("org.jlleitschuh.gradle.ktlint") + target.applyDesktopDesignKmp() - val catalog = target.extensions.getByType(VersionCatalogsExtension::class.java).named("libs") + val catalog = target.versionCatalog() val kotlinVersion = catalog.findVersion("kotlin").get().requiredVersion val composeVersion = catalog.findVersion("compose").get().requiredVersion - target.extensions.configure(KtlintExtension::class.java) { extension -> - extension.additionalEditorconfig.set( - mapOf("ktlint_function_naming_ignore_when_annotated_with" to "Composable"), - ) - } - target.extensions.configure(KotlinMultiplatformExtension::class.java) { kotlin -> - kotlin.jvm("desktop") { jvm -> - jvm.compilerOptions.jvmTarget.set(JvmTarget.JVM_21) - } - kotlin.jvmToolchain(21) - check( - kotlin.targets - .filter { it.platformType != KotlinPlatformType.common } - .map { it.name } == listOf("desktop"), - ) { - "HarvestCircle shared must declare exactly one KMP platform target named desktop" - } - } - target.dependencies.add("commonMainImplementation", catalog.findLibrary("compose-foundation").get()) target.dependencies.add("commonMainImplementation", catalog.findLibrary("compose-runtime").get()) target.dependencies.add("commonMainImplementation", catalog.findLibrary("compose-ui").get()) @@ -64,22 +35,4 @@ public class HarvestCircleKmpSharedPlugin : Plugin<Project> { target.tasks.named("check") { task -> task.dependsOn(verifySharedBoundary) } } - private fun composeDesktopDependency( - target: Project, - composeVersion: String, - ): String { - val osName = target.providers.gradleProperty("nativeOs").getOrElse(System.getProperty("os.name")) - val architecture = target.providers.gradleProperty("nativeArch").getOrElse(System.getProperty("os.arch")) - val suffix = - when (resolveNativeTarget(osName, architecture, "harvestcircle_ffi").jnaPrefix) { - "darwin-aarch64" -> "macos-arm64" - "darwin-x86-64" -> "macos-x64" - "linux-aarch64" -> "linux-arm64" - "linux-x86-64" -> "linux-x64" - "win32-aarch64" -> "windows-arm64" - "win32-x86-64" -> "windows-x64" - else -> error("Unsupported Compose desktop host: $osName/$architecture") - } - return "org.jetbrains.compose.desktop:desktop-jvm-$suffix:$composeVersion" - } } diff --git a/build.gradle.kts b/build.gradle.kts @@ -1,3 +1,38 @@ plugins { id("org.harvestcircle.build.root") } + +tasks.register("designFormatCheck") { + group = "verification" + description = "Checks formatting for every HarvestCircle-owned design module." + dependsOn( + ":app:design_system:ktlintCheck", + ":tools:design_catalog:ktlintCheck", + ) +} + +tasks.register("designLint") { + group = "verification" + description = "Runs static analysis for every HarvestCircle-owned design module." + dependsOn( + ":app:design_system:detektCommonMainSourceSet", + ":app:design_system:detektCommonTestSourceSet", + ":tools:design_catalog:detektCommonMainSourceSet", + ":tools:design_catalog:detektCommonTestSourceSet", + ) +} + +tasks.register("designTest") { + group = "verification" + description = "Runs tests for every HarvestCircle-owned design module." + dependsOn( + ":app:design_system:desktopTest", + ":tools:design_catalog:desktopTest", + ) +} + +tasks.register("designCheck") { + group = "verification" + description = "Runs the complete HarvestCircle-owned design module lifecycle." + dependsOn("designFormatCheck", "designLint", "designTest") +} diff --git a/settings.gradle.kts b/settings.gradle.kts @@ -43,4 +43,9 @@ rootProject.name = "harvestcircle" enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS") -include(":app:desktop", ":app:shared") +include( + ":app:design_system", + ":app:desktop", + ":app:shared", + ":tools:design_catalog", +) diff --git a/tools/design_catalog/build.gradle.kts b/tools/design_catalog/build.gradle.kts @@ -0,0 +1,9 @@ +plugins { + id("org.harvestcircle.build.design-catalog") +} + +compose.desktop { + application { + mainClass = "org.harvestcircle.designcatalog.MainKt" + } +}