Build configuration, performance tuning, signing, and distribution for mt.
| Platform | Architecture | Runner | Bundle |
|---|---|---|---|
| macOS | ARM64 | Self-hosted [macOS, ARM64] |
.app, .dmg |
| Linux | amd64 | Blacksmith blacksmith-4vcpu-ubuntu-2404 (configurable, see CI Runner Policy) |
.deb |
| Windows | x64 | Self-hosted [self-hosted, Windows, X64] |
.exe (NSIS) |
All task tauri:* commands default to nightly with parallel codegen and sccache
(RUSTUP_TOOLCHAIN=nightly, RUSTFLAGS="-Zthreads=16", RUSTC_WRAPPER=sccache).
| Task | Description |
|---|---|
task tauri:dev |
Run development server |
task tauri:dev:mcp |
Dev server with MCP bridge for AI agent debugging |
task tauri:build |
Build for current platform (auto-detects {{OS}}/{{ARCH}}) |
task tauri:build:arm64 |
Build for Apple Silicon (macOS only) |
task tauri:build:x64 |
Build for Intel x86_64 (macOS only) |
task tauri:build:signed |
Build signed + notarized .app and .dmg (macOS) |
task tauri:build:dmg |
Build signed + notarized .dmg only (macOS) |
task tauri:icons |
Generate app icons from static/logo.png |
task tauri:build:windows |
Build Windows x64 NSIS .exe installer |
task tauri:build:linux-amd64 |
Build Linux amd64 .deb via Docker |
task tauri:clean |
Clean all build artifacts |
task tauri:clean:rust |
Clean only Rust build artifacts |
task tauri:doctor |
Run Tauri environment check |
Override the nightly default if needed:
# Single command
RUSTUP_TOOLCHAIN=stable RUSTFLAGS="" task tauri:dev
# Or export in shell
export RUSTUP_TOOLCHAIN=stable
export RUSTFLAGS=""
task tauri:devrustup update nightlyIf a nightly update breaks the build, pin to a specific date:
rustup install nightly-2026-01-27
# Then update taskfiles/tauri.yml:
# RUSTUP_TOOLCHAIN: nightly-2026-01-27All Tauri build tasks use sccache as the Rust compiler wrapper (RUSTC_WRAPPER=sccache). sccache caches compiled crate artifacts globally, so new worktrees and clean builds reuse previously compiled objects.
# Check cache hit rates
sccache --show-stats
# Clear the cache (if needed)
sccache --zero-statsFirst build populates the cache; subsequent workspaces benefit from cache hits on unchanged crates. This is especially useful with Conductor workspaces where each workspace starts with an empty target/ directory.
[profile.dev]
split-debuginfo = "unpacked" # macOS: faster incremental debug builds
debug = "line-tables-only" # Reduced debug info, still get line numbers in backtraces
[profile.dev.build-override]
opt-level = 3 # Optimize proc-macros and build scriptsFor full debugging (variable inspection in debuggers), temporarily change to:
[profile.dev]
debug = true # or debug = 2 for maximum infoPer-target linker configuration:
[target.aarch64-apple-darwin]
rustflags = ["-C", "link-arg=-fuse-ld=lld"]
[target.x86_64-apple-darwin]
rustflags = ["-C", "link-arg=-fuse-ld=lld"]
[target.x86_64-unknown-linux-gnu]
rustflags = ["-C", "link-arg=-fuse-ld=mold"]
[target.x86_64-pc-windows-msvc]
rustflags = ["-C", "link-arg=-fuse-ld=lld"]| Linker | Status | Notes |
|---|---|---|
| lld | Recommended | Currently configured, fast and stable |
| ld-prime | Alternative | Apple's default, similar performance |
| sold | Avoid | Fastest but has codesign issues |
| Linker | Status | Notes |
|---|---|---|
| mold | Recommended | Currently configured, fastest option |
| lld | Alternative | Good fallback |
| Linker | Status | Notes |
|---|---|---|
| rust-lld | Recommended | Fast for full builds |
| link.exe | Alternative | Better for tiny incrementals |
The -Zthreads=N flag enables parallel codegen, which significantly improves incremental build times. This is a nightly-only feature.
Measured on Apple M4 Max, macOS 15.7.1:
| Scenario | Time | Notes |
|---|---|---|
| Cold build | ~50.2s | Full rebuild from clean |
| Incremental build | ~1.06s | After touching src/main.rs |
| Scenario | Time | Improvement | Notes |
|---|---|---|---|
| Cold build | ~50.1s | -0.3% | Negligible difference |
| Incremental build | ~0.82s | -23% | Significant improvement |
Key finding: Nightly with -Zthreads=16 provides 23% faster incremental builds with 50x better variance (Ο=0.012s vs Ο=0.588s), while maintaining full test compatibility (596 Rust tests pass).
Use hyperfine for accurate measurements:
# Cold build (3 runs with cargo clean before each)
hyperfine --runs 3 --prepare 'cargo clean' \
'cargo build -p mt-tauri'
# Incremental build (5 runs, 1 warmup)
hyperfine --warmup 1 --runs 5 --prepare 'touch crates/mt-tauri/src/main.rs' \
'cargo build -p mt-tauri'
# Build timing breakdown (HTML report)
cargo build -p mt-tauri --timings
# Output: target/cargo-timings/cargo-timing.html# Cold build comparison
hyperfine --runs 3 --prepare 'cargo clean' \
'cargo build -p mt-tauri' \
'RUSTUP_TOOLCHAIN=nightly RUSTFLAGS="-Zthreads=16" cargo build -p mt-tauri'
# Incremental build comparison
hyperfine --warmup 1 --runs 5 --prepare 'touch crates/mt-tauri/src/main.rs' \
'cargo build -p mt-tauri' \
'RUSTUP_TOOLCHAIN=nightly RUSTFLAGS="-Zthreads=16" cargo build -p mt-tauri'Before benchmarking, verify:
rustc -Vv # Stable version
RUSTUP_TOOLCHAIN=nightly rustc -Vv # Nightly version
env | grep RUSTFLAGS # Check for conflicting flags
env | grep RUSTC_WRAPPER # Should show sccache (disable with `unset RUSTC_WRAPPER` for raw benchmarks)Ensure consistent power state (AC power, low power mode off).
Cranelift is an experimental codegen backend for Rust that can dramatically improve debug build times. However, it is not compatible with mt due to SIMD limitations.
Tested on 2026-01-28 with nightly-2026-01-27. Build fails with:
llvm.aarch64.neon.sqdmulh.v2i32 is not yet supported.
See https://github.com/rust-lang/rustc_codegen_cranelift/issues/171
This error occurs in multiple Tauri plugin build scripts that use SIMD intrinsics (tauri-plugin-fs, tauri-plugin-store, tauri-plugin-shell, tauri-plugin-opener, tauri-plugin-global-shortcut).
Per rustc_codegen_cranelift#171:
std::simdis fully supportedstd::arch(platform-specific SIMD intrinsics) is only partially supported- ARM NEON intrinsics like
sqdmulhare not yet implemented
Stick with nightly + -Zthreads=16 for now. When Cranelift SIMD support matures (or if Tauri plugins stop using raw NEON intrinsics), reconsider.
# Testing Cranelift (if revisiting)
rustup component add rustc-codegen-cranelift-preview --toolchain nightly
RUSTUP_TOOLCHAIN=nightly CARGO_PROFILE_DEV_CODEGEN_BACKEND=cranelift \
cargo build -p mt-tauri -Zcodegen-backendBy default, rust-analyzer runs continuous background diagnostics and cargo check on every save. On a large workspace this can saturate the CPU even when you haven't changed anything.
| Setting | Default | Description |
|---|---|---|
rust-analyzer.checkOnSave |
true |
Run cargo check when a file is saved. Keep enabled for on-save feedback. |
rust-analyzer.diagnostics.enable |
true |
Continuous background analysis (type errors, borrow-check squiggles). Disable to stop builds when nothing has been saved. |
rust-analyzer.procMacro.enable |
true |
Expand procedural macros in the background. Disable to reduce CPU at the cost of missing diagnostics in macro-heavy code. |
Background diagnostics are disabled. checkOnSave remains at its default (true), so cargo check still runs on each manual save. If you want inline squiggles back but with less background work, swap to disabling only procMacro.enable instead.
mt is distributed as a direct download (not via the Mac App Store). This requires:
- Code signing with a Developer ID Application certificate
- Notarization via Apple's notary service (scans for malware, issues a trust ticket)
- Stapling the notarization ticket to the app bundle
Without all three, macOS Gatekeeper blocks the app on users' machines.
- Apple Developer Program membership
- Developer ID Application certificate (created in Xcode or Apple Developer portal)
- App Store Connect API key (for notarization)
All signing secrets are stored in .env (loaded via Taskfile dotenv). See .env.example for the template.
| Variable | Purpose |
|---|---|
APPLE_SIGNING_IDENTITY |
Full signing identity string, e.g. Developer ID Application: Name (TEAMID) |
APPLE_CERTIFICATE |
Base64-encoded .p12 certificate export |
APPLE_CERTIFICATE_PASSWORD |
Password set during .p12 export |
APPLE_API_KEY |
App Store Connect API key ID (10-char alphanumeric) |
APPLE_API_ISSUER |
App Store Connect API issuer UUID |
APPLE_API_KEY_B64 |
Base64-encoded .p8 private key content |
KEYCHAIN_PASSWORD |
CI-only: password for the temporary signing keychain |
crates/mt-tauri/Entitlements.plist declares hardened runtime entitlements:
| Entitlement | Reason |
|---|---|
com.apple.security.cs.allow-jit |
WebView/JS engine |
com.apple.security.cs.allow-unsigned-executable-memory |
WebView/JS engine |
com.apple.security.cs.allow-dyld-environment-variables |
Bundled dylibs |
com.apple.security.network.client |
Last.fm API calls |
com.apple.security.files.user-selected.read-write |
User-selected music directories |
The app is not sandboxed β a music player needs broad filesystem access for library scanning.
# Ensure .env is populated with signing secrets
task tauri:build:signedThis will:
- Decode the base64 API key to
/tmp/auth_key.p8 - Build the Tauri app for
aarch64-apple-darwin - Sign with the Developer ID certificate
- Submit to Apple's notary service and wait for approval
- Staple the notarization ticket
- Build the DMG installer
# Verify code signature
codesign --verify --deep --strict \
target/aarch64-apple-darwin/release/bundle/macos/mt.app
# Verify Gatekeeper acceptance (requires notarization)
spctl --assess --type execute --verbose \
target/aarch64-apple-darwin/release/bundle/macos/mt.app
# Inspect applied entitlements
codesign -d --entitlements - \
target/aarch64-apple-darwin/release/bundle/macos/mt.appCreating a new certificate:
- Open Xcode > Settings > Accounts > Manage Certificates
- Click
+> Developer ID Application - Export as
.p12from Keychain Access
Encoding for .env:
# Certificate (.p12 -> base64)
openssl base64 -A -in cert.p12 | pbcopy
# API key (.p8 -> base64)
openssl base64 -A -in AuthKey_XXXXXXXXXX.p8 | pbcopyFinding your signing identity:
security find-identity -v -p codesigningcrates/mt-tauri/tauri.conf.json macOS bundle config:
"macOS": {
"minimumSystemVersion": "10.15",
"entitlements": "./Entitlements.plist",
"dmg": {
"windowSize": { "width": 660, "height": 400 },
"appPosition": { "x": 180, "y": 170 },
"applicationFolderPosition": { "x": 480, "y": 170 }
}
}The signing identity is not hardcoded in config β Tauri reads APPLE_SIGNING_IDENTITY from the environment, so unsigned dev builds still work.
Linux builds produce .deb packages. The bundle target and dependencies are configured in crates/mt-tauri/tauri.conf.json:
"bundle": {
"targets": ["app", "dmg", "deb"],
"linux": {
"deb": {
"depends": ["libwebkit2gtk-4.1-0", "libayatana-appindicator3-1", "libgtk-3-0"],
"section": "sound"
}
}
}Build locally:
task tauri:buildtask tauri:build auto-detects the current platform via {{OS}}/{{ARCH}} and selects the correct Rust target triple.
Linux amd64 builds use a multi-stage Dockerfile (docker/Dockerfile.linux-amd64) for both CI and local builds. The Dockerfile uses cargo-chef to cache compiled dependencies separately from application code.
Stages:
| Stage | Purpose | Target |
|---|---|---|
deps |
System packages, Rust nightly, Node.js, cargo-chef, tauri-cli | (base) |
planner |
cargo chef prepare β generates recipe.json from manifests |
(internal) |
cook |
cargo chef cook β compiles all dependencies (cached layer) |
(internal) |
check |
cargo check --workspace |
--target check (test workflow) |
build |
cargo tauri build + cargo tauri bundle |
(internal) |
artifacts |
Extract .deb and binary |
--target artifacts (release workflow) |
CI usage:
In CI, Blacksmith Docker layer caching persists all layers on NVMe-backed sticky disks. After the first run, the deps and cook layers are cached β subsequent runs only rebuild changed source code.
# test.yml β cargo check only
- uses: useblacksmith/setup-docker-builder@v1
- uses: useblacksmith/build-push-action@v2
with:
target: check
# release.yml β full build + bundle, extract artifacts
- uses: useblacksmith/setup-docker-builder@v1
- uses: useblacksmith/build-push-action@v2
with:
target: artifacts
outputs: type=local,dest=distLocal usage:
# Build .deb via task (QEMU emulation on Apple Silicon)
task build:linux-amd64
# Or build directly with Docker
docker build --platform linux/amd64 --target check -f docker/Dockerfile.linux-amd64 .
docker build --platform linux/amd64 --target artifacts --output type=local,dest=dist -f docker/Dockerfile.linux-amd64 .
# Copy to target machine
scp dist/*.deb zima:~/Downloads/Windows builds produce NSIS .exe installers with self-signed code signing.
- Visual Studio Build Tools (MSVC v143+, Windows 11 SDK)
- WebView2 runtime (pre-installed on Windows 10 1803+ and Windows 11)
- Chocolatey (for CI dependency management)
crates/mt-tauri/tauri.conf.json includes nsis in bundle targets with per-user + per-machine install support:
"windows": {
"nsis": {
"installMode": "both"
}
}CI uses a self-signed certificate generated at build time via New-SelfSignedCertificate. This prevents Windows Defender real-time protection false positives but does not eliminate SmartScreen "unrecognized app" warnings (that requires an EV certificate with download reputation).
The signing flow:
- Install Windows SDK (provides
signtool.exe) - Generate a
CodeSigningCertwith subjectCN=MT - Export to PFX with password from
WINDOWS_CERT_PASSWORDsecret - Tauri calls
signtool.exeviabundle.windows.signCommandusing structured{ cmd, args }format to handle spaces in the signtool path (passed as a--configoverride that also disablesbeforeBuildCommand) - Both the application binary and NSIS installer are signed
- DigiCert timestamp server ensures signatures remain valid after cert expiry
| Variable | Purpose |
|---|---|
WINDOWS_CERT_PASSWORD |
Password for the self-signed PFX certificate (GitHub secret) |
Use scripts/build.ps1 from a native PowerShell terminal (not git bash β the Cert:\ drive and certificate cmdlets require the PowerShell Security module, which fails to auto-load under git bash):
# Full build with self-signed code signing (recommended)
.\scripts\build.ps1
# Skip dependency installation (faster on subsequent runs)
.\scripts\build.ps1 -SkipDeps
# Build without code signing
.\scripts\build.ps1 -SkipSign
# Clean Rust artifacts before building
.\scripts\build.ps1 -Clean
# Provide your own certificate password
.\scripts\build.ps1 -CertPassword 'my-secret-password'The script replicates the CI pipeline locally:
- Installs prerequisites via Chocolatey (cmake, rustup, node, go-task, MSVC build tools) β skipped if already present
- Configures the nightly Rust toolchain and
x86_64-pc-windows-msvctarget - Builds the frontend (
npm ci+npm run build) β skipsnpm ciwhennode_modulesis already up to date - Generates a self-signed
CodeSigningCertand exports it to a temporary PFX - Writes a
sign.cmdbatch wrapper that invokessigntool.exe signwith the PFX - Calls
npx @tauri-apps/cli build --bundles nsiswith a JSON config override that setsbundle.windows.signCommandtocmd /C sign.cmd %1(.cmdfiles requirecmd.exeto execute β they cannot be launched directly viaCreateProcess) - Cleans up the certificate and temp files
Output is written to target/x86_64-pc-windows-msvc/release/bundle/nsis/.
Note: Signing with a self-signed certificate prevents Windows Defender false positives but does not eliminate SmartScreen "unrecognized publisher" warnings. That requires an EV certificate with established download reputation.
The release pipeline (.github/workflows/release.yml) runs on version tags (v*) and manual workflow_dispatch.
Three parallel jobs:
Runs on a self-hosted [macOS, ARM64] runner:
- Imports the certificate into a temporary CI keychain
- Decodes the
.p8API key fromAPPLE_API_KEY_B64secret - Builds with
tauri-actionwhich handles signing + notarization - Creates a draft GitHub Release with the signed
.dmg - Cleans up the keychain and key file (runs in
always()step)
Runs on a configurable Blacksmith runner (default: blacksmith-4vcpu-ubuntu-2404, selectable via linux-runner workflow input). Uses a containerized build via docker/Dockerfile.linux-amd64 with Blacksmith Docker layer caching:
- Sets up the Blacksmith Docker builder (
useblacksmith/setup-docker-builder) - Builds via
useblacksmith/build-push-actiontargeting theartifactsstage - Extracts
.deband binary from the container todist/ - Attaches the
.debto the same draft GitHub Release
The test workflow (test.yml) uses the same Dockerfile but targets the check stage (cargo check only).
Runs on a self-hosted [self-hosted, Windows, X64] runner:
- Sets up the Tauri build environment (Chocolatey installs cmake and rustup;
~/.cargo/binis prepended toGITHUB_PATH;RUSTUP_TOOLCHAINis set to the fully qualifiednightly-2026-02-09-x86_64-pc-windows-msvcto ensure the MSVC-hosted toolchain is used β see Windows Toolchain Pinning below) - Installs Windows SDK for
signtool.exe - Generates a self-signed
CodeSigningCertand exports to PFX - Builds the frontend explicitly (
npm run buildinapp/frontend/) - Writes a config override that disables
beforeBuildCommand(frontend already built) and setsbundle.windows.signCommandusing structured{ cmd, args }format (handles spaces insigntool.exepath) - Builds with
tauri-actionwhich calls the sign command for both the binary and NSIS installer - Attaches the signed
.exeto the same draft GitHub Release - Cleans up the certificate and config override (runs in
always()step)
Runner assignment is optimized for developer iteration speed (PR/push), not release throughput.
| Job | Runner | Rationale |
|---|---|---|
rust (lint/test) |
[macOS, ARM64] |
Self-hosted, no queue contention |
build(macos) |
[macOS, ARM64] |
Eligible on all ARM64 self-hosted runners |
build(linux) |
Blacksmith (configurable) | Containerized via Docker, deps cached by Blacksmith sticky disks |
build(windows) |
[self-hosted, Windows, X64] |
Self-hosted, registry-only cargo cache |
deno-lint |
blacksmith-4vcpu-ubuntu-2404 |
Lightweight, vanilla runner |
vitest-tests |
blacksmith-4vcpu-ubuntu-2404 |
Lightweight, vanilla runner |
playwright-tests |
[macOS, ARM64] |
Needs WebKit, self-hosted |
Linux runner toggle: Both test.yml and release.yml accept a linux-runner workflow_dispatch input. Default is blacksmith-4vcpu-ubuntu-2404. To test a different runner label, trigger manually and select from the dropdown. No file edits required for rollback.
macOS runner broadening: macOS jobs use [macOS, ARM64] (no studio pin) so they can schedule on any eligible self-hosted ARM64 runner.
Lightweight jobs stay on vanilla runners: deno-lint and vitest-tests remain on standard Blacksmith runners. Migration to custom images is deferred until a measured benefit is demonstrated.
The self-hosted Windows runner can accumulate stale rustup state across runs. In particular, a system-level settings file (C:\Windows\system32\config\systemprofile\.rustup\settings.toml) may set default_host_triple to x86_64-pc-windows-gnu. When RUSTUP_TOOLCHAIN is set to a bare channel name like nightly-2026-02-09, rustup resolves it using the default host triple β producing the GNU-hosted toolchain, which requires dlltool.exe (not present on MSVC-only runners) and fails with:
error: error calling dlltool 'dlltool.exe': program not found
To prevent this, the CI configuration uses three layers of defense:
-
Fully qualified
RUSTUP_TOOLCHAIN(taskfiles/ci.yml): On Windows, the env var is set tonightly-2026-02-09-x86_64-pc-windows-msvc(with the host triple suffix), eliminating any ambiguity in toolchain resolution. -
rustup set default-host x86_64-pc-windows-msvc(.github/actions/setup-tauri-build/action.ymlandtaskfiles/ci.yml): Run before toolchain installation to override any stale GNU default. -
Explicit
PINNED_RUSTpassthrough (.github/actions/setup-tauri-build/action.yml): The composite action extracts the Rust version from.tool-versionsusing PowerShell and passes it totask ci:setup PINNED_RUST=..., bypassing the Taskfile'ssh: grep | awkwhich may fail when Git Bash utilities aren't in PATH.
Tauri on Ubuntu/Debian requires these packages:
sudo apt install -y \
libwebkit2gtk-4.1-dev \
libayatana-appindicator3-dev \
libgtk-3-dev \
libssl-dev \
pkg-config \
cmake \
build-essential \
moldThe mold linker is configured in .cargo/config.toml for Linux targets. Install it or switch to lld if unavailable.
The packages above are build-time dependencies (headers, dev libraries, compilers). The .deb package also declares runtime dependencies in crates/mt-tauri/tauri.conf.json under bundle.linux.deb.depends β these are pulled in automatically when installing the .deb via dpkg or apt.
Debian 13 (trixie), Raspberry Pi OS (bookworm+), and most modern distros use PipeWire as the default audio stack. Applications that output audio via ALSA (like mt, which uses Rodio/Symphonia β ALSA) need pipewire-alsa to route audio through PipeWire.
Without pipewire-alsa installed, ALSA cannot find any usable PCM device and logs errors like:
ALSA lib conf.c:XXX:parse_def Unknown PCM pipewire
ALSA lib conf.c:XXX:parse_def Unknown PCM pulse
ALSA lib conf.c:XXX:parse_def Unknown PCM jack
ALSA lib conf.c:XXX:parse_def Unknown PCM oss
The app launches but produces no audio output.
Fix: pipewire-alsa is declared in the .deb depends, so installing the package resolves this automatically:
sudo apt install ./mt_*.deb # pulls in pipewire-alsaFor manual installs or non-deb distributions:
# PipeWire-based systems (Debian 13+, Fedora, Arch, etc.)
sudo apt install pipewire-alsa
# PulseAudio-based systems (older Ubuntu/Debian)
sudo apt install libasound2-pluginsThe app includes several runtime memory optimizations, particularly important on resource-constrained Linux platforms.
The library store's _sectionCache stores only summary metadata (track count, total duration, timestamp) β never full track arrays. Section switching fetches tracks from the local SQLite backend. This prevents duplicate multi-MB track arrays from accumulating in the WebView's JS heap.
- File:
app/frontend/js/stores/library.js - Impact: ~200-400 MB reduction with large libraries
WebKitGTK spawns multiple processes and threads, each of which can create a glibc malloc arena (~64 MB virtual per arena). Two environment variables are set at Rust startup (before any threads spawn) and inherited by WebKit child processes:
#[cfg(target_os = "linux")]
unsafe {
std::env::set_var("MALLOC_ARENA_MAX", "2");
std::env::set_var("MALLOC_TRIM_THRESHOLD_", "131072");
}- File:
crates/mt-tauri/src/lib.rs - Impact: ~50-100 MB RSS reduction on Linux
The global rayon thread pool is capped at 4 threads with 2 MB stacks (down from per-core threads with 8 MB stacks). Music scanning only needs a few parallel workers.
- File:
crates/mt-tauri/src/lib.rs - Impact: ~12 MB virtual reduction (cross-platform)
The r2d2 pool is sized for a desktop app workload: max_size(4), min_idle(1).
- File:
crates/mt-tauri/src/db/mod.rs
The LRU artwork cache (pure Rust, lru + parking_lot) is capped at 50 entries via ArtworkCache::with_capacity(50).
- File:
crates/mt-tauri/src/lib.rs