From 34cc1ac966e0b6b67bf3e805503ee6828aa59d33 Mon Sep 17 00:00:00 2001 From: Max <112043822+Maxaubert@users.noreply.github.com> Date: Wed, 14 Oct 2026 03:10:27 +0200 Subject: [PATCH] build: write all build output to build\ instead of the repo root (#484) build.bat now puts every binary and linker PDB in build\ (laid out like an install: Wind.exe, WindTray.exe, WindConfig.exe, wind_tests.exe and a mirrored ui\dist) and every object, compiler PDB and .res file in build\obj\\ (wind, wind_uiaccess, tray, config, test, check\*). Nothing lands in the repo root or src\ any more. The working directory stays the repo root, so the test binary still finds tests\fixtures and a self-test writes wind_selftest.png where it is started. Consumers updated: installer\wind.nsi (File sources and the WindUA probe), tools\release.ps1 (WindUA rename, output checks, signing), tools\uiaccess_setup.ps1 (deploy sources), tools\installer_check.ps1 (WindUA probe), CI step comments, .gitignore, README, CLAUDE.md, installer\README.md, docs\VERIFICATION.md and docs/architecture 04, 08, 11, 12. A dev build keeps its ini, profiles and logs next to build\Wind.exe (ResolveIniPath/ResolveLogDir unchanged). Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01TdjopXZfsUiesYxYLYHEdH --- .github/actions/build-installer/action.yml | 2 +- .github/workflows/build.yml | 2 +- .gitignore | 15 +++-- CLAUDE.md | 5 +- README.md | 4 +- build.bat | 71 ++++++++++++++++------ docs/VERIFICATION.md | 2 +- docs/architecture/04-render-engine.md | 3 +- docs/architecture/08-config-profiles.md | 2 +- docs/architecture/11-build-test-release.md | 35 +++++++---- docs/architecture/12-instrumentation.md | 4 +- installer/README.md | 5 +- installer/wind.nsi | 20 +++--- tools/installer_check.ps1 | 2 +- tools/release.ps1 | 16 ++--- tools/uiaccess_setup.ps1 | 8 ++- 16 files changed, 129 insertions(+), 67 deletions(-) diff --git a/.github/actions/build-installer/action.yml b/.github/actions/build-installer/action.yml index bfa11faa..df13f65d 100644 --- a/.github/actions/build-installer/action.yml +++ b/.github/actions/build-installer/action.yml @@ -50,7 +50,7 @@ runs: working-directory: ui run: npx playwright install chromium && npx playwright test --reporter=line - # Builds Wind.exe + WindConfig.exe + ui/dist, then compiles the installer and runs + # Builds build/Wind.exe + WindConfig.exe + ui/dist, then compiles the installer and runs # tools/installer_check.ps1. The runner is administrator, so the gate's silent # install/uninstall round-trip actually executes rather than skipping itself. - name: Build installer diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 1597b0ee..f0cc17a3 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -29,7 +29,7 @@ jobs: shell: cmd working-directory: ui run: npm run test:rules - - name: Build settings host and UI (WindConfig.exe, ui/dist) + - name: Build settings host and UI (build/WindConfig.exe, ui/dist) shell: cmd run: build.bat config - name: Build the UIAccess variant diff --git a/.gitignore b/.gitignore index 1f43745c..814f2b71 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,10 @@ -# Build output +# Build output: build.bat writes everything to build/ (binaries, plus build/obj//), and +# a dev build keeps its magnifier.ini, profiles and logs next to its exe there too. The installer +# goes to dist/. +build/ +dist/ +# Anywhere else: clones that predate build/ still hold root-level .obj/.exe/.pdb files, and +# tools/testenv builds gamesim.exe in place. Kept so none of those can be committed by accident. *.obj *.exe *.pdb @@ -6,8 +12,6 @@ *.lib *.exp *.res -build/ -dist/ # Vendored binaries that are part of the tree, not build output !third_party/webview2/x64/*.lib !installer/MicrosoftEdgeWebview2Setup.exe @@ -26,7 +30,10 @@ Thumbs.db .superpowers/ docs/superpowers/ -# Runtime and diagnostic output (a dev build keeps its ini, profiles and logs next to the exe) +# Runtime and diagnostic output. A dev build keeps its ini, profiles and logs next to the exe, +# now build/ (ignored above); the root entries cover clones that ran a pre-build/ Wind.exe. +# A self-test (WIND_SELFTEST=1) writes wind_selftest.png and wind_hdr_diag.txt to its working +# directory, normally the repo root. /magnifier.ini /profiles/ /logs/ diff --git a/CLAUDE.md b/CLAUDE.md index 03f7a066..ddf49c78 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -6,6 +6,8 @@ New plans and brainstorm output are not committed. `docs/specs`, `docs/design` a are frozen history cited from code, not kept in step with it. ## Commands +- All build output goes to `build\` (exes, PDBs, `ui\dist` copy; objects in `build\obj\\`), + never the repo root. A dev `build\Wind.exe` keeps its ini, profiles and logs in `build\`. See 11. - `build.bat`: `Wind.exe` + `WindTray.exe` (uiAccess=false). Wind refuses to start without UIAccess; run a plain build with `WIND_ALLOW_NO_UIACCESS=1` (or `WIND_SELFTEST=1`). - `build.bat test`: builds and runs the doctest binary; exit 0 = pass. @@ -121,7 +123,8 @@ Chapter numbers refer to `docs/architecture/NN-*.md`. session is the floor. - The reveal is gated on the present fence (and composite evidence over fullscreen apps). - Always restore the OS cursor and release `ClipCursor` on every exit path, including the crash filter. -- Verify the overlay only from inside: `WIND_SELFTEST=1 Wind.exe` writes `wind_selftest.png`. +- Verify the overlay only from inside: `WIND_SELFTEST=1 build\Wind.exe` writes `wind_selftest.png` + to its working directory (the repo root when started from there). - `zorderBand` ships 0: band 16 covers the shell but loses the cursor under the Snipping Tool. Re-test both before changing it. Never let a refused band be silent (`CreateBandedWindow`). diff --git a/README.md b/README.md index fd9405ea..903a7ec2 100644 --- a/README.md +++ b/README.md @@ -79,7 +79,9 @@ profile. Preferences > Show advanced settings shows the rest. Every setting is a ## Development `build.bat` builds Wind, `build.bat test` runs the unit tests, and `build.bat config` builds the -Settings app. A plain `build.bat` build has no UIAccess, so Wind refuses to start; run it with +Settings app. Everything they build lands in `build\` (`build\Wind.exe` and friends, objects in +`build\obj\`), never the repo root; a dev build keeps its `magnifier.ini` and logs there too. +A plain `build.bat` build has no UIAccess, so Wind refuses to start; run it with `WIND_ALLOW_NO_UIACCESS=1` set, or deploy the signed build with `tools\uiaccess_setup.ps1`. In `ui/`, `npm run test:rules` runs the JS bind-rule and config-defaults tests and `npx playwright test` the Settings UI tests. The developer docs are in [docs/architecture](docs/architecture/README.md). diff --git a/build.bat b/build.bat index b6bfedbc..41b5394c 100644 --- a/build.bat +++ b/build.bat @@ -21,6 +21,18 @@ call "%VSPATH%\VC\Auxiliary\Build\vcvars64.bat" >nul 2>&1 if errorlevel 1 (echo [build] vcvars64.bat failed & exit /b 1) cd /d "%ROOT%" +rem --- Build output ------------------------------------------------------------ +rem Everything the compiler, linker and rc.exe produce goes under build\, never the repo root: +rem the binaries in build\ itself (the install layout, so a dev Wind.exe finds WindTray.exe, +rem WindConfig.exe and ui\dist next to it the way an installed one does), and objects, compiler +rem PDBs and .res files in build\obj\\. One object dir per target, because targets compile +rem the same sources with different defines (WIND_UIACCESS, WIND_TESTS), and the tray and settings +rem hosts each have their own main.cpp. The working directory stays the repo root, so the test +rem binary finds tests\fixtures and a self-test writes its files where it is started from. +set "BUILD=%ROOT%build" +set "OBJ=%BUILD%\obj" +if not exist "%BUILD%" mkdir "%BUILD%" + if /i "%1"=="test" goto :test if /i "%1"=="check" goto :check if /i "%1"=="uiaccess" goto :uiaccess @@ -29,7 +41,7 @@ if /i "%1"=="installer" goto :installer if /i "%1"=="tray" goto :tray rem --- App build (normal: uiAccess=false, runs from anywhere) ---------------- -call :core_exe "" Wind.manifest +call :core_exe "" Wind.manifest wind if errorlevel 1 exit /b 1 call :tray_exe exit /b %errorlevel% @@ -39,20 +51,24 @@ rem Embeds Wind.uiaccess.manifest so the overlay CAN use a high z-band (opt-i rem to cover the Start menu / taskbar / tray. Shipped default is 0 - see issue #162. rem Deploy via tools\uiaccess_setup.ps1. :uiaccess -call :core_exe /DWIND_UIACCESS Wind.uiaccess.manifest +call :core_exe /DWIND_UIACCESS Wind.uiaccess.manifest wind_uiaccess if errorlevel 1 exit /b 1 call :tray_exe exit /b %errorlevel% rem --- Wind.exe itself, shared by the app and uiaccess targets. %1 = extra define ("" for none), -rem %2 = the manifest to embed. Everything else (sources, flags, libraries) is the same. +rem %2 = the manifest to embed, %3 = its object dir under build\obj. Everything else (sources, +rem flags, libraries) is the same. Output: build\Wind.exe and build\Wind.pdb. :core_exe +set "CORE_OBJ=%OBJ%\%~3" +if not exist "%CORE_OBJ%" mkdir "%CORE_OBJ%" rem Compile the app-icon resource (rc.exe ships with the Windows SDK, on PATH via vcvars). -rc /nologo /fo "%ROOT%src\wind.res" "%ROOT%src\wind.rc" +rc /nologo /fo "%CORE_OBJ%\wind.res" "%ROOT%src\wind.rc" if errorlevel 1 (echo [build] rc.exe failed & exit /b 1) cl /nologo /std:c++17 /EHsc /O2 /W4 /Zi /DUNICODE /D_UNICODE %~1 ^ - src\*.cpp src\wind.res ^ - /Fe:Wind.exe ^ + /Fo"%CORE_OBJ%\\" /Fd"%CORE_OBJ%\\" ^ + src\*.cpp "%CORE_OBJ%\wind.res" ^ + /Fe:"%BUILD%\Wind.exe" ^ /link Magnification.lib Dwmapi.lib user32.lib shell32.lib gdi32.lib Dbghelp.lib ^ d3d11.lib dxgi.lib dxguid.lib d3dcompiler.lib windowscodecs.lib ole32.lib oleaut32.lib uuid.lib advapi32.lib ^ /MANIFEST:EMBED /MANIFESTUAC:NO /MANIFESTINPUT:%~2 /SUBSYSTEM:WINDOWS ^ @@ -67,14 +83,15 @@ call :tray_exe exit /b %errorlevel% :tray_exe -rc /nologo /fo "%ROOT%src\tray_app\wind_tray.res" "%ROOT%src\tray_app\wind_tray.rc" +if not exist "%OBJ%\tray" mkdir "%OBJ%\tray" +rc /nologo /fo "%OBJ%\tray\wind_tray.res" "%ROOT%src\tray_app\wind_tray.rc" if errorlevel 1 (echo [build] rc.exe failed for WindTray & exit /b 1) -rem Objects go to src\tray_app\ so the shared sources never overwrite Wind.exe's .obj files. +rem Own object dir (build\obj\tray) so the shared sources never overwrite Wind.exe's .obj files. cl /nologo /std:c++17 /EHsc /O2 /W4 /Zi /DUNICODE /D_UNICODE ^ - /Fo"%ROOT%src\tray_app\\" /Fd"%ROOT%WindTray.pdb" ^ + /Fo"%OBJ%\tray\\" /Fd"%OBJ%\tray\\" ^ src\tray_app\*.cpp src\tray_items.cpp src\profiles.cpp src\config.cpp src\logging.cpp src\ini_edit.cpp ^ - src\tray_app\wind_tray.res ^ - /Fe:WindTray.exe ^ + "%OBJ%\tray\wind_tray.res" ^ + /Fe:"%BUILD%\WindTray.exe" ^ /link user32.lib shell32.lib gdi32.lib Dwmapi.lib Dbghelp.lib shlwapi.lib ole32.lib version.lib ^ advapi32.lib ntdll.lib ^ /MANIFEST:EMBED /MANIFESTUAC:NO /MANIFESTINPUT:Wind.manifest /SUBSYSTEM:WINDOWS ^ @@ -94,38 +111,52 @@ if exist "%ROOT%ui\package.json" ( if not exist dist\index.html (popd & echo [build] ui build produced no dist\index.html & exit /b 1) popd ) +if not exist "%OBJ%\config" mkdir "%OBJ%\config" rem Same app-icon resource as Wind.exe (rc.exe ships with the Windows SDK, on PATH via vcvars). -rc /nologo /fo "%ROOT%src\wind.res" "%ROOT%src\wind.rc" +rc /nologo /fo "%OBJ%\config\wind.res" "%ROOT%src\wind.rc" if errorlevel 1 (echo [build] rc.exe failed & exit /b 1) cl /nologo /std:c++17 /EHsc /O2 /W4 /DUNICODE /D_UNICODE ^ /I third_party\webview2\include ^ - src\config_ui\main.cpp src\ini_edit.cpp src\profiles.cpp src\config.cpp src\logging.cpp src\wind.res ^ - /Fe:WindConfig.exe ^ + /Fo"%OBJ%\config\\" /Fd"%OBJ%\config\\" ^ + src\config_ui\main.cpp src\ini_edit.cpp src\profiles.cpp src\config.cpp src\logging.cpp "%OBJ%\config\wind.res" ^ + /Fe:"%BUILD%\WindConfig.exe" ^ /link third_party\webview2\x64\WebView2LoaderStatic.lib ^ user32.lib shell32.lib shlwapi.lib ole32.lib version.lib advapi32.lib ntdll.lib Dwmapi.lib /SUBSYSTEM:WINDOWS -exit /b %errorlevel% +if errorlevel 1 exit /b 1 +rem WindConfig.exe loads its page from \ui\dist, so mirror the UI into build\ui\dist, the +rem install layout. ui\dist stays the copy the installer and tools\uiaccess_setup.ps1 ship. +rem robocopy exit codes below 8 mean success. +if exist "%ROOT%ui\dist\index.html" ( + robocopy "%ROOT%ui\dist" "%BUILD%\ui\dist" /MIR /NJH /NJS /NFL /NDL /NP >nul + if errorlevel 8 (echo [build] copying ui\dist to build\ui\dist failed & exit /b 1) +) +exit /b 0 rem --- Test build (pure-logic sources only; no ) ----------------- :test rem /wd5285 silences a known doctest 2.4.11 header warning under MSVC /W4. +if not exist "%OBJ%\test" mkdir "%OBJ%\test" cl /nologo /std:c++17 /EHsc /W4 /wd5285 /DWIND_TESTS /I third_party ^ + /Fo"%OBJ%\test\\" /Fd"%OBJ%\test\\" ^ tests\*.cpp ^ src\transform.cpp src\zoom_controller.cpp src\config.cpp src\profiles.cpp src\cursor_mapper.cpp src\lock_detector.cpp src\cursor_lock.cpp src\crosshair.cpp src\ini_edit.cpp src\logging.cpp src\tray_items.cpp src\hitch_record.cpp src\render_params.cpp src\tray_app\tray_pin_logic.cpp ^ - /Fe:wind_tests.exe + /Fe:"%BUILD%\wind_tests.exe" if errorlevel 1 exit /b 1 -"%ROOT%wind_tests.exe" +rem Runs from the repo root (the current directory): the tests find tests\fixtures from there. +"%BUILD%\wind_tests.exe" exit /b %errorlevel% rem --- Compile-only check (no link; verifies all sources compile) ----------- :check -cl /nologo /std:c++17 /EHsc /W4 /DUNICODE /D_UNICODE /c src\*.cpp +for %%D in (core tray config) do if not exist "%OBJ%\check\%%D" mkdir "%OBJ%\check\%%D" +cl /nologo /std:c++17 /EHsc /W4 /DUNICODE /D_UNICODE /c /Fo"%OBJ%\check\core\\" src\*.cpp if errorlevel 1 exit /b 1 rem WindTray.exe sources (issue #291); own object dir, its main.cpp would overwrite Wind's main.obj. -cl /nologo /std:c++17 /EHsc /W4 /DUNICODE /D_UNICODE /c /Fo"%ROOT%src\tray_app\\" src\tray_app\*.cpp +cl /nologo /std:c++17 /EHsc /W4 /DUNICODE /D_UNICODE /c /Fo"%OBJ%\check\tray\\" src\tray_app\*.cpp if errorlevel 1 exit /b 1 rem WindConfig.exe's host (same flags as :config); own object dir for the same reason. cl /nologo /std:c++17 /EHsc /W4 /DUNICODE /D_UNICODE /c /I third_party\webview2\include ^ - /Fo"%ROOT%src\config_ui\\" src\config_ui\main.cpp + /Fo"%OBJ%\check\config\\" src\config_ui\main.cpp exit /b %errorlevel% rem --- Installer (needs NSIS; winget install NSIS.NSIS) --------------------- diff --git a/docs/VERIFICATION.md b/docs/VERIFICATION.md index 38b1fc97..d5b753d9 100644 --- a/docs/VERIFICATION.md +++ b/docs/VERIFICATION.md @@ -14,7 +14,7 @@ install/uninstall round trip (`tools\installer_check.ps1`). - [ ] Upgrade while Wind runs zoomed: no "file in use" error, the OS cursor is visible afterwards. Upgrade with Settings open: WindConfig closes. - [ ] Uninstall keeping settings: `magnifier.ini` survives. Uninstall removing settings: it does not. - [ ] At 100% and 225% DPI the setup window is sharp and its hit targets line up. -- [ ] Without UIAccess Wind refuses to start: a plain `build.bat` Wind.exe (no `WIND_ALLOW_NO_UIACCESS`) shows the "Wind can't start" dialog telling you to run the installer again, then exits with no tray icon and no magnifier. +- [ ] Without UIAccess Wind refuses to start: a plain `build.bat` `build\Wind.exe` (no `WIND_ALLOW_NO_UIACCESS`) shows the "Wind can't start" dialog telling you to run the installer again, then exits with no tray icon and no magnifier. ## Desktop zoom - [ ] First launch runs the guided setup; the chosen binds work. diff --git a/docs/architecture/04-render-engine.md b/docs/architecture/04-render-engine.md index 19452970..6645867d 100644 --- a/docs/architecture/04-render-engine.md +++ b/docs/architecture/04-render-engine.md @@ -33,7 +33,8 @@ monitor. Every style is required: | `DwmSetWindowAttribute(DWMWA_EXCLUDED_FROM_PEEK)` | Keeps the view during a taskbar-thumbnail Aero Peek. | Because the overlay is capture-excluded, external screenshots cannot see it. Verify from inside the -process: `WIND_SELFTEST=1 Wind.exe` writes `wind_selftest.png` (`RenderEngine::dumpFrame`). +process: `WIND_SELFTEST=1 build\Wind.exe` writes `wind_selftest.png` to its working directory +(`RenderEngine::dumpFrame`). ## Present: blt model only diff --git a/docs/architecture/08-config-profiles.md b/docs/architecture/08-config-profiles.md index f5eba4a0..ce515d30 100644 --- a/docs/architecture/08-config-profiles.md +++ b/docs/architecture/08-config-profiles.md @@ -54,7 +54,7 @@ flowchart LR `wind::ResolveIniPath()` (`src/config_path.h`) probes whether the exe's directory is writable (a sentinel file opened with `FILE_FLAG_DELETE_ON_CLOSE`). -- Writable (dev build): the ini sits next to the exe. +- Writable (dev build): the ini sits next to the exe, in `build\` (see [11](11-build-test-release.md)). - Read-only (`C:\Program Files\Wind`): `%LOCALAPPDATA%\Wind\magnifier.ini`, seeded from a template next to the exe if one exists, otherwise created by `LoadConfig` with a commented header. - **Never hardcode `L"magnifier.ini"`.** Program Files is read-only for the non-admin runtime, and a diff --git a/docs/architecture/11-build-test-release.md b/docs/architecture/11-build-test-release.md index c1eab305..4d987b81 100644 --- a/docs/architecture/11-build-test-release.md +++ b/docs/architecture/11-build-test-release.md @@ -7,17 +7,30 @@ deploy, the installer's elevation rules and the release workflows. ## build.bat targets -`build.bat` locates MSVC with vswhere, runs `vcvars64.bat` and dispatches on its first argument: +`build.bat` locates MSVC with vswhere, runs `vcvars64.bat` and dispatches on its first argument. +**Nothing is written to the repo root.** Binaries and their linker PDBs go to `build\`, which is +laid out like an install (`Wind.exe`, `WindTray.exe`, `WindConfig.exe` and `ui\dist` side by side, +so `ExeDir()` lookups work unchanged). Objects, compiler PDBs and `.res` files go to +`build\obj\\` (`wind`, `wind_uiaccess`, `tray`, `config`, `test`, `check\*`): targets +compile the same sources with different defines, and the tray and settings hosts each have a +`main.cpp`. The working directory stays the repo root. | Target | Output | What it is | |---|---|---| -| (none) | `Wind.exe` + `WindTray.exe` | The app with the `uiAccess=false` manifest. It refuses to start without UIAccess (issue #460) unless `WIND_ALLOW_NO_UIACCESS=1` or `WIND_SELFTEST=1` is set | -| `test` | `wind_tests.exe` | The doctest binary over the pure sources; runs it and returns its exit code | -| `check` | – | Compile-only pass over `src\*.cpp`, `src\tray_app\*.cpp` and `src\config_ui\main.cpp` | -| `uiaccess` | `Wind.exe` + `WindTray.exe` | `Wind.uiaccess.manifest` and `/DWIND_UIACCESS`; useful only signed and in Program Files. `WindTray.exe` always keeps the plain manifest | -| `tray` | `WindTray.exe` | The tray helper alone, objects in `src\tray_app\` | -| `config` | `WindConfig.exe` | Builds the Svelte app to `ui/dist/`, then the WebView2 host against `third_party/webview2` | -| `installer` | `dist\Wind-Setup-x64-.exe` | `makensis /WX installer\wind.nsi`, then `tools\installer_check.ps1` | +| (none) | `build\Wind.exe` + `build\WindTray.exe` | The app with the `uiAccess=false` manifest. It refuses to start without UIAccess (issue #460) unless `WIND_ALLOW_NO_UIACCESS=1` or `WIND_SELFTEST=1` is set | +| `test` | `build\wind_tests.exe` | The doctest binary over the pure sources; runs it from the repo root (it finds `tests\fixtures` there) and returns its exit code | +| `check` | – | Compile-only pass over `src\*.cpp`, `src\tray_app\*.cpp` and `src\config_ui\main.cpp`, objects in `build\obj\check\` | +| `uiaccess` | `build\Wind.exe` + `build\WindTray.exe` | `Wind.uiaccess.manifest` and `/DWIND_UIACCESS`; useful only signed and in Program Files. `WindTray.exe` always keeps the plain manifest | +| `tray` | `build\WindTray.exe` | The tray helper alone | +| `config` | `build\WindConfig.exe` | Builds the Svelte app to `ui/dist/`, then the WebView2 host against `third_party/webview2`, then mirrors `ui\dist` to `build\ui\dist` (the host loads `\ui\dist`) | +| `installer` | `dist\Wind-Setup-x64-.exe` | `makensis /WX installer\wind.nsi` (packs `build\*.exe` and `ui\dist`), then `tools\installer_check.ps1` | + +**A dev build's runtime files live in `build\`.** `build\` is writable, so `ResolveIniPath()` and +`ResolveLogDir()` keep a dev build's `magnifier.ini`, `profiles\` and `logs\` next to +`build\Wind.exe`, as they did at the root before. Deleting `build\` deletes them too; an ini left at +the root by an older clone is no longer read (move it into `build\` to keep it). A self-test writes +`wind_selftest.png` and `wind_hdr_diag.txt` to its working directory, so +`WIND_SELFTEST=1 build\Wind.exe` from the repo root leaves them at the root (gitignored). The installer treats NSIS warnings as errors: an unreferenced define in generated layout data is how a page ends up wired to nothing. @@ -74,8 +87,8 @@ locally trusted certificate and run from a secure location, in practice `C:\Prog Without it, Wind reads `TokenUIAccess` at init and Auto keeps the desktop on Render. `tools/uiaccess_setup.ps1` (elevated) stops Wind, builds `uiaccess` and `config`, creates or reuses -a self-signed "Wind Dev Test Cert", trusts it, signs all three exes (unsigned fresh builds trip a -Defender false positive) and copies them with `ui\dist` to Program Files. It does not deploy an +a self-signed "Wind Dev Test Cert", trusts it, signs all three exes in `build\` (unsigned fresh +builds trip a Defender false positive) and copies them with `ui\dist` to Program Files. It does not deploy an ini. It logs to `tools\uiaccess_setup.log`; a good deploy ends with `status=Valid` and `DONE`. - Pass an **absolute** `-File` path: the elevated process starts in System32. @@ -145,7 +158,7 @@ environment-driven: `WIND_SIGN_THUMBPRINT`, or `WIND_SIGN_PFX` plus `WIND_SIGN_P - With a certificate it signs the three exes and the installer, signing the payload before makensis packs it (UIAccess is granted on `Wind.exe`'s own signature). - Without one (CI today) it packs both variants: the ordinary build and the UIAccess build as - `WindUA.exe`. Setup signs `WindUA.exe` on each PC with a per-machine certificate whose private key + `build\WindUA.exe` (the `uiaccess` build's `build\Wind.exe`, renamed). Setup signs `WindUA.exe` on each PC with a per-machine certificate whose private key it deletes at once (`installer/local-sign.ps1`). If that fails, setup keeps the ordinary build, says signing failed and does not start Wind: without UIAccess, Wind refuses to start ("Wind can't start", `src/uiaccess_gate.h`). - **Never put the self-signed dev certificate in CI**: it is trusted by nobody and looks worse than no signature. diff --git a/docs/architecture/12-instrumentation.md b/docs/architecture/12-instrumentation.md index a77da7b5..dc48f353 100644 --- a/docs/architecture/12-instrumentation.md +++ b/docs/architecture/12-instrumentation.md @@ -30,7 +30,7 @@ Both binaries log through `src/logging.cpp`. The pure half (formatting, rotation unit-tested; the Win32 half is excluded from the test build. - **Files.** The `logs\` folder next to the exe when that folder is writable (dev and portable - builds), else `%LOCALAPPDATA%\Wind\logs\` (the Program Files install; `ResolveLogDir`, the same + builds; a dev build's is `build\logs\`), else `%LOCALAPPDATA%\Wind\logs\` (the Program Files install; `ResolveLogDir`, the same rule as the ini): `wind-core.log` from Wind.exe, `wind-config.log` from WindConfig.exe. Rotation at 1 MiB over three generations. A second instance that cannot open the shared log writes `wind-core-.log`; once it holds the @@ -107,7 +107,7 @@ Zero cost when off. | Knob | Effect | |---|---| -| `WIND_SELFTEST=1` | Renders headlessly and writes `wind_selftest.png`; the only way to capture the render overlay | +| `WIND_SELFTEST=1` | Renders headlessly and writes `wind_selftest.png` to the working directory; the only way to capture the render overlay | | `WIND_PACINGTEST=1` | Runs the render path at a forced cadence (proved the blt microstutter is DWM phase, not the loop) | | `WIND_NOPARK=1` | Disables overlay parking for A/B | | `WIND_NOHOOK` | Skips the LL hooks to exercise the polling fallback | diff --git a/installer/README.md b/installer/README.md index 5522f8f6..6f3808c1 100644 --- a/installer/README.md +++ b/installer/README.md @@ -35,7 +35,8 @@ The elevation rules (HKLM autostart, launching through `explorer.exe`, stopping build.bat installer ``` -Needs NSIS (`winget install NSIS.NSIS`). The target compiles the script and runs +Needs NSIS (`winget install NSIS.NSIS`). It packs the binaries from `build\` (`build.bat`, +`build.bat config`) and the UI from `ui\dist`, so build those first. The target compiles the script and runs `tools\installer_check.ps1`, which checks that every packed file exists, that every rectangle the screens read was generated, and that a silent install and uninstall round-trip (the round trip needs an elevated shell). For a release artifact use `tools\release.ps1`, which builds the payload, @@ -43,7 +44,7 @@ signs it when a certificate is configured, and packs the installer. ## Local signing -Without a code-signing certificate, `release.ps1` also builds the UIAccess variant as `WindUA.exe`, +Without a code-signing certificate, `release.ps1` also builds the UIAccess variant as `build\WindUA.exe`, which makes `wind.nsi` define `LOCAL_SIGN`. Setup then runs `local-sign.ps1`: - A fresh `CN=Wind Local Signing` certificate is trusted in LocalMachine Root and TrustedPublisher, diff --git a/installer/wind.nsi b/installer/wind.nsi index d3782002..d20a5a22 100644 --- a/installer/wind.nsi +++ b/installer/wind.nsi @@ -53,9 +53,9 @@ VIAddVersionKey "LegalCopyright" "Copyright (c) 2026 ${PUBLISHER}" ; UIAccess for everyone (issue #261). A release with a real code-signing certificate ships ; Wind.exe already signed as the uiAccess build, and there is nothing to do. Without one, -; tools\release.ps1 also builds the uiAccess variant as WindUA.exe, and setup signs it on the +; tools\release.ps1 also builds the uiAccess variant as build\WindUA.exe, and setup signs it on the ; PC it installs to (local-sign.ps1 says how, and why that is safe). Its presence decides. -!if /FileExists "..\WindUA.exe" +!if /FileExists "..\build\WindUA.exe" !define LOCAL_SIGN !endif @@ -102,15 +102,15 @@ Section "Wind" SEC_WIND ; all ("A referral was returned from the server"), so setup must never leave one in place: ; killed mid-signing, it did exactly that (reproduced 2026-09-28). This way an interruption ; at any point leaves a Wind that runs. - File "..\Wind.exe" - File "..\WindConfig.exe" - File "..\WindTray.exe" + File "..\build\Wind.exe" + File "..\build\WindConfig.exe" + File "..\build\WindTray.exe" ; Staged under $INSTDIR, not in $PLUGINSDIR: that is the user's %TEMP%, writable by a ; non-elevated process, and this step runs a script and signs a binary as admin. Program ; Files is admin-only, so nothing unprivileged can swap either between copy and use. RMDir /r "$INSTDIR\.stage" CreateDirectory "$INSTDIR\.stage\ua" - File "/oname=$INSTDIR\.stage\ua\Wind.exe" "..\WindUA.exe" + File "/oname=$INSTDIR\.stage\ua\Wind.exe" "..\build\WindUA.exe" File "/oname=$INSTDIR\.stage\local-sign.ps1" "local-sign.ps1" DetailPrint "Signing Wind on this PC..." nsExec::ExecToLog '"$WINDIR\Sysnative\WindowsPowerShell\v1.0\powershell.exe" -NoProfile -NonInteractive -ExecutionPolicy Bypass -File "$INSTDIR\.stage\local-sign.ps1" -Stage "$INSTDIR\.stage\ua\Wind.exe" -Dir "$INSTDIR"' @@ -120,7 +120,7 @@ Section "Wind" SEC_WIND CopyFiles /SILENT "$INSTDIR\.stage\ua\Wind.exe" "$INSTDIR\Wind.exe" ${If} ${Errors} DetailPrint "Could not place the signed build; keeping the standard one." - File "..\Wind.exe" + File "..\build\Wind.exe" StrCpy $SignFailed 1 ${EndIf} ${Else} @@ -135,9 +135,9 @@ Section "Wind" SEC_WIND MessageBox MB_ICONSTOP|MB_OK "Setup couldn't sign Wind on this PC, so Windows won't give it UI Access, and Wind can't start without it.$\r$\n$\r$\nRun the Wind installer again. If this keeps happening, check that security software isn't blocking PowerShell while Setup runs." /SD IDOK ${EndIf} !else - File "..\Wind.exe" - File "..\WindConfig.exe" - File "..\WindTray.exe" + File "..\build\Wind.exe" + File "..\build\WindConfig.exe" + File "..\build\WindTray.exe" !endif ; the licence the user accepted, kept next to the app so the terms are always findable File "/oname=LICENSE.txt" "..\LICENSE" diff --git a/tools/installer_check.ps1 b/tools/installer_check.ps1 index 45452378..181bb400 100644 --- a/tools/installer_check.ps1 +++ b/tools/installer_check.ps1 @@ -164,7 +164,7 @@ if (-not (Test-Path $setup)) { Check "silent install placed WindTray.exe" { Test-Path (Join-Path $scratch 'WindTray.exe') } Check "silent install placed ui\dist" { Test-Path (Join-Path $scratch 'ui\dist\index.html') } # Local signing (issue #261): only when this build packed the uiAccess variant. - if (Test-Path (Join-Path $root 'WindUA.exe')) { + if (Test-Path (Join-Path $root 'build\WindUA.exe')) { $sig = Get-AuthenticodeSignature (Join-Path $scratch 'Wind.exe') Check "local signing: Wind.exe signature is Valid" { $sig.Status -eq 'Valid' } Check "local signing: signed by CN=Wind Local Signing" { $sig.SignerCertificate.Subject -eq 'CN=Wind Local Signing' } diff --git a/tools/release.ps1 b/tools/release.ps1 index 8f0594d7..56010f23 100644 --- a/tools/release.ps1 +++ b/tools/release.ps1 @@ -8,7 +8,7 @@ $env:WIND_SIGN_PASSWORD its password With a certificate this builds the uiAccess=true variant and signs it. Without one it - builds BOTH: the ordinary Wind.exe, and the uiAccess variant as WindUA.exe, which setup + builds BOTH: the ordinary build\Wind.exe, and the uiAccess variant as build\WindUA.exe, which setup signs on the PC it installs to (installer\local-sign.ps1, issue #261). If that signing fails, setup installs the ordinary build, and the app degrades correctly: transform_model.cpp probes TokenUIAccess and disables the desktop transform pick. @@ -86,7 +86,9 @@ if ($cert) { Write-Warning "unsigned release: setup signs the uiAccess build on each PC (installer\local-sign.ps1)." } -$ua = "$root\WindUA.exe" +# build.bat puts every binary in build\ (never the repo root); the installer packs them from there. +$bin = "$root\build" +$ua = "$bin\WindUA.exe" if (-not $SkipBuild) { # No certificate: build the uiAccess variant as well, as WindUA.exe. Setup signs it on the # PC it installs to (installer\local-sign.ps1, issue #261) and keeps the standard build as @@ -96,7 +98,7 @@ if (-not $SkipBuild) { if (-not $cert) { Write-Host "=== building WindUA.exe (uiaccess, signed at install) ===" Invoke-Build 'uiaccess' - Move-Item "$root\Wind.exe" $ua -Force + Move-Item "$bin\Wind.exe" $ua -Force } Write-Host "=== building Wind.exe ($(if ($variant) { $variant } else { 'standard' })) ===" Invoke-Build $variant @@ -105,16 +107,16 @@ if (-not $SkipBuild) { } foreach ($f in 'Wind.exe', 'WindTray.exe', 'WindConfig.exe') { - if (-not (Test-Path "$root\$f")) { throw "missing build output: $f" } + if (-not (Test-Path "$bin\$f")) { throw "missing build output: build\$f" } } if (-not (Test-Path "$root\ui\dist\index.html")) { throw "missing ui\dist (build.bat config)" } # The payload is signed BEFORE makensis packs it: signing the installer does not sign what # is inside it, and UIAccess is granted on Wind.exe's own signature. if ($cert) { - Invoke-Sign "$root\Wind.exe" $cert - Invoke-Sign "$root\WindConfig.exe" $cert - Invoke-Sign "$root\WindTray.exe" $cert + Invoke-Sign "$bin\Wind.exe" $cert + Invoke-Sign "$bin\WindConfig.exe" $cert + Invoke-Sign "$bin\WindTray.exe" $cert } Write-Host "=== compiling the installer ===" diff --git a/tools/uiaccess_setup.ps1 b/tools/uiaccess_setup.ps1 index 86eda35d..3f2aa5a9 100644 --- a/tools/uiaccess_setup.ps1 +++ b/tools/uiaccess_setup.ps1 @@ -8,9 +8,11 @@ $log = Join-Path $PSScriptRoot 'uiaccess_setup.log' Start-Transcript -Path $log -Force try { $root = Split-Path -Parent $PSScriptRoot - $src = "$root\Wind.exe" - $cfgSrc = "$root\WindConfig.exe" - $traySrc = "$root\WindTray.exe" # built by build.bat uiaccess; plain manifest, NO uiAccess (#291) + # build.bat writes every binary to build\ (never the repo root). ui\dist is the UI build itself; + # build.bat config also mirrors it to build\ui\dist, but this deploys the source copy. + $src = "$root\build\Wind.exe" + $cfgSrc = "$root\build\WindConfig.exe" + $traySrc = "$root\build\WindTray.exe" # built by build.bat uiaccess; plain manifest, NO uiAccess (#291) $uiSrc = "$root\ui\dist" Write-Output "=== 0a. stop any running Wind / WindConfig (dev or deployed) so the exes are not locked ==="