Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
80 changes: 80 additions & 0 deletions .agents/skills/msbuild-loader-netcore/SKILL.md
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

Copy link
Copy Markdown

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

Copy link
Copy Markdown
Member Author

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.

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)

Copy link
Copy Markdown

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 some context why this env variables are important to msbuildLocator

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The 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)`.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The 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?
Does VisualStudioInstance really represent installed visual studio or installed msbuild?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The 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)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The 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.
67 changes: 67 additions & 0 deletions .agents/skills/msbuild-loader-netframework/SKILL.md
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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mb add here why this bug exists

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The 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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The 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.
31 changes: 31 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# Microsoft.Build.Locator — Copilot instructions

Copy link
Copy Markdown

Choose a reason for hiding this comment

The 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.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The 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)
I suggest to write it in simple words and into two different bullet points:

  1. The core rule of MSBuildLocator: never ship MSBuild's DLLs with the app.
    The app should ship without  Microsoft.Build.dll . At runtime, Locator's registered handler redirects those loads to the real MSBuild install (VS or SDK).

• 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.
• MSBL001 safeguard. The Locator NuGet package ships an MSBuild  .targets  file that runs inside the consuming build. The  EnsureMSBuildAssembliesNotCopied  target scans the resolved packages for any MSBuild package about to be copied locally. If it finds one, the build fails with error MSBL001, which names the offending package and asks for two attributes.
• The fix — two attributes, on the flagged    in the consuming project's  .csproj . Add  ExcludeAssets="runtime"  (stops the DLL from being copied to output) and  PrivateAssets="all"  (keeps the dependency from flowing to downstream consumers):

  1. Keep the target's package list in sync.
    The target checks a hardcoded list of package IDs ( Microsoft.Build ,  .Framework ,  .Utilities.Core ,  .Tasks.Core , …) — see  Microsoft.Build.Locator.targets#L3-L14 . If MSBuild ever ships a new redistributable assembly that also shouldn't be copied, it must be added here or the guard won't catch it.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The 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.
Loading