显示生成date
我目前有一个应用程序在其标题窗口中显示内部版本号。 这对于大多数用户来说没有任何意义,因为他们想知道他们是否拥有最新版本 – 他们倾向于将其称为“上周四”,而不是构build1.0.8.4321。
计划是把build设date,而不是 – 所以“应用程序build于21/10/2009”例如。
我正在努力寻找一个编程的方式来把生成date作为一个文本string使用像这样。
对于版本号,我使用:
Assembly.GetExecutingAssembly().GetName().Version.ToString()
在定义了如何出现之后。
我想在编译date(和时间,奖励点)的东西。
在这里的指针非常赞赏(如果适当的话可以辩解一下),或者整洁的解决scheme…
杰夫·阿特伍德(Jeff Atwood)在确定build设date(Build Date Date)方面遇到了一些难题 。
最可靠的方法是从embedded在可执行文件中的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核心1.0 工作 ,但在.Net核心1.1版本(1900 – 2020年范围随机年) 停止工作,
在下面添加预构build事件命令行:
echo %date% %time% > "$(ProjectDir)\Resources\BuildDate.txt"
添加这个文件作为资源,现在你的资源中有“BuildDate”string。
要创build资源,请参阅如何在.NET中创build和使用资源 。
新的方式
我改变了我的想法,目前使用这个技巧来获得正确的build立date。
#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#编译器)实际上会提供自动构build和修订号1.0.*
将会发生的情况是,build立时间等于当地时间2000年1月1日以来的天数,修正时间等于当地时间午夜以来的秒数除以2。
请参阅社区内容, 自动构build和修订版本号
例如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)
在下面添加预构build事件命令行:
echo %date% %time% > "$(ProjectDir)\Resources\BuildDate.txt"
添加这个文件作为资源,现在你的资源中有“BuildDate”string。
将文件插入资源(作为公共文本文件)后,我通过访问它
string strCompTime = Properties.Resources.BuildDate;
要创build资源,请参阅如何在.NET中创build和使用资源 。
我只是C#新手所以也许我的回答听起来很愚蠢 – 我显示生成date从可执行文件上次写入date:
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方法,但得到了奇怪的结果:从命令的date是2012-05-29,但窗口资源pipe理器的date显示2012-05-23。 search这个差异后,我发现该文件可能创build于2012-05-23(如Windows资源pipe理器所示),但在2012-05-29复制到当前文件夹(如File.GetCreationTime命令所示) – so为了安全起见,我正在使用File.GetLastWriteTime命令。
Zalek
对于需要在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中的任何工作)。
这里没有讨论的选项是将自己的数据插入到AssemblyInfo.cs中,“AssemblyInformationalVersion”字段似乎是合适的 – 我们有几个项目,我们在做一些类似的构build步骤(但是我并不完全满意有效的方式,所以不想重现我们所得到的)。
有关于codeproject的主题的文章: http : //www.codeproject.com/KB/dotnet/Customizing_csproj_files.aspx
您可以使用项目生成后事件将文本文件写入到目标目录中,并使用当前的date时间。 然后您可以在运行时读取该值。 这是一个有点哈克,但它应该工作。
我不确定,但也许Build Incrementer帮助。
不同的,PCL友好的方法是使用MSBuild内联任务来将构build时间replace为应用程序属性返回的string。 我们正在使用这种方法成功地应用了Xamarin.Forms,Xamarin.Android和Xamarin.iOS项目。
编辑:
通过将所有逻辑移动到SetBuildDate.targets
文件中,并使用Regex
而不是简单的stringreplace来简化,以便每个构build可以修改文件而不需要“重置”。
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
文件,该项目包含一个带有string属性BuildDate
的简单类,构build时间将被replace为BuildDate
:
public class BuildMetadata { public static string BuildDate => "This can be any arbitrary string"; }
将此文件BuildMetadata.cs
添加到项目。 每个构build都会修改它,但是可以通过重复构build(重复replace)的方式进行修改,因此您可以根据需要在源代码pipe理中包含或省略它。
我很惊讶没有人提到的一种方法是使用T4文本模板来生成代码。
<#@ 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); } } } }
优点:
- 语言环境无关
- 除了编译时间以外,还有更多的function
缺点:
- 仅适用于您控制源的库
- 需要configuration您的项目(并构build服务器,如果没有select它) 在预生成步骤中执行模板 。 (另请参见无VS的T4 )。
上面的方法可以通过在内存中使用文件的图像(而不是从存储重新读取)调整已经加载进程中的程序集:
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; } }
您可以在构build过程中启动一个额外的步骤,将date戳记写入可以显示的文件。
在项目属性选项卡上查看构build事件选项卡。 有一个选项可以执行预编译或后编译命令。
如果这是一个Windows应用程序,则可以使用应用程序可执行文件path:new System.IO.FileInfo(Application.ExecutablePath).LastWriteTime.ToString(“yyyy.MM.dd”)