Build configuration, performance tuning, signing, and distribution for mt.
| Platform | Architecture | Runner | Bundle |
|---|---|---|---|
| macOS | ARM64 | Self-hosted [macOS, ARM64] |
.app, .dmg |
| Linux | amd64 | ubuntu-latest (CI) or Docker (Dockerfile.linux-amd64) |
.deb |
| Linux | arm64 | Docker (Dockerfile.linux-arm64) |
.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:build:linux-arm64 |
Build Linux arm64 .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.aarch64-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-backendmt 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.
Both Linux architectures can be built locally via Docker, which is useful for producing .deb packages from a macOS development machine.
| Architecture | Task | Dockerfile | Notes |
|---|---|---|---|
| arm64 | task build:linux-arm64 |
docker/Dockerfile.linux-arm64 |
Native on Apple Silicon |
| amd64 | task build:linux-amd64 |
docker/Dockerfile.linux-amd64 |
QEMU emulation on Apple Silicon |
The arm64 build runs natively on Apple Silicon with no emulation overhead. The amd64 build uses --platform linux/amd64 which triggers QEMU emulation β functional but slower.
Artifacts are written to dist/linux-{arm64,amd64}/.
# Build amd64 .deb
task build:linux-amd64
# Build arm64 .deb
task build:linux-arm64
# Copy to target machine
scp dist/linux-amd64/*.deb zima:~/Downloads/
scp dist/linux-arm64/*.deb rpi:~/Downloads/
# Debug shell (inspect build environment)
task build:linux-amd64:shell
task build:linux-arm64:shellWindows 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 ubuntu-latest:
- Sets up the Tauri build environment via the shared composite action
- Builds with
tauri-actiontargetingx86_64-unknown-linux-gnu --bundles deb - Attaches the
.debto the same draft GitHub Release
Runs on a self-hosted [self-hosted, Windows, X64] runner:
- Sets up the Tauri build environment (Chocolatey installs cmake and rustup;
RUSTUP_TOOLCHAIN=nightly-2026-02-09is exported toGITHUB_ENVand~/.cargo/binis prepended toGITHUB_PATHto ensure the nightly toolchain takes precedence) - 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)
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 platforms like Raspberry Pi (Linux ARM64).
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