From cc17a7d6bf6d4eb164a1b38bbf378135536ae745 Mon Sep 17 00:00:00 2001 From: Ryan Dens Date: Tue, 31 Oct 2023 22:23:32 -0700 Subject: [PATCH 01/37] :white_check_mark: add windows to CI --- .github/workflows/gradle.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/gradle.yml b/.github/workflows/gradle.yml index d3164f0..2c145a9 100644 --- a/.github/workflows/gradle.yml +++ b/.github/workflows/gradle.yml @@ -15,7 +15,10 @@ jobs: - uses: gradle/actions/wrapper-validation@v6 build: name: "Javaagent Gradle Plugin Validation" - runs-on: ubuntu-latest + strategy: + matrix: + platform: [ubuntu-latest, windows-latest] + runs-on: ${{ matrix.platform }} steps: - uses: actions/checkout@v7.0.0 - uses: actions/setup-java@v5.4.0 From fbbb5285843e7d08e7417cfa0541646a8d56333c Mon Sep 17 00:00:00 2001 From: Punya Biswal Date: Tue, 18 Jul 2023 23:55:12 -0400 Subject: [PATCH 02/37] Use Unix line endings for all files See https://github.com/diffplug/spotless/issues/39 for details --- .gitattributes | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .gitattributes diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..d10bbd5 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,5 @@ +* text eol=lf + +# Vendored/binary files must not be line-ending normalized. +*.jar binary +gradlew.bat binary From 352f517106e780300b80cd1486003564ceb38a63 Mon Sep 17 00:00:00 2001 From: Punya Biswal Date: Tue, 18 Jul 2023 22:39:57 -0400 Subject: [PATCH 03/37] Add support for Windows --- .../JavaagentApplicationDistributionPlugin.kt | 12 +++--------- .../JavaagentAwareStartScriptGenerator.kt | 17 ++++++++++------- .../kotlin/com/ryandens/javaagent/Platform.kt | 16 ++++++++++++++++ 3 files changed, 29 insertions(+), 16 deletions(-) create mode 100644 plugin/src/main/kotlin/com/ryandens/javaagent/Platform.kt diff --git a/plugin/src/main/kotlin/com/ryandens/javaagent/JavaagentApplicationDistributionPlugin.kt b/plugin/src/main/kotlin/com/ryandens/javaagent/JavaagentApplicationDistributionPlugin.kt index 3645e5f..3caf7c1 100644 --- a/plugin/src/main/kotlin/com/ryandens/javaagent/JavaagentApplicationDistributionPlugin.kt +++ b/plugin/src/main/kotlin/com/ryandens/javaagent/JavaagentApplicationDistributionPlugin.kt @@ -6,7 +6,6 @@ import org.gradle.api.Project import org.gradle.api.artifacts.Configuration import org.gradle.api.distribution.DistributionContainer import org.gradle.api.distribution.plugins.DistributionPlugin -import org.gradle.api.internal.plugins.WindowsStartScriptGenerator import org.gradle.api.plugins.ApplicationPlugin import org.gradle.api.tasks.application.CreateStartScripts @@ -52,14 +51,9 @@ class JavaagentApplicationDistributionPlugin : .plus(it.defaultJvmOpts ?: listOf()) it.inputs.files(javaagentConfiguration) // custom start script generator that replaces the placeholder - it.unixStartScriptGenerator = - JavaagentAwareStartScriptGenerator( - javaagentConfiguration.map { configuration -> - configuration.files - }, - ) - // TODO build support for windows - it.windowsStartScriptGenerator = WindowsStartScriptGenerator() + val agentFiles = javaagentConfiguration.map { configuration -> configuration.files } + it.unixStartScriptGenerator = JavaagentAwareStartScriptGenerator(agentFiles, Platform.UNIX) + it.windowsStartScriptGenerator = JavaagentAwareStartScriptGenerator(agentFiles, Platform.WINDOWS) } } } diff --git a/plugin/src/main/kotlin/com/ryandens/javaagent/JavaagentAwareStartScriptGenerator.kt b/plugin/src/main/kotlin/com/ryandens/javaagent/JavaagentAwareStartScriptGenerator.kt index 247a0fa..1225a83 100644 --- a/plugin/src/main/kotlin/com/ryandens/javaagent/JavaagentAwareStartScriptGenerator.kt +++ b/plugin/src/main/kotlin/com/ryandens/javaagent/JavaagentAwareStartScriptGenerator.kt @@ -3,7 +3,6 @@ package com.ryandens.javaagent import org.gradle.api.Transformer import org.gradle.api.internal.plugins.DefaultTemplateBasedStartScriptGenerator import org.gradle.api.internal.plugins.StartScriptTemplateBindingFactory -import org.gradle.api.internal.plugins.UnixStartScriptGenerator import org.gradle.api.provider.Provider import org.gradle.jvm.application.scripts.JavaAppStartScriptGenerationDetails import org.gradle.jvm.application.scripts.ScriptGenerator @@ -12,18 +11,19 @@ import java.io.Writer class JavaagentAwareStartScriptGenerator( private val javaagentConfiguration: Provider>, + private val platform: Platform, private val inner: ScriptGenerator = DefaultTemplateBasedStartScriptGenerator( - "\n", - FakeTransformer(StartScriptTemplateBindingFactory.unix()), - UnixStartScriptGenerator().template, + platform.lineSeparator, + FakeTransformer(platform.templateBindingFactory), + platform.template, ), ) : ScriptGenerator { override fun generateScript( details: JavaAppStartScriptGenerationDetails, destination: Writer, ) { - inner.generateScript(details, Fake(destination, javaagentConfiguration)) + inner.generateScript(details, Fake(destination, javaagentConfiguration, platform.pathSeparator)) } private class FakeTransformer( @@ -46,7 +46,8 @@ class JavaagentAwareStartScriptGenerator( private class Fake( private val inner: Writer, private val javaagentFiles: Provider>, - ) : Writer() { + private val pathSeparator: String, + ) : Writer() { override fun close() { inner.close() } @@ -76,7 +77,9 @@ class JavaagentAwareStartScriptGenerator( } else { str.replace( "-javaagent:COM_RYANDENS_JAVAAGENTS_PLACEHOLDER.jar", - javaagentFiles.get().joinToString(" ") { jar -> "-javaagent:\$APP_HOME/agent-libs/${jar.name}" }, + javaagentFiles.get().joinToString( + " ", + ) { jar -> "-javaagent:\$APP_HOME${pathSeparator}agent-libs${pathSeparator}${jar.name}" }, ) } super.write(replace) diff --git a/plugin/src/main/kotlin/com/ryandens/javaagent/Platform.kt b/plugin/src/main/kotlin/com/ryandens/javaagent/Platform.kt new file mode 100644 index 0000000..3bd7983 --- /dev/null +++ b/plugin/src/main/kotlin/com/ryandens/javaagent/Platform.kt @@ -0,0 +1,16 @@ +package com.ryandens.javaagent + +import org.gradle.api.internal.plugins.StartScriptTemplateBindingFactory +import org.gradle.api.internal.plugins.UnixStartScriptGenerator +import org.gradle.api.internal.plugins.WindowsStartScriptGenerator +import org.gradle.api.resources.TextResource + +enum class Platform( + val lineSeparator: String, + val pathSeparator: String, + val templateBindingFactory: StartScriptTemplateBindingFactory, + val template: TextResource, +) { + UNIX("\n", "/", StartScriptTemplateBindingFactory.unix(), UnixStartScriptGenerator().template), + WINDOWS("\r\n", "\\", StartScriptTemplateBindingFactory.windows(), WindowsStartScriptGenerator().template), +} From 615a34239ba57966bad8b53c90a9972c25722571 Mon Sep 17 00:00:00 2001 From: Punya Biswal Date: Wed, 19 Jul 2023 00:39:33 -0400 Subject: [PATCH 04/37] Use portable path separators --- .../com/ryandens/javaagent/JavaagentPluginFunctionalTest.kt | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/plugin/src/functionalTest/kotlin/com/ryandens/javaagent/JavaagentPluginFunctionalTest.kt b/plugin/src/functionalTest/kotlin/com/ryandens/javaagent/JavaagentPluginFunctionalTest.kt index 45dcb45..38288c2 100644 --- a/plugin/src/functionalTest/kotlin/com/ryandens/javaagent/JavaagentPluginFunctionalTest.kt +++ b/plugin/src/functionalTest/kotlin/com/ryandens/javaagent/JavaagentPluginFunctionalTest.kt @@ -5,6 +5,7 @@ import org.gradle.testkit.runner.BuildResult import org.gradle.testkit.runner.GradleRunner import org.gradle.testkit.runner.TaskOutcome import java.io.File +import java.nio.file.Paths import kotlin.test.AfterTest import kotlin.test.BeforeTest import kotlin.test.Test @@ -191,10 +192,11 @@ DEFAULT_JVM_OPTS="-javaagent:${"$"}APP_HOME/lib/simple-agent.jar -Xmx256m" } private fun createJavaagentProject(dependencies: String) { - File("src/functionalTest/resources/hello-world-project/").copyRecursively(helloWorldDir) + val helloWorldDir = File(functionalTestDir, "hello-world") + Paths.get("src", "functionalTest", "resources", "hello-world-project").toFile().copyRecursively(helloWorldDir) val simpleAgentTestDir = File(functionalTestDir, "simple-agent") val simpleAgentBuildScript = simpleAgentTestDir.resolve("build.gradle.kts") - File("../simple-agent/").copyRecursively(simpleAgentTestDir) + Paths.get("..", "simple-agent").toFile().copyRecursively(simpleAgentTestDir) simpleAgentBuildScript.writeText( simpleAgentBuildScript.readText().replace( "id(\"com.ryandens.java-conventions\")\n", From 44188032ec7ddb77098dc6ce0d7d1d719a6a2935 Mon Sep 17 00:00:00 2001 From: Ryan Dens Date: Tue, 31 Oct 2023 23:03:16 -0700 Subject: [PATCH 05/37] :bug: get rid of hardcoded file separators in tests --- .../javaagent/JavaagentPluginFunctionalTest.kt | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/plugin/src/functionalTest/kotlin/com/ryandens/javaagent/JavaagentPluginFunctionalTest.kt b/plugin/src/functionalTest/kotlin/com/ryandens/javaagent/JavaagentPluginFunctionalTest.kt index 38288c2..b184aea 100644 --- a/plugin/src/functionalTest/kotlin/com/ryandens/javaagent/JavaagentPluginFunctionalTest.kt +++ b/plugin/src/functionalTest/kotlin/com/ryandens/javaagent/JavaagentPluginFunctionalTest.kt @@ -26,7 +26,7 @@ class JavaagentPluginFunctionalTest { @BeforeTest fun beforeEach() { - functionalTestDir = File("build/functionalTest") + functionalTestDir = File("build", "functionalTest") functionalTestDir.mkdirs() helloWorldDir = File(functionalTestDir, "hello-world") } @@ -234,11 +234,10 @@ DEFAULT_JVM_OPTS="-javaagent:${"$"}APP_HOME/lib/simple-agent.jar -Xmx256m" } task execStartScript(type: Exec) { - inputs.files(fileTree('${helloWorldDir.canonicalPath}/build/install/') { - builtBy tasks.named('installDist') - }) - workingDir '${helloWorldDir.canonicalPath}/build/install/hello-world/bin/' - commandLine './hello-world' + dependsOn('installDist') + inputs.files(layout.buildDirectory.dir('install')) + workingDir '${helloWorldDir.canonicalPath + File.separator}build${File.separator}install${File.separator}hello-world${File.separator}bin${File.separator}' + commandLine '.${File.separator}hello-world' environment JAVA_HOME: "${Jvm.current().getJavaHome()}" } From e4ba41f39d3b073a59ed61013294e209dc8a2add Mon Sep 17 00:00:00 2001 From: Ryan Dens Date: Tue, 31 Oct 2023 23:06:25 -0700 Subject: [PATCH 06/37] :bug: more path separator cleanup --- .../javaagent/JavaagentPluginFunctionalTest.kt | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/plugin/src/functionalTest/kotlin/com/ryandens/javaagent/JavaagentPluginFunctionalTest.kt b/plugin/src/functionalTest/kotlin/com/ryandens/javaagent/JavaagentPluginFunctionalTest.kt index b184aea..d7ca138 100644 --- a/plugin/src/functionalTest/kotlin/com/ryandens/javaagent/JavaagentPluginFunctionalTest.kt +++ b/plugin/src/functionalTest/kotlin/com/ryandens/javaagent/JavaagentPluginFunctionalTest.kt @@ -121,14 +121,14 @@ class JavaagentPluginFunctionalTest { val result = runBuild(listOf("--configuration-cache", "build", "installDist", "execStartScript")) // verify the distribution was created properly - val applicationDistribution = File(functionalTestDir, "hello-world/build/distributions/hello-world.tar") + val applicationDistribution = File(functionalTestDir, "hello-world${File.separator}build${File.separator}distributions${File.separator}hello-world.tar") assertTrue(applicationDistribution.exists()) // verify the expected text was injected into the start script val expectedDefaultJavaOpts = """ DEFAULT_JVM_OPTS="-javaagent:${"$"}APP_HOME/agent-libs/simple-agent.jar -Xmx256m" """ - val applicationDistributionScript = File(functionalTestDir, "hello-world/build/scripts/hello-world") + val applicationDistributionScript = File(functionalTestDir, "hello-world${File.separator}build${File.separator}scripts${File.separator}hello-world") assertTrue(applicationDistributionScript.readText().contains(expectedDefaultJavaOpts)) /* @@ -141,7 +141,7 @@ DEFAULT_JVM_OPTS="-javaagent:${"$"}APP_HOME/lib/simple-agent.jar -Xmx256m" */ // verify the agent was added to the /lib/ dir of the distribution - assertTrue(File(functionalTestDir, "hello-world/build/install/hello-world/agent-libs/simple-agent.jar").exists()) + assertTrue(File(functionalTestDir, "hello-world${File.separator}build${File.separator}install${File.separator}hello-world${File.separator}agent-libs${File.separator}simple-agent.jar").exists()) // Verify the result assertTrue(result.output.contains("Hello World!")) @@ -181,13 +181,13 @@ DEFAULT_JVM_OPTS="-javaagent:${"$"}APP_HOME/lib/simple-agent.jar -Xmx256m" val result = runBuild(listOf("build", "installDist", "execStartScript")) // verify the distribution was created properly - val applicationDistribution = File(functionalTestDir, "hello-world/build/distributions/hello-world.tar") + val applicationDistribution = File(functionalTestDir, "hello-world${File.separator}build${File.separator}distributions${File.separator}hello-world.tar") assertTrue(applicationDistribution.exists()) - val applicationDistributionScript = File(functionalTestDir, "hello-world/build/scripts/hello-world") + val applicationDistributionScript = File(functionalTestDir, "hello-world${File.separator}build${File.separator}scripts${File.separator}hello-world") assertTrue(applicationDistributionScript.readText().contains("""DEFAULT_JVM_OPTS="-Xmx256m""")) - assertFalse(File(functionalTestDir, "hello-world/build/install/hello-world/agent-libs/").exists()) + assertFalse(File(functionalTestDir, "hello-world${File.separator}build${File.separator}install${File.separator}hello-world${File.separator}agent-libs/").exists()) assertTrue(result.output.contains("Hello World!")) } From 35e885f31c13ff2215f3841a9eb9e0eb31086235 Mon Sep 17 00:00:00 2001 From: Ryan Dens Date: Tue, 31 Oct 2023 23:13:20 -0700 Subject: [PATCH 07/37] :art: --- .../JavaagentPluginFunctionalTest.kt | 26 ++++++++++++++----- 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/plugin/src/functionalTest/kotlin/com/ryandens/javaagent/JavaagentPluginFunctionalTest.kt b/plugin/src/functionalTest/kotlin/com/ryandens/javaagent/JavaagentPluginFunctionalTest.kt index d7ca138..06f4ea7 100644 --- a/plugin/src/functionalTest/kotlin/com/ryandens/javaagent/JavaagentPluginFunctionalTest.kt +++ b/plugin/src/functionalTest/kotlin/com/ryandens/javaagent/JavaagentPluginFunctionalTest.kt @@ -121,14 +121,16 @@ class JavaagentPluginFunctionalTest { val result = runBuild(listOf("--configuration-cache", "build", "installDist", "execStartScript")) // verify the distribution was created properly - val applicationDistribution = File(functionalTestDir, "hello-world${File.separator}build${File.separator}distributions${File.separator}hello-world.tar") + val applicationDistribution = + File(functionalTestDir, "hello-world${File.separator}build${File.separator}distributions${File.separator}hello-world.tar") assertTrue(applicationDistribution.exists()) // verify the expected text was injected into the start script val expectedDefaultJavaOpts = """ DEFAULT_JVM_OPTS="-javaagent:${"$"}APP_HOME/agent-libs/simple-agent.jar -Xmx256m" """ - val applicationDistributionScript = File(functionalTestDir, "hello-world${File.separator}build${File.separator}scripts${File.separator}hello-world") + val applicationDistributionScript = + File(functionalTestDir, "hello-world${File.separator}build${File.separator}scripts${File.separator}hello-world") assertTrue(applicationDistributionScript.readText().contains(expectedDefaultJavaOpts)) /* @@ -141,7 +143,12 @@ DEFAULT_JVM_OPTS="-javaagent:${"$"}APP_HOME/lib/simple-agent.jar -Xmx256m" */ // verify the agent was added to the /lib/ dir of the distribution - assertTrue(File(functionalTestDir, "hello-world${File.separator}build${File.separator}install${File.separator}hello-world${File.separator}agent-libs${File.separator}simple-agent.jar").exists()) + assertTrue( + File( + functionalTestDir, + "hello-world/build/install/hello-world/agent-libs/simple-agent.jar".replace("/", File.separator), + ).exists(), + ) // Verify the result assertTrue(result.output.contains("Hello World!")) @@ -181,13 +188,20 @@ DEFAULT_JVM_OPTS="-javaagent:${"$"}APP_HOME/lib/simple-agent.jar -Xmx256m" val result = runBuild(listOf("build", "installDist", "execStartScript")) // verify the distribution was created properly - val applicationDistribution = File(functionalTestDir, "hello-world${File.separator}build${File.separator}distributions${File.separator}hello-world.tar") + val applicationDistribution = + File(functionalTestDir, "hello-world${File.separator}build${File.separator}distributions${File.separator}hello-world.tar") assertTrue(applicationDistribution.exists()) - val applicationDistributionScript = File(functionalTestDir, "hello-world${File.separator}build${File.separator}scripts${File.separator}hello-world") + val applicationDistributionScript = + File(functionalTestDir, "hello-world${File.separator}build${File.separator}scripts${File.separator}hello-world") assertTrue(applicationDistributionScript.readText().contains("""DEFAULT_JVM_OPTS="-Xmx256m""")) - assertFalse(File(functionalTestDir, "hello-world${File.separator}build${File.separator}install${File.separator}hello-world${File.separator}agent-libs/").exists()) + assertFalse( + File( + functionalTestDir, + "hello-world${File.separator}build${File.separator}install${File.separator}hello-world${File.separator}agent-libs/", + ).exists(), + ) assertTrue(result.output.contains("Hello World!")) } From 98be98ad3617f1c67e8aace47ded31cdd97b6ca4 Mon Sep 17 00:00:00 2001 From: Ryan Dens Date: Tue, 31 Oct 2023 23:22:47 -0700 Subject: [PATCH 08/37] :bug: use layout for specifying dir where possible --- .../com/ryandens/javaagent/JavaagentPluginFunctionalTest.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugin/src/functionalTest/kotlin/com/ryandens/javaagent/JavaagentPluginFunctionalTest.kt b/plugin/src/functionalTest/kotlin/com/ryandens/javaagent/JavaagentPluginFunctionalTest.kt index 06f4ea7..f78be5f 100644 --- a/plugin/src/functionalTest/kotlin/com/ryandens/javaagent/JavaagentPluginFunctionalTest.kt +++ b/plugin/src/functionalTest/kotlin/com/ryandens/javaagent/JavaagentPluginFunctionalTest.kt @@ -250,7 +250,7 @@ DEFAULT_JVM_OPTS="-javaagent:${"$"}APP_HOME/lib/simple-agent.jar -Xmx256m" task execStartScript(type: Exec) { dependsOn('installDist') inputs.files(layout.buildDirectory.dir('install')) - workingDir '${helloWorldDir.canonicalPath + File.separator}build${File.separator}install${File.separator}hello-world${File.separator}bin${File.separator}' + workingDir(layout.buildDirectory.dir('install').map { it.dir('hello-world').dir('bin') }) commandLine '.${File.separator}hello-world' environment JAVA_HOME: "${Jvm.current().getJavaHome()}" } From 337b027f6a28d72191fea6e0a36aecb6c5e06fe8 Mon Sep 17 00:00:00 2001 From: Ryan Dens Date: Wed, 17 Dec 2025 19:36:12 -0800 Subject: [PATCH 09/37] :fire: remove gitattributes --- .gitattributes | 5 ----- 1 file changed, 5 deletions(-) delete mode 100644 .gitattributes diff --git a/.gitattributes b/.gitattributes deleted file mode 100644 index d10bbd5..0000000 --- a/.gitattributes +++ /dev/null @@ -1,5 +0,0 @@ -* text eol=lf - -# Vendored/binary files must not be line-ending normalized. -*.jar binary -gradlew.bat binary From 9a0463115d56837c524d14748f8b781b3018842d Mon Sep 17 00:00:00 2001 From: Ryan Dens Date: Wed, 17 Dec 2025 19:42:13 -0800 Subject: [PATCH 10/37] :art: --- .../ryandens/javaagent/JavaagentAwareStartScriptGenerator.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugin/src/main/kotlin/com/ryandens/javaagent/JavaagentAwareStartScriptGenerator.kt b/plugin/src/main/kotlin/com/ryandens/javaagent/JavaagentAwareStartScriptGenerator.kt index 1225a83..5658eae 100644 --- a/plugin/src/main/kotlin/com/ryandens/javaagent/JavaagentAwareStartScriptGenerator.kt +++ b/plugin/src/main/kotlin/com/ryandens/javaagent/JavaagentAwareStartScriptGenerator.kt @@ -47,7 +47,7 @@ class JavaagentAwareStartScriptGenerator( private val inner: Writer, private val javaagentFiles: Provider>, private val pathSeparator: String, - ) : Writer() { + ) : Writer() { override fun close() { inner.close() } From 3fd65d372766f94927eb671e582ee42dd32fd215 Mon Sep 17 00:00:00 2001 From: Ryan Dens Date: Wed, 17 Dec 2025 19:51:54 -0800 Subject: [PATCH 11/37] :rotating_light: --- .../javaagent/JavaagentApplicationDistributionPlugin.kt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/plugin/src/main/kotlin/com/ryandens/javaagent/JavaagentApplicationDistributionPlugin.kt b/plugin/src/main/kotlin/com/ryandens/javaagent/JavaagentApplicationDistributionPlugin.kt index 3caf7c1..f7174a3 100644 --- a/plugin/src/main/kotlin/com/ryandens/javaagent/JavaagentApplicationDistributionPlugin.kt +++ b/plugin/src/main/kotlin/com/ryandens/javaagent/JavaagentApplicationDistributionPlugin.kt @@ -7,7 +7,9 @@ import org.gradle.api.artifacts.Configuration import org.gradle.api.distribution.DistributionContainer import org.gradle.api.distribution.plugins.DistributionPlugin import org.gradle.api.plugins.ApplicationPlugin +import org.gradle.api.provider.Provider import org.gradle.api.tasks.application.CreateStartScripts +import java.io.File /** * Gradle plugin for configuration of the [DistributionPlugin.MAIN_DISTRIBUTION_NAME] after it has been configured by @@ -51,7 +53,7 @@ class JavaagentApplicationDistributionPlugin : .plus(it.defaultJvmOpts ?: listOf()) it.inputs.files(javaagentConfiguration) // custom start script generator that replaces the placeholder - val agentFiles = javaagentConfiguration.map { configuration -> configuration.files } + val agentFiles: Provider> = javaagentConfiguration.map { configuration -> configuration.files } it.unixStartScriptGenerator = JavaagentAwareStartScriptGenerator(agentFiles, Platform.UNIX) it.windowsStartScriptGenerator = JavaagentAwareStartScriptGenerator(agentFiles, Platform.WINDOWS) } From ead01bd6f023bf21c3b69739e85d844f41c6fdd8 Mon Sep 17 00:00:00 2001 From: "John Y. Pazekha" <35344069+neboskreb@users.noreply.github.com> Date: Mon, 22 Dec 2025 17:53:22 +0100 Subject: [PATCH 12/37] Small fixes to the Windows branch (#298) Just a couple of minor fixes to add to the branch before its PR can proceed further --- plugin/build.gradle.kts | 1 + .../ryandens/javaagent/JavaagentPluginFunctionalTest.kt | 9 ++++++--- .../ryandens/javaagent/JavaForkOptionsConfigurer.java | 8 +++++++- 3 files changed, 14 insertions(+), 4 deletions(-) diff --git a/plugin/build.gradle.kts b/plugin/build.gradle.kts index f0c6e68..ff6072e 100644 --- a/plugin/build.gradle.kts +++ b/plugin/build.gradle.kts @@ -8,6 +8,7 @@ dependencies { testImplementation("org.jetbrains.kotlin:kotlin-test") testImplementation("org.jetbrains.kotlin:kotlin-test-junit") testImplementation("org.apache.commons:commons-compress:1.28.0") + testImplementation("org.apache.commons:commons-text:1.15.0") } gradlePlugin { diff --git a/plugin/src/functionalTest/kotlin/com/ryandens/javaagent/JavaagentPluginFunctionalTest.kt b/plugin/src/functionalTest/kotlin/com/ryandens/javaagent/JavaagentPluginFunctionalTest.kt index f78be5f..f8b9825 100644 --- a/plugin/src/functionalTest/kotlin/com/ryandens/javaagent/JavaagentPluginFunctionalTest.kt +++ b/plugin/src/functionalTest/kotlin/com/ryandens/javaagent/JavaagentPluginFunctionalTest.kt @@ -1,5 +1,6 @@ package com.ryandens.javaagent +import org.apache.commons.text.StringEscapeUtils import org.gradle.internal.jvm.Jvm import org.gradle.testkit.runner.BuildResult import org.gradle.testkit.runner.GradleRunner @@ -213,7 +214,7 @@ DEFAULT_JVM_OPTS="-javaagent:${"$"}APP_HOME/lib/simple-agent.jar -Xmx256m" Paths.get("..", "simple-agent").toFile().copyRecursively(simpleAgentTestDir) simpleAgentBuildScript.writeText( simpleAgentBuildScript.readText().replace( - "id(\"com.ryandens.java-conventions\")\n", + "id(\"com.ryandens.java-conventions\")", "", ), ) @@ -226,6 +227,8 @@ DEFAULT_JVM_OPTS="-javaagent:${"$"}APP_HOME/lib/simple-agent.jar -Xmx256m" """, ) + val commandLine = StringEscapeUtils.escapeJava(""".${File.separator}hello-world""") + val javaHome = StringEscapeUtils.escapeJava(Jvm.current().getJavaHome().toString()) helloWorldDir.resolve("build.gradle").writeText( """ plugins { @@ -251,8 +254,8 @@ DEFAULT_JVM_OPTS="-javaagent:${"$"}APP_HOME/lib/simple-agent.jar -Xmx256m" dependsOn('installDist') inputs.files(layout.buildDirectory.dir('install')) workingDir(layout.buildDirectory.dir('install').map { it.dir('hello-world').dir('bin') }) - commandLine '.${File.separator}hello-world' - environment JAVA_HOME: "${Jvm.current().getJavaHome()}" + commandLine '$commandLine' + environment JAVA_HOME: "$javaHome" } test { diff --git a/plugin/src/main/java/com/ryandens/javaagent/JavaForkOptionsConfigurer.java b/plugin/src/main/java/com/ryandens/javaagent/JavaForkOptionsConfigurer.java index 5912925..d197971 100644 --- a/plugin/src/main/java/com/ryandens/javaagent/JavaForkOptionsConfigurer.java +++ b/plugin/src/main/java/com/ryandens/javaagent/JavaForkOptionsConfigurer.java @@ -8,6 +8,7 @@ import java.util.Set; import java.util.stream.Collectors; import org.gradle.api.provider.Provider; +import org.gradle.internal.os.OperatingSystem; import org.gradle.process.CommandLineArgumentProvider; import org.gradle.process.JavaForkOptions; @@ -42,7 +43,12 @@ public Iterable asArguments() { .map( file -> { try { - return "-javaagent:" + file.getCanonicalPath(); + String path = file.getCanonicalPath(); + if (OperatingSystem.current().isWindows()) { + // Don't let the spaces in the Windows path break the command line + path = '"' + path + '"'; + } + return "-javaagent:" + path; } catch (IOException e) { throw new UncheckedIOException(e); } From 0d24570e920420ef8cd3b8b070aed82cd44d5b5c Mon Sep 17 00:00:00 2001 From: Ryan Dens Date: Mon, 22 Dec 2025 08:54:30 -0800 Subject: [PATCH 13/37] :art: fix formatting violations --- .../javaagent/JavaForkOptionsConfigurer.java | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/plugin/src/main/java/com/ryandens/javaagent/JavaForkOptionsConfigurer.java b/plugin/src/main/java/com/ryandens/javaagent/JavaForkOptionsConfigurer.java index d197971..fcc5386 100644 --- a/plugin/src/main/java/com/ryandens/javaagent/JavaForkOptionsConfigurer.java +++ b/plugin/src/main/java/com/ryandens/javaagent/JavaForkOptionsConfigurer.java @@ -43,12 +43,12 @@ public Iterable asArguments() { .map( file -> { try { - String path = file.getCanonicalPath(); - if (OperatingSystem.current().isWindows()) { - // Don't let the spaces in the Windows path break the command line - path = '"' + path + '"'; - } - return "-javaagent:" + path; + String path = file.getCanonicalPath(); + if (OperatingSystem.current().isWindows()) { + // Don't let the spaces in the Windows path break the command line + path = '"' + path + '"'; + } + return "-javaagent:" + path; } catch (IOException e) { throw new UncheckedIOException(e); } From 9f560a521168cbd9cc53fd7980f650954caa8655 Mon Sep 17 00:00:00 2001 From: "macroscopeapp[bot]" <170038800+macroscopeapp[bot]@users.noreply.github.com> Date: Mon, 6 Jul 2026 03:53:58 +0000 Subject: [PATCH 14/37] Remove manual Windows path quoting in JavaForkOptionsConfigurer.asArguments() --- .../com/ryandens/javaagent/JavaForkOptionsConfigurer.java | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/plugin/src/main/java/com/ryandens/javaagent/JavaForkOptionsConfigurer.java b/plugin/src/main/java/com/ryandens/javaagent/JavaForkOptionsConfigurer.java index fcc5386..5912925 100644 --- a/plugin/src/main/java/com/ryandens/javaagent/JavaForkOptionsConfigurer.java +++ b/plugin/src/main/java/com/ryandens/javaagent/JavaForkOptionsConfigurer.java @@ -8,7 +8,6 @@ import java.util.Set; import java.util.stream.Collectors; import org.gradle.api.provider.Provider; -import org.gradle.internal.os.OperatingSystem; import org.gradle.process.CommandLineArgumentProvider; import org.gradle.process.JavaForkOptions; @@ -43,12 +42,7 @@ public Iterable asArguments() { .map( file -> { try { - String path = file.getCanonicalPath(); - if (OperatingSystem.current().isWindows()) { - // Don't let the spaces in the Windows path break the command line - path = '"' + path + '"'; - } - return "-javaagent:" + path; + return "-javaagent:" + file.getCanonicalPath(); } catch (IOException e) { throw new UncheckedIOException(e); } From f828625ff04111bd13a18f984b895a0af364f1a6 Mon Sep 17 00:00:00 2001 From: "macroscopeapp[bot]" <170038800+macroscopeapp[bot]@users.noreply.github.com> Date: Mon, 6 Jul 2026 03:54:35 +0000 Subject: [PATCH 15/37] Fix Windows build by using gradlew.bat conditionally based on platform --- .github/workflows/gradle.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/gradle.yml b/.github/workflows/gradle.yml index 2c145a9..a7b72dd 100644 --- a/.github/workflows/gradle.yml +++ b/.github/workflows/gradle.yml @@ -32,4 +32,4 @@ jobs: cache-read-only: false - name: Execute Gradle build - run: ./gradlew build + run: ${{ matrix.platform == 'windows-latest' && 'gradlew.bat' || './gradlew' }} build From 1b281503b8a2a388d3d2c9dee387f3c634011518 Mon Sep 17 00:00:00 2001 From: "macroscopeapp[bot]" <170038800+macroscopeapp[bot]@users.noreply.github.com> Date: Mon, 6 Jul 2026 03:56:14 +0000 Subject: [PATCH 16/37] Fix Windows build by adding .\\ prefix to gradlew.bat in workflow --- .github/workflows/gradle.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/gradle.yml b/.github/workflows/gradle.yml index a7b72dd..90d5e3a 100644 --- a/.github/workflows/gradle.yml +++ b/.github/workflows/gradle.yml @@ -32,4 +32,4 @@ jobs: cache-read-only: false - name: Execute Gradle build - run: ${{ matrix.platform == 'windows-latest' && 'gradlew.bat' || './gradlew' }} build + run: ${{ matrix.platform == 'windows-latest' && '.\gradlew.bat' || './gradlew' }} build From f297e8503a9463c8976fe647a81b8dcccce5d967 Mon Sep 17 00:00:00 2001 From: "macroscopeapp[bot]" <170038800+macroscopeapp[bot]@users.noreply.github.com> Date: Mon, 6 Jul 2026 04:06:03 +0000 Subject: [PATCH 17/37] Restore Windows path quoting in JavaForkOptionsConfigurer to fix test failures --- .../com/ryandens/javaagent/JavaForkOptionsConfigurer.java | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/plugin/src/main/java/com/ryandens/javaagent/JavaForkOptionsConfigurer.java b/plugin/src/main/java/com/ryandens/javaagent/JavaForkOptionsConfigurer.java index 5912925..fcc5386 100644 --- a/plugin/src/main/java/com/ryandens/javaagent/JavaForkOptionsConfigurer.java +++ b/plugin/src/main/java/com/ryandens/javaagent/JavaForkOptionsConfigurer.java @@ -8,6 +8,7 @@ import java.util.Set; import java.util.stream.Collectors; import org.gradle.api.provider.Provider; +import org.gradle.internal.os.OperatingSystem; import org.gradle.process.CommandLineArgumentProvider; import org.gradle.process.JavaForkOptions; @@ -42,7 +43,12 @@ public Iterable asArguments() { .map( file -> { try { - return "-javaagent:" + file.getCanonicalPath(); + String path = file.getCanonicalPath(); + if (OperatingSystem.current().isWindows()) { + // Don't let the spaces in the Windows path break the command line + path = '"' + path + '"'; + } + return "-javaagent:" + path; } catch (IOException e) { throw new UncheckedIOException(e); } From 26af4e59da20ea55b1c8a5f6c2037fdd5c629d9c Mon Sep 17 00:00:00 2001 From: "macroscopeapp[bot]" <170038800+macroscopeapp[bot]@users.noreply.github.com> Date: Mon, 6 Jul 2026 04:16:15 +0000 Subject: [PATCH 18/37] Fix Windows functional tests to use correct script paths and line endings --- .../JavaagentJibExtensionFunctionalTest.kt | 6 +-- .../JavaagentPluginFunctionalTest.kt | 37 +++++++++++-------- 2 files changed, 24 insertions(+), 19 deletions(-) diff --git a/jib/src/functionalTest/kotlin/JavaagentJibExtensionFunctionalTest.kt b/jib/src/functionalTest/kotlin/JavaagentJibExtensionFunctionalTest.kt index dd5d4b5..f450896 100644 --- a/jib/src/functionalTest/kotlin/JavaagentJibExtensionFunctionalTest.kt +++ b/jib/src/functionalTest/kotlin/JavaagentJibExtensionFunctionalTest.kt @@ -91,14 +91,14 @@ class JavaagentJibExtensionFunctionalTest { buildArgs: List, ): BuildResult { val helloWorldDir = File(functionalTestDir, "hello-world") - File("src/functionalTest/resources/hello-world-project/").copyRecursively(helloWorldDir) + File("src${File.separator}functionalTest${File.separator}resources${File.separator}hello-world-project").copyRecursively(helloWorldDir) val simpleAgentTestDir = File(functionalTestDir, "simple-agent") val simpleAgentBuildScript = simpleAgentTestDir.resolve("build.gradle.kts") - File("../simple-agent/").copyRecursively(simpleAgentTestDir) + File("..${File.separator}simple-agent").copyRecursively(simpleAgentTestDir) // remove a constant from the above file simpleAgentBuildScript.writeText( simpleAgentBuildScript.readText().replace( - "id(\"com.ryandens.java-conventions\")\n", + "id(\"com.ryandens.java-conventions\")", "", ), ) diff --git a/plugin/src/functionalTest/kotlin/com/ryandens/javaagent/JavaagentPluginFunctionalTest.kt b/plugin/src/functionalTest/kotlin/com/ryandens/javaagent/JavaagentPluginFunctionalTest.kt index f8b9825..8aadb01 100644 --- a/plugin/src/functionalTest/kotlin/com/ryandens/javaagent/JavaagentPluginFunctionalTest.kt +++ b/plugin/src/functionalTest/kotlin/com/ryandens/javaagent/JavaagentPluginFunctionalTest.kt @@ -2,6 +2,7 @@ package com.ryandens.javaagent import org.apache.commons.text.StringEscapeUtils import org.gradle.internal.jvm.Jvm +import org.gradle.internal.os.OperatingSystem import org.gradle.testkit.runner.BuildResult import org.gradle.testkit.runner.GradleRunner import org.gradle.testkit.runner.TaskOutcome @@ -25,6 +26,10 @@ class JavaagentPluginFunctionalTest { private lateinit var helloWorldDir: File + private val isWindows = OperatingSystem.current().isWindows + + private val scriptExtension = if (isWindows) ".bat" else "" + @BeforeTest fun beforeEach() { functionalTestDir = File("build", "functionalTest") @@ -127,21 +132,17 @@ class JavaagentPluginFunctionalTest { assertTrue(applicationDistribution.exists()) // verify the expected text was injected into the start script - val expectedDefaultJavaOpts = """ -DEFAULT_JVM_OPTS="-javaagent:${"$"}APP_HOME/agent-libs/simple-agent.jar -Xmx256m" -""" val applicationDistributionScript = - File(functionalTestDir, "hello-world${File.separator}build${File.separator}scripts${File.separator}hello-world") - assertTrue(applicationDistributionScript.readText().contains(expectedDefaultJavaOpts)) - - /* - * TODO add support for windows - * val expectedWindowsDefaultJvmOpts = """ -DEFAULT_JVM_OPTS="-javaagent:${"$"}APP_HOME/lib/simple-agent.jar -Xmx256m" + File(functionalTestDir, "hello-world${File.separator}build${File.separator}scripts${File.separator}hello-world$scriptExtension") + if (isWindows) { + val expectedWindowsDefaultJvmOpts = """set DEFAULT_JVM_OPTS="-javaagent:%APP_HOME%\agent-libs\simple-agent.jar" "-Xmx256m"""" + assertTrue(applicationDistributionScript.readText().contains(expectedWindowsDefaultJvmOpts)) + } else { + val expectedDefaultJavaOpts = """ +DEFAULT_JVM_OPTS="-javaagent:${"$"}APP_HOME/agent-libs/simple-agent.jar -Xmx256m" """ - val applicationDistributionScript = File(functionalTestDir, "hello-world/build/scripts/hello-world.bat") - assertTrue(applicationDistributionScript.readText().contains(expectedWindowsDefaultJvmOpts)) - */ + assertTrue(applicationDistributionScript.readText().contains(expectedDefaultJavaOpts)) + } // verify the agent was added to the /lib/ dir of the distribution assertTrue( @@ -194,8 +195,12 @@ DEFAULT_JVM_OPTS="-javaagent:${"$"}APP_HOME/lib/simple-agent.jar -Xmx256m" assertTrue(applicationDistribution.exists()) val applicationDistributionScript = - File(functionalTestDir, "hello-world${File.separator}build${File.separator}scripts${File.separator}hello-world") - assertTrue(applicationDistributionScript.readText().contains("""DEFAULT_JVM_OPTS="-Xmx256m""")) + File(functionalTestDir, "hello-world${File.separator}build${File.separator}scripts${File.separator}hello-world$scriptExtension") + if (isWindows) { + assertTrue(applicationDistributionScript.readText().contains("""set DEFAULT_JVM_OPTS="-Xmx256m"""")) + } else { + assertTrue(applicationDistributionScript.readText().contains("""DEFAULT_JVM_OPTS="-Xmx256m""")) + } assertFalse( File( @@ -227,7 +232,7 @@ DEFAULT_JVM_OPTS="-javaagent:${"$"}APP_HOME/lib/simple-agent.jar -Xmx256m" """, ) - val commandLine = StringEscapeUtils.escapeJava(""".${File.separator}hello-world""") + val commandLine = StringEscapeUtils.escapeJava(""".${File.separator}hello-world$scriptExtension""") val javaHome = StringEscapeUtils.escapeJava(Jvm.current().getJavaHome().toString()) helloWorldDir.resolve("build.gradle").writeText( """ From 6cd7d5bcc7f7b9b88835bb8980b8a07c668d26d0 Mon Sep 17 00:00:00 2001 From: "macroscopeapp[bot]" <170038800+macroscopeapp[bot]@users.noreply.github.com> Date: Mon, 6 Jul 2026 04:18:13 +0000 Subject: [PATCH 19/37] Fix spotless formatting violation in JavaagentJibExtensionFunctionalTest.kt --- .../kotlin/JavaagentJibExtensionFunctionalTest.kt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/jib/src/functionalTest/kotlin/JavaagentJibExtensionFunctionalTest.kt b/jib/src/functionalTest/kotlin/JavaagentJibExtensionFunctionalTest.kt index f450896..c8aa543 100644 --- a/jib/src/functionalTest/kotlin/JavaagentJibExtensionFunctionalTest.kt +++ b/jib/src/functionalTest/kotlin/JavaagentJibExtensionFunctionalTest.kt @@ -91,7 +91,9 @@ class JavaagentJibExtensionFunctionalTest { buildArgs: List, ): BuildResult { val helloWorldDir = File(functionalTestDir, "hello-world") - File("src${File.separator}functionalTest${File.separator}resources${File.separator}hello-world-project").copyRecursively(helloWorldDir) + File( + "src${File.separator}functionalTest${File.separator}resources${File.separator}hello-world-project", + ).copyRecursively(helloWorldDir) val simpleAgentTestDir = File(functionalTestDir, "simple-agent") val simpleAgentBuildScript = simpleAgentTestDir.resolve("build.gradle.kts") File("..${File.separator}simple-agent").copyRecursively(simpleAgentTestDir) From 3a81f206b955fd2a66b18e8126855b2c6a9ad266 Mon Sep 17 00:00:00 2001 From: "macroscopeapp[bot]" <170038800+macroscopeapp[bot]@users.noreply.github.com> Date: Mon, 6 Jul 2026 04:28:23 +0000 Subject: [PATCH 20/37] Fix Windows start script generation to use %APP_HOME% instead of $APP_HOME --- .../ryandens/javaagent/JavaagentAwareStartScriptGenerator.kt | 5 +++-- plugin/src/main/kotlin/com/ryandens/javaagent/Platform.kt | 5 +++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/plugin/src/main/kotlin/com/ryandens/javaagent/JavaagentAwareStartScriptGenerator.kt b/plugin/src/main/kotlin/com/ryandens/javaagent/JavaagentAwareStartScriptGenerator.kt index 5658eae..71d9e1b 100644 --- a/plugin/src/main/kotlin/com/ryandens/javaagent/JavaagentAwareStartScriptGenerator.kt +++ b/plugin/src/main/kotlin/com/ryandens/javaagent/JavaagentAwareStartScriptGenerator.kt @@ -23,7 +23,7 @@ class JavaagentAwareStartScriptGenerator( details: JavaAppStartScriptGenerationDetails, destination: Writer, ) { - inner.generateScript(details, Fake(destination, javaagentConfiguration, platform.pathSeparator)) + inner.generateScript(details, Fake(destination, javaagentConfiguration, platform.pathSeparator, platform.appHomeVar)) } private class FakeTransformer( @@ -47,6 +47,7 @@ class JavaagentAwareStartScriptGenerator( private val inner: Writer, private val javaagentFiles: Provider>, private val pathSeparator: String, + private val appHomeVar: String, ) : Writer() { override fun close() { inner.close() @@ -79,7 +80,7 @@ class JavaagentAwareStartScriptGenerator( "-javaagent:COM_RYANDENS_JAVAAGENTS_PLACEHOLDER.jar", javaagentFiles.get().joinToString( " ", - ) { jar -> "-javaagent:\$APP_HOME${pathSeparator}agent-libs${pathSeparator}${jar.name}" }, + ) { jar -> "-javaagent:${appHomeVar}${pathSeparator}agent-libs${pathSeparator}${jar.name}" }, ) } super.write(replace) diff --git a/plugin/src/main/kotlin/com/ryandens/javaagent/Platform.kt b/plugin/src/main/kotlin/com/ryandens/javaagent/Platform.kt index 3bd7983..5df2a19 100644 --- a/plugin/src/main/kotlin/com/ryandens/javaagent/Platform.kt +++ b/plugin/src/main/kotlin/com/ryandens/javaagent/Platform.kt @@ -8,9 +8,10 @@ import org.gradle.api.resources.TextResource enum class Platform( val lineSeparator: String, val pathSeparator: String, + val appHomeVar: String, val templateBindingFactory: StartScriptTemplateBindingFactory, val template: TextResource, ) { - UNIX("\n", "/", StartScriptTemplateBindingFactory.unix(), UnixStartScriptGenerator().template), - WINDOWS("\r\n", "\\", StartScriptTemplateBindingFactory.windows(), WindowsStartScriptGenerator().template), + UNIX("\n", "/", "\$APP_HOME", StartScriptTemplateBindingFactory.unix(), UnixStartScriptGenerator().template), + WINDOWS("\r\n", "\\", "%APP_HOME%", StartScriptTemplateBindingFactory.windows(), WindowsStartScriptGenerator().template), } From 10914f374a1e80b69bc97cd331c4696eafbdebb2 Mon Sep 17 00:00:00 2001 From: Ryan Dens Date: Sun, 5 Jul 2026 22:05:54 -0700 Subject: [PATCH 21/37] Run Windows start script through cmd /c in execStartScript functional test On Windows a `.bat` file is not a directly executable image, so Gradle's Exec task (which uses ProcessBuilder/CreateProcess) fails to launch it with "A problem occurred starting process 'command '.\hello-world.bat''". Route the generated distribution start script through the command interpreter (`cmd /c hello-world.bat`) on Windows while keeping `./hello-world` on Unix. Fixes the 3 execStartScript-based functional tests on windows-latest. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../javaagent/JavaagentPluginFunctionalTest.kt | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/plugin/src/functionalTest/kotlin/com/ryandens/javaagent/JavaagentPluginFunctionalTest.kt b/plugin/src/functionalTest/kotlin/com/ryandens/javaagent/JavaagentPluginFunctionalTest.kt index 8aadb01..d5eb6d8 100644 --- a/plugin/src/functionalTest/kotlin/com/ryandens/javaagent/JavaagentPluginFunctionalTest.kt +++ b/plugin/src/functionalTest/kotlin/com/ryandens/javaagent/JavaagentPluginFunctionalTest.kt @@ -232,7 +232,15 @@ DEFAULT_JVM_OPTS="-javaagent:${"$"}APP_HOME/agent-libs/simple-agent.jar -Xmx256m """, ) - val commandLine = StringEscapeUtils.escapeJava(""".${File.separator}hello-world$scriptExtension""") + // On Windows a `.bat` file is not a directly executable image, so it cannot be launched via + // ProcessBuilder/CreateProcess (which is what Gradle's Exec task uses). It must be run through the + // command interpreter instead, e.g. `cmd /c hello-world.bat`. + val execCommandLine = + if (isWindows) { + "'cmd', '/c', 'hello-world.bat'" + } else { + "'./hello-world'" + } val javaHome = StringEscapeUtils.escapeJava(Jvm.current().getJavaHome().toString()) helloWorldDir.resolve("build.gradle").writeText( """ @@ -259,7 +267,7 @@ DEFAULT_JVM_OPTS="-javaagent:${"$"}APP_HOME/agent-libs/simple-agent.jar -Xmx256m dependsOn('installDist') inputs.files(layout.buildDirectory.dir('install')) workingDir(layout.buildDirectory.dir('install').map { it.dir('hello-world').dir('bin') }) - commandLine '$commandLine' + commandLine $execCommandLine environment JAVA_HOME: "$javaHome" } From 7367316adf621a92841a3846f45a6af28fe59b87 Mon Sep 17 00:00:00 2001 From: Ryan Dens Date: Sun, 5 Jul 2026 22:18:42 -0700 Subject: [PATCH 22/37] Keep JVM opts as separate quoted tokens in Windows start script The Unix start script relies on shell word-splitting of the unquoted $DEFAULT_JVM_OPTS, so FakeTransformer collapses the individually-quoted JVM opts into one space-separated string. On Windows the batch script does not word-split inside quotes, so collapsing them glued `-Xmx256m` onto the `-javaagent:` path, producing an invalid argument and making `java` (and thus execStartScript) exit non-zero for distributions with an agent. Skip the collapse on Windows so each opt stays its own quoted token, e.g. set DEFAULT_JVM_OPTS="-javaagent:%APP_HOME%\agent-libs\x.jar" "-Xmx256m" Fixes the two agent-bearing execStartScript functional tests on windows-latest. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../JavaagentAwareStartScriptGenerator.kt | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/plugin/src/main/kotlin/com/ryandens/javaagent/JavaagentAwareStartScriptGenerator.kt b/plugin/src/main/kotlin/com/ryandens/javaagent/JavaagentAwareStartScriptGenerator.kt index 71d9e1b..dd2e7b3 100644 --- a/plugin/src/main/kotlin/com/ryandens/javaagent/JavaagentAwareStartScriptGenerator.kt +++ b/plugin/src/main/kotlin/com/ryandens/javaagent/JavaagentAwareStartScriptGenerator.kt @@ -15,7 +15,7 @@ class JavaagentAwareStartScriptGenerator( private val inner: ScriptGenerator = DefaultTemplateBasedStartScriptGenerator( platform.lineSeparator, - FakeTransformer(platform.templateBindingFactory), + FakeTransformer(platform.templateBindingFactory, platform), platform.template, ), ) : ScriptGenerator { @@ -28,6 +28,7 @@ class JavaagentAwareStartScriptGenerator( private class FakeTransformer( private val inner: StartScriptTemplateBindingFactory, + private val platform: Platform, ) : Transformer, JavaAppStartScriptGenerationDetails> by inner { override fun transform(`in`: JavaAppStartScriptGenerationDetails): MutableMap { val result = inner.transform(`in`) @@ -38,7 +39,16 @@ class JavaagentAwareStartScriptGenerator( } else { jvmOpts } - result["defaultJvmOpts"] = trimmedJvmOpts.replace("\" \"", " ") + // On Unix the individually-quoted JVM opts are collapsed into a single space-separated string, which the + // shell then word-splits back apart when the unquoted $DEFAULT_JVM_OPTS is expanded. On Windows the batch + // script does not word-split inside quotes, so each opt must remain its own quoted token; collapsing them + // would glue e.g. `-Xmx256m` onto the `-javaagent:` path and produce an invalid argument. + result["defaultJvmOpts"] = + if (platform == Platform.WINDOWS) { + trimmedJvmOpts + } else { + trimmedJvmOpts.replace("\" \"", " ") + } return result } } From f662f09ef73131f46a5a5651e9c441e708b1b5b6 Mon Sep 17 00:00:00 2001 From: "macroscopeapp[bot]" <170038800+macroscopeapp[bot]@users.noreply.github.com> Date: Mon, 6 Jul 2026 05:24:44 +0000 Subject: [PATCH 23/37] Fix Windows batch script: use separate quoted tokens for multiple javaagent args --- .../javaagent/JavaagentAwareStartScriptGenerator.kt | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/plugin/src/main/kotlin/com/ryandens/javaagent/JavaagentAwareStartScriptGenerator.kt b/plugin/src/main/kotlin/com/ryandens/javaagent/JavaagentAwareStartScriptGenerator.kt index dd2e7b3..9d8f6dd 100644 --- a/plugin/src/main/kotlin/com/ryandens/javaagent/JavaagentAwareStartScriptGenerator.kt +++ b/plugin/src/main/kotlin/com/ryandens/javaagent/JavaagentAwareStartScriptGenerator.kt @@ -23,7 +23,7 @@ class JavaagentAwareStartScriptGenerator( details: JavaAppStartScriptGenerationDetails, destination: Writer, ) { - inner.generateScript(details, Fake(destination, javaagentConfiguration, platform.pathSeparator, platform.appHomeVar)) + inner.generateScript(details, Fake(destination, javaagentConfiguration, platform.pathSeparator, platform.appHomeVar, platform)) } private class FakeTransformer( @@ -58,6 +58,7 @@ class JavaagentAwareStartScriptGenerator( private val javaagentFiles: Provider>, private val pathSeparator: String, private val appHomeVar: String, + private val platform: Platform, ) : Writer() { override fun close() { inner.close() @@ -86,10 +87,14 @@ class JavaagentAwareStartScriptGenerator( "", ).replace("-javaagent:COM_RYANDENS_JAVAAGENTS_PLACEHOLDER.jar", "") } else { + // On Windows, each -javaagent option must be a separately quoted token, + // so we use `" "` (close quote, space, open quote) as separator. + // On Unix, all options can be space-separated within a single quoted string. + val separator = if (platform == Platform.WINDOWS) "\" \"" else " " str.replace( "-javaagent:COM_RYANDENS_JAVAAGENTS_PLACEHOLDER.jar", javaagentFiles.get().joinToString( - " ", + separator, ) { jar -> "-javaagent:${appHomeVar}${pathSeparator}agent-libs${pathSeparator}${jar.name}" }, ) } From 8dc638cfdd86698a5af22c5d0b5828cb230d107e Mon Sep 17 00:00:00 2001 From: Ryan Dens Date: Sun, 5 Jul 2026 22:39:18 -0700 Subject: [PATCH 24/37] Strip quoted placeholder token in Windows start script when no agents The no-agent removal relied on the Unix opt-collapsing to turn the placeholder into an unquoted `-javaagent:...PLACEHOLDER.jar ` substring. On Windows each opt stays its own quoted token, so removing only the unquoted placeholder left `DEFAULT_JVM_OPTS="" "-Xmx256m"`, and the empty first argument made `java` (execStartScript) fail. Also strip the surrounding quotes and trailing space of the Windows-style `"-javaagent:...PLACEHOLDER.jar"` token so an agent-free distribution produces `set DEFAULT_JVM_OPTS="-Xmx256m"`. Unix output is unchanged. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../JavaagentAwareStartScriptGenerator.kt | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/plugin/src/main/kotlin/com/ryandens/javaagent/JavaagentAwareStartScriptGenerator.kt b/plugin/src/main/kotlin/com/ryandens/javaagent/JavaagentAwareStartScriptGenerator.kt index 9d8f6dd..d98ba3a 100644 --- a/plugin/src/main/kotlin/com/ryandens/javaagent/JavaagentAwareStartScriptGenerator.kt +++ b/plugin/src/main/kotlin/com/ryandens/javaagent/JavaagentAwareStartScriptGenerator.kt @@ -80,12 +80,16 @@ class JavaagentAwareStartScriptGenerator( val files = javaagentFiles.get() val replace = if (files.isEmpty()) { - // handles case gracefully where there is a trailing space that needs to be removed if ogther default jvm opts are supplied + // Remove the placeholder opt when there are no agents. On Windows each opt is its own quoted token + // (`"-javaagent:...PLACEHOLDER.jar" "-Xmx256m"`), so the surrounding quotes and trailing space must + // be stripped too; on Unix the opts are collapsed into a single quoted string + // (`"-javaagent:...PLACEHOLDER.jar -Xmx256m"`), so only the unquoted placeholder and its trailing + // space are removed. The trailing-space variants run first to avoid leaving a dangling separator. str - .replace( - "-javaagent:COM_RYANDENS_JAVAAGENTS_PLACEHOLDER.jar ", - "", - ).replace("-javaagent:COM_RYANDENS_JAVAAGENTS_PLACEHOLDER.jar", "") + .replace("\"-javaagent:COM_RYANDENS_JAVAAGENTS_PLACEHOLDER.jar\" ", "") + .replace("\"-javaagent:COM_RYANDENS_JAVAAGENTS_PLACEHOLDER.jar\"", "") + .replace("-javaagent:COM_RYANDENS_JAVAAGENTS_PLACEHOLDER.jar ", "") + .replace("-javaagent:COM_RYANDENS_JAVAAGENTS_PLACEHOLDER.jar", "") } else { // On Windows, each -javaagent option must be a separately quoted token, // so we use `" "` (close quote, space, open quote) as separator. From 91b18519cffbf36f1d204cdf1fc5d9d901a0789e Mon Sep 17 00:00:00 2001 From: Ryan Dens Date: Sun, 5 Jul 2026 23:11:01 -0700 Subject: [PATCH 25/37] TEMP: enable showStandardStreams to diagnose Windows OTel agent output --- plugin/build.gradle.kts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/plugin/build.gradle.kts b/plugin/build.gradle.kts index ff6072e..b2001d0 100644 --- a/plugin/build.gradle.kts +++ b/plugin/build.gradle.kts @@ -67,4 +67,8 @@ gradlePlugin { tasks.functionalTest { inputs.file(layout.projectDirectory.dir("../simple-agent/build.gradle.kts")) inputs.files(layout.projectDirectory.dir("../simple-agent/src/")) + testLogging { + showStandardStreams = true + exceptionFormat = org.gradle.api.tasks.testing.logging.TestExceptionFormat.FULL + } } From be076b658a5c5eb25e016187e550d0a1dd6b89d4 Mon Sep 17 00:00:00 2001 From: Ryan Dens Date: Sun, 5 Jul 2026 23:19:24 -0700 Subject: [PATCH 26/37] Don't quote javaagent path for forked JVMs (fixes OTel agent on Windows) The Windows-only quoting in JavaForkOptionsConfigurer wrapped the agent path in literal double quotes. The JVM launches fine (the Windows C runtime strips the outer quotes when parsing the command line), but agents that re-derive their own jar location get the still-quoted path: the OpenTelemetry agent's premain throws IllegalArgumentException from appendToBootstrapClassLoaderSearch, so its VersionLogger line never prints and the two run/test functional tests fail on windows-latest. The quoting is also unnecessary: these arguments are passed straight to a forked JVM via ProcessBuilder (no shell), which already quotes arguments containing spaces. Drop the manual quoting for all platforms. Also reverts the temporary showStandardStreams diagnostic logging. Co-Authored-By: Claude Opus 4.8 (1M context) --- plugin/build.gradle.kts | 4 ---- .../javaagent/JavaForkOptionsConfigurer.java | 15 ++++++++------- 2 files changed, 8 insertions(+), 11 deletions(-) diff --git a/plugin/build.gradle.kts b/plugin/build.gradle.kts index b2001d0..ff6072e 100644 --- a/plugin/build.gradle.kts +++ b/plugin/build.gradle.kts @@ -67,8 +67,4 @@ gradlePlugin { tasks.functionalTest { inputs.file(layout.projectDirectory.dir("../simple-agent/build.gradle.kts")) inputs.files(layout.projectDirectory.dir("../simple-agent/src/")) - testLogging { - showStandardStreams = true - exceptionFormat = org.gradle.api.tasks.testing.logging.TestExceptionFormat.FULL - } } diff --git a/plugin/src/main/java/com/ryandens/javaagent/JavaForkOptionsConfigurer.java b/plugin/src/main/java/com/ryandens/javaagent/JavaForkOptionsConfigurer.java index fcc5386..c2c6f0c 100644 --- a/plugin/src/main/java/com/ryandens/javaagent/JavaForkOptionsConfigurer.java +++ b/plugin/src/main/java/com/ryandens/javaagent/JavaForkOptionsConfigurer.java @@ -8,7 +8,6 @@ import java.util.Set; import java.util.stream.Collectors; import org.gradle.api.provider.Provider; -import org.gradle.internal.os.OperatingSystem; import org.gradle.process.CommandLineArgumentProvider; import org.gradle.process.JavaForkOptions; @@ -43,12 +42,14 @@ public Iterable asArguments() { .map( file -> { try { - String path = file.getCanonicalPath(); - if (OperatingSystem.current().isWindows()) { - // Don't let the spaces in the Windows path break the command line - path = '"' + path + '"'; - } - return "-javaagent:" + path; + // Do not quote the path to guard against spaces. These arguments go + // straight + // to a forked JVM via ProcessBuilder (no shell), which already quotes + // arguments containing spaces. Literal quotes would remain in the agent + // path + // and break agents that re-derive their own jar location, e.g. the + // OpenTelemetry agent fails to append its bootstrap jar to the classloader. + return "-javaagent:" + file.getCanonicalPath(); } catch (IOException e) { throw new UncheckedIOException(e); } From a8339af89a6b65e274455b3e5ed1274c0cbf31e6 Mon Sep 17 00:00:00 2001 From: Ryan Dens Date: Sun, 5 Jul 2026 23:31:04 -0700 Subject: [PATCH 27/37] TEMP: re-enable showStandardStreams to re-check Windows OTel output --- plugin/build.gradle.kts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/plugin/build.gradle.kts b/plugin/build.gradle.kts index ff6072e..b2001d0 100644 --- a/plugin/build.gradle.kts +++ b/plugin/build.gradle.kts @@ -67,4 +67,8 @@ gradlePlugin { tasks.functionalTest { inputs.file(layout.projectDirectory.dir("../simple-agent/build.gradle.kts")) inputs.files(layout.projectDirectory.dir("../simple-agent/src/")) + testLogging { + showStandardStreams = true + exceptionFormat = org.gradle.api.tasks.testing.logging.TestExceptionFormat.FULL + } } From eccb1692dac7f3e2c35b97e105b2e40209740073 Mon Sep 17 00:00:00 2001 From: Ryan Dens Date: Sun, 5 Jul 2026 23:46:43 -0700 Subject: [PATCH 28/37] Bump OpenTelemetry agent to 1.30.0 in functional tests for Windows OpenTelemetry javaagent 1.11.1 fails to load on Windows: its premain throws IllegalArgumentException from appendToBootstrapClassLoaderSearch (an old bug in how it locates its own bootstrap jar), so it never logs its version banner. The `can attach to test task` and `can attach two agents` tests assert on that banner, so they failed on windows-latest. Bump those tests to 1.30.0, which logs its version banner on Windows -- the same version the `can handle upgrade` distribution test already exercises successfully there. Also removes the temporary showStandardStreams diagnostic logging. Co-Authored-By: Claude Opus 4.8 (1M context) --- plugin/build.gradle.kts | 4 ---- .../javaagent/JavaagentPluginFunctionalTest.kt | 10 ++++++++-- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/plugin/build.gradle.kts b/plugin/build.gradle.kts index b2001d0..ff6072e 100644 --- a/plugin/build.gradle.kts +++ b/plugin/build.gradle.kts @@ -67,8 +67,4 @@ gradlePlugin { tasks.functionalTest { inputs.file(layout.projectDirectory.dir("../simple-agent/build.gradle.kts")) inputs.files(layout.projectDirectory.dir("../simple-agent/src/")) - testLogging { - showStandardStreams = true - exceptionFormat = org.gradle.api.tasks.testing.logging.TestExceptionFormat.FULL - } } diff --git a/plugin/src/functionalTest/kotlin/com/ryandens/javaagent/JavaagentPluginFunctionalTest.kt b/plugin/src/functionalTest/kotlin/com/ryandens/javaagent/JavaagentPluginFunctionalTest.kt index d5eb6d8..c36a1bf 100644 --- a/plugin/src/functionalTest/kotlin/com/ryandens/javaagent/JavaagentPluginFunctionalTest.kt +++ b/plugin/src/functionalTest/kotlin/com/ryandens/javaagent/JavaagentPluginFunctionalTest.kt @@ -76,7 +76,10 @@ class JavaagentPluginFunctionalTest { } @Test fun `can attach to test task`() { - val otelVersion = "1.11.1" + // Use an OpenTelemetry agent recent enough to load correctly on Windows. Older releases (e.g. 1.11.1) + // throw IllegalArgumentException from appendToBootstrapClassLoaderSearch on Windows and never log their + // version banner, which this test asserts on. + val otelVersion = "1.30.0" val dependencies = """ javaagent project(':simple-agent') testJavaagent 'io.opentelemetry.javaagent:opentelemetry-javaagent:$otelVersion' @@ -99,7 +102,10 @@ class JavaagentPluginFunctionalTest { } @Test fun `can attach two agents to application run task`() { - val otelVersion = "1.11.1" + // Use an OpenTelemetry agent recent enough to load correctly on Windows. Older releases (e.g. 1.11.1) + // throw IllegalArgumentException from appendToBootstrapClassLoaderSearch on Windows and never log their + // version banner, which this test asserts on. + val otelVersion = "1.30.0" val dependencies = """ javaagent project(':simple-agent') javaagent 'io.opentelemetry.javaagent:opentelemetry-javaagent:$otelVersion' From 67a0d85f8c9befafc14f10dab5d2e38df938de94 Mon Sep 17 00:00:00 2001 From: Ryan Dens Date: Sun, 5 Jul 2026 23:59:02 -0700 Subject: [PATCH 29/37] Make run task depend on javaagent artifacts to fix build-ordering race JavaagentApplicationRunPlugin configured the run task's -javaagent args via a CommandLineArgumentProvider but never registered the javaagent configuration as a task input, so Gradle established no dependency on the tasks that build the agent jars. Under parallel execution the run task could launch with -javaagent:simple-agent.jar before :simple-agent:jar had run, failing with "Error opening zip file or JAR manifest missing" (reproducible 100% with --rerun-tasks, and intermittently on CI). Register the configuration as an input (a Configuration is Buildable), the same way JavaagentApplicationDistributionPlugin already does, so the agent jars are built before the run task launches. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../com/ryandens/javaagent/JavaagentApplicationRunPlugin.kt | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/plugin/src/main/kotlin/com/ryandens/javaagent/JavaagentApplicationRunPlugin.kt b/plugin/src/main/kotlin/com/ryandens/javaagent/JavaagentApplicationRunPlugin.kt index 0c64570..b036a40 100644 --- a/plugin/src/main/kotlin/com/ryandens/javaagent/JavaagentApplicationRunPlugin.kt +++ b/plugin/src/main/kotlin/com/ryandens/javaagent/JavaagentApplicationRunPlugin.kt @@ -21,6 +21,10 @@ class JavaagentApplicationRunPlugin : ) { // configure the run task to use the `javaagent` flag pointing to the dependency stored in the local Maven repository project.tasks.named(ApplicationPlugin.TASK_RUN_NAME, JavaExec::class.java).configure { + // Register the configuration as an input so the run task depends on the tasks that build the + // javaagent artifacts (a Configuration is Buildable). Without this the agent jar can be launched + // via -javaagent before it has been produced, failing with "Error opening zip file". + it.inputs.files(javaagentConfiguration) JavaForkOptionsConfigurer.configureJavaForkOptions(it, javaagentConfiguration.map { configuration -> configuration.files }) } } From 884b1bdd6f43ea2a023a190c9cc2b8d8e9522df6 Mon Sep 17 00:00:00 2001 From: Ryan Dens Date: Mon, 6 Jul 2026 00:08:38 -0700 Subject: [PATCH 30/37] Revert run task dependsOn change (fix build-ordering race in a separate PR) The inputs.files/dependsOn wiring added to JavaagentApplicationRunPlugin to fix the run-task build-ordering race also surfaced a pre-existing missing dependency in the otel example (:app:run consuming :app:extendedAgent output without a declared dependency), failing :otel:functionalTest validation. Revert to keep this PR focused on Windows support; the run-task ordering race will be addressed separately. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../com/ryandens/javaagent/JavaagentApplicationRunPlugin.kt | 4 ---- 1 file changed, 4 deletions(-) diff --git a/plugin/src/main/kotlin/com/ryandens/javaagent/JavaagentApplicationRunPlugin.kt b/plugin/src/main/kotlin/com/ryandens/javaagent/JavaagentApplicationRunPlugin.kt index b036a40..0c64570 100644 --- a/plugin/src/main/kotlin/com/ryandens/javaagent/JavaagentApplicationRunPlugin.kt +++ b/plugin/src/main/kotlin/com/ryandens/javaagent/JavaagentApplicationRunPlugin.kt @@ -21,10 +21,6 @@ class JavaagentApplicationRunPlugin : ) { // configure the run task to use the `javaagent` flag pointing to the dependency stored in the local Maven repository project.tasks.named(ApplicationPlugin.TASK_RUN_NAME, JavaExec::class.java).configure { - // Register the configuration as an input so the run task depends on the tasks that build the - // javaagent artifacts (a Configuration is Buildable). Without this the agent jar can be launched - // via -javaagent before it has been produced, failing with "Error opening zip file". - it.inputs.files(javaagentConfiguration) JavaForkOptionsConfigurer.configureJavaForkOptions(it, javaagentConfiguration.map { configuration -> configuration.files }) } } From f92cb4347d43f32c5da1a08e3d83602fc4fd1837 Mon Sep 17 00:00:00 2001 From: Ryan Dens Date: Mon, 6 Jul 2026 00:25:32 -0700 Subject: [PATCH 31/37] :arrow_up: test with newer version of OTel agent --- example-projects/buildSrc/build.gradle.kts | 4 ++-- .../javaagent/JavaagentPluginFunctionalTest.kt | 12 ++++++------ 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/example-projects/buildSrc/build.gradle.kts b/example-projects/buildSrc/build.gradle.kts index d943606..3998a9f 100644 --- a/example-projects/buildSrc/build.gradle.kts +++ b/example-projects/buildSrc/build.gradle.kts @@ -12,6 +12,6 @@ dependencies { implementation("com.ryandens:plugin:0.11.0") */ implementation("com.gradleup.shadow:com.gradleup.shadow.gradle.plugin:9.4.3") - implementation("io.opentelemetry.instrumentation.muzzle-generation:io.opentelemetry.instrumentation.muzzle-generation.gradle.plugin:2.27.0-alpha") - implementation("io.opentelemetry.instrumentation.muzzle-check:io.opentelemetry.instrumentation.muzzle-check.gradle.plugin:2.27.0-alpha") + implementation("io.opentelemetry.instrumentation.muzzle-generation:io.opentelemetry.instrumentation.muzzle-generation.gradle.plugin:2.29.0-alpha") + implementation("io.opentelemetry.instrumentation.muzzle-check:io.opentelemetry.instrumentation.muzzle-check.gradle.plugin:2.29.0-alpha") } diff --git a/plugin/src/functionalTest/kotlin/com/ryandens/javaagent/JavaagentPluginFunctionalTest.kt b/plugin/src/functionalTest/kotlin/com/ryandens/javaagent/JavaagentPluginFunctionalTest.kt index c36a1bf..6a84f35 100644 --- a/plugin/src/functionalTest/kotlin/com/ryandens/javaagent/JavaagentPluginFunctionalTest.kt +++ b/plugin/src/functionalTest/kotlin/com/ryandens/javaagent/JavaagentPluginFunctionalTest.kt @@ -79,7 +79,7 @@ class JavaagentPluginFunctionalTest { // Use an OpenTelemetry agent recent enough to load correctly on Windows. Older releases (e.g. 1.11.1) // throw IllegalArgumentException from appendToBootstrapClassLoaderSearch on Windows and never log their // version banner, which this test asserts on. - val otelVersion = "1.30.0" + val otelVersion = "2.29.0" val dependencies = """ javaagent project(':simple-agent') testJavaagent 'io.opentelemetry.javaagent:opentelemetry-javaagent:$otelVersion' @@ -105,7 +105,7 @@ class JavaagentPluginFunctionalTest { // Use an OpenTelemetry agent recent enough to load correctly on Windows. Older releases (e.g. 1.11.1) // throw IllegalArgumentException from appendToBootstrapClassLoaderSearch on Windows and never log their // version banner, which this test asserts on. - val otelVersion = "1.30.0" + val otelVersion = "2.29.0" val dependencies = """ javaagent project(':simple-agent') javaagent 'io.opentelemetry.javaagent:opentelemetry-javaagent:$otelVersion' @@ -170,23 +170,23 @@ DEFAULT_JVM_OPTS="-javaagent:${"$"}APP_HOME/agent-libs/simple-agent.jar -Xmx256m @Test fun `can handle upgrade of agent with build cache`() { createJavaagentProject( """ - javaagent 'io.opentelemetry.javaagent:opentelemetry-javaagent:1.30.0' + javaagent 'io.opentelemetry.javaagent:opentelemetry-javaagent:2.28.0' """.trimIndent(), ) val firstBuild = runBuild(listOf("--build-cache", "build", "installDist", "execStartScript")) assertTrue( - firstBuild.output.contains("io.opentelemetry.javaagent.tooling.VersionLogger - opentelemetry-javaagent - version: 1.30.0"), + firstBuild.output.contains("io.opentelemetry.javaagent.tooling.VersionLogger - opentelemetry-javaagent - version: 2.28.0"), ) val buildScript = helloWorldDir.resolve("build.gradle") // replace the agent version with a newer version - buildScript.writeText(buildScript.readText().replace("1.30.0", "1.31.0")) + buildScript.writeText(buildScript.readText().replace("2.28.0", "2.29.0")) val secondBuild = runBuild(listOf("--build-cache", "build", "installDist", "execStartScript")) assertTrue( - secondBuild.output.contains("io.opentelemetry.javaagent.tooling.VersionLogger - opentelemetry-javaagent - version: 1.31.0"), + secondBuild.output.contains("io.opentelemetry.javaagent.tooling.VersionLogger - opentelemetry-javaagent - version: 2.29.0"), ) } From 397ecf070e37a76948b9a05ebf15ad15b22abec0 Mon Sep 17 00:00:00 2001 From: Ryan Dens Date: Mon, 6 Jul 2026 00:42:05 -0700 Subject: [PATCH 32/37] Use absolute (not canonical) javaagent path in JavaForkOptionsConfigurer The OpenTelemetry agent locates its own jar from the -javaagent path the JVM was given, then appends it to the bootstrap classloader search. On the Windows CI runner the Gradle caches sit behind a symlink/junction, so getCanonicalPath() resolved to a path whose round-trip through the agent's jar-location logic produced a name that appendToBootstrapClassLoaderSearch rejected with IllegalArgumentException -- the agent then bailed out before logging its version banner, failing the run/test functional tests on windows-latest. Switch to getAbsolutePath(), the same unresolved form the distribution start scripts already use successfully on Windows. Also drops the now-unnecessary IOException handling. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../javaagent/JavaForkOptionsConfigurer.java | 24 ++++++------------- 1 file changed, 7 insertions(+), 17 deletions(-) diff --git a/plugin/src/main/java/com/ryandens/javaagent/JavaForkOptionsConfigurer.java b/plugin/src/main/java/com/ryandens/javaagent/JavaForkOptionsConfigurer.java index c2c6f0c..cf8e921 100644 --- a/plugin/src/main/java/com/ryandens/javaagent/JavaForkOptionsConfigurer.java +++ b/plugin/src/main/java/com/ryandens/javaagent/JavaForkOptionsConfigurer.java @@ -1,8 +1,6 @@ package com.ryandens.javaagent; import java.io.File; -import java.io.IOException; -import java.io.UncheckedIOException; import java.util.ArrayList; import java.util.List; import java.util.Set; @@ -39,21 +37,13 @@ public static void configureJavaForkOptions( @Override public Iterable asArguments() { return javaagentConfiguration.get().stream() - .map( - file -> { - try { - // Do not quote the path to guard against spaces. These arguments go - // straight - // to a forked JVM via ProcessBuilder (no shell), which already quotes - // arguments containing spaces. Literal quotes would remain in the agent - // path - // and break agents that re-derive their own jar location, e.g. the - // OpenTelemetry agent fails to append its bootstrap jar to the classloader. - return "-javaagent:" + file.getCanonicalPath(); - } catch (IOException e) { - throw new UncheckedIOException(e); - } - }) + // Use the absolute path rather than the canonical path. On Windows CI the Gradle + // caches live behind a symlink/junction, and getCanonicalPath() resolves it to a + // real path whose round-trip through the OpenTelemetry agent's own jar-location + // logic yields a name that appendToBootstrapClassLoaderSearch rejects with + // IllegalArgumentException. The distribution start scripts already use a plain, + // unresolved path, which works. + .map(file -> "-javaagent:" + file.getAbsolutePath()) .collect(Collectors.toList()); } }); From 726ad81951025fb02c150efff0338b736b44635f Mon Sep 17 00:00:00 2001 From: Ryan Dens Date: Mon, 6 Jul 2026 00:57:34 -0700 Subject: [PATCH 33/37] TEMP: print exact -javaagent arg + showStandardStreams to diagnose Windows OTel path --- plugin/build.gradle.kts | 4 ++++ .../com/ryandens/javaagent/JavaForkOptionsConfigurer.java | 8 +++++++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/plugin/build.gradle.kts b/plugin/build.gradle.kts index ff6072e..b2001d0 100644 --- a/plugin/build.gradle.kts +++ b/plugin/build.gradle.kts @@ -67,4 +67,8 @@ gradlePlugin { tasks.functionalTest { inputs.file(layout.projectDirectory.dir("../simple-agent/build.gradle.kts")) inputs.files(layout.projectDirectory.dir("../simple-agent/src/")) + testLogging { + showStandardStreams = true + exceptionFormat = org.gradle.api.tasks.testing.logging.TestExceptionFormat.FULL + } } diff --git a/plugin/src/main/java/com/ryandens/javaagent/JavaForkOptionsConfigurer.java b/plugin/src/main/java/com/ryandens/javaagent/JavaForkOptionsConfigurer.java index cf8e921..9a4679b 100644 --- a/plugin/src/main/java/com/ryandens/javaagent/JavaForkOptionsConfigurer.java +++ b/plugin/src/main/java/com/ryandens/javaagent/JavaForkOptionsConfigurer.java @@ -43,7 +43,13 @@ public Iterable asArguments() { // logic yields a name that appendToBootstrapClassLoaderSearch rejects with // IllegalArgumentException. The distribution start scripts already use a plain, // unresolved path, which works. - .map(file -> "-javaagent:" + file.getAbsolutePath()) + .map( + file -> { + String arg = "-javaagent:" + file.getAbsolutePath(); + // TEMP diagnostic: print the exact -javaagent arg passed to the forked JVM + System.err.println("DIAG_JAVAAGENT_ARG=[" + arg + "]"); + return arg; + }) .collect(Collectors.toList()); } }); From 5344af8e774c409764b98a1b6bde4058f5ee436a Mon Sep 17 00:00:00 2001 From: Ryan Dens Date: Mon, 6 Jul 2026 01:07:04 -0700 Subject: [PATCH 34/37] TEMP: disable fail-fast to let Windows job complete for diagnostics --- .github/workflows/gradle.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/gradle.yml b/.github/workflows/gradle.yml index 90d5e3a..58b2e10 100644 --- a/.github/workflows/gradle.yml +++ b/.github/workflows/gradle.yml @@ -16,6 +16,7 @@ jobs: build: name: "Javaagent Gradle Plugin Validation" strategy: + fail-fast: false matrix: platform: [ubuntu-latest, windows-latest] runs-on: ${{ matrix.platform }} From 27c290e652cdc1c427e2b27e09493807a538bf2e Mon Sep 17 00:00:00 2001 From: Ryan Dens Date: Mon, 6 Jul 2026 01:23:43 -0700 Subject: [PATCH 35/37] Fix Windows MAX_PATH failure for OpenTelemetry agent in run/test tests The `can attach to test task` and `can attach two agents` tests attach the OpenTelemetry javaagent from the TestKit's Gradle cache. That cache lives under the deeply nested build/tmp/functionalTest/work/.gradle-test-kit, so on the Windows CI runner the agent jar path D:\a\...\.gradle-test-kit\caches\modules-2\files-2.1\io.opentelemetry.javaagent\ opentelemetry-javaagent\\\opentelemetry-javaagent-.jar was 267 characters -- past Windows' 260-char MAX_PATH. The JVM's native appendToBootstrapClassLoaderSearch rejects such paths with IllegalArgumentException, so the agent bailed out before logging its version banner. simple-agent (121 chars) and the distribution start scripts (which copy the agent into a short agent-libs/ path) were unaffected, and Linux has no such limit -- which is why the failure was Windows-only and independent of the agent version or path canonicalization. Root the TestKit Gradle home at the drive root on Windows so the cached agent jar path stays well within MAX_PATH. Also reverts the temporary diagnostics and the speculative JavaForkOptionsConfigurer changes (restored to the main baseline), which were not the cause. Co-Authored-By: Claude Opus 4.8 (1M context) --- plugin/build.gradle.kts | 4 ---- .../JavaagentPluginFunctionalTest.kt | 22 ++++++++++++++----- .../javaagent/JavaForkOptionsConfigurer.java | 17 ++++++-------- 3 files changed, 23 insertions(+), 20 deletions(-) diff --git a/plugin/build.gradle.kts b/plugin/build.gradle.kts index b2001d0..ff6072e 100644 --- a/plugin/build.gradle.kts +++ b/plugin/build.gradle.kts @@ -67,8 +67,4 @@ gradlePlugin { tasks.functionalTest { inputs.file(layout.projectDirectory.dir("../simple-agent/build.gradle.kts")) inputs.files(layout.projectDirectory.dir("../simple-agent/src/")) - testLogging { - showStandardStreams = true - exceptionFormat = org.gradle.api.tasks.testing.logging.TestExceptionFormat.FULL - } } diff --git a/plugin/src/functionalTest/kotlin/com/ryandens/javaagent/JavaagentPluginFunctionalTest.kt b/plugin/src/functionalTest/kotlin/com/ryandens/javaagent/JavaagentPluginFunctionalTest.kt index 6a84f35..4132400 100644 --- a/plugin/src/functionalTest/kotlin/com/ryandens/javaagent/JavaagentPluginFunctionalTest.kt +++ b/plugin/src/functionalTest/kotlin/com/ryandens/javaagent/JavaagentPluginFunctionalTest.kt @@ -76,9 +76,7 @@ class JavaagentPluginFunctionalTest { } @Test fun `can attach to test task`() { - // Use an OpenTelemetry agent recent enough to load correctly on Windows. Older releases (e.g. 1.11.1) - // throw IllegalArgumentException from appendToBootstrapClassLoaderSearch on Windows and never log their - // version banner, which this test asserts on. + // A real third-party javaagent used to verify the plugin attaches it and it logs its version banner. val otelVersion = "2.29.0" val dependencies = """ javaagent project(':simple-agent') @@ -102,9 +100,7 @@ class JavaagentPluginFunctionalTest { } @Test fun `can attach two agents to application run task`() { - // Use an OpenTelemetry agent recent enough to load correctly on Windows. Older releases (e.g. 1.11.1) - // throw IllegalArgumentException from appendToBootstrapClassLoaderSearch on Windows and never log their - // version banner, which this test asserts on. + // A real third-party javaagent used to verify the plugin attaches it and it logs its version banner. val otelVersion = "2.29.0" val dependencies = """ javaagent project(':simple-agent') @@ -299,6 +295,20 @@ DEFAULT_JVM_OPTS="-javaagent:${"$"}APP_HOME/agent-libs/simple-agent.jar -Xmx256m runner.withPluginClasspath() runner.withArguments(buildArgs) runner.withProjectDir(functionalTestDir) + if (isWindows) { + // Keep the TestKit Gradle home short. Its default lives under the deeply nested + // build/tmp/functionalTest/work/.gradle-test-kit, which pushes cached agent jars -- e.g. the + // OpenTelemetry javaagent at caches/modules-2/files-2.1/...//opentelemetry-javaagent-.jar + // -- past Windows' 260-character MAX_PATH. The JVM's native appendToBootstrapClassLoaderSearch + // then rejects the path with IllegalArgumentException and the agent fails to install. Rooting the + // TestKit home at the drive root keeps the full agent jar path comfortably within the limit. + val driveRoot = + functionalTestDir.absoluteFile + .toPath() + .root + .toFile() + runner.withTestKitDir(File(driveRoot, "gtk")) + } return runner.build() } } diff --git a/plugin/src/main/java/com/ryandens/javaagent/JavaForkOptionsConfigurer.java b/plugin/src/main/java/com/ryandens/javaagent/JavaForkOptionsConfigurer.java index 9a4679b..5912925 100644 --- a/plugin/src/main/java/com/ryandens/javaagent/JavaForkOptionsConfigurer.java +++ b/plugin/src/main/java/com/ryandens/javaagent/JavaForkOptionsConfigurer.java @@ -1,6 +1,8 @@ package com.ryandens.javaagent; import java.io.File; +import java.io.IOException; +import java.io.UncheckedIOException; import java.util.ArrayList; import java.util.List; import java.util.Set; @@ -37,18 +39,13 @@ public static void configureJavaForkOptions( @Override public Iterable asArguments() { return javaagentConfiguration.get().stream() - // Use the absolute path rather than the canonical path. On Windows CI the Gradle - // caches live behind a symlink/junction, and getCanonicalPath() resolves it to a - // real path whose round-trip through the OpenTelemetry agent's own jar-location - // logic yields a name that appendToBootstrapClassLoaderSearch rejects with - // IllegalArgumentException. The distribution start scripts already use a plain, - // unresolved path, which works. .map( file -> { - String arg = "-javaagent:" + file.getAbsolutePath(); - // TEMP diagnostic: print the exact -javaagent arg passed to the forked JVM - System.err.println("DIAG_JAVAAGENT_ARG=[" + arg + "]"); - return arg; + try { + return "-javaagent:" + file.getCanonicalPath(); + } catch (IOException e) { + throw new UncheckedIOException(e); + } }) .collect(Collectors.toList()); } From 06728e845a5466d3ad9fa8fa87ae16c561ab48d9 Mon Sep 17 00:00:00 2001 From: Ryan Dens Date: Mon, 6 Jul 2026 01:33:14 -0700 Subject: [PATCH 36/37] Revert temporary fail-fast:false diagnostic in CI matrix --- .github/workflows/gradle.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/gradle.yml b/.github/workflows/gradle.yml index 58b2e10..90d5e3a 100644 --- a/.github/workflows/gradle.yml +++ b/.github/workflows/gradle.yml @@ -16,7 +16,6 @@ jobs: build: name: "Javaagent Gradle Plugin Validation" strategy: - fail-fast: false matrix: platform: [ubuntu-latest, windows-latest] runs-on: ${{ matrix.platform }} From 54dca347686361b8a865e3b4edbba694ada9ee14 Mon Sep 17 00:00:00 2001 From: Ryan Dens Date: Mon, 6 Jul 2026 09:43:49 -0700 Subject: [PATCH 37/37] Address review feedback on start script generator and test deps - Platform now carries a defaultJvmOptsMapper (noop on Windows, collapse on Unix) and an agentArgSeparator, so JavaagentAwareStartScriptGenerator composes platform behavior instead of branching on `platform == Platform.WINDOWS`. - Document what Fake.write does (rewrites/removes the javaagent placeholder in the rendered start script). - Drop the org.apache.commons:commons-text test dependency; its only remaining use was escaping JAVA_HOME for the generated Groovy build script, now done with a small inline backslash/quote escape. Co-Authored-By: Claude Opus 4.8 (1M context) --- plugin/build.gradle.kts | 1 - .../JavaagentPluginFunctionalTest.kt | 10 ++++- .../JavaagentAwareStartScriptGenerator.kt | 40 +++++++++---------- .../kotlin/com/ryandens/javaagent/Platform.kt | 33 ++++++++++++++- 4 files changed, 58 insertions(+), 26 deletions(-) diff --git a/plugin/build.gradle.kts b/plugin/build.gradle.kts index ff6072e..f0c6e68 100644 --- a/plugin/build.gradle.kts +++ b/plugin/build.gradle.kts @@ -8,7 +8,6 @@ dependencies { testImplementation("org.jetbrains.kotlin:kotlin-test") testImplementation("org.jetbrains.kotlin:kotlin-test-junit") testImplementation("org.apache.commons:commons-compress:1.28.0") - testImplementation("org.apache.commons:commons-text:1.15.0") } gradlePlugin { diff --git a/plugin/src/functionalTest/kotlin/com/ryandens/javaagent/JavaagentPluginFunctionalTest.kt b/plugin/src/functionalTest/kotlin/com/ryandens/javaagent/JavaagentPluginFunctionalTest.kt index 4132400..a7640dc 100644 --- a/plugin/src/functionalTest/kotlin/com/ryandens/javaagent/JavaagentPluginFunctionalTest.kt +++ b/plugin/src/functionalTest/kotlin/com/ryandens/javaagent/JavaagentPluginFunctionalTest.kt @@ -1,6 +1,5 @@ package com.ryandens.javaagent -import org.apache.commons.text.StringEscapeUtils import org.gradle.internal.jvm.Jvm import org.gradle.internal.os.OperatingSystem import org.gradle.testkit.runner.BuildResult @@ -243,7 +242,14 @@ DEFAULT_JVM_OPTS="-javaagent:${"$"}APP_HOME/agent-libs/simple-agent.jar -Xmx256m } else { "'./hello-world'" } - val javaHome = StringEscapeUtils.escapeJava(Jvm.current().getJavaHome().toString()) + // Escape backslashes and quotes so a Windows JAVA_HOME embeds cleanly in the generated Groovy string literal. + val javaHome = + Jvm + .current() + .getJavaHome() + .toString() + .replace("\\", "\\\\") + .replace("\"", "\\\"") helloWorldDir.resolve("build.gradle").writeText( """ plugins { diff --git a/plugin/src/main/kotlin/com/ryandens/javaagent/JavaagentAwareStartScriptGenerator.kt b/plugin/src/main/kotlin/com/ryandens/javaagent/JavaagentAwareStartScriptGenerator.kt index d98ba3a..5f517d7 100644 --- a/plugin/src/main/kotlin/com/ryandens/javaagent/JavaagentAwareStartScriptGenerator.kt +++ b/plugin/src/main/kotlin/com/ryandens/javaagent/JavaagentAwareStartScriptGenerator.kt @@ -15,7 +15,7 @@ class JavaagentAwareStartScriptGenerator( private val inner: ScriptGenerator = DefaultTemplateBasedStartScriptGenerator( platform.lineSeparator, - FakeTransformer(platform.templateBindingFactory, platform), + FakeTransformer(platform.templateBindingFactory, platform.defaultJvmOptsMapper), platform.template, ), ) : ScriptGenerator { @@ -23,12 +23,15 @@ class JavaagentAwareStartScriptGenerator( details: JavaAppStartScriptGenerationDetails, destination: Writer, ) { - inner.generateScript(details, Fake(destination, javaagentConfiguration, platform.pathSeparator, platform.appHomeVar, platform)) + inner.generateScript( + details, + Fake(destination, javaagentConfiguration, platform.pathSeparator, platform.appHomeVar, platform.agentArgSeparator), + ) } private class FakeTransformer( private val inner: StartScriptTemplateBindingFactory, - private val platform: Platform, + private val defaultJvmOptsMapper: (String) -> String, ) : Transformer, JavaAppStartScriptGenerationDetails> by inner { override fun transform(`in`: JavaAppStartScriptGenerationDetails): MutableMap { val result = inner.transform(`in`) @@ -39,16 +42,7 @@ class JavaagentAwareStartScriptGenerator( } else { jvmOpts } - // On Unix the individually-quoted JVM opts are collapsed into a single space-separated string, which the - // shell then word-splits back apart when the unquoted $DEFAULT_JVM_OPTS is expanded. On Windows the batch - // script does not word-split inside quotes, so each opt must remain its own quoted token; collapsing them - // would glue e.g. `-Xmx256m` onto the `-javaagent:` path and produce an invalid argument. - result["defaultJvmOpts"] = - if (platform == Platform.WINDOWS) { - trimmedJvmOpts - } else { - trimmedJvmOpts.replace("\" \"", " ") - } + result["defaultJvmOpts"] = defaultJvmOptsMapper(trimmedJvmOpts) return result } } @@ -58,7 +52,7 @@ class JavaagentAwareStartScriptGenerator( private val javaagentFiles: Provider>, private val pathSeparator: String, private val appHomeVar: String, - private val platform: Platform, + private val agentArgSeparator: String, ) : Writer() { override fun close() { inner.close() @@ -76,6 +70,14 @@ class JavaagentAwareStartScriptGenerator( inner.write(cbuf, off, len) } + /** + * Rewrites the javaagent placeholder that the templated `defaultJvmOpts` injects into the rendered start + * script. When the configuration resolves to one or more agents, the single placeholder is replaced by one + * `-javaagent:` option per agent, each pointing at the distribution's `agent-libs` directory (joined with + * the platform's [agentArgSeparator]). When there are no agents, the placeholder together with its + * surrounding quoting and trailing space is stripped out. Text that does not contain the placeholder is + * written through unchanged. + */ override fun write(str: String) { val files = javaagentFiles.get() val replace = @@ -91,15 +93,11 @@ class JavaagentAwareStartScriptGenerator( .replace("-javaagent:COM_RYANDENS_JAVAAGENTS_PLACEHOLDER.jar ", "") .replace("-javaagent:COM_RYANDENS_JAVAAGENTS_PLACEHOLDER.jar", "") } else { - // On Windows, each -javaagent option must be a separately quoted token, - // so we use `" "` (close quote, space, open quote) as separator. - // On Unix, all options can be space-separated within a single quoted string. - val separator = if (platform == Platform.WINDOWS) "\" \"" else " " str.replace( "-javaagent:COM_RYANDENS_JAVAAGENTS_PLACEHOLDER.jar", - javaagentFiles.get().joinToString( - separator, - ) { jar -> "-javaagent:${appHomeVar}${pathSeparator}agent-libs${pathSeparator}${jar.name}" }, + files.joinToString( + agentArgSeparator, + ) { jar -> "-javaagent:$appHomeVar${pathSeparator}agent-libs$pathSeparator${jar.name}" }, ) } super.write(replace) diff --git a/plugin/src/main/kotlin/com/ryandens/javaagent/Platform.kt b/plugin/src/main/kotlin/com/ryandens/javaagent/Platform.kt index 5df2a19..7d69dac 100644 --- a/plugin/src/main/kotlin/com/ryandens/javaagent/Platform.kt +++ b/plugin/src/main/kotlin/com/ryandens/javaagent/Platform.kt @@ -11,7 +11,36 @@ enum class Platform( val appHomeVar: String, val templateBindingFactory: StartScriptTemplateBindingFactory, val template: TextResource, + /** + * Separator placed between multiple `-javaagent` options. On Windows each option must remain its own + * quoted token (`" "` = close quote, space, open quote); on Unix the options share a single quoted string, + * so a plain space suffices. + */ + val agentArgSeparator: String, + /** + * Normalizes the template's rendered `defaultJvmOpts`. On Unix the individually quoted options are + * collapsed into a single space-separated string that the shell word-splits back apart when the unquoted + * `$DEFAULT_JVM_OPTS` is expanded; on Windows the batch script does not word-split inside quotes, so each + * option must stay its own quoted token and the value is left unchanged. + */ + val defaultJvmOptsMapper: (String) -> String, ) { - UNIX("\n", "/", "\$APP_HOME", StartScriptTemplateBindingFactory.unix(), UnixStartScriptGenerator().template), - WINDOWS("\r\n", "\\", "%APP_HOME%", StartScriptTemplateBindingFactory.windows(), WindowsStartScriptGenerator().template), + UNIX( + "\n", + "/", + "\$APP_HOME", + StartScriptTemplateBindingFactory.unix(), + UnixStartScriptGenerator().template, + " ", + { it.replace("\" \"", " ") }, + ), + WINDOWS( + "\r\n", + "\\", + "%APP_HOME%", + StartScriptTemplateBindingFactory.windows(), + WindowsStartScriptGenerator().template, + "\" \"", + { it }, + ), }