- .dmg распространяется неподписанным; инструкция для конечного пользователя (правый клик → «Открыть» или снятие карантина xattr) — в README - хеш и путь текущего установщика 0.1.1 - сборки Windows/Linux отложены: репозиторий в Gitea, раннеры есть у GitHub Actions; локальная часть (desktop-check, desktop-release --target) готова
33 KiB
desktop — desktop-обёртка glchat (Tauri 2)
Тот же веб-клиент из web/, обёрнутый в Tauri 2 (docs/client-tauri.md). Отдельного
UI-кода нет: страница инстанса загружается в системный webview, а обёртка даёт
платформенную интеграцию — трей, нативные уведомления, глобальный push-to-talk,
автозапуск, deep links и автообновление.
desktop/
└── src-tauri/ # Rust-проект обёртки
├── src/ # окно, трей, шорткаты, уведомления, deep links, обновления
├── capabilities/ # allowlist IPC: локальная страница настроек и домен инстанса
├── ui/ # локальная страница настроек обёртки (tauri://localhost)
├── icons/ # иконки приложения (сгенерированы из web/public/icons)
└── tauri.conf.json # конфигурация сборки и плагинов
Сборка и запуск
make desktop-check # cargo fmt --check, clippy, тесты (кэш в .cache/)
make desktop-build # бандлы текущей ОС (macOS: .app и .dmg)
make desktop-build-app # только .app + артефакты обновления, без .dmg
open desktop/src-tauri/target/release/bundle/macos/glchat.app
Требуется Rust 1.77+ и Node 22 (Tauri CLI ставится через npx). Кэши cargo и
npm держатся в .cache/ — как у остальных инструментов проекта.
make desktop-build-app (tauri build --bundles app) нужен там, где .dmg
собрать нельзя: bundle_dmg.sh монтирует образ через hdiutil и управляет
Finder через AppleScript, поэтому из песочницы агента и из headless-окружения он
падает (см. «Что не сделано»).
open --args из песочницы агента аргументы не доставляет (проверено на
macOS 27: и на бандле glchat, и на системном TextEdit — процесс запускается без
них; из обычного терминала то же самое работает). Поэтому make desktop-run ARGS='--minimized' и замер свёрнутого окна в scripts/desktop-perf.py надо
запускать из обычного терминала, а не из песочницы.
Сборка релиза с артефактами автообновления использует ключ подписи:
# ключ и пароль не коммитятся: приватный ключ живёт в CI-секретах,
# локально — в build/desktop-keys/ (каталог build/ в .gitignore)
npx @tauri-apps/cli@^2 signer generate -w build/desktop-keys/glchat-updater.key -p '…'
make desktop-build
Публичный ключ уже вписан в src-tauri/tauri.conf.json и в UPDATER_PUBKEY
(src/updates.rs). Без локального ключа make desktop-build собирает бандл без
артефактов обновления, чтобы сборка не падала у тех, у кого ключа нет.
Аргументы командной строки
| Аргумент | Что делает |
|---|---|
--instance <url> |
адрес инстанса вместо сохранённого (--instance=https://… тоже работает) |
--minimized |
запуск свёрнутым в трей (используется автозапуском) |
--settings |
сразу открыть страницу настроек обёртки |
--autostart on|off |
включить или выключить автозапуск при установке скриптом |
--update-now |
сразу проверить обновление и поставить его без диалога (тихое обновление) |
Настройки
Хранятся в settings.json каталога конфигурации приложения
(~/Library/Application Support/su.mhspx.glchat/ на macOS):
| Ключ | Смысл |
|---|---|
instanceUrl |
адрес клиента, по умолчанию https://gl.mhspx.su |
closeToTray |
закрытие окна сворачивает приложение в трей |
autostart |
запуск при входе в систему (--minimized) |
notifications |
показывать системные уведомления |
pttShortcut, muteShortcut, deafenShortcut |
глобальные комбинации (пусто — выключено) |
Меняются из меню трея и со страницы настроек обёртки (трей → «Настройки обёртки…»). Сессия и токены живут только в cookie webview: обёртка их не хранит.
Deep links
Схема glchat:// (регистрируется бандлом):
glchat://invite/<code>→ страница приглашения;glchat://guild/<guildId>/channel/<channelId>→ сервер и комната.
Ссылка, пришедшая до загрузки клиента (холодный старт), ждёт в очереди и
доставляется после desktop_ready.
Автообновление
Обёртка проверяет обновления сама: фоновая проверка через 30 секунд после старта
и вручную — трей → «Проверить обновления…». Адрес манифеста —
GET {instance}/updates/{target}/{arch}/{current_version} (target:
darwin/windows/linux, arch: aarch64/x86_64; см. src/updates.rs).
Найденное обновление показывается в интерфейсе, установка спрашивается
отдельным диалогом («во время звонка перезапуск без согласия недопустим»).
Для управляемых машин есть тихий режим — флаг --update-now:
open -a /Applications/glchat.app --args --update-now # из обычного терминала
С этим флагом обёртка проверяет обновление сразу при старте (не через 30 секунд) и ставит найденное без вопросов, после чего перезапускается; диалогов нет вовсе, а если обновления нет — приложение просто работает дальше. По умолчанию флаг выключен, поведение обычного запуска не меняется. Подпись артефакта проверяется в обоих режимах: флаг убирает только вопрос пользователю, но не проверку (docs/DECISIONS.md, D-077).
В журнале (~/Library/Logs/su.mhspx.glchat/glchat.log) видны и версия запущенной
сборки, и ход обновления:
обёртка запущена: версия=0.1.0, адрес=https://gl.mhspx.su, окно=видимо, автозапуск=true
тихое обновление по флагу --update-now: проверка сразу при старте
доступно обновление 0.1.1
тихое обновление по флагу: 0.1.0 → 0.1.1
обновление 0.1.1 установлено, перезапуск
обёртка запущена: версия=0.1.1, …
Манифест отдаёт сам инстанс (internal/server/updates.go), отдельная статика в
Caddy не нужна: и встроенный Caddy профиля, и внешний прокси стенда проксируют
весь домен на приложение, поэтому /updates/... доходит до него без правок
чужих конфигов.
Раскладка каталога (UPDATES_DIR; на стенде /opt/glchat/updates, в контейнере
смонтирован как /app/updates:ro):
/opt/glchat/updates/
└── darwin/aarch64/
├── glchat.app.tar.gz # артефакт релиза (создаёт tauri build)
├── glchat.app.tar.gz.sig # подпись ed25519 (формат minisign)
├── 0.1.1.json # манифест текущего релиза
└── latest.json # манифест последнего релиза — его и отдаёт инстанс
Манифестов прошлых релизов в каталоге быть не должно: обёртка спрашивает
/updates/{target}/{arch}/{своя версия}, и оставшийся <старая версия>.json
перекрыл бы новый релиз — клиент со старой версией получил бы «обновлений нет».
Публикующий скрипт сам убирает такие файлы (см. ниже).
Выпуск релиза
Версия живёт в одном месте — файл VERSION в корне репозитория; из него её
берут Makefile (версия сервера и образов), deploy/install.sh (IMAGE_TAG)
и desktop-обёртка (desktop/src-tauri/tauri.conf.json + Cargo.toml — их
значения должны совпадать с VERSION).
-
Поднять версию:
echo 0.1.2 > VERSION # версия сервера и обёртки # те же значения — в desktop/src-tauri/tauri.conf.json, Cargo.toml, Cargo.lock make web-build && make build # сервер собирается с новой версией -
Проверки и сборка бандла:
make go-lint && go test -tags sqlite_fts5 -race -count=1 ./internal/... cd web && npm run check && npm run lint && npm test && cd .. make desktop-check # fmt, clippy, cargo test make desktop-build # .app + .dmg + артефакты обновления -
Зафиксировать и пометить релиз:
git add VERSION Makefile deploy/install.sh desktop/src-tauri scripts/desktop-release.sh git commit -m "release: 0.1.2" git tag -a v0.1.2 -m "glchat 0.1.2" && git push origin main --tags -
Опубликовать обновление на инстансе:
sudo make desktop-release # локальный инстанс make desktop-release-upload HOST=user@server # по SSH (sudo спросит пароль) make desktop-release-upload HOST=user@server \ GLCHAT_SSH_OPTS="-i ~/.ssh/key -o IdentitiesOnly=yes" # свой ключ, без TTY (CI) make desktop-release ARGS='--dry-run' # посмотреть план -
Проверить канал и живьём обновиться со старой версии — см. следующий раздел.
Скрипт scripts/desktop-release.sh берёт версию из tauri.conf.json, сам
определяет target/arch по имени артефакта, копирует артефакт и .sig, собирает
манифест (version, notes, pub_date, platforms.<target>-<arch>.signature/url),
кладёт его и как <version>.json, и как latest.json, а манифесты прошлых
релизов убирает. Несколько платформ одной версии (macOS + Windows + Linux)
складываются в один манифест: запускайте скрипт по разу на артефакт. Каталог
обновлений на инстансе обычно принадлежит root — тогда rsync идёт через sudo;
если каталог записываем (стенд), sudo не нужен и загрузка работает без
терминала.
latest.json нужен потому, что обёртка приходит со своей установленной
версией: «новее ли релиз» решает клиент сравнением semver
(tauri-plugin-updater), поэтому на запрос любой версии сервер отдаёт
последний манифест, а если есть манифест конкретной версии — его. Без этого
обновление не получил бы никто, кроме тех, у кого уже стоит последняя версия.
Владельцу прокси добавлять ничего не нужно: /updates/... уходит в
catch-all handle и доходит до приложения (так же, как статика клиента).
Вариант «раздавать обновления статикой мимо приложения» требует правила
rewrite на latest.json с подстановкой target/arch в Caddy — лишняя
работа без выгоды, поэтому не рекомендуется.
Проверка (стенд):
curl -s https://gl.mhspx.su/updates/darwin/aarch64/0.1.0 | jq . # манифест для старой версии
curl -sI https://gl.mhspx.su/updates/darwin/aarch64/glchat.app.tar.gz | head -3 # 200 + размер
# в логе обёртки (~/Library/Logs/su.mhspx.glchat/glchat.log) —
# «обновлений нет» (манифест разобран) вместо
# «проверка обновлений не удалась: … 404»
Порядок обновления: проверка → уведомление в интерфейсе → скачивание → подтверждение в диалоге (во время звонка перезапуск без согласия не делается) → подпись проверяется до установки, downgrade отклоняется.
Приёмка релиза на живом инстансе (26.09.2026, 0.1.0 → 0.1.1)
Проверено на боевом канале стенда, без локальных заглушек:
- манифест опубликован
scripts/desktop-release.sh --remote, артефактglchat.app.tar.gz(3 204 953 Б) и подпись лежат в/opt/glchat/updates/darwin/aarch64/; - обёртка 0.1.0, запущенная с
--update-now, в журнале (UTC) показала:доступно обновление 0.1.1→тихое обновление по флагу: 0.1.0 → 0.1.1→обновление 0.1.1 установлено, перезапуск→обёртка запущена: версия=0.1.1→обновлений нет(17:08:23–17:08:25Z); - запрос старой версии
GET /updates/darwin/aarch64/0.1.0отдаёт манифест 0.1.1 — это и есть та точка, где раньше мешал оставшийся0.1.0.json; /Applications/glchat.appпосле установки — 0.1.1, ярлык на рабочем столе (~/Desktop/glchat.app) указывает на него.
Важно про место установки. tauri-plugin-updater распаковывает обновление
во временный каталог и переносит бандл rename(2). Если приложение лежит на
другом томе, чем временный каталог (например, на внешнем диске
/Volumes/…, а TMPDIR — на загрузочном), установка падает с
установка обновления не удалась: Cross-device link (os error 18), хотя
скачивание и проверка подписи проходят. Поэтому приложение для автообновления
надо держать на загрузочном томе — так делает .dmg (ставит в /Applications);
ярлык на рабочем столе лучше вести на /Applications/glchat.app, а не на
сборку в репозитории.
Локальная проверка «поверх старой версии» (без стенда)
Установка обновления проверена локально на настоящем сервере инстанса: сборка 0.1.0 → манифест 0.1.1 → скачивание → проверка подписи → установка → перезапуск на 0.1.1. Порядок действий (все данные — в каталоге репозитория, стенд не затрагивается):
# 1. артефакт текущей версии и приложение 0.1.0 (с ключом подписи)
make desktop-build-app
# 2. версия 0.1.1 в desktop/src-tauri/tauri.conf.json и Cargo.toml, затем:
make desktop-build-app
# 3. раскладка манифеста в свой каталог, ссылки — на локальный сервер
bash scripts/desktop-release.sh --artifact \
desktop/src-tauri/target/release/bundle/macos/glchat.app.tar.gz \
--version 0.1.1 --base-url http://127.0.0.1:8100 --dir /tmp/updates
# 4. настоящий сервер инстанса со своим каталогом обновлений и клиентом
LISTEN_ADDR=127.0.0.1:8099 DATA_DIR=/tmp/glchat-e2e/data WEB_ROOT=$PWD/web/dist \
UPDATES_DIR=/tmp/updates DOMAIN=127.0.0.1:8099 TLS_ENABLED=false \
APP_VERSION=0.1.0-local SESSION_PEPPER=… MASTER_KEY=… TOTP_ENCRYPTION_KEY=… \
./build/glchat
# 5. запуск 0.1.0 с тихим обновлением (адрес инстанса — локальный)
/Applications/glchat.app/Contents/MacOS/glchat-desktop \
--instance http://127.0.0.1:8099 --update-now
Что нужно знать, повторяя это:
- локальный стенд — http, а
tauri-plugin-updaterпо умолчанию требуетhttps(«The configured updater endpoint must use a secure protocol»), для проверки сборку делают с временным флагомtauri build --config '{"plugins":{"updater":{"dangerousInsecureTransportProtocol":true}}}'. В релизной сборке этого флага быть не должно: боевой адрес —https://; - приложение должно лежать на пути без символических ссылок: macOS-проверка
tauri-utilsотвергаетcurrent_exe()с symlink-компонентом, поэтому/tmp/glchat-e2e/…не годится (/tmp— ссылка на/private/tmp), а/private/tmp/glchat-e2e/…годится. Тот же смысл у требования ставить приложение в/Applications, а не запускать через ссылку; --update-nowобязателен: без него установку спрашивают диалогом, а кликнуть в headless-прогоне некому;- диагностика в логе:
GET /updates/darwin/aarch64/0.1.0в журнале сервера, затемдоступно обновление 0.1.1→тихое обновление по флагу: 0.1.0 → 0.1.1→обновление 0.1.1 установлено, перезапуск→ строка запуска сверсия=0.1.1и запрос манифеста уже для0.1.1; - проверка подписи проверяется и «от обратного»: если испортить байт в
артефакте, установка обязана упасть с
The signature verification failed, а версия на диске — остаться прежней.
Уведомления и переход в канал
Решение «показывать ли уведомление» принимает веб-клиент (упоминания и личные сообщения, окно неактивно или комната не открыта), обёртка показывает системное уведомление и обновляет счётчик.
Клик по уведомлению: у tauri-plugin-notification на desktop нет колбэка
действия, поэтому реализовано лучшее доступное поведение — уведомление,
показанное при неактивном окне, запоминается, и когда приложение
активируется в течение 10 секунд (окно получило фокус или macOS прислала
Reopen), окно поднимается и открывается комната из уведомления
(glchat://guild/<id>/channel/<id>, для личной беседы glchat://dm/<id>).
Ограничение (честно): ОС не сообщает, что активация вызвана именно кликом по
уведомлению, поэтому переход сработает и при обычном возврате в приложение в
течение этих 10 секунд. Клик по уведомлению старше 10 секунд только поднимает
окно. Полноценный колбэк требует своего моста к UNUserNotificationCenter
(UNUserNotificationCenterDelegate) — отдельная задача.
Ресурсы и холодный старт
Замеры собраны скриптом scripts/desktop-perf.py (macOS arm64, Mac на M-серии,
инстанс gl.mhspx.su через внешний прокси; подробности — docs/PERF.md):
| Метрика | Значение | Бюджет (docs/client-tauri.md §1) |
|---|---|---|
Холодный старт (open → процесс → строка «загружена страница») |
773–898 мс (три прогона) | ≤ 2 с |
| Из них: запуск процесса | 74–78 мс | — |
| Из них: загрузка страницы клиента | 696–821 мс | — |
| RAM, клиент открыт и простаивает (RSS: обёртка + 4 процесса WebKit) | 254 МБ (117 + 137) | ≤ 350 МБ |
RAM, окно свёрнуто в трей (--minimized) |
256 МБ | ≤ 350 МБ |
| Физический след (Activity Monitor) | 106 МБ | — |
Наблюдения:
- свёрнутое в трей окно памяти не освобождает: webview с загруженным клиентом продолжает жить (так работают WKWebView и трей-режим) — экономия была бы только при выгрузке страницы, а это потеря мгновенного возврата в клиент;
- первый запуск сразу после сборки бандла (холодный кэш файловой системы и страницы) дал 1,26 с — тоже в бюджете;
- почти вся память — WebKit (страница клиента), сама обёртка занимает ~117 МБ.
Повторить замер:
make desktop-build-app # свежий .app (+ артефакты обновления)
python3 scripts/desktop-perf.py --runs 3 # 3 холодных старта + простой в трее
Что не сделано (открытые пункты Фазы 6)
- Автообновление проверено локально «поверх старой версии» (2026-09-26):
0.1.0 → манифест 0.1.1 → скачивание → проверка подписи → установка →
перезапуск на 0.1.1, подробности и команды — «Локальная проверка» выше.
Открытым остаётся публикация реального релиза на стенде (номер версии
поднимает оператор) и то, что локальная проверка шла по http с временным
флагом
dangerousInsecureTransportProtocol— на стенде канал https. - Аватары в уведомлениях (thumb/буква) и группировка — сейчас иконка приложения.
- Захват экрана и микрофон: разрешения macOS выдаёт webview, отдельных экранов-инструкций у обёртки нет.
- Подпись и нотаризация macOS — сознательно не делаются (решение владельца
26.09.2026): приложение распространяется неподписанным
.dmg, карантин снимает конечный пользователь (инструкция ниже). EV/Trusted Signing для Windows — тем же решением не делаем. .dmg: собирается из обычного терминала (Finder/AppleScript для раскладки окна); в песочнице агентаhdiutil createи AppleScript запрещены, но образ без оформления окна собирается флагом--sandbox-safeкbundle_dmg.sh(проверено 2026-09-26, см. ниже).- Windows/Linux-бандлы: конфигурация кросс-платформенная, но собирались и
проверялись только macOS-arm64. Сборки под другие ОС отложены: репозиторий
живёт в Gitea на мини-ПК, а
windows-latest/ubuntu-24.04-раннеры есть у GitHub Actions, поэтому владелец зеркалит проект в GitHub позже и запускает сборку там. Локальная часть готова и проверена:make desktop-check,scripts/desktop-release.sh --artifact … --target windows|linux(манифест обновления для каждой платформы складывается в общий файл версии).
Что осталось оператору (точные команды)
macOS: .dmg
bundle_dmg.sh создаёт образ через hdiutil create и раскладывает иконки через
AppleScript/Finder. Из песочницы агента и из headless-сессии обе операции
запрещены (hdiutil create → «Операция не разрешена», AppleScript → -10004).
Из обычного терминала на macOS:
cd /Volumes/Samsung/projects/glchat
make desktop-build # .app + .dmg + артефакты обновления
ls -lh desktop/src-tauri/target/release/bundle/dmg/
Если терминала с графической сессией нет (CI, headless), тот же образ
собирается без оформления окна — тем же скриптом Tauri с флагом
--sandbox-safe (он пропускает AppleScript и hdiutil create):
cd /Volumes/Samsung/projects/glchat
make desktop-build-app # .app и артефакты обновления
rm -rf /tmp/dmg-stage && mkdir -p /tmp/dmg-stage
cp -R desktop/src-tauri/target/release/bundle/macos/glchat.app /tmp/dmg-stage/
cd desktop/src-tauri/target/release/bundle/dmg
bash bundle_dmg.sh --volname glchat --volicon icon.icns \
--icon glchat.app 180 170 --app-drop-link 480 170 --hide-extension glchat.app \
--sandbox-safe glchat_0.1.0_aarch64.dmg /tmp/dmg-stage
hdiutil verify glchat_0.1.0_aarch64.dmg
Такой .dmg монтируется и содержит glchat.app и ссылку Applications, но без
красивой раскладки иконок — для локальных проверок и CI этого достаточно, для
публикации лучше собрать обычным make desktop-build.
Артефакты без .dmg (для автообновления) собираются где угодно:
make desktop-build-app.
macOS: подпись Developer ID и нотаризация — сознательно не делаем
Решение владельца (2026-09-26): подпись Developer ID и нотаризацию не
делаем — это платный аккаунт Apple Developer, а приложение распространяется
внутри своего круга. Распространяется неподписанный .dmg; действия при
первом запуске выполняет конечный пользователь (ниже). Подпись артефакта
обновления (ed25519, ключ инстанса) от этого не зависит и проверяется всегда —
её отсутствие или порча артефакта установку отменяют.
Что делает конечный пользователь (короткая инструкция, её же можно передать вместе с файлом):
- Открыть
.dmgи перетащитьglchatвApplications. - Первый запуск — правый клик по приложению → «Открыть» → «Открыть» в диалоге «разработчик не может быть проверен». Дальше приложение запускается обычным двойным кликом.
- Если macOS не показывает кнопку «Открыть» (карантин): в терминале
xattr -dr com.apple.quarantine /Applications/glchat.app, либо «Системные настройки → Конфиденциальность и безопасность → Всё равно открыть».
Текущий установщик релиза 0.1.1:
desktop/src-tauri/target/release/bundle/dmg/glchat_0.1.1_aarch64.dmg
(3,1 МБ, sha256 7f93609164dc5fdfe6255b88eaf909c02ba819a7e4fb79951cc50b142da825c0).
Собирается make desktop-build (или bundle_dmg.sh --sandbox-safe в headless).
Если однажды понадобится подпись, команды остаются прежними (переменные читает сам Tauri, в репозиторий они не попадают):
security find-identity -v -p codesigning # проверить наличие сертификата
export APPLE_SIGNING_IDENTITY="Developer ID Application: … (TEAMID)"
export APPLE_ID="…@example.com"
export APPLE_PASSWORD="app-specific-password"
export APPLE_TEAM_ID="TEAMID"
make desktop-build # Tauri подпишет и отправит на нотаризацию
xcrun stapler validate desktop/src-tauri/target/release/bundle/macos/glchat.app
spctl --assess --type execute --verbose=4 desktop/src-tauri/target/release/bundle/macos/glchat.app
Проверка «не подписано»: codesign -dv --verbose=4 <app> покажет Signature=adhoc.
Windows (x64): MSI/NSIS + подпись
Сборка идёт на Windows-машине или в CI (windows-latest), Rust + Node 22:
npx @tauri-apps/cli@^2 build --bundles nsis,msi
# подпись (EV/Trusted Signing или signtool с сертификатом):
signtool sign /fd SHA256 /tr http://timestamp.digicert.com /td SHA256 `
/f cert.pfx /p $env:CERT_PASSWORD `
desktop\src-tauri\target\release\bundle\nsis\glchat_0.1.0_x64-setup.exe
Артефакты обновления (*.nsis.zip/*.msi + .sig) публикуются тем же
скриптом: bash scripts/desktop-release.sh --artifact <файл> --target windows --arch x86_64.
Linux (AppImage + deb)
Сборка на Ubuntu 24.04/26.04 (нужны libwebkit2gtk-4.1-dev, librsvg2-dev,
patchelf, libssl-dev, build-essential):
sudo apt-get install -y libwebkit2gtk-4.1-dev librsvg2-dev patchelf libssl-dev build-essential
npx @tauri-apps/cli@^2 build --bundles appimage,deb
bash scripts/desktop-release.sh --artifact desktop/src-tauri/target/release/bundle/appimage/*.AppImage.tar.gz \
--target linux --arch x86_64
На Wayland нативная тряска окна при несохранённых изменениях не работает
(композитор запрещает set_position) — остаётся CSS-тряска контента; это
ожидаемое поведение, а не дефект.
Ключ подписи обновлений
Приватный ключ — только в CI-секретах (TAURI_SIGNING_PRIVATE_KEY,
TAURI_SIGNING_PRIVATE_KEY_PASSWORD); локально — build/desktop-keys/
(каталог в .gitignore). Публичный ключ уже вшит в сборку и меняется только
вместе с ключом: старые сборки отвергнут манифест, подписанный новым ключом.