Use ArgFile for classpath argument on Windows

This commit uses @argfile syntax for classpath argument on Windows OS
to avoid creating a command-line that is too long.

See gh-44305

Signed-off-by: Dmytro Nosan <dimanosan@gmail.com>
This commit is contained in:
Dmytro Nosan 2025-02-17 13:08:59 +02:00 committed by Stéphane Nicoll
parent 0a42082671
commit a6b80831f0
8 changed files with 322 additions and 109 deletions

View File

@ -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.plugins.annotations.Parameter;
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 @@ public abstract class AbstractAotMojo extends AbstractDependencyFilterMojo {
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();

View File

@ -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.File;
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.apache.maven.toolchain.ToolchainManager;
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 @@ public abstract class AbstractRunMojo extends AbstractDependencyFilterMojo {
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 @@ public abstract class AbstractRunMojo extends AbstractDependencyFilterMojo {
}
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;
}
}
}

View File

@ -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();
}
}

View File

@ -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");
}
}

View File

@ -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 @@ final class CommandLineBuilder {
}
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 @@ final class CommandLineBuilder {
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.
*/

View File

@ -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 @@ package org.springframework.boot.maven;
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\"");
}
}

View File

@ -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("\\", "\\\\") + "\"");
}
}

View File

@ -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 @@ class CommandLineBuilderTests {
.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);
}
}
}