Как пропатчить 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 # создаем каталоги пакетов mkdir -p com/intellij/openapi/application # скачивание рабочей версии PathManager.java curl https://raw.githubusercontent.com/JetBrains/intellij-community/refs/heads/master/platform/util/src/com/intellij/openapi/application/PathManager.java -o com/intellij/openapi/application/PathManager.java # компилируем класс с использованием библиотек из 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.