From c7c74daf8e3041e35b2b996bd434183813f49432 Mon Sep 17 00:00:00 2001 From: Viktor Hofer Date: Wed, 25 Feb 2026 10:33:36 +0100 Subject: [PATCH] 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> --- .gitattributes | 2 + AGENTS.md | 19 + src/dotnet-msbuild/README.md | 4 +- .../build-failure-analysis.md | 28 +- .../agentic-workflows/build-perf-audit.md | 43 +- .../agentic-workflows/msbuild-pr-review.md | 52 +- ...knowledge.lock.md => build-errors.lock.md} | 227 +++- ...-knowledge.lock.md => performance.lock.md} | 2 +- ...ock.md => style-and-modernization.lock.md} | 131 +- src/dotnet-msbuild/build.js | 198 --- src/dotnet-msbuild/build.ps1 | 172 +++ .../copilot-extension/README.md | 18 +- .../copilot-extension/package.json | 2 +- .../src/intent-classifier.js | 4 +- .../src/knowledge/build-errors.lock.md | 227 +++- .../src/knowledge/modernization.lock.md | 956 -------------- .../src/knowledge/performance.lock.md | 2 +- ...ock.md => style-and-modernization.lock.md} | 1140 +++++++++-------- .../directory-build-organization/SKILL.md | 353 +---- .../references/common-patterns.md | 56 + .../references/multi-level-examples.md | 164 +++ 21 files changed, 1631 insertions(+), 2169 deletions(-) create mode 100644 .gitattributes create mode 100644 AGENTS.md rename src/dotnet-msbuild/agentic-workflows/shared/compiled/{build-failure-knowledge.lock.md => build-errors.lock.md} (74%) rename src/dotnet-msbuild/agentic-workflows/shared/compiled/{perf-audit-knowledge.lock.md => performance.lock.md} (99%) rename src/dotnet-msbuild/agentic-workflows/shared/compiled/{pr-review-knowledge.lock.md => style-and-modernization.lock.md} (92%) delete mode 100644 src/dotnet-msbuild/build.js create mode 100644 src/dotnet-msbuild/build.ps1 delete mode 100644 src/dotnet-msbuild/copilot-extension/src/knowledge/modernization.lock.md rename src/dotnet-msbuild/copilot-extension/src/knowledge/{style-guide.lock.md => style-and-modernization.lock.md} (61%) create mode 100644 src/dotnet-msbuild/skills/directory-build-organization/references/common-patterns.md create mode 100644 src/dotnet-msbuild/skills/directory-build-organization/references/multi-level-examples.md diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 00000000..bf094344 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +# Normalize all text files to LF line endings +* text=auto eol=lf diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 00000000..ddca3659 --- /dev/null +++ b/AGENTS.md @@ -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//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. diff --git a/src/dotnet-msbuild/README.md b/src/dotnet-msbuild/README.md index ce3cd679..fb7c1acf 100644 --- a/src/dotnet-msbuild/README.md +++ b/src/dotnet-msbuild/README.md @@ -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 diff --git a/src/dotnet-msbuild/agentic-workflows/build-failure-analysis.md b/src/dotnet-msbuild/agentic-workflows/build-failure-analysis.md index 07072198..4fcde69c 100644 --- a/src/dotnet-msbuild/agentic-workflows/build-failure-analysis.md +++ b/src/dotnet-msbuild/agentic-workflows/build-failure-analysis.md @@ -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 diff --git a/src/dotnet-msbuild/agentic-workflows/build-perf-audit.md b/src/dotnet-msbuild/agentic-workflows/build-perf-audit.md index 31c9b554..bbf312bd 100644 --- a/src/dotnet-msbuild/agentic-workflows/build-perf-audit.md +++ b/src/dotnet-msbuild/agentic-workflows/build-perf-audit.md @@ -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 diff --git a/src/dotnet-msbuild/agentic-workflows/msbuild-pr-review.md b/src/dotnet-msbuild/agentic-workflows/msbuild-pr-review.md index ab6dbb9d..334f7556 100644 --- a/src/dotnet-msbuild/agentic-workflows/msbuild-pr-review.md +++ b/src/dotnet-msbuild/agentic-workflows/msbuild-pr-review.md @@ -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 -- `` tags with HintPath that should be `` (note: `` 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 (``) +- **AP-05** `` with HintPath that should be `` (note: `` 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 `` 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 `` 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 diff --git a/src/dotnet-msbuild/agentic-workflows/shared/compiled/build-failure-knowledge.lock.md b/src/dotnet-msbuild/agentic-workflows/shared/compiled/build-errors.lock.md similarity index 74% rename from src/dotnet-msbuild/agentic-workflows/shared/compiled/build-failure-knowledge.lock.md rename to src/dotnet-msbuild/agentic-workflows/shared/compiled/build-errors.lock.md index df46132d..dac56169 100644 --- a/src/dotnet-msbuild/agentic-workflows/shared/compiled/build-failure-knowledge.lock.md +++ b/src/dotnet-msbuild/agentic-workflows/shared/compiled/build-errors.lock.md @@ -1,4 +1,4 @@ - + # 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. \ No newline at end of file +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 + + + + +``` + +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 + + + +``` + +### 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 + + + + + + + + + + + + + + + +``` + +### 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 + + + $(IntermediateOutputPath)Generated\ + $(GeneratedCodeDir)MyGeneratedFile.cs + + + + + + + + + + + +``` + +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 `` inside a `` - 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) \ No newline at end of file diff --git a/src/dotnet-msbuild/agentic-workflows/shared/compiled/perf-audit-knowledge.lock.md b/src/dotnet-msbuild/agentic-workflows/shared/compiled/performance.lock.md similarity index 99% rename from src/dotnet-msbuild/agentic-workflows/shared/compiled/perf-audit-knowledge.lock.md rename to src/dotnet-msbuild/agentic-workflows/shared/compiled/performance.lock.md index 4b3bef34..ce1b5c33 100644 --- a/src/dotnet-msbuild/agentic-workflows/shared/compiled/perf-audit-knowledge.lock.md +++ b/src/dotnet-msbuild/agentic-workflows/shared/compiled/performance.lock.md @@ -1,4 +1,4 @@ - + # Build Performance Baseline & Optimization diff --git a/src/dotnet-msbuild/agentic-workflows/shared/compiled/pr-review-knowledge.lock.md b/src/dotnet-msbuild/agentic-workflows/shared/compiled/style-and-modernization.lock.md similarity index 92% rename from src/dotnet-msbuild/agentic-workflows/shared/compiled/pr-review-knowledge.lock.md rename to src/dotnet-msbuild/agentic-workflows/shared/compiled/style-and-modernization.lock.md index 5e8fa437..7e187e76 100644 --- a/src/dotnet-msbuild/agentic-workflows/shared/compiled/pr-review-knowledge.lock.md +++ b/src/dotnet-msbuild/agentic-workflows/shared/compiled/style-and-modernization.lock.md @@ -1,4 +1,4 @@ - + # 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 + + + + + + + + + +``` + +**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 + + + + + + + + + + + + + +``` + +**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 `true` and `` items for all packages. +**Step 1:** Create `Directory.Pa -**Step 2:** Remove `Version` from each project's `PackageReference`: - -```xml - - - - - -``` - -## 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 - - - net8.0 - enable - enable - true - Contoso - Copyright © Contoso 2024 - - -``` - -**`Directory.Build.targets`** (for targets/tasks — placed at repo or src root): - -```xml - - - - - -``` - -**Keep in individual `.csproj` files** only what is project-specific: - -```xml - - - Exe - MyApp - - - - - - -``` - -## 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` \ No newline at end of file +[truncated] \ No newline at end of file diff --git a/src/dotnet-msbuild/build.js b/src/dotnet-msbuild/build.js deleted file mode 100644 index 3a8311bf..00000000 --- a/src/dotnet-msbuild/build.js +++ /dev/null @@ -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 = `\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."); diff --git a/src/dotnet-msbuild/build.ps1 b/src/dotnet-msbuild/build.ps1 new file mode 100644 index 00000000..2902a02b --- /dev/null +++ b/src/dotnet-msbuild/build.ps1 @@ -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 = "`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 diff --git a/src/dotnet-msbuild/copilot-extension/README.md b/src/dotnet-msbuild/copilot-extension/README.md index 1d923190..22138b68 100644 --- a/src/dotnet-msbuild/copilot-extension/README.md +++ b/src/dotnet-msbuild/copilot-extension/README.md @@ -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) diff --git a/src/dotnet-msbuild/copilot-extension/package.json b/src/dotnet-msbuild/copilot-extension/package.json index 8f58ed7f..5a4f6f29 100644 --- a/src/dotnet-msbuild/copilot-extension/package.json +++ b/src/dotnet-msbuild/copilot-extension/package.json @@ -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": [ diff --git a/src/dotnet-msbuild/copilot-extension/src/intent-classifier.js b/src/dotnet-msbuild/copilot-extension/src/intent-classifier.js index 6f9c44a7..ee4c231a 100644 --- a/src/dotnet-msbuild/copilot-extension/src/intent-classifier.js +++ b/src/dotnet-msbuild/copilot-extension/src/intent-classifier.js @@ -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: { diff --git a/src/dotnet-msbuild/copilot-extension/src/knowledge/build-errors.lock.md b/src/dotnet-msbuild/copilot-extension/src/knowledge/build-errors.lock.md index df46132d..dac56169 100644 --- a/src/dotnet-msbuild/copilot-extension/src/knowledge/build-errors.lock.md +++ b/src/dotnet-msbuild/copilot-extension/src/knowledge/build-errors.lock.md @@ -1,4 +1,4 @@ - + # 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. \ No newline at end of file +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 + + + + +``` + +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 + + + +``` + +### 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 + + + + + + + + + + + + + + + +``` + +### 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 + + + $(IntermediateOutputPath)Generated\ + $(GeneratedCodeDir)MyGeneratedFile.cs + + + + + + + + + + + +``` + +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 `` inside a `` - 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) \ No newline at end of file diff --git a/src/dotnet-msbuild/copilot-extension/src/knowledge/modernization.lock.md b/src/dotnet-msbuild/copilot-extension/src/knowledge/modernization.lock.md deleted file mode 100644 index 4433d517..00000000 --- a/src/dotnet-msbuild/copilot-extension/src/knowledge/modernization.lock.md +++ /dev/null @@ -1,956 +0,0 @@ - - -# MSBuild Modernization: Legacy to SDK-style Migration - -## Identifying Legacy vs SDK-style Projects - -**Legacy indicators:** - -- `` -- Explicit file lists (`` for every `.cs` file) -- `ToolsVersion` attribute on `` element -- `packages.config` file present -- `Properties\AssemblyInfo.cs` with assembly-level attributes - -**SDK-style indicators:** - -- `` attribute on root element -- Minimal content — a simple project may be 10–15 lines -- No explicit file includes (implicit globbing) -- `` 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 - - - - - - Debug - AnyCPU - Library - MyLibrary - MyLibrary - v4.7.2 - 512 - true - - - - -``` - -```xml - - - - net472 - - -``` - -## Migration Checklist: Legacy → SDK-style - -### Step 1: Replace Project Root Element - -**BEFORE:** - -```xml - - - - - - -``` - -**AFTER:** - -```xml - - - -``` - -Remove the XML declaration, `ToolsVersion`, `xmlns`, and both `` lines. The `Sdk` attribute replaces all of them. - -### Step 2: Set TargetFramework - -**BEFORE:** - -```xml - - v4.7.2 - -``` - -**AFTER:** - -```xml - - net472 - -``` - -**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 - - - - - - - - - - - - - - -``` - -**AFTER:** - -Delete all of these `` and `` 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 - - - -``` - -### 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 - - MyLibrary - A useful library - Contoso - MyLibrary - Copyright © Contoso 2024 - 1.2.0 - -``` - -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 - - false - -``` - -### Step 5: Migrate packages.config → PackageReference - -**BEFORE** (`packages.config`): - -```xml - - - - - - -``` - -**AFTER** (in `.csproj`): - -```xml - - - - - -``` - -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 `` section from `app.config` if present - -### Step 6: Remove Unnecessary Boilerplate - -Delete all of the following — the SDK provides sensible defaults: - -```xml - - - - - - - Debug - AnyCPU - {...} - Library - Properties - 512 - true - true - - - - - true - full - false - bin\Debug\ - DEBUG;TRACE - prompt - 4 - - - pdbonly - true - bin\Release\ - TRACE - prompt - 4 - - - - - - - - - - - - - - - - - -``` - -**Keep** only properties that differ from SDK defaults (e.g., `Exe`, `` if it differs from the assembly name, custom ``). - -### Step 7: Enable Modern Features - -After migration, consider enabling modern C# features: - -```xml - - net8.0 - enable - enable - latest - -``` - -- `enable` — enables nullable reference type analysis -- `enable` — auto-imports common namespaces (.NET 6+) -- `latest` — uses the latest C# language version (or specify e.g. `12.0`) - -## Complete Before/After Example - -**BEFORE** (legacy — 65 lines): - -```xml - - - - - Debug - AnyCPU - {12345678-1234-1234-1234-123456789ABC} - Library - Properties - MyLibrary - MyLibrary - v4.7.2 - 512 - true - - - true - full - false - bin\Debug\ - DEBUG;TRACE - prompt - 4 - - - pdbonly - true - bin\Release\ - TRACE - prompt - 4 - - - - - - - - - - - - - - - - - - - - -``` - -**AFTER** (SDK-style — 11 lines): - -```xml - - - net472 - - - - - - -``` - -## Common Migration Issues - -**Embedded resources:** files not in a standard location may need explicit includes: - -```xml - - - -``` - -**Content files with CopyToOutputDirectory:** these still need explicit entries: - -```xml - - - - -``` - -**Multi-targeting:** change the element name from singular to plural: - -```xml - -net8.0 - - -net472;net8.0 -``` - -**WPF/WinForms projects:** use the appropriate SDK or properties: - -```xml - - - - - - - true - - true - - -``` - -**Test projects:** use the standard SDK with test framework packages: - -```xml - - - net8.0 - false - - - - - - - -``` - -## 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 `true` and `` items for all packages. - -**Step 2:** Remove `Version` from each project's `PackageReference`: - -```xml - - - - - -``` - -## 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 - - - net8.0 - enable - enable - true - Contoso - Copyright © Contoso 2024 - - -``` - -**`Directory.Build.targets`** (for targets/tasks — placed at repo or src root): - -```xml - - - - - -``` - -**Keep in individual `.csproj` files** only what is project-specific: - -```xml - - - Exe - MyApp - - - - - - -``` - -## 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 - - - $(MSBuildThisFileDirectory)artifacts\bin\ - $(MSBuildThisFileDirectory)artifacts\obj\$(MSBuildProjectName)\ - -``` - -**Language settings:** - -```xml - - latest - enable - enable - latest-recommended - -``` - -**Assembly and package metadata:** - -```xml - - Contoso - Contoso Engineering - Copyright © Contoso $(CurrentYear) - Contoso Platform - MIT - https://github.com/contoso/platform - https://github.com/contoso/platform - -``` - -**Build behavior and warnings:** - -```xml - - true - - $(NoWarn);CS1591 - -``` - -**Code analysis:** - -```xml - - true - true - -``` - -**Common analyzer PackageReferences (apply to all projects):** - -```xml - - - all - runtime; build; native; contentfiles; analyzers - - -``` - -### What NOT to Put Here - -- **Project-specific TFMs** — each project should declare its own `` or `` -- **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 - - - -``` - -**Conditional targets based on project type:** - -```xml - - - - - - -``` - -**Late-bound property overrides (values that depend on SDK properties):** - -```xml - - - $(OutputPath)$(AssemblyName).xml - -``` - -**Post-build validation:** - -```xml - - - -``` - -## 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 - - - true - - - - - - - - - - - - - - - -``` - -## 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 - - - - - -``` - -**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 - - - latest - enable - true - - -``` - -**`src/Directory.Build.props`:** - -```xml - - - - - true - true - - -``` - -**`test/Directory.Build.props`:** - -```xml - - - - - false - $(NoWarn);CS1591 - - - - - - - - - -``` - -## Common Patterns - -### Pattern: Shared Analyzers via GlobalPackageReference - -In `Directory.Packages.props`: - -```xml - - - - -``` - -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 - - - false - true - -``` - -In `Directory.Build.targets`: - -```xml - - - false - - - - true - -``` - -### Pattern: Before/After Repository Cleanup - -**Before — duplicated settings in every .csproj:** - -```xml - - - - net8.0 - latest - enable - enable - true - Contoso - Contoso Engineering - - - - - - - - - - - net8.0 - latest - enable - enable - true - Contoso - Contoso Engineering - - - - - - -``` - -**After — centralized with Directory.Build files:** - -```xml - - - - latest - enable - enable - true - Contoso - Contoso Engineering - - - - - - - true - - - - - - - - - - - - - - net8.0 - - - - - - - - - - net8.0 - - - - - -``` - -### Pattern: Artifact Output Layout (.NET 8+) - -In `Directory.Build.props`: - -```xml - - $(MSBuildThisFileDirectory)artifacts - -``` - -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 `` 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. \ No newline at end of file diff --git a/src/dotnet-msbuild/copilot-extension/src/knowledge/performance.lock.md b/src/dotnet-msbuild/copilot-extension/src/knowledge/performance.lock.md index 4b3bef34..ce1b5c33 100644 --- a/src/dotnet-msbuild/copilot-extension/src/knowledge/performance.lock.md +++ b/src/dotnet-msbuild/copilot-extension/src/knowledge/performance.lock.md @@ -1,4 +1,4 @@ - + # Build Performance Baseline & Optimization diff --git a/src/dotnet-msbuild/copilot-extension/src/knowledge/style-guide.lock.md b/src/dotnet-msbuild/copilot-extension/src/knowledge/style-and-modernization.lock.md similarity index 61% rename from src/dotnet-msbuild/copilot-extension/src/knowledge/style-guide.lock.md rename to src/dotnet-msbuild/copilot-extension/src/knowledge/style-and-modernization.lock.md index 84028b60..b6bded53 100644 --- a/src/dotnet-msbuild/copilot-extension/src/knowledge/style-guide.lock.md +++ b/src/dotnet-msbuild/copilot-extension/src/knowledge/style-and-modernization.lock.md @@ -1,4 +1,4 @@ - + # 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 + + + + + + + + + +``` + +**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 + + + + + + + + + + + + + +``` + +**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,507 @@ When reviewing an MSBuild file, scan for these in order: --- +# MSBuild Modernization: Legacy to SDK-style Migration + +## Identifying Legacy vs SDK-style Projects + +**Legacy indicators:** + +- `` +- Explicit file lists (`` for every `.cs` file) +- `ToolsVersion` attribute on `` element +- `packages.config` file present +- `Properties\AssemblyInfo.cs` with assembly-level attributes + +**SDK-style indicators:** + +- `` attribute on root element +- Minimal content — a simple project may be 10–15 lines +- No explicit file includes (implicit globbing) +- `` 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 + + + + + + Debug + AnyCPU + Library + MyLibrary + MyLibrary + v4.7.2 + 512 + true + + + + +``` + +```xml + + + + net472 + + +``` + +## Migration Checklist: Legacy → SDK-style + +### Step 1: Replace Project Root Element + +**BEFORE:** + +```xml + + + + + + +``` + +**AFTER:** + +```xml + + + +``` + +Remove the XML declaration, `ToolsVersion`, `xmlns`, and both `` lines. The `Sdk` attribute replaces all of them. + +### Step 2: Set TargetFramework + +**BEFORE:** + +```xml + + v4.7.2 + +``` + +**AFTER:** + +```xml + + net472 + +``` + +**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 + + + + + + + + + + + + + + +``` + +**AFTER:** + +Delete all of these `` and `` 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 + + + +``` + +### 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 + + MyLibrary + A useful library + Contoso + MyLibrary + Copyright © Contoso 2024 + 1.2.0 + +``` + +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 + + false + +``` + +### Step 5: Migrate packages.config → PackageReference + +**BEFORE** (`packages.config`): + +```xml + + + + + + +``` + +**AFTER** (in `.csproj`): + +```xml + + + + + +``` + +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 `` section from `app.config` if present + +### Step 6: Remove Unnecessary Boilerplate + +Delete all of the following — the SDK provides sensible defaults: + +```xml + + + + + + + Debug + AnyCPU + {...} + Library + Properties + 512 + true + true + + + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + + + + + + + + + + + + + + +``` + +**Keep** only properties that differ from SDK defaults (e.g., `Exe`, `` if it differs from the assembly name, custom ``). + +### Step 7: Enable Modern Features + +After migration, consider enabling modern C# features: + +```xml + + net8.0 + enable + enable + latest + +``` + +- `enable` — enables nullable reference type analysis +- `enable` — auto-imports common namespaces (.NET 6+) +- `latest` — uses the latest C# language version (or specify e.g. `12.0`) + +## Complete Before/After Example + +**BEFORE** (legacy — 65 lines): + +```xml + + + + + Debug + AnyCPU + {12345678-1234-1234-1234-123456789ABC} + Library + Properties + MyLibrary + MyLibrary + v4.7.2 + 512 + true + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + + + + + + + + + + + + + + + + + +``` + +**AFTER** (SDK-style — 11 lines): + +```xml + + + net472 + + + + + + +``` + +## Common Migration Issues + +**Embedded resources:** files not in a standard location may need explicit includes: + +```xml + + + +``` + +**Content files with CopyToOutputDirectory:** these still need explicit entries: + +```xml + + + + +``` + +**Multi-targeting:** change the element name from singular to plural: + +```xml + +net8.0 + + +net472;net8.0 +``` + +**WPF/WinForms projects:** use the appropriate SDK or properties: + +```xml + + + + + + + true + + true + + +``` + +**Test projects:** use the standard SDK with test framework packages: + +```xml + + + net8.0 + false + + + + + + + +``` + +## 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 `true` and `` items for all packages. + +**Step 2:** Remove `Version` from each project's `PackageReference`: + +```xml + + + + + +``` + +## 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 + + + net8.0 + enable + enable + true + Contoso + Copyright © Contoso 2024 + + +``` + +**`Directory.Build.targets`** (for targets/tasks — placed at repo or src root): + +```xml + + + + + +``` + +**Keep in individual `.csproj` files** only what is project-specific: + +```xml + + + Exe + MyApp + + + + + + +``` + +## 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` + +--- + +## directory-build-organization + # Organizing Build Infrastructure with Directory.Build Files ## Directory.Build.props vs Directory.Build.targets @@ -616,124 +1167,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 - - - $(MSBuildThisFileDirectory)artifacts\bin\ - $(MSBuildThisFileDirectory)artifacts\obj\$(MSBuildProjectName)\ - + + + latest + enable + enable + true + true + Contoso + Contoso Engineering + + ``` -**Language settings:** - -```xml - - latest - enable - enable - latest-recommended - -``` - -**Assembly and package metadata:** - -```xml - - Contoso - Contoso Engineering - Copyright © Contoso $(CurrentYear) - Contoso Platform - MIT - https://github.com/contoso/platform - https://github.com/contoso/platform - -``` - -**Build behavior and warnings:** - -```xml - - true - - $(NoWarn);CS1591 - -``` - -**Code analysis:** - -```xml - - true - true - -``` - -**Common analyzer PackageReferences (apply to all projects):** - -```xml - - - all - runtime; build; native; contentfiles; analyzers - - -``` - -### What NOT to Put Here - -- **Project-specific TFMs** — each project should declare its own `` or `` -- **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 - - - -``` + + + + -**Conditional targets based on project type:** - -```xml - - - - - - -``` - -**Late-bound property overrides (values that depend on SDK properties):** - -```xml - - - $(OutputPath)$(AssemblyName).xml - -``` - -**Post-build validation:** - -```xml - - - + + + $(OutputPath)$(AssemblyName).xml + + ``` ## Directory.Packages.props (Central Package Management) @@ -782,54 +1249,29 @@ 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. +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 Directory.Build Examples -**Add this at the TOP of inner `Directory.Build.props` files:** +Full file examples for a typical multi-level repo layout. + +## Repo-level `Directory.Build.props` ```xml - - - -``` - -**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 - latest enable true + ``` -**`src/Directory.Build.props`:** +## `src/Directory.Build.props` ```xml + @@ -837,13 +1279,15 @@ repo/ true true + ``` -**`test/Directory.Build.props`:** +## `test/Directory.Build.props` ```xml + @@ -858,56 +1302,18 @@ repo/ + ``` -## Common Patterns - -### Pattern: Shared Analyzers via GlobalPackageReference - -In `Directory.Packages.props`: - -```xml - - - - -``` - -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 - - - false - true - -``` - -In `Directory.Build.targets`: - -```xml - - - false - - - - true - -``` - -### Pattern: Before/After Repository Cleanup +## Before/After: Centralizing Duplicated Settings **Before — duplicated settings in every .csproj:** ```xml + net8.0 latest @@ -917,14 +1323,17 @@ In `Directory.Build.targets`: Contoso Contoso Engineering + + + net8.0 latest @@ -934,369 +1343,12 @@ In `Directory.Build.targets`: Contoso Contoso Engineering + - -``` -**After — centralized with Directory.Build files:** - -```xml - - - - latest - enable - enable - true - Contoso - Contoso Engineering - - - - - - - true - - - - - - - - - - - - - - net8.0 - - - - - - - - - - net8.0 - - - - - -``` - -### Pattern: Artifact Output Layout (.NET 8+) - -In `Directory.Build.props`: - -```xml - - $(MSBuildThisFileDirectory)artifacts - -``` - -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 `` 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. - ---- - -## check-bin-obj-clash - -# Detecting OutputPath and IntermediateOutputPath Clashes - -## Overview - -This skill helps identify when multiple MSBuild project evaluations share the same `OutputPath` or `IntermediateOutputPath`. This is a common source of build failures including: - -- File access conflicts during parallel builds -- Missing or overwritten output files -- Intermittent build failures -- "File in use" errors -- **NuGet restore errors like `Cannot create a file when that file already exists`** - this strongly indicates multiple projects share the same `IntermediateOutputPath` where `project.assets.json` is written - -Clashes can occur between: -- **Different projects** sharing the same output directory -- **Multi-targeting builds** (e.g., `TargetFrameworks=net8.0;net9.0`) where the path doesn't include the target framework -- **Multiple solution builds** where the same project is built from different solutions in a single build - -**Note:** Project instances with `BuildProjectReferences=false` should be **ignored** when analyzing clashes - these are P2P reference resolution builds that only query metadata (via `GetTargetPath`) and do not actually write to output directories. - -## When to Use This Skill - -**Invoke this skill immediately when you see:** -- `Cannot create a file when that file already exists` during NuGet restore -- `The process cannot access the file because it is being used by another process` -- Intermittent build failures that succeed on retry -- Missing output files or unexpected overwriting - -## Step 1: Generate a Binary Log - -Use the `binlog-generation` skill to generate a binary log with the correct naming convention. - -## Step 2: Load the Binary Log - -``` -load_binlog with path: "" -``` - -## Step 3: List All Projects - -``` -list_projects with binlog_file: "" -``` - -This returns all projects with their IDs and file paths. - -## Step 4: Get Evaluations for Each Project - -For each unique project file path, list its evaluations: - -``` -list_evaluations with: - - binlog_file: "" - - projectFilePath: "" -``` - -Multiple evaluations for the same project indicate multi-targeting or multiple build configurations. - -## Step 5: Check Global Properties for Each Evaluation - -For each evaluation, get the global properties to understand the build configuration: - -``` -get_evaluation_global_properties with: - - binlog_file: "" - - evaluationId: -``` - -Look for properties like `TargetFramework`, `Configuration`, `Platform`, and `RuntimeIdentifier` that should differentiate output paths. - -Also check **solution-related properties** to identify multi-solution builds: -- `SolutionFileName`, `SolutionName`, `SolutionPath`, `SolutionDir`, `SolutionExt` — differ when a project is built from multiple solutions -- `CurrentSolutionConfigurationContents` — the number of project entries reveals which solution an evaluation belongs to (e.g., 1 project vs ~49 projects) - -Look for **extra global properties that don't affect output paths** but create distinct MSBuild project instances: -- `PublishReadyToRun` — a publish setting that doesn't change `OutputPath` or `IntermediateOutputPath`, but MSBuild treats it as a distinct project instance, preventing result caching and causing redundant target execution (e.g., `CopyFilesToOutputDirectory` running again) -- Any other global property that differs between evaluations but doesn't contribute to path differentiation - -### Filter Out Non-Build Evaluations - -When analyzing clashes, filter evaluations based on the type of clash you're investigating: - -1. **For OutputPath clashes**: Exclude restore-phase evaluations (where `MSBuildRestoreSessionId` global property is set). These don't write to output directories. - -2. **For IntermediateOutputPath clashes**: Include restore-phase evaluations, as NuGet restore writes `project.assets.json` to the intermediate output path. - -3. **Always exclude `BuildProjectReferences=false`**: These are P2P metadata queries, not actual builds that write files. - -## Step 6: Get Output Paths for Each Evaluation - -For each evaluation, retrieve the `OutputPath` and `IntermediateOutputPath`: - -``` -get_evaluation_properties_by_name with: - - binlog_file: "" - - evaluationId: - - propertyNames: ["OutputPath", "IntermediateOutputPath", "BaseOutputPath", "BaseIntermediateOutputPath", "TargetFramework", "Configuration", "Platform"] -``` - -## Step 7: Identify Clashes - -Compare the `OutputPath` and `IntermediateOutputPath` values across all evaluations: - -1. **Normalize paths** - Convert to absolute paths and normalize separators -2. **Group by path** - Find evaluations that share the same OutputPath or IntermediateOutputPath -3. **Report clashes** - Any group with more than one evaluation indicates a clash - -## Step 8: Verify Clashes via CopyFilesToOutputDirectory (Optional) - -As additional evidence for OutputPath clashes, check if multiple project builds execute the `CopyFilesToOutputDirectory` target to the same path. Note that not all clashes manifest here - compilation outputs and other targets may also conflict. - -``` -search_binlog with: - - binlog_file: "" - - query: "$target CopyFilesToOutputDirectory project(.csproj)" -``` - -Then for each project ID that ran this target, examine the Copy task messages: - -``` -list_tasks_in_target with: - - binlog_file: "" - - projectId: - - targetId: -``` - -Look for evidence of clashes in the messages: -- `Copying file from "..." to "..."` - Active file writes -- `Did not copy from file "..." to file "..." because the "SkipUnchangedFiles" parameter was set to "true"` - Indicates a second build attempted to write to the same location - -The `SkipUnchangedFiles` skip message often masks clashes - the build succeeds but is vulnerable to race conditions in parallel builds. - -## Step 9: Check CoreCompile Execution Patterns (Optional) - -To understand which project instance did the actual compilation vs redundant work, check `CoreCompile`: - -``` -search_binlog with: - - binlog_file: "" - - query: "$target CoreCompile project(.csproj)" -``` - -Compare the durations: -- The instance with a long `CoreCompile` duration (e.g., seconds) is the **primary build** that did the actual compilation -- Instances where `CoreCompile` was skipped (duration ~0-10ms) are **redundant builds** — they didn't recompile but may still run other targets like `CopyFilesToOutputDirectory` that write to the same output directory - -This helps distinguish the "real" build from redundant instances created by extra global properties or multi-solution builds. - -### Caveat: `under()` Search in Multi-Solution Builds - -When using `search_binlog` with `under($project SolutionName)` to determine which solution a project instance belongs to, be aware that `under()` matches through the **entire build hierarchy**. If both solutions share a common ancestor (e.g., Arcade SDK's `Build.proj`), all project instances will appear "under" both solutions. - -Instead, use `get_evaluation_global_properties` and compare the `SolutionFileName` / `CurrentSolutionConfigurationContents` properties to reliably determine which solution an evaluation belongs to. - -### Expected Output Structure - -For each evaluation, collect: -- Project file path -- Evaluation ID -- TargetFramework (if multi-targeting) -- Configuration -- OutputPath -- IntermediateOutputPath - -### Clash Detection Logic - -``` -For each unique OutputPath: - - If multiple evaluations share it → CLASH - -For each unique IntermediateOutputPath: - - If multiple evaluations share it → CLASH -``` - -## Common Causes and Fixes - -### Multi-targeting without TargetFramework in path - -**Problem:** Project uses `TargetFrameworks` but OutputPath doesn't vary by framework. - -```xml - -bin\$(Configuration)\ -``` - -**Fix:** Include TargetFramework in the path: - -```xml - -bin\$(Configuration)\$(TargetFramework)\ -``` - -Or rely on SDK defaults which handle this automatically: - -```xml -true -true -``` - -### Shared output directory across projects (CANNOT be fixed with AppendTargetFramework) - -**Problem:** Multiple projects explicitly set the same `BaseOutputPath` or `BaseIntermediateOutputPath`. - -```xml - -..\SharedOutput\ -..\SharedObj\ - - -..\SharedOutput\ -..\SharedObj\ -``` - -**IMPORTANT:** Even with `AppendTargetFrameworkToOutputPath=true`, this will still clash! .NET writes certain files directly to the `IntermediateOutputPath` without the TargetFramework suffix, including: - -- `project.assets.json` (NuGet restore output) -- Other NuGet-related files - -This causes errors like `Cannot create a file when that file already exists` during parallel restore. - -**Fix:** Each project MUST have a unique `BaseIntermediateOutputPath`. Do not share intermediate output directories across projects: - -```xml - -..\obj\ProjectA\ - - -..\obj\ProjectB\ -``` - -Or simply use the SDK defaults which place `obj` inside each project's directory. - -### RuntimeIdentifier builds clashing - -**Problem:** Building for multiple RIDs without RID in path. - -**Fix:** Ensure RuntimeIdentifier is in the path: - -```xml -true -``` - -### Multiple solutions building the same project - -**Problem:** A single build invokes multiple solutions (e.g., via MSBuild task or command line) that include the same project. Each solution build evaluates and builds the project independently, with different `Solution*` global properties that don't affect the output path. - -**How to detect:** Compare `SolutionFileName` and `CurrentSolutionConfigurationContents` across evaluations for the same project. Different values indicate multi-solution builds. For example: - -| Property | Eval from Solution A | Eval from Solution B | -|---|---|---| -| `SolutionFileName` | `BuildAnalyzers.sln` | `Main.slnx` | -| `CurrentSolutionConfigurationContents` | 1 project entry | ~49 project entries | -| `OutputPath` | `bin\Release\netstandard2.0\` | `bin\Release\netstandard2.0\` ← **clash** | - -**Example:** A repo build script builds `BuildAnalyzers.sln` then `Main.slnx`, and both solutions include `SharedAnalyzers.csproj`. Both builds write to `bin\Release\netstandard2.0\`. The first build compiles; the second skips compilation but still runs `CopyFilesToOutputDirectory`. + - - $(MSBuildThisFileDirectory)artifacts\bin\ - $(MSBuildThisFileDirectory)artifacts\obj\$(MSBuildProjectName)\ - + + + latest + enable + enable + true + true + Contoso + Contoso Engineering + + ``` -**Language settings:** - -```xml - - latest - enable - enable - latest-recommended - -``` - -**Assembly and package metadata:** - -```xml - - Contoso - Contoso Engineering - Copyright © Contoso $(CurrentYear) - Contoso Platform - MIT - https://github.com/contoso/platform - https://github.com/contoso/platform - -``` - -**Build behavior and warnings:** - -```xml - - true - - $(NoWarn);CS1591 - -``` - -**Code analysis:** - -```xml - - true - true - -``` - -**Common analyzer PackageReferences (apply to all projects):** - -```xml - - - all - runtime; build; native; contentfiles; analyzers - - -``` - -### What NOT to Put Here - -- **Project-specific TFMs** — each project should declare its own `` or `` -- **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 - - - -``` + + + + -**Conditional targets based on project type:** - -```xml - - - - - - -``` - -**Late-bound property overrides (values that depend on SDK properties):** - -```xml - - - $(OutputPath)$(AssemblyName).xml - -``` - -**Post-build validation:** - -```xml - - - + + + $(OutputPath)$(AssemblyName).xml + + ``` ## 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 @@ -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 - - - latest - enable - true - - -``` - -**`src/Directory.Build.props`:** - -```xml - - - - - true - true - - -``` - -**`test/Directory.Build.props`:** - -```xml - - - - - false - $(NoWarn);CS1591 - - - - - - - - - -``` - -## Common Patterns - -### Pattern: Shared Analyzers via GlobalPackageReference - -In `Directory.Packages.props`: - -```xml - - - - -``` - -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 - - - false - true - -``` - -In `Directory.Build.targets`: - -```xml - - - false - - - - true - -``` - -### Pattern: Before/After Repository Cleanup - -**Before — duplicated settings in every .csproj:** - -```xml - - - - net8.0 - latest - enable - enable - true - Contoso - Contoso Engineering - - - - - - - - - - - net8.0 - latest - enable - enable - true - Contoso - Contoso Engineering - - - - - - -``` - -**After — centralized with Directory.Build files:** - -```xml - - - - latest - enable - enable - true - Contoso - Contoso Engineering - - - - - - - true - - - - - - - - - - - - - - net8.0 - - - - - - - - - - net8.0 - - - - - -``` - -### Pattern: Artifact Output Layout (.NET 8+) - -In `Directory.Build.props`: - -```xml - - $(MSBuildThisFileDirectory)artifacts - -``` - -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 `$(MSBuildThisFileDirectory)artifacts` 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 diff --git a/src/dotnet-msbuild/skills/directory-build-organization/references/common-patterns.md b/src/dotnet-msbuild/skills/directory-build-organization/references/common-patterns.md new file mode 100644 index 00000000..1b86b4cf --- /dev/null +++ b/src/dotnet-msbuild/skills/directory-build-organization/references/common-patterns.md @@ -0,0 +1,56 @@ +# Common Directory.Build Patterns + +## Conditional Settings by Project Type + +Detect test projects by naming convention in `Directory.Build.props`: + +```xml + + false + true + +``` + +Use `Directory.Build.targets` for conditions on SDK-defined properties like `OutputType`: + +```xml + + false + + + + true + +``` + +## Post-Build Validation + +Validate that `Pack` produced the expected output: + +```xml + + + +``` + +## 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/ +``` diff --git a/src/dotnet-msbuild/skills/directory-build-organization/references/multi-level-examples.md b/src/dotnet-msbuild/skills/directory-build-organization/references/multi-level-examples.md new file mode 100644 index 00000000..e8ac22f0 --- /dev/null +++ b/src/dotnet-msbuild/skills/directory-build-organization/references/multi-level-examples.md @@ -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 + + + + latest + enable + true + + + +``` + +## `src/Directory.Build.props` + +```xml + + + + + + true + true + + + +``` + +## `test/Directory.Build.props` + +```xml + + + + + + false + $(NoWarn);CS1591 + + + + + + + + + + +``` + +## Before/After: Centralizing Duplicated Settings + +**Before — duplicated settings in every .csproj:** + +```xml + + + + + net8.0 + latest + enable + enable + true + Contoso + Contoso Engineering + + + + + + + + + + + + + + net8.0 + latest + enable + enable + true + Contoso + Contoso Engineering + + + + + + + + +``` + +**After — centralized with Directory.Build files:** + +```xml + + + + + latest + enable + enable + true + Contoso + Contoso Engineering + + + + + + + + + true + + + + + + + + + + + + + + + + + + net8.0 + + + + + + + + + + + + + net8.0 + + + + + + + +```