Build System and Scripts - Garume/Manifold GitHub Wiki
Manifold uses a PowerShell-based build system composed of discrete scripts in the build/ directory. Each script handles a single concern — restore, build, test, format, pack, benchmark, or architecture validation — and can be invoked independently or orchestrated through the quality.ps1 meta-script. The MSBuild layer is configured centrally via Directory.Build.props and Directory.Build.targets, enforcing consistent compiler settings, code analysis, and NuGet packaging across all projects. This page covers every script, the MSBuild configuration, artifact output layout, and how these pieces connect to the CI/CD and Release Pipeline.
For the overall repository layout and project listing, see Project Structure and Tech Stack. For architecture validation details, see Architecture Validation and Code Quality.
The following diagram illustrates how the build scripts relate to one another and the order in which quality.ps1 invokes them.
flowchart TD
Q["quality.ps1"]
R["restore.ps1"]
B["build.ps1"]
F["format.ps1"]
T["test.ps1"]
A["architecture.ps1"]
P["pack.ps1"]
BM["benchmark.ps1"]
Q --> R
Q --> B
Q --> F
Q --> T
Q --> A
R -->|"dotnet restore"| SOL["Manifold.slnx"]
B -->|"dotnet build"| SRC["src/ & samples/"]
F -->|"dotnet format"| SOL
T -->|"dotnet test"| TST["tests/"]
A -->|"Validate structure"| REPO["Repository root"]
P -->|"dotnet pack"| PKG[".artifacts/packages/"]
BM -->|"dotnet run -c Release"| BENCH["benchmarks/"]
Sources: build/quality.ps1:1-35, build/restore.ps1:1-22, build/build.ps1:1-41
All build scripts source Get-DotNetCommand.ps1 to resolve the .NET CLI executable. The function first checks for a local SDK installation at .dotnet/dotnet.exe within the repository root, then falls back to the system-wide dotnet command.
function Get-DotNetCommand {
param([string]$RepositoryRoot)
$localDotNet = Join-Path $RepositoryRoot '.dotnet\dotnet.exe'
if (Test-Path $localDotNet) {
return $localDotNet
}
$command = Get-Command dotnet -ErrorAction Stop
return $command.Source
}Sources: build/Get-DotNetCommand.ps1:1-16
Restores NuGet packages for the entire solution.
| Parameter | Type | Default | Description |
|---|---|---|---|
-Solution |
string | Manifold.slnx |
Solution file to restore |
Runs dotnet restore against the specified solution and exits with the .NET CLI exit code on failure.
Sources: build/restore.ps1:1-22
Builds source and sample projects extracted from the solution file. The script parses the .slnx XML to select only projects under src/ and samples/, excluding test and benchmark projects.
| Parameter | Type | Default | Description |
|---|---|---|---|
-Solution |
string | Manifold.slnx |
Solution file to parse |
-NoRestore |
switch | $true |
Skip NuGet restore |
$buildProjects =
$solutionDocument.SelectNodes('//Project[@Path]') |
ForEach-Object { $_.Path } |
Where-Object { $_ -match '^((src|samples)[\\/].+\.csproj)$' }Each matched project is built individually with dotnet build. If no matching projects are found, the script falls back to building the entire solution.
Sources: build/build.ps1:1-41, Manifold.slnx:1-22
Discovers and runs all test projects under the tests/ directory.
| Parameter | Type | Default | Description |
|---|---|---|---|
-Solution |
string | Manifold.slnx |
Solution file to parse |
-NoBuild |
switch | $false |
Skip build before testing |
When building is enabled (the default), the script creates a unique output directory under .artifacts/test-output/<guid>/ and directs each test project's output there via the OutDir MSBuild property. This isolates test assemblies from one another and avoids file-locking conflicts.
flowchart TD
TP["test.ps1"]
SD["Parse Manifold.slnx"]
FP["Filter tests/ projects"]
OD["Create .artifacts/test-output/GUID/"]
R1["dotnet test Manifold.Tests"]
R2["dotnet test Manifold.Cli.Tests"]
R3["dotnet test Manifold.Generators.Tests"]
R4["dotnet test Manifold.Mcp.Tests"]
R5["dotnet test Manifold.Samples.Tests"]
TP --> SD --> FP --> OD
OD --> R1
OD --> R2
OD --> R3
OD --> R4
OD --> R5
The five test projects discovered from the solution file are:
| Test Project | Target |
|---|---|
Manifold.Tests |
Core contracts |
Manifold.Cli.Tests |
CLI runtime |
Manifold.Generators.Tests |
Source generator |
Manifold.Mcp.Tests |
MCP runtime |
Manifold.Samples.Tests |
Sample integration |
For more on the testing strategy, see Testing Strategy.
Sources: build/test.ps1:1-50, Manifold.slnx:8-14
Enforces code formatting via dotnet format.
| Parameter | Type | Default | Description |
|---|---|---|---|
-Solution |
string | Manifold.slnx |
Solution file to format |
-Fix |
switch | $false |
Apply fixes instead of verifying |
-NoRestore |
switch | $false |
Skip NuGet restore |
By default, the script runs in verification mode (--verify-no-changes), causing CI to fail if any formatting violations are detected. Pass -Fix to auto-correct violations locally.
Sources: build/format.ps1:1-33
The orchestration script that runs the full quality gate. It executes scripts in a strict sequence, stopping on the first failure.
| Parameter | Type | Default | Description |
|---|---|---|---|
-Solution |
string | Manifold.slnx |
Solution file |
-SkipRestore |
switch | $false |
Skip the restore step |
Execution order:
-
restore.ps1— Restore NuGet packages -
build.ps1 -NoRestore— Build src/ and samples/ -
format.ps1 -NoRestore— Verify code formatting -
test.ps1— Run all test projects -
architecture.ps1— Validate repository structure
sequenceDiagram
participant Q as quality.ps1
participant R as restore.ps1
participant B as build.ps1
participant F as format.ps1
participant T as test.ps1
participant A as architecture.ps1
Q->>R: Restore packages
R-->>Q: Exit code
Q->>B: Build (--no-restore)
B-->>Q: Exit code
Q->>F: Format (--no-restore)
F-->>Q: Exit code
Q->>T: Run tests
T-->>Q: Exit code
Q->>A: Validate architecture
A-->>Q: Exit code
Sources: build/quality.ps1:1-35
Creates NuGet packages for all source projects.
| Parameter | Type | Default | Description |
|---|---|---|---|
-Solution |
string | Manifold.slnx |
Solution file to parse |
-NoRestore |
switch | $false |
Skip NuGet restore |
-NoBuild |
switch | $false |
Skip build before packing |
-PackageVersion |
string | (empty) | Override the package version |
The script filters projects matching src/*.csproj from the solution, then runs dotnet pack in Release configuration. Packages are written to .artifacts/packages/.
$arguments = @('pack', $packProject, '-c', 'Release',
"-p:PackageOutputPath=$packageOutput")
if (-not [string]::IsNullOrWhiteSpace($PackageVersion)) {
$arguments += "-p:PackageVersion=$PackageVersion"
}The four NuGet packages produced are:
| Package | Description |
|---|---|
Manifold |
Core contracts and descriptors |
Manifold.Cli |
CLI binding and fast invocation runtime |
Manifold.Mcp |
MCP binding and fast invocation runtime |
Manifold.Generators |
Source generator for operation registration |
Sources: build/pack.ps1:1-56, src/Manifold/Manifold.csproj:1-19
Runs BenchmarkDotNet benchmarks for CLI and/or MCP performance testing.
| Parameter | Type | Default | Description |
|---|---|---|---|
-Target |
all, cli, mcp
|
all |
Which benchmark suite to run |
-NoRestore |
switch | $false |
Skip NuGet restore |
-BenchmarkArguments |
string[] | @('--filter', '*') |
Arguments forwarded to BenchmarkDotNet |
Each benchmark project is run via dotnet run -c Release with output isolated to .artifacts/benchmark-output/<guid>/<project>/. The -- separator passes remaining arguments to BenchmarkDotNet.
| Target | Project |
|---|---|
cli |
benchmarks/Manifold.Benchmarks/Manifold.Benchmarks.csproj |
mcp |
benchmarks/Manifold.Mcp.Benchmarks/Manifold.Mcp.Benchmarks.csproj |
all |
Both of the above |
For benchmark results and analysis, see Performance and Benchmarks.
Sources: build/benchmark.ps1:1-60
Validates repository structural invariants. This script performs two categories of checks:
Required files — Verifies that essential repository files exist:
| Required File | Purpose |
|---|---|
Manifold.slnx |
Solution file |
README.md |
Repository documentation |
LICENSE |
License file |
build/pack.ps1 |
Package build script |
.github/workflows/ci.yml |
CI workflow |
Forbidden references — Scans all .cs, .csproj, .props, and .targets files under src/, tests/, and samples/ for references to legacy namespaces:
DalamudMCP.PluginDalamudMCP.ProtocolDalamudMCP.CliDalamudMCP.Framework
If any violations are found, the script emits errors and exits with code 1.
For more details on architecture validation, see Architecture Validation and Code Quality.
Sources: build/architecture.ps1:1-51
The root Directory.Build.props file applies to every project in the repository. It establishes a consistent compilation baseline:
| Property | Value | Purpose |
|---|---|---|
TargetFramework |
net10.0 |
.NET 10.0 target |
LangVersion |
latest |
Latest C# language version |
ImplicitUsings |
enable |
Auto-import common namespaces |
Nullable |
enable |
Nullable reference types |
TreatWarningsAsErrors |
true |
Fail on any warning |
WarningsAsErrors |
nullable |
Explicit nullable warnings as errors |
EnforceCodeStyleInBuild |
true |
Code style rules during build |
AnalysisLevel |
latest-recommended |
Latest analyzer rules |
EnableNETAnalyzers |
true |
Enable .NET code analyzers |
Deterministic |
true |
Reproducible builds |
GenerateDocumentationFile |
true |
Emit XML doc files |
NoWarn |
1591 |
Suppress missing XML doc warnings |
RestorePackagesWithLockFile |
true |
Use packages.lock.json |
Projects that set IsManifoldTestProject=true receive additional configuration:
<PropertyGroup Condition="'$(IsManifoldTestProject)' == 'true'">
<OutputType>Exe</OutputType>
<UseMicrosoftTestingPlatformRunner>true</UseMicrosoftTestingPlatformRunner>
<TestingPlatformDotnetTestSupport>true</TestingPlatformDotnetTestSupport>
</PropertyGroup>Test projects also automatically receive coverlet.MTP (version 8.0.0) for code coverage and xunit.analyzers (version 1.27.0) for test quality checks.
Sources: Directory.Build.props:1-34
The targets file handles two concerns:
-
Package content — For packable projects (
IsPackable=true), it includes the rootREADME.mdandLICENSEfiles in the NuGet package:
<ItemGroup Condition="'$(IsPackable)' == 'true'">
<None Include="$(MSBuildThisFileDirectory)README.md"
Pack="true" PackagePath="" Visible="false" />
<None Include="$(MSBuildThisFileDirectory)LICENSE"
Pack="true" PackagePath="" Visible="false" />
</ItemGroup>-
Coverage threshold enforcement — For test projects, a custom target runs before the
Testtarget and fails the build ifCoverageThresholdis not set:
<Target Name="FailIfCoverageThresholdMissing"
BeforeTargets="Test"
Condition="'$(IsManifoldTestProject)' == 'true' and '$(CoverageThreshold)' == ''">
<Error Text="CoverageThreshold must be set for all test projects." />
</Target>Sources: Directory.Build.targets:1-18
Each source project under src/ defines its own NuGet metadata. All four packages share a common metadata pattern:
| Property | Value |
|---|---|
| Authors | Garume |
| License | MIT |
| Repository | https://github.com/Garume/Manifold |
| IncludeSymbols |
true (except Manifold.Generators) |
| SymbolPackageFormat | snupkg |
flowchart TD
SLN["Manifold.slnx"]
PS["pack.ps1"]
ART[".artifacts/packages/"]
M["Manifold.csproj"]
MC["Manifold.Cli.csproj"]
MM["Manifold.Mcp.csproj"]
MG["Manifold.Generators.csproj"]
NM["Manifold.nupkg"]
NMC["Manifold.Cli.nupkg"]
NMM["Manifold.Mcp.nupkg"]
NMG["Manifold.Generators.nupkg"]
SLN --> PS
PS --> M --> NM
PS --> MC --> NMC
PS --> MM --> NMM
PS --> MG --> NMG
NM --> ART
NMC --> ART
NMM --> ART
NMG --> ART
The Manifold.Generators package is a special case: it targets netstandard2.0 (as required for Roslyn analyzers/generators), sets IncludeBuildOutput=false, and does not produce symbol packages.
Sources: src/Manifold/Manifold.csproj:1-19
Build scripts write outputs to the .artifacts/ directory, organized by purpose:
.artifacts/
├── packages/ # NuGet .nupkg and .snupkg files
│ ├── Manifold.x.y.z.nupkg
│ ├── Manifold.Cli.x.y.z.nupkg
│ ├── Manifold.Mcp.x.y.z.nupkg
│ └── Manifold.Generators.x.y.z.nupkg
├── test-output/ # Test run outputs
│ └── <guid>/
│ ├── Manifold.Tests/
│ ├── Manifold.Cli.Tests/
│ ├── Manifold.Generators.Tests/
│ ├── Manifold.Mcp.Tests/
│ └── Manifold.Samples.Tests/
└── benchmark-output/ # Benchmark run outputs
└── <guid>/
├── Manifold.Benchmarks/
└── Manifold.Mcp.Benchmarks/
Each test and benchmark run generates a new GUID-based subdirectory, ensuring isolation between runs without requiring cleanup of previous results.
Sources: build/test.ps1:23-27, build/benchmark.ps1:31-33, build/pack.ps1:24-25
The build scripts are invoked by two GitHub Actions workflows:
Triggered on every push and pull request. Runs the full quality gate followed by package creation:
- name: Quality
shell: pwsh
run: ./build/quality.ps1
- name: Pack
shell: pwsh
run: ./build/pack.ps1 -NoRestoreTriggered on version tags (v*) or manual dispatch. Runs quality checks, packs with a version override, pushes to NuGet.org, and creates a GitHub Release:
- name: Pack
shell: pwsh
run: ./build/pack.ps1 -NoRestore -PackageVersion $env:PACKAGE_VERSIONflowchart TD
TAG["Git tag v*"]
VER["Resolve version"]
QG["quality.ps1"]
PK["pack.ps1 -PackageVersion"]
NL["NuGet login"]
NP["dotnet nuget push"]
GR["gh release create"]
TAG --> VER --> QG --> PK --> NL --> NP --> GR
For complete CI/CD documentation, see CI/CD and Release Pipeline.
Sources: .github/workflows/ci.yml:1-24, .github/workflows/publish.yml:1-101
- Architecture Overview — High-level system architecture and package relationships
- Project Structure and Tech Stack — Directory layout and project listing
- Architecture Validation and Code Quality — Detailed architecture validation and code style enforcement
- CI/CD and Release Pipeline — GitHub Actions workflows and release process
- Testing Strategy — Test projects, coverage thresholds, and test patterns
- Performance and Benchmarks — Benchmark infrastructure and results
- Source Generator — Manifold.Generators — Source generator package details