diff --git a/.idea/modules.xml b/.idea/modules.xml
index 29aefff421f5..ff356d644e91 100644
--- a/.idea/modules.xml
+++ b/.idea/modules.xml
@@ -1129,6 +1129,7 @@
+
diff --git a/build/bazel-generated-file-list.txt b/build/bazel-generated-file-list.txt
index 4d6b9d1a8c2e..10afd97f3deb 100644
--- a/build/bazel-generated-file-list.txt
+++ b/build/bazel-generated-file-list.txt
@@ -582,6 +582,7 @@ platform/jewel/foundation
platform/jewel/ide-laf-bridge
platform/jewel/int-ui/int-ui-decorated-window
platform/jewel/int-ui/int-ui-standalone
+platform/jewel/int-ui/int-ui-standalone-tests
platform/jewel/markdown/core
platform/jewel/markdown/extensions/autolink
platform/jewel/markdown/extensions/gfm-alerts
diff --git a/platform/jewel/.idea/ktfmt.xml b/platform/jewel/.idea/ktfmt.xml
index 615c312e88df..0d8bf4dc7901 100644
--- a/platform/jewel/.idea/ktfmt.xml
+++ b/platform/jewel/.idea/ktfmt.xml
@@ -2,8 +2,8 @@
-
+
diff --git a/platform/jewel/buildSrc/src/main/kotlin/org/jetbrains/jewel/buildlogic/metalava/ApiChangedException.kt b/platform/jewel/buildSrc/src/main/kotlin/org/jetbrains/jewel/buildlogic/metalava/ApiChangedException.kt
index c5963e1df636..577ffaf6ff1b 100644
--- a/platform/jewel/buildSrc/src/main/kotlin/org/jetbrains/jewel/buildlogic/metalava/ApiChangedException.kt
+++ b/platform/jewel/buildSrc/src/main/kotlin/org/jetbrains/jewel/buildlogic/metalava/ApiChangedException.kt
@@ -5,4 +5,4 @@ package org.jetbrains.jewel.buildlogic.metalava
import org.gradle.api.GradleException
-class ApiChangedException(msg: String, cause: Throwable) : GradleException(msg, cause)
+class ApiChangedException(msg: String, cause: Throwable? = null) : GradleException(msg, cause)
diff --git a/platform/jewel/buildSrc/src/main/kotlin/org/jetbrains/jewel/buildlogic/metalava/CheckMetalavaApiTask.kt b/platform/jewel/buildSrc/src/main/kotlin/org/jetbrains/jewel/buildlogic/metalava/CheckMetalavaApiTask.kt
index e500d9cd4507..8e1435c67ccf 100644
--- a/platform/jewel/buildSrc/src/main/kotlin/org/jetbrains/jewel/buildlogic/metalava/CheckMetalavaApiTask.kt
+++ b/platform/jewel/buildSrc/src/main/kotlin/org/jetbrains/jewel/buildlogic/metalava/CheckMetalavaApiTask.kt
@@ -12,11 +12,12 @@ import org.gradle.api.tasks.CacheableTask
import org.gradle.api.tasks.Classpath
import org.gradle.api.tasks.Input
import org.gradle.api.tasks.InputFile
+import org.gradle.api.tasks.InputFiles
+import org.gradle.api.tasks.Optional
import org.gradle.api.tasks.PathSensitive
import org.gradle.api.tasks.PathSensitivity
import org.gradle.api.tasks.TaskAction
import org.gradle.process.ExecOperations
-import org.gradle.process.internal.ExecException
import org.gradle.workers.WorkAction
import org.gradle.workers.WorkParameters
import org.gradle.workers.WorkerExecutor
@@ -37,8 +38,15 @@ abstract class CheckMetalavaApiTask @Inject constructor(private val workerExecut
@get:Input abstract val stableApiOnly: Property
+ @get:Input abstract val updateBaseline: Property
+
@get:PathSensitive(PathSensitivity.RELATIVE) @get:InputFile abstract val referenceApiFile: RegularFileProperty
+ @get:Optional
+ @get:PathSensitive(PathSensitivity.RELATIVE)
+ @get:InputFiles
+ abstract val baselineFile: ConfigurableFileCollection
+
@get:PathSensitive(PathSensitivity.RELATIVE) @get:InputFile abstract val currentApiFile: RegularFileProperty
@TaskAction
@@ -54,6 +62,8 @@ abstract class CheckMetalavaApiTask @Inject constructor(private val workerExecut
referenceApiFile.set(this@CheckMetalavaApiTask.referenceApiFile)
currentApiFile.set(this@CheckMetalavaApiTask.currentApiFile)
stableApiOnly.set(this@CheckMetalavaApiTask.stableApiOnly)
+ updateBaseline.set(this@CheckMetalavaApiTask.updateBaseline)
+ baselineFile.from(this@CheckMetalavaApiTask.baselineFile)
}
}
@@ -64,6 +74,8 @@ abstract class CheckMetalavaApiTask @Inject constructor(private val workerExecut
val referenceApiFile: RegularFileProperty
val currentApiFile: RegularFileProperty
val stableApiOnly: Property
+ val updateBaseline: Property
+ val baselineFile: ConfigurableFileCollection
}
abstract class Action : WorkAction {
@@ -78,46 +90,55 @@ abstract class CheckMetalavaApiTask @Inject constructor(private val workerExecut
systemProperty("apple.awt.UIElement", "true")
mainClass.set("com.android.tools.metalava.Driver")
classpath = parameters.metalavaClasspath
- args =
- listOf(
- "--format=v4",
- "--jdk-home",
- jdkHome,
- "--source-files",
- parameters.currentApiFile.get().asFile.absolutePath,
- "--check-compatibility:api:released",
- parameters.referenceApiFile.get().asFile.absolutePath,
- "--warnings-as-errors",
- ) +
- if (parameters.stableApiOnly.get()) {
- STABLE_ONLY_API_ARGS
- } else {
- EXPERIMENTAL_API_ARGS
- }
+ args = buildList {
+ add("--format=v4")
+ add("--jdk-home")
+ add(jdkHome)
+ add("--source-files")
+ add(parameters.currentApiFile.get().asFile.absolutePath)
+ add("--check-compatibility:api:released")
+ add(parameters.referenceApiFile.get().asFile.absolutePath)
+ add("--warnings-as-errors")
+ if (parameters.stableApiOnly.get()) {
+ addAll(STABLE_ONLY_API_ARGS)
+ } else {
+ addAll(EXPERIMENTAL_API_ARGS)
+ }
+
+ val baselineFile = parameters.baselineFile.singleFile
+ baselineFile.createNewFile() // Ensure baseline file exists
+
+ add("--baseline")
+ add(baselineFile.absolutePath)
+
+ if (parameters.updateBaseline.get()) {
+ add("--pass-baseline-updates")
+ add("--update-baseline")
+ add(baselineFile.absolutePath)
+ }
+ }
isIgnoreExitValue = true
}
- try {
- result.assertNormalExitValue()
- } catch (e: ExecException) {
+ if (result.exitValue != 0) {
val msg =
"""
- API changed! Run `./gradlew ${updateApi()}` and then commit the changes. For backwards-incompatible
- changes, be sure to update the major version. DO NOT USE JDK 24.
+ [Metalava] The API _might_ have changed in an incompatible way!
+ The Metalava process exited with a non-zero value — check the stderr output for details.
+
+ If this is intended, and this is not happening during the release process,
+ update the baseline file and then commit the changes.
+
+ IMPORTANT: DO NOT USE JDK 24 WHEN RUNNING METALAVA.
"""
.trimIndent()
.wrapInStars(margin = 1)
-
- throw ApiChangedException(msg, e)
+
+ throw ApiChangedException(msg)
}
}
- private fun updateApi(): String {
- val path = parameters.projectPath.get()
- return if (path == ":") ":updateMetalavaApi" else "$path:updateMetalavaApi"
- }
-
private fun String.wrapInStars(margin: Int = 0): String {
require(margin >= 0) { "Expected margin >= 0, was $margin" }
diff --git a/platform/jewel/buildSrc/src/main/kotlin/org/jetbrains/jewel/buildlogic/metalava/MetalavaConfigurer.kt b/platform/jewel/buildSrc/src/main/kotlin/org/jetbrains/jewel/buildlogic/metalava/MetalavaConfigurer.kt
index 2200e05d7be8..6ee07a06cb55 100644
--- a/platform/jewel/buildSrc/src/main/kotlin/org/jetbrains/jewel/buildlogic/metalava/MetalavaConfigurer.kt
+++ b/platform/jewel/buildSrc/src/main/kotlin/org/jetbrains/jewel/buildlogic/metalava/MetalavaConfigurer.kt
@@ -43,12 +43,20 @@ internal class MetalavaConfigurer(private val project: Project, private val vers
val jdkHomePath = Jvm.current().javaHome.absolutePath
// If no version is specified, default to the current one
- val targetVersion =
- (properties["metalavaTargetRelease"] as? String)?.trim()?.takeIf { it.isNotBlank() }
- ?: getJewelVersion()
+ val requestedVersion = (properties["metalavaTargetRelease"] as? String)?.trim()?.takeIf { it.isNotBlank() }
+ if (requestedVersion != null) {
+ logger.info("Metalava API check target version set to $requestedVersion")
+ } else {
+ logger.info(
+ "Metalava API check target version not specified, using current Jewel version: ${getJewelVersion()}"
+ )
+ }
+ val targetVersion = requestedVersion ?: getJewelVersion()
- setupTasks(targetVersion, classes, jdkHomePath, sourceDirectories, stableOnly = true)
- setupTasks(targetVersion, classes, jdkHomePath, sourceDirectories, stableOnly = false)
+ val updateBaseline = (properties["update-baseline"]?.toString()?.toBooleanStrictOrNull()) == true
+
+ setupTasks(targetVersion, classes, jdkHomePath, sourceDirectories, stableOnly = true, updateBaseline)
+ setupTasks(targetVersion, classes, jdkHomePath, sourceDirectories, stableOnly = false, updateBaseline)
tasks.register("checkMetalavaApi") {
dependsOn(tasks.named("checkMetalavaStableApi"), tasks.named("checkMetalavaExperimentalApi"))
@@ -78,6 +86,7 @@ internal class MetalavaConfigurer(private val project: Project, private val vers
jdkHomePath: String,
sourceDirectories: Provider,
stableOnly: Boolean,
+ updateBaselineForCheckTask: Boolean,
) {
val taskDescriptor = if (stableOnly) "Stable" else "Experimental"
validateJewelVersion(targetVersion)
@@ -115,6 +124,9 @@ internal class MetalavaConfigurer(private val project: Project, private val vers
currentApiFile.set(generateApiTask.flatMap { it.output })
referenceApiFile.set(targetApiFile)
+ updateBaseline.set(updateBaselineForCheckTask)
+ baselineFile.from(layout.projectDirectory.files("metalava/${baselineFileName(stableOnly)}"))
+
dependsOn(generateApiTask)
}
}
@@ -127,6 +139,13 @@ internal class MetalavaConfigurer(private val project: Project, private val vers
append(".txt")
}
+ private fun Project.baselineFileName(stableApiOnly: Boolean) = buildString {
+ append(project.name)
+ append("-baseline-")
+ if (stableApiOnly) append("stable-")
+ append("current.txt")
+ }
+
companion object {
const val TASK_GROUP = "metalava"
}
diff --git a/platform/jewel/decorated-window/metalava/decorated-window-baseline-current.txt b/platform/jewel/decorated-window/metalava/decorated-window-baseline-current.txt
new file mode 100644
index 000000000000..9a97707763f1
--- /dev/null
+++ b/platform/jewel/decorated-window/metalava/decorated-window-baseline-current.txt
@@ -0,0 +1 @@
+// Baseline format: 1.0
diff --git a/platform/jewel/decorated-window/metalava/decorated-window-baseline-stable-current.txt b/platform/jewel/decorated-window/metalava/decorated-window-baseline-stable-current.txt
new file mode 100644
index 000000000000..9a97707763f1
--- /dev/null
+++ b/platform/jewel/decorated-window/metalava/decorated-window-baseline-stable-current.txt
@@ -0,0 +1 @@
+// Baseline format: 1.0
diff --git a/platform/jewel/docs/pr-guide.md b/platform/jewel/docs/pr-guide.md
index 21e8719f45ef..e9a330c3d1d5 100644
--- a/platform/jewel/docs/pr-guide.md
+++ b/platform/jewel/docs/pr-guide.md
@@ -17,6 +17,8 @@ these steps:
6. Run the `ApiCheckTest` to make sure the API dumps are up to date
7. Ensure there are no breaking changes in stable APIs (they look like removed lines in `api-dump.txt` files), and try
to avoid or at least minimise changes in experimental APIs too (see `api-dump-experimental.txt` files)
+8. Check that there are no new issues reported by Metalava either, with `scripts/metalava-signatures.main.kts validate`;
+ if any is found, update the baseline files accordingly with the `--update-baseline` parameter.
If your change includes changes to the module structure and/or to dependencies, refer to the following sections as well
for more guidance.
diff --git a/platform/jewel/docs/releasing-guide.md b/platform/jewel/docs/releasing-guide.md
index e88f5b0fca0b..9bbb7ea08265 100644
--- a/platform/jewel/docs/releasing-guide.md
+++ b/platform/jewel/docs/releasing-guide.md
@@ -19,15 +19,19 @@ High-level steps:
```shell
./gradlew check detekt detektMain detektTest --continue
```
-4. Run the Metalava validator against the previous release on the `master` branch, and fix any issues you find:
+4. In all Metalava baseline files (`metalava/{moduleName}-baseline[-stable]-current.txt`), remove all findings, only
+ leaving the baseline version header on the first line. This lets you see all issues accumulated over this release in
+ the following steps
+5. Run the Metalava validator against the previous release on the `master` branch, and fix any issues you find:
```shell
./scripts/metalava-signatures.main.kts validate --release
```
-5. Generate the new Metalava signatures for the new release:
+6. Generate the new Metalava signatures for the new release:
```shell
./scripts/metalava-signatures.main.kts update --release
```
-6. Cherry-pick the changes to the target release branches (e.g., `252`)
+7. Commit all changes and get them merged to `master`
+8. Cherry-pick the changes to the target release branches (e.g., `252`)
1. Make sure you've not included IJP major release-specific changes
2. Update the Kotlin version in the [Gradle version catalog](../gradle/libs.versions.toml) to match the IJ Platform's Kotlin version
3. Update other related versions if needed
@@ -42,7 +46,7 @@ High-level steps:
./scripts/metalava-signatures.sh --validate --release
```
11. Open a merge request for each cherry-pick branch on Space
-7. When both MRs are approved and merged:
+9. When both MRs are approved and merged:
1. Run the TeamCity job to publish the artefacts to Maven Central
2. Tag the commits the releases were cut from, with this format: `JEWEL-[Jewel version]-[major IJP version]`. For
example, for Jewel 0.30.0, `JEWEL-0.30.0-251` on the 251 branch and `JEWEL-0.30.0-252` on the 252 branch.
diff --git a/platform/jewel/foundation/metalava/foundation-baseline-current.txt b/platform/jewel/foundation/metalava/foundation-baseline-current.txt
new file mode 100644
index 000000000000..9a97707763f1
--- /dev/null
+++ b/platform/jewel/foundation/metalava/foundation-baseline-current.txt
@@ -0,0 +1 @@
+// Baseline format: 1.0
diff --git a/platform/jewel/foundation/metalava/foundation-baseline-stable-current.txt b/platform/jewel/foundation/metalava/foundation-baseline-stable-current.txt
new file mode 100644
index 000000000000..9a97707763f1
--- /dev/null
+++ b/platform/jewel/foundation/metalava/foundation-baseline-stable-current.txt
@@ -0,0 +1 @@
+// Baseline format: 1.0
diff --git a/platform/jewel/ide-laf-bridge/metalava/ide-laf-bridge-baseline-current.txt b/platform/jewel/ide-laf-bridge/metalava/ide-laf-bridge-baseline-current.txt
new file mode 100644
index 000000000000..9a97707763f1
--- /dev/null
+++ b/platform/jewel/ide-laf-bridge/metalava/ide-laf-bridge-baseline-current.txt
@@ -0,0 +1 @@
+// Baseline format: 1.0
diff --git a/platform/jewel/ide-laf-bridge/metalava/ide-laf-bridge-baseline-stable-current.txt b/platform/jewel/ide-laf-bridge/metalava/ide-laf-bridge-baseline-stable-current.txt
new file mode 100644
index 000000000000..9a97707763f1
--- /dev/null
+++ b/platform/jewel/ide-laf-bridge/metalava/ide-laf-bridge-baseline-stable-current.txt
@@ -0,0 +1 @@
+// Baseline format: 1.0
diff --git a/platform/jewel/int-ui/int-ui-decorated-window/metalava/int-ui-decorated-window-baseline-current.txt b/platform/jewel/int-ui/int-ui-decorated-window/metalava/int-ui-decorated-window-baseline-current.txt
new file mode 100644
index 000000000000..9a97707763f1
--- /dev/null
+++ b/platform/jewel/int-ui/int-ui-decorated-window/metalava/int-ui-decorated-window-baseline-current.txt
@@ -0,0 +1 @@
+// Baseline format: 1.0
diff --git a/platform/jewel/int-ui/int-ui-decorated-window/metalava/int-ui-decorated-window-baseline-stable-current.txt b/platform/jewel/int-ui/int-ui-decorated-window/metalava/int-ui-decorated-window-baseline-stable-current.txt
new file mode 100644
index 000000000000..9a97707763f1
--- /dev/null
+++ b/platform/jewel/int-ui/int-ui-decorated-window/metalava/int-ui-decorated-window-baseline-stable-current.txt
@@ -0,0 +1 @@
+// Baseline format: 1.0
diff --git a/platform/jewel/int-ui/int-ui-standalone-tests/BUILD.bazel b/platform/jewel/int-ui/int-ui-standalone-tests/BUILD.bazel
new file mode 100644
index 000000000000..f4580ee041b0
--- /dev/null
+++ b/platform/jewel/int-ui/int-ui-standalone-tests/BUILD.bazel
@@ -0,0 +1,39 @@
+### auto-generated section `build intellij.platform.jewel.intUi.standalone.tests` start
+load("//build:compiler-options.bzl", "create_kotlinc_options")
+load("@rules_jvm//:jvm.bzl", "jvm_library")
+
+create_kotlinc_options(
+ name = "custom_jewel-intUi-standalone-tests",
+ opt_in = [
+ "androidx.compose.ui.ExperimentalComposeUiApi",
+ "androidx.compose.foundation.ExperimentalFoundationApi",
+ "org.jetbrains.jewel.foundation.ExperimentalJewelApi",
+ "org.jetbrains.jewel.foundation.InternalJewelApi",
+ ],
+ x_context_parameters = True,
+ x_explicit_api_mode = "strict"
+)
+
+jvm_library(
+ name = "jewel-intUi-standalone-tests_test_lib",
+ module_name = "intellij.platform.jewel.intUi.standalone.tests",
+ visibility = ["//visibility:public"],
+ srcs = glob(["src/test/kotlin/**/*.kt", "src/test/kotlin/**/*.java", "src/test/kotlin/**/*.form"], allow_empty = True),
+ kotlinc_opts = ":custom_jewel-intUi-standalone-tests",
+ deps = [
+ "@lib//:kotlin-stdlib",
+ "@lib//:kotlin-test",
+ "@lib//:junit5",
+ "//platform/jewel/int-ui/int-ui-standalone:jewel-intUi-standalone",
+ ]
+)
+### auto-generated section `build intellij.platform.jewel.intUi.standalone.tests` end
+
+### auto-generated section `test intellij.platform.jewel.intUi.standalone.tests` start
+load("@community//build:tests-options.bzl", "jps_test")
+
+jps_test(
+ name = "jewel-intUi-standalone-tests_test",
+ runtime_deps = [":jewel-intUi-standalone-tests_test_lib"]
+)
+### auto-generated section `test intellij.platform.jewel.intUi.standalone.tests` end
\ No newline at end of file
diff --git a/platform/jewel/int-ui/int-ui-standalone-tests/build.gradle.kts b/platform/jewel/int-ui/int-ui-standalone-tests/build.gradle.kts
new file mode 100644
index 000000000000..20c1ca484b92
--- /dev/null
+++ b/platform/jewel/int-ui/int-ui-standalone-tests/build.gradle.kts
@@ -0,0 +1,10 @@
+plugins { jewel }
+
+dependencies {
+ testImplementation(kotlin("test"))
+ testImplementation(libs.junit.jupiter)
+ testRuntimeOnly(libs.junit.platform.engine)
+ testRuntimeOnly(libs.junit.platform.launcher)
+}
+
+tasks.test { useJUnitPlatform() }
diff --git a/platform/jewel/int-ui/int-ui-standalone-tests/intellij.platform.jewel.intUi.standalone.tests.iml b/platform/jewel/int-ui/int-ui-standalone-tests/intellij.platform.jewel.intUi.standalone.tests.iml
new file mode 100644
index 000000000000..116a4639eb42
--- /dev/null
+++ b/platform/jewel/int-ui/int-ui-standalone-tests/intellij.platform.jewel.intUi.standalone.tests.iml
@@ -0,0 +1,31 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/platform/jewel/int-ui/int-ui-standalone-tests/src/test/kotlin/org/jetbrains/jewel/intui/core/theme/GeneratedThemeUpToDateTest.kt b/platform/jewel/int-ui/int-ui-standalone-tests/src/test/kotlin/org/jetbrains/jewel/intui/core/theme/GeneratedThemeUpToDateTest.kt
new file mode 100644
index 000000000000..edcdc464eb9b
--- /dev/null
+++ b/platform/jewel/int-ui/int-ui-standalone-tests/src/test/kotlin/org/jetbrains/jewel/intui/core/theme/GeneratedThemeUpToDateTest.kt
@@ -0,0 +1,122 @@
+// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
+package org.jetbrains.jewel.intui.core.theme
+
+import java.io.File
+import kotlin.test.assertEquals
+import kotlin.test.assertTrue
+import org.junit.jupiter.api.Test
+
+internal class GeneratedThemeUpToDateTest {
+ @Test
+ fun `should have the right major version in the light file header`() {
+ val currentDir = File("").canonicalFile.takeIf { it.isDirectory } ?: error("Cannot find current directory")
+ val majorBuildNumber = getMajorIjpVersion(currentDir)
+
+ val themesDir = findThemesDir(currentDir)
+
+ val themeFile = themesDir.resolve(LIGHT_THEME_FILENAME)
+ assertTrue(themeFile.isFile, "The light theme file does not exist")
+
+ val themeFirstLine = themeFile.useLines { lines -> lines.first() }
+ assertTrue(
+ themeFirstLine.endsWith(" for IJP $majorBuildNumber"),
+ "The light theme file does not have the right IJP version in its header; " +
+ "run the Jewel theme generator Gradle task to fix the issue.",
+ )
+ }
+
+ @Test
+ fun `should have the right major version in the dark file header`() {
+ val currentDir = File("").canonicalFile.takeIf { it.isDirectory } ?: error("Cannot find current directory")
+ val majorBuildNumber = getMajorIjpVersion(currentDir)
+
+ val themesDir = findThemesDir(currentDir)
+
+ val themeFile = themesDir.resolve(DARK_THEME_FILENAME)
+ assertTrue(themeFile.isFile, "The dark theme file does not exist")
+
+ val themeFirstLine = themeFile.useLines { lines -> lines.first() }
+ assertTrue(
+ themeFirstLine.endsWith(" for IJP $majorBuildNumber"),
+ "The dark theme file does not have the right IJP version in its header; " +
+ "run the Jewel theme generator Gradle task to fix the issue.",
+ )
+ }
+
+ private fun getMajorIjpVersion(currentDir: File): String {
+ val communityRoot = findCommunityRoot(currentDir) ?: error("Could not find the IJ community root directory")
+ val buildNumber = communityRoot.resolve("build.txt").readText().trim()
+ check(validateBuildNumber(buildNumber)) { "The build number in build.txt does not seem valid: '$buildNumber'" }
+
+ return buildNumber.substringBefore(".")
+ }
+
+ private fun findCommunityRoot(base: File): File? {
+ val firstAttempt =
+ findDir(base) {
+ it.resolve(COMMUNITY_ROOT_MARKER_FILE_NAME).isFile && it.resolve(COMMUNITY_IML_FILE_NAME).isFile
+ }
+ if (firstAttempt != null) return firstAttempt
+
+ // We did not find it traversing up. Maybe we're in the monorepo, and it's actually a child?
+ if (
+ base.resolve(COMMUNITY_ROOT_MARKER_IN_MONOREPO_FILE_NAME).isFile &&
+ base.resolve(COMMUNITY_IML_IN_MONOREPO_FILE_NAME).isFile
+ ) {
+ return base.resolve(COMMUNITY_DIR_IN_MONOREPO_NAME).takeIf { it.isDirectory }
+ }
+
+ // Could not find the community directory
+ return null
+ }
+
+ private fun findDir(base: File, isDesiredDir: (File) -> Boolean): File? {
+ var current = base
+ while (true) {
+ if (!current.canRead()) {
+ System.err.println("Directory is not readable, stopping search: ${current.absolutePath}")
+ return null
+ }
+
+ if (isDesiredDir(current)) {
+ return current.canonicalFile
+ }
+
+ current = current.parentFile ?: return null
+ }
+ }
+
+ private fun findThemesDir(currentDir: File): File {
+ val resolved = currentDir.resolve(GENERATED_THEMES_DIR_PATH).absoluteFile
+ val themesDir =
+ resolved.takeIf { it.isDirectory } ?: error("Cannot find themes directory at ${resolved.canonicalPath}")
+ assertEquals(2, themesDir.listFiles()?.size, "Number of generated theme files")
+ return themesDir
+ }
+
+ private fun validateBuildNumber(buildNumber: String): Boolean {
+ // Examples:
+ // * 253.1234.567
+ // * 241.SNAPSHOT
+ if (buildNumber.isBlank()) return false
+ if (buildNumber.length < 5) return false
+ if (buildNumber.take(3).toIntOrNull()?.takeIf { it > 240 } == null) return false
+ if (buildNumber[3] != '.') return false
+
+ val afterDot = buildNumber.drop(4)
+ return afterDot == "SNAPSHOT" || afterDot.all { it.isDigit() || it == '.' }
+ }
+}
+
+private const val GENERATED_THEMES_DIR_PATH =
+ "../int-ui-standalone/generated/theme/org/jetbrains/jewel/intui/core/theme/"
+private const val DARK_THEME_FILENAME = "IntUiDarkTheme.kt"
+private const val LIGHT_THEME_FILENAME = "IntUiLightTheme.kt"
+
+// Values taken from JpsModuleToBazel's logic
+private const val COMMUNITY_ROOT_MARKER_FILE_NAME = ".community.root.marker"
+private const val COMMUNITY_IML_FILE_NAME = "intellij.idea.community.main.iml"
+private const val COMMUNITY_DIR_IN_MONOREPO_NAME = "community"
+private const val COMMUNITY_ROOT_MARKER_IN_MONOREPO_FILE_NAME =
+ "$COMMUNITY_DIR_IN_MONOREPO_NAME/$COMMUNITY_ROOT_MARKER_FILE_NAME"
+private const val COMMUNITY_IML_IN_MONOREPO_FILE_NAME = "$COMMUNITY_DIR_IN_MONOREPO_NAME/$COMMUNITY_IML_FILE_NAME"
diff --git a/platform/jewel/int-ui/int-ui-standalone/build.gradle.kts b/platform/jewel/int-ui/int-ui-standalone/build.gradle.kts
index 86744dbe707b..e5ee40249560 100644
--- a/platform/jewel/int-ui/int-ui-standalone/build.gradle.kts
+++ b/platform/jewel/int-ui/int-ui-standalone/build.gradle.kts
@@ -53,14 +53,17 @@ tasks {
mustRunAfter(generateThemes)
mustRunAfter(ktfmtFormatMain)
}
+
lintKotlinMain {
mustRunAfter(generateThemes)
mustRunAfter(ktfmtFormatMain)
}
+
withType().configureEach {
mustRunAfter(generateThemes)
mustRunAfter(ktfmtFormatMain)
}
+
detektMain {
mustRunAfter(generateThemes)
mustRunAfter(ktfmtFormatMain)
diff --git a/platform/jewel/int-ui/int-ui-standalone/intellij.platform.jewel.intUi.standalone.iml b/platform/jewel/int-ui/int-ui-standalone/intellij.platform.jewel.intUi.standalone.iml
index 5542762ee978..615366adfa51 100644
--- a/platform/jewel/int-ui/int-ui-standalone/intellij.platform.jewel.intUi.standalone.iml
+++ b/platform/jewel/int-ui/int-ui-standalone/intellij.platform.jewel.intUi.standalone.iml
@@ -14,9 +14,7 @@
-
- $MAVEN_REPOSITORY$/org/jetbrains/kotlin/kotlin-compose-compiler-plugin/2.2.20/kotlin-compose-compiler-plugin-2.2.20.jar
-
+ $MAVEN_REPOSITORY$/org/jetbrains/kotlin/kotlin-compose-compiler-plugin/2.2.20/kotlin-compose-compiler-plugin-2.2.20.jar
diff --git a/platform/jewel/int-ui/int-ui-standalone/metalava/int-ui-standalone-baseline-current.txt b/platform/jewel/int-ui/int-ui-standalone/metalava/int-ui-standalone-baseline-current.txt
new file mode 100644
index 000000000000..9a97707763f1
--- /dev/null
+++ b/platform/jewel/int-ui/int-ui-standalone/metalava/int-ui-standalone-baseline-current.txt
@@ -0,0 +1 @@
+// Baseline format: 1.0
diff --git a/platform/jewel/int-ui/int-ui-standalone/metalava/int-ui-standalone-baseline-stable-current.txt b/platform/jewel/int-ui/int-ui-standalone/metalava/int-ui-standalone-baseline-stable-current.txt
new file mode 100644
index 000000000000..9a97707763f1
--- /dev/null
+++ b/platform/jewel/int-ui/int-ui-standalone/metalava/int-ui-standalone-baseline-stable-current.txt
@@ -0,0 +1 @@
+// Baseline format: 1.0
diff --git a/platform/jewel/markdown/core/metalava/core-baseline-current.txt b/platform/jewel/markdown/core/metalava/core-baseline-current.txt
new file mode 100644
index 000000000000..9a97707763f1
--- /dev/null
+++ b/platform/jewel/markdown/core/metalava/core-baseline-current.txt
@@ -0,0 +1 @@
+// Baseline format: 1.0
diff --git a/platform/jewel/markdown/core/metalava/core-baseline-stable-current.txt b/platform/jewel/markdown/core/metalava/core-baseline-stable-current.txt
new file mode 100644
index 000000000000..9a97707763f1
--- /dev/null
+++ b/platform/jewel/markdown/core/metalava/core-baseline-stable-current.txt
@@ -0,0 +1 @@
+// Baseline format: 1.0
diff --git a/platform/jewel/markdown/extensions/autolink/metalava/autolink-baseline-current.txt b/platform/jewel/markdown/extensions/autolink/metalava/autolink-baseline-current.txt
new file mode 100644
index 000000000000..9a97707763f1
--- /dev/null
+++ b/platform/jewel/markdown/extensions/autolink/metalava/autolink-baseline-current.txt
@@ -0,0 +1 @@
+// Baseline format: 1.0
diff --git a/platform/jewel/markdown/extensions/autolink/metalava/autolink-baseline-stable-current.txt b/platform/jewel/markdown/extensions/autolink/metalava/autolink-baseline-stable-current.txt
new file mode 100644
index 000000000000..9a97707763f1
--- /dev/null
+++ b/platform/jewel/markdown/extensions/autolink/metalava/autolink-baseline-stable-current.txt
@@ -0,0 +1 @@
+// Baseline format: 1.0
diff --git a/platform/jewel/markdown/extensions/gfm-alerts/metalava/gfm-alerts-baseline-current.txt b/platform/jewel/markdown/extensions/gfm-alerts/metalava/gfm-alerts-baseline-current.txt
new file mode 100644
index 000000000000..9a97707763f1
--- /dev/null
+++ b/platform/jewel/markdown/extensions/gfm-alerts/metalava/gfm-alerts-baseline-current.txt
@@ -0,0 +1 @@
+// Baseline format: 1.0
diff --git a/platform/jewel/markdown/extensions/gfm-alerts/metalava/gfm-alerts-baseline-stable-current.txt b/platform/jewel/markdown/extensions/gfm-alerts/metalava/gfm-alerts-baseline-stable-current.txt
new file mode 100644
index 000000000000..9a97707763f1
--- /dev/null
+++ b/platform/jewel/markdown/extensions/gfm-alerts/metalava/gfm-alerts-baseline-stable-current.txt
@@ -0,0 +1 @@
+// Baseline format: 1.0
diff --git a/platform/jewel/markdown/extensions/gfm-strikethrough/metalava/gfm-strikethrough-baseline-current.txt b/platform/jewel/markdown/extensions/gfm-strikethrough/metalava/gfm-strikethrough-baseline-current.txt
new file mode 100644
index 000000000000..9a97707763f1
--- /dev/null
+++ b/platform/jewel/markdown/extensions/gfm-strikethrough/metalava/gfm-strikethrough-baseline-current.txt
@@ -0,0 +1 @@
+// Baseline format: 1.0
diff --git a/platform/jewel/markdown/extensions/gfm-strikethrough/metalava/gfm-strikethrough-baseline-stable-current.txt b/platform/jewel/markdown/extensions/gfm-strikethrough/metalava/gfm-strikethrough-baseline-stable-current.txt
new file mode 100644
index 000000000000..9a97707763f1
--- /dev/null
+++ b/platform/jewel/markdown/extensions/gfm-strikethrough/metalava/gfm-strikethrough-baseline-stable-current.txt
@@ -0,0 +1 @@
+// Baseline format: 1.0
diff --git a/platform/jewel/markdown/extensions/gfm-tables/metalava/gfm-tables-baseline-current.txt b/platform/jewel/markdown/extensions/gfm-tables/metalava/gfm-tables-baseline-current.txt
new file mode 100644
index 000000000000..9a97707763f1
--- /dev/null
+++ b/platform/jewel/markdown/extensions/gfm-tables/metalava/gfm-tables-baseline-current.txt
@@ -0,0 +1 @@
+// Baseline format: 1.0
diff --git a/platform/jewel/markdown/extensions/gfm-tables/metalava/gfm-tables-baseline-stable-current.txt b/platform/jewel/markdown/extensions/gfm-tables/metalava/gfm-tables-baseline-stable-current.txt
new file mode 100644
index 000000000000..9a97707763f1
--- /dev/null
+++ b/platform/jewel/markdown/extensions/gfm-tables/metalava/gfm-tables-baseline-stable-current.txt
@@ -0,0 +1 @@
+// Baseline format: 1.0
diff --git a/platform/jewel/markdown/extensions/images/metalava/images-baseline-current.txt b/platform/jewel/markdown/extensions/images/metalava/images-baseline-current.txt
new file mode 100644
index 000000000000..9a97707763f1
--- /dev/null
+++ b/platform/jewel/markdown/extensions/images/metalava/images-baseline-current.txt
@@ -0,0 +1 @@
+// Baseline format: 1.0
diff --git a/platform/jewel/markdown/extensions/images/metalava/images-baseline-stable-current.txt b/platform/jewel/markdown/extensions/images/metalava/images-baseline-stable-current.txt
new file mode 100644
index 000000000000..9a97707763f1
--- /dev/null
+++ b/platform/jewel/markdown/extensions/images/metalava/images-baseline-stable-current.txt
@@ -0,0 +1 @@
+// Baseline format: 1.0
diff --git a/platform/jewel/markdown/ide-laf-bridge-styling/metalava/ide-laf-bridge-styling-baseline-current.txt b/platform/jewel/markdown/ide-laf-bridge-styling/metalava/ide-laf-bridge-styling-baseline-current.txt
new file mode 100644
index 000000000000..9a97707763f1
--- /dev/null
+++ b/platform/jewel/markdown/ide-laf-bridge-styling/metalava/ide-laf-bridge-styling-baseline-current.txt
@@ -0,0 +1 @@
+// Baseline format: 1.0
diff --git a/platform/jewel/markdown/ide-laf-bridge-styling/metalava/ide-laf-bridge-styling-baseline-stable-current.txt b/platform/jewel/markdown/ide-laf-bridge-styling/metalava/ide-laf-bridge-styling-baseline-stable-current.txt
new file mode 100644
index 000000000000..9a97707763f1
--- /dev/null
+++ b/platform/jewel/markdown/ide-laf-bridge-styling/metalava/ide-laf-bridge-styling-baseline-stable-current.txt
@@ -0,0 +1 @@
+// Baseline format: 1.0
diff --git a/platform/jewel/markdown/int-ui-standalone-styling/metalava/int-ui-standalone-styling-baseline-current.txt b/platform/jewel/markdown/int-ui-standalone-styling/metalava/int-ui-standalone-styling-baseline-current.txt
new file mode 100644
index 000000000000..9a97707763f1
--- /dev/null
+++ b/platform/jewel/markdown/int-ui-standalone-styling/metalava/int-ui-standalone-styling-baseline-current.txt
@@ -0,0 +1 @@
+// Baseline format: 1.0
diff --git a/platform/jewel/markdown/int-ui-standalone-styling/metalava/int-ui-standalone-styling-baseline-stable-current.txt b/platform/jewel/markdown/int-ui-standalone-styling/metalava/int-ui-standalone-styling-baseline-stable-current.txt
new file mode 100644
index 000000000000..9a97707763f1
--- /dev/null
+++ b/platform/jewel/markdown/int-ui-standalone-styling/metalava/int-ui-standalone-styling-baseline-stable-current.txt
@@ -0,0 +1 @@
+// Baseline format: 1.0
diff --git a/platform/jewel/scripts/metalava-signatures.main.kts b/platform/jewel/scripts/metalava-signatures.main.kts
index 5333e3727666..21746478ec4b 100755
--- a/platform/jewel/scripts/metalava-signatures.main.kts
+++ b/platform/jewel/scripts/metalava-signatures.main.kts
@@ -1,157 +1,137 @@
#!/usr/bin/env kotlin
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
@file:DependsOn("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.8.1")
+@file:DependsOn("com.github.ajalt.clikt:clikt-jvm:5.0.3")
@file:Import("utils.main.kts")
@file:Suppress("RAW_RUN_BLOCKING")
+import com.github.ajalt.clikt.core.CliktCommand
+import com.github.ajalt.clikt.core.Context
+import com.github.ajalt.clikt.core.main
+import com.github.ajalt.clikt.core.subcommands
+import com.github.ajalt.clikt.parameters.options.flag
+import com.github.ajalt.clikt.parameters.options.option
import com.github.pgreze.process.Redirect
import kotlin.system.exitProcess
import kotlin.time.Duration.Companion.minutes
import kotlinx.coroutines.runBlocking
-private fun printHelp() {
- println(
- """
- |Usage: kotlin metalava-signatures.main.kts [options]
- |Commands:
- | update Generate and update API signature dumps.
- | validate Validate the current signatures against previously generated ones.
- |
- |Options:
- | --release Tells Metalava to create a versioned release archival dump (or validate against one).
- | If omitted, it will use the current Jewel API version from gradle.properties.
- | --module Only checks/generates the dump for one module (e.g., ':ui').
- | --stable-only Only run tasks for the stable API surface.
- | --experimental-only Only run tasks for the experimental API surface.
- | --force Forces a clean build before running the tasks.
- | --help Display this help and exit.
- """
- .trimMargin())
-}
+private abstract class BaseMetalavaCommand(name: String) : CliktCommand(name = name) {
+ private val release: String? by
+ option(
+ "--release",
+ help =
+ "Tells Metalava to check against, or create, a versioned release archival dump. " +
+ "If omitted, it will use the current Jewel API version from gradle.properties.",
+ )
+ private val module: String? by
+ option("--module", help = "Only checks/generates the dump for one module (e.g., ':ui').")
+ private val stableOnly by option("--stable-only", help = "Only run tasks for the stable API surface.").flag()
+ private val experimentalOnly by
+ option("--experimental-only", help = "Only run tasks for the experimental API surface.").flag()
+ private val force by option("--force", help = "Forces a clean build before running the tasks.").flag()
-print("⏳ Locating Jewel folder...")
-
-private val jewelDir = findJewelRoot()
-
-if (jewelDir == null || !jewelDir.isDirectory) {
- printlnErr("Could not find the Jewel folder. Please make sure you're running the script from somewhere inside it.")
- exitProcess(1)
-}
-
-println(" DONE: ${jewelDir!!.absolutePath}")
-
-if (args.isEmpty()) {
- printHelp()
- exitProcess(1)
-}
-
-private val verb = args.firstOrNull()
-private val remainingArgs = args.drop(1)
-
-private val baseTask =
- when (verb) {
- "update" -> "updateMetalava"
- "validate" -> "checkMetalava"
- "--help" -> {
- printHelp()
- exitProcess(0)
- }
- else -> {
- println("Unknown command: $verb")
- printHelp()
+ protected fun runTask(baseTask: String) {
+ if (stableOnly && experimentalOnly) {
+ printlnErr("Error: --stable-only and --experimental-only cannot be used at the same time.")
exitProcess(1)
}
+
+ print("⏳ Locating Jewel folder...")
+ val jewelDir = findJewelRoot()
+ if (jewelDir == null || !jewelDir.isDirectory) {
+ printlnErr(
+ "Could not find the Jewel folder. " +
+ "Please make sure you're running the script from somewhere inside it."
+ )
+ exitProcess(1)
+ }
+ println(" DONE: ${jewelDir.absolutePath}")
+
+ val apiSurface =
+ when {
+ stableOnly -> "Stable"
+ experimentalOnly -> "Experimental"
+ else -> ""
+ }
+ val task = "$baseTask${apiSurface}Api"
+
+ val taskPath =
+ module?.let {
+ var path = it
+ if (!path.startsWith(":")) {
+ path = ":$path"
+ }
+ "$path:"
+ } ?: ""
+
+ val releaseFlag = release?.let { "-PmetalavaTargetRelease=$it" }
+ val cleanTask = if (force) "clean" else null
+
+ val commands = buildList {
+ add("./gradlew")
+ cleanTask?.let { add(it) }
+ add("$taskPath$task")
+ releaseFlag?.let { add(it) }
+ addAll(contributeArgs())
+ add("--continue")
+ }
+
+ println("⏳ Executing: ${commands.joinToString(" ")}")
+
+ val result = runBlocking {
+ runCommand(
+ commands.joinToString(" "),
+ jewelDir,
+ timeoutAmount = 60.minutes,
+ outputRedirect = Redirect.PRINT,
+ )
+ }
+
+ if (result.isSuccess) {
+ println("\n✅ Done!")
+ }
}
-private var taskPath = ""
-private var releaseFlag: String? = null
-private var cleanTask: String? = null
-private var apiSurface: String? = null
+ protected open fun contributeArgs(): List = emptyList()
+}
-private val argIterator = remainingArgs.iterator()
+private class UpdateCommand : BaseMetalavaCommand(name = "update") {
+ override fun help(context: Context): String = "Update stored Metalava API signature dumps."
-while (argIterator.hasNext()) {
- when (val arg = argIterator.next()) {
- "--release" -> {
- if (!argIterator.hasNext()) {
- printlnErr("Error: Missing version for --release")
- printHelp()
- exitProcess(1)
- }
- val version = argIterator.next()
- if (version.startsWith("--")) {
- printlnErr("Error: Missing version for --release")
- printHelp()
- exitProcess(1)
- }
- releaseFlag = "-PmetalavaTargetRelease=$version"
- }
- "--module" -> {
- if (!argIterator.hasNext()) {
- printlnErr("Error: Missing module path for --module")
- printHelp()
- exitProcess(1)
- }
- val modulePath = argIterator.next()
- if (modulePath.startsWith("--")) {
- printlnErr("Error: Missing module path for --module")
- printHelp()
- exitProcess(1)
- }
- taskPath = "$modulePath:"
-
- if (!taskPath.startsWith(":")) {
- taskPath = ":$taskPath"
- }
- }
- "--stable-only" -> {
- if (apiSurface != null) {
- printlnErr("Error: --stable-only and --experimental-only cannot be used at the same time.")
- printHelp()
- exitProcess(1)
- }
- apiSurface = "Stable"
- }
- "--experimental-only" -> {
- if (apiSurface != null) {
- printlnErr("Error: --stable-only and --experimental-only cannot be used at the same time.")
- printHelp()
- exitProcess(1)
- }
- apiSurface = "Experimental"
- }
- "--force" -> {
- cleanTask = "clean"
- }
- "--help" -> {
- printHelp()
- exitProcess(0)
- }
- else -> {
- println("Unknown option: $arg")
- printHelp()
- exitProcess(1)
- }
+ override fun run() {
+ runTask("updateMetalava")
}
}
-private val apiSurfaceTaskNamePart = apiSurface ?: ""
-private val task = "$baseTask${apiSurfaceTaskNamePart}Api"
+private class ValidateCommand : BaseMetalavaCommand(name = "validate") {
+ private val updateBaseline by
+ option(
+ "--update-baseline",
+ "--update-baselines",
+ help = "Writes any API check issues to the baseline file for this release.",
+ )
+ .flag()
-private val commands = buildList {
- add("./gradlew")
- cleanTask?.let { add(it) }
- add("$taskPath$task")
- releaseFlag?.let { add(it) }
- add("--continue")
+ override fun help(context: Context): String = "Validate the current signatures against previously stored ones."
+
+ override fun run() {
+ runTask("checkMetalava")
+ }
+
+ override fun contributeArgs(): List =
+ if (updateBaseline) {
+ listOf("-Pupdate-baseline=true")
+ } else {
+ emptyList()
+ }
}
-println("⏳ Executing: ${commands.joinToString(" ")}")
+private class MetalavaSignaturesCommand : CliktCommand() {
+ override fun help(context: Context): String =
+ "A script to update and validate Metalava API signatures for Jewel modules."
-private val result = runBlocking {
- runCommand(commands.joinToString(" "), jewelDir, timeoutAmount = 60.minutes, outputRedirect = Redirect.PRINT)
+ override fun run() = Unit
}
-if (result.isSuccess) {
- println("\n✅ Done!")
-}
+MetalavaSignaturesCommand().subcommands(UpdateCommand(), ValidateCommand()).main(args)
diff --git a/platform/jewel/settings.gradle.kts b/platform/jewel/settings.gradle.kts
index ec4e95c887b3..2931a57bff13 100644
--- a/platform/jewel/settings.gradle.kts
+++ b/platform/jewel/settings.gradle.kts
@@ -37,6 +37,7 @@ include(
":ide-laf-bridge",
":int-ui:int-ui-decorated-window",
":int-ui:int-ui-standalone",
+ ":int-ui:int-ui-standalone-tests",
":detekt-plugin",
":markdown:core",
":markdown:extensions:autolink",
diff --git a/platform/jewel/ui/metalava/ui-baseline-current.txt b/platform/jewel/ui/metalava/ui-baseline-current.txt
new file mode 100644
index 000000000000..bdcf52364d58
--- /dev/null
+++ b/platform/jewel/ui/metalava/ui-baseline-current.txt
@@ -0,0 +1,3 @@
+// Baseline format: 1.0
+RemovedClass: org.jetbrains.jewel.ui.util.ModifierExtensionsKt:
+ Binary breaking change: Removed class org.jetbrains.jewel.ui.util.ModifierExtensionsKt
diff --git a/platform/jewel/ui/metalava/ui-baseline-stable-current.txt b/platform/jewel/ui/metalava/ui-baseline-stable-current.txt
new file mode 100644
index 000000000000..bdcf52364d58
--- /dev/null
+++ b/platform/jewel/ui/metalava/ui-baseline-stable-current.txt
@@ -0,0 +1,3 @@
+// Baseline format: 1.0
+RemovedClass: org.jetbrains.jewel.ui.util.ModifierExtensionsKt:
+ Binary breaking change: Removed class org.jetbrains.jewel.ui.util.ModifierExtensionsKt