Pull to refresh

WPF Applications Deployment: объединение .NET сборок в одном исполняемом файле

Reading time 4 min
Views 6.6K
Когда требуется объединить несколько управляемых сборок в одном файле, можно воспользоваться утилитой ILMerge:

ILMerge.exe /t:winexe /out:test.exe test1.exe test2.dll

В данном примере из двух сборок будет создан объединённый исполняемый файл. Атрибут /t:winexe указывает на то, что результатом будет оконное (WinForms) приложение.

Однако, с приложениями WPF утилита ILMerge работать не умеет. Это связано с особенностями компиляции XAML-файлов, использующихся в архитектуре WPF для декларативного описания структуры, поведения и анимации пользовательского интерфейса:
  • XAML-файл компилируется в BAML-код (аналог IL), который затем размещается в ресурсах сборки.
  • С помощью объявлений пространства имён XML XAML-файл может ссылаться на другие сборки.
  • XAML-файлы могут ссылаться друг на друга используя, к примеру, такие элементы, как объединённые словари ресурсов и фреймы.

При объединении подобных сборок ILMerge следовало бы исправить все URI доступа к BAML-ресурсам, однако этого не происходит.

К счастью есть другой способ: разместить файлы сборок внутри объединённой в качестве встроенных ресурсов.

Для того, чтобы размещение происходило автоматически, достаточно изменить файл проекта, добавив к нему следующую цель сразу после импорта файла Microsoft.CSharp.targets:

<Target Name="AfterResolveReferences">
   <ItemGroup>
    <EmbeddedResource Include="@(ReferenceCopyLocalPaths)"
      Condition="'%(ReferenceCopyLocalPaths.Extension)' == '.dll'">
     <LogicalName>%(ReferenceCopyLocalPaths.DestinationSubDirectory)%(ReferenceCopyLocalPaths.Filename)%(ReferenceCopyLocalPaths.Extension)</LogicalName>
    </EmbeddedResource>
   </ItemGroup>
 </Target>

Выполнение данной цели происходит сразу же после завершения задачи ResolveAssemblyReference. В результате выполнения указанной цели связанные сборки упаковываются в виде встроенных ресурсов. Каждый ресурс именуется согласно относительному пути и имени файла сборки.
Использование пути до сборки в именовании встроенных ресурсов является более гибким подходом и позволяет, к примеру, встраивать сборку вместе с локализациями, которые, как правило, имеют то же имя.

После того, как сборки упакованы в едином файле, необходимо загрузить все сборки в память до того, как будут инициализированы ядро и программная инфраструктура WPF.
«Волшебной» точкой входа в приложение WPF является файл App.xaml. Однако, на самом деле App.xaml компонуется в файл App.g.cs, который можно найти внутри проекта в каталоге obj. В файле App.g.cs находится стандартная точка входа — статитческий метод Main.

Таким образом, для того, чтобы загрузить вложенные сборки до инициализации ядра WPF необходимо переопределить стандартную точку входа.

Например вот так:

public class Program
{
    [STAThread]
    public static void Main()
    {
        App.Main();
    }
}

image

Последний шаг — собственный обработчик события AppDomain.AssemblyResolve, которое срабатывает каждый раз когда стандартный загрузчик не может определить физическое расположение очередной сборки.

public class Program
{
    static Dictionary<string, Assembly> assembliesDictionary = new Dictionary<string, Assembly>();

    [STAThread]
    public static void Main()
    {
        AppDomain.CurrentDomain.AssemblyResolve += OnResolveAssembly;
        App.Main();
    }

    private static Assembly OnResolveAssembly(object sender, ResolveEventArgs args)
    {
        AssemblyName assemblyName = new AssemblyName(args.Name);
        Assembly executingAssembly = Assembly.GetExecutingAssembly();
        string path = string.Format("{0}.dll", assemblyName.Name);

        if (assemblyName.CultureInfo.Equals(CultureInfo.InvariantCulture) == false)
        {
            path = String.Format(@"{0}\{1}", assemblyName.CultureInfo, path);
        }

        if (!assembliesDictionary.ContainsKey(path))
        {
            using (Stream assemblyStream = executingAssembly.GetManifestResourceStream(path))
            {
                if (assemblyStream != null)
                {
                    var assemblyRawBytes = new byte[assemblyStream.Length];
                    assemblyStream.Read(assemblyRawBytes, 0, assemblyRawBytes.Length);
                    using (var pdbStream = executingAssembly.GetManifestResourceStream(Path.ChangeExtension(path, "pdb")))
                    {
                        if (pdbStream != null)
                        {
                            var pdbData = new Byte[pdbStream.Length];
                            pdbStream.Read(pdbData, 0, pdbData.Length);
                            var assembly = Assembly.Load(assemblyRawBytes, pdbData);
                            assembliesDictionary.Add(path, assembly);
                            return assembly;
                        }
                    }
                    assembliesDictionary.Add(path, Assembly.Load(assemblyRawBytes));
                }
                else
                {
                    assembliesDictionary.Add(path, null);
                }
            }
        }
        return assembliesDictionary[path];
    }
}


Резюме


Представленный способ прост в реализации и предназначен для автоматической сборки управляемых модулей любого .NET приложения в одном исполняемом файле. Однако с точки зрения практической ценности, очевидно, что данное решение подходходит лишь для сборки небольших проектов. При этом неуправляемый код, очевидно, остаётся «за бортом» объединённого файла приложения.

Список литературы:


1. Построение приложения WPF | msdn.microsoft.com/ru-ru/library/aa970678.aspx
2. MSBuild | msdn.microsoft.com/ru-ru/library/ms171452.aspx
3. Влад Чистяков: MSBuild | www.rsdn.ru/article/devtools/msbuild-05.xml
4. Задача ResolveAssemblyReference | msdn.microsoft.com/ru-ru/library/9ad3f294.aspx
5. Расширение процесса построения Visual Studio | msdn.microsoft.com/ru-ru/library/ms366724.aspx
6. Разрешение загрузки сборок | msdn.microsoft.com/ru-ru/library/ff527268.aspx2
7. C# + WPF + сторонние сборки -> один .exe-шник | habrahabr.ru/blogs/personal/67836

Послесловие


В июне 2012 года вышел в свет opensource-плагин Costura для Visual Studio 2010, позволяющий свести описанный выше процесс к управлению настройками проекта.
Tags:
Hubs:
+2
Comments 3
Comments Comments 3

Articles