import javax.inject.Inject plugins { alias(libs.plugins.android.application) alias(libs.plugins.kotlin.compose) alias(libs.plugins.ksp) } android { namespace = "net.metacircular.engpad" compileSdk = 36 defaultConfig { applicationId = "net.metacircular.engpad" minSdk = 30 targetSdk = 36 versionCode = 1 versionName = "0.1.0" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" } buildTypes { release { isMinifyEnabled = true proguardFiles( getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro" ) } } compileOptions { sourceCompatibility = JavaVersion.VERSION_17 targetCompatibility = JavaVersion.VERSION_17 } buildFeatures { compose = true } lint { warningsAsErrors = true abortOnError = true checkDependencies = true } } kotlin { jvmToolchain(17) } // --- Protobuf / gRPC code generation via protoc --- // // The protobuf-gradle-plugin (0.9.6) does not yet support AGP 9, so we drive // protoc directly. A custom task exposes DirectoryProperty outputs that the // AGP Variant API can consume. abstract class GenerateProtoTask : DefaultTask() { @get:Inject abstract val execOps: ExecOperations @get:InputDirectory abstract val protoSourceDir: DirectoryProperty @get:InputFiles abstract val protocFile: ConfigurableFileCollection @get:InputFiles abstract val grpcJavaPluginFile: ConfigurableFileCollection @get:InputFiles abstract val grpcKotlinPluginFile: ConfigurableFileCollection /** Protobuf JAR containing well-known type .proto files (google/protobuf/*.proto). */ @get:InputFiles abstract val protobufIncludesJar: ConfigurableFileCollection /** Temp directory for extracting well-known proto includes. */ @get:OutputDirectory abstract val includesExtractDir: DirectoryProperty /** Java sources generated by protoc (messages). */ @get:OutputDirectory abstract val javaOutputDir: DirectoryProperty /** Kotlin sources generated by protoc (Kotlin DSL builders). */ @get:OutputDirectory abstract val kotlinOutputDir: DirectoryProperty /** Java sources generated by protoc-gen-grpc-java (service stubs). */ @get:OutputDirectory abstract val grpcOutputDir: DirectoryProperty /** Kotlin sources generated by protoc-gen-grpc-kotlin (coroutine stubs). */ @get:OutputDirectory abstract val grpcKtOutputDir: DirectoryProperty @TaskAction fun generate() { val javaOut = javaOutputDir.get().asFile val kotlinOut = kotlinOutputDir.get().asFile val grpcOut = grpcOutputDir.get().asFile val grpcKtOut = grpcKtOutputDir.get().asFile listOf(javaOut, kotlinOut, grpcOut, grpcKtOut).forEach { it.mkdirs() } // Extract well-known .proto files from the protobuf JAR. val extractDir = includesExtractDir.get().asFile extractDir.mkdirs() val jar = protobufIncludesJar.singleFile java.util.zip.ZipFile(jar).use { zip -> zip.entries().asSequence() .filter { it.name.endsWith(".proto") } .forEach { entry -> val outFile = File(extractDir, entry.name) outFile.parentFile.mkdirs() zip.getInputStream(entry).use { input -> outFile.outputStream().use { output -> input.copyTo(output) } } } } val protoc = protocFile.singleFile.also { it.setExecutable(true) } val grpcJava = grpcJavaPluginFile.singleFile.also { it.setExecutable(true) } val grpcKt = grpcKotlinPluginFile.singleFile.also { it.setExecutable(true) } val protoDir = protoSourceDir.get().asFile val protoFiles = protoDir.walkTopDown().filter { it.extension == "proto" }.toList() if (protoFiles.isEmpty()) return execOps.exec { commandLine( protoc.absolutePath, "--proto_path=${protoDir.absolutePath}", "--proto_path=${extractDir.absolutePath}", "--java_out=lite:${javaOut.absolutePath}", "--kotlin_out=lite:${kotlinOut.absolutePath}", "--plugin=protoc-gen-grpc=${grpcJava.absolutePath}", "--grpc_out=lite:${grpcOut.absolutePath}", "--plugin=protoc-gen-grpckt=${grpcKt.absolutePath}", "--grpckt_out=lite:${grpcKtOut.absolutePath}", *protoFiles.map { it.absolutePath }.toTypedArray(), ) } } } val protobufIncludes: Configuration by configurations.creating { isTransitive = false } val protocArtifact: Configuration by configurations.creating { isTransitive = false } val grpcJavaPlugin: Configuration by configurations.creating { isTransitive = false } val grpcKotlinPlugin: Configuration by configurations.creating { isTransitive = false } // Determine the OS/arch classifier for native protoc binaries. val protocClassifier: String = run { val os = System.getProperty("os.name").lowercase() val arch = System.getProperty("os.arch").lowercase() val osName = when { os.contains("mac") || os.contains("darwin") -> "osx" os.contains("linux") -> "linux" os.contains("windows") -> "windows" else -> error("Unsupported OS: $os") } val archName = when { arch.contains("aarch64") || arch.contains("arm64") -> "aarch_64" arch.contains("amd64") || arch.contains("x86_64") -> "x86_64" else -> error("Unsupported architecture: $arch") } "$osName-$archName" } dependencies { protobufIncludes("com.google.protobuf:protobuf-java:${libs.versions.protobuf.get()}") protocArtifact("com.google.protobuf:protoc:${libs.versions.protobuf.get()}:$protocClassifier@exe") grpcJavaPlugin("io.grpc:protoc-gen-grpc-java:${libs.versions.grpc.get()}:$protocClassifier@exe") grpcKotlinPlugin("io.grpc:protoc-gen-grpc-kotlin:${libs.versions.grpcKotlin.get()}:jdk8@jar") } val protoGenBase = layout.buildDirectory.dir("generated/source/proto/main") val generateProto by tasks.registering(GenerateProtoTask::class) { description = "Generate Kotlin/gRPC sources from .proto files" group = "build" protoSourceDir.set(layout.projectDirectory.dir("src/main/proto")) protocFile.from(protocArtifact) grpcJavaPluginFile.from(grpcJavaPlugin) grpcKotlinPluginFile.from(grpcKotlinPlugin) protobufIncludesJar.from(protobufIncludes) includesExtractDir.set(protoGenBase.map { it.dir("includes") }) javaOutputDir.set(protoGenBase.map { it.dir("java") }) kotlinOutputDir.set(protoGenBase.map { it.dir("kotlin") }) grpcOutputDir.set(protoGenBase.map { it.dir("grpc") }) grpcKtOutputDir.set(protoGenBase.map { it.dir("grpckt") }) } // Register generated directories with AGP via the Variant API (AGP 9+). androidComponents { onVariants { variant -> variant.sources.java?.addGeneratedSourceDirectory( generateProto, GenerateProtoTask::javaOutputDir, ) variant.sources.java?.addGeneratedSourceDirectory( generateProto, GenerateProtoTask::grpcOutputDir, ) variant.sources.kotlin?.addGeneratedSourceDirectory( generateProto, GenerateProtoTask::kotlinOutputDir, ) variant.sources.kotlin?.addGeneratedSourceDirectory( generateProto, GenerateProtoTask::grpcKtOutputDir, ) } } // --- end protobuf generation --- dependencies { implementation(libs.androidx.core.ktx) implementation(libs.androidx.activity.compose) implementation(platform(libs.compose.bom)) implementation(libs.compose.ui) implementation(libs.compose.ui.tooling.preview) implementation(libs.compose.material3) debugImplementation(libs.compose.ui.tooling) implementation(libs.navigation.compose) implementation(libs.lifecycle.viewmodel.compose) implementation(libs.lifecycle.runtime.compose) implementation(libs.room.runtime) implementation(libs.room.ktx) ksp(libs.room.compiler) implementation(libs.coroutines.android) implementation(libs.reorderable) implementation(libs.protobuf.kotlin.lite) implementation(libs.grpc.okhttp) implementation(libs.grpc.protobuf.lite) implementation(libs.grpc.stub) implementation(libs.grpc.kotlin.stub) testImplementation(libs.junit) testImplementation(libs.coroutines.test) testImplementation(libs.room.testing) }