Chapter 11

Shipping

From a plugin that works on your machine to a signed installer users can double-click. Four commands get you there:

cargo truce build     # bundle formats into target/bundles/ without installing
cargo truce install   # build + install into your local plugin directories
cargo truce validate  # run auval + pluginval + clap-validator against installed bundles
cargo truce package   # produce a signed distributable installer (.pkg / .exe)

Per-format requirements (SDKs, env vars, signing specifics) live in docs/formats/; the install destinations table on the quick-start page lists the exact user / system path each format lands at. This chapter covers the cross-format cargo truce workflow and signing.

#Enabling formats

Scaffolded plugins ship CLAP + VST3 + standalone by default. Add more formats to [features].default in Cargo.toml, or pass them explicitly:

cargo truce install --vst2
cargo truce install --lv2
cargo truce install --au3             # AU v3, macOS only
cargo truce install --aax             # AAX, needs AAX SDK
cargo truce install --clap --vst3 --lv2   # explicit subset

Flags add to the active feature set for this one invocation — they don't modify Cargo.toml. To enable a format every time, add it to default:

[features]
default = ["clap", "vst3", "lv2"]   # CLAP + VST3 + LV2 every time

Per-format setup (Xcode required for AU v3, PACE wraptool for retail AAX, AAX_SDK_PATH, etc.) is in the per-format pages: clap · vst3 · vst2 · lv2 · au · aax.

#install

cargo truce install                    # every format in your default features
cargo truce install --clap             # just CLAP
cargo truce install --no-build         # install the existing bundles, skip rebuild
cargo truce install -p my-gain         # single plugin in a workspace (cargo crate name)
cargo truce install --user             # per-user paths (default — no sudo / admin)
cargo truce install --system           # system-wide paths (sudo on macOS, admin on Windows)

Builds, bundles, codesigns on macOS, and writes into the standard plugin directories on your machine. User-scope is the default on every platform — the dev loop doesn't prompt for a password.

  • macOS user (default): ~/Library/Audio/Plug-Ins/{CLAP,VST3, Components,VST,LV2}/. No sudo.
  • macOS system (--system): /Library/Audio/Plug-Ins/.... Prompts for sudo once per run.
  • Windows user (default): %LOCALAPPDATA%\Programs\Common\ {CLAP,VST3}\, %APPDATA%\LV2\. No Administrator shell needed.
  • Windows system (--system): %COMMONPROGRAMFILES%\.... Run from an Administrator prompt.
  • Linux: ~/.clap, ~/.vst3, ~/.vst, ~/.lv2. The Linux scope flags are accepted for symmetry with macOS / Windows but resolve to the same paths every host already scans.

AAX and AU v3 are always system-scope (Pro Tools / pluginkit only scan the system root); --user for those formats falls back to the system path with a one-line note: ... is system-only. Windows VST2 is also system-only on Windows. The install scope is a per-invocation developer choice — cargo truce install has no truce.toml override, only the CLI flag.

Full per-platform path table — bolded to mark the default destination per (OS, format) — is in the quick start's install destinations section.

cargo truce uninstall mirrors the same flags. By default it scans both scopes (handy when you switched scope mid-iteration); pass --user / --system to limit. cargo truce doctor prints both paths per format with a writable / sudo / not-present marker, and cargo truce validate warns when the same plugin name is installed in both scopes (hosts pick one and shadow the other).

#build

Same bundle layout as install, but written to target/bundles/<Plugin>.{clap,vst3,...} instead of the system plugin directories:

cargo truce build                    # every default format
cargo truce build --clap --vst3      # subset
cargo truce build --au3              # AU v3 .app, fully signed
cargo truce build --aax              # AAX .aaxplugin, fully signed
cargo truce build --shell                   # hot-reload shell build

Every format flag produces a complete, signed bundle in target/bundles/.

#validate

Runs the free validators against your installed bundles:

cargo truce validate                 # every available validator, permissive
cargo truce validate --all           # same as no flag
cargo truce validate --clap          # CLAP via clap-validator
cargo truce validate --pluginval     # VST3 via pluginval
cargo truce validate --auval         # AU v2 via auval (macOS)
cargo truce validate --auval3        # AU v3 via auval (macOS)
cargo truce validate --vst2          # VST2 dlopen + AEffect smoke (macOS)
cargo truce validate --clap --pluginval -p my-gain   # subset, single plugin
  • clap-validator (https://github.com/free-audio/clap-validator) exercises CLAP lifecycle, parameters, state, and process safety.
  • pluginval (Tracktion, https://github.com/Tracktion/pluginval) runs at strictness 10 (max) against the installed VST3 bundle.
  • auval (macOS only, built into CoreAudio) exercises AU v2 + AU v3 lifecycle and parameter behaviour.
  • VST2 smoke is built in — it dlopens the dylib and verifies VSTPluginMain returns a well-formed AEffect.

Set CLAP_VALIDATOR=/path/to/clap-validator to override auto-discovery. cargo truce doctor tells you what's found.

#Strict mode for CI

Per-format flags fail the run if their validator is missing:

Invocation Validator missing →
cargo truce validate (no flag) warning, exit 0
cargo truce validate --all warning, exit 0
cargo truce validate --clap error, exit non-zero
cargo truce validate --pluginval error, exit non-zero

Wire --clap --pluginval (or whichever subset you want) into your CI so a missing validator binary fails the build instead of silently passing.

#Installing the validators

cargo install --locked --git https://github.com/free-audio/clap-validator clap-validator
# pluginval: download the binary from https://github.com/Tracktion/pluginval/releases

auval ships with macOS — nothing to install.

#CI step (GitHub Actions)

Drop this into the job that already builds and installs your plugin. It mirrors what truce's own CI runs on every PR:

- name: Install validators
  run: |
    cargo install --locked --git https://github.com/free-audio/clap-validator clap-validator
    curl -fsSL -o /tmp/pluginval.zip \
      https://github.com/Tracktion/pluginval/releases/download/v1.0.4/pluginval_Linux.zip
    unzip -oq /tmp/pluginval.zip -d ~/.local/bin
    chmod +x ~/.local/bin/pluginval
    echo "$HOME/.local/bin" >> "$GITHUB_PATH"

- name: Install plugin
  run: cargo truce install --user --clap --vst3 -p my-gain

- name: Validate
  run: cargo truce validate --clap --pluginval -p my-gain

Substitute the macOS / Windows pluginval download URL on those runners. Headless Linux jobs need xvfb-run -a in front of cargo truce validate because pluginval and clap-validator both open a window during state checks.

#package

cargo truce package                          # every default format, universal arch, signed
cargo truce package -p my-gain               # single plugin (cargo crate name)
cargo truce package --formats clap,vst3,aax  # subset
cargo truce package --host-only              # skip the cross-arch build (dev iteration)
cargo truce package --no-pace-sign           # skip PACE/iLok signing on AAX (also aliased to --no-sign on macOS)
cargo truce package --no-sign                # Windows: skip Authenticode signing entirely
cargo truce package --no-notarize            # macOS: sign but skip Apple notarization
cargo truce package --no-installer           # Windows: stage files, skip ISCC
cargo truce package --ask                    # end user picks scope at install time (default)
cargo truce package --user                   # hard-locked user-scope installer
cargo truce package --system                 # hard-locked system-scope installer

Output: target/dist/<Name>-<version>-macos.pkg, target/dist/<Name>-<version>-windows.exe, or target/dist/<bundle_id>-<version>-linux-<arch>.tar.gz (the Linux tarball ships an install.sh instead of taking a --user / --system scope at build time — see Linux below). macOS and Windows builds are optionally suffixed with -user / -system when the scope is hard-locked so a --user and --system build of the same plugin don't overwrite each other in dist/. Version comes from [workspace.package] version or [package] version in Cargo.toml.

#Scope (--ask / --user / --system)

--ask (the default) lets the end user pick at install time:

  • macOS: Installer.app shows a "Destination Select" page with "Install for me only" pre-selected; "Install for all users" triggers the standard auth prompt.
  • Windows: Inno Setup shows a "Choose installation mode" page with "Install for me only" / "Install for all users". The wizard relaunches itself elevated only if the user picks all-users.

--user and --system hard-lock the choice — useful for IT- managed studios that always want one scope, or a plugin author who needs to ship a user-only or system-only build separately. Set the project-wide default with [packaging] preferred_scope = "user" | "system" | "ask" in truce.toml; the CLI flag wins when both are set.

System-only formats (AAX, AU v3, Windows VST2, the standalone .app) stay in the package under every scope. On macOS, the Distribution XML marks those choices with auth="Root" while user-viable formats stay auth="None" in --user mode — so a --user install drops CLAP / VST3 / LV2 / AU v2 in ~/Library/ without an admin prompt, and asks for admin only when the user keeps AAX / AU v3 / standalone selected (those land in /Library/... / /Applications/ as they must). On Windows the equivalent is one elevation prompt at install time when system- only formats are included. Drop the system-only formats with --formats clap,vst3,... if you want a no-admin installer.

Defaults to universal. macOS bundles are lipo'd fat Mach-O (x86_64 + aarch64). Windows installers carry both x64 and arm64 payloads and install the right one per machine.

#macOS flow

cargo truce package (on macOS)
    ↓
1. Build each format × arch    (x86_64 + aarch64 by default)
2. lipo per format             → fat Mach-O in target/release/
3. Stage into target/package/  (one fat bundle per format)
4. Codesign bundles            Developer ID Application + hardened runtime + timestamp
5. pkgbuild per format         → components/<name>-<format>.pkg
                                 (--ownership preserve + per-format Scripts/preinstall
                                  that wipes any stale install at the destination
                                  before shove writes the new payload)
6. productbuild                → target/dist/<Name>-<version>-macos.pkg
                                 (signed with Developer ID Installer; Distribution
                                  marks system-only choices auth="Root" so the
                                  installer escalates only for those)
7. notarytool + staple         (if [macos.packaging].notarize = true)

Minimum config for signed + notarised macOS builds. The notarize flag goes in truce.toml (it's a project-level release decision); signing identities and Apple credentials go in .cargo/config.toml [env].

# truce.toml
[macos.packaging]
notarize = true
# .cargo/config.toml — gitignored.
[env]
TRUCE_SIGNING_IDENTITY           = "Developer ID Application: Your Name (TEAMID)"
TRUCE_INSTALLER_SIGNING_IDENTITY = "Developer ID Installer: Your Name (TEAMID)"

One-time notarisation keychain setup:

xcrun notarytool store-credentials TRUCE_NOTARY \
  --apple-id "you@example.com" \
  --team-id "TEAMID" \
  --password "<app-specific-password-from-appleid.apple.com>"

cargo truce install --au2 clears ~/Library/Caches/AudioUnitCache automatically so the new component shows up in auval and DAW scans without a manual step. If you ever need a heavier reset, cargo truce reset-au kills the AudioComponentRegistrar daemon too. AAX is built fat from the Avid SDK on macOS (both Apple archs ship in the SDK).

#Windows flow

cargo truce package (on Windows)
    ↓
1. Build each format × arch    (x86_64-pc-windows-msvc + aarch64-pc-windows-msvc)
2. Stage into target\package\  (VST3/AAX bundles carry both archs in arch subdirs;
                                CLAP/VST2 stage both DLLs side-by-side)
3. Authenticode-sign binaries  signtool.exe
4. PACE-sign AAX               wraptool.exe (skipped if PACE_ACCOUNT unset)
5. Render .iss                 target\package\windows\<bundle_id>\installer.iss
6. Compile installer           ISCC.exe → dist\<Name>-<version>-windows.exe
7. Authenticode-sign installer signtool.exe

Requirements:

  • Inno Setup 6 for ISCC.exe (6.3+ if packaging --universal).
  • Windows 10/11 SDK for signtool.exe.
  • PACE wraptool on PATH + PACE_ACCOUNT / PACE_SIGN_ID env vars, only if you're signing AAX for retail Pro Tools.

cargo truce doctor reports what's present.

Three Authenticode credential sources, tried in order. All three are configured via env vars in .cargo/config.toml [env]:

  1. Azure Trusted Signing (recommended, ~$120/yr, no hardware token). Set TRUCE_AZURE_ACCOUNT + TRUCE_AZURE_PROFILE (optional TRUCE_AZURE_DLIB to point at a non-default Azure.CodeSigning.Dlib, optional TRUCE_AZURE_ENDPOINT to sign through a region other than the East US default), and AZURE_TENANT_ID / AZURE_CLIENT_ID / AZURE_CLIENT_SECRET for auth.
  2. SHA1 cert thumbprint — typical for OV/EV certs on a hardware token. Set TRUCE_CERT_SHA1 (+ optional TRUCE_CERT_STORE if the cert isn't in signtool's default My store).
  3. .pfx file — set TRUCE_PFX_PATH and put the password in TRUCE_PFX_PASSWORD.

With no credentials package still runs; it prints a single warning and emits unsigned binaries. Users get SmartScreen "Unknown publisher" prompts.

#Universal (x64 + ARM64) Windows

Windows PE has no fat-binary equivalent to Mach-O. The installer compiles both archs separately and installs the right one via Inno Setup Check: directives for single-file formats, and side-by-side arch sub-directories for bundle formats:

Plugin.vst3/Contents/x86_64-win/Plugin.vst3
Plugin.vst3/Contents/arm64-win/Plugin.vst3
Plugin.aaxplugin/Contents/x64/Plugin.aaxplugin

Requirements on the build host:

  • VS Installer: "MSVC v143 - VS 2022 C++ ARM64/ARM64EC build tools" and "Windows 11 SDK (ARM64)".
  • rustup installed. The missing aarch64-pc-windows-msvc target is auto-added on first use by cargo truce package (same preflight covers x86_64-apple-darwin / aarch64-apple-darwin on macOS). Homebrew's rust package shadows rustup and breaks this — which cargo must point at ~/.cargo/bin/cargo.

AAX stays host-arch-only under --universal: Avid's Windows AAX SDK ships x64 libs only. The package step stages AAX for the host arch and prints a note.

#Linux

cargo truce package

Produces target/dist/{bundle_id}-{version}-linux-{arch}.tar.gz — one tarball per [[plugin]] and one per [[suite]]. Each archive contains the staged bundles grouped by format (clap/, vst3/, vst/, lv2/), a standalone/ directory if the plugin has the standalone feature enabled, an auto-generated README.txt, and an executable install.sh:

./install.sh                      # interactive
./install.sh --all                # user scope, every plugin
./install.sh --system             # /usr/lib/... + /usr/local/bin
./install.sh --plugin my-gain     # one plugin (repeatable)
./install.sh --help               # every option

Flags:

Flag Default Effect
--target <triple> host triple Repeatable. Reads target/bundles/<triple>/ and tags the archive name with the matching arch slug.
--no-build off Skip the implicit cargo truce build step.
--no-per-plugin off Skip per-plugin tarballs; only emit the suite archives.
--suite <name> / --no-suite <name> every [[suite]] Restrict which suite archives get produced.

No .deb / .rpm / AppImage / AUR support yet. No signed installers on Linux — the tarball + install.sh is the only path today. End users who'd rather build from source can git clone the project and run cargo truce install themselves.

#Secrets belong in .cargo/config.toml

Signing identities, AAX SDK paths, notary credentials — anything machine- or developer-specific — go in .cargo/config.toml (gitignored), not truce.toml (committed):

# .cargo/config.toml
[env]
TRUCE_SIGNING_IDENTITY           = "Developer ID Application: Your Name (TEAMID)"
TRUCE_INSTALLER_SIGNING_IDENTITY = "Developer ID Installer: Your Name (TEAMID)"
AAX_SDK_PATH                     = "/Users/you/aax-sdk-2-9-0"
APPLE_ID                         = "you@example.com"
TEAM_ID                          = "TEAMID"
TRUCE_PFX_PASSWORD               = "…"          # if using .pfx

Env vars take precedence over equivalent truce.toml fields.

#CI

#macOS (GitHub Actions)

jobs:
  package-macos:
    runs-on: macos-latest
    steps:
      - uses: actions/checkout@v4
      - uses: dtolnay/rust-toolchain@stable

      - name: Import certs
        env:
          CERT_P12: ${{ secrets.MACOS_CERT_P12_BASE64 }}
          CERT_PW:  ${{ secrets.MACOS_CERT_PASSWORD }}
        run: |
          echo "$CERT_P12" | base64 --decode > cert.p12
          security create-keychain -p "" build.keychain
          security import cert.p12 -k build.keychain -P "$CERT_PW" \
            -T /usr/bin/codesign -T /usr/bin/productbuild
          security set-key-partition-list -S apple-tool:,apple: -s -k "" build.keychain
          security default-keychain -s build.keychain

      - name: Store notarization creds
        run: |
          xcrun notarytool store-credentials TRUCE_NOTARY \
            --apple-id "${{ secrets.APPLE_ID }}" \
            --team-id "${{ secrets.TEAM_ID }}" \
            --password "${{ secrets.APPLE_APP_PASSWORD }}"

      - run: cargo truce package
      - uses: actions/upload-artifact@v4
        with: { name: macos-installer, path: target/dist/*.pkg }

#Windows (GitHub Actions)

jobs:
  package-windows:
    runs-on: windows-latest
    steps:
      - uses: actions/checkout@v4
      - uses: dtolnay/rust-toolchain@stable
        with: { targets: x86_64-pc-windows-msvc,aarch64-pc-windows-msvc }

      - name: Install Inno Setup
        run: choco install innosetup --no-progress

      - name: Trusted Signing auth
        env:
          AZURE_TENANT_ID:     ${{ secrets.AZURE_TENANT_ID }}
          AZURE_CLIENT_ID:     ${{ secrets.AZURE_CLIENT_ID }}
          AZURE_CLIENT_SECRET: ${{ secrets.AZURE_CLIENT_SECRET }}
        run: echo "Auth via env"

      - run: cargo truce package
      - uses: actions/upload-artifact@v4
        with: { name: windows-installer, path: target/dist/*.exe }

AAX CI needs the AAX SDK cached on the runner (via actions/cache, keyed on a hash of the SDK tarball). Or exclude AAX from [packaging].formats in CI and build it only locally from a developer machine with the SDK installed.

#Troubleshooting

cargo truce package says "no formats to package". No plugin has any of clap, vst3, vst2, lv2, au, aax in its default Cargo features, and [packaging].formats isn't set. Either add a format to default, or pass --formats clap explicitly.

macOS: notarisation says "Invalid". Run cargo truce package --no-notarize, then submit manually:

xcrun notarytool submit target/dist/<Name>-<version>-macos.pkg \
  --keychain-profile TRUCE_NOTARY --wait
xcrun notarytool log <submission-id> --keychain-profile TRUCE_NOTARY

Most "Invalid" results are an unsigned nested binary — often a bundle was staged after signing, or ad-hoc signed at install but not re-signed for package.

Windows: "unknown publisher". Authenticode isn't configured. See the credential section above.

Windows: ISCC.exe not found. Install Inno Setup 6.

Pro Tools rejects AAX with error -7054. PACE signing required for retail Pro Tools. Use Pro Tools Developer with a dev iLok licence for local testing, or set up a paid PACE signing account before shipping.

Nothing rebuilds when I change a transitive dep. Your plugin probably depends on truce via git. Use [patch."https://github.com/truce-audio/truce"] in Cargo.toml to point at a local checkout during development.


#Reference

  • truce.toml — every field tracked in your committed project config: vendors, plugins, suites, packaging, installer appearance.
  • .cargo/config.toml — every environment variable: signing identities, SDK paths, notary credentials, validator overrides, hot-reload.
  • cargo truce CLI — every subcommand and flag in one place.

#What's next

You've shipped a plugin. Browse the formats for per-format gotchas, or jump back to the reference when you need to look something up.