[JEWEL-1051] Fix Metalava CI checks, add gen'd theme sanity checks

This fixes the issues we've been having with PRs that introduce changes
in a release, and also gives us a way to deal with the platform removing
deprecated and unused APIs in their usual cleanup cycles.

The solution is using Metalava baselines for any changes mid-cycle; we
write all changes there and, when it's time to release, we validate and
store the changes for that release. While this is not a perfect solution
it at least allows us to see those baseline changes in the PRs. Then we
can at least review them.

The release and PR documentations were updated accordingly.

This also updates the generated themes and adds a sanity check test that
should ensure they get updated when the major platform version is also
bumped (and, that we don't mistakenly cherry-pick their changes to an
older release branch).

Note: this also updates ktfmt's IJ plugin settings since the new plugin
version changed the storage format slightly.

closes https://github.com/JetBrains/intellij-community/pull/3276

(cherry picked from commit 5127f3429d3a16710797c6e7e279abd0dcfbd21e)


(cherry picked from commit 394a30be911b17a7eb03169ac96191f877933915)

IJ-MR-179919

GitOrigin-RevId: 2bd74824ae8928616a63ffa0e1cb2420b92e871a
This commit is contained in:
Sebastiano Poggi
2025-10-31 12:48:53 +00:00
committed by intellij-monorepo-bot
parent d9df4937bb
commit 1f3e06d0b1
44 changed files with 437 additions and 173 deletions
+1
View File
@@ -1129,6 +1129,7 @@
<module fileurl="file://$PROJECT_DIR$/platform/jewel/ide-laf-bridge/intellij.platform.jewel.ideLafBridge.iml" filepath="$PROJECT_DIR$/platform/jewel/ide-laf-bridge/intellij.platform.jewel.ideLafBridge.iml" />
<module fileurl="file://$PROJECT_DIR$/platform/jewel/int-ui/int-ui-decorated-window/intellij.platform.jewel.intUi.decoratedWindow.iml" filepath="$PROJECT_DIR$/platform/jewel/int-ui/int-ui-decorated-window/intellij.platform.jewel.intUi.decoratedWindow.iml" />
<module fileurl="file://$PROJECT_DIR$/platform/jewel/int-ui/int-ui-standalone/intellij.platform.jewel.intUi.standalone.iml" filepath="$PROJECT_DIR$/platform/jewel/int-ui/int-ui-standalone/intellij.platform.jewel.intUi.standalone.iml" />
<module fileurl="file://$PROJECT_DIR$/platform/jewel/int-ui/int-ui-standalone-tests/intellij.platform.jewel.intUi.standalone.tests.iml" filepath="$PROJECT_DIR$/platform/jewel/int-ui/int-ui-standalone-tests/intellij.platform.jewel.intUi.standalone.tests.iml" />
<module fileurl="file://$PROJECT_DIR$/platform/jewel/markdown/core/intellij.platform.jewel.markdown.core.iml" filepath="$PROJECT_DIR$/platform/jewel/markdown/core/intellij.platform.jewel.markdown.core.iml" />
<module fileurl="file://$PROJECT_DIR$/platform/jewel/markdown/extensions/autolink/intellij.platform.jewel.markdown.extensions.autolink.iml" filepath="$PROJECT_DIR$/platform/jewel/markdown/extensions/autolink/intellij.platform.jewel.markdown.extensions.autolink.iml" />
<module fileurl="file://$PROJECT_DIR$/platform/jewel/markdown/extensions/gfm-alerts/intellij.platform.jewel.markdown.extensions.gfmAlerts.iml" filepath="$PROJECT_DIR$/platform/jewel/markdown/extensions/gfm-alerts/intellij.platform.jewel.markdown.extensions.gfmAlerts.iml" />
+1
View File
@@ -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
+1 -1
View File
@@ -2,8 +2,8 @@
<project version="4">
<component name="KtfmtSettings">
<option name="customBlockIndent" value="4" />
<option name="customManageTrailingCommas" value="true" />
<option name="customMaxLineLength" value="120" />
<option name="customTrailingCommaManagementStrategy" value="Complete" />
<option name="enableKtfmt" value="Enabled" />
<option name="enabled" value="true" />
<option name="uiFormatterStyle" value="Custom" />
@@ -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)
@@ -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<Boolean>
@get:Input abstract val updateBaseline: Property<Boolean>
@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<Boolean>
val updateBaseline: Property<Boolean>
val baselineFile: ConfigurableFileCollection
}
abstract class Action : WorkAction<Parameters> {
@@ -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" }
@@ -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<DefaultTask>("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<FileCollection>,
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"
}
@@ -0,0 +1 @@
// Baseline format: 1.0
@@ -0,0 +1 @@
// Baseline format: 1.0
+2
View File
@@ -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.
+8 -4
View File
@@ -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 <previous-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 <new-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 <new-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.
@@ -0,0 +1 @@
// Baseline format: 1.0
@@ -0,0 +1 @@
// Baseline format: 1.0
@@ -0,0 +1 @@
// Baseline format: 1.0
@@ -0,0 +1 @@
// Baseline format: 1.0
@@ -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
@@ -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() }
@@ -0,0 +1,31 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="JAVA_MODULE" version="4">
<component name="FacetManager">
<facet type="kotlin-language" name="Kotlin">
<configuration version="5" platform="JVM 21" allPlatforms="JVM [21]" useProjectSettings="false">
<compilerSettings>
<option name="additionalArguments" value="-Xjvm-default=all -opt-in=androidx.compose.ui.ExperimentalComposeUiApi -Xcontext-parameters -opt-in=androidx.compose.foundation.ExperimentalFoundationApi -opt-in=org.jetbrains.jewel.foundation.ExperimentalJewelApi -opt-in=org.jetbrains.jewel.foundation.InternalJewelApi -Xexplicit-api=strict -XXLanguage:+AllowEagerSupertypeAccessibilityChecks" />
</compilerSettings>
<compilerArguments>
<stringArguments>
<stringArg name="jvmTarget" arg="21" />
<stringArg name="apiVersion" arg="2.2" />
<stringArg name="languageVersion" arg="2.2" />
</stringArguments>
</compilerArguments>
</configuration>
</facet>
</component>
<component name="NewModuleRootManager" inherit-compiler-output="true">
<exclude-output />
<content url="file://$MODULE_DIR$">
<sourceFolder url="file://$MODULE_DIR$/src/test/kotlin" isTestSource="true" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
<orderEntry type="library" name="kotlin-stdlib" level="project" />
<orderEntry type="library" scope="TEST" name="kotlin-test" level="project" />
<orderEntry type="library" scope="TEST" name="JUnit5" level="project" />
<orderEntry type="module" module-name="intellij.platform.jewel.intUi.standalone" />
</component>
</module>
@@ -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"
@@ -53,14 +53,17 @@ tasks {
mustRunAfter(generateThemes)
mustRunAfter(ktfmtFormatMain)
}
lintKotlinMain {
mustRunAfter(generateThemes)
mustRunAfter(ktfmtFormatMain)
}
withType<Detekt>().configureEach {
mustRunAfter(generateThemes)
mustRunAfter(ktfmtFormatMain)
}
detektMain {
mustRunAfter(generateThemes)
mustRunAfter(ktfmtFormatMain)
@@ -14,9 +14,7 @@
</stringArguments>
<arrayArguments>
<arrayArg name="pluginClasspaths">
<args>
<arg>$MAVEN_REPOSITORY$/org/jetbrains/kotlin/kotlin-compose-compiler-plugin/2.2.20/kotlin-compose-compiler-plugin-2.2.20.jar</arg>
</args>
<args>$MAVEN_REPOSITORY$/org/jetbrains/kotlin/kotlin-compose-compiler-plugin/2.2.20/kotlin-compose-compiler-plugin-2.2.20.jar</args>
</arrayArg>
</arrayArguments>
</compilerArguments>
@@ -0,0 +1 @@
// Baseline format: 1.0
@@ -0,0 +1 @@
// Baseline format: 1.0
@@ -0,0 +1 @@
// Baseline format: 1.0
@@ -0,0 +1 @@
// Baseline format: 1.0
@@ -0,0 +1 @@
// Baseline format: 1.0
@@ -0,0 +1 @@
// Baseline format: 1.0
@@ -0,0 +1 @@
// Baseline format: 1.0
@@ -0,0 +1 @@
// Baseline format: 1.0
@@ -0,0 +1 @@
// Baseline format: 1.0
@@ -0,0 +1 @@
// Baseline format: 1.0
@@ -0,0 +1 @@
// Baseline format: 1.0
@@ -0,0 +1 @@
// Baseline format: 1.0
@@ -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 <command> [options]
|Commands:
| update Generate and update API signature dumps.
| validate Validate the current signatures against previously generated ones.
|
|Options:
| --release <version> 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 <path> 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<String> = 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<String> =
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)
+1
View File
@@ -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",
@@ -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
@@ -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