我目前有一个应用程序在其标题窗口中显示内部版本号.这很好,除非对大多数用户没有任何意义,他们想知道他们是否拥有最新版本 - 他们倾向于将其称为"上周四",而不是构建1.0.8.4321.
计划是将构建日期放在那里 - 所以"应用程序构建于2009年10月21日".
我正在努力寻找一种程序化的方法来将构建日期作为文本字符串拉出来像这样使用.
对于内部版本号,我使用了:
Assembly.GetExecutingAssembly().GetName().Version.ToString()
在确定了那些如何出现之后.
我想在编译日期(和时间,奖励积分)这样的东西.
这里的指针非常赞赏(如果合适的话,请原谅双关语),或更整洁的解决方案......
杰夫阿特伍德在确定构建日期方面有一些关于这个问题的说法.
最可靠的方法是从可执行文件中嵌入的PE头中检索链接器时间戳- 一些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范围内提供随机年份)
在下面添加到预构建事件命令行:
echo %date% %time% > "$(ProjectDir)\Resources\BuildDate.txt"
将此文件添加为资源,现在您的资源中包含"BuildDate"字符串.
要创建资源,请参阅如何在.NET中创建和使用资源.
正如@ 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.
请参阅社区内容,自动构建和修订号
例如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)
在下面添加到预构建事件命令行:
echo %date% %time% > "$(ProjectDir)\Resources\BuildDate.txt"
将此文件添加为资源,现在您的资源中包含"BuildDate"字符串.
将文件插入资源(作为公共文本文件)后,我通过访问它
string strCompTime = Properties.Resources.BuildDate;
要创建资源,请参阅如何在.NET中创建和使用资源.
我很惊讶的一种方法是使用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); } } } }
优点:
语言环境无关
允许的不仅仅是编译时间
缺点:
仅适用于控制源的库
需要配置项目(和构建服务器,如果没有提取它),以在预构建步骤中执行模板.(另见没有VS的T4).
关于从程序集PE头的字节中提取构建日期/版本信息的技术,Microsoft已更改从Visual Studio 15.4开始的默认构建参数.新的默认值包括确定性编译,它使有效的时间戳和自动递增的版本号成为过去.时间戳字段仍然存在,但它会填充永久值,该值是某个或其他内容的散列,但不是任何构建时间的指示.
http://blog.paranoidcoding.com/2016/04/05/deterministic-builds-in-roslyn.html 这里有一些详细的背景
对于那些优先考虑确定性编译的有用时间戳的人,有一种方法可以覆盖新的默认值.您可以在感兴趣的程序集的.csproj文件中包含一个标记,如下所示:
... false
更新:我赞同此处另一个答案中描述的T4文本模板解决方案.我用它来干净地解决我的问题而不会失去确定性编译的好处.有一点需要注意的是,Visual Studio只在保存.tt文件时运行T4编译器,而不是在构建时运行.如果从源代码控制中排除.cs结果(因为您希望生成它)并且另一个开发人员检出代码,这可能会很麻烦.没有重新保存,他们将没有.cs文件.nuget上有一个包(我认为叫做AutoT4),它使T4编译成为每个构建的一部分.我还没有在生产部署期间面对这个问题的解决方案,但我希望类似的东西可以做到.
我只是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
这里有很多很棒的答案,但我觉得我可以添加自己的,因为简单,性能(与资源相关的解决方案相比)跨平台(也适用于Net Core)和避免使用任何第三方工具.只需将此msbuild目标添加到csproj即可.
现在你已经Builtin.CompileTime
或者new DateTime(Builtin.CompileTime, DateTimeKind.Utc)
如果你需要那样的话.
ReSharper不会喜欢它.您可以忽略他或者将部分类添加到项目中,但无论如何它都可以工作.
对于需要在Windows 8/Windows Phone 8中获得编译时间的任何人:
public static async TaskRetrieveLinkerTimestamp(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 TaskRetrieveLinkerTimestampAsync(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 (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中的任何内容).
通过在内存中使用文件的映像(而不是从存储中重新读取它),可以调整上面的方法,以便已经在进程中加载的程序集:
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; } }
这里没有讨论的选项是将你自己的数据插入到AssemblyInfo.cs中,"AssemblyInformationalVersion"字段似乎是合适的 - 我们有几个项目,我们正在做一些类似于构建步骤的事情(但是我对它并不完全满意)这样做的方式并不是真的想要重现我们所拥有的东西.
在codeproject上有一篇关于这个主题的文章:http://www.codeproject.com/KB/dotnet/Customizing_csproj_files.aspx
对于.NET Core项目,我改编了Postlagerkarte的答案,用构建日期更新程序集版权字段.
以下内容可以直接添加到PropertyGroup
csproj中的第一个:
Copyright © $([System.DateTime]::UtcNow.Year) Travis Troyer ($([System.DateTime]::UtcNow.ToString("s")))
或者将内部表达式直接粘贴到Visual Studio中项目属性的"包"部分中的"版权"字段中:
Copyright © $([System.DateTime]::UtcNow.Year) Travis Troyer ($([System.DateTime]::UtcNow.ToString("s")))
这可能有点令人困惑,因为Visual Studio将评估表达式并在窗口中显示当前值,但它也将在后台适当地更新项目文件.
您可以将
上面的元素放入Directory.Build.props
解决方案根目录中的文件中,并将其自动应用于目录中的所有项目,假设每个项目都不提供自己的版权值.
Copyright © $([System.DateTime]::UtcNow.Year) Travis Troyer ($([System.DateTime]::UtcNow.ToString("s")))
Directory.Build.props:自定义您的构建
示例表达式将为您提供如下版权:
Copyright © 2018 Travis Troyer (2018-05-30T14:46:23)
您可以从Windows中的文件属性查看版权信息,也可以在运行时获取它:
var version = FileVersionInfo.GetVersionInfo(Assembly.GetEntryAssembly().Location); Console.WriteLine(version.LegalCopyright);
在2018年,上述某些解决方案不再起作用或不能与.NET Core一起使用.
我使用以下方法,这很简单,适用于我的.NET Core 2.0项目.
将以下内容添加到PropertyGroup内的.csproj中:
$([System.DateTime]::Now)
这定义了一个PropertyFunction,您可以在预构建命令中访问该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(); } } }
我需要一个可在任何平台(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包含在您的项目中。在任何操作系统上都像冠军!