NBox. Мануал для российских программеров.
Недавно собрался и написал краткий ман по 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
Что вообще может содержать в себе конфигурационный файл ?
- Определение настроек сжатия.
Здесь все достаточно очевидно, вы определяете способ сжатия, и затем используете ссылки на него,
когда определяете сборки и файлы, внедряемые в проект. Единственное, что стоит отметить – то, что
на данный момент управление степенью сжатия не реализовано. - Определение сборок.
Каждая сборка имеет следующие атрибуты :
- 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>
- Определение файлов
С файлами все аналогично, только отсутствуют алиасы и добавлены следующие 2 атрибуты :
- extract-to-path – куда будет помещен файл при распаковке. Если не указывать, файл будет распакован в ту директорию, откуда стартовало приложение.
- overwrite-on-extracting – режим перезаписи. Может принимать значения Always, CheckExists, CheckSize, Never.
- Определение результирующей сборки.
Тут, в принципе, тоже все достаточно просто и интуитивно понятно.
Вы отмечаете те сборки и файлы, которые должны быть включены, задаете имя сборки, иконку, дополнительные опции компилятора. Единственный неочевидный атрибут – 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-ресурсы. Можно просто добавить их как файлы, чтобы при загрузке они распаковались рядом с приложением.
Конечно, сначала стоит попробовать включить их стандартно – как сборки, возможно, проблемы не возникнет – если разработчики этой библиотеки использовали относительные пути относительно сборки или обращений к ресурсам нет вообще.
0