Resolve the versioned SDK directory in the Native AOT CLI#55110
Resolve the versioned SDK directory in the Native AOT CLI#55110JeremyKuhne wants to merge 5 commits into
Conversation
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.
There was a problem hiding this comment.
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 supportingBufferScope/TypeInfo) to provide a single canonical SDK-directory resolver shared by managed and AOT paths. - Implement
SdkRootLocatorto resolvesdk_dir(host-provided preferred; otherwise self-locate the loadeddotnet-aotmodule) and publish it viaDOTNET_SDK_ROOT. - Update
--info Base Pathand SDK-relative command resolution to useSdkPaths, plus add unit/integration tests and adnharness 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. |
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.
| if (!string.IsNullOrEmpty(sdkDirectory)) | ||
| { | ||
| Environment.SetEnvironmentVariable(SdkPaths.EnvironmentVariableName, sdkDirectory); | ||
| } |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
| // 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"); |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
left a comment
There was a problem hiding this comment.
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
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.
Summary
In a deployed SDK the muxer loads
dotnet-aot.dllfrom 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, andProcess.MainModule.FileNameresolve to the muxer / install root.Assembly.Locationreturns""(and ILC errors withIL3000).So SDK-relative resolution (MSBuild,
DotnetTools/, forwarders) that keys offAppContext.BaseDirectorybreaks in the AOT CLI. This gives the AOT bubble a single, correct notion of the versioned SDK directory.Changes
Microsoft.DotNet.Cli.Utils.SdkPaths–SdkDirectoryresolves theMicrosoft.DotNet.Sdk.RootAppContext value → the SDK assembly directory →AppContext.BaseDirectory(resolved once and cached).SdkRootLocator(dotnet-aot) – prefers the host-providedsdk_dir; otherwise self-locates thedotnet-aotmodule (GetModuleHandleEx+GetModuleFileNamevia CsWin32 on Windows,dladdron Unix).NativeEntryPoint.ExecuteCorepublishes the resolved directory as the process-localMicrosoft.DotNet.Sdk.RootAppContext 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. aruntimeconfig.jsonconfigPropertiesentry) 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'sBase PathreadSdkPaths.BufferScope/TypeInfohelpers (ported from dotnet/msbuild) back the self-locate long-path grow loop.CliStrings(+ xlf).dnharness gains a-Layout Separated/-SelfLocatemode (run-dn.ps1) that mirrors the deployed muxer, with integration + unit tests.src/Cli/dotnet-aot/SdkRootResolution.md, plus theadd-dotnet-aot-commandanddotnet-aot-compatagent skills.Testing
Microsoft.DotNet.Cli.Utilsbuilds clean (net-current + net472);dotnet.csprojand the manageddotnet-aotbuild clean.dotnet-aot.Tests(26) andBufferScopeTests(34) pass.--infoBase Pathresolves to the versioned SDK subfolder in both the passed-sdk_dirand self-locate cases.