Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

generateSolution supports unity >= 2022 [ATLAS-1772] #196

Merged
merged 5 commits into from
Apr 23, 2024
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -457,8 +458,15 @@ class UnityPluginIntegrationSpec extends UnityIntegrationSpec {
}

@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 +482,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")

}
}
7 changes: 6 additions & 1 deletion src/main/groovy/wooga/gradle/unity/UnityPlugin.groovy
Original file line number Diff line number Diff line change
Expand Up @@ -418,16 +418,21 @@ class UnityPlugin implements Plugin<Project> {
task.group = GROUP
task.projectManifestFile.convention(extension.projectManifestFile)
task.upmPackages.putAll(extension.upmPackages)
//needed in order to be able to generate solutions
task.upmPackages.put("com.unity.ide.rider", "3.0.28")
Azurelol marked this conversation as resolved.
Show resolved Hide resolved
task.upmPackages.putAll(extension.enableTestCodeCoverage.map {
it ? ["com.unity.testtools.codecoverage": "1.1.0"] : [:]
})
}
project.tasks.withType(Test).configureEach { testTask ->
testTask.dependsOn(addUPMPackagesTask)
}
project.tasks.withType(GenerateSolution).configureEach {genSolutionTask ->
genSolutionTask.dependsOn(addUPMPackagesTask)
}
addUPMPackagesTask.configure { task ->
task.onlyIf {
def upmPackageCount = task.upmPackages.forUseAtConfigurationTime().getOrElse([:]).size()
def upmPackageCount = task.upmPackages.getOrElse([:]).size()
upmPackageCount > 0
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,58 @@
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 RunCSScript {
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)
}



class GenerateSolution extends UnityTask {

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.sourceCsScript.convention(defaultScript)
this.destCsScript.convention(this.sourceCsScript)
this.executeMethod.set(project.provider {
unityVersion.majorVersion >= 2022?
"Wooga.UnityPlugin.DefaultSolutionGenerator.GenerateSolution" :
"UnityEditor.SyncVS.SyncSolution"
})

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

Expand Down
82 changes: 82 additions & 0 deletions src/main/groovy/wooga/gradle/unity/tasks/RunCSScript.groovy
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
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

class RunCSScript extends UnityTask {
Joaquimmnetto marked this conversation as resolved.
Show resolved Hide resolved

private final RegularFileProperty sourceCsScript = objects.fileProperty()

@InputFile
RegularFileProperty getSourceCsScript() {
Joaquimmnetto marked this conversation as resolved.
Show resolved Hide resolved
return sourceCsScript
}

void setSourceCsScript(Provider<RegularFile> sourceCsScript) {
this.sourceCsScript.set(sourceCsScript)
}

void setSourceCsScript(RegularFile sourceCsScript) {
this.sourceCsScript.set(sourceCsScript)
}

void setSourceCsScript(File sourceCsScript) {
this.sourceCsScript.set(sourceCsScript)
}

private final RegularFileProperty destCsScript = objects.fileProperty()

@InputFile
RegularFileProperty getDestCsScript() {
Joaquimmnetto marked this conversation as resolved.
Show resolved Hide resolved
return destCsScript
}

void setDestCsScript(Provider<RegularFile> destCsScript) {
this.destCsScript.set(destCsScript)
}

void setDestCsScript(RegularFile destCsScript) {
this.destCsScript.set(destCsScript)
}

void setDestCsScript(File destCsScript) {
this.destCsScript.set(destCsScript)
}

@Override //this input is mandatory for this task, so overriding the previous annotation.
@Input
Property<String> getExecuteMethod() {
return super.getExecuteMethod()
}

RunCSScript() {
finalizedBy(project.tasks.register("_${this.name}_cleanup") {
onlyIf {
destCsScript.present && destCsScript.get().asFile.file
}
doLast {
def baseFile = destCsScript.get().asFile
def metafile = new File(baseFile.absolutePath + ".meta")
baseFile.delete()
metafile.delete()

}
})
}

@Override
protected void preExecute() {
Files.copy(sourceCsScript.get().asFile.toPath(), destCsScript.get().asFile.toPath())
destCsScript.asFile.get().deleteOnExit()
// if(!executeMethod.present) {
// throw new IllegalArgumentException("'executeMethod' property should be present to run ")
// }
}
}
47 changes: 47 additions & 0 deletions src/main/resources/DefaultSolutionGenerator.cs
Original file line number Diff line number Diff line change
@@ -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<IExternalCodeEditor> 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<IExternalCodeEditor>;

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