личный блог

Статьи · rust

Pake: сайт в настольное приложение одной командой — разбор по исходникам

У меня в браузере есть вкладки, которые живут неделями: почта, трекер задач, мессенджер, пара веб-интерфейсов для серверов. Они теряются среди остальных сорока, закрываются вместе с окном и не попадают в переключатель приложений по Cmd+Tab. Обычное решение — завернуть сайт в отдельное приложение. Обычная цена — Electron, то есть свой экземпляр Chromium в каждой обёртке. Pake обещает то же самое одной командой и, по словам README, почти в двадцать раз легче.

У проекта 62 тысячи звёзд на GitHub, история с 14 октября 2022 года и 2241 коммит без учёта слияний. Он даёт программу, которая будет жить у пользователя рядом с почтой и рабочими сессиями. Поэтому я не стал ограничиваться README и прочитал исходники версии 3.17.6 (коммит 19c32e6 от 19 октября 2026 года). Приложение я не собирал и не запускал: всё, что ниже, взято из кода, истории репозитория и файлов его релизов на GitHub.

Вопросов у меня было три: что на самом деле делает «одна команда», откуда берётся малый размер и что окно Pake позволяет сайту, который в нём открыт. На третий вопрос ответ меня удивил, и он же определяет вывод в конце.

Что это и из чего состоит

Pake — это две программы в одном репозитории. Первая — CLI на TypeScript: он принимает адрес и параметры и собирает установщик. Вторая — шаблон настольного приложения на Rust и Tauri 2, который CLI каждый раз компилирует заново, подставив твои настройки.

Часть Где лежит Объём
CLI bin/, TypeScript 5845 строк
Приложение src-tauri/src/, Rust 6131 строка
Скрипты, внедряемые в страницу src-tauri/src/inject/, JavaScript 3522 строки
Тесты tests/: модульные, браузерные, интеграционные 12 278 строк

У CLI 54 опции: размеры окна, иконка, скрытый заголовок, трей, глобальная клавиша вызова, прокси, свой CSS и JS, режим инкогнито, несколько окон. Готовых приложений в релизах 16, их список лежит в default_app_list.json: ChatGPT, Gemini, YouTube, Notion, X, WeChat и другие. Собирает их конвейер GitHub Actions той же командой pake, которая доступна пользователю.

Проект держится на одном человеке: из 2241 коммита 1397 подписаны его именем Tw93. Второй по числу коммитов участник сделал 301.

Что делает «одна команда»

В README всё выглядит так:

pnpm install -g pake-cli
pake https://github.com --name GitHub

За этой строкой стоит компиляция Rust-проекта на твоей машине. Порядок такой, по bin/builders/BaseBuilder.ts и bin/utils/build-workspace.ts:

  1. CLI ищет rustc. Если его нет, спрашивает разрешения и ставит Rust через https://sh.rustup.rs (на Windows — через winget). Без терминала, например в CI, не спрашивает, а завершается с ошибкой.
  2. Копирует шаблон src-tauri во временный каталог pake-build-*. Установленный пакет при этом не меняется.
  3. Пишет в копию конфигурацию: адрес, размеры окна, иконку, твои CSS и JS.
  4. Запускает tauri build. Каталог target с результатами компиляции общий для всех сборок и защищён файлом блокировки, поэтому вторая сборка идёт быстрее первой.

Время первой сборки CLI называет сам: «5–10 минут», на Windows — «10–15 минут». Таймаут на сборку — 15 минут. На Linux нужны системные пакеты WebKitGTK, на Windows — Visual Studio Build Tools или MinGW (--windows-toolchain gnu). Так что «одна команда» верна только во второй раз. В первый раз это установка Rust-тулчейна и компиляция зависимостей: Cargo.lock перечисляет 567 пакетов.

Для тех, кто не хочет ставить Rust, есть сборка в GitHub Actions: форк, параметры в форме, готовый установщик в артефактах.

Почему пакет маленький и где это не так

Малый размер получается из двух вещей. Первая — в установщик не входит браузерный движок. Tauri открывает страницу в системном webview: WKWebView на macOS, WebView2 на Windows, WebKitGTK на Linux. Вторая — профиль сборки, настроенный на размер, а не на скорость:

[profile.release]
panic = "abort"
codegen-units = 1
lto = "thin"
opt-level = "z"
strip = true

opt-level = "z" — оптимизация по размеру, strip убирает символы, panic = "abort" выкидывает код раскрутки стека. Для обёртки над сайтом скорость Rust-части почти не важна: вся работа идёт в webview.

Обещание «обычно меньше 10 МБ» я сверил по файлам последнего релиза. Вот приложение ChatGPT из V3.17.6:

Формат Размер
.msi для Windows 3,6 МБ
.deb для Linux 4,6 МБ
.dmg для macOS 9,5 МБ
.AppImage для Linux 76,7 МБ

.dmg вдвое больше остальных, потому что релизы для macOS собираются с --multi-arch: в одном файле два бинарника, для Intel и для Apple Silicon. AppImage — исключение из обещания: он везёт WebKitGTK с собой, чтобы запускаться на любом дистрибутиве. Сравнение «в 20 раз меньше Electron» по репозиторию не проверить, своих замеров Electron-сборок в нём нет. Порядок величин правдоподобен.

У системного webview есть обратная сторона. Один и тот же сайт открывается тремя разными движками, и на каждом ведёт себя по-своему. Это видно по коду: строка User-Agent по умолчанию на macOS и Linux выдаёт себя за Safari 26, на Windows — за Chrome 147. На Windows и Linux движку дополнительно передаётся --disable-blink-features=AutomationControlled, чтобы сайты не принимали окно за автоматизированный браузер. В заметках к V3.17.3 есть пример такой подгонки: Linux-сборки перестали представляться Chrome, и у YouTube заработали субтитры.

Что Pake делает со страницей

Окно Pake — это не «голый» webview. Перед загрузкой сайта Rust-часть внедряет в него до 13 скриптов, часть из них зависит от опций: в window.rs они подключаются через initialization_script. Самый крупный, event.js на 1666 строк, добавляет горячие клавиши (назад, вперёд, масштаб, копирование адреса), перехватывает скачивания, переписывает window.open и чинит ввод через китайские методы ввода в WebKit.

Строчка README про «удаление рекламы» — это CSS. В style.js лежат правки для 13 групп сайтов, и каждая применяется только на своём домене. Для YouTube правка такая:

#masthead-ad {
  display: none !important;
}

Для X скрываются продвигаемые посты ([data-testid="placementTracking"] article) и ссылка на платное продвижение. Это не блокировщик: запросы к рекламным серверам уходят, просто блок не показывается. Свой CSS и JS можно добавить опцией --inject, тогда CLI склеит файлы в custom.js и встроит в приложение.

Отдельная деталь — скачивания. Ссылку на файл перехватывает скрипт и передаёт Rust-команде download_file. Та скачивает файл сама и с версии 3.17.3 подставляет куки и Referer окна, чтобы работали файлы за логином. Тут начинается главный вопрос разбора: какая страница может вызывать такие команды.

Ключевое решение: кто может вызывать команды приложения

В Tauri страница общается с Rust-частью через IPC: вызывает команды по имени. У Pake их 11: скачать файл, показать уведомление, поставить счётчик на иконку в Dock, изменить масштаб, перейти по адресу и ещё несколько. К ним добавляются разрешения плагинов: открыть ссылку в системном браузере, развернуть окно на весь экран. В конфигурации стоит "withGlobalTauri": true, то есть объект window.__TAURI__ есть на каждой странице, открытой в окне. Что из этого реально сработает, решает список разрешений — capability.

С 5 декабря 2024 года, когда вызовы перевели на Tauri 2 (первая версия с этим — V3.0.0 от 19 декабря), в capabilities/default.json было записано:

"remote": {
  "urls": ["https://*.*"]
}

Шаблон https://*.* подходит к любому сайту по HTTPS. Собственные команды приложения вообще не были занесены в манифест разрешений и проверку не проходили. Значит, команды Pake мог вызывать не только упакованный сайт, но и любая страница, на которую окно перешло. А переходить окно умеет: on_navigation в window.rs разрешает любые переходы, а опции --force-internal-navigation и --safe-domain специально оставляют внешние ссылки внутри окна, например для входа через корпоративный SSO.

Исправление вышло 30 сентября 2026 года коммитом 211e1eb9 («fix: restrict native IPC to configured origins») и попало в релиз V3.17.3 от 5 октября. Теперь команды занесены в манифест в build.rs, а доступ для сайта строится при запуске из адреса, который был упакован:

let origin = format!("{}://{}{}/*", url.scheme(), host.replace(':', "\\:"), port);
...
capability.identifier = "pake-configured-origins".into();
capability.local = false;
capability.remote = Some(CapabilityRemote { urls: origins });

Права получают схема, хост и порт упакованного адреса, и ничего больше. Поддомены, страницы входа и адреса, куда сайт перенаправил, их не получают. Хост с *, ? или скобками отвергается, чтобы шаблон не превратился в маску. В заметках к релизу автор прямо пишет, что упаковывать нужно конечный адрес, https://mail.google.com, а не https://gmail.com. Иначе почта откроется, но уведомления и скачивания через приложение работать не будут.

Само исправление сделано аккуратно. Есть модульные тесты на разбор адресов, а tests/integration/ipc-security.mjs собирает настоящее приложение, открывает в нём «чужой» источник и проверяет, что ему отказано. Но этот тест работает только на macOS и в CI не запускается: по AGENTS.md его гоняют вручную.

Обновлений у собранных приложений нет

У этой истории есть продолжение, которое важнее самого исправления. В Cargo.toml нет плагина обновлений Tauri, в коде нет проверки новых версий. Приложение, собранное Pake, остаётся таким, каким его собрали. Исправление в CLI не доходит до установленных программ, их нужно пересобрать.

Масштаб можно оценить по GitHub. С 19 декабря 2024 года по 4 октября 2026 года вышло 25 релизов с готовыми приложениями, и их файлы скачали 260 318 раз. Три релиза после исправления скачали 8105 раз. Как поступить с уже установленными обёртками:

Что уходит в сеть

Само приложение сторонних адресов в коде не содержит. Кроме упакованного сайта, в Rust-части и скриптах есть только ссылка на репозиторий Pake в меню. Данные сайта, то есть куки и локальное хранилище, лежат в профиле приложения. С --incognito они не сохраняются между запусками. Плагин tauri-plugin-oauth подключён в lib.rs, но разрешений на его команды в наборе нет, и в коде Pake он не вызывается.

CLI ходит в сеть больше, чем ожидаешь:

`https://img.logo.dev/${domain}?token=${token}&format=png&size=256`,
`https://cdn.brandfetch.io/${domain}/w/400/h/400?c=${key}`,
`https://logo.clearbit.com/${domain}?size=256`,
`https://www.google.com/s2/favicons?domain=${domain}&sz=256`,
`https://favicon.is/${domain}`,

Публичные ключи logo.dev и Brandfetch записаны прямо в bin/options/icon.ts. Для внутренних имён (localhost, IP-адреса, .local, .lan, .internal) CLI сначала ищет иконку по имени приложения в наборе dashboard-icons на jsDelivr. Если там её нет, внутренний домен уходит тем же пяти сервисам. Упаковал grafana.corp.internal без иконки — пять сторонних сервисов узнали имя хоста. Защита простая: всегда передавать --icon с локальным файлом.

Конвейер проекта собран аккуратно. Сторонние действия GitHub Actions закреплены по хешу коммита, официальные actions/* — по тегу. Собранный dist/cli.js хранится в репозитории, но CI проверяет, что он совпадает с исходниками в bin/. Файл не минифицирован и читается глазами.

Установка и запуск

Нужны Node.js 20.9 или новее (README рекомендует 22) и Rust 1.85 или новее. Сам репозиторий закрепляет тулчейн 1.95.0 в rust-toolchain.toml. На macOS сборка нацелена на версию 14.0. Пример с настройками окна из README:

pake https://weekly.tw93.fun --name Weekly \
  --icon https://cdn.tw93.fun/pake/weekly.icns \
  --width 1200 --height 800 --hide-title-bar

Для автоматизации есть --json: результат одним JSON-объектом в stdout, предупреждения отдельным массивом. Есть --config app.json со схемой в schema/pake.schema.json для описания приложения файлом. Есть упаковка локальной сборки фронтенда: pake ./dist --name MyTool. Для ИИ-агентов проект держит файл llms.txt с описанием этого контракта и готовый навык для агента с доступом к файлам и командной строке.

Лицензии

До 11 июня 2026 года Pake распространялся по MIT. С версии V3.11.10 он под GPL-3.0-or-later с отдельным исключением в LICENSE-EXCEPTION. Исключение говорит, что приложения, собранные стандартным процессом, не становятся GPL и их можно распространять на любых условиях, в том числе закрытых. Изменённая версия самого Pake остаётся под GPL. Отдельный TRADEMARK.md просит форки не называться Pake.

Зависимости приложения — Tauri и его плагины — под MIT или Apache-2.0. В CLI есть sharp (Apache-2.0) для обработки иконок, а он тянет собранную библиотеку libvips под LGPL-3.0. Она работает только при сборке и в готовое приложение не попадает.

Ограничения и риски

Кому подойдёт

Pake хорош в том, что обещает. Обёртка над сайтом весом 4–10 МБ, собранная одной командой и с нормальными горячими клавишами, — это удобно, и код за ней явно поддерживается: за последние три месяца в репозитории 265 коммитов, у сложных мест подробные комментарии со ссылками на номера задач, а модульных тестов 62 файла.

Мой вывод такой:

Олег Букатчук
Олег Букатчук
DevOps- и SRE-инженер. Строил отказоустойчивую инфраструктуру для классифайда, e-commerce и госсектора: cian.ru, aliexpress.ru, mos.ru.