diff --git a/src/Tasks.UnitTests/WriteCodeFragment_Tests.cs b/src/Tasks.UnitTests/WriteCodeFragment_Tests.cs index 374df4ef58c..22f43163eb1 100644 --- a/src/Tasks.UnitTests/WriteCodeFragment_Tests.cs +++ b/src/Tasks.UnitTests/WriteCodeFragment_Tests.cs @@ -1201,5 +1201,57 @@ private static void CheckContent(string actualContent, string[] expectedAttribut expectedContent.ShouldBe(normalizedActualContent); } + + /// + /// Test that the generated comment is culture-invariant for reproducible builds + /// + [Theory] + [InlineData("en-US")] + [InlineData("fr-FR")] + [InlineData("ja-JP")] + [InlineData("de-DE")] + public void CommentIsInvariantCulture(string cultureName) + { + var originalCulture = System.Globalization.CultureInfo.CurrentUICulture; + try + { + System.Globalization.CultureInfo.CurrentUICulture = new System.Globalization.CultureInfo(cultureName); + + WriteCodeFragment task = new WriteCodeFragment(); + MockEngine engine = new MockEngine(true); + task.BuildEngine = engine; + TaskItem attribute = new TaskItem("System.AssemblyVersionAttribute"); + attribute.SetMetadata("_Parameter1", "1.0.0.0"); + task.AssemblyAttributes = [attribute]; + task.Language = "c#"; + task.OutputDirectory = new TaskItem(Path.GetTempPath()); + bool result = task.Execute(); + + result.ShouldBeTrue(); + string content = File.ReadAllText(task.OutputFile.ItemSpec); + + // Helper function to extract the WriteCodeFragment comment from generated code + static string ExtractMSBuildComment(string content) => + content.Split(MSBuildConstants.CrLf) + .FirstOrDefault(line => line.Trim().StartsWith("//") && line.Contains("MSBuild")); + + string comment = ExtractMSBuildComment(content); + + // Comment should be the expected invariant English text regardless of culture + comment.ShouldNotBeNullOrWhiteSpace(); + comment.Trim().ShouldBe("// Generated by the MSBuild WriteCodeFragment class."); + + // Cleanup + if (task.OutputFile != null && File.Exists(task.OutputFile.ItemSpec)) + { + File.Delete(task.OutputFile.ItemSpec); + } + } + finally + { + // Restore original culture + System.Globalization.CultureInfo.CurrentUICulture = originalCulture; + } + } } } diff --git a/src/Tasks/AssemblyResources.cs b/src/Tasks/AssemblyResources.cs index 51a9b19d201..1e31b9bafc6 100644 --- a/src/Tasks/AssemblyResources.cs +++ b/src/Tasks/AssemblyResources.cs @@ -29,6 +29,22 @@ internal static string GetString(string name) return resource; } + /// + /// Loads the specified resource string using invariant culture, either from the assembly's primary resources, or its shared resources. + /// This method is useful for generating code that should be culture-independent for reproducible builds. + /// + /// This method is thread-safe. + /// The resource string, or null if not found. + internal static string GetInvariantString(string name) + { + // NOTE: the ResourceManager.GetString() method is thread-safe + string resource = PrimaryResources.GetString(name, CultureInfo.InvariantCulture) ?? SharedResources.GetString(name, CultureInfo.InvariantCulture); + + ErrorUtilities.VerifyThrow(resource != null, "Missing resource '{0}'", name); + + return resource; + } + /// /// Gets the assembly's primary resources i.e. the resources exclusively owned by this assembly. /// diff --git a/src/Tasks/WriteCodeFragment.cs b/src/Tasks/WriteCodeFragment.cs index e45988febff..cf3d673835f 100644 --- a/src/Tasks/WriteCodeFragment.cs +++ b/src/Tasks/WriteCodeFragment.cs @@ -168,7 +168,7 @@ private string GenerateCode(out string extension) unit.Namespaces.Add(globalNamespace); // Declare authorship. Unfortunately CodeDOM puts this comment after the attributes. - string comment = ResourceUtilities.GetResourceString("WriteCodeFragment.Comment"); + string comment = AssemblyResources.GetInvariantString("WriteCodeFragment.Comment"); globalNamespace.Comments.Add(new CodeCommentStatement(comment)); if (AssemblyAttributes == null)