-
Notifications
You must be signed in to change notification settings - Fork 90
Add AGENTS.md and per-TFM MSBuild loader skills #381
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,80 @@ | ||
| --- | ||
| name: msbuild-loader-netcore | ||
| description: >- | ||
| How MSBuildLocator loads MSBuild assemblies and discovers the .NET SDK on .NET | ||
| / .NET Core (the net8.0 target / #if NETCOREAPP branches in src/MSBuildLocator). | ||
| Covers the AssemblyLoadContext.Default.Resolving handler, search-path probing, | ||
| the SDK environment variables set on registration, hostfxr-based SDK discovery, | ||
| the dotnet location probe order, and the AllowQueryAll* widening flags. Use when | ||
| editing or reviewing net8.0 loader, registration, or SDK-discovery code, or | ||
| diagnosing assembly-resolution behavior on .NET Core hosts. | ||
| --- | ||
|
|
||
| # .NET / .NET Core (`net8.0` / `NETCOREAPP`) MSBuild loader | ||
|
|
||
| Scope: `src/MSBuildLocator/MSBuildLocator.cs` `#if NETCOREAPP` branches, plus | ||
| `DotNetSdkLocationHelper.cs` and `NativeMethods.cs`. For build/test and | ||
| cross-cutting conventions, see `AGENTS.md`. | ||
|
|
||
| ## Assembly-resolution handler | ||
| - `s_registeredHandler` is a static | ||
| `Func<AssemblyLoadContext, AssemblyName, Assembly>`; registration hooks | ||
| `AssemblyLoadContext.Default.Resolving`. | ||
| - Resolving can fire repeatedly; successful loads are cached in the local | ||
| `loadedAssemblies` dictionary keyed by `AssemblyName.FullName`. | ||
| - Resolution is not thread-safe; keep the `loadedAssemblies` lock around cache | ||
| lookup, path probing, `Assembly.LoadFrom`, and cache insert. | ||
| - The resolver receives `AssemblyName` directly — do not parse an event-args | ||
| name string (that is the net46 path). | ||
| - For each registered `msbuildSearchPaths` entry, probe | ||
| `Path.Combine(msbuildPath, assemblyName.Name + ".dll")` and load with | ||
| `Assembly.LoadFrom(targetAssembly)`. | ||
|
|
||
| ## SDK environment variables (NETCOREAPP only) | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Would be great to have here some context why this env variables are important to msbuildLocator
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It would! I think we shouldn't need these any more and just finding the right SDK should do the trick. But I'm not proposing changing that right now |
||
| - Registration sets MSBuild SDK env vars via `ApplyDotNetSdkEnvironmentVariables`: | ||
| - `MSBUILD_EXE_PATH` = `<sdk>\MSBuild.dll` | ||
| - `MSBuildExtensionsPath` = `<sdk>` | ||
| - `MSBuildSDKsPath` = `<sdk>\Sdks` | ||
| - `RegisterMSBuildPath(string)` applies these for that path before registering. | ||
| - `RegisterMSBuildPath(string[])` applies these for the first search path only, | ||
| then registers all paths. | ||
| - `RegisterInstance` applies these only when | ||
| `instance.DiscoveryType == DiscoveryType.DotNetSdk`. | ||
| - Do not move this setup into net46; Framework has its own `MSBUILD_EXE_PATH` | ||
| compatibility branch under `#if NET46` (see the `msbuild-loader-netframework` | ||
| skill). | ||
|
|
||
| ## SDK discovery | ||
| - Lives in `DotNetSdkLocationHelper`; instances are | ||
| `VisualStudioInstance(name: ".NET Core SDK", ..., DiscoveryType.DotNetSdk)`. | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I am confused, why this is called VisualStudioInstance but we have here DiscoveryType.DotNetSdk?
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It's misnamed. It should be InstalledMSBuild or something. (We picked the name before supporting finding the SDK)
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
| - Uses `NativeMethods` hostfxr P/Invoke under `#if NETCOREAPP` only: | ||
| - `hostfxr_resolve_sdk2` — best SDK, honoring `global.json` via | ||
| `WorkingDirectory`. | ||
| - `hostfxr_get_available_sdks` — installed SDK enumeration. | ||
| - `ResolveDotnetPathCandidates` preference order (tried in order): | ||
| `DOTNET_ROOT` (`DOTNET_ROOT(x86)` in a 32-bit process) → current process | ||
| directory when running under `dotnet` → `DOTNET_HOST_PATH` → | ||
| `DOTNET_MSBUILD_SDK_RESOLVER_CLI_DIR` → `PATH`. | ||
| - A successful `hostfxr_resolve_sdk2` sets `DOTNET_HOST_PATH` (if empty) to | ||
| `<dotnetPath>\dotnet(.exe)`. | ||
| - Default query returns the best SDK first, then unique SDK versions | ||
| newest-first from the available SDKs. | ||
|
|
||
| ## Widening flags | ||
| - `AllowQueryAllRuntimeVersions` / `VisualStudioInstanceQueryOptions.AllowAllRuntimeVersions` | ||
| include SDKs whose major/minor runtime exceeds `Environment.Version`. | ||
| - `AllowQueryAllDotnetLocations` / `VisualStudioInstanceQueryOptions.AllowAllDotnetLocations` | ||
| keep probing all dotnet candidate locations instead of stopping after the | ||
| first location that has SDKs. | ||
|
|
||
| ## What net8.0 does NOT do | ||
| - No Developer Console or Visual Studio Setup COM discovery; | ||
| `FEATURE_VISUALSTUDIOSETUP` package references/constants are net46-only in the | ||
| csproj. | ||
|
|
||
| ## Register-before-load contract | ||
| - `CanRegister` is false when already registered, or once any signed | ||
| `Microsoft.Build*` core assembly is loaded. | ||
| - JIT caveat: JIT-compilation of a method referencing `Microsoft.Build` types is | ||
| enough to load those assemblies and break registration. Keep locator calls | ||
| isolated before any such reference. | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,67 @@ | ||
| --- | ||
| name: msbuild-loader-netframework | ||
| description: >- | ||
| How MSBuildLocator loads MSBuild assemblies and discovers installs on .NET | ||
| Framework (the net46 target / #if NET46 / non-NETCOREAPP branches in | ||
| src/MSBuildLocator). Covers the AppDomain.AssemblyResolve handler, search-path | ||
| probing, the pre-17.1 MSBuild.exe x86/amd64 fix, and Developer Console + Visual | ||
| Studio Setup (COM) discovery. Use when editing or reviewing net46 loader, | ||
| registration, or VS-discovery code, or diagnosing assembly-resolution behavior | ||
| on .NET Framework hosts. | ||
| --- | ||
|
|
||
| # .NET Framework (`net46`) MSBuild loader | ||
|
|
||
| Scope: `src/MSBuildLocator/MSBuildLocator.cs` `#if NET46` and Framework (`#else` | ||
| of `NETCOREAPP`) branches. `FEATURE_VISUALSTUDIOSETUP` is defined only when | ||
| `TargetFramework == net46` in `Microsoft.Build.Locator.csproj`. For build/test | ||
| and cross-cutting conventions, see `AGENTS.md`. | ||
|
|
||
| ## Assembly-resolution handler | ||
| - `s_registeredHandler` is a static `ResolveEventHandler`; `IsRegistered` is | ||
| `s_registeredHandler != null`. | ||
| - `RegisterMSBuildPathsInternally` stores the handler in the static field before | ||
| subscribing to `AppDomain.CurrentDomain.AssemblyResolve`; the static field | ||
| keeps the delegate alive so it persists. | ||
| - `AssemblyResolve` can fire repeatedly for the same assembly; results are cached | ||
| in `loadedAssemblies` keyed by `AssemblyName.FullName`. | ||
| - Resolution is explicitly not thread-safe; every cache lookup/load runs under | ||
| `lock (loadedAssemblies)`. | ||
| - Handler path: parse `eventArgs.Name` with `new AssemblyName(eventArgs.Name)`; | ||
| for each registered search path, if `<msbuildPath>\<Name>.dll` exists, return | ||
| `Assembly.LoadFrom(targetAssembly)`. | ||
| - Search paths come from `RegisterMSBuildPath(...)`, or from | ||
| `RegisterInstance(...)` as `instance.MSBuildPath` plus the VS NuGet path when | ||
| it exists. | ||
|
|
||
| ## Pre-17.1 MSBuild.exe bitness fix | ||
| - net46 reads `FileVersionInfo` from the first registered path containing | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Mb add here why this bug exists
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah this is one it backed out from the code and it should probably just be dropped |
||
| `MSBuild.exe`. | ||
| - For versions `< 17.1`, set `MSBUILD_EXE_PATH` to drive MSBuild's own lookup. | ||
| - If that pre-17.1 path ends in `\amd64`, strip the trailing folder and point | ||
| `MSBUILD_EXE_PATH` at the sibling x86 `MSBuild.exe`. | ||
|
|
||
| ## Discovery sources (net46 only) | ||
| - Developer command prompt: `GetDevConsoleInstance()` reads `VSINSTALLDIR`, then | ||
| parses `VSCMD_VER` (trimming any suffix after `-`), then falls back to | ||
| `VisualStudioVersion`; yields `DiscoveryType.DeveloperConsole`. | ||
| - Visual Studio Setup COM API: under `FEATURE_VISUALSTUDIOSETUP`, | ||
| `VisualStudioLocationHelper.GetInstances()` enumerates VS 2017+ setup instances | ||
| with `Microsoft.Component.MSBuild`; yields `DiscoveryType.VisualStudioSetup`. | ||
| - `DiscoveryType.DotNetSdk` exists in the enum but belongs to the Core path; net46 | ||
| `GetInstances(...)` does not call SDK discovery. | ||
|
|
||
| ## What net46 does NOT do | ||
| - No `AssemblyLoadContext`, `hostfxr`, or `.NET SDK` discovery — those are | ||
| `#if NETCOREAPP` paths (see the `msbuild-loader-netcore` skill). | ||
| - `ApplyDotNetSdkEnvironmentVariables(...)` exists in the file, but both | ||
| `RegisterMSBuildPath(...)` call sites that invoke it are `#if NETCOREAPP`. Do | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Actually we have three ApplyDotNetSdkEnvironmentVariables calls, and this third one not mentioned here is not if-gated but is guarded at runtime by DiscoveryType == DotNetSdk |
||
| not assume SDK env vars (`MSBUILD_EXE_PATH` to `MSBuild.dll`, | ||
| `MSBuildExtensionsPath`, `MSBuildSDKsPath`) are set on net46. | ||
|
|
||
| ## Register-before-load contract | ||
| - `CanRegister` is false once any strong-named `Microsoft.Build*` assembly in | ||
| `s_msBuildAssemblies` is loaded in the current `AppDomain`. | ||
| - JIT caveat: a method that references `Microsoft.Build` types can trip the | ||
| contract when JIT-compiled, even if that reference never executes. Keep locator | ||
| calls isolated before any such reference. | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,31 @@ | ||
| # Microsoft.Build.Locator — Copilot instructions | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is an agent smart enough to know what if we change logic described in skills agent should change skill as well? |
||
|
|
||
| This library exists to locate an MSBuild install (Visual Studio or .NET SDK) and register an assembly-resolution handler so the host app loads MSBuild's assemblies from that install. This is required for any use of the .NET API from an application that is not part of Visual Studio or the .NET SDK. | ||
|
|
||
| A .NET Framework application can only locate MSBuild from a Visual Studio installation, and a .NET 8+ application can only locate MSBuild from a .NET SDK. This library's approach to finding the library and loading the assemblies is entirely different on the different runtimes. See .agents/skills/msbuild-loader-netcore/SKILL.md and .agents/skills/msbuild-loader-netframework/SKILL.md for details. | ||
|
|
||
| The core `Microsoft.Build.Locator.dll` must have minimal dependencies—nothing outside the core libraries provided by .NET for the relevant TargetFramework. | ||
|
|
||
| ## Build / test (root `MSBuildLocator.sln`, .NET CLI) | ||
| - `dotnet restore` / `dotnet build` / `dotnet test` | ||
| - Single test: `dotnet test --filter "FullyQualifiedName~QueryInstancesTests"` or `--filter "Name=<Method>"` | ||
| - Tests: xUnit + Shouldly, in `src/MSBuildLocator.Tests`. | ||
| - Versioning: Nerdbank.GitVersioning. Use SemVer 2 and update `version.json` on breaking changes or feature additions. | ||
|
|
||
| ## Multi-targeting (central constraint) | ||
| Library: `net46` + `net8.0`. Tests: `net472` + `net8.0`. Non-trivial code forks per TFM via `#if NETCOREAPP`, `#if NET46`, and `FEATURE_VISUALSTUDIOSETUP` (defined only for `net46`). Always check whether a change must be mirrored or excluded across these conditionals; for what each fork actually does, load the `msbuild-loader-netcore` or `msbuild-loader-netframework` skill. | ||
|
|
||
| ## Architecture (namespace `Microsoft.Build.Locator`) | ||
| - `MSBuildLocator.cs` — entry point: `RegisterDefaults`, `RegisterInstance`, `RegisterMSBuildPath`, `QueryVisualStudioInstances`, `CanRegister`, handler registration. Both TFM forks live here. `Unregister()` is kept only for back-compat and is a no-op (the resolver is never removed). | ||
| - `DotNetSdkLocationHelper.cs`, `NativeMethods.cs` — `.NET SDK` discovery (hostfxr); see the `msbuild-loader-netcore` skill. | ||
| - `VisualStudioLocationHelper.cs` — Visual Studio Setup (COM) discovery; see the `msbuild-loader-netframework` skill. | ||
| - `VisualStudioInstance.cs`, `VisualStudioInstanceQueryOptions.cs`, `DiscoveryType.cs` — result/option types. | ||
| - `Utils/SemanticVersion*.cs`, `VersionComparer.cs` — internal SemVer parse/compare to order instances. | ||
| - Props/targets shipped from `src/MSBuildLocator/build/` (packed to `build/` + `buildTransitive/`). `EnsureMSBuildAssembliesNotCopied` emits error **MSBL001** when a consumer references `Microsoft.Build*` packages without `PrivateAssets="all"` / `ExcludeAssets="runtime"` (copying those assemblies locally breaks redirection). Keep the target's package list in sync with MSBuild's layout. | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This point seems a little confusing if a human will read this. (at least for me)
• Why a local copy breaks it. The resolve handler only fires on a miss — when the loader can't find an assembly. If a copy of Microsoft.Build.dll sits in the bin /output folder, the loader finds that one and the handler never runs.
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @copilot make this fix |
||
|
|
||
| ## Conventions | ||
| - Contract-stable public API: csproj `EnablePackageValidation` + `PackageValidationBaselineVersion` (1.6.1). Intentional API changes require updating `src/MSBuildLocator/CompatibilitySuppressions.xml` and an appropriate semver update. | ||
| - XML doc comments on public members; match existing style. | ||
| - Strong-name signed (`key.snk`) — don't remove signing. | ||
| - Build settings centralized in `Directory.Build.props` / `Directory.Solution.props` / `Directory.Build.rsp` — edit there, not per-project. | ||
| - Register-before-load contract: callers must register via Locator BEFORE any `Microsoft.Build.*` type loads (`CanRegister` → false once loaded). Preserve this + the lazy-loading patterns protecting it when refactoring. | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would be great to have here non-window specific logic listed, how the flow differs for windows vs other OS
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There shouldn't be a difference beyond there not being a net472 option off of Windows.