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

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

План заключается в том, чтобы вместо этого добавить дату сборки там - например, "Приложение построено на 21/10/2009".

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

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

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

после определения того, как они появились.

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

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

Ответ 1

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

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

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();

UPDATE: метод работал на .Net Core 1.0, но прекратил работу после выпуска .Net Core 1.1 (дает случайные годы в диапазоне 1900-2020)

Ответ 2

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

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

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

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

Ответ 3

Путь

Как указано @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 (или компилятор С#) фактически предоставляет номера автоматической сборки и редакции, если вы измените атрибут 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)

Ответ 4

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

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

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

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

string strCompTime = Properties.Resources.BuildDate;

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

Ответ 5

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

Минусы:

Ответ 6

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

Некоторые подробные сведения здесь

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

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

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

Ответ 7

Я просто новичок С#, поэтому, возможно, мой ответ звучит глупо - я показываю дату сборки с даты, когда исполняемый файл был в последний раз записан:

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

ZAlek

Ответ 8

Для тех, кому требуется время компиляции в 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;
    }

ПРИМЕЧАНИЕ. Во всех случаях вы работаете в песочнице, так что вы сможете получить время компиляции сборок, которые вы развертываете с вашим приложением. (т.е. это не будет работать ни на чем в ПКК).

Ответ 9

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

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.CompileTime или new DateTime(Builtin.CompileTime, DateTimeKind.Utc) если вам это нужно.

ReSharper это не понравится. Вы можете игнорировать его или добавить частичный класс в проект, но он все равно работает.

Ответ 11

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

Там есть статья на тему о кодепероке: http://www.codeproject.com/KB/dotnet/Customizing_csproj_files.aspx

Ответ 12

В 2018 году некоторые из вышеперечисленных решений больше не работают или не работают с .NET Core.

Я использую следующий подход, который прост и работает для моего проекта .NET Core 2.0.

Добавьте следующее в ваш .csproj внутри PropertyGroup:

    <Today>$([System.DateTime]::Now)</Today>

Это определяет PropertyFunction, к которой вы можете получить доступ в вашей команде pre build.

Ваша предварительная сборка выглядит так

echo $(today) > $(ProjectDir)BuildTimeStamp.txt

Установите свойство BuildTimeStamp.txt для встроенного ресурса.

Теперь вы можете прочитать отметку времени, как это

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

Ответ 13

Для проектов .NET Core я адаптировал ответ Postlagerkarte, чтобы обновить поле Copyright для сборки с указанием даты сборки.

Прямое редактирование csproj

Следующее может быть добавлено непосредственно в первую группу PropertyGroup в csproj:

<Copyright>Copyright © $([System.DateTime]::UtcNow.Year) Travis Troyer ($([System.DateTime]::UtcNow.ToString("s")))</Copyright>

Альтернатива: свойства проекта Visual Studio

Или вставьте внутреннее выражение непосредственно в поле Copyright в разделе Package свойств проекта в Visual Studio:

Copyright © $([System.DateTime]::UtcNow.Year) Travis Troyer ($([System.DateTime]::UtcNow.ToString("s")))

Это может немного сбивать с толку, потому что Visual Studio оценит выражение и отобразит текущее значение в окне, но также соответствующим образом обновит файл проекта за кулисами.

Комплексное решение через Directory.Build.props

Вы можете добавить элемент <Copyright> выше в файл Directory.Build.props в корне вашего решения и автоматически применить его ко всем проектам в каталоге, предполагая, что каждый проект не предоставляет свое собственное значение Copyright.

<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);

Ответ 14

Мне нужно было универсальное решение, которое работало бы с проектом NETStandard на любой платформе (iOS, Android и Windows). Для этого я решил автоматически сгенерировать файл CS с помощью скрипта PowerShell. Вот скрипт 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 включен в ваш проект. Работает как чемпион на любой ОС!

Ответ 15

Я просто делаю:

File.GetCreationTime(GetType().Assembly.Location)

Ответ 16

Вы можете использовать этот проект: https://github.com/dwcullop/BuildInfo

Он использует T4 для автоматизации отметки даты сборки. Есть несколько версий (разных веток), в том числе одна, которая дает вам Git Hash ветки, которую вы в данный момент извлекли, если вы любите подобные вещи.

Раскрытие: я написал модуль.

Ответ 17

Я не уверен, но, возможно, помогает Build Incrementer.

Ответ 18

Вы можете использовать событие post-build проекта для записи текстового файла в целевой каталог с текущим временем datetime. Затем вы можете прочитать значение во время выполнения. Он немного взломан, но он должен работать.

Ответ 19

Другой подход, основанный на PCL, будет заключаться в использовании встроенной задачи MSBuild для замены времени сборки на строку, возвращаемую свойством в приложении. Мы успешно используем этот подход в приложении, которое имеет проекты Xamarin.Forms, Xamarin.Android и Xamarin.iOS.

EDIT:

Упрощен путем перемещения всей логики в файл SetBuildDate.targets и использования Regex вместо простой замены строки, чтобы файл мог быть модифицирован каждой сборкой без "reset".

Определение встроенной задачи MSBuild (сохранено в файле SetBuildDate.targets, локальном для проекта Xamarin.Forms для этого примера):

<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>

Вызов указанной выше встроенной задачи в файле csproj Xamarin.Forms в целевой BeforeBuild:

  <!-- 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 установлено в файл BuildMetadata.cs в проекте Xamarin.Forms, который содержит простой класс со строковым свойством BuildDate, в который будет добавлено время сборки:

public class BuildMetadata
{
    public static string BuildDate => "This can be any arbitrary string";
}

Добавьте этот файл BuildMetadata.cs в проект. Он будет изменен каждой сборкой, но таким образом, чтобы повторные сборки (повторные замены), поэтому вы можете включать или опускать их в исходное управление по желанию.

Ответ 20

Небольшое обновление от "Нового пути" от Джона.

Вам нужно построить путь вместо использования строки CodeBase при работе с ASP.NET/MVC

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

Ответ 21

Вы можете запустить дополнительный шаг в процессе сборки, который записывает штамп даты в файл, который затем может отображаться.

На вкладке свойств проектов просмотрите вкладку событий построения. Существует возможность выполнить команду pre или post build.

Ответ 22

Я использовал предложение Абдуррахима. Однако, это, казалось, дало странный формат времени и также добавило сокращение для дня как часть даты сборки; Пример: вс 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);

Хорошей новостью является то, что это сработало.

Ответ 23

Если это приложение Windows, вы можете просто использовать исполняемый путь приложения: новый System.IO.FileInfo(Application.ExecutablePath).LastWriteTime.ToString( "yyyy.MM.dd" )

Ответ 24

это может быть Assembly execAssembly = Assembly.GetExecutingAssembly(); var creationTime = new FileInfo(execAssembly.Location).CreationTime; // "2019-09-08T14:29:12.2286642-04:00"