import com.jetbrains.plugin.structure.base.utils.isFile import org.apache.tools.ant.taskdefs.condition.Os import org.gradle.api.tasks.testing.logging.TestExceptionFormat import org.jetbrains.changelog.Changelog import org.jetbrains.intellij.platform.gradle.tasks.PrepareSandboxTask import org.jetbrains.intellij.platform.gradle.tasks.RunIdeTask import org.jetbrains.kotlin.gradle.dsl.JvmTarget import org.jetbrains.kotlin.gradle.tasks.KotlinCompile import java.io.ByteArrayOutputStream import java.security.MessageDigest import kotlin.io.path.absolute import kotlin.io.path.isDirectory import org.jetbrains.intellij.platform.gradle.Constants import org.jetbrains.intellij.platform.gradle.TestFrameworkType import kotlin.io.path.pathString plugins { id("me.filippov.gradle.jvm.wrapper") id("org.jetbrains.changelog") version "2.0.0" id("org.jetbrains.intellij.platform") kotlin("jvm") } repositories { maven("https://cache-redirector.jetbrains.com/intellij-dependencies") maven("https://cache-redirector.jetbrains.com/intellij-repository/releases") maven("https://cache-redirector.jetbrains.com/intellij-repository/snapshots") maven("https://cache-redirector.jetbrains.com/maven-central") intellijPlatform { defaultRepositories() jetbrainsRuntime() } } dependencies { testImplementation("com.fasterxml.jackson.core:jackson-databind:2.14.0") } apply { plugin("kotlin") } kotlin { sourceSets { main { kotlin.srcDir("src/rider/main/kotlin") } test { kotlin.srcDir("src/rider/test/kotlin") } } } sourceSets { main { kotlin.srcDir("src/rider/generated/kotlin") resources.srcDir("src/rider/main/resources") } } project.version = "${property("majorVersion")}." + "${property("minorVersion")}." + "${property("buildCounter")}" if (System.getenv("TEAMCITY_VERSION") != null) { logger.lifecycle("##teamcity[buildNumber '${project.version}']") } else { logger.lifecycle("Plugin version: ${project.version}") } val buildConfigurationProp = project.property("buildConfiguration").toString() val repoRoot by extra { project.rootDir } val isWindows by extra { Os.isFamily(Os.FAMILY_WINDOWS) } val idePluginId by extra { "RiderPlugin" } val dotNetSolutionId by extra { "UnrealLink" } val dotNetDir by extra { File(repoRoot, "src/dotnet") } val dotNetBinDir by extra { dotNetDir.resolve("$idePluginId.$dotNetSolutionId").resolve("bin") } val dotNetPluginId by extra { "$idePluginId.${project.name}" } val dotNetSolution by extra { File(repoRoot, "$dotNetSolutionId.sln") } val modelDir = File(repoRoot, "protocol/src/main/kotlin/model") val hashBaseDir = File(repoRoot, "build/rdgen") val ktOutputRelativePath = "src/rider/main/kotlin/com/jetbrains/rider/model" val cppOutputRoot = File(repoRoot, "src/cpp/RiderLink/Source/RiderLink/Public/Model") val csOutputRoot = File(repoRoot, "src/dotnet/RiderPlugin.UnrealLink/obj/model") val ktOutputRoot = File(repoRoot, ktOutputRelativePath) val riderLinkDir = File("$rootDir/src/cpp/RiderLink") val currentBranchName = getBranchName() fun TaskContainerScope.setupCleanup(task: Task) { withType { delete(task.outputs.files) } } fun getBranchName(): String { val stdOut = ByteArrayOutputStream() val result = project.exec { executable = "git" args = listOf("rev-parse", "--abbrev-ref", "HEAD") workingDir = projectDir standardOutput = stdOut } if (result.exitValue == 0) { val output = stdOut.toString().trim() if (output.isNotEmpty()) return output } return "net222" } fun getProductMonorepoRoot(): File? { var currentDir = repoRoot while (currentDir.parent != null) { if (currentDir.resolve(".ultimate.root.marker").exists()) { return currentDir } currentDir = currentDir.parentFile } return null } changelog { version.set(project.version.toString()) // https://github.com/JetBrains/gradle-changelog-plugin/blob/main/src/main/kotlin/org/jetbrains/changelog/Changelog.kt#L23 // This is just common semVerRegex with the addition of a forth optional group (number) ( x.x.x[.x][-alpha43] ) headerParserRegex.set( """^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)\.?(0|[1-9]\d*)?(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*) (?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?${'$'}""" .trimMargin().toRegex()) groups.set(listOf("Added", "Changed", "Deprecated", "Removed", "Fixed", "Known Issues")) keepUnreleasedSection.set(true) itemPrefix.set("-") } dependencies { intellijPlatform { val dependencyPath = File(projectDir, "dependencies") if (dependencyPath.exists()) { val localPath = dependencyPath.canonicalPath local(localPath) logger.lifecycle("Will use ${File(localPath, "build.txt").readText()} from $localPath as RiderSDK") } else { val version = "${project.property("majorVersion")}-SNAPSHOT" logger.lifecycle("*** Using Rider SDK $version from intellij-snapshots repository") rider(version, useInstaller = false) } jetbrainsRuntime() instrumentationTools() // Workaround for https://youtrack.jetbrains.com/issue/IDEA-179607 bundledPlugin("rider.intellij.plugin.appender") bundledPlugin("com.intellij.cidr.debugger") bundledPlugin("com.jetbrains.rider-cpp") // TODO: Temporary I hope hope hope bundledLibrary(provider { project.intellijPlatform.platformPath.resolve("lib/testFramework.jar").pathString }) } } intellijPlatform { tasks { val currentReleaseNotesAsHtml = """

New in "${project.version}"

${changelog.renderItem(changelog.getLatest(), Changelog.OutputType.HTML)}

See the CHANGELOG for more details and history.

""".trimIndent() val currentReleaseNotesAsMarkdown = """ ## New in ${project.version} ${changelog.renderItem(changelog.getLatest())} See the [CHANGELOG](https://github.com/JetBrains/UnrealLink/blob/$currentBranchName/CHANGELOG.md) for more details and history. """.trimIndent() val dumpCurrentChangelog by registering { val outputFile = layout.buildDirectory.file("release_notes.md") outputs.file(outputFile) doLast { file(outputFile).writeText(currentReleaseNotesAsMarkdown) } } // PatchPluginXml gets the latest (always Unreleased) section from the current changelog and writes it into plugin.xml // dumpCurrentChangelog dumps the same section to file (for Marketplace changelog) // After, patchChangelog rename [Unreleased] to [202x.x.x.x] and create new empty Unreleased. // So order is important! patchPluginXml { changeNotes.set( provider { currentReleaseNotesAsHtml }) } patchChangelog { mustRunAfter(patchPluginXml, dumpCurrentChangelog) } publishPlugin { dependsOn(patchPluginXml, dumpCurrentChangelog, patchChangelog) token.set(System.getenv("UNREALLINK_intellijPublishToken")) val pubChannels = project.findProperty("publishChannels") if ( pubChannels != null) { val chan = pubChannels.toString().split(',') println("Channels for publish $chan") channels.set(chan) } else { channels.set(listOf("alpha")) } } } } val riderModel: Configuration by configurations.creating { isCanBeConsumed = true isCanBeResolved = false } artifacts { add(riderModel.name, provider { intellijPlatform.platformPath.resolve("lib/rd/rider-model.jar").also { check(it.isFile) { "rider-model.jar is not found at $riderModel" } } }) { builtBy(Constants.Tasks.INITIALIZE_INTELLIJ_PLATFORM_PLUGIN) } } tasks { val dotNetSdkPath by lazy { val sdkPath = intellijPlatform.platformPath.resolve("lib/DotNetSdkForRdPlugins").absolute() assert(sdkPath.isDirectory()) println(".NET SDK path: $sdkPath") return@lazy sdkPath.toRealPath() } instrumentCode { enabled = false } withType().configureEach { maxHeapSize = "4096m" } withType().configureEach { maxHeapSize = "4096m" if (project.hasProperty("ignoreFailures")) { ignoreFailures = true } useTestNG { } testLogging { showStandardStreams = true showExceptions = true exceptionFormat = TestExceptionFormat.FULL } } withType().configureEach { dependsOn("generateModels") compilerOptions { jvmTarget.set(JvmTarget.JVM_21) } } val prepareRiderBuildProps by registering { group = "RiderBackend" val generatedFile = layout.buildDirectory.file("DotNetSdkPath.generated.props") inputs.property("dotNetSdkFile", { dotNetSdkPath.toString() }) outputs.file(generatedFile) doLast { project.file(generatedFile).writeText( """ | | $dotNetSdkPath | |""".trimMargin() ) } } val prepareNuGetConfig by registering { group = "RiderBackend" dependsOn(prepareRiderBuildProps) val generatedFile = project.projectDir.resolve("NuGet.Config") inputs.property("dotNetSdkFile", { dotNetSdkPath.toString() }) outputs.file(generatedFile) doLast { val dotNetSdkFile = dotNetSdkPath logger.info("dotNetSdk location: '$dotNetSdkFile'") val nugetConfigText = """ | | | | | | | """.trimMargin() generatedFile.writeText(nugetConfigText) logger.info("Generated content:\n$nugetConfigText") } } val buildResharperHost by registering { group = "RiderBackend" description = "Build backend for Rider" dependsOn(":generateModels", prepareNuGetConfig) inputs.file(file(dotNetSolution)) inputs.dir(file("$repoRoot/src/dotnet")) outputs.dir(file("$repoRoot/src/dotnet/RiderPlugin.UnrealLink/bin/RiderPlugin.UnrealLink/$buildConfigurationProp")) doLast { val warningsAsErrors: String by project.extra val buildArguments = listOf( "build", dotNetSolution.canonicalPath, "-consoleLoggerParameters:ErrorsOnly", "/p:Configuration=$buildConfigurationProp", "/p:Version=${project.version}", "/p:TreatWarningsAsErrors=$warningsAsErrors", "/bl:${dotNetSolution.name}.binlog", "/nologo" ) logger.info("call dotnet.cmd with '{}'", buildArguments) project.exec { executable = "$rootDir/tools/dotnet.cmd" args = buildArguments workingDir = dotNetSolution.parentFile } } } val patchUpluginVersion by register("patchUpluginVersion") { val pathToUpluginTemplate = File("${project.rootDir}/src/cpp/RiderLink/RiderLink.uplugin.template") val filePathToUplugin = File("${project.rootDir}/src/cpp/RiderLink/RiderLink.uplugin") inputs.file(pathToUpluginTemplate) inputs.property("version", project.version) outputs.file(filePathToUplugin) doLast { if(filePathToUplugin.exists()) filePathToUplugin.delete() pathToUpluginTemplate.copyTo(filePathToUplugin) val text = filePathToUplugin.readLines().map { it.replace("%PLUGIN_VERSION%", "${project.version}") } filePathToUplugin.writeText(text.joinToString(System.lineSeparator())) } } withType { delete(patchUpluginVersion.outputs.files) } val generateChecksum by register("generateChecksum") { dependsOn(":generateModels") val upluginFile = riderLinkDir.resolve("RiderLink.uplugin.template") val resourcesDir = riderLinkDir.resolve("Resources") val sourceDir = riderLinkDir.resolve("Source") val checksumFile = riderLinkDir.resolve("Resources/checksum") inputs.file(upluginFile) inputs.dir(resourcesDir) inputs.dir(sourceDir) outputs.file(checksumFile) doLast { checksumFile.delete() val inputFiles = sequence{ yield(upluginFile) resourcesDir.walkTopDown().forEach { if(it.isFile && (it.nameWithoutExtension != "checksum")) yield(it) } sourceDir.walkTopDown().forEach { if(it.isFile) yield(it) } } val instance = MessageDigest.getInstance("MD5") inputFiles.forEach { instance.update(it.readBytes()) } checksumFile.writeBytes(instance.digest()) } } withType { delete(generateChecksum.outputs.files) } val packCppSide by registering(Zip::class) { dependsOn(patchUpluginVersion) dependsOn(":generateModels") dependsOn(generateChecksum) archiveFileName.set("RiderLink.zip") excludes.addAll(arrayOf("RiderLink.uplugin.template", "Intermediate", "Binaries")) destinationDirectory.set(File("$rootDir/build/distributions")) from("$rootDir/src/cpp/RiderLink") } withType { dependsOn(buildResharperHost, packCppSide) outputs.upToDateWhen { false } //need to dotnet artifacts be included when only dotnet sources were changed val outputFolder = dotNetBinDir .resolve(dotNetPluginId) .resolve(buildConfigurationProp) val dllFiles = listOf( File(outputFolder, "$dotNetPluginId.dll"), File(outputFolder, "$dotNetPluginId.pdb") ) dllFiles.forEach { from(it) { into("${intellijPlatform.projectName.get()}/dotnet") } } from(packCppSide.get().archiveFile) { into("${intellijPlatform.projectName.get()}/EditorPlugin") } doLast { dllFiles.forEach { file -> if (!file.exists()) throw RuntimeException("File $file does not exist") } } } val generateModels by registering { group = "protocol" description = "Generates protocol models." dependsOn(":protocol:rdgen") } withType { delete(csOutputRoot, cppOutputRoot, ktOutputRoot) } buildSearchableOptions { } val getUnrealEngineProject by register("getUnrealEngineProject") { doLast { val ueProjectPathTxt = rootDir.resolve("UnrealEngineProjectPath.txt") if (ueProjectPathTxt.exists()) { val ueProjectPath = ueProjectPathTxt.readText() val ueProjectPathDir = File(ueProjectPath) if (!ueProjectPathDir.exists()) throw AssertionError("$ueProjectPathDir doesn't exist") if (!ueProjectPathDir.isDirectory) throw AssertionError("$ueProjectPathDir is not directory") val isUEProject = ueProjectPathDir.listFiles()?.any { it.extension == "uproject" } if (isUEProject == true) { extra["UnrealProjectPath"] = ueProjectPathDir } else { throw AssertionError("Add path to a valid UnrealEngine project folder to: $ueProjectPathTxt") } } else { ueProjectPathTxt.createNewFile() throw AssertionError("Add path to a valid UnrealEngine project folder to: $ueProjectPathTxt") } } } val symlinkPluginToUnrealProject by registering { dependsOn(getUnrealEngineProject) dependsOn(patchUpluginVersion) doLast { val unrealProjectPath = getUnrealEngineProject.extra["UnrealProjectPath"] as File val targetDir = File("$unrealProjectPath/Plugins/Developer/RiderLink") if(targetDir.exists()) { val stdOut = ByteArrayOutputStream() // Check if it's Junction val result = exec { commandLine = if(isWindows) listOf("cmd.exe", "/c", "fsutil", "reparsepoint", "query", targetDir.absolutePath, "|", "find", "Print Name:") else listOf("find", targetDir.absolutePath, "-maxdepth", "1", "-type", "l", "-ls") isIgnoreExitValue = true standardOutput = stdOut } // Check if it's Junction to local RiderLink if(result.exitValue == 0) { val output = stdOut.toString().trim() if (output.isNotEmpty()) { val pathToJunction = if (isWindows) output.substringAfter("Print Name:").trim() else output.substringAfter("->").trim() if (File(pathToJunction) == riderLinkDir) { println("Junction is already correct") throw StopExecutionException() } } } // If it's not Junction or if it's a Junction but doesn't point to local RiderLink - delete it targetDir.delete() } targetDir.parentFile.mkdirs() val stdOut = ByteArrayOutputStream() val result = exec { commandLine = if(isWindows) listOf("cmd.exe", "/c", "mklink", "/J", targetDir.absolutePath, riderLinkDir.absolutePath) else listOf("ln", "-s", riderLinkDir.absolutePath, targetDir.absolutePath) errorOutput = stdOut isIgnoreExitValue = true } if (result.exitValue != 0) { println(stdOut.toString().trim()) } } } wrapper { gradleVersion = "8.7" distributionUrl = "https://cache-redirector.jetbrains.com/services.gradle.org/distributions/gradle-${gradleVersion}-bin.zip" } }