Add method to locate the Kotlin compiler in common locations

This commit is contained in:
Erik C. Thauvin 2025-03-21 21:08:13 -07:00
parent 9fcc5cc362
commit a57ae52a1c
Signed by: erik
GPG key ID: 776702A6A2DA330E
4 changed files with 189 additions and 26 deletions

View file

@ -44,6 +44,14 @@ for all available configuration options.
Please make sure the Kotlin compiler is [installed](https://kotlinlang.org/docs/command-line.html#install-the-compiler).
The plugin will look in common locations such as:
- `KOTLIN_HOME`
- `PATH`
- SDKMAN!
- Homebrew
- JetBrains Toolbox (IntelliJ IDEA, Android Studio)
- etc.
You can also manually configure the Kotlin home location as follows:
```java

View file

@ -26,9 +26,7 @@ import rife.tools.FileUtils;
import java.io.File;
import java.io.IOException;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.*;
import java.util.logging.Level;
import java.util.logging.Logger;
@ -40,6 +38,9 @@ import java.util.logging.Logger;
*/
public class CompileKotlinOperation extends AbstractOperation<CompileKotlinOperation> {
private static final Logger LOGGER = Logger.getLogger(CompileKotlinOperation.class.getName());
private static final String OS_NAME =
System.getProperty("os.name") != null ? System.getProperty("os.name").toLowerCase(Locale.US) : null;
private static final String KOTLINC_EXECUTABLE = "kotlinc" + (isWindows() ? ".bat" : "");
private final Collection<String> compileMainClasspath_ = new ArrayList<>();
private final Collection<String> compileTestClasspath_ = new ArrayList<>();
private final Collection<File> mainSourceDirectories_ = new ArrayList<>();
@ -55,6 +56,145 @@ public class CompileKotlinOperation extends AbstractOperation<CompileKotlinOpera
private BaseProject project_;
private File workDir_;
private static String findKotlincInDir(String directory) {
var kotlinc = new File(directory, KOTLINC_EXECUTABLE);
if (isExecutable(kotlinc)) {
return kotlinc.getAbsolutePath();
}
// Check bin subdirectory if it exists
var binDir = new File(directory, "bin");
if (binDir.exists() && binDir.isDirectory()) {
kotlinc = new File(binDir, KOTLINC_EXECUTABLE);
if (isExecutable(kotlinc)) {
return kotlinc.getAbsolutePath();
}
}
return null;
}
/**
* Locates the Kotlin compiler (kotlinc) executable.
*
* @return The path to the kotlinc executable, or {@code kotlinc}/{@code kotlinc.bat} if not found.
*/
public static String findKotlincPath() {
String kotlincPath;
// Check KOTLIN_HOME environment variable first
var kotlinHome = System.getenv("KOTLIN_HOME");
if (kotlinHome != null && !kotlinHome.isEmpty()) {
kotlincPath = findKotlincInDir(kotlinHome);
if (kotlincPath != null) {
return kotlincPath;
}
}
// Check PATH environment variable
var pathEnv = System.getenv("PATH");
if (pathEnv != null && !pathEnv.isEmpty()) {
var pathDirs = pathEnv.split(File.pathSeparator);
for (var dir : pathDirs) {
kotlincPath = findKotlincInDir(dir);
if (kotlincPath != null) {
return kotlincPath;
}
}
}
// Common installation paths (e.g., SDKMAN!, IntelliJ IDEA, etc.)
List<String> commonPaths = new ArrayList<>();
if (isLinux()) {
commonPaths.add("/usr/bin");
commonPaths.add("/usr/local/bin");
commonPaths.add("/usr/local/kotlin/bin");
commonPaths.add("/opt/kotlin/bin");
var userHome = System.getProperty("user.home");
if (userHome != null) {
commonPaths.add(userHome + "/.sdkman/candidates/kotlin/current/bin"); // SDKMAN!
commonPaths.add(userHome + "/.local/share/JetBrains/Toolbox/apps/intellij-idea-ultimate/plugins/Kotlin/bin"); // Toolbox IDEA Ultimate
commonPaths.add(userHome + "/.local/share/JetBrains/Toolbox/apps/intellij-idea-community-edition/plugins/Kotlin/bin"); // Toolbox IDEA CE
commonPaths.add(userHome + "/.local/share/JetBrains/Toolbox/apps/android-studio/plugins/Kotlin/bin"); // Toolbox Android Studio
}
} else if (isWindows()) {
var localAppData = System.getenv("LOCALAPPDATA");
if (localAppData != null) {
commonPaths.add(localAppData + "\\Programs\\IntelliJ IDEA Ultimate\\plugins\\Kotlin\\kotlinc\\bin"); // Toolbox IDEA Ultimate
commonPaths.add(localAppData + "\\Programs\\IntelliJ IDEA Community Edition\\plugins\\Kotlin\\kotlinc\\bin"); // Toolbox IDEA CE
commonPaths.add(localAppData + "\\Programs\\Android Studio\\plugins\\Kotlin\\kotlinc\\bin"); // Toolbox Android Studio
}
var programFiles = System.getenv("ProgramFiles");
if (programFiles != null) {
commonPaths.add(programFiles + File.separator + "Kotlin");
}
} else if (isMacOS()) {
commonPaths.add("/usr/local/bin"); // Homebrew
commonPaths.add("/opt/homebrew/bin"); // Homebrew
var userHome = System.getProperty("user.home");
if (userHome != null) {
commonPaths.add(userHome + "/.sdkman/candidates/kotlin/current/bin"); // SDKMAN!
}
commonPaths.add("/Applications/IntelliJ IDEA Ultimate.app/Contents/plugins/Kotlin/bin"); //IntelliJ IDEA Ultimate
commonPaths.add("/Applications/IntelliJ IDEA.app/Contents/plugins/Kotlin/bin"); //IntelliJ IDEA
commonPaths.add("/Applications/Android Studio.app/Contents/plugins/Kotlin/bin"); //Android Studio
}
for (var location : commonPaths) {
kotlincPath = findKotlincInDir(location);
if (kotlincPath != null) {
return kotlincPath;
}
}
// Try 'which' or 'where' commands (less reliable but sometimes works)
try {
Process process;
if (isWindows()) {
process = Runtime.getRuntime().exec("where kotlinc");
} else {
process = Runtime.getRuntime().exec("which kotlinc");
}
try (var scanner = new Scanner(process.getInputStream())) {
if (scanner.hasNextLine()) {
kotlincPath = scanner.nextLine().trim();
if (isExecutable(new File(kotlincPath))) {
return kotlincPath;
}
}
}
} catch (Exception ignored) {
// Ignore exceptions from which/where, as they might not be available
}
return KOTLINC_EXECUTABLE;
}
private static boolean isExecutable(File file) {
return file != null && file.exists() && file.isFile() && file.canExecute();
}
/**
* Determines if the operating system is Linux.
*
* @return true if the operating system is Linux, false otherwise.
*/
public static boolean isLinux() {
return OS_NAME != null && (OS_NAME.contains("linux") || OS_NAME.contains("unix")); // Consider Unix-like systems as well.
}
/**
* Determines if the current operating system is macOS.
*
* @return true if the OS is macOS, false otherwise.
*/
public static boolean isMacOS() {
return OS_NAME != null && (OS_NAME.contains("mac") || OS_NAME.contains("darwin"));
}
/**
* Determines if the given string is not blank.
*
@ -65,6 +205,15 @@ public class CompileKotlinOperation extends AbstractOperation<CompileKotlinOpera
return s != null && !s.isBlank();
}
/**
* Determines if the current operating system is Windows.
*
* @return true if the operating system is Windows, false otherwise.
*/
public static boolean isWindows() {
return OS_NAME != null && OS_NAME.contains("win");
}
/**
* Provides the main build destination directory.
*
@ -299,7 +448,14 @@ public class CompileKotlinOperation extends AbstractOperation<CompileKotlinOpera
var args = new ArrayList<String>();
// kotlinc
args.add(kotlinCompiler());
if (kotlinc_ != null) {
args.add(kotlinc_.getAbsolutePath());
} else if (kotlinHome_ != null) {
args.add(Objects.requireNonNullElseGet(findKotlincInDir(kotlinHome_.getAbsolutePath()),
CompileKotlinOperation::findKotlincPath));
} else {
args.add(findKotlincPath());
}
// classpath
if (classpath != null && !classpath.isEmpty()) {
@ -386,7 +542,6 @@ public class CompileKotlinOperation extends AbstractOperation<CompileKotlinOpera
* <p>
* Sets the following from the project:
* <ul>
* <li>{@link #kotlinHome() kotlinHome} to the {@code KOTLIN_HOME} environment variable, if set.</li>
* <li>{@link #workDir() workDir} to the project's directory.</li>
* <li>{@link #buildMainDirectory() buildMainDirectory}</li>
* <li>{@link #buildTestDirectory() buildTestDirectory}</li>
@ -405,14 +560,6 @@ public class CompileKotlinOperation extends AbstractOperation<CompileKotlinOpera
*/
public CompileKotlinOperation fromProject(BaseProject project) {
project_ = project;
if (kotlinHome_ == null) {
var env = System.getenv("KOTLIN_HOME");
if (env != null) {
kotlinHome_ = new File(env);
}
}
workDir_ = new File(project.workDirectory().getAbsolutePath());
var op = buildMainDirectory(project.buildMainDirectory())
@ -437,18 +584,6 @@ public class CompileKotlinOperation extends AbstractOperation<CompileKotlinOpera
return op;
}
private String kotlinCompiler() {
if (kotlinc_ != null) {
return kotlinc_.getAbsolutePath();
} else if (kotlinHome_ != null) {
var kotlinc = Path.of(kotlinHome_.getAbsolutePath(), "bin", "kotlinc").toFile();
if (kotlinc.exists() && kotlinc.canExecute()) {
return kotlinc.getAbsolutePath();
}
}
return "kotlinc";
}
/**
* Provides the Kotlin home directory, if it differs from the default {@code KOTLIN_HOME}.
*

View file

@ -31,6 +31,7 @@ import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.Objects;
import java.util.logging.ConsoleHandler;
import java.util.logging.Level;
@ -209,6 +210,11 @@ class CompileKotlinOperationTest {
}
}
@Test
void testFindKotlincPath() {
assertThat(CompileKotlinOperation.findKotlincPath()).doesNotStartWith("kotlinc");
}
@Test
void testFromProject() {
var examples = new File("examples");
@ -226,6 +232,21 @@ class CompileKotlinOperationTest {
assertThat(op.testSourceDirectories()).isEmpty();
}
@Test
void testIsOS() {
var osName = System.getProperty("os.name");
if (osName != null) {
var os = osName.toLowerCase(Locale.US);
if (os.contains("win")) {
assertThat(CompileKotlinOperation.isWindows()).isTrue();
} else if (os.contains("linux") || os.contains("unix")) {
assertThat(CompileKotlinOperation.isLinux()).isTrue();
} else if (os.contains("mac") || os.contains("darwin")) {
assertThat(CompileKotlinOperation.isMacOS()).isTrue();
}
}
}
@Test
void testKotlinHome() {
var foo = new File("foo");

View file

@ -45,7 +45,6 @@ class CompileOptionsTest {
.collect(Collectors.joining(File.pathSeparator));
}
@Test
@SuppressWarnings("PMD.UnitTestShouldIncludeAssert")
void testArgs() {