diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..31647dd --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,125 @@ +name: Release + +# Push a v* tag (matching the app versions) to build the macOS DMG and Windows MSI and +# attach them to a draft GitHub release. Review the draft, then publish it. +# Run manually (Actions -> Release -> Run workflow) to build the installers as workflow +# artifacts without touching releases; pass an existing tag to (re)build its draft release. + +on: + push: + tags: ['v*'] + # Packaging changes get a full installer build on their PR (artifacts only, no release). + pull_request: + paths: + - .github/workflows/release.yml + - CursorFollowMac/scripts/** + - CursorFollowWin/scripts/** + - CursorFollowWin/installer/** + - CursorFollowWin/CursorFollowWin.csproj + workflow_dispatch: + inputs: + tag: + description: Existing tag to build a draft release for (blank = artifacts only) + required: false + +permissions: + contents: read + +concurrency: + group: release-${{ github.ref }}-${{ inputs.tag }} + cancel-in-progress: false + +env: + TAG: ${{ github.event_name == 'push' && github.ref_name || inputs.tag }} + +jobs: + macos: + runs-on: macos-latest + steps: + - uses: actions/checkout@v4 + with: + ref: ${{ env.TAG || github.ref }} + + - name: Import self-signed signing identity + env: + P12_BASE64: ${{ secrets.MAC_SIGNING_P12 }} + P12_PASSWORD: ${{ secrets.MAC_SIGNING_P12_PASSWORD }} + run: | + if [ -z "$P12_BASE64" ]; then + if [ -n "$TAG" ]; then echo "::error::MAC_SIGNING_P12 secret is not set"; exit 1; fi + echo "::warning::No signing secret; building ad-hoc signed (artifacts only)" + echo "SIGN_IDENTITY=-" >> "$GITHUB_ENV" + exit 0 + fi + KC="$RUNNER_TEMP/signing.keychain-db" + KC_PASS="$(openssl rand -hex 24)" + security create-keychain -p "$KC_PASS" "$KC" + security set-keychain-settings -lut 21600 "$KC" + security unlock-keychain -p "$KC_PASS" "$KC" + echo "$P12_BASE64" | base64 --decode > "$RUNNER_TEMP/identity.p12" + security import "$RUNNER_TEMP/identity.p12" -k "$KC" -P "$P12_PASSWORD" -T /usr/bin/codesign + rm -f "$RUNNER_TEMP/identity.p12" + security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$KC_PASS" "$KC" >/dev/null + security list-keychains -d user -s "$KC" $(security list-keychains -d user | tr -d '"') + + - name: Build, sign, package, verify + run: ./CursorFollowMac/scripts/package.sh + + - uses: actions/upload-artifact@v4 + with: + name: macos-dmg + path: build/CursorFollowMac-*.dmg + if-no-files-found: error + + windows: + runs-on: windows-latest + steps: + - uses: actions/checkout@v4 + with: + ref: ${{ env.TAG || github.ref }} + - uses: actions/setup-dotnet@v4 + with: + dotnet-version: 10.0.x + - name: Build MSI (unsigned) + shell: pwsh + run: ./CursorFollowWin/scripts/package.ps1 -Rid win-x64 + - uses: actions/upload-artifact@v4 + with: + name: windows-msi + path: CursorFollowWin/installer/dist/*.msi + if-no-files-found: error + + release: + if: ${{ github.event_name == 'push' || inputs.tag != '' }} + needs: [macos, windows] + runs-on: ubuntu-latest + permissions: + contents: write + steps: + - uses: actions/checkout@v4 + with: + ref: ${{ env.TAG }} + - uses: actions/download-artifact@v4 + with: + path: dist + merge-multiple: true + - name: Check installer versions match the tag + run: | + v="${TAG#v}" + ls dist + ls dist/CursorFollowMac-"$v".dmg dist/CursorFollowWin-"$v"-win-x64.msi + - name: Release notes from CHANGELOG.md + run: | + v="${TAG#v}" + awk -v v="$v" '$0 == "## " v {f=1; next} /^## / {f=0} f' CHANGELOG.md > notes.md + test -s notes.md || { echo "::error::No '## $v' section in CHANGELOG.md"; exit 1; } + - name: Create or update draft release + env: + GH_TOKEN: ${{ github.token }} + run: | + if gh release view "$TAG" >/dev/null 2>&1; then + gh release upload "$TAG" dist/* --clobber + gh release edit "$TAG" --notes-file notes.md + else + gh release create "$TAG" dist/* --draft --verify-tag --title "CursorFollow ${TAG#v}" --notes-file notes.md + fi diff --git a/CHANGELOG.md b/CHANGELOG.md index 7897f51..4667cb1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -30,6 +30,7 @@ CursorFollow captures a target window and renders it into a stable, shareable wi - Auto-follow (cursor-under-window with foreground fallback), live frame telemetry, and single-instance guard. - Global hotkeys, Start-with-Windows, and capture-quality presets. - Distributed as a per-user **WiX MSI** installer (no admin required; clean upgrade and uninstall). +- Fixed: Quit could hang, leaving the process and output window running, if the Settings window had been opened ([#5](https://github.com/LoganRupe/CursorFollow/issues/5)). ### Linux diff --git a/CursorFollowMac/CursorFollowMac/OutputWindowController.swift b/CursorFollowMac/CursorFollowMac/OutputWindowController.swift index 476d25f..1d56a5c 100644 --- a/CursorFollowMac/CursorFollowMac/OutputWindowController.swift +++ b/CursorFollowMac/CursorFollowMac/OutputWindowController.swift @@ -386,7 +386,9 @@ private final class SurfaceLayerView: NSView { // MARK: - Overlay View /// Transparent passthrough host so clicks reach the surface view beneath. -private final class PassthroughHostingView: NSHostingView { +/// Deliberately non-generic: Xcode 26's optimizer crashes (EarlyPerfInliner) inlining the +/// deinit of a generic private NSHostingView subclass in Release builds. +private final class PassthroughHostingView: NSHostingView { override func hitTest(_ point: NSPoint) -> NSView? { nil } override var isOpaque: Bool { false } override func viewDidMoveToWindow() { diff --git a/CursorFollowWin/App.xaml.cs b/CursorFollowWin/App.xaml.cs index e531556..bb33486 100644 --- a/CursorFollowWin/App.xaml.cs +++ b/CursorFollowWin/App.xaml.cs @@ -169,6 +169,13 @@ private void ExitApplication() _outputWindow?.DisposeCapture(); _singleInstanceMutex?.Dispose(); AppLog.Shutdown(); + + // Close every window ourselves rather than relying on Application.Exit to do it. With the + // Settings window open (three windows), Exit closed only some of them and returned; the + // window it skipped kept the message loop, and so the process, alive (issue #5). + _settingsWindow?.Close(); + _panelWindow?.Close(); + _outputWindow?.Close(); Current.Exit(); } diff --git a/CursorFollowWin/CursorFollowWin.csproj b/CursorFollowWin/CursorFollowWin.csproj index c9bb839..a7a4292 100644 --- a/CursorFollowWin/CursorFollowWin.csproj +++ b/CursorFollowWin/CursorFollowWin.csproj @@ -25,7 +25,6 @@ 0.5.1 0.5.1.0 0.5.1.0 - 1.0 Logan Rupe CursorFollow Logan Rupe @@ -43,10 +42,6 @@ win-x64 - - win-$(Platform).pubxml - - @@ -112,6 +107,10 @@ + + true + true False True - ICE38;ICE43;ICE57;ICE61;ICE91 + + ICE03;ICE38;ICE43;ICE57;ICE61;ICE64;ICE91 diff --git a/CursorFollowWin/installer/Package.wxs b/CursorFollowWin/installer/Package.wxs index 71378a7..4e300c6 100644 --- a/CursorFollowWin/installer/Package.wxs +++ b/CursorFollowWin/installer/Package.wxs @@ -103,6 +103,8 @@ Installer has no single-value remove-on-uninstall, and we must not delete the shared Run key wholesale). RemoveDataFolder removes %LocalAppData%\CursorFollowWin (settings.json + Logs\). + RemoveInstallTree removes what is left of the install folder after RemoveFiles: the + per-language subfolders of the self-contained Windows App SDK. Impersonate=yes so HKCU / %LOCALAPPDATA% resolve to the installing user; Return=ignore so a missing key/folder never fails the uninstall. @@ -115,11 +117,17 @@ Directory="INSTALLFOLDER" ExeCommand="cmd.exe /c rmdir /s /q "%LOCALAPPDATA%\CursorFollowWin"" Execute="deferred" Impersonate="yes" Return="ignore" /> + + diff --git a/CursorFollowWin/scripts/publish.ps1 b/CursorFollowWin/scripts/publish.ps1 index 79013be..a0a7696 100644 --- a/CursorFollowWin/scripts/publish.ps1 +++ b/CursorFollowWin/scripts/publish.ps1 @@ -4,7 +4,7 @@ Produce a self-contained, ReadyToRun publish of CursorFollowWin for one runtime. .DESCRIPTION - Wraps `dotnet publish` with the project's publish profile. Trimming is disabled in the + Wraps `dotnet publish`; the self-contained/ReadyToRun settings live in the csproj. Trimming is disabled in the project (WinUI/XAML reflection is not trim-safe); this script just selects the RID and configuration and returns the absolute publish path so package.ps1 can harvest it. @@ -33,12 +33,12 @@ $platform = @{ 'win-x64' = 'x64'; 'win-x86' = 'x86'; 'win-arm64' = 'ARM64' }[$Ri Write-Host "==> Publishing $Configuration / $Rid (self-contained, ReadyToRun)" -ForegroundColor Cyan # Route dotnet's stdout to the host so it does not pollute this script's success stream # (callers capture the returned publish path). -dotnet publish $proj -c $Configuration -p:Platform=$platform -p:PublishProfile="win-$platform" --nologo | Out-Host -if ($LASTEXITCODE -ne 0) { throw "dotnet publish failed (exit $LASTEXITCODE)." } - -# Path mirrors the publish profile's PublishDir: bin\\\\publish +# Publish path: bin\\\\publish (pinned with -o; the platform-specific default +# would otherwise insert a bin\\ level). $tfm = 'net10.0-windows10.0.26100.0' $publishDir = Join-Path $repo "bin/$Configuration/$tfm/$Rid/publish" +dotnet publish $proj -c $Configuration -p:Platform=$platform -r $Rid -o $publishDir --nologo | Out-Host +if ($LASTEXITCODE -ne 0) { throw "dotnet publish failed (exit $LASTEXITCODE)." } if (-not (Test-Path $publishDir)) { throw "Publish folder not found: $publishDir" } $exe = Join-Path $publishDir 'CursorFollowWin.exe' @@ -47,6 +47,12 @@ if (-not (Test-Path $exe)) { throw "Published exe missing: $exe" } if (-not (Test-Path (Join-Path $publishDir 'CursorFollowWin.pri'))) { throw "Root resources PRI (CursorFollowWin.pri) missing from publish — the app would fail to load XAML." } +# Self-contained guard: without these the MSI would need a separately installed runtime. +foreach ($dll in 'coreclr.dll', 'Microsoft.WindowsAppRuntime.dll') { + if (-not (Test-Path (Join-Path $publishDir $dll))) { + throw "$dll missing from publish — the build is not self-contained." + } +} $resolved = (Resolve-Path $publishDir).Path Write-Host "==> Publish OK: $resolved" -ForegroundColor Green diff --git a/Docs/Distribution/Packaging.md b/Docs/Distribution/Packaging.md index 84188cf..ca95eb2 100644 --- a/Docs/Distribution/Packaging.md +++ b/Docs/Distribution/Packaging.md @@ -29,6 +29,13 @@ The Windows installer is covered in [CursorFollowWin/docs/Installer.md](../../Cu password and choose **Always Allow**. 3. Optional, nicer DMG layout: `brew install create-dmg`. +For CI, the release workflow imports the same identity from two repository secrets: + +```bash +base64 -i CursorFollowSelfSigned.p12 | gh secret set MAC_SIGNING_P12 +tr -d '\n' < CursorFollowSelfSigned.p12.password | gh secret set MAC_SIGNING_P12_PASSWORD +``` + To restore the identity on a new build machine, double-click the `.p12` (or `security import CursorFollowSelfSigned.p12 -k ~/Library/Keychains/login.keychain-db -T /usr/bin/codesign`). diff --git a/README.md b/README.md index 475d566..1b9d7db 100644 --- a/README.md +++ b/README.md @@ -75,11 +75,13 @@ The general reset is the same everywhere — quit your meeting app and CursorFol ### Releasing -Releases are published manually to [GitHub Releases](https://github.com/LoganRupe/CursorFollow/releases). For each version: +GitHub Actions builds the installers ([.github/workflows/release.yml](.github/workflows/release.yml)): -1. **macOS** — run `./CursorFollowMac/scripts/package.sh` to produce a self-signed `CursorFollowMac-.dmg` (details in [Docs/Distribution/Packaging.md](Docs/Distribution/Packaging.md)). -2. **Windows** — build the `.msi` (see [Windows build](CursorFollowWin/README.md#build)). -3. Create a new GitHub release, tag the version (e.g. `v0.5.1`), and upload both the `.dmg` and `.msi` as release assets. +1. Bump the version on every platform (`MARKETING_VERSION` in the Xcode project, `Version`/`FileVersion`/`AssemblyVersion` in `CursorFollowWin.csproj`, `VERSION` in `CursorFollowLinux/Makefile`) and move the **Unreleased** notes in `CHANGELOG.md` under a `## ` heading. +2. Merge to `main`, then tag and push: `git tag -a v0.5.2 -m "CursorFollow 0.5.2" && git push origin v0.5.2`. +3. The workflow builds the signed macOS DMG and the Windows MSI and attaches both to a **draft** release, with the changelog section as notes. Review it, then publish. + +macOS signing in CI uses the self-signed identity from two repository secrets, `MAC_SIGNING_P12` (base64 of the `.p12`) and `MAC_SIGNING_P12_PASSWORD`. See [Docs/Distribution/Packaging.md](Docs/Distribution/Packaging.md). Local builds still work: `./CursorFollowMac/scripts/package.sh` and `./CursorFollowWin/scripts/package.ps1`. ## License