This guide explains in broad strokes how to set up C++ tool chains on Windows for usage of all big three compilers with CMake and Ninja through PowerShell. PowerShell is advised because of its customizabilty, which we'll use to conveniently juggle between the different toolchains.
-
Download and install Ninja.
Ninja is a build system, its purpose is to run compile commands and keep track of your project files' compilation status, to tell whether they're up-to-date or need recompiling.
Extract the executable file to a folder and add that folder's path to your systemPATHenvironment variable. -
Download and install Cmake.
CMake is a meta-build system, its purpose is to take in scripts that describe your project structure and turn it into a sequence of compile commands to be ran by a build system (Ninja).
Simply run the installer and make sure to leave the option Add CMake to the PATH environment variable enabled.
- Download and install Visual Studio Community 2022.
VS is Microsoft's IDE and installing it is how you get MSVC, Microsoft's C and C++ compiler.
VS as a whole is a gigantic piece of software and we only need bits and parts of it. From the linked page above you'll be getting the so-called VS Installer. Proceed as follows:- Run the installer
- Click the Install button on the entry for VS 2022 Community
- In the window that opens:
- In tab Workloads, check the entry which reads C++ desktop development, under Desktop and mobile.
- In the sidebar that appears on the right, uncheck the bloatware entries Live Share, IntelliCode and GitHub Copilot.
- If you're on Windows 10 and the Windows 11 SDK got automatically selected, you probably want to uncheck that too and scroll down a bit to instead pick a Windows 10 SDK (it doesn't really matter which one, just take the one with the biggest number). Make sure you end up with a Windows SDK installed, your setup will not work properly otherwise!
- Click Install and go grab your favorite hot beverage.
- Download and install LLVM.
LLVM is an open-source collection of compiler technologies, which Clang is a part of. From the latest release, scroll down in the
assets and download
LLVM-<version>-win64.exe, which is the installer for LLVM and all of its components. Run it and make you way through it.
While installing Visual Studio above gave you both MSVC and its implementation of the C++ standard library (colloquially named STL), installing LLVM gives you Clang but not libc++, LLVM's own imlementation of the C++ standard library. However, this turns out to be fine since Clang is able to detect and use MSVC's STL.
- Download and install MSYS2.
MSYS2 is a distribution of MinGW, an up-to-date and still maintained port of Linux tools to Windows. It comes with a package manager
from which we can install GCC. Run the installer and make your way through it.
Once installed, search for MSYS2 MINGW64 in the Windows start menu. This will launch a terminal to interact with the "MinGW64" environment of MSYS2. Run the following commands in it:pacman -Syu, to update package databases. The update process may state that MSYS2 needs to be restarted for the update to complete. If this happens, close the terminal, reopen it, and then runpacman -Syuagain. This command should be ran every once in a while to keep things up-to-date.pacman -S mingw-w64-x86_64-gcc, to install GCC and its dependencies.
When invoked on its own, CMake looks for compilers in a bunch of places and will settle with the first one that it finds. If you want to use a compiler which isn't being picked up by CMake, you need to nudge it in the right direction, and there are a couple ways about that:
- CMake will use the compilers whose paths are provided in variables
CMAKE_C_COMPILERandCMAKE_CXX_COMPILER, and you can supply those when invoking CMake by giving it extra arguments-DCMAKE_C_COMPILER="..."and-DCMAKE_CXX_COMPILER="..."(replace the ellipses with actual paths). - CMake will also automatically attempt to fill in the two aforementioned variables from environment variables
CCandCXX, respectively, so you can put the paths to your compilers of choice into those environment variables before invoking CMake, this time without having to supply extra arguments in that regard.
There is an issue with both of these methods: you are required to fill in the full path to your compiler, which is a severe
inconvenience. That is unless you added to your PATH environment variable each of the directories where your compilers can be found,
in which case simply providing the name of the compiler is enough for CMake to correctly find it. However, permanently adding stuff to
your PATH can cause undesirable side effects, and in the case of MinGW, those will happen sooner rather than later. Even if GCC is to
be ignored, one can imagine having to juggle between several builds of the same compiler, and having to adjust the name of the
executable or the order of directories in PATH prior to invoking CMake is less than savory. We want uniformity and convenience, and
the solution proposed hereafter tackles all of those problems while being easily extensible: custom PowerShell commands to update PATH
and push certain compilers into the appropriate environment variables for CMake to pick up on.
We could indeed entertain the idea that since MinGW tools should primarily be used from the MSYS2 terminal, we should instead invoke CMake from the MSYS2 terminal whenever we'd want to use GCC. And since we want uniformity, we can then do the same thing that we're about to do with PowerShell, namely write Bash scripts to push certain compilers in the correct environment variables. However, I am more comfortable with PowerShell, so PowerShell is what I did all this with, and that's what you get in this gist. Deal with it.
- Launch PowerShell
- Run
$PROFILE - Open or create the file at the path that gets printed, and paste the following in there:
PowerShell profile (click me)
# Global VS setup info
$VsEdition = 'Community'
$VsSetups = @{
'2022' = @{ 'path' = "C:\Program Files\Microsoft Visual Studio\2022\$VsEdition"; 'gen' = 'Visual Studio 17 2022'; };
}
# Utilities
function Append-Tokens() {
param(
[Parameter(Mandatory = $true, ValueFromPipeline = $true)]
[string] $Data,
[Parameter(Mandatory = $true)]
[string] $Delimiter,
[parameter(Mandatory = $true)]
[string[]] $Values
)
if (-not $Data.EndsWith($Delimiter)) {
$Data += $Delimiter
}
foreach ($v in $Values) {
$Data += $v + $Delimiter
}
return $Data
}
function Remove-Tokens() {
param(
[Parameter(Mandatory = $true, ValueFromPipeline = $true)]
[string] $Data,
[Parameter(Mandatory = $true)]
[string] $Delimiter,
[parameter(Mandatory = $true)]
[string[]] $Values
)
$ValueMatch = {
param($ItVal)
$ItVal = $ItVal.Trim()
foreach ($v in $Values) {
if (($ItVal -ieq $v.Trim()) -or ($ItVal -eq '')) {
return $true
}
}
return $false
}
$sp = [System.Collections.Generic.List[System.Object]]($Data -Split $Delimiter)
$removed = $sp.RemoveAll($ValueMatch)
return $sp | Join-String -Separator $Delimiter
}
function Get-Assemblies() {
param($Match, [Switch]$NamesOnly);
$matchingAsms = [Threading.Thread]::GetDomain().GetAssemblies() |
? {$_.Location -match $Match }
if ($NamesOnly.IsPresent) {
return $matchingAsms | select @{Name="Name"; Expression={ (dir $_.Location).Name }}
} else {
return $matchingAsms
}
}
# Env setup entries
$DevEnvs = [ordered]@{
'MSYS2' = @{ 'enabled' = $false; 'paths' = "$env:MSYS_HOME\usr\bin" ; 'c' = 'gcc' ; 'cxx' = 'g++' ; 'gen' = 'MinGW Makefiles' ; };
'MinGW' = @{ 'enabled' = $false; 'paths' = "$env:MSYS_HOME\mignw64\bin"; 'c' = 'gcc' ; 'cxx' = 'g++' ; 'gen' = 'MinGW Makefiles' ; };
'LLVM' = @{ 'enabled' = $false; 'paths' = "$env:LLVM_HOME\bin" ; 'c' = 'clang'; 'cxx' = 'clang++'; 'gen' = 'Ninja' ; };
}
# Calculate and append VC++ specific env setup entries
foreach ($Version in $VsSetups.Keys) {
$ThisVsPath = $VsSetups[$Version]['path']
$ThisVcPaths = @(
"$ThisVsPath\\MSBuild\Current\Bin\amd64",
"$ThisVsPath\Common7\IDE\",
"$ThisVsPath\Common7\Tools\",
"C:\Windows\Microsoft.NET\Framework\v4.0.30319"
)
$ShortenedVersion = $Version.Substring(2, 2)
$VCVersion = "VC$ShortenedVersion"
$DevEnvs["$VCVersion"] = @{
'enabled' = $false;
'paths' = $ThisVcPaths;
'var' = '$env:POSH_ENV_' + $VCVersion;
'c' = 'cl';
'cxx' = 'cl';
'gen' = $VsSetups[$Version]['gen'];
}
}
# Stacking dev envs capacity
$DevEnvPriority = $DevEnvs.keys[(@($DevEnvs.keys).length - 1)..0]
function Push-LastCompilersOnCMakeEnv() {
foreach ($Name in $DevEnvPriority) {
if ($DevEnvs[$Name]['enabled']) {
$env:CC = $DevEnvs[$Name]['c']
$env:CXX = $DevEnvs[$Name]['cxx']
$env:CMAKE_GENERATOR = $DevEnvs[$Name]['gen']
Write-Host "Pushed compilers and generator for enabled env '$Name' on CMake env"
break
}
}
}
function Has-LoadedDifferentVCAssembliesBefore() {
param([int] $Version)
$DifferentAssemblies = Get-Assemblies -Match 'Microsoft.VisualStudio' | Where-Object { -not $_.Location.Contains("$Version")}
return $DifferentAssemblies.Count -gt 0
}
function Enable-DevEnv() {
param([string] $Name)
if ($Name.StartsWith('VC')) {
$Version = 2000 + $Name.Substring(2, 2)
if (-not ($VsSetups.Keys -contains "$Version")) {
Write-Error "The requested version of VC++ is not installed on the current system, or this script could not find the related setup info."
return 1
}
if (Has-LoadedDifferentVCAssembliesBefore $Version) {
Write-Error "A different VC++ environment was loaded in this PowerShell session prior to this one. PowerShell cannot unload assemblies once loaded, and loading additional assemblies for the requested VC++ environment would cause conflicts. PowerShell must be restarted before another VC++ environment may be loaded."
return 1
}
}
if (-not $DevEnvs[$Name]['enabled']) {
if ($Name.StartsWith('VC')) {
$VsPath = $VsSetups["$Version"]['path']
& "$VsPath\Common7\Tools\Launch-VsDevShell.ps1" -SkipAutomaticLocation -Arch 'amd64' -HostArch 'amd64'
} else {
$env:PATH = $env:PATH | Append-Tokens -Delimiter ';' -Values $($DevEnvs[$Name]['paths'])
}
if ($Name -eq 'MinGW') {
$env:MSYSTEM = "MINGW64"
Enable-DevEnv 'MSYS2'
}
$env:CMAKE_GENERATOR = $DevEnvs[$Name]['gen']
if ($env:CMAKE_GENERATOR -eq 'Ninja') {
$env:CMAKE_EXPORT_COMPILE_COMMANDS = $true
}
$env:CC = $DevEnvs[$Name]['c']
$env:CXX = $DevEnvs[$Name]['cxx']
$DevEnvs[$Name]['enabled'] = $true
}
return 0
}
function Disable-DevEnv() {
param([string] $Name)
if ($DevEnvs[$Name]['enabled']) {
$env:PATH = $env:PATH | Remove-Tokens -Delimiter ';' -Values $DevEnvs[$Name]['paths']
if ($Name.StartsWith('VC')) {
Remove-Module -Name 'Microsoft.VisualStudio.DevShell'
}
$env:CC = $null
$env:CXX = $null
if ($env:CMAKE_GENERATOR -eq 'Ninja') {
$env:CMAKE_EXPORT_COMPILE_COMMANDS = $null
}
$env:CMAKE_GENERATOR = $null
$DevEnvs[$Name]['enabled'] = $false
Push-LastCompilersOnCMakeEnv
if ($Name -eq 'MinGW') {
$env:MSYSTEM = $null
Disable-DevEnv 'MSYS2'
}
}
}
# region msys2
function msys2() { Enable-DevEnv 'MSYS2' }
function unmsys2() { Disable-DevEnv 'MSYS2' }
#region mingw
function mingw() { Enable-DevEnv 'MinGW' }
function unmingw() { Disable-DevEnv 'MinGW' }
# region llvm
function llvm() { Enable-DevEnv 'LLVM' }
function unllvm() { Disable-DevEnv 'LLVM' }
# region vc++
$activeVcEnv = $null
function vc() {
param(
[string] $Version
)
if ((Enable-DevEnv ('VC' + $Version)) -eq 0) {
$activeVcEnv = 'VC' + $Version
}
}
function unvc() {
if ($activeVcEnv -ne $null) {
Disable-DevEnv $activeVcEnv
$activeVcEnv = $null
}
}$PROFILE is a script that PowerShell executes when starting up. With the above, PowerShell will start with a bunch of new commands
defined, which can be used to tweak the environment from which we invoke CMake. The list of new commands (the relevant ones) is the
following:
mingw- enables the MinGW environment, with compilersgccandg++(undo withunmingw)llvm- enables the LLVM environment, with compilersclangandclang++(undo withunllvm)vc 22- enables the VC++ environment for Visual Studio 2022, with compilercl(undo withunvc)
First enable one of these environment with the appropriate command, and then invoke CMake like you normally would. It should pick the proper toolchain. To clear the environment, simply run the matching undo command, or just close the terminal entirely.
You should of course tweak the script to match your setup and needs. In particular, each of the entries in map $DevEnvs has a paths
variable, which lists the paths to append to the PATH environment variable when the compiler is to be pushed. In those, I use to
environment variables like $env:LLVM_HOME and $env:MSYS_HOME. I manually defined those on my system, and it's unlikely that you have
them at all. You need to change those to reflect the binary directories of LLVM and MSYS 2 respectively.
This kind of environment setup map can be extended to include different installations of VS, but also entirely different toolchains like MinGW Clang or a custom-built Clang, or even something not C++-related.
Are you juggling with so many terminals at once that it becomes hard to keep track of which terminal has which environment enabled?
Chances are, if you're that much of a terminal person, you've got some kind of customization going on. For PowerShell,
Oh My Posh is the most popular option. If you're not using it already, consider giving
it a try, it's very neat and convenient.
In this section, we will customize the theme that Oh My Posh uses, and make it react to environment variables to display in the prompt a
small indicator of which environment is currently in use.
First we need to tweak the PowerShell profile script again.
- In variable
$DevEnvs, add a new keyvarto each of the registered environment entries. It should contain the name of an environment variable (as a string) that should be monitored by Oh My Posh to tell whether an environment is active. It could look like this:
$DevEnvs = [ordered]@{
'MSYS2' = @{ ... ; 'var' = '$env:POSH_ENV_MSYS2'; };
'MinGW' = @{ ... ; 'var' = '$env:POSH_ENV_MinGW'; };
'LLVM' = @{ ... ; 'var' = '$env:POSH_ENV_LLVM' ; };
}- Don't forget to add that new
varkey to the computed VC environment entries, slightly below:
$DevEnvs["$VCVersion"] = @{
# ...
'var' = '$env:POSH_ENV_' + $VCVersion;
}- Next, we need to turn on that new environment variable whenever the corresponding environment is enabled. This happens in function
Enable-DevEnv:
function Enable-DevEnv() {
param([string] $Name)
# ...
if (-not $DevEnvs[$Name]['enabled']) {
# ...
($DevEnvs[$Name]['var'] + ' = "1"') | Invoke-Expression
}
}- Similarly, the variable should be turned off whenever the corresponding environment is disabled, which happens in function
Disable-DevEnv:
function Disable-DevEnv() {
param([string] $Name)
if ($DevEnvs[$Name]['enabled']) {
# ...
($DevEnvs[$Name]['var'] + ' = "0"') | Invoke-Expression
}
}This is it for tweaking the PowerShell profile. Now we need to customise the Oh My Posh theme. First, create a copy of it somewhere
convenient. Then, inside it, locate the block which starts with "alignment": right". In the "segments" list of that block, add a
new object like so:
{
"type": "text",
"style": "plain",
"foreground": "#34eb55",
"templates": [
"{{ if eq .Env.POSH_ENV_MSYS2 \"1\" }}[MSYS2]{{ end }}",
"{{ if eq .Env.POSH_ENV_MinGW \"1\" }}[MinGW]{{ end }}",
"{{ if eq .Env.POSH_ENV_VC22 \"1\" }}[VC++]{{ end }}",
"{{ if eq .Env.POSH_ENV_LLVM \"1\" }}[LLVM]{{ end }}",
],
"templates_logic": "join"
}Now Oh My Posh will query your newly defined variables and will print a green, right-aligned segment that reads [env] as part of the
prompt, and whose actual text reflects the environment currently enabled. The only thing that's left to do before that is to tell Oh My
Posh to use your custom theme, and you can do that by changing the Oh My Posh initialization in your PowerShell profile like so:
oh-my-posh init pwsh --config "path\to\your\custom\theme.json" | Invoke-ExpressionThis is what the result may look like:

For whatever reason, both lldb-dap.exe and lldb.exe itself use functions from python310.dll and will fail with error 0xC0000035
if that DLL is not found nearby or on the PATH. They also use stuff from the Python 3.10 standard library and will attempt to find
modules in python310.zip, which must also be located nearby. You can get both of these files by downloading the
"Windows embeddabled package (64-bit)" from the bottom of this page. From the
downloaded archive, extract the two needed files directly next to lldb-dap.exe and lldb.exe. Your debug sessions should launch
properly now.
It is possible that LLDB is unable to load the debug symbols for your executable. LLDB will use one of two PDB parsers: a so-called
"native", LLVM in-house implementation ; or Microsoft's Debug Interface Access (DIA) binaries. The reason why LLDB does not hit
breakpoints is likely that it tries to use DIA to parse the PDB file, but cannot, because it can't find DLL msdia140.dll, and thus
continues with no symbols loaded. There are two ways about solving this issue:
- Get
msdia140.dllfrom your VS installation (for VS 2022 Community, it is located atC:\Program Files\Microsoft Visual Studio\2022\Community\Common7\IDE), and copy it next tolldb.exe. - Define environment variable
LLDB_USE_NATIVE_PDB_READERwith value1prior to invoking LLDB. This will cause LLDB to use the LLVM in-house PDB parser rather than DIA, and your symbols will load properly as a result. To that effect:- In your PowerShell profile: you can add an extra check in function
Enable-DevEnvand create that variable whenever environment LLVM is enabled. - If you're using LLDB with LLDB-DAP in VS Code: open the settings of the LLDB-DAP extension and define that variable in the "Lldb-dap: Environment" setting.
- In your PowerShell profile: you can add an extra check in function
LLDB should now hit your breakpoints.
Still figuring that one out, come back later
