/* * Copyright (C) 2012 RoboVM AB * * 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 <http://www.gnu.org/licenses/gpl-2.0.html>. */ package org.robovm.compiler.config; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.OutputStream; import java.io.OutputStreamWriter; import java.io.Reader; import java.io.Writer; import java.lang.reflect.Field; import java.lang.reflect.Method; import java.lang.reflect.Modifier; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.Enumeration; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Properties; import java.util.ServiceLoader; import java.util.TreeMap; import java.util.jar.Attributes; import java.util.jar.JarFile; import java.util.zip.ZipEntry; import java.util.zip.ZipFile; import org.apache.commons.io.FileUtils; import org.apache.commons.io.IOUtils; import org.robovm.compiler.DependencyGraph; import org.robovm.compiler.ITable; import org.robovm.compiler.MarshalerLookup; import org.robovm.compiler.VTable; import org.robovm.compiler.Version; import org.robovm.compiler.clazz.Clazz; import org.robovm.compiler.clazz.Clazzes; import org.robovm.compiler.clazz.Path; import org.robovm.compiler.config.OS.Family; import org.robovm.compiler.config.tools.Tools; import org.robovm.compiler.llvm.DataLayout; import org.robovm.compiler.log.Logger; import org.robovm.compiler.plugin.CompilerPlugin; import org.robovm.compiler.plugin.LaunchPlugin; import org.robovm.compiler.plugin.Plugin; import org.robovm.compiler.plugin.PluginArgument; import org.robovm.compiler.plugin.TargetPlugin; import org.robovm.compiler.plugin.annotation.AnnotationImplPlugin; import org.robovm.compiler.plugin.lambda.LambdaPlugin; import org.robovm.compiler.plugin.objc.InterfaceBuilderClassesPlugin; import org.robovm.compiler.plugin.objc.ObjCBlockPlugin; import org.robovm.compiler.plugin.objc.ObjCMemberPlugin; import org.robovm.compiler.plugin.objc.ObjCProtocolProxyPlugin; import org.robovm.compiler.target.ConsoleTarget; import org.robovm.compiler.target.Target; import org.robovm.compiler.target.ios.IOSTarget; import org.robovm.compiler.target.ios.ProvisioningProfile; import org.robovm.compiler.target.ios.SigningIdentity; import org.robovm.compiler.util.DigestUtil; import org.robovm.compiler.util.InfoPList; import org.robovm.compiler.util.io.RamDiskTools; import org.simpleframework.xml.Element; import org.simpleframework.xml.ElementList; import org.simpleframework.xml.Root; import org.simpleframework.xml.Serializer; import org.simpleframework.xml.convert.Converter; import org.simpleframework.xml.convert.Registry; import org.simpleframework.xml.convert.RegistryStrategy; import org.simpleframework.xml.core.Persister; import org.simpleframework.xml.filter.PlatformFilter; import org.simpleframework.xml.stream.Format; import org.simpleframework.xml.stream.InputNode; import org.simpleframework.xml.stream.OutputNode; /** * Holds compiler configuration. */ @Root public class Config { /** * The max file name length of files stored in the cache. OS X has a limit * of 255 characters. Class names are very unlikely to be this long but some * JVM language compilers (e.g. the Scala compiler) are known to generate * very long class names for auto-generated classes. See #955. */ private static final int MAX_FILE_NAME_LENGTH = 255; public enum Cacerts { full }; public enum TreeShakerMode { none, conservative, aggressive }; @Element(required = false) private File installDir = null; @Element(required = false) private String executableName = null; @Element(required = false) private String imageName = null; @Element(required = false) private Boolean skipRuntimeLib = null; @Element(required = false) private File mainJar; @Element(required = false) private String mainClass; @Element(required = false) private Cacerts cacerts = null; @Element(required = false) private OS os = null; @ElementList(required = false, inline = true) private ArrayList<Arch> archs = null; @ElementList(required = false, entry = "root") private ArrayList<String> roots; @ElementList(required = false, entry = "pattern") private ArrayList<String> forceLinkClasses; @ElementList(required = false, entry = "lib") private ArrayList<Lib> libs; @ElementList(required = false, entry = "symbol") private ArrayList<String> exportedSymbols; @ElementList(required = false, entry = "symbol") private ArrayList<String> unhideSymbols; @ElementList(required = false, entry = "framework") private ArrayList<String> frameworks; @ElementList(required = false, entry = "framework") private ArrayList<String> weakFrameworks; @ElementList(required = false, entry = "path") private ArrayList<File> frameworkPaths; @ElementList(required = false, entry = "resource") private ArrayList<Resource> resources; @ElementList(required = false, entry = "classpathentry") private ArrayList<File> bootclasspath; @ElementList(required = false, entry = "classpathentry") private ArrayList<File> classpath; @ElementList(required = false, entry = "argument") private ArrayList<String> pluginArguments; @Element(required = false, name = "target") private String targetType; @Element(required = false, name = "treeShaker") private TreeShakerMode treeShakerMode; @Element(required = false) private String iosSdkVersion; @Element(required = false, name = "iosInfoPList") private File iosInfoPListFile = null; @Element(required = false, name = "infoPList") private File infoPListFile = null; @Element(required = false) private File iosEntitlementsPList; @Element(required = false) private Tools tools; private SigningIdentity iosSignIdentity; private ProvisioningProfile iosProvisioningProfile; private String iosDeviceType; private InfoPList infoPList; private boolean iosSkipSigning = false; private Properties properties = new Properties(); private Home home = null; private File tmpDir; private File cacheDir = new File(System.getProperty("user.home"), ".robovm/cache"); private File ccBinPath = null; private boolean clean = false; private boolean debug = false; private boolean useDebugLibs = false; private boolean skipLinking = false; private boolean skipInstall = false; private boolean dumpIntermediates = false; private int threads = Runtime.getRuntime().availableProcessors(); private Logger logger = Logger.NULL_LOGGER; /* * The fields below are all initialized in build() and must not be included * when constructing Config clone. We mark them as transient which will make * the builder() method skip them. */ private transient List<Plugin> plugins = new ArrayList<>(); private transient Target target = null; private transient File osArchDepLibDir; private transient File osArchCacheDir; private transient Clazzes clazzes; private transient VTable.Cache vtableCache; private transient ITable.Cache itableCache; private transient List<Path> resourcesPaths = new ArrayList<Path>(); private transient DataLayout dataLayout; private transient MarshalerLookup marshalerLookup; private transient Config configBeforeBuild; private transient DependencyGraph dependencyGraph; private transient Arch sliceArch; protected Config() throws IOException { // Add standard plugins this.plugins.addAll(0, Arrays.asList( new InterfaceBuilderClassesPlugin(), new ObjCProtocolProxyPlugin(), new ObjCMemberPlugin(), new ObjCBlockPlugin(), new AnnotationImplPlugin(), new LambdaPlugin() )); this.loadPluginsFromClassPath(); } /** * Returns a new {@link Builder} which builds exactly this {@link Config} * when {@link Builder#build()} is called. */ public Builder builder() throws IOException { return new Builder(clone(configBeforeBuild)); } public Home getHome() { return home; } public File getInstallDir() { return installDir; } public String getExecutableName() { return executableName; } public String getImageName() { return imageName; } public File getExecutablePath() { return new File(installDir, getExecutableName()); } public File getImagePath() { return getExecutablePath(); } public File getCacheDir() { return osArchCacheDir; } public File getCcBinPath() { return ccBinPath; } public OS getOs() { return os; } public Arch getArch() { return sliceArch; } public List<Arch> getArchs() { return archs == null ? Collections.<Arch> emptyList() : Collections.unmodifiableList(archs); } public String getTriple() { return sliceArch.getLlvmName() + "-unknown-" + os.getLlvmName(); } public String getClangTriple() { return sliceArch.getClangName() + "-unknown-" + os.getLlvmName(); } public DataLayout getDataLayout() { return dataLayout; } public boolean isClean() { return clean; } public boolean isDebug() { return debug; } public boolean isUseDebugLibs() { return useDebugLibs; } public boolean isDumpIntermediates() { return dumpIntermediates; } public boolean isSkipRuntimeLib() { return skipRuntimeLib != null && skipRuntimeLib.booleanValue(); } public boolean isSkipLinking() { return skipLinking; } public boolean isSkipInstall() { return skipInstall; } public int getThreads() { return threads; } public File getMainJar() { return mainJar; } public String getMainClass() { return mainClass; } public Cacerts getCacerts() { return cacerts == null ? Cacerts.full : cacerts; } public List<Path> getResourcesPaths() { return resourcesPaths; } public void addResourcesPath(Path path) { resourcesPaths.add(path); } public DependencyGraph getDependencyGraph() { return dependencyGraph; } public File getTmpDir() { if (tmpDir == null) { try { tmpDir = File.createTempFile("robovm", ".tmp"); } catch (IOException e) { throw new RuntimeException(e); } tmpDir.delete(); tmpDir.mkdirs(); } return tmpDir; } public List<String> getForceLinkClasses() { return forceLinkClasses == null ? Collections.<String> emptyList() : Collections.unmodifiableList(forceLinkClasses); } public List<String> getExportedSymbols() { return exportedSymbols == null ? Collections.<String> emptyList() : Collections.unmodifiableList(exportedSymbols); } public List<String> getUnhideSymbols() { return unhideSymbols == null ? Collections.<String> emptyList() : Collections.unmodifiableList(unhideSymbols); } public List<Lib> getLibs() { return libs == null ? Collections.<Lib> emptyList() : Collections.unmodifiableList(libs); } public List<String> getFrameworks() { return frameworks == null ? Collections.<String> emptyList() : Collections.unmodifiableList(frameworks); } public List<String> getWeakFrameworks() { return weakFrameworks == null ? Collections.<String> emptyList() : Collections.unmodifiableList(weakFrameworks); } public List<File> getFrameworkPaths() { return frameworkPaths == null ? Collections.<File> emptyList() : Collections.unmodifiableList(frameworkPaths); } public List<Resource> getResources() { return resources == null ? Collections.<Resource> emptyList() : Collections.unmodifiableList(resources); } public File getOsArchDepLibDir() { return osArchDepLibDir; } public Clazzes getClazzes() { return clazzes; } public VTable.Cache getVTableCache() { return vtableCache; } public ITable.Cache getITableCache() { return itableCache; } public MarshalerLookup getMarshalerLookup() { return marshalerLookup; } public List<CompilerPlugin> getCompilerPlugins() { List<CompilerPlugin> compilerPlugins = new ArrayList<>(); for (Plugin plugin : plugins) { if (plugin instanceof CompilerPlugin) { compilerPlugins.add((CompilerPlugin) plugin); } } return compilerPlugins; } public List<LaunchPlugin> getLaunchPlugins() { List<LaunchPlugin> launchPlugins = new ArrayList<>(); for (Plugin plugin : plugins) { if (plugin instanceof LaunchPlugin) { launchPlugins.add((LaunchPlugin) plugin); } } return launchPlugins; } public List<TargetPlugin> getTargetPlugins() { List<TargetPlugin> targetPlugins = new ArrayList<>(); for (Plugin plugin : plugins) { if (plugin instanceof TargetPlugin) { targetPlugins.add((TargetPlugin) plugin); } } return targetPlugins; } public List<Plugin> getPlugins() { return plugins; } public List<String> getPluginArguments() { return pluginArguments == null ? Collections.<String> emptyList() : Collections.unmodifiableList(pluginArguments); } public List<File> getBootclasspath() { return bootclasspath == null ? Collections.<File> emptyList() : Collections.unmodifiableList(bootclasspath); } public List<File> getClasspath() { return classpath == null ? Collections.<File> emptyList() : Collections.unmodifiableList(classpath); } public Properties getProperties() { return properties; } public Logger getLogger() { return logger; } public Target getTarget() { return target; } public String getTargetType() { return targetType; } public TreeShakerMode getTreeShakerMode() { return treeShakerMode == null ? TreeShakerMode.none : treeShakerMode; } public String getIosSdkVersion() { return iosSdkVersion; } public String getIosDeviceType() { return iosDeviceType; } public InfoPList getIosInfoPList() { return getInfoPList(); } public InfoPList getInfoPList() { if (infoPList == null && iosInfoPListFile != null) { infoPList = new InfoPList(iosInfoPListFile); } else if (infoPList == null && infoPListFile != null) { infoPList = new InfoPList(infoPListFile); } return infoPList; } public File getIosEntitlementsPList() { return iosEntitlementsPList; } public SigningIdentity getIosSignIdentity() { return iosSignIdentity; } public ProvisioningProfile getIosProvisioningProfile() { return iosProvisioningProfile; } public boolean isIosSkipSigning() { return iosSkipSigning; } public Tools getTools() { return tools; } private static File makeFileRelativeTo(File dir, File f) { if (f.getParentFile() == null) { return dir; } return new File(makeFileRelativeTo(dir, f.getParentFile()), f.getName()); } public String getArchiveName(Path path) { if (path.getFile().isFile()) { return path.getFile().getName(); } else { return "classes" + path.getIndex() + ".jar"; } } static String getFileName(Clazz clazz, String ext) { return getFileName(clazz.getInternalName(), ext, MAX_FILE_NAME_LENGTH); } static String getFileName(String internalName, String ext, int maxFileNameLength) { String packagePath = internalName.substring(0, internalName.lastIndexOf('/') + 1); String className = internalName.substring(internalName.lastIndexOf('/') + 1); String suffix = ext.startsWith(".") ? ext : "." + ext; int length = className.length() + suffix.length(); if (length > maxFileNameLength) { String sha1 = DigestUtil.sha1(className); className = className.substring(0, Math.max(0, maxFileNameLength - suffix.length() - sha1.length())) + sha1; } return packagePath.replace('/', File.separatorChar) + className + suffix; } public File getLlFile(Clazz clazz) { return new File(getCacheDir(clazz.getPath()), getFileName(clazz, "class.ll")); } public File getCFile(Clazz clazz) { return new File(getCacheDir(clazz.getPath()), getFileName(clazz, "class.c")); } public File getBcFile(Clazz clazz) { return new File(getCacheDir(clazz.getPath()), getFileName(clazz, "class.bc")); } public File getSFile(Clazz clazz) { return new File(getCacheDir(clazz.getPath()), getFileName(clazz, "class.s")); } public File getOFile(Clazz clazz) { return new File(getCacheDir(clazz.getPath()), getFileName(clazz, "class.o")); } public File getLinesOFile(Clazz clazz) { return new File(getCacheDir(clazz.getPath()), getFileName(clazz, "class.lines.o")); } public File getLinesLlFile(Clazz clazz) { return new File(getCacheDir(clazz.getPath()), getFileName(clazz, "class.lines.ll")); } public File getInfoFile(Clazz clazz) { return new File(getCacheDir(clazz.getPath()), getFileName(clazz, "class.info")); } public File getCacheDir(Path path) { File srcRoot = path.getFile().getParentFile(); String name = path.getFile().getName(); try { return new File(makeFileRelativeTo(osArchCacheDir, srcRoot.getCanonicalFile()), name); } catch (IOException e) { throw new RuntimeException(e); } } /** * Returns the directory where generated classes are stored for the * specified {@link Path}. Generated classes are stored in the cache * directory in a dir at the same level as the cache dir for the * {@link Path} with <code>.generated</code> appended to the dir name. */ public File getGeneratedClassDir(Path path) { File pathCacheDir = getCacheDir(path); return new File(pathCacheDir.getParentFile(), pathCacheDir.getName() + ".generated"); } private static Map<Object, Object> getManifestAttributes(File jarFile) throws IOException { JarFile jf = null; try { jf = new JarFile(jarFile); return new HashMap<Object, Object>(jf.getManifest().getMainAttributes()); } finally { jf.close(); } } private static String getImplementationVersion(File jarFile) throws IOException { return (String) getManifestAttributes(jarFile).get(Attributes.Name.IMPLEMENTATION_VERSION); } private static String getMainClass(File jarFile) throws IOException { return (String) getManifestAttributes(jarFile).get(Attributes.Name.MAIN_CLASS); } private File extractIfNeeded(Path path) throws IOException { if (path.getFile().isFile()) { File pathCacheDir = getCacheDir(path); File target = new File(pathCacheDir.getParentFile(), pathCacheDir.getName() + ".extracted"); if (!target.exists() || path.getFile().lastModified() > target.lastModified()) { FileUtils.deleteDirectory(target); target.mkdirs(); try (ZipFile zipFile = new ZipFile(path.getFile())) { Enumeration<? extends ZipEntry> entries = zipFile.entries(); while (entries.hasMoreElements()) { ZipEntry entry = entries.nextElement(); if (entry.getName().startsWith("META-INF/robovm/") && !entry.isDirectory()) { File f = new File(target, entry.getName()); f.getParentFile().mkdirs(); try (InputStream in = zipFile.getInputStream(entry); OutputStream out = new FileOutputStream(f)) { IOUtils.copy(in, out); if (entry.getTime() != -1) { f.setLastModified(entry.getTime()); } } } } } target.setLastModified(path.getFile().lastModified()); } return target; } else { return path.getFile(); } } private <T> ArrayList<T> mergeLists(ArrayList<T> from, ArrayList<T> to) { if (from == null) { return to; } to = to != null ? to : new ArrayList<T>(); for (T o : from) { if (!to.contains(o)) { to.add(o); } } return to; } private void mergeConfig(Config from, Config to) { to.exportedSymbols = mergeLists(from.exportedSymbols, to.exportedSymbols); to.unhideSymbols = mergeLists(from.unhideSymbols, to.unhideSymbols); to.forceLinkClasses = mergeLists(from.forceLinkClasses, to.forceLinkClasses); to.frameworkPaths = mergeLists(from.frameworkPaths, to.frameworkPaths); to.frameworks = mergeLists(from.frameworks, to.frameworks); to.libs = mergeLists(from.libs, to.libs); to.resources = mergeLists(from.resources, to.resources); to.weakFrameworks = mergeLists(from.weakFrameworks, to.weakFrameworks); } private void mergeConfigsFromClasspath() throws IOException { List<String> dirs = Arrays.asList( "META-INF/robovm/" + os + "/" + sliceArch, "META-INF/robovm/" + os); // The algorithm below preserves the order of config data from the // classpath. Last the config from this object is added. // First merge all configs on the classpath to an empty Config Config config = new Config(); for (Path path : clazzes.getPaths()) { for (String dir : dirs) { if (path.contains(dir + "/robovm.xml")) { File configXml = new File(new File(extractIfNeeded(path), dir), "robovm.xml"); Builder builder = new Builder(); builder.read(configXml); mergeConfig(builder.config, config); break; } } } // Then merge with this Config mergeConfig(this, config); // Copy back to this Config this.exportedSymbols = config.exportedSymbols; this.unhideSymbols = config.unhideSymbols; this.forceLinkClasses = config.forceLinkClasses; this.frameworkPaths = config.frameworkPaths; this.frameworks = config.frameworks; this.libs = config.libs; this.resources = config.resources; this.weakFrameworks = config.weakFrameworks; } private static <T> List<T> toList(Iterator<T> it) { List<T> l = new ArrayList<T>(); while (it.hasNext()) { l.add(it.next()); } return l; } private void loadPluginsFromClassPath() throws IOException { ClassLoader classLoader = getClass().getClassLoader(); ServiceLoader<CompilerPlugin> compilerPluginLoader = ServiceLoader.load(CompilerPlugin.class, classLoader); ServiceLoader<LaunchPlugin> launchPluginLoader = ServiceLoader.load(LaunchPlugin.class, classLoader); ServiceLoader<TargetPlugin> targetPluginLoader = ServiceLoader.load(TargetPlugin.class, classLoader); plugins.addAll(toList(compilerPluginLoader.iterator())); plugins.addAll(toList(launchPluginLoader.iterator())); plugins.addAll(toList(targetPluginLoader.iterator())); } private static Config clone(Config config) throws IOException { Config clone = new Config(); for (Field f : Config.class.getDeclaredFields()) { if (!Modifier.isStatic(f.getModifiers()) && !Modifier.isTransient(f.getModifiers())) { f.setAccessible(true); try { Object o = f.get(config); if (o instanceof Collection && o instanceof Cloneable) { // Clone collections. Assume the class has a public // clone() method. Method m = o.getClass().getMethod("clone"); o = m.invoke(o); } f.set(clone, o); } catch (Throwable t) { throw new Error(t); } } } return clone; } private Config build() throws IOException { // Create a clone of this Config before we have done anything with it so // that builder() has a fresh Config it can use. this.configBeforeBuild = clone(this); if (home == null) { home = Home.find(); } if (bootclasspath == null) { bootclasspath = new ArrayList<File>(); } if (classpath == null) { classpath = new ArrayList<File>(); } if (mainJar != null) { mainClass = getMainClass(mainJar); classpath.add(mainJar); } if (executableName == null && imageName != null) { executableName = imageName; } if (!skipLinking && executableName == null && mainClass == null) { throw new IllegalArgumentException("No target and no main class specified"); } if (!skipLinking && classpath.isEmpty()) { throw new IllegalArgumentException("No classpath specified"); } if (skipLinking) { skipInstall = true; } if (executableName == null) { executableName = mainClass; } if (imageName == null || !imageName.equals(executableName)) { imageName = executableName; } List<File> realBootclasspath = bootclasspath == null ? new ArrayList<File>() : bootclasspath; if (!isSkipRuntimeLib()) { realBootclasspath = new ArrayList<File>(bootclasspath); realBootclasspath.add(0, home.rtPath); } this.vtableCache = new VTable.Cache(); this.itableCache = new ITable.Cache(); this.marshalerLookup = new MarshalerLookup(this); if (!skipInstall) { if (installDir == null) { installDir = new File(".", executableName); } installDir.mkdirs(); } if (targetType != null) { if (ConsoleTarget.TYPE.equals(targetType)) { target = new ConsoleTarget(); } else if (IOSTarget.TYPE.equals(targetType)) { target = new IOSTarget(); } else { for (TargetPlugin plugin : getTargetPlugins()) { if (plugin.getTarget().getType().equals(targetType)) { target = plugin.getTarget(); break; } } if (target == null) { throw new IllegalArgumentException("Unsupported target '" + targetType + "'"); } } } else { // Auto if (os == OS.ios) { target = new IOSTarget(); } else { target = new ConsoleTarget(); } } if (!getArchs().isEmpty()) { sliceArch = getArchs().get(0); } target.init(this); os = target.getOs(); sliceArch = target.getArch(); dataLayout = new DataLayout(getTriple()); osArchDepLibDir = new File(new File(home.libVmDir, os.toString()), sliceArch.toString()); if (treeShakerMode != null && treeShakerMode != TreeShakerMode.none && os.getFamily() == Family.darwin && sliceArch == Arch.x86) { logger.warn("Tree shaking is not supported when building " + "for OS X/iOS x86 32-bit due to a bug in Xcode's linker. No tree " + "shaking will be performed. Run in 64-bit mode instead to " + "use tree shaking."); treeShakerMode = TreeShakerMode.none; } dependencyGraph = new DependencyGraph(getTreeShakerMode()); RamDiskTools ramDiskTools = new RamDiskTools(); ramDiskTools.setupRamDisk(this, this.cacheDir, this.tmpDir); this.cacheDir = ramDiskTools.getCacheDir(); this.tmpDir = ramDiskTools.getTmpDir(); File osDir = new File(cacheDir, os.toString()); File archDir = new File(osDir, sliceArch.toString()); osArchCacheDir = new File(archDir, debug ? "debug" : "release"); osArchCacheDir.mkdirs(); this.clazzes = new Clazzes(this, realBootclasspath, classpath); mergeConfigsFromClasspath(); return this; } public static class Home { private File binDir = null; private File libVmDir = null; private File rtPath = null; private Map<Cacerts, File> cacertsPath = null; private boolean dev = false; public Home(File homeDir) { this(homeDir, true); } protected Home(File homeDir, boolean validate) { if (validate) { validate(homeDir); } binDir = new File(homeDir, "bin"); libVmDir = new File(homeDir, "lib/vm"); rtPath = new File(homeDir, "lib/robovm-rt.jar"); cacertsPath = new HashMap<Cacerts, File>(); cacertsPath.put(Cacerts.full, new File(homeDir, "lib/robovm-cacerts-full.jar")); } private Home(File devDir, File binDir, File libVmDir, File rtPath) { this.binDir = binDir; this.libVmDir = libVmDir; this.rtPath = rtPath; cacertsPath = new HashMap<Cacerts, File>(); cacertsPath.put(Cacerts.full, new File(devDir, "cacerts/full/target/robovm-cacerts-full-" + Version.getVersion() + ".jar")); this.dev = true; } public boolean isDev() { return dev; } public File getBinDir() { return binDir; } public File getLibVmDir() { return libVmDir; } public File getRtPath() { return rtPath; } public File getCacertsPath(Cacerts cacerts) { return cacertsPath.get(cacerts); } public static Home find() { // Check if ROBOVM_DEV_ROOT has been set. If set it should be // pointing at the root of a complete RoboVM source tree. if (System.getenv("ROBOVM_DEV_ROOT") != null) { File dir = new File(System.getenv("ROBOVM_DEV_ROOT")); return validateDevRootDir(dir); } if (System.getProperty("ROBOVM_DEV_ROOT") != null) { File dir = new File(System.getProperty("ROBOVM_DEV_ROOT")); return validateDevRootDir(dir); } if (System.getenv("ROBOVM_HOME") != null) { File dir = new File(System.getenv("ROBOVM_HOME")); return new Home(dir); } List<File> candidates = new ArrayList<File>(); File userHome = new File(System.getProperty("user.home")); candidates.add(new File(userHome, "Applications/robovm")); candidates.add(new File(userHome, ".robovm/home")); candidates.add(new File("/usr/local/lib/robovm")); candidates.add(new File("/opt/robovm")); candidates.add(new File("/usr/lib/robovm")); for (File dir : candidates) { if (dir.exists()) { return new Home(dir); } } throw new IllegalArgumentException("ROBOVM_HOME not set and no RoboVM " + "installation found in " + candidates); } public static void validate(File dir) { String error = "Path " + dir + " is not a valid RoboVM install directory: "; // Check for required dirs and match the compiler version with our // version. if (!dir.exists()) { throw new IllegalArgumentException(error + "no such path"); } if (!dir.isDirectory()) { throw new IllegalArgumentException(error + "not a directory"); } File libDir = new File(dir, "lib"); if (!libDir.exists() || !libDir.isDirectory()) { throw new IllegalArgumentException(error + "lib/ missing or invalid"); } File binDir = new File(dir, "bin"); if (!binDir.exists() || !binDir.isDirectory()) { throw new IllegalArgumentException(error + "bin/ missing or invalid"); } File libVmDir = new File(libDir, "vm"); if (!libVmDir.exists() || !libVmDir.isDirectory()) { throw new IllegalArgumentException(error + "lib/vm/ missing or invalid"); } File rtJarFile = new File(libDir, "robovm-rt.jar"); if (!rtJarFile.exists() || !rtJarFile.isFile()) { throw new IllegalArgumentException(error + "lib/robovm-rt.jar missing or invalid"); } // Compare the version of this compiler with the version of the // robovm-rt.jar in the home dir. They have to match. try { String thisVersion = Version.getVersion(); String thatVersion = getImplementationVersion(rtJarFile); if (thisVersion == null || thatVersion == null || !thisVersion.equals(thatVersion)) { throw new IllegalArgumentException(error + "version mismatch (expected: " + thisVersion + ", was: " + thatVersion + ")"); } } catch (IOException e) { throw new IllegalArgumentException(error + "failed to get version of rt jar", e); } } private static Home validateDevRootDir(File dir) { String error = "Path " + dir + " is not a valid RoboVM source tree: "; // Check for required dirs. if (!dir.exists()) { throw new IllegalArgumentException(error + "no such path"); } if (!dir.isDirectory()) { throw new IllegalArgumentException(error + "not a directory"); } File vmBinariesDir = new File(dir, "vm/target/binaries"); if (!vmBinariesDir.exists() || !vmBinariesDir.isDirectory()) { throw new IllegalArgumentException(error + "vm/target/binaries/ missing or invalid"); } File binDir = new File(dir, "bin"); if (!binDir.exists() || !binDir.isDirectory()) { throw new IllegalArgumentException(error + "bin/ missing or invalid"); } String rtJarName = "robovm-rt-" + Version.getVersion() + ".jar"; File rtJar = new File(dir, "rt/target/" + rtJarName); File rtClasses = new File(dir, "rt/target/classes/"); File rtSource = rtJar; if (!rtJar.exists() || rtJar.isDirectory()) { if (!rtClasses.exists() || rtClasses.isFile()) { throw new IllegalArgumentException(error + "rt/target/" + rtJarName + " missing or invalid"); } else { rtSource = rtClasses; } } return new Home(dir, binDir, vmBinariesDir, rtSource); } } public static class Builder { final Config config; Builder(Config config) { this.config = config; } public Builder() throws IOException { this.config = new Config(); } public Builder os(OS os) { config.os = os; return this; } public Builder arch(Arch arch) { return archs(arch); } public Builder archs(Arch ... archs) { return archs(Arrays.asList(archs)); } public Builder archs(List<Arch> archs) { if (config.archs == null) { config.archs = new ArrayList<>(); } config.archs.clear(); config.archs.addAll(archs); return this; } public Builder clearClasspathEntries() { if (config.classpath != null) { config.classpath.clear(); } return this; } public Builder addClasspathEntry(File f) { if (config.classpath == null) { config.classpath = new ArrayList<File>(); } config.classpath.add(f); return this; } public Builder clearBootClasspathEntries() { if (config.bootclasspath != null) { config.bootclasspath.clear(); } return this; } public Builder addBootClasspathEntry(File f) { if (config.bootclasspath == null) { config.bootclasspath = new ArrayList<File>(); } config.bootclasspath.add(f); return this; } public Builder mainJar(File f) { config.mainJar = f; return this; } public Builder installDir(File installDir) { config.installDir = installDir; return this; } public Builder executableName(String executableName) { config.executableName = executableName; return this; } public Builder imageName(String imageName) { config.imageName = imageName; return this; } public Builder home(Home home) { config.home = home; return this; } public Builder cacheDir(File cacheDir) { config.cacheDir = cacheDir; return this; } public Builder clean(boolean b) { config.clean = b; return this; } public Builder ccBinPath(File ccBinPath) { config.ccBinPath = ccBinPath; return this; } public Builder debug(boolean b) { config.debug = b; return this; } public Builder useDebugLibs(boolean b) { config.useDebugLibs = b; return this; } public Builder dumpIntermediates(boolean b) { config.dumpIntermediates = b; return this; } public Builder skipRuntimeLib(boolean b) { config.skipRuntimeLib = b; return this; } public Builder skipLinking(boolean b) { config.skipLinking = b; return this; } public Builder skipInstall(boolean b) { config.skipInstall = b; return this; } public Builder threads(int threads) { config.threads = threads; return this; } public Builder mainClass(String mainClass) { config.mainClass = mainClass; return this; } public Builder tmpDir(File tmpDir) { config.tmpDir = tmpDir; return this; } public Builder logger(Logger logger) { config.logger = logger; return this; } public Builder treeShakerMode(TreeShakerMode treeShakerMode) { config.treeShakerMode = treeShakerMode; return this; } public Builder clearForceLinkClasses() { if (config.forceLinkClasses != null) { config.forceLinkClasses.clear(); } return this; } public Builder addForceLinkClass(String pattern) { if (config.forceLinkClasses == null) { config.forceLinkClasses = new ArrayList<String>(); } config.forceLinkClasses.add(pattern); return this; } public Builder clearExportedSymbols() { if (config.exportedSymbols != null) { config.exportedSymbols.clear(); } return this; } public Builder addExportedSymbol(String symbol) { if (config.exportedSymbols == null) { config.exportedSymbols = new ArrayList<String>(); } config.exportedSymbols.add(symbol); return this; } public Builder clearUnhideSymbols() { if (config.unhideSymbols != null) { config.unhideSymbols.clear(); } return this; } public Builder addUnhideSymbol(String symbol) { if (config.unhideSymbols == null) { config.unhideSymbols = new ArrayList<String>(); } config.unhideSymbols.add(symbol); return this; } public Builder clearLibs() { if (config.libs != null) { config.libs.clear(); } return this; } public Builder addLib(Lib lib) { if (config.libs == null) { config.libs = new ArrayList<Lib>(); } config.libs.add(lib); return this; } public Builder clearFrameworks() { if (config.frameworks != null) { config.frameworks.clear(); } return this; } public Builder addFramework(String framework) { if (config.frameworks == null) { config.frameworks = new ArrayList<String>(); } config.frameworks.add(framework); return this; } public Builder clearWeakFrameworks() { if (config.weakFrameworks != null) { config.weakFrameworks.clear(); } return this; } public Builder addWeakFramework(String framework) { if (config.weakFrameworks == null) { config.weakFrameworks = new ArrayList<String>(); } config.weakFrameworks.add(framework); return this; } public Builder clearFrameworkPaths() { if (config.frameworkPaths != null) { config.frameworkPaths.clear(); } return this; } public Builder addFrameworkPath(File frameworkPath) { if (config.frameworkPaths == null) { config.frameworkPaths = new ArrayList<File>(); } config.frameworkPaths.add(frameworkPath); return this; } public Builder clearResources() { if (config.resources != null) { config.resources.clear(); } return this; } public Builder addResource(Resource resource) { if (config.resources == null) { config.resources = new ArrayList<Resource>(); } config.resources.add(resource); return this; } public Builder targetType(String targetType) { config.targetType = targetType; return this; } public Builder clearProperties() { config.properties.clear(); return this; } public Builder addProperties(Properties properties) { config.properties.putAll(properties); return this; } public Builder addProperties(File file) throws IOException { Properties props = new Properties(); Reader reader = null; try { reader = new InputStreamReader(new FileInputStream(file), "utf-8"); props.load(reader); addProperties(props); } finally { IOUtils.closeQuietly(reader); } return this; } public Builder addProperty(String name, String value) { config.properties.put(name, value); return this; } public Builder cacerts(Cacerts cacerts) { config.cacerts = cacerts; return this; } public Builder tools(Tools tools) { config.tools = tools; return this; } public Builder iosSdkVersion(String sdkVersion) { config.iosSdkVersion = sdkVersion; return this; } public Builder iosDeviceType(String deviceType) { config.iosDeviceType = deviceType; return this; } public Builder iosInfoPList(File infoPList) { config.iosInfoPListFile = infoPList; return this; } public Builder infoPList(File infoPList) { config.infoPListFile = infoPList; return this; } public Builder iosEntitlementsPList(File entitlementsPList) { config.iosEntitlementsPList = entitlementsPList; return this; } public Builder iosSignIdentity(SigningIdentity signIdentity) { config.iosSignIdentity = signIdentity; return this; } public Builder iosProvisioningProfile(ProvisioningProfile iosProvisioningProfile) { config.iosProvisioningProfile = iosProvisioningProfile; return this; } public Builder iosSkipSigning(boolean b) { config.iosSkipSigning = b; return this; } public Builder addCompilerPlugin(CompilerPlugin compilerPlugin) { config.plugins.add(compilerPlugin); return this; } public Builder addLaunchPlugin(LaunchPlugin plugin) { config.plugins.add(plugin); return this; } public Builder addTargetPlugin(TargetPlugin plugin) { config.plugins.add(plugin); return this; } public void addPluginArgument(String argName) { if (config.pluginArguments == null) { config.pluginArguments = new ArrayList<>(); } config.pluginArguments.add(argName); } public Config build() throws IOException { for (CompilerPlugin plugin : config.getCompilerPlugins()) { plugin.beforeConfig(this, config); } return config.build(); } /** * Reads properties from a project basedir. If {@code isTest} is * {@code true} this method will first attempt to load a * {@code robovm.test.properties} file in {@code basedir}. * <p> * If no test specific file is found or if {@code isTest} is * {@code false} this method attempts to load a * {@code robovm.properties} and a {@code robovm.local.properties} file * in {@code basedir} and merges them so that properties from the local * file (if it exists) override properties in the non-local file. * <p> * If {@code isTest} is {@code true} and no test specific properties * file was found this method will append {@code Test} to the * {@code app.id} and {@code app.name} properties (if they exist). * <p> * If none of the files can be found found this method does nothing. */ public void readProjectProperties(File basedir, boolean isTest) throws IOException { File testPropsFile = new File(basedir, "robovm.test.properties"); File localPropsFile = new File(basedir, "robovm.local.properties"); File propsFile = new File(basedir, "robovm.properties"); if (isTest && testPropsFile.exists()) { config.logger.info("Loading test RoboVM config properties file: " + testPropsFile.getAbsolutePath()); addProperties(testPropsFile); } else { Properties props = new Properties(); if (propsFile.exists()) { config.logger.info("Loading default RoboVM config properties file: " + propsFile.getAbsolutePath()); try (Reader reader = new InputStreamReader(new FileInputStream(propsFile), "utf-8")) { props.load(reader); } } if (localPropsFile.exists()) { config.logger.info("Loading local RoboVM config properties file: " + localPropsFile.getAbsolutePath()); try (Reader reader = new InputStreamReader(new FileInputStream(localPropsFile), "utf-8")) { props.load(reader); } } if (isTest) { modifyPropertyForTest(props, "app.id"); modifyPropertyForTest(props, "app.name"); modifyPropertyForTest(props, "app.executable"); } addProperties(props); } } private void modifyPropertyForTest(Properties props, String propName) { String propValue = props.getProperty(propName); if (propValue != null && !propValue.endsWith("Test")) { String newPropValue = propValue + "Test"; config.logger.info("Changing %s property from '%s' to '%s'", propName, propValue, newPropValue); props.setProperty(propName, newPropValue); } } /** * Reads a config file from a project basedir. If {@code isTest} is * {@code true} this method will first attempt to load a * {@code robovm.test.xml} file in {@code basedir}. * <p> * If no test-specific file is found or if {@code isTest} is * {@code false} this method attempts to load a {@code robovm.xml} file * in {@code basedir}. * <p> * If none of the files can be found found this method does nothing. */ public void readProjectConfig(File basedir, boolean isTest) throws IOException { File testConfigFile = new File(basedir, "robovm.test.xml"); File configFile = new File(basedir, "robovm.xml"); if (isTest && testConfigFile.exists()) { config.logger.info("Loading test RoboVM config file: " + testConfigFile.getAbsolutePath()); read(testConfigFile); } else if (configFile.exists()) { config.logger.info("Loading default RoboVM config file: " + configFile.getAbsolutePath()); read(configFile); } } public void read(File file) throws IOException { Reader reader = null; try { reader = new InputStreamReader(new FileInputStream(file), "utf-8"); read(reader, file.getAbsoluteFile().getParentFile()); } finally { IOUtils.closeQuietly(reader); } } public void read(Reader reader, File wd) throws IOException { try { Serializer serializer = createSerializer(wd); serializer.read(config, reader); } catch (IOException e) { throw e; } catch (RuntimeException e) { throw e; } catch (Exception e) { throw (IOException) new IOException().initCause(e); } // <roots> was renamed to <forceLinkClasses> but we still support // <roots>. We need to copy <roots> to <forceLinkClasses> and set // <roots> to null. if (config.roots != null && !config.roots.isEmpty()) { if (config.forceLinkClasses == null) { config.forceLinkClasses = new ArrayList<String>(); } config.forceLinkClasses.addAll(config.roots); config.roots = null; } } public void write(File file) throws IOException { Writer writer = null; try { writer = new OutputStreamWriter(new FileOutputStream(file), "utf-8"); write(writer, file.getAbsoluteFile().getParentFile()); } finally { IOUtils.closeQuietly(writer); } } public void write(Writer writer, File wd) throws IOException { try { Serializer serializer = createSerializer(wd); serializer.write(config, writer); } catch (IOException e) { throw e; } catch (RuntimeException e) { throw e; } catch (Exception e) { throw (IOException) new IOException().initCause(e); } } private Serializer createSerializer(final File wd) throws Exception { RelativeFileConverter fileConverter = new RelativeFileConverter(wd); Serializer resourceSerializer = new Persister( new RegistryStrategy(new Registry().bind(File.class, fileConverter)), new PlatformFilter(config.properties), new Format(2)); Registry registry = new Registry(); RegistryStrategy registryStrategy = new RegistryStrategy(registry); Serializer serializer = new Persister(registryStrategy, new PlatformFilter(config.properties), new Format(2)); registry.bind(File.class, fileConverter); registry.bind(Lib.class, new RelativeLibConverter(fileConverter)); registry.bind(Resource.class, new ResourceConverter(fileConverter, resourceSerializer)); return serializer; } /** * Fetches the {@link PluginArgument}s of all registered plugins for * parsing. */ public Map<String, PluginArgument> fetchPluginArguments() { Map<String, PluginArgument> args = new TreeMap<>(); for (Plugin plugin : config.plugins) { for (PluginArgument arg : plugin.getArguments().getArguments()) { args.put(plugin.getArguments().getPrefix() + ":" + arg.getName(), arg); } } return args; } public List<Plugin> getPlugins() { return config.getPlugins(); } } public static final class Lib { private final String value; private final boolean force; public Lib(String value, boolean force) { this.value = value; this.force = force; } public String getValue() { return value; } public boolean isForce() { return force; } @Override public String toString() { return "Lib [value=" + value + ", force=" + force + "]"; } @Override public int hashCode() { final int prime = 31; int result = 1; result = prime * result + (force ? 1231 : 1237); result = prime * result + ((value == null) ? 0 : value.hashCode()); return result; } @Override public boolean equals(Object obj) { if (this == obj) { return true; } if (obj == null) { return false; } if (getClass() != obj.getClass()) { return false; } Lib other = (Lib) obj; if (force != other.force) { return false; } if (value == null) { if (other.value != null) { return false; } } else if (!value.equals(other.value)) { return false; } return true; } } private static final class RelativeLibConverter implements Converter<Lib> { private final RelativeFileConverter fileConverter; public RelativeLibConverter(RelativeFileConverter fileConverter) { this.fileConverter = fileConverter; } @Override public Lib read(InputNode node) throws Exception { String value = node.getValue(); if (value == null) { return null; } InputNode forceNode = node.getAttribute("force"); boolean force = forceNode == null || Boolean.valueOf(forceNode.getValue()); if (value.endsWith(".a") || value.endsWith(".o")) { return new Lib(fileConverter.read(value).getAbsolutePath(), force); } else { return new Lib(value, force); } } @Override public void write(OutputNode node, Lib lib) throws Exception { String value = lib.getValue(); boolean force = lib.isForce(); if (value.endsWith(".a") || value.endsWith(".o")) { fileConverter.write(node, new File(value)); } else { node.setValue(value); } if (!force) { node.setAttribute("force", "false"); } } } private static final class RelativeFileConverter implements Converter<File> { private final String wdPrefix; public RelativeFileConverter(File wd) { if (wd.isFile()) { wd = wd.getParentFile(); } String prefix = wd.getAbsolutePath(); if (prefix.endsWith(File.separator)) { prefix = prefix.substring(0, prefix.length() - 1); } wdPrefix = prefix; } File read(String value) { if (value == null) { return null; } File file = new File(value); if (!file.isAbsolute()) { file = new File(wdPrefix, value); } return file; } @Override public File read(InputNode node) throws Exception { return read(node.getValue()); } @Override public void write(OutputNode node, File value) throws Exception { String path = value.getAbsolutePath(); if (path.equals(wdPrefix)) { if ("directory".equals(node.getName())) { // Skip node.remove(); } else { node.setValue(""); } } else if (path.startsWith(wdPrefix) && path.charAt(wdPrefix.length()) == File.separatorChar) { node.setValue(path.substring(wdPrefix.length() + 1)); } else { node.setValue(path); } } } private static final class ResourceConverter implements Converter<Resource> { private final RelativeFileConverter fileConverter; private final Serializer serializer; public ResourceConverter(RelativeFileConverter fileConverter, Serializer serializer) { this.fileConverter = fileConverter; this.serializer = serializer; } @Override public Resource read(InputNode node) throws Exception { String value = node.getValue(); if (value != null && value.trim().length() > 0) { return new Resource(fileConverter.read(value)); } return serializer.read(Resource.class, node); } @Override public void write(OutputNode node, Resource resource) throws Exception { File path = resource.getPath(); if (path != null) { fileConverter.write(node, path); } else { node.remove(); serializer.write(resource, node.getParent()); } } } }