首页 文章

显示构建日期

提问于
浏览
231

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

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

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

对于内部版本号,我用过:

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

在确定了那些如何出现之后 .

我想在编译日期(和时间,奖励积分)这样的东西 .

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

22 回答

  • 8

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

    它利用T4自动化构建日期时间戳 . 有几个版本(不同的分支),包括一个给你当前签出的分支的Git哈希,如果你是那种东西 .

    披露:我写了模块 .

  • 80

    杰夫阿特伍德在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,但 stopped working after .Net Core 1.1 版本(在1900-2020范围内随机提供年份)

  • 36

    新方式

    我改变了主意,目前使用这个技巧来获得正确的构建日期 .

    #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

    例如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)
    
  • 16

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

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

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

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

  • 8

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

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

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

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

    string strCompTime = Properties.Resources.BuildDate;
    

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

  • 7

    我很惊讶的一种方法是使用T4 Text Templates进行代码生成 .

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

    优点:

    • 与语言环境无关

    • 允许的不仅仅是编译时间

    缺点:

  • 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

  • 2

    关于从程序集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编译成为每个构建的一部分 . 我还没有在 生产环境 部署期间面对这个问题的解决方案,但我希望类似的东西能够做到正确 .

  • 2

    对于需要在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中的任何内容) .

  • 1

    这里没有讨论的选项是将您自己的数据插入AssemblyInfo.cs,“AssemblyInformationalVersion”字段似乎合适 - 我们有几个项目,我们正在做类似于构建步骤的事情(但是我对它并不完全满意)这样的方式,所以不要真的想重现我们已经得到的东西) .

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

  • 11

    通过在内存中使用文件的图像(而不是从存储中重新读取它),可以为程序集 already loaded within the process 调整上述方法:

    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;
        }
    }
    
  • 10

    这里有很多很棒的答案,但我觉得我可以添加自己的,因为简单,性能(与资源相关的解决方案相比)跨平台(也适用于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不会喜欢它 . 您可以忽略他或者将部分类添加到项目中,但无论如何它都可以工作 .

  • 1

    在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();
                }
            }
        }
    
  • 0

    对于.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);
    
  • 5

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

  • 0

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

  • 4

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

    EDIT:

    通过将所有逻辑移动到 SetBuildDate.targets 文件中简化,并使用 Regex 而不是简单的字符串替换,以便每个构建都可以修改文件而不使用"reset" .

    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 添加到项目中 . 它将被每个构建修改,但是以允许重复构建(重复替换)的方式进行修改,因此您可以根据需要在源代码控制中包含或省略它 .

  • 2

    我需要一个可以在任何平台(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包含在您的项目中 . 在任何操作系统上都像冠军一样!

  • 2

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

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

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

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

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

  • 84

    我使用了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);
    

    这里的好消息是它有效 .

  • 2

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

相关问题