From 24f3279d0cc67951c40353ae27f3e2983f8b54e7 Mon Sep 17 00:00:00 2001 From: Manfred Endres <2523575+Larusso@users.noreply.github.com> Date: Tue, 14 Mar 2023 16:30:09 +0100 Subject: [PATCH] Add discrete install provisioning profile task (#5) 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 --- build.gradle | 3 +- .../ios/IOSBuildPluginIntegrationSpec.groovy | 423 +++++++++--------- .../tasks/IOSBuildTaskIntegrationSpec.groovy | 13 + ...odeSigningIdentitiesIntegrationSpec.groovy | 12 +- ...ProvisioningProfilesIntegrationSpec.groovy | 123 +++++ .../build/unity/ios/IOSBuildPlugin.groovy | 59 ++- .../unity/ios/IOSBuildPluginExtension.groovy | 16 + .../build/unity/ios/MobileProvisioning.groovy | 67 +++ .../internal/DefaultMobileProvisioning.groovy | 95 ++++ .../ios/tasks/InstallProvisionProfiles.groovy | 103 +++++ .../test/ios/MobileProvisionMock.groovy | 113 +++++ .../internal/MobileProvisioningSpec.groovy | 39 ++ 12 files changed, 829 insertions(+), 237 deletions(-) create mode 100644 src/integrationTest/groovy/wooga/gradle/build/unity/ios/tasks/IOSBuildTaskIntegrationSpec.groovy create mode 100644 src/integrationTest/groovy/wooga/gradle/build/unity/ios/tasks/InstallProvisioningProfilesIntegrationSpec.groovy create mode 100644 src/main/groovy/wooga/gradle/build/unity/ios/MobileProvisioning.groovy create mode 100644 src/main/groovy/wooga/gradle/build/unity/ios/internal/DefaultMobileProvisioning.groovy create mode 100644 src/main/groovy/wooga/gradle/build/unity/ios/tasks/InstallProvisionProfiles.groovy create mode 100644 src/test/groovy/com/wooga/gradle/test/ios/MobileProvisionMock.groovy create mode 100644 src/test/groovy/wooga/gradle/build/unity/ios/internal/MobileProvisioningSpec.groovy diff --git a/build.gradle b/build.gradle index 95ab468..09be664 100644 --- a/build.gradle +++ b/build.gradle @@ -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[' diff --git a/src/integrationTest/groovy/wooga/gradle/build/unity/ios/IOSBuildPluginIntegrationSpec.groovy b/src/integrationTest/groovy/wooga/gradle/build/unity/ios/IOSBuildPluginIntegrationSpec.groovy index de09eaa..0fb2107 100644 --- a/src/integrationTest/groovy/wooga/gradle/build/unity/ios/IOSBuildPluginIntegrationSpec.groovy +++ b/src/integrationTest/groovy/wooga/gradle/build/unity/ios/IOSBuildPluginIntegrationSpec.groovy @@ -351,6 +351,8 @@ class IOSBuildPluginIntegrationSpec extends IOSBuildIntegrationSpec { SighRenew | "provisioningName" | "provisioningNameValue" | "String" | ConventionSource.extension(extensionName, property) | _ | _ SighRenew | "adhoc" | true | "Boolean" | ConventionSource.extension(extensionName, property) | _ | _ SighRenew | "fileName" | "signing.mobileprovision" | "String" | _ | _ | _ + SighRenew | "readOnly" | true | "Boolean" | _ | _ | _ + SighRenew | "skipInstall" | true | "Boolean" | _ | _ | _ PilotUpload | "devPortalTeamId" | "test.teamId" | "String" | ConventionSource.extension(extensionName, "teamId") | _ | _ PilotUpload | "appIdentifier" | "test.appIdentifier" | "String" | ConventionSource.extension(extensionName, property) | _ | _ @@ -366,213 +368,220 @@ class IOSBuildPluginIntegrationSpec extends IOSBuildIntegrationSpec { runPropertyQuery(get, set).matches(rawValue) where: - property | invocation | rawValue | type | location - "keychainPassword" | _ | "testPassword1" | _ | PropertyLocation.environment - "keychainPassword" | _ | "testPassword2" | _ | PropertyLocation.property - "keychainPassword" | assignment | "testPassword3" | "String" | PropertyLocation.script - "keychainPassword" | assignment | "testPassword4" | "Provider" | PropertyLocation.script - "keychainPassword" | providerSet | "testPassword5" | "String" | PropertyLocation.script - "keychainPassword" | providerSet | "testPassword6" | "Provider" | PropertyLocation.script - "keychainPassword" | setter | "testPassword7" | "String" | PropertyLocation.script - "keychainPassword" | setter | "testPassword8" | "Provider" | PropertyLocation.script - "keychainPassword" | _ | null | _ | PropertyLocation.none - - "signingIdentities" | _ | TestValue.set("ID1,ID2").expect(["ID1", "ID2"]) | _ | PropertyLocation.environment - "signingIdentities" | _ | TestValue.set("ID1,ID2").expect(["ID1", "ID2"]) | _ | PropertyLocation.property - "signingIdentities" | assignment | ["ID1", "ID2"] | "List" | PropertyLocation.script - "signingIdentities" | assignment | ["ID1", "ID2"] | "Provider>" | PropertyLocation.script - "signingIdentities" | providerSet | ["ID1", "ID2"] | "List" | PropertyLocation.script - "signingIdentities" | providerSet | ["ID1", "ID2"] | "Provider>" | PropertyLocation.script - "signingIdentities" | setter | ["ID1", "ID2"] | "List" | PropertyLocation.script - "signingIdentities" | setter | ["ID1", "ID2"] | "Provider>" | PropertyLocation.script - "signingIdentities" | customSetter("setSigningIdentity") | TestValue.set(wrapValueBasedOnType("code sign: ID3", "String")).expect(["code sign: ID3"]) | "Provider>" | PropertyLocation.script - "signingIdentities" | customSetter("setSigningIdentity") | TestValue.set(wrapValueBasedOnType("code sign: ID3", "String")).expect(["code sign: ID3"]) | "String>" | PropertyLocation.script - "signingIdentities" | _ | [] | _ | PropertyLocation.none - - "codeSigningIdentityFile" | _ | osPath("/path/to/p12") | _ | PropertyLocation.environment - "codeSigningIdentityFile" | _ | osPath("/path/to/p12") | _ | PropertyLocation.property - "codeSigningIdentityFile" | assignment | osPath("/path/to/p12") | "File" | PropertyLocation.script - "codeSigningIdentityFile" | assignment | osPath("/path/to/p12") | "Provider" | PropertyLocation.script - "codeSigningIdentityFile" | providerSet | osPath("/path/to/p12") | "File" | PropertyLocation.script - "codeSigningIdentityFile" | providerSet | osPath("/path/to/p12") | "Provider" | PropertyLocation.script - "codeSigningIdentityFile" | setter | osPath("/path/to/p12") | "File" | PropertyLocation.script - "codeSigningIdentityFile" | setter | osPath("/path/to/p12") | "Provider" | PropertyLocation.script - "codeSigningIdentityFile" | _ | null | _ | PropertyLocation.none - - "codeSigningIdentityFilePassphrase" | _ | "testPassphrase1" | _ | PropertyLocation.environment - "codeSigningIdentityFilePassphrase" | _ | "testPassphrase2" | _ | PropertyLocation.property - "codeSigningIdentityFilePassphrase" | assignment | "testPassphrase3" | "String" | PropertyLocation.script - "codeSigningIdentityFilePassphrase" | assignment | "testPassphrase4" | "Provider" | PropertyLocation.script - "codeSigningIdentityFilePassphrase" | providerSet | "testPassphrase5" | "String" | PropertyLocation.script - "codeSigningIdentityFilePassphrase" | providerSet | "testPassphrase6" | "Provider" | PropertyLocation.script - "codeSigningIdentityFilePassphrase" | setter | "testPassphrase7" | "String" | PropertyLocation.script - "codeSigningIdentityFilePassphrase" | setter | "testPassphrase8" | "Provider" | PropertyLocation.script - "codeSigningIdentityFilePassphrase" | _ | null | _ | PropertyLocation.none - - "appIdentifier" | _ | "com.test.app.1" | _ | PropertyLocation.environment - "appIdentifier" | _ | "com.test.app.2" | _ | PropertyLocation.property - "appIdentifier" | assignment | "com.test.app.3" | "String" | PropertyLocation.script - "appIdentifier" | assignment | "com.test.app.4" | "Provider" | PropertyLocation.script - "appIdentifier" | providerSet | "com.test.app.5" | "String" | PropertyLocation.script - "appIdentifier" | providerSet | "com.test.app.6" | "Provider" | PropertyLocation.script - "appIdentifier" | setter | "com.test.app.7" | "String" | PropertyLocation.script - "appIdentifier" | setter | "com.test.app.8" | "Provider" | PropertyLocation.script - "appIdentifier" | _ | null | _ | PropertyLocation.none - - "provisioningProfileAppId" | _ | "com.test.app.1.*" | _ | PropertyLocation.environment - "provisioningProfileAppId" | _ | "com.test.app.2.*" | _ | PropertyLocation.property - "provisioningProfileAppId" | assignment | "com.test.app.3.*" | "String" | PropertyLocation.script - "provisioningProfileAppId" | assignment | "com.test.app.4.*" | "Provider" | PropertyLocation.script - "provisioningProfileAppId" | providerSet | "com.test.app.5.*" | "String" | PropertyLocation.script - "provisioningProfileAppId" | providerSet | "com.test.app.6.*" | "Provider" | PropertyLocation.script - "provisioningProfileAppId" | setter | "com.test.app.7.*" | "String" | PropertyLocation.script - "provisioningProfileAppId" | setter | "com.test.app.8.*" | "Provider" | PropertyLocation.script - "provisioningProfileAppId" | _ | null | _ | PropertyLocation.none - - "teamId" | _ | "team1" | _ | PropertyLocation.environment - "teamId" | _ | "team2" | _ | PropertyLocation.property - "teamId" | assignment | "team3" | "String" | PropertyLocation.script - "teamId" | assignment | "team4" | "Provider" | PropertyLocation.script - "teamId" | providerSet | "team5" | "String" | PropertyLocation.script - "teamId" | providerSet | "team6" | "Provider" | PropertyLocation.script - "teamId" | setter | "team7" | "String" | PropertyLocation.script - "teamId" | setter | "team8" | "Provider" | PropertyLocation.script - "teamId" | _ | null | _ | PropertyLocation.none - - "scheme" | _ | "value1" | _ | PropertyLocation.environment - "scheme" | _ | "value2" | _ | PropertyLocation.property - "scheme" | assignment | "value3" | "String" | PropertyLocation.script - "scheme" | assignment | "value4" | "Provider" | PropertyLocation.script - "scheme" | providerSet | "value5" | "String" | PropertyLocation.script - "scheme" | providerSet | "value6" | "Provider" | PropertyLocation.script - "scheme" | setter | "value7" | "String" | PropertyLocation.script - "scheme" | setter | "value8" | "Provider" | PropertyLocation.script - "scheme" | _ | null | _ | PropertyLocation.none - - "configuration" | _ | "value1" | _ | PropertyLocation.environment - "configuration" | _ | "value2" | _ | PropertyLocation.property - "configuration" | assignment | "value3" | "String" | PropertyLocation.script - "configuration" | assignment | "value4" | "Provider" | PropertyLocation.script - "configuration" | providerSet | "value5" | "String" | PropertyLocation.script - "configuration" | providerSet | "value6" | "Provider" | PropertyLocation.script - "configuration" | setter | "value7" | "String" | PropertyLocation.script - "configuration" | setter | "value8" | "Provider" | PropertyLocation.script - "configuration" | _ | null | _ | PropertyLocation.none - - "provisioningName" | _ | "value1" | _ | PropertyLocation.environment - "provisioningName" | _ | "value2" | _ | PropertyLocation.property - "provisioningName" | assignment | "value3" | "String" | PropertyLocation.script - "provisioningName" | assignment | "value4" | "Provider" | PropertyLocation.script - "provisioningName" | providerSet | "value5" | "String" | PropertyLocation.script - "provisioningName" | providerSet | "value6" | "Provider" | PropertyLocation.script - "provisioningName" | setter | "value7" | "String" | PropertyLocation.script - "provisioningName" | setter | "value8" | "Provider" | PropertyLocation.script - "provisioningName" | _ | null | _ | PropertyLocation.none - - "adhoc" | _ | true | _ | PropertyLocation.environment - "adhoc" | _ | true | _ | PropertyLocation.property - "adhoc" | assignment | true | "Boolean" | PropertyLocation.script - "adhoc" | assignment | true | "Provider" | PropertyLocation.script - "adhoc" | providerSet | true | "Boolean" | PropertyLocation.script - "adhoc" | providerSet | true | "Provider" | PropertyLocation.script - "adhoc" | setter | true | "Boolean" | PropertyLocation.script - "adhoc" | setter | true | "Provider" | PropertyLocation.script - "adhoc" | _ | false | _ | PropertyLocation.none - - "publishToTestFlight" | _ | true | _ | PropertyLocation.environment - "publishToTestFlight" | _ | true | _ | PropertyLocation.property - "publishToTestFlight" | assignment | true | "Boolean" | PropertyLocation.script - "publishToTestFlight" | assignment | true | "Provider" | PropertyLocation.script - "publishToTestFlight" | providerSet | true | "Boolean" | PropertyLocation.script - "publishToTestFlight" | providerSet | true | "Provider" | PropertyLocation.script - "publishToTestFlight" | setter | true | "Boolean" | PropertyLocation.script - "publishToTestFlight" | setter | true | "Provider" | PropertyLocation.script - "publishToTestFlight" | _ | false | _ | PropertyLocation.none - - "exportOptionsPlist" | _ | osPath("/path/to/exportOptions.plist") | _ | PropertyLocation.environment - "exportOptionsPlist" | _ | osPath("/path/to/exportOptions.plist") | _ | PropertyLocation.property - "exportOptionsPlist" | assignment | osPath("/path/to/exportOptions.plist") | "File" | PropertyLocation.script - "exportOptionsPlist" | assignment | osPath("/path/to/exportOptions.plist") | "Provider" | PropertyLocation.script - "exportOptionsPlist" | providerSet | osPath("/path/to/exportOptions.plist") | "File" | PropertyLocation.script - "exportOptionsPlist" | providerSet | osPath("/path/to/exportOptions.plist") | "Provider" | PropertyLocation.script - "exportOptionsPlist" | setter | osPath("/path/to/exportOptions.plist") | "File" | PropertyLocation.script - "exportOptionsPlist" | setter | osPath("/path/to/exportOptions.plist") | "Provider" | PropertyLocation.script - "exportOptionsPlist" | _ | projectFile("exportOptions.plist") | _ | PropertyLocation.none - - "preferWorkspace" | _ | false | _ | PropertyLocation.environment - "preferWorkspace" | _ | false | _ | PropertyLocation.property - "preferWorkspace" | assignment | false | "Boolean" | PropertyLocation.script - "preferWorkspace" | assignment | false | "Provider" | PropertyLocation.script - "preferWorkspace" | providerSet | false | "Boolean" | PropertyLocation.script - "preferWorkspace" | providerSet | false | "Provider" | PropertyLocation.script - "preferWorkspace" | setter | false | "Boolean" | PropertyLocation.script - "preferWorkspace" | setter | false | "Provider" | PropertyLocation.script - "preferWorkspace" | _ | true | _ | PropertyLocation.none - - "xcodeProjectDirectory" | _ | osPath("/path/to/project") | _ | PropertyLocation.environment - "xcodeProjectDirectory" | _ | osPath("/path/to/project2") | _ | PropertyLocation.property - "xcodeProjectDirectory" | assignment | osPath("/path/to/project3") | "File" | PropertyLocation.script - "xcodeProjectDirectory" | assignment | osPath("/path/to/project4") | "Provider" | PropertyLocation.script - "xcodeProjectDirectory" | providerSet | osPath("/path/to/project5") | "File" | PropertyLocation.script - "xcodeProjectDirectory" | providerSet | osPath("/path/to/project6") | "Provider" | PropertyLocation.script - "xcodeProjectDirectory" | setter | osPath("/path/to/project7") | "File" | PropertyLocation.script - "xcodeProjectDirectory" | setter | osPath("/path/to/project8") | "Provider" | PropertyLocation.script - "xcodeProjectDirectory" | _ | projectFile("") | _ | PropertyLocation.none - - "xcodeProjectPath" | _ | osPath("/path/to/project") | _ | PropertyLocation.environment - "xcodeProjectPath" | _ | osPath("/path/to/project2") | _ | PropertyLocation.property - "xcodeProjectPath" | assignment | osPath("/path/to/project3") | "File" | PropertyLocation.script - "xcodeProjectPath" | assignment | osPath("/path/to/project4") | "Provider" | PropertyLocation.script - "xcodeProjectPath" | providerSet | osPath("/path/to/project5") | "File" | PropertyLocation.script - "xcodeProjectPath" | providerSet | osPath("/path/to/project6") | "Provider" | PropertyLocation.script - "xcodeProjectPath" | setter | osPath("/path/to/project7") | "File" | PropertyLocation.script - "xcodeProjectPath" | setter | osPath("/path/to/project8") | "Provider" | PropertyLocation.script - "xcodeProjectPath" | _ | projectFile("Unity-iPhone.xcodeproj") | _ | PropertyLocation.none - - "xcodeWorkspacePath" | _ | osPath("/path/to/project") | _ | PropertyLocation.environment - "xcodeWorkspacePath" | _ | osPath("/path/to/project2") | _ | PropertyLocation.property - "xcodeWorkspacePath" | assignment | osPath("/path/to/project3") | "File" | PropertyLocation.script - "xcodeWorkspacePath" | assignment | osPath("/path/to/project4") | "Provider" | PropertyLocation.script - "xcodeWorkspacePath" | providerSet | osPath("/path/to/project5") | "File" | PropertyLocation.script - "xcodeWorkspacePath" | providerSet | osPath("/path/to/project6") | "Provider" | PropertyLocation.script - "xcodeWorkspacePath" | setter | osPath("/path/to/project7") | "File" | PropertyLocation.script - "xcodeWorkspacePath" | setter | osPath("/path/to/project8") | "Provider" | PropertyLocation.script - "xcodeWorkspacePath" | _ | projectFile("Unity-iPhone.xcworkspace") | _ | PropertyLocation.none - - "projectBaseName" | _ | "value1" | _ | PropertyLocation.environment - "projectBaseName" | _ | "value2" | _ | PropertyLocation.property - "projectBaseName" | assignment | "value3" | "String" | PropertyLocation.script - "projectBaseName" | assignment | "value4" | "Provider" | PropertyLocation.script - "projectBaseName" | providerSet | "value5" | "String" | PropertyLocation.script - "projectBaseName" | providerSet | "value6" | "Provider" | PropertyLocation.script - "projectBaseName" | setter | "value7" | "String" | PropertyLocation.script - "projectBaseName" | setter | "value8" | "Provider" | PropertyLocation.script - "projectBaseName" | _ | "Unity-iPhone" | _ | PropertyLocation.none - - "cocoapods.executableName" | _ | "value1" | _ | PropertyLocation.environment - "cocoapods.executableName" | _ | "value2" | _ | PropertyLocation.property - "cocoapods.executableName" | assignment | "value3" | "String" | PropertyLocation.script - "cocoapods.executableName" | assignment | "value4" | "Provider" | PropertyLocation.script - "cocoapods.executableName" | providerSet | "value5" | "String" | PropertyLocation.script - "cocoapods.executableName" | providerSet | "value6" | "Provider" | PropertyLocation.script - "cocoapods.executableName" | setter | "value7" | "String" | PropertyLocation.script - "cocoapods.executableName" | setter | "value8" | "Provider" | PropertyLocation.script - "cocoapods.executableName" | _ | "pod" | _ | PropertyLocation.none - - "cocoapods.executableDirectory" | _ | osPath("/path/to/project") | _ | PropertyLocation.environment - "cocoapods.executableDirectory" | _ | osPath("/path/to/project2") | _ | PropertyLocation.property - "cocoapods.executableDirectory" | assignment | osPath("/path/to/project3") | "File" | PropertyLocation.script - "cocoapods.executableDirectory" | assignment | osPath("/path/to/project4") | "Provider" | PropertyLocation.script - "cocoapods.executableDirectory" | providerSet | osPath("/path/to/project5") | "File" | PropertyLocation.script - "cocoapods.executableDirectory" | providerSet | osPath("/path/to/project6") | "Provider" | PropertyLocation.script - "cocoapods.executableDirectory" | setter | osPath("/path/to/project7") | "File" | PropertyLocation.script - "cocoapods.executableDirectory" | setter | osPath("/path/to/project8") | "Provider" | PropertyLocation.script - "cocoapods.executableDirectory" | _ | null | _ | PropertyLocation.none - - "projectPath" | _ | projectFile("Unity-iPhone.xcodeproj") | "File" | PropertyLocation.none - "xcodeProjectFileName" | _ | "Unity-iPhone.xcodeproj" | "String" | PropertyLocation.none - "xcodeWorkspaceFileName" | _ | "Unity-iPhone.xcworkspace" | "String" | PropertyLocation.none - "preferredProjectFileName" | _ | "Unity-iPhone.xcworkspace" | "String" | PropertyLocation.none + property | invocation | rawValue | type | location + "keychainPassword" | _ | "testPassword1" | _ | PropertyLocation.environment + "keychainPassword" | _ | "testPassword2" | _ | PropertyLocation.property + "keychainPassword" | assignment | "testPassword3" | "String" | PropertyLocation.script + "keychainPassword" | assignment | "testPassword4" | "Provider" | PropertyLocation.script + "keychainPassword" | providerSet | "testPassword5" | "String" | PropertyLocation.script + "keychainPassword" | providerSet | "testPassword6" | "Provider" | PropertyLocation.script + "keychainPassword" | setter | "testPassword7" | "String" | PropertyLocation.script + "keychainPassword" | setter | "testPassword8" | "Provider" | PropertyLocation.script + "keychainPassword" | _ | null | _ | PropertyLocation.none + + "signingIdentities" | _ | TestValue.set("ID1,ID2").expect(["ID1", "ID2"]) | _ | PropertyLocation.environment + "signingIdentities" | _ | TestValue.set("ID1,ID2").expect(["ID1", "ID2"]) | _ | PropertyLocation.property + "signingIdentities" | assignment | ["ID1", "ID2"] | "List" | PropertyLocation.script + "signingIdentities" | assignment | ["ID1", "ID2"] | "Provider>" | PropertyLocation.script + "signingIdentities" | providerSet | ["ID1", "ID2"] | "List" | PropertyLocation.script + "signingIdentities" | providerSet | ["ID1", "ID2"] | "Provider>" | PropertyLocation.script + "signingIdentities" | setter | ["ID1", "ID2"] | "List" | PropertyLocation.script + "signingIdentities" | setter | ["ID1", "ID2"] | "Provider>" | PropertyLocation.script + "signingIdentities" | customSetter("setSigningIdentity") | TestValue.set(wrapValueBasedOnType("code sign: ID3", "String")).expect(["code sign: ID3"]) | "Provider>" | PropertyLocation.script + "signingIdentities" | customSetter("setSigningIdentity") | TestValue.set(wrapValueBasedOnType("code sign: ID3", "String")).expect(["code sign: ID3"]) | "String>" | PropertyLocation.script + "signingIdentities" | _ | [] | _ | PropertyLocation.none + + "codeSigningIdentityFile" | _ | osPath("/path/to/p12") | _ | PropertyLocation.environment + "codeSigningIdentityFile" | _ | osPath("/path/to/p12") | _ | PropertyLocation.property + "codeSigningIdentityFile" | assignment | osPath("/path/to/p12") | "File" | PropertyLocation.script + "codeSigningIdentityFile" | assignment | osPath("/path/to/p12") | "Provider" | PropertyLocation.script + "codeSigningIdentityFile" | providerSet | osPath("/path/to/p12") | "File" | PropertyLocation.script + "codeSigningIdentityFile" | providerSet | osPath("/path/to/p12") | "Provider" | PropertyLocation.script + "codeSigningIdentityFile" | setter | osPath("/path/to/p12") | "File" | PropertyLocation.script + "codeSigningIdentityFile" | setter | osPath("/path/to/p12") | "Provider" | PropertyLocation.script + "codeSigningIdentityFile" | _ | null | _ | PropertyLocation.none + + "codeSigningIdentityFilePassphrase" | _ | "testPassphrase1" | _ | PropertyLocation.environment + "codeSigningIdentityFilePassphrase" | _ | "testPassphrase2" | _ | PropertyLocation.property + "codeSigningIdentityFilePassphrase" | assignment | "testPassphrase3" | "String" | PropertyLocation.script + "codeSigningIdentityFilePassphrase" | assignment | "testPassphrase4" | "Provider" | PropertyLocation.script + "codeSigningIdentityFilePassphrase" | providerSet | "testPassphrase5" | "String" | PropertyLocation.script + "codeSigningIdentityFilePassphrase" | providerSet | "testPassphrase6" | "Provider" | PropertyLocation.script + "codeSigningIdentityFilePassphrase" | setter | "testPassphrase7" | "String" | PropertyLocation.script + "codeSigningIdentityFilePassphrase" | setter | "testPassphrase8" | "Provider" | PropertyLocation.script + "codeSigningIdentityFilePassphrase" | _ | null | _ | PropertyLocation.none + + "appIdentifier" | _ | "com.test.app.1" | _ | PropertyLocation.environment + "appIdentifier" | _ | "com.test.app.2" | _ | PropertyLocation.property + "appIdentifier" | assignment | "com.test.app.3" | "String" | PropertyLocation.script + "appIdentifier" | assignment | "com.test.app.4" | "Provider" | PropertyLocation.script + "appIdentifier" | providerSet | "com.test.app.5" | "String" | PropertyLocation.script + "appIdentifier" | providerSet | "com.test.app.6" | "Provider" | PropertyLocation.script + "appIdentifier" | setter | "com.test.app.7" | "String" | PropertyLocation.script + "appIdentifier" | setter | "com.test.app.8" | "Provider" | PropertyLocation.script + "appIdentifier" | _ | null | _ | PropertyLocation.none + + "provisioningProfileAppId" | _ | "com.test.app.1.*" | _ | PropertyLocation.environment + "provisioningProfileAppId" | _ | "com.test.app.2.*" | _ | PropertyLocation.property + "provisioningProfileAppId" | assignment | "com.test.app.3.*" | "String" | PropertyLocation.script + "provisioningProfileAppId" | assignment | "com.test.app.4.*" | "Provider" | PropertyLocation.script + "provisioningProfileAppId" | providerSet | "com.test.app.5.*" | "String" | PropertyLocation.script + "provisioningProfileAppId" | providerSet | "com.test.app.6.*" | "Provider" | PropertyLocation.script + "provisioningProfileAppId" | setter | "com.test.app.7.*" | "String" | PropertyLocation.script + "provisioningProfileAppId" | setter | "com.test.app.8.*" | "Provider" | PropertyLocation.script + "provisioningProfileAppId" | _ | null | _ | PropertyLocation.none + + "teamId" | _ | "team1" | _ | PropertyLocation.environment + "teamId" | _ | "team2" | _ | PropertyLocation.property + "teamId" | assignment | "team3" | "String" | PropertyLocation.script + "teamId" | assignment | "team4" | "Provider" | PropertyLocation.script + "teamId" | providerSet | "team5" | "String" | PropertyLocation.script + "teamId" | providerSet | "team6" | "Provider" | PropertyLocation.script + "teamId" | setter | "team7" | "String" | PropertyLocation.script + "teamId" | setter | "team8" | "Provider" | PropertyLocation.script + "teamId" | _ | null | _ | PropertyLocation.none + + "scheme" | _ | "value1" | _ | PropertyLocation.environment + "scheme" | _ | "value2" | _ | PropertyLocation.property + "scheme" | assignment | "value3" | "String" | PropertyLocation.script + "scheme" | assignment | "value4" | "Provider" | PropertyLocation.script + "scheme" | providerSet | "value5" | "String" | PropertyLocation.script + "scheme" | providerSet | "value6" | "Provider" | PropertyLocation.script + "scheme" | setter | "value7" | "String" | PropertyLocation.script + "scheme" | setter | "value8" | "Provider" | PropertyLocation.script + "scheme" | _ | null | _ | PropertyLocation.none + + "configuration" | _ | "value1" | _ | PropertyLocation.environment + "configuration" | _ | "value2" | _ | PropertyLocation.property + "configuration" | assignment | "value3" | "String" | PropertyLocation.script + "configuration" | assignment | "value4" | "Provider" | PropertyLocation.script + "configuration" | providerSet | "value5" | "String" | PropertyLocation.script + "configuration" | providerSet | "value6" | "Provider" | PropertyLocation.script + "configuration" | setter | "value7" | "String" | PropertyLocation.script + "configuration" | setter | "value8" | "Provider" | PropertyLocation.script + "configuration" | _ | null | _ | PropertyLocation.none + + "provisioningName" | _ | "value1" | _ | PropertyLocation.environment + "provisioningName" | _ | "value2" | _ | PropertyLocation.property + "provisioningName" | assignment | "value3" | "String" | PropertyLocation.script + "provisioningName" | assignment | "value4" | "Provider" | PropertyLocation.script + "provisioningName" | providerSet | "value5" | "String" | PropertyLocation.script + "provisioningName" | providerSet | "value6" | "Provider" | PropertyLocation.script + "provisioningName" | setter | "value7" | "String" | PropertyLocation.script + "provisioningName" | setter | "value8" | "Provider" | PropertyLocation.script + "provisioningName" | _ | null | _ | PropertyLocation.none + + "provisioningProfiles" | assignment | ["net.test.foo": "Provisoning Name Foo"] | "Map" | PropertyLocation.script + "provisioningProfiles" | providerSet | ["net.test.foo": "Provisoning Name Foo"] | "Map" | PropertyLocation.script + "provisioningProfiles" | providerSet | ["net.test.foo": "Provisoning Name Foo"] | "Provider>" | PropertyLocation.script + "provisioningProfiles" | setter | ["net.test.foo": "Provisoning Name Foo"] | "Map" | PropertyLocation.script + "provisioningProfiles" | setter | ["net.test.foo": "Provisoning Name Foo"] | "Provider>" | PropertyLocation.script + "provisioningProfiles" | _ | [:] | _ | PropertyLocation.none + + "adhoc" | _ | true | _ | PropertyLocation.environment + "adhoc" | _ | true | _ | PropertyLocation.property + "adhoc" | assignment | true | "Boolean" | PropertyLocation.script + "adhoc" | assignment | true | "Provider" | PropertyLocation.script + "adhoc" | providerSet | true | "Boolean" | PropertyLocation.script + "adhoc" | providerSet | true | "Provider" | PropertyLocation.script + "adhoc" | setter | true | "Boolean" | PropertyLocation.script + "adhoc" | setter | true | "Provider" | PropertyLocation.script + "adhoc" | _ | false | _ | PropertyLocation.none + + "publishToTestFlight" | _ | true | _ | PropertyLocation.environment + "publishToTestFlight" | _ | true | _ | PropertyLocation.property + "publishToTestFlight" | assignment | true | "Boolean" | PropertyLocation.script + "publishToTestFlight" | assignment | true | "Provider" | PropertyLocation.script + "publishToTestFlight" | providerSet | true | "Boolean" | PropertyLocation.script + "publishToTestFlight" | providerSet | true | "Provider" | PropertyLocation.script + "publishToTestFlight" | setter | true | "Boolean" | PropertyLocation.script + "publishToTestFlight" | setter | true | "Provider" | PropertyLocation.script + "publishToTestFlight" | _ | false | _ | PropertyLocation.none + + "exportOptionsPlist" | _ | osPath("/path/to/exportOptions.plist") | _ | PropertyLocation.environment + "exportOptionsPlist" | _ | osPath("/path/to/exportOptions.plist") | _ | PropertyLocation.property + "exportOptionsPlist" | assignment | osPath("/path/to/exportOptions.plist") | "File" | PropertyLocation.script + "exportOptionsPlist" | assignment | osPath("/path/to/exportOptions.plist") | "Provider" | PropertyLocation.script + "exportOptionsPlist" | providerSet | osPath("/path/to/exportOptions.plist") | "File" | PropertyLocation.script + "exportOptionsPlist" | providerSet | osPath("/path/to/exportOptions.plist") | "Provider" | PropertyLocation.script + "exportOptionsPlist" | setter | osPath("/path/to/exportOptions.plist") | "File" | PropertyLocation.script + "exportOptionsPlist" | setter | osPath("/path/to/exportOptions.plist") | "Provider" | PropertyLocation.script + "exportOptionsPlist" | _ | projectFile("exportOptions.plist") | _ | PropertyLocation.none + + "preferWorkspace" | _ | false | _ | PropertyLocation.environment + "preferWorkspace" | _ | false | _ | PropertyLocation.property + "preferWorkspace" | assignment | false | "Boolean" | PropertyLocation.script + "preferWorkspace" | assignment | false | "Provider" | PropertyLocation.script + "preferWorkspace" | providerSet | false | "Boolean" | PropertyLocation.script + "preferWorkspace" | providerSet | false | "Provider" | PropertyLocation.script + "preferWorkspace" | setter | false | "Boolean" | PropertyLocation.script + "preferWorkspace" | setter | false | "Provider" | PropertyLocation.script + "preferWorkspace" | _ | true | _ | PropertyLocation.none + + "xcodeProjectDirectory" | _ | osPath("/path/to/project") | _ | PropertyLocation.environment + "xcodeProjectDirectory" | _ | osPath("/path/to/project2") | _ | PropertyLocation.property + "xcodeProjectDirectory" | assignment | osPath("/path/to/project3") | "File" | PropertyLocation.script + "xcodeProjectDirectory" | assignment | osPath("/path/to/project4") | "Provider" | PropertyLocation.script + "xcodeProjectDirectory" | providerSet | osPath("/path/to/project5") | "File" | PropertyLocation.script + "xcodeProjectDirectory" | providerSet | osPath("/path/to/project6") | "Provider" | PropertyLocation.script + "xcodeProjectDirectory" | setter | osPath("/path/to/project7") | "File" | PropertyLocation.script + "xcodeProjectDirectory" | setter | osPath("/path/to/project8") | "Provider" | PropertyLocation.script + "xcodeProjectDirectory" | _ | projectFile("") | _ | PropertyLocation.none + + "xcodeProjectPath" | _ | osPath("/path/to/project") | _ | PropertyLocation.environment + "xcodeProjectPath" | _ | osPath("/path/to/project2") | _ | PropertyLocation.property + "xcodeProjectPath" | assignment | osPath("/path/to/project3") | "File" | PropertyLocation.script + "xcodeProjectPath" | assignment | osPath("/path/to/project4") | "Provider" | PropertyLocation.script + "xcodeProjectPath" | providerSet | osPath("/path/to/project5") | "File" | PropertyLocation.script + "xcodeProjectPath" | providerSet | osPath("/path/to/project6") | "Provider" | PropertyLocation.script + "xcodeProjectPath" | setter | osPath("/path/to/project7") | "File" | PropertyLocation.script + "xcodeProjectPath" | setter | osPath("/path/to/project8") | "Provider" | PropertyLocation.script + "xcodeProjectPath" | _ | projectFile("Unity-iPhone.xcodeproj") | _ | PropertyLocation.none + + "xcodeWorkspacePath" | _ | osPath("/path/to/project") | _ | PropertyLocation.environment + "xcodeWorkspacePath" | _ | osPath("/path/to/project2") | _ | PropertyLocation.property + "xcodeWorkspacePath" | assignment | osPath("/path/to/project3") | "File" | PropertyLocation.script + "xcodeWorkspacePath" | assignment | osPath("/path/to/project4") | "Provider" | PropertyLocation.script + "xcodeWorkspacePath" | providerSet | osPath("/path/to/project5") | "File" | PropertyLocation.script + "xcodeWorkspacePath" | providerSet | osPath("/path/to/project6") | "Provider" | PropertyLocation.script + "xcodeWorkspacePath" | setter | osPath("/path/to/project7") | "File" | PropertyLocation.script + "xcodeWorkspacePath" | setter | osPath("/path/to/project8") | "Provider" | PropertyLocation.script + "xcodeWorkspacePath" | _ | projectFile("Unity-iPhone.xcworkspace") | _ | PropertyLocation.none + + "projectBaseName" | _ | "value1" | _ | PropertyLocation.environment + "projectBaseName" | _ | "value2" | _ | PropertyLocation.property + "projectBaseName" | assignment | "value3" | "String" | PropertyLocation.script + "projectBaseName" | assignment | "value4" | "Provider" | PropertyLocation.script + "projectBaseName" | providerSet | "value5" | "String" | PropertyLocation.script + "projectBaseName" | providerSet | "value6" | "Provider" | PropertyLocation.script + "projectBaseName" | setter | "value7" | "String" | PropertyLocation.script + "projectBaseName" | setter | "value8" | "Provider" | PropertyLocation.script + "projectBaseName" | _ | "Unity-iPhone" | _ | PropertyLocation.none + + "cocoapods.executableName" | _ | "value1" | _ | PropertyLocation.environment + "cocoapods.executableName" | _ | "value2" | _ | PropertyLocation.property + "cocoapods.executableName" | assignment | "value3" | "String" | PropertyLocation.script + "cocoapods.executableName" | assignment | "value4" | "Provider" | PropertyLocation.script + "cocoapods.executableName" | providerSet | "value5" | "String" | PropertyLocation.script + "cocoapods.executableName" | providerSet | "value6" | "Provider" | PropertyLocation.script + "cocoapods.executableName" | setter | "value7" | "String" | PropertyLocation.script + "cocoapods.executableName" | setter | "value8" | "Provider" | PropertyLocation.script + "cocoapods.executableName" | _ | "pod" | _ | PropertyLocation.none + + "cocoapods.executableDirectory" | _ | osPath("/path/to/project") | _ | PropertyLocation.environment + "cocoapods.executableDirectory" | _ | osPath("/path/to/project2") | _ | PropertyLocation.property + "cocoapods.executableDirectory" | assignment | osPath("/path/to/project3") | "File" | PropertyLocation.script + "cocoapods.executableDirectory" | assignment | osPath("/path/to/project4") | "Provider" | PropertyLocation.script + "cocoapods.executableDirectory" | providerSet | osPath("/path/to/project5") | "File" | PropertyLocation.script + "cocoapods.executableDirectory" | providerSet | osPath("/path/to/project6") | "Provider" | PropertyLocation.script + "cocoapods.executableDirectory" | setter | osPath("/path/to/project7") | "File" | PropertyLocation.script + "cocoapods.executableDirectory" | setter | osPath("/path/to/project8") | "Provider" | PropertyLocation.script + "cocoapods.executableDirectory" | _ | null | _ | PropertyLocation.none + + "projectPath" | _ | projectFile("Unity-iPhone.xcodeproj") | "File" | PropertyLocation.none + "xcodeProjectFileName" | _ | "Unity-iPhone.xcodeproj" | "String" | PropertyLocation.none + "xcodeWorkspaceFileName" | _ | "Unity-iPhone.xcworkspace" | "String" | PropertyLocation.none + "preferredProjectFileName" | _ | "Unity-iPhone.xcworkspace" | "String" | PropertyLocation.none set = new PropertySetterWriter(extensionName, property) diff --git a/src/integrationTest/groovy/wooga/gradle/build/unity/ios/tasks/IOSBuildTaskIntegrationSpec.groovy b/src/integrationTest/groovy/wooga/gradle/build/unity/ios/tasks/IOSBuildTaskIntegrationSpec.groovy new file mode 100644 index 0000000..8c65ee5 --- /dev/null +++ b/src/integrationTest/groovy/wooga/gradle/build/unity/ios/tasks/IOSBuildTaskIntegrationSpec.groovy @@ -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 extends IOSBuildIntegrationSpec implements TaskIntegrationSpec { + def setup() { + buildFile << """ + task ${subjectUnderTestName}(type: ${subjectUnderTestTypeName}) + """.stripIndent() + } +} diff --git a/src/integrationTest/groovy/wooga/gradle/build/unity/ios/tasks/ImportCodeSigningIdentitiesIntegrationSpec.groovy b/src/integrationTest/groovy/wooga/gradle/build/unity/ios/tasks/ImportCodeSigningIdentitiesIntegrationSpec.groovy index 69cdba7..0a427fc 100644 --- a/src/integrationTest/groovy/wooga/gradle/build/unity/ios/tasks/ImportCodeSigningIdentitiesIntegrationSpec.groovy +++ b/src/integrationTest/groovy/wooga/gradle/build/unity/ios/tasks/ImportCodeSigningIdentitiesIntegrationSpec.groovy @@ -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 { @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") diff --git a/src/integrationTest/groovy/wooga/gradle/build/unity/ios/tasks/InstallProvisioningProfilesIntegrationSpec.groovy b/src/integrationTest/groovy/wooga/gradle/build/unity/ios/tasks/InstallProvisioningProfilesIntegrationSpec.groovy new file mode 100644 index 0000000..cd05fe8 --- /dev/null +++ b/src/integrationTest/groovy/wooga/gradle/build/unity/ios/tasks/InstallProvisioningProfilesIntegrationSpec.groovy @@ -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 { + + @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" + "outputDirectory" | toProviderSet(property) | "/path/to/outputDir" | _ | "File" + "outputDirectory" | toProviderSet(property) | "/path/to/outputDir" | _ | "Provider" + "outputDirectory" | toSetter(property) | "/path/to/outputDir" | _ | "File" + "outputDirectory" | toSetter(property) | "/path/to/outputDir" | _ | "Provider" + + "logFile" | property | "/path/to/log" | _ | "File" + "logFile" | property | "/path/to/log" | _ | "Provider" + "logFile" | toProviderSet(property) | "/path/to/log" | _ | "File" + "logFile" | toProviderSet(property) | "/path/to/log" | _ | "Provider" + "logFile" | toSetter(property) | "/path/to/log" | _ | "File" + "logFile" | toSetter(property) | "/path/to/log" | _ | "Provider" + + "logToStdout" | toProviderSet(property) | true | _ | "Boolean" + "logToStdout" | toProviderSet(property) | true | _ | "Provider" + "logToStdout" | toSetter(property) | true | _ | "Boolean" + "logToStdout" | toSetter(property) | true | _ | "Provider" + 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(mock, installedProfile) + } + + and: "a future provisioning profile location" + assert !files.any { it.second.exists() } + + and: + appendToSubjectTask(""" + provisioningProfiles.from(${wrapValueBasedOnType(files.collect { it.first }, "List")}) + 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(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")}) + 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] + } +} diff --git a/src/main/groovy/wooga/gradle/build/unity/ios/IOSBuildPlugin.groovy b/src/main/groovy/wooga/gradle/build/unity/ios/IOSBuildPlugin.groovy index b17e34a..84a6cda 100644 --- a/src/main/groovy/wooga/gradle/build/unity/ios/IOSBuildPlugin.groovy +++ b/src/main/groovy/wooga/gradle/build/unity/ios/IOSBuildPlugin.groovy @@ -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 @@ -110,6 +111,25 @@ class IOSBuildPlugin implements Plugin { 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() { @@ -169,6 +189,8 @@ class IOSBuildPlugin implements Plugin { 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() }))) @@ -207,6 +229,17 @@ class IOSBuildPlugin implements Plugin { } }) + project.tasks.withType(InstallProvisionProfiles.class, new Action() { + @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() { @Override void execute(PodInstallTask task) { @@ -294,29 +327,15 @@ class IOSBuildPlugin implements Plugin { }) 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 podInstall = tasks.register("podInstall", PodInstallTask) { it.projectDirectory.set(extension.xcodeProjectDirectory) it.xcodeWorkspaceFileName.set(extension.xcodeWorkspaceFileName) @@ -324,7 +343,7 @@ class IOSBuildPlugin implements Plugin { } 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 })) } diff --git a/src/main/groovy/wooga/gradle/build/unity/ios/IOSBuildPluginExtension.groovy b/src/main/groovy/wooga/gradle/build/unity/ios/IOSBuildPluginExtension.groovy index 18c92a7..63b9c2b 100644 --- a/src/main/groovy/wooga/gradle/build/unity/ios/IOSBuildPluginExtension.groovy +++ b/src/main/groovy/wooga/gradle/build/unity/ios/IOSBuildPluginExtension.groovy @@ -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 @@ -266,6 +267,21 @@ trait IOSBuildPluginExtension extends BaseSpec { }) } + private final MapProperty provisioningProfiles = objects.mapProperty(String, String) + + @Input + MapProperty getProvisioningProfiles() { + provisioningProfiles + } + + void setProvisioningProfiles(Map value) { + provisioningProfiles.set(value) + } + + void setProvisioningProfiles(Provider> value) { + provisioningProfiles.set(value) + } + private final List> exportOptionsActions = [] void exportOptions(Action action) { diff --git a/src/main/groovy/wooga/gradle/build/unity/ios/MobileProvisioning.groovy b/src/main/groovy/wooga/gradle/build/unity/ios/MobileProvisioning.groovy new file mode 100644 index 0000000..50c2dda --- /dev/null +++ b/src/main/groovy/wooga/gradle/build/unity/ios/MobileProvisioning.groovy @@ -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 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 + } +} diff --git a/src/main/groovy/wooga/gradle/build/unity/ios/internal/DefaultMobileProvisioning.groovy b/src/main/groovy/wooga/gradle/build/unity/ios/internal/DefaultMobileProvisioning.groovy new file mode 100644 index 0000000..fedc2a8 --- /dev/null +++ b/src/main/groovy/wooga/gradle/build/unity/ios/internal/DefaultMobileProvisioning.groovy @@ -0,0 +1,95 @@ +package wooga.gradle.build.unity.ios.internal + +import com.dd.plist.NSDictionary +import com.dd.plist.XMLPropertyListParser +import wooga.gradle.build.unity.ios.MobileProvisioning + +class DefaultMobileProvisioning implements MobileProvisioning, GroovyInterceptable { + private static final String UUID_KEY = "UUID" + private static final String APP_ID_NAME_KEY = "AppIDName" + private static final String NAME_KEY = "Name" + private static final String TEAM_IDENTIFIER_KEY = "TeamIdentifier" + private static final String TEAM_NAME_KEY = "TeamName" + private static final String TIME_TO_LIVE_KEY = "TimeToLive" + private static final String VERSION_KEY = "Version" + private static final String CREATION_DATE_KEY= "CreationDate" + private static final String EXPIRATION_DATE_KEY= "ExpirationDate" + private static final String IS_XCODE_MANAGED_KEY= "IsXcodeManaged" + private static final String DEVELOPER_CERTIFICATES_KEY= "DeveloperCertificates" + private static final String DER_ENCODED_PROFILE_KEY = "DER-Encoded-Profile" + private static final String ENTITLEMENTS_KEY = "Entitlements" + + private Map plist = new HashMap() + + DefaultMobileProvisioning(Map data) { + plist = data + } + + private static int PLIST_START_OFFSET = 0x3E + + String getName() { + plist[NAME_KEY] + } + + String getAppIdName() { + plist[APP_ID_NAME_KEY] + } + + String getUuid() { + plist[UUID_KEY] + } + + String getTeamName() { + plist[TEAM_NAME_KEY] + } + + List getTeamIdentifier() { + plist[TEAM_IDENTIFIER_KEY] as List + } + + Date getExpirationDate() { + (Date) plist[EXPIRATION_DATE_KEY] + } + + Date getCreationDate() { + (Date) plist[CREATION_DATE_KEY] + } + + Boolean isExpired() { + expirationDate < new Date() + } + + Integer getTimeToLive() { + Integer.parseInt(plist[TIME_TO_LIVE_KEY].toString()) + } + + Boolean isXcodeManaged() { + Boolean.parseBoolean(plist[IS_XCODE_MANAGED_KEY].toString()) + } + + Integer getVersion() { + Integer.parseInt(plist[VERSION_KEY].toString()) + } + + Map getEntitlements() { + (Map) plist[ENTITLEMENTS_KEY] + } + + static DefaultMobileProvisioning open(InputStream data) { + if (data.skip(PLIST_START_OFFSET) == PLIST_START_OFFSET) { + def lines = data.readLines() + def plistEndIndex = lines.findIndexOf { it.startsWith("") } + lines[plistEndIndex] = "" + def bytes = lines.subList(0, plistEndIndex + 1).join("\n").bytes + + NSDictionary rootDict = (NSDictionary) XMLPropertyListParser.parse(bytes) + return new DefaultMobileProvisioning((Map) rootDict.toJavaObject()) + } else { + throw new IllegalArgumentException("Provided file is to short to be a provisioning profile") + } + } + + static DefaultMobileProvisioning open(File plistFile) { + open(plistFile.newInputStream()) + } +} diff --git a/src/main/groovy/wooga/gradle/build/unity/ios/tasks/InstallProvisionProfiles.groovy b/src/main/groovy/wooga/gradle/build/unity/ios/tasks/InstallProvisionProfiles.groovy new file mode 100644 index 0000000..89ee445 --- /dev/null +++ b/src/main/groovy/wooga/gradle/build/unity/ios/tasks/InstallProvisionProfiles.groovy @@ -0,0 +1,103 @@ +package wooga.gradle.build.unity.ios.tasks + +import com.wooga.gradle.BaseSpec +import com.wooga.gradle.io.LogFileSpec +import com.wooga.gradle.io.ProcessOutputSpec +import org.gradle.api.DefaultTask +import org.gradle.api.file.ConfigurableFileCollection +import org.gradle.api.file.Directory +import org.gradle.api.file.DirectoryProperty +import org.gradle.api.file.FileCollection +import org.gradle.api.provider.Provider +import org.gradle.api.tasks.InputFiles +import org.gradle.api.tasks.Internal +import org.gradle.api.tasks.OutputFiles +import org.gradle.api.tasks.TaskAction +import wooga.gradle.build.unity.ios.MobileProvisioning +import wooga.gradle.build.unity.ios.internal.DefaultMobileProvisioning +import java.io.PrintWriter +import java.text.SimpleDateFormat + +class InstallProvisionProfiles extends DefaultTask implements BaseSpec, LogFileSpec, ProcessOutputSpec { + + private final ConfigurableFileCollection provisioningProfiles = objects.fileCollection() + + @InputFiles + ConfigurableFileCollection getProvisioningProfiles() { + provisioningProfiles + } + + private final DirectoryProperty outputDirectory = objects.directoryProperty() + + @Internal + DirectoryProperty getOutputDirectory() { + outputDirectory + } + + void setOutputDirectory(Provider value) { + outputDirectory.set(value) + } + + void setOutputDirectory(File value) { + outputDirectory.set(value) + } + + @Internal + Provider> getProfiles() { + provisioningProfiles.getElements().map { + it.collect { + DefaultMobileProvisioning.open(it.asFile) + } + } as Provider> + } + + @OutputFiles + protected FileCollection getOutputFiles() { + def files = objects.fileCollection() + files.setFrom(provisioningProfiles.collect { + outputDirectory.file("${readUUIDFromMobileProvision(it)}.mobileprovision") + }) + files + } + + static String readUUIDFromMobileProvision(File input) { + DefaultMobileProvisioning.open(input).uuid + } + + @TaskAction + protected void install() { + def logFile = logFile.asFile.getOrNull() + if(logFile) { + logFile.parentFile.mkdirs() + } + def output = new PrintWriter(getOutputStream(logFile), true) + output.println("Install Profiles: ${provisioningProfiles.size()}") + provisioningProfiles.each { + def profile = DefaultMobileProvisioning.open(it) + SimpleDateFormat outputFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'"); + + output.println("""\ + Install mobile provisioning profile: + ---------------------------------------------------------------- + Name: ${profile.name} + Uuid: ${profile.uuid} + Team: ${profile.teamName}(${profile.teamIdentifier}) + Creation date: ${outputFormat.format(profile.creationDate)} + Expiration date: ${outputFormat.format(profile.expirationDate)} + Is xcode managed: ${profile.isXcodeManaged()} + Entitlements: ${profile.entitlements} + ---------------------------------------------------------------- + """.stripIndent().trim()) + + if (profile.expired) { + logger.warn("profile ${profile.name}(${profile.uuid}) is expired") + output.println("profile ${profile.name}(${profile.uuid}) is expired") + } + + def out = outputDirectory.file("${readUUIDFromMobileProvision(it)}.mobileprovision").get().asFile + output.println("to: '${out.absolutePath}'\n") + out.bytes = it.bytes + } + output.close() + } +} diff --git a/src/test/groovy/com/wooga/gradle/test/ios/MobileProvisionMock.groovy b/src/test/groovy/com/wooga/gradle/test/ios/MobileProvisionMock.groovy new file mode 100644 index 0000000..bad82d2 --- /dev/null +++ b/src/test/groovy/com/wooga/gradle/test/ios/MobileProvisionMock.groovy @@ -0,0 +1,113 @@ +package com.wooga.gradle.test.ios + +import com.dd.plist.NSDate +import com.dd.plist.NSDictionary +import com.dd.plist.XMLPropertyListParser +import groovy.transform.stc.ClosureParams +import groovy.transform.stc.FromString +import wooga.gradle.build.unity.ios.MobileProvisioning + +class MobileProvisionMock implements MobileProvisioning { + + String uuid = UUID.randomUUID() + String appIdName = "Mock AppId" + String name = "Mock Profile" + List teamIdentifier = ["randomId"] + String teamName = "Mock Team" + Integer timeToLive = 265 + Integer version = 1 + + private Date creationDate + + void setCreationDate(Date creationDate) { + this.creationDate = normalizeDate(creationDate) + } + + Date getCreationDate() { + return creationDate + } + + private Date expirationDate + + void setExpirationDate(Date expirationDate) { + this.expirationDate = normalizeDate(expirationDate) + } + + Date getExpirationDate() { + return expirationDate + } + + Boolean xcodeManaged = false + Map entitlements = [:] + + MobileProvisionMock() { + setCreationDate(new Date()) + setExpirationDate(new Date(creationDate.time + 100000)) + } + + MobileProvisionMock(String uuid, String name) { + super() + this.uuid = uuid + this.name = name + } + + @Override + Boolean isExpired() { + expirationDate < new Date() + } + + @Override + Boolean isXcodeManaged() { + xcodeManaged + } + + private Date normalizeDate(Date input) { + def nsDateString = NSDate.fromJavaObject(input).toXMLPropertyList() + ((NSDate) XMLPropertyListParser.parse(nsDateString.bytes)).toJavaObject(Date) + } + + byte[] asBytes() { + def s = new ByteArrayOutputStream() + Random rd = new Random() + byte[] intro = new byte[0x3E] + rd.nextBytes(intro) + //fill first bytes with random bytes + s.write(intro, 0, intro.length) + + //write the provisioning profile plist content + def plistRaw = [ + "UUID" : uuid, + "AppIDName" : appIdName, + "Name" : name, + "TeamIdentifier" : teamIdentifier, + "TeamName" : teamName, + "TimeToLive" : timeToLive, + "Version" : version, + "CreationDate" : creationDate, + "ExpirationDate" : expirationDate, + "IsXcodeManaged" : xcodeManaged, + "DeveloperCertificates": [], + "DER-Encoded-Profile" : "", + "Entitlements" : entitlements, + ] + s.write(NSDictionary.fromJavaObject(plistRaw).toXMLPropertyList().bytes) + + //write other random data as stand in for certificates + byte[] outro = new byte[200] + rd.nextBytes(intro) + //fill first bytes with random bytes + s.write(outro, 0, intro.length) + s.toByteArray() + } + + static File createMock(@ClosureParams(value = FromString.class, options = "com.wooga.gradle.test.ios.MobileProvisionMock") Closure configuration = null) { + def mock = new MobileProvisionMock() + if (configuration) { + configuration.setDelegate(mock) + configuration.call(mock) + } + def file = File.createTempFile("mock", ".mobileprovisioning") + file.bytes = mock.asBytes() + file + } +} diff --git a/src/test/groovy/wooga/gradle/build/unity/ios/internal/MobileProvisioningSpec.groovy b/src/test/groovy/wooga/gradle/build/unity/ios/internal/MobileProvisioningSpec.groovy new file mode 100644 index 0000000..d6185fe --- /dev/null +++ b/src/test/groovy/wooga/gradle/build/unity/ios/internal/MobileProvisioningSpec.groovy @@ -0,0 +1,39 @@ +package wooga.gradle.build.unity.ios.internal + +import com.wooga.gradle.test.ios.MobileProvisionMock +import spock.lang.Shared +import spock.lang.Specification + +class MobileProvisioningSpec extends Specification { + + @Shared + MobileProvisionMock profile + + def setup() { + profile = new MobileProvisionMock() + } + + def "basic mock which can act as a standin for a mobile provision file"() { + given: "some known profile values" + profile.uuid = UUID.randomUUID() + profile.name = "some name" + + + and: "a profile file" + def f = File.createTempFile("test", ".mobileprovisioning") + f.bytes = profile.asBytes() + + when: + def subject = DefaultMobileProvisioning.open(f) + + then: + subject.uuid == profile.uuid + subject.name == profile.name + subject.teamName == profile.teamName + subject.teamIdentifier == profile.teamIdentifier + subject.expirationDate.time == profile.expirationDate.time + subject.creationDate.time == profile.creationDate.time + subject.timeToLive == profile.timeToLive + subject.xcodeManaged == profile.xcodeManaged + } +}