[От редакции: статья отражает авторский опыт, и не может быть основанием для вопросов или претензий к компании Directum.]
По сути, особо незачем. DirectumRX прекрасно живёт на привычных Windows и Linux. Но, во-первых, как человеку убежденному, что каждый обязан разбираться в том, на что тратит большую часть жизни, вопрос "а можно ли?" уже давно не давал покоя. А если нельзя, то почему, и что нужно сделать, чтобы стало «льзя».
Во-вторых, маководы среди разработчиков Директума подавляющее меньшинство, но такие, как я, всё же встречаются. А macOS вместе с железкой, на которой она работает, развивается семимильными шагами: мобильный чип уже так вымахал, что соревноваться ему остаётся разве с вечностью. Я даже "продал" MacBook товарищу, показав, что его проект, который на мощном десктопе собирался несколько минут, на ноутбуке от батареи собрался за 20 секунд. Но мы отвлеклись.
Так вот: этих мощностей вполне хватит и тяжёлому DirectumRX вместе со всеми сервисами и студией разработки. Заодно проснулся спортивный интерес наглядно замерить эту разницу.
Сразу отвечу на главный вопрос: можно ли?
Спойлер: можно. Правда, вендор общается только с Windows и Linux, но нас это не остановит.
Исходные такие:
Обычно ею является утилита DirectumLauncher и набор консольных команд в файлах do.sh. Для визуального режима установки это работает следующим образом:

А дальше всё зависит от ОС: лаунчер смотрит на систему и выбирает идентификатор рантайма (rid) - win-x64 или linux-x64. На Windows сервисы живут в IIS, на Linux - в Docker. Есть ещё три хостовые .NET-утилиты (encryptor, certificate-tool, DBConverter) и пачка ELF-инструментов в tools/. Вот там-то и спрятаны основные проблемы для macOS.
ELF - бинарь Linux
Mach-O - бинарь Darwin, ядра, которое использует macOS
Это первый вопрос, который пришел в голову. Ответ: нет, и не надо. ELF и Mach-O - это не два контейнера с одинаковым содержимым: внутри разные системные вызовы, разные соглашения, а ещё разные архитектуры (AMD64 и ARM). Переупаковать без исходников - это уже не порт, а реверс-инжиниринг.
Поэтому я выбрал иную стратегию "не меняй бинарник, меняй где он запускается":
Если сократить до одной фразы: мы не учим Mac запускать Linux-бинарники, а просто даём им их Linux.
Сначала - маленькие правки, чтобы лаунчер вообще понял, что он на маке. Добавил is_macos(), ветку в do.sh (ищет Python 3.14 в трёх местах) и небольшую зависимость rpds-py для веб-установщика.
Вот так лаунчер узнаёт, что он на маке, и при этом продолжает думать, что пишет под Linux (runtime_methods.py):
def is_macos() -> bool:
return platforms.OperatingSystem.Current() is platforms.OperatingSystem.MACOSX
def get_rid_by_os() -> str:
# Для macOS возвращается linux-x64, сервисы и образы работают в Linux-контейнерах
if platforms.OperatingSystem.IsWindows():
return RuntimeIdentifiersBase.WINDOWS
return RuntimeIdentifiersBase.LINUX
А в do.sh появилась отдельная ветка для Darwin: она ищет Python 3.14 и не трогает Linux-часть:
#!/bin/bash
dp0="$(dirname "$(readlink -f "$0")")"
if [[ "$(uname -s)" == "Darwin" ]]; then
is_python_ok() {
[[ -x "$1" ]] && "$1" -c 'import sys; sys.exit(0 if sys.version_info >= (3, 14) else 1)' 2>/dev/null
}
...
"$python_bin_path" -s "$dp0/src/main.py" "$@"
exit $?
fi
Но самый вкусный кусок - обёртка для утилит. Вызывающий код по-прежнему собирает команду «запусти encryptor», но вместо самого encryptor подставляется маленький bash-скрипт-переводчик, который делает docker run --platform linux/amd64. В скрипте всё, что нужно: монтирование каталогов, рабочая папка и --add-host, чтобы утилита могла достучаться до базы на хосте.
Подмена делается в одном месте, в ToolBase, чтобы не править каждую утилиту отдельно (tool_base.py):
@property
def run_in_linux_container(self) -> bool:
return False
def _adapt_exe_for_host(self, exe_path: str) -> str:
if self.run_in_linux_container and runtime_methods.is_macos():
return linux_container_runner.create_wrapper(exe_path)
return exe_path
Так как некоторые утилиты являются ELF-бинарниками, обойдем это переносом их запуска в контейнере. У трёх утилит (encryptor, certificate-tool, DBConverter) флаг включается одной строкой:
class EncryptorTool(ToolBase):
@property
def run_in_linux_container(self) -> bool:
return True
И вот то, что получается - сам скрипт-переводчик encryptor.container.sh (пути указаны для примера):
#!/bin/bash
exec docker run --rm -i --platform linux/amd64 \
--add-host host.docker.internal:host-gateway \
--add-host <имя-машины>:<ip> \
-e HOME=/tmp -e DOTNET_BUNDLE_EXTRACT_BASE_DIR=/tmp/.net \
-w "$PWD" -v /Users/me/DirectumLauncher:/Users/me/DirectumLauncher \
--entrypoint /Users/me/DirectumLauncher/etc/.../encryptor \
mcr.microsoft.com/dotnet/runtime-deps:8.0 "$@"
И вот тут алилуя, ./do.sh enc encrypt выдал:

Бинарь запускается, можно двигаться дальше.
Если бы macOS был Linux, на этом бы всё закончилось. Но он не Linux, и лаунчер уже на первом шаге спотыкается о бытовуху:
inotify и root (sysctl.py):
@action
@runtime_methods.not_for_windows
@runtime_methods.not_for_macos
@Help.show_only_linux
def set_inotify_instances_limit(new_limit: int = 65536) -> None:
...
def _check_run_as_root() -> None:
if runtime_methods.is_macos():
return
if os.geteuid() != 0:
Проверка требования Linux (deploy_condition_utils.py):
if current_os is not platforms.OperatingSystem.MACOSX:
if current_os is not platforms.OperatingSystem.LINUX:
errors.append("Error: Required linux operating system'")
if current_architecture is not platforms.Architecture.x86_64:
errors.append("Error: Required 64 bit OS'")
SIP и точка входа (update_entrypoint.py):
def _get_entrypoint_dirs() -> list[str]:
if runtime_methods.is_macos():
existing = [d for d in ('/opt/homebrew/bin', '/usr/local/bin') if os.path.isdir(d)]
return existing or ['/usr/local/bin']
return [os.path.join('/', 'usr', 'bin')]
Сертификаты из Keychain (host_mappings.py):
result = subprocess.run(['security', 'find-certificate', '-a', '-p', *MACOS_KEYCHAINS],
capture_output=True, text=True, timeout=120, check=False)
...
if runtime_methods.is_macos():
return {_get_macos_ca_bundle(): {'bind': r'/etc/ssl/certs/ca-certificates.crt', 'mode': 'ro'}}
Сокет Docker (docker_tools.py):
_MACOS_DOCKER_SOCKET_CANDIDATES = (
'~/.docker/run/docker.sock', # Docker Desktop
'~/.orbstack/run/docker.sock', # OrbStack
'~/.colima/default/docker.sock', # Colima
'~/.rd/docker.sock', # Rancher Desktop
)
...
class docker_client(ContextDecorator):
def __enter__(self) -> DockerClient:
ensure_docker_host()
self.client = docker.from_env(timeout=300)
Веб-установщик. В дистрибутиве есть ./DirectumLauncher --host=0.0.0.0, но это ELF, и на маке он не заведется. На самом деле функционал его прост: он находит do.sh и вызывает действие installer run. Значит, на маке пишем то же самое, но просто руками: ./do.sh installer run --port=5001 (порт выбираем отличный от 5000, потому что на macOS он занят AirPlay Receiver, ловим форбидден). Внутри - Flask с socketio и статика.


Development Studio. Уже поинтереснее. Внутри пакета Electron 41 + .NET 8. JS и управляемые dll переносимы как есть, они кроссплатформенные, непереносимы только сам Electron, ELF-хост и три нативных библиотеки (SkiaSharp, HarfBuzzSharp, Magick.NET). Поэтому был сконструирован скрипт, который собирает DevelopmentStudio.app следующим образом:
Плагин получил патч: на маке он сам вызывает сборку, помнит из какого пакета собрано и делает ярлык в ~/Applications. Репозитории git настраиваются в блоке CONFIGURATIONS секции DevelopmentStudioDesktop (@GIT_ROOT_DIRECTORY и REPOSITORIES).
В плагине всё сводится к одной ветке if is_macos() (development_studio_desktop.py):
if is_macos():
return self._get_exe_path_on_macos(force_extract)
...
if not os.path.isfile(executable_path) or not self._is_macos_app_actual(package_path):
exit_code = process.try_execute(f'bash "{_MACOS_BUILD_SCRIPT}" "{package_path}" "{self._bin_path}"')
if exit_code != 0 or not os.path.isfile(executable_path):
raise RuntimeError(f"Cannot build {_MACOS_APP_NAME} for macOS (exit code: {exit_code}).")
self._write_macos_build_info(package_path)
А замена ELF-хоста - это маленький скрипт, который скрипт сборки кладёт на место Sungero.DevelopmentStudio.Desktop.Host:
#!/bin/bash
DIR="$(cd "$(dirname "$0")" && pwd)"
export DOTNET_ROLL_FORWARD="${DOTNET_ROLL_FORWARD:-Major}"
exec "$DOTNET" "$DIR/Sungero.DevelopmentStudio.Desktop.Host.dll" "$@"
Запускаем получившееся приложение. Выглядит неплохо.

Чтобы не плодить TL;DR процесс установки не будет продемонстрирован, он ничем не отличается от такового на Linux. Продемонстрирую что контейнеры поднялись и веб-клиент доступен.


Студия открывается, сторонний редактор кода открывается, плагины применяются.

Попробуем создать и опубликовать простую модификацию в перекрытии карточки сотрудника. Просто отправим сообщение "Hello, world!".


Сработало!

Небольшая оговорочка: сравнивать мак с Windows-машиной не совсем корректно. На Windows сервисы поднимает ServiceRunner, контейнеров нет, всё нативное. Плюс MacBook всё время работал от батареи. Но от того и результаты и интереснее. Проценты считаем относительно времени на Windows.
Импорт базового решения + публикация. Секундомер - с момента выбора базового пакета для импорта до зелёной галочки "Публикация завершена" и возврата работоспособности веб-клиента.
Разница почти в минуту, а за минуту можно сделать столько всего полезного!
Экспорт базового пакета. Секундомер - c момента нажатия кнопки экспорта до того, как студия предложит выбрать папку для сохранения пакета.
Вот это уже звучит как заявка на конспирологические теории о том, где зарыты рабочие часы.
Запуск студии. Секундомер - с момента запуска студии до конца skeleton-анимации и полной инициализации.
Похоже на погрешность, но всё-таки тоже результат.
Что по итогу? Мак оказался быстрее Windows во всех трёх замерах, от 13% до 88%, несмотря на то, что сервисы у него идут в контейнерах под бинарной трансляцией, а некоторые утилиты под эмуляцией через Docker. Замеры, естественно, не лабораторные, но направление понятно.
Важно. В 2020-м году Apple выпустила свои чипы на архитектуре ARM64, заменив тем самым Intel x86_64. И компания считает, что софт, который за 6 лет не был адаптирован разработчиками под новую для ОС архитектуру как интернет - не нужен. Для того чтобы переход был не такой резкий, был выпущен бинарный транслятор Rosetta 2, но по заявлению Apple он будет полностью вырезан (ну или сильно ограничен) в 2027 году, начиная с macOS 28. Это сделает невозможным работу того, что было описано в этой статье, alas. Хотя затронет ли это решение Docker не уточняется, поживем - увидим.

Отличная статья, как раз хотел купить макбук
давно хотел попробовать, очень полезно!
спасибо)
Авторизуйтесь, чтобы написать комментарий