Skip to content

Commit 147f575

Browse files
authored
Ensure satellite resources are published when building in VS (#21347)
* Ensure satellite resources are published when building in VS Fixes #21355
1 parent 4438f33 commit 147f575

File tree

6 files changed

+431
-1
lines changed

6 files changed

+431
-1
lines changed
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
using System.Linq;
5+
using System.Xml.Linq;
6+
using Microsoft.Build.Framework;
7+
using Microsoft.Build.Utilities;
8+
9+
namespace Microsoft.AspNetCore.Components.WebAssembly.Build
10+
{
11+
public class BlazorReadSatelliteAssemblyFile : Task
12+
{
13+
[Output]
14+
public ITaskItem[] SatelliteAssembly { get; set; }
15+
16+
[Required]
17+
public ITaskItem ReadFile { get; set; }
18+
19+
public override bool Execute()
20+
{
21+
var document = XDocument.Load(ReadFile.ItemSpec);
22+
SatelliteAssembly = document.Root
23+
.Elements()
24+
.Select(e =>
25+
{
26+
// <Assembly Name="..." Culture="..." DestinationSubDirectory="..." />
27+
28+
var taskItem = new TaskItem(e.Attribute("Name").Value);
29+
taskItem.SetMetadata("Culture", e.Attribute("Culture").Value);
30+
taskItem.SetMetadata("DestinationSubDirectory", e.Attribute("DestinationSubDirectory").Value);
31+
32+
return taskItem;
33+
}).ToArray();
34+
35+
return true;
36+
}
37+
}
38+
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
using System.IO;
5+
using System.Xml;
6+
using System.Xml.Linq;
7+
using Microsoft.Build.Framework;
8+
using Microsoft.Build.Utilities;
9+
10+
namespace Microsoft.AspNetCore.Components.WebAssembly.Build
11+
{
12+
public class BlazorWriteSatelliteAssemblyFile : Task
13+
{
14+
[Required]
15+
public ITaskItem[] SatelliteAssembly { get; set; }
16+
17+
[Required]
18+
public ITaskItem WriteFile { get; set; }
19+
20+
public override bool Execute()
21+
{
22+
using var fileStream = File.Create(WriteFile.ItemSpec);
23+
WriteSatelliteAssemblyFile(fileStream);
24+
return true;
25+
}
26+
27+
internal void WriteSatelliteAssemblyFile(Stream stream)
28+
{
29+
var root = new XElement("SatelliteAssembly");
30+
31+
foreach (var item in SatelliteAssembly)
32+
{
33+
// <Assembly Name="..." Culture="..." DestinationSubDirectory="..." />
34+
35+
root.Add(new XElement("Assembly",
36+
new XAttribute("Name", item.ItemSpec),
37+
new XAttribute("Culture", item.GetMetadata("Culture")),
38+
new XAttribute("DestinationSubDirectory", item.GetMetadata("DestinationSubDirectory"))));
39+
}
40+
41+
var xmlWriterSettings = new XmlWriterSettings
42+
{
43+
Indent = true,
44+
OmitXmlDeclaration = true
45+
};
46+
47+
using var writer = XmlWriter.Create(stream, xmlWriterSettings);
48+
var xDocument = new XDocument(root);
49+
50+
xDocument.Save(writer);
51+
}
52+
}
53+
}

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

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,9 @@
7575
<MakeDir Directories="$(_BlazorIntermediateOutputPath)" />
7676
</Target>
7777

78+
<UsingTask TaskName="BlazorWriteSatelliteAssemblyFile" AssemblyFile="$(_BlazorTasksPath)" />
79+
<UsingTask TaskName="BlazorReadSatelliteAssemblyFile" AssemblyFile="$(_BlazorTasksPath)" />
80+
7881
<Target Name="_ResolveBlazorOutputs" DependsOnTargets="_ResolveBlazorOutputsWhenLinked;_ResolveBlazorOutputsWhenNotLinked">
7982
<!--
8083
These are the items calculated as the closure of the runtime assemblies, either by calling the linker
@@ -140,6 +143,45 @@
140143
<TargetOutputPath>$(_BaseBlazorRuntimeOutputPath)%(FileName)%(Extension)</TargetOutputPath>
141144
</_BlazorOutputWithTargetPath>
142145

146+
<_BlazorWriteSatelliteAssembly Include="@(_BlazorOutputWithTargetPath->WithMetadataValue('BootManifestResourceType', 'satellite'))" />
147+
</ItemGroup>
148+
149+
<!--
150+
When building with BuildingProject=false, satellite assemblies do not get resolved (the ones for the current project and the one for
151+
referenced project). BuildingProject=false is typically set for referenced projects when building inside VisualStudio.
152+
To workaround this, we'll stash metadata during a regular build, and rehydrate from it when BuildingProject=false.
153+
-->
154+
155+
<PropertyGroup>
156+
<_BlazorSatelliteAssemblyStashFile>$(_BlazorIntermediateOutputPath)blazor.satelliteasm.props</_BlazorSatelliteAssemblyStashFile>
157+
</PropertyGroup>
158+
159+
<BlazorWriteSatelliteAssemblyFile
160+
SatelliteAssembly="@(_BlazorWriteSatelliteAssembly)"
161+
WriteFile="$(_BlazorSatelliteAssemblyStashFile)"
162+
Condition="'$(BuildingProject)' == 'true' AND '@(_BlazorWriteSatelliteAssembly->Count())' != '0'" />
163+
164+
<Delete
165+
Files="$(_BlazorSatelliteAssemblyStashFile)"
166+
Condition="'$(BuildingProject)' == 'true' AND '@(_BlazorWriteSatelliteAssembly->Count())' == '0' and EXISTS('$(_BlazorSatelliteAssemblyStashFile)')" />
167+
168+
<BlazorReadSatelliteAssemblyFile
169+
ReadFile="$(_BlazorSatelliteAssemblyStashFile)"
170+
Condition="'$(BuildingProject)' != 'true' AND EXISTS('$(_BlazorSatelliteAssemblyStashFile)')">
171+
<Output TaskParameter="SatelliteAssembly" ItemName="_BlazorReadSatelliteAssembly" />
172+
</BlazorReadSatelliteAssemblyFile>
173+
174+
<ItemGroup>
175+
<FileWrites Include="$(_BlazorSatelliteAssemblyStashFile)" Condition="Exists('$(_BlazorSatelliteAssemblyStashFile)')" />
176+
</ItemGroup>
177+
178+
<ItemGroup Condition="'@(_BlazorReadSatelliteAssembly->Count())' != '0'">
179+
<!-- We've imported a previously stashed file. Let's turn in to a _BlazorOutputWithTargetPath -->
180+
<_BlazorOutputWithTargetPath Include="@(_BlazorReadSatelliteAssembly)">
181+
<BootManifestResourceType>satellite</BootManifestResourceType>
182+
<BootManifestResourceName>%(_BlazorReadSatelliteAssembly.DestinationSubDirectory)%(FileName)%(Extension)</BootManifestResourceName>
183+
<TargetOutputPath>$(_BlazorRuntimeBinOutputPath)%(_BlazorReadSatelliteAssembly.DestinationSubDirectory)%(FileName)%(Extension)</TargetOutputPath>
184+
</_BlazorOutputWithTargetPath>
143185
</ItemGroup>
144186

145187
<!--
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
using System.Collections.Generic;
5+
using System.IO;
6+
using System.Xml.Linq;
7+
using Microsoft.Build.Framework;
8+
using Microsoft.Build.Utilities;
9+
using Moq;
10+
using Xunit;
11+
12+
namespace Microsoft.AspNetCore.Components.WebAssembly.Build
13+
{
14+
public class BlazorReadSatelliteAssemblyFileTest
15+
{
16+
[Fact]
17+
public void WritesAndReadsRoundTrip()
18+
{
19+
// Arrange/Act
20+
var tempFile = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName());
21+
22+
var writer = new BlazorWriteSatelliteAssemblyFile
23+
{
24+
BuildEngine = Mock.Of<IBuildEngine>(),
25+
WriteFile = new TaskItem(tempFile),
26+
SatelliteAssembly = new[]
27+
{
28+
new TaskItem("Resources.fr.dll", new Dictionary<string, string>
29+
{
30+
["Culture"] = "fr",
31+
["DestinationSubDirectory"] = "fr\\",
32+
}),
33+
new TaskItem("Resources.ja-jp.dll", new Dictionary<string, string>
34+
{
35+
["Culture"] = "ja-jp",
36+
["DestinationSubDirectory"] = "ja-jp\\",
37+
}),
38+
},
39+
};
40+
41+
var reader = new BlazorReadSatelliteAssemblyFile
42+
{
43+
BuildEngine = Mock.Of<IBuildEngine>(),
44+
ReadFile = new TaskItem(tempFile),
45+
};
46+
47+
writer.Execute();
48+
49+
Assert.True(File.Exists(tempFile), "Write should have succeeded.");
50+
51+
reader.Execute();
52+
53+
Assert.Collection(
54+
reader.SatelliteAssembly,
55+
assembly =>
56+
{
57+
Assert.Equal("Resources.fr.dll", assembly.ItemSpec);
58+
Assert.Equal("fr", assembly.GetMetadata("Culture"));
59+
Assert.Equal("fr\\", assembly.GetMetadata("DestinationSubDirectory"));
60+
},
61+
assembly =>
62+
{
63+
Assert.Equal("Resources.ja-jp.dll", assembly.ItemSpec);
64+
Assert.Equal("ja-jp", assembly.GetMetadata("Culture"));
65+
Assert.Equal("ja-jp\\", assembly.GetMetadata("DestinationSubDirectory"));
66+
});
67+
}
68+
}
69+
}

src/Components/WebAssembly/Build/test/BuildIntegrationTests/BuildIncrementalismTest.cs

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
// Copyright (c) .NET Foundation. All rights reserved.
22
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
33

4+
using System.IO;
5+
using System.Text.Json;
46
using System.Threading.Tasks;
57
using Xunit;
68

@@ -36,5 +38,122 @@ public async Task Build_WithLinker_IsIncremental()
3638
}
3739
}
3840
}
41+
42+
[Fact]
43+
public async Task Build_SatelliteAssembliesFileIsPreserved()
44+
{
45+
// Arrange
46+
using var project = ProjectDirectory.Create("standalone", additionalProjects: new[] { "razorclasslibrary" });
47+
File.Move(Path.Combine(project.DirectoryPath, "Resources.ja.resx.txt"), Path.Combine(project.DirectoryPath, "Resource.ja.resx"));
48+
var result = await MSBuildProcessManager.DotnetMSBuild(project);
49+
50+
Assert.BuildPassed(result);
51+
52+
var satelliteAssemblyCacheFile = Path.Combine(project.IntermediateOutputDirectory, "blazor", "blazor.satelliteasm.props");
53+
var satelliteAssemblyFile = Path.Combine(project.BuildOutputDirectory, "wwwroot", "_framework", "_bin", "ja", "standalone.resources.dll");
54+
var bootJson = Path.Combine(project.DirectoryPath, project.BuildOutputDirectory, "wwwroot", "_framework", "blazor.boot.json");
55+
56+
// Assert
57+
for (var i = 0; i < 3; i++)
58+
{
59+
result = await MSBuildProcessManager.DotnetMSBuild(project);
60+
Assert.BuildPassed(result);
61+
62+
Verify();
63+
}
64+
65+
// Assert - incremental builds with BuildingProject=false
66+
for (var i = 0; i < 3; i++)
67+
{
68+
result = await MSBuildProcessManager.DotnetMSBuild(project, args: "/p:BuildingProject=false");
69+
Assert.BuildPassed(result);
70+
71+
Verify();
72+
}
73+
74+
void Verify()
75+
{
76+
Assert.FileExists(result, satelliteAssemblyCacheFile);
77+
Assert.FileExists(result, satelliteAssemblyFile);
78+
79+
var bootJsonFile = JsonSerializer.Deserialize<GenerateBlazorBootJson.BootJsonData>(File.ReadAllText(bootJson), new JsonSerializerOptions { PropertyNameCaseInsensitive = true });
80+
var satelliteResources = bootJsonFile.resources.satelliteResources;
81+
var kvp = Assert.Single(satelliteResources);
82+
Assert.Equal("ja", kvp.Key);
83+
Assert.Equal("ja/standalone.resources.dll", Assert.Single(kvp.Value).Key);
84+
}
85+
}
86+
87+
[Fact]
88+
public async Task Build_SatelliteAssembliesFileIsCreated_IfNewFileIsAdded()
89+
{
90+
// Arrange
91+
using var project = ProjectDirectory.Create("standalone", additionalProjects: new[] { "razorclasslibrary" });
92+
var result = await MSBuildProcessManager.DotnetMSBuild(project);
93+
94+
Assert.BuildPassed(result);
95+
96+
var satelliteAssemblyCacheFile = Path.Combine(project.IntermediateOutputDirectory, "blazor", "blazor.satelliteasm.props");
97+
var satelliteAssemblyFile = Path.Combine(project.BuildOutputDirectory, "wwwroot", "_framework", "_bin", "ja", "standalone.resources.dll");
98+
var bootJson = Path.Combine(project.DirectoryPath, project.BuildOutputDirectory, "wwwroot", "_framework", "blazor.boot.json");
99+
100+
result = await MSBuildProcessManager.DotnetMSBuild(project);
101+
Assert.BuildPassed(result);
102+
103+
Assert.FileDoesNotExist(result, satelliteAssemblyCacheFile);
104+
Assert.FileDoesNotExist(result, satelliteAssemblyFile);
105+
var bootJsonFile = JsonSerializer.Deserialize<GenerateBlazorBootJson.BootJsonData>(File.ReadAllText(bootJson), new JsonSerializerOptions { PropertyNameCaseInsensitive = true });
106+
var satelliteResources = bootJsonFile.resources.satelliteResources;
107+
Assert.Null(satelliteResources);
108+
109+
File.Move(Path.Combine(project.DirectoryPath, "Resources.ja.resx.txt"), Path.Combine(project.DirectoryPath, "Resource.ja.resx"));
110+
result = await MSBuildProcessManager.DotnetMSBuild(project);
111+
Assert.BuildPassed(result);
112+
113+
Assert.FileExists(result, satelliteAssemblyCacheFile);
114+
Assert.FileExists(result, satelliteAssemblyFile);
115+
bootJsonFile = JsonSerializer.Deserialize<GenerateBlazorBootJson.BootJsonData>(File.ReadAllText(bootJson), new JsonSerializerOptions { PropertyNameCaseInsensitive = true });
116+
satelliteResources = bootJsonFile.resources.satelliteResources;
117+
var kvp = Assert.Single(satelliteResources);
118+
Assert.Equal("ja", kvp.Key);
119+
Assert.Equal("ja/standalone.resources.dll", Assert.Single(kvp.Value).Key);
120+
}
121+
122+
[Fact]
123+
public async Task Build_SatelliteAssembliesFileIsDeleted_IfAllSatelliteFilesAreRemoved()
124+
{
125+
// Arrange
126+
using var project = ProjectDirectory.Create("standalone", additionalProjects: new[] { "razorclasslibrary" });
127+
File.Move(Path.Combine(project.DirectoryPath, "Resources.ja.resx.txt"), Path.Combine(project.DirectoryPath, "Resource.ja.resx"));
128+
129+
var result = await MSBuildProcessManager.DotnetMSBuild(project);
130+
131+
Assert.BuildPassed(result);
132+
133+
var satelliteAssemblyCacheFile = Path.Combine(project.IntermediateOutputDirectory, "blazor", "blazor.satelliteasm.props");
134+
var satelliteAssemblyFile = Path.Combine(project.BuildOutputDirectory, "wwwroot", "_framework", "_bin", "ja", "standalone.resources.dll");
135+
var bootJson = Path.Combine(project.DirectoryPath, project.BuildOutputDirectory, "wwwroot", "_framework", "blazor.boot.json");
136+
137+
result = await MSBuildProcessManager.DotnetMSBuild(project);
138+
Assert.BuildPassed(result);
139+
140+
Assert.FileExists(result, satelliteAssemblyCacheFile);
141+
Assert.FileExists(result, satelliteAssemblyFile);
142+
var bootJsonFile = JsonSerializer.Deserialize<GenerateBlazorBootJson.BootJsonData>(File.ReadAllText(bootJson), new JsonSerializerOptions { PropertyNameCaseInsensitive = true });
143+
var satelliteResources = bootJsonFile.resources.satelliteResources;
144+
var kvp = Assert.Single(satelliteResources);
145+
Assert.Equal("ja", kvp.Key);
146+
Assert.Equal("ja/standalone.resources.dll", Assert.Single(kvp.Value).Key);
147+
148+
149+
File.Delete(Path.Combine(project.DirectoryPath, "Resource.ja.resx"));
150+
result = await MSBuildProcessManager.DotnetMSBuild(project);
151+
Assert.BuildPassed(result);
152+
153+
Assert.FileDoesNotExist(result, satelliteAssemblyCacheFile);
154+
bootJsonFile = JsonSerializer.Deserialize<GenerateBlazorBootJson.BootJsonData>(File.ReadAllText(bootJson), new JsonSerializerOptions { PropertyNameCaseInsensitive = true });
155+
satelliteResources = bootJsonFile.resources.satelliteResources;
156+
Assert.Null(satelliteResources);
157+
}
39158
}
40159
}

0 commit comments

Comments
 (0)