diff --git a/CHANGELOG.md b/CHANGELOG.md index 68ff115..9eefdff 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,10 @@ # CHANGELOG +## 1.0.0-rc02 (2026-03-16) + +- Fixed issue during dependency resolution when configuration cache is enabled + ## 1.0.0-rc01 (2026-03-14) - Fixed NPE caused by empty module suppressions when the baseline is manually edited diff --git a/codecov.yml b/codecov.yml index ab540e5..c490ce0 100644 --- a/codecov.yml +++ b/codecov.yml @@ -3,4 +3,7 @@ coverage: project: default: target: 80% # the required coverage value - threshold: 1% # the leniency in hitting the target \ No newline at end of file + threshold: 1% # the leniency in hitting the target + patch: + default: + target: 80% \ No newline at end of file diff --git a/projectguard/src/main/kotlin/com/rubensousa/projectguard/plugin/ProjectGuardExtension.kt b/projectguard/src/main/kotlin/com/rubensousa/projectguard/plugin/ProjectGuardExtension.kt index 7580420..989953d 100644 --- a/projectguard/src/main/kotlin/com/rubensousa/projectguard/plugin/ProjectGuardExtension.kt +++ b/projectguard/src/main/kotlin/com/rubensousa/projectguard/plugin/ProjectGuardExtension.kt @@ -30,6 +30,7 @@ import com.rubensousa.projectguard.plugin.internal.ReportSpec import com.rubensousa.projectguard.plugin.internal.getDependencyPath import org.gradle.api.Action import org.gradle.api.artifacts.MinimalExternalModuleDependency +import org.gradle.api.internal.provider.DefaultProvider import org.gradle.api.model.ObjectFactory import org.gradle.api.provider.Provider import org.gradle.kotlin.dsl.listProperty @@ -45,6 +46,7 @@ abstract class ProjectGuardExtension @Inject constructor( private val dependencyRestrictionSpecs = objects.listProperty() private val reportSpec = objects.property().convention(ReportSpec(showLibrariesInGraph = false)) private val options = objects.property().convention(PluginOptions(lifecycleTask = null)) + private val provider = DefaultProvider { getSpec() } override fun restrictModule(modulePath: String, action: Action) { val scope = ModuleRestrictionScopeImpl() @@ -126,13 +128,25 @@ abstract class ProjectGuardExtension @Inject constructor( override fun report(action: Action) { val scope = ReportScopeImpl() action.execute(scope) - reportSpec.set(ReportSpec(showLibrariesInGraph = scope.showLibrariesInGraph)) + reportSpec.set( + reportSpec.get().copy( + showLibrariesInGraph = scope.showLibrariesInGraph + ) + ) } override fun options(action: Action) { val scope = OptionScopeImpl() action.execute(scope) - options.set(PluginOptions(lifecycleTask = scope.lifecycleTask)) + options.set( + options.get().copy( + lifecycleTask = scope.lifecycleTask, + ) + ) + } + + internal fun getSpecProvider(): Provider { + return provider } internal fun getSpec(): ProjectGuardSpec { diff --git a/projectguard/src/main/kotlin/com/rubensousa/projectguard/plugin/ProjectGuardPlugin.kt b/projectguard/src/main/kotlin/com/rubensousa/projectguard/plugin/ProjectGuardPlugin.kt index 2b2f413..52ab989 100644 --- a/projectguard/src/main/kotlin/com/rubensousa/projectguard/plugin/ProjectGuardPlugin.kt +++ b/projectguard/src/main/kotlin/com/rubensousa/projectguard/plugin/ProjectGuardPlugin.kt @@ -17,7 +17,7 @@ package com.rubensousa.projectguard.plugin import com.android.build.api.variant.AndroidComponentsExtension -import com.rubensousa.projectguard.plugin.internal.DependencyGraphBuilder +import com.rubensousa.projectguard.plugin.internal.DependencyConfiguration import com.rubensousa.projectguard.plugin.internal.task.TaskAggregateDependencyDump import com.rubensousa.projectguard.plugin.internal.task.TaskAggregateRestrictionDump import com.rubensousa.projectguard.plugin.internal.task.TaskBaseline @@ -53,7 +53,6 @@ class ProjectGuardPlugin : Plugin { private val htmlAggregateReportFilePath = "reports/$pluginId" private val dependenciesFilePath = "reports/$pluginId/dependencies.json" private val jsonReportFilePath = "reports/$pluginId/report.json" - private val graphBuilder = DependencyGraphBuilder() private val androidPluginIds = listOf( "com.android.test", "com.android.application", @@ -292,7 +291,7 @@ class ProjectGuardPlugin : Plugin { ) { group = "verification" description = "Verifies if there are any dependency restrictions" - pluginSpec.set(project.provider { extension.getSpec() }) + pluginSpec.set(extension.getSpecProvider()) outputs.upToDateWhen { false } } } @@ -307,7 +306,7 @@ class ProjectGuardPlugin : Plugin { ) { group = "verification" description = "Verifies if there are any dependency restrictions" - pluginSpec.set(project.provider { extension.getSpec() }) + pluginSpec.set(extension.getSpecProvider()) outputs.upToDateWhen { false } } } @@ -323,7 +322,7 @@ class ProjectGuardPlugin : Plugin { group = "other" description = "Generates a JSON report of all dependency restrictions for this module." projectPath.set(project.path) - specProperty.set(project.provider { extension.getSpec() }) + specProperty.set(extension.getSpecProvider()) outputFile.set( project.layout.buildDirectory.file(jsonReportFilePath) ) @@ -340,7 +339,14 @@ class ProjectGuardPlugin : Plugin { group = "other" description = "Generates a JSON containing the dependencies of this module." projectPath.set(project.path) - dependencyGraph.set(project.provider { graphBuilder.buildFromProject(project) }) + project.configurations.forEach { config -> + if (config.isCanBeResolved && DependencyConfiguration.isConfigurationSupported(config.name)) { + components.put( + config.name, + config.incoming.resolutionResult.rootComponent + ) + } + } outputFile.set( project.layout.buildDirectory.file(dependenciesFilePath) ) diff --git a/projectguard/src/main/kotlin/com/rubensousa/projectguard/plugin/internal/DependencyGraphBuilder.kt b/projectguard/src/main/kotlin/com/rubensousa/projectguard/plugin/internal/DependencyGraphBuilder.kt index 3f264c7..5d19f9a 100644 --- a/projectguard/src/main/kotlin/com/rubensousa/projectguard/plugin/internal/DependencyGraphBuilder.kt +++ b/projectguard/src/main/kotlin/com/rubensousa/projectguard/plugin/internal/DependencyGraphBuilder.kt @@ -17,9 +17,13 @@ package com.rubensousa.projectguard.plugin.internal import com.rubensousa.projectguard.plugin.internal.report.DependencyGraphDump -import org.gradle.api.Project -import org.gradle.api.artifacts.ExternalModuleDependency -import org.gradle.api.artifacts.ProjectDependency +import org.gradle.api.artifacts.component.ModuleComponentIdentifier +import org.gradle.api.artifacts.component.ModuleComponentSelector +import org.gradle.api.artifacts.component.ProjectComponentIdentifier +import org.gradle.api.artifacts.result.ResolvedComponentResult +import org.gradle.api.artifacts.result.ResolvedDependencyResult +import org.gradle.api.internal.artifacts.result.DefaultUnresolvedDependencyResult +import org.gradle.api.provider.Provider internal class DependencyGraphBuilder { @@ -42,35 +46,53 @@ internal class DependencyGraphBuilder { return graph } - fun buildFromProject(project: Project): DependencyGraph { + fun buildFromComponents(results: Map>): DependencyGraph { val graph = DependencyGraph() - project.configurations - .filter { config -> config.isCanBeResolved && DependencyConfiguration.isConfigurationSupported(config.name) } - .forEach { config -> - val moduleId = project.path - config.incoming.dependencies - .forEach { dependency -> - when (dependency) { - is ProjectDependency -> { - if (dependency.path != moduleId) { - graph.addInternalDependency( - module = moduleId, - dependency = dependency.path, - configurationId = config.name - ) - } - } - - is ExternalModuleDependency -> { + results.forEach { (configurationId, resultProvider) -> + val result = resultProvider.get() + val resultId = result.id + if (resultId is ProjectComponentIdentifier) { + val moduleId = resultId.projectPath + result.dependencies.forEach { dependencyResult -> + when (dependencyResult) { + is DefaultUnresolvedDependencyResult -> { + val requested = dependencyResult.requested + if (requested is ModuleComponentSelector) { graph.addLibraryDependency( module = moduleId, - dependency = "${dependency.group}:${dependency.name}", - configurationId = config.name + dependency = "${requested.group}:${requested.module}", + configurationId = configurationId, ) } } + + is ResolvedDependencyResult -> { + val selected = dependencyResult.selected + when (val projectId = selected.id) { + is ProjectComponentIdentifier -> { + if (projectId.projectPath != moduleId) { + graph.addInternalDependency( + module = moduleId, + dependency = projectId.projectPath, + configurationId = configurationId, + ) + } + } + + is ModuleComponentIdentifier -> { + graph.addLibraryDependency( + module = moduleId, + dependency = "${projectId.group}:${projectId.module}", + configurationId = configurationId, + ) + } + } + } } + } } + } return graph } -} \ No newline at end of file + +} diff --git a/projectguard/src/main/kotlin/com/rubensousa/projectguard/plugin/internal/task/TaskDependencyDump.kt b/projectguard/src/main/kotlin/com/rubensousa/projectguard/plugin/internal/task/TaskDependencyDump.kt index 2708682..0bb36c6 100644 --- a/projectguard/src/main/kotlin/com/rubensousa/projectguard/plugin/internal/task/TaskDependencyDump.kt +++ b/projectguard/src/main/kotlin/com/rubensousa/projectguard/plugin/internal/task/TaskDependencyDump.kt @@ -17,42 +17,46 @@ package com.rubensousa.projectguard.plugin.internal.task import com.rubensousa.projectguard.plugin.internal.DependencyGraph +import com.rubensousa.projectguard.plugin.internal.DependencyGraphBuilder import com.rubensousa.projectguard.plugin.internal.report.ConfigurationDependencies import com.rubensousa.projectguard.plugin.internal.report.DependencyGraphDump import com.rubensousa.projectguard.plugin.internal.report.DependencyGraphModuleDump import com.rubensousa.projectguard.plugin.internal.report.DependencyReferenceDump import com.rubensousa.projectguard.plugin.internal.report.JsonFileWriter import org.gradle.api.DefaultTask +import org.gradle.api.artifacts.result.ResolvedComponentResult import org.gradle.api.file.RegularFileProperty +import org.gradle.api.provider.MapProperty import org.gradle.api.provider.Property -import org.gradle.api.tasks.CacheableTask +import org.gradle.api.provider.Provider import org.gradle.api.tasks.Input import org.gradle.api.tasks.OutputFile import org.gradle.api.tasks.TaskAction +import org.gradle.work.DisableCachingByDefault import java.io.File -@CacheableTask +@DisableCachingByDefault(because = "Component result is not serializable") internal abstract class TaskDependencyDump : DefaultTask() { @get:Input internal abstract val projectPath: Property @get:Input - internal abstract val dependencyGraph: Property + internal abstract val components: MapProperty> @get:OutputFile internal abstract val outputFile: RegularFileProperty @TaskAction fun projectGuardDependencyDump() { + val graph = DependencyGraphBuilder().buildFromComponents(components.get()) val executor = DependencyDumpExecutor( moduleId = projectPath.get(), outputFile = outputFile.get().asFile, - dependencyGraph = dependencyGraph.get() + dependencyGraph = graph, ) executor.execute() } - } internal class DependencyDumpExecutor( diff --git a/projectguard/src/main/kotlin/com/rubensousa/projectguard/plugin/internal/task/TaskRestrictionDump.kt b/projectguard/src/main/kotlin/com/rubensousa/projectguard/plugin/internal/task/TaskRestrictionDump.kt index 8de6cc8..492cb54 100644 --- a/projectguard/src/main/kotlin/com/rubensousa/projectguard/plugin/internal/task/TaskRestrictionDump.kt +++ b/projectguard/src/main/kotlin/com/rubensousa/projectguard/plugin/internal/task/TaskRestrictionDump.kt @@ -30,7 +30,6 @@ import kotlinx.serialization.json.Json import org.gradle.api.DefaultTask import org.gradle.api.file.RegularFileProperty import org.gradle.api.provider.Property -import org.gradle.api.tasks.CacheableTask import org.gradle.api.tasks.Input import org.gradle.api.tasks.InputFile import org.gradle.api.tasks.OutputFile @@ -39,7 +38,6 @@ import org.gradle.api.tasks.PathSensitivity import org.gradle.api.tasks.TaskAction import java.io.File -@CacheableTask internal abstract class TaskRestrictionDump : DefaultTask() { @get:Input diff --git a/projectguard/src/test/kotlin/com/rubensousa/projectguard/plugin/PluginCacheTest.kt b/projectguard/src/test/kotlin/com/rubensousa/projectguard/plugin/PluginCacheTest.kt index af14397..930641f 100644 --- a/projectguard/src/test/kotlin/com/rubensousa/projectguard/plugin/PluginCacheTest.kt +++ b/projectguard/src/test/kotlin/com/rubensousa/projectguard/plugin/PluginCacheTest.kt @@ -19,7 +19,6 @@ package com.rubensousa.projectguard.plugin import com.google.common.truth.Truth.assertThat import org.gradle.testkit.runner.TaskOutcome import org.junit.Before -import org.junit.Ignore import org.junit.Rule import org.junit.Test import org.junit.rules.TemporaryFolder @@ -36,6 +35,8 @@ class PluginCacheTest { @Before fun setup() { rootBuildFile = temporaryFolder.newFile("build.gradle.kts") + val propertiesFile = temporaryFolder.newFile("gradle.properties") + propertiesFile.writeText("org.gradle.configuration-cache=true") rootBuildFile.writeText( """ plugins { @@ -67,7 +68,7 @@ class PluginCacheTest { } @Test - fun `outputs from projectGuardDependencyDump are re-used from cache`() { + fun `outputs from projectGuardDependencyDump are not re-used from cache`() { // given pluginRunner.createModule("a") pluginRunner.createModule("b") @@ -80,7 +81,7 @@ class PluginCacheTest { val result = pluginRunner.runTask(libraryDependencyTask) // then - assertThat(result).isEqualTo(TaskOutcome.FROM_CACHE) + assertThat(result).isEqualTo(TaskOutcome.SUCCESS) } @Test @@ -118,24 +119,6 @@ class PluginCacheTest { assertThat(result).isEqualTo(TaskOutcome.UP_TO_DATE) } - @Ignore("Not working for now") - @Test - fun `outputs from projectGuardAggregateDependencyDump are re-used from cache`() { - // given - pluginRunner.createModule("a") - pluginRunner.createModule("b") - pluginRunner.addDependency(from = "a", to = "b") - val task = ":projectGuardAggregateDependencyDump" - pluginRunner.runTask(task) - - // when - pluginRunner.deleteBuildDirs() - val result = pluginRunner.runTask(task) - - // then - assertThat(result).isEqualTo(TaskOutcome.FROM_CACHE) - } - @Test fun `outputs from projectGuardAggregateDependencyDump are not re-used if dependencies changed`() { // given @@ -170,7 +153,7 @@ class PluginCacheTest { } @Test - fun `outputs from projectGuardRestrictionDump are cached`() { + fun `outputs from projectGuardRestrictionDump are not cached`() { // given pluginRunner.createModule("a") pluginRunner.createModule("b") @@ -183,7 +166,7 @@ class PluginCacheTest { val result = pluginRunner.runTask(task) // then - assertThat(result).isEqualTo(TaskOutcome.FROM_CACHE) + assertThat(result).isEqualTo(TaskOutcome.SUCCESS) } @Test diff --git a/projectguard/src/test/kotlin/com/rubensousa/projectguard/plugin/PluginIntegrationTest.kt b/projectguard/src/test/kotlin/com/rubensousa/projectguard/plugin/PluginIntegrationTest.kt index 10a6496..27da4c0 100644 --- a/projectguard/src/test/kotlin/com/rubensousa/projectguard/plugin/PluginIntegrationTest.kt +++ b/projectguard/src/test/kotlin/com/rubensousa/projectguard/plugin/PluginIntegrationTest.kt @@ -32,6 +32,8 @@ class PluginIntegrationTest { @Before fun setup() { + val propertiesFile = temporaryFolder.newFile("gradle.properties") + propertiesFile.writeText("org.gradle.configuration-cache=true") rootBuildFile = temporaryFolder.newFile("build.gradle.kts") rootBuildFile.writeText( """ diff --git a/projectguard/src/test/kotlin/com/rubensousa/projectguard/plugin/ProjectGuardExtensionTest.kt b/projectguard/src/test/kotlin/com/rubensousa/projectguard/plugin/ProjectGuardExtensionTest.kt index 7ee47ae..36a77ea 100644 --- a/projectguard/src/test/kotlin/com/rubensousa/projectguard/plugin/ProjectGuardExtensionTest.kt +++ b/projectguard/src/test/kotlin/com/rubensousa/projectguard/plugin/ProjectGuardExtensionTest.kt @@ -42,6 +42,18 @@ class ProjectGuardExtensionTest { assertThat(guards.first().denied.first().modulePath).isEqualTo(":legacy") } + @Test + fun `extension provider returns spec`() { + // given + val extension = createExtension() + + // when + val spec = extension.getSpec() + + // then + assertThat(extension.getSpecProvider().get()).isEqualTo(spec) + } + @Test fun `extension correctly configures module restriction`() { // given diff --git a/projectguard/src/test/kotlin/com/rubensousa/projectguard/plugin/internal/DependencyGraphBuilderTest.kt b/projectguard/src/test/kotlin/com/rubensousa/projectguard/plugin/internal/DependencyGraphBuilderTest.kt index ac90eea..fd7ec9a 100644 --- a/projectguard/src/test/kotlin/com/rubensousa/projectguard/plugin/internal/DependencyGraphBuilderTest.kt +++ b/projectguard/src/test/kotlin/com/rubensousa/projectguard/plugin/internal/DependencyGraphBuilderTest.kt @@ -18,6 +18,9 @@ package com.rubensousa.projectguard.plugin.internal import com.google.common.truth.Truth.assertThat import org.gradle.api.Project +import org.gradle.api.artifacts.result.ResolvedComponentResult +import org.gradle.api.provider.Provider +import org.gradle.kotlin.dsl.repositories import org.gradle.testfixtures.ProjectBuilder import org.junit.Before import kotlin.test.Test @@ -53,7 +56,7 @@ class DependencyGraphBuilderTest { val legacyProjectB = consumerProject.addLegacyDependency("b") // when - val graph = graphBuilder.buildFromProject(consumerProject) + val graph = graphBuilder.buildFromComponents(consumerProject.getResolvedConfigurations()) // then val compileConfiguration = graph.getConfigurations().find { it.id == DependencyConfiguration.COMPILE }!! @@ -61,6 +64,39 @@ class DependencyGraphBuilderTest { .containsExactly(legacyProjectA.path, legacyProjectB.path) } + @Test + fun `graph is built correctly when library is included but not resolved`() { + // given + val library = "com.google.truth:truth:1.4.5" + consumerProject.dependencies.add("implementation", library) + + // when + val graph = graphBuilder.buildFromComponents(consumerProject.getResolvedConfigurations()) + + // then + val compileConfiguration = graph.getConfigurations().find { it.id == DependencyConfiguration.COMPILE }!! + assertThat(compileConfiguration.getDependencies(consumerProject.path).map { it.id }) + .containsExactly("com.google.truth:truth") + } + + @Test + fun `graph is built correctly when library is included and resolved`() { + // given + val library = "com.google.truth:truth:1.4.5" + consumerProject.repositories { + mavenCentral() + } + consumerProject.dependencies.add("implementation", library) + + // when + val graph = graphBuilder.buildFromComponents(consumerProject.getResolvedConfigurations()) + + // then + val compileConfiguration = graph.getConfigurations().find { it.id == DependencyConfiguration.COMPILE }!! + assertThat(compileConfiguration.getDependencies(consumerProject.path).map { it.id }) + .containsExactly("com.google.truth:truth") + } + @Test fun `graph is built correctly for testImplementation configuration`() { // given @@ -68,7 +104,7 @@ class DependencyGraphBuilderTest { val legacyProjectC = consumerProject.addLegacyTestDependency("c") // when - val graph = graphBuilder.buildFromProject(consumerProject) + val graph = graphBuilder.buildFromComponents(consumerProject.getResolvedConfigurations()) // then val testConfiguration = graph.getConfigurations().find { it.id == DependencyConfiguration.TEST }!! @@ -76,6 +112,16 @@ class DependencyGraphBuilderTest { .containsExactly(legacyProjectA.path, legacyProjectC.path) } + private fun Project.getResolvedConfigurations(): Map> { + val output = mutableMapOf>() + project.configurations.forEach { config -> + if (config.isCanBeResolved) { + output[config.name] = config.incoming.resolutionResult.rootComponent + } + } + return output + } + private fun Project.addLegacyDependency(dependency: String): Project { val legacyProject = createLegacySubProject(dependency) dependencies.add("implementation", legacyProject) diff --git a/sample/gradle.properties b/sample/gradle.properties new file mode 100644 index 0000000..2d8d1e4 --- /dev/null +++ b/sample/gradle.properties @@ -0,0 +1 @@ +android.useAndroidX=true \ No newline at end of file