Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 32 additions & 1 deletion .github/workflows/build-windows.yml
Original file line number Diff line number Diff line change
Expand Up @@ -179,9 +179,40 @@ jobs:
Write-Host "Created: dist/$packageName.zip"
Get-ChildItem -Path "dist/"

# Pinned and checksummed: this compiler produces the executable users run.
# 6.x rather than 7.x deliberately - packaging/windows/vapourbox.iss is
# written against 6. Installed to its own directory and handed over as
# $ISCC: the runner image already has an ISCC.exe on PATH (a Chocolatey
# shim, version unpinned), and without this that one does the compiling.
- name: Install Inno Setup
shell: pwsh
run: |
$version = "6.7.3"
$sha256 = "9c73c3bae7ed48d44112a0f48e66742c00090bdb5bef71d9d3c056c66e97b732"
$tag = "is-" + ($version -replace '\.', '_')
$setup = Join-Path $env:RUNNER_TEMP "innosetup-$version.exe"
Invoke-WebRequest -Uri "https://github.com/jrsoftware/issrc/releases/download/$tag/innosetup-$version.exe" -OutFile $setup -UseBasicParsing
$actual = (Get-FileHash -Algorithm SHA256 $setup).Hash.ToLower()
if ($actual -ne $sha256) { throw "Inno Setup $version failed its checksum: $actual" }
$dir = Join-Path $env:RUNNER_TEMP "InnoSetup-$version"
$install = Start-Process -FilePath $setup -ArgumentList "/VERYSILENT", "/SUPPRESSMSGBOXES", "/NORESTART", "/SP-", "/CURRENTUSER", "/NOICONS", "/DIR=$dir" -Wait -PassThru
if ($install.ExitCode -ne 0) { throw "Inno Setup installer exited with $($install.ExitCode)" }
$iscc = Join-Path $dir "ISCC.exe"
if (-not (Test-Path $iscc)) { throw "Inno Setup $version did not install ISCC.exe to $dir" }
"ISCC=$iscc" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append

- name: Build installer
shell: pwsh
run: |
$version = "${{ inputs.version }}"
./Scripts/build-windows-installer.ps1 -Version $version -SourceDir "dist/VapourBox-$version-windows-x64" -OutputDir dist

- name: Upload artifact
uses: actions/upload-artifact@v5
with:
name: VapourBox-${{ inputs.version }}-windows-x64
path: dist/VapourBox-${{ inputs.version }}-windows-x64.zip
if-no-files-found: error
path: |
dist/VapourBox-${{ inputs.version }}-windows-x64-setup.exe
dist/VapourBox-${{ inputs.version }}-windows-x64.zip

32 changes: 29 additions & 3 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -263,8 +263,8 @@ dart run build_runner build
# macOS
./Scripts/package-macos.sh --version X.Y.Z [--skip-build]

# Windows
.\Scripts\package-windows.ps1 -Version "X.Y.Z" [-SkipBuild]
# Windows — writes an installer (-setup.exe) and a zip of the same tree
.\Scripts\package-windows.ps1 -Version "X.Y.Z" [-SkipBuild] [-SkipInstaller]

# Linux — writes an AppImage (+ its .zsync) and a tarball of the same tree
./Scripts/package-linux.sh --version X.Y.Z [--skip-build] [--skip-appimage]
Expand Down Expand Up @@ -1439,6 +1439,31 @@ placement and a version skew would change chroma per-OS.
without it the app reports "Version file missing" and re-downloads the
published bundle over a locally built one.
- Show in Folder: `cmd /c explorer /select, <path>`
- **Ships as an Inno Setup installer, with the zip of the same tree as the
portable option** (`packaging/windows/vapourbox.iss`, compiled by
`Scripts/build-windows-installer.ps1`). The rules that keep it working:
- **Per-user only (`PrivilegesRequired=lowest`), with no all-users option.**
On Windows the app installs deps and add-ons into `deps\` and `addons\`
**next to the executable**, so the install directory must be user-writable;
under `Program Files` the first-launch download fails. Don't add
`PrivilegesRequiredOverridesAllowed` or an admin install without first
moving those directories out of the app folder.
- **Never change `AppId`.** It is how an upgrade finds the existing install;
a new one installs a second copy with its own uninstall entry.
- **Upgrade keeps `deps\`/`addons\`; uninstall removes them**
(`[UninstallDelete]` — the app downloaded them, so the uninstaller doesn't
otherwise know they exist). `[InstallDelete]` clears `templates\` and
`data\` first, because Inno never removes a file a newer version dropped
and `exe_dir\templates` is the first path the worker searches.
- **The installer is unsigned**, like the rest of the Windows build, so
SmartScreen warns on it; the README says how to proceed.
- `build-windows.yml` assembles the package directory inline rather than
calling `package-windows.ps1` (and the two zips differ in layout), which
is why the installer is its own script both call. Inno Setup is pinned to
6.7.3 by SHA-256 in the workflow and passed to the script as `$env:ISCC`
— the runner image has its own unpinned `ISCC.exe` on `PATH`, which wins
otherwise. The `.iss` is written against 6, not 7.
- Not verifiable from macOS: `build-windows.yml` is the test.

### macOS

Expand Down Expand Up @@ -1611,7 +1636,8 @@ Notes:
| `Scripts/update-version.sh` | Update version across all files (`--app X.Y.Z --deps X.Y.Z --deps-tag deps-vX.Y.Z`) |
| `Scripts/check-deps-changed.sh` | Detect if deps changed since last release (exit 0=changed, 1=unchanged) |
| `Scripts/package-macos.sh` | Package macOS app |
| `Scripts/package-windows.ps1` | Package Windows app |
| `Scripts/package-windows.ps1` | Package Windows app (installer + zip) |
| `Scripts/build-windows-installer.ps1` | Compile the Inno Setup installer from an assembled package dir |
| `Scripts/package-linux.sh` | Package Linux app (AppImage + `.zsync` + tarball) |
| `Scripts/package-deps-macos.sh` | Package macOS deps |
| `Scripts/package-deps-windows.ps1` | Package Windows deps |
Expand Down
12 changes: 8 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ VapourBox runs [VapourSynth](https://www.vapoursynth.com/), QTGMC and FFmpeg —
|----------|------------|------|
| macOS (Apple Silicon) | macOS 15 Sequoia | `VapourBox-x.x.x-macos-arm64.dmg` |
| macOS (Intel) | macOS 12 Monterey | `VapourBox-x.x.x-macos-x64.dmg` |
| Windows (x64) | Windows 10/11 | `VapourBox-x.x.x-windows-x64.zip` |
| Windows (x64) | Windows 10/11 | `VapourBox-x.x.x-windows-x64-setup.exe` |
| Linux (x64) | glibc 2.39 (Ubuntu 24.04, Debian 13) | `VapourBox-x.x.x-linux-x64.AppImage` |
| Linux (arm64) | glibc 2.39 (Ubuntu 24.04, Debian 13) | `VapourBox-x.x.x-linux-arm64.AppImage` |

Expand All @@ -58,11 +58,15 @@ VapourBox is signed with an Apple Developer ID and notarized, so it opens withou
<details>
<summary><b>Installing on Windows</b></summary>

1. Download the `.zip`.
2. Extract it anywhere (e.g. `C:\VapourBox`).
3. Run `vapourbox.exe`.
1. Download the `-setup.exe` installer and run it.
2. The installer isn't code-signed, so Windows SmartScreen may say it "protected your PC" — choose **More info → Run anyway**.
3. Launch VapourBox from the Start menu.
4. First launch downloads its processing dependencies (~145 MB).

It installs for the current user only (under `%LOCALAPPDATA%\Programs\VapourBox`), so it never asks for administrator rights. Running a newer installer upgrades in place and keeps the downloaded dependencies; uninstalling from **Settings → Apps** removes them too. Your saved presets are left alone.

**Portable option:** each release also carries a `.zip` of the same build. Extract it anywhere you can write to (e.g. `C:\VapourBox`, not `Program Files`) and run `vapourbox.exe`.

</details>

<details>
Expand Down
72 changes: 72 additions & 0 deletions Scripts/build-windows-installer.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
# Build the VapourBox Windows installer (Inno Setup) from an assembled package
# directory - the same tree the .zip is made from.
#
# Called by package-windows.ps1 and by build-windows.yml, so there is one
# definition of the installer however the package directory was assembled.
#
# Prerequisites:
# - Inno Setup 6 (https://jrsoftware.org/isdl.php). ISCC.exe is found on PATH,
# in the default install locations, or via $env:ISCC.
#
# Usage: .\Scripts\build-windows-installer.ps1 -Version "0.1.0" -SourceDir dist\VapourBox-0.1.0-windows-x64

param(
[Parameter(Mandatory = $true)][string]$Version,
[Parameter(Mandatory = $true)][string]$SourceDir,
[string]$OutputDir = ""
)

$ErrorActionPreference = "Stop"

$ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
$ProjectRoot = Split-Path -Parent $ScriptDir
$IssFile = Join-Path $ProjectRoot "packaging\windows\vapourbox.iss"
if (-not $OutputDir) { $OutputDir = Join-Path $ProjectRoot "dist" }

if (-not (Test-Path $SourceDir)) { throw "Package directory not found: $SourceDir" }
$SourceDir = (Resolve-Path $SourceDir).Path
New-Item -ItemType Directory -Force -Path $OutputDir | Out-Null
$OutputDir = (Resolve-Path $OutputDir).Path

# An installer built from an incomplete tree installs cleanly and then fails
# on the first video dropped, so check for the pieces before compiling.
foreach ($required in @(
"vapourbox.exe",
"vapourbox-worker.exe",
"templates\pipeline_template.vpy",
"templates\preview_template.vpy",
"templates\pipe_source.py",
"data\flutter_assets")) {
if (-not (Test-Path (Join-Path $SourceDir $required))) {
throw "Package directory is incomplete: $required is missing from $SourceDir"
}
}

$Iscc = $null
$Candidates = @(
$env:ISCC,
(Get-Command "ISCC.exe" -ErrorAction SilentlyContinue | Select-Object -ExpandProperty Source -First 1),
"${env:ProgramFiles(x86)}\Inno Setup 6\ISCC.exe",
"$env:ProgramFiles\Inno Setup 6\ISCC.exe",
"$env:LOCALAPPDATA\Programs\Inno Setup 6\ISCC.exe"
)
foreach ($candidate in $Candidates) {
if ($candidate -and (Test-Path $candidate)) { $Iscc = $candidate; break }
}
if (-not $Iscc) {
throw "Inno Setup 6 (ISCC.exe) not found. Install it from https://jrsoftware.org/isdl.php, or set `$env:ISCC to its path."
}

$Installer = Join-Path $OutputDir "VapourBox-$Version-windows-x64-setup.exe"
if (Test-Path $Installer) { Remove-Item $Installer }

Write-Host "Building installer with $Iscc" -ForegroundColor Yellow
& $Iscc /Qp "/DAppVersion=$Version" "/DSourceDir=$SourceDir" "/DOutputDir=$OutputDir" $IssFile
if ($LASTEXITCODE -ne 0) { throw "Inno Setup failed with exit code $LASTEXITCODE" }
if (-not (Test-Path $Installer)) { throw "Inno Setup reported success but $Installer was not created" }

$SizeMb = [math]::Round((Get-Item $Installer).Length / 1MB, 1)
$Sha256 = (Get-FileHash -Algorithm SHA256 $Installer).Hash.ToLower()
Write-Host "Installer: $Installer" -ForegroundColor Green
Write-Host "Size: $SizeMb MB"
Write-Host "SHA256: $Sha256"
4 changes: 2 additions & 2 deletions Scripts/ci-build-and-release.sh
Original file line number Diff line number Diff line change
Expand Up @@ -185,7 +185,7 @@ echo " Downloading Linux artifact..."
gh run download "$LINUX_RUN_ID" --dir "$DOWNLOAD_DIR"

echo " Downloaded artifacts:"
find "$DOWNLOAD_DIR" -type f \( -name "*.zip" -o -name "*.dmg" -o -name "*.tar.gz" -o -name "*.AppImage" -o -name "*.AppImage.zsync" -o -name "*.sha256.json" \) | while read -r f; do
find "$DOWNLOAD_DIR" -type f \( -name "*.zip" -o -name "*.dmg" -o -name "*.tar.gz" -o -name "*-setup.exe" -o -name "*.AppImage" -o -name "*.AppImage.zsync" -o -name "*.sha256.json" \) | while read -r f; do
SIZE=$(du -sh "$f" | cut -f1)
echo " $(basename "$f") ($SIZE)"
done
Expand All @@ -195,7 +195,7 @@ echo ""
echo -e "${BLUE}[4/4] Uploading to release ${RELEASE_TAG}...${NC}"

UPLOAD_COUNT=0
find "$DOWNLOAD_DIR" -type f \( -name "*.zip" -o -name "*.dmg" -o -name "*.tar.gz" -o -name "*.AppImage" -o -name "*.AppImage.zsync" -o -name "*.sha256.json" \) | while read -r f; do
find "$DOWNLOAD_DIR" -type f \( -name "*.zip" -o -name "*.dmg" -o -name "*.tar.gz" -o -name "*-setup.exe" -o -name "*.AppImage" -o -name "*.AppImage.zsync" -o -name "*.sha256.json" \) | while read -r f; do
BASENAME=$(basename "$f")
echo " Uploading $BASENAME..."
gh release upload "$RELEASE_TAG" "$f" --clobber
Expand Down
20 changes: 16 additions & 4 deletions Scripts/package-windows.ps1
Original file line number Diff line number Diff line change
@@ -1,16 +1,19 @@
# Package VapourBox App for Windows (without dependencies)
# Creates a standalone zip file with the app only
# Creates an installer (the primary download) and a standalone zip (the
# portable option) from the same tree, with the app only.
# Dependencies are packaged separately with package-deps-windows.ps1
#
# Prerequisites:
# - Flutter SDK installed
# - Rust toolchain installed
# - Inno Setup 6, for the installer (or pass -SkipInstaller)
#
# Usage: .\Scripts\package-windows.ps1 -Version "0.1.0"
# Usage: .\Scripts\package-windows.ps1 -Version "0.1.0" [-SkipBuild] [-SkipInstaller]

param(
[string]$Version = "1.0.0",
[switch]$SkipBuild = $false
[switch]$SkipBuild = $false,
[switch]$SkipInstaller = $false
)

$ErrorActionPreference = "Stop"
Expand Down Expand Up @@ -157,6 +160,14 @@ if (Test-Path $ZipFile) {
}
Compress-Archive -Path "$PackageDir\*" -DestinationPath $ZipFile -CompressionLevel Optimal

# Create installer
if (-not $SkipInstaller) {
Write-Host "Creating installer..." -ForegroundColor Yellow
& (Join-Path $ScriptDir "build-windows-installer.ps1") -Version $Version -SourceDir $PackageDir -OutputDir $DistDir
} else {
Write-Host "Skipping installer (-SkipInstaller)" -ForegroundColor Gray
}

# Calculate sizes
$PackageSize = (Get-ChildItem -Recurse $PackageDir | Measure-Object -Property Length -Sum).Sum / 1MB
$ZipSize = (Get-Item $ZipFile).Length / 1MB
Expand All @@ -170,5 +181,6 @@ Write-Host ""
Write-Host "Package size: $([math]::Round($PackageSize, 1)) MB"
Write-Host "Zip size: $([math]::Round($ZipSize, 1)) MB"
Write-Host ""
Write-Host "To distribute, share the zip file:"
Write-Host "To distribute, share the installer (VapourBox-$Version-windows-x64-setup.exe)"
Write-Host "or the zip file:"
Write-Host " $ZipFile"
6 changes: 3 additions & 3 deletions Scripts/release.sh
Original file line number Diff line number Diff line change
Expand Up @@ -303,14 +303,14 @@ RELEASE_NOTES="## VapourBox $APP_VERSION
- TODO: Add release notes

### Downloads
- **Windows**: \`VapourBox-$APP_VERSION-windows-x64.zip\`
- **Windows**: \`VapourBox-$APP_VERSION-windows-x64-setup.exe\` (or the portable \`.zip\`)
- **macOS (Apple Silicon)**: \`VapourBox-$APP_VERSION-macos-arm64.zip\` (contains signed & notarized DMG)
- **macOS (Intel)**: \`VapourBox-$APP_VERSION-macos-x64.zip\` (contains signed & notarized DMG)
- **Linux (x64)**: \`VapourBox-$APP_VERSION-linux-x64.AppImage\` (or the \`.tar.gz\`)
- **Linux (arm64)**: \`VapourBox-$APP_VERSION-linux-arm64.AppImage\` (or the \`.tar.gz\`)

### Installation
- **Windows**: Extract zip and run \`vapourbox.exe\`
- **Windows**: Run the installer — or extract the zip and run \`vapourbox.exe\`
- **macOS**: Open the DMG and drag VapourBox to Applications
- **Linux**: \`chmod +x\` the AppImage and run it — or extract the tarball and run \`./vapourbox\`

Expand Down Expand Up @@ -345,7 +345,7 @@ echo -e "${GREEN}╚════════════════════
echo ""
echo "Next steps:"
echo " 1. Wait for Windows and Linux builds to complete (if using GitHub Actions)"
echo " 2. Upload the Windows zip and the Linux AppImages (each with its .zsync) and tarballs to the draft release"
echo " 2. Upload the Windows installer and zip, and the Linux AppImages (each with its .zsync) and tarballs to the draft release"
echo " 3. Test the release builds"
echo " 4. Edit release notes"
echo " 5. Publish the release"
Expand Down
57 changes: 57 additions & 0 deletions app/test/packaging_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,63 @@ void main() {
});
}

// The Windows installer is built from the same tree as the zip, by a script
// of its own that both the local packaging script and CI call.
group('Windows installer', () {
final iss = _read(root, ['packaging', 'windows', 'vapourbox.iss']);
final installer = _read(root, ['Scripts', 'build-windows-installer.ps1']);

test('installs per-user, because deps download beside the executable', () {
// dependency_manager.dart puts deps\ next to the exe on Windows; an
// install under Program Files makes that download fail.
final deps =
_read(root, ['app', 'lib', 'services', 'dependency_manager.dart']);
expect(deps, contains("path.join(appDir, 'deps', 'windows-x64')"),
reason: 'if deps no longer live beside the exe, the per-user '
'restriction below can be revisited');
expect(iss, contains('PrivilegesRequired=lowest'));
expect(iss, isNot(contains('PrivilegesRequiredOverridesAllowed')));
});

test('uninstall removes what the app downloaded; upgrade does not', () {
final uninstall = iss.split('[UninstallDelete]').last;
expect(uninstall, contains(r'{app}\deps'));
expect(uninstall, contains(r'{app}\addons'));
final installDelete =
iss.split('[InstallDelete]').last.split('[Files]').first;
expect(installDelete, isNot(contains(r'{app}\deps')));
expect(installDelete, isNot(contains(r'{app}\addons')));
expect(installDelete, contains(r'{app}\templates'));
});

test('one filename across the .iss, the script, CI and the upload', () {
expect(
iss,
contains(
'OutputBaseFilename={#AppName}-{#AppVersion}-windows-x64-setup'));
expect(installer, contains(r'VapourBox-$Version-windows-x64-setup.exe'));

final workflow =
_read(root, ['.github', 'workflows', 'build-windows.yml']);
expect(workflow, contains('build-windows-installer.ps1'));
expect(workflow, contains('-windows-x64-setup.exe\n'));
expect(workflow, contains('-windows-x64.zip\n'));

final local = _read(root, ['Scripts', 'package-windows.ps1']);
expect(local, contains('build-windows-installer.ps1'));

final upload = _read(root, ['Scripts', 'ci-build-and-release.sh']);
final finds = upload
.split('\n')
.where((l) => l.startsWith('find ') && l.contains('*.zip'))
.toList();
expect(finds, isNotEmpty);
for (final line in finds) {
expect(line, contains('"*-setup.exe"'));
}
});
});

// The Linux AppImage embeds update information naming a `.zsync` on the
// latest GitHub release. Three files have to agree on that name, and nothing
// fails at build time if they don't — the updater just finds nothing.
Expand Down
13 changes: 11 additions & 2 deletions docs/BUILDING.md
Original file line number Diff line number Diff line change
Expand Up @@ -262,9 +262,18 @@ the Windows resource compiler read them at build time.

### Windows
```powershell
.\Scripts\package-windows.ps1 -Version "1.0.0" [-SkipBuild]
.\Scripts\package-windows.ps1 -Version "1.0.0" [-SkipBuild] [-SkipInstaller]
```
Output: `dist/VapourBox-1.0.0-windows-x64.zip`
Output, both from the same tree:

- `dist/VapourBox-1.0.0-windows-x64-setup.exe` — the installer, the primary download
- `dist/VapourBox-1.0.0-windows-x64.zip` — the portable option

The installer needs [Inno Setup 6](https://jrsoftware.org/isdl.php) (`ISCC.exe` on
`PATH`, in its default location, or named by `$env:ISCC`); `-SkipInstaller` builds
the zip alone. It is defined by `packaging/windows/vapourbox.iss` and compiled by
`Scripts/build-windows-installer.ps1`, which can also be run directly against an
already-assembled package directory.

### Linux
```bash
Expand Down
Loading
Loading