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 f5c693f..b2a8c66 100644 --- a/Fody/Configuration.cs +++ b/Fody/Configuration.cs @@ -3,7 +3,9 @@ public class Configuration { - public bool UseProject; + public bool UseProject = false; + public bool UseFileVersion = false; + 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) + UseProject = ConvertAndThrowIfNotBoolean(attr.Value); + } + + attr = config.Attribute("UseFileVersion"); + if (HasValue(attr)) + { + UseFileVersion = ConvertAndThrowIfNotBoolean(attr.Value); + } + + attr = config.Attribute("ChangeString"); + if (HasValue(attr)) + { + ChangeString = attr.Value; + } + + if (UseFileVersion) + OverwriteFileVersion = false; + else + { + attr = config.Attribute("OverwriteFileVersion"); + if (HasValue(attr)) { - throw new WeavingException($"Unable to parse '{attr.Value}' as a boolean, please use true or false."); + OverwriteFileVersion = ConvertAndThrowIfNotBoolean(attr.Value); } } + } - attr = config.Attribute("ChangeString"); - if (!string.IsNullOrWhiteSpace(attr?.Value)) + private bool HasValue(XAttribute attr) + { + return !String.IsNullOrWhiteSpace(attr?.Value); + } + + private bool ConvertAndThrowIfNotBoolean(string value) + { + try + { + var result = Convert.ToBoolean(value); + return result; + } + catch { - ChangeString = config.Attribute("ChangeString").Value; + 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 bb1fa15..047d436 100644 --- a/Fody/FormatStringTokenResolver.cs +++ b/Fody/FormatStringTokenResolver.cs @@ -12,16 +12,15 @@ public class FormatStringTokenResolver static DateTime now = DateTime.Now; static DateTime utcNow = DateTime.UtcNow; - public string ReplaceTokens(string template, ModuleDefinition moduleDefinition, Repository repo, string changestring) + public string ReplaceTokens(string template, System.Version version, Repository repo, string changestring) { - var assemblyVersion = moduleDefinition.Assembly.Name.Version; 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 92b43b2..c5b3211 100644 --- a/Fody/ModuleWeaver.cs +++ b/Fody/ModuleWeaver.cs @@ -7,6 +7,8 @@ using Version = System.Version; using Fody.PeImage; using Fody.VersionResources; +using Mono.Collections.Generic; +using System.Collections.Generic; public class ModuleWeaver { @@ -21,9 +23,14 @@ public class ModuleWeaver static bool isPathSet; readonly FormatStringTokenResolver formatStringTokenResolver; string assemblyInfoVersion; - Version assemblyVersion; + Version versionToUse; bool dotGitDirExists; + Configuration _config; + + const string INFO_ATTRIBUTE = "AssemblyInformationalVersionAttribute"; + const string FILE_ATTRIBUTE = "AssemblyFileVersionAttribute"; + public ModuleWeaver() { LogInfo = s => { }; @@ -35,14 +42,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 +69,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, INFO_ATTRIBUTE); 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 +88,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 +96,26 @@ public void Execute() } } + private CustomAttribute GetCustomAttribute(Collection attributes, string attributeName) + { + return attributes.FirstOrDefault(x => x.AttributeType.Name == attributeName); + } + + private Version GetAssemblyFileVersion(Collection customAttributes) + { + var afvAttribute = GetCustomAttribute(customAttributes, FILE_ATTRIBUTE); + if (afvAttribute != null) + { + var assemblyFileVersionString = (string)afvAttribute.ConstructorArguments[0].Value; + VerifyStartsWithVersion(assemblyFileVersionString); + return Version.Parse(assemblyFileVersionString); + } + else + { + throw new WeavingException("AssemblyFileVersion attribute could not be found."); + } + } + void VerifyStartsWithVersion(string versionString) { var prefix = new string(versionString.TakeWhile(x => char.IsDigit(x) || x == '.').ToArray()); @@ -137,13 +167,13 @@ static string GetProcessorArchitecture() TypeDefinition GetVersionAttribute() { var msCoreLib = ModuleDefinition.AssemblyResolver.Resolve("mscorlib"); - var msCoreAttribute = msCoreLib.MainModule.Types.FirstOrDefault(x => x.Name == "AssemblyInformationalVersionAttribute"); + var msCoreAttribute = msCoreLib.MainModule.Types.FirstOrDefault(x => x.Name == INFO_ATTRIBUTE); if (msCoreAttribute != null) { return msCoreAttribute; } var systemRuntime = ModuleDefinition.AssemblyResolver.Resolve("System.Runtime"); - return systemRuntime.MainModule.Types.First(x => x.Name == "AssemblyInformationalVersionAttribute"); + return systemRuntime.MainModule.Types.First(x => x.Name == INFO_ATTRIBUTE); } int? GetVerPatchWaitTimeout() @@ -187,21 +217,16 @@ public void AfterWeaving() var versions = reader.Read(); 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; @@ -217,4 +242,10 @@ public void AfterWeaving() throw new WeavingException($"Failed to update the assembly information. {ex.Message}"); } } + + private void SetTableValue(Dictionary dict, string key, string value) + { + if (dict.ContainsKey(key)) + dict[key] = value; + } } \ No newline at end of file diff --git a/README.md b/README.md index f9fdfe8..67fa736 100644 --- a/README.md +++ b/README.md @@ -31,16 +31,20 @@ The tokens are: - `%version2%` is replaced with the major and minor version (1.0) - `%version3%` is replaced with the major, minor, and revision version (1.0.0) - `%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 - `%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 - `%haschanges%` is replaced with the string defined in the ChangeString attribute in the configuration, see below. +- `%user%` is replaced with the current user name +- `%machinename%` is replaced with the current machine name > NOTE: if you already have an AssemblyInformationalVersion attribute and it doesn't use replacement tokens, it will not be modified at all. ## Configuration -All config options are attributes of Stamp in FodyWeavers.xml +All config options are attributes of the `Stamp` node in `FodyWeavers.xml` ### ChangeString @@ -48,15 +52,31 @@ Define the string used to indicate that the code was built from a non clean repo *Default is `HasChanges`* - + ### UseProjectGit -Define if you want to start Stamp to start searching for the Git repository in the ProjectDir (true) or the SolutionDir (false). +Define if you want to start Stamp to start searching for the Git repository in the ProjectDir (`true`) or the SolutionDir (`false`). *Default is `false`* - + + +### OverwriteFileVersion + +By default, Stamp will overwrite the `AssemblyFileVersion` with the `AssemblyVersion`. Setting this to `false` will preserve the existing `AssemblyFileVersion`. + +*Default is `true`* + + + +### 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`* + + ## Icon diff --git a/Tests/TaskTests.cs b/Tests/TaskTests.cs index 3bf8bb3..bb24545 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,26 +14,34 @@ public class TaskTests // ReSharper disable once PrivateFieldCanBeConvertedToLocalVariable string beforeAssemblyPath; string afterAssemblyPath; + protected XElement config = null; - 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); var moduleDefinition = ModuleDefinition.ReadModule(afterAssemblyPath); + + var versionInfo = FileVersionInfo.GetVersionInfo(afterAssemblyPath); + Trace.WriteLine(String.Format("Before: AssemblyVersion={0}, FileVersion={1}, Config={2}", + moduleDefinition.Assembly.Name.Version, versionInfo.FileVersion, config)); + var currentDirectory = AssemblyLocation.CurrentDirectory(); var weavingTask = new ModuleWeaver - { - ModuleDefinition = moduleDefinition, - AddinDirectoryPath = currentDirectory, - SolutionDirectoryPath = currentDirectory, - AssemblyFilePath = afterAssemblyPath, - }; + { + ModuleDefinition = moduleDefinition, + AddinDirectoryPath = currentDirectory, + SolutionDirectoryPath = currentDirectory, + AssemblyFilePath = afterAssemblyPath, + Config = config + }; weavingTask.Execute(); moduleDefinition.Write(afterAssemblyPath); @@ -40,7 +50,6 @@ public TaskTests() assembly = Assembly.LoadFile(afterAssemblyPath); } - [Test] public void EnsureAttributeExists() { @@ -49,7 +58,7 @@ public void EnsureAttributeExists() .First(); Assert.IsNotNull(customAttributes.InformationalVersion); Assert.IsNotEmpty(customAttributes.InformationalVersion); - Trace.WriteLine(customAttributes.InformationalVersion); + Trace.WriteLine($"InfoVersion: {customAttributes.InformationalVersion}"); } [Test] @@ -60,8 +69,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}"); } @@ -73,4 +82,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 15f4e03..aeb9bae 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 0512bb1..d29695a 100644 --- a/Tests/TokenResolverTests.cs +++ b/Tests/TokenResolverTests.cs @@ -8,18 +8,19 @@ [TestFixture] public class TokenResolverTests { - ModuleDefinition moduleDefinition; + System.Version version; FormatStringTokenResolver resolver; string beforeAssemblyPath; - [TestFixtureSetUp] + [OneTimeSetUp] public void FixtureSetUp() { beforeAssemblyPath = Path.GetFullPath(Path.Combine(TestContext.CurrentContext.TestDirectory, @"..\..\..\AssemblyToProcess\bin\Debug\AssemblyToProcess.dll")); #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 +38,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 +49,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 +60,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 +71,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 +82,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 +95,7 @@ public void Replace_branch() { var branchName = repo.Head.Name; - var result = resolver.ReplaceTokens("%branch%", moduleDefinition, repo, ""); + var result = resolver.ReplaceTokens("%branch%", version, repo, ""); Assert.AreEqual(branchName, result); }); @@ -107,7 +108,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 +119,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 +139,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 +152,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); }); @@ -165,8 +166,8 @@ public void Replace_time() var now = DateTime.Now; var utcNow = DateTime.UtcNow; - Assert.AreEqual(now.ToString("yyMMddmm"), resolver.ReplaceTokens("%now:yyMMddmm%", moduleDefinition, repo, "")); - Assert.AreEqual(utcNow.ToShortDateString(), resolver.ReplaceTokens("%utcnow%", moduleDefinition, repo, "")); + Assert.AreEqual(now.ToString("yyMMddmm"), resolver.ReplaceTokens("%now:yyMMddmm%", version, repo, "")); + Assert.AreEqual(utcNow.ToShortDateString(), resolver.ReplaceTokens("%utcnow%", version, repo, "")); }); } @@ -181,7 +182,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); });