import groovy.json.JsonOutput import java.io.File import java.security.MessageDigest import org.gradle.api.artifacts.component.ModuleComponentIdentifier import org.gradle.api.artifacts.ExternalModuleDependency import org.gradle.api.artifacts.ProjectDependency plugins { alias(libs.plugins.android.application) apply false alias(libs.plugins.android.library) apply false alias(libs.plugins.android.test) apply false alias(libs.plugins.kotlin.jvm) apply false alias(libs.plugins.compose) apply false alias(libs.plugins.hilt) apply false alias(libs.plugins.ksp) apply false alias(libs.plugins.ktlint) apply false } val allowed = mapOf( ":performance" to setOf(":app"), ":app" to setOf(":features", ":core", ":platform"), ":features" to setOf(":core"), ":core" to setOf(":rust-bridge"), ":platform" to setOf(":core"), ":rust-bridge" to emptySet(), ) subprojects { apply(plugin = "org.jlleitschuh.gradle.ktlint") extensions.configure { version.set(rootProject.libs.versions.ktlint.get()) filter { exclude("**/build/**") } } dependencyLocking { lockAllConfigurations() } val ownerPath = project.path configurations.configureEach { dependencies.withType().configureEach { val destination = this.path require(destination == ownerPath || destination in allowed.getValue(ownerPath)) { "$ownerPath cannot depend on $destination" } } dependencies.withType().configureEach { require(group.orEmpty().let { group -> listOf("org.eclipse.jgit", "org.bouncycastle", "androidx.room", "com.squareup.retrofit2", "com.squareup.okhttp3").none { group.startsWith(it) } }) { "Domain libraries must stay in crates/storage: $group:$name" } } val enforceDomain = name.endsWith("RuntimeClasspath") || name.endsWith("CompileClasspath") resolutionStrategy.eachDependency { require(requested.version.orEmpty().let { version -> !version.contains('+') && !version.contains("SNAPSHOT") && !version.startsWith("latest.") && version.none { it in "[]()," } }) { "Dynamic versions are forbidden" } require(!enforceDomain || listOf("org.eclipse.jgit", "org.bouncycastle", "androidx.room", "com.squareup.retrofit2", "com.squareup.okhttp3", "org.pgpainless", "com.google.mlkit").none { requested.group == it || requested.group.startsWith("$it.") }) { "Transitive domain libraries must stay in crates/storage" } } } } val cargoExecutable = providers.environmentVariable("CARGO") .orElse(providers.environmentVariable("PATH").map { path -> path.split(File.pathSeparatorChar) .map { File(it, "cargo") } .firstOrNull { it.isFile && it.canExecute() }?.absolutePath ?: error("Cargo is required: put it in PATH or set CARGO to its absolute path") }) val workspace = layout.projectDirectory.dir("..").asFile val checkArchitecture = tasks.register("checkArchitecture") { workingDir = workspace inputs.files(fileTree(".") { include("**/src/**/*.kt", "**/build.gradle.kts"); exclude("**/build/**", ".gradle/**") }) inputs.files(workspace.resolve("Cargo.lock"), workspace.resolve("tools/android-build/src/main.rs")) commandLine(cargoExecutable.get(), "run", "--locked", "--package", "ironstorage-android-build", "--", "check", layout.projectDirectory.asFile.absolutePath) } subprojects { tasks.matching { it.name == "preBuild" || it.name == "check" }.configureEach { dependsOn(rootProject.tasks.named("checkArchitecture")) } } tasks.register("auditNativePackages") { dependsOn(":app:assembleDebug", ":app:assembleRelease", ":app:bundleRelease") workingDir = workspace val packages = listOf( layout.projectDirectory.file("app/build/outputs/apk/debug/app-debug.apk"), layout.projectDirectory.file("app/build/outputs/apk/release/app-release-unsigned.apk"), layout.projectDirectory.file("app/build/outputs/bundle/release/app-release.aab") ) inputs.files(packages) commandLine( listOf( cargoExecutable.get(), "+${libs.versions.rust.get()}", "run", "--locked", "--package", "ironstorage-android-build", "--", "audit-package" ) + packages.map { it.asFile.absolutePath } ) } // Each project resolves its own configurations under Gradle's project lock. subprojects { val owner = project tasks.register("dependencyInventory") { val output = layout.buildDirectory.file("reports/android-dependencies.json") outputs.file(output) outputs.upToDateWhen { false } doLast { val rows = listOf("debugRuntimeClasspath", "releaseRuntimeClasspath", "debugAndroidTestRuntimeClasspath", "debugUnitTestRuntimeClasspath", "benchmarkRuntimeClasspath").flatMap { name -> val configuration = owner.configurations.findByName(name) if (configuration == null || !configuration.isCanBeResolved) emptyList() else { configuration.incoming.artifactView { componentFilter { it is ModuleComponentIdentifier } }.artifacts.artifacts.mapNotNull { artifact -> val id = artifact.id.componentIdentifier as? ModuleComponentIdentifier id?.let { mapOf( "group" to it.group, "name" to it.module, "version" to it.version, "sha256" to MessageDigest.getInstance("SHA-256").digest(artifact.file.readBytes()) .joinToString("") { byte -> "%02x".format(byte) }, "scope" to "${owner.path}:$name" ) } } } }.sortedWith(compareBy({ it.getValue("scope") }, { it.getValue("group") }, { it.getValue("name") }, { it.getValue("version") }, { it.getValue("sha256") })) output.get().asFile.apply { parentFile.mkdirs() writeText(JsonOutput.prettyPrint(JsonOutput.toJson(rows)) + "\n") } } notCompatibleWithConfigurationCache("Resolves reviewed artifact graphs") } } tasks.register("dependencyInventory") { dependsOn(subprojects.map { "${it.path}:dependencyInventory" }) } tasks.register("androidVerification") { dependsOn("checkArchitecture", "dependencyInventory", "auditNativePackages") dependsOn(subprojects.map { "${it.path}:ktlintCheck" }) dependsOn(subprojects.filter { it.name != "performance" }.map { "${it.path}:lintDebug" }) dependsOn(subprojects.filter { it.name != "performance" }.map { "${it.path}:testDebugUnitTest" }) dependsOn(":app:assembleBenchmark", ":app:assembleProfile", ":performance:assembleBenchmark") dependsOn(":core:assembleDebugAndroidTest", ":app:assembleDebugAndroidTest", ":rust-bridge:assembleDebugAndroidTest", ":platform:assembleDebugAndroidTest") }