Skip to content

Migrate GenAPI and GenFacades tasks to the multithreaded task model - #17539

Draft
ViktorHofer wants to merge 2 commits into
dotnet:mainfrom
ViktorHofer:mt-safe-subset-7
Draft

ViktorHofer wants to merge 2 commits into
dotnet:mainfrom
ViktorHofer:mt-safe-subset-7

Conversation

@ViktorHofer

@ViktorHofer ViktorHofer commented Sep 11, 2026

Copy link
Copy Markdown
Member

Seventh safe subset of #17381: Microsoft.DotNet.GenAPI and Microsoft.DotNet.GenFacades.

One task is annotated with [MSBuildMultiThreadableTask] (ClearAssemblyReferenceVersions); the other three keep IMultiThreadableTask without the attribute, so they continue to route through the TaskHost while their paths still resolve against the project. Routing keys off the attribute alone, so the interface only causes TaskEnvironment to be injected — removing it would revert path resolution to the process current directory while leaving the task exactly as unsafe.

  • GenAPITask hands raw LibPath/Assembly values to HostEnvironment, which expands them with Environment.ExpandEnvironmentVariables and probes with Directory.Exists/File.Exists (Microsoft.Cci.Extensions/HostEnvironment.cs:719-740).
  • GenPartialFacadeSource and NotSupportedAssemblyGenerator derive from RoslynBuildTask, which subscribes an instance handler to the process-wide AssemblyLoadContext.Resolving event, so with differing RoslynAssembliesPath values one instance can service another's resolution.

AbsolutePath is threaded through SourceGenerator, TypeParser and GenPartialFacadeSourceGenerator. Retyping those helper parameters is what actually resolves the transitive MSBuildTask0005 chain out of GenPartialFacadeSource. The first two were already internal; GenPartialFacadeSourceGenerator is now internal too, so none of this is public API. It was never reachable as API anyway — build task projects set IncludeBuildOutput=false and pack under tools/, so a PackageReference exposes no compile-time assets, and the only caller in the dotnet org is the one in-repo call site.

Two issues found auditing the migration itself

  • Canonicalization loss. GenPartialFacadeSourceGenerator deduplicated seeds with Path.GetFullPath, which anchors and canonicalizes — the comment on the line says "Normalizing and Removing Relative Segments". GetAbsolutePath only anchors, so Distinct() no longer collapsed two spellings of the same seed, and the duplicate-name check immediately below would have reported them as "multiple versions of these assemblies". Restored with the GetCanonicalForm() polyfill, which required adding the usual <Compile> link to the GenFacades project. AbsolutePath is a readonly struct implementing IEquatable<AbsolutePath> with a path-aware comparer, so Distinct() is sound on it.
  • Empty item specs. TypeParser.GetSourceTrees explicitly skipped empty source files rather than treating them as an error. [Required] on an ITaskItem[] only validates the collection, not each ItemSpec, and GetAbsolutePath throws on an empty path — so absolutizing every item spec turned a skipped entry into a hard failure. They are now filtered before resolution. (ReferenceAssembly and OutputSourcePath are [Required] scalars, which MSBuild rejects when empty, so they need no guard.)

Validation

  • dotnet build Arcade.slnx -c Release --no-incremental: 0 errors, 0 warnings. Incremental builds skip up-to-date projects, so the analyzer does not re-run and reports a false "0 warnings"; --no-incremental is required for an authoritative count.
  • Positive control: reverting the ClearAssemblyReferenceVersions call site to File.Open(Assembly, ...) produces MSBuildTask0003, confirming the analyzer is live on the newly annotated task and that the clean run is meaningful.
  • Neither assembly has an in-repo test suite.

Annotates ClearAssemblyReferenceVersions with [MSBuildMultiThreadableTask]
and routes path handling in both assemblies through the injected
TaskEnvironment. AbsolutePath is threaded through SourceGenerator and
TypeParser, which is what resolves the transitive MSBuildTask0005 chain out
of GenPartialFacadeSource.

GenAPITask, GenPartialFacadeSource and NotSupportedAssemblyGenerator keep
IMultiThreadableTask without the attribute, so they still route to the
TaskHost while their paths resolve against the project. Each records why at
its declaration: GenAPITask hands raw paths to HostEnvironment, which
expands and probes them with process-wide APIs, and the two RoslynBuildTask
subclasses subscribe an instance handler to the process-wide
AssemblyLoadContext.Resolving event.

Two issues found while auditing the migration itself:

- GenPartialFacadeSourceGenerator deduplicated seeds with Path.GetFullPath,
  which both anchors and canonicalizes. GetAbsolutePath only anchors, so
  Distinct no longer collapsed two spellings of the same seed and the
  duplicate-name check below it would report them as multiple versions of
  one assembly. Restored via the GetCanonicalForm polyfill.
- Empty compile file item specs were skipped by TypeParser.GetSourceTrees
  rather than treated as an error. Absolutizing every item spec turned that
  into a throw, so they are filtered before resolution.
Copilot AI lite review requested due to automatic review settings September 11, 2026 08:15

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot review overview

🟡 Changes recommended

The public API change is breaking, and contract-project-relative paths may resolve against the wrong project directory.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Review tier: Lite
Findings: 1 High severity

New issues introduced by this change (1)
Severity Finding
High severity src/​Microsoft.DotNet.GenFacades/​GenPartialFacadeSourceGenerator.cs — Preserve the public generator API
What changed in this PR

Migrates GenAPI and GenFacades tasks to TaskEnvironment path handling and multithreaded task support.

Changes:

  • Threads AbsolutePath through facade generation.
  • Restores seed canonicalization and filters empty source items.
  • Adds task environment injection and annotates ClearAssemblyReferenceVersions.
  • Review identified a breaking public API change and a contract-project path resolution issue.
File Summary
src/​Microsoft.DotNet.GenFacades/​TypeParser.cs Parses absolute source paths.
src/​Microsoft.DotNet.GenFacades/​SourceGenerator.cs Uses absolute compile and output paths.
src/​Microsoft.DotNet.GenFacades/​NotSupportedAssemblyGenerator.cs Resolves task paths through TaskEnvironment.
src/​Microsoft.DotNet.GenFacades/​Microsoft.DotNet.GenFacades.csproj Includes the path canonicalization helper.
src/​Microsoft.DotNet.GenFacades/​GenPartialFacadeSourceGenerator.cs Canonicalizes and deduplicates seeds.
src/​Microsoft.DotNet.GenFacades/​GenPartialFacadeSource.cs Resolves inputs and outputs and filters empty items.
src/​Microsoft.DotNet.GenFacades/​ClearAssemblyReferenceVersions.cs Enables multithreaded routing and path resolution.
src/​Microsoft.DotNet.GenAPI/​GenAPITask.cs Adds environment-aware path handling.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread src/Microsoft.DotNet.GenFacades/GenPartialFacadeSourceGenerator.cs
akoeplinger
akoeplinger previously approved these changes Sep 11, 2026

@akoeplinger akoeplinger left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

LGTM but I'd probably change the PR title since the tasks aren't actually migrated

@ViktorHofer
ViktorHofer marked this pull request as draft September 11, 2026 09:14
@ViktorHofer

Copy link
Copy Markdown
Member Author

@akoeplinger sorry, this wasn't yet ready for review. I just drafted the PR.

The type is only reachable from GenPartialFacadeSource in the same assembly,
and build task packages ship their assembly under tools/ with
IncludeBuildOutput=false, so a PackageReference exposes no compile-time
assets to bind against. Marking it internal matches SourceGenerator and
TypeParser and keeps the AbsolutePath signature change off the public
surface.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot review overview

🔵 Needs a closer look

Address the two unresolved moderate issues involving whitespace-only paths and empty item specifications.

Review tier: Lite
Findings: None

Issues resolved since last review (1)
Severity Finding
High severity src/​Microsoft.DotNet.GenFacades/​GenPartialFacadeSourceGenerator.cs — Preserve the public generator API View resolved comment
Previously missed findings (1)

In code that hasn't changed since last review

src/Microsoft.DotNet.GenFacades/NotSupportedAssemblyGenerator.cs:78

  • This resolves every item spec before checking it. An empty ITaskItem.ItemSpec is still possible even with [Required] on the array; previously File.Exists("") returned false and the task logged the existing not-found error, whereas GetAbsolutePath("") throws and turns the item into an unhandled task failure. Guard empty specs before this call (preserving the prior error/skip behavior).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants