diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 99c9617..4a1be59 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -14,18 +14,40 @@ jobs: fail-fast: false # tauri-action's find-or-create-release check races across parallel # matrix jobs and can create two separate release objects for the - # same tag, each missing the other's assets — force serial builds + # same tag, each missing the other's assets — force serial builds. + # Every platform added below shares that release object, so they all + # have to stay in this one serialized matrix. max-parallel: 1 matrix: include: - - target: x86_64-pc-windows-msvc + - os: windows-latest + target: x86_64-pc-windows-msvc arch: x64 - - target: aarch64-pc-windows-msvc + - os: windows-latest + target: aarch64-pc-windows-msvc arch: arm64 - runs-on: windows-latest + # 22.04 is the oldest runner with webkit2gtk-4.1 (what Tauri 2 + # needs); building on it keeps the glibc floor as low as possible + - os: ubuntu-22.04 + target: x86_64-unknown-linux-gnu + arch: x64 + - os: macos-latest + target: aarch64-apple-darwin + arch: arm64 + - os: macos-latest + target: x86_64-apple-darwin + arch: x64 + runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v4 + - if: runner.os == 'Linux' + run: | + sudo apt-get update + sudo apt-get install -y build-essential curl wget file \ + libwebkit2gtk-4.1-dev librsvg2-dev libssl-dev libxdo-dev \ + libayatana-appindicator3-dev patchelf + - uses: actions/setup-node@v4 with: node-version: 22 @@ -42,6 +64,10 @@ jobs: - run: npm ci + # TAURI_SIGNING_* below is the updater's minisign keypair, not OS code + # signing — there is no Apple certificate, so macOS builds are only + # ad-hoc signed (bundle.macOS.signingIdentity "-") and Gatekeeper still + # asks the user to allow them on first launch. - uses: tauri-apps/tauri-action@v0 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index a0f9d2f..27136a5 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -215,6 +215,40 @@ recursing forever. deleted sheets become `.numi` files under `backups/deleted/` and are pruned by age. Saving is debounced at 400 ms. +## Platforms and packaging + +- One codebase, three desktop targets. `tauri.conf.json` holds everything + shared; the per-platform bits live in `tauri.windows.conf.json`, + `tauri.linux.conf.json` and `tauri.macos.conf.json`, which Tauri merges over + the base config (JSON Merge Patch) according to the target being built. Each + one only sets `bundle.targets` and that platform's bundler options — NSIS on + Windows, deb/rpm/AppImage on Linux, .app/.dmg on macOS. +- `release.yml` builds five artifacts on three runner families: Windows x64 and + arm64, Linux x64 (`ubuntu-22.04` — the oldest runner carrying webkit2gtk-4.1, + which keeps the glibc floor low), macOS arm64 and x64. Two separate macOS + targets rather than `universal-apple-darwin`: it matches how Windows already + ships per-architecture, halves each download, and gives the updater the + `darwin-aarch64`/`darwin-x86_64` keys it looks up by default. +- The whole matrix stays `max-parallel: 1`. Every job hands its assets to the + *same* GitHub Release object, and tauri-action's find-or-create check races if + two jobs reach it at once — adding platforms did not change that constraint, + it made it apply to more jobs. +- No OS code signing anywhere. Windows ships unsigned; macOS is *ad-hoc* signed + (`bundle.macOS.signingIdentity: "-"`), which needs no Apple certificate but is + required for Apple Silicon to run a downloaded binary at all — without it the + arm64 build is rejected as damaged rather than merely warned about. Gatekeeper + still asks the user to allow the app on first launch either way. +- `bundle.fileAssociations` covers `.numi`/`.sum` on Windows (NSIS registry + entries) and macOS (`CFBundleDocumentTypes`). On Linux it writes nothing + useful: registering an extension there also needs a shared-mime-info XML that + Tauri does not generate, so `.numi` files are opened by dragging them into the + window rather than from the file manager. +- `icons/icon.icns` is emitted by `scripts/gen-icon.ts` alongside the PNGs and + the `.ico`, hand-rolled the same dependency-free way: `icns` magic + total + length, then one `[OSType][length][PNG]` chunk per size, using the modern + PNG-payload tags `iconutil` produces (`icp4`/`icp5`, `ic07`–`ic14`). Rerun it + with `npm run icons` after touching the artwork. + ## Auto-update - `updater.ts` calls `tauri-plugin-updater`'s `check()` on boot; if a signed @@ -242,7 +276,9 @@ recursing forever. flipping it while the app is running takes effect immediately); the manual button ignores the toggle and always works. - The update endpoint is `latest.json` published alongside each GitHub - Release. `release.yml` signs both architectures with a minisign keypair: + Release. `release.yml` signs every platform's updater artifact with a + minisign keypair (this is Tauri's own update signing, unrelated to OS code + signing): the public half is embedded in `tauri.conf.json`; the private half + its password live only as `TAURI_SIGNING_PRIVATE_KEY(_PASSWORD)` repo secrets (write-only — there is no durable copy in the repo). Losing that key means diff --git a/ARCHITECTURE.ru.md b/ARCHITECTURE.ru.md index 30486cf..dfcc9ed 100644 --- a/ARCHITECTURE.ru.md +++ b/ARCHITECTURE.ru.md @@ -217,6 +217,40 @@ src-tauri/ удалённые листы становятся `.numi`-файлами в `backups/deleted/` и удаляются по возрасту. Сохранение откладывается на 400 мс. +## Платформы и упаковка + +- Одна кодовая база, три десктопных платформы. Всё общее лежит в + `tauri.conf.json`, платформенные куски — в `tauri.windows.conf.json`, + `tauri.linux.conf.json` и `tauri.macos.conf.json`, которые Tauri накладывает + поверх базового конфига (JSON Merge Patch) в зависимости от собираемой цели. + Каждый задаёт только `bundle.targets` и опции своего упаковщика: NSIS на + Windows, deb/rpm/AppImage на Linux, .app/.dmg на macOS. +- `release.yml` собирает пять артефактов на трёх видах раннеров: Windows x64 и + arm64, Linux x64 (`ubuntu-22.04` — самый старый раннер с webkit2gtk-4.1, что + держит планку glibc низко), macOS arm64 и x64. Две отдельные macOS-цели вместо + `universal-apple-darwin`: так же, как Windows уже собирается по архитектурам, + вдвое меньше вес загрузки, и апдейтер получает ключи `darwin-aarch64` / + `darwin-x86_64`, которые ищет по умолчанию. +- Вся матрица остаётся `max-parallel: 1`. Все джобы отдают артефакты в *один и + тот же* объект GitHub Release, а проверка find-or-create в tauri-action гонит, + если две джобы доходят до неё одновременно — добавление платформ это + ограничение не сняло, а распространило на большее число джоб. +- Подписи ОС нет нигде. Windows выходит неподписанным; macOS подписан *ad-hoc* + (`bundle.macOS.signingIdentity: "-"`) — сертификат Apple для этого не нужен, но + без него Apple Silicon вообще откажется запускать скачанный бинарник (сочтёт + повреждённым, а не просто предупредит). Gatekeeper в любом случае попросит + пользователя разрешить приложение при первом запуске. +- `bundle.fileAssociations` покрывает `.numi`/`.sum` на Windows (записи в реестр + из NSIS) и macOS (`CFBundleDocumentTypes`). На Linux он не даёт ничего + полезного: там для регистрации расширения нужен ещё XML для shared-mime-info, + который Tauri не генерирует, так что `.numi` открываются перетаскиванием в + окно, а не из файлового менеджера. +- `icons/icon.icns` создаёт `scripts/gen-icon.ts` рядом с PNG и `.ico`, вручную + и без зависимостей: магия `icns` + общая длина, дальше по чанку + `[OSType][длина][PNG]` на размер, с современными PNG-тегами, которые выдаёт + `iconutil` (`icp4`/`icp5`, `ic07`–`ic14`). После правки картинки перегенерить + через `npm run icons`. + ## Автообновление - `updater.ts` вызывает `check()` из `tauri-plugin-updater` при запуске; если @@ -245,7 +279,9 @@ src-tauri/ тике интервала, так что переключение прямо во время работы срабатывает сразу); ручная кнопка тумблер игнорирует и работает всегда. - Эндпоинт обновлений — `latest.json`, публикуемый вместе с каждым GitHub - Release. `release.yml` подписывает обе архитектуры парой ключей minisign: + Release. `release.yml` подписывает артефакт обновления каждой платформы парой + ключей minisign (это собственная подпись обновлений Tauri, к подписи кода в ОС + отношения не имеет): публичная половина зашита в `tauri.conf.json`; приватная половина + пароль живут только как секреты репозитория `TAURI_SIGNING_PRIVATE_KEY(_PASSWORD)` (write-only — в репозитории нет постоянной копии). Потеря этого ключа diff --git a/README.md b/README.md index bf56fc2..4e83165 100644 --- a/README.md +++ b/README.md @@ -29,16 +29,29 @@ Works in **English and Russian** at the same time: `5 метров в см`, `20 ## Install -Grab the latest `Summarum_*_x64-setup.exe` (or `arm64-setup.exe` on Windows -on ARM) from [Releases](../../releases), run it, done. The whole app is -about 2 MB. It checks for updates on its own after that. +Grab the build for your system from [Releases](../../releases). The whole app +is about 2 MB and checks for updates on its own after that. + +- **Windows** — `Summarum_*_x64-setup.exe` (or `arm64-setup.exe` on Windows on + ARM). Run it, done. +- **Linux** — `.AppImage` (`chmod +x`, then run) or the `.deb`/`.rpm` if you'd + rather install it properly. Self-updating only works on the AppImage; deb and + rpm are updated by downloading the next one. +- **macOS** — `.dmg`, Apple Silicon or Intel. These builds are not signed with + an Apple Developer certificate, so the first launch has to be a right-click → + **Open** rather than a double-click (or run + `xattr -cr /Applications/Summarum.app` once in a terminal). Every launch after + that is normal. Requirements: -- **Windows 10 (1803+) or Windows 11**, 64-bit. -- **[WebView2 Runtime](https://developer.microsoft.com/en-us/microsoft-edge/webview2/)** — +- **Windows 10 (1803+) or Windows 11**, 64-bit, plus the + **[WebView2 Runtime](https://developer.microsoft.com/en-us/microsoft-edge/webview2/)** — already part of Windows 11 and most updated Windows 10 systems; if it's missing, the installer downloads it automatically. +- **Linux** with `webkit2gtk-4.1` (Ubuntu 22.04+, Debian 12+, Fedora 36+) and + `libayatana-appindicator3` for the tray icon. +- **macOS 10.13** or newer. - Internet connection is only needed for live rates and market data — everything else works offline (rates fall back to a cached/bundled snapshot). @@ -165,19 +178,28 @@ To build from source you need: - [Node.js](https://nodejs.org/) 22.6 or newer (`npm run icons` relies on Node's built-in TypeScript support) -- [Rust](https://rustup.rs/) (stable, MSVC toolchain) -- [Visual Studio Build Tools](https://visualstudio.microsoft.com/visual-cpp-build-tools/) +- [Rust](https://rustup.rs/), stable +- On Windows: the MSVC toolchain plus + [Visual Studio Build Tools](https://visualstudio.microsoft.com/visual-cpp-build-tools/) with the "Desktop development with C++" workload (the MSVC linker) +- On Linux: `libwebkit2gtk-4.1-dev`, `libayatana-appindicator3-dev`, + `librsvg2-dev`, `libxdo-dev`, `libssl-dev`, `patchelf` and a C toolchain +- On macOS: Xcode Command Line Tools - See the [Tauri prerequisites](https://tauri.app/start/prerequisites/) page if anything refuses to compile +`npm run tauri build` produces whatever the host platform bundles — NSIS on +Windows, deb/rpm/AppImage on Linux, .app/.dmg on macOS. Cross-compiling +between the three is not supported; the release workflow builds each on its +own runner. + Stack: [Tauri 2](https://tauri.app) + TypeScript + CodeMirror 6. ## Credits Inspired by the excellent [Numi](https://numi.app) by Dmitry Nikolaev — if you're on macOS, buy it. Summarum is an independent project, built from -scratch for Windows. +scratch for Windows, Linux and macOS. ## License diff --git a/README.ru.md b/README.ru.md index 9ce57f8..bd3fcba 100644 --- a/README.ru.md +++ b/README.ru.md @@ -27,14 +27,28 @@ AAPL в USD → $213.55 ## Установка -Скачай последний `Summarum_*_x64-setup.exe` (или `arm64-setup.exe` для Windows on ARM) из [Releases](../../releases), запусти — готово. Весь установщик около 2 МБ. Дальше обновления проверяются сами. +Скачай сборку под свою систему из [Releases](../../releases). Приложение весит +около 2 МБ, дальше обновления проверяются сами. + +- **Windows** — `Summarum_*_x64-setup.exe` (или `arm64-setup.exe` для Windows on + ARM). Запусти — готово. +- **Linux** — `.AppImage` (`chmod +x` и запускай) либо `.deb`/`.rpm`, если хочется + поставить нормальным пакетом. Самообновление работает только у AppImage, deb и + rpm обновляются скачиванием следующей версии. +- **macOS** — `.dmg` для Apple Silicon или Intel. Сборки не подписаны сертификатом + Apple Developer, поэтому первый запуск — правый клик → **Открыть**, а не двойной + клик (либо один раз выполнить `xattr -cr /Applications/Summarum.app` в терминале). + Дальше запускается как обычно. Требования: -- **Windows 10 (1803+) или Windows 11**, 64-бит. -- **[WebView2 Runtime](https://developer.microsoft.com/en-us/microsoft-edge/webview2/)** — +- **Windows 10 (1803+) или Windows 11**, 64-бит, и + **[WebView2 Runtime](https://developer.microsoft.com/en-us/microsoft-edge/webview2/)** — уже встроен в Windows 11 и большинство обновлённых Windows 10; если отсутствует, установщик скачает его автоматически. +- **Linux** с `webkit2gtk-4.1` (Ubuntu 22.04+, Debian 12+, Fedora 36+) и + `libayatana-appindicator3` для иконки в трее. +- **macOS 10.13** или новее. - Интернет нужен только для живых курсов и рыночных данных — всё остальное работает офлайн (курсы берутся из кэша или встроенного снимка). @@ -159,17 +173,25 @@ npm run tauri build # собрать установщик Для сборки нужны: - [Node.js](https://nodejs.org/) 22.6 или новее -- [Rust](https://rustup.rs/) (stable, MSVC toolchain) -- [Visual Studio Build Tools](https://visualstudio.microsoft.com/visual-cpp-build-tools/) +- [Rust](https://rustup.rs/), stable +- На Windows: toolchain MSVC и + [Visual Studio Build Tools](https://visualstudio.microsoft.com/visual-cpp-build-tools/) с компонентом «Desktop development with C++» (нужен линковщик MSVC) +- На Linux: `libwebkit2gtk-4.1-dev`, `libayatana-appindicator3-dev`, + `librsvg2-dev`, `libxdo-dev`, `libssl-dev`, `patchelf` и компилятор C +- На macOS: Xcode Command Line Tools - Если что-то не собирается — см. [Tauri prerequisites](https://tauri.app/start/prerequisites/) +`npm run tauri build` собирает то, что умеет хост-платформа: NSIS на Windows, +deb/rpm/AppImage на Linux, .app/.dmg на macOS. Кросс-компиляция между ними не +поддерживается — релизный workflow собирает каждую платформу на своём раннере. + Стек: [Tauri 2](https://tauri.app) + TypeScript + CodeMirror 6. ## Благодарность Вдохновлён [Numi](https://numi.app) от Дмитрия Николаева — -если ты на macOS, купи его. Summarum — независимый проект, написанный с нуля для Windows. +если ты на macOS, купи его. Summarum — независимый проект, написанный с нуля для Windows, Linux и macOS. ## Лицензия diff --git a/package.json b/package.json index 311be76..4df55b8 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "summarum", "private": true, "version": "1.2.6", - "description": "Summarum — natural language calculator notebook for Windows", + "description": "Summarum — natural language calculator notebook", "type": "module", "scripts": { "dev": "vite", diff --git a/scripts/gen-icon.ts b/scripts/gen-icon.ts index 04e878a..2784cbb 100644 --- a/scripts/gen-icon.ts +++ b/scripts/gen-icon.ts @@ -113,7 +113,7 @@ function draw(size: number): Buffer { fs.mkdirSync(outDir, { recursive: true }); const pngs = new Map(); -for (const size of [16, 32, 48, 64, 128, 256, 512]) { +for (const size of [16, 32, 48, 64, 128, 256, 512, 1024]) { pngs.set(size, encodePng(size, draw(size))); } @@ -154,3 +154,38 @@ function buildIco(sizes: number[]): Buffer { fs.writeFileSync(path.join(outDir, "icon.ico"), buildIco([16, 32, 48, 64, 128, 256])); console.log("icon.ico (6 sizes)"); + +// .icns: "icns" magic + total length, then [OSType][length incl. header][PNG]. +// Both lengths are big-endian and count their own 8-byte header. The tag set +// mirrors what `iconutil` emits for a modern .iconset — every ic*/icp* entry +// below carries a plain PNG payload (10.7+), so no JPEG 2000 encoder is needed. +const ICNS_ENTRIES: Array<[string, number]> = [ + ["icp4", 16], + ["icp5", 32], + ["ic11", 32], // 16@2x + ["ic12", 64], // 32@2x + ["ic07", 128], + ["ic13", 256], // 128@2x + ["ic08", 256], + ["ic14", 512], // 256@2x + ["ic09", 512], + ["ic10", 1024], // 512@2x +]; + +function buildIcns(): Buffer { + const parts = ICNS_ENTRIES.map(([tag, size]) => { + const data = pngs.get(size)!; + const head = Buffer.alloc(8); + head.write(tag, 0, 4, "ascii"); + head.writeUInt32BE(8 + data.length, 4); + return Buffer.concat([head, data]); + }); + const body = Buffer.concat(parts); + const header = Buffer.alloc(8); + header.write("icns", 0, 4, "ascii"); + header.writeUInt32BE(8 + body.length, 4); + return Buffer.concat([header, body]); +} + +fs.writeFileSync(path.join(outDir, "icon.icns"), buildIcns()); +console.log(`icon.icns (${ICNS_ENTRIES.length} entries)`); diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index c489c8e..2647f10 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "summarum" version = "1.2.6" -description = "Summarum — natural language calculator notebook for Windows" +description = "Summarum — natural language calculator notebook" edition = "2021" [build-dependencies] diff --git a/src-tauri/icons/icon.icns b/src-tauri/icons/icon.icns new file mode 100644 index 0000000..dd9c4d8 Binary files /dev/null and b/src-tauri/icons/icon.icns differ diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index 5f2fc06..85f870e 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -1,6 +1,9 @@ // Summarum — Tauri shell: tray, hide-to-tray window, app-data storage, // currency rates fetching with an on-disk cache, extension script loading. -#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] +#![cfg_attr( + all(not(debug_assertions), target_os = "windows"), + windows_subsystem = "windows" +)] use serde::{Deserialize, Serialize}; use std::collections::HashSet; diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 9056d06..0539a82 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -28,14 +28,14 @@ }, "bundle": { "active": true, - "targets": ["nsis"], "createUpdaterArtifacts": true, "icon": [ "icons/32x32.png", "icons/128x128.png", "icons/128x128@2x.png", "icons/icon.png", - "icons/icon.ico" + "icons/icon.ico", + "icons/icon.icns" ], "fileAssociations": [ { @@ -44,13 +44,7 @@ "description": "Summarum calculation sheet", "role": "Editor" } - ], - "windows": { - "nsis": { - "installMode": "currentUser", - "languages": ["English", "Russian"] - } - } + ] }, "plugins": { "updater": { diff --git a/src-tauri/tauri.linux.conf.json b/src-tauri/tauri.linux.conf.json new file mode 100644 index 0000000..88aa25f --- /dev/null +++ b/src-tauri/tauri.linux.conf.json @@ -0,0 +1,5 @@ +{ + "bundle": { + "targets": ["deb", "rpm", "appimage"] + } +} diff --git a/src-tauri/tauri.macos.conf.json b/src-tauri/tauri.macos.conf.json new file mode 100644 index 0000000..c32199d --- /dev/null +++ b/src-tauri/tauri.macos.conf.json @@ -0,0 +1,8 @@ +{ + "bundle": { + "targets": ["app", "dmg"], + "macOS": { + "signingIdentity": "-" + } + } +} diff --git a/src-tauri/tauri.windows.conf.json b/src-tauri/tauri.windows.conf.json new file mode 100644 index 0000000..f445f53 --- /dev/null +++ b/src-tauri/tauri.windows.conf.json @@ -0,0 +1,11 @@ +{ + "bundle": { + "targets": ["nsis"], + "windows": { + "nsis": { + "installMode": "currentUser", + "languages": ["English", "Russian"] + } + } + } +}