Build System and Scripts - Garume/Manifold GitHub Wiki

Build System and Scripts

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.

Build Pipeline Overview

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/"]
Loading

Sources: build/quality.ps1:1-35, build/restore.ps1:1-22, build/build.ps1:1-41

Shared Helper: Get-DotNetCommand

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

Script Reference

restore.ps1

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

build.ps1

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

test.ps1

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
Loading

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

format.ps1

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

quality.ps1

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:

  1. restore.ps1 — Restore NuGet packages
  2. build.ps1 -NoRestore — Build src/ and samples/
  3. format.ps1 -NoRestore — Verify code formatting
  4. test.ps1 — Run all test projects
  5. 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
Loading

Sources: build/quality.ps1:1-35

pack.ps1

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

benchmark.ps1

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

architecture.ps1

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.Plugin
  • DalamudMCP.Protocol
  • DalamudMCP.Cli
  • DalamudMCP.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

MSBuild Configuration

Directory.Build.props

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

Test Project Configuration

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

Directory.Build.targets

The targets file handles two concerns:

  1. Package content — For packable projects (IsPackable=true), it includes the root README.md and LICENSE files 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>
  1. Coverage threshold enforcement — For test projects, a custom target runs before the Test target and fails the build if CoverageThreshold is 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

NuGet Package Configuration

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
Loading

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

Artifact Output Structure

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

CI Integration

The build scripts are invoked by two GitHub Actions workflows:

ci.yml (Continuous Integration)

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 -NoRestore

publish.yml (Release Publishing)

Triggered 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_VERSION
flowchart 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
Loading

For complete CI/CD documentation, see CI/CD and Release Pipeline.

Sources: .github/workflows/ci.yml:1-24, .github/workflows/publish.yml:1-101

Related Pages

⚠️ **GitHub.com Fallback** ⚠️