/* * 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.target; import java.io.BufferedOutputStream; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; 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.HashSet; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Set; import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.zip.ZipEntry; import java.util.zip.ZipFile; import java.util.zip.ZipOutputStream; import org.apache.commons.io.FileUtils; import org.apache.commons.io.IOUtils; import org.apache.commons.lang3.StringUtils; import org.robovm.compiler.clazz.Path; import org.robovm.compiler.config.Arch; import org.robovm.compiler.config.Config; import org.robovm.compiler.config.OS; import org.robovm.compiler.config.Resource; import org.robovm.compiler.config.Resource.Walker; import org.robovm.compiler.util.ToolchainUtil; import org.simpleframework.xml.Transient; /** * @author niklas * */ public abstract class AbstractTarget implements Target { @Transient protected Config config; protected AbstractTarget() { } @Override public void init(Config config) { this.config = config; } @Override public boolean canLaunch() { return true; } @Override public LaunchParameters createLaunchParameters() { return new LaunchParameters(); } public String getInstallRelativeArchivePath(Path path) { String name = config.getArchiveName(path); if (path.isInBootClasspath()) { return "lib" + File.separator + "boot" + File.separator + name; } return "lib" + File.separator + name; } public boolean canLaunchInPlace() { return true; } protected List<String> getTargetExportedSymbols() { return Collections.emptyList(); } protected List<String> getTargetCcArgs() { return Collections.emptyList(); } protected List<String> getTargetLibs() { return Collections.emptyList(); } public void build(List<File> objectFiles) throws IOException { File outFile = new File(config.getTmpDir(), config.getExecutableName()); config.getLogger().info("Building %s binary %s", config.getTarget().getType(), outFile); LinkedList<String> ccArgs = new LinkedList<String>(); LinkedList<String> libs = new LinkedList<String>(); ccArgs.addAll(getTargetCcArgs()); libs.addAll(getTargetLibs()); String libSuffix = config.isUseDebugLibs() ? "-dbg" : ""; libs.add("-lrobovm-bc" + libSuffix); if (config.getOs().getFamily() == OS.Family.darwin) { libs.add("-force_load"); libs.add(new File(config.getOsArchDepLibDir(), "librobovm-rt" + libSuffix + ".a").getAbsolutePath()); } else { libs.addAll(Arrays.asList("-Wl,--whole-archive", "-lrobovm-rt" + libSuffix, "-Wl,--no-whole-archive")); } if (config.isSkipInstall()) { libs.add("-lrobovm-debug" + libSuffix); } libs.addAll(Arrays.asList( "-lrobovm-core" + libSuffix, "-lgc" + libSuffix, "-lpthread", "-ldl", "-lm", "-lz")); if (config.getOs().getFamily() == OS.Family.linux) { libs.add("-lrt"); } if (config.getOs().getFamily() == OS.Family.darwin) { libs.add("-liconv"); libs.add("-lsqlite3"); libs.add("-framework"); libs.add("Foundation"); } ccArgs.add("-L"); ccArgs.add(config.getOsArchDepLibDir().getAbsolutePath()); List<String> exportedSymbols = new ArrayList<String>(); exportedSymbols.addAll(getTargetExportedSymbols()); exportedSymbols.add("JNI_OnLoad_*"); exportedSymbols.addAll(config.getExportedSymbols()); if (config.getOs().getFamily() == OS.Family.linux) { ccArgs.add("-Wl,-rpath=$ORIGIN"); ccArgs.add("-Wl,--gc-sections"); // ccArgs.add("-Wl,--print-gc-sections"); if (!exportedSymbols.isEmpty()) { // Create an ld version script which makes the exported symbols global // and all other symbols local. StringBuilder sb = new StringBuilder(); sb.append("{\n "); sb.append(StringUtils.join(exportedSymbols, ";\n ")); sb.append(";\n};\n"); File dynamicListFile = new File(config.getTmpDir(), "exported_symbols"); FileUtils.writeStringToFile(dynamicListFile, sb.toString()); ccArgs.add("-Wl,--dynamic-list=" + dynamicListFile.getAbsolutePath()); } } else if (config.getOs().getFamily() == OS.Family.darwin) { ccArgs.add("-ObjC"); if (config.isSkipInstall()) { exportedSymbols.add("catch_exception_raise"); } for (int i = 0; i < exportedSymbols.size(); i++) { // On Darwin symbols are always prefixed with a '_'. We'll prepend // '_' to each symbol here so the user won't have to. exportedSymbols.set(i, "_" + exportedSymbols.get(i)); } if (!config.getUnhideSymbols().isEmpty()) { List<String> aliasedSymbols = new ArrayList<String>(); for (String symbol : config.getUnhideSymbols()) { aliasedSymbols.add("_" + symbol + " __unhidden_" + symbol); } File aliasedSymbolsFile = new File(config.getTmpDir(), "aliased_symbols"); FileUtils.writeLines(aliasedSymbolsFile, "ASCII", aliasedSymbols); ccArgs.add("-Xlinker"); ccArgs.add("-alias_list"); ccArgs.add("-Xlinker"); ccArgs.add(aliasedSymbolsFile.getAbsolutePath()); exportedSymbols.add("__unhidden_*"); } File exportedSymbolsFile = new File(config.getTmpDir(), "exported_symbols"); FileUtils.writeLines(exportedSymbolsFile, "ASCII", exportedSymbols); ccArgs.add("-exported_symbols_list"); ccArgs.add(exportedSymbolsFile.getAbsolutePath()); ccArgs.add("-Wl,-no_implicit_dylibs"); ccArgs.add("-Wl,-dead_strip"); } if (config.getOs().getFamily() == OS.Family.darwin && !config.getFrameworks().isEmpty()) { for (String p : config.getFrameworks()) { libs.add("-framework"); libs.add(p); } } if (config.getOs().getFamily() == OS.Family.darwin && !config.getWeakFrameworks().isEmpty()) { for (String p : config.getWeakFrameworks()) { libs.add("-weak_framework"); libs.add(p); } } if (config.getOs().getFamily() == OS.Family.darwin && !config.getFrameworkPaths().isEmpty()) { for (File p : config.getFrameworkPaths()) { ccArgs.add("-F" + p.getAbsolutePath()); } } if (!config.getLibs().isEmpty()) { objectFiles = new ArrayList<File>(objectFiles); for (Config.Lib lib : config.getLibs()) { String p = lib.getValue(); if (p.endsWith(".o")) { objectFiles.add(new File(p)); } else if (p.endsWith(".a")) { // .a file if (config.getOs().getFamily() == OS.Family.darwin) { if (lib.isForce()) { libs.add("-force_load"); } libs.add(new File(p).getAbsolutePath()); } else { if (lib.isForce()) { libs.add("-Wl,--whole-archive"); } libs.add(new File(p).getAbsolutePath()); if (lib.isForce()) { libs.add("-Wl,--no-whole-archive"); } } } else if (p.endsWith(".dylib") || p.endsWith(".so")) { libs.add(new File(p).getAbsolutePath()); } else { // link via -l if suffix is omitted libs.add("-l" + p); } } } ccArgs.add("-fPIC"); if (config.getOs() == OS.macosx) { if (!config.getFrameworks().contains("CoreServices")) { libs.add("-framework"); libs.add("CoreServices"); } } else if (config.getOs() == OS.ios) { if (!config.getFrameworks().contains("MobileCoreServices")) { libs.add("-framework"); libs.add("MobileCoreServices"); } } doBuild(outFile, ccArgs, objectFiles, libs); } protected void doBuild(File outFile, List<String> ccArgs, List<File> objectFiles, List<String> libs) throws IOException { ToolchainUtil.link(config, ccArgs, objectFiles, libs, outFile); } protected String getExecutable() { return config.getExecutableName(); } @Override public void buildFat(Map<Arch, File> slices) throws IOException { File destFile = new File(config.getTmpDir(), getExecutable()); List<File> files = new ArrayList<>(slices.values()); if (slices.size() > 1) { if (config.getOs() == OS.linux) { throw new UnsupportedOperationException("Fat binaries are not supported when building linux binaries"); } config.getLogger().info("Building fat binary for archs %s", StringUtils.join(slices.keySet())); ToolchainUtil.lipo(config, destFile, files); } else if (!files.get(0).equals(destFile)) { FileUtils.copyFile(files.get(0), destFile); destFile.setExecutable(true, false); } } protected void copyResources(File destDir) throws IOException { for (Resource res : config.getResources()) { res.walk(new Walker() { @Override public boolean processDir(Resource resource, File dir, File destDir) throws IOException { return AbstractTarget.this.processDir(resource, dir, destDir); } @Override public void processFile(Resource resource, File file, File destDir) throws IOException { copyFile(resource, file, destDir); } }, destDir); } } protected void copyDynamicFrameworks(File destDir) throws IOException { final Set<String> swiftLibraries = new HashSet<>(); File frameworksDir = new File(destDir, "Frameworks"); for (String framework : config.getFrameworks()) { boolean isCustomFramework = false; File frameworkDir = null; for (File path : config.getFrameworkPaths()) { frameworkDir = new File(path, framework + ".framework"); if (frameworkDir.exists() && frameworkDir.isDirectory()) { isCustomFramework = true; break; } } if (isCustomFramework) { // check if this is a dynamic framework by finding // at least ony dylib in the root folder boolean isDynamicFramework = false; for(File file: frameworkDir.listFiles()) { if(file.isFile() && isDynamicLibrary(file)) { isDynamicFramework = true; break; } } if(isDynamicFramework) { config.getLogger().info("Copying framework %s from %s to %s", framework, frameworkDir, destDir); new Resource(frameworkDir).walk(new Walker() { @Override public boolean processDir(Resource resource, File dir, File destDir) throws IOException { return !(dir.getName().equals("Headers") || dir.getName().equals("PrivateHeaders") || dir.getName().equals("Modules") || dir.getName().equals("Versions") || dir.getName() .equals("Documentation")); } @Override public void processFile(Resource resource, File file, File destDir) throws IOException { if (!isStaticLibrary(file)) { copyFile(resource, file, destDir); if (isDynamicLibrary(file)) { // remove simulator archs for device builds if (config.getOs() == OS.ios && config.getArch().isArm()) { String archs = ToolchainUtil.lipoInfo(config, file); File inFile = new File(destDir, file.getName()); File tmpFile = new File(destDir, file.getName() + ".tmp"); FileUtils.copyFile(inFile, tmpFile); if(archs.contains(Arch.x86.getClangName())) { ToolchainUtil.lipoRemoveArchs(config, inFile, tmpFile, Arch.x86); } if(archs.contains(Arch.x86_64.getClangName())) { ToolchainUtil.lipoRemoveArchs(config, inFile, tmpFile, Arch.x86_64); } FileUtils.copyFile(tmpFile, inFile); tmpFile.delete(); } // check if this dylib depends on Swift // and register those libraries to be copied // to bundle.app/Frameworks String dependencies = ToolchainUtil.otool(file); Pattern swiftLibraryPattern = Pattern.compile("libswift.+\\.dylib"); Matcher matcher = swiftLibraryPattern.matcher(dependencies); while (matcher.find()) { String library = dependencies.substring(matcher.start(), matcher.end()); swiftLibraries.add(library); } } } } }, frameworksDir); } } } // copy Swift libraries if required if (!swiftLibraries.isEmpty()) { copySwiftLibs(swiftLibraries, frameworksDir); } } protected void copySwiftLibs(Collection<String> swiftLibraries, File targetDir) throws IOException { String system = null; if (config.getOs() == OS.ios) { if (config.getArch().isArm()) { system = "iphoneos"; } else { system = "iphonesimulator"; } } else { system = "mac"; } File swiftDir = new File(ToolchainUtil.findXcodePath(), "Toolchains/XcodeDefault.xctoolchain/usr/lib/swift/" + system); for (String library : swiftLibraries) { config.getLogger().info("Copying swift lib %s from %s to %s", library, swiftDir, targetDir); File swiftLibrary = new File(swiftDir, library); FileUtils.copyFileToDirectory(swiftLibrary, targetDir); } } protected boolean isDynamicLibrary(File file) throws IOException { String result = ToolchainUtil.file(file); return result.contains("shared library"); } protected boolean isStaticLibrary(File file) throws IOException { String result = ToolchainUtil.file(file); return result.contains("ar archive"); } protected boolean processDir(Resource resource, File dir, File destDir) throws IOException { return true; } protected void copyFile(Resource resource, File file, File destDir) throws IOException { config.getLogger().info("Copying resource %s to %s", file, destDir); FileUtils.copyFileToDirectory(file, destDir, true); } public void install() throws IOException { config.getLogger().info("Installing %s binary to %s", config.getTarget().getType(), config.getInstallDir()); config.getInstallDir().mkdirs(); doInstall(config.getInstallDir(), config.getExecutableName(), config.getInstallDir()); } @Override public List<Arch> getDefaultArchs() { return Collections.emptyList(); } @Override public void archive() throws IOException { throw new UnsupportedOperationException("Archiving is not supported for this target"); } protected void doInstall(File installDir, String image, File resourcesDir) throws IOException { if (!config.getTmpDir().equals(installDir) || !image.equals(config.getExecutableName())) { File destFile = new File(installDir, image); FileUtils.copyFile(new File(config.getTmpDir(), config.getExecutableName()), destFile); destFile.setExecutable(true, false); } for (File f : config.getOsArchDepLibDir().listFiles()) { if (f.getName().matches(".*\\.(so|dylib)(\\.1)?")) { FileUtils.copyFileToDirectory(f, installDir); } } stripArchives(installDir); copyResources(resourcesDir); copyDynamicFrameworks(installDir); } public Process launch(LaunchParameters launchParameters) throws IOException { if (config.isSkipLinking()) { throw new IllegalStateException("Cannot skip linking if target should be run"); } // Add -rvm:log=warn to command line arguments if no logging level has been set explicitly boolean add = true; for (String arg : launchParameters.getArguments()) { if (arg.startsWith("-rvm:log=")) { add = false; break; } } if (add) { List<String> args = new ArrayList<String>(launchParameters.getArguments()); args.add(0, "-rvm:log=warn"); launchParameters.setArguments(args); } Map<String, String> env = new HashMap<>(launchParameters.getEnvironment() != null ? launchParameters.getEnvironment() : Collections.<String, String>emptyMap()); env.put("ROBOVM_LAUNCH_MODE", config.isDebug() ? "debug" : "release"); launchParameters.setEnvironment(env); return doLaunch(launchParameters); } protected Process doLaunch(LaunchParameters launchParameters) throws IOException { return createLauncher(launchParameters).execAsync(); } protected Launcher createLauncher(LaunchParameters launchParameters) throws IOException { throw new UnsupportedOperationException(); } protected Target build(Config config) { return this; } protected void stripArchives(File installDir) throws IOException { List<Path> allPaths = new ArrayList<Path>(); allPaths.addAll(config.getClazzes().getPaths()); allPaths.addAll(config.getResourcesPaths()); for (Path path : allPaths) { File destJar = new File(installDir, getInstallRelativeArchivePath(path)); if (!destJar.getParentFile().exists()) { destJar.getParentFile().mkdirs(); } stripArchive(path, destJar); } } protected void stripArchive(Path path, File output) throws IOException { if (!config.isClean() && output.exists() && !path.hasChangedSince(output.lastModified())) { config.getLogger().info("Not creating stripped archive file %s for unchanged path %s", output, path.getFile()); return; } config.getLogger().info("Creating stripped archive file %s", output); ZipOutputStream out = null; try { out = new ZipOutputStream(new BufferedOutputStream(new FileOutputStream(output))); if (path.getFile().isFile()) { ZipFile archive = null; try { archive = new ZipFile(path.getFile()); Enumeration<? extends ZipEntry> entries = archive.entries(); while (entries.hasMoreElements()) { ZipEntry entry = entries.nextElement(); if (entry.getName().toLowerCase().endsWith(".class")) { continue; } if (entry.getName().startsWith("META-INF/robovm/")) { // Don't include anything under META-INF/robovm/ continue; } ZipEntry newEntry = new ZipEntry(entry.getName()); newEntry.setTime(entry.getTime()); out.putNextEntry(newEntry); InputStream in = null; try { in = archive.getInputStream(entry); IOUtils.copy(in, out); out.closeEntry(); } finally { IOUtils.closeQuietly(in); } } } finally { try { archive.close(); } catch (Throwable t) {} } } else { String basePath = path.getFile().getAbsolutePath(); @SuppressWarnings("unchecked") Collection<File> files = FileUtils.listFiles(path.getFile(), null, true); for (File f : files) { if (f.getName().toLowerCase().endsWith(".class")) { continue; } String entryName = f.getAbsolutePath().substring(basePath.length() + 1); if (entryName.startsWith("META-INF/robovm/")) { // Don't include anything under META-INF/robovm/ continue; } ZipEntry newEntry = new ZipEntry(entryName); newEntry.setTime(f.lastModified()); out.putNextEntry(newEntry); InputStream in = null; try { in = new FileInputStream(f); IOUtils.copy(in, out); out.closeEntry(); } finally { IOUtils.closeQuietly(in); } } } } catch (IOException e) { IOUtils.closeQuietly(out); output.delete(); } finally { IOUtils.closeQuietly(out); } } }