diff --git a/documentation/specs/custom-cultures.md b/documentation/specs/custom-cultures.md new file mode 100644 index 00000000000..a3e683c25e4 --- /dev/null +++ b/documentation/specs/custom-cultures.md @@ -0,0 +1,42 @@ +# MSBuild Custom Cultures Support + +## Overview + +The `EnableCustomCulture` property provides an opt-in mechanism for handling custom culture-specific resources in MSBuild projects. This feature allows for greater control over which directories are treated as culture-specific resources during the build process. + +## Purpose + +In some projects, directory names that match culture name patterns might not actually be culture resources. This can cause issues with resource compilation and deployment. This feature flag enables: + +1. Control over whether custom culture detection is enabled +2. Fine-grained configuration of which directories should be excluded from culture-specific resource processing + +## Usage + +### Enabling the Feature + +To enable the custom cultures feature, set the `EnableCustomCulture` property `true`. + +```xml + + true + +``` + +### Excluding Specific Directories + +When the feature is enabled, you can specify directories that should not be treated as culture-specific resources using the `NonCultureResourceDirectories` property: + +```xml + + long;hash;temp + +``` + +In this example, directories named "long", "hash", or "temp" will not be processed as culture-specific resources and the assemblied inside of them will be skipped, even if their names match culture naming patterns. Globbing is not supported. + +## Additional Notes + +- This feature does not affect the standard resource handling for well-known cultures. +- The feature is designed to be backward compatible - existing projects without the feature flag will behave the same as before. +- Performance impact is minimal, as the exclusion check happens only during the resource discovery phase of the build. \ No newline at end of file diff --git a/documentation/wiki/ChangeWaves.md b/documentation/wiki/ChangeWaves.md index 6b88ed66097..833299ed296 100644 --- a/documentation/wiki/ChangeWaves.md +++ b/documentation/wiki/ChangeWaves.md @@ -26,7 +26,7 @@ A wave of features is set to "rotate out" (i.e. become standard functionality) t ### 17.14 - ~[.SLNX support - use the new parser for .sln and .slnx](https://github.com/dotnet/msbuild/pull/10836)~ reverted after compat problems discovered -- [Support custom culture in RAR](https://github.com/dotnet/msbuild/pull/11000) +- ~~[Support custom culture in RAR](https://github.com/dotnet/msbuild/pull/11000)~~ - see [11607](https://github.com/dotnet/msbuild/pull/11607) for details - [VS Telemetry](https://github.com/dotnet/msbuild/pull/11255) ### 17.12 diff --git a/eng/BootStrapMsBuild.targets b/eng/BootStrapMsBuild.targets index d4330ba658d..4789ffcec85 100644 --- a/eng/BootStrapMsBuild.targets +++ b/eng/BootStrapMsBuild.targets @@ -63,14 +63,27 @@ + + + + + + + + + + + - + diff --git a/eng/Versions.props b/eng/Versions.props index 161311613a2..95468f090ef 100644 --- a/eng/Versions.props +++ b/eng/Versions.props @@ -2,7 +2,7 @@ - 17.14.1release + 17.14.2release 17.13.9 15.1.0.0 preview diff --git a/src/BuildCheck.UnitTests/EndToEndTests.cs b/src/BuildCheck.UnitTests/EndToEndTests.cs index 4b5a516e649..08e3fccb43f 100644 --- a/src/BuildCheck.UnitTests/EndToEndTests.cs +++ b/src/BuildCheck.UnitTests/EndToEndTests.cs @@ -12,7 +12,6 @@ using Microsoft.Build.Shared; using Microsoft.Build.UnitTests; using Microsoft.Build.UnitTests.Shared; -using Microsoft.VisualStudio.TestPlatform.Utilities; using Shouldly; using Xunit; using Xunit.Abstractions; diff --git a/src/BuildCheck.UnitTests/TestAssets/EmbeddedResourceTest/EntryProject/EntryProject.csproj b/src/BuildCheck.UnitTests/TestAssets/EmbeddedResourceTest/EntryProject/EntryProject.csproj index 1ac36d043de..228409c378c 100644 --- a/src/BuildCheck.UnitTests/TestAssets/EmbeddedResourceTest/EntryProject/EntryProject.csproj +++ b/src/BuildCheck.UnitTests/TestAssets/EmbeddedResourceTest/EntryProject/EntryProject.csproj @@ -4,6 +4,7 @@ net9.0 enable enable + true diff --git a/src/BuildCheck.UnitTests/TestAssets/EmbeddedResourceTest/ReferencedProject/ReferencedProject.csproj b/src/BuildCheck.UnitTests/TestAssets/EmbeddedResourceTest/ReferencedProject/ReferencedProject.csproj index 4208181be80..c03ac65c696 100644 --- a/src/BuildCheck.UnitTests/TestAssets/EmbeddedResourceTest/ReferencedProject/ReferencedProject.csproj +++ b/src/BuildCheck.UnitTests/TestAssets/EmbeddedResourceTest/ReferencedProject/ReferencedProject.csproj @@ -5,6 +5,7 @@ net8.0 enable enable + true diff --git a/src/Framework/Traits.cs b/src/Framework/Traits.cs index bcdc4ac195c..a70ed22074f 100644 --- a/src/Framework/Traits.cs +++ b/src/Framework/Traits.cs @@ -7,11 +7,12 @@ namespace Microsoft.Build.Framework { /// - /// Represents toggleable features of the MSBuild engine + /// Represents toggleable features of the MSBuild engine. /// internal class Traits { private static Traits _instance = new Traits(); + public static Traits Instance { get @@ -132,7 +133,6 @@ public Traits() public readonly bool InProcNodeDisabled = Environment.GetEnvironmentVariable("MSBUILDNOINPROCNODE") == "1"; - /// /// Variables controlling opt out at the level of not initializing telemetry infrastructure. Set to "1" or "true" to opt out. /// mirroring diff --git a/src/MSBuild.Bootstrap.Utils/Tasks/LocateVisualStudioTask.cs b/src/MSBuild.Bootstrap.Utils/Tasks/LocateVisualStudioTask.cs new file mode 100644 index 00000000000..91cf7608e36 --- /dev/null +++ b/src/MSBuild.Bootstrap.Utils/Tasks/LocateVisualStudioTask.cs @@ -0,0 +1,62 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.IO; +using System.Runtime.InteropServices; +using System.Text; +using Microsoft.Build.Framework; +using Microsoft.Build.Utilities; + +namespace MSBuild.Bootstrap.Utils.Tasks +{ + public class LocateVisualStudioTask : ToolTask + { + private readonly StringBuilder _standardOutput = new(); + + [Output] + public string VsInstallPath { get; set; } + + protected override string ToolName => "vswhere.exe"; + + protected override string GenerateFullPathToTool() + { + string programFilesX86 = Environment.GetFolderPath(Environment.SpecialFolder.ProgramFilesX86); + string vsWherePath = Path.Combine(programFilesX86, "Microsoft Visual Studio", "Installer", ToolName); + + + return vsWherePath; + } + + protected override string GenerateCommandLineCommands() => "-latest -prerelease -property installationPath"; + + public override bool Execute() + { + if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + Log.LogMessage(MessageImportance.High, "Not running on Windows. Skipping Visual Studio detection."); + return true; + } + + _ = ExecuteTool(GenerateFullPathToTool(), string.Empty, GenerateCommandLineCommands()); + + if (!Log.HasLoggedErrors) + { + VsInstallPath = _standardOutput.ToString().Trim(); + } + + return true; + } + + // Override to capture standard output + protected override void LogEventsFromTextOutput(string singleLine, MessageImportance messageImportance) + { + if (!string.IsNullOrWhiteSpace(singleLine)) + { + _ = _standardOutput.AppendLine(singleLine); + } + + base.LogEventsFromTextOutput(singleLine, messageImportance); + } + } +} diff --git a/src/MSBuild/MSBuild/Microsoft.Build.CommonTypes.xsd b/src/MSBuild/MSBuild/Microsoft.Build.CommonTypes.xsd index 05365f4f62a..56c53a3af4d 100644 --- a/src/MSBuild/MSBuild/Microsoft.Build.CommonTypes.xsd +++ b/src/MSBuild/MSBuild/Microsoft.Build.CommonTypes.xsd @@ -1893,6 +1893,7 @@ elementFormDefault="qualified"> + boolean diff --git a/src/Tasks.UnitTests/AssemblyDependency/Miscellaneous.cs b/src/Tasks.UnitTests/AssemblyDependency/Miscellaneous.cs index 164f91774e0..76123167442 100644 --- a/src/Tasks.UnitTests/AssemblyDependency/Miscellaneous.cs +++ b/src/Tasks.UnitTests/AssemblyDependency/Miscellaneous.cs @@ -3269,41 +3269,6 @@ public void ParentAssemblyResolvedFromAForGac() Assert.Equal(reference2.ResolvedSearchPath, parentReferenceFolders[0].Directory); } - /// - /// Generate a fake reference which has been resolved from the gac. We will use it to verify the creation of the exclusion list. - /// - /// - private ReferenceTable GenerateTableWithAssemblyFromTheGlobalLocation(string location) - { - ReferenceTable referenceTable = new ReferenceTable(null, false, false, false, false, Array.Empty(), null, null, null, null, null, null, SystemProcessorArchitecture.None, fileExists, null, null, null, null, -#if FEATURE_WIN32_REGISTRY - null, null, null, -#endif - null, null, new Version("4.0"), null, null, null, true, false, null, null, false, null, WarnOrErrorOnTargetArchitectureMismatchBehavior.None, false, false, null); - - AssemblyNameExtension assemblyNameExtension = new AssemblyNameExtension(new AssemblyName("Microsoft.VisualStudio.Interopt, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089")); - TaskItem taskItem = new TaskItem("Microsoft.VisualStudio.Interopt, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"); - - Reference reference = new Reference(isWinMDFile, fileExists, getRuntimeVersion); - reference.MakePrimaryAssemblyReference(taskItem, false, ".dll"); - // "Resolve the assembly from the gac" - reference.FullPath = "c:\\Microsoft.VisualStudio.Interopt.dll"; - reference.ResolvedSearchPath = location; - referenceTable.AddReference(assemblyNameExtension, reference); - - assemblyNameExtension = new AssemblyNameExtension(new AssemblyName("Team.System, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089")); - taskItem = new TaskItem("Team, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"); - - reference = new Reference(isWinMDFile, fileExists, getRuntimeVersion); - reference.MakePrimaryAssemblyReference(taskItem, false, ".dll"); - - // "Resolve the assembly from the gac" - reference.FullPath = "c:\\Team.System.dll"; - reference.ResolvedSearchPath = location; - referenceTable.AddReference(assemblyNameExtension, reference); - return referenceTable; - } - /// /// Given a reference that resolves to a bad image, we should get a warning and /// no reference. We don't want an exception. @@ -6735,11 +6700,11 @@ public void ReferenceTableDependentItemsInDenyList3() [Fact] public void ReferenceTableDependentItemsInDenyList4() { - ReferenceTable referenceTable = new ReferenceTable(null, false, false, false, false, Array.Empty(), null, null, null, null, null, null, SystemProcessorArchitecture.None, fileExists, null, null, null, + ReferenceTable referenceTable = new ReferenceTable(null, false, false, false, false, false, Array.Empty(), null, null, null, null, null, null, SystemProcessorArchitecture.None, fileExists, null, null, null, #if FEATURE_WIN32_REGISTRY null, null, null, #endif - null, null, null, new Version("4.0"), null, null, null, true, false, null, null, false, null, WarnOrErrorOnTargetArchitectureMismatchBehavior.None, false, false, null); + null, null, null, new Version("4.0"), null, null, null, true, false, null, null, false, null, WarnOrErrorOnTargetArchitectureMismatchBehavior.None, false, false, null, Array.Empty()); MockEngine mockEngine; ResolveAssemblyReference rar; Dictionary denyList; @@ -6913,11 +6878,11 @@ public void ReferenceTableDependentItemsInDenyListPrimaryWithSpecificVersion() private static ReferenceTable MakeEmptyReferenceTable(TaskLoggingHelper log) { - ReferenceTable referenceTable = new ReferenceTable(null, false, false, false, false, Array.Empty(), null, null, null, null, null, null, SystemProcessorArchitecture.None, fileExists, null, null, null, null, + ReferenceTable referenceTable = new ReferenceTable(null, false, false, false, false, false, Array.Empty(), null, null, null, null, null, null, SystemProcessorArchitecture.None, fileExists, null, null, null, null, #if FEATURE_WIN32_REGISTRY null, null, null, #endif - null, null, new Version("4.0"), null, log, null, true, false, null, null, false, null, WarnOrErrorOnTargetArchitectureMismatchBehavior.None, false, false, null); + null, null, new Version("4.0"), null, log, null, true, false, null, null, false, null, WarnOrErrorOnTargetArchitectureMismatchBehavior.None, false, false, null, Array.Empty()); return referenceTable; } diff --git a/src/Tasks.UnitTests/Microsoft.Build.Tasks.UnitTests.csproj b/src/Tasks.UnitTests/Microsoft.Build.Tasks.UnitTests.csproj index 81b5048f0f7..2ff059b2833 100644 --- a/src/Tasks.UnitTests/Microsoft.Build.Tasks.UnitTests.csproj +++ b/src/Tasks.UnitTests/Microsoft.Build.Tasks.UnitTests.csproj @@ -64,6 +64,9 @@ PreserveNewest + + PreserveNewest + PreserveNewest @@ -155,6 +158,9 @@ PreserveNewest + + PreserveNewest + PreserveNewest diff --git a/src/Tasks.UnitTests/ResolveAssemblyReference_CustomCultureTests.cs b/src/Tasks.UnitTests/ResolveAssemblyReference_CustomCultureTests.cs new file mode 100644 index 00000000000..1a7d7ed0562 --- /dev/null +++ b/src/Tasks.UnitTests/ResolveAssemblyReference_CustomCultureTests.cs @@ -0,0 +1,89 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.IO; +using Microsoft.Build.UnitTests; +using Microsoft.Build.UnitTests.Shared; +using Shouldly; +using Xunit; + +namespace Microsoft.Build.Tasks.UnitTests +{ + /// + /// Unit tests for the ResolveAssemblyReference task. + /// + public class ResolveAssemblyReference_CustomCultureTests + { + private static string TestAssetsRootPath { get; } = Path.Combine( + Path.GetDirectoryName(typeof(AddToWin32Manifest_Tests).Assembly.Location) ?? AppContext.BaseDirectory, + "TestResources", + "CustomCulture"); + + [WindowsOnlyTheory] + [InlineData(true, "", true, true)] + [InlineData(false)] + [InlineData(true, "yue", false, true)] + [InlineData(false, "yue", false, true)] + [InlineData(true, "euy", true)] + [InlineData(true, "yue;euy")] + [InlineData(true, "euy;yue")] + public void E2EScenarioTests(bool enableCustomCulture, string customCultureExclusions = "", bool isYueCultureExpected = false, bool isEuyCultureExpected = false) + { + using (TestEnvironment env = TestEnvironment.Create()) + { + // Set up project paths + var testAssetsPath = TestAssetsRootPath; + var solutionFolder = env.CreateFolder(); + var solutionPath = solutionFolder.Path; + + // Create and configure ProjectB + var projectBName = "ProjectB.csproj"; + var projBOutputPath = env.CreateFolder().Path; + var projectBFolder = Path.Combine(solutionPath, projectBName); + Directory.CreateDirectory(projectBFolder); + var projBContent = File.ReadAllText(Path.Combine(testAssetsPath, projectBName)) + .Replace("OutputPathPlaceholder", projBOutputPath) + .Replace("NonCultureResourceDirectoriesPlaceholder", customCultureExclusions) + .Replace("EnableCustomCulturePlaceholder", enableCustomCulture.ToString()); + env.CreateFile(Path.Combine(projectBFolder, projectBName), projBContent); + + // Copy ProjectA files to test solution folder + CopyTestAsset(testAssetsPath, "ProjectA.csproj", solutionPath); + CopyTestAsset(testAssetsPath, "Test.resx", solutionPath); + CopyTestAsset(testAssetsPath, "Test.yue.resx", solutionPath); + CopyTestAsset(testAssetsPath, "Test.euy.resx", solutionPath); + + env.SetCurrentDirectory(projectBFolder); + var output = RunnerUtilities.ExecBootstrapedMSBuild("-restore", out bool buildSucceeded); + + buildSucceeded.ShouldBeTrue($"MSBuild should complete successfully. Build output: {output}"); + + var yueCultureResourceDll = Path.Combine(projBOutputPath, "yue", "ProjectA.resources.dll"); + AssertCustomCulture(isYueCultureExpected, "yue", yueCultureResourceDll); + + var euyCultureResourceDll = Path.Combine(projBOutputPath, "euy", "ProjectA.resources.dll"); + AssertCustomCulture(isEuyCultureExpected, "euy", euyCultureResourceDll); + } + + void AssertCustomCulture(bool isCultureExpectedToExist, string customCultureName, string cultureResourcePath) + { + if (enableCustomCulture && isCultureExpectedToExist) + { + File.Exists(cultureResourcePath).ShouldBeTrue($"Expected '{customCultureName}' resource DLL not found at: {cultureResourcePath}"); + } + else + { + File.Exists(cultureResourcePath).ShouldBeFalse($"Unexpected '{customCultureName}' culture DLL was found at: {cultureResourcePath}"); + } + } + } + + private void CopyTestAsset(string sourceFolder, string fileName, string destinationFolder) + { + var sourcePath = Path.Combine(sourceFolder, fileName); + + File.Copy(sourcePath, Path.Combine(destinationFolder, fileName)); + } + } +} diff --git a/src/Tasks.UnitTests/TestResources/CustomCulture/ProjectA.csproj b/src/Tasks.UnitTests/TestResources/CustomCulture/ProjectA.csproj new file mode 100644 index 00000000000..aa6d648f1b1 --- /dev/null +++ b/src/Tasks.UnitTests/TestResources/CustomCulture/ProjectA.csproj @@ -0,0 +1,25 @@ + + + + Library + net472 + + + + True + + + + ResXFileCodeGenerator + + + yue + Test.yue.resources + + + euy + Test.euy.resources + + + + diff --git a/src/Tasks.UnitTests/TestResources/CustomCulture/ProjectB.csproj b/src/Tasks.UnitTests/TestResources/CustomCulture/ProjectB.csproj new file mode 100644 index 00000000000..1daa05a8bc7 --- /dev/null +++ b/src/Tasks.UnitTests/TestResources/CustomCulture/ProjectB.csproj @@ -0,0 +1,19 @@ + + + + net472 + false + Library + OutputPathPlaceholder + EnableCustomCulturePlaceholder + + + + NonCultureResourceDirectoriesPlaceholder + + + + + + + diff --git a/src/Tasks.UnitTests/TestResources/CustomCulture/Test.euy.resx b/src/Tasks.UnitTests/TestResources/CustomCulture/Test.euy.resx new file mode 100644 index 00000000000..e6136e281c2 --- /dev/null +++ b/src/Tasks.UnitTests/TestResources/CustomCulture/Test.euy.resx @@ -0,0 +1,58 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + diff --git a/src/Tasks.UnitTests/TestResources/CustomCulture/Test.resx b/src/Tasks.UnitTests/TestResources/CustomCulture/Test.resx new file mode 100644 index 00000000000..e6136e281c2 --- /dev/null +++ b/src/Tasks.UnitTests/TestResources/CustomCulture/Test.resx @@ -0,0 +1,58 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + diff --git a/src/Tasks.UnitTests/TestResources/CustomCulture/Test.yue.resx b/src/Tasks.UnitTests/TestResources/CustomCulture/Test.yue.resx new file mode 100644 index 00000000000..e6136e281c2 --- /dev/null +++ b/src/Tasks.UnitTests/TestResources/CustomCulture/Test.yue.resx @@ -0,0 +1,58 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + diff --git a/src/Tasks/AssemblyDependency/ReferenceTable.cs b/src/Tasks/AssemblyDependency/ReferenceTable.cs index de1a4b26b20..f82b1b43eb0 100644 --- a/src/Tasks/AssemblyDependency/ReferenceTable.cs +++ b/src/Tasks/AssemblyDependency/ReferenceTable.cs @@ -135,7 +135,7 @@ internal sealed class ReferenceTable private readonly bool _doNotCopyLocalIfInGac; /// - /// Shoould the framework attribute version mismatch be ignored. + /// Should the framework attribute version mismatch be ignored. /// private readonly bool _ignoreFrameworkAttributeVersionMismatch; @@ -145,7 +145,17 @@ internal sealed class ReferenceTable private readonly GetAssemblyPathInGac _getAssemblyPathInGac; /// - /// Should a warning or error be emitted on architecture mismatch + /// Contains the list of directories that should NOT be considered as custom culture directories. + /// + private readonly string[] _nonCultureResourceDirectories = []; + + /// + /// Is true, custom culture processing is enabled. + /// + private readonly bool _enableCustomCulture = false; + + /// + /// Should a warning or error be emitted on architecture mismatch. /// private readonly WarnOrErrorOnTargetArchitectureMismatchBehavior _warnOrErrorOnTargetArchitectureMismatch = WarnOrErrorOnTargetArchitectureMismatchBehavior.Warning; @@ -174,6 +184,7 @@ internal sealed class ReferenceTable /// If true, then search for satellite files. /// If true, then search for serialization assembly files. /// If true, then search for related files. + /// If true, custom culture processing is enabled. /// Paths to search for dependent assemblies on. /// /// List of literal assembly file names to be considered when SearchPaths has {CandidateAssemblyFiles}. @@ -206,6 +217,7 @@ internal sealed class ReferenceTable /// /// /// + /// #else /// /// Construct. @@ -215,13 +227,14 @@ internal sealed class ReferenceTable /// If true, then search for satellite files. /// If true, then search for serialization assembly files. /// If true, then search for related files. + /// If true, custom culture processing is enabled. /// Paths to search for dependent assemblies on. /// /// List of literal assembly file names to be considered when SearchPaths has {CandidateAssemblyFiles}. /// Resolved sdk items /// Path to the FX. /// Installed assembly XML tables. - /// Like x86 or IA64\AMD64, the processor architecture being targetted. + /// Like x86 or IA64\AMD64, the processor architecture being targeted. /// Delegate used for checking for the existence of a file. /// Delegate used for files. /// Delegate used for getting directories. @@ -234,7 +247,7 @@ internal sealed class ReferenceTable /// Version of the runtime to target. /// Version of the framework targeted by the project. /// Target framework moniker we are targeting. - /// Logging helper to allow the logging of meessages from the Reference Table. + /// Logging helper to allow the logging of messages from the Reference Table. /// /// /// @@ -244,6 +257,7 @@ internal sealed class ReferenceTable /// /// /// + /// #endif internal ReferenceTable( IBuildEngine buildEngine, @@ -251,6 +265,7 @@ internal ReferenceTable( bool findSatellites, bool findSerializationAssemblies, bool findRelatedFiles, + bool enableCustomCulture, string[] searchPaths, string[] allowedAssemblyExtensions, string[] relatedFileExtensions, @@ -284,7 +299,8 @@ internal ReferenceTable( WarnOrErrorOnTargetArchitectureMismatchBehavior warnOrErrorOnTargetArchitectureMismatch, bool ignoreFrameworkAttributeVersionMismatch, bool unresolveFrameworkAssembliesFromHigherFrameworks, - ConcurrentDictionary assemblyMetadataCache) + ConcurrentDictionary assemblyMetadataCache, + string[] nonCultureResourceDirectories) { _log = log; _findDependencies = findDependencies; @@ -317,6 +333,8 @@ internal ReferenceTable( _warnOrErrorOnTargetArchitectureMismatch = warnOrErrorOnTargetArchitectureMismatch; _ignoreFrameworkAttributeVersionMismatch = ignoreFrameworkAttributeVersionMismatch; _assemblyMetadataCache = assemblyMetadataCache; + _nonCultureResourceDirectories = nonCultureResourceDirectories; + _enableCustomCulture = enableCustomCulture; // Set condition for when to check assembly version against the target framework version _checkAssemblyVersionAgainstTargetFrameworkVersion = unresolveFrameworkAssembliesFromHigherFrameworks || ((_projectTargetFramework ?? ReferenceTable.s_targetFrameworkVersion_40) <= ReferenceTable.s_targetFrameworkVersion_40); @@ -971,8 +989,9 @@ private void FindSatellites( // Is there a candidate satellite in that folder? string cultureName = Path.GetFileName(subDirectory); - // Custom or unknown cultures can be met as well - if (ChangeWaves.AreFeaturesEnabled(ChangeWaves.Wave17_14) || CultureInfoCache.IsValidCultureString(cultureName)) + // Custom or unknown cultures can be met only if the feature is enabled and the directory was not added to the exclusion list. + if ((_enableCustomCulture && !_nonCultureResourceDirectories.Contains(cultureName)) + || CultureInfoCache.IsValidCultureString(cultureName)) { string satelliteAssembly = Path.Combine(subDirectory, satelliteFilename); if (_fileExists(satelliteAssembly)) diff --git a/src/Tasks/AssemblyDependency/ResolveAssemblyReference.cs b/src/Tasks/AssemblyDependency/ResolveAssemblyReference.cs index b322052e386..8dd35fcdcbc 100644 --- a/src/Tasks/AssemblyDependency/ResolveAssemblyReference.cs +++ b/src/Tasks/AssemblyDependency/ResolveAssemblyReference.cs @@ -175,8 +175,10 @@ internal static void Initialize(TaskLoggingHelper log) private ITaskItem[] _resolvedSDKReferences = Array.Empty(); private bool _ignoreDefaultInstalledAssemblyTables = false; private bool _ignoreDefaultInstalledAssemblySubsetTables = false; + private bool _enableCustomCulture = false; private string[] _candidateAssemblyFiles = []; private string[] _targetFrameworkDirectories = []; + private string[] _nonCultureResourceDirectories = []; private string[] _searchPaths = []; private string[] _allowedAssemblyExtensions = [".winmd", ".dll", ".exe"]; private string[] _relatedFileExtensions = [".pdb", ".xml", ".pri"]; @@ -420,6 +422,24 @@ public string[] TargetFrameworkDirectories set { _targetFrameworkDirectories = value; } } + /// + /// Contains list of directories that point to custom culture resources that has to be ignored by MSBuild. + /// + public string[] NonCultureResourceDirectories + { + get { return _nonCultureResourceDirectories; } + set { _nonCultureResourceDirectories = value; } + } + + /// + /// Contains the information if custom culture is enabled. + /// + public bool EnableCustomCulture + { + get { return _enableCustomCulture; } + set { _enableCustomCulture = value; } + } + /// /// A list of XML files that contain assemblies that are expected to be installed on the target machine. /// @@ -1505,7 +1525,10 @@ private void LogInputs() } Log.LogMessage(importance, property, "TargetFrameworkDirectories"); - Log.LogMessage(importance, indent + String.Join(",", TargetFrameworkDirectories)); + Log.LogMessage(importance, indent + string.Join(",", TargetFrameworkDirectories)); + + Log.LogMessage(importance, property, "NonCultureResourceDirectories"); + Log.LogMessage(importance, indent + string.Join(",", NonCultureResourceDirectories)); Log.LogMessage(importance, property, "InstalledAssemblyTables"); foreach (ITaskItem installedAssemblyTable in InstalledAssemblyTables) @@ -1541,6 +1564,9 @@ private void LogInputs() Log.LogMessage(importance, property, "AutoUnify"); Log.LogMessage(importance, indent + AutoUnify.ToString()); + Log.LogMessage(importance, property, "EnableCustomCulture"); + Log.LogMessage(importance, $"{indent}{EnableCustomCulture}"); + Log.LogMessage(importance, property, "CopyLocalDependenciesWhenParentReferenceInGac"); Log.LogMessage(importance, indent + _copyLocalDependenciesWhenParentReferenceInGac); @@ -2380,6 +2406,7 @@ internal bool Execute( _findSatellites, _findSerializationAssemblies, _findRelatedFiles, + _enableCustomCulture, _searchPaths, _allowedAssemblyExtensions, _relatedFileExtensions, @@ -2413,7 +2440,8 @@ internal bool Execute( _warnOrErrorOnTargetArchitectureMismatch, _ignoreTargetFrameworkAttributeVersionMismatch, _unresolveFrameworkAssembliesFromHigherFrameworks, - assemblyMetadataCache); + assemblyMetadataCache, + _nonCultureResourceDirectories); dependencyTable.FindDependenciesOfExternallyResolvedReferences = FindDependenciesOfExternallyResolvedReferences; diff --git a/src/Tasks/CreateCSharpManifestResourceName.cs b/src/Tasks/CreateCSharpManifestResourceName.cs index 6f3b6b5a06d..c7f838b16ef 100644 --- a/src/Tasks/CreateCSharpManifestResourceName.cs +++ b/src/Tasks/CreateCSharpManifestResourceName.cs @@ -51,7 +51,7 @@ protected override string CreateManifestName( Actual implementation is in a static method called CreateManifestNameImpl. The reason is that CreateManifestName can't be static because it is an override of a method declared in the base class, but its convenient - to expose a static version anyway for unittesting purposes. + to expose a static version anyway for unit testing purposes. */ return CreateManifestNameImpl( fileName, @@ -62,7 +62,8 @@ The reason is that CreateManifestName can't be static because it is an culture, binaryStream, Log, - treatAsCultureNeutral); + treatAsCultureNeutral, + EnableCustomCulture); } /// @@ -81,6 +82,7 @@ The reason is that CreateManifestName can't be static because it is an /// File contents binary stream, may be null /// Task's TaskLoggingHelper, for logging warnings or errors /// Whether to treat the current file as 'culture-neutral' and retain the culture in the name. + /// Whether custom culture handling is expected. /// Returns the manifest name internal static string CreateManifestNameImpl( string fileName, @@ -91,7 +93,8 @@ internal static string CreateManifestNameImpl( string culture, // may be null Stream binaryStream, // File contents binary stream, may be null TaskLoggingHelper log, - bool treatAsCultureNeutral = false) + bool treatAsCultureNeutral = false, + bool enableCustomCulture = false) { // Use the link file name if there is one, otherwise, fall back to file name. string embeddedFileName = FileUtilities.FixFilePath(linkFileName); @@ -103,13 +106,12 @@ internal static string CreateManifestNameImpl( dependentUponFileName = FileUtilities.FixFilePath(dependentUponFileName); Culture.ItemCultureInfo info; - if (!string.IsNullOrEmpty(culture) && ChangeWaves.AreFeaturesEnabled(ChangeWaves.Wave17_14)) + if (!string.IsNullOrEmpty(culture) && enableCustomCulture) { info = new Culture.ItemCultureInfo() { culture = culture, - cultureNeutralFilename = - embeddedFileName.RemoveLastInstanceOf("." + culture, StringComparison.OrdinalIgnoreCase), + cultureNeutralFilename = embeddedFileName.RemoveLastInstanceOf("." + culture, StringComparison.OrdinalIgnoreCase), }; } else diff --git a/src/Tasks/CreateManifestResourceName.cs b/src/Tasks/CreateManifestResourceName.cs index 934d67c6a68..d974b1a8d1c 100644 --- a/src/Tasks/CreateManifestResourceName.cs +++ b/src/Tasks/CreateManifestResourceName.cs @@ -28,6 +28,8 @@ public abstract class CreateManifestResourceName : TaskExtension private ITaskItem[] _resourceFiles; + private bool _enableCustomCulture; + [SuppressMessage("Microsoft.Design", "CA1051:DoNotDeclareVisibleInstanceFields", Justification = "Shipped this way in Dev11 Beta (go-live)")] [SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "Taskitem", Justification = "Shipped this way in Dev11 Beta (go-live)")] protected Dictionary itemSpecToTaskitem = new Dictionary(StringComparer.OrdinalIgnoreCase); @@ -56,6 +58,15 @@ public ITaskItem[] ResourceFiles set => _resourceFiles = value; } + /// + /// Contains the information if custom culture is enabled. + /// + public bool EnableCustomCulture + { + get { return _enableCustomCulture; } + set { _enableCustomCulture = value; } + } + /// /// Rootnamespace to use for naming. /// diff --git a/src/Tasks/CreateVisualBasicManifestResourceName.cs b/src/Tasks/CreateVisualBasicManifestResourceName.cs index 0115685336f..d2cf7f405ef 100644 --- a/src/Tasks/CreateVisualBasicManifestResourceName.cs +++ b/src/Tasks/CreateVisualBasicManifestResourceName.cs @@ -50,7 +50,7 @@ protected override string CreateManifestName( Actual implementation is in a static method called CreateManifestNameImpl. The reason is that CreateManifestName can't be static because it is an override of a method declared in the base class, but its convenient - to expose a static version anyway for unittesting purposes. + to expose a static version anyway for unit testing purposes. */ return CreateManifestNameImpl( fileName, @@ -61,7 +61,8 @@ The reason is that CreateManifestName can't be static because it is an culture, binaryStream, Log, - treatAsCultureNeutral); + treatAsCultureNeutral, + EnableCustomCulture); } /// @@ -80,6 +81,7 @@ The reason is that CreateManifestName can't be static because it is an /// File contents binary stream, may be null /// Task's TaskLoggingHelper, for logging warnings or errors /// Whether to treat the current file as 'culture-neutral' and retain the culture in the name. + /// Whether custom culture handling is expected. /// Returns the manifest name internal static string CreateManifestNameImpl( string fileName, @@ -90,7 +92,8 @@ internal static string CreateManifestNameImpl( string culture, Stream binaryStream, // File contents binary stream, may be null TaskLoggingHelper log, - bool treatAsCultureNeutral = false) + bool treatAsCultureNeutral = false, + bool enableCustomCulture = false) { // Use the link file name if there is one, otherwise, fall back to file name. string embeddedFileName = linkFileName; @@ -102,13 +105,12 @@ internal static string CreateManifestNameImpl( dependentUponFileName = FileUtilities.FixFilePath(dependentUponFileName); Culture.ItemCultureInfo info; - if (!string.IsNullOrEmpty(culture) && ChangeWaves.AreFeaturesEnabled(ChangeWaves.Wave17_14)) + if (!string.IsNullOrEmpty(culture) && enableCustomCulture) { info = new Culture.ItemCultureInfo() { culture = culture, - cultureNeutralFilename = - embeddedFileName.RemoveLastInstanceOf("." + culture, StringComparison.OrdinalIgnoreCase) + cultureNeutralFilename = embeddedFileName.RemoveLastInstanceOf("." + culture, StringComparison.OrdinalIgnoreCase), }; } else diff --git a/src/Tasks/Microsoft.CSharp.CurrentVersion.targets b/src/Tasks/Microsoft.CSharp.CurrentVersion.targets index 60045885791..0280966ef15 100644 --- a/src/Tasks/Microsoft.CSharp.CurrentVersion.targets +++ b/src/Tasks/Microsoft.CSharp.CurrentVersion.targets @@ -100,6 +100,7 @@ Copyright (C) Microsoft Corporation. All rights reserved. diff --git a/src/Tasks/Microsoft.Common.CurrentVersion.targets b/src/Tasks/Microsoft.Common.CurrentVersion.targets index 48b9de51827..0c4ce55ad13 100644 --- a/src/Tasks/Microsoft.Common.CurrentVersion.targets +++ b/src/Tasks/Microsoft.Common.CurrentVersion.targets @@ -2412,6 +2412,15 @@ Copyright (C) Microsoft Corporation. All rights reserved. + + + + + + + false + + - + false - + - +