[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
}