All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.robovm.compiler.AppCompiler Maven / Gradle / Ivy

The newest version!
/*
 * Copyright (C) 2012 RoboVM AB
 * Copyright (C) 2018 Daniel Thommes, NeverNull GmbH, 
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * as published by the Free Software Foundation; either version 2
 * of the License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see .
 */
package org.robovm.compiler;

import org.apache.commons.exec.ExecuteException;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringUtils;
import org.json.simple.JSONObject;
import org.json.simple.JSONValue;
import org.robovm.compiler.clazz.*;
import org.robovm.compiler.config.*;
import org.robovm.compiler.config.Config.TreeShakerMode;
import org.robovm.compiler.config.StripArchivesConfig.StripArchivesBuilder;
import org.robovm.compiler.log.ConsoleLogger;
import org.robovm.compiler.plugin.*;
import org.robovm.compiler.target.ConsoleTarget;
import org.robovm.compiler.target.LaunchParameters;
import org.robovm.compiler.target.ios.*;
import org.robovm.compiler.util.AntPathMatcher;
import org.simpleframework.xml.Serializer;

import java.io.*;
import java.net.URL;
import java.net.URLConnection;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;

import java.io.BufferedInputStream;
import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import java.util.TreeSet;
import java.util.UUID;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.Executor;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

/**
 *
 * @version $Id$
 */
public class AppCompiler {

    /**
     * Name of the file that contains the dependencies between classpath and Main binary
     */
    public static final String CLASSPATHS_FILENAME = "classpath_dependencies.txt";

    /**
     * Names of root classes. These classes will always be linked in. These are
     * here because they are either required by the RoboVM specific native VM
     * libraries or by the Android's libcore native code.
     */
    private static final String[] ROOT_CLASSES = {
        "java/io/FileDescriptor",
        "java/io/PrintWriter",
        "java/io/Serializable",
        "java/io/StringWriter",
        "java/lang/AbstractMethodError",
        "java/lang/annotation/Annotation",
        "java/lang/annotation/AnnotationFormatError",
        "java/lang/ArithmeticException",
        "java/lang/ArrayIndexOutOfBoundsException",
        "java/lang/ArrayStoreException",
        "java/lang/Boolean",
        "java/lang/Byte",
        "java/lang/Character",
        "java/lang/Class",
        "java/lang/ClassCastException",
        "java/lang/ClassLoader",
        "java/lang/ClassLoader$SystemClassLoader",
        "java/lang/ClassNotFoundException",
        "java/lang/Cloneable",
        "java/lang/Daemons",
        "java/lang/Double",
        "java/lang/Enum",
        "java/lang/Error",
        "java/lang/ExceptionInInitializerError",
        "java/lang/Float",
        "java/lang/IllegalAccessError",
        "java/lang/IllegalArgumentException",
        "java/lang/IllegalMonitorStateException",
        "java/lang/IllegalStateException",
        "java/lang/IncompatibleClassChangeError",
        "java/lang/IndexOutOfBoundsException",
        "java/lang/InstantiationError",
        "java/lang/InstantiationException",
        "java/lang/Integer",
        "java/lang/InternalError",
        "java/lang/InterruptedException",
        "java/lang/LinkageError",
        "java/lang/Long",
        "java/lang/NegativeArraySizeException",
        "java/lang/NoClassDefFoundError",
        "java/lang/NoSuchFieldError",
        "java/lang/NoSuchMethodError",
        "java/lang/NullPointerException",
        "java/lang/Object",
        "java/lang/OutOfMemoryError",
        "java/lang/RealToString",
        "java/lang/ref/FinalizerReference",
        "java/lang/ref/PhantomReference",
        "java/lang/ref/Reference",
        "java/lang/ref/ReferenceQueue",
        "java/lang/ref/SoftReference",
        "java/lang/ref/WeakReference",
        "java/lang/reflect/AccessibleObject",
        "java/lang/reflect/Constructor",
        "java/lang/reflect/Field",
        "java/lang/reflect/InvocationHandler",
        "java/lang/reflect/InvocationTargetException",
        "java/lang/reflect/Method",
        "java/lang/reflect/Proxy",
        "java/lang/reflect/UndeclaredThrowableException",
        "java/lang/Runtime",
        "java/lang/RuntimeException",
        "java/lang/Short",
        "java/lang/StackOverflowError",
        "java/lang/StackTraceElement",
        "java/lang/String",
        "java/lang/System",
        "java/lang/Thread",
        "java/lang/Thread$UncaughtExceptionHandler",
        "java/lang/ThreadGroup",
        "java/lang/Throwable",
        "java/lang/TypeNotPresentException",
        "java/lang/UnsatisfiedLinkError",
        "java/lang/UnsupportedOperationException",
        "java/lang/VerifyError",
        "java/lang/VMClassLoader",
        "java/math/BigDecimal",
        "java/net/Inet6Address",
        "java/net/InetAddress",
        "java/net/InetSocketAddress",
        "java/net/InetUnixAddress",
        "java/net/Socket",
        "java/net/SocketImpl",
        "java/nio/charset/CharsetICU",
        "java/nio/DirectByteBuffer",
        "java/text/Bidi$Run",
        "java/text/ParsePosition",
        "java/util/Calendar",
        "java/util/regex/PatternSyntaxException",
        "java/util/zip/Deflater",
        "java/util/zip/Inflater",
        "libcore/icu/LocaleData",
        "libcore/icu/NativeDecimalFormat$FieldPositionIterator",
        "libcore/io/ErrnoException",
        "libcore/io/GaiException",
        "libcore/io/StructAddrinfo",
        "libcore/io/StructFlock",
        "libcore/io/StructGroupReq",
        "libcore/io/StructLinger",
        "libcore/io/StructPasswd",
        "libcore/io/StructPollfd",
        "libcore/io/StructStat",
        "libcore/io/StructStatVfs",
        "libcore/io/StructTimeval",
        "libcore/io/StructUcred",
        "libcore/io/StructUtsname",
        "libcore/util/MutableInt",
        "libcore/util/MutableLong",
        "org/robovm/rt/bro/Struct"
    };

    private static final String TRUSTED_CERTIFICATE_STORE_CLASS =
            "com/android/org/conscrypt/TrustedCertificateStore";

    /**
     * An {@link Executor} which runs tasks immediately without creating a
     * separate thread.
     */
    static final Executor SAME_THREAD_EXECUTOR = new Executor() {
        public void execute(Runnable r) {
            r.run();
        }
    };

    private final Config config;
    private final ClassCompiler classCompiler;
    private final Linker linker;

    public AppCompiler(Config config) {
        this.config = config;
        this.classCompiler = new ClassCompiler(config);
        this.linker = new Linker(config);
    }

    public Config getConfig() {
        return config;
    }

    /**
     * Returns all {@link Clazz}es in all {@link Path}s matching the specified
     * ANT-style pattern.
     */
    private Collection getMatchingClasses(String pattern) {
        AntPathMatcher matcher = new AntPathMatcher(pattern, ".");
        Map matches = new HashMap();
        for (Path path : config.getClazzes().getPaths()) {
            for (Clazz clazz : path.listClasses()) {
                if (!matches.containsKey(clazz.getClassName())
                        && matcher.matches(clazz.getClassName())) {

                    matches.put(clazz.getClassName(), clazz);
                }
            }
        }
        return matches.values();
    }


    /**
     * returns list of methods that has to be forced linked in case of aggressive tree shaker
     */
    private Collection getMatchingForceLinkMethods(Clazz clazz) {
        List forceLinkMethods = config.getForceLinkMethods();
        ClazzInfo ci = clazz.getClazzInfo();
        if (config.getTreeShakerMode() == TreeShakerMode.aggressive && !ci.getMethods().isEmpty() && !forceLinkMethods.isEmpty()) {

            // prepare list of signatures for all matched entrues
            Set signatures = new HashSet<>();
            for (ForceLinkMethodsConfig entry : forceLinkMethods) {
                if (entry.matchesClass(ci))
                    signatures.addAll(entry.getMethods());
            }

            // class matches one or more patterns check for method signatures
            if (!signatures.isEmpty()) {
                Set matches = new HashSet<>();
                for (MethodInfo mi : ci.getMethods()) {
                    if (signatures.contains(mi.getName() + mi.getDesc()))
                        matches.add(mi);
                }

                if (!matches.isEmpty())
                    return matches;
            }
        }

        // no methods has to be forced linked
        return Collections.emptyList();
    }

    /**
     * Returns all root classes. These are the minimum set of classes that needs
     * to be compiled and linked. The compiler will use this set to determine
     * which classes need to be recompiled and linked in through the root
     * classes' dependencies.
     * 
     * The classes matching {@link #ROOT_CLASS_PATTERNS} and
     * {@link #ROOT_CLASSES} will always be included. If a main class has been
     * specified it will also become a root. Any root class pattern specified on
     * the command line (as returned by {@link Config#getRoots()} will also be
     * used to find root classes. If no main class has been specified and
     * {@link Config#getRoots()} returns an empty set all classes available on
     * the bootclasspath and the classpath will become roots.
     */
    private TreeSet getRootClasses() {
        TreeSet classes = new TreeSet();
        for (String rootClassName : ROOT_CLASSES) {
            Clazz clazz = config.getClazzes().load(rootClassName);
            if (clazz == null) {
                throw new CompilerException("Root class " + rootClassName + " not found");
            }
            classes.add(clazz);
        }

        if (config.getMainClass() != null) {
            Clazz clazz = config.getClazzes().load(config.getMainClass().replace('.', '/'));
            if (clazz == null) {
                throw new CompilerException("Main class " + config.getMainClass() + " not found");
            }
            classes.add(clazz);
        }

        if (config.getForceLinkClasses().isEmpty()) {
            if (config.getMainClass() == null) {
                classes.addAll(config.getClazzes().listClasses());
            }
        } else {
            for (String pattern : config.getForceLinkClasses()) {
                if (pattern == null || pattern.trim().isEmpty()) {
                    continue;
                }
                pattern = pattern.trim();
                if (pattern.indexOf('*') == -1) {
                    Clazz clazz = config.getClazzes().load(pattern.replace('.', '/'));
                    if (clazz == null) {
                        throw new CompilerException("Root class " + pattern + " not found");
                    }
                    classes.add(clazz);
                } else {
                    Collection matches = getMatchingClasses(pattern);
                    if (matches.isEmpty()) {
                        config.getLogger().warn("Root pattern %s matches no classes", pattern);
                    } else {
                        classes.addAll(matches);
                    }
                }
            }
        }
        return classes;
    }

    private boolean compile(Executor executor, ClassCompilerListener listener,
            Clazz clazz, Set compileQueue, Set compiled) throws IOException {

        boolean result = false;
        if (config.isClean() || classCompiler.mustCompile(clazz)) {
            classCompiler.compile(clazz, executor, listener);
            result = true;
        }
        return result;
    }

    static void addMetaInfImplementations(Clazzes clazzes, Clazz clazz, Set compiled, Set compileQueue)
            throws IOException {
        String metaInfName = "META-INF/services/" + clazz.getClassName();
        IOException throwLater = null;
        for (InputStream is : clazzes.loadResources(metaInfName)) {
            try (BufferedReader r = new BufferedReader(new InputStreamReader(is, "UTF8"))) {
                for (;;) {
                    String line = r.readLine();
                    if (line == null) {
                        break;
                    }
                    if (line.startsWith("#")) {
                        continue;
                    }
                    String implClazzName = line.replace('.', '/');
                    Clazz implClazz = clazzes.load(implClazzName);
                    if (implClazz != null && !compiled.contains(implClazz)) {
                        compileQueue.add(implClazz);
                    }
                }
            } catch (IOException ex) {
                throwLater = ex;
            }
        }
        if (throwLater != null) {
            throw throwLater;
        }
    }

    public Set compile(Set rootClasses, boolean compileDependencies,
            final ClassCompilerListener listener) throws IOException {

        config.getLogger().info("Compiling classes using %d threads", config.getThreads());

        final Executor executor = (config.getThreads() <= 1)
                ? SAME_THREAD_EXECUTOR
                : new ThreadPoolExecutor(config.getThreads() - 1, config.getThreads() - 1,
                        0L, TimeUnit.MILLISECONDS,
                        // Use a bounded queue to avoid memory problems if the
                        // worker threads are slower than the enqueuing thread.
                        // The optimal thread pool size and queue size have been
                        // determined by trial and error.
                        new ArrayBlockingQueue((config.getThreads() - 1) * 20));
        class HandleFailureListener implements ClassCompilerListener {
            volatile Throwable t;

            @Override
            public void success(Clazz clazz) {
                if (listener != null) {
                    listener.success(clazz);
                }
            }

            @Override
            public void failure(Clazz clazz, Throwable t) {
                // Compilation failed. Save the error and stop the executor.
                this.t = t;
                if (executor instanceof ExecutorService) {
                    ((ExecutorService) executor).shutdown();
                }
                if (listener != null) {
                    listener.failure(clazz, t);
                }
            }
        };
        HandleFailureListener listenerWrapper = new HandleFailureListener();

        DependencyGraph dependencyGraph = config.getDependencyGraph();

        TreeSet compileQueue = new TreeSet<>(rootClasses);
        long start = System.currentTimeMillis();
        Set linkClasses = new HashSet();
        int compiledCount = 0;
        outer: while (!compileQueue.isEmpty() && !Thread.currentThread().isInterrupted()) {
            while (!compileQueue.isEmpty() ) {
                if (Thread.currentThread().isInterrupted()) {
                    break outer;
                }
                Clazz clazz = compileQueue.pollFirst();
                if (!linkClasses.contains(clazz)) {
                    if (compile(executor, listenerWrapper, clazz, compileQueue, linkClasses)) {
                        compiledCount++;
                        if (listenerWrapper.t != null) {
                            // We have a failed compilation. Stop compiling.
                            break outer;
                        }
                    }

                    Collection forceLinkMethods = getMatchingForceLinkMethods(clazz);
                    dependencyGraph.add(clazz, rootClasses.contains(clazz), forceLinkMethods);
                    linkClasses.add(clazz);

                    // notify plugins
                    for (CompilerPlugin plugin : config.getCompilerPlugins()) {
                        plugin.afterClassDependenciesResolved(config, clazz);
                    }

                    if (compileDependencies) {
                        addMetaInfImplementations(config.getClazzes(), clazz, linkClasses, compileQueue);
                    }
                }
            }

            if (compileDependencies) {
                for (String className : dependencyGraph.findReachableClasses()) {
                    Clazz depClazz = config.getClazzes().load(className);
                    if (depClazz != null && !linkClasses.contains(depClazz)) {
                        compileQueue.add(depClazz);
                    }
                }
            }
        }

        // Shutdown the executor and wait for running tasks to complete.
        if (executor instanceof ExecutorService) {
            // save interrupted status (also Thread.interrupted() clears it)
            // as if it stays set executorService.awaitTermination will exit with
            // InterruptedException and clear interrupted state
            boolean wasInterrupted = Thread.interrupted();
            ExecutorService executorService = (ExecutorService) executor;
            executorService.shutdown();
            try {
                executorService.awaitTermination(Long.MAX_VALUE, TimeUnit.DAYS);
            } catch (InterruptedException ignored) {
            }
            if (wasInterrupted)
                Thread.currentThread().interrupt();
        }

        if (listenerWrapper.t != null) {
            // The compilation failed. Rethrow the exception in the callback.
            if (listenerWrapper.t instanceof IOException) {
                throw (IOException) listenerWrapper.t;
            }
            if (listenerWrapper.t instanceof RuntimeException) {
                throw (RuntimeException) listenerWrapper.t;
            }
            if (listenerWrapper.t instanceof Error) {
                throw (Error) listenerWrapper.t;
            }
            throw new CompilerException(listenerWrapper.t);
        }

        if (!Thread.currentThread().isInterrupted()) {
            long duration = System.currentTimeMillis() - start;
            config.getLogger().info("Compiled %d classes in %.2f seconds", compiledCount, duration / 1000.0);
        }

        return linkClasses;
    }

    private void compile() throws IOException {
        // FIXME: dkimitsa -- update check is disabled as facility is not available anymore and due GDRP related
        //        moments
        // updateCheck();

        //Let's look, if we really need to recompile
        if (needsRecompilation(config)) {

            Set linkClasses = compile(getRootClasses(), true, null);

            if (Thread.currentThread().isInterrupted()) {
                return;
            }

            if (linkClasses.contains(config.getClazzes().load(TRUSTED_CERTIFICATE_STORE_CLASS))) {
                if (config.getCacerts() != null) {
                    config.addResourcesPath(config.getClazzes().createResourcesBootclasspathPath(
                            config.getHome().getCacertsPath(config.getCacerts())));
                }
            }

            long start = System.currentTimeMillis();
            linker.link(linkClasses);
            long duration = System.currentTimeMillis() - start;
            config.getLogger().info("Linked %d classes in %.2f seconds", linkClasses.size(), duration / 1000.0);

            storeRecompileInfo(config, linkClasses);

        } else {
            config.getLogger().info("No classes were modified. Skipping compilation and linking.");
        }
    }

    /**
     * Write the classpaths file that contains a list of class and jar files that were input for the Main binary
     *
     * @param classPathsFile
     * @param linkClasses
     * @throws IOException
     */
    private void storeRecompileInfo(Config config, Set linkClasses) throws IOException {
        File classPathsFile = new File(config.getTmpDir(), CLASSPATHS_FILENAME);
        Set paths = new HashSet<>();
        for (Clazz clazz : linkClasses) {
            Path path = clazz.getPath();
            if (path instanceof ZipFilePath) {
                // comes from a jar - let's consider the jar only.
                paths.add(path.toString());
            } else {
                //is a single class file either in the output dir or in robovm's cache, need to access the real file
                paths.add(((DirectoryPath.DirectoryPathClazz) clazz).getClassFile().getAbsolutePath());
            }
        }

        try (OutputStreamWriter writer = new OutputStreamWriter(new FileOutputStream(classPathsFile), StandardCharsets.UTF_8)) {
            for (String path : paths) {
                writer.write(path);
                writer.write("\n");
            }
        }

        // Persisting the config for later comparison
        File configFile = new File(config.getTmpDir(), "config.xml");
        try (FileOutputStream output = new FileOutputStream(configFile)) {
            try {
                IOUtils.write(configToXml(config), output, StandardCharsets.UTF_8.toString());
            } catch (Exception e) {
                config.getLogger().error("Error when computing config's equality. " +
                        "Forcing recompilation: %s:%s", e.getClass().getSimpleName(), e.getMessage());
            }
        }

    }

    /**
     * Checks, whether recompilation of the Main binary is necessary by looking at the classPathsFile
     *
     * @param classPathsFile
     * @return
     * @throws IOException
     */
    private boolean needsRecompilation(Config config) throws IOException {
        if (!config.isSmartSkipRebuild()) {
            return true;
        }

        //User can force clean via command line
        if (config.isClean()) {
            config.getLogger().info("Config requires clean, rebuilding.");
            return true;
        }

        //Check for newer input files compared to Main binary
        File classPathsFile = new File(config.getTmpDir(), CLASSPATHS_FILENAME);
        File binaryFile = new File(config.getTmpDir(), config.getExecutableName());

        if (!classPathsFile.exists()) {
            config.getLogger().info("Dependency info was not found, compilation is required.");
            return true;
        }
        if(!binaryFile.exists()){
            config.getLogger().info("Main binary was not found, compilation is required.");
            return true;
        }

        //Compare the modification dates of all classes / jars that were linked during the last run with the binary
        long binaryFileModified = binaryFile.lastModified();
        try (BufferedReader b = new BufferedReader(new FileReader(classPathsFile))) {
            String readLine = "";
            while ((readLine = b.readLine()) != null) {
                File classPathFile = new File(readLine);
                // class was removed or is newer?
                if (!classPathFile.exists() || classPathFile.lastModified() > binaryFileModified) {
                    config.getLogger().info("Found modified file, compilation is required: %s", classPathFile);
                    return true;
                }
            }
        }

        //Has the configuration changed between runs (e.g. forcelink)?
        boolean configsEqual;
        try {
            //If there has been a previous run, we have a corresponding file
            File configFile = new File(config.getTmpDir(), "config.xml");
            if (configFile.exists()) {
                String previousConfig = FileUtils.readFileToString(configFile, StandardCharsets.UTF_8.toString());
                //Writing the configuration as XML with SimpleXML
                configsEqual = configToXml(config).equals(previousConfig);
            } else {
                config.getLogger().info("Could not find a config.xml file, compilation is required.");
                configsEqual = false;
            }
        } catch (Exception e) {
            config.getLogger().error("Error when computing config's equality. " +
                    "Forcing recompilation: %s:%s", e.getClass().getSimpleName(), e.getMessage());
            configsEqual = false;
        }

        if (!configsEqual) {
            config.getLogger().info("Configurations differ, compilation is required.");
        }
        //recompile, if configs are not equal
        return !configsEqual;
    }

    private String configToXml(Config config) throws Exception {
        Serializer serializer = Config.Builder.createSerializer(config, config.getTmpDir());
        StringWriter writer = new StringWriter();
        serializer.write(this.config, writer);
        String xml = writer.toString();
        //In debug mode, there is a random port number used - strip this for comparability
        xml = xml.replaceAll("debug:jdwpport=\\d*?", "debug:jdwpport=REMOVED");
        //Remove smartSkipRebuild info, as switching smartSkipRebuild on will result in differing configs otherwise
        xml = xml.replaceAll(".*?", "");
        return xml;
    }

    public static void main(String[] args) throws IOException {

        AppCompiler compiler = null;
        Config.Builder builder = null;

        boolean verbose = false;
        boolean run = false;
        boolean archive = false;
        List archs = new ArrayList<>();
        StripArchivesBuilder stripArchivesBuilder = new StripArchivesBuilder();
        String dumpConfigFile = null;
        List runArgs = new ArrayList();
        try {
            builder = new Config.Builder();
            Map pluginArguments = builder.fetchPluginArguments();

            int i = 0;
            while (i < args.length) {
                if ("-cp".equals(args[i]) || "-classpath".equals(args[i])) {
                    for (String p : args[++i].split(File.pathSeparator)) {
                        builder.addClasspathEntry(new File(p));
                    }
                } else if ("-bcp".equals(args[i]) || "-bootcp".equals(args[i]) || "-bootclasspath".equals(args[i])) {
                    for (String p : args[++i].split(File.pathSeparator)) {
                        builder.addBootClasspathEntry(new File(p));
                    }
                } else if ("-jar".equals(args[i])) {
                    builder.mainJar(new File(args[++i]));
                } else if ("-o".equals(args[i])) {
                    builder.executableName(args[++i]);
                } else if ("-d".equals(args[i])) {
                    builder.installDir(new File(args[++i]));
                } else if ("-cache".equals(args[i])) {
                    builder.cacheDir(new File(args[++i]));
                } else if ("-home".equals(args[i])) {
                    builder.home(new Config.Home(new File(args[++i])));
                } else if ("-tmp".equals(args[i])) {
                    builder.tmpDir(new File(args[++i]));
                } else if ("-threads".equals(args[i])) {
                    String s = args[++i];
                    try {
                        int n = Integer.parseInt(s);
                        // Make sure n > 0 and cap at 128 threads.
                        n = Math.max(n, 1);
                        n = Math.min(n, 128);
                        builder.threads(n);
                    } catch (NumberFormatException e) {
                        throw new IllegalArgumentException("Unparsable thread count: " + s);
                    }
                } else if ("-run".equals(args[i])) {
                    run = true;
                } else if ("-verbose".equals(args[i])) {
                    verbose = true;
                } else if ("-config".equals(args[i])) {
                    builder.read(new File(args[++i]));
                } else if ("-dumpconfig".equals(args[i])) {
                    dumpConfigFile = args[++i];
                } else if ("-properties".equals(args[i])) {
                    builder.addProperties(new File(args[++i]));
                } else if (args[i].startsWith("-P")) {
                    int index = args[i].indexOf('=');
                    if (index <= 0) {
                        throw new IllegalArgumentException("Malformed property: " + args[i]);
                    }
                    String name = args[i].substring(2, index);
                    String value = args[i].substring(index + 1);
                    builder.addProperty(name, value);
                } else if ("-debug".equals(args[i])) {
                    builder.debug(true);
                } else if ("-use-debug-libs".equals(args[i])) {
                    builder.useDebugLibs(true);
                } else if ("-dump-intermediates".equals(args[i])) {
                    builder.dumpIntermediates(true);
                } else if ("-dynamic-jni".equals(args[i])) {
                    // TODO: Old option not used any longer. We still accept it
                    // for now. Delete it in a future release.
                } else if ("-skiprt".equals(args[i])) {
                    builder.skipRuntimeLib(true);
                } else if ("-skipsign".equals(args[i])) {
                    builder.iosSkipSigning(true);
                } else if ("-clean".equals(args[i])) {
                    builder.clean(true);
                } else if ("-help".equals(args[i]) || "-?".equals(args[i])) {
                    printUsageAndExit(null, builder.getPlugins());
                } else if ("-version".equals(args[i])) {
                    printVersionAndExit();
                } else if ("-cc".equals(args[i])) {
                    builder.ccBinPath(new File(args[++i]));
                } else if ("-os".equals(args[i])) {
                    String s = args[++i];
                    builder.os("auto".equals(s) ? null : OS.valueOf(s));
                } else if ("-arch".equals(args[i])) {
                    String s = args[++i];
                    if (!"auto".equals(s)) {
                        archs.add(Arch.parse(s));
                    }
                } else if ("-archs".equals(args[i])) {
                    for (String s : args[++i].split(":")) {
                        if (!"auto".equals(s)) {
                            archs.add(Arch.parse(s));
                        }
                    }
                } else if ("-target".equals(args[i])) {
                    String s = args[++i];
                    builder.targetType("auto".equals(s) ? null : s);
                } else if ("-treeshaker".equals(args[i])) {
                    String s = args[++i];
                    builder.treeShakerMode(TreeShakerMode.valueOf(s));
                } else if ("-forcelinkclasses".equals(args[i])) {
                    for (String p : args[++i].split(":")) {
                        p = p.replace('#', '*');
                        builder.addForceLinkClass(p);
                    }
                } else if ("-libs".equals(args[i])) {
                    for (String p : args[++i].split(":")) {
                        builder.addLib(new Config.Lib(p, true));
                    }
                } else if ("-exportedsymbols".equals(args[i])) {
                    for (String p : args[++i].split(":")) {
                        builder.addExportedSymbol(p);
                    }
                } else if ("-unhidesymbols".equals(args[i])) {
                    for (String p : args[++i].split(":")) {
                        builder.addUnhideSymbol(p);
                    }
                } else if ("-frameworks".equals(args[i])) {
                    for (String p : args[++i].split(":")) {
                        builder.addFramework(p);
                    }
                } else if ("-weakframeworks".equals(args[i])) {
                    for (String p : args[++i].split(":")) {
                        builder.addWeakFramework(p);
                    }
                } else if ("-resources".equals(args[i])) {
                    for (String p : args[++i].split(":")) {
                        if (AntPathMatcher.isPattern(p)) {
                            File dir = new File(AntPathMatcher.rtrimWildcardTokens(p));
                            String pattern = AntPathMatcher.extractPattern(p);
                            builder.addResource(new Resource(dir, null).include(pattern));
                        } else {
                            builder.addResource(new Resource(new File(p)));
                        }
                    }
                } else if ("-strip-archives-include".equals(args[i])) {
                   stripArchivesBuilder.addInclude(args[++i].split(":"));
                } else if ("-strip-archives-exclude".equals(args[i])) {
               	 stripArchivesBuilder.addExclude(args[++i].split(":"));
                } else if ("-cacerts".equals(args[i])) {
                    String name = args[++i];
                    Config.Cacerts cacerts = null;
                    if (!"none".equals(name)) {
                        try {
                            cacerts = Config.Cacerts.valueOf(name);
                        } catch (IllegalArgumentException e) {
                            throw new IllegalArgumentException("Illegal -cacerts value: " + name);
                        }
                    }
                    builder.cacerts(cacerts);
                } else if ("-plist".equals(args[i])) {
                    builder.infoPList(new File(args[++i]));
                } else if ("-entitlements".equals(args[i])) {
                    builder.iosEntitlementsPList(new File(args[++i]));
                } else if ("-signidentity".equals(args[i])) {
                    builder.iosSignIdentity(SigningIdentity.find(SigningIdentity.list(), args[++i]));
                } else if ("-provisioningprofile".equals(args[i])) {
                    builder.iosProvisioningProfile(ProvisioningProfile.find(ProvisioningProfile.list(), args[++i]));
                } else if ("-sdk".equals(args[i])) {
                    builder.iosSdkVersion(args[++i]);
                } else if ("-printdevicetypes".equals(args[i])) {
                    printDeviceTypesAndExit();
                } else if ("-devicetype".equals(args[i])) {
                    builder.iosDeviceType(args[++i]);
                } else if ("-archive".equals(args[i])) {
                    archive = true;
                } else if ("-createipa".equals(args[i])) {
                    archive = true;
                } else if ("-ipaarchs".equals(args[i])) {
                    for (String s : args[++i].split(":")) {
                        if (!"auto".equals(s)) {
                            archs.add(Arch.parse(s));
                        }
                    }
                } else if (args[i].startsWith("-D")) {
                } else if (args[i].startsWith("-X")) {
                } else if (args[i].startsWith("-rvm:")) {
                    runArgs.add(args[i]);
                } else if (args[i].startsWith("-")) {
                    String argName = args[i].substring(1, args[i].length());
                    if (argName.contains("=")) {
                        argName = argName.substring(0, argName.indexOf('='));
                    }
                    PluginArgument arg = pluginArguments.get(argName);
                    if (arg != null) {
                        builder.addPluginArgument(args[i].substring(1));
                    } else {
                        throw new IllegalArgumentException("Unrecognized option: " + args[i]);
                    }
                } else {
                    builder.mainClass(args[i++]);
                    break;
                }
                i++;
            }

            builder.archs(archs.toArray(new Arch[archs.size()]));
            builder.stripArchivesBuilder(stripArchivesBuilder);

            while (i < args.length) {
                runArgs.add(args[i++]);
            }

            if (archive && run) {
                throw new IllegalArgumentException("Specify either -run or -createipa/-archive, not both");
            }

            builder.logger(new ConsoleLogger(verbose));
            builder.skipInstall(run);

            if (dumpConfigFile != null) {
                if (dumpConfigFile.equals("-")) {
                    builder.write(new OutputStreamWriter(System.out), new File("."));
                } else {
                    File file = new File(dumpConfigFile);
                    if (file.exists()) {
                        throw new IllegalArgumentException("Cannot dump config to " + file.getAbsolutePath()
                                + ". The file already exists.");
                    }
                    builder.write(file);
                }
                return;
            }

            compiler = new AppCompiler(builder.build());

        } catch (Throwable t) {
            String message = t.getMessage();
            if (t instanceof ArrayIndexOutOfBoundsException) {
                message = "Missing argument";
            }
            if (t instanceof IndexOutOfBoundsException) {
                message = "Missing argument";
            }
            if (verbose && !(t instanceof StringIndexOutOfBoundsException) && !(t instanceof IllegalArgumentException)) {
                t.printStackTrace();
            }
            printUsageAndExit(message, builder != null ? builder.getPlugins() : null);
        }

        try {
            if (archive) {
                compiler.build();
                compiler.archive();
            } else {
                if (run && !compiler.config.getTarget().canLaunch()) {
                    throw new IllegalArgumentException("Cannot launch when building " 
                            + compiler.config.getTarget().getType() + " binaries");
                }
                if (run) {
                    compiler.compile(); // Just compile the first slice if multiple archs have been specified
                    LaunchParameters launchParameters = compiler.config.getTarget().createLaunchParameters();
                    if (launchParameters instanceof IOSSimulatorLaunchParameters) {
                        IOSSimulatorLaunchParameters simParams = (IOSSimulatorLaunchParameters) launchParameters;
                        String deviceName = null;
                        String sdkVersion = null;
                        if (compiler.config.getIosDeviceType() != null) {
                            String[] parts = compiler.config.getIosDeviceType().split("[:;, ]+");
                            deviceName = parts[0].trim();
                            sdkVersion = parts.length > 1 ? parts[1].trim() : null;
                        }
                        DeviceType type = DeviceType.getBestDeviceType(
                                compiler.config.getArch(), null, deviceName, sdkVersion);
                        simParams.setDeviceType(type);
                    }
                    launchParameters.setArguments(runArgs);
                    compiler.launch(launchParameters);
                } else {
                    compiler.build();
                    compiler.config.getTarget().install();
                }
            }
        } catch (Throwable t) {
            String message = t.getMessage();
            if (verbose && !(t instanceof ExecuteException)) {
                t.printStackTrace();
            }
            printUsageAndExit(message, builder.getPlugins());
        }
    }

    /**
     * Builds the binary (possibly a fat binary with multiple archs).
     */
    public void build() throws IOException {
        List archs = this.config.getArchs();
        if (archs.isEmpty()) {
            archs = config.getTarget().getDefaultArchs();
        }
        if (archs.isEmpty()) {
            throw new IllegalArgumentException("No archs specified in config");
        }
        if (archs.size() == 1 && this.config.getArch().equals(archs.get(0))) {
            // No need to clone configs for each slice.
            compile();
        } else {
            Map slices = new TreeMap<>();
            for (Arch arch : archs) {
                this.config.getLogger().info("Building %s slice", arch);
                Config sliceConfig = this.config.builder()
                        .arch(arch)
                        .tmpDir(new File(this.config.getTmpDir(), arch.toString()))
                        .build();
                new AppCompiler(sliceConfig).compile();
                slices.put(arch, new File(sliceConfig.getTmpDir(), sliceConfig.getExecutableName()));
                for (Path path : sliceConfig.getResourcesPaths()) {
                    if (!this.config.getResourcesPaths().contains(path)) {
                        this.config.addResourcesPath(path);
                    }
                }
            }
            this.config.getTarget().buildFat(slices);
        }
    }

    /**
     * Archives the binary previously built using {@link #build()} along with
     * all resources specified in the {@link Config} and supporting files and
     * stores the archive in the {@link Config#getInstallDir()}.
     */
    public void archive() throws IOException {
        config.getTarget().archive();
    }

    /**
     * Installs the binary previously built using {@link #build()} along with
     * all resources specified in the {@link Config} and supporting files into
     * the {@link Config#getInstallDir()}.
     */
    public void install() throws IOException {
        config.getTarget().install();
    }

    public int launch(LaunchParameters launchParameters) throws Throwable {
        return launch(launchParameters, null);
    }

    public int launch(LaunchParameters launchParameters, InputStream inputStream) throws Throwable {
        try {
            return launchAsync(launchParameters, inputStream).waitFor();
        } finally {
            launchAsyncCleanup();
        }
    }

    public Process launchAsync(LaunchParameters launchParameters) throws Throwable {
        return launchAsync(launchParameters, null);
    }

    public Process launchAsync(LaunchParameters launchParameters, InputStream inputStream) throws Throwable {
        for (LaunchPlugin plugin : config.getLaunchPlugins()) {
            plugin.beforeLaunch(config, launchParameters);
        }
        try {
            Process process = config.getTarget().launch(launchParameters);
            for (LaunchPlugin plugin : config.getLaunchPlugins()) {
                plugin.afterLaunch(config, launchParameters, process);
            }
            return process;
        } catch (Throwable e) {
            for (LaunchPlugin plugin : config.getLaunchPlugins()) {
                plugin.launchFailed(config, launchParameters);
            }
            throw e;
        }
    }

    public void launchAsyncCleanup() {
        for (LaunchPlugin plugin : config.getLaunchPlugins()) {
            plugin.cleanup();
        }
    }

    private static void printDeviceTypesAndExit() throws IOException {
        List types = DeviceType.listDeviceTypes();
        for (DeviceType type : types) {
            System.out.println(type.getSimpleDeviceTypeId());
        }
        System.exit(0);
    }

    private static void printVersionAndExit() {
        System.err.println(Version.getCompilerVersion());
        System.exit(0);
    }

    private static void printUsageAndExit(String errorMessage, List plugins) {
        if (errorMessage != null) {
            System.err.format("robovm: %s\n", errorMessage);
        }
        List targets = new ArrayList<>();
        targets.add(ConsoleTarget.TYPE);
        targets.add(IOSTarget.TYPE);
        for (Plugin plugin : plugins) {
            if (plugin instanceof TargetPlugin) {
                targets.add(((TargetPlugin) plugin).getTarget().getType());
            }
        }
        // @formatter:off 
        System.err.println("Usage: robovm [-options] class [run-args]");
        System.err.println("   or  robovm [-options] -jar jarfile [run-args]");
        System.err.println("Options:");
        
        System.err.println("  -bootclasspath  ");
        System.err.println("  -bootcp         ");
        System.err.println("  -bcp            : separated list of directories, JAR archives, and ZIP \n" 
                         + "                        archives to search for class files. Used to locate the \n" 
                         + "                        java.* and javax.* classes. Default is \n"
                         + "                        /lib/robovm-rt.jar.");
        System.err.println("  -cp             ");
        System.err.println("  -classpath      : separated list of directories, JAR archives, and ZIP \n" 
                         + "                        archives to search for class files.");
        System.err.println("  -cache           Directory where cached compiled class files will be placed.\n" 
                         + "                        Default is ~/.robovm/cache");
        System.err.println("  -clean                Compile class files even if a compiled version already \n" 
                         + "                        exists in the cache.");
        System.err.println("  -d               Install the generated executable and other files in .\n" 
                         + "                        Default is /. Ignored if -run is specified.");
        System.err.println("  -dump-intermediates   Dump intermediate files like assembler files and LLVM bitcode\n" 
                         + "                        files to disk under ~/.robovm/cache/ or where the cache is.");
        System.err.println("  -cc             Path to the c compiler binary. gcc and clang are supported.");
        System.err.println("  -home            Directory where RoboVM runtime has been installed.\n"
                         + "                        Default is $ROBOVM_HOME. If not set the following paths\n" 
                         + "                        will be searched: ~/Applications/robovm/, ~/.robovm/home/,\n" 
                         + "                        /usr/local/lib/robovm/, /opt/robovm/, /usr/lib/robovm/.");
        System.err.println("  -tmp             Directory where temporary files will be placed during\n"
                         + "                        compilation. By default a new dir will be created under\n" 
                         + "                        ${java.io.tmpdir}.");
        System.err.println("  -jar            Use main class as specified by the manifest in this JAR \n" 
                         + "                        archive.");
        System.err.println("  -o              The name of the target binary");
        System.err.println("  -os             The name of the OS to build for. Allowed values are \n" 
                         + "                        'auto', 'linux', 'macosx' and 'ios'. Default is 'auto' which\n" 
                         + "                        means use the LLVM deafult.");
        System.err.println("  -arch           The name of the LLVM arch to compile for. Allowed values\n" 
                         + "                        are 'auto', 'x86', 'x86_64', 'thumbv7', 'arm64'. Default is\n" 
                         + "                        'auto' which means use the LLVM default.");
        System.err.println("  -archs          : separated list of archs. Used to build a fat binary which\n" 
                         + "                        includes all the specified archs. Allowed values\n" 
                         + "                        are 'x86', 'x86_64', 'thumbv7', 'arm64'.");
        System.err.println("  -env            The name platform environment. Allowed values\n"
                         + "                        are 'Native', 'Simulator'. Default is 'Native'");
        System.err.println("  -target         The target to build for. One of:\n"
                         + "                          'auto', '" + StringUtils.join(targets, "', '") + "'\n" 
                         + "                        The default is 'auto' which means use -os to decide.");
        System.err.println("  -forcelinkclasses \n" 
                         + "                        : separated list of class patterns matching\n" 
                         + "                        classes that must be linked in even if not referenced\n" 
                         + "                        (directly or indirectly) from the main class. If no main\n" 
                         + "                        class is specified all classes will be linked in unless this\n" 
                         + "                        option has been given. A pattern is an ANT style path pattern,\n" 
                         + "                        e.g. com.foo.**.bar.*.Main. An alternative syntax using # is\n" 
                         + "                        also supported, e.g. com.##.#.Main.");
        System.err.println("  -treeshaker     The tree shaking algorithm to use. 'none', 'conservative' or\n" 
                         + "                        'aggressive'. 'aggressive' will remove all unreachable method\n" 
                         + "                        implementations when it's safe to do so. 'conservative' only\n" 
                         + "                        removes unreachable methods marked as @WeaklyLinked. Methods\n" 
                         + "                        in the main class and in force linked classes will never be\n" 
                         + "                        stripped. Default is 'none'.");
        System.err.println("  -threads           The number of threads to use during class compilation. By\n" 
                         + "                        default the number returned by Runtime.availableProcessors()\n" 
                         + "                        will be used (" + Runtime.getRuntime().availableProcessors() + " on this host).");
        System.err.println("  -run                  Run the executable directly without installing it (-d is\n" 
                         + "                        ignored). The executable will be executed from the\n" 
                         + "                        temporary dir specified with -tmp.");
        System.err.println("  -archive              Archives the binary along with resources and supporting\n" 
                         + "                        files in a format suitable for distribution (e.g. an IPA\n" 
                         + "                        file for iOS apps). The archive will be created in the\n" 
                         + "                        install dir specified using -d.");
        System.err.println("  -debug                Generates debug information");
        System.err.println("  -use-debug-libs       Links against debug versions of the RoboVM VM libraries");
        System.err.println("  -libs           : separated list of static library files (.a), object\n"
                         + "                        files (.o) and system libraries that should be included\n" 
                         + "                        when linking the final executable.");
        System.err.println("  -exportedsymbols \n" 
                         + "                        : separated list of symbols that should be exported\n"
                         + "                        when linking the executable. This can be used when\n" 
                         + "                        linking in function which will be called using bro.\n" 
                         + "                        Wildcards can be used. * matches zero or more characters,\n" 
                         + "                        ? matches one character. [abc], [a-z] matches one character\n" 
                         + "                        from the specified set of characters.");
        System.err.println("  -unhidesymbols \n" 
                         + "                        : separated list of global hidden symbols in linked in static\n" 
                         + "                        libraries or frameworks that should be unhidden to be\n" 
                         + "                        accessible to bro @Bridge methods. Wildcards are not\n" 
                         + "                        supported. Unhidden symbols will always be exported.");
        System.err.println("  -frameworks     : separated list of frameworks that should be included\n" 
                         + "                        when linking the final executable.");
        System.err.println("  -weakframeworks \n" 
                         + "                        : separated list of frameworks that should be weakly linked\n" 
                         + "                        into the final executable.");
        System.err.println("  -frameworkpaths \n" 
                         + "                        : separated list of framework search paths used when searching\n" 
                         + "                        for custom frameworks.");
        System.err.println("  -resources      : separated list of files and directories that should be\n"
                         + "                        copied to the install dir. Accepts Ant-style patterns.\n" 
                         + "                        If a pattern is specified the longest non-pattern path before\n" 
                         + "                        the first wildcard will be used as base directory and will\n" 
                         + "                        not be recreated in the install dir.");
        System.err.println("  -strip-archives-include \n" 
                         + "                        : separated list of file patterns. Matching files will always\n"
                         + "                        be included when stripping archives. The default is to include\n"
                         + "                        all non .class files when stripping archives.\n"
                         + "                        -strip-archives-include and -strip-archives-exclude patterns\n"
                         + "                        will be evaluated in order and the first matching pattern will\n"
                         + "                        specify whether a specific file should be included or not.");
        
        System.err.println("  -strip-archives-exclude \n"
                         + "                        : separated list of file patterns. Matching files will always\n"
                         + "                        be excluded when stripping archives. The default is to include\n"
                         + "                        all non .class files when stripping archives.\n"
                         + "                        -strip-archives-include and -strip-archives-exclude patterns\n"
                         + "                        will be evaluated in order and the first matching pattern will\n"
                         + "                        specify whether a specific file should be included or not.");
        System.err.println("  -cacerts       Use the specified cacerts file. Allowed value are 'none',\n" 
                         + "                        'full'. Default is 'full' but no cacerts will be included\n" 
                         + "                        unless the code actually needs them.");
        System.err.println("  -skiprt               Do not add default robovm-rt.jar to bootclasspath");
        System.err.println("  -config         Reads the specified configuration XML file. Values set in\n" 
                         + "                        the file will override values set earlier in the command\n" 
                         + "                        line. Later options will override values set in the XML file.\n" 
                         + "                        Can be specified multiple times to read multiple config files.");
        System.err.println("  -dumpconfig     Dumps a configuration XML file to the specified file. Specify\n" 
                         + "                        '-' to dump the config to stdout.");
        System.err.println("  -properties     Reads a Java properties file which will be used when resolving\n" 
                         + "                        variables (enclosed in ${...}) in config XML files and\n" 
                         + "                        Info.plist files. Can be specified multiple times.");
        System.err.println("  -Pname=value          Sets a property value. See the -properties option.");
        System.err.println("  -verbose              Output messages about what the compiler is doing");
        System.err.println("  -version              Print the version of the compiler and exit");
        System.err.println("  -help, -?             Display this information");
        System.err.println("Target specific options:");
        System.err.println("  -createipa            (iOS) Create a .IPA file from the app bundle and place it in\n"
                         + "                        the install dir specified with -d. Alias for -archive.");
        System.err.println("  -ipaarchs             (iOS) : separated list of architectures to include in the IPA.\n" 
                         + "                        Either thumbv7 or arm64 or both. Alias for -archs.");
        System.err.println("  -plist          (iOS/OSX) Info.plist file to be used by the app. If not specified\n"
                         + "                        a simple Info.plist will be generated with a CFBundleIdentifier\n" 
                         + "                        based on the main class name or executable file name.");
        System.err.println("  -entitlements   (iOS) Property list (.plist) file containing entitlements\n" 
                         + "                        passed to codesign when signing the app.");
        System.err.println("  -resourcerules  (iOS) Property list (.plist) file containing resource rules\n" 
                         + "                        passed to codesign when signing the app.");
        System.err.println("  -signidentity     (iOS) Sign using this identity. Default is to look for an\n" 
                         + "                        identity starting with 'iPhone Developer' or 'iOS Development'.\n" 
                         + "                        Enclose in '/' to search by regexp, e.g. '/foo|bar/'");
        System.err.println("  -skipsign             (iOS) Skips signing of the compiled Application. Can be used\n"
                         + "                        to create unsigned packages for testing on a jailbroken device.");
        System.err.println("  -provisioningprofile \n" 
                         + "                        (iOS) Provisioning profile to use when building for a device.\n" 
                         + "                        Either a UUID, an app name or app id prefix. If not specified\n" 
                         + "                        a provisioning profile matching the signing identity and bundle\n" 
                         + "                        id from the Info.plist file will be used.");
        System.err.println("  -sdk         (iOS) Version number of the iOS SDK to build against. If not\n" 
                         + "                        specified the latest SDK that can be found will be used.");
        System.err.println("iOS simulator launch options:");
        System.err.println("  -printdevicetypes     The device type ids that can be used to launch a specific\n"
                         + "                        simulator via the -devicetype flag.");
        System.err.println("  -devicetype     The device type to use to launch the simulator e.g. \"iPhone-6, 8.0\"\n"
                         + "                        (defaults to an iPhone simulator using the latest SDK).");
        
        if(plugins != null) {
            for(Plugin plugin: plugins) {
                if(plugin.getArguments().getArguments().size() > 0) {
                    System.err.println(plugin.getClass().getSimpleName() + " options:");
                    for(PluginArgument arg: plugin.getArguments().getArguments()) {
                        String argString = "  -" + plugin.getArguments().getPrefix() + ":" + arg.getName() + (arg.hasValue()? "=" + arg.getValueName(): "");
                        int whitespace = Math.max(1, 24 - argString.length());
                        System.err.println(argString + repeat(" ", whitespace) + arg.getDescription());
                    }
                }
            }
        }
        System.exit(errorMessage != null ? 1 : 0);
        // @formatter:on
    }

    private static String repeat(String s, int n) {
        StringBuilder builder = new StringBuilder();
        for (int i = 0; i < n; i++) {
            builder.append(s);
        }
        return builder.toString();
    }

    private class UpdateChecker extends Thread {
        private final String address;
        private volatile JSONObject result;

        public UpdateChecker(String address) {
            this.address = address;
            setDaemon(true);
        }

        @Override
        public void run() {
            result = fetchJson(address);
        }
    }

    /**
     * Performs an update check. If a newer version of RoboVM is available a
     * message will be printed to the log. The update check is also used to
     * gather some anonymous usage statistics.
     */
    private void updateCheck() {
        try {
            String uuid = getInstallUuid();
            if (uuid == null) {
                return;
            }
            long lastCheckTime = getLastUpdateCheckTime();
            if (System.currentTimeMillis() - lastCheckTime < 6 * 60 * 60 * 1000) {
                // Only check for an update once every 6 hours
                return;
            }
            updateLastUpdateCheckTime();
            String osName = System.getProperty("os.name", "Unknown");
            String osArch = System.getProperty("os.arch", "Unknown");
            String osVersion = System.getProperty("os.version", "Unknown");
            UpdateChecker t = new UpdateChecker("http://robovm.mobidevelop.com/version?"
                    + "uuid=" + URLEncoder.encode(uuid, "UTF-8") + "&"
                    + "version=" + URLEncoder.encode(Version.getCompilerVersion(), "UTF-8") + "&"
                    + "osName=" + URLEncoder.encode(osName, "UTF-8") + "&"
                    + "osArch=" + URLEncoder.encode(osArch, "UTF-8") + "&"
                    + "osVersion=" + URLEncoder.encode(osVersion, "UTF-8"));
            t.start();
            t.join(5 * 1000); // Wait for a maximum of 5 seconds
            JSONObject result = t.result;
            if (result != null) {
                String version = (String) result.get("version");
                if (version != null && Version.isOlderThan(Version.getCompilerVersion(), version)) {
                    config.getLogger().info("A new version of RoboVM is available. "
                            + "Current version: %s. New version: %s.", Version.getCompilerVersion(), version);
                }
            }
        } catch (Throwable t) {
            if (config.getHome().isDev()) {
                t.printStackTrace();
            }
        }
    }

    private String getInstallUuid() throws IOException {
        File uuidFile = new File(new File(System.getProperty("user.home"), ".robovm"), "uuid");
        uuidFile.getParentFile().mkdirs();
        String uuid = uuidFile.exists() ? FileUtils.readFileToString(uuidFile, "UTF-8") : null;
        if (uuid == null) {
            uuid = UUID.randomUUID().toString();
            FileUtils.writeStringToFile(uuidFile, uuid, "UTF-8");
        }
        uuid = uuid.trim();
        if (uuid.matches("[0-9a-fA-F-]{36}")) {
            return uuid;
        }
        return null;
    }

    private long getLastUpdateCheckTime() {
        try {
            File timeFile = new File(new File(System.getProperty("user.home"), ".robovm"), "last-update-check");
            timeFile.getParentFile().mkdirs();
            return timeFile.exists() ? Long.parseLong(FileUtils.readFileToString(timeFile, "UTF-8").trim()) : 0;
        } catch (IOException e) {
            return 0;
        }
    }

    private void updateLastUpdateCheckTime() throws IOException {
        File timeFile = new File(new File(System.getProperty("user.home"), ".robovm"), "last-update-check");
        timeFile.getParentFile().mkdirs();
        FileUtils.writeStringToFile(timeFile, String.valueOf(System.currentTimeMillis()), "UTF-8");
    }

    private JSONObject fetchJson(String address) {
        try {
            URL url = new URL(address);
            URLConnection conn = url.openConnection();
            conn.setConnectTimeout(5 * 1000);
            conn.setReadTimeout(5 * 1000);
            try (InputStream in = new BufferedInputStream(conn.getInputStream())) {
                return (JSONObject) JSONValue.parseWithException(IOUtils.toString(in, "UTF-8"));
            }
        } catch (Exception e) {
            if (config.getHome().isDev()) {
                e.printStackTrace();
            }
        }
        return null;
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy