From a4f8da7dd851da3c76f4c22d3c4c147071d34525 Mon Sep 17 00:00:00 2001 From: "Galvan, Mark" Date: Tue, 25 Aug 2026 16:44:28 -0700 Subject: [PATCH] Add CMake build system for PresentMon, IntelPresentMon, tools, tests, and MSI packaging --- .gitignore | 16 +- BUILDING.md | 754 +++++++- CMakeLists.txt | 200 ++ .../AppCef/Batch/cef-build-wrapper.ps1 | 153 +- IntelPresentMon/AppCef/Batch/cef-lock.psm1 | 341 +++- IntelPresentMon/AppCef/Batch/pull-cef.ps1 | 61 +- IntelPresentMon/AppCef/Batch/upgrade-cef.ps1 | 36 +- IntelPresentMon/AppCef/Batch/validate-cef.ps1 | 32 +- IntelPresentMon/AppCef/CMakeLists.txt | 82 + IntelPresentMon/AppCef/ceflock.md | 87 +- .../AppCef/source/util/ActionClientServer.cpp | 2 +- .../AppCef/source/util/IpcInvocationManager.h | 2 +- .../AppCef/source/util/ThreadCompass.cpp | 2 +- .../source/util/cact/CefExecutionContext.h | 2 +- IntelPresentMon/AppCef/source/winmain.cpp | 1 - IntelPresentMon/AppCef/web-dist-lock.json | 16 + IntelPresentMon/AppCef/webui.md | 30 +- .../CommonUtilities/CMakeLists.txt | 122 ++ IntelPresentMon/CommonUtilities/Metrics.cmake | 49 + IntelPresentMon/CommonUtilities/win/WinAPI.h | 4 + IntelPresentMon/ControlLib/CMakeLists.txt | 60 + IntelPresentMon/Core/CMakeLists.txt | 113 ++ IntelPresentMon/Interprocess/CMakeLists.txt | 72 + IntelPresentMon/KernelProcess/CMakeLists.txt | 71 + .../KernelProcess/KernelProcess.args.json | 4 - .../PMInstaller/PMInstaller.wixproj | 38 +- IntelPresentMon/PMInstaller/PresentMon.wxs | 73 + .../PMInstallerLib/PMInstallerLib.wixproj | 14 +- .../PMInstallerLib/wix-uci-dist.ps1 | 238 ++- .../PmonCMakePackaging.wixproj.props | 18 + IntelPresentMon/PresentMonAPI2/CMakeLists.txt | 46 + .../PresentMonAPI2Loader/CMakeLists.txt | 47 + .../PresentMonAPI2Loader/Implementation.cpp | 2 +- .../PresentMonAPI2Tests/CMakeLists.txt | 110 ++ .../PresentMonAPI2Tests/ModuleInit.cpp | 18 +- .../PresentMonAPI2Tests.vcxproj | 3 + .../update_machine_expectations.py | 117 +- .../PresentMonAPIWrapper/CMakeLists.txt | 47 + .../PresentMonAPIWrapperCommon/CMakeLists.txt | 41 + .../PresentMonMiddleware/CMakeLists.txt | 72 + .../PresentMonService/CMakeLists.txt | 157 ++ IntelPresentMon/SampleClient/CMakeLists.txt | 54 + IntelPresentMon/SampleClient/LogSetup.cpp | 26 +- .../ServiceMergeModule.wixproj | 11 +- IntelPresentMon/UnitTests/CMakeLists.txt | 49 + IntelPresentMon/UnitTests/UnitTests.vcxproj | 2 + IntelPresentMon/Versioning/CMakeLists.txt | 63 + .../Versioning/scripts/pre-build.ps1 | 22 +- PresentData/CMakeLists.txt | 63 + PresentMon.sln | 5 +- PresentMon/CMakeLists.txt | 61 + Provider/CMakeLists.txt | 51 + Reflector/Properties/launchSettings.json | 8 - Scripts/auxdata.py | 49 + Scripts/bootstrap.py | 72 + Scripts/cef.py | 88 + Scripts/discover_tests.py | 93 + Scripts/generate_etl_csv_tests.py | 71 + Scripts/package.py | 180 ++ Scripts/pmon/__init__.py | 1 + Scripts/pmon/auxdata.py | 227 +++ Scripts/pmon/cef.py | 497 +++++ Scripts/pmon/common.py | 174 ++ Scripts/pmon/discovery.py | 273 +++ Scripts/pmon/packaging.py | 556 ++++++ Scripts/pmon/signing.py | 72 + Scripts/pmon/web.py | 49 + Scripts/setup_dev_certificate.py | 265 +++ Scripts/sign.py | 43 + Scripts/versioning.py | 149 ++ Scripts/web.py | 44 + Tests/CMakeLists.txt | 29 + Tests/PresentMonTestData.targets | 8 + Tests/PresentMonTests.cpp | 58 +- .../PresentMonTests.generated.runsettings.in | 25 + Tests/PresentMonTests.runsettings | 24 + Tests/PresentMonTests.runsettings.template | 21 +- Tests/PresentMonTests.vcxproj | 5 +- Tests/README.md | 40 +- Tests/auxdata.md | 44 +- Tests/ensure-aux.ps1 | 115 ++ {ETLTrimmer => Tools/ETLTrimmer}/.gitignore | 0 Tools/ETLTrimmer/CMakeLists.txt | 39 + {ETLTrimmer => Tools/ETLTrimmer}/CliOptions.h | 2 +- .../ETLTrimmer}/ETLTrimmer.args.json | 0 .../ETLTrimmer}/ETLTrimmer.vcxproj | 12 +- .../ETLTrimmer}/ETLTrimmer.vcxproj.filters | 0 {ETLTrimmer => Tools/ETLTrimmer}/main.cpp | 18 +- Tools/Reflector/CMakeLists.txt | 88 + {Reflector => Tools/Reflector}/Program.cs | 51 +- .../Reflector/Properties/launchSettings.json | 7 + .../Reflector}/Reflector.csproj | 0 .../Reflector}/StructDumpers.h.scriban | 0 Tools/etw_list/CMakeLists.txt | 20 + Tools/pm_convert_csv/CMakeLists.txt | 26 + Version.props | 1 - cmake/PresentMonAuxTestData.cmake | 54 + cmake/PresentMonCTest.cmake | 602 +++++++ cmake/PresentMonCef.cmake | 218 +++ cmake/PresentMonCompiler.cmake | 71 + cmake/PresentMonCppUnitTest.cmake | 62 + cmake/PresentMonGenerateEnumMetric.cmake | 24 + cmake/PresentMonMessageCompiler.cmake | 60 + cmake/PresentMonOptions.cmake | 194 ++ cmake/PresentMonOutput.cmake | 43 + cmake/PresentMonPackaging.cmake | 1603 +++++++++++++++++ cmake/PresentMonPackagingPreflight.cmake | 205 +++ cmake/PresentMonPackagingVerifyOutput.cmake | 13 + cmake/PresentMonPython.cmake | 76 + cmake/PresentMonShaders.cmake | 176 ++ cmake/PresentMonSigning.cmake | 349 ++++ cmake/PresentMonSigningPreflight.cmake | 14 + cmake/PresentMonUci.cmake | 78 + cmake/PresentMonUiPayload.cmake | 200 ++ cmake/PresentMonVcpkg.cmake | 164 ++ cmake/PresentMonVersion.cmake | 87 + cmake/PresentMonVisualStudio.cmake | 310 ++++ requirements.txt | 2 + 118 files changed, 11550 insertions(+), 359 deletions(-) create mode 100644 CMakeLists.txt create mode 100644 IntelPresentMon/AppCef/CMakeLists.txt create mode 100644 IntelPresentMon/AppCef/web-dist-lock.json create mode 100644 IntelPresentMon/CommonUtilities/CMakeLists.txt create mode 100644 IntelPresentMon/CommonUtilities/Metrics.cmake create mode 100644 IntelPresentMon/ControlLib/CMakeLists.txt create mode 100644 IntelPresentMon/Core/CMakeLists.txt create mode 100644 IntelPresentMon/Interprocess/CMakeLists.txt create mode 100644 IntelPresentMon/KernelProcess/CMakeLists.txt create mode 100644 IntelPresentMon/PmonCMakePackaging.wixproj.props create mode 100644 IntelPresentMon/PresentMonAPI2/CMakeLists.txt create mode 100644 IntelPresentMon/PresentMonAPI2Loader/CMakeLists.txt create mode 100644 IntelPresentMon/PresentMonAPI2Tests/CMakeLists.txt create mode 100644 IntelPresentMon/PresentMonAPIWrapper/CMakeLists.txt create mode 100644 IntelPresentMon/PresentMonAPIWrapperCommon/CMakeLists.txt create mode 100644 IntelPresentMon/PresentMonMiddleware/CMakeLists.txt create mode 100644 IntelPresentMon/PresentMonService/CMakeLists.txt create mode 100644 IntelPresentMon/SampleClient/CMakeLists.txt create mode 100644 IntelPresentMon/UnitTests/CMakeLists.txt create mode 100644 IntelPresentMon/Versioning/CMakeLists.txt create mode 100644 PresentData/CMakeLists.txt create mode 100644 PresentMon/CMakeLists.txt create mode 100644 Provider/CMakeLists.txt delete mode 100644 Reflector/Properties/launchSettings.json create mode 100644 Scripts/auxdata.py create mode 100644 Scripts/bootstrap.py create mode 100644 Scripts/cef.py create mode 100644 Scripts/discover_tests.py create mode 100644 Scripts/generate_etl_csv_tests.py create mode 100644 Scripts/package.py create mode 100644 Scripts/pmon/__init__.py create mode 100644 Scripts/pmon/auxdata.py create mode 100644 Scripts/pmon/cef.py create mode 100644 Scripts/pmon/common.py create mode 100644 Scripts/pmon/discovery.py create mode 100644 Scripts/pmon/packaging.py create mode 100644 Scripts/pmon/signing.py create mode 100644 Scripts/pmon/web.py create mode 100644 Scripts/setup_dev_certificate.py create mode 100644 Scripts/sign.py create mode 100644 Scripts/versioning.py create mode 100644 Scripts/web.py create mode 100644 Tests/CMakeLists.txt create mode 100644 Tests/PresentMonTestData.targets create mode 100644 Tests/PresentMonTests.generated.runsettings.in create mode 100644 Tests/PresentMonTests.runsettings create mode 100644 Tests/ensure-aux.ps1 rename {ETLTrimmer => Tools/ETLTrimmer}/.gitignore (100%) create mode 100644 Tools/ETLTrimmer/CMakeLists.txt rename {ETLTrimmer => Tools/ETLTrimmer}/CliOptions.h (96%) rename {ETLTrimmer => Tools/ETLTrimmer}/ETLTrimmer.args.json (100%) rename {ETLTrimmer => Tools/ETLTrimmer}/ETLTrimmer.vcxproj (93%) rename {ETLTrimmer => Tools/ETLTrimmer}/ETLTrimmer.vcxproj.filters (100%) rename {ETLTrimmer => Tools/ETLTrimmer}/main.cpp (96%) create mode 100644 Tools/Reflector/CMakeLists.txt rename {Reflector => Tools/Reflector}/Program.cs (90%) create mode 100644 Tools/Reflector/Properties/launchSettings.json rename {Reflector => Tools/Reflector}/Reflector.csproj (100%) rename {Reflector => Tools/Reflector}/StructDumpers.h.scriban (100%) create mode 100644 Tools/etw_list/CMakeLists.txt create mode 100644 Tools/pm_convert_csv/CMakeLists.txt create mode 100644 cmake/PresentMonAuxTestData.cmake create mode 100644 cmake/PresentMonCTest.cmake create mode 100644 cmake/PresentMonCef.cmake create mode 100644 cmake/PresentMonCompiler.cmake create mode 100644 cmake/PresentMonCppUnitTest.cmake create mode 100644 cmake/PresentMonGenerateEnumMetric.cmake create mode 100644 cmake/PresentMonMessageCompiler.cmake create mode 100644 cmake/PresentMonOptions.cmake create mode 100644 cmake/PresentMonOutput.cmake create mode 100644 cmake/PresentMonPackaging.cmake create mode 100644 cmake/PresentMonPackagingPreflight.cmake create mode 100644 cmake/PresentMonPackagingVerifyOutput.cmake create mode 100644 cmake/PresentMonPython.cmake create mode 100644 cmake/PresentMonShaders.cmake create mode 100644 cmake/PresentMonSigning.cmake create mode 100644 cmake/PresentMonSigningPreflight.cmake create mode 100644 cmake/PresentMonUci.cmake create mode 100644 cmake/PresentMonUiPayload.cmake create mode 100644 cmake/PresentMonVcpkg.cmake create mode 100644 cmake/PresentMonVersion.cmake create mode 100644 cmake/PresentMonVisualStudio.cmake create mode 100644 requirements.txt diff --git a/.gitignore b/.gitignore index d4492e415..38e42315e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,10 +1,16 @@ -## Ignore Visual Studio temporary files, build results, and +## Ignore Visual Studio temporary files, build results, and ## files generated by popular Visual Studio add-ons. # Build results /build/ +/build-*/ +__pycache__/ +*.py[cod] PresentMon/build/ +# Local MSI/MSM parity and upgrade references (see Design/Phase6-Packaging.md) +/MSI/ + # Claude Code .claude/ @@ -51,15 +57,15 @@ IntelPresentMon/PresentMonAPI2/Internal.h IntelPresentMon/PMInstallerLib/Generated/ # Reflector Tool -Reflector/bin/** -Reflector/obj/** +Tools/Reflector/bin/** +Tools/Reflector/obj/** # Misc ephermeral files IntelPresentMon/SampleClient/log.txt -ETLTrimmer/Release/ETLTrimmer.Build.CppClean.log +Tools/ETLTrimmer/Release/ETLTrimmer.Build.CppClean.log -ETLTrimmer/Release/ETLTrimmer.vcxproj.FileListAbsolute.txt +Tools/ETLTrimmer/Release/ETLTrimmer.vcxproj.FileListAbsolute.txt PresentMon/ddETWExternalEvents_MSG00001.bin diff --git a/BUILDING.md b/BUILDING.md index 3b4298e96..f3cd047da 100644 --- a/BUILDING.md +++ b/BUILDING.md @@ -1,107 +1,741 @@ -# Building PresentMon +# Building PresentMon with CMake -## Install Build Tool Dependencies +This file is the source of truth for building PresentMon from source. PresentMon +currently supports Windows builds generated by CMake for Visual Studio 2022 and +Visual Studio 2026. The commands below are run from the repository root, where +`CMakeLists.txt` and `requirements.txt` are located. -- Visual Studio 2022 +This guide covers **Developer** builds only. Most developers use the +**standalone console** or the **full Developer configuration** (x64). The +**standalone ETW provider** and **Reflector** code generator are optional, +specialized paths with their own binary directories. -- [vcpkg](https://github.com/microsoft/vcpkg) +**Primary build paths** -- [CMake](https://cmake.org) +1. [Build the standalone console](#build-the-standalone-console) — + `PMON_BUILD_SCOPE=CONSOLE` on x64 or Win32; optional console ETL regression + tests. +2. [Build the full Developer configuration](#build-the-full-developer-configuration) — + x64 default (`PMON_BUILD_SCOPE=FULL`): console, service, SDK, capture UI, + x64 ETW provider, tools (including ETLTrimmer), and tests. -- [Node.js / NPM](https://nodejs.org/en/download) +**See also (optional)** -- [v3 of the WiX toolset AND VS extension](https://wixtoolset.org/docs/wix3/) +- [Build the standalone provider](#build-the-standalone-provider) — + `PMON_BUILD_SCOPE=PROVIDER` on x64 or Win32. +- [Build Reflector (code generation)](#build-reflector-code-generation) — + separate CMake project under `Tools/Reflector` (x64 only); no C++ product targets. -Note: if you only want to build the PresentData library, or the PresentMon Console application -you only need Visual Studio. Ignore the other build and source dependency instructions and build -`PresentData\PresentData.vcxproj` or `PresentMon\ConsoleApplication.sln`. +Developer Debug builds are unsigned. Developer Release builds use a local test +certificate for `PresentMon.exe` and automatically create an unsigned developer +MSI. The developer MSI is for local testing only and is not an official release +package. -## Install Source Dependencies +## Common setup -1. Run the repository bootstrap script: +### Supported Visual Studio versions - ```powershell - > cd PresentMonRepoDir - > .\bootstrap.ps1 - ``` +Install one of the following Visual Studio versions. CMake selects the native +toolset from the generator; do not pass `-T`. - The bootstrap script: +| Visual Studio | CMake generator | Native toolset | +| --- | --- | --- | +| Visual Studio 2022 | `Visual Studio 17 2022` | v143 | +| Visual Studio 2026 | `Visual Studio 18 2026` | v145 | - - Restores the locked Chromium Embedded Framework (CEF) payload. - - Pulls the pinned auxiliary test data. - - Installs and builds the AppCef web UI. +Install the `Desktop development with C++` workload, the native MSVC x86/x64 +build tools for the selected Visual Studio version, and a Windows 10 or Windows +11 SDK. Install the Visual Studio vcpkg component unless you will provide a +separate vcpkg checkout. - See the detailed guides for [CEF lock management](IntelPresentMon/AppCef/ceflock.md), [AppCef web UI setup](IntelPresentMon/AppCef/webui.md), and [auxiliary test data](Tests/auxdata.md). +Each Visual Studio generator and target architecture requires its own CMake +binary directory. Do not configure Visual Studio 2022 and Visual Studio 2026, +or x64 and Win32, into the same directory. Debug and Release are +multi-configuration variants and should use the same binary directory. -2. Create and install a trusted test certificate. This is only required for the Release build. Open a command shell as administrator and run the following: +### Software required for every build - ```bat - > makecert -r -pe -n "CN=Test Certificate - For Internal Use Only" -ss PrivateCertStore testcert.cer - > certutil -addstore root testcert.cer - ``` +- Windows 10 or Windows 11 on an x64 build machine. +- Visual Studio 2022 or Visual Studio 2026 with the components described above. +- CMake 4.2.2 or newer, available as `cmake` on `PATH`. +- Python 3.10 or newer with pip. The examples use the Windows `py` launcher. +- Git, available as `git` on `PATH`. +- vcpkg from the selected Visual Studio installation, a repository-local + `vcpkg` checkout, or `VCPKG_ROOT`. -## Building PresentMon +Standalone console and provider builds do not require Node.js, CEF, ATL, WiX, +SignTool, a test certificate, or the Microsoft C++ unit-test framework. +GoogleTest is restored through vcpkg only when console ETL tests are enabled. -Build `PresentMon.sln` in Visual Studio or msbuild. e.g.: +### Additional software for the full Developer build -```bat -> msbuild /p:Platform=x64,Configuration=Release PresentMon.sln +A full Developer Debug build also requires: + +- the Visual Studio Test Tools for C++ used by `PresentMonUnitTests` and + `PresentMonAPI2Tests`; +- the `C++ ATL for latest v143 build tools (x86 & x64)` component for Visual + Studio 2022, or the corresponding latest v145 ATL component for Visual Studio + 2026; and +- Node.js 20.19 or newer in the Node 20 line, or Node.js 22.12 or newer, with + npm. + +A full Developer Release build additionally requires: + +- a Windows SDK containing SignTool; +- WiX Toolset 3.x, normally WiX 3.14; and +- the .NET Framework 4.8 targeting pack. + +WiX 4 is not compatible with the current installer authoring. + +### Environment variables + +A standard installation does not require any PresentMon-specific environment +variables. CMake finds the Visual Studio instance from the `-G` argument and +uses vcpkg from that selected instance when no other vcpkg checkout is supplied. + +Set only the variables needed for a nonstandard installation: + +| Variable | When it is needed | +| --- | --- | +| `VCPKG_ROOT` | vcpkg is not installed in the selected Visual Studio instance and there is no `vcpkg` directory in the repository root | +| `WIX` | the WiX Toolset 3 binaries are not installed in their normal Program Files location; the shared WiX 3 MSBuild targets must still be installed in their standard location | + +Set these before the first configure in the same PowerShell session. For +example: + +```powershell +$env:VCPKG_ROOT = "C:\Source\vcpkg" +$env:WIX = "C:\Program Files (x86)\WiX Toolset v3.14" +``` + +`VCPKG_ROOT` and `WIX` are not needed when the corresponding software is in a +standard location. If `VCPKG_ROOT` is set from an older workflow, either keep +that checkout current enough for Visual Studio 2026 (see +[Troubleshooting](#visual-studio-2026-configure-and-vcpkg)) or unset it when +configuring with `-G "Visual Studio 18 2026"` so CMake uses the vcpkg bundled +with that Visual Studio instance. UCI is optional in the full Developer build: +its default `AUTO` mode enables UCI when a complete SDK is found and otherwise +builds without it. Standalone console and provider builds do not use UCI. + +The CMake cache alternatives for nonstandard locations are +`-DPMON_WIX_ROOT=...`, `-DPMON_UCI_ROOT=...`, and +`-DPMON_DEPENDENCY_ROOT=...`. `PMON_DEPENDENCY_ROOT` defaults to +`build\ThirdParty` under the repository root. + +### Command preflight + +Open PowerShell, change to the PresentMon repository root, and confirm the common +tools before configuring: + +```powershell +if (-not (Test-Path .\CMakeLists.txt)) { throw "Run this guide from the PresentMon repository root." } +cmake --version +py -3 --version +py -3 -m pip --version +git --version +if ($env:VCPKG_ROOT) { & "$env:VCPKG_ROOT\vcpkg.exe" version } else { "VCPKG_ROOT unset; configure uses vcpkg from the Visual Studio instance selected by -G" } +``` + +For a full Developer build, also confirm Node.js and npm: + +```powershell +node --version +npm --version +``` + +If `py` is unavailable but `python.exe` is on `PATH`, use `python` instead of +`py -3` throughout this guide. + +Install the Python tooling entry point once: + +```powershell +py -3 -m pip install -r requirements.txt +``` + +The build tooling currently uses only the Python standard library. +`requirements.txt` remains the installation entry point for future pinned +Python packages. + +### First-configure downloads and shared dependencies + +A clean configure requires HTTPS access to GitHub and GitHub release assets, +the vcpkg registry, and the AuxData location recorded by the repository. A full +Developer configure also accesses the npm registry and the CEF download +location recorded in `cef-lock.json`. Developer Release signing needs access to +the configured timestamp service. + +The first configure can take substantially longer than later configures. CMake +installs the vcpkg manifest. When console tests are enabled, it also restores +the pinned AuxData used by the ETL tests. A full Developer configure restores +AuxData, CEF, builds the CEF wrapper libraries, and runs `npm ci` for the web +application. + +All binary directories share restored dependencies under `build\ThirdParty` by +default. Compiled vcpkg installations and CEF stages are separated by Visual +Studio toolset so VS 2022 and VS 2026 builds do not overwrite one another. Keep +several gigabytes free on both the checkout drive and system drive; temporary +CEF extraction uses `%SystemDrive%\pcef` by default. + +## Build the standalone console + +This is the smallest supported PresentMon build. Selecting +`PMON_BUILD_SCOPE=CONSOLE` enables `PresentMonConsole` and disables the service, +SDK, capture UI, provider, standalone tools, and packaging. CEF and web +preparation are skipped. Console tests default to off. Add +`-DPMON_BUILD_TESTS=ON` during configure when the ETL regression suite is +wanted; that enables `PresentMonTests` while the Microsoft C++ unit tests and +API2 tests remain disabled. + +### Configure x64 + +For Visual Studio 2022: + +```powershell +cmake -B build-console-vs2022-x64 -S . -G "Visual Studio 17 2022" -A x64 -DPMON_BUILD_SCOPE=CONSOLE +``` + +For Visual Studio 2026: + +```powershell +cmake -B build-console-vs2026-x64 -S . -G "Visual Studio 18 2026" -A x64 -DPMON_BUILD_SCOPE=CONSOLE +``` + +### Configure Win32 + +Win32 produces the x86 console and the x86 form of `PresentMonTests`. Use a +separate binary directory from x64. + +For Visual Studio 2022: + +```powershell +cmake -B build-console-vs2022-win32 -S . -G "Visual Studio 17 2022" -A Win32 -DPMON_BUILD_SCOPE=CONSOLE +``` + +For Visual Studio 2026: + +```powershell +cmake -B build-console-vs2026-win32 -S . -G "Visual Studio 18 2026" -A Win32 -DPMON_BUILD_SCOPE=CONSOLE +``` + +### Build the console + +Set `$buildDir` to the binary directory you configured, then select Debug or +Release. Standalone console Release outputs are unsigned and do not require +signing or installer prerequisites. + +```powershell +$buildDir = "build-console-vs2022-x64" +cmake --build $buildDir --config Debug --target PresentMonConsole +cmake --build $buildDir --config Release --target PresentMonConsole +``` + +The versioned executable is written directly to the selected configuration +directory: + +```text +\Debug\PresentMon--x64.exe +\Release\PresentMon--x64.exe +\Debug\PresentMon--x86.exe +\Release\PresentMon--x86.exe +``` + +To enable the tests after configuring a console-only tree, reconfigure the same +tree with `PMON_BUILD_TESTS=ON`. The generator, architecture, and build scope +are already recorded in the binary directory: + +```powershell +cmake -B $buildDir -S . -DPMON_BUILD_TESTS=ON +``` + +Then build `PresentMonTests`. It depends on the matching console target, so this +command also builds the console when necessary: + +```powershell +cmake --build $buildDir --config Debug --target PresentMonTests +``` + +List and run only the deterministic gold ETL/CSV tests with: + +```powershell +ctest --test-dir $buildDir -C Debug -L console.gold -N +ctest --test-dir $buildDir -C Debug -L console.gold --output-on-failure +``` + +The `console.gold` label excludes the realtime command-line cases that need a +presenting graphics application and live ETW access. Substitute `Release` for +`Debug` after building the Release `PresentMonTests` target. + +`PresentMonTests` runs the cases in `Tests\Gold` plus one optional ETL/CSV +directory. That optional directory is the pinned `Tests\AuxData\Data` by +default, which is where the AuxData cases come from. + +`PMON_PRESENTMON_TESTS_CUSTOM_DIR` points the optional directory somewhere else. +It **replaces** the pinned AuxData directory rather than adding a second one, so +a run configured this way registers the `Tests\Gold` cases plus the custom cases +and no AuxData cases. Clear the option to go back to AuxData. Pass an absolute +path on the configure command and then rebuild `PresentMonTests`: + +```powershell +cmake -B $buildDir -S . -DPMON_PRESENTMON_TESTS_CUSTOM_DIR="D:/TestData/PresentMon" +cmake --build $buildDir --config Debug --target PresentMonTests +ctest --test-dir $buildDir -C Debug -L console.gold --output-on-failure +``` + +```powershell +cmake -B $buildDir -S . -DPMON_PRESENTMON_TESTS_CUSTOM_DIR= +``` + +ETL files in that directory must be named `test_case_.etl`, each with at +least one matching gold CSV that shares the ETL stem, such as `test_case_42.etl` +with `test_case_42.csv` and optionally `test_case_42_v1.csv`. Files named any +other way are ignored. Configure warns when the directory contains no matching +ETL files, since the result would be a run of `Tests\Gold` alone. + +Use `-DPMON_CTEST_FULL_CSV_TESTS=ON` during configure to register the optional +`Tests\Full` CSV gold suite when that data is available. That data is not part +of the repository. Populate `Tests\Full` with `test_case_.etl` files and +their matching gold CSVs; ETL files named any other way are ignored. The suite +registers no tests when the directory is missing or holds no matching pairs, and +configure warns when the option is on without the directory present. Tests are +discovered on each CTest invocation, so populating the directory later takes +effect without reconfiguring. + +## Build the full Developer configuration + +The full Developer configuration is x64 and enables the console, service, SDK, +capture UI, x64 ETW provider (`Intel-PresentMon.dll`), the x64 tools slice +(including ETLTrimmer), and all test projects. It does not configure Win32 +console or Win32 provider trees. For provider-only builds on x64 or Win32, see +[Build the standalone provider](#build-the-standalone-provider). Configure the +full tree once, then use the same binary directory for Debug and Release. +`PMON_BUILD_SCOPE` defaults to `FULL`, so the configure commands below do not +need an extra scope option. + +### Configure with Visual Studio 2022 + +```powershell +cmake -B build -S . -G "Visual Studio 17 2022" -A x64 +``` + +The generated solution is `build\PresentMon.sln`. + +### Configure with Visual Studio 2026 + +```powershell +cmake -B build-vs2026 -S . -G "Visual Studio 18 2026" -A x64 +``` + +The generated solution is `build-vs2026\PresentMon.slnx`. + +### Developer Debug + +Build the complete Debug configuration with one of these commands: + +```powershell +cmake --build build --config Debug +cmake --build build-vs2026 --config Debug +``` + +Run only the command matching the generator you configured. Runnable output is +under `\Debug`. + +## Set up the Developer Release test certificate + +Developer Release signs `PresentMon.exe` with a local test certificate. The +required certificate has the subject `Test Certificate - For Internal Use +Only`, includes a private key in `CurrentUser\My`, and has the same certificate +trusted in `LocalMachine\Root`. + +Check the certificate without changing the machine: + +```powershell +py -3 Scripts\setup_dev_certificate.py --check +``` + +If the check fails, run the setup from a normal PowerShell: + +```powershell +py -3 Scripts\setup_dev_certificate.py +``` + +The script is idempotent. It creates or locates the certificate in +`CurrentUser\My`. If administrator rights are needed to trust it, the script +writes `build\ThirdParty\setup\presentmon-testcert.cer` and prints the exact +`certutil` command to run from an elevated Command Prompt. After running that +command, return to the original PowerShell and verify the setup: + +```powershell +py -3 Scripts\setup_dev_certificate.py --check +``` + +Alternatively, run the setup script from an elevated PowerShell to create and +trust the certificate in one operation, then run `--check`. + +CMake does not check the certificate during configure because the generated +Visual Studio tree can build either Debug or Release, and Debug does not need a +certificate. A missing or untrusted certificate causes the Release build to +fail when it reaches the signing step. Running `--check` before the Release +build provides the intended early validation. + +## Developer Release and MSI + +After WiX 3, the .NET Framework 4.8 targeting pack, SignTool, and the developer +certificate are ready, build Release with one of these commands: + +```powershell +cmake --build build --config Release +cmake --build build-vs2026 --config Release +``` + +Run only the command matching the generator you configured. The default +Developer Release build compiles the complete Release configuration, signs +`PresentMon.exe` with the local test certificate, and automatically creates: + +```text +\Release\en-us\PresentMon.msi +``` + +The MSI is unsigned and is intended only for local developer installation and +validation. Runnable Release output is under `\Release`. + +## Set up machine expectations + +A small group of `PresentMonAPI2Tests` cases assert hardware telemetry: CPU and +GPU names, memory size, and observed ranges for utilization, frequency, and +power. Those values differ on every machine, so the tests compare against a +per-machine baseline in `IntelPresentMon\machine_expectations.json`. The file is +machine-local and is not committed. + +Like the developer certificate, this is a one-time setup step. Until it is +done, the affected tests fail with a message naming the file and the command to +generate it. Check the current state without changing anything: + +```powershell +py -3 IntelPresentMon\PresentMonAPI2Tests\update_machine_expectations.py --check +``` + +Unlike the certificate, the baseline cannot be created before the tests have +run, because it is generated from measurements the tests record. Provisioning is +therefore a record-then-generate loop, and the first run is expected to fail. + +Build the Debug configuration, then record measurements by running the tests +that produce them: + +```powershell +ctest --test-dir build -C Debug -R "InterimBroadcasterTests|IpcMcIntegrationTests" --output-on-failure +``` + +Measurements accumulate in +`\Debug\TestOutput\MachineExpectations\measurements.jsonl`. Delete +that file before re-recording if you want a clean set. Then open the generator: + +```powershell +py -3 IntelPresentMon\PresentMonAPI2Tests\update_machine_expectations.py --measurements build\Debug\TestOutput\MachineExpectations\measurements.jsonl +``` + +The generator opens an editor showing the values it derived, and writes the file +when you save. + +**Widen the ranges before saving.** The generated minimum and maximum for each +volatile metric describe only what that one run happened to observe, which is +almost always too narrow to survive the next run. A machine that measured +`PM_METRIC_CPU_UTILIZATION` between 23.8 and 100 will fail the next run at 23.5. +These tests check that a metric is populated and plausible, not that the machine +is in a particular state, so prefer bounds that describe what is physically +sensible: `0` to `100` for a utilization percentage, `0` to a comfortable +ceiling for frequency and power. Identity and capability values such as +`PM_METRIC_GPU_NAME` and `PM_METRIC_GPU_MEM_SIZE` are stable and should be left +as generated. + +The baseline also carries `wait_multiplier`, which scales how long these tests +wait for asynchronous results. It defaults to `1.0` and is floored at `1.0`. +Raise it on a slow machine, or when a test fails with a message such as +`Timed out waiting for playback frames` while passing when run on its own — that +pattern means the full suite is loading the machine enough to exceed the default +waits rather than that anything is broken. + +Verify the result, optionally checking that the baseline covers the metrics this +machine actually reports: + +```powershell +py -3 IntelPresentMon\PresentMonAPI2Tests\update_machine_expectations.py --check +py -3 IntelPresentMon\PresentMonAPI2Tests\update_machine_expectations.py --check --measurements build\Debug\TestOutput\MachineExpectations\measurements.jsonl +``` + +Without `--measurements`, `--check` confirms only that the file exists and is +well formed; it cannot tell whether the baseline was authored on this machine. +With `--measurements` it also verifies that every measured metric has an +expectation and that the recorded CPU and GPU identity match. + +Re-record and regenerate after a hardware change, a driver change that alters +reported metrics, or when a new metric is added to these tests. + +## Run tests with CTest + +Tests are enabled by default in a full Developer configuration. Build the +configuration before running CTest. Use the same binary directory and +configuration for both commands. Complete +[Set up machine expectations](#set-up-machine-expectations) first, or the +hardware telemetry cases fail. + +List all registered Debug tests without running them: + +```powershell +ctest --test-dir build -C Debug -N +``` + +The recommended default is the deterministic suite: + +```powershell +ctest --test-dir build -C Debug -L deterministic --output-on-failure +``` + +Run only the standalone console gold ETL/CSV tests with: + +```powershell +ctest --test-dir build -C Debug -L console.gold --output-on-failure +``` + +Run every registered test with: + +```powershell +ctest --test-dir build -C Debug --output-on-failure ``` -## Running PresentMon +The complete suite includes environment-dependent realtime tests. Those cases +can require a presenting graphics application, live ETW access, and membership +in the `Performance Log Users` group. Use the `deterministic` label on machines +that do not provide those runtime prerequisites. + +The `deterministic` label means the case does not depend on the runtime +environment above. It does not mean the case needs no setup: the hardware +telemetry cases are labeled `deterministic` and still require the one-time +[machine expectations](#set-up-machine-expectations) baseline, in the same way +that a Developer Release build requires the one-time test certificate. + +For Visual Studio 2026, replace `build` with `build-vs2026`. Substitute +`Release` for `Debug` only after building the Release configuration. + +### Test discovery + +CTest registers each Google Test and Microsoft C++ Unit Test method as a +separate test. `PresentMonTests` creates Google Test cases from the current +contents of the Gold, pinned AuxData, and configured optional ETL/CSV +directories. Discovery therefore runs whenever CTest loads the test tree so +changes to those external files are reflected without rebuilding the test +executable. CMake's native `gtest_discover_tests()` pre-test mode caches its +inventory based on the executable and generated include timestamps, which does +not track these external data directories. + +Google Test discovery clears `PRESENTMON_ADDITIONAL_TEST_DIR` so a developer's +environment cannot silently change the registered CTest inventory. Microsoft +C++ Unit Test methods are discovered through `vstest.console.exe` from the +Visual Studio instance selected by CMake because CMake has no native VSTest +leaf-test discovery facility. Both discovery paths assign the labels, fixtures, +and resource locks used by the commands above. + +## Build the standalone provider + +The ETW provider can be built alone when the full PresentMon stack is not +needed. Selecting `PMON_BUILD_SCOPE=PROVIDER` enables only `pmon_provider` and +disables the console, service, SDK, capture UI, standalone tools, tests, and +packaging. Use a dedicated binary directory; do not reuse a console or full +Developer tree. -### Intel PresentMon +Win32 and x64 provider builds are independent. The Win32 DLL is +`Intel-PresentMon32.dll`; the x64 DLL is `Intel-PresentMon.dll`. -Intel PresentMon is the UI application, `PresentMon.exe`. +### Configure Win32 -For Debug builds, the easiest IDE workflow is to set `Client/KernelProcess` as the startup project and launch `PresentMon.exe` with the service running as a child process: +For Visual Studio 2022: -```bat -> --svc-as-child --files-working --log-level verbose --middleware-dll-path .\PresentMonAPI2.dll --log-middleware-copy +```powershell +cmake -B build-provider-vs2022-win32 -S . -G "Visual Studio 17 2022" -A Win32 -DPMON_BUILD_SCOPE=PROVIDER ``` -For Release builds, either move the full Release output payload to a secure directory such as "Program Files" or "System32", or disable the secure directory check for local development. You cannot run release builds from the IDE typically. The installer is often the easier path for Release validation: +For Visual Studio 2026: -```bat -> build\Release\en-us\PresentMon.msi +```powershell +cmake -B build-provider-vs2026-win32 -S . -G "Visual Studio 18 2026" -A Win32 -DPMON_BUILD_SCOPE=PROVIDER ``` -### PresentMon Service +### Configure x64 -To start the service, open a command window as Administrator, then run the following commands (using the full binPath to your build executable): +For Visual Studio 2022: -```bat -> sc.exe create PresentMonService binPath="C:\...\PresentMonRepoDir\build\Release\PresentMonService.exe" -> sc.exe start PresentMonService +```powershell +cmake -B build-provider-vs2022-x64 -S . -G "Visual Studio 17 2022" -A x64 -DPMON_BUILD_SCOPE=PROVIDER ``` -When you are finished, stop and remove the service with: +For Visual Studio 2026: -```bat -> sc.exe stop PresentMonService -> sc.exe delete PresentMonService +```powershell +cmake -B build-provider-vs2026-x64 -S . -G "Visual Studio 18 2026" -A x64 -DPMON_BUILD_SCOPE=PROVIDER ``` -### PresentMon Standalone Console +### Build the provider -The standalone console application is `PresentMon-dev-x64.exe`: +```powershell +$buildDir = "build-provider-vs2022-win32" +cmake --build $buildDir --config Debug --target pmon_provider +cmake --build $buildDir --config Release --target pmon_provider +``` + +Output is written to the selected configuration directory: + +```text +\Debug\Intel-PresentMon32.dll +\Release\Intel-PresentMon32.dll +\Debug\Intel-PresentMon.dll +\Release\Intel-PresentMon.dll +``` + +Standalone provider Release outputs are unsigned. -```bat -> build\Release\PresentMon-dev-x64.exe +## Build Reflector (code generation) + +Reflector regenerates `IntelPresentMon/CommonUtilities/ref/gen/GeneratedReflection.h` +from vendor headers. Configure the standalone CMake project in `Tools/Reflector` +(no vcpkg, Python bootstrap, CEF, C++ targets, tests, or packaging). Requires +the [.NET 8 SDK](https://dotnet.microsoft.com/download) on PATH. Use a dedicated +x64 binary directory (separate from repository-root C++ builds). + +### Configure x64 + +For Visual Studio 2022: + +```powershell +cmake -B build-reflector-vs2022-x64 -S Tools/Reflector -G "Visual Studio 17 2022" -A x64 ``` +For Visual Studio 2026: + +```powershell +cmake -B build-reflector-vs2026-x64 -S Tools/Reflector -G "Visual Studio 18 2026" -A x64 +``` + +### Build and run codegen + +```powershell +$buildDir = "build-reflector-vs2022-x64" +cmake --build $buildDir --config Release --target pmon_reflector +cmake --build $buildDir --config Release --target pmon_reflector_codegen +``` + +`pmon_reflector_codegen` runs `dotnet run` for the Reflector project. It is not +built as part of `FULL` or other C++ scopes. + +Reflector uses CppAst (libclang) to parse four vendor headers (IGCL, NVAPI, NVML, +ADL). Expect CPU-heavy work with little console output after each `Parsing …` +line; IGCL is often the slowest step (on the order of ~20 minutes on a typical +dev machine), even though NVAPI is larger by line count. Do not start a second +codegen while Reflector is still running (the executable may stay locked). + +## Running the capture application + +The capture application target is `KernelProcess`; its output is +`PresentMon.exe`. For Debug, open the generated solution, set `KernelProcess` +as the startup project, and use these arguments: + +```text +--svc-as-child --files-working --log-level verbose --middleware-dll-path .\PresentMonAPI2.dll --log-middleware-copy +``` + +Use `\Debug` as the working directory. With `--svc-as-child`, the +UI starts `PresentMonService.exe` from the same directory. + +Realtime ETW workflows normally require membership in `Performance Log Users`. +Sign out and back in after adding the user to that group. Debug uses +`uiAccess=false`. Release `PresentMon.exe` uses `uiAccess=true` and normally +runs from a secure location such as Program Files. ## Troubleshooting -- If you are seeing vcpkg errors when updating to a new version of PresentMon (e.g., "error: while checking out baseline from commit...") then try updating your vcpkg checkout. +If CMake reports a generator or architecture mismatch, use a fresh binary +directory. CMake cannot change `-G` or `-A` for an existing binary tree. + +If vcpkg is not found, install the vcpkg component in the selected Visual Studio +instance or set `VCPKG_ROOT` before configuring a fresh binary directory. + +### Visual Studio 2026 configure and vcpkg + +PresentMon supports machines that have both Visual Studio 2022 and Visual Studio +2026 installed. The `-G` argument selects the Visual Studio version and its +native toolset (`Visual Studio 17 2022` -> v143, `Visual Studio 18 2026` -> +v145). CMake and vcpkg must use that same instance and toolset. Without that +coupling, vcpkg tends to discover the newest installed toolset and compiled +dependencies can disagree with the toolset that links the PresentMon binaries. + +During configure, PresentMon pins the selected Visual Studio installation for +vcpkg and generates overlay triplets that set `VCPKG_PLATFORM_TOOLSET` to the +native toolset for the generator. Do not disable those pins or cross-pair +generators and toolsets. + +**Typical symptom.** Configure fails while running vcpkg install. The log at +`\vcpkg-manifest-install.log` contains text like `Unable to find a +valid Visual Studio instance` at +`C:\Program Files\Microsoft Visual Studio\18\Professional` with toolset version +`v145`, while only Visual Studio 2022 paths appear under `The following Visual +Studio instances were considered`. That usually means `VCPKG_ROOT` points at a +standalone vcpkg checkout that is too old to treat the Visual Studio 2026 +installation as valid, not that Visual Studio 2026 is missing. + +**Option 1: Use vcpkg from the Visual Studio 2026 instance (recommended when +you are not maintaining a separate checkout).** Install the vcpkg component in +the Visual Studio 2026 instance. Unset `VCPKG_ROOT` in the configure session, +or remove it from your profile when configuring with `-G "Visual Studio 18 +2026"`. Use a fresh binary directory and rerun the configure command from this +guide. + +```powershell +Remove-Item Env:VCPKG_ROOT -ErrorAction SilentlyContinue +cmake -B build-console-vs2026-x64 -S . -G "Visual Studio 18 2026" -A x64 -DPMON_BUILD_SCOPE=CONSOLE +``` + +**Option 2: Keep a standalone vcpkg checkout under `VCPKG_ROOT`.** Update that +clone before configuring for Visual Studio 2026. The vcpkg executable must be +new enough to recognize Visual Studio 2026 and v145. Updating the tool is +separate from the manifest baseline in `vcpkg-configuration.json`; change the +baseline only when the repository updates it or configure reports a registry +problem. + +```powershell +cd C:\Source\vcpkg +git pull +.\bootstrap-vcpkg.bat +.\vcpkg.exe version +$env:VCPKG_ROOT = "C:\Source\vcpkg" +cmake -B build-console-vs2026-x64 -S . -G "Visual Studio 18 2026" -A x64 -DPMON_BUILD_SCOPE=CONSOLE +``` + +Replace `C:\Source\vcpkg` with your checkout path. After updating vcpkg, +configure a fresh binary directory; do not reuse a tree that was partially +configured with an incompatible vcpkg. + +**Option 3: Build with Visual Studio 2022 only.** Use `-G "Visual Studio 17 +2022"` and a separate binary directory (for example `build-console-vs2022-x64`). +Do not point a Visual Studio 2026 generator at a Visual Studio 2022-only +toolchain or share one binary directory between generators. + +If CEF, AuxData, or npm content is missing or stale, rerun the original +configure command. Automatic dependency preparation validates and repairs the +locked inputs. -- Make sure vcpkg is using the same Visual Studio installation as the solution build. If needed, set `VCPKG_VISUAL_STUDIO_PATH` before running vcpkg. +If Developer Release signing fails, run: + +```powershell +py -3 Scripts\setup_dev_certificate.py --check +``` +The Windows dialog `A referral was returned from the server` means the Release +certificate chain is not trusted or the `uiAccess` executable is not running +from a secure location. -- If you get an error dialog from PresentMon.exe stating "A referral was returned form the server." - you most likely do not have the certificate that the PresentMon service was signed with installed - into your trusted root. Ensure that the trusted test certificate setup completed successfully. If - you built the installer on another PC or received it from a trusted third party, you need to - install the certificate on the target PC as well. +If the Developer Release build cannot find WiX, install WiX Toolset 3 or set +`WIX` before configuring. A complete installation must provide the shared WiX +3 MSBuild targets under the standard Program Files MSBuild location. If the WiX +tool binaries are installed outside their normal location, the equivalent +CMake option is `-DPMON_WIX_ROOT="C:/path/to/WiX"`. -- Add the development user to the Performance Log Users group to run from the IDE, run tests, etc. without launching the IDE as administrator. +If a rebuild reports that a test DLL is in use, close Visual Studio Test +Explorer and any remaining `testhost.exe` processes that loaded it. diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 000000000..755676e93 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,200 @@ +cmake_minimum_required(VERSION 4.2.2) + +if(NOT CMAKE_HOST_WIN32) + message(FATAL_ERROR "PresentMon currently supports Windows only.") +endif() + +list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/cmake") + +include(PresentMonOptions) + +if(DEFINED PMON_BUILD_SCOPE) + string(TOUPPER "${PMON_BUILD_SCOPE}" _pmon_requested_build_scope) + if(_pmon_requested_build_scope STREQUAL "REFLECTOR") + message( + FATAL_ERROR + "Reflector is a separate CMake project. Configure from Tools/Reflector " + "(see BUILDING.md). Example:\n" + " cmake -B build-reflector-vs2026-x64 -S Tools/Reflector " + "-G \"Visual Studio 18 2026\" -A x64" + ) + endif() +endif() + +pmon_define_build_scope_cache() + +include(PresentMonVisualStudio) +include(PresentMonVersion) +include(PresentMonVcpkg) +# The generator and its native toolset are settled before any dependency work +# so that compiled dependency directories can be qualified by toolset family. +pmon_configure_toolchain() +pmon_read_version("${CMAKE_CURRENT_SOURCE_DIR}/Version.props") +pmon_configure_vcpkg() + +project( + PresentMon + VERSION "${PMON_VERSION}" + DESCRIPTION "Intel PresentMon" + LANGUAGES C CXX +) + +pmon_validate_toolchain() + +if(NOT MSVC) + message(FATAL_ERROR "PresentMon currently requires the Microsoft Visual C++ toolchain.") +endif() + +set_property(GLOBAL PROPERTY USE_FOLDERS ON) + +if(CMAKE_CONFIGURATION_TYPES) + set( + CMAKE_CONFIGURATION_TYPES + "Debug;Release" + CACHE STRING + "Supported build configurations" + FORCE + ) +endif() + +include(PresentMonOptions) +include(PresentMonPython) +include(PresentMonOutput) +include(PresentMonCompiler) +include(PresentMonMessageCompiler) +include(PresentMonUci) +include(PresentMonSigning) +include(PresentMonCef) +include(PresentMonShaders) +include(PresentMonUiPayload) +include(PresentMonAuxTestData) +include(PresentMonCTest) + +pmon_configure_options() +pmon_configure_output_directories() +pmon_configure_python() +pmon_create_compiler_targets() +pmon_create_version_target() +pmon_create_version_resource_target() +pmon_configure_uci() +pmon_configure_signing() +pmon_prepare_dependencies() +if(PMON_ARCHITECTURE STREQUAL "x64" AND PMON_BUILD_UI) + pmon_configure_cef() +else() + set( + PMON_CEF_EFFECTIVE_ROOT + "Not applicable" + CACHE INTERNAL + "Resolved CEF stage" + FORCE + ) +endif() +if(PMON_ARCHITECTURE STREQUAL "x64" AND PMON_BUILD_UI) + pmon_configure_shaders() + pmon_configure_ui_payload() + pmon_configure_kernel_payload() +endif() + +if(NOT PMON_BUILD_SCOPE STREQUAL "PROVIDER") + add_subdirectory(IntelPresentMon/CommonUtilities) + add_subdirectory(PresentData) + include(IntelPresentMon/CommonUtilities/Metrics.cmake) +endif() + +if(PMON_BUILD_CONSOLE) + add_subdirectory(PresentMon) +endif() +if(PMON_BUILD_PROVIDER) + add_subdirectory(Provider) +endif() +if(PMON_BUILD_TOOLS) + if(PMON_ARCHITECTURE STREQUAL "x64") + add_subdirectory(Tools/ETLTrimmer) + endif() + add_subdirectory(Tools/etw_list) + add_subdirectory(Tools/pm_convert_csv) +endif() + +# The service, SDK, and UI stacks have no maintained Win32 configuration. +if(PMON_ARCHITECTURE STREQUAL "x64") + if(PMON_BUILD_SERVICE OR PMON_BUILD_SDK OR PMON_BUILD_UI) + add_subdirectory(IntelPresentMon/Versioning) + add_subdirectory(IntelPresentMon/Interprocess) + add_subdirectory(IntelPresentMon/ControlLib) + add_subdirectory(IntelPresentMon/PresentMonAPIWrapperCommon) + add_subdirectory(IntelPresentMon/PresentMonAPIWrapper) + add_subdirectory(IntelPresentMon/PresentMonService) + add_subdirectory(IntelPresentMon/PresentMonMiddleware) + add_subdirectory(IntelPresentMon/PresentMonAPI2) + add_subdirectory(IntelPresentMon/PresentMonAPI2Loader) + endif() + if(PMON_BUILD_SDK) + add_subdirectory(IntelPresentMon/SampleClient) + endif() + if(PMON_BUILD_UI) + add_subdirectory(IntelPresentMon/Core) + add_subdirectory(IntelPresentMon/AppCef) + add_subdirectory(IntelPresentMon/KernelProcess) + endif() +endif() + +if(PMON_BUILD_TESTS) + enable_testing() + pmon_configure_aux_testdata() + pmon_configure_presentmon_test_data_directory() + if(PMON_ARCHITECTURE STREQUAL "x64") + # IntelPresentMon/UnitTests and PresentMonAPI2Tests are x64-only (see Phase5-Tests.md). + else() + if(PMON_BUILD_UNIT_TESTS OR PMON_BUILD_API2_TESTS) + message( + STATUS + "Intel unit/API2 test options are set but ${PMON_ARCHITECTURE} has no test targets; only PresentMonTests may apply." + ) + endif() + endif() + if(PMON_BUILD_PRESENTMON_TESTS) + add_subdirectory(Tests) + endif() + if(PMON_ARCHITECTURE STREQUAL "x64") + if(PMON_BUILD_UNIT_TESTS) + add_subdirectory(IntelPresentMon/UnitTests) + endif() + if(PMON_BUILD_API2_TESTS) + add_subdirectory(IntelPresentMon/PresentMonAPI2Tests) + endif() + endif() + pmon_register_all_ctests() +endif() + +pmon_add_production_signing_targets() +pmon_add_win32_component_signing_targets() +include(PresentMonPackaging) +pmon_register_packaging_targets() + +message(STATUS "") +message(STATUS "PresentMon configuration") +message(STATUS " Version: ${PMON_VERSION}") +message(STATUS " File version: ${PMON_FILE_VERSION}") +message(STATUS " Generator: ${CMAKE_GENERATOR}") +message(STATUS " Platform toolset: ${PMON_VS_TOOLSET}") +message(STATUS " VS instance: ${PMON_VS_INSTANCE_ROOT}") +message(STATUS " Architecture: ${PMON_ARCHITECTURE}") +message(STATUS " Build scope: ${PMON_BUILD_SCOPE}") +message(STATUS " Deployment profile: ${PMON_DEPLOYMENT_PROFILE}") +message(STATUS " Output root: ${PMON_OUTPUT_ROOT}") +message(STATUS " Dependency root: ${PMON_DEPENDENCY_ROOT}") +message(STATUS " Python: ${PMON_PYTHON_EXECUTABLE}") +message(STATUS " UCI mode: ${PMON_ENABLE_UCI}") +message(STATUS " UCI included: ${PMON_UCI_ENABLED}") +message(STATUS " Signing backend: ${PMON_SELECTED_SIGNING_BACKEND}") +message(STATUS " CEF stage: ${PMON_CEF_EFFECTIVE_ROOT}") +message(STATUS " Build tests: ${PMON_BUILD_TESTS}") +if(PMON_BUILD_TESTS) + message(STATUS " PresentMonTests: ${PMON_BUILD_PRESENTMON_TESTS}") + message(STATUS " Unit tests: ${PMON_BUILD_UNIT_TESTS}") + message(STATUS " API2 tests: ${PMON_BUILD_API2_TESTS}") + message(STATUS " CTest full CSV: ${PMON_CTEST_FULL_CSV_TESTS}") +endif() +message(STATUS "") +message(STATUS "Configured enabled PresentMon targets.") diff --git a/IntelPresentMon/AppCef/Batch/cef-build-wrapper.ps1 b/IntelPresentMon/AppCef/Batch/cef-build-wrapper.ps1 index a138829b2..50eefc172 100644 --- a/IntelPresentMon/AppCef/Batch/cef-build-wrapper.ps1 +++ b/IntelPresentMon/AppCef/Batch/cef-build-wrapper.ps1 @@ -1,94 +1,123 @@ -<# +<# .SYNOPSIS - Out-of-source build of the CEF C++ wrapper. + Build the CEF C++ wrapper with CMake. .DESCRIPTION - 1. Takes a single, positional parameter: the path to your CEF redist root (the dir with CMakeLists.txt). - 2. If `/build` already exists, exits immediately. - 3. Finds VS via vswhere and locates vcvarsall.bat. - 4. Creates and cds into `/build`. - 5. Invokes CMake **directly**. - 6. Invokes msbuild for Release and then Debug (each in its own vcvarsall/msbuild invocation). + Configures an out-of-source Visual Studio build and builds both Release and + Debug. The defaults match the supported PresentMon CMake toolchain. .PARAMETER RedistPath - (Positional) Path to the root of your CEF redist directory. + Path to the root of the extracted CEF distribution. + +.PARAMETER Clean + Remove the existing wrapper build directory before configuring. + +.PARAMETER Generator + CMake generator used for the wrapper build. + +.PARAMETER Platform + CMake generator platform used for the wrapper build. + +.PARAMETER Toolset + CMake generator toolset used for the wrapper build. #> +[CmdletBinding()] param( - [Parameter(Mandatory=$true, Position=0)] + [Parameter(Mandatory = $true, Position = 0)] [string]$RedistPath, [Parameter()] - [switch]$Clean + [switch]$Clean, + + [Parameter()] + [string]$Generator = 'Visual Studio 17 2022', + + [Parameter()] + [string]$Platform = 'x64', + + [Parameter()] + [string]$Toolset = 'v143' ) -# Resolve & verify -try { $RedistPath = (Resolve-Path $RedistPath).ProviderPath } catch { - Write-Error "Cannot resolve path: $RedistPath"; exit 1 -} -if (-not (Test-Path $RedistPath -PathType Container)) { - Write-Error "Not a valid directory: $RedistPath"; exit 1 +$ErrorActionPreference = 'Stop' + +$RedistPath = (Resolve-Path $RedistPath).ProviderPath +if (-not (Test-Path -LiteralPath $RedistPath -PathType Container)) { + throw "Not a valid CEF distribution directory: $RedistPath" } -# Skip if already built, unless the caller requested a clean wrapper build. -$buildDir = Join-Path $RedistPath "build" -if (Test-Path $buildDir) { +$cmake = Get-Command cmake -ErrorAction Stop +$buildDir = Join-Path $RedistPath 'build' +if (Test-Path -LiteralPath $buildDir) { if ($Clean) { $resolvedBuildDir = (Resolve-Path $buildDir).ProviderPath - $resolvedRedistPath = (Resolve-Path $RedistPath).ProviderPath - if (-not $resolvedBuildDir.StartsWith($resolvedRedistPath, [System.StringComparison]::OrdinalIgnoreCase)) { - Write-Error "Refusing to clean unexpected build directory: $resolvedBuildDir" - exit 1 + $expectedBuildDir = [System.IO.Path]::GetFullPath((Join-Path $RedistPath 'build')) + if (-not $resolvedBuildDir.Equals($expectedBuildDir, [System.StringComparison]::OrdinalIgnoreCase)) { + throw "Refusing to clean unexpected build directory: $resolvedBuildDir" } Write-Host "Removing existing CEF wrapper build directory: $buildDir" - Remove-Item -LiteralPath $buildDir -Recurse -Force + Remove-Item -LiteralPath $resolvedBuildDir -Recurse -Force } else { Write-Host "Found existing build directory: $buildDir" - Write-Host "Skipping configuration and build." + Write-Host 'Skipping configuration and build.' exit 0 } } -# Locate VS / vcvarsall -$vswhere = "C:\Program Files (x86)\Microsoft Visual Studio\Installer\vswhere.exe" -if (-not (Test-Path $vswhere)) { Write-Error "vswhere.exe not found"; exit 1 } -$vsRoot = & $vswhere -latest -products * -prerelease -requires Microsoft.Component.MSBuild -property installationPath -if (-not $vsRoot) { Write-Error "No Visual Studio with MSBuild found"; exit 1 } -$vcvars = Join-Path $vsRoot "VC\Auxiliary\Build\vcvarsall.bat" -if (-not (Test-Path $vcvars)) { Write-Error "vcvarsall.bat not found"; exit 1 } -Write-Host "Using vcvarsall.bat at: $vcvars`n" - -# Make build dir & enter -Write-Host "Creating build directory at $buildDir" -New-Item -ItemType Directory -Path $buildDir | Out-Null -Push-Location $buildDir - -# Settings -$Arch = "x64" -$Platform = "x64" -$Generator = "Visual Studio 17" -$Solution = Join-Path $buildDir "cef.sln" - -# 1) Configure with CMake (no vcvars needed) -Write-Host "Running CMake configure..." -cmake -G "$Generator" -A $Platform -DUSE_SANDBOX=OFF "$RedistPath" -if ($LASTEXITCODE -ne 0) { - Write-Error "CMake configuration failed (exit code $LASTEXITCODE)"; Pop-Location; exit $LASTEXITCODE +# CEF enables /MP without a process limit. On high-core-count systems that can +# exhaust compiler resources and surface as C1001 or D8040. Use a conservative +# default while allowing callers to select a different explicit limit. +$compilerProcessLimit = 4 +if ($env:CL_MPCount) { + $configuredCompilerProcessLimit = 0 + if (-not [int]::TryParse($env:CL_MPCount, [ref]$configuredCompilerProcessLimit) -or + $configuredCompilerProcessLimit -lt 1 -or + $configuredCompilerProcessLimit -gt 65536) { + throw "CL_MPCount must be an integer between 1 and 65536: $env:CL_MPCount" + } + $compilerProcessLimit = $configuredCompilerProcessLimit +} else { + $env:CL_MPCount = [string]$compilerProcessLimit } -# 2) Build Release -Write-Host "`nBuilding Release..." -& cmd.exe /c "`"$vcvars`" $Arch && msbuild `"$Solution`" /m /p:Configuration=Release;Platform=$Platform" -if ($LASTEXITCODE -ne 0) { - Write-Error "Release build failed (exit code $LASTEXITCODE)"; Pop-Location; exit $LASTEXITCODE +$configureArguments = @( + '-S', $RedistPath, + '-B', $buildDir, + '-G', $Generator +) +if ($Platform) { + $configureArguments += @('-A', $Platform) +} +if ($Toolset) { + $configureArguments += @('-T', $Toolset) } +$configureArguments += '-DUSE_SANDBOX=OFF' -# 3) Build Debug -Write-Host "`nBuilding Debug..." -& cmd.exe /c "`"$vcvars`" $Arch && msbuild `"$Solution`" /m /p:Configuration=Debug;Platform=$Platform" +$toolchainDescription = $Generator +if ($Platform) { + $toolchainDescription += ", platform $Platform" +} +if ($Toolset) { + $toolchainDescription += ", toolset $Toolset" +} +Write-Host "Configuring the CEF wrapper with $toolchainDescription." +& $cmake.Source @configureArguments if ($LASTEXITCODE -ne 0) { - Write-Error "Debug build failed (exit code $LASTEXITCODE)"; Pop-Location; exit $LASTEXITCODE + throw "CEF wrapper CMake configuration failed with exit code $LASTEXITCODE." +} + +foreach ($configuration in @('Release', 'Debug')) { + Write-Host "Building the CEF wrapper for $configuration with up to $compilerProcessLimit compiler processes." + & $cmake.Source --build $buildDir --config $configuration + if ($LASTEXITCODE -ne 0) { + throw "CEF wrapper $configuration build failed with exit code $LASTEXITCODE." + } + + $wrapperLibrary = Join-Path $buildDir "libcef_dll_wrapper\$configuration\libcef_dll_wrapper.lib" + if (-not (Test-Path -LiteralPath $wrapperLibrary -PathType Leaf)) { + throw "CEF wrapper output was not produced: $wrapperLibrary" + } } -Pop-Location -Write-Host "`nAll done: CEF wrapper built in $buildDir" +Write-Host "CEF wrapper Debug and Release builds completed in $buildDir" diff --git a/IntelPresentMon/AppCef/Batch/cef-lock.psm1 b/IntelPresentMon/AppCef/Batch/cef-lock.psm1 index 0a3be082e..6c6ab2552 100644 --- a/IntelPresentMon/AppCef/Batch/cef-lock.psm1 +++ b/IntelPresentMon/AppCef/Batch/cef-lock.psm1 @@ -17,9 +17,75 @@ function Get-CefLockPath { } function Get-CefStagePath { + param( + [Parameter()] + [ValidateSet('Legacy', 'CMake')] + [string]$StageKind = 'Legacy' + ) + + if ($StageKind -eq 'CMake') { + return Join-Path (Get-RepoRoot) 'build\ThirdParty\cef' + } return Join-Path (Get-AppCefRoot) 'Cef' } +function Get-CefStageMutexName { + param( + [Parameter()] + [ValidateSet('Legacy', 'CMake')] + [string]$StageKind = 'Legacy' + ) + + $identity = ((Get-RepoRoot).ToLowerInvariant() + '|' + $StageKind.ToLowerInvariant()) + $bytes = [System.Text.Encoding]::UTF8.GetBytes($identity) + $sha256 = [System.Security.Cryptography.SHA256]::Create() + try { + $hash = $sha256.ComputeHash($bytes) + } finally { + $sha256.Dispose() + } + $suffix = -join ($hash[0..15] | ForEach-Object { $_.ToString('x2') }) + return "Local\PresentMonCefStage-$suffix" +} + +function Enter-CefStageLock { + param( + [Parameter()] + [ValidateSet('Legacy', 'CMake')] + [string]$StageKind = 'Legacy', + + [Parameter()] + [int]$TimeoutSeconds = 3600 + ) + + $name = Get-CefStageMutexName -StageKind $StageKind + $mutex = [System.Threading.Mutex]::new($false, $name) + try { + try { + $acquired = $mutex.WaitOne([System.TimeSpan]::FromSeconds($TimeoutSeconds)) + } catch [System.Threading.AbandonedMutexException] { + $acquired = $true + } + if (-not $acquired) { + throw "Timed out waiting for the $StageKind CEF stage lock." + } + return $mutex + } catch { + $mutex.Dispose() + throw + } +} + +function Exit-CefStageLock { + param([Parameter(Mandatory = $true)][System.Threading.Mutex]$Mutex) + + try { + $Mutex.ReleaseMutex() + } finally { + $Mutex.Dispose() + } +} + function Test-UriSource { param([Parameter(Mandatory = $true)][string]$Source) @@ -313,11 +379,16 @@ function Resolve-CefDistributionRoot { } function Assert-CefWrapperBuilt { - param([Parameter(Mandatory = $true)][string]$CefRoot) + param( + [Parameter(Mandatory = $true)][string]$CefRoot, + [Parameter()][string]$Generator = 'Visual Studio 17 2022', + [Parameter()][string]$Platform = 'x64', + [Parameter()][string]$Toolset = 'v143' + ) $buildScript = Join-Path $PSScriptRoot 'cef-build-wrapper.ps1' Write-Host "Building CEF wrapper from a clean build directory." - & $buildScript $CefRoot -Clean + & $buildScript $CefRoot -Clean -Generator $Generator -Platform $Platform -Toolset $Toolset if ($LASTEXITCODE -ne 0) { throw "CEF wrapper build failed with exit code $LASTEXITCODE." } @@ -339,48 +410,182 @@ function Copy-DirectoryContents { Copy-Item -Destination $Destination -Recurse -Force } -function Stage-CefDistribution { - param([Parameter(Mandatory = $true)][string]$CefRoot) - - Assert-CefWrapperBuilt -CefRoot $CefRoot +function Assert-CefStageBuildInputsAtPath { + param([Parameter(Mandatory = $true)][string]$StagePath) + + $requiredFiles = @( + 'Include\include\base\cef_callback.h', + 'Include\include\cef_app.h', + 'Include\include\cef_client.h', + 'Include\include\cef_parser.h', + 'Include\include\cef_scheme.h', + 'Include\include\cef_task.h', + 'Include\include\cef_v8.h', + 'Include\include\cef_version.h', + 'Include\include\internal\cef_types.h', + 'Include\include\wrapper\cef_closure_task.h', + 'Include\include\wrapper\cef_helpers.h', + 'Lib\Debug\libcef.lib', + 'Lib\Debug\libcef_dll_wrapper.lib', + 'Lib\Release\libcef.lib', + 'Lib\Release\libcef_dll_wrapper.lib' + ) - $stage = Get-CefStagePath - $appRoot = Get-AppCefRoot - $resolvedAppRoot = (Resolve-Path $appRoot).ProviderPath - $stageParent = (Resolve-Path (Split-Path -Parent $stage)).ProviderPath - if (-not $stageParent.Equals($resolvedAppRoot, [System.StringComparison]::OrdinalIgnoreCase)) { - throw "Refusing to clean unexpected CEF stage path: $stage" + $errors = New-Object System.Collections.Generic.List[string] + foreach ($relative in $requiredFiles) { + $path = Join-Path $StagePath $relative + if (-not (Test-Path -LiteralPath $path -PathType Leaf)) { + $errors.Add("Missing CEF build input: $relative") + continue + } + if ((Get-Item -LiteralPath $path).Length -eq 0) { + $errors.Add("Empty CEF build input: $relative") + } } - if (Test-Path $stage) { - Remove-Item -LiteralPath $stage -Recurse -Force + if ($errors.Count -ne 0) { + throw "CEF build inputs are incomplete at ${StagePath}:`n$($errors -join "`n")" } +} + +function Copy-CefDistributionToPath { + param( + [Parameter(Mandatory = $true)][string]$CefRoot, + [Parameter(Mandatory = $true)][string]$Destination + ) - New-Item -ItemType Directory -Force -Path (Join-Path $stage 'Bin') | Out-Null - New-Item -ItemType Directory -Force -Path (Join-Path $stage 'Lib\Debug') | Out-Null - New-Item -ItemType Directory -Force -Path (Join-Path $stage 'Lib\Release') | Out-Null - New-Item -ItemType Directory -Force -Path (Join-Path $stage 'Include') | Out-Null - New-Item -ItemType Directory -Force -Path (Join-Path $stage 'Resources') | Out-Null + New-Item -ItemType Directory -Force -Path (Join-Path $Destination 'Bin') | Out-Null + New-Item -ItemType Directory -Force -Path (Join-Path $Destination 'Lib\Debug') | Out-Null + New-Item -ItemType Directory -Force -Path (Join-Path $Destination 'Lib\Release') | Out-Null + New-Item -ItemType Directory -Force -Path (Join-Path $Destination 'Include') | Out-Null + New-Item -ItemType Directory -Force -Path (Join-Path $Destination 'Resources') | Out-Null - Copy-DirectoryContents -Source (Join-Path $CefRoot 'Release') -Destination (Join-Path $stage 'Bin') -ExcludeFileName @('cef_sandbox.lib', 'libcef.lib') - Copy-DirectoryContents -Source (Join-Path $CefRoot 'include') -Destination (Join-Path $stage 'Include\include') - Copy-DirectoryContents -Source (Join-Path $CefRoot 'Resources') -Destination (Join-Path $stage 'Resources') + Copy-DirectoryContents -Source (Join-Path $CefRoot 'Release') -Destination (Join-Path $Destination 'Bin') -ExcludeFileName @('cef_sandbox.lib', 'libcef.lib') + Copy-DirectoryContents -Source (Join-Path $CefRoot 'include') -Destination (Join-Path $Destination 'Include\include') + Copy-DirectoryContents -Source (Join-Path $CefRoot 'Resources') -Destination (Join-Path $Destination 'Resources') - Copy-Item -LiteralPath (Join-Path $CefRoot 'Release\libcef.lib') -Destination (Join-Path $stage 'Lib\Debug\libcef.lib') -Force - Copy-Item -LiteralPath (Join-Path $CefRoot 'Release\libcef.lib') -Destination (Join-Path $stage 'Lib\Release\libcef.lib') -Force - Copy-DirectoryContents -Source (Join-Path $CefRoot 'build\libcef_dll_wrapper\Debug') -Destination (Join-Path $stage 'Lib\Debug') - Copy-DirectoryContents -Source (Join-Path $CefRoot 'build\libcef_dll_wrapper\Release') -Destination (Join-Path $stage 'Lib\Release') + Copy-Item -LiteralPath (Join-Path $CefRoot 'Release\libcef.lib') -Destination (Join-Path $Destination 'Lib\Debug\libcef.lib') -Force + Copy-Item -LiteralPath (Join-Path $CefRoot 'Release\libcef.lib') -Destination (Join-Path $Destination 'Lib\Release\libcef.lib') -Force + Copy-DirectoryContents -Source (Join-Path $CefRoot 'build\libcef_dll_wrapper\Debug') -Destination (Join-Path $Destination 'Lib\Debug') + Copy-DirectoryContents -Source (Join-Path $CefRoot 'build\libcef_dll_wrapper\Release') -Destination (Join-Path $Destination 'Lib\Release') - $localesPath = Join-Path $stage 'Resources\locales' + $localesPath = Join-Path $Destination 'Resources\locales' if (Test-Path $localesPath -PathType Container) { Get-ChildItem -LiteralPath $localesPath -File | Where-Object { $_.Name -ine 'en-US.pak' } | Remove-Item -Force } - Write-Host "Staged CEF files at $stage" + Assert-CefStageBuildInputsAtPath -StagePath $Destination + Write-Host "Staged CEF files at $Destination" +} + +function Stage-CefDistribution { + param( + [Parameter(Mandatory = $true)][string]$CefRoot, + [Parameter()] + [ValidateSet('Legacy', 'CMake')] + [string]$StageKind = 'Legacy', + [Parameter()][string]$Generator = 'Visual Studio 17 2022', + [Parameter()][string]$Platform = 'x64', + [Parameter()][string]$Toolset = 'v143' + ) + + $stage = Get-CefStagePath -StageKind $StageKind + Assert-CefWrapperBuilt -CefRoot $CefRoot -Generator $Generator -Platform $Platform -Toolset $Toolset + if (Test-Path -LiteralPath $stage) { + Remove-Item -LiteralPath $stage -Recurse -Force + } + New-Item -ItemType Directory -Force -Path $stage | Out-Null + Copy-CefDistributionToPath -CefRoot $CefRoot -Destination $stage +} + +function New-CefStageWorkPath { + param( + [Parameter()] + [ValidateSet('Legacy', 'CMake')] + [string]$StageKind = 'Legacy' + ) + + $destination = Get-CefStagePath -StageKind $StageKind + New-Item -ItemType Directory -Force -Path (Split-Path -Parent $destination) | Out-Null + $suffix = [System.IO.Path]::GetRandomFileName() -replace '\.', '' + $work = "$destination.staging-$suffix" + New-Item -ItemType Directory -Path $work | Out-Null + return $work +} + +function Publish-CefStage { + param( + [Parameter(Mandatory = $true)][string]$Source, + [Parameter()] + [ValidateSet('Legacy', 'CMake')] + [string]$StageKind = 'Legacy' + ) + + $destination = Get-CefStagePath -StageKind $StageKind + $retired = $null + if (Test-Path -LiteralPath $destination) { + $suffix = [System.IO.Path]::GetRandomFileName() -replace '\.', '' + $retired = "$destination.retired-$suffix" + Move-Item -LiteralPath $destination -Destination $retired -Force + } + + try { + Move-Item -LiteralPath $Source -Destination $destination -Force + } catch { + if ($retired) { + Move-Item -LiteralPath $retired -Destination $destination -Force + $retired = $null + } + throw + } + + if ($retired) { + try { + Remove-Item -LiteralPath $retired -Recurse -Force + } catch { + Write-Warning "Failed to remove the previous CEF stage: $retired" + } + } +} + +function Install-CefStage { + param( + [Parameter(Mandatory = $true)][string]$CefRoot, + [Parameter()] + [ValidateSet('Legacy', 'CMake')] + [string]$StageKind = 'Legacy', + [Parameter()][string]$Generator = 'Visual Studio 17 2022', + [Parameter()][string]$Platform = 'x64', + [Parameter()][string]$Toolset = 'v143' + ) + + $stage = Get-CefStagePath -StageKind $StageKind + $mutex = Enter-CefStageLock -StageKind $StageKind + $work = $null + try { + Assert-CefWrapperBuilt -CefRoot $CefRoot -Generator $Generator -Platform $Platform -Toolset $Toolset + $work = New-CefStageWorkPath -StageKind $StageKind + Copy-CefDistributionToPath -CefRoot $CefRoot -Destination $work + Assert-CefStagePathMatchesLock -StagePath $work + Publish-CefStage -Source $work -StageKind $StageKind + } finally { + if ($work -and (Test-Path -LiteralPath $work)) { + Remove-Item -LiteralPath $work -Recurse -Force + } + Exit-CefStageLock -Mutex $mutex + } + + Write-Host "Published CEF stage at $stage" } function Get-CefPayloadEntriesFromStage { - $stage = Get-CefStagePath + param( + [Parameter()] + [ValidateSet('Legacy', 'CMake')] + [string]$StageKind = 'Legacy' + ) + + $stage = Get-CefStagePath -StageKind $StageKind $entries = @() $groups = @( @{ Name = 'Bin'; Root = (Join-Path $stage 'Bin'); OutputPrefix = '' }, @@ -429,28 +634,25 @@ function New-CefLockObject { size = $sourceInfo.Length sha256 = Get-FileSha256 -Path $sourceInfo.FullName } - } else { + } elseif ($sourceKind -eq 'archive') { $sourceInput = Get-ObjectPropertyValue -Object $Source -Name 'input' $sourceResolved = (Resolve-Path $sourceInput).ProviderPath $sourceInfo = Get-Item -LiteralPath $sourceResolved - $sourceType = if ($sourceInfo.PSIsContainer) { - $stagePath = (Resolve-Path (Get-CefStagePath)).ProviderPath - if ($sourceResolved.Equals($stagePath, [System.StringComparison]::OrdinalIgnoreCase)) { 'staged' } else { 'directory' } - } else { - 'archive' + if ($sourceInfo.PSIsContainer) { + throw 'CEF lock generation requires a URI or archive so the lock records an archive SHA-256.' } $source = [ordered]@{ - type = $sourceType + type = 'archive' fileName = $sourceInfo.Name + size = $sourceInfo.Length + sha256 = Get-FileSha256 -Path $sourceResolved } $repoRelativePath = ConvertTo-RepoRelativePath -Path $sourceResolved if ($repoRelativePath) { $source.path = $repoRelativePath } - if (-not $sourceInfo.PSIsContainer) { - $source.size = $sourceInfo.Length - $source.sha256 = Get-FileSha256 -Path $sourceResolved - } + } else { + throw "CEF lock generation does not support source type: $sourceKind" } return [ordered]@{ @@ -458,7 +660,7 @@ function New-CefLockObject { generatedBy = 'IntelPresentMon/AppCef/Batch/upgrade-cef.ps1' source = $source cef = Read-CefVersionMetadata -CefRoot $CefRoot - payload = Get-CefPayloadEntriesFromStage + payload = Get-CefPayloadEntriesFromStage -StageKind Legacy } } @@ -519,19 +721,41 @@ function Compare-CefPayload { return $errors } -function Assert-CefStageMatchesLock { +function Assert-CefStagePathMatchesLock { + param([Parameter(Mandatory = $true)][string]$StagePath) + + if (-not (Test-Path -LiteralPath $StagePath -PathType Container)) { + throw "CEF stage directory not found: $StagePath" + } + $lock = Read-CefLock - $metadata = Read-CefVersionMetadata -CefRoot (Get-CefStagePath) + $metadata = Read-CefVersionMetadata -CefRoot $StagePath foreach ($name in $lock.cef.PSObject.Properties.Name) { if ([string]$metadata[$name] -ne [string]$lock.cef.$name) { throw "Staged CEF metadata does not match lock for $name. Expected $($lock.cef.$name), found $($metadata[$name])." } } - $errors = @(Compare-CefPayload -Expected $lock.payload -Root (Get-CefStagePath) -Mode Stage) + $errors = @(Compare-CefPayload -Expected $lock.payload -Root $StagePath -Mode Stage) if ($errors.Count -ne 0) { - throw "Staged CEF payload does not match $(Get-CefLockPath):`n$($errors -join "`n")" + throw "Staged CEF payload at $StagePath does not match $(Get-CefLockPath):`n$($errors -join "`n")" + } + Assert-CefStageBuildInputsAtPath -StagePath $StagePath + Write-Host "CEF staged payload at $StagePath matches lock and its build inputs are present." +} + +function Assert-CefStageMatchesLock { + param( + [Parameter()] + [ValidateSet('Legacy', 'CMake')] + [string]$StageKind = 'Legacy' + ) + + $mutex = Enter-CefStageLock -StageKind $StageKind + try { + Assert-CefStagePathMatchesLock -StagePath (Get-CefStagePath -StageKind $StageKind) + } finally { + Exit-CefStageLock -Mutex $mutex } - Write-Host "CEF staged payload matches lock." } function Assert-CefOutputMatchesLock { @@ -662,6 +886,7 @@ function Assert-CefInstallerInputsMatchLock { throw "Installer CEF fragment is stale: $actualPath. Run IntelPresentMon\AppCef\Batch\upgrade-cef.ps1 to refresh it." } } + } finally { if (Test-Path $temp) { Remove-Item -LiteralPath $temp -Recurse -Force @@ -669,3 +894,27 @@ function Assert-CefInstallerInputsMatchLock { } Write-Host "CEF installer fragments match lock." } + +Export-ModuleMember -Function @( + 'Get-RepoRoot', + 'Get-CefStagePath', + 'Enter-CefStageLock', + 'Exit-CefStageLock', + 'Test-UriSource', + 'Test-CefKeepWorkDirectories', + 'Get-CefTempDirectories', + 'Clear-CefTempDirectories', + 'Read-CefLock', + 'Get-FileSha256', + 'Get-ObjectPropertyValue', + 'Resolve-CefSource', + 'Resolve-CefDistributionRoot', + 'Stage-CefDistribution', + 'Install-CefStage', + 'New-CefLockObject', + 'Write-CefLock', + 'Assert-CefStageMatchesLock', + 'Assert-CefOutputMatchesLock', + 'Update-CefInstallerFragments', + 'Assert-CefInstallerInputsMatchLock' +) diff --git a/IntelPresentMon/AppCef/Batch/pull-cef.ps1 b/IntelPresentMon/AppCef/Batch/pull-cef.ps1 index 02af93b08..cd9093e77 100644 --- a/IntelPresentMon/AppCef/Batch/pull-cef.ps1 +++ b/IntelPresentMon/AppCef/Batch/pull-cef.ps1 @@ -7,17 +7,46 @@ or accepts a CEF distribution archive or extracted CEF distribution directory as a fallback. Stages the AppCef dependency files and verifies the staged runtime payload against AppCef\cef-lock.json. + +.PARAMETER StageKind + Legacy restores IntelPresentMon\AppCef\Cef for MSBuild. CMake restores + build\ThirdParty\cef and requires the source archive to match the lock. + +.PARAMETER Generator + CMake generator used to build the CEF wrapper. + +.PARAMETER Platform + Optional CMake generator platform used to build the CEF wrapper. + +.PARAMETER Toolset + Optional CMake generator toolset used to build the CEF wrapper. #> [CmdletBinding()] param( [Parameter(Position = 0)] - [string]$SourcePath + [string]$SourcePath, + + [Parameter()] + [ValidateSet('Legacy', 'CMake')] + [string]$StageKind = 'Legacy', + + [Parameter()] + [string]$Generator = 'Visual Studio 17 2022', + + [Parameter()] + [string]$Platform = 'x64', + + [Parameter()] + [string]$Toolset = 'v143' ) $ErrorActionPreference = 'Stop' Import-Module (Join-Path $PSScriptRoot 'cef-lock.psm1') -Force -DisableNameChecking +$stagePath = Get-CefStagePath -StageKind $StageKind +Write-Host "Restoring the $StageKind CEF stage at $stagePath" + $lock = Read-CefLock if (-not $SourcePath) { $sourceUri = Get-ObjectPropertyValue -Object $lock.source -Name 'uri' @@ -39,19 +68,39 @@ if (-not $SourcePath) { $completed = $false try { + $lockedUri = Get-ObjectPropertyValue -Object $lock.source -Name 'uri' + $lockedSha256 = Get-ObjectPropertyValue -Object $lock.source -Name 'sha256' + if (($StageKind -eq 'CMake') -and (-not $lockedSha256)) { + throw 'The CEF lock must contain a source SHA-256 before restoring the CMake stage.' + } + if (($StageKind -eq 'CMake') -and (Test-UriSource -Source $SourcePath)) { + if (-not $lockedUri) { + throw 'The CEF lock does not define a source URI for the CMake stage.' + } + $requestedUri = ([System.Uri]$SourcePath).AbsoluteUri + $expectedUri = ([System.Uri]$lockedUri).AbsoluteUri + if (-not $requestedUri.Equals($expectedUri, [System.StringComparison]::Ordinal)) { + throw "The CMake CEF stage accepts only the URI recorded in the lock: $lockedUri" + } + } + $source = Resolve-CefSource -Source $SourcePath $sourceArchivePath = Get-ObjectPropertyValue -Object $source -Name 'archivePath' - $lockedSha256 = Get-ObjectPropertyValue -Object $lock.source -Name 'sha256' - if ((Test-Path $sourceArchivePath -PathType Leaf) -and $lockedSha256) { + $sourceKind = Get-ObjectPropertyValue -Object $source -Name 'type' + + if (($StageKind -eq 'CMake') -and ($sourceKind -eq 'directory')) { + throw 'The CMake CEF stage requires the locked URI or a local copy of the locked archive. Extracted directories are supported only for the Legacy stage.' + } + + if (Test-Path $sourceArchivePath -PathType Leaf) { $actualHash = Get-FileSha256 -Path $sourceArchivePath - if ($actualHash -ne $lockedSha256) { + if ($lockedSha256 -and ($actualHash -ne $lockedSha256)) { throw "CEF archive hash does not match lock. Expected $lockedSha256, found $actualHash." } } $cefRoot = Resolve-CefDistributionRoot -Path $sourceArchivePath - Stage-CefDistribution -CefRoot $cefRoot - Assert-CefStageMatchesLock + Install-CefStage -CefRoot $cefRoot -StageKind $StageKind -Generator $Generator -Platform $Platform -Toolset $Toolset $completed = $true } finally { if ($completed) { diff --git a/IntelPresentMon/AppCef/Batch/upgrade-cef.ps1 b/IntelPresentMon/AppCef/Batch/upgrade-cef.ps1 index 427377438..d67a4fbf2 100644 --- a/IntelPresentMon/AppCef/Batch/upgrade-cef.ps1 +++ b/IntelPresentMon/AppCef/Batch/upgrade-cef.ps1 @@ -4,10 +4,10 @@ .DESCRIPTION Accepts a new CEF distribution URI for the normal maintainer workflow, or - a local archive/extracted directory as a fallback. Stages the AppCef - dependency files, updates AppCef\cef-lock.json, and regenerates the CEF - WiX fragments used by the installer. This command can create the lock file - when it does not already exist. + a local archive as a fallback. Stages the AppCef dependency files, updates + AppCef\cef-lock.json, and regenerates the CEF WiX fragments used by the + installer. This command can create the lock file when it does not already + exist. #> [CmdletBinding()] @@ -20,25 +20,33 @@ $ErrorActionPreference = 'Stop' Import-Module (Join-Path $PSScriptRoot 'cef-lock.psm1') -Force -DisableNameChecking $completed = $false +$stageMutex = Enter-CefStageLock -StageKind Legacy try { $resolvedSource = Resolve-CefSource -Source $Source + if ((Get-ObjectPropertyValue -Object $resolvedSource -Name 'type') -eq 'directory') { + throw 'CEF lock upgrades require a URI or archive so the lock records an archive SHA-256. Extracted directories are supported only by the Legacy pull workflow.' + } $cefRoot = Resolve-CefDistributionRoot -Path (Get-ObjectPropertyValue -Object $resolvedSource -Name 'archivePath') - Stage-CefDistribution -CefRoot $cefRoot + Stage-CefDistribution -CefRoot $cefRoot -StageKind Legacy $lock = New-CefLockObject -CefRoot $cefRoot -Source $resolvedSource Write-CefLock -Lock $lock Update-CefInstallerFragments - Assert-CefStageMatchesLock + Assert-CefStageMatchesLock -StageKind Legacy Assert-CefInstallerInputsMatchLock $completed = $true } finally { - if ($completed) { - if (Test-CefKeepWorkDirectories) { - Write-Host "Keeping CEF work directories because PRESENTMON_CEF_KEEP_WORK is set." - } else { - Clear-CefTempDirectories + try { + if ($completed) { + if (Test-CefKeepWorkDirectories) { + Write-Host "Keeping CEF work directories because PRESENTMON_CEF_KEEP_WORK is set." + } else { + Clear-CefTempDirectories + } + } elseif ((Get-CefTempDirectories).Count -ne 0) { + Write-Host "Leaving CEF work directories after failed upgrade:" + Get-CefTempDirectories | ForEach-Object { Write-Host " $_" } } - } elseif ((Get-CefTempDirectories).Count -ne 0) { - Write-Host "Leaving CEF work directories after failed upgrade:" - Get-CefTempDirectories | ForEach-Object { Write-Host " $_" } + } finally { + Exit-CefStageLock -Mutex $stageMutex } } diff --git a/IntelPresentMon/AppCef/Batch/validate-cef.ps1 b/IntelPresentMon/AppCef/Batch/validate-cef.ps1 index 16b8fe756..97aad9d40 100644 --- a/IntelPresentMon/AppCef/Batch/validate-cef.ps1 +++ b/IntelPresentMon/AppCef/Batch/validate-cef.ps1 @@ -1,23 +1,47 @@ <# .SYNOPSIS - Validate AppCef CEF files against the repository lock. + Validate CEF files against the repository lock. + +.PARAMETER Mode + Stage validates a restored CEF stage. Output validates a build output + directory that received the locked CEF runtime payload. Installer also + validates the generated installer fragments. + +.PARAMETER OutputRoot + Directory holding the copied CEF runtime payload. Required for Output and + Installer validation. + +.PARAMETER StageKind + Selects the fixed Legacy or CMake stage. Legacy is the default. #> [CmdletBinding()] param( [Parameter(Mandatory = $true)] - [ValidateSet('Stage', 'Installer')] + [ValidateSet('Stage', 'Output', 'Installer')] [string]$Mode, [Parameter()] - [string]$OutputRoot + [string]$OutputRoot, + + [Parameter()] + [ValidateSet('Legacy', 'CMake')] + [string]$StageKind = 'Legacy' ) $ErrorActionPreference = 'Stop' Import-Module (Join-Path $PSScriptRoot 'cef-lock.psm1') -Force -DisableNameChecking if ($Mode -eq 'Stage') { - Assert-CefStageMatchesLock + Assert-CefStageMatchesLock -StageKind $StageKind + exit 0 +} + +if ($Mode -eq 'Output') { + if (-not $OutputRoot) { + throw 'OutputRoot is required for output CEF validation.' + } + Assert-CefOutputMatchesLock -OutputRoot $OutputRoot exit 0 } diff --git a/IntelPresentMon/AppCef/CMakeLists.txt b/IntelPresentMon/AppCef/CMakeLists.txt new file mode 100644 index 000000000..b2d4e3282 --- /dev/null +++ b/IntelPresentMon/AppCef/CMakeLists.txt @@ -0,0 +1,82 @@ +add_executable( + PresentMonUI + WIN32 + source/DataBindAccessor.cpp + source/NanoCefBrowserClient.cpp + source/NanoCefProcessHandler.cpp + source/SchemeFileHandler.cpp + source/SchemeHandlerFactory.cpp + source/util/ActionClientServer.cpp + source/util/AsyncEndpoint.cpp + source/util/AsyncEndpointCollection.cpp + source/util/AsyncEndpointManager.cpp + source/util/cact/CefActionRegistration.cpp + source/util/CefValues.cpp + source/util/FontEnumerator.cpp + source/util/IpcInvocationManager.cpp + source/util/KernelActionRegistration.cpp + source/util/LogSetup.cpp + source/util/PathSanitaryCheck.cpp + source/util/SignalManager.cpp + source/util/ThreadCompass.cpp + source/winmain.cpp + AppCef.rc + compatibility.manifest +) + +target_link_libraries( + PresentMonUI + PRIVATE + pmon::build_options + pmon::warnings_default + pmon::version_resource + pmon::release_link_policy + pmon::common_utilities + pmon::core + pmon::interprocess + pmon::versioning + dwrite + dwmapi + shcore +) +target_compile_definitions( + PresentMonUI + PRIVATE + _SILENCE_CXX17_RESULT_OF_DEPRECATION_WARNING + PM_BUILD_WINSDK_VERSION_STR="10.0" + "PM_BUILD_CRT_RUNTIME_STR=\"$,MultiThreadedDebug,MultiThreaded>\"" +) +target_include_directories( + PresentMonUI + PRIVATE + # RC.exe resolves the ICON statement in AppCef.rc against its own + # working directory, which for a CMake-generated Visual Studio + # project is not this source directory. Scoped to RC only; C++ + # includes resolve through linked targets and quoted sibling + # includes, never through this directory. + $<$:${CMAKE_CURRENT_SOURCE_DIR}> +) +target_compile_options( + PresentMonUI + PRIVATE + /EHa + /fp:fast + $<$:/guard:cf> +) +target_link_options( + PresentMonUI + PRIVATE + $<$:/guard:cf> +) +set_target_properties( + PresentMonUI + PROPERTIES + # CefNano is the other MultiByte target alongside SampleClient. + PMON_MULTIBYTE ON + FOLDER "UI and Capture" +) + +pmon_target_uses_cef(PresentMonUI) +pmon_target_uses_shaders(PresentMonUI) +pmon_target_uses_ui_payload(PresentMonUI) + diff --git a/IntelPresentMon/AppCef/ceflock.md b/IntelPresentMon/AppCef/ceflock.md index 7c6b4a08c..36ff26d9a 100644 --- a/IntelPresentMon/AppCef/ceflock.md +++ b/IntelPresentMon/AppCef/ceflock.md @@ -2,9 +2,10 @@ `cef-lock.json` records the Chromium Embedded Framework (CEF) distribution used by AppCef. The lock keeps the staged runtime payload reproducible and gives maintainers a clear path for intentional CEF upgrades. -## Restore Locked CEF +## Legacy Visual Studio Restore -From the repository root, run: +The classic Visual Studio build restores its legacy CEF stage with this +repository PowerShell helper: ```bat > IntelPresentMon\AppCef\Batch\pull-cef.ps1 @@ -12,7 +13,82 @@ From the repository root, run: The script downloads the URI recorded in `IntelPresentMon\AppCef\cef-lock.json`, extracts the archive, rebuilds the CEF C++ wrapper from a clean build directory, stages the AppCef CEF files, and verifies that the staged runtime payload matches the lock file. -The normal restore path never modifies `cef-lock.json`. +The restore never modifies `cef-lock.json`. The current CMake build does not +invoke this helper; its normal Python-backed path is described below. + +## Legacy PowerShell Stage Selection + +`pull-cef.ps1` and `validate-cef.ps1` accept `-StageKind Legacy|CMake`. +Only these two published destinations exist: + +```text +Legacy IntelPresentMon\AppCef\Cef +CMake build\ThirdParty\cef +``` + +`Legacy` is the default, so the command in the previous section and existing +MSBuild callers remain unchanged. For compatibility maintenance, the helper can +also operate on the CMake stage directly: + +```bat +> IntelPresentMon\AppCef\Batch\pull-cef.ps1 -StageKind CMake +> IntelPresentMon\AppCef\Batch\validate-cef.ps1 -Mode Stage -StageKind CMake +``` + +The two-stage selection exists only during migration. When MSBuild is retired, +the `Legacy` stage kind, its callers, and `IntelPresentMon\AppCef\Cef` must be +removed; `build\ThirdParty\cef` then becomes the only published stage. + +There is no arbitrary destination parameter. Temporary extraction, +wrapper-build, staging, and retired directories are internal to restore and do +not create another supported published stage. Restore and stage validation are +serialized so validation does not inspect a stage while it is being replaced. + +## CMake + +The CMake build always uses the shared `build/ThirdParty/cef/` stage and never +writes to the Visual Studio stage. Normal configuration validates the stage and +restores it automatically when it is absent or stale: + +```bat +> cmake -B build -S . -G "Visual Studio 17 2022" +> cmake --build build --config Debug +``` + +For Visual Studio 2026 (Experimental), use its distinct binary tree: + +```bat +> cmake -B build-vs2026 -S . -G "Visual Studio 18 2026" +> cmake --build build-vs2026 --config Debug +``` + +`PMON_CEF_SOURCE` optionally supplies the URI recorded in the lock or a local +archive. An alternate URI is rejected. A local archive must match the SHA-256 +in `cef-lock.json`; an extracted directory is not accepted for the CMake stage. +The locked URI is used when the variable is empty. For a network-independent +configuration, supply the matching local archive: + +```bat +> cmake -B build -S . -G "Visual Studio 17 2022" -DPMON_CEF_SOURCE:FILEPATH=C:\path\to\cef_archive.tar.bz2 +> cmake --build build --config Debug +``` + +`pmon_stage_cef_runtime` incrementally copies the locked runtime files into the +build output. Configure-time validation and `pmon_restore_cef` verify the shared +stage before it is used or published. +`pmon_verify_cef` is the explicit offline CI check for the published CMake stage +and its staged runtime payload. + +The explicit restore and verification targets remain available for maintenance +and CI. Runtime staging becomes part of the normal build when a product target +uses CEF. Archives, wrapper-build inputs, and the published CEF SDK are shared +by all CMake build directories; only runnable output files are copied into each +configuration's output directory. + +`pull-cef.ps1` also accepts `-Generator`, `-Platform`, and `-Toolset` for the +wrapper build. Their defaults are `Visual Studio 17 2022`, `x64`, and `v143`. +Passing an empty platform or toolset omits the corresponding CMake generator +option. ## Upgrade Locked CEF @@ -52,7 +128,12 @@ As a fallback, both scripts can accept a local archive path: ```bat > IntelPresentMon\AppCef\Batch\pull-cef.ps1 path\to\cef_archive.tar.bz2 +> IntelPresentMon\AppCef\Batch\pull-cef.ps1 path\to\cef_archive.tar.bz2 -StageKind CMake > IntelPresentMon\AppCef\Batch\upgrade-cef.ps1 path\to\cef_archive.tar.bz2 ``` +Only the legacy pull workflow retains its extracted-directory fallback. Lock +upgrades require a URI or archive so every new lock records an archive SHA-256. +The CMake-stage pull accepts only the locked URI or a matching archive. + If CEF was upgraded from a local archive instead of a URI, `pull-cef.ps1` must also be given a matching local archive path. diff --git a/IntelPresentMon/AppCef/source/util/ActionClientServer.cpp b/IntelPresentMon/AppCef/source/util/ActionClientServer.cpp index f42224f1f..f3f9d7c5e 100644 --- a/IntelPresentMon/AppCef/source/util/ActionClientServer.cpp +++ b/IntelPresentMon/AppCef/source/util/ActionClientServer.cpp @@ -1,5 +1,5 @@ #include "ActionClientServer.h" -#include "../KernelProcess/kact/OpenSession.h" +#include namespace p2c::client::util { diff --git a/IntelPresentMon/AppCef/source/util/IpcInvocationManager.h b/IntelPresentMon/AppCef/source/util/IpcInvocationManager.h index 7ea55275f..24744d5fe 100644 --- a/IntelPresentMon/AppCef/source/util/IpcInvocationManager.h +++ b/IntelPresentMon/AppCef/source/util/IpcInvocationManager.h @@ -16,7 +16,7 @@ #include "AsyncEndpointCollection.h" #include "ActionClientServer.h" #include "V8Transfer.h" -#include "../KernelProcess/kact/PushSpecification.h" +#include namespace p2c::client::util diff --git a/IntelPresentMon/AppCef/source/util/ThreadCompass.cpp b/IntelPresentMon/AppCef/source/util/ThreadCompass.cpp index 8d6eb02c8..bb8704bed 100644 --- a/IntelPresentMon/AppCef/source/util/ThreadCompass.cpp +++ b/IntelPresentMon/AppCef/source/util/ThreadCompass.cpp @@ -1,7 +1,7 @@ // Copyright (C) 2022 Intel Corporation // SPDX-License-Identifier: MIT #include "ThreadCompass.h" -#include "../CommonUtilities/win/WinAPI.h" +#include #include namespace p2c::client::util diff --git a/IntelPresentMon/AppCef/source/util/cact/CefExecutionContext.h b/IntelPresentMon/AppCef/source/util/cact/CefExecutionContext.h index bfce88f12..0d09250e3 100644 --- a/IntelPresentMon/AppCef/source/util/cact/CefExecutionContext.h +++ b/IntelPresentMon/AppCef/source/util/cact/CefExecutionContext.h @@ -1,7 +1,7 @@ #pragma once #include #include -#include "../CommonUtilities/pipe/ManualAsyncEvent.h" +#include #include #include #include diff --git a/IntelPresentMon/AppCef/source/winmain.cpp b/IntelPresentMon/AppCef/source/winmain.cpp index d97924d71..77d8b9f62 100644 --- a/IntelPresentMon/AppCef/source/winmain.cpp +++ b/IntelPresentMon/AppCef/source/winmain.cpp @@ -13,7 +13,6 @@ #include #include #include -#include #include #include #include diff --git a/IntelPresentMon/AppCef/web-dist-lock.json b/IntelPresentMon/AppCef/web-dist-lock.json new file mode 100644 index 000000000..36aaf351c --- /dev/null +++ b/IntelPresentMon/AppCef/web-dist-lock.json @@ -0,0 +1,16 @@ +{ + "outputs": [ + "index.html", + "css/index.css", + "js/app.js", + "fonts/materialdesignicons-webfont.woff2", + "fonts/roboto-cyrillic-400-normal.woff2", + "fonts/roboto-cyrillic-ext-400-normal.woff2", + "fonts/roboto-greek-400-normal.woff2", + "fonts/roboto-latin-400-normal.woff2", + "fonts/roboto-latin-ext-400-normal.woff2", + "fonts/roboto-math-400-normal.woff2", + "fonts/roboto-symbols-400-normal.woff2", + "fonts/roboto-vietnamese-400-normal.woff2" + ] +} diff --git a/IntelPresentMon/AppCef/webui.md b/IntelPresentMon/AppCef/webui.md index b607b1616..55fc2afe7 100644 --- a/IntelPresentMon/AppCef/webui.md +++ b/IntelPresentMon/AppCef/webui.md @@ -2,15 +2,34 @@ The AppCef web UI lives under `IntelPresentMon\AppCef\ipm-ui-vue`. It is a Vue and Vite application whose production assets are built before the capture application is packaged or run from staged files. -## Bootstrap Path +## Normal CMake Path -From the repository root, run: +The examples use the Windows `py` launcher. On a machine with only +`python.exe`, use `python` wherever `py -3` appears. After installing the Python +requirements, configure and build normally: ```powershell -> .\bootstrap.ps1 +> py -3 -m pip install -r requirements.txt +> cmake -B build -S . -G "Visual Studio 17 2022" +> cmake --build build --config Debug ``` -The bootstrap script enters `IntelPresentMon\AppCef`, runs `Batch\build-web.bat`, installs npm packages with `npm ci`, and builds the production web assets with `npm run build`. +For Visual Studio 2026 (Experimental), use its distinct binary tree: + +```powershell +> cmake -B build-vs2026 -S . -G "Visual Studio 18 2026" +> cmake --build build-vs2026 --config Debug +``` + +CMake invokes `Scripts\web.py` during configuration. It runs `npm ci` only when +the package lock fingerprint or installed dependency check is stale, then builds the +production web assets. Subsequent configurations reuse the current npm install. + +To prepare only the web UI, run: + +```powershell +> py -3 Scripts\web.py ensure +``` ## Manual Production Build @@ -23,7 +42,8 @@ To rebuild only the web UI, run: > popd ``` -Run this on a fresh clone, after package changes, or when the production web assets need to be refreshed. +This manual workflow remains useful for focused web development. The normal +CMake path performs the equivalent preparation automatically. ## Development Server diff --git a/IntelPresentMon/CommonUtilities/CMakeLists.txt b/IntelPresentMon/CommonUtilities/CMakeLists.txt new file mode 100644 index 000000000..9653671af --- /dev/null +++ b/IntelPresentMon/CommonUtilities/CMakeLists.txt @@ -0,0 +1,122 @@ +set(common_utilities_sources + cli/CliFramework.cpp + Env.cpp + Exception.cpp + file/FileUtils.cpp + file/PathUtils.cpp + file/SecureSubdirectory.cpp + file/TempFile.cpp + Hash.cpp + IntervalWaiter.cpp + log/BasicFileDriver.cpp + log/Channel.cpp + log/ChannelFlusher.cpp + log/CopyDriver.cpp + log/DiagnosticDriver.cpp + log/EntryBuilder.cpp + log/EntryMarshallInjector.cpp + log/ErrorCode.cpp + log/ErrorCodeResolvePolicy.cpp + log/ErrorCodeResolver.cpp + log/GlobalPolicy.cpp + log/HrLogger.cpp + log/IdentificationTable.cpp + log/Level.cpp + log/LinePolicy.cpp + log/LineTable.cpp + log/Log.cpp + log/MarshallDriver.cpp + log/MsvcDebugDriver.cpp + log/NamedPipeMarshallReceiver.cpp + log/NamedPipeMarshallSender.cpp + log/PanicLogger.cpp + log/SimpleFileStrategy.cpp + log/StackTrace.cpp + log/StdioDriver.cpp + log/Subsystem.cpp + log/TextFormatter.cpp + log/TimePoint.cpp + log/Verbose.cpp + Memory.cpp + mt/Thread.cpp + pipe/CoroMutex.cpp + pipe/Pipe.cpp + PrecisionWaiter.cpp + Qpc.cpp + ref/GeneratedReflection.cpp + str/String.cpp + test/CrtDiagnosticsRedirect.cpp + win/com/ComManager.cpp + win/com/ProcessSpawnSink.cpp + win/com/WbemConnection.cpp + win/com/WbemListener.cpp + win/com/WbemSink.cpp + win/Event.cpp + win/Handle.cpp + win/HrError.cpp + win/HrErrorCodeProvider.cpp + win/MessageBox.cpp + win/Privileges.cpp + win/ProcessMapBuilder.cpp + win/Security.cpp + win/Utilities.cpp +) + +find_package(boost_asio CONFIG REQUIRED) +find_package(cereal CONFIG REQUIRED) +find_package(CLI11 CONFIG REQUIRED) +find_package(concurrentqueue CONFIG REQUIRED) +find_path(WINREG_INCLUDE_DIRS "winreg/WinReg.hpp" REQUIRED) + +add_library(pmon_common_utilities STATIC ${common_utilities_sources}) +add_library(pmon::common_utilities ALIAS pmon_common_utilities) +target_compile_definitions( + pmon_common_utilities + PUBLIC + CEREAL_THREAD_SAFE=1 + PRIVATE + _LIB +) +target_include_directories( + pmon_common_utilities + PUBLIC + "${PROJECT_SOURCE_DIR}" + "${WINREG_INCLUDE_DIRS}" +) +target_link_libraries( + pmon_common_utilities + PUBLIC + Boost::asio + cereal::cereal + CLI11::CLI11 + concurrentqueue::concurrentqueue + PRIVATE + pmon::build_options + pmon::warnings_default + advapi32 + dbghelp + ole32 + rpcrt4 + shell32 + user32 + wbemuuid +) +target_compile_options(pmon_common_utilities PRIVATE /EHa) +target_compile_options( + pmon_common_utilities + PRIVATE + $<$,$>:/guard:cf> +) +target_link_options( + pmon_common_utilities + INTERFACE + $<$,$>:/guard:cf> +) +set_target_properties( + pmon_common_utilities + PROPERTIES + OUTPUT_NAME IPMCommonUtilities + ARCHIVE_OUTPUT_DIRECTORY + "${PMON_OUTPUT_ROOT}/lib/CommonUtilities-${PMON_ARCHITECTURE}-$" + FOLDER "Foundation" +) diff --git a/IntelPresentMon/CommonUtilities/Metrics.cmake b/IntelPresentMon/CommonUtilities/Metrics.cmake new file mode 100644 index 000000000..5cef94250 --- /dev/null +++ b/IntelPresentMon/CommonUtilities/Metrics.cmake @@ -0,0 +1,49 @@ +set(metrics_sources + "${CMAKE_CURRENT_LIST_DIR}/mc/AnimationErrorTracker.cpp" + "${CMAKE_CURRENT_LIST_DIR}/mc/DisplayFrameQueue.cpp" + "${CMAKE_CURRENT_LIST_DIR}/mc/MetricsCalculator.cpp" + "${CMAKE_CURRENT_LIST_DIR}/mc/MetricsCalculatorAnimation.cpp" + "${CMAKE_CURRENT_LIST_DIR}/mc/MetricsCalculatorCpuGpu.cpp" + "${CMAKE_CURRENT_LIST_DIR}/mc/MetricsCalculatorDisplay.cpp" + "${CMAKE_CURRENT_LIST_DIR}/mc/MetricsCalculatorInput.cpp" + "${CMAKE_CURRENT_LIST_DIR}/mc/MetricsCalculatorInstrumented.cpp" + "${CMAKE_CURRENT_LIST_DIR}/mc/MetricsCalculatorInterFrameEvent.cpp" + "${CMAKE_CURRENT_LIST_DIR}/mc/MetricsTypes.cpp" + "${CMAKE_CURRENT_LIST_DIR}/mc/SwapChainState.cpp" + "${CMAKE_CURRENT_LIST_DIR}/mc/UnifiedSwapChain.cpp" +) + +add_library(pmon_metrics STATIC ${metrics_sources}) +add_library(pmon::metrics ALIAS pmon_metrics) +target_compile_definitions(pmon_metrics PRIVATE _LIB) +target_include_directories( + pmon_metrics + PUBLIC + "${PROJECT_SOURCE_DIR}" +) +target_link_libraries( + pmon_metrics + PUBLIC + pmon::common_utilities + pmon::present_data + PRIVATE + pmon::build_options + pmon::warnings_default +) +target_compile_options( + pmon_metrics + PRIVATE + $<$,$>:/guard:cf> +) +target_link_options( + pmon_metrics + INTERFACE + $<$,$>:/guard:cf> +) +set_target_properties( + pmon_metrics + PROPERTIES + ARCHIVE_OUTPUT_DIRECTORY + "${PMON_OUTPUT_ROOT}/lib/Metrics-${PMON_ARCHITECTURE}-$" + FOLDER "Foundation" +) diff --git a/IntelPresentMon/CommonUtilities/win/WinAPI.h b/IntelPresentMon/CommonUtilities/win/WinAPI.h index b9888c02a..f46e8ad8c 100644 --- a/IntelPresentMon/CommonUtilities/win/WinAPI.h +++ b/IntelPresentMon/CommonUtilities/win/WinAPI.h @@ -3,8 +3,12 @@ #pragma once #define _WIN32_WINNT 0x0603 #include +#ifndef WIN32_LEAN_AND_MEAN #define WIN32_LEAN_AND_MEAN +#endif +#ifndef NOMINMAX #define NOMINMAX +#endif #ifndef STRICT #define STRICT #endif diff --git a/IntelPresentMon/ControlLib/CMakeLists.txt b/IntelPresentMon/ControlLib/CMakeLists.txt new file mode 100644 index 000000000..99ed4a321 --- /dev/null +++ b/IntelPresentMon/ControlLib/CMakeLists.txt @@ -0,0 +1,60 @@ +find_package(nlohmann_json CONFIG REQUIRED) + +add_library( + pmon_control + STATIC + adl/Adl2Wrapper.cpp + adl/AdlTelemetryProvider.cpp + igcl/cApiWrapper.cpp + igcl/IgclErrorCodeProvider.cpp + igcl/IgclTelemetryProvider.cpp + nvapi/NvapiWrapper.cpp + nvapi/NvapiTelemetryProvider.cpp + nvml/NvmlWrapper.cpp + nvml/NvmlTelemetryProvider.cpp + TelemetryAvailabilityMerge.cpp + TelemetryMetricDiscovery.cpp + TelemetryCoordinator.cpp + TelemetryDeviceFingerprint.cpp + uci/UciTelemetryProvider.cpp + uci/UciWrapper.cpp + mock/MockTelemetryProvider.cpp + wmi/WmiTelemetryProvider.cpp +) +add_library(pmon::control ALIAS pmon_control) + +target_include_directories( + pmon_control + PUBLIC + "${PROJECT_SOURCE_DIR}" +) +target_link_libraries( + pmon_control + PUBLIC + pmon::interprocess + # UciSdk.h selects PMON_HAS_UCI_SDK with __has_include, so the include + # path alone decides whether UCI support is compiled in. + pmon::uci_sdk + PRIVATE + pmon::build_options + pmon::warnings_default + nlohmann_json::nlohmann_json + wbemuuid +) +target_compile_options( + pmon_control + PRIVATE + /Zc:preprocessor + $<$:/guard:cf> +) +target_link_options( + pmon_control + INTERFACE + $<$:/guard:cf> +) +set_target_properties( + pmon_control + PROPERTIES + OUTPUT_NAME ControlLib + FOLDER "Service and SDK" +) diff --git a/IntelPresentMon/Core/CMakeLists.txt b/IntelPresentMon/Core/CMakeLists.txt new file mode 100644 index 000000000..ff183fd0c --- /dev/null +++ b/IntelPresentMon/Core/CMakeLists.txt @@ -0,0 +1,113 @@ +set(core_sources + gfx/Graphics.cpp + gfx/impl/FastRenderer.cpp + gfx/layout/AxisMapping.cpp + gfx/layout/Element.cpp + gfx/layout/FlexElement.cpp + gfx/layout/GraphData.cpp + gfx/layout/GraphElement.cpp + gfx/layout/HistogramPlotElement.cpp + gfx/layout/LinePlotElement.cpp + gfx/layout/PlotElement.cpp + gfx/layout/ReadoutElement.cpp + gfx/layout/style/RawAttributeHelpers.cpp + gfx/layout/style/Selector.cpp + gfx/layout/style/StyleCompiler.cpp + gfx/layout/style/Stylesheet.cpp + gfx/layout/TextElement.cpp + gfx/prim/BrushPrimitive.cpp + gfx/prim/EnumConversion.cpp + gfx/prim/RectBorderPrimitive.cpp + gfx/prim/RectMultiBorderPrimitive.cpp + gfx/prim/RectPrimitive.cpp + gfx/prim/RetainedPrimitive.cpp + gfx/prim/TextPrimitive.cpp + gfx/prim/TextStylePrimitive.cpp + infra/LogSetup.cpp + infra/util/CooldownTimer.cpp + infra/util/FolderResolver.cpp + kernel/Kernel.cpp + kernel/Overlay.cpp + kernel/OverlayContainer.cpp + kernel/WindowActivateHandler.cpp + kernel/WindowMoveHandler.cpp + kernel/WindowSpawnHandler.cpp + pmon/DynamicQuery.cpp + pmon/FrameMetricCapabilitiesCsv.cpp + pmon/metric/DynamicPollingFetcher.cpp + pmon/metric/MetricFetcher.cpp + pmon/PresentMon.cpp + pmon/RawFrameDataWriter.cpp + pmon/StatisticsTracker.cpp + pmon/Timekeeper.cpp + win/EventHookManager.cpp + win/GpuUtilization.cpp + win/HotkeyListener.cpp + win/Key.cpp + win/MessageBox.cpp + win/MessageMap.cpp + win/ModSet.cpp + win/OverlayWindow.cpp + win/ProcessMapBuilder.cpp + win/StandardWindow.cpp + win/WinAPI.cpp + win/Window.cpp + win/WndClass.cpp +) +list(TRANSFORM core_sources PREPEND "source/") + +add_library(pmon_core STATIC ${core_sources}) +add_library(pmon::core ALIAS pmon_core) + +target_compile_definitions( + pmon_core + PRIVATE + IS_DEBUG=$,1,0> + _CRT_SECURE_NO_WARNINGS +) +target_include_directories( + pmon_core + PUBLIC + "${PROJECT_SOURCE_DIR}" + # Public Core headers use sibling-relative includes such as + # . + "${PROJECT_SOURCE_DIR}/IntelPresentMon" +) +target_link_libraries( + pmon_core + PUBLIC + pmon::common_utilities + pmon::api_wrapper + pmon::api_wrapper_common + PresentMonAPI2Loader + PRIVATE + pmon::build_options + pmon::warnings_default + d2d1 + d3d11 + d3dcompiler + dcomp + dwrite + dxgi + pdh + user32 + winmm +) +target_compile_options( + pmon_core + PRIVATE + /EHa + /fp:fast + $<$:/guard:cf> +) +target_link_options( + pmon_core + INTERFACE + $<$:/guard:cf> +) +set_target_properties( + pmon_core + PROPERTIES + OUTPUT_NAME Core + FOLDER "UI and Capture" +) diff --git a/IntelPresentMon/Interprocess/CMakeLists.txt b/IntelPresentMon/Interprocess/CMakeLists.txt new file mode 100644 index 000000000..69d7e0169 --- /dev/null +++ b/IntelPresentMon/Interprocess/CMakeLists.txt @@ -0,0 +1,72 @@ +set(enum_metric_header "${PMON_OUTPUT_ROOT}/obj/generated/metadata/EnumMetric.h") + +add_custom_command( + OUTPUT "${enum_metric_header}" + COMMAND + "${CMAKE_COMMAND}" + "-DPMON_AWK_EXECUTABLE=${PROJECT_SOURCE_DIR}/Tools/awk.exe" + "-DPMON_AWK_PROGRAM=${PROJECT_SOURCE_DIR}/Tools/generate/EnumMetric/metrics.awk" + "-DPMON_METRICS_CSV=${CMAKE_CURRENT_SOURCE_DIR}/../metrics.csv" + "-DPMON_ENUM_METRIC_OUTPUT=${enum_metric_header}" + -P "${PROJECT_SOURCE_DIR}/cmake/PresentMonGenerateEnumMetric.cmake" + DEPENDS + "${CMAKE_CURRENT_SOURCE_DIR}/../metrics.csv" + "${PROJECT_SOURCE_DIR}/Tools/generate/EnumMetric/metrics.awk" + "${PROJECT_SOURCE_DIR}/cmake/PresentMonGenerateEnumMetric.cmake" + COMMENT "Generating EnumMetric.h" + VERBATIM +) +set_source_files_properties("${enum_metric_header}" PROPERTIES GENERATED TRUE) + +find_package(boost_interprocess CONFIG REQUIRED) + +add_library( + pmon_interprocess + STATIC + source/DataStores.cpp + source/Interprocess.cpp + source/IntrospectionHelpers.cpp + source/IntrospectionPopulators.cpp + source/metadata/MetadataValidators.cpp + source/MetricCapabilities.cpp + source/ShmNamer.cpp + "${enum_metric_header}" +) +add_library(pmon::interprocess ALIAS pmon_interprocess) + +target_compile_definitions(pmon_interprocess PRIVATE _LIB) +target_include_directories( + pmon_interprocess + PUBLIC + "${PROJECT_SOURCE_DIR}" + # IntrospectionMetadata.h resolves from here. + "${PMON_OUTPUT_ROOT}/obj/generated" +) +target_link_libraries( + pmon_interprocess + PUBLIC + pmon::common_utilities + pmon::metrics + Boost::interprocess + PRIVATE + pmon::build_options + pmon::warnings_default +) +target_compile_options( + pmon_interprocess + PRIVATE + /EHa + /we4062 + $<$:/guard:cf> +) +target_link_options( + pmon_interprocess + INTERFACE + $<$:/guard:cf> +) +set_target_properties( + pmon_interprocess + PROPERTIES + OUTPUT_NAME Interprocess + FOLDER "Service and SDK" +) diff --git a/IntelPresentMon/KernelProcess/CMakeLists.txt b/IntelPresentMon/KernelProcess/CMakeLists.txt new file mode 100644 index 000000000..7d9aca5ea --- /dev/null +++ b/IntelPresentMon/KernelProcess/CMakeLists.txt @@ -0,0 +1,71 @@ +find_package(boost_process CONFIG REQUIRED) + +add_executable( + KernelProcess + WIN32 + kact/KernelActionRegistration.cpp + MakeOverlaySpec.cpp + winmain.cpp + KernelProcess.rc +) +# Matches the CefNano and PresentMonService ProjectReferences in +# KernelProcess.vcxproj: KernelProcess launches both by name at runtime and +# does not link either. PresentMonAPI2.dll is loaded at runtime (see +# --middleware-dll-path); build it into the output dir before F5. +add_dependencies(KernelProcess PresentMonUI PresentMonService PresentMonAPI2) + +target_link_libraries( + KernelProcess + PRIVATE + pmon::build_options + pmon::warnings_default + pmon::version_resource + pmon::release_link_policy + pmon::common_utilities + pmon::core + pmon::interprocess + pmon::api_wrapper + pmon::versioning + Boost::process + ntdll +) +target_compile_definitions( + KernelProcess + PRIVATE + _CONSOLE + PM_KERNEL_PROCESS_BUILD=1 +) +target_include_directories( + KernelProcess + PRIVATE + # RC.exe resolves the ICON statement in KernelProcess.rc against its own + # working directory, which for a CMake-generated Visual Studio project is + # not this source directory. + $<$:${CMAKE_CURRENT_SOURCE_DIR}> +) +target_compile_options( + KernelProcess + PRIVATE + $<$:/guard:cf> +) +target_link_options( + KernelProcess + PRIVATE + $<$:/guard:cf> +) +pmon_apply_kernel_process_manifest(KernelProcess) +pmon_attach_kernel_process_developer_signing(KernelProcess) +set_target_properties( + KernelProcess + PROPERTIES + OUTPUT_NAME PresentMon + FOLDER "UI and Capture" + VS_DEBUGGER_WORKING_DIRECTORY "${PMON_OUTPUT_ROOT}/$" + # Matches KernelProcess.args.json preset VS-Debug-Run for F5 from the + # CMake-generated Visual Studio solution. + VS_DEBUGGER_COMMAND_ARGUMENTS "--svc-as-child --files-working --log-level verbose --middleware-dll-path .\\PresentMonAPI2.dll --log-middleware-copy" +) + +set_property(DIRECTORY "${CMAKE_BINARY_DIR}" PROPERTY VS_STARTUP_PROJECT KernelProcess) + +pmon_target_uses_kernel_payload(KernelProcess) diff --git a/IntelPresentMon/KernelProcess/KernelProcess.args.json b/IntelPresentMon/KernelProcess/KernelProcess.args.json index 9b07d5c52..05cfe9d05 100644 --- a/IntelPresentMon/KernelProcess/KernelProcess.args.json +++ b/IntelPresentMon/KernelProcess/KernelProcess.args.json @@ -185,10 +185,6 @@ { "Id": "9364f1fd-7146-4233-9e19-83cd7e3b7ddb", "Command": "--log-middleware-copy" - }, - { - "Id": "c8f4e2a1-9b3d-4f7e-a1c2-6d5e8f0b2a15", - "Command": "--svc-flag enable-mock-telemetry" } ] }, diff --git a/IntelPresentMon/PMInstaller/PMInstaller.wixproj b/IntelPresentMon/PMInstaller/PMInstaller.wixproj index 5be62e1d4..a4505ac8b 100644 --- a/IntelPresentMon/PMInstaller/PMInstaller.wixproj +++ b/IntelPresentMon/PMInstaller/PMInstaller.wixproj @@ -1,6 +1,7 @@ + Debug x86 @@ -12,9 +13,10 @@ PMInstaller - ..\..\build\$(Configuration)\ - ..\..\build\obj\PMInstaller-$(Platform)-$(Configuration)\ - Debug;ConsoleAppVersion=$(PresentMonVersion);ProductVersion=$(PresentMonProductVersion) + ..\..\build\$(Configuration)\ + ..\..\build\obj\PMInstaller-$(Platform)-$(Configuration)\ + Debug;ConsoleAppVersion=$(PresentMonVersion);ProductVersion=$(PresentMonProductVersion) + $(DefineConstants);Debug;ConsoleAppVersion=$(PresentMonVersion);ProductVersion=$(PresentMonProductVersion) -arch x64 @@ -22,10 +24,11 @@ - ..\..\build\$(Configuration)\ - ..\..\build\obj\PMInstaller-$(Platform)-$(Configuration)\ + ..\..\build\$(Configuration)\ + ..\..\build\obj\PMInstaller-$(Platform)-$(Configuration)\ -arch x64 - ConsoleAppVersion=$(PresentMonVersion);ProductVersion=$(PresentMonProductVersion) + ConsoleAppVersion=$(PresentMonVersion);ProductVersion=$(PresentMonProductVersion) + $(DefineConstants);ConsoleAppVersion=$(PresentMonVersion);ProductVersion=$(PresentMonProductVersion) ICE60 @@ -42,7 +45,11 @@ - + + $(PmonInstallerExtensionDll) + PMInstallerExtension + + ..\..\build\obj\PMInstallerExtension-$(Configuration)\PMInstallerExtension.dll PMInstallerExtension @@ -55,7 +62,7 @@ WixUtilExtension - + Provider {807370fb-ade0-403a-a278-df50893e5f94} @@ -119,7 +126,15 @@ - + + + + + + + - + + Library PMInstallerLib - $(SolutionDir)build\obj\$(MSBuildProjectName)-$(Platform)-$(Configuration) - $(SolutionDir)build\$(Configuration) + $(SolutionDir)build\obj\$(MSBuildProjectName)-$(Platform)-$(Configuration) + $(SolutionDir)build\$(Configuration) $(PMON_UCI_SDK_DIR) $(MSBuildProjectDirectory)\..\ControlLib\uci\external - $(DefineConstants);UciDistDir=$(UciSdkDir) + $(DefineConstants);UciDistDir=$(UciSdkDir) $(WIX)bin\ $(MSBuildExtensionsPath32)\Microsoft\WiX\v3.x\Wix.targets @@ -17,13 +18,14 @@ true {6478f2f6-92ad-4b1c-a13e-c8cea9125152} - Generated\UciDist.wxs + Generated\UciDist.wxs + $(PmonUciDistFragmentPath) - + PresentMonAPI2 {5ddba061-53a0-4835-8aaf-943f403f924f} @@ -41,7 +43,7 @@ INSTALLFOLDER - + diff --git a/IntelPresentMon/PMInstallerLib/wix-uci-dist.ps1 b/IntelPresentMon/PMInstallerLib/wix-uci-dist.ps1 index 57f0cf7ca..e6d78bf21 100644 --- a/IntelPresentMon/PMInstallerLib/wix-uci-dist.ps1 +++ b/IntelPresentMon/PMInstallerLib/wix-uci-dist.ps1 @@ -1,7 +1,12 @@ [CmdletBinding()] param( [string]$UciSdkDir, - [string]$OutPath = 'UciDist.wxs' + [string]$OutPath = 'UciDist.wxs', + [string]$DistPath, + [string]$WixRoot, + [string]$HeatExe, + [switch]$CMakePackaging, + [string]$UciEnabled = '' ) function Get-DeterministicGuid { @@ -23,6 +28,27 @@ function Get-DeterministicGuid { return (New-Object System.Guid -ArgumentList (, $bytes)).ToString().ToUpperInvariant() } +function Get-DeterministicWixId { + param( + [Parameter(Mandatory = $true)] + [string]$Name, + [Parameter(Mandatory = $true)] + [string]$Prefix + ) + + $md5 = [System.Security.Cryptography.MD5]::Create() + try { + $bytes = $md5.ComputeHash([System.Text.Encoding]::UTF8.GetBytes($Name.ToLowerInvariant())) + } + finally { + $md5.Dispose() + } + + $hashHex = [System.BitConverter]::ToString($bytes).Replace('-', '').ToLowerInvariant() + # Merge modules decorate Component/File ids with ".{ModulePackageGuid}" (~37 chars). MSI columns allow 72. + return $Prefix + $hashHex.Substring(0, 8) +} + function Write-EmptyUciDistFragment { param( [Parameter(Mandatory = $true)] @@ -53,49 +79,181 @@ function Write-EmptyUciDistFragment { } } -if ([string]::IsNullOrWhiteSpace($UciSdkDir)) { - $UciSdkDir = $env:PMON_UCI_SDK_DIR +function Resolve-UciEnabledFlag { + param( + [string]$RawValue, + [bool]$DefaultWhenUnset + ) + + if ([string]::IsNullOrWhiteSpace($RawValue)) { + return $DefaultWhenUnset + } + + switch ($RawValue.Trim().ToLowerInvariant()) { + '1' { return $true } + 'true' { return $true } + 'on' { return $true } + 'yes' { return $true } + '0' { return $false } + 'false' { return $false } + 'off' { return $false } + 'no' { return $false } + default { + throw "Invalid -UciEnabled value '$RawValue'. Use true or false." + } + } } -if ([string]::IsNullOrWhiteSpace($UciSdkDir)) { - $UciSdkDir = Join-Path $PSScriptRoot '..\ControlLib\uci\external' + +function Resolve-HeatExecutable { + param( + [string]$HeatExePath, + [string]$WixRootPath + ) + + if (-not [string]::IsNullOrWhiteSpace($HeatExePath)) { + if (-not (Test-Path -LiteralPath $HeatExePath -PathType Leaf)) { + throw "heat.exe not found at '$HeatExePath'." + } + return $HeatExePath + } + + $resolvedWixRoot = $WixRootPath + if ([string]::IsNullOrWhiteSpace($resolvedWixRoot)) { + $resolvedWixRoot = $env:WIX + } + if ([string]::IsNullOrWhiteSpace($resolvedWixRoot)) { + throw 'WiX root is not set. Pass -WixRoot, -HeatExe, or set the WIX environment variable.' + } + + $candidate = Join-Path $resolvedWixRoot 'bin\heat.exe' + if (-not (Test-Path -LiteralPath $candidate -PathType Leaf)) { + throw "heat.exe not found at '$candidate'. Verify the WiX installation path." + } + return $candidate +} + +function Test-HarvestedRuntimeSources { + param( + [Parameter(Mandatory = $true)] + [string]$StageRoot, + + [Parameter(Mandatory = $true)] + [xml]$Document, + + [Parameter(Mandatory = $true)] + [System.Xml.XmlNamespaceManager]$NamespaceManager + ) + + $stageFullPath = (Resolve-Path -LiteralPath $StageRoot).Path + if (-not $stageFullPath.EndsWith('\')) { + $stageFullPath += '\' + } + + $fileNodes = $Document.SelectNodes('//w:File', $NamespaceManager) + foreach ($fileNode in $fileNodes) { + $source = $fileNode.GetAttribute('Source') + if ([string]::IsNullOrWhiteSpace($source)) { + throw 'Encountered a harvested File node without a Source attribute.' + } + + if ($source -match '\.\.') { + throw "Harvested source contains parent traversal: $source" + } + + $normalizedSource = $source -replace '/', '\' + if ($normalizedSource -match '^[a-zA-Z]:\\') { + $resolvedSource = $normalizedSource + } + elseif ($normalizedSource -like '$(var.UciDistDir)*') { + $relative = $normalizedSource.Substring('$(var.UciDistDir)'.Length).TrimStart('\') + $resolvedSource = Join-Path $stageFullPath $relative + } + else { + $resolvedSource = Join-Path $stageFullPath $normalizedSource.TrimStart('\') + } + + $resolvedSource = [System.IO.Path]::GetFullPath($resolvedSource) + if (-not $resolvedSource.StartsWith($stageFullPath, [System.StringComparison]::OrdinalIgnoreCase)) { + throw "Harvested source escapes the runtime stage: $source" + } + + if (-not (Test-Path -LiteralPath $resolvedSource -PathType Leaf)) { + throw "Harvested source file is missing from the runtime stage: $source" + } + + $item = Get-Item -LiteralPath $resolvedSource -Force + if ($item.Attributes -band [IO.FileAttributes]::ReparsePoint) { + throw "Harvested source is a reparse point: $resolvedSource" + } + + } } -$distPath = $UciSdkDir $outDir = Split-Path -Parent $OutPath if (-not [string]::IsNullOrWhiteSpace($outDir) -and -not (Test-Path $outDir -PathType Container)) { New-Item -Path $outDir -ItemType Directory -Force | Out-Null } -Push-Location $PSScriptRoot +$uciEnabledForCMake = $false +if ($CMakePackaging) { + $uciEnabledForCMake = Resolve-UciEnabledFlag -RawValue $UciEnabled -DefaultWhenUnset $false + if (-not $uciEnabledForCMake) { + Write-EmptyUciDistFragment -OutPath $OutPath + exit 0 + } -if (-not (Test-Path $distPath -PathType Container)) { - Pop-Location - Write-Warning "UCI dist directory '$distPath' was not found. Skipping UCI payload files." - Write-EmptyUciDistFragment -OutPath $OutPath - exit 0 -} + if ([string]::IsNullOrWhiteSpace($DistPath)) { + throw 'CMake UCI packaging requires -DistPath when UCI is enabled.' + } + if (-not (Test-Path -LiteralPath $DistPath -PathType Container)) { + throw "UCI runtime stage directory was not found: $DistPath" + } -if ($null -eq (Get-ChildItem -Path $distPath -Recurse -File -Force | Select-Object -First 1)) { - Pop-Location - Write-Warning "UCI dist directory '$distPath' contains no files. Skipping UCI payload files." - Write-EmptyUciDistFragment -OutPath $OutPath - exit 0 -} + $dllPath = Join-Path $DistPath 'unified-collector-interface.dll' + $collectorsPath = Join-Path $DistPath 'collectors' + if (-not (Test-Path -LiteralPath $dllPath -PathType Leaf)) { + throw "Enabled UCI runtime stage is missing unified-collector-interface.dll at $dllPath" + } + if (-not (Test-Path -LiteralPath $collectorsPath -PathType Container)) { + throw "Enabled UCI runtime stage is missing collectors directory at $collectorsPath" + } + $collectorFile = Get-ChildItem -LiteralPath $collectorsPath -Recurse -File -Force | Select-Object -First 1 + if ($null -eq $collectorFile) { + throw "Enabled UCI runtime stage collectors directory contains no files: $collectorsPath" + } -if (-not $env:WIX) { - Pop-Location - Write-Error 'Environment variable WIX is not set. Please install WIX or set WIX to the WiX installation directory.' - exit 1 + $distPathForHeat = (Resolve-Path -LiteralPath $DistPath).Path } +else { + Push-Location $PSScriptRoot + + if ([string]::IsNullOrWhiteSpace($UciSdkDir)) { + $UciSdkDir = $env:PMON_UCI_SDK_DIR + } + if ([string]::IsNullOrWhiteSpace($UciSdkDir)) { + $UciSdkDir = Join-Path $PSScriptRoot '..\ControlLib\uci\external' + } + + $distPathForHeat = $UciSdkDir -$heatExe = Join-Path $env:WIX 'bin\heat.exe' -if (-not (Test-Path $heatExe)) { - Pop-Location - Write-Error "heat.exe not found at '$heatExe'. Verify your WIX environment variable points to the correct location." - exit 1 + if (-not (Test-Path $distPathForHeat -PathType Container)) { + Pop-Location + Write-Warning "UCI dist directory '$distPathForHeat' was not found. Skipping UCI payload files." + Write-EmptyUciDistFragment -OutPath $OutPath + exit 0 + } + + if ($null -eq (Get-ChildItem -Path $distPathForHeat -Recurse -File -Force | Select-Object -First 1)) { + Pop-Location + Write-Warning "UCI dist directory '$distPathForHeat' contains no files. Skipping UCI payload files." + Write-EmptyUciDistFragment -OutPath $OutPath + exit 0 + } } -& $heatExe dir $distPath ` +$heatExePath = Resolve-HeatExecutable -HeatExePath $HeatExe -WixRootPath $WixRoot + +& $heatExePath dir $distPathForHeat ` -srd -sreg -scom ` -dr service_folder ` -cg uci_dist_files ` @@ -105,8 +263,10 @@ if (-not (Test-Path $heatExe)) { $code = $LASTEXITCODE if ($code -ne 0) { - Pop-Location - Write-Error "heat.exe failed for '$distPath' (exit code $code)" + if (-not $CMakePackaging) { + Pop-Location + } + Write-Error "heat.exe failed for '$distPathForHeat' (exit code $code)" exit $code } @@ -114,12 +274,18 @@ if ($code -ne 0) { $ns = New-Object System.Xml.XmlNamespaceManager($doc.NameTable) $ns.AddNamespace('w', 'http://schemas.microsoft.com/wix/2006/wi') +if ($CMakePackaging) { + Test-HarvestedRuntimeSources -StageRoot $distPathForHeat -Document $doc -NamespaceManager $ns +} + $componentNodes = $doc.SelectNodes('//w:Component', $ns) foreach ($componentNode in $componentNodes) { $componentNode.SetAttribute('Win64', 'yes') $fileNode = $componentNode.SelectSingleNode('w:File', $ns) if ($null -eq $fileNode) { - Pop-Location + if (-not $CMakePackaging) { + Pop-Location + } Write-Error 'Encountered a harvested component without a File node.' exit 1 } @@ -127,6 +293,10 @@ foreach ($componentNode in $componentNodes) { $source = $fileNode.GetAttribute('Source') $guid = Get-DeterministicGuid -Name $source $componentNode.SetAttribute('Guid', "{$guid}") + $fileId = Get-DeterministicWixId -Name "file:$source" -Prefix 'f' + $componentId = Get-DeterministicWixId -Name "cmp:$source" -Prefix 'c' + $fileNode.SetAttribute('Id', $fileId) + $componentNode.SetAttribute('Id', $componentId) } $settings = New-Object System.Xml.XmlWriterSettings @@ -142,5 +312,7 @@ try { } finally { $writer.Dispose() - Pop-Location + if (-not $CMakePackaging) { + Pop-Location + } } diff --git a/IntelPresentMon/PmonCMakePackaging.wixproj.props b/IntelPresentMon/PmonCMakePackaging.wixproj.props new file mode 100644 index 000000000..76d0abb61 --- /dev/null +++ b/IntelPresentMon/PmonCMakePackaging.wixproj.props @@ -0,0 +1,18 @@ + + + + + false + false + $(DefineConstants);PmonUseExplicitConsolePath=true;PmonCMakePackaging=true;PmonConsoleTargetPath=$(PmonConsoleTargetPath);PmonConsoleTargetFileName=$(PmonConsoleTargetFileName);PmonPresentMonAPI2LoaderLibSource=$(PmonPresentMonAPI2LoaderLibSource);PmonPresentMonAPI2HeaderSource=$(PmonPresentMonAPI2HeaderSource);Provider.TargetPath=$(PmonProviderTargetPath);Provider.ProjectDir=$(PmonProviderProjectDir);PresentMon.TargetPath=$(PmonPresentMonTargetPath);PresentMon.TargetDir=$(PmonPresentMonTargetDir);PresentMon.TargetFileName=$(PmonPresentMonTargetFileName);KernelProcess.TargetPath=$(PmonKernelProcessTargetPath);KernelProcess.TargetFileName=$(PmonKernelProcessTargetFileName);PresentMonService.TargetPath=$(PmonPresentMonServiceTargetPath);PresentMonService.TargetDir=$(PmonPresentMonServiceTargetDir);PresentMonService.TargetFileName=$(PmonPresentMonServiceTargetFileName);PresentMonAPI2.TargetPath=$(PmonPresentMonAPI2TargetPath);PresentMonAPI2.TargetFileName=$(PmonPresentMonAPI2TargetFileName);PresentMonAPI2.ProjectDir=$(PmonPresentMonAPI2ProjectDir);PresentMonAPI2Loader.TargetPath=$(PmonPresentMonAPI2LoaderTargetPath);PresentMonAPI2Loader.TargetDir=$(PmonPresentMonAPI2LoaderTargetDir);PresentMonAPI2Loader.TargetFileName=$(PmonPresentMonAPI2LoaderTargetFileName);SolutionDir=$(PmonSolutionDir);UciDistDir=$(PmonUciDistDir) + + + $(PmonOutputPath) + + + $(PmonIntermediateOutputPath) + + + + + diff --git a/IntelPresentMon/PresentMonAPI2/CMakeLists.txt b/IntelPresentMon/PresentMonAPI2/CMakeLists.txt new file mode 100644 index 000000000..06c51a47c --- /dev/null +++ b/IntelPresentMon/PresentMonAPI2/CMakeLists.txt @@ -0,0 +1,46 @@ +add_library( + PresentMonAPI2 + SHARED + dllmain.cpp + PresentMonAPI.cpp + PresentMonDiagnostics.cpp + PresentMonAPI2.rc +) + +target_compile_definitions( + PresentMonAPI2 + PRIVATE + PRESENTMONAPI2_EXPORTS + _WINDOWS + _USRDLL + $<$:_CRTDBG_MAP_ALLOC> +) +target_link_libraries( + PresentMonAPI2 + PRIVATE + pmon::build_options + pmon::warnings_default + pmon::version_resource + pmon::release_link_policy + pmon::common_utilities + pmon::middleware + shlwapi +) +target_compile_options( + PresentMonAPI2 + PRIVATE + /EHa + $<$:/guard:cf> +) +target_link_options( + PresentMonAPI2 + PRIVATE + /SUBSYSTEM:WINDOWS + /MANIFESTUAC:NO + $<$:/guard:cf> +) +set_target_properties( + PresentMonAPI2 + PROPERTIES + FOLDER "Service and SDK" +) diff --git a/IntelPresentMon/PresentMonAPI2Loader/CMakeLists.txt b/IntelPresentMon/PresentMonAPI2Loader/CMakeLists.txt new file mode 100644 index 000000000..cde334f3a --- /dev/null +++ b/IntelPresentMon/PresentMonAPI2Loader/CMakeLists.txt @@ -0,0 +1,47 @@ +add_library( + PresentMonAPI2Loader + SHARED + dllmain.cpp + Implementation.cpp + PresentMonAPI2Loader.rc +) + +target_compile_definitions( + PresentMonAPI2Loader + PRIVATE + PRESENTMONAPI2_EXPORTS + _WINDOWS + _USRDLL +) +target_include_directories( + PresentMonAPI2Loader + PUBLIC + "${PROJECT_SOURCE_DIR}" +) +target_link_libraries( + PresentMonAPI2Loader + PRIVATE + pmon::build_options + pmon::warnings_default + pmon::version_resource + pmon::release_link_policy + pmon::common_utilities + pmon::versioning +) +target_compile_options( + PresentMonAPI2Loader + PRIVATE + $<$:/guard:cf> +) +target_link_options( + PresentMonAPI2Loader + PRIVATE + /SUBSYSTEM:WINDOWS + /MANIFESTUAC:NO + $<$:/guard:cf> +) +set_target_properties( + PresentMonAPI2Loader + PROPERTIES + FOLDER "Service and SDK" +) diff --git a/IntelPresentMon/PresentMonAPI2Loader/Implementation.cpp b/IntelPresentMon/PresentMonAPI2Loader/Implementation.cpp index dc3096008..76abef842 100644 --- a/IntelPresentMon/PresentMonAPI2Loader/Implementation.cpp +++ b/IntelPresentMon/PresentMonAPI2Loader/Implementation.cpp @@ -378,7 +378,7 @@ PRESENTMON_API2_EXPORT LoggingSingletons pmLinkLoggingPtrs_(pmon::util::log::ICh { if (!middlewareLoadedSuccessfully_) { if (auto status = LoadLibrary_(); status != PM_STATUS_SUCCESS) { - throw LoaderExcept_(status); + return {}; } } if (!pFunc_pmLinkLoggingPtrs__) { diff --git a/IntelPresentMon/PresentMonAPI2Tests/CMakeLists.txt b/IntelPresentMon/PresentMonAPI2Tests/CMakeLists.txt new file mode 100644 index 000000000..5019d7493 --- /dev/null +++ b/IntelPresentMon/PresentMonAPI2Tests/CMakeLists.txt @@ -0,0 +1,110 @@ +include(PresentMonCppUnitTest) + +find_package(boost_process CONFIG REQUIRED) +find_package(boost_filesystem CONFIG REQUIRED) +find_package(unofficial-vincentlaucsb-csv-parser CONFIG REQUIRED) + +set(api2_gen_script "${PROJECT_SOURCE_DIR}/Scripts/generate_etl_csv_tests.py") +set(api2_csv "${CMAKE_CURRENT_SOURCE_DIR}/test_cases.csv") + +set(api2_generated_headers) +foreach(config IN LISTS CMAKE_CONFIGURATION_TYPES) + set( + generated_header + "${PMON_OUTPUT_ROOT}/obj/${config}/PresentMonAPI2Tests/Generated/EtlCsvTestCases.g.h" + ) + add_custom_command( + OUTPUT "${generated_header}" + COMMAND + "${PMON_PYTHON_EXECUTABLE}" + "${api2_gen_script}" + --input-csv "${api2_csv}" + --output-header "${generated_header}" + DEPENDS + "${api2_gen_script}" + "${api2_csv}" + COMMENT "Generating EtlCsvTestCases.g.h (${config})" + VERBATIM + ) + list(APPEND api2_generated_headers "${generated_header}") +endforeach() + +add_custom_target( + pmon_api2_etl_csv_cases + DEPENDS ${api2_generated_headers} +) +set_target_properties( + pmon_api2_etl_csv_cases + PROPERTIES + FOLDER "Build" +) + +add_library( + PresentMonAPI2Tests + SHARED + EtlTests.cpp + InterimBroadcasterTests.cpp + IpcComponentTests.cpp + IpcMcIntegrationTests.cpp + Logging.cpp + MockTelemetryTests.cpp + ModuleInit.cpp + MultiClientTests.cpp + PacedFrameTests.cpp + PacedPollingTests.cpp + RealtimeMetricTests.cpp + UiLaunchTests.cpp + "${PROJECT_SOURCE_DIR}/IntelPresentMon/CommonUtilities/test/MachineExpectations.cpp" +) +add_dependencies(PresentMonAPI2Tests pmon_api2_etl_csv_cases) +add_dependencies(PresentMonAPI2Tests pmon_ensure_aux_testdata) + +target_include_directories( + PresentMonAPI2Tests + PRIVATE + "${PROJECT_SOURCE_DIR}" + "${PROJECT_SOURCE_DIR}/IntelPresentMon" + "${PMON_OUTPUT_ROOT}/obj/$/PresentMonAPI2Tests/Generated" +) +target_link_libraries( + PresentMonAPI2Tests + PRIVATE + pmon::build_options + pmon::warnings_default + pmon::release_link_policy + pmon::common_utilities + pmon::interprocess + pmon::api_wrapper + pmon::middleware + pmon::versioning + PresentMonAPI2Loader + PresentMonAPI2 + Boost::process + Boost::filesystem + unofficial::vincentlaucsb-csv-parser::csv + ntdll +) +set_target_properties( + PresentMonAPI2Tests + PROPERTIES + OUTPUT_NAME PresentMonAPI2Tests + FOLDER "Tests" +) + +pmon_apply_cppunittest_framework(PresentMonAPI2Tests) + +set(api2_runtime_dependencies + PresentMonService + PresentMonAPI2 + PresentMonAPI2Loader + KernelProcess + PresentMonUI + SampleClient +) +if(TARGET pmon_stage_ui_payload) + list(APPEND api2_runtime_dependencies pmon_stage_ui_payload) +endif() +if(TARGET pmon_stage_kernel_payload) + list(APPEND api2_runtime_dependencies pmon_stage_kernel_payload) +endif() +add_dependencies(PresentMonAPI2Tests ${api2_runtime_dependencies}) diff --git a/IntelPresentMon/PresentMonAPI2Tests/ModuleInit.cpp b/IntelPresentMon/PresentMonAPI2Tests/ModuleInit.cpp index 30879b69e..71620f006 100644 --- a/IntelPresentMon/PresentMonAPI2Tests/ModuleInit.cpp +++ b/IntelPresentMon/PresentMonAPI2Tests/ModuleInit.cpp @@ -26,6 +26,22 @@ void WipeAndRecreate(const fs::path& path) } } +void EnsureExists(const fs::path& path) +{ + // Create without wiping. Machine expectation measurements accumulate across the + // whole suite via std::ios::app, but module initialize runs once per load of this + // DLL. CTest registers every test method as its own vstest.console process, so + // wiping here would discard the previous test's measurements and leave at most one + // test case recorded. Provisioning deletes measurements.jsonl explicitly instead. + try { + fs::create_directories(path); + } + catch (const std::exception& ex) { + Logger::WriteMessage(std::format("Failed to create folder [{}]: {}\n", path.string(), ex.what()).c_str()); + throw; // let MSTest see this as a test infrastructure error + } +} + TEST_MODULE_INITIALIZE(Api2TestModuleInit) { pmon::test::InstallTestDiagnosticsRedirect(); @@ -53,7 +69,7 @@ TEST_MODULE_INITIALIZE(Api2TestModuleInit) WipeAndRecreate(InterimBroadcasterTests::logFolder_); WipeAndRecreate(InterimBroadcasterTests::outFolder_); WipeAndRecreate(IpcMcIntegrationTests::logFolder_); - WipeAndRecreate(pmon::util::test::MachineExpectationOutputFolder); + EnsureExists(pmon::util::test::MachineExpectationOutputFolder); WipeAndRecreate(RealtimeMetricTests::logFolder_); WipeAndRecreate(RealtimeMetricTests::outFolder_); WipeAndRecreate(LoggingTests::logFolder_); diff --git a/IntelPresentMon/PresentMonAPI2Tests/PresentMonAPI2Tests.vcxproj b/IntelPresentMon/PresentMonAPI2Tests/PresentMonAPI2Tests.vcxproj index dfe0afa76..73c39af3d 100644 --- a/IntelPresentMon/PresentMonAPI2Tests/PresentMonAPI2Tests.vcxproj +++ b/IntelPresentMon/PresentMonAPI2Tests/PresentMonAPI2Tests.vcxproj @@ -17,6 +17,8 @@ PresentMonAPI2Tests 10.0 NativeUnitTestProject + $(MSBuildProjectDirectory)\..\..\Tests\PresentMonTests.runsettings + $(MSBuildProjectDirectory)\..\..\Tests\PresentMonTests.local.runsettings @@ -173,5 +175,6 @@ + diff --git a/IntelPresentMon/PresentMonAPI2Tests/update_machine_expectations.py b/IntelPresentMon/PresentMonAPI2Tests/update_machine_expectations.py index ca80ac209..37b5d1906 100644 --- a/IntelPresentMon/PresentMonAPI2Tests/update_machine_expectations.py +++ b/IntelPresentMon/PresentMonAPI2Tests/update_machine_expectations.py @@ -6,6 +6,7 @@ import json import math import os +import sys from pathlib import Path import tkinter as tk from tkinter import filedialog, messagebox, ttk @@ -240,6 +241,104 @@ def write_expectation(self): messagebox.showinfo("Wrote expectation", self.expectation_path) +def find_expectation(root, measurement): + """Read-only lookup of the expectation for one measurement, or None.""" + metric = measurement["metric"] + if measurement["section"] == "system": + return root.get("system", {}).get(metric) + device_id = int(measurement.get("device_id", 0)) + for gpu in root.get("gpu", []): + if int(gpu.get("device_id", -1)) == device_id: + return gpu.get("metrics", {}).get(metric) + return None + + +def count_expectations(root): + system_count = len(root.get("system", {})) + gpus = [(gpu.get("device_id"), len(gpu.get("metrics", {}))) for gpu in root.get("gpu", [])] + return system_count, gpus + + +def check_expectations(expectation_path, measurements_path): + """Validate the machine expectation file. Returns (ok, lines).""" + lines = [] + if not os.path.exists(expectation_path): + lines.append(f"error: Machine expectations not found: {expectation_path}") + lines.append(" This machine has not been provisioned yet. Record measurements by") + lines.append(" running the API2 tests, then generate the file:") + lines.append(f' python "{os.path.abspath(__file__)}" --measurements "{os.path.abspath(measurements_path)}"') + return False, lines + + try: + with open(expectation_path, "r", encoding="utf-8") as f: + root = json.load(f) + except (json.JSONDecodeError, OSError) as e: + lines.append(f"error: Machine expectations could not be read: {expectation_path}") + lines.append(f" {e}") + return False, lines + + if not isinstance(root, dict): + lines.append(f"error: Machine expectation root must be an object: {expectation_path}") + return False, lines + + normalize_gpu_expectations(root) + if not isinstance(root.get("system", {}), dict): + lines.append("error: Machine expectation 'system' must be an object.") + return False, lines + if not isinstance(root.get("gpu", []), list): + lines.append("error: Machine expectation 'gpu' must be a list.") + return False, lines + + system_count, gpus = count_expectations(root) + if not system_count and not any(count for _, count in gpus): + lines.append(f"error: Machine expectations contain no metrics: {expectation_path}") + return False, lines + + lines.append(f"Machine expectations present: {expectation_path}") + summary = f" system, {system_count} metric(s)" + for device_id, count in gpus: + summary += f"; gpu device_id {device_id}, {count} metric(s)" + lines.append(summary) + + # Without measurements this can only confirm the file is well formed. It cannot + # tell whether the baseline was authored on this machine. + if not measurements_path or not os.path.exists(measurements_path): + lines.append(" (pass --measurements to also verify coverage for this machine)") + return True, lines + + records = load_measurements(measurements_path) + missing = [] + mismatched = [] + test_cases = set() + for record in records: + test_cases.add(record.get("test_case", "")) + for measurement in record.get("measurements", []): + expectation = find_expectation(root, measurement) + if expectation is None: + missing.append(f"{measurement['section']}.{measurement['metric']}") + elif measurement.get("identity_only") and isinstance(expectation, str): + if expectation != measurement.get("value"): + mismatched.append( + f"{measurement['section']}.{measurement['metric']}: " + f"expected {expectation!r}, measured {measurement.get('value')!r}" + ) + + lines.append(f" measurements: {len(records)} test case(s) recorded") + if mismatched: + lines.append("error: Machine expectations were authored on different hardware:") + for item in sorted(set(mismatched)): + lines.append(f" {item}") + return False, lines + if missing: + lines.append("error: Machine expectations are missing entries for measured metrics:") + for item in sorted(set(missing)): + lines.append(f" {item}") + return False, lines + + lines.append(" coverage: all measured metrics have expectations") + return True, lines + + def main(): parser = argparse.ArgumentParser(description="Generate or update PresentMon API2 machine expectations.") parser.add_argument( @@ -253,8 +352,21 @@ def main(): help="Path to the editable machine expectation JSON file.", ) parser.add_argument("--print", action="store_true", help="Print generated JSON and exit.") + parser.add_argument( + "--check", + action="store_true", + help="Verify the machine expectation file exists and is usable, then exit. " + "Exits non-zero when this machine is not provisioned. Add --measurements " + "to also verify the baseline covers this machine.", + ) args = parser.parse_args() + if args.check: + ok, lines = check_expectations(args.expectation, args.measurements) + for line in lines: + print(line, file=sys.stderr if not ok else sys.stdout) + return 0 if ok else 1 + records = load_measurements(args.measurements) existing = None if os.path.exists(args.expectation): @@ -266,11 +378,12 @@ def main(): for change in changes: print(f"{change['section']}.{change['metric']}: {change['before']} -> {change['after']}") print(pretty_json(generated)) - return + return 0 app = ExpectationEditor(args.measurements, args.expectation) app.mainloop() + return 0 if __name__ == "__main__": - main() + raise SystemExit(main()) diff --git a/IntelPresentMon/PresentMonAPIWrapper/CMakeLists.txt b/IntelPresentMon/PresentMonAPIWrapper/CMakeLists.txt new file mode 100644 index 000000000..04382f435 --- /dev/null +++ b/IntelPresentMon/PresentMonAPIWrapper/CMakeLists.txt @@ -0,0 +1,47 @@ +add_library( + pmon_api_wrapper + STATIC + BlobContainer.cpp + DiagnosticHandler.cpp + DynamicQuery.cpp + EtlLogger.cpp + FixedQuery.cpp + FrameQuery.cpp + ProcessTracker.cpp + Session.cpp + StaticQuery.cpp +) +add_library(pmon::api_wrapper ALIAS pmon_api_wrapper) + +target_compile_definitions(pmon_api_wrapper PRIVATE _LIB) +target_include_directories( + pmon_api_wrapper + PUBLIC + "${PROJECT_SOURCE_DIR}" +) +target_link_libraries( + pmon_api_wrapper + PUBLIC + pmon::common_utilities + pmon::api_wrapper_common + PRIVATE + pmon::build_options + pmon::warnings_default +) +target_compile_options( + pmon_api_wrapper + PRIVATE + /EHa + $<$:/guard:cf> +) +target_link_options( + pmon_api_wrapper + INTERFACE + $<$:/guard:cf> +) +set_target_properties( + pmon_api_wrapper + PROPERTIES + OUTPUT_NAME PresentMonAPIWrapper + FOLDER "Service and SDK" +) diff --git a/IntelPresentMon/PresentMonAPIWrapperCommon/CMakeLists.txt b/IntelPresentMon/PresentMonAPIWrapperCommon/CMakeLists.txt new file mode 100644 index 000000000..82aaace1b --- /dev/null +++ b/IntelPresentMon/PresentMonAPIWrapperCommon/CMakeLists.txt @@ -0,0 +1,41 @@ +add_library( + pmon_api_wrapper_common + STATIC + EnumMap.cpp + Exception.cpp + Introspection.cpp + PmErrorCodeProvider.cpp +) +add_library(pmon::api_wrapper_common ALIAS pmon_api_wrapper_common) + +target_compile_definitions(pmon_api_wrapper_common PRIVATE _LIB) +target_include_directories( + pmon_api_wrapper_common + PUBLIC + "${PROJECT_SOURCE_DIR}" +) +target_link_libraries( + pmon_api_wrapper_common + PUBLIC + pmon::common_utilities + PRIVATE + pmon::build_options + pmon::warnings_default +) +target_compile_options( + pmon_api_wrapper_common + PRIVATE + /EHa + $<$:/guard:cf> +) +target_link_options( + pmon_api_wrapper_common + INTERFACE + $<$:/guard:cf> +) +set_target_properties( + pmon_api_wrapper_common + PROPERTIES + OUTPUT_NAME PresentMonAPIWrapperCommon + FOLDER "Service and SDK" +) diff --git a/IntelPresentMon/PresentMonMiddleware/CMakeLists.txt b/IntelPresentMon/PresentMonMiddleware/CMakeLists.txt new file mode 100644 index 000000000..cfc8b6319 --- /dev/null +++ b/IntelPresentMon/PresentMonMiddleware/CMakeLists.txt @@ -0,0 +1,72 @@ +find_package(boost_circular_buffer CONFIG REQUIRED) +find_package(boost_container CONFIG REQUIRED) + +add_library( + pmon_middleware + STATIC + DynamicQuery.cpp + DynamicStat.cpp + FrameEventQuery.cpp + FrameMetricsSource.cpp + LogSetup.cpp + MetricBinding.cpp + Middleware.cpp + QueryValidation.cpp +) +add_library(pmon::middleware ALIAS pmon_middleware) +# ActionClient.h includes the generated AllActions.h. INTERFACE libraries carry +# no build rule, so the ordering edge has to be stated on the real target. +add_dependencies(pmon_middleware pmon_service_actions) + +# MetricBinding.cpp expands the metric binding tables past the default COFF +# section limit. +set_source_files_properties( + MetricBinding.cpp + PROPERTIES COMPILE_OPTIONS /bigobj +) + +target_compile_definitions( + pmon_middleware + PRIVATE + _LIB + $<$:_CRTDBG_MAP_ALLOC> +) +target_include_directories( + pmon_middleware + PUBLIC + "${PROJECT_SOURCE_DIR}" +) +target_link_libraries( + pmon_middleware + PUBLIC + pmon::common_utilities + pmon::interprocess + pmon::metrics + pmon::api_wrapper_common + pmon::versioning + # Headers only: the action protocol definitions live with the service. + pmon::service_headers + Boost::circular_buffer + Boost::container + PRIVATE + pmon::build_options + pmon::warnings_default +) +target_compile_options( + pmon_middleware + PRIVATE + /EHa + /we4062 + $<$:/guard:cf> +) +target_link_options( + pmon_middleware + INTERFACE + $<$:/guard:cf> +) +set_target_properties( + pmon_middleware + PROPERTIES + OUTPUT_NAME PresentMonMiddleware + FOLDER "Service and SDK" +) diff --git a/IntelPresentMon/PresentMonService/CMakeLists.txt b/IntelPresentMon/PresentMonService/CMakeLists.txt new file mode 100644 index 000000000..bd69ab534 --- /dev/null +++ b/IntelPresentMon/PresentMonService/CMakeLists.txt @@ -0,0 +1,157 @@ +# AllActions.h is a tracked, maintained artifact that aggregates acts/*.h, so it +# is regenerated in place rather than in the build tree. The script rewrites the +# file only when the aggregated set actually changed. +file( + GLOB service_action_headers + CONFIGURE_DEPENDS + "${CMAKE_CURRENT_SOURCE_DIR}/acts/*.h" +) + +add_custom_command( + OUTPUT "${CMAKE_CURRENT_SOURCE_DIR}/AllActions.h" + COMMAND + cmd /c "${CMAKE_CURRENT_SOURCE_DIR}/build-scripts/pre-build.bat" + DEPENDS + "${CMAKE_CURRENT_SOURCE_DIR}/build-scripts/pre-build.bat" + ${service_action_headers} + WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}" + COMMENT "Generating AllActions.h" + VERBATIM +) +add_custom_target( + pmon_service_actions + DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/AllActions.h" +) +set_property(TARGET pmon_service_actions PROPERTY FOLDER "Build") + +# Header-only view of the service used by clients that speak the action +# protocol. It deliberately carries no objects so that linking a client does not +# pull service implementation code into the client binary, matching the MSBuild +# behaviour where a reference to an Application project contributes headers and +# build ordering only. +add_library(pmon_service_headers INTERFACE) +add_library(pmon::service_headers ALIAS pmon_service_headers) +add_dependencies(pmon_service_headers pmon_service_actions) +target_include_directories( + pmon_service_headers + INTERFACE + "${PROJECT_SOURCE_DIR}" + "${PROJECT_SOURCE_DIR}/IntelPresentMon" +) +set_property(TARGET pmon_service_headers PROPERTY FOLDER "Service and SDK") + +add_library( + pmon_service_core + STATIC + ActionExecutionContext.cpp + ActionServer.cpp + EtwLogger.cpp + EtwLogSession.cpp + LogSetup.cpp + MockPresentMonSession.cpp + PMMainThread.cpp + PresentMon.cpp + PresentMonSession.cpp + RealtimePresentMonSession.cpp + Service.cpp + testing/TestControl.cpp +) +add_library(pmon::service_core ALIAS pmon_service_core) +add_dependencies(pmon_service_core pmon_service_actions) + +target_link_libraries( + pmon_service_core + PUBLIC + pmon::service_headers + pmon::common_utilities + pmon::present_data + pmon::control + pmon::interprocess + pmon::api_wrapper_common + pmon::versioning + PRIVATE + pmon::build_options + pmon::warnings_default + pmon::version_resource + pdh + shlwapi + tdh +) +target_compile_options( + pmon_service_core + PRIVATE + $<$:/guard:cf> +) +target_link_options( + pmon_service_core + INTERFACE + $<$:/guard:cf> +) +set_target_properties( + pmon_service_core + PROPERTIES + OUTPUT_NAME PresentMonServiceCore + ARCHIVE_OUTPUT_DIRECTORY "${PMON_OUTPUT_ROOT}/obj/$" + FOLDER "Service and SDK" +) + +add_executable( + PresentMonService + ActionRegistration.cpp + ServiceMain.cpp + PresentMonService.rc +) +add_dependencies(PresentMonService pmon_service_actions) +target_link_libraries( + PresentMonService + PRIVATE + pmon::service_core + pmon::build_options + pmon::warnings_default + pmon::version_resource + pmon::release_link_policy +) +target_compile_options( + PresentMonService + PRIVATE + $<$:/guard:cf> +) +target_link_options( + PresentMonService + PRIVATE + $<$:/guard:cf> +) +set_target_properties( + PresentMonService + PROPERTIES + FOLDER "Service and SDK" +) + +# The service reads this manifest from its own directory at runtime. +add_custom_command( + TARGET PresentMonService + POST_BUILD + COMMAND + "${CMAKE_COMMAND}" -E copy_if_different + "${PROJECT_SOURCE_DIR}/PresentMon/ddETWExternal.xml" + "$/ddETWExternal.xml" + COMMENT "Staging ddETWExternal.xml" + VERBATIM +) + +if(PMON_UCI_ENABLED) + add_custom_command( + TARGET PresentMonService + POST_BUILD + COMMAND + "${CMAKE_COMMAND}" -E copy_if_different + "${PMON_UCI_RUNTIME_DLL}" + "$/" + COMMAND + "${CMAKE_COMMAND}" -E copy_directory + "${PMON_UCI_EFFECTIVE_ROOT}/collectors" + "$/collectors" + COMMENT "Staging UCI runtime payload" + VERBATIM + ) +endif() diff --git a/IntelPresentMon/SampleClient/CMakeLists.txt b/IntelPresentMon/SampleClient/CMakeLists.txt new file mode 100644 index 000000000..9375430b0 --- /dev/null +++ b/IntelPresentMon/SampleClient/CMakeLists.txt @@ -0,0 +1,54 @@ +find_package(boost_process CONFIG REQUIRED) + +add_executable( + SampleClient + Console.cpp + DiagnosticDemo.cpp + EtlLogger.cpp + IpcComponentServer.cpp + LogDemo.cpp + LogSetup.cpp + MultiClient.cpp + PacedFramePlayback.cpp + PacedPlayback.cpp + SampleClient.cpp + ServiceCrashClient.cpp +) + +target_link_libraries( + SampleClient + PRIVATE + pmon::build_options + pmon::warnings_default + pmon::release_link_policy + pmon::common_utilities + pmon::interprocess + pmon::api_wrapper + PresentMonAPI2Loader + Boost::process + winmm +) +target_include_directories( + SampleClient + PRIVATE + "${PROJECT_SOURCE_DIR}/IntelPresentMon" +) +target_compile_options( + SampleClient + PRIVATE + /EHa + $<$:/guard:cf> +) +target_link_options( + SampleClient + PRIVATE + /SUBSYSTEM:CONSOLE + $<$:/guard:cf> +) +set_target_properties( + SampleClient + PROPERTIES + # SampleClient is the one MultiByte target in this phase. + PMON_MULTIBYTE ON + FOLDER "Service and SDK" +) diff --git a/IntelPresentMon/SampleClient/LogSetup.cpp b/IntelPresentMon/SampleClient/LogSetup.cpp index c94da3e69..158b291d2 100644 --- a/IntelPresentMon/SampleClient/LogSetup.cpp +++ b/IntelPresentMon/SampleClient/LogSetup.cpp @@ -86,31 +86,43 @@ namespace p2sam } if (opt.logLevel) { GlobalPolicy::Get().SetLogLevel(*opt.logLevel); - getters.getGlobalPolicy().SetLogLevel(*opt.logLevel); + if (getters) { + getters.getGlobalPolicy().SetLogLevel(*opt.logLevel); + } } if (opt.logVerboseModules) { for (auto mod : *opt.logVerboseModules) { GlobalPolicy::Get().ActivateVerboseModule(mod); - getters.getGlobalPolicy().ActivateVerboseModule(mod); + if (getters) { + getters.getGlobalPolicy().ActivateVerboseModule(mod); + } } } if (opt.logTraceLevel) { GlobalPolicy::Get().SetTraceLevel(*opt.logTraceLevel); - getters.getGlobalPolicy().SetTraceLevel(*opt.logTraceLevel); + if (getters) { + getters.getGlobalPolicy().SetTraceLevel(*opt.logTraceLevel); + } } if (opt.logTraceExceptions) { GlobalPolicy::Get().SetExceptionTrace(*opt.logTraceExceptions); GlobalPolicy::Get().SetSehTracing(*opt.logTraceExceptions); - getters.getGlobalPolicy().SetExceptionTrace(*opt.logTraceExceptions); - getters.getGlobalPolicy().SetSehTracing(*opt.logTraceExceptions); + if (getters) { + getters.getGlobalPolicy().SetExceptionTrace(*opt.logTraceExceptions); + getters.getGlobalPolicy().SetSehTracing(*opt.logTraceExceptions); + } } if (opt.logDenyList) { pmquell(LineTable::IngestList(*opt.logDenyList, true)) - pmquell(getters.getLineTable().IngestList_(*opt.logDenyList, true)) + if (getters) { + pmquell(getters.getLineTable().IngestList_(*opt.logDenyList, true)) + } } else if (opt.logAllowList) { pmquell(LineTable::IngestList(*opt.logAllowList, false)) - pmquell(getters.getLineTable().IngestList_(*opt.logAllowList, false)) + if (getters) { + pmquell(getters.getLineTable().IngestList_(*opt.logAllowList, false)) + } } } catch (...) {} diff --git a/IntelPresentMon/ServiceMergeModule/ServiceMergeModule.wixproj b/IntelPresentMon/ServiceMergeModule/ServiceMergeModule.wixproj index c8b5969f1..48bc8deae 100644 --- a/IntelPresentMon/ServiceMergeModule/ServiceMergeModule.wixproj +++ b/IntelPresentMon/ServiceMergeModule/ServiceMergeModule.wixproj @@ -1,23 +1,26 @@ + Module PresentMonSharedService - $(SolutionDir)build\obj\$(MSBuildProjectName)-$(Platform)-$(Configuration) - $(SolutionDir)build\$(Configuration)\en-us\ + $(SolutionDir)build\obj\$(MSBuildProjectName)-$(Platform)-$(Configuration) + $(SolutionDir)build\$(Configuration)\en-us\ $(WIX)bin\ $(MSBuildExtensionsPath32)\Microsoft\WiX\v3.x\Wix.targets PM.ServiceDirId=service_folder;ProductVersion=$(PresentMonProductVersion) + + ICE03 {dc72c6b9-ca78-46c4-9962-e927b7a8d607} - + PMInstallerLib {6478f2f6-92ad-4b1c-a13e-c8cea9125152} @@ -28,4 +31,4 @@ - \ No newline at end of file + diff --git a/IntelPresentMon/UnitTests/CMakeLists.txt b/IntelPresentMon/UnitTests/CMakeLists.txt new file mode 100644 index 000000000..439b5707b --- /dev/null +++ b/IntelPresentMon/UnitTests/CMakeLists.txt @@ -0,0 +1,49 @@ +include(PresentMonCppUnitTest) + +add_library( + PresentMonUnitTests + SHARED + ExtremeQueue.cpp + FixedVectorTests.cpp + InterPresentActivityTests.cpp + LoggingTests.cpp + MetricCapabilitiesTests.cpp + MetricsCore.cpp + TelemetryAvailabilityMergeTests.cpp + SwapChainTests.cpp + Style.cpp + Timing.cpp + "${PROJECT_SOURCE_DIR}/IntelPresentMon/CommonUtilities/test/MachineExpectations.cpp" +) +target_include_directories( + PresentMonUnitTests + PRIVATE + "${PROJECT_SOURCE_DIR}/IntelPresentMon" +) +target_link_libraries( + PresentMonUnitTests + PRIVATE + pmon::build_options + pmon::warnings_default + pmon::release_link_policy + pmon::common_utilities + pmon::present_data + pmon::metrics + pmon::control + pmon::core + tdh +) +target_compile_options( + PresentMonUnitTests + PRIVATE + /EHa + /fp:fast +) +set_target_properties( + PresentMonUnitTests + PROPERTIES + OUTPUT_NAME PresentMonUnitTests + FOLDER "Tests" +) + +pmon_apply_cppunittest_framework(PresentMonUnitTests) diff --git a/IntelPresentMon/UnitTests/UnitTests.vcxproj b/IntelPresentMon/UnitTests/UnitTests.vcxproj index 753e744cc..d098ee85b 100644 --- a/IntelPresentMon/UnitTests/UnitTests.vcxproj +++ b/IntelPresentMon/UnitTests/UnitTests.vcxproj @@ -18,6 +18,8 @@ PresentMonUnitTests 10.0 NativeUnitTestProject + $(MSBuildProjectDirectory)\..\..\Tests\PresentMonTests.runsettings + $(MSBuildProjectDirectory)\..\..\Tests\PresentMonTests.local.runsettings diff --git a/IntelPresentMon/Versioning/CMakeLists.txt b/IntelPresentMon/Versioning/CMakeLists.txt new file mode 100644 index 000000000..d34b39c83 --- /dev/null +++ b/IntelPresentMon/Versioning/CMakeLists.txt @@ -0,0 +1,63 @@ +# BuildId.cpp includes "generated/build_id.h" relative to its own directory, so +# the header must stay in the source tree. IntelPresentMon/Versioning/generated +# is gitignored, which keeps the generated output untracked. +set(build_id_output_dir "${CMAKE_CURRENT_SOURCE_DIR}/generated") + +# The generator has its own signature-based change detection and rewrites the +# header only when the worktree actually changed, so it is safe to run on every +# build. MSBuild forces the same behaviour with a nonexistent custom-build +# output. +add_custom_target( + pmon_build_id + COMMAND + "${PMON_PYTHON_EXECUTABLE}" + "${PROJECT_SOURCE_DIR}/Scripts/versioning.py" + --repo-root "${PROJECT_SOURCE_DIR}" + --output-dir "${build_id_output_dir}" + BYPRODUCTS + "${build_id_output_dir}/build_id.h" + "${build_id_output_dir}/signature_a_prev.txt" + "${build_id_output_dir}/signature_b_prev.txt" + WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}" + COMMENT "Generating build IDs" + VERBATIM +) +set_property(TARGET pmon_build_id PROPERTY FOLDER "Build") + +add_library( + pmon_versioning + STATIC + BuildId.cpp + PresentMonAPIVersion.cpp +) +add_library(pmon::versioning ALIAS pmon_versioning) +add_dependencies(pmon_versioning pmon_build_id) + +target_compile_definitions(pmon_versioning PRIVATE _LIB) +target_include_directories( + pmon_versioning + PUBLIC + "${PROJECT_SOURCE_DIR}" +) +target_link_libraries( + pmon_versioning + PRIVATE + pmon::build_options + pmon::warnings_default +) +target_compile_options( + pmon_versioning + PRIVATE + $<$:/guard:cf> +) +target_link_options( + pmon_versioning + INTERFACE + $<$:/guard:cf> +) +set_target_properties( + pmon_versioning + PROPERTIES + OUTPUT_NAME IPMVersioning + FOLDER "Foundation" +) diff --git a/IntelPresentMon/Versioning/scripts/pre-build.ps1 b/IntelPresentMon/Versioning/scripts/pre-build.ps1 index 891c1a477..ebc824091 100644 --- a/IntelPresentMon/Versioning/scripts/pre-build.ps1 +++ b/IntelPresentMon/Versioning/scripts/pre-build.ps1 @@ -48,6 +48,24 @@ function New-BuildIdHeader { Set-Content -LiteralPath $outFile -Value $content } +function Get-FileMd5HashHex { + param([Parameter(Mandatory)][string]$Path) + $md5 = [System.Security.Cryptography.MD5]::Create() + try { + $fs = [System.IO.File]::OpenRead($Path) + try { + $bytes = $md5.ComputeHash($fs) + } + finally { + $fs.Dispose() + } + } + finally { + $md5.Dispose() + } + return ([System.BitConverter]::ToString($bytes) -replace '-', '') +} + ########################## @@ -142,9 +160,7 @@ foreach ($line in $signatureA_current_lines) { continue } - # Get MD5 hash of the file - # (You can change -Algorithm to e.g. SHA256 if desired) - $hash = (Get-FileHash $changedFile -Algorithm MD5).Hash + $hash = Get-FileMd5HashHex -Path $changedFile $signatureB_current += $hash } } diff --git a/PresentData/CMakeLists.txt b/PresentData/CMakeLists.txt new file mode 100644 index 000000000..162f69b83 --- /dev/null +++ b/PresentData/CMakeLists.txt @@ -0,0 +1,63 @@ +set(present_data_sources + Debug.cpp + GpuTrace.cpp + InterPresentActivity.cpp + NvidiaTraceConsumer.cpp + PresentMonTraceConsumer.cpp + PresentMonTraceSession.cpp + TraceConsumer.cpp + TraceLogging.cpp +) + +add_library(pmon_present_data STATIC ${present_data_sources}) +add_library(pmon::present_data ALIAS pmon_present_data) +target_include_directories( + pmon_present_data + PUBLIC + "${PROJECT_SOURCE_DIR}" +) +target_compile_definitions( + pmon_present_data + PRIVATE + _LIB + _WIN32_WINNT=0x0601 + NTDDI_VERSION=0x06010000 + WIN32_LEAN_AND_MEAN +) +target_compile_options( + pmon_present_data + PRIVATE + $<$,$>:/guard:cf> +) +target_link_options( + pmon_present_data + INTERFACE + $<$,$>:/guard:cf> +) +target_link_libraries( + pmon_present_data + PUBLIC + pmon::common_utilities + PRIVATE + pmon::build_options + pmon::warnings_strict + advapi32 + tdh +) +set_target_properties( + pmon_present_data + PROPERTIES + OUTPUT_NAME PresentData + ARCHIVE_OUTPUT_DIRECTORY + "${PMON_OUTPUT_ROOT}/lib/PresentData-${PMON_ARCHITECTURE}-$" + FOLDER "Foundation" +) +add_custom_command( + TARGET pmon_present_data + POST_BUILD + COMMAND + "${CMAKE_COMMAND}" -E copy_if_different + "$" + "${PMON_OUTPUT_ROOT}/$/PresentData.lib" + VERBATIM +) diff --git a/PresentMon.sln b/PresentMon.sln index 49289c607..32b286c83 100644 --- a/PresentMon.sln +++ b/PresentMon.sln @@ -81,11 +81,11 @@ Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "Provider", "Provider\Provid EndProject Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "Versioning", "IntelPresentMon\Versioning\Versioning.vcxproj", "{C73AA532-E532-4D93-9279-905444653C08}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Reflector", "Reflector\Reflector.csproj", "{5805A705-ADB3-4888-A244-4AAB86A1005D}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Reflector", "Tools\Reflector\Reflector.csproj", "{5805A705-ADB3-4888-A244-4AAB86A1005D}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tools", "Tools", "{AE6C0AE0-2EEF-4590-BAE8-5888B89E22C5}" EndProject -Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "ETLTrimmer", "ETLTrimmer\ETLTrimmer.vcxproj", "{F9BD47F5-4AA3-4BA3-91F2-2AC7B270299C}" +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "ETLTrimmer", "Tools\ETLTrimmer\ETLTrimmer.vcxproj", "{F9BD47F5-4AA3-4BA3-91F2-2AC7B270299C}" EndProject Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "PresentMonAPI2Loader", "IntelPresentMon\PresentMonAPI2Loader\PresentMonAPI2Loader.vcxproj", "{8F86D067-2437-46FC-8F82-4D7155CECED7}" EndProject @@ -426,6 +426,7 @@ Global {807370FB-ADE0-403A-A278-DF50893E5F94} = {61877103-313D-4140-8449-834D5D73C72E} {C73AA532-E532-4D93-9279-905444653C08} = {B4CC5828-9638-42EC-A692-E81E8227DD84} {F9BD47F5-4AA3-4BA3-91F2-2AC7B270299C} = {AE6C0AE0-2EEF-4590-BAE8-5888B89E22C5} + {5805A705-ADB3-4888-A244-4AAB86A1005D} = {AE6C0AE0-2EEF-4590-BAE8-5888B89E22C5} {8F86D067-2437-46FC-8F82-4D7155CECED7} = {6DCB803B-9FCE-456C-87B9-1365F59BD190} {6478F2F6-92AD-4B1C-A13E-C8CEA9125152} = {61877103-313D-4140-8449-834D5D73C72E} {DC72C6B9-CA78-46C4-9962-E927B7A8D607} = {61877103-313D-4140-8449-834D5D73C72E} diff --git a/PresentMon/CMakeLists.txt b/PresentMon/CMakeLists.txt new file mode 100644 index 000000000..cda8df08f --- /dev/null +++ b/PresentMon/CMakeLists.txt @@ -0,0 +1,61 @@ +pmon_compile_message_manifest( + presentmon_manifest_resource + "${CMAKE_CURRENT_SOURCE_DIR}/ddETWExternal.xml" + "ddETWExternalEvents" +) + +add_executable( + PresentMonConsole + CommandLine.cpp + Console.cpp + ConsumerThread.cpp + CsvOutput.cpp + LogSetup.cpp + MainThread.cpp + OutputThread.cpp + Privilege.cpp + "${presentmon_manifest_resource}" +) +target_compile_definitions( + PresentMonConsole + PRIVATE + _WIN32_WINNT=0x0601 + NTDDI_VERSION=0x06010000 + WIN32_LEAN_AND_MEAN + _CONSOLE +) +target_compile_options( + PresentMonConsole + PRIVATE + $<$:/guard:cf> +) +target_link_options( + PresentMonConsole + PRIVATE + /DELAYLOAD:advapi32.dll + /DELAYLOAD:shell32.dll + /DELAYLOAD:tdh.dll + /DELAYLOAD:user32.dll + $<$:/guard:cf> +) +target_link_libraries( + PresentMonConsole + PRIVATE + pmon::build_options + pmon::warnings_strict + pmon::warnings_strict_link + pmon::release_link_policy + pmon::version_header + pmon::metrics + advapi32 + delayimp + shell32 + tdh + user32 +) +set_target_properties( + PresentMonConsole + PROPERTIES + OUTPUT_NAME "PresentMon-${PMON_VERSION}-${PMON_ARCH_TAG}" + FOLDER "PresentMon/Console" +) diff --git a/Provider/CMakeLists.txt b/Provider/CMakeLists.txt new file mode 100644 index 000000000..a3807d25b --- /dev/null +++ b/Provider/CMakeLists.txt @@ -0,0 +1,51 @@ +pmon_compile_message_manifest( + provider_manifest_resource + "${CMAKE_CURRENT_SOURCE_DIR}/Intel-PresentMon.man" + "Intel-PresentMon" +) + +add_library( + pmon_provider + SHARED + dllmain.cpp + Version.rc + "${provider_manifest_resource}" +) +target_link_libraries( + pmon_provider + PRIVATE + pmon::build_options + pmon::warnings_strict + pmon::warnings_strict_link + pmon::release_link_policy + pmon::version_resource +) +target_compile_definitions( + pmon_provider + PRIVATE + _WINDOWS + _USRDLL + $<$:ISX86> +) +target_compile_options( + pmon_provider + PRIVATE + $<$,$>:/guard:cf> +) +target_link_options( + pmon_provider + PRIVATE + $<$,$>:/guard:cf> +) + +if(PMON_ARCHITECTURE STREQUAL "Win32") + set(provider_output_name "Intel-PresentMon32") +else() + set(provider_output_name "Intel-PresentMon") +endif() +set_target_properties( + pmon_provider + PROPERTIES + OUTPUT_NAME "${provider_output_name}" + FOLDER "PresentMon/Provider" +) diff --git a/Reflector/Properties/launchSettings.json b/Reflector/Properties/launchSettings.json deleted file mode 100644 index 1c12ad45e..000000000 --- a/Reflector/Properties/launchSettings.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "profiles": { - "Reflector": { - "commandName": "Project", - "workingDirectory": "C:\\Users\\Chili\\Desktop\\cpp\\ipm-relay" - } - } -} \ No newline at end of file diff --git a/Scripts/auxdata.py b/Scripts/auxdata.py new file mode 100644 index 000000000..b4c0039ff --- /dev/null +++ b/Scripts/auxdata.py @@ -0,0 +1,49 @@ +from __future__ import annotations + +import argparse +import sys +from pathlib import Path + +from pmon import auxdata +from pmon.common import BuildError, ensure_python_version + + +def parse_arguments() -> argparse.Namespace: + parser = argparse.ArgumentParser(description="Restore or validate PresentMon auxiliary test data.") + parser.add_argument("operation", choices=("ensure", "restore", "verify")) + parser.add_argument( + "--repo-root", + type=Path, + default=Path(__file__).resolve().parents[1], + ) + parser.add_argument("--dependency-root", type=Path) + return parser.parse_args() + + +def main() -> int: + try: + ensure_python_version() + arguments = parse_arguments() + repo_root = arguments.repo_root.resolve() + dependency_root = ( + arguments.dependency_root.resolve() + if arguments.dependency_root + else repo_root / "build" / "ThirdParty" + ) + if arguments.operation == "ensure": + auxdata.ensure(repo_root, dependency_root) + elif arguments.operation == "restore": + auxdata.ensure(repo_root, dependency_root, force=True) + else: + errors = auxdata.validate(repo_root) + if errors: + raise BuildError("Auxiliary data validation failed:\n" + "\n".join(errors)) + print("Pinned auxiliary test data is present and valid.") + return 0 + except BuildError as error: + print(f"error: {error}", file=sys.stderr) + return 1 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/Scripts/bootstrap.py b/Scripts/bootstrap.py new file mode 100644 index 000000000..99a8b5a2d --- /dev/null +++ b/Scripts/bootstrap.py @@ -0,0 +1,72 @@ +from __future__ import annotations + +import argparse +import sys +from pathlib import Path + +from pmon import auxdata, cef, web +from pmon.common import BuildError, ensure_python_version, log + + +def parse_arguments() -> argparse.Namespace: + parser = argparse.ArgumentParser(description="Prepare PresentMon build dependencies.") + parser.add_argument("--repo-root", type=Path, required=True) + parser.add_argument("--dependency-root", type=Path, required=True) + parser.add_argument("--architecture", choices=("x64", "Win32"), required=True) + parser.add_argument("--with-cef", action="store_true") + parser.add_argument("--with-auxdata", action="store_true") + parser.add_argument("--with-web", action="store_true") + parser.add_argument("--generator") + parser.add_argument("--platform") + parser.add_argument("--toolset") + parser.add_argument("--generator-instance") + parser.add_argument("--cef-source") + arguments = parser.parse_args() + # Only the CEF stage compiles wrapper libraries, so only it needs the full + # Visual Studio selection. The other dependencies are toolset-independent. + if arguments.with_cef: + missing = [ + "--" + name.replace("_", "-") + for name in ("generator", "platform", "toolset", "generator_instance") + if not getattr(arguments, name) + ] + if missing: + parser.error(f"--with-cef requires {', '.join(missing)}.") + return arguments + + +def main() -> int: + try: + ensure_python_version() + arguments = parse_arguments() + repo_root = arguments.repo_root.resolve() + dependency_root = arguments.dependency_root.resolve() + if not (repo_root / "CMakeLists.txt").is_file(): + raise BuildError(f"Repository root does not contain CMakeLists.txt: {repo_root}") + dependency_root.mkdir(parents=True, exist_ok=True) + + log(f"Python: {sys.executable}") + log(f"Shared dependency root: {dependency_root}") + log(f"Target architecture: {arguments.architecture}") + if arguments.with_cef: + cef.ensure( + repo_root, + dependency_root, + arguments.generator, + arguments.platform, + arguments.toolset, + arguments.generator_instance, + arguments.cef_source, + ) + if arguments.with_auxdata: + auxdata.ensure(repo_root, dependency_root) + if arguments.with_web: + web.ensure(repo_root, dependency_root) + return 0 + except BuildError as error: + print(f"error: {error}", file=sys.stderr) + return 1 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/Scripts/cef.py b/Scripts/cef.py new file mode 100644 index 000000000..4881a9f36 --- /dev/null +++ b/Scripts/cef.py @@ -0,0 +1,88 @@ +from __future__ import annotations + +import argparse +import sys +from pathlib import Path + +from pmon import cef +from pmon.common import BuildError, ensure_python_version + + +# Each operation names the selections it needs. Compiling the wrapper libraries +# needs the full Visual Studio selection; validating a stage needs only the +# toolset that qualifies it; validating the published runtime payload needs +# neither, because that payload is toolset-independent. +REQUIRED_ARGUMENTS = { + "ensure": ("generator", "platform", "toolset", "generator_instance"), + "restore": ("generator", "platform", "toolset", "generator_instance"), + "verify-stage": ("toolset",), + "verify-output": ("output_root",), +} + + +def parse_arguments() -> argparse.Namespace: + parser = argparse.ArgumentParser(description="Restore or validate PresentMon CEF inputs.") + parser.add_argument("operation", choices=tuple(REQUIRED_ARGUMENTS)) + parser.add_argument("--repo-root", type=Path, required=True) + parser.add_argument("--dependency-root", type=Path, required=True) + parser.add_argument("--output-root", type=Path) + parser.add_argument("--generator") + parser.add_argument("--platform") + parser.add_argument("--toolset") + parser.add_argument("--generator-instance") + parser.add_argument("--source") + arguments = parser.parse_args() + missing = [ + "--" + name.replace("_", "-") + for name in REQUIRED_ARGUMENTS[arguments.operation] + if not getattr(arguments, name) + ] + if missing: + parser.error(f"{arguments.operation} requires {', '.join(missing)}.") + return arguments + + +def main() -> int: + try: + ensure_python_version() + arguments = parse_arguments() + repo_root = arguments.repo_root.resolve() + dependency_root = arguments.dependency_root.resolve() + if arguments.operation == "ensure": + cef.ensure( + repo_root, + dependency_root, + arguments.generator, + arguments.platform, + arguments.toolset, + arguments.generator_instance, + arguments.source, + ) + elif arguments.operation == "restore": + cef.restore( + repo_root, + dependency_root, + arguments.generator, + arguments.platform, + arguments.toolset, + arguments.generator_instance, + arguments.source, + ) + elif arguments.operation == "verify-stage": + errors = cef.validate_stage(repo_root, dependency_root, arguments.toolset) + if errors: + raise BuildError("CEF stage validation failed:\n" + "\n".join(errors)) + print("CEF stage matches the lock and contains all build inputs.") + else: + errors = cef.validate_output(repo_root, arguments.output_root.resolve()) + if errors: + raise BuildError("CEF output validation failed:\n" + "\n".join(errors)) + print("CEF output payload matches the lock.") + return 0 + except BuildError as error: + print(f"error: {error}", file=sys.stderr) + return 1 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/Scripts/discover_tests.py b/Scripts/discover_tests.py new file mode 100644 index 000000000..5edbf3175 --- /dev/null +++ b/Scripts/discover_tests.py @@ -0,0 +1,93 @@ +from __future__ import annotations + +import argparse +import sys +from pathlib import Path + +from pmon.common import BuildError, ensure_python_version +from pmon.discovery import generate_gtest_ctest, generate_vstest_ctest + + +def parse_arguments() -> argparse.Namespace: + parser = argparse.ArgumentParser(description="Generate CTest entries from built test binaries.") + subparsers = parser.add_subparsers(dest="operation", required=True) + + gtest = subparsers.add_parser("gtest") + gtest.add_argument("--test-executable", type=Path, required=True) + gtest.add_argument("--output-file", type=Path, required=True) + gtest.add_argument("--test-prefix", required=True) + gtest.add_argument("--test-filter", required=True) + gtest.add_argument("--presentmon-path", type=Path, required=True) + gtest.add_argument("--gold-directory", type=Path, required=True) + gtest.add_argument("--optional-test-directory", default="") + gtest.add_argument("--working-directory", type=Path, required=True) + gtest.add_argument("--labels", default="") + gtest.add_argument("--fixtures-required", default="") + gtest.add_argument("--resource-lock", default="") + gtest.add_argument( + "--allow-empty", + action="store_true", + help="Register no tests instead of failing when the group discovers none. " + "For optional data-driven groups whose data directory may be absent.", + ) + + vstest = subparsers.add_parser("vstest") + vstest.add_argument("--vstest-console", type=Path, required=True) + vstest.add_argument("--test-container", type=Path, required=True) + vstest.add_argument("--output-file", type=Path, required=True) + vstest.add_argument("--test-prefix", required=True) + vstest.add_argument("--working-directory", type=Path, required=True) + vstest.add_argument("--base-labels", default="") + vstest.add_argument("--default-labels", default="") + vstest.add_argument("--environment-labels", default="") + vstest.add_argument("--environment-name-regex", default="") + vstest.add_argument("--test-environment", default="") + vstest.add_argument("--fixtures-required", default="") + vstest.add_argument("--resource-lock", default="") + return parser.parse_args() + + +def main() -> int: + try: + ensure_python_version() + arguments = parse_arguments() + if arguments.operation == "gtest": + generate_gtest_ctest( + arguments.test_executable.resolve(), + arguments.output_file.resolve(), + arguments.test_prefix, + arguments.test_filter, + arguments.presentmon_path.resolve(), + arguments.gold_directory.resolve(), + Path(arguments.optional_test_directory).resolve() + if arguments.optional_test_directory + else None, + arguments.working_directory.resolve(), + arguments.labels, + arguments.fixtures_required, + arguments.resource_lock, + arguments.allow_empty, + ) + else: + generate_vstest_ctest( + arguments.vstest_console.resolve(), + arguments.test_container.resolve(), + arguments.output_file.resolve(), + arguments.test_prefix, + arguments.working_directory.resolve(), + arguments.base_labels, + arguments.default_labels, + arguments.environment_labels, + arguments.environment_name_regex, + arguments.test_environment, + arguments.fixtures_required, + arguments.resource_lock, + ) + return 0 + except BuildError as error: + print(f"error: {error}", file=sys.stderr) + return 1 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/Scripts/generate_etl_csv_tests.py b/Scripts/generate_etl_csv_tests.py new file mode 100644 index 000000000..c5532d6d0 --- /dev/null +++ b/Scripts/generate_etl_csv_tests.py @@ -0,0 +1,71 @@ +from __future__ import annotations + +import argparse +import csv +import re +import sys +from pathlib import Path + +from pmon.common import BuildError, ensure_python_version, write_text_atomic + + +def parse_arguments() -> argparse.Namespace: + parser = argparse.ArgumentParser(description="Generate API2 ETL CSV test methods.") + parser.add_argument("--input-csv", type=Path, required=True) + parser.add_argument("--output-header", type=Path, required=True) + return parser.parse_args() + + +def method_name(name: str, ordinal: int) -> str: + sanitized = re.sub(r"[^A-Za-z0-9_]", "_", name) + if not sanitized.strip(): + sanitized = "Unnamed" + return f"EtlCsv_{ordinal:03d}_{sanitized}" + + +def is_true(value: str) -> bool: + return value.strip().lower() in ("true", "1", "yes", "y") + + +def generate(input_csv: Path, output_header: Path) -> None: + if not input_csv.is_file(): + raise BuildError(f"API2 test case CSV was not found: {input_csv}") + with input_csv.open("r", encoding="utf-8-sig", newline="") as stream: + rows = list(csv.DictReader(stream)) + lines = [ + "// Copyright (C) 2022-2023 Intel Corporation", + "// SPDX-License-Identifier: MIT", + "//", + "// Generated by generate_etl_csv_tests.py from test_cases.csv.", + "// Do not edit this file directly.", + "", + ] + for row_index, row in enumerate(rows): + if is_true(row.get("IsExpectedFailure", "")): + continue + name = method_name(row.get("TestName", ""), row_index + 1) + lines.extend( + ( + f"TEST_METHOD({name})", + "{", + f"\tRunTestCaseFromCsvRow(testCasesCsvPath_, {row_index});", + "}", + "", + ) + ) + write_text_atomic(output_header, "\r\n".join(lines) + "\r\n") + + +def main() -> int: + try: + ensure_python_version() + arguments = parse_arguments() + generate(arguments.input_csv.resolve(), arguments.output_header.resolve()) + return 0 + except (BuildError, csv.Error) as error: + print(f"error: {error}", file=sys.stderr) + return 1 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/Scripts/package.py b/Scripts/package.py new file mode 100644 index 000000000..099b3dfff --- /dev/null +++ b/Scripts/package.py @@ -0,0 +1,180 @@ +from __future__ import annotations + +import argparse +import sys +import xml.etree.ElementTree as ElementTree +from pathlib import Path + +from pmon import packaging +from pmon.common import BuildError, ensure_python_version + + +def _optional_path(value: str) -> Path | None: + return Path(value).resolve() if value else None + + +def parse_arguments() -> argparse.Namespace: + parser = argparse.ArgumentParser(description="Prepare and protect PresentMon package inputs.") + subparsers = parser.add_subparsers(dest="operation", required=True) + + stage_uci = subparsers.add_parser("stage-uci") + stage_uci.add_argument("--enabled", required=True) + stage_uci.add_argument("--sdk-root", default="") + stage_uci.add_argument("--stage-dir", type=Path, required=True) + stage_uci.add_argument("--manifest-path", type=Path, required=True) + + generate_uci = subparsers.add_parser("generate-uci") + generate_uci.add_argument("--enabled", required=True) + generate_uci.add_argument("--stage-dir", type=Path, required=True) + generate_uci.add_argument("--output-path", type=Path, required=True) + generate_uci.add_argument("--wix-root", type=Path, required=True) + + + snapshot = subparsers.add_parser("native-snapshot") + snapshot.add_argument("mode", choices=("record", "verify")) + snapshot.add_argument("--snapshot-path", type=Path, required=True) + snapshot.add_argument("--native-paths-list", required=True) + + production = subparsers.add_parser("verify-production-inputs") + production.add_argument("--signtool", type=Path, required=True) + production.add_argument("--native-paths-list", required=True) + production.add_argument("--uci-enabled", required=True) + production.add_argument("--uci-stage-dir", default="") + production.add_argument("--uci-manifest-path", default="") + production.add_argument("--skip-uci", action="store_true") + + link = subparsers.add_parser("verify-link-preflight") + link.add_argument("--repo-root", type=Path, required=True) + link.add_argument("--dependency-root", type=Path, required=True) + link.add_argument("--toolset", required=True) + link.add_argument("--payload-root", type=Path, required=True) + link.add_argument("--deployment-profile", choices=("DEVELOPER", "PRODUCTION"), required=True) + link.add_argument("--signtool", type=Path, required=True) + link.add_argument("--native-paths-list", required=True) + link.add_argument("--uci-enabled", required=True) + link.add_argument("--uci-stage-dir", default="") + link.add_argument("--uci-manifest-path", default="") + link.add_argument("--skip-production-pe", action="store_true") + + require_signed = subparsers.add_parser("require-signed-payload") + require_signed.add_argument("--stamp", type=Path, required=True) + require_signed.add_argument("--signtool", type=Path, required=True) + require_signed.add_argument("--native-paths-list", required=True) + require_signed.add_argument("--uci-enabled", required=True) + require_signed.add_argument("--uci-stage-dir", default="") + require_signed.add_argument("--uci-manifest-path", default="") + + verify_msi = subparsers.add_parser("verify-msi") + verify_msi.add_argument("--msi-path", type=Path, required=True) + verify_msi.add_argument("--payload-root", type=Path, required=True) + verify_msi.add_argument("--repo-root", type=Path, required=True) + verify_msi.add_argument("--signtool", type=Path, required=True) + verify_msi.add_argument("--native-paths-list", required=True) + verify_msi.add_argument("--snapshot-path", type=Path, required=True) + verify_msi.add_argument("--skip-msi-authenticode", action="store_true") + + sign = subparsers.add_parser("sign-installer") + sign.add_argument("--msi-path", type=Path, required=True) + sign.add_argument("--payload-root", type=Path, required=True) + sign.add_argument("--repo-root", type=Path, required=True) + sign.add_argument("--signtool", type=Path, required=True) + sign.add_argument("--edss-script", type=Path, required=True) + sign.add_argument("--native-paths-list", required=True) + sign.add_argument("--snapshot-path", type=Path, required=True) + sign.add_argument("--in-process-signing", action="store_true") + sign.add_argument("--impersonated-signing", action="store_true") + sign.add_argument("--skip-edss", action="store_true") + + return parser.parse_args() + + +def main() -> int: + try: + ensure_python_version() + arguments = parse_arguments() + operation = arguments.operation + if operation == "stage-uci": + packaging.stage_uci_runtime( + arguments.enabled, + _optional_path(arguments.sdk_root), + arguments.stage_dir.resolve(), + arguments.manifest_path.resolve(), + ) + elif operation == "generate-uci": + packaging.generate_uci_fragment( + arguments.enabled, + arguments.stage_dir.resolve(), + arguments.output_path.resolve(), + arguments.wix_root.resolve(), + ) + elif operation == "native-snapshot": + packaging.native_snapshot( + arguments.mode, + arguments.snapshot_path.resolve(), + arguments.native_paths_list, + ) + elif operation == "verify-production-inputs": + packaging.verify_production_inputs( + arguments.signtool.resolve(), + arguments.native_paths_list, + arguments.uci_enabled, + _optional_path(arguments.uci_stage_dir), + _optional_path(arguments.uci_manifest_path), + arguments.skip_uci, + ) + elif operation == "verify-link-preflight": + packaging.verify_link_preflight( + arguments.repo_root.resolve(), + arguments.dependency_root.resolve(), + arguments.toolset, + arguments.payload_root.resolve(), + arguments.deployment_profile, + arguments.signtool.resolve(), + arguments.native_paths_list, + arguments.uci_enabled, + _optional_path(arguments.uci_stage_dir), + _optional_path(arguments.uci_manifest_path), + arguments.skip_production_pe, + ) + elif operation == "require-signed-payload": + packaging.require_signed_payload( + arguments.stamp.resolve(), + arguments.signtool.resolve(), + arguments.native_paths_list, + arguments.uci_enabled, + _optional_path(arguments.uci_stage_dir), + _optional_path(arguments.uci_manifest_path), + ) + elif operation == "verify-msi": + packaging.verify_msi_payload( + arguments.msi_path.resolve(), + arguments.payload_root.resolve(), + arguments.repo_root.resolve(), + arguments.signtool.resolve(), + arguments.native_paths_list, + arguments.snapshot_path.resolve(), + arguments.skip_msi_authenticode, + ) + elif operation == "sign-installer": + if arguments.in_process_signing and arguments.impersonated_signing: + raise BuildError("Specify at most one of --in-process-signing and --impersonated-signing.") + in_process = arguments.in_process_signing or not arguments.impersonated_signing + packaging.sign_production_installer( + arguments.msi_path.resolve(), + arguments.payload_root.resolve(), + arguments.repo_root.resolve(), + arguments.signtool.resolve(), + arguments.edss_script.resolve(), + arguments.native_paths_list, + arguments.snapshot_path.resolve(), + in_process, + arguments.skip_edss, + ) + return 0 + except (BuildError, OSError, ElementTree.ParseError) as error: + print(f"error: {error}", file=sys.stderr) + return 1 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/Scripts/pmon/__init__.py b/Scripts/pmon/__init__.py new file mode 100644 index 000000000..1d9c6ba46 --- /dev/null +++ b/Scripts/pmon/__init__.py @@ -0,0 +1 @@ +"""PresentMon build orchestration helpers.""" diff --git a/Scripts/pmon/auxdata.py b/Scripts/pmon/auxdata.py new file mode 100644 index 000000000..6dfe99f7b --- /dev/null +++ b/Scripts/pmon/auxdata.py @@ -0,0 +1,227 @@ +from __future__ import annotations + +import json +import shutil +import tarfile +import urllib.request +from pathlib import Path + +from .common import BuildError, download, file_lock, log, read_json, run, sha256_file + + +OWNER = "planetchili" +REPOSITORY = "IPMAuxTestData" + + +def _git_output(arguments: list[str], cwd: Path) -> str: + result = run(["git", *arguments], cwd=cwd, capture_output=True) + return result.stdout.strip() + + +def _load_lock(repo_root: Path) -> str: + lock_path = repo_root / "Tests" / "aux-data.lock.json" + lock = read_json(lock_path) + if not isinstance(lock, dict): + raise BuildError(f"Auxiliary data lock is not an object: {lock_path}") + pinned_commit = str(lock.get("pinnedCommitHash", "")).strip() + if not pinned_commit: + raise BuildError(f"Auxiliary data lock has no pinnedCommitHash: {lock_path}") + return pinned_commit + + +def _load_manifest_from_commit(aux_root: Path, pinned_commit: str) -> dict[str, object]: + text = _git_output(["show", f"{pinned_commit}:etl-manifest.json"], aux_root) + try: + manifest = json.loads(text) + except json.JSONDecodeError as error: + raise BuildError(f"Invalid auxiliary data manifest at {pinned_commit}: {error}") from error + if not isinstance(manifest, dict): + raise BuildError("Auxiliary data manifest is not an object.") + return manifest + + +def validate(repo_root: Path) -> list[str]: + pinned_commit = _load_lock(repo_root) + aux_root = repo_root / "Tests" / "AuxData" + errors: list[str] = [] + if not (aux_root / ".git").is_dir(): + return [f"Auxiliary data repository is missing: {aux_root}"] + + try: + current_commit = _git_output(["rev-parse", "HEAD"], aux_root) + except BuildError as error: + return [str(error)] + if current_commit.lower() != pinned_commit.lower(): + errors.append( + f"Auxiliary data commit mismatch; expected {pinned_commit}, found {current_commit}." + ) + return errors + + try: + manifest = _load_manifest_from_commit(aux_root, pinned_commit) + except BuildError as error: + return [str(error)] + entries = manifest.get("etlFiles") + if not isinstance(entries, list) or not entries: + return ["Auxiliary data manifest has no etlFiles entries."] + + data_root = aux_root / "Data" + for entry in entries: + if not isinstance(entry, dict): + errors.append("Auxiliary data manifest contains a malformed entry.") + continue + name = str(entry.get("name", "")).strip() + expected_hash = str(entry.get("hash", "")).strip().lower() + if not name or not expected_hash: + errors.append("Auxiliary data manifest entry is missing name or hash.") + continue + path = data_root / name + if not path.is_file(): + errors.append(f"Missing auxiliary test file: {name}") + elif sha256_file(path) != expected_hash: + errors.append(f"Hash mismatch for auxiliary test file: {name}") + return errors + + +def _release(tag: str) -> dict[str, object]: + uri = f"https://api.github.com/repos/{OWNER}/{REPOSITORY}/releases/tags/{tag}" + request = urllib.request.Request( + uri, + headers={ + "User-Agent": "PresentMon-auxdata", + "Accept": "application/vnd.github+json", + }, + ) + try: + with urllib.request.urlopen(request) as response: + value = json.load(response) + except Exception as error: + raise BuildError(f"Failed to read auxiliary data release {tag}: {error}") from error + if not isinstance(value, dict): + raise BuildError(f"Unexpected GitHub release response for tag {tag}.") + return value + + +def _extract_single_file(archive: Path, data_root: Path, expected_name: str) -> None: + try: + with tarfile.open(archive, "r:gz") as bundle: + members = [member for member in bundle.getmembers() if member.isfile()] + matching = [member for member in members if Path(member.name).name == expected_name] + if len(matching) != 1: + raise BuildError( + f"Archive {archive.name} does not contain exactly one {expected_name} file." + ) + member = matching[0] + source = bundle.extractfile(member) + if source is None: + raise BuildError(f"Failed to read {expected_name} from {archive.name}.") + destination = data_root / expected_name + temporary = destination.with_suffix(f"{destination.suffix}.incoming") + with source, temporary.open("wb") as output: + shutil.copyfileobj(source, output) + temporary.replace(destination) + except (OSError, tarfile.TarError) as error: + raise BuildError(f"Failed to extract {archive}: {error}") from error + + +def restore(repo_root: Path, dependency_root: Path) -> None: + pinned_commit = _load_lock(repo_root) + aux_root = repo_root / "Tests" / "AuxData" + remote_uri = f"https://github.com/{OWNER}/{REPOSITORY}.git" + if not aux_root.exists(): + log(f"Cloning auxiliary data repository into {aux_root}") + run(["git", "clone", remote_uri, aux_root]) + if not (aux_root / ".git").is_dir(): + raise BuildError(f"Auxiliary data path is not a Git repository: {aux_root}") + + current_remote = _git_output(["remote", "get-url", "origin"], aux_root) + if current_remote.startswith("git@github.com:"): + run(["git", "remote", "set-url", "origin", remote_uri], cwd=aux_root) + run(["git", "remote", "set-url", "--push", "origin", remote_uri], cwd=aux_root) + + log("Fetching the pinned auxiliary data commit.") + run(["git", "fetch", "origin", "--tags"], cwd=aux_root) + run(["git", "checkout", "--detach", pinned_commit], cwd=aux_root) + manifest = _load_manifest_from_commit(aux_root, pinned_commit) + entries = manifest.get("etlFiles") + if not isinstance(entries, list) or not entries: + raise BuildError("Auxiliary data manifest has no etlFiles entries.") + + data_root = aux_root / "Data" + data_root.mkdir(parents=True, exist_ok=True) + cache_root = dependency_root / "downloads" / "auxdata" + releases: dict[str, dict[str, object]] = {} + for entry in entries: + if not isinstance(entry, dict): + raise BuildError("Auxiliary data manifest contains a malformed entry.") + name = str(entry.get("name", "")).strip() + expected_hash = str(entry.get("hash", "")).strip().lower() + tag = str(entry.get("releaseTag", "")).strip() + if not name or not expected_hash or not tag: + raise BuildError("Auxiliary data manifest entry is missing name, hash, or releaseTag.") + destination = data_root / name + if destination.is_file() and sha256_file(destination) == expected_hash: + continue + + if tag not in releases: + releases[tag] = _release(tag) + release = releases[tag] + assets = release.get("assets") + if not isinstance(assets, list): + raise BuildError(f"Auxiliary data release {tag} has no assets.") + archive_name = f"{name}.tar.gz" + asset = next( + ( + item + for item in assets + if isinstance(item, dict) and str(item.get("name", "")).lower() == archive_name.lower() + ), + None, + ) + if asset is None: + raise BuildError(f"Asset {archive_name} was not found in release {tag}.") + uri = str(asset.get("browser_download_url", "")) + if not uri: + raise BuildError(f"Asset {archive_name} has no download URL.") + archive = cache_root / tag / archive_name + for attempt in range(2): + if not archive.is_file(): + temporary = archive.with_suffix(f"{archive.suffix}.incoming") + log(f"Downloading auxiliary test data {archive_name}.") + download(uri, temporary) + temporary.replace(archive) + try: + _extract_single_file(archive, data_root, name) + except BuildError: + archive.unlink(missing_ok=True) + if attempt == 0: + log(f"Cached auxiliary archive is invalid; downloading {archive_name} again.") + continue + raise + actual_hash = sha256_file(destination) + if actual_hash == expected_hash: + break + archive.unlink(missing_ok=True) + destination.unlink(missing_ok=True) + if attempt == 0: + log(f"Cached auxiliary payload hash is invalid; downloading {archive_name} again.") + continue + raise BuildError( + f"Hash mismatch after restoring {name}; expected {expected_hash}, found {actual_hash}." + ) + + +def ensure(repo_root: Path, dependency_root: Path, force: bool = False) -> None: + lock_path = dependency_root / "locks" / "auxdata.lock" + with file_lock(lock_path, timeout_seconds=1800): + errors = validate(repo_root) + if not force and not errors: + log("Pinned auxiliary test data is present and valid.") + return + if errors: + log("Auxiliary test data is missing, stale, or invalid; restoring it.") + restore(repo_root, dependency_root) + errors = validate(repo_root) + if errors: + raise BuildError("Auxiliary data restore did not validate:\n" + "\n".join(errors)) + log("Pinned auxiliary test data was restored and validated.") diff --git a/Scripts/pmon/cef.py b/Scripts/pmon/cef.py new file mode 100644 index 000000000..086864a4e --- /dev/null +++ b/Scripts/pmon/cef.py @@ -0,0 +1,497 @@ +from __future__ import annotations + +import json +import os +import re +import shutil +import tempfile +import urllib.parse +from pathlib import Path + +from .common import ( + BuildError, + download, + file_lock, + log, + read_json, + replace_directory, + require_relative_path, + run, + sha256_file, +) + + +BUILD_INPUTS = ( + "Include/include/base/cef_callback.h", + "Include/include/cef_app.h", + "Include/include/cef_client.h", + "Include/include/cef_parser.h", + "Include/include/cef_scheme.h", + "Include/include/cef_task.h", + "Include/include/cef_v8.h", + "Include/include/cef_version.h", + "Include/include/internal/cef_types.h", + "Include/include/wrapper/cef_closure_task.h", + "Include/include/wrapper/cef_helpers.h", + "Lib/Debug/libcef.lib", + "Lib/Debug/libcef_dll_wrapper.lib", + "Lib/Release/libcef.lib", + "Lib/Release/libcef_dll_wrapper.lib", +) + +VERSION_NAMES = ( + "CEF_VERSION", + "CEF_VERSION_MAJOR", + "CEF_VERSION_MINOR", + "CEF_VERSION_PATCH", + "CEF_COMMIT_NUMBER", + "CEF_COMMIT_HASH", + "CHROME_VERSION_MAJOR", + "CHROME_VERSION_MINOR", + "CHROME_VERSION_BUILD", + "CHROME_VERSION_PATCH", +) + + +def _lock_path(repo_root: Path) -> Path: + return repo_root / "IntelPresentMon" / "AppCef" / "cef-lock.json" + + +def _load_lock(repo_root: Path) -> dict[str, object]: + path = _lock_path(repo_root) + value = read_json(path) + if not isinstance(value, dict): + raise BuildError(f"CEF lock is not an object: {path}") + if value.get("schemaVersion") != 1: + raise BuildError(f"Unsupported CEF lock schema in {path}") + payload = value.get("payload") + if not isinstance(payload, list) or not payload: + raise BuildError(f"CEF lock payload is empty: {path}") + return value + + +# Compiled CEF stages are qualified by platform toolset family so that a v143 +# and a v145 build tree never share wrapper libraries. The toolset becomes a +# single path component, and an empty or compound value would silently resolve +# to the shared parent, so the family form is required here. +def _require_toolset(toolset: str) -> str: + value = (toolset or "").strip() + if not re.fullmatch(r"v[0-9]+", value): + raise BuildError( + "CEF operations require a platform toolset family such as v143 or " + f"v145; received: {toolset!r}" + ) + return value + + +def _stage_root(dependency_root: Path, toolset: str) -> Path: + return dependency_root / "cef" / _require_toolset(toolset) + + +def _version_metadata(stage_root: Path) -> dict[str, object]: + version_path = stage_root / "Include" / "include" / "cef_version.h" + if not version_path.is_file(): + version_path = stage_root / "include" / "cef_version.h" + if not version_path.is_file(): + raise BuildError(f"CEF version header was not found beneath {stage_root}") + text = version_path.read_text(encoding="utf-8", errors="replace") + metadata: dict[str, object] = {} + for name in VERSION_NAMES: + match = re.search(rf"^#define\s+{re.escape(name)}\s+(.+?)\s*$", text, re.MULTILINE) + if not match: + continue + value = match.group(1).strip().strip('"') + try: + metadata[name] = int(value) + except ValueError: + metadata[name] = value + if "CEF_VERSION" not in metadata: + raise BuildError(f"CEF_VERSION was not found in {version_path}") + return metadata + + +def _validate_build_inputs(stage_root: Path) -> list[str]: + errors: list[str] = [] + for relative in BUILD_INPUTS: + path = stage_root / Path(relative) + if not path.is_file(): + errors.append(f"Missing CEF build input: {relative}") + elif path.stat().st_size == 0: + errors.append(f"Empty CEF build input: {relative}") + return errors + + +def validate_stage(repo_root: Path, dependency_root: Path, toolset: str) -> list[str]: + lock = _load_lock(repo_root) + stage_root = _stage_root(dependency_root, toolset) + if not stage_root.is_dir(): + return [f"CEF stage directory is missing: {stage_root}"] + + errors: list[str] = [] + try: + actual_metadata = _version_metadata(stage_root) + except BuildError as error: + errors.append(str(error)) + actual_metadata = {} + expected_metadata = lock.get("cef") + if not isinstance(expected_metadata, dict): + errors.append("CEF lock metadata is missing.") + else: + for name, expected in expected_metadata.items(): + actual = actual_metadata.get(name) + if str(actual) != str(expected): + errors.append( + f"CEF metadata mismatch for {name}; expected {expected}, found {actual}." + ) + + expected_paths: set[str] = set() + payload = lock["payload"] + assert isinstance(payload, list) + for raw_entry in payload: + if not isinstance(raw_entry, dict): + errors.append("CEF lock contains a malformed payload entry.") + continue + relative_value = str(raw_entry.get("stagePath", "")) + try: + relative = require_relative_path(relative_value, "CEF stage path") + except BuildError as error: + errors.append(str(error)) + continue + expected_paths.add(relative.as_posix().lower()) + path = stage_root / relative + if not path.is_file(): + errors.append(f"Missing CEF file: {relative.as_posix()}") + continue + expected_size = int(raw_entry.get("size", -1)) + if path.stat().st_size != expected_size: + errors.append( + f"Size mismatch for {relative.as_posix()}; expected {expected_size}, " + f"found {path.stat().st_size}." + ) + expected_hash = str(raw_entry.get("sha256", "")).lower() + actual_hash = sha256_file(path) + if actual_hash != expected_hash: + errors.append( + f"Hash mismatch for {relative.as_posix()}; expected {expected_hash}, " + f"found {actual_hash}." + ) + + for group in ("Bin", "Resources"): + root = stage_root / group + if not root.is_dir(): + errors.append(f"CEF staged {group} directory is missing: {root}") + continue + for path in root.rglob("*"): + if path.is_file(): + relative = path.relative_to(stage_root).as_posix().lower() + if relative not in expected_paths: + errors.append(f"Unexpected CEF file: {path.relative_to(stage_root).as_posix()}") + errors.extend(_validate_build_inputs(stage_root)) + return errors + + +def validate_output(repo_root: Path, output_root: Path) -> list[str]: + lock = _load_lock(repo_root) + if not output_root.is_dir(): + return [f"CEF output directory is missing: {output_root}"] + errors: list[str] = [] + payload = lock["payload"] + assert isinstance(payload, list) + for raw_entry in payload: + if not isinstance(raw_entry, dict): + errors.append("CEF lock contains a malformed payload entry.") + continue + relative_value = str(raw_entry.get("path", "")) + try: + relative = require_relative_path(relative_value, "CEF output path") + except BuildError as error: + errors.append(str(error)) + continue + path = output_root / relative + if not path.is_file(): + errors.append(f"Missing CEF file: {relative.as_posix()}") + continue + expected_size = int(raw_entry.get("size", -1)) + if path.stat().st_size != expected_size: + errors.append(f"Size mismatch for {relative.as_posix()}") + expected_hash = str(raw_entry.get("sha256", "")).lower() + if sha256_file(path) != expected_hash: + errors.append(f"Hash mismatch for {relative.as_posix()}") + return errors + + +def _work_parent() -> Path: + configured = os.environ.get("PRESENTMON_CEF_WORK_ROOT") + if configured: + root = Path(configured) + else: + system_drive = os.environ.get("SystemDrive") + root = Path(f"{system_drive}\\pcef") if system_drive else Path(tempfile.gettempdir()) / "pcef" + root.mkdir(parents=True, exist_ok=True) + return root + + +def _cached_archive(repo_root: Path, dependency_root: Path, source: str | None) -> Path: + lock = _load_lock(repo_root) + source_lock = lock.get("source") + if not isinstance(source_lock, dict): + raise BuildError("CEF lock source is missing.") + locked_uri = str(source_lock.get("uri", "")) + expected_hash = str(source_lock.get("sha256", "")).lower() + expected_size = int(source_lock.get("size", -1)) + file_name = str(source_lock.get("fileName", "")).strip() + if not expected_hash or not file_name: + raise BuildError("CEF lock must contain source fileName and sha256 values.") + + if source: + parsed = urllib.parse.urlparse(source) + if parsed.scheme in ("http", "https"): + if source != locked_uri: + raise BuildError(f"CEF accepts only the URI recorded in the lock: {locked_uri}") + uri = source + local_source = None + else: + local_source = Path(source).resolve() + if not local_source.is_file(): + raise BuildError(f"CEF source archive was not found: {local_source}") + uri = "" + else: + local_source = None + uri = locked_uri + if not local_source and not uri: + raise BuildError("CEF lock does not define a source URI.") + + archive = dependency_root / "downloads" / "cef" / file_name + archive.parent.mkdir(parents=True, exist_ok=True) + if local_source: + actual_hash = sha256_file(local_source) + if actual_hash != expected_hash: + raise BuildError( + f"CEF archive hash does not match lock; expected {expected_hash}, found {actual_hash}." + ) + if local_source != archive: + temporary = archive.with_name(f"{archive.name}.incoming-{os.getpid()}") + shutil.copy2(local_source, temporary) + temporary.replace(archive) + return archive + + if archive.is_file(): + if archive.stat().st_size == expected_size and sha256_file(archive) == expected_hash: + return archive + archive.unlink() + temporary = archive.with_name(f"{archive.name}.incoming-{os.getpid()}") + log(f"Downloading locked CEF archive from {uri}") + download(uri, temporary) + actual_hash = sha256_file(temporary) + if actual_hash != expected_hash: + temporary.unlink(missing_ok=True) + raise BuildError( + f"CEF archive hash does not match lock; expected {expected_hash}, found {actual_hash}." + ) + if temporary.stat().st_size != expected_size: + temporary.unlink(missing_ok=True) + raise BuildError("CEF archive size does not match the lock.") + temporary.replace(archive) + return archive + + +def _extract_distribution(archive: Path, work_root: Path) -> Path: + extraction_root = work_root / "extract" + extraction_root.mkdir() + log(f"Extracting CEF archive into {extraction_root}") + run(["cmake", "-E", "tar", "xjf", archive], cwd=extraction_root) + candidates = [extraction_root, *[path for path in extraction_root.iterdir() if path.is_dir()]] + for candidate in candidates: + if all( + path.exists() + for path in ( + candidate / "CMakeLists.txt", + candidate / "Release", + candidate / "Resources", + candidate / "include", + ) + ): + return candidate + raise BuildError(f"Could not locate a CEF distribution root beneath {extraction_root}") + + +def _build_wrapper( + distribution_root: Path, + generator: str, + platform: str, + toolset: str, + generator_instance: str, +) -> None: + # The wrapper libraries must be produced by the same Visual Studio + # installation and toolset as the parent build, so none of these selections + # may be inferred here. + for description, value in ( + ("generator", generator), + ("platform", platform), + ("toolset", toolset), + ("generator instance", generator_instance), + ): + if not value: + raise BuildError(f"Building the CEF wrapper requires an explicit {description}.") + build_root = distribution_root / "build" + if build_root.exists(): + shutil.rmtree(build_root) + arguments = [ + "cmake", + "-S", + distribution_root, + "-B", + build_root, + "-G", + generator, + "-A", + platform, + "-T", + toolset, + f"-DCMAKE_GENERATOR_INSTANCE={generator_instance}", + "-DUSE_SANDBOX=OFF", + ] + environment = os.environ.copy() + environment.setdefault("CL_MPCount", "4") + log("Configuring the CEF wrapper.") + run(arguments, env=environment) + for configuration in ("Release", "Debug"): + log(f"Building the CEF wrapper for {configuration}.") + run( + ["cmake", "--build", build_root, "--config", configuration], + env=environment, + ) + output = build_root / "libcef_dll_wrapper" / configuration / "libcef_dll_wrapper.lib" + if not output.is_file(): + raise BuildError(f"CEF wrapper output was not produced: {output}") + + +def _copy_directory(source: Path, destination: Path, excluded_names: set[str] | None = None) -> None: + if not source.is_dir(): + raise BuildError(f"CEF source directory was not found: {source}") + destination.mkdir(parents=True, exist_ok=True) + excluded = {name.lower() for name in (excluded_names or set())} + for path in source.iterdir(): + if path.is_file() and path.name.lower() in excluded: + continue + target = destination / path.name + if path.is_dir(): + shutil.copytree(path, target, dirs_exist_ok=True) + else: + shutil.copy2(path, target) + + +def _create_stage(distribution_root: Path, destination: Path) -> None: + for relative in ("Bin", "Lib/Debug", "Lib/Release", "Include", "Resources"): + (destination / relative).mkdir(parents=True, exist_ok=True) + _copy_directory( + distribution_root / "Release", + destination / "Bin", + {"cef_sandbox.lib", "libcef.lib"}, + ) + _copy_directory(distribution_root / "include", destination / "Include" / "include") + _copy_directory(distribution_root / "Resources", destination / "Resources") + shutil.copy2( + distribution_root / "Release" / "libcef.lib", + destination / "Lib" / "Debug" / "libcef.lib", + ) + shutil.copy2( + distribution_root / "Release" / "libcef.lib", + destination / "Lib" / "Release" / "libcef.lib", + ) + _copy_directory( + distribution_root / "build" / "libcef_dll_wrapper" / "Debug", + destination / "Lib" / "Debug", + ) + _copy_directory( + distribution_root / "build" / "libcef_dll_wrapper" / "Release", + destination / "Lib" / "Release", + ) + locales = destination / "Resources" / "locales" + if locales.is_dir(): + for path in locales.iterdir(): + if path.is_file() and path.name.lower() != "en-us.pak": + path.unlink() + + +def restore( + repo_root: Path, + dependency_root: Path, + generator: str, + platform: str, + toolset: str, + generator_instance: str, + source: str | None = None, + skip_if_valid: bool = False, +) -> None: + toolset = _require_toolset(toolset) + # The archive cache and this lock stay shared across toolsets, so a v143 and + # a v145 restore serialize instead of publishing concurrently. + lock_file = dependency_root / "locks" / "cef.lock" + with file_lock(lock_file): + if skip_if_valid: + errors = validate_stage(repo_root, dependency_root, toolset) + if not errors: + log("Locked CEF stage is present and valid.") + return + log("CEF stage is missing, stale, or invalid; restoring it.") + archive = _cached_archive(repo_root, dependency_root, source) + work_root = Path(tempfile.mkdtemp(prefix="restore-", dir=_work_parent())) + keep_work = os.environ.get("PRESENTMON_CEF_KEEP_WORK", "").lower() in ( + "1", + "true", + "yes", + ) + success = False + try: + distribution_root = _extract_distribution(archive, work_root) + _build_wrapper(distribution_root, generator, platform, toolset, generator_instance) + staged = work_root / "stage" + staged.mkdir() + _create_stage(distribution_root, staged) + publish_root = dependency_root / f".cef-publish-{os.getpid()}" + if publish_root.exists(): + shutil.rmtree(publish_root) + try: + # Publication replaces only the selected toolset stage. The + # shared cef/ parent and every other stage beneath it, including + # an older unqualified one, are never renamed or removed. + shutil.copytree(staged, publish_root / "cef" / toolset) + errors = validate_stage(repo_root, publish_root, toolset) + if errors: + raise BuildError("Restored CEF stage did not validate:\n" + "\n".join(errors)) + replace_directory( + publish_root / "cef" / toolset, + _stage_root(dependency_root, toolset), + ) + finally: + shutil.rmtree(publish_root, ignore_errors=True) + success = True + log(f"Published locked CEF stage at {_stage_root(dependency_root, toolset)}") + finally: + if success and not keep_work: + shutil.rmtree(work_root, ignore_errors=True) + elif not success: + log(f"CEF restore work directory retained after failure: {work_root}") + + +def ensure( + repo_root: Path, + dependency_root: Path, + generator: str, + platform: str, + toolset: str, + generator_instance: str, + source: str | None = None, + force: bool = False, +) -> None: + restore( + repo_root, + dependency_root, + generator, + platform, + toolset, + generator_instance, + source, + skip_if_valid=not force, + ) diff --git a/Scripts/pmon/common.py b/Scripts/pmon/common.py new file mode 100644 index 000000000..ab4eb8a96 --- /dev/null +++ b/Scripts/pmon/common.py @@ -0,0 +1,174 @@ +from __future__ import annotations + +import hashlib +import json +import os +import shutil +import subprocess +import sys +import tempfile +import time +import urllib.request +from collections.abc import Iterable, Sequence +from contextlib import contextmanager +from pathlib import Path + + +class BuildError(RuntimeError): + pass + + +def log(message: str) -> None: + print(message, flush=True) + + +def run( + arguments: Sequence[str | os.PathLike[str]], + *, + cwd: Path | None = None, + env: dict[str, str] | None = None, + capture_output: bool = False, +) -> subprocess.CompletedProcess[str]: + command = [os.fspath(argument) for argument in arguments] + try: + return subprocess.run( + command, + cwd=cwd, + env=env, + check=True, + text=True, + capture_output=capture_output, + ) + except FileNotFoundError as error: + raise BuildError(f"Required command was not found: {command[0]}") from error + except subprocess.CalledProcessError as error: + command_text = subprocess.list2cmdline(command) + details = "" + if capture_output: + details = f"\n{error.stdout or ''}{error.stderr or ''}".rstrip() + raise BuildError( + f"Command failed with exit code {error.returncode}: {command_text}{details}" + ) from error + + +def sha256_file(path: Path, chunk_size: int = 1024 * 1024) -> str: + digest = hashlib.sha256() + with path.open("rb") as stream: + while chunk := stream.read(chunk_size): + digest.update(chunk) + return digest.hexdigest() + + +def require_relative_path(value: str, description: str) -> Path: + normalized = value.replace("\\", "/") + candidate = Path(normalized) + if ( + not normalized + or candidate.is_absolute() + or candidate.drive + or ".." in candidate.parts + or ";" in normalized + ): + raise BuildError(f"Unsafe {description}: {value}") + return candidate + + +def download(uri: str, destination: Path) -> None: + destination.parent.mkdir(parents=True, exist_ok=True) + request = urllib.request.Request(uri, headers={"User-Agent": "PresentMon-build"}) + try: + with urllib.request.urlopen(request) as response, destination.open("wb") as output: + shutil.copyfileobj(response, output) + except Exception as error: + raise BuildError(f"Failed to download {uri}: {error}") from error + + +def read_json(path: Path) -> object: + try: + with path.open("r", encoding="utf-8-sig") as stream: + return json.load(stream) + except (OSError, json.JSONDecodeError) as error: + raise BuildError(f"Failed to read JSON file {path}: {error}") from error + + +@contextmanager +def file_lock(path: Path, timeout_seconds: int = 3600) -> Iterable[None]: + if os.name != "nt": + raise BuildError("PresentMon dependency locking currently requires Windows.") + + import msvcrt + + path.parent.mkdir(parents=True, exist_ok=True) + stream = path.open("a+b") + stream.seek(0, os.SEEK_END) + if stream.tell() == 0: + stream.write(b"0") + stream.flush() + deadline = time.monotonic() + timeout_seconds + try: + while True: + try: + stream.seek(0) + msvcrt.locking(stream.fileno(), msvcrt.LK_NBLCK, 1) + break + except OSError: + if time.monotonic() >= deadline: + raise BuildError(f"Timed out waiting for dependency lock: {path}") + time.sleep(0.25) + yield + finally: + try: + stream.seek(0) + msvcrt.locking(stream.fileno(), msvcrt.LK_UNLCK, 1) + finally: + stream.close() + + +@contextmanager +def temporary_directory(parent: Path, prefix: str) -> Iterable[Path]: + parent.mkdir(parents=True, exist_ok=True) + directory = Path(tempfile.mkdtemp(prefix=prefix, dir=parent)) + try: + yield directory + finally: + shutil.rmtree(directory, ignore_errors=True) + + +def replace_directory(source: Path, destination: Path) -> None: + destination.parent.mkdir(parents=True, exist_ok=True) + backup = destination.with_name(f"{destination.name}.previous-{os.getpid()}") + if backup.exists(): + shutil.rmtree(backup) + if destination.exists(): + destination.rename(backup) + try: + source.rename(destination) + except Exception: + if backup.exists() and not destination.exists(): + backup.rename(destination) + raise + finally: + if backup.exists(): + shutil.rmtree(backup) + + +def write_text_atomic(path: Path, content: str) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + descriptor, temporary_name = tempfile.mkstemp( + prefix=f"{path.name}.", + suffix=".tmp", + dir=path.parent, + ) + temporary = Path(temporary_name) + try: + with os.fdopen(descriptor, "w", encoding="utf-8", newline="") as stream: + stream.write(content) + temporary.replace(path) + finally: + temporary.unlink(missing_ok=True) + + +def ensure_python_version(minimum: tuple[int, int] = (3, 10)) -> None: + if sys.version_info < minimum: + version = ".".join(str(part) for part in minimum) + raise BuildError(f"PresentMon build scripts require Python {version} or newer.") diff --git a/Scripts/pmon/discovery.py b/Scripts/pmon/discovery.py new file mode 100644 index 000000000..f0965158f --- /dev/null +++ b/Scripts/pmon/discovery.py @@ -0,0 +1,273 @@ +from __future__ import annotations + +import json +import os +import re +import subprocess +import tempfile +import uuid +from pathlib import Path + +from .common import BuildError, write_text_atomic + + +def _cmake_bracket(value: str) -> str: + for equals_count in range(1, 17): + equals = "=" * equals_count + if f"]{equals}]" not in value: + return f"[{equals}[{value}]{equals}]" + raise BuildError("Unable to quote a value for generated CMake input.") + + +def _run_capture(arguments: list[str], cwd: Path | None = None) -> str: + try: + result = subprocess.run( + arguments, + cwd=cwd, + check=False, + text=True, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + ) + except FileNotFoundError as error: + raise BuildError(f"Required command was not found: {arguments[0]}") from error + if result.returncode: + command = subprocess.list2cmdline(arguments) + raise BuildError( + f"Command failed with exit code {result.returncode}: {command}\n{result.stdout}" + ) + return result.stdout + + +def generate_gtest_ctest( + test_executable: Path, + output_file: Path, + test_prefix: str, + test_filter: str, + presentmon_path: Path, + gold_directory: Path, + optional_test_directory: Path | None, + working_directory: Path, + labels: str, + fixtures_required: str, + resource_lock: str, + allow_empty: bool = False, +) -> None: + if not test_executable.is_file(): + raise BuildError(f"Google Test executable was not built: {test_executable}") + if not presentmon_path.is_file(): + raise BuildError(f"PresentMon executable was not built: {presentmon_path}") + + runner_arguments = [ + f"--presentmon={presentmon_path}", + f"--golddir={gold_directory}", + ] + if optional_test_directory: + runner_arguments.append(f"--opttestdir={optional_test_directory}") + + environment = os.environ.copy() + environment.pop("PRESENTMON_ADDITIONAL_TEST_DIR", None) + arguments = [ + str(test_executable), + *runner_arguments, + "--gtest_list_tests", + f"--gtest_filter={test_filter}", + ] + try: + result = subprocess.run( + arguments, + cwd=working_directory, + env=environment, + check=False, + text=True, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + ) + except FileNotFoundError as error: + raise BuildError(f"Google Test executable was not found: {test_executable}") from error + if result.returncode: + raise BuildError( + f"Google Test discovery failed for {test_executable}\n{result.stdout}" + ) + + test_names: list[str] = [] + current_suite = "" + for line in result.stdout.splitlines(): + if not line: + continue + if not line[0].isspace(): + suite_name = line.split("#", 1)[0].strip() + current_suite = suite_name if suite_name.endswith(".") else "" + elif current_suite: + test_name = line.split("#", 1)[0].strip() + if test_name: + test_names.append(f"{current_suite}{test_name}") + if not test_names: + if not allow_empty: + raise BuildError(f"Google Test discovery found no tests matching {test_filter}.") + # Optional data-driven group with no data present. Register nothing rather than + # failing: this file is included on every CTest invocation, so raising here would + # break the whole test tree, not just this group. Discovery re-runs each time, so + # the tests appear as soon as the data directory is populated. + write_text_atomic( + output_file, + "\r\n".join(( + "# Generated from Google Test discovery. Do not edit.", + f"# No tests matched {test_filter}; the optional data directory is empty.", + )) + "\r\n", + ) + return + if len(set(test_names)) != len(test_names): + raise BuildError(f"Google Test discovery returned duplicate tests for {test_filter}.") + + ctest_labels = ";".join(value for value in labels.split(",") if value) + lines = ["# Generated from Google Test discovery. Do not edit."] + for test_name in test_names: + ctest_name = f"{test_prefix}{test_name}" + test_arguments = [f"--gtest_filter={test_name}", *runner_arguments] + command = " ".join(_cmake_bracket(value) for value in test_arguments) + lines.append( + f"add_test({_cmake_bracket(ctest_name)} " + f"{_cmake_bracket(str(test_executable))} {command})" + ) + properties = [ + "WORKING_DIRECTORY", + _cmake_bracket(str(working_directory)), + "SKIP_REGULAR_EXPRESSION", + _cmake_bracket(r"\[ SKIPPED \]"), + "ENVIRONMENT", + _cmake_bracket("PRESENTMON_ADDITIONAL_TEST_DIR="), + ] + for name, value in ( + ("LABELS", ctest_labels), + ("FIXTURES_REQUIRED", fixtures_required), + ("RESOURCE_LOCK", resource_lock), + ): + if value: + properties.extend((name, _cmake_bracket(value))) + if re.search(r"(^|\.)DISABLED_", test_name): + properties.extend(("DISABLED", "TRUE")) + lines.append( + f"set_tests_properties({_cmake_bracket(ctest_name)} PROPERTIES " + f"{' '.join(properties)})" + ) + write_text_atomic(output_file, "\r\n".join(lines) + "\r\n") + + +def _add_discovered_tests( + tests: dict[str, dict[str, object]], + discovered: object, +) -> None: + if discovered is None: + return + entries = discovered if isinstance(discovered, list) else [discovered] + for entry in entries: + if not isinstance(entry, dict): + continue + fully_qualified_name = str(entry.get("FullyQualifiedName", "")) + if fully_qualified_name: + tests[fully_qualified_name] = entry + + +def generate_vstest_ctest( + vstest_console: Path, + test_container: Path, + output_file: Path, + test_prefix: str, + working_directory: Path, + base_labels: str, + default_labels: str, + environment_labels: str, + environment_name_regex: str, + test_environment: str, + fixtures_required: str, + resource_lock: str, +) -> None: + if not vstest_console.is_file(): + raise BuildError(f"vstest.console.exe was not found: {vstest_console}") + if not test_container.is_file(): + raise BuildError(f"VSTest container was not built: {test_container}") + + diagnostic_file = Path(tempfile.gettempdir()) / f"pmon-vstest-{uuid.uuid4().hex}.log" + try: + _run_capture( + [ + str(vstest_console), + "/InIsolation", + f"/ListTests:{test_container}", + f"/Diag:{diagnostic_file}", + ] + ) + tests: dict[str, dict[str, object]] = {} + expected_test_count: int | None = None + marker = "Received message: " + for line in diagnostic_file.read_text(encoding="utf-8-sig", errors="replace").splitlines(): + marker_index = line.find(marker) + if marker_index < 0: + continue + try: + message = json.loads(line[marker_index + len(marker) :]) + except json.JSONDecodeError: + continue + if not isinstance(message, dict): + continue + payload = message.get("Payload") + if message.get("MessageType") == "TestDiscovery.TestFound": + _add_discovered_tests(tests, payload) + elif message.get("MessageType") == "TestDiscovery.Completed": + if not isinstance(payload, dict): + raise BuildError("VSTest completion payload is malformed.") + expected_test_count = int(payload.get("TotalTests", -1)) + _add_discovered_tests(tests, payload.get("LastDiscoveredTests")) + if expected_test_count is None: + raise BuildError( + "VSTest diagnostic output did not contain a discovery completion message." + ) + if len(tests) != expected_test_count: + raise BuildError( + f"VSTest reported {expected_test_count} tests but only {len(tests)} " + "fully qualified names were captured." + ) + + split_labels = lambda value: [item for item in value.split(",") if item] + base_label_list = split_labels(base_labels) + default_label_list = split_labels(default_labels) + environment_label_list = split_labels(environment_labels) + name_pattern = re.compile(environment_name_regex) if environment_name_regex else None + lines = ["# Generated from VSTest discovery. Do not edit."] + for fully_qualified_name in sorted(tests): + test_case = tests[fully_qualified_name] + ctest_name = f"{test_prefix}{fully_qualified_name.replace('::', '.')}" + filter_argument = f"/TestCaseFilter:FullyQualifiedName={fully_qualified_name}" + display_name = str(test_case.get("DisplayName", "")) + selected_labels = ( + environment_label_list + if name_pattern and name_pattern.search(display_name) + else default_label_list + ) + labels = list(dict.fromkeys([*base_label_list, *selected_labels])) + lines.append( + f"add_test({_cmake_bracket(ctest_name)} " + f"{_cmake_bracket(str(vstest_console))} /InIsolation " + f"{_cmake_bracket(str(test_container))} " + f"{_cmake_bracket(filter_argument)})" + ) + properties = [ + "WORKING_DIRECTORY", + _cmake_bracket(str(working_directory)), + ] + for name, value in ( + ("LABELS", ";".join(labels)), + ("ENVIRONMENT", test_environment), + ("FIXTURES_REQUIRED", fixtures_required), + ("RESOURCE_LOCK", resource_lock), + ): + if value: + properties.extend((name, _cmake_bracket(value))) + lines.append( + f"set_tests_properties({_cmake_bracket(ctest_name)} PROPERTIES " + f"{' '.join(properties)})" + ) + write_text_atomic(output_file, "\r\n".join(lines) + "\r\n") + finally: + diagnostic_file.unlink(missing_ok=True) diff --git a/Scripts/pmon/packaging.py b/Scripts/pmon/packaging.py new file mode 100644 index 000000000..2f39cc525 --- /dev/null +++ b/Scripts/pmon/packaging.py @@ -0,0 +1,556 @@ +from __future__ import annotations + +import hashlib +import os +import re +import shutil +import subprocess +import tempfile +import uuid +import xml.etree.ElementTree as ElementTree +from pathlib import Path + +from .cef import validate_output, validate_stage +from .common import BuildError, read_json, run, sha256_file, write_text_atomic + + +WIX_NAMESPACE = "http://schemas.microsoft.com/wix/2006/wi" +REPARSE_POINT = 0x0400 + + +def _is_true(value: str | bool) -> bool: + if isinstance(value, bool): + return value + return value.strip().lower() in ("true", "1", "on", "yes") + + +def _split_paths(value: str) -> list[Path]: + return [Path(path).resolve() for path in value.split("|") if path] + + +def _is_reparse_point(path: Path) -> bool: + stat = path.lstat() + return path.is_symlink() or bool(getattr(stat, "st_file_attributes", 0) & REPARSE_POINT) + + +def _clear_directory(path: Path) -> None: + resolved = path.resolve() + if resolved == Path(resolved.anchor) or len(resolved.parts) < 3: + raise BuildError(f"Refusing to clear unsafe staging directory: {resolved}") + resolved.mkdir(parents=True, exist_ok=True) + for child in resolved.iterdir(): + if child.is_symlink() or child.is_file(): + child.unlink() + else: + shutil.rmtree(child) + + +def stage_uci_runtime( + enabled: str | bool, + sdk_root: Path | None, + stage_dir: Path, + manifest_path: Path, +) -> None: + _clear_directory(stage_dir) + if not _is_true(enabled): + write_text_atomic(manifest_path, "") + return + if sdk_root is None or not sdk_root.is_dir(): + raise BuildError(f"UCI is enabled but its SDK root is missing: {sdk_root}") + sdk_root = sdk_root.resolve() + runtime_dll = sdk_root / "unified-collector-interface.dll" + collectors = sdk_root / "collectors" + if not runtime_dll.is_file(): + raise BuildError(f"Enabled UCI runtime is missing required file: {runtime_dll}") + if not collectors.is_dir(): + raise BuildError(f"Enabled UCI runtime is missing required directory: {collectors}") + collector_files = [path for path in collectors.rglob("*") if path.is_file()] + if not collector_files: + raise BuildError(f"Enabled UCI runtime requires a file under: {collectors}") + + relative_files: list[str] = [] + for root_name, directory_names, file_names in os.walk(sdk_root, followlinks=False): + root = Path(root_name) + for directory_name in directory_names: + directory = root / directory_name + if _is_reparse_point(directory): + raise BuildError(f"Reparse point is not allowed in the UCI SDK: {directory}") + for file_name in file_names: + source = root / file_name + if _is_reparse_point(source): + raise BuildError(f"Reparse point is not allowed in the UCI SDK: {source}") + relative = source.relative_to(sdk_root) + destination = stage_dir / relative + destination.parent.mkdir(parents=True, exist_ok=True) + shutil.copy2(source, destination) + relative_files.append(relative.as_posix()) + if not relative_files: + raise BuildError(f"Enabled UCI SDK contains no files: {sdk_root}") + write_text_atomic(manifest_path, "".join(f"{value}\r\n" for value in sorted(relative_files))) + + +def _empty_uci_fragment(output_path: Path) -> None: + content = ( + '\r\n' + f'\r\n' + " \r\n" + ' \r\n' + " \r\n" + "\r\n" + ) + write_text_atomic(output_path, content) + + +def _deterministic_guid(name: str) -> str: + value = bytearray(hashlib.md5(name.lower().encode("utf-8")).digest()) + value[6] = (value[6] & 0x0F) | 0x30 + value[8] = (value[8] & 0x3F) | 0x80 + return str(uuid.UUID(bytes_le=bytes(value))).upper() + + +def _deterministic_wix_id(name: str, prefix: str) -> str: + digest = hashlib.md5(name.lower().encode("utf-8")).hexdigest() + return f"{prefix}{digest[:8]}" + + +def _resolve_heat(wix_root: Path) -> Path: + heat = wix_root / "bin" / "heat.exe" + if not heat.is_file(): + raise BuildError(f"WiX heat.exe was not found: {heat}") + return heat + + +def _write_xml(document: ElementTree.ElementTree, output_path: Path) -> None: + ElementTree.register_namespace("", WIX_NAMESPACE) + ElementTree.indent(document, space=" ") + temporary = output_path.with_name(f"{output_path.name}.incoming-{os.getpid()}") + output_path.parent.mkdir(parents=True, exist_ok=True) + try: + document.write(temporary, encoding="utf-8", xml_declaration=True) + content = temporary.read_text(encoding="utf-8").replace("\n", "\r\n") + "\r\n" + write_text_atomic(output_path, content) + finally: + temporary.unlink(missing_ok=True) + + +def generate_uci_fragment( + enabled: str | bool, + stage_dir: Path, + output_path: Path, + wix_root: Path, +) -> None: + if not _is_true(enabled): + _empty_uci_fragment(output_path) + return + stage_dir = stage_dir.resolve() + if not (stage_dir / "unified-collector-interface.dll").is_file(): + raise BuildError("Enabled UCI stage is missing unified-collector-interface.dll.") + collectors = stage_dir / "collectors" + if not collectors.is_dir() or not any(path.is_file() for path in collectors.rglob("*")): + raise BuildError("Enabled UCI stage collectors directory is missing or empty.") + output_path.parent.mkdir(parents=True, exist_ok=True) + run( + [ + _resolve_heat(wix_root), + "dir", + stage_dir, + "-srd", + "-sreg", + "-scom", + "-dr", + "service_folder", + "-cg", + "uci_dist_files", + "-var", + "var.UciDistDir", + "-suid", + "-g1", + "-ag", + "-out", + output_path, + ] + ) + + document = ElementTree.parse(output_path) + namespace = {"w": WIX_NAMESPACE} + stage_prefix = f"{stage_dir}{os.sep}".lower() + for file_node in document.findall(".//w:File", namespace): + source = file_node.get("Source", "") + if not source or ".." in Path(source.replace("\\", "/")).parts: + raise BuildError(f"Unsafe harvested UCI source: {source}") + normalized = source.replace("/", "\\") + variable = "$(var.UciDistDir)" + if normalized.lower().startswith(variable.lower()): + relative = normalized[len(variable) :].lstrip("\\") + resolved = (stage_dir / relative).resolve() + elif re.match(r"^[A-Za-z]:\\", normalized): + resolved = Path(normalized).resolve() + else: + resolved = (stage_dir / normalized.lstrip("\\")).resolve() + if not f"{resolved}".lower().startswith(stage_prefix) or not resolved.is_file(): + raise BuildError(f"Harvested source is outside or missing from the UCI stage: {source}") + if _is_reparse_point(resolved): + raise BuildError(f"Harvested source is a reparse point: {resolved}") + + for component in document.findall(".//w:Component", namespace): + file_node = component.find("w:File", namespace) + if file_node is None: + raise BuildError("Harvested UCI component does not contain a File element.") + source = file_node.get("Source", "") + component.set("Win64", "yes") + component.set("Guid", f"{{{_deterministic_guid(source)}}}") + component.set("Id", _deterministic_wix_id(f"cmp:{source}", "c")) + file_node.set("Id", _deterministic_wix_id(f"file:{source}", "f")) + _write_xml(document, output_path) + + +def _native_snapshot_line(path: Path) -> str: + if not path.is_file(): + raise BuildError(f"Native packaging input is missing: {path}") + stat = path.stat() + windows_ticks = stat.st_mtime_ns // 100 + 621355968000000000 + return f"{path}|{stat.st_size}|{windows_ticks}|{sha256_file(path).upper()}" + + +def native_snapshot(mode: str, snapshot_path: Path, paths_list: str) -> None: + paths = _split_paths(paths_list) + if not paths: + raise BuildError("Native paths list must contain at least one path.") + current = [_native_snapshot_line(path) for path in paths] + if mode == "record": + write_text_atomic(snapshot_path, "".join(f"{line}\r\n" for line in current)) + return + if not snapshot_path.is_file(): + raise BuildError(f"Native payload snapshot is missing: {snapshot_path}") + expected = snapshot_path.read_text(encoding="ascii").splitlines() + if current != expected: + mismatches = [str(path) for path, old, new in zip(paths, expected, current) if old != new] + raise BuildError(f"Native payload changed during the WiX build: {', '.join(mismatches)}") + + +def verify_authenticode(signtool: Path, path: Path) -> None: + if not signtool.is_file(): + raise BuildError("Production packaging requires SignTool for Authenticode verification.") + if not path.is_file(): + raise BuildError(f"Packaged PE input is missing: {path}") + run([signtool, "verify", "/pa", path], capture_output=True) + + +def verify_production_inputs( + signtool: Path, + native_paths_list: str, + uci_enabled: str | bool, + uci_stage_dir: Path | None, + uci_manifest_path: Path | None, + skip_uci: bool, +) -> None: + for path in _split_paths(native_paths_list): + print(f"Verifying Authenticode packaging input: {path}") + verify_authenticode(signtool, path) + if _is_true(uci_enabled) and not skip_uci: + if uci_stage_dir is None or uci_manifest_path is None or not uci_manifest_path.is_file(): + raise BuildError("UCI production verification requires its stage and manifest.") + pe_count = 0 + for relative in uci_manifest_path.read_text(encoding="utf-8-sig").splitlines(): + if not relative.strip(): + continue + path = uci_stage_dir / relative + if not path.is_file(): + raise BuildError(f"UCI staged file is missing: {path}") + if path.suffix.lower() in (".exe", ".dll"): + pe_count += 1 + print(f"Verified UCI staged runtime manifest ({pe_count} PE files).") + + +def verify_link_preflight( + repo_root: Path, + dependency_root: Path, + toolset: str, + payload_root: Path, + deployment_profile: str, + signtool: Path, + native_paths_list: str, + uci_enabled: str | bool, + uci_stage_dir: Path | None, + uci_manifest_path: Path | None, + skip_production_pe: bool, +) -> None: + stage_errors = validate_stage(repo_root, dependency_root, toolset) + if stage_errors: + raise BuildError("CEF shared stage validation failed:\n" + "\n".join(stage_errors)) + cef_errors = validate_output(repo_root, payload_root) + if cef_errors: + raise BuildError("CEF packaging input validation failed:\n" + "\n".join(cef_errors)) + verify_cef_installer_fragments(repo_root) + if deployment_profile.upper() == "PRODUCTION" and not skip_production_pe: + verify_production_inputs( + signtool, + native_paths_list, + uci_enabled, + uci_stage_dir, + uci_manifest_path, + False, + ) + + +def _stable_cef_wix_id(prefix: str, value: str) -> str: + safe = re.sub(r"[^A-Za-z0-9_.]", "_", value) + if len(safe) > 45: + safe = safe[-45:] + digest = hashlib.sha256(value.encode("utf-8")).hexdigest()[:8] + return f"{prefix}_{safe}_{digest}" + + +def _cef_fragment_entries(repo_root: Path, group: str) -> list[str]: + lock = read_json(repo_root / "IntelPresentMon" / "AppCef" / "cef-lock.json") + if not isinstance(lock, dict) or not isinstance(lock.get("payload"), list): + raise BuildError("CEF lock payload is missing or malformed.") + entries: list[str] = [] + for entry in lock["payload"]: + if isinstance(entry, dict) and entry.get("group") == group: + relative = str(entry.get("path", "")).replace("\\", "/") + if not relative or relative.startswith("/") or ".." in Path(relative).parts: + raise BuildError(f"Unsafe CEF installer path in the lock: {relative}") + entries.append(relative) + if not entries: + raise BuildError(f"CEF lock contains no {group} installer entries.") + return sorted(entries) + + +def _verify_cef_installer_fragment( + path: Path, + component_group: str, + entries: list[str], +) -> None: + if not path.is_file(): + raise BuildError(f"Installer CEF fragment is missing: {path}") + document = ElementTree.parse(path) + namespace = {"w": WIX_NAMESPACE} + directory_refs = document.findall(".//w:DirectoryRef", namespace) + if len(directory_refs) != 1 or directory_refs[0].get("Id") != "pm_app_folder": + raise BuildError(f"Installer CEF fragment has an invalid DirectoryRef: {path}") + + expected_directories = sorted( + { + relative.rsplit("/", 1)[0] + for relative in entries + if "/" in relative + } + ) + if any("/" in value for value in expected_directories): + raise BuildError("Nested CEF installer directories deeper than one level are unsupported.") + actual_directories = { + node.get("Name", ""): node.get("Id", "") + for node in directory_refs[0].findall("w:Directory", namespace) + } + expected_directory_map = { + value: _stable_cef_wix_id("cef_dir", value) for value in expected_directories + } + if actual_directories != expected_directory_map: + raise BuildError(f"Installer CEF directory entries are stale: {path}") + + groups = [ + node + for node in document.findall(".//w:ComponentGroup", namespace) + if node.get("Id") == component_group + ] + if len(groups) != 1: + raise BuildError(f"Installer CEF component group is missing or duplicated: {path}") + actual_components = { + node.get("Id", ""): node for node in groups[0].findall("w:Component", namespace) + } + expected_component_ids = { + _stable_cef_wix_id("cef_cmp", relative) for relative in entries + } + if set(actual_components) != expected_component_ids: + raise BuildError(f"Installer CEF component entries are stale: {path}") + + for relative in entries: + component_id = _stable_cef_wix_id("cef_cmp", relative) + component = actual_components[component_id] + directory = relative.rsplit("/", 1)[0] if "/" in relative else "" + expected_directory = ( + _stable_cef_wix_id("cef_dir", directory) if directory else "pm_app_folder" + ) + if component.get("Directory") != expected_directory or component.get("Guid") != "*": + raise BuildError(f"Installer CEF component metadata is stale: {path}") + files = component.findall("w:File", namespace) + leaf = relative.rsplit("/", 1)[-1] + if len(files) != 1: + raise BuildError(f"Installer CEF component must contain one file: {path}") + file_node = files[0] + expected_source = "$(var.PresentMon.TargetDir)\\" + relative.replace("/", "\\") + expected_file_id = _stable_cef_wix_id("cef_file", relative) + if ( + file_node.get("Id") != expected_file_id + or file_node.get("Name") != leaf + or file_node.get("KeyPath") != "yes" + or file_node.get("Source") != expected_source + ): + raise BuildError(f"Installer CEF file metadata is stale: {path}") + + +def verify_cef_installer_fragments(repo_root: Path) -> None: + installer_root = repo_root / "IntelPresentMon" / "PMInstaller" + for file_name, component_group, lock_group in ( + ("CefBinaries.wxs", "CefBinaries", "Bin"), + ("CefResources.wxs", "CefResources", "Resources"), + ): + _verify_cef_installer_fragment( + installer_root / file_name, + component_group, + _cef_fragment_entries(repo_root, lock_group), + ) + + +def require_signed_payload( + stamp: Path, + signtool: Path, + native_paths_list: str, + uci_enabled: str | bool, + uci_stage_dir: Path | None, + uci_manifest_path: Path | None, +) -> None: + if not stamp.is_file(): + raise BuildError( + f"Production payload sign stamp is missing: {stamp}. " + "Run pmon_sign_production_payload first." + ) + verify_production_inputs( + signtool, + native_paths_list, + uci_enabled, + uci_stage_dir, + uci_manifest_path, + True, + ) + + +def _source_path_for_leaf(repo_root: Path, payload_root: Path, leaf: str) -> Path | None: + lock_path = repo_root / "IntelPresentMon" / "AppCef" / "cef-lock.json" + lock = read_json(lock_path) + if isinstance(lock, dict): + payload = lock.get("payload", []) + if isinstance(payload, list): + for entry in payload: + if isinstance(entry, dict): + relative = Path(str(entry.get("path", ""))) + if relative.name == leaf: + return payload_root / relative + direct = payload_root / leaf + return direct if direct.is_file() else None + + +def _expected_native_hashes(snapshot_path: Path, native_paths: list[Path]) -> dict[str, str]: + if not snapshot_path.is_file(): + raise BuildError(f"Native payload snapshot is missing: {snapshot_path}") + leaves = {path.name for path in native_paths} + expected: dict[str, str] = {} + for line in snapshot_path.read_text(encoding="ascii").splitlines(): + parts = line.split("|") + if len(parts) < 4: + raise BuildError(f"Native snapshot line is missing SHA256: {line}") + leaf = Path(parts[0]).name + if leaf in leaves: + expected[leaf] = parts[3].upper() + missing = leaves - expected.keys() + if missing: + raise BuildError(f"Native snapshot is missing entries: {', '.join(sorted(missing))}") + return expected + + +def verify_msi_payload( + msi_path: Path, + payload_root: Path, + repo_root: Path, + signtool: Path, + native_paths_list: str, + snapshot_path: Path, + skip_msi_authenticode: bool, +) -> None: + msi_path = msi_path.resolve() + if not skip_msi_authenticode: + verify_authenticode(signtool, msi_path) + native_paths = _split_paths(native_paths_list) + native_by_leaf = {path.name: path for path in native_paths} + expected = _expected_native_hashes(snapshot_path, native_paths) + with tempfile.TemporaryDirectory(prefix="pmon-msi-verify-") as temporary_name: + extract_root = Path(temporary_name) / "admin" + extract_root.mkdir() + msiexec = Path(os.environ.get("WINDIR", r"C:\Windows")) / "System32" / "msiexec.exe" + run( + [ + msiexec, + "/a", + msi_path, + "/qn", + f"TARGETDIR={extract_root.resolve()}", + "/norestart", + ], + capture_output=True, + ) + seen: set[str] = set() + for extracted in extract_root.rglob("*"): + if not extracted.is_file() or extracted.suffix.lower() not in (".exe", ".dll"): + continue + leaf = extracted.name + extracted_hash = sha256_file(extracted).upper() + if leaf in native_by_leaf: + seen.add(leaf) + if not skip_msi_authenticode: + verify_authenticode(signtool, extracted) + if extracted_hash != expected[leaf]: + raise BuildError(f"Embedded MSI native payload hash mismatch for {leaf}.") + continue + source = _source_path_for_leaf(repo_root, payload_root, leaf) + if source and source.is_file() and sha256_file(source).upper() != extracted_hash: + raise BuildError(f"Embedded MSI payload hash mismatch for {leaf}.") + missing = native_by_leaf.keys() - seen + if missing: + raise BuildError(f"Embedded MSI is missing native files: {', '.join(sorted(missing))}") + + +def sign_production_installer( + msi_path: Path, + payload_root: Path, + repo_root: Path, + signtool: Path, + edss_script: Path, + native_paths_list: str, + snapshot_path: Path, + in_process_signing: bool, + skip_edss: bool, +) -> None: + if not skip_edss: + if not edss_script.is_file(): + raise BuildError(f"External EDSS signing script was not found: {edss_script}") + arguments: list[str | Path] = [ + "powershell.exe", + "-NoLogo", + "-NoProfile", + "-NonInteractive", + "-ExecutionPolicy", + "Bypass", + "-File", + edss_script, + "-OutputRoot", + payload_root, + "-RepoRoot", + repo_root, + "-SignMsiOnly", + "-Verify", + ] + if in_process_signing: + arguments.append("-InProcessSigning") + else: + arguments.append("-ImpersonatedSigning") + run(arguments) + verify_msi_payload( + msi_path, + payload_root, + repo_root, + signtool, + native_paths_list, + snapshot_path, + False, + ) diff --git a/Scripts/pmon/signing.py b/Scripts/pmon/signing.py new file mode 100644 index 000000000..72a59e311 --- /dev/null +++ b/Scripts/pmon/signing.py @@ -0,0 +1,72 @@ +from __future__ import annotations + +from pathlib import Path + +from .common import BuildError, run + +_DEVELOPER_CERTIFICATE_STORE_FALLBACKS: dict[str, tuple[str, ...]] = { + "my": ("PrivateCertStore",), + "privatecertstore": ("My",), +} + + +def _developer_certificate_stores(primary_store: str) -> list[str]: + stores = [primary_store] + normalized = primary_store.replace(" ", "").lower() + for fallback in _DEVELOPER_CERTIFICATE_STORE_FALLBACKS.get(normalized, ()): + if fallback not in stores: + stores.append(fallback) + return stores + + +def sign_developer_binary( + configuration: str, + signtool: Path, + target: Path, + certificate_store: str, + certificate_name: str, + timestamp_url: str, +) -> None: + if configuration.lower() != "release": + return + if not signtool.is_file(): + raise BuildError( + "SignTool was not found. Install the Windows SDK or set " + "PMON_SIGNTOOL_EXECUTABLE." + ) + if not target.is_file(): + raise BuildError(f"Signing target output was not found: {target}") + stores = _developer_certificate_stores(certificate_store) + sign_errors: list[BuildError] = [] + for store in stores: + try: + run( + [ + signtool, + "sign", + "/a", + "/v", + "/s", + store, + "/n", + certificate_name, + "/t", + timestamp_url, + "/fd", + "sha1", + target, + ], + capture_output=True, + ) + sign_errors.clear() + break + except BuildError as error: + sign_errors.append(error) + if sign_errors: + last_error = sign_errors[-1] + raise BuildError( + "Developer signing failed. Install the test certificate in PrivateCertStore " + "(legacy Visual Studio / MSBuild setup) or run Scripts/setup_dev_certificate.py " + f"for CurrentUser\\My. {last_error}" + ) from last_error + run([signtool, "verify", "/pa", "/v", target]) diff --git a/Scripts/pmon/web.py b/Scripts/pmon/web.py new file mode 100644 index 000000000..f058b410f --- /dev/null +++ b/Scripts/pmon/web.py @@ -0,0 +1,49 @@ +from __future__ import annotations + +import hashlib +import shutil +from pathlib import Path + +from .common import BuildError, file_lock, log, run, sha256_file + + +def _fingerprint(web_root: Path) -> str: + digest = hashlib.sha256() + for name in ("package.json", "package-lock.json"): + path = web_root / name + if not path.is_file(): + raise BuildError(f"Web dependency input is missing: {path}") + digest.update(name.encode("ascii")) + digest.update(sha256_file(path).encode("ascii")) + return digest.hexdigest() + + +def ensure(repo_root: Path, dependency_root: Path, force: bool = False) -> None: + web_root = repo_root / "IntelPresentMon" / "AppCef" / "ipm-ui-vue" + node_modules = web_root / "node_modules" + stamp = node_modules / ".pmon-dependencies.sha256" + expected = _fingerprint(web_root) + with file_lock(dependency_root / "locks" / "web.lock", timeout_seconds=1800): + npm = shutil.which("npm.cmd") or shutil.which("npm") + if not npm: + raise BuildError("npm was not found. Install Node.js before configuring PresentMon.") + if not force and node_modules.is_dir() and stamp.is_file(): + if stamp.read_text(encoding="ascii").strip() == expected: + try: + run([npm, "ls", "--depth=0", "--silent"], cwd=web_root, capture_output=True) + log("Web dependencies are present and current.") + return + except BuildError: + log("Web dependencies are incomplete or invalid; reinstalling them.") + log("Installing locked web dependencies with npm ci.") + run([npm, "ci"], cwd=web_root) + stamp.write_text(expected + "\n", encoding="ascii") + + +def build(repo_root: Path, dependency_root: Path) -> None: + ensure(repo_root, dependency_root) + web_root = repo_root / "IntelPresentMon" / "AppCef" / "ipm-ui-vue" + npm = shutil.which("npm.cmd") or shutil.which("npm") + if not npm: + raise BuildError("npm was not found. Install Node.js before building PresentMonUI.") + run([npm, "run", "build"], cwd=web_root) diff --git a/Scripts/setup_dev_certificate.py b/Scripts/setup_dev_certificate.py new file mode 100644 index 000000000..b51566dde --- /dev/null +++ b/Scripts/setup_dev_certificate.py @@ -0,0 +1,265 @@ +from __future__ import annotations + +import argparse +import ctypes +import subprocess +import sys +import tempfile +from ctypes import wintypes +from pathlib import Path + +from pmon.common import BuildError, ensure_python_version, run + + +SUBJECT_NAME = "Test Certificate - For Internal Use Only" +CURRENT_USER = 0x00010000 +LOCAL_MACHINE = 0x00020000 +READ_ONLY = 0x00008000 +X509_ASN_ENCODING = 0x00000001 +PKCS_7_ASN_ENCODING = 0x00010000 +FIND_SUBJECT_STRING = 0x00080007 +SHA1_HASH_PROPERTY = 3 +SYSTEM_STORE_PROVIDER = 10 + + +class CertificateContext(ctypes.Structure): + _fields_ = [ + ("dwCertEncodingType", wintypes.DWORD), + ("pbCertEncoded", ctypes.POINTER(ctypes.c_ubyte)), + ("cbCertEncoded", wintypes.DWORD), + ("pCertInfo", ctypes.c_void_p), + ("hCertStore", ctypes.c_void_p), + ] + + +CertificateContextPointer = ctypes.POINTER(CertificateContext) + + +def parse_arguments() -> argparse.Namespace: + parser = argparse.ArgumentParser( + description="Create and trust the PresentMon developer code-signing certificate." + ) + parser.add_argument("--check", action="store_true", help="Check setup without changing stores.") + parser.add_argument( + "--certificate-output", + type=Path, + help="Public certificate location used by the manual trust procedure.", + ) + return parser.parse_args() + + +def _crypt32() -> ctypes.WinDLL: + if sys.platform != "win32": + raise BuildError("Developer certificate setup is supported only on Windows.") + library = ctypes.WinDLL("crypt32", use_last_error=True) + library.CertOpenStore.argtypes = [ + ctypes.c_void_p, + wintypes.DWORD, + ctypes.c_void_p, + wintypes.DWORD, + ctypes.c_void_p, + ] + library.CertOpenStore.restype = ctypes.c_void_p + library.CertFindCertificateInStore.argtypes = [ + ctypes.c_void_p, + wintypes.DWORD, + wintypes.DWORD, + wintypes.DWORD, + ctypes.c_void_p, + CertificateContextPointer, + ] + library.CertFindCertificateInStore.restype = CertificateContextPointer + library.CertEnumCertificatesInStore.argtypes = [ + ctypes.c_void_p, + CertificateContextPointer, + ] + library.CertEnumCertificatesInStore.restype = CertificateContextPointer + library.CertGetCertificateContextProperty.argtypes = [ + CertificateContextPointer, + wintypes.DWORD, + ctypes.c_void_p, + ctypes.POINTER(wintypes.DWORD), + ] + library.CertGetCertificateContextProperty.restype = wintypes.BOOL + library.CertFreeCertificateContext.argtypes = [CertificateContextPointer] + library.CertFreeCertificateContext.restype = wintypes.BOOL + library.CertCloseStore.argtypes = [ctypes.c_void_p, wintypes.DWORD] + library.CertCloseStore.restype = wintypes.BOOL + return library + + +def _open_store(library: ctypes.WinDLL, name: str, location: int) -> int: + store_name = ctypes.c_wchar_p(name) + store = library.CertOpenStore( + ctypes.c_void_p(SYSTEM_STORE_PROVIDER), + 0, + None, + location | READ_ONLY, + ctypes.cast(store_name, ctypes.c_void_p), + ) + if not store: + raise BuildError(f"Could not open the Windows certificate store {name}.") + return store + + +def _thumbprint(library: ctypes.WinDLL, context: CertificateContextPointer) -> bytes: + size = wintypes.DWORD() + if not library.CertGetCertificateContextProperty( + context, SHA1_HASH_PROPERTY, None, ctypes.byref(size) + ): + raise BuildError("Could not read a certificate thumbprint.") + buffer = (ctypes.c_ubyte * size.value)() + if not library.CertGetCertificateContextProperty( + context, SHA1_HASH_PROPERTY, buffer, ctypes.byref(size) + ): + raise BuildError("Could not read a certificate thumbprint.") + return bytes(buffer[: size.value]) + + +def _find_developer_certificate() -> tuple[bytes, bytes] | None: + library = _crypt32() + store = _open_store(library, "MY", CURRENT_USER) + context = CertificateContextPointer() + try: + subject = ctypes.c_wchar_p(SUBJECT_NAME) + context = library.CertFindCertificateInStore( + store, + X509_ASN_ENCODING | PKCS_7_ASN_ENCODING, + 0, + FIND_SUBJECT_STRING, + ctypes.cast(subject, ctypes.c_void_p), + None, + ) + if not context: + return None + encoded = ctypes.string_at( + context.contents.pbCertEncoded, + context.contents.cbCertEncoded, + ) + return _thumbprint(library, context), encoded + finally: + if context: + library.CertFreeCertificateContext(context) + library.CertCloseStore(store, 0) + + +def _root_contains(thumbprint: bytes) -> bool: + library = _crypt32() + store = _open_store(library, "ROOT", LOCAL_MACHINE) + context = CertificateContextPointer() + try: + while True: + context = library.CertEnumCertificatesInStore(store, context) + if not context: + return False + if _thumbprint(library, context) == thumbprint: + return True + finally: + library.CertCloseStore(store, 0) + + +def _is_elevated() -> bool: + return bool(ctypes.windll.shell32.IsUserAnAdmin()) + + +def _create_certificate() -> None: + policy = "\r\n".join( + ( + "[Version]", + 'Signature="$Windows NT$"', + "", + "[NewRequest]", + f'Subject="CN={SUBJECT_NAME}"', + "KeyLength=2048", + "KeySpec=2", + "Exportable=TRUE", + "MachineKeySet=FALSE", + "RequestType=Cert", + "HashAlgorithm=sha256", + "KeyUsage=0x80", + 'ProviderName="Microsoft Enhanced RSA and AES Cryptographic Provider"', + "ProviderType=24", + "", + "[EnhancedKeyUsageExtension]", + "OID=1.3.6.1.5.5.7.3.3", + "", + ) + ) + with tempfile.TemporaryDirectory(prefix="pmon-cert-") as temporary_name: + temporary = Path(temporary_name) + policy_path = temporary / "certificate.inf" + certificate_path = temporary / "certificate.cer" + policy_path.write_text(policy, encoding="ascii", newline="") + run(["certreq.exe", "-q", "-new", "-user", policy_path, certificate_path]) + if _find_developer_certificate() is not None: + return + try: + run(["certreq.exe", "-q", "-accept", "-user", certificate_path]) + except BuildError: + # Self-signed dev certs are often installed by certreq -new; -accept can + # fail with CERT_E_UNTRUSTEDROOT until the same cert is trusted in Root. + if _find_developer_certificate() is not None: + return + run(["certutil.exe", "-user", "-addstore", "My", certificate_path]) + + +def configure(certificate_output: Path, check_only: bool) -> None: + certificate = _find_developer_certificate() + if certificate is None: + if check_only: + raise BuildError( + f"Developer certificate is missing from CurrentUser\\My: {SUBJECT_NAME}" + ) + _create_certificate() + certificate = _find_developer_certificate() + if certificate is None: + raise BuildError("Certificate creation completed but it was not installed.") + print("Created the developer certificate in CurrentUser\\My.") + else: + print("Developer certificate is present in CurrentUser\\My.") + + thumbprint, encoded = certificate + if _root_contains(thumbprint): + print("Developer certificate is trusted in LocalMachine\\Root.") + return + if check_only: + raise BuildError("Developer certificate is not trusted in LocalMachine\\Root.") + + certificate_output.parent.mkdir(parents=True, exist_ok=True) + certificate_output.write_bytes(encoded) + if not _is_elevated(): + command = subprocess.list2cmdline( + ["certutil.exe", "-addstore", "-f", "Root", str(certificate_output)] + ) + raise BuildError( + "Administrator rights are required for LocalMachine\\Root trust. " + f"Run this command from an elevated Command Prompt: {command}" + ) + run(["certutil.exe", "-addstore", "-f", "Root", certificate_output]) + if not _root_contains(thumbprint): + raise BuildError("certutil completed but LocalMachine\\Root trust was not found.") + print("Trusted the developer certificate in LocalMachine\\Root.") + + +def main() -> int: + try: + ensure_python_version() + arguments = parse_arguments() + output = arguments.certificate_output + if output is None: + output = ( + Path(__file__).resolve().parents[1] + / "build" + / "ThirdParty" + / "setup" + / "presentmon-testcert.cer" + ) + configure(output.resolve(), arguments.check) + return 0 + except BuildError as error: + print(f"error: {error}", file=sys.stderr) + return 1 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/Scripts/sign.py b/Scripts/sign.py new file mode 100644 index 000000000..bd5930cb2 --- /dev/null +++ b/Scripts/sign.py @@ -0,0 +1,43 @@ +from __future__ import annotations + +import argparse +import sys +from pathlib import Path + +from pmon.common import BuildError, ensure_python_version +from pmon.signing import sign_developer_binary + + +def parse_arguments() -> argparse.Namespace: + parser = argparse.ArgumentParser(description="Run PresentMon signing steps.") + subparsers = parser.add_subparsers(dest="operation", required=True) + developer = subparsers.add_parser("developer") + developer.add_argument("--configuration", required=True) + developer.add_argument("--signtool", type=Path, required=True) + developer.add_argument("--target", type=Path, required=True) + developer.add_argument("--certificate-store", required=True) + developer.add_argument("--certificate-name", required=True) + developer.add_argument("--timestamp-url", required=True) + return parser.parse_args() + + +def main() -> int: + try: + ensure_python_version() + arguments = parse_arguments() + sign_developer_binary( + arguments.configuration, + arguments.signtool.resolve(), + arguments.target.resolve(), + arguments.certificate_store, + arguments.certificate_name, + arguments.timestamp_url, + ) + return 0 + except BuildError as error: + print(f"error: {error}", file=sys.stderr) + return 1 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/Scripts/versioning.py b/Scripts/versioning.py new file mode 100644 index 000000000..4c498cb88 --- /dev/null +++ b/Scripts/versioning.py @@ -0,0 +1,149 @@ +from __future__ import annotations + +import argparse +import datetime +import hashlib +import os +import subprocess +import sys +from pathlib import Path + +from pmon.common import BuildError, ensure_python_version, write_text_atomic + + +def parse_arguments() -> argparse.Namespace: + parser = argparse.ArgumentParser(description="Generate the PresentMon build ID header.") + parser.add_argument("--repo-root", type=Path, required=True) + parser.add_argument("--output-dir", type=Path, required=True) + return parser.parse_args() + + +def git(repo_root: Path, *arguments: str) -> str: + command = ["git", "-C", str(repo_root), *arguments] + try: + result = subprocess.run( + command, + check=False, + text=True, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + ) + except FileNotFoundError as error: + raise BuildError("git is required to generate the PresentMon build ID.") from error + if result.returncode: + raise BuildError( + f"git command failed with exit code {result.returncode}: " + f"{subprocess.list2cmdline(command)}\n{result.stderr}" + ) + return result.stdout + + +def timestamp() -> str: + now = datetime.datetime.now() + return f"{now.year}.{now.month}.{now.day}.{now.hour:02d}:{now.minute:02d}:{now.second:02d}" + + +def build_id_content(git_hash: str, build_time: str, dirty: bool) -> str: + git_hash = git_hash.strip() or "unknown" + dirty_token = "true" if dirty else "false" + lines = [ + "#pragma once", + "// Auto-generated by versioning.py", + "", + f'#define PM_BID_GIT_HASH "{git_hash}"', + f'#define PM_BID_GIT_HASH_SHORT "{git_hash[:7]}"', + f'#define PM_BID_TIME "{build_time}"', + f'#define PM_BID_UID "{git_hash}-{build_time}"', + f"#define PM_BID_DIRTY {dirty_token}", + ] + return "\r\n".join(lines) + "\r\n" + + +def file_line_count(path: Path) -> int: + with path.open("rb") as stream: + return sum(1 for _ in stream) + + +def md5_hex(path: Path) -> str: + digest = hashlib.md5() + with path.open("rb") as stream: + while chunk := stream.read(1024 * 1024): + digest.update(chunk) + return digest.hexdigest().upper() + + +def generate(repo_root: Path, output_dir: Path) -> None: + output_dir.mkdir(parents=True, exist_ok=True) + build_id_path = output_dir / "build_id.h" + if os.environ.get("PM_VCPKG_MODE") == "1": + git_hash = next( + ( + value.strip() + for value in ( + os.environ.get("PM_VCPKG_REF", ""), + os.environ.get("PM_VCPKG_SHA512", ""), + "BADF00D5", + ) + if value.strip() + ), + "BADF00D5", + ) + write_text_atomic(build_id_path, build_id_content(git_hash, timestamp(), False)) + print("[build_id_gen] Generated vcpkg build ID.") + return + + git_hash = git(repo_root, "rev-parse", "HEAD").strip() + numstat_lines = git(repo_root, "diff", "--numstat", "HEAD").splitlines() + untracked_files = [ + value + for value in git(repo_root, "ls-files", "--others", "--exclude-standard").splitlines() + if value + ] + untracked_lines: list[str] = [] + for relative in untracked_files: + path = repo_root / relative + if path.is_file(): + untracked_lines.append(f"{file_line_count(path)}\t0\t{relative}") + + signature_lines = [git_hash, *numstat_lines, *untracked_lines] + signature_a = " ".join(signature_lines) + changed_paths: list[Path] = [] + for line in [*numstat_lines, *untracked_lines]: + fields = line.split("\t", 2) + if len(fields) == 3: + relative = fields[2] + if " => " in relative: + relative = relative.rsplit(" => ", 1)[1].rstrip("}") + path = repo_root / relative + if path.is_file(): + changed_paths.append(path) + signature_b = "".join(md5_hex(path) for path in changed_paths) + + signature_a_path = output_dir / "signature_a_prev.txt" + signature_b_path = output_dir / "signature_b_prev.txt" + previous_a = signature_a_path.read_text(encoding="utf-8", errors="replace") if signature_a_path.is_file() else "" + previous_b = signature_b_path.read_text(encoding="utf-8", errors="replace") if signature_b_path.is_file() else "" + if build_id_path.is_file() and signature_a.strip() == previous_a.strip() and signature_b.strip() == previous_b.strip(): + print("[build_id_gen] Worktree signature is unchanged; retaining build_id.h.") + return + + write_text_atomic(signature_a_path, signature_a + "\r\n") + write_text_atomic(signature_b_path, signature_b + "\r\n") + dirty = bool(git(repo_root, "status", "--porcelain", "--untracked-files=normal").strip()) + write_text_atomic(build_id_path, build_id_content(git_hash, timestamp(), dirty)) + print("[build_id_gen] Generated build_id.h.") + + +def main() -> int: + try: + ensure_python_version() + arguments = parse_arguments() + generate(arguments.repo_root.resolve(), arguments.output_dir.resolve()) + return 0 + except (BuildError, OSError) as error: + print(f"error: {error}", file=sys.stderr) + return 1 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/Scripts/web.py b/Scripts/web.py new file mode 100644 index 000000000..fcba128b6 --- /dev/null +++ b/Scripts/web.py @@ -0,0 +1,44 @@ +from __future__ import annotations + +import argparse +import sys +from pathlib import Path + +from pmon import web +from pmon.common import BuildError, ensure_python_version + + +def parse_arguments() -> argparse.Namespace: + parser = argparse.ArgumentParser(description="Prepare or build PresentMon web assets.") + parser.add_argument("operation", choices=("ensure", "build")) + parser.add_argument( + "--repo-root", + type=Path, + default=Path(__file__).resolve().parents[1], + ) + parser.add_argument("--dependency-root", type=Path) + return parser.parse_args() + + +def main() -> int: + try: + ensure_python_version() + arguments = parse_arguments() + repo_root = arguments.repo_root.resolve() + dependency_root = ( + arguments.dependency_root.resolve() + if arguments.dependency_root + else repo_root / "build" / "ThirdParty" + ) + if arguments.operation == "ensure": + web.ensure(repo_root, dependency_root) + else: + web.build(repo_root, dependency_root) + return 0 + except BuildError as error: + print(f"error: {error}", file=sys.stderr) + return 1 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/Tests/CMakeLists.txt b/Tests/CMakeLists.txt new file mode 100644 index 000000000..6cf173eaf --- /dev/null +++ b/Tests/CMakeLists.txt @@ -0,0 +1,29 @@ +find_package(GTest CONFIG REQUIRED) + +add_executable( + PresentMonTests + CommandLineTests.cpp + GoldEtlCsvTests.cpp + PresentMonTests.cpp + PresentMon.cpp +) +target_compile_definitions(PresentMonTests PRIVATE _CONSOLE) +target_link_libraries( + PresentMonTests + PRIVATE + pmon::build_options + pmon::warnings_strict + pmon::warnings_strict_link + pmon::release_link_policy + pmon::version_header + GTest::gtest +) +set_target_properties( + PresentMonTests + PROPERTIES + OUTPUT_NAME "PresentMonTests-${PMON_VERSION}-${PMON_ARCH_TAG}" + FOLDER "Tests" +) +add_dependencies(PresentMonTests PresentMonConsole) +add_dependencies(PresentMonTests pmon_ensure_aux_testdata) +pmon_register_presentmon_gtest_leaf_tests(PresentMonTests PresentMonConsole) diff --git a/Tests/PresentMonTestData.targets b/Tests/PresentMonTestData.targets new file mode 100644 index 000000000..bf548c569 --- /dev/null +++ b/Tests/PresentMonTestData.targets @@ -0,0 +1,8 @@ + + + + + + diff --git a/Tests/PresentMonTests.cpp b/Tests/PresentMonTests.cpp index 8840140d0..65cde266b 100644 --- a/Tests/PresentMonTests.cpp +++ b/Tests/PresentMonTests.cpp @@ -4,8 +4,6 @@ #include #include "PresentMonTests.h" -#include - bool EnsureDirectoryCreated(std::wstring path) { for (auto i = path.find(L'\\');; i = path.find(L'\\', i + 1)) { @@ -151,7 +149,7 @@ int wmain( // Check for additional test directory from environment variable // This allows developers to specify their own test directories without // modifying the source code. Set PRESENTMON_ADDITIONAL_TEST_DIR environment - // variable or use a .runsettings.user file (see template). + // variable or use an attached runsettings file (see template). std::wstring additionalTestDir; wchar_t* envTestDir = nullptr; size_t envTestDirLen = 0; @@ -212,7 +210,12 @@ int wmain( // If there is a help argument print help information before GoogleTest // does. auto help = false; + auto listTests = false; for (int i = 1; i < argc; ++i) { + if (_wcsicmp(argv[i], L"--gtest_list_tests") == 0) { + listTests = true; + } + if (_wcsicmp(argv[i], L"--help") == 0 || _wcsicmp(argv[i], L"-h") == 0 || _wcsicmp(argv[i], L"-?") == 0 || @@ -237,13 +240,12 @@ int wmain( } } - // Note: InitGoogleTest() will remove the arguments it recognizes. + // Let GoogleTest consume its arguments before validating PresentMonTests-specific options. testing::InitGoogleTest(&argc, argv); if (help) { return 0; } - // Parse remaining command line arguments for custom commands. wchar_t* presentMonPathArg = nullptr; wchar_t* goldDirArg = nullptr; wchar_t* optTestDirArg = nullptr; @@ -260,7 +262,7 @@ int wmain( continue; } - if (_wcsnicmp(argv[i], L"--opttestdir=", 10) == 0) { + if (_wcsnicmp(argv[i], L"--opttestdir=", 13) == 0) { optTestDirArg = argv[i] + 13; continue; } @@ -308,7 +310,7 @@ int wmain( if (!CheckPath("--presentmon", &PresentMon::exePath_, presentMonPathArg, false, nullptr) || !CheckPath("--golddir", &goldDir, goldDirArg, true, &goldDirExists) || - !CheckPath("--outdir", &outDir_, outDirArg, true, &outDirExists) || + (!listTests && !CheckPath("--outdir", &outDir_, outDirArg, true, &outDirExists)) || (hasOptTestDir && !CheckPath("--opttestdir", &optTestDir, optTestDirArg, true, &optTestDirExists))) { return 1; } @@ -323,9 +325,13 @@ int wmain( // Add tests from optional test directory if specified (command line) if (hasOptTestDir) { - printf("INFO: Using optional test directory from command line: %ls\n", optTestDir.c_str()); + if (!listTests) { + printf("INFO: Using optional test directory from command line: %ls\n", optTestDir.c_str()); + } if (optTestDirExists) { - printf("INFO: Adding tests from test directory: %ls\n", optTestDir.c_str()); + if (!listTests) { + printf("INFO: Adding tests from test directory: %ls\n", optTestDir.c_str()); + } AddGoldEtlCsvTests(optTestDir, optTestDir.size()); } else { fprintf(stderr, "warning: optional test directory does not exist: %ls\n", optTestDir.c_str()); @@ -335,7 +341,9 @@ int wmain( // Add tests from additional test directory (environment variable) if (!additionalTestDir.empty()) { - printf("INFO: Using additional test directory from environment: %ls\n", additionalTestDir.c_str()); + if (!listTests) { + printf("INFO: Using additional test directory from environment: %ls\n", additionalTestDir.c_str()); + } // Ensure directory ends with backslash if (additionalTestDir.back() != L'\\') { additionalTestDir += L'\\'; @@ -343,34 +351,38 @@ int wmain( auto attr = GetFileAttributes(additionalTestDir.c_str()); if (attr != INVALID_FILE_ATTRIBUTES && (attr & FILE_ATTRIBUTE_DIRECTORY)) { - printf("INFO: Adding tests from additional directory: %ls\n", additionalTestDir.c_str()); + if (!listTests) { + printf("INFO: Adding tests from additional directory: %ls\n", additionalTestDir.c_str()); + } AddGoldEtlCsvTests(additionalTestDir, additionalTestDir.size()); } else { fprintf(stderr, "warning: additional test directory does not exist: %ls\n", additionalTestDir.c_str()); } } - if (!hasOptTestDir && additionalTestDir.empty()) { + if (!listTests && !hasOptTestDir && additionalTestDir.empty()) { printf("INFO: No optional test directories specified.\n"); } - if (outDirExists) { - if (deleteOutDir) { - fprintf(stderr, "warning: output directory already exists: %ls\n", outDir_.c_str()); - fprintf(stderr, " Continuing, but directory won't be deleted afterwards. Use the\n"); - fprintf(stderr, " --nodelete argument, or delete directory before running, to remove this\n"); - fprintf(stderr, " warning.\n"); - deleteOutDir = false; + if (!listTests) { + if (outDirExists) { + if (deleteOutDir) { + fprintf(stderr, "warning: output directory already exists: %ls\n", outDir_.c_str()); + fprintf(stderr, " Continuing, but directory won't be deleted afterwards. Use the\n"); + fprintf(stderr, " --nodelete argument, or delete directory before running, to remove this\n"); + fprintf(stderr, " warning.\n"); + deleteOutDir = false; + } + } else { + EnsureDirectoryCreated(outDir_); } - } else { - EnsureDirectoryCreated(outDir_); } // Run all the tests int result = RUN_ALL_TESTS(); // If there were any failures, disable deleting of the output directory. - if (deleteOutDir && ::testing::UnitTest::GetInstance()->failed_test_count() > 0) { + if (!listTests && deleteOutDir && ::testing::UnitTest::GetInstance()->failed_test_count() > 0) { fprintf(stderr, "warning: not deleting output directory since there were errors\n"); fprintf(stderr, " %ls\n", outDir_.c_str()); deleteOutDir = false; @@ -378,7 +390,7 @@ int wmain( // If we created the output directory and the user told us to, delete the // output directory. - if (deleteOutDir) { + if (!listTests && deleteOutDir) { DeleteDirectory(outDir_); } diff --git a/Tests/PresentMonTests.generated.runsettings.in b/Tests/PresentMonTests.generated.runsettings.in new file mode 100644 index 000000000..198451e34 --- /dev/null +++ b/Tests/PresentMonTests.generated.runsettings.in @@ -0,0 +1,25 @@ + + + + + @PMON_PRESENTMON_TESTS_TARGET_PLATFORM@ + 1 + +@PMON_PRESENTMON_TESTS_ENV_XML@ + + + + + + 1 + MethodLevel + + + + + true + + @PMON_PRESENTMON_TESTS_DISCOVERY_ARGS@ + + + diff --git a/Tests/PresentMonTests.runsettings b/Tests/PresentMonTests.runsettings new file mode 100644 index 000000000..706b8837f --- /dev/null +++ b/Tests/PresentMonTests.runsettings @@ -0,0 +1,24 @@ + + + + + 1 + + ..\..\Tests\AuxData\Data + + + + + + 1 + MethodLevel + + + + + true + + --golddir="..\..\Tests\Gold" --opttestdir="..\..\Tests\AuxData\Data" + + + diff --git a/Tests/PresentMonTests.runsettings.template b/Tests/PresentMonTests.runsettings.template index 90e7a0741..d432164f5 100644 --- a/Tests/PresentMonTests.runsettings.template +++ b/Tests/PresentMonTests.runsettings.template @@ -1,20 +1,31 @@ - - x64 + 1 C:\path\to\your\test\directory - + + + + 1 + MethodLevel + + + true + + + --golddir="..\..\Tests\Gold" --opttestdir="C:\path\to\your\test\directory" + - \ No newline at end of file + diff --git a/Tests/PresentMonTests.vcxproj b/Tests/PresentMonTests.vcxproj index 94697371e..6f3d1c8f7 100644 --- a/Tests/PresentMonTests.vcxproj +++ b/Tests/PresentMonTests.vcxproj @@ -41,6 +41,8 @@ 10.0 10.0 10.0 + $(MSBuildProjectDirectory)\PresentMonTests.runsettings + $(MSBuildProjectDirectory)\PresentMonTests.local.runsettings @@ -333,5 +335,6 @@ + - \ No newline at end of file + diff --git a/Tests/README.md b/Tests/README.md index 6acaec59f..daeaad8bf 100644 --- a/Tests/README.md +++ b/Tests/README.md @@ -2,10 +2,27 @@ PresentMon testing is primarily done by having a specific PresentMon build analyze a collection of ETW logs and ensuring its output matches the expected result. The PresentMonTests application will add a test for every matching `test_case_.etl`/.csv pair it finds under a specified root directory. -`Tools\run_tests.cmd` will build all configurations of PresentMon, and use PresentMonTests to validate the x86 and x64 builds using the contents of the Tests\Gold directory. +For the classic Visual Studio build, `Tools\run_tests.cmd` builds all +configurations of PresentMon and uses PresentMonTests to validate the x86 and +x64 builds with the contents of `Tests\Gold`. The usual automated path mirrors Visual Studio Test Explorer: `vstest.console` on `PresentMonAPI2Tests` and `PresentMonUnitTests`, plus the `PresentMonTests` gtest executable against Debug x64 outputs under `build\Debug\`. +For CMake builds, CTest discovers every framework test as a separate flat entry. +List them with: + +```powershell +ctest --test-dir build -C Debug -N +ctest --test-dir build -C Debug -L deterministic --output-on-failure +``` + +Names preserve the familiar executable, suite, and method components, for +example `PresentMonUnitTests.AlgorithmTests.TestExtremeQueue.MaxAddEmpty`. +Running CTest normally prints one pass/fail row per method. Use `-R` to select a +method or suite, and `-L deterministic` to exclude environment-dependent cases. +The complete configure, build, and test workflow is documented in +[BUILDING.md](../BUILDING.md#run-tests-with-ctest). + #### PresentMonTestEtls Coverage @@ -49,16 +66,25 @@ The following expected cases are currently missing (WIP): - [2,17-23] All Windows7 paths - [30] Non-Win7 Microsoft_Windows_Dwm_Core::FlipChain_(Pending|Complete|Dirty) with previous Microsoft_Windows_Dwm_Core::PresentHistory[Detailed]::Start -## Adding Custom Test Directories +## Default and Custom Test Directories + +The default test directory is `Tests\AuxData\Data`. Building or running an +ETL-dependent test checks the pinned auxiliary data and restores it if needed. +The checked-in and CMake-generated Visual Studio projects automatically attach +the corresponding runsettings file. -To add your own test directory with ETL files and gold CSV files: +To replace the default with your own directory of ETL files and gold CSV files: ### Method 1: Using local runsettings file (Recommended) -1. Copy `PresentMonTests.runsettings.template` to `PresentMonTests.local.runsettings` -2. Edit the `PRESENTMON_ADDITIONAL_TEST_DIR` path to point to your test directory -3. In Visual Studio, go to **Test** → **Configure Run Settings** → **Select Solution Wide runsettings File** -4. Select your `PresentMonTests.local.runsettings` file +1. Copy `PresentMonTests.runsettings.template` to `PresentMonTests.local.runsettings`. +2. Replace both example custom paths with your directory. +3. Refresh Test Explorer. The root solution selects the local file automatically. + A CMake build detects the change, regenerates its attached runsettings file, + and uses the same custom path for CTest. + +No test recompilation is required for a directory-only change. CMake users can +instead configure with `-DPMON_PRESENTMON_TESTS_CUSTOM_DIR=...`. ### Method 2: Command Line Parameter diff --git a/Tests/auxdata.md b/Tests/auxdata.md index 46cdef34c..6d1f6d884 100644 --- a/Tests/auxdata.md +++ b/Tests/auxdata.md @@ -2,17 +2,48 @@ Auxiliary test data is stored outside the main repository in the `planetchili/IPMAuxTestData` repository. The main repository pins the expected auxiliary data commit in `Tests\aux-data.lock.json`. -## Bootstrap Path +The canonical runtime location is always `Tests\AuxData\Data`. Builds of the +ETL-dependent test projects and their CTest entries validate this data through +the Python auxiliary-data tool. CMake configuration restores the pinned data +automatically when it is absent or stale. No network access occurs when the +local data is already valid. -From the repository root, run: +## Normal CMake Path + +The examples use the Windows `py` launcher. On a machine with only +`python.exe`, use `python` wherever `py -3` appears. After installing the Python +requirements, configure and build normally: + +```powershell +> py -3 -m pip install -r requirements.txt +> cmake -B build -S . -G "Visual Studio 17 2022" +> cmake --build build --config Debug +``` + +For Visual Studio 2026 (Experimental), use its distinct binary tree: ```powershell -> .\bootstrap.ps1 +> cmake -B build-vs2026 -S . -G "Visual Studio 18 2026" +> cmake --build build-vs2026 --config Debug ``` -The bootstrap script runs `Tests\pull-aux.ps1`, which clones `IPMAuxTestData` into `Tests\AuxData` if needed, checks out the commit pinned by `Tests\aux-data.lock.json`, downloads ETL release assets listed by the auxiliary manifest, extracts them, and verifies their hashes. +CMake invokes `Scripts\auxdata.py` during configuration and wires the same +verification into the affected build and CTest targets. The tool clones +`IPMAuxTestData` into `Tests\AuxData` if needed, checks out the commit pinned by +`Tests\aux-data.lock.json`, downloads release assets listed by the auxiliary +manifest, extracts them, and verifies their hashes. -## Manual Pull +To restore or verify only the auxiliary data, run: + +```powershell +> py -3 Scripts\auxdata.py restore +> py -3 Scripts\auxdata.py verify +``` + +Downloaded archives are cached under `build\ThirdParty\downloads\auxdata` and +are shared by all CMake build directories. + +## Legacy Manual Pull To refresh only the auxiliary test data, run: @@ -30,6 +61,9 @@ By default, the script uses: The auxiliary repository is intentionally separate from the main repository. Do not commit the downloaded ETL payloads to the main repository. +These PowerShell scripts remain available while the classic DevStudio build is +supported. They are not used by the CMake build graph. + ## Updating Auxiliary Data Maintainers updating auxiliary test data should work in `Tests\AuxData`, then run: diff --git a/Tests/ensure-aux.ps1 b/Tests/ensure-aux.ps1 new file mode 100644 index 000000000..a28e194dc --- /dev/null +++ b/Tests/ensure-aux.ps1 @@ -0,0 +1,115 @@ +param( + [string]$MainRepoPath = ".\", + [string]$AuxDataRepoPath = ".\AuxData", + [string]$DataSubdirName = "Data", + [string]$LockFile = "aux-data.lock.json" +) + +$ErrorActionPreference = "Stop" + +function Resolve-InputPath([string]$Path, [string]$BasePath) { + if ([System.IO.Path]::IsPathRooted($Path)) { + return [System.IO.Path]::GetFullPath($Path) + } + return [System.IO.Path]::GetFullPath((Join-Path $BasePath $Path)) +} + +function Test-AuxDataReady( + [string]$ResolvedMainRepoPath, + [string]$ResolvedAuxDataRepoPath, + [string]$ResolvedLockFile, + [string]$ResolvedDataSubdirName +) { + if (-not (Test-Path -LiteralPath (Join-Path $ResolvedAuxDataRepoPath ".git"))) { + return $false + } + + $lockFilePath = Join-Path $ResolvedMainRepoPath $ResolvedLockFile + if (-not (Test-Path -LiteralPath $lockFilePath)) { + throw "Auxiliary data lock file not found: $lockFilePath" + } + $lock = Get-Content -LiteralPath $lockFilePath | ConvertFrom-Json + $pinnedCommit = "$($lock.pinnedCommitHash)".Trim() + if (-not $pinnedCommit) { + throw "Auxiliary data lock file has no pinnedCommitHash: $lockFilePath" + } + + $currentCommit = & git -C $ResolvedAuxDataRepoPath rev-parse HEAD 2>$null + if ($LASTEXITCODE -ne 0 -or "$currentCommit".Trim() -ine $pinnedCommit) { + return $false + } + + $manifestRef = "${pinnedCommit}:etl-manifest.json" + $manifestText = & git -C $ResolvedAuxDataRepoPath show $manifestRef 2>$null + if ($LASTEXITCODE -ne 0 -or -not $manifestText) { + return $false + } + $manifest = ($manifestText -join [System.Environment]::NewLine) | ConvertFrom-Json + if (-not $manifest.etlFiles) { + return $false + } + + $dataRoot = Join-Path $ResolvedAuxDataRepoPath $ResolvedDataSubdirName + foreach ($entry in $manifest.etlFiles) { + $etlPath = Join-Path $dataRoot $entry.name + if (-not (Test-Path -LiteralPath $etlPath)) { + return $false + } + $actualHash = (Get-FileHash -LiteralPath $etlPath -Algorithm SHA256).Hash + if ($actualHash -ine $entry.hash) { + return $false + } + } + + return $true +} + +$resolvedMainRepoPath = Resolve-InputPath $MainRepoPath $PSScriptRoot +$resolvedAuxDataRepoPath = Resolve-InputPath $AuxDataRepoPath $PSScriptRoot + +$hashAlgorithm = [System.Security.Cryptography.SHA256]::Create() +try { + $pathBytes = [System.Text.Encoding]::UTF8.GetBytes($resolvedAuxDataRepoPath.ToLowerInvariant()) + $mutexHash = -join ($hashAlgorithm.ComputeHash($pathBytes) | ForEach-Object { $_.ToString("x2") }) +} +finally { + $hashAlgorithm.Dispose() +} +$mutex = New-Object System.Threading.Mutex($false, "PresentMonAuxDataEnsure-$mutexHash") +$mutexAcquired = $false + +try { + try { + $mutexAcquired = $mutex.WaitOne([System.TimeSpan]::FromMinutes(30)) + } + catch [System.Threading.AbandonedMutexException] { + $mutexAcquired = $true + } + if (-not $mutexAcquired) { + throw "Timed out waiting for another auxiliary test data restore to finish." + } + + if (Test-AuxDataReady $resolvedMainRepoPath $resolvedAuxDataRepoPath $LockFile $DataSubdirName) { + Write-Host "Pinned auxiliary test data is present and valid." + } + else { + Write-Host "Auxiliary test data is missing, stale, or invalid. Restoring the pinned data." + & (Join-Path $PSScriptRoot "pull-aux.ps1") ` + -MainRepoPath $resolvedMainRepoPath ` + -AuxDataRepoPath $resolvedAuxDataRepoPath ` + -DataSubdirName $DataSubdirName ` + -LockFile $LockFile + + if (-not (Test-AuxDataReady $resolvedMainRepoPath $resolvedAuxDataRepoPath $LockFile $DataSubdirName)) { + throw "Auxiliary test data restore completed without producing a valid pinned data set." + } + + Write-Host "Pinned auxiliary test data was restored and validated." + } +} +finally { + if ($mutexAcquired) { + $mutex.ReleaseMutex() + } + $mutex.Dispose() +} diff --git a/ETLTrimmer/.gitignore b/Tools/ETLTrimmer/.gitignore similarity index 100% rename from ETLTrimmer/.gitignore rename to Tools/ETLTrimmer/.gitignore diff --git a/Tools/ETLTrimmer/CMakeLists.txt b/Tools/ETLTrimmer/CMakeLists.txt new file mode 100644 index 000000000..dbb281dcb --- /dev/null +++ b/Tools/ETLTrimmer/CMakeLists.txt @@ -0,0 +1,39 @@ +include(CheckIncludeFileCXX) +check_include_file_cxx(atlbase.h PMON_ETLTRIMMER_HAS_ATL) +if(NOT PMON_ETLTRIMMER_HAS_ATL) + unset(PMON_ETLTRIMMER_HAS_ATL CACHE) + message( + FATAL_ERROR + "ETLTrimmer requires the Visual Studio Installer component " + "\"C++ ATL for latest ${PMON_VS_TOOLSET} build tools (x86 & x64)\". " + "Install that component and configure again, or configure with " + "-DPMON_BUILD_TOOLS=OFF to intentionally omit developer tools. " + "Selected Visual Studio instance: ${PMON_VS_INSTANCE_ROOT}" + ) +endif() + +add_executable(ETLTrimmer main.cpp) +target_compile_definitions(ETLTrimmer PRIVATE _CONSOLE) +target_compile_options( + ETLTrimmer + PRIVATE + $<$:/guard:cf> +) +target_link_options( + ETLTrimmer + PRIVATE + $<$:/guard:cf> +) +target_link_libraries( + ETLTrimmer + PRIVATE + pmon::build_options + pmon::warnings_default + pmon::release_link_policy + pmon::common_utilities + pmon::present_data + ole32 + oleaut32 + tdh +) +set_target_properties(ETLTrimmer PROPERTIES FOLDER "Tools") diff --git a/ETLTrimmer/CliOptions.h b/Tools/ETLTrimmer/CliOptions.h similarity index 96% rename from ETLTrimmer/CliOptions.h rename to Tools/ETLTrimmer/CliOptions.h index c942fcdc5..a3c884f26 100644 --- a/ETLTrimmer/CliOptions.h +++ b/Tools/ETLTrimmer/CliOptions.h @@ -1,5 +1,5 @@ #pragma once -#include "../IntelPresentMon/CommonUtilities/cli/CliFramework.h" +#include "../../IntelPresentMon/CommonUtilities/cli/CliFramework.h" #include namespace clio diff --git a/ETLTrimmer/ETLTrimmer.args.json b/Tools/ETLTrimmer/ETLTrimmer.args.json similarity index 100% rename from ETLTrimmer/ETLTrimmer.args.json rename to Tools/ETLTrimmer/ETLTrimmer.args.json diff --git a/ETLTrimmer/ETLTrimmer.vcxproj b/Tools/ETLTrimmer/ETLTrimmer.vcxproj similarity index 93% rename from ETLTrimmer/ETLTrimmer.vcxproj rename to Tools/ETLTrimmer/ETLTrimmer.vcxproj index 5444f62fa..b881d40ed 100644 --- a/ETLTrimmer/ETLTrimmer.vcxproj +++ b/Tools/ETLTrimmer/ETLTrimmer.vcxproj @@ -38,13 +38,13 @@ - - + + - - + + @@ -96,10 +96,10 @@ - + {08a704d8-ca1c-45e9-8ede-542a1a43b53e} - + {892028e5-32f6-45fc-8ab2-90fcbcac4bf6} diff --git a/ETLTrimmer/ETLTrimmer.vcxproj.filters b/Tools/ETLTrimmer/ETLTrimmer.vcxproj.filters similarity index 100% rename from ETLTrimmer/ETLTrimmer.vcxproj.filters rename to Tools/ETLTrimmer/ETLTrimmer.vcxproj.filters diff --git a/ETLTrimmer/main.cpp b/Tools/ETLTrimmer/main.cpp similarity index 96% rename from ETLTrimmer/main.cpp rename to Tools/ETLTrimmer/main.cpp index b821046ae..9004b6910 100644 --- a/ETLTrimmer/main.cpp +++ b/Tools/ETLTrimmer/main.cpp @@ -1,6 +1,6 @@ -#include "../IntelPresentMon/CommonUtilities/win/WinAPI.h" -#include "../IntelPresentMon/CommonUtilities/win/Utilities.h" -#include "../IntelPresentMon/CommonUtilities/Hash.h" +#include "../../IntelPresentMon/CommonUtilities/win/WinAPI.h" +#include "../../IntelPresentMon/CommonUtilities/win/Utilities.h" +#include "../../IntelPresentMon/CommonUtilities/Hash.h" #include #include #include @@ -16,12 +16,12 @@ #include #include #include "CliOptions.h" -#include "../PresentData/PresentMonTraceSession.hpp" -#include "../PresentData/PresentMonTraceConsumer.hpp" -#include "../PresentData/ETW/Microsoft_Windows_EventMetadata.h" -#include "../PresentData/ETW/NT_Process.h" -#include "../PresentData/ETW/Microsoft_Windows_Kernel_Process.h" -#include "../PresentData/ETW/Microsoft_Windows_DxgKrnl.h" +#include "../../PresentData/PresentMonTraceSession.hpp" +#include "../../PresentData/PresentMonTraceConsumer.hpp" +#include "../../PresentData/ETW/Microsoft_Windows_EventMetadata.h" +#include "../../PresentData/ETW/NT_Process.h" +#include "../../PresentData/ETW/Microsoft_Windows_Kernel_Process.h" +#include "../../PresentData/ETW/Microsoft_Windows_DxgKrnl.h" namespace rn = std::ranges; namespace vi = rn::views; diff --git a/Tools/Reflector/CMakeLists.txt b/Tools/Reflector/CMakeLists.txt new file mode 100644 index 000000000..43ee16535 --- /dev/null +++ b/Tools/Reflector/CMakeLists.txt @@ -0,0 +1,88 @@ +cmake_minimum_required(VERSION 4.2.2) + +if(NOT WIN32) + message(FATAL_ERROR "PresentMon Reflector supports Windows only.") +endif() + +cmake_path(SET PMON_REPO_ROOT NORMALIZE "${CMAKE_CURRENT_SOURCE_DIR}/../..") +list(APPEND CMAKE_MODULE_PATH "${PMON_REPO_ROOT}/cmake") + +include(PresentMonVersion) +pmon_read_version("${PMON_REPO_ROOT}/Version.props") + +project( + PresentMonReflector + VERSION "${PMON_VERSION}" + DESCRIPTION "Intel PresentMon reflection code generator" +) + +if(NOT CMAKE_SIZEOF_VOID_P EQUAL 8) + message( + FATAL_ERROR + "PresentMon Reflector requires an x64 generator platform (pass -A x64)." + ) +endif() + +find_program( + PMON_DOTNET_EXECUTABLE + dotnet + DOC "The .NET SDK dotnet CLI" +) +if(NOT PMON_DOTNET_EXECUTABLE) + message( + FATAL_ERROR + "The .NET SDK (dotnet) is required but was not found on PATH. " + "Install the .NET 8 SDK and retry configure." + ) +endif() + +set_property(GLOBAL PROPERTY USE_FOLDERS ON) + +if(CMAKE_CONFIGURATION_TYPES) + set( + CMAKE_CONFIGURATION_TYPES + "Debug;Release" + CACHE STRING + "Supported build configurations" + FORCE + ) +endif() + +set(PMON_REFLECTOR_PROJECT "${CMAKE_CURRENT_SOURCE_DIR}/Reflector.csproj") + +add_custom_target( + pmon_reflector + COMMAND + "${PMON_DOTNET_EXECUTABLE}" + build + "${PMON_REFLECTOR_PROJECT}" + -c + $,Debug,Release> + WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}" + COMMENT "Building Reflector (.NET)" + VERBATIM +) +set_target_properties(pmon_reflector PROPERTIES FOLDER "Tools") + +add_custom_target( + pmon_reflector_codegen + COMMAND + "${PMON_DOTNET_EXECUTABLE}" + run + --project + "${PMON_REFLECTOR_PROJECT}" + -c + $,Debug,Release> + WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}" + COMMENT "Running Reflector code generation" + VERBATIM +) +set_target_properties(pmon_reflector_codegen PROPERTIES FOLDER "Tools") + +message(STATUS "") +message(STATUS "PresentMon Reflector configuration") +message(STATUS " Version: ${PMON_VERSION}") +message(STATUS " Generator: ${CMAKE_GENERATOR}") +message(STATUS " .NET SDK: ${PMON_DOTNET_EXECUTABLE}") +message(STATUS "") +message(STATUS "Targets: pmon_reflector, pmon_reflector_codegen") diff --git a/Reflector/Program.cs b/Tools/Reflector/Program.cs similarity index 90% rename from Reflector/Program.cs rename to Tools/Reflector/Program.cs index 930462cb8..2e9ad8e4c 100644 --- a/Reflector/Program.cs +++ b/Tools/Reflector/Program.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.IO; using System.Linq; using CppAst; @@ -82,9 +83,15 @@ static void Main(string[] args) return; } var headerDirectory = Path.GetDirectoryName(headerPath)!; - Console.WriteLine($"Parsing {target.DisplayName}: {target.IncludePath}"); - - var compilation = CppParser.ParseFile(ToClangPath_(headerPath)); + Console.WriteLine( + $"Parsing {target.DisplayName}: {target.IncludePath} (CppAst/libclang; may take several minutes on large headers)"); + Console.Out.Flush(); + + var parseStopwatch = Stopwatch.StartNew(); + var compilation = CppParser.ParseFile(ToClangPath_(headerPath), CreateParserOptions_()); + parseStopwatch.Stop(); + Console.WriteLine( + $"Finished parsing {target.DisplayName} in {parseStopwatch.Elapsed.TotalSeconds:F1}s."); if (compilation.HasErrors) { Console.WriteLine($"Parsing failed for {target.DisplayName}:"); foreach (var message in compilation.Diagnostics.Messages) { @@ -163,7 +170,7 @@ static void Main(string[] args) Console.WriteLine( $"Reachable dump surface: {typedefs.Count} rooted typedef(s), {structs.Count} type dumper(s), {enumsOut.Count} enum dumper(s)."); - var templatePath = Path.Combine(repositoryRoot, "Reflector", "StructDumpers.h.scriban"); + var templatePath = Path.Combine(GetReflectorProjectDirectory(), "StructDumpers.h.scriban"); Console.WriteLine($"Loading template: {MakeRepositoryRelativePath_(repositoryRoot, templatePath)}"); var templateText = File.ReadAllText(templatePath); var template = Template.Parse(templateText); @@ -407,20 +414,44 @@ static CppType UnwrapType(CppType type) return type; } - static string FindRepositoryRoot() + static CppParserOptions CreateParserOptions_() + { + // Vendor headers are monolithic (IGCL ~9k lines, NVAPI ~24k). Default CppAst options parse + // all comments and system includes, which can take 15+ minutes per header on Windows. + var options = new CppParserOptions().ConfigureForWindowsMsvc(CppTargetCpu.X86_64); + options.ParseComments = false; + options.ParseSystemIncludes = false; + return options; + } + + static string GetReflectorProjectDirectory() { - foreach (var candidate in new[] { Directory.GetCurrentDirectory(), AppContext.BaseDirectory }) { + foreach (var candidate in new[] { AppContext.BaseDirectory, Directory.GetCurrentDirectory() }) { var current = new DirectoryInfo(candidate); while (current != null) { - if (File.Exists(Path.Combine(current.FullName, "PresentMon.sln")) && - File.Exists(Path.Combine(current.FullName, "Reflector", "StructDumpers.h.scriban"))) { - return current.FullName; + var scribanPath = Path.Combine( + current.FullName, + "Tools", + "Reflector", + "StructDumpers.h.scriban"); + if (File.Exists(scribanPath)) { + return Path.GetDirectoryName(scribanPath)!; } current = current.Parent; } } - throw new DirectoryNotFoundException("Unable to locate repository root."); + throw new DirectoryNotFoundException("Unable to locate Tools/Reflector project directory."); + } + + static string FindRepositoryRoot() + { + var reflectorDir = GetReflectorProjectDirectory(); + var repositoryRoot = Directory.GetParent(reflectorDir)!.Parent!.FullName; + if (!File.Exists(Path.Combine(repositoryRoot, "PresentMon.sln"))) { + throw new DirectoryNotFoundException("Unable to locate repository root."); + } + return repositoryRoot; } static string ToSystemPath_(string path) diff --git a/Tools/Reflector/Properties/launchSettings.json b/Tools/Reflector/Properties/launchSettings.json new file mode 100644 index 000000000..82686d8e2 --- /dev/null +++ b/Tools/Reflector/Properties/launchSettings.json @@ -0,0 +1,7 @@ +{ + "profiles": { + "Reflector": { + "commandName": "Project" + } + } +} diff --git a/Reflector/Reflector.csproj b/Tools/Reflector/Reflector.csproj similarity index 100% rename from Reflector/Reflector.csproj rename to Tools/Reflector/Reflector.csproj diff --git a/Reflector/StructDumpers.h.scriban b/Tools/Reflector/StructDumpers.h.scriban similarity index 100% rename from Reflector/StructDumpers.h.scriban rename to Tools/Reflector/StructDumpers.h.scriban diff --git a/Tools/etw_list/CMakeLists.txt b/Tools/etw_list/CMakeLists.txt new file mode 100644 index 000000000..d876b6631 --- /dev/null +++ b/Tools/etw_list/CMakeLists.txt @@ -0,0 +1,20 @@ +add_executable(etw_list etw_list.cpp) +target_compile_definitions(etw_list PRIVATE _CONSOLE) +target_link_libraries( + etw_list + PRIVATE + pmon::build_options + pmon::warnings_strict + pmon::warnings_strict_link + pmon::release_link_policy + pmon::version_header + advapi32 + ole32 + tdh +) +set_target_properties( + etw_list + PROPERTIES + OUTPUT_NAME "etw_list-${PMON_VERSION}-${PMON_ARCH_TAG}" + FOLDER "Tools" +) diff --git a/Tools/pm_convert_csv/CMakeLists.txt b/Tools/pm_convert_csv/CMakeLists.txt new file mode 100644 index 000000000..e054d4440 --- /dev/null +++ b/Tools/pm_convert_csv/CMakeLists.txt @@ -0,0 +1,26 @@ +add_executable(pm_convert_csv pm_convert_csv.cpp) +target_compile_definitions(pm_convert_csv PRIVATE _CONSOLE) +target_link_libraries( + pm_convert_csv + PRIVATE + pmon::build_options + pmon::warnings_strict + pmon::warnings_strict_link + pmon::release_link_policy +) +set_target_properties( + pm_convert_csv + PROPERTIES + RUNTIME_OUTPUT_DIRECTORY + "${PMON_OUTPUT_ROOT}/obj/runtime/pm_convert_csv-${PMON_ARCHITECTURE}-$" + FOLDER "Tools" +) +add_custom_command( + TARGET pm_convert_csv + POST_BUILD + COMMAND + "${CMAKE_COMMAND}" -E copy_if_different + "$" + "${PMON_OUTPUT_ROOT}/$/pm_convert_csv.exe" + VERBATIM +) diff --git a/Version.props b/Version.props index 50e1db0f3..0ca433902 100644 --- a/Version.props +++ b/Version.props @@ -2,7 +2,6 @@ true - 2.6.0 2.6.0.0 $(PresentMonFileVersion) diff --git a/cmake/PresentMonAuxTestData.cmake b/cmake/PresentMonAuxTestData.cmake new file mode 100644 index 000000000..d46bdc4be --- /dev/null +++ b/cmake/PresentMonAuxTestData.cmake @@ -0,0 +1,54 @@ +include_guard(GLOBAL) + +function(pmon_configure_aux_testdata) + set(tests_dir "${PROJECT_SOURCE_DIR}/Tests") + set(aux_data_dir "${tests_dir}/AuxData/Data") + add_custom_target( + pmon_ensure_aux_testdata + COMMAND + "${PMON_PYTHON_EXECUTABLE}" + "${PROJECT_SOURCE_DIR}/Scripts/auxdata.py" + ensure + --repo-root "${PROJECT_SOURCE_DIR}" + --dependency-root "${PMON_DEPENDENCY_ROOT}" + WORKING_DIRECTORY "${tests_dir}" + COMMENT "Ensuring pinned auxiliary test data is available" + USES_TERMINAL + VERBATIM + ) + set_target_properties( + pmon_ensure_aux_testdata + PROPERTIES + EXCLUDE_FROM_DEFAULT_BUILD TRUE + FOLDER "Build" + ) + + add_custom_target( + pmon_restore_aux_testdata + COMMAND + "${PMON_PYTHON_EXECUTABLE}" + "${PROJECT_SOURCE_DIR}/Scripts/auxdata.py" + restore + --repo-root "${PROJECT_SOURCE_DIR}" + --dependency-root "${PMON_DEPENDENCY_ROOT}" + WORKING_DIRECTORY "${tests_dir}" + COMMENT "Restoring pinned auxiliary test data into Tests/AuxData" + USES_TERMINAL + VERBATIM + ) + set_target_properties( + pmon_restore_aux_testdata + PROPERTIES + EXCLUDE_FROM_DEFAULT_BUILD TRUE + FOLDER "Build" + ) + + set( + PMON_AUX_TESTDATA_DIR + "${aux_data_dir}" + CACHE INTERNAL + "Canonical pinned auxiliary test data directory" + FORCE + ) + set(PMON_AUX_TESTDATA_DIR "${aux_data_dir}" PARENT_SCOPE) +endfunction() diff --git a/cmake/PresentMonCTest.cmake b/cmake/PresentMonCTest.cmake new file mode 100644 index 000000000..dbef9b53e --- /dev/null +++ b/cmake/PresentMonCTest.cmake @@ -0,0 +1,602 @@ +include_guard(GLOBAL) + +# VSTest display-name classification validated against PresentMonAPI2Tests.dll +# (128 tests: 113 deterministic, 15 environment). +set( + PMON_API2_CTEST_ENVIRONMENT_NAME_REGEX + "RealtimeOpenSessionTest|RealtimeTrackProcessTest|RealtimeFrameMetricsTest|ApplicationLaunch|TrackUntrack|TrackPresenter|TargetTracked|UntrackOnClose|UntrackOnMurder|OpenServiceTest|DynamicQueryWithoutTracked|ServiceCrash|EtwFlush|TelemetryPeriod|FrameStoreRealtime|FrameStorePaced|FrameStoreBackpressured|FrameStorePlayback|LegacyBackpressured" +) + +function(pmon_normalize_test_data_path input_path output_variable) + file(TO_CMAKE_PATH "${input_path}" normalized_path) + get_filename_component( + normalized_path + "${normalized_path}" + ABSOLUTE + BASE_DIR "${PROJECT_SOURCE_DIR}/Tests" + ) + set(${output_variable} "${normalized_path}" PARENT_SCOPE) +endfunction() + +function(pmon_configure_presentmon_test_data_directory) + set( + PMON_PRESENTMON_TESTS_CUSTOM_DIR + "" + CACHE PATH + "Custom PresentMon ETL/CSV test directory. Empty uses the pinned Tests/AuxData/Data directory." + ) + + pmon_normalize_test_data_path("${PMON_AUX_TESTDATA_DIR}" canonical_data_dir) + string(TOLOWER "${canonical_data_dir}" canonical_data_dir_lower) + + set(custom_dir "") + set(custom_dir_source "") + if(PMON_PRESENTMON_TESTS_CUSTOM_DIR) + set(custom_dir "${PMON_PRESENTMON_TESTS_CUSTOM_DIR}") + set(custom_dir_source "PMON_PRESENTMON_TESTS_CUSTOM_DIR") + endif() + + file( + GLOB local_runsettings_files + CONFIGURE_DEPENDS + "${PROJECT_SOURCE_DIR}/Tests/PresentMonTests.local.runsettings" + ) + if(NOT custom_dir AND local_runsettings_files) + list(GET local_runsettings_files 0 runsettings) + file(READ "${runsettings}" runsettings_text) + string( + REGEX MATCH + "([^<]+)" + _ + "${runsettings_text}" + ) + if(CMAKE_MATCH_1) + string(STRIP "${CMAKE_MATCH_1}" local_dir) + if(local_dir) + pmon_normalize_test_data_path("${local_dir}" local_dir) + string(TOLOWER "${local_dir}" local_dir_lower) + if(NOT local_dir_lower STREQUAL canonical_data_dir_lower) + set(custom_dir "${local_dir}") + set(custom_dir_source "PresentMonTests.local.runsettings") + endif() + endif() + endif() + endif() + + if(custom_dir) + pmon_normalize_test_data_path("${custom_dir}" custom_dir) + endif() + if(custom_dir AND NOT IS_DIRECTORY "${custom_dir}") + message( + WARNING + "PresentMon custom test directory is missing: ${custom_dir}. Falling back to ${canonical_data_dir}." + ) + set(custom_dir "") + set(custom_dir_source "") + endif() + + # PresentMonTests always runs Tests/Gold plus exactly one optional directory. That + # optional slot holds the pinned AuxData by default; a custom directory redirects the + # slot rather than adding a second one, so the AuxData cases stop being registered. + if(custom_dir) + set(test_data_dir "${custom_dir}") + message( + STATUS + "PresentMon test data: ${test_data_dir} (${custom_dir_source}; replaces pinned AuxData)" + ) + file(GLOB custom_etl_files "${custom_dir}/test_case_*.etl") + if(NOT custom_etl_files) + message( + WARNING + "PresentMon custom test directory ${custom_dir} contains no " + "test_case_.etl files, so it will contribute no tests and the " + "pinned AuxData cases are no longer registered either. Only Tests/Gold " + "cases will run. Name ETL files test_case_.etl with matching gold " + "CSVs, or clear PMON_PRESENTMON_TESTS_CUSTOM_DIR to restore AuxData." + ) + endif() + else() + set(test_data_dir "${canonical_data_dir}") + message(STATUS "PresentMon test data: ${test_data_dir} (pinned AuxData default)") + endif() + + set(PMON_PRESENTMON_TESTS_DATA_DIR "${test_data_dir}" PARENT_SCOPE) +endfunction() + +function(pmon_configure_presentmon_test_runsettings) + set(gold_dir "${PROJECT_SOURCE_DIR}/Tests/Gold") + set(test_data_dir "${PMON_PRESENTMON_TESTS_DATA_DIR}") + foreach(xml_variable IN ITEMS gold_dir test_data_dir) + string(REPLACE "&" "&" ${xml_variable}_xml "${${xml_variable}}") + string(REPLACE "<" "<" ${xml_variable}_xml "${${xml_variable}_xml}") + string(REPLACE ">" ">" ${xml_variable}_xml "${${xml_variable}_xml}") + endforeach() + set( + discovery_args + "--golddir="${gold_dir_xml}" --opttestdir="${test_data_dir_xml}"" + ) + set( + env_xml + " ${test_data_dir_xml}" + ) + if(PMON_ARCHITECTURE STREQUAL "x64") + set(target_platform "x64") + else() + set(target_platform "x86") + endif() + + set( + generated_runsettings + "${CMAKE_BINARY_DIR}/PresentMonTests.generated.runsettings" + ) + set(PMON_PRESENTMON_TESTS_DISCOVERY_ARGS "${discovery_args}") + set(PMON_PRESENTMON_TESTS_ENV_XML "${env_xml}") + set(PMON_PRESENTMON_TESTS_TARGET_PLATFORM "${target_platform}") + configure_file( + "${PROJECT_SOURCE_DIR}/Tests/PresentMonTests.generated.runsettings.in" + "${generated_runsettings}" + @ONLY + ) + set(PMON_PRESENTMON_TESTS_GENERATED_RUNSETTINGS "${generated_runsettings}" CACHE FILEPATH + "Google Test Adapter runsettings for the CMake build tree" FORCE + ) + set(PMON_PRESENTMON_TESTS_GENERATED_RUNSETTINGS "${generated_runsettings}" PARENT_SCOPE) + message(STATUS "CMake Test Explorer runsettings: ${generated_runsettings} (attached automatically)") +endfunction() + +function(pmon_apply_test_runsettings runsettings_path) + foreach(target_name IN ITEMS PresentMonTests PresentMonUnitTests PresentMonAPI2Tests) + if(TARGET ${target_name}) + set_property( + TARGET ${target_name} + PROPERTY VS_GLOBAL_RunSettingsFilePath "${runsettings_path}" + ) + endif() + endforeach() +endfunction() + +function(pmon_apply_presentmon_tests_vs_debugger target_name) + if(NOT TARGET ${target_name}) + return() + endif() + + set( + debugger_args + "--presentmon=${PMON_OUTPUT_ROOT}/$(Configuration)/PresentMon-${PMON_VERSION}-${PMON_ARCH_TAG}.exe" + "--golddir=${PROJECT_SOURCE_DIR}/Tests/Gold" + ) + list(APPEND debugger_args "--opttestdir=${PMON_PRESENTMON_TESTS_DATA_DIR}") + list(JOIN debugger_args " " debugger_args_line) + + set_target_properties( + ${target_name} + PROPERTIES + VS_DEBUGGER_WORKING_DIRECTORY "${PMON_OUTPUT_ROOT}/$(Configuration)" + VS_DEBUGGER_COMMAND_ARGUMENTS "${debugger_args_line}" + ) +endfunction() + +# Resolves vstest.console.exe from the Visual Studio instance CMake selected and +# published as PMON_VS_INSTANCE_ROOT. No other Visual Studio installation is +# consulted. PMON_VSTEST_CONSOLE is a PresentMon-owned internal result and is +# recomputed on every configure rather than honored from the cache. +function(pmon_find_vstest_console output_variable) + set( + vstest_console + "${PMON_VS_INSTANCE_ROOT}/Common7/IDE/CommonExtensions/Microsoft/TestWindow/vstest.console.exe" + ) + if(NOT EXISTS "${vstest_console}") + message( + FATAL_ERROR + "vstest.console.exe not found:\n" + " ${vstest_console}\n" + "Selected Visual Studio instance:\n" + " ${PMON_VS_INSTANCE_ROOT}\n" + "Install the C++ Test Tools workload component in that instance. " + "PresentMon does not substitute another Visual Studio installation." + ) + endif() + + set(PMON_VSTEST_CONSOLE "${vstest_console}" CACHE INTERNAL "Path to vstest.console.exe") + set(${output_variable} "${vstest_console}" PARENT_SCOPE) +endfunction() + +function(pmon_register_vstest_leaf_tests) + set(options) + set( + oneValueArgs + TARGET + TEST_PREFIX + ENVIRONMENT_NAME_REGEX + TEST_ENVIRONMENT + FIXTURES_REQUIRED + RESOURCE_LOCK + ) + set(multiValueArgs BASE_LABELS DEFAULT_LABELS ENVIRONMENT_LABELS) + cmake_parse_arguments( + ARG + "${options}" + "${oneValueArgs}" + "${multiValueArgs}" + ${ARGN} + ) + if(NOT ARG_TARGET OR NOT ARG_TEST_PREFIX) + message( + FATAL_ERROR + "pmon_register_vstest_leaf_tests requires TARGET and TEST_PREFIX." + ) + endif() + if(NOT TARGET ${ARG_TARGET}) + message( + FATAL_ERROR + "pmon_register_vstest_leaf_tests TARGET ${ARG_TARGET} does not exist." + ) + endif() + + pmon_find_vstest_console(vstest_console) + list(JOIN ARG_BASE_LABELS "," base_labels) + list(JOIN ARG_DEFAULT_LABELS "," default_labels) + list(JOIN ARG_ENVIRONMENT_LABELS "," environment_labels) + + set(discovery_dir "${CMAKE_BINARY_DIR}/vstest-discovery") + file(MAKE_DIRECTORY "${discovery_dir}") + set(include_base "${discovery_dir}/${ARG_TARGET}") + set(ctest_include_file "${include_base}-$-include.cmake") + set(discovered_tests_file "${include_base}-$-tests.cmake") + set(discovery_script "${PROJECT_SOURCE_DIR}/Scripts/discover_tests.py") + + string(CONCAT include_content + "if(EXISTS [==[$]==])\n" + " execute_process(\n" + " COMMAND [==[${PMON_PYTHON_EXECUTABLE}]==] [==[${discovery_script}]==] vstest\n" + " --vstest-console [==[${vstest_console}]==]\n" + " --test-container [==[$]==]\n" + " --output-file [==[${discovered_tests_file}]==]\n" + " --test-prefix [==[${ARG_TEST_PREFIX}]==]\n" + " --working-directory [==[$]==]\n" + " --base-labels [==[${base_labels}]==]\n" + " --default-labels [==[${default_labels}]==]\n" + " --environment-labels [==[${environment_labels}]==]\n" + " --environment-name-regex [==[${ARG_ENVIRONMENT_NAME_REGEX}]==]\n" + " --test-environment [==[${ARG_TEST_ENVIRONMENT}]==]\n" + " --fixtures-required [==[${ARG_FIXTURES_REQUIRED}]==]\n" + " --resource-lock [==[${ARG_RESOURCE_LOCK}]==]\n" + " RESULT_VARIABLE discovery_result\n" + " OUTPUT_VARIABLE discovery_stdout\n" + " ERROR_VARIABLE discovery_stderr\n" + " )\n" + " if(NOT discovery_result EQUAL 0)\n" + " message(FATAL_ERROR \"VSTest discovery failed for ${ARG_TARGET}:\\n\${discovery_stdout}\\n\${discovery_stderr}\")\n" + " endif()\n" + " include([==[${discovered_tests_file}]==])\n" + "else()\n" + " add_test(${ARG_TARGET}_NOT_BUILT ${ARG_TARGET}_NOT_BUILT)\n" + "endif()\n" + ) + file(GENERATE + OUTPUT "${ctest_include_file}" + CONTENT "${include_content}" + ) + + if(CMAKE_CONFIGURATION_TYPES) + string( + REPLACE + "-$-include.cmake" + "-\${CTEST_CONFIGURATION_TYPE}-include.cmake" + include_file_for_ctest + "${ctest_include_file}" + ) + set(wrapper_file "${include_base}-include.cmake") + file(WRITE "${wrapper_file}" + "if(EXISTS \"${include_file_for_ctest}\")\n" + " include(\"${include_file_for_ctest}\")\n" + "else()\n" + " add_test(${ARG_TARGET}_NOT_BUILT ${ARG_TARGET}_NOT_BUILT)\n" + "endif()\n" + ) + set(ctest_include_file "${wrapper_file}") + endif() + + set_property( + DIRECTORY + APPEND + PROPERTY TEST_INCLUDE_FILES "${ctest_include_file}" + ) +endfunction() + +function(pmon_register_auxdata_fixture) + if(TARGET pmon_ensure_aux_testdata) + add_test( + NAME presentmon.auxdata.ensure + COMMAND + "${PMON_PYTHON_EXECUTABLE}" + "${PROJECT_SOURCE_DIR}/Scripts/auxdata.py" + ensure + --repo-root "${PROJECT_SOURCE_DIR}" + --dependency-root "${PMON_DEPENDENCY_ROOT}" + ) + set_tests_properties( + presentmon.auxdata.ensure + PROPERTIES + FIXTURES_SETUP pmon_auxdata + LABELS "testdata" + WORKING_DIRECTORY "${PROJECT_SOURCE_DIR}/Tests" + ) + endif() +endfunction() + +function(pmon_register_presentmon_gtest_group) + set(options ALLOW_EMPTY) + set( + oneValueArgs + TARGET + CONSOLE_TARGET + GROUP_NAME + TEST_PREFIX + TEST_FILTER + GOLD_DIRECTORY + OPTIONAL_TEST_DIRECTORY + FIXTURES_REQUIRED + RESOURCE_LOCK + ) + set(multiValueArgs LABELS) + cmake_parse_arguments( + ARG + "${options}" + "${oneValueArgs}" + "${multiValueArgs}" + ${ARGN} + ) + if( + NOT ARG_TARGET + OR NOT ARG_CONSOLE_TARGET + OR NOT ARG_GROUP_NAME + OR NOT ARG_TEST_PREFIX + OR NOT ARG_TEST_FILTER + OR NOT ARG_GOLD_DIRECTORY + ) + message( + FATAL_ERROR + "pmon_register_presentmon_gtest_group is missing a required argument." + ) + endif() + + list(JOIN ARG_LABELS "," labels_csv) + # Optional data-driven groups register nothing when their data is absent. Without + # this, discovery raises and the generated include aborts every CTest invocation in + # the tree, not just this group. + set(allow_empty_argument "") + if(ARG_ALLOW_EMPTY) + set(allow_empty_argument " --allow-empty\n") + endif() + set(include_base "${CMAKE_CURRENT_BINARY_DIR}/${ARG_TARGET}-${ARG_GROUP_NAME}") + set(ctest_include_file "${include_base}-$-include.cmake") + set(discovered_tests_file "${include_base}-$-tests.cmake") + set(discovery_script "${PROJECT_SOURCE_DIR}/Scripts/discover_tests.py") + string(CONCAT include_content + "if(EXISTS [==[$]==])\n" + " execute_process(\n" + " COMMAND [==[${PMON_PYTHON_EXECUTABLE}]==] [==[${discovery_script}]==] gtest\n" + " --test-executable [==[$]==]\n" + " --output-file [==[${discovered_tests_file}]==]\n" + " --test-prefix [==[${ARG_TEST_PREFIX}]==]\n" + " --test-filter [==[${ARG_TEST_FILTER}]==]\n" + " --presentmon-path [==[$]==]\n" + " --gold-directory [==[${ARG_GOLD_DIRECTORY}]==]\n" + " --optional-test-directory [==[${ARG_OPTIONAL_TEST_DIRECTORY}]==]\n" + " --working-directory [==[$]==]\n" + " --labels [==[${labels_csv}]==]\n" + " --fixtures-required [==[${ARG_FIXTURES_REQUIRED}]==]\n" + " --resource-lock [==[${ARG_RESOURCE_LOCK}]==]\n" + "${allow_empty_argument}" + " RESULT_VARIABLE discovery_result\n" + " OUTPUT_VARIABLE discovery_stdout\n" + " ERROR_VARIABLE discovery_stderr\n" + " )\n" + " if(NOT discovery_result EQUAL 0)\n" + " message(FATAL_ERROR \"Google Test discovery failed for ${ARG_GROUP_NAME}:\\n\${discovery_stdout}\\n\${discovery_stderr}\")\n" + " endif()\n" + " include([==[${discovered_tests_file}]==])\n" + "else()\n" + " add_test(${ARG_TARGET}_${ARG_GROUP_NAME}_NOT_BUILT ${ARG_TARGET}_${ARG_GROUP_NAME}_NOT_BUILT)\n" + "endif()\n" + ) + file(GENERATE + OUTPUT "${ctest_include_file}" + CONTENT "${include_content}" + ) + + if(CMAKE_CONFIGURATION_TYPES) + string( + REPLACE + "-$-include.cmake" + "-\${CTEST_CONFIGURATION_TYPE}-include.cmake" + include_file_for_ctest + "${ctest_include_file}" + ) + set(wrapper_file "${include_base}-include.cmake") + file(WRITE "${wrapper_file}" + "if(EXISTS \"${include_file_for_ctest}\")\n" + " include(\"${include_file_for_ctest}\")\n" + "else()\n" + " add_test(${ARG_TARGET}_${ARG_GROUP_NAME}_NOT_BUILT ${ARG_TARGET}_${ARG_GROUP_NAME}_NOT_BUILT)\n" + "endif()\n" + ) + set(ctest_include_file "${wrapper_file}") + endif() + + set_property( + DIRECTORY + APPEND + PROPERTY TEST_INCLUDE_FILES "${ctest_include_file}" + ) +endfunction() + +function(pmon_register_presentmon_gtest_leaf_tests target_name console_target_name) + if(NOT TARGET ${target_name} OR NOT TARGET ${console_target_name}) + message( + FATAL_ERROR + "pmon_register_presentmon_gtest_leaf_tests requires existing test and console targets." + ) + endif() + + pmon_register_presentmon_gtest_group( + TARGET ${target_name} + CONSOLE_TARGET ${console_target_name} + GROUP_NAME gold + TEST_PREFIX "PresentMonTests." + TEST_FILTER "GoldEtlCsvTests.*" + GOLD_DIRECTORY "${PROJECT_SOURCE_DIR}/Tests/Gold" + OPTIONAL_TEST_DIRECTORY "${PMON_PRESENTMON_TESTS_DATA_DIR}" + LABELS "PresentMonTests;GoldEtlCsvTests;console;console.gold;deterministic" + FIXTURES_REQUIRED pmon_auxdata + RESOURCE_LOCK pmon_console + ) + pmon_register_presentmon_gtest_group( + TARGET ${target_name} + CONSOLE_TARGET ${console_target_name} + GROUP_NAME commandline + TEST_PREFIX "PresentMonTests." + TEST_FILTER "CommandLineTests.*" + GOLD_DIRECTORY "${PROJECT_SOURCE_DIR}/Tests/Gold" + LABELS "PresentMonTests;CommandLineTests;console;console.realtime;environment" + RESOURCE_LOCK pmon_console + ) + + if(PMON_CTEST_FULL_CSV_TESTS OR IS_DIRECTORY "${PROJECT_SOURCE_DIR}/Tests/Full") + if(PMON_CTEST_FULL_CSV_TESTS AND NOT IS_DIRECTORY "${PROJECT_SOURCE_DIR}/Tests/Full") + message( + WARNING + "PMON_CTEST_FULL_CSV_TESTS is ON but ${PROJECT_SOURCE_DIR}/Tests/Full does " + "not exist. The Full CSV suite will register no tests until that directory " + "contains test_case_.etl files with matching gold CSVs." + ) + endif() + # ALLOW_EMPTY: the Full data set is optional and is not part of the repository. + pmon_register_presentmon_gtest_group( + TARGET ${target_name} + CONSOLE_TARGET ${console_target_name} + GROUP_NAME full + TEST_PREFIX "PresentMonTests.Full." + TEST_FILTER "GoldEtlCsvTests.*" + GOLD_DIRECTORY "${PROJECT_SOURCE_DIR}/Tests/Full" + LABELS "PresentMonTests;GoldEtlCsvTests;console;console.gold;full;deterministic" + RESOURCE_LOCK pmon_console + ALLOW_EMPTY + ) + endif() +endfunction() + +function(pmon_collect_ctest_build_dependencies output_list) + set(deps) + if(TARGET PresentMonConsole) + list(APPEND deps PresentMonConsole) + endif() + if(TARGET PresentMonTests) + list(APPEND deps PresentMonTests) + endif() + if(TARGET PresentMonUnitTests) + list(APPEND deps PresentMonUnitTests) + endif() + if(TARGET PresentMonAPI2Tests) + list(APPEND deps PresentMonAPI2Tests) + if(TARGET PresentMonService) + list(APPEND deps PresentMonService PresentMonAPI2 PresentMonAPI2Loader KernelProcess PresentMonUI) + endif() + if(TARGET pmon_stage_ui_payload) + list(APPEND deps pmon_stage_ui_payload) + endif() + if(TARGET pmon_stage_kernel_payload) + list(APPEND deps pmon_stage_kernel_payload) + endif() + endif() + set(${output_list} "${deps}" PARENT_SCOPE) +endfunction() + +function(pmon_register_all_ctests) + pmon_configure_presentmon_test_runsettings() + pmon_apply_test_runsettings("${PMON_PRESENTMON_TESTS_GENERATED_RUNSETTINGS}") + if(TARGET PresentMonTests) + pmon_apply_presentmon_tests_vs_debugger(PresentMonTests) + endif() + pmon_register_auxdata_fixture() + + if(PMON_BUILD_UNIT_TESTS AND TARGET PresentMonUnitTests) + pmon_register_vstest_leaf_tests( + TARGET PresentMonUnitTests + TEST_PREFIX "PresentMonUnitTests." + BASE_LABELS PresentMonUnitTests unit + DEFAULT_LABELS deterministic + RESOURCE_LOCK pmon_vstest + ) + endif() + + if(PMON_BUILD_API2_TESTS AND TARGET PresentMonAPI2Tests) + pmon_register_vstest_leaf_tests( + TARGET PresentMonAPI2Tests + TEST_PREFIX "PresentMonAPI2Tests." + BASE_LABELS PresentMonAPI2Tests api2 + DEFAULT_LABELS deterministic + ENVIRONMENT_LABELS api2.realtime environment + ENVIRONMENT_NAME_REGEX "${PMON_API2_CTEST_ENVIRONMENT_NAME_REGEX}" + TEST_ENVIRONMENT + "PRESENTMON_ADDITIONAL_TEST_DIR=${PMON_PRESENTMON_TESTS_DATA_DIR}" + FIXTURES_REQUIRED pmon_auxdata + RESOURCE_LOCK pmon_vstest + ) + endif() + + pmon_register_ctest_convenience_targets() +endfunction() + +function(pmon_register_ctest_convenience_targets) + if(TARGET pmon_run_deterministic_tests) + return() + endif() + + add_custom_target( + pmon_run_all_tests + COMMAND + ${CMAKE_CTEST_COMMAND} + --test-dir + "${CMAKE_BINARY_DIR}" + -C + $ + --output-on-failure + USES_TERMINAL + VERBATIM + ) + set_target_properties( + pmon_run_all_tests + PROPERTIES + EXCLUDE_FROM_DEFAULT_BUILD TRUE + FOLDER "Build" + ) + + add_custom_target( + pmon_run_deterministic_tests + COMMAND + ${CMAKE_CTEST_COMMAND} + --test-dir + "${CMAKE_BINARY_DIR}" + -C + $ + -L + deterministic + --output-on-failure + USES_TERMINAL + VERBATIM + ) + set_target_properties( + pmon_run_deterministic_tests + PROPERTIES + EXCLUDE_FROM_DEFAULT_BUILD TRUE + FOLDER "Build" + ) + + pmon_collect_ctest_build_dependencies(ctest_build_deps) + if(ctest_build_deps) + add_dependencies(pmon_run_all_tests ${ctest_build_deps}) + add_dependencies(pmon_run_deterministic_tests ${ctest_build_deps}) + endif() +endfunction() diff --git a/cmake/PresentMonCef.cmake b/cmake/PresentMonCef.cmake new file mode 100644 index 000000000..972c11389 --- /dev/null +++ b/cmake/PresentMonCef.cmake @@ -0,0 +1,218 @@ +include_guard(GLOBAL) + +function(_pmon_normalize_cef_relative_path input_path output_variable) + set(path "${input_path}") + string(REPLACE "\\" "/" path "${path}") + + cmake_path(IS_ABSOLUTE path is_absolute) + if( + NOT path + OR is_absolute + OR path MATCHES "(^|/)\\.\\.(/|$)" + OR path MATCHES "^[A-Za-z]:" + OR path MATCHES ";" + ) + message(FATAL_ERROR "Unsafe relative path in the CEF lock: ${input_path}") + endif() + + cmake_path(NORMAL_PATH path OUTPUT_VARIABLE normalized_path) + if(normalized_path STREQUAL ".") + message(FATAL_ERROR "Unsafe relative path in the CEF lock: ${input_path}") + endif() + + set(${output_variable} "${normalized_path}" PARENT_SCOPE) +endfunction() + +# The CEF stage holds wrapper libraries compiled by the selected platform +# toolset, so it is qualified as /cef/ and shared only +# by build trees using that toolset. The archive cache and the restore lock +# below it stay shared. The stage remains separate from the MSBuild stage in +# IntelPresentMon/AppCef/Cef so both build systems stay independent. +function(pmon_configure_cef) + set(app_cef_dir "${PROJECT_SOURCE_DIR}/IntelPresentMon/AppCef") + set(cef_lock "${app_cef_dir}/cef-lock.json") + set(cef_root "${PMON_DEPENDENCY_ROOT}/cef/${PMON_VS_TOOLSET}") + cmake_path(NORMAL_PATH cef_root OUTPUT_VARIABLE cef_root) + set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS "${cef_lock}") + + set(restore_source_argument) + if(PMON_CEF_SOURCE) + set(restore_source_argument --source "${PMON_CEF_SOURCE}") + endif() + + # This explicit target can repair or refresh the automatically prepared + # stage without recreating the CMake build tree. + add_custom_target( + pmon_restore_cef + COMMAND + "${PMON_PYTHON_EXECUTABLE}" + "${PROJECT_SOURCE_DIR}/Scripts/cef.py" + restore + --repo-root "${PROJECT_SOURCE_DIR}" + --dependency-root "${PMON_DEPENDENCY_ROOT}" + --generator "${CMAKE_GENERATOR}" + --platform "${PMON_ARCHITECTURE}" + --toolset "${PMON_VS_TOOLSET}" + --generator-instance "${CMAKE_GENERATOR_INSTANCE}" + ${restore_source_argument} + COMMENT "Restoring the locked CEF distribution into ${cef_root}" + USES_TERMINAL + VERBATIM + ) + set_target_properties( + pmon_restore_cef + PROPERTIES + EXCLUDE_FROM_DEFAULT_BUILD TRUE + FOLDER "Build" + ) + + file(READ "${cef_lock}" cef_lock_json) + string(JSON payload_count ERROR_VARIABLE json_error LENGTH "${cef_lock_json}" payload) + if(json_error) + message(FATAL_ERROR "Failed to read the CEF lock payload: ${json_error}") + endif() + if(payload_count EQUAL 0) + message(FATAL_ERROR "The CEF lock payload is empty: ${cef_lock}") + endif() + + set(runtime_destination "${PMON_OUTPUT_ROOT}/$") + set(runtime_outputs) + set(runtime_output_keys) + math(EXPR last_payload_index "${payload_count} - 1") + foreach(payload_index RANGE ${last_payload_index}) + string(JSON output_path GET "${cef_lock_json}" payload ${payload_index} path) + string(JSON stage_path GET "${cef_lock_json}" payload ${payload_index} stagePath) + _pmon_normalize_cef_relative_path("${output_path}" output_path) + _pmon_normalize_cef_relative_path("${stage_path}" stage_path) + + string(TOLOWER "${output_path}" output_key) + list(FIND runtime_output_keys "${output_key}" duplicate_output_index) + if(NOT duplicate_output_index EQUAL -1) + message(FATAL_ERROR "Duplicate output path in the CEF lock: ${output_path}") + endif() + + list(APPEND runtime_output_keys "${output_key}") + set(runtime_output "${runtime_destination}/${output_path}") + set(stage_input "${cef_root}/${stage_path}") + cmake_path(GET runtime_output PARENT_PATH runtime_output_directory) + list(APPEND runtime_outputs "${runtime_output}") + + add_custom_command( + OUTPUT "${runtime_output}" + COMMAND "${CMAKE_COMMAND}" -E make_directory "${runtime_output_directory}" + COMMAND + "${CMAKE_COMMAND}" -E copy_if_different + "${stage_input}" + "${runtime_output}" + COMMAND "${CMAKE_COMMAND}" -E touch_nocreate "${runtime_output}" + DEPENDS "${stage_input}" "${cef_lock}" + COMMENT "Staging CEF runtime file ${output_path}" + VERBATIM + ) + endforeach() + + add_custom_target( + pmon_stage_cef_runtime + DEPENDS ${runtime_outputs} + ) + set_target_properties( + pmon_stage_cef_runtime + PROPERTIES + EXCLUDE_FROM_DEFAULT_BUILD TRUE + FOLDER "Build" + ) + + # Explicit CI verification validates both the restored stage and the + # runtime files copied to the product output directory. + add_custom_target( + pmon_verify_cef + COMMAND + "${PMON_PYTHON_EXECUTABLE}" + "${PROJECT_SOURCE_DIR}/Scripts/cef.py" + verify-stage + --repo-root "${PROJECT_SOURCE_DIR}" + --dependency-root "${PMON_DEPENDENCY_ROOT}" + --toolset "${PMON_VS_TOOLSET}" + COMMAND + "${PMON_PYTHON_EXECUTABLE}" + "${PROJECT_SOURCE_DIR}/Scripts/cef.py" + verify-output + --repo-root "${PROJECT_SOURCE_DIR}" + --dependency-root "${PMON_DEPENDENCY_ROOT}" + --output-root "${runtime_destination}" + DEPENDS pmon_stage_cef_runtime + COMMENT "Verifying the CEF stage and staged runtime payload" + USES_TERMINAL + VERBATIM + ) + set_target_properties( + pmon_verify_cef + PROPERTIES + EXCLUDE_FROM_DEFAULT_BUILD TRUE + FOLDER "Build" + ) + + # Consumers use these targets instead of paths under AppCef/Cef. + add_library(pmon_cef_headers INTERFACE) + add_library(pmon::cef_headers ALIAS pmon_cef_headers) + target_include_directories(pmon_cef_headers INTERFACE "${cef_root}/Include") + set_property(TARGET pmon_cef_headers PROPERTY FOLDER "ThirdParty") + + add_library(pmon_cef_libcef SHARED IMPORTED GLOBAL) + add_library(pmon::cef_libcef ALIAS pmon_cef_libcef) + set_target_properties( + pmon_cef_libcef + PROPERTIES + IMPORTED_CONFIGURATIONS "DEBUG;RELEASE" + IMPORTED_LOCATION "${cef_root}/Bin/libcef.dll" + IMPORTED_LOCATION_DEBUG "${cef_root}/Bin/libcef.dll" + IMPORTED_LOCATION_RELEASE "${cef_root}/Bin/libcef.dll" + IMPORTED_IMPLIB_DEBUG "${cef_root}/Lib/Debug/libcef.lib" + IMPORTED_IMPLIB_RELEASE "${cef_root}/Lib/Release/libcef.lib" + MAP_IMPORTED_CONFIG_RELWITHDEBINFO "Release" + MAP_IMPORTED_CONFIG_MINSIZEREL "Release" + ) + + add_library(pmon_cef_wrapper STATIC IMPORTED GLOBAL) + add_library(pmon::cef_wrapper ALIAS pmon_cef_wrapper) + set_target_properties( + pmon_cef_wrapper + PROPERTIES + IMPORTED_CONFIGURATIONS "DEBUG;RELEASE" + IMPORTED_LOCATION_DEBUG "${cef_root}/Lib/Debug/libcef_dll_wrapper.lib" + IMPORTED_LOCATION_RELEASE "${cef_root}/Lib/Release/libcef_dll_wrapper.lib" + MAP_IMPORTED_CONFIG_RELWITHDEBINFO "Release" + MAP_IMPORTED_CONFIG_MINSIZEREL "Release" + ) + + add_library(pmon_cef INTERFACE) + add_library(pmon::cef ALIAS pmon_cef) + target_link_libraries( + pmon_cef + INTERFACE + pmon_cef_headers + pmon_cef_wrapper + pmon_cef_libcef + ) + set_property(TARGET pmon_cef PROPERTY FOLDER "ThirdParty") + + set(PMON_CEF_EFFECTIVE_ROOT "${cef_root}" CACHE INTERNAL "Resolved CEF stage" FORCE) + set(PMON_CEF_EFFECTIVE_ROOT "${cef_root}" PARENT_SCOPE) +endfunction() + +# Links a target against the CEF headers and libraries and stages the runtime +# payload next to it. Restore and validation remain explicit operations; normal +# product builds only perform incremental staging. +function(pmon_target_uses_cef target) + if(NOT TARGET pmon_stage_cef_runtime) + message(FATAL_ERROR "pmon_configure_cef() must run before pmon_target_uses_cef().") + endif() + + target_link_libraries(${target} PRIVATE pmon::cef) + add_dependencies(${target} pmon_stage_cef_runtime) + + set_target_properties( + pmon_stage_cef_runtime + PROPERTIES EXCLUDE_FROM_DEFAULT_BUILD FALSE + ) +endfunction() diff --git a/cmake/PresentMonCompiler.cmake b/cmake/PresentMonCompiler.cmake new file mode 100644 index 000000000..1f984c58d --- /dev/null +++ b/cmake/PresentMonCompiler.cmake @@ -0,0 +1,71 @@ +include_guard(GLOBAL) + +function(pmon_create_compiler_targets) + set( + CMAKE_MSVC_RUNTIME_LIBRARY + "MultiThreaded$<$:Debug>" + PARENT_SCOPE + ) + + # Shared defaults from the tracked legacy projects. + set(CMAKE_INTERPROCEDURAL_OPTIMIZATION_RELEASE TRUE PARENT_SCOPE) + set( + CMAKE_MSVC_DEBUG_INFORMATION_FORMAT + "$<$:EditAndContinue>$<$:ProgramDatabase>" + PARENT_SCOPE + ) + set(CMAKE_VS_JUST_MY_CODE_DEBUGGING "$" PARENT_SCOPE) + + add_library(pmon_build_options INTERFACE) + add_library(pmon::build_options ALIAS pmon_build_options) + target_compile_options( + pmon_build_options + INTERFACE + /MP + /sdl + /external:W0 + $<$:/permissive-> + $<$:/std:c++latest> + $<$:/Gy> + $<$:/Oi> + ) + # Most targets are Unicode. The few MultiByte ones (SampleClient, CefNano) + # opt out by setting the PMON_MULTIBYTE property on the consuming target. + target_compile_definitions( + pmon_build_options + INTERFACE + CEREAL_THREAD_SAFE=1 + BOOST_ALLOW_DEPRECATED_HEADERS + "$<$>>:UNICODE;_UNICODE>" + "$<$>:_MBCS>" + ) + set_property(TARGET pmon_build_options PROPERTY FOLDER "Build") + + add_library(pmon_warnings_default INTERFACE) + add_library(pmon::warnings_default ALIAS pmon_warnings_default) + target_compile_options(pmon_warnings_default INTERFACE /W3) + set_property(TARGET pmon_warnings_default PROPERTY FOLDER "Build") + + add_library(pmon_warnings_strict INTERFACE) + add_library(pmon::warnings_strict ALIAS pmon_warnings_strict) + target_compile_options(pmon_warnings_strict INTERFACE /W4 /WX) + set_property(TARGET pmon_warnings_strict PROPERTY FOLDER "Build") + + # Keep linker /WX off static libraries, which would propagate it to consumers. + add_library(pmon_warnings_strict_link INTERFACE) + add_library(pmon::warnings_strict_link ALIAS pmon_warnings_strict_link) + target_link_options(pmon_warnings_strict_link INTERFACE /WX) + set_property(TARGET pmon_warnings_strict_link PROPERTY FOLDER "Build") + + # Shared legacy Release policy for executable and DLL link steps. + add_library(pmon_release_link_policy INTERFACE) + add_library(pmon::release_link_policy ALIAS pmon_release_link_policy) + target_link_options( + pmon_release_link_policy + INTERFACE + $<$:/DEBUG> + $<$:/OPT:REF> + $<$:/OPT:ICF> + ) + set_property(TARGET pmon_release_link_policy PROPERTY FOLDER "Build") +endfunction() diff --git a/cmake/PresentMonCppUnitTest.cmake b/cmake/PresentMonCppUnitTest.cmake new file mode 100644 index 000000000..6e43745af --- /dev/null +++ b/cmake/PresentMonCppUnitTest.cmake @@ -0,0 +1,62 @@ +include_guard(GLOBAL) + +# Microsoft C++ Unit Test Framework discovery. +# +# The framework ships with the Visual Studio instance CMake selected, which +# pmon_configure_toolchain() published as PMON_VS_INSTANCE_ROOT. Both artifacts +# are derived directly beneath that instance; no other Visual Studio +# installation is consulted and no compiler-path or vswhere search runs here. +# +# PMON_CPPUNITTEST_INCLUDE_DIR and PMON_CPPUNITTEST_LIBRARY are PresentMon-owned +# internal results. They are recomputed from the selected instance on every +# configure rather than honored from the cache. + +function(_pmon_cppunittest_arch_tag output_variable) + if(PMON_ARCHITECTURE STREQUAL "Win32") + set(${output_variable} "x86" PARENT_SCOPE) + elseif(PMON_ARCHITECTURE STREQUAL "x64") + set(${output_variable} "x64" PARENT_SCOPE) + else() + message(FATAL_ERROR "CppUnitTest framework lookup supports x64 and Win32 only.") + endif() +endfunction() + +function(pmon_find_cppunittest_framework) + set(_unittest_root "${PMON_VS_INSTANCE_ROOT}/VC/Auxiliary/VS/UnitTest") + set(_include_dir "${_unittest_root}/include") + _pmon_cppunittest_arch_tag(_unittest_arch) + set(_library "${_unittest_root}/lib/${_unittest_arch}/Microsoft.VisualStudio.TestTools.CppUnitTestFramework.lib") + + if(NOT EXISTS "${_include_dir}/CppUnitTest.h") + message( + FATAL_ERROR + "Microsoft C++ Unit Test Framework header not found:\n" + " ${_include_dir}/CppUnitTest.h\n" + "Selected Visual Studio instance:\n" + " ${PMON_VS_INSTANCE_ROOT}\n" + "Install the C++ Test Tools workload component in that instance. " + "PresentMon does not substitute another Visual Studio installation." + ) + endif() + if(NOT EXISTS "${_library}") + message( + FATAL_ERROR + "Microsoft C++ Unit Test Framework ${_unittest_arch} library not found:\n" + " ${_library}\n" + "Selected Visual Studio instance:\n" + " ${PMON_VS_INSTANCE_ROOT}\n" + "Install the C++ Test Tools workload component in that instance. " + "PresentMon does not substitute another Visual Studio installation." + ) + endif() + + set(PMON_CPPUNITTEST_INCLUDE_DIR "${_include_dir}" CACHE INTERNAL "CppUnitTest include directory") + set(PMON_CPPUNITTEST_LIBRARY "${_library}" CACHE INTERNAL "CppUnitTest import library") +endfunction() + +function(pmon_apply_cppunittest_framework target) + pmon_find_cppunittest_framework() + target_include_directories(${target} PRIVATE "${PMON_CPPUNITTEST_INCLUDE_DIR}") + target_link_libraries(${target} PRIVATE "${PMON_CPPUNITTEST_LIBRARY}") + target_link_options(${target} PRIVATE /SUBSYSTEM:WINDOWS) +endfunction() diff --git a/cmake/PresentMonGenerateEnumMetric.cmake b/cmake/PresentMonGenerateEnumMetric.cmake new file mode 100644 index 000000000..178095120 --- /dev/null +++ b/cmake/PresentMonGenerateEnumMetric.cmake @@ -0,0 +1,24 @@ +# Script invoked via cmake -P (not include()). Runs the metrics awk program and captures stdout into the generated header. +# Used instead of Tools/generate/EnumMetric/generate.cmd so the output location +# follows PMON_OUTPUT_ROOT instead of being hard-coded relative to the script. + +cmake_path(GET PMON_ENUM_METRIC_OUTPUT PARENT_PATH output_directory) +file(MAKE_DIRECTORY "${output_directory}") + +execute_process( + COMMAND + "${PMON_AWK_EXECUTABLE}" + -f "${PMON_AWK_PROGRAM}" + "${PMON_METRICS_CSV}" + OUTPUT_FILE "${PMON_ENUM_METRIC_OUTPUT}" + RESULT_VARIABLE awk_result + ERROR_VARIABLE awk_error +) + +if(NOT awk_result EQUAL 0) + file(REMOVE "${PMON_ENUM_METRIC_OUTPUT}") + message( + FATAL_ERROR + "Failed to generate ${PMON_ENUM_METRIC_OUTPUT} (exit ${awk_result}): ${awk_error}" + ) +endif() diff --git a/cmake/PresentMonMessageCompiler.cmake b/cmake/PresentMonMessageCompiler.cmake new file mode 100644 index 000000000..3ffbd1a6d --- /dev/null +++ b/cmake/PresentMonMessageCompiler.cmake @@ -0,0 +1,60 @@ +include_guard(GLOBAL) + +function(pmon_compile_message_manifest output_variable manifest_path output_name) + if(NOT PMON_MC_EXECUTABLE) + find_program(PMON_MC_EXECUTABLE mc.exe) + endif() + if(NOT PMON_MC_EXECUTABLE) + set( + sdk_mc + "$ENV{ProgramFiles\(x86\)}/Windows Kits/10/bin/${CMAKE_VS_WINDOWS_TARGET_PLATFORM_VERSION}/x64/mc.exe" + ) + if(EXISTS "${sdk_mc}") + set( + PMON_MC_EXECUTABLE + "${sdk_mc}" + CACHE FILEPATH + "Windows message compiler" + FORCE + ) + endif() + endif() + if(NOT PMON_MC_EXECUTABLE) + message(FATAL_ERROR "Windows SDK message compiler mc.exe was not found.") + endif() + + get_filename_component(manifest_path "${manifest_path}" ABSOLUTE) + set(output_directory "${PROJECT_BINARY_DIR}/generated/mc/${output_name}") + set(generated_header "${output_directory}/${output_name}.h") + set(generated_resource "${output_directory}/${output_name}.rc") + + add_custom_command( + OUTPUT + "${generated_header}" + "${generated_resource}" + BYPRODUCTS + "${output_directory}/${output_name}_MSG00001.bin" + "${output_directory}/${output_name}TEMP.BIN" + COMMAND + "${CMAKE_COMMAND}" -E make_directory "${output_directory}" + COMMAND + "${PMON_MC_EXECUTABLE}" + -um + -v + -z "${output_name}" + -h "${output_directory}" + -r "${output_directory}" + "${manifest_path}" + DEPENDS + "${manifest_path}" + COMMENT "Compiling message manifest ${manifest_path}" + VERBATIM + ) + + set_source_files_properties( + "${generated_header}" + "${generated_resource}" + PROPERTIES GENERATED TRUE + ) + set(${output_variable} "${generated_resource}" PARENT_SCOPE) +endfunction() diff --git a/cmake/PresentMonOptions.cmake b/cmake/PresentMonOptions.cmake new file mode 100644 index 000000000..a5cef1b56 --- /dev/null +++ b/cmake/PresentMonOptions.cmake @@ -0,0 +1,194 @@ +include_guard(GLOBAL) + +include(CMakeDependentOption) + +function(pmon_define_enum_cache variable default_value description) + set(allowed_values ${ARGN}) + + if(NOT DEFINED ${variable}) + set(${variable} "${default_value}") + endif() + + string(TOUPPER "${${variable}}" normalized_value) + if(NOT normalized_value IN_LIST allowed_values) + list(JOIN allowed_values ", " allowed_values_text) + message( + FATAL_ERROR + "${variable} must be one of: ${allowed_values_text}. " + "Received: ${${variable}}" + ) + endif() + + set(${variable} "${normalized_value}" CACHE STRING "${description}" FORCE) + set_property(CACHE ${variable} PROPERTY STRINGS ${allowed_values}) +endfunction() + +function(pmon_define_build_scope_cache) + pmon_define_enum_cache( + PMON_BUILD_SCOPE + FULL + "PresentMon component set" + FULL + CONSOLE + PROVIDER + ) +endfunction() + +function(pmon_configure_options) + pmon_define_build_scope_cache() + pmon_define_enum_cache( + PMON_DEPLOYMENT_PROFILE + DEVELOPER + "PresentMon deployment policy" + DEVELOPER + PRODUCTION + ) + pmon_define_enum_cache( + PMON_ENABLE_UCI + AUTO + "UCI integration mode" + AUTO + ON + OFF + ) + + if(PMON_BUILD_SCOPE STREQUAL "FULL") + option(PMON_BUILD_CONSOLE "Build the standalone PresentMon console" ON) + option(PMON_BUILD_SERVICE "Build the PresentMon service" ON) + option(PMON_BUILD_SDK "Build the PresentMon SDK and API libraries" ON) + option(PMON_BUILD_UI "Build the Intel PresentMon UI" ON) + option(PMON_BUILD_PROVIDER "Build the ETW provider" ON) + option(PMON_BUILD_TOOLS "Build standalone tools" ON) + + set(build_tests_default ON) + if(PMON_DEPLOYMENT_PROFILE STREQUAL "PRODUCTION") + set(build_tests_default OFF) + endif() + option( + PMON_BUILD_TESTS + "Build test projects and register CTest entries" + ${build_tests_default} + ) + elseif(PMON_BUILD_SCOPE STREQUAL "CONSOLE") + set(PMON_BUILD_CONSOLE ON CACHE BOOL "Build the standalone PresentMon console" FORCE) + set(PMON_BUILD_SERVICE OFF CACHE BOOL "Build the PresentMon service" FORCE) + set(PMON_BUILD_SDK OFF CACHE BOOL "Build the PresentMon SDK and API libraries" FORCE) + set(PMON_BUILD_UI OFF CACHE BOOL "Build the Intel PresentMon UI" FORCE) + set(PMON_BUILD_PROVIDER OFF CACHE BOOL "Build the ETW provider" FORCE) + set(PMON_BUILD_TOOLS OFF CACHE BOOL "Build standalone tools" FORCE) + option(PMON_BUILD_TESTS "Build test projects and register CTest entries" OFF) + elseif(PMON_BUILD_SCOPE STREQUAL "PROVIDER") + set(PMON_BUILD_CONSOLE OFF CACHE BOOL "Build the standalone PresentMon console" FORCE) + set(PMON_BUILD_SERVICE OFF CACHE BOOL "Build the PresentMon service" FORCE) + set(PMON_BUILD_SDK OFF CACHE BOOL "Build the PresentMon SDK and API libraries" FORCE) + set(PMON_BUILD_UI OFF CACHE BOOL "Build the Intel PresentMon UI" FORCE) + set(PMON_BUILD_PROVIDER ON CACHE BOOL "Build the ETW provider" FORCE) + set(PMON_BUILD_TOOLS OFF CACHE BOOL "Build standalone tools" FORCE) + set(PMON_BUILD_TESTS OFF CACHE BOOL "Build test projects and register CTest entries" FORCE) + endif() + option( + PMON_AUTO_PREPARE_DEPENDENCIES + "Restore and validate CEF, web, and auxiliary test-data inputs during configure" + ON + ) + option( + PMON_CTEST_FULL_CSV_TESTS + "Register CTest entry for the Tests/Full gold CSV suite" + OFF + ) + + cmake_dependent_option( + PMON_BUILD_PRESENTMON_TESTS + "Build PresentMonTests (GTest console regression)" + ON + "PMON_BUILD_TESTS;PMON_BUILD_CONSOLE" + OFF + ) + cmake_dependent_option( + PMON_BUILD_UNIT_TESTS + "Build PresentMonUnitTests (Visual Studio C++ unit tests; requires Core/UI stack, x64 only)" + ON + "PMON_BUILD_TESTS;PMON_BUILD_SDK;PMON_BUILD_UI" + OFF + ) + cmake_dependent_option( + PMON_BUILD_API2_TESTS + "Build PresentMonAPI2Tests (Visual Studio C++ integration tests; x64 only)" + ON + "PMON_BUILD_TESTS;PMON_BUILD_SERVICE;PMON_BUILD_SDK;PMON_BUILD_UI" + OFF + ) + + set( + PMON_UCI_ROOT + "" + CACHE PATH + "UCI SDK root; PMON_UCI_SDK_DIR is used when this is empty" + ) + set( + PMON_CEF_SOURCE + "" + CACHE STRING + "Optional locked CEF archive or URI used during automatic dependency preparation" + ) + set( + PMON_EDSS_SIGN_SCRIPT + "" + CACHE FILEPATH + "External EDSS signing script (PRODUCTION: set PMON_EDSS_SIGN_SCRIPT env or -D before configure)" + ) + option( + PMON_EDSS_IN_PROCESS_SIGNING + "Pass -InProcessSigning to PMON_EDSS_SIGN_SCRIPT (SignTool as the current user; default). When OFF, pass -ImpersonatedSigning." + ON + ) + cmake_dependent_option( + PMON_PACKAGING_MSI_EDSS_SIGN_SKIP + "Skip EDSS invoke on pmon_sign_production_installer; run MSI Authenticode and embedded PE verification only" + OFF + "PMON_DEPLOYMENT_PROFILE STREQUAL PRODUCTION" + OFF + ) + set( + PMON_SIGNTOOL_EXECUTABLE + "" + CACHE FILEPATH + "Path to SignTool for the developer My-store backend" + ) + set( + PMON_SIGNTOOL_CERTIFICATE_STORE + "PrivateCertStore" + CACHE STRING + "Certificate store used by the direct SignTool backend (SignTool /s PrivateCertStore; legacy MSBuild parity)" + ) + set( + PMON_SIGNTOOL_CERTIFICATE_NAME + "Test Certificate - For Internal Use Only" + CACHE STRING + "Certificate subject used by the direct SignTool backend" + ) + set( + PMON_SIGNTOOL_TIMESTAMP_URL + "http://timestamp.comodoca.com/authenticode" + CACHE STRING + "Timestamp server URL for the developer SignTool post-build step" + ) + + foreach(option_name IN ITEMS + PMON_BUILD_SCOPE + PMON_DEPLOYMENT_PROFILE + PMON_ENABLE_UCI + PMON_BUILD_CONSOLE + PMON_BUILD_SERVICE + PMON_BUILD_SDK + PMON_BUILD_UI + PMON_BUILD_PROVIDER + PMON_BUILD_TOOLS + PMON_BUILD_TESTS + PMON_BUILD_PRESENTMON_TESTS + PMON_BUILD_UNIT_TESTS + PMON_BUILD_API2_TESTS + ) + set(${option_name} "${${option_name}}" PARENT_SCOPE) + endforeach() +endfunction() diff --git a/cmake/PresentMonOutput.cmake b/cmake/PresentMonOutput.cmake new file mode 100644 index 000000000..2532f0bfa --- /dev/null +++ b/cmake/PresentMonOutput.cmake @@ -0,0 +1,43 @@ +include_guard(GLOBAL) + +function(pmon_configure_output_directories) + set(generator_platform "${CMAKE_GENERATOR_PLATFORM}") + if(NOT generator_platform AND CMAKE_VS_PLATFORM_NAME) + set(generator_platform "${CMAKE_VS_PLATFORM_NAME}") + endif() + + if(generator_platform STREQUAL "Win32") + set(pmon_architecture "Win32") + set(pmon_arch_tag "x86") + elseif(generator_platform STREQUAL "x64") + set(pmon_architecture "x64") + set(pmon_arch_tag "x64") + else() + message( + FATAL_ERROR + "Unsupported generator platform: ${generator_platform}. " + "Use x64 or Win32." + ) + endif() + + set( + PMON_OUTPUT_ROOT + "${CMAKE_BINARY_DIR}" + CACHE PATH + "Root directory for externally consumed build artifacts" + ) + + set(CMAKE_RUNTIME_OUTPUT_DIRECTORY "${PMON_OUTPUT_ROOT}/$" PARENT_SCOPE) + set(CMAKE_LIBRARY_OUTPUT_DIRECTORY "${PMON_OUTPUT_ROOT}/$" PARENT_SCOPE) + set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY "${PMON_OUTPUT_ROOT}/$" PARENT_SCOPE) + set(CMAKE_PDB_OUTPUT_DIRECTORY "${PMON_OUTPUT_ROOT}/$" PARENT_SCOPE) + set( + CMAKE_COMPILE_PDB_OUTPUT_DIRECTORY + "${PMON_OUTPUT_ROOT}/obj/$" + PARENT_SCOPE + ) + + set(PMON_OUTPUT_ROOT "${PMON_OUTPUT_ROOT}" PARENT_SCOPE) + set(PMON_ARCHITECTURE "${pmon_architecture}" PARENT_SCOPE) + set(PMON_ARCH_TAG "${pmon_arch_tag}" PARENT_SCOPE) +endfunction() diff --git a/cmake/PresentMonPackaging.cmake b/cmake/PresentMonPackaging.cmake new file mode 100644 index 000000000..ad0a1a138 --- /dev/null +++ b/cmake/PresentMonPackaging.cmake @@ -0,0 +1,1603 @@ +include_guard(GLOBAL) + +set(PMON_PACKAGING_CMAKE_DIR "${CMAKE_CURRENT_LIST_DIR}") +set(PMON_PACKAGING_PYTHON_SCRIPT "${CMAKE_SOURCE_DIR}/Scripts/package.py") + +set( + PMON_WIX_ROOT + "" + CACHE PATH + "Root of a WiX Toolset 3.x installation (optional; WIX env and standard Program Files paths are fallbacks)" +) + +function(pmon_normalize_path_input input_path output_variable) + set(path "${input_path}") + string(STRIP "${path}" path) + while(path MATCHES "(.+)[/\\\\]$") + set(path "${CMAKE_MATCH_1}") + endwhile() + set(${output_variable} "${path}" PARENT_SCOPE) +endfunction() + +function(pmon_packaging_components_enabled output_variable) + set(enabled TRUE) + foreach(option_name IN ITEMS + PMON_BUILD_CONSOLE + PMON_BUILD_SERVICE + PMON_BUILD_SDK + PMON_BUILD_UI + PMON_BUILD_PROVIDER + ) + if(NOT ${option_name}) + set(enabled FALSE) + endif() + endforeach() + set(${output_variable} "${enabled}" PARENT_SCOPE) +endfunction() + +function(pmon_require_packaging_native_targets) + set(required_native_targets + PresentMonUI + KernelProcess + PresentMonService + PresentMonAPI2 + PresentMonAPI2Loader + pmon_provider + PresentMonConsole + ) + foreach(native_target IN LISTS required_native_targets) + if(NOT TARGET ${native_target}) + message( + FATAL_ERROR + "Packaging requires CMake target \"${native_target}\". " + "Enable the corresponding PMON_BUILD_* options in an x64 configure." + ) + endif() + endforeach() +endfunction() + +function(pmon_resolve_wix_root output_variable reason_variable) + set(resolved_root "") + set(reason "") + + if(PMON_WIX_ROOT AND NOT "${PMON_WIX_ROOT}" STREQUAL "") + set(wix_cache_input "${PMON_WIX_ROOT}") + pmon_normalize_path_input("${wix_cache_input}" wix_cache_input) + cmake_path(ABSOLUTE_PATH wix_cache_input NORMALIZE OUTPUT_VARIABLE resolved_root) + if(NOT IS_DIRECTORY "${resolved_root}") + set(reason "PMON_WIX_ROOT is not a directory: ${resolved_root}") + set(resolved_root "") + endif() + endif() + + if(NOT resolved_root AND DEFINED ENV{WIX} AND NOT "$ENV{WIX}" STREQUAL "") + set(wix_env_raw $ENV{WIX}) + pmon_normalize_path_input("${wix_env_raw}" wix_env_input) + if(wix_env_input) + cmake_path(ABSOLUTE_PATH wix_env_input NORMALIZE OUTPUT_VARIABLE env_wix_root) + if(IS_DIRECTORY "${env_wix_root}") + set(resolved_root "${env_wix_root}") + else() + set(reason "WIX environment variable is not a directory: ${env_wix_root}") + endif() + endif() + endif() + + if(NOT resolved_root) + file( + GLOB wix_install_candidates + LIST_DIRECTORIES TRUE + "$ENV{ProgramFiles\(x86\)}/WiX Toolset v3.*" + ) + if(wix_install_candidates) + list(SORT wix_install_candidates COMPARE NATURAL ORDER DESCENDING) + list(GET wix_install_candidates 0 resolved_root) + set(reason "") + elseif(NOT reason) + set(reason "WiX Toolset 3 was not found. Set PMON_WIX_ROOT or the WIX environment variable.") + endif() + endif() + + if(resolved_root) + string(TOLOWER "${resolved_root}" resolved_root_lower) + if(resolved_root_lower MATCHES "toolset v4") + set(reason "WiX Toolset 4 is not supported (resolved: ${resolved_root}).") + set(resolved_root "") + else() + set(wix_candle "${resolved_root}/bin/candle.exe") + set(wix_wix_exe "${resolved_root}/bin/wix.exe") + if(EXISTS "${wix_wix_exe}" AND NOT EXISTS "${wix_candle}") + set( + reason + "Resolved WiX path appears to be WiX Toolset 4 (wix.exe without candle.exe): ${resolved_root}" + ) + set(resolved_root "") + endif() + endif() + endif() + + set(${output_variable} "${resolved_root}" PARENT_SCOPE) + set(${reason_variable} "${reason}" PARENT_SCOPE) +endfunction() + +function(pmon_print_packaging_summary wix_root wix_reason) + set(payload_release_dir "${PMON_OUTPUT_ROOT}/Release") + set(msi_output "${payload_release_dir}/en-us/PresentMon.msi") + set(msm_output "${payload_release_dir}/en-us/PresentMonSharedService.msm") + + message(STATUS "PresentMon packaging") + if(wix_root) + message(STATUS " Packaging targets: available (x64, all package components ON)") + message(STATUS " WiX root (preview): ${wix_root}") + elseif(wix_reason) + message(STATUS " Packaging targets: unavailable (native developer builds remain available)") + message(STATUS " WiX root (preview): not found (${wix_reason})") + else() + message(STATUS " Packaging targets: unavailable (native developer builds remain available)") + message(STATUS " WiX root (preview): not found") + endif() + if(CMAKE_VS_MSBUILD_COMMAND) + message(STATUS " MSBuild: ${CMAKE_VS_MSBUILD_COMMAND}") + else() + message(STATUS " MSBuild: unavailable (Visual Studio generator required for packaging)") + endif() + message(STATUS " Payload root: ${payload_release_dir}") + message(STATUS " UCI included: ${PMON_UCI_ENABLED}") + message(STATUS " MSI output: ${msi_output}") + message(STATUS " MSM output: ${msm_output}") + if(PMON_DEPLOYMENT_PROFILE STREQUAL "PRODUCTION") + message(STATUS " Production signature enforcement: active") + else() + message(STATUS " Production signature enforcement: inactive") + endif() +endfunction() + +function(pmon_packaging_native_dir_with_trailing_sep input_path output_variable) + file(TO_NATIVE_PATH "${input_path}" native_path) + if(NOT native_path MATCHES "[/\\\\]$") + string(APPEND native_path "\\") + endif() + set(${output_variable} "${native_path}" PARENT_SCOPE) +endfunction() + +function(pmon_packaging_msbuild_dir_path input_path output_variable) + get_filename_component(abs_path "${input_path}" ABSOLUTE) + file(TO_CMAKE_PATH "${abs_path}" cmake_path) + string(REPLACE "\\" "/" msbuild_path "${cmake_path}") + if(NOT msbuild_path MATCHES "/$") + string(APPEND msbuild_path "/") + endif() + set(${output_variable} "${msbuild_path}" PARENT_SCOPE) +endfunction() + +function(pmon_packaging_msbuild_file_path input_path output_variable) + get_filename_component(abs_path "${input_path}" ABSOLUTE) + file(TO_CMAKE_PATH "${abs_path}" cmake_path) + string(REPLACE "\\" "/" msbuild_path "${cmake_path}") + set(${output_variable} "${msbuild_path}" PARENT_SCOPE) +endfunction() + +function(pmon_packaging_repo_root_native output_variable) + pmon_packaging_msbuild_dir_path("${CMAKE_SOURCE_DIR}" native_root) + set(${output_variable} "${native_root}" PARENT_SCOPE) +endfunction() + +# The retained .wixproj files default WixTargetsPath to +# $(MSBuildExtensionsPath32)/Microsoft/WiX/v3.x/Wix.targets, which resolves +# inside whichever Visual Studio instance supplied MSBuild and so requires a +# Visual Studio WiX extension in that instance. The packaging lane selects the +# WiX 3 MSBuild integration itself instead: the targets come from the fixed WiX +# 3 MSBuild location, and the tasks assembly and tool directory come from the +# WiX 3 root already resolved by the PMON_WIX_ROOT/WIX/Program Files policy. +# Passing them as global properties makes both supported generators build the +# same .wixproj files against the same WiX 3 installation. +set( + PMON_PACKAGING_WIX_TARGETS_PATH + "$ENV{ProgramFiles\(x86\)}/MSBuild/Microsoft/WiX/v3.x/wix.targets" +) + +function(pmon_packaging_wix_msbuild_integration_properties output_variable) + pmon_packaging_msbuild_file_path( + "${PMON_PACKAGING_WIX_TARGETS_PATH}" + wix_targets_path_msbuild + ) + pmon_packaging_msbuild_file_path( + "${PMON_PACKAGING_WIX_ROOT}/bin/WixTasks.dll" + wix_tasks_path_msbuild + ) + # WixToolPath keeps its trailing separator because the WiX targets + # concatenate it directly, as in $(WixToolPath)..\sdk\. These are the + # forward-slash forms the other directory properties here already use, which + # also keeps a trailing separator out of MSBuild command-line quoting. + pmon_packaging_msbuild_dir_path( + "${PMON_PACKAGING_WIX_ROOT}/bin" + wix_tool_path_msbuild + ) + set( + ${output_variable} + "/p:WixTargetsPath=${wix_targets_path_msbuild}" + "/p:WixTasksPath=${wix_tasks_path_msbuild}" + "/p:WixToolPath=${wix_tool_path_msbuild}" + PARENT_SCOPE + ) +endfunction() + +function(pmon_packaging_wix_cmake_common_msbuild_properties output_variable) + pmon_packaging_repo_root_native(repo_root_native) + pmon_packaging_wix_msbuild_integration_properties(wix_integration_props) + pmon_packaging_msbuild_dir_path( + "${PMON_OUTPUT_ROOT}/Release" + payload_root_native + ) + pmon_packaging_msbuild_dir_path( + "${PMON_OUTPUT_ROOT}/Release" + release_payload_dir_native + ) + set( + extension_dll + "${PMON_OUTPUT_ROOT}/obj/PMInstallerExtension-Release/PMInstallerExtension.dll" + ) + pmon_packaging_msbuild_file_path("${extension_dll}" extension_dll_native) + set( + wixlib_output + "${PMON_OUTPUT_ROOT}/Release/PMInstallerLib.wixlib" + ) + pmon_packaging_msbuild_file_path("${wixlib_output}" wixlib_output_native) + set(uci_stage_dir "${CMAKE_BINARY_DIR}/packaging/Release/UciDistStage") + set(uci_fragment "${CMAKE_BINARY_DIR}/packaging/Release/Generated/UciDist.wxs") + pmon_packaging_msbuild_dir_path("${uci_stage_dir}" uci_stage_native) + pmon_packaging_msbuild_file_path("${uci_fragment}" uci_fragment_native) + set(provider_project_dir "${CMAKE_SOURCE_DIR}/Provider") + pmon_packaging_msbuild_dir_path( + "${provider_project_dir}" + provider_project_dir_native + ) + set(api2_project_dir "${CMAKE_SOURCE_DIR}/IntelPresentMon/PresentMonAPI2") + pmon_packaging_msbuild_dir_path( + "${api2_project_dir}" + api2_project_dir_native + ) + set(api2_header "${CMAKE_SOURCE_DIR}/IntelPresentMon/PresentMonAPI2/PresentMonAPI.h") + pmon_packaging_msbuild_file_path("${api2_header}" api2_header_native) + set( + loader_import_lib + "${PMON_OUTPUT_ROOT}/Release/PresentMonAPI2Loader.lib" + ) + pmon_packaging_msbuild_file_path("${loader_import_lib}" loader_import_lib_native) + set( + props + ${wix_integration_props} + /p:PmonCMakePackaging=true + /p:Configuration=Release + /p:Platform=x86 + /p:BuildProjectReferences=false + /p:PresentMonVersion=${PMON_VERSION} + /p:PresentMonProductVersion=${PMON_PRODUCT_VERSION} + "/p:PmonSolutionDir=${repo_root_native}" + "/p:PmonPayloadRoot=${payload_root_native}" + "/p:PmonInstallerExtensionDll=${extension_dll_native}" + "/p:PmonWixLibPath=${wixlib_output_native}" + "/p:PmonUciDistDir=${uci_stage_native}" + "/p:PmonUciDistFragmentPath=${uci_fragment_native}" + "/p:PmonProviderProjectDir=${provider_project_dir_native}" + "/p:PmonPresentMonAPI2ProjectDir=${api2_project_dir_native}" + "/p:PmonProviderTargetPath=${release_payload_dir_native}$" + "/p:PmonPresentMonTargetPath=${release_payload_dir_native}$" + "/p:PmonPresentMonTargetDir=${release_payload_dir_native}" + "/p:PmonPresentMonTargetFileName=$" + "/p:PmonKernelProcessTargetPath=${release_payload_dir_native}$" + "/p:PmonKernelProcessTargetFileName=$" + "/p:PmonPresentMonServiceTargetPath=${release_payload_dir_native}$" + "/p:PmonPresentMonServiceTargetDir=${release_payload_dir_native}" + "/p:PmonPresentMonServiceTargetFileName=$" + "/p:PmonPresentMonAPI2TargetPath=${release_payload_dir_native}$" + "/p:PmonPresentMonAPI2TargetFileName=$" + "/p:PmonPresentMonAPI2LoaderTargetPath=${release_payload_dir_native}$" + "/p:PmonPresentMonAPI2LoaderTargetDir=${release_payload_dir_native}" + "/p:PmonPresentMonAPI2LoaderTargetFileName=$" + "/p:PmonPresentMonAPI2LoaderLibSource=${loader_import_lib_native}" + "/p:PmonPresentMonAPI2HeaderSource=${api2_header_native}" + "/p:PmonConsoleTargetPath=${release_payload_dir_native}$" + "/p:PmonConsoleTargetFileName=$" + ) + set(${output_variable} "${props}" PARENT_SCOPE) +endfunction() + +function(pmon_packaging_wixlib_msbuild_properties output_variable) + pmon_packaging_wix_cmake_common_msbuild_properties(common_props) + pmon_packaging_msbuild_dir_path( + "${PMON_OUTPUT_ROOT}/Release" + wixlib_output_dir_native + ) + pmon_packaging_msbuild_dir_path( + "${CMAKE_BINARY_DIR}/packaging/Release/intermediate/PMInstallerLib" + wixlib_intermediate_native + ) + set( + props + ${common_props} + "/p:PmonOutputPath=${wixlib_output_dir_native}" + "/p:PmonIntermediateOutputPath=${wixlib_intermediate_native}" + ) + set(${output_variable} "${props}" PARENT_SCOPE) +endfunction() + +function(pmon_packaging_installer_msbuild_properties output_variable) + pmon_packaging_wix_cmake_common_msbuild_properties(common_props) + pmon_packaging_msbuild_dir_path( + "${PMON_OUTPUT_ROOT}/Release" + installer_output_dir_native + ) + pmon_packaging_msbuild_dir_path( + "${CMAKE_BINARY_DIR}/packaging/Release/intermediate/PMInstaller" + installer_intermediate_native + ) + set( + props + ${common_props} + "/p:PmonOutputPath=${installer_output_dir_native}" + "/p:PmonIntermediateOutputPath=${installer_intermediate_native}" + ) + set(${output_variable} "${props}" PARENT_SCOPE) +endfunction() + +function(pmon_packaging_merge_module_msbuild_properties output_variable) + pmon_packaging_wix_cmake_common_msbuild_properties(common_props) + pmon_packaging_msbuild_dir_path( + "${PMON_OUTPUT_ROOT}/Release/en-us" + msm_output_dir_native + ) + pmon_packaging_msbuild_dir_path( + "${CMAKE_BINARY_DIR}/packaging/Release/intermediate/ServiceMergeModule" + msm_intermediate_native + ) + set( + props + ${common_props} + "/p:PmonOutputPath=${msm_output_dir_native}" + "/p:PmonIntermediateOutputPath=${msm_intermediate_native}" + ) + set(${output_variable} "${props}" PARENT_SCOPE) +endfunction() + +function(pmon_packaging_resolve_wix_dll wix_root output_variable) + set(wix_dll "${wix_root}/bin/wix.dll") + if(NOT EXISTS "${wix_dll}") + set(wix_dll "${wix_root}/SDK/wix.dll") + endif() + set(${output_variable} "${wix_dll}" PARENT_SCOPE) +endfunction() + +function(pmon_packaging_preflight_script_args output_variable build_config) + set( + ${output_variable} + "${CMAKE_COMMAND}" + -DPMON_BUILD_CONFIG=${build_config} + -DPMON_ARCHITECTURE=${PMON_ARCHITECTURE} + -DPMON_BUILD_CONSOLE=${PMON_BUILD_CONSOLE} + -DPMON_BUILD_SERVICE=${PMON_BUILD_SERVICE} + -DPMON_BUILD_SDK=${PMON_BUILD_SDK} + -DPMON_BUILD_UI=${PMON_BUILD_UI} + -DPMON_BUILD_PROVIDER=${PMON_BUILD_PROVIDER} + -DPMON_OUTPUT_ROOT=${PMON_OUTPUT_ROOT} + -DPMON_VERSION=${PMON_VERSION} + -DPMON_PRODUCT_VERSION=${PMON_PRODUCT_VERSION} + -DPMON_DEPLOYMENT_PROFILE=${PMON_DEPLOYMENT_PROFILE} + -DPMON_UCI_ENABLED=${PMON_UCI_ENABLED} + # The root resolved at configure time, not the raw PMON_WIX_ROOT option, + # which is empty whenever WiX came from WIX or Program Files. Preflight + # must validate the same installation that WixTasksPath and WixToolPath + # already encode, so this leaves it nothing to rediscover. + -DPMON_WIX_ROOT=${PMON_PACKAGING_WIX_ROOT} + -DPMON_SOURCE_DIR=${CMAKE_SOURCE_DIR} + -DPMON_CMAKE_BINARY_DIR=${CMAKE_BINARY_DIR} + -DCMAKE_VS_MSBUILD_COMMAND=${CMAKE_VS_MSBUILD_COMMAND} + PARENT_SCOPE + ) +endfunction() + +function(pmon_packaging_preflight_command output_variable build_config) + pmon_packaging_preflight_script_args(preflight_args "${build_config}") + set( + ${output_variable} + ${preflight_args} + -P "${PMON_PACKAGING_CMAKE_DIR}/PresentMonPackagingPreflight.cmake" + PARENT_SCOPE + ) +endfunction() + +function(pmon_add_pm_installer_extension_target) + if(TARGET PMInstallerExtension) + return() + endif() + + if( + NOT DEFINED PMON_PACKAGING_WIX_ROOT + OR "${PMON_PACKAGING_WIX_ROOT}" STREQUAL "" + ) + message( + FATAL_ERROR + "PMInstallerExtension packaging requires PMON_PACKAGING_WIX_ROOT." + ) + endif() + set(wix_root "${PMON_PACKAGING_WIX_ROOT}") + + set(extension_project_dir "${CMAKE_SOURCE_DIR}/IntelPresentMon/PMInstallerExtension") + set(extension_csproj "${extension_project_dir}/PMInstallerExtension.csproj") + set(extension_output_dir "${PMON_OUTPUT_ROOT}/obj/PMInstallerExtension-Release") + set(extension_dll "${extension_output_dir}/PMInstallerExtension.dll") + set(extension_pdb "${extension_output_dir}/PMInstallerExtension.pdb") + set( + extension_stamp + "${CMAKE_BINARY_DIR}/packaging/Release/PMInstallerExtension.stamp" + ) + set( + extension_intermediate_dir + "${CMAKE_BINARY_DIR}/packaging/Release/intermediate/PMInstallerExtension" + ) + + pmon_packaging_resolve_wix_dll("${wix_root}" wix_dll) + pmon_packaging_native_dir_with_trailing_sep("${wix_root}" wix_root_native) + pmon_packaging_native_dir_with_trailing_sep( + "${extension_output_dir}" + extension_output_dir_native + ) + pmon_packaging_native_dir_with_trailing_sep( + "${extension_intermediate_dir}" + extension_intermediate_dir_native + ) + + pmon_packaging_preflight_command(preflight_command Release) + + set( + extension_depends + "${extension_csproj}" + "${extension_project_dir}/PMPreProcessor.cs" + "${extension_project_dir}/Properties/AssemblyInfo.cs" + "${wix_dll}" + ) + + add_custom_command( + OUTPUT "${extension_stamp}" + BYPRODUCTS "${extension_dll}" "${extension_pdb}" + COMMAND "${CMAKE_COMMAND}" -E make_directory "${CMAKE_BINARY_DIR}/packaging/Release" + COMMAND + "${CMAKE_COMMAND}" -E make_directory "${extension_output_dir}" + COMMAND + "${CMAKE_COMMAND}" -E make_directory "${extension_intermediate_dir}" + COMMAND ${preflight_command} + COMMAND + "${CMAKE_VS_MSBUILD_COMMAND}" + "${extension_csproj}" + /t:Build + /p:Configuration=Release + /p:Platform=AnyCPU + "/p:OutputPath=${extension_output_dir_native}" + "/p:IntermediateOutputPath=${extension_intermediate_dir_native}" + "/p:WIX=${wix_root_native}" + /p:BuildProjectReferences=false + /p:UseSharedCompilation=false + /nodeReuse:false + /m:1 + /nologo + /v:minimal + COMMAND + "${CMAKE_COMMAND}" + "-DPMON_VERIFY_OUTPUT=${extension_dll}" + -P "${PMON_PACKAGING_CMAKE_DIR}/PresentMonPackagingVerifyOutput.cmake" + COMMAND "${CMAKE_COMMAND}" -E touch "${extension_stamp}" + DEPENDS ${extension_depends} + COMMENT "Building PMInstallerExtension (WiX preprocessor extension, Release)" + VERBATIM + ) + + add_custom_target( + PMInstallerExtension + DEPENDS "${extension_stamp}" + ) + set_target_properties( + PMInstallerExtension + PROPERTIES + EXCLUDE_FROM_DEFAULT_BUILD ON + FOLDER "Packaging" + ) +endfunction() + +function(pmon_add_generate_uci_wix_target) + if(TARGET pmon_generate_uci_wix) + return() + endif() + + if( + NOT DEFINED PMON_PACKAGING_WIX_ROOT + OR "${PMON_PACKAGING_WIX_ROOT}" STREQUAL "" + ) + message( + FATAL_ERROR + "pmon_generate_uci_wix requires PMON_PACKAGING_WIX_ROOT." + ) + endif() + set(wix_root "${PMON_PACKAGING_WIX_ROOT}") + + set(packaging_script "${PMON_PACKAGING_PYTHON_SCRIPT}") + set(uci_stage_dir "${CMAKE_BINARY_DIR}/packaging/Release/UciDistStage") + set(uci_generated_dir "${CMAKE_BINARY_DIR}/packaging/Release/Generated") + set(uci_fragment "${uci_generated_dir}/UciDist.wxs") + set(uci_manifest "${uci_generated_dir}/.uci_runtime_manifest.txt") + set(uci_stamp "${CMAKE_BINARY_DIR}/packaging/Release/UciDist.stamp") + + pmon_packaging_preflight_command(preflight_command Release) + + set( + uci_command_depends + "${packaging_script}" + "${CMAKE_SOURCE_DIR}/Scripts/pmon/packaging.py" + ) + if(PMON_UCI_ENABLED) + list(APPEND uci_command_depends "${PMON_UCI_RUNTIME_DLL}") + file(GLOB_RECURSE uci_sdk_files CONFIGURE_DEPENDS + "${PMON_UCI_EFFECTIVE_ROOT}/*" + ) + list(APPEND uci_command_depends ${uci_sdk_files}) + endif() + + if(PMON_UCI_ENABLED) + set(uci_enabled_arg "true") + else() + set(uci_enabled_arg "false") + endif() + + add_custom_command( + OUTPUT "${uci_stamp}" + BYPRODUCTS "${uci_fragment}" "${uci_manifest}" + COMMAND "${CMAKE_COMMAND}" -E make_directory "${uci_stage_dir}" + COMMAND "${CMAKE_COMMAND}" -E make_directory "${uci_generated_dir}" + COMMAND ${preflight_command} + COMMAND + "${PMON_PYTHON_EXECUTABLE}" + "${packaging_script}" + stage-uci + --enabled "${uci_enabled_arg}" + --sdk-root "${PMON_UCI_EFFECTIVE_ROOT}" + --stage-dir "${uci_stage_dir}" + --manifest-path "${uci_manifest}" + COMMAND + "${PMON_PYTHON_EXECUTABLE}" + "${packaging_script}" + generate-uci + --enabled "${uci_enabled_arg}" + --stage-dir "${uci_stage_dir}" + --output-path "${uci_fragment}" + --wix-root "${wix_root}" + COMMAND "${CMAKE_COMMAND}" -E touch "${uci_stamp}" + DEPENDS ${uci_command_depends} + COMMENT "Staging runtime UCI payload and generating UciDist.wxs (Release)" + VERBATIM + ) + + add_custom_target( + pmon_generate_uci_wix + DEPENDS "${uci_stamp}" + ) + set_target_properties( + pmon_generate_uci_wix + PROPERTIES + EXCLUDE_FROM_DEFAULT_BUILD ON + FOLDER "Packaging" + ) +endfunction() + +function(pmon_add_pm_installer_lib_target) + if(TARGET PMInstallerLib) + return() + endif() + + if( + NOT DEFINED PMON_PACKAGING_WIX_ROOT + OR "${PMON_PACKAGING_WIX_ROOT}" STREQUAL "" + ) + message( + FATAL_ERROR + "PMInstallerLib packaging requires PMON_PACKAGING_WIX_ROOT." + ) + endif() + set(wix_root "${PMON_PACKAGING_WIX_ROOT}") + + set(wixlib_project_dir "${CMAKE_SOURCE_DIR}/IntelPresentMon/PMInstallerLib") + set(wixlib_wixproj "${wixlib_project_dir}/PMInstallerLib.wixproj") + set( + wixlib_output + "${PMON_OUTPUT_ROOT}/Release/PMInstallerLib.wixlib" + ) + set(wixlib_stamp "${CMAKE_BINARY_DIR}/packaging/Release/PMInstallerLib.stamp") + set( + wixlib_intermediate_dir + "${CMAKE_BINARY_DIR}/packaging/Release/intermediate/PMInstallerLib" + ) + set( + native_snapshot + "${CMAKE_BINARY_DIR}/packaging/Release/PMInstallerLib.native_snapshot.txt" + ) + set( + verify_native_script + "${PMON_PACKAGING_PYTHON_SCRIPT}" + ) + set(uci_fragment "${CMAKE_BINARY_DIR}/packaging/Release/Generated/UciDist.wxs") + set( + extension_dll + "${PMON_OUTPUT_ROOT}/obj/PMInstallerExtension-Release/PMInstallerExtension.dll" + ) + + pmon_packaging_wixlib_msbuild_properties(wixlib_msbuild_props) + pmon_packaging_native_dir_with_trailing_sep("${wix_root}" wix_root_native) + pmon_packaging_preflight_command(preflight_command Release) + + set(uci_stamp "${CMAKE_BINARY_DIR}/packaging/Release/UciDist.stamp") + + set( + wixlib_command_depends + "${wixlib_wixproj}" + "${wixlib_project_dir}/Library.wxs" + "${wixlib_project_dir}/Library.en-us.wxl" + "${CMAKE_SOURCE_DIR}/IntelPresentMon/PmonCMakePackaging.wixproj.props" + "${extension_dll}" + "${uci_fragment}" + "${uci_stamp}" + "${verify_native_script}" + "$" + "$" + "$/ddETWExternal.xml" + ) + + if(PMON_DEPLOYMENT_PROFILE STREQUAL "PRODUCTION") + list( + APPEND + wixlib_command_depends + "${CMAKE_BINARY_DIR}/packaging/Release/ProductionPayloadSigned.stamp" + "${CMAKE_BINARY_DIR}/packaging/Release/ProductionPackageInputsVerified.stamp" + ) + endif() + + set(release_payload_dir "${PMON_OUTPUT_ROOT}/Release") + set( + wixlib_native_paths_list + "${release_payload_dir}/$|${release_payload_dir}/$|${release_payload_dir}/ddETWExternal.xml" + ) + + set(wixlib_production_relink_commands "") + if(PMON_DEPLOYMENT_PROFILE STREQUAL "PRODUCTION") + set( + wixlib_production_relink_commands + COMMAND "${CMAKE_COMMAND}" -E rm -f "${wixlib_output}" + COMMAND "${CMAKE_COMMAND}" -E remove_directory "${wixlib_intermediate_dir}" + COMMAND "${CMAKE_COMMAND}" -E make_directory "${wixlib_intermediate_dir}" + ) + endif() + + add_custom_command( + OUTPUT "${wixlib_stamp}" + BYPRODUCTS "${wixlib_output}" + COMMAND "${CMAKE_COMMAND}" -E make_directory "${CMAKE_BINARY_DIR}/packaging/Release" + COMMAND + "${CMAKE_COMMAND}" -E make_directory "${wixlib_intermediate_dir}" + COMMAND + "${CMAKE_COMMAND}" -E make_directory "${PMON_OUTPUT_ROOT}/Release" + COMMAND ${preflight_command} + COMMAND + "${PMON_PYTHON_EXECUTABLE}" + "${verify_native_script}" + native-snapshot record + --snapshot-path "${native_snapshot}" + --native-paths-list "${wixlib_native_paths_list}" + ${wixlib_production_relink_commands} + COMMAND + "${CMAKE_VS_MSBUILD_COMMAND}" + "${wixlib_wixproj}" + /t:Build + ${wixlib_msbuild_props} + "/p:WIX=${wix_root_native}" + /p:UseSharedCompilation=false + /nodeReuse:false + /m:1 + /nologo + /v:minimal + COMMAND + "${CMAKE_COMMAND}" + "-DPMON_VERIFY_OUTPUT=${wixlib_output}" + -P "${PMON_PACKAGING_CMAKE_DIR}/PresentMonPackagingVerifyOutput.cmake" + COMMAND + "${PMON_PYTHON_EXECUTABLE}" + "${verify_native_script}" + native-snapshot verify + --snapshot-path "${native_snapshot}" + --native-paths-list "${wixlib_native_paths_list}" + COMMAND "${CMAKE_COMMAND}" -E touch "${wixlib_stamp}" + DEPENDS ${wixlib_command_depends} + COMMENT "Building PMInstallerLib.wixlib (WiX shared service library, Release)" + VERBATIM + ) + + add_custom_target( + PMInstallerLib + DEPENDS "${wixlib_stamp}" + ) + set_target_properties( + PMInstallerLib + PROPERTIES + EXCLUDE_FROM_DEFAULT_BUILD ON + FOLDER "Packaging" + ) +endfunction() + +function(pmon_packaging_installer_native_pe_paths output_variable) + set(release_dir "${PMON_OUTPUT_ROOT}/Release") + set( + ${output_variable} + "${release_dir}/$|${release_dir}/$|${release_dir}/$|${release_dir}/$|${release_dir}/$|${release_dir}/$|${release_dir}/$" + PARENT_SCOPE + ) +endfunction() + +function(pmon_packaging_merge_module_native_paths output_variable) + set(release_dir "${PMON_OUTPUT_ROOT}/Release") + set( + ${output_variable} + "${release_dir}/$|${release_dir}/$|${release_dir}/ddETWExternal.xml" + PARENT_SCOPE + ) +endfunction() + +function(pmon_packaging_merge_module_native_pe_paths output_variable) + set(release_dir "${PMON_OUTPUT_ROOT}/Release") + set( + ${output_variable} + "${release_dir}/$|${release_dir}/$" + PARENT_SCOPE + ) +endfunction() + +function(pmon_packaging_uci_runtime_paths uci_stage_dir uci_manifest_path) + set( + ${uci_stage_dir} + "${CMAKE_BINARY_DIR}/packaging/Release/UciDistStage" + PARENT_SCOPE + ) + set( + ${uci_manifest_path} + "${CMAKE_BINARY_DIR}/packaging/Release/Generated/.uci_runtime_manifest.txt" + PARENT_SCOPE + ) +endfunction() + +function(pmon_add_developer_release_installer_to_default_build) + if(NOT PMON_DEPLOYMENT_PROFILE STREQUAL "DEVELOPER") + return() + endif() + + # Visual Studio's ALL_BUILD project references are not configuration- + # specific. Keep this bridge in ALL for both configurations, make its + # command Release-only, and let the canonical installer target retain the + # explicit packaging graph and Release-only preflight. + string( + CONCAT + developer_release_installer_command + "$<$:${CMAKE_COMMAND};--build;" + "${CMAKE_BINARY_DIR};--config;Release;--target;" + "PresentMonInstaller;--parallel;1>" + ) + add_custom_target( + pmon_developer_release_installer ALL + COMMAND "${developer_release_installer_command}" + COMMAND_EXPAND_LISTS + VERBATIM + ) + set_target_properties( + pmon_developer_release_installer + PROPERTIES FOLDER "Packaging" + ) +endfunction() + +function(pmon_add_present_mon_installer_target) + if(TARGET PresentMonInstaller) + return() + endif() + + if( + NOT DEFINED PMON_PACKAGING_WIX_ROOT + OR "${PMON_PACKAGING_WIX_ROOT}" STREQUAL "" + ) + message( + FATAL_ERROR + "PresentMonInstaller packaging requires PMON_PACKAGING_WIX_ROOT." + ) + endif() + set(wix_root "${PMON_PACKAGING_WIX_ROOT}") + + set(installer_project_dir "${CMAKE_SOURCE_DIR}/IntelPresentMon/PMInstaller") + set(installer_wixproj "${installer_project_dir}/PMInstaller.wixproj") + set(msi_output "${PMON_OUTPUT_ROOT}/Release/en-us/PresentMon.msi") + set(installer_stamp "${CMAKE_BINARY_DIR}/packaging/Release/PresentMonInstaller.stamp") + set( + installer_intermediate_dir + "${CMAKE_BINARY_DIR}/packaging/Release/intermediate/PMInstaller" + ) + set( + installer_log_dir + "${CMAKE_BINARY_DIR}/packaging/Release/logs" + ) + set(installer_binlog "${installer_log_dir}/PMInstaller.binlog") + set( + native_snapshot + "${CMAKE_BINARY_DIR}/packaging/Release/PresentMonInstaller.native_snapshot.txt" + ) + set( + verify_native_script + "${PMON_PACKAGING_PYTHON_SCRIPT}" + ) + set( + link_preflight_script + "${PMON_PACKAGING_PYTHON_SCRIPT}" + ) + pmon_packaging_uci_runtime_paths(uci_stage_dir uci_manifest_path) + set(link_preflight_skip_pe_args "") + set( + wixlib_output + "${PMON_OUTPUT_ROOT}/Release/PMInstallerLib.wixlib" + ) + set( + extension_dll + "${PMON_OUTPUT_ROOT}/obj/PMInstallerExtension-Release/PMInstallerExtension.dll" + ) + + set( + verify_msi_script + "${PMON_PACKAGING_PYTHON_SCRIPT}" + ) + set( + wixlib_native_snapshot + "${CMAKE_BINARY_DIR}/packaging/Release/PMInstallerLib.native_snapshot.txt" + ) + pmon_packaging_merge_module_native_paths(wixlib_native_paths) + + pmon_packaging_installer_msbuild_properties(installer_msbuild_props) + pmon_packaging_native_dir_with_trailing_sep("${wix_root}" wix_root_native) + pmon_packaging_preflight_command(preflight_command Release) + pmon_packaging_installer_native_pe_paths(installer_native_pe_paths) + + set(wixlib_stamp "${CMAKE_BINARY_DIR}/packaging/Release/PMInstallerLib.stamp") + + set( + installer_command_depends + "${installer_wixproj}" + "${installer_project_dir}/PresentMon.wxs" + "${installer_project_dir}/CefBinaries.wxs" + "${installer_project_dir}/CefResources.wxs" + "${installer_project_dir}/en-us.wxl" + "${installer_project_dir}/license.rtf" + "${CMAKE_SOURCE_DIR}/IntelPresentMon/PmonCMakePackaging.wixproj.props" + "${wixlib_output}" + "${wixlib_stamp}" + "${extension_dll}" + "${verify_native_script}" + "${link_preflight_script}" + "${verify_msi_script}" + "${CMAKE_SOURCE_DIR}/Scripts/pmon/packaging.py" + "${wixlib_native_snapshot}" + ) + + set(installer_wixlib_snapshot_verify_command "") + set(installer_embedded_native_verify_command "") + if(PMON_DEPLOYMENT_PROFILE STREQUAL "PRODUCTION") + set( + installer_wixlib_snapshot_verify_command + COMMAND + "${PMON_PYTHON_EXECUTABLE}" + "${verify_native_script}" + native-snapshot verify + --snapshot-path "${wixlib_native_snapshot}" + --native-paths-list "${wixlib_native_paths}" + ) + set( + installer_embedded_native_verify_command + COMMAND + "${PMON_PYTHON_EXECUTABLE}" + "${verify_msi_script}" + verify-msi + --msi-path "${msi_output}" + --payload-root "${PMON_OUTPUT_ROOT}/Release" + --repo-root "${CMAKE_SOURCE_DIR}" + --signtool "${PMON_SIGNTOOL_EXECUTABLE}" + --native-paths-list "${installer_native_pe_paths}" + --snapshot-path "${native_snapshot}" + --skip-msi-authenticode + ) + endif() + + if(PMON_DEPLOYMENT_PROFILE STREQUAL "PRODUCTION") + list( + APPEND + installer_command_depends + "${CMAKE_BINARY_DIR}/packaging/Release/ProductionPayloadSigned.stamp" + "${CMAKE_BINARY_DIR}/packaging/Release/ProductionPackageInputsVerified.stamp" + ) + endif() + + add_custom_command( + OUTPUT "${installer_stamp}" + BYPRODUCTS "${msi_output}" + COMMAND "${CMAKE_COMMAND}" -E make_directory "${CMAKE_BINARY_DIR}/packaging/Release" + COMMAND "${CMAKE_COMMAND}" -E make_directory "${installer_intermediate_dir}" + COMMAND "${CMAKE_COMMAND}" -E make_directory "${installer_log_dir}" + COMMAND + "${CMAKE_COMMAND}" -E make_directory + "${PMON_OUTPUT_ROOT}/Release/en-us" + COMMAND ${preflight_command} + ${installer_wixlib_snapshot_verify_command} + COMMAND + "${PMON_PYTHON_EXECUTABLE}" + "${link_preflight_script}" + verify-link-preflight + --payload-root "${PMON_OUTPUT_ROOT}/Release" + --repo-root "${CMAKE_SOURCE_DIR}" + --dependency-root "${PMON_DEPENDENCY_ROOT}" + --toolset "${PMON_VS_TOOLSET}" + --deployment-profile "${PMON_DEPLOYMENT_PROFILE}" + --signtool "${PMON_SIGNTOOL_EXECUTABLE}" + --native-paths-list "${installer_native_pe_paths}" + --uci-enabled "${PMON_UCI_ENABLED}" + --uci-stage-dir "${uci_stage_dir}" + --uci-manifest-path "${uci_manifest_path}" + ${link_preflight_skip_pe_args} + COMMAND + "${PMON_PYTHON_EXECUTABLE}" + "${verify_native_script}" + native-snapshot record + --snapshot-path "${native_snapshot}" + --native-paths-list "${installer_native_pe_paths}" + COMMAND + "${CMAKE_VS_MSBUILD_COMMAND}" + "${installer_wixproj}" + /t:Build + ${installer_msbuild_props} + "/p:WIX=${wix_root_native}" + /p:UseSharedCompilation=false + /nodeReuse:false + "/bl:${installer_binlog}" + /m:1 + /nologo + /v:minimal + COMMAND + "${CMAKE_COMMAND}" + "-DPMON_VERIFY_OUTPUT=${msi_output}" + -P "${PMON_PACKAGING_CMAKE_DIR}/PresentMonPackagingVerifyOutput.cmake" + ${installer_embedded_native_verify_command} + COMMAND + "${PMON_PYTHON_EXECUTABLE}" + "${verify_native_script}" + native-snapshot verify + --snapshot-path "${native_snapshot}" + --native-paths-list "${installer_native_pe_paths}" + COMMAND "${CMAKE_COMMAND}" -E touch "${installer_stamp}" + DEPENDS ${installer_command_depends} + COMMENT "Linking PresentMon.msi (WiX product package, Release)" + VERBATIM + ) + + add_custom_target( + PresentMonInstaller + DEPENDS "${installer_stamp}" + ) + set_target_properties( + PresentMonInstaller + PROPERTIES + EXCLUDE_FROM_DEFAULT_BUILD ON + FOLDER "Packaging" + ) + pmon_add_developer_release_installer_to_default_build() +endfunction() + +function(pmon_add_present_mon_merge_module_target) + if(TARGET PresentMonMergeModule) + return() + endif() + + if( + NOT DEFINED PMON_PACKAGING_WIX_ROOT + OR "${PMON_PACKAGING_WIX_ROOT}" STREQUAL "" + ) + message( + FATAL_ERROR + "PresentMonMergeModule packaging requires PMON_PACKAGING_WIX_ROOT." + ) + endif() + set(wix_root "${PMON_PACKAGING_WIX_ROOT}") + + set(msm_project_dir "${CMAKE_SOURCE_DIR}/IntelPresentMon/ServiceMergeModule") + set(msm_wixproj "${msm_project_dir}/ServiceMergeModule.wixproj") + set(msm_output "${PMON_OUTPUT_ROOT}/Release/en-us/PresentMonSharedService.msm") + set(msm_stamp "${CMAKE_BINARY_DIR}/packaging/Release/PresentMonMergeModule.stamp") + set( + msm_intermediate_dir + "${CMAKE_BINARY_DIR}/packaging/Release/intermediate/ServiceMergeModule" + ) + set( + msm_log_dir + "${CMAKE_BINARY_DIR}/packaging/Release/logs" + ) + set(msm_binlog "${msm_log_dir}/ServiceMergeModule.binlog") + set( + native_snapshot + "${CMAKE_BINARY_DIR}/packaging/Release/PresentMonMergeModule.native_snapshot.txt" + ) + set( + verify_native_script + "${PMON_PACKAGING_PYTHON_SCRIPT}" + ) + set( + production_pe_verify_script + "${PMON_PACKAGING_PYTHON_SCRIPT}" + ) + set( + wixlib_output + "${PMON_OUTPUT_ROOT}/Release/PMInstallerLib.wixlib" + ) + + pmon_packaging_merge_module_msbuild_properties(msm_msbuild_props) + pmon_packaging_native_dir_with_trailing_sep("${wix_root}" wix_root_native) + pmon_packaging_preflight_command(preflight_command Release) + pmon_packaging_merge_module_native_paths(msm_native_paths) + pmon_packaging_merge_module_native_pe_paths(msm_native_pe_paths) + pmon_packaging_uci_runtime_paths(uci_stage_dir uci_manifest_path) + set(msm_production_pe_verify_command "") + if(PMON_DEPLOYMENT_PROFILE STREQUAL "PRODUCTION") + set( + msm_production_pe_verify_command + COMMAND + "${PMON_PYTHON_EXECUTABLE}" + "${production_pe_verify_script}" + verify-production-inputs + --signtool "${PMON_SIGNTOOL_EXECUTABLE}" + --native-paths-list "${msm_native_pe_paths}" + --uci-enabled "${PMON_UCI_ENABLED}" + --uci-stage-dir "${uci_stage_dir}" + --uci-manifest-path "${uci_manifest_path}" + ) + endif() + + set(wixlib_stamp "${CMAKE_BINARY_DIR}/packaging/Release/PMInstallerLib.stamp") + + set(msm_production_relink_commands "") + if(PMON_DEPLOYMENT_PROFILE STREQUAL "PRODUCTION") + set( + msm_production_relink_commands + COMMAND "${CMAKE_COMMAND}" -E rm -f "${msm_output}" + COMMAND "${CMAKE_COMMAND}" -E remove_directory "${msm_intermediate_dir}" + COMMAND "${CMAKE_COMMAND}" -E make_directory "${msm_intermediate_dir}" + ) + endif() + + set( + msm_command_depends + "${msm_wixproj}" + "${msm_project_dir}/ServiceMergeModule.wxs" + "${CMAKE_SOURCE_DIR}/IntelPresentMon/PmonCMakePackaging.wixproj.props" + "${wixlib_output}" + "${wixlib_stamp}" + "${verify_native_script}" + "${production_pe_verify_script}" + ) + + add_custom_command( + OUTPUT "${msm_stamp}" + BYPRODUCTS "${msm_output}" + COMMAND "${CMAKE_COMMAND}" -E make_directory "${CMAKE_BINARY_DIR}/packaging/Release" + COMMAND "${CMAKE_COMMAND}" -E make_directory "${msm_intermediate_dir}" + COMMAND "${CMAKE_COMMAND}" -E make_directory "${msm_log_dir}" + COMMAND + "${CMAKE_COMMAND}" -E make_directory + "${PMON_OUTPUT_ROOT}/Release/en-us" + COMMAND ${preflight_command} + ${msm_production_pe_verify_command} + COMMAND + "${PMON_PYTHON_EXECUTABLE}" + "${verify_native_script}" + native-snapshot record + --snapshot-path "${native_snapshot}" + --native-paths-list "${msm_native_paths}" + ${msm_production_relink_commands} + COMMAND + "${CMAKE_VS_MSBUILD_COMMAND}" + "${msm_wixproj}" + /t:Build + ${msm_msbuild_props} + "/p:WIX=${wix_root_native}" + /p:UseSharedCompilation=false + /nodeReuse:false + "/bl:${msm_binlog}" + /m:1 + /nologo + /v:minimal + COMMAND + "${CMAKE_COMMAND}" + "-DPMON_VERIFY_OUTPUT=${msm_output}" + -P "${PMON_PACKAGING_CMAKE_DIR}/PresentMonPackagingVerifyOutput.cmake" + COMMAND + "${PMON_PYTHON_EXECUTABLE}" + "${verify_native_script}" + native-snapshot verify + --snapshot-path "${native_snapshot}" + --native-paths-list "${msm_native_paths}" + COMMAND "${CMAKE_COMMAND}" -E touch "${msm_stamp}" + DEPENDS ${msm_command_depends} + COMMENT "Linking PresentMonSharedService.msm (WiX merge module, Release)" + VERBATIM + ) + + add_custom_target( + PresentMonMergeModule + DEPENDS "${msm_stamp}" + ) + set_target_properties( + PresentMonMergeModule + PROPERTIES + EXCLUDE_FROM_DEFAULT_BUILD ON + FOLDER "Packaging" + ) +endfunction() + +function(pmon_add_verify_production_package_inputs_target) + if(NOT PMON_DEPLOYMENT_PROFILE STREQUAL "PRODUCTION") + return() + endif() + + if(TARGET pmon_verify_production_package_inputs) + return() + endif() + + set( + production_pe_verify_script + "${PMON_PACKAGING_PYTHON_SCRIPT}" + ) + set( + verify_stamp + "${CMAKE_BINARY_DIR}/packaging/Release/ProductionPackageInputsVerified.stamp" + ) + pmon_packaging_installer_native_pe_paths(installer_native_pe_paths) + pmon_packaging_uci_runtime_paths(uci_stage_dir uci_manifest_path) + set( + payload_sign_stamp + "${CMAKE_BINARY_DIR}/packaging/Release/ProductionPayloadSigned.stamp" + ) + + add_custom_command( + OUTPUT "${verify_stamp}" + COMMAND "${CMAKE_COMMAND}" -E make_directory "${CMAKE_BINARY_DIR}/packaging/Release" + COMMAND + "${PMON_PYTHON_EXECUTABLE}" + "${production_pe_verify_script}" + verify-production-inputs + --signtool "${PMON_SIGNTOOL_EXECUTABLE}" + --native-paths-list "${installer_native_pe_paths}" + --uci-enabled "${PMON_UCI_ENABLED}" + --uci-stage-dir "${uci_stage_dir}" + --uci-manifest-path "${uci_manifest_path}" + --skip-uci + COMMAND "${CMAKE_COMMAND}" -E touch "${verify_stamp}" + DEPENDS + "${production_pe_verify_script}" + "${CMAKE_SOURCE_DIR}/Scripts/pmon/packaging.py" + "${payload_sign_stamp}" + COMMENT "Verify production Authenticode on EDSS native PE inputs before UCI staging (Release)" + VERBATIM + ) + + add_custom_target( + pmon_verify_production_package_inputs + DEPENDS "${verify_stamp}" + ) + set_target_properties( + pmon_verify_production_package_inputs + PROPERTIES + EXCLUDE_FROM_DEFAULT_BUILD ON + FOLDER "Packaging" + ) +endfunction() + +function(pmon_add_sign_production_installer_target) + if(NOT PMON_DEPLOYMENT_PROFILE STREQUAL "PRODUCTION") + return() + endif() + + if(TARGET pmon_sign_production_installer) + return() + endif() + + set( + sign_script + "${PMON_PACKAGING_PYTHON_SCRIPT}" + ) + set( + verify_msi_script + "${PMON_PACKAGING_PYTHON_SCRIPT}" + ) + set(msi_output "${PMON_OUTPUT_ROOT}/Release/en-us/PresentMon.msi") + set( + sign_stamp + "${CMAKE_BINARY_DIR}/packaging/Release/PresentMonInstallerSigned.stamp" + ) + set( + native_snapshot + "${CMAKE_BINARY_DIR}/packaging/Release/PresentMonInstaller.native_snapshot.txt" + ) + set( + installer_packaging_stamp + "${CMAKE_BINARY_DIR}/packaging/Release/PresentMonInstaller.stamp" + ) + pmon_packaging_installer_native_pe_paths(installer_native_pe_paths) + + set( + payload_sign_stamp + "${CMAKE_BINARY_DIR}/packaging/Release/ProductionPayloadSigned.stamp" + ) + + set(sign_skip_edss_args "") + if(PMON_PACKAGING_MSI_EDSS_SIGN_SKIP) + set(sign_skip_edss_args --skip-edss) + endif() + + set(sign_mode_args "") + if(PMON_EDSS_IN_PROCESS_SIGNING) + set(sign_mode_args --in-process-signing) + else() + set(sign_mode_args --impersonated-signing) + endif() + + add_custom_command( + OUTPUT "${sign_stamp}" + COMMAND + "${PMON_PYTHON_EXECUTABLE}" + "${sign_script}" + sign-installer + --msi-path "${msi_output}" + --payload-root "${PMON_OUTPUT_ROOT}/Release" + --repo-root "${CMAKE_SOURCE_DIR}" + --signtool "${PMON_SIGNTOOL_EXECUTABLE}" + --edss-script "${PMON_EDSS_SIGN_SCRIPT}" + --native-paths-list "${installer_native_pe_paths}" + --snapshot-path "${native_snapshot}" + ${sign_mode_args} + ${sign_skip_edss_args} + COMMAND "${CMAKE_COMMAND}" -E touch "${sign_stamp}" + DEPENDS + "${sign_script}" + "${verify_msi_script}" + "${CMAKE_SOURCE_DIR}/Scripts/pmon/packaging.py" + "${installer_packaging_stamp}" + "${native_snapshot}" + "${payload_sign_stamp}" + "${msi_output}" + COMMENT "Production MSI signing gate and post-sign verification (Release)" + VERBATIM + ) + + add_custom_target( + pmon_sign_production_installer + DEPENDS + PresentMonInstaller + "${sign_stamp}" + ) + set_target_properties( + pmon_sign_production_installer + PROPERTIES + EXCLUDE_FROM_DEFAULT_BUILD ON + FOLDER "Packaging" + ) +endfunction() + +function(pmon_add_require_signed_production_payload_target) + if(NOT PMON_DEPLOYMENT_PROFILE STREQUAL "PRODUCTION") + return() + endif() + + if(TARGET pmon_require_signed_production_payload) + return() + endif() + + set( + preflight_script + "${PMON_PACKAGING_PYTHON_SCRIPT}" + ) + set( + payload_sign_stamp + "${CMAKE_BINARY_DIR}/packaging/Release/ProductionPayloadSigned.stamp" + ) + pmon_packaging_installer_native_pe_paths(installer_native_pe_paths) + pmon_packaging_uci_runtime_paths(uci_stage_dir uci_manifest_path) + + add_custom_target( + pmon_require_signed_production_payload + COMMAND + "${PMON_PYTHON_EXECUTABLE}" + "${preflight_script}" + require-signed-payload + --stamp "${payload_sign_stamp}" + --signtool "${PMON_SIGNTOOL_EXECUTABLE}" + --native-paths-list "${installer_native_pe_paths}" + --uci-enabled "${PMON_UCI_ENABLED}" + --uci-stage-dir "${uci_stage_dir}" + --uci-manifest-path "${uci_manifest_path}" + DEPENDS + "${preflight_script}" + "${CMAKE_SOURCE_DIR}/Scripts/pmon/packaging.py" + "${payload_sign_stamp}" + COMMENT "Require signed+verified production payload before packaging (Release)" + VERBATIM + ) + set_target_properties( + pmon_require_signed_production_payload + PROPERTIES + EXCLUDE_FROM_DEFAULT_BUILD ON + FOLDER "Packaging" + ) + + if(TARGET pmon_sign_production_payload) + add_dependencies( + pmon_require_signed_production_payload + pmon_sign_production_payload + ) + endif() + + if(NOT TARGET pmon_production_payload_signed) + add_custom_target( + pmon_production_payload_signed + DEPENDS + pmon_require_signed_production_payload + COMMENT + "Production gate: EDSS payload signed (stamp) and verified before packaging" + ) + set_target_properties( + pmon_production_payload_signed + PROPERTIES + EXCLUDE_FROM_DEFAULT_BUILD ON + FOLDER "Packaging" + ) + endif() +endfunction() + +function(pmon_add_production_payload_signed_packaging_dependencies) + if(NOT PMON_DEPLOYMENT_PROFILE STREQUAL "PRODUCTION") + return() + endif() + if(NOT TARGET pmon_production_payload_signed) + return() + endif() + + foreach( + packaging_target + IN + ITEMS + PMInstallerExtension + pmon_generate_uci_wix + pmon_verify_production_package_inputs + ) + if(TARGET ${packaging_target}) + add_dependencies( + ${packaging_target} + pmon_production_payload_signed + ) + endif() + endforeach() +endfunction() + +function(pmon_register_packaging_targets) + if(NOT PMON_BUILD_SCOPE STREQUAL "FULL") + message(STATUS "PresentMon packaging targets are disabled by PMON_BUILD_SCOPE=${PMON_BUILD_SCOPE}.") + return() + endif() + + if(NOT PMON_ARCHITECTURE STREQUAL "x64") + message( + STATUS + "PresentMon packaging targets are unavailable: packaging requires an x64 configure." + ) + return() + endif() + + pmon_packaging_components_enabled(components_enabled) + if(NOT components_enabled) + set(missing_options "") + foreach(option_name IN ITEMS + PMON_BUILD_CONSOLE + PMON_BUILD_SERVICE + PMON_BUILD_SDK + PMON_BUILD_UI + PMON_BUILD_PROVIDER + ) + if(NOT ${option_name}) + if(missing_options) + set(missing_options "${missing_options}, ${option_name}=OFF") + else() + set(missing_options "${option_name}=OFF") + endif() + endif() + endforeach() + message( + STATUS + "PresentMon packaging targets are unavailable: enable ${missing_options}." + ) + return() + endif() + + pmon_require_packaging_native_targets() + + pmon_resolve_wix_root(wix_root wix_reason) + pmon_print_packaging_summary("${wix_root}" "${wix_reason}") + + if(NOT wix_root) + set( + PMON_PACKAGING_AVAILABLE + FALSE + CACHE INTERNAL + "PresentMon packaging targets registered" + FORCE + ) + if(PMON_DEPLOYMENT_PROFILE STREQUAL "PRODUCTION") + message(FATAL_ERROR "Production configuration requires WiX Toolset 3. ${wix_reason}") + endif() + if(PMON_DEPLOYMENT_PROFILE STREQUAL "DEVELOPER") + add_custom_target( + PresentMonInstaller + COMMAND + "${CMAKE_COMMAND}" -E echo + "Developer Release requires WiX Toolset 3 to build PresentMon.msi. ${wix_reason}" + COMMAND "${CMAKE_COMMAND}" -E false + COMMENT "PresentMon.msi is unavailable without WiX Toolset 3" + VERBATIM + ) + set_target_properties( + PresentMonInstaller + PROPERTIES + EXCLUDE_FROM_DEFAULT_BUILD ON + FOLDER "Packaging" + ) + pmon_add_developer_release_installer_to_default_build() + endif() + return() + endif() + + set(PMON_PACKAGING_AVAILABLE TRUE CACHE INTERNAL "PresentMon packaging targets registered" FORCE) + set( + PMON_PACKAGING_WIX_ROOT + "${wix_root}" + CACHE INTERNAL + "Resolved WiX Toolset 3 root used by packaging MSBuild invocations" + ) + + pmon_add_pm_installer_extension_target() + pmon_add_generate_uci_wix_target() + pmon_add_pm_installer_lib_target() + pmon_add_present_mon_installer_target() + pmon_add_present_mon_merge_module_target() + pmon_add_verify_production_package_inputs_target() + pmon_add_require_signed_production_payload_target() + pmon_add_production_payload_signed_packaging_dependencies() + pmon_add_sign_production_installer_target() + set(pmon_packaging_payload_targets + PresentMonUI + KernelProcess + PresentMonService + PresentMonAPI2 + PresentMonAPI2Loader + pmon_provider + PresentMonConsole + ) + set(pmon_packaging_staging_targets + pmon_stage_cef_runtime + pmon_verify_cef + pmon_stage_ui_payload + pmon_stage_kernel_payload + pmon_compile_shaders + ) + if(TARGET pmon_developer_release_installer) + add_dependencies( + pmon_developer_release_installer + ${pmon_packaging_payload_targets} + ${pmon_packaging_staging_targets} + ) + endif() + add_dependencies(PresentMonInstaller PMInstallerExtension) + add_dependencies(PresentMonInstaller PMInstallerLib) + if(NOT PMON_DEPLOYMENT_PROFILE STREQUAL "PRODUCTION") + add_dependencies(PresentMonInstaller ${pmon_packaging_staging_targets}) + endif() + if(TARGET pmon_verify_production_package_inputs) + add_dependencies(PresentMonInstaller pmon_verify_production_package_inputs) + add_dependencies(PMInstallerLib pmon_verify_production_package_inputs) + endif() + add_dependencies(PMInstallerLib PMInstallerExtension) + add_dependencies(PMInstallerLib pmon_generate_uci_wix) + add_dependencies(PresentMonMergeModule PMInstallerLib) + add_dependencies(PresentMonMergeModule pmon_generate_uci_wix) + + if(PMON_DEPLOYMENT_PROFILE STREQUAL "PRODUCTION" AND NOT PMON_PACKAGING_MSI_EDSS_SIGN_SKIP) + message( + STATUS + "pmon_sign_production_installer: invokes PMON_EDSS_SIGN_SCRIPT with -SignMsiOnly, " + "-OutputRoot, -RepoRoot, -Verify, and -InProcessSigning or -ImpersonatedSigning (MSI only; MSM is " + "unsigned per legacy policy; natives are signed by pmon_sign_production_payload). " + "Use -DPMON_PACKAGING_MSI_EDSS_SIGN_SKIP=ON " + "to verify a manually signed MSI without invoking EDSS." + ) + endif() + + if(NOT TARGET pmon_build_packages) + add_custom_target( + pmon_build_packages + DEPENDS + PresentMonInstaller + PresentMonMergeModule + COMMENT "Build unsigned PresentMon MSI and MSM" + ) + set_target_properties( + pmon_build_packages + PROPERTIES + EXCLUDE_FROM_DEFAULT_BUILD ON + FOLDER "Packaging" + ) + endif() + + if(TARGET pmon_production_payload_signed) + add_dependencies(pmon_build_packages pmon_production_payload_signed) + elseif(TARGET pmon_require_signed_production_payload) + add_dependencies(pmon_build_packages pmon_require_signed_production_payload) + endif() + + if(PMON_DEPLOYMENT_PROFILE STREQUAL "PRODUCTION" AND NOT TARGET pmon_production_packaging_full) + # Serial gate: payload EDSS (step 1) before unsigned MSI/MSM link (step 2). Not used by + # pmon_build_packages alone (stamp + preflight only via pmon_require_signed_production_payload). + add_custom_target( + pmon_production_packaging_after_payload_sign + DEPENDS + pmon_build_packages + COMMENT "Production packaging: build packages after payload sign (internal ordering)" + ) + set_target_properties( + pmon_production_packaging_after_payload_sign + PROPERTIES + EXCLUDE_FROM_DEFAULT_BUILD ON + FOLDER "Packaging" + ) + if(TARGET pmon_sign_production_payload) + add_dependencies( + pmon_production_packaging_after_payload_sign + pmon_sign_production_payload + ) + endif() + add_dependencies(pmon_sign_production_installer pmon_production_packaging_after_payload_sign) + + add_custom_target( + pmon_production_packaging_full + COMMENT + "Production packaging: EDSS natives, build MSI/MSM, SignMsiOnly" + ) + if(TARGET pmon_sign_production_payload) + add_dependencies( + pmon_production_packaging_full + pmon_sign_production_payload + ) + endif() + if(TARGET pmon_sign_production_installer) + add_dependencies( + pmon_production_packaging_full + pmon_sign_production_installer + ) + endif() + set_target_properties( + pmon_production_packaging_full + PROPERTIES + EXCLUDE_FROM_DEFAULT_BUILD ON + FOLDER "Packaging" + ) + + endif() +endfunction() diff --git a/cmake/PresentMonPackagingPreflight.cmake b/cmake/PresentMonPackagingPreflight.cmake new file mode 100644 index 000000000..f12d11298 --- /dev/null +++ b/cmake/PresentMonPackagingPreflight.cmake @@ -0,0 +1,205 @@ +# Script invoked via cmake -P (not include()). Packaging preflight; fails fast before MSBuild or stubbed package steps. + +if(NOT DEFINED PMON_BUILD_CONFIG OR "${PMON_BUILD_CONFIG}" STREQUAL "") + message(FATAL_ERROR "Packaging preflight requires PMON_BUILD_CONFIG (pass Release).") +endif() + +if(NOT PMON_BUILD_CONFIG STREQUAL "Release") + message( + FATAL_ERROR + "PresentMon packaging targets are Release-only. " + "Requested configuration: ${PMON_BUILD_CONFIG}. " + "Rebuild with --config Release." + ) +endif() + +if(NOT PMON_ARCHITECTURE STREQUAL "x64") + message(FATAL_ERROR "PresentMon packaging is x64-only. Current architecture: ${PMON_ARCHITECTURE}.") +endif() + +foreach(required_option IN ITEMS + PMON_BUILD_CONSOLE + PMON_BUILD_SERVICE + PMON_BUILD_SDK + PMON_BUILD_UI + PMON_BUILD_PROVIDER +) + if(NOT ${required_option}) + message( + FATAL_ERROR + "Packaging requires ${required_option}=ON. Reconfigure with all package-producing components enabled." + ) + endif() +endforeach() + +if(NOT CMAKE_VS_MSBUILD_COMMAND OR NOT EXISTS "${CMAKE_VS_MSBUILD_COMMAND}") + message( + FATAL_ERROR + "Packaging requires MSBuild from the Visual Studio generator " + "(CMAKE_VS_MSBUILD_COMMAND). Reconfigure a fresh binary directory with a " + "supported Visual Studio generator." + ) +endif() + +set(dotnet_framework_reference_root + "$ENV{ProgramFiles\(x86\)}/Reference Assemblies/Microsoft/Framework/.NETFramework/v4.8" +) +if(NOT IS_DIRECTORY "${dotnet_framework_reference_root}") + message( + FATAL_ERROR + "Packaging requires the .NET Framework 4.8 targeting pack at " + "\"${dotnet_framework_reference_root}\"." + ) +endif() + +if(NOT PMON_SOURCE_DIR OR "${PMON_SOURCE_DIR}" STREQUAL "") + message(FATAL_ERROR "Packaging preflight requires PMON_SOURCE_DIR (repository root).") +endif() + +set(required_authoring_files + "${PMON_SOURCE_DIR}/IntelPresentMon/PMInstaller/PMInstaller.wixproj" + "${PMON_SOURCE_DIR}/IntelPresentMon/PMInstaller/PresentMon.wxs" + "${PMON_SOURCE_DIR}/IntelPresentMon/PMInstaller/license.rtf" + "${PMON_SOURCE_DIR}/IntelPresentMon/PMInstaller/en-us.wxl" + "${PMON_SOURCE_DIR}/IntelPresentMon/PMInstallerLib/PMInstallerLib.wixproj" + "${PMON_SOURCE_DIR}/IntelPresentMon/ServiceMergeModule/ServiceMergeModule.wixproj" +) +foreach(authoring_file IN LISTS required_authoring_files) + if(NOT EXISTS "${authoring_file}") + message(FATAL_ERROR "Missing WiX authoring input: ${authoring_file}") + endif() +endforeach() + +set(payload_release_dir "${PMON_OUTPUT_ROOT}/Release") +if(NOT IS_DIRECTORY "${payload_release_dir}") + file(MAKE_DIRECTORY "${payload_release_dir}") +endif() +set(write_probe "${payload_release_dir}/.pmon_packaging_write_probe") +file(WRITE "${write_probe}" "probe\n") +file(REMOVE "${write_probe}") + +set(wix_root "") +if(PMON_WIX_ROOT AND NOT "${PMON_WIX_ROOT}" STREQUAL "") + set(wix_cache_input "${PMON_WIX_ROOT}") + string(STRIP "${wix_cache_input}" wix_cache_input) + while(wix_cache_input MATCHES "(.+)[/\\\\]$") + set(wix_cache_input "${CMAKE_MATCH_1}") + endwhile() + cmake_path(ABSOLUTE_PATH wix_cache_input NORMALIZE OUTPUT_VARIABLE wix_root) +endif() +if(NOT wix_root AND DEFINED ENV{WIX} AND NOT "$ENV{WIX}" STREQUAL "") + set(wix_env_raw $ENV{WIX}) + set(wix_env_input "${wix_env_raw}") + string(STRIP "${wix_env_input}" wix_env_input) + while(wix_env_input MATCHES "(.+)[/\\\\]$") + set(wix_env_input "${CMAKE_MATCH_1}") + endwhile() + if(wix_env_input) + cmake_path(ABSOLUTE_PATH wix_env_input NORMALIZE OUTPUT_VARIABLE wix_root) + endif() +endif() +if(NOT wix_root) + file( + GLOB wix_install_candidates + LIST_DIRECTORIES TRUE + "$ENV{ProgramFiles\(x86\)}/WiX Toolset v3.*" + ) + if(wix_install_candidates) + list(SORT wix_install_candidates COMPARE NATURAL ORDER DESCENDING) + list(GET wix_install_candidates 0 wix_root) + endif() +endif() + +if(NOT wix_root OR NOT IS_DIRECTORY "${wix_root}") + message( + FATAL_ERROR + "WiX Toolset 3 is required for packaging. Set -DPMON_WIX_ROOT to the WiX 3 install root " + "or set the WIX environment variable. No WiX 3 root was found." + ) +endif() + +string(TOLOWER "${wix_root}" wix_root_lower) +if(wix_root_lower MATCHES "wi[x ]? toolset v4" OR wix_root_lower MATCHES "/v4(\\|/|$)") + message( + FATAL_ERROR + "WiX Toolset 4 is not supported. Use WiX Toolset 3.x. Resolved root: ${wix_root}" + ) +endif() + +set(wix_bin "${wix_root}/bin") +set(wix_candle "${wix_bin}/candle.exe") +set(wix_wix_exe "${wix_bin}/wix.exe") +if(EXISTS "${wix_wix_exe}" AND NOT EXISTS "${wix_candle}") + message( + FATAL_ERROR + "The resolved WiX path looks like WiX Toolset 4 (wix.exe without candle.exe). " + "PresentMon requires WiX Toolset 3.x at: ${wix_root}" + ) +endif() + +set(wix_dll "${wix_bin}/wix.dll") +if(NOT EXISTS "${wix_dll}") + set(wix_dll "${wix_root}/SDK/wix.dll") +endif() +if(NOT EXISTS "${wix_dll}") + message( + FATAL_ERROR + "WiX Toolset 3 installation is incomplete. Missing wix.dll under " + "\"${wix_bin}\" or \"${wix_root}/SDK\"." + ) +endif() + +set(required_wix_tools + "${wix_bin}/heat.exe" + "${wix_bin}/candle.exe" + "${wix_bin}/light.exe" + "${wix_bin}/WixTasks.dll" + "${wix_bin}/WixUIExtension.dll" + "${wix_bin}/WixUtilExtension.dll" +) +foreach(wix_tool IN LISTS required_wix_tools) + if(NOT EXISTS "${wix_tool}") + message(FATAL_ERROR "WiX Toolset 3 installation is incomplete. Missing: ${wix_tool}") + endif() +endforeach() + +# Every .wixproj is built against this fixed location rather than the WiX +# MSBuild integration of the Visual Studio instance that supplied MSBuild. +set(wix_targets_path "$ENV{ProgramFiles\(x86\)}/MSBuild/Microsoft/WiX/v3.x/wix.targets") +if(NOT EXISTS "${wix_targets_path}") + message( + FATAL_ERROR + "The WiX Toolset 3 MSBuild targets are required for packaging but were " + "not found at \"${wix_targets_path}\". Reinstall WiX Toolset 3.x with " + "its MSBuild integration." + ) +endif() + +set(msi_output "${PMON_OUTPUT_ROOT}/Release/en-us/PresentMon.msi") +set(msm_output "${PMON_OUTPUT_ROOT}/Release/en-us/PresentMonSharedService.msm") + +message(STATUS "") +message(STATUS "PresentMon packaging preflight (Release)") +message(STATUS " Deployment profile: ${PMON_DEPLOYMENT_PROFILE}") +message(STATUS " Product version: ${PMON_PRODUCT_VERSION}") +message(STATUS " Console version: ${PMON_VERSION}") +message(STATUS " WiX root: ${wix_root}") +message(STATUS " MSBuild: ${CMAKE_VS_MSBUILD_COMMAND}") +message(STATUS " Payload root: ${payload_release_dir}") +message(STATUS " UCI included: ${PMON_UCI_ENABLED}") +message(STATUS " MSI output: ${msi_output}") +message(STATUS " MSM output: ${msm_output}") +if(PMON_DEPLOYMENT_PROFILE STREQUAL "PRODUCTION") + message(STATUS " Production signature enforcement: active (PE preflight and MSI sign gate)") + set(payload_sign_stamp "${PMON_CMAKE_BINARY_DIR}/packaging/Release/ProductionPayloadSigned.stamp") + if(NOT EXISTS "${payload_sign_stamp}") + message( + FATAL_ERROR + "Production packaging requires a signed native payload stamp at \"${payload_sign_stamp}\". " + "Run the pmon_sign_production_payload target before pmon_build_packages." + ) + endif() +else() + message(STATUS " Production signature enforcement: inactive (developer profile)") +endif() +message(STATUS "") diff --git a/cmake/PresentMonPackagingVerifyOutput.cmake b/cmake/PresentMonPackagingVerifyOutput.cmake new file mode 100644 index 000000000..e0a444278 --- /dev/null +++ b/cmake/PresentMonPackagingVerifyOutput.cmake @@ -0,0 +1,13 @@ +# Script invoked via cmake -P (not include()). + +if(NOT DEFINED PMON_VERIFY_OUTPUT OR "${PMON_VERIFY_OUTPUT}" STREQUAL "") + message(FATAL_ERROR "PresentMonPackagingVerifyOutput.cmake requires PMON_VERIFY_OUTPUT.") +endif() + +if(NOT EXISTS "${PMON_VERIFY_OUTPUT}") + message( + FATAL_ERROR + "Packaging step succeeded but the expected output is missing: ${PMON_VERIFY_OUTPUT}" + ) +endif() + \ No newline at end of file diff --git a/cmake/PresentMonPython.cmake b/cmake/PresentMonPython.cmake new file mode 100644 index 000000000..aeb0d62d2 --- /dev/null +++ b/cmake/PresentMonPython.cmake @@ -0,0 +1,76 @@ +include_guard(GLOBAL) + +function(pmon_configure_python) + set(Python3_FIND_VIRTUALENV FIRST) + if(PMON_PYTHON_EXECUTABLE) + set(Python3_EXECUTABLE "${PMON_PYTHON_EXECUTABLE}") + elseif(WIN32) + find_program(pmon_python_launcher NAMES py.exe py NO_CACHE) + if(pmon_python_launcher) + execute_process( + COMMAND + "${pmon_python_launcher}" + -3 + -c + "import sys; assert sys.version_info >= (3, 10); print(sys.executable)" + RESULT_VARIABLE launcher_result + OUTPUT_VARIABLE launcher_python + OUTPUT_STRIP_TRAILING_WHITESPACE + ERROR_QUIET + ) + if(NOT launcher_result AND launcher_python) + file(TO_CMAKE_PATH "${launcher_python}" launcher_python) + if(EXISTS "${launcher_python}") + set(Python3_EXECUTABLE "${launcher_python}") + endif() + endif() + endif() + endif() + find_package(Python3 3.10 REQUIRED COMPONENTS Interpreter) + set( + PMON_PYTHON_EXECUTABLE + "${Python3_EXECUTABLE}" + CACHE FILEPATH + "Python interpreter used by PresentMon build tooling" + FORCE + ) + set(PMON_PYTHON_EXECUTABLE "${Python3_EXECUTABLE}" PARENT_SCOPE) +endfunction() + +function(pmon_prepare_dependencies) + if(NOT PMON_AUTO_PREPARE_DEPENDENCIES) + message(STATUS "Automatic non-vcpkg dependency preparation is disabled.") + return() + endif() + + set(arguments + "${PMON_PYTHON_EXECUTABLE}" + "${PROJECT_SOURCE_DIR}/Scripts/bootstrap.py" + --repo-root "${PROJECT_SOURCE_DIR}" + --dependency-root "${PMON_DEPENDENCY_ROOT}" + --architecture "${PMON_ARCHITECTURE}" + --generator "${CMAKE_GENERATOR}" + --platform "${PMON_ARCHITECTURE}" + --toolset "${PMON_VS_TOOLSET}" + --generator-instance "${CMAKE_GENERATOR_INSTANCE}" + ) + if(PMON_ARCHITECTURE STREQUAL "x64" AND PMON_BUILD_UI) + list(APPEND arguments --with-cef --with-web) + if(PMON_CEF_SOURCE) + list(APPEND arguments --cef-source "${PMON_CEF_SOURCE}") + endif() + endif() + if(PMON_BUILD_TESTS) + list(APPEND arguments --with-auxdata) + endif() + + execute_process( + COMMAND ${arguments} + WORKING_DIRECTORY "${PROJECT_SOURCE_DIR}" + RESULT_VARIABLE result + COMMAND_ECHO STDOUT + ) + if(result) + message(FATAL_ERROR "PresentMon dependency preparation failed with exit code ${result}.") + endif() +endfunction() diff --git a/cmake/PresentMonShaders.cmake b/cmake/PresentMonShaders.cmake new file mode 100644 index 000000000..379dd88e7 --- /dev/null +++ b/cmake/PresentMonShaders.cmake @@ -0,0 +1,176 @@ +include_guard(GLOBAL) + +# Resolves fxc.exe, the Windows SDK HLSL shader compiler used by the legacy +# Shaders.vcxitems FxCompile items. No BuildCustomizations import selects it +# in MSBuild, so it resolves through the ambient SDK tool directory rather +# than a pinned version. CMake must resolve the same way: against the SDK it +# selected for the rest of the build (CMAKE_VS_WINDOWS_TARGET_PLATFORM_VERSION), +# not the newest SDK installed or whatever fxc.exe happens to be first on +# PATH. Either of those could silently pick a different SDK version than the +# rest of the product on a machine with multiple SDKs installed. +function(_pmon_find_fxc_compiler output_variable) + if(PMON_FXC_EXECUTABLE) + # Resolved to an absolute path before EXISTS/IS_DIRECTORY: both are + # documented as well-defined only for full paths, and a relative + # override otherwise validates against CMAKE_CURRENT_SOURCE_DIR here + # while the custom build command runs with the generated project + # file's directory as its working directory, not the source root. An + # unresolved relative path would configure successfully and then fail + # every build with "the system cannot find the path specified." + cmake_path( + ABSOLUTE_PATH PMON_FXC_EXECUTABLE + BASE_DIRECTORY "${PROJECT_SOURCE_DIR}" + NORMALIZE + OUTPUT_VARIABLE resolved_fxc_executable + ) + if(NOT EXISTS "${resolved_fxc_executable}") + message( + FATAL_ERROR + "PMON_FXC_EXECUTABLE is set to '${PMON_FXC_EXECUTABLE}', which " + "resolves to '${resolved_fxc_executable}', and that does not " + "exist." + ) + endif() + if(IS_DIRECTORY "${resolved_fxc_executable}") + message( + FATAL_ERROR + "PMON_FXC_EXECUTABLE is set to '${PMON_FXC_EXECUTABLE}', which " + "resolves to '${resolved_fxc_executable}', and that is a " + "directory. Set it to the fxc.exe file itself." + ) + endif() + set(${output_variable} "${resolved_fxc_executable}" PARENT_SCOPE) + return() + endif() + + if(NOT CMAKE_VS_WINDOWS_TARGET_PLATFORM_VERSION) + message( + FATAL_ERROR + "CMAKE_VS_WINDOWS_TARGET_PLATFORM_VERSION is not set; cannot " + "resolve the matching fxc.exe. Set PMON_FXC_EXECUTABLE to an " + "explicit path to override." + ) + endif() + + set( + sdk_fxc + "$ENV{ProgramFiles\(x86\)}/Windows Kits/10/bin/${CMAKE_VS_WINDOWS_TARGET_PLATFORM_VERSION}/x64/fxc.exe" + ) + if(NOT EXISTS "${sdk_fxc}") + message( + FATAL_ERROR + "The HLSL shader compiler fxc.exe was not found for the Windows " + "SDK CMake selected for this build " + "(${CMAKE_VS_WINDOWS_TARGET_PLATFORM_VERSION}) at ${sdk_fxc}. " + "Install that SDK's tools, or set PMON_FXC_EXECUTABLE to an " + "explicit fxc.exe path to override." + ) + endif() + + # Not cached into PMON_FXC_EXECUTABLE: that variable is reserved for an + # explicit override, checked above. Caching the auto-discovered path + # there would make it indistinguishable from a real override on the next + # configure, so a later SDK change would keep resolving to the stale + # compiler instead of following CMAKE_VS_WINDOWS_TARGET_PLATFORM_VERSION. + set(${output_variable} "${sdk_fxc}" PARENT_SCOPE) +endfunction() + +# Compiles one HLSL source into the shipping .cso payload at +# //Shaders/.cso, matching the legacy +# ObjectFileOutput path $(OutDir)Shaders\%(Filename).cso. The legacy +# FxCompile items apply the same ShaderModel/ShaderType across Debug and +# Release, so one add_custom_command() rule, parameterized by $, +# covers every configuration; each built configuration still invokes fxc.exe +# separately, since OUTPUT and the compiled flags both vary by $. +function(_pmon_add_hlsl_shader) + set(one_value_args SOURCE PROFILE ENTRY OUTPUT_VARIABLE) + cmake_parse_arguments(ARG "" "${one_value_args}" "" ${ARGN}) + + _pmon_find_fxc_compiler(fxc_executable) + + get_filename_component(shader_name "${ARG_SOURCE}" NAME_WE) + set(output_directory "${PMON_OUTPUT_ROOT}/$/Shaders") + set(output_file "${output_directory}/${shader_name}.cso") + + # /Zi (Debug only, see below) embeds the compiled source path into the + # .cso debug chunk. Passing it in native backslash form byte-for-byte + # matches the legacy MSBuild-invoked fxc.exe, which always receives + # backslash paths; CMake's own forward-slash form only changes those + # embedded path bytes; it does not change the compiled bytecode. + file(TO_NATIVE_PATH "${ARG_SOURCE}" native_source) + + # Shaders.vcxitems declares no EnableDebuggingInformation or + # DisableOptimizations metadata, but the imported Microsoft.Cpp.Common.props + # defaults both to true for FxCompile items whenever $(UseDebugLibraries) is + # true (CefNano.vcxproj sets it true for Debug|x64, false for Release|x64), + # which is /Od /Zi; Release gets neither flag. This is not visible in any + # PresentMon project file and was confirmed by capturing the real fxc.exe + # command line from a direct MSBuild invocation of CefNano.vcxproj in both + # configurations. CMake has no UseDebugLibraries equivalent to key off of, + # so this reproduces the resulting behavior directly off $. + add_custom_command( + OUTPUT "${output_file}" + COMMAND "${CMAKE_COMMAND}" -E make_directory "${output_directory}" + COMMAND + "${fxc_executable}" + /nologo + /T "${ARG_PROFILE}" + /E "${ARG_ENTRY}" + /Fo "${output_file}" + "$<$:/Od;/Zi>" + "${native_source}" + DEPENDS "${ARG_SOURCE}" "${fxc_executable}" + COMMENT "Compiling HLSL shader ${shader_name} (${ARG_PROFILE})" + COMMAND_EXPAND_LISTS + VERBATIM + ) + + set(${ARG_OUTPUT_VARIABLE} "${output_file}" PARENT_SCOPE) +endfunction() + +# Configures the two shaders compiled by the legacy PresentMonUI build. +function(pmon_configure_shaders) + set( + PMON_FXC_EXECUTABLE + "" + CACHE FILEPATH + "Optional explicit fxc.exe path; the CMake-selected Windows SDK's fxc.exe is used when empty" + ) + + set(shaders_dir "${PROJECT_SOURCE_DIR}/IntelPresentMon/Shaders") + + _pmon_add_hlsl_shader( + SOURCE "${shaders_dir}/Line_PS.hlsl" + PROFILE ps_4_0 + ENTRY main + OUTPUT_VARIABLE pixel_shader_output + ) + _pmon_add_hlsl_shader( + SOURCE "${shaders_dir}/Line_VS.hlsl" + PROFILE vs_4_0 + ENTRY main + OUTPUT_VARIABLE vertex_shader_output + ) + + add_custom_target( + pmon_compile_shaders + DEPENDS + "${pixel_shader_output}" + "${vertex_shader_output}" + ) + set_target_properties( + pmon_compile_shaders + PROPERTIES + EXCLUDE_FROM_DEFAULT_BUILD FALSE + FOLDER "Build" + ) +endfunction() + +# Makes shader compilation part of a product target's normal build. +function(pmon_target_uses_shaders target) + if(NOT TARGET pmon_compile_shaders) + message(FATAL_ERROR "pmon_configure_shaders() must run before pmon_target_uses_shaders().") + endif() + + add_dependencies(${target} pmon_compile_shaders) +endfunction() diff --git a/cmake/PresentMonSigning.cmake b/cmake/PresentMonSigning.cmake new file mode 100644 index 000000000..396e5e337 --- /dev/null +++ b/cmake/PresentMonSigning.cmake @@ -0,0 +1,349 @@ +include_guard(GLOBAL) + + + +set(PMON_SIGNING_CMAKE_DIR "${CMAKE_CURRENT_LIST_DIR}") + + + +function(pmon_find_signtool output_variable) + + if(PMON_SIGNTOOL_EXECUTABLE AND EXISTS "${PMON_SIGNTOOL_EXECUTABLE}") + + set(${output_variable} "${PMON_SIGNTOOL_EXECUTABLE}" PARENT_SCOPE) + + return() + + endif() + + + + set(windows_kits_root "$ENV{ProgramFiles\(x86\)}/Windows Kits/10/bin") + + if(PMON_ARCHITECTURE STREQUAL "Win32") + set(signtool_glob_suffix "x86") + else() + set(signtool_glob_suffix "x64") + endif() + + file( + + GLOB signtool_candidates + + LIST_DIRECTORIES FALSE + + "${windows_kits_root}/*/${signtool_glob_suffix}/signtool.exe" + + ) + + if(signtool_candidates) + + list(SORT signtool_candidates COMPARE NATURAL ORDER DESCENDING) + + list(GET signtool_candidates 0 signtool_path) + + set(${output_variable} "${signtool_path}" PARENT_SCOPE) + + else() + + set(${output_variable} "" PARENT_SCOPE) + + endif() + +endfunction() + + + +function(pmon_configure_signing) + + set(edss_script "") + if(PMON_EDSS_SIGN_SCRIPT AND NOT "${PMON_EDSS_SIGN_SCRIPT}" STREQUAL "") + set(edss_script "${PMON_EDSS_SIGN_SCRIPT}") + elseif(DEFINED ENV{PMON_EDSS_SIGN_SCRIPT} AND NOT "$ENV{PMON_EDSS_SIGN_SCRIPT}" STREQUAL "") + set(edss_script "$ENV{PMON_EDSS_SIGN_SCRIPT}") + endif() + + + + pmon_find_signtool(signtool_path) + + + + set(selected_backend "NONE") + + if(PMON_DEPLOYMENT_PROFILE STREQUAL "PRODUCTION") + + if(NOT edss_script OR NOT EXISTS "${edss_script}") + + message( + FATAL_ERROR + "Production builds require an external EDSS signing script (not in this repo). " + "Set the PMON_EDSS_SIGN_SCRIPT environment variable or -DPMON_EDSS_SIGN_SCRIPT " + "to the full path of your signing PowerShell script (e.g. under PresentMonBuilder) " + "before configuring. The file must exist at configure time." + ) + + endif() + + set(selected_backend "EDSS") + + elseif( + PMON_DEPLOYMENT_PROFILE STREQUAL "DEVELOPER" + AND PMON_BUILD_UI + ) + + set(selected_backend "SIGNTOOL") + + endif() + + + + set(PMON_EDSS_SIGN_SCRIPT "${edss_script}" CACHE FILEPATH "EDSS signing script" FORCE) + + set(PMON_SIGNTOOL_EXECUTABLE "${signtool_path}" CACHE FILEPATH "SignTool executable" FORCE) + + set( + + PMON_SELECTED_SIGNING_BACKEND + + "${selected_backend}" + + CACHE INTERNAL + + "Signing lane implied by PMON_DEPLOYMENT_PROFILE" + + FORCE + + ) + + + + set(PMON_SELECTED_SIGNING_BACKEND "${selected_backend}" PARENT_SCOPE) + +endfunction() + + + +function(pmon_apply_kernel_process_manifest target) + + target_link_options( + + ${target} + + PRIVATE + + # Debug keeps the default non-uiAccess manifest in every profile. + + # Release enables uiAccess for both DEVELOPER and PRODUCTION profiles. + + $<$:/MANIFESTUAC:level='asInvoker'> + + $<$:/MANIFESTUAC:uiAccess='true'> + + ) + +endfunction() + + + +function(pmon_attach_kernel_process_developer_signing target) + if(NOT PMON_DEPLOYMENT_PROFILE STREQUAL "DEVELOPER") + return() + endif() + add_custom_command( + TARGET ${target} + POST_BUILD + COMMAND + "${PMON_PYTHON_EXECUTABLE}" + "${PROJECT_SOURCE_DIR}/Scripts/sign.py" + developer + --configuration $ + --target "$" + --signtool "${PMON_SIGNTOOL_EXECUTABLE}" + --certificate-store "${PMON_SIGNTOOL_CERTIFICATE_STORE}" + --certificate-name "${PMON_SIGNTOOL_CERTIFICATE_NAME}" + --timestamp-url "${PMON_SIGNTOOL_TIMESTAMP_URL}" + COMMENT "Signing Release KernelProcess for local development" + VERBATIM + ) +endfunction() + + +function(pmon_add_explicit_developer_signing_target signing_target binary_target component_name) + add_custom_target( + ${signing_target} + COMMAND + "${CMAKE_COMMAND}" + "-DPMON_BUILD_CONFIG=$" + -P "${PMON_SIGNING_CMAKE_DIR}/PresentMonSigningPreflight.cmake" + COMMAND + "${PMON_PYTHON_EXECUTABLE}" + "${PROJECT_SOURCE_DIR}/Scripts/sign.py" + developer + --configuration $ + --target "$" + --signtool "${PMON_SIGNTOOL_EXECUTABLE}" + --certificate-store "${PMON_SIGNTOOL_CERTIFICATE_STORE}" + --certificate-name "${PMON_SIGNTOOL_CERTIFICATE_NAME}" + --timestamp-url "${PMON_SIGNTOOL_TIMESTAMP_URL}" + DEPENDS + ${binary_target} + "${PROJECT_SOURCE_DIR}/Scripts/sign.py" + "${PROJECT_SOURCE_DIR}/Scripts/pmon/signing.py" + "${PMON_SIGNING_CMAKE_DIR}/PresentMonSigningPreflight.cmake" + COMMENT "Signing Release ${component_name} for local development" + USES_TERMINAL + VERBATIM + ) + set_target_properties( + ${signing_target} + PROPERTIES + EXCLUDE_FROM_DEFAULT_BUILD ON + FOLDER "Signing" + ) +endfunction() + + + +function(pmon_add_production_signing_targets) + + if(NOT PMON_DEPLOYMENT_PROFILE STREQUAL "PRODUCTION") + + return() + + endif() + # Run after Release payload is built (build/Release under PMON_OUTPUT_ROOT). + + set(pmon_production_sign_mode_args "") + + if(PMON_EDSS_IN_PROCESS_SIGNING) + + set(pmon_production_sign_mode_args -InProcessSigning) + + else() + + set(pmon_production_sign_mode_args -ImpersonatedSigning) + + endif() + + set(pmon_production_sign_scope_args "") + if(PMON_ARCHITECTURE STREQUAL "Win32") + if(PMON_BUILD_SCOPE STREQUAL "CONSOLE") + set(pmon_production_sign_scope_args -SignWin32ConsoleOnly) + elseif(PMON_BUILD_SCOPE STREQUAL "PROVIDER") + set(pmon_production_sign_scope_args -SignWin32ProviderOnly) + endif() + endif() + + set( + payload_sign_stamp + "${CMAKE_BINARY_DIR}/packaging/Release/ProductionPayloadSigned.stamp" + ) + + set(standalone_win32_signing_preflight "") + if( + PMON_ARCHITECTURE STREQUAL "Win32" + AND ( + PMON_BUILD_SCOPE STREQUAL "CONSOLE" + OR PMON_BUILD_SCOPE STREQUAL "PROVIDER" + ) + ) + set( + standalone_win32_signing_preflight + COMMAND + "${CMAKE_COMMAND}" + "-DPMON_BUILD_CONFIG=$" + -P "${PMON_SIGNING_CMAKE_DIR}/PresentMonSigningPreflight.cmake" + ) + endif() + + add_custom_target( + pmon_sign_production_payload + ${standalone_win32_signing_preflight} + COMMAND "${CMAKE_COMMAND}" -E make_directory "${CMAKE_BINARY_DIR}/packaging/Release" + COMMAND + powershell + -NoProfile + -ExecutionPolicy + Bypass + -File + "${PMON_EDSS_SIGN_SCRIPT}" + -OutputRoot + "${PMON_OUTPUT_ROOT}/Release" + -RepoRoot + "${CMAKE_SOURCE_DIR}" + ${pmon_production_sign_mode_args} + ${pmon_production_sign_scope_args} + -Verify + COMMAND "${CMAKE_COMMAND}" -E touch "${payload_sign_stamp}" + WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}" + COMMENT "Signing the production payload through EDSS" + USES_TERMINAL + VERBATIM + ) + set_target_properties( + pmon_sign_production_payload + PROPERTIES + EXCLUDE_FROM_DEFAULT_BUILD ON + FOLDER "Packaging" + ) + foreach( + payload_target + IN ITEMS + PresentMonUI + KernelProcess + PresentMonService + PresentMonAPI2 + PresentMonAPI2Loader + pmon_provider + PresentMonConsole + pmon_stage_cef_runtime + pmon_stage_ui_payload + pmon_stage_kernel_payload + pmon_compile_shaders + ) + if(TARGET ${payload_target}) + add_dependencies(pmon_sign_production_payload ${payload_target}) + endif() + endforeach() + +endfunction() + + + +function(pmon_add_win32_component_signing_targets) + if(NOT PMON_ARCHITECTURE STREQUAL "Win32") + return() + endif() + + if(PMON_BUILD_SCOPE STREQUAL "CONSOLE") + set(binary_target PresentMonConsole) + set(developer_signing_target pmon_sign_developer_console) + set(production_signing_target pmon_sign_production_console) + set(component_name "Win32 console") + elseif(PMON_BUILD_SCOPE STREQUAL "PROVIDER") + set(binary_target pmon_provider) + set(developer_signing_target pmon_sign_developer_provider) + set(production_signing_target pmon_sign_production_provider) + set(component_name "Win32 provider") + else() + return() + endif() + + if(PMON_DEPLOYMENT_PROFILE STREQUAL "DEVELOPER") + pmon_add_explicit_developer_signing_target( + ${developer_signing_target} + ${binary_target} + "${component_name}" + ) + elseif(PMON_DEPLOYMENT_PROFILE STREQUAL "PRODUCTION") + add_custom_target(${production_signing_target}) + add_dependencies(${production_signing_target} pmon_sign_production_payload) + set_target_properties( + ${production_signing_target} + PROPERTIES + EXCLUDE_FROM_DEFAULT_BUILD ON + FOLDER "Signing" + ) + endif() +endfunction() diff --git a/cmake/PresentMonSigningPreflight.cmake b/cmake/PresentMonSigningPreflight.cmake new file mode 100644 index 000000000..b0a736cc4 --- /dev/null +++ b/cmake/PresentMonSigningPreflight.cmake @@ -0,0 +1,14 @@ +# Script invoked via cmake -P (not include()). + +if(NOT DEFINED PMON_BUILD_CONFIG OR "${PMON_BUILD_CONFIG}" STREQUAL "") + message(FATAL_ERROR "Signing preflight requires PMON_BUILD_CONFIG (pass Release).") +endif() + +if(NOT PMON_BUILD_CONFIG STREQUAL "Release") + message( + FATAL_ERROR + "PresentMon signing targets are Release-only. " + "Requested configuration: ${PMON_BUILD_CONFIG}. " + "Rebuild with --config Release." + ) +endif() diff --git a/cmake/PresentMonUci.cmake b/cmake/PresentMonUci.cmake new file mode 100644 index 000000000..399eea162 --- /dev/null +++ b/cmake/PresentMonUci.cmake @@ -0,0 +1,78 @@ +include_guard(GLOBAL) + +function(pmon_configure_uci) + set(uci_root "${PMON_UCI_ROOT}") + if(NOT uci_root AND DEFINED ENV{PMON_UCI_SDK_DIR}) + set(uci_root "$ENV{PMON_UCI_SDK_DIR}") + endif() + if(NOT uci_root) + set(uci_root "${PROJECT_SOURCE_DIR}/IntelPresentMon/ControlLib/uci/external") + endif() + cmake_path(ABSOLUTE_PATH uci_root NORMALIZE OUTPUT_VARIABLE normalized_uci_root) + + set(uci_required_files + "${normalized_uci_root}/include/uci/uci.h" + "${normalized_uci_root}/include/uci/uci-data-records.h" + "${normalized_uci_root}/unified-collector-interface.dll" + ) + + set(uci_sdk_valid TRUE) + foreach(required_file IN LISTS uci_required_files) + if(NOT EXISTS "${required_file}") + set(uci_sdk_valid FALSE) + endif() + endforeach() + if(NOT IS_DIRECTORY "${normalized_uci_root}/collectors") + set(uci_sdk_valid FALSE) + else() + file( + GLOB_RECURSE + uci_collector_files + LIST_DIRECTORIES FALSE + "${normalized_uci_root}/collectors/*" + ) + if(NOT uci_collector_files) + set(uci_sdk_valid FALSE) + endif() + endif() + + set(uci_enabled FALSE) + if(PMON_ENABLE_UCI STREQUAL "ON") + if(NOT uci_sdk_valid) + message( + FATAL_ERROR + "PMON_ENABLE_UCI=ON requires a valid UCI SDK at " + "${normalized_uci_root}. Expected include/uci/uci.h, " + "include/uci/uci-data-records.h, and " + "unified-collector-interface.dll, plus a non-empty collectors directory." + ) + endif() + set(uci_enabled TRUE) + elseif(PMON_ENABLE_UCI STREQUAL "AUTO" AND uci_sdk_valid) + set(uci_enabled TRUE) + endif() + + add_library(pmon_uci_sdk INTERFACE) + add_library(pmon::uci_sdk ALIAS pmon_uci_sdk) + set_property(TARGET pmon_uci_sdk PROPERTY FOLDER "ThirdParty") + + if(uci_enabled) + target_include_directories( + pmon_uci_sdk + INTERFACE + "${normalized_uci_root}/include" + ) + message(STATUS "UCI SDK enabled from ${normalized_uci_root}") + elseif(PMON_ENABLE_UCI STREQUAL "AUTO") + message(STATUS "UCI SDK not found; building without UCI support") + else() + message(STATUS "UCI support disabled") + endif() + + set(PMON_UCI_EFFECTIVE_ROOT "${normalized_uci_root}" CACHE INTERNAL "Resolved UCI SDK root" FORCE) + set(PMON_UCI_ENABLED "${uci_enabled}" CACHE INTERNAL "UCI inclusion state" FORCE) + set(PMON_UCI_RUNTIME_DLL "${normalized_uci_root}/unified-collector-interface.dll" CACHE INTERNAL "") + + set(PMON_UCI_EFFECTIVE_ROOT "${normalized_uci_root}" PARENT_SCOPE) + set(PMON_UCI_ENABLED "${uci_enabled}" PARENT_SCOPE) +endfunction() diff --git a/cmake/PresentMonUiPayload.cmake b/cmake/PresentMonUiPayload.cmake new file mode 100644 index 000000000..fdf566d79 --- /dev/null +++ b/cmake/PresentMonUiPayload.cmake @@ -0,0 +1,200 @@ +include_guard(GLOBAL) + +function(_pmon_normalize_ui_payload_relative_path input_path output_variable) + set(path "${input_path}") + string(REPLACE "\\" "/" path "${path}") + + cmake_path(IS_ABSOLUTE path is_absolute) + if( + NOT path + OR is_absolute + OR path MATCHES "(^|/)\\.\\.(/|$)" + OR path MATCHES "^[A-Za-z]:" + OR path MATCHES ";" + ) + message(FATAL_ERROR "Unsafe relative path in the UI payload lock: ${input_path}") + endif() + + cmake_path(NORMAL_PATH path OUTPUT_VARIABLE normalized_path) + if(normalized_path STREQUAL ".") + message(FATAL_ERROR "Unsafe relative path in the UI payload lock: ${input_path}") + endif() + + set(${output_variable} "${normalized_path}" PARENT_SCOPE) +endfunction() + +function(_pmon_add_tracked_payload_copy source_file destination_relative output_variable) + set(extra_depends ${ARGN}) + set(runtime_destination "${PMON_OUTPUT_ROOT}/$") + set(runtime_output "${runtime_destination}/${destination_relative}") + cmake_path(GET runtime_output PARENT_PATH runtime_output_directory) + + add_custom_command( + OUTPUT "${runtime_output}" + COMMAND "${CMAKE_COMMAND}" -E make_directory "${runtime_output_directory}" + COMMAND + "${CMAKE_COMMAND}" -E copy_if_different + "${source_file}" + "${runtime_output}" + COMMAND "${CMAKE_COMMAND}" -E touch_nocreate "${runtime_output}" + DEPENDS "${source_file}" ${extra_depends} + COMMENT "Staging payload file ${destination_relative}" + VERBATIM + ) + + set(${output_variable} "${runtime_output}" PARENT_SCOPE) +endfunction() + +function(pmon_configure_ui_payload) + set(app_cef_dir "${PROJECT_SOURCE_DIR}/IntelPresentMon/AppCef") + set(web_ui_dir "${app_cef_dir}/ipm-ui-vue") + set(web_dist_dir "${web_ui_dir}/dist") + set(web_dist_lock "${app_cef_dir}/web-dist-lock.json") + set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS "${web_dist_lock}") + + set(web_configure_depends + "${web_ui_dir}/package.json" + "${web_ui_dir}/package-lock.json" + "${web_ui_dir}/vite.config.ts" + "${web_ui_dir}/index.html" + "${web_ui_dir}/tsconfig.json" + "${web_ui_dir}/tsconfig.app.json" + "${web_ui_dir}/tsconfig.node.json" + "${web_dist_lock}" + ) + file(GLOB_RECURSE web_ui_sources CONFIGURE_DEPENDS + "${web_ui_dir}/src/*" + ) + list(APPEND web_configure_depends ${web_ui_sources}) + + set(web_build_stamp "${CMAKE_BINARY_DIR}/obj/ipm-ui-vue/$/build.stamp") + add_custom_command( + OUTPUT "${web_build_stamp}" + COMMAND "${CMAKE_COMMAND}" -E make_directory "${CMAKE_BINARY_DIR}/obj/ipm-ui-vue/$" + COMMAND + "${PMON_PYTHON_EXECUTABLE}" + "${PROJECT_SOURCE_DIR}/Scripts/web.py" + build + --repo-root "${PROJECT_SOURCE_DIR}" + --dependency-root "${PMON_DEPENDENCY_ROOT}" + COMMAND "${CMAKE_COMMAND}" -E touch "${web_build_stamp}" + DEPENDS ${web_configure_depends} + COMMENT "Building ipm-ui-vue web assets ($)" + VERBATIM + ) + + file(READ "${web_dist_lock}" web_dist_lock_json) + string(JSON output_count ERROR_VARIABLE json_error LENGTH "${web_dist_lock_json}" outputs) + if(json_error) + message(FATAL_ERROR "Failed to read the web dist lock: ${json_error}") + endif() + if(output_count EQUAL 0) + message(FATAL_ERROR "The web dist lock outputs list is empty: ${web_dist_lock}") + endif() + + set(payload_outputs "${web_build_stamp}") + set(web_output_keys) + math(EXPR last_output_index "${output_count} - 1") + foreach(output_index RANGE ${last_output_index}) + string(JSON output_path GET "${web_dist_lock_json}" outputs ${output_index}) + _pmon_normalize_ui_payload_relative_path("${output_path}" output_path) + + string(TOLOWER "${output_path}" output_key) + list(FIND web_output_keys "${output_key}" duplicate_output_index) + if(NOT duplicate_output_index EQUAL -1) + message(FATAL_ERROR "Duplicate web dist output path in the lock: ${output_path}") + endif() + list(APPEND web_output_keys "${output_key}") + + set(dist_input "${web_dist_dir}/${output_path}") + _pmon_add_tracked_payload_copy( + "${dist_input}" + "ipm-ui-vue/${output_path}" + staged_output + "${web_build_stamp}" + "${web_dist_lock}" + ) + list(APPEND payload_outputs "${staged_output}") + endforeach() + + foreach(preset_index RANGE 0 3) + _pmon_add_tracked_payload_copy( + "${web_ui_dir}/presets/preset-${preset_index}.json" + "Presets/preset-${preset_index}.json" + preset_output + ) + list(APPEND payload_outputs "${preset_output}") + endforeach() + + _pmon_add_tracked_payload_copy( + "${web_ui_dir}/BlockLists/TargetBlockList.txt" + "BlockLists/TargetBlockList.txt" + blocklist_output + ) + list(APPEND payload_outputs "${blocklist_output}") + + add_custom_target( + pmon_stage_ui_payload + DEPENDS ${payload_outputs} + ) + set_target_properties( + pmon_stage_ui_payload + PROPERTIES + EXCLUDE_FROM_DEFAULT_BUILD TRUE + FOLDER "Build" + ) +endfunction() + +function(pmon_configure_kernel_payload) + set(kernel_dir "${PROJECT_SOURCE_DIR}/IntelPresentMon/KernelProcess") + set(payload_outputs) + + _pmon_add_tracked_payload_copy( + "${kernel_dir}/presentmon-cli.cmd" + "presentmon-cli.cmd" + cmd_output + ) + list(APPEND payload_outputs "${cmd_output}") + + _pmon_add_tracked_payload_copy( + "${kernel_dir}/presentmon-cli.bat" + "pathed/presentmon-cli.bat" + bat_output + ) + list(APPEND payload_outputs "${bat_output}") + + add_custom_target( + pmon_stage_kernel_payload + DEPENDS ${payload_outputs} + ) + set_target_properties( + pmon_stage_kernel_payload + PROPERTIES + EXCLUDE_FROM_DEFAULT_BUILD TRUE + FOLDER "Build" + ) +endfunction() + +function(pmon_target_uses_ui_payload target) + if(NOT TARGET pmon_stage_ui_payload) + message(FATAL_ERROR "pmon_configure_ui_payload() must run before pmon_target_uses_ui_payload().") + endif() + + add_dependencies(${target} pmon_stage_ui_payload) + set_target_properties( + pmon_stage_ui_payload + PROPERTIES EXCLUDE_FROM_DEFAULT_BUILD FALSE + ) +endfunction() + +function(pmon_target_uses_kernel_payload target) + if(NOT TARGET pmon_stage_kernel_payload) + message(FATAL_ERROR "pmon_configure_kernel_payload() must run before pmon_target_uses_kernel_payload().") + endif() + + add_dependencies(${target} pmon_stage_kernel_payload) + set_target_properties( + pmon_stage_kernel_payload + PROPERTIES EXCLUDE_FROM_DEFAULT_BUILD FALSE + ) +endfunction() diff --git a/cmake/PresentMonVcpkg.cmake b/cmake/PresentMonVcpkg.cmake new file mode 100644 index 000000000..c728327c5 --- /dev/null +++ b/cmake/PresentMonVcpkg.cmake @@ -0,0 +1,164 @@ +include_guard(GLOBAL) + +# vcpkg integration. +# +# Compiled vcpkg installations are qualified by the validated platform toolset +# family so that a v143 build and a v145 build never consume, replace, or +# reconcile away one another's packages: +# +# build/ThirdParty/vcpkg_installed/// +# +# Source downloads are toolset- and architecture-independent and stay shared at +# build/ThirdParty/vcpkg-downloads. +# +# Two selections are involved, and neither one implies the other: +# +# 1. Which vcpkg is used. The bundled fallback comes from the Visual Studio +# instance CMake selected and published as PMON_VS_INSTANCE_ROOT; it must +# never run its own latest-version search, because the newest installed +# Visual Studio is not necessarily the one the generator selected. +# 2. Which toolset vcpkg compiles packages with. vcpkg runs its own +# Visual Studio discovery and defaults VCPKG_PLATFORM_TOOLSET to the +# newest toolset installed on the machine, so a Visual Studio 2022 build +# would otherwise link v145 packages against the v143 STL. Generated +# overlay triplets pin the toolset while preserving the target and host +# triplet names. +# +# vcpkg filters its discovery by instance and by toolset separately, so the +# toolset pin alone would still allow a different installation of the same +# Visual Studio version. The instance is pinned as well, through the +# VCPKG_VISUAL_STUDIO_PATH environment variable rather than the triplet file: +# vcpkg matches it against the native installation path, and a triplet-file +# value is re-emitted unescaped into vcpkg's generated dependency-info script, +# where a native path fails to parse as CMake. + +# Writes an overlay triplet that reproduces the built-in windows-static triplet +# for `architecture` and pins the validated Visual Studio platform toolset. +function(_pmon_write_vcpkg_triplet directory triplet architecture) + file( + WRITE "${directory}/${triplet}.cmake" + "# Generated by PresentMonVcpkg.cmake. Do not edit.\n" + "#\n" + "# Reproduces the built-in ${triplet} triplet and pins the platform\n" + "# toolset selected by the CMake generator, so that packages are\n" + "# compiled with the same toolset that links them.\n" + "set(VCPKG_TARGET_ARCHITECTURE ${architecture})\n" + "set(VCPKG_CRT_LINKAGE static)\n" + "set(VCPKG_LIBRARY_LINKAGE static)\n" + "set(VCPKG_PLATFORM_TOOLSET ${PMON_VS_TOOLSET})\n" + ) +endfunction() + +function(pmon_configure_vcpkg) + set( + PMON_DEPENDENCY_ROOT + "${CMAKE_CURRENT_SOURCE_DIR}/build/ThirdParty" + CACHE PATH + "Shared root for restored build dependencies" + ) + option(PMON_USE_VCPKG "Use the pinned vcpkg manifest" ON) + option( + PMON_AUTO_RESTORE_VCPKG + "Allow vcpkg to restore manifest dependencies during CMake configure" + ON + ) + if(NOT PMON_USE_VCPKG) + return() + endif() + + if(NOT PMON_VS_TOOLSET OR NOT PMON_VS_INSTANCE_ROOT) + message( + FATAL_ERROR + "pmon_configure_toolchain() must run before pmon_configure_vcpkg(). " + "The validated platform toolset qualifies the vcpkg installation " + "directory and pins the toolset vcpkg compiles packages with." + ) + endif() + + if(NOT CMAKE_TOOLCHAIN_FILE) + set(vcpkg_root "") + if(DEFINED ENV{VCPKG_ROOT}) + set(vcpkg_root "$ENV{VCPKG_ROOT}") + elseif(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/vcpkg/scripts/buildsystems/vcpkg.cmake") + set(vcpkg_root "${CMAKE_CURRENT_SOURCE_DIR}/vcpkg") + else() + # The vcpkg bundled with the Visual Studio instance CMake selected. + set(vcpkg_root "${PMON_VS_INSTANCE_ROOT}/VC/vcpkg") + endif() + + set(vcpkg_toolchain "${vcpkg_root}/scripts/buildsystems/vcpkg.cmake") + if(NOT EXISTS "${vcpkg_toolchain}") + message( + FATAL_ERROR + "PMON_USE_VCPKG=ON, but vcpkg was not found at:\n" + " ${vcpkg_toolchain}\n" + "The selected Visual Studio instance is\n" + " ${PMON_VS_INSTANCE_ROOT}\n" + "Install the vcpkg component in that instance, set VCPKG_ROOT, " + "provide a repository-local vcpkg checkout, or disable " + "PMON_USE_VCPKG." + ) + endif() + + set( + CMAKE_TOOLCHAIN_FILE + "${vcpkg_toolchain}" + CACHE FILEPATH + "vcpkg toolchain file" + ) + endif() + if(CMAKE_GENERATOR_PLATFORM STREQUAL "Win32") + set(default_target_triplet "x86-windows-static") + else() + set(default_target_triplet "x64-windows-static") + endif() + set(VCPKG_TARGET_TRIPLET "${default_target_triplet}" CACHE STRING "vcpkg target triplet") + set(VCPKG_HOST_TRIPLET "x64-windows-static" CACHE STRING "vcpkg host triplet") + set(VCPKG_MANIFEST_DIR "${CMAKE_CURRENT_SOURCE_DIR}" CACHE PATH "vcpkg manifest directory") + set( + VCPKG_MANIFEST_INSTALL + "${PMON_AUTO_RESTORE_VCPKG}" + CACHE BOOL + "Restore vcpkg manifest dependencies during configure" + ) + + # Overlay triplets keep the target and host triplet names unchanged while + # replacing vcpkg's newest-toolset default with the validated toolset. They + # are regenerated every configure so a Visual Studio update is picked up, + # and they cover the host triplet as well as the target triplet. + set(vcpkg_triplet_dir "${CMAKE_CURRENT_BINARY_DIR}/vcpkg-triplets") + _pmon_write_vcpkg_triplet("${vcpkg_triplet_dir}" "x64-windows-static" "x64") + _pmon_write_vcpkg_triplet("${vcpkg_triplet_dir}" "x86-windows-static" "x86") + + # PresentMon owns the overlay because these triplets pin the toolset. + set( + VCPKG_OVERLAY_TRIPLETS + "${vcpkg_triplet_dir}" + CACHE STRING + "Overlay triplets to use for vcpkg install in manifest mode" + FORCE + ) + + # The instance filter is a native path and is supplied per process, so it + # never reaches the triplet file that vcpkg re-emits as CMake code. + cmake_path( + NATIVE_PATH PMON_VS_INSTANCE_ROOT + NORMALIZE + vcpkg_visual_studio_path + ) + set(ENV{VCPKG_VISUAL_STUDIO_PATH} "${vcpkg_visual_studio_path}") + + set( + VCPKG_INSTALLED_DIR + "${PMON_DEPENDENCY_ROOT}/vcpkg_installed/${PMON_VS_TOOLSET}/${default_target_triplet}" + CACHE PATH + "Per-toolset, per-architecture vcpkg installation root" + FORCE + ) + + # Downloaded sources are toolset- and architecture-independent. + set( + ENV{VCPKG_DOWNLOADS} + "${PMON_DEPENDENCY_ROOT}/vcpkg-downloads" + ) +endfunction() diff --git a/cmake/PresentMonVersion.cmake b/cmake/PresentMonVersion.cmake new file mode 100644 index 000000000..b8acc113c --- /dev/null +++ b/cmake/PresentMonVersion.cmake @@ -0,0 +1,87 @@ +include_guard(GLOBAL) + +function(pmon_read_xml_property content property output_variable) + string( + REGEX MATCH + "<${property}[^>]*>([^<]+)" + property_match + "${content}" + ) + if(NOT property_match) + message(FATAL_ERROR "Could not read ${property} from Version.props.") + endif() + set(${output_variable} "${CMAKE_MATCH_1}" PARENT_SCOPE) +endfunction() + +function(pmon_read_version version_props_path) + if(NOT EXISTS "${version_props_path}") + message(FATAL_ERROR "Version properties file not found: ${version_props_path}") + endif() + + set_property( + DIRECTORY APPEND + PROPERTY CMAKE_CONFIGURE_DEPENDS "${version_props_path}" + ) + file(READ "${version_props_path}" version_props_content) + pmon_read_xml_property( + "${version_props_content}" + "PresentMonVersion" + parsed_version + ) + pmon_read_xml_property( + "${version_props_content}" + "PresentMonFileVersion" + parsed_file_version + ) + + string(TIMESTAMP copyright_year "%Y" UTC) + + set(PMON_VERSION "${parsed_version}" PARENT_SCOPE) + set(PMON_FILE_VERSION "${parsed_file_version}" PARENT_SCOPE) + set(PMON_PRODUCT_VERSION "${parsed_file_version}" PARENT_SCOPE) + set( + PMON_COPYRIGHT + "Copyright (C) 2017-${copyright_year}" + PARENT_SCOPE + ) +endfunction() + +function(pmon_create_version_target) + set(version_header "${PMON_OUTPUT_ROOT}/obj/generated/version.h") + file( + GENERATE + OUTPUT "${version_header}" + CONTENT "char const* PRESENT_MON_VERSION = \"${PMON_VERSION}\";\n" + ) + + add_library(pmon_version_header INTERFACE) + add_library(pmon::version_header ALIAS pmon_version_header) + target_include_directories( + pmon_version_header + INTERFACE + "${PMON_OUTPUT_ROOT}/obj" + ) + set_property(TARGET pmon_version_header PROPERTY FOLDER "Build") + + set(PMON_VERSION_HEADER "${version_header}" PARENT_SCOPE) +endfunction() + +# Supplies the PM_VER_* definitions that IntelPresentMon .rc scripts expect. +# MSBuild injects these through the ResourceCompile section of Common.props. +function(pmon_create_version_resource_target) + string(REPLACE "." "," file_version_numbers "${PMON_FILE_VERSION}") + string(REPLACE "." "," product_version_numbers "${PMON_PRODUCT_VERSION}") + + add_library(pmon_version_resource INTERFACE) + add_library(pmon::version_resource ALIAS pmon_version_resource) + target_compile_definitions( + pmon_version_resource + INTERFACE + "PM_VER_FILE_NUM=${file_version_numbers}" + "PM_VER_PRODUCT_NUM=${product_version_numbers}" + "PM_VER_FILE_STR=\"${PMON_FILE_VERSION}\"" + "PM_VER_PRODUCT_STR=\"${PMON_PRODUCT_VERSION}\"" + "PM_VER_COPYRIGHT=\"${PMON_COPYRIGHT}\"" + ) + set_property(TARGET pmon_version_resource PROPERTY FOLDER "Build") +endfunction() diff --git a/cmake/PresentMonVisualStudio.cmake b/cmake/PresentMonVisualStudio.cmake new file mode 100644 index 000000000..d60acb107 --- /dev/null +++ b/cmake/PresentMonVisualStudio.cmake @@ -0,0 +1,310 @@ +include_guard(GLOBAL) + +# Visual Studio generator and platform toolset policy. +# +# The -G argument is the user-facing Visual Studio version selector. Each +# supported generator is paired with exactly one native platform toolset, and +# cross-pairings (for example Visual Studio 2026 with v143) are not supported: +# +# Visual Studio 17 2022 -> v143 +# Visual Studio 18 2026 -> v145 +# +# Users are not required to pass -T. CMake selects the native toolset by +# default and the selection is validated after project() initializes the +# toolchain. CMake also selects the concrete Visual Studio installation and +# records it in CMAKE_GENERATOR_INSTANCE; that instance is authoritative for +# every Visual Studio-owned tool this build uses, so no module may run an +# independent latest-version search. +# +# Normalized results are published as internal cache entries: +# +# PMON_VS_GENERATOR the validated Visual Studio generator +# PMON_VS_TOOLSET the validated toolset family (v143 or v145) +# PMON_VS_INSTANCE_ROOT the installation directory of the selected instance +# +# PMON_VS_TOOLSET deliberately carries only the toolset family. It never +# carries an MSVC servicing version such as 14.50 or 14.51, because it is used +# to qualify compiled dependency directories that must remain stable across +# Visual Studio servicing updates. + +set(PMON_SUPPORTED_VS_GENERATORS "Visual Studio 17 2022" "Visual Studio 18 2026") + +# Maps a supported generator to its native platform toolset, or to an empty +# string when the generator is not supported. +function(_pmon_native_toolset_for_generator generator out_var) + if("${generator}" STREQUAL "Visual Studio 17 2022") + set("${out_var}" "v143" PARENT_SCOPE) + elseif("${generator}" STREQUAL "Visual Studio 18 2026") + set("${out_var}" "v145" PARENT_SCOPE) + else() + set("${out_var}" "" PARENT_SCOPE) + endif() +endfunction() + +function(_pmon_supported_generator_summary out_var) + set(summary "") + foreach(generator IN LISTS PMON_SUPPORTED_VS_GENERATORS) + _pmon_native_toolset_for_generator("${generator}" toolset) + string(APPEND summary " -G \"${generator}\" (platform toolset ${toolset})\n") + endforeach() + set("${out_var}" "${summary}" PARENT_SCOPE) +endfunction() + +# Extracts and validates the installation location from a generator-instance +# specification that includes a leading location field. +function(_pmon_instance_root_from_specification specification out_var) + # CMAKE_GENERATOR_INSTANCE is either , ,=... + # or =... The installation location is the leading field. + string(REPLACE "," ";" instance_fields "${specification}") + list(GET instance_fields 0 instance_root) + if(instance_root MATCHES "^[A-Za-z_][A-Za-z0-9_]*=") + message( + FATAL_ERROR + "CMAKE_GENERATOR_INSTANCE (${specification}) does not " + "specify an installation location. PresentMon needs the location " + "of the selected Visual Studio instance. Configure a fresh binary " + "directory without CMAKE_GENERATOR_INSTANCE, or include the " + "installation path in its value." + ) + endif() + + cmake_path(SET instance_root NORMALIZE "${instance_root}") + if(NOT IS_DIRECTORY "${instance_root}") + message( + FATAL_ERROR + "The Visual Studio instance selected by CMake does not exist:\n" + " ${instance_root}\n" + "Install ${CMAKE_GENERATOR} or configure a fresh binary directory." + ) + endif() + + set("${out_var}" "${instance_root}" PARENT_SCOPE) +endfunction() + +# Selects the Visual Studio instance before project() initializes languages. +# CMake has not populated a default CMAKE_GENERATOR_INSTANCE at this point. If +# the user did not provide an explicit instance, use the generator-specific +# COMNTOOLS environment selector documented by CMake. The selector is scoped +# to this CMake process and ensures that CMake and pre-project dependency setup +# use the same installation. +function(_pmon_select_vs_instance_root out_var) + if(CMAKE_GENERATOR_INSTANCE) + _pmon_instance_root_from_specification( + "${CMAKE_GENERATOR_INSTANCE}" + instance_root + ) + set("${out_var}" "${instance_root}" PARENT_SCOPE) + return() + endif() + + if(CMAKE_GENERATOR STREQUAL "Visual Studio 17 2022") + set(instance_environment "VS170COMNTOOLS") + set(common_tools "$ENV{VS170COMNTOOLS}") + set(version_range "[17.0,18.0)") + else() + set(instance_environment "VS180COMNTOOLS") + set(common_tools "$ENV{VS180COMNTOOLS}") + set(version_range "[18.0,19.0)") + endif() + + if(common_tools) + cmake_path(SET common_tools NORMALIZE "${common_tools}") + # COMNTOOLS commonly ends with a trailing separator. cmake_path + # PARENT_PATH treats that as part of the path and only removes the + # separator, so strip it before walking up to the instance root. + string(REGEX REPLACE "[/\\\\]+$" "" common_tools "${common_tools}") + cmake_path(GET common_tools PARENT_PATH common7_root) + cmake_path(GET common7_root PARENT_PATH instance_root) + if(NOT IS_DIRECTORY "${instance_root}/Common7/Tools") + message( + FATAL_ERROR + "${instance_environment} does not identify a valid Visual " + "Studio installation:\n" + " ${common_tools}\n" + "Fix or unset ${instance_environment}, then configure a fresh " + "binary directory." + ) + endif() + else() + set( + vswhere + "$ENV{ProgramFiles\(x86\)}/Microsoft Visual Studio/Installer/vswhere.exe" + ) + if(NOT EXISTS "${vswhere}") + message( + FATAL_ERROR + "Visual Studio Installer discovery is unavailable because " + "vswhere.exe was not found at:\n ${vswhere}" + ) + endif() + + execute_process( + COMMAND + "${vswhere}" + -latest + -products * + -version "${version_range}" + -requires Microsoft.Component.MSBuild + -property installationPath + RESULT_VARIABLE vswhere_result + OUTPUT_VARIABLE instance_root + OUTPUT_STRIP_TRAILING_WHITESPACE + ERROR_VARIABLE vswhere_error + ERROR_STRIP_TRAILING_WHITESPACE + ) + if(NOT vswhere_result EQUAL 0 OR NOT instance_root) + message( + FATAL_ERROR + "No Visual Studio instance compatible with generator " + "${CMAKE_GENERATOR} was found. Install that Visual Studio " + "version with MSBuild and its native C++ toolset.\n" + "vswhere: ${vswhere_error}" + ) + endif() + cmake_path(SET instance_root NORMALIZE "${instance_root}") + endif() + + if(NOT IS_DIRECTORY "${instance_root}/Common7/Tools") + message( + FATAL_ERROR + "Visual Studio Installer discovery returned an invalid " + "installation for generator ${CMAKE_GENERATOR}:\n" + " ${instance_root}" + ) + endif() + + if(CMAKE_GENERATOR STREQUAL "Visual Studio 17 2022") + set(ENV{VS170COMNTOOLS} "${instance_root}/Common7/Tools") + else() + set(ENV{VS180COMNTOOLS} "${instance_root}/Common7/Tools") + endif() + + set("${out_var}" "${instance_root}" PARENT_SCOPE) +endfunction() + +function(_pmon_resolve_recorded_vs_instance_root out_var) + if(NOT CMAKE_GENERATOR_INSTANCE) + message( + FATAL_ERROR + "CMake did not record a Visual Studio instance for generator " + "${CMAKE_GENERATOR}. PresentMon resolves every Visual Studio-owned " + "tool from the selected instance, so configuration cannot continue." + ) + endif() + _pmon_instance_root_from_specification( + "${CMAKE_GENERATOR_INSTANCE}" + instance_root + ) + set("${out_var}" "${instance_root}" PARENT_SCOPE) +endfunction() + +# Runs before project(). Accepts only the supported Visual Studio generators, +# rejects an explicit generator/toolset cross-pairing, and establishes the +# expected toolset family early enough for dependency modules to qualify their +# compiled artifact directories before the vcpkg toolchain is initialized. +function(pmon_configure_toolchain) + _pmon_supported_generator_summary(supported_summary) + + _pmon_native_toolset_for_generator("${CMAKE_GENERATOR}" expected_toolset) + if(NOT expected_toolset) + message( + FATAL_ERROR + "Unsupported CMake generator \"${CMAKE_GENERATOR}\".\n" + "PresentMon supports only these generators:\n" + "${supported_summary}" + "Configure a fresh binary directory with a supported generator. " + "CMake cannot change the generator of an existing binary tree." + ) + endif() + + # -T is optional. When it names a toolset, reject a cross-pairing here, + # before any dependency restore work runs. + if(CMAKE_GENERATOR_TOOLSET) + string(REPLACE "," ";" toolset_fields "${CMAKE_GENERATOR_TOOLSET}") + list(GET toolset_fields 0 requested_toolset) + string(STRIP "${requested_toolset}" requested_toolset) + string(TOLOWER "${requested_toolset}" requested_toolset) + # Fields such as host=x64 or version=14.44 select details of the native + # toolset rather than replacing it; only a leading toolset name pairs. + if(requested_toolset AND NOT requested_toolset MATCHES "=") + if(NOT requested_toolset STREQUAL "${expected_toolset}") + message( + FATAL_ERROR + "Generator/toolset cross-pairing is not supported.\n" + " Generator: ${CMAKE_GENERATOR}\n" + " Native toolset: ${expected_toolset}\n" + " Requested (-T): ${CMAKE_GENERATOR_TOOLSET}\n" + "PresentMon enforces these pairings:\n" + "${supported_summary}" + "Drop -T and let CMake select the native toolset." + ) + endif() + endif() + endif() + + _pmon_select_vs_instance_root(instance_root) + + set( + PMON_VS_GENERATOR + "${CMAKE_GENERATOR}" + CACHE INTERNAL + "Validated Visual Studio generator" + ) + set( + PMON_VS_TOOLSET + "${expected_toolset}" + CACHE INTERNAL + "Validated platform toolset family qualifying compiled dependencies" + ) + set( + PMON_VS_INSTANCE_ROOT + "${instance_root}" + CACHE INTERNAL + "Installation root of the Visual Studio instance selected by CMake" + ) +endfunction() + +# Runs after project(). Confirms CMake selected the native toolset and retained +# the Visual Studio instance resolved before dependency initialization. +function(pmon_validate_toolchain) + if(NOT PMON_VS_TOOLSET) + message( + FATAL_ERROR + "pmon_configure_toolchain() must run before project()." + ) + endif() + + if(NOT CMAKE_VS_PLATFORM_TOOLSET) + message( + FATAL_ERROR + "CMake did not report a Visual Studio platform toolset for " + "generator ${CMAKE_GENERATOR}. PresentMon requires the " + "${PMON_VS_TOOLSET} platform toolset." + ) + endif() + + string(TOLOWER "${CMAKE_VS_PLATFORM_TOOLSET}" selected_toolset) + if(NOT selected_toolset STREQUAL "${PMON_VS_TOOLSET}") + message( + FATAL_ERROR + "Generator ${CMAKE_GENERATOR} requires platform toolset " + "${PMON_VS_TOOLSET}, but CMake selected " + "${CMAKE_VS_PLATFORM_TOOLSET}.\n" + "PresentMon does not support cross-pairings. Install the " + "${PMON_VS_TOOLSET} build tools in the selected Visual Studio " + "instance and configure a fresh binary directory." + ) + endif() + + _pmon_resolve_recorded_vs_instance_root(selected_instance_root) + if(NOT selected_instance_root STREQUAL "${PMON_VS_INSTANCE_ROOT}") + message( + FATAL_ERROR + "The Visual Studio generator instance changed during configure.\n" + " Before project(): ${PMON_VS_INSTANCE_ROOT}\n" + " After project(): ${selected_instance_root}\n" + "Configure a fresh binary directory and do not modify " + "CMAKE_GENERATOR_INSTANCE in project code or a toolchain file." + ) + endif() +endfunction() diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 000000000..4997dccac --- /dev/null +++ b/requirements.txt @@ -0,0 +1,2 @@ +# PresentMon build orchestration currently uses only the Python standard library. +# Keep this file as the single installation entry point for future pinned tooling.