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)