From cb6cc1ff65194d4f982bde4ca67d8015a912ba32 Mon Sep 17 00:00:00 2001 From: Manfred Endres Date: Tue, 19 Jun 2018 17:38:56 +0200 Subject: [PATCH] Add ios task classes and wire them up based on xcode project --- build.gradle | 6 + .../build/unity/UnityBuildPluginConsts.groovy | 20 +- .../{ => unity}/base/BaseBuildPlugin.groovy | 2 +- .../build/unity/ios/IOSBuildPlugin.groovy | 180 ++++++++++ .../unity/ios/IOSBuildPluginExtension.groovy | 57 ++++ .../ios/XCAction.groovy} | 23 +- .../DefaultIOSBuildPluginExtension.groovy | 166 ++++++++++ .../tasks/ImportProvisioningProfile.groovy | 84 +++++ .../build/unity/ios/tasks/KeychainTask.groovy | 178 ++++++++++ .../unity/ios/tasks/ListKeychainTask.groovy | 125 +++++++ .../unity/ios/tasks/LockKeychainTask.groovy | 104 ++++++ .../unity/ios/tasks/XCodeArchiveTask.groovy | 310 ++++++++++++++++++ .../unity/ios/tasks/XCodeExportTask.groovy | 138 ++++++++ .../unity/tasks/UnityBuildPlayerTask.groovy | 1 + .../net.wooga.build-base.properties | 2 +- ...s => net.wooga.build-unity-ios.properties} | 2 +- .../UnityBuildPluginActivationSpec.groovy | 2 +- .../{ => unity}/UnityBuildPluginSpec.groovy | 3 +- 18 files changed, 1376 insertions(+), 27 deletions(-) rename src/main/groovy/wooga/gradle/build/{ => unity}/base/BaseBuildPlugin.groovy (95%) create mode 100644 src/main/groovy/wooga/gradle/build/unity/ios/IOSBuildPlugin.groovy create mode 100644 src/main/groovy/wooga/gradle/build/unity/ios/IOSBuildPluginExtension.groovy rename src/main/groovy/wooga/gradle/build/{ios/IOSBuildPlugin.groovy => unity/ios/XCAction.groovy} (75%) create mode 100644 src/main/groovy/wooga/gradle/build/unity/ios/internal/DefaultIOSBuildPluginExtension.groovy create mode 100644 src/main/groovy/wooga/gradle/build/unity/ios/tasks/ImportProvisioningProfile.groovy create mode 100644 src/main/groovy/wooga/gradle/build/unity/ios/tasks/KeychainTask.groovy create mode 100644 src/main/groovy/wooga/gradle/build/unity/ios/tasks/ListKeychainTask.groovy create mode 100644 src/main/groovy/wooga/gradle/build/unity/ios/tasks/LockKeychainTask.groovy create mode 100644 src/main/groovy/wooga/gradle/build/unity/ios/tasks/XCodeArchiveTask.groovy create mode 100644 src/main/groovy/wooga/gradle/build/unity/ios/tasks/XCodeExportTask.groovy rename src/main/resources/META-INF/gradle-plugins/{net.wooga.build-ios.properties => net.wooga.build-unity-ios.properties} (90%) rename src/test/groovy/wooga/gradle/build/{ => unity}/UnityBuildPluginActivationSpec.groovy (96%) rename src/test/groovy/wooga/gradle/build/{ => unity}/UnityBuildPluginSpec.groovy (98%) diff --git a/build.gradle b/build.gradle index ca743483..f3858e9d 100644 --- a/build.gradle +++ b/build.gradle @@ -33,6 +33,12 @@ pluginBundle { displayName = 'Gradle Unity build plugin' description = 'This plugin provides tasks for exporting platform projects from Unity3D projects' } + + ios { + id = 'net.wooga.build-unity-ios' + displayName = 'Gradle Unity iOS build plugin' + description = 'This plugin provides tasks for building exported iOS projects from Unity3D' + } } } diff --git a/src/main/groovy/wooga/gradle/build/unity/UnityBuildPluginConsts.groovy b/src/main/groovy/wooga/gradle/build/unity/UnityBuildPluginConsts.groovy index 9e4f6e5d..32fbc78c 100644 --- a/src/main/groovy/wooga/gradle/build/unity/UnityBuildPluginConsts.groovy +++ b/src/main/groovy/wooga/gradle/build/unity/UnityBuildPluginConsts.groovy @@ -38,7 +38,7 @@ class UnityBuildPluginConsts { static String DEFAULT_EXPORT_METHOD_NAME = "Wooga.UnityBuild.NewAutomatedBuild.Export" /** - * Gradle property name to set the default value for {@code platforms}. + * Gradle property baseName to set the default value for {@code platforms}. * * @value "unityBuild.platforms" * @see UnityBuildPluginExtension#getPlatforms() @@ -46,7 +46,7 @@ class UnityBuildPluginConsts { static String PLATFORMS_OPTION = "unityBuild.platforms" /** - * Environment variable name to set the default value for {@code platforms}. + * Environment variable baseName to set the default value for {@code platforms}. * * @value "UNITY_BUILD_PLATFORMS" * @see UnityBuildPluginExtension#getPlatforms() @@ -54,21 +54,21 @@ class UnityBuildPluginConsts { static String PLATFORMS_ENV_VAR = "UNITY_BUILD_PLATFORMS" /** - * Gradle property name to set the default value for {@code buildPlatform}. + * Gradle property baseName to set the default value for {@code buildPlatform}. * * @value "unityBuild.platform" */ static String PLATFORM_OPTION = "unityBuild.platform" /** - * Environment variable name to set the default value for {@code buildPlatform}. + * Environment variable baseName to set the default value for {@code buildPlatform}. * * @value "UNITY_BUILD_PLATFORM" */ static String PLATFORM_ENV_VAR = "UNITY_BUILD_PLATFORM" /** - * Gradle property name to set the default value for {@code environments}. + * Gradle property baseName to set the default value for {@code environments}. * * @value "unityBuild.environments" * @see UnityBuildPluginExtension#getEnvironments() @@ -76,7 +76,7 @@ class UnityBuildPluginConsts { static String ENVIRONMENTS_OPTION = "unityBuild.environments" /** - * Environment variable name to set the default value for {@code environments}. + * Environment variable baseName to set the default value for {@code environments}. * * @value "unityBuild.environments" * @see UnityBuildPluginExtension#getEnvironments() @@ -84,21 +84,21 @@ class UnityBuildPluginConsts { static String ENVIRONMENTS_ENV_VAR = "UNITY_BUILD_ENVIRONMENTS" /** - * Gradle property name to set the default value for {@code buildEnvironment}. + * Gradle property baseName to set the default value for {@code buildEnvironment}. * * @value "unityBuild.environment" */ static String ENVIRONMENT_OPTION = "unityBuild.environment" /** - * Environment variable name to set the default value for {@code buildEnvironment}. + * Environment variable baseName to set the default value for {@code buildEnvironment}. * * @value "UNITY_BUILD_ENVIRONMENT" */ static String ENVIRONMENT_ENV_VAR = "UNITY_BUILD_ENVIRONMENT" /** - * Gradle property name to set the default value for {@code exportMethodName}. + * Gradle property baseName to set the default value for {@code exportMethodName}. * * @value "unityBuild.exportMethodName" * @see UnityBuildPluginExtension#getExportMethodName() @@ -114,7 +114,7 @@ class UnityBuildPluginConsts { static String EXPORT_METHOD_NAME_ENV_VAR = "UNITY_BUILD_EXPORT_METHOD_NAME" /** - * Gradle property name to set the default value for {@code toolsVersion}. + * Gradle property baseName to set the default value for {@code toolsVersion}. * * @value "unityBuild.toolsVersion" * @see UnityBuildPluginExtension#getToolsVersion() diff --git a/src/main/groovy/wooga/gradle/build/base/BaseBuildPlugin.groovy b/src/main/groovy/wooga/gradle/build/unity/base/BaseBuildPlugin.groovy similarity index 95% rename from src/main/groovy/wooga/gradle/build/base/BaseBuildPlugin.groovy rename to src/main/groovy/wooga/gradle/build/unity/base/BaseBuildPlugin.groovy index a1980f7b..357f85f1 100644 --- a/src/main/groovy/wooga/gradle/build/base/BaseBuildPlugin.groovy +++ b/src/main/groovy/wooga/gradle/build/unity/base/BaseBuildPlugin.groovy @@ -15,7 +15,7 @@ * */ -package wooga.gradle.build.base +package wooga.gradle.build.unity.base import org.gradle.api.Plugin import org.gradle.api.Project diff --git a/src/main/groovy/wooga/gradle/build/unity/ios/IOSBuildPlugin.groovy b/src/main/groovy/wooga/gradle/build/unity/ios/IOSBuildPlugin.groovy new file mode 100644 index 00000000..025e8bd1 --- /dev/null +++ b/src/main/groovy/wooga/gradle/build/unity/ios/IOSBuildPlugin.groovy @@ -0,0 +1,180 @@ +/* + * Copyright 2017 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 wooga.gradle.build.unity.ios + +import org.gradle.api.Action +import org.gradle.api.Plugin +import org.gradle.api.Project +import org.gradle.api.logging.Logger +import org.gradle.api.logging.Logging +import org.gradle.api.plugins.BasePlugin +import org.gradle.api.tasks.bundling.Zip +import org.gradle.util.GUtil +import wooga.gradle.build.unity.ios.internal.DefaultIOSBuildPluginExtension +import wooga.gradle.build.unity.ios.tasks.ImportProvisioningProfile +import wooga.gradle.build.unity.ios.tasks.KeychainTask +import wooga.gradle.build.unity.ios.tasks.ListKeychainTask +import wooga.gradle.build.unity.ios.tasks.LockKeychainTask +import wooga.gradle.build.unity.ios.tasks.XCodeArchiveTask +import wooga.gradle.build.unity.ios.tasks.XCodeExportTask + +class IOSBuildPlugin implements Plugin { + + private static final Logger LOG = Logging.getLogger(IOSBuildPlugin.class) + static final String EXTENSION_NAME = "iosBuild" + + @Override + void apply(Project project) { + //check if system is running mac os + String osName = System.getProperty("os.name").toLowerCase() + if (!osName.contains('mac os')) { + LOG.warn("This plugin is only supported on Mac OS systems.") + return + } + + project.pluginManager.apply(BasePlugin.class) + def extension = project.getExtensions().create(IOSBuildPluginExtension, EXTENSION_NAME, DefaultIOSBuildPluginExtension.class) + + //register some defaults + project.tasks.withType(XCodeArchiveTask.class, new Action() { + @Override + void execute(XCodeArchiveTask task) { + def conventionMapping = task.getConventionMapping() + conventionMapping.map("version", { project.version }) + conventionMapping.map("clean", { false }) + conventionMapping.map("destinationDir", { + project.file("${project.buildDir}/outputs/archives") + }) + conventionMapping.map("baseName", { project.name }) + conventionMapping.map("extension", { "xcarchive" }) + conventionMapping.map("scheme", { extension.getScheme() }) + conventionMapping.map("configuration", { extension.getConfiguration() }) + } + }) + + project.tasks.withType(KeychainTask.class, new Action() { + @Override + void execute(KeychainTask task) { + def conventionMapping = task.getConventionMapping() + conventionMapping.map("baseName", { "build" }) + conventionMapping.map("extension", { "keychain" }) + conventionMapping.map("password", { extension.getKeychainPassword() }) + conventionMapping.map("certificatePassword", { extension.getCertificatePassphrase() }) + conventionMapping.map("destinationDir", { + project.file("${project.buildDir}/sign/keychains") + }) + } + }) + + project.tasks.withType(ImportProvisioningProfile.class, new Action() { + @Override + void execute(ImportProvisioningProfile task) { + def conventionMapping = task.getConventionMapping() + conventionMapping.map("username", { extension.fastlaneCredentials.username }) + conventionMapping.map("password", { extension.fastlaneCredentials.password }) + conventionMapping.map("teamId", { extension.getTeamId() }) + conventionMapping.map("appIdentifier", { extension.getAppIdentifier() }) + } + }) + + def projects = project.fileTree(project.projectDir) { it.include("*.xcodeproj/project.pbxproj") }.files + projects.each { File xcodeProject -> + def base = xcodeProject.parentFile + def taskNameBase = base.name.replace('.xcodeproj', '').toLowerCase().replaceAll(/[-_.]/, '') + if (projects.size() == 1) { + taskNameBase = "" + } + generateBuildTasks(taskNameBase, project, base) + } + } + + private static String maybeBaseName(String baseName, String taskName) { + if (GUtil.isTrue(taskName)) { + if (GUtil.isTrue(baseName)) { + return baseName + taskName.capitalize() + } else { + return taskName + } + } + return "" + } + + void generateBuildTasks(final String baseName, final Project project, File xcodeProject) { + def tasks = project.tasks + def buildKeychain = tasks.create(maybeBaseName(baseName, "buildKeychain"), KeychainTask) { + it.baseName = maybeBaseName(baseName, "build") + it.certificates = project.fileTree(project.projectDir) { it.include("*.p12") } + } + + def unlockKeychain = tasks.create(maybeBaseName(baseName, "unlockKeychain"), LockKeychainTask) { + it.lockAction = LockKeychainTask.LockAction.unlock + it.password = { buildKeychain.getPassword() } + it.keychain = buildKeychain + } + + def lockKeychain = tasks.create(maybeBaseName(baseName, "lockKeychain"), LockKeychainTask) { + it.lockAction = LockKeychainTask.LockAction.lock + it.password = { buildKeychain.getPassword() } + it.keychain = buildKeychain + } + + def addKeychain = tasks.create(maybeBaseName(baseName, "addKeychain"), ListKeychainTask) { + it.action = ListKeychainTask.Action.add + it.keychain buildKeychain + } + + def removeKeychain = tasks.create(maybeBaseName(baseName, "removeKeychain"), ListKeychainTask) { + it.action = ListKeychainTask.Action.remove + it.keychain buildKeychain + } + + def importProvisioningProfiles = tasks.create(maybeBaseName(baseName, "importProvisioningProfiles"), ImportProvisioningProfile) { + it.dependsOn addKeychain, unlockKeychain + it.mobileProvisioningProfile = project.file("${maybeBaseName(baseName, 'ci')}.mobileprovision") + } + + def xcodeArchive = tasks.create(maybeBaseName(baseName, "xcodeArchive"), XCodeArchiveTask) { + it.dependsOn unlockKeychain + it.finalizedBy lockKeychain + + it.provisioningProfile = importProvisioningProfiles + it.projectPath = xcodeProject + it.buildKeychain = buildKeychain + } + + def xcodeExport = tasks.create(maybeBaseName(baseName, "xcodeExport"), XCodeExportTask) { + it.exportOptionsPlist project.file("exportOptions.plist") + it.archivePath xcodeArchive + } + + def archiveDSYM = tasks.create(maybeBaseName(baseName, "archiveDSYM"), Zip) { + it.dependsOn xcodeArchive + it.archiveName = xcodeArchive.archiveName.replace(xcodeArchive.extension, 'zip') + it.destinationDir = project.file("${project.buildDir}/outputs") + it.from(project.file("$xcodeArchive.outputs.files.singleFile/dSYMs")) + } + + project.artifacts { + archives(xcodeExport.artifact) + archives(archiveDSYM) + } + + archiveDSYM.mustRunAfter xcodeExport // not to spend time archiving if export fails + project.tasks.getByName(BasePlugin.ASSEMBLE_TASK_NAME).dependsOn xcodeExport, archiveDSYM, removeKeychain + } +} diff --git a/src/main/groovy/wooga/gradle/build/unity/ios/IOSBuildPluginExtension.groovy b/src/main/groovy/wooga/gradle/build/unity/ios/IOSBuildPluginExtension.groovy new file mode 100644 index 00000000..802c49e1 --- /dev/null +++ b/src/main/groovy/wooga/gradle/build/unity/ios/IOSBuildPluginExtension.groovy @@ -0,0 +1,57 @@ +/* + * Copyright 2017 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 wooga.gradle.build.unity.ios + +import org.gradle.api.Action +import org.gradle.api.credentials.PasswordCredentials + +interface IOSBuildPluginExtension { + + PasswordCredentials getFastlaneCredentials() + void setFastlaneCredentials(PasswordCredentials cred) + + IOSBuildPluginExtension fastlaneCredentials(Closure configuration) + IOSBuildPluginExtension fastlaneCredentials(Action action) + IOSBuildPluginExtension fastlaneCredentials(PasswordCredentials cred) + + + String getKeychainPassword() + void setKeychainPassword(String value) + IOSBuildPluginExtension keychainPassword(String password) + + String getCertificatePassphrase() + void setCertificatePassphrase(String passphrase) + IOSBuildPluginExtension certificatePassphrase(String passphrase) + + String getAppIdentifier() + void setAppIdentifier(String identifier) + IOSBuildPluginExtension appIdentifier(String identifier) + + String getTeamId() + void setTeamId(String id) + IOSBuildPluginExtension teamId(String id) + + String getScheme() + void setScheme(String scheme) + IOSBuildPluginExtension scheme(String scheme) + + String getConfiguration() + void setConfiguration(String configuration) + IOSBuildPluginExtension configuration(String configuration) + +} \ No newline at end of file diff --git a/src/main/groovy/wooga/gradle/build/ios/IOSBuildPlugin.groovy b/src/main/groovy/wooga/gradle/build/unity/ios/XCAction.groovy similarity index 75% rename from src/main/groovy/wooga/gradle/build/ios/IOSBuildPlugin.groovy rename to src/main/groovy/wooga/gradle/build/unity/ios/XCAction.groovy index 9c9f0e76..9916c0d8 100644 --- a/src/main/groovy/wooga/gradle/build/ios/IOSBuildPlugin.groovy +++ b/src/main/groovy/wooga/gradle/build/unity/ios/XCAction.groovy @@ -15,15 +15,16 @@ * */ -package wooga.gradle.build.ios +package wooga.gradle.build.unity.ios -import org.gradle.api.Plugin -import org.gradle.api.Project - -class IOSBuildPlugin implements Plugin { - - @Override - void apply(Project project) { - - } -} +enum XCAction { + build, + buildForTesting, + analyze, + archive, + test, + testWithoutBuilding, + installSrc, + install, + clean +} \ No newline at end of file diff --git a/src/main/groovy/wooga/gradle/build/unity/ios/internal/DefaultIOSBuildPluginExtension.groovy b/src/main/groovy/wooga/gradle/build/unity/ios/internal/DefaultIOSBuildPluginExtension.groovy new file mode 100644 index 00000000..f2177d67 --- /dev/null +++ b/src/main/groovy/wooga/gradle/build/unity/ios/internal/DefaultIOSBuildPluginExtension.groovy @@ -0,0 +1,166 @@ +/* + * Copyright 2017 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 wooga.gradle.build.unity.ios.internal + +import org.gradle.api.Action +import org.gradle.api.credentials.PasswordCredentials +import org.gradle.api.internal.artifacts.repositories.DefaultPasswordCredentials + +import wooga.gradle.build.unity.ios.IOSBuildPluginExtension +import static org.gradle.util.ConfigureUtil.configureUsing + +class DefaultIOSBuildPluginExtension implements IOSBuildPluginExtension { + + private final PasswordCredentials fastlaneCredentials + private String keychainPassword + private String certificatePassphrase + private String applicationIdentifier + private String teamId + private String scheme + private String configuration + + @Override + PasswordCredentials getFastlaneCredentials() { + fastlaneCredentials + } + + @Override + void setFastlaneCredentials(PasswordCredentials cred) { + fastlaneCredentials.setUsername(cred.username) + fastlaneCredentials.setPassword(cred.password) + this + } + + @Override + IOSBuildPluginExtension fastlaneCredentials(Closure closure) { + fastlaneCredentials(configureUsing(closure)) + this + } + + @Override + IOSBuildPluginExtension fastlaneCredentials(Action action) { + action.execute(fastlaneCredentials) + this + } + + @Override + IOSBuildPluginExtension fastlaneCredentials(PasswordCredentials cred) { + setFastlaneCredentials(cred) + this + } + + @Override + String getKeychainPassword() { + keychainPassword + } + + @Override + void setKeychainPassword(String value) { + keychainPassword = value + } + + @Override + IOSBuildPluginExtension keychainPassword(String password) { + setKeychainPassword(password) + this + } + + @Override + String getCertificatePassphrase() { + certificatePassphrase + } + + @Override + void setCertificatePassphrase(String passphrase) { + certificatePassphrase = passphrase + } + + @Override + IOSBuildPluginExtension certificatePassphrase(String passphrase) { + setCertificatePassphrase(passphrase) + this + } + + @Override + String getAppIdentifier() { + return applicationIdentifier + } + + @Override + void setAppIdentifier(String identifier) { + applicationIdentifier = identifier + } + + @Override + IOSBuildPluginExtension appIdentifier(String identifier) { + setAppIdentifier(identifier) + return this + } + + @Override + String getTeamId() { + return teamId + } + + @Override + void setTeamId(String id) { + teamId = id + } + + @Override + IOSBuildPluginExtension teamId(String id) { + setTeamId(id) + return this + } + + @Override + String getScheme() { + scheme + } + + @Override + void setScheme(String scheme) { + this.scheme = scheme + } + + @Override + IOSBuildPluginExtension scheme(String scheme) { + setScheme(scheme) + this + } + + @Override + String getConfiguration() { + configuration + } + + @Override + void setConfiguration(String configuration) { + this.configuration = configuration + } + + @Override + IOSBuildPluginExtension configuration(String configuration) { + setConfiguration(configuration) + this + } + + DefaultIOSBuildPluginExtension() { + fastlaneCredentials = new DefaultPasswordCredentials() + } +} diff --git a/src/main/groovy/wooga/gradle/build/unity/ios/tasks/ImportProvisioningProfile.groovy b/src/main/groovy/wooga/gradle/build/unity/ios/tasks/ImportProvisioningProfile.groovy new file mode 100644 index 00000000..337a473c --- /dev/null +++ b/src/main/groovy/wooga/gradle/build/unity/ios/tasks/ImportProvisioningProfile.groovy @@ -0,0 +1,84 @@ +/* + * Copyright 2017 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 wooga.gradle.build.unity.ios.tasks + +import org.gradle.api.file.FileCollection +import org.gradle.api.internal.ConventionTask +import org.gradle.api.tasks.Input +import org.gradle.api.tasks.Optional +import org.gradle.api.tasks.OutputFile +import org.gradle.api.tasks.OutputFiles +import org.gradle.api.tasks.TaskAction + +class ImportProvisioningProfile extends ConventionTask { + + @Input + String appIdentifier + + @Input + String teamId + + @Optional + @Input + String username + + @Optional + @Input + String password + + private Object mobileProvisioningProfile + + @OutputFile + File getMobileProvisioningProfile() { + project.files(mobileProvisioningProfile).singleFile + } + + @OutputFiles + FileCollection getOutputFiles() { + project.files(mobileProvisioningProfile) + } + + void setMobileProvisioningProfile(Object profile) { + mobileProvisioningProfile = profile + } + + ImportProvisioningProfile mobileProvisioningProfile(Object profile) { + mobileProvisioningProfile = profile + this + } + + @TaskAction + protected void importProfilesl() { + project.exec { + executable "fastlane" + args "sigh" + if (password) { + environment('FASTLANE_PASSWORD', password) + } + + if (username) { + args "--username", username + } + + args "--team_id", teamId + args "--app_identifier", appIdentifier + args "--filename", getMobileProvisioningProfile().getName() + args "--output_path", getMobileProvisioningProfile().parentFile + } + } +} diff --git a/src/main/groovy/wooga/gradle/build/unity/ios/tasks/KeychainTask.groovy b/src/main/groovy/wooga/gradle/build/unity/ios/tasks/KeychainTask.groovy new file mode 100644 index 00000000..5a26f4c6 --- /dev/null +++ b/src/main/groovy/wooga/gradle/build/unity/ios/tasks/KeychainTask.groovy @@ -0,0 +1,178 @@ +/* + * Copyright 2017 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 wooga.gradle.build.unity.ios.tasks + +import org.gradle.api.file.FileCollection +import org.gradle.api.internal.ConventionTask +import org.gradle.api.tasks.* + +import java.nio.charset.StandardCharsets +import java.security.MessageDigest + +class KeychainTask extends ConventionTask { + + private String extension + + @Internal + String getExtension() { + extension + } + + void setExtension(String value) { + extension = value + } + + KeychainTask extension(String extension) { + setExtension(extension) + this + } + + private String baseName + + @Internal + String getBaseName() { + baseName + } + + void setBaseName(String value) { + baseName = value + } + + KeychainTask baseName(String baseName) { + setBaseName(baseName) + this + } + + @Input + String getKeychainName() { + getBaseName() + "." + getExtension() + } + + private String password + + @Input + String getPassword() { + password + } + + void setPassword(String value) { + password = value + } + + KeychainTask password(String password) { + setPassword(password) + this + } + + private String certificatePassword + + @Input + String getCertificatePassword() { + certificatePassword + } + + void setCertificatePassword(String value) { + certificatePassword = value + } + + KeychainTask certificatePassword(String certificatePassword) { + setCertificatePassword(certificatePassword) + this + } + + @SkipWhenEmpty + @InputFiles + FileCollection certificates + + private File destinationDir + + @Internal + File getDestinationDir() { + destinationDir + } + + void setDestinationDir(File value) { + destinationDir = value + } + + KeychainTask destinationDir(File destinationDir) { + setDestinationDir(destinationDir) + this + } + + @OutputFile + File getOutputPath() { + new File(getDestinationDir(), getKeychainName()) + } + + @Internal + File getTempKeychainPath() { + new File(temporaryDir, getKeychainName()) + } + + @Internal + File getTempLockFile() { + MessageDigest digest = MessageDigest.getInstance("SHA-1") + digest.update(getKeychainName().getBytes("ASCII")) + byte[] passwordDigest = digest.digest() + String hexString = passwordDigest.collect { String.format('%02x', it) }.join() + new File(temporaryDir, ".fl${hexString.substring(0,8).toUpperCase()}") + } + + @TaskAction + protected void keychain() { + + if(getTempKeychainPath().exists()) { + getTempKeychainPath().delete() + } + + List commands = new ArrayList() + commands << "create-keychain -p ${getPassword()} ${getTempKeychainPath()}" + commands << "unlock-keychain -p ${getPassword()} ${getTempKeychainPath()}" + commands << "set-keychain-settings ${getTempKeychainPath()}" + + certificates.files.each { File file -> + def keychain = getTempKeychainPath() + def password = getCertificatePassword() + commands << "" + commands << "import $file -k $keychain -P $password -f pkcs12 -t cert -T /usr/bin/codesign" + commands << "" + } + + logger.info("Run scurity tasks:") + logger.info(commands.join("\n")) + + project.exec { + executable "security" + args "-i" + standardInput = new ByteArrayInputStream(commands.join("\n").getBytes(StandardCharsets.UTF_8)) + } + + def extension = getExtension() + //move + project.sync { + from temporaryDir + include "*.$extension" + into getDestinationDir() + } + + //delete + getTempLockFile().deleteOnExit() + getTempLockFile().delete() + } +} diff --git a/src/main/groovy/wooga/gradle/build/unity/ios/tasks/ListKeychainTask.groovy b/src/main/groovy/wooga/gradle/build/unity/ios/tasks/ListKeychainTask.groovy new file mode 100644 index 00000000..d9614e40 --- /dev/null +++ b/src/main/groovy/wooga/gradle/build/unity/ios/tasks/ListKeychainTask.groovy @@ -0,0 +1,125 @@ +/* + * Copyright 2017 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 wooga.gradle.build.unity.ios.tasks + +import org.gradle.api.DefaultTask +import org.gradle.api.file.FileCollection +import org.gradle.api.specs.Spec +import org.gradle.api.tasks.Input +import org.gradle.api.tasks.InputFiles +import org.gradle.api.tasks.TaskAction +import org.gradle.util.GUtil + +class ListKeychainTask extends DefaultTask { + + enum Action { + add, + remove + } + + private Action action + private List activeKeychains + private List keychains = new ArrayList() + + @Input + Action getAction() { + action + } + + void setAction(Object action) { + this.action = action as Action + } + + ListKeychainTask action(Object action) { + setAction(action) + this + } + + @InputFiles + FileCollection getKeychains() { + project.files(*keychains.toArray()) + } + + void setKeychains(Iterable keychains) { + this.keychains.removeAll() + this.keychains(keychains) + } + + ListKeychainTask keychains(Object... keychains) { + this.keychains.addAll(Arrays.asList(keychains)) + this + } + + ListKeychainTask keychains(Iterable keychains) { + GUtil.addToCollection(this.keychains, keychains) + this + } + + ListKeychainTask keychain(Object keychain) { + keychains.add(keychain) + this + } + + ListKeychainTask() { + super() + outputs.upToDateWhen(new Spec() { + @Override + boolean isSatisfiedBy(ListKeychainTask element) { + def activeKeychains = element.getActiveKeychains() + def contains = activeKeychains.containsAll(getKeychains().getFiles().collect { it.path }) + return contains == (getAction() == Action.add) + } + }) + } + + protected List getActiveKeychains() { + if (!activeKeychains) { + def listOutput = new ByteArrayOutputStream() + project.exec { + executable "security" + args "list-keychains" + args "-d", "user" + standardOutput = listOutput + } + activeKeychains = listOutput.toString().readLines().collect { it.replace('"', '').trim() } + } + activeKeychains + } + + @TaskAction + protected list() { + def activeKeychains = getActiveKeychains().clone() + def keychains = getKeychains().files.collect { it.path } + + if (getAction() == Action.add) { + activeKeychains.addAll(keychains) + } else { + activeKeychains.removeAll(keychains) + } + + project.exec { + executable "security" + args "list-keychains" + args "-d", "user" + args "-s" + activeKeychains.unique().each { + args it + } + } + } +} diff --git a/src/main/groovy/wooga/gradle/build/unity/ios/tasks/LockKeychainTask.groovy b/src/main/groovy/wooga/gradle/build/unity/ios/tasks/LockKeychainTask.groovy new file mode 100644 index 00000000..4418e653 --- /dev/null +++ b/src/main/groovy/wooga/gradle/build/unity/ios/tasks/LockKeychainTask.groovy @@ -0,0 +1,104 @@ +/* + * Copyright 2017 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 wooga.gradle.build.unity.ios.tasks + +import org.gradle.api.DefaultTask +import org.gradle.api.file.FileCollection +import org.gradle.api.tasks.Input +import org.gradle.api.tasks.InputFile +import org.gradle.api.tasks.InputFiles +import org.gradle.api.tasks.SkipWhenEmpty +import org.gradle.api.tasks.TaskAction + +import java.util.concurrent.Callable + +class LockKeychainTask extends DefaultTask { + + enum LockAction { + lock, + unlock + } + + private LockAction action + private Object keychain + + @Input + LockAction getAction() { + action + } + + void setLockAction(Object action) { + this.action = action as LockAction + } + + LockKeychainTask lockAction(Object action) { + setLockAction(action) + this + } + + private Object password + + @Input + String getPassword() { + if(Callable.isInstance(password)) { + return ((Callable)password).call().toString() + } + + password.toString() + } + + void setPassword(Object value) { + password = value + } + + LockKeychainTask password(Object password) { + setPassword(password) + this + } + + @SkipWhenEmpty + @InputFiles + protected FileCollection getInputFiles() { + project.files(keychain) + } + + @InputFile + File getKeychain() { + project.files(keychain).getSingleFile() + } + + void setKeychain(Object keyChain) { + keychain = keyChain + } + + LockKeychainTask keychain(Object keyChain) { + setKeychain(keyChain) + } + + @TaskAction + protected void unlock() { + project.exec { + executable "security" + args "${getAction()}-keychain" + if(getAction() == LockAction.unlock) { + args "-p", getPassword() + } + args getKeychain() + } + } +} diff --git a/src/main/groovy/wooga/gradle/build/unity/ios/tasks/XCodeArchiveTask.groovy b/src/main/groovy/wooga/gradle/build/unity/ios/tasks/XCodeArchiveTask.groovy new file mode 100644 index 00000000..7adb2c63 --- /dev/null +++ b/src/main/groovy/wooga/gradle/build/unity/ios/tasks/XCodeArchiveTask.groovy @@ -0,0 +1,310 @@ +/* + * Copyright 2017 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 wooga.gradle.build.unity.ios.tasks + +import org.gradle.api.file.FileCollection +import org.gradle.api.internal.ConventionTask +import org.gradle.api.tasks.* +import org.gradle.util.GUtil +import wooga.gradle.build.unity.ios.XCAction + +class XCodeArchiveTask extends ConventionTask { + + private Object projectPath + private Object buildKeychain + private Object provisioningProfile + + private Object destinationDir + private String customName + + private Boolean clean + + @Input + Boolean getClean() { + clean + } + + void setClean(Boolean value) { + clean = value + } + + XCodeArchiveTask clean(Boolean clean) { + setClean(clean) + this + } + + @Optional + @InputFiles + protected FileCollection getInputFiles() { + def files = [projectPath, buildKeychain, provisioningProfile].findAll {it != null} + project.files(*files.toArray()) + } + + @Optional + @Input + Set getBuildActions() { + def s = new HashSet() + s << XCAction.archive + + if(getClean()) { + s << XCAction.clean + } + s + } + + @Optional + @InputDirectory + File getProjectPath() { + project.files(projectPath).getSingleFile() + } + + void setProjectPath(Object path) { + projectPath = path + } + + XCodeArchiveTask projectPath(Object path) { + setExportOptionsPlist(path) + } + + @Optional + @InputFile + File getBuildKeychain() { + project.files(buildKeychain).getSingleFile() + } + + void setBuildKeychain(Object keyChain) { + buildKeychain = keyChain + } + + XCodeArchiveTask buildKeychain(Object keyChain) { + setBuildKeychain(keyChain) + } + + @Optional + @InputFile + File getProvisioningProfile() { + project.files(provisioningProfile).getSingleFile() + } + + void setProvisioningProfile(Object profile) { + provisioningProfile = profile + } + + XCodeArchiveTask provisioningProfile(Object profile) { + setProvisioningProfile(profile) + } + + private String scheme + + @Optional + @Input + String getScheme() { + scheme + } + + void setScheme(String scheme) { + this.scheme = scheme + } + + XCodeArchiveTask scheme(String scheme) { + setScheme(scheme) + this + } + + private String configuration + + @Input + String getConfiguration() { + configuration + } + + void setConfiguration(String value) { + configuration = value + } + + XCodeArchiveTask configuration(String configuration) { + setConfiguration(configuration) + this + } + + @Internal("Represented as part of archivePath") + String getArchiveName() { + if (customName != null) { + return customName + } + String name = GUtil.elvis(getBaseName(), "") + maybe(getBaseName(), getAppendix()) + name += maybe(name, getVersion().toString()) + name += maybe(name, getClassifier()) + name += GUtil.isTrue(getExtension()) ? "." + getExtension() : "" + return name + } + + private static String maybe(String prefix, String value) { + if (GUtil.isTrue(value)) { + if (GUtil.isTrue(prefix)) { + return "-".concat(value) + } else { + return value + } + } + return "" + } + + private String baseName + + @Internal("Represented as part of archivePath") + String getBaseName() { + baseName + } + + void setBaseName(String value) { + baseName = value + } + + XCodeArchiveTask baseName(String baseName) { + setBaseName(baseName) + this + } + + private String appendix + + @Internal("Represented as part of archivePath") + String getAppendix() { + appendix + } + + void setAppendix(String value) { + appendix = value + } + + XCodeArchiveTask appendix(String appendix) { + setAppendix(appendix) + this + } + + private String version + + @Internal("Represented as part of archivePath") + String getVersion() { + version + } + + void setVersion(String value) { + version = value + } + + XCodeArchiveTask version(String version) { + setVersion(version) + this + } + + private String extension + + @Internal("Represented as part of archivePath") + String getExtension() { + extension + } + + void setExtension(String value) { + extension = value + } + + XCodeArchiveTask extension(String extension) { + setExtension(extension) + this + } + + private String classifier + + @Internal("Represented as part of archivePath") + String getClassifier() { + classifier + } + + void setClassifier(String value) { + classifier = value + } + + XCodeArchiveTask classifier(String classifier) { + setClassifier(classifier) + this + } + + @OutputDirectory + File getArchivePath() { + return new File(getDestinationDir(), getArchiveName()) + } + + @Internal("Represented as part of archivePath") + File getDestinationDir() { + if(!destinationDir) { + return temporaryDir + } + project.file(destinationDir) + } + + void setDestinationDir(Object destinationDir) { + this.destinationDir = destinationDir + } + + XCodeArchiveTask destinationDir(Object destinationDir) { + setDestinationDir(destinationDir) + this + } + + @TaskAction + protected executeXcodeBuild() { + List arguments = new ArrayList() + arguments << "xcodebuild" + + getBuildActions().each { + arguments << it.toString() + } + + if(getProjectPath()) { + arguments << "-project" << getProjectPath().getPath() + } + + if(getScheme()) { + arguments << "-scheme" << getScheme() + } + + if(getConfiguration()) { + arguments << "-configuration" << getConfiguration() + } + + if(getBuildKeychain()) { + arguments << "OTHER_CODE_SIGN_FLAGS=--keychain ${getBuildKeychain()}" + } + + arguments << "-archivePath" << getArchivePath().getPath() + def derivedDataPath = new File(getTemporaryDir(),"derivedData") + derivedDataPath.mkdirs() + arguments << "-derivedDataPath" << derivedDataPath + + project.exec { + executable "/usr/bin/xcrun" + args = arguments + } + + project.copy { + from getArchivePath() + into project.file("${project.buildDir}/intermediate/${getArchiveName()}") + } + } +} \ No newline at end of file diff --git a/src/main/groovy/wooga/gradle/build/unity/ios/tasks/XCodeExportTask.groovy b/src/main/groovy/wooga/gradle/build/unity/ios/tasks/XCodeExportTask.groovy new file mode 100644 index 00000000..16776325 --- /dev/null +++ b/src/main/groovy/wooga/gradle/build/unity/ios/tasks/XCodeExportTask.groovy @@ -0,0 +1,138 @@ +/* + * Copyright 2017 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 wooga.gradle.build.unity.ios.tasks + +import org.apache.commons.io.FilenameUtils +import org.gradle.api.DefaultTask +import org.gradle.api.artifacts.PublishArtifact +import org.gradle.api.file.FileCollection +import org.gradle.api.internal.artifacts.publish.AbstractPublishArtifact +import org.gradle.api.tasks.* + +class XCodeExportTask extends DefaultTask { + private Object archivePath + private Object exportPath + private Object exportOptionsPlist + + PublishArtifact getArtifact() { + new AbstractPublishArtifact(this) { + @Override + String getName() { + return null + } + + @Override + String getExtension() { + return "ipa" + } + + @Override + String getType() { + return "iOS application archive" + } + + @Override + String getClassifier() { + return null + } + + @Override + File getFile() { + return project.fileTree("${project.buildDir}/outputs"){ it.include "*.ipa"}.singleFile + } + + @Override + Date getDate() { + return null + } + } + } + + @SkipWhenEmpty + @InputFiles + protected FileCollection getInputFiles() { + project.files(this.archivePath, this.exportOptionsPlist) + } + + @InputFile + File getExportOptionsPlist() { + project.files(exportOptionsPlist).getSingleFile() + } + + void setExportOptionsPlist(Object path) { + exportOptionsPlist = path + } + + XCodeExportTask exportOptionsPlist(Object path) { + setExportOptionsPlist(path) + } + + @InputDirectory + File getArchivePath() { + project.files(archivePath).getSingleFile() + } + + void setArchivePath(Object path) { + archivePath = path + } + + XCodeExportTask archivePath(Object path) { + setArchivePath(path) + } + + @OutputDirectory + File getExportPath() { + if(!exportPath) { + return temporaryDir + } + + project.file(exportPath) + } + + void setExportPath(Object path) { + exportPath = path + } + + XCodeExportTask exportPath(Object path) { + setExportPath(path) + } + + @TaskAction + protected void export() { + List arguments = new ArrayList() + arguments << "xcodebuild" + arguments << "-exportArchive" + arguments << "-exportPath" << getExportPath().getPath() + arguments << "-exportOptionsPlist" << getExportOptionsPlist().getPath() + arguments << "-archivePath" << getArchivePath().getPath() + + project.exec { + executable "/usr/bin/xcrun" + args = arguments + } + + project.copy { + from temporaryDir + include "*.ipa" + into project.file("$project.buildDir/outputs") + it.rename { filename -> + FilenameUtils.getBaseName(getArchivePath().getPath()) + '.ipa' + } + } + } +} diff --git a/src/main/groovy/wooga/gradle/build/unity/tasks/UnityBuildPlayerTask.groovy b/src/main/groovy/wooga/gradle/build/unity/tasks/UnityBuildPlayerTask.groovy index c773e628..2403577a 100644 --- a/src/main/groovy/wooga/gradle/build/unity/tasks/UnityBuildPlayerTask.groovy +++ b/src/main/groovy/wooga/gradle/build/unity/tasks/UnityBuildPlayerTask.groovy @@ -111,6 +111,7 @@ class UnityBuildPlayerTask extends AbstractUnityProjectTask { String customArgs = "-CustomArgs:platform=${getBuildPlatform()};" customArgs += "environment=${getBuildEnvironment()};" customArgs += "outputPath=${getOutputDirectory().getPath()};" + customArgs += "version=${project.version};" if(getToolsVersion()) { customArgs += "toolsVersion=${getToolsVersion()}" diff --git a/src/main/resources/META-INF/gradle-plugins/net.wooga.build-base.properties b/src/main/resources/META-INF/gradle-plugins/net.wooga.build-base.properties index 3b4fbb13..1552e742 100644 --- a/src/main/resources/META-INF/gradle-plugins/net.wooga.build-base.properties +++ b/src/main/resources/META-INF/gradle-plugins/net.wooga.build-base.properties @@ -15,4 +15,4 @@ # # -implementation-class=wooga.gradle.build.base.BaseBuildPlugin \ No newline at end of file +implementation-class=wooga.gradle.build.unity.base.BaseBuildPlugin \ No newline at end of file diff --git a/src/main/resources/META-INF/gradle-plugins/net.wooga.build-ios.properties b/src/main/resources/META-INF/gradle-plugins/net.wooga.build-unity-ios.properties similarity index 90% rename from src/main/resources/META-INF/gradle-plugins/net.wooga.build-ios.properties rename to src/main/resources/META-INF/gradle-plugins/net.wooga.build-unity-ios.properties index 364242ae..f64d3393 100644 --- a/src/main/resources/META-INF/gradle-plugins/net.wooga.build-ios.properties +++ b/src/main/resources/META-INF/gradle-plugins/net.wooga.build-unity-ios.properties @@ -15,4 +15,4 @@ # # -implementation-class=wooga.gradle.build.ios.IOSBuildPlugin \ No newline at end of file +implementation-class=wooga.gradle.build.unity.ios.IOSBuildPlugin \ No newline at end of file diff --git a/src/test/groovy/wooga/gradle/build/UnityBuildPluginActivationSpec.groovy b/src/test/groovy/wooga/gradle/build/unity/UnityBuildPluginActivationSpec.groovy similarity index 96% rename from src/test/groovy/wooga/gradle/build/UnityBuildPluginActivationSpec.groovy rename to src/test/groovy/wooga/gradle/build/unity/UnityBuildPluginActivationSpec.groovy index eb44a09a..b0b600a8 100644 --- a/src/test/groovy/wooga/gradle/build/UnityBuildPluginActivationSpec.groovy +++ b/src/test/groovy/wooga/gradle/build/unity/UnityBuildPluginActivationSpec.groovy @@ -15,7 +15,7 @@ * */ -package wooga.gradle.build +package wooga.gradle.build.unity import nebula.test.PluginProjectSpec diff --git a/src/test/groovy/wooga/gradle/build/UnityBuildPluginSpec.groovy b/src/test/groovy/wooga/gradle/build/unity/UnityBuildPluginSpec.groovy similarity index 98% rename from src/test/groovy/wooga/gradle/build/UnityBuildPluginSpec.groovy rename to src/test/groovy/wooga/gradle/build/unity/UnityBuildPluginSpec.groovy index 79a318b3..1028321f 100644 --- a/src/test/groovy/wooga/gradle/build/UnityBuildPluginSpec.groovy +++ b/src/test/groovy/wooga/gradle/build/unity/UnityBuildPluginSpec.groovy @@ -15,12 +15,11 @@ * */ -package wooga.gradle.build +package wooga.gradle.build.unity import nebula.test.ProjectSpec import org.gradle.api.DefaultTask import spock.lang.Unroll -import wooga.gradle.build.unity.UnityBuildPlugin import wooga.gradle.build.unity.internal.DefaultUnityBuildPluginExtension import wooga.gradle.build.unity.tasks.UnityBuildPlayerTask