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..d24ff75 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,26 @@ # DockDockBuild Changelog +## [2.4.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..0e1f5b4 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.4.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 249e583..d997cfc 100644 Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 8049c68..c61a118 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,7 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-9.4.1-bin.zip +networkTimeout=10000 +validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew index a69d9cb..739907d 100755 --- a/gradlew +++ b/gradlew @@ -1,7 +1,7 @@ #!/bin/sh # -# Copyright © 2015-2021 the original authors. +# Copyright © 2015 the original authors. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -15,6 +15,8 @@ # See the License for the specific language governing permissions and # limitations under the License. # +# SPDX-License-Identifier: Apache-2.0 +# ############################################################################## # @@ -55,7 +57,7 @@ # Darwin, MinGW, and NonStop. # # (3) This script is generated from the Groovy template -# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# https://github.com/gradle/gradle/blob/2d6327017519d23b96af35865dc997fcb544fb40/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt # within the Gradle project. # # You can find Gradle at https://github.com/gradle/gradle/. @@ -80,13 +82,11 @@ do esac done -APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit - -APP_NAME="Gradle" +# This is normally unused +# shellcheck disable=SC2034 APP_BASE_NAME=${0##*/} - -# 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"' +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd -P "${APP_HOME:-./}" > /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..b88c0d9 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,8 +24,9 @@ - + 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() + } + } +}