Thread TaskEnvironment through StaticWebAsset path helpers (foundation for dotnet/msbuild#14036) - #54708
Conversation
…dotnet/msbuild#14036) Adds TaskEnvironment-aware overloads on StaticWebAsset for every helper that resolves a relative path against a base directory: Normalize, NormalizeContentRootPath, FromTaskItem, FromV1TaskItem, FromTaskItemGroup, ToAssetDictionary, ApplyDefaults, ResolveFile, ComputeIntegrity, HasContentRoot, MaterializeFrameworkAsset. All parameterless overloads are preserved and delegate to the new ones with TaskEnvironment.Fallback so unmigrated callers compile and behave identically. MT-migrated tasks can now thread their own TaskEnvironment through the call stack and stop leaking the process CWD into ContentRoot / RelatedAsset / FileInfo lookups. Adds 10 unit tests under StaticWebAssetTaskEnvironmentTests covering: NormalizeContentRootPath / Normalize / FromTaskItem / FromV1TaskItem / FromTaskItemGroup / ResolveFile / HasContentRoot with the env overload (decoy-CWD pattern, asserts resolution against TaskEnvironment.ProjectDirectory), the unchanged behavior of the parameterless overloads for back-compat, canonicalization of ".." segments, and pass-through of already-absolute inputs. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
| /// project directory rather than the process current directory. Required for multithreaded | ||
| /// MSBuild execution. | ||
| /// </summary> | ||
| public static StaticWebAsset FromTaskItem(ITaskItem item, TaskEnvironment env, bool validate = false) |
There was a problem hiding this comment.
Here is the entry point if you're looking at the PR
| // the directory separators to use Path.DirectorySeparator | ||
| // * Appends a trailing directory separator at the end. | ||
| public static string NormalizeContentRootPath(string path) | ||
| => Path.GetFullPath(path) + |
There was a problem hiding this comment.
This is the thing the PR is fundamentally trying to fix
| var destPath = Path.Combine(fxDir, Normalize(fileSystemRelativePath)); | ||
| destPath = Path.GetFullPath(destPath); | ||
| // Absolutize against env.ProjectDirectory (MT-safe), then canonicalize (".." resolution etc.). | ||
| destPath = Path.GetFullPath((string)env.GetAbsolutePath(destPath)); |
There was a problem hiding this comment.
Pull request overview
This PR refactors StaticWebAsset path-resolution helpers to accept a TaskEnvironment, so relative paths can be rooted against the MSBuild task’s project directory (rather than the process current working directory). This is foundational for making Static Web Assets tasks safe under MSBuild multithreaded execution (dotnet/msbuild#14036), while retaining back-compat via TaskEnvironment.Fallback.
Changes:
- Add
TaskEnvironment-aware overloads forStaticWebAssethelpers that resolve/normalize filesystem paths, with existing overloads delegating toTaskEnvironment.Fallback. - Update internal path normalization and file resolution (
Normalize*,ResolveFile,MaterializeFrameworkAsset, etc.) to absolutize viaenv.GetAbsolutePathand then canonicalize viaPath.GetFullPath. - Add a new unit test suite validating MT-safe rooting behavior and confirming parameterless overloads still use process CWD.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated no comments.
| File | Description |
|---|---|
src/StaticWebAssetsSdk/Tasks/Data/StaticWebAsset.cs |
Introduces TaskEnvironment overloads and routes path resolution through env.GetAbsolutePath to avoid Environment.CurrentDirectory hazards under MT builds. |
test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/StaticWebAssetTaskEnvironmentTests.cs |
Adds focused tests covering env-based absolutization/canonicalization and back-compat behavior for existing overloads. |
Review of #54708 — TaskEnvironment migration of
|
Adds two tests to GeneratePackageAssetsManifestFileTest: 1. RelatedAsset_Unmapped_ProducesError — symmetric to the existing Endpoints_UnmappedAssetFile_ProducesError. Exercises the previously-uncovered error branch in GeneratePackageAssetsManifestFile that fires when an asset's RelatedAsset cannot be remapped to a package-relative path (referential integrity violation). 2. RoundTrip_GenerateThenRead_RelatedAssetResolvesToConsumerAbsolutePath — end-to-end test exercising the producer/consumer contract: feeds GeneratePackageAssetsManifestFile output into ReadPackageAssetsManifest with a synthetic packageRoot and asserts the consumer's RelatedAsset metadata equals the primary asset's Identity (re-anchored to the consumer's packageRoot, with no producer-side build-time path leakage). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Fixes the latent transitive call-stack hazard in
StaticWebAsset.Normalize()and related helpers (StaticWebAsset.cs:1067and friends) wherePath.GetFullPath(...)andnew FileInfo(...)resolve relative paths againstEnvironment.CurrentDirectoryinstead of the task's project directory. Tracks dotnet/msbuild#14036.This is the foundational refactor that unblocks per-task migrations to fully MT-safe behavior (see PRs #54700-#54707 for the per-task migrations; the gated test in #54707 documents the bug).
What this PR does
Adds
TaskEnvironment-aware overloads for every helper onStaticWebAssetthat resolves a relative path against a base directory:Normalize()Normalize(TaskEnvironment env)NormalizeContentRootPath(string)NormalizeContentRootPath(string, TaskEnvironment)FromTaskItem(ITaskItem, bool)FromTaskItem(ITaskItem, TaskEnvironment, bool)FromV1TaskItem(ITaskItem)FromV1TaskItem(ITaskItem, TaskEnvironment)FromTaskItemGroup(ITaskItem[], bool)FromTaskItemGroup(ITaskItem[], TaskEnvironment, bool)ToAssetDictionary(ITaskItem[], bool)ToAssetDictionary(ITaskItem[], TaskEnvironment, bool)ApplyDefaults()ApplyDefaults(TaskEnvironment)ResolveFile(string, string)/ResolveFile()ResolveFile(string, string, TaskEnvironment)/ResolveFile(TaskEnvironment)ComputeIntegrity(string, string)ComputeIntegrity(string, string, TaskEnvironment)HasContentRoot(string)HasContentRoot(string, TaskEnvironment)MaterializeFrameworkAsset(...)TaskEnvironment envparameterInternally, each
Path.GetFullPath(x)is replaced withPath.GetFullPath((string)env.GetAbsolutePath(x))and eachnew FileInfo(x)withnew FileInfo((string)env.GetAbsolutePath(x)).env.GetAbsolutePathrootsxagainstTaskEnvironment.ProjectDirectory(MT-safe via MSBuild'sAsyncLocalFileUtilities.CurrentThreadWorkingDirectory); the outerPath.GetFullPathpreserves the prior canonicalization (..resolution).Back-compat
Every existing call site keeps compiling and behaves identically. The parameterless overloads are retained and simply delegate to the new ones with
TaskEnvironment.Fallback, which is exactly the previous (multi-process) behavior — relative paths still resolve against the process CWD when noTaskEnvironmentis supplied. TestNormalizeContentRootPath_WithoutEnvOverload_StillUsesProcessCurrentDirectory_ForBackCompatpins this.Compatibility sins audit
[Output]-bearing types touched.ResolveFileexception message still reports the original (un-absolutized)identity/originalItemSpecinputs as supplied by the caller.??swallowing exceptions): no new null-coalescing introduced.ResolveFileadds a smallAbsolutizeForFileInfolocal function that explicitly short-circuits null/empty inputs to preserve the pre-existingFileInfo("")→ArgumentExceptionpath so the secondFileInfocan still be tried.env.GetAbsolutePathinPath.GetFullPathpreserves..resolution — covered byNormalizeContentRootPath_WithTaskEnvironment_PreservesCanonicalization_DotDot.env.GetAbsolutePath("")throwsArgumentException, same asPath.GetFullPath("")/new FileInfo("").ResolveFileshort-circuits null/empty so the secondFileInfois still attempted, preserving pre-existing behavior.Test coverage
10 new unit tests in
test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/StaticWebAssetTaskEnvironmentTests.cs:NormalizeContentRootPath_WithTaskEnvironment_AbsolutizesAgainstProjectDirectory_NotProcessCurrentDirectoryNormalizeContentRootPath_WithoutEnvOverload_StillUsesProcessCurrentDirectory_ForBackCompatNormalize_WithTaskEnvironment_AbsolutizesContentRootAndRelatedAssetAgainstProjectDirectoryFromTaskItem_WithTaskEnvironment_HydratesAssetWithProjectDirectoryAbsolutizedPathsFromV1TaskItem_WithTaskEnvironment_HydratesAssetWithProjectDirectoryAbsolutizedPathsApplyDefaults→ResolveFile)FromTaskItemGroup_WithTaskEnvironment_AbsolutizesAllAssetsResolveFile_WithTaskEnvironment_ResolvesIdentityRelativeToProjectDirectoryFileInfocorrectly rootedHasContentRoot_WithTaskEnvironment_ComparesAgainstProjectDirectoryNormalizedFormNormalizeContentRootPath_WithTaskEnvironment_PreservesCanonicalization_DotDot..resolution preservedNormalize_WithTaskEnvironment_AbsolutePathInputs_ArePreservedAndCanonicalizedAll 10 pass locally (
./.dotnet/dotnet exec ...Microsoft.NET.Sdk.StaticWebAssets.Tests.dll -method "*StaticWebAssetTaskEnvironmentTests*").Follow-ups (separate PRs)
This PR ships only the
StaticWebAssetAPI. Existing call sites still invoke the parameterless overloads and therefore retain the pre-PR behavior. Per-task migrations to threadTaskEnvironmentintoFromTaskItem*(and un-skip the gated demo test in #54707) will follow per task — small, mechanical change once the foundation is in place.Co-authored-by: Copilot 223556219+Copilot@users.noreply.github.com