package org.graalvm.launcher;
import java.io.File;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Deque;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.graalvm.home.HomeFinder;
import org.graalvm.options.OptionCategory;
import org.graalvm.polyglot.Context;
import org.graalvm.polyglot.Engine;
import org.graalvm.polyglot.Language;
import org.graalvm.polyglot.PolyglotException;
import org.graalvm.polyglot.PolyglotException.StackFrame;
import org.graalvm.polyglot.Source;
import org.graalvm.polyglot.Value;
public final class PolyglotLauncher extends LanguageLauncherBase {
private String mainLanguage = null;
private boolean verbose = false;
private boolean version = false;
private boolean shell = false;
@Override
protected void printHelp(OptionCategory maxCategory) {
Engine engine = getTempEngine();
printVersion(engine);
System.out.println();
System.out.println("Usage: polyglot [OPTION]... [FILE] [ARGS]...");
List<Language> languages = sortedLanguages(engine);
System.out.print("Available languages: ");
String sep = "";
for (Language language : languages) {
System.out.print(sep);
System.out.print(language.getId());
sep = ", ";
}
System.out.println();
System.out.println("Basic Options:");
launcherOption("--language <lang>", "Specifies the main language.");
launcherOption("--file [<lang>:]FILE", "Additional file to execute.");
launcherOption("--eval [<lang>:]CODE", "Evaluates code snippets, for example, '--eval js:42'.");
launcherOption("--shell", "Start a multi language shell.");
launcherOption("--verbose", "Enable verbose stack trace for internal errors.");
}
@Override
protected void collectArguments(Set<String> args) {
args.addAll(Arrays.asList(
"--language",
"--file [<lang>:]FILE",
"--eval [<lang>:]CODE",
"--shell"));
super.collectArguments(args);
}
@Override
protected void printVersion() {
printVersion(getTempEngine());
printPolyglotVersions();
}
protected void printVersion(Engine engine) {
String engineImplementationName = engine.getImplementationName();
if (isAOT()) {
engineImplementationName += " Native";
}
println(String.format("%s polyglot launcher %s", engineImplementationName, engine.getVersion()));
}
private List<String> parsePolyglotLauncherOptions(Deque<String> arguments, List<Script> scripts) {
List<String> unrecognizedArgs = new ArrayList<>();
while (!arguments.isEmpty()) {
String arg = arguments.removeFirst();
if (arg.equals("--eval") || arg.equals("--file")) {
String value = getNextArgument(arguments, arg);
int index = value.indexOf(":");
String languageId = null;
String script;
if (index != -1) {
languageId = value.substring(0, index);
script = value.substring(index + 1);
} else {
script = value;
}
if (arg.equals("--eval")) {
scripts.add(new EvalScript(languageId, script));
} else {
scripts.add(new FileScript(languageId, script, false));
}
} else if (arg.equals("--")) {
break;
} else if (!arg.startsWith("-")) {
scripts.add(new FileScript(mainLanguage, arg, true));
break;
} else if (arg.equals("--version")) {
version = true;
} else if (arg.equals("--shell")) {
shell = true;
} else if (arg.equals("--verbose")) {
verbose = true;
} else if (arg.equals("--language")) {
mainLanguage = getNextArgument(arguments, arg);
} else {
unrecognizedArgs.add(arg);
}
}
return unrecognizedArgs;
}
private String getNextArgument(Deque<String> arguments, String option) {
if (arguments.isEmpty()) {
throw abort(option + " expects an argument");
}
return arguments.removeFirst();
}
private void launch(String[] args) {
List<String> argumentsList = new ArrayList<>(Arrays.asList(args));
for (;;) {
try {
launchImpl(argumentsList);
} catch (RestartInJVMException ex) {
argumentsList.add(0, "--jvm");
continue;
}
return;
}
}
private void launchImpl(List<String> argumentsList) {
if (isAOT()) {
List<String> originalArgs = Collections.unmodifiableList(new ArrayList<>(argumentsList));
maybeNativeExec(originalArgs, argumentsList, true);
}
final Deque<String> arguments = new ArrayDeque<>(argumentsList);
if (!arguments.isEmpty() && arguments.getFirst().equals("--use-launcher")) {
arguments.removeFirst();
String launcherName = getNextArgument(arguments, "--use-launcher");
switchToLauncher(launcherName, new HashMap<>(), new ArrayList<>(arguments));
return;
}
List<Script> scripts = new ArrayList<>();
List<String> unrecognizedArgs = parsePolyglotLauncherOptions(arguments, scripts);
Map<String, String> polyglotOptions = new HashMap<>();
parseUnrecognizedOptions(null, polyglotOptions, unrecognizedArgs);
String[] programArgs = arguments.toArray(new String[0]);
if (runLauncherAction()) {
return;
}
argumentsProcessingDone();
final Context.Builder contextBuilder = Context.newBuilder().options(polyglotOptions);
contextBuilder.allowAllAccess(true);
setupContextBuilder(contextBuilder);
if (version) {
printVersion(Engine.newBuilder().options(polyglotOptions).build());
throw exit();
}
if (shell) {
runShell(contextBuilder);
} else if (scripts.size() == 0) {
throw abort("No files specified. Use --help for usage instructions.");
} else {
runScripts(scripts, contextBuilder, programArgs);
}
}
static final Map<String, Class<AbstractLanguageLauncher>> AOT_LAUNCHER_CLASSES;
static {
if (IS_AOT) {
AOT_LAUNCHER_CLASSES = new HashMap<>();
List<URL> classpath = new ArrayList<>();
List<String> classes = new ArrayList<>();
HomeFinder.getInstance().getLanguageHomes().values().stream().flatMap(PolyglotLauncher::loadPolyglotConfig).forEach(c -> {
c.classpath.stream().map(c.dir::resolve).map(p -> {
if (!Files.exists(p)) {
throw new RuntimeException(p + " does not exist");
}
try {
return p.normalize().toUri().toURL();
} catch (MalformedURLException e) {
throw new RuntimeException(e);
}
}).forEach(classpath::add);
classes.add(c.launcher);
});
URLClassLoader loader = new URLClassLoader(classpath.toArray(new URL[0]), PolyglotLauncher.class.getClassLoader());
for (String launcher : classes) {
AOT_LAUNCHER_CLASSES.put(launcher, getLauncherClass(launcher, loader));
}
} else {
AOT_LAUNCHER_CLASSES = null;
}
}
private static Stream<PolyglotLauncherConfig> loadPolyglotConfig(Path p) {
Path configPath = p.resolve("polyglot.config");
if (!Files.exists(configPath)) {
return null;
}
try {
return Files.lines(configPath, StandardCharsets.UTF_8).map(String::trim).filter(s -> !s.isEmpty()).map(l -> PolyglotLauncherConfig.parse(l, configPath));
} catch (IOException e) {
throw new RuntimeException(e);
}
}
private static final class PolyglotLauncherConfig {
final Path dir;
final List<String> classpath;
final String launcher;
static PolyglotLauncherConfig parse(String spec, Path context) {
String[] parts = spec.split("\\|");
if (parts.length != 2) {
throw new RuntimeException("Expected 2 `|`-separated parts in polyglot config (" + context + "). Got: " + Arrays.toString(parts));
}
return new PolyglotLauncherConfig(context.getParent(), Arrays.asList(parts[0].split(":")), parts[1]);
}
PolyglotLauncherConfig(Path dir, List<String> classpath, String launcher) {
this.dir = dir;
this.classpath = classpath;
this.launcher = launcher;
}
}
@SuppressWarnings("unchecked")
private static Class<AbstractLanguageLauncher> getLauncherClass(String launcherName, ClassLoader loader) {
try {
Class<?> launcherClass = Class.forName(launcherName, false, loader);
if (!AbstractLanguageLauncher.class.isAssignableFrom(launcherClass)) {
throw new RuntimeException("Launcher class " + launcherName + " does not extend AbstractLanguageLauncher");
}
return (Class<AbstractLanguageLauncher>) launcherClass;
} catch (ClassNotFoundException e) {
throw new RuntimeException("Launcher class " + launcherName + " does not exist", e);
}
}
private void switchToLauncher(String launcherName, Map<String, String> options, List<String> args) {
Class<AbstractLanguageLauncher> launcherClass;
if (isAOT()) {
launcherClass = AOT_LAUNCHER_CLASSES.get(launcherName);
if (launcherClass == null) {
throw abort("Could not find class '" + launcherName +
"'.\nYou might need to rebuild the polyglot launcher with 'gu rebuild-images polyglot'.\nThe following launchers are available:\n" +
AOT_LAUNCHER_CLASSES.keySet().stream().sorted().map(s -> " - " + s).collect(Collectors.joining("\n")));
}
} else {
List<URL> classpath = new ArrayList<>();
HomeFinder.getInstance().getLanguageHomes().values().stream().flatMap(PolyglotLauncher::loadPolyglotConfig).filter(c -> launcherName.endsWith(c.launcher)).forEach(c -> {
c.classpath.stream().map(c.dir::resolve).map(p -> {
if (!Files.exists(p)) {
throw new RuntimeException(p + " does not exist");
}
try {
return p.normalize().toUri().toURL();
} catch (MalformedURLException e) {
throw new RuntimeException(e);
}
}).forEach(classpath::add);
});
URLClassLoader loader = new URLClassLoader(classpath.toArray(new URL[0]), PolyglotLauncher.class.getClassLoader());
launcherClass = getLauncherClass(launcherName, loader);
}
AbstractLanguageLauncher launcher;
try {
launcher = launcherClass.getDeclaredConstructor().newInstance();
} catch (Exception e) {
throw new RuntimeException("Failed to instantiate launcher class " + launcherName, e);
}
launcher.setPolyglot(true);
launcher.launch(args, options, false);
}
private void checkLanguage(String language, Engine engine) {
if (language == null) {
return;
}
if (!engine.getLanguages().containsKey(language)) {
throw abort("Language '" + language + "' not found");
}
}
private void runScripts(List<Script> scripts, Context.Builder contextBuilder, String[] programArgs) {
Script mainScript = scripts.get(scripts.size() - 1);
try (Context context = contextBuilder.arguments(mainScript.getLanguage(), programArgs).build()) {
Engine engine = context.getEngine();
checkLanguage(mainLanguage, engine);
for (Script script : scripts) {
checkLanguage(script.languageId, engine);
}
for (Script script : scripts) {
try {
Value result = context.eval(script.getSource());
if (script.isPrintResult()) {
System.out.println(result);
}
} catch (PolyglotException e) {
throw abort(e);
} catch (IOException e) {
throw abort(e);
} catch (Throwable t) {
throw abort(t);
}
}
}
}
AbortException abort(PolyglotException e) {
if (e.isInternalError()) {
System.err.println("Internal error occurred: " + e.toString());
if (verbose) {
e.printStackTrace(System.err);
} else {
System.err.println("Run with --verbose to see the full stack trace.");
}
throw exit(1);
} else if (e.isExit()) {
throw exit(e.getExitStatus());
} else if (e.isSyntaxError()) {
throw abort(e.getMessage(), 1);
} else {
List<StackFrame> trace = new ArrayList<>();
for (StackFrame stackFrame : e.getPolyglotStackTrace()) {
trace.add(stackFrame);
}
for (int i = trace.size() - 1; i >= 0; i--) {
if (trace.get(i).isHostFrame()) {
trace.remove(i);
} else {
break;
}
}
if (e.isHostException()) {
System.err.println(e.asHostException().toString());
} else {
System.err.println(e.getMessage());
}
for (StackFrame stackFrame : trace) {
System.err.print(" at ");
System.err.println(stackFrame.toString());
}
throw exit(1);
}
}
private void runShell(Context.Builder contextBuilder) {
try (Context context = contextBuilder.build();
MultiLanguageShell polyglotShell = new MultiLanguageShell(context, mainLanguage)) {
throw exit(polyglotShell.runRepl());
} catch (IOException e) {
throw abort(e);
}
}
public static void main(String[] args) {
PolyglotLauncher launcher = new PolyglotLauncher();
try {
try {
launcher.launch(args);
} catch (AbortException e) {
throw e;
} catch (PolyglotException e) {
launcher.handlePolyglotException(e);
} catch (Throwable t) {
throw launcher.abort(t);
}
} catch (AbortException e) {
launcher.handleAbortException(e);
}
}
private static final class RestartInJVMException extends RuntimeException {
static final long serialVersionUID = 1;
RestartInJVMException() {
}
}
private abstract class Script {
final String languageId;
Script(String languageId) {
this.languageId = languageId;
}
final String getLanguage() {
String language = languageId;
if (language == null) {
language = findLanguage();
}
if (language == null) {
if (mainLanguage != null) {
return mainLanguage;
}
}
if (language == null) {
final String msg = "Cannot determine language for '%s' %s";
if (isAOT()) {
getError().println(String.format(msg, this, "Trying with --jvm mode..."));
throw new RestartInJVMException();
}
throw abort(String.format(msg, this, this.getLanguageSpecifierHelp()));
}
return language;
}
protected String findLanguage() {
return null;
}
public abstract boolean isPrintResult();
public abstract Source getSource() throws IOException;
protected abstract String getLanguageSpecifierHelp();
}
private class FileScript extends Script {
private final boolean main;
final String file;
private Source source;
FileScript(String languageId, String file, boolean main) {
super(languageId);
this.file = file;
this.main = main;
}
@Override
public Source getSource() throws IOException {
if (source == null) {
source = Source.newBuilder(getLanguage(), new File(file)).build();
}
return source;
}
@Override
protected String findLanguage() {
try {
return Source.findLanguage(new File(file));
} catch (IOException e) {
return null;
}
}
@Override
public String toString() {
return file;
}
@Override
protected String getLanguageSpecifierHelp() {
if (main) {
return "use the --language option";
} else {
return "specify the language using --file <language>:<file>. ";
}
}
@Override
public boolean isPrintResult() {
return false;
}
}
private class EvalScript extends Script {
final String script;
EvalScript(String languageId, String script) {
super(languageId);
this.script = script;
}
@Override
public Source getSource() {
return Source.create(getLanguage(), script);
}
@Override
public String toString() {
return script;
}
@Override
protected String getLanguageSpecifierHelp() {
return "specify the language using --eval <language>:<source>.";
}
@Override
public boolean isPrintResult() {
return true;
}
}
}