Ensure satellite resources are published when building in VS (#21347)

* Ensure satellite resources are published when building in VS

Fixes https://github.com/dotnet/aspnetcore/issues/21355
This commit is contained in:
Pranav K 2020-04-30 14:57:04 -07:00 committed by GitHub
parent 4438f33304
commit 147f5758c6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 431 additions and 1 deletions

View File

@ -0,0 +1,38 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.Linq;
using System.Xml.Linq;
using Microsoft.Build.Framework;
using Microsoft.Build.Utilities;
namespace Microsoft.AspNetCore.Components.WebAssembly.Build
{
public class BlazorReadSatelliteAssemblyFile : Task
{
[Output]
public ITaskItem[] SatelliteAssembly { get; set; }
[Required]
public ITaskItem ReadFile { get; set; }
public override bool Execute()
{
var document = XDocument.Load(ReadFile.ItemSpec);
SatelliteAssembly = document.Root
.Elements()
.Select(e =>
{
// <Assembly Name="..." Culture="..." DestinationSubDirectory="..." />
var taskItem = new TaskItem(e.Attribute("Name").Value);
taskItem.SetMetadata("Culture", e.Attribute("Culture").Value);
taskItem.SetMetadata("DestinationSubDirectory", e.Attribute("DestinationSubDirectory").Value);
return taskItem;
}).ToArray();
return true;
}
}
}

View File

@ -0,0 +1,53 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.IO;
using System.Xml;
using System.Xml.Linq;
using Microsoft.Build.Framework;
using Microsoft.Build.Utilities;
namespace Microsoft.AspNetCore.Components.WebAssembly.Build
{
public class BlazorWriteSatelliteAssemblyFile : Task
{
[Required]
public ITaskItem[] SatelliteAssembly { get; set; }
[Required]
public ITaskItem WriteFile { get; set; }
public override bool Execute()
{
using var fileStream = File.Create(WriteFile.ItemSpec);
WriteSatelliteAssemblyFile(fileStream);
return true;
}
internal void WriteSatelliteAssemblyFile(Stream stream)
{
var root = new XElement("SatelliteAssembly");
foreach (var item in SatelliteAssembly)
{
// <Assembly Name="..." Culture="..." DestinationSubDirectory="..." />
root.Add(new XElement("Assembly",
new XAttribute("Name", item.ItemSpec),
new XAttribute("Culture", item.GetMetadata("Culture")),
new XAttribute("DestinationSubDirectory", item.GetMetadata("DestinationSubDirectory"))));
}
var xmlWriterSettings = new XmlWriterSettings
{
Indent = true,
OmitXmlDeclaration = true
};
using var writer = XmlWriter.Create(stream, xmlWriterSettings);
var xDocument = new XDocument(root);
xDocument.Save(writer);
}
}
}

View File

@ -75,6 +75,9 @@
<MakeDir Directories="$(_BlazorIntermediateOutputPath)" />
</Target>
<UsingTask TaskName="BlazorWriteSatelliteAssemblyFile" AssemblyFile="$(_BlazorTasksPath)" />
<UsingTask TaskName="BlazorReadSatelliteAssemblyFile" AssemblyFile="$(_BlazorTasksPath)" />
<Target Name="_ResolveBlazorOutputs" DependsOnTargets="_ResolveBlazorOutputsWhenLinked;_ResolveBlazorOutputsWhenNotLinked">
<!--
These are the items calculated as the closure of the runtime assemblies, either by calling the linker
@ -140,6 +143,45 @@
<TargetOutputPath>$(_BaseBlazorRuntimeOutputPath)%(FileName)%(Extension)</TargetOutputPath>
</_BlazorOutputWithTargetPath>
<_BlazorWriteSatelliteAssembly Include="@(_BlazorOutputWithTargetPath->WithMetadataValue('BootManifestResourceType', 'satellite'))" />
</ItemGroup>
<!--
When building with BuildingProject=false, satellite assemblies do not get resolved (the ones for the current project and the one for
referenced project). BuildingProject=false is typically set for referenced projects when building inside VisualStudio.
To workaround this, we'll stash metadata during a regular build, and rehydrate from it when BuildingProject=false.
-->
<PropertyGroup>
<_BlazorSatelliteAssemblyStashFile>$(_BlazorIntermediateOutputPath)blazor.satelliteasm.props</_BlazorSatelliteAssemblyStashFile>
</PropertyGroup>
<BlazorWriteSatelliteAssemblyFile
SatelliteAssembly="@(_BlazorWriteSatelliteAssembly)"
WriteFile="$(_BlazorSatelliteAssemblyStashFile)"
Condition="'$(BuildingProject)' == 'true' AND '@(_BlazorWriteSatelliteAssembly->Count())' != '0'" />
<Delete
Files="$(_BlazorSatelliteAssemblyStashFile)"
Condition="'$(BuildingProject)' == 'true' AND '@(_BlazorWriteSatelliteAssembly->Count())' == '0' and EXISTS('$(_BlazorSatelliteAssemblyStashFile)')" />
<BlazorReadSatelliteAssemblyFile
ReadFile="$(_BlazorSatelliteAssemblyStashFile)"
Condition="'$(BuildingProject)' != 'true' AND EXISTS('$(_BlazorSatelliteAssemblyStashFile)')">
<Output TaskParameter="SatelliteAssembly" ItemName="_BlazorReadSatelliteAssembly" />
</BlazorReadSatelliteAssemblyFile>
<ItemGroup>
<FileWrites Include="$(_BlazorSatelliteAssemblyStashFile)" Condition="Exists('$(_BlazorSatelliteAssemblyStashFile)')" />
</ItemGroup>
<ItemGroup Condition="'@(_BlazorReadSatelliteAssembly->Count())' != '0'">
<!-- We've imported a previously stashed file. Let's turn in to a _BlazorOutputWithTargetPath -->
<_BlazorOutputWithTargetPath Include="@(_BlazorReadSatelliteAssembly)">
<BootManifestResourceType>satellite</BootManifestResourceType>
<BootManifestResourceName>%(_BlazorReadSatelliteAssembly.DestinationSubDirectory)%(FileName)%(Extension)</BootManifestResourceName>
<TargetOutputPath>$(_BlazorRuntimeBinOutputPath)%(_BlazorReadSatelliteAssembly.DestinationSubDirectory)%(FileName)%(Extension)</TargetOutputPath>
</_BlazorOutputWithTargetPath>
</ItemGroup>
<!--

View File

@ -0,0 +1,69 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.Collections.Generic;
using System.IO;
using System.Xml.Linq;
using Microsoft.Build.Framework;
using Microsoft.Build.Utilities;
using Moq;
using Xunit;
namespace Microsoft.AspNetCore.Components.WebAssembly.Build
{
public class BlazorReadSatelliteAssemblyFileTest
{
[Fact]
public void WritesAndReadsRoundTrip()
{
// Arrange/Act
var tempFile = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName());
var writer = new BlazorWriteSatelliteAssemblyFile
{
BuildEngine = Mock.Of<IBuildEngine>(),
WriteFile = new TaskItem(tempFile),
SatelliteAssembly = new[]
{
new TaskItem("Resources.fr.dll", new Dictionary<string, string>
{
["Culture"] = "fr",
["DestinationSubDirectory"] = "fr\\",
}),
new TaskItem("Resources.ja-jp.dll", new Dictionary<string, string>
{
["Culture"] = "ja-jp",
["DestinationSubDirectory"] = "ja-jp\\",
}),
},
};
var reader = new BlazorReadSatelliteAssemblyFile
{
BuildEngine = Mock.Of<IBuildEngine>(),
ReadFile = new TaskItem(tempFile),
};
writer.Execute();
Assert.True(File.Exists(tempFile), "Write should have succeeded.");
reader.Execute();
Assert.Collection(
reader.SatelliteAssembly,
assembly =>
{
Assert.Equal("Resources.fr.dll", assembly.ItemSpec);
Assert.Equal("fr", assembly.GetMetadata("Culture"));
Assert.Equal("fr\\", assembly.GetMetadata("DestinationSubDirectory"));
},
assembly =>
{
Assert.Equal("Resources.ja-jp.dll", assembly.ItemSpec);
Assert.Equal("ja-jp", assembly.GetMetadata("Culture"));
Assert.Equal("ja-jp\\", assembly.GetMetadata("DestinationSubDirectory"));
});
}
}
}

View File

@ -1,6 +1,8 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.IO;
using System.Text.Json;
using System.Threading.Tasks;
using Xunit;
@ -36,5 +38,122 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Build
}
}
}
[Fact]
public async Task Build_SatelliteAssembliesFileIsPreserved()
{
// Arrange
using var project = ProjectDirectory.Create("standalone", additionalProjects: new[] { "razorclasslibrary" });
File.Move(Path.Combine(project.DirectoryPath, "Resources.ja.resx.txt"), Path.Combine(project.DirectoryPath, "Resource.ja.resx"));
var result = await MSBuildProcessManager.DotnetMSBuild(project);
Assert.BuildPassed(result);
var satelliteAssemblyCacheFile = Path.Combine(project.IntermediateOutputDirectory, "blazor", "blazor.satelliteasm.props");
var satelliteAssemblyFile = Path.Combine(project.BuildOutputDirectory, "wwwroot", "_framework", "_bin", "ja", "standalone.resources.dll");
var bootJson = Path.Combine(project.DirectoryPath, project.BuildOutputDirectory, "wwwroot", "_framework", "blazor.boot.json");
// Assert
for (var i = 0; i < 3; i++)
{
result = await MSBuildProcessManager.DotnetMSBuild(project);
Assert.BuildPassed(result);
Verify();
}
// Assert - incremental builds with BuildingProject=false
for (var i = 0; i < 3; i++)
{
result = await MSBuildProcessManager.DotnetMSBuild(project, args: "/p:BuildingProject=false");
Assert.BuildPassed(result);
Verify();
}
void Verify()
{
Assert.FileExists(result, satelliteAssemblyCacheFile);
Assert.FileExists(result, satelliteAssemblyFile);
var bootJsonFile = JsonSerializer.Deserialize<GenerateBlazorBootJson.BootJsonData>(File.ReadAllText(bootJson), new JsonSerializerOptions { PropertyNameCaseInsensitive = true });
var satelliteResources = bootJsonFile.resources.satelliteResources;
var kvp = Assert.Single(satelliteResources);
Assert.Equal("ja", kvp.Key);
Assert.Equal("ja/standalone.resources.dll", Assert.Single(kvp.Value).Key);
}
}
[Fact]
public async Task Build_SatelliteAssembliesFileIsCreated_IfNewFileIsAdded()
{
// Arrange
using var project = ProjectDirectory.Create("standalone", additionalProjects: new[] { "razorclasslibrary" });
var result = await MSBuildProcessManager.DotnetMSBuild(project);
Assert.BuildPassed(result);
var satelliteAssemblyCacheFile = Path.Combine(project.IntermediateOutputDirectory, "blazor", "blazor.satelliteasm.props");
var satelliteAssemblyFile = Path.Combine(project.BuildOutputDirectory, "wwwroot", "_framework", "_bin", "ja", "standalone.resources.dll");
var bootJson = Path.Combine(project.DirectoryPath, project.BuildOutputDirectory, "wwwroot", "_framework", "blazor.boot.json");
result = await MSBuildProcessManager.DotnetMSBuild(project);
Assert.BuildPassed(result);
Assert.FileDoesNotExist(result, satelliteAssemblyCacheFile);
Assert.FileDoesNotExist(result, satelliteAssemblyFile);
var bootJsonFile = JsonSerializer.Deserialize<GenerateBlazorBootJson.BootJsonData>(File.ReadAllText(bootJson), new JsonSerializerOptions { PropertyNameCaseInsensitive = true });
var satelliteResources = bootJsonFile.resources.satelliteResources;
Assert.Null(satelliteResources);
File.Move(Path.Combine(project.DirectoryPath, "Resources.ja.resx.txt"), Path.Combine(project.DirectoryPath, "Resource.ja.resx"));
result = await MSBuildProcessManager.DotnetMSBuild(project);
Assert.BuildPassed(result);
Assert.FileExists(result, satelliteAssemblyCacheFile);
Assert.FileExists(result, satelliteAssemblyFile);
bootJsonFile = JsonSerializer.Deserialize<GenerateBlazorBootJson.BootJsonData>(File.ReadAllText(bootJson), new JsonSerializerOptions { PropertyNameCaseInsensitive = true });
satelliteResources = bootJsonFile.resources.satelliteResources;
var kvp = Assert.Single(satelliteResources);
Assert.Equal("ja", kvp.Key);
Assert.Equal("ja/standalone.resources.dll", Assert.Single(kvp.Value).Key);
}
[Fact]
public async Task Build_SatelliteAssembliesFileIsDeleted_IfAllSatelliteFilesAreRemoved()
{
// Arrange
using var project = ProjectDirectory.Create("standalone", additionalProjects: new[] { "razorclasslibrary" });
File.Move(Path.Combine(project.DirectoryPath, "Resources.ja.resx.txt"), Path.Combine(project.DirectoryPath, "Resource.ja.resx"));
var result = await MSBuildProcessManager.DotnetMSBuild(project);
Assert.BuildPassed(result);
var satelliteAssemblyCacheFile = Path.Combine(project.IntermediateOutputDirectory, "blazor", "blazor.satelliteasm.props");
var satelliteAssemblyFile = Path.Combine(project.BuildOutputDirectory, "wwwroot", "_framework", "_bin", "ja", "standalone.resources.dll");
var bootJson = Path.Combine(project.DirectoryPath, project.BuildOutputDirectory, "wwwroot", "_framework", "blazor.boot.json");
result = await MSBuildProcessManager.DotnetMSBuild(project);
Assert.BuildPassed(result);
Assert.FileExists(result, satelliteAssemblyCacheFile);
Assert.FileExists(result, satelliteAssemblyFile);
var bootJsonFile = JsonSerializer.Deserialize<GenerateBlazorBootJson.BootJsonData>(File.ReadAllText(bootJson), new JsonSerializerOptions { PropertyNameCaseInsensitive = true });
var satelliteResources = bootJsonFile.resources.satelliteResources;
var kvp = Assert.Single(satelliteResources);
Assert.Equal("ja", kvp.Key);
Assert.Equal("ja/standalone.resources.dll", Assert.Single(kvp.Value).Key);
File.Delete(Path.Combine(project.DirectoryPath, "Resource.ja.resx"));
result = await MSBuildProcessManager.DotnetMSBuild(project);
Assert.BuildPassed(result);
Assert.FileDoesNotExist(result, satelliteAssemblyCacheFile);
bootJsonFile = JsonSerializer.Deserialize<GenerateBlazorBootJson.BootJsonData>(File.ReadAllText(bootJson), new JsonSerializerOptions { PropertyNameCaseInsensitive = true });
satelliteResources = bootJsonFile.resources.satelliteResources;
Assert.Null(satelliteResources);
}
}
}

View File

@ -419,7 +419,6 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Build
Assert.FileExists(result, blazorPublishDirectory, "index.html");
// Verify static web assets from referenced projects are copied.
// Uncomment once https://github.com/aspnet/AspNetCore/issues/17426 is resolved.
Assert.FileExists(result, publishDirectory, "wwwroot", "_content", "RazorClassLibrary", "wwwroot", "exampleJsInterop.js");
Assert.FileExists(result, publishDirectory, "wwwroot", "_content", "RazorClassLibrary", "styles.css");
@ -433,6 +432,109 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Build
assetsManifestPath: "custom-service-worker-assets.js");
}
[Fact]
public async Task Publish_HostedApp_VisualStudio()
{
// Simulates publishing the same way VS does by setting BuildProjectReferences=false.
// Arrange
using var project = ProjectDirectory.Create("blazorhosted", additionalProjects: new[] { "standalone", "razorclasslibrary", });
project.TargetFramework = "netcoreapp3.1";
project.Configuration = "Release";
var result = await MSBuildProcessManager.DotnetMSBuild(project, "Build", "/p:BuildInsideVisualStudio=true");
Assert.BuildPassed(result);
result = await MSBuildProcessManager.DotnetMSBuild(project, "Publish", "/p:BuildProjectReferences=false /p:BuildInsideVisualStudio=true");
var publishDirectory = project.PublishOutputDirectory;
// Make sure the main project exists
Assert.FileExists(result, publishDirectory, "blazorhosted.dll");
var blazorPublishDirectory = Path.Combine(publishDirectory, "wwwroot");
Assert.FileExists(result, blazorPublishDirectory, "_framework", "blazor.boot.json");
Assert.FileExists(result, blazorPublishDirectory, "_framework", "blazor.webassembly.js");
Assert.FileExists(result, blazorPublishDirectory, "_framework", "wasm", "dotnet.wasm");
Assert.FileExists(result, blazorPublishDirectory, "_framework", "wasm", DotNetJsFileName);
Assert.FileExists(result, blazorPublishDirectory, "_framework", "_bin", "standalone.dll");
Assert.FileExists(result, blazorPublishDirectory, "_framework", "_bin", "Microsoft.Extensions.Logging.Abstractions.dll"); // Verify dependencies are part of the output.
// Verify static assets are in the publish directory
Assert.FileExists(result, blazorPublishDirectory, "index.html");
// Verify static web assets from referenced projects are copied.
Assert.FileExists(result, publishDirectory, "wwwroot", "_content", "RazorClassLibrary", "wwwroot", "exampleJsInterop.js");
Assert.FileExists(result, publishDirectory, "wwwroot", "_content", "RazorClassLibrary", "styles.css");
// Verify web.config
Assert.FileExists(result, publishDirectory, "web.config");
VerifyBootManifestHashes(result, blazorPublishDirectory);
VerifyServiceWorkerFiles(result, blazorPublishDirectory,
serviceWorkerPath: Path.Combine("serviceworkers", "my-service-worker.js"),
serviceWorkerContent: "// This is the production service worker",
assetsManifestPath: "custom-service-worker-assets.js");
}
// Regression test to verify satellite assemblies from the blazor app are copied to the published app's wwwroot output directory as
// part of publishing in VS
[Fact]
public async Task Publish_HostedApp_VisualStudio_WithSatelliteAssemblies()
{
// Simulates publishing the same way VS does by setting BuildProjectReferences=false.
// Arrange
var project = ProjectDirectory.Create("blazorhosted", additionalProjects: new[] { "standalone", "razorclasslibrary", "classlibrarywithsatelliteassemblies" });
project.TargetFramework = "netcoreapp3.1";
project.Configuration = "Release";
var standaloneProjectDirectory = Path.Combine(project.DirectoryPath, "..", "standalone");
// Setup the standalone app to have it's own satellite assemblies as well as transitively imported satellite assemblies.
var resxfileInProject = Path.Combine(standaloneProjectDirectory , "Resources.ja.resx.txt");
File.Move(resxfileInProject, Path.Combine(standaloneProjectDirectory, "Resource.ja.resx"));
var standaloneProjFile = Path.Combine(standaloneProjectDirectory, "standalone.csproj");
var existing = File.ReadAllText(standaloneProjFile);
var updatedContent = @"
<PropertyGroup>
<DefineConstants>$(DefineConstants);REFERENCE_classlibrarywithsatelliteassemblies</DefineConstants>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include=""..\classlibrarywithsatelliteassemblies\classlibrarywithsatelliteassemblies.csproj"" />
</ItemGroup>";
var updated = existing.Replace("<!-- Test Placeholder -->", updatedContent);
File.WriteAllText(standaloneProjFile, updated);
var result = await MSBuildProcessManager.DotnetMSBuild(project, "Build", $"/restore");
Assert.BuildPassed(result);
result = await MSBuildProcessManager.DotnetMSBuild(project, "Publish", "/p:BuildProjectReferences=false");
var publishDirectory = project.PublishOutputDirectory;
// Make sure the main project exists
Assert.FileExists(result, publishDirectory, "blazorhosted.dll");
var blazorPublishDirectory = Path.Combine(publishDirectory, "wwwroot");
Assert.FileExists(result, blazorPublishDirectory, "_framework", "blazor.boot.json");
Assert.FileExists(result, blazorPublishDirectory, "_framework", "_bin", "ja", "standalone.resources.dll");
Assert.FileExists(result, blazorPublishDirectory, "_framework", "_bin", "fr", "Microsoft.CodeAnalysis.resources.dll");
var bootJson = Path.Combine(project.DirectoryPath, blazorPublishDirectory, "_framework", "blazor.boot.json");
var bootJsonFile = JsonSerializer.Deserialize<GenerateBlazorBootJson.BootJsonData>(File.ReadAllText(bootJson), new JsonSerializerOptions { PropertyNameCaseInsensitive = true });
var satelliteResources = bootJsonFile.resources.satelliteResources;
Assert.Contains("es-ES", satelliteResources.Keys);
Assert.Contains("es-ES/classlibrarywithsatelliteassemblies.resources.dll", satelliteResources["es-ES"].Keys);
Assert.Contains("fr", satelliteResources.Keys);
Assert.Contains("fr/Microsoft.CodeAnalysis.CSharp.resources.dll", satelliteResources["fr"].Keys);
Assert.Contains("ja", satelliteResources.Keys);
Assert.Contains("ja/standalone.resources.dll", satelliteResources["ja"].Keys);
VerifyServiceWorkerFiles(result, blazorPublishDirectory,
serviceWorkerPath: Path.Combine("serviceworkers", "my-service-worker.js"),
serviceWorkerContent: "// This is the production service worker",
assetsManifestPath: "custom-service-worker-assets.js");
}
private static void AddSiblingProjectFileContent(ProjectDirectory project, string content)
{
var path = Path.Combine(project.SolutionPath, "standalone", "standalone.csproj");
@ -543,5 +645,12 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Build
var json = jsContents.Substring(jsonStart, jsonLength);
return JsonSerializer.Deserialize<GenerateServiceWorkerAssetsManifest.AssetsManifestFile>(json);
}
private static GenerateBlazorBootJson.BootJsonData ReadBootJsonData(MSBuildResult result, string path)
{
return JsonSerializer.Deserialize<GenerateBlazorBootJson.BootJsonData>(
File.ReadAllText(Path.Combine(result.Project.DirectoryPath, path)),
new JsonSerializerOptions { PropertyNameCaseInsensitive = true });
}
}
}