From a7be973c9b46aae93106fd4a907aa2ecaf996bb5 Mon Sep 17 00:00:00 2001
From: Dmytro Nosan <dimanosan@gmail.com>
Date: Mon, 17 Feb 2025 13:08:59 +0200
Subject: [PATCH] Use ArgFile for classpath argument on Windows

This commit uses @argfile syntax for classpath argument on Windows OS
to avoid `CreateProcess error=206, The filename or extension is too long`.

See gh-44252

Signed-off-by: Dmytro Nosan <dimanosan@gmail.com>
---
 .../boot/maven/AbstractAotMojo.java           |  6 +-
 .../boot/maven/AbstractRunMojo.java           | 72 +--------------
 .../springframework/boot/maven/ArgFile.java   | 81 +++++++++++++++++
 .../boot/maven/ClasspathBuilder.java          | 89 +++++++++++++++++++
 .../boot/maven/CommandLineBuilder.java        | 30 +------
 ...actRunMojoTests.java => ArgFileTests.java} | 14 +--
 .../boot/maven/ClasspathBuilderTests.java     | 69 ++++++++++++++
 .../boot/maven/CommandLineBuilderTests.java   | 70 ++++++++++++++-
 8 files changed, 322 insertions(+), 109 deletions(-)
 create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/ArgFile.java
 create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/ClasspathBuilder.java
 rename spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/test/java/org/springframework/boot/maven/{AbstractRunMojoTests.java => ArgFileTests.java} (72%)
 create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/test/java/org/springframework/boot/maven/ClasspathBuilderTests.java

diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/AbstractAotMojo.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/AbstractAotMojo.java
index 8c6c9599ae3d..1158a2d74861 100644
--- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/AbstractAotMojo.java
+++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/AbstractAotMojo.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2012-2023 the original author or authors.
+ * Copyright 2012-2025 the original author or authors.
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -46,8 +46,6 @@
 import org.apache.maven.shared.artifact.filter.collection.ArtifactsFilter;
 import org.apache.maven.toolchain.ToolchainManager;
 
-import org.springframework.boot.maven.CommandLineBuilder.ClasspathBuilder;
-
 /**
  * Abstract base class for AOT processing MOJOs.
  *
@@ -149,7 +147,7 @@ protected final void compileSourceFiles(URL[] classPath, File sourcesDirectory,
 			JavaCompilerPluginConfiguration compilerConfiguration = new JavaCompilerPluginConfiguration(this.project);
 			List<String> options = new ArrayList<>();
 			options.add("-cp");
-			options.add(ClasspathBuilder.build(Arrays.asList(classPath)));
+			options.add(ClasspathBuilder.build(classPath));
 			options.add("-d");
 			options.add(outputDirectory.toPath().toAbsolutePath().toString());
 			String releaseVersion = compilerConfiguration.getReleaseVersion();
diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/AbstractRunMojo.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/AbstractRunMojo.java
index 71eb7569f5c8..c0bdcf7da1cd 100644
--- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/AbstractRunMojo.java
+++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/AbstractRunMojo.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2012-2024 the original author or authors.
+ * Copyright 2012-2025 the original author or authors.
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -20,15 +20,10 @@
 import java.io.IOException;
 import java.net.MalformedURLException;
 import java.net.URL;
-import java.nio.charset.Charset;
-import java.nio.charset.UnsupportedCharsetException;
-import java.nio.file.Files;
-import java.nio.file.Path;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collections;
 import java.util.List;
-import java.util.Locale;
 import java.util.Map;
 import java.util.Set;
 import java.util.stream.Collectors;
@@ -46,7 +41,6 @@
 import org.springframework.boot.loader.tools.FileUtils;
 import org.springframework.util.Assert;
 import org.springframework.util.ObjectUtils;
-import org.springframework.util.StringUtils;
 
 /**
  * Base class to run a Spring Boot application.
@@ -351,45 +345,18 @@ private void addActiveProfileArgument(RunArguments arguments) {
 
 	private void addClasspath(List<String> args) throws MojoExecutionException {
 		try {
-			StringBuilder classpath = new StringBuilder();
-			for (URL ele : getClassPathUrls()) {
-				if (!classpath.isEmpty()) {
-					classpath.append(File.pathSeparator);
-				}
-				classpath.append(new File(ele.toURI()));
-			}
+			String classpath = ClasspathBuilder.build(getClassPathUrls());
 			if (getLog().isDebugEnabled()) {
 				getLog().debug("Classpath for forked process: " + classpath);
 			}
 			args.add("-cp");
-			if (needsClasspathArgFile()) {
-				args.add("@" + ArgFile.create(classpath).path());
-			}
-			else {
-				args.add(classpath.toString());
-			}
+			args.add(classpath);
 		}
 		catch (Exception ex) {
 			throw new MojoExecutionException("Could not build classpath", ex);
 		}
 	}
 
-	private boolean needsClasspathArgFile() {
-		// Windows limits the maximum command length, so we use an argfile there
-		return runsOnWindows();
-	}
-
-	private boolean runsOnWindows() {
-		String os = System.getProperty("os.name");
-		if (!StringUtils.hasLength(os)) {
-			if (getLog().isWarnEnabled()) {
-				getLog().warn("System property os.name is not set");
-			}
-			return false;
-		}
-		return os.toLowerCase(Locale.ROOT).contains("win");
-	}
-
 	protected URL[] getClassPathUrls() throws MojoExecutionException {
 		try {
 			List<URL> urls = new ArrayList<>();
@@ -468,37 +435,4 @@ static String format(String key, String value) {
 
 	}
 
-	record ArgFile(Path path) {
-
-		private void write(CharSequence content) throws IOException {
-			Files.writeString(this.path, "\"" + escape(content) + "\"", getCharset());
-		}
-
-		private Charset getCharset() {
-			String nativeEncoding = System.getProperty("native.encoding");
-			if (nativeEncoding == null) {
-				return Charset.defaultCharset();
-			}
-			try {
-				return Charset.forName(nativeEncoding);
-			}
-			catch (UnsupportedCharsetException ex) {
-				return Charset.defaultCharset();
-			}
-		}
-
-		private String escape(CharSequence content) {
-			return content.toString().replace("\\", "\\\\");
-		}
-
-		static ArgFile create(CharSequence content) throws IOException {
-			Path tempFile = Files.createTempFile("spring-boot-", ".argfile");
-			tempFile.toFile().deleteOnExit();
-			ArgFile argFile = new ArgFile(tempFile);
-			argFile.write(content);
-			return argFile;
-		}
-
-	}
-
 }
diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/ArgFile.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/ArgFile.java
new file mode 100644
index 000000000000..c262fda002b4
--- /dev/null
+++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/ArgFile.java
@@ -0,0 +1,81 @@
+/*
+ * Copyright 2012-2025 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.boot.maven;
+
+import java.io.IOException;
+import java.nio.charset.Charset;
+import java.nio.charset.UnsupportedCharsetException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+
+/**
+ *
+ * Utility class that represents `argument` as a file. Mostly used to avoid `Path too long
+ * on ...` on Windows.
+ *
+ * @author Moritz Halbritter
+ * @author Dmytro Nosan
+ */
+final class ArgFile {
+
+	private final Path path;
+
+	private ArgFile(Path path) {
+		this.path = path.toAbsolutePath();
+	}
+
+	/**
+	 * Creates a new {@code ArgFile} with the given content.
+	 * @param content the content to write to the argument file
+	 * @return a new {@code ArgFile}
+	 * @throws IOException if an I/O error occurs
+	 */
+	static ArgFile create(CharSequence content) throws IOException {
+		Path tempFile = Files.createTempFile("spring-boot-", ".argfile");
+		tempFile.toFile().deleteOnExit();
+		ArgFile argFile = new ArgFile(tempFile);
+		argFile.write(content);
+		return argFile;
+	}
+
+	private void write(CharSequence content) throws IOException {
+		Files.writeString(this.path, "\"" + escape(content) + "\"", getCharset());
+	}
+
+	private Charset getCharset() {
+		String nativeEncoding = System.getProperty("native.encoding");
+		if (nativeEncoding == null) {
+			return Charset.defaultCharset();
+		}
+		try {
+			return Charset.forName(nativeEncoding);
+		}
+		catch (UnsupportedCharsetException ex) {
+			return Charset.defaultCharset();
+		}
+	}
+
+	private String escape(CharSequence content) {
+		return content.toString().replace("\\", "\\\\");
+	}
+
+	@Override
+	public String toString() {
+		return this.path.toString();
+	}
+
+}
diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/ClasspathBuilder.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/ClasspathBuilder.java
new file mode 100644
index 000000000000..a4ad501998c0
--- /dev/null
+++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/ClasspathBuilder.java
@@ -0,0 +1,89 @@
+/*
+ * Copyright 2012-2025 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.boot.maven;
+
+import java.io.File;
+import java.io.IOException;
+import java.net.URISyntaxException;
+import java.net.URL;
+import java.util.Locale;
+
+import org.springframework.util.ObjectUtils;
+import org.springframework.util.StringUtils;
+
+/**
+ * Helper class to build the -cp (classpath) argument of a java process.
+ *
+ * @author Stephane Nicoll
+ * @author Dmytro Nosan
+ */
+final class ClasspathBuilder {
+
+	private ClasspathBuilder() {
+	}
+
+	/**
+	 * Builds a classpath string or an argument file representing the classpath, depending
+	 * on the operating system.
+	 * @param urls an array of {@link URL} representing the elements of the classpath
+	 * @return the classpath; on Windows, the path to an argument file is returned,
+	 * prefixed with '@'
+	 */
+	static String build(URL... urls) {
+		if (ObjectUtils.isEmpty(urls)) {
+			return "";
+		}
+		if (urls.length == 1) {
+			return toFile(urls[0]).toString();
+		}
+		StringBuilder builder = new StringBuilder();
+		for (URL url : urls) {
+			if (!builder.isEmpty()) {
+				builder.append(File.pathSeparator);
+			}
+			builder.append(toFile(url));
+		}
+		String classpath = builder.toString();
+		if (runsOnWindows()) {
+			try {
+				return "@" + ArgFile.create(classpath);
+			}
+			catch (IOException ex) {
+				return classpath;
+			}
+		}
+		return classpath;
+	}
+
+	private static File toFile(URL url) {
+		try {
+			return new File(url.toURI());
+		}
+		catch (URISyntaxException ex) {
+			throw new IllegalArgumentException(ex);
+		}
+	}
+
+	private static boolean runsOnWindows() {
+		String os = System.getProperty("os.name");
+		if (!StringUtils.hasText(os)) {
+			return false;
+		}
+		return os.toLowerCase(Locale.ROOT).contains("win");
+	}
+
+}
diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/CommandLineBuilder.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/CommandLineBuilder.java
index ba5a45ce8667..00b023fb252a 100644
--- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/CommandLineBuilder.java
+++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/CommandLineBuilder.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2012-2024 the original author or authors.
+ * Copyright 2012-2025 the original author or authors.
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -16,8 +16,6 @@
 
 package org.springframework.boot.maven;
 
-import java.io.File;
-import java.net.URISyntaxException;
 import java.net.URL;
 import java.util.ArrayList;
 import java.util.Arrays;
@@ -84,7 +82,7 @@ List<String> build() {
 		}
 		if (!this.classpathElements.isEmpty()) {
 			commandLine.add("-cp");
-			commandLine.add(ClasspathBuilder.build(this.classpathElements));
+			commandLine.add(ClasspathBuilder.build(this.classpathElements.toArray(URL[]::new)));
 		}
 		commandLine.add(this.mainClass);
 		if (!this.arguments.isEmpty()) {
@@ -93,30 +91,6 @@ List<String> build() {
 		return commandLine;
 	}
 
-	static class ClasspathBuilder {
-
-		static String build(List<URL> classpathElements) {
-			StringBuilder classpath = new StringBuilder();
-			for (URL element : classpathElements) {
-				if (!classpath.isEmpty()) {
-					classpath.append(File.pathSeparator);
-				}
-				classpath.append(toFile(element));
-			}
-			return classpath.toString();
-		}
-
-		private static File toFile(URL element) {
-			try {
-				return new File(element.toURI());
-			}
-			catch (URISyntaxException ex) {
-				throw new IllegalArgumentException(ex);
-			}
-		}
-
-	}
-
 	/**
 	 * Format System properties.
 	 */
diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/test/java/org/springframework/boot/maven/AbstractRunMojoTests.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/test/java/org/springframework/boot/maven/ArgFileTests.java
similarity index 72%
rename from spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/test/java/org/springframework/boot/maven/AbstractRunMojoTests.java
rename to spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/test/java/org/springframework/boot/maven/ArgFileTests.java
index ba5669ee356c..2c83f86187c2 100644
--- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/test/java/org/springframework/boot/maven/AbstractRunMojoTests.java
+++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/test/java/org/springframework/boot/maven/ArgFileTests.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2012-2024 the original author or authors.
+ * Copyright 2012-2025 the original author or authors.
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -18,24 +18,24 @@
 
 import java.io.IOException;
 import java.nio.charset.StandardCharsets;
+import java.nio.file.Paths;
 
 import org.junit.jupiter.api.Test;
 
-import org.springframework.boot.maven.AbstractRunMojo.ArgFile;
-
 import static org.assertj.core.api.Assertions.assertThat;
 
 /**
- * Tests for {@link AbstractRunMojo}.
+ * Tests for {@link ArgFile}.
  *
  * @author Moritz Halbritter
+ * @author Dmytro Nosan
  */
-class AbstractRunMojoTests {
+class ArgFileTests {
 
 	@Test
-	void argfileEscapesContent() throws IOException {
+	void argFileEscapesContent() throws IOException {
 		ArgFile file = ArgFile.create("some \\ content");
-		assertThat(file.path()).content(StandardCharsets.UTF_8).isEqualTo("\"some \\\\ content\"");
+		assertThat(Paths.get(file.toString())).content(StandardCharsets.UTF_8).isEqualTo("\"some \\\\ content\"");
 	}
 
 }
diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/test/java/org/springframework/boot/maven/ClasspathBuilderTests.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/test/java/org/springframework/boot/maven/ClasspathBuilderTests.java
new file mode 100644
index 000000000000..8216c2188117
--- /dev/null
+++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/test/java/org/springframework/boot/maven/ClasspathBuilderTests.java
@@ -0,0 +1,69 @@
+/*
+ * Copyright 2012-2025 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.boot.maven;
+
+import java.io.File;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.condition.DisabledOnOs;
+import org.junit.jupiter.api.condition.EnabledOnOs;
+import org.junit.jupiter.api.condition.OS;
+import org.junit.jupiter.api.io.TempDir;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * Tests for {@link ClasspathBuilder}.
+ *
+ * @author Dmytro Nosan
+ */
+class ClasspathBuilderTests {
+
+	@Test
+	void buildWithEmptyClassPath() {
+		assertThat(ClasspathBuilder.build()).isEmpty();
+	}
+
+	@Test
+	void buildWithSingleClassPathURL(@TempDir Path tempDir) throws Exception {
+		Path file = tempDir.resolve("test.jar");
+		assertThat(ClasspathBuilder.build(file.toUri().toURL())).isEqualTo(file.toString());
+	}
+
+	@Test
+	@DisabledOnOs(OS.WINDOWS)
+	void buildWithMultipleClassPathURLs(@TempDir Path tempDir) throws Exception {
+		Path file = tempDir.resolve("test.jar");
+		Path file1 = tempDir.resolve("test1.jar");
+		assertThat(ClasspathBuilder.build(file.toUri().toURL(), file1.toUri().toURL()))
+			.isEqualTo(file + File.pathSeparator + file1);
+	}
+
+	@Test
+	@EnabledOnOs(OS.WINDOWS)
+	void buildWithMultipleClassPathURLsOnWindows(@TempDir Path tempDir) throws Exception {
+		Path file = tempDir.resolve("test.jar");
+		Path file1 = tempDir.resolve("test1.jar");
+		String classpath = ClasspathBuilder.build(file.toUri().toURL(), file1.toUri().toURL());
+		assertThat(classpath).startsWith("@");
+		assertThat(Paths.get(classpath.substring(1)))
+			.hasContent("\"" + (file + File.pathSeparator + file1).replace("\\", "\\\\") + "\"");
+	}
+
+}
diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/test/java/org/springframework/boot/maven/CommandLineBuilderTests.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/test/java/org/springframework/boot/maven/CommandLineBuilderTests.java
index aedd17bd48b3..4d5e245be44d 100644
--- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/test/java/org/springframework/boot/maven/CommandLineBuilderTests.java
+++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/test/java/org/springframework/boot/maven/CommandLineBuilderTests.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2012-2023 the original author or authors.
+ * Copyright 2012-2025 the original author or authors.
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -16,10 +16,25 @@
 
 package org.springframework.boot.maven;
 
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.lang.management.ManagementFactory;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.Arrays;
+import java.util.List;
 import java.util.Map;
 
 import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.condition.DisabledOnOs;
+import org.junit.jupiter.api.condition.EnabledOnOs;
+import org.junit.jupiter.api.condition.OS;
+import org.junit.jupiter.api.io.TempDir;
 
+import org.springframework.boot.loader.tools.JavaExecutable;
 import org.springframework.boot.maven.sample.ClassWithMainMethod;
 
 import static org.assertj.core.api.Assertions.assertThat;
@@ -76,4 +91,57 @@ void buildWithNullIntermediateArgumentIsIgnored() {
 			.containsExactly(CLASS_NAME, "--test", "--another");
 	}
 
+	@Test
+	@DisabledOnOs(OS.WINDOWS)
+	void buildWithClassPath(@TempDir Path tempDir) throws Exception {
+		Path file = tempDir.resolve("test.jar");
+		Path file1 = tempDir.resolve("test1.jar");
+		assertThat(CommandLineBuilder.forMainClass(CLASS_NAME)
+			.withClasspath(file.toUri().toURL(), file1.toUri().toURL())
+			.build()).containsExactly("-cp", file + File.pathSeparator + file1, CLASS_NAME);
+	}
+
+	@Test
+	@EnabledOnOs(OS.WINDOWS)
+	void buildWithClassPathOnWindows(@TempDir Path tempDir) throws Exception {
+		Path file = tempDir.resolve("test.jar");
+		Path file1 = tempDir.resolve("test1.jar");
+		List<String> args = CommandLineBuilder.forMainClass(CLASS_NAME)
+			.withClasspath(file.toUri().toURL(), file1.toUri().toURL())
+			.build();
+		assertThat(args).hasSize(3);
+		assertThat(args.get(0)).isEqualTo("-cp");
+		assertThat(args.get(1)).startsWith("@");
+		assertThat(args.get(2)).isEqualTo(CLASS_NAME);
+		assertThat(Paths.get(args.get(1).substring(1)))
+			.hasContent("\"" + (file + File.pathSeparator + file1).replace("\\", "\\\\") + "\"");
+	}
+
+	@Test
+	void buildAndRunWithLongClassPath() throws IOException, InterruptedException {
+		StringBuilder classPath = new StringBuilder(ManagementFactory.getRuntimeMXBean().getClassPath());
+		while (classPath.length() < 35000) {
+			classPath.append(File.pathSeparator).append(classPath);
+		}
+		URL[] urls = Arrays.stream(classPath.toString().split(File.pathSeparator)).map(this::toURL).toArray(URL[]::new);
+		List<String> command = CommandLineBuilder.forMainClass(ClassWithMainMethod.class.getName())
+			.withClasspath(urls)
+			.build();
+		ProcessBuilder pb = new JavaExecutable().processBuilder(command.toArray(new String[0]));
+		Process process = pb.start();
+		assertThat(process.waitFor()).isEqualTo(0);
+		try (InputStream inputStream = process.getInputStream()) {
+			assertThat(inputStream).hasContent("Hello World");
+		}
+	}
+
+	private URL toURL(String path) {
+		try {
+			return Paths.get(path).toUri().toURL();
+		}
+		catch (MalformedURLException ex) {
+			throw new RuntimeException(ex);
+		}
+	}
+
 }