Здравствуйте, эта статья не про аниме, но мы точно знаем как пропатчить Idea для FreeBSD. И не боимся об этом рассказывать.

Intellij и FreeBSD
Помимо проблем с блокировками пользователей из РФ, у команды Intellij есть еще явное предубеждение против моей любимой операционной системы — FreeBSD, поддержку которой они постоянно ломают в своих продуктах, препятствуя использованию в этом окружении.
Но волю советского инженера не сломить, поэтому автор продолжает цинично использовать то что нельзя там где это невозможно, невзирая на мнение этой замечательной компании и ее сотрудников.
Потому что когда-то учил инженерное дело настоящим образом и неоднократно патчил 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)
Эту же ошибку но в графическом исполнении вы можете узреть на заглавном скриншоте к статье — до такой степени в Jetbrains не любят BSD‑шников.
Исходный код сommunity-версии 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
который и отвечает за определение текущей ОС из переменных окружения.
Основная логика класса 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); } }
Запуск показал что логика рабочая, название ОС и признак isUnix определяются корректно:

Так как же тогда получилось что правильная логика не работает?
Все опросто:
версия класса в релизной версии отличается от версии в репозитории.
Чтобы в этом убедиться, достаточно декомпилировать PathManager.class из релизной сборки Idea:

Как видите проверки на isUnix тут нет:
.. else if (SystemInfoRt.isUnix) { return OS.GENERIC_UNIX; } ..
Что и порождает эту искусственную ошибку.
Исправление
Проблема найдена, что уже неплохо, но к сожалению чтобы ее исправить стандартным путем необходимо:
внести правку в код класса
PathManager;пересобрать как минимум библиотеку, в которой этот класс находится, как максимум — среду целиком;
скопировать обновленную библиотеку в дистрибутив Idea, либо использовать собранную кастомную версию.
Помимо того что все правки, внесенные таким способом удалятся при следующем обновлении Idea, есть еще проблема самой сборки:
в Idea давно присутствуют нативные библиотеки, поэтому полная сборка из исходников под FreeBSD представляет определенную проблему.
Да, можно попробовать собрать Idea и под Linux, если у вас есть возможность выкачать ~20Гб исходного кода и время для того чтобы развернуть весь тулчейн для сборки — вперед, это налучший вариант для долгосрочного сопровождения.
К сожалению в моем случае нужно было какое-то решение «здесь и сейчас», времени и сил для развертывания полноценной сборки всей Idea из исходников не было.
Так что мы пойдем другим путем ультрахардкора:
Java позволяет частичную пересборку с использованием готовой бинарной сборки с совпадающими классами.
Это означает что можно взять обновленную версию PathManager.java из репозитория и собрать локально только один этот класс, подложив в CLASSPATH библиотеки из бинарной сборки 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.
Но сборка патча лишь половина проблемы, вторая половина — как его теперь установить в текущую бинарную копию Idea не привлекая внимание санитаров.
Установка патча
В 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" ..
Ну и собственно результат:

Применимость
Это точно не последний случай, когда приходится вручную исправлять софт замечательной компании Jetbrains — альтернативные ОС там действительно не любят, поэтому описанная технология «кровавого патчинга» будет применяться во славу высоких технологий и прогресса еще не раз и не два.
Но мы BSD‑шники привычные — таков путь.
Добавлю, что возможность переопределения и частичной перекомпиляции чужих классов — очень мощный инструмент, который неоднократно меня выручал, поэтому вам точно стоит о нем знать.
Таким способом можно например вставлять отладочные строки внутрь классов Spring Framework, можно менять логику поведения классов из чужих библиотек и все это без заморочек с полной релизной сборкой, рефлексией, модификацией байт-кода или технологией Java Agent.
Этим же методом мы неоднократно делали бекпорт нужных фич, которые реализовывались только в новой версии библиотеки и никогда бы не появились в старой.
Собственно описываемый выше патч — пример такого бекпорта функционала, реализованного в текущей develop‑версии, но еще не перенесенного в релизную.
К сожалению известно о таком методе лишь очень небольшому количеству современных разработчиков, так что надеюсь этой статьей приоткрыл некоторым из читателей новые горизонты разработки на Java.
PS
Оригинал статьи как обычно в нашем блоге, опыт эксплуатации Idea в BSD‑системах — большой, поэтому смогу помочь и другим BSD‑шникам, если кому‑то из них вдруг придет в голову заниматься разработкой на BSD.
Контакты в профиле.
