Geeks With Blogs
Colin Bowern ... more of the usual bool

For some of our projects we're looking to not only deploy assemblies to the GAC, but generate native images and copy the debug symbols to the GAC (which buys us line numbers in exceptions -- it's much easier to debug when you know the line number!).  I've been experimenting with the VS2005 Setup Project to try and automate the process.  During this experimentation realized a number of short comings:

  • Cannot have an assembly be used as a Custom Action and deployed into the GAC at the same time.
    • Solution: Move Installer class to a separate assembly.
  • There's no fancy “Generate Native Image“ flag.
    • Solution: Write a Custom Action to generate a native image.
  • The Global Assembly Cache folder is reserved for signed outputs only.
    • Solution: Write a Custom Action to copy the unsigned outputs as part of the install.

It seems as usual the Setup Project that ships with VS2005 is weak in a number of areas (e.g. deployment of multiple instance applications).  If the product groups were all forced to use the Setup Project to deploy all Microsoft apps I bet it would change drastically (hint hint MSFT).  At this point I don't want to pay a billion dollars for InstallShield (which in past experience tends to crash numerous times), nor do I want to have to script out something for a seamingly common set of functions that I'm sure a lot of us developers have needs for.

For anyone who is trying to NGen or copy the Debug Symbols (aka Program Database, PDB) to the GAC during installation below is a snippet of my Installer class.  A few notes:

  • This class has been tested with the RTM version of the .NET Framework 2.0.
  • None of the paths are hard-coded.  I wanted to future-proof it so that it doesn't break under future builds and so it would work in scenarios where people moved the GAC.
  • Since the installer remains separate from the rest of the classes in my solution I did hard code the assembly names.  I know it's not the greatest idea but it works for me partly because they all share the same version number and public key token.  If you needed to handle other assemblies you could probably go with a manifest xml file or something like that as the source and then read the version numbers and tokens from the GAC.
  • Debug Symbols need to reside in the same folder as the installer class.  Once again, you can feel free to modify the code to be more picky.  The Context propery supports parameters which can be passed in through the CustomActionData propery of the Custom Action.
  • I couldn't copy debug symbols during the Install phase as the destination folders in the GAC didn't exist.  As such it ended up as part of the Commit phase.

And now on to the code:

    1 private string AssemblyCachePath, FrameworkPath;

    2 private string[] AssemblyNames = new string[]

    3     { "MyAssembly", "MySecondAssembly" };

    4 

    5 public Installer()

    6 {

    7     FrameworkPath = RuntimeEnvironment.GetRuntimeDirectory();

    8 

    9     int MaxPathLength = 1024;

   10     StringBuilder Buffer = new StringBuilder(MaxPathLength);

   11     GetCachePath(ASM_CACHE_FLAGS.ASM_CACHE_ROOT, Buffer, ref MaxPathLength);

   12     AssemblyCachePath = Buffer.ToString();

   13     //...

   14 }

   15 

   16 [DllImport("fusion.dll")]

   17 public extern static int GetCachePath(

   18     ASM_CACHE_FLAGS dwCacheFlags,

   19     [MarshalAs(UnmanagedType.LPWStr)] StringBuilder pwzCachePath,

   20     ref int pcchPath);

   21 

   22 [SecurityPermission(SecurityAction.Demand)]

   23 public override void Install(System.Collections.IDictionary stateSaver)

   24 {

   25     base.Install(stateSaver);

   26 

   27     Assembly ExecutingAssembly = Assembly.GetExecutingAssembly();

   28     AssemblyName ExecutingAssemblyName = ExecutingAssembly.GetName();

   29     string ExecutingAssemblyFullName = ExecutingAssemblyName.FullName;

   30 

   31     ProcessStartInfo NGenStartInfo = new ProcessStartInfo(

   32         Path.Combine( FrameworkPath, "ngen.exe" )

   33         );

   34 

   35     try

   36     {

   37         for (int index = AssemblyNames.GetUpperBound(0);

   38                 index >= AssemblyNames.GetLowerBound(0); index--)

   39         {

   40             // Create Native Image

   41             NGenStartInfo.Arguments = String.Format("install \"{0}\"",

   42                 ExecutingAssemblyFullName.Replace(

   43                     ExecutingAssemblyName.Name,

   44                     AssemblyNames[index]));

   45 

   46             NGenStartInfo.RedirectStandardOutput = true;

   47             NGenStartInfo.UseShellExecute = false;

   48 

   49             using (Process NGenProcess = Process.Start(NGenStartInfo))

   50             {

   51                 NGenProcess.WaitForExit();

   52                 Context.LogMessage(NGenProcess.StandardOutput.ReadToEnd());

   53             }

   54         }

   55     }

   56     catch (Exception ex)

   57     {

   58         throw new InstallException(ex.Message, ex);

   59     }

   60 }

   61 

   62 public override void Commit(System.Collections.IDictionary savedState)

   63 {

   64     base.Commit(savedState);

   65 

   66     Assembly ExecutingAssembly = Assembly.GetExecutingAssembly();

   67     AssemblyName ExecutingAssemblyName = ExecutingAssembly.GetName();

   68     string ExecutingAssemblyFullName = ExecutingAssemblyName.FullName;

   69 

   70     string GacFolder = String.Format(

   71         "{0}\\GAC_{1}\\{2}\\{3}__{4}",

   72         AssemblyCachePath,

   73         ExecutingAssemblyName.ProcessorArchitecture,

   74         ExecutingAssemblyName.Name,

   75         ExecutingAssemblyName.Version,

   76         ByteArrayToString(ExecutingAssemblyName.GetPublicKeyToken()));

   77 

   78     string DebugSymbolsPath = Path.GetDirectoryName(ExecutingAssembly.Location);

   79 

   80     try

   81     {

   82         for (int index = AssemblyNames.GetUpperBound(0);

   83                 index >= AssemblyNames.GetLowerBound(0); index--)

   84         {

   85             // Copy Debug Symbols to GAC Folder

   86             string DebugSymbolsFile = AssemblyNames[index] + ".pdb";

   87             string DestinationFile = Path.Combine(

   88                 GacFolder.Replace(

   89                     ExecutingAssemblyName.Name,

   90                     AssemblyNames[index]),

   91                 DebugSymbolsFile);

   92 

   93             string SourceFile = Path.Combine(DebugSymbolsPath, DebugSymbolsFile);

   94             if (File.Exists(SourceFile))

   95             {

   96                 Context.LogMessage(String.Format(

   97                     "Copying {0} to {1}",

   98                     SourceFile,

   99                     DestinationFile));

  100 

  101                 File.Copy(SourceFile, DestinationFile, true);

  102             }

  103         }

  104     }

  105     catch (Exception ex)

  106     {

  107         throw new InstallException(ex.Message, ex);

  108     }

  109 }

  110 

  111 public override void Uninstall(System.Collections.IDictionary savedState)

  112 {

  113     base.Uninstall(savedState);

  114 

  115     Assembly ExecutingAssembly = Assembly.GetExecutingAssembly();

  116     AssemblyName ExecutingAssemblyName = ExecutingAssembly.GetName();

  117     string ExecutingAssemblyFullName = ExecutingAssemblyName.FullName;

  118 

  119     ProcessStartInfo NGenStartInfo = new ProcessStartInfo(

  120         Path.Combine( FrameworkPath, "ngen.exe" )

  121         );

  122 

  123     try

  124     {

  125         for (int index = AssemblyNames.GetLowerBound(0);

  126                 index <= AssemblyNames.GetUpperBound(0); index++)

  127         {

  128             // Create Native Image

  129             NGenStartInfo.Arguments = String.Format("uninstall \"{0}\"",

  130                 ExecutingAssemblyFullName.Replace(

  131                     ExecutingAssemblyName.Name,

  132                     AssemblyNames[index]));

  133 

  134             NGenStartInfo.RedirectStandardOutput = true;

  135             NGenStartInfo.UseShellExecute = false;

  136 

  137             using (Process NGenProcess = Process.Start(NGenStartInfo))

  138             {

  139                 NGenProcess.WaitForExit();

  140                 Context.LogMessage(NGenProcess.StandardOutput.ReadToEnd());

  141             }

  142         }

  143     }

  144     catch (Exception ex)

  145     {

  146         throw new InstallException(ex.Message, ex);

  147     }

  148 }

  149 

  150 public override void Rollback(System.Collections.IDictionary savedState)

  151 {

  152     base.Rollback(savedState);

  153 

  154     Assembly ExecutingAssembly = Assembly.GetExecutingAssembly();

  155     AssemblyName ExecutingAssemblyName = ExecutingAssembly.GetName();

  156     string ExecutingAssemblyFullName = ExecutingAssemblyName.FullName;

  157 

  158     ProcessStartInfo NGenStartInfo = new ProcessStartInfo(

  159         Path.Combine( FrameworkPath, "ngen.exe" )

  160         );

  161 

  162     try

  163     {

  164         for (int index = AssemblyNames.GetUpperBound(0);

  165                 index >= AssemblyNames.GetLowerBound(0); index--)

  166         {

  167             // Create Native Image

  168             NGenStartInfo.Arguments = String.Format("uninstall \"{0}\"",

  169                 ExecutingAssemblyFullName.Replace(

  170                     ExecutingAssemblyName.Name,

  171                     AssemblyNames[index]));

  172 

  173             NGenStartInfo.RedirectStandardOutput = true;

  174             NGenStartInfo.UseShellExecute = false;

  175 

  176             using (Process NGenProcess = Process.Start(NGenStartInfo))

  177             {

  178                 NGenProcess.WaitForExit();

  179                 Context.LogMessage(NGenProcess.StandardOutput.ReadToEnd());

  180             }

  181         }

  182     }

  183     catch (Exception ex)

  184     {

  185         throw new InstallException(ex.Message, ex);

  186     }

  187 }

  188 

  189 private string ByteArrayToString(byte[] byteArray)

  190 {

  191     if (byteArray == null)

  192         return null;

  193 

  194     StringBuilder buffer = new StringBuilder(byteArray.Length);

  195     for (int index = 0; index < byteArray.Length; index++)

  196     {

  197         buffer.AppendFormat("{0:x2}", byteArray[index]);

  198     }

  199 

  200     return buffer.ToString();

  201 }

  202 

  203 public enum ASM_CACHE_FLAGS

  204 {

  205     ASM_CACHE_ZAP = 0x01,

  206     ASM_CACHE_GAC = 0x02,

  207     ASM_CACHE_DOWNLOAD = 0x04,

  208     ASM_CACHE_ROOT = 0x08

  209 }

Posted on Thursday, March 23, 2006 9:37 PM Build and Deployment | Back to top


Comments on this post: Installing with Native Images and Debug Symbols

No comments posted yet.
Your comment:
 (will show your gravatar)


Copyright © Colin Bowern | Powered by: GeeksWithBlogs.net