diff --git a/tools/cli/pom.xml b/tools/cli/pom.xml index dc6031ac9..56c4a098f 100644 --- a/tools/cli/pom.xml +++ b/tools/cli/pom.xml @@ -39,6 +39,24 @@ commons-cli 1.2 + + org.teavm + teavm-classlib + ${project.version} + runtime + + + org.teavm + teavm-metaprogramming-impl + ${project.version} + runtime + + + org.teavm + teavm-jso-impl + ${project.version} + runtime + @@ -59,6 +77,37 @@ org.apache.maven.plugins maven-javadoc-plugin + + + org.apache.maven.plugins + maven-jar-plugin + + + + org.teavm.cli.TeaVMRunner + + + + + + + org.apache.maven.plugins + maven-shade-plugin + 3.1.0 + + + package + + shade + + + + + + + + + \ No newline at end of file diff --git a/tools/cli/src/main/java/org/teavm/cli/AccumulatingTeaVMToolLog.java b/tools/cli/src/main/java/org/teavm/cli/AccumulatingTeaVMToolLog.java new file mode 100644 index 000000000..b64bbca3f --- /dev/null +++ b/tools/cli/src/main/java/org/teavm/cli/AccumulatingTeaVMToolLog.java @@ -0,0 +1,124 @@ +/* + * Copyright 2017 Alexey Andreev. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.teavm.cli; + +import java.util.ArrayList; +import java.util.List; +import org.teavm.tooling.TeaVMToolLog; + +public class AccumulatingTeaVMToolLog implements TeaVMToolLog { + private TeaVMToolLog delegatedLog; + private List pendingMessages = new ArrayList<>(); + + public AccumulatingTeaVMToolLog(TeaVMToolLog delegatedLog) { + this.delegatedLog = delegatedLog; + } + + public void flush() { + for (Message message : pendingMessages) { + switch (message.kind) { + case INFO: + if (message.throwable != null) { + delegatedLog.info(message.text, message.throwable); + } else { + delegatedLog.info(message.text); + } + break; + case DEBUG: + if (message.throwable != null) { + delegatedLog.debug(message.text, message.throwable); + } else { + delegatedLog.debug(message.text); + } + break; + case WARNING: + if (message.throwable != null) { + delegatedLog.warning(message.text, message.throwable); + } else { + delegatedLog.warning(message.text); + } + break; + case ERROR: + if (message.throwable != null) { + delegatedLog.error(message.text, message.throwable); + } else { + delegatedLog.error(message.text); + } + break; + } + } + pendingMessages.clear(); + } + + @Override + public void info(String text) { + pendingMessages.add(new Message(MessageKind.INFO, text, null)); + } + + @Override + public void debug(String text) { + pendingMessages.add(new Message(MessageKind.DEBUG, text, null)); + } + + @Override + public void warning(String text) { + pendingMessages.add(new Message(MessageKind.WARNING, text, null)); + } + + @Override + public void error(String text) { + pendingMessages.add(new Message(MessageKind.ERROR, text, null)); + } + + @Override + public void info(String text, Throwable e) { + pendingMessages.add(new Message(MessageKind.INFO, text, e)); + } + + @Override + public void debug(String text, Throwable e) { + pendingMessages.add(new Message(MessageKind.DEBUG, text, e)); + } + + @Override + public void warning(String text, Throwable e) { + pendingMessages.add(new Message(MessageKind.WARNING, text, e)); + } + + @Override + public void error(String text, Throwable e) { + pendingMessages.add(new Message(MessageKind.ERROR, text, e)); + } + + static class Message { + MessageKind kind; + String text; + Throwable throwable; + + public Message(MessageKind kind, String text, Throwable throwable) { + this.kind = kind; + this.text = text; + this.throwable = throwable; + } + } + + enum MessageKind { + INFO, + DEBUG, + WARNING, + ERROR + } +} diff --git a/tools/cli/src/main/java/org/teavm/cli/TeaVMRunner.java b/tools/cli/src/main/java/org/teavm/cli/TeaVMRunner.java index fd800d936..56874d9dd 100644 --- a/tools/cli/src/main/java/org/teavm/cli/TeaVMRunner.java +++ b/tools/cli/src/main/java/org/teavm/cli/TeaVMRunner.java @@ -15,15 +15,28 @@ */ package org.teavm.cli; -import java.io.BufferedReader; import java.io.File; import java.io.IOException; -import java.io.InputStreamReader; -import java.io.UnsupportedEncodingException; import java.net.MalformedURLException; import java.net.URL; import java.net.URLClassLoader; - +import java.nio.file.FileSystems; +import java.nio.file.FileVisitResult; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.SimpleFileVisitor; +import java.nio.file.StandardWatchEventKinds; +import java.nio.file.WatchEvent; +import java.nio.file.WatchKey; +import java.nio.file.WatchService; +import java.nio.file.attribute.BasicFileAttributes; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; import org.apache.commons.cli.CommandLine; import org.apache.commons.cli.CommandLineParser; import org.apache.commons.cli.HelpFormatter; @@ -32,7 +45,6 @@ import org.apache.commons.cli.Options; import org.apache.commons.cli.ParseException; import org.apache.commons.cli.PosixParser; import org.teavm.backend.wasm.render.WasmBinaryVersion; -import org.teavm.tooling.ClassAlias; import org.teavm.tooling.RuntimeCopyOperation; import org.teavm.tooling.TeaVMTargetType; import org.teavm.tooling.TeaVMTool; @@ -43,17 +55,21 @@ import org.teavm.vm.TeaVMProgressFeedback; import org.teavm.vm.TeaVMProgressListener; public final class TeaVMRunner { - private static long startTime; - private static long phaseStartTime; - private static TeaVMPhase currentPhase; - private static String[] classPath; + private static Options options = new Options(); + private TeaVMTool tool = new TeaVMTool(); + private AccumulatingTeaVMToolLog log = new AccumulatingTeaVMToolLog(new ConsoleTeaVMToolLog()); + private CommandLine commandLine; + private long startTime; + private long phaseStartTime; + private String[] classPath; + private boolean interactive; - private TeaVMRunner() { + static { + setupOptions(); } @SuppressWarnings("static-access") - public static void main(String[] args) { - Options options = new Options(); + private static void setupOptions() { options.addOption(OptionBuilder .withArgName("target") .hasArg() @@ -114,21 +130,21 @@ public final class TeaVMRunner { .withDescription("Additional classpath that will be reloaded by TeaVM each time in wait mode") .withLongOpt("classpath") .create('p')); - options.addOption(OptionBuilder - .withLongOpt("classalias") - .withArgName("alias") - .hasArgs() - .withDescription("Alias names for classes. Specify as fully.qualified.Name:AliasName") - .create()); options.addOption(OptionBuilder .withLongOpt("wasm-version") .withArgName("version") .hasArg() .withDescription("WebAssembly binary version (11, 12, 13)") .create()); + } + private TeaVMRunner(CommandLine commandLine) { + this.commandLine = commandLine; + } + + public static void main(String[] args) { if (args.length == 0) { - printUsage(options); + printUsage(); return; } CommandLineParser parser = new PosixParser(); @@ -136,11 +152,38 @@ public final class TeaVMRunner { try { commandLine = parser.parse(options, args); } catch (ParseException e) { - printUsage(options); + printUsage(); return; } - TeaVMTool tool = new TeaVMTool(); + TeaVMRunner runner = new TeaVMRunner(commandLine); + runner.parseArguments(); + runner.setUp(); + runner.runAll(); + } + + private void parseArguments() { + parseClassPathOptions(); + parseTargetOption(); + parseOutputOptions(); + parseDebugOptions(); + parseOptimizationOption(); + parseIncrementalOptions(); + parseJavaScriptOptions(); + parseWasmOptions(); + + interactive = commandLine.hasOption('w'); + + String[] args = commandLine.getArgs(); + if (args.length > 1) { + System.err.println("Unexpected arguments"); + printUsage(); + } else if (args.length == 1) { + tool.setMainClass(args[0]); + } + } + + private void parseTargetOption() { if (commandLine.hasOption("t")) { switch (commandLine.getOptionValue('t').toLowerCase()) { case "javascript": @@ -153,12 +196,18 @@ public final class TeaVMRunner { break; } } + } + + private void parseOutputOptions() { if (commandLine.hasOption("d")) { tool.setTargetDirectory(new File(commandLine.getOptionValue("d"))); } if (commandLine.hasOption("f")) { tool.setTargetFileName(commandLine.getOptionValue("f")); } + } + + private void parseJavaScriptOptions() { if (commandLine.hasOption("m")) { tool.setMinifying(true); } else { @@ -177,21 +226,28 @@ public final class TeaVMRunner { break; default: System.err.println("Wrong parameter for -r option specified"); - printUsage(options); - return; + printUsage(); } } + } + + private void parseDebugOptions() { if (commandLine.hasOption('g')) { tool.setDebugInformationGenerated(true); } + if (commandLine.hasOption('S')) { + tool.setSourceMapsFileGenerated(true); + } + } + private void parseOptimizationOption() { if (commandLine.hasOption("O")) { int level; try { level = Integer.parseInt(commandLine.getOptionValue("O")); } catch (NumberFormatException e) { System.err.print("Wrong optimization level"); - printUsage(options); + printUsage(); return; } switch (level) { @@ -206,14 +262,12 @@ public final class TeaVMRunner { break; default: System.err.print("Wrong optimization level"); - printUsage(options); - return; + printUsage(); } } + } - if (commandLine.hasOption('S')) { - tool.setSourceMapsFileGenerated(true); - } + private void parseIncrementalOptions() { if (commandLine.hasOption('i')) { tool.setIncremental(true); } @@ -222,98 +276,15 @@ public final class TeaVMRunner { } else { tool.setCacheDirectory(new File(tool.getTargetDirectory(), "teavm-cache")); } + } + + private void parseClassPathOptions() { if (commandLine.hasOption('p')) { classPath = commandLine.getOptionValues('p'); } - - if (commandLine.hasOption("classalias")) { - String[] aliasStrings = commandLine.getOptionValues("classalias"); - - for (String aliasString : aliasStrings) { - int i = aliasString.indexOf(':'); - if (i == -1) { - System.err.print("Wrong alias specification"); - printUsage(options); - return; - } - - ClassAlias alias = new ClassAlias(); - alias.setClassName(aliasString.substring(0, i)); - alias.setAlias(aliasString.substring(i + 1)); - tool.getClassAliases().add(alias); - } - } - - boolean interactive = commandLine.hasOption('w'); - setupWasm(tool, commandLine, options); - - args = commandLine.getArgs(); - if (args.length > 1) { - System.err.println("Unexpected arguments"); - printUsage(options); - return; - } else if (args.length == 1) { - tool.setMainClass(args[0]); - } - tool.setLog(new ConsoleTeaVMToolLog()); - tool.getProperties().putAll(System.getProperties()); - tool.setProgressListener(progressListener); - - if (interactive) { - boolean quit = false; - BufferedReader reader; - try { - reader = new BufferedReader(new InputStreamReader(System.in, "UTF-8")); - } catch (UnsupportedEncodingException e) { - e.printStackTrace(); - System.exit(-2); - return; - } - do { - try { - build(tool); - } catch (Exception e) { - e.printStackTrace(System.err); - } - System.out.println("Press enter to repeat or enter 'q' to quit"); - try { - String line = reader.readLine().trim(); - if (!line.isEmpty()) { - if (line.equals("q")) { - quit = true; - } else { - System.out.println("Unrecognized command"); - } - } - } catch (IOException e) { - e.printStackTrace(); - System.exit(-2); - } - } while (!quit); - } else { - try { - build(tool); - } catch (Exception e) { - e.printStackTrace(System.err); - System.exit(-2); - } - if (!tool.getProblemProvider().getSevereProblems().isEmpty()) { - System.exit(-2); - } - } } - private static void build(TeaVMTool tool) throws TeaVMToolException { - resetClassLoader(tool); - currentPhase = null; - startTime = System.currentTimeMillis(); - phaseStartTime = System.currentTimeMillis(); - tool.generate(); - reportPhaseComplete(); - System.out.println("Build complete for " + ((System.currentTimeMillis() - startTime) / 1000.0) + " seconds"); - } - - private static void setupWasm(TeaVMTool tool, CommandLine commandLine, Options options) { + private void parseWasmOptions() { if (commandLine.hasOption("wasm-version")) { String value = commandLine.getOptionValue("wasm-version"); try { @@ -324,16 +295,229 @@ public final class TeaVMRunner { break; default: System.err.print("Wrong version value"); - printUsage(options); + printUsage(); } } catch (NumberFormatException e) { System.err.print("Wrong version value"); - printUsage(options); + printUsage(); } } } - private static void resetClassLoader(TeaVMTool tool) { + private void setUp() { + tool.setLog(log); + tool.getProperties().putAll(System.getProperties()); + } + + private void runAll() { + if (interactive) { + buildInteractive(); + } else { + buildNonInteractive(); + } + } + + private void buildInteractive() { + InteractiveWatcher watcher = new InteractiveWatcher(); + + while (true) { + ProgressListenerImpl progressListener = new ProgressListenerImpl(); + Thread thread = null; + try { + watcher.progressListener = progressListener; + thread = new Thread(watcher); + thread.start(); + if (progressListener.cancelRequested.get()) { + continue; + } + build(progressListener); + } catch (Exception e) { + e.printStackTrace(System.err); + } finally { + if (!progressListener.cancelRequested.get()) { + thread.interrupt(); + } + } + + try { + System.out.println("Waiting for changes..."); + watcher.waitForChange(); + System.out.println(); + System.out.println("Changes detected. Recompiling..."); + } catch (InterruptedException | IOException e) { + break; + } + } + } + + class InteractiveWatcher implements Runnable { + volatile ProgressListenerImpl progressListener = new ProgressListenerImpl(); + private WatchService watchService; + private Map keysToPath = new HashMap<>(); + private Map pathsToKey = new HashMap<>(); + + InteractiveWatcher() { + try { + watchService = FileSystems.getDefault().newWatchService(); + for (String entry : classPath) { + Path path = Paths.get(entry); + File file = path.toFile(); + if (file.exists()) { + if (!file.isDirectory()) { + registerSingle(path.getParent()); + } else { + register(path); + } + } + } + } catch (IOException e) { + System.err.println("Error setting up file watcher"); + e.printStackTrace(System.err); + System.exit(2); + } + } + + private void register(Path path) throws IOException { + Files.walkFileTree(path, new SimpleFileVisitor() { + @Override + public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) + throws IOException { + registerSingle(dir); + return FileVisitResult.CONTINUE; + } + }); + } + + private void registerSingle(Path path) throws IOException { + WatchKey key = path.register(watchService, StandardWatchEventKinds.ENTRY_CREATE, + StandardWatchEventKinds.ENTRY_DELETE, StandardWatchEventKinds.ENTRY_MODIFY); + keysToPath.put(key, path); + pathsToKey.put(path, key); + } + + @Override + public void run() { + Thread thread = new Thread(() -> { + try { + waitForChange(); + if (Thread.currentThread().isInterrupted()) { + progressListener.cancelRequested.set(true); + System.out.println("Classpath changed during compilation. Cancelling..."); + } + } catch (InterruptedException | IOException e) { + // do nothing + } + }); + thread.start(); + if (thread.isAlive()) { + thread.interrupt(); + } + } + + void waitForChange() throws InterruptedException, IOException { + take(); + while (poll(750)) { + // continue polling + } + while (pollNow()) { + // continue polling + } + } + + private void take() throws InterruptedException, IOException { + while (true) { + WatchKey key = watchService.take(); + if (key != null) { + if (!filter(key).isEmpty()) { + break; + } + } + } + } + + private boolean poll(int milliseconds) throws IOException, InterruptedException { + long end = System.currentTimeMillis() + milliseconds; + while (true) { + int timeToWait = (int) (end - System.currentTimeMillis()); + WatchKey key = watchService.poll(timeToWait, TimeUnit.MILLISECONDS); + if (key == null) { + return false; + } + if (!filter(key).isEmpty()) { + break; + } + } + return true; + } + + private boolean pollNow() throws IOException { + WatchKey key = watchService.poll(); + if (key == null) { + return false; + } + filter(key); + return true; + } + + private List filter(WatchKey key) throws IOException { + List result = new ArrayList<>(); + for (WatchEvent event : key.pollEvents()) { + Path path = filter(key, event); + if (path != null) { + result.add(path); + } + } + key.reset(); + return result; + } + + private Path filter(WatchKey baseKey, WatchEvent event) throws IOException { + if (!(event.context() instanceof Path)) { + return null; + } + Path basePath = keysToPath.get(baseKey); + Path path = basePath.resolve((Path) event.context()); + WatchKey key = pathsToKey.get(path); + + if (event.kind() == StandardWatchEventKinds.ENTRY_DELETE) { + if (key != null) { + pathsToKey.remove(path); + keysToPath.remove(key); + key.cancel(); + } + } else if (event.kind() == StandardWatchEventKinds.ENTRY_CREATE) { + if (Files.isDirectory(path)) { + register(path); + } + } + + return path; + } + } + + private void buildNonInteractive() { + try { + build(new ProgressListenerImpl()); + } catch (Exception e) { + e.printStackTrace(System.err); + System.exit(-2); + } + if (!tool.getProblemProvider().getSevereProblems().isEmpty()) { + System.exit(-2); + } + } + + private void build(ProgressListenerImpl progressListener) throws TeaVMToolException { + tool.setProgressListener(progressListener); + resetClassLoader(); + startTime = System.currentTimeMillis(); + phaseStartTime = System.currentTimeMillis(); + tool.generate(); + reportPhaseComplete(); + System.out.println("Build complete for " + ((System.currentTimeMillis() - startTime) / 1000.0) + " seconds"); + } + + private void resetClassLoader() { if (classPath == null || classPath.length == 0) { return; } @@ -351,13 +535,17 @@ public final class TeaVMRunner { tool.setClassLoader(new URLClassLoader(urls, TeaVMRunner.class.getClassLoader())); } - private static TeaVMProgressListener progressListener = new TeaVMProgressListener() { + class ProgressListenerImpl implements TeaVMProgressListener { + private TeaVMPhase currentPhase; + AtomicBoolean cancelRequested = new AtomicBoolean(); + @Override public TeaVMProgressFeedback progressReached(int progress) { - return TeaVMProgressFeedback.CONTINUE; + return cancelRequested.get() ? TeaVMProgressFeedback.CANCEL : TeaVMProgressFeedback.CONTINUE; } @Override public TeaVMProgressFeedback phaseStarted(TeaVMPhase phase, int count) { + log.flush(); if (currentPhase != phase) { if (currentPhase != null) { reportPhaseComplete(); @@ -382,15 +570,16 @@ public final class TeaVMRunner { } currentPhase = phase; } - return TeaVMProgressFeedback.CONTINUE; + return cancelRequested.get() ? TeaVMProgressFeedback.CANCEL : TeaVMProgressFeedback.CONTINUE; } - }; - - private static void reportPhaseComplete() { - System.out.println(" complete for " + ((System.currentTimeMillis() - phaseStartTime) / 1000.0) + " seconds"); } - private static void printUsage(Options options) { + private void reportPhaseComplete() { + System.out.println(" complete for " + ((System.currentTimeMillis() - phaseStartTime) / 1000.0) + " seconds"); + log.flush(); + } + + private static void printUsage() { HelpFormatter formatter = new HelpFormatter(); formatter.printHelp("java " + TeaVMRunner.class.getName() + " [OPTIONS] [qualified.main.Class]", options); System.exit(-1);