[#1 - Powershelling with ninja]
How does it work?
Folder structure
project/
├── sandpit/ <--- Sandpit used to test and measure code from host projects like winx86_64/include
│ ├── dll/
│ ├── include/
│ ├── lib/
│ ├── src/
│ │ └── main.c
│ └── vendor/
└── winx86_64/ <--- Host project for Windows
├── dll/
├── include/ <--- Multiple headers forming Unity build
│ ├── aa_main.h
│ ├── ab_engine.h
│ └── More headers...
├── lib/
│ ├── debug/
│ ├── release/
│ └── static/ <--- Some libs have to be used for both debug/release configurations
│ ├── dbghelp.lib
│ └── User32.Lib
├── src/
│ └── main.c <--- The only source file for Unity build
└── vendor/ <--- 'External' includes from 3rd party libraries
Building code
The example above shows a small subset of the project. Notably, there is no sign of CMake, Makefiles, or other complex code generators. My code is built simply by executing either ./build.ps1 or ./build_sandpit.ps1. Naturally, PowerShell does not execute the actual compilation itself; rather, it generates Ninja rules and hands the build process off to Ninja. The folder structure shown above directly drives how the code is built.
You might have noticed that there are no multiple .c files listed anywhere. This is intentional: the entire project is compiled and linked together as a single Unity build.
Compiler/Linker flags
$ClangClFlags = "/clang:-std=c23"
$DebugCompilerFlags = "/nologo /MDd /Z7 /Ob0 /Od /RTC1 /W4 /EHsc /GR- /fp:fast /TC /DMACRO_ENABLED $ClangClFlags"
$ReleaseCompilerFlags = "/nologo /MD /Zi /O2 /Ob2 /Zo /FAs /DNDEBUG /EHsc /FS /GR- /fp:fast /Fa$OutReleaseAsm\ /TC /DMACRO_ENABLED $ClangClFlags"
$DebugCompilerFlags += " /I$NinjaInclude /external:I$NinjaIncludeVendor /external:W0 "
$ReleaseCompilerFlags += " /I$NinjaInclude /external:I$NinjaIncludeVendor /external:W0 "
$DebugLinkerFlags = "/SUBSYSTEM:WINDOWS /NOLOGO /DEBUG:FULL /OPT:REF /OPT:ICF /INCREMENTAL:NO /PDBALTPATH:%_PDB%"
$ReleaseLinkerFlags = "/SUBSYSTEM:WINDOWS /NOLOGO /DEBUG:FULL /OPT:REF /OPT:ICF /INCREMENTAL:NO /PDBALTPATH:%_PDB%"
$LibrarianFlags = "/NOLOGO"
if ($ProjectType -eq "dll"){
$DebugCompilerFlags = "$DebugCompilerFlags /LD /D_USRDLL /D_WINDLL"
$ReleaseCompilerFlags = "$ReleaseCompilerFlags /LD /D_USRDLL /D_WINDLL"
$DebugLinkerFlags = "$DebugLinkerFlags /DLL /OPT:NOICF"
$ReleaseLinkerFlags = "$ReleaseLinkerFlags /DLL /OPT:NOICF"
}
The snippet above details all the compiler, linker, and librarian flags I use. There is no hidden build system managing them behind the scenes - what you see is what you get.
For this project, I am using the clang-cl compiler, the lld-link linker, and the llvm-lib librarian. I chose this toolchain primarily because it supports the latest C23 standard, offers excellent debugging tools, and supports most of the significant MSVC compiler flags. Another major benefit is the VS Code extension that utilizes clangd for IntelliSense, which integrates seamlessly with my setup.
I will not discuss specific flags here, as the official documentation for each tool remains the best resource for that information.
MSVC compiler flags:
https://learn.microsoft.com/en-us/cpp/build/reference/compiler-options-listed-alphabetically?view=msvc-170
MSVC linker flags:
https://learn.microsoft.com/en-us/cpp/build/reference/linker-options?view=msvc-170
MSVC librarian flags:
https://learn.microsoft.com/en-us/cpp/build/reference/overview-of-lib?view=msvc-170
clang-cl manual:
https://clang.llvm.org/docs/UsersManual.html#clang-cl
Ninja
$NinjaContent = @"
rule $("$ProjectName")_release_cc
command = $($Compiler) $($NinjaCompilerCommands) $($CompilerObjectOut)`$out $($ReleaseCompilerFlags) `$in
description = Compiling `$out
deps = $($NinjaDeps)
rule $("$ProjectName")_release_lk
command = $($Linker) $($LinkerObjectOut)`$out $($ReleaseLinkerFlags) @`$out.rsp
description = Linking `$out
rspfile = `$out.rsp
rspfile_content = `$in
"@
if ($ProjectType -eq "exe") {
$NinjaContent += "`nbuild $($NinjaDebugBin)/$($ExecutableName): $("$ProjectName")_debug_lk $ListOfDebugObj $ListOfStaticLibs $ListOfDebugLibs "
$NinjaContent += "`nbuild $($NinjaReleaseBin)/$($ExecutableName): $("$ProjectName")_release_lk $ListOfReleaseObj $ListOfStaticLibs $ListOfReleaseLibs"
}
elseif ($ProjectType -eq "lib") {
$NinjaContent += "`nbuild $($NinjaDebugLib)/$($LibName): $("$ProjectName")_lib $ListOfDebugObj $ListOfStaticLibs $ListOfDebugLibs"
$NinjaContent += "`nbuild $($NinjaReleaseLib)/$($LibName): $("$ProjectName")_lib $ListOfReleaseObj $ListOfStaticLibs $ListOfReleaseLibs"
}
elseif ($ProjectType -eq "dll") {
$NinjaContent += "`nbuild $($NinjaDebugLib)/$($DllName) | $($NinjaDebugLib)/$($LibName): $("$ProjectName")_debug_dll $ListOfDebugObj $ListOfStaticLibs $ListOfDebugLibs"
$NinjaContent += "`nbuild $($NinjaReleaseLib)/$($DllName) | $($NinjaReleaseLib)/$($LibName): $("$ProjectName")_release_dll $ListOfReleaseObj $ListOfStaticLibs $ListOfReleaseLibs"
}
The snippet section above illustrates how I generate the compilation and linking rules for Ninja. These rules are essentially the same command-line statements that you would otherwise execute manually. The build statement provides Ninja with the exact recipe required to compile a file, specifying the rule to apply along with any additional arguments, such as file lists.
Ninja manual:
https://ninja-build.org/manual.html
Interesting fact
It is worth noting that the Ninja build system can be utilised for custom tooling far beyond standard code compilation. As a result, my future build scripts may leverage this capability to automate the processing of HLSL shaders and the manipulation of texture assets.