Add a unified cross-platform build and packaging system - #143
Conversation
Единая система сборки и упаковки entropy на базе go-task, заменяющая разрозненные скрипты. Кросс-компиляция бинарей через cargo-zigbuild, упаковка — лучшими инструментами под каждый формат. Что добавлено: - Taskfile.yml — оркестратор: build / package:* / release / docker:*, с определением ОС и версией из Cargo.toml. - Dockerfile.build — самодостаточный тулчейн (Rust, zig, mingw, nfpm, wixl, nsis, linuxdeploy, quill, lipo, libdmg-hfsplus) без завязки на внешние registry-образы. На Linux-хосте сборка возможна и без него. - Linux: nfpm (deb + rpm + archlinux одним конфигом) + AppImage. Вынесены статические ассеты: entropy.desktop, AppStream metainfo, udev-правило 59-vial.rules (в /usr/lib/udev/rules.d, TAG+="uaccess"). - Windows: MSI через wixl + NSIS через makensis + portable exe, всё собирается из-под Linux. - macOS: гибрид — package:macos на маке (build_macos_app.sh), плюс experimental кросс-сборка из Linux (build_macos_cross.sh): cargo-zigbuild universal2 + внешний SDK + подпись quill + dmg. - prepare_macos_sdk.sh — тянет закреплённый SDK из joseluisq/macosx-sdks в gitignore-кэш. - assets/entropy.svg — масштабируемый источник иконки; задача icons генерит hicolor PNG, .ico и .icns. - .tool-versions фиксирует Rust 1.97.0 (в проекте версия не была закреплена). Почему точечные инструменты, а не cargo-packager: у него нет rpm, а Arch/pacman отдаёт лишь PKGBUILD; nfpm покрывает deb+rpm+arch одним конфигом и берёт готовый бинарь от zigbuild как есть. Windows-инсталлятор переведён с Inno Setup на wixl/NSIS: iscc есть только под Windows, а makensis/wixl собирают из Linux. Удалены installer/entropy.iss и scripts/build_windows_installer.ps1.
В образ dmg кроме Entropy.app кладём символьную ссылку на /Applications, чтобы пользователь мог перетащить приложение мышкой — стандартная раскладка macOS-инсталлятора. Для этого .app и симлинк собираются во временном staging-каталоге, который и передаётся genisoimage.
Доводка ещё не влитой системы сборки (5295ac9): исправление корректности упаковки, унификация именования и подключение к CI. Зависимости пакетов сверены с фактической линковкой бинаря: он тянет только libc, а GUI-стек (libGL, xkbcommon, X11/xcb) грузит через dlopen, hidapi использует чистый Rust-hidraw (без libudev), а файловые диалоги идут через xdg-desktop-portal, не через GTK. Поэтому из depends убраны gtk3, libudev1 и systemd-libs, а portal вынесен в recommends. Windows: NSIS удалён — он дублировал MSI (оба ставят per-machine). Остаются portable exe и MSI (wixl). Taskfile переименован по схеме «стратегия × цель»: build/package (нативно под текущую ОС), цели linux:/windows:/macos: и кросс docker:* (docker:dist собирает Linux+Windows в контейнере). Добавлен фолбэк magick→convert (в bookworm только ImageMagick 6), артефакты из контейнера chown-ятся обратно на хост-uid. Dockerfile переименован из Dockerfile.build (единственный в проекте): выпилены неиспользуемые пакеты (nsis, osslsigncode, jq, uuid-runtime, linuxdeploy), appimagetool забейкан и запинен (1.9.1), cargo-zigbuild запинен на ~0.19 под zig 0.13. AppImage-скрипт переиспользует канонические .desktop и .svg вместо inline-заглушек. CI (release.yml) теперь гоняет Taskfile как единый источник правды: Linux и Windows собираются в toolchain-контейнере, macOS — нативно на mac-раннерах.
Задача icons падала с подсказкой только для Debian (librsvg2-bin), поэтому на openSUSE было неочевидно, что именно ставить: пакет там называется rsvg-convert. В сообщения об отсутствии rsvg-convert, ImageMagick и png2icns добавлены имена пакетов для openSUSE, Fedora и Arch. В README секция Development дополнена разделом Packaging: точки входа task package и task docker:dist, перечень отдельных таргетов, таблица инструментов с именами пакетов по четырём дистрибутивам и отдельный список того, чего в репозиториях дистрибутивов нет (nfpm, go-task, appimagetool, zig с cargo-zigbuild). Оттуда же убраны два устаревших примера — прямой вызов scripts/build_macos_app.sh и cargo build --target x86_64-pc-windows-gnu: оба пути теперь закрыты таргетами Taskfile, а Windows-сборка идёт через cargo-zigbuild, а не через голый cargo. Кэш go-task (.task/) добавлен в .gitignore — это локальные чексуммы sources, они машинно-зависимы и в репозитории не нужны.
Файл в репозитории собран не задачей icons: в нём четыре слоя (16, 32, 48, 256), тогда как task icons кладёт в .ico шесть (добавляются 64 и 128). Пересобрал текущей задачей, чтобы закоммиченный файл совпадал с тем, что получают локальная сборка и контейнер. Файл трекается намеренно — build.rs берёт его при сборке под Windows, и без него сборка на машине без rsvg-convert невозможна. Побочный эффект: байтовое представление зависит от версии ImageMagick (локально 7, в контейнере 6), поэтому diff по нему будет появляться после прогона icons на другой машине.
Чинит всё, что мешало собрать релизные артефакты локально и в CI, и переносит подготовку окружения внутрь проекта. Контейнер и docker-путь: - Dockerfile собирал CMake-таргет dmg вместо dmg-bin, а dmg — это статическая библиотека, поэтому образ не собирался вообще; - в образе не было envsubst (gettext-base), из-за чего контейнерная сборка linux-пакетов не работала никогда, включая релизный workflow; - docker:run возвращал 0 при любой ошибке внутри контейнера, так что провал выглядел успехом; теперь код возврата пробрасывается, а chown выполняется в любом случае, не только при успехе; - на SELinux-хостах контейнер не мог прочитать смонтированный каталог (user_home_t против container_t) — добавляется --security-opt label=disable, но только когда getenforce сообщает, что SELinux активен. Подготовка окружения: - task prepare и prepare:cross ставят зависимости под текущую ОС; scripts/prepare_env.sh определяет семейство дистрибутива и умеет apt/zypper/dnf/pacman/apk, для Windows добавлен prepare_env.ps1 (winget); - rust и zig берутся из .tool-versions через asdf: версии одинаковы у всех и ничего не ставится в систему глобально; - nfpm, quill, dmg и appimagetool складываются в .cache/tools внутри репозитория, PATH дополняется только на время команды, а то, что уже есть у разработчика, имеет приоритет и повторно не скачивается; - zlib в rpm-дистрибутивах запрашивается как pkgconfig(zlib): запрос по имени zlib-devel конфликтует с предустановленным zlib-ng-compat-devel. macOS: - нативная сборка собирает обе арки и сливает их через lipo, кросс-сборка давала universal2 и раньше, поэтому релиз теперь везёт один универсальный .dmg вместо отдельных файлов под arm64 и x86_64, а CI сведён к одному job; - .app.zip больше не собирается, распространяется только dmg; - бандл без entropy.icns больше не собирается молча: именно так уезжало приложение с пустой иконкой в Finder; - исправлен вызов утилиты dmg (принимает <in.iso> <out.dmg> без подкоманды) и добавлен fallback на mkisofs для дистрибутивов без genisoimage; - пометка EXPERIMENTAL снята, путь рабочий. В prepare_macos_sdk.sh осталась оговорка про лицензию Apple SDK — она о юридической стороне, а не о зрелости кода, поэтому релизы по-прежнему собираются на маках. Иконки: - задача icons снова собирает .icns: его не было в generates, поэтому отсутствие файла оставалось незамеченным; на macOS используется iconutil; - app_icon.rs рисовал второй логотип, который подменял иконку бандла в Dock после запуска; теперь он повторяет геометрию и палитру assets/entropy.svg, а сглаживание считается от размера, иначе трей 32x32 шёл лесенкой. Документация: - BUILD.md описывает обе стратегии сборки, все задачи Taskfile, пакеты по дистрибутивам и разбор типовых поломок; - в README раздел разработки сокращён до quick start со ссылкой на BUILD.md.
Workflow и так запускается только по push тега, а в каждом job дублировалась та же проверка через if: startsWith(github.ref_name, 'v0.'). Условие всегда было истинным — на других тегах запуска просто не бывает, — поэтому убрано из всех трёх job'ов. Заодно фильтр отвязан от нулевой мажорной версии. Паттерн 'v0.*' тихо не запустил бы релиз на первом же теге v1.0.0: workflow не стартовал бы вовсе, без ошибки и без следа в Actions, и заметить это можно было бы только по отсутствию релиза. Теперь ловятся semver-теги любой мажорной версии, стабильные и предрелизные — отдельными паттернами, чтобы посторонние теги вроде vial-test или nightly релиз не запускали.
Ручной запуск релиза (workflow_dispatch с именем существующего тега) нужен, чтобы перевыпустить артефакты, не двигая тег: например, когда сборка упала на загрузке файлов. Сам по себе dispatch ломал бы весь workflow, потому что тот опирался на github.ref_name, а при ручном запуске это имя ветки, а не тега. Тогда: checkout собрал бы код ветки вместо релизного, awk не нашёл бы секцию в CHANGELOG, а softprops/action-gh-release без явного tag_name опубликовал бы релиз поверх ветки. Поэтому версия вычисляется один раз в env.RELEASE_TAG (inputs.tag при ручном запуске, иначе имя тега), и на неё же завязаны checkout во всех job'ах, release notes, имя релиза, prerelease и make_latest. Добавлен job validate: он отсекает случайный ввод вроде имени ветки до того, как начнут собираться артефакты, и печатает понятную ошибку вместо падения awk где-то в середине пайплайна. Concurrency: - релизы группируются по тегу с cancel-in-progress: false — прерывать публикацию нельзя, часть артефактов уже была бы загружена; - сборки в PR наоборот отменяются новым пушем, очередь устаревших прогонов не нужна. Заодно printf'ы блока Downloads собраны в один блок с общим редиректом (SC2129 из actionlint).
Таблица платформ и список загрузок остались от прежней схемы сборки: у Linux значился только AppImage, у Windows — только портативный exe, а macOS шёл двумя строками под arm64 и x86_64. Приведено к тому, что реально публикуется: пакеты deb/rpm/archlinux рядом с AppImage, MSI рядом с портативным exe, и один универсальный dmg вместо двух сборок под разные архитектуры Mac. Имена файлов в списке сверены с тем, что выдают nfpm и скрипты упаковки.
Ветка в текущем виде не поехала бы ни в PR-гейте, ни в релизе: часть проблем ломала сборку сразу, часть — только в CI, где их некому было бы заметить до первого тега. Блокеры сборки: - macOS падал и в PR, и в релизе: build_macos_app.sh требует assets/entropy.icns, но файл лежал в .gitignore, а ни macos:app, ни workflow его не генерировали. На раннере он и не сгенерировался бы — там нечем растеризовать SVG. Теперь .icns закоммичен рядом с .ico, и clean его не трогает. - Dockerfile не собирался с zig 0.16: с 0.15 у ziglang поменялась схема имён архивов (zig-<arch>-linux-, а не zig-linux-<arch>-), был 404. - appimagetool 1.9.1 качает runtime с GitHub на каждой сборке и виснет без таймаута, если соединение оборвалось (проверено — 17 минут в CLOSE-WAIT). В CI это тихий простой job'а до его лимита, хотя Dockerfile обещал сборку без сети. Runtime теперь пиннут и передаётся через --runtime-file, у curl появились таймауты. - task windows:* и macos:cross не работали при asdf: ZIG резолвился в shim, а cargo запускает build script из каталога крейта в реестре, где .tool-versions не находится — shim печатает подсказку вместо версии, и cargo-zigbuild падает на разборе semver. Резолвим реальный бинарник. Публикация релиза: - В GitHub Release уезжал мусор: dist/**/* тянул сгенерированный конфиг nfpm, а dist/macos/* — распакованный Entropy.app целиком. Пути теперь перечислены явно, плюс if-no-files-found: error. - Версия артефактов бралась из Cargo.toml, поэтому предрелиз v0.3.2-rc.1 выложил бы файлы с именами будущего стабильного v0.3.2, а deb/rpm — с неотличимой версией пакета. VERSION прокидывается из тега сквозь Taskfile, контейнер и скрипты; nfpm сам разворачивает суффикс в 0.3.2~rc.1. MSI ProductVersion и CFBundleVersion принимают только числовые компоненты, поэтому для них суффикс отбрасывается отдельно. - Всё, что можно проверить до сборки, ушло в validate: формат тега, наличие секции в CHANGELOG и сверка с Cargo.toml. Раньше отсутствие секции роняло релиз уже после десятков минут сборки. Заодно это страхует расширенный фильтр тегов: легаси-теги v1.13.x/v1.14.x, что живут в репозитории, отсекаются обеими проверками. - Образ для docker:dist пересобирался на каждом релизе с нуля, включая компиляцию cargo-zigbuild и libdmg-hfsplus. Собираем его отдельным шагом с кэшем слоёв, а DOCKER_IMAGE_READY выключает повторную сборку. Уклад upstream: - build.yml вернулся к матрице upstream: снова два macOS-раннера по одной арке. Универсальный бандл нужен релизу, а в PR-гейте он лишь удваивал время; кроме того, job "Build (macos-15-intel)" мог быть прописан в required checks, и его пропажа подвесила бы все PR. Дифф к upstream сузился до concurrency и расширения артефакта. - build_macos_app.sh снова понимает TARGET (одна арка) — так workflow не расходится с upstream и с открытым PR ergohaven#74. Пакеты: - В packaging/linux/59-vial.rules не хватало второго правила — для устройств по Bluetooth, где серийный номер Vial в hidraw не виден. Пользователи deb/rpm/arch не получили бы доступ к BLE-клавиатурам. - В зависимости добавлен libwayland-client: eframe собран с фичей wayland и грузит его через dlopen. - prepare теперь проверяет не имена пакетов, а сами инструменты, и сообщает, чего не хватает: набор пакетов покрывает дистрибутивы неравномерно (в alpine нет msitools и icnsutils), и раньше нехватка всплывала на середине сборки. - zig и cargo-zigbuild выровнены между .tool-versions, Dockerfile и prepare_env.sh: линия cargo-zigbuild рассчитана на конкретный zig, поэтому они должны двигаться вместе. Проверено сборкой с VERSION=0.3.2-rc.1: нативно linux:pkg, linux:appimage и windows:all, затем полный docker:dist в контейнере с нуля. Метаданные deb/rpm, ProductVersion в MSI и содержимое AppImage — как ожидалось, права после контейнера восстановлены, закоммиченные иконки не перезаписаны.
Upstream за это время принял ergohaven#111 «Verify AppImage packaging tool downloads»: пиннутый релиз appimagetool с контрольной суммой, атомарное скачивание во временный файл и три теста, которые гоняются в PR-гейте. Конфликт в scripts/build_linux_appimage.sh разрешён в пользу upstream. Ветка переводила сборку на appimagetool 1.9.1 из нового репозитория AppImage/appimagetool, а он, в отличие от AppImageKit 12, не несёт runtime внутри и тянет его с GitHub на каждой сборке — на оборванном соединении это зависало без таймаута. Ветка лечила симптом, добавляя пиннутый runtime и --runtime-file; после возврата на пиннутый AppImageKit 12 проблема исчезает сама, а инструмент дополнительно проверяется по SHA-256. Свой обвес убран целиком. Из ветки в скрипте осталось то, что не спорит с ergohaven#111: .desktop и иконки берутся из packaging/linux и assets вместо инлайновых heredoc'ов, libudev больше не бандлится (hidapi собран с чистым hidraw), а appimagetool кэшируется в .cache/tools рядом с nfpm, quill и dmg. К curl добавлены таймауты: без них любая загрузка в CI может висеть до лимита job'а. Dockerfile больше не качает свою версию инструмента, а берёт релиз и сумму из scripts/appimagetool_pin.sh — иначе в контейнере оказался бы не тот appimagetool, который проверяют тесты. Слой перенесён в конец файла, чтобы обновление пина не инвалидировало сборку cargo-zigbuild. Проверено: три теста upstream (test_verify_sha256, test_build_linux_appimage, test_update_appimagetool_pin) проходят на изменённом скрипте, AppImage собирается, cargo test — 525 тестов зелёные.
| - name: Build Linux + Windows artifacts | ||
| env: | ||
| DOCKER_IMAGE_READY: '1' | ||
| run: task docker:dist VERSION=${{ needs.validate.outputs.version }} |
There was a problem hiding this comment.
Could we validate the full tag with a strict semver regex and pass the validated version through a quoted environment variable at every task boundary?
There was a problem hiding this comment.
Done in 47610bf.
The tag is now matched as a whole with a bash regex (^v[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.]+)?$) and the version is handed to task through an environment variable instead of being interpolated into the command text.
Worth recording why it is bash and not grep -Eqx: grep matches line by line, so a multi-line workflow_dispatch input like v1.2.3\n<command> passes on its first line. I checked both against the same table of tags — v1.2.3;id, v1.2.3-rc.1$(id), v1.2, v1.2.3- are rejected by both, but only the bash form rejects the multi-line case. The on.push.tags filter never covered this, since workflow_dispatch takes a free-form string.
|
|
||
| - name: Install Rust | ||
| uses: dtolnay/rust-toolchain@stable | ||
| uses: dtolnay/rust-toolchain@master |
There was a problem hiding this comment.
Could we pin third-party actions to reviewed commit SHAs and grant contents: write only to the publish job?
There was a problem hiding this comment.
Done in 47610bf, both halves.
contents: write now sits on the release job only; the workflow default is contents: read. The build jobs run a third-party toolchain and have no reason to hold a token that can write to the repository.
Every action is pinned to a commit SHA with the version in a trailing comment, in build.yml too — dtolnay/rust-toolchain@master was the worst offender. I pinned the SHA behind the major tag each workflow already used, so no action changes behaviour here. Since the repository had no SHA pinning at all before this, I also added .github/dependabot.yml (github-actions + docker, monthly, grouped) — without it the pins quietly freeze together with their CVEs, which is a worse place to be than floating tags.
| name: release-linux-windows | ||
| # Только сами пакеты: в dist/ лежит ещё и сгенерированный конфиг nfpm, | ||
| # а всё, что попадёт сюда, окажется файлом в GitHub Release. | ||
| path: | |
There was a problem hiding this comment.
Could we assert every required package glob, or upload each format as an individually required artifact before creating the release?
There was a problem hiding this comment.
Done in 47610bf — added a Check every package format was produced step that fails on any missing format, plus the equivalent .dmg check on the macOS job. You are right that if-no-files-found: error only fires when nothing matches, so a missing .rpm would have shipped silently.
I kept a single artifact rather than one per format: six upload/download pairs buy nothing once the check is explicit. The patterns match by format, not by full name — nfpm rewrites the prerelease version (0.3.2-rc.1 -> 0.3.2~rc.1 for deb/rpm, 0.3.2rc.1 for arch), so anchoring on the tag would produce false failures. Verified against a real dist/ from a full container run, and by removing the .msi to confirm it exits non-zero.
| # macOS is built on a real Mac (hybrid model); the darwin Rust targets and zig are | ||
| # installed here only for the optional/experimental cross-build path. | ||
| ARG RUST_VERSION=1.97 | ||
| FROM rust:${RUST_VERSION}-bookworm |
There was a problem hiding this comment.
Could we pin the base image and source revisions, verify every archive checksum, and use the same immutable pins in local preparation?
There was a problem hiding this comment.
Done in 293c2fa. All of it was fair — the image called itself reproducible while pinning almost nothing.
- base image pinned by index digest (index, not platform, so it still builds on arm64);
scripts/tool_pins.shis now the single source of versions and SHA-256 digests, read by both theDockerfileandscripts/prepare_env.sh, so the container and a developer machine cannot drift;- every download is verified before use, and an unknown platform is an error rather than an unverified fetch;
- libdmg-hfsplus is fetched by revision.
The libdmg pin came with a surprise worth flagging: pinning master broke the build. The repository default branch is only_what_core_needs, and master still carries the filevault/AES code, which does not compile against OpenSSL 3. git clone --depth 1 had been silently taking the default branch all along. The pin points there.
One consequence to be aware of: a digest-pinned base image needs the Dependabot config added in 47610bf, otherwise it goes stale with its CVEs.
| # обратный прежнему, поэтому старая схема zig-linux-<arch> отдаёт 404. | ||
| RUN set -eux; \ | ||
| arch="$(uname -m)"; \ | ||
| curl -fsSL "https://ziglang.org/download/${ZIG_VERSION}/zig-${arch}-linux-${ZIG_VERSION}.tar.xz" -o /tmp/zig.tar.xz; \ |
There was a problem hiding this comment.
Could we add bounded retries plus connection and overall timeouts to every bootstrap download, matching the AppImage path?
There was a problem hiding this comment.
Done in 293c2fa — --retry 3 --retry-all-errors --connect-timeout 15 --max-time N on every bootstrap download, and on the macOS SDK fetch as well.
This one had prior art in the branch: the original appimagetool 1.9.1 path hung for 17 minutes in CLOSE-WAIT on a stalled download precisely because there was no --max-time. In CI that is a job idling to its limit rather than failing.
| cp -r "$ROOT/assets/icons/hicolor/." "$APPDIR/usr/share/icons/hicolor/" | ||
| fi | ||
|
|
||
| if [[ ! -x "$APPIMAGETOOL" ]]; then |
There was a problem hiding this comment.
Could we verify the cached binary on every build and atomically refetch it when the checksum differs?
There was a problem hiding this comment.
Half of this was already in place, half is now done in 72c26c0.
The cached binary was already verified on every build (the else branch below the hunk you commented on), and the download was already atomic — mktemp + mv with an EXIT trap. What was missing is the refetch: a mismatch just failed, so bumping the pin with scripts/update_appimagetool_pin.sh left every local build broken until you manually removed .cache/tools/appimagetool-*.
Now a mismatching cache is refetched and the replacement is verified exactly as strictly, so an unverified binary is still never executed.
Please look at the test change with this in mind: the #111 test asserted the old contract (corrupt cache -> fail, curl never called). I split that case in two — corrupt cache with a valid source now refetches and builds, corrupt cache with a corrupt source still fails, leaves no temporary file and never runs the tool. If you would rather keep a hard failure there, say so and I will revert to failing with a clearer message that names the cache path instead.
|
|
||
| log "Downloading macOS SDK $SDK_VERSION from $URL" | ||
| curl -fsSL "$URL" -o "$tmp/$ARCHIVE" | ||
|
|
There was a problem hiding this comment.
Could we commit the SDK checksum, verify it before extraction, and reject URL or version overrides without a trusted digest?
There was a problem hiding this comment.
Done in 293c2fa. The digest for the pinned 15.5 SDK now lives in scripts/tool_pins.sh, it is verified before extraction, and an override without a trusted digest is rejected: MACOS_SDK_URL requires MACOS_SDK_SHA256, and an unknown MACOS_SDK_VERSION fails because there is no pinned digest for it.
This was the weakest link of the three: a third-party rebuild of Apples SDK, unpacked and used for linking, with verification opt-in. It is mitigated only by the path being experimental and unused by releases.
| sh: awk -F '"' '/^version = / { print $2; exit }' Cargo.toml | ||
| # MSI ProductVersion и CFBundleVersion принимают только числовые компоненты, | ||
| # поэтому предрелизный суффикс туда не попадает — его несут имена файлов. | ||
| NUMERIC_VERSION: '{{ (splitList "-" .VERSION) | first }}' |
There was a problem hiding this comment.
Could we preserve prerelease ordering numerically, or enable and test same-version upgrades from RC to stable?
There was a problem hiding this comment.
Done in 72c26c0, by making the MSI upgrade range inclusive — that is the only one of the two options that actually works, since MSI ignores the fourth version field when comparing, so a prerelease cannot be ordered below the stable release through ProductVersion at all.
AllowSameVersionUpgrades turned out to be a trap: wixl 0.101 in bookworm (which is what the container ships) silently ignores the attribute, emitting only a GLib warning on stderr and building a working MSI with no upgrade behaviour. So MajorUpgrade is expanded into explicit Upgrade/UpgradeVersion with Maximum="$(var.Version).1", which 0.101 and 0.106 interpret identically.
Verified with msiinfo on an MSI from a full container run: upgrade range [0.0.0, 0.3.2.1) with ENTROPY_UPGRADE_DETECTED, downgrade detection unchanged, and LaunchConditions / InstallValidate / RemoveExistingProducts at the same sequence positions MajorUpgrade used to generate. So the stable release now replaces an installed rc instead of leaving two entries in the programs list.
Not verified: an actual install/upgrade on Windows — I have no Windows machine. The MSI tables are checked, the installer behaviour is not.
| sources: | ||
| - assets/entropy.svg | ||
| generates: | ||
| - assets/icons/hicolor/256x256/apps/entropy.png |
There was a problem hiding this comment.
Could we declare all generated icon sizes as outputs, or validate the complete icon set through one tracked stamp?
There was a problem hiding this comment.
Done in 72c26c0 — all seven sizes are listed, along with the .ico and .icns. You are right about the failure mode: deleting any size other than 256 left the task up to date and the build then failed inside png2icns, which needs 512.
| - task: macos:all | ||
| package:windows: | ||
| cmds: | ||
| - task: windows:all |
There was a problem hiding this comment.
Could we route Windows packaging through Docker, or make the native command produce only the EXE as the documentation promises?
There was a problem hiding this comment.
Done in 72c26c0 — the second option, since the first would be wrong: cargo-zigbuild is pointless on Windows (the host compiler is already the target one) and wixl comes from msitools, which has no Windows build at all.
task package on Windows now runs a new windows:native task: native cargo build plus the portable .exe, and it points at task docker:windows for the installer. The windows:* cross tasks are unchanged for Linux and the container.
While fixing this I found the task file was unusable on Windows for a more basic reason: the global VERSION variable was parsed with awk, which does not exist there, so any task invocation failed — including task build. It is plain shell now, which go-tasks built-in interpreter runs on every platform.
Not verified on a real Windows host (no machine available): the PowerShell was syntax-checked with the PowerShell parser in a container, and the task graph was checked by dry-run, but neither is a substitute for running it there.
| internal: true | ||
| deps: [build, icons] | ||
| vars: | ||
| PKG_ARCH: '{{default "amd64" .PKG_ARCH}}' |
There was a problem hiding this comment.
Could we pin Docker to linux/amd64, or derive and verify the package architecture from an explicit build target
There was a problem hiding this comment.
Done in 72c26c0, taking the second option — deriving and verifying rather than forcing linux/amd64, because on Apple Silicon that would mean building Rust under QEMU emulation.
PKG_ARCH now follows the host (amd64/arm64) and feeds the AppImage file name and the ARCH passed to appimagetool as well. On top of that the label is verified against the binary with readelf before packaging, so a mismatch fails with a hint instead of shipping. Checked both ways: a normal run packages amd64, and task linux:deb PKG_ARCH=arm64 against an x86_64 binary now fails.
task linux:appimage deliberately refuses to run anywhere but x86_64: the pinned appimagetool is an x86_64 build and would not even execute on arm64, so producing a mislabelled AppImage is the one outcome worth avoiding. deb/rpm/arch build fine there.
This was reachable in practice, not just in theory — BUILD.md recommends task docker:dist to macOS users, and Docker Desktop on Apple Silicon runs an arm64 container by default.
| docker run --rm {{.SELINUX}} | ||
| -e HOST_UID={{.UID}} -e HOST_GID={{.GID}} | ||
| -v "$PWD":/work -w /work {{.IMAGE}} | ||
| sh -c 'task {{.TASKS}} VERSION={{.VERSION}}; rc=$?; chown -R "$HOST_UID:$HOST_GID" {{.DIST}} target .task .intentionally-empty-file.o 2>/dev/null || true; exit $rc' |
There was a problem hiding this comment.
Could we include every generated asset in ownership restoration, or run the build with the host UID and GID?
There was a problem hiding this comment.
Done in 72c26c0, with the second option: the container now runs as the invoking UID/GID (--user, plus an explicit HOME), and the ownership fixup is gone entirely.
The chown list was indeed incomplete — it missed assets/icons and, more annoyingly, the committed assets/entropy.ico and assets/entropy.icns, which came back as root-owned and broke the next local task icons or task clean.
Running as the host user surfaced a real bug in the image: cargo install cargo-zigbuild left $CARGO_HOME/registry root-owned, so the build failed with Permission denied before producing anything. That is fixed in 293c2fa by removing the registry cache from the image — cargo recreates it as the current user. Verified with a full task docker:dist from scratch: all six artifacts built, everything owned by me, nothing left as root.
On Windows the flag is simply omitted (no id), where the bind mount is already mapped to the current user.
| # Mac, а Rosetta перестаёт влиять на результат — арка выбирается явно, а не по | ||
| # тому, под какой архитектурой запущен шелл. TARGET (одна арка) поддержан ради | ||
| # вызывающих, которые ещё передают его: PR-гейт собирает по одной арке на раннер. | ||
| TARGETS="${TARGETS:-${TARGET:-aarch64-apple-darwin x86_64-apple-darwin}}" |
There was a problem hiding this comment.
Could we add a macOS CI case that runs the default universal path and verifies both the arm64 and x86_64 slices?
There was a problem hiding this comment.
Done in 47610bf — the PR gate now builds the same universal bundle as the release and asserts both slices with lipo -archs, instead of one architecture per runner.
You are pointing at a real hole: the lipo branch of this script, the universal-apple-darwin output path and the -universal.dmg name were executed for the first time at tag time. The script already had validate_binary_arch, which checks every requested slice — nothing ever ran it in universal mode.
Two things to weigh: wall-clock on the gate goes up (two compilations on one runner instead of two runners in parallel; total runner minutes are unchanged), and #74 touches build.yml too, so this will need coordinating with that branch.
I cannot verify this one locally — no Mac — so the first CI run on this PR is the actual test.
| # QUILL_SIGN_PASSWORD to sign with a real Developer ID instead. | ||
| if command -v quill >/dev/null 2>&1; then | ||
| log "Signing with quill" | ||
| quill sign "$APP/Contents/MacOS/entropy" >&2 || echo "quill sign failed (continuing unsigned)" >&2 |
There was a problem hiding this comment.
Could we fail signing by default, with an explicit and clearly labeled opt-in only if unsigned cross-built artifacts are intentional?
There was a problem hiding this comment.
Done in 6950758 — signing failure or a missing quill now fails the build. ALLOW_UNSIGNED=1 is the explicit opt-in, and it names the result -unsigned.dmg so it cannot be mistaken for a signed one in dist/.
The old || continuing unsigned was exactly the shape of thing that goes unnoticed on an experimental path that releases do not exercise.
Not verified on real hardware: this path cross-builds from Linux and the resulting bundle has never been opened on a Mac.
| have_tool dmg || missing+=("libdmg-hfsplus (.dmg)") | ||
| } | ||
|
|
||
| ((${#missing[@]})) || return 0 |
There was a problem hiding this comment.
Could we return a nonzero status when requested tools are still missing, so preparation remains a trustworthy gate?
There was a problem hiding this comment.
Done in 248fcc1 — report_missing_tools returns non-zero and prepare exits with it, so it works as a gate.
I kept an escape hatch: --no-strict restores the old warn-and-succeed behaviour. The reason is that on some distros a package genuinely does not exist (no msitools or icnsutils on alpine, for instance), and "install what you can" is a legitimate request there. Default is strict, which is what you asked for.
| # Rust needs the MSVC linker from the Visual Studio build tools; rustup prompts | ||
| # for it on first run, but installing it up front keeps `task build` unattended. | ||
| $packages = @( | ||
| @{ Id = 'Rustlang.Rustup'; Command = 'rustup'; Why = 'Rust toolchain' }, |
There was a problem hiding this comment.
Could we read the repository’s pinned Rust version and install or select that toolchain before invoking bare cargo?
There was a problem hiding this comment.
Done in 248fcc1 — the script reads rust from .tool-versions and runs rustup toolchain install plus rustup override set --path <repo> before anything invokes cargo.
Your comment exposed a wider inconsistency: the repository had three different answers to "which Rust". .tool-versions pinned it, asdf applied it on Linux and macOS, release.yml read it explicitly for the macOS job, and build.yml used dtolnay/rust-toolchain@stable. That last one is now on the pinned version too (47610bf), so the PR gate checks what the release is built with.
I stopped short of adding a rust-toolchain.toml, which would be the cleaner cross-cutting fix: asdf-rust does not read it, so it would mean two files to keep in sync. Happy to do it as a follow-up if you prefer that direction.
Not verified on a real Windows host: the file was syntax-checked with the PowerShell parser, but winget and rustup override were not actually run.
Образ назывался воспроизводимым, но по факту таким не был: базовый тег rust:1.97-bookworm пересобирается апстримом, zig, nfpm, go-task, quill и lipo качались без единой проверки, а libdmg-hfsplus собирался из произвольного HEAD стороннего репозитория. Проверялся ровно один инструмент — appimagetool, и то потому, что этого потребовал ergohaven#111. Что делаем: - scripts/tool_pins.sh становится единственным источником версий и контрольных сумм; его читают и Dockerfile, и scripts/prepare_env.sh, чтобы контейнер и машина разработчика не разъезжались по тулчейну; - базовый образ пиннится дайджестом индекса (не платформы) — образ собирается и на arm64; - каждая загрузка сверяется с суммой до использования, неизвестная платформа даёт ошибку, а не непроверенную загрузку; - libdmg-hfsplus берётся по ревизии. Пин указывает на дефолтную ветку репозитория (only_what_core_needs): на master остались filevault и AES-обвязка, которые не собираются с OpenSSL 3 — первая же сборка с пином на master упала именно на этом; - macOS SDK всегда сверяется с пиннутой суммой, а переопределение MACOS_SDK_URL или MACOS_SDK_VERSION без явного MACOS_SDK_SHA256 отклоняется: SDK — сторонняя пересборка, которая распаковывается и участвует в линковке; - у всех загрузок появились --retry и таймауты. Без --max-time оборванное соединение висит до лимита job'а, а не падает — ровно на это мы уже наступали с appimagetool 1.9.1. Отдельно чистится кэш реестра cargo после cargo install: он оставался root-only, и сборка под UID хозяина каталога падала на правах записи.
Локальная подготовка расходилась с образом сразу по трём пунктам: свои константы версий, загрузка инструментов в pipe (сумму там проверить негде), клон libdmg-hfsplus с плавающего HEAD. Плюс prepare завершался успешно, даже если половина инструментов не встала, — нехватка всплывала уже на середине сборки. Что делаем: - версии и суммы берутся из scripts/tool_pins.sh, теми же значениями, что и в образе; - nfpm и quill качаются в файл и сверяются до распаковки, libdmg-hfsplus собирается из пиннутой ревизии; - report_missing_tools возвращает ненулевой код, и prepare падает, если инструменты для заявленных целей отсутствуют. Для «поставь что можешь» на дистрибутивах, где части пакетов просто нет, добавлен --no-strict. На Windows prepare теперь читает .tool-versions и выбирает пиннутую версию Rust через rustup override. Раньше версию подхватывал только asdf на Linux и macOS, а на Windows cargo собирал чем угодно — «собралось локально» ничего не говорило о сборке релиза.
Разбор замечаний ревью по Taskfile и упаковке. PKG_ARCH был жёстко amd64, а бинарник собирался под текущий хост. На arm64 (Docker Desktop на Apple Silicon по умолчанию поднимает arm64- контейнер, и BUILD.md сам предлагает маководам task docker:dist) пакет уезжал с меткой amd64 и arm64-бинарником внутри: такой ставится и не запускается. Теперь арка выводится из хоста, попадает и в имя AppImage, и в ARCH для appimagetool, а метка сверяется с самим файлом через readelf — расхождение роняет упаковку с подсказкой. AppImage при этом честно отказывается собираться не на x86_64: пин appimagetool — x86_64. task package на Windows разворачивался в windows:all, то есть в cargo-zigbuild и wixl, которых на Windows нет и не может быть (wixl из msitools под Windows не существует). Добавлена windows:native: нативный портативный exe, а MSI — через task docker:windows, как и обещает документация. Заодно чинится разбор версии: он шёл через awk, из-за чего Taskfile на Windows не читался вообще, включая task build. Теперь это чистый shell, который выполняет встроенный интерпретатор go-task. Контейнер запускается от UID и GID хозяина каталога вместо root, а chown-постобработка убрана. Она всё равно не покрывала assets/icons и закоммиченные assets/entropy.ico и .icns — после сборки они оставались root:root, и следующая локальная задача падала на правах. generates у icons перечисляет все семь размеров, а не только 256: без этого удаление любого другого png не сбрасывало up-to-date, и сборка падала уже на png2icns, которому нужен 512. Испорченный или устаревший кэш appimagetool перекачивается, а не роняет сборку: расхождение почти всегда значит бамп пина. Свежая загрузка проверяется так же строго, поэтому непроверенный бинарник по-прежнему не исполняется никогда. Тест из ergohaven#111 переписан под это: вместо одного кейса теперь два — «кэш не сходится, источник валиден» и «источник тоже испорчен», второй по-прежнему требует падения и уборки временного файла. MSI: предрелиз и стабильный релиз несут одинаковый ProductVersion (0.3.2-rc.1 -> 0.3.2, суффикс MSI не принимает), поэтому апгрейд обязан захватывать равную версию — иначе тестер, поставивший rc, не обновится, а получит вторую запись в списке программ. AllowSameVersionUpgrades для этого не годится: wixl 0.101 из bookworm молча игнорирует атрибут, печатая только warning в stderr. MajorUpgrade развёрнут в явные Upgrade и UpgradeVersion с верхней границей «версия + лишний компонент», что одинаково понимают и 0.101, и 0.106. Позиции LaunchConditions, InstallValidate и RemoveExistingProducts сверены с тем, что генерировал MajorUpgrade, — поведение прежнее.
Проверка тега была глобом (v[0-9]*.[0-9]*.[0-9]*), а имя тега попадало в
текст shell-команд подстановкой ${{ }}. Ref-имена допускают '$', ';' и
обратные кавычки, так что тег вида v1.2.3;<команда> проходил проверку и
доезжал до командной строки. Фильтр on.push.tags прикрывает только push,
а workflow_dispatch принимает произвольную строку.
Что делаем:
- тег сверяется bash-регуляркой целиком. Промежуточный вариант через
grep -Eqx отвергнут: он сопоставляет построчно, и многострочный ввод в
workflow_dispatch («v1.2.3\n<команда>») проходил бы по первой строке;
- версия передаётся в task переменной окружения, а не подстановкой;
- contents: write остаётся только у публикующего job'а. Сборочные
прогоняют сторонний тулчейн, и токен с правом записи им не нужен;
- добавлен шаг, проверяющий, что собран каждый формат пакета:
if-no-files-found ловит только полную пустоту, и пропавший .rpm уехал
бы в релиз незамеченным. Проверка идёт по формату, а не по имени: nfpm
переписывает предрелизную версию (0.3.2-rc.1 -> 0.3.2~rc.1 в deb и
rpm, 0.3.2rc.1 в arch), и привязка к тегу давала бы ложные падения;
- все actions пиннуты коммитами с версией в комментарии, включая
dtolnay/rust-toolchain@master. Чтобы пины не застыли вместе с
уязвимостями, добавлен .github/dependabot.yml на actions и docker.
PR-гейт собирает тот же универсальный бандл, что и релиз, и проверяет
обе арки через lipo. Раньше каждая арка шла своим раннером, и ветка со
слиянием слайсов впервые исполнялась уже на теге — то есть ровно тот
путь, которым выпускается macOS, до релиза никто не проверял. Версия
Rust там же берётся из .tool-versions вместо @stable: гейт должен
проверять то, чем собирается релиз.
quill sign вызывался с «|| continuing unsigned», а отсутствие quill вообще проходило молча. В итоге в dist/ мог оказаться .dmg, который на Mac не запустится, и понять это по содержимому каталога было нельзя. Теперь неудачная подпись или отсутствующий quill — ошибка сборки. Осознанный отказ от подписи остаётся возможным через ALLOW_UNSIGNED=1, и такой образ называется -unsigned.dmg, чтобы его нельзя было спутать с подписанным.
BUILD.md приводится в соответствие с поведением после правок по ревью: арка пакетов следует хосту и сверяется с бинарником, task package на Windows делает только портативный exe, контейнер работает от текущего пользователя и ничего не оставляет под root, prepare гейтит по недостающим инструментам и понимает --no-strict, версии и суммы загружаемых инструментов живут в scripts/tool_pins.sh, кросс-сборка macOS требует подписи, а PR-гейт проверяет универсальный бандл. Раздел про починку root-owned файлов оставлен: у тех, кто уже собирал старой версией, такие файлы на диске остались, но команда переписана — контейнер больше не работает от root по умолчанию.
Review feedback addressedAll 16 comments are implemented and pushed as six commits ( Three bugs surfaced while implementing the fixes
One change needs a maintainer opinion The appimagetool refetch contradicts a test that came in with #111, which asserted "corrupt cache -> fail, no refetch". I split that case into two — corrupt cache with a valid source refetches and verifies, corrupt cache with a corrupt source still fails and never executes the tool — so the "no unverified binary is ever run" invariant holds. If you would rather keep the hard failure, I will revert to failing with a message that names the cache path instead. Details in the thread on Also note What was verified, and how
What was not verified, and whyI do not have a Mac or a Windows machine, so everything below is reasoned and statically checked but never executed on the target platform:
If someone with either machine can run |
kissetfall
left a comment
There was a problem hiding this comment.
There is valuable build work here, but the current 31-file migration is not safe to merge as one change. The blocking issues are:
- The release workflow expands publishing from the repository's
v0.*public channel to every semver major. Entropy's public GitHub release contract is intentionallyv0.*; please preserve that restriction. - The new release produces only
entropy-vX-macos-universal.dmg, whilesrc/app_update.rs::platform_asset_matchescurrently requiresmacosplusarm64orx86_64. About Entropy would therefore report an update but fail to select/download the macOS asset. The updater and its tests must be updated together with any universal naming change. - Removing the existing Inno Setup path in favor of MSI is a product/installer migration, not a transparent build refactor. It needs an explicit maintainer decision plus native Windows install, upgrade, uninstall, and portable-EXE smoke tests.
- The native macOS and Windows paths have not run, this branch conflicts with current
main, and no PR CI evidence is available. The overlap with draft #74 also needs one chosen signing/packaging direction.
Please split this into reviewable stages: (a) Taskfile/Docker/reproducible build infrastructure, (b) Linux packages, and (c) installer + release workflow + universal macOS changes. That keeps each release-surface migration independently testable and avoids replacing every platform path in one merge.
# Conflicts: # README.md
RUСпасибо за разбор — критика по делу, принимаем полностью. 31 файл в одном PR действительно не ревьюится, и три релизные поверхности сразу менять не следовало. Этот PR закрываем. Вместо него будет серия маленьких PR: каждый с минимальным набором изменений, который можно принять, проверить и откатить независимо от остальных. Порядок предлагаем чуть иной, чем в вашем разбиении:
Docker едет в первом шаге, а не отдельно от Linux-пакетов, по одной причине: сам по себе Taskfile — это обёртка над Ваши четыре пункта, чтобы они не потерялись между PR:
ENThanks for the review — the criticism is fair and we accept it. A 31-file change is not reviewable in one piece, and changing three release surfaces at once was the wrong call. We are closing this PR. It will be replaced by a series of small ones, each carrying the minimum change that can be reviewed, verified and reverted on its own. The proposed order differs slightly from yours:
Docker rides along with the Linux packages rather than landing separately because a Taskfile on its own is just a wrapper around Your four blocking points, so they do not get lost between PRs:
|
|
Закрываем в пользу серии небольших PR, как договорились выше. Первый из них — #152: Taskfile, воспроизводимый образ и Linux-пакеты (deb, rpm, archlinux, AppImage). Дальше — перенос существующих сборок Windows и macOS в тот же flow, и только потом релизный workflow, вопрос MSI против Inno Setup и universal macOS вместе с правкой Closing in favour of the series of smaller PRs agreed above. The first one is #152: the Taskfile, the reproducible image and the Linux packages. Windows and macOS move into the same flow next; the release workflow, the MSI-vs-Inno-Setup decision and the universal macOS bundle come after that, one decision per PR. |
RU
Кратко
Единая система сборки и упаковки на go-task:
task packageсобирает нативные пакеты для текущей ОС,task docker:dist— Linux и Windows воспроизводимо в контейнере с пиннутым тулчейном. Релизный workflow ходит тем же путём, что и разработчик локально.Что появляется в сборке:
deb,rpm,archlinux(nfpm) рядом с AppImage. Пакет ставит бинарник,.desktop, AppStream metainfo, udev-правила и иконки hicolor..exeи MSI-инсталлятор (wixl), оба кросс-собираются из Linux через cargo-zigbuild..dmgвместо двух отдельных под arm64 и x86_64: обе арки собираются и сливаютсяlipo.Что меняется в CI
build.ymlтронут минимально — матрица, имена job'ов и шаг с тестами#111оставлены как есть. Добавлена толькоconcurrency(новый пуш в PR отменяет прошлый прогон) и артефакт macOS сменился с.app.zipна.dmg, потому что скрипт теперь собирает.dmg.release.ymlпереработан:Cargo.toml— иначе предрелизv0.3.2-rc.1выложил бы файлы с именами будущего стабильногоv0.3.2;validateдо сборки проверяет формат тега, наличие секции вCHANGELOG.mdи совпадение сCargo.toml— раньше отсутствие секции роняло релиз уже после десятков минут сборки;dist/**/*утащил бы сгенерированный конфиг nfpm и распакованныйEntropy.appцеликом;Связь с #74
В #74 подпись и нотаризация предполагаются только на нативном Mac. Кросс-путь в этом PR (
task macos:cross, помечен экспериментальным) использует quill — он подписывает Mach-O из-под Linux и умеет не только ad-hoc, но и Developer ID (QUILL_SIGN_P12), и нотаризацию через Apple Notary API (quill notarize,quill sign-and-notarize).То есть после этого PR открывается путь, при котором вся сборка под все три платформы, включая подпись и нотаризацию macOS, выполняется из-под Linux — без Mac в цепочке релиза. Сейчас этот путь не задействован: релизный macOS-артефакт по-прежнему собирается на macOS-раннере нативно, а
macos:crossподписывает только ad-hoc и нотаризацию не вызывает. Развернуть его до полноценного — отдельная задача, и она пересекается с тем, что решает #74.Стоит учесть, что #74 правит те же
build.yml,release.ymlиscripts/build_macos_app.sh. Здесьbuild_macos_app.shпереписан под универсальный бандл, но продолжает понимать переменнуюTARGETс одной архитектурой, чтобы вызов из workflow не расходился с веткой #74.Слияние с #111
Ветка изначально переводила сборку на
appimagetool1.9.1 из нового репозитория AppImage/appimagetool. В отличие от AppImageKit 12, он не несёт runtime внутри и тянет его с GitHub на каждой сборке — на оборванном соединении это зависает без таймаута (проверено: 17 минут вCLOSE-WAIT, в CI это простой job'а до его лимита).После того как upstream принял #111, конфликт разрешён в пользу upstream: возвращён пиннутый AppImageKit 12 с проверкой SHA-256, а собственный обвес с runtime убран — на пиннутом инструменте проблемы нет. Три теста из #111 проходят на изменённом скрипте.
Dockerfileберёт релиз и контрольную сумму из того жеscripts/appimagetool_pin.sh, иначе в контейнере оказался бы не тот инструмент, который проверяют тесты.Проверено
task docker:distв контейнере с нуля и нативныеtask linux:pkg,linux:appimage,windows:all— сVERSION=0.3.2-rc.1.0.3.2~rc.1для deb и rpm; зависимости, udev-правила и AppStream-файл на месте.ProductVersionв MSI иCFBundleVersionполучают числовую версию — оба формата не принимают суффикс..desktop, полный набор иконок hicolor.validateпрогнана на пяти тегах, включая легаси-v1.14.3— он отсекается.scripts/test_verify_sha256.sh,scripts/test_build_linux_appimage.sh,scripts/test_update_appimagetool_pin.sh,shellcheck,cargo test— 525 тестов зелёные.Что не проверено: macOS-сборка запускалась только локально на Linux в кросс-режиме, нативный путь на macOS-раннере проверит первый прогон CI.
Что хотелось бы уточнить у мейнтейнеров
installer/entropy.iss,scripts/build_windows_installer.ps1) в пользу MSI через wixl: он кросс-собирается из Linux, а Inno Setup требует Windows. Ссылок на удалённые файлы в репозитории не осталось, но подтвердите, что от.issможно отказаться.release.ymlрасширен сv0.*до semver (v1.2.3иv1.2.3-rc.1), чтобы первый тег следующей мажорной версии не остался без релиза. В репозитории лежат легаси-тегиv1.13.x, но проверки вvalidateих отсекают.0.3.2rc.1и считается новее0.3.2. Для rc это терпимо, но если важно — можно не публиковать Arch-пакеты для предрелизов.EN
Summary
A single build and packaging system driven by go-task.
task packageproduces native packages for the current OS;task docker:distbuilds the Linux and Windows artifacts reproducibly inside a pinned toolchain image. The release workflow runs the same path a developer runs locally.deb,rpmandarchlinuxpackages (nfpm) alongside the AppImage, shipping the binary,.desktopentry, AppStream metainfo, udev rules and hicolor icons..exeplus an MSI installer (wixl), both cross-built from Linux with cargo-zigbuild..dmginstead of separate arm64 and x86_64 downloads; both slices are merged withlipo.CI changes
build.ymlis touched as little as possible: the matrix, job names and the#111test step are unchanged. Onlyconcurrencyis added, and the macOS artifact moves from.app.zipto.dmgbecause that is what the script now produces.release.ymlis reworked: artifact versions come from the tag rather thanCargo.toml, avalidatejob checks the tag format, theCHANGELOG.mdsection and theCargo.tomlversion before the builds run, the toolchain image is built in a layer-cached step, only the packages themselves are attached to the release, and a release can be re-run manually from the Actions tab against an existing tag.Relation to #74
#74 assumes signing and notarization happen on a native Mac. The cross path here (
task macos:cross, marked experimental) uses quill, which signs Mach-O binaries from Linux and supports Developer ID identities (QUILL_SIGN_P12) and Apple's Notary API (quill notarize,quill sign-and-notarize).So this PR opens the door to building every platform, including signed and notarized macOS artifacts, entirely from Linux — no Mac in the release chain. That path is not used yet: release macOS artifacts are still built natively on a macOS runner, and
macos:crossonly signs ad-hoc without notarizing. Taking it further overlaps with what #74 solves, so it is worth deciding together.Note that #74 touches the same
build.yml,release.ymlandscripts/build_macos_app.sh. The script here was rewritten for the universal bundle but still honours a single-architectureTARGET, so the workflow call site stays compatible with that branch.Relation to #111
The branch originally switched to
appimagetool1.9.1, which — unlike AppImageKit 12 — does not embed the runtime and downloads it from GitHub on every build, hanging with no timeout when that download stalls. After #111 landed, the conflict was resolved in favour of upstream: the pinned AppImageKit 12 with SHA-256 verification is back and the custom runtime handling is gone. All three tests from #111 pass against the modified script, and theDockerfilenow reads the release and checksum from the samescripts/appimagetool_pin.sh.Validation
Full
task docker:distfrom scratch plus nativelinux:pkg,linux:appimageandwindows:all, all withVERSION=0.3.2-rc.1; package metadata, MSIProductVersion, AppImage contents, thevalidatelogic across five tags, the three#111tests,shellcheck, andcargo test(525 passing). Not verified: the native macOS runner path — the first CI run will cover it.Related: #74
Related: #111