Skip to content

Commit 4c1f63b

Browse files
committed
Merge pull request #44305 from nosan
* pr/44305: Polish "Use ArgFile for classpath argument on Windows" Use ArgFile for classpath argument on Windows Closes gh-44305
2 parents 0a42082 + cd8c12d commit 4c1f63b

File tree

7 files changed

+411
-144
lines changed

7 files changed

+411
-144
lines changed

spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/AbstractAotMojo.java

+2-4
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2012-2023 the original author or authors.
2+
* Copyright 2012-2025 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -46,8 +46,6 @@
4646
import org.apache.maven.shared.artifact.filter.collection.ArtifactsFilter;
4747
import org.apache.maven.toolchain.ToolchainManager;
4848

49-
import org.springframework.boot.maven.CommandLineBuilder.ClasspathBuilder;
50-
5149
/**
5250
* Abstract base class for AOT processing MOJOs.
5351
*
@@ -149,7 +147,7 @@ protected final void compileSourceFiles(URL[] classPath, File sourcesDirectory,
149147
JavaCompilerPluginConfiguration compilerConfiguration = new JavaCompilerPluginConfiguration(this.project);
150148
List<String> options = new ArrayList<>();
151149
options.add("-cp");
152-
options.add(ClasspathBuilder.build(Arrays.asList(classPath)));
150+
options.add(ClasspathBuilder.forURLs(classPath).build().argument());
153151
options.add("-d");
154152
options.add(outputDirectory.toPath().toAbsolutePath().toString());
155153
String releaseVersion = compilerConfiguration.getReleaseVersion();

spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/AbstractRunMojo.java

+6-70
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2012-2024 the original author or authors.
2+
* Copyright 2012-2025 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -20,15 +20,10 @@
2020
import java.io.IOException;
2121
import java.net.MalformedURLException;
2222
import java.net.URL;
23-
import java.nio.charset.Charset;
24-
import java.nio.charset.UnsupportedCharsetException;
25-
import java.nio.file.Files;
26-
import java.nio.file.Path;
2723
import java.util.ArrayList;
2824
import java.util.Arrays;
2925
import java.util.Collections;
3026
import java.util.List;
31-
import java.util.Locale;
3227
import java.util.Map;
3328
import java.util.Set;
3429
import java.util.stream.Collectors;
@@ -44,9 +39,9 @@
4439
import org.apache.maven.toolchain.ToolchainManager;
4540

4641
import org.springframework.boot.loader.tools.FileUtils;
42+
import org.springframework.boot.maven.ClasspathBuilder.Classpath;
4743
import org.springframework.util.Assert;
4844
import org.springframework.util.ObjectUtils;
49-
import org.springframework.util.StringUtils;
5045

5146
/**
5247
* Base class to run a Spring Boot application.
@@ -351,45 +346,19 @@ private void addActiveProfileArgument(RunArguments arguments) {
351346

352347
private void addClasspath(List<String> args) throws MojoExecutionException {
353348
try {
354-
StringBuilder classpath = new StringBuilder();
355-
for (URL ele : getClassPathUrls()) {
356-
if (!classpath.isEmpty()) {
357-
classpath.append(File.pathSeparator);
358-
}
359-
classpath.append(new File(ele.toURI()));
360-
}
349+
Classpath classpath = ClasspathBuilder.forURLs(getClassPathUrls()).build();
361350
if (getLog().isDebugEnabled()) {
362-
getLog().debug("Classpath for forked process: " + classpath);
351+
getLog().debug("Classpath for forked process: "
352+
+ classpath.elements().map(Object::toString).collect(Collectors.joining(File.separator)));
363353
}
364354
args.add("-cp");
365-
if (needsClasspathArgFile()) {
366-
args.add("@" + ArgFile.create(classpath).path());
367-
}
368-
else {
369-
args.add(classpath.toString());
370-
}
355+
args.add(classpath.argument());
371356
}
372357
catch (Exception ex) {
373358
throw new MojoExecutionException("Could not build classpath", ex);
374359
}
375360
}
376361

377-
private boolean needsClasspathArgFile() {
378-
// Windows limits the maximum command length, so we use an argfile there
379-
return runsOnWindows();
380-
}
381-
382-
private boolean runsOnWindows() {
383-
String os = System.getProperty("os.name");
384-
if (!StringUtils.hasLength(os)) {
385-
if (getLog().isWarnEnabled()) {
386-
getLog().warn("System property os.name is not set");
387-
}
388-
return false;
389-
}
390-
return os.toLowerCase(Locale.ROOT).contains("win");
391-
}
392-
393362
protected URL[] getClassPathUrls() throws MojoExecutionException {
394363
try {
395364
List<URL> urls = new ArrayList<>();
@@ -468,37 +437,4 @@ static String format(String key, String value) {
468437

469438
}
470439

471-
record ArgFile(Path path) {
472-
473-
private void write(CharSequence content) throws IOException {
474-
Files.writeString(this.path, "\"" + escape(content) + "\"", getCharset());
475-
}
476-
477-
private Charset getCharset() {
478-
String nativeEncoding = System.getProperty("native.encoding");
479-
if (nativeEncoding == null) {
480-
return Charset.defaultCharset();
481-
}
482-
try {
483-
return Charset.forName(nativeEncoding);
484-
}
485-
catch (UnsupportedCharsetException ex) {
486-
return Charset.defaultCharset();
487-
}
488-
}
489-
490-
private String escape(CharSequence content) {
491-
return content.toString().replace("\\", "\\\\");
492-
}
493-
494-
static ArgFile create(CharSequence content) throws IOException {
495-
Path tempFile = Files.createTempFile("spring-boot-", ".argfile");
496-
tempFile.toFile().deleteOnExit();
497-
ArgFile argFile = new ArgFile(tempFile);
498-
argFile.write(content);
499-
return argFile;
500-
}
501-
502-
}
503-
504440
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
/*
2+
* Copyright 2012-2025 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.boot.maven;
18+
19+
import java.io.File;
20+
import java.io.IOException;
21+
import java.net.URISyntaxException;
22+
import java.net.URL;
23+
import java.nio.charset.Charset;
24+
import java.nio.charset.UnsupportedCharsetException;
25+
import java.nio.file.Files;
26+
import java.nio.file.Path;
27+
import java.nio.file.Paths;
28+
import java.util.ArrayList;
29+
import java.util.Arrays;
30+
import java.util.Collections;
31+
import java.util.List;
32+
import java.util.Locale;
33+
import java.util.stream.Collectors;
34+
import java.util.stream.Stream;
35+
36+
import org.springframework.util.ObjectUtils;
37+
import org.springframework.util.StringUtils;
38+
39+
/**
40+
* Helper class to build the -cp (classpath) argument of a java process.
41+
*
42+
* @author Stephane Nicoll
43+
* @author Dmytro Nosan
44+
*/
45+
class ClasspathBuilder {
46+
47+
private final List<URL> urls;
48+
49+
protected ClasspathBuilder(List<URL> urls) {
50+
this.urls = urls;
51+
}
52+
53+
/**
54+
* Builds a classpath string or an argument file representing the classpath, depending
55+
* on the operating system.
56+
* @param urls an array of {@link URL} representing the elements of the classpath
57+
* @return the classpath; on Windows, the path to an argument file is returned,
58+
* prefixed with '@'
59+
*/
60+
static ClasspathBuilder forURLs(List<URL> urls) {
61+
return new ClasspathBuilder(new ArrayList<>(urls));
62+
}
63+
64+
/**
65+
* Builds a classpath string or an argument file representing the classpath, depending
66+
* on the operating system.
67+
* @param urls an array of {@link URL} representing the elements of the classpath
68+
* @return the classpath; on Windows, the path to an argument file is returned,
69+
* prefixed with '@'
70+
*/
71+
static ClasspathBuilder forURLs(URL... urls) {
72+
return new ClasspathBuilder(Arrays.asList(urls));
73+
}
74+
75+
Classpath build() {
76+
if (ObjectUtils.isEmpty(this.urls)) {
77+
return new Classpath("", Collections.emptyList());
78+
}
79+
if (this.urls.size() == 1) {
80+
Path file = toFile(this.urls.get(0));
81+
return new Classpath(file.toString(), List.of(file));
82+
}
83+
List<Path> files = this.urls.stream().map(ClasspathBuilder::toFile).toList();
84+
String argument = files.stream().map(Object::toString).collect(Collectors.joining(File.pathSeparator));
85+
if (needsClasspathArgFile()) {
86+
argument = createArgFile(argument);
87+
}
88+
return new Classpath(argument, files);
89+
}
90+
91+
protected boolean needsClasspathArgFile() {
92+
String os = System.getProperty("os.name");
93+
if (!StringUtils.hasText(os)) {
94+
return false;
95+
}
96+
// Windows limits the maximum command length, so we use an argfile
97+
return os.toLowerCase(Locale.ROOT).contains("win");
98+
}
99+
100+
/**
101+
* Create a temporary file with the given {@code} classpath. Return a suitable
102+
* argument to load the file, that is the full path prefixed by {@code @}.
103+
* @param classpath the classpath to use
104+
* @return a suitable argument for the classpath using a file
105+
*/
106+
private String createArgFile(String classpath) {
107+
try {
108+
return "@" + writeClasspathToFile(classpath);
109+
}
110+
catch (IOException ex) {
111+
return classpath;
112+
}
113+
}
114+
115+
private Path writeClasspathToFile(CharSequence classpath) throws IOException {
116+
Path tempFile = Files.createTempFile("spring-boot-", ".argfile");
117+
tempFile.toFile().deleteOnExit();
118+
Files.writeString(tempFile, "\"" + escape(classpath) + "\"", getCharset());
119+
return tempFile;
120+
}
121+
122+
private static Charset getCharset() {
123+
String nativeEncoding = System.getProperty("native.encoding");
124+
if (nativeEncoding == null) {
125+
return Charset.defaultCharset();
126+
}
127+
try {
128+
return Charset.forName(nativeEncoding);
129+
}
130+
catch (UnsupportedCharsetException ex) {
131+
return Charset.defaultCharset();
132+
}
133+
}
134+
135+
private static String escape(CharSequence content) {
136+
return content.toString().replace("\\", "\\\\");
137+
}
138+
139+
private static Path toFile(URL url) {
140+
try {
141+
return Paths.get(url.toURI());
142+
}
143+
catch (URISyntaxException ex) {
144+
throw new IllegalArgumentException(ex);
145+
}
146+
}
147+
148+
static final class Classpath {
149+
150+
private final String argument;
151+
152+
private final List<Path> elements;
153+
154+
private Classpath(String argument, List<Path> elements) {
155+
this.argument = argument;
156+
this.elements = elements;
157+
}
158+
159+
/**
160+
* Return the {@code -cp} argument value.
161+
* @return the argument to use
162+
*/
163+
String argument() {
164+
return this.argument;
165+
}
166+
167+
/**
168+
* Return the classpath elements.
169+
* @return the JAR files to use
170+
*/
171+
Stream<Path> elements() {
172+
return this.elements.stream();
173+
}
174+
175+
}
176+
177+
}

spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/CommandLineBuilder.java

+2-28
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2012-2024 the original author or authors.
2+
* Copyright 2012-2025 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -16,8 +16,6 @@
1616

1717
package org.springframework.boot.maven;
1818

19-
import java.io.File;
20-
import java.net.URISyntaxException;
2119
import java.net.URL;
2220
import java.util.ArrayList;
2321
import java.util.Arrays;
@@ -84,7 +82,7 @@ List<String> build() {
8482
}
8583
if (!this.classpathElements.isEmpty()) {
8684
commandLine.add("-cp");
87-
commandLine.add(ClasspathBuilder.build(this.classpathElements));
85+
commandLine.add(ClasspathBuilder.forURLs(this.classpathElements).build().argument());
8886
}
8987
commandLine.add(this.mainClass);
9088
if (!this.arguments.isEmpty()) {
@@ -93,30 +91,6 @@ List<String> build() {
9391
return commandLine;
9492
}
9593

96-
static class ClasspathBuilder {
97-
98-
static String build(List<URL> classpathElements) {
99-
StringBuilder classpath = new StringBuilder();
100-
for (URL element : classpathElements) {
101-
if (!classpath.isEmpty()) {
102-
classpath.append(File.pathSeparator);
103-
}
104-
classpath.append(toFile(element));
105-
}
106-
return classpath.toString();
107-
}
108-
109-
private static File toFile(URL element) {
110-
try {
111-
return new File(element.toURI());
112-
}
113-
catch (URISyntaxException ex) {
114-
throw new IllegalArgumentException(ex);
115-
}
116-
}
117-
118-
}
119-
12094
/**
12195
* Format System properties.
12296
*/

0 commit comments

Comments
 (0)