Skip to main content

MSBuild SDK Packaging Best Practices

Building MSBuild SDKs with compiled tasks presents unique challenges, particularly around file locking during development. This guide documents Microsoft’s patterns and best practices for creating robust, maintainable MSBuild SDK packages.

The File Lock Problem

When developing MSBuild SDKs that include compiled tasks, a common issue arises:
  1. The SDK project compiles its task assemblies
  2. The same build process attempts to load those assemblies
  3. MSBuild locks the loaded assemblies in memory
  4. Subsequent builds fail because the compiler cannot overwrite locked files
This creates a frustrating development cycle where you must kill MSBuild processes or restart Visual Studio between builds.

Microsoft’s Solution Pattern

Microsoft addresses this challenge through architectural separation and careful build orchestration.

1. Separation of Concerns Architecture

Microsoft separates SDK projects into distinct components:
  • Task Projects (Microsoft.NET.Build.Tasks) - Contains compiled MSBuild tasks
  • SDK Projects (Microsoft.Build.NoTargets) - Contains props/targets files and SDK structure
  • Test Projects - Separate unit test projects for each component
This separation ensures that task compilation and SDK packaging never occur in the same build context.

2. The NoTargets Pattern

Microsoft created Microsoft.Build.NoTargets specifically for projects that don’t compile assemblies but need MSBuild integration:
This SDK type prevents assembly compilation while maintaining full MSBuild extensibility.

3. Task Assembly Packaging Configuration

For SDKs with compiled tasks, Microsoft uses specific packaging properties:

4. Standard Directory Structure

Microsoft’s SDK packages follow this consistent structure:

5. Multi-Stage Build Process

Microsoft avoids file locks through a multi-stage approach:
  1. Stage 1: Build task assemblies in an isolated project
  2. Stage 2: Package pre-built assemblies into the SDK
  3. Stage 3: Test the packaged SDK in a separate solution
The key insight: The SDK project never compiles and uses its own tasks simultaneously.

6. Runtime-Aware Task Loading

In Sdk.targets files, Microsoft uses runtime detection for task loading:

7. Development vs. Package Detection

Microsoft SDKs intelligently detect their execution context:

8. Preventing Circular Dependencies

The fundamental principle: The SDK project must never load its own compiled tasks during its own build. Strategies to achieve this:
For a robust MSBuild SDK with compiled tasks, use this three-project structure:

Project 1: Task Library

CloudNimble.DotNetDocs.Sdk.Tasks.csproj

Project 2: SDK Package

CloudNimble.DotNetDocs.Sdk.csproj

Project 3: Integration Tests

CloudNimble.DotNetDocs.Sdk.Tests.csproj

Development Workflow

  1. Build Tasks First: Always build the tasks project independently
  2. Then Package: Build the SDK project to create the package
  3. Test Separately: Use a different solution to test the packaged SDK
  4. Use Local Feeds: Configure a local NuGet feed for development testing

Additional Best Practices

Disable Node Reuse During Development

Add to Directory.Build.props in your development folder:

Use Intermediate Output Directories

Avoid conflicts by using unique output paths:

Version Your Task Assemblies

Include version numbers in task assembly paths during development:

Conclusion

By following Microsoft’s patterns of separation, staged builds, and careful dependency management, you can create robust MSBuild SDKs without encountering file lock issues. The key is ensuring your SDK project never attempts to compile and load its own tasks within the same build context. Remember: Separate concerns, build in stages, and test in isolation.