Data e ora di compilazione degli assembly
La versione di un assembly è lo strumento che abbiamo per dare “un’età” alle nostre librerie. Se correttamente mantenuta ed aggiornata, ci permette di capire a quando risale una compilazione identificando subito caratteristiche ed eventuali bug presenti in quella release. E’ buona norma quindi incrementare la versione ad ogni modifica mantenendo un changelog parallelo con le descrizioni più o meno dettagliate delle modifiche apportate.
Purtroppo però a volte può capitare di dimenticarsi di incrementare la versione, magari a causa della fretta di correggere un bug in produzione o più semplicemente per distrazione. La sola versione poi spesso è difficile da ricordare a mente, soprattutto se cambia solo nella build o nella revision, e senza il changelog sotto mano potrebbe non essere di grande aiuto.
La data di compilazione invece è sempre aggiornata (per definizione) ed avere la collocazione temporale assoluta dell’assembly può aiutare la nostra mente a ricordare i dettagli della release. Versione degli assembly più la loro data di compilazione può quindi aiutarci a capire il contesto d’esecuzione nel caso ci fosse qualcosa che non va. E’ consigliabile quindi rendere disponibili tali informazioni sugli assembly che compongono la soluzione tramite la UI, tramite i log o tramite qualsiasi altro mezzo.
Esistono diversi modi per ottenere la data di compilazione. Ne vediamo tre.
1. Data e ora dal file
Sicuramente il metodo più semplice, ma il più inaffidabile. Consiste nell’associare la data di compilazione di un assembly alla data di ultima modifica del file stesso.
Una gestione a livello di stream del file (eseguibile o DLL) però potrebbe modificare tale valore rendendo il dato non attentibile. Un semplice trasferimento FTP ad esempio potrebbe modificare la data di ultima modifica di un file.
Un banale esempio di metodo per ottenere la data di compilazione di un assembly dal suo file è il seguente.
1public DateTime GetAssemblyTimeUtc(System.Reflection.Assembly assembly)
2{
3 return System.IO.File.GetLastWriteTimeUtc(assembly.Location);
4}
La tecninca funziona ovviamente con qualsiasi tipo di file.
2. Data e ora dalla versione
Questa tecnica permette di determinare il momento della compilazione dal numero di build e di revision. Ciò però non permette allo sviluppatore di gestire a piacimento tali numeri lasciandogli di fatto la possibilità di cambiare solo la major e la minor della versione.
Per ottenere la data di compilazione dalla versione è necessario impostare l’attributo AssemblyVersion del progetto mettendo l’asterisco (*) nella build ed omettendo la revision come nell’esempio seguente.
1[assembly: AssemblyVersion("1.2.*")]
Così facendo avremo la versione composta come segue.
- Major version: definito dallo sviluppatore, 1.
- Minor version: definito dallo sviluppatore, 2.
- Build: assegnato automaticamente, il numero di giorni dal 1 Gennaio 2000.
- Revision: assegnato automaticamente, il numero di secondi, diviso 2, dall’ultima mezzanotte prima della compilazione.
I valori di build e revision assegnati automaticamente sono calcolati sull’orario locale. Ne segue che la macchina che compila il codice deve avere l’orologio di sistema settato correttamente.
Un esempio quindi di metodo che restituisce la data di compilazione di un assembly con questa tecnica è il seguente.
1public DateTime GetAssemblyTimeUtc(System.Reflection.Assembly assembly)
2{
3 Version ver = assembly.GetName().Version;
4
5 DateTime baseLocalTime =
6 new DateTime(2000, 1, 1, 0, 0, 0, DateTimeKind.Local);
7 return baseLocalTime
8 .AddDays(ver.Build)
9 .AddSeconds(ver.Revision * 2)
10 .ToUniversalTime();
11}
3. Data e ora dall’header PE/COFF
Googlando nella rete mi sono imbattuto in questa soluzione che reputo in assoluto la migliore delle tre.
Da Wikipedia:
“Il formato Portable Executable (PE) è un formato di file per file eseguibili, file oggetto, librerie condivise e device drivers, usato nelle versioni a 32-bit e 64-bit del sistema operativo Microsoft Windows. […] Il formato PE è praticamente una struttura dati che incapsula le informazioni necessarie al loader di Windows per gestire il codice eseguibile. […] Il formato PE è una versione modificata del formato COFF (Common Object File Format) di Unix. Infatti molto spesso viene anche chiamato PE/COFF.“.
Tornando al nostro caso, la soluzione consiste nel leggere la data di compilazione dai campi del COFF header presente nel file della libreria ed è basata sul documento Microsoft PE and COFF Specification. L’header PE di Microsoft consiste in un MS-DOS stub, una PE signature, un COFF header e altri header opzionali.
Nella posizione 0x3C
dell’MS-DOS stub c’è scritto l’offset assoluto da cui inizia la PE signature, la quale è immediatamente seguita dal COFF header. La PE signature sono 4 byte contenenti esattamente la stringa “PE” nei primi 2, e 0 (null
) nei secondi due. Nel COFF header, tra i vari campi, c’è anche il campo TimeDateStamp
che indica il momento in cui il file è stato creato, ed esattamente contiene il numero di secondi dal 1 gennaio 1970. Anche in questo caso, la macchina che fa la compilazione deve avere l’orologio di sistema settato correttamente. Per la precisione, il COFF header ha la seguente struttura.
Offset | Size | Field |
---|---|---|
0 | 2 | Machine |
2 | 2 | NumberOfSection |
4 | 4 | TimeDateStamp |
8 | 4 | PointerToSymbolTable |
12 | 4 | NumberOfSymbols |
16 | 2 | SizeOfOptionalHeader |
18 | 2 | Characteristics |
Questo ci permette di ottenere la data di compilazione senza interferire sulla gestione del numero di versione, inclusi build e revision. Riassumendo quindi il tutto in codice, i seguenti metodi permettono la lettura della data di creazione di un eseguibile o di una DLL, sia essa gestita o non gestita.
1public DateTime GetAssemblyTimeUtc(System.Reflection.Assembly assembly)
2{
3 return GetAssemblyTimeUtc(assembly.Location);
4}
5
6public DateTime GetAssemblyTimeUtc(string filePath)
7{
8 byte[] buffer = new byte[4];
9 using (System.IO.Stream sr = new System.IO.FileStream(filePath,
10 System.IO.FileMode.Open,
11 System.IO.FileAccess.Read))
12 {
13 // get the COFF header offset
14 sr.Position = 0x3C;
15 sr.Read(buffer, 0, 4);
16 uint coffOffset = BitConverter.ToUInt32(buffer, 0);
17
18 // check the PE signature
19 sr.Position = coffOffset;
20 sr.Read(buffer, 0, 4);
21 if (Encoding.ASCII.GetString(buffer, 0, 4) != "PE\0\0")
22 {
23 throw new BadImageFormatException(
24 "The file is not a PE format image file.");
25 }
26
27 // read the TimeDateStamp field of the COFF header
28 sr.Read(buffer, 0, 2); // Machine
29 sr.Read(buffer, 0, 2); // NumberOfSections
30 sr.Read(buffer, 0, 4); // TimeDateStamp
31 uint secondsOffset = BitConverter.ToUInt32(buffer, 0);
32
33 // add the seconds offset to the base time
34 DateTime baseUtcTime =
35 new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
36 return baseUtcTime.AddSeconds(secondsOffset);
37 }
38}