Skip to content

Commit fbbeeb2

Browse files
committed
#1566: Add Argfile class for handling javac argfile creation.
1 parent 861d097 commit fbbeeb2

3 files changed

Lines changed: 161 additions & 14 deletions

File tree

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
/*
2+
* SPDX-FileCopyrightText: Copyright (c) 2011-2026 Yegor Bugayenko
3+
* SPDX-License-Identifier: MIT
4+
*/
5+
package com.qulice.errorprone;
6+
7+
import java.io.File;
8+
import java.io.IOException;
9+
import java.io.UncheckedIOException;
10+
import java.nio.charset.StandardCharsets;
11+
import java.nio.file.Files;
12+
import java.util.List;
13+
14+
/**
15+
* A {@code javac} argfile materialised on disk.
16+
*
17+
* <p>Wraps a list of command-line tokens and writes them to a single
18+
* file in {@code javac}'s argfile format — one double-quoted token per
19+
* line with backslashes and double quotes escaped — so even very long
20+
* classpaths and source lists stay below the Windows
21+
* {@code CreateProcess} command-line limit when passed to {@code javac}
22+
* as {@code @path}. The {@code -J} launcher flags must NOT be included
23+
* here; {@code javac} forbids them inside argfiles.</p>
24+
*
25+
* @since 1.0
26+
*/
27+
public final class Argfile {
28+
29+
/**
30+
* Where the argfile will be written.
31+
*/
32+
private final File place;
33+
34+
/**
35+
* Tokens to write, in order, one per line.
36+
*/
37+
private final List<String> args;
38+
39+
/**
40+
* Constructor.
41+
* @param place Target file path
42+
* @param args Tokens to write
43+
*/
44+
public Argfile(final File place, final List<String> args) {
45+
this.place = place;
46+
this.args = args;
47+
}
48+
49+
/**
50+
* Write the argfile and return its path.
51+
* @return The path of the just-written argfile
52+
*/
53+
public File save() {
54+
final StringBuilder content = new StringBuilder(this.args.size() * 64);
55+
for (final String arg : this.args) {
56+
content.append('"');
57+
for (int idx = 0; idx < arg.length(); idx += 1) {
58+
final char chr = arg.charAt(idx);
59+
if (chr == '\\' || chr == '"') {
60+
content.append('\\');
61+
}
62+
content.append(chr);
63+
}
64+
content.append('"').append(System.lineSeparator());
65+
}
66+
try {
67+
Files.writeString(
68+
this.place.toPath(),
69+
content.toString(),
70+
StandardCharsets.UTF_8
71+
);
72+
} catch (final IOException ex) {
73+
throw new UncheckedIOException(
74+
String.format("Unable to write argfile %s", this.place), ex
75+
);
76+
}
77+
return this.place;
78+
}
79+
}

src/main/java/com/qulice/errorprone/ErrorProneValidator.java

Lines changed: 27 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -140,41 +140,54 @@ private List<String> run(final List<File> sources) {
140140
}
141141

142142
/**
143-
* Build the {@code javac} command line.
143+
* Build the {@code javac} command line. To stay below the
144+
* Windows {@code CreateProcess} 32 KB command-line limit even on
145+
* projects with thousands of sources or long classpaths, every
146+
* argument other than the launcher itself and the {@code -J}
147+
* flags (which {@code javac} forbids inside argfiles) is written
148+
* to a temporary argfile and passed as {@code @argfile}.
144149
* @param sources Java source files to feed
145150
* @return Argv
146151
*/
147152
private List<String> command(final List<File> sources) {
148153
final List<String> command = new ArrayList<>(
149-
sources.size() + ErrorProneValidator.JVM_FLAGS.size() + 11
154+
ErrorProneValidator.JVM_FLAGS.size() + 2
150155
);
151156
command.add(ErrorProneValidator.javac());
152157
for (final String flag : ErrorProneValidator.JVM_FLAGS) {
153158
command.add("-J".concat(flag));
154159
}
155-
command.add("-XDcompilePolicy=simple");
156-
command.add("-XDaddTypeAnnotationsToSymbol=true");
157-
command.add("--should-stop=ifError=FLOW");
158-
command.add("-proc:none");
159-
command.add("-Xplugin:ErrorProne");
160-
command.add("-processorpath");
161-
command.add(ErrorProneValidator.pluginClasspath());
162160
final File outdir = new File(this.env.tempdir(), "errorprone-classes");
163161
if (!outdir.exists() && !outdir.mkdirs()) {
164162
throw new IllegalStateException(
165163
String.format("Unable to create %s", outdir)
166164
);
167165
}
168-
command.add("-d");
169-
command.add(outdir.getAbsolutePath());
166+
final List<String> args = new ArrayList<>(sources.size() + 11);
167+
args.add("-XDcompilePolicy=simple");
168+
args.add("-XDaddTypeAnnotationsToSymbol=true");
169+
args.add("--should-stop=ifError=FLOW");
170+
args.add("-proc:none");
171+
args.add("-Xplugin:ErrorProne");
172+
args.add("-processorpath");
173+
args.add(ErrorProneValidator.pluginClasspath());
174+
args.add("-d");
175+
args.add(outdir.getAbsolutePath());
170176
final Collection<String> classpath = this.env.classpath();
171177
if (!classpath.isEmpty()) {
172-
command.add("-classpath");
173-
command.add(String.join(File.pathSeparator, classpath));
178+
args.add("-classpath");
179+
args.add(String.join(File.pathSeparator, classpath));
174180
}
175181
for (final File source : sources) {
176-
command.add(source.getAbsolutePath());
182+
args.add(source.getAbsolutePath());
177183
}
184+
command.add(
185+
"@".concat(
186+
new Argfile(
187+
new File(this.env.tempdir(), "errorprone-args.txt"), args
188+
).save().getAbsolutePath()
189+
)
190+
);
178191
return command;
179192
}
180193

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
/*
2+
* SPDX-FileCopyrightText: Copyright (c) 2011-2026 Yegor Bugayenko
3+
* SPDX-License-Identifier: MIT
4+
*/
5+
package com.qulice.errorprone;
6+
7+
import java.io.File;
8+
import java.nio.charset.StandardCharsets;
9+
import java.nio.file.Files;
10+
import java.util.List;
11+
import org.hamcrest.MatcherAssert;
12+
import org.hamcrest.Matchers;
13+
import org.junit.jupiter.api.Test;
14+
import org.junit.jupiter.api.io.TempDir;
15+
16+
/**
17+
* Test case for {@link Argfile}.
18+
* @since 1.0
19+
*/
20+
final class ArgfileTest {
21+
22+
@Test
23+
void writesQuotedTokensOnePerLine(@TempDir final File dir) throws Exception {
24+
final File path = new File(dir, "args.txt");
25+
new Argfile(
26+
path,
27+
List.of("-classpath", "C:\\one\\two.jar;C:\\Program Files\\x.jar")
28+
).save();
29+
MatcherAssert.assertThat(
30+
"argfile must escape backslashes and keep one token per line",
31+
Files.readString(path.toPath(), StandardCharsets.UTF_8),
32+
Matchers.equalTo(
33+
String.join(
34+
System.lineSeparator(),
35+
"\"-classpath\"",
36+
"\"C:\\\\one\\\\two.jar;C:\\\\Program Files\\\\x.jar\"",
37+
""
38+
)
39+
)
40+
);
41+
}
42+
43+
@Test
44+
void escapesEmbeddedDoubleQuotes(@TempDir final File dir) throws Exception {
45+
final File path = new File(dir, "args.txt");
46+
new Argfile(path, List.of("a\"b")).save();
47+
MatcherAssert.assertThat(
48+
"embedded double quotes must be backslash-escaped",
49+
Files.readString(path.toPath(), StandardCharsets.UTF_8),
50+
Matchers.equalTo(
51+
String.join(System.lineSeparator(), "\"a\\\"b\"", "")
52+
)
53+
);
54+
}
55+
}

0 commit comments

Comments
 (0)