显示构建日期

时间:2009-10-21 13:50:46

标签: c# date time compilation

我目前有一个应用程序在其标题窗口中显示内部版本号。这很好,除非对大多数用户没有任何意义,他们想知道他们是否拥有最新版本 - 他们倾向于将其称为“上周四”,而不是建立1.0.8.4321。

计划是将构建日期放在那里 - 例如“应用程序构建于2009年10月21日”。

我很难找到一种程序化的方法来将构建日期拉出来作为文本字符串使用。

对于内部版本号,我使用了:

Assembly.GetExecutingAssembly().GetName().Version.ToString()

在定义了这些内容之后。

我喜欢编译日期(和时间,奖励积分)的类似内容。

这里的指针非常赞赏(如果合适的话,请原谅双关语),或更整洁的解决方案...

26 个答案:

答案 0 :(得分:348)

杰夫阿特伍德在Determining Build Date the hard way中对这个问题有一些说法。

最可靠的方法是从可执行文件中嵌入的PE header中检索链接器时间戳 - 一些C#代码(由Joe Spivey提供),从评论到Jeff的文章:

public static DateTime GetLinkerTime(this Assembly assembly, TimeZoneInfo target = null)
{
    var filePath = assembly.Location;
    const int c_PeHeaderOffset = 60;
    const int c_LinkerTimestampOffset = 8;

    var buffer = new byte[2048];

    using (var stream = new FileStream(filePath, FileMode.Open, FileAccess.Read))
        stream.Read(buffer, 0, 2048);

    var offset = BitConverter.ToInt32(buffer, c_PeHeaderOffset);
    var secondsSince1970 = BitConverter.ToInt32(buffer, offset + c_LinkerTimestampOffset);
    var epoch = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);

    var linkTimeUtc = epoch.AddSeconds(secondsSince1970);

    var tz = target ?? TimeZoneInfo.Local;
    var localTime = TimeZoneInfo.ConvertTimeFromUtc(linkTimeUtc, tz);

    return localTime;
}

用法示例:

var linkTimeLocal = Assembly.GetExecutingAssembly().GetLinkerTime();

更新:该方法适用于.Net Core 1.0,但在.Net Core 1.1 发布后停止工作(在1900-2020范围内提供随机年份)

答案 1 :(得分:87)

在下面添加到预构建事件命令行:

echo %date% %time% > "$(ProjectDir)\Resources\BuildDate.txt"

将此文件添加为资源, 现在你的资源中有'BuildDate'字符串。

要创建资源,请参阅How to create and use resources in .NET

答案 2 :(得分:86)

方式

正如comments中的@ c00000fd所指出的那样。微软正在改变这一点。虽然许多人不使用他们编译器的最新版本,但我怀疑这种改变使得这种方法毫无疑问是糟糕的。虽然这是一个有趣的练习,但我建议人们通过任何其他必要的方法将构建日期嵌入到二进制文件中,如果跟踪二进制文件本身的构建日期很重要。

这可以通过一些简单的代码生成来完成,这可能是构建脚本中的第一步。这一点,以及ALM / Build / DevOps工具对此有很大帮助的事实,应该优先于其他任何工具。

我将此答案的其余部分仅用于历史目的。

新方式

我改变了主意,现在使用这个技巧来获得正确的构建日期。

#region Gets the build date and time (by reading the COFF header)

// http://msdn.microsoft.com/en-us/library/ms680313

struct _IMAGE_FILE_HEADER
{
    public ushort Machine;
    public ushort NumberOfSections;
    public uint TimeDateStamp;
    public uint PointerToSymbolTable;
    public uint NumberOfSymbols;
    public ushort SizeOfOptionalHeader;
    public ushort Characteristics;
};

static DateTime GetBuildDateTime(Assembly assembly)
{
    var path = assembly.GetName().CodeBase;
    if (File.Exists(path))
    {
        var buffer = new byte[Math.Max(Marshal.SizeOf(typeof(_IMAGE_FILE_HEADER)), 4)];
        using (var fileStream = new FileStream(path, FileMode.Open, FileAccess.Read))
        {
            fileStream.Position = 0x3C;
            fileStream.Read(buffer, 0, 4);
            fileStream.Position = BitConverter.ToUInt32(buffer, 0); // COFF header offset
            fileStream.Read(buffer, 0, 4); // "PE\0\0"
            fileStream.Read(buffer, 0, buffer.Length);
        }
        var pinnedBuffer = GCHandle.Alloc(buffer, GCHandleType.Pinned);
        try
        {
            var coffHeader = (_IMAGE_FILE_HEADER)Marshal.PtrToStructure(pinnedBuffer.AddrOfPinnedObject(), typeof(_IMAGE_FILE_HEADER));

            return TimeZone.CurrentTimeZone.ToLocalTime(new DateTime(1970, 1, 1) + new TimeSpan(coffHeader.TimeDateStamp * TimeSpan.TicksPerSecond));
        }
        finally
        {
            pinnedBuffer.Free();
        }
    }
    return new DateTime();
}

#endregion

旧方式

那么,你如何生成内部版本号?如果将AssemblyVersion属性更改为例如,Visual Studio(或C#编译器)实际上提供了自动构建和修订号。 1.0.*

将会发生的情况是,构建将等于自2000年1月1日当地时间以来的天数,并且修订将等于自当地时间午夜以来的秒数除以2。

请参阅社区内容Automatic Build and Revision numbers

e.g。的AssemblyInfo.cs

[assembly: AssemblyVersion("1.0.*")] // important: use wildcard for build and revision numbers!

SampleCode.cs

var version = Assembly.GetEntryAssembly().GetName().Version;
var buildDateTime = new DateTime(2000, 1, 1).Add(new TimeSpan(
TimeSpan.TicksPerDay * version.Build + // days since 1 January 2000
TimeSpan.TicksPerSecond * 2 * version.Revision)); // seconds since midnight, (multiply by 2 to get original)

答案 3 :(得分:41)

在下面添加到预构建事件命令行:

echo %date% %time% > "$(ProjectDir)\Resources\BuildDate.txt"

将此文件添加为资源,现在您的资源中包含“BuildDate”字符串。

将文件插入资源(作为公共文本文件)后,我通过

访问它
string strCompTime = Properties.Resources.BuildDate;

要创建资源,请参阅How to create and use resources in .NET

答案 4 :(得分:20)

令我惊讶的是,没有人提到的一种方法是使用T4 Text Templates来生成代码。

<#@ template debug="false" hostspecific="true" language="C#" #>
<#@ assembly name="System.Core" #>
<#@ import namespace="System" #>
<#@ output extension=".g.cs" #>
using System;
namespace Foo.Bar
{
    public static partial class Constants
    {
        public static DateTime CompilationTimestampUtc { get { return new DateTime(<# Write(DateTime.UtcNow.Ticks.ToString()); #>L, DateTimeKind.Utc); } }
    }
}

优点:

  • 语言环境无关
  • 允许的不仅仅是编译时间

缺点:

答案 5 :(得分:16)

关于从程序集PE头的字节中提取构建日期/版本信息的技术,Microsoft已更改以Visual Studio 15.4开头的默认构建参数。新的默认值包括确定性编译,它使有效的时间戳和自动递增的版本号成为过去。时间戳字段仍然存在,但它会填充永久值,该值是某个或其他内容的散列,但不是任何构建时间的指示。

http://blog.paranoidcoding.com/2016/04/05/deterministic-builds-in-roslyn.html Some detailed background here

对于那些优先考虑确定性编译的有用时间戳的人,有一种方法可以覆盖新的默认值。您可以在感兴趣的程序集的.csproj文件中包含一个标记,如下所示:

  <PropertyGroup>
      ...
      <Deterministic>false</Deterministic>
  </PropertyGroup>

更新: 我赞同此处另一个答案中描述的T4文本模板解决方案。我用它来干净地解决我的问题而不会失去确定性编译的好处。有一点需要注意的是,Visual Studio只在保存.tt文件时运行T4编译器,而不是在构建时运行。如果从源代码控制中排除.cs结果(因为您希望生成它)并且另一个开发人员检出代码,这可能会很麻烦。没有重新保存,他们将没有.cs文件。 nuget上有一个包(我认为叫做AutoT4),它使T4编译成为每个构建的一部分。我还没有在生产部署期间面对这个问题的解决方案,但我希望类似的东西可以做到。

答案 6 :(得分:12)

我只是C#newbie所以也许我的答案听起来很傻 - 我显示了从上一次写入可执行文件之日起的构建日期:

string w_file = "MyProgram.exe"; 
string w_directory = Directory.GetCurrentDirectory();

DateTime c3 =  File.GetLastWriteTime(System.IO.Path.Combine(w_directory, w_file));
RTB_info.AppendText("Program created at: " + c3.ToString());

我尝试使用File.GetCreationTime方法,但得到了奇怪的结果:命令的日期是2012-05-29,但是Window Explorer的日期显示为2012-05-23。在搜索到这种差异后,我发现该文件可能是在2012-05-23创建的(如Windows资源管理器所示),但在2012-05-29复制到当前文件夹(如File.GetCreationTime命令所示) - 所以为了安全起见,我正在使用File.GetLastWriteTime命令。

Zalek

答案 7 :(得分:10)

上面的方法可以通过在内存中使用文件的图像(而不是从存储中重新读取)来调整已经在进程中加载​​的程序集

using System;
using System.Runtime.InteropServices;
using Assembly = System.Reflection.Assembly;

static class Utils
{
    public static DateTime GetLinkerDateTime(this Assembly assembly, TimeZoneInfo tzi = null)
    {
        // Constants related to the Windows PE file format.
        const int PE_HEADER_OFFSET = 60;
        const int LINKER_TIMESTAMP_OFFSET = 8;

        // Discover the base memory address where our assembly is loaded
        var entryModule = assembly.ManifestModule;
        var hMod = Marshal.GetHINSTANCE(entryModule);
        if (hMod == IntPtr.Zero - 1) throw new Exception("Failed to get HINSTANCE.");

        // Read the linker timestamp
        var offset = Marshal.ReadInt32(hMod, PE_HEADER_OFFSET);
        var secondsSince1970 = Marshal.ReadInt32(hMod, offset + LINKER_TIMESTAMP_OFFSET);

        // Convert the timestamp to a DateTime
        var epoch = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
        var linkTimeUtc = epoch.AddSeconds(secondsSince1970);
        var dt = TimeZoneInfo.ConvertTimeFromUtc(linkTimeUtc, tzi ?? TimeZoneInfo.Local);
        return dt;
    }
}

答案 8 :(得分:10)

对于需要在Windows 8 / Windows Phone 8中获得编译时间的任何人:

    public static async Task<DateTimeOffset?> RetrieveLinkerTimestamp(Assembly assembly)
    {
        var pkg = Windows.ApplicationModel.Package.Current;
        if (null == pkg)
        {
            return null;
        }

        var assemblyFile = await pkg.InstalledLocation.GetFileAsync(assembly.ManifestModule.Name);
        if (null == assemblyFile)
        {
            return null;
        }

        using (var stream = await assemblyFile.OpenSequentialReadAsync())
        {
            using (var reader = new DataReader(stream))
            {
                const int PeHeaderOffset = 60;
                const int LinkerTimestampOffset = 8;

                //read first 2048 bytes from the assembly file.
                byte[] b = new byte[2048];
                await reader.LoadAsync((uint)b.Length);
                reader.ReadBytes(b);
                reader.DetachStream();

                //get the pe header offset
                int i = System.BitConverter.ToInt32(b, PeHeaderOffset);

                //read the linker timestamp from the PE header
                int secondsSince1970 = System.BitConverter.ToInt32(b, i + LinkerTimestampOffset);

                var dt = new DateTimeOffset(1970, 1, 1, 0, 0, 0, DateTimeOffset.Now.Offset) + DateTimeOffset.Now.Offset;
                return dt.AddSeconds(secondsSince1970);
            }
        }
    }

对于需要在Windows Phone 7中获得编译时间的任何人:

    public static async Task<DateTimeOffset?> RetrieveLinkerTimestampAsync(Assembly assembly)
    {
        const int PeHeaderOffset = 60;
        const int LinkerTimestampOffset = 8;            
        byte[] b = new byte[2048];

        try
        {
            var rs = Application.GetResourceStream(new Uri(assembly.ManifestModule.Name, UriKind.Relative));
            using (var s = rs.Stream)
            {
                var asyncResult = s.BeginRead(b, 0, b.Length, null, null);
                int bytesRead = await Task.Factory.FromAsync<int>(asyncResult, s.EndRead);
            }
        }
        catch (System.IO.IOException)
        {
            return null;
        }

        int i = System.BitConverter.ToInt32(b, PeHeaderOffset);
        int secondsSince1970 = System.BitConverter.ToInt32(b, i + LinkerTimestampOffset);
        var dt = new DateTimeOffset(1970, 1, 1, 0, 0, 0, DateTimeOffset.Now.Offset) + DateTimeOffset.Now.Offset;
        dt = dt.AddSeconds(secondsSince1970);
        return dt;
    }

注意:在所有情况下,您都在沙箱中运行,因此您只能获得使用应用程序部署的程序集的编译时间。 (即这不适用于GAC中的任何内容)。

答案 9 :(得分:8)

此处未讨论的选项是将您自己的数据插入AssemblyInfo.cs,“AssemblyInformationalVersion”字段似乎合适 - 我们有几个项目,我们正在做类似构建步骤的事情(但我并不完全对工作方式感到满意所以不要真正想要重现我们所拥有的东西。

在codeproject上有一篇关于这个主题的文章:http://www.codeproject.com/KB/dotnet/Customizing_csproj_files.aspx

答案 10 :(得分:8)

对于.NET Core项目,我改编了Postlagerkarte的答案,用构建日期更新程序集版权字段。

直接编辑csproj

以下内容可以直接添加到csproj中的第一个PropertyGroup

<Copyright>Copyright © $([System.DateTime]::UtcNow.Year) Travis Troyer ($([System.DateTime]::UtcNow.ToString("s")))</Copyright>

替代方案:Visual Studio项目属性

或者将内部表达式直接粘贴到Visual Studio中项目属性的“包”部分的“版权”字段中:

Copyright © $([System.DateTime]::UtcNow.Year) Travis Troyer ($([System.DateTime]::UtcNow.ToString("s")))

这可能有点令人困惑,因为Visual Studio将评估表达式并在窗口中显示当前值,但它也会在幕后适当地更新项目文件。

通过Directory.Build.props

解决方案范围

您可以将上面的<Copyright>元素放入解决方案根目录中的Directory.Build.props文件中,并将其自动应用于目录中的所有项目,假设每个项目都不提供自己的版权值。

<Project>
 <PropertyGroup>
   <Copyright>Copyright © $([System.DateTime]::UtcNow.Year) Travis Troyer ($([System.DateTime]::UtcNow.ToString("s")))</Copyright>
 </PropertyGroup>
</Project>

Directory.Build.props:Customize your build

输出

示例表达式将为您提供如下版权:

Copyright © 2018 Travis Troyer (2018-05-30T14:46:23)

检索

您可以在Windows中查看文件属性中的版权信息,也可以在运行时抓取它:

var version = FileVersionInfo.GetVersionInfo(Assembly.GetEntryAssembly().Location);

Console.WriteLine(version.LegalCopyright);

答案 11 :(得分:7)

这里有很多很棒的答案,但我觉得我可以添加自己的,因为简单,性能(与资源相关的解决方案相比)跨平台(也适用于Net Core)和避免使用任何第三方工具。只需将此msbuild目标添加到csproj。

<Target Name="Date" BeforeTargets="CoreCompile">
    <WriteLinesToFile File="$(IntermediateOutputPath)gen.cs" Lines="static partial class Builtin { public static long CompileTime = $([System.DateTime]::UtcNow.Ticks) %3B }" Overwrite="true" />
    <ItemGroup>
        <Compile Include="$(IntermediateOutputPath)gen.cs" />
    </ItemGroup>
</Target>

现在,如果您需要,可以Builtin.CompileTimenew DateTime(Builtin.CompileTime, DateTimeKind.Utc)

ReSharper不会喜欢它。您可以忽略他或者将部分类添加到项目中,但无论如何都可以。

答案 12 :(得分:6)

2018年,上述某些解决方案不再起作用,或者无法与.NET Core一起使用。

我使用以下方法,该方法很简单,适用于我的.NET Core 2.0项目。

将以下内容添加到PropertyGroup内的.csproj中:

    <Today>$([System.DateTime]::Now)</Today>

这定义了一个PropertyFunction,您可以在预构建命令中访问它。

您的预构建看起来像这样

echo $(today) > $(ProjectDir)BuildTimeStamp.txt

将BuildTimeStamp.txt的属性设置为Embedded资源。

现在您可以像这样阅读时间戳

public static class BuildTimeStamp
    {
        public static string GetTimestamp()
        {
            var assembly = Assembly.GetEntryAssembly(); 

            var stream = assembly.GetManifestResourceStream("NamespaceGoesHere.BuildTimeStamp.txt");

            using (var reader = new StreamReader(stream))
            {
                return reader.ReadToEnd();
            }
        }
    }

答案 13 :(得分:3)

我只是这样做:

File.GetCreationTime(GetType().Assembly.Location)

答案 14 :(得分:3)

您可以使用以下项目:https://github.com/dwcullop/BuildInfo

它利用T4来自动构建日期时间戳。有几种版本(不同的分支),其中包括一个版本,如果您喜欢这种类型的版本,它可以为您提供当前已签出分支的Git Hash。

披露:我编写了模块。

答案 15 :(得分:3)

我需要一个可在任何平台(iOS,Android和Windows)上使用NETStandard项目的通用解决方案。为此,我决定通过PowerShell脚本自动生成CS文件。这是PowerShell脚本:

param($outputFile="BuildDate.cs")

$buildDate = Get-Date -date (Get-Date).ToUniversalTime() -Format o
$class = 
"using System;
using System.Globalization;

namespace MyNamespace
{
    public static class BuildDate
    {
        public const string BuildDateString = `"$buildDate`";
        public static readonly DateTime BuildDateUtc = DateTime.Parse(BuildDateString, null, DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal);
    }
}"

Set-Content -Path $outputFile -Value $class

将PowerScript文件另存为GenBuildDate.ps1并将其添加到项目中。最后,将以下行添加到Pre-Build事件中:

powershell -File $(ProjectDir)GenBuildDate.ps1 -outputFile $(ProjectDir)BuildDate.cs

确保BuildDate.cs包含在您的项目中。在任何操作系统上都像冠军一样工作!

答案 16 :(得分:2)

来自Jhon的“新方式”答案的小更新。

在使用ASP.NET / MVC时,您需要构建路径而不是使用CodeBase字符串

    var codeBase = assembly.GetName().CodeBase;
    UriBuilder uri = new UriBuilder(codeBase);
    string path = Uri.UnescapeDataString(uri.Path);

答案 17 :(得分:2)

我不确定,但也许Build Incrementer有帮助。

答案 18 :(得分:2)

您可以使用项目post-build事件将文本文件写入目标目录并使用当前日期时间。然后,您可以在运行时读取该值。这有点hacky,但它应该工作。

答案 19 :(得分:2)

一种不同的,PCL友好的方法是使用MSBuild内联任务将构建时间替换为应用程序上的属性返回的字符串。我们在具有Xamarin.Forms,Xamarin.Android和Xamarin.iOS项目的应用程序中成功使用此方法。

修改

通过将所有逻辑移动到SetBuildDate.targets文件中并使用Regex而不是简单的字符串替换来简化,以便每个构建都可以修改文件,而无需重置&#34;重置&#34 ;

MSBuild内联任务定义(保存在本示例的Xamarin.Forms项目本地的SetBuildDate.targets文件中):

<Project xmlns='http://schemas.microsoft.com/developer/msbuild/2003' ToolsVersion="12.0">

  <UsingTask TaskName="SetBuildDate" TaskFactory="CodeTaskFactory" 
    AssemblyFile="$(MSBuildToolsPath)\Microsoft.Build.Tasks.v12.0.dll">
    <ParameterGroup>
      <FilePath ParameterType="System.String" Required="true" />
    </ParameterGroup>
    <Task>
      <Code Type="Fragment" Language="cs"><![CDATA[

        DateTime now = DateTime.UtcNow;
        string buildDate = now.ToString("F");
        string replacement = string.Format("BuildDate => \"{0}\"", buildDate);
        string pattern = @"BuildDate => ""([^""]*)""";
        string content = File.ReadAllText(FilePath);
        System.Text.RegularExpressions.Regex rgx = new System.Text.RegularExpressions.Regex(pattern);
        content = rgx.Replace(content, replacement);
        File.WriteAllText(FilePath, content);
        File.SetLastWriteTimeUtc(FilePath, now);

   ]]></Code>
    </Task>
  </UsingTask>

</Project>

在目标BeforeBuild中的Xamarin.Forms csproj文件中调用上面的内联任务:

  <!-- To modify your build process, add your task inside one of the targets below and uncomment it. 
       Other similar extension points exist, see Microsoft.Common.targets.  -->
  <Import Project="SetBuildDate.targets" />
  <Target Name="BeforeBuild">
    <SetBuildDate FilePath="$(MSBuildProjectDirectory)\BuildMetadata.cs" />
  </Target>

FilePath属性设置为Xamarin.Forms项目中的BuildMetadata.cs文件,该文件包含一个带有字符串属性BuildDate的简单类,构建时间将替换为该类:

public class BuildMetadata
{
    public static string BuildDate => "This can be any arbitrary string";
}

将此文件BuildMetadata.cs添加到项目中。它将被每个构建修改,但以允许重复构建(重复替换)的方式进行修改,因此您可以根据需要在源代码管理中包含或省略它。

答案 20 :(得分:1)

您可以在构建过程中启动一个额外步骤,将日期戳写入文件,然后显示该文件。

在项目属性选项卡上,查看构建事件选项卡。可以选择执行pre或post构建命令。

答案 21 :(得分:1)

我使用了Abdurrahim的建议。但是,它似乎给出了一种奇怪的时间格式,并且还将当天的缩写添加为构建日期的一部分;例如:太阳12/24/2017 13:21:05.43。我只需要日期,所以我不得不使用子串消除其余的。

echo %date% %time% > "$(ProjectDir)\Resources\BuildDate.txt"添加到预构建事件后,我只是执行了以下操作:

string strBuildDate = YourNamespace.Properties.Resources.BuildDate;
string strTrimBuildDate = strBuildDate.Substring(4).Remove(10);

这里的好消息是它有效。

答案 22 :(得分:0)

如果这是一个Windows应用程序,您只需使用应用程序可执行文件路径: new System.IO.FileInfo(Application.ExecutablePath).LastWriteTime.ToString(“yyyy.MM.dd”)

答案 23 :(得分:0)

可能是 Assembly execAssembly = Assembly.GetExecutingAssembly(); var creationTime = new FileInfo(execAssembly.Location).CreationTime; // "2019-09-08T14:29:12.2286642-04:00"

答案 24 :(得分:0)

我刚刚添加了预构建事件命令:

powershell -Command Get-Date -Format 'yyyy-MM-ddTHH:mm:sszzz' > Resources\BuildDateTime.txt

在项目属性中生成一个资源文件,然后可以从代码中轻松读取该文件。

答案 25 :(得分:0)

对于我的项目(.Net Core 2.1 Web 应用程序)的建议解决方案,我遇到了困难。我结合了上面的各种建议并进行了简化,并将日期转换为我需要的格式。

回声命令:

echo Build %DATE:~-4%/%DATE:~-10,2%/%DATE:~-7,2% %time% > "$(ProjectDir)\BuildDate.txt"

代码:

Logger.Info(File.ReadAllText(@"./BuildDate.txt").Trim());

似乎可以。输出:

2021-03-25 18:41:40,877 [1] INFO Config - Build 2021/03/25 18:41:37.58

没什么特别的,我只是结合了这里的建议和其他相关问题,并进行了简化。