Skip to content
Merged
Changes from all 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
191 changes: 100 additions & 91 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import com.jetbrains.plugin.structure.base.utils.isFile
import org.jetbrains.changelog.markdownToHTML
import org.jetbrains.intellij.platform.gradle.Constants
import org.jetbrains.intellij.platform.gradle.IntelliJPlatformType
import java.util.zip.ZipFile
import kotlin.io.path.absolute
import kotlin.io.path.isDirectory

Expand Down Expand Up @@ -47,13 +48,42 @@ tasks.wrapper {
distributionUrl = "https://cache-redirector.jetbrains.com/services.gradle.org/distributions/gradle-${gradleVersion}-all.zip"
}

dependencies {
intellijPlatform {
rider(ProductVersion, useInstaller = false)
jetbrainsRuntime()
testFramework(org.jetbrains.intellij.platform.gradle.TestFrameworkType.Bundled)

// TODO: add plugins
// bundledPlugin("uml")
// bundledPlugin("com.jetbrains.ChooseRuntime:1.0.9")
}

testImplementation("org.testng:testng:7.10.2")
}

intellijPlatform {
pluginVerification {
ides {
create(IntelliJPlatformType.Rider, ProductVersion)
}
}

pluginConfiguration {
version = providers.gradleProperty("PluginVersion")
ideaVersion {
sinceBuild = providers.gradleProperty("pluginSinceBuild")
}
}
}

version = extra["PluginVersion"] as String


// ============ Plugin File Definitions ==============

val pluginStagingDir = layout.buildDirectory.dir("plugin-staging").get().asFile
val pluginContentDir = file("${pluginStagingDir}/${rootProject.name}")
val pluginStagingContentDir = file("${pluginStagingDir}/${rootProject.name}")
val signingManifestFile = file("${pluginStagingDir}/files-to-sign.txt")

val dotNetSrcDir = File(projectDir, "src/dotnet")
Expand All @@ -72,11 +102,13 @@ val dotNetFilesToSign = listOf(
"${DotnetPluginId}.dll",
)

// JAR files that need signing (only our own code, not third-party dependencies)
val jarFilesToSign = listOf(
"${rootProject.name}-${version}.jar",
"${rootProject.name}-${version}-searchableOptions.jar",
)
// JAR files that need signing (only our own code)
val jarFilesToSign = mutableListOf<String>().apply {
add("${rootProject.name}-${version}.jar")
if (intellijPlatform.buildSearchableOptions.get()) {
add("${rootProject.name}-${version}-searchableOptions.jar")
}
}.toList()


tasks.processResources {
Expand Down Expand Up @@ -174,8 +206,8 @@ val testDotNet by tasks.registering {

// ========= Two-Phase Build for Signing Support ================

// Preparation for signing. Build all dll's and jar's and puts them into ${pluginStagingDir}
val preparePluginForSigning by tasks.registering(Sync::class) {
// Preparation for plugin internals signing. Build all dll's and jar's and puts them into ${pluginStagingDir}
val preparePluginInternalsForSigning by tasks.registering(Sync::class) {
description = "Prepares plugin files for signing and generates signing manifest"
group = "build"

Expand All @@ -190,20 +222,27 @@ val preparePluginForSigning by tasks.registering(Sync::class) {
}

// Destination: the plugin content directory inside staging
into(pluginContentDir)
into(pluginStagingContentDir)

// Capture script-level vals into locals to avoid error with gradle task cache
val signingManifestFile = signingManifestFile
val pluginStagingDir = pluginStagingDir
val jarFilesToSign = jarFilesToSign
val dotNetFilesToSign = dotNetFilesToSign
val projectName = rootProject.name

// After syncing, generate the signing manifest
doLast {
val filesToSign = mutableListOf<String>()

// Add JAR files that need signing (only our own code)
jarFilesToSign.forEach { jarName ->
filesToSign.add("${rootProject.name}/lib/${jarName}")
filesToSign.add("${projectName}/lib/${jarName}")
}

// Add .NET files that need signing
dotNetFilesToSign.forEach { fileName ->
filesToSign.add("${rootProject.name}/dotnet/${fileName}")
filesToSign.add("${projectName}/dotnet/${fileName}")
}

// Write manifest
Expand All @@ -222,117 +261,77 @@ val validatePluginStaging by tasks.registering {
description = "Validates that plugin staging directory exists and contains required files"
group = "build"

// Capture script-level vals into locals to avoid error with gradle task cache
val pluginStagingContentDir = pluginStagingContentDir
val dotNetOutputFiles = dotNetOutputFiles
val jarFilesToSign = jarFilesToSign

doLast {
if (!pluginContentDir.exists()) {
if (!pluginStagingContentDir.exists()) {
throw RuntimeException(
"Plugin staging directory not found: ${pluginContentDir}\n" +
"Run './gradlew preparePluginForSigning' first."
"Plugin staging directory not found: ${pluginStagingContentDir}\n" +
"Run './gradlew preparePluginInternalsForSigning' first."
)
}

// Validate expected .NET output files exist
dotNetOutputFiles.forEach { fileName ->
val file = file("${pluginContentDir}/dotnet/${fileName}")
val file = pluginStagingContentDir.resolve("dotnet/${fileName}")
if (!file.exists()) throw RuntimeException("Expected .NET file not found: ${file}")
}

// Validate expected JAR files exist
jarFilesToSign.forEach { jarName ->
val file = file("${pluginContentDir}/lib/${jarName}")
val file = pluginStagingContentDir.resolve("lib/${jarName}")
if (!file.exists()) throw RuntimeException("Expected JAR file not found: ${file}")
}
}
}

// Assembles the final zip-archive by taking the files from ${pluginStagingDir}
// Assembles the final zip-archive from staged (potentially externally signed) files.
// Produces a ZIP with "-from-staging" suffix by default (override with -PoutputPluginFileSuffix=<value>)
// Can be used in pipeline: preparePluginInternalsForSigning -> external sign -> assemblePlugin
val assemblePlugin by tasks.registering(Zip::class) {
description = "Assembles the final plugin ZIP from staged files"
description = "Assembles the plugin ZIP from staged files with '-from-staging' classifier"
group = "build"

dependsOn(validatePluginStaging)

from(pluginStagingDir) {
// Include plugin content directory
include("${rootProject.name}/**")
// Exclude signing manifest from final ZIP
exclude("files-to-sign.txt")
}
from(pluginStagingDir)
include("${rootProject.name}/**")
exclude("files-to-sign.txt")

// Use same naming convention as original BuildPluginTask:
// archiveBaseName comes from plugin.xml (via IntelliJ plugin extension)
archiveBaseName.convention(intellijPlatform.projectName)
archiveClassifier.set(providers.gradleProperty("outputPluginFileSuffix").orElse("from-staging"))
destinationDirectory.set(layout.buildDirectory.dir("distributions"))

// Register artifact to INTELLIJ_PLATFORM_DISTRIBUTION configuration (same as original BuildPluginTask)
// This ensures publishPlugin and other dependent tasks can find the archive
val intellijPlatformDistributionConfiguration = configurations[Constants.Configurations.INTELLIJ_PLATFORM_DISTRIBUTION]
artifacts.add(intellijPlatformDistributionConfiguration.name, this)

doLast {
// Copy to output directory
copy {
from(archiveFile)
into("${rootDir}/output")
}

// TODO: See also org.jetbrains.changelog: https://github.com/JetBrains/gradle-changelog-plugin
val changelogText = file("${rootDir}/CHANGELOG.md").readText()
val changelogMatches = Regex("(?s)(-.+?)(?=##|$)").findAll(changelogText)
val changeNotes = changelogMatches.map {
it.groups[1]!!.value.replace("(?s)- ".toRegex(), "\u2022 ").replace("`", "").replace(",", "%2C").replace(";", "%3B")
}.take(1).joinToString()

exec {
executable(layout.projectDirectory.file("dotnet.cmd"))
args("pack", "${DotnetSolution}", "--configuration", BuildConfiguration, "--output", "${rootDir}/output", "/p:PackageReleaseNotes=${changeNotes}", "/p:PackageVersion=${version}")
workingDir(rootDir)
}

println("Plugin assembled: output/${rootProject.name}-${version}.zip")
}
}

tasks.buildPlugin {
// Disable the IntelliJ plugin's default buildPlugin behavior for ZIP creation
// and make it use our two-phase approach instead
actions.clear()

// Make it depend on our assembly task
dependsOn(preparePluginForSigning)
dependsOn(assemblePlugin)

// Phase 2 (assemblePlugin + all its dependencies) must run after Phase 1
assemblePlugin.get().mustRunAfter(preparePluginForSigning)
assemblePlugin.get().taskDependencies.getDependencies(assemblePlugin.get()).forEach { task ->
task.mustRunAfter(preparePluginForSigning)
}
}
// ==============================================================

dependencies {
intellijPlatform {
rider(ProductVersion, useInstaller = false)
jetbrainsRuntime()
testFramework(org.jetbrains.intellij.platform.gradle.TestFrameworkType.Bundled)

// TODO: add plugins
// bundledPlugin("uml")
// bundledPlugin("com.jetbrains.ChooseRuntime:1.0.9")
}
// buildPlugin keeps its default Zip behavior (sources from prepareSandbox + jarSearchableOptions).
// We add dependsOn(preparePluginInternalsForSigning) to ensure the staging directory is populated,
// then verify the archive matches the staging directory.
tasks.buildPlugin {
// Ensure that the staging directory is populated
dependsOn(preparePluginInternalsForSigning)

testImplementation("org.testng:testng:7.10.2")
}
val pluginStagingContentDir = pluginStagingContentDir
val projectName = rootProject.name

intellijPlatform {
pluginVerification {
ides {
create(IntelliJPlatformType.Rider, ProductVersion)
doLast {
// Verify the archive matches the staging directory to be sure that
// buildPlugin and preparePluginInternalsForSigning+assemblePlugin produces the same results
val zipFiles = ZipFile(archiveFile.get().asFile).use {
it.entries().asSequence().filterNot { e -> e.isDirectory }.map { e -> e.name }.sorted().toList()
}
}

pluginConfiguration {
version = providers.gradleProperty("PluginVersion")
ideaVersion {
sinceBuild = providers.gradleProperty("pluginSinceBuild")
val stagingFiles = pluginStagingContentDir.walkTopDown().filter { it.isFile }
.map { "${projectName}/${it.relativeTo(pluginStagingContentDir).path.replace('\\', '/')}" }
.sorted().toList()

check(zipFiles == stagingFiles) {
"Plugin archive and staging directory are out of sync!\n" +
" Only in archive: ${zipFiles - stagingFiles.toSet()}\n" +
" Only in staging: ${stagingFiles - zipFiles.toSet()}"
}
}
}
Expand Down Expand Up @@ -402,6 +401,16 @@ tasks.prepareSandbox {
}
}

tasks.prepareTestSandbox {
dependsOn(compileDotNet)

dotNetOutputFiles.forEach { fileName ->
from("${dotNetOutputDir}/${fileName}") {
into("${rootProject.name}/dotnet")
}
}
}

tasks.publishPlugin {
dependsOn(testDotNet)
dependsOn(tasks.buildPlugin)
Expand Down
Loading