Distribution
Shipping real native installers — cross-arch builds, signing, notarization, and CI.
murasaki bundle / murasaki installer ship real installers on macOS
(.app / .dmg), Windows (a portable .zip, an NSIS .exe, and an
MSI .msi), and Linux (an AppDir + .AppImage, and a .deb),
cross-arch (arm64 / x64) on all three. The Linux .AppImage runs and
self-updates like the macOS/Windows payloads do; see
Linux: AppImage and .deb below for its update
story, the .deb's system-package-manager caveat, and the FUSE note for
running an AppImage in a container/CI.
| Target | Bundle | Installer | Public code signing |
|---|---|---|---|
| macOS arm64 / x64 | .app + .app.zip | .dmg | Developer ID + notarization built in |
| Windows x64 / arm64 | portable folder + .zip | NSIS .exe; WiX .msi on Windows | Authenticode via SignTool built in |
| Linux x64 / arm64 | AppDir + .AppImage | .deb | GPG detached signatures via --sign; no distro-repo/keyring trust |
A generated file is not automatically distribution-ready. Test the installed artifact on a clean machine, validate its signature, and verify every production dependency/resource. Murasaki is pre-1.0 and its Node dependency packaging does not cover every dynamic native addon or runtime-discovered asset layout yet.
Cross-arch builds
The bundle carries a portable, target-specific Node runtime (downloaded from
nodejs.org and cached under ~/.murasaki/node/) plus the compiled native
launcher binary — not whatever node happens to be running the CLI. Murasaki
verifies Node's clear-signed SHASUMS256.txt.asc against pinned official Node
release-key fingerprints before trusting the selected archive checksum. That
means an Apple Silicon dev machine can also produce an Intel build, and vice
versa:
Runtime downloads are HTTPS-only, timeout- and size-bounded, streamed to a private temporary file, signature- and checksum-verified before caching. Murasaki re-hashes a cached Node or AppImage runtime before every use; a partial or modified cache entry is discarded rather than packaged.
murasaki bundle --arch x64 # x64 .app on an Apple Silicon Mac
murasaki bundle --arch arm64 # arm64 .app on an Intel Mac
murasaki bundle --target win32-arm64 # an arm64 Windows bundlemurasaki installer forwards --arch / --target to bundle the same way.
Windows targets are win32-x64 / win32-arm64.
URL schemes and file associations
Declare packaged-app handlers in murasaki.config.ts:
import { defineConfig } from 'murasaki'
export default defineConfig({
appId: 'com.example.notes',
productName: 'Notes',
protocols: [{ scheme: 'example-notes', name: 'Notes link' }],
fileAssociations: [{
extensions: ['enote'],
name: 'Notes document',
role: 'editor',
mimeType: 'application/x-example-note',
}],
})The packaging result differs by target:
| Artifact | Registration behavior |
|---|---|
macOS .app / .dmg | CFBundleURLTypes, CFBundleDocumentTypes, and exported document UTIs are written to the app's Info.plist before signing. The DMG contains that app unchanged. |
Windows NSIS .exe | Registers protocols, ProgIDs, Open With entries, and Default Apps capabilities per-user by default, or per-machine with installMode: 'perMachine'. |
Windows WiX .msi | Registers the same handlers per-machine. |
Windows portable folder / .zip | Does not modify the registry or register itself automatically. |
Linux AppDir / .AppImage / .deb | MimeType= lines (x-scheme-handler/<scheme> per protocol, application/x-<extension> per file-association extension) are written into the .desktop file. The .deb installs it under usr/share/applications/ and refreshes the desktop database on install/remove; a manually-extracted AppDir/.AppImage has no equivalent OS-level registration step. Cold-start argv (the %U/%F the .desktop Exec= line expands to) and second-instance activation both work either way — see below. |
Matching cold-start, second-instance, and macOS open events are delivered to
Node Main's openRequested() hook after ready(). The Windows portable build
can still receive a registered-looking URL or file when it is passed directly
on its command line, but installation-free artifacts intentionally do not
claim operating-system defaults.
Windows registration makes the app an available handler; it does not overwrite the user's protected default-app choice. Test both a fresh install and an upgrade, and verify that uninstall removes only your app's handler entries.
Signing & notarization
Murasaki owns the signing orchestration, not the publisher identity. Supply
your Apple Developer ID or Windows certificate/Artifact Signing profile;
--sign deliberately fails instead of silently publishing unsigned output.
By default, murasaki bundle produces an ad-hoc signed .app, and
murasaki installer puts that app in a .dmg. This lets macOS verify local
bundle integrity, but it does not identify a trusted developer and the result
is not notarizable. For your own downloaded development artifact, you can
remove quarantine with:
xattr -dr com.apple.quarantine "<path>"Do not make that command the installation path for end users. Public, warning-free distribution requires Developer ID signing and notarization.
For warning-free distribution, sign and notarize with your own Apple Developer ID — Murasaki ships no certificate of its own:
murasaki bundle --sign # Developer ID-sign the .app
murasaki installer --sign --notarize # + submit the .dmg to Apple, staple the ticket--signsigns the.appwith a hardened runtime (Apple's documented flow: inner code first, then the outer bundle). The signing identity resolves from$MURASAKI_SIGN_IDENTITY, thenconfig.sign.identity, then the first "Developer ID Application" identity in your keychain. Murasaki signs the main app and bundled Node helper with separate entitlement sets: host/system rights stay on the app, while only Node receives JIT / unsigned- executable-memory / no-library-validation. App Sandbox is not currently supported;sign.appSandbox: trueis rejected rather than emitting an invalid inherited-helper signature. Override the hardened-runtime sets independently withconfig.sign.entitlementsandconfig.sign.helperEntitlements; missing or invalid files fail closed.--notarizerequires--sign(notarization only accepts Developer ID-signed code) and reads credentials fromAPPLE_ID,APPLE_TEAM_ID, andAPPLE_APP_PASSWORD(an app-specific password) — never from config or a file. It submits the.dmgto Apple's notary service, waits for the result, then staples the ticket so Gatekeeper can verify it offline.
Both require a paid Apple Developer Program membership.
Validate the macOS artifact
Run these against the final .app after signing and the final .dmg after
notarization:
codesign --verify --strict --verbose=2 "dist/bundle/My App.app"
codesign -dvvv --entitlements :- "dist/bundle/My App.app"
spctl -a -t open --context context:primary-signature -vv "dist/My App-1.0.0.dmg"
xcrun stapler validate "dist/My App-1.0.0.dmg"codesign --verify checks the app's code/resource seal. spctl evaluates the
distributed DMG against Gatekeeper policy. stapler validate checks that its
notarization ticket is attached. These answer different questions; run all of
them for a public release.
Windows installers and signing
murasaki bundle --target win32-x64 produces a portable folder and .zip.
murasaki installer additionally invokes tools already available on the build
host:
makensisproduces the NSIS.exeand may run on macOS or Windows.- WiX v4 produces the
.msiand runs on Windows.
For a hermetic CI toolchain, set MURASAKI_NSIS_PATH and/or
MURASAKI_WIX_PATH to the exact executable. An explicitly configured path is
authoritative: if it is missing or cannot run, that installer type is skipped
and the command fails when no other installer can be produced.
The NSIS installer can be perUser (the default, no elevation) or
perMachine; MSI is always per-machine. See
Configuration for branding and
upgrade identity.
Run the signed release on Windows (cross-building unsigned Windows artifacts still works elsewhere):
pnpm exec murasaki bundle --target win32-x64 --sign
pnpm exec murasaki installer --target win32-x64 --signThe bundle command signs <productName>.exe before creating the portable ZIP.
The installer command reuses that signed payload, then signs the generated NSIS
setup and MSI. Every signing operation uses SHA-256, an RFC 3161 timestamp by
default, and a separate signtool verify /pa /v /tw pass. A signing or
verification failure stops the release.
An unsigned installer is useful for local packaging checks, but current Windows application-control policy can block it even when the files are otherwise well formed. Murasaki warns in that case. Sign public artifacts; do not ask users to disable SmartScreen or Smart App Control.
Choose one signer. Config values can be overridden in CI without editing the checkout:
| Signer | Config | Environment override |
|---|---|---|
| PFX/P12 | sign.windows.certificateFile | MURASAKI_WINDOWS_CERTIFICATE_FILE + optional MURASAKI_WINDOWS_CERTIFICATE_PASSWORD |
| Imported certificate by subject | sign.windows.certificateSubjectName | MURASAKI_WINDOWS_CERTIFICATE_SUBJECT |
| Imported certificate by thumbprint | sign.windows.certificateSha1 | MURASAKI_WINDOWS_CERTIFICATE_SHA1 |
| Microsoft Artifact Signing | sign.windows.artifactSigning.{dlib,metadata} | MURASAKI_WINDOWS_ARTIFACT_SIGNING_DLIB + MURASAKI_WINDOWS_ARTIFACT_SIGNING_METADATA |
With none of those selectors, SignTool /a chooses the best code-signing
certificate from CurrentUser/My. Set certificateStore: 'localMachine' for
the machine store. Override the SignTool executable with
MURASAKI_SIGNTOOL_PATH, and the timestamp service with
MURASAKI_WINDOWS_TIMESTAMP_URL (false disables it, which is not recommended
for public releases).
For Microsoft Artifact Signing, dlib points to
Azure.CodeSigning.Dlib.dll and metadata to the non-secret account/profile
JSON. Authentication stays in Azure CLI, workload identity, or managed
identity; do not put credentials in that JSON or Murasaki config. Murasaki uses
Microsoft's Artifact Signing timestamp authority by default for this provider.
Do not treat the updater's Ed25519 manifest signature as an Authenticode replacement: it protects Murasaki's update channel, not Windows SmartScreen's publisher identity. A correctly signed new publisher can still see SmartScreen prompts while reputation develops.
Windows signed releases with GitHub Actions
This minimal PFX example writes the certificate only into the runner temp directory. Prefer an imported store certificate or cloud/HSM provider when that matches your certificate policy:
jobs:
windows:
runs-on: windows-2025
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
with:
node-version: 24
- run: pnpm install --frozen-lockfile
- name: Materialize signing certificate
shell: pwsh
env:
CERTIFICATE_BASE64: ${{ secrets.WINDOWS_CERTIFICATE_PFX }}
run: |
[IO.File]::WriteAllBytes(
"$env:RUNNER_TEMP\release.pfx",
[Convert]::FromBase64String($env:CERTIFICATE_BASE64)
)
- name: Build signed installers
env:
MURASAKI_WINDOWS_CERTIFICATE_FILE: ${{ runner.temp }}\release.pfx
MURASAKI_WINDOWS_CERTIFICATE_PASSWORD: ${{ secrets.WINDOWS_CERTIFICATE_PASSWORD }}
run: pnpm exec murasaki installer --target win32-x64 --signmacOS signed releases with GitHub Actions
Build + (optionally) sign + notarize a .dmg on tag push and attach it to a
GitHub Release. Add this as .github/workflows/release.yml in your app:
name: Release
on:
push:
tags: ['v*']
jobs:
release:
runs-on: macos-14
permissions:
contents: write
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
with:
node-version: 24
- run: pnpm install
- name: Import signing certificate
if: ${{ secrets.APPLE_CERTIFICATE_P12 != '' }}
env:
CERT_P12: ${{ secrets.APPLE_CERTIFICATE_P12 }}
CERT_PW: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
run: |
KC="$RUNNER_TEMP/app.keychain-db"
security create-keychain -p "" "$KC"
security set-keychain-settings -lut 21600 "$KC"
security unlock-keychain -p "" "$KC"
echo "$CERT_P12" | base64 --decode > "$RUNNER_TEMP/cert.p12"
security import "$RUNNER_TEMP/cert.p12" -k "$KC" -P "$CERT_PW" -T /usr/bin/codesign
security set-key-partition-list -S apple-tool:,apple: -s -k "" "$KC"
security list-keychains -d user -s "$KC" $(security list-keychains -d user | tr -d '"')
- name: Build installer
env:
APPLE_ID: ${{ secrets.APPLE_ID }}
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
APPLE_APP_PASSWORD: ${{ secrets.APPLE_APP_PASSWORD }}
HAS_CERT: ${{ secrets.APPLE_CERTIFICATE_P12 != '' }}
run: |
if [ "$HAS_CERT" = "true" ]; then
pnpm exec murasaki installer --sign --notarize
else
pnpm exec murasaki installer
fi
- uses: softprops/action-gh-release@v2
with:
files: dist/*.dmgAdd these repository secrets to sign + notarize (omit them all for an unsigned
.dmg): APPLE_CERTIFICATE_P12 (base64 of your Developer ID .p12),
APPLE_CERTIFICATE_PASSWORD, APPLE_ID, APPLE_TEAM_ID, APPLE_APP_PASSWORD.
Never commit these secrets to config or source — --notarize deliberately
only reads them from the environment.
Linux: AppImage and .deb
The native launcher runs the produced AppDir/.AppImage/.deb: window,
webview, single-instance locking, deep links, graceful shutdown, and
crash reporting all work the same way they do on macOS/Windows.
murasaki installer --sign GPG-signs the artifacts — see
Signing: GPG detached signatures
below. .rpm and repository metadata are not implemented yet — see
"What's not implemented yet" below.
murasaki bundle --target linux-x64 # dist/bundle/<Name>.AppDir/ + <Name>-<version>-linux-x64.AppImage
murasaki bundle --target linux-arm64
murasaki installer --target linux-x64 # dist/<debname>_<version>_amd64.deb
murasaki installer --target linux-arm64 # dist/<debname>_<version>_arm64.debBoth cross-build from macOS/Windows/CI the same way bundle --target win32-x64 already does — no Linux host required. bundle needs mksquashfs
on PATH to build the .AppImage (the AppDir folder itself has no extra
requirement):
brew install squashfs # macOS
apt install squashfs-tools # Debian/Ubuntu
dnf install squashfs-tools # Fedorainstaller's .deb is a pure-Node ar/tar writer — it needs no dpkg-deb
and no extra host tool.
The AppDir mirrors the macOS .app's Contents/Resources layout under
usr/lib/<appId>/resources/ (client build, server actions, a downloaded
Node runtime, murasaki-meta.json, …), plus the freedesktop.org pieces every
Linux desktop expects: AppRun, a root and usr/share/applications/ <appId>.desktop, and a usr/share/icons/hicolor/ icon-theme fan-out
generated from config.icon (16 through 512px).
Running an AppImage without FUSE
AppImages normally mount themselves via FUSE. Some hosts — notably CI
runners and minimal containers — don't reliably have a working
/dev/fuse. Install libfuse2 (libfuse2t64 on Ubuntu 24.04+) for the
normal FUSE-mount path, or pass --appimage-extract-and-run to have the
AppImage runtime extract itself to a temp directory and run from there
instead — no FUSE required, at the cost of a slower startup. Murasaki's own
CI (.github/workflows/app-package-linux.yml) always uses
--appimage-extract-and-run for exactly this reason.
AppImage vs .deb: different update stories
.AppImageis murasaki's self-contained update payload and self-update target:useUpdate()'sinstall()journal-swaps the running.AppImagefile in place (same same-volume-backup + startup-health-acknowledgement guarantees as the macOS.app.zip/Windows NSIS setup swap), then relaunches it with--appimage-extract-and-run(works whether or not the host has FUSE). A failed first launch after an update automatically rolls back to the previous version.murasaki release --manifestscans for<Name>-<version>-linux-{x64,arm64}.AppImage, see Auto-update..debis package-manager-owned. It's never an update payload — upgrades are apt/dpkg's job, the same way an OS package manager owns any other installed package's lifecycle.murasaki release --manifestnever looks for a.deb, and a.deb-installed (or manually-extracted, bare AppDir) app'suseUpdate().check()reports a structured "updates are managed by the system package manager" result instead of checking a manifest — never an error.
Signing: GPG detached signatures
Murasaki owns the signing orchestration here too — it never generates or
stores a GPG key for you. Supply an existing key; --sign fails closed
instead of silently shipping unsigned artifacts.
pnpm exec murasaki installer --target linux-x64 --sign--sign produces a detached, ASCII-armored GPG signature (<artifact>.sig)
for the .AppImage, for the .deb, and for a combined SHA256SUMS file that
lists both artifacts' checksums, so a recipient can verify either the
per-file signature or the checksum manifest. When dpkg-sig is on PATH,
Murasaki also opportunistically embeds a Debian-native signature directly
into the .deb; this is best-effort and does not block --sign if
dpkg-sig is unavailable.
The signing key resolves from $MURASAKI_GPG_KEY (a key ID, fingerprint, or
email known to your local gpg keyring), then sign.linux.gpgKey in
murasaki.config.ts. The passphrase comes only from
$MURASAKI_GPG_PASSPHRASE or an already-unlocked gpg-agent — never from
config or a file.
Recipients verify with their own copy of gpg:
gpg --verify MyApp-1.0.0-linux-x64.AppImage.sig MyApp-1.0.0-linux-x64.AppImage
gpg --verify SHA256SUMS.sig SHA256SUMSThis proves the artifact matches what your key signed; it does not by itself establish that your key is trustworthy — recipients still need your public key out-of-band (a keyserver, your website, a release note) to make that judgment call themselves.
What's not implemented yet
- No
.rpmor repository metadata (apt/dnf repo indexes). - No apt/dnf keyring or distro-repository trust integration for the GPG signature above — it proves artifact integrity, not repository trust.
- No self-update for the
.debor a bare/manually-extracted AppDir — only the.AppImagehas a file to swap (see above).
Release checklist
- Build from a clean checkout with a locked dependency graph.
- Launch the bundle before wrapping it in an installer.
- Install, update, and uninstall on clean target machines for every platform/architecture you publish.
- Test configured URL schemes and file associations for cold start and while the primary app instance is already running.
- Exercise Node Main shutdown, offline startup, and failed network requests.
- Verify macOS signatures/notarization, Windows Authenticode signatures, and Linux GPG signatures.
- Generate and sign the Murasaki update manifest only after final payloads are immutable.
- Publish payloads,
latest.json, andlatest.json.sigtogether. - Keep signing credentials and
MURASAKI_UPDATE_KEYin CI secrets; never inmurasaki.config.ts.
Code-signing and update signing are independent. See Security and Auto-update before enabling in-app updates.