Pages

Friday, February 21, 2014

Automatically Embed Copy Local Assemblies with Symbols in MSBuild

Many have written about how to automatically embed assemblies into an executable, such as:
Lots of questions about this on stackoverflow as well, such as:
However, none of these show how pdb-files can be embedded as well to ensure symbols are also loaded when resolving embedded assemblies as detailed in Embedded Assembly Loading with support for Symbols and Portable Class Libraries in C#.

Automatically embed all dll- and pdb-files exclude xml-files

The solution, shown below, is a simple extension of what Daniel Chambers has described, but also includes pdb-files and exclude copying of xml-files to the output directory since many libraries often include these documentation files.
<Target Name="EmbedReferencedAssemblies" AfterTargets="ResolveAssemblyReferences">
  <ItemGroup>
    <!-- get list of assemblies marked as CopyToLocal -->
    <FilesToEmbed Include="@(ReferenceCopyLocalPaths)" 
                  Condition="('%(ReferenceCopyLocalPaths.Extension)' == '.dll' Or '%(ReferenceCopyLocalPaths.Extension)' == '.pdb')" />
    <FilesToExclude Include="@(ReferenceCopyLocalPaths)" 
                  Condition="'%(ReferenceCopyLocalPaths.Extension)' == '.xml'" />

    <!-- add these assemblies to the list of embedded resources -->
    <EmbeddedResource Include="@(FilesToEmbed)">
      <LogicalName>%(FilesToEmbed.DestinationSubDirectory)%(FilesToEmbed.Filename)%(FilesToEmbed.Extension)</LogicalName>
    </EmbeddedResource>

    <!-- no need to copy the assemblies locally anymore -->
    <ReferenceCopyLocalPaths Remove="@(FilesToEmbed)" />
    <ReferenceCopyLocalPaths Remove="@(FilesToExclude)" />
  </ItemGroup>

  <Message Importance="high" Text="Embedding: @(FilesToEmbed->'%(Filename)%(Extension)', ', ')" />
</Target>
To use this simply copy and paste this into the executable project file (e.g. *.csproj) right after:
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />

Automatically embed all dll- and pdb-files exclude xml-files and mixed mode assemblies

However, unfortunately as far as I know embedding mixed mode assemblies (e.g. with both managed and native code from for example a C++/CLI project) does not work. So these still have to be copied to the build output. At least, if you do not want to extract the embedded file, as detailed in Single Assembly Deployment of Managed and Unmanaged Code.

One solution to this is to simply exclude these files by adding exclude conditions to the above xml. For example:
<Target Name="EmbedReferencedAssemblies" AfterTargets="ResolveAssemblyReferences">
  <ItemGroup>
    <!-- get list of assemblies marked as CopyToLocal -->
    <FilesToEmbed Include="@(ReferenceCopyLocalPaths)" 
                  Condition="('%(ReferenceCopyLocalPaths.Extension)' == '.dll' Or '%(ReferenceCopyLocalPaths.Extension)' == '.pdb') And '%(Filename)'!='MixedModeAssemblyA' And '%(Filename)'!='MixedModeAssemblyB'" />
    <FilesToExclude Include="@(ReferenceCopyLocalPaths)" 
                  Condition="'%(ReferenceCopyLocalPaths.Extension)' == '.xml'" />

    <!-- add these assemblies to the list of embedded resources -->
    <EmbeddedResource Include="@(FilesToEmbed)">
      <LogicalName>%(FilesToEmbed.DestinationSubDirectory)%(FilesToEmbed.Filename)%(FilesToEmbed.Extension)</LogicalName>
    </EmbeddedResource>

    <!-- no need to copy the assemblies locally anymore -->
    <ReferenceCopyLocalPaths Remove="@(FilesToEmbed)" />
    <ReferenceCopyLocalPaths Remove="@(FilesToExclude)" />
  </ItemGroup>

  <Message Importance="high" Text="Embedding: @(FilesToEmbed->'%(Filename)%(Extension)', ', ')" />
</Target>
I would love to have a solution that actually checks whether an assembly is mixed mode (i.e. not pure) before embedding it. Or at least create a list of assembly names to exclude instead of the crude condition hack above.

One could also imagine checking the path of the assembly and whether this has AnyCPU, x86, x64 in the path or similarly as a convention for embedding or not embedding the given assembly. Lots of other improvements should be possible...

There is also a complete solution out there in the form of Costura.Fody, which exists as a convenient nuget package as well, see http://www.nuget.org/packages/Costura.Fody. This does, however, rely on IL rewriting which may be a problem for some. It does look as if it handles all possible issues via configuration, though.

Tuesday, February 18, 2014

Embedded Assembly Loading with support for Symbols and Portable Class Libraries in C#

Jeffrey Richter has previously written about how to deploy a single executable file for an application by embedding dependencies as resources in the main application assembly in Jeffrey Richter: Excerpt #2 from CLR via C#, Third Edition.

However, this solution has a few issues. It does not handle Portable Class Libraries (PCLs) and does not show how to support loading symbols from embedded pdb-files either. The code presented below handles both.

As with the solution Jeffrey Richter details, one simply adds a handler to the current domains AssemblyResolve event, which is called whenever an assembly could not be resolved directly. However, this also occurs when an embedded portable class library (such as Autofac) has defined a dependency towards any BCL assembly (e.g. System.Core 2.0.5.0), in this case you have to check if the assembly is retargetable and then load it directly via the usual CLR mechanism so the appropriate version is loaded.

For a better debug experience and better exception stack traces it is recommended to include pdb-files as well. pdb-files are handled by simply loading these if they have been embedded, have the same name as the dll-file and then using the Assembly.Load overload that also loads raw symbol data from a byte array.

To use the code do the following:
  • Call SetupEmbeddedAssemblyResolve as the first thing in your application
  • Add dependencies incl. pdb-files, if needed, to your project via Add as Link and change the build action to Embedded Resource and Copy to Output Directory to Do not copy
  • Change the assembly references properties under References and set Copy Local to false
UPDATE: See Automatically Embed Copy Local Assemblies with Symbols in MSBuild for how to automatically embed assemblies instead of doing this manually.
private static void SetupEmbeddedAssemblyResolve()
{
    AppDomain.CurrentDomain.AssemblyResolve += (sender, args) =>
    {
        var name = args.Name;
        var asmName = new AssemblyName(name);

        // Any retargetable assembly should be resolved directly using normal load e.g. System.Core issue 
        if (name.EndsWith("Retargetable=Yes"))
        {
            return Assembly.Load(asmName);
        }

        var executingAssembly = Assembly.GetExecutingAssembly();
        var resourceNames = executingAssembly.GetManifestResourceNames();

        var resourceToFind = asmName.Name + ".dll";
        var resourceName = resourceNames.SingleOrDefault(n => n.Contains(resourceToFind));

        if (string.IsNullOrWhiteSpace(resourceName)) { return null; }

        var symbolsToFind = asmName.Name + ".pdb";
        var symbolsName = resourceNames.SingleOrDefault(n => n.Contains(symbolsToFind));

        var assemblyData = LoadResourceBytes(executingAssembly, resourceName);

        if (string.IsNullOrWhiteSpace(symbolsName))
        { 
            Trace.WriteLine(string.Format("Loading '{0}' as embedded resource '{1}'", resourceToFind, resourceName));

            return Assembly.Load(assemblyData);
        }
        else
        {
            var symbolsData = LoadResourceBytes(executingAssembly, symbolsName);

            Trace.WriteLine(string.Format("Loading '{0}' as embedded resource '{1}' with symbols '{2}'", resourceToFind, resourceName, symbolsName));

            return Assembly.Load(assemblyData, symbolsData);
        }
    };
}

private static byte[] LoadResourceBytes(Assembly executingAssembly, string resourceName)
{
    using (var stream = executingAssembly.GetManifestResourceStream(resourceName))
    {
        var data = new byte[stream.Length];

        stream.Read(data, 0, data.Length);

        return data;
    }
}
Based on Jeffrey Richter: Excerpt #2 from CLR via C#, Third Edition and FileNotFoundException when trying to load Autofac as an embedded assembly.