package com.oracle.svm.configure;
import java.io.BufferedReader;
import java.io.FileOutputStream;
import java.io.FileReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.net.URI;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.NoSuchFileException;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Iterator;
import java.util.List;
import java.util.stream.Collectors;
import com.oracle.svm.configure.config.ConfigurationSet;
import com.oracle.svm.configure.filters.FilterConfigurationParser;
import com.oracle.svm.configure.filters.ModuleFilterTools;
import com.oracle.svm.configure.filters.RuleNode;
import com.oracle.svm.configure.json.JsonWriter;
import com.oracle.svm.configure.trace.AccessAdvisor;
import com.oracle.svm.configure.trace.TraceProcessor;
import com.oracle.svm.core.SubstrateUtil;
import com.oracle.svm.core.util.VMError;
public class ConfigurationTool {
private static final String HELP_TEXT = getResource("/Help.txt") + System.lineSeparator();
private static class UsageException extends RuntimeException {
static final long serialVersionUID = 1L;
UsageException(String message) {
super(message);
}
}
public static void main(String[] args) {
try {
if (args.length == 0) {
throw new UsageException("No arguments provided.");
}
Iterator<String> argsIter = Arrays.asList(args).iterator();
String first = argsIter.next();
if (first.equals("command-file")) {
argsIter = handleCommandFile(argsIter);
if (!argsIter.hasNext()) {
throw new UsageException("No arguments provided in the command file.");
}
first = argsIter.next();
}
switch (first) {
case "generate":
generate(argsIter, false);
break;
case "process-trace":
generate(argsIter, true);
break;
case "generate-filters":
generateFilterRules(argsIter);
break;
case "help":
case "--help":
System.out.println(HELP_TEXT);
break;
default:
throw new UsageException("Unknown subcommand: " + first);
}
} catch (UsageException e) {
System.err.println(e.getMessage() + System.lineSeparator() +
"Use 'native-image-configure help' for usage.");
System.exit(2);
} catch (Exception e) {
e.printStackTrace();
System.exit(1);
}
}
private static Path requirePath(String current, String value) {
if (value == null || value.trim().isEmpty()) {
throw new UsageException("Argument must be provided for: " + current);
}
return Paths.get(value);
}
private static URI requirePathUri(String current, String value) {
return requirePath(current, value).toUri();
}
private static Iterator<String> handleCommandFile(Iterator<String> args) {
if (!args.hasNext()) {
throw new UsageException("Path to a command file must be provided.");
}
Path filePath = Paths.get(args.next());
if (args.hasNext()) {
throw new UsageException("Too many arguments to command-file passed. Expected a single argument: <path to a command file>.");
}
try {
List<String> lines = Files.readAllLines(filePath);
return lines.iterator();
} catch (IOException e) {
throw new UsageException("Failed to read the command file at " + filePath + ". Check if the file exists, you have the required permissions and that the file is actually a text file.");
}
}
@SuppressWarnings("fallthrough")
private static void generate(Iterator<String> argsIter, boolean acceptTraceFileArgs) throws IOException {
List<URI> traceInputs = new ArrayList<>();
boolean builtinCallerFilter = true;
boolean builtinHeuristicFilter = true;
List<Path> callerFilterFiles = new ArrayList<>();
ConfigurationSet inputSet = new ConfigurationSet();
ConfigurationSet outputSet = new ConfigurationSet();
while (argsIter.hasNext()) {
String[] parts = argsIter.next().split("=", 2);
String current = parts[0];
String value = (parts.length > 1) ? parts[1] : null;
ConfigurationSet set = outputSet;
switch (current) {
case "--input-dir":
inputSet.addDirectory(requirePath(current, value));
break;
case "--output-dir":
Path directory = requirePath(current, value);
if (!Files.exists(directory)) {
Files.createDirectory(directory);
} else if (!Files.isDirectory(directory)) {
throw new NoSuchFileException(value);
}
outputSet.addDirectory(directory);
break;
case "--reflect-input":
set = inputSet;
case "--reflect-output":
set.getReflectConfigPaths().add(requirePathUri(current, value));
break;
case "--jni-input":
set = inputSet;
case "--jni-output":
set.getJniConfigPaths().add(requirePathUri(current, value));
break;
case "--proxy-input":
set = inputSet;
case "--proxy-output":
set.getProxyConfigPaths().add(requirePathUri(current, value));
break;
case "--resource-input":
set = inputSet;
case "--resource-output":
set.getResourceConfigPaths().add(requirePathUri(current, value));
break;
case "--serialization-input":
set = inputSet;
case "--serialization-output":
set.getSerializationConfigPaths().add(requirePathUri(current, value));
break;
case "--trace-input":
traceInputs.add(requirePathUri(current, value));
break;
case "--no-filter":
builtinCallerFilter = false;
builtinHeuristicFilter = false;
break;
case "--no-builtin-caller-filter":
builtinCallerFilter = false;
break;
case "--no-builtin-heuristic-filter":
builtinHeuristicFilter = false;
break;
case "--caller-filter-file":
callerFilterFiles.add(requirePath(current, value));
break;
case "--":
if (acceptTraceFileArgs) {
argsIter.forEachRemaining(arg -> traceInputs.add(Paths.get(arg).toUri()));
} else {
throw new UsageException("Unknown argument: " + current);
}
break;
default:
if (!acceptTraceFileArgs || current.startsWith("-")) {
throw new UsageException("Unknown argument: " + current);
}
traceInputs.add(Paths.get(current).toUri());
break;
}
}
RuleNode callersFilter = null;
if (!builtinCallerFilter) {
callersFilter = RuleNode.createRoot();
callersFilter.addOrGetChildren("**", RuleNode.Inclusion.Include);
}
if (!callerFilterFiles.isEmpty()) {
if (callersFilter == null) {
callersFilter = AccessAdvisor.copyBuiltinCallerFilterTree();
}
for (Path path : callerFilterFiles) {
try {
FilterConfigurationParser parser = new FilterConfigurationParser(callersFilter);
parser.parseAndRegister(new FileReader(path.toFile()));
} catch (Exception e) {
throw new UsageException("Cannot parse filter file " + path + ": " + e);
}
}
callersFilter.removeRedundantNodes();
}
AccessAdvisor advisor = new AccessAdvisor();
advisor.setHeuristicsEnabled(builtinHeuristicFilter);
if (callersFilter != null) {
advisor.setCallerFilterTree(callersFilter);
}
TraceProcessor p;
try {
p = new TraceProcessor(advisor, inputSet.loadJniConfig(ConfigurationSet.FAIL_ON_EXCEPTION), inputSet.loadReflectConfig(ConfigurationSet.FAIL_ON_EXCEPTION),
inputSet.loadProxyConfig(ConfigurationSet.FAIL_ON_EXCEPTION), inputSet.loadResourceConfig(ConfigurationSet.FAIL_ON_EXCEPTION),
inputSet.loadSerializationConfig(ConfigurationSet.FAIL_ON_EXCEPTION));
} catch (IOException e) {
throw e;
} catch (Throwable t) {
throw new RuntimeException(t);
}
if (traceInputs.isEmpty() && inputSet.isEmpty()) {
throw new UsageException("No inputs specified.");
}
for (URI uri : traceInputs) {
try (Reader reader = Files.newBufferedReader(Paths.get(uri))) {
p.process(reader);
}
}
if (outputSet.isEmpty()) {
System.err.println("Warning: no outputs specified, validating inputs only.");
}
for (URI uri : outputSet.getReflectConfigPaths()) {
try (JsonWriter writer = new JsonWriter(Paths.get(uri))) {
p.getReflectionConfiguration().printJson(writer);
}
}
for (URI uri : outputSet.getJniConfigPaths()) {
try (JsonWriter writer = new JsonWriter(Paths.get(uri))) {
p.getJniConfiguration().printJson(writer);
}
}
for (URI uri : outputSet.getProxyConfigPaths()) {
try (JsonWriter writer = new JsonWriter(Paths.get(uri))) {
p.getProxyConfiguration().printJson(writer);
}
}
for (URI uri : outputSet.getResourceConfigPaths()) {
try (JsonWriter writer = new JsonWriter(Paths.get(uri))) {
p.getResourceConfiguration().printJson(writer);
}
}
for (URI uri : outputSet.getSerializationConfigPaths()) {
try (JsonWriter writer = new JsonWriter(Paths.get(uri))) {
p.getSerializationConfiguration().printJson(writer);
}
}
}
private static void generateFilterRules(Iterator<String> argsIter) throws IOException {
Path outputPath = null;
boolean reduce = false;
List<String> args = new ArrayList<>();
while (argsIter.hasNext()) {
String arg = argsIter.next();
if (arg.startsWith("--reduce")) {
String[] parts = arg.split("=", 2);
reduce = (parts.length < 2) || Boolean.parseBoolean(parts[1]);
} else {
args.add(arg);
}
}
RuleNode rootNode = null;
for (String arg : args) {
String[] parts = arg.split("=", 2);
String current = parts[0];
String value = (parts.length > 1) ? parts[1] : null;
switch (current) {
case "--include-packages-from-modules":
case "--exclude-packages-from-modules":
if (SubstrateUtil.HOSTED) {
if (rootNode != null) {
throw new UsageException(current + " must be specified before other rule-creating arguments");
}
RuleNode.Inclusion inclusion = current.startsWith("--include") ? RuleNode.Inclusion.Include : RuleNode.Inclusion.Exclude;
String[] moduleNames = (value != null) ? value.split(",") : new String[0];
rootNode = ModuleFilterTools.generateFromModules(moduleNames, inclusion, reduce);
} else {
throw new UsageException(current + " is currently not supported in the native-image build of this tool.");
}
break;
case "--input-file":
rootNode = maybeCreateRootNode(rootNode);
try (FileReader reader = new FileReader(requirePath(current, value).toFile())) {
FilterConfigurationParser parser = new FilterConfigurationParser(rootNode);
parser.parseAndRegister(reader);
}
break;
case "--output-file":
outputPath = requirePath(current, value);
break;
case "--include-classes":
rootNode = addSingleRule(rootNode, current, value, RuleNode.Inclusion.Include);
break;
case "--exclude-classes":
rootNode = addSingleRule(rootNode, current, value, RuleNode.Inclusion.Exclude);
break;
default:
throw new UsageException("Unknown argument: " + current);
}
}
rootNode = maybeCreateRootNode(rootNode);
rootNode.removeRedundantNodes();
if (outputPath != null) {
try (FileOutputStream os = new FileOutputStream(outputPath.toFile())) {
rootNode.printJsonTree(os);
}
} else {
rootNode.printJsonTree(System.out);
}
}
private static RuleNode maybeCreateRootNode(RuleNode rootNode) {
return (rootNode != null) ? rootNode : RuleNode.createRoot();
}
private static RuleNode addSingleRule(RuleNode rootNode, String argName, String qualifiedPkg, RuleNode.Inclusion inclusion) {
RuleNode root = maybeCreateRootNode(rootNode);
if (qualifiedPkg == null || qualifiedPkg.isEmpty()) {
throw new UsageException("Argument must be provided for: " + argName);
}
if (qualifiedPkg.indexOf('*') != -1 && !qualifiedPkg.endsWith(".**") && !qualifiedPkg.endsWith(".*")) {
throw new UsageException("Rule may only contain '*' at the end, either as .* to include all classes in the package, " +
"or as .** to include all classes in the package and all of its subpackages");
}
root.addOrGetChildren(qualifiedPkg, inclusion);
return root;
}
private static String getResource(String resourceName) {
try (InputStream input = ConfigurationTool.class.getResourceAsStream(resourceName)) {
BufferedReader reader = new BufferedReader(new InputStreamReader(input, StandardCharsets.UTF_8));
return reader.lines().collect(Collectors.joining("\n"));
} catch (IOException e) {
VMError.shouldNotReachHere(e);
}
return null;
}
}