| import groovy.xml.XmlParser |
| import org.gradle.api.JavaVersion.VERSION_17 |
| import org.gradle.api.tasks.testing.logging.TestExceptionFormat |
| import org.gradle.api.tasks.testing.logging.TestLogEvent |
| import org.gradle.nativeplatform.platform.internal.DefaultNativePlatform |
| import org.jetbrains.intellij.tasks.* |
| import org.jetbrains.kotlin.gradle.dsl.JvmTarget |
| import org.jetbrains.kotlin.gradle.dsl.KotlinVersion |
| import org.jetbrains.kotlin.gradle.tasks.KotlinCompile |
| import org.jsoup.Jsoup |
| import java.io.Writer |
| import kotlin.concurrent.thread |
| |
| // The same as `--stacktrace` param |
| gradle.startParameter.showStacktrace = ShowStacktrace.ALWAYS |
| |
| val isCI = System.getenv("CI") != null |
| val isTeamcity = System.getenv("TEAMCITY_VERSION") != null |
| |
| val channel = prop("publishChannel") |
| val platformVersion = prop("platformVersion").toInt() |
| val baseIDE = prop("baseIDE") |
| val ideToRun = prop("ideToRun").ifEmpty { baseIDE } |
| val ideaVersion = prop("ideaVersion") |
| val clionVersion = prop("clionVersion") |
| val rustRoverVersion = prop("rustRoverVersion") |
| val baseVersion = versionForIde(baseIDE) |
| val baseVersionForRun = versionForIde(ideToRun) |
| |
| val tomlPlugin = "org.toml.lang" |
| val nativeDebugPlugin = if (baseIDE == "idea") prop("nativeDebugPlugin") else "com.intellij.nativeDebug" |
| val graziePlugin = "tanvd.grazi" |
| val psiViewerPlugin: String by project |
| val intelliLangPlugin = "org.intellij.intelliLang" |
| val copyrightPlugin = "com.intellij.copyright" |
| val javaPlugin = "com.intellij.java" |
| val javaIdePlugin = "com.intellij.java.ide" |
| val javaScriptPlugin = "JavaScript" |
| val clionPlugins = listOf("com.intellij.cidr.base", "com.intellij.clion", nativeDebugPlugin) |
| val mlCompletionPlugin = "com.intellij.completion.ml.ranking" |
| |
| val compileNativeCodeTaskName = "compileNativeCode" |
| |
| val grammarKitFakePsiDeps = "grammar-kit-fake-psi-deps" |
| |
| val basePluginArchiveName = "intellij-rust" |
| |
| plugins { |
| idea |
| kotlin("jvm") version "1.9.0" |
| id("org.jetbrains.intellij") version "1.16.1" |
| id("org.jetbrains.grammarkit") version "2022.3.1" |
| id("net.saliman.properties") version "1.5.2" |
| id("org.gradle.test-retry") version "1.5.3" |
| } |
| |
| idea { |
| module { |
| // https://github.com/gradle/kotlin-dsl/issues/537/ |
| excludeDirs = excludeDirs + file("testData") + file("deps") + file("bin") + |
| file("$grammarKitFakePsiDeps/src/main/kotlin") |
| } |
| } |
| |
| allprojects { |
| apply { |
| plugin("idea") |
| plugin("kotlin") |
| plugin("org.jetbrains.grammarkit") |
| plugin("org.jetbrains.intellij") |
| plugin("org.gradle.test-retry") |
| } |
| |
| repositories { |
| mavenCentral() |
| maven("https://cache-redirector.jetbrains.com/repo.maven.apache.org/maven2") |
| maven("https://cache-redirector.jetbrains.com/intellij-dependencies") |
| } |
| |
| idea { |
| module { |
| generatedSourceDirs.add(file("src/gen")) |
| } |
| } |
| |
| intellij { |
| version.set(baseVersion) |
| downloadSources.set(!isCI) |
| updateSinceUntilBuild.set(true) |
| instrumentCode.set(false) |
| ideaDependencyCachePath.set(dependencyCachePath) |
| sandboxDir.set(layout.buildDirectory.dir("$ideToRun-sandbox-$platformVersion").map { it.asFile.absolutePath }) |
| } |
| |
| configure<JavaPluginExtension> { |
| sourceCompatibility = VERSION_17 |
| targetCompatibility = VERSION_17 |
| } |
| |
| tasks { |
| withType<KotlinCompile> { |
| compilerOptions { |
| jvmTarget.set(JvmTarget.JVM_17) |
| languageVersion.set(KotlinVersion.DEFAULT) |
| // see https://plugins.jetbrains.com/docs/intellij/using-kotlin.html#kotlin-standard-library |
| apiVersion.set(KotlinVersion.KOTLIN_1_8) |
| freeCompilerArgs.set(listOf("-Xjvm-default=all")) |
| } |
| } |
| withType<PatchPluginXmlTask> { |
| sinceBuild.set(prop("sinceBuild")) |
| untilBuild.set(prop("untilBuild")) |
| } |
| |
| // All these tasks don't make sense for non-root subprojects |
| // Root project (i.e. `:plugin`) enables them itself if needed |
| runIde { enabled = false } |
| prepareSandbox { enabled = false } |
| buildSearchableOptions { enabled = false } |
| |
| test { |
| systemProperty("java.awt.headless", "true") |
| testLogging { |
| showStandardStreams = prop("showStandardStreams").toBoolean() |
| afterSuite( |
| KotlinClosure2<TestDescriptor, TestResult, Unit>({ desc, result -> |
| if (desc.parent == null) { // will match the outermost suite |
| val output = "Results: ${result.resultType} (${result.testCount} tests, ${result.successfulTestCount} passed, ${result.failedTestCount} failed, ${result.skippedTestCount} skipped)" |
| println(output) |
| } |
| }) |
| ) |
| } |
| if (isCI) { |
| retry { |
| maxRetries.set(3) |
| maxFailures.set(5) |
| } |
| } |
| } |
| |
| // It makes sense to copy native binaries only for root ("intellij-rust") and "plugin" projects because: |
| // - `intellij-rust` is supposed to provide all necessary functionality related to procedural macro expander. |
| // So the binaries are required for the corresponding tests. |
| // - `plugin` is root project to build plugin artifact and exactly its sandbox is included into the plugin artifact |
| if (project.name in listOf("intellij-rust", "plugin")) { |
| task<Exec>(compileNativeCodeTaskName) { |
| workingDir = rootDir.resolve("native-helper") |
| executable = "cargo" |
| // Hack to use unstable `--out-dir` option work for stable toolchain |
| // https://doc.rust-lang.org/cargo/commands/cargo-build.html#output-options |
| environment("RUSTC_BOOTSTRAP", "1") |
| |
| val hostPlatform = DefaultNativePlatform.host() |
| val archName = when (val archName = hostPlatform.architecture.name) { |
| "arm-v8", "aarch64" -> "arm64" |
| else -> archName |
| } |
| val outDir = "${rootDir}/bin/${hostPlatform.operatingSystem.toFamilyName()}/$archName" |
| args("build", "--release", "-Z", "unstable-options", "--out-dir", outDir) |
| |
| // It may be useful to disable compilation of native code. |
| // For example, CI builds native code for each platform in separate tasks and puts it into `bin` dir manually |
| // so there is no need to do it again. |
| enabled = prop("compileNativeCode").toBoolean() |
| } |
| } |
| } |
| |
| sourceSets { |
| main { |
| java.srcDirs("src/gen") |
| resources.srcDirs("src/$platformVersion/main/resources") |
| } |
| test { |
| resources.srcDirs("src/$platformVersion/test/resources") |
| } |
| } |
| kotlin { |
| sourceSets { |
| main { |
| kotlin.srcDirs("src/$platformVersion/main/kotlin") |
| } |
| test { |
| kotlin.srcDirs("src/$platformVersion/test/kotlin") |
| } |
| } |
| } |
| |
| val testOutput = configurations.create("testOutput") |
| |
| dependencies { |
| compileOnly(kotlin("stdlib-jdk8")) |
| testOutput(sourceSets.getByName("test").output.classesDirs) |
| } |
| |
| afterEvaluate { |
| tasks.withType<AbstractTestTask> { |
| testLogging { |
| if (hasProp("showTestStatus") && prop("showTestStatus").toBoolean()) { |
| events = setOf(TestLogEvent.STARTED, TestLogEvent.PASSED, TestLogEvent.SKIPPED, TestLogEvent.FAILED) |
| } |
| exceptionFormat = TestExceptionFormat.FULL |
| } |
| } |
| |
| tasks.withType<Test>().configureEach { |
| jvmArgs = listOf("-Xmx2g", "-XX:-OmitStackTraceInFastThrow") |
| |
| // We need to prevent the platform-specific shared JNA library to loading from the system library paths, |
| // because otherwise it can lead to compatibility issues. |
| // Also note that IDEA does the same thing at startup, and not only for tests. |
| systemProperty("jna.nosys", "true") |
| |
| // The factory should be set up automatically in `IdeaForkJoinWorkerThreadFactory.setupForkJoinCommonPool`, |
| // but when tests are launched by Gradle this may not happen because Gradle can use the pool earlier. |
| // Setting this factory is critical for `ReadMostlyRWLock` performance, so ensure it is properly set |
| systemProperty( |
| "java.util.concurrent.ForkJoinPool.common.threadFactory", |
| "com.intellij.concurrency.IdeaForkJoinWorkerThreadFactory" |
| ) |
| if (isTeamcity) { |
| // Make teamcity builds green if only muted tests fail |
| // https://youtrack.jetbrains.com/issue/TW-16784 |
| ignoreFailures = true |
| } |
| if (hasProp("excludeTests")) { |
| exclude(prop("excludeTests")) |
| } |
| } |
| } |
| } |
| |
| |
| val Project.dependencyCachePath |
| get(): String { |
| val cachePath = file("${rootProject.projectDir}/deps") |
| // If cache path doesn't exist, we need to create it manually |
| // because otherwise gradle-intellij-plugin will ignore it |
| if (!cachePath.exists()) { |
| cachePath.mkdirs() |
| } |
| return cachePath.absolutePath |
| } |
| |
| val pluginProjects: List<Project> |
| get() = rootProject.allprojects.filter { it.name != grammarKitFakePsiDeps } |
| |
| // Special module with run, build, and publish tasks |
| project(":plugin") { |
| val pluginVersion = System.getenv("BUILD_NUMBER") ?: "${platformVersion}.${prop("buildNumber")}" |
| version = if (pluginVersion.contains(".")) { |
| val split = pluginVersion.split(".").toMutableList() |
| split[0] = platformVersion.toString() |
| // From 232 branch, plugin version `232.9921` and `233.9921` were published |
| // These versions were based on IJ platform `232.9921` build |
| // |
| // From 233 branch, plugin versions are `233.8264` and `232.8264`, which are based on IJ platform `233.8264` build |
| // Since we publish versions for 2 platform versions from a single branch, plugin versions has to be hacked this way, |
| // Otherwise newly published `233.8264` version would be considered lower than `233.9921` published earlier |
| // |
| // TODO: For `241`, come up with new plugin versioning to avoid this problem |
| split[1] = (split[1].toIntOrNull()?.plus(10000))?.toString() ?: split[1] |
| split.joinToString(".") |
| } else { |
| pluginVersion |
| } |
| |
| intellij { |
| version.set(baseVersionForRun) |
| pluginName.set("intellij-rust") |
| val pluginList = mutableListOf( |
| tomlPlugin, |
| intelliLangPlugin, |
| graziePlugin, |
| psiViewerPlugin, |
| javaScriptPlugin, |
| mlCompletionPlugin |
| ) |
| if (ideToRun == "idea") { |
| pluginList += listOf( |
| copyrightPlugin, |
| javaPlugin, |
| nativeDebugPlugin |
| ) |
| } |
| plugins.set(pluginList) |
| } |
| |
| dependencies { |
| implementation(project(":")) |
| implementation(project(":idea")) |
| implementation(project(":clion")) |
| implementation(project(":debugger")) |
| implementation(project(":profiler")) |
| implementation(project(":copyright")) |
| implementation(project(":coverage")) |
| implementation(project(":intelliLang")) |
| implementation(project(":duplicates")) |
| implementation(project(":grazie")) |
| implementation(project(":js")) |
| implementation(project(":ml-completion")) |
| } |
| |
| // Collects all jars produced by compilation of project modules and merges them into singe one. |
| // We need to put all plugin manifest files into single jar to make new plugin model work |
| val mergePluginJarTask = task<Jar>("mergePluginJars") { |
| duplicatesStrategy = DuplicatesStrategy.FAIL |
| archiveBaseName.set(basePluginArchiveName) |
| |
| exclude("META-INF/MANIFEST.MF") |
| exclude("**/classpath.index") |
| |
| val pluginLibDir by lazy { |
| val sandboxTask = tasks.prepareSandbox.get() |
| sandboxTask.destinationDir.resolve("${sandboxTask.pluginName.get()}/lib") |
| } |
| |
| val pluginJars by lazy { |
| pluginLibDir.listFiles().orEmpty().filter { it.isPluginJar() } |
| } |
| |
| destinationDirectory.set(project.layout.dir(provider { pluginLibDir })) |
| |
| doFirst { |
| for (file in pluginJars) { |
| from(zipTree(file)) |
| } |
| } |
| |
| doLast { |
| delete(pluginJars) |
| } |
| } |
| |
| // Add plugin sources to the plugin ZIP. |
| // gradle-intellij-plugin will use it as a plugin sources if the plugin is used as a dependency |
| val createSourceJar = task<Jar>("createSourceJar") { |
| dependsOn(":generateLexer") |
| dependsOn(":generateParser") |
| dependsOn(":debugger:generateGrammarSource") |
| |
| for (prj in pluginProjects) { |
| from(prj.kotlin.sourceSets.main.get().kotlin) { |
| include("**/*.java") |
| include("**/*.kt") |
| } |
| } |
| |
| destinationDirectory.set(layout.buildDirectory.dir("libs")) |
| archiveBaseName.set(basePluginArchiveName) |
| archiveClassifier.set("src") |
| } |
| |
| tasks { |
| buildPlugin { |
| dependsOn(createSourceJar) |
| from(createSourceJar) { into("lib/src") } |
| // Set proper name for final plugin zip. |
| // Otherwise, base name is the same as gradle module name |
| archiveBaseName.set(basePluginArchiveName) |
| } |
| runIde { enabled = true } |
| prepareSandbox { |
| finalizedBy(mergePluginJarTask) |
| enabled = true |
| } |
| withType<RunIdeBase> { |
| // Force `mergePluginJarTask` be executed before any task based on `RunIdeBase` (for example, `runIde` or `buildSearchableOptions`). |
| // Otherwise, these tasks fail because of implicit dependency. |
| // Should be dropped when jar merging is implemented in `gradle-intellij-plugin` itself |
| dependsOn(mergePluginJarTask) |
| } |
| verifyPlugin { |
| dependsOn(mergePluginJarTask) |
| } |
| buildSearchableOptions { |
| enabled = prop("enableBuildSearchableOptions").toBoolean() |
| } |
| withType<PrepareSandboxTask> { |
| dependsOn(named(compileNativeCodeTaskName)) |
| |
| // Copy native binaries |
| from("${rootDir}/bin") { |
| into("${pluginName.get()}/bin") |
| include("**") |
| } |
| // Copy pretty printers |
| from("$rootDir/prettyPrinters") { |
| into("${pluginName.get()}/prettyPrinters") |
| include("**/*.py") |
| } |
| } |
| |
| withType<RunIdeTask> { |
| // Default args for IDEA installation |
| jvmArgs("-Xmx768m", "-XX:+UseG1GC", "-XX:SoftRefLRUPolicyMSPerMB=50") |
| // Disable plugin auto reloading. See `com.intellij.ide.plugins.DynamicPluginVfsListener` |
| jvmArgs("-Didea.auto.reload.plugins=false") |
| // Don't show "Tip of the Day" at startup |
| jvmArgs("-Dide.show.tips.on.startup.default.value=false") |
| // uncomment if `unexpected exception ProcessCanceledException` prevents you from debugging a running IDE |
| // jvmArgs("-Didea.ProcessCanceledException=disabled") |
| |
| // Uncomment to enable FUS testing mode |
| // jvmArgs("-Dfus.internal.test.mode=true") |
| |
| // Uncomment to enable localization testing mode |
| // jvmArgs("-Didea.l10n=true") |
| } |
| |
| withType<PatchPluginXmlTask> { |
| pluginDescription.set(provider { file("description.html").readText() }) |
| } |
| |
| withType<PublishPluginTask> { |
| token.set(prop("publishToken")) |
| channels.set(listOf(channel)) |
| } |
| } |
| |
| // Generates event scheme for Rust plugin FUS events to `plugin/build/eventScheme.json` |
| task<RunIdeTask>("buildEventsScheme") { |
| dependsOn(tasks.prepareSandbox) |
| args("buildEventsScheme", "--outputFile=${buildDir.resolve("eventScheme.json").absolutePath}", "--pluginId=org.rust.lang") |
| // BACKCOMPAT: 2023.2. Update value to 233 and this comment |
| // `IDEA_BUILD_NUMBER` variable is used by `buildEventsScheme` task to write `buildNumber` to output json. |
| // It will be used by TeamCity automation to set minimal IDE version for new events |
| environment("IDEA_BUILD_NUMBER", "232") |
| } |
| } |
| |
| project(":$grammarKitFakePsiDeps") |
| |
| project(":") { |
| intellij { |
| plugins.set(listOf(tomlPlugin)) |
| } |
| |
| sourceSets { |
| main { |
| if (channel == "nightly" || channel == "dev") { |
| resources.srcDirs("src/main/resources-nightly") |
| resources.srcDirs("src/$platformVersion/main/resources-nightly") |
| } else { |
| resources.srcDirs("src/main/resources-stable") |
| resources.srcDirs("src/$platformVersion/main/resources-stable") |
| } |
| } |
| } |
| |
| dependencies { |
| implementation("com.fasterxml.jackson.dataformat:jackson-dataformat-toml:2.14.2") { |
| exclude(module = "jackson-core") |
| exclude(module = "jackson-databind") |
| exclude(module = "jackson-annotations") |
| } |
| api("io.github.z4kn4fein:semver:1.4.2") { |
| excludeKotlinDeps() |
| } |
| implementation("org.eclipse.jgit:org.eclipse.jgit:6.5.0.202303070854-r") { |
| exclude("org.slf4j") |
| } |
| testImplementation("com.squareup.okhttp3:mockwebserver:4.11.0") |
| } |
| |
| tasks { |
| generateLexer { |
| sourceFile.set(file("src/main/grammars/RustLexer.flex")) |
| targetDir.set("src/gen/org/rust/lang/core/lexer") |
| targetClass.set("_RustLexer") |
| purgeOldFiles.set(true) |
| } |
| generateParser { |
| sourceFile.set(file("src/main/grammars/RustParser.bnf")) |
| targetRoot.set("src/gen") |
| pathToParser.set("org/rust/lang/core/parser/RustParser.java") |
| pathToPsiRoot.set("org/rust/lang/core/psi") |
| purgeOldFiles.set(true) |
| classpath(project(":$grammarKitFakePsiDeps").sourceSets.main.get().runtimeClasspath) |
| } |
| withType<KotlinCompile> { |
| dependsOn(generateLexer, generateParser) |
| } |
| |
| // In tests `resources` directory is used instead of `sandbox` |
| processTestResources { |
| dependsOn(named(compileNativeCodeTaskName)) |
| from("${rootDir}/bin") { |
| into("bin") |
| include("**") |
| } |
| } |
| } |
| |
| task("resolveDependencies") { |
| doLast { |
| rootProject.allprojects |
| .map { it.configurations } |
| .flatMap { it.filter { c -> c.isCanBeResolved } } |
| .forEach { it.resolve() } |
| } |
| } |
| } |
| |
| project(":idea") { |
| intellij { |
| version.set(ideaVersion) |
| plugins.set(listOf( |
| javaPlugin, |
| // this plugin registers `com.intellij.ide.projectView.impl.ProjectViewPane` for IDEA that we use in tests |
| javaIdePlugin |
| )) |
| } |
| dependencies { |
| implementation(project(":")) |
| testImplementation(project(":", "testOutput")) |
| } |
| } |
| |
| project(":clion") { |
| intellij { |
| version.set(clionVersion) |
| plugins.set(clionPlugins) |
| } |
| dependencies { |
| implementation(project(":")) |
| implementation(project(":debugger")) |
| testImplementation(project(":", "testOutput")) |
| } |
| } |
| |
| project(":debugger") { |
| apply { |
| plugin("antlr") |
| } |
| intellij { |
| if (baseIDE == "idea") { |
| plugins.set(listOf(nativeDebugPlugin)) |
| } else { |
| version.set(clionVersion) |
| plugins.set(clionPlugins) |
| } |
| } |
| |
| // Kotlin Gradle support doesn't generate proper extensions if the plugin is not declared in `plugin` block. |
| // But if we do it, `antlr` plugin will be applied to root project as well that we want to avoid. |
| // So, let's define all necessary things manually |
| val antlr by configurations |
| val generateGrammarSource: AntlrTask by tasks |
| val generateTestGrammarSource: AntlrTask by tasks |
| |
| dependencies { |
| implementation(project(":")) |
| antlr("org.antlr:antlr4:4.13.0") |
| implementation("org.antlr:antlr4-runtime:4.13.0") |
| testImplementation(project(":", "testOutput")) |
| } |
| tasks { |
| compileKotlin { |
| dependsOn(generateGrammarSource) |
| } |
| compileTestKotlin { |
| dependsOn(generateTestGrammarSource) |
| } |
| |
| generateGrammarSource { |
| arguments.add("-no-listener") |
| arguments.add("-visitor") |
| outputDirectory = file("src/gen/org/rust/debugger/lang") |
| } |
| } |
| // Exclude antlr4 from transitive dependencies of `:debugger:api` configuration (https://github.com/gradle/gradle/issues/820) |
| configurations.api { |
| setExtendsFrom(extendsFrom.filter { it.name != "antlr" }) |
| } |
| } |
| |
| project(":profiler") { |
| intellij { |
| version.set(clionVersion) |
| plugins.set(clionPlugins) |
| } |
| dependencies { |
| implementation(project(":")) |
| testImplementation(project(":", "testOutput")) |
| } |
| } |
| |
| project(":intelliLang") { |
| intellij { |
| plugins.set(listOf(intelliLangPlugin)) |
| } |
| dependencies { |
| implementation(project(":")) |
| testImplementation(project(":", "testOutput")) |
| } |
| } |
| |
| project(":copyright") { |
| intellij { |
| version.set(ideaVersion) |
| plugins.set(listOf(copyrightPlugin)) |
| } |
| dependencies { |
| implementation(project(":")) |
| testImplementation(project(":", "testOutput")) |
| } |
| } |
| |
| project(":duplicates") { |
| dependencies { |
| implementation(project(":")) |
| testImplementation(project(":", "testOutput")) |
| } |
| } |
| |
| project(":coverage") { |
| dependencies { |
| implementation(project(":")) |
| testImplementation(project(":", "testOutput")) |
| } |
| } |
| |
| project(":grazie") { |
| intellij { |
| plugins.set(listOf(graziePlugin)) |
| } |
| dependencies { |
| implementation(project(":")) |
| testImplementation(project(":", "testOutput")) |
| } |
| } |
| |
| project(":js") { |
| intellij { |
| plugins.set(listOf(javaScriptPlugin)) |
| } |
| dependencies { |
| implementation(project(":")) |
| testImplementation(project(":", "testOutput")) |
| } |
| } |
| |
| project(":ml-completion") { |
| intellij { |
| plugins.set(listOf(mlCompletionPlugin)) |
| } |
| dependencies { |
| implementation("org.jetbrains.intellij.deps.completion:completion-ranking-rust:0.4.1") |
| implementation(project(":")) |
| testImplementation(project(":", "testOutput")) |
| } |
| } |
| |
| task("updateCargoOptions") { |
| doLast { |
| val file = File("src/main/kotlin/org/rust/cargo/util/CargoOptions.kt") |
| file.bufferedWriter().use { |
| it.writeln(""" |
| /* |
| * Use of this source code is governed by the MIT license that can be |
| * found in the LICENSE file. |
| */ |
| |
| package org.rust.cargo.util |
| |
| data class CargoOption(val name: String, val description: String) { |
| val longName: String get() = "--${'$'}name" |
| } |
| |
| """.trimIndent()) |
| it.writeCargoOptions("https://doc.rust-lang.org/cargo/commands") |
| } |
| } |
| } |
| |
| |
| fun Writer.writeCargoOptions(baseUrl: String) { |
| |
| data class CargoOption( |
| val name: String, |
| val description: String |
| ) |
| |
| data class CargoCommand( |
| val name: String, |
| val description: String, |
| val options: List<CargoOption> |
| ) |
| |
| fun fetchCommand(commandUrl: String): CargoCommand { |
| val document = Jsoup.connect("$baseUrl/$commandUrl").get() |
| |
| val fullCommandDesc = document.select("div[class=sectionbody] > p").text() |
| val parts = fullCommandDesc.split(" - ", limit = 2) |
| check(parts.size == 2) { "Invalid page format: $baseUrl/$commandUrl$" } |
| val commandName = parts.first().removePrefix("cargo-") |
| val commandDesc = parts.last() |
| |
| val options = document |
| .select("dt > strong:matches(^--)") |
| .map { option -> |
| val optionName = option.text().removePrefix("--") |
| val nextSiblings = generateSequence(option.parent()) { it.nextElementSibling() } |
| val descElement = nextSiblings.first { it.tagName() == "dd" } |
| val fullOptionDesc = descElement.select("p").text() |
| val optionDesc = fullOptionDesc.substringBefore(". ").removeSuffix(".") |
| CargoOption(optionName, optionDesc) |
| } |
| |
| return CargoCommand(commandName, commandDesc, options) |
| } |
| |
| fun fetchCommands(): List<CargoCommand> { |
| val document = Jsoup.connect("$baseUrl/cargo.html").get() |
| val urls = document.select("dt > a[href]").map { it.attr("href") } |
| return urls.map { fetchCommand(it) } |
| } |
| |
| fun writeEnumVariant(command: CargoCommand, isLast: Boolean) { |
| val variantName = command.name.toUpperCase().replace('-', '_') |
| val renderedOptions = command.options.joinToString( |
| separator = ",\n ", |
| prefix = "\n ", |
| postfix = "\n " |
| ) { "CargoOption(\"${it.name}\", \"\"\"${it.description}\"\"\")" } |
| |
| writeln(""" |
| | $variantName( |
| | description = "${command.description}", |
| | options = ${if (command.options.isEmpty()) "emptyList()" else "listOf($renderedOptions)"} |
| | )${if (isLast) ";" else ","} |
| """.trimMargin()) |
| writeln() |
| } |
| |
| val commands = fetchCommands() |
| writeln("enum class CargoCommands(val description: String, val options: List<CargoOption>) {") |
| for ((index, command) in commands.withIndex()) { |
| writeEnumVariant(command, isLast = index == commands.size - 1) |
| } |
| writeln(" val presentableName: String get() = name.toLowerCase().replace('_', '-')") |
| writeln("}") |
| } |
| |
| fun Writer.writeln(str: String = "") { |
| write(str) |
| write("\n") |
| } |
| |
| fun hasProp(name: String): Boolean = extra.has(name) |
| |
| fun prop(name: String): String = |
| extra.properties[name] as? String |
| ?: error("Property `$name` is not defined in gradle.properties") |
| |
| fun versionForIde(ideName: String): String = when (ideName) { |
| "idea" -> ideaVersion |
| "clion" -> clionVersion |
| "rustRover" -> rustRoverVersion |
| else -> error("Unexpected IDE name: `$baseIDE`") |
| } |
| |
| inline operator fun <T : Task> T.invoke(a: T.() -> Unit): T = apply(a) |
| |
| fun String.execute(wd: String? = null, ignoreExitCode: Boolean = false, print: Boolean = true): String = |
| split(" ").execute(wd, ignoreExitCode, print) |
| |
| fun List<String>.execute(wd: String? = null, ignoreExitCode: Boolean = false, print: Boolean = true): String { |
| val process = ProcessBuilder(this) |
| .also { pb -> wd?.let { pb.directory(File(it)) } } |
| .start() |
| var result = "" |
| val errReader = thread { process.errorStream.bufferedReader().forEachLine { println(it) } } |
| val outReader = thread { |
| process.inputStream.bufferedReader().forEachLine { line -> |
| if (print) { |
| println(line) |
| } |
| result += line |
| } |
| } |
| process.waitFor() |
| outReader.join() |
| errReader.join() |
| if (process.exitValue() != 0 && !ignoreExitCode) error("Non-zero exit status for `$this`") |
| return result |
| } |
| |
| fun File.isPluginJar(): Boolean { |
| if (!isFile) return false |
| if (extension != "jar") return false |
| return zipTree(this).files.any { it.isManifestFile() } |
| } |
| |
| fun File.isManifestFile(): Boolean { |
| if (extension != "xml") return false |
| val rootNode = try { |
| val parser = XmlParser() |
| parser.parse(this) |
| } catch (e: Exception) { |
| logger.error("Failed to parse $path", e) |
| return false |
| } |
| return rootNode.name() == "idea-plugin" |
| } |
| |
| fun <T : ModuleDependency> T.excludeKotlinDeps() { |
| exclude(module = "kotlin-reflect") |
| exclude(module = "kotlin-runtime") |
| exclude(module = "kotlin-stdlib") |
| exclude(module = "kotlin-stdlib-common") |
| exclude(module = "kotlin-stdlib-jdk8") |
| exclude(module = "kotlinx-serialization-core") |
| } |