Как разработчику-одиночке распространять десктопные приложения без предупреждений безопасности
TuBrief 편집팀
2026년 7월 15일
0
Computing/Software원본 영상을 바탕으로 AI의 도움을 받아 작성했습니다. 원본 영상이 기준입니다.
커뮤니티의 다른 글
댓글 (0)
Log in to leave a comment
아직 작성된 글이 없습니다
원본 영상을 바탕으로 AI의 도움을 받아 작성했습니다. 원본 영상이 기준입니다.
Log in to leave a comment
아직 작성된 글이 없습니다
Если вы закончили работу над десктопным приложением, которое отлично работает локально, считайте, что разработка завершена лишь наполовину. Настоящие трудности начинаются в тот момент, когда пользователь нажимает на ссылку для скачивания, а на экране появляется красное окно с предупреждением вроде "Файл поврежден" или "Компьютер защищен".
Преодоление барьеров безопасности ОС, контроль скорости сборки и создание системы автоматического обновления, которая сама подтягивает новую версию при каждом запуске — это утомительнее, чем кажется. Даже если вы выбрали Tauri v2 из-за того, что он легче Electron, реальные проблемы процесса распространения никуда не деваются. Мы подготовили руководство о том, как индивидуальным разработчикам или небольшим командам распространять свой продукт аккуратно, не тратя лишнее время и деньги.
Десктопное приложение без цифровой подписи (Code Signing) воспринимается операционной системой как вредоносное ПО. Чтобы на ПК пользователя не всплывали предупреждения безопасности, требуются деньги и бюрократия.
Для распространения под macOS обязательна подписка на Apple Developer Program за 99 долларов в год. После получения аккаунта необходимо создать файл src-tauri/Entitlements.plist, в котором определены исключения для безопасности памяти, чтобы Tauri WebView мог корректно работать. Без этой настройки приложение будет вылетать сразу при запуске.
xml <?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> <plist version="1.0"> <dict> <key>com.apple.security.cs.allow-jit</key> <true/> <key>com.apple.security.cs.allow-unsigned-executable-memory</key> <true/> </dict> </plist>
Укажите этот файл в опциях bundle в src-tauri/tauri.conf.json.
json { "bundle": { "macOS": { "signingIdentity": "Developer ID Application: Your Name (TEAMID)", "entitlements": "./Entitlements.plist", "minimumSystemVersion": "11.0", "dmg": { "appPosition": { "x": 180, "y": 170 }, "applicationFolderPosition": { "x": 480, "y": 170 } } } } }
Чтобы пройти фильтр Windows SmartScreen, раньше нужно было получать EV-сертификат (Extended Validation) в виде физического USB-токена, что стоило 400–700 долларов в год. Это не только дорого, но и крайне неудобно для частного лица.
Альтернатива — облачный сервис подписи от Microsoft: Azure Trusted Signing (ATS). За подписку около 9,99 долларов в месяц вы получаете возможность подписывать файлы внутри облака HSM под управлением Microsoft, избавляясь от необходимости хранить физические ключи.
AZURE_TENANT_ID, CLIENT_ID, CLIENT_SECRET) и данные ATS в секреты (Secrets) GitHub Actions.sign-tool, чтобы наложить цифровую подпись на MSI или EXE, скомпилированные Tauri.Приложение, подписанное таким образом, с самого первого скачивания обходит предупреждение Windows SmartScreen, что позволяет избежать оттока пользователей на этапе установки.
Хотя Tauri легкий, процесс его сборки требует работы компилятора Rust и нативных цепочек инструментов для каждой ОС. Даже если всё работает на вашем компьютере, часто возникают ошибки линковщика на машинах коллег, а из-за загрязнения зависимостей в локальной среде сборка может повредиться. Сборки для дистрибуции должны выполняться в изолированном CI/CD пайплайне.
Проблема в том, что стандартные хостинг-раннеры GitHub Actions недостаточно мощны для сборки Rust. Если каждый раз скачивать зависимости заново, одна релизная сборка может занимать более 10 минут.
Вместо использования actions/cache для сжатия и передачи файлов в облако, лучше использовать специализированные плагины кэширования (swatinem/rust-cache), работающие с высокоскоростными NVMe-хранилищами, или выделенные раннеры (Namespace, Depot и т.д.).
Согласно логам сборки музыкального плеера с открытым исходным кодом spotify-player, сравнение производительности обычного раннера GitHub и выделенного раннера с кэшированием локальных томов выглядит так:
| Платформа и конфигурация кэша | Время обычного раннера GitHub | Время с оптимизацией кэша | Ускорение сборки |
|---|---|---|---|
| Ubuntu Linux | 9 мин 31 сек | 34 сек | 94.0% быстрее |
| macOS Darwin | 9 мин 31 сек | 27 сек | 95.2% быстрее |
| Windows MSVC | 9 мин 31 сек | 44 сек | 92.2% быстрее |
| Стоимость процесса | $0.44 за запуск | $0.074 за запуск | 83.1% экономии |
Простая настройка инфраструктуры с постоянным кэшированием томов сокращает время ожидания сборки для команды минимум на 40%.
Настройка автоматизации публикации в .github/workflows/publish.yml выглядит так:
yaml jobs: build-binaries: strategy: matrix: platform: [macos-latest, windows-latest] runs-on: ${{ matrix.platform }} # ... после шагов сборки вызов tauri-action
Если добавить tauri-apps/tauri-action в самом конце рабочего процесса, то при каждом пуше нового тега установщики для обеих ОС будут автоматически подписываться и регистрироваться в черновике GitHub Release.
При упаковке установщика для Windows необходимо выбрать способ установки движка WebView2. Если интернет-соединение гарантировано и критически важен минимальный размер файла, подойдет downloadBootstrapper, который не увеличивает размер пакета. Если же вы ориентируетесь на закрытые сети или офлайн-среду, надежнее использовать offlineInstaller, даже если это добавит около 127 МБ к размеру файла.
Также важен способ обработки данных в приложениях на базе Tauri. Хранение данных в IndexedDB или LocalStorage браузера небезопасно.
Действительно, при переходе Tauri с v1 на v2 в Windows изменилась доменная схема WebView с [https://tauri.localhost](https://tauri.localhost) на [http://tauri.localhost](http://tauri.localhost). Из-за этого путь к кэшу браузера принудительно сменился, и многие пользователи потеряли свои данные.
Чтобы предотвратить катастрофу с потерей данных после обновления, важную информацию следует хранить не в хранилище WebView, а напрямую в SQLite-файле в области нативной файловой системы. API appDataDir в Tauri v2 автоматически находит безопасный путь для «песочницы» в соответствии со стандартами ОС:
C:\Users\<UserName>\AppData\Roaming\<BundleIdentifier>/Users/<UserName>/Library/Application Support/<BundleIdentifier>Пример кода Rust (src-tauri/src/lib.rs) для привязки SQLite-базы данных к безопасному пути и выполнения миграции схем:
`rust
use std::fs;
use tauri::Manager;
use tauri_plugin_sql::{Migration, MigrationKind};
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
let database_migrations = vec![
Migration {
version: 1,
description: "initialize_user_profiles_table",
sql: "CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
email TEXT NOT NULL UNIQUE
);",
kind: MigrationKind::Up,
}
];
tauri::Builder::default()
.setup(|app| {
let local_app_dir = app.path().app_data_dir()
.expect("Critical: Could not resolve target operating system app data path.");
if !local_app_dir.exists() {
fs::create_dir_all(&local_app_dir)
.expect("Critical: Failed to establish persistent storage directory structure.");
}
Ok(())
})
.plugin(
tauri_plugin_sql::Builder::default()
.add_migrations("sqlite:users.db", database_migrations)
.build()
)
.run(tauri::generate_context!())
.expect("error while running tauri application");
}
`
При такой архитектуре даже если внутренний кэш Electron или Chromium будет удален при автоматическом обновлении или переустановке, база данных пользователя останется в сохранности.
Постоянно призывать пользователя заходить на сайт и скачивать новую версию вручную — плохая практика, повышающая процент отказа. Необходимо создать систему, которая тихо доставляет файлы обновления через комбинацию облачного объектного хранилища и CDN.
Эффективная связка для сервера распространения — Cloudflare R2 и AWS CloudFront. У Cloudflare R2 нет платы за исходящий трафик (Egress Fees), что позволяет свести расходы на сетевой трафик при раздаче тяжелых обновлений к нулю.
Файл метаданных (latest.json), который клиент проверяет на наличие новой версии, не должен кэшироваться в CDN или браузере. В ответе заголовка необходимо указать следующую политику:
http Cache-Control: no-cache, no-store, must-revalidate
Напротив, сами бинарные файлы установщиков являются неизменяемыми (Immutable) и содержат уникальный хэш в названии, поэтому их нужно настроить так, чтобы CDN хранил их как можно дольше, снижая нагрузку на основной сервер:
http Cache-Control: public, max-age=31536000, immutable
В Tauri v2 опции, связанные с обновлениями, переместились в блок plugins.updater. Ниже приведен пример конфигурации tauri.conf.json:
json { "bundle": { "createUpdaterArtifacts": true }, "plugins": { "updater": { "active": true, "endpoints": [ "https://cdn.myapp.com/releases/latest.json" ], "dialog": false, "pubkey": "dW5zaWduZWQgYm91bmRmaXg...", "windows": { "installMode": "passive" } } } }
Чтобы в Windows обновление проходило без назойливых подтверждающих окон, установите installMode в значение passive или quiet. Режим passive вместо мастера установки показывает лишь индикатор прогресса, после чего тихо завершает замену файлов.
После настройки подключите @tauri-apps/plugin-updater и @tauri-apps/plugin-process на стороне фронтенда, чтобы проверять наличие обновлений при запуске приложения и предлагать перезагрузку:
`typescript
import { check } from "@tauri-apps/plugin-updater";
import { ask } from "@tauri-apps/plugin-dialog";
import { relaunch } from "@tauri-apps/plugin-process";
export async function runBackgroundUpdater(): Promise {
try {
const updatePayload = await check();
if (updatePayload && updatePayload.available) {
const userResponse = await ask(
`Доступна новая версия [v${updatePayload.version}]. Обновить и перезапустить приложение сейчас?`,
{
title: "Автоматическое обновление ПО",
kind: "info",
okLabel: "Установить и перезапустить",
cancelLabel: "Применить позже"
}
);
if (userResponse) {
await updatePayload.downloadAndInstall();
await relaunch();
}
}
} catch (error) {
console.error("Ошибка при проверке обновлений:", error);
}
}
`
Размещение этой функции на этапе инициализации основного React-компонента или View гарантирует, что пользователь всегда будет иметь актуальную версию ПО без необходимости самостоятельно следить за выходом патчей.
appDataDir, контролируемом нативной частью, и выполняйте миграции схем, чтобы данные не повреждались при обновлениях.