Прежде чем перейти к статье, хочу вам представить, экономическую онлайн игру Brave Knights, в которой вы можете играть и зарабатывать. Регистируйтесь, играйте и зарабатывайте!
LONG REED: ~18 мин
Ключевые выводы
Разработчики хотят распространять свои приложения командной строки как отдельный исполняемый файл.
GraalVM может компилировать ваши Java-приложения в нативные образы в машинном коде, но имеет некоторые ограничения.
Picocli - это библиотека для написания приложений CLI на JVM, которая может помочь преодолеть ограничения GraalVM, в том числе в Windows.
Настройка набора инструментов GraalVM для создания нативных образов в Windows плохо документирована.
Мечта: исполняемые файлы Java
Язык программирования Go стал популярным для написания приложений командной строки. Для этого может быть много причин, но один аспект, в котором выделяется Go, - это возможность скомпилировать программу в один собственный исполняемый файл. Это значительно упрощает распространение программы.
Программы Java традиционно было трудно распространять, потому что они требовали, чтобы на целевой машине была установлена виртуальная машина Java. Можно связать с приложением последнюю JVM, но это увеличивает размер пакета примерно на 200 МБ. Все движется в правильном направлении: система модулей Java (JPMS), представленная в Java 9, включает утилиту jlink, которая позволяет приложению создавать настраиваемую, минимизированную JRE, размер которой может составлять всего 30-40 МБ, и Java. 14 включает jpackage, которая может создать установщик, содержащий этот минимальный JRE с вашим приложением.
Тем не менее, для приложений командной строки установщик не идеален. В идеале мы хотим распространять нашу утилиту CLI как «настоящий» нативный исполняемый файл без упакованной среды выполнения. GraalVM позволяет нам делать это с помощью программ, написанных на Java.
GraalVM
GraalVM - это универсальная виртуальная машина, которая может запускать приложения, написанные на JavaScript, Python, Ruby, R, языках на основе JVM, таких как Java, Scala, Clojure, Kotlin, и языках на основе LLVM, таких как C и C ++. Один интересный аспект заключается в том, что GraalVM позволяет смешивать языки программирования: программы, частично написанные на JavaScript, R, Python или Ruby, могут быть вызваны из Java и могут обмениваться данными друг с другом. Еще одна особенность - возможность создавать нативный образ, и это то, что мы рассмотрим в этой статье.
Нативные образы GraalVM
GraalVM Native Image позволяет вам заранее скомпилировать Java-код в автономный исполняемый файл, называемый нативным образом. Этот исполняемый файл включает приложение, библиотеки, JDK и не запускается на виртуальной машине Java, но включает необходимые компоненты, такие как управление памятью и планирование потоков с другой виртуальной машины, называемой «Substrate VM». Субстратная виртуальная машина - это имя для компонентов среды выполнения (таких как деоптимизатор, сборщик мусора, планирование потоков и т. д.). Полученная программа имеет более быстрое время запуска и меньшие накладные расходы на оперативную память по сравнению с виртуальной машиной Java.
Ограничения на нативные образы
Для того чтобы реализация была небольшой и лаконичной, а также для обеспечения возможности агрессивной опережающей оптимизации, Native Image не поддерживает все функции Java. Полный набор ограничений задокументирован в GitHub проекте.
Особый интерес представляют два ограничения:
рефлексия
ресурсы
По сути, чтобы создать автономный двоичный файл, компилятор изображений в машинном коде должен заранее знать все классы вашего приложения, их зависимости и ресурсы, которые они используют. Отражение и пакеты ресурсов часто требуют настройки. Позже мы увидим пример этого.
Picocli
Picocli - это современная библиотека и фреймворк для создания приложений командной строки на JVM. Он поддерживает Java, Groovy, Kotlin и Scala. Ему менее 3 лет, но он стал довольно популярным, его скачивали более 500 000 раз в месяц. Язык Groovy использует picocli для реализации CliBuilder DSL.
Picocli стремится быть «самым простым способом создания многофункциональных приложений командной строки, которые могут работать как на JVM, так и вне ее». Он предлагает цветной вывод, автозаполнение TAB, подкоманды и некоторые уникальные функции по сравнению с другими библиотеками CLI JVM, такие как отрицательные параметры, повторяющиеся составные группы аргументов, повторяющиеся подкоманды и сложная обработка цитируемых аргументов. Его исходный код находится в одном файле, поэтому его можно при желании включить в качестве источника, чтобы избежать добавления зависимости. Picocli гордится своей обширной и тщательной документацией.
Picocli использует отражение, поэтому он уязвим для ограничений нативного образа GraalVM Java, но предлагает процессор аннотаций, который генерирует файлы конфигурации, которые устраняют это ограничение во время компиляции.
Конкретный сценарий использования
Давайте возьмем конкретный пример утилиты командной строки, которую мы напишем на Java и скомпилируем в единственный нативный исполняемый файл. Попутно мы рассмотрим некоторые особенности библиотеки picocli, которые помогут упростить использование нашей утилиты.
Мы создадим утилиту CLI для проверки контрольной суммы, которая принимает именованный параметр -a или --algorithm и позиционный параметр, который представляет собой файл, контрольную сумму которого нужно вычислить.
Мы хотим, чтобы наши пользователи могли использовать нашу утилиту проверки контрольных сумм Java так же, как они используют приложения, написанные на C++ или других языках. Что-то вроде этого:
$ echo hi > hi.txt
$ checksum -a md5 hi.txt
764efa883dda1e11db47671c4a3bbd9e
$ checksum -a sha1 hi.txt
55ca6286e3e4f4fba5d0448333fa99fc5a404a73
Это минимум, который мы ожидаем от приложения командной строки, но мы не собираемся довольствоваться приложением с наименьшим общим знаменателем, мы хотим создать отличное приложение с интерфейсом командной строки, которое доставит удовольствие нашим пользователям. Что это значит и как это сделать?
CLI приложения полезны
Мы пошли на компромисс: выбрав интерфейс командной строки (CLI) вместо графического пользовательского интерфейса (GUI), наше приложение будет труднее освоить для новых пользователей. Мы можем частично компенсировать это, предоставляя хорошую онлайн-помощь.
Наше приложение должно отображать справочное сообщение, когда пользователь запрашивает помощь с параметром -h или --help, или когда указан недопустимый ввод пользователя. Он также должен отображать информацию о версии при запросе с -V или --version. Мы увидим, как Picocli позволяет это сделать легко.
Пользовательский опыт
Мы можем сделать наше приложение более удобным для пользователя, используя цвета на поддерживаемых платформах. Это не только хорошо выглядит, но и снижает когнитивную нагрузку на пользователя: контраст выделяет важную информацию, такую как команды, параметры и параметры, из окружающего текста.
В справочном сообщении по использованию, создаваемом приложением на основе Picocli, по умолчанию используются цвета. Наш пример контрольной суммы выглядит примерно так:
Как правило, приложения должны выводить цвета только тогда, когда они используются в интерактивном режиме; при выполнении сценария мы не хотим, чтобы файл журнала был загроможден escape-кодами ANSI. К счастью, Picocli позаботится об этом автоматически. Это подводит нас к следующей теме: хорошие CLI приложения предназначены для комбинирования с другими командами.
CLI приложения хорошо работают с другими
Stdout или stderr
Многие утилиты CLI используют стандартные потоки ввода-вывода, поэтому их можно комбинировать с другими утилитами. Дьявол часто кроется в деталях. Когда пользователь запрашивает помощь, приложение должно распечатать сообщение справки по использованию на стандартный вывод. Это позволяет пользователям направлять вывод в другой инструмент, например, grep или less.
С другой стороны, при недопустимом вводе сообщение об ошибке и справочное сообщение должны быть напечатаны в стандартный поток ошибок: в случае, если вывод нашей программы используется как ввод для другой программы, мы не хотим, чтобы наше сообщение об ошибке прерывалось.
Код ответа
Когда ваша программа завершается, она возвращает код ответа. Нулевой код ответа часто используется для обозначения успеха, а ненулевой код выхода часто указывает на какой-либо сбой.
Это позволяет пользователям объединять несколько команд в цепочку с помощью &&, зная, что в случае сбоя какой-либо команды в последовательности вся последовательность остановится.
По умолчанию picocli возвращает 2 при недопустимом вводе пользователем, 1 при возникновении исключения в бизнес-логике приложения и ноль в противном случае (когда все прошло хорошо). Конечно, легко настроить другие коды ответа в вашем приложении, но для нашего примера контрольной суммы подходят значения по умолчанию.
Обратите внимание, что библиотека picocli не будет вызывать System.exit. Она просто возвращает целое число, и приложение должно определять вызывать или нет System.exit.
Компактный код
Разделы выше описывают довольно много функций. Вы могли подумать, что для этого потребуется много кода, но большая часть «стандартного поведения CLI» обеспечивается библиотекой picocli. В нашем приложении все, что нам нужно сделать, это определить наши параметры и позиционные параметры и реализовать бизнес-логику, сделав наш класс Callable или Runnable. Мы можем уместить все приложение в одну строку кода в методе main:
import picocli.CommandLine;
import picocli.CommandLine.Command;
import picocli.CommandLine.Option;
import picocli.CommandLine.Parameters;
import java.io.File;
import java.math.BigInteger;
import java.nio.file.Files;
import java.security.MessageDigest;
import java.util.concurrent.Callable;
@Command(name = "checksum", mixinStandardHelpOptions = true,
version = "checksum 4.0",
description = "Prints the checksum (MD5 by default) of a file to STDOUT.")
class CheckSum implements Callable<Integer> {
@Parameters(index = "0", arity = "1",
description = "The file whose checksum to calculate.")
private File file;
@Option(names = {"-a", "--algorithm"},
description = "MD5, SHA-1, SHA-256, ...")
private String algorithm = "MD5";
// this example implements Callable, so parsing, error handling
// and handling user requests for usage help or version help
// can be done with one line of code.
public static void main(String... args) {
int exitCode = new CommandLine(new CheckSum()).execute(args);
System.exit(exitCode);
}
@Override
public Integer call() throws Exception { // the business logic...
byte[] data = Files.readAllBytes(file.toPath());
byte[] digest = MessageDigest.getInstance(algorithm).digest(data);
String format = "%0" + (digest.length*2) + "x%n";
System.out.printf(format, new BigInteger(1, digest));
return 0;
}
}
Итак, у нас есть пример реалистичной утилиты на Java. Далее давайте посмотрим, как превратить его в нативный исполняемый файл.
Нативный образ
Конфигурация рефлексии
Ранее мы упоминали, что собственный компилятор образов имеет некоторые ограничения: рефлексия поддерживается, но требует настройки.
Это влияет picocli-приложения: во время выполнения, picocli использует рефлексию, чтобы обнаружить любые @command -annotated подкоманд и @option и @Parameters -annotated параметров команды и позиционные параметры.
Следовательно, нам необходимо предоставить GraalVM файл конфигурации, определяющий все аннотированные классы, методы и поля. Такой конфигурационный файл выглядит примерно так:
[
{
"name" : "CheckSum",
"allDeclaredConstructors" : true,
"allPublicConstructors" : true,
"allDeclaredMethods" : true,
"allPublicMethods" : true,
"fields" : [
{ "name" : "algorithm" },
{ "name" : "file" }
]
},
{
"name" : "picocli.CommandLine$AutoHelpMixin",
"allDeclaredConstructors" : true,
"allPublicConstructors" : true,
"allDeclaredMethods" : true,
"allPublicMethods" : true,
"fields" : [
{ "name" : "helpRequested" },
{ "name" : "versionRequested" }
]
}
]
Это быстро становится довольно громоздким для утилит с множеством опций, но, к счастью, нам не нужно делать это вручную.
Обработчик аннотаций Picocli
Модуль picocli-codegen включает в себя процессор аннотаций, который может строить модель из аннотаций picocli во время компиляции, а не во время выполнения.
Процессор аннотаций генерирует файлы конфигурации Graal в META-INF / native-image / picocli-generated / $ project во время компиляции для включения в jar приложения. Сюда входят файлы конфигурации для отражения, ресурсов и динамических прокси . Встраивая эти файлы конфигурации, ваш jar-файл мгновенно становится доступным для Graal. В большинстве случаев при создании образа в машинном коде никакой дополнительной настройки не требуется.
В качестве бонуса процессор аннотаций показывает ошибки для недопустимых аннотаций или атрибутов сразу после компиляции, а не во время тестирования во время выполнения, что приводит к сокращению циклов обратной связи.
Итак, все, что нам осталось сделать, это скомпилировать наш исходный файл CheckSum.java с jar-файлом picocli-codegen в пути classpath:
Компиляция CheckSum.java и создание checkum.jar в Linux. Замените разделитель пути ":" на ";", чтобы эти команды работали в Windows.
mkdir classes
javac -cp .:picocli-4.2.0.jar:picocli-codegen-4.2.0.jar -d classes CheckSum.java
cd classes && jar -cvef CheckSum ../checksum.jar * && cd ..
Вы можете найти сгенерированные файлы конфигурации в каталоге META-INF/native-image/picocli-generated/ внутри jar файла:
jar -tf checksum.jar
META-INF/
META-INF/MANIFEST.MF
CheckSum.class
META-INF/native-image/
META-INF/native-image/picocli-generated/
META-INF/native-image/picocli-generated/proxy-config.json
META-INF/native-image/picocli-generated/reflect-config.json
META-INF/native-image/picocli-generated/resource-config.json
Мы закончили с нашим приложением. Давайте сделаем нативный образ в качестве следующего шага!
GraalVM Native Image Toolchain
Чтобы создать нативный образ, нам нужно установить GraalVM, убедиться, что установлена утилита native-image, и установить набор инструментов компилятора C / C ++ для ОС, на которой мы строим. У меня были некоторые проблемы с этим, поэтому, надеюсь, приведенные ниже шаги прояснят ситуацию для других разработчиков.
Разработка - это 10% вдохновения и 90% настройки вашей среды.
- Неизвестный разработчик
Установка GraalVM
Сначала установите последнюю версию GraalVM, 20.0 на момент написания этой статьи. Страница « Приступая к работе с GraalVM» - лучшее место для получения самых свежих инструкций по установке GraalVM в различных операционных системах и контейнерах.
Установка Native Image Utility
GraalVM поставляется с утилитой для создания нативных образов. В последних версиях GraalVM это необходимо сначала загрузить и установить отдельно с помощью инструмента Graal Updater:
Установка утилиты генератора native-image для Java 11 в Linux
gu install -L /path/to/native-image-installable-svm-java11-linux-amd64-20.0.0.jar
Этот шаг также стал необходимым с версией GraalVM для Windows начиная с 20.0.
Для получения дополнительных сведений см. раздел Native Image Справочного руководства GraalVM.
Установка Compiler Toolchain
Набор инструментов компилятора Linux и MacOS
Для компиляции native-image зависит от локальной инструментальной цепочки, поэтому в Linux и MacOS нам нужны glibc-devel, zlib-devel (файлы заголовков для библиотеки C и zlib) и gcc, которые будут доступны в нашей системе.
Для этого в Linux: sudo dnf install gcc glibc-devel zlib-devel или sudo apt-get install build-essential libz-dev.
В macOS выполните xcode-select --install.
Набор инструментов компилятора Windows для Java 8
GraalVM начала предлагать экспериментальную поддержку нативных образов Windows начиная с версии 19.2.0.
Поддержка Windows по-прежнему является экспериментальной, а официальная документация скудна с подробностями, касающимися нативных образов для Windows. Начиная с версии 19.3 GraalVM поддерживает как Java 8, так и Java 11, а в Windows для этого требуются различные цепочки инструментов.
Чтобы создавать нативные образы с использованием версии GraalVM для Java 8, вам потребуется Microsoft Windows SDK для Windows 7 и .NET Framework 4, а также компиляторы C из KB2519277. Вы можете установить их, используя шоколадный:
choco установить windows-sdk-7.1 kb2519277
Затем (из командной строки) активируйте среду sdk-7.1:
call "C:\Program Files\Microsoft SDKs\Windows\v7.1\Bin\SetEnv.cmd"
Это запускает новую командную строку с включенной средой sdk-7.1. Выполните все последующие команды в этом окне командной строки. Это работает для всех версий GraalVM Java 8 от 19.2.0 до 20.0.
Набор инструментов компилятора Java 11
Чтобы создавать нативные образы с использованием версии GraalVM для Java 11 (19.3.0 и выше), вы можете либо установить среду разработки Visual Studio 2017 (обязательно включив инструменты Visual C ++ для CMake), либо вы можете установить Visual C Build Tools Workload для Visual Studio 2017 с использованием chocolatey:
choco install visualstudio2017-workload-vctools
После установки настройте среду из командной строки с помощью этой команды:
call "C:\Program Files (x86)\Microsoft Visual Studio\2017\BuildTools\VC\Auxiliary\Build\vcvars64.bat"
СОВЕТ | Если вы установили IDE Visual Studio 2017, замените BuildTools в приведенной выше команде на Community или Enterprise, в зависимости от вашей версии Visual Studio. |
Создание нативного образа
Утилита native-image может скомпилировать Java приложение в нативный образ, который может работать как нативное исполняемое приложение на платформе, где Java приложение скомпилировано. В Linux это может выглядеть так:
Создание нативного образа в Linux
$ /usr/lib/jvm/graalvm/bin/native-image \
-cp classes:picocli-4.2.0.jar --no-server \
--static -H:Name=checksum CheckSum
Выполнение утилиты native-image заняло около минуты на моем ноутбуке. В результате получен такой вывод:
[checksum:1073] classlist: 3,124.74 ms, 1.14 GB
[checksum:1073] (cap): 2,885.31 ms, 1.14 GB
[checksum:1073] setup: 4,767.19 ms, 1.14 GB
[checksum:1073] (typeflow): 8,733.59 ms, 1.94 GB
[checksum:1073] (objects): 6,073.44 ms, 1.94 GB
[checksum:1073] (features): 313.28 ms, 1.94 GB
[checksum:1073] analysis: 15,384.41 ms, 1.94 GB
[checksum:1073] (clinit): 322.84 ms, 1.94 GB
[checksum:1073] universe: 793.02 ms, 1.94 GB
[checksum:1073] (parse): 2,191.69 ms, 1.94 GB
[checksum:1073] (inline): 2,064.62 ms, 2.13 GB
[checksum:1073] (compile): 14,960.43 ms, 2.73 GB
[checksum:1073] compile: 20,040.78 ms, 2.73 GB
[checksum:1073] image: 1,272.17 ms, 2.73 GB
[checksum:1073] write: 722.20 ms, 2.73 GB
[checksum:1073] [total]: 46,743.28 ms, 2.73 GB
В результате у нас есть нативный исполняемый файл Linux. Интересно, что нативный двоичный файл, созданный с помощью версии GraalVM для Java 11, немного больше, чем тот, который был создан с помощью версии GraalVM для Java 8:
-rwxrwxrwx 1 remko remko 14744296 Feb 19 09:51 java11-20.0/checksum*
-rwxrwxrwx 1 remko remko 12393600 Feb 19 09:48 java8-20.0/checksum*
Мы видим, что размер двоичного файла составляет 12,4–14,7 МБ. Мы можем считать это большим или маленьким, в зависимости от того, с чем сравнивать. Для меня это приемлемый размер.
Запустим приложение, чтобы убедиться, что оно работает. Мы можем также сравнить время запуска приложения на обычной JIT-машине JVM со временем запуска нативного образа:
$ time java -cp classes:picocli-4.2.0.jar CheckSum hi.txt
764efa883dda1e11db47671c4a3bbd9e
real 0m0.415s ← startup is 415 millis with normal Java
user 0m0.609s
sys 0m0.313s
$ time ./checksum hi.txt
764efa883dda1e11db47671c4a3bbd9e
real 0m0.004s ← native image starts up in 4 millis
user 0m0.002s
sys 0m0.002s
Итак, по крайней мере, в Linux мы теперь можем распространять наше Java-приложение как отдельный исполняемый файл. Какова ситуация в Windows?
Нативный образ в Windows
Поддержка нативных образов в Windows имеет некоторые недостатки, поэтому мы рассмотрим ее более подробно.
Создание нативных образов в Windows
Само по себе создание нативного образа не проблема. Например:
Создание нативного образа в Windows
C:\apps\graalvm-ce-java8-20.0.0\bin\native-image ^
-cp picocli-4.2.0.jar --static -jar checksum.jar
Утилита native-image.cmd в Windows дает аналогичный результат, который мы видели в Linux, и требует сопоставимое количество времени и приводит к немного меньшему исполняемому файлу - 11,3 МБ для версии GraalVM для Java 8 и 14,2 МБ для версии GraalVM. двоичный файл, созданный с помощью версии GraalVM для Java 11.
Бинарные файлы работают нормально, с одним отличием: мы не видим цвета ANSI на консоли. Давайте посмотрим, как это исправить.
Нативные образы Windows с цветным выводом
Чтобы получить цвета ANSI в командной строке Windows, нам нужно использовать библиотеку Jansi. К сожалению, у Jansi (начиная с версии 1.18) есть некоторые проблемы, которые означают, что она не может производить цветной вывод в нативном образе GraalVM. Чтобы обойти эту проблему, picocli предлагает сопутствующую библиотеку Jansi, picocli-jansi-graalvm, которая позволяет библиотеке Jansi правильно работать с образами GraalVM в Windows.
Мы меняем основной метод, чтобы сообщить Jansi о включении рендеринга escape-кодов ANSI в Windows, например:
//...
import picocli.jansi.graalvm.AnsiConsole;
//...
public class CheckSum implements Callable<Integer> {
// ...
public static void main(String[] args) {
int exitCode = 0;
// enable colors on Windows
try (AnsiConsole ansi = AnsiConsole.windowsInstall()) {
exitCode = new CommandLine(new CheckSum()).execute(args);
}
System.exit(exitCode);
}
}
Далее создаем новый нативный образ с помощью команды native-image (обратите внимание, что в GraalVM 19.3 возникла необходимость указывать jar-файлы в пути к классам):
set GRAALVM_HOME=C:\apps\graalvm-ce-java11-20.0.0
%GRAALVM_HOME%\bin\native-image ^
-cp "picocli-4.2.0.jar;jansi-1.18.jar;picocli-jansi-graalvm-1.1.0.jar;checksum.jar" ^
picocli.nativecli.demo.CheckSum checksum
И у нас появились цвета в нашем консольном приложении DOS:
Это требует немного дополнительных усилий, но теперь наше нативное приложение Windows CLI может использовать цветовое выделение, чтобы обеспечить такое же взаимодействие с пользователем, как и в Linux.
Размер полученных двоичных файлов не сильно изменился с добавлением библиотек Jansi: сборка с использованием Java 11 GraalVM дала двоичный файл 14,3 МБ, сборка с помощью Java 8 GraalVM дала двоичный файл 11,3 МБ.
Запуск нативных образов в Windows
Мы почти закончили, но есть еще одна ошибка, которая не сразу очевидна.
Нативный двоичный файл, который мы только что создали, отлично работает на машине, на которой мы только что его создали, но когда вы запустите его на другом компьютере с Windows, вы можете увидеть следующую ошибку:
Оказывается, что наш нативный образ нуждается в msvcr100.dll из VS C ++ Redistributable 2010. Эта библиотека может быть размещен в том же каталоге, что и ехе, или в C:\Windows\System32. В настоящее время ведется работа, чтобы попытаться решить эту проблему.
С GraalVM для Java 11 мы получаем аналогичную ошибку, за исключением того, что он сообщает о другой отсутствующей DLL, VCRUNTIME140.dll :
На данный момент нам нужно будет распространить эти библиотеки DLL вместе с нашим приложением или попросить наших пользователей загрузить и установить Microsoft Visual C ++ 2015 Redistributable Update 3 RC, чтобы получить VCRUNTIME140.dll для нативных образов на основе Java 11 или Microsoft Visual C++ 2010 SP1 Redistributable Package (x64) для получения msvcr100.dll для нативных образов на основе Java 8.
GraalVM не поддерживает кросс-компиляцию, хотя может в будущем. На данный момент нам нужно скомпилировать в Linux, чтобы получить исполняемый файл Linux, скомпилировать в MacOS, чтобы получить исполняемый файл MacOS, и скомпилировать в Windows, чтобы получить исполняемый файл Windows.
Вывод
Приложения командной строки - это канонический вариант использования нативных образов GraalVM: теперь мы можем разрабатывать на Java (или другом языке JVM) и распространять наши приложения CLI как один относительно небольшой собственный исполняемый файл. (За исключением Windows, где нам может потребоваться распространить дополнительную динамическую DLL.) Быстрый запуск и уменьшенный объем памяти - приятные бонусы.
У нативных образов GraalVM есть некоторые ограничения, и приложениям, возможно, потребуется проделать некоторую работу, прежде чем их можно будет превратить в нативный образ.
Picocli упрощает написание приложений командной строки на многих языках, основанных на JVM, и предоставляет несколько дополнительных функций, позволяющих безболезненно превратить ваши приложения CLI в нативные образы.
Попробуйте Picocli и GraalVM в вашем следующем приложении командной строки!