Skip to content

Commit d73fd46

Browse files
authored
fix(gradle): resolve dependencies after capturing project tasks (#34045)
<!-- Please make sure you have read the submission guidelines before posting an PR --> <!-- https://github.com/nrwl/nx/blob/master/CONTRIBUTING.md#-submitting-a-pr --> <!-- Please make sure that your commit message follows our format --> <!-- Example: `fix(nx): must begin with lowercase` --> <!-- If this is a particularly complex change or feature addition, you can request a dedicated Nx release for this pull request branch. Mention someone from the Nx team or the `@nrwl/nx-pipelines-reviewers` and they will confirm if the PR warrants its own release for testing purposes, and generate it for you if appropriate. --> ## Current Behavior <!-- This is the behavior we have today --> When processing Kotlin Multiplatform (KMP) projects, the Nx Gradle plugin encounters ConcurrentModificationException errors because KMP dynamically modifies the Gradle project's task and configuration containers during dependency resolution. The plugin was resolving configuration dependencies before processing tasks, which triggered KMP's hierarchy finalization and dynamic task creation while the plugin was still iterating over these collections. ## Expected Behavior <!-- This is the behavior we should expect with the changes in this PR --> The Gradle plugin should handle Kotlin Multiplatform projects without errors by: 1. Processing tasks before resolving configuration dependencies, preventing KMP from modifying task containers during iteration 2. Creating immutable snapshots of task and configuration collections before iteration to avoid concurrent modification issues ## Related Issue(s) <!-- Please link the issue being fixed so it gets closed when this is merged. --> Fixes NXC-3633
1 parent 99a9216 commit d73fd46

File tree

2 files changed

+20
-13
lines changed

2 files changed

+20
-13
lines changed

packages/gradle/project-graph/src/main/kotlin/dev/nx/gradle/utils/ProjectDependencyUtils.kt

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,11 @@ private fun buildDependenciesForProject(project: Project): Set<Dependency> {
1919
val sourcePath = project.projectDir.absolutePath
2020
val sourceFilePath = project.buildFile.takeIf { it.exists() }?.absolutePath ?: ""
2121

22+
// Create a snapshot of configurations to avoid ConcurrentModificationException
23+
// with Kotlin Multiplatform which adds configurations dynamically
2224
project.configurations
23-
.matching { it.isCanBeResolved }
25+
.filter { it.isCanBeResolved }
26+
.toList()
2427
.forEach { conf ->
2528
try {
2629
conf.incoming.resolutionResult.allDependencies.forEach { dependency ->

packages/gradle/project-graph/src/main/kotlin/dev/nx/gradle/utils/ProjectUtils.kt

Lines changed: 16 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -18,16 +18,7 @@ fun createNodeForProject(
1818
val logger = project.logger
1919
logger.info("${Date()} ${project.name} createNodeForProject: get nodes and dependencies")
2020

21-
// Initialize dependencies with an empty Set to prevent null issues
22-
val dependencies: MutableSet<Dependency> =
23-
try {
24-
getDependenciesForProject(project)
25-
} catch (e: Exception) {
26-
logger.info(
27-
"${Date()} ${project.name} createNodeForProject: get dependencies error: ${e.message}")
28-
mutableSetOf()
29-
}
30-
logger.info("${Date()} ${project.name} createNodeForProject: got dependencies")
21+
val dependencies: MutableSet<Dependency> = mutableSetOf()
3122

3223
// Initialize nodes and externalNodes with empty maps to prevent null issues
3324
var nodes: Map<String, ProjectNode>
@@ -37,6 +28,15 @@ fun createNodeForProject(
3728
val gradleTargets: GradleTargets =
3829
processTargetsForProject(
3930
project, dependencies, targetNameOverrides, workspaceRoot, atomized, targetNamePrefix)
31+
32+
try {
33+
val configDependencies = getDependenciesForProject(project)
34+
dependencies.addAll(configDependencies)
35+
logger.info("${Date()} ${project.name} createNodeForProject: got configuration dependencies")
36+
} catch (e: Exception) {
37+
logger.info(
38+
"${Date()} ${project.name} createNodeForProject: get dependencies error: ${e.message}")
39+
}
4040
val projectRoot = project.projectDir.path
4141

4242
// Read project-level nx config if it exists
@@ -112,13 +112,17 @@ fun processTargetsForProject(
112112
val ciTestTargetBaseName = targetNameOverrides["ciTestTargetName"]?.let { applyPrefix(it) }
113113
val testTargetName = applyPrefix(targetNameOverrides.getOrDefault("testTargetName", "test"))
114114

115-
val testTasks = project.tasks.withType(Test::class.java)
115+
// Create a snapshot of test tasks to avoid ConcurrentModificationException
116+
// with Kotlin Multiplatform which adds tasks dynamically
117+
val testTasks = project.tasks.withType(Test::class.java).toList()
116118
val hasCiTestTarget = ciTestTargetBaseName != null && testTasks.isNotEmpty() && atomized
117119

118120
logger.info(
119121
"${project.name}: hasCiTestTarget = $hasCiTestTarget (ciTestTargetName=$ciTestTargetBaseName, testTasks.size=${testTasks.size}, atomized=$atomized)")
120122

121-
project.tasks.forEach { task ->
123+
// Create a snapshot of all tasks to avoid ConcurrentModificationException
124+
// with Kotlin Multiplatform which adds tasks dynamically
125+
project.tasks.toList().forEach { task ->
122126
try {
123127
val now = Date()
124128
logger.info("$now ${project.name}: Processing task ${task.path}")

0 commit comments

Comments
 (0)