mirror of
https://github.com/dotnet/skills.git
synced 2026-09-20 09:49:54 +08:00
Clean up dotnet-msbuild: unify lock files, compact skill, add .gitattributes (#116)
* Clean up dotnet-msbuild: unify lock files, compact skill, add .gitattributes - Add .gitattributes with * text=auto eol=lf for consistent line endings - Unify compiled knowledge lock file names across copilot-extension and agentic-workflows (build-errors, performance, style-and-modernization) - Deduplicate KnowledgeMap in build.ps1 into shared KnowledgeGroups - Compact directory-build-organization skill (16K -> 8K chars) so it fits within compiled bundle limits; move detailed examples to references/ - Remove broken links to non-existent docs/copilot-extension-design.md - Regenerate all lock files Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Add repo-level AGENTS.md with component build instructions Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Inline references/ content during knowledge compilation Update Read-Skill in build.ps1 to resolve [text](references/*.md) links and replace them with the referenced file content. This ensures compiled lock files include the full reference material alongside the SKILL.md content. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Update agentic workflows to leverage compiled knowledge - build-failure-analysis: add binlog-mcp tool usage, bin/obj clash and generated file checks, common error categories - build-perf-audit: add bottleneck classification, concrete optimization recommendations (MSBuild Server, ArtifactsPath, graph build), incremental build health check - msbuild-pr-review: reference AP codes from anti-pattern catalog, add Central Package Management and Directory.Build centralization checks Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Change agentic workflow triggers to comment-based invocation - build-failure-analysis: /analyze-build-failure - build-perf-audit: /audit-build-perf - msbuild-pr-review: /review-msbuild Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -0,0 +1,2 @@
|
||||
# Normalize all text files to LF line endings
|
||||
* text=auto eol=lf
|
||||
@@ -0,0 +1,19 @@
|
||||
# Repository Instructions
|
||||
|
||||
This repository contains skill components under `src/`. Each subdirectory in `src/` is an independent component (e.g., `src/dotnet-msbuild`, `src/dotnet`).
|
||||
|
||||
## Build
|
||||
|
||||
When you modify files in a component, check whether that component has a `build.ps1` file in its root directory. If it does, run it after making changes to validate and regenerate any compiled artifacts.
|
||||
|
||||
```powershell
|
||||
pwsh src/<component>/build.ps1
|
||||
```
|
||||
|
||||
**Example:** After editing skills in `src/dotnet-msbuild/`, run:
|
||||
|
||||
```powershell
|
||||
pwsh src/dotnet-msbuild/build.ps1
|
||||
```
|
||||
|
||||
This validates skill frontmatter and recompiles knowledge lock files. Always commit the regenerated lock files together with your changes.
|
||||
@@ -10,6 +10,8 @@ Comprehensive MSBuild and .NET build skills: failure diagnosis, performance opti
|
||||
|-------|-------------|
|
||||
| [`binlog-failure-analysis`](skills/binlog-failure-analysis/) | Binary log analysis for deep build failure diagnosis |
|
||||
| [`binlog-generation`](skills/binlog-generation/) | Binary log generation conventions |
|
||||
| [`check-bin-obj-clash`](skills/check-bin-obj-clash/) | Output path conflict detection for multi-targeting and multi-project builds |
|
||||
| [`including-generated-files`](skills/including-generated-files/) | Including build-generated files in MSBuild's build process |
|
||||
|
||||
### Build Performance Optimization
|
||||
|
||||
@@ -28,8 +30,6 @@ Comprehensive MSBuild and .NET build skills: failure diagnosis, performance opti
|
||||
| [`msbuild-antipatterns`](skills/msbuild-antipatterns/) | Anti-pattern catalog with detection rules, severity, and BAD→GOOD fixes |
|
||||
| [`msbuild-modernization`](skills/msbuild-modernization/) | Legacy to SDK-style project migration with before/after examples |
|
||||
| [`directory-build-organization`](skills/directory-build-organization/) | Directory.Build.props/targets/rsp organization and central package management |
|
||||
| [`check-bin-obj-clash`](skills/check-bin-obj-clash/) | Output path conflict detection for multi-targeting and multi-project builds |
|
||||
| [`including-generated-files`](skills/including-generated-files/) | Including build-generated files in MSBuild's build process |
|
||||
|
||||
## 🤖 Agents
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
---
|
||||
on:
|
||||
workflow_run:
|
||||
workflows: ["CI", "Build", "CI Build"]
|
||||
types: [completed]
|
||||
issue_comment:
|
||||
types: [created]
|
||||
body: "/analyze-build-failure"
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
@@ -12,7 +12,7 @@ permissions:
|
||||
|
||||
imports:
|
||||
- shared/binlog-mcp.md
|
||||
- shared/compiled/build-failure-knowledge.lock.md
|
||||
- shared/compiled/build-errors.lock.md
|
||||
|
||||
tools:
|
||||
github:
|
||||
@@ -32,15 +32,24 @@ You are an MSBuild build failure analysis agent. When a CI build workflow comple
|
||||
|
||||
1. **Check if the triggering workflow failed**: Use the GitHub tools to check the workflow run status. If it succeeded, exit without action.
|
||||
|
||||
2. **Get failure details**:
|
||||
2. **Get failure details**:
|
||||
- Get the failed workflow run details and job logs
|
||||
- Identify which jobs and steps failed
|
||||
- Look for .NET build error patterns (CS, MSB, NU, NETSDK, FS, BC error codes)
|
||||
- Look for .NET build error patterns (CS, MSB, NU, NETSDK, FS, BC, AD error codes)
|
||||
|
||||
3. **Analyze the failure**:
|
||||
- If binlog files are available as artifacts, download and analyze them with binlog-mcp tools
|
||||
- If binlog files are available as artifacts, download and analyze them with binlog-mcp tools:
|
||||
1. `load_binlog` to load the binary log
|
||||
2. `get_diagnostics` for errors and warnings
|
||||
3. `search_binlog` for specific patterns (see query language in imported knowledge)
|
||||
- Otherwise, analyze the build output logs for error patterns
|
||||
- Use MSBuild knowledge to identify root causes
|
||||
- Check for common failure categories:
|
||||
- **Compile errors** (CS prefix): missing types, syntax errors, nullable violations
|
||||
- **MSBuild errors** (MSB prefix): target failures, import issues, property evaluation
|
||||
- **NuGet errors** (NU prefix): restore failures, version conflicts, missing packages
|
||||
- **SDK errors** (NETSDK prefix): SDK not found, workload issues, TFM problems
|
||||
- **Bin/obj clashes**: multiple projects or TFMs writing to the same output directory — use `search_binlog` for file access errors or MSB3277 warnings
|
||||
- **Generated file issues**: source generators failing or generated files not included in compilation (CS8785, AD0001)
|
||||
|
||||
4. **Post findings**:
|
||||
- If the failure is associated with a pull request, post a comment on the PR
|
||||
@@ -51,6 +60,7 @@ You are an MSBuild build failure analysis agent. When a CI build workflow comple
|
||||
## Guidelines
|
||||
- Only post comments for genuine build failures, not infrastructure issues
|
||||
- Be specific: reference exact error codes, file paths, and line numbers when available
|
||||
- Suggest concrete fixes, not vague advice
|
||||
- Suggest concrete fixes, not vague advice — show corrected XML or commands
|
||||
- If binlogs are available, always prefer binlog analysis over parsing console output
|
||||
- If you can't determine the cause, say so rather than guessing
|
||||
- Don't repeat the entire build log — summarize the key errors
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
---
|
||||
on:
|
||||
schedule: weekly
|
||||
issue_comment:
|
||||
types: [created]
|
||||
body: "/audit-build-perf"
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
@@ -9,7 +11,7 @@ permissions:
|
||||
|
||||
imports:
|
||||
- shared/binlog-mcp.md
|
||||
- shared/compiled/perf-audit-knowledge.lock.md
|
||||
- shared/compiled/performance.lock.md
|
||||
|
||||
tools:
|
||||
github:
|
||||
@@ -30,32 +32,45 @@ You are a build performance auditing agent. Each week, you analyze the repositor
|
||||
|
||||
1. **Build with binlog**: Run `dotnet build /bl:perf-audit.binlog -m` to generate a performance baseline
|
||||
|
||||
2. **Analyze performance**:
|
||||
2. **Analyze performance** using binlog-mcp tools:
|
||||
- Load the binlog with `load_binlog`
|
||||
- Get total build duration
|
||||
- Run `get_node_timeline` for parallelism analysis
|
||||
- Run `get_expensive_projects(top_number=10, sortByExclusive=true)`
|
||||
- Run `get_expensive_targets(top_number=10)`
|
||||
- Run `get_expensive_tasks(top_number=10)`
|
||||
- Run `get_expensive_analyzers(top_number=5)`
|
||||
- `get_node_timeline` → assess parallelism utilization across build nodes
|
||||
- `get_expensive_projects(top_number=10, sortByExclusive=true)` → find time-heavy projects
|
||||
- `get_expensive_targets(top_number=15)` → find dominant targets (Csc, RAR, Copy)
|
||||
- `get_expensive_tasks(top_number=15)` → find dominant tasks
|
||||
- `get_expensive_analyzers(top_number=10)` → check Roslyn analyzer overhead
|
||||
|
||||
3. **Track trends**: Use `cache-memory` to store and compare:
|
||||
3. **Classify bottlenecks** into categories:
|
||||
- **Serialization**: nodes idle, one project blocking others → project graph issue
|
||||
- **Compilation**: Csc task dominant → too much code in one project, or expensive analyzers
|
||||
- **Resolution**: ResolveAssemblyReference dominant → too many references
|
||||
- **I/O**: Copy/Move tasks dominant → excessive file copying, consider hardlinks
|
||||
- **Evaluation**: slow startup before compilation → expensive glob patterns or deep import chains
|
||||
- **Analyzers**: disproportionate analyzer time → specific analyzer is expensive
|
||||
|
||||
4. **Track trends**: Use `cache-memory` to store and compare:
|
||||
- Total build duration
|
||||
- Top 5 most expensive projects and their times
|
||||
- Analyzer overhead percentage
|
||||
- Node utilization percentage
|
||||
|
||||
4. **Generate report**: Create an issue with:
|
||||
5. **Generate report**: Create an issue with:
|
||||
- **Summary**: Total build time, comparison to previous week
|
||||
- **Top bottlenecks**: Most expensive projects/targets/tasks
|
||||
- **Top bottlenecks**: Most expensive projects/targets/tasks with durations
|
||||
- **Trends**: Is build time improving or degrading?
|
||||
- **Recommendations**: Actionable suggestions for improvement
|
||||
- **Analyzer impact**: If analyzer time is >30% of compilation, flag it
|
||||
- **Recommendations** prioritized by effort:
|
||||
- *Quick wins*: `/maxcpucount`, `RunAnalyzers=false` in dev, MSBuild Server (`DOTNET_CLI_USE_MSBUILD_SERVER=1`)
|
||||
- *Medium effort*: `ArtifactsPath` for bin/obj separation, incremental build fixes (missing Inputs/Outputs on custom targets), disable expensive analyzers in CI
|
||||
- *Large effort*: graph build (`/graph`), project splitting, dependency graph trimming
|
||||
- **Analyzer impact**: If analyzer time is >30% of compilation, flag specific analyzers
|
||||
- **Incremental build health**: Check if no-op builds are truly fast (should be <5% of clean build)
|
||||
|
||||
5. **Only create issue if noteworthy**: Don't create an issue if build times are stable and within acceptable range. Only report when:
|
||||
6. **Only create issue if noteworthy**: Don't create an issue if build times are stable and within acceptable range. Only report when:
|
||||
- Build time increased >10% from previous audit
|
||||
- A new bottleneck appeared in top 5
|
||||
- Node utilization dropped below 70%
|
||||
- Incremental builds are broken (no-op build > 10% of clean build time)
|
||||
- It's the first audit (establish baseline)
|
||||
|
||||
## Guidelines
|
||||
|
||||
@@ -1,24 +1,15 @@
|
||||
---
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, synchronize]
|
||||
paths:
|
||||
- "**/*.csproj"
|
||||
- "**/*.vbproj"
|
||||
- "**/*.fsproj"
|
||||
- "**/*.props"
|
||||
- "**/*.targets"
|
||||
- "**/Directory.Build.*"
|
||||
- "**/Directory.Packages.props"
|
||||
- "**/nuget.config"
|
||||
- "**/global.json"
|
||||
issue_comment:
|
||||
types: [created]
|
||||
body: "/review-msbuild"
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: read
|
||||
|
||||
imports:
|
||||
- shared/compiled/pr-review-knowledge.lock.md
|
||||
- shared/compiled/style-and-modernization.lock.md
|
||||
|
||||
tools:
|
||||
github:
|
||||
@@ -38,38 +29,41 @@ You are a specialized reviewer for MSBuild project file changes. When a PR modif
|
||||
|
||||
1. **Get the PR diff**: Retrieve the changed files and their diffs
|
||||
2. **Filter to MSBuild files**: Focus only on .csproj, .vbproj, .fsproj, .props, .targets, Directory.Build.*, Directory.Packages.props, nuget.config, global.json
|
||||
3. **Analyze each changed file** against these criteria:
|
||||
3. **Analyze each changed file** against the anti-pattern catalog and modernization guide in the imported knowledge:
|
||||
|
||||
### Check for Anti-patterns
|
||||
- Hardcoded absolute paths (should use MSBuild properties)
|
||||
- Explicit file includes that SDK handles automatically
|
||||
- `<Reference>` tags with HintPath that should be `<PackageReference>` (note: `<Reference>` is valid for .NET Framework GAC assemblies)
|
||||
- Missing `Condition` quotes: must be `'$(Prop)' == 'value'`
|
||||
- Properties conditioned on `$(TargetFramework)` in `.props` files (silently fails for single-targeting projects — move to `.targets`)
|
||||
- Missing `PrivateAssets="all"` on analyzer/tool packages
|
||||
- Properties that belong in Directory.Build.props (if duplicated)
|
||||
### Check for Anti-patterns (AP codes from imported knowledge)
|
||||
- **AP-01** Hardcoded absolute paths (should use `$(MSBuildThisFileDirectory)` or similar)
|
||||
- **AP-02** Explicit file includes that SDK handles automatically (`<Compile Include="**/*.cs" />`)
|
||||
- **AP-05** `<Reference>` with HintPath that should be `<PackageReference>` (note: `<Reference>` is valid for .NET Framework GAC assemblies)
|
||||
- **AP-06** Missing `Condition` quotes: must be `'$(Prop)' == 'value'`
|
||||
- **AP-08** Missing `PrivateAssets="all"` on analyzer/tool packages
|
||||
- **AP-10** Custom targets missing `Inputs`/`Outputs` (breaks incremental builds)
|
||||
- **AP-12** Properties that belong in Directory.Build.props (if duplicated across projects)
|
||||
- **AP-17** Side effects during property evaluation (file writes, network calls)
|
||||
- **AP-18** Platform-specific `<Exec>` without OS condition guard
|
||||
- **AP-21** Properties conditioned on `$(TargetFramework)` in `.props` files (silently fails for single-targeting projects — move to `.targets`). **Item and target conditions are NOT affected** and must not be flagged.
|
||||
|
||||
### Check for Correctness
|
||||
- Custom targets missing `Inputs`/`Outputs` (breaks incremental builds)
|
||||
- Potential bin/obj path clashes in multi-targeting
|
||||
- Package version conflicts
|
||||
- Incorrect TFM syntax
|
||||
- Side effects during property evaluation (file writes, network calls)
|
||||
- Platform-specific `<Exec>` without OS condition guard
|
||||
- Condition logic that is always true/false
|
||||
|
||||
### Check for Modernization Opportunities
|
||||
- Legacy project format that could be SDK-style
|
||||
- `packages.config` that should be PackageReference
|
||||
- Properties that could use Central Package Management
|
||||
- Properties that could use Central Package Management (`Directory.Packages.props`)
|
||||
- Duplicated settings that should be centralized in `Directory.Build.props`
|
||||
|
||||
4. **Post review**: Comment on the PR with findings organized by severity:
|
||||
- 🔴 Issues that should be fixed before merge
|
||||
- 🟡 Suggestions for improvement
|
||||
- 🔴 Issues that should be fixed before merge (broken builds, correctness issues)
|
||||
- 🟡 Suggestions for improvement (anti-patterns, modernization)
|
||||
- 🟢 Positive patterns observed
|
||||
|
||||
## Guidelines
|
||||
- Only comment on MSBuild-specific issues, not general code quality
|
||||
- Reference AP codes when flagging anti-patterns (e.g., "AP-08: Missing PrivateAssets")
|
||||
- Be constructive and explain WHY something is an issue
|
||||
- Provide the correct code when suggesting a fix
|
||||
- Provide the corrected XML when suggesting a fix — show BAD → GOOD
|
||||
- Don't comment if the changes look good — only post when there are actionable findings
|
||||
- Keep comments concise and focused
|
||||
|
||||
+225
-2
@@ -1,4 +1,4 @@
|
||||
<!-- AUTO-GENERATED — DO NOT EDIT. Regenerate with: node src/dotnet-msbuild/build.js -->
|
||||
<!-- AUTO-GENERATED — DO NOT EDIT -->
|
||||
|
||||
# Analyzing MSBuild Failures with Binary Logs
|
||||
|
||||
@@ -285,6 +285,97 @@ When binlog analysis reveals these patterns, here's the fast path:
|
||||
|
||||
---
|
||||
|
||||
# Generate Binary Logs
|
||||
|
||||
**Pass the `/bl` switch when running any MSBuild-based command.** This is a non-negotiable requirement for all .NET builds.
|
||||
|
||||
## Commands That Require /bl
|
||||
|
||||
You MUST add the `/bl:{}` flag to:
|
||||
- `dotnet build`
|
||||
- `dotnet test`
|
||||
- `dotnet pack`
|
||||
- `dotnet publish`
|
||||
- `dotnet restore`
|
||||
- `msbuild` or `msbuild.exe`
|
||||
- Any other command that invokes MSBuild
|
||||
|
||||
## Preferred: Use `{}` for Automatic Unique Names
|
||||
|
||||
> **Note:** The `{}` placeholder requires MSBuild 17.8+ / .NET 8 SDK or later.
|
||||
|
||||
The `{}` placeholder in the binlog filename is replaced by MSBuild with a unique identifier, guaranteeing no two builds ever overwrite each other — without needing to track or check existing files.
|
||||
|
||||
```bash
|
||||
# Every invocation produces a distinct file automatically
|
||||
dotnet build /bl:{}
|
||||
dotnet test /bl:{}
|
||||
dotnet build --configuration Release /bl:{}
|
||||
```
|
||||
|
||||
**PowerShell requires escaping the braces:**
|
||||
|
||||
```powershell
|
||||
# PowerShell: escape { } as {{ }}
|
||||
dotnet build -bl:{{}}
|
||||
dotnet test -bl:{{}}
|
||||
```
|
||||
|
||||
## Why This Matters
|
||||
|
||||
1. **Unique names prevent overwrites** - You can always go back and analyze previous builds
|
||||
2. **Failure analysis** - When a build fails, the binlog is already there for immediate analysis
|
||||
3. **Comparison** - You can compare builds before and after changes
|
||||
4. **No re-running builds** - You never need to re-run a failed build just to generate a binlog
|
||||
|
||||
## Examples
|
||||
|
||||
```bash
|
||||
# ✅ CORRECT - {} generates a unique name automatically (bash/cmd)
|
||||
dotnet build /bl:{}
|
||||
dotnet test /bl:{}
|
||||
|
||||
# ✅ CORRECT - PowerShell escaping
|
||||
dotnet build -bl:{{}}
|
||||
dotnet test -bl:{{}}
|
||||
|
||||
# ❌ WRONG - Missing /bl flag entirely
|
||||
dotnet build
|
||||
dotnet test
|
||||
|
||||
# ❌ WRONG - No filename (overwrites the same msbuild.binlog every time)
|
||||
dotnet build /bl
|
||||
dotnet build /bl
|
||||
```
|
||||
|
||||
## When a Specific Filename Is Required
|
||||
|
||||
If the binlog filename needs to be known upfront (e.g., for CI artifact upload), or if `{}` is not available in the installed MSBuild version, pick a name that won't collide with existing files:
|
||||
|
||||
1. Check for existing `*.binlog` files in the directory
|
||||
2. Choose a name not already taken (e.g., by incrementing a counter from the highest existing number)
|
||||
|
||||
```bash
|
||||
# Example: directory contains 3.binlog — use 4.binlog
|
||||
dotnet build /bl:4.binlog
|
||||
```
|
||||
|
||||
## Cleaning the Repository
|
||||
|
||||
When cleaning the repository with `git clean`, **always exclude binlog files** to preserve your build history:
|
||||
|
||||
```bash
|
||||
# ✅ CORRECT - Exclude binlog files from cleaning
|
||||
git clean -fdx -e "*.binlog"
|
||||
|
||||
# ❌ WRONG - This deletes binlog files (they're usually in .gitignore)
|
||||
git clean -fdx
|
||||
```
|
||||
|
||||
This is especially important when iterating on build fixes - you need the binlogs to analyze what changed between builds.
|
||||
|
||||
---
|
||||
|
||||
# Detecting OutputPath and IntermediateOutputPath Clashes
|
||||
|
||||
## Overview
|
||||
@@ -620,4 +711,136 @@ When multiple evaluations share an output path, compare these global properties
|
||||
|
||||
## Testing Fixes
|
||||
|
||||
After making changes to fix path clashes, clean and rebuild to verify. See the `binlog-generation` skill's "Cleaning the Repository" section on how to clean the repository while preserving binlog files.
|
||||
After making changes to fix path clashes, clean and rebuild to verify. See the `binlog-generation` skill's "Cleaning the Repository" section on how to clean the repository while preserving binlog files.
|
||||
|
||||
---
|
||||
|
||||
# Including Generated Files Into Your Build
|
||||
|
||||
## Overview
|
||||
|
||||
Files generated during the build are generally ignored by the build process. This leads to confusing results such as:
|
||||
- Generated files not being included in the output directory
|
||||
- Generated source files not being compiled
|
||||
- Globs not capturing files created during the build
|
||||
|
||||
This happens because of how MSBuild's build phases work.
|
||||
|
||||
## Quick Takeaway
|
||||
|
||||
For code files generated during the build - we need to add those to `Compile` and `FileWrites` item groups within the target generating the file(s):
|
||||
|
||||
```xml
|
||||
<ItemGroup>
|
||||
<Compile Include="$(GeneratedFilePath)" />
|
||||
<FileWrites Include="$(GeneratedFilePath)" />
|
||||
</ItemGroup>
|
||||
```
|
||||
|
||||
The target generating the file(s) should be hooked before CoreCompile and BeforeCompile targets - `BeforeTargets="CoreCompile;BeforeCompile"`
|
||||
|
||||
## Why Generated Files Are Ignored
|
||||
|
||||
For detailed explanation, see [How MSBuild Builds Projects](https://docs.microsoft.com/visualstudio/msbuild/build-process-overview).
|
||||
|
||||
### Evaluation Phase
|
||||
|
||||
MSBuild reads your project, imports everything, creates Properties, expands globs for Items **outside of Targets**, and sets up the build process.
|
||||
|
||||
### Execution Phase
|
||||
|
||||
MSBuild runs Targets & Tasks with the provided Properties & Items to perform the build.
|
||||
|
||||
**Key Takeaway:** Files generated during execution don't exist during evaluation, therefore they aren't found. This particularly affects files that are globbed by default, such as source files (`.cs`).
|
||||
|
||||
## Solution: Manually Add Generated Files
|
||||
|
||||
When files are generated during the build, manually add them into the build process. The approach depends on the type of file being generated.
|
||||
|
||||
### Use `$(IntermediateOutputPath)` for Generated File Location
|
||||
|
||||
Always use `$(IntermediateOutputPath)` as the base directory for generated files. **Do not** hardcode `obj\` or construct the intermediary path manually (e.g., `obj\$(Configuration)\$(TargetFramework)\`). The intermediate output path can be redirected to a different location in some build configurations (e.g., shared output directories, CI environments). Using `$(IntermediateOutputPath)` ensures your target works correctly regardless of the actual path.
|
||||
|
||||
### Always Add Generated Files to `FileWrites`
|
||||
|
||||
Every generated file should be added to the `FileWrites` item group. This ensures that MSBuild's `Clean` target properly removes your generated files. Without this, generated files will accumulate as stale artifacts across builds.
|
||||
|
||||
```xml
|
||||
<ItemGroup>
|
||||
<FileWrites Include="$(IntermediateOutputPath)my-generated-file.xyz" />
|
||||
</ItemGroup>
|
||||
```
|
||||
|
||||
### Basic Pattern (Non-Code Files)
|
||||
|
||||
For generated files that need to be copied to output (config files, data files, etc.), add them to `Content` or `None` items before `BeforeBuild`:
|
||||
|
||||
```xml
|
||||
<Target Name="IncludeGeneratedFiles" BeforeTargets="BeforeBuild">
|
||||
|
||||
<!-- Your logic that generates files goes here -->
|
||||
|
||||
<ItemGroup>
|
||||
<None Include="$(IntermediateOutputPath)my-generated-file.xyz" CopyToOutputDirectory="PreserveNewest"/>
|
||||
|
||||
<!-- Capture all files of a certain type with a glob -->
|
||||
<None Include="$(IntermediateOutputPath)generated\*.xyz" CopyToOutputDirectory="PreserveNewest"/>
|
||||
|
||||
<!-- Register generated files for proper cleanup -->
|
||||
<FileWrites Include="$(IntermediateOutputPath)my-generated-file.xyz" />
|
||||
<FileWrites Include="$(IntermediateOutputPath)generated\*.xyz" />
|
||||
</ItemGroup>
|
||||
</Target>
|
||||
```
|
||||
|
||||
### For Generated Source Files (Code That Needs Compilation)
|
||||
|
||||
If you're generating `.cs` files that need to be compiled, use **`BeforeTargets="CoreCompile;BeforeCompile"`**. This is the correct timing for adding `Compile` items — it runs late enough that the file generation has occurred, but before the compiler runs. Using `BeforeBuild` is too early for some scenarios and may not work reliably with all SDK features.
|
||||
|
||||
```xml
|
||||
<Target Name="IncludeGeneratedSourceFiles" BeforeTargets="CoreCompile;BeforeCompile">
|
||||
<PropertyGroup>
|
||||
<GeneratedCodeDir>$(IntermediateOutputPath)Generated\</GeneratedCodeDir>
|
||||
<GeneratedFilePath>$(GeneratedCodeDir)MyGeneratedFile.cs</GeneratedFilePath>
|
||||
</PropertyGroup>
|
||||
|
||||
<MakeDir Directories="$(GeneratedCodeDir)" />
|
||||
|
||||
<!-- Your logic that generates the .cs file goes here -->
|
||||
|
||||
<ItemGroup>
|
||||
<Compile Include="$(GeneratedFilePath)" />
|
||||
<FileWrites Include="$(GeneratedFilePath)" />
|
||||
</ItemGroup>
|
||||
</Target>
|
||||
```
|
||||
|
||||
Note: Specifying both `CoreCompile` and `BeforeCompile` ensures the target runs before whichever target comes first, providing robust ordering regardless of customizations in the build.
|
||||
|
||||
## Target Timing
|
||||
|
||||
Choose the `BeforeTargets` value based on the type of file being generated:
|
||||
|
||||
- **`BeforeTargets="BeforeBuild"`** — For non-code files added to `None` or `Content`. Runs early enough for copy-to-output scenarios.
|
||||
- **`BeforeTargets="CoreCompile;BeforeCompile"`** — For generated source files added to `Compile`. Ensures the file is included before the compiler runs.
|
||||
- **`BeforeTargets="AssignTargetPaths"`** — The "final stop" before `None` and `Content` items (among others) are transformed into new items. Use as a fallback if `BeforeBuild` is too early.
|
||||
|
||||
## Globbing Behavior
|
||||
|
||||
Globs behave according to **when** the glob took place:
|
||||
|
||||
| Glob Location | Files Captured |
|
||||
|---------------|----------------|
|
||||
| Outside of a target | Only files visible during Evaluation phase (before build starts) |
|
||||
| Inside of a target | Files visible when the target runs (can capture generated files if timed correctly) |
|
||||
|
||||
This is why the solution places the `<ItemGroup>` inside a `<Target>` - the glob runs during execution when the generated files exist.
|
||||
|
||||
## Relevant Links
|
||||
|
||||
- [How MSBuild Builds Projects](https://docs.microsoft.com/visualstudio/msbuild/build-process-overview)
|
||||
- [Evaluation Phase](https://docs.microsoft.com/visualstudio/msbuild/build-process-overview#evaluation-phase)
|
||||
- [Execution Phase](https://docs.microsoft.com/visualstudio/msbuild/build-process-overview#execution-phase)
|
||||
- [Common Item Types](https://docs.microsoft.com/visualstudio/msbuild/common-msbuild-project-items)
|
||||
- [How the SDK imports items by default](https://github.com/dotnet/sdk/blob/main/src/Tasks/Microsoft.NET.Build.Tasks/targets/Microsoft.NET.Sdk.DefaultItems.props)
|
||||
- [Official docs: Handle generated files](https://learn.microsoft.com/visualstudio/msbuild/customize-your-build#handle-generated-files)
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
<!-- AUTO-GENERATED — DO NOT EDIT. Regenerate with: node src/dotnet-msbuild/build.js -->
|
||||
<!-- AUTO-GENERATED — DO NOT EDIT -->
|
||||
|
||||
# Build Performance Baseline & Optimization
|
||||
|
||||
+57
-74
@@ -1,4 +1,4 @@
|
||||
<!-- AUTO-GENERATED — DO NOT EDIT. Regenerate with: node src/dotnet-msbuild/build.js -->
|
||||
<!-- AUTO-GENERATED — DO NOT EDIT -->
|
||||
|
||||
# MSBuild Anti-Pattern Catalog
|
||||
|
||||
@@ -181,7 +181,28 @@ Use this catalog when scanning project files for improvements.
|
||||
|
||||
**Why it's bad**: Without `PrivateAssets="all"`, analyzer and build-tool packages flow as transitive dependencies to consumers of your library. Consumers get unwanted analyzers or build-time tools they didn't ask for.
|
||||
|
||||
See [`references/private-assets.md`](references/private-assets.md) for BAD/GOOD examples and the full list of packages that need this.
|
||||
See # PrivateAssets for Analyzers and Build Tools
|
||||
|
||||
Analyzer and build-tool packages should always use `PrivateAssets="all"` to prevent them from flowing as transitive dependencies to consumers of your library.
|
||||
|
||||
```xml
|
||||
<!-- BAD: Flows to consumers -->
|
||||
<PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.556" />
|
||||
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="8.0.0" />
|
||||
<PackageReference Include="MinVer" Version="5.0.0" />
|
||||
|
||||
<!-- GOOD: Stays private -->
|
||||
<PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.556" PrivateAssets="all" />
|
||||
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="8.0.0" PrivateAssets="all" />
|
||||
<PackageReference Include="MinVer" Version="5.0.0" PrivateAssets="all" />
|
||||
```
|
||||
|
||||
**Packages that almost always need `PrivateAssets="all"`:**
|
||||
- Roslyn analyzers (`*.Analyzers`, `*.CodeFixes`)
|
||||
- Source generators
|
||||
- SourceLink packages (`Microsoft.SourceLink.*`)
|
||||
- Versioning tools (`MinVer`, `Nerdbank.GitVersioning`)
|
||||
- Build-only tools (`Microsoft.DotNet.ApiCompat`, etc.) for BAD/GOOD examples and the full list of packages that need this.
|
||||
|
||||
---
|
||||
|
||||
@@ -276,7 +297,36 @@ See `directory-build-organization` skill for full guidance on structuring `Direc
|
||||
|
||||
**Why it's bad**: The target runs on every build, even when nothing changed. This defeats incremental build and slows down no-op builds.
|
||||
|
||||
See [`references/incremental-build-inputs-outputs.md`](references/incremental-build-inputs-outputs.md) for BAD/GOOD examples and the full pattern including FileWrites registration.
|
||||
See # Incremental Build: Inputs and Outputs on Custom Targets
|
||||
|
||||
Custom targets **must** specify `Inputs` and `Outputs` attributes so MSBuild can skip them when up-to-date. Without both attributes, the target runs on every build.
|
||||
|
||||
```xml
|
||||
<!-- BAD: Runs every time -->
|
||||
<Target Name="GenerateBuildInfo" BeforeTargets="CoreCompile">
|
||||
<WriteLinesToFile File="$(IntermediateOutputPath)BuildInfo.g.cs"
|
||||
Lines="// Generated at $(Version)" Overwrite="true" />
|
||||
</Target>
|
||||
|
||||
<!-- GOOD: Skipped when up-to-date -->
|
||||
<Target Name="GenerateBuildInfo" BeforeTargets="CoreCompile"
|
||||
Inputs="$(MSBuildProjectFile)" Outputs="$(IntermediateOutputPath)BuildInfo.g.cs">
|
||||
<WriteLinesToFile File="$(IntermediateOutputPath)BuildInfo.g.cs"
|
||||
Lines="// Generated at $(Version)" Overwrite="true" />
|
||||
<ItemGroup>
|
||||
<FileWrites Include="$(IntermediateOutputPath)BuildInfo.g.cs" />
|
||||
<Compile Include="$(IntermediateOutputPath)BuildInfo.g.cs" />
|
||||
</ItemGroup>
|
||||
</Target>
|
||||
```
|
||||
|
||||
**Key points:**
|
||||
- **`Inputs`** should include `$(MSBuildProjectFile)` plus any source files that drive generation
|
||||
- **`Outputs`** should use `$(IntermediateOutputPath)` so generated files go in `obj/` and are managed by MSBuild
|
||||
- **`FileWrites`** registration ensures `dotnet clean` removes the generated file
|
||||
- **`Compile` inclusion** adds the generated file to compilation without requiring it at evaluation time
|
||||
|
||||
See the `incremental-build` skill for deep guidance on diagnosing broken incremental builds, FileWrites tracking, and Visual Studio's Fast Up-to-Date Check. for BAD/GOOD examples and the full pattern including FileWrites registration.
|
||||
|
||||
See `incremental-build` skill for deep guidance on Inputs/Outputs, FileWrites, and up-to-date checks.
|
||||
|
||||
@@ -584,6 +634,8 @@ When reviewing an MSBuild file, scan for these in order:
|
||||
|
||||
---
|
||||
|
||||
## msbuild-modernization
|
||||
|
||||
# MSBuild Modernization: Legacy to SDK-style Migration
|
||||
|
||||
## Identifying Legacy vs SDK-style Projects
|
||||
@@ -1008,75 +1060,6 @@ After migration, consider enabling modern C# features:
|
||||
|
||||
Centralizes NuGet version management across a multi-project solution. See [https://learn.microsoft.com/en-us/nuget/consume-packages/central-package-management](https://learn.microsoft.com/en-us/nuget/consume-packages/central-package-management) for details.
|
||||
|
||||
**Step 1:** Create `Directory.Packages.props` at the repository root with `<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>` and `<PackageVersion>` items for all packages.
|
||||
**Step 1:** Create `Directory.Pa
|
||||
|
||||
**Step 2:** Remove `Version` from each project's `PackageReference`:
|
||||
|
||||
```xml
|
||||
<!-- BEFORE -->
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
||||
|
||||
<!-- AFTER -->
|
||||
<PackageReference Include="Newtonsoft.Json" />
|
||||
```
|
||||
|
||||
## Directory.Build Consolidation
|
||||
|
||||
Identify properties repeated across multiple `.csproj` files and move them to shared files.
|
||||
|
||||
**`Directory.Build.props`** (for properties — placed at repo or src root):
|
||||
|
||||
```xml
|
||||
<Project>
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||
<Company>Contoso</Company>
|
||||
<Copyright>Copyright © Contoso 2024</Copyright>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
```
|
||||
|
||||
**`Directory.Build.targets`** (for targets/tasks — placed at repo or src root):
|
||||
|
||||
```xml
|
||||
<Project>
|
||||
<Target Name="PrintBuildInfo" AfterTargets="Build">
|
||||
<Message Importance="High" Text="Built $(AssemblyName) → $(TargetPath)" />
|
||||
</Target>
|
||||
</Project>
|
||||
```
|
||||
|
||||
**Keep in individual `.csproj` files** only what is project-specific:
|
||||
|
||||
```xml
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<AssemblyName>MyApp</AssemblyName>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Serilog" />
|
||||
<ProjectReference Include="..\MyLibrary\MyLibrary.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
```
|
||||
|
||||
## Tools and Automation
|
||||
|
||||
| Tool | Usage |
|
||||
|------|-------|
|
||||
| `dotnet try-convert` | Automated legacy-to-SDK conversion. Install: `dotnet tool install -g try-convert` |
|
||||
| .NET Upgrade Assistant | Full migration including API changes. Install: `dotnet tool install -g upgrade-assistant` |
|
||||
| Visual Studio | Right-click `packages.config` → *Migrate packages.config to PackageReference* |
|
||||
| Manual migration | Often cleanest for simple projects — follow the checklist above |
|
||||
|
||||
**Recommended approach:**
|
||||
|
||||
1. Run `try-convert` for a first pass
|
||||
2. Review and clean up the output manually
|
||||
3. Build and fix any issues
|
||||
4. Enable modern features (nullable, implicit usings)
|
||||
5. Consolidate shared settings into `Directory.Build.props`
|
||||
[truncated]
|
||||
@@ -1,198 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
// Build entry point for the dotnet-msbuild component.
|
||||
// Validates skills and compiles knowledge bundles.
|
||||
// Run: node src/dotnet-msbuild/build.js
|
||||
|
||||
const fs = require("node:fs");
|
||||
const path = require("node:path");
|
||||
|
||||
const SKILLS_DIR = path.resolve(__dirname, "skills");
|
||||
const DOMAIN_GATE_PATTERN = /Only activate in MSBuild\/\.NET build context/;
|
||||
|
||||
// ── Step 1: Validate skills ─────────────────────────────────────────
|
||||
|
||||
console.log("=== Validating skills ===\n");
|
||||
|
||||
let errors = 0;
|
||||
|
||||
const skillDirs = fs.readdirSync(SKILLS_DIR, { withFileTypes: true })
|
||||
.filter(d => d.isDirectory() && d.name !== "shared");
|
||||
|
||||
for (const dir of skillDirs) {
|
||||
const skillFile = path.join(SKILLS_DIR, dir.name, "SKILL.md");
|
||||
if (!fs.existsSync(skillFile)) continue;
|
||||
|
||||
const content = fs.readFileSync(skillFile, "utf-8");
|
||||
|
||||
const match = content.match(/^---\s*\n([\s\S]*?)\n---/);
|
||||
if (!match) {
|
||||
console.error(`❌ ${dir.name}: Missing YAML frontmatter`);
|
||||
errors++;
|
||||
continue;
|
||||
}
|
||||
|
||||
const frontmatter = match[1];
|
||||
const descMatch = frontmatter.match(/description:\s*"([^"]*)"/);
|
||||
if (!descMatch) {
|
||||
console.error(`❌ ${dir.name}: Missing description in frontmatter`);
|
||||
errors++;
|
||||
continue;
|
||||
}
|
||||
|
||||
const description = descMatch[1];
|
||||
if (!DOMAIN_GATE_PATTERN.test(description)) {
|
||||
console.error(`❌ ${dir.name}: Description missing domain gate. Must include 'Only activate in MSBuild/.NET build context.'`);
|
||||
errors++;
|
||||
}
|
||||
}
|
||||
|
||||
if (errors > 0) {
|
||||
console.error(`\n${errors} validation error(s) found.`);
|
||||
process.exit(1);
|
||||
} else {
|
||||
console.log(`✅ All ${skillDirs.length} skills pass validation.\n`);
|
||||
}
|
||||
|
||||
// ── Step 2: Compile knowledge bundles ────────────────────────────────
|
||||
|
||||
console.log("=== Compiling knowledge ===\n");
|
||||
|
||||
const KNOWLEDGE_TARGETS = {
|
||||
"copilot-extension": {
|
||||
outputDir: path.resolve(__dirname, "copilot-extension/src/knowledge"),
|
||||
maxChars: 50000,
|
||||
knowledgeMap: {
|
||||
"build-errors": [
|
||||
"binlog-failure-analysis",
|
||||
"check-bin-obj-clash",
|
||||
],
|
||||
performance: [
|
||||
"build-perf-baseline",
|
||||
"build-perf-diagnostics",
|
||||
"incremental-build",
|
||||
"build-parallelism",
|
||||
"eval-performance",
|
||||
],
|
||||
"style-guide": [
|
||||
"msbuild-antipatterns",
|
||||
"directory-build-organization",
|
||||
"check-bin-obj-clash",
|
||||
"including-generated-files",
|
||||
],
|
||||
modernization: [
|
||||
"msbuild-modernization",
|
||||
"directory-build-organization",
|
||||
],
|
||||
},
|
||||
},
|
||||
"agentic-workflows": {
|
||||
outputDir: path.resolve(__dirname, "agentic-workflows/shared/compiled"),
|
||||
maxChars: 40000,
|
||||
knowledgeMap: {
|
||||
"build-failure-knowledge": [
|
||||
"binlog-failure-analysis",
|
||||
"check-bin-obj-clash",
|
||||
],
|
||||
"pr-review-knowledge": [
|
||||
"msbuild-antipatterns",
|
||||
"msbuild-modernization",
|
||||
"directory-build-organization",
|
||||
"check-bin-obj-clash",
|
||||
"incremental-build",
|
||||
],
|
||||
"perf-audit-knowledge": [
|
||||
"build-perf-baseline",
|
||||
"build-perf-diagnostics",
|
||||
"incremental-build",
|
||||
"build-parallelism",
|
||||
"eval-performance",
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
function readSkill(skillName) {
|
||||
const skillPath = path.join(SKILLS_DIR, skillName, "SKILL.md");
|
||||
if (!fs.existsSync(skillPath)) {
|
||||
console.warn(` ⚠ Skill not found: ${skillName} (${skillPath})`);
|
||||
return null;
|
||||
}
|
||||
|
||||
let content = fs.readFileSync(skillPath, "utf-8");
|
||||
|
||||
// Strip YAML frontmatter (tolerate both LF and CRLF)
|
||||
const frontmatterMatch = content.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n/);
|
||||
if (frontmatterMatch) {
|
||||
content = content.slice(frontmatterMatch[0].length);
|
||||
}
|
||||
|
||||
return content.trim();
|
||||
}
|
||||
|
||||
function compileKnowledgeFile(outputName, skillNames, outputDir, maxChars) {
|
||||
const ext = ".lock.md";
|
||||
console.log(` Compiling: ${outputName}${ext}`);
|
||||
|
||||
const sections = [];
|
||||
let totalChars = 0;
|
||||
|
||||
const header = `<!-- AUTO-GENERATED — DO NOT EDIT. Regenerate with: node src/dotnet-msbuild/build.js -->\n\n`;
|
||||
totalChars += header.length;
|
||||
|
||||
for (const skillName of skillNames) {
|
||||
const content = readSkill(skillName);
|
||||
if (!content) continue;
|
||||
|
||||
if (totalChars + content.length > maxChars) {
|
||||
console.warn(
|
||||
` ⚠ Truncating ${skillName} — would exceed ${maxChars} char limit`
|
||||
);
|
||||
const remaining = maxChars - totalChars;
|
||||
if (remaining > 500) {
|
||||
sections.push(
|
||||
`## ${skillName}\n\n${content.slice(0, remaining)}\n\n[truncated]`
|
||||
);
|
||||
totalChars += remaining;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
sections.push(content);
|
||||
totalChars += content.length;
|
||||
console.log(
|
||||
` ✓ ${skillName} (${content.length.toLocaleString()} chars)`
|
||||
);
|
||||
}
|
||||
|
||||
const output = header + sections.join("\n\n---\n\n");
|
||||
const outputPath = path.join(outputDir, `${outputName}${ext}`);
|
||||
fs.writeFileSync(outputPath, output, "utf-8");
|
||||
console.log(
|
||||
` → ${outputName}${ext} (${output.length.toLocaleString()} chars total)`
|
||||
);
|
||||
}
|
||||
|
||||
function compileTarget(targetName, config) {
|
||||
console.log(`\n📦 Target: ${targetName}`);
|
||||
console.log(` Output: ${config.outputDir}`);
|
||||
|
||||
fs.mkdirSync(config.outputDir, { recursive: true });
|
||||
|
||||
for (const [outputName, skillNames] of Object.entries(config.knowledgeMap)) {
|
||||
compileKnowledgeFile(
|
||||
outputName,
|
||||
skillNames,
|
||||
config.outputDir,
|
||||
config.maxChars
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`Skills source: ${SKILLS_DIR}`);
|
||||
|
||||
for (const [name, config] of Object.entries(KNOWLEDGE_TARGETS)) {
|
||||
compileTarget(name, config);
|
||||
}
|
||||
|
||||
console.log("\n✅ Build complete.");
|
||||
@@ -0,0 +1,172 @@
|
||||
# Build entry point for the dotnet-msbuild component.
|
||||
# Validates skills and compiles knowledge bundles.
|
||||
# Run: pwsh src/dotnet-msbuild/build.ps1
|
||||
|
||||
Set-StrictMode -Version Latest
|
||||
$ErrorActionPreference = 'Stop'
|
||||
|
||||
$SkillsDir = Join-Path $PSScriptRoot 'skills'
|
||||
$DomainGatePattern = 'Only activate in MSBuild/\.NET build context'
|
||||
|
||||
# ── Step 1: Validate skills ─────────────────────────────────────────
|
||||
|
||||
Write-Host '=== Validating skills ===' -ForegroundColor Cyan
|
||||
Write-Host ''
|
||||
|
||||
$errors = 0
|
||||
|
||||
$skillDirs = Get-ChildItem -Path $SkillsDir -Directory |
|
||||
Where-Object { $_.Name -ne 'shared' }
|
||||
|
||||
foreach ($dir in $skillDirs) {
|
||||
$skillFile = Join-Path $dir.FullName 'SKILL.md'
|
||||
if (-not (Test-Path $skillFile)) { continue }
|
||||
|
||||
$content = Get-Content $skillFile -Raw
|
||||
|
||||
if ($content -notmatch '(?s)^---\s*\r?\n(.*?)\r?\n---') {
|
||||
Write-Host "❌ $($dir.Name): Missing YAML frontmatter" -ForegroundColor Red
|
||||
$errors++
|
||||
continue
|
||||
}
|
||||
|
||||
$frontmatter = $Matches[1]
|
||||
if ($frontmatter -notmatch 'description:\s*"([^"]*)"') {
|
||||
Write-Host "❌ $($dir.Name): Missing description in frontmatter" -ForegroundColor Red
|
||||
$errors++
|
||||
continue
|
||||
}
|
||||
|
||||
$description = $Matches[1]
|
||||
if ($description -notmatch $DomainGatePattern) {
|
||||
Write-Host "❌ $($dir.Name): Description missing domain gate. Must include 'Only activate in MSBuild/.NET build context.'" -ForegroundColor Red
|
||||
$errors++
|
||||
}
|
||||
}
|
||||
|
||||
if ($errors -gt 0) {
|
||||
Write-Host "`n$errors validation error(s) found." -ForegroundColor Red
|
||||
exit 1
|
||||
} else {
|
||||
Write-Host "✅ All $($skillDirs.Count) skills pass validation.`n" -ForegroundColor Green
|
||||
}
|
||||
|
||||
# ── Step 2: Compile knowledge bundles ────────────────────────────────
|
||||
|
||||
Write-Host '=== Compiling knowledge ===' -ForegroundColor Cyan
|
||||
Write-Host ''
|
||||
|
||||
$KnowledgeGroups = [ordered]@{
|
||||
'build-errors' = @(
|
||||
'binlog-failure-analysis'
|
||||
'binlog-generation'
|
||||
'check-bin-obj-clash'
|
||||
'including-generated-files'
|
||||
)
|
||||
'performance' = @(
|
||||
'build-perf-baseline'
|
||||
'build-perf-diagnostics'
|
||||
'incremental-build'
|
||||
'build-parallelism'
|
||||
'eval-performance'
|
||||
)
|
||||
'style-and-modernization' = @(
|
||||
'msbuild-antipatterns'
|
||||
'msbuild-modernization'
|
||||
'directory-build-organization'
|
||||
)
|
||||
}
|
||||
|
||||
$KnowledgeTargets = @{
|
||||
'copilot-extension' = @{
|
||||
OutputDir = Join-Path $PSScriptRoot 'copilot-extension' 'src' 'knowledge'
|
||||
MaxChars = 50000
|
||||
}
|
||||
'agentic-workflows' = @{
|
||||
OutputDir = Join-Path $PSScriptRoot 'agentic-workflows' 'shared' 'compiled'
|
||||
MaxChars = 40000
|
||||
}
|
||||
}
|
||||
|
||||
function Read-Skill([string]$SkillName) {
|
||||
$skillDir = Join-Path $SkillsDir $SkillName
|
||||
$skillPath = Join-Path $skillDir 'SKILL.md'
|
||||
if (-not (Test-Path $skillPath)) {
|
||||
Write-Host " ⚠ Skill not found: $SkillName ($skillPath)" -ForegroundColor Yellow
|
||||
return $null
|
||||
}
|
||||
|
||||
$content = Get-Content $skillPath -Raw
|
||||
|
||||
# Strip YAML frontmatter (tolerate both LF and CRLF)
|
||||
if ($content -match '(?s)^---\r?\n.*?\r?\n---\r?\n(.*)$') {
|
||||
$content = $Matches[1]
|
||||
}
|
||||
|
||||
# Inline linked references: replace [text](references/file.md) with file content
|
||||
$content = [regex]::Replace($content, '\[([^\]]*)\]\((references/[^\)]+\.md)\)', {
|
||||
param($m)
|
||||
$refPath = Join-Path $skillDir $m.Groups[2].Value
|
||||
if (Test-Path $refPath) {
|
||||
$refContent = (Get-Content $refPath -Raw).Trim()
|
||||
return $refContent
|
||||
}
|
||||
return $m.Value
|
||||
})
|
||||
|
||||
return $content.Trim()
|
||||
}
|
||||
|
||||
function Compile-KnowledgeFile([string]$OutputName, [string[]]$SkillNames, [string]$OutputDir, [int]$MaxChars) {
|
||||
$ext = '.lock.md'
|
||||
Write-Host " Compiling: $OutputName$ext"
|
||||
|
||||
$sections = [System.Collections.Generic.List[string]]::new()
|
||||
$totalChars = 0
|
||||
|
||||
$header = "<!-- AUTO-GENERATED — DO NOT EDIT -->`n`n"
|
||||
$totalChars += $header.Length
|
||||
|
||||
foreach ($skillName in $SkillNames) {
|
||||
$content = Read-Skill $skillName
|
||||
if ($null -eq $content) { continue }
|
||||
|
||||
if ($totalChars + $content.Length -gt $MaxChars) {
|
||||
Write-Host " ⚠ Truncating $skillName — would exceed $MaxChars char limit" -ForegroundColor Yellow
|
||||
$remaining = $MaxChars - $totalChars
|
||||
if ($remaining -gt 500) {
|
||||
$sections.Add("## $skillName`n`n$($content.Substring(0, $remaining))`n`n[truncated]")
|
||||
$totalChars += $remaining
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
$sections.Add($content)
|
||||
$totalChars += $content.Length
|
||||
Write-Host " ✓ $skillName ($($content.Length.ToString('N0')) chars)"
|
||||
}
|
||||
|
||||
$output = $header + ($sections -join "`n`n---`n`n")
|
||||
$outputPath = Join-Path $OutputDir "$OutputName$ext"
|
||||
[System.IO.File]::WriteAllText($outputPath, $output)
|
||||
Write-Host " → $OutputName$ext ($($output.Length.ToString('N0')) chars total)"
|
||||
}
|
||||
|
||||
function Compile-Target([string]$TargetName, [hashtable]$Config) {
|
||||
Write-Host "`n📦 Target: $TargetName"
|
||||
Write-Host " Output: $($Config.OutputDir)"
|
||||
|
||||
New-Item -Path $Config.OutputDir -ItemType Directory -Force | Out-Null
|
||||
|
||||
foreach ($entry in $KnowledgeGroups.GetEnumerator()) {
|
||||
Compile-KnowledgeFile -OutputName $entry.Key -SkillNames $entry.Value -OutputDir $Config.OutputDir -MaxChars $Config.MaxChars
|
||||
}
|
||||
}
|
||||
|
||||
Write-Host "Skills source: $SkillsDir"
|
||||
|
||||
foreach ($entry in $KnowledgeTargets.GetEnumerator()) {
|
||||
Compile-Target -TargetName $entry.Key -Config $entry.Value
|
||||
}
|
||||
|
||||
Write-Host "`n✅ Build complete." -ForegroundColor Green
|
||||
@@ -13,7 +13,7 @@ Users invoke it with `@msbuild` in any Copilot Chat:
|
||||
|
||||
## Architecture
|
||||
|
||||
This is a **serverless MVP** (Option A from the [design doc](../../../docs/copilot-extension-design.md)):
|
||||
This is a **serverless MVP**:
|
||||
|
||||
- **Runtime**: Node.js (deployable to Azure Functions, AWS Lambda, Vercel, or any serverless platform)
|
||||
- **Knowledge**: MSBuild skill content compiled into system prompts
|
||||
@@ -44,11 +44,10 @@ copilot-extension/
|
||||
│ ├── index.js # Entry point — handles Copilot webhook
|
||||
│ ├── domain-check.js # MSBuild domain relevance detection
|
||||
│ ├── intent-classifier.js # Routes to the right knowledge area
|
||||
│ └── knowledge/ # Compiled skill content (generated by build.js)
|
||||
│ ├── build-errors.lock.md # From binlog-failure-analysis + check-bin-obj-clash
|
||||
│ ├── performance.lock.md # From build-perf-baseline + build-perf-diagnostics + incremental-build + build-parallelism + eval-performance
|
||||
│ ├── style-guide.lock.md # From msbuild-antipatterns + directory-build-organization + check-bin-obj-clash + including-generated-files
|
||||
│ └── modernization.lock.md # From msbuild-modernization + directory-build-organization
|
||||
│ └── knowledge/ # Compiled skill content (generated by build.ps1)
|
||||
│ ├── build-errors.lock.md # From binlog-failure-analysis + binlog-generation + check-bin-obj-clash + including-generated-files
|
||||
│ ├── performance.lock.md # From build-perf-baseline + build-perf-diagnostics + incremental-build + build-parallelism + eval-performance
|
||||
│ └── style-and-modernization.lock.md # From msbuild-antipatterns + msbuild-modernization + directory-build-organization
|
||||
└── app.yml # GitHub App manifest for registration
|
||||
```
|
||||
|
||||
@@ -67,7 +66,7 @@ Extract and compile skill content into optimized knowledge files:
|
||||
|
||||
```bash
|
||||
# From repo root
|
||||
node src/dotnet-msbuild/build.js
|
||||
pwsh src/dotnet-msbuild/build.ps1
|
||||
|
||||
# Or from copilot-extension/
|
||||
npm run compile-knowledge
|
||||
@@ -118,7 +117,7 @@ npm start
|
||||
npm test
|
||||
|
||||
# Recompile knowledge after skill changes
|
||||
node src/dotnet-msbuild/build.js
|
||||
pwsh src/dotnet-msbuild/build.ps1
|
||||
```
|
||||
|
||||
## How It Works
|
||||
@@ -134,12 +133,11 @@ node src/dotnet-msbuild/build.js
|
||||
|
||||
## Extending
|
||||
|
||||
- **Add knowledge**: Update skills in `src/dotnet-msbuild/skills/`, then run `node src/dotnet-msbuild/build.js`
|
||||
- **Add knowledge**: Update skills in `src/dotnet-msbuild/skills/`, then run `pwsh src/dotnet-msbuild/build.ps1`
|
||||
- **Add intents**: Edit `src/intent-classifier.js` to recognize new categories
|
||||
- **Add tools**: For v2, add MCP server integration for binlog analysis
|
||||
|
||||
## Related
|
||||
|
||||
- [Design Document](../../../docs/copilot-extension-design.md) — Full design rationale
|
||||
- [Skills Repository](../) — Source knowledge base
|
||||
- [GitHub Copilot Extensions Docs](https://docs.github.com/copilot/building-copilot-extensions)
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
"main": "src/index.js",
|
||||
"scripts": {
|
||||
"start": "node src/index.js",
|
||||
"compile-knowledge": "node ../build.js",
|
||||
"compile-knowledge": "pwsh ../build.ps1",
|
||||
"test": "node --test src/**/*.test.js"
|
||||
},
|
||||
"keywords": [
|
||||
|
||||
@@ -11,11 +11,11 @@ const INTENTS = {
|
||||
description: "Build performance optimization",
|
||||
},
|
||||
STYLE_REVIEW: {
|
||||
knowledgeKey: "style-guide",
|
||||
knowledgeKey: "style-and-modernization",
|
||||
description: "Project file quality and anti-patterns",
|
||||
},
|
||||
MODERNIZATION: {
|
||||
knowledgeKey: "modernization",
|
||||
knowledgeKey: "style-and-modernization",
|
||||
description: "Legacy project modernization",
|
||||
},
|
||||
GENERAL: {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<!-- AUTO-GENERATED — DO NOT EDIT. Regenerate with: node src/dotnet-msbuild/build.js -->
|
||||
<!-- AUTO-GENERATED — DO NOT EDIT -->
|
||||
|
||||
# Analyzing MSBuild Failures with Binary Logs
|
||||
|
||||
@@ -285,6 +285,97 @@ When binlog analysis reveals these patterns, here's the fast path:
|
||||
|
||||
---
|
||||
|
||||
# Generate Binary Logs
|
||||
|
||||
**Pass the `/bl` switch when running any MSBuild-based command.** This is a non-negotiable requirement for all .NET builds.
|
||||
|
||||
## Commands That Require /bl
|
||||
|
||||
You MUST add the `/bl:{}` flag to:
|
||||
- `dotnet build`
|
||||
- `dotnet test`
|
||||
- `dotnet pack`
|
||||
- `dotnet publish`
|
||||
- `dotnet restore`
|
||||
- `msbuild` or `msbuild.exe`
|
||||
- Any other command that invokes MSBuild
|
||||
|
||||
## Preferred: Use `{}` for Automatic Unique Names
|
||||
|
||||
> **Note:** The `{}` placeholder requires MSBuild 17.8+ / .NET 8 SDK or later.
|
||||
|
||||
The `{}` placeholder in the binlog filename is replaced by MSBuild with a unique identifier, guaranteeing no two builds ever overwrite each other — without needing to track or check existing files.
|
||||
|
||||
```bash
|
||||
# Every invocation produces a distinct file automatically
|
||||
dotnet build /bl:{}
|
||||
dotnet test /bl:{}
|
||||
dotnet build --configuration Release /bl:{}
|
||||
```
|
||||
|
||||
**PowerShell requires escaping the braces:**
|
||||
|
||||
```powershell
|
||||
# PowerShell: escape { } as {{ }}
|
||||
dotnet build -bl:{{}}
|
||||
dotnet test -bl:{{}}
|
||||
```
|
||||
|
||||
## Why This Matters
|
||||
|
||||
1. **Unique names prevent overwrites** - You can always go back and analyze previous builds
|
||||
2. **Failure analysis** - When a build fails, the binlog is already there for immediate analysis
|
||||
3. **Comparison** - You can compare builds before and after changes
|
||||
4. **No re-running builds** - You never need to re-run a failed build just to generate a binlog
|
||||
|
||||
## Examples
|
||||
|
||||
```bash
|
||||
# ✅ CORRECT - {} generates a unique name automatically (bash/cmd)
|
||||
dotnet build /bl:{}
|
||||
dotnet test /bl:{}
|
||||
|
||||
# ✅ CORRECT - PowerShell escaping
|
||||
dotnet build -bl:{{}}
|
||||
dotnet test -bl:{{}}
|
||||
|
||||
# ❌ WRONG - Missing /bl flag entirely
|
||||
dotnet build
|
||||
dotnet test
|
||||
|
||||
# ❌ WRONG - No filename (overwrites the same msbuild.binlog every time)
|
||||
dotnet build /bl
|
||||
dotnet build /bl
|
||||
```
|
||||
|
||||
## When a Specific Filename Is Required
|
||||
|
||||
If the binlog filename needs to be known upfront (e.g., for CI artifact upload), or if `{}` is not available in the installed MSBuild version, pick a name that won't collide with existing files:
|
||||
|
||||
1. Check for existing `*.binlog` files in the directory
|
||||
2. Choose a name not already taken (e.g., by incrementing a counter from the highest existing number)
|
||||
|
||||
```bash
|
||||
# Example: directory contains 3.binlog — use 4.binlog
|
||||
dotnet build /bl:4.binlog
|
||||
```
|
||||
|
||||
## Cleaning the Repository
|
||||
|
||||
When cleaning the repository with `git clean`, **always exclude binlog files** to preserve your build history:
|
||||
|
||||
```bash
|
||||
# ✅ CORRECT - Exclude binlog files from cleaning
|
||||
git clean -fdx -e "*.binlog"
|
||||
|
||||
# ❌ WRONG - This deletes binlog files (they're usually in .gitignore)
|
||||
git clean -fdx
|
||||
```
|
||||
|
||||
This is especially important when iterating on build fixes - you need the binlogs to analyze what changed between builds.
|
||||
|
||||
---
|
||||
|
||||
# Detecting OutputPath and IntermediateOutputPath Clashes
|
||||
|
||||
## Overview
|
||||
@@ -620,4 +711,136 @@ When multiple evaluations share an output path, compare these global properties
|
||||
|
||||
## Testing Fixes
|
||||
|
||||
After making changes to fix path clashes, clean and rebuild to verify. See the `binlog-generation` skill's "Cleaning the Repository" section on how to clean the repository while preserving binlog files.
|
||||
After making changes to fix path clashes, clean and rebuild to verify. See the `binlog-generation` skill's "Cleaning the Repository" section on how to clean the repository while preserving binlog files.
|
||||
|
||||
---
|
||||
|
||||
# Including Generated Files Into Your Build
|
||||
|
||||
## Overview
|
||||
|
||||
Files generated during the build are generally ignored by the build process. This leads to confusing results such as:
|
||||
- Generated files not being included in the output directory
|
||||
- Generated source files not being compiled
|
||||
- Globs not capturing files created during the build
|
||||
|
||||
This happens because of how MSBuild's build phases work.
|
||||
|
||||
## Quick Takeaway
|
||||
|
||||
For code files generated during the build - we need to add those to `Compile` and `FileWrites` item groups within the target generating the file(s):
|
||||
|
||||
```xml
|
||||
<ItemGroup>
|
||||
<Compile Include="$(GeneratedFilePath)" />
|
||||
<FileWrites Include="$(GeneratedFilePath)" />
|
||||
</ItemGroup>
|
||||
```
|
||||
|
||||
The target generating the file(s) should be hooked before CoreCompile and BeforeCompile targets - `BeforeTargets="CoreCompile;BeforeCompile"`
|
||||
|
||||
## Why Generated Files Are Ignored
|
||||
|
||||
For detailed explanation, see [How MSBuild Builds Projects](https://docs.microsoft.com/visualstudio/msbuild/build-process-overview).
|
||||
|
||||
### Evaluation Phase
|
||||
|
||||
MSBuild reads your project, imports everything, creates Properties, expands globs for Items **outside of Targets**, and sets up the build process.
|
||||
|
||||
### Execution Phase
|
||||
|
||||
MSBuild runs Targets & Tasks with the provided Properties & Items to perform the build.
|
||||
|
||||
**Key Takeaway:** Files generated during execution don't exist during evaluation, therefore they aren't found. This particularly affects files that are globbed by default, such as source files (`.cs`).
|
||||
|
||||
## Solution: Manually Add Generated Files
|
||||
|
||||
When files are generated during the build, manually add them into the build process. The approach depends on the type of file being generated.
|
||||
|
||||
### Use `$(IntermediateOutputPath)` for Generated File Location
|
||||
|
||||
Always use `$(IntermediateOutputPath)` as the base directory for generated files. **Do not** hardcode `obj\` or construct the intermediary path manually (e.g., `obj\$(Configuration)\$(TargetFramework)\`). The intermediate output path can be redirected to a different location in some build configurations (e.g., shared output directories, CI environments). Using `$(IntermediateOutputPath)` ensures your target works correctly regardless of the actual path.
|
||||
|
||||
### Always Add Generated Files to `FileWrites`
|
||||
|
||||
Every generated file should be added to the `FileWrites` item group. This ensures that MSBuild's `Clean` target properly removes your generated files. Without this, generated files will accumulate as stale artifacts across builds.
|
||||
|
||||
```xml
|
||||
<ItemGroup>
|
||||
<FileWrites Include="$(IntermediateOutputPath)my-generated-file.xyz" />
|
||||
</ItemGroup>
|
||||
```
|
||||
|
||||
### Basic Pattern (Non-Code Files)
|
||||
|
||||
For generated files that need to be copied to output (config files, data files, etc.), add them to `Content` or `None` items before `BeforeBuild`:
|
||||
|
||||
```xml
|
||||
<Target Name="IncludeGeneratedFiles" BeforeTargets="BeforeBuild">
|
||||
|
||||
<!-- Your logic that generates files goes here -->
|
||||
|
||||
<ItemGroup>
|
||||
<None Include="$(IntermediateOutputPath)my-generated-file.xyz" CopyToOutputDirectory="PreserveNewest"/>
|
||||
|
||||
<!-- Capture all files of a certain type with a glob -->
|
||||
<None Include="$(IntermediateOutputPath)generated\*.xyz" CopyToOutputDirectory="PreserveNewest"/>
|
||||
|
||||
<!-- Register generated files for proper cleanup -->
|
||||
<FileWrites Include="$(IntermediateOutputPath)my-generated-file.xyz" />
|
||||
<FileWrites Include="$(IntermediateOutputPath)generated\*.xyz" />
|
||||
</ItemGroup>
|
||||
</Target>
|
||||
```
|
||||
|
||||
### For Generated Source Files (Code That Needs Compilation)
|
||||
|
||||
If you're generating `.cs` files that need to be compiled, use **`BeforeTargets="CoreCompile;BeforeCompile"`**. This is the correct timing for adding `Compile` items — it runs late enough that the file generation has occurred, but before the compiler runs. Using `BeforeBuild` is too early for some scenarios and may not work reliably with all SDK features.
|
||||
|
||||
```xml
|
||||
<Target Name="IncludeGeneratedSourceFiles" BeforeTargets="CoreCompile;BeforeCompile">
|
||||
<PropertyGroup>
|
||||
<GeneratedCodeDir>$(IntermediateOutputPath)Generated\</GeneratedCodeDir>
|
||||
<GeneratedFilePath>$(GeneratedCodeDir)MyGeneratedFile.cs</GeneratedFilePath>
|
||||
</PropertyGroup>
|
||||
|
||||
<MakeDir Directories="$(GeneratedCodeDir)" />
|
||||
|
||||
<!-- Your logic that generates the .cs file goes here -->
|
||||
|
||||
<ItemGroup>
|
||||
<Compile Include="$(GeneratedFilePath)" />
|
||||
<FileWrites Include="$(GeneratedFilePath)" />
|
||||
</ItemGroup>
|
||||
</Target>
|
||||
```
|
||||
|
||||
Note: Specifying both `CoreCompile` and `BeforeCompile` ensures the target runs before whichever target comes first, providing robust ordering regardless of customizations in the build.
|
||||
|
||||
## Target Timing
|
||||
|
||||
Choose the `BeforeTargets` value based on the type of file being generated:
|
||||
|
||||
- **`BeforeTargets="BeforeBuild"`** — For non-code files added to `None` or `Content`. Runs early enough for copy-to-output scenarios.
|
||||
- **`BeforeTargets="CoreCompile;BeforeCompile"`** — For generated source files added to `Compile`. Ensures the file is included before the compiler runs.
|
||||
- **`BeforeTargets="AssignTargetPaths"`** — The "final stop" before `None` and `Content` items (among others) are transformed into new items. Use as a fallback if `BeforeBuild` is too early.
|
||||
|
||||
## Globbing Behavior
|
||||
|
||||
Globs behave according to **when** the glob took place:
|
||||
|
||||
| Glob Location | Files Captured |
|
||||
|---------------|----------------|
|
||||
| Outside of a target | Only files visible during Evaluation phase (before build starts) |
|
||||
| Inside of a target | Files visible when the target runs (can capture generated files if timed correctly) |
|
||||
|
||||
This is why the solution places the `<ItemGroup>` inside a `<Target>` - the glob runs during execution when the generated files exist.
|
||||
|
||||
## Relevant Links
|
||||
|
||||
- [How MSBuild Builds Projects](https://docs.microsoft.com/visualstudio/msbuild/build-process-overview)
|
||||
- [Evaluation Phase](https://docs.microsoft.com/visualstudio/msbuild/build-process-overview#evaluation-phase)
|
||||
- [Execution Phase](https://docs.microsoft.com/visualstudio/msbuild/build-process-overview#execution-phase)
|
||||
- [Common Item Types](https://docs.microsoft.com/visualstudio/msbuild/common-msbuild-project-items)
|
||||
- [How the SDK imports items by default](https://github.com/dotnet/sdk/blob/main/src/Tasks/Microsoft.NET.Build.Tasks/targets/Microsoft.NET.Sdk.DefaultItems.props)
|
||||
- [Official docs: Handle generated files](https://learn.microsoft.com/visualstudio/msbuild/customize-your-build#handle-generated-files)
|
||||
@@ -1,956 +0,0 @@
|
||||
<!-- AUTO-GENERATED — DO NOT EDIT. Regenerate with: node src/dotnet-msbuild/build.js -->
|
||||
|
||||
# MSBuild Modernization: Legacy to SDK-style Migration
|
||||
|
||||
## Identifying Legacy vs SDK-style Projects
|
||||
|
||||
**Legacy indicators:**
|
||||
|
||||
- `<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />`
|
||||
- Explicit file lists (`<Compile Include="..." />` for every `.cs` file)
|
||||
- `ToolsVersion` attribute on `<Project>` element
|
||||
- `packages.config` file present
|
||||
- `Properties\AssemblyInfo.cs` with assembly-level attributes
|
||||
|
||||
**SDK-style indicators:**
|
||||
|
||||
- `<Project Sdk="Microsoft.NET.Sdk">` attribute on root element
|
||||
- Minimal content — a simple project may be 10–15 lines
|
||||
- No explicit file includes (implicit globbing)
|
||||
- `<PackageReference>` items instead of `packages.config`
|
||||
|
||||
**Quick check:** if a `.csproj` is more than 50 lines for a simple class library or console app, it is likely legacy format.
|
||||
|
||||
```xml
|
||||
<!-- Legacy: ~80+ lines for a simple library -->
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" />
|
||||
<PropertyGroup>
|
||||
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
|
||||
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
|
||||
<OutputType>Library</OutputType>
|
||||
<RootNamespace>MyLibrary</RootNamespace>
|
||||
<AssemblyName>MyLibrary</AssemblyName>
|
||||
<TargetFrameworkVersion>v4.7.2</TargetFrameworkVersion>
|
||||
<FileAlignment>512</FileAlignment>
|
||||
<Deterministic>true</Deterministic>
|
||||
</PropertyGroup>
|
||||
<!-- ... 60+ more lines ... -->
|
||||
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
|
||||
</Project>
|
||||
```
|
||||
|
||||
```xml
|
||||
<!-- SDK-style: ~8 lines for the same library -->
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net472</TargetFramework>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
```
|
||||
|
||||
## Migration Checklist: Legacy → SDK-style
|
||||
|
||||
### Step 1: Replace Project Root Element
|
||||
|
||||
**BEFORE:**
|
||||
|
||||
```xml
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props"
|
||||
Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
|
||||
<!-- ... project content ... -->
|
||||
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
|
||||
</Project>
|
||||
```
|
||||
|
||||
**AFTER:**
|
||||
|
||||
```xml
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<!-- ... project content ... -->
|
||||
</Project>
|
||||
```
|
||||
|
||||
Remove the XML declaration, `ToolsVersion`, `xmlns`, and both `<Import>` lines. The `Sdk` attribute replaces all of them.
|
||||
|
||||
### Step 2: Set TargetFramework
|
||||
|
||||
**BEFORE:**
|
||||
|
||||
```xml
|
||||
<PropertyGroup>
|
||||
<TargetFrameworkVersion>v4.7.2</TargetFrameworkVersion>
|
||||
</PropertyGroup>
|
||||
```
|
||||
|
||||
**AFTER:**
|
||||
|
||||
```xml
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net472</TargetFramework>
|
||||
</PropertyGroup>
|
||||
```
|
||||
|
||||
**TFM mapping table:**
|
||||
|
||||
| Legacy `TargetFrameworkVersion` | SDK-style `TargetFramework` |
|
||||
|---------------------------------|-----------------------------|
|
||||
| `v4.6.1` | `net461` |
|
||||
| `v4.7.2` | `net472` |
|
||||
| `v4.8` | `net48` |
|
||||
| (migrating to .NET 6) | `net6.0` |
|
||||
| (migrating to .NET 8) | `net8.0` |
|
||||
|
||||
### Step 3: Remove Explicit File Includes
|
||||
|
||||
**BEFORE:**
|
||||
|
||||
```xml
|
||||
<ItemGroup>
|
||||
<Compile Include="Controllers\HomeController.cs" />
|
||||
<Compile Include="Models\User.cs" />
|
||||
<Compile Include="Models\Order.cs" />
|
||||
<Compile Include="Services\AuthService.cs" />
|
||||
<Compile Include="Services\OrderService.cs" />
|
||||
<Compile Include="Properties\AssemblyInfo.cs" />
|
||||
<!-- ... 50+ more lines ... -->
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Content Include="Views\Home\Index.cshtml" />
|
||||
<Content Include="Views\Shared\_Layout.cshtml" />
|
||||
<!-- ... more content files ... -->
|
||||
</ItemGroup>
|
||||
```
|
||||
|
||||
**AFTER:**
|
||||
|
||||
Delete all of these `<Compile>` and `<Content>` item groups entirely. SDK-style projects include them automatically via implicit globbing.
|
||||
|
||||
**Exception:** keep explicit entries only for files that need special metadata or reside outside the project directory:
|
||||
|
||||
```xml
|
||||
<ItemGroup>
|
||||
<Content Include="..\shared\config.json" Link="config.json" CopyToOutputDirectory="PreserveNewest" />
|
||||
</ItemGroup>
|
||||
```
|
||||
|
||||
### Step 4: Remove AssemblyInfo.cs
|
||||
|
||||
**BEFORE** (`Properties\AssemblyInfo.cs`):
|
||||
|
||||
```csharp
|
||||
using System.Reflection;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
[assembly: AssemblyTitle("MyLibrary")]
|
||||
[assembly: AssemblyDescription("A useful library")]
|
||||
[assembly: AssemblyCompany("Contoso")]
|
||||
[assembly: AssemblyProduct("MyLibrary")]
|
||||
[assembly: AssemblyCopyright("Copyright © Contoso 2024")]
|
||||
[assembly: ComVisible(false)]
|
||||
[assembly: Guid("...")]
|
||||
[assembly: AssemblyVersion("1.2.0.0")]
|
||||
[assembly: AssemblyFileVersion("1.2.0.0")]
|
||||
```
|
||||
|
||||
**AFTER** (in `.csproj`):
|
||||
|
||||
```xml
|
||||
<PropertyGroup>
|
||||
<AssemblyTitle>MyLibrary</AssemblyTitle>
|
||||
<Description>A useful library</Description>
|
||||
<Company>Contoso</Company>
|
||||
<Product>MyLibrary</Product>
|
||||
<Copyright>Copyright © Contoso 2024</Copyright>
|
||||
<Version>1.2.0</Version>
|
||||
</PropertyGroup>
|
||||
```
|
||||
|
||||
Delete `Properties\AssemblyInfo.cs` — the SDK auto-generates assembly attributes from these properties.
|
||||
|
||||
**Alternative:** if you prefer to keep `AssemblyInfo.cs`, disable auto-generation:
|
||||
|
||||
```xml
|
||||
<PropertyGroup>
|
||||
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
|
||||
</PropertyGroup>
|
||||
```
|
||||
|
||||
### Step 5: Migrate packages.config → PackageReference
|
||||
|
||||
**BEFORE** (`packages.config`):
|
||||
|
||||
```xml
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<packages>
|
||||
<package id="Newtonsoft.Json" version="13.0.3" targetFramework="net472" />
|
||||
<package id="Serilog" version="3.1.1" targetFramework="net472" />
|
||||
<package id="Microsoft.Extensions.DependencyInjection" version="8.0.0" targetFramework="net472" />
|
||||
</packages>
|
||||
```
|
||||
|
||||
**AFTER** (in `.csproj`):
|
||||
|
||||
```xml
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
||||
<PackageReference Include="Serilog" Version="3.1.1" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.0" />
|
||||
</ItemGroup>
|
||||
```
|
||||
|
||||
Delete `packages.config` after migration.
|
||||
|
||||
**Migration options:**
|
||||
|
||||
- **Visual Studio:** right-click `packages.config` → *Migrate packages.config to PackageReference*
|
||||
- **CLI:** `dotnet migrate-packages-config` or manual conversion
|
||||
- **Binding redirects:** SDK-style projects auto-generate binding redirects — remove the `<runtime>` section from `app.config` if present
|
||||
|
||||
### Step 6: Remove Unnecessary Boilerplate
|
||||
|
||||
Delete all of the following — the SDK provides sensible defaults:
|
||||
|
||||
```xml
|
||||
<!-- DELETE: SDK imports (replaced by Sdk attribute) -->
|
||||
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" ... />
|
||||
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
|
||||
|
||||
<!-- DELETE: default Configuration/Platform (SDK provides these) -->
|
||||
<PropertyGroup>
|
||||
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
|
||||
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
|
||||
<ProjectGuid>{...}</ProjectGuid>
|
||||
<OutputType>Library</OutputType> <!-- keep only if not Library -->
|
||||
<AppDesignerFolder>Properties</AppDesignerFolder>
|
||||
<FileAlignment>512</FileAlignment>
|
||||
<AutoGenerateBindingRedirects>true</AutoGenerateBindingRedirects>
|
||||
<Deterministic>true</Deterministic>
|
||||
</PropertyGroup>
|
||||
|
||||
<!-- DELETE: standard Debug/Release configurations (SDK defaults match) -->
|
||||
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
|
||||
<DebugSymbols>true</DebugSymbols>
|
||||
<DebugType>full</DebugType>
|
||||
<Optimize>false</Optimize>
|
||||
<OutputPath>bin\Debug\</OutputPath>
|
||||
<DefineConstants>DEBUG;TRACE</DefineConstants>
|
||||
<ErrorReport>prompt</ErrorReport>
|
||||
<WarningLevel>4</WarningLevel>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
|
||||
<DebugType>pdbonly</DebugType>
|
||||
<Optimize>true</Optimize>
|
||||
<OutputPath>bin\Release\</OutputPath>
|
||||
<DefineConstants>TRACE</DefineConstants>
|
||||
<ErrorReport>prompt</ErrorReport>
|
||||
<WarningLevel>4</WarningLevel>
|
||||
</PropertyGroup>
|
||||
|
||||
<!-- DELETE: framework assembly references (implicit in SDK) -->
|
||||
<ItemGroup>
|
||||
<Reference Include="System" />
|
||||
<Reference Include="System.Core" />
|
||||
<Reference Include="System.Data" />
|
||||
<Reference Include="System.Xml" />
|
||||
<Reference Include="System.Xml.Linq" />
|
||||
<Reference Include="Microsoft.CSharp" />
|
||||
</ItemGroup>
|
||||
|
||||
<!-- DELETE: packages.config reference -->
|
||||
<None Include="packages.config" />
|
||||
|
||||
<!-- DELETE: designer service entries -->
|
||||
<Service Include="{508349B6-6B84-11D3-8410-00C04F8EF8E0}" />
|
||||
```
|
||||
|
||||
**Keep** only properties that differ from SDK defaults (e.g., `<OutputType>Exe</OutputType>`, `<RootNamespace>` if it differs from the assembly name, custom `<DefineConstants>`).
|
||||
|
||||
### Step 7: Enable Modern Features
|
||||
|
||||
After migration, consider enabling modern C# features:
|
||||
|
||||
```xml
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<LangVersion>latest</LangVersion>
|
||||
</PropertyGroup>
|
||||
```
|
||||
|
||||
- `<Nullable>enable</Nullable>` — enables nullable reference type analysis
|
||||
- `<ImplicitUsings>enable</ImplicitUsings>` — auto-imports common namespaces (.NET 6+)
|
||||
- `<LangVersion>latest</LangVersion>` — uses the latest C# language version (or specify e.g. `12.0`)
|
||||
|
||||
## Complete Before/After Example
|
||||
|
||||
**BEFORE** (legacy — 65 lines):
|
||||
|
||||
```xml
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props"
|
||||
Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
|
||||
<PropertyGroup>
|
||||
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
|
||||
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
|
||||
<ProjectGuid>{12345678-1234-1234-1234-123456789ABC}</ProjectGuid>
|
||||
<OutputType>Library</OutputType>
|
||||
<AppDesignerFolder>Properties</AppDesignerFolder>
|
||||
<RootNamespace>MyLibrary</RootNamespace>
|
||||
<AssemblyName>MyLibrary</AssemblyName>
|
||||
<TargetFrameworkVersion>v4.7.2</TargetFrameworkVersion>
|
||||
<FileAlignment>512</FileAlignment>
|
||||
<Deterministic>true</Deterministic>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
|
||||
<DebugSymbols>true</DebugSymbols>
|
||||
<DebugType>full</DebugType>
|
||||
<Optimize>false</Optimize>
|
||||
<OutputPath>bin\Debug\</OutputPath>
|
||||
<DefineConstants>DEBUG;TRACE</DefineConstants>
|
||||
<ErrorReport>prompt</ErrorReport>
|
||||
<WarningLevel>4</WarningLevel>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
|
||||
<DebugType>pdbonly</DebugType>
|
||||
<Optimize>true</Optimize>
|
||||
<OutputPath>bin\Release\</OutputPath>
|
||||
<DefineConstants>TRACE</DefineConstants>
|
||||
<ErrorReport>prompt</ErrorReport>
|
||||
<WarningLevel>4</WarningLevel>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<Reference Include="System" />
|
||||
<Reference Include="System.Core" />
|
||||
<Reference Include="System.Xml.Linq" />
|
||||
<Reference Include="Microsoft.CSharp" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Compile Include="Models\User.cs" />
|
||||
<Compile Include="Models\Order.cs" />
|
||||
<Compile Include="Services\UserService.cs" />
|
||||
<Compile Include="Services\OrderService.cs" />
|
||||
<Compile Include="Helpers\StringExtensions.cs" />
|
||||
<Compile Include="Properties\AssemblyInfo.cs" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<None Include="packages.config" />
|
||||
</ItemGroup>
|
||||
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
|
||||
</Project>
|
||||
```
|
||||
|
||||
**AFTER** (SDK-style — 11 lines):
|
||||
|
||||
```xml
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net472</TargetFramework>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
||||
<PackageReference Include="Serilog" Version="3.1.1" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
```
|
||||
|
||||
## Common Migration Issues
|
||||
|
||||
**Embedded resources:** files not in a standard location may need explicit includes:
|
||||
|
||||
```xml
|
||||
<ItemGroup>
|
||||
<EmbeddedResource Include="..\shared\Schemas\*.xsd" LinkBase="Schemas" />
|
||||
</ItemGroup>
|
||||
```
|
||||
|
||||
**Content files with CopyToOutputDirectory:** these still need explicit entries:
|
||||
|
||||
```xml
|
||||
<ItemGroup>
|
||||
<Content Include="appsettings.json" CopyToOutputDirectory="PreserveNewest" />
|
||||
<None Include="scripts\*.sql" CopyToOutputDirectory="PreserveNewest" />
|
||||
</ItemGroup>
|
||||
```
|
||||
|
||||
**Multi-targeting:** change the element name from singular to plural:
|
||||
|
||||
```xml
|
||||
<!-- Single target -->
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
|
||||
<!-- Multiple targets -->
|
||||
<TargetFrameworks>net472;net8.0</TargetFrameworks>
|
||||
```
|
||||
|
||||
**WPF/WinForms projects:** use the appropriate SDK or properties:
|
||||
|
||||
```xml
|
||||
<!-- Option A: WindowsDesktop SDK -->
|
||||
<Project Sdk="Microsoft.NET.Sdk.WindowsDesktop">
|
||||
|
||||
<!-- Option B: properties in standard SDK (preferred for .NET 5+) -->
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<UseWPF>true</UseWPF>
|
||||
<!-- or -->
|
||||
<UseWindowsForms>true</UseWindowsForms>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
```
|
||||
|
||||
**Test projects:** use the standard SDK with test framework packages:
|
||||
|
||||
```xml
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<IsPackable>false</IsPackable>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.9.0" />
|
||||
<PackageReference Include="xunit" Version="2.7.0" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.5.7" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
```
|
||||
|
||||
## Central Package Management Migration
|
||||
|
||||
Centralizes NuGet version management across a multi-project solution. See [https://learn.microsoft.com/en-us/nuget/consume-packages/central-package-management](https://learn.microsoft.com/en-us/nuget/consume-packages/central-package-management) for details.
|
||||
|
||||
**Step 1:** Create `Directory.Packages.props` at the repository root with `<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>` and `<PackageVersion>` items for all packages.
|
||||
|
||||
**Step 2:** Remove `Version` from each project's `PackageReference`:
|
||||
|
||||
```xml
|
||||
<!-- BEFORE -->
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
||||
|
||||
<!-- AFTER -->
|
||||
<PackageReference Include="Newtonsoft.Json" />
|
||||
```
|
||||
|
||||
## Directory.Build Consolidation
|
||||
|
||||
Identify properties repeated across multiple `.csproj` files and move them to shared files.
|
||||
|
||||
**`Directory.Build.props`** (for properties — placed at repo or src root):
|
||||
|
||||
```xml
|
||||
<Project>
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||
<Company>Contoso</Company>
|
||||
<Copyright>Copyright © Contoso 2024</Copyright>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
```
|
||||
|
||||
**`Directory.Build.targets`** (for targets/tasks — placed at repo or src root):
|
||||
|
||||
```xml
|
||||
<Project>
|
||||
<Target Name="PrintBuildInfo" AfterTargets="Build">
|
||||
<Message Importance="High" Text="Built $(AssemblyName) → $(TargetPath)" />
|
||||
</Target>
|
||||
</Project>
|
||||
```
|
||||
|
||||
**Keep in individual `.csproj` files** only what is project-specific:
|
||||
|
||||
```xml
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<AssemblyName>MyApp</AssemblyName>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Serilog" />
|
||||
<ProjectReference Include="..\MyLibrary\MyLibrary.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
```
|
||||
|
||||
## Tools and Automation
|
||||
|
||||
| Tool | Usage |
|
||||
|------|-------|
|
||||
| `dotnet try-convert` | Automated legacy-to-SDK conversion. Install: `dotnet tool install -g try-convert` |
|
||||
| .NET Upgrade Assistant | Full migration including API changes. Install: `dotnet tool install -g upgrade-assistant` |
|
||||
| Visual Studio | Right-click `packages.config` → *Migrate packages.config to PackageReference* |
|
||||
| Manual migration | Often cleanest for simple projects — follow the checklist above |
|
||||
|
||||
**Recommended approach:**
|
||||
|
||||
1. Run `try-convert` for a first pass
|
||||
2. Review and clean up the output manually
|
||||
3. Build and fix any issues
|
||||
4. Enable modern features (nullable, implicit usings)
|
||||
5. Consolidate shared settings into `Directory.Build.props`
|
||||
|
||||
---
|
||||
|
||||
# Organizing Build Infrastructure with Directory.Build Files
|
||||
|
||||
## Directory.Build.props vs Directory.Build.targets
|
||||
|
||||
Understanding which file to use is critical. They differ in **when** they are imported during evaluation:
|
||||
|
||||
**Evaluation order:**
|
||||
|
||||
```
|
||||
Directory.Build.props → SDK .props → YourProject.csproj → SDK .targets → Directory.Build.targets
|
||||
```
|
||||
|
||||
| Use `.props` for | Use `.targets` for |
|
||||
|---|---|
|
||||
| Setting property defaults | Custom build targets |
|
||||
| Common item definitions | Late-bound property overrides |
|
||||
| Properties projects can override | Post-build steps |
|
||||
| Assembly/package metadata | Conditional logic on final values |
|
||||
| Analyzer PackageReferences | Targets that depend on SDK-defined properties |
|
||||
|
||||
**Rule of thumb:** Properties and items go in `.props`. Custom targets and late-bound logic go in `.targets`.
|
||||
|
||||
Because `.props` is imported before the project file, the project can override any value set there. Because `.targets` is imported after everything, it gets the final say—but projects cannot override `.targets` values.
|
||||
|
||||
### ⚠️ Critical: TargetFramework Availability in .props vs .targets
|
||||
|
||||
**Property conditions on `$(TargetFramework)` in `.props` files silently fail for single-targeting projects** — the property is empty during `.props` evaluation. Move TFM-conditional properties to `.targets` instead. ItemGroup and Target conditions are not affected.
|
||||
|
||||
See the AP-21 section in the [msbuild-antipatterns skill](../msbuild-antipatterns/SKILL.md) for the full explanation.
|
||||
|
||||
## Directory.Build.props
|
||||
|
||||
### What to Put Here
|
||||
|
||||
**Output settings:**
|
||||
|
||||
```xml
|
||||
<PropertyGroup>
|
||||
<!-- Use with caution — see bin/obj clash skill for risks -->
|
||||
<BaseOutputPath>$(MSBuildThisFileDirectory)artifacts\bin\</BaseOutputPath>
|
||||
<BaseIntermediateOutputPath>$(MSBuildThisFileDirectory)artifacts\obj\$(MSBuildProjectName)\</BaseIntermediateOutputPath>
|
||||
</PropertyGroup>
|
||||
```
|
||||
|
||||
**Language settings:**
|
||||
|
||||
```xml
|
||||
<PropertyGroup>
|
||||
<LangVersion>latest</LangVersion>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<AnalysisLevel>latest-recommended</AnalysisLevel>
|
||||
</PropertyGroup>
|
||||
```
|
||||
|
||||
**Assembly and package metadata:**
|
||||
|
||||
```xml
|
||||
<PropertyGroup>
|
||||
<Company>Contoso</Company>
|
||||
<Authors>Contoso Engineering</Authors>
|
||||
<Copyright>Copyright © Contoso $(CurrentYear)</Copyright>
|
||||
<Product>Contoso Platform</Product>
|
||||
<PackageLicenseExpression>MIT</PackageLicenseExpression>
|
||||
<RepositoryUrl>https://github.com/contoso/platform</RepositoryUrl>
|
||||
<PackageProjectUrl>https://github.com/contoso/platform</PackageProjectUrl>
|
||||
</PropertyGroup>
|
||||
```
|
||||
|
||||
**Build behavior and warnings:**
|
||||
|
||||
```xml
|
||||
<PropertyGroup>
|
||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||
<WarningsAsErrors />
|
||||
<NoWarn>$(NoWarn);CS1591</NoWarn>
|
||||
</PropertyGroup>
|
||||
```
|
||||
|
||||
**Code analysis:**
|
||||
|
||||
```xml
|
||||
<PropertyGroup>
|
||||
<EnforceCodeStyleInBuild>true</EnforceCodeStyleInBuild>
|
||||
<EnableNETAnalyzers>true</EnableNETAnalyzers>
|
||||
</PropertyGroup>
|
||||
```
|
||||
|
||||
**Common analyzer PackageReferences (apply to all projects):**
|
||||
|
||||
```xml
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="3.3.4">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
|
||||
</PackageReference>
|
||||
</ItemGroup>
|
||||
```
|
||||
|
||||
### What NOT to Put Here
|
||||
|
||||
- **Project-specific TFMs** — each project should declare its own `<TargetFramework>` or `<TargetFrameworks>`
|
||||
- **Project-specific PackageReferences** — unless truly universal (e.g., analyzers for all projects)
|
||||
- **Targets or complex build logic** — use `Directory.Build.targets` instead
|
||||
- **Properties that depend on SDK-defined values** — those won't be available yet during `.props` evaluation
|
||||
|
||||
## Directory.Build.targets
|
||||
|
||||
### What to Put Here
|
||||
|
||||
**Custom build targets:**
|
||||
|
||||
```xml
|
||||
<Target Name="ValidateProjectSettings" BeforeTargets="Build">
|
||||
<Error Text="All libraries must target netstandard2.0 or higher"
|
||||
Condition="'$(OutputType)' == 'Library' AND '$(TargetFramework)' == 'net472'" />
|
||||
</Target>
|
||||
```
|
||||
|
||||
**Conditional targets based on project type:**
|
||||
|
||||
```xml
|
||||
<Target Name="GenerateBuildInfo" BeforeTargets="CoreCompile"
|
||||
Condition="'$(GenerateBuildInfo)' == 'true'">
|
||||
<WriteLinesToFile File="$(IntermediateOutputPath)BuildInfo.g.cs"
|
||||
Lines="[assembly: System.Reflection.AssemblyMetadata("BuildDate", "$(Today)")]"
|
||||
Overwrite="true" />
|
||||
<ItemGroup>
|
||||
<Compile Include="$(IntermediateOutputPath)BuildInfo.g.cs" />
|
||||
</ItemGroup>
|
||||
</Target>
|
||||
```
|
||||
|
||||
**Late-bound property overrides (values that depend on SDK properties):**
|
||||
|
||||
```xml
|
||||
<PropertyGroup>
|
||||
<!-- DocumentationFile depends on OutputPath, which is set by the SDK -->
|
||||
<DocumentationFile Condition="'$(IsPackable)' == 'true'">$(OutputPath)$(AssemblyName).xml</DocumentationFile>
|
||||
</PropertyGroup>
|
||||
```
|
||||
|
||||
**Post-build validation:**
|
||||
|
||||
```xml
|
||||
<Target Name="ValidatePackageOutput" AfterTargets="Pack"
|
||||
Condition="'$(IsPackable)' == 'true'">
|
||||
<Error Text="Package was not created at $(PackageOutputPath)$(PackageId).$(PackageVersion).nupkg"
|
||||
Condition="!Exists('$(PackageOutputPath)$(PackageId).$(PackageVersion).nupkg')" />
|
||||
</Target>
|
||||
```
|
||||
|
||||
## Directory.Packages.props (Central Package Management)
|
||||
|
||||
Central Package Management (CPM) provides a single source of truth for all NuGet package versions. See [https://learn.microsoft.com/en-us/nuget/consume-packages/central-package-management](https://learn.microsoft.com/en-us/nuget/consume-packages/central-package-management) for details.
|
||||
|
||||
**Enable CPM in `Directory.Packages.props` at the repo root:**
|
||||
|
||||
```xml
|
||||
<Project>
|
||||
<PropertyGroup>
|
||||
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageVersion Include="Microsoft.Extensions.Logging" Version="8.0.0" />
|
||||
<PackageVersion Include="Newtonsoft.Json" Version="13.0.3" />
|
||||
<PackageVersion Include="xunit" Version="2.9.0" />
|
||||
<PackageVersion Include="xunit.runner.visualstudio" Version="2.8.2" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<!-- GlobalPackageReference applies to ALL projects — great for analyzers -->
|
||||
<GlobalPackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.556" />
|
||||
<GlobalPackageReference Include="Microsoft.CodeAnalysis.NetAnalyzers" Version="8.0.0" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
```
|
||||
|
||||
## Directory.Build.rsp
|
||||
|
||||
Contains default MSBuild CLI arguments applied to all builds under the directory tree.
|
||||
|
||||
**Example `Directory.Build.rsp`:**
|
||||
|
||||
```
|
||||
/maxcpucount
|
||||
/nodeReuse:false
|
||||
/consoleLoggerParameters:Summary;ForceNoAlign
|
||||
/warnAsMessage:MSB3277
|
||||
```
|
||||
|
||||
- Works with both `msbuild` and `dotnet` CLI in modern .NET versions
|
||||
- Great for enforcing consistent CI and local build flags
|
||||
- Each argument goes on its own line
|
||||
|
||||
## Multi-level Directory.Build Files
|
||||
|
||||
MSBuild only auto-imports the **first** `Directory.Build.props` (or `.targets`) it finds walking up from the project directory. To chain multiple levels, you must explicitly import the parent.
|
||||
|
||||
**Add this at the TOP of inner `Directory.Build.props` files:**
|
||||
|
||||
```xml
|
||||
<Project>
|
||||
<Import Project="$([MSBuild]::GetPathOfFileAbove('Directory.Build.props', '$(MSBuildThisFileDirectory)../'))"
|
||||
Condition="Exists('$([MSBuild]::GetPathOfFileAbove('Directory.Build.props', '$(MSBuildThisFileDirectory)../'))')" />
|
||||
|
||||
<!-- Inner-level overrides go here -->
|
||||
</Project>
|
||||
```
|
||||
|
||||
**Example layout:**
|
||||
|
||||
```
|
||||
repo/
|
||||
Directory.Build.props ← repo-wide settings (lang version, company info, analyzers)
|
||||
Directory.Build.targets ← repo-wide targets
|
||||
Directory.Packages.props ← central package versions
|
||||
src/
|
||||
Directory.Build.props ← src-specific (imports repo-level, sets IsPackable=true)
|
||||
MyLib/
|
||||
MyLib.csproj
|
||||
MyApp/
|
||||
MyApp.csproj
|
||||
test/
|
||||
Directory.Build.props ← test-specific (imports repo-level, sets IsPackable=false)
|
||||
MyLib.Tests/
|
||||
MyLib.Tests.csproj
|
||||
```
|
||||
|
||||
**Repo-level `Directory.Build.props`:**
|
||||
|
||||
```xml
|
||||
<Project>
|
||||
<PropertyGroup>
|
||||
<LangVersion>latest</LangVersion>
|
||||
<Nullable>enable</Nullable>
|
||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
```
|
||||
|
||||
**`src/Directory.Build.props`:**
|
||||
|
||||
```xml
|
||||
<Project>
|
||||
<Import Project="$([MSBuild]::GetPathOfFileAbove('Directory.Build.props', '$(MSBuildThisFileDirectory)../'))"
|
||||
Condition="Exists('$([MSBuild]::GetPathOfFileAbove('Directory.Build.props', '$(MSBuildThisFileDirectory)../'))')" />
|
||||
|
||||
<PropertyGroup>
|
||||
<IsPackable>true</IsPackable>
|
||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
```
|
||||
|
||||
**`test/Directory.Build.props`:**
|
||||
|
||||
```xml
|
||||
<Project>
|
||||
<Import Project="$([MSBuild]::GetPathOfFileAbove('Directory.Build.props', '$(MSBuildThisFileDirectory)../'))"
|
||||
Condition="Exists('$([MSBuild]::GetPathOfFileAbove('Directory.Build.props', '$(MSBuildThisFileDirectory)../'))')" />
|
||||
|
||||
<PropertyGroup>
|
||||
<IsPackable>false</IsPackable>
|
||||
<NoWarn>$(NoWarn);CS1591</NoWarn>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="xunit" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" />
|
||||
<PackageReference Include="NSubstitute" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
```
|
||||
|
||||
## Common Patterns
|
||||
|
||||
### Pattern: Shared Analyzers via GlobalPackageReference
|
||||
|
||||
In `Directory.Packages.props`:
|
||||
|
||||
```xml
|
||||
<ItemGroup>
|
||||
<GlobalPackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.556" />
|
||||
<GlobalPackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="3.3.4" />
|
||||
</ItemGroup>
|
||||
```
|
||||
|
||||
This ensures every project in the repo gets these analyzers without any per-project configuration.
|
||||
|
||||
### Pattern: Conditional Settings by Project Type
|
||||
|
||||
In `Directory.Build.props`:
|
||||
|
||||
```xml
|
||||
<!-- Detect test projects by naming convention -->
|
||||
<PropertyGroup Condition="$(MSBuildProjectName.EndsWith('.Tests')) OR $(MSBuildProjectName.EndsWith('.UnitTests'))">
|
||||
<IsPackable>false</IsPackable>
|
||||
<IsTestProject>true</IsTestProject>
|
||||
</PropertyGroup>
|
||||
```
|
||||
|
||||
In `Directory.Build.targets`:
|
||||
|
||||
```xml
|
||||
<!-- Detect project output type after SDK has set defaults -->
|
||||
<PropertyGroup Condition="'$(OutputType)' == 'Exe'">
|
||||
<SelfContained>false</SelfContained>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition="'$(OutputType)' == 'Library' AND '$(IsTestProject)' != 'true'">
|
||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||
</PropertyGroup>
|
||||
```
|
||||
|
||||
### Pattern: Before/After Repository Cleanup
|
||||
|
||||
**Before — duplicated settings in every .csproj:**
|
||||
|
||||
```xml
|
||||
<!-- src/LibA/LibA.csproj -->
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<LangVersion>latest</LangVersion>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||
<Company>Contoso</Company>
|
||||
<Authors>Contoso Engineering</Authors>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.556" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
||||
<!-- src/LibB/LibB.csproj — same boilerplate repeated -->
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<LangVersion>latest</LangVersion>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||
<Company>Contoso</Company>
|
||||
<Authors>Contoso Engineering</Authors>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.556" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging" Version="8.0.0" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
```
|
||||
|
||||
**After — centralized with Directory.Build files:**
|
||||
|
||||
```xml
|
||||
<!-- Directory.Build.props -->
|
||||
<Project>
|
||||
<PropertyGroup>
|
||||
<LangVersion>latest</LangVersion>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||
<Company>Contoso</Company>
|
||||
<Authors>Contoso Engineering</Authors>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
|
||||
<!-- Directory.Packages.props -->
|
||||
<Project>
|
||||
<PropertyGroup>
|
||||
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageVersion Include="Newtonsoft.Json" Version="13.0.3" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Logging" Version="8.0.0" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<GlobalPackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.556" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
||||
<!-- src/LibA/LibA.csproj — clean and minimal -->
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Newtonsoft.Json" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
||||
<!-- src/LibB/LibB.csproj — clean and minimal -->
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Extensions.Logging" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
```
|
||||
|
||||
### Pattern: Artifact Output Layout (.NET 8+)
|
||||
|
||||
In `Directory.Build.props`:
|
||||
|
||||
```xml
|
||||
<PropertyGroup>
|
||||
<ArtifactsPath>$(MSBuildThisFileDirectory)artifacts</ArtifactsPath>
|
||||
</PropertyGroup>
|
||||
```
|
||||
|
||||
This produces a structured output layout:
|
||||
|
||||
```
|
||||
artifacts/
|
||||
bin/
|
||||
MyLib/
|
||||
debug/
|
||||
release/
|
||||
MyApp/
|
||||
debug/
|
||||
release/
|
||||
obj/
|
||||
MyLib/
|
||||
MyApp/
|
||||
publish/
|
||||
MyApp/
|
||||
```
|
||||
|
||||
The `ArtifactsPath` property (.NET 8+) automatically sets `BaseOutputPath`, `BaseIntermediateOutputPath`, and `PackageOutputPath` with project-name-separated directories, avoiding bin/obj clashes by default.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
| Problem | Cause | Fix |
|
||||
|---|---|---|
|
||||
| `Directory.Build.props` isn't picked up | File name casing wrong (exact match required on Linux/macOS) | Verify exact casing: `Directory.Build.props` (capital D, B) |
|
||||
| Properties from `.props` are ignored by projects | Project sets the same property after the import | Move the property to `Directory.Build.targets` to set it after the project |
|
||||
| Multi-level import doesn't work | Missing `GetPathOfFileAbove` import in inner file | Add the `<Import>` element at the top of the inner file (see Multi-level section) |
|
||||
| Properties using SDK values are empty in `.props` | SDK properties aren't defined yet during `.props` evaluation | Move to `.targets` which is imported after the SDK |
|
||||
| `Directory.Packages.props` not found | File not at repo root or not named exactly | Must be named `Directory.Packages.props` and at or above the project directory |
|
||||
| Property condition on `$(TargetFramework)` doesn't match in `.props` | `TargetFramework` isn't set yet for single-targeting projects during `.props` evaluation | Move property to `.targets`, or use ItemGroup/Target conditions instead (which evaluate late) |
|
||||
|
||||
**Diagnosis:** Use the preprocessed project output to see all imports and final property values:
|
||||
|
||||
```bash
|
||||
dotnet msbuild -pp:output.xml MyProject.csproj
|
||||
```
|
||||
|
||||
This expands all imports inline so you can see exactly where each property is set and what the final evaluated value is.
|
||||
@@ -1,4 +1,4 @@
|
||||
<!-- AUTO-GENERATED — DO NOT EDIT. Regenerate with: node src/dotnet-msbuild/build.js -->
|
||||
<!-- AUTO-GENERATED — DO NOT EDIT -->
|
||||
|
||||
# Build Performance Baseline & Optimization
|
||||
|
||||
|
||||
+596
-544
File diff suppressed because it is too large
Load Diff
@@ -35,124 +35,40 @@ See the AP-21 section in the [msbuild-antipatterns skill](../msbuild-antipattern
|
||||
|
||||
## Directory.Build.props
|
||||
|
||||
### What to Put Here
|
||||
|
||||
**Output settings:**
|
||||
Good candidates: language settings, assembly/package metadata, build warnings, code analysis, common analyzers.
|
||||
|
||||
```xml
|
||||
<PropertyGroup>
|
||||
<!-- Use with caution — see bin/obj clash skill for risks -->
|
||||
<BaseOutputPath>$(MSBuildThisFileDirectory)artifacts\bin\</BaseOutputPath>
|
||||
<BaseIntermediateOutputPath>$(MSBuildThisFileDirectory)artifacts\obj\$(MSBuildProjectName)\</BaseIntermediateOutputPath>
|
||||
</PropertyGroup>
|
||||
<Project>
|
||||
<PropertyGroup>
|
||||
<LangVersion>latest</LangVersion>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||
<EnforceCodeStyleInBuild>true</EnforceCodeStyleInBuild>
|
||||
<Company>Contoso</Company>
|
||||
<Authors>Contoso Engineering</Authors>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
```
|
||||
|
||||
**Language settings:**
|
||||
|
||||
```xml
|
||||
<PropertyGroup>
|
||||
<LangVersion>latest</LangVersion>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<AnalysisLevel>latest-recommended</AnalysisLevel>
|
||||
</PropertyGroup>
|
||||
```
|
||||
|
||||
**Assembly and package metadata:**
|
||||
|
||||
```xml
|
||||
<PropertyGroup>
|
||||
<Company>Contoso</Company>
|
||||
<Authors>Contoso Engineering</Authors>
|
||||
<Copyright>Copyright © Contoso $(CurrentYear)</Copyright>
|
||||
<Product>Contoso Platform</Product>
|
||||
<PackageLicenseExpression>MIT</PackageLicenseExpression>
|
||||
<RepositoryUrl>https://github.com/contoso/platform</RepositoryUrl>
|
||||
<PackageProjectUrl>https://github.com/contoso/platform</PackageProjectUrl>
|
||||
</PropertyGroup>
|
||||
```
|
||||
|
||||
**Build behavior and warnings:**
|
||||
|
||||
```xml
|
||||
<PropertyGroup>
|
||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||
<WarningsAsErrors />
|
||||
<NoWarn>$(NoWarn);CS1591</NoWarn>
|
||||
</PropertyGroup>
|
||||
```
|
||||
|
||||
**Code analysis:**
|
||||
|
||||
```xml
|
||||
<PropertyGroup>
|
||||
<EnforceCodeStyleInBuild>true</EnforceCodeStyleInBuild>
|
||||
<EnableNETAnalyzers>true</EnableNETAnalyzers>
|
||||
</PropertyGroup>
|
||||
```
|
||||
|
||||
**Common analyzer PackageReferences (apply to all projects):**
|
||||
|
||||
```xml
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="3.3.4">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
|
||||
</PackageReference>
|
||||
</ItemGroup>
|
||||
```
|
||||
|
||||
### What NOT to Put Here
|
||||
|
||||
- **Project-specific TFMs** — each project should declare its own `<TargetFramework>` or `<TargetFrameworks>`
|
||||
- **Project-specific PackageReferences** — unless truly universal (e.g., analyzers for all projects)
|
||||
- **Targets or complex build logic** — use `Directory.Build.targets` instead
|
||||
- **Properties that depend on SDK-defined values** — those won't be available yet during `.props` evaluation
|
||||
**Do NOT put here:** project-specific TFMs, project-specific PackageReferences, targets/build logic, or properties depending on SDK-defined values (not available during `.props` evaluation).
|
||||
|
||||
## Directory.Build.targets
|
||||
|
||||
### What to Put Here
|
||||
|
||||
**Custom build targets:**
|
||||
Good candidates: custom build targets, late-bound property overrides (values depending on SDK properties), post-build validation.
|
||||
|
||||
```xml
|
||||
<Target Name="ValidateProjectSettings" BeforeTargets="Build">
|
||||
<Error Text="All libraries must target netstandard2.0 or higher"
|
||||
Condition="'$(OutputType)' == 'Library' AND '$(TargetFramework)' == 'net472'" />
|
||||
</Target>
|
||||
```
|
||||
<Project>
|
||||
<Target Name="ValidateProjectSettings" BeforeTargets="Build">
|
||||
<Error Text="All libraries must target netstandard2.0 or higher"
|
||||
Condition="'$(OutputType)' == 'Library' AND '$(TargetFramework)' == 'net472'" />
|
||||
</Target>
|
||||
|
||||
**Conditional targets based on project type:**
|
||||
|
||||
```xml
|
||||
<Target Name="GenerateBuildInfo" BeforeTargets="CoreCompile"
|
||||
Condition="'$(GenerateBuildInfo)' == 'true'">
|
||||
<WriteLinesToFile File="$(IntermediateOutputPath)BuildInfo.g.cs"
|
||||
Lines="[assembly: System.Reflection.AssemblyMetadata("BuildDate", "$(Today)")]"
|
||||
Overwrite="true" />
|
||||
<ItemGroup>
|
||||
<Compile Include="$(IntermediateOutputPath)BuildInfo.g.cs" />
|
||||
</ItemGroup>
|
||||
</Target>
|
||||
```
|
||||
|
||||
**Late-bound property overrides (values that depend on SDK properties):**
|
||||
|
||||
```xml
|
||||
<PropertyGroup>
|
||||
<!-- DocumentationFile depends on OutputPath, which is set by the SDK -->
|
||||
<DocumentationFile Condition="'$(IsPackable)' == 'true'">$(OutputPath)$(AssemblyName).xml</DocumentationFile>
|
||||
</PropertyGroup>
|
||||
```
|
||||
|
||||
**Post-build validation:**
|
||||
|
||||
```xml
|
||||
<Target Name="ValidatePackageOutput" AfterTargets="Pack"
|
||||
Condition="'$(IsPackable)' == 'true'">
|
||||
<Error Text="Package was not created at $(PackageOutputPath)$(PackageId).$(PackageVersion).nupkg"
|
||||
Condition="!Exists('$(PackageOutputPath)$(PackageId).$(PackageVersion).nupkg')" />
|
||||
</Target>
|
||||
<PropertyGroup>
|
||||
<!-- DocumentationFile depends on OutputPath, which is set by the SDK -->
|
||||
<DocumentationFile Condition="'$(IsPackable)' == 'true'">$(OutputPath)$(AssemblyName).xml</DocumentationFile>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
```
|
||||
|
||||
## Directory.Packages.props (Central Package Management)
|
||||
@@ -201,9 +117,7 @@ Contains default MSBuild CLI arguments applied to all builds under the directory
|
||||
|
||||
## Multi-level Directory.Build Files
|
||||
|
||||
MSBuild only auto-imports the **first** `Directory.Build.props` (or `.targets`) it finds walking up from the project directory. To chain multiple levels, you must explicitly import the parent.
|
||||
|
||||
**Add this at the TOP of inner `Directory.Build.props` files:**
|
||||
MSBuild only auto-imports the **first** `Directory.Build.props` (or `.targets`) it finds walking up from the project directory. To chain multiple levels, explicitly import the parent at the **top** of the inner file. See [multi-level-examples](references/multi-level-examples.md) for full file examples.
|
||||
|
||||
```xml
|
||||
<Project>
|
||||
@@ -218,227 +132,18 @@ MSBuild only auto-imports the **first** `Directory.Build.props` (or `.targets`)
|
||||
|
||||
```
|
||||
repo/
|
||||
Directory.Build.props ← repo-wide settings (lang version, company info, analyzers)
|
||||
Directory.Build.props ← repo-wide (lang version, company info, analyzers)
|
||||
Directory.Build.targets ← repo-wide targets
|
||||
Directory.Packages.props ← central package versions
|
||||
src/
|
||||
Directory.Build.props ← src-specific (imports repo-level, sets IsPackable=true)
|
||||
MyLib/
|
||||
MyLib.csproj
|
||||
MyApp/
|
||||
MyApp.csproj
|
||||
test/
|
||||
Directory.Build.props ← test-specific (imports repo-level, sets IsPackable=false)
|
||||
MyLib.Tests/
|
||||
MyLib.Tests.csproj
|
||||
Directory.Build.props ← test-specific (imports repo-level, sets IsPackable=false, adds test packages)
|
||||
```
|
||||
|
||||
**Repo-level `Directory.Build.props`:**
|
||||
## Artifact Output Layout (.NET 8+)
|
||||
|
||||
```xml
|
||||
<Project>
|
||||
<PropertyGroup>
|
||||
<LangVersion>latest</LangVersion>
|
||||
<Nullable>enable</Nullable>
|
||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
```
|
||||
|
||||
**`src/Directory.Build.props`:**
|
||||
|
||||
```xml
|
||||
<Project>
|
||||
<Import Project="$([MSBuild]::GetPathOfFileAbove('Directory.Build.props', '$(MSBuildThisFileDirectory)../'))"
|
||||
Condition="Exists('$([MSBuild]::GetPathOfFileAbove('Directory.Build.props', '$(MSBuildThisFileDirectory)../'))')" />
|
||||
|
||||
<PropertyGroup>
|
||||
<IsPackable>true</IsPackable>
|
||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
```
|
||||
|
||||
**`test/Directory.Build.props`:**
|
||||
|
||||
```xml
|
||||
<Project>
|
||||
<Import Project="$([MSBuild]::GetPathOfFileAbove('Directory.Build.props', '$(MSBuildThisFileDirectory)../'))"
|
||||
Condition="Exists('$([MSBuild]::GetPathOfFileAbove('Directory.Build.props', '$(MSBuildThisFileDirectory)../'))')" />
|
||||
|
||||
<PropertyGroup>
|
||||
<IsPackable>false</IsPackable>
|
||||
<NoWarn>$(NoWarn);CS1591</NoWarn>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="xunit" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" />
|
||||
<PackageReference Include="NSubstitute" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
```
|
||||
|
||||
## Common Patterns
|
||||
|
||||
### Pattern: Shared Analyzers via GlobalPackageReference
|
||||
|
||||
In `Directory.Packages.props`:
|
||||
|
||||
```xml
|
||||
<ItemGroup>
|
||||
<GlobalPackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.556" />
|
||||
<GlobalPackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="3.3.4" />
|
||||
</ItemGroup>
|
||||
```
|
||||
|
||||
This ensures every project in the repo gets these analyzers without any per-project configuration.
|
||||
|
||||
### Pattern: Conditional Settings by Project Type
|
||||
|
||||
In `Directory.Build.props`:
|
||||
|
||||
```xml
|
||||
<!-- Detect test projects by naming convention -->
|
||||
<PropertyGroup Condition="$(MSBuildProjectName.EndsWith('.Tests')) OR $(MSBuildProjectName.EndsWith('.UnitTests'))">
|
||||
<IsPackable>false</IsPackable>
|
||||
<IsTestProject>true</IsTestProject>
|
||||
</PropertyGroup>
|
||||
```
|
||||
|
||||
In `Directory.Build.targets`:
|
||||
|
||||
```xml
|
||||
<!-- Detect project output type after SDK has set defaults -->
|
||||
<PropertyGroup Condition="'$(OutputType)' == 'Exe'">
|
||||
<SelfContained>false</SelfContained>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition="'$(OutputType)' == 'Library' AND '$(IsTestProject)' != 'true'">
|
||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||
</PropertyGroup>
|
||||
```
|
||||
|
||||
### Pattern: Before/After Repository Cleanup
|
||||
|
||||
**Before — duplicated settings in every .csproj:**
|
||||
|
||||
```xml
|
||||
<!-- src/LibA/LibA.csproj -->
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<LangVersion>latest</LangVersion>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||
<Company>Contoso</Company>
|
||||
<Authors>Contoso Engineering</Authors>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.556" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
||||
<!-- src/LibB/LibB.csproj — same boilerplate repeated -->
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<LangVersion>latest</LangVersion>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||
<Company>Contoso</Company>
|
||||
<Authors>Contoso Engineering</Authors>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.556" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging" Version="8.0.0" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
```
|
||||
|
||||
**After — centralized with Directory.Build files:**
|
||||
|
||||
```xml
|
||||
<!-- Directory.Build.props -->
|
||||
<Project>
|
||||
<PropertyGroup>
|
||||
<LangVersion>latest</LangVersion>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||
<Company>Contoso</Company>
|
||||
<Authors>Contoso Engineering</Authors>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
|
||||
<!-- Directory.Packages.props -->
|
||||
<Project>
|
||||
<PropertyGroup>
|
||||
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageVersion Include="Newtonsoft.Json" Version="13.0.3" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Logging" Version="8.0.0" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<GlobalPackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.556" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
||||
<!-- src/LibA/LibA.csproj — clean and minimal -->
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Newtonsoft.Json" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
||||
<!-- src/LibB/LibB.csproj — clean and minimal -->
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Extensions.Logging" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
```
|
||||
|
||||
### Pattern: Artifact Output Layout (.NET 8+)
|
||||
|
||||
In `Directory.Build.props`:
|
||||
|
||||
```xml
|
||||
<PropertyGroup>
|
||||
<ArtifactsPath>$(MSBuildThisFileDirectory)artifacts</ArtifactsPath>
|
||||
</PropertyGroup>
|
||||
```
|
||||
|
||||
This produces a structured output layout:
|
||||
|
||||
```
|
||||
artifacts/
|
||||
bin/
|
||||
MyLib/
|
||||
debug/
|
||||
release/
|
||||
MyApp/
|
||||
debug/
|
||||
release/
|
||||
obj/
|
||||
MyLib/
|
||||
MyApp/
|
||||
publish/
|
||||
MyApp/
|
||||
```
|
||||
|
||||
The `ArtifactsPath` property (.NET 8+) automatically sets `BaseOutputPath`, `BaseIntermediateOutputPath`, and `PackageOutputPath` with project-name-separated directories, avoiding bin/obj clashes by default.
|
||||
Set `<ArtifactsPath>$(MSBuildThisFileDirectory)artifacts</ArtifactsPath>` in `Directory.Build.props` to automatically produce project-name-separated `bin/`, `obj/`, and `publish/` directories under a single `artifacts/` folder, avoiding bin/obj clashes by default. See [common-patterns](references/common-patterns.md) for the directory layout and additional patterns (conditional settings by project type, post-pack validation).
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
# Common Directory.Build Patterns
|
||||
|
||||
## Conditional Settings by Project Type
|
||||
|
||||
Detect test projects by naming convention in `Directory.Build.props`:
|
||||
|
||||
```xml
|
||||
<PropertyGroup Condition="$(MSBuildProjectName.EndsWith('.Tests')) OR $(MSBuildProjectName.EndsWith('.UnitTests'))">
|
||||
<IsPackable>false</IsPackable>
|
||||
<IsTestProject>true</IsTestProject>
|
||||
</PropertyGroup>
|
||||
```
|
||||
|
||||
Use `Directory.Build.targets` for conditions on SDK-defined properties like `OutputType`:
|
||||
|
||||
```xml
|
||||
<PropertyGroup Condition="'$(OutputType)' == 'Exe'">
|
||||
<SelfContained>false</SelfContained>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition="'$(OutputType)' == 'Library' AND '$(IsTestProject)' != 'true'">
|
||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||
</PropertyGroup>
|
||||
```
|
||||
|
||||
## Post-Build Validation
|
||||
|
||||
Validate that `Pack` produced the expected output:
|
||||
|
||||
```xml
|
||||
<Target Name="ValidatePackageOutput" AfterTargets="Pack"
|
||||
Condition="'$(IsPackable)' == 'true'">
|
||||
<Error Text="Package was not created at $(PackageOutputPath)$(PackageId).$(PackageVersion).nupkg"
|
||||
Condition="!Exists('$(PackageOutputPath)$(PackageId).$(PackageVersion).nupkg')" />
|
||||
</Target>
|
||||
```
|
||||
|
||||
## Artifact Output Layout (.NET 8+)
|
||||
|
||||
Setting `ArtifactsPath` in `Directory.Build.props` produces this structure:
|
||||
|
||||
```
|
||||
artifacts/
|
||||
bin/
|
||||
MyLib/
|
||||
debug/
|
||||
release/
|
||||
MyApp/
|
||||
debug/
|
||||
release/
|
||||
obj/
|
||||
MyLib/
|
||||
MyApp/
|
||||
publish/
|
||||
MyApp/
|
||||
```
|
||||
+164
@@ -0,0 +1,164 @@
|
||||
# Multi-level Directory.Build Examples
|
||||
|
||||
Full file examples for a typical multi-level repo layout.
|
||||
|
||||
## Repo-level `Directory.Build.props`
|
||||
|
||||
```xml
|
||||
<Project>
|
||||
|
||||
<PropertyGroup>
|
||||
<LangVersion>latest</LangVersion>
|
||||
<Nullable>enable</Nullable>
|
||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||
</PropertyGroup>
|
||||
|
||||
</Project>
|
||||
```
|
||||
|
||||
## `src/Directory.Build.props`
|
||||
|
||||
```xml
|
||||
<Project>
|
||||
|
||||
<Import Project="$([MSBuild]::GetPathOfFileAbove('Directory.Build.props', '$(MSBuildThisFileDirectory)../'))"
|
||||
Condition="Exists('$([MSBuild]::GetPathOfFileAbove('Directory.Build.props', '$(MSBuildThisFileDirectory)../'))')" />
|
||||
|
||||
<PropertyGroup>
|
||||
<IsPackable>true</IsPackable>
|
||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||
</PropertyGroup>
|
||||
|
||||
</Project>
|
||||
```
|
||||
|
||||
## `test/Directory.Build.props`
|
||||
|
||||
```xml
|
||||
<Project>
|
||||
|
||||
<Import Project="$([MSBuild]::GetPathOfFileAbove('Directory.Build.props', '$(MSBuildThisFileDirectory)../'))"
|
||||
Condition="Exists('$([MSBuild]::GetPathOfFileAbove('Directory.Build.props', '$(MSBuildThisFileDirectory)../'))')" />
|
||||
|
||||
<PropertyGroup>
|
||||
<IsPackable>false</IsPackable>
|
||||
<NoWarn>$(NoWarn);CS1591</NoWarn>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="xunit" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" />
|
||||
<PackageReference Include="NSubstitute" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
```
|
||||
|
||||
## Before/After: Centralizing Duplicated Settings
|
||||
|
||||
**Before — duplicated settings in every .csproj:**
|
||||
|
||||
```xml
|
||||
<!-- src/LibA/LibA.csproj -->
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<LangVersion>latest</LangVersion>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||
<Company>Contoso</Company>
|
||||
<Authors>Contoso Engineering</Authors>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.556" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
<!-- src/LibB/LibB.csproj — same boilerplate repeated -->
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<LangVersion>latest</LangVersion>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||
<Company>Contoso</Company>
|
||||
<Authors>Contoso Engineering</Authors>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.556" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging" Version="8.0.0" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
```
|
||||
|
||||
**After — centralized with Directory.Build files:**
|
||||
|
||||
```xml
|
||||
<!-- Directory.Build.props -->
|
||||
<Project>
|
||||
|
||||
<PropertyGroup>
|
||||
<LangVersion>latest</LangVersion>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||
<Company>Contoso</Company>
|
||||
<Authors>Contoso Engineering</Authors>
|
||||
</PropertyGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
<!-- Directory.Packages.props -->
|
||||
<Project>
|
||||
|
||||
<PropertyGroup>
|
||||
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageVersion Include="Newtonsoft.Json" Version="13.0.3" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Logging" Version="8.0.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<GlobalPackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.556" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
<!-- src/LibA/LibA.csproj — clean and minimal -->
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Newtonsoft.Json" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
<!-- src/LibB/LibB.csproj — clean and minimal -->
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Extensions.Logging" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
```
|
||||
Reference in New Issue
Block a user