Summary of Changes CMake Library Git Module
Summary of modifications since commit a9c278edb9296e26a071dad78daa687bd30baf5a in the cmake/lib git submodule.
Three commits were made between 2026-10-17 and 2026-10-19.
Dynamic Subcommand Loader
Files: bin/build.py, bin/build-demo.py (new)
The Problem
The main build.py script was growing monolithic. Every new subcommand (like demo, docker, wine, publish)
had to be hardcoded inside build.py itself, making the script harder to maintain and extending it required modifying
the core build infrastructure.
The Solution
Introduced _load_subcommand_modules() in bin/build.py, a function that dynamically
discovers and imports all build-*.py files at runtime using importlib.util. Each subcommand module defines and
registers a SubCommand subclass, keeping subcommand logic fully decoupled from the main script.
Key design decisions:
- Avoid re-importing
build.py: Subcommand modules must usesys.modules["__main__"]orsys.modules["build"]to reference the already-loaded main module, rather thanfrom build import ..., which would re-execute module-level code and create duplicate registries. - Wine symlink resolution: Under Wine,
os.path.realpath()does not resolve Linux symlinks, so the loader searches both the script's directory and ansbin/subdirectory to find subcommand modules regardless of howbuild.pywas invoked. - Deduplication: A
seen_filesset prevents the same module from being loaded twice when found in multiple search directories.
bin/build-demo.py serves as the first example subcommand, demonstrating the pattern
for future extensions.
CMake Execution Profiling
Files: bin/build.py
The Problem
When a CMake configure or build step takes an unexpectedly long time, there was no built-in way to identify which target, source file, or CMake script is causing the slowdown. Developers had to resort to external profiling tools or manual timing.
The Solution
Added a --profile flag to the native subcommand in bin/build.py. When enabled, it
passes CMake's native profiling flags to both the configure and build steps:
- Configure:
--profiling-format=google-trace --profiling-output=profile-make.json - Build:
--profiling-format=google-trace --profiling-output=profile-build.json
The output is in Google Trace format, viewable in chrome://tracing or about:tracing in Firefox, providing a
visual timeline of all CMake operations. This makes it trivial to identify slow targets, expensive CMake scripts,
or bottlenecks in the build graph.
MSVC Debug Information Format
Files: SfBaseConfig.cmake
The Problem
When building with MSVC (including via Wine), the default debug information format produces separate .pdb files.
For RelWithDebInfo and Debug build types, this can cause issues with certain debuggers and deployment scenarios
where the .pdb file must be co-located with the binary.
The Solution
Added MSVC_DEBUG_INFORMATION_FORMAT property set to Embedded for both RelWithDebInfo and Debug build types
in SfBaseConfig.cmake. This embeds the debug symbols directly into the binary,
eliminating the need for separate .pdb files and simplifying debugging workflows, especially in Wine environments
where file path mapping between Linux and Windows can complicate .pdb discovery.
Wine TEMP Directory Symlink Removal
Files: bin/build.py
The Problem
The wine subcommand in bin/build.py contained a workaround that created a symlink from
the Wine user's Temp directory to the project's cmake-build directory. This was needed because Wine's TEMP and
TMP environment variables were not properly set in the Docker image, causing temporary files to be created in
locations outside the Docker mount and potentially lost.
The Root Cause
The underlying issue was in the Docker image setup (external/docker-runner repository). Two problems existed:
-
Registry key formatting error: In
builder/cpp.Dockerfile, the Windows registry entries forTEMPandTMPwere incorrectly formatted. The keys were written as"TEMP=Z:\\tmp"and"TMP=Z:\\tmp", which is invalid registry syntax. The correct format requires double quotes around the key name:"TEMP"="Z:\\tmp"and"TMP"="Z:\\tmp". Without proper quoting, Wine never read these environment variables, leavingTEMPandTMPundefined or pointing to default locations. -
Missing Temp directory symlink: The Wine user's
Tempdirectory at${WINEPREFIX}/drive_c/users/user/Tempwas not linked to a persistent location, causing temporary files to be lost when the Wine prefix was recreated.
The Solution
The fix was made in the external/docker-runner repository (commit a28cc8b):
-
Fixed registry key formatting: Changed
"TEMP=Z:\\tmp"to"TEMP"="Z:\\tmp"and"TMP=Z:\\tmp"to"TMP"="Z:\\tmp"in the Windows registry import script withinbuilder/cpp.Dockerfile. -
Added Temp directory symlink: Added
ln -s /tmp "${WINEPREFIX}/drive_c/users/user/Temp"to link the Wine user's Temp directory to the host's/tmpdirectory, ensuring temporary files persist and are accessible.
With these fixes in place, the workaround in bin/build.py that manually created the
symlink was no longer needed and was removed. Removing dead workarounds reduces maintenance burden and potential
side effects.
Qt Version Integration in Packaging
Files: SfBaseConfig.cmake, tpl/cpack/prepare.cmake, tpl/cpack/pkg-app.cmake
The Problem
When a project produces multiple packages — some depending on Qt and others not — there was no way to distinguish Qt-dependent packages from non-Qt packages by name alone. This made it difficult to trace which package version corresponds to which Qt version, especially when maintaining multiple Qt version builds.
The Solution
The mechanism works in three steps:
-
Flag the target: In
This stores the flag list in the target'sSfBaseConfig.cmake, theSf_AddTarget()function accepts aFLAGSparameter (line 790) that can includeqtas a flag. For example:SF_FLAGSproperty (line 813). The helper functionSf_HasTargetFlag()(line 841) can then check if a given target has a specific flag set. -
Build the Qt components list: In
https://git.scanframe.com/library/cmake-lib/-/blob/main/tpl/cpack/prepare.cmake, each executable target is checked for theqtflag (line 148). If found, the component name is added to theSF_QT_COMPONENTSlist (line 152). This list is then available to all CPack generator configurations. -
Append Qt version to package names: In
tpl/cpack/pkg-app.cmake, each generator checks if the component is inSF_QT_COMPONENTS, and if so, appends-qt${SF_QT_VERSION}to the package filename:- RPM (line 22):
CPACK_RPM_${_ComponentUpper}_PACKAGE_NAMEgets-qt${SF_QT_VERSION}appended. - DEB (line 47):
CPACK_DEBIAN_${_ComponentUpper}_PACKAGE_NAMEgets-qt${SF_QT_VERSION}appended. - Archive (line 137): filename gets
-qt${SF_QT_VERSION}appended. - NSIS (line 184):
CPACK_PACKAGE_FILE_NAMEgets-qt${SF_QT_VERSION}appended if any component uses Qt. - AppImage (line 200):
CPACK_PACKAGE_FILE_NAMEgets-qt${SF_QT_VERSION}appended ifSF_QT_COMPONENTSis not empty.
- RPM (line 22):
For example, a package that was previously named myapp-gnu13.2-1.0.0 now becomes
myapp-gnu13.2-qt6.10.3-1.0.0 when the target is flagged with qt and Qt 6.10.3 is used, making the Qt
dependency and version explicit in the package name.
Wine Version Detection
Files: tpl/root/src/hwl/inc/hwl/hello.h, tpl/root/src/hwl/src/hello.cpp, tpl/root/src/cli/main.cpp
The Problem
The isWine() function in the helper library called GetModuleHandleA and GetProcAddress on every invocation,
repeatedly querying ntdll.dll for the wine_get_version symbol. Additionally, there was no way to retrieve the
actual Wine version string — only a boolean "running under Wine or not" check existed.
The Solution
Two improvements in tpl/root/src/hwl/src/hello.cpp:
- Cached detection:
isWine()now stores the result in astatic const bool, performing theGetModuleHandleW/GetProcAddresslookup only once. The function also switched fromGetModuleHandleAtoGetModuleHandleWfor proper Unicode support. - Version retrieval: A new function
getWineVersion()was added that returns the Wine version string by callingwine_get_version(). The result is also cached via a sentinel pattern (UINTPTR_MAX) to avoid repeatedGetProcAddresscalls.
The template application tpl/root/src/cli/main.cpp was updated to display
the Wine version string instead of the boolean "Yes/No" indicator.
Refined Output File Naming Conventions
Files: tpl/cpack/pkg-app.cmake
The Problem
Archive and installer filenames had inconsistent structure. The toolchain string was concatenated with a + separator
inside a list element (e.g., win+gnu), making the filename harder to parse programmatically. Additionally, the
architecture naming differed between Windows (x86_64) and Linux (x86-64) in ways that weren't intentional.
The Solution
Refactored filename construction in tpl/cpack/pkg-app.cmake to use consistent
string concatenation instead of list-based assembly. The toolchain string is now appended directly without the +
prefix inside list elements. For Windows archives, SF_ARCHITECTURE is used directly instead of SF_ARCHITECTURE_SAFE,
preserving the native architecture name.
Build Script Logging Improvements
Files: bin/build.py
The Problem
When uploading packages to Nexus, the log output only showed filenames without sizes. For large packages, this made it hard to verify whether an upload completed correctly or to compare expected vs actual package sizes.
The Solution
Added get_readable_file_size() static method to bin/build.py that converts byte sizes
to human-readable format (B, KiB, MiB, GiB, TiB). The Nexus upload log messages now include the file size in
parentheses, e.g., Uploading APT repo file: package.deb (15.23 MiB).
CPack Configuration Enhancements
Files: tpl/cpack/pkg-qt-lib.cmake, tpl/cpack/project.cmake
The Problem
For Qt library DEB packages, the description was only set in CPACK_DEBIAN_*_DESCRIPTION, but not in
CPACK_DEBIAN_*_DESCRIPTION_SUMMARY. This meant package managers showing only the summary line would display
no description. Additionally, CPACK_PACKAGE_DESCRIPTION_SUMMARY was leaking into Qt component packages with
unwanted shared text.
The Solution
- In
tpl/cpack/pkg-qt-lib.cmake, both_DESCRIPTIONand_DESCRIPTION_SUMMARYare now set from the same_descriptionvariable, ensuring consistency. - In
tpl/cpack/project.cmake,CPACK_PACKAGE_DESCRIPTION_SUMMARYis cleared for Qt component packages to prevent unwanted inherited descriptions. - The
SF_ZIP_MANIFEST_FILEstatus message is now only printed on Windows, where it's relevant. ShowAllVars()is now enabled (uncommented) for debugging CPack configuration issues.
Google Test MinGW Linking Fix
Files: SfGoogleTestConfig.cmake
The Problem
The Windows MinGW compiler produces linking errors when building GoogleTest-based tests:
undefined reference to '__imp__Exit'undefined reference to '__imp_quick_exit'
This issue is documented in the project's own src/CMakeLists.txt, which has a comment
above the GoogleTest subdirectory noting the problem.
The Root Cause
The issue stems from libmingw32.a's UCRT-only _Exit/quick_exit wrappers when using msvcrt toolchains.
The symbols are not properly resolved during linking because mingwex (which provides the implementations) is not
linked after gtest on the link line.
The Solution
Added a conditional workaround at the end of SfGoogleTestConfig.cmake:
The INTERFACE keyword is critical — it ensures mingwex is emitted after gtest on the link line, which is
required for the symbols to resolve correctly. This centralized fix in the CMake library means all projects using
this library automatically get the workaround.
Template Project Updates
Files: tpl/root/AGENTS.md, tpl/root/CMakePresets.json, tpl/root/src/qt/CMakeLists.txt,
tpl/root/src/tests/google/CMakeLists.txt
AGENTS.md Reorganization
The template AGENTS.md was restructured:
- Added a Response Rules section at the top to guide AI agents working on projects using this template.
- Reordered sections:
Response Rules→Commands→Boundaries→C++ Code Style→Testing Strategy→Commit Style→Review of Code. - Removed the basic "Build the Project" and "Running Unit Tests" sections that were redundant with the build script's own help output.
- Added a Review of Code section linking to the reviewer documentation.
- Fixed indentation consistency (tabs vs spaces).
CMakePresets.json Generator Order
In CMakePresets.json, the CPack generator order for Linux presets was
changed from [TGZ, AppImage, DEB] to [TGZ, DEB, AppImage].
The Problem
When packaging the Qt library, the AppImage generator fails and stops the CPack build, preventing DEB packages from being generated. Since Qt library publishing is currently a manual process, this is an acceptable limitation, but it means DEB packages — which are the primary automated output — should be generated first.
The Solution
AppImage was moved to the last position in the generator list. This way, TGZ and DEB packages are always produced before AppImage is attempted. If AppImage fails, the DEB package is already available for manual publishing. A lazy but practical workaround until Qt library AppImage support is properly fixed.