Getting the file version of a native exe in MSBuil

2019-05-14 15:18发布

问题:

I have a number of Visual C++ projects in a Visual Studio 2010 solution. Also in this solution is a WiX project that builds the installer for the executable that is the product of one of the C++ projects.

The executable has a resource file in its project which writes the version of the program to the executable.

Now I'd like to version the WiX-built installer with the same number as the one written to the executable by the resource file. I've searched the WiX related posts on StackOverflow and found this post:

Referencing a WixVariable defined in a WiX Library Project from a WiX Setup Project

The accepted answer to which, seems to indicate that a possible solution is to use MSBuild and the GetAssemblyIdentity task in the BeforeBuild Target to acquire the version number from another file (in the case of the SO question a DLL, in my case the executable) and expose it to WiX before WiX builds the installer.

I tried adding this to the MSBuild portion of my .wixproj file but when I try to build the installer I get an error returned saying:

error MSB3441: Cannot get assembly name for "<ExePath>". Could not load file or assembly '<ExeName>.exe' or one of its dependencies. The module was expected to contain an assembly manifest.

I can't seem to find any information on MSDN about this error as it relates to MSBuild. I've checked the built executable and it definitely has a version number on it (as well as the rest of the information from the .rc file) and the WiX project depends on the project that outputs the executable; so I assume its BeforeBuild task is running after the project it depends on has been built completely.

Should I be using a different task instead of GetAssemblyIdentity to retrieve the version number from the .exe in MSBuild, are there other requirements to satisfy before GetAssemblyIdentity will work, or is it just not possible to get this type of information about .exe files in MSBuild?

EDIT :

I accepted Rob's answer since I was misunderstanding the difference between ProductVersion and FileVersion, and the WiX technique that he suggested was working as intended and is a step towards the solution I needed.

FileVersion is an attribute of executables only. Msi files are essentially databases and ProductVersion is an entry in that database; they have no FileVersion attribute to set. The method he suggests correctly sets ProductVersion in the .msi database.

The title of this question is now not really related to what the problem I actually had was, since I was asking for a solution I believed I needed at the time. I've now solved the root problem which was simply getting access to the ProductVersion of the installer. I found a cscript script posted online here: http://kentie.net/article/wixnameversion/index.htm that shows how to access the ProductVersion of the .msi. Using that has allowed me to extract the ProductVersion and use it in other tools.

回答1:

An easier solution, if you don't need the version in MSBuild, is to just reference the version of the file directly in your .wxs file. Here's a snippet that shows what to do:

<?xml version="1.0" encoding="UTF-8"?>
<Wix xmlns="http://schemas.microsoft.com/wix/2006/wi">
  <Product Version="!(bind.fileVersion.ExeWithVersion)" ...>

    ...

   <Component ...>
     <File Id="ExeWithVersion" Source="path\to\your\versioned\file.exe" />
   </Component> 

   ...

  </Product>
</Wix>

The magic is that the !(bind.fileVersion.Xxx) says to look up the File element with Id='Xxx' and get its version. This is far away the easiest way to get the version of the file into your MSI package.



回答2:

I needed the file version one time, and I ended up writing a custom task to get the FileVersion because I couldn't find anything.

namespace GranadaCoder.Framework.CrossDomain.MSBuild.Tasks.IO//.FileVersionTask
{
    using System;
    using System.Collections.Generic;
    using System.Collections;
    using System.Linq;
    using System.Globalization;
    using System.Diagnostics;
    using System.IO;
    using System.Text;
    using System.Security;

    using Microsoft.Build.Framework;
    using Microsoft.Build.Utilities;

    public class FileVersionTask : FileBasedTaskBase
    {
        private static readonly string ROOT_DIRECTORY = "myrootdir";
        private static readonly string FULL_PATH = "myfullpath";
        private static readonly string FILE_NAME = "myfilename";
        private static readonly string DIRECTORY = "mydirectory";
        private static readonly string EXTENSION = "myextension";
        private static readonly string VERSION = "myfileversion";

        /// <summary>
        /// Gets or sets the source files.
        /// </summary>
        /// <value>The source files.</value>
        [Required]
        public string SourceFiles { get; set; }

        /// <summary>
        /// Gets the file versions as a Task Output property.
        /// </summary>
        /// <value>The file versions.</value>
        [Output]
        public ITaskItem[] FileVersions
        { get; private set; }

        /// <summary>
        /// Task Entry Point.
        /// </summary>
        /// <returns></returns>
        protected override bool AbstractExecute()
        {
            InternalExecute();
            return !Log.HasLoggedErrors;
        }

        /// <summary>
        /// Internal Execute Wrapper.
        /// </summary>
        private void InternalExecute()
        {
            IList<string> files = null;

            if (String.IsNullOrEmpty(this.SourceFiles))
            {
                Log.LogWarning("No SourceFiles specified");
                return;
            }

            if (!String.IsNullOrEmpty(this.SourceFiles))
            {
                Console.WriteLine(this.SourceFiles);
                files = base.ConvertSourceFileStringToList(this.SourceFiles);
            }

            //List<string> fileVersions = new List<string>();

            ArrayList itemsAsStringArray = new ArrayList();

            foreach (string f in files)
            {
                FileInfoWrapper fiw = null;
                fiw = this.DetermineFileVersion(f);

                IDictionary currentMetaData = new System.Collections.Hashtable();

                currentMetaData.Add(ROOT_DIRECTORY, fiw.RootDirectory);
                currentMetaData.Add(FULL_PATH, fiw.FullPath);
                currentMetaData.Add(FILE_NAME, fiw.FileName);
                currentMetaData.Add(DIRECTORY, fiw.Directory);
                currentMetaData.Add(EXTENSION, fiw.Extension);
                currentMetaData.Add(VERSION, fiw.Version);

                itemsAsStringArray.Add(new TaskItem(fiw.Version, currentMetaData));

            }
            this.FileVersions = (ITaskItem[])itemsAsStringArray.ToArray(typeof(ITaskItem));
        }


        /// <summary>
        /// Determines the file version.
        /// </summary>
        /// <param name="fileName">Name of the file.</param>
        /// <returns>File version or 0.0.0.0 if value cannot be determined</returns>
        private FileInfoWrapper DetermineFileVersion(string fileName)
        {
            FileInfoWrapper fiw = new FileInfoWrapper();
            fiw.Directory = string.Empty;
            fiw.Extension = string.Empty;
            fiw.FileName = string.Empty;
            fiw.FullPath = string.Empty;
            fiw.RootDirectory = string.Empty;
            fiw.Version = "0.0.0.0";
            try
            {
                if (System.IO.File.Exists(fileName))
                {
                    fiw.Extension = System.IO.Path.GetExtension(fileName);
                    fiw.FileName = System.IO.Path.GetFileNameWithoutExtension(fileName);
                    fiw.FullPath = fileName;// System.IO.Path.GetFileName(fileName);
                    fiw.RootDirectory = System.IO.Path.GetPathRoot(fileName);

                    //Take the full path and remove the root directory to mimic the DotNet default behavior of '%filename'
                    fiw.Directory = System.IO.Path.GetDirectoryName(fileName).Remove(0, fiw.RootDirectory.Length);

                    FileVersionInfo fvi = FileVersionInfo.GetVersionInfo(fileName);
                    if (null != fvi)
                    {
                        if (null != fvi.FileVersion)
                        {
                            fiw.Version = fvi.FileVersion;
                        }
                    }
                }
            }
            catch (Exception ex)
            {
                if (ex is IOException
                    || ex is UnauthorizedAccessException
                    || ex is PathTooLongException
                    || ex is DirectoryNotFoundException
                    || ex is SecurityException)
                {
                    Log.LogWarning("Error trying to determine file version " + fileName + ". " + ex.Message);
                }
                else
                {
                    Log.LogErrorFromException(ex);
                    throw;
                }
            }
            return fiw;
        }




        /// <summary>
        /// Internal wrapper class to hold file properties of interest.
        /// </summary>
        internal sealed class FileInfoWrapper
        {
            public string Directory { get; set; }
            public string Extension { get; set; }
            public string FileName { get; set; }
            public string FullPath { get; set; }
            public string RootDirectory { get; set; }
            public string Version { get; set; }
        }
    }
}

.msbuild example

<?xml version="1.0" encoding="utf-8"?>
<Project DefaultTargets="AllTargetsWrapper" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">

  <UsingTask AssemblyFile="GranadaCoder.Framework.CrossDomain.MSBuild.dll" TaskName="FileVersionTask"/>




  <Target Name="AllTargetsWrapper">
    <CallTarget Targets="FileVersionTask1" />
    <CallTarget Targets="FileVersionTask2" />
  </Target>


  <PropertyGroup>
    <WorkingCheckout>c:\Program Files\MSBuild</WorkingCheckout>
  </PropertyGroup>


  <ItemGroup>
    <MyTask1ExcludeFiles Include="$(WorkingCheckout)\**\*.rtf" />
    <MyTask1ExcludeFiles Include="$(WorkingCheckout)\**\*.doc" />
  </ItemGroup>

  <ItemGroup>
    <MyTask1IncludeFiles Include="$(WorkingCheckout)\**\*.exe" Exclude="@(MyTask1ExcludeFiles)" />
  </ItemGroup>

  <Target Name="FileVersionTask1">
    <FileVersionTask SourceFiles="@(MyTask1IncludeFiles)" >

      <Output TaskParameter="FileVersions"  ItemName="MyFileVersionItemNames"/>

    </FileVersionTask>


    <Message Text=" MyFileVersionItemNames MetaData  "/>
    <Message Text="  ------------------------------- "/>
    <Message Text="   "/>


    <Message Text="directory: "/>
    <Message Text="@(MyFileVersionItemNames->'%(mydirectory)')"/>
    <Message Text="   "/>
    <Message Text="   "/>
    <Message Text="extension: "/>
    <Message Text="@(MyFileVersionItemNames->'%(myextension)')"/>
    <Message Text="   "/>
    <Message Text="   "/>
    <Message Text="filename: "/>
    <Message Text="@(MyFileVersionItemNames->'%(myfilename)')"/>
    <Message Text="   "/>
    <Message Text="   "/>
    <Message Text="fullpath: "/>
    <Message Text="@(MyFileVersionItemNames->'%(myfullpath)')"/>
    <Message Text="   "/>
    <Message Text="   "/>
    <Message Text="rootdir: "/>
    <Message Text="@(MyFileVersionItemNames->'%(myrootdir)')"/>
    <Message Text="   "/>
    <Message Text="   "/>

    <Message Text="fileversion: "/>
    <Message Text="@(MyFileVersionItemNames->'%(myfileversion)')"/>
    <Message Text="   "/>
    <Message Text="   "/>


    <Message Text="   "/>
    <Message Text="   "/>
    <Message Text="rootdir + directory + filename + extension: "/>
    <Message Text="@(MyFileVersionItemNames->'%(myrootdir)%(mydirectory)%(myfilename)%(myextension)')"/>
    <Message Text="   "/>
    <Message Text="   "/>



    <Message Text="List of files using special characters (carriage return)"/>
    <Message Text="@(MyFileVersionItemNames->'&quot;%(myfullpath)&quot;' , '%0D%0A')"/>
    <Message Text="   "/>
    <Message Text="   "/>



  </Target>



  <ItemGroup>
    <MyTask2IncludeFiles Include="c:\windows\notepad.exe"  />
  </ItemGroup>

  <Target Name="FileVersionTask2">
    <FileVersionTask SourceFiles="@(MyTask2IncludeFiles)" >
      <Output TaskParameter="FileVersions" PropertyName="SingleFileFileVersion"/>
    </FileVersionTask>

    <Message Text="SingleFileFileVersion = $(SingleFileFileVersion)   "/>

  </Target>


</Project>