Skip to content

Commit

Permalink
Add discrete install provisioning profile task (#5)
Browse files Browse the repository at this point in the history
Add discrete install profile task

Description
===========

We use fastlane at the moment to import provisional and install provisioning profiles.
The install part is indirect because fastlane itself does it for us under the hood.
This worked for us very well over the years. But there is no real chance to install
profiles in any other way. Hence a discrete install profile task.

To be able to install provisioning profiles one needs to be able to read the UUID out
of the included meta data. The reason is that all profiles are stored on the system
with the UUID as the file name. I created a new helper class + test mock class to
read provisioning profiles and fetch the metadata from them.
A profile consists of 3 pieces:

1. A header (some bytes long) I have no information what information is stored there
2. The meta data in plist format
3. a series of certificates (unknown format)

We are mainly interested in the meta data.

I removed the import provisioning profiles task from the direct task dependency list.
Instead the new task `installProvisioningProfiles` will be used in it's place. This new task will
have a dependency (_indirect_) to the `importProvisioningProfiles` task. This means that one
could reconfigure the `importProvisioningProfiles` tasks and skip the fastlane backed task `importProvisioningProfiles` all together.

This is needed for out new alternative flow where we store the profiles in the aws secrets manager.

Changes
=======

* ![ADD] `InstallProvisioningProfiles` task type
  • Loading branch information
Larusso committed Mar 14, 2023
1 parent 1d77a5f commit 24f3279
Show file tree
Hide file tree
Showing 12 changed files with 829 additions and 237 deletions.
3 changes: 2 additions & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,8 @@ cveHandler {
dependencies {
api 'net.wooga.gradle:macos-security:[1.0.1,2['
api 'net.wooga.gradle:xcodebuild:[1,2['
api 'net.wooga.gradle:fastlane:[1.0.1,2['
api 'net.wooga.gradle:fastlane:[1.2,2['
implementation "com.googlecode.plist:dd-plist:1.23"
integrationTestImplementation'com.wooga.spock.extensions:spock-macos-keychain-extension:[1,2['
implementation 'com.wooga.gradle:gradle-commons:[1,2['
testImplementation 'com.wooga.gradle:gradle-commons-test:[1,2['
Expand Down

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package wooga.gradle.build.unity.ios.tasks

import com.wooga.gradle.test.TaskIntegrationSpec
import org.gradle.api.Task
import wooga.gradle.build.unity.ios.IOSBuildIntegrationSpec

abstract class IOSBuildTaskIntegrationSpec<T extends Task> extends IOSBuildIntegrationSpec implements TaskIntegrationSpec<T> {
def setup() {
buildFile << """
task ${subjectUnderTestName}(type: ${subjectUnderTestTypeName})
""".stripIndent()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,21 +12,15 @@ import static com.wooga.gradle.test.PropertyUtils.toProviderSet
import static com.wooga.gradle.test.PropertyUtils.toSetter

@Requires({ os.macOs })
class ImportCodeSigningIdentitiesIntegrationSpec extends IOSBuildIntegrationSpec {

String subjectUnderTestName = "importSigningIdentity"
String subjectUnderTestTypeName = ImportCodeSigningIdentities.class.name
class ImportCodeSigningIdentitiesIntegrationSpec extends IOSBuildTaskIntegrationSpec<ImportCodeSigningIdentities> {

@Keychain(unlockKeychain = true)
MacOsKeychain buildKeychain

def setup() {
buildFile << """
task ${subjectUnderTestName}(type: ${subjectUnderTestTypeName}) {
//inputKeychain = file('${buildKeychain.location}')
appendToSubjectTask("""
keychain = file('${buildKeychain.location}')
}
""".stripIndent()
""".stripIndent())
}

@Unroll("import #taskStatus when #reason")
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
package wooga.gradle.build.unity.ios.tasks

import com.wooga.gradle.PlatformUtils
import com.wooga.gradle.test.PropertyQueryTaskWriter
import com.wooga.gradle.test.ios.MobileProvisionMock
import spock.lang.Requires
import spock.lang.Unroll

import static com.wooga.gradle.test.PropertyUtils.toProviderSet
import static com.wooga.gradle.test.PropertyUtils.toSetter

class InstallProvisioningProfilesIntegrationSpec extends IOSBuildTaskIntegrationSpec<InstallProvisionProfiles> {

@Requires({ PlatformUtils.mac })
@Unroll("can set property #property with #method and type #type")
def "can set property"() {
given: "a task to read back the value"
def query = new PropertyQueryTaskWriter("${subjectUnderTestName}.${property}")
query.write(buildFile)

and: "a set property"
appendToSubjectTask("${method}($value)")

when:
def result = runTasksSuccessfully(query.taskName)

then:
query.matches(result, expectedValue)

where:
property | method | rawValue | returnValue | type
"outputDirectory" | property | "/path/to/outputDir" | _ | "File"
"outputDirectory" | property | "/path/to/outputDir" | _ | "Provider<Directory>"
"outputDirectory" | toProviderSet(property) | "/path/to/outputDir" | _ | "File"
"outputDirectory" | toProviderSet(property) | "/path/to/outputDir" | _ | "Provider<Directory>"
"outputDirectory" | toSetter(property) | "/path/to/outputDir" | _ | "File"
"outputDirectory" | toSetter(property) | "/path/to/outputDir" | _ | "Provider<Directory>"

"logFile" | property | "/path/to/log" | _ | "File"
"logFile" | property | "/path/to/log" | _ | "Provider<RegularFile>"
"logFile" | toProviderSet(property) | "/path/to/log" | _ | "File"
"logFile" | toProviderSet(property) | "/path/to/log" | _ | "Provider<RegularFile>"
"logFile" | toSetter(property) | "/path/to/log" | _ | "File"
"logFile" | toSetter(property) | "/path/to/log" | _ | "Provider<RegularFile>"

"logToStdout" | toProviderSet(property) | true | _ | "Boolean"
"logToStdout" | toProviderSet(property) | true | _ | "Provider<Boolean>"
"logToStdout" | toSetter(property) | true | _ | "Boolean"
"logToStdout" | toSetter(property) | true | _ | "Provider<Boolean>"
value = wrapValueBasedOnType(rawValue, type, wrapValueFallback)
expectedValue = returnValue == _ ? rawValue : returnValue
}

def "installs provided provisioning profiles to the output directory"() {
given: "a mock mobile provisioning file with a known uuid"
def files = uuids.collect { UUID id ->
def mock = MobileProvisionMock.createMock({
it.uuid = id
})
def installedProfile = new File(projectDir, "build/profiles/${id}.mobileprovision")
new Tuple2<File, File>(mock, installedProfile)
}

and: "a future provisioning profile location"
assert !files.any { it.second.exists() }

and:
appendToSubjectTask("""
provisioningProfiles.from(${wrapValueBasedOnType(files.collect { it.first }, "List<File>")})
outputDirectory = ${wrapValueBasedOnType(new File(projectDir, "build/profiles"), "File")}
""".stripIndent())

when:
runTasksSuccessfully(subjectUnderTestName)

then:
files.every { it.second.exists() }
files.every {
def mockBytes = it.first.bytes
def profileBytes = it.second.bytes
mockBytes == profileBytes
}

where:
uuids = [UUID.randomUUID(), UUID.randomUUID(), UUID.randomUUID()]
}

def "writes log to logfile and stdout when configured"() {
given: "a mock mobile provisioning file with a known uuid"
def files = uuids.collect { UUID id ->
def mock = MobileProvisionMock.createMock({
it.uuid = id
})
def installedProfile = new File(projectDir, "build/profiles/${id}.mobileprovision")
new Tuple2<File, File>(mock, installedProfile)
}

and: "a future logfile"
def logFile = new File(projectDir, logFilePath)
assert !logFile.exists()

and:
appendToSubjectTask("""
provisioningProfiles.from(${wrapValueBasedOnType(files.collect { it.first }, "List<File>")})
outputDirectory = ${wrapValueBasedOnType(new File(projectDir, "build/profiles"), "File")}
logFile = ${wrapValueBasedOnType(logFile, "File")}
logToStdout = ${wrapValueBasedOnType(logToStdout, "Boolean")}
""".stripIndent())

when:
def result = runTasksSuccessfully(subjectUnderTestName)

then:
logFile.exists()
logFile.text.contains("Install Profiles: ${uuids.size()}")
result.standardOutput.contains(logFile.text) == logToStdout

where:
uuids = [UUID.randomUUID(), UUID.randomUUID(), UUID.randomUUID()]
logFilePath = osPath("build/logs/custom.log")
logToStdout << [true, false]
}
}
59 changes: 39 additions & 20 deletions src/main/groovy/wooga/gradle/build/unity/ios/IOSBuildPlugin.groovy
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import org.gradle.api.tasks.Sync
import org.gradle.api.tasks.TaskProvider
import wooga.gradle.build.unity.ios.internal.DefaultIOSBuildPluginExtension
import wooga.gradle.build.unity.ios.tasks.ImportCodeSigningIdentities
import wooga.gradle.build.unity.ios.tasks.InstallProvisionProfiles
import wooga.gradle.build.unity.ios.tasks.PodInstallTask
import wooga.gradle.fastlane.FastlanePlugin
import wooga.gradle.fastlane.FastlanePluginExtension
Expand Down Expand Up @@ -110,6 +111,25 @@ class IOSBuildPlugin implements Plugin<Project> {
extension.keychainPassword.convention(IOSBuildPluginConventions.keychainPassword.getStringValueProvider(project))
extension.publishToTestFlight.convention(IOSBuildPluginConventions.publishToTestFlight.getBooleanValueProvider(project))
extension.scheme.convention(IOSBuildPluginConventions.scheme.getStringValueProvider(project))
extension.provisioningProfiles.convention(extension.exportOptions.map({
def profiles = it.getProvisioningProfiles()
def appIdentifier = extension.appIdentifier.getOrElse("")
def provisioningProfileAppId = extension.provisioningProfileAppId.getOrElse("")
if (appIdentifier != provisioningProfileAppId) {
if(provisioningProfileAppId.endsWith(".*")) {
String wildCardPrefix = provisioningProfileAppId.substring(0, provisioningProfileAppId.length() -2)
profiles = profiles.collectEntries { appId, name ->
if (appId.startsWith(wildCardPrefix)) {
return [provisioningProfileAppId, name]
}
[appId, name]
}
} else {
LOG.warn("property 'provisioningProfileAppId' has a different value than 'appIdentifier' but is not a wildcard Id. Potential miss-configuration")
}
}
profiles
}).orElse([:]))

//register some defaults
project.tasks.withType(XcodeArchive.class, new Action<XcodeArchive>() {
Expand Down Expand Up @@ -169,6 +189,8 @@ class IOSBuildPlugin implements Plugin<Project> {
fastlaneExtension.password.getOrNull()
}))

task.readOnly.convention(true)
task.skipInstall.convention(true)
task.teamId.convention(extension.getTeamId())
task.appIdentifier.convention(extension.getAppIdentifier())
task.destinationDir.convention(project.layout.dir(project.provider({ task.getTemporaryDir() })))
Expand Down Expand Up @@ -207,6 +229,17 @@ class IOSBuildPlugin implements Plugin<Project> {
}
})

project.tasks.withType(InstallProvisionProfiles.class, new Action<InstallProvisionProfiles>() {
@Override
void execute(InstallProvisionProfiles task) {
task.logFile.convention(project.layout.buildDirectory.file("logs/${task.name}.log"))
task.logToStdout.convention(project.provider {project.logger.isInfoEnabled()})
task.outputDirectory.convention(project.layout.dir(project.provider {
new File("${System.getProperty("user.home")}/Library/MobileDevice/Provisioning\\ Profiles/")
}))
}
})

project.tasks.withType(PodInstallTask.class, new Action<PodInstallTask>() {
@Override
void execute(PodInstallTask task) {
Expand Down Expand Up @@ -294,37 +327,23 @@ class IOSBuildPlugin implements Plugin<Project> {
})

def importProvisioningProfiles = tasks.register("importProvisioningProfiles", SighRenewBatch) {
it.profiles.set(extension.exportOptions.map({
def profiles = it.getProvisioningProfiles()
def appIdentifier = extension.appIdentifier.get()
def provisioningProfileAppId = extension.provisioningProfileAppId.get()
if (appIdentifier != provisioningProfileAppId) {
if(provisioningProfileAppId.endsWith(".*")) {
String wildCardPrefix = provisioningProfileAppId.substring(0, provisioningProfileAppId.length() -2)
profiles = profiles.collectEntries { appId, name ->
if (appId.startsWith(wildCardPrefix)) {
return [provisioningProfileAppId, name]
}
[appId, name]
}
} else {
logger.warn("property 'provisioningProfileAppId' has a different value than 'appIdentifier' but is not a wildcard Id. Potential miss-configuration")
}
}
profiles
}))
it.profiles.set(extension.provisioningProfiles)
it.dependsOn addKeychain, buildKeychain, unlockKeychain
it.finalizedBy removeKeychain, lockKeychain
}

def installProvisioningProfiles = tasks.register("installProvisioningProfiles", InstallProvisionProfiles) {
it.provisioningProfiles.from(importProvisioningProfiles.get().getOutputs())
}

TaskProvider<PodInstallTask> podInstall = tasks.register("podInstall", PodInstallTask) {
it.projectDirectory.set(extension.xcodeProjectDirectory)
it.xcodeWorkspaceFileName.set(extension.xcodeWorkspaceFileName)
it.xcodeProjectFileName.set(extension.xcodeProjectFileName)
}

def xcodeArchive = tasks.register("xcodeArchive", XcodeArchive) {
it.dependsOn addKeychain, unlockKeychain, importProvisioningProfiles, podInstall, buildKeychain
it.dependsOn addKeychain, unlockKeychain, installProvisioningProfiles, podInstall, buildKeychain
it.projectPath.set(extension.projectPath)
it.buildKeychain.set(buildKeychain.flatMap({ it.keychain }))
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import org.gradle.api.file.DirectoryProperty
import org.gradle.api.file.RegularFile
import org.gradle.api.file.RegularFileProperty
import org.gradle.api.provider.ListProperty
import org.gradle.api.provider.MapProperty
import org.gradle.api.provider.Property
import org.gradle.api.provider.Provider
import org.gradle.api.tasks.Input
Expand Down Expand Up @@ -266,6 +267,21 @@ trait IOSBuildPluginExtension extends BaseSpec {
})
}

private final MapProperty<String, String> provisioningProfiles = objects.mapProperty(String, String)

@Input
MapProperty<String, String> getProvisioningProfiles() {
provisioningProfiles
}

void setProvisioningProfiles(Map<String, String> value) {
provisioningProfiles.set(value)
}

void setProvisioningProfiles(Provider<Map<String, String>> value) {
provisioningProfiles.set(value)
}

private final List<Action<ExportOptions>> exportOptionsActions = []

void exportOptions(Action<ExportOptions> action) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
package wooga.gradle.build.unity.ios

trait MobileProvisioning {
abstract String getName()

abstract String getAppIdName()

abstract String getUuid()

abstract String getTeamName()

abstract List<String> getTeamIdentifier()

abstract Date getExpirationDate()

abstract Date getCreationDate()

abstract Boolean isExpired()

abstract Integer getTimeToLive()

abstract Boolean isXcodeManaged()

abstract Integer getVersion()

abstract Map getEntitlements()

boolean equals(MobileProvisioning o) {
if (this.is(o)) return true

if (!(o instanceof MobileProvisioning)) {
return false
}

MobileProvisioning that = (MobileProvisioning) o

if (appIdName != that.appIdName) return false
if (creationDate != that.creationDate) return false
if (entitlements != that.entitlements) return false
if (expirationDate != that.expirationDate) return false
if (name != that.name) return false
if (teamIdentifier != that.teamIdentifier) return false
if (teamName != that.teamName) return false
if (timeToLive != that.timeToLive) return false
if (uuid != that.uuid) return false
if (version != that.version) return false
if (xcodeManaged != that.xcodeManaged) return false

return true
}

int hashCode() {
int result
result = (uuid != null ? uuid.hashCode() : 0)
result = 31 * result + (appIdName != null ? appIdName.hashCode() : 0)
result = 31 * result + (name != null ? name.hashCode() : 0)
result = 31 * result + (teamIdentifier != null ? teamIdentifier.hashCode() : 0)
result = 31 * result + (teamName != null ? teamName.hashCode() : 0)
result = 31 * result + (timeToLive != null ? timeToLive.hashCode() : 0)
result = 31 * result + (version != null ? version.hashCode() : 0)
result = 31 * result + (creationDate != null ? creationDate.hashCode() : 0)
result = 31 * result + (expirationDate != null ? expirationDate.hashCode() : 0)
result = 31 * result + (xcodeManaged != null ? xcodeManaged.hashCode() : 0)
result = 31 * result + (entitlements != null ? entitlements.hashCode() : 0)
return result
}
}
Loading

0 comments on commit 24f3279

Please sign in to comment.