From 881823ad416bf590239f8c05a5ddd7f31c0151e9 Mon Sep 17 00:00:00 2001 From: Jeremy Kuhne Date: Tue, 18 Aug 2026 11:22:33 -0700 Subject: [PATCH 1/2] Improve Native AOT command migration skill --- .../skills/add-dotnet-aot-command/SKILL.md | 412 +++++++++++++----- src/Cli/dotnet-aot/AotSourceFiles.props | 3 +- 2 files changed, 298 insertions(+), 117 deletions(-) diff --git a/.github/skills/add-dotnet-aot-command/SKILL.md b/.github/skills/add-dotnet-aot-command/SKILL.md index e6779a02e910..b1d97a1d7952 100644 --- a/.github/skills/add-dotnet-aot-command/SKILL.md +++ b/.github/skills/add-dotnet-aot-command/SKILL.md @@ -1,56 +1,65 @@ --- 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. + Add, enable, or review a dotnet CLI command or feature in the Native AOT CLI + (src/Cli/dotnet-aot) and prove its compatibility. USE FOR: migrating a command or + option to AOT, reviewing an AOT migration PR, defining conservative eligibility and + managed fallback, changing AotSourceFiles.props or AotDependencies.props, validating + AOT/managed parity, NativeAOT-publishing tests, checking binary-size impact, or using + the dn harness and separated SDK layout. DO NOT USE FOR: resolving IL trim/AOT + analyzer warnings alone (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. +Use this workflow to author or review a Native AOT CLI migration. The goal is not merely to make a +command compile in the native library. The goal is to preserve the managed CLI contract, fall back +before the AOT path commits unsupported work, and prove the actual native artifact and layout. -> Paths use `$(SdkTargetFramework)` = `net11.0` and `win-x64`; adjust for other TFMs/RIDs. +Before changing or reviewing code, verify the current behavior in the owning source and tests. Then read: -## When to use +- `src/Cli/dotnet-aot/DESIGN.md` for the current host and fallback architecture. +- `src/Cli/dotnet-aot/SdkRootResolution.md` when code resolves SDK-relative content. -- 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`. +Commands below assume PowerShell from the repository root. Adjust the RID for the host. Do not hard-code +the target framework; the projects and harness discover `$(SdkTargetFramework)`. -**Not for:** IL trim/AOT warnings (use **dotnet-aot-compat**); managed `dotnet.Tests` runs (use -**incremental-test**); managed-only changes with no AOT impact. +## Non-negotiable contracts + +1. **One CLI, two execution bubbles.** Reuse shared definitions, implementations, helpers, and + resources. Do not create an AOT-only parser or command copy when the managed owner can be linked. +2. **Eligibility is an allowlist.** Unknown options, ambiguous operands, dynamic parser extensions, + and unsupported combinations fall back to managed execution. +3. **Fallback has a commit point.** Complete every fallback predicate before command output, command + telemetry ownership, file/cache mutation, restore, build, or process launch. Split probing from + execution or buffer output if necessary. +4. **Precedence is behavior.** Preserve ordering among built-ins, external/tool commands, projects, + explicit files, positional files, and shorthand forms. +5. **The native artifact is the test subject.** Managed builds and in-process tests do not replace a + clean Native AOT publish, native test run, assembled `dn` run, or platform run. +6. **Size is part of the change.** Report the native-library delta and largest rooted dependencies for + meaningful closure changes. ## 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. +`dotnet-aot` is a Native AOT shared library (`NativeLib=Shared`) loaded by the host. It exports +`dotnet_execute`; `NativeEntryPoint` either invokes shared CLI code in-process or hosts the existing +managed `dotnet.dll` through hostfxr. `DOTNET_CLI_ENABLEAOT=false` is the explicit opt-out. Unsupported +command shapes use `CommandNotAvailableInAotException` or an earlier entry-point decision to request +managed fallback. -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). +The closure has two owners: -Dispatch: `dotnet-aot/NativeEntryPoint.cs` (`dotnet_execute`) is P/Invoked by the `dn` host (`src/Cli/dn`): +- `AotSourceFiles.props`: linked source files and generated/embedded resources. It is imported by the + product and test projects so they compile the same command surface. +- `AotDependencies.props`: package/project references, framework references, runtime feature switches, + and dependency-owned target imports shared by product and tests. -- `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. -- `run` has an intentionally narrow AOT path: launch-only reuse through explicit `--file`, - positional discovery, or implicit shorthand. `--no-build` can use prior synthetic output; - build-enabled and no-build runs can use unchanged validated replayed/MSBuild `RunProperties`. - Project profiles decorate cached launches, while no-build Executable profiles can bypass cache. - Positional discovery defers when the current directory contains a project; shorthand first - preserves external resolution. Stale, ambiguous, or build-required shapes defer. -- Otherwise / on fall-through: host `{sdkDir}/dotnet.dll` via hostfxr (same source, JIT-compiled). +Keep `dotnet-aot.csproj` focused on native compiler/linker and project configuration. `CLI_AOT` gates +the smallest incompatible portions of shared files; `#if !CLI_AOT` must leave the managed path intact. +`DotnetCsproj` is also defined and can expose an unexpected transitive source closure. Types already available (do **not** re-add their sources): `Microsoft.DotNet.Cli.Utils`, `Microsoft.DotNet.Configurer`, `Microsoft.DotNet.Cli.Definitions`, `Microsoft.DotNet.ProjectTools`, @@ -88,92 +97,263 @@ So deriving an SDK-relative path (`MSBuild.dll`, `Sdks/`, `DotnetTools/`, target ``` 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. +"the SDK directory" to the above contract. Search current call sites rather than maintaining a static +list in this skill; update `SdkRootResolution.md` when the contract or ownership changes. + +## Author workflow + +### 1. Measure the managed contract + +Run the managed CLI before editing and capture a behavior matrix: + +- Successful, help, malformed, missing, and ambiguous forms. +- Every option/operand shape intended for AOT, one unknown option, and nearby managed-only forms. +- Exit code, stdout, stderr, created/changed/deleted files, cache state, restore/build invocations, and + child processes. +- External-command, project, explicit-file, positional-file, and shorthand collisions where relevant. +- First-run, telemetry opt-out, cancellation, SDK selection, and existing-artifact state where relevant. + +Measure behavior; do not infer it from option names or a reading of the implementation. + +### 2. Define eligibility before implementation + +Write a table in the PR notes or tests for every supported and unsupported shape. Name the exact signal +available before output or mutation: + +| Shape | Result | Pre-commit signal | Required evidence | +| --- | --- | --- | --- | +| Supported | Handle in AOT | Complete allowlisted parse plus supported input/service state | Native success and managed parity | +| Known unsupported | Fall back | Explicit option/input/service predicate | Entry-point fallback assertion and real managed result | +| Unknown or ambiguous | Fall back | Not in the allowlist | Negative parser/entry-point test | +| Failure after commit | Return AOT result/error | Eligibility already established | Native failure/output/side-effect parity | + +If the signal requires output-producing or mutating work, introduce a probe/execute split or buffer the +output until AOT commits. Do not use a broad catch to replay an invocation after work begins. + +### 3. Reuse the managed owner + +Trace the definition to the code that directly computes or mutates behavior. Prefer, in order: + +1. Existing referenced assembly/API. +2. Existing shared source and resources. +3. A pure helper extracted without changing its contract. +4. A narrow `CLI_AOT` guard around the incompatible member or action. +5. Explicit managed fallback for dynamic/reflection-heavy or otherwise unsupported behavior. + +Keep fallback local to the owning command action. Do not remove definitions from the command tree or +teach `NativeEntryPoint` command-specific semantics when `CommandNotAvailableInAotException` suffices. + +### 4. Extend the closure in the owning props file + +- Add source and resources to `AotSourceFiles.props` in the existing owner group or a labeled + command-specific group. Reuse common scaffolding; duplicate `Compile`/`EmbeddedResource` items fail. +- Add package/project/framework references, runtime feature switches, and dependency target imports to + `AotDependencies.props`. Normal package versions remain centrally managed. +- Put Windows-only source/dependencies under `Condition="'$(TargetOS)' == 'windows'"`. +- Keep native compiler/linker configuration in `dotnet-aot.csproj`. + +Inspect each dependency for trim/AOT warnings, reflection/dynamic code, serialization contracts, +resources/localization, build assets, static initialization, process-global state, native libraries, +and binary-size contribution. + +### 5. Preserve both preprocessor views + +Condition the smallest incompatible block. Compare the managed and AOT views for lost comments, +changed accessibility, relaxed nullable/analyzer settings, altered resources, and unrelated cleanup. +Run the existing managed tests for every shared implementation changed. + +### 6. Add tests that identify the selected path + +Add the smallest applicable layers: + +1. Parser/definition tests for accepted and rejected syntax. +2. Entry-point tests that observe handled versus managed fallback before a real host transition. +3. Shared command tests for host-independent semantics. +4. Native-published tests for the actual AOT closure. +5. `dn` integration for native loading, hostfxr fallback, output, and process state. +6. Separated-layout tests for SDK-root consumers. +7. Managed/AOT parity tests for output, exit code, side effects, and work performed. + +Make the fixture capable of reaching the asserted branch. Mutate an eligibility predicate or the input +shape and confirm the focused test fails for the intended reason. A green skipped native test is not +native evidence. + +### 7. Update contracts and report evidence + +Update `DESIGN.md`, `SdkRootResolution.md`, resources/help, tests, and this skill when the change alters +a documented invariant. Prefer decision rules over static command inventories. + +The completion report must name supported/unsupported shapes, commit point, shared files, new dependency +roots, warning treatment, exact validation executed, skipped tests, parity results, platform coverage, +and binary-size delta. + +## Closure and implementation gotchas + +- **MSBuild XML comments cannot contain `--`** (`MSB4024`) or end with `-`. +- **`Microsoft.Build` does not flow transitively from `Cli.Utils`** because its reference excludes the + runtime asset. Keep required direct MSBuild packages and target imports in `AotDependencies.props`. +- **`DotnetCsproj` is defined for dotnet-aot.** A newly linked file can expose extra conditional code. + Inspect that closure and narrow the owning conditional; do not copy a helper merely to avoid tracing it. +- **Do not pass `-noRestore` with `-getItem`.** The response file already appends it (`MSB1001`). +- **`dotnet-aot.Tests` uses Microsoft.Testing.Platform.** `dotnet test` is not the runner; invoke the + built executable for a focused managed iteration or use `run-aot-tests.ps1` for native execution. +- **Existing tests may assert exclusions.** Search before enabling a command; an intentional + `DoesNotContain` can need a carefully justified inversion. +- **A Roslyn pragma is not native-publish evidence.** ILC can report the same trim/AOT warning during + publish. Use **dotnet-aot-compat**, keep suppression scope narrow, and add a tracking issue for temporary + dependency warnings. +- **A shared Native AOT library is not a standalone AOT app.** On each affected OS, check interaction + with native libraries and process-global state already loaded by the host. +- **Flat layouts can hide SDK-root defects.** Use `-Layout Separated`; add `-SelfLocate` to exercise the + native-module fallback. + +## Reviewer workflow + +For a review, also follow the repository **code-review** skill's findings-first output rules. Use this +rubric to find AOT-specific defects. + +### 1. Reconstruct both execution paths + +Trace host arguments and SDK root, parser/action selection, every eligibility predicate, first observable +effect, AOT execution/telemetry ownership, and managed host transition. If any fallback predicate follows +stdout/stderr, mutation, restore/build, cache work, or process launch, flag the commit-point violation. + +### 2. Compare with the managed owner + +Look for copied definitions, bodies, validators, resources, path logic, diagnostics, and exception +mapping. Require a concrete reason not to share. Review both sides of every changed `CLI_AOT` block for +lost managed behavior, comments, tests, or analyzer coverage. + +### 3. Attack the allowlist + +Try an unknown option, a managed-only option, wrong operand counts, ambiguous discovery, dynamic help, +existing artifacts, and command/project/file/external collisions. Require clean managed fallback before +AOT output or mutation, and require tests to observe which path ran. + +### 4. Audit the closure and size + +Verify source/resources and dependencies are in their respective props files. Check package build assets, +feature switches, reflection, serializers, direct P/Invoke, platform-native libraries, static state, and +warning suppressions. Request clean-build and native-publish evidence. For size growth, inspect rooted +dependency paths rather than only changed source lines. + +### 5. Validate the tests + +Confirm native artifacts existed and ran, skipped counts are reported, process streams cannot deadlock, +paths are platform-neutral, process state is restored, fixtures reach the branch, and assertions inspect +real values rather than headers. Check pre-existing managed tests when code moved behind conditionals. + +### 6. Challenge claims with the validation matrix + +Compilation does not prove ILC compatibility. Native publish does not prove native tests ran. Flat `dn` +does not prove SDK-root behavior. `-Mode Compare` compares captured output but does not by itself prove +exit-code or side-effect parity. One host RID does not prove other platforms. Require evidence matching +each claim and label unresolved questions as questions rather than defects. + +Common blockers: + +- Denylist eligibility or unknown options handled in AOT. +- Fallback after output, telemetry ownership, or non-idempotent work. +- AOT-only copies of managed behavior or resources. +- Package/project references in `AotSourceFiles.props`. +- Incremental-only build evidence after closure changes. +- Native tests skipped because `dn` or the published executable was absent. +- Flat-layout SDK-root claims. +- Unexplained warning suppression, feature switch, or binary-size increase. +- Open-PR behavior described as current `main` behavior. + +## Validation ladder + +Run the cheapest discriminating check after each edit, then broaden. Use the **targeted-test** skill when +the request explicitly asks to select or run narrow SDK tests. + +### 1. Focused parser/entry-point test + +Build `test/dotnet-aot.Tests/dotnet-aot.Tests.csproj`, then invoke the generated MTP executable with: + +```powershell +dotnet-aot.Tests.exe --filter "FullyQualifiedName~" +``` + +Record whether the test asserts AOT handling, fallback, or semantics. Do not call this a native run. + +### 2. Managed and clean closure builds + +```powershell +.\.dotnet\dotnet.exe build src\Cli\dotnet\dotnet.csproj -c Debug +.\.dotnet\dotnet.exe clean src\Cli\dotnet-aot\dotnet-aot.csproj -c Debug +.\.dotnet\dotnet.exe clean test\dotnet-aot.Tests\dotnet-aot.Tests.csproj -c Debug +.\.dotnet\dotnet.exe build src\Cli\dotnet-aot\dotnet-aot.csproj -c Debug +.\.dotnet\dotnet.exe build test\dotnet-aot.Tests\dotnet-aot.Tests.csproj -c Debug +``` + +The clean builds are mandatory after source/resource/dependency closure changes; stale intermediates have +previously hidden missing includes. + +### 3. Product Native AOT publish ```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 -src\Cli\dn\run-dn.ps1 -Command "run --file C:\tmp\app.cs --no-build --no-launch-profile" -NoBuild -src\Cli\dn\run-dn.ps1 -Command "run C:\tmp\app.cs --no-build --no-launch-profile" -NoBuild -src\Cli\dn\run-dn.ps1 -Command "C:\tmp\app.cs --no-build --no-launch-profile" -NoBuild -src\Cli\dn\run-dn.ps1 -Command "run --file C:\tmp\app.cs --no-build --launch-profile MyProfile" -NoBuild +.\.dotnet\dotnet.exe publish src\Cli\dotnet-aot\dotnet-aot.csproj -r win-x64 -c Debug ``` -- `DOTNET_CLI_ENABLEAOT=true` runs in-process in the platform-specific `dotnet-aot` native library; - `DOTNET_CLI_ENABLEAOT=false` makes `dn` host 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. -- For focused integration tests against an assembled harness, set - `DOTNET_AOT_TEST_DN_PATH` to the full `dn` executable path. -- `Commit` and workloads reflect the `DOTNET_ROOT` layout - both paths read the same root, so they agree. +This is the ILC/linker check. Investigate product warnings separately from known test-only rollups. Run +on each affected OS rather than cross-publishing and assuming native execution. + +### 4. Native-published test suite + +```powershell +.\test\dotnet-aot.Tests\run-aot-tests.ps1 -Trx +``` + +The script publishes the MTP test application with Native AOT, verifies the executable exists, supplies +the SDK/host paths, runs it, and can emit a TRX. Report executed, passed, failed, and skipped counts. + +### 5. Real `dn` integration and parity + +Use `run-dn.ps1`; do not duplicate its publish/copy logic: + +```powershell +.\src\Cli\dn\run-dn.ps1 -Command "" -Mode Compare +.\src\Cli\dn\run-dn.ps1 -Command "" -Mode Aot -NoBuild +``` + +The script publishes `dotnet-aot` and `dn`, builds/copies managed `dotnet.dll`, sets `DOTNET_ROOT`, and +toggles `DOTNET_CLI_ENABLEAOT`. `Compare` writes `artifacts/log/dn-aot.txt` and `dn-managed.txt` and diffs +captured output. Separately compare exit code, files/cache, restore/build count, and child processes when +the command can affect them. Set `DOTNET_AOT_TEST_DN_PATH` for focused integration tests against the +assembled harness. + +### 6. Separated SDK-root layout + +```powershell +.\src\Cli\dn\run-dn.ps1 -Command "" -Mode Compare -Layout Separated +.\src\Cli\dn\run-dn.ps1 -Command "" -Mode Compare -Layout Separated -SelfLocate +``` + +The first exercises the host-provided versioned SDK directory. The second makes `dn` pass an empty +`sdk_dir` so `dotnet-aot` must self-locate its native module. Use this for every new SDK-relative path. + +### 7. Size and platform evidence + +Record the before/after native library byte size and inspect the dependency graph for meaningful changes. +Use the AOT size-analysis workflow when available. Run native publish, native tests, and relevant `dn` +scenarios on each affected supported OS/architecture; cross-OS publish is not an execution result. + +## Completion report + +Report: -The VS Code tasks `publish-and-copy-dn-aot` + `copy-all-deps` do the same build/assemble. +- Supported and deliberately unsupported shapes. +- Eligibility/fallback signal and first commit point. +- Shared source/resources and dependency roots changed. +- Focused, managed, clean-build, native-publish, native-test, `dn`, separated-layout, and platform checks + actually run, including skipped counts. +- Managed/AOT differences in output, exit code, side effects, and work performed. +- Native binary size delta, major contributors, warning/feature-switch treatment, and remaining risk. ## 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. +- **code-review** - findings-first review presentation for PR or local changes. +- **dotnet-aot-compat** - resolve IL trim/AOT warnings surfaced by native publish. +- **incremental-test** - run managed `dotnet.Tests` against the redist SDK layout. +- **targeted-test** - select and run the smallest relevant SDK tests with retained diagnostics. diff --git a/src/Cli/dotnet-aot/AotSourceFiles.props b/src/Cli/dotnet-aot/AotSourceFiles.props index 0da8aa1def61..55d801d9dae5 100644 --- a/src/Cli/dotnet-aot/AotSourceFiles.props +++ b/src/Cli/dotnet-aot/AotSourceFiles.props @@ -13,7 +13,8 @@ group, otherwise the duplicate / items will fail the build once both changes merge. * Each command contributes its own clearly-labeled group below, referencing only - its command-specific sources and dependencies. + its command-specific sources and resources. Add package and project dependencies + to AotDependencies.props so the product and tests consume the same closure. NativeEntryPoint.cs and ManagedHost.cs are added separately by each consumer because ManagedHost.cs requires InternalsVisibleTo access to NativeWrapper and the test From 3a6d51bc5902d0e95bfd7e13cd0f62affcece045 Mon Sep 17 00:00:00 2001 From: Jeremy Kuhne Date: Tue, 18 Aug 2026 12:40:24 -0700 Subject: [PATCH 2/2] Address Native AOT skill review feedback --- .../skills/add-dotnet-aot-command/SKILL.md | 11 +- src/Cli/dn/run-dn.ps1 | 133 +++++++++++------- 2 files changed, 88 insertions(+), 56 deletions(-) diff --git a/.github/skills/add-dotnet-aot-command/SKILL.md b/.github/skills/add-dotnet-aot-command/SKILL.md index b1d97a1d7952..52ad92183708 100644 --- a/.github/skills/add-dotnet-aot-command/SKILL.md +++ b/.github/skills/add-dotnet-aot-command/SKILL.md @@ -200,8 +200,8 @@ and binary-size delta. - **Existing tests may assert exclusions.** Search before enabling a command; an intentional `DoesNotContain` can need a carefully justified inversion. - **A Roslyn pragma is not native-publish evidence.** ILC can report the same trim/AOT warning during - publish. Use **dotnet-aot-compat**, keep suppression scope narrow, and add a tracking issue for temporary - dependency warnings. + publish. Use **dotnet-aot-compat**, keep suppression scope narrow, and reference an existing tracking + issue for temporary dependency warnings. Ask the user before creating a new issue. - **A shared Native AOT library is not a standalone AOT app.** On each affected OS, check interaction with native libraries and process-global state already loaded by the host. - **Flat layouts can hide SDK-root defects.** Use `-Layout Separated`; add `-SelfLocate` to exercise the @@ -269,10 +269,13 @@ the request explicitly asks to select or run narrow SDK tests. ### 1. Focused parser/entry-point test -Build `test/dotnet-aot.Tests/dotnet-aot.Tests.csproj`, then invoke the generated MTP executable with: +Use the targeted-test runner so it builds the project, resolves the evaluated `TargetPath`, and invokes +the Microsoft.Testing.Platform application without assuming its generated executable is on `PATH`: ```powershell -dotnet-aot.Tests.exe --filter "FullyQualifiedName~" +.\.dotnet\dotnet.exe .github\skills\targeted-test\scripts\RunTargetedTests.cs -- ` + --project test\dotnet-aot.Tests\dotnet-aot.Tests.csproj ` + --filter "FullyQualifiedName~" ``` Record whether the test asserts AOT handling, fallback, or semantics. Do not call this a native run. diff --git a/src/Cli/dn/run-dn.ps1 b/src/Cli/dn/run-dn.ps1 index 8d15069397a6..27a8471c7575 100644 --- a/src/Cli/dn/run-dn.ps1 +++ b/src/Cli/dn/run-dn.ps1 @@ -135,67 +135,96 @@ $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 +$environmentVariableNames = @( + "DOTNET_ROOT", + "DOTNET_CLI_TELEMETRY_OPTOUT", + "DOTNET_AOT_SDK_DIR", + "DOTNET_AOT_BLANK_SDKDIR", + "DOTNET_CLI_ENABLEAOT" +) +$previousEnvironment = @{} +foreach ($variableName in $environmentVariableNames) { + $environmentVariable = Get-Item "Env:\$variableName" -ErrorAction SilentlyContinue + $previousEnvironment[$variableName] = [pscustomobject]@{ + Exists = $null -ne $environmentVariable + Value = $environmentVariable.Value + } } -function Invoke-Dn([bool]$enableAot) { - if ($enableAot) { - $env:DOTNET_CLI_ENABLEAOT = "true" +try { + $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 { - $env:DOTNET_CLI_ENABLEAOT = "false" + Remove-Item Env:\DOTNET_AOT_SDK_DIR -ErrorAction SilentlyContinue } - & $dnExe @argList 2>&1 -} - -switch ($Mode) { - "Aot" { - Write-Host "===== AOT (DOTNET_CLI_ENABLEAOT=true) =====" -ForegroundColor Green - Invoke-Dn $true + if ($SelfLocate) { + $env:DOTNET_AOT_BLANK_SDKDIR = "1" + Write-Host "Self-locate: enabled (dn passes empty sdk_dir; dotnet-aot self-locates)" -ForegroundColor Cyan } - "Managed" { - Write-Host "===== Managed (DOTNET_CLI_ENABLEAOT=false) =====" -ForegroundColor Green - Invoke-Dn $false + else { + Remove-Item Env:\DOTNET_AOT_BLANK_SDKDIR -ErrorAction SilentlyContinue } - "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 + + function Invoke-Dn([bool]$enableAot) { + if ($enableAot) { + $env:DOTNET_CLI_ENABLEAOT = "true" } else { - Write-Host "IDENTICAL: AOT and managed output match line-for-line." -ForegroundColor Green + $env:DOTNET_CLI_ENABLEAOT = "false" + } + & $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=false) =====" -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 + } + } + } +} +finally { + foreach ($variableName in $environmentVariableNames) { + $previousValue = $previousEnvironment[$variableName] + if ($previousValue.Exists) { + Set-Item "Env:\$variableName" -Value $previousValue.Value + } + elseif (Test-Path "Env:\$variableName") { + Remove-Item "Env:\$variableName" } } }