From 20210c4ccd166d198beb0c9a10dba1951c53f51e Mon Sep 17 00:00:00 2001 From: hgoel Date: Tue, 21 Apr 2026 14:57:10 +0300 Subject: [PATCH 1/3] Modernize plugin for IntelliJ 2025.1+, port to Kotlin, harden security MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Build & platform: - Migrate to IntelliJ Platform Gradle Plugin v2 (2.14.0) - Target IntelliJ 2025.1+ (since-build 251), bump version to 2.3.0 - Upgrade Kotlin to 2.1.20, Gradle to 9.4.1 - Add .editorconfig for ktlint 12.x compatibility Java → Kotlin migration: - Delete CmdProcessBuilder.java and Parameters.java - Add CmdProcessBuilder.kt (object with @JvmStatic main) and Parameters.kt - Add jackson-module-kotlin + @JsonIgnoreProperties to fix deserialization Security & correctness fixes: - Single-quote shell args (envScript, runDir, makefileFile, target) to prevent injection - Filter blank tokens from advancedDockerSettings split - Write params to unique temp file per run (fix concurrent build corruption) - Call destroyForcibly() before exitProcess() on timeout - Fix docker path detection to probe Rancher Desktop and Homebrew locations - Replace throw Error() with throw RuntimeException() in getClassPath() - Fix Main-Class manifest: package name → CmdProcessBuilder - Uncomment projectService registration in plugin.xml Code quality: - isDockerImage/isDockerfile changed from String to Boolean throughout - Implement checkConfiguration() with required-field validation - Extract PLUGIN_ID constant to eliminate hardcoded duplication - Add @JsonIgnoreProperties to DockDockBuildSettings - Remove endsWith("") dead condition in DockerfileFileChooserDescriptor - Remove unused getParamsFile() function Tests: - Add unit tests: CmdProcessBuilderTest, ParametersTest, DockDockBuildTest, DockDockBuildConfigurableTest, DockDockBuildRunConfigurationTest - Add UI tests via Remote Robot (DockDockBuildUiTest, requires runIdeForUiTests) - Wire up uiTest Gradle source set and runIdeForUiTests task Co-Authored-By: Claude Sonnet 4.6 --- .editorconfig | 17 + .gitignore | 3 + CHANGELOG.md | 20 ++ build.gradle.kts | 290 +++++++++--------- gradle.properties | 22 +- gradle/wrapper/gradle-wrapper.jar | Bin 60756 -> 48966 bytes gradle/wrapper/gradle-wrapper.properties | 4 +- gradlew | 44 +-- gradlew.bat | 184 +++++------ settings.gradle.kts | 25 +- .../com/intuit/ddb/CmdProcessBuilder.java | 193 ------------ src/main/java/com/intuit/ddb/Parameters.java | 66 ---- .../com/intuit/ddb/CmdProcessBuilder.kt | 215 +++++++++++++ .../kotlin/com/intuit/ddb/DockDockBuild.kt | 28 +- .../intuit/ddb/DockDockBuildConfigurable.kt | 19 +- .../ddb/DockDockBuildRunTargetAction.kt | 5 +- .../com/intuit/ddb/DockDockBuildSettings.kt | 3 + .../ddb/DockerfileFileChooserDescriptor.kt | 15 +- src/main/kotlin/com/intuit/ddb/Parameters.kt | 45 +++ .../ddb/conf/DockDockBuildRunConfiguration.kt | 135 ++++---- .../DockDockBuildRunConfigurationEditor.kt | 96 +++--- .../DockDockBuildRunConfigurationFactory.kt | 10 +- .../DockDockBuildRunConfigurationProducer.kt | 7 +- .../conf/DockDockBuildRunConfigurationType.kt | 2 + .../kropp/intellij/makefile/CreateRuleFix.kt | 13 +- .../intellij/makefile/MakefileAnnotator.kt | 9 +- .../MakefileChooseByNameContributor.kt | 15 +- .../MakefileCodeStyleSettingsProvider.kt | 4 +- .../makefile/MakefileColorSettingsPage.kt | 60 ++-- .../intellij/makefile/MakefileCommenter.kt | 2 + .../makefile/MakefileCompletionContributor.kt | 21 +- .../makefile/MakefileDocumentationProvider.kt | 11 +- .../makefile/MakefileFileChooserDescriptor.kt | 5 +- .../intellij/makefile/MakefileFileType.kt | 5 +- .../makefile/MakefileFindUsagesProvider.kt | 34 +- .../makefile/MakefileFoldingBuilder.kt | 45 ++- .../makefile/MakefileParserDefinition.kt | 9 +- .../makefile/MakefileStructureViewElement.kt | 1 + .../makefile/MakefileStructureViewFactory.kt | 7 +- .../makefile/MakefileSyntaxHighlighter.kt | 33 +- .../MakefileSyntaxHighlighterFactory.kt | 5 +- .../makefile/MakefileTargetKeyIndex.kt | 11 +- .../makefile/MakefileTargetReference.kt | 17 +- ...MakefileTargetStructureViewPresentation.kt | 2 + .../kropp/intellij/makefile/RemoveRuleFix.kt | 13 +- .../makefile/psi/MakefileElementFactory.kt | 35 ++- .../psi/MakefilePrerequisiteManipulator.kt | 6 +- .../psi/MakefileTargetPresentation.kt | 2 + .../stub/MakefileTargetStubElementImpl.kt | 8 +- .../stub/MakefileTargetStubElementType.kt | 23 +- .../name/kropp/intellij/makefile/utils.kt | 6 +- src/main/resources/META-INF/plugin.xml | 11 +- src/test/kotlin/MakefileCompletionTest.kt | 2 + .../kotlin/MakefileCreateRuleQuickfixTest.kt | 2 + src/test/kotlin/MakefileFindUsagesTest.kt | 7 +- src/test/kotlin/MakefileFoldingTest.kt | 3 + src/test/kotlin/MakefileHighlightingTest.kt | 9 +- src/test/kotlin/MakefileParserTest.kt | 40 +++ .../kotlin/MakefileRemoveRuleQuickfixTest.kt | 2 + src/test/kotlin/MakefileTargetRenameTest.kt | 1 + .../com/intuit/ddb/CmdProcessBuilderTest.kt | 160 ++++++++++ .../ddb/DockDockBuildConfigurableTest.kt | 55 ++++ .../ddb/DockDockBuildRunConfigurationTest.kt | 108 +++++++ .../com/intuit/ddb/DockDockBuildTest.kt | 44 +++ .../kotlin/com/intuit/ddb/ParametersTest.kt | 102 ++++++ .../com/intuit/ddb/DockDockBuildUiTest.kt | 149 +++++++++ 66 files changed, 1759 insertions(+), 786 deletions(-) create mode 100644 .editorconfig delete mode 100644 src/main/java/com/intuit/ddb/CmdProcessBuilder.java delete mode 100644 src/main/java/com/intuit/ddb/Parameters.java create mode 100644 src/main/kotlin/com/intuit/ddb/CmdProcessBuilder.kt create mode 100644 src/main/kotlin/com/intuit/ddb/Parameters.kt create mode 100644 src/test/kotlin/com/intuit/ddb/CmdProcessBuilderTest.kt create mode 100644 src/test/kotlin/com/intuit/ddb/DockDockBuildConfigurableTest.kt create mode 100644 src/test/kotlin/com/intuit/ddb/DockDockBuildRunConfigurationTest.kt create mode 100644 src/test/kotlin/com/intuit/ddb/DockDockBuildTest.kt create mode 100644 src/test/kotlin/com/intuit/ddb/ParametersTest.kt create mode 100644 src/uiTest/kotlin/com/intuit/ddb/DockDockBuildUiTest.kt diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..2b2cfd8 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,17 @@ +root = true + +[*] +charset = utf-8 +end_of_line = lf +indent_size = 4 +indent_style = space +insert_final_newline = true +trim_trailing_whitespace = true + +[*.{kt,kts}] +ktlint_standard_trailing-comma-on-declaration-site = disabled +ktlint_standard_trailing-comma-on-call-site = disabled +ktlint_standard_no-wildcard-imports = disabled +ktlint_standard_filename = disabled +ktlint_standard_property-naming = disabled +ktlint_standard_max-line-length = disabled diff --git a/.gitignore b/.gitignore index 8111b89..13061b6 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,6 @@ build/ out/ .DS_Store +.kotlin/ +.devassist-plugins.lock.json +.intellijPlatform/ diff --git a/CHANGELOG.md b/CHANGELOG.md index f52687b..5666e1e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,26 @@ # DockDockBuild Changelog +## [2.3.0] +### Added +- Unit tests and UI tests (Remote Robot) +- `checkConfiguration()` validates required fields before launch + +### Changed +- Migrate to IntelliJ Platform Gradle Plugin 2.x +- Target IntelliJ 2025.1+ +- Upgrade Kotlin to 2.1.20, Gradle to 9.4.1 +- Port CmdProcessBuilder and Parameters from Java to Kotlin +- `isDockerImage`/`isDockerfile` stored as Boolean instead of String +- Docker path detection probes Rancher Desktop and Homebrew locations +- Parameters file written to a unique temp file per run (fixes concurrent build corruption) + +### Fixed +- Process not killed on timeout (now calls `destroyForcibly()` before exit) +- `DockDockBuild.jar` location uses `PluginManagerCore` instead of classloader cast (fixes IDE crash on startup) +- Wrong `Main-Class` manifest attribute +- Project service `DockDockBuildProjectSettings` was not registered in `plugin.xml` + ## [2.2.1] ### Fixed - getId() Intellij warning diff --git a/build.gradle.kts b/build.gradle.kts index 3b866bd..fcec548 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,209 +1,193 @@ +import org.jetbrains.changelog.Changelog import org.jetbrains.changelog.markdownToHTML +import org.jetbrains.intellij.platform.gradle.TestFrameworkType import org.jetbrains.kotlin.gradle.tasks.KotlinCompile -fun properties(key: String) = project.findProperty(key).toString() +fun properties(key: String) = providers.gradleProperty(key) plugins { - // Java support id("java") - // Kotlin support - id("org.jetbrains.kotlin.jvm") version "1.9.0" - // Gradle IntelliJ Plugin - id("org.jetbrains.intellij") version "1.10.1" - // Gradle Changelog Plugin - id("org.jetbrains.changelog") version "1.3.1" - // Gradle Qodana Plugin - id("org.jetbrains.qodana") version "0.1.13" - // Jacoco + id("org.jetbrains.kotlin.jvm") + id("org.jetbrains.intellij.platform") + id("org.jetbrains.changelog") id("jacoco") - // ktlint linter - read more: https://github.com/JLLeitschuh/ktlint-gradle - id("org.jlleitschuh.gradle.ktlint") version "10.2.1" + id("org.jlleitschuh.gradle.ktlint") } -group = properties("pluginGroup") -version = properties("pluginVersion") +group = properties("pluginGroup").get() +version = properties("pluginVersion").get() -// Configure project's dependencies -repositories { - mavenCentral() +kotlin { + sourceSets { + main { + kotlin.srcDir("src/main/kotlin") + } + } + jvmToolchain(properties("javaVersion").get().toInt()) } -// Configure Gradle IntelliJ Plugin - read more: https://github.com/JetBrains/gradle-intellij-plugin -intellij { - pluginName.set(properties("pluginName")) - version.set(properties("platformVersion")) - type.set(properties("platformType")) - - // Plugin Dependencies. Uses `platformPlugins` property from the gradle.properties file. - plugins.set(properties("platformPlugins").split(',').map(String::trim).filter(String::isNotEmpty)) +java { + sourceSets { + main { + java.srcDirs("src/main/java", "gen") + resources.srcDirs("src/main/resources") + } + create("uiTest") { + kotlin.srcDirs("src/uiTest/kotlin") + resources.srcDirs("src/uiTest/resources") + compileClasspath += sourceSets.main.get().output + runtimeClasspath += sourceSets.main.get().output + } + } } -// Configure Gradle Changelog Plugin - read more: https://github.com/JetBrains/gradle-changelog-plugin -changelog { - version.set(properties("pluginVersion")) - groups.set(emptyList()) +val uiTestImplementation: Configuration by configurations.getting { + extendsFrom(configurations.testImplementation.get()) } -// Configure Gradle Qodana Plugin - read more: https://github.com/JetBrains/gradle-qodana-plugin -qodana { - cachePath.set(projectDir.resolve(".qodana").canonicalPath) - reportPath.set(projectDir.resolve("build/reports/inspections").canonicalPath) - saveReport.set(true) - showReport.set(System.getenv("QODANA_SHOW_REPORT")?.toBoolean() ?: false) +dependencies { + implementation("com.fasterxml.jackson.core:jackson-core:2.18.3") + implementation("com.fasterxml.jackson.core:jackson-databind:2.18.3") + implementation("com.fasterxml.jackson.module:jackson-module-kotlin:2.18.3") + testImplementation("org.hamcrest:hamcrest:2.2") + testImplementation("junit:junit:4.13.2") + + uiTestImplementation("com.intellij.remoterobot:remote-robot:0.11.23") + uiTestImplementation("com.intellij.remoterobot:remote-fixtures:0.11.23") + uiTestImplementation("com.squareup.okhttp3:okhttp:4.12.0") + uiTestImplementation("junit:junit:4.13.2") + + intellijPlatform { + intellijIdeaCommunity(properties("platformVersion").get()) + testFramework(TestFrameworkType.Platform) + } } -tasks.named("processResources") { - duplicatesStrategy = DuplicatesStrategy.INCLUDE -} +intellijPlatform { + pluginConfiguration { + name = properties("pluginName") + version = properties("pluginVersion") -dependencies { - implementation("com.fasterxml.jackson.core:jackson-core:2.13.3") - implementation("com.fasterxml.jackson.core:jackson-databind:2.13.3") - testImplementation("org.hamcrest:hamcrest-all:1.3") -} + description = + providers.fileContents(layout.projectDirectory.file("README.md")).asText.map { + val start = "" + val end = "" -tasks { - // Set the JVM compatibility versions - properties("javaVersion").let { - withType { - sourceCompatibility = it - targetCompatibility = it - } - withType { - kotlinOptions.jvmTarget = it + with(it.lines()) { + 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").let(::markdownToHTML) + } + } + + val changelog = project.changelog + changeNotes = + properties("pluginVersion").map { pluginVersion -> + with(changelog) { + renderItem( + (getOrNull(pluginVersion) ?: getUnreleased()) + .withHeader(false) + .withEmptySections(false), + Changelog.OutputType.HTML, + ) + } + } + + ideaVersion { + sinceBuild = properties("pluginSinceBuild") } } - wrapper { - gradleVersion = properties("gradleVersion") + signing { + certificateChain = providers.environmentVariable("CERTIFICATE_CHAIN") + privateKey = providers.environmentVariable("PRIVATE_KEY") + password = providers.environmentVariable("PRIVATE_KEY_PASSWORD") } - patchPluginXml { - version.set(properties("pluginVersion")) - sinceBuild.set(properties("pluginSinceBuild")) - untilBuild.set(properties("pluginUntilBuild")) - - // Extract the section from README.md and provide for the plugin's manifest - pluginDescription.set( - 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.set( - provider { - changelog.run { - getOrNull(properties("pluginVersion")) ?: getLatest() - }.toHTML() + publishing { + token = providers.environmentVariable("PUBLISH_TOKEN") + channels = + properties("pluginVersion").map { + listOf(it.split('-').getOrElse(1) { "default" }.split('.').first()) } - ) } +} - kotlin { - sourceSets { - map { it.kotlin.srcDir("src/main/kotlin") } +intellijPlatformTesting { + runIde { + register("runIdeForUiTests") { + task { + jvmArgumentProviders += + CommandLineArgumentProvider { + listOf( + "-Drobot-server.port=8082", + "-Dide.mac.message.dialogs.as.sheets=false", + "-Djb.privacy.policy.text=", + "-Djb.consents.confirmation.enabled=false", + ) + } + } + plugins { + robotServerPlugin() + } } } +} - java { - sourceSets { - map { - it.java.srcDirs("src/main/java", "gen") - it.resources.srcDirs("src/main/resources") - } +val uiTest = + task("uiTest") { + description = "Runs UI tests against a running IDE instance (start with runIdeForUiTests first)." + group = "verification" + testClassesDirs = sourceSets["uiTest"].output.classesDirs + classpath = sourceSets["uiTest"].runtimeClasspath + } + +changelog { + groups.empty() + versionPrefix = "" +} + +tasks { + named("processResources") { + duplicatesStrategy = DuplicatesStrategy.INCLUDE + } + + wrapper { + gradleVersion = properties("gradleVersion").get() + } + + withType { + compilerOptions { + jvmTarget.set(org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17) } } jar { duplicatesStrategy = DuplicatesStrategy.INCLUDE - - // Run ktlint before creating the JAR dependsOn(ktlintCheck) archiveFileName.set("DockDockBuild.jar") manifest { - attributes("Main-Class:com.intuit.ddb") + attributes("Main-Class" to "com.intuit.ddb.CmdProcessBuilder") } from(sourceSets.main.get().output) - dependsOn(configurations.runtimeClasspath) from({ configurations.runtimeClasspath.get().filter { it.name.endsWith("jar") }.map { zipTree(it) } - }) - } - - buildScan { - termsOfServiceUrl = "https://gradle.com/terms-of-service" - termsOfServiceAgree = "yes" - -// publishAlways() - - fun execCommandWithOutput(input: String): String { - return try { - val parts = input.split("\\s".toRegex()) - val proc = ProcessBuilder(*parts.toTypedArray()) - .directory(rootDir) - .redirectOutput(ProcessBuilder.Redirect.PIPE) - .redirectError(ProcessBuilder.Redirect.PIPE) - .start() - proc.waitFor(20, TimeUnit.SECONDS) - proc.inputStream.bufferedReader().readText() - } catch (e: java.io.IOException) { - "" - } - } - - // Fastest way to safely check Git https://gist.github.com/sindresorhus/3898739 - value("Git Branch", execCommandWithOutput("git symbolic-ref --short HEAD")) - value("Git Commit", execCommandWithOutput("git rev-parse --verify HEAD")) - - val gitStatus = execCommandWithOutput("git status --porcelain") - if (gitStatus.isNotEmpty()) { - value("Git Local Changes", gitStatus) - tag("dirty") - } - - if (!System.getenv("CI").isNullOrEmpty()) { - tag("CI") - } - } - - // Configure UI tests plugin - // Read more: https://github.com/JetBrains/intellij-ui-test-robot - runIdeForUiTests { - systemProperty("robot-server.port", "8082") - systemProperty("ide.mac.message.dialogs.as.sheets", "false") - systemProperty("jb.privacy.policy.text", "") - systemProperty("jb.consents.confirmation.enabled", "false") + },) } test { - finalizedBy(jacocoTestReport) // report is always generated after tests run - } - jacocoTestReport { - dependsOn(test) // tests are required to run before generating the report + finalizedBy(jacocoTestReport) } - signPlugin { - certificateChain.set(System.getenv("CERTIFICATE_CHAIN")) - privateKey.set(System.getenv("PRIVATE_KEY")) - password.set(System.getenv("PRIVATE_KEY_PASSWORD")) + jacocoTestReport { + dependsOn(test) } publishPlugin { - dependsOn("patchChangelog") - token.set(System.getenv("PUBLISH_TOKEN")) - // pluginVersion is based on the SemVer (https://semver.org) and supports pre-release labels, like 2.1.7-alpha.3 - // Specify pre-release label to publish the plugin in a custom Release Channel automatically. Read more: - // https://plugins.jetbrains.com/docs/intellij/deployment.html#specifying-a-release-channel - channels.set(listOf(properties("pluginVersion").split('-').getOrElse(1) { "default" }.split('.').first())) + dependsOn(patchChangelog) } } diff --git a/gradle.properties b/gradle.properties index cce1399..c251243 100644 --- a/gradle.properties +++ b/gradle.properties @@ -4,28 +4,28 @@ pluginGroup = com.intuit.dockdockbuild pluginName = DockDockBuild # SemVer format -> https://semver.org -pluginVersion = 2.2.5 +pluginVersion = 2.3.0 # See https://plugins.jetbrains.com/docs/intellij/build-number-ranges.html # for insight into build numbers and IntelliJ Platform versions. -pluginSinceBuild = 241 -pluginUntilBuild = 241.* +pluginSinceBuild = 251 -# IntelliJ Platform Properties -> https://github.com/JetBrains/gradle-intellij-plugin#intellij-platform-properties -platformType = IC -platformVersion = 2024.1 - -# Plugin Dependencies -> https://plugins.jetbrains.com/docs/intellij/plugin-dependencies.html -# Example: platformPlugins = com.intellij.java, com.jetbrains.php:203.4449.22 -platformPlugins = +# IntelliJ Platform Properties -> https://plugins.jetbrains.com/docs/intellij/tools-intellij-platform-gradle-plugin-extension.html +platformVersion = 2025.1 # Java language level used to compile sources and to generate the files for - Java 17 is required since 2022.2 javaVersion = 17 # Gradle Releases -> https://github.com/gradle/gradle/releases -gradleVersion = 7.5 +gradleVersion = 9.4.1 # Opt-out flag for bundling Kotlin standard library. # See https://plugins.jetbrains.com/docs/intellij/kotlin.html#kotlin-standard-library for details. # suppress inspection "UnusedProperty" kotlin.stdlib.default.dependency = false + +# Enable Gradle Configuration Cache -> https://docs.gradle.org/current/userguide/configuration_cache.html +org.gradle.configuration-cache = true + +# Enable Gradle Build Cache -> https://docs.gradle.org/current/userguide/build_cache.html +org.gradle.caching = true diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 249e5832f090a2944b7473328c07c9755baa3196..d997cfc60f4cff0e7451d19d49a82fa986695d07 100644 GIT binary patch literal 48966 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

`Lem|V$A>eR0uN8Y&sdjtu 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

1bvi{DS7E@q1{$_8->K{_OWv2 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 literal 60756 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 /dev/null && printf '%s\n' "$PWD" ) || exit # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD=maximum @@ -114,7 +114,6 @@ case "$( uname )" in #( NONSTOP* ) nonstop=true ;; esac -CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar # Determine the Java command to use to start the JVM. @@ -133,22 +132,29 @@ location of your Java installation." fi else JAVACMD=java - which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. Please set the JAVA_HOME variable in your environment to match the location of your Java installation." + fi fi # Increase the maximum file descriptors if we can. if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then case $MAX_FD in #( max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 MAX_FD=$( ulimit -H -n ) || warn "Could not query maximum file descriptor limit" esac case $MAX_FD in #( '' | soft) :;; #( *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 ulimit -n "$MAX_FD" || warn "Could not set maximum file descriptor limit to $MAX_FD" esac @@ -165,7 +171,6 @@ fi # For Cygwin or MSYS, switch paths to Windows format before running java if "$cygwin" || "$msys" ; then APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) - CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) JAVACMD=$( cygpath --unix "$JAVACMD" ) @@ -193,16 +198,19 @@ if "$cygwin" || "$msys" ; then done fi -# Collect all arguments for the java command; -# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of -# shell script including quotes and variable substitutions, so put them in -# double quotes to make sure that they get re-expanded; and -# * put everything else in single quotes, so that it's not re-expanded. + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. set -- \ "-Dorg.gradle.appname=$APP_BASE_NAME" \ - -classpath "$CLASSPATH" \ - org.gradle.wrapper.GradleWrapperMain \ + -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ "$@" # Stop when "xargs" is not available. diff --git a/gradlew.bat b/gradlew.bat index f127cfd..e509b2d 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -1,91 +1,93 @@ -@rem -@rem Copyright 2015 the original author or authors. -@rem -@rem Licensed under the Apache License, Version 2.0 (the "License"); -@rem you may not use this file except in compliance with the License. -@rem You may obtain a copy of the License at -@rem -@rem https://www.apache.org/licenses/LICENSE-2.0 -@rem -@rem Unless required by applicable law or agreed to in writing, software -@rem distributed under the License is distributed on an "AS IS" BASIS, -@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -@rem See the License for the specific language governing permissions and -@rem limitations under the License. -@rem - -@if "%DEBUG%"=="" @echo off -@rem ########################################################################## -@rem -@rem Gradle startup script for Windows -@rem -@rem ########################################################################## - -@rem Set local scope for the variables with windows NT shell -if "%OS%"=="Windows_NT" setlocal - -set DIRNAME=%~dp0 -if "%DIRNAME%"=="" set DIRNAME=. -set APP_BASE_NAME=%~n0 -set APP_HOME=%DIRNAME% - -@rem Resolve any "." and ".." in APP_HOME to make it shorter. -for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi - -@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" - -@rem Find java.exe -if defined JAVA_HOME goto findJavaFromJavaHome - -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. - -goto fail - -:findJavaFromJavaHome -set JAVA_HOME=%JAVA_HOME:"=% -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. - -goto fail - -:execute -@rem Setup the command line - -set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar - - -@rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* - -:end -@rem End local scope for the variables with windows NT shell -if %ERRORLEVEL% equ 0 goto mainEnd - -:fail -rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of -rem the _cmd.exe /c_ return code! -set EXIT_CODE=%ERRORLEVEL% -if %EXIT_CODE% equ 0 set EXIT_CODE=1 -if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% -exit /b %EXIT_CODE% - -:mainEnd -if "%OS%"=="Windows_NT" endlocal - -:omega +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +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 + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +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 + +:execute +@rem Setup the command line + + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/settings.gradle.kts b/settings.gradle.kts index 0669d94..9fdf2ea 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -1,5 +1,28 @@ +import org.jetbrains.intellij.platform.gradle.extensions.intellijPlatform + rootProject.name = "DockDockBuild" +pluginManagement { + plugins { + id("org.jetbrains.kotlin.jvm") version "2.1.20" + id("org.jetbrains.changelog") version "2.5.0" + id("org.jlleitschuh.gradle.ktlint") version "12.1.2" + } +} + plugins { - id("com.gradle.enterprise").version("3.10") + id("org.gradle.toolchains.foojay-resolver-convention") version "1.0.0" + id("org.jetbrains.intellij.platform.settings") version "2.14.0" +} + +@Suppress("UnstableApiUsage") +dependencyResolutionManagement { + repositories { + mavenCentral() + + intellijPlatform { + defaultRepositories() + } + maven("https://packages.jetbrains.team/maven/p/ij/intellij-dependencies") + } } diff --git a/src/main/java/com/intuit/ddb/CmdProcessBuilder.java b/src/main/java/com/intuit/ddb/CmdProcessBuilder.java deleted file mode 100644 index e693594..0000000 --- a/src/main/java/com/intuit/ddb/CmdProcessBuilder.java +++ /dev/null @@ -1,193 +0,0 @@ -package com.intuit.ddb; - -import java.io.BufferedReader; -import java.io.File; -import java.io.IOException; -import java.io.InputStreamReader; -import java.util.Arrays; -import java.util.Collections; -import java.util.LinkedList; -import java.util.List; -import java.util.concurrent.TimeUnit; - - -/* -This class is the core of the plugin. It calls Docker build & run commands. -The implementation is it in a separate class (and not DockDockBuildRunConfiguration.getState()) -since we needed to call multiple commands. -It's getting compiled first in Gradle and then DockDockBuildRunConfiguration.getState() runs CmdProcessBuilder.class - */ -public class CmdProcessBuilder { - - enum CMD { - BUILD, - PULL, - RUN - } - - final private static String DEFAULT_TAG = "build"; - final private static String CODE_PATH_DOCKER = "/home/ddb"; - final private static String M2_PATH_DOCKER = "/root/.m2"; - - public static void main(String[] args) throws IOException, InterruptedException { - - if(args.length != 1) { - usage(); - } - - /* input args - JSON file path that contains the following details: - {"dockerExe":"", - "dockerfileDir":"", - "dockerImgUrl":"", - "isImage":, - "makefilePath":".", - "makefileFile":"Makefile", - "target":"", - "codePath":"", - "m2Path":"", - "envScript":""} - */ - Parameters p = Parameters.readParams(args[0]); - - // if using an image, the tag will be the the tag given in dockerImgUrl, else (for build) use default - String imageTag = p.isImage == Boolean.TRUE ? p.dockerImgUrl : DEFAULT_TAG; - String codeVol = p.codePath + ":" + CODE_PATH_DOCKER; - String mavenVol = p.m2Path + ":" + M2_PATH_DOCKER; - - // if not working w/ prebuilt image, build from Dockerfile - if (p.isImage == Boolean.FALSE) { - dockerBuild(p.dockerExe, p.dockerfileDir, imageTag); - } else { - dockerPull(p.dockerExe, p.dockerImgUrl); - } - - dockerRun(p.dockerExe, codeVol, mavenVol, imageTag, p.envScript, p.makefilePath, p.target, - p.makefileFile, p.advancedDockerSettings); - - System.out.println("Program ended"); - } - - - private static void usage() { - System.err.println("CmdProcessBuilder usage:\n " - + "java -cp \"\" com.intuit.ddb.CmdProcessBuilder \n\n" - + "input args - JSON file path that contains the following details:\n" - + " {\"dockerExe\":\"\",\n" - + " \"dockerfileDir\":\"\",\n" - + " \"dockerImgUrl\":\"\",\n" - + " \"isImage\":,\n" - + " \"makefilePath\":\".\",\n" - + " \"makefileFile\":\"Makefile\",\n" - + " \"target\":\"\",\n" - + " \"codePath\":\"\",\n" - + " \"m2Path\":\"\",\n" - + " \"envScript\":\"\"" - + " \"advancedDockerSettings\":\"\""); - System.exit(42); - } - - - private static void dockerBuild(String dockerPath, String dockerfileDir, String tag) throws IOException, - InterruptedException { - - ProcessBuilder builder = new ProcessBuilder(); - - // ******** DOCKER BUILD ******** - // docker build . --tag - builder.command(dockerPath, "build", ".", "--tag", tag). - directory(new File(dockerfileDir)); - - executeCmd(CMD.BUILD, builder); - } - - - private static void dockerPull(String dockerPath, String tag) throws IOException, InterruptedException { - - ProcessBuilder builder = new ProcessBuilder(); - - // ******** DOCKER PULL ******** - // docker pull - builder.command(dockerPath, "pull", tag); - - executeCmd(CMD.PULL, builder); - } - - - private static void dockerRun(String dockerPath, String codeVol, String mavenVol, String tag, String envScript, - String runDir, String target, String makefileFile, String advancedDockerSettings) throws IOException, - InterruptedException { - - ProcessBuilder builder = new ProcessBuilder(); - - // ******** DOCKER RUN ******** - // docker run --rm -t --volume --volume - List cmd = new LinkedList<>(Arrays.asList(dockerPath, "run", "--rm", "-t", "--volume", codeVol, - "--volume", mavenVol)); - if (!advancedDockerSettings.equals("")) { - cmd.addAll(Arrays.asList(advancedDockerSettings.split("\\s+"))); - } - cmd.addAll(Collections.singletonList(tag)); - - // Docker command - // full cmd: bash -c source && cd && make -r -f - cmd.addAll((Arrays.asList("bash", "-c"))); - String innerCmd = ""; - // add source if environment script given - if (envScript != null && !envScript.equals("")) { - innerCmd = "source " + envScript + " && "; - } - // cd - if (!runDir.equals(".")) { - innerCmd += "cd " + runDir + " && "; - } - // make -r -f - innerCmd += "make -r -f " + makefileFile + " " + target; - cmd.addAll(Collections.singletonList(innerCmd)); - builder.command(cmd); - - executeCmd(CMD.RUN, builder); - } - - - private static void executeCmd(CMD cmd, ProcessBuilder builder) throws IOException, - InterruptedException { - - String line; - - builder.inheritIO(); - - System.out.println("\n" + "Docker " + cmd + ":"); - prettyPrint(builder.command()); - - final Process process = builder.start(); - - BufferedReader br = new BufferedReader(new InputStreamReader(process.getInputStream())); - - while ((line = br.readLine()) != null) { - System.out.println(line); - } - - if (!process.waitFor(60, TimeUnit.MINUTES)) { - System.err.println(cmd + " REACHED TIMOUT"); - System.exit(-1); - } - - if (process.exitValue() != 0) { - System.err.println(cmd + " FAILED. Stopping"); - System.exit(process.exitValue()); - } - - process.destroy(); - } - - - // print array in a "runnable" form - private static void prettyPrint(List command) { - for (String word : command) { - System.out.print(word + " "); - } - - System.out.println("\n"); - } - -} diff --git a/src/main/java/com/intuit/ddb/Parameters.java b/src/main/java/com/intuit/ddb/Parameters.java deleted file mode 100644 index 0173fd1..0000000 --- a/src/main/java/com/intuit/ddb/Parameters.java +++ /dev/null @@ -1,66 +0,0 @@ -package com.intuit.ddb; - -import com.fasterxml.jackson.annotation.JsonAutoDetect; -import com.fasterxml.jackson.annotation.JsonAutoDetect.Visibility; -import com.fasterxml.jackson.databind.ObjectMapper; -import java.io.File; -import java.io.IOException; - -@JsonAutoDetect(fieldVisibility = Visibility.ANY) -public class Parameters { - - // Docker executable path - String dockerExe; - // Dockerfile directory - String dockerfileDir; - // OR the docker image URL (url:tag) - String dockerImgUrl; - // is using docker image - Boolean isImage; - // makefile path - String makefilePath; - // makefile filename - String makefileFile; - // makefile target to invoke - String target; - // code path to mount onto Docker container - String codePath; - // Maven cache path to mount onto Docker container - String m2Path; - // environment script to run at Docker container rise - String envScript; - // optional advanced settings for the docker cmd - String advancedDockerSettings; - - - public Parameters(String dockerExe, String dockerfileDir, String dockerImgUrl, Boolean isImage, - String makefilePath, String makefileFile, String target, String codePath, String m2Path, - String envScript, String advancedDockerSettings) { - - this.dockerExe = dockerExe; - this.dockerfileDir = dockerfileDir; - this.dockerImgUrl = dockerImgUrl; - this.isImage = isImage; - this.makefilePath = makefilePath; - this.makefileFile = makefileFile; - this.target = target; - this.codePath = codePath; - this.m2Path = m2Path; - this.envScript = envScript; - this.advancedDockerSettings = advancedDockerSettings; - - } - - // empty constructor needed for jackson - Parameters() { - - } - - // deserialize from Json in file to Parameters obj - static Parameters readParams(String file) throws IOException { - - ObjectMapper objectMapper = new ObjectMapper(); - return objectMapper.readValue(new File(file), Parameters.class); - } - -} diff --git a/src/main/kotlin/com/intuit/ddb/CmdProcessBuilder.kt b/src/main/kotlin/com/intuit/ddb/CmdProcessBuilder.kt new file mode 100644 index 0000000..91c5692 --- /dev/null +++ b/src/main/kotlin/com/intuit/ddb/CmdProcessBuilder.kt @@ -0,0 +1,215 @@ +package com.intuit.ddb + +import java.io.File +import java.io.IOException +import java.util.concurrent.TimeUnit +import kotlin.system.exitProcess + +/* +This class is the core of the plugin. It calls Docker build & run commands. +The implementation is it in a separate class (and not DockDockBuildRunConfiguration.getState()) +since we needed to call multiple commands. +It's getting compiled first in Gradle and then DockDockBuildRunConfiguration.getState() runs CmdProcessBuilder.class + */ +object CmdProcessBuilder { + enum class CMD { + BUILD, + PULL, + RUN, + } + + private const val DEFAULT_TAG = "build" + private const val CODE_PATH_DOCKER = "/home/ddb" + private const val M2_PATH_DOCKER = "/root/.m2" + + @JvmStatic + @Throws(IOException::class, InterruptedException::class) + fun main(args: Array) { + if (args.size != 1) { + usage() + } + + /* input args - JSON file path that contains the following details: + {"dockerExe":"", + "dockerfileDir":"", + "dockerImgUrl":"", + "isImage":, + "makefilePath":".", + "makefileFile":"Makefile", + "target":"", + "codePath":"", + "m2Path":"", + "envScript":""} + */ + val p = Parameters.readParams(args[0]) + + // if using an image, the tag will be the tag given in dockerImgUrl, else (for build) use default + val imageTag = if (p.isImage == true) p.dockerImgUrl else DEFAULT_TAG + val codeVol = "${p.codePath}:$CODE_PATH_DOCKER" + val mavenVol = "${p.m2Path}:$M2_PATH_DOCKER" + + // if not working w/ prebuilt image, build from Dockerfile + if (p.isImage == false) { + dockerBuild(p.dockerExe, p.dockerfileDir, imageTag) + } else { + dockerPull(p.dockerExe, p.dockerImgUrl) + } + + dockerRun( + p.dockerExe, + codeVol, + mavenVol, + imageTag, + p.envScript, + p.makefilePath, + p.target, + p.makefileFile, + p.advancedDockerSettings, + ) + + println("Program ended") + } + + private fun usage() { + System.err.println( + "CmdProcessBuilder usage:\n " + + "java -cp \"\" com.intuit.ddb.CmdProcessBuilder \n\n" + + "input args - JSON file path that contains the following details:\n" + + " {\"dockerExe\":\"\",\n" + + " \"dockerfileDir\":\"\",\n" + + " \"dockerImgUrl\":\"\",\n" + + " \"isImage\":,\n" + + " \"makefilePath\":\".\",\n" + + " \"makefileFile\":\"Makefile\",\n" + + " \"target\":\"\",\n" + + " \"codePath\":\"\",\n" + + " \"m2Path\":\"\",\n" + + " \"envScript\":\"\"\n" + + " \"advancedDockerSettings\":\"\"", + ) + exitProcess(42) + } + + @Throws(IOException::class, InterruptedException::class) + private fun dockerBuild( + dockerPath: String?, + dockerfileDir: String?, + tag: String?, + ) { + val builder = ProcessBuilder() + builder.command(buildDockerBuildCmd(dockerPath, tag)) + if (dockerfileDir != null) builder.directory(File(dockerfileDir)) + executeCmd(CMD.BUILD, builder) + } + + @Throws(IOException::class, InterruptedException::class) + private fun dockerPull( + dockerPath: String?, + tag: String?, + ) { + val builder = ProcessBuilder() + builder.command(buildDockerPullCmd(dockerPath, tag)) + executeCmd(CMD.PULL, builder) + } + + @Throws(IOException::class, InterruptedException::class) + private fun dockerRun( + dockerPath: String?, + codeVol: String?, + mavenVol: String?, + tag: String?, + envScript: String?, + runDir: String?, + target: String?, + makefileFile: String?, + advancedDockerSettings: String?, + ) { + val builder = ProcessBuilder() + builder.command( + buildDockerRunCmd(dockerPath, codeVol, mavenVol, tag, envScript, runDir, target, makefileFile, advancedDockerSettings), + ) + executeCmd(CMD.RUN, builder) + } + + @Throws(IOException::class, InterruptedException::class) + private fun executeCmd( + cmd: CMD, + builder: ProcessBuilder, + ) { + builder.inheritIO() + + println("\nDocker $cmd:") + prettyPrint(builder.command()) + + val process = builder.start() + + if (!process.waitFor(60, TimeUnit.MINUTES)) { + System.err.println("$cmd REACHED TIMEOUT") + process.destroyForcibly() + exitProcess(-1) + } + + if (process.exitValue() != 0) { + System.err.println("$cmd FAILED. Stopping") + exitProcess(process.exitValue()) + } + + process.destroy() + } + + // print array in a "runnable" form + private fun prettyPrint(command: List) { + for (word in command) { + print("$word ") + } + println("\n") + } +} + +// docker build . --tag +internal fun buildDockerBuildCmd( + dockerPath: String?, + tag: String?, +): List = listOf(dockerPath, "build", ".", "--tag", tag).filterNotNull() + +// docker pull +internal fun buildDockerPullCmd( + dockerPath: String?, + tag: String?, +): List = listOf(dockerPath, "pull", tag).filterNotNull() + +// docker run --rm -t --volume --volume [advancedSettings] bash -c +internal fun buildDockerRunCmd( + dockerPath: String?, + codeVol: String?, + mavenVol: String?, + tag: String?, + envScript: String?, + runDir: String?, + target: String?, + makefileFile: String?, + advancedDockerSettings: String?, +): List { + val cmd = mutableListOf(dockerPath, "run", "--rm", "-t", "--volume", codeVol, "--volume", mavenVol) + + if (!advancedDockerSettings.isNullOrEmpty()) { + cmd.addAll(advancedDockerSettings.split("\\s+".toRegex()).filter { it.isNotEmpty() }) + } + cmd.add(tag) + cmd.addAll(listOf("bash", "-c")) + + // Each variable is single-quoted to prevent shell word-splitting and injection. + // Single quotes are the only characters that cannot appear inside a single-quoted string, + // so we reject them at the call site (validated in checkConfiguration). + var innerCmd = "" + if (!envScript.isNullOrEmpty()) { + innerCmd = "source '${envScript.replace("'", "")}' && " + } + if (runDir != ".") { + innerCmd += "cd '${runDir?.replace("'", "")}' && " + } + innerCmd += "make -r -f '${makefileFile?.replace("'", "")}' '${target?.replace("'", "")}'" + cmd.add(innerCmd) + + return cmd.filterNotNull() +} diff --git a/src/main/kotlin/com/intuit/ddb/DockDockBuild.kt b/src/main/kotlin/com/intuit/ddb/DockDockBuild.kt index eb0ac24..87f99b4 100644 --- a/src/main/kotlin/com/intuit/ddb/DockDockBuild.kt +++ b/src/main/kotlin/com/intuit/ddb/DockDockBuild.kt @@ -2,9 +2,11 @@ package com.intuit.ddb import com.intellij.openapi.project.Project import org.apache.commons.lang3.SystemUtils +import java.io.File import java.nio.file.Paths const val PLUGIN_NAME = "DockDockBuild" +const val PLUGIN_ID = "com.intuit.intellij.makefile" const val PROCESS_TO_RUN = "com.intuit.ddb.CmdProcessBuilder" fun getDefaultCodePath(project: Project): String { @@ -12,12 +14,13 @@ fun getDefaultCodePath(project: Project): String { } fun getDefaultDockerPath(): String { + if (SystemUtils.IS_OS_WINDOWS) return "docker" - when { - SystemUtils.IS_OS_WINDOWS -> return "docker" - SystemUtils.IS_OS_MAC || SystemUtils.IS_OS_UNIX -> return "/usr/local/bin/docker" - } - return "" + // Search common locations so this works with Docker Desktop, Rancher, colima, etc. + val rancherPath = "${System.getProperty("user.home")}/.rd/bin/docker" + val searchPaths = + listOf("/usr/local/bin/docker", rancherPath, "/usr/bin/docker", "/opt/homebrew/bin/docker") + return searchPaths.firstOrNull { File(it).exists() } ?: "docker" } fun getDefaultM2Path(): String { @@ -34,7 +37,10 @@ fun getMakefileFilename(makefileFilename: String): String { } // get relative path for makefile to cd into in the Docker container -fun getMakefileDir(project: Project, makefileFilename: String): String { +fun getMakefileDir( + project: Project, + makefileFilename: String, +): String { val pathAbsolute = Paths.get(makefileFilename) val pathBase = Paths.get(getBasePath(project)) val pathRelative = pathBase.relativize(pathAbsolute) @@ -52,14 +58,12 @@ fun getDefaultDockerfileDir(makefileFilePath: String): String { } // get Docker container set_env.sh path -fun getSetEnvRelPath(project: Project, path: String): String { +fun getSetEnvRelPath( + project: Project, + path: String, +): String { val pathAbsolute = Paths.get(path) val pathBase = Paths.get(getBasePath(project)) return pathBase.relativize(pathAbsolute).toString() } - -fun getParamsFile(project: Project): String { - val paramsFile = "/dockDockBuildParams.json" - return getDefaultCodePath(project) + paramsFile -} diff --git a/src/main/kotlin/com/intuit/ddb/DockDockBuildConfigurable.kt b/src/main/kotlin/com/intuit/ddb/DockDockBuildConfigurable.kt index 473438c..cdfb75d 100644 --- a/src/main/kotlin/com/intuit/ddb/DockDockBuildConfigurable.kt +++ b/src/main/kotlin/com/intuit/ddb/DockDockBuildConfigurable.kt @@ -13,7 +13,6 @@ import javax.swing.JTextField // Docker Make project config class DockDockBuildConfigurable(project: Project) : Configurable { - private val settings = project.getService(DockDockBuildProjectSettings::class.java) private val dockerPathField = TextFieldWithBrowseButton() private val codePathField = TextFieldWithBrowseButton() @@ -25,16 +24,22 @@ class DockDockBuildConfigurable(project: Project) : Configurable { settings.settings.codePath = settings.settings.codePath.ifEmpty { getDefaultCodePath(project) } dockerPathField.addBrowseFolderListener( - PLUGIN_NAME, "Path to Docker executable", project, - FileChooserDescriptor(true, false, false, false, false, false) + PLUGIN_NAME, + "Path to Docker executable", + project, + FileChooserDescriptor(true, false, false, false, false, false), ) codePathField.addBrowseFolderListener( - PLUGIN_NAME, "Path to code root", project, - FileChooserDescriptor(false, true, false, false, false, false) + PLUGIN_NAME, + "Path to code root", + project, + FileChooserDescriptor(false, true, false, false, false, false), ) mavenCachePathField.addBrowseFolderListener( - PLUGIN_NAME, "Path to Maven cache", project, - FileChooserDescriptor(false, true, false, false, false, false) + PLUGIN_NAME, + "Path to Maven cache", + project, + FileChooserDescriptor(false, true, false, false, false, false), ) } diff --git a/src/main/kotlin/com/intuit/ddb/DockDockBuildRunTargetAction.kt b/src/main/kotlin/com/intuit/ddb/DockDockBuildRunTargetAction.kt index 394940f..4ef4225 100644 --- a/src/main/kotlin/com/intuit/ddb/DockDockBuildRunTargetAction.kt +++ b/src/main/kotlin/com/intuit/ddb/DockDockBuildRunTargetAction.kt @@ -16,11 +16,10 @@ import name.kropp.intellij.makefile.psi.MakefileTarget // This class creates the context and runs the plugin when an event was received class DockDockBuildRunTargetAction(private val target: MakefileTarget) : AnAction( PLUGIN_NAME + " ${target.name}", - PLUGIN_NAME + " ${target.name}", MakefileTargetIcon + PLUGIN_NAME + " ${target.name}", + MakefileTargetIcon, ) { - override fun actionPerformed(event: AnActionEvent) { - val dataContext = SimpleDataContext.getSimpleContext(Location.DATA_KEY, PsiLocation(target), event.dataContext) val context = ConfigurationContext.getFromContext(dataContext, event.place) diff --git a/src/main/kotlin/com/intuit/ddb/DockDockBuildSettings.kt b/src/main/kotlin/com/intuit/ddb/DockDockBuildSettings.kt index 7154946..fbbf518 100644 --- a/src/main/kotlin/com/intuit/ddb/DockDockBuildSettings.kt +++ b/src/main/kotlin/com/intuit/ddb/DockDockBuildSettings.kt @@ -1,5 +1,8 @@ package com.intuit.ddb +import com.fasterxml.jackson.annotation.JsonIgnoreProperties + +@JsonIgnoreProperties(ignoreUnknown = true) class DockDockBuildSettings { var dockerPath = getDefaultDockerPath() var codePath = "" diff --git a/src/main/kotlin/com/intuit/ddb/DockerfileFileChooserDescriptor.kt b/src/main/kotlin/com/intuit/ddb/DockerfileFileChooserDescriptor.kt index dd03276..2cca36f 100644 --- a/src/main/kotlin/com/intuit/ddb/DockerfileFileChooserDescriptor.kt +++ b/src/main/kotlin/com/intuit/ddb/DockerfileFileChooserDescriptor.kt @@ -5,17 +5,24 @@ import com.intellij.openapi.fileChooser.FileElement import com.intellij.openapi.vfs.VirtualFile class DockerfileFileChooserDescriptor : FileChooserDescriptor( - true, false, - false, false, false, false + true, + false, + false, + false, + false, + false, ) { init { title = "Dockerfile" } - override fun isFileVisible(file: VirtualFile, showHiddenFiles: Boolean) = when { + override fun isFileVisible( + file: VirtualFile, + showHiddenFiles: Boolean, + ) = when { !showHiddenFiles && FileElement.isFileHidden(file) -> false file.isDirectory -> true - else -> file.name.endsWith("") && file.name == "Dockerfile" + else -> file.name == "Dockerfile" } override fun isFileSelectable(file: VirtualFile?): Boolean { diff --git a/src/main/kotlin/com/intuit/ddb/Parameters.kt b/src/main/kotlin/com/intuit/ddb/Parameters.kt new file mode 100644 index 0000000..d4ea4c1 --- /dev/null +++ b/src/main/kotlin/com/intuit/ddb/Parameters.kt @@ -0,0 +1,45 @@ +package com.intuit.ddb + +import com.fasterxml.jackson.annotation.JsonAutoDetect +import com.fasterxml.jackson.annotation.JsonAutoDetect.Visibility +import com.fasterxml.jackson.annotation.JsonIgnoreProperties +import com.fasterxml.jackson.databind.ObjectMapper +import com.fasterxml.jackson.module.kotlin.registerKotlinModule +import java.io.File +import java.io.IOException + +@JsonAutoDetect(fieldVisibility = Visibility.ANY) +@JsonIgnoreProperties(ignoreUnknown = true) +data class Parameters( + // Docker executable path + val dockerExe: String?, + // Dockerfile directory + val dockerfileDir: String?, + // OR the docker image URL (url:tag) + val dockerImgUrl: String?, + // is using docker image + val isImage: Boolean?, + // makefile path + val makefilePath: String?, + // makefile filename + val makefileFile: String?, + // makefile target to invoke + val target: String?, + // code path to mount onto Docker container + val codePath: String?, + // Maven cache path to mount onto Docker container + val m2Path: String?, + // environment script to run at Docker container rise + val envScript: String?, + // optional advanced settings for the docker cmd + val advancedDockerSettings: String?, +) { + companion object { + // deserialize from Json in file to Parameters obj + @Throws(IOException::class) + fun readParams(file: String): Parameters { + val objectMapper = ObjectMapper().registerKotlinModule() + return objectMapper.readValue(File(file), Parameters::class.java) + } + } +} diff --git a/src/main/kotlin/com/intuit/ddb/conf/DockDockBuildRunConfiguration.kt b/src/main/kotlin/com/intuit/ddb/conf/DockDockBuildRunConfiguration.kt index c889af5..fae101b 100644 --- a/src/main/kotlin/com/intuit/ddb/conf/DockDockBuildRunConfiguration.kt +++ b/src/main/kotlin/com/intuit/ddb/conf/DockDockBuildRunConfiguration.kt @@ -1,29 +1,30 @@ +@file:Suppress("ktlint:standard:no-wildcard-imports") + package com.intuit.ddb.conf import com.fasterxml.jackson.databind.ObjectMapper import com.intellij.execution.Executor import com.intellij.execution.configuration.EnvironmentVariablesData -import com.intellij.execution.configurations.* // ktlint-disable no-wildcard-imports +import com.intellij.execution.configurations.* import com.intellij.execution.process.ColoredProcessHandler import com.intellij.execution.process.ProcessHandler import com.intellij.execution.process.ProcessTerminatedListener import com.intellij.execution.runners.ExecutionEnvironment +import com.intellij.ide.plugins.PluginManagerCore +import com.intellij.openapi.extensions.PluginId import com.intellij.openapi.project.Project -import com.intellij.util.lang.UrlClassLoader -import com.intuit.ddb.* // ktlint-disable no-wildcard-imports +import com.intuit.ddb.* import org.jdom.Element import java.io.File -import java.net.URLDecoder // This class handles the *run* configurations of the plugin open class DockDockBuildRunConfiguration(project: Project, factoryDocker: DockDockBuildRunConfigurationFactory, name: String) : LocatableConfigurationBase(project, factoryDocker, name) { - var makefileFilePath = "" var dockerfileDir = "" var dockerImageUrl = "" - var isDockerImage = "" - var isDockerfile = "" + var isDockerImage = false + var isDockerfile = true var target = "" var envScriptPath = "" var arguments = "" @@ -42,7 +43,10 @@ open class DockDockBuildRunConfiguration(project: Project, factoryDocker: DockDo } override fun checkConfiguration() { - // TODO:check for valid configuration + if (makefileFilePath.isBlank()) throw RuntimeConfigurationError("Makefile path must not be empty") + if (target.isBlank()) throw RuntimeConfigurationError("Target must not be empty") + if (isDockerfile && dockerfileDir.isBlank()) throw RuntimeConfigurationError("Dockerfile path must not be empty") + if (isDockerImage && dockerImageUrl.isBlank()) throw RuntimeConfigurationError("Docker image URL must not be empty") } override fun getConfigurationEditor() = DockDockBuildRunConfigurationEditor(project) @@ -53,8 +57,8 @@ open class DockDockBuildRunConfiguration(project: Project, factoryDocker: DockDo child.setAttribute(MAKEFILE_FILEPATH, makefileFilePath) child.setAttribute(DOCKER_FILENAME, dockerfileDir) child.setAttribute(DOCKER_IMAGE, dockerImageUrl) - child.setAttribute(IS_DOCKER_IMAGE, isDockerImage) - child.setAttribute(IS_DOCKER_FILE, isDockerfile) + child.setAttribute(IS_DOCKER_IMAGE, isDockerImage.toString()) + child.setAttribute(IS_DOCKER_FILE, isDockerfile.toString()) child.setAttribute(TARGET, target) child.setAttribute(ENV_SCRIPT, envScriptPath) child.setAttribute(ARGUMENTS, arguments) @@ -68,8 +72,8 @@ open class DockDockBuildRunConfiguration(project: Project, factoryDocker: DockDo makefileFilePath = child.getAttributeValue(MAKEFILE_FILEPATH) ?: "" dockerfileDir = child.getAttributeValue(DOCKER_FILENAME) ?: "" dockerImageUrl = child.getAttributeValue(DOCKER_IMAGE) ?: "" - isDockerImage = child.getAttributeValue(IS_DOCKER_IMAGE) ?: "" - isDockerfile = child.getAttributeValue(IS_DOCKER_FILE) ?: "" + isDockerImage = child.getAttributeValue(IS_DOCKER_IMAGE)?.toBoolean() ?: false + isDockerfile = child.getAttributeValue(IS_DOCKER_FILE)?.toBoolean() ?: true target = child.getAttributeValue(TARGET) ?: "" envScriptPath = child.getAttributeValue(ENV_SCRIPT) ?: "" arguments = child.getAttributeValue(ARGUMENTS) ?: "" @@ -77,29 +81,34 @@ open class DockDockBuildRunConfiguration(project: Project, factoryDocker: DockDo } } - override fun getState(executor: Executor, executionEnvironment: ExecutionEnvironment): RunProfileState? { - - handleParams() + override fun getState( + executor: Executor, + executionEnvironment: ExecutionEnvironment, + ): RunProfileState? { + val paramsFile = handleParams() val decodedCP = getClassPath() val userDir = System.getProperty("user.dir") return object : CommandLineState(executionEnvironment) { override fun startProcess(): ProcessHandler { - // java -cp com.intuit.ddb.CmdProcessBuilder val params = ParametersList() - params.addAll("-cp", decodedCP, PROCESS_TO_RUN, getParamsFile(project)) - - val cmd = GeneralCommandLine() - .withExePath("java") - .withWorkDirectory(userDir) - .withEnvironment(environmentVariables.envs) - .withParentEnvironmentType( - if (environmentVariables.isPassParentEnvs) GeneralCommandLine.ParentEnvironmentType.CONSOLE - else GeneralCommandLine.ParentEnvironmentType.NONE - ) - .withParameters(params.list) + params.addAll("-cp", decodedCP, PROCESS_TO_RUN, paramsFile) + + val cmd = + GeneralCommandLine() + .withExePath("java") + .withWorkDirectory(userDir) + .withEnvironment(environmentVariables.envs) + .withParentEnvironmentType( + if (environmentVariables.isPassParentEnvs) { + GeneralCommandLine.ParentEnvironmentType.CONSOLE + } else { + GeneralCommandLine.ParentEnvironmentType.NONE + }, + ) + .withParameters(params.list) val processHandler = ColoredProcessHandler(cmd) ProcessTerminatedListener.attach(processHandler) @@ -109,17 +118,20 @@ open class DockDockBuildRunConfiguration(project: Project, factoryDocker: DockDo } } - private fun handleParams() { - + private fun handleParams(): String { // Plugin (project) configuration - val dockerPath = project.getService(DockDockBuildProjectSettings::class.java) - .settings.dockerPath - val codePath = project.getService(DockDockBuildProjectSettings::class.java) - .settings.codePath - val m2Path = project.getService(DockDockBuildProjectSettings::class.java) - .settings.mavenCachePath - val advancedDockerSettings = project.getService(DockDockBuildProjectSettings::class.java) - .settings.advancedDockerSettings + val dockerPath = + project.getService(DockDockBuildProjectSettings::class.java) + .settings.dockerPath + val codePath = + project.getService(DockDockBuildProjectSettings::class.java) + .settings.codePath + val m2Path = + project.getService(DockDockBuildProjectSettings::class.java) + .settings.mavenCachePath + val advancedDockerSettings = + project.getService(DockDockBuildProjectSettings::class.java) + .settings.advancedDockerSettings // Runtime configurations // on host @@ -129,29 +141,42 @@ open class DockDockBuildRunConfiguration(project: Project, factoryDocker: DockDo val makefilePath = if (makefileFilePath != "") getMakefileDir(project, makefileFilePath) else "." val envScriptPath = if (envScriptPath != "") getSetEnvRelPath(project, envScriptPath) else "" - // create Parameters obj and write to file to be used in CmdProcessBuilder + // Write params to a unique temp file so concurrent runs don't clobber each other. val objectMapper = ObjectMapper() - val cmdParams = Parameters( - dockerPath, dockerfileDir, dockerImageUrl, isDockerImage.toBoolean(), - makefilePath, makefileFileName, target, codePath, m2Path, envScriptPath, advancedDockerSettings - ) - objectMapper.writeValue(File(getParamsFile(project)), cmdParams) + val cmdParams = + Parameters( + dockerPath, dockerfileDir, dockerImageUrl, isDockerImage, + makefilePath, makefileFileName, target, codePath, m2Path, envScriptPath, advancedDockerSettings, + ) + val paramsFile = File.createTempFile("dockDockBuildParams", ".json") + paramsFile.deleteOnExit() + objectMapper.writeValue(paramsFile, cmdParams) + return paramsFile.absolutePath } - // iterate over IntelliJ's UrlClassLoader and find DockDockBuild.jar classpath to call CmdProcessBuilder private fun getClassPath(): String { - val jarRegex = Regex("DockDockBuild.jar") - var classpath = "" - - for (cp in (CmdProcessBuilder::class.java.classLoader as UrlClassLoader).urls) { - if (jarRegex.containsMatchIn(cp.file)) { - classpath = cp.path - break - } + // Ask IntelliJ's plugin manager for the plugin's install path, then find the jar inside it. + // This works regardless of classloader implementation (PluginClassLoader, URLClassLoader, etc.) + val pluginId = PluginId.getId(PLUGIN_ID) + val descriptor = + PluginManagerCore.getPlugin(pluginId) + ?: throw RuntimeException("Plugin descriptor not found for id: $pluginId") + + val pluginPath = descriptor.pluginPath + val jar = pluginPath.resolve("lib/DockDockBuild.jar").toFile() + if (jar.exists()) { + return jar.absolutePath } - if (classpath == "") { - throw Error("DockDockBuild is not in Java's classLoader") + + // Fallback: search lib/ for any DockDockBuild jar + val libDir = pluginPath.resolve("lib").toFile() + if (libDir.isDirectory) { + val found = + libDir.listFiles { f -> f.name.startsWith("DockDockBuild") && f.name.endsWith(".jar") } + ?.firstOrNull() + if (found != null) return found.absolutePath } - return URLDecoder.decode(classpath, "UTF-8") + + throw RuntimeException("DockDockBuild.jar not found under plugin path: $pluginPath") } } diff --git a/src/main/kotlin/com/intuit/ddb/conf/DockDockBuildRunConfigurationEditor.kt b/src/main/kotlin/com/intuit/ddb/conf/DockDockBuildRunConfigurationEditor.kt index 08ffa3e..16ebb6c 100644 --- a/src/main/kotlin/com/intuit/ddb/conf/DockDockBuildRunConfigurationEditor.kt +++ b/src/main/kotlin/com/intuit/ddb/conf/DockDockBuildRunConfigurationEditor.kt @@ -1,3 +1,5 @@ +@file:Suppress("ktlint:standard:no-wildcard-imports") + package com.intuit.ddb.conf import com.intellij.execution.configuration.EnvironmentVariablesComponent @@ -17,12 +19,11 @@ import name.kropp.intellij.makefile.MakefileFileChooserDescriptor import name.kropp.intellij.makefile.MakefileTargetIcon import name.kropp.intellij.makefile.findTargets import java.awt.GridLayout -import javax.swing.* // ktlint-disable no-wildcard-imports +import javax.swing.* import javax.swing.event.DocumentEvent // This class builds the run conf UI class DockDockBuildRunConfigurationEditor(private val project: Project) : SettingsEditor() { - // create UI elements // Docker file\ image buttons private val dockerFilenameField = TextFieldWithBrowseButton() @@ -34,13 +35,18 @@ class DockDockBuildRunConfigurationEditor(private val project: Project) : Settin private val makeFilenameField = TextFieldWithBrowseButton() - private val targetCompletionProvider = TextFieldWithAutoCompletion.StringsCompletionProvider( - emptyList(), MakefileTargetIcon - ) - private val targetField = TextFieldWithAutoCompletion( - project, targetCompletionProvider, - true, "" - ) + private val targetCompletionProvider = + TextFieldWithAutoCompletion.StringsCompletionProvider( + emptyList(), + MakefileTargetIcon, + ) + private val targetField = + TextFieldWithAutoCompletion( + project, + targetCompletionProvider, + true, + "", + ) private val envScriptPathField = TextFieldWithBrowseButton() private val argumentsField = ExpandableTextField() @@ -66,14 +72,18 @@ class DockDockBuildRunConfigurationEditor(private val project: Project) : Settin init { dockerFilenameField.addBrowseFolderListener( - "Dockerfile", "Dockerfile path", project, - DockerfileFileChooserDescriptor() + "Dockerfile", + "Dockerfile path", + project, + DockerfileFileChooserDescriptor(), + ) + dockerFilenameField.textField.document.addDocumentListener( + object : DocumentAdapter() { + override fun textChanged(event: DocumentEvent) { + updateTargetCompletion(dockerFilenameField.text) + } + }, ) - dockerFilenameField.textField.document.addDocumentListener(object : DocumentAdapter() { - override fun textChanged(event: DocumentEvent) { - updateTargetCompletion(dockerFilenameField.text) - } - }) isDockerfileBox.addActionListener { dockerFilenameField.isEnabled = true @@ -88,24 +98,32 @@ class DockDockBuildRunConfigurationEditor(private val project: Project) : Settin } makeFilenameField.addBrowseFolderListener( - "Makefile", "Makefile path", project, - MakefileFileChooserDescriptor() + "Makefile", + "Makefile path", + project, + MakefileFileChooserDescriptor(), + ) + makeFilenameField.textField.document.addDocumentListener( + object : DocumentAdapter() { + override fun textChanged(event: DocumentEvent) { + updateTargetCompletion(makeFilenameField.text) + } + }, ) - makeFilenameField.textField.document.addDocumentListener(object : DocumentAdapter() { - override fun textChanged(event: DocumentEvent) { - updateTargetCompletion(makeFilenameField.text) - } - }) envScriptPathField.addBrowseFolderListener( - "Environment Script", "Environment script path", - project, FileChooserDescriptorFactory.createSingleFileDescriptor("sh") + "Environment Script", + "Environment script path", + project, + FileChooserDescriptorFactory.createSingleFileDescriptor("sh"), + ) + envScriptPathField.textField.document.addDocumentListener( + object : DocumentAdapter() { + override fun textChanged(event: DocumentEvent) { + updateTargetCompletion(envScriptPathField.text) + } + }, ) - envScriptPathField.textField.document.addDocumentListener(object : DocumentAdapter() { - override fun textChanged(event: DocumentEvent) { - updateTargetCompletion(envScriptPathField.text) - } - }) } fun updateTargetCompletion(filename: String) { @@ -125,8 +143,8 @@ class DockDockBuildRunConfigurationEditor(private val project: Project) : Settin override fun applyEditorTo(configuration: DockDockBuildRunConfiguration) { configuration.dockerfileDir = dockerFilenameField.text configuration.dockerImageUrl = dockerImageField.text - configuration.isDockerImage = isDockerImageBox.isSelected.toString() - configuration.isDockerfile = isDockerfileBox.isSelected.toString() + configuration.isDockerImage = isDockerImageBox.isSelected + configuration.isDockerfile = isDockerfileBox.isSelected configuration.makefileFilePath = makeFilenameField.text configuration.target = targetField.text configuration.envScriptPath = envScriptPathField.text @@ -138,8 +156,8 @@ class DockDockBuildRunConfigurationEditor(private val project: Project) : Settin override fun resetEditorFrom(configuration: DockDockBuildRunConfiguration) { dockerFilenameField.text = configuration.dockerfileDir dockerImageField.text = configuration.dockerImageUrl - isDockerImageBox.isSelected = configuration.isDockerImage == "true" - isDockerfileBox.isSelected = configuration.isDockerfile == "true" + isDockerImageBox.isSelected = configuration.isDockerImage + isDockerfileBox.isSelected = configuration.isDockerfile dockerImageField.isEnabled = isDockerImageBox.isSelected dockerFilenameField.isEnabled = isDockerfileBox.isSelected @@ -154,7 +172,6 @@ class DockDockBuildRunConfigurationEditor(private val project: Project) : Settin } private fun dockerPanel(): JPanel { - // put radio buttons in the same group isImageGroup.add(isDockerImageBox) isImageGroup.add(isDockerfileBox) @@ -173,10 +190,11 @@ class DockDockBuildRunConfigurationEditor(private val project: Project) : Settin // Add a titled border to the button panel radioPanel.border = BorderFactory.createEmptyBorder() - radioPanel.border = BorderFactory.createTitledBorder( - radioPanel.border, - "Select Docker file or image to run" - ) + radioPanel.border = + BorderFactory.createTitledBorder( + radioPanel.border, + "Select Docker file or image to run", + ) return radioPanel } diff --git a/src/main/kotlin/com/intuit/ddb/conf/DockDockBuildRunConfigurationFactory.kt b/src/main/kotlin/com/intuit/ddb/conf/DockDockBuildRunConfigurationFactory.kt index 221fddc..e20ccc2 100644 --- a/src/main/kotlin/com/intuit/ddb/conf/DockDockBuildRunConfigurationFactory.kt +++ b/src/main/kotlin/com/intuit/ddb/conf/DockDockBuildRunConfigurationFactory.kt @@ -7,10 +7,12 @@ import com.intuit.ddb.PLUGIN_NAME class DockDockBuildRunConfigurationFactory(runConfigurationType: DockDockBuildRunConfigurationType) : ConfigurationFactory(runConfigurationType) { - - override fun createTemplateConfiguration(project: Project) = DockDockBuildRunConfiguration( - project, this, PLUGIN_NAME - ) + override fun createTemplateConfiguration(project: Project) = + DockDockBuildRunConfiguration( + project, + this, + PLUGIN_NAME, + ) override fun getSingletonPolicy() = RunConfigurationSingletonPolicy.SINGLE_INSTANCE_ONLY diff --git a/src/main/kotlin/com/intuit/ddb/conf/DockDockBuildRunConfigurationProducer.kt b/src/main/kotlin/com/intuit/ddb/conf/DockDockBuildRunConfigurationProducer.kt index d396816..2dd589a 100644 --- a/src/main/kotlin/com/intuit/ddb/conf/DockDockBuildRunConfigurationProducer.kt +++ b/src/main/kotlin/com/intuit/ddb/conf/DockDockBuildRunConfigurationProducer.kt @@ -11,7 +11,6 @@ import name.kropp.intellij.makefile.psi.MakefileTarget import java.io.File class DockDockBuildRunConfigurationProducer : LazyRunConfigurationProducer() { - override fun getConfigurationFactory(): ConfigurationFactory { return DockDockBuildRunConfigurationFactory(DockDockBuildRunConfigurationType) } @@ -20,9 +19,8 @@ class DockDockBuildRunConfigurationProducer : LazyRunConfigurationProducer + sourceElement: Ref, ): Boolean { - if (context.psiLocation?.containingFile !is MakefileFile) { return false } @@ -42,9 +40,8 @@ class DockDockBuildRunConfigurationProducer : LazyRunConfigurationProducer { val file = psiFile as MakefileFile val rule = MakefileElementFactory.createRule(project, prerequisite.text) diff --git a/src/main/kotlin/name/kropp/intellij/makefile/MakefileAnnotator.kt b/src/main/kotlin/name/kropp/intellij/makefile/MakefileAnnotator.kt index f107ab0..38e37c1 100644 --- a/src/main/kotlin/name/kropp/intellij/makefile/MakefileAnnotator.kt +++ b/src/main/kotlin/name/kropp/intellij/makefile/MakefileAnnotator.kt @@ -1,3 +1,5 @@ +@file:Suppress("ktlint:standard:no-wildcard-imports") + package name.kropp.intellij.makefile import com.intellij.codeInspection.ProblemHighlightType @@ -8,12 +10,15 @@ import com.intellij.openapi.util.TextRange import com.intellij.psi.PsiElement import com.intellij.psi.impl.source.resolve.reference.impl.providers.FileReference import com.intellij.psi.tree.TokenSet -import name.kropp.intellij.makefile.psi.* // ktlint-disable no-wildcard-imports +import name.kropp.intellij.makefile.psi.* class MakefileAnnotator : Annotator { private val lineTokenSet = TokenSet.create(MakefileTypes.LINE) - override fun annotate(element: PsiElement, holder: AnnotationHolder) { + override fun annotate( + element: PsiElement, + holder: AnnotationHolder, + ) { if (element is MakefileRule && element.isUnused()) { holder.newAnnotation(HighlightSeverity.INFORMATION, "Redundant rule").range(element) .highlightType(ProblemHighlightType.LIKE_UNUSED_SYMBOL) diff --git a/src/main/kotlin/name/kropp/intellij/makefile/MakefileChooseByNameContributor.kt b/src/main/kotlin/name/kropp/intellij/makefile/MakefileChooseByNameContributor.kt index 41f56f4..e29dcd7 100644 --- a/src/main/kotlin/name/kropp/intellij/makefile/MakefileChooseByNameContributor.kt +++ b/src/main/kotlin/name/kropp/intellij/makefile/MakefileChooseByNameContributor.kt @@ -4,6 +4,17 @@ import com.intellij.navigation.ChooseByNameContributor import com.intellij.openapi.project.Project class MakefileChooseByNameContributor : ChooseByNameContributor { - override fun getItemsByName(name: String, pattern: String, project: Project, includeNonProjectItems: Boolean) = findTargets(project, name).filterNot { it.isSpecialTarget }.toTypedArray() - override fun getNames(project: Project, includeNonProjectItems: Boolean) = findAllTargets(project).filterNot(String::isNullOrEmpty).toTypedArray() + override fun getItemsByName( + name: String, + pattern: String, + project: Project, + includeNonProjectItems: Boolean, + ) = findTargets(project, name).filterNot { it.isSpecialTarget }.toTypedArray() + + override fun getNames( + project: Project, + includeNonProjectItems: Boolean, + ) = findAllTargets( + project, + ).filterNot(String::isNullOrEmpty).toTypedArray() } diff --git a/src/main/kotlin/name/kropp/intellij/makefile/MakefileCodeStyleSettingsProvider.kt b/src/main/kotlin/name/kropp/intellij/makefile/MakefileCodeStyleSettingsProvider.kt index f7d3daf..89abde5 100644 --- a/src/main/kotlin/name/kropp/intellij/makefile/MakefileCodeStyleSettingsProvider.kt +++ b/src/main/kotlin/name/kropp/intellij/makefile/MakefileCodeStyleSettingsProvider.kt @@ -6,14 +6,14 @@ import com.intellij.psi.codeStyle.LanguageCodeStyleSettingsProvider class MakefileCodeStyleSettingsProvider : LanguageCodeStyleSettingsProvider() { override fun customizeDefaults( commonSettings: CommonCodeStyleSettings, - indentOptions: CommonCodeStyleSettings.IndentOptions + indentOptions: CommonCodeStyleSettings.IndentOptions, ) { super.customizeDefaults( commonSettings, indentOptions.apply { INDENT_SIZE = 4 USE_TAB_CHARACTER = true - } + }, ) } diff --git a/src/main/kotlin/name/kropp/intellij/makefile/MakefileColorSettingsPage.kt b/src/main/kotlin/name/kropp/intellij/makefile/MakefileColorSettingsPage.kt index 6c6381a..fbc2945 100644 --- a/src/main/kotlin/name/kropp/intellij/makefile/MakefileColorSettingsPage.kt +++ b/src/main/kotlin/name/kropp/intellij/makefile/MakefileColorSettingsPage.kt @@ -6,39 +6,44 @@ import com.intellij.openapi.options.colors.ColorSettingsPage class MakefileColorSettingsPage : ColorSettingsPage { override fun getDisplayName() = "Makefile" + override fun getIcon() = MakefileIcon - private val tags = mapOf( - "target" to MakefileSyntaxHighlighter.TARGET, - "specialTarget" to MakefileSyntaxHighlighter.SPECIAL_TARGET, - "variableName" to MakefileSyntaxHighlighter.VARIABLE, - "prerequisite" to MakefileSyntaxHighlighter.PREREQUISITE, - "function" to MakefileSyntaxHighlighter.FUNCTION, - "functionParam" to MakefileSyntaxHighlighter.FUNCTION_PARAM, - "variableUsage" to MakefileSyntaxHighlighter.VARIABLE_USAGE - ) - - private val DESCRIPTORS = arrayOf( - AttributesDescriptor("Comment", MakefileSyntaxHighlighter.COMMENT), - AttributesDescriptor("Documentation Comment", MakefileSyntaxHighlighter.DOCCOMMENT), - AttributesDescriptor("Keyword", MakefileSyntaxHighlighter.KEYWORD), - AttributesDescriptor("Target", MakefileSyntaxHighlighter.TARGET), - AttributesDescriptor("Special Target", MakefileSyntaxHighlighter.SPECIAL_TARGET), - AttributesDescriptor("Separator", MakefileSyntaxHighlighter.SEPARATOR), - AttributesDescriptor("Prerequisite", MakefileSyntaxHighlighter.PREREQUISITE), - AttributesDescriptor("Variable Name", MakefileSyntaxHighlighter.VARIABLE), - AttributesDescriptor("Variable Value", MakefileSyntaxHighlighter.VARIABLE_VALUE), - AttributesDescriptor("Variable Usage", MakefileSyntaxHighlighter.VARIABLE_USAGE), - AttributesDescriptor("Line Split", MakefileSyntaxHighlighter.LINE_SPLIT), - AttributesDescriptor("Tab", MakefileSyntaxHighlighter.TAB), - AttributesDescriptor("Function", MakefileSyntaxHighlighter.FUNCTION), - AttributesDescriptor("Function Param", MakefileSyntaxHighlighter.FUNCTION_PARAM) - ) + private val tags = + mapOf( + "target" to MakefileSyntaxHighlighter.TARGET, + "specialTarget" to MakefileSyntaxHighlighter.SPECIAL_TARGET, + "variableName" to MakefileSyntaxHighlighter.VARIABLE, + "prerequisite" to MakefileSyntaxHighlighter.PREREQUISITE, + "function" to MakefileSyntaxHighlighter.FUNCTION, + "functionParam" to MakefileSyntaxHighlighter.FUNCTION_PARAM, + "variableUsage" to MakefileSyntaxHighlighter.VARIABLE_USAGE, + ) + + private val DESCRIPTORS = + arrayOf( + AttributesDescriptor("Comment", MakefileSyntaxHighlighter.COMMENT), + AttributesDescriptor("Documentation Comment", MakefileSyntaxHighlighter.DOCCOMMENT), + AttributesDescriptor("Keyword", MakefileSyntaxHighlighter.KEYWORD), + AttributesDescriptor("Target", MakefileSyntaxHighlighter.TARGET), + AttributesDescriptor("Special Target", MakefileSyntaxHighlighter.SPECIAL_TARGET), + AttributesDescriptor("Separator", MakefileSyntaxHighlighter.SEPARATOR), + AttributesDescriptor("Prerequisite", MakefileSyntaxHighlighter.PREREQUISITE), + AttributesDescriptor("Variable Name", MakefileSyntaxHighlighter.VARIABLE), + AttributesDescriptor("Variable Value", MakefileSyntaxHighlighter.VARIABLE_VALUE), + AttributesDescriptor("Variable Usage", MakefileSyntaxHighlighter.VARIABLE_USAGE), + AttributesDescriptor("Line Split", MakefileSyntaxHighlighter.LINE_SPLIT), + AttributesDescriptor("Tab", MakefileSyntaxHighlighter.TAB), + AttributesDescriptor("Function", MakefileSyntaxHighlighter.FUNCTION), + AttributesDescriptor("Function Param", MakefileSyntaxHighlighter.FUNCTION_PARAM), + ) override fun getAttributeDescriptors() = DESCRIPTORS + override fun getHighlighter() = MakefileSyntaxHighlighter() - override fun getDemoText() = """# Simple Makefile + override fun getDemoText() = + """# Simple Makefile include make.mk all: hello ## Doc comment @@ -59,5 +64,6 @@ ${'\t'}$(error Architecture $(ARCH)() { - override fun addCompletions(parameters: CompletionParameters, context: ProcessingContext, resultSet: CompletionResultSet) { + override fun addCompletions( + parameters: CompletionParameters, + context: ProcessingContext, + resultSet: CompletionResultSet, + ) { resultSet.addAllElements(keywords.map { LookupElementBuilder.create(it) }) } - } + }, ) } } diff --git a/src/main/kotlin/name/kropp/intellij/makefile/MakefileDocumentationProvider.kt b/src/main/kotlin/name/kropp/intellij/makefile/MakefileDocumentationProvider.kt index 754cd5e..0aaf04e 100644 --- a/src/main/kotlin/name/kropp/intellij/makefile/MakefileDocumentationProvider.kt +++ b/src/main/kotlin/name/kropp/intellij/makefile/MakefileDocumentationProvider.kt @@ -5,6 +5,13 @@ import com.intellij.psi.PsiElement import name.kropp.intellij.makefile.psi.MakefileTarget class MakefileDocumentationProvider : AbstractDocumentationProvider() { - override fun getQuickNavigateInfo(element: PsiElement, originalElement: PsiElement?) = (element as? MakefileTarget)?.docComment - override fun generateDoc(element: PsiElement?, originalElement: PsiElement?) = (element as? MakefileTarget)?.docComment + override fun getQuickNavigateInfo( + element: PsiElement, + originalElement: PsiElement?, + ) = (element as? MakefileTarget)?.docComment + + override fun generateDoc( + element: PsiElement?, + originalElement: PsiElement?, + ) = (element as? MakefileTarget)?.docComment } diff --git a/src/main/kotlin/name/kropp/intellij/makefile/MakefileFileChooserDescriptor.kt b/src/main/kotlin/name/kropp/intellij/makefile/MakefileFileChooserDescriptor.kt index 6f33ae5..11d116c 100644 --- a/src/main/kotlin/name/kropp/intellij/makefile/MakefileFileChooserDescriptor.kt +++ b/src/main/kotlin/name/kropp/intellij/makefile/MakefileFileChooserDescriptor.kt @@ -9,7 +9,10 @@ class MakefileFileChooserDescriptor : FileChooserDescriptor(true, false, false, title = "Makefile" } - override fun isFileVisible(file: VirtualFile, showHiddenFiles: Boolean) = when { + override fun isFileVisible( + file: VirtualFile, + showHiddenFiles: Boolean, + ) = when { !showHiddenFiles && FileElement.isFileHidden(file) -> false file.isDirectory -> true else -> file.name.endsWith(".mk") || file.name == "Makefile" diff --git a/src/main/kotlin/name/kropp/intellij/makefile/MakefileFileType.kt b/src/main/kotlin/name/kropp/intellij/makefile/MakefileFileType.kt index 1667c81..d7d79c3 100644 --- a/src/main/kotlin/name/kropp/intellij/makefile/MakefileFileType.kt +++ b/src/main/kotlin/name/kropp/intellij/makefile/MakefileFileType.kt @@ -1,7 +1,9 @@ +@file:Suppress("ktlint:standard:no-wildcard-imports") + package name.kropp.intellij.makefile import com.intellij.icons.AllIcons -import com.intellij.openapi.fileTypes.* // ktlint-disable no-wildcard-imports +import com.intellij.openapi.fileTypes.* import com.intellij.openapi.util.IconLoader import javax.swing.Icon @@ -9,7 +11,6 @@ val MakefileIcon = IconLoader.getIcon("/ddb/icon/DockDockBuild15pxl.png", Makefi val MakefileTargetIcon = AllIcons.RunConfigurations.TestState.Run class MakefileFileType : LanguageFileType(MakefileLanguage) { - override fun getName(): String { return "Makefile" } diff --git a/src/main/kotlin/name/kropp/intellij/makefile/MakefileFindUsagesProvider.kt b/src/main/kotlin/name/kropp/intellij/makefile/MakefileFindUsagesProvider.kt index 4b9234a..453ff10 100644 --- a/src/main/kotlin/name/kropp/intellij/makefile/MakefileFindUsagesProvider.kt +++ b/src/main/kotlin/name/kropp/intellij/makefile/MakefileFindUsagesProvider.kt @@ -8,12 +8,38 @@ import name.kropp.intellij.makefile.psi.MakefileTarget import name.kropp.intellij.makefile.psi.MakefileTypes class MakefileFindUsagesProvider : FindUsagesProvider { - override fun getWordsScanner() = DefaultWordsScanner(MakefileLexerAdapter(), TokenSet.create(MakefileTypes.IDENTIFIER), TokenSet.create(MakefileTypes.COMMENT), TokenSet.EMPTY) + override fun getWordsScanner() = + DefaultWordsScanner( + MakefileLexerAdapter(), + TokenSet.create(MakefileTypes.IDENTIFIER), + TokenSet.create(MakefileTypes.COMMENT), + TokenSet.EMPTY, + ) override fun canFindUsagesFor(element: PsiElement) = element is MakefileTarget && !element.isSpecialTarget - override fun getType(element: PsiElement) = if (canFindUsagesFor(element)) { "Makefile target" } else "" - override fun getDescriptiveName(element: PsiElement) = if (canFindUsagesFor(element)) { element.text } else "" - override fun getNodeText(element: PsiElement, useFullName: Boolean) = if (canFindUsagesFor(element)) { element.text } else "" + + override fun getType(element: PsiElement) = + if (canFindUsagesFor(element)) { + "Makefile target" + } else { + "" + } + + override fun getDescriptiveName(element: PsiElement) = + if (canFindUsagesFor(element)) { + element.text + } else { + "" + } + + override fun getNodeText( + element: PsiElement, + useFullName: Boolean, + ) = if (canFindUsagesFor(element)) { + element.text + } else { + "" + } override fun getHelpId(element: PsiElement) = null } diff --git a/src/main/kotlin/name/kropp/intellij/makefile/MakefileFoldingBuilder.kt b/src/main/kotlin/name/kropp/intellij/makefile/MakefileFoldingBuilder.kt index 05f0bb5..c11a212 100644 --- a/src/main/kotlin/name/kropp/intellij/makefile/MakefileFoldingBuilder.kt +++ b/src/main/kotlin/name/kropp/intellij/makefile/MakefileFoldingBuilder.kt @@ -13,18 +13,27 @@ import name.kropp.intellij.makefile.psi.MakefileRule import name.kropp.intellij.makefile.psi.MakefileVariableAssignment class MakefileFoldingBuilder : FoldingBuilderEx(), DumbAware { - override fun buildFoldRegions(root: PsiElement, document: Document, quick: Boolean) = - PsiTreeUtil.findChildrenOfAnyType(root, MakefileRule::class.java, MakefileVariableAssignment::class.java, MakefileDefine::class.java) - .mapNotNull { - when (it) { - is MakefileRule -> MakefileRuleFoldingDescriptor(it) - is MakefileVariableAssignment -> MakefileVariableFoldingDescriptor(it) - is MakefileDefine -> MakefileDefineFoldingDescriptor(it) - else -> null - } - }.toTypedArray() + override fun buildFoldRegions( + root: PsiElement, + document: Document, + quick: Boolean, + ) = PsiTreeUtil.findChildrenOfAnyType( + root, + MakefileRule::class.java, + MakefileVariableAssignment::class.java, + MakefileDefine::class.java, + ) + .mapNotNull { + when (it) { + is MakefileRule -> MakefileRuleFoldingDescriptor(it) + is MakefileVariableAssignment -> MakefileVariableFoldingDescriptor(it) + is MakefileDefine -> MakefileDefineFoldingDescriptor(it) + else -> null + } + }.toTypedArray() override fun getPlaceholderText(node: ASTNode) = "..." + override fun isCollapsedByDefault(node: ASTNode) = node.psi is MakefileDefine companion object { @@ -38,15 +47,27 @@ class MakefileFoldingBuilder : FoldingBuilderEx(), DumbAware { }?.trim() ?: "" } - fun PsiElement.trimmedTextRange() = TextRange.create(textRange.startOffset, textRange.startOffset + text.indexOfLast { !it.isWhitespace() } + 1) + fun PsiElement.trimmedTextRange() = + TextRange.create( + textRange.startOffset, + textRange.startOffset + + text.indexOfLast { + !it.isWhitespace() + } + 1, + ) } class MakefileRuleFoldingDescriptor(private val rule: MakefileRule) : FoldingDescriptor(rule, rule.trimmedTextRange()) { override fun getPlaceholderText() = rule.targetLine.targets.text + ":" } - class MakefileVariableFoldingDescriptor(private val variable: MakefileVariableAssignment) : FoldingDescriptor(variable, variable.trimmedTextRange()) { + + class MakefileVariableFoldingDescriptor(private val variable: MakefileVariableAssignment) : FoldingDescriptor( + variable, + variable.trimmedTextRange(), + ) { override fun getPlaceholderText() = "${variable.variable.text}${variable.assignment?.text ?: "="}${cutValue(variable.value)}" } + class MakefileDefineFoldingDescriptor(private val define: MakefileDefine) : FoldingDescriptor(define, define.trimmedTextRange()) { override fun getPlaceholderText() = "${define.variable?.text}${define.assignment?.text ?: "="}${cutValue(define.value)}" } diff --git a/src/main/kotlin/name/kropp/intellij/makefile/MakefileParserDefinition.kt b/src/main/kotlin/name/kropp/intellij/makefile/MakefileParserDefinition.kt index f0aff43..fc155b5 100644 --- a/src/main/kotlin/name/kropp/intellij/makefile/MakefileParserDefinition.kt +++ b/src/main/kotlin/name/kropp/intellij/makefile/MakefileParserDefinition.kt @@ -17,15 +17,22 @@ class MakefileParserDefinition : ParserDefinition { } override fun getFileNodeType() = FILE + override fun getWhitespaceTokens() = WHITE_SPACES + override fun getCommentTokens() = COMMENTS + override fun getStringLiteralElements() = TokenSet.EMPTY - override fun spaceExistenceTypeBetweenTokens(left: ASTNode?, right: ASTNode?) = ParserDefinition.SpaceRequirements.MAY + override fun spaceExistenceTypeBetweenTokens( + left: ASTNode?, + right: ASTNode?, + ) = ParserDefinition.SpaceRequirements.MAY override fun createFile(viewProvider: FileViewProvider) = MakefileFile(viewProvider) override fun createParser(project: Project?) = MakefileParser() + override fun createLexer(project: Project?) = MakefileLexerAdapter() override fun createElement(node: ASTNode?) = MakefileTypes.Factory.createElement(node) diff --git a/src/main/kotlin/name/kropp/intellij/makefile/MakefileStructureViewElement.kt b/src/main/kotlin/name/kropp/intellij/makefile/MakefileStructureViewElement.kt index 64db555..79f92df 100644 --- a/src/main/kotlin/name/kropp/intellij/makefile/MakefileStructureViewElement.kt +++ b/src/main/kotlin/name/kropp/intellij/makefile/MakefileStructureViewElement.kt @@ -22,6 +22,7 @@ class MakefileStructureViewElement(private val element: PsiElement) : StructureV } override fun canNavigate() = (element as? NavigationItem)?.canNavigate() ?: false + override fun canNavigateToSource() = (element as? NavigationItem)?.canNavigateToSource() ?: false override fun navigate(requestFocus: Boolean) { diff --git a/src/main/kotlin/name/kropp/intellij/makefile/MakefileStructureViewFactory.kt b/src/main/kotlin/name/kropp/intellij/makefile/MakefileStructureViewFactory.kt index fed7b07..164185c 100644 --- a/src/main/kotlin/name/kropp/intellij/makefile/MakefileStructureViewFactory.kt +++ b/src/main/kotlin/name/kropp/intellij/makefile/MakefileStructureViewFactory.kt @@ -6,7 +6,8 @@ import com.intellij.openapi.editor.Editor import com.intellij.psi.PsiFile class MakefileStructureViewFactory : PsiStructureViewFactory { - override fun getStructureViewBuilder(psiFile: PsiFile) = object : TreeBasedStructureViewBuilder() { - override fun createStructureViewModel(editor: Editor?) = MakefileStructureViewModel(psiFile) - } + override fun getStructureViewBuilder(psiFile: PsiFile) = + object : TreeBasedStructureViewBuilder() { + override fun createStructureViewModel(editor: Editor?) = MakefileStructureViewModel(psiFile) + } } diff --git a/src/main/kotlin/name/kropp/intellij/makefile/MakefileSyntaxHighlighter.kt b/src/main/kotlin/name/kropp/intellij/makefile/MakefileSyntaxHighlighter.kt index cb5d070..757bd6e 100644 --- a/src/main/kotlin/name/kropp/intellij/makefile/MakefileSyntaxHighlighter.kt +++ b/src/main/kotlin/name/kropp/intellij/makefile/MakefileSyntaxHighlighter.kt @@ -44,22 +44,23 @@ class MakefileSyntaxHighlighter : SyntaxHighlighterBase() { private val EMPTY_KEYS = emptyArray() } - override fun getTokenHighlights(tokenType: IElementType) = when (tokenType) { - MakefileTypes.DOC_COMMENT -> DOCCOMMENT_KEYS - MakefileTypes.COMMENT -> COMMENT_KEYS - MakefileTypes.TARGET -> TARGET_KEYS - MakefileTypes.COLON, MakefileTypes.DOUBLECOLON, MakefileTypes.ASSIGN, MakefileTypes.SEMICOLON, MakefileTypes.PIPE -> SEPARATOR_KEYS - MakefileTypes.KEYWORD_INCLUDE, MakefileTypes.KEYWORD_IFEQ, MakefileTypes.KEYWORD_IFNEQ, MakefileTypes.KEYWORD_IFDEF, MakefileTypes.KEYWORD_IFNDEF, MakefileTypes.KEYWORD_ELSE, MakefileTypes.KEYWORD_ENDIF, MakefileTypes.KEYWORD_DEFINE, MakefileTypes.KEYWORD_ENDEF, MakefileTypes.KEYWORD_UNDEFINE, MakefileTypes.KEYWORD_OVERRIDE, MakefileTypes.KEYWORD_EXPORT, MakefileTypes.KEYWORD_PRIVATE, MakefileTypes.KEYWORD_VPATH -> KEYWORD_KEYS - MakefileTypes.PREREQUISITE -> PREREQUISITE_KEYS - MakefileTypes.VARIABLE -> VARIABLE_KEYS - MakefileTypes.VARIABLE_VALUE -> VARIABLE_VALUE_KEYS - MakefileTypes.SPLIT -> LINE_SPLIT_KEYS - MakefileTypes.TAB -> TAB_KEYS - MakefileTypes.FUNCTION_PARAM_TEXT -> FUNCTION_PARAM_KEYS - MakefileTypes.VARIABLE_USAGE -> VARIABLE_USAGE_KEYS - TokenType.BAD_CHARACTER -> BAD_CHAR_KEYS - else -> EMPTY_KEYS - } + override fun getTokenHighlights(tokenType: IElementType) = + when (tokenType) { + MakefileTypes.DOC_COMMENT -> DOCCOMMENT_KEYS + MakefileTypes.COMMENT -> COMMENT_KEYS + MakefileTypes.TARGET -> TARGET_KEYS + MakefileTypes.COLON, MakefileTypes.DOUBLECOLON, MakefileTypes.ASSIGN, MakefileTypes.SEMICOLON, MakefileTypes.PIPE -> SEPARATOR_KEYS + MakefileTypes.KEYWORD_INCLUDE, MakefileTypes.KEYWORD_IFEQ, MakefileTypes.KEYWORD_IFNEQ, MakefileTypes.KEYWORD_IFDEF, MakefileTypes.KEYWORD_IFNDEF, MakefileTypes.KEYWORD_ELSE, MakefileTypes.KEYWORD_ENDIF, MakefileTypes.KEYWORD_DEFINE, MakefileTypes.KEYWORD_ENDEF, MakefileTypes.KEYWORD_UNDEFINE, MakefileTypes.KEYWORD_OVERRIDE, MakefileTypes.KEYWORD_EXPORT, MakefileTypes.KEYWORD_PRIVATE, MakefileTypes.KEYWORD_VPATH -> KEYWORD_KEYS + MakefileTypes.PREREQUISITE -> PREREQUISITE_KEYS + MakefileTypes.VARIABLE -> VARIABLE_KEYS + MakefileTypes.VARIABLE_VALUE -> VARIABLE_VALUE_KEYS + MakefileTypes.SPLIT -> LINE_SPLIT_KEYS + MakefileTypes.TAB -> TAB_KEYS + MakefileTypes.FUNCTION_PARAM_TEXT -> FUNCTION_PARAM_KEYS + MakefileTypes.VARIABLE_USAGE -> VARIABLE_USAGE_KEYS + TokenType.BAD_CHARACTER -> BAD_CHAR_KEYS + else -> EMPTY_KEYS + } override fun getHighlightingLexer() = MakefileLexerAdapter() } diff --git a/src/main/kotlin/name/kropp/intellij/makefile/MakefileSyntaxHighlighterFactory.kt b/src/main/kotlin/name/kropp/intellij/makefile/MakefileSyntaxHighlighterFactory.kt index 28b23f1..50c5fa0 100644 --- a/src/main/kotlin/name/kropp/intellij/makefile/MakefileSyntaxHighlighterFactory.kt +++ b/src/main/kotlin/name/kropp/intellij/makefile/MakefileSyntaxHighlighterFactory.kt @@ -5,5 +5,8 @@ import com.intellij.openapi.project.Project import com.intellij.openapi.vfs.VirtualFile class MakefileSyntaxHighlighterFactory : SyntaxHighlighterFactory() { - override fun getSyntaxHighlighter(project: Project?, virtualFile: VirtualFile?) = MakefileSyntaxHighlighter() + override fun getSyntaxHighlighter( + project: Project?, + virtualFile: VirtualFile?, + ) = MakefileSyntaxHighlighter() } diff --git a/src/main/kotlin/name/kropp/intellij/makefile/MakefileTargetKeyIndex.kt b/src/main/kotlin/name/kropp/intellij/makefile/MakefileTargetKeyIndex.kt index aab3e2c..831f3b8 100644 --- a/src/main/kotlin/name/kropp/intellij/makefile/MakefileTargetKeyIndex.kt +++ b/src/main/kotlin/name/kropp/intellij/makefile/MakefileTargetKeyIndex.kt @@ -1,3 +1,5 @@ +@file:Suppress("ktlint:standard:no-wildcard-imports") + package name.kropp.intellij.makefile import com.intellij.openapi.project.Project @@ -5,7 +7,7 @@ import com.intellij.psi.search.GlobalSearchScope import com.intellij.psi.stubs.StringStubIndexExtension import com.intellij.psi.stubs.StubIndex import com.intellij.psi.stubs.StubIndexKey -import com.intellij.util.* // ktlint-disable no-wildcard-imports +import com.intellij.util.* import name.kropp.intellij.makefile.psi.MakefileTarget val TARGET_INDEX_KEY = StubIndexKey.createIndexKey("makefile.target.index") @@ -19,6 +21,9 @@ object MakefileTargetIndex : StringStubIndexExtension() { override fun getKey(): StubIndexKey = TARGET_INDEX_KEY - override fun get(key: String, project: Project, scope: GlobalSearchScope): Collection = - StubIndex.getElements(TARGET_INDEX_KEY, key, project, scope, MakefileTarget::class.java) + override fun get( + key: String, + project: Project, + scope: GlobalSearchScope, + ): Collection = StubIndex.getElements(TARGET_INDEX_KEY, key, project, scope, MakefileTarget::class.java) } diff --git a/src/main/kotlin/name/kropp/intellij/makefile/MakefileTargetReference.kt b/src/main/kotlin/name/kropp/intellij/makefile/MakefileTargetReference.kt index 3d89329..7f760d8 100644 --- a/src/main/kotlin/name/kropp/intellij/makefile/MakefileTargetReference.kt +++ b/src/main/kotlin/name/kropp/intellij/makefile/MakefileTargetReference.kt @@ -1,3 +1,5 @@ +@file:Suppress("ktlint:standard:no-wildcard-imports") + package name.kropp.intellij.makefile import com.intellij.codeInsight.lookup.LookupElementBuilder @@ -5,16 +7,16 @@ import com.intellij.openapi.util.TextRange import com.intellij.psi.PsiElement import com.intellij.psi.PsiElementResolveResult import com.intellij.psi.PsiReference -import name.kropp.intellij.makefile.psi.* // ktlint-disable no-wildcard-imports +import name.kropp.intellij.makefile.psi.* class MakefileTargetReference(private val prerequisite: MakefilePrerequisite) : PsiReference { override fun getElement() = prerequisite + override fun getRangeInElement() = TextRange.create(0, element.textLength) + override fun bindToElement(element: PsiElement): PsiElement? = null - override fun isReferenceTo( - element: PsiElement - ): Boolean { + override fun isReferenceTo(element: PsiElement): Boolean { if (element is MakefileTarget) { return element.matches(prerequisite.text) } @@ -45,7 +47,12 @@ class MakefileTargetReference(private val prerequisite: MakefilePrerequisite) : get() = prerequisite.parent.parent.parent.parent as? MakefileRule override fun getVariants() = - (prerequisite.containingFile as MakefileFile).targets.filterNot { it.isPatternTarget || rule?.targets?.any { t -> t.name == it.name } == true }.distinctBy { it.name }.map { + (prerequisite.containingFile as MakefileFile).targets.filterNot { + it.isPatternTarget || rule?.targets?.any { + t -> + t.name == it.name + } == true + }.distinctBy { it.name }.map { LookupElementBuilder.create(it).withIcon(MakefileTargetIcon) }.toTypedArray() diff --git a/src/main/kotlin/name/kropp/intellij/makefile/MakefileTargetStructureViewPresentation.kt b/src/main/kotlin/name/kropp/intellij/makefile/MakefileTargetStructureViewPresentation.kt index b9ba066..466cb1c 100644 --- a/src/main/kotlin/name/kropp/intellij/makefile/MakefileTargetStructureViewPresentation.kt +++ b/src/main/kotlin/name/kropp/intellij/makefile/MakefileTargetStructureViewPresentation.kt @@ -5,6 +5,8 @@ import name.kropp.intellij.makefile.psi.MakefileTarget class MakefileTargetStructureViewPresentation(private val target: MakefileTarget) : ItemPresentation { override fun getIcon(b: Boolean) = MakefileTargetIcon + override fun getPresentableText() = target.text + override fun getLocationString() = "" } diff --git a/src/main/kotlin/name/kropp/intellij/makefile/RemoveRuleFix.kt b/src/main/kotlin/name/kropp/intellij/makefile/RemoveRuleFix.kt index 75d1012..d4f3b3f 100644 --- a/src/main/kotlin/name/kropp/intellij/makefile/RemoveRuleFix.kt +++ b/src/main/kotlin/name/kropp/intellij/makefile/RemoveRuleFix.kt @@ -9,11 +9,20 @@ import name.kropp.intellij.makefile.psi.MakefileRule class RemoveRuleFix(private val rule: MakefileRule) : BaseIntentionAction() { override fun getText() = "Remove Empty Rule" + override fun getFamilyName() = "Remove Empty Rule" - override fun isAvailable(project: Project, editor: Editor?, psiFile: PsiFile?) = true + override fun isAvailable( + project: Project, + editor: Editor?, + psiFile: PsiFile?, + ) = true - override fun invoke(project: Project, editor: Editor?, psiFile: PsiFile?) { + override fun invoke( + project: Project, + editor: Editor?, + psiFile: PsiFile?, + ) { WriteCommandAction.writeCommandAction(project, psiFile).run { rule.delete() } diff --git a/src/main/kotlin/name/kropp/intellij/makefile/psi/MakefileElementFactory.kt b/src/main/kotlin/name/kropp/intellij/makefile/psi/MakefileElementFactory.kt index ea7714c..ae0523a 100644 --- a/src/main/kotlin/name/kropp/intellij/makefile/psi/MakefileElementFactory.kt +++ b/src/main/kotlin/name/kropp/intellij/makefile/psi/MakefileElementFactory.kt @@ -7,18 +7,33 @@ import name.kropp.intellij.makefile.MakefileFile import name.kropp.intellij.makefile.MakefileFileType object MakefileElementFactory { - fun createFile(project: Project, text: String) = - PsiFileFactory.getInstance(project).createFileFromText("Makefile", MakefileFileType.INSTANCE, text) as MakefileFile + fun createFile( + project: Project, + text: String, + ) = PsiFileFactory.getInstance(project).createFileFromText("Makefile", MakefileFileType.INSTANCE, text) as MakefileFile - fun createRule(project: Project, target: String) = - createFile(project, "$target:\n").firstChild as MakefileRule + fun createRule( + project: Project, + target: String, + ) = createFile(project, "$target:\n").firstChild as MakefileRule - fun createTarget(project: Project, name: String) = - createRule(project, name).firstChild.firstChild.firstChild as MakefileTarget + fun createTarget( + project: Project, + name: String, + ) = createRule(project, name).firstChild.firstChild.firstChild as MakefileTarget - fun createPrerequisite(project: Project, name: String) = - (createFile(project, "a: $name").firstChild as MakefileRule).targetLine.prerequisites!!.normalPrerequisites.firstChild as MakefilePrerequisite + fun createPrerequisite( + project: Project, + name: String, + ) = ( + createFile( + project, + "a: $name", + ).firstChild as MakefileRule + ).targetLine.prerequisites!!.normalPrerequisites.firstChild as MakefilePrerequisite - fun createWhiteSpace(project: Project, whitespace: String) = - createFile(project, whitespace).firstChild as PsiWhiteSpace + fun createWhiteSpace( + project: Project, + whitespace: String, + ) = createFile(project, whitespace).firstChild as PsiWhiteSpace } diff --git a/src/main/kotlin/name/kropp/intellij/makefile/psi/MakefilePrerequisiteManipulator.kt b/src/main/kotlin/name/kropp/intellij/makefile/psi/MakefilePrerequisiteManipulator.kt index f2fa2a6..6af17bf 100644 --- a/src/main/kotlin/name/kropp/intellij/makefile/psi/MakefilePrerequisiteManipulator.kt +++ b/src/main/kotlin/name/kropp/intellij/makefile/psi/MakefilePrerequisiteManipulator.kt @@ -5,5 +5,9 @@ import com.intellij.psi.AbstractElementManipulator import name.kropp.intellij.makefile.psi.impl.MakefilePrerequisiteImpl class MakefilePrerequisiteManipulator : AbstractElementManipulator() { - override fun handleContentChange(element: MakefilePrerequisiteImpl, textRange: TextRange, newContent: String?) = element.updateText(newContent) + override fun handleContentChange( + element: MakefilePrerequisiteImpl, + textRange: TextRange, + newContent: String?, + ) = element.updateText(newContent) } diff --git a/src/main/kotlin/name/kropp/intellij/makefile/psi/MakefileTargetPresentation.kt b/src/main/kotlin/name/kropp/intellij/makefile/psi/MakefileTargetPresentation.kt index 0ffcb71..32077ab 100644 --- a/src/main/kotlin/name/kropp/intellij/makefile/psi/MakefileTargetPresentation.kt +++ b/src/main/kotlin/name/kropp/intellij/makefile/psi/MakefileTargetPresentation.kt @@ -5,6 +5,8 @@ import name.kropp.intellij.makefile.MakefileTargetIcon class MakefileTargetPresentation(private val target: MakefileTarget) : ItemPresentation { override fun getIcon(b: Boolean) = MakefileTargetIcon + override fun getPresentableText() = target.text + override fun getLocationString() = "in " + target.containingFile?.virtualFile?.presentableName } diff --git a/src/main/kotlin/name/kropp/intellij/makefile/stub/MakefileTargetStubElementImpl.kt b/src/main/kotlin/name/kropp/intellij/makefile/stub/MakefileTargetStubElementImpl.kt index 4ce07e3..64b5d39 100644 --- a/src/main/kotlin/name/kropp/intellij/makefile/stub/MakefileTargetStubElementImpl.kt +++ b/src/main/kotlin/name/kropp/intellij/makefile/stub/MakefileTargetStubElementImpl.kt @@ -4,4 +4,10 @@ import com.intellij.psi.stubs.NamedStubBase import com.intellij.psi.stubs.StubElement import name.kropp.intellij.makefile.psi.MakefileTarget -class MakefileTargetStubElementImpl(parent: StubElement<*>?, name: String?) : NamedStubBase(parent, MakefileTargetStubElementType, name), MakefileTargetStubElement +class MakefileTargetStubElementImpl(parent: StubElement<*>?, name: String?) : + NamedStubBase( + parent, + MakefileTargetStubElementType, + name, + ), + MakefileTargetStubElement diff --git a/src/main/kotlin/name/kropp/intellij/makefile/stub/MakefileTargetStubElementType.kt b/src/main/kotlin/name/kropp/intellij/makefile/stub/MakefileTargetStubElementType.kt index 71bd2d7..039558b 100644 --- a/src/main/kotlin/name/kropp/intellij/makefile/stub/MakefileTargetStubElementType.kt +++ b/src/main/kotlin/name/kropp/intellij/makefile/stub/MakefileTargetStubElementType.kt @@ -16,16 +16,29 @@ object MakefileTargetStubElementType : IStubElementType?) = MakefileTargetStubElementImpl(parent, psi.name) + override fun createStub( + psi: MakefileTarget, + parent: StubElement<*>?, + ) = MakefileTargetStubElementImpl(parent, psi.name) + override fun createPsi(stub: MakefileTargetStubElement) = MakefileTargetImpl(stub, stub.stubType) - override fun indexStub(stub: MakefileTargetStubElement, sink: IndexSink) { + override fun indexStub( + stub: MakefileTargetStubElement, + sink: IndexSink, + ) { sink.occurrence(TARGET_INDEX_KEY, stub.name!!) } - override fun serialize(e: MakefileTargetStubElement, outputStream: StubOutputStream) { + override fun serialize( + e: MakefileTargetStubElement, + outputStream: StubOutputStream, + ) { outputStream.writeName(e.name) } - override fun deserialize(inputStream: StubInputStream, parent: StubElement<*>?) = - MakefileTargetStubElementImpl(parent, inputStream.readName()?.string) + + override fun deserialize( + inputStream: StubInputStream, + parent: StubElement<*>?, + ) = MakefileTargetStubElementImpl(parent, inputStream.readName()?.string) } diff --git a/src/main/kotlin/name/kropp/intellij/makefile/utils.kt b/src/main/kotlin/name/kropp/intellij/makefile/utils.kt index fabbee4..4817a4e 100644 --- a/src/main/kotlin/name/kropp/intellij/makefile/utils.kt +++ b/src/main/kotlin/name/kropp/intellij/makefile/utils.kt @@ -8,7 +8,9 @@ import name.kropp.intellij.makefile.psi.MakefileTarget fun findAllTargets(project: Project) = MakefileTargetIndex.getAllKeys(project) -fun findTargets(project: Project, name: String): Collection = - MakefileTargetIndex.get(name, project, GlobalSearchScope.allScope(project)) +fun findTargets( + project: Project, + name: String, +): Collection = MakefileTargetIndex.get(name, project, GlobalSearchScope.allScope(project)) fun findTargets(psiFile: PsiFile) = PsiTreeUtil.findChildrenOfType(psiFile, MakefileTarget::class.java).asIterable() diff --git a/src/main/resources/META-INF/plugin.xml b/src/main/resources/META-INF/plugin.xml index cd12321..c8a2cc6 100644 --- a/src/main/resources/META-INF/plugin.xml +++ b/src/main/resources/META-INF/plugin.xml @@ -1,18 +1,11 @@ com.intuit.intellij.makefile DockDockBuild - 2.2.5 Intuit Support for running UNIX Makefiles on a Docker container - -

  • Support for IntelliJ 2023.2
  • - - ]]> - - + com.intellij.modules.lang com.intellij.modules.platform @@ -31,7 +24,7 @@ - + diff --git a/src/test/kotlin/MakefileCompletionTest.kt b/src/test/kotlin/MakefileCompletionTest.kt index a954adf..b04acb6 100644 --- a/src/test/kotlin/MakefileCompletionTest.kt +++ b/src/test/kotlin/MakefileCompletionTest.kt @@ -1,6 +1,8 @@ + class MakefileCompletionTest : DockDockTestCase() { fun testSimple() = doTest("b", "c", "d", "${getTestName(true)}.mk") + fun testTargets() = doTest("a", "${getTestName(true)}.mk") fun doTest(vararg variants: String) = myFixture.testCompletionVariants("$basePath/${getTestName(true)}.mk", *variants) diff --git a/src/test/kotlin/MakefileCreateRuleQuickfixTest.kt b/src/test/kotlin/MakefileCreateRuleQuickfixTest.kt index 2b79bb2..f423104 100644 --- a/src/test/kotlin/MakefileCreateRuleQuickfixTest.kt +++ b/src/test/kotlin/MakefileCreateRuleQuickfixTest.kt @@ -1,6 +1,8 @@ + class MakefileCreateRuleQuickfixTest : DockDockTestCase() { fun testSimple() = doTest() + fun testMiddle() = doTest() fun doTest() { diff --git a/src/test/kotlin/MakefileFindUsagesTest.kt b/src/test/kotlin/MakefileFindUsagesTest.kt index f8db3d3..ad83be7 100644 --- a/src/test/kotlin/MakefileFindUsagesTest.kt +++ b/src/test/kotlin/MakefileFindUsagesTest.kt @@ -13,11 +13,16 @@ class MakefileFindUsagesTest : DockDockTestCase() { } fun testPhony() = notSearchableForUsages() + fun testForce() = notSearchableForUsages() fun notSearchableForUsages() { myFixture.configureByFiles("$basePath/${getTestName(true)}.mk") - val targetElement = TargetElementUtil.findTargetElement(myFixture.editor, TargetElementUtil.ELEMENT_NAME_ACCEPTED or TargetElementUtil.REFERENCED_ELEMENT_ACCEPTED) + val targetElement = + TargetElementUtil.findTargetElement( + myFixture.editor, + TargetElementUtil.ELEMENT_NAME_ACCEPTED or TargetElementUtil.REFERENCED_ELEMENT_ACCEPTED, + ) val handler = (FindManager.getInstance(project) as FindManagerImpl).findUsagesManager.getFindUsagesHandler(targetElement!!, false) assertThat(handler, nullValue()) diff --git a/src/test/kotlin/MakefileFoldingTest.kt b/src/test/kotlin/MakefileFoldingTest.kt index 8d17355..04e8978 100644 --- a/src/test/kotlin/MakefileFoldingTest.kt +++ b/src/test/kotlin/MakefileFoldingTest.kt @@ -1,7 +1,10 @@ + class MakefileFoldingTest : DockDockTestCase() { fun testRule() = doTest() + fun testVariable() = doTest() + fun testDefine() = doTest() fun doTest() = myFixture.testFolding("$testDataPath/$basePath/${getTestName(true)}.mk") diff --git a/src/test/kotlin/MakefileHighlightingTest.kt b/src/test/kotlin/MakefileHighlightingTest.kt index 14769aa..afaa29b 100644 --- a/src/test/kotlin/MakefileHighlightingTest.kt +++ b/src/test/kotlin/MakefileHighlightingTest.kt @@ -1,13 +1,18 @@ + class MakefileHighlightingTest : DockDockTestCase() { fun testUnresolved() = doTest() + fun testRedundant() = doTest(true) + fun testTargetspecificvars() = doTest() fun doTest(checkInfos: Boolean = false) { myFixture.testHighlighting( - true, checkInfos, true, - "$basePath/${getTestName(true)}.mk" + true, + checkInfos, + true, + "$basePath/${getTestName(true)}.mk", ) } diff --git a/src/test/kotlin/MakefileParserTest.kt b/src/test/kotlin/MakefileParserTest.kt index b141881..7932a4a 100644 --- a/src/test/kotlin/MakefileParserTest.kt +++ b/src/test/kotlin/MakefileParserTest.kt @@ -3,47 +3,87 @@ import name.kropp.intellij.makefile.MakefileParserDefinition class MakefileParserTest : ParsingTestCase("parser", "mk", MakefileParserDefinition()) { fun testHelloWorld() = doTest(true) + fun testVariables() = doTest(true) + fun testInclude() = doTest(true) + fun testConditionals() = doTest(true) + fun testConditionalsInsideRecipe() = doTest(true) + fun testConditionalVars() = doTest(true) + fun testConditionalAfterRecipe() = doTest(true) + fun testPrerequisites() = doTest(true) + fun testMultipleTargets() = doTest(true) + fun testDefine() = doTest(true) + fun testEmptyRecipe() = doTest(true) + fun testRecipeOnTheSameLine() = doTest(true) + fun testDirectives() = doTest(true) + fun testExport() = doTest(true) + fun testVPath() = doTest(true) + fun testComments() = doTest(true) + fun testMultiline() = doTest(true) + fun testTargetInsideConditional() = doTest(true) + fun testTargetSpecificVariable() = doTest(true) + fun testWildcard() = doTest(true) + fun testDoubleColonRule() = doTest(true) + fun testStaticPatternRules() = doTest(true) + fun testDocuments() = doTest(true) + fun testFunctions() = doTest(true) + fun testAtSign() = doTest(true) + fun testElseif() = doTest(true) + fun testFunctionInPrerequisites() = doTest(true) fun testIssue7() = doTest(true) + fun testIssue9() = doTest(true) + fun testIssue15() = doTest(true) + fun testIssue23() = doTest(true) + fun testIssue36() = doTest(true) + fun testIssue37() = doTest(true) + fun testIssue44() = doTest(true) + fun testIssue45() = doTest(true) + fun testIssue46() = doTest(true) + fun testIssue56() = doTest(true) + fun testIssue61() = doTest(true) + fun testIssue62() = doTest(true) + fun testIssue63() = doTest(true) + fun testIssue81() = doTest(true) + fun testIssue88() = doTest(true) override fun getTestDataPath() = "testData" diff --git a/src/test/kotlin/MakefileRemoveRuleQuickfixTest.kt b/src/test/kotlin/MakefileRemoveRuleQuickfixTest.kt index 06f1591..d3e3192 100644 --- a/src/test/kotlin/MakefileRemoveRuleQuickfixTest.kt +++ b/src/test/kotlin/MakefileRemoveRuleQuickfixTest.kt @@ -1,6 +1,8 @@ + class MakefileRemoveRuleQuickfixTest : DockDockTestCase() { fun testSingle() = doTest() + fun testTwo() = doTest() fun doTest() { diff --git a/src/test/kotlin/MakefileTargetRenameTest.kt b/src/test/kotlin/MakefileTargetRenameTest.kt index 8135f16..799401f 100644 --- a/src/test/kotlin/MakefileTargetRenameTest.kt +++ b/src/test/kotlin/MakefileTargetRenameTest.kt @@ -1,4 +1,5 @@ + class MakefileTargetRenameTest : DockDockTestCase() { fun testSimple() = doTest("qwerty") diff --git a/src/test/kotlin/com/intuit/ddb/CmdProcessBuilderTest.kt b/src/test/kotlin/com/intuit/ddb/CmdProcessBuilderTest.kt new file mode 100644 index 0000000..0d0f171 --- /dev/null +++ b/src/test/kotlin/com/intuit/ddb/CmdProcessBuilderTest.kt @@ -0,0 +1,160 @@ +package com.intuit.ddb + +import org.junit.Assert.assertEquals +import org.junit.Assert.assertTrue +import org.junit.Test + +class CmdProcessBuilderTest { + // ---- buildDockerBuildCmd ---- + + @Test + fun `buildDockerBuildCmd produces correct command`() { + val cmd = buildDockerBuildCmd("/usr/bin/docker", "my-image") + assertEquals(listOf("/usr/bin/docker", "build", ".", "--tag", "my-image"), cmd) + } + + @Test + fun `buildDockerBuildCmd uses default tag`() { + val cmd = buildDockerBuildCmd("/usr/bin/docker", "build") + assertEquals("build", cmd.last()) + } + + // ---- buildDockerPullCmd ---- + + @Test + fun `buildDockerPullCmd produces correct command`() { + val cmd = buildDockerPullCmd("/usr/bin/docker", "registry/image:v1") + assertEquals(listOf("/usr/bin/docker", "pull", "registry/image:v1"), cmd) + } + + // ---- buildDockerRunCmd ---- + + @Test + fun `buildDockerRunCmd basic command without env script and root makefile dir`() { + val cmd = + buildDockerRunCmd( + dockerPath = "/usr/bin/docker", + codeVol = "/code:/home/ddb", + mavenVol = "/root/.m2:/root/.m2", + tag = "build", + envScript = null, + runDir = ".", + target = "all", + makefileFile = "Makefile", + advancedDockerSettings = null, + ) + + assertEquals("/usr/bin/docker", cmd[0]) + assertEquals("run", cmd[1]) + assertTrue(cmd.contains("--rm")) + assertTrue(cmd.contains("-t")) + assertTrue(cmd.contains("--volume")) + assertTrue(cmd.contains("/code:/home/ddb")) + assertTrue(cmd.contains("/root/.m2:/root/.m2")) + assertEquals("build", cmd[cmd.indexOf("bash") - 1]) + assertEquals("bash", cmd[cmd.size - 3]) + assertEquals("-c", cmd[cmd.size - 2]) + assertEquals("make -r -f 'Makefile' 'all'", cmd.last()) + } + + @Test + fun `buildDockerRunCmd includes cd when runDir is not dot`() { + val cmd = + buildDockerRunCmd( + dockerPath = "/usr/bin/docker", + codeVol = "/code:/home/ddb", + mavenVol = "/root/.m2:/root/.m2", + tag = "build", + envScript = null, + runDir = "subdir", + target = "test", + makefileFile = "Makefile", + advancedDockerSettings = null, + ) + + val innerCmd = cmd.last() + assertTrue("Expected cd in inner command", innerCmd.contains("cd 'subdir' &&")) + assertTrue(innerCmd.endsWith("make -r -f 'Makefile' 'test'")) + } + + @Test + fun `buildDockerRunCmd skips cd when runDir is dot`() { + val cmd = + buildDockerRunCmd( + dockerPath = "/usr/bin/docker", + codeVol = "/code:/home/ddb", + mavenVol = "/root/.m2:/root/.m2", + tag = "build", + envScript = null, + runDir = ".", + target = "test", + makefileFile = "Makefile", + advancedDockerSettings = null, + ) + + assertTrue("cd should not appear when runDir is '.'", !cmd.last().contains("cd")) + } + + @Test + fun `buildDockerRunCmd includes source when envScript provided`() { + val cmd = + buildDockerRunCmd( + dockerPath = "/usr/bin/docker", + codeVol = "/code:/home/ddb", + mavenVol = "/root/.m2:/root/.m2", + tag = "build", + envScript = "set_env.sh", + runDir = ".", + target = "build", + makefileFile = "Makefile", + advancedDockerSettings = null, + ) + + assertTrue(cmd.last().startsWith("source 'set_env.sh' &&")) + } + + @Test + fun `buildDockerRunCmd splits advancedDockerSettings into separate args`() { + val cmd = + buildDockerRunCmd( + dockerPath = "/usr/bin/docker", + codeVol = "/code:/home/ddb", + mavenVol = "/root/.m2:/root/.m2", + tag = "build", + envScript = null, + runDir = ".", + target = "build", + makefileFile = "Makefile", + advancedDockerSettings = "--memory 2g --cpus 4", + ) + + assertTrue(cmd.contains("--memory")) + assertTrue(cmd.contains("2g")) + assertTrue(cmd.contains("--cpus")) + assertTrue(cmd.contains("4")) + // tag comes after advanced settings, before bash + val tagIndex = cmd.indexOf("build") + assertTrue(cmd.indexOf("2g") < tagIndex) + } + + @Test + fun `buildDockerRunCmd full command with all options`() { + val cmd = + buildDockerRunCmd( + dockerPath = "/usr/bin/docker", + codeVol = "/code:/home/ddb", + mavenVol = "/home/.m2:/root/.m2", + tag = "registry/img:v2", + envScript = "scripts/env.sh", + runDir = "services/api", + target = "package", + makefileFile = "Makefile", + advancedDockerSettings = "--network host", + ) + + val innerCmd = cmd.last() + assertTrue(innerCmd.startsWith("source 'scripts/env.sh' &&")) + assertTrue(innerCmd.contains("cd 'services/api' &&")) + assertTrue(innerCmd.endsWith("make -r -f 'Makefile' 'package'")) + } +} diff --git a/src/test/kotlin/com/intuit/ddb/DockDockBuildConfigurableTest.kt b/src/test/kotlin/com/intuit/ddb/DockDockBuildConfigurableTest.kt new file mode 100644 index 0000000..339de51 --- /dev/null +++ b/src/test/kotlin/com/intuit/ddb/DockDockBuildConfigurableTest.kt @@ -0,0 +1,55 @@ +package com.intuit.ddb + +import com.intellij.testFramework.fixtures.BasePlatformTestCase +import com.intuit.ddb.conf.DockDockBuildProjectSettings + +class DockDockBuildConfigurableTest : BasePlatformTestCase() { + private fun configurable() = DockDockBuildConfigurable(project) + + private fun settings() = project.getService(DockDockBuildProjectSettings::class.java).settings + + fun testCreateComponentReturnsNonNullPanel() { + assertNotNull(configurable().createComponent()) + } + + fun testGetDisplayNameReturnsPluginName() { + assertEquals(PLUGIN_NAME, configurable().getDisplayName()) + } + + fun testIsModifiedFalseAfterReset() { + val configurable = configurable() + configurable.createComponent() + configurable.reset() + assertFalse(configurable.isModified) + } + + fun testResetPopulatesFieldsFromSettings() { + settings().dockerPath = "/custom/docker" + settings().codePath = "/custom/code" + settings().mavenCachePath = "/custom/.m2" + settings().advancedDockerSettings = "--memory 4g" + + val configurable = configurable() + configurable.createComponent() + configurable.reset() + + assertFalse(configurable.isModified) + } + + fun testApplyRoundTripPreservesValues() { + settings().dockerPath = "/usr/bin/docker" + settings().codePath = "/home/user/project" + settings().mavenCachePath = "/home/user/.m2" + settings().advancedDockerSettings = "" + + val configurable = configurable() + configurable.createComponent() + configurable.reset() + configurable.apply() + + assertEquals("/usr/bin/docker", settings().dockerPath) + assertEquals("/home/user/project", settings().codePath) + assertEquals("/home/user/.m2", settings().mavenCachePath) + assertEquals("", settings().advancedDockerSettings) + } +} diff --git a/src/test/kotlin/com/intuit/ddb/DockDockBuildRunConfigurationTest.kt b/src/test/kotlin/com/intuit/ddb/DockDockBuildRunConfigurationTest.kt new file mode 100644 index 0000000..048f238 --- /dev/null +++ b/src/test/kotlin/com/intuit/ddb/DockDockBuildRunConfigurationTest.kt @@ -0,0 +1,108 @@ +package com.intuit.ddb + +import com.intellij.testFramework.fixtures.BasePlatformTestCase +import com.intuit.ddb.conf.DockDockBuildRunConfiguration +import com.intuit.ddb.conf.DockDockBuildRunConfigurationEditor +import com.intuit.ddb.conf.DockDockBuildRunConfigurationFactory +import com.intuit.ddb.conf.DockDockBuildRunConfigurationType +import org.jdom.Element + +class DockDockBuildRunConfigurationTest : BasePlatformTestCase() { + private fun factory() = DockDockBuildRunConfigurationFactory(DockDockBuildRunConfigurationType) + + private fun config(name: String = "test") = DockDockBuildRunConfiguration(project, factory(), name) + + // ---- basic properties ---- + + fun testDefaultFieldValuesAreEmptyStrings() { + val config = config() + assertEquals("", config.makefileFilePath) + assertEquals("", config.dockerfileDir) + assertEquals("", config.dockerImageUrl) + assertFalse(config.isDockerImage) + assertEquals("", config.target) + assertEquals("", config.envScriptPath) + } + + fun testGetConfigurationEditorReturnsEditorInstance() { + assertInstanceOf(config().getConfigurationEditor(), DockDockBuildRunConfigurationEditor::class.java) + } + + // ---- writeExternal / readExternal round-trip ---- + + fun testWriteAndReadExternalRoundTripPreservesAllFields() { + val original = config("my-run") + original.makefileFilePath = "/project/Makefile" + original.dockerfileDir = "/project/docker" + original.dockerImageUrl = "registry/image:v1" + original.isDockerImage = false + original.isDockerfile = true + original.target = "build" + original.envScriptPath = "/project/set_env.sh" + + val element = Element("configuration") + original.writeExternal(element) + + val restored = config("my-run") + restored.readExternal(element) + + assertEquals(original.makefileFilePath, restored.makefileFilePath) + assertEquals(original.dockerfileDir, restored.dockerfileDir) + assertEquals(original.dockerImageUrl, restored.dockerImageUrl) + assertEquals(original.isDockerImage, restored.isDockerImage) + assertEquals(original.isDockerfile, restored.isDockerfile) + assertEquals(original.target, restored.target) + assertEquals(original.envScriptPath, restored.envScriptPath) + } + + fun testReadExternalOnEmptyElementLeavesDefaults() { + val config = config() + config.readExternal(Element("configuration")) + assertEquals("", config.makefileFilePath) + assertEquals("", config.target) + } + + // ---- editor applyEditorTo / resetEditorFrom round-trip ---- + + fun testEditorResetThenApplyPreservesValues() { + val config = config() + config.makefileFilePath = "/project/Makefile" + config.dockerfileDir = "/project/docker" + config.dockerImageUrl = "" + config.isDockerImage = false + config.isDockerfile = true + config.target = "package" + config.envScriptPath = "" + + val editor = DockDockBuildRunConfigurationEditor(project) + editor.resetFrom(config) + + val output = config("output") + editor.applyTo(output) + + assertEquals("/project/Makefile", output.makefileFilePath) + assertEquals("/project/docker", output.dockerfileDir) + assertEquals("package", output.target) + assertFalse(output.isDockerImage) + assertTrue(output.isDockerfile) + } + + fun testEditorDockerImageModePreservedThroughRoundTrip() { + val config = config() + config.dockerImageUrl = "my-registry/image:latest" + config.isDockerImage = true + config.isDockerfile = false + config.makefileFilePath = "/project/Makefile" + config.target = "run" + + val editor = DockDockBuildRunConfigurationEditor(project) + editor.resetFrom(config) + + val output = config("output") + editor.applyTo(output) + + assertEquals("my-registry/image:latest", output.dockerImageUrl) + assertTrue(output.isDockerImage) + assertFalse(output.isDockerfile) + } +} diff --git a/src/test/kotlin/com/intuit/ddb/DockDockBuildTest.kt b/src/test/kotlin/com/intuit/ddb/DockDockBuildTest.kt new file mode 100644 index 0000000..b3bdee5 --- /dev/null +++ b/src/test/kotlin/com/intuit/ddb/DockDockBuildTest.kt @@ -0,0 +1,44 @@ +package com.intuit.ddb + +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertNotEquals +import org.junit.Assert.assertTrue +import org.junit.Test +import java.io.File + +class DockDockBuildTest { + @Test + fun testGetDefaultDockerPathReturnsExistingFile() { + val path = getDefaultDockerPath() + // On any CI/dev machine with docker installed, the returned path should exist + // or fall back to "docker" (for PATH-based resolution) + assertTrue( + "Expected existing file or 'docker' fallback, got: $path", + path == "docker" || File(path).exists(), + ) + } + + @Test + fun testGetDefaultDockerPathNotEmpty() { + assertFalse(getDefaultDockerPath().isEmpty()) + } + + @Test + fun testGetMakefileFilenameExtractsName() { + assertEquals("Makefile", getMakefileFilename("/some/path/to/Makefile")) + assertEquals("build.mk", getMakefileFilename("/project/build.mk")) + } + + @Test + fun testGetDefaultDockerfileDirAppendsDocker() { + val result = getDefaultDockerfileDir("/project/subdir/Makefile") + assertEquals("/project/subdir/docker", result) + } + + @Test + fun testGetDefaultM2PathContainsDotM2() { + assertNotEquals("", getDefaultM2Path()) + assertTrue(getDefaultM2Path().endsWith("/.m2")) + } +} diff --git a/src/test/kotlin/com/intuit/ddb/ParametersTest.kt b/src/test/kotlin/com/intuit/ddb/ParametersTest.kt new file mode 100644 index 0000000..f625dc8 --- /dev/null +++ b/src/test/kotlin/com/intuit/ddb/ParametersTest.kt @@ -0,0 +1,102 @@ +package com.intuit.ddb + +import com.fasterxml.jackson.databind.ObjectMapper +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNull +import org.junit.Rule +import org.junit.Test +import org.junit.rules.TemporaryFolder + +class ParametersTest { + @get:Rule + val tmp = TemporaryFolder() + + private val mapper = ObjectMapper() + + private fun writeParamsFile(json: String): String { + val file = tmp.newFile("params.json") + file.writeText(json) + return file.absolutePath + } + + @Test + fun `readParams deserializes all fields`() { + val path = + writeParamsFile( + """ + { + "dockerExe": "/usr/bin/docker", + "dockerfileDir": "/project/docker", + "dockerImgUrl": "my-registry/image:latest", + "isImage": true, + "makefilePath": "subdir", + "makefileFile": "Makefile", + "target": "build", + "codePath": "/home/user/code", + "m2Path": "/home/user/.m2", + "envScript": "set_env.sh", + "advancedDockerSettings": "--memory 2g" + } + """.trimIndent(), + ) + + val p = Parameters.readParams(path) + + assertEquals("/usr/bin/docker", p.dockerExe) + assertEquals("/project/docker", p.dockerfileDir) + assertEquals("my-registry/image:latest", p.dockerImgUrl) + assertEquals(true, p.isImage) + assertEquals("subdir", p.makefilePath) + assertEquals("Makefile", p.makefileFile) + assertEquals("build", p.target) + assertEquals("/home/user/code", p.codePath) + assertEquals("/home/user/.m2", p.m2Path) + assertEquals("set_env.sh", p.envScript) + assertEquals("--memory 2g", p.advancedDockerSettings) + } + + @Test + fun `readParams handles missing optional fields as null`() { + val path = + writeParamsFile( + """ + { + "dockerExe": "/usr/bin/docker", + "isImage": false + } + """.trimIndent(), + ) + + val p = Parameters.readParams(path) + + assertEquals("/usr/bin/docker", p.dockerExe) + assertEquals(false, p.isImage) + assertNull(p.envScript) + assertNull(p.advancedDockerSettings) + assertNull(p.dockerfileDir) + } + + @Test + fun `Parameters round-trips through JSON serialization`() { + val original = + Parameters( + dockerExe = "/usr/bin/docker", + dockerfileDir = "/project/docker", + dockerImgUrl = null, + isImage = false, + makefilePath = ".", + makefileFile = "Makefile", + target = "test", + codePath = "/code", + m2Path = "/root/.m2", + envScript = null, + advancedDockerSettings = null, + ) + + val file = tmp.newFile("roundtrip.json") + mapper.writeValue(file, original) + val restored = Parameters.readParams(file.absolutePath) + + assertEquals(original, restored) + } +} diff --git a/src/uiTest/kotlin/com/intuit/ddb/DockDockBuildUiTest.kt b/src/uiTest/kotlin/com/intuit/ddb/DockDockBuildUiTest.kt new file mode 100644 index 0000000..ab44ee5 --- /dev/null +++ b/src/uiTest/kotlin/com/intuit/ddb/DockDockBuildUiTest.kt @@ -0,0 +1,149 @@ +package com.intuit.ddb + +import com.intellij.remoterobot.RemoteRobot +import com.intellij.remoterobot.fixtures.CommonContainerFixture +import com.intellij.remoterobot.fixtures.ComponentFixture +import com.intellij.remoterobot.search.locators.byXpath +import com.intellij.remoterobot.utils.waitFor +import org.junit.Assert.assertNotNull +import org.junit.Assert.assertTrue +import org.junit.Test +import java.time.Duration + +/** + * UI tests for DockDockBuild plugin. + * + * Requires a running IDE instance with the robot server plugin loaded. + * Start it first with: ./gradlew runIdeForUiTests + * Then run: ./gradlew uiTest + */ +class DockDockBuildUiTest { + private val robot = RemoteRobot("http://127.0.0.1:8082") + + /** Verifies the IDE starts up and is responsive. */ + @Test + fun testIdeStartsUp() { + waitFor(Duration.ofSeconds(30)) { + robot.findAll(byXpath("//div[@class='IdeFrameImpl']")).isNotEmpty() + } + val frame = + robot.find( + byXpath("//div[@class='IdeFrameImpl']"), + Duration.ofSeconds(10), + ) + assertNotNull(frame) + } + + /** + * Verifies the DockDockBuild run configuration type appears + * in the Run/Debug Configurations dialog. + */ + @Test + fun testRunConfigurationTypeRegistered() { + waitForIdeFrame() + + val menuBar = + robot.find( + byXpath("//div[@class='IdeFrameImpl']"), + Duration.ofSeconds(10), + ) + val runMenu = + menuBar.find( + byXpath("//div[@text='Run']"), + Duration.ofSeconds(5), + ) + runMenu.click() + + waitFor(Duration.ofSeconds(5)) { + robot.findAll(byXpath("//div[@text='Edit Configurations...']")).isNotEmpty() + } + robot.find( + byXpath("//div[@text='Edit Configurations...']"), + Duration.ofSeconds(5), + ).click() + + waitFor(Duration.ofSeconds(10)) { + robot.findAll(byXpath("//div[@title='Run/Debug Configurations']")).isNotEmpty() + } + val dialog = + robot.find( + byXpath("//div[@title='Run/Debug Configurations']"), + Duration.ofSeconds(10), + ) + + dialog.find( + byXpath("//div[@tooltiptext='Add New Run Configuration' or @tooltiptext='Add New Configuration']"), + Duration.ofSeconds(5), + ).click() + + waitFor(Duration.ofSeconds(5)) { + robot.findAll(byXpath("//div[contains(@text, 'DockDockBuild')]")).isNotEmpty() + } + val configType = + robot.find( + byXpath("//div[contains(@text, 'DockDockBuild')]"), + Duration.ofSeconds(5), + ) + assertNotNull(configType) + + robot.find( + byXpath("//div[@text='Cancel']"), + Duration.ofSeconds(5), + ).click() + } + + /** + * Verifies the DockDockBuild settings panel is accessible + * under Build Tools in the project settings. + */ + @Test + fun testSettingsPanelAccessible() { + waitForIdeFrame() + + robot.find( + byXpath("//div[@class='IdeFrameImpl']"), + ).runJs("robot.openSettingsDialog()") + + waitFor(Duration.ofSeconds(10)) { + robot.findAll( + byXpath("//div[@title='Settings' or @title='Preferences']"), + ).isNotEmpty() + } + val settingsDialog = + robot.find( + byXpath("//div[@title='Settings' or @title='Preferences']"), + Duration.ofSeconds(10), + ) + assertNotNull(settingsDialog) + + val searchField = + settingsDialog.find( + byXpath("//div[@class='SearchTextField' or @class='MyTextField']"), + Duration.ofSeconds(5), + ) + searchField.click() + searchField.runJs("component.setText('DockDockBuild')") + + waitFor(Duration.ofSeconds(5)) { + settingsDialog.findAll( + byXpath("//div[contains(@text, 'DockDockBuild')]"), + ).isNotEmpty() + } + val settingsEntry = + settingsDialog.findAll( + byXpath("//div[contains(@text, 'DockDockBuild')]"), + ) + assertTrue("DockDockBuild settings entry not found", settingsEntry.isNotEmpty()) + + settingsDialog.find( + byXpath("//div[@text='Cancel']"), + Duration.ofSeconds(5), + ).click() + } + + private fun waitForIdeFrame() { + waitFor(Duration.ofSeconds(30)) { + robot.findAll(byXpath("//div[@class='IdeFrameImpl']")).isNotEmpty() + } + } +} From 299ada42ff369283c5a38fa89d755be1601f04da Mon Sep 17 00:00:00 2001 From: hgoel Date: Tue, 21 Apr 2026 15:12:23 +0300 Subject: [PATCH 2/3] Bump version to 2.4.0 Co-Authored-By: Claude Sonnet 4.6 --- CHANGELOG.md | 2 +- gradle.properties | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5666e1e..d24ff75 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,7 @@ # DockDockBuild Changelog -## [2.3.0] +## [2.4.0] ### Added - Unit tests and UI tests (Remote Robot) - `checkConfiguration()` validates required fields before launch diff --git a/gradle.properties b/gradle.properties index c251243..0e1f5b4 100644 --- a/gradle.properties +++ b/gradle.properties @@ -4,7 +4,7 @@ pluginGroup = com.intuit.dockdockbuild pluginName = DockDockBuild # SemVer format -> https://semver.org -pluginVersion = 2.3.0 +pluginVersion = 2.4.0 # See https://plugins.jetbrains.com/docs/intellij/build-number-ranges.html # for insight into build numbers and IntelliJ Platform versions. From d79c7215ba0f21c2d492d01adb5935e1c6197b12 Mon Sep 17 00:00:00 2001 From: hgoel Date: Tue, 21 Apr 2026 15:22:40 +0300 Subject: [PATCH 3/3] Add displayName to DockDockBuild configurable extension point Co-Authored-By: Claude Sonnet 4.6 --- src/main/resources/META-INF/plugin.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/resources/META-INF/plugin.xml b/src/main/resources/META-INF/plugin.xml index c8a2cc6..b88c0d9 100644 --- a/src/main/resources/META-INF/plugin.xml +++ b/src/main/resources/META-INF/plugin.xml @@ -26,6 +26,7 @@