Skip to content

Commit

Permalink
generateSolution supports unity >= 2022 (#196)
Browse files Browse the repository at this point in the history
## Description
FIx the `generateSolution` task to work for unity 2022 and above, due to
`"UnityEditor.SyncVS.SyncSolution"` not being available anymore on those
versions.

We now put a script inside of the unity project and run a method in it
to generate the solution. It is more complex, but its the more stable
solution as it doesn't depend on the project having a specific
**active** external editor. However, we still need to have **some**
external editor UPM package, so we now install the `com.unity.ide.rider`
upm package if its not present.

To run the script we now have a task to execute scripts in a unity
project through a task (RunCSScript).

## Changes
* ![FIX] fix `generateSolution` not working out of the box on unity 2022
and above
* ![ADD] `RunCSScript` task type
* ![ADD] installs `com.unity.ide.rider` upm package

[NEW]:      https://resources.atlas.wooga.com/icons/icon_new.svg "New"
[ADD]:      https://resources.atlas.wooga.com/icons/icon_add.svg "Add"
[IMPROVE]: https://resources.atlas.wooga.com/icons/icon_improve.svg
"Improve"
[CHANGE]: https://resources.atlas.wooga.com/icons/icon_change.svg
"Change"
[FIX]:      https://resources.atlas.wooga.com/icons/icon_fix.svg "Fix"
[UPDATE]: https://resources.atlas.wooga.com/icons/icon_update.svg
"Update"

[BREAK]: https://resources.atlas.wooga.com/icons/icon_break.svg "Remove"
[REMOVE]: https://resources.atlas.wooga.com/icons/icon_remove.svg
"Remove"
[IOS]:      https://resources.atlas.wooga.com/icons/icon_iOS.svg "iOS"
[ANDROID]: https://resources.atlas.wooga.com/icons/icon_android.svg
"Android"
[WEBGL]: https://resources.atlas.wooga.com/icons/icon_webGL.svg "WebGL"
[GRADLE]: https://resources.atlas.wooga.com/icons/icon_gradle.svg
"GRADLE"
[UNITY]: https://resources.atlas.wooga.com/icons/icon_unity.svg "Unity"
[LINUX]: https://resources.atlas.wooga.com/icons/icon_linux.svg "Linux"
[WIN]: https://resources.atlas.wooga.com/icons/icon_windows.svg
"Windows"
[MACOS]:    https://resources.atlas.wooga.com/icons/icon_iOS.svg "macOS"
  • Loading branch information
Joaquimmnetto authored Apr 23, 2024
1 parent 77b5628 commit 0b2b025
Show file tree
Hide file tree
Showing 10 changed files with 324 additions and 38 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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}
Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,10 +38,14 @@ class GenerateSolutionTaskIntegrationSpec extends UnityTaskIntegrationSpec<Gener
@Requires({ os.macOs })
@UnityPluginTestOptions(unityPath = UnityPathResolution.Default)
@UnityInstallation(version = "2019.4.24f1", cleanup = false)
def "generates .sln file when running generateSolution task"(Installation unity) {
def "generates .sln file when running generateSolution task for unity 2019.4"(Installation unity) {
given: "an unity3D project"
def project_path = "build/test_project"
environmentVariables.set("UNITY_PATH", unity.getExecutable().getPath())
buildFile << """
unity {
unityPath = ${wrapValueBasedOnType(unity.executable, File)}
}
"""
appendToSubjectTask("""createProject = "${project_path}" """,
"""buildTarget = "Android" """)

Expand All @@ -50,7 +54,36 @@ class GenerateSolutionTaskIntegrationSpec extends UnityTaskIntegrationSpec<Gener

then:"solution file is generated"
result.standardOutput.contains("Starting process 'command '${unity.getExecutable().getPath()}'")
result.wasExecuted(":_${subjectUnderTestName}_cleanup")
fileExists(project_path)
fileExists(project_path, "test_project.sln")
!fileExists(project_path, "Assets/SolutionGenerator.cs")
!fileExists(project_path, "Assets/SolutionGenerator.cs.meta")
}


@Requires({ os.macOs })
@UnityPluginTestOptions(unityPath = UnityPathResolution.Default)
@UnityInstallation(version = "2022.3.18f1", cleanup = false)
def "generates .sln file when running generateSolution task for unity 2022.3"(Installation unity) {
given: "an unity3D project"
buildFile << """
unity {
unityPath = ${wrapValueBasedOnType(unity.executable, File)}
}
"""
appendToSubjectTask("""createProject = "${projectDir.absolutePath}" """,
"""buildTarget = "Android" """)

when:"generateSolution task is called"
def result = runTasks(subjectUnderTestName)

then:"solution file is generated"
result.standardOutput.contains("Starting process 'command '${unity.getExecutable().getPath()}'")
projectDir.list().any{ it.endsWith(".sln") }
result.wasExecuted(":_${subjectUnderTestName}_cleanup")
!fileExists(projectDir.absolutePath, "Assets/SolutionGenerator.cs")
!fileExists(projectDir.absolutePath, "Assets/SolutionGenerator.cs.meta")

}
}
37 changes: 26 additions & 11 deletions src/main/groovy/wooga/gradle/unity/UnityPlugin.groovy
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ class UnityPlugin implements Plugin<Project> {
generateSolution(GenerateSolution),
ensureProjectManifest(Unity),
addUPMPackages(AddUPMPackages),
addIdeUPMPackage(AddUPMPackages),
setResolutionStrategy(SetResolutionStrategy)

private final Class taskClass
Expand Down Expand Up @@ -243,7 +244,7 @@ class UnityPlugin implements Plugin<Project> {
addTestTasks(project, extension)
addSetAPICompatibilityLevelTasks(project, extension)
addGenerateSolutionTask(project)
addAddUPMPackagesTask(project, extension)
addAddUPMPackageTasks(project, extension)
addActivateAndReturnLicenseTasks(project, extension)
}

Expand Down Expand Up @@ -413,23 +414,37 @@ class UnityPlugin implements Plugin<Project> {
}
}

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)
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,14 +51,14 @@ class UnityProjectManifest extends HashMap<String, Object> implements GroovyInte

Map getDependencies() {
if (!containsKey(dependenciesKey)) {
this[dependenciesKey] = [:]
setDependencies([:])
}

(Map)this[dependenciesKey]
}

void setDependencies(Map<String, Object> map) {
this[dependenciesKey] = map
this.put(dependenciesKey, map)
}

void addDependencies(Map<String, Object> map) {
Expand All @@ -68,6 +68,10 @@ class UnityProjectManifest extends HashMap<String, Object> implements GroovyInte
void addDependency(String key, Object value) {
getDependencies()[key] = value
}

Object getDependencyVersion(String key) {
return getDependencies()[key]
}
}


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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())

}
}
Original file line number Diff line number Diff line change
@@ -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<RegularFile> 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<RegularFile> 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<String> 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()
}
}
50 changes: 47 additions & 3 deletions src/main/groovy/wooga/gradle/unity/tasks/GenerateSolution.groovy
Original file line number Diff line number Diff line change
@@ -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<Directory> 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"
})

}
}
Loading

0 comments on commit 0b2b025

Please sign in to comment.