All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.openbakery.XcodeBuildArchiveTask.groovy Maven / Gradle / Ivy

Go to download

XCode-Plugin is a plugin to allow custom XCode projects to build as generated by CMake

There is a newer version: 0.0.201
Show newest version
/*
 * 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

import groovy.io.FileType
import org.apache.commons.io.FileUtils
import org.gradle.api.tasks.TaskAction
import org.openbakery.codesign.ProvisioningProfileReader
import org.openbakery.xcode.Type
import org.openbakery.xcode.Xcodebuild

import static groovy.io.FileType.ANY
import static groovy.io.FileType.FILES
import static groovy.io.FileVisitResult.SKIP_SUBTREE

class XcodeBuildArchiveTask extends AbstractXcodeBuildTask {

	public static final String ARCHIVE_FOLDER = "archive"

	XcodeBuildArchiveTask() {
		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"
	}


	def getOutputDirectory() {
		def archiveDirectory = new File(project.getBuildDir(), ARCHIVE_FOLDER)
		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
		).toList()

		def applicationPath = "Applications/" + parameters.applicationBundleName

		for (String item in fileList) {
			icons.add(applicationPath + "/" + item)
		}


		return icons
	}

	def getMacOSXIcons() {
		File appInfoPlist  = new File(parameters.applicationBundle, "Contents/Info.plist")
		ArrayList icons = new ArrayList<>();

		def icnsFileName = plistHelper.getValueFromPlist(appInfoPlist, "CFBundleIconFile")

		if (icnsFileName == null || icnsFileName == "") {
			return icons
		}

		def iconPath = "Applications/" + parameters.applicationBundleName + "/Contents/Resources/" + icnsFileName + ".icns"
		icons.add(iconPath)

		return icons
	}



	def getValueFromBundleInfoPlist(File bundle, String key) {
		File appInfoPlist
		if (parameters.type == Type.macOS) {
			appInfoPlist = new File(bundle, "Contents/Info.plist")
		} else {
			appInfoPlist = new File(bundle, "Info.plist")
		}
		return plistHelper.getValueFromPlist(appInfoPlist, key)
	}


	def createInfoPlist(def applicationsDirectory) {

		StringBuilder content = new StringBuilder();


		def name = parameters.bundleName
		def schemeName = name
		def applicationPath = "Applications/" + parameters.applicationBundleName
		def bundleIdentifier = getValueFromBundleInfoPlist(parameters.applicationBundle, "CFBundleIdentifier")
		int time = System.currentTimeMillis() / 1000;

		def creationDate = formatDate(new Date());

		def shortBundleVersion = getValueFromBundleInfoPlist(parameters.applicationBundle, "CFBundleShortVersionString")
		def bundleVersion = getValueFromBundleInfoPlist(parameters.applicationBundle, "CFBundleVersion")

		List icons
		if (parameters.type == Type.iOS) {
			icons = getiOSIcons()
		} else {
			icons = getMacOSXIcons()
		}

		content.append("\n")
		content.append("\n")
		content.append("\n")
		content.append("\n")
		content.append("	ApplicationProperties\n")
		content.append("	\n")
		content.append("		ApplicationPath\n")
		content.append("		" + applicationPath + "\n")
		content.append("		CFBundleIdentifier\n")
		content.append("		" + bundleIdentifier + "\n")

		if (shortBundleVersion != null) {
			content.append("		CFBundleShortVersionString\n")
			content.append("		" + shortBundleVersion + "\n")
		}

		if (bundleVersion != null) {
			content.append("		CFBundleVersion\n")
			content.append("		" + bundleVersion + "\n")
		}

		if (getSigningIdentity()) {
			content.append("		SigningIdentity\n")
			content.append("		" + getSigningIdentity() + "\n")

		}

		if (icons.size() > 0) {
			content.append("		IconPaths\n")
			content.append("		\n")
			for (String icon : icons) {
				content.append("			" + icon + "\n")
			}
			content.append("		\n")
		}

		content.append("	\n")
		content.append("	ArchiveVersion\n")
		content.append("	2\n")
		content.append("	CreationDate\n")
		content.append("	" + creationDate + "\n")
		content.append("	Name\n")
		content.append("	" + name  + "\n")
		content.append("	SchemeName\n")
		content.append("	" + schemeName + "\n")
		content.append("\n")
		content.append("")

		File infoPlist = new File(applicationsDirectory, "Info.plist")
		FileUtils.writeStringToFile(infoPlist, content.toString())
	}


	def createFrameworks(def archiveDirectory, Xcodebuild 	xcodebuild) {

		File frameworksPath = new File(archiveDirectory, "Products/Applications/" + parameters.applicationBundleName + "/Frameworks")
		if (frameworksPath.exists()) {


			def libNames = []
			frameworksPath.eachFile() {
				libNames.add(it.getName())
			}

			logger.debug("swiftlibs to add: {}", libNames);

			File swiftLibs = new File(xcodebuild.getToolchainDirectory(), "usr/lib/swift/iphoneos")

			swiftLibs.eachFile() {
				logger.debug("candidate for copy? {}: {}", it.name, libNames.contains(it.name))
				if (libNames.contains(it.name)) {
					copy(it, getSwiftSupportDirectory())
				}
			}
		}

	}

	def getSwiftSupportDirectory() {
		def swiftSupportPath = "SwiftSupport"

		if (xcode.version.major > 6) {
			swiftSupportPath += "/iphoneos"
		}

		File swiftSupportDirectory = new File(getArchiveDirectory(), swiftSupportPath);
		if (!swiftSupportDirectory.exists()) {
			swiftSupportDirectory.mkdirs()
		}
		return swiftSupportDirectory
	}

	def deleteDirectoryIfEmpty(File base, String child) {
		File directory = new File(base, child)
		if (directory.exists() && directory.list().length == 0) {
			directory.deleteDir();
		}
	}

	def deleteEmptyFrameworks(File applicationsDirectory) {
		// if frameworks directory is emtpy
		File appPath = new File(applicationsDirectory, "Products/Applications/" + parameters.applicationBundleName)
		deleteDirectoryIfEmpty(appPath, "Frameworks")



	}

	def deleteFrameworksInExtension(File applicationsDirectory) {


		File plugins = new File(applicationsDirectory, parameters.applicationBundleName + "/Plugins")
		if (!plugins.exists()) {
			return
		}

		plugins.eachFile(FileType.DIRECTORIES) { file ->
			if (file.toString().endsWith(".appex")) {
				File frameworkDirectory = new File(file, "Frameworks");
				if (frameworkDirectory.exists()) {
					FileUtils.deleteDirectory(frameworkDirectory)
				}
			}
		}

	}


	def copyDsyms(File archiveDirectory, File dSymDirectory) {

		archiveDirectory.eachFileRecurse(FileType.DIRECTORIES) { directory ->
			if (directory.toString().toLowerCase().endsWith(".dsym")) {
				copy(directory, dSymDirectory)
			}
		}

	}

	def createEntitlements(File bundle) {

		if (parameters.type != Type.iOS) {
			logger.warn("Entitlements handling is only implemented for iOS!")
			return
		}

		String bundleIdentifier = getValueFromBundleInfoPlist(bundle, "CFBundleIdentifier")
		if (bundleIdentifier == null) {
			logger.debug("No entitlements embedded, because no bundle identifier found in bundle {}", bundle)
			return
		}
		BuildConfiguration buildConfiguration = project.xcodebuild.getBuildConfiguration(bundleIdentifier)
		if (buildConfiguration == null) {
			logger.debug("No entitlements embedded, because no buildConfiguration for bundle identifier {}", bundleIdentifier)
			return
		}

		File destinationDirectory = getDestinationDirectoryForBundle(bundle)
		if (buildConfiguration.entitlements) {
			File entitlementFile = new File(destinationDirectory, "archived-expanded-entitlements.xcent")
			FileUtils.copyFile(new File(project.projectDir, buildConfiguration.entitlements), entitlementFile)
			modifyEntitlementsFile(entitlementFile, bundleIdentifier)
		}
	}

	def modifyEntitlementsFile(File entitlementFile, String bundleIdentifier) {
		if (!entitlementFile.exists()) {
			logger.warn("Entitlements File does not exist {}", entitlementFile)
			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)
		if (provisioningProfile != null && provisioningProfile.exists()) {
			ProvisioningProfileReader reader = new ProvisioningProfileReader(provisioningProfile, commandRunner)
			applicationIdentifier = reader.getApplicationIdentifierPrefix()
		}

		plistHelper.addValueForPlist(entitlementFile, "application-identifier", applicationIdentifier + "." + bundleIdentifier)

		List keychainAccessGroups = plistHelper.getValueFromPlist(entitlementFile, "keychain-access-groups")

		if (keychainAccessGroups != null && keychainAccessGroups.size() > 0) {
			def modifiedKeychainAccessGroups = []
			keychainAccessGroups.each() { group ->
				modifiedKeychainAccessGroups << group.replace(ProvisioningProfileReader.APPLICATION_IDENTIFIER_PREFIX, applicationIdentifier + ".")
			}
			plistHelper.setValueForPlist(entitlementFile, "keychain-access-groups", modifiedKeychainAccessGroups)
		}
	}


	@TaskAction
	def archive() {
		parameters = project.xcodebuild.xcodebuildParameters.merge(parameters)
		if (parameters.isSimulatorBuildOf(Type.iOS)) {
			logger.debug("Create zip archive")

			// create zip archive
			String zipFileName = parameters.bundleName
			if (project.xcodebuild.bundleNameSuffix != null) {
				zipFileName += project.xcodebuild.bundleNameSuffix
			}
			zipFileName += ".zip"

			def zipFile = new File(project.getBuildDir(), "archive/" + zipFileName)
			def baseDirectory = parameters.applicationBundle.parentFile

			createZip(zipFile, baseDirectory, parameters.applicationBundle)
			return
		}

		final excludedDirs = ['.svn', '.git', '.hg', '.idea', 'node_modules', '.gradle', 'CMakeFiles']
		def file = null
		project.projectDir.traverse(
				type         : ANY,
				nameFilter	 : ~/.*\.xcodeproj$/,
				preDir       : { if (it.name in excludedDirs) return SKIP_SUBTREE },
				visitRoot	 : true) {
			if(file == null) {
				file = it.parentFile
			}
		}


		logger.debug("Create xcarchive")
		Xcodebuild xcodebuild
		if(file == null)
			xcodebuild = new Xcodebuild(project.projectDir, commandRunner, xcode, parameters, getDestinations())
		else
			xcodebuild = new Xcodebuild(file, commandRunner, xcode, parameters, getDestinations())

		if (project.xcodebuild.useXcodebuildArchive) {

			File outputFile = new File(project.getBuildDir(), "xcodebuild-archive-output.txt")
			commandRunner.setOutputFile(outputFile)

			xcodebuild.executeArchive(createXcodeBuildOutputAppender("XcodeBuildArchive"), project.xcodebuild.environment, getArchiveDirectory().absolutePath)

			return
		}


		// create xcarchive
		// copy application bundle
		copy(parameters.applicationBundle, getApplicationsDirectory())

		File onDemandResources = new File(parameters.outputPath, "OnDemandResources")
		if (onDemandResources.exists()) {
			copy(onDemandResources, getProductsDirectory())
		}

		// copy onDemandResources

		def dSymDirectory = new File(getArchiveDirectory(), "dSYMs")
		dSymDirectory.mkdirs()
		copyDsyms(parameters.outputPath, dSymDirectory)

		List appBundles = getAppBundles(parameters.outputPath)
		for (File bundle : appBundles) {
			createEntitlements(bundle)
		}

		File applicationsDirectory = getApplicationsDirectory()
		File archiveDirectory = getArchiveDirectory()
		createInfoPlist(archiveDirectory)
		createFrameworks(archiveDirectory, xcodebuild)
		deleteEmptyFrameworks(archiveDirectory)
		deleteXCTestIfExists(applicationsDirectory)
		deleteFrameworksInExtension(applicationsDirectory)
		copyBCSymbolMaps(archiveDirectory, applicationsDirectory)

		if (project.xcodebuild.type == Type.iOS) {
			File applicationFolder = new File(getArchiveDirectory(), "Products/Applications/" + parameters.applicationBundleName)
			convertInfoPlistToBinary(applicationFolder)
		}

		logger.debug("create archive done")
	}

	def copyBCSymbolMaps(File archiveDirectory, File applicationsDirectory) {
		if (!parameters.bitcode) {
			logger.debug("bitcode is not activated, so to not create BCSymbolMaps")
			return
		}
		File bcSymbolMapsDirectory = new File(applicationsDirectory, parameters.applicationBundleName + "/BCSymbolMaps")
		bcSymbolMapsDirectory.mkdirs()

		parameters.outputPath.eachFileRecurse { file ->
			if (file.toString().endsWith(".bcsymbolmap")) {
				FileUtils.copyFileToDirectory(file, bcSymbolMapsDirectory)
			}
		}

	}

	def deleteXCTestIfExists(File applicationsDirectory) {
		File plugins = new File(applicationsDirectory, project.xcodebuild.applicationBundle.name + "/Contents/Plugins")
		if (!plugins.exists()) {
			return
		}
		plugins.eachFile (FileType.DIRECTORIES) { file ->
			if (file.toString().endsWith("xctest")) {
				FileUtils.deleteDirectory(file)
				return true
			}
		}
	}

	File getProductsDirectory() {
		File productsDirectory = new File(getArchiveDirectory(), "Products")
		productsDirectory.mkdirs()
		return productsDirectory
	}

	File getApplicationsDirectory() {
		File applicationsDirectory = new File(getProductsDirectory(), "Applications")
		applicationsDirectory.mkdirs()
		return applicationsDirectory
	}

	File getDestinationDirectoryForBundle(File bundle) {
		String relative = parameters.outputPath.toURI().relativize(bundle.toURI()).getPath();
		return new File(getApplicationsDirectory(), relative)
	}

	def convertInfoPlistToBinary(File archiveDirectory) {

		archiveDirectory.eachFileRecurse(FILES) {
			if (it.name.endsWith('.plist')) {
				logger.debug("convert plist to binary {}", it)
				def commandList = ["/usr/bin/plutil", "-convert", "binary1", it.absolutePath]
				try {
					commandRunner.run(commandList)
				} catch (CommandRunnerException ex) {
					logger.lifecycle("Unable to convert!")
				}
			}
		}

	}


	def removeResourceRules(File appDirectory) {

		File resourceRules = new File(appDirectory, "ResourceRules.plist")
		logger.lifecycle("delete {}", resourceRules)
		if (resourceRules.exists()) {
			resourceRules.delete()
		}

		logger.lifecycle("remove CFBundleResourceSpecification from {}", new File(appDirectory, "Info.plist"))

		setValueForPlist(new File(appDirectory, "Info.plist"), "Delete: CFBundleResourceSpecification")

	}


	File getArchiveDirectory() {

		def archiveDirectoryName =  XcodeBuildArchiveTask.ARCHIVE_FOLDER + "/" +  project.xcodebuild.bundleName

		if (project.xcodebuild.bundleNameSuffix != null) {
			archiveDirectoryName += project.xcodebuild.bundleNameSuffix
		}
		archiveDirectoryName += ".xcarchive"

		def archiveDirectory = new File(project.getBuildDir(), archiveDirectoryName)
		archiveDirectory.mkdirs()
		return archiveDirectory
	}


}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy