显示构建日期

311
我现在有一个应用程序,在其标题窗口中显示构建编号。这很好,但对于大多数用户来说毫无意义,他们想知道自己是否拥有最新的构建版本——他们倾向于称其为“上周四”的版本,而不是1.0.8.4321。
计划改成在那里放置构建日期,例如“应用程序创建于21/10/2009”。
我正在努力找到一种编程方式来提取构建日期作为文本字符串以供使用。
对于构建编号,我使用了:
Assembly.GetExecutingAssembly().GetName().Version.ToString()

对于编译日期(如果有时间更好),我想要类似那样的东西。

非常感谢指针(如果适用,请原谅双关语),或更加简洁的解决方案...


2
我尝试了提供的方法来获取程序集的构建数据,这在简单的情况下是有效的,但如果两个程序集合并在一起,我得到的构建时间不正确,比实际时间晚了一个小时。有什么建议吗? - user662160
32个回答

378

Jeff Atwood在 这篇文章 中就这个问题提出了一些看法。

最可靠的方法是从可执行文件中嵌入的PE头中检索链接器时间戳 -- Jeff文章评论中的Joe Spivey提供了相应的C#代码:

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范围内的随机年份。


10
我对此的语气有所改变,但挖掘实际PE头时仍需非常小心。就我所知,这个PE的东西比使用版本号要可靠得多,而且我想将版本号与构建日期分开分配。 - John Leidegren
7
我喜欢这个,正在使用它,但是倒数第二行的 .AddHours() 有些繁琐,并且(我认为)不考虑DST。如果你想使用本地时间,应该使用更简洁的 dt.ToLocalTime(); 。中间部分也可以通过 using() 块大大简化。 - JLRishe
9
是的,这个在使用 .net core(1940 年代、1960 年代等)时对我也失效了。 - eoleary
10
虽然PE头的使用在今天似乎是一个不错的选择,但需注意微软正在尝试确定性构建(这将使此头文件无用),甚至可能在未来的C#编译器版本中将其设置为默认选项(出于良好的原因)。这篇文章很有价值:http://blog.paranoidcoding.com/2016/04/05/deterministic-builds-in-roslyn.html。 这里有与.NET Core相关的答案(简而言之:“这是故意的”):https://developercommunity.visualstudio.com/content/problem/35873/invalid-timestamp-in-pe-header-of-compiled-net-cor.html。 - Paweł Bulwan
20
对于那些发现此方法不再起作用的人,问题并不是.NET Core的问题。请参阅我的答案,了解有关Visual Studio 15.4开始的新构建参数默认值的信息。 - Tom
显示剩余18条评论

144

请在预构建事件命令行中添加以下内容:

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

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

要创建资源,请参见如何在.NET中创建和使用资源


6
+1,简单而有效。我甚至可以用这样一行代码从文件中获取值:String buildDate = <MyClassLibraryName>.Properties.Resources.BuildDate。 - davidfrancis
16
另一个选项是创建一个类:(第一次编译后必须包含在项目中) echo namespace My.app.namespace { public static class Build { public static string Timestamp = "%DATE% %TIME%".Substring(0,16);}} > "$(ProjectDir)\BuildTimestamp.cs" - - - --> 然后可以通过 Build.Timestamp 调用它。 - FabianSilva
13
这是一个很好的解决方案。唯一的问题是,%date%和%time%命令行变量是本地化的,因此输出将根据用户使用的Windows语言而有所不同。 - V.S.
2
+1,这是比读取PE头更好的方法 - 因为有几种情况下那根本行不通(例如Windows Phone应用程序)。 - Matt Whitfield
24
聪明。你也可以使用PowerShell来更精确地控制格式,例如获取UTC日期时间并按照ISO8601格式进行格式化:powershell -Command "((Get-Date).ToUniversalTime()).ToString("s") | Out-File '$(ProjectDir)Resources\BuildDate.txt'" - dbruning
显示剩余11条评论

99

路径

正如在 评论 中 @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属性为例如1.0.*,Visual Studio(或C#编译器)实际上会提供自动构建和修订号。

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

请参见社群内容,自动生成构建和修订号码

例如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
如果TimeZone.CurrentTimeZone.IsDaylightSavingTime(buildDateTime) == true,我刚刚添加了一个小时。 - e4rthdog
5
很遗憾,我在没有彻底审查的情况下使用了这种方法,在生产中出现了问题。问题在于当JIT编译器启动时,PE头信息会被改变,因此被负评。现在我不得不做一些不必要的“研究”来解释为什么我们看到的安装日期是构建日期。 - Jason D
18
在哪个宇宙中,你的问题变成了我的问题?你如何证明自己的投票反对只是因为你遇到了这个实现没考虑到的问题?你免费获得了它并且测试不周全。另外,你有什么证据证明头部被 JIT 编译器重写了?你是从进程内存还是文件中读取这些信息? - John Leidegren
7
我注意到,如果你在Web应用程序中运行,.Codebase属性似乎是一个URL(file:// c:/path/to/binary.dll)。 这会导致File.Exists调用失败。我使用"assembly.Location"代替CodeBase属性解决了这个问题。 - mdryden
8
不要依赖Windows PE头来进行此操作。自从Windows 10和可再现构建技术出现以来,IMAGE_FILE_HEADER :: TimeDateStamp字段被设置为随机数值,并且不再是时间戳。 - c00000fd
显示剩余14条评论

65

将以下内容添加到预构建事件命令行中:

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

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

将文件插入资源(作为公共文本文件),我通过以下方式访问它

string strCompTime = Properties.Resources.BuildDate;

要创建资源,请参见如何在.NET中创建和使用资源


1
@DavidGorsline - 评论的 Markdown 是正确的,因为它在引用 这个答案。我声望不够,无法回滚您的更改,否则我会自己处理。 - Wai Ha Lee
2
@Wai Ha Lee - a) 你引用的答案并没有给出实际检索编译日期/时间的代码。b) 当时我还没有足够的声望来添加评论(我本来会这样做的),只能发布帖子。所以c) 我发布了完整的答案,让人们可以在一个区域获取所有细节。 - brewmanz
如果您看到的是Úte%而不是%date%,请查看此处:https://developercommunity.visualstudio.com/content/problem/237752/prebuild-event-command-line-date-issue.html简而言之,执行以下操作:echo %25date%25 %25time%25 - Qodex

63

我惊讶的发现还没有人提到的一种方法是使用T4文本模板进行代码生成。

<#@ 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); } }
    }
}

优点:

  • 与语言环境无关
  • 不仅仅限于编译时的时间

缺点:


1
所以,这现在是最佳答案。还需要324个点赞才能成为最高票答案:)。Stackoverflow需要一种方式来展示最快的攀升者。 - pauldendulk
2
@pauldendulk,没有什么帮助,因为最受欢迎的答案和被接受的答案几乎总是最快获得赞成票。自从我回答这个问题以来,这个问题的被接受的答案已经获得了+60/-2的支持。 - Peter Taylor
4
如果其他人想知道,这是让它在VS 2017上运行所需的步骤:我必须将其设置为设计时T4模板(花费了我一些时间来弄清楚,我首先添加了一个预处理器模板)。我还必须将此程序集包括进项目的引用中:Microsoft.VisualStudio.TextTemplating.Interfaces.10.0。最后,在命名空间之前,我的模板必须包含 "using System;",否则对DateTime的引用会失败。 - Andy
2
搞定了。请确保安装了答案中链接的 Clarius.TransformOnBuild NuGet 包(当然),并且模板在最后使用 TextTemplatingPreProcessor 自定义工具...现在可以运行了。 - Andy
1
我非常喜欢T4,但它不适合这种用例。在框架项目中配置编译时生成很麻烦,在带有新的“SDK”csproj系统的.Net Core中几乎不可能。我只需要构建日期,因此更简单和可靠的WriteLinesToFile MSBuild任务方法更好: https://dev59.com/D3I-5IYBdhLWcg3w8dYQ#50905092 - Artfunkel
显示剩余4条评论

58

这里有许多优秀的答案,但我觉得我可以加入我的答案,因为它简单,性能好(与资源相关的解决方案相比),跨平台(也适用于Net Core)并且避免了任何第三方工具。只需将此msbuild目标添加到csproj中。

<Target Name="Date" BeforeTargets="BeforeBuild">
    <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.CompileTime,例如:

var compileTime = new DateTime(Builtin.CompileTime, DateTimeKind.Utc);

ReSharper不会喜欢这样做。你可以忽略它或向项目中添加一个部分类,但无论如何它都能正常工作。

更新:现在ReSharper在选项的第一页中有一个选项:“MSBuild访问”、“在每次编译后从MSBuild获取数据”。这有助于生成的代码的可见性。


2
我可以使用ASP.NET Core 2.1构建并在本地开发(运行网站),但是从VS 2017进行Web部署发布时会出现错误:“当前上下文中不存在'Builtin'的名称”。另外,如果我从Razor视图访问Builtin.CompileTime - Jeremy Cook
1
@Matteo,如答案中所述,您可以使用“Builtin.CompileTime”或“new DateTime(Builtin.CompileTime,DateTimeKind.Utc)”。Visual Studio IntelliSense能够立即看到这一点。旧版的ReSharper可能会在设计时发出警告,但看起来他们在新版本中已经解决了这个问题。https://clip2net.com/s/46rgaaO - Dmitry Gusarov
2
我使用了这个版本,所以不需要额外的代码来获取日期。同时,最新版本的resharper也没有任何问题。<WriteLinesToFile File="$(IntermediateOutputPath)BuildInfo.cs" Lines="using System %3B internal static partial class BuildInfo { public static long DateBuiltTicks = $([System.DateTime]::UtcNow.Ticks) %3B public static DateTime DateBuilt => new DateTime(DateBuiltTicks, DateTimeKind.Utc) %3B }" Overwrite="true" /> - Softlion
11
在 Visual Studio 2022 中,我不得不将 ItemGroup 部分移出 Target 块才能使它正常工作。 - Daniel Ellis
3
对于任何想知道 %3B 是什么的人,它是一个 URL 编码的分号 ; - J Scott
显示剩余8条评论

33

对于 .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: 自定义构建

输出

该示例表达式将会给你生成如下版权声明:

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

检索

您可以在 Windows 中查看文件属性中的版权信息,或者在运行时获取:

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

Console.WriteLine(version.LegalCopyright);

24

关于从程序集 PE 头的字节中提取构建日期/版本信息的技术,微软在 Visual Studio 15.4 开始更改了默认的构建参数。新的默认设置包括确定性编译,使得有效时间戳和自动递增的版本号成为过去的事情。时间戳字段仍然存在,但它被填充了一个永久值,该值是某些哈希值,而不是任何构建时间的指示。

这里有一些详细的背景资料

对于那些优先考虑有用时间戳的人,可以通过在感兴趣的程序集的 .csproj 文件中包含一个标记来覆盖新的默认设置,具体如下:

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

更新: 我支持在此处另一个答案中描述的T4文本模板解决方案。我使用它清洁地解决了我的问题,而不会失去确定性编译的好处。关于它的一个警告是,Visual Studio仅在保存.tt文件时运行T4编译器,而不是在构建时运行。如果你将.cs结果从源代码控制中排除(因为你希望它被生成),并且另一个开发人员检出了代码,则可能会很棘手。如果没有重新保存,他们将没有.cs文件。NuGet上有一个包(我认为称为AutoT4),可以使T4编译成为每个构建的一部分。我还没有在生产部署期间面对此解决方案,但我期望有类似的东西可以解决它。


这解决了我的问题,在一个使用最旧答案的 sln 中。 - pauldendulk
您对T4的谨慎是完全合理的,但请注意它已经出现在我的答案中。 - Peter Taylor
但现在每次编译后,你的二进制文件都不同,所以确定性编译就变得无用了,不是吗? - undefined

21
对于.NET Core (.NET 5+) 的项目,可以这样做。好处是不需要添加或嵌入任何文件,也不需要使用 T4 或预构建脚本。
在您的项目中添加一个类,如下所示:
namespace SuperDuper
{
    [AttributeUsage(AttributeTargets.Assembly)]
    public class BuildDateTimeAttribute : Attribute
    {
        public DateTime Built { get; }
        public BuildDateTimeAttribute(string date)
        {
            this.Built = DateTime.Parse(date);
        }
    }
}

更新您项目的.csproj文件,包含类似以下内容的内容:
<ItemGroup>
    <AssemblyAttribute Include="SuperDuper.BuildDateTime">
        <_Parameter1>$([System.DateTime]::Now.ToString("s"))</_Parameter1>
    </AssemblyAttribute>
</ItemGroup>

请注意,_Parameter1 是一个神奇的名称 - 它表示我们的 BuildDateTime 属性类的构造函数的第一个(也是唯一的)参数。默认情况下,它期望它的类型是 string
这就是记录您程序集的构建日期时间所需的全部内容。
然后,要读取您程序集的构建日期时间,请执行类似以下的操作:
private static DateTime? getAssemblyBuildDateTime()
{
    var assembly = System.Reflection.Assembly.GetExecutingAssembly();
    var attr = Attribute.GetCustomAttribute(assembly, typeof(BuildDateTimeAttribute)) as BuildDateTimeAttribute;
    return attr?.Built;
}

注意1(根据评论中的Flydog57):如果您的.csproj文件中列出了属性GenerateAssemblyInfo,并将其设置为false,则构建将不会生成程序集信息,您的程序集中将没有BuildDateTime信息。因此,要么不在.csproj文件中提及GenerateAssemblyInfo(这是新项目的默认行为,如果没有明确设置为false,则GenerateAssemblyInfo默认为true),要么将其明确设置为true。
注意2(根据评论中的Teddy):在_Parameter1的示例中,我们使用::Now来使用DateTime.Now,它是您计算机上的本地日期和时间,受适用的夏令时和本地时区的影响。如果您希望,可以使用::UtcNow来使用DateTime.UtcNow,以便将构建日期和时间记录为UTC/GMT。

完美的解决方案,唯一的问题是在“... DateTime dt))”处缺少一个圆括号。 - Ivan Silkin
@IvanSilkin,缺少圆括号已添加,谢谢。 - Jinlye
3
这个方法非常有效,在适用的平台上应该被接受为答案(在.NET 6上测试过)。然而,重要的是在<PropertyGroup>中设置<GenerateAssemblyInfo>true或省略。如果设置为false,这将导致沮丧。 - Flydog57
@Flydog57 回答已更新,提到了 GenerateAssemblyInfo 问题。 - Jinlye
到目前为止,这是所有建议中最好的。提示:您也可以使用UtcNow。 - Teddy
显示剩余4条评论

18

我只是 C# 的新手,所以我的答案可能听起来有点傻 - 我显示构建日期是根据可执行文件最后写入的日期:

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资源管理器显示的日期是2012-05-23。在寻找这个差异时,我发现该文件可能是在2012-05-23创建的(如Windows资源管理器所示),但在2012-05-29被复制到当前文件夹(如File.GetCreationTime命令所示) - 为了安全起见,我使用File.GetLastWriteTime命令。

5
我不确定这个可执行文件是否可以防止被复制到其他驱动器、计算机或网络中。 - Stealth Rabbi
这是脑海中首先想到的,但你知道它不可靠,有许多软件用于在网络上移动文件,在下载后不会更新属性,我会选择@Abdurrahim的答案。 - Mubashar
我知道这很老,但我刚刚发现使用类似代码的安装过程(至少在使用ClickOnce时)会更新程序集文件时间。这并不是非常有用。虽然不确定是否适用于此解决方案。 - bobwki
你可能真正想要的是 LastWriteTime,因为它准确地反映了可执行文件实际更新的时间。 - David R Tribble
抱歉,可执行文件的写入时间并不是构建时间的可靠指示。文件时间戳可以由于许多超出您控制范围的事情而被重写。 - Tom
对我没有用。返回了文件安装到新计算机上的日期。 - Ray White

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接