Как пропатчить Intellij Idea для FreeBSD
Несмотря на уход из РФ, продукция этой компании все также остается важным инструментом разработчика ПО, который приходится постоянно обслуживать, обходя все препоны и преграды.
Intellij и FreeBSD
Помимо проблем с блокировками для РФ, есть еще проблема поддержки моей любимой операционной системы — FreeBSD, создатели Idea ее не жалуют и постоянно ломают поддержку в своих продуктах, препятствуя использованию в этом окружении.
Но конечно волю советского инженера не сломить, поэтому автор продолжает вращать на х#ю мнение этой замечательной компании в целом и причуды ее п#зданутых сотрудников в частности.
Потому что когда-то учил инженерное дело настоящим образом и уже патчил Idea вручную.
на примере самой популярной среды разработки Intellij Idea показываю как патчить софт на Java подручными средствами.
И класть пролетарский болт на любые зарубежные корпоративные законы и правила.
Изучение проблемы
При попытке обновления Idea в очередной раз самоубилась, поэтому была скачана последняя релизная сборка с официального сайта, версии:
251.25410.129
К великому сожалению, в этой версии при попытке запуска появляется искусственная ошибка с сообщением про неподдерживаемую ОС, запуск Idea на этом останавливается:
./bin/idea.sh [0.005s][warning][cds] Archived non-system classes are disabled because the java.system.class.loader property is specified (value = "com.intellij.util.lang.PathClassLoader"). To use archived non-system classes, this property must not be set **Start Failed** Internal error java.lang.UnsupportedOperationException: Unsupported OS:FreeBSD at com.intellij.openapi.application.PathManager.getLocalOS(PathManager.java:501) at com.intellij.openapi.application.PathManager.platformPath(PathManager.java:927) at com.intellij.openapi.application.PathManager.getDefaultConfigPathFor(PathManager.java:394) at com.intellij.openapi.application.PathManager.getCustomOptionsDirectory(PathManager.java:448) at com.intellij.openapi.application.PathManager.loadProperties(PathManager.java:710) at com.intellij.idea.Main.mainImpl(Main.kt:65) at com.intellij.idea.Main.main(Main.kt:47)
Исходный код Community версии Idea хостится на Github, поэтому класс из которого выбрасывается ошибка:
com.intellij.openapi.application.PathManager
легко можно найти поиском в репозитории.
Место ошибки выглядит вот так:
.. @ApiStatus.Internal public static @NotNull OS getLocalOS() { if (SystemInfoRt.isMac) { return OS.MACOS; } else if (SystemInfoRt.isWindows) { return OS.WINDOWS; } else if (SystemInfoRt.isLinux) { return OS.LINUX; } else if (SystemInfoRt.isUnix) { return OS.GENERIC_UNIX; } else { throw new UnsupportedOperationException("Unsupported OS:" + SystemInfoRt.OS_NAME); } } ..
Как видите, основная логика находится в другом классе:
com.intellij.openapi.util.SystemInfoRt
который как раз и отвечает за определение ОС из переменных окружения.
Основная логика выглядит следующим образом:
.. public static final String OS_NAME; public static final String OS_VERSION; static { String name = System.getProperty("os.name"); String version = System.getProperty("os.version").toLowerCase(Locale.ENGLISH); if (name.startsWith("Windows") && name.matches("Windows \\d+")) { // for whatever reason, JRE reports "Windows 11" as a name and "10.0" as a version on Windows 11 try { String version2 = name.substring("Windows".length() + 1) + ".0"; if (Float.parseFloat(version2) > Float.parseFloat(version)) { version = version2; } } catch (NumberFormatException ignored) { } name = "Windows"; } OS_NAME = name; OS_VERSION = version; } private static final String _OS_NAME = OS_NAME.toLowerCase(Locale.ENGLISH); public static final boolean isWindows = _OS_NAME.startsWith("windows"); public static final boolean isMac = _OS_NAME.startsWith("mac"); public static final boolean isLinux = _OS_NAME.startsWith("linux"); public static final boolean isFreeBSD = _OS_NAME.startsWith("freebsd"); public static final boolean isSolaris = _OS_NAME.startsWith("sunos"); public static final boolean isUnix = !isWindows; public static final boolean isXWindow = isUnix && !isMac; ..
Казалось бы все ок и проблем не видно.
Для проверки я создал простенький shebang-скрипт (да Java теперь тоже так умеет), в который запихнул логику из класса SystemInfoRt
:
#!/usr/local/openjdk24/bin/java --source 11 import java.util.Locale; public class batchjob { final static class SystemInfoRt { public static final String OS_NAME; public static final String OS_VERSION; static { String name = System.getProperty("os.name"); String version = System.getProperty("os.version") .toLowerCase(Locale.ENGLISH); if (name.startsWith("Windows") && name.matches("Windows \\d+")) { // for whatever reason, JRE reports "Windows 11" as a name and // "10.0" as a version on Windows 11 try { String version2 = name.substring("Windows".length() + 1) + ".0"; if (Float.parseFloat(version2) > Float.parseFloat(version)) { version = version2; } } catch (NumberFormatException ignored) { } name = "Windows"; } OS_NAME = name; OS_VERSION = version; } private static final String _OS_NAME = OS_NAME.toLowerCase(Locale.ENGLISH); public static final boolean isWindows = _OS_NAME.startsWith("windows"); public static final boolean isMac = _OS_NAME.startsWith("mac"); public static final boolean isLinux = _OS_NAME.startsWith("linux"); public static final boolean isFreeBSD = _OS_NAME.startsWith("freebsd"); public static final boolean isSolaris = _OS_NAME.startsWith("sunos"); public static final boolean isUnix = !isWindows; public static final boolean isXWindow = isUnix && !isMac; public static final boolean isJBSystemMenu = isMac && Boolean.parseBoolean(System .getProperty("jbScreenMenuBar.enabled", "true")); public static final boolean isFileSystemCaseSensitive = isUnix && !isMac || "true".equalsIgnoreCase(System .getProperty("idea.case.sensitive.fs")); private SystemInfoRt() {} } public static void main(String[] args) { System.out.println("name: '" + System.getProperty("os.name")+"'"); System.out.println("unix:" + SystemInfoRt.isUnix); } }
Запуск показал что логика полностью рабочая:
Так как же тогда получилось что правильная логика не работает?
версия класса в релизной версии отличается от версии в репозитории.
Чтобы в этом убедиться, достаточно декомпилировать PathManager.class
из релизной версии Idea:
Как видите проверки на isUnix
тут нет:
.. else if (SystemInfoRt.isUnix) { return OS.GENERIC_UNIX; } ..
Что и порождает эту искусственную ошибку.
Исправление
Проблема найдена, что уже неплохо, но к сожалению чтобы ее исправить штатным путем надо:
- внести правку в код класса PathManager;
- пересобрать как минимум библиотеку, в которой этот класс находится;
- скопировать обновленную библиотеку в дистрибутив Idea.
Помимо того что все ваши правки, внесенные таким способом удалятся при следующем обновлении Idea, есть еще проблема самой сборки:
в Idea давно есть нативные библиотеки, поэтому полная сборка среды из исходников под FreeBSD представляет определенную проблему.
Даже если у вас получится собрать, поддерживать такое долговременно не выйдет без серьезных усилий.
Получается вариант «специальной олимпиады» короч.
Так что мы пойдем другим путем ультранасилия:
Java позволяет частичную пересборку с использованием готовой бинарной сборки с этими же классами
Так что можно взять рабочу версию PathManager.java
из репозитория и собрать его локально, используя библиотеки из бинарной сборки Idea.
Целиком скрипт выглядит вот так:
#!/usr/local/bin/bash # путь к распакованной Intellij Idea export IDEA=/opt/app/idea-IC-251.25410.129/lib # скачивание рабочей версии PathManager.java curl https://raw.githubusercontent.com/JetBrains/intellij-community/refs/heads/master/platform/util/src/com/intellij/openapi/application/PathManager.java -o PathManager.java # создаем каталоги пакетов mkdir -p com/intellij/openapi/application # компилируем класс с использованием библиотек из Idea javac -cp .:$IDEA/annotations.jar:$IDEA/util.jar:$IDEA/util_rt.jar:$IDEA/util-8.jar com/intellij/openapi/application/PathManager.java # создаем .jar-файл с пропатченной версией jar cf patch.jar com/intellij/openapi/application/*.class
В результате выполнения в текущем каталоге должен появиться файл patch.jar с исправленной версией PathManager
.
Но сборка патча лишь половина проблемы, вторая половина — как его теперь установить не привлекая внимание санитаров.
Установка патча
В Java есть т. н. «иерархия загрузчиков классов» и учет порядка библиотек (JAR-файлов), указываемых в CLASSPATH при запуске приложения.
Это означает, что если указать JAR с нашим патчем в списке CLASSPATH до JAR с оригиналом класса, то будет загружен и инициализирован класс из патча, а оригинал — пропущен.
В скрипте запуска Idea (файл bin/idea.sh
) есть перечисление стартовых библиотек:
.. CLASS_PATH="$IDE_HOME/lib/platform-loader.jar" CLASS_PATH="$CLASS_PATH:$IDE_HOME/lib/util-8.jar" CLASS_PATH="$CLASS_PATH:$IDE_HOME/lib/util.jar" CLASS_PATH="$CLASS_PATH:$IDE_HOME/lib/app-client.jar" ..
Достаточно скопировать файл с патчем в каталог lib
и добавить его в этот список до оригинала util-8.jar
.
.. CLASS_PATH="$IDE_HOME/lib/platform-loader.jar" CLASS_PATH="$CLASS_PATH:$IDE_HOME/lib/patch.jar" CLASS_PATH="$CLASS_PATH:$IDE_HOME/lib/util-8.jar" CLASS_PATH="$CLASS_PATH:$IDE_HOME/lib/util.jar" CLASS_PATH="$CLASS_PATH:$IDE_HOME/lib/app-client.jar" ..
Применимость
Возможность переопределения и частичной перекомпиляции чужих классов — очень мощный инструмент, который неоднократно меня выручал.
Таким способом например можно вставлять отладочные строки внутрь классов Spring Framework, можно частично менять логику поведения чужих классов и все это без заморочек с их полной релизной сборкой.
К сожалению знают о таком методе очень небольшое количество разработчиков, так что надеюсь этой статьей открыл некоторым из читателей новые горизонты разработки на Java.