Skip to content

Build Installer

Build Installer #34

name: Build Installer
# Same as build.yml (native + Rust + AOT publish for x64 and ARM64) but with an
# extra step that builds the WiX MSI installer from the publish output and
# uploads it as an artifact. Triggered manually from the Actions tab.
on:
workflow_dispatch:
inputs:
platform:
description: Platform(s) to build
type: choice
default: Both
options:
- Both
- x64
- ARM64
native:
description: Native DLLs (vcpkg + C++ + Rust)
type: choice
default: Build fresh
options:
- Build fresh
- Use committed (skip native build)
jobs:
# Turn the "platform" choice into the build matrix. The matrix context is not
# available in a job-level `if:`, so we compute the matrix here and the build
# job consumes it via fromJSON -- that way an UN-selected platform's runner
# never even starts (vs. starting and skipping every step).
setup:
name: Select platform(s)
runs-on: ubuntu-latest
outputs:
matrix: ${{ steps.pick.outputs.matrix }}
steps:
- id: pick
shell: pwsh
run: |
$x64 = '{"runner":"windows-latest","platform":"x64","rid":"win-x64","vcpkg-triplet":"x64-windows-release","install-root":"vcpkg_installed_x64","rust-target":"x86_64-pc-windows-msvc","publish-profile":"win-x64","toolset":"v145"}'
$arm = '{"runner":"windows-11-arm","platform":"ARM64","rid":"win-arm64","vcpkg-triplet":"arm64-windows-release","install-root":"vcpkg_installed_arm64","rust-target":"aarch64-pc-windows-msvc","publish-profile":"win-ARM64","toolset":"v143"}'
$items = switch ("${{ inputs.platform }}") {
'x64' { $x64 }
'ARM64' { $arm }
default { "$x64,$arm" }
}
Add-Content -Path $env:GITHUB_OUTPUT -Value "matrix={`"include`":[$items]}"
build:
needs: setup
strategy:
# Run the selected platform(s) in parallel. If one fails, let the other
# finish so you get failure information for both at the same time.
fail-fast: false
matrix: ${{ fromJSON(needs.setup.outputs.matrix) }}
runs-on: ${{ matrix.runner }}
name: Build Installer (${{ matrix.platform }})
env:
# vcpkg binary cache: stores compressed pre-built packages so vcpkg
# install is a cache-hit on repeat runs instead of a full recompile.
# "clear" resets default sources; "files,<path>,readwrite" adds our
# local folder as the only source.
VCPKG_BINARY_SOURCES: "clear;files,${{ github.workspace }}/vcpkg_cache,readwrite"
steps:
# -----------------------------------------------------------------------
# Checkout
# -----------------------------------------------------------------------
# This brings in the committed overlay port files at
# Src/build_libheif/ports/libheif/ and the custom triplet at
# Src/build_libheif/triplets/arm64-windows-release.cmake.
# Those files are what tell vcpkg to use dav1d instead of aom.
# -----------------------------------------------------------------------
- name: Checkout
uses: actions/checkout@v4
# -----------------------------------------------------------------------
# Toolchain setup
# -----------------------------------------------------------------------
- name: Setup .NET 10
uses: actions/setup-dotnet@v4
with:
dotnet-version: '10.0.x'
- name: Setup MSBuild
uses: microsoft/setup-msbuild@v2
# Install the stable Rust toolchain and pre-add the cross-compilation
# target for this platform. On windows-latest the host IS x86_64 so
# the target is native; on windows-11-arm the host IS aarch64 so same.
#
# Everything from here through "Assemble External binaries" is the native
# build; it is skipped when the "native" input is set to reuse the DLLs
# already committed under Src/FlyPhotos/External/<Platform>/.
- name: Setup Rust
if: ${{ inputs.native == 'Build fresh' }}
uses: dtolnay/rust-toolchain@stable
with:
targets: ${{ matrix.rust-target }}
# -----------------------------------------------------------------------
# Caches
# -----------------------------------------------------------------------
# vcpkg binary cache -- keyed on the manifest + overlay port files.
# A change to any port file or vcpkg.json invalidates the cache and
# triggers a full recompile of the affected packages.
- name: Cache vcpkg binary packages
if: ${{ inputs.native == 'Build fresh' }}
uses: actions/cache@v4
with:
path: vcpkg_cache
key: vcpkg-${{ matrix.vcpkg-triplet }}-${{ hashFiles('Src/build_libheif/vcpkg.json', 'Src/build_libheif/vcpkg-configuration.json', 'Src/build_libheif/ports/libheif/**', 'Src/build_libheif/triplets/**') }}
restore-keys: |
vcpkg-${{ matrix.vcpkg-triplet }}-
# Rust build cache -- caches ~/.cargo/registry (downloaded crate source)
# and the target/ directory (compiled artifacts). Keyed automatically
# by Cargo.lock + rust-target so a dependency change invalidates it.
- name: Cache Rust build
if: ${{ inputs.native == 'Build fresh' }}
uses: Swatinem/rust-cache@v2
with:
workspaces: Src/fly_rust_bridge
key: ${{ matrix.rust-target }}
# -----------------------------------------------------------------------
# Step 1 -- vcpkg: heif.dll, dav1d.dll, libde265.dll, libpng16.dll
# -----------------------------------------------------------------------
# The overlay ports at Src/build_libheif/ports/ contain the patched
# libheif port that adds dav1d support (see how_to_patch_heif.md).
# --overlay-ports tells vcpkg to use those files instead of the built-in
# port registry -- without this flag vcpkg would build against aom.
# --overlay-triplets provides the custom arm64-windows-release triplet
# (vcpkg ships x64-windows-release built-in but not the ARM64 equivalent).
# --x-install-root isolates each platform's packages so they don't
# interfere with each other when both platforms run in parallel.
- name: Install vcpkg dependencies
if: ${{ inputs.native == 'Build fresh' }}
working-directory: Src/build_libheif
run: |
vcpkg install `
--triplet ${{ matrix.vcpkg-triplet }} `
--x-manifest-root=. `
--x-install-root=${{ matrix.install-root }} `
--overlay-ports=ports `
--overlay-triplets=triplets
# -----------------------------------------------------------------------
# Step 2 -- Native C++: FlyNativeLib.dll, FlyNativeLibHeif.dll,
# FlyContextMenuHelper.exe
# -----------------------------------------------------------------------
# PlatformToolset is per-runner (matrix.toolset): the windows-latest x64
# runner is on Visual Studio 2026 (v145), but windows-11-arm is still on
# Visual Studio 2022 (newest toolset v143) and has no v145. ATL ships with
# both images for their default toolset, so each compiles with its own.
# Build order matters: FlyNativeLib has no vcpkg dependency and must be
# built before FlyNativeLibHeif which links against heif.lib.
# Output lands in Src/<Platform>/Release/ (not Src/<Project>/<Platform>/Release/).
# The .vcxproj OutDir is $(ProjectDir)\..\$(Platform)\$(Configuration)\.
- name: Build FlyNativeLib
if: ${{ inputs.native == 'Build fresh' }}
working-directory: Src/FlyNativeLib
run: |
msbuild FlyNativeLib.vcxproj `
/p:Configuration=Release `
/p:Platform=${{ matrix.platform }} `
/p:PlatformToolset=${{ matrix.toolset }}
- name: Build FlyNativeLibHeif
if: ${{ inputs.native == 'Build fresh' }}
working-directory: Src/FlyNativeLibHeif
run: |
msbuild FlyNativeLibHeif.vcxproj `
/p:Configuration=Release `
/p:Platform=${{ matrix.platform }} `
/p:PlatformToolset=${{ matrix.toolset }}
- name: Build FlyContextMenuHelper
if: ${{ inputs.native == 'Build fresh' }}
working-directory: Src/FlyContextMenuHelper
run: |
msbuild FlyContextMenuHelper.vcxproj `
/p:Configuration=Release `
/p:Platform=${{ matrix.platform }} `
/p:PlatformToolset=${{ matrix.toolset }}
# -----------------------------------------------------------------------
# Step 3 -- Rust: fly_rust_bridge.dll (RAW decode + SVG render)
# -----------------------------------------------------------------------
# --release activates the optimised profile in Cargo.toml:
# opt-level=3, lto=true, codegen-units=1, panic=abort, strip=true.
# --target must be specified or cargo builds for the host and the DLL
# lands in target/release/ (no triplet subfolder) -- easy to mis-deploy.
- name: Build fly_rust_bridge
if: ${{ inputs.native == 'Build fresh' }}
working-directory: Src/fly_rust_bridge
run: |
cargo build --release --target ${{ matrix.rust-target }}
# -----------------------------------------------------------------------
# Step 4 -- Assemble External\<Platform>
# -----------------------------------------------------------------------
# Copies all required DLLs into the one folder that FlyPhotos.csproj
# reads from via: <ExternalBinaries Include="External\$(Platform)\*.*" />
# The FlattenDllsOnPublish MSBuild target then copies them into the
# publish root during dotnet publish.
#
# IMPORTANT: we do NOT clean the folder first. We copy each file
# individually so that if any copy source is missing the step fails
# with a clear error rather than silently succeeding with a partial set.
- name: Assemble External binaries
if: ${{ inputs.native == 'Build fresh' }}
shell: pwsh
run: |
$platform = "${{ matrix.platform }}"
$triplet = "${{ matrix.vcpkg-triplet }}"
$installRoot = "Src/build_libheif/${{ matrix.install-root }}"
$rustTarget = "${{ matrix.rust-target }}"
$externalDir = "Src/FlyPhotos/External/$platform"
New-Item -ItemType Directory -Path $externalDir -Force | Out-Null
# vcpkg DLLs: heif.dll, dav1d.dll, libde265.dll, libpng16.dll
$vcpkgBinDir = "$installRoot/$triplet/bin"
if (-not (Test-Path $vcpkgBinDir)) {
Write-Error "vcpkg bin dir not found: $vcpkgBinDir"
exit 1
}
Get-ChildItem "$vcpkgBinDir/*.dll" | Copy-Item -Destination $externalDir -Force
# Native C++ DLLs (OutDir = Src\<Platform>\Release\)
Copy-Item "Src/$platform/Release/FlyNativeLib.dll" -Destination $externalDir -Force
Copy-Item "Src/$platform/Release/FlyNativeLibHeif.dll" -Destination $externalDir -Force
Copy-Item "Src/$platform/Release/FlyContextMenuHelper.exe" -Destination $externalDir -Force
# Rust DLL
Copy-Item "Src/fly_rust_bridge/target/$rustTarget/release/fly_rust_bridge.dll" `
-Destination $externalDir -Force
# -----------------------------------------------------------------------
# Step 5 -- Verify External before publishing
# -----------------------------------------------------------------------
# Fail loudly here rather than producing a publish artifact that ships
# without a required DLL. dotnet publish would succeed but the app
# would crash at runtime when it tries to P/Invoke into the missing DLL.
- name: Verify External binaries
shell: pwsh
run: |
$externalDir = "Src/FlyPhotos/External/${{ matrix.platform }}"
$expected = @(
"heif.dll", "dav1d.dll", "libde265.dll", "libpng16.dll",
"FlyNativeLib.dll", "FlyNativeLibHeif.dll", "FlyContextMenuHelper.exe",
"fly_rust_bridge.dll"
)
$failed = $false
foreach ($f in $expected) {
if (Test-Path "$externalDir/$f") {
Write-Host " [OK] $f" -ForegroundColor Green
} else {
Write-Host " [MISSING] $f" -ForegroundColor Red
$failed = $true
}
}
if ($failed) { exit 1 }
# -----------------------------------------------------------------------
# Step 6 -- dotnet publish (AOT, self-contained, trimmed)
# -----------------------------------------------------------------------
# The publish profile is selected from the csproj property:
# <PublishProfile>Properties\PublishProfiles\win-$(Platform).pubxml</PublishProfile>
# The profile writes output to FlyPhotos/bin/win-x64/publish/ (or win-arm64).
# We do NOT use -o to override this -- the installer harvests the profile
# output path and would break if it changed.
#
# The RemoveAIMLFiles MSBuild target (defined in FlyPhotos.csproj) runs
# after publish and strips large Windows AI/ML DLLs that WindowsAppSDK
# includes but FlyPhotos does not use (~150 MB removed).
- name: Publish
working-directory: Src
run: |
dotnet publish FlyPhotos/FlyPhotos.csproj `
-c Release `
/p:Platform=${{ matrix.platform }} `
/p:PublishProfile=Properties/PublishProfiles/${{ matrix.publish-profile }}.pubxml
# -----------------------------------------------------------------------
# Step 7 -- Build the WiX MSI installer
# -----------------------------------------------------------------------
# Builds FlyPhotosSetup.wixproj (WiX v5, SDK-style) from the Step 6 publish
# output. No devenv -- `dotnet build` restores the WiX SDK from NuGet. The MSI
# version is the wixproj's own <BuildVersion> string. Output lands in
# Src/FlyPhotosSetup/bin/<Platform>/Release/FlyPhotosInstaller_<version>_<Platform>.msi.
- name: Build MSI installer
working-directory: Src
run: |
dotnet build FlyPhotosSetup/FlyPhotosSetup.wixproj `
-c Release `
/p:Platform=${{ matrix.platform }}
# -----------------------------------------------------------------------
# Step 8 -- Upload artifacts (publish folder + MSI)
# -----------------------------------------------------------------------
- name: Upload publish artifact
uses: actions/upload-artifact@v4
with:
name: FlyPhotos-${{ matrix.platform }}
path: Src/FlyPhotos/bin/${{ matrix.rid }}/publish/
# The wixproj names the output FlyPhotosInstaller_<version>_<platform>.msi
# (version from its <BuildVersion>); match it by pattern. On a fresh runner the
# Release folder is clean, so exactly one MSI matches.
- name: Upload MSI artifact
uses: actions/upload-artifact@v4
with:
name: FlyPhotos-${{ matrix.platform }}-MSI
path: Src/FlyPhotosSetup/bin/${{ matrix.platform }}/Release/FlyPhotosInstaller_*_${{ matrix.platform }}.msi