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
-
+
-
+