Отображение даты сборки - PullRequest
       64

Отображение даты сборки

240 голосов
/ 21 октября 2009

В настоящее время у меня есть приложение, отображающее номер сборки в окне заголовка. Это хорошо, за исключением того, что это ничего не значит для большинства пользователей, которые хотят знать, есть ли у них последняя сборка - они склонны называть ее "прошлой четвергой", а не сборкой 1.0.8.4321.

Вместо этого планируется указать дату сборки - например, «Приложение построено 21/10/2009».

Я изо всех сил пытаюсь найти программный способ извлечения даты сборки в виде текстовой строки для такого использования.

Для номера сборки я использовал:

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

после определения, как они возникли.

Я бы хотел что-то подобное для даты компиляции (и времени, для бонусных баллов).

Здесь очень ценятся указатели (извините за каламбур, если необходимо) или более аккуратные решения ...

Ответы [ 23 ]

348 голосов
/ 21 октября 2009

Джеффу Этвуду было что сказать по этому вопросу в Определение даты сборки трудным путем .

Оказывается, наиболее надежным методом является получение метки времени компоновщика из PE-заголовка , встроенного в исполняемый файл - некоторый код C # (Джо Спиви) для этого из комментариев к статье Джеффа: 1007 *

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)

87 голосов
/ 19 марта 2013

Добавьте ниже в командную строку события перед сборкой:

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

Добавить этот файл как ресурс, теперь у вас есть строка 'BuildDate' в ваших ресурсах.

Чтобы создать ресурсы, см. Как создавать и использовать ресурсы в .NET .

86 голосов
/ 21 октября 2009

Путь

Как указано @ c00000fd в комментариях . Microsoft меняет это. И хотя многие люди не используют последнюю версию своего компилятора, я подозреваю, что это изменение делает этот подход, безусловно, плохим. И хотя это забавное упражнение, я бы порекомендовал людям просто вставлять дату сборки в свой бинарный файл любым другим необходимым способом, если важно отслеживать дату сборки самого бинарного файла.

Это можно сделать с помощью некоторой тривиальной генерации кода, которая, вероятно, уже является первым шагом в вашем скрипте сборки. Это и тот факт, что инструменты 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

Старый путь

Ну, как вы генерируете номера сборки? Visual Studio (или компилятор C #) фактически предоставляет номера автоматической сборки и редакции, если вы измените атрибут AssemblyVersion, например, на. 1.0.*

То, что произойдет, это то, что сборка будет равна числу дней с 1 января 2000 года по местному времени, а пересмотр будет равен количеству секунд с полуночи по местному времени, деленному на 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)
41 голосов
/ 04 марта 2014

Добавьте ниже в командную строку события перед сборкой:

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

Добавьте этот файл в качестве ресурса, теперь у вас есть строка 'BuildDate' в ваших ресурсах.

После вставки файла в Ресурс (в виде открытого текстового файла) я получил к нему доступ через

string strCompTime = Properties.Resources.BuildDate;

Чтобы создать ресурсы, см. Как создавать и использовать ресурсы в .NET .

20 голосов
/ 19 октября 2016

Один из подходов, о которых я удивляюсь, еще никто не упомянул, - это использование 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); } }
    }
}

Плюсы:

  • Locale независимый
  • Позволяет намного больше, чем просто время компиляции

Минусы:

16 голосов
/ 04 января 2018

Что касается техники извлечения информации о дате / версии сборки из байтов PE-заголовка сборки, Microsoft изменила параметры сборки по умолчанию, начиная с Visual Studio 15.4. Новое значение по умолчанию включает в себя детерминированную компиляцию, которая делает действительную временную метку и автоматически увеличивающиеся номера версий ушедшими в прошлое. Поле отметки времени все еще присутствует, но оно заполняется постоянным значением, которое является хешем того или иного, но не указанием времени сборки.

Подробный фон

Для тех, кто отдает предпочтение временной метке над детерминированной компиляцией, есть способ переопределить новое значение по умолчанию. Вы можете включить тег в файл .csproj интересующей сборки следующим образом:

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

Обновление: Я одобряю решение для текстового шаблона T4, описанное в другом ответе здесь. Я использовал это, чтобы решить мою проблему чисто, не теряя преимущества детерминированной компиляции. Одно предостережение по этому поводу заключается в том, что Visual Studio запускает компилятор T4 только при сохранении файла .tt, а не во время сборки. Это может быть неудобно, если вы исключите результат .cs из управления исходным кодом (поскольку вы ожидаете, что он будет сгенерирован), а другой разработчик проверит код. Без сохранения у них не будет файла .cs. В nuget есть пакет (который называется AutoT4), который делает компиляцию T4 частью каждой сборки. Я еще не сталкивался с решением этой проблемы во время развертывания производства, но я ожидаю, что что-то подобное сделает это правильным.

12 голосов
/ 15 июня 2012

Я просто новичок в 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 Explorer показала 2012-05-23. После поиска этого несоответствия я обнаружил, что файл, вероятно, был создан 2012-05-23 (как показано в проводнике Windows), но скопирован в текущую папку 2012-05-29 (как показано командой File.GetCreationTime) - так чтобы быть в безопасности, я использую команду File.GetLastWriteTime.

ZAlek

10 голосов
/ 13 июня 2017

Приведенный выше метод можно настроить для сборок , уже загруженных в процессе , используя изображение файла в памяти (в отличие от повторного чтения его из хранилища):

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 голосов
/ 21 августа 2013

Для всех, кому нужно получить время компиляции в 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).

8 голосов
/ 21 октября 2009

Опция, не обсуждаемая здесь, заключается в вставке ваших собственных данных в AssemblyInfo.cs, поле «AssemblyInformationalVersion» кажется подходящим - у нас есть пара проектов, в которых мы делали нечто подобное в качестве шага сборки (однако я не совсем доволен тем, как это работает, поэтому не хочу воспроизводить то, что у нас есть).

Есть статья на эту тему по codeproject: http://www.codeproject.com/KB/dotnet/Customizing_csproj_files.aspx

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...