Skip to content

Commit 4628dfb

Browse files
Cache assemblies and wasm using content hashes (#18859)
1 parent e0fe30c commit 4628dfb

File tree

18 files changed

+813
-204
lines changed

18 files changed

+813
-204
lines changed

src/Components/Blazor/Build/src/Tasks/GenerateBlazorBootJson.cs

Lines changed: 103 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,14 @@
22
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
33

44
using System;
5+
using System.Collections.Generic;
56
using System.IO;
6-
using System.Linq;
77
using System.Reflection;
88
using System.Runtime.Serialization.Json;
9+
using System.Text;
910
using Microsoft.Build.Framework;
1011
using Microsoft.Build.Utilities;
12+
using ResourceHashesByNameDictionary = System.Collections.Generic.Dictionary<string, string>;
1113

1214
namespace Microsoft.AspNetCore.Blazor.Build
1315
{
@@ -17,53 +19,107 @@ public class GenerateBlazorBootJson : Task
1719
public string AssemblyPath { get; set; }
1820

1921
[Required]
20-
public ITaskItem[] References { get; set; }
22+
public ITaskItem[] Resources { get; set; }
23+
24+
[Required]
25+
public bool DebugBuild { get; set; }
2126

2227
[Required]
2328
public bool LinkerEnabled { get; set; }
2429

30+
[Required]
31+
public bool CacheBootResources { get; set; }
32+
2533
[Required]
2634
public string OutputPath { get; set; }
2735

2836
public override bool Execute()
2937
{
38+
using var fileStream = File.Create(OutputPath);
3039
var entryAssemblyName = AssemblyName.GetAssemblyName(AssemblyPath).Name;
31-
var assemblies = References.Select(GetUriPath).OrderBy(c => c, StringComparer.Ordinal).ToArray();
3240

33-
using var fileStream = File.Create(OutputPath);
34-
WriteBootJson(fileStream, entryAssemblyName, assemblies, LinkerEnabled);
41+
try
42+
{
43+
WriteBootJson(fileStream, entryAssemblyName);
44+
}
45+
catch (Exception ex)
46+
{
47+
Log.LogErrorFromException(ex);
48+
}
3549

36-
return true;
50+
return !Log.HasLoggedErrors;
51+
}
3752

38-
static string GetUriPath(ITaskItem item)
53+
// Internal for tests
54+
internal void WriteBootJson(Stream output, string entryAssemblyName)
55+
{
56+
var result = new BootJsonData
3957
{
40-
var outputPath = item.GetMetadata("RelativeOutputPath");
41-
if (string.IsNullOrEmpty(outputPath))
58+
entryAssembly = entryAssemblyName,
59+
cacheBootResources = CacheBootResources,
60+
debugBuild = DebugBuild,
61+
linkerEnabled = LinkerEnabled,
62+
resources = new Dictionary<ResourceType, ResourceHashesByNameDictionary>()
63+
};
64+
65+
// Build a two-level dictionary of the form:
66+
// - BootResourceType (e.g., "assembly")
67+
// - UriPath (e.g., "System.Text.Json.dll")
68+
// - ContentHash (e.g., "4548fa2e9cf52986")
69+
if (Resources != null)
70+
{
71+
foreach (var resource in Resources)
4272
{
43-
outputPath = Path.GetFileName(item.ItemSpec);
44-
}
73+
var resourceTypeMetadata = resource.GetMetadata("BootResourceType");
74+
if (!Enum.TryParse<ResourceType>(resourceTypeMetadata, out var resourceType))
75+
{
76+
throw new NotSupportedException($"Unsupported BootResourceType metadata value: {resourceTypeMetadata}");
77+
}
4578

46-
return outputPath.Replace('\\', '/');
79+
if (!result.resources.TryGetValue(resourceType, out var resourceList))
80+
{
81+
resourceList = new ResourceHashesByNameDictionary();
82+
result.resources.Add(resourceType, resourceList);
83+
}
84+
85+
var resourceFileRelativePath = GetResourceFileRelativePath(resource);
86+
if (!resourceList.ContainsKey(resourceFileRelativePath))
87+
{
88+
resourceList.Add(resourceFileRelativePath, $"sha256-{resource.GetMetadata("FileHash")}");
89+
}
90+
}
4791
}
92+
93+
var serializer = new DataContractJsonSerializer(typeof(BootJsonData), new DataContractJsonSerializerSettings
94+
{
95+
UseSimpleDictionaryFormat = true
96+
});
97+
98+
using var writer = JsonReaderWriterFactory.CreateJsonWriter(output, Encoding.UTF8, ownsStream: false, indent: true);
99+
serializer.WriteObject(writer, result);
48100
}
49101

50-
internal static void WriteBootJson(Stream stream, string entryAssemblyName, string[] assemblies, bool linkerEnabled)
102+
private static string GetResourceFileRelativePath(ITaskItem item)
51103
{
52-
var data = new BootJsonData
104+
// The build targets use RelativeOutputPath in the case of satellite assemblies, which
105+
// will have relative paths like "fr\\SomeAssembly.resources.dll". If RelativeOutputPath
106+
// is specified, we want to use all of it.
107+
var outputPath = item.GetMetadata("RelativeOutputPath");
108+
109+
if (string.IsNullOrEmpty(outputPath))
53110
{
54-
entryAssembly = entryAssemblyName,
55-
assemblies = assemblies,
56-
linkerEnabled = linkerEnabled,
57-
};
111+
// If RelativeOutputPath was not specified, we assume the item will be placed at the
112+
// root of whatever directory is used for its resource type (e.g., assemblies go in _bin)
113+
outputPath = Path.GetFileName(item.ItemSpec);
114+
}
58115

59-
var serializer = new DataContractJsonSerializer(typeof(BootJsonData));
60-
serializer.WriteObject(stream, data);
116+
return outputPath.Replace('\\', '/');
61117
}
62118

119+
#pragma warning disable IDE1006 // Naming Styles
63120
/// <summary>
64121
/// Defines the structure of a Blazor boot JSON file
65122
/// </summary>
66-
#pragma warning disable IDE1006 // Naming Styles
67123
public class BootJsonData
68124
{
69125
/// <summary>
@@ -72,15 +128,39 @@ public class BootJsonData
72128
public string entryAssembly { get; set; }
73129

74130
/// <summary>
75-
/// Gets the closure of assemblies to be loaded by Blazor WASM. This includes the application entry assembly.
131+
/// Gets the set of resources needed to boot the application. This includes the transitive
132+
/// closure of .NET assemblies (including the entrypoint assembly), the dotnet.wasm file,
133+
/// and any PDBs to be loaded.
134+
///
135+
/// Within <see cref="ResourceHashesByNameDictionary"/>, dictionary keys are resource names,
136+
/// and values are SHA-256 hashes formatted in prefixed base-64 style (e.g., 'sha256-abcdefg...')
137+
/// as used for subresource integrity checking.
76138
/// </summary>
77-
public string[] assemblies { get; set; }
139+
public Dictionary<ResourceType, ResourceHashesByNameDictionary> resources { get; set; }
140+
141+
/// <summary>
142+
/// Gets a value that determines whether to enable caching of the <see cref="resources"/>
143+
/// inside a CacheStorage instance within the browser.
144+
/// </summary>
145+
public bool cacheBootResources { get; set; }
146+
147+
/// <summary>
148+
/// Gets a value that determines if this is a debug build.
149+
/// </summary>
150+
public bool debugBuild { get; set; }
78151

79152
/// <summary>
80153
/// Gets a value that determines if the linker is enabled.
81154
/// </summary>
82155
public bool linkerEnabled { get; set; }
83156
}
157+
158+
public enum ResourceType
159+
{
160+
assembly,
161+
pdb,
162+
wasm
163+
}
84164
#pragma warning restore IDE1006 // Naming Styles
85165
}
86166
}

src/Components/Blazor/Build/src/targets/Blazor.MonoRuntime.targets

Lines changed: 65 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -52,26 +52,6 @@
5252
<Target
5353
Name="PrepareBlazorOutputs"
5454
DependsOnTargets="_ResolveBlazorInputs;_ResolveBlazorOutputs;_GenerateBlazorBootJson">
55-
56-
<ItemGroup>
57-
<MonoWasmFile Include="$(DotNetWebAssemblyRuntimePath)*" />
58-
<BlazorJSFile Include="$(BlazorJSPath)" />
59-
<BlazorJSFile Include="$(BlazorJSMapPath)" Condition="Exists('$(BlazorJSMapPath)')" />
60-
61-
<BlazorOutputWithTargetPath Include="@(MonoWasmFile)">
62-
<TargetOutputPath>$(BlazorRuntimeWasmOutputPath)%(FileName)%(Extension)</TargetOutputPath>
63-
</BlazorOutputWithTargetPath>
64-
<BlazorOutputWithTargetPath Include="@(BlazorJSFile)">
65-
<TargetOutputPath>$(BaseBlazorRuntimeOutputPath)%(FileName)%(Extension)</TargetOutputPath>
66-
</BlazorOutputWithTargetPath>
67-
</ItemGroup>
68-
69-
<ItemGroup Label="Static content supplied by NuGet packages">
70-
<_BlazorPackageContentOutput Include="@(BlazorPackageContentFile)" Condition="%(SourcePackage) != ''">
71-
<TargetOutputPath>$(BaseBlazorPackageContentOutputPath)%(SourcePackage)\%(RecursiveDir)\%(Filename)%(Extension)</TargetOutputPath>
72-
</_BlazorPackageContentOutput>
73-
<BlazorOutputWithTargetPath Include="@(_BlazorPackageContentOutput)" />
74-
</ItemGroup>
7555
</Target>
7656

7757
<Target Name="_ResolveBlazorInputs" DependsOnTargets="ResolveReferences;ResolveRuntimePackAssets">
@@ -128,6 +108,11 @@
128108
Message="Unrecongnized value for BlazorLinkOnBuild: '$(BlazorLinkOnBuild)'. Valid values are 'true' or 'false'."
129109
Condition="'$(BlazorLinkOnBuild)' != 'true' AND '$(BlazorLinkOnBuild)' != 'false'" />
130110

111+
<!--
112+
These are the items calculated as the closure of the runtime assemblies, either by calling the linker
113+
or by calling our custom ResolveBlazorRuntimeDependencies task if the linker was disabled. Other than
114+
satellite assemblies, this should include all assemblies needed to run the application.
115+
-->
131116
<ItemGroup>
132117
<!--
133118
ReferenceCopyLocalPaths includes all files that are part of the build out with CopyLocalLockFileAssemblies on.
@@ -146,6 +131,49 @@
146131
<BlazorRuntimeFile>true</BlazorRuntimeFile>
147132
<TargetOutputPath>$(BlazorRuntimeBinOutputPath)%(FileName)%(Extension)</TargetOutputPath>
148133
<RelativeOutputPath>%(FileName)%(Extension)</RelativeOutputPath>
134+
</BlazorOutputWithTargetPath>
135+
</ItemGroup>
136+
137+
<!--
138+
We need to know at build time (not publish time) whether or not to include pdbs in the
139+
blazor.boot.json file, so this is controlled by the BlazorEnableDebugging flag, whose
140+
default value is determined by the build configuration.
141+
-->
142+
<ItemGroup Condition="'$(BlazorEnableDebugging)' != 'true'">
143+
<BlazorOutputWithTargetPath Remove="@(BlazorOutputWithTargetPath)" Condition="'%(Extension)' == '.pdb'" />
144+
</ItemGroup>
145+
146+
<!--
147+
The following itemgroup attempts to extend the set to include satellite assemblies.
148+
The mechanism behind this (or whether it's correct) is a bit unclear so
149+
https://github.com/dotnet/aspnetcore/issues/18951 tracks the need for follow-up.
150+
-->
151+
<ItemGroup>
152+
<!--
153+
ReferenceCopyLocalPaths includes all files that are part of the build out with CopyLocalLockFileAssemblies on.
154+
Remove assemblies that are inputs to calculating the assembly closure. Instead use the resolved outputs, since it is the minimal set.
155+
-->
156+
<_BlazorCopyLocalPaths Include="@(ReferenceCopyLocalPaths)" Condition="'%(Extension)' == '.dll'" />
157+
<_BlazorCopyLocalPaths Remove="@(_BlazorManagedRuntimeAssemby)" Condition="'%(Extension)' == '.dll'" />
158+
159+
<BlazorOutputWithTargetPath Include="@(_BlazorCopyLocalPaths)">
160+
<BlazorRuntimeFile>true</BlazorRuntimeFile>
161+
<TargetOutputPath>$(BlazorRuntimeBinOutputPath)%(_BlazorCopyLocalPaths.DestinationSubDirectory)%(FileName)%(Extension)</TargetOutputPath>
162+
<RelativeOutputPath>%(_BlazorCopyLocalPaths.DestinationSubDirectory)%(FileName)%(Extension)</RelativeOutputPath>
163+
</BlazorOutputWithTargetPath>
164+
</ItemGroup>
165+
166+
<ItemGroup>
167+
<MonoWasmFile Include="$(DotNetWebAssemblyRuntimePath)*" />
168+
<BlazorJSFile Include="$(BlazorJSPath)" />
169+
<BlazorJSFile Include="$(BlazorJSMapPath)" Condition="Exists('$(BlazorJSMapPath)')" />
170+
171+
<BlazorOutputWithTargetPath Include="@(MonoWasmFile)">
172+
<TargetOutputPath>$(BlazorRuntimeWasmOutputPath)%(FileName)%(Extension)</TargetOutputPath>
173+
<BlazorRuntimeFile>true</BlazorRuntimeFile>
174+
</BlazorOutputWithTargetPath>
175+
<BlazorOutputWithTargetPath Include="@(BlazorJSFile)">
176+
<TargetOutputPath>$(BaseBlazorRuntimeOutputPath)%(FileName)%(Extension)</TargetOutputPath>
149177
</BlazorOutputWithTargetPath>
150178
</ItemGroup>
151179
</Target>
@@ -267,7 +295,7 @@
267295

268296
<ItemGroup>
269297
<_LinkerResult Include="$(BlazorIntermediateLinkerOutputPath)*.dll" />
270-
<_LinkerResult Include="$(BlazorIntermediateLinkerOutputPath)*.pdb" Condition="'$(BlazorEnableDebugging)' == 'true'" />
298+
<_LinkerResult Include="$(BlazorIntermediateLinkerOutputPath)*.pdb" />
271299
</ItemGroup>
272300

273301
<WriteLinesToFile File="$(_BlazorLinkerOutputCache)" Lines="@(_LinkerResult)" Overwrite="true" />
@@ -318,13 +346,27 @@
318346
Inputs="$(MSBuildAllProjects);@(BlazorOutputWithTargetPath)"
319347
Outputs="$(BlazorBootJsonIntermediateOutputPath)">
320348
<ItemGroup>
321-
<_BlazorRuntimeFile Include="@(BlazorOutputWithTargetPath->WithMetadataValue('BlazorRuntimeFile', 'true'))" />
349+
<_BlazorBootResource Include="@(BlazorOutputWithTargetPath->WithMetadataValue('BlazorRuntimeFile', 'true'))" />
350+
<_BlazorBootResource BootResourceType="assembly" Condition="'%(Extension)' == '.dll'" />
351+
<_BlazorBootResource BootResourceType="pdb" Condition="'%(Extension)' == '.pdb'" />
352+
<_BlazorBootResource BootResourceType="wasm" Condition="'%(Extension)' == '.wasm'" />
322353
</ItemGroup>
323354

355+
<GetFileHash Files="@(_BlazorBootResource->HasMetadata('BootResourceType'))" Algorithm="SHA256" HashEncoding="base64">
356+
<Output TaskParameter="Items" ItemName="_BlazorBootResourceWithHash" />
357+
</GetFileHash>
358+
359+
<PropertyGroup>
360+
<_IsDebugBuild>false</_IsDebugBuild>
361+
<_IsDebugBuild Condition="'$(Configuration)' == 'Debug'">true</_IsDebugBuild>
362+
<BlazorCacheBootResources Condition="'$(BlazorCacheBootResources)' == ''">true</BlazorCacheBootResources>
363+
</PropertyGroup>
324364
<GenerateBlazorBootJson
325365
AssemblyPath="@(IntermediateAssembly)"
326-
References="@(_BlazorRuntimeFile)"
366+
Resources="@(_BlazorBootResourceWithHash)"
367+
DebugBuild="$(_IsDebugBuild)"
327368
LinkerEnabled="$(BlazorLinkOnBuild)"
369+
CacheBootResources="$(BlazorCacheBootResources)"
328370
OutputPath="$(BlazorBootJsonIntermediateOutputPath)" />
329371

330372
<ItemGroup>

src/Components/Blazor/Build/test/BootJsonWriterTest.cs

Lines changed: 0 additions & 41 deletions
This file was deleted.

0 commit comments

Comments
 (0)