Skip to content

Commit 862570f

Browse files
author
Aleksandar Gradinac
committed
[GR-33590] Support @argFile in the native-image tool
PullRequest: graal/9704
2 parents 36cfbfa + 7c4a040 commit 862570f

File tree

3 files changed

+258
-23
lines changed

3 files changed

+258
-23
lines changed

substratevm/src/com.oracle.svm.driver/resources/Help.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,10 @@ Usage: native-image [options] class [imagename] [options]
77
(to build an image for a class)
88
or native-image [options] -jar jarfile [imagename] [options]
99
(to build an image for a jar file)
10+
1011
where options include:
12+
13+
@argument files one or more argument files containing options
1114
-cp <class search path of directories and zip/jar files>
1215
-classpath <class search path of directories and zip/jar files>
1316
--class-path <class search path of directories and zip/jar files>

substratevm/src/com.oracle.svm.driver/src/com/oracle/svm/driver/DefaultOptionHandler.java

Lines changed: 220 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,12 @@
2525
package com.oracle.svm.driver;
2626

2727
import java.io.File;
28+
import java.io.IOException;
29+
import java.nio.charset.StandardCharsets;
2830
import java.nio.file.Files;
2931
import java.nio.file.Path;
3032
import java.nio.file.Paths;
33+
import java.util.ArrayList;
3134
import java.util.Arrays;
3235
import java.util.List;
3336
import java.util.regex.Pattern;
@@ -61,6 +64,7 @@ class DefaultOptionHandler extends NativeImage.OptionHandler<NativeImage> {
6164
}
6265

6366
boolean useDebugAttach = false;
67+
boolean disableAtFiles = false;
6468

6569
private static void singleArgumentCheck(ArgumentQueue args, String arg) {
6670
if (!args.isEmpty()) {
@@ -220,6 +224,10 @@ public boolean consume(ArgumentQueue args) {
220224
nativeImage.showNewline();
221225
System.exit(0);
222226
return true;
227+
case "--disable-@files":
228+
args.poll();
229+
disableAtFiles = true;
230+
return true;
223231
}
224232

225233
String debugAttach = "--debug-attach";
@@ -306,9 +314,221 @@ public boolean consume(ArgumentQueue args) {
306314
}
307315
return true;
308316
}
317+
if (headArg.startsWith("@") && !disableAtFiles) {
318+
args.poll();
319+
headArg = headArg.substring(1);
320+
Path argFile = Paths.get(headArg);
321+
NativeImage.NativeImageArgsProcessor processor = nativeImage.new NativeImageArgsProcessor(argFile.toString());
322+
readArgFile(argFile).forEach(processor::accept);
323+
List<String> leftoverArgs = processor.apply(false);
324+
if (leftoverArgs.size() > 0) {
325+
NativeImage.showError("Found unrecognized options while parsing argument file '" + argFile + "':\n" + String.join("\n", leftoverArgs));
326+
}
327+
return true;
328+
}
309329
return false;
310330
}
311331

332+
// Ported from JDK11's java.base/share/native/libjli/args.c
333+
enum PARSER_STATE {
334+
FIND_NEXT,
335+
IN_COMMENT,
336+
IN_QUOTE,
337+
IN_ESCAPE,
338+
SKIP_LEAD_WS,
339+
IN_TOKEN
340+
}
341+
342+
class CTX_ARGS {
343+
PARSER_STATE state;
344+
int cptr;
345+
int eob;
346+
char quoteChar;
347+
List<String> parts;
348+
String options;
349+
}
350+
351+
// Ported from JDK11's java.base/share/native/libjli/args.c
352+
private List<String> readArgFile(Path file) {
353+
List<String> arguments = new ArrayList<>();
354+
// Use of the at sign (@) to recursively interpret files isn't supported.
355+
arguments.add("--disable-@files");
356+
357+
String options = null;
358+
try {
359+
options = new String(Files.readAllBytes(file), StandardCharsets.UTF_8);
360+
} catch (IOException e) {
361+
NativeImage.showError("Error reading argument file", e);
362+
}
363+
364+
CTX_ARGS ctx = new CTX_ARGS();
365+
ctx.state = PARSER_STATE.FIND_NEXT;
366+
ctx.parts = new ArrayList<>(4);
367+
ctx.quoteChar = '"';
368+
ctx.cptr = 0;
369+
ctx.eob = options.length();
370+
ctx.options = options;
371+
372+
String token = nextToken(ctx);
373+
while (token != null) {
374+
arguments.add(token);
375+
token = nextToken(ctx);
376+
}
377+
378+
// remaining partial token
379+
if (ctx.state == PARSER_STATE.IN_TOKEN || ctx.state == PARSER_STATE.IN_QUOTE) {
380+
if (ctx.parts.size() != 0) {
381+
token = String.join("", ctx.parts);
382+
arguments.add(token);
383+
}
384+
}
385+
return arguments;
386+
}
387+
388+
// Ported from JDK11's java.base/share/native/libjli/args.c
389+
@SuppressWarnings("fallthrough")
390+
private static String nextToken(CTX_ARGS ctx) {
391+
int nextc = ctx.cptr;
392+
int eob = ctx.eob;
393+
int anchor = nextc;
394+
String token;
395+
396+
for (; nextc < eob; nextc++) {
397+
char ch = ctx.options.charAt(nextc);
398+
399+
// Skip white space characters
400+
if (ctx.state == PARSER_STATE.FIND_NEXT || ctx.state == PARSER_STATE.SKIP_LEAD_WS) {
401+
while (ch == ' ' || ch == '\n' || ch == '\r' || ch == '\t' || ch == '\f') {
402+
nextc++;
403+
if (nextc >= eob) {
404+
return null;
405+
}
406+
ch = ctx.options.charAt(nextc);
407+
}
408+
ctx.state = (ctx.state == PARSER_STATE.FIND_NEXT) ? PARSER_STATE.IN_TOKEN : PARSER_STATE.IN_QUOTE;
409+
anchor = nextc;
410+
// Deal with escape sequences
411+
} else if (ctx.state == PARSER_STATE.IN_ESCAPE) {
412+
// concatenation directive
413+
if (ch == '\n' || ch == '\r') {
414+
ctx.state = PARSER_STATE.SKIP_LEAD_WS;
415+
} else {
416+
// escaped character
417+
char[] escaped = new char[2];
418+
escaped[1] = '\0';
419+
switch (ch) {
420+
case 'n':
421+
escaped[0] = '\n';
422+
break;
423+
case 'r':
424+
escaped[0] = '\r';
425+
break;
426+
case 't':
427+
escaped[0] = '\t';
428+
break;
429+
case 'f':
430+
escaped[0] = '\f';
431+
break;
432+
default:
433+
escaped[0] = ch;
434+
break;
435+
}
436+
ctx.parts.add(String.valueOf(escaped));
437+
ctx.state = PARSER_STATE.IN_QUOTE;
438+
}
439+
// anchor to next character
440+
anchor = nextc + 1;
441+
continue;
442+
// ignore comment to EOL
443+
} else if (ctx.state == PARSER_STATE.IN_COMMENT) {
444+
while (ch != '\n' && ch != '\r') {
445+
nextc++;
446+
if (nextc >= eob) {
447+
return null;
448+
}
449+
ch = ctx.options.charAt(nextc);
450+
}
451+
anchor = nextc + 1;
452+
ctx.state = PARSER_STATE.FIND_NEXT;
453+
continue;
454+
}
455+
456+
assert (ctx.state != PARSER_STATE.IN_ESCAPE);
457+
assert (ctx.state != PARSER_STATE.FIND_NEXT);
458+
assert (ctx.state != PARSER_STATE.SKIP_LEAD_WS);
459+
assert (ctx.state != PARSER_STATE.IN_COMMENT);
460+
461+
switch (ch) {
462+
case ' ':
463+
case '\t':
464+
case '\f':
465+
if (ctx.state == PARSER_STATE.IN_QUOTE) {
466+
continue;
467+
}
468+
// fall through
469+
case '\n':
470+
case '\r':
471+
if (ctx.parts.size() == 0) {
472+
token = ctx.options.substring(anchor, nextc);
473+
} else {
474+
ctx.parts.add(ctx.options.substring(anchor, nextc));
475+
token = String.join("", ctx.parts);
476+
ctx.parts = new ArrayList<>();
477+
}
478+
ctx.cptr = nextc + 1;
479+
ctx.state = PARSER_STATE.FIND_NEXT;
480+
return token;
481+
case '#':
482+
if (ctx.state == PARSER_STATE.IN_QUOTE) {
483+
continue;
484+
}
485+
ctx.state = PARSER_STATE.IN_COMMENT;
486+
anchor = nextc + 1;
487+
break;
488+
case '\\':
489+
if (ctx.state != PARSER_STATE.IN_QUOTE) {
490+
continue;
491+
}
492+
ctx.parts.add(ctx.options.substring(anchor, nextc));
493+
ctx.state = PARSER_STATE.IN_ESCAPE;
494+
// anchor after backslash character
495+
anchor = nextc + 1;
496+
break;
497+
case '\'':
498+
case '"':
499+
if (ctx.state == PARSER_STATE.IN_QUOTE && ctx.quoteChar != ch) {
500+
// not matching quote
501+
continue;
502+
}
503+
// partial before quote
504+
if (anchor != nextc) {
505+
ctx.parts.add(ctx.options.substring(anchor, nextc));
506+
}
507+
// anchor after quote character
508+
anchor = nextc + 1;
509+
if (ctx.state == PARSER_STATE.IN_TOKEN) {
510+
ctx.quoteChar = ch;
511+
ctx.state = PARSER_STATE.IN_QUOTE;
512+
} else {
513+
ctx.state = PARSER_STATE.IN_TOKEN;
514+
}
515+
break;
516+
default:
517+
break;
518+
}
519+
}
520+
521+
assert (nextc == eob);
522+
// Only need partial token, not comment or whitespaces
523+
if (ctx.state == PARSER_STATE.IN_TOKEN || ctx.state == PARSER_STATE.IN_QUOTE) {
524+
if (anchor < nextc) {
525+
// not yet return until end of stream, we have part of a token.
526+
ctx.parts.add(ctx.options.substring(anchor, nextc));
527+
}
528+
}
529+
return null;
530+
}
531+
312532
private void processClasspathArgs(String cpArgs) {
313533
for (String cp : cpArgs.split(File.pathSeparator, Integer.MAX_VALUE)) {
314534
/* Conform to `java` command empty cp entry handling. */

substratevm/src/com.oracle.svm.driver/src/com/oracle/svm/driver/NativeImage.java

Lines changed: 35 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -815,7 +815,7 @@ protected NativeImage(BuildConfiguration config) {
815815
/* Discover supported MacroOptions */
816816
optionRegistry = new MacroOption.Registry();
817817

818-
/* Default handler needs to be fist */
818+
/* Default handler needs to be first */
819819
defaultOptionHandler = new DefaultOptionHandler(this);
820820
registerOptionHandler(defaultOptionHandler);
821821
apiOptionHandler = new APIOptionHandler(this);
@@ -1372,21 +1372,24 @@ protected static List<String> createImageBuilderArgs(ArrayList<String> imageArgs
13721372
return result;
13731373
}
13741374

1375+
protected static String createVMInvocationArgumentFile(List<String> arguments) {
1376+
try {
1377+
Path argsFile = Files.createTempFile("vminvocation", ".args");
1378+
String joinedOptions = String.join("\n", arguments);
1379+
Files.write(argsFile, joinedOptions.getBytes());
1380+
argsFile.toFile().deleteOnExit();
1381+
return "@" + argsFile;
1382+
} catch (IOException e) {
1383+
throw showError(e.getMessage());
1384+
}
1385+
}
1386+
13751387
protected static String createImageBuilderArgumentFile(List<String> imageBuilderArguments) {
13761388
try {
1377-
Path argsFile = Files.createTempFile("native-image", "args");
1389+
Path argsFile = Files.createTempFile("native-image", ".args");
13781390
String joinedOptions = String.join("\0", imageBuilderArguments);
13791391
Files.write(argsFile, joinedOptions.getBytes());
1380-
Runtime.getRuntime().addShutdownHook(new Thread() {
1381-
@Override
1382-
public void run() {
1383-
try {
1384-
Files.delete(argsFile);
1385-
} catch (IOException e) {
1386-
System.err.println("Failed to delete temporary image builder arguments file: " + argsFile.toString());
1387-
}
1388-
}
1389-
});
1392+
argsFile.toFile().deleteOnExit();
13901393
return NativeImageGeneratorRunner.IMAGE_BUILDER_ARG_FILE_OPTION + argsFile.toString();
13911394
} catch (IOException e) {
13921395
throw showError(e.getMessage());
@@ -1395,37 +1398,46 @@ public void run() {
13951398

13961399
protected int buildImage(List<String> javaArgs, LinkedHashSet<Path> bcp, LinkedHashSet<Path> cp, LinkedHashSet<Path> mp, ArrayList<String> imageArgs, LinkedHashSet<Path> imagecp,
13971400
LinkedHashSet<Path> imagemp) {
1398-
/* Construct ProcessBuilder command from final arguments */
1399-
List<String> command = new ArrayList<>();
1400-
command.add(canonicalize(config.getJavaExecutable()).toString());
1401-
command.addAll(javaArgs);
1401+
List<String> arguments = new ArrayList<>();
1402+
arguments.addAll(javaArgs);
14021403
if (!bcp.isEmpty()) {
1403-
command.add(bcp.stream().map(Path::toString).collect(Collectors.joining(File.pathSeparator, "-Xbootclasspath/a:", "")));
1404+
arguments.add(bcp.stream().map(Path::toString).collect(Collectors.joining(File.pathSeparator, "-Xbootclasspath/a:", "")));
14041405
}
14051406

14061407
if (!cp.isEmpty()) {
1407-
command.addAll(Arrays.asList("-cp", cp.stream().map(Path::toString).collect(Collectors.joining(File.pathSeparator))));
1408+
arguments.addAll(Arrays.asList("-cp", cp.stream().map(Path::toString).collect(Collectors.joining(File.pathSeparator))));
14081409
}
14091410
if (!mp.isEmpty()) {
14101411
List<String> strings = Arrays.asList("--module-path", mp.stream().map(Path::toString).collect(Collectors.joining(File.pathSeparator)));
1411-
command.addAll(strings);
1412+
arguments.addAll(strings);
14121413
}
14131414

14141415
if (USE_NI_JPMS) {
1415-
command.addAll(Arrays.asList("--module", DEFAULT_GENERATOR_MODULE_NAME + "/" + DEFAULT_GENERATOR_CLASS_NAME));
1416+
arguments.addAll(Arrays.asList("--module", DEFAULT_GENERATOR_MODULE_NAME + "/" + DEFAULT_GENERATOR_CLASS_NAME));
14161417
} else {
1417-
command.add(config.getGeneratorMainClass());
1418+
arguments.add(config.getGeneratorMainClass());
14181419
}
14191420
if (IS_AOT && OS.getCurrent().hasProcFS) {
14201421
/*
14211422
* GR-8254: Ensure image-building VM shuts down even if native-image dies unexpected
14221423
* (e.g. using CTRL-C in Gradle daemon mode)
14231424
*/
1424-
command.addAll(Arrays.asList(SubstrateOptions.WATCHPID_PREFIX, "" + ProcessProperties.getProcessID()));
1425+
arguments.addAll(Arrays.asList(SubstrateOptions.WATCHPID_PREFIX, "" + ProcessProperties.getProcessID()));
14251426
}
14261427
List<String> finalImageBuilderArgs = createImageBuilderArgs(imageArgs, imagecp, imagemp);
1427-
List<String> completeCommandList = Stream.concat(command.stream(), finalImageBuilderArgs.stream()).collect(Collectors.toList());
1428+
1429+
/* Construct ProcessBuilder command from final arguments */
1430+
List<String> command = new ArrayList<>();
1431+
command.add(canonicalize(config.getJavaExecutable()).toString());
1432+
List<String> completeCommandList = new ArrayList<>(command);
1433+
if (config.useJavaModules()) { // Only in JDK9+ 'java' executable supports @argFiles.
1434+
command.add(createVMInvocationArgumentFile(arguments));
1435+
} else {
1436+
command.addAll(arguments);
1437+
}
14281438
command.add(createImageBuilderArgumentFile(finalImageBuilderArgs));
1439+
1440+
completeCommandList.addAll(Stream.concat(arguments.stream(), finalImageBuilderArgs.stream()).collect(Collectors.toList()));
14291441
final String commandLine = SubstrateUtil.getShellCommandString(completeCommandList, true);
14301442
if (isDiagnostics()) {
14311443
// write to the diagnostics dir

0 commit comments

Comments
 (0)