Ogravius Dev Blog

Document. Forget. Repeat. ¯\_(ツ)_/¯

Last updated: 2026/08/09

[#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.