diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 92a2ed02..7e323713 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -14,9 +14,9 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Validate Gradle Wrapper - uses: gradle/wrapper-validation-action@v1 + uses: gradle/wrapper-validation-action@v2 check: @@ -35,15 +35,15 @@ jobs: steps: # Setup environment - name: Checkout repository - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Set up JDK - uses: actions/setup-java@v3 + uses: actions/setup-java@v4 with: java-version: ${{ matrix.jdk }} distribution: temurin # Setup caches and collect metadata - name: Setup cache for Gradle and dependencies - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: | ~/.gradle/caches @@ -55,7 +55,7 @@ jobs: - name: Collect metadata for upcoming steps and jobs run: ./gradlew --stacktrace metadata listProductsReleases - name: Setup cache for IntelliJ Plugin Verifier - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: | ~/.pluginVerifier/ides @@ -74,21 +74,21 @@ jobs: # Upload artifacts - name: Upload build reports if: always() - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: reports-jdk${{matrix.jdk}} path: build/reports/ if-no-files-found: ignore - name: Upload build result if: ${{ matrix.primary }} - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: build-result path: build/distributions/ if-no-files-found: error - name: Upload metadata if: ${{ matrix.primary }} - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: metadata path: build/metadata/ @@ -104,7 +104,7 @@ jobs: steps: # Remove old release drafts - name: Remove old release drafts - uses: actions/github-script@v6 + uses: actions/github-script@v7 with: script: | const owner = context.repo.owner; @@ -116,12 +116,12 @@ jobs: } # Download build artifacts - name: Download build result - uses: actions/download-artifact@v3 + uses: actions/download-artifact@v4 with: name: build-result path: build/distributions/ - name: Download metadata - uses: actions/download-artifact@v3 + uses: actions/download-artifact@v4 with: name: metadata path: build/metadata/ @@ -134,7 +134,7 @@ jobs: echo "zipname=$(basename "$(cat build/metadata/zipfile.txt)")" >> "$GITHUB_OUTPUT" # Fail job if the release tag already exists and points to a different commit - name: Check release tag - uses: actions/github-script@v6 + uses: actions/github-script@v7 env: TAG_NAME: v${{ steps.metadata.outputs.version }} with: @@ -167,7 +167,7 @@ jobs: } # Create GitHub release draft - name: Create GitHub release draft - uses: actions/github-script@v6 + uses: actions/github-script@v7 env: TAG_NAME: v${{ steps.metadata.outputs.version }} ZIP_FILE: ${{ steps.metadata.outputs.zipfile }} diff --git a/.github/workflows/bump-version.yml b/.github/workflows/bump-version.yml index eed0f9bb..ee3fbc5d 100644 --- a/.github/workflows/bump-version.yml +++ b/.github/workflows/bump-version.yml @@ -19,9 +19,9 @@ jobs: steps: # Setup environment - name: Checkout repository - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Set up JDK 17 - uses: actions/setup-java@v3 + uses: actions/setup-java@v4 with: java-version: 17 distribution: temurin @@ -42,7 +42,7 @@ jobs: fi # Setup cache - name: Setup cache for Gradle and dependencies - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: | ~/.gradle/caches @@ -53,7 +53,7 @@ jobs: ${{hashFiles('gradle.properties', '**/*.gradle.kts')}}" # Update files - name: Obtain release notes - uses: actions/github-script@v6 + uses: actions/github-script@v7 with: script: | await require('.github/scripts/get-release-notes.js')({ diff --git a/.github/workflows/publish-to-jetbrains.yml b/.github/workflows/publish-to-jetbrains.yml index ec854e85..9bad5219 100644 --- a/.github/workflows/publish-to-jetbrains.yml +++ b/.github/workflows/publish-to-jetbrains.yml @@ -19,15 +19,15 @@ jobs: steps: # Setup environment - name: Checkout repository - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Set up JDK 17 - uses: actions/setup-java@v3 + uses: actions/setup-java@v4 with: java-version: 17 distribution: temurin # Setup cache - name: Setup cache for Gradle and dependencies - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: | ~/.gradle/caches @@ -38,7 +38,7 @@ jobs: ${{hashFiles('gradle.properties', '**/*.gradle.kts')}}" # Build and publish - name: Obtain release notes - uses: actions/github-script@v6 + uses: actions/github-script@v7 with: script: | await require('.github/scripts/get-release-notes.js')({ @@ -57,7 +57,7 @@ jobs: # Upload artifacts - name: Upload build reports if: steps.gradle-build.outcome == 'success' || steps.gradle-build.outcome == 'failure' - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: build-reports path: build/reports/ diff --git a/build.gradle.kts b/build.gradle.kts index 2e1e24c9..533671bf 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,18 +1,14 @@ import org.jetbrains.changelog.Changelog import org.jetbrains.changelog.markdownToHTML -import org.jetbrains.grammarkit.tasks.GenerateLexerTask -import org.jetbrains.grammarkit.tasks.GenerateParserTask import org.jetbrains.intellij.tasks.RunPluginVerifierTask plugins { - // Java support id("java") - // gradle-intellij-plugin - read more: https://github.com/JetBrains/gradle-intellij-plugin - id("org.jetbrains.intellij") version "1.16.0" - // gradle-changelog-plugin - read more: https://github.com/JetBrains/gradle-changelog-plugin - id("org.jetbrains.changelog") version "2.2.0" - // grammarkit - read more: https://github.com/JetBrains/gradle-grammar-kit-plugin - id("org.jetbrains.grammarkit") version "2022.3.2" + alias(libs.plugins.jetbrains.intellij) + alias(libs.plugins.jetbrains.changelog) + alias(libs.plugins.jetbrains.grammarkit) + id("local.bump-version") + id("local.jbr-guidance") } // Import variables from gradle.properties file @@ -38,9 +34,9 @@ repositories { } dependencies { - testImplementation(platform("org.junit:junit-bom:5.9.1")) - testImplementation("org.junit.jupiter:junit-jupiter") - testRuntimeOnly("org.junit.vintage:junit-vintage-engine") + testImplementation(platform(libs.junit5.bom)) + testImplementation(libs.junit5.jupiter) + testRuntimeOnly(libs.junit5.vintage.engine) } // Configure gradle-intellij-plugin plugin. @@ -77,30 +73,6 @@ sourceSets { } } -val tasksUsingDownloadedJbr = mutableListOf() -// Check https://github.com/gradle/gradle/issues/20151 if an alternative for deprecated buildFinished became available. -gradle.buildFinished { - val regex = Regex("""\.gradle/.*/jbr/.*/java\b""") - for (task in tasksUsingDownloadedJbr) { - if (task.state.failure?.cause?.message?.contains(regex) == true) { - logger.error(""" - | - |! Info for users on NixOS: - |! - |! The JetBrains Runtime (JBR) downloaded by Gradle is not compatible with NixOS. - |! You may run the ‘:jbr’ task to configure the runtime of instead. - |! Alternatively, you may run the following command within the project directory. - |! - |! nix-build '' -A jetbrains.jdk -o jbr - |! - |! This will create a symlink to the package jetbrains.jdk of nixpkgs at - |! ${'$'}projectDir/jbr, which is automatically detected by future builds. - """.trimMargin()) - break - } - } -} - tasks { withType { options.encoding = "UTF-8" @@ -109,59 +81,45 @@ tasks { options.encoding = "UTF-8" } - task("jbr") { - description = "Create a symlink to package jetbrains.jdk" - group = "build setup" - commandLine("nix-build", "", "-A", "jetbrains.jdk", "-o", "jbr") - } - - withType { - project.file("jbr/bin/java") - .takeIf { it.exists() } - ?.let { projectExecutable = it.toString() } - ?: tasksUsingDownloadedJbr.add(this) - } - withType { systemProperty("idea.test.execution.policy", "org.nixos.idea.NixTestExecutionPolicy") systemProperty("plugin.testDataPath", rootProject.rootDir.resolve("src/test/testData").path) } - task("metadata") { - outputs.upToDateWhen { false } - doLast { - val dir = project.layout.buildDirectory.dir("metadata").get().asFile - dir.mkdirs() - dir.resolve("version.txt").writeText(pluginVersion) - dir.resolve("zipfile.txt").writeText(buildPlugin.get().archiveFile.get().toString()) - dir.resolve("latest_changelog.md").writeText(with(changelog) { + task("metadata") { + outputDir = layout.buildDirectory.dir("metadata") + file("version.txt", pluginVersion) + file("zipfile.txt") { buildPlugin.get().archiveFile.get().toString() } + file("latest_changelog.md") { + with(changelog) { renderItem( (getOrNull(pluginVersion) ?: getUnreleased()) .withHeader(false) .withEmptySections(false), Changelog.OutputType.MARKDOWN ) - }) + } } } - val generateNixLexer by registering(GenerateLexerTask::class) { + generateLexer { sourceFile = file("src/main/lang/Nix.flex") - targetDir = "src/gen/java/org/nixos/idea/lang" - targetClass = "_NixLexer" + targetOutputDir = file("src/gen/java/org/nixos/idea/lang") purgeOldFiles = true } - val generateNixParser by registering(GenerateParserTask::class) { + generateParser { sourceFile = file("src/main/lang/Nix.bnf") - targetRoot = "src/gen/java" + targetRootOutputDir = file("src/gen/java") pathToParser = "/org/nixos/idea/lang/NixParser" pathToPsiRoot = "/org/nixos/idea/psi" purgeOldFiles = true + // Task :generateLexer deletes files generated by this task when executed afterward. + mustRunAfter(generateLexer) } compileJava { - dependsOn(generateNixLexer, generateNixParser) + dependsOn(generateLexer, generateParser) } test { @@ -174,15 +132,19 @@ tasks { untilBuild = pluginUntilBuild // Extract the section from README.md and provide for the plugin's manifest - pluginDescription = projectDir.resolve("README.md").readText().lines().run { - val start = "" - val end = "" - - if (!containsAll(listOf(start, end))) { - throw GradleException("Plugin description section not found in README.md:\n$start ... $end") - } - subList(indexOf(start) + 1, indexOf(end)) - }.joinToString("\n").run { markdownToHTML(this) } + pluginDescription = providers.provider { + projectDir.resolve("README.md").readText().lines() + .run { + val start = "" + val end = "" + if (!containsAll(listOf(start, end))) { + throw GradleException("Plugin description section not found in README.md:\n$start ... $end") + } + subList(indexOf(start) + 1, indexOf(end)) + } + .joinToString("\n") + .run { markdownToHTML(this) } + } // Get the latest available change notes from the changelog file changeNotes = provider { @@ -199,13 +161,16 @@ tasks { runPluginVerifier { failureLevel = RunPluginVerifierTask.FailureLevel.ALL + // Version 1.364 seems to be broken and always complains about supposedly missing 'plugin.xml': + // https://youtrack.jetbrains.com/issue/MP-6388 + verifierVersion = "1.307" } publishPlugin { - token = System.getenv("JETBRAINS_TOKEN") - channels = listOf(pluginVersion.split('-').getOrElse(1) { "default" }.split('.').first()) + token = providers.environmentVariable("JETBRAINS_TOKEN") + // Note: `listOf("foo").first()` does not what you think on Java 21 and Gradle 8.6. (The return type is TaskProvider) + // See https://github.com/gradle/gradle/issues/27699 and https://youtrack.jetbrains.com/issue/KT-65235. + channels = listOf(pluginVersion.split('-').getOrElse(1) { "default" }.split('.')[0]) } } - -apply(from = "gradle/bumpVersion.gradle.kts") diff --git a/gradle.properties b/gradle.properties index c753ac80..7eb49fe3 100644 --- a/gradle.properties +++ b/gradle.properties @@ -9,3 +9,9 @@ pluginUntilBuild = 233.* platformType = IC platformVersion = 2022.3.3 + +# Gradle Configuration +# -> https://docs.gradle.org/current/userguide/build_environment.html#sec:gradle_configuration_properties +org.gradle.parallel=true +org.gradle.caching=true +org.gradle.configuration-cache=true diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml new file mode 100644 index 00000000..ebc3723b --- /dev/null +++ b/gradle/libs.versions.toml @@ -0,0 +1,15 @@ +# Gradle Version Catalog +# -> https://docs.gradle.org/current/userguide/platforms.html#sub::toml-dependencies-format + +[libraries] +junit5-bom = { module = "org.junit:junit-bom", version = "5.9.1" } +junit5-jupiter = { module = "org.junit.jupiter:junit-jupiter" } +junit5-vintage-engine = { module = "org.junit.vintage:junit-vintage-engine" } + +[plugins] +# gradle-intellij-plugin - read more: https://github.com/JetBrains/gradle-intellij-plugin +jetbrains-intellij = { id = "org.jetbrains.intellij", version = "1.17.2" } +# gradle-changelog-plugin - read more: https://github.com/JetBrains/gradle-changelog-plugin +jetbrains-changelog = { id = "org.jetbrains.changelog", version = "2.2.0" } +# grammarkit - read more: https://github.com/JetBrains/gradle-grammar-kit-plugin +jetbrains-grammarkit = { id = "org.jetbrains.grammarkit", version = "2022.3.2.2" } diff --git a/gradle/plugins/build.gradle.kts b/gradle/plugins/build.gradle.kts new file mode 100644 index 00000000..6f445dcb --- /dev/null +++ b/gradle/plugins/build.gradle.kts @@ -0,0 +1,28 @@ +plugins { + `kotlin-dsl` +} + +repositories { + gradlePluginPortal() +} + +dependencies { + implementation(plugin(libs.plugins.jetbrains.intellij)) +} + +/** + * Maps plugin dependencies to its maven coordinates. + * + * Hopefully, there will be native support for adding plugins as a dependency in the future. + * See [Gradle Issue #17963 – Accept plugin declarations from version catalog also as libraries](https://github.com/gradle/gradle/issues/17963). + */ +fun plugin(pluginProvider: Provider): Provider> { + return pluginProvider.map { + val id = it.pluginId + mapOf( + "group" to id, + "name" to "$id.gradle.plugin", // PLUGIN_MARKER_SUFFIX + "version" to it.version.requiredVersion, + ) + } +} diff --git a/gradle/plugins/settings.gradle.kts b/gradle/plugins/settings.gradle.kts new file mode 100644 index 00000000..04ebc121 --- /dev/null +++ b/gradle/plugins/settings.gradle.kts @@ -0,0 +1,6 @@ +dependencyResolutionManagement { + // Reuse version catalog from the main build. + versionCatalogs { + create("libs", { from(files("../libs.versions.toml")) }) + } +} diff --git a/gradle/plugins/src/main/kotlin/MetadataTask.kt b/gradle/plugins/src/main/kotlin/MetadataTask.kt new file mode 100644 index 00000000..f8344683 --- /dev/null +++ b/gradle/plugins/src/main/kotlin/MetadataTask.kt @@ -0,0 +1,36 @@ +import org.gradle.api.DefaultTask +import org.gradle.api.file.DirectoryProperty +import org.gradle.api.provider.MapProperty +import org.gradle.api.provider.ProviderFactory +import org.gradle.api.tasks.Input +import org.gradle.api.tasks.OutputDirectory +import org.gradle.api.tasks.TaskAction +import javax.inject.Inject + +abstract class MetadataTask : DefaultTask() { + + @get:Input + abstract val files: MapProperty + + @get:OutputDirectory + abstract val outputDir: DirectoryProperty + + @get:Inject + protected abstract val providers: ProviderFactory + + fun file(fileName: String, content: String) { + files.put(fileName, content) + } + + fun file(fileName: String, content: () -> String) { + files.put(fileName, providers.provider(content)) + } + + @TaskAction + protected fun writeFiles() { + val dir = outputDir.get().asFile + for ((fileName, content) in files.get()) { + dir.resolve(fileName).writeText(content) + } + } +} diff --git a/gradle/bumpVersion.gradle.kts b/gradle/plugins/src/main/kotlin/local.bump-version.gradle.kts similarity index 100% rename from gradle/bumpVersion.gradle.kts rename to gradle/plugins/src/main/kotlin/local.bump-version.gradle.kts diff --git a/gradle/plugins/src/main/kotlin/local.jbr-guidance.gradle.kts b/gradle/plugins/src/main/kotlin/local.jbr-guidance.gradle.kts new file mode 100644 index 00000000..f6a78cc9 --- /dev/null +++ b/gradle/plugins/src/main/kotlin/local.jbr-guidance.gradle.kts @@ -0,0 +1,99 @@ +import org.gradle.kotlin.dsl.support.serviceOf +import org.gradle.tooling.Failure +import org.gradle.tooling.events.FailureResult +import org.gradle.tooling.events.FinishEvent +import org.gradle.tooling.events.OperationCompletionListener +import org.jetbrains.intellij.tasks.RunIdeBase +import java.util.function.Predicate + +val jbrHome = rootProject.file("jbr") + +task("jbr") { + description = "Create a symlink to package jetbrains.jdk" + group = "build setup" + commandLine("nix-build", "", "-A", "jetbrains.jdk", "-o", jbrHome) +} + +jbrHome.resolve("bin/java").takeIf { it.exists() } + ?.also { jbrExecutable -> + // Override JVM of gradle-intellij-plugin with JVM at `jbr/bin/java` + // https://github.com/JetBrains/gradle-intellij-plugin/issues/1437#issuecomment-1987310948 + tasks.withType { + projectExecutable = jbrExecutable.toString() + } + pluginManager.withPlugin("org.jetbrains.intellij") { + // Uses `withPlugin` because the following code must run after the gradle-intellij-plugin got applied. + // We must also use `afterEvaluate`, as the gradle-intellij-plugin uses `afterEvaluate` as well. + // Otherwise, gradle-intellij-plugin would just overwrite our configuration. + afterEvaluate { + tasks.withType { + executable = jbrExecutable.toString() + } + } + } + } + ?: run { + // There is no JVM at `jbr/bin/java`. Monitor the build and provide some helpful message when it fails. + val service = gradle.sharedServices.registerIfAbsent("jbr-guidance", JbrGuidance::class) {} + serviceOf().onTaskCompletion(service) + // If the configuration cache is enabled, the JBR failure was triggered during serialization + // before any task gets executed. (As of gradle-intellij-plugin 1.17.2 and Gradle 8.6.) + // Unfortunately, only failures in tasks are reported to the JbrGuidance build service. + // If there are tasks scheduled, but none of them is executed, we therefore also print the message. + project.gradle.taskGraph.whenReady { + if (allTasks.isNotEmpty()) { + service.get().expectAtLeastOneTask() + } + } + } + +abstract class JbrGuidance : BuildService, OperationCompletionListener, AutoCloseable { + private val regex = Regex("""\.gradle/.*/(jbr|jbre)/.*/java\b""") + private val logger = Logging.getLogger("jbr-guidance") + private var taskMissing = false + private var observedJbrError = false + + fun expectAtLeastOneTask() { + taskMissing = true + } + + override fun onFinish(event: FinishEvent?) { + taskMissing = false + val result = event?.result as? FailureResult + result?.failures?.forEach { failure -> + if (anyCauseMatches(failure) { it.message?.contains(regex) == true }) { + observedJbrError = true + } + } + } + + override fun close() { + if (observedJbrError || taskMissing) { + logger.error( + """ + | + |! Info for users on NixOS: + |! + |! The JetBrains Runtime (JBR) downloaded by Gradle is not compatible with NixOS. + |! You may run the ‘:jbr’ task to configure the runtime of instead. + |! Alternatively, you may run the following command within the project directory. + |! + |! nix-build '' -A jetbrains.jdk -o jbr + |! + |! This will create a symlink to the package jetbrains.jdk of nixpkgs at + |! ${'$'}projectDir/jbr, which is automatically detected by future builds. + """.trimMargin() + ) + } + } + + private fun anyCauseMatches(failure: Failure?, condition: Predicate): Boolean { + return if (failure == null) { + false + } else if (condition.test(failure)) { + true + } else { + failure.causes.any { anyCauseMatches(it, condition) } + } + } +} diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 7f93135c..d64cd491 100644 Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 3fa8f862..a80b22ce 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.6-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/gradlew.bat b/gradlew.bat index 6689b85b..7101f8e4 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -43,11 +43,11 @@ set JAVA_EXE=java.exe %JAVA_EXE% -version >NUL 2>&1 if %ERRORLEVEL% equ 0 goto execute -echo. -echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 goto fail @@ -57,11 +57,11 @@ set JAVA_EXE=%JAVA_HOME%/bin/java.exe if exist "%JAVA_EXE%" goto execute -echo. -echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 goto fail diff --git a/settings.gradle.kts b/settings.gradle.kts index 80ec3a45..aa5f47d4 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -1 +1,7 @@ +pluginManagement { + includeBuild("gradle/plugins") +} + rootProject.name = "NixIDEA" + +enableFeaturePreview("STABLE_CONFIGURATION_CACHE")