Ogravius Dev Blog

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

Last updated: 2026/07/26

[Powershell script]

Licensed under: >[Unlicense]<

Referenced in: >[#1 - Powershelling with ninja]<

Note: The provided script is as-is and must be used at your own risk. Treat every line of script with suspicion, and never treat it as production ready.

Calling script (build.ps1)

$ProjectName = "static_lib"
$ProjectType = "lib"
$static_lib = @{
    ProjectName = $ProjectName
    ProjectType = $ProjectType
}

$ProjectName = "dynamic_lib"
$ProjectType = "dll"
$dynamic_lib = @{
    ProjectName = $ProjectName
    ProjectType = $ProjectType
}

$ProjectName = "core"
$ProjectType = "exe"
$core = @{
    ProjectName = $ProjectName
    ProjectType = $ProjectType
}

# Run the deployment script
.\.build/per_project.ps1 @static_lib
.\.build/per_project.ps1 @dynamic_lib
# Ensure all files are copied before running core if it has dependencies on static_lib or dynamic_lib
# This is not handled automatically in this example
.\.build/per_project.ps1 @core

Building script (per_project.ps1)

# ==============================================================================
# SECTION 1: Parameters and Initial Setup
# ==============================================================================
# This section defines how the script is called and sets up the directory structure.

# The script builds both configuration debug/release at the same time.
# .out folder is handled by Ninja
Param(
    [Parameter(Mandatory=$true)]
    [string]$ProjectName,
    
    # Validates that the user only passes 'exe', 'lib', or 'dll' as the project type
    [ValidateSet("exe", "lib", "dll")]
    [string]$ProjectType
)

Write-Host "--- Attempting to build $ProjectName --- " -ForegroundColor DarkGreen

# PowerShell settings
# "Stop" ensures the script completely halts if any error occurs, preventing cascading failures.
$ErrorActionPreference = "Stop" 

# Directory Paths 
# Replacing backslashes with forward slashes ensures compatibility with Ninja/C++ paths
$CurrentDirectory = (Get-Location).ToString().Replace('\', '/')

$Out = ".out"
$ProjectFolder = "project"
$ProjectOutFolder = "$Out/$ProjectName"
$ProjectSrcFiles = "$ProjectName/src"
$ProjectIncludeFiles = "$ProjectName/include"
$ProjectVendorIncludeFiles = "$ProjectName/include/vendor"
$ProjectVendorLibDebugFiles = "$ProjectName/lib/debug"
$ProjectVendorLibReleaseFiles = "$ProjectName/lib/release"
$ProjectVendorDllDebugFiles = "$ProjectName/dll/debug"
$ProjectVendorDllReleaseFiles = "$ProjectName/dll/release"

# Ninja Paths
# The '../../' are used here to escape the ninja build folder back to the root.
$NinjaSource = "../../$ProjectFolder/$ProjectSrcFiles"
$NinjaInclude = "../../$ProjectFolder/$ProjectIncludeFiles"
$NinjaIncludeVendor = "../../$ProjectFolder/$ProjectVendorIncludeFiles"
$NinjaDebugLibVendor = "../../$ProjectFolder/$ProjectVendorLibDebugFiles"
$NinjaReleaseLibVendor = "../../$ProjectFolder/$ProjectVendorLibReleaseFiles"

$NinjaFile = "$ProjectOutFolder/build.ninja"

# Define output folder structures for Debug and Release builds
$NinjaDebugOut = "debug"
$NinjaReleaseOut = "release"
$NinjaDebugBin = "$NinjaDebugOut/bin"
$NinjaReleaseBin = "$NinjaReleaseOut/bin"
$NinjaDebugLib = "$NinjaDebugOut/lib"
$NinjaReleaseLib = "$NinjaReleaseOut/lib"
$NinjaDebugObj = "$NinjaDebugOut/obj"
$NinjaReleaseObj = "$NinjaReleaseOut/obj"
$OutReleaseAsm = "$NinjaReleaseOut/asm"

# ==============================================================================
# SECTION 2: MSVC Toolchain Validation & Flags Definition
# ==============================================================================
# This section verifies we are on Windows and that the MSVC compiler is available.
# It then defines all the compiler and linker flags needed for C++.

# Support old legacy PowerShell and newer PowerShell Core
$RunningOnWindows = $env:OS -match "Windows" -or $IsWindows
if ($RunningOnWindows) {
    
    # Verify MSVC tools are present in the current environment
    # Note: This requires running the script inside a "Developer PowerShell for VS"
    if (!(Get-Command cl.exe -ErrorAction SilentlyContinue)) {
        Write-Host "Error: cl.exe not found. Please run this in a Developer PowerShell." -ForegroundColor Red
        exit 1
    }
    if (!(Get-Command link.exe -ErrorAction SilentlyContinue)) {
        Write-Host "Error: link.exe not found. Please run this in a Developer PowerShell." -ForegroundColor Red
        exit 1
    }
    if (!(Get-Command lib.exe -ErrorAction SilentlyContinue)) {
        Write-Host "Error: lib.exe not found. Please run this in a Developer PowerShell." -ForegroundColor Red
        exit 1
    }
    
    Write-Host "Detected Windows - Using MSVC" -ForegroundColor Cyan

    # Define ninja specific variables for dependency tracking
    # /showIncludes: Causes the compiler to output a list of include files. Ninja uses this for dependency tracking.
    # /c: Compile without linking.
    $NinjaCompilerCommands = "/showIncludes /c"
    $NinjaCommand = "/c"
    $NinjaDeps = "msvc"

    # Define output file names based on the project name
    $ExecutableName = "$ProjectName.exe"
    $LibName = "$ProjectName.lib"
    $DllName = "$ProjectName.dll"

    # --- Compiler Flags (cl.exe) ---
    $Compiler = "cl.exe"
    
    # DEBUG COMPILER FLAGS:
    # /nologo : Suppresses the startup copyright banner.
    # /MDd    : Links against the debug, multithreaded dynamic C runtime library (MSVCPRTD.lib/MSVCRTD.lib).
    # /Z7     : Embeds full C/C++ debug info directly into the .obj files (avoids parallel PDB lock issues).
    # /Ob0    : Disables inline expansion entirely.
    # /Od     : Disables optimization (essential for predictable debugging).
    # /RTC1   : Enables run-time error checks (catches uninitialized variables and stack frame errors).
    # /W4     : Sets warning level to 4 (strict).
    # /EHsc   : Enables standard C++ exception handling (assumes extern "C" functions don't throw).
    # /GR-    : Disables Run-Time Type Information (RTTI) to reduce binary bloat.
    # /fp:fast: Trades strict IEEE floating-point accuracy for performance speed.
    $DebugCompilerFlags = "/nologo /MDd /Z7 /Ob0 /Od /RTC1 /W4 /EHsc /GR- /fp:fast "
    
    # RELEASE COMPILER FLAGS:
    # /nologo : Suppresses the startup copyright banner.
    # /MD     : Links against the release, multithreaded dynamic C runtime library (MSVCPRT.lib/MSVCRT.lib).
    # /Zi     : Generates a separate .pdb file containing debug information (useful for crash dumps).
    # /O2     : Maximizes optimization for speed.
    # /Ob2    : Allows aggressive inline expansion of functions.
    # /Zo     : Enhances debugging information for optimized code (keeps track of variables after O2 optimization).
    # /FAs    : Generates an assembly listing file intertwined with source code (.asm).
    # /DNDEBUG: Defines the NDEBUG macro (disables standard assert() calls).
    # /EHsc   : Enables standard C++ exception handling.
    # /FS     : Forces synchronous PDB writes (crucial for Ninja parallel builds writing to the same PDB).
    # /GR-    : Disables Run-Time Type Information (RTTI) to reduce binary bloat.
    # /fp:fast: Trades strict IEEE floating-point accuracy for performance speed.
    $ReleaseCompilerFlags = "/nologo /MD /Zi /O2 /Ob2 /Zo /FAs /DNDEBUG /EHsc /FS /GR- /fp:fast /Fa$OutReleaseAsm\ "
    
    # INCLUDE DIRECTORIES:
    # /I            : Adds a directory to the standard include search path.
    # /external:I   : Adds a directory to the external include search path (treats headers as 3rd party).
    # /external:W0  : Suppresses all warnings (Warning Level 0) from external headers.
    $DebugCompilerFlags += "/I$NinjaInclude /external:I$NinjaIncludeVendor /external:W0 "
    $ReleaseCompilerFlags += "/I$NinjaInclude /external:I$NinjaIncludeVendor /external:W0 "
    
    # OUTPUT FLAG:
    # /Fo: Specifies the exact name/path for the compiled output object file.
    $CompilerObjectOut = "/Fo"

    # --- Linker Flags (link.exe) ---
    $Linker = "link.exe"
    
    # GENERAL LINKER FLAGS (Shared between Debug and Release for symbol preservation):
    # /NOLOGO       : Suppresses the startup copyright banner.
    # /DEBUG:FULL   : Generates complete debugging information in the PDB.
    # /OPT:REF      : Eliminates unreferenced functions and data (removes dead code).
    # /OPT:ICF      : Performs Identical COMDAT Folding (merges identical functions/read-only data to save space).
    # /INCREMENTAL:NO : Forces a full link every time (slower, but safer for CI and Ninja).
    # /PDBALTPATH   : Embeds the exact PDB path inside the final binary for easier debugger resolution (And to hide the exposed user path)
    $DebugLinkerFlags = "/NOLOGO /DEBUG:FULL /OPT:REF /OPT:ICF /INCREMENTAL:NO /PDBALTPATH:%_PDB%"
    $ReleaseLinkerFlags = "/NOLOGO /DEBUG:FULL /OPT:REF /OPT:ICF /INCREMENTAL:NO /PDBALTPATH:%_PDB%"
    
    # /OUT: Specifies the exact name/path for the final linked binary (.exe or .dll).
    $LinkerObjectOut = "/OUT:"

    # Adjust flags if we are specifically building a Dynamic-Link Library (DLL)
    if ($ProjectType -eq "dll"){
        # /LD       : Tells the compiler to create a DLL.
        # /D_USRDLL : Standard preprocessor macro indicating a user DLL is being built.
        # /D_WINDLL : Standard preprocessor macro indicating a Windows DLL.
        $DebugCompilerFlags = "$DebugCompilerFlags /LD /D_USRDLL /D_WINDLL"
        $ReleaseCompilerFlags = "$ReleaseCompilerFlags /LD /D_USRDLL /D_WINDLL"
        
        # /DLL      : Instructs the linker to build a DLL rather than an executable.
        # /OPT:NOICF: Disables Identical COMDAT Folding (often required for DLLs to avoid merging exported functions).
        $DebugLinkerFlags = "$DebugLinkerFlags /DLL /OPT:NOICF"
        $ReleaseLinkerFlags = "$ReleaseLinkerFlags /DLL /OPT:NOICF"
    }

    # --- Librarian Flags (lib.exe) for Static Libs ---
    $Archiver = "lib.exe"
    # /NOLOGO : Suppresses the startup copyright banner.
    $ArchiverFlags = "/NOLOGO"
    # /OUT:   : Specifies the exact name/path for the output static library (.lib).
    $ArchiverObjectOut = "/OUT:"
}
else {
     # Failsafe for unsupported operating systems
     Write-Host "--- Cannot run on Non-Windows platform ---" -ForegroundColor Red
     exit 1
}


# ==============================================================================
# SECTION 3: Workspace Preparation & Ninja Rule Definitions
# ==============================================================================
# This creates the necessary output folders and defines the raw Ninja instructions.

# Clean up any existing build.ninja file before regenerating it
Remove-Item -Path $NinjaFile -Force -ErrorAction SilentlyContinue

# Ensure the output directory exists
if (!(Test-Path $ProjectOutFolder)) {
    #Create necessary folders here as the rest will be created by ninja
    New-Item -ItemType Directory -Path "$ProjectOutFolder/$OutReleaseAsm"
}

Write-Host "--- Starting Build ---" -ForegroundColor DarkGreen

# Ninja natively handles Windows Command Line limits using response files (.rsp).
# This Here-String contains the core rules Ninja uses to compile, link, and archive.
$NinjaContent = @"
rule $("$ProjectName")_debug_cc
    command = $($Compiler) $($NinjaCompilerCommands) $($CompilerObjectOut)`$out $($DebugCompilerFlags) `$in 
    description = Compiling `$out 
    deps = $($NinjaDeps)

rule $("$ProjectName")_release_cc
    command = $($Compiler) $($NinjaCompilerCommands) $($CompilerObjectOut)`$out $($ReleaseCompilerFlags) `$in 
    description = Compiling `$out 
    deps = $($NinjaDeps)

rule $("$ProjectName")_debug_lk
    command = $($Linker) $($LinkerObjectOut)`$out $($DebugLinkerFlags) @`$out.rsp 
    description = Linking `$out
    rspfile = `$out.rsp
    rspfile_content = `$in

rule $("$ProjectName")_release_lk
    command = $($Linker) $($LinkerObjectOut)`$out $($ReleaseLinkerFlags) @`$out.rsp 
    description = Linking `$out
    rspfile = `$out.rsp
    rspfile_content = `$in

rule $("$ProjectName")_lib
    command = $($Archiver) $($ArchiverObjectOut)`$out $($ArchiverFlags) `$in 

rule $("$ProjectName")_debug_dll
    command = $($Linker) $($LinkerObjectOut)`$out $($DebugLinkerFlags) @`$out.rsp 
    description = Linking `$out
    rspfile = `$out.rsp
    rspfile_content = `$in 

rule $("$ProjectName")_release_dll
    command = $($Linker) $($LinkerObjectOut)`$out $($ReleaseLinkerFlags) @`$out.rsp 
    description = Linking `$out
    rspfile = `$out.rsp
    rspfile_content = `$in 
"@


# ==============================================================================
# SECTION 4: File Parsing and String Optimization
# ==============================================================================
# This section dynamically finds source/library files and formats them into Ninja targets.
# It uses .NET collections for performance optimizations over standard PowerShell arrays.
# --- Source Files ---
# Gather all source files (stripping the extensions to just get the BaseName)
$ListOfSourceFiles = (Get-ChildItem -Path "$ProjectFolder/$ProjectSrcFiles" -File).BaseName

# PERFORMANCE OPTIMIZATION: Use .NET generic lists instead of PowerShell arrays (+=).
# PowerShell rebuilds arrays in memory on every addition, which is very slow. 
# Pre-allocating capacity here makes file parsing significantly faster.
$DebugObjs = [System.Collections.Generic.List[string]]::new($ListOfSourceFiles.Count)
$ReleaseObjs = [System.Collections.Generic.List[string]]::new($ListOfSourceFiles.Count)

# PERFORMANCE OPTIMIZATION: Assigning a loop directly to a variable catches all 
# implicit outputs instantly without needing to manually append them one by one.
$ObjectRules = foreach ($File in $ListOfSourceFiles) {
    $DebugObjs.Add("$($NinjaDebugObj)/$($File).obj")
    $ReleaseObjs.Add("$($NinjaReleaseObj)/$($File).obj")
    
    # Generating the Ninja build statements for each specific object file
    "`nbuild $($NinjaDebugObj)/$($File).obj: $("$ProjectName")_debug_cc $($NinjaSource)/$($File).cpp`n" +
    "`nbuild $($NinjaReleaseObj)/$($File).obj: $("$ProjectName")_release_cc $($NinjaSource)/$($File).cpp" 
}

# Collapse the lists into space-separated strings required by Ninja
$ListOfDebugObj = $DebugObjs -join " "
$ListOfReleaseObj = $ReleaseObjs -join " "

# Append the compiled rules string exactly once to avoid multiple memory allocations
$NinjaContent += "`n" + ($ObjectRules -join "") + "`n"

# --- Library Files ---
# Gather all pre-compiled vendor libraries
$ListOfDebugLibFiles = (Get-ChildItem -Path "$ProjectFolder/$ProjectVendorLibDebugFiles" -File)
$ListOfReleaseLibFiles = (Get-ChildItem -Path "$ProjectFolder/$ProjectVendorLibReleaseFiles" -File)

# Initialize optimized lists for libraries
$DebugLibs = [System.Collections.Generic.List[string]]::new($ListOfDebugLibFiles.Count)
$ReleaseLibs = [System.Collections.Generic.List[string]]::new($ListOfReleaseLibFiles.Count)

foreach ($File in $ListOfDebugLibFiles) {
    $DebugLibs.Add("$($NinjaDebugLibVendor)/$($File)") 
}
foreach ($File in $ListOfReleaseLibFiles) {
    $ReleaseLibs.Add("$($NinjaReleaseLibVendor)/$($File)") 
}

# Collapse library lists into strings
$ListOfDebugLibs = $DebugLibs -join " "
$ListOfReleaseLibs = $ReleaseLibs -join " "


# ==============================================================================
# SECTION 5: Target Build Definitions
# ==============================================================================
# Tells Ninja exactly what the final output should be (Exe, Lib, or Dll) 
# by combining the object files and library files collected above.

if ($ProjectType -eq "exe") {
    $NinjaContent += "`nbuild $($NinjaDebugBin)/$($ExecutableName): $("$ProjectName")_debug_lk $ListOfDebugObj $ListOfDebugLibs"
    $NinjaContent += "`nbuild $($NinjaReleaseBin)/$($ExecutableName): $("$ProjectName")_release_lk $ListOfReleaseObj $ListOfReleaseLibs"
}
elseif ($ProjectType -eq "lib") {
    $NinjaContent += "`nbuild $($NinjaDebugLib)/$($LibName): $("$ProjectName")_lib $ListOfDebugObj $ListOfDebugLibs"
    $NinjaContent += "`nbuild $($NinjaReleaseLib)/$($LibName): $("$ProjectName")_lib $ListOfReleaseObj $ListOfReleaseLibs"
}
elseif ($ProjectType -eq "dll") {
    # Note the | syntax here: it creates an implicit dependency between the DLL and the static import library
    $NinjaContent += "`nbuild $($NinjaDebugLib)/$($DllName) | $($NinjaDebugLib)/$($LibName): $("$ProjectName")_debug_dll $ListOfDebugObj $ListOfDebugLibs"
    $NinjaContent += "`nbuild $($NinjaReleaseLib)/$($DllName) | $($NinjaReleaseLib)/$($LibName): $("$ProjectName")_release_dll $ListOfReleaseObj $ListOfReleaseLibs"
}    


# ==============================================================================
# SECTION 6: File Generation and Execution
# ==============================================================================
# Saves the build file to disk and triggers the Ninja build process.

# Output the generated string into build.ninja using ASCII encoding for maximum compatibility
$NinjaContent | Out-File -FilePath $NinjaFile -Encoding ascii

# Execute Ninja
# -C changes the working directory to where our build.ninja file is located.
# @args forwards any additional flags passed into this script (e.g., `-t clean`) directly to Ninja.
ninja -C "$CurrentDirectory/$ProjectOutFolder" @args

# Post-Build Checks
# $LASTEXITCODE is a special variable containing the exit code of the last run native app (ninja)
# 0 indicates success.
if ($LASTEXITCODE -eq 0) {
    Write-Host "--- Build Complete! ---" -ForegroundColor DarkGreen

    # The last step should be moving dlls where executable lives so we can easily run it from there
    # and to allow others scripts to pickup everything they need from the bin folder
    $ListOfDebugDllFiles = (Get-ChildItem -Path "$ProjectFolder/$ProjectVendorDllDebugFiles" -File)
    $ListOfReleaseDllFiles = (Get-ChildItem -Path "$ProjectFolder/$ProjectVendorDllReleaseFiles" -File)

    if ($ListOfDebugDllFiles.Count -gt 0) {
        Copy-Item -Path "$ProjectFolder/$ProjectVendorDllDebugFiles/*" -Destination "$ProjectOutFolder/$NinjaDebugBin" -Force
    }
    if ($ListOfReleaseDllFiles.Count -gt 0) {
        Copy-Item -Path "$ProjectFolder/$ProjectVendorDllReleaseFiles/*" -Destination "$ProjectOutFolder/$NinjaReleaseBin" -Force
    }
}
else {
    Write-Host "--- Build Incomplete! ---" -ForegroundColor Red
    exit 1
}