org.robovm.compiler.target.ios.IOSTarget Maven / Gradle / Ivy
The newest version!
/*
* Copyright (C) 2012 RoboVM AB
* Copyright (C) 2018 Daniel Thommes, NeverNull GmbH,
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see .
*/
package org.robovm.compiler.target.ios;
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.PropertyListFormatException;
import com.dd.plist.PropertyListParser;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.FilenameUtils;
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.apache.commons.lang3.tuple.Pair;
import org.robovm.compiler.CompilerException;
import org.robovm.compiler.config.*;
import org.robovm.compiler.log.Logger;
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.PList;
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 org.xml.sax.SAXException;
import javax.xml.parsers.ParserConfigurationException;
import java.io.File;
import java.io.FileFilter;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.OutputStream;
import java.math.BigInteger;
import java.nio.file.Files;
import java.nio.file.StandardCopyOption;
import java.security.MessageDigest;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.*;
/**
* @author niklas
*
*/
public class IOSTarget extends AbstractTarget {
final List excludedKeys = Arrays.asList(
"com.apple.developer.icloud-container-development-container-identifiers",
"com.apple.developer.icloud-container-environment",
"com.apple.developer.icloud-container-identifiers",
"com.apple.developer.icloud-services",
"com.apple.developer.restricted-resource-mode",
"com.apple.developer.ubiquity-container-identifiers",
"com.apple.developer.ubiquity-kvstore-identifier",
"inter-app-audio",
"com.apple.developer.homekit",
"com.apple.developer.healthkit",
"com.apple.developer.in-app-payments",
"com.apple.developer.associated-domains",
"com.apple.security.application-groups",
"com.apple.developer.maps",
"com.apple.developer.networking.vpn.api",
"com.apple.external-accessory.wireless-configuration"
);
public static final String TYPE = "ios";
private Arch arch;
private SDK sdk;
private File entitlementsPList;
private SigningIdentity signIdentity;
private ProvisioningProfile provisioningProfile;
@Deprecated
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) {
Environment env = arch.getEnv();
CpuArch cpuArch = arch.getCpuArch();
return env == Environment.Simulator &&
(cpuArch == CpuArch.x86_64 || cpuArch == CpuArch.arm64);
}
public static boolean isDeviceArch(Arch arch) {
Environment env = arch.getEnv();
CpuArch cpuArch = arch.getCpuArch();
return env == Environment.Native && (cpuArch == CpuArch.thumbv7 || cpuArch == CpuArch.arm64);
}
/**
* Late initialization as we cannot assume that the tmp dir is available at configuration creation
*
* @return
*/
private File getPartialPListDir() {
if (!partialPListDir.exists()) {
partialPListDir.mkdirs();
}
return partialPListDir;
}
public List 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 {
return new SimLauncherProcess(config.getLogger(), getAppDir(), getBundleId(), (IOSSimulatorLaunchParameters) launchParameters);
}
private Launcher createIOSDevLauncher(LaunchParameters launchParameters)
throws IOException {
IOSDeviceLaunchParameters deviceLaunchParameters = (IOSDeviceLaunchParameters) launchParameters;
String deviceUdid = deviceLaunchParameters.getDeviceId();
int forwardPort = deviceLaunchParameters.getForwardPort();
// TODO: FIXME: proxy AppLauncherCallback here: device to be captured as it is being used in junit client
// its a subject for future rework
AppLauncherCallback callback = deviceLaunchParameters.getAppPathCallback() != null ? new AppLauncherCallback() {
final AppLauncherCallback delegate = deviceLaunchParameters.getAppPathCallback();
@Override
public void setAppLaunchInfo(AppLauncherInfo info) {
device = info.getDevice();
delegate.setAppLaunchInfo(info);
}
@Override
public byte[] filterOutput(byte[] data) {
return delegate.filterOutput(data);
}
} : null;
OutputStream out = null;
if (launchParameters.getStdoutFifo() != null) {
out = new OpenOnWriteFileOutputStream(launchParameters.getStdoutFifo());
} else {
out = System.out;
}
Map env = launchParameters.getEnvironment();
if (env == null) {
env = new HashMap<>();
}
//Fix for #71, see http://stackoverflow.com/questions/37800790/hide-strange-unwanted-xcode-8-logs
env.put("OS_ACTIVITY_DT_MODE", "");
AppLauncher launcher = new AppLauncher(deviceUdid, getAppDir()) {
protected void log(String s, Object... args) {
config.getLogger().info(s, args);
}
}
.stdout(out)
.closeOutOnExit(true)
.args(launchParameters.getArguments(true).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 ccArgs,
List objectFiles, List 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('.')));
int minMajorSupportedVersion = Integer.parseInt(config.getOs().getMinVersion().substring(0, config.getOs().getMinVersion().indexOf('.')));
if (majorVersionNumber < minMajorSupportedVersion) {
throw new CompilerException("MinimumOSVersion of " + minVersion + " is not supported. "
+ "The minimum version for this platform is " + config.getOs().getMinVersion());
}
} catch (NumberFormatException e) {
throw new CompilerException("Failed to get major version number from "
+ "MinimumOSVersion string '" + minVersion + "'");
}
ccArgs.add("--target=" + config.getClangTriple(getMinimumOSVersion()));
if (isDeviceArch(arch)) {
if (config.isEnableBitcode()) {
// tells clang to keep bitcode while linking
ccArgs.add("-fembed-bitcode");
}
}
ccArgs.add("-isysroot");
ccArgs.add(sdk.getRoot().getAbsolutePath());
// add runtime path to swift libs first to support swift-5 libs location
if (config.hasSwiftSupport()) {
libArgs.add("-Xlinker");
libArgs.add("-rpath");
libArgs.add("-Xlinker");
libArgs.add("/usr/lib/swift");
}
// 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");
if (!isDeviceArch(arch)) {
// add simulated entitlement to allow Security framework to work on simulator
File simEntitlement = createSimulatedEntitlementsPList(getBundleId());
ccArgs.add("-Xlinker");
ccArgs.add("-sectcreate");
ccArgs.add("-Xlinker");
ccArgs.add("__TEXT");
ccArgs.add("-Xlinker");
ccArgs.add("__entitlements");
ccArgs.add("-Xlinker");
ccArgs.add(simEntitlement.getAbsolutePath());
}
super.doBuild(outFile, ccArgs, objectFiles, libArgs);
}
protected void prepareInstall(File installDir) throws IOException {
createInfoPList(installDir);
generateDsym(getDsymDir(installDir), new File(installDir, getExecutable()));
if (isDeviceArch(arch)) {
// strip local symbols
strip(installDir, getExecutable());
// remove bitcode to minimize binary size if not required
if (!config.isEnableBitcode()) {
config.getLogger().info("Striping bitcode from binary: %s", new File(installDir, getExecutable()));
stripBitcode(new File(installDir, getExecutable()));
}
if (config.isIosSkipSigning()) {
config.getLogger().warn("Skipping code signing. The resulting app will "
+ "be unsigned and will not run on unjailbroken devices");
codesignApp(SigningIdentity.ADHOC, getOrCreateEntitlementsPList(false, getBundleId()), installDir);
} else {
String appIdPrefix = provisioningProfile.getAppIdPrefix();
// Copy the provisioning profile
copyProvisioningProfile(provisioningProfile, installDir);
boolean getTaskAllow = provisioningProfile.getType() == Type.Development;
signFrameworks(signIdentity, installDir);
// app extensions
provisionAppExtensions(config.getAppExtensions(), signIdentity, installDir);
signAppExtensions(signIdentity, installDir, appIdPrefix, getTaskAllow);
// watch app
if (config.getWatchKitApp() != null) {
provisionWatchApp(signIdentity, installDir);
signWatchApp(signIdentity, installDir, appIdPrefix, getTaskAllow);
}
// app
codesignApp(signIdentity, getOrCreateEntitlementsPList(getTaskAllow, getBundleId()), installDir);
}
}
}
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"));
}
@Override
public void prepareLaunch() throws IOException {
prepareLaunch(getAppDir());
}
protected void prepareLaunch(File appDir) throws IOException {
super.doInstall(appDir, getExecutable(), appDir);
createInfoPList(appDir);
generateDsym(getDsymDir(appDir), new File(appDir, getExecutable()));
copyToIndexedDir(appDir, getExecutable(), getDsymDir(appDir), new File(appDir, getExecutable()));
// strip symbols to reduce application size, all debugger symbols converted into globals
strip(appDir, getExecutable());
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");
codesignApp(SigningIdentity.ADHOC, getOrCreateEntitlementsPList(true, getBundleId()), appDir);
} else {
String appIdPrefix = provisioningProfile.getAppIdPrefix();
copyProvisioningProfile(provisioningProfile, appDir);
boolean getTaskAllow = provisioningProfile.getType() == Type.Development;
signFrameworks(signIdentity, appDir);
// app extensions
provisionAppExtensions(config.getAppExtensions(), signIdentity, appDir);
signAppExtensions(signIdentity, appDir, appIdPrefix, getTaskAllow);
// watch app
if (config.getWatchKitApp() != null) {
provisionWatchApp(signIdentity, appDir);
signWatchApp(signIdentity, appDir, appIdPrefix, getTaskAllow);
}
// sign the app
codesignApp(signIdentity, getOrCreateEntitlementsPList(getTaskAllow, getBundleId()), appDir);
}
} else { // simulator
if (sdk.getVersionCode() >= 0x0B0300) {
// code signing of frameworks and app extensions are required since iOS 11.3
signFrameworks(SigningIdentity.ADHOC, appDir);
signAppExtensions(SigningIdentity.ADHOC, appDir, null, true);
if (config.getWatchKitApp() != null)
signWatchApp(SigningIdentity.ADHOC, appDir, null, true);
}
// sign the app
// NB: it is not required as app can run without it but Xcode does this
codesignApp(SigningIdentity.ADHOC, createEntitlementsPList(true), appDir);
}
}
private void signFrameworks(SigningIdentity identity, File appDir) 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(identity, swiftLib);
}
}
// sign embedded frameworks
for (File framework : frameworksDir.listFiles()) {
if (framework.isDirectory() && framework.getName().endsWith(".framework")) {
codesignCustomFramework(identity, framework);
}
}
}
}
private void signAppExtensions(SigningIdentity identity, File appDir, String appIdPrefix, boolean getTaskAllow) throws IOException {
// sign dynamic frameworks first
File extensionsDir = new File(appDir, "PlugIns");
if (extensionsDir.exists() && extensionsDir.isDirectory()) {
// sign embedded app-extensions
for (File extension : extensionsDir.listFiles()) {
if (extension.isDirectory() && extension.getName().endsWith(".appex"))
signSingleAppExtension(identity, extension, appIdPrefix, getTaskAllow);
}
}
}
private void signSingleAppExtension(SigningIdentity identity, File extDir, String appIdPrefix, boolean getTaskAllow) throws IOException {
String appExBundleId = null;
// NB: appIdPrefix is null for simulator build
if (appIdPrefix != null) {
// read extension bundle id from plist (id was generated during copy phase up to robovm.xml config)
File infoPlistFile = new File(extDir, "Info.plist");
try {
NSDictionary infoPlist = (NSDictionary) PropertyListParser.parse(infoPlistFile);
appExBundleId = infoPlist.get("CFBundleIdentifier").toString();
} catch (PropertyListFormatException | SAXException | ParserConfigurationException | ParseException e) {
throw new IOException(e);
}
appExBundleId = appIdPrefix + "." + appExBundleId;
}
// create entitlements
File entitlements = createEntitlementForAppEx(getTaskAllow, appExBundleId);
// now sign
codesignAppExtension(identity, entitlements, extDir);
}
private void signWatchApp(SigningIdentity identity, File appDir, String appIdPrefix, boolean getTaskAllow) throws IOException {
// sign watch app
WatchKitApp waConfig = config.getWatchKitApp();
String waName = waConfig.getWatchAppName();
File waDir = new File(appDir, "Watch/" + waName);
if (!waDir.exists() || !waDir.isDirectory())
throw new IllegalStateException("Error while signing WatchApp, watch directory doesn't exist: " + waDir.getAbsolutePath());
// sign all extension first
signAppExtensions(identity, waDir, appIdPrefix, getTaskAllow);
// sign watch app (same way as app extension)
signSingleAppExtension(identity, waDir, appIdPrefix, getTaskAllow);
}
/**
* generates simple entitlement plist which is required for AppEx during submit to app store
* @param bundleId -- might be null for simulator
*/
private File createEntitlementForAppEx(boolean getTaskAllow, String bundleId) throws IOException {
try {
File destFile = new File(config.getTmpDir(), "AppExtEntitlements.plist");
NSDictionary dict = new NSDictionary();
if (bundleId != null)
dict.put("application-identifier", bundleId);
// xcode uses prefix for simulators entitlements
String prefix = isDeviceArch(arch) ? "" : "com.apple.security.";
dict.put(prefix + "get-task-allow", getTaskAllow);
PropertyListParser.saveAsXML(dict, destFile);
return destFile;
} catch (IOException e) {
throw e;
} catch (Exception e) {
throw new RuntimeException(e);
}
}
/**
* finds and copies provisioning profiles for AppExtensions
*
* @param extensions extension information from robovm.xml
* @param signIdentity ideantity used to sign application
* @param installDir of application
*/
private void provisionAppExtensions(List extensions, SigningIdentity signIdentity, File installDir) throws IOException {
File pluginsDir = new File(installDir, "PlugIns");
if (pluginsDir.exists() && pluginsDir.isDirectory()) {
Map extensionsMap = new HashMap<>();
extensions.forEach(e -> extensionsMap.put(e.getNameWithExt(".appex"), e));
// move through all extensions in directory
for (File extPath : pluginsDir.listFiles()) {
if (!extPath.isDirectory() || !extPath.getName().endsWith(".appex")) {
config.getLogger().info("Skipping not expected file/dir '%s' in PlugIns folder: %s",
extPath.getName(), pluginsDir.getAbsolutePath());
continue;
}
String name = extPath.getName();
AppExtension extension = extensionsMap.get(name);
if (extension == null) {
extension = AppExtension.DEFAULT_RULE;
config.getLogger().info("Using default signing rules for app extension " + name);
}
provisionSingleAppExtension(extension, signIdentity, extPath);
}
}
}
/**
* provision single app extension:
* - finds and copies profile for it
*/
private void provisionSingleAppExtension(AppExtension extension, SigningIdentity signIdentity, File extPath) throws IOException {
if (extension.skipSigning())
return;
// read extension bundle id from plist (id was generated during copy phase up to robovm.xml config)
File infoPlistFile = new File(extPath, "Info.plist");
String appExBundleId;
try {
NSDictionary infoPlist = (NSDictionary) PropertyListParser.parse(infoPlistFile);
appExBundleId = infoPlist.get("CFBundleIdentifier").toString();
} catch (PropertyListFormatException | SAXException | ParserConfigurationException | ParseException e) {
throw new IOException(e);
}
ProvisioningProfile appExtProfile;
String profileName = extension.getProfile();
if (profileName != null) {
// profile is specified in robovm.xml
appExtProfile = ProvisioningProfile.find(ProvisioningProfile.list(), profileName);
} else {
// find profile that matches app ext bundle id
appExtProfile = ProvisioningProfile.find(ProvisioningProfile.list(), signIdentity, appExBundleId);
}
if (appExtProfile != null) {
config.getLogger().info("Copying %s provisioning profile for : %s (%s)",
appExtProfile.getType(),
appExtProfile.getName(),
appExtProfile.getEntitlements().objectForKey("application-identifier"));
FileUtils.copyFile(appExtProfile.getFile(), new File(extPath, "embedded.mobileprovision"));
} else {
throw new RuntimeException("Failed to locate provisioning profile for " + extPath.getName());
}
}
/**
* provision WatchApplication and its extensions
* @param signIdentity used for signing the application
* @param installDir of application
*/
private void provisionWatchApp(SigningIdentity signIdentity, File installDir) throws IOException {
WatchKitApp waConfig = config.getWatchKitApp();
// provision app itself
File waDir = new File(installDir, "Watch/" + waConfig.getWatchAppName());
provisionSingleAppExtension(waConfig.getApp(), signIdentity, waDir);
// provision plugins
provisionAppExtensions(waConfig.getExtensions(), signIdentity, new File(waDir, "/PlugIns" ));
}
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 codesignAppExtension(SigningIdentity identity, File entitlementsPList, File extensionDir) throws IOException {
config.getLogger().info("Code signing app-extension '%s' using identity '%s' with fingerprint %s", extensionDir.getName(), identity.getName(),
identity.getFingerprint());
codesign(identity, entitlementsPList, false, false, true, extensionDir);
}
private void codesign(SigningIdentity identity, File entitlementsPList, boolean preserveMetadata,
boolean verbose, boolean allocate, File target) throws IOException {
// just a wrapper that forces "--generate-entitlement-der" for all kind of signing
boolean generateDerEntitlement = true;
codesign(identity, entitlementsPList, preserveMetadata, generateDerEntitlement, verbose, allocate, target);
}
private void codesign(SigningIdentity identity, File entitlementsPList, boolean preserveMetadata,
boolean generateDerEntitlement, boolean verbose, boolean allocate, File target) throws IOException {
List
© 2015 - 2025 Weber Informatics LLC | Privacy Policy