feat(gradle): add batch runner (#30457)
<!-- 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 --> Gradle tasks are run by invoking the Gradle CLI ## Expected Behavior <!-- This is the behavior we should expect with the changes in this PR --> Gradle tasks are run through the Gradle Tooling API and is more performant. ## Related Issue(s) <!-- Please link the issue being fixed so it gets closed when this is merged. --> Fixes # --------- Co-authored-by: Jason Jean <jasonjean1993@gmail.com>
This commit is contained in:
parent
57724d3df9
commit
624f0359e3
2
.github/workflows/ci.yml
vendored
2
.github/workflows/ci.yml
vendored
@ -76,7 +76,7 @@ jobs:
|
|||||||
uses: actions/setup-java@v4
|
uses: actions/setup-java@v4
|
||||||
with:
|
with:
|
||||||
distribution: temurin
|
distribution: temurin
|
||||||
java-version: 21
|
java-version: 17
|
||||||
|
|
||||||
- name: Check Documentation
|
- name: Check Documentation
|
||||||
run: pnpm nx documentation
|
run: pnpm nx documentation
|
||||||
|
|||||||
@ -64,13 +64,18 @@ launch-templates:
|
|||||||
- name: Load Cargo Env
|
- name: Load Cargo Env
|
||||||
script: echo "PATH=$HOME/.cargo/bin:$PATH" >> $NX_CLOUD_ENV
|
script: echo "PATH=$HOME/.cargo/bin:$PATH" >> $NX_CLOUD_ENV
|
||||||
|
|
||||||
- name: Setup Java 21
|
- name: Setup Java 17
|
||||||
script: |
|
script: |
|
||||||
sudo apt update
|
sudo apt update
|
||||||
sudo apt install -y openjdk-21-jdk
|
sudo apt install -y openjdk-17-jdk
|
||||||
sudo update-alternatives --set java /usr/lib/jvm/java-21-openjdk-amd64/bin/java
|
sudo update-alternatives --set java /usr/lib/jvm/java-17-openjdk-amd64/bin/java
|
||||||
java -version
|
java -version
|
||||||
|
|
||||||
|
- name: Setup Gradle
|
||||||
|
script: |
|
||||||
|
./gradlew wrapper
|
||||||
|
./gradlew --version
|
||||||
|
|
||||||
linux-extra-large:
|
linux-extra-large:
|
||||||
resource-class: 'docker_linux_amd64/extra_large'
|
resource-class: 'docker_linux_amd64/extra_large'
|
||||||
image: 'ubuntu22.04-node20.11-v10'
|
image: 'ubuntu22.04-node20.11-v10'
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
plugins {
|
plugins {
|
||||||
id("dev.nx.gradle.project-graph") version("0.0.2")
|
id("dev.nx.gradle.project-graph") version("0.1.0")
|
||||||
id("com.ncorti.ktfmt.gradle") version("+")
|
id("com.ncorti.ktfmt.gradle") version("+")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -8387,6 +8387,23 @@
|
|||||||
"isExternal": false,
|
"isExternal": false,
|
||||||
"disableCollapsible": false
|
"disableCollapsible": false
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"id": "executors",
|
||||||
|
"path": "/nx-api/gradle/executors",
|
||||||
|
"name": "executors",
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"id": "gradle",
|
||||||
|
"path": "/nx-api/gradle/executors/gradle",
|
||||||
|
"name": "gradle",
|
||||||
|
"children": [],
|
||||||
|
"isExternal": false,
|
||||||
|
"disableCollapsible": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"isExternal": false,
|
||||||
|
"disableCollapsible": false
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"id": "generators",
|
"id": "generators",
|
||||||
"path": "/nx-api/gradle/generators",
|
"path": "/nx-api/gradle/generators",
|
||||||
|
|||||||
@ -2103,7 +2103,17 @@
|
|||||||
},
|
},
|
||||||
"root": "/packages/gradle",
|
"root": "/packages/gradle",
|
||||||
"source": "/packages/gradle/src",
|
"source": "/packages/gradle/src",
|
||||||
"executors": {},
|
"executors": {
|
||||||
|
"/nx-api/gradle/executors/gradle": {
|
||||||
|
"description": "The Gradlew executor is used to run Gradle tasks.",
|
||||||
|
"file": "generated/packages/gradle/executors/gradle.json",
|
||||||
|
"hidden": false,
|
||||||
|
"name": "gradle",
|
||||||
|
"originalFilePath": "/packages/gradle/src/executors/gradle/schema.json",
|
||||||
|
"path": "/nx-api/gradle/executors/gradle",
|
||||||
|
"type": "executor"
|
||||||
|
}
|
||||||
|
},
|
||||||
"generators": {
|
"generators": {
|
||||||
"/nx-api/gradle/generators/init": {
|
"/nx-api/gradle/generators/init": {
|
||||||
"description": "Initializes a Gradle project in the current workspace",
|
"description": "Initializes a Gradle project in the current workspace",
|
||||||
|
|||||||
@ -2087,7 +2087,17 @@
|
|||||||
"originalFilePath": "shared/packages/gradle/gradle-plugin"
|
"originalFilePath": "shared/packages/gradle/gradle-plugin"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"executors": [],
|
"executors": [
|
||||||
|
{
|
||||||
|
"description": "The Gradlew executor is used to run Gradle tasks.",
|
||||||
|
"file": "generated/packages/gradle/executors/gradle.json",
|
||||||
|
"hidden": false,
|
||||||
|
"name": "gradle",
|
||||||
|
"originalFilePath": "/packages/gradle/src/executors/gradle/schema.json",
|
||||||
|
"path": "gradle/executors/gradle",
|
||||||
|
"type": "executor"
|
||||||
|
}
|
||||||
|
],
|
||||||
"generators": [
|
"generators": [
|
||||||
{
|
{
|
||||||
"description": "Initializes a Gradle project in the current workspace",
|
"description": "Initializes a Gradle project in the current workspace",
|
||||||
|
|||||||
37
docs/generated/packages/gradle/executors/gradle.json
Normal file
37
docs/generated/packages/gradle/executors/gradle.json
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
{
|
||||||
|
"name": "gradle",
|
||||||
|
"batchImplementation": "./src/executors/gradle/gradle-batch.impl",
|
||||||
|
"implementation": "/packages/gradle/src/executors/gradle/gradle.impl.ts",
|
||||||
|
"schema": {
|
||||||
|
"$schema": "https://json-schema.org/schema",
|
||||||
|
"version": 2,
|
||||||
|
"title": "Gradle Impl executor",
|
||||||
|
"description": "The Gradle Impl executor is used to run Gradle tasks.",
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"taskName": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "The name of the Gradle task to run."
|
||||||
|
},
|
||||||
|
"testClassName": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "The test class name to run for test task."
|
||||||
|
},
|
||||||
|
"args": {
|
||||||
|
"oneOf": [
|
||||||
|
{ "type": "array", "items": { "type": "string" } },
|
||||||
|
{ "type": "string" }
|
||||||
|
],
|
||||||
|
"description": "The arguments to pass to the Gradle task.",
|
||||||
|
"examples": [["--warning-mode", "all"], "--stracktrace"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["taskName"],
|
||||||
|
"presets": []
|
||||||
|
},
|
||||||
|
"description": "The Gradlew executor is used to run Gradle tasks.",
|
||||||
|
"aliases": [],
|
||||||
|
"hidden": false,
|
||||||
|
"path": "/packages/gradle/src/executors/gradle/schema.json",
|
||||||
|
"type": "executor"
|
||||||
|
}
|
||||||
@ -485,6 +485,8 @@
|
|||||||
- [gradle](/nx-api/gradle)
|
- [gradle](/nx-api/gradle)
|
||||||
- [documents](/nx-api/gradle/documents)
|
- [documents](/nx-api/gradle/documents)
|
||||||
- [Overview](/nx-api/gradle/documents/overview)
|
- [Overview](/nx-api/gradle/documents/overview)
|
||||||
|
- [executors](/nx-api/gradle/executors)
|
||||||
|
- [gradle](/nx-api/gradle/executors/gradle)
|
||||||
- [generators](/nx-api/gradle/generators)
|
- [generators](/nx-api/gradle/generators)
|
||||||
- [init](/nx-api/gradle/generators/init)
|
- [init](/nx-api/gradle/generators/init)
|
||||||
- [ci-workflow](/nx-api/gradle/generators/ci-workflow)
|
- [ci-workflow](/nx-api/gradle/generators/ci-workflow)
|
||||||
|
|||||||
@ -31,9 +31,7 @@ describe('Gradle', () => {
|
|||||||
expect(projects).toContain(gradleProjectName);
|
expect(projects).toContain(gradleProjectName);
|
||||||
|
|
||||||
const buildOutput = runCLI('build app', { verbose: true });
|
const buildOutput = runCLI('build app', { verbose: true });
|
||||||
expect(buildOutput).toContain('nx run list:');
|
|
||||||
expect(buildOutput).toContain(':list:classes');
|
expect(buildOutput).toContain(':list:classes');
|
||||||
expect(buildOutput).toContain('nx run utilities:');
|
|
||||||
expect(buildOutput).toContain(':utilities:classes');
|
expect(buildOutput).toContain(':utilities:classes');
|
||||||
|
|
||||||
checkFilesExist(
|
checkFilesExist(
|
||||||
@ -83,11 +81,8 @@ dependencies {
|
|||||||
|
|
||||||
let buildOutput = runCLI('build app2', { verbose: true });
|
let buildOutput = runCLI('build app2', { verbose: true });
|
||||||
// app2 depends on app
|
// app2 depends on app
|
||||||
expect(buildOutput).toContain('nx run app:');
|
|
||||||
expect(buildOutput).toContain(':app:classes');
|
expect(buildOutput).toContain(':app:classes');
|
||||||
expect(buildOutput).toContain('nx run list:');
|
|
||||||
expect(buildOutput).toContain(':list:classes');
|
expect(buildOutput).toContain(':list:classes');
|
||||||
expect(buildOutput).toContain('nx run utilities:');
|
|
||||||
expect(buildOutput).toContain(':utilities:classes');
|
expect(buildOutput).toContain(':utilities:classes');
|
||||||
|
|
||||||
checkFilesExist(`app2/build/libs/app2.jar`);
|
checkFilesExist(`app2/build/libs/app2.jar`);
|
||||||
@ -96,7 +91,7 @@ dependencies {
|
|||||||
it('should run atomized test target', () => {
|
it('should run atomized test target', () => {
|
||||||
updateJson('nx.json', (json) => {
|
updateJson('nx.json', (json) => {
|
||||||
json.plugins.find((p) => p.plugin === '@nx/gradle').options[
|
json.plugins.find((p) => p.plugin === '@nx/gradle').options[
|
||||||
'ciTargetName'
|
'ciTestTargetName'
|
||||||
] = 'test-ci';
|
] = 'test-ci';
|
||||||
return json;
|
return json;
|
||||||
});
|
});
|
||||||
|
|||||||
3
packages/gradle/batch-runner/.gitignore
vendored
Normal file
3
packages/gradle/batch-runner/.gitignore
vendored
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
# Ignore Gradle project-specific cache directory
|
||||||
|
bin
|
||||||
|
build
|
||||||
33
packages/gradle/batch-runner/build.gradle.kts
Normal file
33
packages/gradle/batch-runner/build.gradle.kts
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
group = "dev.nx.gradle"
|
||||||
|
|
||||||
|
plugins {
|
||||||
|
// Apply the org.jetbrains.kotlin.jvm Plugin to add support for Kotlin.
|
||||||
|
id("org.jetbrains.kotlin.jvm") version "2.1.10"
|
||||||
|
// Apply the application plugin to add support for building a CLI application in Java.
|
||||||
|
application
|
||||||
|
id("com.github.johnrengelman.shadow") version "8.1.1"
|
||||||
|
id("com.ncorti.ktfmt.gradle") version "+"
|
||||||
|
id("dev.nx.gradle.project-graph") version "0.1.0"
|
||||||
|
}
|
||||||
|
|
||||||
|
repositories {
|
||||||
|
// Use Maven Central for resolving dependencies.
|
||||||
|
mavenCentral()
|
||||||
|
// need for gradle-tooling-api
|
||||||
|
maven { url = uri("https://repo.gradle.org/gradle/libs-releases/") }
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
val toolingApiVersion = "8.13" // Match the Gradle version you're working with
|
||||||
|
|
||||||
|
implementation("org.gradle:gradle-tooling-api:$toolingApiVersion")
|
||||||
|
runtimeOnly("org.slf4j:slf4j-simple:1.7.10")
|
||||||
|
implementation("com.google.code.gson:gson:2.10.1")
|
||||||
|
}
|
||||||
|
|
||||||
|
application {
|
||||||
|
// Define the main class for the application.
|
||||||
|
mainClass.set("dev.nx.gradle.NxBatchRunnerKt")
|
||||||
|
}
|
||||||
|
|
||||||
|
kotlin { jvmToolchain { languageVersion.set(JavaLanguageVersion.of(17)) } }
|
||||||
33
packages/gradle/batch-runner/project.json
Normal file
33
packages/gradle/batch-runner/project.json
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
{
|
||||||
|
"name": "gradle-batch-runner",
|
||||||
|
"$schema": "node_modules/nx/schemas/project-schema.json",
|
||||||
|
"projectRoot": "packages/gradle/batch-runner",
|
||||||
|
"sourceRoot": "packages/gradle/batch-runner/src",
|
||||||
|
"targets": {
|
||||||
|
"assemble": {
|
||||||
|
"command": "./gradlew :batch-runner:assemble",
|
||||||
|
"inputs": [
|
||||||
|
"{projectRoot}/src/**",
|
||||||
|
"{projectRoot}/build.gradle.kts",
|
||||||
|
"{projectRoot}/settings.gradle.kts"
|
||||||
|
],
|
||||||
|
"outputs": ["{projectRoot}/build"],
|
||||||
|
"cache": true
|
||||||
|
},
|
||||||
|
"test": {
|
||||||
|
"command": "./gradlew :batch-runner:test",
|
||||||
|
"options": {
|
||||||
|
"args": []
|
||||||
|
},
|
||||||
|
"cache": true
|
||||||
|
},
|
||||||
|
"lint": {
|
||||||
|
"command": "./gradlew :batch-runner:ktfmtCheck",
|
||||||
|
"cache": true
|
||||||
|
},
|
||||||
|
"format": {
|
||||||
|
"command": "./gradlew :batch-runner:ktfmtFormat",
|
||||||
|
"cache": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
17
packages/gradle/batch-runner/settings.gradle.kts
Normal file
17
packages/gradle/batch-runner/settings.gradle.kts
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
/*
|
||||||
|
* This file was generated by the Gradle 'init' task.
|
||||||
|
*
|
||||||
|
* The settings file is used to specify which projects to include in your build.
|
||||||
|
* For more detailed information on multi-project builds, please refer to https://docs.gradle.org/8.5/userguide/building_swift_projects.html in the Gradle documentation.
|
||||||
|
* This project uses @Incubating APIs which are subject to change.
|
||||||
|
*/
|
||||||
|
|
||||||
|
pluginManagement {
|
||||||
|
repositories {
|
||||||
|
mavenLocal()
|
||||||
|
mavenCentral()
|
||||||
|
gradlePluginPortal()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
rootProject.name = "batch-runner"
|
||||||
@ -0,0 +1,51 @@
|
|||||||
|
package dev.nx.gradle
|
||||||
|
|
||||||
|
import com.google.gson.Gson
|
||||||
|
import dev.nx.gradle.cli.configureLogger
|
||||||
|
import dev.nx.gradle.cli.parseArgs
|
||||||
|
import dev.nx.gradle.runner.runTasksInParallel
|
||||||
|
import dev.nx.gradle.util.logger
|
||||||
|
import java.io.File
|
||||||
|
import kotlin.system.exitProcess
|
||||||
|
import org.gradle.tooling.GradleConnector
|
||||||
|
import org.gradle.tooling.ProjectConnection
|
||||||
|
|
||||||
|
fun main(args: Array<String>) {
|
||||||
|
val options = parseArgs(args)
|
||||||
|
configureLogger(options.quiet)
|
||||||
|
|
||||||
|
if (options.workspaceRoot.isBlank()) {
|
||||||
|
logger.severe("❌ Missing required arguments --workspaceRoot")
|
||||||
|
exitProcess(1)
|
||||||
|
}
|
||||||
|
if (options.tasks.isEmpty()) {
|
||||||
|
logger.severe("❌ Missing required arguments --tasks")
|
||||||
|
exitProcess(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
var connection: ProjectConnection? = null
|
||||||
|
|
||||||
|
try {
|
||||||
|
connection =
|
||||||
|
GradleConnector.newConnector().forProjectDirectory(File(options.workspaceRoot)).connect()
|
||||||
|
|
||||||
|
val results = runTasksInParallel(connection, options.tasks, options.args)
|
||||||
|
|
||||||
|
val reportJson = Gson().toJson(results)
|
||||||
|
println(reportJson)
|
||||||
|
|
||||||
|
val summary = results.values.groupBy { it.success }
|
||||||
|
logger.info(
|
||||||
|
"📊 Summary: ✅ ${summary[true]?.size ?: 0} succeeded, ❌ ${summary[false]?.size ?: 0} failed")
|
||||||
|
} catch (e: Exception) {
|
||||||
|
logger.severe("💥 Failed to run tasks: ${e.message}")
|
||||||
|
exitProcess(1)
|
||||||
|
} finally {
|
||||||
|
try {
|
||||||
|
connection?.close()
|
||||||
|
logger.info("✅ Gradle connection closed.")
|
||||||
|
} catch (e: Exception) {
|
||||||
|
logger.warning("⚠️ Failed to close Gradle connection cleanly: ${e.message}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,47 @@
|
|||||||
|
package dev.nx.gradle.cli
|
||||||
|
|
||||||
|
import com.google.gson.Gson
|
||||||
|
import com.google.gson.reflect.TypeToken
|
||||||
|
import dev.nx.gradle.data.GradleTask
|
||||||
|
import dev.nx.gradle.data.NxBatchOptions
|
||||||
|
import dev.nx.gradle.util.logger
|
||||||
|
|
||||||
|
fun parseArgs(args: Array<String>): NxBatchOptions {
|
||||||
|
val argMap = mutableMapOf<String, String>()
|
||||||
|
|
||||||
|
args.forEach {
|
||||||
|
when {
|
||||||
|
it.startsWith("--") && it.contains("=") -> {
|
||||||
|
val (key, value) = it.split("=", limit = 2)
|
||||||
|
argMap[key] = value
|
||||||
|
}
|
||||||
|
it.startsWith("--") -> {
|
||||||
|
argMap[it] = "true"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val gson = Gson()
|
||||||
|
val tasksJson = argMap["--tasks"]
|
||||||
|
val tasksMap: Map<String, GradleTask> =
|
||||||
|
if (tasksJson != null) {
|
||||||
|
val taskType = object : TypeToken<Map<String, GradleTask>>() {}.type
|
||||||
|
gson.fromJson(tasksJson, taskType)
|
||||||
|
} else emptyMap()
|
||||||
|
|
||||||
|
return NxBatchOptions(
|
||||||
|
workspaceRoot = argMap["--workspaceRoot"] ?: "",
|
||||||
|
tasks = tasksMap,
|
||||||
|
args = argMap["--args"] ?: "",
|
||||||
|
quiet = argMap["--quiet"]?.toBoolean() ?: false)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun configureLogger(quiet: Boolean) {
|
||||||
|
if (quiet) {
|
||||||
|
logger.setLevel(java.util.logging.Level.OFF)
|
||||||
|
logger.useParentHandlers = false
|
||||||
|
logger.handlers.forEach { it.level = java.util.logging.Level.OFF }
|
||||||
|
} else {
|
||||||
|
logger.setLevel(java.util.logging.Level.INFO)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,3 @@
|
|||||||
|
package dev.nx.gradle.data
|
||||||
|
|
||||||
|
data class GradleTask(val taskName: String, val testClassName: String? = null)
|
||||||
@ -0,0 +1,8 @@
|
|||||||
|
package dev.nx.gradle.data
|
||||||
|
|
||||||
|
data class NxBatchOptions(
|
||||||
|
val workspaceRoot: String,
|
||||||
|
val tasks: Map<String, GradleTask>,
|
||||||
|
val args: String,
|
||||||
|
val quiet: Boolean
|
||||||
|
)
|
||||||
@ -0,0 +1,8 @@
|
|||||||
|
package dev.nx.gradle.data
|
||||||
|
|
||||||
|
data class TaskResult(
|
||||||
|
val success: Boolean,
|
||||||
|
val startTime: Long,
|
||||||
|
val endTime: Long,
|
||||||
|
var terminalOutput: String
|
||||||
|
)
|
||||||
@ -0,0 +1,53 @@
|
|||||||
|
package dev.nx.gradle.runner
|
||||||
|
|
||||||
|
import dev.nx.gradle.data.GradleTask
|
||||||
|
import dev.nx.gradle.data.TaskResult
|
||||||
|
import dev.nx.gradle.util.logger
|
||||||
|
import kotlin.math.max
|
||||||
|
import kotlin.math.min
|
||||||
|
import org.gradle.tooling.events.ProgressEvent
|
||||||
|
import org.gradle.tooling.events.task.TaskFailureResult
|
||||||
|
import org.gradle.tooling.events.task.TaskFinishEvent
|
||||||
|
import org.gradle.tooling.events.task.TaskStartEvent
|
||||||
|
import org.gradle.tooling.events.task.TaskSuccessResult
|
||||||
|
|
||||||
|
fun buildListener(
|
||||||
|
tasks: Map<String, GradleTask>,
|
||||||
|
taskStartTimes: MutableMap<String, Long>,
|
||||||
|
taskResults: MutableMap<String, TaskResult>
|
||||||
|
): (ProgressEvent) -> Unit = { event ->
|
||||||
|
when (event) {
|
||||||
|
is TaskStartEvent -> {
|
||||||
|
tasks.entries
|
||||||
|
.find { it.value.taskName == event.descriptor.taskPath }
|
||||||
|
?.key
|
||||||
|
?.let { nxTaskId ->
|
||||||
|
taskStartTimes[nxTaskId] = min(System.currentTimeMillis(), event.eventTime)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
is TaskFinishEvent -> {
|
||||||
|
val taskPath = event.descriptor.taskPath
|
||||||
|
val success =
|
||||||
|
when (event.result) {
|
||||||
|
is TaskSuccessResult -> {
|
||||||
|
logger.info("✅ Task finished successfully: $taskPath")
|
||||||
|
true
|
||||||
|
}
|
||||||
|
is TaskFailureResult -> {
|
||||||
|
logger.warning("❌ Task failed: $taskPath")
|
||||||
|
false
|
||||||
|
}
|
||||||
|
else -> true
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks.entries
|
||||||
|
.find { it.value.taskName == taskPath }
|
||||||
|
?.key
|
||||||
|
?.let { nxTaskId ->
|
||||||
|
val endTime = max(System.currentTimeMillis(), event.eventTime)
|
||||||
|
val startTime = taskStartTimes[nxTaskId] ?: event.result.startTime
|
||||||
|
taskResults[nxTaskId] = TaskResult(success, startTime, endTime, "")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,175 @@
|
|||||||
|
package dev.nx.gradle.runner
|
||||||
|
|
||||||
|
import dev.nx.gradle.data.GradleTask
|
||||||
|
import dev.nx.gradle.data.TaskResult
|
||||||
|
import dev.nx.gradle.runner.OutputProcessor.buildTerminalOutput
|
||||||
|
import dev.nx.gradle.runner.OutputProcessor.splitOutputPerTask
|
||||||
|
import dev.nx.gradle.util.logger
|
||||||
|
import java.io.ByteArrayOutputStream
|
||||||
|
import org.gradle.tooling.ProjectConnection
|
||||||
|
import org.gradle.tooling.events.OperationType
|
||||||
|
|
||||||
|
fun runTasksInParallel(
|
||||||
|
connection: ProjectConnection,
|
||||||
|
tasks: Map<String, GradleTask>,
|
||||||
|
additionalArgs: String,
|
||||||
|
): Map<String, TaskResult> {
|
||||||
|
logger.info("▶️ Running all tasks in a single Gradle run: ${tasks.keys.joinToString(", ")}")
|
||||||
|
|
||||||
|
val (testClassTasks, buildTasks) = tasks.entries.partition { it.value.testClassName != null }
|
||||||
|
|
||||||
|
logger.info("🧪 Test launcher tasks: ${testClassTasks.joinToString(", ") { it.key }}")
|
||||||
|
logger.info("🛠️ Build launcher tasks: ${buildTasks.joinToString(", ") { it.key }}")
|
||||||
|
|
||||||
|
val allResults = mutableMapOf<String, TaskResult>()
|
||||||
|
|
||||||
|
val outputStream = ByteArrayOutputStream()
|
||||||
|
val errorStream = ByteArrayOutputStream()
|
||||||
|
|
||||||
|
val args = buildList {
|
||||||
|
addAll(listOf("--info", "--continue", "--parallel", "--build-cache"))
|
||||||
|
addAll(additionalArgs.split(" ").filter { it.isNotBlank() })
|
||||||
|
}
|
||||||
|
|
||||||
|
val taskNames = tasks.values.map { it.taskName }.distinct()
|
||||||
|
|
||||||
|
if (buildTasks.isNotEmpty()) {
|
||||||
|
allResults.putAll(
|
||||||
|
runBuildLauncher(
|
||||||
|
connection,
|
||||||
|
buildTasks.associate { it.key to it.value },
|
||||||
|
taskNames,
|
||||||
|
args,
|
||||||
|
outputStream,
|
||||||
|
errorStream))
|
||||||
|
}
|
||||||
|
|
||||||
|
if (testClassTasks.isNotEmpty()) {
|
||||||
|
allResults.putAll(
|
||||||
|
runTestLauncher(
|
||||||
|
connection,
|
||||||
|
testClassTasks.associate { it.key to it.value },
|
||||||
|
taskNames,
|
||||||
|
args,
|
||||||
|
outputStream,
|
||||||
|
errorStream))
|
||||||
|
}
|
||||||
|
|
||||||
|
return allResults
|
||||||
|
}
|
||||||
|
|
||||||
|
fun runBuildLauncher(
|
||||||
|
connection: ProjectConnection,
|
||||||
|
tasks: Map<String, GradleTask>,
|
||||||
|
taskNames: List<String>,
|
||||||
|
args: List<String>,
|
||||||
|
outputStream: ByteArrayOutputStream,
|
||||||
|
errorStream: ByteArrayOutputStream
|
||||||
|
): Map<String, TaskResult> {
|
||||||
|
val taskStartTimes = mutableMapOf<String, Long>()
|
||||||
|
val taskResults = mutableMapOf<String, TaskResult>()
|
||||||
|
|
||||||
|
var globalOutput: String
|
||||||
|
|
||||||
|
try {
|
||||||
|
connection
|
||||||
|
.newBuild()
|
||||||
|
.apply {
|
||||||
|
forTasks(*taskNames.toTypedArray())
|
||||||
|
withArguments(*args.toTypedArray())
|
||||||
|
setStandardOutput(outputStream)
|
||||||
|
setStandardError(errorStream)
|
||||||
|
addProgressListener(buildListener(tasks, taskStartTimes, taskResults), OperationType.TASK)
|
||||||
|
}
|
||||||
|
.run()
|
||||||
|
globalOutput = buildTerminalOutput(outputStream, errorStream)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
globalOutput =
|
||||||
|
buildTerminalOutput(outputStream, errorStream) + "\nException occurred: ${e.message}"
|
||||||
|
logger.warning("\ud83d\udca5 Gradle run failed: ${e.message}")
|
||||||
|
} finally {
|
||||||
|
outputStream.close()
|
||||||
|
errorStream.close()
|
||||||
|
}
|
||||||
|
|
||||||
|
val perTaskOutput = splitOutputPerTask(globalOutput)
|
||||||
|
tasks.forEach { (taskId, taskConfig) ->
|
||||||
|
val taskOutput = perTaskOutput[taskConfig.taskName] ?: globalOutput
|
||||||
|
taskResults[taskId]?.let { taskResults[taskId] = it.copy(terminalOutput = taskOutput) }
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info("\u2705 Finished build tasks")
|
||||||
|
return taskResults
|
||||||
|
}
|
||||||
|
|
||||||
|
fun runTestLauncher(
|
||||||
|
connection: ProjectConnection,
|
||||||
|
tasks: Map<String, GradleTask>,
|
||||||
|
taskNames: List<String>,
|
||||||
|
args: List<String>,
|
||||||
|
outputStream: ByteArrayOutputStream,
|
||||||
|
errorStream: ByteArrayOutputStream
|
||||||
|
): Map<String, TaskResult> {
|
||||||
|
val taskStartTimes = mutableMapOf<String, Long>()
|
||||||
|
val taskResults = mutableMapOf<String, TaskResult>()
|
||||||
|
val testTaskStatus = mutableMapOf<String, Boolean>()
|
||||||
|
val testStartTimes = mutableMapOf<String, Long>()
|
||||||
|
val testEndTimes = mutableMapOf<String, Long>()
|
||||||
|
|
||||||
|
tasks.forEach { (nxTaskId, taskConfig) ->
|
||||||
|
if (taskConfig.testClassName != null) {
|
||||||
|
testTaskStatus[nxTaskId] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val globalStart = System.currentTimeMillis()
|
||||||
|
var globalOutput: String
|
||||||
|
|
||||||
|
try {
|
||||||
|
connection
|
||||||
|
.newTestLauncher()
|
||||||
|
.apply {
|
||||||
|
forTasks(*taskNames.toTypedArray())
|
||||||
|
tasks.values.mapNotNull { it.testClassName }.forEach { withJvmTestClasses(it) }
|
||||||
|
withArguments(*args.toTypedArray())
|
||||||
|
setStandardOutput(outputStream)
|
||||||
|
setStandardError(errorStream)
|
||||||
|
addProgressListener(
|
||||||
|
testListener(
|
||||||
|
tasks, taskStartTimes, taskResults, testTaskStatus, testStartTimes, testEndTimes),
|
||||||
|
OperationType.TEST)
|
||||||
|
}
|
||||||
|
.run()
|
||||||
|
globalOutput = buildTerminalOutput(outputStream, errorStream)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
globalOutput =
|
||||||
|
buildTerminalOutput(outputStream, errorStream) + "\nException occurred: ${e.message}"
|
||||||
|
logger.warning("\ud83d\udca5 Gradle test run failed: ${e.message}")
|
||||||
|
} finally {
|
||||||
|
outputStream.close()
|
||||||
|
errorStream.close()
|
||||||
|
}
|
||||||
|
|
||||||
|
val globalEnd = System.currentTimeMillis()
|
||||||
|
|
||||||
|
tasks.forEach { (nxTaskId, taskConfig) ->
|
||||||
|
if (taskConfig.testClassName != null) {
|
||||||
|
val success = testTaskStatus[nxTaskId] ?: false
|
||||||
|
val startTime = testStartTimes[nxTaskId] ?: globalStart
|
||||||
|
val endTime = testEndTimes[nxTaskId] ?: globalEnd
|
||||||
|
|
||||||
|
if (!taskResults.containsKey(nxTaskId)) {
|
||||||
|
taskResults[nxTaskId] = TaskResult(success, startTime, endTime, "")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val perTaskOutput = splitOutputPerTask(globalOutput)
|
||||||
|
tasks.forEach { (taskId, taskConfig) ->
|
||||||
|
val taskOutput = perTaskOutput[taskConfig.taskName] ?: globalOutput
|
||||||
|
taskResults[taskId]?.let { taskResults[taskId] = it.copy(terminalOutput = taskOutput) }
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info("\u2705 Finished test tasks")
|
||||||
|
return taskResults
|
||||||
|
}
|
||||||
@ -0,0 +1,33 @@
|
|||||||
|
package dev.nx.gradle.runner
|
||||||
|
|
||||||
|
import java.io.ByteArrayOutputStream
|
||||||
|
|
||||||
|
object OutputProcessor {
|
||||||
|
fun buildTerminalOutput(stdOut: ByteArrayOutputStream, stdErr: ByteArrayOutputStream): String {
|
||||||
|
val output = stdOut.toString("UTF-8")
|
||||||
|
val errorOutput = stdErr.toString("UTF-8")
|
||||||
|
return buildString {
|
||||||
|
if (output.isNotBlank()) append(output).append("\n")
|
||||||
|
if (errorOutput.isNotBlank()) append(errorOutput)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun splitOutputPerTask(globalOutput: String): Map<String, String> {
|
||||||
|
val unescapedOutput = globalOutput.replace("\\u003e", ">").replace("\\n", "\n")
|
||||||
|
val taskHeaderRegex = Regex("(?=> Task (:[^\\s]+))")
|
||||||
|
val sections = unescapedOutput.split(taskHeaderRegex)
|
||||||
|
val taskOutputMap = mutableMapOf<String, String>()
|
||||||
|
|
||||||
|
for (section in sections) {
|
||||||
|
val lines = section.trim().lines()
|
||||||
|
if (lines.isEmpty()) continue
|
||||||
|
val header = lines.firstOrNull { it.startsWith("> Task ") }
|
||||||
|
if (header != null) {
|
||||||
|
val taskMatch = Regex("> Task (:[^\\s]+)").find(header)
|
||||||
|
val taskName = taskMatch?.groupValues?.get(1) ?: continue
|
||||||
|
taskOutputMap[taskName] = section.trim()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return taskOutputMap
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,59 @@
|
|||||||
|
package dev.nx.gradle.runner
|
||||||
|
|
||||||
|
import dev.nx.gradle.data.GradleTask
|
||||||
|
import dev.nx.gradle.data.TaskResult
|
||||||
|
import dev.nx.gradle.util.logger
|
||||||
|
import kotlin.math.max
|
||||||
|
import kotlin.math.min
|
||||||
|
import org.gradle.tooling.events.ProgressEvent
|
||||||
|
import org.gradle.tooling.events.task.TaskFinishEvent
|
||||||
|
import org.gradle.tooling.events.task.TaskStartEvent
|
||||||
|
import org.gradle.tooling.events.test.*
|
||||||
|
|
||||||
|
fun testListener(
|
||||||
|
tasks: Map<String, GradleTask>,
|
||||||
|
taskStartTimes: MutableMap<String, Long>,
|
||||||
|
taskResults: MutableMap<String, TaskResult>,
|
||||||
|
testTaskStatus: MutableMap<String, Boolean>,
|
||||||
|
testStartTimes: MutableMap<String, Long>,
|
||||||
|
testEndTimes: MutableMap<String, Long>
|
||||||
|
): (ProgressEvent) -> Unit = { event ->
|
||||||
|
when (event) {
|
||||||
|
is TaskStartEvent,
|
||||||
|
is TaskFinishEvent -> buildListener(tasks, taskStartTimes, taskResults)(event)
|
||||||
|
is TestStartEvent -> {
|
||||||
|
(event.descriptor as? JvmTestOperationDescriptor)?.className?.let { className ->
|
||||||
|
tasks.entries
|
||||||
|
.find { entry -> entry.value.testClassName?.let { className.endsWith(it) } ?: false }
|
||||||
|
?.key
|
||||||
|
?.let { nxTaskId ->
|
||||||
|
testStartTimes.compute(nxTaskId) { _, old ->
|
||||||
|
min(old ?: event.eventTime, event.eventTime)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
is TestFinishEvent -> {
|
||||||
|
(event.descriptor as? JvmTestOperationDescriptor)?.className?.let { className ->
|
||||||
|
tasks.entries
|
||||||
|
.find { entry -> entry.value.testClassName?.let { className.endsWith(it) } ?: false }
|
||||||
|
?.key
|
||||||
|
?.let { nxTaskId ->
|
||||||
|
testEndTimes.compute(nxTaskId) { _, old ->
|
||||||
|
max(old ?: event.eventTime, event.eventTime)
|
||||||
|
}
|
||||||
|
when (event.result) {
|
||||||
|
is TestSuccessResult -> logger.info("\u2705 Test passed: $nxTaskId $className")
|
||||||
|
is TestFailureResult -> {
|
||||||
|
testTaskStatus[nxTaskId] = false
|
||||||
|
logger.warning("\u274C Test failed: $nxTaskId $className")
|
||||||
|
}
|
||||||
|
is TestSkippedResult ->
|
||||||
|
logger.warning("\u26A0\uFE0F Test skipped: $nxTaskId $className")
|
||||||
|
else -> logger.warning("\u26A0\uFE0F Unknown test result: $nxTaskId $className")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,5 @@
|
|||||||
|
package dev.nx.gradle.util
|
||||||
|
|
||||||
|
import java.util.logging.Logger
|
||||||
|
|
||||||
|
val logger: Logger = Logger.getLogger("NxBatchRunner")
|
||||||
@ -1,3 +1,10 @@
|
|||||||
{
|
{
|
||||||
"executors": {}
|
"executors": {
|
||||||
|
"gradle": {
|
||||||
|
"batchImplementation": "./src/executors/gradle/gradle-batch.impl",
|
||||||
|
"implementation": "./src/executors/gradle/gradle.impl",
|
||||||
|
"schema": "./src/executors/gradle/schema.json",
|
||||||
|
"description": "The Gradlew executor is used to run Gradle tasks."
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,14 +3,14 @@ plugins {
|
|||||||
`maven-publish`
|
`maven-publish`
|
||||||
signing
|
signing
|
||||||
id("com.ncorti.ktfmt.gradle") version "+"
|
id("com.ncorti.ktfmt.gradle") version "+"
|
||||||
id("dev.nx.gradle.project-graph") version "0.0.2"
|
id("dev.nx.gradle.project-graph") version "0.1.0"
|
||||||
id("org.jetbrains.kotlin.jvm") version "2.1.10"
|
id("org.jetbrains.kotlin.jvm") version "2.1.10"
|
||||||
id("com.gradle.plugin-publish") version "1.2.1"
|
id("com.gradle.plugin-publish") version "1.2.1"
|
||||||
}
|
}
|
||||||
|
|
||||||
group = "dev.nx.gradle"
|
group = "dev.nx.gradle"
|
||||||
|
|
||||||
version = "0.0.2"
|
version = "0.1.0"
|
||||||
|
|
||||||
repositories { mavenCentral() }
|
repositories { mavenCentral() }
|
||||||
|
|
||||||
@ -118,3 +118,5 @@ signing {
|
|||||||
}
|
}
|
||||||
|
|
||||||
tasks.test { useJUnitPlatform() }
|
tasks.test { useJUnitPlatform() }
|
||||||
|
|
||||||
|
java { toolchain.languageVersion.set(JavaLanguageVersion.of(17)) }
|
||||||
|
|||||||
@ -1,90 +1,86 @@
|
|||||||
package dev.nx.gradle.utils
|
package dev.nx.gradle.utils
|
||||||
|
|
||||||
import dev.nx.gradle.data.*
|
import dev.nx.gradle.data.NxTargets
|
||||||
|
import dev.nx.gradle.data.TargetGroups
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import org.gradle.api.Task
|
import org.gradle.api.Task
|
||||||
import org.gradle.api.file.FileCollection
|
import org.gradle.api.file.FileCollection
|
||||||
|
|
||||||
const val testCiTargetGroup = "verification"
|
const val testCiTargetGroup = "verification"
|
||||||
|
|
||||||
/**
|
private val testFileNameRegex =
|
||||||
* Add atomized ci test targets Going to loop through each test files and create a target for each
|
Regex("^(?!(abstract|fake)).*?(Test)(s)?\\d*", RegexOption.IGNORE_CASE)
|
||||||
* It is going to modify targets and targetGroups in place
|
|
||||||
*/
|
private val classDeclarationRegex = Regex("""class\s+([A-Za-z_][A-Za-z0-9_]*)""")
|
||||||
|
|
||||||
fun addTestCiTargets(
|
fun addTestCiTargets(
|
||||||
testFiles: FileCollection,
|
testFiles: FileCollection,
|
||||||
projectBuildPath: String,
|
projectBuildPath: String,
|
||||||
testTask: Task,
|
testTask: Task,
|
||||||
|
testTargetName: String,
|
||||||
targets: NxTargets,
|
targets: NxTargets,
|
||||||
targetGroups: TargetGroups,
|
targetGroups: TargetGroups,
|
||||||
projectRoot: String,
|
projectRoot: String,
|
||||||
workspaceRoot: String,
|
workspaceRoot: String,
|
||||||
ciTargetName: String
|
ciTestTargetName: String
|
||||||
) {
|
) {
|
||||||
ensureTargetGroupExists(targetGroups, testCiTargetGroup)
|
ensureTargetGroupExists(targetGroups, testCiTargetGroup)
|
||||||
|
|
||||||
val gradlewCommand = getGradlewCommand()
|
|
||||||
val ciDependsOn = mutableListOf<Map<String, String>>()
|
val ciDependsOn = mutableListOf<Map<String, String>>()
|
||||||
|
|
||||||
val filteredTestFiles = testFiles.filter { isTestFile(it, workspaceRoot) }
|
testFiles
|
||||||
|
.filter { isTestFile(it, workspaceRoot) }
|
||||||
|
.forEach { testFile ->
|
||||||
|
val className = getTestClassNameIfAnnotated(testFile) ?: return@forEach
|
||||||
|
|
||||||
filteredTestFiles.forEach { testFile ->
|
val targetName = "$ciTestTargetName--$className"
|
||||||
val className = getTestClassNameIfAnnotated(testFile) ?: return@forEach
|
targets[targetName] =
|
||||||
|
buildTestCiTarget(
|
||||||
|
projectBuildPath, className, testFile, testTask, projectRoot, workspaceRoot)
|
||||||
|
targetGroups[testCiTargetGroup]?.add(targetName)
|
||||||
|
|
||||||
val testCiTarget =
|
ciDependsOn.add(mapOf("target" to targetName, "projects" to "self", "params" to "forward"))
|
||||||
buildTestCiTarget(
|
}
|
||||||
gradlewCommand = gradlewCommand,
|
|
||||||
projectBuildPath = projectBuildPath,
|
|
||||||
testClassName = className,
|
|
||||||
testFile = testFile,
|
|
||||||
testTask = testTask,
|
|
||||||
projectRoot = projectRoot,
|
|
||||||
workspaceRoot = workspaceRoot)
|
|
||||||
|
|
||||||
val targetName = "$ciTargetName--$className"
|
testTask.logger.info("${testTask.path} generated CI targets: ${ciDependsOn.map { it["target"] }}")
|
||||||
targets[targetName] = testCiTarget
|
|
||||||
targetGroups[testCiTargetGroup]?.add(targetName)
|
|
||||||
|
|
||||||
ciDependsOn.add(mapOf("target" to targetName, "projects" to "self", "params" to "forward"))
|
|
||||||
}
|
|
||||||
|
|
||||||
testTask.logger.info("$testTask ci tasks: $ciDependsOn")
|
|
||||||
|
|
||||||
if (ciDependsOn.isNotEmpty()) {
|
if (ciDependsOn.isNotEmpty()) {
|
||||||
ensureParentCiTarget(
|
ensureParentCiTarget(
|
||||||
targets = targets,
|
targets,
|
||||||
targetGroups = targetGroups,
|
targetGroups,
|
||||||
ciTargetName = ciTargetName,
|
ciTestTargetName,
|
||||||
projectBuildPath = projectBuildPath,
|
projectBuildPath,
|
||||||
dependsOn = ciDependsOn)
|
testTask,
|
||||||
|
testTargetName,
|
||||||
|
ciDependsOn)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getTestClassNameIfAnnotated(file: File): String? {
|
private fun getTestClassNameIfAnnotated(file: File): String? {
|
||||||
if (!file.exists()) return null
|
return file
|
||||||
|
.takeIf { it.exists() }
|
||||||
val content = file.readText()
|
?.readText()
|
||||||
if (!content.contains("@Test")) return null
|
?.takeIf { it.contains("@Test") || it.contains("@TestTemplate") }
|
||||||
|
?.let { content ->
|
||||||
val classRegex = Regex("""class\s+([A-Za-z_][A-Za-z0-9_]*)""")
|
val className = classDeclarationRegex.find(content)?.groupValues?.getOrNull(1)
|
||||||
val match = classRegex.find(content)
|
return if (className != null && !className.startsWith("Fake")) {
|
||||||
return match?.groupValues?.get(1)
|
className
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun ensureTargetGroupExists(targetGroups: TargetGroups, group: String) {
|
fun ensureTargetGroupExists(targetGroups: TargetGroups, group: String) {
|
||||||
if (!targetGroups.containsKey(group)) {
|
targetGroups.getOrPut(group) { mutableListOf() }
|
||||||
targetGroups[group] = mutableListOf()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun isTestFile(file: File, workspaceRoot: String): Boolean {
|
private fun isTestFile(file: File, workspaceRoot: String): Boolean {
|
||||||
val fileName = file.name.substringBefore(".")
|
val fileName = file.name.substringBefore(".")
|
||||||
val regex = "^(?!abstract).*?(Test)(s)?\\d*".toRegex(RegexOption.IGNORE_CASE)
|
return file.path.startsWith(workspaceRoot) && testFileNameRegex.matches(fileName)
|
||||||
return file.path.startsWith(workspaceRoot) && regex.matches(fileName)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun buildTestCiTarget(
|
private fun buildTestCiTarget(
|
||||||
gradlewCommand: String,
|
|
||||||
projectBuildPath: String,
|
projectBuildPath: String,
|
||||||
testClassName: String,
|
testClassName: String,
|
||||||
testFile: File,
|
testFile: File,
|
||||||
@ -94,7 +90,11 @@ private fun buildTestCiTarget(
|
|||||||
): MutableMap<String, Any?> {
|
): MutableMap<String, Any?> {
|
||||||
val target =
|
val target =
|
||||||
mutableMapOf<String, Any?>(
|
mutableMapOf<String, Any?>(
|
||||||
"command" to "$gradlewCommand ${projectBuildPath}:test --tests $testClassName",
|
"executor" to "@nx/gradle:gradle",
|
||||||
|
"options" to
|
||||||
|
mapOf(
|
||||||
|
"taskName" to "${projectBuildPath}:${testTask.name}",
|
||||||
|
"testClassName" to testClassName),
|
||||||
"metadata" to
|
"metadata" to
|
||||||
getMetadata("Runs Gradle test $testClassName in CI", projectBuildPath, "test"),
|
getMetadata("Runs Gradle test $testClassName in CI", projectBuildPath, "test"),
|
||||||
"cache" to true,
|
"cache" to true,
|
||||||
@ -103,14 +103,14 @@ private fun buildTestCiTarget(
|
|||||||
getDependsOnForTask(testTask, null)
|
getDependsOnForTask(testTask, null)
|
||||||
?.takeIf { it.isNotEmpty() }
|
?.takeIf { it.isNotEmpty() }
|
||||||
?.let {
|
?.let {
|
||||||
testTask.logger.info("$testTask: processed ${it.size} dependsOn")
|
testTask.logger.info("${testTask.path}: found ${it.size} dependsOn entries")
|
||||||
target["dependsOn"] = it
|
target["dependsOn"] = it
|
||||||
}
|
}
|
||||||
|
|
||||||
getOutputsForTask(testTask, projectRoot, workspaceRoot)
|
getOutputsForTask(testTask, projectRoot, workspaceRoot)
|
||||||
?.takeIf { it.isNotEmpty() }
|
?.takeIf { it.isNotEmpty() }
|
||||||
?.let {
|
?.let {
|
||||||
testTask.logger.info("$testTask: processed ${it.size} outputs")
|
testTask.logger.info("${testTask.path}: found ${it.size} outputs entries")
|
||||||
target["outputs"] = it
|
target["outputs"] = it
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -120,24 +120,31 @@ private fun buildTestCiTarget(
|
|||||||
private fun ensureParentCiTarget(
|
private fun ensureParentCiTarget(
|
||||||
targets: NxTargets,
|
targets: NxTargets,
|
||||||
targetGroups: TargetGroups,
|
targetGroups: TargetGroups,
|
||||||
ciTargetName: String,
|
ciTestTargetName: String,
|
||||||
projectBuildPath: String,
|
projectBuildPath: String,
|
||||||
|
testTask: Task,
|
||||||
|
testTargetName: String,
|
||||||
dependsOn: List<Map<String, String>>
|
dependsOn: List<Map<String, String>>
|
||||||
) {
|
) {
|
||||||
val ciTarget =
|
val ciTarget =
|
||||||
targets.getOrPut(ciTargetName) {
|
targets.getOrPut(ciTestTargetName) {
|
||||||
mutableMapOf<String, Any?>().apply {
|
mutableMapOf<String, Any?>(
|
||||||
put("executor", "nx:noop")
|
"executor" to "nx:noop",
|
||||||
put("metadata", getMetadata("Runs Gradle Tests in CI", projectBuildPath, "test", "test"))
|
"metadata" to
|
||||||
put("dependsOn", mutableListOf<Map<String, String>>())
|
getMetadata(
|
||||||
put("cache", true)
|
"Runs Gradle ${testTask.name} in CI",
|
||||||
}
|
projectBuildPath,
|
||||||
|
testTask.name,
|
||||||
|
testTargetName),
|
||||||
|
"dependsOn" to mutableListOf<Any?>(),
|
||||||
|
"cache" to true)
|
||||||
}
|
}
|
||||||
|
|
||||||
val dependsOnList = ciTarget.getOrPut("dependsOn") { mutableListOf<Any?>() } as MutableList<Any?>
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
val dependsOnList = ciTarget["dependsOn"] as? MutableList<Any?> ?: mutableListOf()
|
||||||
dependsOnList.addAll(dependsOn)
|
dependsOnList.addAll(dependsOn)
|
||||||
|
|
||||||
if (targetGroups[testCiTargetGroup]?.contains(ciTargetName) != true) {
|
if (!targetGroups[testCiTargetGroup].orEmpty().contains(ciTestTargetName)) {
|
||||||
targetGroups[testCiTargetGroup]?.add(ciTargetName)
|
targetGroups[testCiTargetGroup]?.add(ciTestTargetName)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -63,41 +63,41 @@ fun processTargetsForProject(
|
|||||||
workspaceRoot: String,
|
workspaceRoot: String,
|
||||||
cwd: String
|
cwd: String
|
||||||
): GradleTargets {
|
): GradleTargets {
|
||||||
val targets: NxTargets = mutableMapOf<String, MutableMap<String, Any?>>()
|
val targets: NxTargets = mutableMapOf()
|
||||||
val targetGroups: TargetGroups = mutableMapOf<String, MutableList<String>>()
|
val targetGroups: TargetGroups = mutableMapOf()
|
||||||
val externalNodes = mutableMapOf<String, ExternalNode>()
|
val externalNodes = mutableMapOf<String, ExternalNode>()
|
||||||
|
|
||||||
val projectRoot = project.projectDir.path
|
val projectRoot = project.projectDir.path
|
||||||
project.logger.info("Using workspace root $workspaceRoot")
|
|
||||||
|
|
||||||
var projectBuildPath: String =
|
|
||||||
project
|
|
||||||
.buildTreePath // get the build path of project e.g. :app, :utils:number-utils, :buildSrc
|
|
||||||
if (projectBuildPath.endsWith(":")) { // root project is ":", manually remove last :
|
|
||||||
projectBuildPath = projectBuildPath.dropLast(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
val logger = project.logger
|
val logger = project.logger
|
||||||
|
|
||||||
logger.info("${Date()} ${project}: process targets")
|
logger.info("Using workspace root: $workspaceRoot")
|
||||||
|
|
||||||
var gradleProject = project.buildTreePath
|
val projectBuildPath = project.buildTreePath.trimEnd(':')
|
||||||
if (!gradleProject.endsWith(":")) {
|
|
||||||
gradleProject += ":"
|
logger.info("${Date()} ${project}: Process targets")
|
||||||
}
|
|
||||||
|
val ciTestTargetName = targetNameOverrides["ciTestTargetName"]
|
||||||
|
val ciIntTestTargetName = targetNameOverrides["ciIntTestTargetName"]
|
||||||
|
val ciCheckTargetName = targetNameOverrides.getOrDefault("ciCheckTargetName", "check-ci")
|
||||||
|
val testTargetName = targetNameOverrides.getOrDefault("testTargetName", "test")
|
||||||
|
val intTestTargetName = targetNameOverrides.getOrDefault("intTestTargetName", "intTest")
|
||||||
|
|
||||||
|
val testTasks = project.getTasksByName("test", false)
|
||||||
|
val intTestTasks = project.getTasksByName("intTest", false)
|
||||||
|
val hasCiTestTarget = ciTestTargetName != null && testTasks.isNotEmpty()
|
||||||
|
val hasCiIntTestTarget = ciIntTestTargetName != null && intTestTasks.isNotEmpty()
|
||||||
|
|
||||||
project.tasks.forEach { task ->
|
project.tasks.forEach { task ->
|
||||||
try {
|
try {
|
||||||
logger.info("${Date()} ${project.name}: Processing $task")
|
val now = Date()
|
||||||
val taskName = targetNameOverrides.getOrDefault(task.name + "TargetName", task.name)
|
logger.info("$now ${project.name}: Processing task ${task.path}")
|
||||||
// add task to target groups
|
|
||||||
val group: String? = task.group
|
val taskName = targetNameOverrides.getOrDefault("${task.name}TargetName", task.name)
|
||||||
if (!group.isNullOrBlank()) {
|
|
||||||
if (targetGroups.contains(group)) {
|
// Group task under its group if available
|
||||||
targetGroups[group]?.add(task.name)
|
task.group
|
||||||
} else {
|
?.takeIf { it.isNotBlank() }
|
||||||
targetGroups[group] = mutableListOf(task.name)
|
?.let { group -> targetGroups.getOrPut(group) { mutableListOf() }.add(taskName) }
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val target =
|
val target =
|
||||||
processTask(
|
processTask(
|
||||||
@ -105,53 +105,63 @@ fun processTargetsForProject(
|
|||||||
projectBuildPath,
|
projectBuildPath,
|
||||||
projectRoot,
|
projectRoot,
|
||||||
workspaceRoot,
|
workspaceRoot,
|
||||||
cwd,
|
|
||||||
externalNodes,
|
externalNodes,
|
||||||
dependencies,
|
dependencies,
|
||||||
targetNameOverrides)
|
targetNameOverrides)
|
||||||
|
|
||||||
targets[taskName] = target
|
targets[taskName] = target
|
||||||
|
|
||||||
val ciTargetName = targetNameOverrides.getOrDefault("ciTargetName", null)
|
if (hasCiTestTarget && task.name.startsWith("compileTest")) {
|
||||||
ciTargetName?.let {
|
addTestCiTargets(
|
||||||
if (task.name.startsWith("compileTest")) {
|
task.inputs.sourceFiles,
|
||||||
val testTask = project.getTasksByName("test", false)
|
projectBuildPath,
|
||||||
if (testTask.isNotEmpty()) {
|
testTasks.first(),
|
||||||
addTestCiTargets(
|
testTargetName,
|
||||||
task.inputs.sourceFiles,
|
targets,
|
||||||
projectBuildPath,
|
targetGroups,
|
||||||
testTask.first(),
|
projectRoot,
|
||||||
targets,
|
workspaceRoot,
|
||||||
targetGroups,
|
ciTestTargetName!!)
|
||||||
projectRoot,
|
|
||||||
workspaceRoot,
|
|
||||||
it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add the `$ciTargetName-check` target when processing the "check" task
|
|
||||||
if (task.name == "check") {
|
|
||||||
val replacedDependencies =
|
|
||||||
(target["dependsOn"] as? List<*>)?.map { dep ->
|
|
||||||
if (dep.toString() == targetNameOverrides.getOrDefault("testTargetName", "test"))
|
|
||||||
ciTargetName
|
|
||||||
else dep.toString()
|
|
||||||
} ?: emptyList()
|
|
||||||
|
|
||||||
// Copy the original target and override "dependsOn"
|
|
||||||
val newTarget = target.toMutableMap()
|
|
||||||
newTarget["dependsOn"] = replacedDependencies
|
|
||||||
|
|
||||||
val ciCheckTargetName = "$ciTargetName-check"
|
|
||||||
targets[ciCheckTargetName] = newTarget
|
|
||||||
|
|
||||||
ensureTargetGroupExists(targetGroups, testCiTargetGroup)
|
|
||||||
targetGroups[testCiTargetGroup]?.add(ciCheckTargetName)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
logger.info("${Date()} ${project.name}: Processed $task")
|
|
||||||
|
if (hasCiIntTestTarget && task.name.startsWith("compileIntTest")) {
|
||||||
|
addTestCiTargets(
|
||||||
|
task.inputs.sourceFiles,
|
||||||
|
projectBuildPath,
|
||||||
|
intTestTasks.first(),
|
||||||
|
intTestTargetName,
|
||||||
|
targets,
|
||||||
|
targetGroups,
|
||||||
|
projectRoot,
|
||||||
|
workspaceRoot,
|
||||||
|
ciIntTestTargetName!!)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (task.name == "check" && (hasCiTestTarget || hasCiIntTestTarget)) {
|
||||||
|
val replacedDependencies =
|
||||||
|
(target["dependsOn"] as? List<*>)?.map { dep ->
|
||||||
|
when (dep.toString()) {
|
||||||
|
testTargetName -> ciTestTargetName ?: dep
|
||||||
|
intTestTargetName -> ciIntTestTargetName ?: dep
|
||||||
|
else -> dep
|
||||||
|
}.toString()
|
||||||
|
} ?: emptyList()
|
||||||
|
|
||||||
|
val newTarget: MutableMap<String, Any?> =
|
||||||
|
mutableMapOf(
|
||||||
|
"dependsOn" to replacedDependencies,
|
||||||
|
"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)
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info("$now ${project.name}: Processed task ${task.path}")
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
logger.info("${task}: process task error $e")
|
logger.error("Error processing task ${task.path}: ${e.message}", e)
|
||||||
logger.debug("Stack trace:", e)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,10 +1,9 @@
|
|||||||
package dev.nx.gradle.utils
|
package dev.nx.gradle.utils
|
||||||
|
|
||||||
import dev.nx.gradle.data.*
|
import dev.nx.gradle.data.Dependency
|
||||||
import org.gradle.api.Named
|
import dev.nx.gradle.data.ExternalDepData
|
||||||
import org.gradle.api.NamedDomainObjectProvider
|
import dev.nx.gradle.data.ExternalNode
|
||||||
import org.gradle.api.Task
|
import org.gradle.api.Task
|
||||||
import org.gradle.api.tasks.TaskProvider
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Process a task and convert it into target Going to populate:
|
* Process a task and convert it into target Going to populate:
|
||||||
@ -13,14 +12,12 @@ import org.gradle.api.tasks.TaskProvider
|
|||||||
* - outputs
|
* - outputs
|
||||||
* - command
|
* - command
|
||||||
* - metadata
|
* - metadata
|
||||||
* - options with cwd and args
|
|
||||||
*/
|
*/
|
||||||
fun processTask(
|
fun processTask(
|
||||||
task: Task,
|
task: Task,
|
||||||
projectBuildPath: String,
|
projectBuildPath: String,
|
||||||
projectRoot: String,
|
projectRoot: String,
|
||||||
workspaceRoot: String,
|
workspaceRoot: String,
|
||||||
cwd: String,
|
|
||||||
externalNodes: MutableMap<String, ExternalNode>,
|
externalNodes: MutableMap<String, ExternalNode>,
|
||||||
dependencies: MutableSet<Dependency>,
|
dependencies: MutableSet<Dependency>,
|
||||||
targetNameOverrides: Map<String, String>
|
targetNameOverrides: Map<String, String>
|
||||||
@ -51,13 +48,14 @@ fun processTask(
|
|||||||
target["dependsOn"] = dependsOn
|
target["dependsOn"] = dependsOn
|
||||||
}
|
}
|
||||||
|
|
||||||
val gradlewCommand = getGradlewCommand()
|
target["executor"] = "@nx/gradle:gradle"
|
||||||
target["command"] = "$gradlewCommand ${projectBuildPath}:${task.name}"
|
|
||||||
|
|
||||||
val metadata = getMetadata(task.description ?: "Run ${task.name}", projectBuildPath, task.name)
|
val metadata =
|
||||||
|
getMetadata(
|
||||||
|
task.description ?: "Run ${projectBuildPath}.${task.name}", projectBuildPath, task.name)
|
||||||
target["metadata"] = metadata
|
target["metadata"] = metadata
|
||||||
|
|
||||||
target["options"] = mapOf("cwd" to cwd)
|
target["options"] = mapOf("taskName" to "${projectBuildPath}:${task.name}")
|
||||||
|
|
||||||
return target
|
return target
|
||||||
}
|
}
|
||||||
@ -190,54 +188,9 @@ fun getDependsOnForTask(
|
|||||||
}
|
}
|
||||||
|
|
||||||
return try {
|
return try {
|
||||||
val dependsOnEntries = task.dependsOn
|
// get depends on using taskDependencies.getDependencies(task) because task.dependsOn has
|
||||||
|
// missing deps
|
||||||
// Prefer task.dependsOn
|
val dependsOn =
|
||||||
if (dependsOnEntries.isNotEmpty()) {
|
|
||||||
val resolvedTasks =
|
|
||||||
dependsOnEntries.flatMap { dep ->
|
|
||||||
when (dep) {
|
|
||||||
is Task -> listOf(dep)
|
|
||||||
|
|
||||||
is TaskProvider<*>,
|
|
||||||
is NamedDomainObjectProvider<*> -> {
|
|
||||||
val providerName = (dep as Named).name
|
|
||||||
val foundTask = task.project.tasks.findByName(providerName)
|
|
||||||
if (foundTask != null) {
|
|
||||||
listOf(foundTask)
|
|
||||||
} else {
|
|
||||||
task.logger.info(
|
|
||||||
"${dep::class.simpleName} '$providerName' did not resolve to a task in project ${task.project.name}")
|
|
||||||
emptyList()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
is String -> {
|
|
||||||
val foundTask = task.project.tasks.findByPath(dep)
|
|
||||||
if (foundTask != null) {
|
|
||||||
listOf(foundTask)
|
|
||||||
} else {
|
|
||||||
task.logger.info(
|
|
||||||
"Task string '$dep' could not be resolved in project ${task.project.name}")
|
|
||||||
emptyList()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
else -> {
|
|
||||||
task.logger.info(
|
|
||||||
"Unhandled dependency type ${dep::class.java} for task ${task.path}")
|
|
||||||
emptyList()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (resolvedTasks.isNotEmpty()) {
|
|
||||||
return mapTasksToNames(resolvedTasks)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fallback: taskDependencies.getDependencies(task)
|
|
||||||
val fallbackDeps =
|
|
||||||
try {
|
try {
|
||||||
task.taskDependencies.getDependencies(null)
|
task.taskDependencies.getDependencies(null)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
@ -246,8 +199,8 @@ fun getDependsOnForTask(
|
|||||||
emptySet<Task>()
|
emptySet<Task>()
|
||||||
}
|
}
|
||||||
|
|
||||||
if (fallbackDeps.isNotEmpty()) {
|
if (dependsOn.isNotEmpty()) {
|
||||||
return mapTasksToNames(fallbackDeps)
|
return mapTasksToNames(dependsOn)
|
||||||
}
|
}
|
||||||
|
|
||||||
null
|
null
|
||||||
@ -266,14 +219,15 @@ fun getDependsOnForTask(
|
|||||||
fun getMetadata(
|
fun getMetadata(
|
||||||
description: String?,
|
description: String?,
|
||||||
projectBuildPath: String,
|
projectBuildPath: String,
|
||||||
taskName: String,
|
helpTaskName: String,
|
||||||
nonAtomizedTarget: String? = null
|
nonAtomizedTarget: String? = null
|
||||||
): Map<String, Any?> {
|
): Map<String, Any?> {
|
||||||
val gradlewCommand = getGradlewCommand()
|
val gradlewCommand = getGradlewCommand()
|
||||||
return mapOf(
|
return mapOf(
|
||||||
"description" to description,
|
"description" to description,
|
||||||
"technologies" to arrayOf("gradle"),
|
"technologies" to arrayOf("gradle"),
|
||||||
"help" to mapOf("command" to "$gradlewCommand help --task ${projectBuildPath}:${taskName}"),
|
"help" to
|
||||||
|
mapOf("command" to "$gradlewCommand help --task ${projectBuildPath}:${helpTaskName}"),
|
||||||
"nonAtomizedTarget" to nonAtomizedTarget)
|
"nonAtomizedTarget" to nonAtomizedTarget)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -44,17 +44,18 @@ class AddTestCiTargetsTest {
|
|||||||
|
|
||||||
val targets = mutableMapOf<String, MutableMap<String, Any?>>()
|
val targets = mutableMapOf<String, MutableMap<String, Any?>>()
|
||||||
val targetGroups = mutableMapOf<String, MutableList<String>>()
|
val targetGroups = mutableMapOf<String, MutableList<String>>()
|
||||||
val ciTargetName = "ci"
|
val ciTestTargetName = "ci"
|
||||||
|
|
||||||
addTestCiTargets(
|
addTestCiTargets(
|
||||||
testFiles = testFiles,
|
testFiles = testFiles,
|
||||||
projectBuildPath = ":project-a",
|
projectBuildPath = ":project-a",
|
||||||
testTask = testTask,
|
testTask = testTask,
|
||||||
|
testTargetName = "test",
|
||||||
targets = targets,
|
targets = targets,
|
||||||
targetGroups = targetGroups,
|
targetGroups = targetGroups,
|
||||||
projectRoot = projectRoot.absolutePath,
|
projectRoot = projectRoot.absolutePath,
|
||||||
workspaceRoot = workspaceRoot.absolutePath,
|
workspaceRoot = workspaceRoot.absolutePath,
|
||||||
ciTargetName = ciTargetName)
|
ciTestTargetName = ciTestTargetName)
|
||||||
|
|
||||||
// Assert each test file created a CI target
|
// Assert each test file created a CI target
|
||||||
assertTrue(targets.containsKey("ci--MyFirstTest"))
|
assertTrue(targets.containsKey("ci--MyFirstTest"))
|
||||||
@ -73,7 +74,7 @@ class AddTestCiTargetsTest {
|
|||||||
assertEquals(2, dependsOn!!.size)
|
assertEquals(2, dependsOn!!.size)
|
||||||
|
|
||||||
val firstTarget = targets["ci--MyFirstTest"]!!
|
val firstTarget = targets["ci--MyFirstTest"]!!
|
||||||
assertTrue(firstTarget["command"].toString().contains("--tests MyFirstTest"))
|
assertEquals(firstTarget["executor"], "@nx/gradle:gradle")
|
||||||
assertEquals(true, firstTarget["cache"])
|
assertEquals(true, firstTarget["cache"])
|
||||||
assertTrue((firstTarget["inputs"] as Array<*>)[0].toString().contains("{projectRoot}"))
|
assertTrue((firstTarget["inputs"] as Array<*>)[0].toString().contains("{projectRoot}"))
|
||||||
assertEquals("nx:noop", parentCi["executor"])
|
assertEquals("nx:noop", parentCi["executor"])
|
||||||
|
|||||||
@ -45,7 +45,6 @@ class CreateNodeForProjectTest {
|
|||||||
assertEquals(project.name, projectNode.name)
|
assertEquals(project.name, projectNode.name)
|
||||||
assertNotNull(projectNode.targets["compileJava"], "Expected compileJava target")
|
assertNotNull(projectNode.targets["compileJava"], "Expected compileJava target")
|
||||||
assertNotNull(projectNode.targets["test"], "Expected test target")
|
assertNotNull(projectNode.targets["test"], "Expected test target")
|
||||||
assertEquals("build", projectNode.metadata.targetGroups.keys.firstOrNull())
|
|
||||||
|
|
||||||
// Dependencies and external nodes should default to empty
|
// Dependencies and external nodes should default to empty
|
||||||
assertTrue(result.dependencies.isEmpty(), "Expected no dependencies")
|
assertTrue(result.dependencies.isEmpty(), "Expected no dependencies")
|
||||||
|
|||||||
@ -83,13 +83,12 @@ class ProcessTaskUtilsTest {
|
|||||||
projectBuildPath = ":project",
|
projectBuildPath = ":project",
|
||||||
projectRoot = project.projectDir.path,
|
projectRoot = project.projectDir.path,
|
||||||
workspaceRoot = project.rootDir.path,
|
workspaceRoot = project.rootDir.path,
|
||||||
cwd = ".",
|
|
||||||
externalNodes = mutableMapOf(),
|
externalNodes = mutableMapOf(),
|
||||||
dependencies = mutableSetOf(),
|
dependencies = mutableSetOf(),
|
||||||
targetNameOverrides = emptyMap())
|
targetNameOverrides = emptyMap())
|
||||||
|
|
||||||
assertEquals(true, result["cache"])
|
assertEquals(true, result["cache"])
|
||||||
assertTrue((result["command"] as String).contains("gradlew"))
|
assertEquals(result["executor"], "@nx/gradle:gradle")
|
||||||
assertNotNull(result["metadata"])
|
assertNotNull(result["metadata"])
|
||||||
assertNotNull(result["options"])
|
assertNotNull(result["options"])
|
||||||
}
|
}
|
||||||
|
|||||||
@ -12,6 +12,11 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"build-base": {
|
"build-base": {
|
||||||
|
"dependsOn": [
|
||||||
|
"^build-base",
|
||||||
|
"build-native",
|
||||||
|
"gradle-batch-runner:assemble"
|
||||||
|
],
|
||||||
"executor": "@nx/js:tsc",
|
"executor": "@nx/js:tsc",
|
||||||
"options": {
|
"options": {
|
||||||
"assets": [
|
"assets": [
|
||||||
@ -42,6 +47,11 @@
|
|||||||
"glob": "**/*.d.ts",
|
"glob": "**/*.d.ts",
|
||||||
"output": "/"
|
"output": "/"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"input": "packages/gradle/batch-runner",
|
||||||
|
"glob": "**/*.jar",
|
||||||
|
"output": "/batch-runner"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"input": "",
|
"input": "",
|
||||||
"glob": "LICENSE",
|
"glob": "LICENSE",
|
||||||
|
|||||||
103
packages/gradle/src/executors/gradle/gradle-batch.impl.ts
Normal file
103
packages/gradle/src/executors/gradle/gradle-batch.impl.ts
Normal file
@ -0,0 +1,103 @@
|
|||||||
|
import { ExecutorContext, output, TaskGraph } from '@nx/devkit';
|
||||||
|
import {
|
||||||
|
LARGE_BUFFER,
|
||||||
|
RunCommandsOptions,
|
||||||
|
} from 'nx/src/executors/run-commands/run-commands.impl';
|
||||||
|
import { BatchResults } from 'nx/src/tasks-runner/batch/batch-messages';
|
||||||
|
import { gradleExecutorSchema } from './schema';
|
||||||
|
import { findGradlewFile } from '../../utils/exec-gradle';
|
||||||
|
import { dirname, join } from 'path';
|
||||||
|
import { execSync } from 'child_process';
|
||||||
|
|
||||||
|
export const batchRunnerPath = join(
|
||||||
|
__dirname,
|
||||||
|
'../../../batch-runner/build/libs/batch-runner-all.jar'
|
||||||
|
);
|
||||||
|
|
||||||
|
interface GradleTask {
|
||||||
|
taskName: string;
|
||||||
|
testClassName: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default async function gradleBatch(
|
||||||
|
taskGraph: TaskGraph,
|
||||||
|
inputs: Record<string, gradleExecutorSchema>,
|
||||||
|
overrides: RunCommandsOptions,
|
||||||
|
context: ExecutorContext
|
||||||
|
): Promise<BatchResults> {
|
||||||
|
try {
|
||||||
|
const projectName = taskGraph.tasks[taskGraph.roots[0]]?.target?.project;
|
||||||
|
let projectRoot = context.projectGraph.nodes[projectName]?.data?.root ?? '';
|
||||||
|
const gradlewPath = findGradlewFile(join(projectRoot, 'project.json')); // find gradlew near project root
|
||||||
|
const root = join(context.root, dirname(gradlewPath));
|
||||||
|
|
||||||
|
// set args with passed in args and overrides in command line
|
||||||
|
const input = inputs[taskGraph.roots[0]];
|
||||||
|
|
||||||
|
let args =
|
||||||
|
typeof input.args === 'string'
|
||||||
|
? input.args.trim().split(' ')
|
||||||
|
: Array.isArray(input.args)
|
||||||
|
? input.args
|
||||||
|
: [];
|
||||||
|
if (overrides.__overrides_unparsed__.length) {
|
||||||
|
args.push(...overrides.__overrides_unparsed__);
|
||||||
|
}
|
||||||
|
|
||||||
|
const gradlewTasksToRun: Record<string, GradleTask> = Object.entries(
|
||||||
|
taskGraph.tasks
|
||||||
|
).reduce((gradlewTasksToRun, [taskId, task]) => {
|
||||||
|
const gradlewTaskName = inputs[task.id].taskName;
|
||||||
|
const testClassName = inputs[task.id].testClassName;
|
||||||
|
gradlewTasksToRun[taskId] = {
|
||||||
|
taskName: gradlewTaskName,
|
||||||
|
testClassName: testClassName,
|
||||||
|
};
|
||||||
|
return gradlewTasksToRun;
|
||||||
|
}, {});
|
||||||
|
const gradlewBatchStart = performance.mark(`gradlew-batch:start`);
|
||||||
|
const batchResults = execSync(
|
||||||
|
`java -jar ${batchRunnerPath} --tasks='${JSON.stringify(
|
||||||
|
gradlewTasksToRun
|
||||||
|
)}' --workspaceRoot=${root} --args='${args
|
||||||
|
.join(' ')
|
||||||
|
.replaceAll("'", '"')}' ${
|
||||||
|
process.env.NX_VERBOSE_LOGGING === 'true' ? '' : '--quiet'
|
||||||
|
}`,
|
||||||
|
{
|
||||||
|
windowsHide: true,
|
||||||
|
env: process.env,
|
||||||
|
maxBuffer: LARGE_BUFFER,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
const gradlewBatchEnd = performance.mark(`gradlew-batch:end`);
|
||||||
|
performance.measure(
|
||||||
|
`gradlew-batch`,
|
||||||
|
gradlewBatchStart.name,
|
||||||
|
gradlewBatchEnd.name
|
||||||
|
);
|
||||||
|
const gradlewBatchResults = JSON.parse(
|
||||||
|
batchResults.toString()
|
||||||
|
) as BatchResults;
|
||||||
|
|
||||||
|
Object.keys(taskGraph.tasks).forEach((taskId) => {
|
||||||
|
if (!gradlewBatchResults[taskId]) {
|
||||||
|
gradlewBatchResults[taskId] = {
|
||||||
|
success: false,
|
||||||
|
terminalOutput: `Gradlew batch failed`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return gradlewBatchResults;
|
||||||
|
} catch (e) {
|
||||||
|
output.error({
|
||||||
|
title: `Gradlew batch failed`,
|
||||||
|
bodyLines: [e.toString()],
|
||||||
|
});
|
||||||
|
return taskGraph.roots.reduce((acc, key) => {
|
||||||
|
acc[key] = { success: false, terminalOutput: e.toString() };
|
||||||
|
return acc;
|
||||||
|
}, {} as BatchResults);
|
||||||
|
}
|
||||||
|
}
|
||||||
39
packages/gradle/src/executors/gradle/gradle.impl.ts
Normal file
39
packages/gradle/src/executors/gradle/gradle.impl.ts
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
import { ExecutorContext } from '@nx/devkit';
|
||||||
|
import { gradleExecutorSchema } from './schema';
|
||||||
|
import { findGradlewFile } from '../../utils/exec-gradle';
|
||||||
|
import { dirname, join } from 'node:path';
|
||||||
|
import runCommandsImpl from 'nx/src/executors/run-commands/run-commands.impl';
|
||||||
|
|
||||||
|
export default async function gradleExecutor(
|
||||||
|
options: gradleExecutorSchema,
|
||||||
|
context: ExecutorContext
|
||||||
|
): Promise<{ success: boolean }> {
|
||||||
|
let projectRoot =
|
||||||
|
context.projectGraph.nodes[context.projectName]?.data?.root ?? context.root;
|
||||||
|
let gradlewPath = findGradlewFile(join(projectRoot, 'project.json')); // find gradlew near project root
|
||||||
|
gradlewPath = join(context.root, gradlewPath);
|
||||||
|
|
||||||
|
let args =
|
||||||
|
typeof options.args === 'string'
|
||||||
|
? options.args.trim().split(' ')
|
||||||
|
: Array.isArray(options.args)
|
||||||
|
? options.args
|
||||||
|
: [];
|
||||||
|
if (options.testClassName) {
|
||||||
|
args.push(`--tests`, options.testClassName);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await runCommandsImpl(
|
||||||
|
{
|
||||||
|
command: `${gradlewPath} ${options.taskName}`,
|
||||||
|
cwd: dirname(gradlewPath),
|
||||||
|
args: args,
|
||||||
|
__unparsed__: [],
|
||||||
|
},
|
||||||
|
context
|
||||||
|
);
|
||||||
|
return { success: true };
|
||||||
|
} catch (e) {
|
||||||
|
return { success: false };
|
||||||
|
}
|
||||||
|
}
|
||||||
5
packages/gradle/src/executors/gradle/schema.d.ts
vendored
Normal file
5
packages/gradle/src/executors/gradle/schema.d.ts
vendored
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
export interface gradleExecutorSchema {
|
||||||
|
taskName: string;
|
||||||
|
testClassName?: string;
|
||||||
|
args?: string[] | string;
|
||||||
|
}
|
||||||
33
packages/gradle/src/executors/gradle/schema.json
Normal file
33
packages/gradle/src/executors/gradle/schema.json
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://json-schema.org/schema",
|
||||||
|
"version": 2,
|
||||||
|
"title": "Gradle Impl executor",
|
||||||
|
"description": "The Gradle Impl executor is used to run Gradle tasks.",
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"taskName": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "The name of the Gradle task to run."
|
||||||
|
},
|
||||||
|
"testClassName": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "The test class name to run for test task."
|
||||||
|
},
|
||||||
|
"args": {
|
||||||
|
"oneOf": [
|
||||||
|
{
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "The arguments to pass to the Gradle task.",
|
||||||
|
"examples": [["--warning-mode", "all"], "--stracktrace"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["taskName"]
|
||||||
|
}
|
||||||
@ -1,7 +1,5 @@
|
|||||||
import { AggregateCreateNodesError, logger, output } from '@nx/devkit';
|
import { AggregateCreateNodesError, logger, output } from '@nx/devkit';
|
||||||
import { execGradleAsync, newLineSeparator } from '../../utils/exec-gradle';
|
import { execGradleAsync, newLineSeparator } from '../../utils/exec-gradle';
|
||||||
import { existsSync } from 'fs';
|
|
||||||
import { dirname, join } from 'path';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This function executes the gradle projectReportAll task and returns the output as an array of lines.
|
* This function executes the gradle projectReportAll task and returns the output as an array of lines.
|
||||||
|
|||||||
@ -84,7 +84,7 @@ exports[`@nx/gradle/plugin/nodes should create nodes based on gradle for nested
|
|||||||
]
|
]
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`@nx/gradle/plugin/nodes should create nodes with atomized tests targets based on gradle if ciTargetName is specified 1`] = `
|
exports[`@nx/gradle/plugin/nodes should create nodes with atomized tests targets based on gradle if ciTestTargetName is specified 1`] = `
|
||||||
[
|
[
|
||||||
[
|
[
|
||||||
"proj/application/build.gradle",
|
"proj/application/build.gradle",
|
||||||
@ -466,7 +466,7 @@ exports[`@nx/gradle/plugin/nodes should create nodes with atomized tests targets
|
|||||||
]
|
]
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`@nx/gradle/plugin/nodes should not create nodes with atomized tests targets based on gradle if ciTargetName is not specified 1`] = `
|
exports[`@nx/gradle/plugin/nodes should not create nodes with atomized tests targets based on gradle if ciTestTargetName is not specified 1`] = `
|
||||||
[
|
[
|
||||||
[
|
[
|
||||||
"proj/application/build.gradle",
|
"proj/application/build.gradle",
|
||||||
|
|||||||
@ -8,7 +8,7 @@ import {
|
|||||||
validateDependency,
|
validateDependency,
|
||||||
workspaceRoot,
|
workspaceRoot,
|
||||||
} from '@nx/devkit';
|
} from '@nx/devkit';
|
||||||
import { relative } from 'node:path';
|
import { join, relative } from 'node:path';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
getCurrentProjectGraphReport,
|
getCurrentProjectGraphReport,
|
||||||
@ -29,7 +29,11 @@ export const createDependencies: CreateDependencies<
|
|||||||
Array.from(GRALDEW_FILES)
|
Array.from(GRALDEW_FILES)
|
||||||
);
|
);
|
||||||
const { gradlewFiles } = splitConfigFiles(files);
|
const { gradlewFiles } = splitConfigFiles(files);
|
||||||
await populateProjectGraph(context.workspaceRoot, gradlewFiles, options);
|
await populateProjectGraph(
|
||||||
|
context.workspaceRoot,
|
||||||
|
gradlewFiles.map((file) => join(workspaceRoot, file)),
|
||||||
|
options
|
||||||
|
);
|
||||||
const { dependencies: dependenciesFromReport } =
|
const { dependencies: dependenciesFromReport } =
|
||||||
getCurrentProjectGraphReport();
|
getCurrentProjectGraphReport();
|
||||||
|
|
||||||
|
|||||||
@ -82,12 +82,12 @@ describe('@nx/gradle/plugin/nodes', () => {
|
|||||||
expect(results).toMatchSnapshot();
|
expect(results).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should create nodes with atomized tests targets based on gradle if ciTargetName is specified', async () => {
|
it('should create nodes with atomized tests targets based on gradle if ciTestTargetName is specified', async () => {
|
||||||
const results = await createNodesFunction(
|
const results = await createNodesFunction(
|
||||||
['proj/application/build.gradle'],
|
['proj/application/build.gradle'],
|
||||||
{
|
{
|
||||||
buildTargetName: 'build',
|
buildTargetName: 'build',
|
||||||
ciTargetName: 'test-ci',
|
ciTestTargetName: 'test-ci',
|
||||||
},
|
},
|
||||||
context
|
context
|
||||||
);
|
);
|
||||||
@ -95,7 +95,7 @@ describe('@nx/gradle/plugin/nodes', () => {
|
|||||||
expect(results).toMatchSnapshot();
|
expect(results).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not create nodes with atomized tests targets based on gradle if ciTargetName is not specified', async () => {
|
it('should not create nodes with atomized tests targets based on gradle if ciTestTargetName is not specified', async () => {
|
||||||
const results = await createNodesFunction(
|
const results = await createNodesFunction(
|
||||||
['proj/application/build.gradle'],
|
['proj/application/build.gradle'],
|
||||||
{
|
{
|
||||||
|
|||||||
@ -80,6 +80,12 @@ export const makeCreateNodesForGradleConfigFile =
|
|||||||
options: GradlePluginOptions | undefined,
|
options: GradlePluginOptions | undefined,
|
||||||
context: CreateNodesContext
|
context: CreateNodesContext
|
||||||
) => {
|
) => {
|
||||||
|
if (process.env.VERCEL) {
|
||||||
|
// Vercel does not allow JAVA_VERSION to be set
|
||||||
|
// skip on Vercel
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
const projectRoot = dirname(gradleFilePath);
|
const projectRoot = dirname(gradleFilePath);
|
||||||
options = normalizeOptions(options);
|
options = normalizeOptions(options);
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
export interface GradlePluginOptions {
|
export interface GradlePluginOptions {
|
||||||
testTargetName?: string;
|
testTargetName?: string;
|
||||||
ciTargetName?: string;
|
ciTestTargetName?: string;
|
||||||
|
ciIntTestTargetName?: string;
|
||||||
[taskTargetName: string]: string | undefined | boolean;
|
[taskTargetName: string]: string | undefined | boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
export const nxVersion = require('../../package.json').version;
|
export const nxVersion = require('../../package.json').version;
|
||||||
|
|
||||||
export const gradleProjectGraphPluginName = 'dev.nx.gradle.project-graph';
|
export const gradleProjectGraphPluginName = 'dev.nx.gradle.project-graph';
|
||||||
export const gradleProjectGraphVersion = '0.0.2';
|
export const gradleProjectGraphVersion = '0.1.0';
|
||||||
|
|||||||
@ -75,7 +75,7 @@ async function runTasks(
|
|||||||
const results = await batchExecutor.batchImplementationFactory()(
|
const results = await batchExecutor.batchImplementationFactory()(
|
||||||
batchTaskGraph,
|
batchTaskGraph,
|
||||||
input,
|
input,
|
||||||
tasks[0].overrides,
|
tasks[tasks.length - 1].overrides,
|
||||||
context
|
context
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@ -321,6 +321,9 @@ export class TaskOrchestrator {
|
|||||||
batch: Batch,
|
batch: Batch,
|
||||||
groupId: number
|
groupId: number
|
||||||
) {
|
) {
|
||||||
|
const applyFromCacheOrRunBatchStart = performance.mark(
|
||||||
|
'TaskOrchestrator-apply-from-cache-or-run-batch:start'
|
||||||
|
);
|
||||||
const taskEntries = Object.entries(batch.taskGraph.tasks);
|
const taskEntries = Object.entries(batch.taskGraph.tasks);
|
||||||
const tasks = taskEntries.map(([, task]) => task);
|
const tasks = taskEntries.map(([, task]) => task);
|
||||||
|
|
||||||
@ -374,9 +377,19 @@ export class TaskOrchestrator {
|
|||||||
groupId
|
groupId
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
// Batch is done, mark it as completed
|
||||||
|
const applyFromCacheOrRunBatchEnd = performance.mark(
|
||||||
|
'TaskOrchestrator-apply-from-cache-or-run-batch:end'
|
||||||
|
);
|
||||||
|
performance.measure(
|
||||||
|
'TaskOrchestrator-apply-from-cache-or-run-batch',
|
||||||
|
applyFromCacheOrRunBatchStart.name,
|
||||||
|
applyFromCacheOrRunBatchEnd.name
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async runBatch(batch: Batch, env: NodeJS.ProcessEnv) {
|
private async runBatch(batch: Batch, env: NodeJS.ProcessEnv) {
|
||||||
|
const runBatchStart = performance.mark('TaskOrchestrator-run-batch:start');
|
||||||
try {
|
try {
|
||||||
const batchProcess =
|
const batchProcess =
|
||||||
await this.forkedProcessTaskRunner.forkProcessForBatch(
|
await this.forkedProcessTaskRunner.forkProcessForBatch(
|
||||||
@ -402,6 +415,13 @@ export class TaskOrchestrator {
|
|||||||
task: this.taskGraph.tasks[rootTaskId],
|
task: this.taskGraph.tasks[rootTaskId],
|
||||||
status: 'failure' as TaskStatus,
|
status: 'failure' as TaskStatus,
|
||||||
}));
|
}));
|
||||||
|
} finally {
|
||||||
|
const runBatchEnd = performance.mark('TaskOrchestrator-run-batch:end');
|
||||||
|
performance.measure(
|
||||||
|
'TaskOrchestrator-run-batch',
|
||||||
|
runBatchStart.name,
|
||||||
|
runBatchEnd.name
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -16,3 +16,4 @@ pluginManagement {
|
|||||||
|
|
||||||
rootProject.name = "nx"
|
rootProject.name = "nx"
|
||||||
includeBuild("./packages/gradle/project-graph")
|
includeBuild("./packages/gradle/project-graph")
|
||||||
|
includeBuild("./packages/gradle/batch-runner")
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user