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)" /> <MakeDir Directories="$(_BlazorIntermediateOutputPath)" />
</Target> </Target>
<UsingTask TaskName="BlazorWriteSatelliteAssemblyFile" AssemblyFile="$(_BlazorTasksPath)" />
<UsingTask TaskName="BlazorReadSatelliteAssemblyFile" AssemblyFile="$(_BlazorTasksPath)" />
<Target Name="_ResolveBlazorOutputs" DependsOnTargets="_ResolveBlazorOutputsWhenLinked;_ResolveBlazorOutputsWhenNotLinked"> <Target Name="_ResolveBlazorOutputs" DependsOnTargets="_ResolveBlazorOutputsWhenLinked;_ResolveBlazorOutputsWhenNotLinked">
<!-- <!--
These are the items calculated as the closure of the runtime assemblies, either by calling the linker 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> <TargetOutputPath>$(_BaseBlazorRuntimeOutputPath)%(FileName)%(Extension)</TargetOutputPath>
</_BlazorOutputWithTargetPath> </_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> </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. // 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. // 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 System.Threading.Tasks;
using Xunit; 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"); Assert.FileExists(result, blazorPublishDirectory, "index.html");
// Verify static web assets from referenced projects are copied. // 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", "wwwroot", "exampleJsInterop.js");
Assert.FileExists(result, publishDirectory, "wwwroot", "_content", "RazorClassLibrary", "styles.css"); 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"); 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) private static void AddSiblingProjectFileContent(ProjectDirectory project, string content)
{ {
var path = Path.Combine(project.SolutionPath, "standalone", "standalone.csproj"); 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); var json = jsContents.Substring(jsonStart, jsonLength);
return JsonSerializer.Deserialize<GenerateServiceWorkerAssetsManifest.AssetsManifestFile>(json); 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 });
}
} }
} }