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
pushevents: include[ci]or[build]in the commit message to run build jobs. - For
pull_requestevents: 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:
- Artifact paths - All outputs to
.artifacts/ - Year-based organization -
bin/{Config}/{Year}/{TFM}/ - Design-time support - XAML designer compatibility
- 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:
- Platform target override for designer (AnyCPU instead of x64)
- Intermediate path fixes for wpftmp projects
- Reference resolution before XAML compilation
- 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:
DBTools.Loader.dllDBTools.Themes.dllDBTools.HandyControl.dllDBTools.dll
Source:
build/BuildConstants.cs:63-69Source:build/ArtifactManagement.cs:50-76Source: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
Related Documentation
- Project References - Dependency relationships
- ILRepack & Embedding - Assembly merging details
- Test Pipeline - Testing infrastructure
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