Skip to content

Fix #116: Add /DEBUG /OPT:REF /OPT:ICF to Release linker flags for PDB generation - #162

Open
gregorian-09 wants to merge 2 commits into
MarkSchofield:mainfrom
gregorian-09:fix/issue-116-pdb-release-mode
Open

Fix #116: Add /DEBUG /OPT:REF /OPT:ICF to Release linker flags for PDB generation#162
gregorian-09 wants to merge 2 commits into
MarkSchofield:mainfrom
gregorian-09:fix/issue-116-pdb-release-mode

Conversation

@gregorian-09

Copy link
Copy Markdown

Fix #116: No PDB files generated in Release mode

The Problem

When building with the MSVC or Clang toolchain in Release configuration, no .pdb files are produced. This means debuggers cannot resolve symbols, crash dumps are unreadable, and post-mortem debugging of Release builds is impossible. This is a significant pain point for anyone shipping Windows software.

The root cause is that CMake's built-in platform files (Windows-MSVC.cmake) only add the /DEBUG linker flag for Debug and RelWithDebInfo configurations. For Release (and MinSizeRel), they add /INCREMENTAL:NO but intentionally omit /DEBUG. That makes sense as a conservative default (some projects do not want the extra linker overhead), but a toolchain file's job is to describe the capabilities of the toolchain, and MSVC's linker is perfectly capable of producing PDBs in Release mode.

What Changed

Two files were modified; one is new:

Windows.MSVC.toolchain.cmake and Windows.Clang.toolchain.cmake - Added a foreach block after the compiler flag setup that sets CMAKE_<TGT>_LINKER_FLAGS_RELEASE_INIT for all three link target types (EXE, SHARED, MODULE):

foreach(TGT EXE SHARED MODULE)
    set(CMAKE_${TGT}_LINKER_FLAGS_RELEASE_INIT
        "/machine:${CMAKE_VS_PLATFORM_TOOLSET_ARCHITECTURE} /DEBUG /OPT:REF /OPT:ICF")
endforeach()

The /machine: flag is included so that CMake's platform file does not skip setting it (it has an if(NOT DEFINED) guard). CMake then appends /INCREMENTAL:NO on top of our init value, producing the final set:

/machine:x64 /DEBUG /OPT:REF /OPT:ICF /INCREMENTAL:NO

This matches exactly what the vcpkg team reported as expected in the linked issue.

test/TestReleaseLinkerFlags.cmake (new) - A CMake script-mode unit test that validates the linker flags for all four target architectures (x64, ARM64, ARM, X86) and all three target types, plus verifies that Debug, RelWithDebInfo, and MinSizeRel are not affected.

Why This Approach

I considered a few alternatives:

  1. Appending to CMAKE_<TGT>_LINKER_FLAGS_RELEASE directly - This would not work in a toolchain file because those cache variables are not created until after the platform files run.

  2. Using the _INIT variables without /machine: - CMake's platform file guards /machine: with if(NOT DEFINED), so if we set the _INIT variable, CMake would skip adding the architecture flag. Every binary would lack the correct machine type.

  3. Setting CMAKE_MSVC_DEBUG_INFORMATION_FORMAT - This is a CMake 3.25+ variable that controls how debug info is stored (embedded vs PDB), not whether it is generated. It does not solve the problem.

  4. The chosen approach - Set the full _INIT value including /machine:. The platform file detects it is already defined and skips the machine-type initialization, then appends /INCREMENTAL:NO. Clean, predictable, and matches how CMake's own platform files work.

Verification

I verified this on the actual MSVC compiler toolchain through the Windows cmake.exe:

CMAKE_EXE_LINKER_FLAGS_RELEASE = /machine:x64 /DEBUG /OPT:REF /OPT:ICF /INCREMENTAL:NO

All three example targets built successfully in Release mode, each producing a PDB:

Target PDB Size
CommandLineC.exe 454 KB
CommandLine.exe 500 KB
SharedLibrary.dll 372 KB

Debug mode also confirmed clean. No regressions.

cmake-lint passes on all 14 tracked CMake files with zero issues.

Notes

  • Windows.EWDK.toolchain.cmake inherits the fix automatically (it includes Windows.MSVC.toolchain.cmake at line 121).
  • MinSizeRel has the same problem (no /DEBUG), but this fix only targets Release to match the scope of the reported issue. MinSizeRel can be addressed separately if needed.
  • Static libraries (CMAKE_STATIC_LINKER_FLAGS_*) are intentionally left alone. PDBs are only relevant for linked images.
@MarkSchofield

MarkSchofield commented Jul 2, 2026

Copy link
Copy Markdown
Owner

Thanks for the PR; I really appreciate you taking the time to implement and write-up the details.

But I'm not entirely sure what the right behavior should be here. In my comment on #116 there's two main scenarios:

  1. Using WindowsToolchain as a general CMake toolchain. The goal here is to make it easier to leverage CMake canonically on Windows. And in standard CMake Release builds, I wouldn't expect a Release build to produce PDB's; RelWithDebInfo builds would produce PDBs. If consumers of this Toolchain want to define just, say, Debug and Release builds, where Release builds produce PDB's, then I would expect that to be possible; setting, say, CMAKE_CONFIGURATION_TYPES to be Debug;Release, and setting CMAKE_${TGT}_LINKER_FLAGS_RELEASE_INIT to configure PDB generation, then I'd expect that to work. And if this Toolchain is doing things that prevent that, then that would be a bug.
  2. Using WindowsToolchain for building VCPkg ports. In that case, VCPkg does - theoretically - allow a toolchain to be specified. But the toolchain that VCPkg requires is very specific to the point that I'd want to implement it by wrapping, say, Windows.MSVC.toolchain.cmake. In that case, the specific requirements would configure Windows.MSVC.toolchain.cmake (as in (1)) to produce PDB's for Release builds.

Implementing (2) seems entirely reasonable, but - at the time of my comment on #116 - there were too many problems with vcpkg's support for using a '3rd party toolchain' - the main one being that vcpkg expects the 'PATH' environment to be updated, but doesn't provide a mechanism for the '3rd party toolchain' to do that. Ideally, vcpkg would not be reliant on the 'PATH' environment variable, it would use find_program and allow a '3rd party toolchain' to influence find_program searches.

I think there's real value in supporting a '3rd party toolchain' for building vcpkg ports - the main use-case I would care about would be to be able to use clang-cl.exe for compiling vcpkg ports to - hopefully - improve the performance of the build. In that case, writing a vcpkg-specific toolchain that wraps Windows.Clang.toolchain.cmake seems like a great approach. I'm happy to do that work, or have someone contribute that work, but it would be contingent on vcpkg fully supporting a '3rd party toolchain'.

… flags for PDB generation

The CMake platform files for MSVC only add /DEBUG for Debug and
RelWithDebInfo configurations. Release and MinSizeRel builds get
/INCREMENTAL:NO but no /DEBUG, so no PDB files are produced.

Set CMAKE_<TGT>_LINKER_FLAGS_RELEASE_INIT with /DEBUG /OPT:REF
/OPT:ICF alongside the required /machine: flag so that Release
builds generate PDB files.

Windows.EWDK.toolchain.cmake inherits the fix automatically
as it includes Windows.MSVC.toolchain.cmake.
Problem:
- Generic Release builds should keep CMake's canonical behavior where PDBs are not enabled by default.
- vcpkg Release port builds expect /DEBUG /OPT:REF /OPT:ICF, so that behavior needs a vcpkg-specific integration point instead of changing the general MSVC and Clang toolchains.

Changes:
- Add Windows.Vcpkg.MSVC.toolchain.cmake and Windows.Vcpkg.Clang.toolchain.cmake wrappers that include vcpkg's Windows toolchain before the matching WindowsToolchain compiler wrapper.
- Configure the Clang vcpkg wrapper to use clang-cl via the MSVC frontend variant.
- Remove the generic Release linker flag defaults from Windows.MSVC.toolchain.cmake and Windows.Clang.toolchain.cmake.
- Replace the generic Release linker flag test with a wrapper include-order and vcpkg Release flag test.
- Update README guidance to point VCPKG_CHAINLOAD_TOOLCHAIN_FILE at the new vcpkg-specific wrappers.

Verification:
- cmake -P test/TestVcpkgToolchainWrappers.cmake passed.
- git diff --check --cached passed.
- Windows cmake.exe configured and built example/CommandLineC with Windows.Vcpkg.MSVC.toolchain.cmake; the Release link line included /DEBUG /INCREMENTAL:NO /OPT:REF /OPT:ICF and produced CommandLineC.pdb.
- Windows cmake.exe configured and built example/CommandLineC with Windows.Vcpkg.Clang.toolchain.cmake; clang-cl/lld-link used the same Release linker flags and produced CommandLineC.pdb.

Refs: MarkSchofield#116
@gregorian-09
gregorian-09 force-pushed the fix/issue-116-pdb-release-mode branch from 0604d92 to 27f2b60 Compare July 2, 2026 07:45
@gregorian-09

Copy link
Copy Markdown
Author

Thanks Mark, that distinction makes sense. I reworked the PR around the split you described: the generic Windows.MSVC.toolchain.cmake and Windows.Clang.toolchain.cmake no longer change normal CMake Release behavior, so they stay aligned with the usual convention where RelWithDebInfo is the configuration that produces PDBs.

The vcpkg-specific behavior now lives behind two dedicated wrappers: Windows.Vcpkg.MSVC.toolchain.cmake and Windows.Vcpkg.Clang.toolchain.cmake. They include vcpkg’s Windows toolchain first, then include the matching WindowsToolchain compiler wrapper, so vcpkg keeps its expected Release linker flags (/DEBUG /INCREMENTAL:NO /OPT:REF /OPT:ICF) without changing defaults for general WindowsToolchain consumers.

I also updated the README, replaced the original generic Release-linker test with a wrapper-focused test, and verified both wrappers by configuring and building the CommandLineC example with Windows cmake.exe. The MSVC path produced a PDB with cl.exe, and the Clang path produced a PDB with clang-cl.exe / lld-link.exe.

@MarkSchofield

Copy link
Copy Markdown
Owner

Thanks, @gregorian-09. I'm still thinking about the best approach here. To help me try things out - and to make sure we're all on the same page for the discussion - I added to the 'example' folder - adding VCPkg support. I've pushed it as the "mschofie/vcpkg" branch.

I submoduled vcpkg, added presets based-on my understanding of the current 'best' way to leverage vcpkg. I used a manifest to build 'sqlite3' (because it's small, and should be relatively easy to build) and 'protobuf' (which pulls in 'abseil' and 'utf8-range', and is a bit bigger). And I added custom triplets to specify the new toolchain files.

I did have to make a fix to vcpkg - it unconditionally adds /c65001 to CMAKE_RC_FLAGS, which causes breaks with clang-cl.exe. And I couldn't add 'openssl' as a dependency, because that uses nmake and (as I called out in #116), I can't find a way to affect the find_program from a toolchain.

And I had to make a fix to your files - I had to include scripts/toolchains/windows.cmake after, say, Windows.Clang.toolchain.cmake because scripts/toolchains/windows.cmake checks, say, CMAKE_CXX_COMPILER to build compiler options, so Windows.Clang.toolchain.cmake needs to set that.

And with that, things appear to be looking good. I have to admit, my original feedback might've been wrong - adding more 'toolchain' .cmake files makes things a little confusing, and I'm wondering if we could detect if the toolchain is being used from within a 'port' build (I'm not sure of the best way to detect that right now), should we just use the existing toolchain files ('Windows.Clang.toolchain.cmake'/'Windows.MSVC.toolchain.cmake'), and towards the end, just include 'scripts/toolchains/windows.cmake' as necessary.

I'm a little bit worried about VCPkg's support for this - scripts/toolchains/windows.cmake makes a lot of assumptions that msvc's cl.exe is being used, but there are checks for clang-cl.exe already, so that's promising.

With the example, and the triplet files using clang-cl.exe, building sqlite and protobuf ports was a good deal faster than msvc, which (as I said earlier) is the most compelling use case I can think of. On my machine running cmake configuration of the 'example' folder with preset 'windows-ninja-msvc-x64-vcpkg' took ~270 seconds using 'msvc' to build the ports, but ~217 seconds using clang-cl. That's a ~20% reduction in port-build times.

Just sharing my write-up for now; let me know what you think.

@gregorian-09

Copy link
Copy Markdown
Author

Thanks Mark, this is really useful. Having the example branch makes the tradeoff much clearer, because it moves this from "what should the integration look like?" to "what actually works against real vcpkg port builds?"

The include-order issue makes sense. If scripts/toolchains/windows.cmake is inspecting CMAKE_CXX_COMPILER while constructing its options, then the WindowsToolchain file needs to run first so the compiler identity and paths are already established. I agree that the current wrapper order is backwards for that case.

On the shape of the integration, I see the tradeoff the same way. The dedicated Windows.Vcpkg.*.toolchain.cmake files make the vcpkg-specific behavior explicit and keep the generic toolchains free of vcpkg assumptions, but they do add another set of entry points that users need to understand. Folding the behavior into Windows.MSVC.toolchain.cmake / Windows.Clang.toolchain.cmake would be cleaner from a user-facing perspective, provided we have a reliable signal that the toolchain is being evaluated as part of a vcpkg port build.

I think the main design question is what that signal should be. _VCPKG_ROOT_DIR, VCPKG_ROOT_DIR, Z_VCPKG_ROOT_DIR, or VCPKG_CHAINLOAD_TOOLCHAIN_FILE may be enough, but I’d want to be careful not to accidentally include vcpkg’s toolchain during a normal consumer project configure that happens to use vcpkg as the top-level dependency manager.

So my instinct is:

  1. If we can identify a reliable "port build" signal, folding this into the existing MSVC/Clang toolchains seems better.
  2. If that signal is ambiguous, the explicit wrapper files are probably safer even if they are a little noisier.
  3. Either way, the include order should be WindowsToolchain first, then vcpkg’s scripts/toolchains/windows.cmake.

The clang-cl result is encouraging. A ~20% reduction on sqlite/protobuf port builds is a pretty compelling reason to keep exploring this path.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

2 participants