dotnet / roslyn

The Roslyn .NET compiler provides C# and Visual Basic languages with rich code analysis APIs.
https://docs.microsoft.com/dotnet/csharp/roslyn-sdk/
MIT License
19.08k stars 4.04k forks source link

Roslyn wrote incorrect timedatestime to PE Coff Header of a .Net 8.0 program file #74200

Closed CuteLeon closed 4 months ago

CuteLeon commented 4 months ago

Version Used: How can I get compiler's version? Steps to Reproduce:

  1. Create a new .Net 8.0 WinFrom application
  2. Add below method in above application
    public static DateTime? GetBuildDateTime(this Assembly assembly)
    {
        var location = assembly.Location;
        var buildDateTime = default(DateTime?);
        try
        {
            using var fileStream = new FileStream(location, FileMode.Open, FileAccess.Read);
            using var peReader = new PEReader(fileStream);
            buildDateTime = new DateTime(1970, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc)
                .AddSeconds(peReader.PEHeaders.CoffHeader.TimeDateStamp)
                .ToLocalTime();
        }
        finally
        {
        }
        return buildDateTime;
    }
  3. Call above method and pass 'Assembly.GetEntryAssembly()' in

Diagnostic Id:

Expected Behavior: I hope this method will read a correct compile time from PEHeaders.CoffHeader.TimeDateStamp;

Actual Behavior: I got a negative integer from PEHeaders.CoffHeader.TimeDateStamp, and incorrect compile time;

Some details: Bytes wrotten in binary program file: image Hex value readed from PEReader: image Got build date time: image

CuteLeon commented 4 months ago

I found below content in https://github.com/dotnet/runtime/blob/main/docs/design/specs/PE-COFF.md#deterministic-pecoff-file, think TimeDateStamp doesn't mean compile date time any more, what will be the substitute?

The value of field TimeDateStamp in COFF File Header of a deterministic PE/COFF file does not indicate the date and time when the file was produced and should not be interpreted that way. Instead the value of the field is derived from a hash of the file content.

CuteLeon commented 4 months ago

I implement another solution is that: Create a new AssemblyCompileDateTimeAttribute class and a SourceGenerator project to inject AssemblyCompileDateTimeAttribute declaretion into each project.

jaredpar commented 4 months ago

This behavior is "By Design". The default for .NET SDK applications is to have deterministic builds. To achieve that the timestamp field is replaced with part of a deterministic hash of the compilation inputs. If you desire the old behavior you can set <Deterministic>false</Deterministic> in your project file. Doing so is generally not recommended.

CuteLeon commented 4 months ago

Share my workaround:

public class AssemblyCompileDateTimeAttribute : Attribute
{
    public string CompileDateTime { get; set; }

    public AssemblyCompileDateTimeAttribute(string compileDateTime)
    {
        this.CompileDateTime = compileDateTime;
    }
}
    [Generator(LanguageNames.CSharp)]
    public class CompileDateTimeSourceGenerator : ISourceGenerator
    {
        public void Initialize(GeneratorInitializationContext context)
        {
        }

        public void Execute(GeneratorExecutionContext context)
        {
            var assemblyName = context.Compilation.Assembly.Name;
            var compileDateTime = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fffffff");
            context.AddSource($"{assemblyName}.CompileDateTime.g.cs", $"[assembly: AssemblyTools.AssemblyCompileDateTimeAttribute(\"{compileDateTime}\")]");
        }
    }
    public static DateTime? GetCompileDateTimeFromAttribute(this Assembly assembly)
    {
        var buildDateTime = default(DateTime?);
        try
        {
            var attribute = assembly.GetCustomAttribute<AssemblyCompileDateTimeAttribute>();
            if (attribute is not null &&
                DateTime.TryParseExact(attribute.CompileDateTime, "yyyy-MM-dd HH:mm:ss.fffffff", CultureInfo.InvariantCulture, DateTimeStyles.None, out var compileDateTime))
            {
                buildDateTime = compileDateTime;
            }
        }
        finally
        {
        }
        return buildDateTime;
    }
jjonescz commented 4 months ago

Note that a source generator might be an overkill, you can use MSBuild items to create attributes - you don't even need a custom attribute, there is AssemblyMetadataAttribute that can be used for this purpose - see example in the docs:

<ItemGroup>
  <!-- Include must be the fully qualified .NET type name of the Attribute to create. -->
  <AssemblyAttribute Include="System.Reflection.AssemblyMetadataAttribute">
    <!-- _Parameter1, _Parameter2, etc. correspond to the
        matching parameter of a constructor of that .NET attribute type -->
    <_Parameter1>BuildDate</_Parameter1>
    <_Parameter2>$(Date)</_Parameter2>
  </AssemblyAttribute>
</ItemGroup>

generates:

[assembly: System.Reflection.AssemblyMetadataAttribute("BuildDate", "01/19/2024")]