diff --git a/build.gradle b/build.gradle index 20266602..8889fd2a 100644 --- a/build.gradle +++ b/build.gradle @@ -2,7 +2,7 @@ allprojects { apply plugin: 'java-gradle-plugin' apply plugin: 'groovy' - def versionNumber = "0.15.2" + def versionNumber = "0.16.0.a603076-SNAPSHOT" if (project.hasProperty("versionNumber")) { versionNumber = project.versionNumber @@ -55,6 +55,27 @@ allprojects { substitute module('ch.qos.logback:logback-classic') with module('org.slf4j:slf4j-api:1.7.+') } } + + // Write the plugin's classpath to a file to share with the tests + task createClasspathManifest { + def outputDir = file("$buildDir/$name") + + inputs.files sourceSets.main.runtimeClasspath + outputs.dir outputDir + + doLast { + outputDir.mkdirs() + file("$outputDir/plugin-classpath.txt").text = project.files(project.rootProject.subprojects + .collect { it.sourceSets.main.runtimeClasspath } + .toList()) + .files + .join("\n") + } + } + + dependencies { + testRuntime files(createClasspathManifest) + } } diff --git a/libxcode/src/main/groovy/org/openbakery/CommandRunner.groovy b/libxcode/src/main/groovy/org/openbakery/CommandRunner.groovy index d845bacc..b52aeeff 100644 --- a/libxcode/src/main/groovy/org/openbakery/CommandRunner.groovy +++ b/libxcode/src/main/groovy/org/openbakery/CommandRunner.groovy @@ -38,7 +38,6 @@ class CommandRunner { } - void setOutputFile(File outputFile) { if (outputFile.exists()) { @@ -172,20 +171,35 @@ class CommandRunner { } String runWithResult(String... commandList) { - return runWithResult(Arrays.asList(commandList)); + return runWithResult(Arrays.asList(commandList)) + } + + String runWithResult(Map environmentValues, + String... commandList) { + return runWithResult(defaultBaseDirectory, + commandList.toList(), + environmentValues, + null) } String runWithResult(List commandList) { return runWithResult(defaultBaseDirectory, commandList) } - String runWithResult(String directory, List commandList) { - return runWithResult(directory, commandList, null, null) + String runWithResult(String directory, + List commandList) { + return runWithResult(directory, + commandList, + null, + null) } - String runWithResult(String directory, List commandList, Map environment, OutputAppender outputAppender) { + String runWithResult(String directory, + List commandList, + Map environment, + OutputAppender outputAppender) { commandOutputBuffer = new ArrayList<>(); - run(directory, commandList, environment, outputAppender); + run(directory, commandList, environment, outputAppender) String result = commandOutputBuffer.join("\n") commandOutputBuffer = null; return result diff --git a/libxcode/src/main/groovy/org/openbakery/bundle/ApplicationBundle.groovy b/libxcode/src/main/groovy/org/openbakery/bundle/ApplicationBundle.groovy index 0804c675..2ece258e 100644 --- a/libxcode/src/main/groovy/org/openbakery/bundle/ApplicationBundle.groovy +++ b/libxcode/src/main/groovy/org/openbakery/bundle/ApplicationBundle.groovy @@ -19,7 +19,7 @@ public class ApplicationBundle { addPluginsToAppBundle(applicationPath, bundles) - if (isDeviceBuildOf(Type.iOS)) { + if (isDeviceBuildOf(Type.iOS) || isDeviceBuildOf(Type.tvOS)) { addWatchToAppBundle(applicationPath, bundles) } bundles.add(applicationPath) @@ -28,9 +28,9 @@ public class ApplicationBundle { private void addPluginsToAppBundle(File appBundle, ArrayList bundles) { File plugins - if (isDeviceBuildOf(Type.iOS)) { + if (isDeviceBuildOf(Type.iOS) || isDeviceBuildOf(Type.tvOS)) { plugins = new File(appBundle, "PlugIns") - } else if (this.type == Type.macOS) { + } else if (this.type == Type.macOS) { plugins = new File(appBundle, "Contents/PlugIns") } else { return @@ -43,7 +43,7 @@ public class ApplicationBundle { if (pluginBundle.name.endsWith(".framework")) { // Frameworks have to be signed with this path bundles.add(new File(pluginBundle, "/Versions/Current")) - } else if (pluginBundle.name.endsWith(".appex")) { + } else if (pluginBundle.name.endsWith(".appex")) { for (File appexBundle : pluginBundle.listFiles()) { if (appexBundle.isDirectory() && appexBundle.name.endsWith(".app")) { diff --git a/libxcode/src/main/groovy/org/openbakery/codesign/Codesign.groovy b/libxcode/src/main/groovy/org/openbakery/codesign/Codesign.groovy index c53163af..c1ccda77 100644 --- a/libxcode/src/main/groovy/org/openbakery/codesign/Codesign.groovy +++ b/libxcode/src/main/groovy/org/openbakery/codesign/Codesign.groovy @@ -104,7 +104,7 @@ class Codesign { private void codeSignFrameworks(File bundle) { File frameworksDirectory - if (codesignParameters.type == Type.iOS) { + if (codesignParameters.type == Type.iOS || codesignParameters.type == Type.tvOS) { frameworksDirectory = new File(bundle, "Frameworks") } else { frameworksDirectory = new File(bundle, "Contents/Frameworks") @@ -130,7 +130,7 @@ class Codesign { if (codesignParameters.signingIdentity == null) { performCodesignWithoutIdentity(bundle) } else { - performCodesignWithIdentity(bundle,entitlements) + performCodesignWithIdentity(bundle, entitlements) } } @@ -178,7 +178,7 @@ class Codesign { private String getIdentifierForBundle(File bundle) { File infoPlist - if (codesignParameters.type == Type.iOS) { + if (codesignParameters.type == Type.iOS || codesignParameters.type == Type.tvOS) { infoPlist = new File(bundle, "Info.plist"); } else { infoPlist = new File(bundle, "Contents/Info.plist") @@ -190,7 +190,8 @@ class Codesign { ProvisioningProfileReader createProvisioningProfileReader(String bundleIdentifier, File provisionFile) { if (provisionFile == null) { - if (codesignParameters.type == Type.iOS) { + if (codesignParameters.type == Type.iOS + || codesignParameters.type == Type.tvOS) { throw new IllegalStateException("No provisioning profile found for bundle identifier: " + bundleIdentifier) } // on OS X this is valid @@ -235,7 +236,7 @@ class Codesign { File getXcentFile(File bundle) { def fileList = bundle.list( - [accept: { d, f -> f ==~ /.*xcent/ }] as FilenameFilter + [accept: { d, f -> f ==~ /.*xcent/ }] as FilenameFilter ) if (fileList == null || fileList.toList().isEmpty()) { return null @@ -254,7 +255,7 @@ class Codesign { applicationPrefix = applicationPrefix + "." logger.info("using application prefix: {}", applicationPrefix) - List keychainAccessGroups = configuration.getStringArray("keychain-access-groups") + List keychainAccessGroups = configuration.getStringArray("keychain-access-groups") logger.info("keychain-access-group from configuration: {}", result) keychainAccessGroups.each { item -> diff --git a/libxcode/src/main/groovy/org/openbakery/codesign/ProvisioningProfileReader.groovy b/libxcode/src/main/groovy/org/openbakery/codesign/ProvisioningProfileReader.groovy index ead64e05..121b6cc8 100644 --- a/libxcode/src/main/groovy/org/openbakery/codesign/ProvisioningProfileReader.groovy +++ b/libxcode/src/main/groovy/org/openbakery/codesign/ProvisioningProfileReader.groovy @@ -69,7 +69,10 @@ class ProvisioningProfileReader { checkExpired() } - static File getProvisionFileForIdentifier(String bundleIdentifier, List mobileProvisionFiles, CommandRunner commandRunner, PlistHelper plistHelper) { + static File getProvisionFileForIdentifier(String bundleIdentifier, + List mobileProvisionFiles, + CommandRunner commandRunner, + PlistHelper plistHelper) { def provisionFileMap = [:] for (File mobileProvisionFile : mobileProvisionFiles) { @@ -99,8 +102,8 @@ class ProvisioningProfileReader { } } - logger.info("No provisioning profile found for bundle identifier {}", bundleIdentifier) - logger.info("Available bundle identifier are {}" + provisionFileMap.keySet()) + logger.warn("No provisioning profile found for bundle identifier {}", bundleIdentifier) + logger.warn("Available bundle identifier are {}" + provisionFileMap.keySet()) return null } @@ -142,7 +145,7 @@ class ProvisioningProfileReader { Date expireDate = config.getProperty("ExpirationDate") if (expireDate.before(new Date())) { DateFormat formatter = DateFormat.getDateTimeInstance(DateFormat.LONG, DateFormat.LONG, Locale.getDefault()) - throw new IllegalArgumentException("The Provisioning Profile has expired on " + formatter.format(expireDate) ) + throw new IllegalArgumentException("The Provisioning Profile has expired on " + formatter.format(expireDate)) } } @@ -159,6 +162,14 @@ class ProvisioningProfileReader { return config.getString("TeamIdentifier") } + String getTeamName() { + return config.getString("TeamName") + } + + String getName() { + return config.getString("Name") + } + File getPlistFromProvisioningProfile() { if (provisioningPlist == null) { // unpack provisioning profile to plain plist @@ -170,12 +181,12 @@ class ProvisioningProfileReader { try { commandRunner.run(["security", - "cms", - "-D", - "-i", - provisioningProfile.getCanonicalPath(), - "-o", - provisioningPlist.absolutePath + "cms", + "-D", + "-i", + provisioningProfile.getCanonicalPath(), + "-o", + provisioningPlist.absolutePath ]) } catch (CommandRunnerException ex) { if (!provisioningPlist.exists()) { @@ -204,15 +215,16 @@ class ProvisioningProfileReader { } /* xcent is the archive entitlements */ + void extractEntitlements(File entitlementFile, String bundleIdentifier, List keychainAccessGroups, Configuration configuration) { logger.info("extractEntitlements for " + bundleIdentifier) File plistFromProvisioningProfile = getPlistFromProvisioningProfile() String entitlements = commandRunner.runWithResult([ - "/usr/libexec/PlistBuddy", - "-x", - plistFromProvisioningProfile.absolutePath, - "-c", - "Print Entitlements"]) + "/usr/libexec/PlistBuddy", + "-x", + plistFromProvisioningProfile.absolutePath, + "-c", + "Print Entitlements"]) if (StringUtils.isEmpty(entitlements)) { logger.debug("No entitlements found in {}", plistFromProvisioningProfile) @@ -228,7 +240,7 @@ class ProvisioningProfileReader { String bundleIdentifierPrefix = "" if (applicationIdentifier != null) { String[] tokens = applicationIdentifier.split("\\.") - for (int i=1; i if (value instanceof String) { value = this.replaceVariables(value) } else if (value instanceof List) { - value = this.replaceValuesInList((List)value) + value = this.replaceValuesInList((List) value) } @@ -308,7 +319,7 @@ class ProvisioningProfileReader { def result = [] for (Object item : list) { if (item instanceof String) { - result << replaceVariables((String)item) + result << replaceVariables((String) item) } else { result << item } @@ -330,7 +341,7 @@ class ProvisioningProfileReader { } Configuration entitlements = new ConfigurationFromPlist(entitlementFile) - SetreplaceKeys = configuration.getReplaceEntitlementsKeys() + Set replaceKeys = configuration.getReplaceEntitlementsKeys() for (String key in configuration.getKeys()) { Object value = configuration.get(key) //plistHelper.getValueFromPlist(xcent, key) @@ -366,7 +377,7 @@ class ProvisioningProfileReader { } else { if (currentValue.toString().endsWith('*')) { - plistHelper.setValueForPlist(entitlementFile, value, prefix + "." + bundleIdentifier) + plistHelper.setValueForPlist(entitlementFile, value, prefix + "." + bundleIdentifier) } } } diff --git a/libxcode/src/main/groovy/org/openbakery/codesign/Security.groovy b/libxcode/src/main/groovy/org/openbakery/codesign/Security.groovy index 53034462..401ef292 100644 --- a/libxcode/src/main/groovy/org/openbakery/codesign/Security.groovy +++ b/libxcode/src/main/groovy/org/openbakery/codesign/Security.groovy @@ -1,6 +1,5 @@ package org.openbakery.codesign -import org.apache.commons.io.FileUtils import org.apache.commons.io.FilenameUtils import org.apache.commons.lang.StringUtils import org.openbakery.CommandRunner @@ -9,7 +8,6 @@ import org.slf4j.Logger import org.slf4j.LoggerFactory import java.security.cert.CertificateException -import java.text.ParseException class Security { private static Logger logger = LoggerFactory.getLogger(Security.class) @@ -60,6 +58,7 @@ class Security { for (File keychain in keychainList) { commandList.add(keychain.absolutePath) } + commandRunner.run(commandList) } diff --git a/libxcode/src/main/groovy/org/openbakery/simulators/SimulatorControl.groovy b/libxcode/src/main/groovy/org/openbakery/simulators/SimulatorControl.groovy index eb8bd698..67f0d6ed 100644 --- a/libxcode/src/main/groovy/org/openbakery/simulators/SimulatorControl.groovy +++ b/libxcode/src/main/groovy/org/openbakery/simulators/SimulatorControl.groovy @@ -1,6 +1,7 @@ package org.openbakery.simulators -import org.openbakery.* +import org.openbakery.CommandRunner +import org.openbakery.CommandRunnerException import org.openbakery.xcode.Destination import org.openbakery.xcode.Type import org.openbakery.xcode.Version @@ -11,7 +12,6 @@ import org.slf4j.LoggerFactory class SimulatorControl { - enum Section { DEVICE_TYPE("== Device Types =="), RUNTIMES("== Runtimes =="), @@ -19,6 +19,7 @@ class SimulatorControl { DEVICE_PAIRS("== Device Pairs ==") private final String identifier + Section(String identifier) { this.identifier = identifier } @@ -48,9 +49,6 @@ class SimulatorControl { ArrayList devicePairs - - - public SimulatorControl(CommandRunner commandRunner, Xcode xcode) { this.commandRunner = commandRunner this.xcode = xcode @@ -60,7 +58,7 @@ class SimulatorControl { runtimes = new ArrayList<>() devices = new HashMap<>() deviceTypes = new ArrayList<>() - identifierToDevice = new HashMap<>() + identifierToDevice = new HashMap<>() devicePairs = new ArrayList<>() @@ -103,7 +101,7 @@ class SimulatorControl { if (simulatorDevices != null) { SimulatorDevice device = new SimulatorDevice(line) simulatorDevices.add(device) - identifierToDevice[device.identifier]=device + identifierToDevice[device.identifier] = device } break @@ -130,13 +128,13 @@ class SimulatorControl { } SimulatorDevice parseIdentifierFromDevicePairs(String line) { - def tokenizer = new StringTokenizer(line, "()"); + def tokenizer = new StringTokenizer(line, "()") if (tokenizer.hasMoreTokens()) { // ignore first token tokenizer.nextToken() } if (tokenizer.hasMoreTokens()) { - def identifier = tokenizer.nextToken().trim() + def identifier = tokenizer.nextToken().trim() return getDeviceWithIdentifier(identifier) } return null @@ -153,7 +151,6 @@ class SimulatorControl { return null } - public void waitForDevice(SimulatorDevice device, int timeoutMS = 10000) { def start = System.currentTimeMillis() while ((System.currentTimeMillis() - start) < timeoutMS) { @@ -191,7 +188,7 @@ class SimulatorControl { for (SimulatorRuntime runtime in getRuntimes()) { if (runtime.type == type) { - result.add(runtime); + result.add(runtime) } } Collections.sort(result, new SimulatorRuntimeComparator()) @@ -207,7 +204,6 @@ class SimulatorControl { } - SimulatorDevice getDevice(SimulatorRuntime simulatorRuntime, String name) { for (SimulatorDevice device in getDevices(simulatorRuntime)) { if (device.name.equalsIgnoreCase(name)) { @@ -218,26 +214,19 @@ class SimulatorControl { } - SimulatorRuntime getRuntime(Destination destination) { - for (SimulatorRuntime runtime in getRuntimes()) { - if (runtime.type == Type.iOS && runtime.version.equals(new Version(destination.os))) { - return runtime; - } - } - return null; + Optional getRuntime(Destination destination) { + return Optional.ofNullable(getRuntimes() + .findAll { it.type == destination.targetType } + .findAll { it.version?.equals(new Version(destination.os)) } + .find()) } - SimulatorDevice getDevice(Destination destination) { - SimulatorRuntime runtime = getRuntime(destination); - if (runtime != null) { - - for (SimulatorDevice device in getDevices(runtime)) { - if (device.name.equalsIgnoreCase(destination.name)) { - return device - } - } + Optional getDevice(final Destination destination) { + return getRuntime(destination) + .map { runtime -> + getDevices(runtime) + .find { device -> device.name.equalsIgnoreCase(destination.name) } } - return null } SimulatorDevice getDeviceWithIdentifier(String identifier) { @@ -251,7 +240,7 @@ class SimulatorControl { return null } - List getDevices(SimulatorRuntime runtime) { + List getDevices(SimulatorRuntime runtime) { return getDevices().get(runtime) } @@ -280,15 +269,13 @@ class SimulatorControl { String simctl(String... commands) { - ArrayListparameters = new ArrayList<>() + ArrayList parameters = new ArrayList<>() parameters.add(xcode.getSimctl()) parameters.addAll(commands) return commandRunner.runWithResult(parameters) } - - void deleteAll() { for (Map.Entry> entry : getDevices().entrySet()) { @@ -369,6 +356,7 @@ class SimulatorControl { } catch (CommandRunnerException ex) { // ignore, this exception means that no simulator was running } + try { commandRunner.run("killall", "Simulator") // for xcode 7 } catch (CommandRunnerException ex) { diff --git a/libxcode/src/main/groovy/org/openbakery/simulators/SimulatorRuntime.groovy b/libxcode/src/main/groovy/org/openbakery/simulators/SimulatorRuntime.groovy index a2cf1636..19b82116 100644 --- a/libxcode/src/main/groovy/org/openbakery/simulators/SimulatorRuntime.groovy +++ b/libxcode/src/main/groovy/org/openbakery/simulators/SimulatorRuntime.groovy @@ -57,7 +57,11 @@ class SimulatorRuntime { } - @Override + Type getType() { + return type + } + + @Override public String toString() { return "SimulatorRuntime{" + "name='" + name + '\'' + diff --git a/libxcode/src/main/groovy/org/openbakery/util/PathHelper.groovy b/libxcode/src/main/groovy/org/openbakery/util/PathHelper.groovy new file mode 100644 index 00000000..b7a96265 --- /dev/null +++ b/libxcode/src/main/groovy/org/openbakery/util/PathHelper.groovy @@ -0,0 +1,99 @@ +package org.openbakery.util + +import groovy.transform.CompileStatic +import org.gradle.api.Project +import org.openbakery.xcode.Type + +@CompileStatic +class PathHelper { + public static final String APPLE_TV_OS = "appletvos" + public static final String APPLE_TV_SIMULATOR = "appletvsimulator" + public static final String IPHONE_SIMULATOR = "iphonesimulator" + public static final String IPHONE_OS = "iphoneos" + public static final String FOLDER_ARCHIVE = "archive" + public static final String FOLDER_PACKAGE = "package" + public static final String GENERATED_XCARCHIVE_FILE_NAME = "archive.xcconfig" + public static final String EXTENSION_XCARCHIVE = ".xcarchive" + + static File resolvePath(Type type, + boolean simulator, + File symRoot, + String configuration) { + File result + switch (type) { + case Type.iOS: + result = resolveIosSymRoot(simulator, + symRoot, + configuration) + break + + case Type.tvOS: + result = resolveAppleTvSymRoot(simulator, + symRoot, + configuration) + break + + case Type.macOS: + result = resolveMacOsSymRoot(symRoot, + configuration) + break + + default: + throw new IllegalStateException("WatchOs not implemeted") + break + + } + + return result + } + + static File resolveAppleTvSymRoot(boolean simulator, + File symRoot, + String configuration) { + return resolveSymRoot(symRoot, + configuration, + simulator ? APPLE_TV_SIMULATOR : APPLE_TV_OS) + } + + static File resolveIosSymRoot(boolean simulator, + File symRoot, + String configuration) { + return resolveSymRoot(symRoot, + configuration, + simulator ? IPHONE_SIMULATOR : IPHONE_OS) + } + + static File resolveMacOsSymRoot(File symRoot, + String configuration) { + return new File(symRoot, + configuration) + } + + private static File resolveSymRoot(File symRoot, + String configuration, + String destination) { + return new File(symRoot, + "${configuration}-${destination}") + } + + static File resolveArchiveFolder(Project project) { + return new File(project.getBuildDir(), FOLDER_ARCHIVE) + } + + static File resolveArchiveFile(Project project, + String scheme) { + return new File(resolveArchiveFolder(project), scheme + EXTENSION_XCARCHIVE) + } + + static File resolveArchivingLogFile(Project project) { + return new File(resolveArchiveFolder(project), "xcodebuild-archive-output.txt") + } + + static File resolveXcConfigFile(Project project) { + return new File(resolveArchiveFolder(project), GENERATED_XCARCHIVE_FILE_NAME) + } + + static File resolvePackageFolder(Project project) { + return new File(project.getBuildDir(), FOLDER_PACKAGE) + } +} diff --git a/libxcode/src/main/groovy/org/openbakery/util/PlistHelper.groovy b/libxcode/src/main/groovy/org/openbakery/util/PlistHelper.groovy index 48a689e0..256e23d1 100644 --- a/libxcode/src/main/groovy/org/openbakery/util/PlistHelper.groovy +++ b/libxcode/src/main/groovy/org/openbakery/util/PlistHelper.groovy @@ -17,16 +17,15 @@ class PlistHelper { this.commandRunner = commandRunner } - /** - * Reads the value for the given key from the given plist - * - * @param plist - * @param key - * @param commandRunner The commandRunner to execute commands (This is espacially needed for Unit Tests) - * - * @return returns the value for the given key - */ + * Reads the value for the given key from the given plist + * + * @param plist + * @param key + * @param commandRunner The commandRunner to execute commands (This is espacially needed for Unit Tests) + * + * @return returns the value for the given key + */ def getValueFromPlist(File plist, String key) { try { @@ -75,6 +74,12 @@ class PlistHelper { commandForPlist(plist, "Add :" + key + " string " + value) } + void addDictForPlist(File plist, String key, Map map) { + commandForPlist(plist, "Add :" + key + " dict") + map.keySet() + .each { it -> addValueForPlist(plist, ":$key:$it", map.get(it)) } + } + void addValueForPlist(File plist, String key, Number value) { if (value instanceof Float || value instanceof Double || value instanceof BigDecimal) { commandForPlist(plist, "Add :" + key + " real " + value) diff --git a/libxcode/src/main/groovy/org/openbakery/xcode/Destination.groovy b/libxcode/src/main/groovy/org/openbakery/xcode/Destination.groovy index 587ef405..95ef42b0 100644 --- a/libxcode/src/main/groovy/org/openbakery/xcode/Destination.groovy +++ b/libxcode/src/main/groovy/org/openbakery/xcode/Destination.groovy @@ -5,6 +5,7 @@ import org.apache.commons.lang.StringUtils /* * User: rene */ + class Destination { String platform = null @@ -26,15 +27,19 @@ class Destination { this.os = os } + public Type getTargetType() { + return Type.typeFromString(platform) + } + @Override - public java.lang.String toString() { + public String toString() { return "Destination{" + - "platform='" + platform + '\'' + - ", name='" + name + '\'' + - ", arch='" + arch + '\'' + - ", id='" + id + '\'' + - ", os='" + os + '\'' + - '}'; + "platform='" + platform + '\'' + + ", name='" + name + '\'' + + ", arch='" + arch + '\'' + + ", id='" + id + '\'' + + ", os='" + os + '\'' + + '}'; } boolean equals(other) { @@ -48,9 +53,9 @@ class Destination { } if (StringUtils.equalsIgnoreCase(arch, otherDestination.arch) && - StringUtils.equalsIgnoreCase(name, otherDestination.name) && - StringUtils.equalsIgnoreCase(os, otherDestination.os) && - StringUtils.equalsIgnoreCase(platform, otherDestination.platform)) { + StringUtils.equalsIgnoreCase(name, otherDestination.name) && + StringUtils.equalsIgnoreCase(os, otherDestination.os) && + StringUtils.equalsIgnoreCase(platform, otherDestination.platform)) { return true } return false @@ -67,7 +72,7 @@ class Destination { } - public java.lang.String toPrettyString() { + public String toPrettyString() { StringBuilder builder = new StringBuilder() builder.append(name) if (!StringUtils.isEmpty(platform)) { diff --git a/libxcode/src/main/groovy/org/openbakery/xcode/DestinationResolver.groovy b/libxcode/src/main/groovy/org/openbakery/xcode/DestinationResolver.groovy index a7c8dc1d..be14a92f 100644 --- a/libxcode/src/main/groovy/org/openbakery/xcode/DestinationResolver.groovy +++ b/libxcode/src/main/groovy/org/openbakery/xcode/DestinationResolver.groovy @@ -16,7 +16,8 @@ class DestinationResolver { List allFor(XcodebuildParameters parameters) { - if (parameters.type == Type.iOS && !parameters.simulator) { + if ((parameters.type == Type.iOS || parameters.type == Type.tvOS) + && !parameters.simulator) { return [] } return simulatorControl.getAllDestinations(parameters.type) @@ -35,10 +36,8 @@ class DestinationResolver { def runtime = simulatorControl.getMostRecentRuntime(parameters.type) - if (isSimulatorFor(parameters)) { // filter only on simulator builds - logger.debug("is a simulator build") if (parameters.configuredDestinations != null) { diff --git a/libxcode/src/main/groovy/org/openbakery/xcode/Type.groovy b/libxcode/src/main/groovy/org/openbakery/xcode/Type.groovy index ee474a1f..c9ccd94c 100644 --- a/libxcode/src/main/groovy/org/openbakery/xcode/Type.groovy +++ b/libxcode/src/main/groovy/org/openbakery/xcode/Type.groovy @@ -1,31 +1,26 @@ package org.openbakery.xcode enum Type { - iOS("iOS"), - macOS("macOS"), - tvOS("tvOS"), - watchOS("watchOS") + iOS("iOS"), + macOS("macOS"), + tvOS("tvOS"), + watchOS("watchOS") + private final String value - String value; + private static final String BACKWARD_COMPATIBILITY_MAC_OS = "osx" - Type(String value) { - this.value = value - } + Type(String value) { + this.value = value + } - static Type typeFromString(String string) { - if (string == null) { - return iOS - } - for (Type type in Type.values()) { - // for backward compatibility - if (string.toLowerCase().equalsIgnoreCase("osx")) { - return Type.macOS - } - if (string.toLowerCase().startsWith(type.value.toLowerCase())) { - return type - } - } - return iOS - } + String getValue() { + return value + } + + static Type typeFromString(final String string) { + return values() + .findAll { string?.toLowerCase()?.startsWith(it.value.toLowerCase()) } + .find() ?: (string?.toLowerCase()?.startsWith(BACKWARD_COMPATIBILITY_MAC_OS) ? macOS : null) + } } diff --git a/libxcode/src/main/groovy/org/openbakery/xcode/Version.groovy b/libxcode/src/main/groovy/org/openbakery/xcode/Version.groovy index de92bc57..0652acf1 100644 --- a/libxcode/src/main/groovy/org/openbakery/xcode/Version.groovy +++ b/libxcode/src/main/groovy/org/openbakery/xcode/Version.groovy @@ -1,6 +1,6 @@ package org.openbakery.xcode -class Version implements Comparable { +class Version implements Comparable, Serializable { public int major = -1 public int minor = -1 @@ -11,7 +11,7 @@ class Version implements Comparable { public Version() { } - public Version(String version) { + public Version(String version) { Scanner versionScanner = new Scanner(version); versionScanner.useDelimiter("\\."); @@ -30,8 +30,6 @@ class Version implements Comparable { } catch (InputMismatchException ex) { suffix = versionScanner.next() } - - } @Override @@ -71,13 +69,13 @@ class Version implements Comparable { builder.append(minor) } - if (this.maintenance>= 0) { + if (this.maintenance >= 0) { builder.append(".") builder.append(maintenance) } if (this.suffix != null) { - if (builder.length() > 0 ){ + if (builder.length() > 0) { builder.append(".") } builder.append(suffix) diff --git a/libxcode/src/main/groovy/org/openbakery/xcode/Xcode.groovy b/libxcode/src/main/groovy/org/openbakery/xcode/Xcode.groovy index 8bc50f27..274fb4df 100644 --- a/libxcode/src/main/groovy/org/openbakery/xcode/Xcode.groovy +++ b/libxcode/src/main/groovy/org/openbakery/xcode/Xcode.groovy @@ -1,24 +1,38 @@ package org.openbakery.xcode +import groovy.transform.CompileStatic +import org.gradle.internal.impldep.com.google.common.annotations.VisibleForTesting import org.openbakery.CommandRunner import org.slf4j.Logger import org.slf4j.LoggerFactory +import java.util.regex.Matcher +import java.util.regex.Pattern + +@CompileStatic class Xcode { - private static Logger logger = LoggerFactory.getLogger(Xcode.class) + private Version version = null + private String xcodePath + @VisibleForTesting + private CommandRunner commandRunner - CommandRunner commandRunner + public static final String ENV_DEVELOPER_DIR = "DEVELOPER_DIR" + public static final String XCODE_ACTION_XC_SELECT = "xcode-select" + public static final String XCODE_CONTENT_DEVELOPER = "Contents/Developer" + public static final String XCODE_CONTENT_XC_RUN = "/$XCODE_CONTENT_DEVELOPER/usr/bin/xcrun" + public static + final String XCODE_CONTENT_XCODE_BUILD = "$XCODE_CONTENT_DEVELOPER/usr/bin/xcodebuild" - String xcodePath - Version version = null + private final Logger logger = LoggerFactory.getLogger(Xcode.class) + private static final Pattern VERSION_PATTERN = ~/Xcode\s([^\s]*)\nBuild\sversion\s([^\s]*)/ - public Xcode(CommandRunner commandRunner) { + Xcode(CommandRunner commandRunner) { this(commandRunner, null) } - public Xcode(CommandRunner commandRunner, String version) { + Xcode(CommandRunner commandRunner, String version) { logger.debug("create xcode with version {}", version) this.commandRunner = commandRunner if (version != null) { @@ -26,40 +40,83 @@ class Xcode { } } - void setVersionFromString(String version) { - Version versionToCompare = new Version(version) - String installedXcodes = commandRunner.runWithResult("mdfind", "kMDItemCFBundleIdentifier=com.apple.dt.Xcode") - - - for (String xcode : installedXcodes.split("\n")) { - File xcodeBuildFile = new File(xcode, "Contents/Developer/usr/bin/xcodebuild"); - if (xcodeBuildFile.exists()) { - Version xcodeVersion = getXcodeVersion(xcodeBuildFile.absolutePath) - if (xcodeVersion.suffix != null && versionToCompare.suffix != null) { - if (xcodeVersion.suffix.equalsIgnoreCase(versionToCompare.suffix)) { - xcodePath = xcode - this.version = xcodeVersion - return - } - } else if (xcodeVersion.toString().startsWith(versionToCompare.toString())) { - xcodePath = xcode - this.version = xcodeVersion - return - } - } + CommandRunner getCommandRunner() { + return commandRunner + } + + /** + * Provide the environments values to provide to the command line runner to select + * a Xcode version without using `xcode-select -s` who requires `sudo`. + * + * @param version : The required Xcode version + * @return A map of environment variables to pass to the command runner + */ + Map getXcodeSelectEnvValue(String version) { + setVersionFromString(version) + File file = new File(xcodePath, XCODE_CONTENT_DEVELOPER) + HashMap result = new HashMap() + if (file.exists()) { + result.put(ENV_DEVELOPER_DIR, file.absolutePath) } - throw new IllegalStateException("No Xcode found with build number " + version); + return result + } + + void setVersionFromString(String version) throws IllegalArgumentException { + Optional result = resolveXcodeInstallOfVersion(version) + + if (result.isPresent()) { + selectXcode(result.get()) + } else { + throw new IllegalStateException("No Xcode found with build number " + version) + } } - Version getXcodeVersion(String xcodebuildCommand) { - String xcodeVersion = commandRunner.runWithResult(xcodebuildCommand, "-version"); + Optional resolveXcodeInstallOfVersion(String version) { + if (version == null) + return Optional.empty() + + final Version requiredVersion = new Version(version) + + return Optional.ofNullable(resolveInstalledXcodeVersionsList() + .split("\n") + .iterator() + .collect { new File(it as File, XCODE_CONTENT_XCODE_BUILD) } + .findAll { it.exists() } + .find { + Version candidate = getXcodeVersion(it.absolutePath) + + boolean versionStartWith = candidate.toString() + .startsWith(requiredVersion.toString()) - def VERSION_PATTERN = ~/Xcode\s([^\s]*)\nBuild\sversion\s([^\s]*)/ - def matcher = VERSION_PATTERN.matcher(xcodeVersion) + boolean versionHasSuffix = (candidate.suffix != null + && requiredVersion.suffix != null + && candidate.suffix.equalsIgnoreCase(requiredVersion.suffix)) + + return versionHasSuffix || versionStartWith + }) + } + + void selectXcode(File file) { + String absolutePath = file.absolutePath + Version xcodeVersion = getXcodeVersion(absolutePath) + xcodePath = new File(absolutePath - XCODE_CONTENT_XCODE_BUILD) + this.version = xcodeVersion + } + + String resolveInstalledXcodeVersionsList() { + return commandRunner.runWithResult("mdfind", + "kMDItemCFBundleIdentifier=com.apple.dt.Xcode") + } + + Version getXcodeVersion(String xcodeBuildCommand) { + String xcodeVersion = commandRunner.runWithResult(xcodeBuildCommand, + "-version") + + Matcher matcher = VERSION_PATTERN.matcher(xcodeVersion) if (matcher.matches()) { - Version version = new Version(matcher[0][1]) - version.suffix = matcher[0][2] + Version version = new Version(matcher.group(1)) + version.suffix = matcher.group(2) return version } return null @@ -74,37 +131,38 @@ class Xcode { String getPath() { if (xcodePath == null) { - String result = commandRunner.runWithResult("xcode-select", "-p") - xcodePath = result - "/Contents/Developer" + String result = commandRunner.runWithResult(XCODE_ACTION_XC_SELECT + , "-p") + xcodePath = result - "/$XCODE_CONTENT_DEVELOPER" } return xcodePath } - String getXcodebuild() { if (xcodePath != null) { - return xcodePath + "/Contents/Developer/usr/bin/xcodebuild" + return new File(xcodePath, XCODE_CONTENT_XCODE_BUILD).absolutePath } return "xcodebuild" } String getAltool() { - return getPath() + "/Contents/Applications/Application Loader.app/Contents/Frameworks/ITunesSoftwareService.framework/Support/altool" + return getPath() + "/Contents/Applications/Application Loader" + + ".app/Contents/Frameworks/ITunesSoftwareService.framework/Support/altool" } String getXcrun() { - return getPath() + "/Contents/Developer/usr/bin/xcrun" + return getPath() + XCODE_CONTENT_XC_RUN } String getSimctl() { - return getPath() + "/Contents/Developer/usr/bin/simctl" + return getPath() + "/$XCODE_CONTENT_DEVELOPER/usr/bin/simctl" } @Override - public String toString() { + String toString() { return "Xcode{" + - "xcodePath='" + xcodePath + '\'' + - ", version=" + version + - '}'; + "xcodePath='" + xcodePath + '\'' + + ", version=" + version + + '}' } } diff --git a/libxcode/src/main/groovy/org/openbakery/xcode/Xcodebuild.groovy b/libxcode/src/main/groovy/org/openbakery/xcode/Xcodebuild.groovy index 86fdd945..b84bae48 100644 --- a/libxcode/src/main/groovy/org/openbakery/xcode/Xcodebuild.groovy +++ b/libxcode/src/main/groovy/org/openbakery/xcode/Xcodebuild.groovy @@ -3,10 +3,11 @@ package org.openbakery.xcode import org.openbakery.CommandRunner import org.openbakery.output.OutputAppender +import javax.annotation.Nullable + class Xcodebuild { CommandRunner commandRunner - HashMap buildSettings = null File projectDirectory @@ -15,7 +16,14 @@ class Xcodebuild { XcodebuildParameters parameters List destinations - + public static final String EXECUTABLE = "xcodebuild" + public static final String ACTION_ARCHIVE = "archive" + public static final String ACTION_EXPORT_ARCHIVE = "-exportArchive" + public static final String ARGUMENT_SCHEME = "-scheme" + public static final String ARGUMENT_ARCHIVE_PATH = "-archivePath" + public static final String ARGUMENT_EXPORT_PATH = "-exportPath" + public static final String ARGUMENT_EXPORT_OPTIONS_PLIST = "-exportOptionsPlist" + public static final String ARGUMENT_XCCONFIG = "-xcconfig" public Xcodebuild(File projectDirectory, CommandRunner commandRunner, Xcode xcode, XcodebuildParameters parameters, List destinations) { this.projectDirectory = projectDirectory @@ -39,6 +47,45 @@ class Xcodebuild { } } + static void packageIpa(CommandRunner commandRunner, + File archivePath, + File exportPath, + File exportOptionsPlist) { + + assert archivePath != null && archivePath.exists() + assert exportPath != null && exportPath.exists() + assert exportOptionsPlist != null && exportOptionsPlist.exists() + + commandRunner.run(EXECUTABLE, + ACTION_EXPORT_ARCHIVE, + ARGUMENT_ARCHIVE_PATH, archivePath.absolutePath, + ARGUMENT_EXPORT_PATH, exportPath.absolutePath, + ARGUMENT_EXPORT_OPTIONS_PLIST, exportOptionsPlist.absolutePath) + } + + static void archive(CommandRunner commandRunner, + String scheme, + File outputPath, + File xcConfig, + @Nullable File xcodeApp) { + assert scheme != null + assert xcConfig.exists() && !xcConfig.isDirectory() + + HashMap envMap = new HashMap<>() + + if (xcodeApp != null) { + envMap.put(Xcode.ENV_DEVELOPER_DIR, xcodeApp.absolutePath) + } + + List args = [EXECUTABLE, + ACTION_ARCHIVE, + ARGUMENT_SCHEME, scheme, + ARGUMENT_ARCHIVE_PATH, outputPath.absolutePath, + ARGUMENT_XCCONFIG, xcConfig.absolutePath] + + commandRunner.run(args, envMap) + } + def execute(OutputAppender outputAppender, Map environment) { validateParameters(outputAppender, environment) def commandList = [] @@ -283,51 +330,51 @@ class Xcodebuild { String getToolchainDirectory() { - String buildSetting = getBuildSetting("TOOLCHAIN_DIR") - if (buildSetting != null) { - return buildSetting - } - return "/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain" + String buildSetting = getBuildSetting("TOOLCHAIN_DIR") + if (buildSetting != null) { + return buildSetting } + return "/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain" + } String getPlatformDirectory() { return getBuildSetting("PLATFORM_DIR") } - String loadBuildSettings() { - def commandList = [xcode.xcodebuild, "clean", "-showBuildSettings"] - - if (parameters.scheme != null && parameters.workspace != null) { - commandList.add("-scheme"); - commandList.add(parameters.scheme) - commandList.add("-workspace") - commandList.add(parameters.workspace) - } + String loadBuildSettings() { + def commandList = [xcode.xcodebuild, "clean", "-showBuildSettings"] - return commandRunner.runWithResult(this.projectDirectory.absolutePath, commandList) + if (parameters.scheme != null && parameters.workspace != null) { + commandList.add("-scheme"); + commandList.add(parameters.scheme) + commandList.add("-workspace") + commandList.add(parameters.workspace) } - private String getBuildSetting(String key) { - if (buildSettings == null) { - buildSettings = new HashMap<>() - String[] buildSettingsData = loadBuildSettings().split("\n") - for (line in buildSettingsData) { - int index = line.indexOf("=") - if (index > 0) { - String settingsKey = line.substring(0, index).trim() - String settingsValue = line.substring(index + 1, line.length()).trim() - buildSettings.put(settingsKey, settingsValue) - } + return commandRunner.runWithResult(this.projectDirectory.absolutePath, commandList) + } + + private String getBuildSetting(String key) { + if (buildSettings == null) { + buildSettings = new HashMap<>() + String[] buildSettingsData = loadBuildSettings().split("\n") + for (line in buildSettingsData) { + int index = line.indexOf("=") + if (index > 0) { + String settingsKey = line.substring(0, index).trim() + String settingsValue = line.substring(index + 1, line.length()).trim() + buildSettings.put(settingsKey, settingsValue) } } - return buildSettings.get(key) } + return buildSettings.get(key) + } @Override public String toString() { return "Xcodebuild{" + - "xcodePath='" + xcodePath + '\'' + - parameters + - '}' + "xcodePath='" + xcodePath + '\'' + + parameters + + '}' } } diff --git a/libxcode/src/main/groovy/org/openbakery/xcode/XcodebuildParameters.groovy b/libxcode/src/main/groovy/org/openbakery/xcode/XcodebuildParameters.groovy index 2352a55c..6fc6d8cb 100644 --- a/libxcode/src/main/groovy/org/openbakery/xcode/XcodebuildParameters.groovy +++ b/libxcode/src/main/groovy/org/openbakery/xcode/XcodebuildParameters.groovy @@ -1,6 +1,7 @@ package org.openbakery.xcode import org.apache.commons.io.FilenameUtils +import org.openbakery.util.PathHelper import org.slf4j.Logger import org.slf4j.LoggerFactory @@ -35,24 +36,24 @@ class XcodebuildParameters { @Override public String toString() { return "XcodebuildParameters {" + - ", scheme='" + scheme + '\'' + - ", target='" + target + '\'' + - ", simulator=" + simulator + - ", type=" + type + - ", workspace='" + workspace + '\'' + - ", configuration='" + configuration + '\'' + - ", bitcode=" + bitcode + - ", dstRoot=" + dstRoot + - ", objRoot=" + objRoot + - ", symRoot=" + symRoot + - ", sharedPrecompsDir=" + sharedPrecompsDir + - ", derivedDataPath=" + derivedDataPath + - ", arch=" + arch + - ", additionalParameters=" + additionalParameters + - ", configuredDestinations=" + configuredDestinations + - ", xctestrun=" + xctestrun + - ", applicationBundle=" + applicationBundle + - '}' + ", scheme='" + scheme + '\'' + + ", target='" + target + '\'' + + ", simulator=" + simulator + + ", type=" + type + + ", workspace='" + workspace + '\'' + + ", configuration='" + configuration + '\'' + + ", bitcode=" + bitcode + + ", dstRoot=" + dstRoot + + ", objRoot=" + objRoot + + ", symRoot=" + symRoot + + ", sharedPrecompsDir=" + sharedPrecompsDir + + ", derivedDataPath=" + derivedDataPath + + ", arch=" + arch + + ", additionalParameters=" + additionalParameters + + ", configuredDestinations=" + configuredDestinations + + ", xctestrun=" + xctestrun + + ", applicationBundle=" + applicationBundle + + '}' } @@ -98,7 +99,6 @@ class XcodebuildParameters { } - void setDestination(def destination) { if (destination instanceof List) { @@ -142,12 +142,15 @@ class XcodebuildParameters { File getOutputPath() { if (type == Type.iOS) { - if (simulator) { - return new File(getSymRoot(), "${configuration}-iphonesimulator") - } else { - return new File(getSymRoot(), "${configuration}-iphoneos") - } + return PathHelper.resolveIosSymRoot(simulator, + symRoot, + configuration) + } else if (type == Type.tvOS) { + return PathHelper.resolveAppleTvSymRoot(simulator, + symRoot, + configuration) } - return new File(getSymRoot(), configuration) + + return PathHelper.resolveMacOsSymRoot(symRoot, configuration) } } diff --git a/libxcode/src/test/groovy/org/openbakery/simulators/SimulatorControlSpecification.groovy b/libxcode/src/test/groovy/org/openbakery/simulators/SimulatorControlSpecification.groovy index 8f94d4aa..7c45b25f 100644 --- a/libxcode/src/test/groovy/org/openbakery/simulators/SimulatorControlSpecification.groovy +++ b/libxcode/src/test/groovy/org/openbakery/simulators/SimulatorControlSpecification.groovy @@ -7,15 +7,24 @@ import org.openbakery.xcode.Type import org.openbakery.xcode.Version import org.openbakery.xcode.Xcode import spock.lang.Specification +import spock.lang.Unroll class SimulatorControlSpecification extends Specification { File projectDir SimulatorControl simulatorControl - CommandRunner commandRunner = Mock(CommandRunner); - Xcode xcode = Mock(Xcode); + CommandRunner commandRunner = Mock(CommandRunner) + Xcode xcode = Mock(Xcode) - def SIMCTL = "/Applications/Xcode.app/Contents/Developer/usr/bin/simctl" + private static + final String SIM_CTL_LIST_UNAVAILABLE = "src/test/Resource/simctl-list-unavailable.txt" + + private static final String RES_PATH = "src/test/Resource" + private static final String SIMCTL = "/Applications/Xcode.app/Contents/Developer/usr/bin/simctl" + private static final String SIM_CTL_LIST_XCODE7 = "${RES_PATH}/simctl-list-xcode7.txt" + private static final String SIM_CTL_LIST_XCODE8 = "${RES_PATH}/simctl-list-xcode8.txt" + private static final String SIM_CTL_LIST_XCODE9 = "${RES_PATH}/simctl-list-xcode9.txt" + private static final String SIM_CTL_LIST_XCODE9_1 = "${RES_PATH}/simctl-list-xcode9_1.txt" def setup() { xcode.getSimctl() >> SIMCTL @@ -28,23 +37,27 @@ class SimulatorControlSpecification extends Specification { void mockXcode6() { - commandRunner.runWithResult([SIMCTL, "list"]) >> FileUtils.readFileToString(new File("src/test/Resource/simctl-list-unavailable.txt")) + mockWithFile(SIM_CTL_LIST_UNAVAILABLE) } void mockXcode7() { - commandRunner.runWithResult([SIMCTL, "list"]) >> FileUtils.readFileToString(new File("src/test/Resource/simctl-list-xcode7.txt")) + mockWithFile(SIM_CTL_LIST_XCODE7) } void mockXcode8() { - commandRunner.runWithResult([SIMCTL, "list"]) >> FileUtils.readFileToString(new File("src/test/Resource/simctl-list-xcode8.txt")) + mockWithFile(SIM_CTL_LIST_XCODE8) } void mockXcode9() { - commandRunner.runWithResult([SIMCTL, "list"]) >> FileUtils.readFileToString(new File("src/test/Resource/simctl-list-xcode9.txt")) + mockWithFile(SIM_CTL_LIST_XCODE9) } void mockXcode9_1() { - commandRunner.runWithResult([SIMCTL, "list"]) >> FileUtils.readFileToString(new File("src/test/Resource/simctl-list-xcode9_1.txt")) + mockWithFile(SIM_CTL_LIST_XCODE9_1) + } + + void mockWithFile(String uri) { + commandRunner.runWithResult([SIMCTL, "list"]) >> FileUtils.readFileToString(new File(uri)) } @@ -52,7 +65,6 @@ class SimulatorControlSpecification extends Specification { given: mockXcode6() - expect: List runtimes = simulatorControl.getRuntimes() @@ -73,7 +85,6 @@ class SimulatorControlSpecification extends Specification { runtime1.identifier.equals("com.apple.CoreSimulator.SimRuntime.iOS-7-1") } - def "get most recent iOS runtime"() { given: @@ -145,7 +156,6 @@ class SimulatorControlSpecification extends Specification { } - def "devices iOS7"() { given: mockXcode6() @@ -163,7 +173,6 @@ class SimulatorControlSpecification extends Specification { } - def "devices iOS8"() { given: mockXcode6() @@ -181,8 +190,6 @@ class SimulatorControlSpecification extends Specification { } - - def "delete All"() { given: mockXcode6() @@ -191,22 +198,22 @@ class SimulatorControlSpecification extends Specification { simulatorControl.deleteAll() then: - 1* commandRunner.runWithResult([SIMCTL, "delete", "73C126C8-FD53-44EA-80A3-84F5F19508C0"]) - 1* commandRunner.runWithResult([SIMCTL, "delete", "15F68098-3B21-411D-B553-1C3161C100E7"]) - 1* commandRunner.runWithResult([SIMCTL, "delete", "545260B4-C6B8-4D3A-9348-AD3B882D8D17"]) - 1* commandRunner.runWithResult([SIMCTL, "delete", "454F3900-7B07-422E-A731-D46C821888B5"]) - 1* commandRunner.runWithResult([SIMCTL, "delete", "F60A8735-97D9-48A8-9728-3CC53394F7FC"]) - 1* commandRunner.runWithResult([SIMCTL, "delete", "B8278DAC-97EE-4097-88CA-5650960882A5"]) - 1* commandRunner.runWithResult([SIMCTL, "delete", "E06E8144-D4AB-4616-A19E-9A489FB0CC17"]) - 1* commandRunner.runWithResult([SIMCTL, "delete", "0560469A-813F-4AF7-826C-4598802A7FFD"]) - 1* commandRunner.runWithResult([SIMCTL, "delete", "F029A31F-3CBF-422D-AEF4-D05675BAEDEF"]) - 1* commandRunner.runWithResult([SIMCTL, "delete", "6F2558A0-A789-443B-B142-7BA707E3C9E8"]) - 1* commandRunner.runWithResult([SIMCTL, "delete", "075026D3-C77E-40F9-944C-EBCB565E17D5"]) - 1* commandRunner.runWithResult([SIMCTL, "delete", "5C4434E1-81AC-4448-8237-26029A57E594"]) - 1* commandRunner.runWithResult([SIMCTL, "delete", "E85B0A4D-6B82-4F7C-B4CF-3C00E4EFF3D1"]) - 1* commandRunner.runWithResult([SIMCTL, "delete", "A7400DB8-CDF3-4E6F-AF87-EB2B296D82C5"]) - 1* commandRunner.runWithResult([SIMCTL, "delete", "29C34492-7006-41D7-B634-8703972F725C"]) - 1* commandRunner.runWithResult([SIMCTL, "delete", "50D9CBF1-608C-4866-9B5F-234D7FACBC16"]) + 1 * commandRunner.runWithResult([SIMCTL, "delete", "73C126C8-FD53-44EA-80A3-84F5F19508C0"]) + 1 * commandRunner.runWithResult([SIMCTL, "delete", "15F68098-3B21-411D-B553-1C3161C100E7"]) + 1 * commandRunner.runWithResult([SIMCTL, "delete", "545260B4-C6B8-4D3A-9348-AD3B882D8D17"]) + 1 * commandRunner.runWithResult([SIMCTL, "delete", "454F3900-7B07-422E-A731-D46C821888B5"]) + 1 * commandRunner.runWithResult([SIMCTL, "delete", "F60A8735-97D9-48A8-9728-3CC53394F7FC"]) + 1 * commandRunner.runWithResult([SIMCTL, "delete", "B8278DAC-97EE-4097-88CA-5650960882A5"]) + 1 * commandRunner.runWithResult([SIMCTL, "delete", "E06E8144-D4AB-4616-A19E-9A489FB0CC17"]) + 1 * commandRunner.runWithResult([SIMCTL, "delete", "0560469A-813F-4AF7-826C-4598802A7FFD"]) + 1 * commandRunner.runWithResult([SIMCTL, "delete", "F029A31F-3CBF-422D-AEF4-D05675BAEDEF"]) + 1 * commandRunner.runWithResult([SIMCTL, "delete", "6F2558A0-A789-443B-B142-7BA707E3C9E8"]) + 1 * commandRunner.runWithResult([SIMCTL, "delete", "075026D3-C77E-40F9-944C-EBCB565E17D5"]) + 1 * commandRunner.runWithResult([SIMCTL, "delete", "5C4434E1-81AC-4448-8237-26029A57E594"]) + 1 * commandRunner.runWithResult([SIMCTL, "delete", "E85B0A4D-6B82-4F7C-B4CF-3C00E4EFF3D1"]) + 1 * commandRunner.runWithResult([SIMCTL, "delete", "A7400DB8-CDF3-4E6F-AF87-EB2B296D82C5"]) + 1 * commandRunner.runWithResult([SIMCTL, "delete", "29C34492-7006-41D7-B634-8703972F725C"]) + 1 * commandRunner.runWithResult([SIMCTL, "delete", "50D9CBF1-608C-4866-9B5F-234D7FACBC16"]) } @@ -397,11 +404,12 @@ class SimulatorControlSpecification extends Specification { destination.os = '9.0' when: - SimulatorDevice device = simulatorControl.getDevice(destination) + Optional device = simulatorControl.getDevice(destination) then: - device.name == "iPhone 4s" - device.identifier == "5C8E1FF3-47B7-48B8-96E9-A12740DBC58A" + device.present + device.get().name == "iPhone 4s" + device.get().identifier == "5C8E1FF3-47B7-48B8-96E9-A12740DBC58A" } def "get 8.4 device for destination xcode 7.1"() { @@ -414,11 +422,12 @@ class SimulatorControlSpecification extends Specification { destination.os = '8.4' when: - SimulatorDevice device = simulatorControl.getDevice(destination) + Optional device = simulatorControl.getDevice(destination) then: - device.name == "iPad 2" - device.identifier == "E5089648-1CE4-40D5-8295-8E026BDDFF52" + device.present + device.get().name == "iPad 2" + device.get().identifier == "E5089648-1CE4-40D5-8295-8E026BDDFF52" } def "get 9.1 device for destination xcode 7.1"() { @@ -431,11 +440,12 @@ class SimulatorControlSpecification extends Specification { destination.os = '9.1' when: - SimulatorDevice device = simulatorControl.getDevice(destination) + Optional device = simulatorControl.getDevice(destination) then: - device.name == "iPad 2" - device.identifier == "D72F7CC6-8426-4E0A-A234-34747B1F30DD" + device.present + device.get().name == "iPad 2" + device.get().identifier == "D72F7CC6-8426-4E0A-A234-34747B1F30DD" } @@ -526,7 +536,6 @@ class SimulatorControlSpecification extends Specification { } - def "xcode 8 has Apple TV device type "() { given: mockXcode8() @@ -596,7 +605,6 @@ class SimulatorControlSpecification extends Specification { } - def "devices iOS10"() { given: mockXcode8() @@ -641,35 +649,28 @@ class SimulatorControlSpecification extends Specification { devices.size() == 15 } + @Unroll + def "Ensure that the compilation runtime: #runtimeType with name : #runtimeName and #deviceCount devices is properly defined"() { + expect: + mockWithFile(ctlFile) - def "devices iOS11.1 runtimes" () { - given: - mockXcode9_1() - - when: - List runtimes = simulatorControl.getRuntimes() - - then: - runtimes != null - runtimes.size() == 3 - runtimes.get(0).name == "iOS 11.1" - } + SimulatorRuntime runtime = simulatorControl + .getRuntimes() + .find { it.type == runtimeType } - def "devices iOS11.1"() { - given: - mockXcode9_1() + runtime != null + runtime.name == runtimeName + runtime.type == runtimeType - when: - List runtimes = simulatorControl.getRuntimes() - SimulatorRuntime runtime = runtimes.get(0) - List devices = simulatorControl.getDevices(runtimes.get(0)) + simulatorControl.getDevices(runtime) + .size() == deviceCount - then: - devices != null - devices.size() == 18 - runtime.name == "iOS 11.1" + where: + ctlFile | runtimeType | runtimeName | deviceCount + SIM_CTL_LIST_XCODE9_1 | Type.tvOS | "tvOS 11.1" | 3 + SIM_CTL_LIST_XCODE9_1 | Type.iOS | "iOS 11.1" | 18 + SIM_CTL_LIST_XCODE9_1 | Type.watchOS | "watchOS 4.1" | 6 + SIM_CTL_LIST_XCODE9 | Type.iOS | "iOS 11.0" | 15 } } - - diff --git a/libxcode/src/test/groovy/org/openbakery/util/PathHelperTest.groovy b/libxcode/src/test/groovy/org/openbakery/util/PathHelperTest.groovy new file mode 100644 index 00000000..7baeee5c --- /dev/null +++ b/libxcode/src/test/groovy/org/openbakery/util/PathHelperTest.groovy @@ -0,0 +1,46 @@ +package org.openbakery.util + +import org.openbakery.xcode.Type +import spock.lang.Specification + +class PathHelperTest extends Specification { + def "test iOS and TvOS symroot path resolution"() { + setup: + File tempSymRoot = File.createTempDir() + + expect: + File file = PathHelper.resolvePath(type, + simulator, + tempSymRoot, + configuration) + + file == new File(tempSymRoot, "${configuration}-${outputPath}") + + where: + simulator | configuration | type | outputPath + false | "debug" | Type.iOS | PathHelper.IPHONE_OS + false | "release" | Type.iOS | PathHelper.IPHONE_OS + true | "debug" | Type.iOS | PathHelper.IPHONE_SIMULATOR + true | "release" | Type.iOS | PathHelper.IPHONE_SIMULATOR + false | "debug" | Type.tvOS | PathHelper.APPLE_TV_OS + false | "release" | Type.tvOS | PathHelper.APPLE_TV_OS + true | "debug" | Type.tvOS | PathHelper.APPLE_TV_SIMULATOR + true | "release" | Type.tvOS | PathHelper.APPLE_TV_SIMULATOR + } + + def "Osx symroot path resolution"() { + setup: + File tempSymRoot = File.createTempDir() + + expect: + File file = PathHelper.resolveMacOsSymRoot(tempSymRoot, + configuration) + + file == new File(tempSymRoot, configuration) + + where: + configuration | outputPath + "debug" | _ + "release" | _ + } +} diff --git a/libxcode/src/test/groovy/org/openbakery/xcode/DestinationSpecification.groovy b/libxcode/src/test/groovy/org/openbakery/xcode/DestinationSpecification.groovy new file mode 100644 index 00000000..1d51bd3f --- /dev/null +++ b/libxcode/src/test/groovy/org/openbakery/xcode/DestinationSpecification.groovy @@ -0,0 +1,20 @@ +package org.openbakery.xcode + +import spock.lang.Specification + +class DestinationSpecification extends Specification { + + def "A destination target type should be resolved successfully from it's platform"() { + expect: + new Destination(platform, null, null).targetType == target + + where: + platform | target + "iOS Simulator" | Type.iOS + "tvOS Simulator" | Type.tvOS + "watchOS Simulator" | Type.watchOS + "invalid" | null + "" | null + null | null + } +} diff --git a/libxcode/src/test/groovy/org/openbakery/xcode/TypeSpecification.groovy b/libxcode/src/test/groovy/org/openbakery/xcode/TypeSpecification.groovy index 7a091137..d8f60952 100644 --- a/libxcode/src/test/groovy/org/openbakery/xcode/TypeSpecification.groovy +++ b/libxcode/src/test/groovy/org/openbakery/xcode/TypeSpecification.groovy @@ -4,19 +4,23 @@ import spock.lang.Specification class TypeSpecification extends Specification { - def "typeFromString give proper type"() { expect: - Type.typeFromString("macOS") == Type.macOS - Type.typeFromString("MacOS") == Type.macOS - Type.typeFromString("macos") == Type.macOS - Type.typeFromString("OSX") == Type.macOS - Type.typeFromString("osx") == Type.macOS - Type.typeFromString("Osx") == Type.macOS - Type.typeFromString("ios") == Type.iOS - Type.typeFromString("IOS") == Type.iOS - Type.typeFromString("iOS") == Type.iOS - Type.typeFromString("tvOS") == Type.tvOS - Type.typeFromString("tvos") == Type.tvOS + Type.typeFromString(value) == type + + where: + value | type + "macOS" | Type.macOS + "MacOS" | Type.macOS + "macos" | Type.macOS + "OSX" | Type.macOS + "osx" | Type.macOS + "Osx" | Type.macOS + "ios" | Type.iOS + "IOS" | Type.iOS + "iOS" | Type.iOS + "iOS 7.1 (7.1 - 11D167) (com.apple.CoreSimulator.SimRuntime.iOS-7-1)" | Type.iOS + "tvOS" | Type.tvOS + "tvos" | Type.tvOS } } diff --git a/libxcode/src/test/groovy/org/openbakery/xcode/XcodeSpecification.groovy b/libxcode/src/test/groovy/org/openbakery/xcode/XcodeSpecification.groovy index 7ab51de2..41e4342d 100644 --- a/libxcode/src/test/groovy/org/openbakery/xcode/XcodeSpecification.groovy +++ b/libxcode/src/test/groovy/org/openbakery/xcode/XcodeSpecification.groovy @@ -2,224 +2,249 @@ package org.openbakery.xcode import org.apache.commons.io.FileUtils import org.openbakery.CommandRunner -import org.openbakery.xcode.Version -import org.openbakery.xcode.Xcode import spock.lang.Specification +import spock.lang.Unroll class XcodeSpecification extends Specification { - Xcode xcode + Xcode xcode - CommandRunner commandRunner = Mock(CommandRunner) + CommandRunner commandRunner = Mock(CommandRunner) - File xcode7_1_1 - File xcode6_1 - File xcode6_0 - File xcode5_1 + static File xcode7_1_1 = new File(File.createTempDir(), "Xcode7.1.1.app") + static File xcode6_1 = new File(File.createTempDir(), "Xcode6-1.app") + static File xcode6_0 = new File(File.createTempDir(), "Xcode6.app") + static File xcode5_1 = new File(File.createTempDir(), "Xcode5.app") - def setup() { + def setup() { + xcode = Spy(Xcode, constructorArgs: [commandRunner]) + + new File(xcode7_1_1, "Contents/Developer/usr/bin").mkdirs() + new File(xcode6_1, "Contents/Developer/usr/bin").mkdirs() + new File(xcode6_0, "Contents/Developer/usr/bin").mkdirs() + new File(xcode5_1, "Contents/Developer/usr/bin").mkdirs() - xcode = new Xcode(commandRunner) + new File(xcode7_1_1, "Contents/Developer/usr/bin/xcodebuild").createNewFile() + new File(xcode6_1, "Contents/Developer/usr/bin/xcodebuild").createNewFile() + new File(xcode6_0, "Contents/Developer/usr/bin/xcodebuild").createNewFile() + new File(xcode5_1, "Contents/Developer/usr/bin/xcodebuild").createNewFile() + } + + def cleanup() { + FileUtils.deleteDirectory(xcode7_1_1) + FileUtils.deleteDirectory(xcode6_1) + FileUtils.deleteDirectory(xcode6_0) + FileUtils.deleteDirectory(xcode5_1) + } - xcode7_1_1 = new File(System.getProperty("java.io.tmpdir"), "Xcode7.1.1.app") - xcode6_1 = new File(System.getProperty("java.io.tmpdir"), "Xcode6-1.app") - xcode6_0 = new File(System.getProperty("java.io.tmpdir"), "Xcode6.app") - xcode5_1 = new File(System.getProperty("java.io.tmpdir"), "Xcode5.app") - new File(xcode7_1_1, "Contents/Developer/usr/bin").mkdirs() - new File(xcode6_1, "Contents/Developer/usr/bin").mkdirs() - new File(xcode6_0, "Contents/Developer/usr/bin").mkdirs() - new File(xcode5_1, "Contents/Developer/usr/bin").mkdirs() + def "test default xcode path"() { + given: + useDefaultXcode() - new File(xcode7_1_1, "Contents/Developer/usr/bin/xcodebuild").createNewFile() - new File(xcode6_1, "Contents/Developer/usr/bin/xcodebuild").createNewFile() - new File(xcode6_0, "Contents/Developer/usr/bin/xcodebuild").createNewFile() - new File(xcode5_1, "Contents/Developer/usr/bin/xcodebuild").createNewFile() + expect: + xcode.getPath().equals("/Applications/Xcode.app") + } + def useXcode(String version) { + mockInstalledXcodeVersions() - } + xcode = new Xcode(commandRunner, version) + } - def cleanup() { - FileUtils.deleteDirectory(xcode7_1_1) - FileUtils.deleteDirectory(xcode6_1) - FileUtils.deleteDirectory(xcode6_0) - FileUtils.deleteDirectory(xcode5_1) - } + def mockInstalledXcodeVersions() { + commandRunner.runWithResult(xcode5_1.absolutePath + "/Contents/Developer/usr/bin/xcodebuild", "-version") >> ("Xcode 5.1.1\nBuild version 5B1008") + commandRunner.runWithResult(xcode6_0.absolutePath + "/Contents/Developer/usr/bin/xcodebuild", "-version") >> ("Xcode 6.0\nBuild version 6A000") + commandRunner.runWithResult(xcode6_1.absolutePath + "/Contents/Developer/usr/bin/xcodebuild", "-version") >> ("Xcode 6.4\nBuild version 6E35b") + commandRunner.runWithResult(xcode7_1_1.absolutePath + "/Contents/Developer/usr/bin/xcodebuild", "-version") >> ("Xcode 7.1.1\nBuild version 7B1005") + commandRunner.runWithResult("mdfind", "kMDItemCFBundleIdentifier=com.apple.dt.Xcode") >> xcode5_1.absolutePath + "\n" + xcode6_0.absolutePath + "\n" + xcode6_1.absolutePath + "\n" + xcode7_1_1.absolutePath + } - def "test default xcode path"() { - given: - useDefaultXcode() - - expect: - xcode.getPath().equals("/Applications/Xcode.app") - - } - - - def useXcode(String version) { - commandRunner.runWithResult(xcode5_1.absolutePath + "/Contents/Developer/usr/bin/xcodebuild", "-version") >> ("Xcode 5.1.1\nBuild version 5B1008") - commandRunner.runWithResult(xcode6_0.absolutePath + "/Contents/Developer/usr/bin/xcodebuild", "-version") >> ("Xcode 6.0\nBuild version 6A000") - commandRunner.runWithResult(xcode6_1.absolutePath + "/Contents/Developer/usr/bin/xcodebuild", "-version") >> ("Xcode 6.4\nBuild version 6E35b") - commandRunner.runWithResult(xcode7_1_1.absolutePath + "/Contents/Developer/usr/bin/xcodebuild", "-version") >> ("Xcode 7.1.1\nBuild version 7B1005") - commandRunner.runWithResult("mdfind", "kMDItemCFBundleIdentifier=com.apple.dt.Xcode") >> xcode5_1.absolutePath + "\n" + xcode6_0.absolutePath + "\n" + xcode6_1.absolutePath + "\n" + xcode7_1_1.absolutePath - - xcode = new Xcode(commandRunner, version) - } - - - def useDefaultXcode() { - commandRunner.runWithResult("xcode-select", "-p") >> ("/Applications/Xcode.app/Contents/Developer") - } - - def "xcodebuild of Xcode 5 is used"() { - given: - useXcode("5B1008") - - expect: - xcode.getXcodebuild().endsWith("Xcode5.app/Contents/Developer/usr/bin/xcodebuild") - } - - def "xcodebuild of Xcode 5 simple version number"() { - given: - useXcode("5.1") - - expect: - xcode.getXcodebuild().endsWith("Xcode5.app/Contents/Developer/usr/bin/xcodebuild") - } - - - def "xcode Version Simple 1"() { - - given: - useXcode("5.1.1") - - expect: - xcode.getXcodebuild().endsWith("Xcode5.app/Contents/Developer/usr/bin/xcodebuild") - } - - def "xcodeVersion Simple not found"() { - when: - useXcode("5.1.2") - - then: - thrown(IllegalStateException) - } - - - def "xcodeVersion select last"() { - commandRunner.runWithResult("mdfind", "kMDItemCFBundleIdentifier=com.apple.dt.Xcode") >> xcode6_1.absolutePath + "\n" + xcode6_0.absolutePath + "\n" + xcode5_1.absolutePath - commandRunner.runWithResult(xcode6_1.absolutePath + "/Contents/Developer/usr/bin/xcodebuild", "-version") >> "Xcode 6.0\nBuild version 6A000" - commandRunner.runWithResult(xcode6_0.absolutePath + "/Contents/Developer/usr/bin/xcodebuild", "-version") >> "Xcode 6.0\nBuild version 6A000" - commandRunner.runWithResult(xcode5_1.absolutePath + "/Contents/Developer/usr/bin/xcodebuild", "-version") >> "Xcode 5.1.1\nBuild version 5B1008" - - when: - xcode = new Xcode(commandRunner, '5B1008') - - then: - xcode.getXcodebuild().endsWith("Xcode5.app/Contents/Developer/usr/bin/xcodebuild") - } - - - def "xcodeVersion select not found"() { - useXcode("5B1008") - - when: - xcode = new Xcode(commandRunner, '5B1009') - - then: - thrown(IllegalStateException) - } - - def "version is not null"() { - given: - commandRunner.runWithResult("xcodebuild", "-version") >> ("Xcode 7.3.1\nBuild version 7D1014") - - expect: - xcode.getVersion() != null - xcode.getVersion().major == 7 - xcode.getVersion().minor == 3 - xcode.getVersion().maintenance == 1 - } - - - def "altool default path"() { - given: - useDefaultXcode() - - expect: - xcode.getAltool() == '/Applications/Xcode.app/Contents/Applications/Application Loader.app/Contents/Frameworks/ITunesSoftwareService.framework/Support/altool' - } - - def "altool with xcode 7.1.1"() { - given: - useXcode("7.1") - - expect: - xcode.getAltool().contains('Xcode7.1.1.app') - xcode.getAltool().endsWith('Contents/Applications/Application Loader.app/Contents/Frameworks/ITunesSoftwareService.framework/Support/altool') - } - - def "xcrun default path"() { - given: - useDefaultXcode() - - expect: - xcode.getXcrun() == '/Applications/Xcode.app/Contents/Developer/usr/bin/xcrun' - } - - - def "xcrun with xcode 7.1.1"() { - given: - useXcode("7.1") - - expect: - xcode.getXcrun().endsWith('Xcode7.1.1.app/Contents/Developer/usr/bin/xcrun') - } - - - - def "set xcode version"() { - useXcode("7.1") - - - expect: - xcode.version instanceof Version - xcode.version.major == 7 - xcode.version.minor == 1 - xcode.version.maintenance == 1 - } - - def "set xcode version with xcode 6"() { - useXcode("6.4") - - expect: - xcode.version instanceof Version - xcode.version.major == 6 - xcode.version.minor == 4 - xcode.version.maintenance == -1 - } - - def "get xcode version"() { - given: - commandRunner.runWithResult("xcodebuild", "-version") >> ("Xcode 6.4\nBuild version 6E35b") - - when: - Version version = xcode.version - - then: - version instanceof Version - version.major == 6 - version.minor == 4 - version.maintenance == -1 - } - - - def "simctl default path"() { - given: - useDefaultXcode() - - expect: - xcode.getSimctl() == '/Applications/Xcode.app/Contents/Developer/usr/bin/simctl' - } + def useDefaultXcode() { + commandRunner.runWithResult("xcode-select", "-p") >> ("/Applications/Xcode.app/Contents/Developer") + } + @Unroll + def "xcodebuild of Xcode 5 is used"() { + given: + useXcode("5B1008") + expect: + xcode.getXcodebuild() + .endsWith("Xcode5.app/Contents/Developer/usr/bin/xcodebuild") + + where: + version | _ + "5B1008" | _ + "5.1" | _ + "5.1.1" | _ + } + + def "xcodeVersion Simple not found"() { + when: + useXcode("5.1.2") + + then: + thrown(IllegalStateException) + } + + def "xcodeVersion select last"() { + commandRunner.runWithResult("mdfind", "kMDItemCFBundleIdentifier=com.apple.dt.Xcode") >> xcode6_1.absolutePath + "\n" + xcode6_0.absolutePath + "\n" + xcode5_1.absolutePath + commandRunner.runWithResult(xcode6_1.absolutePath + "/Contents/Developer/usr/bin/xcodebuild", "-version") >> "Xcode 6.0\nBuild version 6A000" + commandRunner.runWithResult(xcode6_0.absolutePath + "/Contents/Developer/usr/bin/xcodebuild", "-version") >> "Xcode 6.0\nBuild version 6A000" + commandRunner.runWithResult(xcode5_1.absolutePath + "/Contents/Developer/usr/bin/xcodebuild", "-version") >> "Xcode 5.1.1\nBuild version 5B1008" + + when: + xcode = new Xcode(commandRunner, '5B1008') + + then: + xcode.getXcodebuild().endsWith("Xcode5.app/Contents/Developer/usr/bin/xcodebuild") + } + + + def "xcodeVersion select not found"() { + useXcode("5B1008") + + when: + xcode = new Xcode(commandRunner, '5B1009') + + then: + thrown(IllegalStateException) + } + + def "version is not null"() { + given: + commandRunner.runWithResult("xcodebuild", "-version") >> ("Xcode 7.3.1\nBuild version 7D1014") + + expect: + xcode.getVersion() != null + xcode.getVersion().major == 7 + xcode.getVersion().minor == 3 + xcode.getVersion().maintenance == 1 + } + + + def "altool default path"() { + given: + useDefaultXcode() + + expect: + xcode.getAltool() == '/Applications/Xcode.app/Contents/Applications/Application Loader.app/Contents/Frameworks/ITunesSoftwareService.framework/Support/altool' + } + + def "altool with xcode 7.1.1"() { + given: + useXcode("7.1") + + expect: + xcode.getAltool().contains('Xcode7.1.1.app') + xcode.getAltool().endsWith('Contents/Applications/Application Loader.app/Contents/Frameworks/ITunesSoftwareService.framework/Support/altool') + } + + def "xcrun default path"() { + given: + useDefaultXcode() + + expect: + xcode.getXcrun() == '/Applications/Xcode.app/Contents/Developer/usr/bin/xcrun' + } + + + def "xcrun with xcode 7.1.1"() { + given: + useXcode("7.1") + + expect: + xcode.getXcrun().endsWith('Xcode7.1.1.app/Contents/Developer/usr/bin/xcrun') + } + + def "set xcode version"() { + setup: + useXcode(version) + + expect: + xcode.version instanceof Version + xcode.version.major == major + xcode.version.minor == minor + xcode.version.maintenance == maintenance + + where: + version | major | minor | maintenance + "5.1.1" | 5 | 1 | 1 + "6.0" | 6 | 0 | -1 + "6.4" | 6 | 4 | -1 + "7.1.1" | 7 | 1 | 1 + } + + def "get xcode version"() { + given: + commandRunner.runWithResult("xcodebuild", "-version") >> ("Xcode 6.4\nBuild version 6E35b") + + when: + Version version = xcode.version + + then: + version instanceof Version + version.major == 6 + version.minor == 4 + version.maintenance == -1 + } + + + def "simctl default path"() { + given: + useDefaultXcode() + + expect: + xcode.getSimctl() == '/Applications/Xcode.app/Contents/Developer/usr/bin/simctl' + } + + def "Should be able to resolve a Xcode version by string without exception when valid"() { + when: + mockInstalledXcodeVersions() + xcode.setVersionFromString(version) + + then: + 1 * xcode.selectXcode(new File(file, Xcode.XCODE_CONTENT_XCODE_BUILD)) + xcode.version.toString() == (version + "." + buildVersion) + noExceptionThrown() + + where: + version | buildVersion | file + "5.1.1" | "5B1008" | xcode5_1 + "6.0" | "6A000" | xcode6_0 + "7.1.1" | "7B1005" | xcode7_1_1 + } + + def "Should raise an exception when resolving a invalid Xcode instance by version string"() { + when: + mockInstalledXcodeVersions() + xcode.setVersionFromString(version) + + then: + thrown(exception) + 0 * xcode.selectXcode(_) + + where: + version | exception + "5.1.3" | IllegalStateException + "10.0" | IllegalStateException + "7.1.3" | IllegalStateException + null | IllegalStateException + } + + def "Should return a map of environment values containing the developer dir key for valid xcode version"() { + when: + mockInstalledXcodeVersions() + Map envValues = xcode.getXcodeSelectEnvValue(version) + + then: + noExceptionThrown() + envValues.get(Xcode.ENV_DEVELOPER_DIR) == new File(file, Xcode.XCODE_CONTENT_DEVELOPER).absolutePath + + where: + version | file + "5.1.1" | xcode5_1 + "6.0" | xcode6_0 + "7.1.1" | xcode7_1_1 + } } diff --git a/libxcode/src/test/groovy/org/openbakery/xcode/XcodebuildParametersSpecification.groovy b/libxcode/src/test/groovy/org/openbakery/xcode/XcodebuildParametersSpecification.groovy index 4beb1d8a..40a2f4af 100644 --- a/libxcode/src/test/groovy/org/openbakery/xcode/XcodebuildParametersSpecification.groovy +++ b/libxcode/src/test/groovy/org/openbakery/xcode/XcodebuildParametersSpecification.groovy @@ -1,11 +1,13 @@ package org.openbakery.xcode +import org.openbakery.util.PathHelper import spock.lang.Specification +import spock.lang.Unroll class XcodebuildParametersSpecification extends Specification { - XcodebuildParameters first = new XcodebuildParameters() + XcodebuildParameters first = new XcodebuildParameters() XcodebuildParameters second = new XcodebuildParameters() def setup() { @@ -16,7 +18,7 @@ class XcodebuildParametersSpecification extends Specification { first.workspace = "workspace" first.configuration = "configuration" first.additionalParameters = "additionalParameters" - first.devices = Devices.UNIVERSAL + first.devices = Devices.UNIVERSAL first.configuredDestinations = ["iPhone 4s"] } @@ -51,7 +53,6 @@ class XcodebuildParametersSpecification extends Specification { } - def "simulator is merged reverse"() { when: first.simulator = true @@ -174,7 +175,8 @@ class XcodebuildParametersSpecification extends Specification { first.outputPath == new File("sym/debug-iphonesimulator") } - def "outputPath with different configurations"(simulator, configuration, type, outputPath) { + @Unroll + def "path type: #type in config #type in #configuration mode and simulator:#simulator"() { when: first.configuration = configuration first.type = type @@ -182,15 +184,22 @@ class XcodebuildParametersSpecification extends Specification { first.symRoot = new File("sym") then: - first.outputPath == new File(outputPath) + File file = type == Type.macOS ? new File("sym/${outputPath}") + : new File("sym/${configuration}-${outputPath}") + + first.outputPath == file where: simulator | configuration | type | outputPath - false | "debug" | Type.iOS | "sym/debug-iphoneos" - false | "release" | Type.iOS | "sym/release-iphoneos" - true | "debug" | Type.iOS | "sym/debug-iphonesimulator" - true | "release" | Type.iOS | "sym/release-iphonesimulator" - false | "release" | Type.macOS | "sym/release" + false | "debug" | Type.iOS | PathHelper.IPHONE_OS + false | "release" | Type.iOS | PathHelper.IPHONE_OS + true | "debug" | Type.iOS | PathHelper.IPHONE_SIMULATOR + true | "release" | Type.iOS | PathHelper.IPHONE_SIMULATOR + false | "release" | Type.macOS | "release" + false | "debug" | Type.tvOS | PathHelper.APPLE_TV_OS + false | "release" | Type.tvOS | PathHelper.APPLE_TV_OS + true | "debug" | Type.tvOS | PathHelper.APPLE_TV_SIMULATOR + true | "release" | Type.tvOS | PathHelper.APPLE_TV_SIMULATOR } } diff --git a/plugin/build.gradle b/plugin/build.gradle index 1da57b09..83cac330 100644 --- a/plugin/build.gradle +++ b/plugin/build.gradle @@ -15,12 +15,11 @@ if (project.hasProperty("publishURL")) { if (project.hasProperty("publishUser")) { publishUser = project.publishUser } + if (project.hasProperty("publishPassword")) { publishPassword = project.publishPassword } - - cobertura.coverageFormats = ['html', 'xml'] apply plugin: 'maven' @@ -46,6 +45,7 @@ dependencies { compile 'org.pegdown:pegdown:1.5.+' compile 'org.openbakery.coverage:CoverageReport:0.9.4' deployerJars 'org.apache.maven.wagon:wagon-ssh:2.10' + compile 'de.undercouch:gradle-download-task:3.4.3' } jar { @@ -60,12 +60,44 @@ task sourcesJar(type: Jar) { classifier = 'sources' } +sourceSets { + test { + groovy { + srcDir file('src/test/groovy') + } + resources { + srcDir file('src/test/Resource') + } + } + + functionalTest { + groovy { + srcDir file('src/functionalTest/groovy') + } + resources { + srcDir file('src/functionalTest/resources') + } + compileClasspath += sourceSets.main.output + configurations.testRuntime + runtimeClasspath += output + compileClasspath + } +} + +task functionalTest(type: Test) { + testClassesDirs = sourceSets.functionalTest.output.classesDirs + classpath = sourceSets.functionalTest.runtimeClasspath +} + +check.dependsOn functionalTest + +gradlePlugin { + testSourceSets sourceSets.functionalTest +} uploadArchives { repositories { mavenDeployer { configuration = configurations.deployerJars - + repository(url: publishURL) { authentication(userName: publishUser, password: publishPassword) } diff --git a/plugin/src/functionalTest/groovy/org/openbakery/KeychainCreateTaskFunctionalTest.groovy b/plugin/src/functionalTest/groovy/org/openbakery/KeychainCreateTaskFunctionalTest.groovy new file mode 100644 index 00000000..71b02662 --- /dev/null +++ b/plugin/src/functionalTest/groovy/org/openbakery/KeychainCreateTaskFunctionalTest.groovy @@ -0,0 +1,162 @@ +package org.openbakery + +import org.gradle.testkit.runner.BuildResult +import org.gradle.testkit.runner.GradleRunner +import org.gradle.testkit.runner.TaskOutcome +import org.junit.Rule +import org.junit.rules.TemporaryFolder +import org.openbakery.signing.KeychainCreateTask +import spock.lang.Shared +import spock.lang.Specification + +import java.nio.file.Paths + +class KeychainCreateTaskFunctionalTest extends Specification { + + @Rule + final TemporaryFolder testProjectDir = new TemporaryFolder() + + List pluginClasspath + + @Shared + File certificate + + File buildFile + + def setup() { + def pluginClasspathResource = getClass().classLoader.findResource("plugin-classpath.txt") + if (pluginClasspathResource == null) { + throw new IllegalStateException("Did not find plugin classpath resource, run `testClasses` build task.") + } + + pluginClasspath = pluginClasspathResource.readLines().collect { new File(it) } + + certificate = findResource("fake_distribution.p12") + assert certificate.exists() + + buildFile = testProjectDir.newFile('build.gradle') + buildFile << """ + plugins { + id 'org.openbakery.xcode-plugin' + } + """ + } + + def "The task list should contain the task"() { + when: + def result = GradleRunner.create() + .withProjectDir(testProjectDir.root) + .withArguments('tasks') + .withPluginClasspath(pluginClasspath) + .build() + + then: + result.output.contains(KeychainCreateTask.TASK_NAME + + " - " + + KeychainCreateTask.TASK_DESCRIPTION) + } + + def "The task should be skipped if invalid configuration"() { + when: + BuildResult result = GradleRunner.create() + .withProjectDir(testProjectDir.root) + .withArguments(KeychainCreateTask.TASK_NAME) + .withPluginClasspath(pluginClasspath) + .build() + + then: + result.output.contains("No signing certificate defined, will skip the keychain creation") + result.output.contains("No signing certificate password defined, will skip the keychain creation") + result.task(":" + KeychainCreateTask.TASK_NAME).outcome == TaskOutcome.SKIPPED + } + + def "The task should be skipped if not certificate password is provided"() { + setup: + buildFile << """ + xcodebuild { + signing { + certificateURI = "$certificate.absolutePath" + } + } + """ + + when: + BuildResult result = GradleRunner.create() + .withProjectDir(testProjectDir.root) + .withArguments(KeychainCreateTask.TASK_NAME) + .withDebug(true) + .withPluginClasspath(pluginClasspath) + .build() + + then: + result.task(":" + KeychainCreateTask.TASK_NAME).outcome == TaskOutcome.SKIPPED + } + + def "The task should be executed if configuration is valid"() { + setup: + buildFile << """ + xcodebuild { + signing { + certificateURI = "${certificate.toURI().toString()}" + certificatePassword = "p4ssword" + } + } + """ + + when: + BuildResult result = GradleRunner.create() + .withProjectDir(testProjectDir.root) + .withArguments(KeychainCreateTask.TASK_NAME) + .withPluginClasspath(pluginClasspath) + .withDebug(true) + .build() + + then: + result.task(":" + KeychainCreateTask.TASK_NAME).outcome == TaskOutcome.SUCCESS + } + + def "The task should automatically delete the temporary keychain file"() { + setup: + buildFile << """ + xcodebuild { + signing { + certificateURI = "${certificate.toURI().toString()}" + certificatePassword = "p4ssword" + } + } + """ + + when: + BuildResult result = GradleRunner.create() + .withProjectDir(testProjectDir.root) + .withArguments(KeychainCreateTask.TASK_NAME) + .withPluginClasspath(pluginClasspath) + .build() + + then: + result.task(":" + KeychainCreateTask.TASK_NAME).outcome == TaskOutcome.SUCCESS + + and: "The temporary certificate provisioningFile1 should be deleted automatically" + new File(testProjectDir.root, "build/codesign") + .listFiles() + .toList() + .findAll {it.name.endsWith(".p12")} + .empty + + and: "The temporary keychain provisioningFile1 should be deleted automatically" + new File(testProjectDir.root, "build/codesign") + .listFiles() + .toList() + .findAll {it.name.endsWith(".keychain")} + .empty + } + + private File findResource(String name) { + ClassLoader classLoader = getClass().getClassLoader() + return (File) Optional.ofNullable(classLoader.getResource(name)) + .map { URL url -> url.toURI() } + .map { URI uri -> Paths.get(uri).toFile() } + .filter { File file -> file.exists() } + .orElseThrow { new Exception("Resource $name cannot be found") } + } +} diff --git a/plugin/src/functionalTest/groovy/org/openbakery/PackageTaskIosAndTvOSTest.groovy b/plugin/src/functionalTest/groovy/org/openbakery/PackageTaskIosAndTvOSTest.groovy new file mode 100644 index 00000000..83c7d69b --- /dev/null +++ b/plugin/src/functionalTest/groovy/org/openbakery/PackageTaskIosAndTvOSTest.groovy @@ -0,0 +1,49 @@ +package org.openbakery + +import org.gradle.testkit.runner.GradleRunner +import org.junit.Rule +import org.junit.rules.TemporaryFolder +import org.openbakery.packaging.PackageTaskIosAndTvOS +import spock.lang.Specification + +class PackageTaskIosAndTvOSTest extends Specification { + + @Rule + final TemporaryFolder testProjectDir = new TemporaryFolder() + + List pluginClasspath + + File buildFile + + def setup() { + buildFile = testProjectDir.newFile('build.gradle') + + def pluginClasspathResource = getClass().classLoader.findResource("plugin-classpath.txt") + if (pluginClasspathResource == null) { + throw new IllegalStateException("Did not find plugin classpath resource, run `testClasses` build task.") + } + + pluginClasspath = pluginClasspathResource.readLines().collect { new File(it) } + } + + def "The task list should contain the task"() { + given: + buildFile << """ + plugins { + id 'org.openbakery.xcode-plugin' + } + """ + + when: + def result = GradleRunner.create() + .withProjectDir(testProjectDir.root) + .withArguments('tasks') + .withPluginClasspath(pluginClasspath) + .build() + + then: + result.output.contains(PackageTaskIosAndTvOS.NAME + + " - " + + PackageTaskIosAndTvOS.DESCRIPTION) + } +} diff --git a/plugin/src/functionalTest/groovy/org/openbakery/PrepareXcodeArchivingFunctionalTest.groovy b/plugin/src/functionalTest/groovy/org/openbakery/PrepareXcodeArchivingFunctionalTest.groovy new file mode 100644 index 00000000..f1606a6d --- /dev/null +++ b/plugin/src/functionalTest/groovy/org/openbakery/PrepareXcodeArchivingFunctionalTest.groovy @@ -0,0 +1,193 @@ +package org.openbakery + +import org.apache.commons.io.FileUtils +import org.gradle.testkit.runner.BuildResult +import org.gradle.testkit.runner.GradleRunner +import org.gradle.testkit.runner.TaskOutcome +import org.junit.Rule +import org.junit.rules.TemporaryFolder +import org.openbakery.util.PathHelper +import spock.lang.Specification + +import java.nio.file.Paths + +class PrepareXcodeArchivingFunctionalTest extends Specification { + + @Rule + final TemporaryFolder testProjectDir = new TemporaryFolder() + + List pluginClasspath + + File buildFile + + + File provisioningFileWildCard + + def setup() { + pluginClasspath = findResource("plugin-classpath.txt") + .readLines() + .collect { new File(it) } + + FileUtils.copyDirectory(findResource("TestProject"), testProjectDir.getRoot()) + + provisioningFileWildCard = findResource("test1.mobileprovision") + } + + def setupBuildFile() { + buildFile = testProjectDir.newFile('build.gradle') + buildFile << """ + plugins { + id 'org.openbakery.xcode-plugin' + } + + xcodebuild { + target = 'TestProject' + scheme = "TestScheme" + signing { + mobileProvisionURI = "${provisioningFileWildCard.toURI().toString()}" + } + } + + """ + } + + def "The task list should contain the task"() { + given: + setupBuildFile() + + when: + def result = GradleRunner.create() + .withProjectDir(testProjectDir.root) + .withArguments('tasks') + .withPluginClasspath(pluginClasspath) + .build() + + then: + result.output.contains(PrepareXcodeArchivingTask.NAME + + " - " + + PrepareXcodeArchivingTask.DESCRIPTION) + } + + def "The task should complete without error and generate the xcconfig file"() { + setup: + setupBuildFile() + + when: + buildFile << """ + xcodebuild { + infoplist { + bundleIdentifier = "org.openbakery.test.ExampleWidget" + } + } + """ + + final File certificate = findResource("fake_distribution.p12") + assert certificate.exists() + buildFile << """ + xcodebuild { + infoplist { + bundleIdentifier = "org.openbakery.test.ExampleWidget" + } + + signing { + certificateURI = "${certificate.toURI().toString()}" + certificatePassword = "p4ssword" + } + } + """ + + BuildResult result = GradleRunner.create() + .withProjectDir(testProjectDir.root) + .withArguments(PrepareXcodeArchivingTask.NAME) + .withPluginClasspath(pluginClasspath) + .build() + + then: "The task should complete without error" + + result.task(":" + PrepareXcodeArchivingTask.NAME) + .outcome == TaskOutcome.SUCCESS + + and: "The archive xcconfig provisioningFile1 should be properly generated and populated from configured values" + + File outputFile = new File(testProjectDir.root, "build/" + + PathHelper.FOLDER_ARCHIVE + + "/" + PathHelper.GENERATED_XCARCHIVE_FILE_NAME) + + outputFile.exists() + + String text = outputFile.text + text.contains("PRODUCT_BUNDLE_IDENTIFIER = org.openbakery.test.ExampleWidget") + text.contains("iPhone Distribution: Test Company Name (12345ABCDE)") + text.contains("PROVISIONING_PROFILE = XXXXFFFF-AAAA-BBBB-CCCC-DDDDEEEEFFFF") + text.contains("PROVISIONING_PROFILE_SPECIFIER = ad hoc") + text.contains("DEVELOPMENT_TEAM = XXXYYYZZZZ") + + and: "Should no contain any entitlements information" + !text.contains("CODE_SIGN_ENTITLEMENTS =") + } + + def "If present the entitlements file should be present into the xcconfig file"() { + setup: + setupBuildFile() + + when: + buildFile << """ + xcodebuild { + infoplist { + bundleIdentifier = "org.openbakery.test.ExampleWidget" + } + } + """ + + final File certificate = findResource("fake_distribution.p12") + assert certificate.exists() + + final File entitlementsFile = findResource("fake.entitlements") + assert entitlementsFile.exists() + + buildFile << """ + xcodebuild { + infoplist { + bundleIdentifier = "org.openbakery.test.ExampleWidget" + } + + signing { + certificateURI = "${certificate.toURI().toString()}" + certificatePassword = "p4ssword" + entitlementsFile = "${entitlementsFile.absolutePath}" + } + } + """ + + BuildResult result = GradleRunner.create() + .withProjectDir(testProjectDir.root) + .withArguments(PrepareXcodeArchivingTask.NAME) + .withPluginClasspath(pluginClasspath) + .build() + + then: "The task should complete without error" + + result.task(":" + PrepareXcodeArchivingTask.NAME) + .outcome == TaskOutcome.SUCCESS + + and: "The archive xcconfig should contains the path to the entitlements provisioningFile1" + + File outputFile = new File(testProjectDir.root, "build/" + + PathHelper.FOLDER_ARCHIVE + + "/" + PathHelper.GENERATED_XCARCHIVE_FILE_NAME) + + outputFile.exists() + + String text = outputFile.text + text.contains("CODE_SIGN_ENTITLEMENTS = ${entitlementsFile.absolutePath}") + } + + private File findResource(String name) { + ClassLoader classLoader = getClass().getClassLoader() + return (File) Optional.ofNullable(classLoader.getResource(name)) + .map { URL url -> url.toURI() } + .map { URI uri -> Paths.get(uri).toFile() } + .filter { File file -> file.exists() } + .orElseThrow { new Exception("Resource $name cannot be found") } + } +} diff --git a/plugin/src/functionalTest/groovy/org/openbakery/carthage/CarthageBootStrapTaskFunctionalTest.groovy b/plugin/src/functionalTest/groovy/org/openbakery/carthage/CarthageBootStrapTaskFunctionalTest.groovy new file mode 100644 index 00000000..ec6a21fc --- /dev/null +++ b/plugin/src/functionalTest/groovy/org/openbakery/carthage/CarthageBootStrapTaskFunctionalTest.groovy @@ -0,0 +1,140 @@ +package org.openbakery.carthage + +import org.gradle.testkit.runner.BuildResult +import org.gradle.testkit.runner.GradleRunner +import org.gradle.testkit.runner.TaskOutcome +import org.junit.Rule +import org.junit.rules.TemporaryFolder +import spock.lang.Specification + +class CarthageBootStrapTaskFunctionalTest extends Specification { + @Rule + final TemporaryFolder testProjectDir = new TemporaryFolder() + + List pluginClasspath + File buildFile + GradleRunner gradleRunner + File carthageFolder + + void setup() { + buildFile = testProjectDir.newFile('build.gradle') + + buildFile << """ + plugins { + id 'org.openbakery.xcode-plugin' + } + """ + + def pluginClasspathResource = getClass().classLoader.findResource("plugin-classpath.txt") + if (pluginClasspathResource == null) { + throw new IllegalStateException("Did not find plugin classpath resource, run `testClasses` build task.") + } + + pluginClasspath = pluginClasspathResource.readLines().collect { new File(it) } + + gradleRunner = GradleRunner.create() + .withProjectDir(testProjectDir.root) + .withArguments(CarthageBootStrapTask.NAME) + .withPluginClasspath(pluginClasspath) + + carthageFolder = new File(testProjectDir.root, "Carthage") + } + + def "The task list should contain the task"() { + when: + BuildResult result = gradleRunner.build() + + then: + result.output.contains(CarthageBootStrapTask.NAME) + } + + def "The task should be skipped if no cartfile is present"() { + when: + BuildResult result = gradleRunner.build() + + then: + result.task(":" + CarthageBootStrapTask.NAME) + .outcome == TaskOutcome.SKIPPED + } + + def "The task should be executed with success if a `cartfile.resolved` file is present"() { + setup: + testProjectDir.newFile(CarthageBootStrapTask.CARTHAGE_FILE) + + when: + BuildResult result = gradleRunner.build() + + then: + result.task(":" + CarthageBootStrapTask.NAME) + .outcome == TaskOutcome.SUCCESS + } + + def "The task should resolve the defined carthage dependencies"() { + setup: + File carthageFile = testProjectDir.newFile("Cartfile") + carthageFile << """ + github "ashleymills/Reachability.swift" + """ + + File carthageResolvedFile = testProjectDir.newFile(CarthageBootStrapTask.CARTHAGE_FILE) + carthageResolvedFile << """ + github "ashleymills/Reachability.swift" "v4.1.0" + """ + + when: + BuildResult result = gradleRunner + .build() + + then: + result.task(":" + CarthageBootStrapTask.NAME) + .outcome == TaskOutcome.SUCCESS + + and: "The resolved framework should be existing only for iOS (default target)" + carthageFolder.exists() + new File(carthageFolder, "Build/iOS/Reachability.framework").exists() + !new File(carthageFolder, "Build/tvOS/Reachability.framework").exists() + + when: "Force reset and build with cache enabled" + assert carthageFolder.deleteDir() + result = gradleRunner.withArguments('--build-cache', CarthageBootStrapTask.NAME) + .build() + + then: "Should resolve the carthage dependencies from cache" + result.task(":" + CarthageBootStrapTask.NAME) + .outcome == TaskOutcome.FROM_CACHE + + new File(carthageFolder, "Build/iOS/Reachability.framework").exists() + !new File(carthageFolder, "Build/tvOS/Reachability.framework").exists() + } + + def "The task should resolve the defined carthage dependencies depending of the configured target"() { + setup: + File carthageFile = testProjectDir.newFile("Cartfile") + carthageFile << """ + github "ashleymills/Reachability.swift" + """ + + File carthageResolvedFile = testProjectDir.newFile(CarthageBootStrapTask.CARTHAGE_FILE) + carthageResolvedFile << """ + github "ashleymills/Reachability.swift" "v4.1.0" + """ + + buildFile << """ + xcodebuild { + type = "tvOS" + } + """ + + when: + BuildResult result = gradleRunner.build() + + then: + result.task(":" + CarthageBootStrapTask.NAME) + .outcome == TaskOutcome.SUCCESS + + and: "The resolved framework should be existing only for iOS (default target)" + carthageFolder.exists() + !new File(carthageFolder, "Build/iOS/Reachability.framework").exists() + new File(carthageFolder, "Build/tvOS/Reachability.framework").exists() + } +} diff --git a/plugin/src/functionalTest/groovy/org/openbakery/signing/ProvisioningInstallTaskFunctionalTest.groovy b/plugin/src/functionalTest/groovy/org/openbakery/signing/ProvisioningInstallTaskFunctionalTest.groovy new file mode 100644 index 00000000..16dce6f6 --- /dev/null +++ b/plugin/src/functionalTest/groovy/org/openbakery/signing/ProvisioningInstallTaskFunctionalTest.groovy @@ -0,0 +1,158 @@ +package org.openbakery.signing + +import org.gradle.testkit.runner.GradleRunner +import org.gradle.testkit.runner.TaskOutcome +import org.junit.Rule +import org.junit.rules.TemporaryFolder +import spock.lang.Specification +import spock.lang.Unroll + +import java.nio.file.Paths + +class ProvisioningInstallTaskFunctionalTest extends Specification { + @Rule + final TemporaryFolder testProjectDir = new TemporaryFolder() + + List pluginClasspath + + File buildFile + File provisioningFile1 + + def setup() { + buildFile = testProjectDir.newFile('build.gradle') + + buildFile << """ + plugins { + id 'org.openbakery.xcode-plugin' + } + """ + + provisioningFile1 = findResource("test1.mobileprovision") + assert provisioningFile1.exists() + + def pluginClasspathResource = getClass().classLoader + .findResource("plugin-classpath.txt") + + if (pluginClasspathResource == null) { + throw new IllegalStateException("Did not find plugin classpath resource, run `testClasses` build task.") + } + + pluginClasspath = pluginClasspathResource.readLines().collect { new File(it) } + } + + def "The task list should contain the task"() { + when: + def result = GradleRunner.create() + .withProjectDir(testProjectDir.root) + .withArguments('tasks') + .withPluginClasspath(pluginClasspath) + .build() + + then: + result.output.contains(ProvisioningInstallTask.TASK_NAME + + " - " + + ProvisioningInstallTask.TASK_DESCRIPTION) + } + + def "If no provisioning is defined, then the task should be skipped"() { + when: + def result = GradleRunner.create() + .withProjectDir(testProjectDir.root) + .withArguments(ProvisioningInstallTask.TASK_NAME) + .withPluginClasspath(pluginClasspath) + .build() + + then: + result.task(":" + ProvisioningInstallTask.TASK_NAME) + .outcome == TaskOutcome.SKIPPED + } + + def "If provisioning list is empty, then the task should be skipped"() { + setup: + buildFile << """ + xcodebuild { + signing { + mobileProvisionList = [] + } + } + """ + + when: + def result = GradleRunner.create() + .withProjectDir(testProjectDir.root) + .withArguments(ProvisioningInstallTask.TASK_NAME) + .withPluginClasspath(pluginClasspath) + .build() + + then: + result.task(":" + ProvisioningInstallTask.TASK_NAME) + .outcome == TaskOutcome.SKIPPED + } + + def "The provisioning can be configured via the mobileProvisionList list"() { + setup: + buildFile << """ + xcodebuild { + signing { + mobileProvisionList = ["${provisioningFile1.toURI().toString()}"] + } + } + """ + + when: + def result = GradleRunner.create() + .withProjectDir(testProjectDir.root) + .withArguments(ProvisioningInstallTask.TASK_NAME) + .withPluginClasspath(pluginClasspath) + .withDebug(true) + .build() + + then: + result.task(":" + ProvisioningInstallTask.TASK_NAME) + .outcome == TaskOutcome.SUCCESS + } + + @Unroll + def "With gradle version : #gradleVersion If provisioning list is present, then the task should be skipped"() { + setup: + buildFile << """ + xcodebuild { + signing { + mobileProvisionURI = "${provisioningFile1.toURI().toString()}" + } + } + """ + + when: + def result = GradleRunner.create() + .withProjectDir(testProjectDir.root) + .withArguments(ProvisioningInstallTask.TASK_NAME) + .withPluginClasspath(pluginClasspath) + .withGradleVersion(gradleVersion) + .build() + + then: + result.task(":" + ProvisioningInstallTask.TASK_NAME) + .outcome == TaskOutcome.SUCCESS + + and: "The temporary provisioning provisioningFile1 should be deleted" + new File(testProjectDir.root, "build/provision") + .listFiles().size() == 0 + + where: + gradleVersion | _ + "4.4" | _ + "4.5" | _ + "4.6" | _ + "4.7" | _ + } + + private File findResource(String name) { + ClassLoader classLoader = getClass().getClassLoader() + return (File) Optional.ofNullable(classLoader.getResource(name)) + .map { URL url -> url.toURI() } + .map { URI uri -> Paths.get(uri).toFile() } + .filter { File file -> file.exists() } + .orElseThrow { new Exception("Resource $name cannot be found") } + } +} diff --git a/plugin/src/functionalTest/resources/TestProject/TestProject.xcodeproj/project.pbxproj b/plugin/src/functionalTest/resources/TestProject/TestProject.xcodeproj/project.pbxproj new file mode 100644 index 00000000..ea040bd5 --- /dev/null +++ b/plugin/src/functionalTest/resources/TestProject/TestProject.xcodeproj/project.pbxproj @@ -0,0 +1,337 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 50; + objects = { + +/* Begin PBXBuildFile section */ + BFD99BC5209C6A8800AF800E /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFD99BC4209C6A8800AF800E /* AppDelegate.swift */; }; + BFD99BC7209C6A8800AF800E /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFD99BC6209C6A8800AF800E /* ViewController.swift */; }; + BFD99BCA209C6A8800AF800E /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = BFD99BC8209C6A8800AF800E /* Main.storyboard */; }; + BFD99BCC209C6A8A00AF800E /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = BFD99BCB209C6A8A00AF800E /* Assets.xcassets */; }; + BFD99BCF209C6A8A00AF800E /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = BFD99BCD209C6A8A00AF800E /* LaunchScreen.storyboard */; }; +/* End PBXBuildFile section */ + +/* Begin PBXFileReference section */ + BFD99BC1209C6A8800AF800E /* TestProject.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = TestProject.app; sourceTree = BUILT_PRODUCTS_DIR; }; + BFD99BC4209C6A8800AF800E /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + BFD99BC6209C6A8800AF800E /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; + BFD99BC9209C6A8800AF800E /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; + BFD99BCB209C6A8A00AF800E /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + BFD99BCE209C6A8A00AF800E /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; + BFD99BD0209C6A8A00AF800E /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + BFD99BBE209C6A8800AF800E /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + BFD99BB8209C6A8700AF800E = { + isa = PBXGroup; + children = ( + BFD99BC3209C6A8800AF800E /* TestProject */, + BFD99BC2209C6A8800AF800E /* Products */, + ); + sourceTree = ""; + }; + BFD99BC2209C6A8800AF800E /* Products */ = { + isa = PBXGroup; + children = ( + BFD99BC1209C6A8800AF800E /* TestProject.app */, + ); + name = Products; + sourceTree = ""; + }; + BFD99BC3209C6A8800AF800E /* TestProject */ = { + isa = PBXGroup; + children = ( + BFD99BC4209C6A8800AF800E /* AppDelegate.swift */, + BFD99BC6209C6A8800AF800E /* ViewController.swift */, + BFD99BC8209C6A8800AF800E /* Main.storyboard */, + BFD99BCB209C6A8A00AF800E /* Assets.xcassets */, + BFD99BCD209C6A8A00AF800E /* LaunchScreen.storyboard */, + BFD99BD0209C6A8A00AF800E /* Info.plist */, + ); + path = TestProject; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + BFD99BC0209C6A8800AF800E /* TestProject */ = { + isa = PBXNativeTarget; + buildConfigurationList = BFD99BD3209C6A8A00AF800E /* Build configuration list for PBXNativeTarget "TestProject" */; + buildPhases = ( + BFD99BBD209C6A8800AF800E /* Sources */, + BFD99BBE209C6A8800AF800E /* Frameworks */, + BFD99BBF209C6A8800AF800E /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = TestProject; + productName = TestProject; + productReference = BFD99BC1209C6A8800AF800E /* TestProject.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + BFD99BB9209C6A8700AF800E /* Project object */ = { + isa = PBXProject; + attributes = { + LastSwiftUpdateCheck = 0930; + LastUpgradeCheck = 0930; + ORGANIZATIONNAME = Massive; + TargetAttributes = { + BFD99BC0209C6A8800AF800E = { + CreatedOnToolsVersion = 9.3; + }; + }; + }; + buildConfigurationList = BFD99BBC209C6A8700AF800E /* Build configuration list for PBXProject "TestProject" */; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = BFD99BB8209C6A8700AF800E; + productRefGroup = BFD99BC2209C6A8800AF800E /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + BFD99BC0209C6A8800AF800E /* TestProject */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + BFD99BBF209C6A8800AF800E /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + BFD99BCF209C6A8A00AF800E /* LaunchScreen.storyboard in Resources */, + BFD99BCC209C6A8A00AF800E /* Assets.xcassets in Resources */, + BFD99BCA209C6A8800AF800E /* Main.storyboard in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + BFD99BBD209C6A8800AF800E /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + BFD99BC7209C6A8800AF800E /* ViewController.swift in Sources */, + BFD99BC5209C6A8800AF800E /* AppDelegate.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXVariantGroup section */ + BFD99BC8209C6A8800AF800E /* Main.storyboard */ = { + isa = PBXVariantGroup; + children = ( + BFD99BC9209C6A8800AF800E /* Base */, + ); + name = Main.storyboard; + sourceTree = ""; + }; + BFD99BCD209C6A8A00AF800E /* LaunchScreen.storyboard */ = { + isa = PBXVariantGroup; + children = ( + BFD99BCE209C6A8A00AF800E /* Base */, + ); + name = LaunchScreen.storyboard; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + BFD99BD1209C6A8A00AF800E /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + CODE_SIGN_IDENTITY = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 11.3; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + BFD99BD2209C6A8A00AF800E /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + CODE_SIGN_IDENTITY = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 11.3; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + BFD99BD4209C6A8A00AF800E /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CODE_SIGN_STYLE = Automatic; + INFOPLIST_FILE = TestProject/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = gradle.xcode.test.TestProject; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 4.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + BFD99BD5209C6A8A00AF800E /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CODE_SIGN_STYLE = Automatic; + INFOPLIST_FILE = TestProject/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = gradle.xcode.test.TestProject; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 4.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + BFD99BBC209C6A8700AF800E /* Build configuration list for PBXProject "TestProject" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + BFD99BD1209C6A8A00AF800E /* Debug */, + BFD99BD2209C6A8A00AF800E /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + BFD99BD3209C6A8A00AF800E /* Build configuration list for PBXNativeTarget "TestProject" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + BFD99BD4209C6A8A00AF800E /* Debug */, + BFD99BD5209C6A8A00AF800E /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = BFD99BB9209C6A8700AF800E /* Project object */; +} diff --git a/plugin/src/functionalTest/resources/TestProject/TestProject.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/plugin/src/functionalTest/resources/TestProject/TestProject.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 00000000..2bf8bd93 --- /dev/null +++ b/plugin/src/functionalTest/resources/TestProject/TestProject.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/plugin/src/functionalTest/resources/TestProject/TestProject.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/plugin/src/functionalTest/resources/TestProject/TestProject.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 00000000..18d98100 --- /dev/null +++ b/plugin/src/functionalTest/resources/TestProject/TestProject.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/plugin/src/functionalTest/resources/TestProject/TestProject/AppDelegate.swift b/plugin/src/functionalTest/resources/TestProject/TestProject/AppDelegate.swift new file mode 100644 index 00000000..0e25d993 --- /dev/null +++ b/plugin/src/functionalTest/resources/TestProject/TestProject/AppDelegate.swift @@ -0,0 +1,46 @@ +// +// AppDelegate.swift +// TestProject +// +// Created by Johann Martinache on 04/05/2018. +// Copyright © 2018 Massive. All rights reserved. +// + +import UIKit + +@UIApplicationMain +class AppDelegate: UIResponder, UIApplicationDelegate { + + var window: UIWindow? + + + func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool { + // Override point for customization after application launch. + return true + } + + func applicationWillResignActive(_ application: UIApplication) { + // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state. + // Use this method to pause ongoing tasks, disable timers, and invalidate graphics rendering callbacks. Games should use this method to pause the game. + } + + func applicationDidEnterBackground(_ application: UIApplication) { + // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later. + // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits. + } + + func applicationWillEnterForeground(_ application: UIApplication) { + // Called as part of the transition from the background to the active state; here you can undo many of the changes made on entering the background. + } + + func applicationDidBecomeActive(_ application: UIApplication) { + // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface. + } + + func applicationWillTerminate(_ application: UIApplication) { + // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. + } + + +} + diff --git a/plugin/src/functionalTest/resources/TestProject/TestProject/Assets.xcassets/AppIcon.appiconset/Contents.json b/plugin/src/functionalTest/resources/TestProject/TestProject/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 00000000..d8db8d65 --- /dev/null +++ b/plugin/src/functionalTest/resources/TestProject/TestProject/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,98 @@ +{ + "images" : [ + { + "idiom" : "iphone", + "size" : "20x20", + "scale" : "2x" + }, + { + "idiom" : "iphone", + "size" : "20x20", + "scale" : "3x" + }, + { + "idiom" : "iphone", + "size" : "29x29", + "scale" : "2x" + }, + { + "idiom" : "iphone", + "size" : "29x29", + "scale" : "3x" + }, + { + "idiom" : "iphone", + "size" : "40x40", + "scale" : "2x" + }, + { + "idiom" : "iphone", + "size" : "40x40", + "scale" : "3x" + }, + { + "idiom" : "iphone", + "size" : "60x60", + "scale" : "2x" + }, + { + "idiom" : "iphone", + "size" : "60x60", + "scale" : "3x" + }, + { + "idiom" : "ipad", + "size" : "20x20", + "scale" : "1x" + }, + { + "idiom" : "ipad", + "size" : "20x20", + "scale" : "2x" + }, + { + "idiom" : "ipad", + "size" : "29x29", + "scale" : "1x" + }, + { + "idiom" : "ipad", + "size" : "29x29", + "scale" : "2x" + }, + { + "idiom" : "ipad", + "size" : "40x40", + "scale" : "1x" + }, + { + "idiom" : "ipad", + "size" : "40x40", + "scale" : "2x" + }, + { + "idiom" : "ipad", + "size" : "76x76", + "scale" : "1x" + }, + { + "idiom" : "ipad", + "size" : "76x76", + "scale" : "2x" + }, + { + "idiom" : "ipad", + "size" : "83.5x83.5", + "scale" : "2x" + }, + { + "idiom" : "ios-marketing", + "size" : "1024x1024", + "scale" : "1x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/plugin/src/functionalTest/resources/TestProject/TestProject/Assets.xcassets/Contents.json b/plugin/src/functionalTest/resources/TestProject/TestProject/Assets.xcassets/Contents.json new file mode 100644 index 00000000..da4a164c --- /dev/null +++ b/plugin/src/functionalTest/resources/TestProject/TestProject/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/plugin/src/functionalTest/resources/TestProject/TestProject/Base.lproj/LaunchScreen.storyboard b/plugin/src/functionalTest/resources/TestProject/TestProject/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 00000000..f83f6fd5 --- /dev/null +++ b/plugin/src/functionalTest/resources/TestProject/TestProject/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/plugin/src/functionalTest/resources/TestProject/TestProject/Base.lproj/Main.storyboard b/plugin/src/functionalTest/resources/TestProject/TestProject/Base.lproj/Main.storyboard new file mode 100644 index 00000000..03c13c22 --- /dev/null +++ b/plugin/src/functionalTest/resources/TestProject/TestProject/Base.lproj/Main.storyboard @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/plugin/src/functionalTest/resources/TestProject/TestProject/Info.plist b/plugin/src/functionalTest/resources/TestProject/TestProject/Info.plist new file mode 100644 index 00000000..16be3b68 --- /dev/null +++ b/plugin/src/functionalTest/resources/TestProject/TestProject/Info.plist @@ -0,0 +1,45 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + APPL + CFBundleShortVersionString + 1.0 + CFBundleVersion + 1 + LSRequiresIPhoneOS + + UILaunchStoryboardName + LaunchScreen + UIMainStoryboardFile + Main + UIRequiredDeviceCapabilities + + armv7 + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + + diff --git a/plugin/src/functionalTest/resources/TestProject/TestProject/ViewController.swift b/plugin/src/functionalTest/resources/TestProject/TestProject/ViewController.swift new file mode 100644 index 00000000..65114f2e --- /dev/null +++ b/plugin/src/functionalTest/resources/TestProject/TestProject/ViewController.swift @@ -0,0 +1,25 @@ +// +// ViewController.swift +// TestProject +// +// Created by Johann Martinache on 04/05/2018. +// Copyright © 2018 Massive. All rights reserved. +// + +import UIKit + +class ViewController: UIViewController { + + override func viewDidLoad() { + super.viewDidLoad() + // Do any additional setup after loading the view, typically from a nib. + } + + override func didReceiveMemoryWarning() { + super.didReceiveMemoryWarning() + // Dispose of any resources that can be recreated. + } + + +} + diff --git a/plugin/src/functionalTest/resources/fake.entitlements b/plugin/src/functionalTest/resources/fake.entitlements new file mode 100644 index 00000000..e69de29b diff --git a/plugin/src/functionalTest/resources/fake_distribution.p12 b/plugin/src/functionalTest/resources/fake_distribution.p12 new file mode 100644 index 00000000..208b05e0 Binary files /dev/null and b/plugin/src/functionalTest/resources/fake_distribution.p12 differ diff --git a/plugin/src/functionalTest/resources/test1.mobileprovision b/plugin/src/functionalTest/resources/test1.mobileprovision new file mode 100644 index 00000000..d8fded0c --- /dev/null +++ b/plugin/src/functionalTest/resources/test1.mobileprovision @@ -0,0 +1,46 @@ +// a Dummy Mobile Provistioning File to test the ProvisioningProfileIDReader + + + + ApplicationIdentifierPrefix + + AAAAAAAAAAA + + CreationDate + 2011-06-16T08:57:51Z + DeveloperCertificates + + + + + Entitlements + + application-identifier + AAAAAAAAAAA.org.openbakery.test.ExampleWidget + get-task-allow + + keychain-access-groups + + AAAAAAAAAAA.* + + + ExpirationDate + 2079-07-04T08:57:51Z + Name + ad hoc + ProvisionedDevices + + 1234 + + TimeToLive + 24855 + UUID + XXXXFFFF-AAAA-BBBB-CCCC-DDDDEEEEFFFF + Version + 1 + TeamIdentifier + + XXXYYYZZZZ + + + \ No newline at end of file diff --git a/plugin/src/main/groovy/org/openbakery/AbstractDistributeTask.groovy b/plugin/src/main/groovy/org/openbakery/AbstractDistributeTask.groovy index e2a36996..a0120cb1 100644 --- a/plugin/src/main/groovy/org/openbakery/AbstractDistributeTask.groovy +++ b/plugin/src/main/groovy/org/openbakery/AbstractDistributeTask.groovy @@ -1,15 +1,14 @@ package org.openbakery import org.apache.commons.io.FileUtils -import org.openbakery.packaging.PackageTask +import org.openbakery.util.PathHelper import java.util.regex.Pattern - /** * User: rene * Date: 11/11/14 */ -class AbstractDistributeTask extends AbstractXcodeTask { +class AbstractDistributeTask extends AbstractXcodeBuildTask { private File archiveDirectory; @@ -89,7 +88,7 @@ class AbstractDistributeTask extends AbstractXcodeTask { } File getBundle(String extension) { - File packageDirectory = new File(project.getBuildDir(), PackageTask.PACKAGE_PATH) + File packageDirectory = PathHelper.resolvePackageFolder(project) if (!packageDirectory.exists()) { throw new IllegalStateException("package does not exist: " + packageDirectory) @@ -132,7 +131,7 @@ class AbstractDistributeTask extends AbstractXcodeTask { if (archiveDirectory != null) { return archiveDirectory; } - File archiveDirectory = new File(project.getBuildDir(), XcodeBuildArchiveTask.ARCHIVE_FOLDER) + File archiveDirectory = PathHelper.resolveArchiveFolder(project) if (!archiveDirectory.exists()) { throw new IllegalStateException("Archive does not exist: " + archiveDirectory) } diff --git a/plugin/src/main/groovy/org/openbakery/AbstractXcodeBuildTask.groovy b/plugin/src/main/groovy/org/openbakery/AbstractXcodeBuildTask.groovy index edb69c97..b50c49a7 100644 --- a/plugin/src/main/groovy/org/openbakery/AbstractXcodeBuildTask.groovy +++ b/plugin/src/main/groovy/org/openbakery/AbstractXcodeBuildTask.groovy @@ -1,35 +1,40 @@ package org.openbakery +import groovy.transform.TypeChecked import org.gradle.api.logging.LogLevel import org.gradle.internal.logging.progress.ProgressLogger import org.gradle.internal.logging.progress.ProgressLoggerFactory import org.gradle.internal.logging.text.StyledTextOutput import org.gradle.internal.logging.text.StyledTextOutputFactory import org.gradle.util.ConfigureUtil -import org.openbakery.codesign.Security +import org.openbakery.codesign.ProvisioningProfileReader import org.openbakery.output.XcodeBuildOutputAppender import org.openbakery.xcode.Destination import org.openbakery.xcode.Devices import org.openbakery.xcode.Type import org.openbakery.xcode.XcodebuildParameters +import java.util.regex.Matcher +import java.util.regex.Pattern + /** * User: rene * Date: 15.07.13 * Time: 11:57 */ +@TypeChecked abstract class AbstractXcodeBuildTask extends AbstractXcodeTask { XcodebuildParameters parameters = new XcodebuildParameters() - private List destinationsCache + private static final Pattern PATTERN = ~/^\s{4}friendlyName:\s(?[^\n]+)/ + AbstractXcodeBuildTask() { super() } - void setTarget(String target) { parameters.target = target } @@ -90,10 +95,96 @@ abstract class AbstractXcodeBuildTask extends AbstractXcodeTask { XcodeBuildOutputAppender createXcodeBuildOutputAppender(String name) { - StyledTextOutput output = getServices().get(StyledTextOutputFactory.class).create(XcodeBuildTask.class, LogLevel.LIFECYCLE); - ProgressLoggerFactory progressLoggerFactory = getServices().get(ProgressLoggerFactory.class); - ProgressLogger progressLogger = progressLoggerFactory.newOperation(XcodeBuildTask.class).start(name, name); + StyledTextOutput output = getServices().get(StyledTextOutputFactory.class).create(XcodeBuildTask.class, LogLevel.LIFECYCLE) + ProgressLoggerFactory progressLoggerFactory = getServices().get(ProgressLoggerFactory.class) + ProgressLogger progressLogger = progressLoggerFactory.newOperation(XcodeBuildTask.class).start(name, name) return new XcodeBuildOutputAppender(progressLogger, output) } + XcodeBuildPluginExtension getXcodeExtension() { + return project.getExtensions() + .getByType(XcodeBuildPluginExtension.class) + } + + String getBundleIdentifier() { + File infoPlist = new File(project.projectDir, getXcodeExtension().infoPlist) + return plistHelper.getValueFromPlist(infoPlist, "CFBundleIdentifier") + } + + InfoPlistExtension getInfoPlistExtension() { + return project.getExtensions().getByType(InfoPlistExtension.class) + } + + File getProvisioningFile() { + List provisioningList = getProvisioningUriList() + .collect { it -> new File(new URI(it)) } + + println "provisioningList : " + provisioningList + + return Optional.ofNullable(ProvisioningProfileReader.getProvisionFileForIdentifier(bundleIdentifier, + provisioningList, + commandRunner, + plistHelper)).orElseThrow { + new IllegalArgumentException("Cannot resolve a valid provisioning " + + "profile for bundle identifier : " + bundleIdentifier) + } + } + + List getProvisioningUriList() { + return getXcodeExtension().signing.mobileProvisionList.get() + } + + String getSignatureFriendlyName() { + return Optional.ofNullable(getKeyContent() + .split(System.getProperty("line.separator")) + .find { PATTERN.matcher(it).matches() }) + .map { PATTERN.matcher(it) } + .filter { Matcher it -> it.matches() } + .map { Matcher it -> + return it.group("friendlyName") + } + .orElseThrow { + new IllegalArgumentException("Failed to resolve the code signing identity from the certificate ") + } + } + + private String getKeyContent() { + final String certificatePassword = getCertificatePassword() + File file = xcodeExtension.signing + .certificate + .get() + .asFile + + return commandRunner.runWithResult(["openssl", + "pkcs12", + "-nokeys", + "-in", + file.absolutePath, + "-passin", + "pass:" + certificatePassword]) + } + + private File getCertificateFile() { + return new File(URI.create(getCertificateString())) + } + + private String getCertificateString() throws IllegalArgumentException { +// return Optional.ofNullable(getXcodeExtension().signing.certificate) +// .filter { String it -> it != null && !it.isEmpty() } +// .orElseThrow { +// new IllegalArgumentException("The certificateURI value is null or empty : " + getXcodeExtension().signing.certificateURI) +// } + + return getXcodeExtension() + .signing + .certificate + } + + private String getCertificatePassword() { + return Optional.ofNullable(getXcodeExtension().signing.certificatePassword.getOrNull()) + .filter { it != null && !it.isEmpty() } + .orElseThrow { + new IllegalArgumentException("The signing certificate password is not defined") + } + } } diff --git a/plugin/src/main/groovy/org/openbakery/AbstractXcodeTask.groovy b/plugin/src/main/groovy/org/openbakery/AbstractXcodeTask.groovy index 33394036..8528ef94 100644 --- a/plugin/src/main/groovy/org/openbakery/AbstractXcodeTask.groovy +++ b/plugin/src/main/groovy/org/openbakery/AbstractXcodeTask.groovy @@ -15,20 +15,17 @@ */ package org.openbakery - import org.apache.commons.io.FilenameUtils import org.apache.commons.lang.StringUtils import org.gradle.api.DefaultTask import org.openbakery.bundle.ApplicationBundle import org.openbakery.codesign.Security import org.openbakery.simulators.SimulatorControl +import org.openbakery.util.PlistHelper import org.openbakery.xcode.DestinationResolver -import org.openbakery.xcode.Version import org.openbakery.xcode.Xcode -import org.openbakery.util.PlistHelper import java.text.SimpleDateFormat - /** * * @author René Pirringer @@ -52,7 +49,6 @@ abstract class AbstractXcodeTask extends DefaultTask { security = new Security(commandRunner) } - /** * Copies a file to a new location * @@ -77,7 +73,7 @@ abstract class AbstractXcodeTask extends DefaultTask { ant.exec(failonerror: "true", - executable: 'ditto') { + executable: 'ditto') { arg(value: source.absolutePath) arg(value: destinationPath.absolutePath) } @@ -102,7 +98,7 @@ abstract class AbstractXcodeTask extends DefaultTask { } try { - ant.get(src: address, dest: toDirectory.getPath(), verbose:true) + ant.get(src: address, dest: toDirectory.getPath(), verbose: true) } catch (Exception ex) { logger.error("cannot download file from the given location: {}", address) throw ex @@ -112,23 +108,6 @@ abstract class AbstractXcodeTask extends DefaultTask { return destinationFile.absolutePath } - def getOSVersion() { - Version result = new Version() - String versionString = System.getProperty("os.version") - Scanner scanner = new Scanner(versionString).useDelimiter("\\.") - if (scanner.hasNext()) { - result.major = scanner.nextInt() - } - if (scanner.hasNext()) { - result.minor = scanner.nextInt() - } - if (scanner.hasNext()) { - result.maintenance = scanner.nextInt(); - } - return result - } - - def createZip(File fileToZip) { File zipFile = new File(fileToZip.parentFile, FilenameUtils.getBaseName(fileToZip.getName()) + ".zip") createZip(zipFile, zipFile.parentFile, fileToZip); @@ -151,7 +130,7 @@ abstract class AbstractXcodeTask extends DefaultTask { logger.debug("baseDirectory: {} ", baseDirectory) for (File file : filesToZip) { - logger.debug("create of: {}: {}", file, file.exists() ) + logger.debug("create of: {}: {}", file, file.exists()) } def arguments = [] @@ -166,8 +145,8 @@ abstract class AbstractXcodeTask extends DefaultTask { logger.debug("arguments: {}", arguments) ant.exec(failonerror: 'true', - executable: '/usr/bin/zip', - dir: baseDirectory) { + executable: '/usr/bin/zip', + dir: baseDirectory) { for (def argument : arguments) { arg(value: argument) @@ -189,7 +168,7 @@ abstract class AbstractXcodeTask extends DefaultTask { List getAppBundles(File appPath) { - ApplicationBundle applicationBundle = new ApplicationBundle(new File(appPath,project.xcodebuild.applicationBundle.name), project.xcodebuild.type, project.xcodebuild.simulator) + ApplicationBundle applicationBundle = new ApplicationBundle(new File(appPath, project.xcodebuild.applicationBundle.name), project.xcodebuild.type, project.xcodebuild.simulator) return applicationBundle.getBundles() } @@ -202,11 +181,18 @@ abstract class AbstractXcodeTask extends DefaultTask { Xcode getXcode() { if (xcode == null) { - xcode = new Xcode(commandRunner, project.xcodebuild.xcodeVersion) + xcode = new Xcode(commandRunner, getProjectXcodeVersion()) } return xcode } + String getProjectXcodeVersion() { + return project.extensions. + getByType(XcodeBuildPluginExtension). + version. + getOrNull() + } + DestinationResolver getDestinationResolver() { if (destinationResolver == null) { destinationResolver = new DestinationResolver(getSimulatorControl()) diff --git a/plugin/src/main/groovy/org/openbakery/InfoPlistExtension.groovy b/plugin/src/main/groovy/org/openbakery/InfoPlistExtension.groovy index 069ac692..17d237de 100644 --- a/plugin/src/main/groovy/org/openbakery/InfoPlistExtension.groovy +++ b/plugin/src/main/groovy/org/openbakery/InfoPlistExtension.groovy @@ -15,21 +15,28 @@ */ package org.openbakery +import org.gradle.api.Project +import org.gradle.api.provider.Property + class InfoPlistExtension { - def String bundleIdentifier = null - def String bundleIdentifierSuffix = null - def String bundleName = null - def String bundleDisplayName = null - def String bundleDisplayNameSuffix = null - def String version = null - def String versionSuffix = null - def String versionPrefix = null - def String shortVersionString = null - def String shortVersionStringSuffix = null - def String shortVersionStringPrefix = null - def List commands = null + String bundleIdentifier = null + String bundleIdentifierSuffix = null + String bundleName = null + String bundleDisplayName = null + String bundleDisplayNameSuffix = null + String version = null + String versionSuffix = null + String versionPrefix = null + String shortVersionString = null + String shortVersionStringSuffix = null + String shortVersionStringPrefix = null + List commands = null + final Property configurationBundleIdentifier + InfoPlistExtension(Project project) { + this.configurationBundleIdentifier = project.objects.property(String) + } void setCommands(Object commands) { if (commands instanceof List) { @@ -42,16 +49,16 @@ class InfoPlistExtension { boolean hasValuesToModify() { return bundleIdentifier != null || - bundleIdentifierSuffix != null || - bundleName != null || - bundleDisplayName != null || - bundleDisplayNameSuffix != null || - version != null || - versionSuffix != null || - versionPrefix != null || - shortVersionString != null || - shortVersionStringSuffix != null || - shortVersionStringPrefix != null || - commands != null; + bundleIdentifierSuffix != null || + bundleName != null || + bundleDisplayName != null || + bundleDisplayNameSuffix != null || + version != null || + versionSuffix != null || + versionPrefix != null || + shortVersionString != null || + shortVersionStringSuffix != null || + shortVersionStringPrefix != null || + commands != null; } -} \ No newline at end of file +} diff --git a/plugin/src/main/groovy/org/openbakery/InfoPlistModifyTask.groovy b/plugin/src/main/groovy/org/openbakery/InfoPlistModifyTask.groovy index 97116028..51b972a5 100644 --- a/plugin/src/main/groovy/org/openbakery/InfoPlistModifyTask.groovy +++ b/plugin/src/main/groovy/org/openbakery/InfoPlistModifyTask.groovy @@ -15,24 +15,24 @@ */ package org.openbakery +import org.gradle.api.provider.Provider import org.gradle.api.tasks.TaskAction - class InfoPlistModifyTask extends AbstractDistributeTask { File infoPlist Boolean modfied = false + public final Provider configurationBundleIdentifier = project.objects.property(String) + + public static final String KeyBundleIdentifier = "CFBundleIdentifier" + public InfoPlistModifyTask() { dependsOn(XcodePlugin.XCODE_CONFIG_TASK_NAME) } - - - @TaskAction def prepare() { - if (!project.infoplist.hasValuesToModify()) { logger.debug("Nothing to modify") return; @@ -48,15 +48,7 @@ class InfoPlistModifyTask extends AbstractDistributeTask { logger.debug("Try to updating {}", infoPlist) - if (project.infoplist.bundleIdentifier != null) { - setValueForPlist("CFBundleIdentifier", project.infoplist.bundleIdentifier) - } - - // add suffix to bundleIdentifier - if (project.infoplist.bundleIdentifierSuffix != null) { - def bundleIdentifier = plistHelper.getValueFromPlist(infoPlist, "CFBundleIdentifier") - setValueForPlist("CFBundleIdentifier", bundleIdentifier + project.infoplist.bundleIdentifierSuffix) - } + modifyBundleIdentifier() // Modify bundle bundleName if (project.infoplist.bundleName != null) { @@ -80,7 +72,7 @@ class InfoPlistModifyTask extends AbstractDistributeTask { modifyShortVersion(infoPlist) - for(String command in project.infoplist.commands) { + for (String command in project.infoplist.commands) { setValueForPlist(command) } @@ -89,6 +81,8 @@ class InfoPlistModifyTask extends AbstractDistributeTask { } else { logger.debug("Nothing was modified!") } + + configurationBundleIdentifier.set(getBundleIdentifier()) } private void modifyVersion(File infoPlist) { @@ -143,22 +137,39 @@ class InfoPlistModifyTask extends AbstractDistributeTask { logger.debug("Modify CFBundleShortVersionString to {}", shortVersionString) setValueForPlist("CFBundleShortVersionString", shortVersionString) - } + private void modifyBundleIdentifier() { + // Resolve bundle identifier + XcodeBuildPluginExtension xcodeExtension = getXcodeExtension() + InfoPlistExtension extension = getInfoPlistExtension() + + if (extension.bundleIdentifier != null) { + setValueForPlist(KeyBundleIdentifier, extension.bundleIdentifier) + } else { + xcodeExtension.getBuildTargetConfiguration(xcodeExtension.scheme.getOrNull(), + xcodeExtension.configuration) + .map { it -> it.bundleIdentifier } + .ifPresent { it -> setValueForPlist(KeyBundleIdentifier, it) } + } + + // Add suffix to bundleIdentifier if defined + if (extension.bundleIdentifierSuffix != null) { + String bundleIdentifier = plistHelper.getValueFromPlist(infoPlist, KeyBundleIdentifier) + setValueForPlist(KeyBundleIdentifier, bundleIdentifier + extension.bundleIdentifierSuffix) + } + } void setValueForPlist(String key, String value) { modfied = true logger.lifecycle("Set {} to {}", key, value) plistHelper.setValueForPlist(infoPlist, key, value) - } - void setValueForPlist(String command) { modfied = true logger.lifecycle("Set {}", command) plistHelper.commandForPlist(infoPlist, command) } -} \ No newline at end of file +} diff --git a/plugin/src/main/groovy/org/openbakery/PrepareXcodeArchivingTask.groovy b/plugin/src/main/groovy/org/openbakery/PrepareXcodeArchivingTask.groovy new file mode 100644 index 00000000..57364b9c --- /dev/null +++ b/plugin/src/main/groovy/org/openbakery/PrepareXcodeArchivingTask.groovy @@ -0,0 +1,116 @@ +package org.openbakery + +import groovy.transform.CompileStatic +import org.gradle.api.DefaultTask +import org.gradle.api.Transformer +import org.gradle.api.file.RegularFile +import org.gradle.api.provider.ListProperty +import org.gradle.api.provider.Property +import org.gradle.api.provider.Provider +import org.gradle.api.tasks.* +import org.openbakery.codesign.ProvisioningProfileReader +import org.openbakery.signing.KeychainCreateTask +import org.openbakery.signing.ProvisioningInstallTask +import org.openbakery.util.PlistHelper + +@CompileStatic +class PrepareXcodeArchivingTask extends DefaultTask { + + @InputFile + @Optional + final Provider entitlementsFile = newInputFile() + + @OutputFile + final Provider outputFile = newOutputFile() + + final ListProperty registeredProvisioningFiles = project.objects.listProperty(File) + final Property commandRunnerProperty = project.objects.property(CommandRunner) + final Property provisioningForConfiguration = project.objects.property(File) + final Property plistHelperProperty = project.objects.property(PlistHelper) + final Property certificateFriendlyName = project.objects.property(String) + final Property configurationBundleIdentifier = project.objects.property(String) + final Property entitlementsFilePath = project.objects.property(String) + + @Internal + final Property provisioningReader = project.objects.property(ProvisioningProfileReader) + + public static final String DESCRIPTION = "Prepare the archive configuration file" + public static final String NAME = "prepareArchiving" + + static final String KEY_BUNDLE_IDENTIFIER = "PRODUCT_BUNDLE_IDENTIFIER" + static final String KEY_CODE_SIGN_IDENTITY = "CODE_SIGN_IDENTITY" + static final String KEY_CODE_SIGN_ENTITLEMENTS = "CODE_SIGN_ENTITLEMENTS" + static final String KEY_DEVELOPMENT_TEAM = "DEVELOPMENT_TEAM" + static final String KEY_PROVISIONING_PROFILE_ID = "PROVISIONING_PROFILE" + static final String KEY_PROVISIONING_PROFILE_SPEC = "PROVISIONING_PROFILE_SPECIFIER" + + PrepareXcodeArchivingTask() { + super() + + dependsOn(XcodePlugin.INFOPLIST_MODIFY_TASK_NAME) + dependsOn(XcodePlugin.XCODE_CONFIG_TASK_NAME) + dependsOn(KeychainCreateTask.TASK_NAME) + dependsOn(ProvisioningInstallTask.TASK_NAME) + + this.description = DESCRIPTION + + this.entitlementsFilePath.set(entitlementsFile.map(new Transformer() { + @Override + String transform(RegularFile regularFile) { + return regularFile.asFile.absolutePath + } + })) + + this.provisioningForConfiguration.set(configurationBundleIdentifier.map(new Transformer() { + @Override + File transform(String bundleIdentifier) { + return ProvisioningProfileReader.getProvisionFileForIdentifier(bundleIdentifier, + registeredProvisioningFiles.getOrNull() as List, + commandRunnerProperty.get(), + plistHelperProperty.get()) + } + })) + + this.provisioningReader.set(provisioningForConfiguration.map(new Transformer() { + @Override + ProvisioningProfileReader transform(File file) { + return new ProvisioningProfileReader(file, + commandRunnerProperty.get()) + } + })) + + this.onlyIf { + return certificateFriendlyName.present && + configurationBundleIdentifier.present && + outputFile.present && + provisioningForConfiguration.present + } + } + + @TaskAction + void generate() { + logger.info("Preparing archiving") + + outputFile.get().asFile.text = "" + + append(KEY_CODE_SIGN_IDENTITY, certificateFriendlyName.get()) + append(KEY_BUNDLE_IDENTIFIER, configurationBundleIdentifier.get()) + + if (provisioningReader.present) { + ProvisioningProfileReader reader = provisioningReader.get() + append(KEY_DEVELOPMENT_TEAM, reader.getTeamIdentifierPrefix()) + append(KEY_PROVISIONING_PROFILE_ID, reader.getUUID()) + append(KEY_PROVISIONING_PROFILE_SPEC, reader.getName()) + } + + if (entitlementsFilePath.present) { + append(KEY_CODE_SIGN_ENTITLEMENTS, entitlementsFilePath.get()) + } + } + + private void append(String key, String value) { + outputFile.get() + .asFile + .append(System.getProperty("line.separator") + key + " = " + value) + } +} diff --git a/plugin/src/main/groovy/org/openbakery/packaging/ReleaseNotesTask.groovy b/plugin/src/main/groovy/org/openbakery/ReleaseNotesTask.groovy similarity index 92% rename from plugin/src/main/groovy/org/openbakery/packaging/ReleaseNotesTask.groovy rename to plugin/src/main/groovy/org/openbakery/ReleaseNotesTask.groovy index e61856d7..7acb2854 100644 --- a/plugin/src/main/groovy/org/openbakery/packaging/ReleaseNotesTask.groovy +++ b/plugin/src/main/groovy/org/openbakery/ReleaseNotesTask.groovy @@ -13,12 +13,12 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.openbakery.packaging +package org.openbakery import org.apache.commons.io.FileUtils import org.gradle.api.DefaultTask import org.gradle.api.tasks.TaskAction - +import org.openbakery.util.PathHelper import org.pegdown.PegDownProcessor @@ -29,7 +29,7 @@ import org.pegdown.PegDownProcessor */ class ReleaseNotesTask extends DefaultTask { - File outputPath = new File(project.getBuildDir(), PackageTask.PACKAGE_PATH) + File outputPath = PathHelper.resolvePackageFolder(project) ReleaseNotesTask() { @@ -59,4 +59,4 @@ class ReleaseNotesTask extends DefaultTask { println "No changelog found!" } } -} \ No newline at end of file +} diff --git a/plugin/src/main/groovy/org/openbakery/XcodeBuildCleanTask.groovy b/plugin/src/main/groovy/org/openbakery/XcodeBuildCleanTask.groovy index 4facd4d9..d3c88ecf 100644 --- a/plugin/src/main/groovy/org/openbakery/XcodeBuildCleanTask.groovy +++ b/plugin/src/main/groovy/org/openbakery/XcodeBuildCleanTask.groovy @@ -24,22 +24,19 @@ class XcodeBuildCleanTask extends DefaultTask { XcodeBuildCleanTask() { super() - dependsOn( - XcodePlugin.KEYCHAIN_CLEAN_TASK_NAME, - XcodePlugin.PROVISIONING_CLEAN_TASK_NAME, - XcodePlugin.HOCKEYAPP_CLEAN_TASK_NAME, - XcodePlugin.HOCKEYAPP_CLEAN_TASK_NAME, - XcodePlugin.DEPLOYGATE_CLEAN_TASK_NAME, - ) + dependsOn(XcodePlugin.HOCKEYAPP_CLEAN_TASK_NAME, + XcodePlugin.HOCKEYAPP_CLEAN_TASK_NAME, + XcodePlugin.DEPLOYGATE_CLEAN_TASK_NAME) + this.description = "Cleans up the generated files from the previous build" } @TaskAction def clean() { - project.xcodebuild.dstRoot.deleteDir() - project.xcodebuild.objRoot.deleteDir() - project.xcodebuild.symRoot.deleteDir() - project.xcodebuild.sharedPrecompsDir.deleteDir() + project.xcodebuild.dstRoot.get().deleteDir() + project.xcodebuild.objRoot.get().deleteDir() + project.xcodebuild.symRoot.get().deleteDir() + project.xcodebuild.sharedPrecompsDir.get().deleteDir() } } diff --git a/plugin/src/main/groovy/org/openbakery/XcodeBuildPluginExtension.groovy b/plugin/src/main/groovy/org/openbakery/XcodeBuildPluginExtension.groovy index 921e333e..207649e8 100644 --- a/plugin/src/main/groovy/org/openbakery/XcodeBuildPluginExtension.groovy +++ b/plugin/src/main/groovy/org/openbakery/XcodeBuildPluginExtension.groovy @@ -18,75 +18,51 @@ package org.openbakery import org.apache.commons.io.filefilter.SuffixFileFilter import org.apache.commons.lang.StringUtils import org.gradle.api.Project +import org.gradle.api.Transformer +import org.gradle.api.file.Directory +import org.gradle.api.file.DirectoryProperty +import org.gradle.api.provider.Property import org.gradle.util.ConfigureUtil -import org.openbakery.signing.Signing -import org.openbakery.xcode.Destination -import org.openbakery.xcode.Devices -import org.openbakery.xcode.Type -import org.openbakery.xcode.Xcode -import org.openbakery.xcode.XcodebuildParameters +import org.openbakery.extension.Signing +import org.openbakery.util.PathHelper import org.openbakery.util.PlistHelper import org.openbakery.util.VariableResolver +import org.openbakery.xcode.* import org.slf4j.Logger import org.slf4j.LoggerFactory - -/* - -^ -should be migrated to this -> and renamed to Device - -enum Devices { - PHONE(1<<0), - PAD(1<<1), - WATCH(1<<2), - TV(1<<3) - - private final int value; - - Devices(int value) { - this.value = value - } - - public int getValue() { - return value - } - - public boolean is(Devices device) { - return (this.value & device.value) > 0 - } - -} - */ - - class XcodeBuildPluginExtension { - public final static KEYCHAIN_NAME_BASE = "gradle-" - - - private static Logger logger = LoggerFactory.getLogger(XcodeBuildPluginExtension.class) + final Property bitcode = project.objects.property(Boolean) + final Property version = project.objects.property(String) + final Property targetType = project.objects.property(Type) + final Property scheme = project.objects.property(String) + final DirectoryProperty archiveDirectory = project.layout.directoryProperty() + final DirectoryProperty schemeArchiveFile = project.layout.directoryProperty() + final DirectoryProperty dstRoot = project.layout.directoryProperty() + final DirectoryProperty objRoot = project.layout.directoryProperty() + final DirectoryProperty symRoot = project.layout.directoryProperty() + final DirectoryProperty sharedPrecompsDir = project.layout.directoryProperty() + final DirectoryProperty derivedDataPath = project.layout.directoryProperty() + final Property xcodeServiceProperty = project.objects.property(XcodeService) + + final Signing signing XcodebuildParameters _parameters = new XcodebuildParameters() String infoPlist = null - String scheme = null + String configuration = 'Debug' boolean simulator = true Type type = Type.iOS - String target = null - Object dstRoot - Object objRoot - Object symRoot - Object sharedPrecompsDir - Object derivedDataPath - Signing signing = null + String target + def additionalParameters = null String bundleNameSuffix = null List arch = null String workspace = null - String xcodeVersion = null + Map environment = null String productName = null String bundleName = null @@ -94,14 +70,11 @@ class XcodeBuildPluginExtension { String ipaFileName = null File projectFile - Boolean bitcode = false - boolean useXcodebuildArchive = false Devices devices = Devices.UNIVERSAL - CommandRunner commandRunner VariableResolver variableResolver PlistHelper plistHelper @@ -110,40 +83,62 @@ class XcodeBuildPluginExtension { HashMap projectSettings = new HashMap<>() - - /** * internal parameters */ private final Project project + private final CommandRunner commandRunner + + private static final Logger logger = LoggerFactory.getLogger(XcodeBuildPluginExtension.class) + + XcodeBuildPluginExtension(Project project, + CommandRunner commandRunner) { + this.project = project + this.commandRunner = commandRunner - public XcodeBuildPluginExtension(Project project) { - this.project = project; - this.signing = new Signing(project) - this.variableResolver = new VariableResolver(project) - commandRunner = new CommandRunner() plistHelper = new PlistHelper(commandRunner) - this.dstRoot = { - return project.getFileResolver().withBaseDir(project.getBuildDir()).resolve("dst") - } + configureServices() + configurePaths() - this.objRoot = { - return project.getFileResolver().withBaseDir(project.getBuildDir()).resolve("obj") - } + this.signing = project.objects.newInstance(Signing, project, commandRunner) + this.variableResolver = new VariableResolver(project) - this.symRoot = { - return project.getFileResolver().withBaseDir(project.getBuildDir()).resolve("sym") - } + this.dstRoot.set(project.layout.buildDirectory.dir("dst")) + this.objRoot.set(project.layout.buildDirectory.dir("obj")) + this.symRoot.set(project.layout.buildDirectory.dir("sym")) + this.sharedPrecompsDir.set(project.layout.buildDirectory.dir("shared")) + this.derivedDataPath.set(project.layout.buildDirectory.dir("derivedData")) - this.sharedPrecompsDir = { - return project.getFileResolver().withBaseDir(project.getBuildDir()).resolve("shared") - } + this.targetType.set(Type.iOS) + } - this.derivedDataPath = { - return project.getFileResolver().withBaseDir(project.getBuildDir()).resolve("derivedData") - } + private void configureServices() { + XcodeService service = project.objects.newInstance(XcodeService, + project) + service.commandRunnerProperty.set(commandRunner) + this.xcodeServiceProperty.set(service) + } + + private void configurePaths() { + this.archiveDirectory.set(project.layout + .buildDirectory + .dir(PathHelper.FOLDER_ARCHIVE)) + + this.schemeArchiveFile.set(scheme.map(new Transformer() { + @Override + Directory transform(String scheme) { + return archiveDirectory.get() + .dir(scheme + PathHelper.EXTENSION_XCARCHIVE) + } + })) + } + Optional getBuildTargetConfiguration(String schemeName, + String configuration) { + return Optional.ofNullable(projectSettings.get(schemeName, null)) + .map { it -> it.buildSettings } + .map { bs -> (BuildConfiguration) bs.get(configuration) } } String getWorkspace() { @@ -157,61 +152,6 @@ class XcodeBuildPluginExtension { return null } - void setDerivedDataPath(File derivedDataPath) { - this.derivedDataPath = derivedDataPath - } - - void setDstRoot(File dstRoot) { - this.dstRoot = dstRoot - } - - void setObjRoot(File objRoot) { - this.objRoot = objRoot - } - - void setSymRoot(File symRoot) { - this.symRoot = symRoot - } - - void setSharedPrecompsDir(File sharedPrecompsDir) { - this.sharedPrecompsDir = sharedPrecompsDir - } - - File getDstRoot() { - if (dstRoot instanceof File) { - return dstRoot - } - return project.file(dstRoot) - } - - File getObjRoot() { - if (objRoot instanceof File) { - return objRoot - } - return project.file(objRoot) - } - - File getSymRoot() { - if (symRoot instanceof File) { - return symRoot - } - return project.file(symRoot) - } - - File getSharedPrecompsDir() { - if (sharedPrecompsDir instanceof File) { - return sharedPrecompsDir - } - return project.file(sharedPrecompsDir) - } - - File getDerivedDataPath() { - if (derivedDataPath instanceof File) { - return derivedDataPath - } - return project.file(derivedDataPath) - } - void signing(Closure closure) { ConfigureUtil.configure(closure, this.signing) } @@ -249,7 +189,6 @@ class XcodeBuildPluginExtension { } - void setArch(Object arch) { if (arch instanceof List) { logger.debug("Arch is List: " + arch + " - " + arch.getClass().getName()) @@ -278,20 +217,12 @@ class XcodeBuildPluginExtension { if (index == -1) { this.environment.put(environmentString, null) } else { - this.environment.put(environmentString.substring(0, index),environmentString.substring(index + 1)) + this.environment.put(environmentString.substring(0, index), environmentString.substring(index + 1)) } } } - void setVersion(String version) { - this.xcodeVersion = version - // check if the version is valid. On creation of the Xcodebuild class an exception is thrown if the version is not valid - xcode = null - //getXcode() - } - - String getValueFromInfoPlist(key) { if (infoPlist != null) { File infoPlistFile = new File(project.projectDir, infoPlist) @@ -327,18 +258,26 @@ class XcodeBuildPluginExtension { return bundleName } - // should be removed an replaced by the xcodebuildParameters.outputPath File getOutputPath() { String path = getConfiguration() - if (type == Type.iOS) { - if (simulator) { - path += "-iphonesimulator" - } else { - path += "-iphoneos" + if (type != Type.macOS) { + path += "-" + if (type == Type.iOS) { + if (simulator) { + path += PathHelper.IPHONE_SIMULATOR + } else { + path += PathHelper.IPHONE_OS + } + } else if (type == Type.tvOS) { + if (simulator) { + path += PathHelper.APPLE_TV_SIMULATOR + } else { + path += PathHelper.APPLE_TV_OS + } } } - return new File(getSymRoot(), path) + return new File(getSymRoot().asFile.get(), path) } @@ -420,13 +359,14 @@ class XcodeBuildPluginExtension { return result } - - - void setType(String type) { this.type = Type.typeFromString(type) + this.targetType.set(this.type) } + Type getType() { + return type + } boolean getSimulator() { if (type == Type.macOS) { @@ -466,31 +406,30 @@ class XcodeBuildPluginExtension { // should be remove in the future, so that every task has its own xcode object Xcode getXcode() { if (xcode == null) { - xcode = new Xcode(commandRunner, xcodeVersion) + xcode = new Xcode(commandRunner, version.get()) } logger.debug("using xcode {}", xcode) - return xcode + return xcode } - XcodebuildParameters getXcodebuildParameters() { def result = new XcodebuildParameters() - result.scheme = this.scheme + result.scheme = this.scheme.getOrNull() result.target = this.target result.simulator = this.simulator result.type = this.type result.workspace = getWorkspace() result.configuration = this.configuration - result.dstRoot = this.getDstRoot() - result.objRoot = this.getObjRoot() - result.symRoot = this.getSymRoot() - result.sharedPrecompsDir = this.getSharedPrecompsDir() - result.derivedDataPath = this.getDerivedDataPath() + result.dstRoot = this.getDstRoot().asFile.getOrNull() + result.objRoot = this.getObjRoot().asFile.getOrNull() + result.symRoot = this.getSymRoot().asFile.getOrNull() + result.sharedPrecompsDir = this.getSharedPrecompsDir().asFile.getOrNull() + result.derivedDataPath = this.derivedDataPath.asFile.getOrNull() result.additionalParameters = this.additionalParameters result.devices = this.devices result.configuredDestinations = this.destinations - result.bitcode = this.bitcode + result.bitcode = this.bitcode.getOrElse(true) result.applicationBundle = getApplicationBundle() if (this.arch != null) { @@ -500,4 +439,4 @@ class XcodeBuildPluginExtension { return result } -} \ No newline at end of file +} diff --git a/plugin/src/main/groovy/org/openbakery/XcodeBuildTask.groovy b/plugin/src/main/groovy/org/openbakery/XcodeBuildTask.groovy index fe438758..494b0c34 100644 --- a/plugin/src/main/groovy/org/openbakery/XcodeBuildTask.groovy +++ b/plugin/src/main/groovy/org/openbakery/XcodeBuildTask.groovy @@ -15,7 +15,10 @@ */ package org.openbakery +import org.gradle.api.Task +import org.gradle.api.specs.Spec import org.gradle.api.tasks.TaskAction +import org.openbakery.xcode.Type import org.openbakery.xcode.Xcodebuild class XcodeBuildTask extends AbstractXcodeBuildTask { @@ -28,6 +31,14 @@ class XcodeBuildTask extends AbstractXcodeBuildTask { XcodePlugin.INFOPLIST_MODIFY_TASK_NAME, ) this.description = "Builds the Xcode project" + + onlyIf(new Spec() { + @Override + boolean isSatisfiedBy(Task task) { + return getXcodeExtension().getType() == Type.macOS || + getXcodeExtension().getType() == Type.watchOS + } + }) } @TaskAction diff --git a/plugin/src/main/groovy/org/openbakery/XcodePlugin.groovy b/plugin/src/main/groovy/org/openbakery/XcodePlugin.groovy index 4a8185c7..ce142110 100644 --- a/plugin/src/main/groovy/org/openbakery/XcodePlugin.groovy +++ b/plugin/src/main/groovy/org/openbakery/XcodePlugin.groovy @@ -25,13 +25,18 @@ import org.gradle.api.tasks.testing.Test import org.openbakery.appledoc.AppledocCleanTask import org.openbakery.appledoc.AppledocTask import org.openbakery.appstore.AppstorePluginExtension -import org.openbakery.appstore.AppstoreValidateTask import org.openbakery.appstore.AppstoreUploadTask +import org.openbakery.appstore.AppstoreValidateTask +import org.openbakery.archiving.XcodeBuildArchiveTask +import org.openbakery.archiving.XcodeBuildArchiveTaskIosAndTvOS +import org.openbakery.archiving.XcodeBuildLegacyArchiveTask +import org.openbakery.carthage.CarthageBootStrapTask import org.openbakery.carthage.CarthageCleanTask import org.openbakery.carthage.CarthageUpdateTask import org.openbakery.cocoapods.CocoapodsBootstrapTask import org.openbakery.cocoapods.CocoapodsInstallTask import org.openbakery.cocoapods.CocoapodsUpdateTask +import org.openbakery.codesign.Security import org.openbakery.configuration.XcodeConfigTask import org.openbakery.coverage.CoverageCleanTask import org.openbakery.coverage.CoveragePluginExtension @@ -42,31 +47,20 @@ import org.openbakery.crashlytics.CrashlyticsUploadTask import org.openbakery.deploygate.DeployGateCleanTask import org.openbakery.deploygate.DeployGatePluginExtension import org.openbakery.deploygate.DeployGateUploadTask +import org.openbakery.extension.Signing import org.openbakery.hockeyapp.HockeyAppCleanTask import org.openbakery.hockeyapp.HockeyAppPluginExtension import org.openbakery.hockeyapp.HockeyAppUploadTask -import org.openbakery.hockeykit.HockeyKitArchiveTask -import org.openbakery.hockeykit.HockeyKitCleanTask -import org.openbakery.hockeykit.HockeyKitImageTask -import org.openbakery.hockeykit.HockeyKitManifestTask -import org.openbakery.hockeykit.HockeyKitPluginExtension -import org.openbakery.hockeykit.HockeyKitReleaseNotesTask +import org.openbakery.hockeykit.* import org.openbakery.oclint.OCLintPluginExtension import org.openbakery.oclint.OCLintTask -import org.openbakery.packaging.ReleaseNotesTask -import org.openbakery.signing.KeychainCleanupTask -import org.openbakery.signing.KeychainCreateTask +import org.openbakery.packaging.PackageLegacyTask import org.openbakery.packaging.PackageTask -import org.openbakery.signing.KeychainRemoveFromSearchListTask -import org.openbakery.signing.ProvisioningCleanupTask -import org.openbakery.signing.ProvisioningInstallTask -import org.openbakery.simulators.SimulatorKillTask -import org.openbakery.simulators.SimulatorsCleanTask -import org.openbakery.simulators.SimulatorsCreateTask -import org.openbakery.simulators.SimulatorsListTask -import org.openbakery.simulators.SimulatorStartTask -import org.openbakery.simulators.SimulatorRunAppTask -import org.openbakery.simulators.SimulatorInstallAppTask +import org.openbakery.packaging.PackageTaskIosAndTvOS +import org.openbakery.signing.* +import org.openbakery.simulators.* +import org.openbakery.util.PlistHelper +import org.openbakery.xcode.Xcode import org.slf4j.Logger import org.slf4j.LoggerFactory @@ -90,8 +84,7 @@ class XcodePlugin implements Plugin { public static final String XCODE_TEST_TASK_NAME = "xcodetest" public static final String XCODE_BUILD_FOR_TEST_TASK_NAME = "xcodebuildForTest" - public static final String XCODE_TEST_RUN_TASK_NAME = "xcodetestrun" - public static final String ARCHIVE_TASK_NAME = "archive" + public static final String XCODE_TEST_RUN_TASK_NAME = "xcodetestrun" public static final String SIMULATORS_LIST_TASK_NAME = "simulatorsList" public static final String SIMULATORS_CREATE_TASK_NAME = "simulatorsCreate" public static final String SIMULATORS_CLEAN_TASK_NAME = "simulatorsClean" @@ -108,13 +101,7 @@ class XcodePlugin implements Plugin { public static final String HOCKEYKIT_IMAGE_TASK_NAME = "hockeykitImage" public static final String HOCKEYKIT_CLEAN_TASK_NAME = "hockeykitClean" public static final String HOCKEYKIT_TASK_NAME = "hockeykit" - public static final String KEYCHAIN_CREATE_TASK_NAME = "keychainCreate" - public static final String KEYCHAIN_CLEAN_TASK_NAME = "keychainClean" - public static final String KEYCHAIN_REMOVE_SEARCH_LIST_TASK_NAME = "keychainRemove" public static final String INFOPLIST_MODIFY_TASK_NAME = 'infoplistModify' - public static final String PROVISIONING_INSTALL_TASK_NAME = 'provisioningInstall' - public static final String PROVISIONING_CLEAN_TASK_NAME = 'provisioningClean' - public static final String PACKAGE_TASK_NAME = 'package' public static final String PACKAGE_RELEASE_NOTES_TASK_NAME = 'packageReleaseNotes' public static final String APPSTORE_UPLOAD_TASK_NAME = 'appstoreUpload' public static final String APPSTORE_VALIDATE_TASK_NAME = 'appstoreValidate' @@ -143,13 +130,20 @@ class XcodePlugin implements Plugin { public static final String SDK_IPHONESIMULATOR = "iphonesimulator" + private Signing signingExtension + private XcodeBuildPluginExtension xcodeBuildPluginExtension + private InfoPlistExtension infoPlistExtension + private CommandRunner commandRunner + private PlistHelper plistHelper + private Security securityTool + private Xcode xcode void apply(Project project) { project.getPlugins().apply(BasePlugin.class); - System.setProperty("java.awt.headless", "true"); - + System.setProperty("java.awt.headless", "true") + setupTools(project) configureExtensions(project) configureClean(project) configureBuild(project) @@ -174,6 +168,12 @@ class XcodePlugin implements Plugin { configureProperties(project) } + void setupTools(Project project) { + this.commandRunner = new CommandRunner() + this.xcode = new Xcode(commandRunner) + this.plistHelper = new PlistHelper(commandRunner) + this.securityTool = new Security(commandRunner) + } void configureProperties(Project project) { @@ -434,10 +434,18 @@ class XcodePlugin implements Plugin { } - void configureExtensions(Project project) { - project.extensions.create("xcodebuild", XcodeBuildPluginExtension, project) - project.extensions.create("infoplist", InfoPlistExtension) + this.xcodeBuildPluginExtension = project.extensions.create("xcodebuild", + XcodeBuildPluginExtension, + project, + commandRunner) + + this.signingExtension = xcodeBuildPluginExtension.signing + + this.infoPlistExtension = project.extensions.create("infoplist", + InfoPlistExtension, + project) + project.extensions.create("hockeykit", HockeyKitPluginExtension, project) project.extensions.create("appstore", AppstorePluginExtension, project) project.extensions.create("hockeyapp", HockeyAppPluginExtension, project) @@ -451,8 +459,7 @@ class XcodePlugin implements Plugin { private void configureTestRunDependencies(Project project) { for (XcodeTestRunTask xcodeTestRunTask : project.getTasks().withType(XcodeTestRunTask.class)) { if (xcodeTestRunTask.runOnDevice()) { - xcodeTestRunTask.dependsOn(XcodePlugin.KEYCHAIN_CREATE_TASK_NAME, XcodePlugin.PROVISIONING_INSTALL_TASK_NAME) - xcodeTestRunTask.finalizedBy(XcodePlugin.KEYCHAIN_REMOVE_SEARCH_LIST_TASK_NAME) + xcodeTestRunTask.dependsOn(KeychainCreateTask.TASK_NAME, ProvisioningInstallTask.TASK_NAME) } } } @@ -476,17 +483,52 @@ class XcodePlugin implements Plugin { } private void configureArchive(Project project) { - XcodeBuildArchiveTask xcodeBuildArchiveTask = project.getTasks().create(ARCHIVE_TASK_NAME, XcodeBuildArchiveTask.class); - xcodeBuildArchiveTask.setGroup(XCODE_GROUP_NAME); - //xcodeBuildArchiveTask.dependsOn(project.getTasks().getByName(BasePlugin.CLEAN_TASK_NAME)); + project.getTasks() + .create(PrepareXcodeArchivingTask.NAME, + PrepareXcodeArchivingTask.class) { + it.group = XCODE_GROUP_NAME + + it.certificateFriendlyName.set(signingExtension.certificateFriendlyName) + it.commandRunnerProperty.set(commandRunner) + it.configurationBundleIdentifier.set(infoPlistExtension.configurationBundleIdentifier) + it.entitlementsFile.set(signingExtension.entitlementsFile) + it.outputFile.set(signingExtension.xcConfigFile) + it.plistHelperProperty.set(plistHelper) + it.registeredProvisioningFiles.set(signingExtension.registeredProvisioningFiles) + } + + project.getTasks().create(XcodeBuildArchiveTaskIosAndTvOS.NAME, + XcodeBuildArchiveTaskIosAndTvOS.class) { + it.setGroup(XCODE_GROUP_NAME) + it.buildType.set(xcodeBuildPluginExtension.type) + it.commandRunnerProperty.set(commandRunner) + it.outputArchiveFile.set(xcodeBuildPluginExtension.schemeArchiveFile) + it.scheme.set(xcodeBuildPluginExtension.scheme) + it.xcode.set(xcode) + it.xcodeVersion.set(xcodeBuildPluginExtension.version) + it.xcConfigFile.set(signingExtension.xcConfigFile) + it.xcodeServiceProperty.set(xcodeBuildPluginExtension.xcodeServiceProperty) + } + + XcodeBuildLegacyArchiveTask xcodeBuildArchiveTask = project.getTasks(). + create(XcodeBuildLegacyArchiveTask.NAME, XcodeBuildLegacyArchiveTask.class) + xcodeBuildArchiveTask.setGroup(XCODE_GROUP_NAME) + + XcodeBuildArchiveTask task = project.tasks.create(XcodeBuildArchiveTask.NAME, + XcodeBuildArchiveTask.class) + task.setGroup(XCODE_GROUP_NAME) } private void configureSimulatorTasks(Project project) { - project.task(SIMULATORS_LIST_TASK_NAME, type: SimulatorsListTask, group: SIMULATORS_LIST_TASK_NAME) + project.task(SIMULATORS_LIST_TASK_NAME, type: SimulatorsListTask.class, group: SIMULATORS_LIST_TASK_NAME) project.task(SIMULATORS_CREATE_TASK_NAME, type: SimulatorsCreateTask, group: SIMULATORS_LIST_TASK_NAME) project.task(SIMULATORS_CLEAN_TASK_NAME, type: SimulatorsCleanTask, group: SIMULATORS_LIST_TASK_NAME) - project.task(SIMULATORS_START_TASK_NAME, type: SimulatorStartTask, group: SIMULATORS_LIST_TASK_NAME) + + project.tasks.create(SIMULATORS_START_TASK_NAME, SimulatorStartTask) { + it.group = SIMULATORS_LIST_TASK_NAME + } + project.task(SIMULATORS_RUN_APP_TASK_NAME, type: SimulatorRunAppTask, group: SIMULATORS_LIST_TASK_NAME) project.task(SIMULATORS_INSTALL_APP_TASK_NAME, type: SimulatorInstallAppTask, group: SIMULATORS_LIST_TASK_NAME) project.task(SIMULATORS_KILL_TASK_NAME, type: SimulatorKillTask, group: SIMULATORS_LIST_TASK_NAME) @@ -504,9 +546,20 @@ class XcodePlugin implements Plugin { } private void configureKeychain(Project project) { - project.task(KEYCHAIN_CREATE_TASK_NAME, type: KeychainCreateTask, group: XCODE_GROUP_NAME) - project.task(KEYCHAIN_CLEAN_TASK_NAME, type: KeychainCleanupTask, group: XCODE_GROUP_NAME) - project.task(KEYCHAIN_REMOVE_SEARCH_LIST_TASK_NAME, type: KeychainRemoveFromSearchListTask, group: XCODE_GROUP_NAME) + project.tasks.create(KeychainCreateTask.TASK_NAME, KeychainCreateTask.class) { + it.group = XCODE_GROUP_NAME + + it.certificateFile.set(signingExtension.certificate) + it.certificateUri.set(signingExtension.certificateURI) + it.certificatePassword.set(signingExtension.certificatePassword) + it.outputDirectory.set(signingExtension.signingDestinationRoot) + it.keyChainFile.set(signingExtension.keyChainFile) + it.keychainTimeout.set(signingExtension.timeout) + it.security.set(securityTool) + it.commandRunnerProperty.set(commandRunner) + + signingExtension.certificateFriendlyName.set(it.certificateFriendlyName) + } } private void configureTest(Project project) { @@ -517,34 +570,75 @@ class XcodePlugin implements Plugin { } private configureInfoPlist(Project project) { - project.task(INFOPLIST_MODIFY_TASK_NAME, type: InfoPlistModifyTask, group: XCODE_GROUP_NAME) + project.tasks.create(INFOPLIST_MODIFY_TASK_NAME, + InfoPlistModifyTask) { + it.group = XCODE_GROUP_NAME + infoPlistExtension.configurationBundleIdentifier.set(it.configurationBundleIdentifier) + } } private configureProvisioning(Project project) { - project.task(PROVISIONING_INSTALL_TASK_NAME, type: ProvisioningInstallTask, group: XCODE_GROUP_NAME) - project.task(PROVISIONING_CLEAN_TASK_NAME, type: ProvisioningCleanupTask, group: XCODE_GROUP_NAME) + project.tasks.create(ProvisioningInstallTask.TASK_NAME, + ProvisioningInstallTask) { + it.group = XCODE_GROUP_NAME + + it.commandRunnerProperty.set(commandRunner) + it.mobileProvisioningList.set(signingExtension.mobileProvisionList) + it.outputDirectory.set(signingExtension.provisioningDestinationRoot) + it.plistHelperProperty.set(plistHelper) + it.plistHelperProperty.set(plistHelper) + + // We use the result of the task to populate the read only values + signingExtension + .registeredProvisioningFiles + .set(it.registeredProvisioning) + + signingExtension + .registeredProvisioning + .set(it.registeredProvisioningFiles) + } } private configurePackage(Project project) { - PackageTask packageTask = project.task(PACKAGE_TASK_NAME, type: PackageTask, group: XCODE_GROUP_NAME) - + configureModernPackager(project) + configureLegacyPackager(project) - project.task(PACKAGE_RELEASE_NOTES_TASK_NAME, type: ReleaseNotesTask, group: XCODE_GROUP_NAME) + project.task(PackageTask.NAME, + type: PackageTask.class, + group: XCODE_GROUP_NAME) - //ProvisioningCleanupTask provisioningCleanup = project.getTasks().getByName(PROVISIONING_CLEAN_TASK_NAME) + project.task(PACKAGE_RELEASE_NOTES_TASK_NAME, + type: ReleaseNotesTask, + group: XCODE_GROUP_NAME) - //KeychainCleanupTask keychainCleanupTask = project.getTasks().getByName(KEYCHAIN_CLEAN_TASK_NAME) + } -/* // disabled clean because of #115 - packageTask.doLast { - provisioningCleanup.clean() - keychainCleanupTask.clean() + private void configureModernPackager(Project project) { + project.tasks.create(PackageTaskIosAndTvOS.NAME, + PackageTaskIosAndTvOS) { + it.group = XCODE_GROUP_NAME + + it.bitCode.set(xcodeBuildPluginExtension.bitcode) + it.buildType.set(xcodeBuildPluginExtension.type) + it.certificateFriendlyName.set(signingExtension.certificateFriendlyName) + it.commandRunner.set(this.commandRunner) + it.plistHelper.set(xcodeBuildPluginExtension.plistHelper) + it.registeredProvisioningFiles.set(signingExtension.registeredProvisioning) + it.scheme.set(xcodeBuildPluginExtension.scheme) + it.signingMethod.set(signingExtension.signingMethod) } -*/ + } + + private void configureLegacyPackager(Project project) { + PackageLegacyTask packageTask = project.task(PackageLegacyTask.NAME, + type: PackageLegacyTask, + group: XCODE_GROUP_NAME) + XcodeBuildTask xcodeBuildTask = project.getTasks().getByName(XCODE_BUILD_TASK_NAME) packageTask.shouldRunAfter(xcodeBuildTask) } + private configureAppstore(Project project) { project.task(APPSTORE_UPLOAD_TASK_NAME, type: AppstoreUploadTask, group: APPSTORE_GROUP_NAME) project.task(APPSTORE_VALIDATE_TASK_NAME, type: AppstoreValidateTask, group: APPSTORE_GROUP_NAME) @@ -597,16 +691,22 @@ class XcodePlugin implements Plugin { private void configureCarthage(Project project) { project.task(CARTHAGE_CLEAN_TASK_NAME, type: CarthageCleanTask, group: CARTHAGE_GROUP_NAME) project.task(CARTHAGE_UPDATE_TASK_NAME, type: CarthageUpdateTask, group: CARTHAGE_GROUP_NAME) + + project.tasks.create(CarthageBootStrapTask.NAME, CarthageBootStrapTask.class) { + it.group = CARTHAGE_GROUP_NAME + it.requiredXcodeVersion.set(xcodeBuildPluginExtension.version) + it.commandRunnerProperty.set(commandRunner) + it.platform.set(xcodeBuildPluginExtension.targetType) + } } private configureCarthageDependencies(Project project) { - CarthageUpdateTask carthageUpdateTask = project.getTasks().getByName(XcodePlugin.CARTHAGE_UPDATE_TASK_NAME) - CarthageCleanTask carthageCleanTask = project.getTasks().getByName(XcodePlugin.CARTHAGE_CLEAN_TASK_NAME) + CarthageBootStrapTask bootStrapTask = project.getTasks().getByName(CarthageBootStrapTask.NAME) as CarthageBootStrapTask + addDependencyToBuild(project, bootStrapTask) - if (carthageUpdateTask.hasCartFile()) { - addDependencyToBuild(project, carthageUpdateTask) - project.getTasks().getByName(BasePlugin.CLEAN_TASK_NAME).dependsOn(carthageCleanTask); - } + project.getTasks() + .getByName(BasePlugin.CLEAN_TASK_NAME) + .dependsOn(project.getTasks().getByName(CARTHAGE_CLEAN_TASK_NAME)) } private void configureOCLint(Project project) { @@ -614,7 +714,7 @@ class XcodePlugin implements Plugin { Task ocLintTask = project.getTasks().create(OCLINT_TASK_NAME); ocLintTask.group = ANALYTICS_GROUP_NAME - ocLintTask.description = "Runs: " + BasePlugin.CLEAN_TASK_NAME + " " + XCODE_BUILD_TASK_NAME + " " + OCLINT_REPORT_TASK_NAME + ocLintTask.description = "Runs: " + BasePlugin.CLEAN_TASK_NAME + " " + XCODE_BUILD_TASK_NAME + " " + OCLINT_REPORT_TASK_NAME ocLintTask.dependsOn(project.getTasks().getByName(BasePlugin.CLEAN_TASK_NAME)) XcodeBuildTask xcodeBuildTask = project.getTasks().getByName(XcodePlugin.XCODE_BUILD_TASK_NAME) diff --git a/plugin/src/main/groovy/org/openbakery/XcodeService.groovy b/plugin/src/main/groovy/org/openbakery/XcodeService.groovy new file mode 100644 index 00000000..4b26a96b --- /dev/null +++ b/plugin/src/main/groovy/org/openbakery/XcodeService.groovy @@ -0,0 +1,93 @@ +package org.openbakery + +import org.gradle.api.Project +import org.gradle.api.Transformer +import org.gradle.api.provider.ListProperty +import org.gradle.api.provider.Property +import org.openbakery.XcodeService.XcodeApp +import org.openbakery.xcode.Version + +import javax.inject.Inject +import java.util.regex.Matcher +import java.util.regex.Pattern + +class XcodeService { + + final Property commandRunnerProperty + final ListProperty installedXcodes + + private static final String CONTENT_DEVELOPER = "Contents/Developer" + private static final String CONTENT_XCODE_BUILD = "$CONTENT_DEVELOPER/usr/bin/xcodebuild" + private static final Pattern VERSION_PATTERN = ~/Xcode\s([^\s]*)\nBuild\sversion\s([^\s]*)/ + + @Inject + XcodeService(Project project) { + commandRunnerProperty = project.objects.property(CommandRunner) + + installedXcodes = project.objects.listProperty(XcodeApp) + installedXcodes.set(commandRunnerProperty.map(new Transformer, CommandRunner>() { + @Override + List transform(CommandRunner commandRunner) { + return commandRunner.runWithResult("mdfind", + "kMDItemCFBundleIdentifier=com.apple.dt.Xcode") + .split("\n") + .collect { new File(it) } + .collect { new XcodeApp(it, getXcodeVersion(commandRunner, it)) } + } + })) + } + + public XcodeApp getInstallationForVersion(final String version) { + return installedXcodes.map(new Transformer>() { + @Override + XcodeApp transform(List xcodeApps) { + return xcodeApps.find { it.version.toString().startsWith(version) } + } + }).getOrNull() + } + + private Version getXcodeVersion(CommandRunner commandRunner, + File file) { + String xcodeVersion = commandRunner.runWithResult( + new File(file, CONTENT_XCODE_BUILD).absolutePath, + "-version") + + Matcher matcher = VERSION_PATTERN.matcher(xcodeVersion) + if (matcher.matches()) { + Version version = new Version(matcher.group(1)) + version.suffix = matcher.group(2) + + return version + } + + return null + } + + static class XcodeApp implements Serializable { + private final File file + private final Version version + + XcodeApp(File file, + Version version) { + this.file = file + this.version = version + } + + File getFile() { + return file + } + + File getContentXcodeBuildFile() { + return new File(file, CONTENT_XCODE_BUILD) + } + + File getContentDeveloperFile() { + return new File(file, CONTENT_DEVELOPER) + } + + Version getVersion() { + return version + } + } + +} diff --git a/plugin/src/main/groovy/org/openbakery/XcodeTestRunTask.groovy b/plugin/src/main/groovy/org/openbakery/XcodeTestRunTask.groovy index 9b601274..91e70770 100644 --- a/plugin/src/main/groovy/org/openbakery/XcodeTestRunTask.groovy +++ b/plugin/src/main/groovy/org/openbakery/XcodeTestRunTask.groovy @@ -152,7 +152,7 @@ class XcodeTestRunTask extends AbstractXcodeBuildTask { CodesignParameters parameters = new CodesignParameters() parameters.signingIdentity = getSigningIdentity() parameters.keychain = project.xcodebuild.signing.keychainPathInternal - parameters.mobileProvisionFiles = project.xcodebuild.signing.mobileProvisionFile + parameters.mobileProvisionFiles = project.extensions.getByType(XcodeBuildPluginExtension).signing.registeredProvisioningFiles.getFiles().asList() parameters.type = project.xcodebuild.type codesign = new Codesign(xcode, parameters, commandRunner, plistHelper) if (project.xcodebuild.signing.hasEntitlementsFile()) { diff --git a/plugin/src/main/groovy/org/openbakery/archiving/XcodeBuildArchiveTask.groovy b/plugin/src/main/groovy/org/openbakery/archiving/XcodeBuildArchiveTask.groovy new file mode 100644 index 00000000..55067054 --- /dev/null +++ b/plugin/src/main/groovy/org/openbakery/archiving/XcodeBuildArchiveTask.groovy @@ -0,0 +1,17 @@ +package org.openbakery.archiving + +import org.openbakery.AbstractXcodeBuildTask + +class XcodeBuildArchiveTask extends AbstractXcodeBuildTask { + + public static final String NAME = "archive" + + XcodeBuildArchiveTask() { + super() + + dependsOn(XcodeBuildArchiveTaskIosAndTvOS.NAME, + XcodeBuildLegacyArchiveTask.NAME) + + setDescription("Archive and export the project") + } +} diff --git a/plugin/src/main/groovy/org/openbakery/archiving/XcodeBuildArchiveTaskIosAndTvOS.groovy b/plugin/src/main/groovy/org/openbakery/archiving/XcodeBuildArchiveTaskIosAndTvOS.groovy new file mode 100644 index 00000000..f1ebe023 --- /dev/null +++ b/plugin/src/main/groovy/org/openbakery/archiving/XcodeBuildArchiveTaskIosAndTvOS.groovy @@ -0,0 +1,98 @@ +package org.openbakery.archiving + +import groovy.transform.CompileStatic +import org.gradle.api.DefaultTask +import org.gradle.api.Task +import org.gradle.api.Transformer +import org.gradle.api.file.Directory +import org.gradle.api.file.RegularFile +import org.gradle.api.provider.Property +import org.gradle.api.provider.Provider +import org.gradle.api.specs.Spec +import org.gradle.api.tasks.Input +import org.gradle.api.tasks.InputFile +import org.gradle.api.tasks.OutputDirectory +import org.gradle.api.tasks.TaskAction +import org.openbakery.CommandRunner +import org.openbakery.PrepareXcodeArchivingTask +import org.openbakery.XcodeService +import org.openbakery.signing.ProvisioningInstallTask +import org.openbakery.xcode.Type +import org.openbakery.xcode.Xcode +import org.openbakery.xcode.Xcodebuild + +@CompileStatic +class XcodeBuildArchiveTaskIosAndTvOS extends DefaultTask { + + @Input + final Provider xcodeVersion = project.objects.property(String) + + @Input + final Provider scheme = project.objects.property(String) + + @Input + final Provider buildType = project.objects.property(Type) + + @InputFile + final Property xcConfigFile = newInputFile() + + @OutputDirectory + final Provider outputArchiveFile = newOutputDirectory() + + final Property xcodeServiceProperty = project.objects.property(XcodeService) + final Property xcode = project.objects.property(Xcode) + final Property commandRunnerProperty = project.objects.property(CommandRunner) + + public static final String NAME = "archiveXcodeBuild" + + XcodeBuildArchiveTaskIosAndTvOS() { + super() + + dependsOn(ProvisioningInstallTask.TASK_NAME) + dependsOn(PrepareXcodeArchivingTask.NAME) + + this.description = "Use the XcodeBuild archive command line to create the project archive" + + onlyIf(new Spec() { + @Override + boolean isSatisfiedBy(Task task) { + return buildType.get() == Type.iOS || buildType.get() == Type.tvOS + } + }) + } + + @TaskAction + void archive() { + assert scheme.present: "No target scheme configured" + assert outputArchiveFile.present: "No output file folder configured" + assert xcConfigFile.present: "No 'xcconfig' file configured" + + logger.lifecycle("Archive project with configuration: " + + "\n\tScheme : ${scheme.get()} " + + "\n\tXcode version : ${xcodeVersion.getOrElse("System default")}") + + + Xcodebuild.archive(commandRunnerProperty.get(), + scheme.get(), + outputArchiveFile.get().asFile, + xcConfigFile.get().asFile, + getXcodeAppForConfiguration().getOrNull()) + } + + private Provider getXcodeAppForConfiguration() { + + Provider xcodeApp + if (xcodeVersion.present) { + xcodeApp = xcodeServiceProperty.map(new Transformer() { + @Override + File transform(XcodeService xcodeService) { + XcodeService.XcodeApp app = xcodeService.getInstallationForVersion(xcodeVersion.get()) + return app.contentDeveloperFile + } + }) + } else { + xcodeApp = project.objects.property(File) + } + return xcodeApp + } +} diff --git a/plugin/src/main/groovy/org/openbakery/XcodeBuildArchiveTask.groovy b/plugin/src/main/groovy/org/openbakery/archiving/XcodeBuildLegacyArchiveTask.groovy similarity index 83% rename from plugin/src/main/groovy/org/openbakery/XcodeBuildArchiveTask.groovy rename to plugin/src/main/groovy/org/openbakery/archiving/XcodeBuildLegacyArchiveTask.groovy index 4810d071..41cd5080 100644 --- a/plugin/src/main/groovy/org/openbakery/XcodeBuildArchiveTask.groovy +++ b/plugin/src/main/groovy/org/openbakery/archiving/XcodeBuildLegacyArchiveTask.groovy @@ -13,47 +13,62 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.openbakery +package org.openbakery.archiving import groovy.io.FileType import org.apache.commons.io.FileUtils +import org.gradle.api.Task +import org.gradle.api.specs.Spec import org.gradle.api.tasks.TaskAction +import org.openbakery.AbstractXcodeBuildTask +import org.openbakery.BuildConfiguration +import org.openbakery.CommandRunnerException +import org.openbakery.XcodeBuildPluginExtension +import org.openbakery.XcodePlugin import org.openbakery.codesign.ProvisioningProfileReader -import org.openbakery.xcode.Type +import org.openbakery.signing.ProvisioningInstallTask +import org.openbakery.util.PathHelper import org.openbakery.xcode.Extension +import org.openbakery.xcode.Type import org.openbakery.xcode.Xcodebuild import static groovy.io.FileType.FILES -class XcodeBuildArchiveTask extends AbstractXcodeBuildTask { +class XcodeBuildLegacyArchiveTask extends AbstractXcodeBuildTask { - public static final String ARCHIVE_FOLDER = "archive" + public static final String NAME = "archiveLegacy" - XcodeBuildArchiveTask() { + XcodeBuildLegacyArchiveTask() { super() - dependsOn(XcodePlugin.XCODE_BUILD_TASK_NAME) - // when creating an xcarchive for iOS then the provisioning profile is need for the team id so that the entitlements is setup properly - dependsOn(XcodePlugin.PROVISIONING_INSTALL_TASK_NAME) - this.description = "Prepare the app bundle that it can be archive" + this.description = "Use the legacy archiver to create the project archive" + + onlyIf(new Spec() { + @Override + boolean isSatisfiedBy(Task task) { + return getXcodeExtension().getType() == Type.macOS || + getXcodeExtension().getType() == Type.watchOS + } + }) + + dependsOn(XcodePlugin.XCODE_BUILD_TASK_NAME, + ProvisioningInstallTask.TASK_NAME) } def getOutputDirectory() { - def archiveDirectory = new File(project.getBuildDir(), ARCHIVE_FOLDER) + def archiveDirectory = PathHelper.resolveArchiveFolder(project) archiveDirectory.mkdirs() return archiveDirectory } - - def getiOSIcons() { ArrayList icons = new ArrayList<>(); File applicationBundle = parameters.applicationBundle def fileList = applicationBundle.list( - [accept: { d, f -> f ==~ /Icon(-\d+)??\.png/ }] as FilenameFilter // matches Icon.png or Icon-72.png + [accept: { d, f -> f ==~ /Icon(-\d+)??\.png/ }] as FilenameFilter // matches Icon.png or Icon-72.png ).toList() def applicationPath = "Applications/" + parameters.applicationBundleName @@ -67,7 +82,7 @@ class XcodeBuildArchiveTask extends AbstractXcodeBuildTask { } def getMacOSXIcons() { - File appInfoPlist = new File(parameters.applicationBundle, "Contents/Info.plist") + File appInfoPlist = new File(parameters.applicationBundle, "Contents/Info.plist") ArrayList icons = new ArrayList<>(); def icnsFileName = plistHelper.getValueFromPlist(appInfoPlist, "CFBundleIconFile") @@ -83,7 +98,6 @@ class XcodeBuildArchiveTask extends AbstractXcodeBuildTask { } - def getValueFromBundleInfoPlist(File bundle, String key) { File appInfoPlist if (parameters.type == Type.macOS) { @@ -112,7 +126,7 @@ class XcodeBuildArchiveTask extends AbstractXcodeBuildTask { def bundleVersion = getValueFromBundleInfoPlist(parameters.applicationBundle, "CFBundleVersion") List icons - if (parameters.type == Type.iOS) { + if (parameters.type == Type.iOS || parameters.type == Type.tvOS) { icons = getiOSIcons() } else { icons = getMacOSXIcons() @@ -160,7 +174,7 @@ class XcodeBuildArchiveTask extends AbstractXcodeBuildTask { content.append(" CreationDate\n") content.append(" " + creationDate + "\n") content.append(" Name\n") - content.append(" " + name + "\n") + content.append(" " + name + "\n") content.append(" SchemeName\n") content.append(" " + schemeName + "\n") content.append("\n") @@ -171,7 +185,7 @@ class XcodeBuildArchiveTask extends AbstractXcodeBuildTask { } - def createFrameworks(def archiveDirectory, Xcodebuild xcodebuild) { + def createFrameworks(def archiveDirectory, Xcodebuild xcodebuild) { File frameworksPath = new File(archiveDirectory, "Products/Applications/" + parameters.applicationBundleName + "/Frameworks") if (frameworksPath.exists()) { @@ -184,7 +198,10 @@ class XcodeBuildArchiveTask extends AbstractXcodeBuildTask { logger.debug("swiftlibs to add: {}", libNames); - File swiftLibs = new File(xcodebuild.getToolchainDirectory(), "usr/lib/swift/iphoneos") + File swiftLibs = new File(xcodebuild.getToolchainDirectory(), + "usr/lib/swift/" + getSwiftLibFolderName()) + + logger.debug("swiftlibs to add: {}", swiftLibs); swiftLibs.eachFile() { logger.debug("candidate for copy? {}: {}", it.name, libNames.contains(it.name)) @@ -196,14 +213,32 @@ class XcodeBuildArchiveTask extends AbstractXcodeBuildTask { } + public String getSwiftLibFolderName() { + String result + switch (parameters.type) { + case Type.tvOS: + result = "appletvos" + break + + case Type.iOS: + result = "iphoneos" + break + + default: + break + } + + return result + } + def getSwiftSupportDirectory() { def swiftSupportPath = "SwiftSupport" if (xcode.version.major > 6) { - swiftSupportPath += "/iphoneos" + swiftSupportPath += "/" + getSwiftLibFolderName() } - File swiftSupportDirectory = new File(getArchiveDirectory(), swiftSupportPath); + File swiftSupportDirectory = new File(getArchiveDirectory(), swiftSupportPath) if (!swiftSupportDirectory.exists()) { swiftSupportDirectory.mkdirs() } @@ -223,7 +258,6 @@ class XcodeBuildArchiveTask extends AbstractXcodeBuildTask { deleteDirectoryIfEmpty(appPath, "Frameworks") - } def deleteFrameworksInExtension(File applicationsDirectory) { @@ -258,8 +292,8 @@ class XcodeBuildArchiveTask extends AbstractXcodeBuildTask { def createEntitlements(File bundle) { - if (parameters.type != Type.iOS) { - logger.warn("Entitlements handling is only implemented for iOS!") + if (parameters.type != Type.iOS && parameters.type != Type.tvOS) { + logger.warn("Entitlements handling is only implemented for iOS and tvOS!") return } @@ -288,8 +322,13 @@ class XcodeBuildArchiveTask extends AbstractXcodeBuildTask { return } - String applicationIdentifier = "UNKNOWN00ID"; // if UNKNOWN00ID this means that not application identifier is found an this value is used as fallback - File provisioningProfile = ProvisioningProfileReader.getProvisionFileForIdentifier(bundleIdentifier, project.xcodebuild.signing.mobileProvisionFile, this.commandRunner, this.plistHelper) + String applicationIdentifier = "UNKNOWN00ID"; + + // if UNKNOWN00ID this means that not application identifier is found an this value is used as fallback + File provisioningProfile = ProvisioningProfileReader.getProvisionFileForIdentifier(bundleIdentifier, + project.extensions.getByType(XcodeBuildPluginExtension).signing.registeredProvisioningFiles.getFiles().asList(), + this.commandRunner, + this.plistHelper) if (provisioningProfile != null && provisioningProfile.exists()) { ProvisioningProfileReader reader = new ProvisioningProfileReader(provisioningProfile, commandRunner) applicationIdentifier = reader.getApplicationIdentifierPrefix() @@ -336,8 +375,9 @@ class XcodeBuildArchiveTask extends AbstractXcodeBuildTask { @TaskAction def archive() { + println "archive" parameters = project.xcodebuild.xcodebuildParameters.merge(parameters) - if (parameters.isSimulatorBuildOf(Type.iOS)) { + if (parameters.isSimulatorBuildOf(Type.iOS) || parameters.isSimulatorBuildOf(Type.tvOS)) { logger.debug("Create zip archive") // create zip archive @@ -367,7 +407,6 @@ class XcodeBuildArchiveTask extends AbstractXcodeBuildTask { return } - // create xcarchive // copy application bundle copy(parameters.applicationBundle, getApplicationsDirectory()) @@ -390,6 +429,7 @@ class XcodeBuildArchiveTask extends AbstractXcodeBuildTask { } File applicationsDirectory = getApplicationsDirectory() + File archiveDirectory = getArchiveDirectory() createInfoPlist(archiveDirectory) createFrameworks(archiveDirectory, xcodebuild) @@ -398,9 +438,11 @@ class XcodeBuildArchiveTask extends AbstractXcodeBuildTask { deleteFrameworksInExtension(applicationsDirectory) copyBCSymbolMaps(archiveDirectory) - if (project.xcodebuild.type == Type.iOS) { + if (project.xcodebuild.type == Type.iOS || project.xcodebuild.type == Type.tvOS) { File applicationFolder = new File(getArchiveDirectory(), "Products/Applications/" + parameters.applicationBundleName) convertInfoPlistToBinary(applicationFolder) + + removeUnneededDylibsFromBundle(applicationFolder) } logger.debug("create archive done") @@ -422,12 +464,20 @@ class XcodeBuildArchiveTask extends AbstractXcodeBuildTask { } + // TODO: Define a `exportOptionsPlist` to avoid that kind of issue + def removeUnneededDylibsFromBundle(File bundle) { + File libswiftRemoteMirror = new File(bundle, "libswiftRemoteMirror.dylib") + if (libswiftRemoteMirror.exists()) { + libswiftRemoteMirror.delete() + } + } + def deleteXCTestIfExists(File applicationsDirectory) { File plugins = new File(applicationsDirectory, project.xcodebuild.applicationBundle.name + "/Contents/Plugins") if (!plugins.exists()) { return } - plugins.eachFile (FileType.DIRECTORIES) { file -> + plugins.eachFile(FileType.DIRECTORIES) { file -> if (file.toString().endsWith("xctest")) { FileUtils.deleteDirectory(file) return true @@ -485,8 +535,7 @@ class XcodeBuildArchiveTask extends AbstractXcodeBuildTask { File getArchiveDirectory() { - - def archiveDirectoryName = XcodeBuildArchiveTask.ARCHIVE_FOLDER + "/" + project.xcodebuild.bundleName + def archiveDirectoryName = PathHelper.FOLDER_ARCHIVE + "/" + project.xcodebuild.bundleName if (project.xcodebuild.bundleNameSuffix != null) { archiveDirectoryName += project.xcodebuild.bundleNameSuffix diff --git a/plugin/src/main/groovy/org/openbakery/carthage/AbstractCarthageTaskBase.groovy b/plugin/src/main/groovy/org/openbakery/carthage/AbstractCarthageTaskBase.groovy new file mode 100644 index 00000000..d26c96ad --- /dev/null +++ b/plugin/src/main/groovy/org/openbakery/carthage/AbstractCarthageTaskBase.groovy @@ -0,0 +1,98 @@ +package org.openbakery.carthage + +import groovy.transform.CompileStatic +import org.gradle.api.provider.Provider +import org.gradle.api.tasks.* +import org.openbakery.AbstractXcodeTask +import org.openbakery.CommandRunnerException +import org.openbakery.XcodeBuildPluginExtension +import org.openbakery.xcode.Type + +@CompileStatic +abstract class AbstractCarthageTaskBase extends AbstractXcodeTask { + + static final String ACTION_BOOTSTRAP = "bootstrap" + static final String ACTION_UPDATE = "update" + static final String ARG_CACHE_BUILDS = "--cache-builds" + static final String ARG_PLATFORM = "--platform" + static final String CARTHAGE_FILE = "Cartfile" + static final String CARTHAGE_FILE_RESOLVED = "Cartfile.resolved" + static final String CARTHAGE_PLATFORM_IOS = "iOS" + static final String CARTHAGE_PLATFORM_MACOS = "Mac" + static final String CARTHAGE_PLATFORM_TVOS = "tvOS" + static final String CARTHAGE_PLATFORM_WATCHOS = "watchOS" + static final String CARTHAGE_USR_BIN_PATH = "/usr/local/bin/carthage" + + AbstractCarthageTaskBase() { + super() + } + + @Input + @Optional + String getRequiredXcodeVersion() { + return getProjectXcodeVersion() + } + + @InputFile + @Optional + Provider getCartFile() { + // Cf https://github.com/gradle/gradle/issues/2016 + File file = project.rootProject.file(CARTHAGE_FILE) + return project.provider { + file.exists() ? file + : File.createTempFile(CARTHAGE_FILE, "") + } + } + + @InputFile + @Optional + Provider getCartResolvedFile() { + // Cf https://github.com/gradle/gradle/issues/2016 + File file = project.rootProject.file(CARTHAGE_FILE_RESOLVED) + return project.provider { + file.exists() ? file + : File.createTempFile(CARTHAGE_FILE_RESOLVED, "resolved") + } + } + + @Input + String getCarthagePlatformName() { + switch (project.extensions.findByType(XcodeBuildPluginExtension).type) { + case Type.iOS: return CARTHAGE_PLATFORM_IOS + case Type.tvOS: return CARTHAGE_PLATFORM_TVOS + case Type.macOS: return CARTHAGE_PLATFORM_MACOS + case Type.watchOS: return CARTHAGE_PLATFORM_WATCHOS + default: return 'all' + } + } + + @OutputDirectory + @PathSensitive(PathSensitivity.NAME_ONLY) + Provider getOutputDirectory() { + return project.provider { + project.rootProject.file("Carthage/Build/" + getCarthagePlatformName()) + } + } + + String getCarthageCommand() { + try { + return commandRunner.runWithResult("which", "carthage") + } catch (CommandRunnerException exception) { + // ignore, because try again with full path below + } + + try { + commandRunner.runWithResult("ls", CARTHAGE_USR_BIN_PATH) + return CARTHAGE_USR_BIN_PATH + } catch (CommandRunnerException exception) { + // ignore, because blow an exception is thrown + } + throw new IllegalStateException("The carthage command was not found. Make sure that Carthage is installed") + } + + boolean hasCartFile() { + return project.rootProject + .file(CARTHAGE_FILE) + .exists() + } +} diff --git a/plugin/src/main/groovy/org/openbakery/carthage/CarthageBootStrapTask.groovy b/plugin/src/main/groovy/org/openbakery/carthage/CarthageBootStrapTask.groovy new file mode 100644 index 00000000..94df3517 --- /dev/null +++ b/plugin/src/main/groovy/org/openbakery/carthage/CarthageBootStrapTask.groovy @@ -0,0 +1,154 @@ +package org.openbakery.carthage + +import groovy.transform.CompileStatic +import org.gradle.api.Action +import org.gradle.api.DefaultTask +import org.gradle.api.Transformer +import org.gradle.api.file.RegularFileProperty +import org.gradle.api.provider.Property +import org.gradle.api.tasks.* +import org.gradle.process.ExecSpec +import org.gradle.workers.WorkerExecutor +import org.openbakery.CommandRunner +import org.openbakery.CommandRunnerException +import org.openbakery.xcode.Type +import org.openbakery.xcode.Xcode + +import javax.inject.Inject + +@CacheableTask +@CompileStatic +class CarthageBootStrapTask extends DefaultTask { + + @Input + @Optional + Property requiredXcodeVersion = project.objects.property(String) + + @Input + Property carthagePlatformName = project.objects.property(String) + + @InputFile + @PathSensitive(PathSensitivity.NAME_ONLY) + RegularFileProperty cartFile = project.layout.fileProperty() + + @OutputDirectory + Property outputDirectory = project.objects.property(File) + + public static final String NAME = "carthageBootstrap" + + final Property platform = project.objects.property(Type) + final Property xcodeProperty = project.objects.property(Xcode) + final Property commandRunnerProperty = project.objects.property(CommandRunner) + + final WorkerExecutor workerExecutor + + static final String CARTHAGE_FILE = "Cartfile.resolved" + static final String CARTHAGE_PLATFORM_IOS = "iOS" + static final String CARTHAGE_PLATFORM_MACOS = "Mac" + static final String CARTHAGE_PLATFORM_TVOS = "tvOS" + static final String CARTHAGE_PLATFORM_WATCHOS = "watchOS" + static final String CARTHAGE_USR_BIN_PATH = "/usr/local/bin/carthage" + static final String ACTION_BOOTSTRAP = "bootstrap" + static final String ARG_PLATFORM = "--platform" + static final String ARG_CACHE_BUILDS = "--cache-builds" + + @Inject + CarthageBootStrapTask(WorkerExecutor workerExecutor) { + super() + this.workerExecutor = workerExecutor + + setDescription "Check out and build the Carthage project dependencies" + + cartFile.set(new File(project.rootProject.rootDir, CARTHAGE_FILE)) + + carthagePlatformName.set(platform.map(new Transformer() { + @Override + String transform(Type type) { + return typeToCarthagePlatform(type) + } + })) + + outputDirectory.set(carthagePlatformName.map(new Transformer() { + @Override + File transform(String platformName) { + return new File(project.rootProject.rootDir, "Carthage/Build/" + platformName) + } + })) + + xcodeProperty.set(commandRunnerProperty.map(new Transformer() { + @Override + Xcode transform(CommandRunner commandRunner) { + return new Xcode(commandRunner) + } + })) + + onlyIf { + return cartFile.asFile.get().exists() + } + } + + void setXcode(Xcode xcode) { + this.xcode = xcode + } + + @TaskAction + void update() { + logger.warn('Bootstrap Carthage for platform ' + carthagePlatformName) + project.exec(new Action() { + @Override + void execute(ExecSpec execSpec) { + execSpec.args = [ACTION_BOOTSTRAP, + ARG_CACHE_BUILDS, + "--new-resolver", + "--color", "always", + ARG_PLATFORM, + carthagePlatformName.getOrNull().toString()] as List + + execSpec.environment(getEnvValues()) + execSpec.executable = getCarthageCommand() + execSpec.workingDir(project.rootProject.projectDir) + } + }) + } + + private final Map getEnvValues() { + final Map envValues + if (requiredXcodeVersion.present) { + envValues = xcodeProperty.get() + .getXcodeSelectEnvValue(requiredXcodeVersion.getOrNull()) + } else { + envValues = [:] + } + + return envValues + } + + private String typeToCarthagePlatform(Type type) { + switch (type) { + case Type.iOS: return CARTHAGE_PLATFORM_IOS + case Type.tvOS: return CARTHAGE_PLATFORM_TVOS + case Type.macOS: return CARTHAGE_PLATFORM_MACOS + case Type.watchOS: return CARTHAGE_PLATFORM_WATCHOS + default: return 'all' + } + } + + private String getCarthageCommand() { + try { + return commandRunnerProperty.get() + .runWithResult("which", "carthage") + } catch (CommandRunnerException exception) { + // ignore, because try again with full path below + } + + try { + commandRunnerProperty.get() + .runWithResult("ls", CARTHAGE_USR_BIN_PATH) + return CARTHAGE_USR_BIN_PATH + } catch (CommandRunnerException exception) { + // ignore, because blow an exception is thrown + } + + throw new IllegalStateException("The carthage command was not found. Make sure that Carthage is installed") + } +} diff --git a/plugin/src/main/groovy/org/openbakery/carthage/CarthageCleanTask.groovy b/plugin/src/main/groovy/org/openbakery/carthage/CarthageCleanTask.groovy index 19ead8e6..43ec92e6 100644 --- a/plugin/src/main/groovy/org/openbakery/carthage/CarthageCleanTask.groovy +++ b/plugin/src/main/groovy/org/openbakery/carthage/CarthageCleanTask.groovy @@ -14,6 +14,4 @@ class CarthageCleanTask extends DefaultTask { def clean() { project.file("Carthage").deleteDir() } - - } diff --git a/plugin/src/main/groovy/org/openbakery/carthage/CarthageUpdateTask.groovy b/plugin/src/main/groovy/org/openbakery/carthage/CarthageUpdateTask.groovy index 60d38bf7..bc742f4a 100644 --- a/plugin/src/main/groovy/org/openbakery/carthage/CarthageUpdateTask.groovy +++ b/plugin/src/main/groovy/org/openbakery/carthage/CarthageUpdateTask.groovy @@ -1,71 +1,14 @@ package org.openbakery.carthage -import org.gradle.api.provider.Provider -import org.gradle.api.tasks.* +import org.gradle.api.tasks.TaskAction import org.gradle.internal.logging.text.StyledTextOutputFactory -import org.openbakery.AbstractXcodeTask import org.openbakery.output.ConsoleOutputAppender -import org.openbakery.xcode.Type -class CarthageUpdateTask extends AbstractXcodeTask { - - static final String ACTION_UPDATE = "update" - static final String ARG_CACHE_BUILDS = "--cache-builds" - static final String ARG_PLATFORM = "--platform" - static final String CARTHAGE_FILE = "Cartfile" - static final String CARTHAGE_FILE_RESOLVED = "Cartfile.resolved" - static final String CARTHAGE_PLATFORM_IOS = "iOS" - static final String CARTHAGE_PLATFORM_MACOS = "Mac" - static final String CARTHAGE_PLATFORM_TVOS = "tvOS" - static final String CARTHAGE_PLATFORM_WATCHOS = "watchOS" - static final String CARTHAGE_USR_BIN_PATH = "/usr/local/bin/carthage" +class CarthageUpdateTask extends AbstractCarthageTaskBase { CarthageUpdateTask() { super() - newOutputDirectory() - setDescription "Installs the carthage dependencies for the given project" - } - - @InputFile - @Optional - @PathSensitive(PathSensitivity.RELATIVE) - Provider getCartFile() { - // Cf https://github.com/gradle/gradle/issues/2016 - File file = project.rootProject.file(CARTHAGE_FILE) - return project.provider { - file.exists() ? file - : File.createTempFile(CARTHAGE_FILE, "") - } - } - - @InputFile - @Optional - @PathSensitive(PathSensitivity.RELATIVE) - Provider getCartResolvedFile() { - // Cf https://github.com/gradle/gradle/issues/2016 - File file = project.rootProject.file(CARTHAGE_FILE_RESOLVED) - return project.provider { - file.exists() ? file - : File.createTempFile(CARTHAGE_FILE_RESOLVED, "resolved") - } - } - - @Input - String getCarthagePlatformName() { - switch (project.xcodebuild.type) { - case Type.iOS: return CARTHAGE_PLATFORM_IOS - case Type.tvOS: return CARTHAGE_PLATFORM_TVOS - case Type.macOS: return CARTHAGE_PLATFORM_MACOS - case Type.watchOS: return CARTHAGE_PLATFORM_WATCHOS - default: return 'all' - } - } - - @OutputDirectory - Provider getOutputDirectory() { - return project.provider { - project.rootProject.file("Carthage/Build/" + getCarthagePlatformName()) - } + setDescription "Update and rebuild the Carthage project dependencies" } @TaskAction @@ -73,7 +16,9 @@ class CarthageUpdateTask extends AbstractXcodeTask { if (hasCartFile()) { logger.info('Update Carthage for platform ' + carthagePlatformName) - def output = services.get(StyledTextOutputFactory).create(CarthageUpdateTask) + def output = services.get(StyledTextOutputFactory) + .create(CarthageUpdateTask) + commandRunner.run( project.projectDir.absolutePath, [getCarthageCommand(), @@ -84,26 +29,4 @@ class CarthageUpdateTask extends AbstractXcodeTask { new ConsoleOutputAppender(output)) } } - - String getCarthageCommand() { - try { - return commandRunner.runWithResult("which", "carthage") - } catch (CommandRunnerException) { - // ignore, because try again with full path below - } - - try { - commandRunner.runWithResult("ls", CARTHAGE_USR_BIN_PATH) - return CARTHAGE_USR_BIN_PATH - } catch (CommandRunnerException) { - // ignore, because blow an exception is thrown - } - throw new IllegalStateException("The carthage command was not found. Make sure that Carthage is installed") - } - - boolean hasCartFile() { - return project.rootProject - .file(CARTHAGE_FILE) - .exists() - } } diff --git a/plugin/src/main/groovy/org/openbakery/coverage/CoverageTask.groovy b/plugin/src/main/groovy/org/openbakery/coverage/CoverageTask.groovy index 64b68eab..4c47646e 100644 --- a/plugin/src/main/groovy/org/openbakery/coverage/CoverageTask.groovy +++ b/plugin/src/main/groovy/org/openbakery/coverage/CoverageTask.groovy @@ -4,6 +4,7 @@ import org.apache.commons.io.FilenameUtils import org.apache.commons.lang.StringUtils import org.gradle.api.tasks.TaskAction import org.openbakery.AbstractXcodeTask +import org.openbakery.XcodeBuildPluginExtension import org.openbakery.xcode.Destination class CoverageTask extends AbstractXcodeTask { @@ -48,17 +49,17 @@ class CoverageTask extends AbstractXcodeTask { def zipFilename = version + ".zip" def zip = new File(project.coverage.outputDirectory, zipFilename) def url = 'https://github.com/gcovr/gcovr/archive/' + zipFilename - ant.get(src: url, dest: project.coverage.outputDirectory, verbose:true) - ant.unzip(src: zip, dest:project.coverage.outputDirectory) + ant.get(src: url, dest: project.coverage.outputDirectory, verbose: true) + ant.unzip(src: zip, dest: project.coverage.outputDirectory) def gcovrCommand = new File(project.coverage.outputDirectory, 'gcovr-' + version + '/scripts/gcovr').absolutePath def commandList = [ - 'python', - gcovrCommand, - '-r', - '.' + 'python', + gcovrCommand, + '-r', + '.' ] String exclude = project.coverage.exclude @@ -100,9 +101,13 @@ class CoverageTask extends AbstractXcodeTask { for (Destination destination : project.coverage.testResultDestinations) { possibleDirectories.add("Build/ProfileData/" + destination.id + "/Coverage.profdata") } - + for (String directory : possibleDirectories) { - this.profileData = new File(project.xcodebuild.derivedDataPath, directory) + this.profileData = new File(project.extensions.findByType(XcodeBuildPluginExtension) + .derivedDataPath + .asFile + .getOrNull(), + directory) if (this.profileData.exists()) { return this.profileData } diff --git a/plugin/src/main/groovy/org/openbakery/crashlytics/CrashlyticsUploadTask.groovy b/plugin/src/main/groovy/org/openbakery/crashlytics/CrashlyticsUploadTask.groovy index 3b648510..2a750d29 100755 --- a/plugin/src/main/groovy/org/openbakery/crashlytics/CrashlyticsUploadTask.groovy +++ b/plugin/src/main/groovy/org/openbakery/crashlytics/CrashlyticsUploadTask.groovy @@ -19,14 +19,14 @@ import org.gradle.api.tasks.TaskAction import org.gradle.internal.logging.text.StyledTextOutput import org.gradle.internal.logging.text.StyledTextOutputFactory import org.openbakery.AbstractDistributeTask -import org.openbakery.XcodePlugin import org.openbakery.output.ConsoleOutputAppender +import org.openbakery.packaging.PackageTask class CrashlyticsUploadTask extends AbstractDistributeTask { CrashlyticsUploadTask() { super() - dependsOn(XcodePlugin.PACKAGE_TASK_NAME) + dependsOn(PackageTask.NAME) this.description = "Upload the IPA to crashlytics for crash reports" } @@ -86,4 +86,4 @@ class CrashlyticsUploadTask extends AbstractDistributeTask { } -} \ No newline at end of file +} diff --git a/plugin/src/main/groovy/org/openbakery/extension/Signing.groovy b/plugin/src/main/groovy/org/openbakery/extension/Signing.groovy new file mode 100644 index 00000000..6d93319a --- /dev/null +++ b/plugin/src/main/groovy/org/openbakery/extension/Signing.groovy @@ -0,0 +1,163 @@ +package org.openbakery.extension + +import org.gradle.api.Project +import org.gradle.api.file.DirectoryProperty +import org.gradle.api.file.RegularFileProperty +import org.gradle.api.provider.ListProperty +import org.gradle.api.provider.Property +import org.gradle.api.provider.Provider +import org.gradle.api.tasks.Internal +import org.openbakery.CommandRunner +import org.openbakery.codesign.CodesignParameters +import org.openbakery.signing.ProvisioningFile +import org.openbakery.signing.SigningMethod +import org.openbakery.util.PathHelper + +import javax.inject.Inject + +class Signing { + + final DirectoryProperty provisioningDestinationRoot = project.layout.directoryProperty() + final DirectoryProperty signingDestinationRoot = project.layout.directoryProperty() + final ListProperty mobileProvisionList = project.objects.listProperty(String) + final Property timeout = project.objects.property(Integer) + final Property signingMethod = project.objects.property(SigningMethod) + final Property certificateFriendlyName = project.objects.property(String) + final Property certificatePassword = project.objects.property(String) + final RegularFileProperty certificate = project.layout.fileProperty() + final Property certificateURI = project.objects.property(String) + final RegularFileProperty entitlementsFile = project.layout.fileProperty() + final RegularFileProperty keychain = project.layout.fileProperty() + final RegularFileProperty keyChainFile = project.layout.fileProperty() + + @Internal + final Provider> registeredProvisioningFiles = project.objects.listProperty(File) + + @Internal + final Provider> registeredProvisioning = project.objects.listProperty(ProvisioningFile) + + @Internal + final RegularFileProperty xcConfigFile = project.layout.fileProperty() + + @Deprecated + final Property> entitlementsMap = project.objects.property(Map) + + @Internal + Object keychainPathInternal + + String identity + String plugin + + public static final String KEYCHAIN_NAME_BASE = "gradle-" + + /** + * internal parameters + */ + private final Project project + private final CommandRunner commandRunner + + @Inject + Signing(Project project, + CommandRunner commandRunner) { + + this.commandRunner = commandRunner + this.project = project + this.signingDestinationRoot.set(project.layout.buildDirectory.dir("codesign")) + this.provisioningDestinationRoot.set(project.layout.buildDirectory.dir("provision")) + + this.keyChainFile.set(signingDestinationRoot.file(KEYCHAIN_NAME_BASE + + System.currentTimeMillis() + + ".keychain")) + this.timeout.set(3600) + + this.xcConfigFile.set(project.layout + .buildDirectory + .file(PathHelper.FOLDER_ARCHIVE + "/" + PathHelper.GENERATED_XCARCHIVE_FILE_NAME)) + } + + void setKeychain(Object keychain) { + if (keychain instanceof String && keychain.matches("^~/.*")) { + keychain = keychain.replaceFirst("~", System.getProperty('user.home')) + } + this.keychain.set(project.file(keychain)) + } + + public void setMethod(String method) { + signingMethod.set(SigningMethod.fromString(method) + .orElseThrow { + new IllegalArgumentException("Method : $method is not a valid export method") + }) + } + + File getKeychainPathInternal() { + return project.file(keychainPathInternal) + } + + void entitlements(Map entitlements) { + if (!this.entitlementsMap.present) { + this.entitlementsMap.set(entitlements) + } else { + this.entitlementsMap.get() << entitlements + } + } + + boolean hasEntitlementsFile() { + return entitlementsFile.present + } + + String getIdentity() { + return this.identity + } + + @Deprecated + void setEntitlementsFile(String value) { + entitlementsFile.set(new File(value)) + } + + void addMobileProvisionFile(File file) { + this.mobileProvisionList.add(file.toURI().toString()) + } + + @Deprecated + void setMobileProvisionURI(String value) { + this.mobileProvisionList.add(value) + } + + @Deprecated + void setMobileProvisionURI(String... values) { + this.mobileProvisionList.get().addAll(values.toList()) + } + + CodesignParameters getCodesignParameters() { + CodesignParameters result = new CodesignParameters() + result.signingIdentity = getIdentity() + if (registeredProvisioningFiles.present) { + result.mobileProvisionFiles = new ArrayList(registeredProvisioningFiles.get() + .asList() + .toArray() as ArrayList) + } + result.keychain = getKeychain().asFile.getOrNull() as File + result.signingIdentity = identity + result.entitlements = entitlementsMap.getOrNull() + result.entitlementsFile = entitlementsFile.asFile.getOrNull() + + return result + } + + @Override + public String toString() { + if (this.keychain != null) { + return "Signing{" + + " identity='" + identity + '\'' + + ", mobileProvisionURI='" + mobileProvisionList.get() + '\'' + + ", keychain='" + keychain + '\'' + + '}'; + } + return "Signing{" + + " identity='" + identity + '\'' + + ", certificateURI='" + certificate.getOrNull() + '\'' + + ", certificatePassword='" + certificatePassword.getOrNull() + '\'' + + ", mobileProvisionURI='" + mobileProvisionList.get() + '\'' + + '}'; + } +} diff --git a/plugin/src/main/groovy/org/openbakery/hockeyapp/HockeyAppUploadTask.groovy b/plugin/src/main/groovy/org/openbakery/hockeyapp/HockeyAppUploadTask.groovy index 6b9a887e..e15723d7 100644 --- a/plugin/src/main/groovy/org/openbakery/hockeyapp/HockeyAppUploadTask.groovy +++ b/plugin/src/main/groovy/org/openbakery/hockeyapp/HockeyAppUploadTask.groovy @@ -15,31 +15,12 @@ */ package org.openbakery.hockeyapp -import org.apache.commons.io.FilenameUtils import org.apache.http.Consts -import org.apache.http.HttpEntity -import org.apache.http.HttpHost -import org.apache.http.HttpResponse -import org.apache.http.client.HttpClient -import org.apache.http.client.config.RequestConfig -import org.apache.http.client.methods.CloseableHttpResponse -import org.apache.http.client.methods.HttpPost import org.apache.http.entity.ContentType -import org.apache.http.entity.mime.MultipartEntity -import org.apache.http.entity.mime.MultipartEntityBuilder -import org.apache.http.entity.mime.content.FileBody -import org.apache.http.entity.mime.content.StringBody -import org.apache.http.impl.client.CloseableHttpClient -import org.apache.http.impl.client.DefaultHttpClient -import org.apache.http.impl.client.HttpClients -import org.apache.http.util.EntityUtils -import org.gradle.api.DefaultTask import org.gradle.api.tasks.TaskAction import org.openbakery.AbstractDistributeTask import org.openbakery.http.HttpUpload -import java.util.regex.Pattern - class HockeyAppUploadTask extends AbstractDistributeTask { @@ -130,7 +111,7 @@ class HockeyAppUploadTask extends AbstractDistributeTask { httpUpload.url = HOCKEY_APP_API_URL + project.hockeyapp.appID + "/provisioning_profiles" - if (project.xcodebuild.signing.mobileProvisionFile.size() != 1) { + if (project.xcodebuild.signing.registeredProvisioningFiles.get().size() != 1) { logger.debug("mobileProvisionFile not found"); return; } @@ -141,7 +122,7 @@ class HockeyAppUploadTask extends AbstractDistributeTask { } httpUpload.postRequest(getHttpHeaders(), - ["mobileprovision": project.xcodebuild.signing.mobileProvisionFile.get(0)] + ["mobileprovision": project.xcodebuild.signing.registeredProvisioningFiles.get().get(0)] ) } diff --git a/plugin/src/main/groovy/org/openbakery/oclint/OCLintTask.groovy b/plugin/src/main/groovy/org/openbakery/oclint/OCLintTask.groovy index c0d75c1f..6b725c4e 100644 --- a/plugin/src/main/groovy/org/openbakery/oclint/OCLintTask.groovy +++ b/plugin/src/main/groovy/org/openbakery/oclint/OCLintTask.groovy @@ -3,6 +3,7 @@ package org.openbakery.oclint import org.apache.commons.io.FilenameUtils import org.gradle.api.tasks.TaskAction import org.openbakery.AbstractXcodeTask +import org.openbakery.util.SystemUtil class OCLintTask extends AbstractXcodeTask { @@ -37,7 +38,7 @@ class OCLintTask extends AbstractXcodeTask { def getDownloadURL() { - if (getOSVersion().minor >= 12) { + if (SystemUtil.getOsVersion().minor >= 12) { return "https://github.com/oclint/oclint/releases/download/v0.13/oclint-0.13-x86_64-darwin-17.0.0.tar.gz" } return "https://github.com/oclint/oclint/releases/download/v0.13/oclint-0.13-x86_64-darwin-16.7.0.tar.gz" diff --git a/plugin/src/main/groovy/org/openbakery/output/ConsoleOutputAppender.java b/plugin/src/main/groovy/org/openbakery/output/ConsoleOutputAppender.java index fa05b9a8..bcd1c5ff 100644 --- a/plugin/src/main/groovy/org/openbakery/output/ConsoleOutputAppender.java +++ b/plugin/src/main/groovy/org/openbakery/output/ConsoleOutputAppender.java @@ -15,7 +15,6 @@ public ConsoleOutputAppender(StyledTextOutput output) { @Override public void append(String line) { output.withStyle(StyledTextOutput.Style.Info).println(line); - } } diff --git a/plugin/src/main/groovy/org/openbakery/packaging/PackageLegacyTask.groovy b/plugin/src/main/groovy/org/openbakery/packaging/PackageLegacyTask.groovy new file mode 100644 index 00000000..2d78576c --- /dev/null +++ b/plugin/src/main/groovy/org/openbakery/packaging/PackageLegacyTask.groovy @@ -0,0 +1,333 @@ +package org.openbakery.packaging + +import org.apache.commons.io.FileUtils +import org.apache.commons.io.FilenameUtils +import org.gradle.api.Task +import org.gradle.api.specs.Spec +import org.gradle.api.tasks.TaskAction +import org.gradle.internal.logging.text.StyledTextOutput +import org.gradle.internal.logging.text.StyledTextOutputFactory +import org.openbakery.AbstractDistributeTask +import org.openbakery.CommandRunnerException +import org.openbakery.bundle.ApplicationBundle +import org.openbakery.codesign.Codesign +import org.openbakery.codesign.CodesignParameters +import org.openbakery.codesign.ProvisioningProfileReader +import org.openbakery.signing.KeychainCreateTask +import org.openbakery.signing.ProvisioningInstallTask +import org.openbakery.util.PathHelper +import org.openbakery.xcode.Type + +class PackageLegacyTask extends AbstractDistributeTask { + + public static final String NAME = "packageLegacy" + + public static final String PACKAGE_PATH = "package" + File outputPath + + + private List appBundles + + String applicationBundleName + StyledTextOutput output + + + CodesignParameters codesignParameters = new CodesignParameters() + + PackageLegacyTask() { + super() + setDescription("Signs the app bundle that was created by the build and creates the ipa") + dependsOn( + KeychainCreateTask.TASK_NAME, + ProvisioningInstallTask.TASK_NAME + ) + + onlyIf(new Spec() { + @Override + boolean isSatisfiedBy(Task task) { + return getXcodeExtension().getType() == Type.watchOS || + getXcodeExtension().getType() == Type.macOS + } + }) + + output = services.get(StyledTextOutputFactory).create(PackageLegacyTask) + + } + + + @TaskAction + void packageApplication() throws IOException { + if (project.xcodebuild.isSimulatorBuildOf(Type.iOS) || project.xcodebuild.isSimulatorBuildOf(Type.tvOS)) { + logger.lifecycle("not a device build, so no codesign and packaging needed") + return + } + outputPath = PathHelper.resolvePackageFolder(project) + + File applicationFolder = createApplicationFolder() + + def applicationName = getApplicationNameFromArchive() + copy(getApplicationBundleDirectory(), applicationFolder) + + applicationBundleName = applicationName + ".app" + + File applicationPath = new File(applicationFolder, applicationBundleName) + + // copy onDemandResources + File onDemandResources = new File(getProductsDirectory(), "OnDemandResources") + if (onDemandResources.exists()) { + copy(onDemandResources, applicationPath) + } + + File bcSymbolsMaps = new File(getArchiveDirectory(), "BCSymbolMaps") + if (bcSymbolsMaps.exists()) { + copy(bcSymbolsMaps, applicationFolder.parentFile) + } + + enumerateExtensionSupportDirectories(getArchiveDirectory()) { File supportDirectory -> + copy(supportDirectory, applicationFolder.parentFile) + } + + ApplicationBundle applicationBundle = new ApplicationBundle(applicationPath, project.xcodebuild.type, project.xcodebuild.simulator) + appBundles = applicationBundle.getBundles() + + File resourceRules = new File(applicationFolder, applicationBundleName + "/ResourceRules.plist") + if (resourceRules.exists()) { + resourceRules.delete() + } + + + File infoPlist = getInfoPlistFile() + + try { + plistHelper.deleteValueFromPlist(infoPlist, "CFBundleResourceSpecification") + } catch (CommandRunnerException ex) { + // ignore, this means that the CFBundleResourceSpecification was not in the infoPlist + } + + def signSettingsAvailable = true + if (project.xcodebuild.signing.mobileProvisionFile == null) { + logger.warn('No mobile provision file provided.') + signSettingsAvailable = false; + } else if (!project.xcodebuild.signing.keychainPathInternal.exists()) { + logger.warn('No certificate or keychain found.') + signSettingsAvailable = false; + } + + codesignParameters.mergeMissing(project.xcodebuild.signing.codesignParameters) + codesignParameters.type = project.xcodebuild.type + codesignParameters.keychain = project.xcodebuild.signing.keychainPathInternal + Codesign codesign = new Codesign(xcode, codesignParameters, commandRunner, plistHelper) + + for (File bundle : appBundles) { + + if (isDeviceBuildiOStvOS()) { + removeFrameworkFromExtensions(bundle) + removeUnneededDylibsFromBundle(bundle) + embedProvisioningProfileToBundle(bundle) + } + + if (signSettingsAvailable) { + logger.info("Codesign app: {}", bundle) + codesign.sign(bundle) + } else { + String message = "Bundle not signed: " + bundle + output.withStyle(StyledTextOutput.Style.Failure).println(message) + } + } + + File appBundle = appBundles.last() + if (isDeviceBuildiOStvOS()) { + + boolean isAdHoc = isAdHoc(appBundle) + createIpa(applicationFolder, !isAdHoc) + } else { + createPackage(appBundle) + } + + } + + boolean isDeviceBuildiOStvOS() { + return project.xcodebuild.isDeviceBuildOf(Type.iOS) || project.xcodebuild.isDeviceBuildOf(Type.tvOS) + } + + boolean isAdHoc(File appBundle) { + File provisionFile = getProvisionFileForBundle(appBundle) + if (provisionFile == null) { + return false + } + ProvisioningProfileReader reader = new ProvisioningProfileReader(provisionFile, this.commandRunner, this.plistHelper) + return reader.isAdHoc() + } + + def removeFrameworkFromExtensions(File bundle) { + // appex extensions should not contain extensions + if (FilenameUtils.getExtension(bundle.toString()).equalsIgnoreCase("appex")) { + File frameworksPath = new File(bundle, "Frameworks") + if (frameworksPath.exists()) { + FileUtils.deleteDirectory(frameworksPath) + } + } + + } + + def removeUnneededDylibsFromBundle(File bundle) { + File libswiftRemoteMirror = new File(bundle, "libswiftRemoteMirror.dylib") + if (libswiftRemoteMirror.exists()) { + libswiftRemoteMirror.delete() + } + } + + File getProvisionFileForBundle(File bundle) { + String bundleIdentifier = getIdentifierForBundle(bundle) + return ProvisioningProfileReader.getProvisionFileForIdentifier(bundleIdentifier, project.xcodebuild.signing.mobileProvisionFile, this.commandRunner, this.plistHelper) + } + + + def addSwiftSupport(File payloadPath, String applicationBundleName) { + File frameworksPath = new File(payloadPath, applicationBundleName + "/Frameworks") + if (!frameworksPath.exists()) { + return null + } + + File swiftLibArchive = new File(getArchiveDirectory(), "SwiftSupport") + + if (swiftLibArchive.exists()) { + copy(swiftLibArchive, payloadPath.getParentFile()) + return new File(payloadPath.getParentFile(), "SwiftSupport") + } + return null + } + + + private void createZipPackage(File packagePath, String extension, boolean includeSwiftSupport) { + File packageBundle = new File(outputPath, getIpaFileName() + "." + extension) + if (!packageBundle.parentFile.exists()) { + packageBundle.parentFile.mkdirs() + } + + List filesToZip = [] + filesToZip << packagePath + + if (includeSwiftSupport) { + File swiftSupportPath = addSwiftSupport(packagePath, applicationBundleName) + if (swiftSupportPath != null) { + filesToZip << swiftSupportPath + } + } + + File bcSymbolMapsPath = new File(packagePath.getParentFile(), "BCSymbolMaps") + if (bcSymbolMapsPath.exists()) { + filesToZip << bcSymbolMapsPath + } + + enumerateExtensionSupportDirectories(packagePath.getParentFile()) { File supportDirectory -> + filesToZip << supportDirectory + } + + createZip(packageBundle, packagePath.getParentFile(), packagePath, *filesToZip) + } + + private void enumerateExtensionSupportDirectories(File parentDirectory, Closure closure) { + def directoryNames = ["MessagesApplicationExtensionSupport"] + + for (String name in directoryNames) { + File supportDirectory = new File(parentDirectory, name) + if (supportDirectory.exists()) { + closure(supportDirectory) + } + } + } + + private void createIpa(File payloadPath, boolean addSwiftSupport) { + createZipPackage(payloadPath, "ipa", addSwiftSupport) + } + + private void createPackage(File packagePath) { + + createZipPackage(packagePath, "zip", false) + } + + + private String getIdentifierForBundle(File bundle) { + File infoPlist + + if (isDeviceBuildiOStvOS()) { + infoPlist = new File(bundle, "Info.plist"); + } else { + infoPlist = new File(bundle, "Contents/Info.plist") + } + + String bundleIdentifier = plistHelper.getValueFromPlist(infoPlist, "CFBundleIdentifier") + return bundleIdentifier + } + + + private void embedProvisioningProfileToBundle(File bundle) { + File mobileProvisionFile = getProvisionFileForBundle(bundle) + if (mobileProvisionFile != null) { + File embeddedProvisionFile + + String profileExtension = FilenameUtils.getExtension(mobileProvisionFile.absolutePath) + embeddedProvisionFile = new File(getAppContentPath(bundle) + "embedded." + profileExtension) + + logger.info("provision profile - {}", embeddedProvisionFile) + + FileUtils.copyFile(mobileProvisionFile, embeddedProvisionFile) + } + } + + private File createSigningDestination(String name) throws IOException { + File destination = new File(outputPath, name) + if (destination.exists()) { + FileUtils.deleteDirectory(destination) + } + destination.mkdirs(); + return destination; + } + + private File createApplicationFolder() throws IOException { + + if (isDeviceBuildiOStvOS()) { + return createSigningDestination("Payload") + } else { + // same folder as signing + if (!outputPath.exists()) { + outputPath.mkdirs() + } + return outputPath + } + } + + private File getInfoPlistFile() { + return new File(getAppContentPath() + "Info.plist") + } + + private String getAppContentPath() { + + return getAppContentPath(appBundles.last()) + } + + private String getAppContentPath(File bundle) { + if (project.xcodebuild.type == Type.iOS || project.xcodebuild.type == Type.tvOS) { + return bundle.absolutePath + "/" + } + return bundle.absolutePath + "/Contents/" + } + + def getIpaFileName() { + if (project.xcodebuild.ipaFileName) { + return project.xcodebuild.ipaFileName + } else { + return getApplicationNameFromArchive() + } + } + + + String getSigningIdentity() { + return codesignParameters.signingIdentity + } + + void setSigningIdentity(String identity) { + codesignParameters.signingIdentity = identity + } +} diff --git a/plugin/src/main/groovy/org/openbakery/packaging/PackageTask.groovy b/plugin/src/main/groovy/org/openbakery/packaging/PackageTask.groovy index 0e83d88f..a51a5df9 100644 --- a/plugin/src/main/groovy/org/openbakery/packaging/PackageTask.groovy +++ b/plugin/src/main/groovy/org/openbakery/packaging/PackageTask.groovy @@ -1,322 +1,15 @@ package org.openbakery.packaging -import org.apache.commons.io.FileUtils -import org.apache.commons.io.FilenameUtils -import org.gradle.api.tasks.TaskAction -import org.gradle.internal.logging.text.StyledTextOutput -import org.gradle.internal.logging.text.StyledTextOutputFactory -import org.openbakery.AbstractDistributeTask -import org.openbakery.CommandRunnerException -import org.openbakery.bundle.ApplicationBundle -import org.openbakery.codesign.Codesign -import org.openbakery.codesign.CodesignParameters -import org.openbakery.xcode.Type -import org.openbakery.XcodePlugin -import org.openbakery.codesign.ProvisioningProfileReader +import org.gradle.api.DefaultTask -class PackageTask extends AbstractDistributeTask { +class PackageTask extends DefaultTask { - public static final String PACKAGE_PATH = "package" - File outputPath - - - private List appBundles - - String applicationBundleName - StyledTextOutput output - - - CodesignParameters codesignParameters = new CodesignParameters() + public static final String NAME = "package" PackageTask() { - super(); - setDescription("Signs the app bundle that was created by the build and creates the ipa"); - dependsOn( - XcodePlugin.KEYCHAIN_CREATE_TASK_NAME, - XcodePlugin.PROVISIONING_INSTALL_TASK_NAME, - ) - finalizedBy( - XcodePlugin.KEYCHAIN_REMOVE_SEARCH_LIST_TASK_NAME - ) - - output = services.get(StyledTextOutputFactory).create(PackageTask) - - } - - - @TaskAction - void packageApplication() throws IOException { - if (project.xcodebuild.isSimulatorBuildOf(Type.iOS)) { - logger.lifecycle("not a device build, so no codesign and packaging needed") - return - } - outputPath = new File(project.getBuildDir(), PACKAGE_PATH) - - File applicationFolder = createApplicationFolder() - - def applicationName = getApplicationNameFromArchive() - copy(getApplicationBundleDirectory(), applicationFolder) - - applicationBundleName = applicationName + ".app" - - File applicationPath = new File(applicationFolder, applicationBundleName) - - // copy onDemandResources - File onDemandResources = new File(getProductsDirectory(), "OnDemandResources") - if (onDemandResources.exists()) { - copy(onDemandResources, applicationPath) - } - - File bcSymbolsMaps = new File(getArchiveDirectory(), "BCSymbolMaps") - if (bcSymbolsMaps.exists()) { - copy(bcSymbolsMaps, applicationFolder.parentFile) - } - - enumerateExtensionSupportDirectories(getArchiveDirectory()) { File supportDirectory -> - copy(supportDirectory, applicationFolder.parentFile) - } - - ApplicationBundle applicationBundle = new ApplicationBundle(applicationPath , project.xcodebuild.type, project.xcodebuild.simulator) - appBundles = applicationBundle.getBundles() - - File resourceRules = new File(applicationFolder, applicationBundleName + "/ResourceRules.plist") - if (resourceRules.exists()) { - resourceRules.delete() - } - - - File infoPlist = getInfoPlistFile() - - try { - plistHelper.deleteValueFromPlist(infoPlist, "CFBundleResourceSpecification") - } catch (CommandRunnerException ex) { - // ignore, this means that the CFBundleResourceSpecification was not in the infoPlist - } - - def signSettingsAvailable = true - if (project.xcodebuild.signing.mobileProvisionFile == null) { - logger.warn('No mobile provision file provided.') - signSettingsAvailable = false; - } else if (!project.xcodebuild.signing.keychainPathInternal.exists()) { - logger.warn('No certificate or keychain found.') - signSettingsAvailable = false; - } - - codesignParameters.mergeMissing(project.xcodebuild.signing.codesignParameters) - codesignParameters.type = project.xcodebuild.type - codesignParameters.keychain = project.xcodebuild.signing.keychainPathInternal - Codesign codesign = new Codesign(xcode, codesignParameters, commandRunner, plistHelper) - - for (File bundle : appBundles) { - - if (project.xcodebuild.isDeviceBuildOf(Type.iOS)) { - removeFrameworkFromExtensions(bundle) - removeUnneededDylibsFromBundle(bundle) - embedProvisioningProfileToBundle(bundle) - } - - if (signSettingsAvailable) { - logger.info("Codesign app: {}", bundle) - codesign.sign(bundle) - } else { - String message = "Bundle not signed: " + bundle - output.withStyle(StyledTextOutput.Style.Failure).println(message) - } - } - - File appBundle = appBundles.last() - if (project.xcodebuild.isDeviceBuildOf(Type.iOS)) { - - boolean isAdHoc = isAdHoc(appBundle) - createIpa(applicationFolder, !isAdHoc) - } else { - createPackage(appBundle) - } - - } - - - - - boolean isAdHoc(File appBundle) { - File provisionFile = getProvisionFileForBundle(appBundle) - if (provisionFile == null) { - return false - } - ProvisioningProfileReader reader = new ProvisioningProfileReader(provisionFile, this.commandRunner, this.plistHelper) - return reader.isAdHoc() - } - - def removeFrameworkFromExtensions(File bundle) { - // appex extensions should not contain extensions - if (FilenameUtils.getExtension(bundle.toString()).equalsIgnoreCase("appex")) { - File frameworksPath = new File(bundle, "Frameworks") - if (frameworksPath.exists()) { - FileUtils.deleteDirectory(frameworksPath) - } - } - - } - - def removeUnneededDylibsFromBundle(File bundle) { - File libswiftRemoteMirror = new File(bundle, "libswiftRemoteMirror.dylib") - if (libswiftRemoteMirror.exists()) { - libswiftRemoteMirror.delete() - } - } - - File getProvisionFileForBundle(File bundle) { - String bundleIdentifier = getIdentifierForBundle(bundle) - return ProvisioningProfileReader.getProvisionFileForIdentifier(bundleIdentifier, project.xcodebuild.signing.mobileProvisionFile, this.commandRunner, this.plistHelper) - } - - - def addSwiftSupport(File payloadPath, String applicationBundleName) { - File frameworksPath = new File(payloadPath, applicationBundleName + "/Frameworks") - if (!frameworksPath.exists()) { - return null - } - - File swiftLibArchive = new File(getArchiveDirectory(), "SwiftSupport") - - if (swiftLibArchive.exists()) { - copy(swiftLibArchive, payloadPath.getParentFile()) - return new File(payloadPath.getParentFile(), "SwiftSupport") - } - return null - } - - - private void createZipPackage(File packagePath, String extension, boolean includeSwiftSupport) { - File packageBundle = new File(outputPath, getIpaFileName() + "." + extension) - if (!packageBundle.parentFile.exists()) { - packageBundle.parentFile.mkdirs() - } - - List filesToZip = [] - filesToZip << packagePath - - if (includeSwiftSupport) { - File swiftSupportPath = addSwiftSupport(packagePath, applicationBundleName) - if (swiftSupportPath != null) { - filesToZip << swiftSupportPath - } - } - - File bcSymbolMapsPath = new File(packagePath.getParentFile(), "BCSymbolMaps") - if (bcSymbolMapsPath.exists()) { - filesToZip << bcSymbolMapsPath - } - - enumerateExtensionSupportDirectories(packagePath.getParentFile()) { File supportDirectory -> - filesToZip << supportDirectory - } - - createZip(packageBundle, packagePath.getParentFile(), packagePath, *filesToZip) - } - - private void enumerateExtensionSupportDirectories(File parentDirectory, Closure closure) { - def directoryNames = ["MessagesApplicationExtensionSupport"] - - for (String name in directoryNames) { - File supportDirectory = new File(parentDirectory, name) - if (supportDirectory.exists()) { - closure(supportDirectory) - } - } - } - - private void createIpa(File payloadPath, boolean addSwiftSupport) { - createZipPackage(payloadPath, "ipa", addSwiftSupport) - } - - private void createPackage(File packagePath) { - - createZipPackage(packagePath, "zip", false) - } - - - - private String getIdentifierForBundle(File bundle) { - File infoPlist - - if (project.xcodebuild.isDeviceBuildOf(Type.iOS)) { - infoPlist = new File(bundle, "Info.plist"); - } else { - infoPlist = new File(bundle, "Contents/Info.plist") - } - - String bundleIdentifier = plistHelper.getValueFromPlist(infoPlist, "CFBundleIdentifier") - return bundleIdentifier - } - - - private void embedProvisioningProfileToBundle(File bundle) { - File mobileProvisionFile = getProvisionFileForBundle(bundle) - if (mobileProvisionFile != null) { - File embeddedProvisionFile - - String profileExtension = FilenameUtils.getExtension(mobileProvisionFile.absolutePath) - embeddedProvisionFile = new File(getAppContentPath(bundle) + "embedded." + profileExtension) - - logger.info("provision profile - {}", embeddedProvisionFile) - - FileUtils.copyFile(mobileProvisionFile, embeddedProvisionFile) - } - } - - private File createSigningDestination(String name) throws IOException { - File destination = new File(outputPath, name); - if (destination.exists()) { - FileUtils.deleteDirectory(destination); - } - destination.mkdirs(); - return destination; - } - - private File createApplicationFolder() throws IOException { - - if (project.xcodebuild.isDeviceBuildOf(Type.iOS)) { - return createSigningDestination("Payload") - } else { - // same folder as signing - if (!outputPath.exists()) { - outputPath.mkdirs() - } - return outputPath - } - } - - private File getInfoPlistFile() { - return new File(getAppContentPath() + "Info.plist") - } - - private String getAppContentPath() { - - return getAppContentPath(appBundles.last()) - } - - private String getAppContentPath(File bundle) { - if (project.xcodebuild.type == Type.iOS) { - return bundle.absolutePath + "/" - } - return bundle.absolutePath + "/Contents/" - } - - def getIpaFileName() { - if (project.xcodebuild.ipaFileName) { - return project.xcodebuild.ipaFileName - } else { - return getApplicationNameFromArchive() - } - } - - - String getSigningIdentity() { - return codesignParameters.signingIdentity - } + super() - void setSigningIdentity(String identity) { - codesignParameters.signingIdentity = identity + dependsOn(PackageLegacyTask.NAME) + dependsOn(PackageTaskIosAndTvOS.NAME) } } diff --git a/plugin/src/main/groovy/org/openbakery/packaging/PackageTaskIosAndTvOS.groovy b/plugin/src/main/groovy/org/openbakery/packaging/PackageTaskIosAndTvOS.groovy new file mode 100644 index 00000000..5f13fdc6 --- /dev/null +++ b/plugin/src/main/groovy/org/openbakery/packaging/PackageTaskIosAndTvOS.groovy @@ -0,0 +1,167 @@ +package org.openbakery.packaging + +import groovy.transform.CompileStatic +import groovy.transform.TypeCheckingMode +import org.gradle.api.DefaultTask +import org.gradle.api.Task +import org.gradle.api.provider.ListProperty +import org.gradle.api.provider.Property +import org.gradle.api.provider.Provider +import org.gradle.api.tasks.* +import org.openbakery.CommandRunner +import org.openbakery.XcodePlugin +import org.openbakery.signing.KeychainCreateTask +import org.openbakery.signing.ProvisioningFile +import org.openbakery.signing.ProvisioningInstallTask +import org.openbakery.signing.SigningMethod +import org.openbakery.util.PathHelper +import org.openbakery.util.PlistHelper +import org.openbakery.xcode.Type +import org.openbakery.xcode.Xcodebuild + +@CompileStatic +class PackageTaskIosAndTvOS extends DefaultTask { + + @Input + public final Property signingMethod = project.objects.property(SigningMethod) + + @Input + final Provider scheme = project.objects.property(String) + + @Input + final Provider buildType = project.objects.property(Type) + + @Input + final ListProperty registeredProvisioningFiles = project.objects.listProperty(ProvisioningFile) + + @Input + final Property certificateFriendlyName = project.objects.property(String) + + @Input + final Provider bitCode = project.objects.property(Boolean) + + final Provider commandRunner = project.objects.property(CommandRunner) + final Provider plistHelper = project.objects.property(PlistHelper) + + public static final String DESCRIPTION = "Package the archive with Xcode-build" + public static final String NAME = "packageWithXcodeBuild" + + private static final String PLIST_KEY_METHOD = "method" + private static final String PLIST_KEY_SIGNING_STYLE = "signingStyle" + private static final String PLIST_KEY_COMPILE_BITCODE = "compileBitcode" + private static final String PLIST_KEY_PROVISIONING_PROFILE = "provisioningProfiles" + private static final String PLIST_KEY_SIGNING_CERTIFICATE = "signingCertificate" + private static final String PLIST_VALUE_SIGNING_METHOD_MANUAL = "manual" + private static final String FILE_EXPORT_OPTIONS_PLIST = "exportOptions.plist" + + PackageTaskIosAndTvOS() { + super() + + description = DESCRIPTION + + dependsOn(KeychainCreateTask.TASK_NAME) + dependsOn(ProvisioningInstallTask.TASK_NAME) + dependsOn(XcodePlugin.XCODE_CONFIG_TASK_NAME) + + onlyIf({ Task task -> + return buildType.get() == Type.iOS || + buildType.get() == Type.tvOS + }) + } + + @Input + @CompileStatic(TypeCheckingMode.SKIP) + Map getProvisioningMap() { + return registeredProvisioningFiles.get() + .collectEntries { [it.getApplicationIdentifier(), it.getName()] } + } + + @InputDirectory + File getArchiveFile() { + return PathHelper.resolveArchiveFile(project, scheme.get()) + } + + @OutputDirectory + File getOutputDirectory() { + File file = PathHelper.resolvePackageFolder(project) + file.mkdirs() + return file + } + + @OutputFile + File getExportOptionsPlistFile() { + return new File(project.buildDir, FILE_EXPORT_OPTIONS_PLIST) + } + + @TaskAction + private void packageArchive() { + logger.info("Packaging the archive") + + assert signingMethod.present: "Cannot package, the signing method is not defined" + assert getArchiveFile().exists() && getArchiveFile().isDirectory() + + generateExportOptionPlist() + packageIt() + } + + private boolean validateBitCodeSettings() { + Boolean bitCodeValue = bitCode.get() + SigningMethod method = signingMethod.get() + + if (method == SigningMethod.AppStore) { + if (buildType.get() == Type.tvOS) { + assert bitCodeValue: "Invalid configuration for the TvOS target " + + "`AppStore` upload requires BitCode enabled." + } + } else { + assert !bitCodeValue: "The BitCode setting (`xcodebuild.bitCode`) should be " + + "enabled only for the `AppStore` signing method" + } + + return bitCodeValue + } + + private void generateExportOptionPlist() { + File file = getExportOptionsPlistFile() + + plistHelper.get().create(file) + + // Signing method + addStringValueForPlist(PLIST_KEY_METHOD, + signingMethod.get().value) + + // Provisioning profiles map list + plistHelper.get().addDictForPlist(file, + PLIST_KEY_PROVISIONING_PROFILE, + getProvisioningMap()) + + // Certificate name + addStringValueForPlist(PLIST_KEY_SIGNING_CERTIFICATE, + certificateFriendlyName.get()) + + // BitCode + plistHelper.get().addValueForPlist(file, + PLIST_KEY_COMPILE_BITCODE, + validateBitCodeSettings()) + + // SigningMethod + addStringValueForPlist(PLIST_KEY_SIGNING_STYLE, + PLIST_VALUE_SIGNING_METHOD_MANUAL) + } + + private void addStringValueForPlist(String key, + String value) { + assert key != null + assert value != null + + plistHelper.get() + .addValueForPlist(getExportOptionsPlistFile(), key, value) + } + + private void packageIt() { + Xcodebuild.packageIpa(commandRunner.get(), + getArchiveFile(), + getOutputDirectory(), + getExportOptionsPlistFile()) + } +} diff --git a/plugin/src/main/groovy/org/openbakery/signing/AbstractKeychainTask.groovy b/plugin/src/main/groovy/org/openbakery/signing/AbstractKeychainTask.groovy deleted file mode 100644 index 506483bf..00000000 --- a/plugin/src/main/groovy/org/openbakery/signing/AbstractKeychainTask.groovy +++ /dev/null @@ -1,52 +0,0 @@ -package org.openbakery.signing - -import org.apache.commons.lang.StringUtils -import org.openbakery.AbstractXcodeTask -import org.openbakery.XcodeBuildPluginExtension -import org.openbakery.codesign.Security - -/** - * Created with IntelliJ IDEA. - * User: rene - * Date: 23.08.13 - * Time: 11:39 - * To change this template use File | Settings | File Templates. - */ -abstract class AbstractKeychainTask extends AbstractXcodeTask { - - Security security - - AbstractKeychainTask() { - security = new Security(commandRunner) - } - - - List getKeychainList() { - return security.getKeychainList() - } - - def setKeychainList(List keychainList) { - security.setKeychainList(keychainList) - } - - /** - * remove all gradle keychains from the keychain search list - * @return - */ - def removeGradleKeychainsFromSearchList() { - ListkeychainList = getKeychainList() - logger.debug("project.xcodebuild.signing.keychain should not be removed: {}", project.xcodebuild.signing.keychainPathInternal) - if (project.xcodebuild.signing.keychainPathInternal != null) { - keychainList.remove(project.xcodebuild.signing.keychainPathInternal) - } - setKeychainList(keychainList) - } - - def cleanupKeychain() { - project.xcodebuild.signing.signingDestinationRoot.deleteDir() - removeGradleKeychainsFromSearchList() - } - - - -} diff --git a/plugin/src/main/groovy/org/openbakery/signing/KeychainCleanupTask.groovy b/plugin/src/main/groovy/org/openbakery/signing/KeychainCleanupTask.groovy deleted file mode 100644 index 3077dcbd..00000000 --- a/plugin/src/main/groovy/org/openbakery/signing/KeychainCleanupTask.groovy +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Copyright 2013 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.openbakery.signing - -import org.gradle.api.tasks.TaskAction -import org.openbakery.XcodeBuildPluginExtension - -class KeychainCleanupTask extends AbstractKeychainTask { - - KeychainCleanupTask() { - super() - this.description = "Cleanup the keychain" - } - - - - @TaskAction - def clean() { - if (project.xcodebuild.signing.keychain) { - logger.debug("Nothing to cleanup") - return - } - cleanupKeychain() - } - -} \ No newline at end of file diff --git a/plugin/src/main/groovy/org/openbakery/signing/KeychainCreateTask.groovy b/plugin/src/main/groovy/org/openbakery/signing/KeychainCreateTask.groovy index 01ae15a6..51d46b5b 100644 --- a/plugin/src/main/groovy/org/openbakery/signing/KeychainCreateTask.groovy +++ b/plugin/src/main/groovy/org/openbakery/signing/KeychainCreateTask.groovy @@ -1,84 +1,232 @@ -/* - * Copyright 2013 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ package org.openbakery.signing -import org.gradle.api.tasks.TaskAction -import org.gradle.api.InvalidUserDataException -import org.openbakery.xcode.Type -import org.openbakery.XcodePlugin +import de.undercouch.gradle.tasks.download.Download +import groovy.transform.CompileStatic +import org.apache.commons.io.FileUtils +import org.gradle.api.file.DirectoryProperty +import org.gradle.api.file.RegularFileProperty +import org.gradle.api.provider.Property +import org.gradle.api.tasks.* +import org.openbakery.CommandRunner +import org.openbakery.CommandRunnerException +import org.openbakery.codesign.Security +import org.openbakery.util.FileUtil +import org.openbakery.util.SystemUtil +import org.openbakery.xcode.Version -class KeychainCreateTask extends AbstractKeychainTask { +import java.util.regex.Matcher +import java.util.regex.Pattern +@CompileStatic +class KeychainCreateTask extends Download { + + @InputFile + @Optional + final RegularFileProperty certificateFile = newInputFile() + + @Input + @Optional + final Property certificateUri = project.objects.property(String) + + @Input + final Property certificatePassword = project.objects.property(String) + + @Input + final Property keychainTimeout = project.objects.property(Integer) + + final Property certificateFriendlyName = project.objects.property(String) + + final Property commandRunnerProperty = project.objects.property(CommandRunner) + + @OutputFile + final RegularFileProperty keyChainFile = newOutputFile() + + final Property security = project.objects.property(Security) + final DirectoryProperty outputDirectory = newOutputDirectory() + + private static final Pattern PATTERN = ~/^\s{4}friendlyName:\s(?[^\n]+)/ + + static final String TASK_NAME = "keychainCreate" + static final String TASK_DESCRIPTION = "Create a keychain that is used for signing the app" + static final String KEYCHAIN_DEFAULT_PASSWORD = "This_is_the_default_keychain_password" + + private File temporaryCertificateFile KeychainCreateTask() { super() - this.description = "Create a keychain that is used for signing the app" + this.description = TASK_DESCRIPTION + onlyIf { + if (!certificateFile.present && !certificateUri.present) { + logger.warn("No signing certificate defined, will skip the keychain creation") + } + + if (!certificatePassword.present) { + logger.warn("No signing certificate password defined, will skip the keychain creation") + } + + if (keyChainFile.present && keyChainFile.asFile.get().exists()) { + logger.debug("Using keychain : " + keyChainFile.get()) + } + + return ((certificateFile.present || certificateUri.present) && + certificatePassword.present) || + (keyChainFile.present && keyChainFile.asFile.get().exists()) + } } @TaskAction - def create() { + void download() { + if (certificateUri.present) { + outputDirectory.get().asFile.mkdirs() + configureDownload() + super.download() + resolveCertificateFile() + } else { + File file = certificateFile.asFile.get() + temporaryCertificateFile = new File(outputDirectory.asFile.get(), file.name) + FileUtils.copyFile(file, temporaryCertificateFile) + } + + parseCertificateFile() + createTemporaryCertificateFile() + createKeyChainAndImportCertificate() + addKeyChainToThePartitionList() + setupOptionalTimeout() + project.gradle.buildFinished { + removeGradleKeychainsFromSearchList() + deleteTemporaryKeyChainFile() + } + } + private void configureDownload() { + this.src(certificateUri.get()) + this.dest(outputDirectory.get().asFile) + this.acceptAnyCertificate(true) + } - if (project.xcodebuild.signing.keychain) { - if (!project.xcodebuild.signing.keychain.exists()) { - throw new IllegalStateException("Keychain not found: " + project.xcodebuild.signing.keychain.absolutePath) + private void resolveCertificateFile() { + temporaryCertificateFile = getOutputFiles().first() + } + + private void parseCertificateFile() { + certificateFriendlyName.set(getSignatureFriendlyName()) + + // Delete on exit the downloaded files + project.gradle.buildFinished { + if (temporaryCertificateFile.exists()) { + temporaryCertificateFile.delete() } - logger.debug("Using keychain {}", project.xcodebuild.signing.keychain) - logger.debug("Internal keychain {}", project.xcodebuild.signing.keychainPathInternal) - return } + } - if (project.xcodebuild.signing.certificateURI == null) { - logger.debug("not certificateURI specifed so do not create the keychain"); - return + private void createTemporaryCertificateFile() { + temporaryCertificateFile = FileUtil.download(project, + outputDirectory.asFile.get(), + certificateUri.present + ? certificateUri.get() + : certificateFile.asFile.get().toURI().toString()) + + // Delete the temporary file on completion + project.gradle.buildFinished { + if (temporaryCertificateFile.exists()) { + temporaryCertificateFile.delete() + } } + } + + private void createKeyChainAndImportCertificate() { + security.get() + .createKeychain(keyChainFile.asFile.getOrNull(), + KEYCHAIN_DEFAULT_PASSWORD) + security.get() + .importCertificate(temporaryCertificateFile, + certificatePassword.get(), + keyChainFile.asFile.get()) + } - if (project.xcodebuild.signing.certificatePassword == null) { - throw new InvalidUserDataException("Property project.xcodebuild.signing.certificatePassword is missing") + private void addKeyChainToThePartitionList() { + Version systemVersion = SystemUtil.getOsVersion() + if (systemVersion.minor >= 9) { + List keychainList = getKeychainList() + keychainList.add(keyChainFile.asFile.get()) + populateKeyChain(keychainList) } - // first cleanup old keychain - cleanupKeychain() + if (systemVersion.minor >= 12) { + security.get().setPartitionList(keyChainFile.asFile.get(), + KEYCHAIN_DEFAULT_PASSWORD) + } + } - def certificateFile = download(project.xcodebuild.signing.signingDestinationRoot, project.xcodebuild.signing.certificateURI) + private void setupOptionalTimeout() { + if (keychainTimeout.present) { + security.get() + .setTimeout(keychainTimeout.get(), + keyChainFile.asFile.get()) + } + } - File keychain = project.xcodebuild.signing.keychainPathInternal + List getKeychainList() { + return security.get().getKeychainList() + } - security.createKeychain(keychain, project.xcodebuild.signing.keychainPassword) - security.importCertificate(new File(certificateFile), project.xcodebuild.signing.certificatePassword, keychain) + void populateKeyChain(List keychainList) { + security.get() + .setKeychainList(keychainList) + } + private void deleteTemporaryKeyChainFile() { + if (keyChainFile.asFile.get()) { + keyChainFile.asFile.get().delete() - if (getOSVersion().minor >= 9) { - List keychainList = getKeychainList() - keychainList.add(keychain) - setKeychainList(keychainList) + logger.info("The temporary keychain file has been deleted") } + } - if (getOSVersion().minor >= 12) { - security.setPartitionList(keychain, project.xcodebuild.signing.keychainPassword) - } + private void removeGradleKeychainsFromSearchList() { + if (keyChainFile.present) { + List list = getKeychainList() + list.removeIf { File file -> + return file.absolutePath == keyChainFile.get().asFile.absolutePath + } + populateKeyChain(list) - // Set a custom timeout on the keychain if requested - if (project.xcodebuild.signing.timeout != null) { - security.setTimeout(project.xcodebuild.signing.timeout, keychain) + logger.info("The temporary keychain has been removed from the search list") } } + String getSignatureFriendlyName() { + return java.util.Optional.ofNullable(getKeyContent(temporaryCertificateFile) + .split(System.getProperty("line.separator")) + .find { PATTERN.matcher(it).matches() }) + .map { PATTERN.matcher(it) } + .filter { Matcher it -> it.matches() } + .map { Matcher it -> + return it.group("friendlyName") + } + .orElseThrow { + new IllegalArgumentException("Failed to resolve the code signing identity from the certificate ") + } + } + private String getKeyContent(File file) { + String result + try { + result = commandRunnerProperty.get() + .runWithResult(["openssl", + "pkcs12", + "-nokeys", + "-in", + file.absolutePath, + "-passin", + "pass:" + certificatePassword.get()]) + } catch (CommandRunnerException exception) { + logger.warn(exception.toString()) + result = null + } -} \ No newline at end of file + return result + } +} diff --git a/plugin/src/main/groovy/org/openbakery/signing/KeychainRemoveFromSearchListTask.groovy b/plugin/src/main/groovy/org/openbakery/signing/KeychainRemoveFromSearchListTask.groovy deleted file mode 100644 index 02985e66..00000000 --- a/plugin/src/main/groovy/org/openbakery/signing/KeychainRemoveFromSearchListTask.groovy +++ /dev/null @@ -1,20 +0,0 @@ -package org.openbakery.signing - -import org.gradle.api.tasks.TaskAction -import org.openbakery.XcodePlugin - -class KeychainRemoveFromSearchListTask extends AbstractKeychainTask { - - KeychainRemoveFromSearchListTask() { - super() - mustRunAfter(XcodePlugin.CRASHLYTICS_TASK_NAME) - this.description = "Removes the gradle keychain from the search list" - } - - - - @TaskAction - def remove() { - removeGradleKeychainsFromSearchList() - } -} diff --git a/plugin/src/main/groovy/org/openbakery/signing/ProvisioningCleanupTask.groovy b/plugin/src/main/groovy/org/openbakery/signing/ProvisioningCleanupTask.groovy deleted file mode 100644 index e7023cc8..00000000 --- a/plugin/src/main/groovy/org/openbakery/signing/ProvisioningCleanupTask.groovy +++ /dev/null @@ -1,54 +0,0 @@ -/* - * Copyright 2013 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.openbakery.signing - -import org.gradle.api.tasks.TaskAction -import org.openbakery.AbstractXcodeTask - - -class ProvisioningCleanupTask extends AbstractXcodeTask { - - ProvisioningCleanupTask() { - } - - @TaskAction - def clean() { - if (project.xcodebuild.signing.mobileProvisionDestinationRoot.exists()) { - logger.debug("deleting {}", project.xcodebuild.signing.mobileProvisionDestinationRoot) - project.xcodebuild.signing.mobileProvisionDestinationRoot.deleteDir() - - if (project.xcodebuild.signing.mobileProvisionDestinationRoot.exists()) { - logger.error("error deleting provisioning: {}", project.xcodebuild.signing.mobileProvisionDestinationRoot) - } - } else { - logger.debug("Provisioning destination cleanup skipped because the destination directory does not exit") - } - - File mobileprovisionPath = new File(System.getProperty("user.home") + "/Library/MobileDevice/Provisioning Profiles/") - - if (mobileprovisionPath.exists()) { - // find all the broken profile links that where created by this plugin - String profileLinksToDelete = commandRunner.runWithResult(["find", "-L", mobileprovisionPath.absolutePath, "-name", ProvisioningInstallTask.PROVISIONING_NAME_BASE +"*", "-type", "l"]); - String[] profiles = profileLinksToDelete.split("\n") - for (String profile : profiles) { - logger.debug("profile to delete {}", profile) - new File(profile).delete(); - } - } else { - logger.debug("nothing to cleanup") - } - } -} \ No newline at end of file diff --git a/plugin/src/main/groovy/org/openbakery/signing/ProvisioningFile.groovy b/plugin/src/main/groovy/org/openbakery/signing/ProvisioningFile.groovy new file mode 100644 index 00000000..ee686b80 --- /dev/null +++ b/plugin/src/main/groovy/org/openbakery/signing/ProvisioningFile.groovy @@ -0,0 +1,61 @@ +package org.openbakery.signing + +import org.apache.commons.io.FilenameUtils + +class ProvisioningFile implements Serializable { + + private File file + private String applicationIdentifier + private String uuid + private String teamIdentifier + private String teamName + private String name + + public static final String PROVISIONING_NAME_BASE = "gradle-" + + ProvisioningFile(File file, + String applicationIdentifier, + String uuid, + String teamIdentifier, + String teamName, + String name) { + this.applicationIdentifier = applicationIdentifier + this.file = file + this.uuid = uuid + this.teamIdentifier = teamIdentifier + this.teamName = teamName + this.name = name + } + + String getApplicationIdentifier() { + return applicationIdentifier + } + + File getFile() { + return file + } + + String getUuid() { + return uuid + } + + String getTeamIdentifier() { + return teamIdentifier + } + + String getTeamName() { + return teamName + } + + String getName() { + return name + } + + String getFormattedName() { + return formattedName(uuid, file) + } + + public static String formattedName(String uuid, File file) { + return PROVISIONING_NAME_BASE + uuid + "." + FilenameUtils.getExtension(file.getName()) + } +} diff --git a/plugin/src/main/groovy/org/openbakery/signing/ProvisioningInstallTask.groovy b/plugin/src/main/groovy/org/openbakery/signing/ProvisioningInstallTask.groovy index 4d1d3bf5..92d913fb 100644 --- a/plugin/src/main/groovy/org/openbakery/signing/ProvisioningInstallTask.groovy +++ b/plugin/src/main/groovy/org/openbakery/signing/ProvisioningInstallTask.groovy @@ -1,88 +1,129 @@ -/* - * Copyright 2013 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ package org.openbakery.signing +import de.undercouch.gradle.tasks.download.Download +import groovy.transform.CompileStatic +import org.apache.commons.io.FileUtils import org.apache.commons.io.FilenameUtils +import org.gradle.api.file.Directory +import org.gradle.api.provider.ListProperty +import org.gradle.api.provider.Property +import org.gradle.api.provider.Provider +import org.gradle.api.tasks.Input +import org.gradle.api.tasks.OutputDirectory +import org.gradle.api.tasks.OutputFiles import org.gradle.api.tasks.TaskAction -import org.openbakery.AbstractXcodeTask +import org.openbakery.CommandRunner import org.openbakery.codesign.ProvisioningProfileReader -import org.openbakery.xcode.Type -import org.openbakery.XcodePlugin +import org.openbakery.util.PlistHelper -class ProvisioningInstallTask extends AbstractXcodeTask { +@CompileStatic +class ProvisioningInstallTask extends Download { - public final static PROVISIONING_NAME_BASE = "gradle-" + @Input + final Provider> mobileProvisioningList = project.objects.listProperty(String) + @OutputFiles + final ListProperty registeredProvisioning = project.objects.listProperty(File) + @OutputDirectory + final Provider outputDirectory = project.layout.directoryProperty() - ProvisioningInstallTask() { - super() - dependsOn(XcodePlugin.PROVISIONING_CLEAN_TASK_NAME) - this.description = "Installs the given provisioning profile" - } + final ListProperty registeredProvisioningFiles = project.objects.listProperty(ProvisioningFile) + final Property commandRunnerProperty = project.objects.property(CommandRunner) + final Property plistHelperProperty = project.objects.property(PlistHelper) - void linkToLibraray(File mobileProvisionFile) { - File provisionPath = new File(System.getProperty("user.home") + "/Library/MobileDevice/Provisioning Profiles/"); - if (!provisionPath.exists()) { - provisionPath.mkdirs() - } - - File mobileProvisionFileLinkToLibrary = new File(System.getProperty("user.home") + "/Library/MobileDevice/Provisioning Profiles/" + mobileProvisionFile.getName()); - if (mobileProvisionFileLinkToLibrary.exists()) { - mobileProvisionFileLinkToLibrary.delete() - } + public static final String TASK_NAME = "provisioningInstall" + public static final String TASK_DESCRIPTION = "Installs the given provisioning profile" + static final File PROVISIONING_DIR = new File(System.getProperty("user.home") + + "/Library/MobileDevice/Provisioning Profiles/") - commandRunner.run(["/bin/ln", "-s", mobileProvisionFile.absolutePath, mobileProvisionFileLinkToLibrary.absolutePath]) + ProvisioningInstallTask() { + super() + this.description = TASK_DESCRIPTION + this.onlyIf { !mobileProvisioningList.get().empty } } @TaskAction - def install() { - + @Override + void download() throws IOException { + outputDirectory.get().asFile.mkdirs() + configureDownload() + super.download() + postDownload() + } - if (project.xcodebuild.signing.mobileProvisionURI == null) { - logger.lifecycle("No provisioning profile specifed so do nothing here") - return - } + private void configureDownload() { + this.src(mobileProvisioningList.get().asList()) + this.dest(outputDirectory.get().asFile) + this.acceptAnyCertificate(true) + } - for (String mobileProvisionURI : project.xcodebuild.signing.mobileProvisionURI) { - def mobileProvisionFile = download(project.xcodebuild.signing.mobileProvisionDestinationRoot, mobileProvisionURI) + void registerProvisioning(ProvisioningFile provisioningFile) { + registeredProvisioning.add(provisioningFile.getFile()) + registeredProvisioningFiles.add(provisioningFile) + } + File registerProvisioningInToUserLibrary(ProvisioningFile provisioningFile) { + PROVISIONING_DIR.mkdirs() + File destinationFile = new File(PROVISIONING_DIR, provisioningFile.getFormattedName()) + if (destinationFile.exists()) { + destinationFile.delete() + } - ProvisioningProfileReader provisioningProfileIdReader = new ProvisioningProfileReader(new File(mobileProvisionFile), this.commandRunner, this.plistHelper) + FileUtils.copyFile(provisioningFile.getFile(), destinationFile) + return destinationFile + } - String uuid = provisioningProfileIdReader.getUUID() + private void postDownload() { + // For convenience we rename the mobile provisioning file in to a formatted name + List files = rename() + deleteFilesOnExit(files.collect { it.file }) + // Register it + files.each(this.®isterProvisioning) - String extension = FilenameUtils.getExtension(mobileProvisionFile) - String mobileProvisionName - mobileProvisionName = PROVISIONING_NAME_BASE + uuid + "." + extension + // Register into the user library + List registeredFiles = files.collect(this.®isterProvisioningInToUserLibrary) + deleteFilesOnExit(registeredFiles) + } + File fileFromPath(String path) { + return new File(outputDirectory.get().asFile, FilenameUtils.getName(path)) + } - File downloadedFile = new File(mobileProvisionFile) - File renamedProvisionFile = new File(downloadedFile.getParentFile(), mobileProvisionName) - downloadedFile.renameTo(renamedProvisionFile) + ProvisioningFile toProvisioningFile(File file) { + ProvisioningProfileReader reader = new ProvisioningProfileReader(file, + commandRunnerProperty.get(), + plistHelperProperty.get()) + + File renamedFile = new File(file.parentFile, + ProvisioningFile.formattedName(reader.getUUID(), file)) + file.renameTo(renamedFile) + + return new ProvisioningFile(renamedFile, + reader.getApplicationIdentifier(), + reader.getUUID(), + reader.getTeamIdentifierPrefix(), + reader.getTeamName(), + reader.getName()) + } - project.xcodebuild.signing.addMobileProvisionFile(renamedProvisionFile) + private List rename() { + return mobileProvisioningList.get() + .collect(this.&fileFromPath) + .collect(this.&toProvisioningFile) + } - linkToLibraray(renamedProvisionFile) + private void deleteFilesOnExit(final List files) { + project.gradle.buildFinished { + files.each { + logger.debug("Delete file : " + it.absolutePath) + it.delete() + } } - } -} \ No newline at end of file +} diff --git a/plugin/src/main/groovy/org/openbakery/signing/Signing.groovy b/plugin/src/main/groovy/org/openbakery/signing/Signing.groovy deleted file mode 100644 index 3bc87880..00000000 --- a/plugin/src/main/groovy/org/openbakery/signing/Signing.groovy +++ /dev/null @@ -1,172 +0,0 @@ -package org.openbakery.signing - -import org.gradle.api.Project -import org.openbakery.CommandRunner -import org.openbakery.codesign.CodesignParameters - -/** - * - * @author René Pirringer - * - */ -class Signing { - - public final static KEYCHAIN_NAME_BASE = "gradle-" - - - String identity - String certificateURI - String certificatePassword - List mobileProvisionURI = null - String keychainPassword = "This_is_the_default_keychain_password" - File keychain - Integer timeout = 3600 - String plugin - Object entitlementsFile - - Map entitlements - - /** - * internal parameters - */ - Object signingDestinationRoot - Object keychainPathInternal - final Project project - final String keychainName = KEYCHAIN_NAME_BASE + System.currentTimeMillis() + ".keychain" - CommandRunner commandRunner - - - Object mobileProvisionDestinationRoot - List mobileProvisionFile = new ArrayList() - - - - - public Signing(Project project) { - this.project = project; - this.commandRunner = new CommandRunner() - - this.signingDestinationRoot = { - return project.getFileResolver().withBaseDir(project.getBuildDir()).resolve("codesign") - } - - this.keychainPathInternal = { - if (this.keychain != null) { - return this.keychain - } - return new File(this.signingDestinationRoot, keychainName) - } - - this.mobileProvisionDestinationRoot = { - return project.getFileResolver().withBaseDir(project.getBuildDir()).resolve("provision") - } - - } - - void setKeychain(Object keychain) { - if (keychain instanceof String && keychain.matches("^~/.*")) { - keychain = keychain.replaceFirst("~", System.getProperty('user.home')) - } - this.keychain = project.file(keychain) - } - - - File getSigningDestinationRoot() { - return project.file(signingDestinationRoot) - } - - void setSigningDestinationRoot(Object keychainDestinationRoot) { - this.signingDestinationRoot = keychainDestinationRoot - } - - File getKeychainPathInternal() { - return project.file(keychainPathInternal) - } - - File getMobileProvisionDestinationRoot() { - return project.file(mobileProvisionDestinationRoot) - } - - void setMobileProvisionDestinationRoot(Object mobileProvisionDestinationRoot) { - this.mobileProvisionDestinationRoot = mobileProvisionDestinationRoot - } - - void setMobileProvisionURI(Object mobileProvisionURI) { - if (mobileProvisionURI instanceof List) { - this.mobileProvisionURI = mobileProvisionURI; - } else { - this.mobileProvisionURI = new ArrayList(); - this.mobileProvisionURI.add(mobileProvisionURI.toString()); - } - } - - - void addMobileProvisionFile(File mobileProvision) { - if (!mobileProvision.exists()) { - throw new IllegalArgumentException("given mobile provision file does not exist: " + mobileProvision.absolutePath) - } - mobileProvisionFile.add(mobileProvision) - } - - - File getEntitlementsFile() { - if (entitlementsFile != null) { - if (entitlementsFile instanceof File) { - return entitlementsFile - } - return project.file(entitlementsFile) - - } - return null - } - - boolean hasEntitlementsFile() { - return entitlementsFile != null && entitlementsFile.exists() - } - - void setEntitlementsFile(Object entitlementsFile) { - this.entitlementsFile = entitlementsFile - } - - String getIdentity() { - return this.identity - } - - - - public void entitlements(Map entitlements) { - this.entitlements = entitlements - - } - - @Override - public String toString() { - if (this.keychain != null) { - return "Signing{" + - " identity='" + identity + '\'' + - ", mobileProvisionURI='" + mobileProvisionURI + '\'' + - ", keychain='" + keychain + '\'' + - '}'; - } - return "Signing{" + - " identity='" + identity + '\'' + - ", certificateURI='" + certificateURI + '\'' + - ", certificatePassword='" + certificatePassword + '\'' + - ", mobileProvisionURI='" + mobileProvisionURI + '\'' + - '}'; - } - - CodesignParameters getCodesignParameters() { - CodesignParameters result = new CodesignParameters() - result.signingIdentity = getIdentity() - result.mobileProvisionFiles = getMobileProvisionFile().clone() - result.keychain = getKeychain() - if (entitlements != null) { - result.entitlements = entitlements.clone() - } - result.entitlementsFile = getEntitlementsFile() - return result - } - - -} diff --git a/plugin/src/main/groovy/org/openbakery/signing/SigningMethod.groovy b/plugin/src/main/groovy/org/openbakery/signing/SigningMethod.groovy new file mode 100644 index 00000000..307710f1 --- /dev/null +++ b/plugin/src/main/groovy/org/openbakery/signing/SigningMethod.groovy @@ -0,0 +1,25 @@ +package org.openbakery.signing + +import groovy.transform.CompileStatic + +@CompileStatic +enum SigningMethod { + AppStore("app-store"), + AdHoc("ad-hoc"), + Enterprise("enterprise"), + Dev("development") + + private final String value + + SigningMethod(String value) { + this.value = value + } + + String getValue() { + return value + } + + static Optional fromString(value) { + return Optional.ofNullable(values().find { it.getValue() == value }) + } +} diff --git a/plugin/src/main/groovy/org/openbakery/simulators/AbstractSimulatorTask.groovy b/plugin/src/main/groovy/org/openbakery/simulators/AbstractSimulatorTask.groovy index 6405f3fc..b16bf6a3 100644 --- a/plugin/src/main/groovy/org/openbakery/simulators/AbstractSimulatorTask.groovy +++ b/plugin/src/main/groovy/org/openbakery/simulators/AbstractSimulatorTask.groovy @@ -11,9 +11,10 @@ class AbstractSimulatorTask extends AbstractXcodeTask { Destination getDestination() { - return getDestinationResolver().getDestinations(project.xcodebuild.getXcodebuildParameters()).first() + return getDestinationResolver() + .getDestinations(project.xcodebuild.getXcodebuildParameters()) + .first() } - } diff --git a/plugin/src/main/groovy/org/openbakery/simulators/SimulatorRunAppTask.groovy b/plugin/src/main/groovy/org/openbakery/simulators/SimulatorRunAppTask.groovy index b9ed6105..56e3ae45 100644 --- a/plugin/src/main/groovy/org/openbakery/simulators/SimulatorRunAppTask.groovy +++ b/plugin/src/main/groovy/org/openbakery/simulators/SimulatorRunAppTask.groovy @@ -22,7 +22,7 @@ class SimulatorRunAppTask extends AbstractSimulatorTask { @TaskAction void run() { - if (!project.xcodebuild.isSimulatorBuildOf(Type.iOS)) { + if (!project.xcodebuild.isSimulatorBuildOf(Type.iOS) && !project.xcodebuild.isSimulatorBuildOf(Type.tvOS)) { throw new IllegalArgumentException("Build is not a simulator build for iOS: Is " + project.xcodebuild.type + " and simulator flag is " + project.xcodebuild.simulator ) } diff --git a/plugin/src/main/groovy/org/openbakery/simulators/SimulatorStartTask.groovy b/plugin/src/main/groovy/org/openbakery/simulators/SimulatorStartTask.groovy index 04965749..b79a4f54 100644 --- a/plugin/src/main/groovy/org/openbakery/simulators/SimulatorStartTask.groovy +++ b/plugin/src/main/groovy/org/openbakery/simulators/SimulatorStartTask.groovy @@ -1,27 +1,21 @@ package org.openbakery.simulators import org.gradle.api.tasks.TaskAction -import org.openbakery.xcode.Destination -import org.openbakery.XcodePlugin class SimulatorStartTask extends AbstractSimulatorTask { - public SimulatorStartTask() { - setDescription("Start iOS Simulators") - } + public SimulatorStartTask() { + setDescription("Start iOS Simulators") + } - - @TaskAction - void run() { - - - Destination destination = getDestination() - - SimulatorDevice device = simulatorControl.getDevice(destination) - - simulatorControl.killAll() - simulatorControl.runDevice(device) - simulatorControl.waitForDevice(device) - } + @TaskAction + void run() { + simulatorControl.getDevice(getDestination()) + .ifPresent { device -> + simulatorControl.killAll() + simulatorControl.runDevice(device) + simulatorControl.waitForDevice(device) + } + } } diff --git a/plugin/src/main/groovy/org/openbakery/util/FileUtil.groovy b/plugin/src/main/groovy/org/openbakery/util/FileUtil.groovy new file mode 100644 index 00000000..dda3590a --- /dev/null +++ b/plugin/src/main/groovy/org/openbakery/util/FileUtil.groovy @@ -0,0 +1,37 @@ +package org.openbakery.util + +import org.apache.commons.io.FilenameUtils +import org.apache.commons.lang.StringUtils +import org.gradle.api.Project + +class FileUtil { + + static File download(Project project, + File toDirectory, + String address) throws IllegalArgumentException { + + if (StringUtils.isEmpty(address)) { + throw new IllegalArgumentException("Cannot download, because no address was given") + } + + if (!toDirectory.exists()) { + toDirectory.mkdirs() + } + + try { + project.ant.get(src: address, + dest: toDirectory.getPath(), + verbose: true) + } catch (Exception ex) { + project.logger.error("cannot download file from the given location: {}", address) + throw ex + } + + File file = new File(toDirectory, FilenameUtils.getName(address)) + return file + } + + static boolean isLocalFile(String uri) { + return new File(new URI(uri)).exists() + } +} diff --git a/plugin/src/main/groovy/org/openbakery/util/SystemUtil.groovy b/plugin/src/main/groovy/org/openbakery/util/SystemUtil.groovy new file mode 100644 index 00000000..7b142d39 --- /dev/null +++ b/plugin/src/main/groovy/org/openbakery/util/SystemUtil.groovy @@ -0,0 +1,43 @@ +package org.openbakery.util + +import org.openbakery.xcode.Version + +class SystemUtil { + static Version getOsVersion() { + Version result = new Version() + String versionString = System.getProperty("os.version") + Scanner scanner = new Scanner(versionString).useDelimiter("\\.") + if (scanner.hasNext()) { + result.major = scanner.nextInt() + } + if (scanner.hasNext()) { + result.minor = scanner.nextInt() + } + if (scanner.hasNext()) { + result.maintenance = scanner.nextInt() + } + return result + } + + public static boolean isValidUri(String value) { + boolean result = true + try { + new File(new URI(value)) + } catch (Exception exception) { + result = false + } + + return result + } + + public static boolean isValidUrl(String value) { + boolean result = true + try { + new File(new URL(value)) + } catch (Exception exception) { + result = false + } + + return result + } +} diff --git a/plugin/src/test/Resource/fake_distribution.p12 b/plugin/src/test/Resource/fake_distribution.p12 new file mode 100644 index 00000000..208b05e0 Binary files /dev/null and b/plugin/src/test/Resource/fake_distribution.p12 differ diff --git a/plugin/src/test/groovy/org/openbakery/AbstractDistributeTaskSpecification.groovy b/plugin/src/test/groovy/org/openbakery/AbstractDistributeTaskSpecification.groovy index ca219b68..5ecbbefc 100644 --- a/plugin/src/test/groovy/org/openbakery/AbstractDistributeTaskSpecification.groovy +++ b/plugin/src/test/groovy/org/openbakery/AbstractDistributeTaskSpecification.groovy @@ -3,6 +3,7 @@ package org.openbakery import org.apache.tools.ant.util.FileUtils import org.gradle.api.Project import org.gradle.testfixtures.ProjectBuilder +import org.openbakery.packaging.PackageLegacyTask import spock.lang.Specification class AbstractDistributeTaskSpecification extends Specification { @@ -23,7 +24,7 @@ class AbstractDistributeTaskSpecification extends Specification { project.apply plugin: org.openbakery.XcodePlugin - distributeTask = project.tasks.findByName(XcodePlugin.PACKAGE_TASK_NAME); + distributeTask = project.tasks.findByName(PackageLegacyTask.NAME) //XcodeProjectFile xcodeProjectFile = new XcodeProjectFile(project, new File(projectDir, "ExampleWatchkit.xcodeproj/project.pbxproj")); diff --git a/plugin/src/test/groovy/org/openbakery/AbstractXcodeBuildTaskSpecification.groovy b/plugin/src/test/groovy/org/openbakery/AbstractXcodeBuildTaskSpecification.groovy index fb6e3170..c82a991a 100644 --- a/plugin/src/test/groovy/org/openbakery/AbstractXcodeBuildTaskSpecification.groovy +++ b/plugin/src/test/groovy/org/openbakery/AbstractXcodeBuildTaskSpecification.groovy @@ -8,27 +8,27 @@ import spock.lang.Specification class AbstractXcodeBuildTaskSpecification extends Specification { - Project project AbstractXcodeBuildTask xcodeBuildTask File projectDir - - + XcodeBuildPluginExtension extension def setup() { projectDir = new File(System.getProperty("java.io.tmpdir"), "gradle-xcodebuild") project = ProjectBuilder.builder().withProjectDir(projectDir).build() project.buildDir = new File('build').absoluteFile - project.apply plugin: org.openbakery.XcodePlugin + project.apply plugin: XcodePlugin xcodeBuildTask = project.getTasks().getByPath(XcodePlugin.XCODE_BUILD_TASK_NAME) + + extension = project.extensions + .getByType(XcodeBuildPluginExtension) } def cleanup() { - FileUtils.deleteDirectory(project.projectDir) + FileUtils.deleteDirectory(project.projectDir) } - def "test get identity when is set manually"() { when: project.xcodebuild.signing.identity = "my identity" @@ -37,7 +37,6 @@ class AbstractXcodeBuildTaskSpecification extends Specification { xcodeBuildTask.getSigningIdentity() == "my identity" } - def "security is initialized"() { expect: xcodeBuildTask.security != null @@ -48,12 +47,12 @@ class AbstractXcodeBuildTaskSpecification extends Specification { given: File keychain = new File(projectDir, "my.keychain") FileUtils.writeStringToFile(keychain, "dummy") - project.xcodebuild.signing.keychain = keychain - project.xcodebuild.signing.identity = null + extension.signing.keychain = keychain + extension.signing.identity = null when: Security security = Mock(Security) - security.getIdentity(project.xcodebuild.signing.getKeychainPathInternal()) >> "my identity from security" + security.getIdentity(extension.signing.getKeychainPathInternal()) >> "my identity from security" xcodeBuildTask.security = security then: @@ -65,11 +64,10 @@ class AbstractXcodeBuildTaskSpecification extends Specification { def "test get identity null and keychain does not exist should return null"() { when: - project.xcodebuild.signing.identity = null - project.xcodebuild.signing.keychain = new File("my.keychain") + extension.signing.identity = null + extension.signing.keychain = new File("my.keychain") then: xcodeBuildTask.getSigningIdentity() == null } - } diff --git a/plugin/src/test/groovy/org/openbakery/InfoPlistModifyTaskSpecification.groovy b/plugin/src/test/groovy/org/openbakery/InfoPlistModifyTaskSpecification.groovy index 0e5d4df0..a580c9ae 100644 --- a/plugin/src/test/groovy/org/openbakery/InfoPlistModifyTaskSpecification.groovy +++ b/plugin/src/test/groovy/org/openbakery/InfoPlistModifyTaskSpecification.groovy @@ -5,8 +5,9 @@ import org.gradle.api.Project import org.gradle.testfixtures.ProjectBuilder import org.openbakery.testdouble.PlistHelperStub import spock.lang.Specification +import spock.lang.Unroll -class InfoPlistModifyTaskSpecification extends Specification{ +class InfoPlistModifyTaskSpecification extends Specification { Project project @@ -18,15 +19,12 @@ class InfoPlistModifyTaskSpecification extends Specification{ CommandRunner commandRunner = Mock(CommandRunner) def setup() { - projectDir = new File(System.getProperty("java.io.tmpdir"), "gradle-xcodebuild") project = ProjectBuilder.builder().withProjectDir(projectDir).build() project.apply plugin: org.openbakery.XcodePlugin - projectDir.mkdirs() - project.xcodebuild.infoPlist = "App-Info.plist" task = project.tasks.findByName('infoplistModify') @@ -35,14 +33,57 @@ class InfoPlistModifyTaskSpecification extends Specification{ infoPlist = new File(task.project.projectDir, "App-Info.plist") FileUtils.writeStringToFile(infoPlist, "dummy") - - } def cleanup() { FileUtils.deleteDirectory(project.projectDir) } + def "add suffix"() { + given: + project.infoplist.bundleIdentifier = 'org.openbakery.test.Example' + project.infoplist.bundleIdentifierSuffix = '.suffix' + + when: + task.prepare() + + then: + plistHelper.getValueFromPlist(infoPlist, "CFBundleIdentifier") == 'org.openbakery.test.Example.suffix' + } + + @Unroll + def "add suffix `#bundleId` suffix : `#suffix`"() { + given: + BuildConfiguration bcRelease = new BuildConfiguration("Target") + bcRelease.bundleIdentifier = bundleId + + BuildTargetConfiguration btc = new BuildTargetConfiguration() + btc.buildSettings[configuration] = bcRelease + + HashMap projectSettings = new HashMap<>() + projectSettings.put(scheme, btc) + + def extension = project.extensions.getByType(XcodeBuildPluginExtension) + extension.projectSettings = projectSettings + extension.scheme.set(scheme) + extension.configuration = configuration + + project.infoplist.bundleIdentifierSuffix = suffix + + when: + task.prepare() + + then: + noExceptionThrown() + plistHelper.getValueFromPlist(infoPlist, "CFBundleIdentifier") == expectedResult + + where: + configuration | scheme | suffix | bundleId | expectedResult + "Release" | "Test" | ".suffix" | "he.llo.world" | "he.llo.world.suffix" + "Release" | "Test" | "" | "he.llo.world" | "he.llo.world" + "Release" | "Test" | null | "he.llo.world" | null + "Debug" | "Test" | null | "he.llo.world" | null + } def "modify BundleIdentifier"() { given: @@ -65,11 +106,10 @@ class InfoPlistModifyTaskSpecification extends Specification{ then: plistHelper.plistCommands[0] == "Add CFBundleURLTypes:0:CFBundleURLName string" - } def "modify command multiple"() { - project.infoplist.commands = ["Add CFBundleURLTypes:0:CFBundleURLName string", "Add CFBundleURLTypes:0:CFBundleURLSchemes array" ] + project.infoplist.commands = ["Add CFBundleURLTypes:0:CFBundleURLName string", "Add CFBundleURLTypes:0:CFBundleURLSchemes array"] when: task.prepare() @@ -100,7 +140,6 @@ class InfoPlistModifyTaskSpecification extends Specification{ then: plistHelper.getValueFromPlist(infoPlist, "CFBundleShortVersionString") == "1.2.3" - } def "nothing to modify"() { diff --git a/plugin/src/test/groovy/org/openbakery/PrepareXcodeArchivingTaskTest.groovy b/plugin/src/test/groovy/org/openbakery/PrepareXcodeArchivingTaskTest.groovy new file mode 100644 index 00000000..8efa86c0 --- /dev/null +++ b/plugin/src/test/groovy/org/openbakery/PrepareXcodeArchivingTaskTest.groovy @@ -0,0 +1,96 @@ +package org.openbakery + +import org.gradle.api.Project +import org.gradle.testfixtures.ProjectBuilder +import org.junit.Rule +import org.junit.rules.TemporaryFolder +import org.openbakery.codesign.ProvisioningProfileReader +import org.openbakery.extension.Signing +import org.openbakery.util.PlistHelper +import spock.lang.Specification + +import static org.openbakery.PrepareXcodeArchivingTask.* + +class PrepareXcodeArchivingTaskTest extends Specification { + + PrepareXcodeArchivingTask subject + Signing signing + Project project + File outputFile + File entitlementsFile + + CommandRunner commandRunner = Mock(CommandRunner) + ProvisioningProfileReader provisioningProfileReader = Mock(ProvisioningProfileReader) + PlistHelper plistHelper = Mock(PlistHelper) + + private static final String FAKE_TEAM_ID = "Team Identifier" + private static final String FAKE_BUNDLE_IDENTIFIER = "co.test.test" + private static final String FAKE_FRIENDLY_NAME = "Fake Certificate FriendlyName (12345)" + private static final String FAKE_UUID = "FAKE_UUID" + private static final String FAKE_PROV_NAME = "Provisioning Name" + + @Rule + final TemporaryFolder tmpDirectory = new TemporaryFolder() + + def setup() { + this.project = ProjectBuilder.builder().withProjectDir(tmpDirectory.root).build() + this.project.apply plugin: XcodePlugin + + this.entitlementsFile = tmpDirectory.newFile("test.entitlements") + this.outputFile = tmpDirectory.newFile("test.xcconfig") + this.signing = project.extensions.findByType(Signing) + + configureSubject() + + provisioningProfileReader.getTeamIdentifierPrefix() >> FAKE_TEAM_ID + provisioningProfileReader.getUUID() >> FAKE_UUID + provisioningProfileReader.getName() >> FAKE_PROV_NAME + } + + def configureSubject() { + subject = project.tasks.findByName(NAME) as PrepareXcodeArchivingTask + subject.certificateFriendlyName.set(FAKE_FRIENDLY_NAME) + subject.commandRunnerProperty.set(commandRunner) + subject.configurationBundleIdentifier.set(FAKE_BUNDLE_IDENTIFIER) + subject.outputFile.set(outputFile) + subject.plistHelperProperty.set(plistHelper) + subject.provisioningReader.set(provisioningProfileReader) + } + + def "The generation should be executed without exception"() { + when: + subject.generate() + + then: + noExceptionThrown() + + and: "The generate file content should be valid" + String text = outputFile.text + text.contains("${KEY_CODE_SIGN_IDENTITY} = ${FAKE_FRIENDLY_NAME}") + text.contains("${KEY_DEVELOPMENT_TEAM} = ${FAKE_TEAM_ID}") + text.contains("${KEY_PROVISIONING_PROFILE_ID} = ${FAKE_UUID}") + text.contains("${KEY_PROVISIONING_PROFILE_SPEC} = ${FAKE_PROV_NAME}") + + and: "And no entitlements information should be present" + !text.contains("${KEY_CODE_SIGN_ENTITLEMENTS} = ") + } + + def "The generate file should refer the entitlements file is present"() { + when: + subject.entitlementsFile.set(entitlementsFile) + subject.generate() + + then: + noExceptionThrown() + + and: + String text = outputFile.text + text.contains("${KEY_CODE_SIGN_IDENTITY} = ${FAKE_FRIENDLY_NAME}") + text.contains("${KEY_DEVELOPMENT_TEAM} = ${FAKE_TEAM_ID}") + text.contains("${KEY_PROVISIONING_PROFILE_ID} = ${FAKE_UUID}") + text.contains("${KEY_PROVISIONING_PROFILE_SPEC} = ${FAKE_PROV_NAME}") + + and: "No entitlements information should be present" + text.contains("${KEY_CODE_SIGN_ENTITLEMENTS} = ${entitlementsFile.absolutePath}") + } +} diff --git a/plugin/src/test/groovy/org/openbakery/XcodeBuildArchiveTaskOSXSpecification.groovy b/plugin/src/test/groovy/org/openbakery/XcodeBuildArchiveTaskOSXSpecification.groovy index 961ae033..4fd8d4b2 100644 --- a/plugin/src/test/groovy/org/openbakery/XcodeBuildArchiveTaskOSXSpecification.groovy +++ b/plugin/src/test/groovy/org/openbakery/XcodeBuildArchiveTaskOSXSpecification.groovy @@ -4,6 +4,7 @@ import org.apache.commons.configuration.plist.XMLPropertyListConfiguration import org.apache.commons.io.FileUtils import org.gradle.api.Project import org.gradle.testfixtures.ProjectBuilder +import org.openbakery.archiving.XcodeBuildLegacyArchiveTask import org.openbakery.testdouble.PlistHelperStub import org.openbakery.xcode.Type import spock.lang.Specification @@ -12,7 +13,7 @@ class XcodeBuildArchiveTaskOSXSpecification extends Specification { Project project - XcodeBuildArchiveTask xcodeBuildArchiveTask; + XcodeBuildLegacyArchiveTask xcodeBuildArchiveTask; File projectDir File buildOutputDirectory @@ -32,7 +33,7 @@ class XcodeBuildArchiveTaskOSXSpecification extends Specification { project.xcodebuild.signing.keychain = "/var/tmp/gradle.keychain" project.xcodebuild.signing.identity = "my identity" - xcodeBuildArchiveTask = project.getTasks().getByPath(XcodePlugin.ARCHIVE_TASK_NAME) + xcodeBuildArchiveTask = project.getTasks().getByPath(XcodeBuildLegacyArchiveTask.NAME) xcodeBuildArchiveTask.commandRunner = commandRunner xcodeBuildArchiveTask.parameters.type = Type.macOS diff --git a/plugin/src/test/groovy/org/openbakery/XcodeBuildArchiveTaskSpecification.groovy b/plugin/src/test/groovy/org/openbakery/XcodeBuildArchiveTaskSpecification.groovy index f84d1752..76142e4e 100644 --- a/plugin/src/test/groovy/org/openbakery/XcodeBuildArchiveTaskSpecification.groovy +++ b/plugin/src/test/groovy/org/openbakery/XcodeBuildArchiveTaskSpecification.groovy @@ -5,6 +5,8 @@ import org.apache.commons.io.FileUtils import org.apache.commons.lang.RandomStringUtils import org.gradle.api.Project import org.gradle.testfixtures.ProjectBuilder +import org.openbakery.archiving.XcodeBuildLegacyArchiveTask +import org.openbakery.signing.ProvisioningInstallTask import org.openbakery.testdouble.PlistHelperStub import org.openbakery.testdouble.SimulatorControlStub import org.openbakery.testdouble.XcodeFake @@ -23,7 +25,7 @@ class XcodeBuildArchiveTaskSpecification extends Specification { Project project - XcodeBuildArchiveTask xcodeBuildArchiveTask + XcodeBuildLegacyArchiveTask xcodeBuildArchiveTask File projectDir File buildOutputDirectory @@ -33,7 +35,6 @@ class XcodeBuildArchiveTaskSpecification extends Specification { PlistHelperStub plistHelper = new PlistHelperStub() def setup() { - String tmpName = "gradle-xcodebuild-" + RandomStringUtils.randomAlphanumeric(5) projectDir = new File(System.getProperty("java.io.tmpdir"), tmpName) project = ProjectBuilder.builder().withProjectDir(projectDir).build() @@ -48,7 +49,7 @@ class XcodeBuildArchiveTaskSpecification extends Specification { project.xcodebuild.signing.identity = "my identity" - xcodeBuildArchiveTask = project.getTasks().getByPath(XcodePlugin.ARCHIVE_TASK_NAME) + xcodeBuildArchiveTask = project.getTasks().getByPath(XcodeBuildLegacyArchiveTask.NAME) xcodeBuildArchiveTask.plistHelper = plistHelper xcodeBuildArchiveTask.commandRunner = commandRunner xcodeBuildArchiveTask.xcode.commandRunner = commandRunner @@ -159,7 +160,7 @@ class XcodeBuildArchiveTaskSpecification extends Specification { dependsOn.size() == 2 dependsOn.contains(XcodePlugin.XCODE_BUILD_TASK_NAME) - dependsOn.contains(XcodePlugin.PROVISIONING_INSTALL_TASK_NAME) + dependsOn.contains(ProvisioningInstallTask.TASK_NAME) } diff --git a/plugin/src/test/groovy/org/openbakery/XcodeBuildForTestTaskSpecification.groovy b/plugin/src/test/groovy/org/openbakery/XcodeBuildForTestTaskSpecification.groovy index 3b5081d9..07a951b4 100644 --- a/plugin/src/test/groovy/org/openbakery/XcodeBuildForTestTaskSpecification.groovy +++ b/plugin/src/test/groovy/org/openbakery/XcodeBuildForTestTaskSpecification.groovy @@ -170,6 +170,7 @@ class XcodeBuildForTestTaskSpecification extends Specification { def "xcodebuild has all available tvOS destinations"() { when: + xcodeBuildForTestTask.parameters.simulator = true xcodeBuildForTestTask.parameters.type = Type.tvOS def destinations = xcodeBuildForTestTask.xcodebuild.destinations diff --git a/plugin/src/test/groovy/org/openbakery/XcodeBuildPluginExtensionSpecification.groovy b/plugin/src/test/groovy/org/openbakery/XcodeBuildPluginExtensionSpecification.groovy index a7478786..8e2bbb09 100644 --- a/plugin/src/test/groovy/org/openbakery/XcodeBuildPluginExtensionSpecification.groovy +++ b/plugin/src/test/groovy/org/openbakery/XcodeBuildPluginExtensionSpecification.groovy @@ -29,8 +29,7 @@ class XcodeBuildPluginExtensionSpecification extends Specification { project = ProjectBuilder.builder().withProjectDir(projectDir).build() project.apply plugin: org.openbakery.XcodePlugin - extension = new XcodeBuildPluginExtension(project) - extension.commandRunner = commandRunner + extension = new XcodeBuildPluginExtension(project, commandRunner) extension.infoPlist = "Info.plist"; @@ -154,7 +153,7 @@ class XcodeBuildPluginExtensionSpecification extends Specification { when: File projectDir = new File("../example/iOS/Example") project = ProjectBuilder.builder().withProjectDir(projectDir).build() - extension = new XcodeBuildPluginExtension(project) + extension = new XcodeBuildPluginExtension(project, commandRunner) XcodeProjectFile xcodeProjectFile = new XcodeProjectFile(project, new File(projectDir, "Example.xcodeproj/project.pbxproj")) extension.projectSettings = xcodeProjectFile.getProjectSettings() extension.type = Type.iOS @@ -176,8 +175,8 @@ class XcodeBuildPluginExtensionSpecification extends Specification { when: File projectDir = new File("../example/iOS/Example") project = ProjectBuilder.builder().withProjectDir(projectDir).build() - extension = new XcodeBuildPluginExtension(project) - extension.commandRunner = new CommandRunner() + extension = new XcodeBuildPluginExtension(project, commandRunner) + XcodeProjectFile xcodeProjectFile = new XcodeProjectFile(project, new File(projectDir, "Example.xcodeproj/project.pbxproj")) extension.projectSettings = xcodeProjectFile.getProjectSettings() @@ -194,12 +193,33 @@ class XcodeBuildPluginExtensionSpecification extends Specification { } - def "application bundle for watchos"() { + def "application bundle for tvOS"() { when: + File projectDir = new File("../example/tvOS/Example_tvOS_Swift") + project = ProjectBuilder.builder().withProjectDir(projectDir).build() + extension = new XcodeBuildPluginExtension(project, commandRunner) + + XcodeProjectFile xcodeProjectFile = new XcodeProjectFile(project, new File(projectDir, "Example_tvOS_Swift.xcodeproj/project.pbxproj")) + extension.projectSettings = xcodeProjectFile.getProjectSettings() + + extension.type = Type.tvOS + extension.simulator = false + extension.target = "Example_tvOS_Swift" + extension.productName = "Example_tvOS_Swift" + extension.infoPlist = "../../example/tvOS/Example_tvOS_Swift/Example_tvOS_Swift/Info.plist" + + String applicationBundle = extension.getApplicationBundle().absolutePath; + + then: + applicationBundle.endsWith("build/sym/Debug-appletvos/Example_tvOS_Swift.app") + + } + def "application bundle for ios"() { + setup: File projectDir = new File("../example/iOS/ExampleWatchkit") project = ProjectBuilder.builder().withProjectDir(projectDir).build() - extension = new XcodeBuildPluginExtension(project) + extension = new XcodeBuildPluginExtension(project, commandRunner) extension.projectSettings = createProjectSettings() extension.type = Type.iOS extension.target = "ExampleWatchkit WatchKit App" @@ -208,21 +228,20 @@ class XcodeBuildPluginExtensionSpecification extends Specification { extension.productType = "app" extension.infoPlist = "../../example/iOS/ExampleWatchkit/ExampleWatchkit WatchKit App/Info.plist" - - String applicationBundle = extension.getApplicationBundle().absolutePath; + when: + String applicationBundle = extension.getApplicationBundle().absolutePath then: - applicationBundle.endsWith("build/sym/Debug-iphoneos/ExampleWatchkit.app") - + applicationBundle.endsWith("build/sym/Debug-iphoneos/ExampleWatchkit WatchKit App.app") } def "application bundle for watch find parent"() { when: File projectDir = new File("../example/iOS/ExampleWatchkit") project = ProjectBuilder.builder().withProjectDir(projectDir).build() - extension = new XcodeBuildPluginExtension(project) + extension = new XcodeBuildPluginExtension(project, commandRunner) extension.projectSettings = createProjectSettings() - extension.type = Type.iOS + extension.type = Type.watchOS extension.target = "ExampleWatchkit WatchKit App" extension.simulator = false @@ -230,11 +249,9 @@ class XcodeBuildPluginExtensionSpecification extends Specification { BuildConfiguration parent = extension.getParent(extension.projectSettings["ExampleWatchkit WatchKit App"].buildSettings["Debug"]) then: - parent.bundleIdentifier == "org.openbakery.test.Example" + parent.bundleIdentifier == "org.openbakery.test.Example.watchkitapp" } - - void mockValueFromPlist(String key, String value) { PlistHelperStub plistHelperStub = new PlistHelperStub() File infoPlist = new File(project.projectDir, "Info.plist") @@ -396,7 +413,7 @@ class XcodeBuildPluginExtensionSpecification extends Specification { when: File projectDir = new File("../example/iOS/Example") project = ProjectBuilder.builder().withProjectDir(projectDir).build() - extension = new XcodeBuildPluginExtension(project) + extension = new XcodeBuildPluginExtension(project, commandRunner) XcodeProjectFile xcodeProjectFile = new XcodeProjectFile(project, new File(projectDir, "Example.xcodeproj/project.pbxproj")) extension.projectSettings = xcodeProjectFile.getProjectSettings() extension.type = Type.iOS @@ -410,11 +427,29 @@ class XcodeBuildPluginExtensionSpecification extends Specification { } + def "get binary tvOS"() { + when: + File projectDir = new File("../example/tvOS/Example_tvOS_Swift") + project = ProjectBuilder.builder().withProjectDir(projectDir).build() + extension = new XcodeBuildPluginExtension(project, commandRunner) + XcodeProjectFile xcodeProjectFile = new XcodeProjectFile(project, new File(projectDir, "Example_tvOS_Swift.xcodeproj/project.pbxproj")) + extension.projectSettings = xcodeProjectFile.getProjectSettings() + extension.type = Type.tvOS + extension.simulator = false + extension.target = "Example_tvOS_Swift" + extension.productType = "appex" + extension.infoPlist = "../../example/tvOS/Example_tvOS_Swift/Example_tvOS_Swift/Info.plist" + + then: + extension.getBinary().toString().endsWith("Debug-appletvos/Example_tvOS_Swift.app/Example_tvOS_Swift") + } + + def "get binary OS X"() { when: File projectDir = new File("../example/OSX/ExampleOSX") project = ProjectBuilder.builder().withProjectDir(projectDir).build() - extension = new XcodeBuildPluginExtension(project) + extension = new XcodeBuildPluginExtension(project, commandRunner) XcodeProjectFile xcodeProjectFile = new XcodeProjectFile(project, new File(projectDir, "ExampleOSX.xcodeproj/project.pbxproj")) extension.projectSettings = xcodeProjectFile.getProjectSettings() extension.type = Type.macOS @@ -432,7 +467,7 @@ class XcodeBuildPluginExtensionSpecification extends Specification { when: File projectDir = new File("../example/iOS/Example") project = ProjectBuilder.builder().withProjectDir(projectDir).build() - extension = new XcodeBuildPluginExtension(project) + extension = new XcodeBuildPluginExtension(project, commandRunner) then: extension.projectFile.canonicalFile == new File("../example/iOS/Example/Example.xcodeproj").canonicalFile @@ -443,7 +478,7 @@ class XcodeBuildPluginExtensionSpecification extends Specification { setup: File projectDir = new File(".") project = ProjectBuilder.builder().withProjectDir(projectDir).build() - extension = new XcodeBuildPluginExtension(project) + extension = new XcodeBuildPluginExtension(project, commandRunner) when: File missingFile = extension.projectFile @@ -457,64 +492,19 @@ class XcodeBuildPluginExtensionSpecification extends Specification { when: File projectDir = new File("../example/iOS") project = ProjectBuilder.builder().withProjectDir(projectDir).build() - extension = new XcodeBuildPluginExtension(project) + extension = new XcodeBuildPluginExtension(project, commandRunner) extension.projectFile = "Example/Example.xcodeproj" then: extension.projectFile.canonicalFile == new File("../example/iOS/Example/Example.xcodeproj").canonicalFile } - - def "XcodebuildParameters are created with proper values"() { - when: - File projectDir = new File("../example/macOS/ExampleOSX") - project = ProjectBuilder.builder().withProjectDir(projectDir).build() - extension = new XcodeBuildPluginExtension(project) - extension.type = Type.macOS - extension.simulator = false - extension.target = "ExampleOSX" - extension.scheme = "ExampleScheme" - - extension.workspace = "workspace" - extension.configuration = "configuration" - extension.additionalParameters = "additionalParameters" - - extension.dstRoot = new File(projectDir, "dstRoot") - extension.objRoot = new File(projectDir, "objRoot") - extension.symRoot = new File(projectDir, "symRoot") - extension.sharedPrecompsDir = new File(projectDir, "sharedPrecompsDir") - extension.derivedDataPath = new File(projectDir, "derivedDataPath") - extension.arch = ['i386'] - - - def parameters = extension.getXcodebuildParameters() - - then: - parameters.type == Type.macOS - parameters.simulator == false - parameters.target == "ExampleOSX" - parameters.scheme == "ExampleScheme" - parameters.workspace == "workspace" - parameters.configuration == "configuration" - parameters.additionalParameters == "additionalParameters" - parameters.dstRoot.canonicalPath == new File(projectDir, "dstRoot").canonicalPath - parameters.objRoot.canonicalPath == new File(projectDir, "objRoot").canonicalPath - parameters.symRoot.canonicalPath == new File(projectDir, "symRoot").canonicalPath - parameters.sharedPrecompsDir.canonicalPath == new File(projectDir, "sharedPrecompsDir").canonicalPath - parameters.derivedDataPath.canonicalPath == new File(projectDir, "derivedDataPath").canonicalPath - - parameters.arch.size() == 1 - parameters.arch[0] == "i386" - - parameters.devices == Devices.UNIVERSAL - } - def "XcodebuildParameters get workspace from project"() { when: File projectDir = new File("../example/iOS/SwiftExample") project = ProjectBuilder.builder().withProjectDir(projectDir).build() - extension = new XcodeBuildPluginExtension(project) + extension = new XcodeBuildPluginExtension(project, commandRunner) extension.type = Type.iOS workspaceDirectory = new File(projectDir, "SwiftExample.xcworkspace") @@ -529,14 +519,14 @@ class XcodeBuildPluginExtensionSpecification extends Specification { def "bitcode is default false"() { expect: - extension.bitcode == false + extension.bitcode.get() == false } def "xcodeparameter is created with simulator true value"() { when: File projectDir = new File("../example/macOS/ExampleOSX") project = ProjectBuilder.builder().withProjectDir(projectDir).build() - extension = new XcodeBuildPluginExtension(project) + extension = new XcodeBuildPluginExtension(project, commandRunner) extension.simulator = true then: @@ -547,18 +537,18 @@ class XcodeBuildPluginExtensionSpecification extends Specification { when: File projectDir = new File("../example/macOS/ExampleOSX") project = ProjectBuilder.builder().withProjectDir(projectDir).build() - extension = new XcodeBuildPluginExtension(project) - extension.bitcode = true + extension = new XcodeBuildPluginExtension(project, commandRunner) + extension.bitcode.set(true) then: - extension.getXcodebuildParameters().bitcode == true + extension.getXcodebuildParameters().bitcode } def "xcodebuildParameters is created with the applicationBundle"() { when: File projectDir = new File("../example/macOS/ExampleOSX") project = ProjectBuilder.builder().withProjectDir(projectDir).build() - extension = new XcodeBuildPluginExtension(project) + extension = new XcodeBuildPluginExtension(project, commandRunner) extension.bundleName = "MyApp" then: diff --git a/plugin/src/test/groovy/org/openbakery/XcodePluginSpecification.groovy b/plugin/src/test/groovy/org/openbakery/XcodePluginSpecification.groovy index 463729a4..1e62a0f9 100644 --- a/plugin/src/test/groovy/org/openbakery/XcodePluginSpecification.groovy +++ b/plugin/src/test/groovy/org/openbakery/XcodePluginSpecification.groovy @@ -9,6 +9,9 @@ import org.gradle.testfixtures.ProjectBuilder import org.openbakery.appstore.AppstorePluginExtension import org.openbakery.appstore.AppstoreUploadTask import org.openbakery.appstore.AppstoreValidateTask +import org.openbakery.archiving.XcodeBuildArchiveTask +import org.openbakery.archiving.XcodeBuildArchiveTaskIosAndTvOS +import org.openbakery.archiving.XcodeBuildLegacyArchiveTask import org.openbakery.carthage.CarthageCleanTask import org.openbakery.carthage.CarthageUpdateTask import org.openbakery.cocoapods.CocoapodsBootstrapTask @@ -46,7 +49,9 @@ class XcodePluginSpecification extends Specification { def "contain task archive"() { expect: - project.tasks.findByName('archive') instanceof XcodeBuildArchiveTask + project.tasks.findByName(XcodeBuildArchiveTask.NAME) instanceof XcodeBuildArchiveTask + project.tasks.findByName(XcodeBuildLegacyArchiveTask.NAME) instanceof XcodeBuildLegacyArchiveTask + project.tasks.findByName(XcodeBuildArchiveTaskIosAndTvOS.NAME) instanceof XcodeBuildArchiveTaskIosAndTvOS } @@ -286,7 +291,7 @@ class XcodePluginSpecification extends Specification { project.tasks.findByName('carthageUpdate') instanceof CarthageUpdateTask } - def "xcodebuild has carthage dependency"() { + def "xcodebuild has carthage bootstrap dependency"() { when: File projectDir = new File("../example/iOS/Example") project = ProjectBuilder.builder().withProjectDir(projectDir).build() @@ -298,7 +303,7 @@ class XcodePluginSpecification extends Specification { then: - task.getTaskDependencies().getDependencies() contains(project.getTasks().getByName(XcodePlugin.CARTHAGE_UPDATE_TASK_NAME)) + task.getTaskDependencies().getDependencies() contains(project.getTasks().getByName(XcodePlugin.CARTHAGE_BOOTSTRAP_TASK_NAME)) } @@ -307,6 +312,11 @@ class XcodePluginSpecification extends Specification { project.tasks.findByName('carthageClean') instanceof CarthageCleanTask } + def "has carthage update task"() { + expect: + project.tasks.findByName('carthageUpdate') instanceof CarthageUpdateTask + } + def "clean has carthage clean dependency"() { when: File projectDir = new File("../example/iOS/Example") diff --git a/plugin/src/test/groovy/org/openbakery/XcodeTestRunTaskSpecification.groovy b/plugin/src/test/groovy/org/openbakery/XcodeTestRunTaskSpecification.groovy index 50a39af2..1a31888d 100644 --- a/plugin/src/test/groovy/org/openbakery/XcodeTestRunTaskSpecification.groovy +++ b/plugin/src/test/groovy/org/openbakery/XcodeTestRunTaskSpecification.groovy @@ -6,6 +6,8 @@ import org.gradle.api.Project import org.gradle.testfixtures.ProjectBuilder import org.openbakery.codesign.Codesign import org.openbakery.output.TestBuildOutputAppender +import org.openbakery.signing.KeychainCreateTask +import org.openbakery.signing.ProvisioningInstallTask import org.openbakery.test.TestResultParser import org.openbakery.testdouble.SimulatorControlStub import org.openbakery.testdouble.XcodeFake @@ -159,7 +161,7 @@ class XcodeTestRunTaskSpecification extends Specification { def "delete derivedData/Logs/Test before test is executed"() { project.xcodebuild.target = "Test" - def testDirectory = new File(project.xcodebuild.derivedDataPath, "Logs/Test") + def testDirectory = new File(project.xcodebuild.derivedDataPath.asFile.getOrNull(), "Logs/Test") FileUtils.writeStringToFile(new File(testDirectory, "foobar"), "dummy"); when: @@ -241,24 +243,10 @@ class XcodeTestRunTaskSpecification extends Specification { project.evaluate() then: - !xcodeTestRunTestTask.getTaskDependencies().getDependencies().contains(project.getTasks().getByName(XcodePlugin.KEYCHAIN_CREATE_TASK_NAME)) - !xcodeTestRunTestTask.getTaskDependencies().getDependencies().contains(project.getTasks().getByName(XcodePlugin.PROVISIONING_INSTALL_TASK_NAME)) + !xcodeTestRunTestTask.getTaskDependencies().getDependencies().contains(project.getTasks().getByName(KeychainCreateTask.TASK_NAME)) + !xcodeTestRunTestTask.getTaskDependencies().getDependencies().contains(project.getTasks().getByName(ProvisioningInstallTask.TASK_NAME)) } - def "when keychain dependency then also has finalized keychain remove"() { - when: - def bundleDirectory = createTestBundleForDeviceBuild() - xcodeTestRunTestTask = project.getTasks().getByPath(XcodePlugin.XCODE_TEST_RUN_TASK_NAME) - xcodeTestRunTestTask.setBundleDirectory(bundleDirectory) - project.evaluate() - - def finalized = xcodeTestRunTestTask.finalizedBy.getDependencies() - def keychainRemoveTask = project.getTasks().getByPath(XcodePlugin.KEYCHAIN_REMOVE_SEARCH_LIST_TASK_NAME) - then: - finalized.contains(keychainRemoveTask) - } - - def "has keychain dependency if device run"() { when: def bundleDirectory = createTestBundleForDeviceBuild() @@ -267,8 +255,8 @@ class XcodeTestRunTaskSpecification extends Specification { project.evaluate() then: - xcodeTestRunTestTask.getTaskDependencies().getDependencies().contains(project.getTasks().getByName(XcodePlugin.KEYCHAIN_CREATE_TASK_NAME)) - xcodeTestRunTestTask.getTaskDependencies().getDependencies().contains(project.getTasks().getByName(XcodePlugin.PROVISIONING_INSTALL_TASK_NAME)) + xcodeTestRunTestTask.getTaskDependencies().getDependencies().contains(project.getTasks().getByName(KeychainCreateTask.TASK_NAME)) + xcodeTestRunTestTask.getTaskDependencies().getDependencies().contains(project.getTasks().getByName(ProvisioningInstallTask.TASK_NAME)) } diff --git a/plugin/src/test/groovy/org/openbakery/XcodeTestTaskSpecification.groovy b/plugin/src/test/groovy/org/openbakery/XcodeTestTaskSpecification.groovy index 09e4ffc6..d2c34731 100644 --- a/plugin/src/test/groovy/org/openbakery/XcodeTestTaskSpecification.groovy +++ b/plugin/src/test/groovy/org/openbakery/XcodeTestTaskSpecification.groovy @@ -518,7 +518,7 @@ class XcodeTestTaskSpecification extends Specification { mockXcodeVersion() project.xcodebuild.target = "Test" - def testDirectory = new File(project.xcodebuild.derivedDataPath, "Logs/Test") + def testDirectory = new File(project.xcodebuild.derivedDataPath.asFile.getOrNull(), "Logs/Test") FileUtils.writeStringToFile(new File(testDirectory, "foobar"), "dummy"); when: diff --git a/plugin/src/test/groovy/org/openbakery/archiving/XcodeBuildArchiveTaskIosAndTvOSTest.groovy b/plugin/src/test/groovy/org/openbakery/archiving/XcodeBuildArchiveTaskIosAndTvOSTest.groovy new file mode 100644 index 00000000..588f0a67 --- /dev/null +++ b/plugin/src/test/groovy/org/openbakery/archiving/XcodeBuildArchiveTaskIosAndTvOSTest.groovy @@ -0,0 +1,114 @@ +package org.openbakery.archiving + +import org.gradle.api.Project +import org.gradle.testfixtures.ProjectBuilder +import org.junit.Rule +import org.junit.rules.ExpectedException +import org.junit.rules.TemporaryFolder +import org.openbakery.CommandRunner +import org.openbakery.XcodePlugin +import org.openbakery.XcodeService +import org.openbakery.xcode.Type +import spock.lang.Shared +import spock.lang.Specification + +import static org.openbakery.xcode.Xcodebuild.* + +class XcodeBuildArchiveTaskIosAndTvOSTest extends Specification { + + @Rule + public ExpectedException exception = ExpectedException.none() + + @Rule + final TemporaryFolder testProjectDir = new TemporaryFolder() + + Project project + XcodeBuildArchiveTaskIosAndTvOS subject + CommandRunner mockCommandRunner = Mock(CommandRunner) + XcodeService.XcodeApp mockXcodeApp = Mock(XcodeService.XcodeApp) + XcodeService mockXcodeService = Mock(XcodeService) + File outputDir + File fakeXccConfig + + @Shared + File fakeXcodeRuntime + + private static final String TEST_SCHEME = "test-scheme" + + void setup() { + project = ProjectBuilder.builder() + .withProjectDir(testProjectDir.root) + .build() + + project.buildDir = testProjectDir.newFile("build.gradle") + project.apply plugin: XcodePlugin + + outputDir = testProjectDir.newFolder("output") + fakeXccConfig = testProjectDir.newFile("test.xcconfig") + fakeXcodeRuntime = testProjectDir.newFile("test.app") + + subject = project.getTasks().getByName(XcodeBuildArchiveTaskIosAndTvOS.NAME) as XcodeBuildArchiveTaskIosAndTvOS + assert subject != null + + subject.xcodeServiceProperty.set(mockXcodeService) + subject.commandRunnerProperty.set(mockCommandRunner) + subject.scheme.set(TEST_SCHEME) + subject.buildType.set(Type.iOS) + subject.outputArchiveFile.set(outputDir) + subject.xcConfigFile.set(fakeXccConfig) + } + + def "The xcode archive should be called with the right configuration"() { + when: + subject.archive() + + then: + noExceptionThrown() + + 1 * mockCommandRunner.run([EXECUTABLE, + ACTION_ARCHIVE, + ARGUMENT_SCHEME, TEST_SCHEME, + ARGUMENT_ARCHIVE_PATH, outputDir.absolutePath, + ARGUMENT_XCCONFIG, fakeXccConfig.absolutePath], _) + } + + def "Should be able to customise xcode version"() { + when: + mockXcodeApp.contentDeveloperFile >> fakeXcodeRuntime + mockXcodeService.getInstallationForVersion(version) >> mockXcodeApp + + subject.xcodeVersion.set(version) + subject.archive() + + then: + noExceptionThrown() + + 1 * mockCommandRunner.run([EXECUTABLE, + ACTION_ARCHIVE, + ARGUMENT_SCHEME, TEST_SCHEME, + ARGUMENT_ARCHIVE_PATH, outputDir.absolutePath, + ARGUMENT_XCCONFIG, fakeXccConfig.absolutePath], + ['DEVELOPER_DIR': fakeXcodeRuntime.absolutePath.toString()]) + + where: + version | envValues + "9.3" | _ + "9.2" | _ + } + + def "The xcode version configuration should be optional"() { + when: + mockXcodeApp.contentDeveloperFile >> fakeXcodeRuntime + subject.xcodeVersion.set(null) + subject.archive() + + then: + noExceptionThrown() + + 1 * mockCommandRunner.run([EXECUTABLE, + ACTION_ARCHIVE, + ARGUMENT_SCHEME, TEST_SCHEME, + ARGUMENT_ARCHIVE_PATH, outputDir.absolutePath, + ARGUMENT_XCCONFIG, fakeXccConfig.absolutePath], [:]) + } +} diff --git a/plugin/src/test/groovy/org/openbakery/carthage/CarthageUpdateTaskSpecification.groovy b/plugin/src/test/groovy/org/openbakery/carthage/CarthageUpdateTaskSpecification.groovy index a0d2ca13..d5b43466 100644 --- a/plugin/src/test/groovy/org/openbakery/carthage/CarthageUpdateTaskSpecification.groovy +++ b/plugin/src/test/groovy/org/openbakery/carthage/CarthageUpdateTaskSpecification.groovy @@ -23,7 +23,7 @@ class CarthageUpdateTaskSpecification extends Specification { CommandRunner commandRunner = Mock(CommandRunner) @Rule - public ExpectedException exception = ExpectedException.none(); + public ExpectedException exception = ExpectedException.none() def setup() { projectDir = File.createTempDir() diff --git a/plugin/src/test/groovy/org/openbakery/coverage/CoverageTaskSpecification.groovy b/plugin/src/test/groovy/org/openbakery/coverage/CoverageTaskSpecification.groovy index fb3f2608..63e0398d 100644 --- a/plugin/src/test/groovy/org/openbakery/coverage/CoverageTaskSpecification.groovy +++ b/plugin/src/test/groovy/org/openbakery/coverage/CoverageTaskSpecification.groovy @@ -36,7 +36,7 @@ class CoverageTaskSpecification extends Specification { def cleanup() { FileUtils.deleteDirectory(project.projectDir) - FileUtils.deleteDirectory(project.xcodebuild.derivedDataPath) + FileUtils.deleteDirectory(project.xcodebuild.derivedDataPath.asFile.getOrNull()) } @@ -116,7 +116,7 @@ class CoverageTaskSpecification extends Specification { coverageTask.binary = createFile("MyApp") coverageTask.report.commandRunner = Mock(org.openbakery.coverage.command.CommandRunner) - File profData = new File(project.xcodebuild.derivedDataPath, "Build/Intermediates/CodeCoverage/MyApp/Coverage.profdata") + File profData = new File(project.xcodebuild.derivedDataPath.asFile.getOrNull(), "Build/Intermediates/CodeCoverage/MyApp/Coverage.profdata") FileUtils.writeStringToFile(profData, "Dummy") when: @@ -135,7 +135,7 @@ class CoverageTaskSpecification extends Specification { destination.setId("MyAppID") project.coverage.setTestResultDestinations([destination]) - File profData = new File(project.xcodebuild.derivedDataPath, "Build/ProfileData/MyAppID/Coverage.profdata") + File profData = new File(project.xcodebuild.derivedDataPath.asFile.getOrNull(), "Build/ProfileData/MyAppID/Coverage.profdata") FileUtils.writeStringToFile(profData, "Dummy") when: @@ -172,7 +172,7 @@ class CoverageTaskSpecification extends Specification { coverageTask = localProject.getTasks().getByPath('coverage') coverageTask.report.commandRunner = Mock(org.openbakery.coverage.command.CommandRunner) - File profData = new File(localProject.xcodebuild.derivedDataPath, "Build/Intermediates/CodeCoverage/ExampleWatchkit/Coverage.profdata") + File profData = new File(localProject.xcodebuild.derivedDataPath.asFile.getOrNull(), "Build/Intermediates/CodeCoverage/ExampleWatchkit/Coverage.profdata") FileUtils.writeStringToFile(profData, "Dummy") when: @@ -328,7 +328,7 @@ class CoverageTaskSpecification extends Specification { coverageTask.binary = createFile("MyApp") coverageTask.report.commandRunner = Mock(org.openbakery.coverage.command.CommandRunner) - File profData = new File(project.xcodebuild.derivedDataPath, "Build/Intermediates/CodeCoverage/Coverage.profdata") + File profData = new File(project.xcodebuild.derivedDataPath.asFile.getOrNull(), "Build/Intermediates/CodeCoverage/Coverage.profdata") FileUtils.writeStringToFile(profData, "Dummy") when: diff --git a/plugin/src/test/groovy/org/openbakery/deploygate/DeployGateUploadTaskSpecification.groovy b/plugin/src/test/groovy/org/openbakery/deploygate/DeployGateUploadTaskSpecification.groovy index 453ea54a..de8e1fce 100644 --- a/plugin/src/test/groovy/org/openbakery/deploygate/DeployGateUploadTaskSpecification.groovy +++ b/plugin/src/test/groovy/org/openbakery/deploygate/DeployGateUploadTaskSpecification.groovy @@ -3,9 +3,8 @@ package org.openbakery.deploygate import org.apache.commons.io.FileUtils import org.gradle.api.Project import org.gradle.testfixtures.ProjectBuilder -import org.openbakery.XcodeBuildArchiveTask +import org.openbakery.util.PathHelper import spock.lang.Specification - /** * User: rene * Date: 11/11/14 @@ -31,7 +30,7 @@ class DeployGateUploadTaskSpecification extends Specification { File ipaBundle = new File(project.getBuildDir(), "package/Test.ipa") FileUtils.writeStringToFile(ipaBundle, "dummy") - File archiveDirectory = new File(project.getBuildDir(), XcodeBuildArchiveTask.ARCHIVE_FOLDER + "/Test.xcarchive") + File archiveDirectory = new File(PathHelper.resolveArchiveFolder(project), "Test.xcarchive") archiveDirectory.mkdirs() infoPlist = new File(archiveDirectory, "Products/Applications/Test.app/Info.plist"); diff --git a/plugin/src/test/groovy/org/openbakery/hockeyapp/HockeyAppTaskSpecification.groovy b/plugin/src/test/groovy/org/openbakery/hockeyapp/HockeyAppTaskSpecification.groovy index 09ffd484..dc1f9ff8 100644 --- a/plugin/src/test/groovy/org/openbakery/hockeyapp/HockeyAppTaskSpecification.groovy +++ b/plugin/src/test/groovy/org/openbakery/hockeyapp/HockeyAppTaskSpecification.groovy @@ -4,9 +4,8 @@ import org.apache.commons.io.FileUtils import org.gradle.api.Project import org.gradle.testfixtures.ProjectBuilder import org.openbakery.CommandRunner -import org.openbakery.XcodeBuildArchiveTask +import org.openbakery.util.PathHelper import spock.lang.Specification - /** * User: rene * Date: 11/11/14 @@ -36,7 +35,7 @@ class HockeyAppTaskSpecification extends Specification { File ipaBundle = new File(project.getBuildDir(), "package/Test.ipa") FileUtils.writeStringToFile(ipaBundle, "dummy") - File archiveDirectory = new File(project.getBuildDir(), XcodeBuildArchiveTask.ARCHIVE_FOLDER + "/Test.xcarchive") + File archiveDirectory = new File(PathHelper.resolveArchiveFolder(project), "Test.xcarchive") archiveDirectory.mkdirs() infoPlist = new File(archiveDirectory, "Products/Applications/Test.app/Info.plist"); diff --git a/plugin/src/test/groovy/org/openbakery/hockeykit/HockeyKitArchiveTaskSpecification.groovy b/plugin/src/test/groovy/org/openbakery/hockeykit/HockeyKitArchiveTaskSpecification.groovy index a73dc8f1..67be1f1c 100644 --- a/plugin/src/test/groovy/org/openbakery/hockeykit/HockeyKitArchiveTaskSpecification.groovy +++ b/plugin/src/test/groovy/org/openbakery/hockeykit/HockeyKitArchiveTaskSpecification.groovy @@ -4,10 +4,9 @@ import org.apache.commons.io.FileUtils import org.gradle.api.Project import org.gradle.testfixtures.ProjectBuilder import org.openbakery.CommandRunner +import org.openbakery.util.PathHelper import org.openbakery.util.PlistHelper -import org.openbakery.XcodeBuildArchiveTask import spock.lang.Specification - /** * User: rene * Date: 11/11/14 @@ -38,7 +37,7 @@ class HockeyKitArchiveTaskSpecification extends Specification { File ipaBundle = new File(project.getBuildDir(), "package/Test.ipa") FileUtils.writeStringToFile(ipaBundle, "dummy") - File archiveDirectory = new File(project.getBuildDir(), XcodeBuildArchiveTask.ARCHIVE_FOLDER + "/Test.xcarchive") + File archiveDirectory = new File(PathHelper.resolveArchiveFolder(project), "Test.xcarchive") archiveDirectory.mkdirs() infoPlist = new File(archiveDirectory, "Products/Applications/Test.app/Info.plist"); diff --git a/plugin/src/test/groovy/org/openbakery/hockeykit/HockeyKitManifestTaskSpecification.groovy b/plugin/src/test/groovy/org/openbakery/hockeykit/HockeyKitManifestTaskSpecification.groovy index 52f940c1..2dcf7b85 100644 --- a/plugin/src/test/groovy/org/openbakery/hockeykit/HockeyKitManifestTaskSpecification.groovy +++ b/plugin/src/test/groovy/org/openbakery/hockeykit/HockeyKitManifestTaskSpecification.groovy @@ -5,7 +5,7 @@ import org.gradle.api.Project import org.gradle.testfixtures.ProjectBuilder import org.openbakery.CommandRunner import org.openbakery.testdouble.PlistHelperStub -import org.openbakery.XcodeBuildArchiveTask +import org.openbakery.util.PathHelper import spock.lang.Specification class HockeyKitManifestTaskSpecification extends Specification { @@ -35,7 +35,7 @@ class HockeyKitManifestTaskSpecification extends Specification { hockeyKitManifestTask.commandRunner = commandRunner - File archiveDirectory = new File(project.getBuildDir(), XcodeBuildArchiveTask.ARCHIVE_FOLDER + "/Test.xcarchive") + File archiveDirectory = new File(PathHelper.resolveArchiveFolder(project), "Test.xcarchive") archiveDirectory.mkdirs() infoPlist = new File(archiveDirectory, "Products/Applications/Test.app/Info.plist"); diff --git a/plugin/src/test/groovy/org/openbakery/packaging/PackageTaskSpecification.groovy b/plugin/src/test/groovy/org/openbakery/packaging/PackageLegacyTaskSpecification.groovy similarity index 93% rename from plugin/src/test/groovy/org/openbakery/packaging/PackageTaskSpecification.groovy rename to plugin/src/test/groovy/org/openbakery/packaging/PackageLegacyTaskSpecification.groovy index 0e036d2a..423a0955 100644 --- a/plugin/src/test/groovy/org/openbakery/packaging/PackageTaskSpecification.groovy +++ b/plugin/src/test/groovy/org/openbakery/packaging/PackageLegacyTaskSpecification.groovy @@ -6,25 +6,25 @@ import org.apache.commons.lang.RandomStringUtils import org.gradle.api.Project import org.gradle.testfixtures.ProjectBuilder import org.openbakery.CommandRunner -import org.openbakery.appledoc.AppledocCleanTask -import org.openbakery.test.ApplicationDummy -import org.openbakery.xcode.Extension -import org.openbakery.xcode.Type -import org.openbakery.XcodeBuildArchiveTask -import org.openbakery.XcodePlugin import org.openbakery.output.StyledTextOutputStub +import org.openbakery.signing.KeychainCreateTask +import org.openbakery.signing.ProvisioningInstallTask +import org.openbakery.test.ApplicationDummy import org.openbakery.testdouble.PlistHelperStub import org.openbakery.testdouble.XcodeFake +import org.openbakery.util.PathHelper +import org.openbakery.xcode.Extension +import org.openbakery.xcode.Type import spock.lang.Specification import java.util.zip.ZipEntry import java.util.zip.ZipFile -class PackageTaskSpecification extends Specification { +class PackageLegacyTaskSpecification extends Specification { Project project - PackageTask packageTask + PackageLegacyTask packageTask ApplicationDummy applicationDummy CommandRunner commandRunner = Mock(CommandRunner) @@ -56,7 +56,7 @@ class PackageTaskSpecification extends Specification { project.xcodebuild.signing.keychain = "/var/tmp/gradle.keychain" - packageTask = project.getTasks().getByPath(XcodePlugin.PACKAGE_TASK_NAME) + packageTask = project.getTasks().getByPath(PackageLegacyTask.NAME) packageTask.plistHelper = plistHelperStub packageTask.commandRunner = commandRunner @@ -86,9 +86,9 @@ class PackageTaskSpecification extends Specification { } void mockExampleApp(boolean withPlugin, boolean withSwift, boolean withFramework = false, boolean adHoc = true, boolean bitcode = false) { - outputPath = new File(project.getBuildDir(), packageTask.PACKAGE_PATH) + outputPath = PathHelper.resolvePackageFolder(project) - archiveDirectory = new File(project.getBuildDir(), XcodeBuildArchiveTask.ARCHIVE_FOLDER + "/Example.xcarchive") + archiveDirectory = new File(PathHelper.resolveArchiveFolder(project), "Example.xcarchive") File payloadDirectory = new File(outputPath, "Payload") payloadAppDirectory = new File(payloadDirectory, "Example.app"); @@ -211,7 +211,7 @@ class PackageTaskSpecification extends Specification { def "swift Framework xcode 6"() { given: mockXcodeVersion() - project.xcodebuild.version = 6 + project.xcodebuild.version.set("6") FileUtils.deleteDirectory(project.projectDir) mockExampleApp(false, true, false, false) @@ -229,7 +229,7 @@ class PackageTaskSpecification extends Specification { def "SwiftSupport should be added for Appstore IPA"() { given: mockXcodeVersion() - project.xcodebuild.version = 7 + project.xcodebuild.version.set("7") FileUtils.deleteDirectory(project.projectDir) mockExampleApp(false, true, false, false) @@ -247,7 +247,7 @@ class PackageTaskSpecification extends Specification { def "SwiftSupport should not be added for AdHoc IPA"() { given: mockXcodeVersion() - project.xcodebuild.version = 7 + project.xcodebuild.version.set("7") FileUtils.deleteDirectory(project.projectDir) mockExampleApp(false, true) @@ -441,20 +441,11 @@ class PackageTaskSpecification extends Specification { when: def dependsOn = packageTask.getDependsOn() then: - dependsOn.contains(XcodePlugin.KEYCHAIN_CREATE_TASK_NAME) - dependsOn.contains(XcodePlugin.PROVISIONING_INSTALL_TASK_NAME) + dependsOn.contains(KeychainCreateTask.TASK_NAME) + dependsOn.contains(ProvisioningInstallTask.TASK_NAME) } - def "finalized"() { - when: - def finalized = packageTask.finalizedBy.getDependencies() - def keychainRemoveTask = project.getTasks().getByPath(XcodePlugin.KEYCHAIN_REMOVE_SEARCH_LIST_TASK_NAME) - then: - finalized.contains(keychainRemoveTask) - } - - def "delete CFBundleResourceSpecification"() { given: mockExampleApp(false, true) diff --git a/plugin/src/test/groovy/org/openbakery/packaging/PackageTask_OSXSpecification.groovy b/plugin/src/test/groovy/org/openbakery/packaging/PackageTask_OSXSpecification.groovy index ff27bf8d..64d6fcb1 100644 --- a/plugin/src/test/groovy/org/openbakery/packaging/PackageTask_OSXSpecification.groovy +++ b/plugin/src/test/groovy/org/openbakery/packaging/PackageTask_OSXSpecification.groovy @@ -5,11 +5,10 @@ import org.apache.commons.io.FilenameUtils import org.gradle.api.Project import org.gradle.testfixtures.ProjectBuilder import org.openbakery.CommandRunner -import org.openbakery.xcode.Type -import org.openbakery.XcodeBuildArchiveTask -import org.openbakery.XcodePlugin import org.openbakery.testdouble.PlistHelperStub import org.openbakery.testdouble.XcodeFake +import org.openbakery.util.PathHelper +import org.openbakery.xcode.Type import spock.lang.Specification import java.util.zip.ZipEntry @@ -18,7 +17,7 @@ import java.util.zip.ZipFile class PackageTask_OSXSpecification extends Specification { Project project - PackageTask packageTask; + PackageLegacyTask packageTask; CommandRunner commandRunner = Mock(CommandRunner) @@ -57,7 +56,7 @@ class PackageTask_OSXSpecification extends Specification { project.xcodebuild.signing.keychain = keychain.absolutePath project.xcodebuild.signing.identity = 'iPhone Developer: Firstname Surename (AAAAAAAAAA)' - packageTask = project.getTasks().getByPath(XcodePlugin.PACKAGE_TASK_NAME) + packageTask = project.getTasks().getByPath(PackageLegacyTask.NAME) packageTask.plistHelper = plistHelperStub packageTask.commandRunner = commandRunner @@ -65,9 +64,9 @@ class PackageTask_OSXSpecification extends Specification { provisionLibraryPath = new File(System.getProperty("user.home") + "/Library/MobileDevice/Provisioning Profiles/"); - archiveDirectory = new File(project.getBuildDir(), XcodeBuildArchiveTask.ARCHIVE_FOLDER + "/Example.xcarchive") + archiveDirectory = new File(PathHelper.resolveArchiveFolder(project), "Example.xcarchive") - outputPath = new File(project.getBuildDir(), packageTask.PACKAGE_PATH) + outputPath = PathHelper.resolvePackageFolder(project) appDirectory = new File(outputPath, "Example.app"); diff --git a/plugin/src/test/groovy/org/openbakery/packaging/PackageTask_WatchAppSpecification.groovy b/plugin/src/test/groovy/org/openbakery/packaging/PackageTask_WatchAppSpecification.groovy index dda0a4f6..cc596e21 100644 --- a/plugin/src/test/groovy/org/openbakery/packaging/PackageTask_WatchAppSpecification.groovy +++ b/plugin/src/test/groovy/org/openbakery/packaging/PackageTask_WatchAppSpecification.groovy @@ -4,18 +4,17 @@ import org.apache.commons.io.FileUtils import org.gradle.api.Project import org.gradle.testfixtures.ProjectBuilder import org.openbakery.CommandRunner -import org.openbakery.xcode.Type -import org.openbakery.XcodeBuildArchiveTask -import org.openbakery.XcodePlugin import org.openbakery.testdouble.PlistHelperStub import org.openbakery.testdouble.XcodeFake +import org.openbakery.util.PathHelper +import org.openbakery.xcode.Type import spock.lang.Specification class PackageTask_WatchAppSpecification extends Specification { Project project - PackageTask packageTask; + PackageLegacyTask packageTask; CommandRunner commandRunner = Mock(CommandRunner) @@ -49,7 +48,7 @@ class PackageTask_WatchAppSpecification extends Specification { project.xcodebuild.signing.keychain = "/var/tmp/gradle.keychain" - packageTask = project.getTasks().getByPath(XcodePlugin.PACKAGE_TASK_NAME) + packageTask = project.getTasks().getByPath(PackageLegacyTask.NAME) packageTask.plistHelper = plistHelperStub packageTask.commandRunner = commandRunner @@ -57,9 +56,9 @@ class PackageTask_WatchAppSpecification extends Specification { provisionLibraryPath = new File(System.getProperty("user.home") + "/Library/MobileDevice/Provisioning Profiles/"); - archiveDirectory = new File(project.getBuildDir(), XcodeBuildArchiveTask.ARCHIVE_FOLDER + "/Example.xcarchive") + archiveDirectory = new File(PathHelper.resolveArchiveFolder(project), "Example.xcarchive") - outputPath = new File(project.getBuildDir(), packageTask.PACKAGE_PATH) + outputPath = PathHelper.resolvePackageFolder(project) File payloadDirectory = new File(outputPath, "Payload") payloadAppDirectory = new File(payloadDirectory, "ExampleWatchKit.app"); diff --git a/plugin/src/test/groovy/org/openbakery/packaging/PackageTask_WatchKitSpecification.groovy b/plugin/src/test/groovy/org/openbakery/packaging/PackageTask_WatchKitSpecification.groovy index 3e13af3e..3f56177a 100644 --- a/plugin/src/test/groovy/org/openbakery/packaging/PackageTask_WatchKitSpecification.groovy +++ b/plugin/src/test/groovy/org/openbakery/packaging/PackageTask_WatchKitSpecification.groovy @@ -4,18 +4,17 @@ import org.apache.commons.io.FileUtils import org.gradle.api.Project import org.gradle.testfixtures.ProjectBuilder import org.openbakery.CommandRunner -import org.openbakery.xcode.Type -import org.openbakery.XcodeBuildArchiveTask -import org.openbakery.XcodePlugin import org.openbakery.testdouble.PlistHelperStub import org.openbakery.testdouble.XcodeFake +import org.openbakery.util.PathHelper +import org.openbakery.xcode.Type import spock.lang.Specification class PackageTask_WatchKitSpecification extends Specification { Project project - PackageTask packageTask; + PackageLegacyTask packageTask; CommandRunner commandRunner = Mock(CommandRunner) @@ -45,7 +44,7 @@ class PackageTask_WatchKitSpecification extends Specification { project.xcodebuild.signing.keychain = "/var/tmp/gradle.keychain" - packageTask = project.getTasks().getByPath(XcodePlugin.PACKAGE_TASK_NAME) + packageTask = project.getTasks().getByPath(PackageLegacyTask.NAME) packageTask.plistHelper = plistHelperStub packageTask.commandRunner = commandRunner @@ -53,9 +52,9 @@ class PackageTask_WatchKitSpecification extends Specification { provisionLibraryPath = new File(System.getProperty("user.home") + "/Library/MobileDevice/Provisioning Profiles/"); - archiveDirectory = new File(project.getBuildDir(), XcodeBuildArchiveTask.ARCHIVE_FOLDER + "/Example.xcarchive") + archiveDirectory = new File(PathHelper.resolveArchiveFolder(project), "Example.xcarchive") - outputPath = new File(project.getBuildDir(), packageTask.PACKAGE_PATH) + outputPath = PathHelper.resolvePackageFolder(project) File payloadDirectory = new File(outputPath, "Payload") payloadAppDirectory = new File(payloadDirectory, "Example.app"); diff --git a/plugin/src/test/groovy/org/openbakery/packaging/ReleaseNotesTest.groovy b/plugin/src/test/groovy/org/openbakery/packaging/ReleaseNotesTest.groovy index c5cdd4ef..baba8159 100644 --- a/plugin/src/test/groovy/org/openbakery/packaging/ReleaseNotesTest.groovy +++ b/plugin/src/test/groovy/org/openbakery/packaging/ReleaseNotesTest.groovy @@ -4,7 +4,7 @@ import org.apache.commons.io.FileUtils import org.gradle.api.Project import org.gradle.testfixtures.ProjectBuilder import org.openbakery.XcodePlugin -import org.openbakery.packaging.ReleaseNotesTask +import org.openbakery.ReleaseNotesTask import org.junit.Before import org.junit.After import org.junit.Test diff --git a/plugin/src/test/groovy/org/openbakery/signing/KeychainCleanupTaskSpecification.groovy b/plugin/src/test/groovy/org/openbakery/signing/KeychainCleanupTaskSpecification.groovy deleted file mode 100644 index c649a011..00000000 --- a/plugin/src/test/groovy/org/openbakery/signing/KeychainCleanupTaskSpecification.groovy +++ /dev/null @@ -1,151 +0,0 @@ -package org.openbakery.signing - -import org.apache.commons.io.FileUtils -import org.gradle.api.Project -import org.gradle.testfixtures.ProjectBuilder -import org.openbakery.CommandRunner -import org.openbakery.XcodeBuildPluginExtension -import org.openbakery.XcodePlugin -import spock.lang.Specification - - -class KeychainCleanupTaskSpecification extends Specification { - - Project project - - KeychainCleanupTask keychainCleanupTask - - CommandRunner commandRunner = Mock(CommandRunner) - File keychainDestinationFile - File certificateFile - - File tmpDirectory - File loginKeychain - - def setup() { - File projectDir = new File("../example/iOS/ExampleWatchkit") - project = ProjectBuilder.builder().withProjectDir(projectDir).build() - tmpDirectory = new File(System.getProperty("java.io.tmpdir"), 'gradle-xcodebuild') - project.buildDir = new File(tmpDirectory, 'build').absoluteFile - - loginKeychain = new File(tmpDirectory, "login.keychain") - FileUtils.writeStringToFile(loginKeychain, "dummy") - - - project.apply plugin: org.openbakery.XcodePlugin - - keychainCleanupTask = project.tasks.findByName(XcodePlugin.KEYCHAIN_CLEAN_TASK_NAME); - keychainCleanupTask.commandRunner = commandRunner - keychainCleanupTask.security.commandRunner = commandRunner - - - certificateFile = File.createTempFile("test", ".cert") - certificateFile.deleteOnExit() - keychainDestinationFile = new File(project.xcodebuild.signing.signingDestinationRoot, certificateFile.getName()) - - } - - def cleanup() { - FileUtils.deleteDirectory(project.buildDir) - FileUtils.deleteDirectory(tmpDirectory) - } - - def "delete keychain OS X 10.8"() { - given: - System.setProperty("os.version", "10.8.0"); - - String result = " \""+ loginKeychain.absolutePath + "\"\n" + - " \"/Library/Keychains/" + XcodeBuildPluginExtension.KEYCHAIN_NAME_BASE + "delete-me.keychain\"\n" + - " \"/Library/Keychains/System.keychain\""; - commandRunner.runWithResult(["security", "list-keychains"]) >> result - - when: - keychainCleanupTask.clean() - - then: - 1 * commandRunner.run(["security", "list-keychains", "-s", loginKeychain.absolutePath]) - - } - - def "delete keychain OS X 10.9"() { - given: - System.setProperty("os.version", "10.9.0"); - - String result = " \""+ loginKeychain.absolutePath + "\"\n" + - " \"/Library/Keychains/" + XcodeBuildPluginExtension.KEYCHAIN_NAME_BASE + "delete-me.keychain\"\n" + - " \"/Library/Keychains/System.keychain\""; - - commandRunner.runWithResult(["security", "list-keychains"]) >> result - - when: - keychainCleanupTask.clean() - - then: - 1 * commandRunner.run(["security", "list-keychains", "-s", loginKeychain.absolutePath]) - - } - - String getSecurityList() { - return " \""+ loginKeychain.absolutePath + "\n" + - " \"/Users/me/Go/pipelines/Build-Appstore/build/codesign/gradle-1431356246879.keychain\"\n" + - " \"/Users/me/Go/pipelines/Build-Test/build/codesign/gradle-1431356877451.keychain\"\n" + - " \"/Users/me/Go/pipelines/Build-Continuous/build/codesign/gradle-1431419900260.keychain\"\n" + - " \"/Library/Keychains/System.keychain\"" - - } - - - def "keychain list update"() { - given: - commandRunner.runWithResult(["security", "list-keychains"]) >> getSecurityList() - - when: - keychainCleanupTask.removeGradleKeychainsFromSearchList() - - then: - 1 * commandRunner.run(["security", "list-keychains", "-s", loginKeychain.absolutePath]) - - } - - - def "get keychain list"() { - given: - commandRunner.runWithResult(["security", "list-keychains"]) >> getSecurityList() - - when: - List keychainList = keychainCleanupTask.getKeychainList() - - then: - keychainList.size == 1 - } - - - def "remove only missing keychain in list"() { - given: - System.setProperty("os.version", "10.9.0"); - - File keychainFile = new File(project.buildDir, "gradle.keychain"); - FileUtils.writeStringToFile(keychainFile, "dummy"); - - String keychainFileName = keychainFile.absolutePath - - String result = " \"" + loginKeychain.absolutePath + "\n" + - " \"" + keychainFileName + "\n" + - " \"/Library/Keychains/System.keychain\""; - - commandRunner.runWithResult(["security", "list-keychains"]) >> result - - - def commandList - - when: - keychainCleanupTask.removeGradleKeychainsFromSearchList() - - then: - 1 * commandRunner.run(_) >> { arguments -> commandList = arguments[0] } - commandList == ["security", "list-keychains", "-s", loginKeychain.absolutePath, keychainFileName] - //1 * commandRunner.run(["security", "list-keychains", "-s", "/Users/me/Library/Keychains/login.keychain", keychainFileName]) - - } - -} diff --git a/plugin/src/test/groovy/org/openbakery/signing/KeychainCreateTaskSpecification.groovy b/plugin/src/test/groovy/org/openbakery/signing/KeychainCreateTaskSpecification.groovy index 36e2579d..af4bb901 100644 --- a/plugin/src/test/groovy/org/openbakery/signing/KeychainCreateTaskSpecification.groovy +++ b/plugin/src/test/groovy/org/openbakery/signing/KeychainCreateTaskSpecification.groovy @@ -3,172 +3,173 @@ package org.openbakery.signing import org.apache.commons.io.FileUtils import org.gradle.api.Project import org.gradle.testfixtures.ProjectBuilder +import org.junit.Rule +import org.junit.rules.TemporaryFolder import org.openbakery.CommandRunner +import org.openbakery.XcodeBuildPluginExtension import org.openbakery.XcodePlugin import org.openbakery.codesign.Security import org.openbakery.xcode.Type -import org.openbakery.xcode.Version import spock.lang.Specification +import spock.lang.Unroll + +import static org.openbakery.signing.KeychainCreateTask.KEYCHAIN_DEFAULT_PASSWORD class KeychainCreateTaskSpecification extends Specification { + @Rule + final TemporaryFolder tmpDirectory = new TemporaryFolder() + + KeychainCreateTask subject + Project project - KeychainCreateTask keychainCreateTask CommandRunner commandRunner = Mock(CommandRunner) File keychainDestinationFile File certificateFile - File tmpDirectory File loginKeychain + File folder + XcodeBuildPluginExtension xcodeBuildPluginExtension + Security mockSecurity - def setup() { - tmpDirectory = new File(System.getProperty("java.io.tmpdir"), 'gradle-xcodebuild') + final static String CERTIFICATE_PASSWORD = "password" + final static String FAKE_CERT_CONTENT = "Bag Attributes\n" + + " localKeyID: FE 93 19 AC CC D7 C1 AC 82 97 02 C2 35 97 B6 CE 37 33 CB 4F\n" + + " friendlyName: iPhone Distribution: Test Company Name (12345ABCDE)" + def setup() { project = ProjectBuilder.builder().build() project.buildDir = new File('build').absoluteFile - project.apply plugin: org.openbakery.XcodePlugin + project.apply plugin: XcodePlugin + + mockSecurity = Mock(Security) - keychainCreateTask = project.tasks.findByName('keychainCreate') - keychainCreateTask.commandRunner = commandRunner - keychainCreateTask.security.commandRunner = commandRunner + subject = project.tasks.findByName('keychainCreate') as KeychainCreateTask + subject.security.set(new Security(commandRunner)) + subject.commandRunnerProperty.set(commandRunner) + xcodeBuildPluginExtension = project.extensions.getByType(XcodeBuildPluginExtension) + folder = xcodeBuildPluginExtension.signing + .signingDestinationRoot + .get() + .asFile - certificateFile = File.createTempFile("test", ".cert") - keychainDestinationFile = new File(project.xcodebuild.signing.signingDestinationRoot, certificateFile.getName()) + certificateFile = tmpDirectory.newFile("test.cert") + certificateFile.text = FAKE_CERT_CONTENT - loginKeychain = new File(tmpDirectory, "login.keychain") + keychainDestinationFile = new File(folder, + certificateFile.getName()) + + loginKeychain = tmpDirectory.newFile("login.keychain") FileUtils.writeStringToFile(loginKeychain, "dummy") project.xcodebuild.type = Type.macOS - project.xcodebuild.signing.certificateURI = certificateFile.toURL() - project.xcodebuild.signing.certificatePassword = "password" + project.xcodebuild.signing.certificatePassword = CERTIFICATE_PASSWORD project.xcodebuild.signing.timeout = null + subject.security.set(mockSecurity) + mockSecurity.getKeychainList() >> [] + commandRunner.runWithResult(_) >> FAKE_CERT_CONTENT } - def cleanup() { - certificateFile.delete() - new File(project.xcodebuild.signing.signingDestinationRoot, certificateFile.getName()).delete() - project.xcodebuild.signing.keychainPathInternal.delete() - FileUtils.deleteDirectory(tmpDirectory) - } - - - def "OSVersion"() { - System.setProperty("os.version", "10.9.0"); - - when: - Version version = keychainCreateTask.getOSVersion() - - then: - version != null; - version.major == 10 - version.minor == 9 - version.maintenance == 0 - } - - - def "create with OS X 10.8"() { + @Unroll + def "Mac OS #version - Temporary keychain should be create and certificate URI imported"() { given: - System.setProperty("os.version", "10.8.0") - - Security security = Mock(Security) - keychainCreateTask.security = security - security.getKeychainList() >> [] + System.setProperty("os.version", version) + project.xcodebuild.signing.certificate = certificateFile when: - keychainCreateTask.create() - - then: - 1 * security.createKeychain(project.xcodebuild.signing.keychainPathInternal, "This_is_the_default_keychain_password") - 1 * security.importCertificate(keychainDestinationFile, "password", project.xcodebuild.signing.keychainPathInternal) - 0 * security.setKeychainList([project.xcodebuild.signing.keychainPathInternal]) + subject.download() + + then: "The `setPartitionList` method should be call only for OS version > 10.12" + 1 * mockSecurity.createKeychain(xcodeBuildPluginExtension.signing.keyChainFile.asFile.get(), + KEYCHAIN_DEFAULT_PASSWORD) + + 1 * mockSecurity.importCertificate(keychainDestinationFile, + CERTIFICATE_PASSWORD, + xcodeBuildPluginExtension.signing.keyChainFile.asFile.get()) + + count * mockSecurity.setPartitionList(xcodeBuildPluginExtension.signing.keyChainFile.asFile.get(), + KEYCHAIN_DEFAULT_PASSWORD) + + where: + version | count + "10.8.0" | 0 + "10.9.0" | 0 + "10.11.0" | 0 + "10.12.0" | 1 + "11.12.0" | 1 + "11.12" | 1 } - - def mockListKeychains() { - String result = " \""+ loginKeychain.absolutePath + "\""; - commandRunner.runWithResult( ["security", "list-keychains"]) >> result - } - - def "create with OS X 10.9 adds keychain to list"() { + @Unroll + def "Mac OS #version - Temporary keychain should be create and certificate File imported"() { given: - System.setProperty("os.version", "10.9.0") - - Security security = Mock(Security) - keychainCreateTask.security = security - security.getKeychainList() >> [] + System.setProperty("os.version", version) + project.xcodebuild.signing.certificate = certificateFile when: - keychainCreateTask.create() - - then: - 1 * security.createKeychain(project.xcodebuild.signing.keychainPathInternal, "This_is_the_default_keychain_password") - 1 * security.importCertificate(keychainDestinationFile, "password", project.xcodebuild.signing.keychainPathInternal) - 1 * security.setKeychainList([project.xcodebuild.signing.keychainPathInternal]) + subject.download() + + then: "The `setPartitionList` method should be call only for OS version > 10.12" + 1 * mockSecurity.createKeychain(xcodeBuildPluginExtension.signing.keyChainFile.asFile.get(), + KEYCHAIN_DEFAULT_PASSWORD) + + 1 * mockSecurity.importCertificate(keychainDestinationFile, + CERTIFICATE_PASSWORD, + xcodeBuildPluginExtension.signing.keyChainFile.asFile.get()) + + count * mockSecurity.setPartitionList(xcodeBuildPluginExtension.signing.keyChainFile.asFile.get(), + KEYCHAIN_DEFAULT_PASSWORD) + + where: + version | count + "10.8.0" | 0 + "10.9.0" | 0 + "10.11.0" | 0 + "10.12.0" | 1 + "11.12.0" | 1 + "11.12" | 1 } - - def "cleanup first"() { + @Unroll + def "The keychain timeout should be called"() { given: - Security security = Mock(Security) - keychainCreateTask.security = security - security.getKeychainList() >> [] - File toBeDeleted = new File(project.xcodebuild.signing.signingDestinationRoot, "my.keychain") - FileUtils.writeStringToFile(toBeDeleted, "dummy") - mockListKeychains() - - when: - keychainCreateTask.create() - - then: - !toBeDeleted.exists() - - } + project.xcodebuild.signing.certificate = certificateFile + if (timeout) + xcodeBuildPluginExtension.signing.timeout.set(timeout) - def "depends on"() { when: - def dependsOn = keychainCreateTask.getDependsOn() - then: - !dependsOn.contains(XcodePlugin.KEYCHAIN_CLEAN_TASK_NAME) - } - - - def "create with macOS 10.12.0 set-key-partition-list"() { - given: - System.setProperty("os.version", "10.12.0") - - Security security = Mock(Security) - keychainCreateTask.security = security - security.getKeychainList() >> [] - - when: - keychainCreateTask.create() + subject.download() then: - 1 * security.createKeychain(project.xcodebuild.signing.keychainPathInternal, "This_is_the_default_keychain_password") - 1 * security.importCertificate(keychainDestinationFile, "password", project.xcodebuild.signing.keychainPathInternal) - 1 * security.setPartitionList(project.xcodebuild.signing.keychainPathInternal, "This_is_the_default_keychain_password") + count * mockSecurity.setTimeout(timeout, + xcodeBuildPluginExtension.signing.keyChainFile.asFile.get()) + + where: + timeout | count + 100 | 1 + 400 | 1 + null | 0 } - - def "create with macOS 10.11.0 NOT set-key-partition-list"() { + def "The temporary keychain file should be present post run"() { given: - System.setProperty("os.version", "10.11.0") + project.xcodebuild.signing.certificate = certificateFile - Security security = Mock(Security) - keychainCreateTask.security = security - security.getKeychainList() >> [] when: - keychainCreateTask.create() + subject.download() then: - 1 * security.createKeychain(project.xcodebuild.signing.keychainPathInternal, "This_is_the_default_keychain_password") - 1 * security.importCertificate(keychainDestinationFile, "password", project.xcodebuild.signing.keychainPathInternal) - 0 * security.setPartitionList(project.xcodebuild.signing.keychainPathInternal, "This_is_the_default_keychain_password") + File file = xcodeBuildPluginExtension.signing + .signingDestinationRoot + .file(certificateFile.name) + .get() + .asFile + file.exists() + file.text == FAKE_CERT_CONTENT } - } diff --git a/plugin/src/test/groovy/org/openbakery/signing/KeychainRemoveFromSearchListTaskSpecification.groovy b/plugin/src/test/groovy/org/openbakery/signing/KeychainRemoveFromSearchListTaskSpecification.groovy deleted file mode 100644 index f72ef2b1..00000000 --- a/plugin/src/test/groovy/org/openbakery/signing/KeychainRemoveFromSearchListTaskSpecification.groovy +++ /dev/null @@ -1,156 +0,0 @@ -package org.openbakery.signing - -import groovy.mock.interceptor.MockFor -import org.apache.commons.io.FileUtils -import org.gradle.api.Project -import org.gradle.testfixtures.ProjectBuilder -import org.junit.Test -import org.openbakery.CommandRunner -import org.openbakery.XcodePlugin -import spock.lang.Specification - -/** - * User: rene - * Date: 14/10/15 - */ -class KeychainRemoveFromSearchListTaskSpecification extends Specification { - - Project project - - KeychainRemoveFromSearchListTask task - File tmpDirectory - File loginKeychain - CommandRunner commandRunner = Mock(CommandRunner) - - def setup() { - File projectDir = new File("../example/iOS/ExampleWatchkit") - - tmpDirectory = new File(System.getProperty("java.io.tmpdir"), 'gradle-xcodebuild') - - project = ProjectBuilder.builder().withProjectDir(projectDir).build() - project.buildDir = new File(tmpDirectory, 'build').absoluteFile - - project.apply plugin: org.openbakery.XcodePlugin - - task = project.tasks.findByName(XcodePlugin.KEYCHAIN_REMOVE_SEARCH_LIST_TASK_NAME); - task.commandRunner = commandRunner - task.security.commandRunner = commandRunner - - loginKeychain = new File(tmpDirectory, "login.keychain") - FileUtils.writeStringToFile(loginKeychain, "dummy") - - - } - - def cleanup() { - FileUtils.deleteDirectory(project.buildDir) - FileUtils.deleteDirectory(tmpDirectory) - } - - def "check group"() { - when: - true - - then: - task.group == XcodePlugin.XCODE_GROUP_NAME - } - - - String createSecurityListResult(String... keychainFiles) { - - StringBuilder builder = new StringBuilder(); - builder.append(" \"") - builder.append(loginKeychain.absolutePath) - builder.append("\"\n") - - - for (String keychainFile in keychainFiles) { - builder.append(" \"") - builder.append(keychainFile) - builder.append("\"\n") - } - - builder.append(" \"/Library/Keychains/System.keychain\"") - return builder.toString() - } - - def "remove"() { - def commandList; - - given: - def securityList = createSecurityListResult( - "/Users/me/Go/pipelines/Build-Appstore/build/codesign/gradle-1431356246879.keychain", - "/Users/me/Go/pipelines/Build-Test/build/codesign/gradle-1431356877451.keychain", - "/Users/me/Go/pipelines/Build-Continuous/build/codesign/gradle-1431419900260.keychain" - ) - - commandRunner.runWithResult(["security", "list-keychains"]) >> securityList - - when: - task.remove() - - then: - 1 * commandRunner.run(_) >> { arguments -> commandList = arguments[0] } - commandList == ["security", "list-keychains", "-s", loginKeychain.absolutePath] - - } - - def "remove with existing files"() { - def commandList; - - given: - File dummyKeychain = new File(project.buildDir, "gradle-1234.keychain") - FileUtils.writeStringToFile(dummyKeychain, "dummy"); - def securityList = createSecurityListResult(dummyKeychain.getAbsolutePath()) - commandRunner.runWithResult(["security", "list-keychains"]) >> securityList - - when: - task.remove() - - then: - 1 * commandRunner.run(_) >> { arguments -> commandList = arguments[0] } - commandList == ["security", "list-keychains", "-s", loginKeychain.absolutePath, dummyKeychain.getAbsolutePath()] - } - - - def "remove current used keychain"() { - def commandList; - - given: - File dummyKeychain = new File(project.buildDir, "gradle-1234.keychain") - FileUtils.writeStringToFile(dummyKeychain, "dummy"); - def securityList = createSecurityListResult(dummyKeychain.getAbsolutePath()) - commandRunner.runWithResult(["security", "list-keychains"]) >> securityList - - project.xcodebuild.signing.keychain = dummyKeychain - - when: - task.remove() - - then: - 1 * commandRunner.run(_) >> { arguments -> commandList = arguments[0] } - commandList == ["security", "list-keychains", "-s", loginKeychain.absolutePath] - } - - - def "remove current used internal keychain"() { - def commandList; - - given: - project.xcodebuild.signing.signingDestinationRoot = project.buildDir - - FileUtils.writeStringToFile(project.xcodebuild.signing.keychainPathInternal, "dummy"); - def securityList = createSecurityListResult(project.xcodebuild.signing.keychainPathInternal.getAbsolutePath()) - commandRunner.runWithResult(["security", "list-keychains"]) >> securityList - - when: - task.remove() - - then: - 1 * commandRunner.run(_) >> { arguments -> commandList = arguments[0] } - commandList == ["security", "list-keychains", "-s", loginKeychain.absolutePath] - } - - - -} diff --git a/plugin/src/test/groovy/org/openbakery/signing/ProvisioningInstallTaskOSXSpecification.groovy b/plugin/src/test/groovy/org/openbakery/signing/ProvisioningInstallTaskOSXSpecification.groovy index a7e78359..50078200 100644 --- a/plugin/src/test/groovy/org/openbakery/signing/ProvisioningInstallTaskOSXSpecification.groovy +++ b/plugin/src/test/groovy/org/openbakery/signing/ProvisioningInstallTaskOSXSpecification.groovy @@ -6,7 +6,6 @@ import org.gradle.testfixtures.ProjectBuilder import org.openbakery.CommandRunner import org.openbakery.codesign.ProvisioningProfileReader import org.openbakery.xcode.Type -import org.openbakery.XcodePlugin import spock.lang.Specification class ProvisioningInstallTaskOSXSpecification extends Specification { @@ -30,9 +29,10 @@ class ProvisioningInstallTaskOSXSpecification extends Specification { project.xcodebuild.type = Type.macOS - provisioningInstallTask = project.getTasks().getByPath(XcodePlugin.PROVISIONING_INSTALL_TASK_NAME) + provisioningInstallTask = project.getTasks() + .getByPath(ProvisioningInstallTask.TASK_NAME) - provisioningInstallTask.commandRunner = commandRunner + provisioningInstallTask.commandRunnerProperty.set(commandRunner) provisionLibraryPath = new File(System.getProperty("user.home") + "/Library/MobileDevice/Provisioning Profiles/"); @@ -49,21 +49,21 @@ class ProvisioningInstallTaskOSXSpecification extends Specification { File testMobileprovision = new File("src/test/Resource/test-wildcard-mac.provisionprofile") project.xcodebuild.signing.mobileProvisionURI = testMobileprovision.toURI().toString() - ProvisioningProfileReader provisioningProfileIdReader = new ProvisioningProfileReader(testMobileprovision, commandRunner) - String uuid = provisioningProfileIdReader.getUUID() + ProvisioningProfileReader reader = new ProvisioningProfileReader(testMobileprovision, commandRunner) + String uuid = reader.getUUID() String name = "gradle-" + uuid + ".provisionprofile"; File source = new File(projectDir, "build/provision/" + name) File destination = new File(provisionLibraryPath, name) when: - provisioningInstallTask.install() + provisioningInstallTask.download() File sourceFile = new File(projectDir, "build/provision/" + name) then: sourceFile.exists() - 1 * commandRunner.run(["/bin/ln", "-s", source.absolutePath , destination.absolutePath]) + 1 * commandRunner.run(["/bin/ln", "-s", source.absolutePath, destination.absolutePath]) } def "multiple ProvisioningProfiles"() { @@ -85,7 +85,7 @@ class ProvisioningInstallTaskOSXSpecification extends Specification { File secondDestination = new File(provisionLibraryPath, secondName) when: - provisioningInstallTask.install() + provisioningInstallTask.download() then: firstFile.exists() diff --git a/plugin/src/test/groovy/org/openbakery/signing/ProvisioningInstallTaskSpecification.groovy b/plugin/src/test/groovy/org/openbakery/signing/ProvisioningInstallTaskSpecification.groovy index 784ac105..43516ea3 100644 --- a/plugin/src/test/groovy/org/openbakery/signing/ProvisioningInstallTaskSpecification.groovy +++ b/plugin/src/test/groovy/org/openbakery/signing/ProvisioningInstallTaskSpecification.groovy @@ -1,130 +1,173 @@ package org.openbakery.signing -import org.apache.commons.io.FileUtils import org.gradle.api.Project import org.gradle.testfixtures.ProjectBuilder +import org.junit.Rule +import org.junit.rules.TemporaryFolder import org.openbakery.CommandRunner -import org.openbakery.codesign.ProvisioningProfileReader +import org.openbakery.XcodeBuildPluginExtension import org.openbakery.XcodePlugin +import org.openbakery.codesign.ProvisioningProfileReader import spock.lang.Specification - class ProvisioningInstallTaskSpecification extends Specification { - Project project - ProvisioningInstallTask provisioningInstallTask; + @Rule + final TemporaryFolder tmpDirectory = new TemporaryFolder() + File testMobileProvision1 + File testMobileProvision2 + Project project + ProvisioningInstallTask subject CommandRunner commandRunner = Mock(CommandRunner) - File provisionLibraryPath - File projectDir + def setup() { + project = ProjectBuilder.builder().withProjectDir(tmpDirectory.root).build() + project.apply plugin: XcodePlugin + subject = project.tasks.findByName(ProvisioningInstallTask.TASK_NAME) as ProvisioningInstallTask - def setup() { + testMobileProvision1 = new File("../libtest/src/main/Resource/test.mobileprovision") + testMobileProvision2 = new File("src/test/Resource/test1.mobileprovision") - projectDir = new File(System.getProperty("java.io.tmpdir"), "gradle-xcodebuild") + assert subject != null + assert testMobileProvision1.exists() + assert testMobileProvision2.exists() + } - project = ProjectBuilder.builder().withProjectDir(projectDir).build() - project.buildDir = new File(projectDir, 'build').absoluteFile - project.apply plugin: org.openbakery.XcodePlugin + private String formatProvisionFileName(File file) { + assert file.exists() - project.xcodebuild.simulator = false + ProvisioningProfileReader provisioningProfileIdReader = new ProvisioningProfileReader(file, commandRunner) + String uuid = provisioningProfileIdReader.getUUID() + String name = "gradle-" + uuid + ".mobileprovision" - provisioningInstallTask = project.getTasks().getByPath(XcodePlugin.PROVISIONING_INSTALL_TASK_NAME) + return name + } - provisioningInstallTask.commandRunner = commandRunner + def "Should process without error a single provisioning file"() { + setup: + project.xcodebuild.signing.mobileProvisionURI = testMobileProvision2.toURI().toString() - provisionLibraryPath = new File(System.getProperty("user.home") + "/Library/MobileDevice/Provisioning Profiles/"); + String name2 = formatProvisionFileName(testMobileProvision2) - } + File downloadedFile2 = new File(tmpDirectory.root, "build/provision/" + name2) + File libraryFile2 = new File(ProvisioningInstallTask.PROVISIONING_DIR, name2) - def cleanup() { - FileUtils.deleteDirectory(projectDir) - } + when: + subject.download() + then: + noExceptionThrown() - def "single ProvisioningProfile"() { + and: + downloadedFile2.text == testMobileProvision2.text + downloadedFile2.exists() - File testMobileprovision = new File("../libtest/src/main/Resource/test.mobileprovision") - project.xcodebuild.signing.mobileProvisionURI = testMobileprovision.toURI().toString() + libraryFile2.text == testMobileProvision2.text + libraryFile2.exists() + } - ProvisioningProfileReader provisioningProfileIdReader = new ProvisioningProfileReader(testMobileprovision, commandRunner) - String uuid = provisioningProfileIdReader.getUUID() - String name = "gradle-" + uuid + ".mobileprovision"; + def "Should process without error a change of build folder"() { + setup: + String alternateBuildFolderName = "alternativeBuildFolder" + project.buildDir = tmpDirectory.newFolder(alternateBuildFolderName) + project.xcodebuild.signing.mobileProvisionURI = testMobileProvision2.toURI().toString() - File source = new File(projectDir, "build/provision/" + name) - File destination = new File(provisionLibraryPath, name) + String name2 = formatProvisionFileName(testMobileProvision2) - File sourceFile = new File(projectDir, "build/provision/" + name) + File downloadedFile2 = new File(tmpDirectory.root, "${alternateBuildFolderName}/provision/" + name2) + File libraryFile2 = new File(ProvisioningInstallTask.PROVISIONING_DIR, name2) when: - provisioningInstallTask.install() + subject.download() then: - sourceFile.exists() - 1 * commandRunner.run(["/bin/ln", "-s", source.absolutePath , destination.absolutePath]) + noExceptionThrown() - } + and: + downloadedFile2.text == testMobileProvision2.text + downloadedFile2.exists() - def "multiple ProvisioningProfiles"() { + libraryFile2.text == testMobileProvision2.text + libraryFile2.exists() + } - File firstMobileprovision = new File("../libtest/src/main/Resource/test.mobileprovision") - File secondMobileprovision = new File("src/test/Resource/test1.mobileprovision") - project.xcodebuild.signing.mobileProvisionURI = [firstMobileprovision.toURI().toString(), secondMobileprovision.toURI().toString() ] + def "Should process without error multiple provisioning files by using the deprecated `setMobileProvisionURI` property"() { - String firstName = "gradle-" + new ProvisioningProfileReader(firstMobileprovision, new CommandRunner()).getUUID() + ".mobileprovision"; - String secondName = "gradle-" + new ProvisioningProfileReader(secondMobileprovision, new CommandRunner()).getUUID() + ".mobileprovision"; + setup: + project.extensions + .findByType(XcodeBuildPluginExtension) + .signing + .setMobileProvisionURI(testMobileProvision1.toURI().toString(), + testMobileProvision2.toURI().toString()) - File firstSource = new File(projectDir, "build/provision/" + firstName) - File firstDestination = new File(provisionLibraryPath, firstName) + String name1 = formatProvisionFileName(testMobileProvision1) + String name2 = formatProvisionFileName(testMobileProvision2) - File secondSource = new File(projectDir, "build/provision/" + secondName) - File secondDestination = new File(provisionLibraryPath, secondName) + File downloadedFile1 = new File(tmpDirectory.root, "build/provision/" + name1) + File libraryFile1 = new File(ProvisioningInstallTask.PROVISIONING_DIR, name1) - File firstFile = new File(projectDir, "build/provision/" + firstName) - File secondFile = new File(projectDir, "build/provision/" + secondName) + File downloadedFile2 = new File(tmpDirectory.root, "build/provision/" + name2) + File libraryFile2 = new File(ProvisioningInstallTask.PROVISIONING_DIR, name2) when: - provisioningInstallTask.install() - + subject.download() then: - firstFile.exists() - secondFile.exists() - 1 * commandRunner.run(["/bin/ln", "-s", firstSource.absolutePath, firstDestination.absolutePath]) - 1 * commandRunner.run(["/bin/ln", "-s", secondSource.absolutePath, secondDestination.absolutePath]) + noExceptionThrown() + + and: + downloadedFile1.text == testMobileProvision1.text + downloadedFile1.exists() + + libraryFile1.text == testMobileProvision1.text + libraryFile1.exists() + + and: + downloadedFile2.text == testMobileProvision2.text + downloadedFile2.exists() + + libraryFile2.text == testMobileProvision2.text + libraryFile2.exists() } + def "Should process without error multiple provisioning files by using directly the `mobileProvisionList` property"() { - def "mobileProvisionFile has mobileprovision extension"() { - given: - File testMobileprovision = new File("../libtest/src/main/Resource/test.mobileprovision") - project.xcodebuild.signing.mobileProvisionURI = testMobileprovision.toURI().toString() + setup: + project.extensions + .findByType(XcodeBuildPluginExtension) + .signing + .mobileProvisionList.set([testMobileProvision1.toURI().toString(), + testMobileProvision2.toURI().toString()]) - ProvisioningProfileReader provisioningProfileIdReader = new ProvisioningProfileReader(testMobileprovision, commandRunner) - String uuid = provisioningProfileIdReader.getUUID() + String name1 = formatProvisionFileName(testMobileProvision1) + String name2 = formatProvisionFileName(testMobileProvision2) + + File downloadedFile1 = new File(tmpDirectory.root, "build/provision/" + name1) + File libraryFile1 = new File(ProvisioningInstallTask.PROVISIONING_DIR, name1) + + File downloadedFile2 = new File(tmpDirectory.root, "build/provision/" + name2) + File libraryFile2 = new File(ProvisioningInstallTask.PROVISIONING_DIR, name2) when: - provisioningInstallTask.install() + subject.download() then: - project.xcodebuild.signing.mobileProvisionFile.size == 1 - project.xcodebuild.signing.mobileProvisionFile[0].toString().endsWith(uuid + ".mobileprovision") - } + noExceptionThrown() - def "has provisionprofile extension"() { - given: - File testMobileprovision = new File("../plugin/src/test/Resource/test-wildcard-mac.provisionprofile") - project.xcodebuild.signing.mobileProvisionURI = testMobileprovision.toURI().toString() + and: + downloadedFile1.text == testMobileProvision1.text + downloadedFile1.exists() - ProvisioningProfileReader provisioningProfileIdReader = new ProvisioningProfileReader(testMobileprovision, commandRunner) - String uuid = provisioningProfileIdReader.getUUID() + libraryFile1.text == testMobileProvision1.text + libraryFile1.exists() - when: - provisioningInstallTask.install() + and: + downloadedFile2.text == testMobileProvision2.text + downloadedFile2.exists() - then: - project.xcodebuild.signing.mobileProvisionFile.size == 1 - project.xcodebuild.signing.mobileProvisionFile[0].toString().endsWith(uuid + ".provisionprofile") + libraryFile2.text == testMobileProvision2.text + libraryFile2.exists() } } diff --git a/plugin/src/test/groovy/org/openbakery/signing/ProvisioningProfileReaderSpecification.groovy b/plugin/src/test/groovy/org/openbakery/signing/ProvisioningProfileReaderSpecification.groovy index 04be076b..6b7644c7 100644 --- a/plugin/src/test/groovy/org/openbakery/signing/ProvisioningProfileReaderSpecification.groovy +++ b/plugin/src/test/groovy/org/openbakery/signing/ProvisioningProfileReaderSpecification.groovy @@ -1,53 +1,55 @@ package org.openbakery.signing -import ch.qos.logback.core.util.FileUtil import org.apache.commons.configuration.plist.XMLPropertyListConfiguration import org.apache.commons.io.FileUtils import org.gradle.api.Project import org.gradle.testfixtures.ProjectBuilder import org.openbakery.CommandRunner +import org.openbakery.XcodeBuildPluginExtension +import org.openbakery.XcodePlugin import org.openbakery.codesign.ProvisioningProfileReader -import org.openbakery.configuration.Configuration import org.openbakery.configuration.ConfigurationFromMap import org.openbakery.configuration.ConfigurationFromPlist -import org.openbakery.xcode.Type -import org.openbakery.XcodePlugin -import org.openbakery.packaging.PackageTask +import org.openbakery.packaging.PackageLegacyTask import org.openbakery.util.PlistHelper +import org.openbakery.xcode.Type import spock.lang.Specification import static org.hamcrest.MatcherAssert.assertThat import static org.hamcrest.Matchers.equalTo import static org.hamcrest.Matchers.is - class ProvisioningProfileReaderSpecification extends Specification { Project project - PackageTask packageTask; + PackageLegacyTask packageTask; File projectDir File buildOutputDirectory File appDirectory CommandRunner commandRunner = Mock(CommandRunner) PlistHelper plistHelper + XcodeBuildPluginExtension extension def setup() { projectDir = new File(System.getProperty("java.io.tmpdir"), "gradle-xcodebuild") project = ProjectBuilder.builder().withProjectDir(projectDir).build() project.buildDir = new File(projectDir, 'build').absoluteFile - project.apply plugin: org.openbakery.XcodePlugin + project.apply plugin: XcodePlugin project.xcodebuild.infoPlist = 'Info.plist' project.xcodebuild.productName = 'Example' project.xcodebuild.productType = 'app' project.xcodebuild.type = Type.macOS - project.xcodebuild.signing.keychain = "/var/tmp/gradle.keychain" + project.xcodebuild.signing.keychain.set(project.file("/var/tmp/gradle.keychain")) + + packageTask = project.getTasks().getByPath(PackageLegacyTask.NAME) - packageTask = project.getTasks().getByPath(XcodePlugin.PACKAGE_TASK_NAME) + extension = project.extensions.findByType(XcodeBuildPluginExtension) - buildOutputDirectory = new File(project.xcodebuild.symRoot, project.xcodebuild.configuration) + + buildOutputDirectory = new File(extension.symRoot.asFile.get(), extension.configuration) buildOutputDirectory.mkdirs() appDirectory = new File(buildOutputDirectory, "Example.app") @@ -74,20 +76,20 @@ class ProvisioningProfileReaderSpecification extends Specification { def "read application identifier prefix"() { when: ProvisioningProfileReader reader = new ProvisioningProfileReader(new File("../libtest/src/main/Resource/test.mobileprovision"), new CommandRunner()) + then: reader.getApplicationIdentifierPrefix().equals("AAAAAAAAAAA") } - def "read application identifier"() { when: ProvisioningProfileReader reader = new ProvisioningProfileReader(new File("../libtest/src/main/Resource/test.mobileprovision"), new CommandRunner()) + then: reader.getApplicationIdentifier() == "org.openbakery.test.Example" } - - def "profile has expired" () { + def "profile has expired"() { when: new ProvisioningProfileReader(new File("src/test/Resource/expired.mobileprovision"), new CommandRunner()) @@ -96,19 +98,15 @@ class ProvisioningProfileReaderSpecification extends Specification { } def "read Mac Provisioning Profile"() { - given: File wildcardMacProfile = new File("src/test/Resource/test-wildcard-mac-development.provisionprofile") when: ProvisioningProfileReader provisioningProfileReader = new ProvisioningProfileReaderIgnoreExpired(wildcardMacProfile, new CommandRunner()) - - def applicationIdentifier = provisioningProfileReader.getApplicationIdentifier() + String applicationIdentifier = provisioningProfileReader.getApplicationIdentifier() then: - assertThat(applicationIdentifier, is(equalTo("*"))) - } def "extract Entitlements has nothing to extract"() { @@ -116,19 +114,17 @@ class ProvisioningProfileReaderSpecification extends Specification { ProvisioningProfileReader reader = new ProvisioningProfileReaderIgnoreExpired(provisioningProfile, commandRunner, new PlistHelper(new CommandRunner())) commandRunner.runWithResult([ - "/usr/libexec/PlistBuddy", - "-x", - provisioningProfile.absolutePath, - "-c", - "Print Entitlements"]) >> null + "/usr/libexec/PlistBuddy", + "-x", + provisioningProfile.absolutePath, + "-c", + "Print Entitlements"]) >> null File entitlementsFile = new File(projectDir, "entitlements.plist") expect: // no exception should be thrown! reader.extractEntitlements(entitlementsFile, "org.openbakery.test.Example", null, null) - - } def "extract Entitlements"() { @@ -186,7 +182,6 @@ class ProvisioningProfileReaderSpecification extends Specification { entitlements.getString("com..apple..application-identifier") == "Z7L2YCUH45.org.openbakery.test.Example" } - def "extract Entitlements with keychain access group"() { given: String expectedContents = "\n" + @@ -262,7 +257,6 @@ class ProvisioningProfileReaderSpecification extends Specification { entitlements.getList("keychain-access-groups").contains("AAAAAAAAAA.com.example.Test") } - def "extract Entitlements test application identifier"() { given: File mobileprovision = new File("src/test/Resource/openbakery.mobileprovision") @@ -287,28 +281,27 @@ class ProvisioningProfileReaderSpecification extends Specification { entitlementsFile.text.contains("CCCCCCCCCC.com.example.Test") } - String getEntitlementWithApplicationIdentifier(String applicationIdentifier) { return "\n" + - "\n" + - "\n" + - "\n" + - " keychain-access-groups\n" + - " \n" + - " AAAAAAAAAA.*\n" + - " \n" + - " get-task-allow\n" + - " \n" + - " application-identifier\n" + - " " + applicationIdentifier + "\n" + - " com.apple.developer.team-identifier\n" + - " AAAAAAAAAA\n" + - " com.apple.developer.ubiquity-kvstore-identifier\n" + - " ABCDE12345.*\n" + - " com.apple.developer.ubiquity-container-identifiers\n" + - " ABCDE12345.*\n" + - "\n" + - "" + "\n" + + "\n" + + "\n" + + " keychain-access-groups\n" + + " \n" + + " AAAAAAAAAA.*\n" + + " \n" + + " get-task-allow\n" + + " \n" + + " application-identifier\n" + + " " + applicationIdentifier + "\n" + + " com.apple.developer.team-identifier\n" + + " AAAAAAAAAA\n" + + " com.apple.developer.ubiquity-kvstore-identifier\n" + + " ABCDE12345.*\n" + + " com.apple.developer.ubiquity-container-identifiers\n" + + " ABCDE12345.*\n" + + "\n" + + "" } def "extract Entitlements with wildcard application identifier that does not match"() { @@ -321,7 +314,7 @@ class ProvisioningProfileReaderSpecification extends Specification { ProvisioningProfileReader reader = new ProvisioningProfileReader(mobileprovision, commandRunner) def keychainAccessGroups = [ - ProvisioningProfileReader.APPLICATION_IDENTIFIER_PREFIX + "org.openbakery.test.Example", + ProvisioningProfileReader.APPLICATION_IDENTIFIER_PREFIX + "org.openbakery.test.Example", ] File entitlementsFile = new File(projectDir, "entitlements.plist") @@ -331,7 +324,6 @@ class ProvisioningProfileReaderSpecification extends Specification { thrown(IllegalStateException.class) } - def "extract Entitlements with wildcard application identifier"() { given: File mobileprovision = new File("src/test/Resource/openbakery.mobileprovision") @@ -342,7 +334,7 @@ class ProvisioningProfileReaderSpecification extends Specification { ProvisioningProfileReader reader = new ProvisioningProfileReader(mobileprovision, commandRunner, new PlistHelper(new CommandRunner())) def keychainAccessGroups = [ - ProvisioningProfileReader.APPLICATION_IDENTIFIER_PREFIX + "org.openbakery.test.Example", + ProvisioningProfileReader.APPLICATION_IDENTIFIER_PREFIX + "org.openbakery.test.Example", ] File entitlementsFile = new File(projectDir, "entitlements.plist") @@ -355,8 +347,6 @@ class ProvisioningProfileReaderSpecification extends Specification { } - - def "extract Entitlements with wildcard application identifier that does match"() { given: File mobileprovision = new File("src/test/Resource/openbakery.mobileprovision") @@ -367,7 +357,7 @@ class ProvisioningProfileReaderSpecification extends Specification { ProvisioningProfileReader reader = new ProvisioningProfileReader(mobileprovision, commandRunner, new PlistHelper(new CommandRunner())) def keychainAccessGroups = [ - ProvisioningProfileReader.APPLICATION_IDENTIFIER_PREFIX + "org.openbakery.test.Example", + ProvisioningProfileReader.APPLICATION_IDENTIFIER_PREFIX + "org.openbakery.test.Example", ] File entitlementsFile = new File(projectDir, "entitlements.plist") @@ -378,7 +368,6 @@ class ProvisioningProfileReaderSpecification extends Specification { entitlementsFile.text.contains("AAAAAAAAAAA.org.openbakery.test.Example.widget") } - def "is ad-hoc profile"() { when: ProvisioningProfileReader reader = new ProvisioningProfileReader(new File("../libtest/src/main/Resource/test.mobileprovision"), new CommandRunner()) @@ -387,7 +376,6 @@ class ProvisioningProfileReaderSpecification extends Specification { reader.isAdHoc() == true } - def "is not ad-hoc profile"() { when: ProvisioningProfileReader reader = new ProvisioningProfileReader(new File("../libtest/src/main/Resource/Appstore.mobileprovision"), new CommandRunner()) @@ -396,7 +384,6 @@ class ProvisioningProfileReaderSpecification extends Specification { reader.isAdHoc() == false } - def "extract Entitlements with wildcard and kvstore should start with team id"() { given: File mobileprovision = new File("src/test/Resource/openbakery-team.mobileprovision") @@ -407,7 +394,7 @@ class ProvisioningProfileReaderSpecification extends Specification { ProvisioningProfileReader reader = new ProvisioningProfileReader(mobileprovision, commandRunner, new PlistHelper(new CommandRunner())) def keychainAccessGroups = [ - ProvisioningProfileReader.APPLICATION_IDENTIFIER_PREFIX + "org.openbakery.test.Example", + ProvisioningProfileReader.APPLICATION_IDENTIFIER_PREFIX + "org.openbakery.test.Example", ] File entitlementsFile = new File(projectDir, "entitlements.plist") @@ -428,7 +415,7 @@ class ProvisioningProfileReaderSpecification extends Specification { ProvisioningProfileReader reader = new ProvisioningProfileReader(mobileprovision, commandRunner, new PlistHelper(new CommandRunner())) def keychainAccessGroups = [ - ProvisioningProfileReader.APPLICATION_IDENTIFIER_PREFIX + "org.openbakery.test.Example", + ProvisioningProfileReader.APPLICATION_IDENTIFIER_PREFIX + "org.openbakery.test.Example", ] File entitlementsFile = new File(projectDir, "entitlements.plist") @@ -439,7 +426,6 @@ class ProvisioningProfileReaderSpecification extends Specification { plistHelper.getValueFromPlist(entitlementsFile, "com.apple.developer.ubiquity-container-identifiers").startsWith("XXXXXZZZZZ.") } - def "get provisioning profile from plist"() { def commandList @@ -453,12 +439,11 @@ class ProvisioningProfileReaderSpecification extends Specification { then: 1 * commandRunner.run(_) >> { arguments -> commandList = arguments[0] } - commandList == ["security","cms","-D","-i", mobileprovision.absolutePath, "-o", expectedProvisioningPlist.absolutePath] + commandList == ["security", "cms", "-D", "-i", mobileprovision.absolutePath, "-o", expectedProvisioningPlist.absolutePath] } - def "provisioning match"() { given: File appMobileprovision = new File("../libtest/src/main/Resource/test.mobileprovision") @@ -467,9 +452,9 @@ class ProvisioningProfileReaderSpecification extends Specification { when: def list = [ - appMobileprovision, - widgetMobileprovision, - wildcardMobileprovision + appMobileprovision, + widgetMobileprovision, + wildcardMobileprovision ] then: @@ -480,7 +465,6 @@ class ProvisioningProfileReaderSpecification extends Specification { } - def "provisioning Match more"() { given: File appMobileprovision = new File("src/test/Resource/openbakery.mobileprovision") @@ -488,8 +472,8 @@ class ProvisioningProfileReaderSpecification extends Specification { when: def list = [ - appMobileprovision, - wildcardMobileprovision + appMobileprovision, + wildcardMobileprovision ] then: @@ -499,7 +483,6 @@ class ProvisioningProfileReaderSpecification extends Specification { } - def "extract Entitlements and merge Example.entitlements"() { given: File mobileprovision = new File("src/test/Resource/openbakery.mobileprovision") @@ -523,7 +506,6 @@ class ProvisioningProfileReaderSpecification extends Specification { } - def setupForEntitlementTest(Map data) { File mobileprovision = new File("src/test/Resource/openbakery.mobileprovision") @@ -539,7 +521,6 @@ class ProvisioningProfileReaderSpecification extends Specification { return destinationFile } - def "extract Entitlements and replace com.apple.developer.icloud-container-identifiers"() { given: Map data = ["com.apple.developer.icloud-container-identifiers": ["iCloud.com.example.Test"]] @@ -553,7 +534,6 @@ class ProvisioningProfileReaderSpecification extends Specification { entitlements.getList("com..apple..developer..icloud-container-identifiers").contains("iCloud.com.example.Test") } - def "extract Entitlements and replace ubiquity-container-identifiers"() { given: Map data = ["com.apple.developer.ubiquity-container-identifiers": ["com.example.Test"]] @@ -623,11 +603,10 @@ class ProvisioningProfileReaderSpecification extends Specification { entitlements.getString("com..apple..developer..icloud-services") == "com.example.test" } - def "extract Entitlements set keychain access group with configuration key and replace AppIdentiferPrefix variable"() { given: Map data = [ - "keychain-access-groups": [ "\$(AppIdentifierPrefix)com.example.Test" ] + "keychain-access-groups": ["\$(AppIdentifierPrefix)com.example.Test"] ] File entitlementsFile = setupForEntitlementTest(data) @@ -640,7 +619,5 @@ class ProvisioningProfileReaderSpecification extends Specification { entitlements.getList("keychain-access-groups").contains("AAAAAAAAAAA.com.example.Test") } - - } diff --git a/plugin/src/test/groovy/org/openbakery/signing/SigningSpecification.groovy b/plugin/src/test/groovy/org/openbakery/signing/SigningSpecification.groovy index 9ad5759e..66dbb2db 100644 --- a/plugin/src/test/groovy/org/openbakery/signing/SigningSpecification.groovy +++ b/plugin/src/test/groovy/org/openbakery/signing/SigningSpecification.groovy @@ -1,142 +1,228 @@ package org.openbakery.signing -import org.apache.commons.io.FileUtils import org.gradle.api.Project import org.gradle.testfixtures.ProjectBuilder +import org.junit.Rule +import org.junit.rules.ExpectedException +import org.junit.rules.TemporaryFolder +import org.openbakery.CommandRunner +import org.openbakery.XcodeBuildPluginExtension +import org.openbakery.XcodePlugin import org.openbakery.codesign.CodesignParameters import org.openbakery.configuration.ConfigurationFromMap +import org.openbakery.extension.Signing import spock.lang.Specification +import spock.lang.Unroll class SigningSpecification extends Specification { - Signing signing - Project project - File projectDir + Signing signing + Project project + File projectDir + + @Rule + final TemporaryFolder testProjectDir = new TemporaryFolder() + + @Rule + public ExpectedException exception = ExpectedException.none() + + private Signing signingExtension + + def setup() { + projectDir = testProjectDir.root + + project = ProjectBuilder.builder().withProjectDir(projectDir).build() + project.buildDir = new File(projectDir, 'build').absoluteFile + project.apply plugin: XcodePlugin + + signing = new Signing(project, new CommandRunner()) + this.signingExtension = project.extensions + .findByType(XcodeBuildPluginExtension) + .signing + } - def setup() { - projectDir = new File(System.getProperty("java.io.tmpdir"), "gradle-xcodebuild") + def cleanup() { + projectDir.delete() + } - project = ProjectBuilder.builder().withProjectDir(projectDir).build() - project.buildDir = new File(projectDir, 'build').absoluteFile - project.apply plugin: org.openbakery.XcodePlugin - signing = new Signing(project) - } + def "has identity"() { + when: + signing.identity = "Me!" + then: + signing.identity == "Me!" + } + @Unroll + def "the signing method can be resolved from a valid string value for method: #method"() { + when: + signing.setMethod(string) + + then: + noExceptionThrown() + signing.signingMethod.get() == method - def cleanup() { - projectDir.delete() - } + where: + string | method + "ad-hoc" | SigningMethod.AdHoc + "app-store" | SigningMethod.AppStore + "development" | SigningMethod.Dev + "enterprise" | SigningMethod.Enterprise + } - def "has identity"() { - when: - signing.identity = "Me!" - then: - signing.identity == "Me!" - } + def "Should throw an expection when trying to parse an invalid signature method"() { + when: + signing.setMethod(string) - def "entitlements data set via closure using xcodebuild"() { + then: + thrown(IllegalArgumentException.class) - when: - project.xcodebuild.signing { - entitlements 'com.apple.security.application-groups': ['group.com.example.App'] - } + where: + string | _ + "ad-hc" | _ + "app-stre" | _ + null | _ + } + + def "The XcConfig file path can be configured and is by default buildDir dependant"() { + when: + File file = signing.xcConfigFile.asFile.get() + + then: + noExceptionThrown() + file.absoluteFile == new File(project.buildDir, "archive/archive.xcconfig") - then: - project.xcodebuild.signing.entitlements instanceof Map - project.xcodebuild.signing.entitlements.containsKey("com.apple.security.application-groups") + when: + File altBuildDir = testProjectDir.newFolder("build-dir-alt") + assert project.buildDir != altBuildDir + project.buildDir = altBuildDir - } + and: + File file2 = signing.xcConfigFile.asFile.get() - def "entitlements data set via closure"() { - when: - signing.entitlements('com.apple.security.application-groups': ['group.com.example.App']) + then: + noExceptionThrown() + file2.absoluteFile == new File(altBuildDir, "archive/archive.xcconfig") + } - then: - signing.entitlements instanceof Map - signing.entitlements.containsKey("com.apple.security.application-groups") - signing.entitlements["com.apple.security.application-groups"] == ['group.com.example.App'] - } + def "entitlements data set via closure using xcodebuild"() { - - def "entitlements data set via closure converted to Configuration"() { - when: - signing.entitlements('com.apple.security.application-groups': ['group.com.example.App']) - - def configuration = new ConfigurationFromMap(signing.entitlements) - - then: - configuration.getStringArray("com.apple.security.application-groups") == ['group.com.example.App'] - - } - - def "codesignParameters is not null"() { - when: - signing.identity = "Me" - - then: - signing.codesignParameters instanceof CodesignParameters - } - - def "codesignParameters has identity"() { - when: - signing.identity = "Me" - then: - signing.codesignParameters.signingIdentity == "Me" - } - - def "codesignParameters has mobileProvisionFiles"() { - when: - - File first = new File(projectDir, "first") - FileUtils.write(first, "first") - File second = new File(projectDir, "second") - FileUtils.write(second, "second") - - signing.addMobileProvisionFile(first) - signing.addMobileProvisionFile(second) - - then: - signing.codesignParameters.mobileProvisionFiles instanceof List - signing.codesignParameters.mobileProvisionFiles == [first, second] - } - - def "codesignParameters has keychain"() { - when: - signing.keychain = new File("my.keychain").absoluteFile - then: - signing.codesignParameters.keychain == new File("my.keychain").absoluteFile - } - - - def "codesignParameters has entitlements"() { - when: - signing.entitlements = ['key': 'value'] - then: - signing.codesignParameters.entitlements == ['key': 'value'] - } - - def "codesignParameters has entitlementsFile"() { - when: - signing.entitlementsFile = new File("entitlements") - then: - signing.codesignParameters.entitlementsFile == new File("entitlements") - } - - - def "codesignParameter entitlementsFile as String"() { - when: - signing.entitlementsFile = "entitlements" - then: - signing.codesignParameters.entitlementsFile instanceof File - signing.codesignParameters.entitlementsFile.path.endsWith("entitlements") - } - - def "codesignParameter entitlementsFile as String full path"() { - when: - signing.entitlementsFile = "file:///entitlements" - then: - signing.codesignParameters.entitlementsFile instanceof File - signing.codesignParameters.entitlementsFile.path.endsWith("entitlements") - } + when: + assert signingExtension != null + signingExtension.entitlements 'com.apple.security.application-groups': ['group.com.example.App'] + + then: + signingExtension.entitlementsMap.get() instanceof Map + signingExtension.entitlementsMap.get().containsKey("com.apple.security.application-groups") + } + + def "entitlements data set via closure"() { + when: + assert signingExtension != null + signingExtension.entitlements('com.apple.security.application-groups': ['group.com.example.App']) + + then: + signingExtension.entitlementsMap.get() instanceof Map + signingExtension.entitlementsMap.get().containsKey("com.apple.security.application-groups") + signingExtension.entitlementsMap.get()["com.apple.security.application-groups"] == ['group.com.example.App'] + } + + def "entitlements data set via closure converted to Configuration"() { + when: + signing.entitlements('com.apple.security.application-groups': ['group.com.example.App']) + + def configuration = new ConfigurationFromMap(signing.entitlementsMap.get()) + + then: + configuration.getStringArray("com.apple.security.application-groups") == ['group.com.example.App'] + } + + def "codesignParameters is not null"() { + when: + signing.identity = "Me" + + then: + signing.codesignParameters instanceof CodesignParameters + } + + def "codesignParameters has identity"() { + when: + signing.identity = "Me" + + then: + signing.codesignParameters.signingIdentity == "Me" + } + + def "codesignParameters has keychain"() { + when: + signing.keychain = new File("my.keychain").absoluteFile + then: + signing.codesignParameters.keychain == new File("my.keychain").absoluteFile + } + + + def "codesignParameters has entitlements"() { + when: + signing.entitlements(['key': 'value']) + + then: + signing.codesignParameters.entitlements == ['key': 'value'] + } + + def "codesignParameters has entitlementsFile"() { + setup: + File entitlementsFile = testProjectDir.newFile("test.entitlements") + + when: + signing.entitlementsFile = entitlementsFile + + then: + signing.codesignParameters.entitlementsFile == entitlementsFile + } + + def "When defining the certificate the friendlyName should be updated"() { + setup: + File cert = new File("src/test/Resource/fake_distribution.p12") + assert cert.exists() + signing.certificate.set(cert.absoluteFile) + + when: "If not password is defined, should trow an exception" + signing.certificateFriendlyName.get() + + then: + thrown IllegalStateException.class + + when: "Defining password" + signing.certificatePassword.set("p4ssword") + + and: + signing.certificateFriendlyName.get() == "iPhone Distribution: Test Company Name (12345ABCDE)" + + then: + noExceptionThrown() + } + + def "codesignParameter entitlementsFile as String"() { + when: + signing.entitlementsFile = "entitlements/test.entitlements" + + then: + noExceptionThrown() + with(signing.entitlementsFile + .get() + .asFile) { + name == "test.entitlements" + parent.endsWith("/entitlements") + } + } + + def "codesignParameter entitlementsFile as String full path"() { + when: + signing.entitlementsFile = "file:///entitlements" + + then: + noExceptionThrown() + signing.codesignParameters.entitlementsFile instanceof File + signing.codesignParameters.entitlementsFile.path.endsWith("entitlements") + } } diff --git a/plugin/src/test/groovy/org/openbakery/simulators/SimulatorInstallAppTaskSpecification.groovy b/plugin/src/test/groovy/org/openbakery/simulators/SimulatorInstallAppTaskSpecification.groovy index 9e675274..37a4c610 100644 --- a/plugin/src/test/groovy/org/openbakery/simulators/SimulatorInstallAppTaskSpecification.groovy +++ b/plugin/src/test/groovy/org/openbakery/simulators/SimulatorInstallAppTaskSpecification.groovy @@ -7,25 +7,20 @@ import org.openbakery.codesign.Codesign import org.openbakery.xcode.Type import spock.lang.Specification -/** - * Created by rene on 27.03.17. - */ class SimulatorInstallAppTaskSpecification extends Specification { - SimulatorInstallAppTask task Project project File projectDir def setup() { - projectDir = new File(System.getProperty("java.io.tmpdir"), "gradle-xcodebuild") project = ProjectBuilder.builder().withProjectDir(projectDir).build() - project.apply plugin: org.openbakery.XcodePlugin + project.apply plugin: XcodePlugin task = project.tasks.findByName(XcodePlugin.SIMULATORS_INSTALL_APP_TASK_NAME) - } + def "create"() { expect: task instanceof SimulatorInstallAppTask @@ -34,7 +29,8 @@ class SimulatorInstallAppTaskSpecification extends Specification { def "depends on"() { when: - def dependsOn = task.getDependsOn() + def dependsOn = task.getDependsOn() + then: dependsOn.size() == 2 dependsOn.contains(XcodePlugin.XCODE_BUILD_TASK_NAME) @@ -53,7 +49,9 @@ class SimulatorInstallAppTaskSpecification extends Specification { task.run() then: - 1* simulatorControl.simctl(["install", "booted", project.xcodebuild.applicationBundle.absolutePath]) + 1 * simulatorControl.simctl(["install", + "booted", + project.xcodebuild.applicationBundle.absolutePath]) } def "codesign is not null"() { @@ -77,8 +75,7 @@ class SimulatorInstallAppTaskSpecification extends Specification { Codesign codesign = Mock(Codesign) task.codesign = codesign - SimulatorControl simulatorControl = Mock(SimulatorControl) - task.simulatorControl = simulatorControl + task.simulatorControl = Mock(SimulatorControl) project.xcodebuild.bundleName = "MyApp" @@ -88,7 +85,4 @@ class SimulatorInstallAppTaskSpecification extends Specification { then: 1 * codesign.sign(project.xcodebuild.applicationBundle) } - - - } diff --git a/plugin/src/test/groovy/org/openbakery/simulators/SimulatorStartTaskSpecification.groovy b/plugin/src/test/groovy/org/openbakery/simulators/SimulatorStartTaskSpecification.groovy index 6ad50d8b..06fd1e90 100644 --- a/plugin/src/test/groovy/org/openbakery/simulators/SimulatorStartTaskSpecification.groovy +++ b/plugin/src/test/groovy/org/openbakery/simulators/SimulatorStartTaskSpecification.groovy @@ -19,29 +19,29 @@ class SimulatorStartTaskSpecification extends Specification { def devices9_1 = [ - new SimulatorDevice("iPhone 4s (8C8C43D3-B53F-4091-8D7C-6A4B38051389) (Shutdown)"), - new SimulatorDevice("iPhone 5 (2EEDED93-1568-4D46-84A2-6E2AE723ECC6) (Shutdown)"), - new SimulatorDevice("iPhone 5s (DE72B92C-D8E2-4CCE-A5E1-9174B6A03209) (Shutdown)"), - new SimulatorDevice("iPhone 6 (CD613910-6F4E-41A3-B0CB-1EBC16449F42) (Shutdown)"), - new SimulatorDevice("iPhone 6 Plus (364A3F52-DED2-4BBD-B40A-B0F3F374E51F) (Shutdown)"), - new SimulatorDevice("iPhone 6s (A3F2EB56-1EA8-4527-8BA3-BEE76AAB6D01) (Shutdown)"), - new SimulatorDevice("iPhone 6s Plus (3EACF222-830E-4BF1-ADB3-75E98AB99480) (Shutdown)"), - new SimulatorDevice("iPad 2 (D72F7CC6-8426-4E0A-A234-34747B1F30DD) (Shutdown)"), - new SimulatorDevice("iPad Retina (DAE7925F-8FC7-42B0-A1F0-7173C3F40114) (Shutdown)"), - new SimulatorDevice("iPad Air (4F432AFB-370C-4741-B7D7-803F3E223C36) (Shutdown)"), - new SimulatorDevice("iPad Air 2 (8064C333-D8F0-43A7-83B4-DFA79071A870) (Shutdown)"), - new SimulatorDevice("iPad Pro (744F7B28-373D-4666-B4DF-8438D1109663) (Shutdown)") + new SimulatorDevice("iPhone 4s (8C8C43D3-B53F-4091-8D7C-6A4B38051389) (Shutdown)"), + new SimulatorDevice("iPhone 5 (2EEDED93-1568-4D46-84A2-6E2AE723ECC6) (Shutdown)"), + new SimulatorDevice("iPhone 5s (DE72B92C-D8E2-4CCE-A5E1-9174B6A03209) (Shutdown)"), + new SimulatorDevice("iPhone 6 (CD613910-6F4E-41A3-B0CB-1EBC16449F42) (Shutdown)"), + new SimulatorDevice("iPhone 6 Plus (364A3F52-DED2-4BBD-B40A-B0F3F374E51F) (Shutdown)"), + new SimulatorDevice("iPhone 6s (A3F2EB56-1EA8-4527-8BA3-BEE76AAB6D01) (Shutdown)"), + new SimulatorDevice("iPhone 6s Plus (3EACF222-830E-4BF1-ADB3-75E98AB99480) (Shutdown)"), + new SimulatorDevice("iPad 2 (D72F7CC6-8426-4E0A-A234-34747B1F30DD) (Shutdown)"), + new SimulatorDevice("iPad Retina (DAE7925F-8FC7-42B0-A1F0-7173C3F40114) (Shutdown)"), + new SimulatorDevice("iPad Air (4F432AFB-370C-4741-B7D7-803F3E223C36) (Shutdown)"), + new SimulatorDevice("iPad Air 2 (8064C333-D8F0-43A7-83B4-DFA79071A870) (Shutdown)"), + new SimulatorDevice("iPad Pro (744F7B28-373D-4666-B4DF-8438D1109663) (Shutdown)") ] def destinations = [ - new Destination("iOS Simulator", "iPhone 4s", "iOS 9"), + new Destination("iOS Simulator", "iPhone 4s", "iOS 9"), ] def setup() { projectDir = new File(System.getProperty("java.io.tmpdir"), "gradle-xcodebuild") project = ProjectBuilder.builder().withProjectDir(projectDir).build() - project.apply plugin:org.openbakery.XcodePlugin + project.apply plugin: org.openbakery.XcodePlugin projectDir.mkdirs() @@ -65,7 +65,7 @@ class SimulatorStartTaskSpecification extends Specification { def "run"() { given: - simulatorControl.getDevice(_) >> devices9_1[0] + simulatorControl.getDevice(_) >> Optional.ofNullable(devices9_1[0]) destinationResolver.getDestinations(_) >> destinations when: @@ -73,9 +73,9 @@ class SimulatorStartTaskSpecification extends Specification { then: - 1 *simulatorControl.killAll() - 1 *simulatorControl.runDevice(devices9_1[0]) - 1 *simulatorControl.waitForDevice(devices9_1[0]) + 1 * simulatorControl.killAll() + 1 * simulatorControl.runDevice(devices9_1[0]) + 1 * simulatorControl.waitForDevice(devices9_1[0]) } @@ -93,9 +93,9 @@ class SimulatorStartTaskSpecification extends Specification { task.run() then: - 1 *simulatorControl.getDevice((Destination)_) >> { arguments -> + 1 * simulatorControl.getDevice((Destination) _) >> { arguments -> destination = arguments[0] - return devices9_1[0] + return Optional.of(devices9_1[0]) } destination != null destination.name == 'iPhone 6s' diff --git a/plugin/src/test/groovy/org/openbakery/util/SystemUtilTest.groovy b/plugin/src/test/groovy/org/openbakery/util/SystemUtilTest.groovy new file mode 100644 index 00000000..48287c9d --- /dev/null +++ b/plugin/src/test/groovy/org/openbakery/util/SystemUtilTest.groovy @@ -0,0 +1,28 @@ +package org.openbakery.util + +import org.openbakery.xcode.Version +import spock.lang.Specification + +class SystemUtilTest extends Specification { + def "Should properly resolve system version from system property"() { + + setup: + System.setProperty("os.version", systemVersion); + + when: + Version version = SystemUtil.getOsVersion() + + then: + version.major == major + version.minor == minor + version.maintenance == maintenance + + where: + systemVersion | major | minor | maintenance + "0.0" | 0 | 0 | -1 + "10.3" | 10 | 3 | -1 + "10.0.1" | 10 | 0 | 1 + "10.1.1" | 10 | 1 | 1 + "10.8.1" | 10 | 8 | 1 + } +} diff --git a/plugin/src/test/groovy/org/openbakery/xcode/DestinationResolverSpecification.groovy b/plugin/src/test/groovy/org/openbakery/xcode/DestinationResolverSpecification.groovy index 948d0704..f1410eac 100644 --- a/plugin/src/test/groovy/org/openbakery/xcode/DestinationResolverSpecification.groovy +++ b/plugin/src/test/groovy/org/openbakery/xcode/DestinationResolverSpecification.groovy @@ -2,13 +2,24 @@ package org.openbakery.xcode import org.gradle.api.Project import org.gradle.testfixtures.ProjectBuilder +import org.junit.Rule +import org.junit.rules.ExpectedException +import org.junit.rules.TemporaryFolder +import org.openbakery.CommandRunner import org.openbakery.XcodeBuildPluginExtension +import org.openbakery.XcodePlugin import org.openbakery.simulators.SimulatorControl import org.openbakery.testdouble.SimulatorControlStub import spock.lang.Specification class DestinationResolverSpecification extends Specification { + @Rule + public ExpectedException exception = ExpectedException.none() + + @Rule + final TemporaryFolder testProjectDir = new TemporaryFolder() + Project project File projectDir XcodeBuildPluginExtension extension @@ -16,18 +27,14 @@ class DestinationResolverSpecification extends Specification { SimulatorControl simulatorControl def setup() { - projectDir = new File(System.getProperty("java.io.tmpdir"), "gradle-xcodebuild") - project = ProjectBuilder.builder().withProjectDir(projectDir).build() - project.apply plugin: org.openbakery.XcodePlugin - extension = new XcodeBuildPluginExtension(project) + project = ProjectBuilder.builder().withProjectDir(testProjectDir.root).build() + project.apply plugin: XcodePlugin + extension = new XcodeBuildPluginExtension(project, new CommandRunner()) simulatorControl = new SimulatorControlStub("simctl-list-xcode7_1.txt") destinationResolver = new DestinationResolver(simulatorControl) } - - def "available destinations for OS X"() { - when: extension.type = Type.macOS @@ -35,11 +42,10 @@ class DestinationResolverSpecification extends Specification { destinationResolver.getDestinations(extension.getXcodebuildParameters()).size() == 1 } - def "XcodebuildParameters are created with iOS destination"() { when: Project project = ProjectBuilder.builder().build() - extension = new XcodeBuildPluginExtension(project) + extension = new XcodeBuildPluginExtension(project, new CommandRunner()) extension.type = Type.iOS extension.destination = ['iPad 2'] @@ -47,13 +53,10 @@ class DestinationResolverSpecification extends Specification { def destinations = destinationResolver.getDestinations(parameters) then: - destinations.size() == 1 destinations[0].name == "iPad 2" - } - def "test configured devices only should add most recent runtime"() { when: extension.destination = ['iPad 2'] @@ -66,7 +69,6 @@ class DestinationResolverSpecification extends Specification { destinations[0].os == "9.1" } - def "available destinations default xcode 7"() { when: destinationResolver.simulatorControl = new SimulatorControlStub("simctl-list-xcode7.txt"); @@ -77,7 +79,6 @@ class DestinationResolverSpecification extends Specification { } - def "available destinations default"() { when: @@ -85,7 +86,6 @@ class DestinationResolverSpecification extends Specification { then: destinations.size() == 22 - } def "available destinations default for 9.1 SDK"() { @@ -94,7 +94,6 @@ class DestinationResolverSpecification extends Specification { } when: - def destinations = destinationResolver.getDestinations(extension.getXcodebuildParameters()) then: @@ -102,9 +101,7 @@ class DestinationResolverSpecification extends Specification { } - def "available destinations match"() { - extension.destination { platform = 'iOS Simulator' name = 'iPad Air' @@ -116,10 +113,8 @@ class DestinationResolverSpecification extends Specification { then: destinations.size() == 1 - } - def "available destinations not match"() { given: extension.destination { @@ -136,7 +131,6 @@ class DestinationResolverSpecification extends Specification { } - def "available destinations match simple single"() { given: extension.destination = 'iPad Air' @@ -148,7 +142,6 @@ class DestinationResolverSpecification extends Specification { destinations.size() == 1 destinations[0].name == "iPad Air" destinations[0].os == "9.1" - } def "available destinations match simple multiple"() { @@ -161,10 +154,8 @@ class DestinationResolverSpecification extends Specification { then: destinations.size() == 2 - } - def "set destinations twice"() { given: @@ -177,7 +168,6 @@ class DestinationResolverSpecification extends Specification { then: destinations.size() == 2 destinations[1].name == 'iPhone 4s' - } def "resolves tvOS destination from the name"() { @@ -205,7 +195,6 @@ class DestinationResolverSpecification extends Specification { destinations[0].name == "Apple TV 1080p" } - def "resolve iPad Pro (12.9 inch)"() { given: simulatorControl = new SimulatorControlStub("simctl-list-xcode8.txt") @@ -219,8 +208,5 @@ class DestinationResolverSpecification extends Specification { destinations.size() == 1 destinations[0].name == 'iPad Pro (12.9 inch)' destinations[0].id == 'C538D7F8-E581-44FF-9B17-5391F84642FB' - - } - }