diff --git a/src/integrationTest/groovy/wooga/gradle/unity/UnityPluginIntegrationSpec.groovy b/src/integrationTest/groovy/wooga/gradle/unity/UnityPluginIntegrationSpec.groovy index b528ff8..a8e3430 100644 --- a/src/integrationTest/groovy/wooga/gradle/unity/UnityPluginIntegrationSpec.groovy +++ b/src/integrationTest/groovy/wooga/gradle/unity/UnityPluginIntegrationSpec.groovy @@ -30,6 +30,7 @@ import com.wooga.spock.extensions.unity.UnityPluginTestOptions import spock.lang.Unroll import wooga.gradle.unity.models.ResolutionStrategy import wooga.gradle.unity.models.UnityCommandLineOption +import wooga.gradle.unity.models.UnityProjectManifest import wooga.gradle.unity.tasks.Test import wooga.gradle.unity.utils.ProjectSettingsFile @@ -456,9 +457,38 @@ class UnityPluginIntegrationSpec extends UnityIntegrationSpec { result.wasExecuted("generateSolution") } + @UnityPluginTestOptions(forceMockTaskRun = false, disableAutoActivateAndLicense = false) + def "add IDE UPM package when running generateSolution task"() { + given: + def manifestFile = new File(projectDir, "Packages/manifest.json").with { + parentFile.mkdirs() + text = new UnityProjectManifest([:]).serialize() + return it + } + + when: + def result = runTasksSuccessfully("generateSolution") + then: + result.wasExecuted("addIdeUPMPackage") + result.wasExecuted("generateSolution") + def deps = UnityProjectManifest.deserialize(manifestFile).getDependencies() + deps[expectedPackageName] == expectedPackageVersion + + where: + expectedPackageName = "com.unity.ide.rider" + expectedPackageVersion = "3.0.28" + } + @Unroll - def "runs addUPMPackages task if there are packages to add"() { + def "runs addUPMPackages task"() { given: + new File(projectDir, manifestFile).with { + delete() + if (hasManifest) { + parentFile.mkdirs() + text = new UnityProjectManifest([:]).serialize() + } + } buildFile << """ unity { enableTestCodeCoverage = ${testCoverageEnabled} @@ -474,11 +504,15 @@ class UnityPluginIntegrationSpec extends UnityIntegrationSpec { result.standardOutput.contains("Task :addUPMPackages SKIPPED") where: - testCoverageEnabled | packagesToInstall | shouldRun - false | [:] | false - false | ["package": "ver"] | true - true | [:] | true - true | ["package": "ver"] | true + hasManifest | testCoverageEnabled | packagesToInstall | shouldRun + false | false | [:] | false + true | false | [:] | true + true | false | ["package": "ver"] | true + false | false | ["package": "ver"] | true + true | true | [:] | true + true | true | ["package": "ver"] | true + false | true | ["package": "ver"] | true + manifestFile = "Packages/manifest.json" } @Unroll diff --git a/src/integrationTest/groovy/wooga/gradle/unity/tasks/GenerateSolutionTaskIntegrationSpec.groovy b/src/integrationTest/groovy/wooga/gradle/unity/tasks/GenerateSolutionTaskIntegrationSpec.groovy index 8e2ee5f..7fc470c 100644 --- a/src/integrationTest/groovy/wooga/gradle/unity/tasks/GenerateSolutionTaskIntegrationSpec.groovy +++ b/src/integrationTest/groovy/wooga/gradle/unity/tasks/GenerateSolutionTaskIntegrationSpec.groovy @@ -38,10 +38,14 @@ class GenerateSolutionTaskIntegrationSpec extends UnityTaskIntegrationSpec { generateSolution(GenerateSolution), ensureProjectManifest(Unity), addUPMPackages(AddUPMPackages), + addIdeUPMPackage(AddUPMPackages), setResolutionStrategy(SetResolutionStrategy) private final Class taskClass @@ -243,7 +244,7 @@ class UnityPlugin implements Plugin { addTestTasks(project, extension) addSetAPICompatibilityLevelTasks(project, extension) addGenerateSolutionTask(project) - addAddUPMPackagesTask(project, extension) + addAddUPMPackageTasks(project, extension) addActivateAndReturnLicenseTasks(project, extension) } @@ -413,23 +414,37 @@ class UnityPlugin implements Plugin { } } - private static void addAddUPMPackagesTask(Project project, final UnityPluginExtension extension) { - def addUPMPackagesTask = project.tasks.register(Tasks.addUPMPackages.toString(), AddUPMPackages) { task -> - task.group = GROUP + private static void addAddUPMPackageTasks(Project project, final UnityPluginExtension extension) { + project.tasks.withType(AddUPMPackages).configureEach { task -> + onlyIf { + def upmPackageCount = task.upmPackages.getOrElse([:]).size() + + task.conventionUpmPackages.getOrElse([:]).size() + if(upmPackageCount == 0) { + logger.info("No UPM packages to install, skipping") + } + return upmPackageCount > 0 + } task.projectManifestFile.convention(extension.projectManifestFile) - task.upmPackages.putAll(extension.upmPackages) - task.upmPackages.putAll(extension.enableTestCodeCoverage.map { + } + + def addUPMPackagesTask = project.tasks.register(Tasks.addUPMPackages.toString(), AddUPMPackages) { it -> + it.group = GROUP + it.upmPackages.putAll(extension.upmPackages) + //needed in order to have coverage reports + //TODO: breaking change, detach this similar to addIdeUpmPackage and GenerateSolution + it.conventionUpmPackages.putAll(extension.enableTestCodeCoverage.map { it ? ["com.unity.testtools.codecoverage": "1.1.0"] : [:] }) } project.tasks.withType(Test).configureEach { testTask -> testTask.dependsOn(addUPMPackagesTask) } - addUPMPackagesTask.configure { task -> - task.onlyIf { - def upmPackageCount = task.upmPackages.forUseAtConfigurationTime().getOrElse([:]).size() - upmPackageCount > 0 - } + + def addIdeUPMPackage = project.tasks.register(Tasks.addIdeUPMPackage.toString(), AddUPMPackages) { + it.conventionUpmPackages.putAll(["com.unity.ide.rider": "3.0.28"]) + } + project.tasks.withType(GenerateSolution).configureEach {genSolutionTask -> + genSolutionTask.dependsOn(addIdeUPMPackage) } } diff --git a/src/main/groovy/wooga/gradle/unity/models/UnityProjectManifest.groovy b/src/main/groovy/wooga/gradle/unity/models/UnityProjectManifest.groovy index 463c613..5223615 100644 --- a/src/main/groovy/wooga/gradle/unity/models/UnityProjectManifest.groovy +++ b/src/main/groovy/wooga/gradle/unity/models/UnityProjectManifest.groovy @@ -51,14 +51,14 @@ class UnityProjectManifest extends HashMap implements GroovyInte Map getDependencies() { if (!containsKey(dependenciesKey)) { - this[dependenciesKey] = [:] + setDependencies([:]) } (Map)this[dependenciesKey] } void setDependencies(Map map) { - this[dependenciesKey] = map + this.put(dependenciesKey, map) } void addDependencies(Map map) { @@ -68,6 +68,10 @@ class UnityProjectManifest extends HashMap implements GroovyInte void addDependency(String key, Object value) { getDependencies()[key] = value } + + Object getDependencyVersion(String key) { + return getDependencies()[key] + } } diff --git a/src/main/groovy/wooga/gradle/unity/tasks/AddUPMPackages.groovy b/src/main/groovy/wooga/gradle/unity/tasks/AddUPMPackages.groovy index d3333e9..a776819 100644 --- a/src/main/groovy/wooga/gradle/unity/tasks/AddUPMPackages.groovy +++ b/src/main/groovy/wooga/gradle/unity/tasks/AddUPMPackages.groovy @@ -45,6 +45,12 @@ class AddUPMPackages extends ProjectManifestTask @Override void modifyProjectManifest(UnityProjectManifest manifest) { + conventionUpmPackages.getOrElse([:]).each { + if(!manifest.getDependencyVersion(it.key)) { + manifest.addDependency(it.key, it.value) + } + } manifest.addDependencies(upmPackages.get()) + } } diff --git a/src/main/groovy/wooga/gradle/unity/tasks/ExecuteCsharpScript.groovy b/src/main/groovy/wooga/gradle/unity/tasks/ExecuteCsharpScript.groovy new file mode 100644 index 0000000..a41c3db --- /dev/null +++ b/src/main/groovy/wooga/gradle/unity/tasks/ExecuteCsharpScript.groovy @@ -0,0 +1,81 @@ +package wooga.gradle.unity.tasks + +import org.gradle.api.file.RegularFile +import org.gradle.api.file.RegularFileProperty +import org.gradle.api.provider.Property +import org.gradle.api.provider.Provider +import org.gradle.api.tasks.Input +import org.gradle.api.tasks.InputFile +import wooga.gradle.unity.UnityTask + +import java.nio.file.Files + +/** + * Copies a script from {@code sourceScript} into a temporary {@code destinationScript} then executes Unity with {@code executeMethod} + */ +class ExecuteCsharpScript extends UnityTask { + + private final RegularFileProperty sourceScript = objects.fileProperty() + + @InputFile + RegularFileProperty getSourceScript() { + return sourceScript + } + + void setSourceScript(Provider sourceCsScript) { + this.sourceScript.set(sourceCsScript) + } + + void setSourceScript(RegularFile sourceCsScript) { + this.sourceScript.set(sourceCsScript) + } + + void setSourceScript(File sourceCsScript) { + this.sourceScript.set(sourceCsScript) + } + + private final RegularFileProperty destinationScript = objects.fileProperty() + + @InputFile + RegularFileProperty getDestinationScript() { + return destinationScript + } + + void setDestinationScript(Provider destCsScript) { + this.destinationScript.set(destCsScript) + } + + void setDestinationScript(RegularFile destCsScript) { + this.destinationScript.set(destCsScript) + } + + void setDestinationScript(File destCsScript) { + this.destinationScript.set(destCsScript) + } + + @Override //this input is mandatory for this task, so overriding the previous annotation. + @Input + Property getExecuteMethod() { + return super.getExecuteMethod() + } + + ExecuteCsharpScript() { + finalizedBy(project.tasks.register("_${this.name}_cleanup") { + onlyIf { + destinationScript.present && destinationScript.get().asFile.file + } + doLast { + def baseFile = destinationScript.get().asFile + def metafile = new File(baseFile.absolutePath + ".meta") + baseFile.delete() + metafile.delete() + } + }) + } + + @Override + protected void preExecute() { + Files.copy(sourceScript.get().asFile.toPath(), destinationScript.get().asFile.toPath()) + destinationScript.asFile.get().deleteOnExit() + } +} diff --git a/src/main/groovy/wooga/gradle/unity/tasks/GenerateSolution.groovy b/src/main/groovy/wooga/gradle/unity/tasks/GenerateSolution.groovy index f01c3d9..c5b939e 100644 --- a/src/main/groovy/wooga/gradle/unity/tasks/GenerateSolution.groovy +++ b/src/main/groovy/wooga/gradle/unity/tasks/GenerateSolution.groovy @@ -1,11 +1,55 @@ package wooga.gradle.unity.tasks -import wooga.gradle.unity.UnityTask +import org.gradle.api.file.Directory +import org.gradle.api.file.DirectoryProperty +import org.gradle.api.provider.Provider +import org.gradle.api.tasks.InputDirectory +import org.gradle.api.tasks.Optional -class GenerateSolution extends UnityTask { +class GenerateSolution extends ExecuteCsharpScript { + private final DirectoryProperty assetsDir = objects.directoryProperty() + + @InputDirectory + @Optional + DirectoryProperty getAssetsDir() { + return assetsDir + } + + void setAssetsDir(Provider assetsDir) { + this.assetsDir.set(assetsDir) + } + + void setAssetsDir(Directory assetsDir) { + this.assetsDir.set(assetsDir) + } + + void setAssetsDir(File assetsDir) { + this.assetsDir.set(assetsDir) + } GenerateSolution() { outputs.upToDateWhen { false } - executeMethod = "UnityEditor.SyncVS.SyncSolution" + + // Not the usual approach we take. Usually we would like to put the default in the plugin conventions and such, but this + // task seems to be designed to be independent from plugin configuration, so we still apply plugin configuration, + // but we set __sensible defaults__. + // This runs before the config block in the plugin, so it can and will be overwritten by plugin configuration when the plugin is applied as well + this.assetsDir.convention(this.projectDirectory.dir("Assets").map {it.asFile.mkdirs();return it}) + + def defaultScript = this.assetsDir + .map { it.file("SolutionGenerator.cs") } + .map { script -> + script.asFile.text = GenerateSolution.classLoader.getResourceAsStream("DefaultSolutionGenerator.cs").text + script.asFile.deleteOnExit() + return script + } + this.sourceScript.convention(defaultScript) + this.destinationScript.convention(this.sourceScript) + this.executeMethod.set(project.provider { + unityVersion.majorVersion >= 2022? + "Wooga.UnityPlugin.DefaultSolutionGenerator.GenerateSolution" : + "UnityEditor.SyncVS.SyncSolution" + }) + } } diff --git a/src/main/groovy/wooga/gradle/unity/tasks/ProjectManifestTask.groovy b/src/main/groovy/wooga/gradle/unity/tasks/ProjectManifestTask.groovy index 9d5788e..7539f8f 100644 --- a/src/main/groovy/wooga/gradle/unity/tasks/ProjectManifestTask.groovy +++ b/src/main/groovy/wooga/gradle/unity/tasks/ProjectManifestTask.groovy @@ -14,20 +14,27 @@ abstract class ProjectManifestTask extends DefaultTask implements abstract void modifyProjectManifest(UnityProjectManifest manifest) + ProjectManifestTask() { + onlyIf { + def manifestFile = projectManifestFile.asFile.getOrNull() + def condition = manifestFile && manifestFile.exists() + if(!condition) { + project.logger.warn("${manifestFile.name} not found, skipping UPM packages install") + } + return condition + } + } + @TaskAction void execute() { def manifestFile = projectManifestFile.asFile.getOrNull() - if (manifestFile && manifestFile.exists()) { - // Deserialize - def manifest = UnityProjectManifest.deserialize(manifestFile) - // Modify - modifyProjectManifest(manifest) - // Serialize - def serialization = manifest.serialize() - manifestFile.write(serialization) - } else { - project.logger.warn("${manifestFileName} not found, skipping UPM packages install: ${upmPackages.get()}") - } + // Deserialize + def manifest = UnityProjectManifest.deserialize(manifestFile) + // Modify + modifyProjectManifest(manifest) + // Serialize + def serialization = manifest.serialize() + manifestFile.write(serialization) } } diff --git a/src/main/groovy/wooga/gradle/unity/traits/AddUnityPackagesSpec.groovy b/src/main/groovy/wooga/gradle/unity/traits/AddUnityPackagesSpec.groovy index 6423d10..5f9608a 100644 --- a/src/main/groovy/wooga/gradle/unity/traits/AddUnityPackagesSpec.groovy +++ b/src/main/groovy/wooga/gradle/unity/traits/AddUnityPackagesSpec.groovy @@ -2,11 +2,8 @@ package wooga.gradle.unity.traits import com.wooga.gradle.BaseSpec import org.gradle.api.provider.MapProperty -import org.gradle.api.provider.Property -import org.gradle.api.provider.Provider import org.gradle.api.tasks.Input import org.gradle.api.tasks.Optional -import wooga.gradle.unity.models.ResolutionStrategy trait AddUnityPackagesSpec extends BaseSpec { @@ -28,5 +25,23 @@ trait AddUnityPackagesSpec extends BaseSpec { void setUpmPackages(Map values) { upmPackages.set(values) } + + @Input + @Optional + MapProperty getConventionUpmPackages() { + conventionUpmPackages + } + + private final MapProperty conventionUpmPackages = objects.mapProperty(String, String) + + void setConventionUpmPackages(MapProperty values) { + conventionUpmPackages.set(values) + } + + void setConventionUpmPackages(Map values) { + conventionUpmPackages.set(values) + } + + } diff --git a/src/main/resources/DefaultSolutionGenerator.cs b/src/main/resources/DefaultSolutionGenerator.cs new file mode 100644 index 0000000..00bd4d3 --- /dev/null +++ b/src/main/resources/DefaultSolutionGenerator.cs @@ -0,0 +1,47 @@ +//from https://forum.unity.com/threads/any-way-to-tell-unity-to-generate-the-sln-file-via-script-or-command-line.392314/ +using System.Collections.Generic; +using UnityEngine; +using UnityEditor; +using Unity.CodeEditor; +using System.Reflection; + +// The recommended way to create the VisualStudio SLN from the command line is a call +// Unity.exe -executeMethod "UnityEditor.SyncVS.SyncSolution" +// +// Unfortunately, as of Unity 2021.3.21f1 the built-in UnityEditor.SyncVS.SyncSolution internally calls +// Unity.CodeEditor.CodeEditor.Editor.CurrentCodeEditor.SyncAll() where CurrentCodeEditor depends on the user preferences +// which may not actually be set to VS on a CI machine. +// (see https://github.com/Unity-Technologies/UnityCsReference/blob/master/Editor/Mono/CodeEditor/SyncVS.cs) +// +// This routine provides an re-implementation that avoids reliability on the preference setting +// Unity.exe -executeMethod "UnityEditor.SyncVS.SyncSolution" +namespace Wooga.UnityPlugin { + public static class DefaultSolutionGenerator + { + public static void GenerateSolution() + { + // Ensure that the mono islands are up-to-date + AssetDatabase.Refresh(); + + List externalCodeEditors; + + // externalCodeEditors = Unity.CodeEditor.Editor.m_ExternalCodeEditors; + // ... unfortunately this is private without any means of access. Use reflection to get the value ... + externalCodeEditors = CodeEditor.Editor.GetType().GetField("m_ExternalCodeEditors", BindingFlags.NonPublic | BindingFlags.Instance).GetValue(CodeEditor.Editor) as List; + + foreach (var externalEditor in externalCodeEditors) + { + var typeName = externalEditor.GetType().Name; + switch (typeName) + { + case "VisualStudioEditor": + case "RiderScriptEditor": + Debug.Log($"Generating solution with {typeName}"); + externalEditor.SyncAll(); + return; + } + } + Debug.LogError("no VisualStudioEditor (com.unity.ide.visualstudio) or RiderScriptEditor (com.unity.ide.rider) registered, can't generate solution"); + } + } +}