Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions e2e/gradle/src/gradle-import.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import { createFileSync } from 'fs-extra';

describe('Nx Import Gradle', () => {
const tempImportE2ERoot = join(e2eCwd, 'nx-import');
beforeAll(() => {
beforeEach(() => {
newProject({
packages: ['@nx/js'],
});
Expand Down Expand Up @@ -49,7 +49,7 @@ describe('Nx Import Gradle', () => {
runCommand(`git commit -am "update"`);
});

afterAll(() => cleanupProject());
afterEach(() => cleanupProject());

it('should be able to import a kotlin gradle app', () => {
const tempGradleProjectName = 'created-gradle-app-kotlin';
Expand Down
5 changes: 4 additions & 1 deletion e2e/gradle/src/gradle.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,10 @@ describe('Gradle', () => {
expect(buildOutput).toContain(':list:classes');
expect(buildOutput).toContain(':utilities:classes');

const bootJarOutput = runCLI('bootJar app', { verbose: true });
const bootJarOutput = runCLI('bootJar app', {
verbose: true,
redirectStderr: true,
});
expect(bootJarOutput).toContain(':app:bootJar');
});

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package dev.nx.gradle.data

import com.google.gson.annotations.SerializedName

enum class DependsOnParams {
@SerializedName("forward") FORWARD,
@SerializedName("ignore") IGNORE
}

data class DependsOnEntry(
val target: String,
val projects: List<String>? = null,
val params: DependsOnParams? = null
)
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package dev.nx.gradle.utils

import dev.nx.gradle.data.DependsOnEntry
import dev.nx.gradle.data.DependsOnParams
import dev.nx.gradle.data.NxTargets
import dev.nx.gradle.data.TargetGroups
import dev.nx.gradle.utils.parsing.containsEssentialTestAnnotations
Expand All @@ -23,7 +25,7 @@ fun addTestCiTargets(
) {
ensureTargetGroupExists(targetGroups, testCiTargetGroup)

val ciDependsOn = mutableListOf<Map<String, String>>()
val ciDependsOn = mutableListOf<DependsOnEntry>()

processTestFiles(
testFiles,
Expand Down Expand Up @@ -58,7 +60,7 @@ private fun processTestFiles(
projectRoot: String,
workspaceRoot: String,
ciTestTargetName: String,
ciDependsOn: MutableList<Map<String, String>>,
ciDependsOn: MutableList<DependsOnEntry>,
gitIgnoreClassifier: GitIgnoreClassifier
) {
testFiles
Expand All @@ -78,8 +80,7 @@ private fun processTestFiles(
gitIgnoreClassifier)
targetGroups[testCiTargetGroup]?.add(targetName)

ciDependsOn.add(
mapOf("target" to targetName, "projects" to "self", "params" to "forward"))
ciDependsOn.add(DependsOnEntry(target = targetName, params = DependsOnParams.FORWARD))
}
}
}
Expand Down Expand Up @@ -143,7 +144,7 @@ private fun ensureParentCiTarget(
testTask: Task,
projectRoot: String,
workspaceRoot: String,
ciDependsOn: List<Map<String, String>>,
ciDependsOn: List<DependsOnEntry>,
gitIgnoreClassifier: GitIgnoreClassifier
) {
if (ciDependsOn.isNotEmpty()) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import dev.nx.gradle.data.*
import java.io.File
import java.util.*
import org.gradle.api.Project
import org.gradle.api.Task
import org.gradle.api.tasks.testing.Test

/**
Expand Down Expand Up @@ -123,6 +124,8 @@ fun processTargetsForProject(
// with Kotlin Multiplatform which adds tasks dynamically
val testTasks = project.tasks.withType(Test::class.java).toList()
val hasCiTestTarget = ciTestTargetBaseName != null && testTasks.isNotEmpty() && atomized
// Pre-index test tasks by prefixed name for O(1) lookup during dependency replacement
val testTasksByPrefixedName = testTasks.associateBy { applyPrefix(it.name) }

logger.info(
"${project.name}: hasCiTestTarget = $hasCiTestTarget (ciTestTargetName=$ciTestTargetBaseName, testTasks.size=${testTasks.size}, atomized=$atomized)")
Expand All @@ -134,14 +137,7 @@ fun processTargetsForProject(
val now = Date()
logger.info("$now ${project.name}: Processing task ${task.path}")

// Apply target name override if applicable, then apply prefix
val targetName =
applyPrefix(
if (task.name == "test" && targetNameOverrides.containsKey("testTargetName")) {
targetNameOverrides["testTargetName"]!!
} else {
task.name
})
val targetName = resolveTargetName(task, targetNameOverrides, targetNamePrefix)

// Group task under its group if available, using the overridden name
task.group
Expand Down Expand Up @@ -204,62 +200,51 @@ fun processTargetsForProject(
if (ciTestTargetBaseName != null) {
val ciCheckTargetName =
applyPrefix(targetNameOverrides.getOrDefault("ciCheckTargetName", "check-ci"))

// Build CI test replacements: maps original target names to their CI equivalents
// e.g., "test" -> "ci-test", "testDebug" -> "ci-test-testDebug"
val ciTestReplacements = mutableMapOf<String, String>()
if (hasCiTestTarget) {
testTasksByPrefixedName.forEach { (prefixedName, testTask) ->
ciTestReplacements[prefixedName] = "$ciTestTargetBaseName-${testTask.name}"
}
// The default test target gets the base CI name (e.g., "test" -> "ci-test")
// Set after the loop so it takes priority over the generic pattern
ciTestReplacements[testTargetName] = ciTestTargetBaseName!!
}

if (task.name == "check") {
val replacedDependencies =
(target["dependsOn"] as? List<*>)?.map { dependency ->
val dependsOn = dependency.toString()

when {
hasCiTestTarget && dependsOn == "$nxProjectName:$testTargetName" -> {
"$nxProjectName:$ciTestTargetBaseName"
}
hasCiTestTarget && dependsOn.startsWith("$nxProjectName:") -> {
val taskName = dependsOn.removePrefix("$nxProjectName:")
// Check if it's a test task that's not the default test target
if (testTasks.any { it.name == taskName } &&
applyPrefix(taskName) != testTargetName) {
"$nxProjectName:$ciTestTargetBaseName-$taskName"
} else {
dependency
}
}
else -> dependency
}
} ?: emptyList()

val newTarget: MutableMap<String, Any?> =
val ciCheckDependsOn =
buildCiDependsOn(
task, project, targetNameOverrides, targetNamePrefix, ciTestReplacements)

targets[ciCheckTargetName] =
mutableMapOf(
"dependsOn" to replacedDependencies,
"dependsOn" to ciCheckDependsOn,
"executor" to "nx:noop",
"cache" to true,
"metadata" to getMetadata("Runs Gradle Check in CI", projectBuildPath, "check"))

targets[ciCheckTargetName] = newTarget
ensureTargetGroupExists(targetGroups, testCiTargetGroup)
targetGroups[testCiTargetGroup]?.add(ciCheckTargetName)
}

if (task.name == "build") {
val ciBuildTargetName =
applyPrefix(targetNameOverrides.getOrDefault("ciBuildTargetName", "build-ci"))
val replacedDependencies =
(target["dependsOn"] as? List<*>)?.map { dep ->
val dependsOn = dep.toString()
if (dependsOn == "$nxProjectName:${applyPrefix("check")}") {
"$nxProjectName:$ciCheckTargetName"
} else {
dep
}
} ?: emptyList()

val newTarget: MutableMap<String, Any?> =
val ciBuildDependsOn =
buildCiDependsOn(
task,
project,
targetNameOverrides,
targetNamePrefix,
mapOf(applyPrefix("check") to ciCheckTargetName))

targets[ciBuildTargetName] =
mutableMapOf(
"dependsOn" to replacedDependencies,
"dependsOn" to ciBuildDependsOn,
"executor" to "nx:noop",
"cache" to true,
"metadata" to getMetadata("Runs Gradle Build in CI", projectBuildPath, "build"))

targets[ciBuildTargetName] = newTarget
ensureTargetGroupExists(targetGroups, "build")
targetGroups["build"]?.add(ciBuildTargetName)
}
Expand All @@ -274,3 +259,57 @@ fun processTargetsForProject(
logger.info("Final targets in processTargetsForProject: $targets")
return GradleTargets(targets, targetGroups, externalNodes)
}

/**
* Build CI dependsOn list from a task's Gradle dependencies. Splits into same-project and
* cross-project entries, groups cross-project deps by target, and optionally replaces same-project
* target names using the provided map (e.g., test -> ci-test, check -> ci-check).
*/
fun buildCiDependsOn(
task: Task,
project: Project,
targetNameOverrides: Map<String, String>,
targetNamePrefix: String,
sameProjectReplacements: Map<String, String> = emptyMap()
): List<DependsOnEntry> {
val allDeps = getDependsOnTask(task)
val result = mutableListOf<DependsOnEntry>()
val crossProjectByTarget = mutableMapOf<String, MutableList<String>>()

allDeps.forEach { depTask ->
val depProject = depTask.project
if (depProject.buildFile.path != null && depProject.buildFile.exists()) {
val depTargetName = resolveTargetName(depTask, targetNameOverrides, targetNamePrefix)

if (depProject == project) {
val finalName = sameProjectReplacements[depTargetName] ?: depTargetName
result.add(DependsOnEntry(target = finalName))
} else {
crossProjectByTarget
.getOrPut(depTargetName) { mutableListOf() }
.add(getNxProjectName(depProject))
}
}
}

crossProjectByTarget.forEach { (targetName, projects) ->
result.add(DependsOnEntry(target = targetName, projects = projects.distinct()))
}

return result
}

/** Resolve a dependency task's target name, applying overrides and prefix. */
fun resolveTargetName(
depTask: Task,
targetNameOverrides: Map<String, String>,
targetNamePrefix: String
): String {
val baseName =
if (depTask.name == "test" && targetNameOverrides.containsKey("testTargetName")) {
targetNameOverrides["testTargetName"]!!
} else {
depTask.name
}
return if (targetNamePrefix.isNotEmpty()) "$targetNamePrefix$baseName" else baseName
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package dev.nx.gradle.utils

import dev.nx.gradle.NxTaskExtension
import dev.nx.gradle.data.Dependency
import dev.nx.gradle.data.DependsOnEntry
import dev.nx.gradle.data.ExternalDepData
import dev.nx.gradle.data.ExternalNode
import java.io.File
Expand Down Expand Up @@ -274,19 +275,16 @@ fun getDependsOnTask(task: Task): Set<Task> {
* @return list of dependsOn task names (possibly replaced), or null if none found or error occurred
*/
// Add a thread-local cache to prevent infinite recursion in dependency resolution
internal val taskDependencyCache = ThreadLocal.withInitial { mutableMapOf<String, List<String>?>() }
internal val taskDependencyCache =
ThreadLocal.withInitial { mutableMapOf<String, List<DependsOnEntry>?>() }

fun getDependsOnForTask(
dependsOnTasks: Set<Task>?,
task: Task,
dependencies: MutableSet<Dependency>? = null,
targetNameOverrides: Map<String, String> = emptyMap(),
targetNamePrefix: String = ""
): List<String>? {

// Helper function to apply prefix to target names
fun applyPrefix(name: String): String =
if (targetNamePrefix.isNotEmpty()) "$targetNamePrefix$name" else name
): List<DependsOnEntry>? {

// Check cache to prevent infinite recursion, but only if dependsOnTasks is null
// When dependsOnTasks is provided, we should not use cache since dependencies might be different
Expand All @@ -297,10 +295,13 @@ fun getDependsOnForTask(
return cache[taskKey]
}

fun mapTasksToNames(tasks: Collection<Task>): List<String> {
return tasks.mapNotNull { depTask ->
fun mapTasksToObjects(tasks: Collection<Task>): List<DependsOnEntry> {
val taskProject = task.project
val sameProjectDependsOn = mutableListOf<DependsOnEntry>()
val crossProjectByTarget = mutableMapOf<String, MutableList<String>>()

tasks.forEach { depTask ->
val depProject = depTask.project
val taskProject = task.project

if (task.name != "buildDependents" &&
depProject != taskProject &&
Expand All @@ -314,18 +315,23 @@ fun getDependsOnForTask(
}

if (depProject.buildFile.path != null && depProject.buildFile.exists()) {
val taskName =
applyPrefix(
if (depTask.name == "test" && targetNameOverrides.containsKey("testTargetName")) {
targetNameOverrides["testTargetName"]!!
} else {
depTask.name
})
"${getNxProjectName(depProject)}:${taskName}"
} else {
null
val targetName = resolveTargetName(depTask, targetNameOverrides, targetNamePrefix)
if (depProject == taskProject) {
sameProjectDependsOn.add(DependsOnEntry(target = targetName))
} else {
crossProjectByTarget
.getOrPut(targetName) { mutableListOf() }
.add(getNxProjectName(depProject))
}
}
}

val crossProjectDependsOn =
crossProjectByTarget.map { (targetName, projects) ->
DependsOnEntry(target = targetName, projects = projects.distinct())
}

return sameProjectDependsOn + crossProjectDependsOn
}

// Add a placeholder to prevent infinite recursion only when not using pre-computed dependencies
Expand All @@ -336,7 +342,7 @@ fun getDependsOnForTask(
val combinedDependsOn = getDependsOnTask(task)
val result =
if (combinedDependsOn.isNotEmpty()) {
mapTasksToNames(combinedDependsOn)
mapTasksToObjects(combinedDependsOn).ifEmpty { null }
} else {
null
}
Expand All @@ -358,7 +364,7 @@ fun getDependsOnForTask(
return try {
val result =
if (dependsOnTasks.isNotEmpty()) {
mapTasksToNames(dependsOnTasks)
mapTasksToObjects(dependsOnTasks).ifEmpty { null }
} else {
null
}
Expand Down
Loading
Loading