diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 1a9b060..240a2cc 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -6,12 +6,40 @@ on: pull_request: workflow_dispatch: +# This workflow only builds and uploads an artifact; it makes no repository +# writes. release.yml needs contents: write for `gh release create` — do not +# copy this block there. +permissions: + contents: read + jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 + # primary.rc references primary.manifest as an opaque file, so windres + # never preprocesses it and its assemblyIdentity version cannot come from + # app_strings.h — it has to be bumped by hand. This makes the two drifting + # apart fail the build instead of relying on a release-checklist step. + # + # Expects name= and version= on one line of the assemblyIdentity element; + # reformatting that element fails this check rather than skipping it. + - name: Verify the manifest version matches app_strings.h + run: | + maj=$(grep -oP 'APP_VERSION_MAJOR\s+\K\d+' resources/app_strings.h) + min=$(grep -oP 'APP_VERSION_MINOR\s+\K\d+' resources/app_strings.h) + pat=$(grep -oP 'APP_VERSION_PATCH\s+\K\d+' resources/app_strings.h) + # VERSIONINFO is MAJOR,MINOR,PATCH,0; the manifest mirrors that. + want="$maj.$min.$pat.0" + got=$(grep -oP ']*\bname="Primary"[^>]*\bversion="\K[^"]+' \ + resources/primary.manifest) + echo "app_strings.h: $want / manifest: ${got:-none}" + if [ "$got" != "$want" ]; then + echo "::error::resources/primary.manifest version ${got:-(not found)} does not match $want from resources/app_strings.h. Update the assemblyIdentity version." + exit 1 + fi + - name: Install MinGW-w64 run: | sudo apt-get update @@ -68,10 +96,19 @@ jobs: done exit $status + # Extracted per macro rather than with one alternation piped through + # `paste`, which silently assumed MAJOR/MINOR/PATCH appear in that order + # in the header. An empty component would build a nonsense version like + # "1..0", so each is asserted non-empty. - name: Verify the version resource matches app_strings.h run: | - src=$(grep -oP 'APP_VERSION_(MAJOR|MINOR|PATCH)\s+\K\d+' \ - resources/app_strings.h | paste -sd.) + maj=$(grep -oP 'APP_VERSION_MAJOR\s+\K\d+' resources/app_strings.h) + min=$(grep -oP 'APP_VERSION_MINOR\s+\K\d+' resources/app_strings.h) + pat=$(grep -oP 'APP_VERSION_PATCH\s+\K\d+' resources/app_strings.h) + for c in "$maj" "$min" "$pat"; do + [ -n "$c" ] || { echo "::error::Could not read APP_VERSION_* from resources/app_strings.h"; exit 1; } + done + src="$maj.$min.$pat" echo "Source version: $src" strings -el Primary.exe | grep -qF "$src" || { echo "::error::Binary does not report source version $src"; exit 1; } diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 4471e74..a36984f 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -23,14 +23,42 @@ jobs: - name: Verify the tag matches the source version run: | tag="${GITHUB_REF_NAME#v}" - src=$(grep -oP 'APP_VERSION_(MAJOR|MINOR|PATCH)\s+\K\d+' \ - resources/app_strings.h | paste -sd.) + # Extracted per macro rather than with one alternation piped through + # `paste`, which silently assumed MAJOR/MINOR/PATCH appear in that + # order in the header. An empty component would build a nonsense + # version like "1..0", so each is asserted non-empty. + maj=$(grep -oP 'APP_VERSION_MAJOR\s+\K\d+' resources/app_strings.h) + min=$(grep -oP 'APP_VERSION_MINOR\s+\K\d+' resources/app_strings.h) + pat=$(grep -oP 'APP_VERSION_PATCH\s+\K\d+' resources/app_strings.h) + for c in "$maj" "$min" "$pat"; do + [ -n "$c" ] || { echo "::error::Could not read APP_VERSION_* from resources/app_strings.h"; exit 1; } + done + src="$maj.$min.$pat" echo "Tag: $tag / source: $src" if [ "$tag" != "$src" ]; then echo "::error::Tag $tag does not match source version $src. Bump APP_VERSION_* in resources/app_strings.h." exit 1 fi + # primary.manifest is referenced by primary.rc as an opaque file, so + # windres never preprocesses it and its assemblyIdentity version has to be + # bumped by hand. Refuse to ship a release where it disagrees with the + # source version. (Kept as a separate step from the tag gate above so the + # failure says which of the two is wrong.) + - name: Verify the manifest version matches the source version + run: | + maj=$(grep -oP 'APP_VERSION_MAJOR\s+\K\d+' resources/app_strings.h) + min=$(grep -oP 'APP_VERSION_MINOR\s+\K\d+' resources/app_strings.h) + pat=$(grep -oP 'APP_VERSION_PATCH\s+\K\d+' resources/app_strings.h) + want="$maj.$min.$pat.0" + got=$(grep -oP ']*\bname="Primary"[^>]*\bversion="\K[^"]+' \ + resources/primary.manifest) + echo "app_strings.h: $want / manifest: ${got:-none}" + if [ "$got" != "$want" ]; then + echo "::error::resources/primary.manifest version ${got:-(not found)} does not match $want from resources/app_strings.h. Update the assemblyIdentity version." + exit 1 + fi + - name: Build shell: bash run: ./build.sh 2>&1 | tee build.log diff --git a/resources/primary.manifest b/resources/primary.manifest index 32bb9b4..c5478f8 100644 --- a/resources/primary.manifest +++ b/resources/primary.manifest @@ -1,7 +1,10 @@ + bumped by hand alongside APP_VERSION_* in resources/app_strings.h. + Both workflows fail the build if the two disagree, so drift is caught + rather than shipped. Keep name= and version= on this one line: the check + looks for them together. --> diff --git a/src/primary.cpp b/src/primary.cpp index 190c010..057d629 100644 --- a/src/primary.cpp +++ b/src/primary.cpp @@ -517,7 +517,6 @@ INT_PTR CALLBACK OptionsDialogProc(HWND hwndDlg, UINT msg, WPARAM wParam, LPARAM // Start or stop monitoring based on setting if (autoSwitchEnabled) { StartAutoSwitchMonitoring(g_hwndMain); - CheckAndApplyAutoSwitch(); // Apply immediately (will detect change and apply) } else { StopAutoSwitchMonitoring(g_hwndMain); } @@ -529,16 +528,15 @@ INT_PTR CALLBACK OptionsDialogProc(HWND hwndDlg, UINT msg, WPARAM wParam, LPARAM L"Failed to update base mouse count. Please check your permissions.", L"Error", MB_ICONERROR | MB_OK); - } else { - // Re-check if auto-switch is enabled, to apply new settings immediately - if (autoSwitchEnabled) { - CheckAndApplyAutoSwitch(); - } } + // Single re-apply point: every setting the check reads — + // direction, auto-switch flag, base device count — is + // persisted by now, so one pass covers them all. + // // Settings may have changed the mapping without the device // state changing, so discard the cached state to force the - // next check to apply the new configuration. + // check to apply the new configuration. if (autoSwitchEnabled && autoSwitchWritten) { g_lastExternalMouseState = EXTERNAL_MOUSE_UNKNOWN; CheckAndApplyAutoSwitch();