Java tutorial
/* * The MIT License * * Copyright (c) 2011 Ray Yamamoto Hilton * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package au.com.rayh; import com.google.common.collect.Lists; import org.apache.commons.lang.StringUtils; import org.jenkinsci.plugins.tokenmacro.MacroEvaluationException; import org.jenkinsci.plugins.tokenmacro.TokenMacro; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.IOException; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Date; import java.util.List; import hudson.EnvVars; import hudson.FilePath; import hudson.Launcher; import hudson.model.AbstractBuild; import hudson.model.BuildListener; import hudson.tasks.Builder; import hudson.util.QuotedStringTokenizer; /** * @author Ray Hilton */ public abstract class AbstractXCodeBuilder extends Builder { /** * @since 1.0 */ public final Boolean cleanBeforeBuild; /** * @since 1.3 */ public final Boolean cleanTestReports; /** * @since 1.0 */ public final String configuration; /** * @since 1.0 */ public final String target; /** * @since 1.0 */ public final String sdk; /** * @since 1.1 */ public final String symRoot; /** * @since 1.2 */ public final String configurationBuildDir; /** * @since 1.0 */ public final String xcodeProjectPath; /** * @since 1.0 */ public final String xcodeProjectFile; /** * @since 1.3 */ public final String xcodebuildArguments; /** * @since 1.2 */ public final String xcodeSchema; /** * @since 1.2 */ public final String xcodeWorkspaceFile; /** * @since 1.0 */ public final String embeddedProfileFile; /** * @since 1.0 */ public final String cfBundleVersionValue; /** * @since 1.0 */ public final String cfBundleShortVersionStringValue; /** * @since 1.0 */ public final Boolean buildIpa; /** * @since 1.0 */ public final Boolean generateArchive; /** * @since 1.5 **/ public final Boolean unlockKeychain; /** * @since 1.4 */ public final String keychainName; /** * @since 1.0 */ public final String keychainPath; /** * @since 1.0 */ public final String keychainPwd; /** * @since 1.3.3 */ public final String codeSigningIdentity; /** * @since 1.4 */ public final Boolean allowFailingBuildResults; /** * @since 1.4 */ public final String ipaName; /** * @since 1.4 */ public final String ipaOutputDirectory; /** * @since 1.4 */ public final Boolean provideApplicationVersion; public final Boolean runTests; public final String testOsVersion; public final String testDevice; public final Boolean resetSimulator; // Fields in config.jelly must match the parameter names in the "DataBoundConstructor" public AbstractXCodeBuilder(Boolean buildIpa, Boolean generateArchive, Boolean cleanBeforeBuild, Boolean cleanTestReports, String configuration, String target, String sdk, String xcodeProjectPath, String xcodeProjectFile, String xcodebuildArguments, String embeddedProfileFile, String cfBundleVersionValue, String cfBundleShortVersionStringValue, Boolean unlockKeychain, String keychainName, String keychainPath, String keychainPwd, String symRoot, String xcodeWorkspaceFile, String xcodeSchema, String configurationBuildDir, String codeSigningIdentity, Boolean allowFailingBuildResults, String ipaName, Boolean provideApplicationVersion, String ipaOutputDirectory, Boolean runTests, String testOsVersion, String testDevice, Boolean resetSimulator) { this.buildIpa = buildIpa; this.generateArchive = generateArchive; this.sdk = sdk; this.target = target; this.cleanBeforeBuild = cleanBeforeBuild; this.cleanTestReports = cleanTestReports; this.configuration = configuration; this.xcodeProjectPath = xcodeProjectPath; this.xcodeProjectFile = xcodeProjectFile; this.xcodebuildArguments = xcodebuildArguments; this.keychainName = keychainName; this.xcodeWorkspaceFile = xcodeWorkspaceFile; this.xcodeSchema = xcodeSchema; this.embeddedProfileFile = embeddedProfileFile; this.codeSigningIdentity = codeSigningIdentity; this.cfBundleVersionValue = cfBundleVersionValue; this.cfBundleShortVersionStringValue = cfBundleShortVersionStringValue; this.unlockKeychain = unlockKeychain; this.keychainPath = keychainPath; this.keychainPwd = keychainPwd; this.symRoot = symRoot; this.configurationBuildDir = configurationBuildDir; this.allowFailingBuildResults = allowFailingBuildResults; this.ipaName = ipaName; this.ipaOutputDirectory = ipaOutputDirectory; this.provideApplicationVersion = provideApplicationVersion; this.runTests = runTests; this.testOsVersion = testOsVersion; this.testDevice = testDevice; this.resetSimulator = resetSimulator; } @Override public boolean perform(AbstractBuild build, Launcher launcher, BuildListener listener) throws InterruptedException, IOException { EnvVars envs = build.getEnvironment(listener); FilePath projectRoot = build.getWorkspace(); // check that the configured tools exist if (!new FilePath(projectRoot.getChannel(), getGlobalConfiguration().getXcodebuildPath()).exists()) { listener.fatalError( Messages.XCodeBuilder_xcodebuildNotFound(getGlobalConfiguration().getXcodebuildPath())); return false; } if (!new FilePath(projectRoot.getChannel(), getGlobalConfiguration().getAgvtoolPath()).exists()) { listener.fatalError(Messages.XCodeBuilder_avgtoolNotFound(getGlobalConfiguration().getAgvtoolPath())); return false; } // Start expanding all string variables in parameters // NOTE: we currently use variable shadowing to avoid having to rewrite all code (and break pull requests), this will be cleaned up at later stage. String configuration = envs.expand(this.configuration); String target = envs.expand(this.target); String sdk = envs.expand(this.sdk); String symRoot = envs.expand(this.symRoot); String configurationBuildDir = envs.expand(this.configurationBuildDir); String xcodeProjectPath = envs.expand(this.xcodeProjectPath); String xcodeProjectFile = envs.expand(this.xcodeProjectFile); String xcodebuildArguments = envs.expand(this.xcodebuildArguments); String xcodeSchema = envs.expand(this.xcodeSchema); String xcodeWorkspaceFile = envs.expand(this.xcodeWorkspaceFile); String embeddedProfileFile = envs.expand(this.embeddedProfileFile); String cfBundleVersionValue = envs.expand(this.cfBundleVersionValue); String cfBundleShortVersionStringValue = envs.expand(this.cfBundleShortVersionStringValue); String codeSigningIdentity = envs.expand(this.codeSigningIdentity); String ipaName = envs.expand(this.ipaName); String ipaOutputDirectory = envs.expand(this.ipaOutputDirectory); // End expanding all string variables in parameters // Set the working directory if (!StringUtils.isEmpty(xcodeProjectPath)) { projectRoot = projectRoot.child(xcodeProjectPath); } listener.getLogger().println(Messages.XCodeBuilder_workingDir(projectRoot)); // Infer as best we can the build platform String buildPlatform = "iphoneos"; if (!StringUtils.isEmpty(sdk)) { if (StringUtils.contains(sdk.toLowerCase(), "iphonesimulator")) { // Building for the simulator buildPlatform = "iphonesimulator"; } } // Set the build directory and the symRoot // String symRootValue = null; if (!StringUtils.isEmpty(symRoot)) { try { // If not empty we use the Token Expansion to replace it // https://wiki.jenkins-ci.org/display/JENKINS/Token+Macro+Plugin symRootValue = TokenMacro.expandAll(build, listener, symRoot).trim(); } catch (MacroEvaluationException e) { listener.error(Messages.XCodeBuilder_symRootMacroError(e.getMessage())); return false; } } String configurationBuildDirValue = null; FilePath buildDirectory; if (!StringUtils.isEmpty(configurationBuildDir)) { try { configurationBuildDirValue = TokenMacro.expandAll(build, listener, configurationBuildDir).trim(); } catch (MacroEvaluationException e) { listener.error(Messages.XCodeBuilder_configurationBuildDirMacroError(e.getMessage())); return false; } } if (configurationBuildDirValue != null) { // If there is a CONFIGURATION_BUILD_DIR, that overrides any use of SYMROOT. Does not require the build platform and the configuration. buildDirectory = new FilePath(projectRoot.getChannel(), configurationBuildDirValue); } else if (symRootValue != null) { // If there is a SYMROOT specified, compute the build directory from that. buildDirectory = new FilePath(projectRoot.getChannel(), symRootValue) .child(configuration + "-" + buildPlatform); } else { // Assume its a build for the handset, not the simulator. buildDirectory = projectRoot.child("build").child(configuration + "-" + buildPlatform); } // XCode Version int returnCode = launcher.launch().envs(envs).cmds(getGlobalConfiguration().getXcodebuildPath(), "-version") .stdout(listener).pwd(projectRoot).join(); if (returnCode > 0) { listener.fatalError(Messages.XCodeBuilder_xcodeVersionNotFound()); return false; // We fail the build if XCode isn't deployed } ByteArrayOutputStream output = new ByteArrayOutputStream(); // Try to read CFBundleShortVersionString from project listener.getLogger().println(Messages.XCodeBuilder_fetchingCFBundleShortVersionString()); String cfBundleShortVersionString = ""; returnCode = launcher.launch().envs(envs) .cmds(getGlobalConfiguration().getAgvtoolPath(), "mvers", "-terse1").stdout(output).pwd(projectRoot) .join(); // only use this version number if we found it if (returnCode == 0) cfBundleShortVersionString = output.toString().trim(); if (StringUtils.isEmpty(cfBundleShortVersionString)) listener.getLogger().println(Messages.XCodeBuilder_CFBundleShortVersionStringNotFound()); else listener.getLogger() .println(Messages.XCodeBuilder_CFBundleShortVersionStringFound(cfBundleShortVersionString)); listener.getLogger() .println(Messages.XCodeBuilder_CFBundleShortVersionStringValue(cfBundleShortVersionString)); output.reset(); // Try to read CFBundleVersion from project listener.getLogger().println(Messages.XCodeBuilder_fetchingCFBundleVersion()); String cfBundleVersion = ""; returnCode = launcher.launch().envs(envs).cmds(getGlobalConfiguration().getAgvtoolPath(), "vers", "-terse") .stdout(output).pwd(projectRoot).join(); // only use this version number if we found it if (returnCode == 0) cfBundleVersion = output.toString().trim(); if (StringUtils.isEmpty(cfBundleVersion)) listener.getLogger().println(Messages.XCodeBuilder_CFBundleVersionNotFound()); else listener.getLogger().println(Messages.XCodeBuilder_CFBundleVersionFound(cfBundleVersion)); listener.getLogger().println(Messages.XCodeBuilder_CFBundleVersionValue(cfBundleVersion)); String buildDescription = cfBundleShortVersionString + " (" + cfBundleVersion + ")"; XCodeAction a = new XCodeAction(buildDescription); build.addAction(a); // Update the Marketing version (CFBundleShortVersionString) if (!StringUtils.isEmpty(cfBundleShortVersionStringValue)) { try { // If not empty we use the Token Expansion to replace it // https://wiki.jenkins-ci.org/display/JENKINS/Token+Macro+Plugin cfBundleShortVersionString = TokenMacro.expandAll(build, listener, cfBundleShortVersionStringValue); listener.getLogger().println( Messages.XCodeBuilder_CFBundleShortVersionStringUpdate(cfBundleShortVersionString)); returnCode = launcher .launch().envs(envs).cmds(getGlobalConfiguration().getAgvtoolPath(), "new-marketing-version", cfBundleShortVersionString) .stdout(listener).pwd(projectRoot).join(); if (returnCode > 0) { listener.fatalError(Messages .XCodeBuilder_CFBundleShortVersionStringUpdateError(cfBundleShortVersionString)); return false; } } catch (MacroEvaluationException e) { listener.fatalError(Messages.XCodeBuilder_CFBundleShortVersionStringMacroError(e.getMessage())); // Fails the build return false; } } // Update the Technical version (CFBundleVersion) if (!StringUtils.isEmpty(cfBundleVersionValue)) { try { // If not empty we use the Token Expansion to replace it // https://wiki.jenkins-ci.org/display/JENKINS/Token+Macro+Plugin cfBundleVersion = TokenMacro.expandAll(build, listener, cfBundleVersionValue); listener.getLogger().println(Messages.XCodeBuilder_CFBundleVersionUpdate(cfBundleVersion)); returnCode = launcher.launch().envs(envs) .cmds(getGlobalConfiguration().getAgvtoolPath(), "new-version", "-all", cfBundleVersion) .stdout(listener).pwd(projectRoot).join(); if (returnCode > 0) { listener.fatalError(Messages.XCodeBuilder_CFBundleVersionUpdateError(cfBundleVersion)); return false; } } catch (MacroEvaluationException e) { listener.fatalError(Messages.XCodeBuilder_CFBundleVersionMacroError(e.getMessage())); // Fails the build return false; } } listener.getLogger() .println(Messages.XCodeBuilder_CFBundleShortVersionStringUsed(cfBundleShortVersionString)); listener.getLogger().println(Messages.XCodeBuilder_CFBundleVersionUsed(cfBundleVersion)); // Clean build directories if (cleanBeforeBuild) { listener.getLogger() .println(Messages.XCodeBuilder_cleaningBuildDir(buildDirectory.absolutize().getRemote())); buildDirectory.deleteRecursive(); } // remove test-reports and *.ipa if (cleanTestReports != null && cleanTestReports) { listener.getLogger().println(Messages.XCodeBuilder_cleaningTestReportsDir( projectRoot.child("test-reports").absolutize().getRemote())); projectRoot.child("test-reports").deleteRecursive(); } if (unlockKeychain != null && unlockKeychain) { // Let's unlock the keychain Keychain keychain = getKeychain(); if (keychain == null) { listener.fatalError(Messages.XCodeBuilder_keychainNotConfigured()); return false; } String keychainPath = envs.expand(keychain.getKeychainPath()); String keychainPwd = envs.expand(keychain.getKeychainPassword()); launcher.launch().envs(envs).cmds("/usr/bin/security", "list-keychains", "-s", keychainPath) .stdout(listener).pwd(projectRoot).join(); launcher.launch().envs(envs) .cmds("/usr/bin/security", "default-keychain", "-d", "user", "-s", keychainPath) .stdout(listener).pwd(projectRoot).join(); if (StringUtils.isEmpty(keychainPwd)) returnCode = launcher.launch().envs(envs).cmds("/usr/bin/security", "unlock-keychain", keychainPath) .stdout(listener).pwd(projectRoot).join(); else returnCode = launcher.launch().envs(envs) .cmds("/usr/bin/security", "unlock-keychain", "-p", keychainPwd, keychainPath) .masks(false, false, false, true, false).stdout(listener).pwd(projectRoot).join(); if (returnCode > 0) { listener.fatalError(Messages.XCodeBuilder_unlockKeychainFailed()); return false; } // Show the keychain info after unlocking, if not, OS X will prompt for the keychain password launcher.launch().envs(envs).cmds("/usr/bin/security", "show-keychain-info", keychainPath) .stdout(listener).pwd(projectRoot).join(); } // display useful setup information listener.getLogger().println(Messages.XCodeBuilder_DebugInfoLineDelimiter()); listener.getLogger().println(Messages.XCodeBuilder_DebugInfoAvailablePProfiles()); /*returnCode =*/ launcher.launch().envs(envs) .cmds("/usr/bin/security", "find-identity", "-p", "codesigning", "-v").stdout(listener) .pwd(projectRoot).join(); if (!StringUtils.isEmpty(codeSigningIdentity)) { listener.getLogger().println(Messages.XCodeBuilder_DebugInfoCanFindPProfile()); /*returnCode =*/ launcher .launch().envs(envs).cmds("/usr/bin/security", "find-certificate", "-a", "-c", codeSigningIdentity, "-Z", "|", "grep", "^SHA-1") .stdout(listener).pwd(projectRoot).join(); // We could fail here, but this doesn't seem to work as it should right now (output not properly redirected. We might need a parser) } listener.getLogger().println(Messages.XCodeBuilder_DebugInfoAvailableSDKs()); /*returnCode =*/ launcher.launch().envs(envs) .cmds(getGlobalConfiguration().getXcodebuildPath(), "-showsdks").stdout(listener).pwd(projectRoot) .join(); { List<String> commandLine = Lists.newArrayList(getGlobalConfiguration().getXcodebuildPath()); commandLine.add("-list"); // xcodebuild -list -workspace $workspace listener.getLogger().println(Messages.XCodeBuilder_DebugInfoAvailableSchemes()); if (!StringUtils.isEmpty(xcodeWorkspaceFile)) { commandLine.add("-workspace"); commandLine.add(xcodeWorkspaceFile + ".xcworkspace"); } else if (!StringUtils.isEmpty(xcodeProjectFile)) { commandLine.add("-project"); commandLine.add(xcodeProjectFile); } returnCode = launcher.launch().envs(envs).cmds(commandLine).stdout(listener).pwd(projectRoot).join(); if (returnCode > 0) return false; } listener.getLogger().println(Messages.XCodeBuilder_DebugInfoLineDelimiter()); // Build StringBuilder xcodeReport = new StringBuilder(Messages.XCodeBuilder_invokeXcodebuild()); XCodeBuildOutputParser reportGenerator = new JenkinsXCodeBuildOutputParser(projectRoot, listener); List<String> commandLine = Lists.newArrayList(getGlobalConfiguration().getXcodebuildPath()); // Prioritizing schema over target setting if (!StringUtils.isEmpty(xcodeSchema)) { commandLine.add("-scheme"); commandLine.add(xcodeSchema); xcodeReport.append(", scheme: ").append(xcodeSchema); } else if (StringUtils.isEmpty(target)) { commandLine.add("-alltargets"); xcodeReport.append("target: ALL"); } else { commandLine.add("-target"); commandLine.add(target); xcodeReport.append("target: ").append(target); } if (!StringUtils.isEmpty(sdk)) { commandLine.add("-sdk"); commandLine.add(sdk); xcodeReport.append(", sdk: ").append(sdk); } else { xcodeReport.append(", sdk: DEFAULT"); } // Prioritizing workspace over project setting if (!StringUtils.isEmpty(xcodeWorkspaceFile)) { commandLine.add("-workspace"); commandLine.add(xcodeWorkspaceFile + ".xcworkspace"); xcodeReport.append(", workspace: ").append(xcodeWorkspaceFile); } else if (!StringUtils.isEmpty(xcodeProjectFile)) { commandLine.add("-project"); commandLine.add(xcodeProjectFile); xcodeReport.append(", project: ").append(xcodeProjectFile); } else { xcodeReport.append(", project: DEFAULT"); } if (!StringUtils.isEmpty(configuration)) { commandLine.add("-configuration"); commandLine.add(configuration); xcodeReport.append(", configuration: ").append(configuration); } if (runTests) { commandLine.add("-destination"); String destination = "devicetype=iOS Simulator,name="; destination += testDevice; destination += ",OS=" + testOsVersion; commandLine.add(destination); xcodeReport.append("testing for: " + testDevice + ", on " + testOsVersion); } if (cleanBeforeBuild) { commandLine.add("clean"); xcodeReport.append(", clean: YES"); } else { xcodeReport.append(", clean: NO"); } String action = runTests ? "test" : "build"; commandLine.add(action); if (generateArchive != null && generateArchive) { commandLine.add("archive"); xcodeReport.append(", archive:YES"); } else { xcodeReport.append(", archive:NO"); } if (!StringUtils.isEmpty(symRootValue)) { commandLine.add("SYMROOT=" + symRootValue); xcodeReport.append(", symRoot: ").append(symRootValue); } else { xcodeReport.append(", symRoot: DEFAULT"); } // CONFIGURATION_BUILD_DIR if (!StringUtils.isEmpty(configurationBuildDirValue)) { commandLine.add("CONFIGURATION_BUILD_DIR=" + configurationBuildDirValue); xcodeReport.append(", configurationBuildDir: ").append(configurationBuildDirValue); } else { xcodeReport.append(", configurationBuildDir: DEFAULT"); } // handle code signing identities if (!StringUtils.isEmpty(codeSigningIdentity)) { commandLine.add("CODE_SIGN_IDENTITY=" + codeSigningIdentity); xcodeReport.append(", codeSignIdentity: ").append(codeSigningIdentity); } else { xcodeReport.append(", codeSignIdentity: DEFAULT"); } // Additional (custom) xcodebuild arguments if (!StringUtils.isEmpty(xcodebuildArguments)) { commandLine.addAll(splitXcodeBuildArguments(xcodebuildArguments)); } // Reset simulator if requested, by blowing away ~/Library/Application Support/iPhone Simulator/VERSION if (resetSimulator) { String path = System.getProperty("user.home") + "/Library/Application Support/iPhone Simulator/" + testOsVersion; xcodeReport.append("resetting simulator at location: ").append(path); FilePath filePath = new FilePath(new File(path)); if (filePath.exists() && filePath.isDirectory()) { filePath.deleteRecursive(); } } listener.getLogger().println(xcodeReport.toString()); returnCode = launcher.launch().envs(envs).cmds(commandLine).stdout(reportGenerator.getOutputStream()) .pwd(projectRoot).join(); if (allowFailingBuildResults != null && allowFailingBuildResults.booleanValue() == false) { if (reportGenerator.getExitCode() != 0) return false; if (returnCode > 0) return false; } // Package IPA if (buildIpa) { if (!buildDirectory.exists() || !buildDirectory.isDirectory()) { listener.fatalError( Messages.XCodeBuilder_NotExistingBuildDirectory(buildDirectory.absolutize().getRemote())); return false; } // clean IPA FilePath ipaOutputPath = null; if (ipaOutputDirectory != null && !StringUtils.isEmpty(ipaOutputDirectory)) { ipaOutputPath = buildDirectory.child(ipaOutputDirectory); // Create if non-existent if (!ipaOutputPath.exists()) { ipaOutputPath.mkdirs(); } } if (ipaOutputPath == null) { ipaOutputPath = buildDirectory; } listener.getLogger().println(Messages.XCodeBuilder_cleaningIPA()); for (FilePath path : ipaOutputPath.list("*.ipa")) { path.delete(); } listener.getLogger().println(Messages.XCodeBuilder_cleaningDSYM()); for (FilePath path : ipaOutputPath.list("*-dSYM.zip")) { path.delete(); } // packaging IPA listener.getLogger().println(Messages.XCodeBuilder_packagingIPA()); List<FilePath> apps = buildDirectory.list(new AppFileFilter()); // FilePath is based on File.listFiles() which can randomly fail | http://stackoverflow.com/questions/3228147/retrieving-the-underlying-error-when-file-listfiles-return-null if (apps == null) { listener.fatalError( Messages.XCodeBuilder_NoAppsInBuildDirectory(buildDirectory.absolutize().getRemote())); return false; } for (FilePath app : apps) { String version = ""; String shortVersion = ""; if (!provideApplicationVersion) { try { output.reset(); returnCode = launcher.launch().envs(envs) .cmds("/usr/libexec/PlistBuddy", "-c", "Print :CFBundleVersion", app.absolutize().child("Info.plist").getRemote()) .stdout(output).pwd(projectRoot).join(); if (returnCode == 0) { version = output.toString().trim(); } output.reset(); returnCode = launcher.launch().envs(envs) .cmds("/usr/libexec/PlistBuddy", "-c", "Print :CFBundleShortVersionString", app.absolutize().child("Info.plist").getRemote()) .stdout(output).pwd(projectRoot).join(); if (returnCode == 0) { shortVersion = output.toString().trim(); } } catch (Exception ex) { listener.getLogger().println("Failed to get version from Info.plist: " + ex.toString()); return false; } } else { if (!StringUtils.isEmpty(cfBundleVersionValue)) { version = cfBundleVersionValue; } else if (!StringUtils.isEmpty(cfBundleShortVersionStringValue)) { shortVersion = cfBundleShortVersionStringValue; } else { listener.getLogger().println( "You have to provide a value for either the marketing or technical version. Found neither."); return false; } } File file = new File(app.absolutize().getRemote()); String lastModified = new SimpleDateFormat("yyyy.MM.dd").format(new Date(file.lastModified())); String baseName = app.getBaseName().replaceAll(" ", "_") + (shortVersion.isEmpty() ? "" : "-" + shortVersion) + (version.isEmpty() ? "" : "-" + version); // If custom .ipa name pattern has been provided, use it and expand version and build date variables if (!StringUtils.isEmpty(ipaName)) { EnvVars customVars = new EnvVars("BASE_NAME", app.getBaseName().replaceAll(" ", "_"), "VERSION", version, "SHORT_VERSION", shortVersion, "BUILD_DATE", lastModified); baseName = customVars.expand(ipaName); } FilePath ipaLocation = ipaOutputPath.child(baseName + ".ipa"); FilePath payload = ipaOutputPath.child("Payload"); payload.deleteRecursive(); payload.mkdirs(); listener.getLogger().println( "Packaging " + app.getBaseName() + ".app => " + ipaLocation.absolutize().getRemote()); List<String> packageCommandLine = new ArrayList<String>(); packageCommandLine.add(getGlobalConfiguration().getXcrunPath()); packageCommandLine.add("-sdk"); if (!StringUtils.isEmpty(sdk)) { packageCommandLine.add(sdk); } else { packageCommandLine.add(buildPlatform); } packageCommandLine.addAll(Lists.newArrayList("PackageApplication", "-v", app.absolutize().getRemote(), "-o", ipaLocation.absolutize().getRemote())); if (!StringUtils.isEmpty(embeddedProfileFile)) { packageCommandLine.add("--embed"); packageCommandLine.add(embeddedProfileFile); } if (!StringUtils.isEmpty(codeSigningIdentity)) { packageCommandLine.add("--sign"); packageCommandLine.add(codeSigningIdentity); } returnCode = launcher.launch().envs(envs).stdout(listener).pwd(projectRoot).cmds(packageCommandLine) .join(); if (returnCode > 0) { listener.getLogger().println("Failed to build " + ipaLocation.absolutize().getRemote()); continue; } // also zip up the symbols, if present returnCode = launcher.launch().envs(envs).stdout(listener).pwd(buildDirectory) .cmds("ditto", "-c", "-k", "--keepParent", "-rsrc", app.absolutize().getRemote() + ".dSYM", ipaOutputPath.child(baseName + "-dSYM.zip").absolutize().getRemote()) .join(); if (returnCode > 0) { listener.getLogger().println(Messages.XCodeBuilder_zipFailed(baseName)); continue; } payload.deleteRecursive(); } } return true; } public Keychain getKeychain() { if (!StringUtils.isEmpty(keychainPath)) { return new Keychain("", keychainPath, keychainPwd, false); } for (Keychain keychain : getGlobalConfiguration().getKeychains()) { if (keychain.getKeychainName().equals(keychainName)) return keychain; } return null; } static List<String> splitXcodeBuildArguments(String xcodebuildArguments) { if (xcodebuildArguments == null || xcodebuildArguments.length() == 0) { return new ArrayList<String>(0); } final QuotedStringTokenizer tok = new QuotedStringTokenizer(xcodebuildArguments); final List<String> result = new ArrayList<String>(); while (tok.hasMoreTokens()) result.add(tok.nextToken()); return result; } public GlobalConfigurationImpl getGlobalConfiguration() { return ((AbstractXCodeDescriptor) super.getDescriptor()).getGlobalConfiguration(); } }