Files
obsidian/Kotlin/Инструментарий/kotlinc.md
T

16 KiB
Raw Blame History

title, description, tags
title description tags
kotlinc Командная строка компилятора, REPL, скрипты .main.kts и ключевые флаги
kotlin
tooling

kotlinc - это компилятор командной строки. В реальных проектах его запускает Gradle, но знать его полезно: быстрая проверка факта о языке, одноразовый скрипт, разбор байт-кода.

kotlinc Hello.kt -include-runtime -d hello.jar
java -jar hello.jar
fun main() {
    println("скомпилировано kotlinc")
}

Проверить версию:

$ kotlinc -version
info: kotlinc-jvm 2.4.21 (JRE 27)

Первое число - версия Kotlin, в скобках - JRE, на которой работает сам компилятор (не версия целевого байт-кода).

Установка

Способ Команда
Homebrew brew install kotlin
SDKMAN sdk install kotlin
Snap snap install --classic kotlin
Chocolatey choco install kotlinc
Вручную архив с GitHub Releases + PATH

Отдельная установка нужна только для работы вне проекта: Gradle-плагин скачивает компилятор сам.

Три режима компиляции

В каталог с .class - для дальнейшей работы с байт-кодом:

kotlinc Demo.kt -d out
javap -p -cp out DemoKt

В JAR без рантайма - для подключения как библиотеки:

kotlinc Lib.kt -d lib.jar

JAR не запустится напрямую: в нём нет stdlib.

В исполняемый JAR - со встроенной stdlib (~5 МБ):

kotlinc App.kt -include-runtime -d app.jar
java -jar app.jar

Ключевые флаги

Флаг Что делает
-d <путь> каталог или .jar для результата
-include-runtime вложить stdlib в JAR
-classpath / -cp зависимости
-jvm-target <v> версия байт-кода (по умолчанию 1.8)
-module-name <n> имя модуля (влияет на манглирование internal)
-nowarn подавить предупреждения
-Werror считать предупреждения ошибками
-Wextra дополнительные проверки K2
-progressive исправления применяются сразу, без цикла миграции
-language-version <v> ограничить возможности языка
-api-version <v> запретить API новее указанной версии stdlib
-opt-in <fq.name> включить API, требующее opt-in
-verbose подробный лог
-X список экспериментальных флагов

Про -jvm-target. Значение по умолчанию - 1.8, то есть байт-код major version 52, даже на JDK 27:

$ kotlinc Demo.kt -d out && javap -v -cp out DemoKt | grep major
  major version: 52

$ kotlinc -jvm-target 21 Demo.kt -d out21 && javap -v -cp out21 DemoKt | grep major
  major version: 65

Gradle-плагин это переопределяет через toolchain, но при ручной компиляции флаг нужно задавать явно.

Про -Werror. Останавливает сборку на любом предупреждении:

$ kotlinc -Werror Demo.kt -d out
error: warnings found and -Werror specified
Demo.kt:5:13: warning: 'fun old(): Int' is deprecated. устарело.

В CI это полезно: не даёт предупреждениям накапливаться. В Gradle - allWarningsAsErrors.set(true).

@Deprecated("устарело, используйте modern()")
fun old() = 1

fun modern() = 2

fun main() {
    @Suppress("DEPRECATION")
    println(old())        // 1
}

@Suppress точечно отключает проверку - так -Werror не блокирует намеренные вызовы устаревшего API.

Мгновенное выполнение

Флаг -e вычисляет выражение без файла:

$ kotlinc -e 'println("из -e: ${2+2}")'
из -e: 4

Удобно для проверки одной строчки - быстрее, чем создавать файл.

Скрипты .main.kts

Файл с расширением .main.kts - это Kotlin-скрипт: код на верхнем уровне, без fun main.

$ cat hello.main.kts
println("из скрипта: ${listOf(1,2,3).sum()}")

$ kotlinc -script hello.main.kts
из скрипта: 6

Главная возможность - зависимости прямо в файле:

$ cat fetch.main.kts
@file:DependsOn("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.11.0")
import kotlinx.coroutines.*

runBlocking { delay(10); println("зависимость из скрипта работает") }

$ kotlinc -script fetch.main.kts
зависимость из скрипта работает

@file:DependsOn скачивает артефакт из Maven Central при первом запуске и кеширует. Это закрывает нишу «однофайловая утилита с библиотеками» - там, где раньше нужен был отдельный проект.

Доступные файловые аннотации скриптов:

Аннотация Что делает
@file:DependsOn("group:artifact:version") зависимость из Maven
@file:Repository("https://...") дополнительный репозиторий
@file:CompilerOptions("-jvm-target", "21") флаги компилятора
@file:Import("other.main.kts") подключить другой скрипт

Аргументы командной строки доступны как args:

// count.main.kts — запуск: kotlinc -script count.main.kts файл.txt
// val path = args.firstOrNull() ?: error("укажите файл")
// println(java.io.File(path).readLines().size)

fun main() {
    // в скрипте args доступен напрямую, здесь — имитация
    val args = arrayOf("демо.txt")
    println("аргументов: ${args.size}")        // аргументов: 1
}

Shebang делает скрипт исполняемым:

#!/usr/bin/env kotlin
println("запущено как ./script.main.kts")

REPL

Запуск kotlinc без аргументов открывает интерактивную оболочку:

$ kotlinc
Welcome to Kotlin version 2.4.21
>>> val x = 42
>>> x * 2
res1: kotlin.Int = 84
>>> :quit

REPL полезен для проверки поведения stdlib. Ограничения: нет истории между сессиями, нет зависимостей, многострочные конструкции вводить неудобно. Для исследований лучше Kotlin Notebook в IDEA или .main.kts.

Смешанная компиляция вручную

Если нужно скомпилировать Kotlin и Java вместе без Gradle, порядок важен:

# 1. Kotlin видит Java-исходники
kotlinc -cp . Lib.kt JLib.java -d out

# 2. javac видит скомпилированные Kotlin-классы
javac -cp out -d out Main.java

# 3. запуск с stdlib в classpath
java -cp out:$KOTLIN_HOME/lib/kotlin-stdlib.jar Main

Kotlin-компилятор принимает .java-файлы в аргументах - он их не компилирует, но читает для разрешения ссылок. Без stdlib в classpath запуск падает:

Exception in thread "main" java.lang.NoClassDefFoundError: kotlin/jvm/internal/Intrinsics

Подробнее: Структура проекта

Демон компилятора

Запуск JVM и инициализация компилятора занимают 1-2 секунды, поэтому kotlinc на каждый файл медленный. Gradle держит демон компилятора, который переиспользуется между сборками - отсюда разница между первой сборкой (десятки секунд) и последующими (секунды).

Настройка демона в gradle.properties:

kotlin.daemon.jvmargs=-Xmx2g
org.gradle.jvmargs=-Xmx4g
kotlin.incremental=true

Недостаточная память демона - частая причина OutOfMemoryError или неожиданно медленных сборок на больших проектах.

Полезные комбинации

# что реально попало в байт-код
kotlinc Demo.kt -d out && javap -p -cp out DemoKt

# аннотации nullability и метаданные
kotlinc Demo.kt -d out && javap -v -p -cp out DemoKt | grep -E "Nullable|NotNull|Metadata"

# проверить, что код чистый
kotlinc -Werror -Wextra Demo.kt -d /dev/null

# совместимость со старым языком
kotlinc -language-version 2.0 -api-version 2.0 Demo.kt -d out

# список экспериментальных флагов
kotlinc -X

Подробнее про чтение байт-кода: Kotlin в байт-коде

[!info] Отличия от Java

Java Kotlin
javac Demo.java kotlinc Demo.kt
java Demo java -jar app.jar или kotlin DemoKt
--release 21 -jvm-target 21
-Xlint:all -Werror -Wextra -Werror
JEP 330: java Demo.java kotlinc -script или .main.kts
jshell kotlinc без аргументов
нет аналога @file:DependsOn в скриптах

Главное практическое различие - stdlib не входит в платформу. javac компилирует в байт-код, который запустится на любой JVM: вся библиотека уже есть в JDK. Kotlin-код зависит от kotlin-stdlib.jar, поэтому нужен либо -include-runtime, либо stdlib в classpath. Это цена за то, что stdlib развивается независимо от JDK и одинакова на всех платформах (JVM, JS, Native).

-jvm-target 1.8 по умолчанию - решение о совместимости, а не недоработка. Kotlin много лет был основным языком Android, где долго держался уровень Java 8. Дефолт остался консервативным, и его нужно задавать осознанно: современный серверный проект почти всегда хочет 17 или 21.

Скрипты в Kotlin сильнее, чем однофайловый запуск в Java. JEP 330 позволяет java Demo.java, но только с тем, что есть в JDK: зависимостей нет. @file:DependsOn в .main.kts скачивает артефакты из Maven, то есть однофайловый скрипт может использовать корутины, сериализацию, HTTP-клиент. Это полноценная замена Python/Bash для задач автоматизации.

Сравнить: JVM

[!warning] Подводные камни -jvm-target по умолчанию 1.8. Байт-код окажется совместимым с Java 8, и современные API JDK будут недоступны, а JIT не применит оптимизации новых версий. На это легко не обратить внимания - предупреждения нет.

JAR без -include-runtime не запускается:

$ kotlinc App.kt -d app.jar && java -jar app.jar
Exception in thread "main" java.lang.NoClassDefFoundError: kotlin/jvm/internal/Intrinsics

Либо добавьте флаг, либо укажите stdlib в -cp.

-include-runtime даёт JAR на ~5 МБ. Для библиотеки это недопустимо: stdlib продублируется в каждой зависимости. Флаг только для конечных приложений.

-e и -script требуют разных форм записи. -e принимает выражение в кавычках, -script - путь к .kts-файлу. Передать .kts в -e или выражение в -script не получится.

Файл скрипта должен называться *.main.kts, не *.kts. Расширение .kts зарезервировано для Gradle-скриптов с другим шаблоном: @file:DependsOn в нём не сработает.

Первый запуск @file:DependsOn медленный. Скачивание артефактов занимает секунды или десятки секунд. Дальше работает кеш ~/.m2, но в CI без сохранённого кеша задержка повторится на каждом прогоне.

-Werror падает на предупреждениях из зависимостей. Вызов устаревшего API библиотеки даст warning и остановит сборку. Нужен точечный @Suppress("DEPRECATION"), а не отключение флага.

REPL не видит зависимостей. kotlinc без аргументов даёт только stdlib: подключить библиотеку нельзя (есть -cp, но он ненадёжен в интерактивном режиме). Для исследований с библиотеками используйте Kotlin Notebook или .main.kts.

Kotlin-компилятор не компилирует .java-файлы. Передача .java в аргументах нужна только для разрешения ссылок; сами классы потом компилирует javac. Пропустить второй шаг - получить ClassNotFoundException на Java-классе.

-module-name влияет на видимость internal. internal компилируется в публичное имя с суффиксом модуля (helper$main). Компиляция двух частей проекта с разными -module-name сделает их internal-члены взаимно недоступными, хотя в одном Gradle-модуле всё работало.

-d /dev/null для проверки работает не везде. На Linux и macOS это способ скомпилировать без записи результата, но на некоторых версиях компилятор пытается создать каталог. Надёжнее писать во временный каталог и удалять.

Далее: Gradle и Kotlin DSL