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

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

Комментарии (25)


  1. ris58h
    26.05.2025 17:36

    jar cf patch.jar com/intellij/openapi/application/*.class

    Патч с одним только PathManager.class не сработает?


    1. alex0x08 Автор
      26.05.2025 17:36

      К сожалению нет тк есть вложенные классы. И подмена .class файлов (или манипуляции с байткодом) в оригинальном JAR тоже не сработают тк Idea при сборке подписывает все свои файлы и проверяет в рантайме.


      1. Yami-no-Ryuu
        26.05.2025 17:36

        Так в classpath можно указать и каталог.

        Ну и в догонку, есть curl -O , сохраняющая имя файла.


        1. alex0x08 Автор
          26.05.2025 17:36

          Так в classpath можно указать и каталог.

          Можно, но очень уж криво будет выглядеть, поскольку файл класса получается не один (внутри есть вложенные). Хотелось все же показать универсальное и повторяемое решение.


  1. Kahelman
    26.05.2025 17:36

    Месье знает толк в извращениях :)


    1. alex0x08 Автор
      26.05.2025 17:36

      Конечно знаю, поэтому не рискнул переносить на Хабр эту статью, тут многих закоротит от такого.


      1. Siemargl
        26.05.2025 17:36

        Может и зря, тут сейчас наблюдается некоторая ретро-мода.


      1. Kahelman
        26.05.2025 17:36

        Прочитал - улет. :). Афтор жги исщо


    1. Yami-no-Ryuu
      26.05.2025 17:36

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


  1. pnmv
    26.05.2025 17:36

    Разрешите поинтересоваться, чем так привлекла имеено Intellij Idea?


    1. alex0x08 Автор
      26.05.2025 17:36

      чем так привлекла имеено Intellij Idea?

      она сама виновата, нефиг было падать.


  1. softwind
    26.05.2025 17:36

    Я пошёл другим путём: у меня idea запускается в docker, отображается на моих X-ах на макоси - делал проект под SCTP, ну и с дуру попробовал, а оно прижилось...


    1. alex0x08 Автор
      26.05.2025 17:36

      «А docker в яйце, а яйцо — в гробу, а гроб — на дереве. А деревьев там — тьма. И все в гробах.» Крепко смерть кощеевую запрятали однако!


  1. tenteaday
    26.05.2025 17:36

    почему бы не попробовать сначала запуститься с -Dos.name=Linux?


    1. alex0x08 Автор
      26.05.2025 17:36

      Тогда бы не было статьи ;)

      Ну и упало бы в другом месте, поскольку при os.name=Linux пошла бы попытка загрузить нативные библиотеки - то что я чинил в прошлой серии.


      1. tenteaday
        26.05.2025 17:36

        если взять >=11 java то не упало бы, ну а статьи да- не было.

        А вообще молодцы, уже починили.
        https://youtrack.jetbrains.com/issue/IJPL-184145 перейдите на 2025.1 https://www.jetbrains.com/idea/download/other.html


        1. alex0x08 Автор
          26.05.2025 17:36

          Ничего, отлетит в другом месте - официально BSD все также не поддерживается, поэтому ни тестовых сборок ни стендов с BSD у них нет, патчи принимают с неохотой и фактически в слепую.


          1. tenteaday
            26.05.2025 17:36

            это вопрос денег, и далеко не аренды тестовых серверов. Фря всегда была университетской, и денежного смысла совершенно нет.

            Кроме того - какие плюсы у использования фри по сравнению с линуксом? Для разработчика могу сказать что отсутствие докера, альтернативный шедулер, другая сеть и немного не так работающие утилиты - постоянный источник гемора.


            1. alex0x08 Автор
              26.05.2025 17:36

              Кроме того - какие плюсы у использования фри по сравнению с линуксом?

              Достаточно что нет и никогда не было чего-то такого или такого.

              Но конечно Фря не про удобство, если вас концептуально не напрягает, что одни только исходники ядра Linux стали занимать больше места чем вся FreeBSD целиком — думаю стоит оставаться на линуксе.


  1. DjUmnik
    26.05.2025 17:36

    А как же девиз Java: Write once, run anywhere?


    1. aleksandy
      26.05.2025 17:36

      Так оно и запускается. Если не использовать платформозависимые подгружаемые библиотеки и не вставлять искусственные проверки, приводящие к исключениям.


    1. spirit1984
      26.05.2025 17:36

      Сразу видно, никогда не пытались использовать на практике Java). Если серьезно, то именно поэтому сделать хорошее десктопное приложение на Java, которое выглядело бы действительно красиво - крайне нетривиальная задача. Поскольку для этого требуется слишком уж тесно использовать особенно взаимодействия конкретной ОС с графической частью


      1. aleksandy
        26.05.2025 17:36

        Нужно только захотеть.


  1. X-P0rt3r
    26.05.2025 17:36

    Решил по аналогии попробовать, вкорячивая последнюю версию Rider на Windows 7, которую уже не поддерживают все современные IDE. Но зависло уже на стадии окончания установки: "Starting post-installation steps in background process".


    1. alex0x08 Автор
      26.05.2025 17:36

      Если хотите "по аналогии", тогда одной попыткой установки не отделаетесь )

      Разбирайтесь теперь что именно пошло не так и где надо поправить. Начать стоит не с установки через инсталлятор а с бинарной сборки, вроде для Windows их тоже выкладывали в виде простого .zip архива.

      Если не найдете - у инсталляторов что MSI что InstallShield есть специальные ключи запуска для распаковки приложения из инсталлятора, без самой установки.