From 9e4e4cce31f144d339b71af62a0bce84d14f4ca1 Mon Sep 17 00:00:00 2001 From: Jeremy Kuhne Date: Wed, 1 Jul 2026 12:48:17 -0700 Subject: [PATCH 1/5] Resolve the versioned SDK directory in the Native AOT CLI In a deployed SDK the muxer loads dotnet-aot.dll from the versioned SDK directory (sdk/), 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. --- .../skills/add-dotnet-aot-command/SKILL.md | 165 +++++++ .github/skills/dotnet-aot-compat/SKILL.md | 269 ++++++++++++ .../dotnet-aot-compat/references/polyfills.md | 43 ++ .../Microsoft.DotNet.Cli.Utils/BufferScope.cs | 197 +++++++++ .../NativeMethods.txt | 2 + .../Microsoft.DotNet.Cli.Utils/SdkPaths.cs | 73 ++++ .../Microsoft.DotNet.Cli.Utils/TypeInfo.cs | 72 +++ src/Cli/dn/Program.cs | 45 +- src/Cli/dn/run-dn.ps1 | 200 +++++++++ src/Cli/dotnet-aot/NativeEntryPoint.cs | 33 +- src/Cli/dotnet-aot/SdkRootLocator.cs | 168 +++++++ src/Cli/dotnet-aot/SdkRootResolution.md | 74 ++++ src/Cli/dotnet-aot/dotnet-aot.csproj | 1 + src/Cli/dotnet/CliStrings.resx | 7 + .../AppBaseCommandResolver.cs | 2 +- .../AppBaseDllCommandResolver.cs | 2 +- .../DotnetToolsCommandResolver.cs | 6 +- src/Cli/dotnet/ParserOptionActions.cs | 2 +- src/Cli/dotnet/xlf/CliStrings.cs.xlf | 10 + src/Cli/dotnet/xlf/CliStrings.de.xlf | 10 + src/Cli/dotnet/xlf/CliStrings.es.xlf | 10 + src/Cli/dotnet/xlf/CliStrings.fr.xlf | 10 + src/Cli/dotnet/xlf/CliStrings.it.xlf | 10 + src/Cli/dotnet/xlf/CliStrings.ja.xlf | 10 + src/Cli/dotnet/xlf/CliStrings.ko.xlf | 10 + src/Cli/dotnet/xlf/CliStrings.pl.xlf | 10 + src/Cli/dotnet/xlf/CliStrings.pt-BR.xlf | 10 + src/Cli/dotnet/xlf/CliStrings.ru.xlf | 10 + src/Cli/dotnet/xlf/CliStrings.tr.xlf | 10 + src/Cli/dotnet/xlf/CliStrings.zh-Hans.xlf | 10 + src/Cli/dotnet/xlf/CliStrings.zh-Hant.xlf | 10 + .../BufferScopeTests.cs | 412 ++++++++++++++++++ .../Microsoft.DotNet.Cli.Utils.Tests.csproj | 1 + test/dotnet-aot.Tests/AotIntegrationTests.cs | 77 +++- .../dotnet-aot.Tests/NativeEntryPointTests.cs | 46 ++ test/dotnet-aot.Tests/dotnet-aot.Tests.csproj | 1 + 36 files changed, 2016 insertions(+), 12 deletions(-) create mode 100644 .github/skills/add-dotnet-aot-command/SKILL.md create mode 100644 .github/skills/dotnet-aot-compat/SKILL.md create mode 100644 .github/skills/dotnet-aot-compat/references/polyfills.md create mode 100644 src/Cli/Microsoft.DotNet.Cli.Utils/BufferScope.cs create mode 100644 src/Cli/Microsoft.DotNet.Cli.Utils/SdkPaths.cs create mode 100644 src/Cli/Microsoft.DotNet.Cli.Utils/TypeInfo.cs create mode 100644 src/Cli/dn/run-dn.ps1 create mode 100644 src/Cli/dotnet-aot/SdkRootLocator.cs create mode 100644 src/Cli/dotnet-aot/SdkRootResolution.md create mode 100644 test/Microsoft.DotNet.Cli.Utils.Tests/BufferScopeTests.cs diff --git a/.github/skills/add-dotnet-aot-command/SKILL.md b/.github/skills/add-dotnet-aot-command/SKILL.md new file mode 100644 index 000000000000..e8b74933744f --- /dev/null +++ b/.github/skills/add-dotnet-aot-command/SKILL.md @@ -0,0 +1,165 @@ +--- +name: add-dotnet-aot-command +description: > + Include a dotnet CLI command or feature in the Native AOT CLI (src/Cli/dotnet-aot) + and prove it works. USE FOR: enabling a command/option in dotnet-aot, adding source + files to AotSourceFiles.props, gating AOT-incompatible code with #if CLI_AOT, building + and NativeAOT-publishing dotnet-aot, writing/updating the AOT parser + integration + tests, running the local dn harness in AOT mode and comparing it to the managed CLI. + DO NOT USE FOR: resolving IL trim/AOT analyzer warnings (use dotnet-aot-compat), + running dotnet.Tests incrementally (use incremental-test), or pure managed CLI work. +license: MIT +--- + +# add-dotnet-aot-command + +How to include a `dotnet` command or feature in the Native AOT CLI (`src/Cli/dotnet-aot`), keep the +AOT surface small, validate it, and run it through the local `dn` harness. + +> Paths use `$(SdkTargetFramework)` = `net11.0` and `win-x64`; adjust for other TFMs/RIDs. + +## When to use + +- Make `dotnet ` work in dotnet-aot, or enable an option/section in the AOT path. +- Add source files to `AotSourceFiles.props`. +- Run `dn` in AOT mode, or compare AOT vs managed output. +- Diagnose why `dotnet test` fails for `dotnet-aot.Tests`. + +**Not for:** IL trim/AOT warnings (use **dotnet-aot-compat**); managed `dotnet.Tests` runs (use +**incremental-test**); managed-only changes with no AOT impact. + +## How the AOT CLI is assembled + +`dotnet-aot` does **not** reference `dotnet.csproj`. It is a shared native library (`NativeLib=Shared`, +`PublishAot=true`, `IsAotCompatible=true`) that **cherry-picks source files** from `src/Cli/dotnet/` via +`src/Cli/dotnet-aot/AotSourceFiles.props`. That `.props` is imported by **both** `dotnet-aot.csproj` and +`test/dotnet-aot.Tests/dotnet-aot.Tests.csproj`, so the tests compile the exact same command surface as +the shipping binary. + +Compile constants (both projects): `CLI_AOT` gates AOT-only vs managed-only code in shared files (`#if +CLI_AOT` / `#if !CLI_AOT`); `DotnetCsproj` is also defined and can pull in extra closure (see Gotchas). + +Dispatch: `dotnet-aot/NativeEntryPoint.cs` (`dotnet_execute`) is P/Invoked by the `dn` host (`src/Cli/dn`): + +- `DOTNET_CLI_ENABLEAOT=true`: parse in-process, run `FirstRunExperience.Setup`, and if + `parseResult.CanBeInvoked()` run the command **in-process**. A command still needing the managed CLI + throws `CommandNotAvailableInAotException` to fall through. +- Otherwise / on fall-through: host `{sdkDir}/dotnet.dll` via hostfxr (same source, JIT-compiled). + +Types already available (do **not** re-add their sources): `Microsoft.DotNet.Cli.Utils`, +`Microsoft.DotNet.Configurer`, `Microsoft.DotNet.Cli.Definitions`, `Microsoft.DotNet.ProjectTools`, +`Microsoft.DotNet.NativeWrapper`, `Microsoft.NET.Sdk.WorkloadManifestReader`. `Cli.Utils` grants +`InternalsVisibleTo` to `dotnet-aot` and `dotnet-aot.Tests`, so its `internal` types (including the +CsWin32 `Windows.Win32.*` COM types and helpers `ComScope`, `BSTR`, `HRESULT`, `CLSID`) are usable +without re-wiring CsWin32. + +## Resolving the versioned SDK root (do NOT use BCL path APIs) + +The muxer loads `dotnet-aot.dll` directly from the versioned SDK directory (e.g. `.../sdk/11.0.100/`), +but inside that process the BCL "where am I" APIs do **not** point there: + +- `AppContext.BaseDirectory`, `Environment.ProcessPath`, `Process.GetCurrentProcess().MainModule` -> the + **muxer / install root**. +- `Assembly.Location` -> the **empty string** (ILC hard-errors with `IL3000`). + +So deriving an SDK-relative path (`MSBuild.dll`, `Sdks/`, `DotnetTools/`, targets) from +`AppContext.BaseDirectory` or a dll path is **wrong** in the AOT bubble. Instead: + +- **In-repo:** read `SdkPaths.SdkDirectory` (in `Microsoft.DotNet.Cli.Utils`), which resolves + `DOTNET_SDK_ROOT` env var -> SDK assembly directory -> `AppContext.BaseDirectory` (once, cached). +- `NativeEntryPoint.ExecuteCore` resolves the SDK directory once (host `sdk_dir`, else self-locating the + `dotnet-aot` module via `SdkRootLocator`) and **publishes it in `DOTNET_SDK_ROOT`** for the compiled-in + assemblies. +- **Out-of-repo code** (MSBuild tasks, NuGet, runtime - no `Cli.Utils` reference) replicates the contract + inline: read `DOTNET_SDK_ROOT` first, else the existing BCL logic. + + ```csharp + string sdkDirectory = + Environment.GetEnvironmentVariable("DOTNET_SDK_ROOT") is { Length: > 0 } sdkRoot + ? sdkRoot + : /* existing logic, e.g. AppContext.BaseDirectory */; + ``` + +When bringing a command into AOT, switch any `AppContext.BaseDirectory` / `Assembly.Location` used as +"the SDK directory" to the above. Not-yet-routed sites: `FormatForwardingApp`, `FsiForwardingApp`, +`VSTestForwardingApp`, `ProjectFactory` / `ProjectToolsCommandResolver`, `VBCSCompilerServer`, +`CSharpCompilerCommand`, `MSBuildForwardingAppWithoutLogging`, `DotnetFiles.SdkRootFolder`. Details: +`src/Cli/dotnet-aot/SdkRootResolution.md`. + +## Procedure + +1. **Find the call site** in shared source (a command parser, `Parser.cs`, `ParserOptionActions.cs`) and + remove/narrow its `#if !CLI_AOT` guard. +2. **Add the source closure** to `AotSourceFiles.props` in a labeled per-command `` (follow the + file's header rules; reuse the "Common AOT scaffolding" group). Only add files under `src/Cli/dotnet/` + that aren't already in a referenced assembly or the `.props`. Windows/COM files go in a + `Condition="'$(TargetOS)' == 'windows'"` group. +3. **Add package references** the command needs (in `AotSourceFiles.props` if both binary and tests need + them; confirm the runtime asset flows - see the `Microsoft.Build` gotcha). +4. **Build managed dotnet-aot first** - fast, and surfaces `CS0246`/`CS0103` closure gaps without ILC. Let + the compiler drive the closure: `.\.dotnet\dotnet build src\Cli\dotnet-aot\dotnet-aot.csproj -c Debug` +5. **Publish as NativeAOT** to surface IL warnings (resolve per **dotnet-aot-compat**): + `.\.dotnet\dotnet publish src\Cli\dotnet-aot\dotnet-aot.csproj -r win-x64 -c Debug`. ILC only analyzes + the reachable closure - don't preemptively suppress warnings that never appear. +6. **Keep the AOT surface small.** Gate heavy subsystems (workload installer, NuGet engine, MSI/COM IPC) + under `#if CLI_AOT` and build only the read-only path you need (mirror `WorkloadInstallDetector`, which + builds the record repository directly with no installer). Gate installer-coupled interfaces under + `#if !CLI_AOT`, with an AOT-only construction path under `#if CLI_AOT`. +7. **Confirm the managed CLI still builds** (the `#else` branches must stay intact): + `.\.dotnet\dotnet build src\Cli\dotnet\dotnet.csproj -c Debug` + +## Gotchas + +- **MSBuild XML comments can't contain `--`** (`MSB4024`). Reword; never end a comment with `-`. +- **The `Microsoft.Build` runtime asset doesn't flow transitively** - `Cli.Utils` references it + `ExcludeAssets="runtime" PrivateAssets="all"`, so dotnet-aot has no `Microsoft.Build.dll` at ILC time. + If you reach a `Microsoft.Build.*` API, add `` to the AOT + closure. +- **`DotnetCsproj` is defined for dotnet-aot**, so adding a shared file can pull in extra `#if DotnetCsproj` + closure. Inline the small helper you need under `#if CLI_AOT` instead. +- **Don't pass `-noRestore` with `-getItem`** - the response file already appends it (`MSB1001`). +- **`dotnet test` does NOT work for `dotnet-aot.Tests`** (Microsoft.Testing.Platform, not VSTest). Run the + built `.exe` directly (see below). +- **Existing tests may assert AOT _exclusions_** - enabling a feature can mean **inverting** a + `DoesNotContain` assertion. Search the test project first. +- **PowerShell git/gh quoting:** single-quote messages/titles containing backticks or `$(...)`. + +## Validate & test + +Tests live in `test/dotnet-aot.Tests`: `AotParserTests` (in-process parser/command behavior) and +`AotIntegrationTests` (end-to-end against the real `dn`; skips if `dn` isn't in the layout). + +Run the suite as a native AOT binary (the real ILC / COM / P-Invoke check) with +`test/dotnet-aot.Tests/run-aot-tests.ps1`. To iterate on one test, build the test project and run +`dotnet-aot.Tests.exe --filter "FullyQualifiedName~"` (MTP runs as an executable; `dotnet test` +doesn't work). `IL3053` rollups for test-only assemblies (FluentAssertions, TestPlatform.ObjectModel, +DataContractSerialization) are not product warnings. + +Assert real **values**, not just headers, so a trim regression that blanks a line is caught - e.g. +`stdout.Should().MatchRegex(@"MSBuild version:\s+\S");`. + +## Run the local `dn` harness in AOT mode + +Use `src/Cli/dn/run-dn.ps1` (don't inline the steps). It publishes `dotnet-aot` (NativeAOT) and `dn`, +builds the managed `dotnet` CLI, assembles them into the `dn` publish dir, points `DOTNET_ROOT` at the +repo's `.dotnet`, and runs `dn ` with `DOTNET_CLI_ENABLEAOT` toggled. **Tell the user these +steps** so they can reproduce it. + +```powershell +src\Cli\dn\run-dn.ps1 -Command "--info" # through the AOT binary +src\Cli\dn\run-dn.ps1 -Command "--info" -Mode Compare # AOT vs managed diff (parity) +src\Cli\dn\run-dn.ps1 -Command "workload --info" -NoBuild # reuse the assembled layout +``` + +- `DOTNET_CLI_ENABLEAOT=true` runs in-process in `dotnet-aot.dll`; unset, `dn` hosts the copied + `dotnet.dll`. `-Mode Compare` diffs the captured output (`artifacts/log/dn-aot.txt`, `dn-managed.txt`). +- `dn` finds the .NET root from `DOTNET_ROOT` (set to `.dotnet`); the publish dir isn't a full SDK. +- The AOT path runs `FirstRunExperience.Setup` first; if it can't complete, it defers to the managed CLI. +- `Commit` and workloads reflect the `DOTNET_ROOT` layout - both paths read the same root, so they agree. + +The VS Code tasks `publish-and-copy-dn-aot` + `copy-all-deps` do the same build/assemble. + +## Related skills + +- **dotnet-aot-compat** - resolve the IL trim/AOT warnings this surfaces. +- **incremental-test** - run the managed `dotnet.Tests` against the redist SDK layout. diff --git a/.github/skills/dotnet-aot-compat/SKILL.md b/.github/skills/dotnet-aot-compat/SKILL.md new file mode 100644 index 000000000000..bcfeca1068e3 --- /dev/null +++ b/.github/skills/dotnet-aot-compat/SKILL.md @@ -0,0 +1,269 @@ +--- +name: dotnet-aot-compat +description: > + Make .NET projects compatible with Native AOT and trimming by systematically + resolving IL trim/AOT analyzer warnings. USE FOR: making projects AOT-compatible, + fixing trimming warnings, resolving IL warnings (IL2026, IL2070, IL2067, IL2072, + IL3050), adding DynamicallyAccessedMembers annotations, enabling IsAotCompatible. + DO NOT USE FOR: publishing native AOT binaries, optimizing binary size, replacing + reflection-heavy libraries with alternatives. + INVOKES: no tools — pure knowledge skill. +license: MIT +--- + +# dotnet-aot-compat + +Make .NET projects compatible with Native AOT and trimming by systematically resolving all IL trim/AOT analyzer warnings. + +## When to Use This Skill + +- **"Make this project AOT-compatible"** +- **"Fix trimming warnings"** or **"fix IL warnings"** +- **"Resolve IL2070 / IL2067 / IL2072 / IL2026 / IL3050 warnings"** +- **"Add DynamicallyAccessedMembers annotations"** +- **"Enable IsAotCompatible in my .csproj"** +- **"My project has trim analyzer warnings after upgrading to net8.0"** +- **"Annotate reflection code for the trimmer"** + +## When Not to Use This Skill + +Do not use this skill when the project exclusively targets .NET Framework (net4x), which does not support the trim/AOT analyzers. + +## Prerequisites + +An existing .NET project targeting net8.0 or later (or multi-targeting with at least one net8.0+ TFM) and the corresponding .NET SDK installed. + +## Background: What AOT Compatibility Means + +Native AOT and the IL trimmer perform static analysis to determine what code is reachable. Reflection can break this analysis because the trimmer can't see what types/members are accessed at runtime. The `IsAotCompatible` property enables analyzers that flag these issues as build warnings (ILXXXX codes). + +## Critical Rules + +### ❌ Never suppress warnings incorrectly + +- **NEVER** use `#pragma warning disable` for IL warnings. It hides warnings from the Roslyn analyzer at build time, but the IL linker and AOT compiler still see the issue. The code will fail at trim/publish time. +- **NEVER** use `[UnconditionalSuppressMessage]`. It tells both the analyzer AND the linker to ignore the warning, meaning the trimmer cannot verify safety. Raising an error at build time is always preferable to hiding the issue and having it silently break at runtime. + +### 💡 Preferred approaches + +- **Prefer** `[DynamicallyAccessedMembers]` annotations to flow type information through the call chain. +- **Prefer** refactoring to eliminate patterns that break annotation flow (e.g., boxing `Type` through `object[]`). +- **Use** `[RequiresUnreferencedCode]` / `[RequiresDynamicCode]` / `[RequiresAssemblyFiles]` to mark methods as fundamentally incompatible with trimming, propagating the requirement to callers. This surfaces the issue clearly rather than hiding it — callers must explicitly acknowledge the incompatibility. + +### Annotation flow is key + +The trimmer tracks `[DynamicallyAccessedMembers]` annotations through assignments, parameter passing, and return values. If this flow is broken (e.g., by boxing a `Type` into `object`, storing in an untyped collection, or casting through interfaces), the trimmer loses track and warns. The fix is to preserve the flow, not suppress the warning. + +## Step-by-Step Procedure + +> **Do not explore the codebase up-front.** The build warnings tell you exactly which files and lines need changes. Follow a tight loop: **build → pick a warning → open that file at that line → apply the fix recipe → rebuild**. Reading or analyzing source files beyond what a specific warning points you to is wasted effort and leads to timeouts. Let the compiler guide you. +> +> ❌ Do NOT run `find`, `ls`, or `grep` to understand the project structure before building. Do NOT read README, docs, or architecture files. Your first action should be Step 1 (enable AOT analysis), then build. + +### Step 1: Enable AOT analysis in the .csproj + +Add `IsAotCompatible`. If the project doesn't exclusively target net8.0+, add a TFM condition (AOT analysis requires net8.0+): + +```xml + + true + +``` + +This automatically sets `EnableTrimAnalyzer=true` and `EnableAotAnalyzer=true` for compatible TFMs. For multi-targeting projects (e.g., `netstandard2.0;net8.0`), the condition ensures no `NETSDK1210` warnings on older TFMs. + +### Step 2: Build and collect warnings + +```bash +dotnet build -f --no-incremental 2>&1 | grep 'IL[0-9]\{4\}' +``` + +Sort and deduplicate. Common warning codes: +- **IL2070**: Reflection call on a `Type` parameter missing `[DynamicallyAccessedMembers]` +- **IL2067**: Passing an unannotated `Type` to a method expecting `[DynamicallyAccessedMembers]` +- **IL2072**: Return value or extracted value missing annotation (often from unboxing) +- **IL2057**: `Type.GetType(string)` with a non-constant argument +- **IL2026**: Calling a method marked `[RequiresUnreferencedCode]` +- **IL2050**: P/invoke method with COM marshalling parameters +- **IL2075**: Return value flows into reflection without annotation +- **IL2091**: Generic argument missing `[DynamicallyAccessedMembers]` required by constraint +- **IL3000**: `Assembly.Location` returns empty string in single-file/AOT apps +- **IL3050**: Calling a method marked `[RequiresDynamicCode]` + +### Step 3: Triage warnings by code (do NOT read every file) + +Group the warnings from Step 2 by warning code and count them. **Do not open individual files yet.** Identify the top 1-2 patterns by count — these drive your fix strategy: + +| Pattern | Typical fix | +|---------|-------------| +| Many IL2026 + IL3050 from `JsonSerializer` | **Go to Strategy C immediately** — create a `JsonSerializerContext`, then batch-update all call sites | +| IL2070/IL2087 on `Type` parameters | Add `[DynamicallyAccessedMembers]` to the innermost method, then cascade outward | +| IL2067 passing unannotated `Type` | Annotate the parameter at the source | + +**In most real projects, IL2026/IL3050 from JsonSerializer dominate.** Start with Strategy C unless the warning breakdown clearly shows otherwise. After the batch JSON fix, handle remaining warnings with Strategies A–B. Only use Strategy D as a last resort. + +### Step 4: Fix warnings iteratively (innermost first) + +Work from the **innermost** reflection call outward. Each fix may cascade new warnings to callers. + +**Stay warning-driven.** For each warning, open only the file and line the compiler reported, identify the pattern, apply the matching fix recipe below, and move on. Do not scan the codebase for similar patterns or try to understand the full architecture — fix what the compiler tells you, rebuild, and let new warnings guide the next change. Fix a small batch of warnings (5-10), then rebuild immediately to check progress. + +**Use sub-agents when available.** If you can launch sub-agents (e.g., via a `task` tool), dispatch **multiple sub-agents in parallel** to edit different files simultaneously. Keep the main loop focused on building, parsing warnings, and dispatching — delegate actual file edits to sub-agents. For batch JSON updates, give each sub-agent 5-10 files to update in one prompt. **After 2 build-fix cycles, dispatch all remaining file edits to sub-agents in parallel — do not continue fixing files sequentially.** Example: + +> Update these files to use source-generated JSON: `src/Models/Resource.Serialization.cs`, `src/Models/Identity.Serialization.cs`, `src/Models/Plan.Serialization.cs`. In each file, replace `JsonSerializer.Serialize(writer, value)` with `JsonSerializer.Serialize(writer, value, MyProjectJsonContext.Default.TypeName)` and `JsonSerializer.Deserialize(ref reader)` with `JsonSerializer.Deserialize(ref reader, MyProjectJsonContext.Default.TypeName)`. Only edit the JsonSerializer call sites. + +#### Strategy A: Add `[DynamicallyAccessedMembers]` (preferred) + +When a method uses reflection on a `Type` parameter, annotate the parameter to tell the trimmer what members are needed: + +```csharp +using System.Diagnostics.CodeAnalysis; + +// Before (warns IL2070): +void Process(Type t) { + var method = t.GetMethod("Foo"); // trimmer can't verify +} + +// After (clean): +void Process([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods)] Type t) { + var method = t.GetMethod("Foo"); // trimmer preserves public methods +} +``` + +When you annotate a parameter, **all callers** must now pass properly annotated types. This cascades outward — follow each caller and annotate or refactor as needed. **The caller's annotation must include at least the same member types as the callee's.** If the callee requires `PublicConstructors | NonPublicConstructors`, the caller must specify the same or a superset — using only `NonPublicConstructors` will produce IL2091. + +#### Strategy B: Refactor to preserve annotation flow + +When annotation flow is broken by boxing (storing `Type` in `object`, `object[]`, or untyped collections), **refactor** to pass the `Type` directly: + +```csharp +// BROKEN: Type boxed into object[], annotation lost +void Process(object[] args) { + Type t = (Type)args[0]; // IL2072: annotation lost through boxing + Evaluate(t, ...); +} + +// FIXED: Pass Type as a separate, annotated parameter +void Process( + object[] args, + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods)] Type calleeType, + ...) { + Evaluate(calleeType, ...); // annotation flows cleanly +} +``` + +Common patterns that break flow and how to fix them: +- **`object[]` parameter bags**: Extract the `Type` into a dedicated annotated parameter +- **Dictionary/List storage**: Use a typed field with annotation instead +- **Interface indirection**: Add annotation to the interface method's parameter +- **Property with boxing getter**: Annotate the property's return type + +#### Strategy C: Source-generated JSON serialization (batch fix) + +When most warnings are IL2026/IL3050 from `JsonSerializer.Serialize`/`Deserialize`, this is a single mechanical fix applied in bulk: + +1. **Collect affected types** — grep for all `JsonSerializer.Serialize` and `JsonSerializer.Deserialize` call sites. Extract the type being serialized (the `` in `Deserialize`, or the runtime type of the object in `Serialize`). + +2. **Create one `JsonSerializerContext`** with `[JsonSerializable]` for every type found. **Skip types from external packages** (e.g., `ResponseError` from `Azure.Core`) — they won't source-generate for types you don't own. Handle external types separately via Gotcha #1 below. + +```csharp +[JsonSerializerContext] +[JsonSerializable(typeof(ManagedServiceIdentity))] +[JsonSerializable(typeof(SystemData))] +// ... one attribute per type YOU OWN +// Do NOT add types from external packages (e.g., ResponseError) +internal partial class MyProjectJsonContext : JsonSerializerContext { } +``` + +3. **Batch-update all call sites** — do not read each file individually. Apply the pattern mechanically: + - `JsonSerializer.Serialize(obj)` → `JsonSerializer.Serialize(obj, MyProjectJsonContext.Default.TypeName)` + - `JsonSerializer.Deserialize(json)` → `JsonSerializer.Deserialize(json, MyProjectJsonContext.Default.TypeName)` + + Find and update all call sites in one pass: + ```bash + # Find all files with JsonSerializer calls + grep -rl 'JsonSerializer\.\(Serialize\|Deserialize\)' src/ --include='*.cs' + ``` + Then use sequential `edit` calls to apply the same transformation to every matching file. **Do not use `sed` for C# code** — generics like `Deserialize()` have angle brackets and nested parentheses that sed will mangle. + +4. **Build once** to verify. Remaining warnings will be non-serialization issues — handle those with Strategies A–B or D. + +#### Strategy D: `[RequiresUnreferencedCode]` (last resort) + +When a method fundamentally requires arbitrary reflection that cannot be statically described: + +```csharp +[RequiresUnreferencedCode("Loads plugins by name using Assembly.Load")] +public void LoadPlugin(string assemblyName) { + var asm = Assembly.Load(assemblyName); + // ... +} +``` + +This propagates to callers — they must also be annotated with `[RequiresUnreferencedCode]`. Use sparingly; it marks the entire call chain as trim-incompatible. + +### Step 5: Rebuild and repeat + +After each small batch of fixes (5-10 warnings), rebuild with `--no-incremental` and check for new warnings. **Do not attempt to fix all warnings before rebuilding** — frequent rebuilds catch mistakes early and reveal cascading warnings. Fixes cascade — annotating an inner method may surface warnings in its callers. Repeat until `0 Warning(s)`. + +### Step 6: Validate all TFMs + +Build all target frameworks to ensure: +- **0 IL warnings** on net8.0+ TFMs +- **No NETSDK1210 warnings** (the `IsAotCompatible` condition handles this) +- **Clean builds** on older TFMs (netstandard2.0, net472, etc.) + +```bash +dotnet build # builds all TFMs +``` + +## Stop Signals + +- **Do not analyze more than 2-3 representative files per warning pattern.** After identifying the fix for a pattern, apply it to all matching files without reading each one first. +- **Start fixing after one build.** Do not do a second analysis pass — begin implementing fixes for the most common warning pattern immediately after Step 3 triage. +- Stop after achieving **0 IL warnings** for net8.0+ TFMs. Don't optimize or refactor already-clean annotations. +- If a warning requires **architectural refactoring** beyond annotation flow fixes (e.g., replacing an entire serialization layer), document it and stop — don't rewrite large subsystems. +- Limit to **3 build-fix iterations** per warning. If annotation flow doesn't resolve it after 3 attempts, escalate to `[RequiresUnreferencedCode]`. +- Don't chase warnings in **third-party dependencies** you can't modify. Note them and move on. +- If the user asked a scoped question (e.g., "fix warnings in this file"), don't expand to the entire project. + +## Polyfills for Older TFMs + +For multi-targeting projects that include netstandard2.0 or net472, you need polyfills for `DynamicallyAccessedMembersAttribute` and related types. See [references/polyfills.md](references/polyfills.md). + +## Common Gotchas + +1. **External types without AOT-safe serialization**: When a type comes from a dependency you can't modify (e.g., `ResponseError` from `Azure.Core`) and it lacks a source-generated serializer, `Options.GetConverter()` is reflection-based and will produce IL warnings. First check if the type implements `IJsonModel` (common in Azure SDK) — if so, bypass `JsonSerializer` entirely: + +```csharp +// Before (IL2026 — JsonSerializer uses reflection): +JsonSerializer.Serialize(writer, errorValue); + +// After (AOT-safe — uses IJsonModel directly): +((IJsonModel)errorValue).Write(writer, ModelReaderWriterOptions.Json); + +// For deserialization: +var error = ((IJsonModel)new ResponseError()).Create(ref reader, ModelReaderWriterOptions.Json); +``` + +Do **not** add the external type to your `JsonSerializerContext` — it won't source-generate for types you don't own. If the type doesn't implement `IJsonModel`, write a custom `JsonConverter` with manual `Utf8JsonReader`/`Utf8JsonWriter` logic and register it via `[JsonSourceGenerationOptions]` on your context. + +2. **Serialization libraries**: Most reflection-based serializers (e.g., `Newtonsoft.Json`, `XmlSerializer`) are not AOT-compatible. Migrate to a source-generation-based serializer such as `System.Text.Json` with a `JsonSerializerContext`. If migration is not feasible, mark the serialization call site with `[RequiresUnreferencedCode]`. + +3. **Shared projects / projitems**: When source is shared between multiple projects via ``, annotations added to shared code affect ALL consuming projects. Verify that all consumers still build cleanly. + +## References + +[Limitations](https://learn.microsoft.com/en-us/dotnet/core/deploying/native-aot/?tabs=windows%2Cnet8#limitations-of-native-aot-deployment) +[Conceptual: Understanding trimming](https://learn.microsoft.com/en-us/dotnet/core/deploying/trimming/trimming-concepts) +[How-to: trim compat](https://learn.microsoft.com/en-us/dotnet/core/deploying/trimming/fixing-warnings) + +## Checklist + +- [ ] Added `` with TFM condition to .csproj +- [ ] Built with AOT analyzers enabled (net8.0+ TFM) +- [ ] Fixed all IL warnings via annotations or refactoring +- [ ] No `#pragma warning disable` or `[UnconditionalSuppressMessage]` used for any IL warning +- [ ] Polyfills present for older TFMs if needed +- [ ] All target frameworks build with 0 warnings +- [ ] Verified shared/linked source doesn't break sibling projects diff --git a/.github/skills/dotnet-aot-compat/references/polyfills.md b/.github/skills/dotnet-aot-compat/references/polyfills.md new file mode 100644 index 000000000000..a577f2e2c619 --- /dev/null +++ b/.github/skills/dotnet-aot-compat/references/polyfills.md @@ -0,0 +1,43 @@ +# Polyfills for Older TFMs + +`DynamicallyAccessedMembersAttribute` shipped in .NET 5. For projects targeting netstandard2.0 or net472, you need a polyfill. The trimmer recognizes the attribute by name, so a local copy works: + +```csharp +#if !NET +namespace System.Diagnostics.CodeAnalysis +{ + [AttributeUsage(AttributeTargets.Field | AttributeTargets.ReturnValue | + AttributeTargets.GenericParameter | AttributeTargets.Parameter | + AttributeTargets.Property, Inherited = false)] + internal sealed class DynamicallyAccessedMembersAttribute : Attribute + { + public DynamicallyAccessedMembersAttribute(DynamicallyAccessedMemberTypes memberTypes) + => MemberTypes = memberTypes; + public DynamicallyAccessedMemberTypes MemberTypes { get; } + } + + [Flags] + internal enum DynamicallyAccessedMemberTypes + { + None = 0, + PublicParameterlessConstructor = 0x0001, + PublicConstructors = 0x0002 | PublicParameterlessConstructor, + NonPublicConstructors = 0x0004, + PublicMethods = 0x0008, + NonPublicMethods = 0x0010, + PublicFields = 0x0020, + NonPublicFields = 0x0040, + PublicNestedTypes = 0x0080, + NonPublicNestedTypes = 0x0100, + PublicProperties = 0x0200, + NonPublicProperties = 0x0400, + PublicEvents = 0x0800, + NonPublicEvents = 0x1000, + Interfaces = 0x2000, + All = ~None // Discouraged — prefer specific flags + } +} +#endif +``` + +Similarly for `RequiresUnreferencedCodeAttribute` and `UnconditionalSuppressMessageAttribute` if needed on older TFMs. diff --git a/src/Cli/Microsoft.DotNet.Cli.Utils/BufferScope.cs b/src/Cli/Microsoft.DotNet.Cli.Utils/BufferScope.cs new file mode 100644 index 000000000000..0aae526ef2c1 --- /dev/null +++ b/src/Cli/Microsoft.DotNet.Cli.Utils/BufferScope.cs @@ -0,0 +1,197 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +// Pulled from dotnet/msbuild (src/Framework/Utilities/BufferScope.cs) so the Native AOT CLI can grow +// a stack-allocated buffer for Win32 P/Invokes (e.g. GetModuleFileName) without heap-allocating in the +// common case. Kept in sync with the upstream version. + +using System; +using System.Buffers; +using System.Diagnostics; + +namespace Microsoft.DotNet.Cli.Utils; + +/// +/// Allows renting a buffer from with a using statement. Can be used directly as if it +/// were a . +/// +internal ref struct BufferScope +{ + private T[]? _array; + private Span _span; + + /// + /// Initializes a new instance of the . + /// + /// The required minimum length. + public BufferScope(int minimumLength) + { + _array = ArrayPool.Shared.Rent(minimumLength); + _span = _array; + } + + /// + /// Create the with an initial buffer. Useful for creating with an initial stack + /// allocated buffer. + /// + /// The initial buffer to use. + public BufferScope(Span initialBuffer) + { + _array = null; + _span = initialBuffer; + } + + /// + /// Create the with an initial buffer. Useful for creating with an initial stack + /// allocated buffer. + /// + /// + /// + /// Creating with a stack allocated buffer: + /// using BufferScope<char> buffer = new(stackalloc char[64]); + /// + /// + /// + /// The initial buffer to use. If not large enough for , a buffer will be rented + /// from the shared . + /// + /// + /// The required minimum length. If the is not large enough, this will rent from + /// the shared . + /// + public BufferScope(Span initialBuffer, int minimumLength) + { + if (initialBuffer.Length >= minimumLength) + { + _array = null; + _span = initialBuffer; + } + else + { + _array = ArrayPool.Shared.Rent(minimumLength); + _span = _array; + } + } + + /// + /// Ensure that the buffer has enough space for number of elements. + /// + /// + /// + /// Consider if creating new instances is possible and cleaner than using + /// this method. + /// + /// + /// The minimum number of elements the buffer should be able to hold. + /// True to copy the existing elements when new space is allocated. + public void EnsureCapacity(int capacity, bool copy = false) + { + if (_span.Length >= capacity) + { + return; + } + + // Keep method separate for better inlining. + IncreaseCapacity(capacity, copy); + } + + private void IncreaseCapacity(int capacity, bool copy) + { + Debug.Assert(capacity > _span.Length); + + T[] newArray = ArrayPool.Shared.Rent(capacity); + if (copy) + { + _span.CopyTo(newArray); + } + + if (_array is not null) + { + ArrayPool.Shared.Return(_array, clearArray: TypeInfo.IsReferenceOrContainsReferences()); + } + + _array = newArray; + _span = _array; + } + + /// + /// Gets or sets the element at the specified index. + /// + /// The zero-based index of the element to get or set. + /// The element at the specified index. + public ref T this[int i] + => ref _span[i]; + + /// + /// Forms a slice out of the buffer starting at a specified index for a specified length. + /// + /// The index at which to begin the slice. + /// The desired length of the slice. + /// A span that consists of elements from the buffer starting at . + public readonly Span Slice(int start, int length) + => _span.Slice(start, length); + + /// + /// + /// This is used by C# to enable using the buffer in a fixed statement. + /// + public readonly ref T GetPinnableReference() + => ref _span.GetPinnableReference(); + + /// + /// Gets the number of elements in the buffer. + /// + public readonly int Length => _span.Length; + + /// + /// Returns the buffer as a . + /// + /// A span that represents the buffer. + public readonly Span AsSpan() + => _span; + + /// + /// Implicitly converts a to a . + /// + /// The buffer scope to convert. + /// A span that represents the buffer. + public static implicit operator Span(BufferScope scope) + => scope._span; + + /// + /// Implicitly converts a to a . + /// + /// The buffer scope to convert. + /// A read-only span that represents the buffer. + public static implicit operator ReadOnlySpan(BufferScope scope) + => scope._span; + + /// + /// Returns an enumerator for the buffer's backing . + /// + public readonly Span.Enumerator GetEnumerator() + => _span.GetEnumerator(); + + /// + /// Releases the rented array back to the if one was rented. + /// + public void Dispose() + { + // Clear the span to avoid accidental use after returning the array. + _span = default; + + if (_array is not null) + { + ArrayPool.Shared.Return(_array, clearArray: TypeInfo.IsReferenceOrContainsReferences()); + } + + _array = null; + } + + /// + /// Returns a string representation of the buffer. + /// + /// A string representation of the buffer. + public override readonly string ToString() + => _span.ToString(); +} diff --git a/src/Cli/Microsoft.DotNet.Cli.Utils/NativeMethods.txt b/src/Cli/Microsoft.DotNet.Cli.Utils/NativeMethods.txt index b22f70b2d57f..c465659f5283 100644 --- a/src/Cli/Microsoft.DotNet.Cli.Utils/NativeMethods.txt +++ b/src/Cli/Microsoft.DotNet.Cli.Utils/NativeMethods.txt @@ -15,6 +15,7 @@ FormatMessage FreeLibrary GetCommandLine GetModuleHandleEx +GetModuleFileName GetProcAddress LoadLibraryEx LocalFree @@ -56,6 +57,7 @@ GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT HRESULT HWND_BROADCAST JOBOBJECT_EXTENDED_LIMIT_INFORMATION +MAX_PATH MUTZ_ISFILE PROCESS_BASIC_INFORMATION REGDB_E_CLASSNOTREG diff --git a/src/Cli/Microsoft.DotNet.Cli.Utils/SdkPaths.cs b/src/Cli/Microsoft.DotNet.Cli.Utils/SdkPaths.cs new file mode 100644 index 000000000000..9877e5aaf05a --- /dev/null +++ b/src/Cli/Microsoft.DotNet.Cli.Utils/SdkPaths.cs @@ -0,0 +1,73 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +#if NET +using System.Diagnostics.CodeAnalysis; +#endif +using System.IO; + +namespace Microsoft.DotNet.Cli.Utils; + +/// +/// Canonical SDK filesystem locations for both the managed CLI and the Native AOT CLI. The first is +/// - the versioned SDK directory (the folder that contains dotnet.dll, +/// MSBuild.dll, Sdks\, DotnetTools\, and the SDK targets). Prefer these over BCL +/// "where am I" APIs so paths resolve correctly in both hosts. +/// +/// +/// +/// Under the Native AOT CLI (dotnet-aot loaded directly by the muxer) the BCL location APIs do +/// NOT point at the versioned SDK directory: and +/// resolve to the install root (the muxer's own +/// directory) and Assembly.Location is empty. The AOT entry point therefore publishes the +/// resolved SDK directory in the DOTNET_SDK_ROOT environment variable, which this helper reads +/// first. Code that needs an SDK-relative path (MSBuild, tasks, tools, forwarders) should use this +/// helper rather than probing a dll path. +/// +/// +/// Resolution order (resolved once and cached): the DOTNET_SDK_ROOT environment variable, then +/// the directory of the SDK assembly (empty under single-file / NativeAOT), then +/// . See src/Cli/dotnet-aot/SdkRootResolution.md. +/// +/// +internal static class SdkPaths +{ + /// + /// The environment variable the Native AOT bridge uses to publish the resolved versioned SDK + /// directory for the assemblies compiled into it. + /// + public const string EnvironmentVariableName = "DOTNET_SDK_ROOT"; + + private static string? s_sdkDirectory; + + /// + /// The versioned SDK directory, resolved once and cached. Prefers the DOTNET_SDK_ROOT + /// environment variable (published by the AOT bridge); otherwise the directory of the SDK assembly, + /// else . + /// + public static string SdkDirectory => s_sdkDirectory ??= ResolveSdkDirectory(); + +#if NET + [UnconditionalSuppressMessage("AOT", "IL3000", + Justification = "Assembly.Location is empty under single-file / NativeAOT; the empty result is " + + "handled by falling back to AppContext.BaseDirectory, and the AOT bridge sets DOTNET_SDK_ROOT " + + "(preferred above), so this path is only reached by the JIT-compiled managed CLI where " + + "Assembly.Location is the versioned SDK directory.")] +#endif + internal static string ResolveSdkDirectory() + { + // The AOT bridge publishes the resolved SDK directory in DOTNET_SDK_ROOT; prefer it. + if (Environment.GetEnvironmentVariable(EnvironmentVariableName) is { Length: > 0 } sdkRoot) + { + return sdkRoot; + } + + // The SDK assemblies ship in the versioned SDK directory, so the location of this assembly is that + // directory for the JIT-compiled managed CLI. Under a single-file / NativeAOT deployment + // Assembly.Location is empty (which is what the IL3000 analyzer flags); fall through to + // AppContext.BaseDirectory in that case - the AOT bridge sets DOTNET_SDK_ROOT (preferred above). + string? assemblyDirectory = Path.GetDirectoryName(typeof(SdkPaths).Assembly.Location); + return string.IsNullOrEmpty(assemblyDirectory) ? AppContext.BaseDirectory : assemblyDirectory; + } +} diff --git a/src/Cli/Microsoft.DotNet.Cli.Utils/TypeInfo.cs b/src/Cli/Microsoft.DotNet.Cli.Utils/TypeInfo.cs new file mode 100644 index 000000000000..e88ff2d7b417 --- /dev/null +++ b/src/Cli/Microsoft.DotNet.Cli.Utils/TypeInfo.cs @@ -0,0 +1,72 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +// Pulled from dotnet/msbuild (src/Framework/Utilities/TypeInfo.cs); a dependency of BufferScope used to +// decide whether ArrayPool buffers must be cleared on return. Kept in sync with the upstream version. + +#if NET +using System.Runtime.CompilerServices; +#else +using System; +using System.Runtime.InteropServices; +#endif +using System.Threading; + +namespace Microsoft.DotNet.Cli.Utils; + +/// +/// Type information for a type . +/// +internal static partial class TypeInfo +{ + // Tri-state: 0 = not computed, 1 = false (no references), 2 = true (has references) + private static int s_hasReferences; + + /// + /// Returns if the type is a reference type or contains references. + /// + public static bool IsReferenceOrContainsReferences() + { + int value = Volatile.Read(ref s_hasReferences); + if (value != 0) + { + return value == 2; + } + +#if NET + bool result = RuntimeHelpers.IsReferenceOrContainsReferences(); +#else + bool result = HasReferences(); + + static bool HasReferences() + { + if (!typeof(T).IsValueType) + { + return true; + } + + if (typeof(T).IsPrimitive + || typeof(T).IsEnum + || typeof(T) == typeof(DateTime)) + { + return false; + } + + try + { + GCHandle handle = GCHandle.Alloc(default(T), GCHandleType.Pinned); + handle.Free(); + return false; + } + catch (Exception) + { + // Contained a reference + return true; + } + } +#endif + + Interlocked.CompareExchange(ref s_hasReferences, result ? 2 : 1, 0); + return result; + } +} diff --git a/src/Cli/dn/Program.cs b/src/Cli/dn/Program.cs index c6246925bc22..112b506011d6 100644 --- a/src/Cli/dn/Program.cs +++ b/src/Cli/dn/Program.cs @@ -21,7 +21,27 @@ static unsafe int Main(string[] args) string hostPath = Environment.ProcessPath!; string baseDir = AppContext.BaseDirectory.TrimEnd(Path.DirectorySeparatorChar); string dotnetRoot = ResolveDotnetRoot(); - string sdkDir = baseDir; + + // The muxer loads dotnet-aot from the resolved SDK directory. For local testing, allow the + // harness to emulate the deployed (non-flat) layout - where dotnet-aot lives in sdk\\ + // while dn stays in the parent - via DOTNET_AOT_SDK_DIR, which overrides both where the + // library is loaded from and the sdk_dir passed to dotnet_execute. Defaults to dn's own + // directory (the flat layout). + string sdkDir = ResolveAotSdkDir(baseDir); + if (!string.Equals(sdkDir, baseDir, StringComparison.OrdinalIgnoreCase)) + { + NativeLibrary.SetDllImportResolver(typeof(Program).Assembly, (name, assembly, searchPath) => + string.Equals(name, "dotnet-aot", StringComparison.Ordinal) + && NativeLibrary.TryLoad(Path.Combine(sdkDir, AotLibraryFileName), out nint handle) + ? handle + : nint.Zero); + } + + // Test hook: pass an empty sdk_dir to exercise dotnet-aot's self-locate fallback while still + // loading the library from the resolved directory above. + string sdkDirArg = string.Equals(Environment.GetEnvironmentVariable("DOTNET_AOT_BLANK_SDKDIR"), "1", StringComparison.Ordinal) + ? string.Empty + : sdkDir; string hostfxrPath = ResolveHostfxrPath(dotnetRoot); // Marshal argv to native platform strings (UTF-16 on Windows, UTF-8 on Unix) @@ -37,7 +57,7 @@ static unsafe int Main(string[] args) nint hpNative = MarshalStringToNative(hostPath); nint drNative = MarshalStringToNative(dotnetRoot); - nint sdNative = MarshalStringToNative(sdkDir); + nint sdNative = MarshalStringToNative(sdkDirArg); nint hfNative = MarshalStringToNative(hostfxrPath); try @@ -99,6 +119,27 @@ private static string ResolveHostfxrPath(string dotnetRoot) File.Exists); } + /// + /// Resolves the directory dotnet-aot is loaded from (and passed as sdk_dir). Honors the + /// DOTNET_AOT_SDK_DIR override for emulating the deployed non-flat layout; otherwise defaults + /// to dn's own directory. + /// + private static string ResolveAotSdkDir(string baseDir) + { + string? overrideDir = Environment.GetEnvironmentVariable("DOTNET_AOT_SDK_DIR"); + return !string.IsNullOrEmpty(overrideDir) && Directory.Exists(overrideDir) + ? Path.TrimEndingDirectorySeparator(Path.GetFullPath(overrideDir)) + : baseDir; + } + + /// + /// The platform-specific file name of the dotnet-aot native library. + /// + private static string AotLibraryFileName => + OperatingSystem.IsWindows() ? "dotnet-aot.dll" + : OperatingSystem.IsMacOS() ? "dotnet-aot.dylib" + : "dotnet-aot.so"; + /// /// Marshals a string to a native platform string (UTF-16 on Windows, UTF-8 on Unix) /// to match hostfxr's char_t definition. diff --git a/src/Cli/dn/run-dn.ps1 b/src/Cli/dn/run-dn.ps1 new file mode 100644 index 000000000000..357c9ee0801d --- /dev/null +++ b/src/Cli/dn/run-dn.ps1 @@ -0,0 +1,200 @@ +#!/usr/bin/env pwsh +# Licensed to the .NET Foundation under one or more agreements. +# The .NET Foundation licenses this file to you under the MIT license. + +<# +.SYNOPSIS + Build the dn AOT harness layout and run a dotnet command through the Native AOT + CLI (dotnet-aot) - and optionally the managed fallback - for comparison. + +.DESCRIPTION + Publishes dotnet-aot (NativeAOT) and the dn native host, builds the managed dotnet + CLI, and assembles them into the dn publish directory (dn + dotnet-aot.dll + the + managed dotnet.dll + deps). Then runs `dn ` with DOTNET_CLI_ENABLEAOT + toggled: + * Aot DOTNET_CLI_ENABLEAOT=true: the command runs in-process in dotnet-aot.dll. + * Managed dn hosts the copied dotnet.dll (same source, JIT-compiled). + * Compare runs both and diffs the output (an empty diff means parity). + + DOTNET_ROOT is pointed at the repo-local .dotnet because the publish directory is + not a full SDK layout. + +.PARAMETER Command + The argument string passed to dn (split on spaces). Example: "--info". + +.PARAMETER Mode + Aot (default), Managed, or Compare. + +.PARAMETER Configuration + Debug (default) or Release. + +.PARAMETER Rid + Runtime identifier. Auto-detected from the host when omitted. + +.PARAMETER Layout + Flat (default) co-locates dotnet-aot with dn. Separated places dotnet-aot (and the managed + payload) in a sdk\\ subfolder while dn stays in the parent, emulating the deployed muxer + layout so AppContext.BaseDirectory is no longer the SDK directory. + +.PARAMETER SelfLocate + With -Layout Separated, make dn pass an empty sdk_dir so dotnet-aot resolves the SDK directory by + self-locating its own module (the fallback path). + +.PARAMETER NoBuild + Skip the publish/build/copy steps and run the already-assembled layout. + +.EXAMPLE + ./run-dn.ps1 -Command "--info" + +.EXAMPLE + ./run-dn.ps1 -Command "--info" -Mode Compare + +.EXAMPLE + ./run-dn.ps1 -Command "workload --info" -NoBuild +#> + +[CmdletBinding()] +param( + [string]$Command = "--info", + [ValidateSet("Aot", "Managed", "Compare")] + [string]$Mode = "Aot", + [ValidateSet("Flat", "Separated")] + [string]$Layout = "Flat", + [switch]$SelfLocate, + [string]$Configuration = "Debug", + [string]$Rid, + [switch]$NoBuild +) + +$ErrorActionPreference = "Stop" + +$repoRoot = (Resolve-Path (Join-Path $PSScriptRoot ".." ".." "..")).Path +$isWin = $IsWindows -or ($env:OS -eq "Windows_NT") +$exeSuffix = if ($isWin) { ".exe" } else { "" } +$dotnet = Join-Path $repoRoot ".dotnet" "dotnet$exeSuffix" +$dnExeName = "dn$exeSuffix" + +if (-not $Rid) { + $arch = [System.Runtime.InteropServices.RuntimeInformation]::OSArchitecture.ToString().ToLowerInvariant() + $os = if ($isWin) { "win" } elseif ($IsMacOS) { "osx" } else { "linux" } + $Rid = "$os-$arch" +} + +Write-Host "Repo root: $repoRoot" +Write-Host "Configuration: $Configuration" +Write-Host "RID: $Rid" +Write-Host "Command: dn $Command" +Write-Host "Mode: $Mode" +Write-Host "" + +function Resolve-PublishPath([string]$relativeGlob) { + # Globs the TFM folder so paths are not pinned to a specific net version. + return (Resolve-Path (Join-Path $repoRoot $relativeGlob) -ErrorAction SilentlyContinue | + Select-Object -First 1).Path +} + +if (-not $NoBuild) { + Write-Host "Publishing dotnet-aot (NativeAOT)..." -ForegroundColor Cyan + & $dotnet publish (Join-Path $repoRoot "src/Cli/dotnet-aot/dotnet-aot.csproj") -r $Rid -c $Configuration + if ($LASTEXITCODE -ne 0) { throw "dotnet-aot publish failed." } + + Write-Host "Publishing dn host..." -ForegroundColor Cyan + & $dotnet publish (Join-Path $repoRoot "src/Cli/dn/dn.csproj") -r $Rid -c $Configuration + if ($LASTEXITCODE -ne 0) { throw "dn publish failed." } + + Write-Host "Building managed dotnet CLI (fallback)..." -ForegroundColor Cyan + & $dotnet build (Join-Path $repoRoot "src/Cli/dotnet/dotnet.csproj") -c $Configuration + if ($LASTEXITCODE -ne 0) { throw "managed dotnet build failed." } + + $dnPublishDir = Resolve-PublishPath "artifacts/bin/dn/$Configuration/*/$Rid/publish" + $aotDll = Resolve-PublishPath "artifacts/bin/dotnet-aot/$Configuration/*/$Rid/publish/dotnet-aot.dll" + $managedDir = (Get-ChildItem -Directory (Join-Path $repoRoot "artifacts/bin/dotnet/$Configuration") | + Sort-Object LastWriteTime -Descending | Select-Object -First 1).FullName + + if (-not $dnPublishDir) { throw "Could not locate the dn publish directory after build." } + if (-not $aotDll) { throw "Could not locate dotnet-aot.dll after publish." } + + $sdkTargetDir = $dnPublishDir + if ($Layout -eq "Separated") { + $sdkTargetDir = Join-Path $dnPublishDir "sdk/11.0.100" + New-Item -ItemType Directory -Force -Path $sdkTargetDir | Out-Null + # dotnet-aot must live only in the versioned SDK subfolder, not next to dn. + Remove-Item (Join-Path $dnPublishDir "dotnet-aot.dll") -Force -ErrorAction SilentlyContinue + } + + Write-Host "Assembling layout into $sdkTargetDir ..." -ForegroundColor Cyan + Copy-Item $aotDll $sdkTargetDir -Force + Copy-Item (Join-Path $managedDir "*") $sdkTargetDir -Recurse -Force +} + +$dnPublishDir = Resolve-PublishPath "artifacts/bin/dn/$Configuration/*/$Rid/publish" +if (-not $dnPublishDir) { throw "dn publish directory not found. Run without -NoBuild first." } + +$dnExe = Join-Path $dnPublishDir $dnExeName +if (-not (Test-Path $dnExe)) { throw "dn host not found at '$dnExe'. Run without -NoBuild first." } + +$argList = $Command.Split(' ', [System.StringSplitOptions]::RemoveEmptyEntries) +$env:DOTNET_ROOT = (Join-Path $repoRoot ".dotnet") +$env:DOTNET_CLI_TELEMETRY_OPTOUT = "1" + +# Emulate the deployed non-flat layout: tell dn to load dotnet-aot from (and pass as sdk_dir) the +# versioned SDK subfolder, optionally forcing the self-locate fallback by blanking sdk_dir. +if ($Layout -eq "Separated") { + $env:DOTNET_AOT_SDK_DIR = (Join-Path $dnPublishDir "sdk/11.0.100") + Write-Host "Layout: Separated (dotnet-aot in $($env:DOTNET_AOT_SDK_DIR))" -ForegroundColor Cyan +} +else { + Remove-Item Env:\DOTNET_AOT_SDK_DIR -ErrorAction SilentlyContinue +} +if ($SelfLocate) { + $env:DOTNET_AOT_BLANK_SDKDIR = "1" + Write-Host "Self-locate: enabled (dn passes empty sdk_dir; dotnet-aot self-locates)" -ForegroundColor Cyan +} +else { + Remove-Item Env:\DOTNET_AOT_BLANK_SDKDIR -ErrorAction SilentlyContinue +} + +function Invoke-Dn([bool]$enableAot) { + if ($enableAot) { + $env:DOTNET_CLI_ENABLEAOT = "true" + } + else { + Remove-Item Env:\DOTNET_CLI_ENABLEAOT -ErrorAction SilentlyContinue + } + & $dnExe @argList 2>&1 +} + +switch ($Mode) { + "Aot" { + Write-Host "===== AOT (DOTNET_CLI_ENABLEAOT=true) =====" -ForegroundColor Green + Invoke-Dn $true + } + "Managed" { + Write-Host "===== Managed (DOTNET_CLI_ENABLEAOT unset) =====" -ForegroundColor Green + Invoke-Dn $false + } + "Compare" { + $logDir = Join-Path $repoRoot "artifacts/log" + New-Item -ItemType Directory -Force -Path $logDir | Out-Null + + $aotOut = Invoke-Dn $true + $managedOut = Invoke-Dn $false + $aotOut | Set-Content (Join-Path $logDir "dn-aot.txt") + $managedOut | Set-Content (Join-Path $logDir "dn-managed.txt") + + Write-Host "===== AOT (DOTNET_CLI_ENABLEAOT=true) =====" -ForegroundColor Green + $aotOut | Write-Output + Write-Host "===== Managed (fallback) =====" -ForegroundColor Green + $managedOut | Write-Output + + $diff = Compare-Object $aotOut $managedOut + Write-Host "" + if ($diff) { + Write-Host "DIFFERENCES (AOT vs managed):" -ForegroundColor Yellow + $diff | Format-Table -AutoSize + } + else { + Write-Host "IDENTICAL: AOT and managed output match line-for-line." -ForegroundColor Green + } + } +} diff --git a/src/Cli/dotnet-aot/NativeEntryPoint.cs b/src/Cli/dotnet-aot/NativeEntryPoint.cs index 1be8d0a2fb46..d2f6353d7840 100644 --- a/src/Cli/dotnet-aot/NativeEntryPoint.cs +++ b/src/Cli/dotnet-aot/NativeEntryPoint.cs @@ -23,6 +23,14 @@ static unsafe partial class NativeEntryPoint /// internal static string? DotnetRoot { get; set; } + /// + /// The versioned SDK directory (the folder containing dotnet.dll, MSBuild.dll, Sdks\, ...), + /// resolved from the host-provided sdk_dir or by self-locating the dotnet-aot module. Also + /// published to the DOTNET_SDK_ROOT environment variable so compiled-in assemblies that probe + /// AppContext.BaseDirectory can find the SDK. See src/Cli/dotnet-aot/SdkRootResolution.md. + /// + internal static string? SdkDirectory { get; set; } + [UnmanagedCallersOnly(EntryPoint = "dotnet_execute")] static int Execute( nint hostPathPtr, // const char_t* host_path @@ -61,6 +69,23 @@ internal static int ExecuteCore( string hostPath, string dotnetRoot, string sdkDir, string hostfxrPath, string[] args) { + // Resolve the versioned SDK directory: prefer the host-provided sdk_dir (the muxer resolves it + // to locate dotnet-aot), otherwise self-locate the dotnet-aot module. Under NativeAOT + // AppContext.BaseDirectory is the install root (the muxer's directory), not the SDK directory, + // so publish the resolved value in DOTNET_SDK_ROOT for the compiled-in assemblies (MSBuild, + // NuGet, the command resolvers, ...) that otherwise probe AppContext.BaseDirectory. See + // src/Cli/dotnet-aot/SdkRootResolution.md. + string sdkDirectory = SdkRootLocator.Resolve(sdkDir); + SdkDirectory = string.IsNullOrEmpty(sdkDirectory) ? null : sdkDirectory; + if (!string.IsNullOrEmpty(sdkDirectory)) + { + Environment.SetEnvironmentVariable(SdkPaths.EnvironmentVariableName, sdkDirectory); + } + else + { + Console.Error.WriteLine(CliStrings.SdkDirectoryCouldNotBeDetermined); + } + // Telemetry is best-effort and must never prevent the CLI from running. Initializing // it can fail on some layouts (e.g. the NativeAOT muxer cannot resolve the crypto // native library used to hash telemetry properties on macOS - see dotnet/sdk#54544), @@ -183,7 +208,7 @@ internal static int ExecuteCore( // or local tool, a command on the PATH, ...) or an implicit file-based app (`dotnet app.cs`). // Resolve and invoke external commands in AOT when possible; defer file-based apps, legacy // project tools, and anything that does not resolve to the managed CLI. - else if (parseResult is not null && TryInvokeExternalCommand(parseResult, args, sdkDir, mainActivity, globalJsonState, out exitCode, out success)) + else if (parseResult is not null && TryInvokeExternalCommand(parseResult, args, sdkDirectory, mainActivity, globalJsonState, out exitCode, out success)) { return exitCode; } @@ -200,8 +225,8 @@ internal static int ExecuteCore( mainActivity.SetTag("command.name", fallbackName); } - string dotnetDll = Path.Join(sdkDir, "dotnet.dll"); - string runtimeConfig = Path.Join(sdkDir, "dotnet.runtimeconfig.json"); + string dotnetDll = Path.Join(sdkDirectory, "dotnet.dll"); + string runtimeConfig = Path.Join(sdkDirectory, "dotnet.runtimeconfig.json"); if (File.Exists(dotnetDll) && File.Exists(runtimeConfig)) { @@ -215,7 +240,7 @@ internal static int ExecuteCore( } // No managed fallback available - Console.Error.WriteLine($"The managed fallback could not be located. Expected '{dotnetDll}' and '{runtimeConfig}'."); + Console.Error.WriteLine(string.Format(CliStrings.ManagedFallbackCouldNotBeLocated, dotnetDll, runtimeConfig)); return exitCode; } finally diff --git a/src/Cli/dotnet-aot/SdkRootLocator.cs b/src/Cli/dotnet-aot/SdkRootLocator.cs new file mode 100644 index 000000000000..e566a272995c --- /dev/null +++ b/src/Cli/dotnet-aot/SdkRootLocator.cs @@ -0,0 +1,168 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +#if TARGET_WINDOWS +using Microsoft.DotNet.Cli.Utils; +using Windows.Win32; +using Windows.Win32.Foundation; +#endif + +namespace Microsoft.DotNet.Cli; + +/// +/// Resolves the versioned SDK directory for the Native AOT bridge. Prefers the SDK directory the +/// host (the muxer, or the dn harness) passed in, and falls back to self-locating the +/// directory of the running dotnet-aot native library on disk. See +/// src/Cli/dotnet-aot/SdkRootResolution.md. +/// +internal static unsafe partial class SdkRootLocator +{ + /// + /// Resolves the versioned SDK directory. When the host supplies a non-empty + /// it is authoritative (and, in debug builds, is asserted to + /// match where dotnet-aot was actually loaded from). Otherwise the directory is + /// self-located from the native library's own module path. + /// + /// The resolved SDK directory, or an empty string when it cannot be determined. + internal static string Resolve(string sdkDirArgument) + { + if (!string.IsNullOrEmpty(sdkDirArgument)) + { + AssertMatchesSelfLocation(sdkDirArgument); + return sdkDirArgument; + } + + return TrySelfLocateSdkDirectory() ?? string.Empty; + } + + /// + /// Returns the directory of the running dotnet-aot native library, or + /// when it cannot be determined - for example when this code runs + /// JIT-compiled in unit tests rather than as part of the NativeAOT image. + /// + internal static string? TrySelfLocateSdkDirectory() + { + // Only meaningful in a NativeAOT image. Under the JIT the module that owns this code is the + // test/runtime host, not dotnet-aot.dll, so a self-location would be misleading. + if (RuntimeFeature.IsDynamicCodeSupported) + { + return null; + } + + try + { + string? modulePath = OperatingSystem.IsWindows() + ? GetSelfModulePathWindows() + : GetSelfModulePathPosix(); + + return string.IsNullOrEmpty(modulePath) ? null : Path.GetDirectoryName(modulePath); + } + catch + { + // The self-locate is a best-effort fallback; it must never fault the CLI. + return null; + } + } + + [Conditional("DEBUG")] + private static void AssertMatchesSelfLocation(string sdkDirArgument) + { + string? selfLocated = TrySelfLocateSdkDirectory(); + Debug.Assert( + selfLocated is null || PathsEqual(sdkDirArgument, selfLocated), + $"Host-provided sdk_dir '{sdkDirArgument}' does not match the directory dotnet-aot was loaded from ('{selfLocated}'). " + + "The host must pass the versioned SDK directory that contains the dotnet-aot binary."); + } + + // The address of a method in this assembly identifies the native module that contains it. + // [UnmanagedCallersOnly] gives the method a stable native entry point in the module to hand to + // GetModuleHandleEx; it is never called, only its address is taken. + [UnmanagedCallersOnly] + private static void SelfAnchor() { } + + private static string? GetSelfModulePathWindows() + { +#if TARGET_WINDOWS + nint address = (nint)(delegate* unmanaged)&SelfAnchor; + HMODULE module; + if (!PInvoke.GetModuleHandleEx( + PInvoke.GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS | PInvoke.GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT, + new PCWSTR((char*)address), + &module)) + { + // GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS resolves the module from an address in it; + // GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT means we hold no reference (do not free it). + return null; + } + + // GetModuleFileName truncates (returning the buffer length and setting ERROR_INSUFFICIENT_BUFFER) + // when the path does not fit, so grow the buffer and retry. Stack-allocate MAX_PATH for the common + // case; a deliberately tiny buffer in debug builds forces the grow path so the AOT tests exercise it. +#if DEBUG + using BufferScope buffer = new(stackalloc char[8]); +#else + using BufferScope buffer = new(stackalloc char[(int)PInvoke.MAX_PATH]); +#endif + while (true) + { + uint length = PInvoke.GetModuleFileName(module, buffer.AsSpan()); + if (length == 0) + { + return null; + } + + if (length < (uint)buffer.Length) + { + return buffer.Slice(0, (int)length).ToString(); + } + + // The path did not fit (length == buffer capacity); grow and retry. + buffer.EnsureCapacity(buffer.Length * 2); + } +#else + // CsWin32 (hence the Windows self-locate) is unavailable off Windows; callers fall back to + // the host-provided sdk_dir. + return null; +#endif + } + + private static string? GetSelfModulePathPosix() + { + // Best-effort on Unix: dladdr resolves the shared object containing the given address. + // libdl.so.2 is the glibc runtime soname; on platforms where it is unavailable the catch in + // TrySelfLocateSdkDirectory falls back to the host-provided sdk_dir. + nint address = (nint)(delegate* unmanaged)&SelfAnchor; + if (dladdr(address, out DlInfo info) == 0 || info.dli_fname == 0) + { + return null; + } + + return Marshal.PtrToStringUTF8(info.dli_fname); + } + + private static bool PathsEqual(string left, string right) + { + static string Normalize(string path) => Path.TrimEndingDirectorySeparator(Path.GetFullPath(path)); + + StringComparison comparison = OperatingSystem.IsWindows() + ? StringComparison.OrdinalIgnoreCase + : StringComparison.Ordinal; + + return string.Equals(Normalize(left), Normalize(right), comparison); + } + + [LibraryImport("libdl.so.2", EntryPoint = "dladdr")] + private static partial int dladdr(nint addr, out DlInfo info); + + [StructLayout(LayoutKind.Sequential)] + private struct DlInfo + { + public nint dli_fname; + public nint dli_fbase; + public nint dli_sname; + public nint dli_saddr; + } +} diff --git a/src/Cli/dotnet-aot/SdkRootResolution.md b/src/Cli/dotnet-aot/SdkRootResolution.md new file mode 100644 index 000000000000..2167d26fa39a --- /dev/null +++ b/src/Cli/dotnet-aot/SdkRootResolution.md @@ -0,0 +1,74 @@ +# Finding the versioned SDK root from dotnet-aot + +Companion to [DESIGN.md](DESIGN.md). + +In a deployed SDK the muxer `dotnet.exe` (in the install root) loads +`dotnet-aot.dll` from the versioned SDK directory and calls `dotnet_execute` +directly. Code in that native bubble needs the versioned SDK directory - the +folder with `dotnet.dll`, `MSBuild.dll`, `Sdks/`, `DotnetTools/`, and the SDK +targets - to resolve SDK-relative paths, but the BCL "where am I" APIs do not +point there. + +## Two directories + +| Concept | Deployed example | Contains | +|---|---|---| +| Install root (`DOTNET_ROOT`) | `C:\Program Files\dotnet` | muxer `dotnet.exe`, `host\fxr\`, shared runtimes, `sdk\` | +| Versioned SDK root | `C:\Program Files\dotnet\sdk\11.0.100` | `dotnet.dll`, `dotnet-aot.dll`, `MSBuild.dll`, `Sdks\`, `DotnetTools\`, targets | + +Code that needs the **root** (workloads, shared runtime, hostfxr) is fine because +the muxer is the process. Code that needs the **versioned SDK directory** (MSBuild, +SDK-shipped tools, targets, forwarders) is what needs care. + +## Why the BCL location APIs return the root + +The managed CLI is hosted through hostfxr, which sets `APP_CONTEXT_BASE_DIRECTORY` +to the managed entry assembly's directory (the versioned SDK directory), so +`AppContext.BaseDirectory` is correct there. `dotnet-aot.dll` is a NativeAOT shared +library that the muxer loads directly: there is no hostfxr step, nothing sets +`APP_CONTEXT_BASE_DIRECTORY`, and the location APIs fall back to the process +executable - the muxer, i.e. the install root. + +Inside the AOT bubble: + +| API | Result | +|---|---| +| `AppContext.BaseDirectory` | install root (muxer dir) | +| `Environment.ProcessPath`, `GetCommandLineArgs()[0]`, `Process.MainModule.FileName` | muxer exe | +| `RuntimeEnvironment.GetRuntimeDirectory()` | muxer dir | +| `Assembly.Location` / `typeof(T).Assembly.Location` | `""` (and ILC fails the build with `IL3000`) | +| self-locating the loaded module (`GetModuleFileName` / `dladdr`) | the real `dotnet-aot` directory | + +Only two mechanisms yield the versioned SDK directory: the host passes it in, or +the module self-locates. + +## Resolution + +- **Host argument (authoritative).** The muxer already resolves the versioned SDK + directory to find `dotnet-aot`, and passes it as the `sdk_dir` argument to + `dotnet_execute`. + > `sdk_dir` is the absolute path of the versioned SDK directory that contains the + > invoked `dotnet-aot` binary. +- **Self-locate (fallback).** When `sdk_dir` is missing or empty, `SdkRootLocator` + finds the `dotnet-aot` module from its own code address: Windows + `GetModuleHandleEx(FROM_ADDRESS | UNCHANGED_REFCOUNT, &export)` + + `GetModuleFileName`; Unix `dladdr`. +- **Publish once.** `NativeEntryPoint.ExecuteCore` writes the resolved directory to + the `DOTNET_SDK_ROOT` environment variable so the compiled-in assemblies find it + without threading a parameter through every call. +- **Read.** In-repo code reads `SdkPaths.SdkDirectory` (in + `Microsoft.DotNet.Cli.Utils`), which resolves `DOTNET_SDK_ROOT` -> the SDK + assembly directory -> `AppContext.BaseDirectory` (once, cached). Out-of-repo code + (MSBuild tasks, NuGet, the runtime) reads `DOTNET_SDK_ROOT` first, else its + existing BCL logic. + +The managed CLI keeps using `AppContext.BaseDirectory`, where it is correct. + +## Testing + +`dn.exe` and `run-dn.ps1` provide a separated layout that places `dotnet-aot.dll` +in a `sdk\\` subdirectory while `dn.exe` stays in the parent, mirroring the +deployed muxer. A flat layout hides SDK-directory bugs because +`AppContext.BaseDirectory` equals `sdk_dir` there by accident. Tests assert that +`--info`'s `Base Path` is the SDK subdirectory in both the passed-`sdk_dir` and +self-locate cases. diff --git a/src/Cli/dotnet-aot/dotnet-aot.csproj b/src/Cli/dotnet-aot/dotnet-aot.csproj index 96ffdbd0fc4d..9b6f78b24f2d 100644 --- a/src/Cli/dotnet-aot/dotnet-aot.csproj +++ b/src/Cli/dotnet-aot/dotnet-aot.csproj @@ -49,6 +49,7 @@ + diff --git a/src/Cli/dotnet/CliStrings.resx b/src/Cli/dotnet/CliStrings.resx index 0e75ab813fe8..7572e0a7827e 100644 --- a/src/Cli/dotnet/CliStrings.resx +++ b/src/Cli/dotnet/CliStrings.resx @@ -653,4 +653,11 @@ setx PATH "%PATH%;{0}" You are running the 'tool install' operation with an 'HTTP' source: {0}. NuGet requires HTTPS sources. To use an HTTP source, you must explicitly set 'allowInsecureConnections' to true in your NuGet.Config file. Refer to https://aka.ms/nuget-https-everywhere for more information. + + warning: could not determine the SDK directory - no sdk_dir was provided and dotnet-aot could not locate its own module; SDK-relative resolution may be incorrect. + {Locked="sdk_dir"}{Locked="dotnet-aot"} + + + The managed fallback could not be located. Expected '{0}' and '{1}'. + diff --git a/src/Cli/dotnet/CommandFactory/CommandResolution/AppBaseCommandResolver.cs b/src/Cli/dotnet/CommandFactory/CommandResolution/AppBaseCommandResolver.cs index 50f400437e84..2820dd9a7969 100644 --- a/src/Cli/dotnet/CommandFactory/CommandResolution/AppBaseCommandResolver.cs +++ b/src/Cli/dotnet/CommandFactory/CommandResolution/AppBaseCommandResolver.cs @@ -13,7 +13,7 @@ public class AppBaseCommandResolver(IEnvironmentProvider environment, internal override string ResolveCommandPath(CommandResolverArguments commandResolverArguments) { return _environment.GetCommandPathFromRootPath( - AppContext.BaseDirectory, + SdkPaths.SdkDirectory, commandResolverArguments.CommandName); } } diff --git a/src/Cli/dotnet/CommandFactory/CommandResolution/AppBaseDllCommandResolver.cs b/src/Cli/dotnet/CommandFactory/CommandResolution/AppBaseDllCommandResolver.cs index cfbb3468fd1d..fc7dea5f9e49 100644 --- a/src/Cli/dotnet/CommandFactory/CommandResolution/AppBaseDllCommandResolver.cs +++ b/src/Cli/dotnet/CommandFactory/CommandResolution/AppBaseDllCommandResolver.cs @@ -18,7 +18,7 @@ public CommandSpec Resolve(CommandResolverArguments commandResolverArguments) } if (commandResolverArguments.CommandName.EndsWith(FileNameSuffixes.DotNet.DynamicLib)) { - var localPath = Path.Combine(AppContext.BaseDirectory, + var localPath = Path.Combine(SdkPaths.SdkDirectory, commandResolverArguments.CommandName); if (File.Exists(localPath)) { diff --git a/src/Cli/dotnet/CommandFactory/CommandResolution/DotnetToolsCommandResolver.cs b/src/Cli/dotnet/CommandFactory/CommandResolution/DotnetToolsCommandResolver.cs index 3c06b61b9900..f135e356d1ca 100644 --- a/src/Cli/dotnet/CommandFactory/CommandResolution/DotnetToolsCommandResolver.cs +++ b/src/Cli/dotnet/CommandFactory/CommandResolution/DotnetToolsCommandResolver.cs @@ -18,8 +18,10 @@ public static DotnetToolsCommandResolver ForSdkRoot(string sdkRoot) public DotnetToolsCommandResolver(string? dotnetToolPath = null) { - // AppContext.BaseDirectory for the NAOT app will be the 'dotnet' root, not the per-SDK version path. - _dotnetToolPath = dotnetToolPath ?? Path.Combine(AppContext.BaseDirectory, "DotnetTools"); + // 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"); } public CommandSpec? Resolve(CommandResolverArguments arguments) diff --git a/src/Cli/dotnet/ParserOptionActions.cs b/src/Cli/dotnet/ParserOptionActions.cs index 6b47eb2b8cda..773fd150f217 100644 --- a/src/Cli/dotnet/ParserOptionActions.cs +++ b/src/Cli/dotnet/ParserOptionActions.cs @@ -162,7 +162,7 @@ public override int Invoke(ParseResult parseResult) // GetDisplayRid consults the shared framework's deps file, which isn't available in AOT. Reporter.Output.WriteLine($" RID: {RuntimeInformation.RuntimeIdentifier}"); #endif - Reporter.Output.WriteLine($" Base Path: {AppContext.BaseDirectory}"); + Reporter.Output.WriteLine($" Base Path: {SdkPaths.SdkDirectory}"); Reporter.Output.WriteLine(); Reporter.Output.WriteLine($"{LocalizableStrings.DotnetWorkloadInfoLabel}"); new WorkloadInfoHelper(isInteractive: false).ShowWorkloadsInfo(showVersion: false); diff --git a/src/Cli/dotnet/xlf/CliStrings.cs.xlf b/src/Cli/dotnet/xlf/CliStrings.cs.xlf index 51062738777f..33632ffbf172 100644 --- a/src/Cli/dotnet/xlf/CliStrings.cs.xlf +++ b/src/Cli/dotnet/xlf/CliStrings.cs.xlf @@ -465,6 +465,11 @@ setx PATH "%PATH%;{0}" Server MSBuild + + The managed fallback could not be located. Expected '{0}' and '{1}'. + The managed fallback could not be located. Expected '{0}' and '{1}'. + + File {0} came from another computer and might be blocked to help protect this computer. For more information, including how to unblock, see https://aka.ms/motw Soubor {0} pochází z jiného počítače a je možné, že se zablokoval, aby se ochránil tento počítač. Další informace včetně toho, jak soubor odblokovat, najdete na adrese https://aka.ms/motw. @@ -713,6 +718,11 @@ setx PATH "%PATH%;{0}" Povolí diagnostický výstup. + + warning: could not determine the SDK directory - no sdk_dir was provided and dotnet-aot could not locate its own module; SDK-relative resolution may be incorrect. + warning: could not determine the SDK directory - no sdk_dir was provided and dotnet-aot could not locate its own module; SDK-relative resolution may be incorrect. + {Locked="sdk_dir"}{Locked="dotnet-aot"} + The '--self-contained' and '--no-self-contained' options cannot be used together. Možnosti --self-contained a --no-self-contained nelze použít společně. diff --git a/src/Cli/dotnet/xlf/CliStrings.de.xlf b/src/Cli/dotnet/xlf/CliStrings.de.xlf index 62505101181e..4c510038db20 100644 --- a/src/Cli/dotnet/xlf/CliStrings.de.xlf +++ b/src/Cli/dotnet/xlf/CliStrings.de.xlf @@ -465,6 +465,11 @@ setx PATH "%PATH%;{0}" MSBuild-Server + + The managed fallback could not be located. Expected '{0}' and '{1}'. + The managed fallback could not be located. Expected '{0}' and '{1}'. + + File {0} came from another computer and might be blocked to help protect this computer. For more information, including how to unblock, see https://aka.ms/motw Die Datei "{0}" stammt von einem anderen Computer und wurde möglicherweise zum Schutz dieses Computers blockiert. Weitere Informationen zum Aufheben der Sperre und zu anderen Aktionen finden Sie unter https://aka.ms/motw. @@ -713,6 +718,11 @@ setx PATH "%PATH%;{0}" Diagnoseausgabe aktivieren. + + warning: could not determine the SDK directory - no sdk_dir was provided and dotnet-aot could not locate its own module; SDK-relative resolution may be incorrect. + warning: could not determine the SDK directory - no sdk_dir was provided and dotnet-aot could not locate its own module; SDK-relative resolution may be incorrect. + {Locked="sdk_dir"}{Locked="dotnet-aot"} + The '--self-contained' and '--no-self-contained' options cannot be used together. Die Optionen „--self-contained“ und „--no-self-contained“ können nicht gemeinsam verwendet werden. diff --git a/src/Cli/dotnet/xlf/CliStrings.es.xlf b/src/Cli/dotnet/xlf/CliStrings.es.xlf index ee5dc7d86822..861ce0fec46c 100644 --- a/src/Cli/dotnet/xlf/CliStrings.es.xlf +++ b/src/Cli/dotnet/xlf/CliStrings.es.xlf @@ -465,6 +465,11 @@ setx PATH "%PATH%;{0}" Servidor de MSBuild + + The managed fallback could not be located. Expected '{0}' and '{1}'. + The managed fallback could not be located. Expected '{0}' and '{1}'. + + File {0} came from another computer and might be blocked to help protect this computer. For more information, including how to unblock, see https://aka.ms/motw El archivo {0} procedía de otro equipo y podría bloquearse para ayudar a proteger este equipo. Para obtener más información, y cómo desbloquearlo, consulte https://aka.ms/motw @@ -713,6 +718,11 @@ setx PATH "%PATH%;{0}" Habilita la salida de diagnóstico. + + warning: could not determine the SDK directory - no sdk_dir was provided and dotnet-aot could not locate its own module; SDK-relative resolution may be incorrect. + warning: could not determine the SDK directory - no sdk_dir was provided and dotnet-aot could not locate its own module; SDK-relative resolution may be incorrect. + {Locked="sdk_dir"}{Locked="dotnet-aot"} + The '--self-contained' and '--no-self-contained' options cannot be used together. Las opciones '--self-contained' y '--no-self-contained' no se pueden usar juntas. diff --git a/src/Cli/dotnet/xlf/CliStrings.fr.xlf b/src/Cli/dotnet/xlf/CliStrings.fr.xlf index d65a11862746..0c9840c4f60c 100644 --- a/src/Cli/dotnet/xlf/CliStrings.fr.xlf +++ b/src/Cli/dotnet/xlf/CliStrings.fr.xlf @@ -465,6 +465,11 @@ setx PATH "%PATH%;{0}" Serveur MSBuild + + The managed fallback could not be located. Expected '{0}' and '{1}'. + The managed fallback could not be located. Expected '{0}' and '{1}'. + + File {0} came from another computer and might be blocked to help protect this computer. For more information, including how to unblock, see https://aka.ms/motw Le fichier {0} provient d'un autre ordinateur et a éventuellement été bloqué pour protéger cet ordinateur. Pour plus d'informations, notamment sur le déblocage, consultez https://aka.ms/motw @@ -713,6 +718,11 @@ setx PATH "%PATH%;{0}" Activez la sortie des diagnostics. + + warning: could not determine the SDK directory - no sdk_dir was provided and dotnet-aot could not locate its own module; SDK-relative resolution may be incorrect. + warning: could not determine the SDK directory - no sdk_dir was provided and dotnet-aot could not locate its own module; SDK-relative resolution may be incorrect. + {Locked="sdk_dir"}{Locked="dotnet-aot"} + The '--self-contained' and '--no-self-contained' options cannot be used together. Les options '--self-contained' et '--no-self-contained' ne peuvent pas être utilisées ensemble. diff --git a/src/Cli/dotnet/xlf/CliStrings.it.xlf b/src/Cli/dotnet/xlf/CliStrings.it.xlf index 9813742ec0c6..5f0ea4a60f13 100644 --- a/src/Cli/dotnet/xlf/CliStrings.it.xlf +++ b/src/Cli/dotnet/xlf/CliStrings.it.xlf @@ -465,6 +465,11 @@ setx PATH "%PATH%;{0}" Server di MSBuild + + The managed fallback could not be located. Expected '{0}' and '{1}'. + The managed fallback could not be located. Expected '{0}' and '{1}'. + + File {0} came from another computer and might be blocked to help protect this computer. For more information, including how to unblock, see https://aka.ms/motw Il file {0} proviene da un altro computer e potrebbe essere stato bloccato per proteggere questo computer. Per altre informazioni, incluse le istruzioni su come sbloccare il file, vedere https://aka.ms/motw @@ -713,6 +718,11 @@ setx PATH "%PATH%;{0}" Abilita l'output di diagnostica. + + warning: could not determine the SDK directory - no sdk_dir was provided and dotnet-aot could not locate its own module; SDK-relative resolution may be incorrect. + warning: could not determine the SDK directory - no sdk_dir was provided and dotnet-aot could not locate its own module; SDK-relative resolution may be incorrect. + {Locked="sdk_dir"}{Locked="dotnet-aot"} + The '--self-contained' and '--no-self-contained' options cannot be used together. Non è possibile usare contemporaneamente le opzioni '--self-contained' e ‘--no-self-contained'. diff --git a/src/Cli/dotnet/xlf/CliStrings.ja.xlf b/src/Cli/dotnet/xlf/CliStrings.ja.xlf index 1c522cd4d096..b3480008562a 100644 --- a/src/Cli/dotnet/xlf/CliStrings.ja.xlf +++ b/src/Cli/dotnet/xlf/CliStrings.ja.xlf @@ -465,6 +465,11 @@ setx PATH "%PATH%;{0}" MSBuild サーバー + + The managed fallback could not be located. Expected '{0}' and '{1}'. + The managed fallback could not be located. Expected '{0}' and '{1}'. + + File {0} came from another computer and might be blocked to help protect this computer. For more information, including how to unblock, see https://aka.ms/motw ファイル {0} は別のコンピューターからのもので、このコンピューターを保護するためにブロックされている可能性があります。ブロックを解除する方法を含む詳細については、https://aka.ms/motw を参照してください @@ -713,6 +718,11 @@ setx PATH "%PATH%;{0}" 診断出力を有効にします。 + + warning: could not determine the SDK directory - no sdk_dir was provided and dotnet-aot could not locate its own module; SDK-relative resolution may be incorrect. + warning: could not determine the SDK directory - no sdk_dir was provided and dotnet-aot could not locate its own module; SDK-relative resolution may be incorrect. + {Locked="sdk_dir"}{Locked="dotnet-aot"} + The '--self-contained' and '--no-self-contained' options cannot be used together. '--self-contained' と '--no-self-contained' オプションは同時に使用できません。 diff --git a/src/Cli/dotnet/xlf/CliStrings.ko.xlf b/src/Cli/dotnet/xlf/CliStrings.ko.xlf index bb4f829bad28..36d652512f88 100644 --- a/src/Cli/dotnet/xlf/CliStrings.ko.xlf +++ b/src/Cli/dotnet/xlf/CliStrings.ko.xlf @@ -465,6 +465,11 @@ setx PATH "%PATH%;{0}" MSBuild 서버 + + The managed fallback could not be located. Expected '{0}' and '{1}'. + The managed fallback could not be located. Expected '{0}' and '{1}'. + + File {0} came from another computer and might be blocked to help protect this computer. For more information, including how to unblock, see https://aka.ms/motw {0} 파일은 다른 컴퓨터에서 제공되었으며 이 컴퓨터를 보호하기 위해 차단되었을 수 있습니다. 차단 해제 방법을 비롯한 자세한 내용은 https://aka.ms/motw를 참조하세요. @@ -713,6 +718,11 @@ setx PATH "%PATH%;{0}" 진단 출력을 사용합니다. + + warning: could not determine the SDK directory - no sdk_dir was provided and dotnet-aot could not locate its own module; SDK-relative resolution may be incorrect. + warning: could not determine the SDK directory - no sdk_dir was provided and dotnet-aot could not locate its own module; SDK-relative resolution may be incorrect. + {Locked="sdk_dir"}{Locked="dotnet-aot"} + The '--self-contained' and '--no-self-contained' options cannot be used together. '--self-contained' 및 '--no-self-contained' 옵션은 함께 사용할 수 없습니다. diff --git a/src/Cli/dotnet/xlf/CliStrings.pl.xlf b/src/Cli/dotnet/xlf/CliStrings.pl.xlf index 514029a7af5c..dbb2a3659d94 100644 --- a/src/Cli/dotnet/xlf/CliStrings.pl.xlf +++ b/src/Cli/dotnet/xlf/CliStrings.pl.xlf @@ -465,6 +465,11 @@ setx PATH "%PATH%;{0}" Serwer MSBuild + + The managed fallback could not be located. Expected '{0}' and '{1}'. + The managed fallback could not be located. Expected '{0}' and '{1}'. + + File {0} came from another computer and might be blocked to help protect this computer. For more information, including how to unblock, see https://aka.ms/motw Plik {0} pochodzi z innego komputera i może zostać zablokowany, aby pomóc chronić ten komputer. Aby uzyskać więcej informacji, w tym o sposobie odblokowywania, zobacz https://aka.ms/motw @@ -713,6 +718,11 @@ setx PATH "%PATH%;{0}" Włącz diagnostyczne dane wyjściowe. + + warning: could not determine the SDK directory - no sdk_dir was provided and dotnet-aot could not locate its own module; SDK-relative resolution may be incorrect. + warning: could not determine the SDK directory - no sdk_dir was provided and dotnet-aot could not locate its own module; SDK-relative resolution may be incorrect. + {Locked="sdk_dir"}{Locked="dotnet-aot"} + The '--self-contained' and '--no-self-contained' options cannot be used together. Opcji „--self-contained” i „--no-self-contained” nie można używać razem. diff --git a/src/Cli/dotnet/xlf/CliStrings.pt-BR.xlf b/src/Cli/dotnet/xlf/CliStrings.pt-BR.xlf index 8c4c4ede7865..00b1ec0b7806 100644 --- a/src/Cli/dotnet/xlf/CliStrings.pt-BR.xlf +++ b/src/Cli/dotnet/xlf/CliStrings.pt-BR.xlf @@ -465,6 +465,11 @@ setx PATH "%PATH%;{0}" Servidor MSBuild + + The managed fallback could not be located. Expected '{0}' and '{1}'. + The managed fallback could not be located. Expected '{0}' and '{1}'. + + File {0} came from another computer and might be blocked to help protect this computer. For more information, including how to unblock, see https://aka.ms/motw O arquivo {0} veio de outro computador e pode estar bloqueado para ajudar a proteger este computador. Para obter mais informações, incluindo como desbloquear, consulte https://aka.ms/motw @@ -713,6 +718,11 @@ setx PATH "%PATH%;{0}" Habilitar saída de diagnóstico. + + warning: could not determine the SDK directory - no sdk_dir was provided and dotnet-aot could not locate its own module; SDK-relative resolution may be incorrect. + warning: could not determine the SDK directory - no sdk_dir was provided and dotnet-aot could not locate its own module; SDK-relative resolution may be incorrect. + {Locked="sdk_dir"}{Locked="dotnet-aot"} + The '--self-contained' and '--no-self-contained' options cannot be used together. As opções '--self-contained' e '--no-self-contained' não podem ser usadas juntas. diff --git a/src/Cli/dotnet/xlf/CliStrings.ru.xlf b/src/Cli/dotnet/xlf/CliStrings.ru.xlf index bf137cffddae..48dfe2cea370 100644 --- a/src/Cli/dotnet/xlf/CliStrings.ru.xlf +++ b/src/Cli/dotnet/xlf/CliStrings.ru.xlf @@ -465,6 +465,11 @@ setx PATH "%PATH%;{0}" Сервер MSBuild + + The managed fallback could not be located. Expected '{0}' and '{1}'. + The managed fallback could not be located. Expected '{0}' and '{1}'. + + File {0} came from another computer and might be blocked to help protect this computer. For more information, including how to unblock, see https://aka.ms/motw Файл {0} получен с другого компьютера и может быть заблокирован для защиты этого компьютера. Дополнительные сведения, включая процедуру разблокировки, см. на странице https://aka.ms/motw @@ -713,6 +718,11 @@ setx PATH "%PATH%;{0}" Включение выходных данных диагностики. + + warning: could not determine the SDK directory - no sdk_dir was provided and dotnet-aot could not locate its own module; SDK-relative resolution may be incorrect. + warning: could not determine the SDK directory - no sdk_dir was provided and dotnet-aot could not locate its own module; SDK-relative resolution may be incorrect. + {Locked="sdk_dir"}{Locked="dotnet-aot"} + The '--self-contained' and '--no-self-contained' options cannot be used together. Параметры "--self-contained" и "--no-self-contained" нельзя использовать вместе. diff --git a/src/Cli/dotnet/xlf/CliStrings.tr.xlf b/src/Cli/dotnet/xlf/CliStrings.tr.xlf index a973e84a0215..f30239640957 100644 --- a/src/Cli/dotnet/xlf/CliStrings.tr.xlf +++ b/src/Cli/dotnet/xlf/CliStrings.tr.xlf @@ -465,6 +465,11 @@ setx PATH "%PATH%;{0}" MSBuild sunucusu + + The managed fallback could not be located. Expected '{0}' and '{1}'. + The managed fallback could not be located. Expected '{0}' and '{1}'. + + File {0} came from another computer and might be blocked to help protect this computer. For more information, including how to unblock, see https://aka.ms/motw {0} adlı dosya başka bir bilgisayardan geldiğinden bilgisayarı korumaya yardımcı olmak için engellenmiş olabilir. Engellemenin nasıl kaldırılacağı da dahil olmak üzere daha fazla bilgi için bkz. https://aka.ms/motw @@ -713,6 +718,11 @@ setx PATH "%PATH%;{0}" Tanılama çıkışını etkinleştirir. + + warning: could not determine the SDK directory - no sdk_dir was provided and dotnet-aot could not locate its own module; SDK-relative resolution may be incorrect. + warning: could not determine the SDK directory - no sdk_dir was provided and dotnet-aot could not locate its own module; SDK-relative resolution may be incorrect. + {Locked="sdk_dir"}{Locked="dotnet-aot"} + The '--self-contained' and '--no-self-contained' options cannot be used together. '--self-contained' ve '--no-self-contained' seçenekleri birlikte kullanılamaz. diff --git a/src/Cli/dotnet/xlf/CliStrings.zh-Hans.xlf b/src/Cli/dotnet/xlf/CliStrings.zh-Hans.xlf index e78ccb7d6353..1045c6821bec 100644 --- a/src/Cli/dotnet/xlf/CliStrings.zh-Hans.xlf +++ b/src/Cli/dotnet/xlf/CliStrings.zh-Hans.xlf @@ -465,6 +465,11 @@ setx PATH "%PATH%;{0}" MSBuild 服务器 + + The managed fallback could not be located. Expected '{0}' and '{1}'. + The managed fallback could not be located. Expected '{0}' and '{1}'. + + File {0} came from another computer and might be blocked to help protect this computer. For more information, including how to unblock, see https://aka.ms/motw 文件 {0} 来自另一台计算机,而且可能会被阻止以帮助保护此计算机。有关详细信息(包括如何解除阻止),请参阅 https://aka.ms/motw @@ -713,6 +718,11 @@ setx PATH "%PATH%;{0}" 启用诊断输出。 + + warning: could not determine the SDK directory - no sdk_dir was provided and dotnet-aot could not locate its own module; SDK-relative resolution may be incorrect. + warning: could not determine the SDK directory - no sdk_dir was provided and dotnet-aot could not locate its own module; SDK-relative resolution may be incorrect. + {Locked="sdk_dir"}{Locked="dotnet-aot"} + The '--self-contained' and '--no-self-contained' options cannot be used together. "--self-contained"和 "--no-self-contained" 选项不能一起使用。 diff --git a/src/Cli/dotnet/xlf/CliStrings.zh-Hant.xlf b/src/Cli/dotnet/xlf/CliStrings.zh-Hant.xlf index 2fdb357cc58e..e58a3162be29 100644 --- a/src/Cli/dotnet/xlf/CliStrings.zh-Hant.xlf +++ b/src/Cli/dotnet/xlf/CliStrings.zh-Hant.xlf @@ -465,6 +465,11 @@ setx PATH "%PATH%;{0}" MSBuild 伺服器 + + The managed fallback could not be located. Expected '{0}' and '{1}'. + The managed fallback could not be located. Expected '{0}' and '{1}'. + + File {0} came from another computer and might be blocked to help protect this computer. For more information, including how to unblock, see https://aka.ms/motw 檔案 {0} 來自另一部電腦,但此電腦可能已封鎖而加以保護。如需詳細資訊,包括如何解除封鎖,請參閱 https://aka.ms/motw @@ -713,6 +718,11 @@ setx PATH "%PATH%;{0}" 啟用診斷輸出。 + + warning: could not determine the SDK directory - no sdk_dir was provided and dotnet-aot could not locate its own module; SDK-relative resolution may be incorrect. + warning: could not determine the SDK directory - no sdk_dir was provided and dotnet-aot could not locate its own module; SDK-relative resolution may be incorrect. + {Locked="sdk_dir"}{Locked="dotnet-aot"} + The '--self-contained' and '--no-self-contained' options cannot be used together. 不能同時使用 '--self-contained' 和 '--no-self-contained' 選項。 diff --git a/test/Microsoft.DotNet.Cli.Utils.Tests/BufferScopeTests.cs b/test/Microsoft.DotNet.Cli.Utils.Tests/BufferScopeTests.cs new file mode 100644 index 000000000000..665f3b057cf1 --- /dev/null +++ b/test/Microsoft.DotNet.Cli.Utils.Tests/BufferScopeTests.cs @@ -0,0 +1,412 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +// Ported from dotnet/msbuild (src/Framework.UnitTests/BufferScope_Tests.cs) to MSTest + FluentAssertions. + +using FluentAssertions; + +namespace Microsoft.DotNet.Cli.Utils; + +[TestClass] +public class BufferScopeTests +{ + [TestMethod] + public void MinimumLengthConstructor_RentsAtLeastRequestedSize() + { + using BufferScope buffer = new(16); + buffer.Length.Should().BeGreaterThanOrEqualTo(16); + } + + [TestMethod] + public void InitialBufferConstructor_UsesProvidedSpan() + { + Span initial = stackalloc char[8]; + using BufferScope buffer = new(initial); + buffer.Length.Should().Be(8); + } + + [TestMethod] + public void InitialBufferWithMinimum_UsesInitialWhenLargeEnough() + { + Span initial = stackalloc byte[32]; + using BufferScope buffer = new(initial, 16); + buffer.Length.Should().Be(32); + } + + [TestMethod] + public void InitialBufferWithMinimum_RentsWhenInitialTooSmall() + { + Span initial = stackalloc byte[4]; + using BufferScope buffer = new(initial, 128); + buffer.Length.Should().BeGreaterThanOrEqualTo(128); + } + + [TestMethod] + public void Indexer_GetsAndSetsValues() + { + using BufferScope buffer = new(4); + buffer[0] = 10; + buffer[1] = 20; + buffer[2] = 30; + buffer[0].Should().Be(10); + buffer[1].Should().Be(20); + buffer[2].Should().Be(30); + } + + [TestMethod] + public void Slice_ReturnsRequestedRange() + { + using BufferScope buffer = new(10); + buffer[0] = 'a'; + buffer[1] = 'b'; + buffer[2] = 'c'; + buffer[3] = 'd'; + + Span slice = buffer.Slice(1, 2); + slice.Length.Should().Be(2); + slice[0].Should().Be('b'); + slice[1].Should().Be('c'); + } + + [TestMethod] + public void ToString_ReturnsSpanContents() + { + Span initial = stackalloc char[5]; + using BufferScope buffer = new(initial); + buffer[0] = 'h'; + buffer[1] = 'e'; + buffer[2] = 'l'; + buffer[3] = 'l'; + buffer[4] = 'o'; + + buffer.ToString().Should().Be("hello"); + } + + [TestMethod] + public void EnsureCapacity_NoOpWhenAlreadyLargeEnough() + { + using BufferScope buffer = new(64); + int originalLength = buffer.Length; + buffer.EnsureCapacity(32); + buffer.Length.Should().Be(originalLength); + } + + [TestMethod] + public void EnsureCapacity_GrowsWhenNeeded() + { + Span initial = stackalloc byte[4]; + using BufferScope buffer = new(initial); + buffer.Length.Should().Be(4); + + buffer.EnsureCapacity(128); + buffer.Length.Should().BeGreaterThanOrEqualTo(128); + } + + [TestMethod] + public void EnsureCapacity_WithCopy_PreservesExistingContents() + { + Span initial = stackalloc int[4]; + using BufferScope buffer = new(initial); + buffer[0] = 1; + buffer[1] = 2; + buffer[2] = 3; + buffer[3] = 4; + + buffer.EnsureCapacity(64, copy: true); + + buffer[0].Should().Be(1); + buffer[1].Should().Be(2); + buffer[2].Should().Be(3); + buffer[3].Should().Be(4); + } + + [TestMethod] + public void AsSpan_ReturnsUnderlyingSpan() + { + using BufferScope buffer = new(8); + Span span = buffer.AsSpan(); + span.Length.Should().Be(buffer.Length); + } + + [TestMethod] + public void ImplicitSpanConversion_Works() + { + using BufferScope buffer = new(8); + buffer[0] = 42; + Span span = buffer; + span[0].Should().Be(42); + } + + [TestMethod] + public void ImplicitReadOnlySpanConversion_Works() + { + using BufferScope buffer = new(8); + buffer[0] = 42; + ReadOnlySpan span = buffer; + span[0].Should().Be(42); + } + + [TestMethod] + public void GetEnumerator_IteratesOverElements() + { + using BufferScope buffer = new(stackalloc int[3]); + buffer[0] = 1; + buffer[1] = 2; + buffer[2] = 3; + + int sum = 0; + foreach (int value in buffer) + { + sum += value; + } + sum.Should().Be(6); + } + + [TestMethod] + public void MinimumLengthConstructor_HandlesZeroLength() + { + using BufferScope buffer = new(0); + buffer.Length.Should().BeGreaterThanOrEqualTo(0); + } + + [TestMethod] + public void InitialBufferConstructor_HandlesEmptySpan() + { + using BufferScope buffer = new([]); + buffer.Length.Should().Be(0); + } + + [TestMethod] + public void InitialBufferWithMinimum_UsesInitialWhenEqualToMinimum() + { + using BufferScope buffer = new(stackalloc char[10], 10); + buffer.Length.Should().Be(10); + } + + [TestMethod] + public void EnsureCapacity_GrowWithoutCopy_ExpandsBuffer() + { + using BufferScope buffer = new(10); + buffer[0] = 42; + buffer[5] = 100; + + buffer.EnsureCapacity(50, copy: false); + + buffer.Length.Should().BeGreaterThanOrEqualTo(50); + } + + [TestMethod] + public void EnsureCapacity_MultipleGrows_PreservesCopiedData() + { + using BufferScope buffer = new(5); + buffer[0] = 1; + buffer[1] = 2; + + buffer.EnsureCapacity(10, copy: true); + buffer[0].Should().Be(1); + buffer[1].Should().Be(2); + + buffer.EnsureCapacity(50, copy: true); + buffer.Length.Should().BeGreaterThanOrEqualTo(50); + buffer[0].Should().Be(1); + buffer[1].Should().Be(2); + } + + [TestMethod] + public void RangeSlicing_FullRange_ReturnsAllElements() + { + using BufferScope buffer = new(stackalloc char[5]); + buffer[0] = 'A'; + buffer[1] = 'B'; + buffer[2] = 'C'; + buffer[3] = 'D'; + buffer[4] = 'E'; + + Span slice = buffer[..]; + slice.Length.Should().Be(5); + slice[0].Should().Be('A'); + slice[4].Should().Be('E'); + } + + [TestMethod] + public void RangeSlicing_PartialRange_ReturnsExpectedElements() + { + using BufferScope buffer = new(stackalloc int[10]); + for (int i = 0; i < 10; i++) + { + buffer[i] = i; + } + + Span slice = buffer[2..8]; + slice.Length.Should().Be(6); + slice[0].Should().Be(2); + slice[5].Should().Be(7); + } + + [TestMethod] + public void RangeSlicing_EmptyRange_ReturnsEmptySpan() + { + using BufferScope buffer = new(10); + Span slice = buffer[5..5]; + slice.Length.Should().Be(0); + } + + [TestMethod] + public void Slice_CanReturnZeroLengthSpan() + { + using BufferScope buffer = new(10); + Span slice = buffer.Slice(5, 0); + slice.Length.Should().Be(0); + } + + [TestMethod] + public void GetEnumerator_EmptyBuffer_YieldsNoElements() + { + using BufferScope buffer = new([]); + + int count = 0; + foreach (string value in buffer) + { + _ = value; + count++; + } + + count.Should().Be(0); + } + + [TestMethod] + public void ToString_EmptyBuffer_ReturnsEmptyString() + { + using BufferScope buffer = new([]); + buffer.ToString().Should().Be(string.Empty); + } + + [TestMethod] + public void GetPinnableReference_CanModifyUnderlyingMemory() + { + using BufferScope buffer = new(stackalloc byte[10]); + buffer[0] = 255; + buffer[9] = 128; + + ref byte reference = ref buffer.GetPinnableReference(); + reference.Should().Be((byte)255); + reference = 100; + + buffer[0].Should().Be((byte)100); + } + + [TestMethod] + public void GetPinnableReference_EmptyBuffer_DoesNotThrow() + { + using BufferScope buffer = new([]); + buffer.GetPinnableReference(); + buffer.Length.Should().Be(0); + } + + [TestMethod] + public void Fixed_PinsPooledBuffer() + { + using BufferScope buffer = new(64); + buffer[0] = 'Y'; + + unsafe + { + fixed (char* p = buffer) + { + (*p).Should().Be('Y'); + *p = 'Z'; + } + } + + buffer[0].Should().Be('Z'); + } + + [TestMethod] + public void WorksWithReferenceTypes() + { + using BufferScope buffer = new(5); + buffer[0] = "Hello"; + buffer[1] = "World"; + + buffer[0].Should().Be("Hello"); + buffer[1].Should().Be("World"); + } + + [TestMethod] + public void WorksWithValueTypeStructs() + { + using BufferScope buffer = new(3); + DateTime date1 = new(2025, 1, 1); + DateTime date2 = new(2025, 12, 31); + + buffer[0] = date1; + buffer[1] = date2; + + buffer[0].Should().Be(date1); + buffer[1].Should().Be(date2); + } + + [TestMethod] + public void CombinedOperations_GrowSliceAndEnumerate() + { + using BufferScope buffer = new(stackalloc int[5], 10); + + for (int i = 0; i < buffer.Length; i++) + { + buffer[i] = i + 1; + } + + buffer.EnsureCapacity(20, copy: true); + + for (int i = 0; i < 5; i++) + { + buffer[i].Should().Be(i + 1); + } + + Span slice = buffer[1..4]; + slice.Length.Should().Be(3); + slice[0].Should().Be(2); + slice[2].Should().Be(4); + + int sum = 0; + foreach (int value in buffer.AsSpan()[..5]) + { + sum += value; + } + + sum.Should().Be(15); + } + + [TestMethod] + public void Dispose_ClearsSpan() + { + BufferScope buffer = new(16); + buffer.Length.Should().BeGreaterThan(0); + buffer.Dispose(); + buffer.Length.Should().Be(0); + } + + [TestMethod] + public void Dispose_SafeToCallMultipleTimes() + { + BufferScope buffer = new(8); + buffer.Dispose(); + // Calling Dispose a second time must not throw. ref structs cannot be + // captured by a lambda, so invoke directly. + buffer.Dispose(); + } + + [TestMethod] + public void Fixed_PinsUnderlyingMemory() + { + using BufferScope buffer = new(stackalloc char[8]); + buffer[0] = 'x'; + unsafe + { + fixed (char* p = buffer) + { + (*p).Should().Be('x'); + } + } + } +} diff --git a/test/Microsoft.DotNet.Cli.Utils.Tests/Microsoft.DotNet.Cli.Utils.Tests.csproj b/test/Microsoft.DotNet.Cli.Utils.Tests/Microsoft.DotNet.Cli.Utils.Tests.csproj index 5295be631e74..744316f095f5 100644 --- a/test/Microsoft.DotNet.Cli.Utils.Tests/Microsoft.DotNet.Cli.Utils.Tests.csproj +++ b/test/Microsoft.DotNet.Cli.Utils.Tests/Microsoft.DotNet.Cli.Utils.Tests.csproj @@ -3,6 +3,7 @@ $(SdkTargetFramework) MicrosoftAspNetCore + true diff --git a/test/dotnet-aot.Tests/AotIntegrationTests.cs b/test/dotnet-aot.Tests/AotIntegrationTests.cs index 399ed2d3546d..eb39331bee94 100644 --- a/test/dotnet-aot.Tests/AotIntegrationTests.cs +++ b/test/dotnet-aot.Tests/AotIntegrationTests.cs @@ -43,7 +43,8 @@ public class AotIntegrationTests private (int exitCode, string stdout, string stderr) RunDn( string[] args, bool enableAot = true, - int timeoutMs = 30_000) + int timeoutMs = 30_000, + Dictionary? extraEnv = null) { string? dnPath = FindDnPath(); if (dnPath is null) @@ -74,6 +75,14 @@ public class AotIntegrationTests psi.Environment.Remove("DOTNET_CLI_ENABLEAOT"); } + if (extraEnv is not null) + { + foreach (KeyValuePair entry in extraEnv) + { + psi.Environment[entry.Key] = entry.Value; + } + } + _log.WriteLine($"Running: {dnPath} {string.Join(" ", args)}"); _log.WriteLine($" DOTNET_CLI_ENABLEAOT={enableAot}"); @@ -119,6 +128,72 @@ public void AotVersion_WithEnableAot_OutputsVersionAndExitsZero() Assert.IsFalse(string.IsNullOrWhiteSpace(stdout), "Expected version output"); } + [TestMethod] + public void AotInfo_SeparatedLayout_BasePathIsResolvedSdkDirectory() + { + SkipIfDnUnavailable(); + RunSeparatedLayoutBasePathTest(selfLocate: false); + } + + [TestMethod] + public void AotInfo_SeparatedLayout_SelfLocate_BasePathIsResolvedSdkDirectory() + { + SkipIfDnUnavailable(); + RunSeparatedLayoutBasePathTest(selfLocate: true); + } + + // Emulates the deployed muxer layout: dotnet-aot lives in a directory other than dn's own, so + // AppContext.BaseDirectory is no longer the SDK directory. Verifies that --info's Base Path still + // reports the resolved SDK directory - whether it was passed in as sdk_dir (selfLocate: false) or + // self-located from the loaded module (selfLocate: true). + private void RunSeparatedLayoutBasePathTest(bool selfLocate) + { + string dnPath = FindDnPath()!; + string sdkLayoutDir = Path.GetDirectoryName(dnPath)!; + string aotLib = OperatingSystem.IsWindows() ? "dotnet-aot.dll" + : OperatingSystem.IsMacOS() ? "dotnet-aot.dylib" + : "dotnet-aot.so"; + string aotSource = Path.Combine(sdkLayoutDir, aotLib); + if (!File.Exists(aotSource)) + { + Assert.Inconclusive($"{aotLib} not found next to dn; build with NativeAOT to enable this test."); + } + + string sdkSubDir = Path.Combine(Path.GetTempPath(), "aot-sep-" + Guid.NewGuid().ToString("N")); + Directory.CreateDirectory(sdkSubDir); + try + { + File.Copy(aotSource, Path.Combine(sdkSubDir, aotLib)); + + var env = new Dictionary { ["DOTNET_AOT_SDK_DIR"] = sdkSubDir }; + if (selfLocate) + { + env["DOTNET_AOT_BLANK_SDKDIR"] = "1"; + } + + var (exitCode, stdout, _) = RunDn(["--info"], enableAot: true, extraEnv: env); + + Assert.AreEqual(0, exitCode); + + bool basePathReferencesSdkDir = false; + foreach (string line in stdout.Split('\n')) + { + if (line.Contains("Base Path:") && line.Contains(sdkSubDir)) + { + basePathReferencesSdkDir = true; + break; + } + } + + Assert.IsTrue(basePathReferencesSdkDir, + $"--info Base Path did not reference the resolved SDK directory '{sdkSubDir}'. Output:\n{stdout}"); + } + finally + { + Directory.Delete(sdkSubDir, recursive: true); + } + } + [TestMethod] public void AotInfo_WithEnableAot_OutputsInfoAndExitsZero() { diff --git a/test/dotnet-aot.Tests/NativeEntryPointTests.cs b/test/dotnet-aot.Tests/NativeEntryPointTests.cs index 100f8c17be25..69edda39255a 100644 --- a/test/dotnet-aot.Tests/NativeEntryPointTests.cs +++ b/test/dotnet-aot.Tests/NativeEntryPointTests.cs @@ -29,6 +29,7 @@ private static void WithEnvRestore(Action action) string? originalTraceParent = Environment.GetEnvironmentVariable(Activities.TRACEPARENT); string? originalTraceState = Environment.GetEnvironmentVariable(Activities.TRACESTATE); string? originalTelemetryOptout = Environment.GetEnvironmentVariable("DOTNET_CLI_TELEMETRY_OPTOUT"); + string? originalSdkRoot = Environment.GetEnvironmentVariable("DOTNET_SDK_ROOT"); try { action(); @@ -40,6 +41,7 @@ private static void WithEnvRestore(Action action) Environment.SetEnvironmentVariable(Activities.TRACEPARENT, originalTraceParent); Environment.SetEnvironmentVariable(Activities.TRACESTATE, originalTraceState); Environment.SetEnvironmentVariable("DOTNET_CLI_TELEMETRY_OPTOUT", originalTelemetryOptout); + Environment.SetEnvironmentVariable("DOTNET_SDK_ROOT", originalSdkRoot); } } @@ -109,6 +111,50 @@ public void ExecuteCore_AotEnabled_InfoCommand_ReturnsZero() }); } + [TestMethod] + public void ExecuteCore_PublishesResolvedSdkDirectory() + { + WithEnvRestore(() => + { + Environment.SetEnvironmentVariable("DOTNET_CLI_ENABLEAOT", "true"); + string sdk = Path.Combine(Path.GetTempPath(), "aot-sdkroot-" + Guid.NewGuid().ToString("N")); + + NativeEntryPoint.ExecuteCore( + hostPath: "test-host", + dotnetRoot: "test-root", + sdkDir: sdk, + hostfxrPath: "", + args: ["--version"]); + + // The host-provided sdk_dir is authoritative and is published for compiled-in assemblies + // via the DOTNET_SDK_ROOT environment variable, the NativeEntryPoint.SdkDirectory property, + // and the shared SdkPaths reader (read uncached; SdkDirectory caches process-wide). + Assert.AreEqual(sdk, Environment.GetEnvironmentVariable("DOTNET_SDK_ROOT")); + Assert.AreEqual(sdk, NativeEntryPoint.SdkDirectory); + Assert.AreEqual(sdk, SdkPaths.ResolveSdkDirectory()); + }); + } + + [TestMethod] + public void SdkPaths_PrefersDotnetSdkRootEnvVar_ThenFallsBackToAResolvedDirectory() + { + WithEnvRestore(() => + { + string sdk = Path.Combine(Path.GetTempPath(), "sdkpaths-" + Guid.NewGuid().ToString("N")); + Environment.SetEnvironmentVariable("DOTNET_SDK_ROOT", sdk); + // SdkDirectory caches its result process-wide, so exercise the heuristic through the uncached + // resolver to observe both the environment-variable and fallback branches in one test. + Assert.AreEqual(sdk, SdkPaths.ResolveSdkDirectory()); + + // With the environment variable unset, the heuristic falls back to the SDK assembly directory + // (the test output directory under the JIT) or AppContext.BaseDirectory - either way a real dir. + Environment.SetEnvironmentVariable("DOTNET_SDK_ROOT", null); + string fallback = SdkPaths.ResolveSdkDirectory(); + Assert.IsFalse(string.IsNullOrEmpty(fallback), "Fallback SDK directory should not be empty."); + Assert.IsTrue(Directory.Exists(fallback), $"Fallback SDK directory '{fallback}' should exist."); + }); + } + [TestMethod] public void ExecuteCore_AotEnabled_UnrecognizedCommand_FallsBack() { diff --git a/test/dotnet-aot.Tests/dotnet-aot.Tests.csproj b/test/dotnet-aot.Tests/dotnet-aot.Tests.csproj index 95d4ec60c982..135f081272b9 100644 --- a/test/dotnet-aot.Tests/dotnet-aot.Tests.csproj +++ b/test/dotnet-aot.Tests/dotnet-aot.Tests.csproj @@ -35,6 +35,7 @@ + From eac22f7f8f4dd5359db8574c8908f4e422c75d1c Mon Sep 17 00:00:00 2001 From: Jeremy Kuhne Date: Wed, 1 Jul 2026 14:45:49 -0700 Subject: [PATCH 2/5] Address PR review: cross-platform self-locate and managed --info 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. --- src/Cli/dn/run-dn.ps1 | 7 +++-- src/Cli/dotnet-aot/SdkRootLocator.cs | 43 ++++++++++++++++++++++++--- src/Cli/dotnet/ParserOptionActions.cs | 7 +++++ 3 files changed, 50 insertions(+), 7 deletions(-) diff --git a/src/Cli/dn/run-dn.ps1 b/src/Cli/dn/run-dn.ps1 index 357c9ee0801d..d7da42f5eb33 100644 --- a/src/Cli/dn/run-dn.ps1 +++ b/src/Cli/dn/run-dn.ps1 @@ -73,6 +73,7 @@ $isWin = $IsWindows -or ($env:OS -eq "Windows_NT") $exeSuffix = if ($isWin) { ".exe" } else { "" } $dotnet = Join-Path $repoRoot ".dotnet" "dotnet$exeSuffix" $dnExeName = "dn$exeSuffix" +$aotLibName = if ($isWin) { "dotnet-aot.dll" } elseif ($IsMacOS) { "dotnet-aot.dylib" } else { "dotnet-aot.so" } if (-not $Rid) { $arch = [System.Runtime.InteropServices.RuntimeInformation]::OSArchitecture.ToString().ToLowerInvariant() @@ -107,19 +108,19 @@ if (-not $NoBuild) { if ($LASTEXITCODE -ne 0) { throw "managed dotnet build failed." } $dnPublishDir = Resolve-PublishPath "artifacts/bin/dn/$Configuration/*/$Rid/publish" - $aotDll = Resolve-PublishPath "artifacts/bin/dotnet-aot/$Configuration/*/$Rid/publish/dotnet-aot.dll" + $aotDll = Resolve-PublishPath "artifacts/bin/dotnet-aot/$Configuration/*/$Rid/publish/$aotLibName" $managedDir = (Get-ChildItem -Directory (Join-Path $repoRoot "artifacts/bin/dotnet/$Configuration") | Sort-Object LastWriteTime -Descending | Select-Object -First 1).FullName if (-not $dnPublishDir) { throw "Could not locate the dn publish directory after build." } - if (-not $aotDll) { throw "Could not locate dotnet-aot.dll after publish." } + if (-not $aotDll) { throw "Could not locate $aotLibName after publish." } $sdkTargetDir = $dnPublishDir if ($Layout -eq "Separated") { $sdkTargetDir = Join-Path $dnPublishDir "sdk/11.0.100" New-Item -ItemType Directory -Force -Path $sdkTargetDir | Out-Null # dotnet-aot must live only in the versioned SDK subfolder, not next to dn. - Remove-Item (Join-Path $dnPublishDir "dotnet-aot.dll") -Force -ErrorAction SilentlyContinue + Remove-Item (Join-Path $dnPublishDir $aotLibName) -Force -ErrorAction SilentlyContinue } Write-Host "Assembling layout into $sdkTargetDir ..." -ForegroundColor Cyan diff --git a/src/Cli/dotnet-aot/SdkRootLocator.cs b/src/Cli/dotnet-aot/SdkRootLocator.cs index e566a272995c..bdc6db5c40ed 100644 --- a/src/Cli/dotnet-aot/SdkRootLocator.cs +++ b/src/Cli/dotnet-aot/SdkRootLocator.cs @@ -131,9 +131,9 @@ private static void SelfAnchor() { } private static string? GetSelfModulePathPosix() { - // Best-effort on Unix: dladdr resolves the shared object containing the given address. - // libdl.so.2 is the glibc runtime soname; on platforms where it is unavailable the catch in - // TrySelfLocateSdkDirectory falls back to the host-provided sdk_dir. + // Best-effort on Unix: dladdr resolves the shared object containing the given address. It is + // mapped to the platform C runtime (libSystem/libdl/libc) by ResolveDlImport; if none load, the + // catch in TrySelfLocateSdkDirectory falls back to the host-provided sdk_dir. nint address = (nint)(delegate* unmanaged)&SelfAnchor; if (dladdr(address, out DlInfo info) == 0 || info.dli_fname == 0) { @@ -154,7 +154,42 @@ private static bool PathsEqual(string left, string right) return string.Equals(Normalize(left), Normalize(right), comparison); } - [LibraryImport("libdl.so.2", EntryPoint = "dladdr")] + // dladdr does not live in the same library across Unix flavors - libSystem on macOS, libdl on + // glibc, libc on musl and glibc 2.34+ - so it is imported under a sentinel name that a + // DllImportResolver (registered in the static constructor) maps to the first library that loads. + private const string DlLibrary = "dotnet-aot-dl"; + + static SdkRootLocator() + { + if (!OperatingSystem.IsWindows()) + { + NativeLibrary.SetDllImportResolver(typeof(SdkRootLocator).Assembly, ResolveDlImport); + } + } + + private static nint ResolveDlImport(string libraryName, System.Reflection.Assembly assembly, DllImportSearchPath? searchPath) + { + if (libraryName != DlLibrary) + { + return 0; + } + + string[] candidates = OperatingSystem.IsMacOS() + ? ["libSystem.dylib"] + : ["libdl.so.2", "libc.so.6", "libc.so"]; + + foreach (string candidate in candidates) + { + if (NativeLibrary.TryLoad(candidate, assembly, searchPath, out nint handle)) + { + return handle; + } + } + + return 0; + } + + [LibraryImport(DlLibrary, EntryPoint = "dladdr")] private static partial int dladdr(nint addr, out DlInfo info); [StructLayout(LayoutKind.Sequential)] diff --git a/src/Cli/dotnet/ParserOptionActions.cs b/src/Cli/dotnet/ParserOptionActions.cs index 773fd150f217..7da4f8fe400c 100644 --- a/src/Cli/dotnet/ParserOptionActions.cs +++ b/src/Cli/dotnet/ParserOptionActions.cs @@ -162,7 +162,14 @@ public override int Invoke(ParseResult parseResult) // GetDisplayRid consults the shared framework's deps file, which isn't available in AOT. Reporter.Output.WriteLine($" RID: {RuntimeInformation.RuntimeIdentifier}"); #endif +#if CLI_AOT + // In the AOT bubble AppContext.BaseDirectory is the muxer/install root, so report the resolved + // versioned SDK directory instead. The managed CLI keeps AppContext.BaseDirectory - it is correct + // there and preserves the existing output (including its trailing directory separator). Reporter.Output.WriteLine($" Base Path: {SdkPaths.SdkDirectory}"); +#else + Reporter.Output.WriteLine($" Base Path: {AppContext.BaseDirectory}"); +#endif Reporter.Output.WriteLine(); Reporter.Output.WriteLine($"{LocalizableStrings.DotnetWorkloadInfoLabel}"); new WorkloadInfoHelper(isInteractive: false).ShowWorkloadsInfo(showVersion: false); From a83b6107af6866f93629c8b6137f46f28aef6ec0 Mon Sep 17 00:00:00 2001 From: Jeremy Kuhne Date: Thu, 2 Jul 2026 12:03:12 -0700 Subject: [PATCH 3/5] Cover upper-bound index in EnsureCapacity_MultipleGrows test 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. --- test/Microsoft.DotNet.Cli.Utils.Tests/BufferScopeTests.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/test/Microsoft.DotNet.Cli.Utils.Tests/BufferScopeTests.cs b/test/Microsoft.DotNet.Cli.Utils.Tests/BufferScopeTests.cs index 665f3b057cf1..13b8fa085ca6 100644 --- a/test/Microsoft.DotNet.Cli.Utils.Tests/BufferScopeTests.cs +++ b/test/Microsoft.DotNet.Cli.Utils.Tests/BufferScopeTests.cs @@ -206,10 +206,15 @@ public void EnsureCapacity_MultipleGrows_PreservesCopiedData() buffer[0].Should().Be(1); buffer[1].Should().Be(2); + // Write to the top of the grown buffer so the second grow's copy is verified at + // both the lower and upper bounds of the used range, not just the first elements. + buffer[9] = 10; + buffer.EnsureCapacity(50, copy: true); buffer.Length.Should().BeGreaterThanOrEqualTo(50); buffer[0].Should().Be(1); buffer[1].Should().Be(2); + buffer[9].Should().Be(10); } [TestMethod] From 661863ebb0368c07dddc578812e842b3e57bfc61 Mon Sep 17 00:00:00 2001 From: Jeremy Kuhne Date: Thu, 2 Jul 2026 14:35:56 -0700 Subject: [PATCH 4/5] Publish the SDK root as the Microsoft.DotNet.Sdk.Root AppContext value 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. --- .../skills/add-dotnet-aot-command/SKILL.md | 13 ++-- .../Microsoft.DotNet.Cli.Utils/SdkPaths.cs | 37 +++++----- src/Cli/dotnet-aot/NativeEntryPoint.cs | 41 +++++++++--- src/Cli/dotnet-aot/SdkRootResolution.md | 17 +++-- src/Cli/dotnet/CliStrings.resx | 3 + .../DotnetToolsCommandResolver.cs | 4 +- src/Cli/dotnet/xlf/CliStrings.cs.xlf | 5 ++ src/Cli/dotnet/xlf/CliStrings.de.xlf | 5 ++ src/Cli/dotnet/xlf/CliStrings.es.xlf | 5 ++ src/Cli/dotnet/xlf/CliStrings.fr.xlf | 5 ++ src/Cli/dotnet/xlf/CliStrings.it.xlf | 5 ++ src/Cli/dotnet/xlf/CliStrings.ja.xlf | 5 ++ src/Cli/dotnet/xlf/CliStrings.ko.xlf | 5 ++ src/Cli/dotnet/xlf/CliStrings.pl.xlf | 5 ++ src/Cli/dotnet/xlf/CliStrings.pt-BR.xlf | 5 ++ src/Cli/dotnet/xlf/CliStrings.ru.xlf | 5 ++ src/Cli/dotnet/xlf/CliStrings.tr.xlf | 5 ++ src/Cli/dotnet/xlf/CliStrings.zh-Hans.xlf | 5 ++ src/Cli/dotnet/xlf/CliStrings.zh-Hant.xlf | 5 ++ .../dotnet-aot.Tests/NativeEntryPointTests.cs | 67 ++++++++++++++++--- 20 files changed, 194 insertions(+), 53 deletions(-) diff --git a/.github/skills/add-dotnet-aot-command/SKILL.md b/.github/skills/add-dotnet-aot-command/SKILL.md index e8b74933744f..407e33542267 100644 --- a/.github/skills/add-dotnet-aot-command/SKILL.md +++ b/.github/skills/add-dotnet-aot-command/SKILL.md @@ -65,17 +65,18 @@ but inside that process the BCL "where am I" APIs do **not** point there: So deriving an SDK-relative path (`MSBuild.dll`, `Sdks/`, `DotnetTools/`, targets) from `AppContext.BaseDirectory` or a dll path is **wrong** in the AOT bubble. Instead: -- **In-repo:** read `SdkPaths.SdkDirectory` (in `Microsoft.DotNet.Cli.Utils`), which resolves - `DOTNET_SDK_ROOT` env var -> SDK assembly directory -> `AppContext.BaseDirectory` (once, cached). +- **In-repo:** read `SdkPaths.SdkDirectory` (in `Microsoft.DotNet.Cli.Utils`), which resolves the + `Microsoft.DotNet.Sdk.Root` AppContext value -> SDK assembly directory -> `AppContext.BaseDirectory` + (once, cached). - `NativeEntryPoint.ExecuteCore` resolves the SDK directory once (host `sdk_dir`, else self-locating the - `dotnet-aot` module via `SdkRootLocator`) and **publishes it in `DOTNET_SDK_ROOT`** for the compiled-in - assemblies. + `dotnet-aot` module via `SdkRootLocator`) and **publishes it as the `Microsoft.DotNet.Sdk.Root` + AppContext value** for the compiled-in assemblies. - **Out-of-repo code** (MSBuild tasks, NuGet, runtime - no `Cli.Utils` reference) replicates the contract - inline: read `DOTNET_SDK_ROOT` first, else the existing BCL logic. + inline: read the `Microsoft.DotNet.Sdk.Root` AppContext value first, else the existing BCL logic. ```csharp string sdkDirectory = - Environment.GetEnvironmentVariable("DOTNET_SDK_ROOT") is { Length: > 0 } sdkRoot + AppContext.GetData("Microsoft.DotNet.Sdk.Root") is string sdkRoot && sdkRoot.Length > 0 ? sdkRoot : /* existing logic, e.g. AppContext.BaseDirectory */; ``` diff --git a/src/Cli/Microsoft.DotNet.Cli.Utils/SdkPaths.cs b/src/Cli/Microsoft.DotNet.Cli.Utils/SdkPaths.cs index 9877e5aaf05a..4f8cdc066860 100644 --- a/src/Cli/Microsoft.DotNet.Cli.Utils/SdkPaths.cs +++ b/src/Cli/Microsoft.DotNet.Cli.Utils/SdkPaths.cs @@ -21,44 +21,47 @@ namespace Microsoft.DotNet.Cli.Utils; /// NOT point at the versioned SDK directory: and /// resolve to the install root (the muxer's own /// directory) and Assembly.Location is empty. The AOT entry point therefore publishes the -/// resolved SDK directory in the DOTNET_SDK_ROOT environment variable, which this helper reads -/// first. Code that needs an SDK-relative path (MSBuild, tasks, tools, forwarders) should use this -/// helper rather than probing a dll path. +/// resolved SDK directory as the Microsoft.DotNet.Sdk.Root AppContext value, which this helper +/// reads first. Code that needs an SDK-relative path (MSBuild, tasks, tools, forwarders) should use +/// this helper rather than probing a dll path. /// /// -/// Resolution order (resolved once and cached): the DOTNET_SDK_ROOT environment variable, then -/// the directory of the SDK assembly (empty under single-file / NativeAOT), then +/// Resolution order (resolved once and cached): the Microsoft.DotNet.Sdk.Root AppContext value, +/// then the directory of the SDK assembly (empty under single-file / NativeAOT), then /// . See src/Cli/dotnet-aot/SdkRootResolution.md. /// /// internal static class SdkPaths { /// - /// The environment variable the Native AOT bridge uses to publish the resolved versioned SDK - /// directory for the assemblies compiled into it. + /// The data name the Native AOT bridge uses to publish the resolved + /// versioned SDK directory for the assemblies compiled into it. An AppContext value is process-local + /// (unlike an environment variable it is not inherited by child processes) and can also be supplied + /// through a runtimeconfig.json configProperties entry. /// - public const string EnvironmentVariableName = "DOTNET_SDK_ROOT"; + public const string DataName = "Microsoft.DotNet.Sdk.Root"; private static string? s_sdkDirectory; /// - /// The versioned SDK directory, resolved once and cached. Prefers the DOTNET_SDK_ROOT - /// environment variable (published by the AOT bridge); otherwise the directory of the SDK assembly, - /// else . + /// The versioned SDK directory, resolved once and cached. Prefers the Microsoft.DotNet.Sdk.Root + /// AppContext value (published by the AOT bridge); otherwise the directory of the SDK assembly, else + /// . /// public static string SdkDirectory => s_sdkDirectory ??= ResolveSdkDirectory(); #if NET [UnconditionalSuppressMessage("AOT", "IL3000", Justification = "Assembly.Location is empty under single-file / NativeAOT; the empty result is " + - "handled by falling back to AppContext.BaseDirectory, and the AOT bridge sets DOTNET_SDK_ROOT " + - "(preferred above), so this path is only reached by the JIT-compiled managed CLI where " + - "Assembly.Location is the versioned SDK directory.")] + "handled by falling back to AppContext.BaseDirectory, and the AOT bridge sets the " + + "Microsoft.DotNet.Sdk.Root AppContext value (preferred above), so this path is only reached " + + "by the JIT-compiled managed CLI where Assembly.Location is the versioned SDK directory.")] #endif internal static string ResolveSdkDirectory() { - // The AOT bridge publishes the resolved SDK directory in DOTNET_SDK_ROOT; prefer it. - if (Environment.GetEnvironmentVariable(EnvironmentVariableName) is { Length: > 0 } sdkRoot) + // The AOT bridge publishes the resolved SDK directory as the Microsoft.DotNet.Sdk.Root AppContext + // value; prefer it. + if (AppContext.GetData(DataName) is string sdkRoot && sdkRoot.Length > 0) { return sdkRoot; } @@ -66,7 +69,7 @@ internal static string ResolveSdkDirectory() // The SDK assemblies ship in the versioned SDK directory, so the location of this assembly is that // directory for the JIT-compiled managed CLI. Under a single-file / NativeAOT deployment // Assembly.Location is empty (which is what the IL3000 analyzer flags); fall through to - // AppContext.BaseDirectory in that case - the AOT bridge sets DOTNET_SDK_ROOT (preferred above). + // AppContext.BaseDirectory in that case - the AOT bridge sets the AppContext value (preferred above). string? assemblyDirectory = Path.GetDirectoryName(typeof(SdkPaths).Assembly.Location); return string.IsNullOrEmpty(assemblyDirectory) ? AppContext.BaseDirectory : assemblyDirectory; } diff --git a/src/Cli/dotnet-aot/NativeEntryPoint.cs b/src/Cli/dotnet-aot/NativeEntryPoint.cs index d2f6353d7840..74d7d8f53f3f 100644 --- a/src/Cli/dotnet-aot/NativeEntryPoint.cs +++ b/src/Cli/dotnet-aot/NativeEntryPoint.cs @@ -26,8 +26,9 @@ static unsafe partial class NativeEntryPoint /// /// The versioned SDK directory (the folder containing dotnet.dll, MSBuild.dll, Sdks\, ...), /// resolved from the host-provided sdk_dir or by self-locating the dotnet-aot module. Also - /// published to the DOTNET_SDK_ROOT environment variable so compiled-in assemblies that probe - /// AppContext.BaseDirectory can find the SDK. See src/Cli/dotnet-aot/SdkRootResolution.md. + /// published as the "Microsoft.DotNet.Sdk.Root" AppContext value (SdkPaths.DataName) so + /// compiled-in assemblies that probe AppContext.BaseDirectory can find the SDK. See + /// src/Cli/dotnet-aot/SdkRootResolution.md. /// internal static string? SdkDirectory { get; set; } @@ -69,23 +70,41 @@ internal static int ExecuteCore( string hostPath, string dotnetRoot, string sdkDir, string hostfxrPath, string[] args) { - // Resolve the versioned SDK directory: prefer the host-provided sdk_dir (the muxer resolves it - // to locate dotnet-aot), otherwise self-locate the dotnet-aot module. Under NativeAOT - // AppContext.BaseDirectory is the install root (the muxer's directory), not the SDK directory, - // so publish the resolved value in DOTNET_SDK_ROOT for the compiled-in assemblies (MSBuild, - // NuGet, the command resolvers, ...) that otherwise probe AppContext.BaseDirectory. See + // Publish the versioned SDK directory as the "Microsoft.DotNet.Sdk.Root" AppContext value + // (SdkPaths.DataName) for the assemblies compiled into the AOT host (MSBuild, NuGet, the command + // resolvers, ...) that otherwise probe AppContext.BaseDirectory - which under the NativeAOT muxer + // is the install root, not the versioned SDK directory. Unlike an environment variable an + // AppContext value is process-local and is not inherited by child processes. See // src/Cli/dotnet-aot/SdkRootResolution.md. - string sdkDirectory = SdkRootLocator.Resolve(sdkDir); - SdkDirectory = string.IsNullOrEmpty(sdkDirectory) ? null : sdkDirectory; + // + // Honor a value a caller already provided (e.g. a runtimeconfig configProperties entry): it is + // authoritative, but it must point to a real directory - fail fast rather than handing the + // compiled-in assemblies a bogus SDK root. Otherwise resolve it from the host-provided sdk_dir + // (the muxer already resolved it to locate dotnet-aot), else self-locate the dotnet-aot module. + string? sdkDirectory = AppContext.GetData(SdkPaths.DataName) as string; if (!string.IsNullOrEmpty(sdkDirectory)) { - Environment.SetEnvironmentVariable(SdkPaths.EnvironmentVariableName, sdkDirectory); + if (!Directory.Exists(sdkDirectory)) + { + Console.Error.WriteLine(string.Format(CliStrings.SdkRootDirectoryDoesNotExist, sdkDirectory, SdkPaths.DataName)); + return 1; + } } else { - Console.Error.WriteLine(CliStrings.SdkDirectoryCouldNotBeDetermined); + sdkDirectory = SdkRootLocator.Resolve(sdkDir); + if (!string.IsNullOrEmpty(sdkDirectory)) + { + AppContext.SetData(SdkPaths.DataName, sdkDirectory); + } + else + { + Console.Error.WriteLine(CliStrings.SdkDirectoryCouldNotBeDetermined); + } } + SdkDirectory = string.IsNullOrEmpty(sdkDirectory) ? null : sdkDirectory; + // Telemetry is best-effort and must never prevent the CLI from running. Initializing // it can fail on some layouts (e.g. the NativeAOT muxer cannot resolve the crypto // native library used to hash telemetry properties on macOS - see dotnet/sdk#54544), diff --git a/src/Cli/dotnet-aot/SdkRootResolution.md b/src/Cli/dotnet-aot/SdkRootResolution.md index 2167d26fa39a..ea58b052e014 100644 --- a/src/Cli/dotnet-aot/SdkRootResolution.md +++ b/src/Cli/dotnet-aot/SdkRootResolution.md @@ -53,14 +53,17 @@ the module self-locates. finds the `dotnet-aot` module from its own code address: Windows `GetModuleHandleEx(FROM_ADDRESS | UNCHANGED_REFCOUNT, &export)` + `GetModuleFileName`; Unix `dladdr`. -- **Publish once.** `NativeEntryPoint.ExecuteCore` writes the resolved directory to - the `DOTNET_SDK_ROOT` environment variable so the compiled-in assemblies find it - without threading a parameter through every call. +- **Publish once.** `NativeEntryPoint.ExecuteCore` publishes the resolved directory as + the `Microsoft.DotNet.Sdk.Root` AppContext value so the compiled-in assemblies find + it without threading a parameter through every call. An AppContext value is + process-local - 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 must point to an existing directory or the bridge errors out. - **Read.** In-repo code reads `SdkPaths.SdkDirectory` (in - `Microsoft.DotNet.Cli.Utils`), which resolves `DOTNET_SDK_ROOT` -> the SDK - assembly directory -> `AppContext.BaseDirectory` (once, cached). Out-of-repo code - (MSBuild tasks, NuGet, the runtime) reads `DOTNET_SDK_ROOT` first, else its - existing BCL logic. + `Microsoft.DotNet.Cli.Utils`), which resolves the `Microsoft.DotNet.Sdk.Root` + AppContext value -> the SDK assembly directory -> `AppContext.BaseDirectory` (once, + cached). Out-of-repo code (MSBuild tasks, NuGet, the runtime) reads the + `Microsoft.DotNet.Sdk.Root` AppContext value first, else its existing BCL logic. The managed CLI keeps using `AppContext.BaseDirectory`, where it is correct. diff --git a/src/Cli/dotnet/CliStrings.resx b/src/Cli/dotnet/CliStrings.resx index 7572e0a7827e..38d73c1edac0 100644 --- a/src/Cli/dotnet/CliStrings.resx +++ b/src/Cli/dotnet/CliStrings.resx @@ -660,4 +660,7 @@ setx PATH "%PATH%;{0}" The managed fallback could not be located. Expected '{0}' and '{1}'. + + The SDK root directory '{0}' provided via the '{1}' setting does not exist. + diff --git a/src/Cli/dotnet/CommandFactory/CommandResolution/DotnetToolsCommandResolver.cs b/src/Cli/dotnet/CommandFactory/CommandResolution/DotnetToolsCommandResolver.cs index f135e356d1ca..a361c8058916 100644 --- a/src/Cli/dotnet/CommandFactory/CommandResolution/DotnetToolsCommandResolver.cs +++ b/src/Cli/dotnet/CommandFactory/CommandResolution/DotnetToolsCommandResolver.cs @@ -19,8 +19,8 @@ public static DotnetToolsCommandResolver ForSdkRoot(string sdkRoot) public DotnetToolsCommandResolver(string? dotnetToolPath = null) { // 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. + // so fall back to SdkPaths.SdkDirectory (the Microsoft.DotNet.Sdk.Root AppContext value when set) + // instead. See src/Cli/dotnet-aot/SdkRootResolution.md. _dotnetToolPath = dotnetToolPath ?? Path.Combine(SdkPaths.SdkDirectory, "DotnetTools"); } diff --git a/src/Cli/dotnet/xlf/CliStrings.cs.xlf b/src/Cli/dotnet/xlf/CliStrings.cs.xlf index 33632ffbf172..d46b79acabcd 100644 --- a/src/Cli/dotnet/xlf/CliStrings.cs.xlf +++ b/src/Cli/dotnet/xlf/CliStrings.cs.xlf @@ -723,6 +723,11 @@ setx PATH "%PATH%;{0}" warning: could not determine the SDK directory - no sdk_dir was provided and dotnet-aot could not locate its own module; SDK-relative resolution may be incorrect. {Locked="sdk_dir"}{Locked="dotnet-aot"} + + The SDK root directory '{0}' provided via the '{1}' setting does not exist. + The SDK root directory '{0}' provided via the '{1}' setting does not exist. + + The '--self-contained' and '--no-self-contained' options cannot be used together. Možnosti --self-contained a --no-self-contained nelze použít společně. diff --git a/src/Cli/dotnet/xlf/CliStrings.de.xlf b/src/Cli/dotnet/xlf/CliStrings.de.xlf index 4c510038db20..fd0e45e477f2 100644 --- a/src/Cli/dotnet/xlf/CliStrings.de.xlf +++ b/src/Cli/dotnet/xlf/CliStrings.de.xlf @@ -723,6 +723,11 @@ setx PATH "%PATH%;{0}" warning: could not determine the SDK directory - no sdk_dir was provided and dotnet-aot could not locate its own module; SDK-relative resolution may be incorrect. {Locked="sdk_dir"}{Locked="dotnet-aot"} + + The SDK root directory '{0}' provided via the '{1}' setting does not exist. + The SDK root directory '{0}' provided via the '{1}' setting does not exist. + + The '--self-contained' and '--no-self-contained' options cannot be used together. Die Optionen „--self-contained“ und „--no-self-contained“ können nicht gemeinsam verwendet werden. diff --git a/src/Cli/dotnet/xlf/CliStrings.es.xlf b/src/Cli/dotnet/xlf/CliStrings.es.xlf index 861ce0fec46c..3aa68aef1e02 100644 --- a/src/Cli/dotnet/xlf/CliStrings.es.xlf +++ b/src/Cli/dotnet/xlf/CliStrings.es.xlf @@ -723,6 +723,11 @@ setx PATH "%PATH%;{0}" warning: could not determine the SDK directory - no sdk_dir was provided and dotnet-aot could not locate its own module; SDK-relative resolution may be incorrect. {Locked="sdk_dir"}{Locked="dotnet-aot"} + + The SDK root directory '{0}' provided via the '{1}' setting does not exist. + The SDK root directory '{0}' provided via the '{1}' setting does not exist. + + The '--self-contained' and '--no-self-contained' options cannot be used together. Las opciones '--self-contained' y '--no-self-contained' no se pueden usar juntas. diff --git a/src/Cli/dotnet/xlf/CliStrings.fr.xlf b/src/Cli/dotnet/xlf/CliStrings.fr.xlf index 0c9840c4f60c..ac86c7384b09 100644 --- a/src/Cli/dotnet/xlf/CliStrings.fr.xlf +++ b/src/Cli/dotnet/xlf/CliStrings.fr.xlf @@ -723,6 +723,11 @@ setx PATH "%PATH%;{0}" warning: could not determine the SDK directory - no sdk_dir was provided and dotnet-aot could not locate its own module; SDK-relative resolution may be incorrect. {Locked="sdk_dir"}{Locked="dotnet-aot"} + + The SDK root directory '{0}' provided via the '{1}' setting does not exist. + The SDK root directory '{0}' provided via the '{1}' setting does not exist. + + The '--self-contained' and '--no-self-contained' options cannot be used together. Les options '--self-contained' et '--no-self-contained' ne peuvent pas être utilisées ensemble. diff --git a/src/Cli/dotnet/xlf/CliStrings.it.xlf b/src/Cli/dotnet/xlf/CliStrings.it.xlf index 5f0ea4a60f13..f6317e6fbf12 100644 --- a/src/Cli/dotnet/xlf/CliStrings.it.xlf +++ b/src/Cli/dotnet/xlf/CliStrings.it.xlf @@ -723,6 +723,11 @@ setx PATH "%PATH%;{0}" warning: could not determine the SDK directory - no sdk_dir was provided and dotnet-aot could not locate its own module; SDK-relative resolution may be incorrect. {Locked="sdk_dir"}{Locked="dotnet-aot"} + + The SDK root directory '{0}' provided via the '{1}' setting does not exist. + The SDK root directory '{0}' provided via the '{1}' setting does not exist. + + The '--self-contained' and '--no-self-contained' options cannot be used together. Non è possibile usare contemporaneamente le opzioni '--self-contained' e ‘--no-self-contained'. diff --git a/src/Cli/dotnet/xlf/CliStrings.ja.xlf b/src/Cli/dotnet/xlf/CliStrings.ja.xlf index b3480008562a..7c10275c0e0f 100644 --- a/src/Cli/dotnet/xlf/CliStrings.ja.xlf +++ b/src/Cli/dotnet/xlf/CliStrings.ja.xlf @@ -723,6 +723,11 @@ setx PATH "%PATH%;{0}" warning: could not determine the SDK directory - no sdk_dir was provided and dotnet-aot could not locate its own module; SDK-relative resolution may be incorrect. {Locked="sdk_dir"}{Locked="dotnet-aot"} + + The SDK root directory '{0}' provided via the '{1}' setting does not exist. + The SDK root directory '{0}' provided via the '{1}' setting does not exist. + + The '--self-contained' and '--no-self-contained' options cannot be used together. '--self-contained' と '--no-self-contained' オプションは同時に使用できません。 diff --git a/src/Cli/dotnet/xlf/CliStrings.ko.xlf b/src/Cli/dotnet/xlf/CliStrings.ko.xlf index 36d652512f88..7bb48f73ed15 100644 --- a/src/Cli/dotnet/xlf/CliStrings.ko.xlf +++ b/src/Cli/dotnet/xlf/CliStrings.ko.xlf @@ -723,6 +723,11 @@ setx PATH "%PATH%;{0}" warning: could not determine the SDK directory - no sdk_dir was provided and dotnet-aot could not locate its own module; SDK-relative resolution may be incorrect. {Locked="sdk_dir"}{Locked="dotnet-aot"} + + The SDK root directory '{0}' provided via the '{1}' setting does not exist. + The SDK root directory '{0}' provided via the '{1}' setting does not exist. + + The '--self-contained' and '--no-self-contained' options cannot be used together. '--self-contained' 및 '--no-self-contained' 옵션은 함께 사용할 수 없습니다. diff --git a/src/Cli/dotnet/xlf/CliStrings.pl.xlf b/src/Cli/dotnet/xlf/CliStrings.pl.xlf index dbb2a3659d94..deee04b47fea 100644 --- a/src/Cli/dotnet/xlf/CliStrings.pl.xlf +++ b/src/Cli/dotnet/xlf/CliStrings.pl.xlf @@ -723,6 +723,11 @@ setx PATH "%PATH%;{0}" warning: could not determine the SDK directory - no sdk_dir was provided and dotnet-aot could not locate its own module; SDK-relative resolution may be incorrect. {Locked="sdk_dir"}{Locked="dotnet-aot"} + + The SDK root directory '{0}' provided via the '{1}' setting does not exist. + The SDK root directory '{0}' provided via the '{1}' setting does not exist. + + The '--self-contained' and '--no-self-contained' options cannot be used together. Opcji „--self-contained” i „--no-self-contained” nie można używać razem. diff --git a/src/Cli/dotnet/xlf/CliStrings.pt-BR.xlf b/src/Cli/dotnet/xlf/CliStrings.pt-BR.xlf index 00b1ec0b7806..dcd79aaac80f 100644 --- a/src/Cli/dotnet/xlf/CliStrings.pt-BR.xlf +++ b/src/Cli/dotnet/xlf/CliStrings.pt-BR.xlf @@ -723,6 +723,11 @@ setx PATH "%PATH%;{0}" warning: could not determine the SDK directory - no sdk_dir was provided and dotnet-aot could not locate its own module; SDK-relative resolution may be incorrect. {Locked="sdk_dir"}{Locked="dotnet-aot"} + + The SDK root directory '{0}' provided via the '{1}' setting does not exist. + The SDK root directory '{0}' provided via the '{1}' setting does not exist. + + The '--self-contained' and '--no-self-contained' options cannot be used together. As opções '--self-contained' e '--no-self-contained' não podem ser usadas juntas. diff --git a/src/Cli/dotnet/xlf/CliStrings.ru.xlf b/src/Cli/dotnet/xlf/CliStrings.ru.xlf index 48dfe2cea370..d4c155aedbf6 100644 --- a/src/Cli/dotnet/xlf/CliStrings.ru.xlf +++ b/src/Cli/dotnet/xlf/CliStrings.ru.xlf @@ -723,6 +723,11 @@ setx PATH "%PATH%;{0}" warning: could not determine the SDK directory - no sdk_dir was provided and dotnet-aot could not locate its own module; SDK-relative resolution may be incorrect. {Locked="sdk_dir"}{Locked="dotnet-aot"} + + The SDK root directory '{0}' provided via the '{1}' setting does not exist. + The SDK root directory '{0}' provided via the '{1}' setting does not exist. + + The '--self-contained' and '--no-self-contained' options cannot be used together. Параметры "--self-contained" и "--no-self-contained" нельзя использовать вместе. diff --git a/src/Cli/dotnet/xlf/CliStrings.tr.xlf b/src/Cli/dotnet/xlf/CliStrings.tr.xlf index f30239640957..0d9bd2347e59 100644 --- a/src/Cli/dotnet/xlf/CliStrings.tr.xlf +++ b/src/Cli/dotnet/xlf/CliStrings.tr.xlf @@ -723,6 +723,11 @@ setx PATH "%PATH%;{0}" warning: could not determine the SDK directory - no sdk_dir was provided and dotnet-aot could not locate its own module; SDK-relative resolution may be incorrect. {Locked="sdk_dir"}{Locked="dotnet-aot"} + + The SDK root directory '{0}' provided via the '{1}' setting does not exist. + The SDK root directory '{0}' provided via the '{1}' setting does not exist. + + The '--self-contained' and '--no-self-contained' options cannot be used together. '--self-contained' ve '--no-self-contained' seçenekleri birlikte kullanılamaz. diff --git a/src/Cli/dotnet/xlf/CliStrings.zh-Hans.xlf b/src/Cli/dotnet/xlf/CliStrings.zh-Hans.xlf index 1045c6821bec..8c16f31cd168 100644 --- a/src/Cli/dotnet/xlf/CliStrings.zh-Hans.xlf +++ b/src/Cli/dotnet/xlf/CliStrings.zh-Hans.xlf @@ -723,6 +723,11 @@ setx PATH "%PATH%;{0}" warning: could not determine the SDK directory - no sdk_dir was provided and dotnet-aot could not locate its own module; SDK-relative resolution may be incorrect. {Locked="sdk_dir"}{Locked="dotnet-aot"} + + The SDK root directory '{0}' provided via the '{1}' setting does not exist. + The SDK root directory '{0}' provided via the '{1}' setting does not exist. + + The '--self-contained' and '--no-self-contained' options cannot be used together. "--self-contained"和 "--no-self-contained" 选项不能一起使用。 diff --git a/src/Cli/dotnet/xlf/CliStrings.zh-Hant.xlf b/src/Cli/dotnet/xlf/CliStrings.zh-Hant.xlf index e58a3162be29..e825a6fce956 100644 --- a/src/Cli/dotnet/xlf/CliStrings.zh-Hant.xlf +++ b/src/Cli/dotnet/xlf/CliStrings.zh-Hant.xlf @@ -723,6 +723,11 @@ setx PATH "%PATH%;{0}" warning: could not determine the SDK directory - no sdk_dir was provided and dotnet-aot could not locate its own module; SDK-relative resolution may be incorrect. {Locked="sdk_dir"}{Locked="dotnet-aot"} + + The SDK root directory '{0}' provided via the '{1}' setting does not exist. + The SDK root directory '{0}' provided via the '{1}' setting does not exist. + + The '--self-contained' and '--no-self-contained' options cannot be used together. 不能同時使用 '--self-contained' 和 '--no-self-contained' 選項。 diff --git a/test/dotnet-aot.Tests/NativeEntryPointTests.cs b/test/dotnet-aot.Tests/NativeEntryPointTests.cs index 69edda39255a..e81b98938f0e 100644 --- a/test/dotnet-aot.Tests/NativeEntryPointTests.cs +++ b/test/dotnet-aot.Tests/NativeEntryPointTests.cs @@ -29,7 +29,7 @@ private static void WithEnvRestore(Action action) string? originalTraceParent = Environment.GetEnvironmentVariable(Activities.TRACEPARENT); string? originalTraceState = Environment.GetEnvironmentVariable(Activities.TRACESTATE); string? originalTelemetryOptout = Environment.GetEnvironmentVariable("DOTNET_CLI_TELEMETRY_OPTOUT"); - string? originalSdkRoot = Environment.GetEnvironmentVariable("DOTNET_SDK_ROOT"); + object? originalSdkRoot = AppContext.GetData(SdkPaths.DataName); try { action(); @@ -41,7 +41,7 @@ private static void WithEnvRestore(Action action) Environment.SetEnvironmentVariable(Activities.TRACEPARENT, originalTraceParent); Environment.SetEnvironmentVariable(Activities.TRACESTATE, originalTraceState); Environment.SetEnvironmentVariable("DOTNET_CLI_TELEMETRY_OPTOUT", originalTelemetryOptout); - Environment.SetEnvironmentVariable("DOTNET_SDK_ROOT", originalSdkRoot); + AppContext.SetData(SdkPaths.DataName, originalSdkRoot); } } @@ -127,28 +127,75 @@ public void ExecuteCore_PublishesResolvedSdkDirectory() args: ["--version"]); // The host-provided sdk_dir is authoritative and is published for compiled-in assemblies - // via the DOTNET_SDK_ROOT environment variable, the NativeEntryPoint.SdkDirectory property, - // and the shared SdkPaths reader (read uncached; SdkDirectory caches process-wide). - Assert.AreEqual(sdk, Environment.GetEnvironmentVariable("DOTNET_SDK_ROOT")); + // via the Microsoft.DotNet.Sdk.Root AppContext value, the NativeEntryPoint.SdkDirectory + // property, and the shared SdkPaths reader (read uncached; SdkDirectory caches process-wide). + Assert.AreEqual(sdk, AppContext.GetData(SdkPaths.DataName)); Assert.AreEqual(sdk, NativeEntryPoint.SdkDirectory); Assert.AreEqual(sdk, SdkPaths.ResolveSdkDirectory()); }); } [TestMethod] - public void SdkPaths_PrefersDotnetSdkRootEnvVar_ThenFallsBackToAResolvedDirectory() + public void ExecuteCore_PresetSdkRootMissingDirectory_ErrorsOut() + { + WithEnvRestore(() => + { + Environment.SetEnvironmentVariable("DOTNET_CLI_ENABLEAOT", "true"); + + // A caller-provided SDK root (e.g. via runtimeconfig) that does not exist is a + // misconfiguration - the bridge must fail fast rather than publish a bogus SDK root. + string missing = Path.Combine(Path.GetTempPath(), "aot-sdkroot-missing-" + Guid.NewGuid().ToString("N")); + AppContext.SetData(SdkPaths.DataName, missing); + + int exitCode = NativeEntryPoint.ExecuteCore( + hostPath: "test-host", + dotnetRoot: "test-root", + sdkDir: "", + hostfxrPath: "", + args: ["--version"]); + + Assert.AreNotEqual(0, exitCode); + }); + } + + [TestMethod] + public void ExecuteCore_PresetSdkRootExistingDirectory_IsHonored() + { + WithEnvRestore(() => + { + Environment.SetEnvironmentVariable("DOTNET_CLI_ENABLEAOT", "true"); + + // An existing caller-provided SDK root is authoritative and is not overwritten by the + // host-provided sdk_dir argument. + string preset = Directory.CreateTempSubdirectory("aot-sdkroot-preset-").FullName; + AppContext.SetData(SdkPaths.DataName, preset); + + NativeEntryPoint.ExecuteCore( + hostPath: "test-host", + dotnetRoot: "test-root", + sdkDir: "some-other-dir", + hostfxrPath: "", + args: ["--version"]); + + Assert.AreEqual(preset, AppContext.GetData(SdkPaths.DataName)); + Assert.AreEqual(preset, NativeEntryPoint.SdkDirectory); + }); + } + + [TestMethod] + public void SdkPaths_PrefersSdkRootAppContextValue_ThenFallsBackToAResolvedDirectory() { WithEnvRestore(() => { string sdk = Path.Combine(Path.GetTempPath(), "sdkpaths-" + Guid.NewGuid().ToString("N")); - Environment.SetEnvironmentVariable("DOTNET_SDK_ROOT", sdk); + AppContext.SetData(SdkPaths.DataName, sdk); // SdkDirectory caches its result process-wide, so exercise the heuristic through the uncached - // resolver to observe both the environment-variable and fallback branches in one test. + // resolver to observe both the AppContext-value and fallback branches in one test. Assert.AreEqual(sdk, SdkPaths.ResolveSdkDirectory()); - // With the environment variable unset, the heuristic falls back to the SDK assembly directory + // With the AppContext value unset, the heuristic falls back to the SDK assembly directory // (the test output directory under the JIT) or AppContext.BaseDirectory - either way a real dir. - Environment.SetEnvironmentVariable("DOTNET_SDK_ROOT", null); + AppContext.SetData(SdkPaths.DataName, null); string fallback = SdkPaths.ResolveSdkDirectory(); Assert.IsFalse(string.IsNullOrEmpty(fallback), "Fallback SDK directory should not be empty."); Assert.IsTrue(Directory.Exists(fallback), $"Fallback SDK directory '{fallback}' should exist."); From c9339b40b8acd862a106814ee28f2cdea0fa5f04 Mon Sep 17 00:00:00 2001 From: Jeremy Kuhne Date: Thu, 2 Jul 2026 14:57:10 -0700 Subject: [PATCH 5/5] Clarify the DotnetToolsCommandResolver SDK-root fallback comment 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. --- .../CommandResolution/DotnetToolsCommandResolver.cs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/Cli/dotnet/CommandFactory/CommandResolution/DotnetToolsCommandResolver.cs b/src/Cli/dotnet/CommandFactory/CommandResolution/DotnetToolsCommandResolver.cs index a361c8058916..a6a6cc249d76 100644 --- a/src/Cli/dotnet/CommandFactory/CommandResolution/DotnetToolsCommandResolver.cs +++ b/src/Cli/dotnet/CommandFactory/CommandResolution/DotnetToolsCommandResolver.cs @@ -18,9 +18,10 @@ public static DotnetToolsCommandResolver ForSdkRoot(string sdkRoot) public DotnetToolsCommandResolver(string? dotnetToolPath = null) { - // AppContext.BaseDirectory for the NAOT app is the install root, not the per-SDK version path, - // so fall back to SdkPaths.SdkDirectory (the Microsoft.DotNet.Sdk.Root AppContext value when set) - // instead. See src/Cli/dotnet-aot/SdkRootResolution.md. + // On the AOT pathway the resolver policy already knows the versioned SDK directory and creates + // this resolver via ForSdkRoot(sdkRoot), so dotnetToolPath is supplied and this fallback isn't + // used there. The fallback is the managed-CLI default, where SdkPaths.SdkDirectory resolves to the + // SDK assembly directory / AppContext.BaseDirectory. See src/Cli/dotnet-aot/SdkRootResolution.md. _dotnetToolPath = dotnetToolPath ?? Path.Combine(SdkPaths.SdkDirectory, "DotnetTools"); }