mirror of
https://gitflic.ru/project/openide/openide.git
synced 2026-09-27 10:03:11 +07:00
[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:
committed by
intellij-monorepo-bot
parent
d9df4937bb
commit
1f3e06d0b1
Generated
+1
@@ -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" />
|
||||
|
||||
@@ -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
|
||||
|
||||
Generated
+1
-1
@@ -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" />
|
||||
|
||||
+1
-1
@@ -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)
|
||||
|
||||
+50
-29
@@ -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" }
|
||||
|
||||
|
||||
+24
-5
@@ -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
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
+1
@@ -0,0 +1 @@
|
||||
// Baseline format: 1.0
|
||||
+1
@@ -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() }
|
||||
+31
@@ -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>
|
||||
+122
@@ -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)
|
||||
|
||||
+1
-3
@@ -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>
|
||||
|
||||
+1
@@ -0,0 +1 @@
|
||||
// Baseline format: 1.0
|
||||
+1
@@ -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
@@ -0,0 +1 @@
|
||||
// Baseline format: 1.0
|
||||
@@ -0,0 +1 @@
|
||||
// Baseline format: 1.0
|
||||
+1
@@ -0,0 +1 @@
|
||||
// Baseline format: 1.0
|
||||
+1
@@ -0,0 +1 @@
|
||||
// Baseline format: 1.0
|
||||
+1
@@ -0,0 +1 @@
|
||||
// Baseline format: 1.0
|
||||
@@ -0,0 +1 @@
|
||||
// Baseline format: 1.0
|
||||
+1
@@ -0,0 +1 @@
|
||||
// Baseline format: 1.0
|
||||
@@ -0,0 +1 @@
|
||||
// Baseline format: 1.0
|
||||
@@ -0,0 +1 @@
|
||||
// Baseline format: 1.0
|
||||
+1
@@ -0,0 +1 @@
|
||||
// Baseline format: 1.0
|
||||
+1
@@ -0,0 +1 @@
|
||||
// Baseline format: 1.0
|
||||
+1
@@ -0,0 +1 @@
|
||||
// Baseline format: 1.0
|
||||
+1
@@ -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)
|
||||
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user