Здравствуйте, эта статья не про аниме, но мы точно знаем как пропатчить 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.
Контакты в профиле.