Skip to content

Commit 82d05ae

Browse files
authored
Add support for loading satellite assemblies (#20033)
* Add support for loading satellite assemblies Fixes #17016
1 parent cb6858f commit 82d05ae

26 files changed

+957
-173
lines changed

src/Components/Web.JS/dist/Release/blazor.webassembly.js

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/Components/Web.JS/src/Platform/BootConfig.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ export interface ResourceGroups {
3232
readonly assembly: ResourceList;
3333
readonly pdb?: ResourceList;
3434
readonly runtime: ResourceList;
35+
readonly satelliteResources?: { [cultureName: string] : ResourceList };
3536
}
3637

3738
export type ResourceList = { [name: string]: string };
38-

src/Components/Web.JS/src/Platform/Mono/MonoPlatform.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,37 @@ function createEmscriptenModuleInstance(resourceLoader: WebAssemblyResourceLoade
207207
// of the extensions in the URLs. This allows loading assemblies with arbitrary filenames.
208208
assembliesBeingLoaded.forEach(r => addResourceAsAssembly(r, changeExtension(r.name, '.dll')));
209209
pdbsBeingLoaded.forEach(r => addResourceAsAssembly(r, r.name));
210+
211+
// Wire-up callbacks for satellite assemblies. Blazor will call these as part of the application
212+
// startup sequence to load satellite assemblies for the application's culture.
213+
window['Blazor']._internal.getSatelliteAssemblies = (culturesToLoadDotNetArray: System_Array<System_String>) : System_Object => {
214+
const culturesToLoad = BINDING.mono_array_to_js_array<System_String, string>(culturesToLoadDotNetArray);
215+
const satelliteResources = resourceLoader.bootConfig.resources.satelliteResources;
216+
217+
if (satelliteResources) {
218+
const resourcePromises = Promise.all(culturesToLoad
219+
.filter(culture => satelliteResources.hasOwnProperty(culture))
220+
.map(culture => resourceLoader.loadResources(satelliteResources[culture], fileName => `_framework/_bin/${fileName}`))
221+
.reduce((previous, next) => previous.concat(next), new Array<LoadingResource>())
222+
.map(async resource => (await resource.response).arrayBuffer()));
223+
224+
return BINDING.js_to_mono_obj(
225+
resourcePromises.then(resourcesToLoad => {
226+
if (resourcesToLoad.length) {
227+
window['Blazor']._internal.readSatelliteAssemblies = () => {
228+
const array = BINDING.mono_obj_array_new(resourcesToLoad.length);
229+
for (var i = 0; i < resourcesToLoad.length; i++) {
230+
BINDING.mono_obj_array_set(array, i, BINDING.js_typed_array_to_array(new Uint8Array(resourcesToLoad[i])));
231+
}
232+
return array;
233+
};
234+
}
235+
236+
return resourcesToLoad.length;
237+
}));
238+
}
239+
return BINDING.js_to_mono_obj(Promise.resolve(0));
240+
}
210241
});
211242

212243
module.postRun.push(() => {

src/Components/Web.JS/src/Platform/Mono/MonoTypes.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Pointer, System_String } from '../Platform';
1+
import { Pointer, System_String, System_Array, System_Object } from '../Platform';
22

33
// Mono uses this global to hang various debugging-related items on
44

@@ -10,9 +10,12 @@ declare interface MONO {
1010

1111
// Mono uses this global to hold low-level interop APIs
1212
declare interface BINDING {
13+
mono_obj_array_new(length: number): System_Array<System_Object>;
14+
mono_obj_array_set(array: System_Array<System_Object>, index: Number, value: System_Object): void;
1315
js_string_to_mono_string(jsString: string): System_String;
14-
js_typed_array_to_array(array: Uint8Array): Pointer;
15-
js_typed_array_to_array<T>(array: Array<T>): Pointer;
16+
js_typed_array_to_array(array: Uint8Array): System_Object;
17+
js_to_mono_obj(jsObject: any) : System_Object;
18+
mono_array_to_js_array<TInput, TOutput>(array: System_Array<TInput>) : Array<TOutput>;
1619
conv_string(dotnetString: System_String | null): string | null;
1720
bind_static_method(fqn: string, signature?: string): Function;
1821
}

src/Components/Web.JS/src/Platform/WebAssemblyConfigLoader.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { BootConfigResult } from './BootConfig';
2-
import { System_String, Pointer } from './Platform';
2+
import { System_String, System_Object } from './Platform';
33

44
export class WebAssemblyConfigLoader {
55
static async initAsync(bootConfigResult: BootConfigResult): Promise<void> {
@@ -9,7 +9,7 @@ export class WebAssemblyConfigLoader {
99
.filter(name => name === 'appsettings.json' || name === `appsettings.${bootConfigResult.applicationEnvironment}.json`)
1010
.map(async name => ({ name, content: await getConfigBytes(name) })));
1111

12-
window['Blazor']._internal.getConfig = (dotNetFileName: System_String) : Pointer | undefined => {
12+
window['Blazor']._internal.getConfig = (dotNetFileName: System_String) : System_Object | undefined => {
1313
const fileName = BINDING.conv_string(dotNetFileName);
1414
const resolvedFile = configFiles.find(f => f.name === fileName);
1515
return resolvedFile ? BINDING.js_typed_array_to_array(resolvedFile.content) : undefined;

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

Lines changed: 62 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
using System.Collections.Generic;
66
using System.IO;
77
using System.Reflection;
8+
using System.Runtime.Serialization;
89
using System.Runtime.Serialization.Json;
910
using System.Text;
1011
using Microsoft.Build.Framework;
@@ -61,28 +62,56 @@ internal void WriteBootJson(Stream output, string entryAssemblyName)
6162
cacheBootResources = CacheBootResources,
6263
debugBuild = DebugBuild,
6364
linkerEnabled = LinkerEnabled,
64-
resources = new Dictionary<ResourceType, ResourceHashesByNameDictionary>(),
65+
resources = new ResourcesData(),
6566
config = new List<string>(),
6667
};
6768

6869
// Build a two-level dictionary of the form:
69-
// - BootResourceType (e.g., "assembly")
70+
// - assembly:
7071
// - UriPath (e.g., "System.Text.Json.dll")
7172
// - ContentHash (e.g., "4548fa2e9cf52986")
73+
// - runtime:
74+
// - UriPath (e.g., "dotnet.js")
75+
// - ContentHash (e.g., "3448f339acf512448")
7276
if (Resources != null)
7377
{
78+
var resourceData = result.resources;
7479
foreach (var resource in Resources)
7580
{
7681
var resourceTypeMetadata = resource.GetMetadata("BootManifestResourceType");
77-
if (!Enum.TryParse<ResourceType>(resourceTypeMetadata, out var resourceType))
82+
ResourceHashesByNameDictionary resourceList;
83+
switch (resourceTypeMetadata)
7884
{
79-
throw new NotSupportedException($"Unsupported BootManifestResourceType metadata value: {resourceTypeMetadata}");
80-
}
81-
82-
if (!result.resources.TryGetValue(resourceType, out var resourceList))
83-
{
84-
resourceList = new ResourceHashesByNameDictionary();
85-
result.resources.Add(resourceType, resourceList);
85+
case "runtime":
86+
resourceList = resourceData.runtime;
87+
break;
88+
case "assembly":
89+
resourceList = resourceData.assembly;
90+
break;
91+
case "pdb":
92+
resourceData.pdb = new ResourceHashesByNameDictionary();
93+
resourceList = resourceData.pdb;
94+
break;
95+
case "satellite":
96+
if (resourceData.satelliteResources is null)
97+
{
98+
resourceData.satelliteResources = new Dictionary<string, ResourceHashesByNameDictionary>(StringComparer.OrdinalIgnoreCase);
99+
}
100+
var resourceCulture = resource.GetMetadata("Culture");
101+
if (resourceCulture is null)
102+
{
103+
Log.LogWarning("Satellite resource {0} does not specify required metadata 'Culture'.", resource);
104+
continue;
105+
}
106+
107+
if (!resourceData.satelliteResources.TryGetValue(resourceCulture, out resourceList))
108+
{
109+
resourceList = new ResourceHashesByNameDictionary();
110+
resourceData.satelliteResources.Add(resourceCulture, resourceList);
111+
}
112+
break;
113+
default:
114+
throw new NotSupportedException($"Unsupported BootManifestResourceType metadata value: {resourceTypeMetadata}");
86115
}
87116

88117
var resourceName = GetResourceName(resource);
@@ -142,7 +171,7 @@ public class BootJsonData
142171
/// and values are SHA-256 hashes formatted in prefixed base-64 style (e.g., 'sha256-abcdefg...')
143172
/// as used for subresource integrity checking.
144173
/// </summary>
145-
public Dictionary<ResourceType, ResourceHashesByNameDictionary> resources { get; set; }
174+
public ResourcesData resources { get; set; } = new ResourcesData();
146175

147176
/// <summary>
148177
/// Gets a value that determines whether to enable caching of the <see cref="resources"/>
@@ -166,11 +195,29 @@ public class BootJsonData
166195
public List<string> config { get; set; }
167196
}
168197

169-
public enum ResourceType
198+
public class ResourcesData
170199
{
171-
assembly,
172-
pdb,
173-
runtime,
200+
/// <summary>
201+
/// .NET Wasm runtime resources (dotnet.wasm, dotnet.js) etc.
202+
/// </summary>
203+
public ResourceHashesByNameDictionary runtime { get; set; } = new ResourceHashesByNameDictionary();
204+
205+
/// <summary>
206+
/// "assembly" (.dll) resources
207+
/// </summary>
208+
public ResourceHashesByNameDictionary assembly { get; set; } = new ResourceHashesByNameDictionary();
209+
210+
/// <summary>
211+
/// "debug" (.pdb) resources
212+
/// </summary>
213+
[DataMember(EmitDefaultValue = false)]
214+
public ResourceHashesByNameDictionary pdb { get; set; }
215+
216+
/// <summary>
217+
/// localization (.satellite resx) resources
218+
/// </summary>
219+
[DataMember(EmitDefaultValue = false)]
220+
public Dictionary<string, ResourceHashesByNameDictionary> satelliteResources { get; set; }
174221
}
175222
#pragma warning restore IDE1006 // Naming Styles
176223
}

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

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,7 @@
55
<AdditionalMonoLinkerOptions>--disable-opt unreachablebodies --verbose --strip-security true --exclude-feature com -v false -c link -u link -b true</AdditionalMonoLinkerOptions>
66

77
<_BlazorJsPath Condition="'$(_BlazorJsPath)' == ''">$(MSBuildThisFileDirectory)..\tools\blazor\blazor.webassembly.js</_BlazorJsPath>
8-
<_BaseBlazorDistPath>dist\</_BaseBlazorDistPath>
9-
<_BaseBlazorRuntimeOutputPath>$(_BaseBlazorDistPath)_framework\</_BaseBlazorRuntimeOutputPath>
8+
<_BaseBlazorRuntimeOutputPath>_framework\</_BaseBlazorRuntimeOutputPath>
109
<_BlazorRuntimeBinOutputPath>$(_BaseBlazorRuntimeOutputPath)_bin\</_BlazorRuntimeBinOutputPath>
1110
<_BlazorRuntimeWasmOutputPath>$(_BaseBlazorRuntimeOutputPath)wasm\</_BlazorRuntimeWasmOutputPath>
1211
<_BlazorBuiltInBclLinkerDescriptor>$(MSBuildThisFileDirectory)BuiltInBclLinkerDescriptor.xml</_BlazorBuiltInBclLinkerDescriptor>

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

Lines changed: 40 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -50,14 +50,14 @@
5050
-->
5151
<ItemGroup>
5252
<!-- Assemblies from packages -->
53-
<_BlazorManagedRuntimeAssemby Include="@(RuntimeCopyLocalItems)" />
53+
<_BlazorManagedRuntimeAssembly Include="@(RuntimeCopyLocalItems)" />
5454

5555
<!-- Assemblies from other references -->
5656
<_BlazorUserRuntimeAssembly Include="@(ReferencePath->WithMetadataValue('CopyLocal', 'true'))" />
5757
<_BlazorUserRuntimeAssembly Include="@(ReferenceDependencyPaths->WithMetadataValue('CopyLocal', 'true'))" />
5858

59-
<_BlazorManagedRuntimeAssemby Include="@(_BlazorUserRuntimeAssembly)" />
60-
<_BlazorManagedRuntimeAssemby Include="@(IntermediateAssembly)" />
59+
<_BlazorManagedRuntimeAssembly Include="@(_BlazorUserRuntimeAssembly)" />
60+
<_BlazorManagedRuntimeAssembly Include="@(IntermediateAssembly)" />
6161
</ItemGroup>
6262

6363
<MakeDir Directories="$(_BlazorIntermediateOutputPath)" />
@@ -70,73 +70,68 @@
7070
satellite assemblies, this should include all assemblies needed to run the application.
7171
-->
7272
<ItemGroup>
73+
<_BlazorJSFile Include="$(_BlazorJSPath)" />
74+
<_BlazorJSFile Include="$(_BlazorJSMapPath)" Condition="Exists('$(_BlazorJSMapPath)')" />
75+
<_DotNetWasmRuntimeFile Include="$(ComponentsWebAssemblyRuntimePath)*" />
76+
7377
<!--
7478
ReferenceCopyLocalPaths includes all files that are part of the build out with CopyLocalLockFileAssemblies on.
7579
Remove assemblies that are inputs to calculating the assembly closure. Instead use the resolved outputs, since it is the minimal set.
80+
81+
ReferenceCopyLocalPaths also includes satellite assemblies from referenced projects but are inexpicably missing
82+
any metadata that might allow them to be differentiated. We'll explicitly add those
83+
to _BlazorOutputWithTargetPath so that satellite assemblies from packages, the current project and referenced project
84+
are all treated the same.
7685
-->
77-
<_BlazorCopyLocalPaths Include="@(ReferenceCopyLocalPaths)" Condition="'%(Extension)' == '.dll'" />
78-
<_BlazorCopyLocalPaths Remove="@(_BlazorManagedRuntimeAssemby)" />
86+
<_BlazorCopyLocalPaths Include="@(ReferenceCopyLocalPaths)"
87+
Exclude="@(_BlazorManagedRuntimeAssembly);@(ReferenceSatellitePaths)"
88+
Condition="'%(Extension)' == '.dll'" />
89+
90+
<_BlazorCopyLocalPaths Include="@(IntermediateSatelliteAssembliesWithTargetPath)">
91+
<DestinationSubDirectory>%(IntermediateSatelliteAssembliesWithTargetPath.Culture)\</DestinationSubDirectory>
92+
</_BlazorCopyLocalPaths>
7993

8094
<_BlazorOutputWithTargetPath Include="@(_BlazorCopyLocalPaths)">
8195
<!-- This group is for satellite assemblies. We set the resource name to include a path, e.g. "fr\\SomeAssembly.resources.dll" -->
82-
<BootManifestResourceType Condition="'%(Extension)' == '.dll'">assembly</BootManifestResourceType>
83-
<BootManifestResourceType Condition="'%(Extension)' == '.pdb'">pdb</BootManifestResourceType>
96+
<BootManifestResourceType Condition="'%(_BlazorCopyLocalPaths.Culture)' == '' AND '%(_BlazorCopyLocalPaths.Extension)' == '.dll'">assembly</BootManifestResourceType>
97+
<BootManifestResourceType Condition="'%(_BlazorCopyLocalPaths.Culture)' != '' AND '%(_BlazorCopyLocalPaths.Extension)' == '.dll'">satellite</BootManifestResourceType>
8498
<BootManifestResourceName>%(_BlazorCopyLocalPaths.DestinationSubDirectory)%(FileName)%(Extension)</BootManifestResourceName>
8599
<TargetOutputPath>$(_BlazorRuntimeBinOutputPath)%(_BlazorCopyLocalPaths.DestinationSubDirectory)%(FileName)%(Extension)</TargetOutputPath>
86100
</_BlazorOutputWithTargetPath>
87101

88-
<_BlazorOutputWithTargetPath Include="@(_BlazorResolvedAssembly)">
89-
<BootManifestResourceType Condition="'%(Extension)' == '.dll'">assembly</BootManifestResourceType>
90-
<BootManifestResourceType Condition="'%(Extension)' == '.pdb'">pdb</BootManifestResourceType>
91-
<BootManifestResourceName>%(FileName)%(Extension)</BootManifestResourceName>
92-
<TargetOutputPath>$(_BlazorRuntimeBinOutputPath)%(FileName)%(Extension)</TargetOutputPath>
102+
<_BlazorOutputWithTargetPath Include="@(ReferenceSatellitePaths)">
103+
<Culture>$([System.String]::Copy('%(ReferenceSatellitePaths.DestinationSubDirectory)').Trim('\').Trim('/'))</Culture>
104+
<BootManifestResourceType>satellite</BootManifestResourceType>
105+
<BootManifestResourceName>%(ReferenceSatellitePaths.DestinationSubDirectory)%(FileName)%(Extension)</BootManifestResourceName>
106+
<TargetOutputPath>$(_BlazorRuntimeBinOutputPath)%(ReferenceSatellitePaths.DestinationSubDirectory)%(FileName)%(Extension)</TargetOutputPath>
93107
</_BlazorOutputWithTargetPath>
94-
</ItemGroup>
95108

96-
<!--
97-
We need to know at build time (not publish time) whether or not to include pdbs in the
98-
blazor.boot.json file, so this is controlled by the BlazorEnableDebugging flag, whose
99-
default value is determined by the build configuration.
100-
-->
101-
<ItemGroup Condition="'$(BlazorEnableDebugging)' != 'true'">
102-
<_BlazorOutputWithTargetPath Remove="@(_BlazorOutputWithTargetPath)" Condition="'%(Extension)' == '.pdb'" />
103-
</ItemGroup>
104-
105-
<!--
106-
The following itemgroup attempts to extend the set to include satellite assemblies.
107-
The mechanism behind this (or whether it's correct) is a bit unclear so
108-
https://github.com/dotnet/aspnetcore/issues/18951 tracks the need for follow-up.
109-
-->
110-
<ItemGroup>
111-
<!--
112-
ReferenceCopyLocalPaths includes all files that are part of the build out with CopyLocalLockFileAssemblies on.
113-
Remove assemblies that are inputs to calculating the assembly closure. Instead use the resolved outputs, since it is the minimal set.
114-
-->
115-
<_BlazorCopyLocalPaths Include="@(ReferenceCopyLocalPaths)" Condition="'%(Extension)' == '.dll'" />
116-
<_BlazorCopyLocalPaths Remove="@(_BlazorManagedRuntimeAssemby)" Condition="'%(Extension)' == '.dll'" />
117-
118-
<_BlazorOutputWithTargetPath Include="@(_BlazorCopyLocalPaths)">
109+
<_BlazorOutputWithTargetPath Include="@(_BlazorResolvedAssembly)">
119110
<BootManifestResourceType Condition="'%(Extension)' == '.dll'">assembly</BootManifestResourceType>
120-
<BootManifestResourceType Condition="'%(Extension)' == '.pdb'">pdb</BootManifestResourceType>
111+
<BootManifestResourceType Condition="'%(_BlazorCopyLocalPaths.Extension)' == '.pdb'">pdb</BootManifestResourceType>
121112
<BootManifestResourceName>%(_BlazorCopyLocalPaths.DestinationSubDirectory)%(FileName)%(Extension)</BootManifestResourceName>
122113
<TargetOutputPath>$(_BlazorRuntimeBinOutputPath)%(_BlazorCopyLocalPaths.DestinationSubDirectory)%(FileName)%(Extension)</TargetOutputPath>
123114
</_BlazorOutputWithTargetPath>
124-
</ItemGroup>
125115

126-
<ItemGroup>
127-
<_DotNetWasmRuntimeFile Include="$(ComponentsWebAssemblyRuntimePath)*" />
128116
<_BlazorOutputWithTargetPath Include="@(_DotNetWasmRuntimeFile)">
129117
<TargetOutputPath>$(_BlazorRuntimeWasmOutputPath)%(FileName)%(Extension)</TargetOutputPath>
130118
<BootManifestResourceType>runtime</BootManifestResourceType>
131119
<BootManifestResourceName>%(FileName)%(Extension)</BootManifestResourceName>
132120
</_BlazorOutputWithTargetPath>
133121

134-
<_BlazorJSFile Include="$(_BlazorJSPath)" />
135-
<_BlazorJSFile Include="$(_BlazorJSMapPath)" Condition="Exists('$(_BlazorJSMapPath)')" />
136122
<_BlazorOutputWithTargetPath Include="@(_BlazorJSFile)">
137123
<TargetOutputPath>$(_BaseBlazorRuntimeOutputPath)%(FileName)%(Extension)</TargetOutputPath>
138124
</_BlazorOutputWithTargetPath>
139125
</ItemGroup>
126+
127+
<!--
128+
We need to know at build time (not publish time) whether or not to include pdbs in the
129+
blazor.boot.json file, so this is controlled by the BlazorEnableDebugging flag, whose
130+
default value is determined by the build configuration.
131+
-->
132+
<ItemGroup Condition="'$(BlazorEnableDebugging)' != 'true'">
133+
<_BlazorOutputWithTargetPath Remove="@(_BlazorOutputWithTargetPath)" Condition="'%(Extension)' == '.pdb'" />
134+
</ItemGroup>
140135
</Target>
141136

142137
<!--
@@ -218,7 +213,7 @@
218213
<Target
219214
Name="_LinkBlazorApplication"
220215
Inputs="$(ProjectAssetsFile);
221-
@(_BlazorManagedRuntimeAssemby);
216+
@(_BlazorManagedRuntimeAssembly);
222217
@(BlazorLinkerDescriptor);
223218
$(MSBuildAllProjects)"
224219
Outputs="$(_BlazorLinkerOutputCache)">
@@ -277,7 +272,7 @@
277272
Name="_ResolveBlazorRuntimeDependencies"
278273
Inputs="$(ProjectAssetsFile);
279274
@(IntermediateAssembly);
280-
@(_BlazorManagedRuntimeAssemby)"
275+
@(_BlazorManagedRuntimeAssembly)"
281276
Outputs="$(_BlazorApplicationAssembliesCacheFile)">
282277

283278
<!--
@@ -287,7 +282,7 @@
287282
-->
288283
<ResolveBlazorRuntimeDependencies
289284
EntryPoint="@(IntermediateAssembly)"
290-
ApplicationDependencies="@(_BlazorManagedRuntimeAssemby)"
285+
ApplicationDependencies="@(_BlazorManagedRuntimeAssembly)"
291286
WebAssemblyBCLAssemblies="@(_WebAssemblyBCLAssembly)">
292287

293288
<Output TaskParameter="Dependencies" ItemName="_BlazorResolvedAssembly" />

0 commit comments

Comments
 (0)