Skip to content

Commit 6bd759a

Browse files
github-actions[bot]javiercnCopilot
authored
[release/11.0-preview6] [Blazor] Fix WebView blazor.modules.json publish crash via conditional fallback (#67374) (#67401)
* [Blazor] Model WebView blazor.modules.json as a framework asset (#67374) Microsoft.AspNetCore.Components.WebView shipped its fallback blazor.modules.json via a static web asset group (BlazorWebViewModules=fallback) plus manifest-promotion targets. At publish, group filtering runs with SkipDeferred=true, so the fallback was not excluded and GenerateStaticWebAssetEndpointsManifest saw two AssetKind=All assets on _framework/blazor.modules.json, throwing 'Sequence contains more than one element' in MAUI Blazor Hybrid apps that also reference a JS-module-contributing RCL. Model blazor.modules.json as a framework static web asset (like Microsoft.AspNetCore.Components.WebAssembly ships its JS): BasePath '/', assets under wwwroot/_framework/, and StaticWebAssetFrameworkPattern '**/*.js;**/*.modules.json'. The framework pattern is matched against the fingerprinted relative path, so a suffix glob (*.modules.json) is required for the JS module manifest to be classified as a Framework asset. The deferred BlazorWebViewModules group + promotion targets are removed; a minimal StaticWebAssets.Groups.targets keeps JSModuleManifestRelativePath and CompressionEnabled for consumers. Add a test project that cracks the built .nupkg files and asserts the static web assets layout/shape for the WebView, WebAssembly, App.Internal.Assets and Identity.UI packages, plus end-to-end build/publish tests that reference the locally-built WebView package from a generated app (and a JS-module RCL) and validate the produced endpoints. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Fix WebView ProjectReference publish conflict and improve packaging tests The framework-asset modeling of blazor.modules.json/blazor.webview.js broke publish for in-repo projects that reference the WebView project via ProjectReference (the WebView E2E test and the Photino sample): the SDK applies StaticWebAssetFrameworkPattern when computing a referenced project's BUILD static web assets but not its PUBLISH assets, so publish ends up with both the materialized framework asset (SourceType=Discovered, SourceId=consumer) and the original (SourceType=Project, SourceId=WebView) at the same _framework/... target path and fails with 'Conflicting assets with the same target path'. Add a no-op-for-package-consumers workaround in StaticWebAssets.Groups.targets that drops the redundant Project-sourced WebView framework assets at publish (the materialized copies are the ones served), and import the groups targets from the WebView E2E test so it (like the Photino sample) also gets JSModuleManifestRelativePath. Package consumers receive these assets as SourceType=Package, so nothing is removed for them. Test improvements: - Build-behavior tests now create working folders under artifacts/tmp instead of the system temp folder. - Each build/publish captures a binary log under artifacts/log so CI collects it and failures can be diagnosed; the working folder is preserved on failure and removed on success. - Tests log the dotnet invocation, output and binlog path via ITestOutputHelper. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Add ProjectReference (P2P) publish regression test for WebView framework assets Adds Publish_ProjectReferenceToWebViewWithJsModuleRcl_SucceedsWithSingleModulesManifest, which references the WebView source project (not the package) plus a JS-module RCL and runs 'dotnet publish'. This is the exact in-repo scenario that regressed in CI: without the StaticWebAssets.Groups.targets workaround it fails with 'Conflicting assets with the same target path _framework/blazor.modules.json'. The test asserts publish succeeds, a single _framework/blazor.modules.json endpoint is produced, and the app's generated manifest (with the RCL module) supersedes the WebView fallback. ConsumerBuild gains an isolateNuGetFeeds option so P2P builds inherit the repo NuGet.config (needed to build the referenced source project) instead of the isolated package feed used by the PackageReference tests. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Use deferred static web asset group for WebView modules.json (rely on dotnet/sdk#54941) Reverts the framework-asset modeling and the in-package publish workaround. With the SDK fix in dotnet/sdk#54941 (resolved deferred static web asset groups are persisted into the build manifest and re-applied, unscoped, when the manifest is reloaded at publish), the idiomatic deferred-group authoring that the WebView package already uses is correct end-to-end: blazor.modules.json is a Package static web asset in the deferred BlazorWebViewModules group, resolved to drop the fallback when the app contributes its own JS modules and keep it otherwise. No framework-asset hack and no package-local workaround are needed, so the WebView product files are unchanged from main. This PR now contributes the static web assets packaging + build/publish regression tests: - Package-layout tests assert the deferred-group shape for WebView (modules.json = Package in BlazorWebViewModules=fallback; webview.js = Framework) and the framework/group shapes for Components.WebAssembly, App.Internal.Assets and Identity.UI. - Build/publish behavior tests (package consumer and ProjectReference consumer) assert a single _framework/blazor.modules.json endpoint and that the app's manifest supersedes the fallback. The publish assertions are skipped until dotnet/sdk#54941 is in the repo SDK (detected via the pre-fix 'Sequence contains more than one element' crash) so the suite stays green meanwhile. Note: this PR depends on dotnet/sdk#54941 flowing into the repo SDK; until then the in-repo WebView publish path (and the publish tests) require that fix. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Fix WebView blazor.modules.json publish crash via conditional fallback (#67374) The WebView package ships a fallback _framework/blazor.modules.json (empty []) for apps that contribute no JS library modules. Modeling it as a deferred static web asset group required tagging/promoting the consumer's SDK-generated manifest and depended on an SDK fix (dotnet/sdk#54941) to filter the group at publish; without it, publish crashed with "Sequence contains more than one element" (#67374). Replace the group authoring with conditional materialization: the package ships the fallback raw under build/ (not as a flowing static web asset) and materializes it as the consumer's own asset during ResolveStaticWebAssetsInputs ONLY when the app has no JS modules of its own. The decision runs before the build manifest / conflict check, so exactly one asset ever lands on _framework/blazor.modules.json and there is never a conflict at build or publish. No asset groups, no consumer manifest tagging, no SDK dependency. Tests validate the package layout and build/publish behavior (package + P2P consumers, with and without JS modules) by cracking the built .nupkg and running isolated consumer builds under artifacts/ with binlog capture. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Javier Calvarro Nelson <jacalvar@microsoft.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 0c6d0f4 commit 6bd759a

9 files changed

Lines changed: 936 additions & 87 deletions

src/Components/WebView/WebView/src/Microsoft.AspNetCore.Components.WebView.csproj

Lines changed: 9 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,9 @@
1010
<GenerateDocumentationFile>true</GenerateDocumentationFile>
1111
<Nullable>annotations</Nullable>
1212

13+
<!-- blazor.webview.js ships as a normal package static web asset under _framework/.
14+
blazor.modules.json is handled specially (see below + StaticWebAssets.Groups.targets). -->
1315
<StaticWebAssetBasePath>/_framework</StaticWebAssetBasePath>
14-
<!-- SWA Framework Assets: mark all JS files as Framework type during pack -->
15-
<StaticWebAssetFrameworkPattern>**/*.js</StaticWebAssetFrameworkPattern>
1616
</PropertyGroup>
1717

1818
<ItemGroup>
@@ -63,22 +63,15 @@
6363
<Content Include="$(_BlazorWebViewJSContentRoot)\blazor.webview.js" Link="wwwroot\blazor.webview.js" ContentRoot="$(_BlazorWebViewJSContentRoot)\" />
6464
</ItemGroup>
6565

66-
<!-- blazor.modules.json is a fallback for WebView apps that don't have JS library modules.
67-
It is tagged with the BlazorWebViewModules=fallback group so it can be conditionally
68-
included via a deferred group when the consumer has no JS library modules. -->
66+
<!-- blazor.modules.json is an EMPTY ([]) fallback. It is shipped RAW (not as a package
67+
static web asset, so it never appears in PackageAssets.json and never auto-flows) and is
68+
materialized as the consumer's own static web asset by StaticWebAssets.Groups.targets
69+
ONLY when the app contributes no JS library modules of its own. When the app DOES have
70+
modules, the SDK generates _framework/blazor.modules.json and we add nothing, so the two
71+
never collide. -->
6972
<ItemGroup>
7073
<Content Remove="blazor.modules.json" />
71-
<Content Include="blazor.modules.json" Link="wwwroot\blazor.modules.json"
72-
AssetTraitName="JSModule" AssetTraitValue="JSModuleManifest" />
73-
</ItemGroup>
74-
75-
<ItemGroup>
76-
<StaticWebAssetGroupDefinition Include="BlazorWebView"
77-
Value="enabled" SourceId="$(PackageId)" Order="0"
78-
IncludePattern="**" ExcludePattern="**/blazor.modules.json" />
79-
<StaticWebAssetGroupDefinition Include="BlazorWebViewModules"
80-
Value="fallback" SourceId="$(PackageId)" Order="1"
81-
IncludePattern="**/blazor.modules.json" />
74+
<None Include="blazor.modules.json" Pack="true" PackagePath="build\blazor.modules.json" />
8275
</ItemGroup>
8376

8477
<!-- blazor.webview.js should exist after eng/Npm.Workspace.nodeproj builds. Warn if not. -->
Lines changed: 55 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -1,90 +1,74 @@
11
<Project>
2+
<!-- This file ships in the package's build/ folder and is auto-imported by consumers via
3+
build/Microsoft.AspNetCore.Components.WebView.targets. (The SDK static web assets pack
4+
imports a file named StaticWebAssets.Groups.targets by convention; the name is kept for
5+
that reason even though this package no longer uses asset groups.)
6+
7+
Purpose: WebView apps fetch _framework/blazor.modules.json at runtime. When the app
8+
contributes its own JS library modules, the SDK generates that manifest. When it does
9+
not, nothing would be served, so this package ships an empty ([]) fallback and
10+
materializes it as the consumer's own static web asset ONLY in that case. The decision is
11+
made here, during ResolveStaticWebAssetsInputs (before the build manifest is generated and
12+
its conflict check runs), so exactly one asset ever lands on the route and no group /
13+
conflict-resolution machinery is required. -->
214
<PropertyGroup>
15+
<!-- Emit the app's own JS module manifest at the path WebView serves. -->
316
<JSModuleManifestRelativePath>_framework/blazor.modules.json</JSModuleManifestRelativePath>
417
<CompressionEnabled>false</CompressionEnabled>
518
</PropertyGroup>
619

7-
<ItemGroup>
8-
<StaticWebAssetGroup Include="BlazorWebView"
9-
Value="enabled"
10-
SourceId="Microsoft.AspNetCore.Components.WebView" />
11-
</ItemGroup>
12-
13-
<!-- BlazorWebViewModules group: resolves to 'default' when the consumer generates
14-
a JS module manifest (JS library modules exist), or 'fallback' when it doesn't.
15-
The SDK-generated manifest is tagged with BlazorWebViewModules=default by
16-
_TagSdkModulesManifestWithGroup (runs before GenerateStaticWebAssetsManifest).
17-
The WebView fallback blazor.modules.json is tagged with BlazorWebViewModules=fallback
18-
via StaticWebAssetGroupDefinition in the csproj. Both coexist in the build manifest
19-
(distinct AssetGroups), and FilterStaticWebAssetGroups picks the right one. -->
20-
21-
<!-- Target 1: Tag the SDK-generated JS module manifest with BlazorWebViewModules=default
22-
before GenerateStaticWebAssetsManifest runs its conflict check. -->
2320
<PropertyGroup>
24-
<GenerateStaticWebAssetsManifestDependsOn>
25-
$(GenerateStaticWebAssetsManifestDependsOn);
26-
_TagSdkModulesManifestWithGroup
27-
</GenerateStaticWebAssetsManifestDependsOn>
21+
<ResolveStaticWebAssetsInputsDependsOn>
22+
$(ResolveStaticWebAssetsInputsDependsOn);
23+
_AddBlazorWebViewModulesFallback;
24+
</ResolveStaticWebAssetsInputsDependsOn>
25+
<!-- Ensure the app's JS module resolution has run so @(_ExistingBuildJSModules) is populated
26+
before we decide whether to add the fallback. -->
27+
<_AddBlazorWebViewModulesFallbackDependsOn Condition="'$(GenerateJSModuleManifest)' == 'true'">GenerateJSModuleManifestBuildStaticWebAssets</_AddBlazorWebViewModulesFallbackDependsOn>
2828
</PropertyGroup>
2929

30-
<Target Name="_TagSdkModulesManifestWithGroup">
30+
<Target Name="_AddBlazorWebViewModulesFallback"
31+
DependsOnTargets="$(_AddBlazorWebViewModulesFallbackDependsOn)">
32+
<PropertyGroup>
33+
<_BlazorWebViewModulesFallbackRoot>$(MSBuildThisFileDirectory)</_BlazorWebViewModulesFallbackRoot>
34+
<_BlazorWebViewModulesFallbackFile>$(_BlazorWebViewModulesFallbackRoot)blazor.modules.json</_BlazorWebViewModulesFallbackFile>
35+
</PropertyGroup>
36+
3137
<ItemGroup>
32-
<_SdkGeneratedModulesManifest Include="@(StaticWebAsset)"
33-
Condition="'%(StaticWebAsset.AssetTraitName)' == 'JSModule' and '%(StaticWebAsset.AssetTraitValue)' == 'JSModuleManifest' and '%(StaticWebAsset.SourceType)' == 'Computed'" />
38+
<_BlazorWebViewModulesFallbackCandidate
39+
Include="$(_BlazorWebViewModulesFallbackFile)"
40+
Condition="'@(_ExistingBuildJSModules)' == '' and Exists('$(_BlazorWebViewModulesFallbackFile)')">
41+
<RelativePath>_framework/blazor.modules.json</RelativePath>
42+
</_BlazorWebViewModulesFallbackCandidate>
3443
</ItemGroup>
3544

36-
<!-- The SDK-generated manifest has AssetKind=Build and AssetMode=CurrentProject,
37-
which means it won't appear in the development manifest or be served at runtime.
38-
WebView needs it served, so promote it to AssetKind=All and AssetMode=All. -->
39-
<ItemGroup Condition="'@(_SdkGeneratedModulesManifest)' != ''">
40-
<StaticWebAsset Remove="@(_SdkGeneratedModulesManifest)" />
41-
<StaticWebAsset Include="@(_SdkGeneratedModulesManifest)">
42-
<AssetGroups>BlazorWebViewModules=default</AssetGroups>
43-
<AssetKind>All</AssetKind>
44-
<AssetMode>All</AssetMode>
45-
</StaticWebAsset>
46-
</ItemGroup>
47-
</Target>
45+
<Message Importance="Low"
46+
Condition="'@(_BlazorWebViewModulesFallbackCandidate)' != ''"
47+
Text="BlazorWebView: app has no JS library modules; adding fallback _framework/blazor.modules.json." />
4848

49-
<!-- Target 2: Resolve the deferred BlazorWebViewModules group after
50-
ResolveStaticWebAssetsInputs has populated _ExistingBuildJSModules. -->
51-
<PropertyGroup>
52-
<FilterDeferredStaticWebAssetGroupsDependsOn>
53-
$(FilterDeferredStaticWebAssetGroupsDependsOn);
54-
_ResolveBlazorWebViewModulesGroup
55-
</FilterDeferredStaticWebAssetGroupsDependsOn>
56-
</PropertyGroup>
49+
<DefineStaticWebAssets Condition="'@(_BlazorWebViewModulesFallbackCandidate)' != ''"
50+
CandidateAssets="@(_BlazorWebViewModulesFallbackCandidate)"
51+
SourceId="$(PackageId)"
52+
SourceType="Discovered"
53+
ContentRoot="$(_BlazorWebViewModulesFallbackRoot)"
54+
BasePath="/"
55+
AssetKind="All"
56+
AssetMode="All"
57+
AssetRole="Primary"
58+
FingerprintCandidates="true">
59+
<Output TaskParameter="Assets" ItemName="_BlazorWebViewModulesFallbackAsset" />
60+
</DefineStaticWebAssets>
5761

58-
<ItemGroup>
59-
<StaticWebAssetGroup Include="BlazorWebViewModules"
60-
SourceId="Microsoft.AspNetCore.Components.WebView" Deferred="true" />
61-
<!-- The SDK-generated manifest has SourceId=$(PackageId) (the consumer project),
62-
so we need a matching group entry for that SourceId too. -->
63-
<StaticWebAssetGroup Include="BlazorWebViewModules"
64-
SourceId="$(PackageId)" Deferred="true" />
65-
</ItemGroup>
62+
<DefineStaticWebAssetEndpoints Condition="'@(_BlazorWebViewModulesFallbackAsset)' != ''"
63+
CandidateAssets="@(_BlazorWebViewModulesFallbackAsset)"
64+
ExistingEndpoints="@(StaticWebAssetEndpoint)"
65+
ContentTypeMappings="@(StaticWebAssetContentTypeMapping)">
66+
<Output TaskParameter="Endpoints" ItemName="_BlazorWebViewModulesFallbackEndpoint" />
67+
</DefineStaticWebAssetEndpoints>
6668

67-
<Target Name="_ResolveBlazorWebViewModulesGroup">
6869
<ItemGroup>
69-
<StaticWebAssetGroup Remove="BlazorWebViewModules" />
70-
<!-- Group for WebView's fallback blazor.modules.json -->
71-
<StaticWebAssetGroup Include="BlazorWebViewModules"
72-
Value="default"
73-
SourceId="Microsoft.AspNetCore.Components.WebView"
74-
Condition="'@(_ExistingBuildJSModules)' != ''" />
75-
<StaticWebAssetGroup Include="BlazorWebViewModules"
76-
Value="fallback"
77-
SourceId="Microsoft.AspNetCore.Components.WebView"
78-
Condition="'@(_ExistingBuildJSModules)' == ''" />
79-
<!-- Group for the consumer's SDK-generated manifest (SourceId=$(PackageId)) -->
80-
<StaticWebAssetGroup Include="BlazorWebViewModules"
81-
Value="default"
82-
SourceId="$(PackageId)"
83-
Condition="'@(_ExistingBuildJSModules)' != ''" />
84-
<StaticWebAssetGroup Include="BlazorWebViewModules"
85-
Value="fallback"
86-
SourceId="$(PackageId)"
87-
Condition="'@(_ExistingBuildJSModules)' == ''" />
70+
<StaticWebAsset Include="@(_BlazorWebViewModulesFallbackAsset)" />
71+
<StaticWebAssetEndpoint Include="@(_BlazorWebViewModulesFallbackEndpoint)" />
8872
</ItemGroup>
8973
</Target>
9074
</Project>
Lines changed: 209 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,209 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System.Diagnostics;
5+
using System.Runtime.CompilerServices;
6+
using System.Text;
7+
using Xunit.Abstractions;
8+
9+
namespace Microsoft.AspNetCore.Components.WebView.StaticWebAssets;
10+
11+
/// <summary>
12+
/// Creates a throwaway solution on disk that references the locally-built WebView package and runs
13+
/// the repo SDK (.dotnet) to build/publish it. Used to validate that consuming the package produces
14+
/// the expected static web asset endpoints (issue #67374).
15+
///
16+
/// Working folders live under the repo's artifacts/tmp directory (not the system temp folder), and
17+
/// every build/publish captures a binary log under artifacts/log so failures can be diagnosed from
18+
/// CI. The working folder is preserved when a build fails and removed on success.
19+
/// </summary>
20+
internal sealed class ConsumerBuild : IDisposable
21+
{
22+
private readonly ITestOutputHelper _output;
23+
private readonly string _root;
24+
private readonly string _packagesFolder;
25+
private readonly string _id;
26+
private bool _preserve;
27+
28+
public ConsumerBuild(ITestOutputHelper output, bool isolateNuGetFeeds = true, [CallerMemberName] string testName = "")
29+
{
30+
_output = output;
31+
_id = $"{testName}-{Guid.NewGuid():N}";
32+
_root = Path.Combine(StaticWebAssetsTestData.ArtifactsTmpDir, "ComponentsWebViewStaticWebAssetsTests", _id);
33+
Directory.CreateDirectory(_root);
34+
_packagesFolder = Path.Combine(_root, ".nuget-packages");
35+
36+
// Isolate the build from the repo and from any other test run.
37+
File.WriteAllText(Path.Combine(_root, "Directory.Build.props"), "<Project />");
38+
File.WriteAllText(Path.Combine(_root, "Directory.Build.targets"), "<Project />");
39+
40+
if (!isolateNuGetFeeds)
41+
{
42+
// ProjectReference (P2P) mode: the app references the WebView source project, so it needs
43+
// no package feed of its own. Inherit the repo's NuGet.config (the working folder lives
44+
// under the repo's artifacts) so the referenced project's dependencies resolve.
45+
return;
46+
}
47+
48+
// Use an isolated global-packages folder so the freshly-built package under test is never
49+
// served stale from a shared cache, while adding the repo's package cache as a read-only
50+
// fallback so the exact transitive package versions the repo restored (which may not be
51+
// published to public feeds yet) can still be resolved.
52+
var repoCache = StaticWebAssetsTestData.NuGetPackageRoot.TrimEnd('\\', '/');
53+
File.WriteAllText(Path.Combine(_root, "NuGet.config"), $"""
54+
<configuration>
55+
<config>
56+
<add key="globalPackagesFolder" value="{_packagesFolder}" />
57+
</config>
58+
<fallbackPackageFolders>
59+
<clear />
60+
<add key="repo-cache" value="{repoCache}" />
61+
</fallbackPackageFolders>
62+
<packageSources>
63+
<clear />
64+
<add key="local-shipping" value="{StaticWebAssetsTestData.ShippingPackagesDir.TrimEnd('\\', '/')}" />
65+
<add key="local-nonshipping" value="{StaticWebAssetsTestData.NonShippingPackagesDir.TrimEnd('\\', '/')}" />
66+
<add key="dotnet-public" value="https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public/nuget/v3/index.json" />
67+
<add key="nuget" value="https://api.nuget.org/v3/index.json" />
68+
</packageSources>
69+
</configuration>
70+
""");
71+
}
72+
73+
public string Root => _root;
74+
75+
public string CreateProject(string relativeDir, string fileName, string content)
76+
{
77+
var dir = Path.Combine(_root, relativeDir);
78+
Directory.CreateDirectory(dir);
79+
var path = Path.Combine(dir, fileName);
80+
File.WriteAllText(path, content);
81+
return path;
82+
}
83+
84+
public void CreateFile(string relativePath, string content)
85+
{
86+
var path = Path.Combine(_root, relativePath);
87+
Directory.CreateDirectory(Path.GetDirectoryName(path)!);
88+
File.WriteAllText(path, content);
89+
}
90+
91+
/// <param name="verb">The dotnet verb plus its options, e.g. "publish -c Release".</param>
92+
/// <param name="projectRelativePath">Project to build, relative to the working folder.</param>
93+
public ProcessResult Run(string verb, string projectRelativePath)
94+
{
95+
// The package version under test is constant (e.g. 11.0.0-dev). Make sure a previously
96+
// extracted copy in the shared repo cache (used as a fallback folder) can't shadow the
97+
// freshly built package; restore will then pull it from the local feed.
98+
EvictFromFallbackCache("Microsoft.AspNetCore.Components.WebView");
99+
100+
// Capture a binary log under artifacts/log so CI uploads it and failures can be analyzed.
101+
var verbName = verb.Split(' ', StringSplitOptions.RemoveEmptyEntries).FirstOrDefault() ?? "build";
102+
var binlogPath = Path.Combine(StaticWebAssetsTestData.ArtifactsLogDir, $"WebViewStaticWebAssets-{_id}-{verbName}.binlog");
103+
Directory.CreateDirectory(StaticWebAssetsTestData.ArtifactsLogDir);
104+
105+
var arguments = $"{verb} \"{Path.Combine(_root, projectRelativePath)}\" -bl:\"{binlogPath}\"";
106+
var psi = new ProcessStartInfo(StaticWebAssetsTestData.DotNetHost)
107+
{
108+
Arguments = arguments,
109+
WorkingDirectory = _root,
110+
RedirectStandardOutput = true,
111+
RedirectStandardError = true,
112+
UseShellExecute = false,
113+
};
114+
115+
// Keep the build hermetic: pin the repo runtime and stop the SDK from reaching outside the
116+
// .dotnet folder. The global-packages folder is configured via NuGet.config.
117+
psi.Environment["DOTNET_ROOT"] = Path.Combine(StaticWebAssetsTestData.RepoRoot, ".dotnet");
118+
psi.Environment["DOTNET_MULTILEVEL_LOOKUP"] = "0";
119+
psi.Environment["DOTNET_SKIP_FIRST_TIME_EXPERIENCE"] = "1";
120+
psi.Environment["DOTNET_NOLOGO"] = "1";
121+
psi.Environment.Remove("MSBuildSDKsPath");
122+
123+
_output.WriteLine($"> dotnet {arguments}");
124+
125+
var output = new StringBuilder();
126+
using var process = new Process { StartInfo = psi };
127+
process.OutputDataReceived += (_, e) => { if (e.Data is not null) { lock (output) { output.AppendLine(e.Data); } } };
128+
process.ErrorDataReceived += (_, e) => { if (e.Data is not null) { lock (output) { output.AppendLine(e.Data); } } };
129+
130+
process.Start();
131+
process.BeginOutputReadLine();
132+
process.BeginErrorReadLine();
133+
134+
if (!process.WaitForExit(milliseconds: 5 * 60 * 1000))
135+
{
136+
try { process.Kill(entireProcessTree: true); } catch { }
137+
_preserve = true;
138+
throw new TimeoutException($"'dotnet {verb}' timed out. Binlog: {binlogPath}\n{output}");
139+
}
140+
141+
process.WaitForExit();
142+
var result = new ProcessResult(process.ExitCode, output.ToString(), binlogPath);
143+
144+
_output.WriteLine(result.Output);
145+
_output.WriteLine($"Exit code: {result.ExitCode}. Binlog: {binlogPath}");
146+
if (!result.Succeeded)
147+
{
148+
// Leave the working folder in place so the failure can be investigated locally.
149+
_preserve = true;
150+
}
151+
152+
return result;
153+
}
154+
155+
public void Dispose()
156+
{
157+
if (_preserve)
158+
{
159+
_output.WriteLine($"Build failed; preserving working folder for investigation: {_root}");
160+
return;
161+
}
162+
163+
try
164+
{
165+
Directory.Delete(_root, recursive: true);
166+
}
167+
catch
168+
{
169+
// Best effort cleanup.
170+
}
171+
}
172+
173+
private static void EvictFromFallbackCache(string packageId)
174+
{
175+
var dir = Path.Combine(
176+
StaticWebAssetsTestData.NuGetPackageRoot,
177+
packageId.ToLowerInvariant(),
178+
StaticWebAssetsTestData.PackageVersion);
179+
180+
try
181+
{
182+
if (Directory.Exists(dir))
183+
{
184+
Directory.Delete(dir, recursive: true);
185+
}
186+
}
187+
catch
188+
{
189+
// Best effort; if it can't be removed restore may still succeed from the local feed.
190+
}
191+
}
192+
}
193+
194+
internal sealed record ProcessResult(int ExitCode, string Output, string BinlogPath)
195+
{
196+
public bool Succeeded => ExitCode == 0;
197+
198+
/// <summary>
199+
/// True when the failure looks like it was caused by an inability to reach the NuGet feeds rather
200+
/// than a real build problem, so offline environments can skip instead of failing.
201+
/// </summary>
202+
public bool LooksLikeNetworkFailure
203+
=> !Succeeded &&
204+
(Output.Contains("Unable to load the service index", StringComparison.OrdinalIgnoreCase) ||
205+
Output.Contains("NU1301", StringComparison.OrdinalIgnoreCase) ||
206+
Output.Contains("Unable to resolve", StringComparison.OrdinalIgnoreCase) && Output.Contains("nuget", StringComparison.OrdinalIgnoreCase) ||
207+
Output.Contains("The remote name could not be resolved", StringComparison.OrdinalIgnoreCase) ||
208+
Output.Contains("No such host is known", StringComparison.OrdinalIgnoreCase));
209+
}

0 commit comments

Comments
 (0)