diff --git a/build-logic/module-detector/build.gradle.kts b/build-logic/module-detector/build.gradle.kts new file mode 100644 index 00000000..f8293c04 --- /dev/null +++ b/build-logic/module-detector/build.gradle.kts @@ -0,0 +1,16 @@ +plugins { + `kotlin-dsl` +} + +dependencies { + testImplementation(libs.test.junit) +} + +gradlePlugin { + plugins { + register("moduleDetector") { + id = "moop.module.detector" + implementationClass = "ModuleDetectorPlugin" + } + } +} diff --git a/build-logic/module-detector/src/main/kotlin/ModuleDetectorPlugin.kt b/build-logic/module-detector/src/main/kotlin/ModuleDetectorPlugin.kt new file mode 100644 index 00000000..56cc5be1 --- /dev/null +++ b/build-logic/module-detector/src/main/kotlin/ModuleDetectorPlugin.kt @@ -0,0 +1,31 @@ +import org.gradle.api.Plugin +import org.gradle.api.initialization.Settings +import java.io.File + +/** + * Includes every directory that has a build.gradle(.kts) file as a module, + * so settings.gradle.kts needs no include list. + */ +class ModuleDetectorPlugin : Plugin { + override fun apply(settings: Settings) { + settings.include(findModules(settings.rootDir)) + } +} + +/** + * Returns the sorted Gradle paths of the modules below [rootDir]. + * Hidden and build directories are skipped, and so are nested builds with their own settings file. + */ +internal fun findModules(rootDir: File): List = + rootDir.subDirs().flatMap { it.modules(":${it.name}") }.sorted() + +private fun File.modules(path: String): List = when { + hasFile("settings.gradle", "settings.gradle.kts") -> emptyList() + hasFile("build.gradle", "build.gradle.kts") -> listOf(path) + else -> subDirs().flatMap { it.modules("$path:${it.name}") } +} + +private fun File.hasFile(vararg names: String): Boolean = names.any { resolve(it).isFile } + +private fun File.subDirs(): List = + listFiles().orEmpty().filter { !it.name.startsWith(".") && it.name != "build" && it.isDirectory } diff --git a/build-logic/module-detector/src/test/kotlin/ModuleDetectorPluginTest.kt b/build-logic/module-detector/src/test/kotlin/ModuleDetectorPluginTest.kt new file mode 100644 index 00000000..563852bb --- /dev/null +++ b/build-logic/module-detector/src/test/kotlin/ModuleDetectorPluginTest.kt @@ -0,0 +1,45 @@ +import org.junit.Assert.assertEquals +import org.junit.Rule +import org.junit.Test +import org.junit.rules.TemporaryFolder + +class ModuleDetectorPluginTest { + + @get:Rule + val tempFolder = TemporaryFolder() + + @Test + fun findModules_includesDirectoriesWithBuildFile() { + createFile("build.gradle.kts") + createFile("app/build.gradle") + createFile("feature/home/api/build.gradle.kts") + createFile("feature/home/impl/build.gradle") + createFile("feature/home/impl/src/build.gradle") + tempFolder.newFolder("docs") + + assertEquals(listOf(":app", ":feature:home:api", ":feature:home:impl"), findModules(tempFolder.root)) + } + + @Test + fun findModules_skipsHiddenAndBuildDirectories() { + createFile(".gradle/cache/build.gradle") + createFile("build/generated/build.gradle") + createFile("feature/build/tmp/build.gradle") + createFile("app/build.gradle") + + assertEquals(listOf(":app"), findModules(tempFolder.root)) + } + + @Test + fun findModules_skipsNestedBuilds() { + createFile("build-logic/settings.gradle.kts") + createFile("build-logic/convention/build.gradle.kts") + createFile("app/build.gradle") + + assertEquals(listOf(":app"), findModules(tempFolder.root)) + } + + private fun createFile(path: String) { + tempFolder.root.resolve(path).apply { parentFile.mkdirs() }.createNewFile() + } +} diff --git a/build-logic/settings.gradle.kts b/build-logic/settings.gradle.kts index 8d885091..e37e14e7 100644 --- a/build-logic/settings.gradle.kts +++ b/build-logic/settings.gradle.kts @@ -11,3 +11,4 @@ dependencyResolutionManagement { } include(":convention") +include(":module-detector") diff --git a/settings.gradle.kts b/settings.gradle.kts index efabaf16..e863c5f8 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -7,6 +7,11 @@ pluginManagement { } } +plugins { + // Automatically includes every directory that has a build.gradle(.kts) file. + id("moop.module.detector") +} + dependencyResolutionManagement { repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) repositories { @@ -23,55 +28,3 @@ dependencyResolutionManagement { } enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS") - -// Automatically detect modules. -ModuleDetector.modules(rootDir).forEach { module -> - include(module) -} - -private object ModuleDetector { - - fun modules(rootDir: File) : List { - return rootDir.listDirs().flatMap { dir -> - findModules(parent = "", dir = dir) - } - } - - private fun findModules(parent: String, dir: File): List { - if (dir.isDirectory.not() || dir.isProject()) { - return emptyList() - } - - val current: String = parent + ":" + dir.name - return if (dir.isModule()) { - println("include '$current'") - listOf(current) - } else { - dir.listDirs().flatMap { subDir -> - findModules(parent = current, dir = subDir) - } - } - } - - private fun File.isProject(): Boolean { - if (isDirectory.not()) { - return false - } - return listFiles().orEmpty().any { - it.isFile && (it.name == "settings.gradle" || it.name == "settings.gradle.kts") - } - } - - private fun File.isModule(): Boolean { - if (isDirectory.not()) { - return false - } - return listFiles().orEmpty().any { - it.isFile && (it.name == "build.gradle" || it.name == "build.gradle.kts") - } - } - - private fun File.listDirs(): List { - return listFiles().orEmpty().filter { it.isDirectory } - } -}