NBox. Мануал для российских программеров.

Written by elwood

Недавно собрался и написал краткий ман по NBox’у на русском языке.

Что это такое и зачем нужно ?

NBox – утилита c открытым исходным кодом, предназначенная для сжатия множества дотнетовых сборок и файлов приложения в одну управляемую сборку, которая будет хранить сжатые сборки в себе и при необходимости загружать их динамически.

Для чего это может понадобиться ?

  • Во-первых, для уменьшения размера дистрибутива (файлы сжимаются по алгоритму LZMA, используемому в популярном архиваторе 7-zip).
  • Во-вторых, иногда для разработчика удобнее предоставлять дистрибутив одним исполняемым файлом
    вместо того, чтобы делать полноценный инсталлятор или же распространять множество файлов в одном архиве (то есть этот инструмент можно использовать в качестве лайт-замены для инсталляторов).
  • В-третьих, загрузка приложения, в котором много зависимостей, занимает обычно больше времени, чем загрузка одного исполняемого файла с последующей подгрузкой необходимых модулей прямо в памяти (особенно это заметно на медленных сменных носителях), – и NBox можно использовать для оптимизации загрузки приложения.

Дополнительные особенности :

  • Возможность включать в результирующую сборку не только managed-сборки, но и библиотеки с неуправляемым кодом. Неуправляемые библиотеки обычно используются через interop, и поэтому они должны быть извлечены перед запуском приложения. Обычно они извлекаются в ту же директорию, в которой расположен исполняемый файл, либо в системную директорию.
  • Возможность включать любые файлы.
    Да, вы можете засунуть любой файл и извлечь его перед запуском приложения в указанную директорию. Это может быть файл конфигурации приложения, какой-либо бинарник, звуковой файл или что-то совсем другое.
  • Корректная работа с WPF-приложениями.
    Стандартные WPF-приложения особенным образом работает с ресурсами, поэтому обычный алгоритм для них не работает. Поэтому приходится слегка похимичить с ресурсами. Либо нужно дублировать ресурсы оригинальной сборки в сжатой, либо менять привязку к абсолютным путям на относительные в исходном коде и xaml.

Пример создания простого конфига.

Возьмем для примера саму утилиту NBox и сожмем её в один исполняемый файл вместе со всеми сборками, необходимыми для ее выполнения. Для этого сначала необходимо загрузить проект в VisualStudio и откомпилировать. Получена bin-директория с файлами

    NBox.exe - основная сборка нашего приложения
    NBox.exe.config - конфигурационный файл, мы можем также включить его
    NBox.pdb - необходим для отладки, не будем его брать
    config-file.xsd - это просто копия файла схемы, пропускаем
    NLog.dll - одна из сборок-зависимостей, сжимаем
    Common.Logging.dll - сжимаем
    Common.Logging.NLog.dll - сжимаем
    ICSharpCode.SharpZipLib.dll - сжимаем

Помещаем все нужные файлы в директорию src. Здесь же создаем директорию output.
Пути прописываем относительно %configdir% – места, где будет лежать конфиг.
Для нашего приложения конфигурационный файл может быть следующим :

<?xml version="1.0" encoding="utf-8" ?>
<configuration xmlns="http://www.elwood.su/projects/nbox/schemas/config-file/v1.0">
 
  <!-- Набор настроек для сжатия. Вы можете определить несколько таких опций (например,
одна будет сжимать очень сильно, другая вообще не будет сжимать - и дергать их по id -->
  <compression-options-set>
    <compression-option id="defaultCompression">
    <!-- Пока здесь можно установить только то, сжимать вообще или нет. Уровень не меняется -->
      <level value="ultra"/>
    </compression-option>
  </compression-options-set>
 
  <!-- Определяем сборки, которые будут входить в исполняемый файл. -->
  <assemblies default-compression-ref="defaultCompression"
              default-include-method="Overlay"
              default-generate-partial-aliases="false"
              default-lazy-load="false">
 
	<!-- Собственно список сборок. -->
    <assembly id="NBox.exe" path="%configdir%/src/NBox.exe"/>
    <assembly id="Common.Logging.dll" path="%configdir%/src/Common.Logging.dll"/>
    <assembly id="Common.Logging.NLog.dll" path="%configdir%/src/Common.Logging.NLog.dll"/>
    <assembly id="NLog.dll" path="%configdir%/src/NLog.dll"/>
    <assembly id="ICSharpCode.SharpZipLib.dll" path="%configdir%/src/ICSharpCode.SharpZipLib.dll"/>
  </assemblies>
 
  <!-- Далее следуют файлы, которые нам нужны. -->
  <files default-include-method="Overlay"
         default-compression-ref="defaultCompression"
         default-overwrite-on-extracting="CheckExist">
    <file id="NBox.exe.config" path="%configdir%/src/NBox.exe.config"
           extract-to-path="%mainassemblydir%/NBox.exe.config"/>
  </files>
 
  <!-- И здесь определяем то, что мы должны получить. assembly-name задает имя
  новой сборки, оно должно отличаться от имен всех сборок проекта,
  чтобы не создавать конфликтов -->
  <output path="%configdir%/output/NBox.exe" assembly-name="NBoxBoxed" grab-resources="false"
          apptype="Console" apartment="STA" machine="x86" main-assembly-ref="NBox.exe">
    <includes>
      <assemblies>
        <assembly ref="Common.Logging.dll"/>
        <assembly ref="Common.Logging.NLog.dll"/>
        <assembly ref="NLog.dll"/>
        <assembly ref="ICSharpCode.SharpZipLib.dll"/>
      </assemblies>
      <files>
        <file ref="NBox.exe.config"/>
      </files>
    </includes>
    <!-- Небольшая оптимизация для компилятора microsoft c# -->
    <compiler-options>/filealign:512</compiler-options>
  </output>
</configuration>

Осталось только запустить NBox командой наподобие следующей :
NBox.exe my-config.xml

Что вообще может содержать в себе конфигурационный файл ?

  1. Определение настроек сжатия.

    Здесь все достаточно очевидно, вы определяете способ сжатия, и затем используете ссылки на него,
    когда определяете сборки и файлы, внедряемые в проект. Единственное, что стоит отметить – то, что
    на данный момент управление степенью сжатия не реализовано.

  2. Определение сборок.

    Каждая сборка имеет следующие атрибуты :

    • id – идентификатор сборки, необходим при связывании проекта
    • path – путь к исходному файлу, возможно, с использованием переменных %configdir% и %root%
    • compression-ref – ссылка на способ сжатия
    • copy-compressed-to – NBox сжимает файлы во временную директорию, содержимое которой позже очищается. И если вы хотите оставить сжатый файл, вы можете задать имя файла, куда он будет скопирован. Это необходимо, если вы используете метод внедрения “файл”, то есть сжатая сборка не будет объединена с исполняемым файлом, а будет лежать рядом и загружаться из файла.
    • include-method – может быть File, Resource или Overlay. При первом способе сжатая сборка лежит рядом с исполняемым файлом и грузится из файла. Resource – сжатая сборка станет частью ресурсов исполняемого файла.
      Overlay – сжатая сборка будет дописана в конец исполняемого файла. Последний способ экономит память, поскольку оверлеи не загружаются в память загрузчиком Windows.
    • file-load-from-path – если вы выбрали метод “File“, то наш собранных exe-шник должен знать, откуда он будет грузить сборку. С помощью переменных %mainassemblydir% и %system32dir% вы можете определить путь к сжатому файлу. Если же вы используете метод внедрения “Resource” или “Overlay“, этот атрибут не нужен.
    • overlay-offset и overlay-lenght – эти атрибуты используются лоадером NBox’а в момент загрузки приложения и на этапе сборки проекта игнорируются.
    • resource-name – аналогично предыдущему
    • lazy-load – если равно true, то сборка будет загружена в момент первого обращения к ней. Иначе – сборка будет принудительно загружена в момент старта приложения.
    • generate-partial-aliases – генерировать ли частичные алиасы по полному имени сборки. Т.е. при true для сборки с полным именем “BettyBoxed, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null” будут сгенерированы таже имена “BettyBoxed, Version=1.0.0.0, Culture=neutral“, “BettyBoxed, Version=1.0.0.0” и просто “BettyBoxed“.
    • aliases – список алиасов, по которым вы можете достучаться до вашей сборки, помимо полного имени и, если generate-partial-aliases был установлен в true, – частичных алиасов. К примеру, вы можете добавить свой алиас к сборке так :
      <aliases>
          <alias value="presentationframework.luna, Culture=neutral"/>
      </aliases>
  3. Определение файлов

    С файлами все аналогично, только отсутствуют алиасы и добавлены следующие 2 атрибуты :

    • extract-to-path – куда будет помещен файл при распаковке. Если не указывать, файл будет распакован в ту директорию, откуда стартовало приложение.
    • overwrite-on-extracting – режим перезаписи. Может принимать значения Always, CheckExists, CheckSize, Never.
  4. Определение результирующей сборки.

    Тут, в принципе, тоже все достаточно просто и интуитивно понятно.
    Вы отмечаете те сборки и файлы, которые должны быть включены, задаете имя сборки, иконку, дополнительные опции компилятора. Единственный неочевидный атрибут – grab-resources – его смысл изложен в следующем разделе.

А что с WPF ?

По поводу WPF. В приложениях WPF, создаваемых VisualStudio, содержится следующий код:

<Application x:Class="ExampleWPF_1.App"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    StartupUri="Window1.xaml">
    <Application.Resources>
 
    </Application.Resources>
</Application>

И он будет искать Window1.xaml в _исполняемом файле_, а не в сборке, в которой это определено.
Так как у нас исполняемым файлом является сжатая, сгенерированная NBox’ом сборка, которая содержит другие сборки в сжатом виде и свои собственные ресурсы, то приложение при загрузке падает с ошибкой “Не могу найти ресурс”. Для того, чтобы этого не происходило, можно привязать загрузку ресурсов к конкретной сборке, например, следующим образом :

<Application x:Class="ExampleWPF_1.App"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    StartupUri="/ExampleWPF_1;component/Window1.xaml">
    <Application.Resources>
 
    </Application.Resources>
</Application>

Либо – для тех приложений, код которых менять по каким-либо причинам не следует, – выставить в
конфиге сжатия флаг grabResources в true. При сжатии NBox продублирует все ресурсы из mainAssembly в свои ресурсы, и обращение к ресурсам будет происходить корректно. Минусы данного способа – результирующая сборка может сильно распухнуть из-за дублирования толстых ресурсов, и обращения к ресурсам теперь идут на самом деле к другой сборке, что может в будущем повлиять на поведение приложения, при изменении поведения подсистемы WPF. Плюс – не нужно модифицировать код.

Основное следствие данной проблемы заключается в том, что вы не можете включать в проект более одной сборки, содержащей WPF-ресурсы (чтобы узнать об их присутствии, можно поискать рефлектором ресурс с названием AssemblyName.g.resources). Почему ? Потому что при grabResources = true мы должны продублировать все ресурсы из всех сборок в один с именем AssemblyNameBoxed.g.resources, но возможности создать несколько ресурсов с одинаковым именем у нас нет.

Что делать в таких случаях ? Например, вы хотите включить темы для оформления WPF – dll, содержащие WPF-ресурсы. Можно просто добавить их как файлы, чтобы при загрузке они распаковались рядом с приложением.
Конечно, сначала стоит попробовать включить их стандартно – как сборки, возможно, проблемы не возникнет – если разработчики этой библиотеки использовали относительные пути относительно сборки или обращений к ресурсам нет вообще.