Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 52 additions & 0 deletions src/Tasks.UnitTests/WriteCodeFragment_Tests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1201,5 +1201,57 @@ private static void CheckContent(string actualContent, string[] expectedAttribut

expectedContent.ShouldBe(normalizedActualContent);
}

/// <summary>
/// Test that the generated comment is culture-invariant for reproducible builds
/// </summary>
[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;
}
}
}
}
16 changes: 16 additions & 0 deletions src/Tasks/AssemblyResources.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,22 @@ internal static string GetString(string name)
return resource;
}

/// <summary>
/// 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.
/// </summary>
/// <remarks>This method is thread-safe.</remarks>
/// <returns>The resource string, or null if not found.</returns>
Comment thread
baronfel marked this conversation as resolved.
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;
}

/// <summary>
/// Gets the assembly's primary resources i.e. the resources exclusively owned by this assembly.
/// </summary>
Expand Down
2 changes: 1 addition & 1 deletion src/Tasks/WriteCodeFragment.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Loading