Как стать автором
Обновить

Как пропатчить Intellij Idea для FreeBSD

Уровень сложностиСредний
Время на прочтение8 мин
Количество просмотров2.9K

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

Да, они опять решили напугать BSD-шников отсутствием официальной поддержки.
Да, они опять решили напугать BSD-шников отсутствием официальной поддержки.

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"
..

Ну и собственно результат:

Полностью рабочая Intellij Idea под FreeBSD последней версии
Полностью рабочая Intellij Idea под FreeBSD последней версии

Применимость

Это точно не последний случай, когда приходится вручную исправлять софт замечательной компании Jetbrains — альтернативные ОС там действительно не любят, поэтому описанная технология «кровавого патчинга» будет применяться во славу высоких технологий и прогресса еще не раз и не два.

Но мы BSD‑шники привычные — таков путь.

Добавлю, что возможность переопределения и частичной перекомпиляции чужих классов — очень мощный инструмент, который неоднократно меня выручал, поэтому вам точно стоит о нем знать.

Таким способом можно например вставлять отладочные строки внутрь классов Spring Framework, можно менять логику поведения классов из чужих библиотек и все это без заморочек с полной релизной сборкой, рефлексией, модификацией байт-кода или технологией Java Agent.

Этим же методом мы неоднократно делали бекпорт нужных фич, которые реализовывались только в новой версии библиотеки и никогда бы не появились в старой.

Собственно описываемый выше патч — пример такого бекпорта функционала, реализованного в текущей develop‑версии, но еще не перенесенного в релизную.

К сожалению известно о таком методе лишь очень небольшому количеству современных разработчиков, так что надеюсь этой статьей приоткрыл некоторым из читателей новые горизонты разработки на Java.

PS

Оригинал статьи как обычно в нашем блоге, опыт эксплуатации Idea в BSD‑системах — большой, поэтому смогу помочь и другим BSD‑шникам, если кому‑то из них вдруг придет в голову заниматься разработкой на BSD.

Контакты в профиле.

Теги:
Хабы:
+40
Комментарии25

Публикации

Работа

Java разработчик
178 вакансий

Ближайшие события