diff --git a/AssemblyToProcess/AssemblyInfo.cs b/AssemblyToProcess/AssemblyInfo.cs index cecf80a..a7ff42b 100644 --- a/AssemblyToProcess/AssemblyInfo.cs +++ b/AssemblyToProcess/AssemblyInfo.cs @@ -3,4 +3,5 @@ [assembly: AssemblyTitle("AssemblyToProcess")] [assembly: AssemblyProduct("AssemblyToProcess")] [assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("4.5.6.7")] //[assembly: AssemblyInformationalVersionAttribute("1.0.0.0/aString")] diff --git a/AssemblyToProcessExistingAttribute/AssemblyInfo.cs b/AssemblyToProcessExistingAttribute/AssemblyInfo.cs index c13160f..9f1a935 100644 --- a/AssemblyToProcessExistingAttribute/AssemblyInfo.cs +++ b/AssemblyToProcessExistingAttribute/AssemblyInfo.cs @@ -3,4 +3,5 @@ [assembly: AssemblyTitle("AssemblyToProcessExistingAttribute")] [assembly: AssemblyProduct("AssemblyToProcessExistingAttribute")] [assembly: AssemblyVersion("1.0.0")] +[assembly: AssemblyFileVersion("4.5.6.7")] [assembly: AssemblyInformationalVersionAttribute("%version3%+%branch%.%githash% %haschanges% %utcnow% %now:yyMMdd%")] diff --git a/Fody/Configuration.cs b/Fody/Configuration.cs index 0903a70..ad9d016 100644 --- a/Fody/Configuration.cs +++ b/Fody/Configuration.cs @@ -4,6 +4,8 @@ public class Configuration { public bool UseProject; + public bool UseFileVersion; + public bool OverwriteFileVersion = true; public string ChangeString = "HasChanges"; public Configuration(XElement config) @@ -14,22 +16,50 @@ public Configuration(XElement config) } var attr = config.Attribute("UseProjectGit"); - if (attr != null) + if (HasValue(attr)) { - try - { - UseProject = Convert.ToBoolean(attr.Value); - } - catch (Exception) - { - throw new WeavingException($"Unable to parse '{attr.Value}' as a boolean, please use true or false."); - } + UseProject = ConvertAndThrowIfNotBoolean(attr.Value); + } + + attr = config.Attribute("UseFileVersion"); + if (HasValue(attr)) + { + UseFileVersion = ConvertAndThrowIfNotBoolean(attr.Value); } attr = config.Attribute("ChangeString"); - if (!string.IsNullOrWhiteSpace(attr?.Value)) + if (HasValue(attr)) { ChangeString = attr.Value; } + + if (UseFileVersion) + OverwriteFileVersion = false; + else + { + attr = config.Attribute("OverwriteFileVersion"); + if (HasValue(attr)) + { + OverwriteFileVersion = ConvertAndThrowIfNotBoolean(attr.Value); + } + } + } + + private static bool HasValue(XAttribute attr) + { + return !string.IsNullOrWhiteSpace(attr?.Value); + } + + private static bool ConvertAndThrowIfNotBoolean(string value) + { + try + { + var result = Convert.ToBoolean(value); + return result; + } + catch + { + throw new WeavingException($"Unable to parse '{value}' as a boolean; please use 'true' or 'false'."); + } } } diff --git a/Fody/FormatStringTokenResolver.cs b/Fody/FormatStringTokenResolver.cs index 32e0cca..364c99f 100644 --- a/Fody/FormatStringTokenResolver.cs +++ b/Fody/FormatStringTokenResolver.cs @@ -12,16 +12,21 @@ public class FormatStringTokenResolver private static DateTime now = DateTime.Now; private static DateTime utcNow = DateTime.UtcNow; + [Obsolete("Use ReplaceTokens with Version. Will be removed in 2.0")] public string ReplaceTokens(string template, ModuleDefinition moduleDefinition, Repository repo, string changestring) { - var assemblyVersion = moduleDefinition.Assembly.Name.Version; + return ReplaceTokens(template, moduleDefinition.Assembly.Name.Version, repo, changestring); + } + + public string ReplaceTokens(string template, System.Version version, Repository repo, string changestring) + { var branch = repo.Head; - template = template.Replace("%version%", assemblyVersion.ToString()); - template = template.Replace("%version1%", assemblyVersion.ToString(1)); - template = template.Replace("%version2%", assemblyVersion.ToString(2)); - template = template.Replace("%version3%", assemblyVersion.ToString(3)); - template = template.Replace("%version4%", assemblyVersion.ToString(4)); + template = template.Replace("%version%", version.ToString()); + template = template.Replace("%version1%", version.ToString(1)); + template = template.Replace("%version2%", version.ToString(2)); + template = template.Replace("%version3%", version.ToString(3)); + template = template.Replace("%version4%", version.ToString(4)); template = template.Replace("%now%", now.ToShortDateString()); template = template.Replace("%utcnow%", utcNow.ToShortDateString()); diff --git a/Fody/ModuleWeaver.cs b/Fody/ModuleWeaver.cs index f5a6d22..6710413 100644 --- a/Fody/ModuleWeaver.cs +++ b/Fody/ModuleWeaver.cs @@ -7,9 +7,15 @@ using Version = System.Version; using Fody.PeImage; using Fody.VersionResources; +using Mono.Collections.Generic; +using System.Collections.Generic; public class ModuleWeaver { + #region public props + // ReSharper disable MemberCanBePrivate.Global + // ReSharper disable AutoPropertyCanBeMadeGetOnly.Global + // ReSharper disable UnusedAutoPropertyAccessor.Global public XElement Config { get; set; } public Action LogInfo { get; set; } public Action LogWarning { get; set; } @@ -18,12 +24,22 @@ public class ModuleWeaver public string ProjectDirectoryPath { get; set; } public string AddinDirectoryPath { get; set; } public string AssemblyFilePath { get; set; } + // ReSharper restore MemberCanBePrivate.Global + // ReSharper restore AutoPropertyCanBeMadeGetOnly.Global + // ReSharper restore UnusedAutoPropertyAccessor.Global + #endregion + private static bool isPathSet; private readonly FormatStringTokenResolver formatStringTokenResolver; private string assemblyInfoVersion; - private Version assemblyVersion; + private Version versionToUse; private bool dotGitDirExists; + private Configuration _config; + + private const string InfoAttributeName = nameof(System.Reflection.AssemblyInformationalVersionAttribute); + private const string FileAttributeName = nameof(System.Reflection.AssemblyFileVersionAttribute); + public ModuleWeaver() { LogInfo = s => { }; @@ -35,14 +51,14 @@ public void Execute() { SetSearchPath(); - var config = new Configuration(Config); + _config = new Configuration(Config); - LogInfo("Starting search for git repository in " + (config.UseProject ? "ProjectDir" : "SolutionDir")); + LogInfo("Starting search for git repository in " + (_config.UseProject ? "ProjectDir" : "SolutionDir")); var customAttributes = ModuleDefinition.Assembly.CustomAttributes; - var gitDir = Repository.Discover(config.UseProject ? ProjectDirectoryPath : SolutionDirectoryPath); + var gitDir = Repository.Discover(_config.UseProject ? ProjectDirectoryPath : SolutionDirectoryPath); if (gitDir == null) { LogWarning("No .git directory found."); @@ -62,13 +78,16 @@ public void Execute() return; } - assemblyVersion = ModuleDefinition.Assembly.Name.Version; + if (!_config.UseFileVersion) + versionToUse = ModuleDefinition.Assembly.Name.Version; + else + versionToUse = GetAssemblyFileVersion(customAttributes); - var customAttribute = customAttributes.FirstOrDefault(x => x.AttributeType.Name == "AssemblyInformationalVersionAttribute"); + var customAttribute = GetCustomAttribute(customAttributes, InfoAttributeName); if (customAttribute != null) { assemblyInfoVersion = (string)customAttribute.ConstructorArguments[0].Value; - assemblyInfoVersion = formatStringTokenResolver.ReplaceTokens(assemblyInfoVersion, ModuleDefinition, repo, config.ChangeString); + assemblyInfoVersion = formatStringTokenResolver.ReplaceTokens(assemblyInfoVersion, versionToUse, repo, _config.ChangeString); VerifyStartsWithVersion(assemblyInfoVersion); customAttribute.ConstructorArguments[0] = new CustomAttributeArgument(ModuleDefinition.TypeSystem.String, assemblyInfoVersion); } @@ -78,7 +97,7 @@ public void Execute() var constructor = ModuleDefinition.ImportReference(versionAttribute.Methods.First(x => x.IsConstructor)); customAttribute = new CustomAttribute(constructor); - assemblyInfoVersion = $"{assemblyVersion} Head:'{repo.Head.FriendlyName}' Sha:{branch.Tip.Sha}{(repo.IsClean() ? "" : " " + config.ChangeString)}"; + assemblyInfoVersion = $"{versionToUse} Head:'{repo.Head.FriendlyName}' Sha:{branch.Tip.Sha}{(repo.IsClean() ? "" : " " + _config.ChangeString)}"; customAttribute.ConstructorArguments.Add(new CustomAttributeArgument(ModuleDefinition.TypeSystem.String, assemblyInfoVersion)); customAttributes.Add(customAttribute); @@ -86,6 +105,24 @@ public void Execute() } } + private static CustomAttribute GetCustomAttribute(Collection attributes, string attributeName) + { + return attributes.FirstOrDefault(x => x.AttributeType.Name == attributeName); + } + + private Version GetAssemblyFileVersion(Collection customAttributes) + { + var afvAttribute = GetCustomAttribute(customAttributes, FileAttributeName); + if (afvAttribute == null) + { + throw new WeavingException("AssemblyFileVersion attribute could not be found."); + } + + var assemblyFileVersionString = (string) afvAttribute.ConstructorArguments[0].Value; + VerifyStartsWithVersion(assemblyFileVersionString); + return Version.Parse(assemblyFileVersionString); + } + private void VerifyStartsWithVersion(string versionString) { var prefix = new string(versionString.TakeWhile(x => char.IsDigit(x) || x == '.').ToArray()); @@ -132,13 +169,13 @@ private static string GetProcessorArchitecture() private TypeDefinition GetVersionAttribute() { var msCoreLib = ModuleDefinition.AssemblyResolver.Resolve(new AssemblyNameReference("mscorlib", null)); - var msCoreAttribute = msCoreLib.MainModule.Types.FirstOrDefault(x => x.Name == "AssemblyInformationalVersionAttribute"); + var msCoreAttribute = msCoreLib.MainModule.Types.FirstOrDefault(x => x.Name == InfoAttributeName); if (msCoreAttribute != null) { return msCoreAttribute; } var systemRuntime = ModuleDefinition.AssemblyResolver.Resolve(new AssemblyNameReference("System.Runtime", null)); - return systemRuntime.MainModule.Types.First(x => x.Name == "AssemblyInformationalVersionAttribute"); + return systemRuntime.MainModule.Types.First(x => x.Name == InfoAttributeName); } public void AfterWeaving() @@ -161,22 +198,22 @@ public void AfterWeaving() var reader = new VersionResourceReader(versionStream); var versions = reader.Read(); + if (versions.FixedFileInfo == null) + { + throw new WeavingException("versions.FixedFileInfo == null"); + } + var fixedFileInfo = versions.FixedFileInfo.Value; - fixedFileInfo.FileVersion = assemblyVersion; - fixedFileInfo.ProductVersion = assemblyVersion; + if (_config.OverwriteFileVersion) + fixedFileInfo.FileVersion = versionToUse; + fixedFileInfo.ProductVersion = versionToUse; versions.FixedFileInfo = fixedFileInfo; foreach (var stringTable in versions.StringFileInfo) { - if (stringTable.Values.ContainsKey("FileVersion")) - { - stringTable.Values["FileVersion"] = assemblyVersion.ToString(); - } - - if (stringTable.Values.ContainsKey("ProductVersion")) - { - stringTable.Values["ProductVersion"] = assemblyInfoVersion; - } + if (_config.OverwriteFileVersion) + SetTableValue(stringTable.Values, "FileVersion", versionToUse.ToString()); + SetTableValue(stringTable.Values, "ProductVersion", assemblyInfoVersion); } versionStream.Position = 0; @@ -189,7 +226,13 @@ public void AfterWeaving() } catch (Exception ex) { - throw new WeavingException($"Failed to update the assembly information. {ex.Message}"); + throw new WeavingException("Failed to update the assembly information. {ex.Message}", ex); } } + + private static void SetTableValue(IDictionary dict, string key, string value) + { + if (dict.ContainsKey(key)) + dict[key] = value; + } } \ No newline at end of file diff --git a/Fody/WeavingException.cs b/Fody/WeavingException.cs index a15a1aa..50f7f5e 100644 --- a/Fody/WeavingException.cs +++ b/Fody/WeavingException.cs @@ -7,4 +7,9 @@ public WeavingException(string message) { } + + /// + public WeavingException(string message, Exception innerException) : base(message, innerException) + { + } } \ No newline at end of file diff --git a/README.md b/README.md index d86e187..8bf472f 100644 --- a/README.md +++ b/README.md @@ -34,7 +34,7 @@ Extracts the git information from disk, combines it with the assembly version, a So if your assembly version is `1.0.0.0`, the working branch is `master` and the last commit is `759e9ddb53271dfa9335a3b27e452749a9b22280` then the following attribute will be added to the assembly. ```c# -[assembly: AssemblyInformationalVersion("1.0.0.0 Head:'master' Sha:759e9ddb53271dfa9335a3b27e452749a9b22280")] + [assembly: AssemblyInformationalVersion("1.0.0.0 Head:'master' Sha:759e9ddb53271dfa9335a3b27e452749a9b22280")] ``` @@ -52,6 +52,8 @@ The tokens are: - `%version4%` is replaced with the major, minor, revision, and build version (`1.0.0.0`) - `%now%` is replaced with the current short date - `%utcnow%` is replaced with the current utc short date +- `%now%` is replaced with the current short date +- `%utcnow%` is replaced with the current utc short date - `%githash%` is replaced with the SHA1 hash of the branch tip of the repository - `%shorthash%` is replaced with the first eight characters of `%githash%` - `%branch%` is replaced with the branch name of the repository @@ -78,7 +80,6 @@ Define the string used to indicate that the code was built from a non clean repo ``` - ### UseProjectGit Define if you want to start Stamp to start searching for the Git repository in the ProjectDir (`true`) or the SolutionDir (`false`). @@ -89,6 +90,24 @@ Define if you want to start Stamp to start searching for the Git repository in t ``` +### OverwriteFileVersion + +By default, Stamp will overwrite the `AssemblyFileVersion` with the `AssemblyVersion`. Setting this to `false` will preserve the existing `AssemblyFileVersion`. + +*Default is `true`* + +```xml + +``` + +### UseFileVersion + +By default, Stamp uses the value from `AssemblyVersion` to construct the `AssemblyInformationalVersion`. Set this to `true` to use the `AssemblyFileVersion` instead. **Note:** If this is set to `true`, `OverwriteFileVersion` will be `false` and will ignore any value explicitly set. + +*Default is `false`* +```xml + +``` ## Icon diff --git a/Tests/TaskTests.cs b/Tests/TaskTests.cs index 705db93..99a2e4e 100644 --- a/Tests/TaskTests.cs +++ b/Tests/TaskTests.cs @@ -4,6 +4,8 @@ using System.Reflection; using Mono.Cecil; using NUnit.Framework; +using System.Xml.Linq; +using System; [TestFixture] public class TaskTests @@ -12,19 +14,25 @@ public class TaskTests // ReSharper disable once PrivateFieldCanBeConvertedToLocalVariable private string beforeAssemblyPath; private string afterAssemblyPath; + protected XElement config; - public TaskTests() + [OneTimeSetUp] + public void Setup() { beforeAssemblyPath = Path.GetFullPath(Path.Combine(TestContext.CurrentContext.TestDirectory, @"..\..\..\AssemblyToProcess\bin\Debug\AssemblyToProcess.dll")); #if (!DEBUG) beforeAssemblyPath = beforeAssemblyPath.Replace("Debug", "Release"); #endif - afterAssemblyPath = beforeAssemblyPath.Replace(".dll", "2.dll"); + afterAssemblyPath = beforeAssemblyPath.Replace(".dll", $"{Guid.NewGuid().ToString()}.dll"); File.Copy(beforeAssemblyPath, afterAssemblyPath, true); using (var moduleDefinition = ModuleDefinition.ReadModule(beforeAssemblyPath)) { + + var versionInfo = FileVersionInfo.GetVersionInfo(afterAssemblyPath); + Trace.WriteLine($"Before: AssemblyVersion={moduleDefinition.Assembly.Name.Version}, FileVersion={versionInfo.FileVersion}, Config={config}"); + var currentDirectory = AssemblyLocation.CurrentDirectory(); var weavingTask = new ModuleWeaver @@ -33,6 +41,7 @@ public TaskTests() AddinDirectoryPath = currentDirectory, SolutionDirectoryPath = currentDirectory, AssemblyFilePath = afterAssemblyPath, + Config = config }; weavingTask.Execute(); @@ -44,16 +53,15 @@ public TaskTests() assembly = Assembly.LoadFile(afterAssemblyPath); } - [Test] public void EnsureAttributeExists() { var customAttributes = (AssemblyInformationalVersionAttribute)assembly - .GetCustomAttributes(typeof (AssemblyInformationalVersionAttribute), false) + .GetCustomAttributes(typeof(AssemblyInformationalVersionAttribute), false) .First(); Assert.IsNotNull(customAttributes.InformationalVersion); Assert.IsNotEmpty(customAttributes.InformationalVersion); - Trace.WriteLine(customAttributes.InformationalVersion); + Trace.WriteLine($"InfoVersion: {customAttributes.InformationalVersion}"); } [Test] @@ -64,8 +72,8 @@ public void Win32Resource() Assert.IsNotEmpty(versionInfo.ProductVersion); Assert.IsNotNull(versionInfo.FileVersion); Assert.IsNotEmpty(versionInfo.FileVersion); - Trace.WriteLine(versionInfo.ProductVersion); - Trace.WriteLine(versionInfo.FileVersion); + Trace.WriteLine($"ProductVersion: {versionInfo.ProductVersion}"); + Trace.WriteLine($"FileVersion: {versionInfo.FileVersion}"); } @@ -77,4 +85,22 @@ public void PeVerify() } #endif +} + +[TestFixture] +class UseFileVersionTests : TaskTests +{ + public UseFileVersionTests() + { + config = XElement.Parse(""); + } +} + +[TestFixture] +class OverwriteFileVersionTests : TaskTests +{ + public OverwriteFileVersionTests() + { + config = XElement.Parse(""); + } } \ No newline at end of file diff --git a/Tests/Tests.csproj b/Tests/Tests.csproj index a1d692b..69940b4 100644 --- a/Tests/Tests.csproj +++ b/Tests/Tests.csproj @@ -96,6 +96,9 @@ Fody + + + diff --git a/Tests/TokenResolverTests.cs b/Tests/TokenResolverTests.cs index 4fd3ca8..a0576f6 100644 --- a/Tests/TokenResolverTests.cs +++ b/Tests/TokenResolverTests.cs @@ -5,12 +5,13 @@ using LibGit2Sharp; using Mono.Cecil; using NUnit.Framework; +using Version = System.Version; [TestFixture] public class TokenResolverTests { - private ModuleDefinition moduleDefinition; private FormatStringTokenResolver resolver; + private Version version; public TokenResolverTests() @@ -19,7 +20,8 @@ public TokenResolverTests() #if (!DEBUG) beforeAssemblyPath = beforeAssemblyPath.Replace("Debug", "Release"); #endif - moduleDefinition = ModuleDefinition.ReadModule(beforeAssemblyPath); + var moduleDefinition = ModuleDefinition.ReadModule(beforeAssemblyPath); + version = moduleDefinition.Assembly.Name.Version; resolver = new FormatStringTokenResolver(); } @@ -37,7 +39,7 @@ public void Replace_version() { DoWithCurrentRepo(repo => { - var result = resolver.ReplaceTokens("%version%", moduleDefinition, repo, ""); + var result = resolver.ReplaceTokens("%version%", version, repo, ""); Assert.AreEqual("1.0.0.0", result); }); @@ -48,7 +50,7 @@ public void Replace_version1() { DoWithCurrentRepo(repo => { - var result = resolver.ReplaceTokens("%version1%", moduleDefinition, repo, ""); + var result = resolver.ReplaceTokens("%version1%", version, repo, ""); Assert.AreEqual("1", result); }); @@ -59,7 +61,7 @@ public void Replace_version2() { DoWithCurrentRepo(repo => { - var result = resolver.ReplaceTokens("%version2%", moduleDefinition, repo, ""); + var result = resolver.ReplaceTokens("%version2%", version, repo, ""); Assert.AreEqual("1.0", result); }); @@ -70,7 +72,7 @@ public void Replace_version3() { DoWithCurrentRepo(repo => { - var result = resolver.ReplaceTokens("%version3%", moduleDefinition, repo, ""); + var result = resolver.ReplaceTokens("%version3%", version, repo, ""); Assert.AreEqual("1.0.0", result); }); @@ -81,7 +83,7 @@ public void Replace_version4() { DoWithCurrentRepo(repo => { - var result = resolver.ReplaceTokens("%version4%", moduleDefinition, repo, ""); + var result = resolver.ReplaceTokens("%version4%", version, repo, ""); Assert.AreEqual("1.0.0.0", result); }); @@ -94,7 +96,7 @@ public void Replace_branch() { var branchName = repo.Head.FriendlyName; - var result = resolver.ReplaceTokens("%branch%", moduleDefinition, repo, ""); + var result = resolver.ReplaceTokens("%branch%", version, repo, ""); Assert.AreEqual(branchName, result); }); @@ -107,7 +109,7 @@ public void Replace_githash() { var sha = repo.Head.Tip.Sha; - var result = resolver.ReplaceTokens("%githash%", moduleDefinition, repo, ""); + var result = resolver.ReplaceTokens("%githash%", version, repo, ""); Assert.AreEqual(sha, result); }); @@ -118,7 +120,7 @@ public void Replace_haschanges() { DoWithCurrentRepo(repo => { - var result = resolver.ReplaceTokens("%haschanges%", moduleDefinition, repo, "HasChanges"); + var result = resolver.ReplaceTokens("%haschanges%", version, repo, "HasChanges"); if (repo.IsClean()) { @@ -138,7 +140,7 @@ public void Replace_user() { var currentUser = Environment.UserName; - var result = resolver.ReplaceTokens("%user%", moduleDefinition, repo, ""); + var result = resolver.ReplaceTokens("%user%", version, repo, ""); Assert.IsTrue(result.EndsWith(currentUser)); }); @@ -151,7 +153,7 @@ public void Replace_machine() { var machineName = Environment.MachineName; - var result = resolver.ReplaceTokens("%machine%", moduleDefinition, repo, ""); + var result = resolver.ReplaceTokens("%machine%", version, repo, ""); Assert.AreEqual(machineName, result); }); @@ -161,7 +163,7 @@ public void Replace_tags() { DoWithCurrentRepo(repo => { - var result = resolver.ReplaceTokens("%lasttag%", moduleDefinition, repo, ""); + var result = resolver.ReplaceTokens("%lasttag%", version, repo, ""); // tags in this repose should have the format %.%.% var match = Regex.IsMatch(result, @"^\d+\.\d+\.\d+$"); @@ -177,8 +179,8 @@ public void Replace_time() var now = DateTime.Now; var utcNow = DateTime.UtcNow; - Assert.AreEqual(now.ToString("yyMMdd"), resolver.ReplaceTokens("%now:yyMMdd%", moduleDefinition, repo, "")); - Assert.AreEqual(utcNow.ToShortDateString(), resolver.ReplaceTokens("%utcnow%", moduleDefinition, repo, "")); + Assert.AreEqual(now.ToString("yyMMdd"), resolver.ReplaceTokens("%now:yyMMdd%", version, repo, "")); + Assert.AreEqual(utcNow.ToShortDateString(), resolver.ReplaceTokens("%utcnow%", version, repo, "")); }); } @@ -193,7 +195,7 @@ public void Replace_environment_variables() var replacementTokens = string.Join("--", environmentVariables.Keys.Cast() .Select(key => "%env[" + key + "]%") .ToArray()); - var result = resolver.ReplaceTokens(replacementTokens, moduleDefinition, repo, ""); + var result = resolver.ReplaceTokens(replacementTokens, version, repo, ""); Assert.AreEqual(expected, result); });