software-development
May 24

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

Несмотря на уход из РФ, продукция этой компании все также остается важным инструментом разработчика ПО, который приходится постоянно обслуживать, обходя все препоны и преграды.

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

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

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

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

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

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

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

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