/*
* 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.ios;
import java.io.File;
import java.io.FileFilter;
import java.io.IOException;
import java.io.OutputStream;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.NoSuchElementException;
import java.util.Set;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.FilenameUtils;
import org.apache.commons.io.IOUtils;
import org.apache.commons.io.filefilter.AndFileFilter;
import org.apache.commons.io.filefilter.PrefixFileFilter;
import org.apache.commons.io.filefilter.RegexFileFilter;
import org.apache.commons.io.filefilter.SuffixFileFilter;
import org.robovm.compiler.CompilerException;
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.log.Logger;
import org.robovm.compiler.log.LoggerProxy;
import org.robovm.compiler.target.AbstractTarget;
import org.robovm.compiler.target.LaunchParameters;
import org.robovm.compiler.target.Launcher;
import org.robovm.compiler.target.ios.ProvisioningProfile.Type;
import org.robovm.compiler.util.Executor;
import org.robovm.compiler.util.ToolchainUtil;
import org.robovm.compiler.util.io.OpenOnWriteFileOutputStream;
import org.robovm.libimobiledevice.AfcClient.UploadProgressCallback;
import org.robovm.libimobiledevice.IDevice;
import org.robovm.libimobiledevice.InstallationProxyClient.StatusCallback;
import org.robovm.libimobiledevice.util.AppLauncher;
import org.robovm.libimobiledevice.util.AppLauncherCallback;
import com.dd.plist.NSArray;
import com.dd.plist.NSDictionary;
import com.dd.plist.NSNumber;
import com.dd.plist.NSObject;
import com.dd.plist.NSString;
import com.dd.plist.PropertyListParser;
/**
* @author niklas
*
*/
public class IOSTarget extends AbstractTarget {
public static final String TYPE = "ios";
private static File iosSimPath;
private Arch arch;
private SDK sdk;
private File entitlementsPList;
private SigningIdentity signIdentity;
private ProvisioningProfile provisioningProfile;
private IDevice device;
private File partialPListDir;
public IOSTarget() {}
public String getType() {
return TYPE;
}
@Override
public Arch getArch() {
return arch;
}
@Override
public LaunchParameters createLaunchParameters() {
if (isSimulatorArch(arch)) {
return new IOSSimulatorLaunchParameters();
}
return new IOSDeviceLaunchParameters();
}
public static boolean isSimulatorArch(Arch arch) {
return arch == Arch.x86 || arch == Arch.x86_64;
}
public static boolean isDeviceArch(Arch arch) {
return arch == Arch.thumbv7 || arch == Arch.arm64;
}
public static synchronized File getIosSimPath() {
if (iosSimPath == null) {
try {
File path = File.createTempFile("ios-sim", "");
FileUtils.copyURLToFile(IOSTarget.class.getResource("/ios-sim"), path);
path.setExecutable(true);
path.deleteOnExit();
iosSimPath = path;
} catch (IOException e) {
throw new Error(e);
}
}
return iosSimPath;
}
public List<SDK> getSDKs() {
if (isSimulatorArch(arch)) {
return SDK.listSimulatorSDKs();
} else {
return SDK.listDeviceSDKs();
}
}
/**
* Returns the {@link IDevice} when an app has been launched on a device.
* Returns {@code null} before {@link #launch(LaunchParameters)} has been
* called or if the app was launched in the simulator.
*/
public IDevice getDevice() {
return device;
}
@Override
protected Launcher createLauncher(LaunchParameters launchParameters) throws IOException {
if (isSimulatorArch(arch)) {
return createIOSSimLauncher(launchParameters);
} else {
return createIOSDevLauncher(launchParameters);
}
}
private Launcher createIOSSimLauncher(LaunchParameters launchParameters)
throws IOException {
File dir = getAppDir();
String iosSimPath = new File(config.getHome().getBinDir(), "ios-sim").getAbsolutePath();
List<Object> args = new ArrayList<Object>();
args.add("launch");
args.add(dir);
args.add("--timeout");
args.add("90");
args.add("--unbuffered");
if (((IOSSimulatorLaunchParameters) launchParameters).getDeviceType() != null) {
DeviceType deviceType = ((IOSSimulatorLaunchParameters) launchParameters).getDeviceType();
args.add("--devicetypeid");
args.add(deviceType.getDeviceTypeId());
}
if (launchParameters.getStdoutFifo() != null) {
args.add("--stdout");
args.add(launchParameters.getStdoutFifo());
}
if (launchParameters.getStderrFifo() != null) {
args.add("--stderr");
args.add(launchParameters.getStderrFifo());
}
if (launchParameters.getEnvironment() != null) {
for (Entry<String, String> entry : launchParameters.getEnvironment().entrySet()) {
args.add("--setenv");
args.add(entry.getKey() + "=" + entry.getValue());
}
}
if (!launchParameters.getArguments().isEmpty()) {
args.add("--args");
args.addAll(launchParameters.getArguments());
}
File xcodePath = new File(ToolchainUtil.findXcodePath());
Map<String, String> env = Collections.singletonMap("DEVELOPER_DIR", xcodePath.getAbsolutePath());
// See issue https://github.com/robovm/robovm/issues/1150, we need
// to swallow the error message by ios-sim on Xcode 7. We need
// to remove this
Logger proxyLogger = new Logger() {
boolean skipWarningsAndErrors = false;
@Override
public void debug(String format, Object... args) {
config.getLogger().debug(format, args);
}
@Override
public void info(String format, Object... args) {
config.getLogger().info(format, args);
}
@Override
public void warn(String format, Object... args) {
// we swallow the first warning message, then
// error() will turn on skipWarningsAndErrors until
// we get another warning.
if (format.toString().contains("DVTPlugInManager.m:257")) {
config.getLogger().info(format, args);
return;
}
// received the "closing" warning, enable
// logging of warnings and errors again
if (skipWarningsAndErrors) {
skipWarningsAndErrors = false;
config.getLogger().info(format, args);
} else {
config.getLogger().warn(format, args);
}
}
@Override
public void error(String format, Object... args) {
if (format.contains(
"Requested but did not find extension point with identifier Xcode.DVTFoundation.DevicePlatformMapping")) {
skipWarningsAndErrors = true;
}
if (skipWarningsAndErrors) {
config.getLogger().info(format, args);
} else {
config.getLogger().error(format, args);
}
}
};
return new Executor(proxyLogger, iosSimPath)
.args(args)
.wd(launchParameters.getWorkingDirectory())
.inheritEnv(false)
.env(env);
}
private Launcher createIOSDevLauncher(LaunchParameters launchParameters)
throws IOException {
IOSDeviceLaunchParameters deviceLaunchParameters = (IOSDeviceLaunchParameters) launchParameters;
String deviceId = deviceLaunchParameters.getDeviceId();
int forwardPort = deviceLaunchParameters.getForwardPort();
AppLauncherCallback callback = deviceLaunchParameters.getAppPathCallback();
if (deviceId == null) {
String[] udids = IDevice.listUdids();
if (udids.length == 0) {
throw new RuntimeException("No devices connected");
}
if (udids.length > 1) {
config.getLogger().warn("More than 1 device connected (%s). "
+ "Using %s.", Arrays.asList(udids), udids[0]);
}
deviceId = udids[0];
}
device = new IDevice(deviceId);
OutputStream out = null;
if (launchParameters.getStdoutFifo() != null) {
out = new OpenOnWriteFileOutputStream(launchParameters.getStdoutFifo());
} else {
out = System.out;
}
Map<String, String> env = launchParameters.getEnvironment();
if (env == null) {
env = new HashMap<>();
}
AppLauncher launcher = new AppLauncher(device, getAppDir()) {
protected void log(String s, Object... args) {
config.getLogger().info(s, args);
}
}
.stdout(out)
.closeOutOnExit(true)
.args(launchParameters.getArguments().toArray(new String[0]))
.env(env)
.forward(forwardPort)
.appLauncherCallback(callback)
.xcodePath(ToolchainUtil.findXcodePath())
.uploadProgressCallback(new UploadProgressCallback() {
boolean first = true;
public void success() {
config.getLogger().info("[100%%] Upload complete");
}
public void progress(File path, int percentComplete) {
if (first) {
config.getLogger().info("[ 0%%] Beginning upload...");
}
first = false;
config.getLogger().info("[%3d%%] Uploading %s...", percentComplete, path);
}
public void error(String message) {}
})
.installStatusCallback(new StatusCallback() {
boolean first = true;
public void success() {
config.getLogger().info("[100%%] Install complete");
}
public void progress(String status, int percentComplete) {
if (first) {
config.getLogger().info("[ 0%%] Beginning installation...");
}
first = false;
config.getLogger().info("[%3d%%] %s", percentComplete, status);
}
public void error(String message) {}
});
return new AppLauncherProcess(config.getLogger(), launcher, launchParameters);
}
@Override
protected void doBuild(File outFile, List<String> ccArgs,
List<File> objectFiles, List<String> libArgs)
throws IOException {
// Always link against UIKit or else it will not be initialized properly
// causing problems with UIAlertView and maybe other classes on iOS 7
// (#195)
if (!config.getFrameworks().contains("UIKit")) {
libArgs.add("-framework");
libArgs.add("UIKit");
}
String minVersion = getMinimumOSVersion();
int majorVersionNumber = -1;
try {
majorVersionNumber = Integer.parseInt(minVersion.substring(0, minVersion.indexOf('.')));
} catch (NumberFormatException e) {
throw new CompilerException("Failed to get major version number from "
+ "MinimumOSVersion string '" + minVersion + "'");
}
if (isDeviceArch(arch)) {
ccArgs.add("-miphoneos-version-min=" + minVersion);
if (config.isDebug()) {
ccArgs.add("-Wl,-no_pie");
}
} else {
ccArgs.add("-mios-simulator-version-min=" + minVersion);
if (config.getArch() == Arch.x86 || config.isDebug()) {
ccArgs.add("-Wl,-no_pie");
}
}
if (majorVersionNumber >= 7) {
// On iOS 7 and higher the linker will default to link against
// libc++ which is needed for C++11 support. We need the older
// libstdc++ as our native libs are compiled against it and need to
// work on iOS 6. If an app needs C++11 support the user will need
// to link against /usr/lib/libc++.dylib explicitly.
ccArgs.add("-stdlib=libstdc++");
}
ccArgs.add("-isysroot");
ccArgs.add(sdk.getRoot().getAbsolutePath());
// specify dynamic library loading path
libArgs.add("-Xlinker");
libArgs.add("-rpath");
libArgs.add("-Xlinker");
libArgs.add("@executable_path/Frameworks");
libArgs.add("-Xlinker");
libArgs.add("-rpath");
libArgs.add("-Xlinker");
libArgs.add("@loader_path/Frameworks");
super.doBuild(outFile, ccArgs, objectFiles, libArgs);
}
protected void prepareInstall(File installDir) throws IOException {
createInfoPList(installDir);
generateDsym(installDir, getExecutable(), false);
if (isDeviceArch(arch)) {
// only strip if this is not a debug build, otherwise
// LLDB can't resolve the DWARF info
if (!config.isDebug()) {
strip(installDir, getExecutable());
}
if (config.isIosSkipSigning()) {
config.getLogger().warn("Skipping code signing. The resulting app will "
+ "be unsigned and will not run on unjailbroken devices");
ldid(entitlementsPList, installDir);
} else {
// Copy the provisioning profile
copyProvisioningProfile(provisioningProfile, installDir);
boolean getTaskAllow = provisioningProfile.getType() == Type.Development;
signFrameworks(installDir, getTaskAllow);
codesignApp(signIdentity, getOrCreateEntitlementsPList(getTaskAllow, getBundleId()), installDir);
// For some odd reason there needs to be a symbolic link in the
// root of
// the app bundle named CodeResources pointing at
// _CodeSignature/CodeResources
new Executor(config.getLogger(), "ln")
.args("-f", "-s", "_CodeSignature/CodeResources", new File(installDir, "CodeResources"))
.exec();
}
}
}
private void copyProvisioningProfile(ProvisioningProfile profile, File destDir) throws IOException {
config.getLogger().info("Copying %s provisioning profile: %s (%s)",
profile.getType(),
profile.getName(),
profile.getEntitlements().objectForKey("application-identifier"));
FileUtils.copyFile(profile.getFile(), new File(destDir, "embedded.mobileprovision"));
}
protected void prepareLaunch(File appDir) throws IOException {
super.doInstall(appDir, getExecutable(), appDir);
createInfoPList(appDir);
generateDsym(appDir, getExecutable(), true);
if (isDeviceArch(arch)) {
if (config.isIosSkipSigning()) {
config.getLogger().warn("Skiping code signing. The resulting app will "
+ "be unsigned and will not run on unjailbroken devices");
ldid(getOrCreateEntitlementsPList(true, getBundleId()), appDir);
} else {
copyProvisioningProfile(provisioningProfile, appDir);
signFrameworks(appDir, true);
// sign the app
codesignApp(signIdentity, getOrCreateEntitlementsPList(true, getBundleId()), appDir);
}
}
}
private void signFrameworks(File appDir, boolean getTaskAllow) throws IOException {
// sign dynamic frameworks first
File frameworksDir = new File(appDir, "Frameworks");
if (frameworksDir.exists() && frameworksDir.isDirectory()) {
// Sign swift rt libs
for (File swiftLib : frameworksDir.listFiles()) {
if (swiftLib.getName().endsWith(".dylib")) {
codesignSwiftLib(signIdentity, swiftLib);
}
}
// sign embedded frameworks
for (File framework : frameworksDir.listFiles()) {
if (framework.isDirectory() && framework.getName().endsWith(".framework")) {
codesignCustomFramework(signIdentity, framework);
}
}
}
}
private void codesignApp(SigningIdentity identity, File entitlementsPList, File appDir) throws IOException {
config.getLogger().info("Code signing app using identity '%s' with fingerprint %s", identity.getName(),
identity.getFingerprint());
codesign(identity, entitlementsPList, false, false, true, appDir);
}
private void codesignSwiftLib(SigningIdentity identity, File swiftLib) throws IOException {
config.getLogger().info("Code signing swift dylib '%s' using identity '%s' with fingerprint %s", swiftLib.getName(), identity.getName(),
identity.getFingerprint());
codesign(identity, null, false, true, false, swiftLib);
}
private void codesignCustomFramework(SigningIdentity identity, File frameworkDir) throws IOException {
config.getLogger().info("Code signing framework '%s' using identity '%s' with fingerprint %s", frameworkDir.getName(), identity.getName(),
identity.getFingerprint());
codesign(identity, null, true, false, true, frameworkDir);
}
private void codesign(SigningIdentity identity, File entitlementsPList, boolean preserveMetadata, boolean verbose, boolean allocate, File target) throws IOException {
List<Object> args = new ArrayList<Object>();
args.add("-f");
args.add("-s");
args.add(identity.getFingerprint());
if (entitlementsPList != null) {
args.add("--entitlements");
args.add(entitlementsPList);
}
if (preserveMetadata) {
args.add("--preserve-metadata=identifier,entitlements,resource-rules");
}
if (verbose) {
args.add("--verbose");
}
args.add(target);
Executor executor = new Executor(config.getLogger(), "codesign");
if (allocate) {
executor.addEnv("CODESIGN_ALLOCATE", ToolchainUtil.findXcodeCommand("codesign_allocate", "iphoneos"));
}
executor.args(args);
executor.exec();
}
private void ldid(File entitlementsPList, File appDir) throws IOException {
File executableFile = new File(appDir, getExecutable());
config.getLogger().info("Pseudo-signing %s", executableFile.getAbsolutePath());
List<Object> args = new ArrayList<Object>();
if (entitlementsPList != null) {
args.add("-S" + entitlementsPList.getAbsolutePath());
} else {
args.add("-S");
}
args.add(executableFile);
new Executor(config.getLogger(), new File(config.getHome().getBinDir(), "ldid"))
.args(args)
.exec();
}
private File getOrCreateEntitlementsPList(boolean getTaskAllow, String bundleId) throws IOException {
try {
File destFile = new File(config.getTmpDir(), "Entitlements.plist");
NSDictionary dict = null;
if (entitlementsPList != null) {
dict = (NSDictionary) PropertyListParser.parse(entitlementsPList);
} else {
dict = (NSDictionary) PropertyListParser.parse(IOUtils.toByteArray(getClass().getResourceAsStream(
"/Entitlements.plist")));
}
if (provisioningProfile != null) {
NSDictionary profileEntitlements = provisioningProfile.getEntitlements();
for (String key : profileEntitlements.allKeys()) {
if (dict.objectForKey(key) == null) {
dict.put(key, profileEntitlements.objectForKey(key));
}
}
dict.put("application-identifier", provisioningProfile.getAppIdPrefix() + "." + bundleId);
}
dict.put("get-task-allow", getTaskAllow);
PropertyListParser.saveAsXML(dict, destFile);
return destFile;
} catch (IOException e) {
throw e;
} catch (Exception e) {
throw new RuntimeException(e);
}
}
private void generateDsym(final File dir, final String executable, boolean copyToIndexedDir) throws IOException {
final File dsymDir = new File(dir.getParentFile(), dir.getName() + ".dSYM");
final File exePath = new File(dir, executable);
FileUtils.deleteDirectory(dsymDir);
Logger logger = new LoggerProxy(config.getLogger()) {
@Override
public void warn(String format, Object... args) {
if (!(format.startsWith("warning:") && format.contains("could not find object file symbol for symbol"))) {
// Suppress this kind of warnings for now. See robovm/robovm#1126.
super.warn(format, args);
}
}
};
final Process process = new Executor(logger, "xcrun")
.args("dsymutil", "-o", dsymDir, exePath)
.execAsync();
if (copyToIndexedDir) {
new Thread() {
public void run() {
try {
process.waitFor();
} catch (InterruptedException e) {
return;
}
copyToIndexedDir(dir, executable, dsymDir, exePath);
}
}.start();
}
}
private void strip(File dir, String executable) throws IOException {
new Executor(config.getLogger(), "xcrun")
.args("strip", "-x", new File(dir, executable))
.exec();
}
@Override
protected void doInstall(File installDir, String executable, File resourcesDir) throws IOException {
super.doInstall(installDir, getExecutable(), resourcesDir);
prepareInstall(installDir);
}
@Override
protected Process doLaunch(LaunchParameters launchParameters) throws IOException {
prepareLaunch(getAppDir());
Process process = super.doLaunch(launchParameters);
if (launchParameters instanceof IOSSimulatorLaunchParameters) {
File script = File.createTempFile("BISTF", ".scpt");
FileUtils.copyURLToFile(getClass().getResource("/BringIOSSimulatorToFront.scpt"), script);
new Executor(config.getHome().isDev() ? config.getLogger() : Logger.NULL_LOGGER, "osascript")
.args(script)
.execAsync();
}
return process;
}
@Override
public List<Arch> getDefaultArchs() {
return Arrays.asList(Arch.thumbv7, Arch.arm64);
}
public void archive() throws IOException {
config.getLogger().info("Creating IPA in %s", config.getInstallDir());
config.getInstallDir().mkdirs();
File tmpDir = new File(config.getInstallDir(), getExecutable() + ".app");
FileUtils.deleteDirectory(tmpDir);
tmpDir.mkdirs();
super.doInstall(tmpDir, getExecutable(), tmpDir);
prepareInstall(tmpDir);
packageApplication(tmpDir);
}
private void packageApplication(File appDir) throws IOException {
File ipaFile = new File(config.getInstallDir(), getExecutable() + ".ipa");
config.getLogger().info("Packaging IPA %s from %s", ipaFile.getName(), appDir.getName());
File tmpDir = new File(config.getInstallDir(), "ipabuild");
FileUtils.deleteDirectory(tmpDir);
tmpDir.mkdirs();
File payloadDir = new File(tmpDir, "Payload");
payloadDir.mkdir();
config.getLogger().info("Copying %s to %s", appDir.getName(), payloadDir);
new Executor(config.getLogger(), "cp")
.args("-Rp", appDir, payloadDir)
.exec();
File frameworksDir = new File(appDir, "Frameworks");
if (frameworksDir.exists()){
String[] swiftLibs = frameworksDir.list(new AndFileFilter(
new PrefixFileFilter("libswift"),
new SuffixFileFilter(".dylib")));
if (swiftLibs.length > 0){
File swiftSupportDir = new File(tmpDir, "SwiftSupport");
swiftSupportDir.mkdir();
copySwiftLibs(Arrays.asList(swiftLibs), swiftSupportDir);
}
}
config.getLogger().info("Zipping %s to %s", tmpDir, ipaFile);
new Executor(Logger.NULL_LOGGER, "zip")
.wd(tmpDir)
.args("--symlinks", "--recurse-paths", ipaFile, ".")
.exec();
config.getLogger().info("Deleting temp dir %s", tmpDir);
FileUtils.deleteDirectory(tmpDir);
}
@Override
protected boolean processDir(Resource resource, File dir, File destDir) throws IOException {
if (dir.getName().endsWith(".atlas")) {
destDir.mkdirs();
ToolchainUtil.textureatlas(config, dir, destDir);
return false;
} else if (dir.getName().endsWith(".xcassets")) {
ToolchainUtil.actool(config, createPartialInfoPlistFile(dir), dir, getAppDir());
return false;
}
return super.processDir(resource, dir, destDir);
}
@Override
protected void copyFile(Resource resource, File file, File destDir)
throws IOException {
if (isDeviceArch(arch) && !resource.isSkipPngCrush()
&& file.getName().toLowerCase().endsWith(".png")) {
destDir.mkdirs();
File outFile = new File(destDir, file.getName());
ToolchainUtil.pngcrush(config, file, outFile);
} else if (file.getName().toLowerCase().endsWith(".strings")) {
destDir.mkdirs();
File outFile = new File(destDir, file.getName());
ToolchainUtil.compileStrings(config, file, outFile);
} else if (file.getName().toLowerCase().endsWith(".storyboard")) {
destDir.mkdirs();
ToolchainUtil.ibtool(config, createPartialInfoPlistFile(file), file, destDir);
} else if (file.getName().toLowerCase().endsWith(".xib")) {
destDir.mkdirs();
String fileName = file.getName();
fileName = fileName.substring(0, fileName.lastIndexOf('.')) + ".nib";
File outFile = new File(destDir, fileName);
ToolchainUtil.ibtool(config, createPartialInfoPlistFile(file), file, outFile);
} else {
super.copyFile(resource, file, destDir);
}
}
private File createPartialInfoPlistFile(File f) throws IOException {
File tmpFile = File.createTempFile(f.getName() + "_", ".plist", partialPListDir);
tmpFile.delete();
return tmpFile;
}
protected File getAppDir() {
File dir = null;
if (!config.isSkipInstall()) {
dir = new File(config.getInstallDir(), getExecutable() + ".app");
if (!dir.exists()) {
dir = config.getInstallDir();
}
} else {
dir = new File(config.getTmpDir(), getExecutable() + ".app");
dir.mkdirs();
}
return dir;
}
@Override
protected String getExecutable() {
if (config.getIosInfoPList() != null) {
String bundleExecutable = config.getIosInfoPList().getBundleExecutable();
if (bundleExecutable != null) {
return bundleExecutable;
}
}
return config.getExecutableName();
}
protected String getBundleId() {
if (config.getIosInfoPList() != null) {
String bundleIdentifier = config.getIosInfoPList().getBundleIdentifier();
if (bundleIdentifier != null) {
return bundleIdentifier;
}
}
return config.getMainClass() != null ? config.getMainClass() : config.getExecutableName();
}
protected String getMinimumOSVersion() {
if (config.getIosInfoPList() != null) {
String minVersion = config.getIosInfoPList().getMinimumOSVersion();
if (minVersion != null) {
return minVersion;
}
}
return config.getOs().getMinVersion();
}
private void putIfAbsent(NSDictionary dict, String key, String value) {
if (dict.objectForKey(key) == null) {
dict.put(key, value);
}
}
protected void customizeInfoPList(NSDictionary dict) {
if (isSimulatorArch(arch)) {
dict.put("CFBundleSupportedPlatforms", new NSArray(new NSString("iPhoneSimulator")));
} else {
dict.put("CFBundleSupportedPlatforms", new NSArray(new NSString("iPhoneOS")));
dict.put("DTPlatformVersion", sdk.getPlatformVersion());
dict.put("DTPlatformBuild", sdk.getPlatformBuild());
dict.put("DTSDKBuild", sdk.getBuild());
// Validation fails without DTXcode and DTXcodeBuild. Try to read
// them from the installed Xcode.
try {
File versionPListFile = new File(new File(ToolchainUtil.findXcodePath()).getParentFile(),
"version.plist");
NSDictionary versionPList = (NSDictionary) PropertyListParser.parse(versionPListFile);
File xcodeInfoPListFile = new File(new File(ToolchainUtil.findXcodePath()).getParentFile(),
"Info.plist");
NSDictionary xcodeInfoPList = (NSDictionary) PropertyListParser.parse(xcodeInfoPListFile);
NSString dtXcodeBuild = (NSString) versionPList.objectForKey("ProductBuildVersion");
if (dtXcodeBuild == null) {
throw new NoSuchElementException("No ProductBuildVersion in " + versionPListFile.getAbsolutePath());
}
NSString dtXcode = (NSString) xcodeInfoPList.objectForKey("DTXcode");
if (dtXcode == null) {
throw new NoSuchElementException("No DTXcode in " + xcodeInfoPListFile.getAbsolutePath());
}
putIfAbsent(dict, "DTXcode", dtXcode.toString());
putIfAbsent(dict, "DTXcodeBuild", dtXcodeBuild.toString());
} catch (Exception e) {
config.getLogger()
.warn("Failed to read DTXcodeBuild/DTXcode from current Xcode install. Will use fake values. (%s: %s)",
e.getClass().getName(), e.getMessage());
}
// Fake Xcode 6.1.1 values if the above fails.
putIfAbsent(dict, "DTXcode", "0611");
putIfAbsent(dict, "DTXcodeBuild", "6A2008a");
}
}
protected void createInfoPList(File dir) throws IOException {
NSDictionary dict = new NSDictionary();
if (config.getIosInfoPList() != null && config.getIosInfoPList().getDictionary() != null) {
NSDictionary infoPListDict = config.getIosInfoPList().getDictionary();
for (String key : infoPListDict.allKeys()) {
dict.put(key, infoPListDict.objectForKey(key));
}
} else {
dict.put("CFBundleVersion", "1.0");
dict.put("CFBundleExecutable", config.getExecutableName());
dict.put("CFBundleName", config.getExecutableName());
dict.put("CFBundleIdentifier", getBundleId());
dict.put("CFBundlePackageType", "APPL");
dict.put("LSRequiresIPhoneOS", true);
NSObject supportedDeviceFamilies = sdk.getDefaultProperty("SUPPORTED_DEVICE_FAMILIES");
if (supportedDeviceFamilies != null) {
// SUPPORTED_DEVICE_FAMILIES is either a NSString of comma
// separated numbers
// or an NSArray with NSStrings. UIDeviceFamily values should be
// NSNumbers.
NSArray families = null;
if (supportedDeviceFamilies instanceof NSString) {
NSString defFamilies = (NSString) supportedDeviceFamilies;
String[] parts = defFamilies.toString().split(",");
families = new NSArray(parts.length);
for (int i = 0; i < families.count(); i++) {
families.setValue(i, new NSNumber(parts[i].trim()));
}
} else {
NSArray defFamilies = (NSArray) supportedDeviceFamilies;
families = new NSArray(defFamilies.count());
for (int i = 0; i < families.count(); i++) {
families.setValue(i, new NSNumber(defFamilies.objectAtIndex(i).toString()));
}
}
dict.put("UIDeviceFamily", families);
}
dict.put("UISupportedInterfaceOrientations", new NSArray(
new NSString("UIInterfaceOrientationPortrait"),
new NSString("UIInterfaceOrientationLandscapeLeft"),
new NSString("UIInterfaceOrientationLandscapeRight"),
new NSString("UIInterfaceOrientationPortraitUpsideDown")
));
dict.put("UISupportedInterfaceOrientations~ipad", new NSArray(
new NSString("UIInterfaceOrientationPortrait"),
new NSString("UIInterfaceOrientationLandscapeLeft"),
new NSString("UIInterfaceOrientationLandscapeRight"),
new NSString("UIInterfaceOrientationPortraitUpsideDown")
));
dict.put("UIRequiredDeviceCapabilities", new NSArray(new NSString("armv7")));
}
dict.put("DTPlatformName", sdk.getPlatformName());
dict.put("DTSDKName", sdk.getCanonicalName());
for (File f : FileUtils.listFiles(partialPListDir, new String[] {"plist"}, false)) {
try {
NSDictionary d = (NSDictionary) PropertyListParser.parse(f);
dict.putAll(d);
} catch (Exception e) {
throw new CompilerException(e);
}
}
if (dict.objectForKey("MinimumOSVersion") == null) {
// This is required
dict.put("MinimumOSVersion", "6.0");
}
customizeInfoPList(dict);
/*
* Make sure CFBundleShortVersionString and CFBundleVersion are at the
* top of the Info.plist file to avoid the "Could not hardlink copy"
* problem when launching on the simulator. com.dd.plist maintains the
* insertion order of keys so we rebuild the dictionary here and make
* sure those two keys are inserted first. See #771.
*/
NSDictionary newDict = new NSDictionary();
if (dict.objectForKey("CFBundleShortVersionString") != null) {
newDict.put("CFBundleShortVersionString", dict.objectForKey("CFBundleShortVersionString"));
dict.remove("CFBundleShortVersionString");
}
if (dict.objectForKey("CFBundleVersion") != null) {
newDict.put("CFBundleVersion", dict.objectForKey("CFBundleVersion"));
dict.remove("CFBundleVersion");
}
for (String key : dict.allKeys()) {
newDict.put(key, dict.objectForKey(key));
}
File tmpInfoPlist = new File(config.getTmpDir(), "Info.plist");
PropertyListParser.saveAsBinary(newDict, tmpInfoPlist);
config.getLogger().info("Installing Info.plist to %s", dir);
FileUtils.copyFile(tmpInfoPlist, new File(dir, tmpInfoPlist.getName()));
}
public void init(Config config) {
super.init(config);
if (config.getArch() == null) {
arch = Arch.thumbv7;
} else {
if (!isSimulatorArch(config.getArch()) && !isDeviceArch(config.getArch())) {
throw new IllegalArgumentException("Arch '" + config.getArch()
+ "' is unsupported for iOS target");
}
arch = config.getArch();
}
if (isDeviceArch(arch)) {
if (!config.isSkipLinking() && !config.isIosSkipSigning()) {
signIdentity = config.getIosSignIdentity();
if (signIdentity == null) {
signIdentity = SigningIdentity.find(SigningIdentity.list(),
"/(?i)iPhone Developer|iOS Development/");
}
}
}
if (config.getIosInfoPList() != null) {
config.getIosInfoPList().parse(config.getProperties());
}
if (isDeviceArch(arch)) {
if (!config.isSkipLinking() &&!config.isIosSkipSigning()) {
provisioningProfile = config.getIosProvisioningProfile();
if (provisioningProfile == null) {
String bundleId = "*";
if (config.getIosInfoPList() != null && config.getIosInfoPList().getBundleIdentifier() != null) {
bundleId = config.getIosInfoPList().getBundleIdentifier();
}
provisioningProfile = ProvisioningProfile.find(ProvisioningProfile.list(), signIdentity, bundleId);
}
}
}
String sdkVersion = config.getIosSdkVersion();
List<SDK> sdks = getSDKs();
if (sdkVersion == null) {
if (sdks.isEmpty()) {
throw new IllegalArgumentException("No " + (isDeviceArch(arch) ? "device" : "simulator")
+ " SDKs installed");
}
Collections.sort(sdks);
this.sdk = sdks.get(sdks.size() - 1);
} else {
for (SDK sdk : sdks) {
if (sdk.getVersion().equals(sdkVersion)) {
this.sdk = sdk;
break;
}
}
if (sdk == null) {
throw new IllegalArgumentException("No SDK found matching version string " + sdkVersion);
}
}
entitlementsPList = config.getIosEntitlementsPList();
partialPListDir = new File(config.getTmpDir(), "partial-plists");
partialPListDir.mkdirs();
try {
FileUtils.cleanDirectory(partialPListDir);
} catch (IOException e) {
throw new CompilerException(e);
}
}
@Override
public OS getOs() {
return OS.ios;
}
@Override
public boolean canLaunchInPlace() {
return false;
}
/**
* Copies the dSYM and the executable to {@code ~/Library/Developer/Xcode/
* DerivedData/RoboVM/Build/Products/<appname>_<timestamp>/}.
*/
private void copyToIndexedDir(File dir, String executable, File dsymDir, File exePath) {
SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMddHHmmss");
final File indexedDir = new File(System.getProperty("user.home"),
"Library/Developer/Xcode/DerivedData/RoboVM/Build/Products/"
+ FilenameUtils.removeExtension(dir.getName()) + "_"
+ sdf.format(new Date()));
indexedDir.mkdirs();
File indexedDSymDir = new File(indexedDir, dsymDir.getName());
File indexedAppDir = new File(indexedDir, dir.getName());
indexedAppDir.mkdirs();
try {
// No need to copy the whole .app folder. Just the exe
// is enough to make symbolication happy.
FileUtils.copyFile(exePath, new File(indexedAppDir, executable));
} catch (IOException e) {
config.getLogger().error("Failed to copy %s to indexed dir %s: %s",
exePath.getAbsolutePath(),
indexedAppDir.getAbsolutePath(), e.getMessage());
}
try {
FileUtils.copyDirectory(dsymDir, indexedDSymDir);
} catch (IOException e) {
config.getLogger().error("Failed to copy %s to indexed dir %s: %s",
dsymDir.getAbsolutePath(),
indexedDir.getAbsolutePath(), e.getMessage());
}
// Now do some cleanup and delete all but the 3 most recent dirs
List<File> dirs = new ArrayList<>(Arrays.asList(indexedDir.getParentFile().listFiles((FileFilter)
new AndFileFilter(
new PrefixFileFilter(FilenameUtils.removeExtension(dir.getName())),
new RegexFileFilter(".*_\\d{14}")))));
Collections.sort(dirs, new Comparator<File>() {
public int compare(File o1, File o2) {
return Long.compare(o1.lastModified(), o2.lastModified());
}
});
if (dirs.size() > 3) {
for (File f : dirs.subList(0, dirs.size() - 3)) {
try {
FileUtils.deleteDirectory(f);
} catch (IOException e) {
config.getLogger().error("Failed to delete diretcory %s",
f.getAbsolutePath(), e.getMessage());
}
}
}
}
}