Table of Contents

Build Pipeline

Overview

DBTools uses a sophisticated build pipeline designed for:

  • Multi-year Revit support (2024, 2025, 2026)
  • Multi-framework targeting (net48 for Revit 2024, net8.0-windows for 2025+)
  • WSL/Windows interoperability (development in WSL, builds via Windows dotnet.exe)
  • Assembly merging (ILRepack for net48, AssemblyLoadContext for net8)
  • Centralized artifacts (all outputs to .artifacts/, no per-project bin/obj)

Source: build.sh:6, build/Build.cs:1

Quick Reference

# Build vendored UI deps (HandyControl -> DBTools.HandyControl)
bash build.sh vendor handycontrol

# Build everything
bash build.sh BuildAll

# Clean + full rebuild
bash build.sh BuildAll --clean

# Build specific Revit year
bash build.sh BuildAll -y 2025

# Debug configuration
bash build.sh BuildAll --debug

# Build only the app (fast local iteration)
bash build.sh BuildApp -y 2026

# Build tests
bash build.sh BuildTests -y 2026

# Help
bash build.sh --help

Source: build.sh:1

CI (GitHub Actions)

CI is opt-in:

  • For push events: include [ci] or [build] in the commit message to run build jobs.
  • For pull_request events: include [ci] or [build] in the PR title or body.
  • Use [ci:test] / [build-test] to run build + tests.
  • Use [skip ci] / [ci skip] to force skip.

Source: .github/workflows/ci.yml:1

Architecture

Build System Stack

┌─────────────────────────────────────────────────────────────┐
│                      build.sh                                │
│        (Thin wrapper - WSL/Git Bash compatible)             │
├─────────────────────────────────────────────────────────────┤
│                  Windows dotnet.exe                          │
│              (via wslpath/cygpath conversion)                │
├─────────────────────────────────────────────────────────────┤
│                    NUKE Build                                │
│          (Build.cs, BuildTargets.cs, etc.)                   │
├─────────────────────────────────────────────────────────────┤
│                  MSBuild + .NET SDK                          │
│      (Directory.Build.props/targets, *.csproj)              │
└─────────────────────────────────────────────────────────────┘

Key Files

File Purpose
build.sh Entry point - thin wrapper invoking NUKE via Windows dotnet.exe
build/Build.cs NUKE orchestration, parameters, cleanup
build/BuildTargets.cs Build targets, Revit path resolution
build/BuildMerging.cs ILRepack assembly merging (net48)
build/BuildHelpers.cs Path conversion utilities
build/ArtifactManagement.cs Metadata, fingerprinting, promotion
Directory.Build.props Centralized project properties
Directory.Build.targets Build targets, NUKE enforcement
Version.props Product version
build/Revit.props Revit-specific TFM definitions

Source: build/Build.cs:17-24

Entry Point: build.sh

The build script (build.sh) is the required entry point for all builds. Direct dotnet build is blocked by MSBuild.

WSL Detection

The script auto-detects the environment and configures appropriate path conversion:

if [[ -f /proc/version ]] && grep -qi microsoft /proc/version 2>/dev/null; then
    # WSL environment
    WIN_DOTNET_EXE="/mnt/c/Program Files/dotnet/dotnet.exe"
    to_win_path() { wslpath -w "$1"; }
else
    # Git Bash or other
    WIN_DOTNET_EXE="/c/Program Files/dotnet/dotnet.exe"
    to_win_path() { cygpath -w "$1"; }
fi

Source: build.sh:6

Vendored Dependencies

Vendored UI assemblies (HandyControl → DBTools.HandyControl) are built via a dedicated script:

bash build-vendored-deps.sh

NUKE Build System

Parameters

Parameter Default Description
--Configuration Release Build configuration
--Years 2024 2025 2026 Revit years to target
--SkipClean true Skip clean for fast incremental builds
--DryRun false Simulate actions without executing
--ValidateManifests true Validate embedded manifests
--ValidateTools true Validate tool UIs

Source: build/Build.cs:36-51

Key Targets

flowchart LR
    subgraph Preflight["Preflight"]
        A[PreflightRevitInstalls]
    end
    
    subgraph Build["Build Phase"]
        B[BuildCore]
        C[BuildApp]
        D[VendoredStageUiToYearOutputs]
        E[BuildTests]
    end
    
    subgraph Package["Package Phase"]
        F[FlattenYearOutputs]
        G[PromoteToDist]
        H[WriteMetadata]
        I[ValidateDist]
    end
    
    A --> B --> C --> D --> E --> F --> G --> H --> I
    
    style Preflight fill:#fff3e0
    style Build fill:#e3f2fd
    style Package fill:#e8f5e9
BuildAll
├── PreflightRevitInstalls
├── BuildCore
├── BuildApp
├── VendoredStageUiToYearOutputs
├── BuildTests
├── FlattenYearOutputs
├── PromoteToDist
├── WriteMetadata
└── ValidateDist
Target Description
BuildAll Full build with validation
BuildOnly Build without validation (CI use)
BuildApp Build DBTools.App only
BuildCore Build DBTools.Core only
BuildTests Build all test projects
Clean Delete staging/intermediate outputs
PromoteToDist Copy validated outputs to dist/

Source: build/Build.cs:165-177

Revit Path Resolution

NUKE auto-detects Revit installations using ricaun.Nuke:

var installs = RevitInstallationUtils.InstalledRevit ?? Array.Empty<RevitInstallation>();
foreach (var inst in installs)
    Serilog.Log.Information("  - Version={Version}, Location={Location}", 
        inst.Version, inst.InstallLocation);

Override with parameters if needed:

  • --Revit2024Dir
  • --Revit2025Dir
  • --Revit2026Dir

Source: build/BuildTargets.cs:35-78

MSBuild Configuration

Directory.Build.props

The centralized props file establishes:

  1. Artifact paths - All outputs to .artifacts/
  2. Year-based organization - bin/{Config}/{Year}/{TFM}/
  3. Design-time support - XAML designer compatibility
  4. Code analysis - Warnings as errors, nullable enabled

Centralized Artifacts

<PropertyGroup>
  <BaseArtifactsDir>$(MSBuildThisFileDirectory).artifacts\</BaseArtifactsDir>
  <BaseIntermediateOutputPath>$(BaseArtifactsDir)obj\</BaseIntermediateOutputPath>
  <BaseOutputPath>$(BaseArtifactsDir)bin\</BaseOutputPath>
</PropertyGroup>

Source: Directory.Build.props:12-14

Year-Based Output

<PropertyGroup Condition="'$(DesignTimeBuild)'!='true'">
  <DBT_OutputYear Condition="'$(RevitYear)'!=''">$(RevitYear)</DBT_OutputYear>
  <DBT_OutputYear Condition="'$(RevitYear)'==''">shared</DBT_OutputYear>
  <OutputPath>$(BaseOutputPath)$(Configuration)\$(DBT_OutputYear)\</OutputPath>
</PropertyGroup>

This produces:

.artifacts/
├── bin/
│   ├── Release/
│   │   ├── 2024/
│   │   │   └── net48/
│   │   ├── 2025/
│   │   │   └── net8.0-windows/
│   │   └── 2026/
│   │       └── net8.0-windows/
│   └── Debug/
│       └── ...
└── obj/
    └── ...

Source: Directory.Build.props:105-121

Code Quality Settings

<PropertyGroup>
  <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
  <LangVersion>latest</LangVersion>
  <Nullable>enable</Nullable>
  <EnableNETAnalyzers>true</EnableNETAnalyzers>
  <AnalysisLevel>latest-recommended</AnalysisLevel>
</PropertyGroup>

Source: Directory.Build.props:142-150

Directory.Build.targets

NUKE Entry Point Enforcement

Direct dotnet build from CLI is blocked to ensure consistent builds:

<Target Name="DBT_EnforceEntryPoint"
        BeforeTargets="CoreCompile"
        Condition="'$(DesignTimeBuild)'!='true' and '$(DBT_Entry)'!='true'">
  <Error Code="DBT0001"
         Text="Direct builds are blocked. Use ./build.sh" />
</Target>

Only builds via build.sh (which sets DBT_Entry=true) are allowed.

Source: Directory.Build.targets:32-37

WPF Designer Fixes

The targets file includes several fixes for WPF/XAML designer compatibility:

  1. Platform target override for designer (AnyCPU instead of x64)
  2. Intermediate path fixes for wpftmp projects
  3. Reference resolution before XAML compilation
  4. Duplicate file removal for generated code

Source: Directory.Build.targets:8-63

Multi-Year Targeting

Target Framework Matrix

Revit Year .NET Version TFM
2024 .NET Framework 4.8 net48
2025 .NET 8 net8.0-windows
2026 .NET 8 net8.0-windows

Revit.props

Defines canonical TFMs for Revit-facing projects:

<PropertyGroup Label="Revit TFMs">
  <DBT_RevitLegacyTFM>net48</DBT_RevitLegacyTFM>
  <DBT_RevitModernTFM>net8.0-windows</DBT_RevitModernTFM>
  <DBT_RevitTargetFrameworks>$(DBT_RevitLegacyTFM);$(DBT_RevitModernTFM)</DBT_RevitTargetFrameworks>
</PropertyGroup>

Source: build/Revit.props:2-6

Conditional Compilation

Projects use conditional compilation for version-specific code:

#if REVIT2024
    // net48-specific code
#elif REVIT2025 || REVIT2026
    // net8-specific code
#endif

Assembly Merging

ILRepack (net48 / Revit 2024)

For Revit 2024 (net48), dependencies are merged into DBTools.dll using ILRepack:

// Merge ALL DLLs except whitelist:
var keepSeparate = new HashSet<string> {
    "DBTools.dll",           // Primary assembly
    "DBTools.Loader.dll",    // Entry point
    "DBTools.HandyControl.dll",
    "DBTools.Themes.dll",
    "Microsoft.Extensions.Logging.Abstractions.dll",
    // Revit API (shouldn't be in output)
    "RevitAPI.dll",
    "RevitAPIUI.dll",
    "AdWindows.dll",
    "UIFramework.dll",
};

Why merge?

  • Revit add-ins share the AppDomain
  • Without merging, different add-ins with same dependency versions can conflict
  • ILRepack internalizes types to prevent conflicts

What stays separate?

  • WPF theme assemblies (need file Location for pack:// URIs)
  • Loader assembly (Revit's entry point)
  • Logging abstractions (shared type identity)

Source: build/BuildMerging.cs:17-65

AssemblyLoadContext (net8 / Revit 2025+)

For Revit 2025+ (net8), isolation is achieved via AssemblyLoadContext:

  • Each add-in gets its own load context
  • Dependencies are isolated without merging
  • No ILRepack needed

Source: build/BuildMerging.cs:11-14

Artifact Structure

Build Outputs

.artifacts/
├── bin/
│   └── Release/
│       ├── 2024/
│       │   └── net48/
│       │       ├── DBTools.dll          (merged)
│       │       ├── DBTools.Loader.dll
│       │       ├── DBTools.Themes.dll
│       │       └── DBTools.*.dll        (UI assemblies)
│       ├── 2025/
│       │   └── net8.0-windows/
│       │       ├── DBTools.dll
│       │       ├── DBTools.Loader.dll
│       │       └── *.dll                (dependencies)
│       └── 2026/
│           └── net8.0-windows/
│               └── ...
├── tests/
│   └── Release/
│       ├── 2024/
│       ├── 2025/
│       └── 2026/
├── vendor/
│   └── handycontrol/
└── .staging/
    └── {BuildId}/
        ├── bundle/
        ├── installer/
        └── logs/

Distribution

After PromoteToDist, production payloads are in:

.artifacts/dist/
└── Release/
    ├── 2024/
    ├── 2025/
    └── 2026/

Atomic Promotion for File Watchers

PromoteToDist uses atomic temp+replace semantics and skips unchanged files when publishing into dist/Release/<year>. This prevents file watchers (including AppLoader) from seeing partial writes mid-copy and triggering reload from an incomplete payload.

Promotion order is deterministic and writes DBTools.dll last:

  1. DBTools.Loader.dll
  2. DBTools.Themes.dll
  3. DBTools.HandyControl.dll
  4. DBTools.dll

Source: build/BuildConstants.cs:63-69 Source: build/ArtifactManagement.cs:50-76 Source: build/BuildHelpers.cs:188-295

Troubleshooting

Common Issues

Problem Solution
"Direct builds are blocked" (DBT0001) Use build.sh instead of dotnet build
"Windows dotnet.exe not found" Ensure .NET SDK installed on Windows side
"Vendored UI assemblies missing" Run bash build.sh vendor handycontrol
XAML designer errors Build once via build.sh to populate artifacts
MSBuild daemon file locks Script auto-cleans; or run dotnet build-server shutdown

Verbose Logging

# Add --Verbose for detailed NUKE output
bash build.sh --Verbose BuildAll

Clean Rebuild

# Full clean + rebuild
bash build.sh --clean BuildAll

Verification Status

Check Status
Source anchors for all claims Yes
UNVERIFIED markers where needed No (all verified)
Cross-references added Yes
Examples tested N/A (documentation)
No assumptions without evidence Yes

Verified by: initial-wiki-session
Date: 2026-01-16