Skip to main content

max / audiofiles

Lock launch scope; add native Windows MSI builder for windows-x86 Extract WiX source into a shared dist/audiofiles.wxs.in template consumed by both the new native PowerShell builder (build-msi-native.ps1, canonical on windows-x86) and the existing macOS/Linux cross-compile fallback (build-msi.sh). Native builder adds opt-in code signing via AF_SIGN_CERT / AF_SIGN_PASS / AF_SIGN_TIMESTAMP_URL env vars — no-op when unset to match current Azure-cert-blocked state. Update deploy.md to canonicalize the native flow and document signing hooks. Add a Windows Installer section to human_testing.md covering install, upgrade, and uninstall. Tighten the launch-scope section in todo.md to point at the new builder. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Author: Max J. <87768334+MaxJMath@users.noreply.github.com> · 2026-05-16 19:47 UTC
Commit: d8089fbfc3fe18105dbd89fd66c13d776db80ae4
Parent: 6e9eaae
6 files changed, +267 insertions, -61 deletions
@@ -0,0 +1,59 @@
1 + <?xml version="1.0" encoding="UTF-8"?>
2 + <!--
3 + AudioFiles MSI source. Consumed by build-msi-native.ps1 (windows-x86, native
4 + WiX toolset) and build-msi.sh (macOS/Linux fallback via msitools wixl).
5 +
6 + Tokens replaced at build time:
7 + @VERSION@ — semver from crates/audiofiles-app/Cargo.toml, with ".0" suffix
8 + (WiX requires four-part version)
9 + -->
10 + <Wix xmlns="http://schemas.microsoft.com/wix/2006/wi">
11 + <Product Id="*"
12 + Name="AudioFiles"
13 + Language="1033"
14 + Version="@VERSION@"
15 + Manufacturer="Maxwell Johnson"
16 + UpgradeCode="8B4D5A2E-7F3C-4E1A-9D6B-2C8A4F5E7D9B">
17 +
18 + <Package InstallerVersion="200"
19 + Compressed="yes"
20 + InstallScope="perUser"
21 + Description="AudioFiles @VERSION@"
22 + Comments="Sample manager with content-addressed storage" />
23 +
24 + <MajorUpgrade DowngradeErrorMessage="A newer version is already installed." />
25 + <MediaTemplate EmbedCab="yes" />
26 +
27 + <Directory Id="TARGETDIR" Name="SourceDir">
28 + <Directory Id="LocalAppDataFolder">
29 + <Directory Id="INSTALLFOLDER" Name="AudioFiles">
30 + <Component Id="MainExecutable" Guid="*">
31 + <File Id="AudioFilesExe"
32 + Source="AudioFiles.exe"
33 + KeyPath="yes" />
34 + </Component>
35 + </Directory>
36 + </Directory>
37 + <Directory Id="ProgramMenuFolder">
38 + <Component Id="StartMenuShortcut" Guid="*">
39 + <Shortcut Id="AudioFilesShortcut"
40 + Name="AudioFiles"
41 + Target="[INSTALLFOLDER]AudioFiles.exe"
42 + WorkingDirectory="INSTALLFOLDER" />
43 + <RegistryValue Root="HKCU"
44 + Key="Software\AudioFiles"
45 + Name="installed"
46 + Type="integer"
47 + Value="1"
48 + KeyPath="yes" />
49 + <RemoveFolder Id="RemoveStartMenu" On="uninstall" />
50 + </Component>
51 + </Directory>
52 + </Directory>
53 +
54 + <Feature Id="Complete" Title="AudioFiles" Level="1">
55 + <ComponentRef Id="MainExecutable" />
56 + <ComponentRef Id="StartMenuShortcut" />
57 + </Feature>
58 + </Product>
59 + </Wix>
@@ -0,0 +1,132 @@
1 + <#
2 + .SYNOPSIS
3 + Build AudioFiles MSI natively on windows-x86 (per the native-builds-per-arch
4 + policy). Also produces a matching standalone .exe.
5 +
6 + .DESCRIPTION
7 + Runs on the windows-x86 build machine. Uses the WiX Toolset (candle.exe,
8 + light.exe) — install from https://wixtoolset.org/ or `winget install
9 + WiXToolset.WiXToolset`.
10 +
11 + Code signing is opt-in via environment variables. If unset, the script
12 + produces an unsigned MSI/EXE (current state — Azure certificate blocker
13 + documented in docs/deploy.md).
14 +
15 + .EXAMPLE
16 + # From repo root:
17 + PS> pwsh -File dist\build-msi-native.ps1
18 +
19 + .ENVIRONMENT
20 + AF_SIGN_CERT Path to .pfx or thumbprint of an installed cert.
21 + When set, MSI and EXE are signed via signtool.exe.
22 + AF_SIGN_PASS Password for the .pfx (skip if cert is in store).
23 + AF_SIGN_TIMESTAMP_URL RFC 3161 timestamp URL.
24 + Default: http://timestamp.digicert.com
25 + #>
26 +
27 + $ErrorActionPreference = 'Stop'
28 +
29 + $ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
30 + $ProjectDir = Resolve-Path (Join-Path $ScriptDir '..')
31 + $DistDir = $ScriptDir
32 + $Target = 'x86_64-pc-windows-msvc'
33 +
34 + # Read version from workspace Cargo.toml (workspace.package.version) or app crate.
35 + $AppCargo = Join-Path $ProjectDir 'crates\audiofiles-app\Cargo.toml'
36 + $VersionLine = Select-String -Path $AppCargo -Pattern '^version\s*=\s*"([^"]+)"' | Select-Object -First 1
37 + if (-not $VersionLine) { throw "Could not parse version from $AppCargo" }
38 + $Version = $VersionLine.Matches[0].Groups[1].Value
39 + $WixVersion = "$Version.0" # WiX requires four-part version
40 + Write-Host "Building AudioFiles v$Version MSI ($Target)" -ForegroundColor Cyan
41 +
42 + # Prerequisites
43 + foreach ($tool in @('candle.exe', 'light.exe')) {
44 + if (-not (Get-Command $tool -ErrorAction SilentlyContinue)) {
45 + throw "$tool not on PATH. Install WiX Toolset from https://wixtoolset.org/"
46 + }
47 + }
48 + $installedTargets = & rustup target list --installed
49 + if ($installedTargets -notcontains $Target) {
50 + throw "Rust target $Target not installed. Run: rustup target add $Target"
51 + }
52 +
53 + # Step 1: Native release build
54 + Write-Host "==> Building release binary..." -ForegroundColor Cyan
55 + Push-Location $ProjectDir
56 + try {
57 + & cargo build --release -p audiofiles-app --target $Target
58 + if ($LASTEXITCODE -ne 0) { throw "cargo build failed (exit $LASTEXITCODE)" }
59 + } finally {
60 + Pop-Location
61 + }
62 +
63 + $ExeSrc = Join-Path $ProjectDir "target\$Target\release\audiofiles-app.exe"
64 + if (-not (Test-Path $ExeSrc)) { throw "Build artifact missing: $ExeSrc" }
65 +
66 + # Step 2: Stage files
67 + Write-Host "==> Staging files..." -ForegroundColor Cyan
68 + $Staging = Join-Path $DistDir '.msi-staging'
69 + if (Test-Path $Staging) { Remove-Item -Recurse -Force $Staging }
70 + New-Item -ItemType Directory -Path $Staging | Out-Null
71 + Copy-Item $ExeSrc (Join-Path $Staging 'AudioFiles.exe')
72 + Copy-Item (Join-Path $DistDir 'audiofiles.png') (Join-Path $Staging 'audiofiles.png')
73 +
74 + # Optional: sign the EXE before embedding it in the MSI so it's signed in-place.
75 + function Invoke-Signtool {
76 + param([string]$Path)
77 + if (-not $env:AF_SIGN_CERT) { return $false }
78 + $signtool = Get-Command 'signtool.exe' -ErrorAction SilentlyContinue
79 + if (-not $signtool) { throw "AF_SIGN_CERT set but signtool.exe not on PATH" }
80 + $timestampUrl = if ($env:AF_SIGN_TIMESTAMP_URL) { $env:AF_SIGN_TIMESTAMP_URL } else { 'http://timestamp.digicert.com' }
81 + $args = @('sign', '/fd', 'SHA256', '/tr', $timestampUrl, '/td', 'SHA256')
82 + if ($env:AF_SIGN_CERT -match '\.pfx$') {
83 + $args += @('/f', $env:AF_SIGN_CERT)
84 + if ($env:AF_SIGN_PASS) { $args += @('/p', $env:AF_SIGN_PASS) }
85 + } else {
86 + # Treat as SHA1 thumbprint of a cert in the local store.
87 + $args += @('/sha1', $env:AF_SIGN_CERT)
88 + }
89 + $args += $Path
90 + Write-Host "==> Signing $Path" -ForegroundColor Cyan
91 + & $signtool.Source @args
92 + if ($LASTEXITCODE -ne 0) { throw "signtool failed on $Path (exit $LASTEXITCODE)" }
93 + return $true
94 + }
95 +
96 + $StagedExe = Join-Path $Staging 'AudioFiles.exe'
97 + $signed = Invoke-Signtool -Path $StagedExe
98 + if (-not $signed) {
99 + Write-Host "==> AF_SIGN_CERT unset — producing UNSIGNED build (see docs/deploy.md)" -ForegroundColor Yellow
100 + }
101 +
102 + # Step 3: Generate WiX source from template
103 + $WxsTemplate = Join-Path $DistDir 'audiofiles.wxs.in'
104 + $Wxs = Join-Path $Staging 'audiofiles.wxs'
105 + (Get-Content -Raw $WxsTemplate).Replace('@VERSION@', $WixVersion) | Set-Content -Path $Wxs -Encoding utf8
106 +
107 + # Step 4: candle + light → MSI
108 + Write-Host "==> Building MSI..." -ForegroundColor Cyan
109 + $MsiName = "AudioFiles_${Version}_x86_64.msi"
110 + $MsiPath = Join-Path $DistDir $MsiName
111 + if (Test-Path $MsiPath) { Remove-Item -Force $MsiPath }
112 +
113 + $Wixobj = Join-Path $Staging 'audiofiles.wixobj'
114 + & candle.exe -nologo -out $Wixobj $Wxs
115 + if ($LASTEXITCODE -ne 0) { throw "candle.exe failed (exit $LASTEXITCODE)" }
116 + & light.exe -nologo -b $Staging -out $MsiPath $Wixobj
117 + if ($LASTEXITCODE -ne 0) { throw "light.exe failed (exit $LASTEXITCODE)" }
118 +
119 + # Step 5: Sign the MSI (if signing is configured)
120 + Invoke-Signtool -Path $MsiPath | Out-Null
121 +
122 + # Step 6: Cleanup + ship the standalone .exe alongside
123 + Remove-Item -Recurse -Force $Staging
124 + $ExeDest = Join-Path $DistDir "AudioFiles_${Version}_x86_64.exe"
125 + Copy-Item $ExeSrc $ExeDest
126 + # Re-sign the standalone exe (the one we signed earlier was the staged copy).
127 + Invoke-Signtool -Path $ExeDest | Out-Null
128 +
129 + Write-Host ""
130 + Write-Host "Done:" -ForegroundColor Green
131 + Write-Host " MSI: $MsiPath"
132 + Write-Host " EXE: $ExeDest"
@@ -45,61 +45,16 @@ mkdir -p "$STAGING"
45 45 cp "$EXE_SRC" "$STAGING/AudioFiles.exe"
46 46 cp "$DIST_DIR/audiofiles.png" "$STAGING/audiofiles.png"
47 47
48 - # Step 3: Generate WiX source
48 + # Step 3: Generate WiX source from the shared template
49 + # (canonical builder is dist/build-msi-native.ps1 on windows-x86; this script
50 + # is a macOS/Linux cross-compile fallback)
51 + WXS_TEMPLATE="$DIST_DIR/audiofiles.wxs.in"
52 + if [ ! -f "$WXS_TEMPLATE" ]; then
53 + echo "Error: WiX template missing: $WXS_TEMPLATE"
54 + exit 1
55 + fi
49 56 WXS="$STAGING/audiofiles.wxs"
50 - cat > "$WXS" << WXS_EOF
51 - <?xml version="1.0" encoding="UTF-8"?>
52 - <Wix xmlns="http://schemas.microsoft.com/wix/2006/wi">
53 - <Product Id="*"
54 - Name="AudioFiles"
55 - Language="1033"
56 - Version="${VERSION}.0"
57 - Manufacturer="Maxwell Johnson"
58 - UpgradeCode="8B4D5A2E-7F3C-4E1A-9D6B-2C8A4F5E7D9B">
59 -
60 - <Package InstallerVersion="200"
61 - Compressed="yes"
62 - InstallScope="perUser"
63 - Description="AudioFiles ${VERSION}"
64 - Comments="Sample manager with content-addressed storage" />
65 -
66 - <MajorUpgrade DowngradeErrorMessage="A newer version is already installed." />
67 - <MediaTemplate EmbedCab="yes" />
68 -
69 - <Directory Id="TARGETDIR" Name="SourceDir">
70 - <Directory Id="LocalAppDataFolder">
71 - <Directory Id="INSTALLFOLDER" Name="AudioFiles">
72 - <Component Id="MainExecutable" Guid="*">
73 - <File Id="AudioFilesExe"
74 - Source="AudioFiles.exe"
75 - KeyPath="yes" />
76 - </Component>
77 - </Directory>
78 - </Directory>
79 - <Directory Id="ProgramMenuFolder">
80 - <Component Id="StartMenuShortcut" Guid="*">
81 - <Shortcut Id="AudioFilesShortcut"
82 - Name="AudioFiles"
83 - Target="[INSTALLFOLDER]AudioFiles.exe"
84 - WorkingDirectory="INSTALLFOLDER" />
85 - <RegistryValue Root="HKCU"
86 - Key="Software\\AudioFiles"
87 - Name="installed"
88 - Type="integer"
89 - Value="1"
90 - KeyPath="yes" />
91 - <RemoveFolder Id="RemoveStartMenu" On="uninstall" />
92 - </Component>
93 - </Directory>
94 - </Directory>
95 -
96 - <Feature Id="Complete" Title="AudioFiles" Level="1">
97 - <ComponentRef Id="MainExecutable" />
98 - <ComponentRef Id="StartMenuShortcut" />
99 - </Feature>
100 - </Product>
101 - </Wix>
102 - WXS_EOF
57 + sed "s/@VERSION@/${VERSION}.0/g" "$WXS_TEMPLATE" > "$WXS"
103 58
104 59 # Step 4: Build MSI
105 60 echo "==> Building MSI..."
M docs/deploy.md +33 -5
@@ -9,7 +9,7 @@ See `_meta/docs/deploy.md` for shared infrastructure (machines, git remotes, col
9 9 | macOS aarch64 | .dmg | local |
10 10 | Linux aarch64 | AppImage | astra |
11 11 | Linux x86_64 | AppImage | pop-os |
12 - | Windows x86_64 | standalone .exe | windows-x86 |
12 + | Windows x86_64 | .msi + standalone .exe | windows-x86 |
13 13
14 14 ## Build Commands
15 15
@@ -26,14 +26,42 @@ scp astra:~/Code/Apps/audiofiles/dist/AudioFiles-*-aarch64.AppImage ~/Dist/audio
26 26 ssh pop-os "source ~/.cargo/env && cd ~/Code/Apps/audiofiles && git pull && bash dist/build-appimage.sh"
27 27 scp pop-os:~/Code/Apps/audiofiles/dist/AudioFiles-*-x86_64.AppImage ~/Dist/audiofiles/linux-x86_64/
28 28
29 - # Windows (windows-x86):
30 - ssh me@windows-x86 "cd C:\Users\me\Code\Apps\audiofiles; git pull; cargo build --release -p audiofiles-app"
31 - scp me@windows-x86:"C:/Users/me/Code/Apps/audiofiles/target/release/audiofiles-app.exe" ~/Dist/audiofiles/windows/AudioFiles_VERSION_x64.exe
29 + # Windows (windows-x86) — native build + MSI installer via WiX:
30 + ssh me@windows-x86 "cd C:\Users\me\Code\Apps\audiofiles; git pull; pwsh -File dist\build-msi-native.ps1"
31 + scp me@windows-x86:"C:/Users/me/Code/Apps/audiofiles/dist/AudioFiles_VERSION_x86_64.msi" ~/Dist/audiofiles/windows/
32 + scp me@windows-x86:"C:/Users/me/Code/Apps/audiofiles/dist/AudioFiles_VERSION_x86_64.exe" ~/Dist/audiofiles/windows/
33 +
34 + # Emergency cross-compile fallback (macOS/Linux host, msitools wixl + cargo-xwin):
35 + bash dist/build-msi.sh
32 36 ```
33 37
38 + ## Windows Installer
39 +
40 + Canonical builder: `dist/build-msi-native.ps1` runs on **windows-x86** with the
41 + WiX Toolset (`winget install WiXToolset.WiXToolset`). It produces an MSI with
42 + Start Menu shortcut, per-user install scope, and major-upgrade behavior.
43 +
44 + WiX source lives in `dist/audiofiles.wxs.in` (shared by both builders).
45 + `@VERSION@` is substituted from `crates/audiofiles-app/Cargo.toml`.
46 +
47 + ### Code signing
48 +
49 + The PowerShell builder will sign the EXE and MSI with `signtool.exe` when these
50 + environment variables are set:
51 +
52 + | Var | Meaning |
53 + |-----|---------|
54 + | `AF_SIGN_CERT` | Path to a `.pfx`, OR SHA1 thumbprint of a cert in the local Windows cert store |
55 + | `AF_SIGN_PASS` | Password for the `.pfx` (omit if cert is in store) |
56 + | `AF_SIGN_TIMESTAMP_URL` | RFC 3161 timestamp URL (default: `http://timestamp.digicert.com`) |
57 +
58 + If `AF_SIGN_CERT` is unset, the build proceeds **unsigned** with a warning —
59 + the current state (code signing blocked on Azure certificate history
60 + requirement; see the GO deploy doc for context, same blocker applies).
61 +
34 62 ## Project-Specific Notes
35 63
36 - - Native egui app (not Tauri). No Tauri CLI, Node.js, or .msi installer needed.
64 + - Native egui app (not Tauri). No Tauri CLI or Node.js needed. Windows ships both a standalone `.exe` and a WiX-built `.msi` installer (see § Windows Installer above).
37 65 - No OTA updater — users download new versions from the MNW storefront.
38 66 - License key activation at first launch via MNW API.
39 67 - Version is in workspace root `Cargo.toml` under `[workspace.package]`.
@@ -124,9 +124,21 @@ Run before every release. Sign-off table at the bottom.
124 124 ### Platform-Specific
125 125
126 126 - [ ] **macOS**: drag-out via NSPasteboardItem; app appears in Dock; no Gatekeeper warning on a signed build
127 - - [ ] **Windows**: drag-out via OLE/COM; .exe installer runs; uninstall is clean
127 + - [ ] **Windows**: drag-out via OLE/COM; standalone .exe runs; uninstall is clean
128 128 - [ ] **Linux**: AppImage launches; drag-out works on X11 and Wayland (symlink fallback)
129 129
130 + ### Windows Installer (run before each release on windows-x86)
131 +
132 + Build with `pwsh -File dist\build-msi-native.ps1` (or `bash dist/build-msi.sh` from macOS/Linux as a fallback).
133 +
134 + - [ ] MSI install succeeds (no UAC errors for per-user scope)
135 + - [ ] Start Menu shortcut "AudioFiles" present after install
136 + - [ ] Launching from Start Menu opens the app, library loads
137 + - [ ] Repair-install: re-run the same MSI — completes without error
138 + - [ ] Major-upgrade: install older MSI, then newer MSI — old version removed cleanly, settings preserved
139 + - [ ] Uninstall via Apps & Features removes Start Menu entry and program files
140 + - [ ] If `AF_SIGN_CERT` was set at build: SmartScreen reputation prompt absent or accepts the cert; right-click .exe → Properties → Digital Signatures shows the expected certificate
141 +
130 142 ### Robustness
131 143
132 144 - [ ] Import a corrupt audio file — rejected, no crash
M docs/todo.md +21 -1
@@ -1,12 +1,32 @@
1 1 # audiofiles TODO
2 2
3 3 ## Status
4 - Done: All pre-beta phases + Phase 11 + SyncKit parity. Active: None. Next: Vocal layer 2, sample forge (phases 10-16).
4 + Done: All pre-beta phases + Phase 11 + SyncKit parity. Active: None. Next: launch (see below), then sample forge post-launch.
5 5
6 6 v0.4.0. Audit grade A- (Ultra Fuzz 1, 2026-05-09). 780 tests. Rust 2024 edition (2026-05-06). rand 0.9. 4 SERIOUS, 10 MINOR findings from 5-axis adversarial audit. Run 20 items all resolved.
7 7
8 8 ---
9 9
10 + ## Launch (Locked Scope, 2026-05-16)
11 +
12 + Public-launch blockers. Everything else in this file is post-launch. Do not promote items into this section without explicit user decision.
13 +
14 + 1. **Test full checkout flow against live Stripe** — end-to-end: subscribe → webhook → blob sync gate passes
15 + 2. **Build v0.4.0 Windows installer on windows-x86** — run `pwsh -File dist\build-msi-native.ps1`. Produces signed-or-unsigned `AudioFiles_0.4.0_x86_64.msi` + `.exe`. WiX source `dist/audiofiles.wxs.in`. Macros/cross-compile fallback: `dist/build-msi.sh`. (Installer config + signing hooks landed 2026-05-16.)
16 + 3. **Code-sign Windows binaries** — gated on Azure cert blocker (same as GO/BB). When unblocked: set `AF_SIGN_CERT` / `AF_SIGN_PASS` / `AF_SIGN_TIMESTAMP_URL` and re-run the PS1 builder. Until then, ship unsigned (matches 0.3.x).
17 + 4. **Test installed build on Windows** — install the MSI, verify Start Menu shortcut, run AudioFiles, complete `docs/human_testing.md` Windows-specific section, uninstall cleanly.
18 +
19 + Explicitly **out of launch scope** (defer until after public launch):
20 + - Phase 10 (Plugin/CLAP processing) — entire phase
21 + - Phase 12 (Chop Engine), Phase 13 (Resample/Time-Stretch), Phase 14 (Layer/Stack), Phase 15 (Batch Forge), Phase 16 (Snapshot History) — entire sample-forge pivot
22 + - Vocal layer 2 classifier (needs new training data)
23 + - Multi-sample instrument UI (KeyZone) — grayed out is acceptable
24 + - All dependency-prune items (do in a quiet week post-launch)
25 + - Ultra Fuzz trust-model items (ed25519 sig, keychain, Rhai timeout) — already flagged architectural
26 + - UX audit power-user gaps, rust-fuzz minor items, shared code extraction
27 +
28 + ---
29 +
10 30 ## Dependency Pruning (2026-05-13)
11 31
12 32 ### High Impact