build.gradle.kts (334 lines of code) (raw):

import com.jetbrains.rd.gradle.dependencies.kotlinVersion import groovy.json.JsonBuilder import groovy.json.JsonSlurper import okhttp3.MultipartBody import okhttp3.OkHttpClient import okhttp3.Request import okhttp3.RequestBody.Companion.asRequestBody import okhttp3.RequestBody.Companion.toRequestBody import org.gradle.api.tasks.testing.logging.TestExceptionFormat import org.jetbrains.kotlin.gradle.dsl.JvmTarget import org.jetbrains.kotlin.gradle.tasks.KotlinCompile import java.util.* import kotlin.time.Duration import kotlin.time.Duration.Companion.milliseconds import kotlin.time.Duration.Companion.minutes import kotlin.time.Duration.Companion.seconds import kotlin.time.toJavaDuration buildscript { dependencies { classpath("com.squareup.okhttp3:okhttp:4.12.0") } project.extra.apply { val repoRoot = rootProject.projectDir set("repoRoot", repoRoot) set("cppRoot", File(repoRoot, "rd-cpp")) set("ktRoot", File(repoRoot, "rd-kt")) set("csRoot", File(repoRoot, "rd-net")) } } plugins { base id("me.filippov.gradle.jvm.wrapper") version "0.14.0" } allprojects { plugins.apply("maven-publish") configurations.all { resolutionStrategy { force("org.jetbrains.kotlin:kotlin-stdlib:$kotlinVersion") force("org.jetbrains.kotlin:kotlin-reflect:$kotlinVersion") force("org.jetbrains.kotlin:kotlin-runtime:$kotlinVersion") force("org.jetbrains.kotlin:kotlin-stdlib-js:$kotlinVersion") } } repositories { mavenCentral() } tasks { withType<Test> { testLogging { showStandardStreams = true exceptionFormat = TestExceptionFormat.FULL } } withType<KotlinCompile> { compilerOptions { jvmTarget.set(JvmTarget.JVM_17) } } withType<JavaCompile> { targetCompatibility = "17" } } } val clean by tasks.getting(Delete::class) { delete(rootProject.buildDir) } if (System.getenv("TEAMCITY_VERSION") == null) { version = "SNAPSHOT" } tasks { val nuGetTargetDir = buildDir.resolve("artifacts").resolve("nuget") val publishingGroup = "publishing" val dotNetBuild by registering(Exec::class) { group = publishingGroup executable = projectDir.resolve("rd-net").resolve("dotnet.cmd").canonicalPath args("build", "/p:Configuration=Release", "/p:PackageVersion=$version", projectDir.resolve("rd-net").resolve("Rd.sln").canonicalPath) environment("DOTNET_NOLOGO", "1") environment("DOTNET_CLI_TELEMETRY_OPTOUT", "1") } val packNuGetLifetimes by registering(Exec::class) { group = publishingGroup dependsOn(dotNetBuild) executable = project.projectDir.resolve("rd-net").resolve("dotnet.cmd").canonicalPath args("pack", "--include-symbols", "/p:Configuration=Release", "/p:PackageVersion=$version", projectDir.resolve("rd-net").resolve("Lifetimes").resolve("Lifetimes.csproj").canonicalPath) environment("DOTNET_NOLOGO", "1") environment("DOTNET_CLI_TELEMETRY_OPTOUT", "1") } val copyNuGetLifetimes by registering(Copy::class) { group = publishingGroup dependsOn(packNuGetLifetimes) from("${projectDir.resolve("rd-net").resolve("Lifetimes").resolve("bin").resolve("Release").canonicalPath}${File.separator}") include("*.nupkg") include("*.snupkg") into(buildDir.resolve("artifacts").resolve("nuget")) } val packDotNetRdFramework by registering(Exec::class) { group = publishingGroup dependsOn(dotNetBuild) executable = project.projectDir.resolve("rd-net").resolve("dotnet.cmd").canonicalPath args("pack", "--include-symbols", "/p:Configuration=Release", "/p:PackageVersion=$version", projectDir.resolve("rd-net").resolve("RdFramework").resolve("RdFramework.csproj").canonicalPath) environment("DOTNET_NOLOGO", "1") environment("DOTNET_CLI_TELEMETRY_OPTOUT", "1") } val copyNuGetRdFramework by registering(Copy::class) { group = publishingGroup dependsOn(packDotNetRdFramework) from("${projectDir.resolve("rd-net").resolve("RdFramework").resolve("bin").resolve("Release").canonicalPath}${File.separator}") include("*.nupkg") include("*.snupkg") into(buildDir.resolve("artifacts").resolve("nuget")) } val packDotNetRdFrameworkReflection by registering(Exec::class) { group = publishingGroup dependsOn(dotNetBuild) executable = project.projectDir.resolve("rd-net").resolve("dotnet.cmd").canonicalPath args("pack", "--include-symbols", "/p:Configuration=Release", "/p:PackageVersion=$version", projectDir.resolve("rd-net").resolve("RdFramework.Reflection").resolve("RdFramework.Reflection.csproj").canonicalPath) environment("DOTNET_NOLOGO", "1") environment("DOTNET_CLI_TELEMETRY_OPTOUT", "1") } val copyNuGetRdFrameworkReflection by registering(Copy::class) { group = publishingGroup dependsOn(packDotNetRdFrameworkReflection) from("${projectDir.resolve("rd-net").resolve("RdFramework.Reflection").resolve("bin").resolve("Release").canonicalPath}${File.separator}") include("*.nupkg") include("*.snupkg") into(buildDir.resolve("artifacts").resolve("nuget")) } val cleanupArtifacts by registering { group = publishingGroup doLast { if (nuGetTargetDir.exists()) { nuGetTargetDir.deleteRecursively() } } } val createNuGetPackages by registering { group = publishingGroup dependsOn(cleanupArtifacts, copyNuGetLifetimes, copyNuGetRdFramework, copyNuGetRdFrameworkReflection) } fun enableNuGetPublishing(url: String, apiKey: String) { val args = mutableListOf<Any>( project.projectDir.resolve("rd-net").resolve("dotnet.cmd").canonicalPath, "nuget", "push", "--source", url, "--api-key", apiKey ) for (file in nuGetTargetDir.listFiles()?.filter { it.extension == "nupkg" } ?: emptyList()) { exec { val argsForCurrentFile = (args + file).toTypedArray() commandLine(*argsForCurrentFile) } } } val publishNuGet by registering { group = publishingGroup dependsOn(createNuGetPackages) val deployToNuGetOrg = rootProject.extra["deployNuGetToNuGetOrg"].toString().toBoolean() val deployToInternal = rootProject.extra["deployNuGetToInternal"].toString().toBoolean() doLast { if (deployToNuGetOrg) { val nuGetOrgApiKey = rootProject.extra["nuGetOrgApiKey"].toString() enableNuGetPublishing("https://api.nuget.org/v3/index.json", nuGetOrgApiKey) } if (deployToInternal) { val internalFeedUrl = rootProject.extra["internalNuGetFeedUrl"].toString() val internalFeedApiKey = rootProject.extra["internalDeployKey"].toString() enableNuGetPublishing(internalFeedUrl, internalFeedApiKey) } } } val packSonatypeCentralBundle by registering(Zip::class) { group = publishingGroup dependsOn( ":rd-core:publishAllPublicationsToArtifactsRepository", ":rd-framework:publishAllPublicationsToArtifactsRepository", ":rd-gen:publishAllPublicationsToArtifactsRepository", ":rd-swing:publishAllPublicationsToArtifactsRepository", ":rd-text:publishAllPublicationsToArtifactsRepository" ) from(layout.buildDirectory.dir("artifacts/maven")) archiveFileName.set("bundle.zip") destinationDirectory.set(layout.buildDirectory) } fun base64Auth(userName: String, accessToken: String): String = Base64.getEncoder().encode("$userName:$accessToken".toByteArray()).toString(Charsets.UTF_8) fun deployToCentralPortal( bundleFile: File, uriBase: String, isUserManaged: Boolean, deploymentName: String, userName: String, accessToken: String ): String { // https://central.sonatype.org/publish/publish-portal-api/#uploading-a-deployment-bundle val publishingType = if (isUserManaged) "USER_MANAGED" else "AUTOMATIC" val uri = uriBase.trimEnd('/') + "/api/v1/publisher/upload?name=$deploymentName&publishingType=$publishingType" val base64Auth = base64Auth(userName, accessToken) println("Sending request to $uri...") val client = OkHttpClient() val request = Request.Builder() .url(uri) .header("Authorization", "Bearer $base64Auth") .post( MultipartBody.Builder() .setType(MultipartBody.FORM) .addFormDataPart("bundle", bundleFile.name, bundleFile.asRequestBody()) .build() ) .build() val response = client.newCall(request).execute() val statusCode = response.code println("Upload status code: $statusCode") val uploadResult = response.body!!.string() println("Upload result: $uploadResult") if (statusCode == 201) { return uploadResult } else { error("Upload error to Central repository. Status code $statusCode.") } } fun waitForUploadToSucceed( uriBase: String, deploymentId: String, isUserManaged: Boolean, userName: String, accessToken: String, maxTimeout: Duration, minTimeBetweenAttempts: Duration ) { val uri = uriBase.trimEnd('/') + "/api/v1/publisher/status?id=$deploymentId" val base64Auth = base64Auth(userName, accessToken) var timeSpent = Duration.ZERO var attemptNumber = 1 var terminatingState = false println("Polling for deployment status for $maxTimeout: $uri") while (timeSpent < maxTimeout) { val remainingTime = maxTimeout - timeSpent println("Polling attempt ${attemptNumber++}, remaining time ${remainingTime}.") val client = OkHttpClient().newBuilder() .callTimeout(remainingTime.toJavaDuration()) .build() val beforeMs = System.currentTimeMillis() try { val request = Request.Builder() .url(uri) .header("Authorization", "Bearer $base64Auth") .post("".toRequestBody()) .build() val response = client.newCall(request).execute() val code = response.code if (code != 200) { error("Response code $code: ${response.body?.string()}") } val jsonResult = JsonSlurper().parse(response.body?.bytes() ?: error("Empty response body.")) as Map<*, *> val state = jsonResult["deploymentState"] println("Current state: $state.") when(state) { "PENDING", "VALIDATING", "PUBLISHING" -> {} "VALIDATED" -> { terminatingState = true if (isUserManaged) { println("Validated successfully.") return } error("State error: deployment is not user managed, but signals it requires a UI interaction.") } "PUBLISHED" -> { terminatingState = true if (!isUserManaged) { println("Published successfully.") return } error("State error: deployment is user managed, but signals it has been published.") } "FAILED" -> { terminatingState = true // The documentation provides no type information for the errors field, so we have to treat // them as opaque. val errors = jsonResult["errors"] val errorsAsString = JsonBuilder(errors).toPrettyString() error("Deployment failed. Errors: $errorsAsString") } else -> logger.warn("Unknown deployment state: $state") } } catch (e: Exception) { if (terminatingState) { throw e } logger.warn("Error during HTTP request: ${e.message}") } finally { val afterMs = System.currentTimeMillis() var attemptTime = (afterMs - beforeMs).coerceAtLeast(0L).milliseconds if (attemptTime < minTimeBetweenAttempts) { val sleepTime = minTimeBetweenAttempts - attemptTime Thread.sleep(sleepTime.inWholeMilliseconds) attemptTime = minTimeBetweenAttempts } timeSpent += attemptTime } } } val publishMavenToCentralPortal by registering { group = publishingGroup dependsOn(packSonatypeCentralBundle) doLast { val uriBase = rootProject.extra["centralPortalUrl"] as String val userName = rootProject.extra["centralPortalUserName"] as String val accessToken = rootProject.extra["centralPortalToken"] as String val isUserManaged = false val deploymentId = deployToCentralPortal( bundleFile = packSonatypeCentralBundle.get().archiveFile.get().asFile, uriBase, isUserManaged, deploymentName = "rd-$version", userName, accessToken ) waitForUploadToSucceed( uriBase, deploymentId, isUserManaged, userName, accessToken, maxTimeout = 60.minutes, minTimeBetweenAttempts = 5.seconds ) } } named("publish") { dependsOn(publishNuGet) val deployToCentral = rootProject.extra["deployMavenToCentralPortal"].toString().toBoolean() if (deployToCentral) { dependsOn(publishMavenToCentralPortal) } } }