From baa44e12b2788c7efcedaa279d70a1c551944bc2 Mon Sep 17 00:00:00 2001 From: Tom Larcher Date: Tue, 30 Jun 2026 16:19:32 +1000 Subject: [PATCH] feat: introduce support for Gradle Isolated Projects Previously the plugin read every sibling project's state at configuration time through `project.allprojects` and a querier that reached into other projects' configurations, dependencies and plugins, which is incompatible with Gradle's Isolated Projects. The graph is now assembled from per-project snapshots, each collected from within its own project, so no cross-project access happens at any point and the plugin is compatible with both the Configuration Cache and Isolated Projects. - Add `ProjectInfo`, a serializable per-project snapshot of a project's path, applied plugin ids, external dependency coordinates and declared project dependencies. - Add `ModuleGraphRegistry`, a build-scoped service that aggregates the snapshots, and `ModuleGraphContributeTask`, which registers a project's snapshot at execution time so it survives configuration-cache reuse. - Add the `dev.iurysouza.modulegraph.settings` settings plugin for one-line multi-project adoption: it applies the plugin to the root project and makes every other project contribute a snapshot that the root `createModuleGraph` task depends on. - Replace `GradleProjectQuerier` with `SnapshotProjectQuerier` and move graph parsing into the task action, so the model is built from the collected snapshot instead of live `Project` instances. - Collect each snapshot through a lazy provider instead of `afterEvaluate`, and resolve module types from the captured data via a pure `resolveModuleType`. - Bump the plugin build to Gradle 8.11.1 for the Isolated-Projects-safe `ProjectDependency.getPath()`. - Add a functional test that runs `createModuleGraph` with the configuration cache and Isolated Projects enabled across a multi-project build and asserts a correct, cache-reusable result. - Document the support in the README and dogfood the settings plugin in the sample build, configuring it from the root build and targeting the sample readmes. --- README.md | 24 ++++ build.gradle.kts | 44 +++++++ plugin-build/gradle.properties | 1 + .../gradle/wrapper/gradle-wrapper.properties | 2 +- plugin-build/modulegraph/build.gradle.kts | 8 ++ .../dev/iurysouza/modulegraph/ModuleType.kt | 42 ++++--- .../gradle/CreateModuleGraphTask.kt | 30 ++++- .../gradle/ModuleGraphContributeTask.kt | 32 ++++++ .../modulegraph/gradle/ModuleGraphPlugin.kt | 56 ++++++--- .../modulegraph/gradle/ModuleGraphRegistry.kt | 27 +++++ .../gradle/ModuleGraphSettingsPlugin.kt | 53 +++++++++ .../gradle/graphparser/model/ProjectInfo.kt | 30 +++++ .../projectquerier/GradleProjectQuerier.kt | 30 ----- .../projectquerier/SnapshotProjectQuerier.kt | 27 +++++ .../ModuleGraphIsolatedProjectsTest.kt | 107 ++++++++++++++++++ sample/build.gradle.kts | 67 ----------- settings.gradle.kts | 8 +- 17 files changed, 451 insertions(+), 137 deletions(-) create mode 100644 plugin-build/modulegraph/src/main/kotlin/dev/iurysouza/modulegraph/gradle/ModuleGraphContributeTask.kt create mode 100644 plugin-build/modulegraph/src/main/kotlin/dev/iurysouza/modulegraph/gradle/ModuleGraphRegistry.kt create mode 100644 plugin-build/modulegraph/src/main/kotlin/dev/iurysouza/modulegraph/gradle/ModuleGraphSettingsPlugin.kt create mode 100644 plugin-build/modulegraph/src/main/kotlin/dev/iurysouza/modulegraph/gradle/graphparser/model/ProjectInfo.kt delete mode 100644 plugin-build/modulegraph/src/main/kotlin/dev/iurysouza/modulegraph/gradle/graphparser/projectquerier/GradleProjectQuerier.kt create mode 100644 plugin-build/modulegraph/src/main/kotlin/dev/iurysouza/modulegraph/gradle/graphparser/projectquerier/SnapshotProjectQuerier.kt create mode 100644 plugin-build/modulegraph/src/test/java/dev/iurysouza/modulegraph/ModuleGraphIsolatedProjectsTest.kt diff --git a/README.md b/README.md index 7adff6e..1a399ec 100644 --- a/README.md +++ b/README.md @@ -243,6 +243,30 @@ You can run the plugin's task right away without any extra configuration. The pl This will generate a module graph in your README file. If you need further customization, keep reading for more detailed examples of how to configure it. +## Isolated Projects & Configuration Cache + +> [!NOTE] +> The information within this section applies purely to projects leveraging the experimental Gradle [Isolated Projects](https://docs.gradle.org/current/userguide/isolated_projects.html) feature. + +The plugin is fully compatible with the [Configuration Cache](https://docs.gradle.org/current/userguide/configuration_cache.html) and [Isolated Projects](https://docs.gradle.org/current/userguide/isolated_projects.html). It never reads another project's state: each project contributes its own dependency snapshot from within its own context, and the graph is assembled at task-execution time. + +For multi-project builds, apply the **settings plugin** once in `settings.gradle.kts`. It applies the plugin to the root project (giving you `createModuleGraph` and the `moduleGraphConfig` extension) and makes every other project contribute automatically: + +```kotlin +// settings.gradle.kts +plugins { + id("dev.iurysouza.modulegraph.settings") version "0.13.0" +} +``` + +Then configure as usual in the root `build.gradle.kts` and run with Isolated Projects enabled: + +```sh +./gradlew createModuleGraph -Dorg.gradle.unsafe.isolated-projects=true +``` + +Applying the regular `dev.iurysouza.modulegraph` plugin still works and graphs the project it is applied to, but only the settings plugin can aggregate sibling projects without breaking isolation. + ## Configuration Docs diff --git a/build.gradle.kts b/build.gradle.kts index 6bee7ab..bb2ccff 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,4 +1,8 @@ import com.github.benmanes.gradle.versions.updates.DependencyUpdatesTask +import dev.iurysouza.modulegraph.LinkText +import dev.iurysouza.modulegraph.ModuleType +import dev.iurysouza.modulegraph.Orientation +import dev.iurysouza.modulegraph.Theme import io.gitlab.arturbosch.detekt.Detekt plugins { @@ -71,3 +75,43 @@ tasks.register("preMerge") { tasks.wrapper { distributionType = Wrapper.DistributionType.ALL } + +moduleGraphConfig { + heading.set("# Primary Graph") + readmePath.set("./sample/README.md") + showFullPath.set(false) + orientation.set(Orientation.TOP_TO_BOTTOM) + linkText.set(LinkText.NONE) + setStyleByModuleType.set(true) + theme.set( + Theme.BASE( + themeVariables = mapOf( + "primaryTextColor" to "#F6F8FAff", + "primaryColor" to "#5a4f7c", + "primaryBorderColor" to "#5a4f7c", + "tertiaryColor" to "#40375c", + "lineColor" to "#f5a623", + "fontSize" to "12px", + ), + focusColor = "#F5A622", + moduleTypes = listOf( + ModuleType.Kotlin("#2C4162"), + ), + ), + ) + excludedConfigurationsRegex.set(""".*test.*""") + graph( + readmePath = "./sample/README.md", + heading = "# Graph with root: gama", + ) { + nestingEnabled = true + rootModulesRegex = ".*gama.*" + } + graph( + readmePath = "./sample/SomeOtherReadme.md", + heading = "# Graph", + ) { + nestingEnabled = false + rootModulesRegex = ".*zeta.*" + } +} diff --git a/plugin-build/gradle.properties b/plugin-build/gradle.properties index 02b04e4..7134a2c 100644 --- a/plugin-build/gradle.properties +++ b/plugin-build/gradle.properties @@ -1,6 +1,7 @@ #Tue Oct 14 16:31:55 CEST 2025 GROUP=dev.iurysouza IMPLEMENTATION_CLASS=dev.iurysouza.modulegraph.gradle.ModuleGraphPlugin +IMPLEMENTATION_CLASS_SETTINGS=dev.iurysouza.modulegraph.gradle.ModuleGraphSettingsPlugin DESCRIPTION=Generates and embbed a mermaid graph showing the project's modules relationships in your README.md file. VERSION=0.13.0 ID=dev.iurysouza.modulegraph diff --git a/plugin-build/gradle/wrapper/gradle-wrapper.properties b/plugin-build/gradle/wrapper/gradle-wrapper.properties index 171d876..4eaec46 100644 --- a/plugin-build/gradle/wrapper/gradle-wrapper.properties +++ b/plugin-build/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-bin.zip networkTimeout=10000 zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/plugin-build/modulegraph/build.gradle.kts b/plugin-build/modulegraph/build.gradle.kts index c73cb96..2d5d4af 100644 --- a/plugin-build/modulegraph/build.gradle.kts +++ b/plugin-build/modulegraph/build.gradle.kts @@ -41,6 +41,14 @@ gradlePlugin { displayName = property("DISPLAY_NAME").toString() tags.set(listOf("mermaid", "diagram")) } + create("${property("ID")}.settings") { + id = "${property("ID")}.settings" + implementationClass = property("IMPLEMENTATION_CLASS_SETTINGS").toString() + version = property("VERSION").toString() + description = property("DESCRIPTION").toString() + displayName = "${property("DISPLAY_NAME")} (Settings)" + tags.set(listOf("mermaid", "diagram")) + } } } diff --git a/plugin-build/modulegraph/src/main/kotlin/dev/iurysouza/modulegraph/ModuleType.kt b/plugin-build/modulegraph/src/main/kotlin/dev/iurysouza/modulegraph/ModuleType.kt index cfffff9..81f1ea3 100644 --- a/plugin-build/modulegraph/src/main/kotlin/dev/iurysouza/modulegraph/ModuleType.kt +++ b/plugin-build/modulegraph/src/main/kotlin/dev/iurysouza/modulegraph/ModuleType.kt @@ -69,35 +69,45 @@ private val defaultPlugins = listOf( ) /** - * Determines the primary plugin type applied to the project based on a precedence order. + * Determines the primary [ModuleType] from a precedence-ordered list of candidates. * - * The first plugin from this ordered list that matches a plugin applied to the project - * is returned as the primary plugin type. If no such plugin is found, `PluginType.Unknown()` - * is returned. + * The first candidate whose id matches an applied plugin id or an external dependency coordinate is + * returned, or [ModuleType.Unknown] if none match. * - * @param customPlugins A list of additional `PluginType` instances that should be considered - * alongside the predefined list of plugins. - * @return The primary `PluginType` applied to the project or `PluginType.Unknown()` if - * no match is found. + * @param pluginIds the ids of plugins applied to the project. + * @param externalDependencies the "group:name" coordinates of the project's external dependencies. + * @param customPlugins additional [ModuleType] candidates considered alongside the defaults. */ -internal fun Project.getModuleType( +internal fun resolveModuleType( + pluginIds: List, + externalDependencies: List, customPlugins: List, ): ModuleType = (customPlugins + defaultPlugins) .distinctBy { it.id } .sortedWith(pluginTypeComparator) - .firstOrNull { - plugins.hasPlugin(it.id) || hasLibraryDependency(it.id) + .firstOrNull { type -> + pluginIds.contains(type.id) || externalDependencies.any { + type.id.toRegex().matches(it) + } } ?: ModuleType.Unknown() -internal fun Project.hasLibraryDependency(dependencyGroupAndName: String): Boolean = runCatching { +/** @return the ids of all known module-type plugins applied to this project. */ +internal fun Project.appliedModuleTypePluginIds( + customPlugins: List, +): List = (customPlugins + defaultPlugins) + .map { it.id } + .distinct() + .filter { plugins.hasPlugin(it) } + +/** @return the "group:name" coordinates of every external dependency declared in this project. */ +internal fun Project.externalDependencyCoordinates(): List = runCatching { configurations.flatMap { configuration -> configuration.dependencies.filterIsInstance() - }.any { dependency -> - dependencyGroupAndName.toRegex().matches("${dependency.group}:${dependency.name}") - } + }.map { dependency -> "${dependency.group}:${dependency.name}" } + .distinct() }.getOrElse { e -> println("Error resolving dependencies: ${e.message}") - false + emptyList() } internal val pluginPrecedenceOrder = listOf( diff --git a/plugin-build/modulegraph/src/main/kotlin/dev/iurysouza/modulegraph/gradle/CreateModuleGraphTask.kt b/plugin-build/modulegraph/src/main/kotlin/dev/iurysouza/modulegraph/gradle/CreateModuleGraphTask.kt index 35b71a3..772a62e 100644 --- a/plugin-build/modulegraph/src/main/kotlin/dev/iurysouza/modulegraph/gradle/CreateModuleGraphTask.kt +++ b/plugin-build/modulegraph/src/main/kotlin/dev/iurysouza/modulegraph/gradle/CreateModuleGraphTask.kt @@ -1,6 +1,8 @@ package dev.iurysouza.modulegraph.gradle import dev.iurysouza.modulegraph.* +import dev.iurysouza.modulegraph.gradle.graphparser.ProjectParser +import dev.iurysouza.modulegraph.gradle.graphparser.projectquerier.SnapshotProjectQuerier import dev.iurysouza.modulegraph.model.GraphConfig import dev.iurysouza.modulegraph.model.GraphParseResult import org.gradle.api.DefaultTask @@ -8,6 +10,7 @@ import org.gradle.api.file.DirectoryProperty import org.gradle.api.logging.LogLevel import org.gradle.api.provider.ListProperty import org.gradle.api.provider.Property +import org.gradle.api.services.ServiceReference import org.gradle.api.tasks.Input import org.gradle.api.tasks.Optional import org.gradle.api.tasks.OutputDirectory @@ -120,9 +123,17 @@ abstract class CreateModuleGraphTask : DefaultTask() { @get:Optional abstract val includeIsolatedModules: Property + /** All graph configs (primary + additional) resolved at configuration time. */ @get:Input - @get:Option(option = "graphModels", description = "The produced graph models") - internal abstract val graphModels: ListProperty + internal abstract val graphConfigsResolved: ListProperty + + /** + * Registry of every project's contributed + * [dev.iurysouza.modulegraph.gradle.graphparser.model.ProjectInfo], read at execution time. + */ + @Suppress("UnstableApiUsage") + @get:ServiceReference(ModuleGraphRegistry.NAME) + internal abstract val registry: Property @get:OutputDirectory @get:Option(option = "projectDirectory", description = "The root project directory") @@ -144,8 +155,19 @@ abstract class CreateModuleGraphTask : DefaultTask() { @TaskAction fun execute() { runCatching { - val results = graphModels.orNull - ?: error("Graph models have not been computed. This is a bug in the plugin - please report it!") + val configs = graphConfigsResolved.get() + val snapshot = registry.get().snapshot() + val allProjectPaths = snapshot.keys.toList() + val projectQuerier = SnapshotProjectQuerier(snapshot) + + val results = configs.map { config -> + val projectGraph = ProjectParser.parseProjectGraph( + allProjectPaths = allProjectPaths, + config = config, + projectQuerier = projectQuerier, + ) + GraphParseResult(projectGraph, config) + } results.forEach { result -> val config = result.config diff --git a/plugin-build/modulegraph/src/main/kotlin/dev/iurysouza/modulegraph/gradle/ModuleGraphContributeTask.kt b/plugin-build/modulegraph/src/main/kotlin/dev/iurysouza/modulegraph/gradle/ModuleGraphContributeTask.kt new file mode 100644 index 0000000..3ca7865 --- /dev/null +++ b/plugin-build/modulegraph/src/main/kotlin/dev/iurysouza/modulegraph/gradle/ModuleGraphContributeTask.kt @@ -0,0 +1,32 @@ +package dev.iurysouza.modulegraph.gradle + +import dev.iurysouza.modulegraph.gradle.graphparser.model.ProjectInfo +import org.gradle.api.DefaultTask +import org.gradle.api.provider.Property +import org.gradle.api.services.ServiceReference +import org.gradle.api.tasks.Input +import org.gradle.api.tasks.TaskAction + +/** + * Registers a single project's [ProjectInfo] into the [ModuleGraphRegistry] at execution time. + * + * The snapshot is captured as a serializable input during configuration and registered when the + * task runs. + */ +internal abstract class ModuleGraphContributeTask : DefaultTask() { + @get:Input + abstract val projectInfo: Property + + @Suppress("UnstableApiUsage") + @get:ServiceReference(ModuleGraphRegistry.NAME) + abstract val registry: Property + + @TaskAction + fun contribute() { + registry.get().register(projectInfo.get()) + } + + companion object { + const val NAME: String = "moduleGraphContribute" + } +} diff --git a/plugin-build/modulegraph/src/main/kotlin/dev/iurysouza/modulegraph/gradle/ModuleGraphPlugin.kt b/plugin-build/modulegraph/src/main/kotlin/dev/iurysouza/modulegraph/gradle/ModuleGraphPlugin.kt index 21e0fda..dcdc372 100644 --- a/plugin-build/modulegraph/src/main/kotlin/dev/iurysouza/modulegraph/gradle/ModuleGraphPlugin.kt +++ b/plugin-build/modulegraph/src/main/kotlin/dev/iurysouza/modulegraph/gradle/ModuleGraphPlugin.kt @@ -1,18 +1,35 @@ package dev.iurysouza.modulegraph.gradle -import dev.iurysouza.modulegraph.gradle.graphparser.ProjectParser -import dev.iurysouza.modulegraph.gradle.graphparser.projectquerier.GradleProjectQuerier +import dev.iurysouza.modulegraph.appliedModuleTypePluginIds +import dev.iurysouza.modulegraph.externalDependencyCoordinates +import dev.iurysouza.modulegraph.gradle.graphparser.model.GradleProjectConfiguration +import dev.iurysouza.modulegraph.gradle.graphparser.model.ProjectInfo import dev.iurysouza.modulegraph.model.GraphConfig -import dev.iurysouza.modulegraph.model.GraphParseResult import org.apache.tools.ant.taskdefs.condition.Os import org.gradle.api.Plugin import org.gradle.api.Project +import org.gradle.api.artifacts.ProjectDependency private const val EXTENSION_NAME = "moduleGraphConfig" private const val TASK_NAME = "createModuleGraph" +/** + * Applies the module graph to a single project: creates the `moduleGraphConfig` extension and the + * `createModuleGraph` task, and contributes the project's own [ProjectInfo] snapshot. + */ open class ModuleGraphPlugin : Plugin { override fun apply(project: Project) { + project.gradle.sharedServices.registerIfAbsent( + ModuleGraphRegistry.NAME, + ModuleGraphRegistry::class.java, + ) {} + val contribution = project.tasks.register( + ModuleGraphContributeTask.NAME, + ModuleGraphContributeTask::class.java, + ) { task -> + task.projectInfo.set(project.provider { project.collectProjectInfo() }) + } + val extension = project.extensions.create( EXTENSION_NAME, ModuleGraphExtension::class.java, @@ -44,6 +61,7 @@ open class ModuleGraphPlugin : Plugin { task.strictMode.set(extension.strictMode) task.nestingEnabled.set(extension.nestingEnabled) task.includeIsolatedModules.set(extension.includeIsolatedModules) + task.dependsOn(contribution) val primaryGraphConfig = getPrimaryGraphConfig(task) val additionalGraphConfigs = task.graphConfigs.getOrElse(emptyList()) @@ -56,21 +74,7 @@ open class ModuleGraphPlugin : Plugin { """.trimIndent(), ) } - - val allProjects = project.allprojects - val allProjectPaths = allProjects.map { it.path } - val projectQuerier = GradleProjectQuerier(allProjects) - - val results = allGraphConfigs.map { config -> - val projectGraph = ProjectParser.parseProjectGraph( - allProjectPaths = allProjectPaths, - config = config, - projectQuerier = projectQuerier, - ) - GraphParseResult(projectGraph, config) - } - - task.graphModels.set(results) + task.graphConfigsResolved.set(allGraphConfigs) } } @@ -139,3 +143,19 @@ open class ModuleGraphPlugin : Plugin { }.build() } } + +/** @return a [ProjectInfo] snapshot of the given project's own configurations and plugins. */ +internal fun Project.collectProjectInfo(): ProjectInfo { + val collectedConfigurations = configurations.map { configuration -> + val projectPaths = configuration.dependencies + .withType(ProjectDependency::class.java) + .map { dependency -> dependency.path } + GradleProjectConfiguration(configuration.name, projectPaths) + } + return ProjectInfo( + path = path, + pluginIds = appliedModuleTypePluginIds(emptyList()), + externalDependencies = externalDependencyCoordinates(), + configurations = collectedConfigurations, + ) +} diff --git a/plugin-build/modulegraph/src/main/kotlin/dev/iurysouza/modulegraph/gradle/ModuleGraphRegistry.kt b/plugin-build/modulegraph/src/main/kotlin/dev/iurysouza/modulegraph/gradle/ModuleGraphRegistry.kt new file mode 100644 index 0000000..46fe8e1 --- /dev/null +++ b/plugin-build/modulegraph/src/main/kotlin/dev/iurysouza/modulegraph/gradle/ModuleGraphRegistry.kt @@ -0,0 +1,27 @@ +package dev.iurysouza.modulegraph.gradle + +import dev.iurysouza.modulegraph.gradle.graphparser.model.ProjectInfo +import dev.iurysouza.modulegraph.gradle.graphparser.model.ProjectPath +import java.util.concurrent.ConcurrentHashMap +import org.gradle.api.services.BuildService +import org.gradle.api.services.BuildServiceParameters + +/** + * Build-scoped service that aggregates each project's [ProjectInfo]. + * + * Projects register their snapshot during configuration; [CreateModuleGraphTask] reads the + * accumulated map at execution time. + */ +internal abstract class ModuleGraphRegistry : BuildService { + private val infos = ConcurrentHashMap() + + fun register(info: ProjectInfo) { + infos[info.path] = info + } + + fun snapshot(): Map = infos.toMap() + + companion object { + const val NAME: String = "moduleGraphRegistry" + } +} diff --git a/plugin-build/modulegraph/src/main/kotlin/dev/iurysouza/modulegraph/gradle/ModuleGraphSettingsPlugin.kt b/plugin-build/modulegraph/src/main/kotlin/dev/iurysouza/modulegraph/gradle/ModuleGraphSettingsPlugin.kt new file mode 100644 index 0000000..5834578 --- /dev/null +++ b/plugin-build/modulegraph/src/main/kotlin/dev/iurysouza/modulegraph/gradle/ModuleGraphSettingsPlugin.kt @@ -0,0 +1,53 @@ +package dev.iurysouza.modulegraph.gradle + +import org.gradle.api.Plugin +import org.gradle.api.initialization.ProjectDescriptor +import org.gradle.api.initialization.Settings + +private const val CONTRIBUTE_TASK = "moduleGraphContribute" + +/** + * Settings plugin that wires the module graph for the whole build. + * + * Apply it in `settings.gradle(.kts)`: + * ``` + * plugins { id("dev.iurysouza.modulegraph.settings") } + * ``` + * + * It applies [ModuleGraphPlugin] to the root project and registers a [ModuleGraphContributeTask] on + * every other project, which the root `createModuleGraph` task depends on. + */ +open class ModuleGraphSettingsPlugin : Plugin { + override fun apply(settings: Settings) { + settings.gradle.sharedServices.registerIfAbsent( + ModuleGraphRegistry.NAME, + ModuleGraphRegistry::class.java, + ) {} + + val contributingPaths = mutableListOf() + + settings.gradle.settingsEvaluated { evaluated -> + contributingPaths += evaluated.rootProject.children.flatMap { it.allPaths() } + } + + @Suppress("UnstableApiUsage") + settings.gradle.lifecycle.beforeProject { project -> + if (project == project.rootProject) { + project.pluginManager.apply(ModuleGraphPlugin::class.java) + project.tasks.named("createModuleGraph").configure { task -> + task.dependsOn(contributingPaths.map { "$it:$CONTRIBUTE_TASK" }) + } + } else { + project.tasks.register( + ModuleGraphContributeTask.NAME, + ModuleGraphContributeTask::class.java, + ) { task -> + task.projectInfo.set(project.provider { project.collectProjectInfo() }) + } + } + } + } + + private fun ProjectDescriptor.allPaths(): List = + listOf(path) + children.flatMap { it.allPaths() } +} diff --git a/plugin-build/modulegraph/src/main/kotlin/dev/iurysouza/modulegraph/gradle/graphparser/model/ProjectInfo.kt b/plugin-build/modulegraph/src/main/kotlin/dev/iurysouza/modulegraph/gradle/graphparser/model/ProjectInfo.kt new file mode 100644 index 0000000..06929f0 --- /dev/null +++ b/plugin-build/modulegraph/src/main/kotlin/dev/iurysouza/modulegraph/gradle/graphparser/model/ProjectInfo.kt @@ -0,0 +1,30 @@ +package dev.iurysouza.modulegraph.gradle.graphparser.model + +import java.io.Serializable as JavaSerializable + +/** + * A serializable snapshot of a single Gradle project's data. + * + * Each project contributes one instance to the + * [dev.iurysouza.modulegraph.gradle.ModuleGraphRegistry]. + */ +internal data class ProjectInfo( + /** The Gradle path of the project, e.g. ":app:feature". */ + val path: ProjectPath, + /** + * The ids of the plugins applied to the project, used to infer its + * [dev.iurysouza.modulegraph.ModuleType]. + */ + val pluginIds: List, + /** + * The "group:name" coordinates of external dependencies, used to infer its + * [dev.iurysouza.modulegraph.ModuleType]. + */ + val externalDependencies: List, + /** The project dependencies declared per configuration. */ + val configurations: List, +) : JavaSerializable { + companion object { + private const val serialVersionUID: Long = 1L + } +} diff --git a/plugin-build/modulegraph/src/main/kotlin/dev/iurysouza/modulegraph/gradle/graphparser/projectquerier/GradleProjectQuerier.kt b/plugin-build/modulegraph/src/main/kotlin/dev/iurysouza/modulegraph/gradle/graphparser/projectquerier/GradleProjectQuerier.kt deleted file mode 100644 index a12448d..0000000 --- a/plugin-build/modulegraph/src/main/kotlin/dev/iurysouza/modulegraph/gradle/graphparser/projectquerier/GradleProjectQuerier.kt +++ /dev/null @@ -1,30 +0,0 @@ -package dev.iurysouza.modulegraph.gradle.graphparser.projectquerier - -import dev.iurysouza.modulegraph.ModuleType -import dev.iurysouza.modulegraph.getModuleType -import dev.iurysouza.modulegraph.gradle.graphparser.model.GradleProjectConfiguration -import dev.iurysouza.modulegraph.gradle.graphparser.model.ProjectPath -import org.gradle.api.Project -import org.gradle.api.artifacts.ProjectDependency - -internal class GradleProjectQuerier(private val allProjects: Set) : ProjectQuerier { - override fun getProjectType( - projectPath: ProjectPath, - customModuleTypes: List, - ): ModuleType { - val project = getProject(projectPath) ?: return ModuleType.Unknown() - return project.getModuleType(customModuleTypes) - } - - override fun getConfigurations(projectPath: ProjectPath): List { - val project = getProject(projectPath) ?: return emptyList() - return project.configurations.map { config -> - val projectPaths = config.dependencies - .withType(ProjectDependency::class.java) - .map { subproject -> subproject.path } - GradleProjectConfiguration(config.name, projectPaths) - } - } - - private fun getProject(path: ProjectPath) = allProjects.firstOrNull { it.path == path } -} diff --git a/plugin-build/modulegraph/src/main/kotlin/dev/iurysouza/modulegraph/gradle/graphparser/projectquerier/SnapshotProjectQuerier.kt b/plugin-build/modulegraph/src/main/kotlin/dev/iurysouza/modulegraph/gradle/graphparser/projectquerier/SnapshotProjectQuerier.kt new file mode 100644 index 0000000..c210682 --- /dev/null +++ b/plugin-build/modulegraph/src/main/kotlin/dev/iurysouza/modulegraph/gradle/graphparser/projectquerier/SnapshotProjectQuerier.kt @@ -0,0 +1,27 @@ +package dev.iurysouza.modulegraph.gradle.graphparser.projectquerier + +import dev.iurysouza.modulegraph.ModuleType +import dev.iurysouza.modulegraph.gradle.graphparser.model.GradleProjectConfiguration +import dev.iurysouza.modulegraph.gradle.graphparser.model.ProjectInfo +import dev.iurysouza.modulegraph.gradle.graphparser.model.ProjectPath +import dev.iurysouza.modulegraph.resolveModuleType + +/** [ProjectQuerier] backed by the [ProjectInfo] snapshot collected from each project. */ +internal class SnapshotProjectQuerier( + private val infoByPath: Map, +) : ProjectQuerier { + override fun getProjectType( + projectPath: ProjectPath, + customModuleTypes: List, + ): ModuleType { + val info = infoByPath[projectPath] ?: return ModuleType.Unknown() + return resolveModuleType( + pluginIds = info.pluginIds, + externalDependencies = info.externalDependencies, + customPlugins = customModuleTypes, + ) + } + + override fun getConfigurations(projectPath: ProjectPath): List = + infoByPath[projectPath]?.configurations ?: emptyList() +} diff --git a/plugin-build/modulegraph/src/test/java/dev/iurysouza/modulegraph/ModuleGraphIsolatedProjectsTest.kt b/plugin-build/modulegraph/src/test/java/dev/iurysouza/modulegraph/ModuleGraphIsolatedProjectsTest.kt new file mode 100644 index 0000000..d2c32cb --- /dev/null +++ b/plugin-build/modulegraph/src/test/java/dev/iurysouza/modulegraph/ModuleGraphIsolatedProjectsTest.kt @@ -0,0 +1,107 @@ +package dev.iurysouza.modulegraph + +import java.io.File +import org.gradle.testkit.runner.GradleRunner +import org.junit.jupiter.api.Assertions.assertFalse +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.io.TempDir + +/** Verifies the graph is generated with the Configuration Cache and Isolated Projects enabled. */ +class ModuleGraphIsolatedProjectsTest { + @TempDir + lateinit var testProjectDir: File + private lateinit var settingsFile: File + private lateinit var rootBuildFile: File + private lateinit var readmeFile: File + + @BeforeEach + fun setup() { + settingsFile = File(testProjectDir, "settings.gradle.kts") + rootBuildFile = File(testProjectDir, "build.gradle.kts") + readmeFile = File(testProjectDir, "README.md") + } + + @Test + fun `graph is generated with isolated projects enabled`() { + settingsFile.writeText( + """ + plugins { id("$MODULEGRAPH_PACKAGE.settings") } + rootProject.name = "ip-sample" + include(":app") + include(":core:data") + include(":core:ui") + """.trimIndent(), + ) + rootBuildFile.writeText( + """ + moduleGraphConfig { + heading.set("### Module Graph") + readmePath.set("${readmeFilePath()}") + } + """.trimIndent(), + ) + writeModule("app", listOf(":core:data", ":core:ui")) + writeModule("core/data", emptyList()) + writeModule("core/ui", listOf(":core:data")) + readmeFile.writeText("### Module Graph") + + val result = GradleRunner.create().withProjectDir(testProjectDir).withArguments( + "createModuleGraph", + "--configuration-cache", + "-Dorg.gradle.unsafe.isolated-projects=true", + ).withPluginClasspath().build() + + val output = result.output + assertFalse( + output.contains("problems were found storing the configuration cache"), + output, + ) + assertTrue( + readmeFile.readText().contains(":app --> :core:data"), + readmeFile.readText(), + ) + assertTrue( + readmeFile.readText().contains(":app --> :core:ui"), + readmeFile.readText(), + ) + assertTrue( + readmeFile.readText().contains(":core:ui --> :core:data"), + readmeFile.readText(), + ) + + // Second run reuses the configuration cache. + val rerun = GradleRunner.create().withProjectDir(testProjectDir).withArguments( + "createModuleGraph", + "--configuration-cache", + "-Dorg.gradle.unsafe.isolated-projects=true", + ).withPluginClasspath().build() + assertTrue( + rerun.output.contains("Reusing configuration cache"), + rerun.output, + ) + } + + private fun writeModule(path: String, projectDeps: List) { + val deps = projectDeps.joinToString("\n") { + " implementation(project(\"$it\"))" + } + File(testProjectDir, "$path/build.gradle.kts").apply { + parentFile.mkdirs() + writeText( + """ + plugins { java } + dependencies { + $deps + } + """.trimIndent(), + ) + } + } + + private fun readmeFilePath() = readmeFile.absolutePath.replace( + "\\", + "\\\\", + ) +} diff --git a/sample/build.gradle.kts b/sample/build.gradle.kts index 979c441..c630f43 100644 --- a/sample/build.gradle.kts +++ b/sample/build.gradle.kts @@ -1,70 +1,3 @@ -import dev.iurysouza.modulegraph.LinkText -import dev.iurysouza.modulegraph.ModuleType -import dev.iurysouza.modulegraph.Orientation -import dev.iurysouza.modulegraph.Theme - -plugins { - id("dev.iurysouza.modulegraph") -} - -// Sample showing usage of `moduleGraphConfig` from Kotlin -// Enable this file by removing the `.x` extension, if present. -// Generate graphs for these configs with: `./gradlew createModuleGraph` -moduleGraphConfig { - /* Setup primary graph */ - - /* Primary graph - required config */ - heading.set("# Primary Graph") - readmePath.set("./README.md") - - /* Primary graph - optional config */ - showFullPath.set(false) - orientation.set(Orientation.TOP_TO_BOTTOM) - linkText.set(LinkText.NONE) - setStyleByModuleType.set(true) - // strictMode.set(false) - theme.set( - Theme.BASE( - themeVariables = mapOf( - // Text - "primaryTextColor" to "#F6F8FAff", - // Node - "primaryColor" to "#5a4f7c", - // Node border - "primaryBorderColor" to "#5a4f7c", - // Container box background - "tertiaryColor" to "#40375c", - "lineColor" to "#f5a623", - "fontSize" to "12px", - ), - focusColor = "#F5A622", - moduleTypes = listOf( - ModuleType.Kotlin("#2C4162"), - ), - ), - ) - excludedConfigurationsRegex.set(""".*test.*""") - // focusedModulesRegex.set(""".*gama.*""") - // excludedModulesRegex.set(".*alpha.*") - // rootModulesRegex.set(".*gama.*") - - /* Setup additional graphs */ - graph( - readmePath = "./README.md", - heading = "# Graph with root: gama", - ) { - nestingEnabled = true - rootModulesRegex = ".*gama.*" - } - graph( - readmePath = "./SomeOtherReadme.md", - heading = "# Graph", - ) { - nestingEnabled = false - rootModulesRegex = ".*zeta.*" - } -} - task("check") { dependsOn(tasks.detekt) dependsOn(tasks.ktlintCheck) diff --git a/settings.gradle.kts b/settings.gradle.kts index 67f81b9..082428f 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -1,4 +1,6 @@ pluginManagement { + includeBuild("plugin-build") + repositories { gradlePluginPortal() mavenCentral() @@ -6,7 +8,12 @@ pluginManagement { } } +plugins { + id("dev.iurysouza.modulegraph.settings") +} + dependencyResolutionManagement { + @Suppress("UnstableApiUsage") repositories { mavenCentral() google() @@ -21,4 +28,3 @@ include(":sample:zeta") include(":sample:test") include(":sample:container:gama") include(":sample:container:delta") -includeBuild("plugin-build")