Skip to content

ci(macos): build an unsigned Apple Silicon community app - #89

Draft
Hao0321 wants to merge 5 commits into
mainfrom
build/macos-community-app
Draft

Hao0321 wants to merge 5 commits into
mainfrom
build/macos-community-app

Conversation

@Hao0321

@Hao0321 Hao0321 commented Oct 6, 2026 •

Copy link
Copy Markdown
Owner

What changed

This adds a GitHub Actions pipeline that builds an Editkin desktop app for Apple Silicon Macs from the community source. The app is ad-hoc signed and is for the maintainer's own use; it is not an official release.

Release builds of the Tauri shell only use files inside the app bundle, so the pipeline stages a complete runtime first:

Bundled file(s) under Contents/Resources Produced by
runtime/node Node 22.23.2 darwin-arm64 tarball, SHA-256 pinned (same pin as stage-platform-runtime.mjs)
runtime/ffmpeg, ffprobe, lib/*.dylib, licenses, FFMPEG-PROVENANCE.json FFmpeg 8.1.2 built from a tarball pinned by size and SHA-256
runtime/whisper-cli and its receipts whisper.cpp 1.9.2, the existing pin and Metal flags
runtime/hao-core cargo build --release --locked
runtime/service.mjs, mcp.mjs, its identity file, remote.mjs node scripts/build-desktop.mjs --community --with-mcp-and-remote
runtime/PLATFORM-MANIFEST.json the stager, with schema 2 and a community block (officialRelease: false, omittedRuntimeFiles)
font-packs/editkin-open-fonts/ the 5 OFL fonts plus 43 static faces from scripts/build-static-font-pack.py, using fontTools 4.60.2 installed by pip in hash-checking mode
agent-runtime-v3/*, demo media, color/aces2, plugins repository files

Not bundled: editkin-gpu-compositor. spikes/gpu-compositor embeds three generated ACES 2 output LUTs with include_bytes! and refuses to start unless their bytes match the owner's pinned SHA-256. The LUTs are gitignored. I regenerated them with scripts/generate-aces-luts.py and the published PyOpenColorIO 2.5.2 wheel on Linux x86_64. Every LUT that OCIO bakes came out with different bytes, so CI cannot recreate them, and that check must not be loosened.

So the community app ships without the compositor:

  • COMMUNITY_OMITTED_RUNTIME_FILES records it.
  • The stager does not stage it and the workflow does not build it.
  • The overlay removes its resource mapping.
  • The app gate fails if it is present.

The default Rec.709 pipeline exports through FFmpeg and does not use it. Scene-linear ACES 2 output and GPU preview are unavailable in this build. The owner's macos-bundle-runtime-gate.mjs requires the compositor in its closed-world set, so the workflow does not run it.

FFmpeg build: library autodetection is off. GPL is enabled, plus libx264, libx265, zimg, libass, dav1d, VideoToolbox and AudioToolbox.

  • --extra-libs=-liconv links macOS's own libiconv. With --disable-autodetect, FFmpeg's configure only probes libc for iconv, which macOS does not provide.
  • The dependencies come from Homebrew bottles. They are copied into runtime/lib, rewritten to @loader_path with every LC_RPATH removed, and ad-hoc re-signed.

Downloads: each one is checked against its pinned SHA-256 in memory and then streamed to tar's stdin, so no unverified archive is written. fontTools is installed by pip --require-hashes from the pinned wheel URL.

The stager fails if:

  • any Mach-O still references a host or build path;
  • a binary is not arm64;
  • a required encoder or filter is missing;
  • dyld loads anything outside the bundle and the OS.

Packaging: the app is built with npx tauri build --features community-desktop --bundles app,dmg and the overlay src-tauri/tauri.macos.community.conf.json. The minimum macOS is raised to the highest minimum any bundled binary declares. No Rust code changes.

Checks in the same job, on the app extracted from the DMG:

  • codesign --verify --deep --strict
  • scripts/macos-community-app-gate.ts: closed file set, hashes, encoders, burned-in captions, a demo frame, and the bundled service's import, preview proxy, save/reopen and export
  • npm run test:journey with the bundled FFmpeg
  • a launch smoke test

Uploaded artifacts (14 days): Editkin-community-macos-arm64.dmg, a zipped .app, SHA256SUMS, and the evidence files.

Triggers:

  • Manual workflow_dispatch, only on main and only for actor id 126182090.
  • pull_request from this repository when the pipeline files change. That is how this PR builds the first artifact.

Other files:

  • scripts/build-desktop.mjs: an opt-in --with-mcp-and-remote for community builds. The existing paths are unchanged.
  • .gitignore: ignores /.platform-runtime*.
  • docs/BUILDING.md: how to run the build and open an ad-hoc-signed app (Finder → right-click → Open, or xattr -dr com.apple.quarantine /Applications/Editkin.app), and its limits.

User journey and platform

macOS 12+ on Apple Silicon. The maintainer downloads the DMG from the workflow run and opens the app.

Features that fail closed:

  • creative and personal packs
  • the Auto Roto product route
  • ACES 2 LUTs and GPU programs
  • scene-linear ACES 2 output and GPU preview (no GPU compositor)
  • motion-composition v2 export
  • Agent setup

Validation

macOS CI (run 37462382388, head 7cad210, macos-15 arm64) built the app and DMG, then checked a clean copy taken out of the DMG:

  • scripts/macos-community-app-gate.ts: GREEN, all 7 steps PASS with no warnings:
    • runtime closure and hashes;
    • Mach-O architecture, signature and relocation;
    • FFmpeg components;
    • dyld loads only bundled and OS libraries;
    • hao-core and whisper-cli;
    • FFmpeg encodes and filters through Editkin's argument builders;
    • the bundled service's import, preview proxy, save/reopen and export.
  • npm run test:journey with the bundled FFmpeg 8.1.2: GREEN. The export is H.264 High + AAC, 960×540, 12 s.
  • Launch: EDITKIN_SMOKE=1 exited 0, so the web UI loaded and called smoke_ready over IPC. A normal launch was still running after 30 s, with its resident service.
  • Artifact SHA256SUMS:
    • a812b9f17678bc24145d725607cb930ecf976b55c55282dbeb3fc8e84480af5d Editkin-community-macos-arm64.dmg
    • ab190d516e73cd9b54cff09717a04ce66256516f78ff61458f196ea8e7905756 Editkin-community-macos-arm64.app.zip
  • CodeQL: no new alerts. Source CI on 3 platforms, Linux GTK desktop and dependency-review: green.

On Linux:

  • actionlint with shellcheck: clean.
  • scripts/lib/macos-community-runtime.test.mjs: 16/16.
  • A structural harness on the merged Tauri configs: verifyBundleClosure passes, and rejects a bundle with a compositor added.
  • escapeFilterOptionPath matches the old two-pass version on 200,000 random strings.
  • pip --require-hashes refuses a wrong hash.
  • bsdtar extracts the pinned tarballs from stdin and rejects junk.
  • typecheck, build and npm test (2184 tests): pass.
  • source:scan and source:verify:self-test: GREEN.
  • Earlier: the FFmpeg tarball's .asc signature verified against the FFmpeg release key FCF9 86EA 15E6 E293 A564 4F10 B432 2F04 D676 58D8.

Not covered:

  • hands-on GUI use;
  • audio output;
  • Gatekeeper on a downloaded, quarantined copy;
  • other Macs and macOS versions;
  • transcription quality.

Security and provenance

  • New network access during CI:
    • nodejs.org, ffmpeg.org (falling back to Ubuntu's mirror), codeload.github.com and files.pythonhosted.org, all hash-pinned.
    • Homebrew bottles. Their versions are not pinned, but each is recorded with its hashes in FFMPEG-PROVENANCE.json.
  • Process execution in CI: brew, cmake, make, cargo, pip, tar, codesign and install_name_tool.
  • Workflow: contents: read only, no secrets, and every action pinned to a full SHA.
  • Integrity checks: none is weakened. The compositor's LUT hash pins are untouched; the compositor is left out instead.
  • Licensing: the bundled FFmpeg is GPL. Redistributing the app or DMG requires offering the complete corresponding source of FFmpeg and every bundled library. The workflow records those sources but does not archive them. Do not publish the artifact as an Editkin download.
  • No secrets or private footage.

Found while building this, not fixed here:

  • scripts/stage-tauri-resources.mjs stages only launcher.mjs and the contract, but the launcher has imported ./lib/regular-file.mjs since fix(security): read script inputs through a single handle and encode fixture URLs #75. This overlay ships that file.
  • @tauri-apps/api 2.11.1 does not match the tauri 2.12.0 crate, hence --ignore-version-mismatches.
  • The pinned ACES 2 LUT hashes cannot be reproduced from the published OCIO 2.5.2 wheel. Re-enabling the compositor here would need the owner's LUT files, or hashes recorded from a reproducible bake.

DCO

Every commit has a Signed-off-by: trailer.

🤖 Generated with Claude Code

https://claude.ai/code/session_01YQbUWKu73ka7dZ58vWVzgY


Generated by Claude Code

Hao0321 and others added 3 commits October 6, 2026 08:09
A portable community app bundle resolves runtime/mcp.mjs (with its
material-color identity sidecar) and runtime/remote.mjs in release mode.
`--community --with-mcp-and-remote` now builds those self-authored
entrypoints next to service.mjs. Plain `--community` still builds only
the service, and the owner's release-input manifest stays limited to
non-community builds.

Signed-off-by: Hao0321 <126182090+Hao0321@users.noreply.github.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YQbUWKu73ka7dZ58vWVzgY
Add a macOS arm64 pipeline for the community-desktop app that does not
depend on owner-only inputs or ffmpeg-static:

- scripts/stage-macos-community-runtime.mjs stages .platform-runtime/:
  FFmpeg 8.1.2 built from the SHA-256-pinned release tarball with
  --disable-autodetect against Homebrew x264/x265/zimg/libass/dav1d,
  its dylib closure copied and rewritten to @loader_path, Node.js and
  whisper.cpp with the owner's pins, hao-core and the GPU compositor,
  and the static caption faces export needs (repo generator plus a
  pinned fontTools wheel). Every Mach-O except Node.js is ad-hoc
  re-signed; hashes, FFmpeg provenance, license texts and a GPL notice
  are recorded. It fails on host or build-path references, non-arm64
  slices and missing encoders, decoders, filters or formats.
- src-tauri/tauri.macos.community.conf.json maps the staged runtime,
  the service/MCP/Remote bundles, the Agent launcher files and the
  generated font pack into the bundle, keeping signingIdentity "-".
- scripts/macos-community-app-gate.ts checks a delivered Editkin.app:
  hashed runtime closure, arm64 Mach-O files, codesign, the libraries
  dyld loads, FFmpeg encodes and filters through Editkin's argument
  builders, and import, preview proxy, save/reopen and export through
  the bundled resident service.
- .github/workflows/macos-community-desktop.yml runs on macos-15 for
  the maintainer's manual runs on main and for same-repository pull
  requests that touch the pipeline, with read-only contents permission,
  no secrets and SHA-pinned actions reused from Source CI. It builds the
  app and DMG, verifies a clean copy from the DMG (including the owner's
  macOS runtime gate, the source journey with the bundled FFmpeg and an
  app launch), and uploads the DMG, a zipped app and SHA256SUMS.
- scripts/lib/macos-community-runtime.mjs holds the pins and pure
  helpers; its unit test runs in the default suite on every platform.

No src-tauri Rust code changes. The app is ad-hoc signed and not an
official release.

Signed-off-by: Hao0321 <126182090+Hao0321@users.noreply.github.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YQbUWKu73ka7dZ58vWVzgY
Explain how to run the macOS community desktop workflow, verify and
open the unsigned app, what it bundles, how to build it locally, and
its limits: Apple Silicon only, owner-only features fail closed, Agent
setup is not expected to work, and redistribution must comply with the
GPL because the bundled FFmpeg includes libx264 and libx265.

Signed-off-by: Hao0321 <126182090+Hao0321@users.noreply.github.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YQbUWKu73ka7dZ58vWVzgY
Comment thread scripts/lib/macos-community-runtime.mjs Fixed
Comment thread scripts/lib/macos-community-runtime.test.mjs Fixed
Comment thread scripts/lib/macos-community-runtime.test.mjs Fixed
Comment thread scripts/stage-macos-community-runtime.mjs Fixed
Comment thread scripts/stage-macos-community-runtime.mjs Fixed
Comment thread scripts/stage-macos-community-runtime.mjs Fixed
Comment thread scripts/stage-macos-community-runtime.mjs Fixed
Hao0321 and others added 2 commits October 6, 2026 12:08
The macOS job failed compiling spikes/gpu-compositor: it include_bytes!
three generated ACES 2 output LUTs that are gitignored, and it refuses
to start unless their bytes match the owner's pinned SHA-256. Running
scripts/generate-aces-luts.py with the published PyOpenColorIO 2.5.2
wheel (Linux x86_64) produced different bytes for every LUT OCIO bakes,
so CI cannot recreate them, and the check must not be loosened.

Leave the compositor out of the community app instead:

- COMMUNITY_OMITTED_RUNTIME_FILES records it; the community
  PLATFORM-MANIFEST is the owner's file set minus that entry and lists
  it under community.omittedRuntimeFiles.
- The stager no longer requires, copies or signs it, and the workflow
  no longer builds it. The Tauri overlay removes its resource mapping.
- The app gate fails if it is shipped. The owner's
  macos-bundle-runtime-gate.mjs requires it in its closed-world set, so
  the workflow no longer runs that gate.

The default Rec.709 pipeline exports through FFmpeg and does not use
the compositor. Scene-linear ACES 2 output and GPU preview are
unavailable in this build; docs/BUILDING.md and the bundled notice say
so.

Also resolve the CodeQL findings on this pipeline:

- Verified downloads are streamed to tar's stdin instead of being
  written to an archive file first.
- fontTools is installed by pip in hash-checking mode from the pinned
  wheel URL.
- escapeFilterOptionPath applies both FFmpeg escape levels in one pass
  (equivalence with captionAss.ts stays covered by the unit test, now
  with tabs, newlines, NBSP, U+3000 and CJK).
- The unit test's JSON merge skips prototype keys, and a configure
  argument check no longer uses an unanchored regex alternation.

Signed-off-by: Hao0321 <126182090+Hao0321@users.noreply.github.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YQbUWKu73ka7dZ58vWVzgY
The macOS job's FFmpeg link failed with undefined _iconv, _iconv_open
and _iconv_close. With --disable-autodetect, FFmpeg's configure probes
iconv only in libc and never adds -liconv, but on macOS iconv is in
/usr/lib/libiconv.2.dylib. Since --enable-iconv was explicit,
CONFIG_ICONV stayed enabled and only the final link noticed.

Pass --extra-libs=-liconv. It reaches the program link through
EXTRALIBS, and the dependency is an OS library that the runtime audit
already allows. The unit test now requires the flag alongside
--enable-iconv.

Signed-off-by: Hao0321 <126182090+Hao0321@users.noreply.github.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YQbUWKu73ka7dZ58vWVzgY
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants