# Windows on ARM (#61): package the ARM64 release the way release.yml does (cross-compiled on an # x64 runner, unsigned here), then install that MSI on a Windows 11 ARM64 runner and run it, and # run the test suite natively on ARM64. name: Windows ARM64 on: push: branches: [main] pull_request: paths: - Cargo.toml - Cargo.lock - "**/Cargo.toml" - "**/build.rs" - .github/workflows/windows-arm64.yml - packaging/windows/** schedule: - cron: "41 4 * * 1" # weekly, so drift in the runner images shows up workflow_dispatch: concurrency: group: windows-arm64-${{ github.ref }} cancel-in-progress: true permissions: contents: read env: CARGO_TERM_COLOR: always CARGO_INCREMENTAL: 0 RUST_BACKTRACE: 1 jobs: package: name: package (cross-compiled on x64) runs-on: windows-latest timeout-minutes: 90 steps: - uses: actions/checkout@v4 - uses: dtolnay/rust-toolchain@stable with: targets: aarch64-pc-windows-msvc - uses: Swatinem/rust-cache@v2 with: key: windows-arm64-package - name: Install WiX shell: pwsh run: dotnet tool install --global wix --version 5.0.2 - name: Build and package (unsigned) shell: pwsh env: DIST: ${{ github.workspace }}\dist\arm64 run: ./packaging/windows/package.ps1 -Arch arm64 - uses: actions/upload-artifact@v4 with: name: pdfcraft-windows-arm64 path: dist/arm64/* if-no-files-found: error retention-days: 7 install: name: install and run on ARM64 needs: package runs-on: windows-11-arm timeout-minutes: 30 steps: - uses: actions/download-artifact@v4 with: name: pdfcraft-windows-arm64 path: dist - name: Install the MSI, check it, run it, uninstall it, reinstall without the desktop shortcut shell: pwsh run: | $ErrorActionPreference = 'Stop' $msi = (Get-ChildItem dist/*.msi | Select-Object -First 1).FullName # A command-line override must not put files in a user profile while registering # their handlers machine-wide (#305). This runs on a disposable CI runner. $p = Start-Process msiexec.exe -WindowStyle Hidden -ArgumentList '/i', "`"$msi`"", '/qn', 'ALLUSERS=2', 'MSIINSTALLPERUSER=1', '/l*v', 'per-user.log' -Wait -PassThru if ($p.ExitCode -ne 1603) { Get-Content per-user.log -Tail 60; throw "per-user install should fail with 1603, got $($p.ExitCode)" } if (-not (Select-String -Path per-user.log -SimpleMatch 'PdfCraft must be installed for all users.')) { Get-Content per-user.log -Tail 60; throw 'per-user install did not fail on the scope guard' } $p = Start-Process msiexec.exe -ArgumentList '/i', "`"$msi`"", '/qn', '/l*v', 'install.log' -Wait -PassThru if ($p.ExitCode -ne 0) { Get-Content install.log -Tail 60; throw "msiexec /i exited $($p.ExitCode)" } $shortcuts = @( (Join-Path ([Environment]::GetFolderPath('CommonPrograms')) 'PdfCraft.lnk'), (Join-Path ([Environment]::GetFolderPath('CommonDesktopDirectory')) 'PdfCraft.lnk') ) # Plain shortcuts to the program (#143), not advertised ones, so TargetPath is the exe. $exePath = Join-Path $env:ProgramFiles 'PdfCraft\pdfcraft.exe' $wsh = New-Object -ComObject WScript.Shell foreach ($shortcut in $shortcuts) { if (-not (Test-Path -LiteralPath $shortcut)) { throw "installer did not create $shortcut" } $target = $wsh.CreateShortcut($shortcut).TargetPath if ($target -ne $exePath) { throw "$shortcut points to '$target', not $exePath" } Write-Output "ok $shortcut -> $target" } $dir = Join-Path $env:ProgramFiles 'PdfCraft' foreach ($exe in 'pdfcraft.exe', 'pdfcraft-cli.exe') { $path = Join-Path $dir $exe if (-not (Test-Path $path)) { Get-ChildItem -Recurse $env:ProgramFiles -Filter $exe | Select-Object -First 3; throw "$exe not installed in $dir" } $bytes = [System.IO.File]::ReadAllBytes($path) $machine = [BitConverter]::ToUInt16($bytes, [BitConverter]::ToInt32($bytes, 0x3C) + 4) if ($machine -ne 0xAA64) { throw "$exe is for machine 0x$('{0:X}' -f $machine), not ARM64" } Write-Output "ok $path (ARM64)" } # The installed CLI runs natively: open, render and edit a small PDF through the engine. $cli = Join-Path $dir 'pdfcraft-cli.exe' & $cli tools | Out-Null if ($LASTEXITCODE -ne 0) { throw "pdfcraft-cli tools exited $LASTEXITCODE" } $body = 'BT /F1 24 Tf 20 150 Td (Hello ARM64) Tj ET' $objs = @('<< /Type /Catalog /Pages 2 0 R >>', '<< /Type /Pages /Kids [3 0 R] /Count 1 >>', '<< /Type /Page /Parent 2 0 R /MediaBox [0 0 300 200] /Contents 4 0 R /Resources << /Font << /F1 5 0 R >> >> >>', "<< /Length $($body.Length) >>`nstream`n$body`nendstream", '<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica >>') $sb = [System.Text.StringBuilder]::new("%PDF-1.7`n"); $offsets = @() for ($i = 0; $i -lt $objs.Count; $i++) { $offsets += $sb.Length; [void]$sb.Append("$($i + 1) 0 obj`n$($objs[$i])`nendobj`n") } $xref = $sb.Length [void]$sb.Append("xref`n0 $($objs.Count + 1)`n0000000000 65535 f `n") foreach ($o in $offsets) { [void]$sb.Append(('{0:D10} 00000 n `n' -f $o)) } [void]$sb.Append("trailer`n<< /Size $($objs.Count + 1) /Root 1 0 R >>`nstartxref`n$xref`n%%EOF`n") New-Item -ItemType Directory -Force smoke | Out-Null [System.IO.File]::WriteAllText("$PWD\smoke\hello.pdf", $sb.ToString(), [System.Text.Encoding]::ASCII) $steps = '[{"tool":"doc_open","args":{"path":"hello.pdf"}},{"tool":"text_paragraphs","args":{"doc":1,"page":1}},{"tool":"text_edit","args":{"doc":1,"page":1,"paragraph":1,"text":"Edited on ARM64"}},{"tool":"page_render","args":{"doc":1,"page":1,"dpi":72},"out":"page.png"},{"tool":"doc_save","args":{"doc":1,"path":"out.pdf"}}]' [System.IO.File]::WriteAllText("$PWD\smoke\steps.json", $steps) $out = & $cli run --script smoke\steps.json --root smoke 2>&1 | Out-String if ($LASTEXITCODE -ne 0) { Write-Output $out; throw "pdfcraft-cli run exited $LASTEXITCODE" } if ($out -notmatch 'Hello ARM64') { Write-Output $out; throw 'text_paragraphs did not read the page' } foreach ($f in 'smoke\page.png', 'smoke\out.pdf') { if (-not (Test-Path $f) -or (Get-Item $f).Length -eq 0) { Write-Output $out; throw "$f was not written" } } Write-Output 'ok pdfcraft-cli: opened, read, edited, rendered and saved a PDF on ARM64' $p = Start-Process msiexec.exe -ArgumentList '/x', "`"$msi`"", '/qn' -Wait -PassThru if ($p.ExitCode -ne 0) { throw "msiexec /x exited $($p.ExitCode)" } if (Test-Path (Join-Path $dir 'pdfcraft.exe')) { throw 'uninstall left pdfcraft.exe behind' } foreach ($shortcut in $shortcuts) { if (Test-Path -LiteralPath $shortcut) { throw "uninstall left $shortcut behind" } } Write-Output 'ok uninstalled' # Unattended installs can opt out of the desktop shortcut (#143); the Start Menu one stays. $p = Start-Process msiexec.exe -ArgumentList '/i', "`"$msi`"", '/qn', 'INSTALLDESKTOPSHORTCUT=0', '/l*v', 'install-nodesktop.log' -Wait -PassThru if ($p.ExitCode -ne 0) { Get-Content install-nodesktop.log -Tail 60; throw "msiexec /i INSTALLDESKTOPSHORTCUT=0 exited $($p.ExitCode)" } if (-not (Test-Path -LiteralPath $shortcuts[0])) { throw "INSTALLDESKTOPSHORTCUT=0 also skipped $($shortcuts[0])" } if (Test-Path -LiteralPath $shortcuts[1]) { throw "INSTALLDESKTOPSHORTCUT=0 still created $($shortcuts[1])" } $p = Start-Process msiexec.exe -ArgumentList '/x', "`"$msi`"", '/qn' -Wait -PassThru if ($p.ExitCode -ne 0) { throw "msiexec /x exited $($p.ExitCode)" } if (Test-Path -LiteralPath $shortcuts[0]) { throw "uninstall left $($shortcuts[0]) behind" } Write-Output 'ok INSTALLDESKTOPSHORTCUT=0: Start Menu shortcut only' test: name: test (native ARM64) runs-on: windows-11-arm timeout-minutes: 120 steps: - uses: actions/checkout@v4 - uses: dtolnay/rust-toolchain@stable - uses: Swatinem/rust-cache@v2 with: key: windows-arm64-test - name: Test run: cargo test --workspace --locked