diff --git a/src/integrationTest/groovy/wooga/gradle/unity/tasks/GenerateUpmPackageTaskIntegrationSpec.groovy b/src/integrationTest/groovy/wooga/gradle/unity/tasks/GenerateUpmPackageTaskIntegrationSpec.groovy index 510ccde..fb68841 100644 --- a/src/integrationTest/groovy/wooga/gradle/unity/tasks/GenerateUpmPackageTaskIntegrationSpec.groovy +++ b/src/integrationTest/groovy/wooga/gradle/unity/tasks/GenerateUpmPackageTaskIntegrationSpec.groovy @@ -16,6 +16,8 @@ import wooga.gradle.unity.testutils.SetupProjectLayoutTestTask import wooga.gradle.unity.utils.PackageManifestBuilder import wooga.gradle.utils.DirectoryComparer +import java.security.Provider + class GenerateUpmPackageTaskIntegrationSpec extends UnityIntegrationSpec { @Override @@ -31,14 +33,14 @@ class GenerateUpmPackageTaskIntegrationSpec extends UnityIntegrationSpec { runPropertyQuery(getter, setter).matches(value) where: - propertyName | type | value - "packageDirectory" | "File" | osPath("/path/to/package/dir") - "packageDirectory" | "Provider" | osPath("/path/to/package/dir") - "packageName" | "String" | "testPackageA" - "packageName" | "Provider" | "testPackageB" + propertyName | type | value + "packageDirectory" | "File" | osPath("/path/to/package/dir") + "packageDirectory" | "Provider" | osPath("/path/to/package/dir") + "packageName" | "String" | "testPackageA" + "packageName" | "Provider" | "testPackageB" setter = new PropertySetterWriter(subjectUnderTestName, propertyName) - .set(value, type) + .set(value, type) getter = new PropertyGetterTaskWriter(setter) } @@ -46,14 +48,14 @@ class GenerateUpmPackageTaskIntegrationSpec extends UnityIntegrationSpec { @UnityPluginTestOptions(unityPath = UnityPathResolution.Default) @UnityInstallation(version = "2022.1.15f1", cleanup = false) def "generates unity package"( - String packageDisplayName, - String unityProjectPath, - String distributionsDirName, - String packageDirectory, - String packageName, - String packageVersion, - String expectedPackageFileName, - Installation unity + String packageDisplayName, + String unityProjectPath, + String distributionsDirName, + String packageDirectory, + String packageName, + String packageVersion, + String expectedPackageFileName, + Installation unity ) { given: "a pre installed unity editor" @@ -115,12 +117,12 @@ class GenerateUpmPackageTaskIntegrationSpec extends UnityIntegrationSpec { where: packageDisplayName = "Foobar" - unityProjectPath = "Wooga.${packageDisplayName}" + unityProjectPath = "Wooga.${packageDisplayName}".toString() distributionsDirName = "build/distributions" packageDirectory = unityProjectPath + "/Assets/Wooga/Foobar" packageName = "com.wooga.foobar" packageVersion = "0.0.1" - expectedPackageFileName = "${packageName}-${packageVersion}.tgz" + expectedPackageFileName = "${packageName}-${packageVersion}.tgz".toString() and: "the injected unity installation" unity = null @@ -130,14 +132,14 @@ class GenerateUpmPackageTaskIntegrationSpec extends UnityIntegrationSpec { @UnityPluginTestOptions(unityPath = UnityPathResolution.Default) @UnityInstallation(version = "2019.4.38f1", cleanup = false) def "uses package name and version from package.json when not specified"( - String packageDisplayName, - String unityProjectPath, - String distributionsDirName, - String packageDirectory, - String packageName, - String packageVersion, - String expectedPackageFileName, - Installation unity + String packageDisplayName, + String unityProjectPath, + String distributionsDirName, + String packageDirectory, + String packageName, + String packageVersion, + String expectedPackageFileName, + Installation unity ) { given: "a pre installed unity editor" @@ -197,12 +199,12 @@ class GenerateUpmPackageTaskIntegrationSpec extends UnityIntegrationSpec { where: packageDisplayName = "Foobar" - unityProjectPath = "Wooga.${packageDisplayName}" + unityProjectPath = "Wooga.${packageDisplayName}".toString() distributionsDirName = "build/distributions" packageDirectory = unityProjectPath + "/Assets/Wooga/Foobar" packageName = "com.wooga.foobar-baz" packageVersion = "1.1.1" - expectedPackageFileName = "${packageName}-${packageVersion}.tgz" + expectedPackageFileName = "${packageName}-${packageVersion}.tgz".toString() and: "the injected unity installation" unity = null @@ -255,6 +257,125 @@ class GenerateUpmPackageTaskIntegrationSpec extends UnityIntegrationSpec { reason = predicate.message } + @Unroll + def "generate package with patched property #property with value #value"() { + + given: + directory(projectPath) + buildFile << """ + unity { + projectDirectory.set(${wrapValueBasedOnType(projectPath, Directory)}) + } + """.stripIndent() + + projectFile(projectPath, "README.MD") + def manifestFile = projectFile(projectPath, GenerateUpmPackage.packageManifestFileName) + manifestFile.write(new PackageManifestBuilder().build()) + + and: + addTask(taskName, GenerateUpmPackage.class.name, false, """ + packageDirectory.set(${wrapValueBasedOnType(projectPath, Directory)}) + archiveVersion.set(${wrapValueBasedOnType(packageVersion, String)}) + packageName = ${wrapValueBasedOnType(packageName, String)} + dependencies[${wrapValueBasedOnType(dependency1, String)}] = "0.0.0" + + patch(${wrapValueBasedOnType(property, String)}, ${wrapValueBasedOnType(value, String)}) + """) + + when: + def result = runTasks(taskName) + + then: + result.success + + and: + def manifest = parseManifestFromPackage(packageName, packageVersion) + manifest[property] == value + + where: + taskName = "upmPack" + projectPath = "Wooga.Foobar" + packageName = projectPath + packageVersion = "1.2.3" + dependency1 = "com.wooga.pancakes" + + property | value + "version" | "4.5.6" + "changelogUrl" | "www.pancakes.com" + } + + @Unroll + def "patches version of dependency #dep to #input"() { + + given: + directory(projectPath) + buildFile << """ + unity { + projectDirectory.set(${wrapValueBasedOnType(projectPath, Directory)}) + } + """.stripIndent() + + projectFile(projectPath, "README.MD") + def manifestFile = projectFile(projectPath, GenerateUpmPackage.packageManifestFileName) + manifestFile.write(new PackageManifestBuilder().build()) + + and: + addTask(taskName, GenerateUpmPackage.class.name, false, """ + packageDirectory.set(${wrapValueBasedOnType(projectPath, Directory)}) + archiveVersion.set(${wrapValueBasedOnType(packageVersion, String)}) + packageName = ${wrapValueBasedOnType(packageName, String)} + + dependencies[${wrapValueBasedOnType(dependency1, String)}] = "0.0.0" + dependencies[${wrapValueBasedOnType(dependency2, String)}] = "0.0.0" + + patchDependency(${wrapValueBasedOnType(dep, String)}, ${wrapValueBasedOnType(input, type)}) + + """) + + when: + def result = runTasks(taskName) + + then: + result.success + + and: + def manifest = parseManifestFromPackage(packageName, packageVersion) + if (expected == _) { + expected = input + } + manifest["dependencies"][dep] == expected + + where: + dep | input | type | expected + "com.wooga.foo" | "2.4.6" | String | _ + "com.wooga.bar" | "5.2.3" | String | _ + "com.wooga.foo" | "1.1.1" | "Provider" | "1.1.1" + + taskName = "upmPack" + projectPath = "Wooga.Foobar" + packageName = projectPath + packageVersion = "1.2.3" + + dependency1 = "com.wooga.foo" + dependency2 = "com.wooga.bar" + } + + private File getPackageFile(String name, String version) { + def distributionsDirName = "build/distributions" + def expectedPackageFileName = "${name}-${version}.tgz" + def distributionsDir = new File(projectDir, distributionsDirName) + def packageFile = new File(distributionsDir, expectedPackageFileName) + } + + private Object parseManifestFromPackage(String name, String version) { + def packageFile = getPackageFile(name, version) + def packageManifestUnpackDir = unpackPackage(packageFile) + def unpackedPackageDir = new File(packageManifestUnpackDir, "package") + def packageManifestFile = new File(unpackedPackageDir, GenerateUpmPackage.packageManifestFileName) + def json = new JsonSlurper().parse(packageManifestFile) + json + } + private static File unpackPackage(File packageFile) { def packageManifestUnpackDir = File.createTempDir(packageFile.name, "_unpack") def ant = new AntBuilder() diff --git a/src/main/groovy/wooga/gradle/unity/tasks/GenerateUpmPackage.groovy b/src/main/groovy/wooga/gradle/unity/tasks/GenerateUpmPackage.groovy index e858faa..bd2ac51 100644 --- a/src/main/groovy/wooga/gradle/unity/tasks/GenerateUpmPackage.groovy +++ b/src/main/groovy/wooga/gradle/unity/tasks/GenerateUpmPackage.groovy @@ -7,6 +7,8 @@ import org.gradle.api.file.Directory import org.gradle.api.file.DirectoryProperty import org.gradle.api.file.FileCollection import org.gradle.api.file.RegularFile +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.specs.Spec @@ -19,12 +21,16 @@ import org.gradle.api.tasks.Optional import org.gradle.api.tasks.SkipWhenEmpty import org.gradle.api.tasks.bundling.Compression import org.gradle.api.tasks.bundling.Tar +import org.gradle.internal.impldep.com.google.common.collect.ImmutableMap /** * A task that will generate an UPM package from a given Unity project */ class GenerateUpmPackage extends Tar implements BaseSpec { + /** + * A custom message that is presented when the packaging task fails + */ enum Message { packageDirectoryNotSet("No package directory was set"), packageManifestFileNotFound("No package manifest file (package.json) was found"), @@ -60,6 +66,9 @@ class GenerateUpmPackage extends Tar implements BaseSpec { packageDirectory.set(value) } + /** + * @return All files in the package, if present + */ @SkipWhenEmpty @InputFiles FileCollection getPackageFiles() { @@ -69,8 +78,6 @@ class GenerateUpmPackage extends Tar implements BaseSpec { project.files() } - private final Provider packageManifestFile = packageDirectory.file(packageManifestFileName) - /** * @return The package manifest file, `package.json`, which defines the package dependencies and other metadata. */ @@ -79,8 +86,7 @@ class GenerateUpmPackage extends Tar implements BaseSpec { packageManifestFile } - - private final Property packageName = objects.property(String) + private final Provider packageManifestFile = packageDirectory.file(packageManifestFileName) /** * @return The officially registered package name. This name must conform to the Unity Package Manager naming convention, @@ -92,6 +98,8 @@ class GenerateUpmPackage extends Tar implements BaseSpec { packageName } + private final Property packageName = objects.property(String) + void setPackageName(Provider value) { packageName.set(value) } @@ -100,47 +108,35 @@ class GenerateUpmPackage extends Tar implements BaseSpec { packageName.set(value) } - public static final packageManifestFileName = "package.json" - - protected void adjustManifestFile() { - def manifest = new File(temporaryDir, packageManifestFileName) - if(manifest.exists()) { - def j = new JsonSlurper() - def manifestContent = j.parse(manifest) - - if(packageName.present) { - manifestContent['name'] = packageName.get() - } - - if(archiveVersion.present) { - manifestContent['version'] = archiveVersion.get() - } - - String json = JsonOutput.toJson(manifestContent) - manifest.text = JsonOutput.prettyPrint(json) - } + /** + * @return Optional dependencies to set + */ + @Input + @Optional + MapProperty getDependencies() { + dependencies } - @Override - protected void copy() { - if (!packageDirectory.present) { - logger.warn(Message.packageDirectoryNotSet.message) - } + private MapProperty dependencies = objects.mapProperty(String, String) - project.copy { - from(packageDirectory) - include(packageManifestFileName) - into(temporaryDir) - } + /** + * @return Optional patches to be applied onto the manifest before it is packaged + */ + @Input + @Optional + MapProperty getPatches() { + patches + } - adjustManifestFile() + private MapProperty patches = objects.mapProperty(String, Object) - from(temporaryDir) - from(packageDirectory) - super.copy() - } + /** + * The default file name for the package manifest, as decided by Unity + */ + public static final packageManifestFileName = "package.json" GenerateUpmPackage() { + setCompression(Compression.GZIP) // Creates a root directory inside the package @@ -169,7 +165,7 @@ class GenerateUpmPackage extends Tar implements BaseSpec { reproducibleFileOrder = true filesMatching(packageManifestFileName) { - if(it.getFile().absolutePath.startsWith(packageDirectory.get().asFile.absolutePath)) { + if (it.getFile().absolutePath.startsWith(packageDirectory.get().asFile.absolutePath)) { it.exclude() } } @@ -197,6 +193,113 @@ class GenerateUpmPackage extends Tar implements BaseSpec { } }) } + + @Override + protected void copy() { + if (!packageDirectory.present) { + logger.warn(Message.packageDirectoryNotSet.message) + } + + project.copy { + from(packageDirectory) + include(packageManifestFileName) + into(temporaryDir) + } + + adjustManifestFile() + + from(temporaryDir) + from(packageDirectory) + super.copy() + } + + /** + * Modifies the project manifest file (package.json) before it is packaged along with the project) + */ + protected void adjustManifestFile() { + def manifest = new File(temporaryDir, packageManifestFileName) + + if (manifest.exists()) { + + // Read + def json = new JsonSlurper() + Map manifestContent = json.parse(manifest) + + // Apply the default properties + if (packageName.present) { + manifestContent['name'] = packageName.get() + } + + if (archiveVersion.present) { + manifestContent['version'] = archiveVersion.get() + } + + if (dependencies.present) { + manifestContent['dependencies'] = dependencies.get() + } + + def _patches = patches.get().collectEntries { + Object value = null + if (it.value instanceof Provider) { + value = ((Provider) it.value).get() + } else { + value = it.value + } + [it.key, value] + } + manifestContent = merge(manifestContent, _patches) + + // Write back + manifest.text = JsonOutput.prettyPrint(JsonOutput.toJson(manifestContent)) + } + } + + /** + * Patches the version of a single dependency in the manifest + * @param name The name of the package + * @param version The version to set + */ + void patchDependency(String name, Object version) { + def key = "dependencies" + if (!patches.get().containsKey(key)) { + patches[key] = new HashMap() + } + + def dependencies = patches[key].get() as Map + dependencies[name] = version + } + + /** + * Patches a single property in the manifest + * @param key The property to patch + * @param value The value to set + */ + void patch(String key, Object value) { + def provider = providers.provider({ value }) + patches.put(key, provider) + } + + private static Map merge(Map lhs, Map rhs) { + + // Have to do this since we cannot clone an ImmutableMap + def clone = new HashMap(lhs) + + return rhs.inject(clone) { map, entry -> + if (map[entry.key] instanceof Map && entry.value instanceof Map) { + map[entry.key] = merge(map[entry.key], entry.value) + } else { + Object value = null + // Unwrap Provider if needed + if (entry.value instanceof Provider) { + value = ((Provider)entry.value).get() + } else{ + value = entry.value + } + map[entry.key] = value + } + return map + } as Map + } }