Skip to content

Resolve the versioned SDK directory in the Native AOT CLI#55110

Open
JeremyKuhne wants to merge 5 commits into
dotnet:mainfrom
JeremyKuhne:aot-sdk-root-resolution
Open

Resolve the versioned SDK directory in the Native AOT CLI#55110
JeremyKuhne wants to merge 5 commits into
dotnet:mainfrom
JeremyKuhne:aot-sdk-root-resolution

Conversation

@JeremyKuhne

@JeremyKuhne JeremyKuhne commented Jul 1, 2026

Copy link
Copy Markdown
Member

Summary

In a deployed SDK the muxer loads dotnet-aot.dll from the versioned SDK directory (sdk/<version>/), but inside that Native AOT bubble the BCL "where am I" APIs do not point at the SDK directory:

  • AppContext.BaseDirectory, Environment.ProcessPath, and Process.MainModule.FileName resolve to the muxer / install root.
  • Assembly.Location returns "" (and ILC errors with IL3000).

So SDK-relative resolution (MSBuild, DotnetTools/, forwarders) that keys off AppContext.BaseDirectory breaks in the AOT CLI. This gives the AOT bubble a single, correct notion of the versioned SDK directory.

Changes

  • Microsoft.DotNet.Cli.Utils.SdkPathsSdkDirectory resolves the Microsoft.DotNet.Sdk.Root AppContext value → the SDK assembly directory → AppContext.BaseDirectory (resolved once and cached).
  • SdkRootLocator (dotnet-aot) – prefers the host-provided sdk_dir; otherwise self-locates the dotnet-aot module (GetModuleHandleEx + GetModuleFileName via CsWin32 on Windows, dladdr on Unix).
  • NativeEntryPoint.ExecuteCore publishes the resolved directory as the process-local Microsoft.DotNet.Sdk.Root AppContext value so the compiled-in assemblies find it – unlike an environment variable it is not inherited by child processes. A caller-provided value (e.g. a runtimeconfig.json configProperties entry) is honored, but the bridge errors out if it is set and does not point to an existing directory. The AppBase / DotnetTools resolvers and --info's Base Path read SdkPaths.
  • BufferScope / TypeInfo helpers (ported from dotnet/msbuild) back the self-locate long-path grow loop.
  • Localized the new diagnostics via CliStrings (+ xlf).
  • The dn harness gains a -Layout Separated / -SelfLocate mode (run-dn.ps1) that mirrors the deployed muxer, with integration + unit tests.
  • Design note src/Cli/dotnet-aot/SdkRootResolution.md, plus the add-dotnet-aot-command and dotnet-aot-compat agent skills.

Testing

  • Microsoft.DotNet.Cli.Utils builds clean (net-current + net472); dotnet.csproj and the managed dotnet-aot build clean.
  • dotnet-aot.Tests (26) and BufferScopeTests (34) pass.
  • Validated on the real AOT binary: --info Base Path resolves to the versioned SDK subfolder in both the passed-sdk_dir and self-locate cases.

In a deployed SDK the muxer loads dotnet-aot.dll from the versioned SDK directory (sdk/<version>), but inside that native bubble the BCL location APIs (AppContext.BaseDirectory, Environment.ProcessPath, Assembly.Location) resolve to the install root, not the SDK directory, which breaks SDK-relative resolution (MSBuild, DotnetTools, forwarders).

Add Microsoft.DotNet.Cli.Utils.SdkPaths whose SdkDirectory resolves DOTNET_SDK_ROOT, then the SDK assembly directory, then AppContext.BaseDirectory (resolved once and cached).

Add SdkRootLocator in dotnet-aot: prefer the host-provided sdk_dir, else self-locate the dotnet-aot module (GetModuleHandleEx + GetModuleFileName via CsWin32 on Windows, dladdr on Unix). NativeEntryPoint publishes the resolved directory in DOTNET_SDK_ROOT, and the AppBase/DotnetTools resolvers and the --info Base Path now read SdkPaths.

Supporting changes: BufferScope/TypeInfo helpers for the self-locate grow loop, localized diagnostics via CliStrings, a dn separated/self-locate harness layout with tests, the SdkRootResolution design note, and the add-dotnet-aot-command and dotnet-aot-compat skills.
@JeremyKuhne JeremyKuhne marked this pull request as ready for review July 1, 2026 20:10
Copilot AI review requested due to automatic review settings July 1, 2026 20:10
@JeremyKuhne JeremyKuhne closed this Jul 1, 2026
@JeremyKuhne JeremyKuhne reopened this Jul 1, 2026

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

This PR introduces a consistent way for the NativeAOT CLI (dotnet-aot) and shared CLI code to resolve the versioned SDK directory (the sdk/<version>/ folder), even when BCL “where am I” APIs point at the install root due to the muxer directly loading the NativeAOT module.

Changes:

  • Add SdkPaths (and supporting BufferScope/TypeInfo) to provide a single canonical SDK-directory resolver shared by managed and AOT paths.
  • Implement SdkRootLocator to resolve sdk_dir (host-provided preferred; otherwise self-locate the loaded dotnet-aot module) and publish it via DOTNET_SDK_ROOT.
  • Update --info Base Path and SDK-relative command resolution to use SdkPaths, plus add unit/integration tests and a dn harness mode to emulate the deployed “separated” layout.

Reviewed changes

Copilot reviewed 36 out of 36 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
test/Microsoft.DotNet.Cli.Utils.Tests/Microsoft.DotNet.Cli.Utils.Tests.csproj Enables unsafe blocks for new buffer pinning tests.
test/Microsoft.DotNet.Cli.Utils.Tests/BufferScopeTests.cs Adds MSTest coverage for BufferScope<T> behavior (including unsafe fixed).
test/dotnet-aot.Tests/NativeEntryPointTests.cs Adds env restore for DOTNET_SDK_ROOT and tests publishing/resolution behavior.
test/dotnet-aot.Tests/dotnet-aot.Tests.csproj Links SdkRootLocator.cs into test build to match product closure.
test/dotnet-aot.Tests/AotIntegrationTests.cs Adds separated-layout --info Base Path integration tests (sdk_dir vs self-locate).
src/Cli/Microsoft.DotNet.Cli.Utils/TypeInfo.cs Adds a helper to determine reference-containing types for ArrayPool clearing.
src/Cli/Microsoft.DotNet.Cli.Utils/SdkPaths.cs Adds canonical SDK directory resolution (env var → assembly dir → AppContext).
src/Cli/Microsoft.DotNet.Cli.Utils/NativeMethods.txt Extends CsWin32 surface for GetModuleFileName and MAX_PATH.
src/Cli/Microsoft.DotNet.Cli.Utils/BufferScope.cs Adds pooled-buffer grow helper for long-path Win32 interop scenarios.
src/Cli/dotnet/xlf/CliStrings.zh-Hant.xlf Adds localized entries for new diagnostics.
src/Cli/dotnet/xlf/CliStrings.zh-Hans.xlf Adds localized entries for new diagnostics.
src/Cli/dotnet/xlf/CliStrings.tr.xlf Adds localized entries for new diagnostics.
src/Cli/dotnet/xlf/CliStrings.ru.xlf Adds localized entries for new diagnostics.
src/Cli/dotnet/xlf/CliStrings.pt-BR.xlf Adds localized entries for new diagnostics.
src/Cli/dotnet/xlf/CliStrings.pl.xlf Adds localized entries for new diagnostics.
src/Cli/dotnet/xlf/CliStrings.ko.xlf Adds localized entries for new diagnostics.
src/Cli/dotnet/xlf/CliStrings.ja.xlf Adds localized entries for new diagnostics.
src/Cli/dotnet/xlf/CliStrings.it.xlf Adds localized entries for new diagnostics.
src/Cli/dotnet/xlf/CliStrings.fr.xlf Adds localized entries for new diagnostics.
src/Cli/dotnet/xlf/CliStrings.es.xlf Adds localized entries for new diagnostics.
src/Cli/dotnet/xlf/CliStrings.de.xlf Adds localized entries for new diagnostics.
src/Cli/dotnet/xlf/CliStrings.cs.xlf Adds localized entries for new diagnostics.
src/Cli/dotnet/ParserOptionActions.cs Switches --info Base Path to SdkPaths.SdkDirectory.
src/Cli/dotnet/CommandFactory/CommandResolution/DotnetToolsCommandResolver.cs Resolves DotnetTools/ relative to SdkPaths.SdkDirectory.
src/Cli/dotnet/CommandFactory/CommandResolution/AppBaseDllCommandResolver.cs Resolves appbase .dll probing relative to SdkPaths.SdkDirectory.
src/Cli/dotnet/CommandFactory/CommandResolution/AppBaseCommandResolver.cs Resolves appbase commands relative to SdkPaths.SdkDirectory.
src/Cli/dotnet/CliStrings.resx Adds new diagnostics strings for SDK-dir and managed fallback resolution failures.
src/Cli/dotnet-aot/SdkRootResolution.md Adds design note documenting SDK-root resolution strategy.
src/Cli/dotnet-aot/SdkRootLocator.cs Implements host-preferred and self-locate fallback SDK directory resolution.
src/Cli/dotnet-aot/NativeEntryPoint.cs Publishes resolved SDK directory via DOTNET_SDK_ROOT and uses it for fallback.
src/Cli/dotnet-aot/dotnet-aot.csproj Includes SdkRootLocator.cs in the NativeAOT project build.
src/Cli/dn/run-dn.ps1 Adds harness script supporting separated layout and self-locate testing.
src/Cli/dn/Program.cs Adds env-driven SDK-dir override and optional “blank sdk_dir” test hook.
.github/skills/dotnet-aot-compat/SKILL.md Adds an agent skill doc for resolving trim/AOT analyzer warnings.
.github/skills/dotnet-aot-compat/references/polyfills.md Adds skill reference doc for polyfilling trim/AOT attributes on older TFMs.
.github/skills/add-dotnet-aot-command/SKILL.md Adds an agent skill doc for adding features to dotnet-aot and validating them.

Comment thread src/Cli/dotnet-aot/SdkRootLocator.cs Outdated
Comment thread src/Cli/dn/run-dn.ps1
Comment thread src/Cli/dotnet-aot/SdkRootResolution.md
SdkRootLocator: import dladdr under a sentinel library mapped by a DllImportResolver (libSystem on macOS, libdl then libc on Linux glibc/musl) so the self-locate fallback works beyond glibc.

ParserOptionActions: gate the --info Base Path so the managed CLI keeps AppContext.BaseDirectory (unchanged output) while the AOT bubble reports the resolved SdkPaths.SdkDirectory.

run-dn.ps1: derive the native library name (.dll/.so/.dylib) by OS so the separated and self-locate harness layouts work off Windows.
Comment on lines +80 to +83
if (!string.IsNullOrEmpty(sdkDirectory))
{
Environment.SetEnvironmentVariable(SdkPaths.EnvironmentVariableName, sdkDirectory);
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

We talked about this a bit, but I'm not sure I like emitting an env var for this. Would like to talk to @dsplaisted about it more. If we could smuggle it via appcontext like we do hostfxr, etc that would be less of a blast radius.

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.

I'm fine with that. @dsplaisted?

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.

Switched to a process-local AppContext value named Microsoft.DotNet.Sdk.Root (smuggled the same way as HOSTFXR_PATH) in 661863e, so it's no longer emitted as an environment variable and isn't inherited by child processes - smaller blast radius as you suggested. Because it's an AppContext/config-style key, a caller can also provide it via a runtimeconfig configProperties entry; the bridge honors that but errors out if it's set and doesn't point to an existing directory. Still happy to walk through it with @dsplaisted.

Comment thread src/Cli/dn/run-dn.ps1
// AppContext.BaseDirectory for the NAOT app is the install root, not the per-SDK version path,
// so fall back to SdkPaths.SdkDirectory (DOTNET_SDK_ROOT when set) instead. See
// src/Cli/dotnet-aot/SdkRootResolution.md.
_dotnetToolPath = dotnetToolPath ?? Path.Combine(SdkPaths.SdkDirectory, "DotnetTools");

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I was already doing sdk resolution when creating this resolver (see TryResolveCommandSpec usage in the native entry point) so I don't expect this would ever really be hit on the AOT pathway.

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.

Confirmed - you're right. On the AOT pathway DefaultCommandResolverPolicy sees a non-null sdkRoot and creates the resolver via DotnetToolsCommandResolver.ForSdkRoot(sdkRoot) with an explicit DotnetTools path, so the SdkPaths.SdkDirectory fallback in the constructor is never hit there - it's the managed-CLI default (where SdkPaths.SdkDirectory resolves to the SDK assembly directory / AppContext.BaseDirectory). I clarified the constructor comment to say that in c9339b4 (the old comment misattributed it to the NAOT case).

@baronfel baronfel left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Left some feedback, we should talk about

  • the usage of the DOTNET_SDK_ROOT env var w/ Daniel to preclear it
  • env var vs appcontext
  • removing some of the work I did for tool resolution in favor of using the sdk dir resolution you've got in this PR

Comment thread src/Cli/dotnet-aot/SdkRootLocator.cs
Comment thread test/Microsoft.DotNet.Cli.Utils.Tests/BufferScopeTests.cs
Addresses PR review feedback: write to buffer[9] (top of the grown buffer) before the second grow and re-check it afterward, so the copy is verified at both the lower and upper bounds of the used range.
Addresses PR review feedback: smuggle the value via AppContext (like hostfxr) instead of an environment variable, which leaks to child processes. The Native AOT bridge now publishes the resolved versioned SDK directory as the process-local Microsoft.DotNet.Sdk.Root AppContext value, and SdkPaths reads it instead of the DOTNET_SDK_ROOT environment variable.

A caller-provided value (e.g. a runtimeconfig configProperties entry) is honored, but the bridge errors out if it is set and does not point to an existing directory. Adds the SdkRootDirectoryDoesNotExist string (+ xlf), updates the docs, and adds tests for the error-out and honor-preset behaviors.
Addresses PR review feedback: on the AOT pathway the resolver is created via ForSdkRoot(sdkRoot) with an explicit DotnetTools path, so the SdkPaths.SdkDirectory fallback in the constructor is the managed-CLI default and is never hit on AOT. The old comment misattributed it to the NAOT case.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants