build.gradle.kts 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490
  1. // Copyright 2009-2025 Weibo, Inc.
  2. // SPDX-FileCopyrightText: 2025 Weibo, Inc.
  3. //
  4. // SPDX-License-Identifier: APACHE2.0
  5. // SPDX-License-Identifier: Apache-2.0
  6. // Convenient for reading variables from gradle.properties
  7. fun properties(key: String) = providers.gradleProperty(key)
  8. buildscript {
  9. dependencies {
  10. classpath("com.google.code.gson:gson:2.10.1")
  11. }
  12. }
  13. plugins {
  14. id("java")
  15. id("org.jetbrains.kotlin.jvm") version "2.0.21"
  16. id("org.jetbrains.intellij.platform") version "2.10.0"
  17. id("org.jlleitschuh.gradle.ktlint") version "11.6.1"
  18. id("io.gitlab.arturbosch.detekt") version "1.23.7"
  19. }
  20. apply("genPlatform.gradle")
  21. // ------------------------------------------------------------
  22. // The 'debugMode' setting controls how plugin resources are prepared during the build process.
  23. // It supports the following three modes:
  24. //
  25. // 1. "idea" — Local development mode (used for debugging VSCode plugin integration)
  26. // - Copies theme resources from src/main/resources/themes to:
  27. // ../resources/<vscodePlugin>/src/integrations/theme/default-themes/
  28. // - Automatically creates a .env file, which the Extension Host (Node.js side) reads at runtime.
  29. // - Enables the VSCode plugin to load resources from this directory for integration testing.
  30. // - Typically used when running IntelliJ with an Extension Host for live debugging and hot-reloading.
  31. //
  32. // 2. "release" — Production build mode (used to generate deployment artifacts)
  33. // - Requires platform.zip to exist, which can be retrieved via git-lfs or generated with genPlatform.gradle.
  34. // - This file includes the full runtime environment for VSCode plugins (e.g., node_modules, platform.txt).
  35. // - The zip is extracted to build/platform/, and its node_modules take precedence over other dependencies.
  36. // - Copies compiled host outputs (dist, package.json, node_modules) and plugin resources.
  37. // - The result is a fully self-contained package ready for deployment across platforms.
  38. //
  39. // 3. "none" (default) — Lightweight mode (used for testing and CI)
  40. // - Does not rely on platform.zip or prepare VSCode runtime resources.
  41. // - Only copies the plugin's core assets such as themes.
  42. // - Useful for early-stage development, static analysis, unit tests, and continuous integration pipelines.
  43. //
  44. // How to configure:
  45. // - Set via gradle argument: -PdebugMode=idea / release / none
  46. // Example: ./gradlew prepareSandbox -PdebugMode=idea
  47. // - Defaults to "none" if not explicitly set.
  48. // ------------------------------------------------------------
  49. ext {
  50. set("debugMode", project.findProperty("debugMode") ?: "none")
  51. set("debugResource", project.projectDir.resolve("../resources").absolutePath)
  52. set("vscodePlugin", project.findProperty("vscodePlugin") ?: "kilocode")
  53. }
  54. project.afterEvaluate {
  55. tasks.findByName(":prepareSandbox")?.inputs?.properties?.put("build_mode", ext.get("debugMode"))
  56. }
  57. group = properties("pluginGroup").get()
  58. version = properties("pluginVersion").get()
  59. repositories {
  60. mavenCentral()
  61. // Fallback mirrors for when Maven Central returns 403 (common in CI environments)
  62. maven {
  63. url = uri("https://repo1.maven.org/maven2/")
  64. content {
  65. includeGroupByRegex("com\\.squareup.*")
  66. includeGroupByRegex("com\\.google.*")
  67. }
  68. }
  69. maven {
  70. url = uri("https://maven-central.storage.googleapis.com/maven2/")
  71. content {
  72. includeGroupByRegex("com\\.squareup.*")
  73. includeGroupByRegex("com\\.google.*")
  74. }
  75. }
  76. intellijPlatform {
  77. defaultRepositories()
  78. }
  79. }
  80. dependencies {
  81. implementation("com.squareup.okhttp3:okhttp:4.10.0")
  82. implementation("com.google.code.gson:gson:2.10.1")
  83. testImplementation("junit:junit:4.13.2")
  84. detektPlugins("io.gitlab.arturbosch.detekt:detekt-formatting:1.23.7")
  85. intellijPlatform {
  86. create(properties("platformType"), properties("platformVersion"))
  87. // Bundled plugins
  88. bundledPlugins(
  89. listOf(
  90. "com.intellij.java",
  91. "org.jetbrains.plugins.terminal",
  92. ),
  93. )
  94. // Plugin verifier
  95. pluginVerifier()
  96. // Instrumentation tools
  97. instrumentationTools()
  98. }
  99. }
  100. // Configure Java toolchain to force Java 21
  101. java {
  102. sourceCompatibility = JavaVersion.VERSION_21
  103. targetCompatibility = JavaVersion.VERSION_21
  104. toolchain {
  105. languageVersion.set(JavaLanguageVersion.of(21))
  106. }
  107. }
  108. // Configure IntelliJ Platform Gradle Plugin 2.x
  109. // Read more: https://plugins.jetbrains.com/docs/intellij/tools-intellij-platform-gradle-plugin.html
  110. intellijPlatform {
  111. pluginConfiguration {
  112. version = properties("pluginVersion")
  113. ideaVersion {
  114. sinceBuild = properties("pluginSinceBuild")
  115. untilBuild = provider { null }
  116. }
  117. }
  118. pluginVerification {
  119. ides {
  120. recommended()
  121. }
  122. }
  123. }
  124. tasks {
  125. // Configure test task to disable CDS (Class Data Sharing) to avoid warning:
  126. // "Archived non-system classes are disabled because the java.system.class.loader
  127. // property is specified (value = "com.intellij.util.lang.PathClassLoader")"
  128. //
  129. // IntelliJ Platform uses a custom PathClassLoader which conflicts with CDS's
  130. // archived non-system classes feature. Disabling CDS for tests eliminates the
  131. // warning while maintaining test functionality. Production builds are unaffected.
  132. withType<Test> {
  133. jvmArgs("-Xshare:off")
  134. }
  135. // Create task for generating configuration files
  136. register("generateConfigProperties") {
  137. description = "Generate properties file containing plugin configuration"
  138. doLast {
  139. val configDir = File("$projectDir/src/main/resources/ai/kilocode/jetbrains/plugin/config")
  140. configDir.mkdirs()
  141. val configFile = File(configDir, "plugin.properties")
  142. configFile.writeText("debug.mode=${ext.get("debugMode")}")
  143. configFile.appendText("\n")
  144. configFile.appendText("debug.resource=${ext.get("debugResource")}")
  145. println("Configuration file generated: ${configFile.absolutePath}")
  146. }
  147. }
  148. buildPlugin {
  149. dependsOn(prepareSandbox)
  150. // Include the jetbrains directory contents from sandbox in the distribution root
  151. doLast {
  152. if (ext.get("debugMode") != "idea" && ext.get("debugMode") != "none") {
  153. val distributionFile = archiveFile.get().asFile
  154. val sandboxPluginsDir = layout.buildDirectory.get().asFile.resolve("idea-sandbox/IC-2024.3/plugins")
  155. val jetbrainsDir = sandboxPluginsDir.resolve("jetbrains")
  156. if (jetbrainsDir.exists() && distributionFile.exists()) {
  157. logger.lifecycle("Adding sandbox resources to distribution ZIP...")
  158. logger.lifecycle("Sandbox jetbrains dir: ${jetbrainsDir.absolutePath}")
  159. logger.lifecycle("Distribution file: ${distributionFile.absolutePath}")
  160. // Extract the existing ZIP
  161. val tempDir = layout.buildDirectory.get().asFile.resolve("temp-dist")
  162. tempDir.deleteRecursively()
  163. tempDir.mkdirs()
  164. copy {
  165. from(zipTree(distributionFile))
  166. into(tempDir)
  167. }
  168. // Copy jetbrains directory CONTENTS directly to plugin root (not the jetbrains folder itself)
  169. val pluginDir = tempDir.resolve(rootProject.name)
  170. copy {
  171. from(jetbrainsDir) // Copy contents of jetbrains dir
  172. into(pluginDir) // Directly into plugin root
  173. }
  174. // Re-create the ZIP with resources included
  175. distributionFile.delete()
  176. ant.invokeMethod(
  177. "zip",
  178. mapOf(
  179. "destfile" to distributionFile.absolutePath,
  180. "basedir" to tempDir.absolutePath,
  181. ),
  182. )
  183. // Clean up temp directory
  184. tempDir.deleteRecursively()
  185. logger.lifecycle("Distribution ZIP updated with sandbox resources at root level")
  186. }
  187. }
  188. }
  189. }
  190. prepareSandbox {
  191. dependsOn("generateConfigProperties")
  192. duplicatesStrategy = DuplicatesStrategy.INCLUDE
  193. if (ext.get("debugMode") == "idea") {
  194. from("${project.projectDir.absolutePath}/src/main/resources/themes/") {
  195. into("${ext.get("debugResource")}/${ext.get("vscodePlugin")}/integrations/theme/default-themes/")
  196. }
  197. doLast {
  198. val vscodePluginDir = File("${ext.get("debugResource")}/${ext.get("vscodePlugin")}")
  199. vscodePluginDir.mkdirs()
  200. File(vscodePluginDir, ".env").createNewFile()
  201. }
  202. } else if (ext.get("debugMode") != "none") {
  203. doFirst {
  204. // Validate required files exist
  205. val vscodePluginDir = File("./plugins/${ext.get("vscodePlugin")}")
  206. if (!vscodePluginDir.exists()) {
  207. throw IllegalStateException("missing plugin dir: ${vscodePluginDir.absolutePath}")
  208. }
  209. val depfile = File("prodDep.txt")
  210. if (!depfile.exists()) {
  211. throw IllegalStateException("missing prodDep.txt")
  212. }
  213. // Handle platform.zip for release mode
  214. if (ext.get("debugMode") == "release") {
  215. val platformZip = File("platform.zip")
  216. if (!platformZip.exists() || platformZip.length() < 1024 * 1024) {
  217. throw IllegalStateException("platform.zip file does not exist or is smaller than 1MB. This file is supported through git lfs and needs to be obtained through git lfs")
  218. }
  219. // Extract platform.zip to the platform subdirectory under the project build directory
  220. val platformDir = File("${layout.buildDirectory.get().asFile}/platform")
  221. platformDir.mkdirs()
  222. copy {
  223. from(zipTree(platformZip))
  224. into(platformDir)
  225. }
  226. }
  227. }
  228. val vscodePluginDir = File("./plugins/${ext.get("vscodePlugin")}")
  229. val depfile = File("prodDep.txt")
  230. val list = mutableListOf<String>()
  231. // Read dependencies during execution
  232. doFirst {
  233. depfile.readLines().forEach { line ->
  234. list.add(line.substringAfterLast("node_modules/") + "/**")
  235. }
  236. }
  237. val pluginName = properties("pluginGroup").get().split(".").last()
  238. // Copy host runtime files
  239. from("../host/dist") { into("$pluginName/runtime/") }
  240. from("../host/package.json") { into("$pluginName/runtime/") }
  241. // Copy host node_modules based on prodDep.txt
  242. from("../resources/node_modules") {
  243. into("$pluginName/node_modules/")
  244. doFirst {
  245. list.forEach {
  246. include(it)
  247. }
  248. }
  249. }
  250. // Copy VSCode plugin extension
  251. from("${vscodePluginDir.path}/extension") { into("$pluginName/${ext.get("vscodePlugin")}") }
  252. // Copy themes
  253. from("src/main/resources/themes/") { into("$pluginName/${ext.get("vscodePlugin")}/integrations/theme/default-themes/") }
  254. // Copy platform files for release mode
  255. if (ext.get("debugMode") == "release") {
  256. val platformDir = File("${layout.buildDirectory.get().asFile}/platform")
  257. from(File(platformDir, "platform.txt")) { into("$pluginName/") }
  258. // Copy platform node_modules last to ensure it takes precedence over host node_modules
  259. from(File(platformDir, "node_modules")) { into("$pluginName/node_modules") }
  260. }
  261. doLast {
  262. File("$destinationDir/$pluginName/${ext.get("vscodePlugin")}/.env").apply {
  263. parentFile.mkdirs()
  264. createNewFile()
  265. }
  266. }
  267. }
  268. }
  269. // Generate configuration file before compilation
  270. withType<JavaCompile> {
  271. dependsOn("generateConfigProperties")
  272. }
  273. // Set the JVM compatibility versions
  274. withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile> {
  275. dependsOn("generateConfigProperties")
  276. compilerOptions {
  277. jvmTarget.set(org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_21)
  278. }
  279. }
  280. withType<JavaCompile> {
  281. sourceCompatibility = "21"
  282. targetCompatibility = "21"
  283. }
  284. signPlugin {
  285. certificateChain.set(System.getenv("CERTIFICATE_CHAIN"))
  286. privateKey.set(System.getenv("PRIVATE_KEY"))
  287. password.set(System.getenv("PRIVATE_KEY_PASSWORD"))
  288. }
  289. publishPlugin {
  290. token.set(System.getenv("PUBLISH_TOKEN"))
  291. }
  292. // Convert the extension's JSON translation files to JetBrains ResourceBundle .properties format
  293. register("convertTranslations") {
  294. description = "Convert JSON translation files to the native ResourceBundle .properties format"
  295. val sourceDir = file("../../src/i18n/locales")
  296. val targetDir = file("src/main/resources/messages")
  297. inputs.dir(sourceDir)
  298. outputs.dir(targetDir)
  299. doLast {
  300. if (!sourceDir.exists()) {
  301. throw IllegalStateException("Source translation directory not found: ${sourceDir.absolutePath}")
  302. }
  303. targetDir.mkdirs()
  304. // Find all JSON bundles (jetbrains.json, kilocode.json, etc.)
  305. val jsonBundles = mutableSetOf<String>()
  306. sourceDir.listFiles()?.forEach { localeDir ->
  307. if (localeDir.isDirectory) {
  308. localeDir.listFiles { file -> file.extension == "json" }?.forEach { jsonFile ->
  309. jsonBundles.add(jsonFile.nameWithoutExtension)
  310. }
  311. }
  312. }
  313. println("Found translation bundles: ${jsonBundles.joinToString(", ")}")
  314. jsonBundles.forEach { bundleName ->
  315. convertBundleToProperties(sourceDir, targetDir, bundleName)
  316. }
  317. println("Converted ${jsonBundles.size} translation bundles to ResourceBundle .properties format")
  318. }
  319. }
  320. named("processResources") {
  321. dependsOn("convertTranslations")
  322. }
  323. }
  324. // Helper function to convert JSON bundle to .properties files
  325. fun convertBundleToProperties(sourceDir: File, targetDir: File, bundleName: String) {
  326. val gson = com.google.gson.Gson()
  327. sourceDir.listFiles()?.forEach { localeDir ->
  328. if (localeDir.isDirectory) {
  329. val jsonFile = File(localeDir, "$bundleName.json")
  330. if (jsonFile.exists()) {
  331. try {
  332. val locale = localeDir.name
  333. val capitalizedBundleName = bundleName.replaceFirstChar { it.uppercase() }
  334. // Determine properties file name
  335. val propertiesFileName = if (locale == "en") {
  336. "${capitalizedBundleName}Bundle.properties"
  337. } else {
  338. "${capitalizedBundleName}Bundle_${locale.replace("-", "_")}.properties"
  339. }
  340. val propertiesFile = File(targetDir, propertiesFileName)
  341. // Parse JSON
  342. val jsonContent = jsonFile.readText()
  343. val jsonObject = gson.fromJson(jsonContent, com.google.gson.JsonObject::class.java)
  344. // Convert to flat properties
  345. val properties = mutableMapOf<String, String>()
  346. flattenJsonObject(jsonObject, "", properties)
  347. // Write properties file
  348. propertiesFile.writeText("# Auto-generated from $bundleName.json - do not edit directly\n")
  349. properties.toSortedMap().forEach { (key, value) ->
  350. // Keep named parameters as {{paramName}} for Kotlin named substitution
  351. val escapedValue = value
  352. .replace("\\", "\\\\")
  353. .replace("\n", "\\n")
  354. .replace("\r", "\\r")
  355. .replace("\t", "\\t")
  356. .replace("=", "\\=")
  357. .replace(":", "\\:")
  358. .replace("#", "\\#")
  359. .replace("!", "\\!")
  360. propertiesFile.appendText("$key=$escapedValue\n")
  361. }
  362. println(" → $locale: ${properties.size} keys → $propertiesFileName")
  363. } catch (e: Exception) {
  364. throw RuntimeException("Failed to convert $jsonFile", e)
  365. }
  366. }
  367. }
  368. }
  369. }
  370. // Helper function to flatten nested JSON objects into dot-notation keys
  371. fun flattenJsonObject(jsonObject: com.google.gson.JsonObject, prefix: String, properties: MutableMap<String, String>) {
  372. for (entry in jsonObject.entrySet()) {
  373. val key = entry.key
  374. val element = entry.value
  375. val fullKey = if (prefix.isEmpty()) key else "$prefix.$key"
  376. when {
  377. element.isJsonObject -> {
  378. flattenJsonObject(element.asJsonObject, fullKey, properties)
  379. }
  380. element.isJsonPrimitive -> {
  381. properties[fullKey] = element.asString
  382. }
  383. else -> {
  384. // Skip arrays and other complex types for now
  385. println(" Warning: Skipping complex type for key: $fullKey")
  386. }
  387. }
  388. }
  389. }
  390. // Configure ktlint
  391. ktlint {
  392. version.set("0.50.0")
  393. debug.set(false)
  394. verbose.set(true)
  395. android.set(false)
  396. outputToConsole.set(true)
  397. outputColorName.set("RED")
  398. ignoreFailures.set(true)
  399. enableExperimentalRules.set(false)
  400. filter {
  401. exclude("**/generated/**")
  402. include("**/kotlin/**")
  403. }
  404. }
  405. // Configure detekt
  406. detekt {
  407. toolVersion = "1.23.7"
  408. config.setFrom(file("detekt.yml"))
  409. buildUponDefaultConfig = true
  410. allRules = false
  411. }
  412. tasks.withType<io.gitlab.arturbosch.detekt.Detekt>().configureEach {
  413. reports {
  414. html.required.set(true)
  415. xml.required.set(true)
  416. txt.required.set(true)
  417. sarif.required.set(true)
  418. md.required.set(true)
  419. }
  420. }