J'ai actuellement une application affichant le numéro de build dans sa fenêtre de titre. C’est bien et bon, sauf que cela ne signifie rien pour la plupart des utilisateurs, qui veulent savoir s’ils disposent de la dernière version - ils ont tendance à parler de "jeudi dernier" plutôt que de la version 1.0.8.4321.
Le plan est de mettre la date de construction à la place - Par exemple, "Application construite le 21/10/2009".
J'ai du mal à trouver un moyen par programme d'extraire la date de compilation sous forme de chaîne de texte à utiliser comme ceci.
Pour le numéro de build, j'ai utilisé:
Assembly.GetExecutingAssembly().GetName().Version.ToString()
après avoir défini comment ceux-ci sont apparus.
Je voudrais quelque chose comme ça pour la date de compilation (et l'heure, pour les points de bonus).
Des pointeurs ici très appréciés (excusez le jeu de mots si approprié), ou des solutions plus ordonnées ...
Jeff Atwood a quelques choses à dire sur ce problème dans Détermination de la date de construction à la dure .
La méthode la plus fiable consiste à récupérer l'horodatage de l'éditeur de liens à partir du en-tête PE incorporé dans le fichier exécutable - du code C # (de Joe Spivey) pour cela à partir des commentaires de l'article de 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;
}
Exemple d'utilisation:
var linkTimeLocal = Assembly.GetExecutingAssembly().GetLinkerTime();
MISE À JOUR: La méthode fonctionnait pour .Net Core 1.0, mais a cessé de fonctionner après la version .Net Core 1.1 (donne des années aléatoires dans la plage 1900-2020)
Ajouter ci-dessous à la ligne de commande de l'événement de pré-construction:
echo %date% %time% > "$(ProjectDir)\Resources\BuildDate.txt"
Ajoutez ce fichier en tant que ressource. Vous avez maintenant la chaîne 'BuildDate' dans vos ressources.
Pour créer des ressources, voir Comment créer et utiliser des ressources dans .NET .
Comme souligné par @ c00000fd dans le commentaires . Microsoft est en train de changer cela. Et bien que beaucoup de gens n'utilisent pas la dernière version de leur compilateur, je soupçonne que ce changement rend cette approche incontestablement mauvaise. Et même s’il s’agit d’un exercice amusant, je recommanderais aux personnes d’intégrer simplement une date de construction dans leur binaire par tout autre moyen nécessaire s’il est important de suivre la date de construction du binaire lui-même.
Cela peut être fait avec une génération de code triviale qui est probablement déjà la première étape de votre script de construction. Cela, et le fait que les outils ALM/Build/DevOps aident beaucoup à cela et devraient être préférés à tout le reste.
Je laisse ici le reste de cette réponse à des fins historiques uniquement.
J'ai changé d'avis à ce sujet et utilise actuellement cette astuce pour obtenir la date de construction correcte.
#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
Eh bien, comment générez-vous des numéros de build? Visual Studio (ou le compilateur C #) fournit en fait des numéros de construction et de révision automatiques si vous changez l'attribut AssemblyVersion en, par exemple. 1.0.*
Ce qui va arriver, c’est que la compilation sera égale au nombre de jours depuis le 1er janvier 2000, heure locale, et que la révision sera égale au nombre de secondes écoulées depuis minuit, heure locale, divisé par 2.
voir Contenu de la communauté, numéros de construction et de révision automatiques
par exemple. 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)
Ajouter ci-dessous à la ligne de commande de l'événement de pré-construction:
echo %date% %time% > "$(ProjectDir)\Resources\BuildDate.txt"
Ajoutez ce fichier en tant que ressource. Vous avez maintenant la chaîne 'BuildDate' dans vos ressources.
Après avoir inséré le fichier dans la ressource (sous forme de fichier texte public), je l’ai accédé via
string strCompTime = Properties.Resources.BuildDate;
Pour créer des ressources, voir Comment créer et utiliser des ressources dans .NET .
Une approche qui, à mon avis, n’a encore été mentionnée est d’utiliser modèles de texte T4 pour la génération de code.
<#@ 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); } }
}
}
Avantages:
Les inconvénients:
En ce qui concerne la technique d'extraction des informations de date/version de construction à partir des octets d'un en-tête Assembly PE, Microsoft a modifié les paramètres de génération par défaut à partir de Visual Studio 15.4. La nouvelle valeur par défaut inclut la compilation déterministe, qui rend le timestamp valide et les numéros de version incrémentés automatiquement obsolètes. Le champ d'horodatage est toujours présent mais il est rempli avec une valeur permanente qui est un hachage de quelque chose, mais pas d'indication du temps de construction.
Quelques informations détaillées ici
Pour ceux qui privilégient un horodatage utile par rapport à la compilation déterministe, il existe un moyen de remplacer le nouveau paramètre par défaut. Vous pouvez inclure une balise dans le fichier .csproj de l’Assemblée qui vous intéresse comme suit:
<PropertyGroup>
...
<Deterministic>false</Deterministic>
</PropertyGroup>
Mise à jour: j'approuve la solution de modèle de texte T4 décrite dans une autre réponse ici. Je l'ai utilisé pour résoudre mon problème proprement sans perdre l'avantage de la compilation déterministe. Une précaution à ce sujet est que Visual Studio n'exécute le compilateur T4 que lorsque le fichier .tt est enregistré, et non au moment de la génération. Cela peut être gênant si vous excluez le résultat .cs du contrôle de code source (car vous vous attendez à ce qu'il soit généré) et qu'un autre développeur extrait le code. Sans réenregistrer, ils n'auront pas le fichier .cs. Il existe un paquet sur nuget (appelé AutoT4 à mon avis) qui fait de la compilation T4 une partie de chaque construction. Je n'ai pas encore confronté la solution à ce problème lors du déploiement en production, mais je m'attends à ce que quelque chose de similaire réussisse.
Je ne suis qu'un débutant en C #, alors peut-être que ma réponse semble stupide - j'affiche la date de compilation à partir de la date à laquelle le fichier exécutable a été écrit pour la dernière fois:
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());
J'ai essayé d'utiliser la méthode File.GetCreationTime mais j'ai obtenu des résultats bizarres: la date de la commande était le 2012-05-29, mais la date de l'explorateur de fenêtres indiquait le 2012-05-23. Après avoir recherché cette anomalie, j'ai constaté que le fichier avait probablement été créé le 2012-05-23 (comme le montre l'explorateur Windows), mais copié dans le dossier actuel le 2012-05-29 (comme le montre la commande File.GetCreationTime) - donc pour être sûr, j'utilise la commande File.GetLastWriteTime.
Zalek
Beaucoup de bonnes réponses ici, mais je pense pouvoir ajouter les miennes pour des raisons de simplicité, de performances (comparées aux solutions liées aux ressources), multiplates-formes (fonctionne également avec Net Core) et d'éviter tout outil tiers. Ajoutez simplement cette cible msbuild au 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>
et maintenant vous avez Builtin.CompileTime
ou new DateTime(Builtin.CompileTime, DateTimeKind.Utc)
si vous en avez besoin de cette façon.
ReSharper ne va pas aimer ça. Vous pouvez aussi l'ignorer ou ajouter une classe partielle au projet, mais cela fonctionne quand même.
Pour tous ceux qui ont besoin de connaître le temps de compilation sous 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);
}
}
}
Pour tous ceux qui ont besoin d’avoir le temps de compilation dans 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;
}
REMARQUE: Dans tous les cas, vous exécutez dans un bac à sable. Ainsi, vous ne pourrez obtenir que le temps de compilation des assemblys que vous déployez avec votre application. (c’est-à-dire que cela ne fonctionnera sur rien dans le GAC).
La méthode ci-dessus peut être modifiée pour les assemblages déjà chargés dans le processus en utilisant l'image du fichier en mémoire (au lieu de la relire à partir de la mémoire):
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;
}
}
L'option non décrite ici consiste à insérer vos propres données dans AssemblyInfo.cs. Le champ "AssemblyInformationalVersion" semble approprié. Nous avons quelques projets dans lesquels nous faisions quelque chose de similaire en tant qu'étape de construction (mais je ne suis pas entièrement satisfait du manière qui fonctionne alors ne veut pas vraiment reproduire ce que nous avons).
Il y a un article sur le sujet sur codeproject: http://www.codeproject.com/KB/dotnet/Customizing_csproj_files.aspx
Pour les projets .NET Core, j'ai adapté la réponse de Postlagerkarte afin de mettre à jour le champ Assembly Copyright avec la date de construction.
Les éléments suivants peuvent être ajoutés directement au premier PropertyGroup
du fichier csproj:
<Copyright>Copyright © $([System.DateTime]::UtcNow.Year) Travis Troyer ($([System.DateTime]::UtcNow.ToString("s")))</Copyright>
Ou collez l'expression interne directement dans le champ Copyright de la section Package des propriétés du projet dans Visual Studio:
Copyright © $([System.DateTime]::UtcNow.Year) Travis Troyer ($([System.DateTime]::UtcNow.ToString("s")))
Cela peut être un peu déroutant, car Visual Studio évaluera l'expression et affichera la valeur actuelle dans la fenêtre, mais mettra également à jour le fichier de projet de manière appropriée en arrière-plan.
Vous pouvez insérer l'élément <Copyright>
ci-dessus dans un fichier Directory.Build.props
situé dans la racine de votre solution et l'appliquer automatiquement à tous les projets du répertoire, en supposant que chaque projet ne fournit pas sa propre valeur de Copyright.
<Project>
<PropertyGroup>
<Copyright>Copyright © $([System.DateTime]::UtcNow.Year) Travis Troyer ($([System.DateTime]::UtcNow.ToString("s")))</Copyright>
</PropertyGroup>
</Project>
Directory.Build.props: personnalisez votre construction
L'exemple d'exemple vous donnera un copyright comme celui-ci:
Copyright © 2018 Travis Troyer (2018-05-30T14:46:23)
Vous pouvez afficher les informations de copyright des propriétés du fichier dans Windows ou les récupérer au moment de l'exécution:
var version = FileVersionInfo.GetVersionInfo(Assembly.GetEntryAssembly().Location);
Console.WriteLine(version.LegalCopyright);
En 2018, certaines des solutions ci-dessus ne fonctionnent plus ou ne fonctionnent pas avec .NET Core.
J'utilise l'approche suivante qui est simple et fonctionne pour mon projet .NET Core 2.0.
Ajoutez les éléments suivants à votre fichier .csproj dans le PropertyGroup:
<Today>$([System.DateTime]::Now)</Today>
Ceci définit un PropertyFunction auquel vous pouvez accéder dans votre commande pre build.
Votre pré-construction ressemble à ceci
echo $(today) > $(ProjectDir)BuildTimeStamp.txt
Définissez la propriété du fichier BuildTimeStamp.txt sur la ressource incorporée.
Maintenant, vous pouvez lire l'horodatage comme ceci
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();
}
}
}
Je fais juste:
File.GetCreationTime(GetType().Assembly.Location)
J'avais besoin d'une solution universelle fonctionnant avec un projet NETStandard sur n'importe quelle plate-forme (iOS, Android et Windows). Pour ce faire, j'ai décidé de générer automatiquement un fichier CS via un script PowerShell. Voici le script 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
Enregistrez le fichier PowerScript sous GenBuildDate.ps1 et ajoutez-le à votre projet. Enfin, ajoutez la ligne suivante à votre événement Pre-Build:
powershell -File $(ProjectDir)GenBuildDate.ps1 -outputFile $(ProjectDir)BuildDate.cs
Assurez-vous que BuildDate.cs est inclus dans votre projet. Fonctionne comme un champion sur n'importe quel OS!
Vous pouvez utiliser ce projet: https://github.com/dwcullop/BuildInfo
Il utilise T4 pour automatiser l’horodatage de la date de construction. Il existe plusieurs versions (différentes branches), dont une qui vous donne le hachage Git de la branche actuellement extraite, si vous aimez ce genre de chose.
Divulgation: j'ai écrit le module.
Une petite mise à jour sur la réponse "New Way" de Jhon.
Vous devez créer le chemin au lieu d'utiliser la chaîne CodeBase lorsque vous utilisez ASP.NET/MVC.
var codeBase = Assembly.GetName().CodeBase;
UriBuilder uri = new UriBuilder(codeBase);
string path = Uri.UnescapeDataString(uri.Path);
Une approche différente, conviviale pour PCL, consisterait à utiliser une tâche en ligne MSBuild pour substituer l'heure de génération dans une chaîne renvoyée par une propriété de l'application. Nous utilisons cette approche avec succès dans une application comportant des projets Xamarin.Forms, Xamarin.Android et Xamarin.iOS.
EDIT:
Simplifié en déplaçant toute la logique dans le fichier SetBuildDate.targets
et en utilisant Regex
à la place de la simple chaîne, remplacez le fichier afin que le fichier puisse être modifié par chaque construction sans "réinitialisation".
La définition de tâche intégrée MSBuild (enregistrée dans un fichier SetBuildDate.targets local du projet Xamarin.Forms pour cet exemple):
<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>
Appel de la tâche en ligne ci-dessus dans le fichier csproj Xamarin.Forms dans la cible 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>
La propriété FilePath
est définie sur un fichier BuildMetadata.cs
dans le projet Xamarin.Forms qui contient une classe simple avec une propriété de chaîne BuildDate
, dans laquelle sera substituée l'heure de construction:
public class BuildMetadata
{
public static string BuildDate => "This can be any arbitrary string";
}
Ajoutez ce fichier BuildMetadata.cs
au projet. Il sera modifié à chaque génération, mais d'une manière qui permette des générations répétées (remplacements répétés), afin que vous puissiez l'inclure ou l'omettre dans le contrôle de source à votre guise.
Je ne suis pas sûr, mais peut-être que Build Incrementer aide.
Vous pouvez utiliser un événement post-build de projet pour écrire un fichier texte dans votre répertoire cible avec la date/heure actuelle. Vous pouvez alors lire la valeur au moment de l'exécution. C'est un petit hacky, mais ça devrait marcher.
J'ai utilisé la suggestion d'Abdurrahim. Cependant, il semblait donner un format de temps étrange et également ajouté l'abréviation pour le jour dans le cadre de la date de construction; exemple: dim 24/12/2017 13: 21: 05.43. Je n'avais besoin que de la date alors j'ai dû éliminer le reste en utilisant une sous-chaîne.
Après avoir ajouté le echo %date% %time% > "$(ProjectDir)\Resources\BuildDate.txt"
à l'événement de pré-construction, je viens de faire ce qui suit:
string strBuildDate = YourNamespace.Properties.Resources.BuildDate;
string strTrimBuildDate = strBuildDate.Substring(4).Remove(10);
La bonne nouvelle est que cela a fonctionné.
Vous pouvez lancer une étape supplémentaire dans le processus de construction qui écrit un horodatage dans un fichier qui peut ensuite être affiché.
Dans l'onglet Propriétés du projet, consultez l'onglet Evénements de construction. Il existe une option pour exécuter une commande avant ou après la construction.
S'il s'agit d'une application Windows, vous pouvez simplement utiliser le chemin de l'exécutable de l'application: new System.IO.FileInfo (Application.ExecutablePath) .LastWriteTime.ToString ("yyyy.MM.dd")