Кроссплатформенные ресурсы в сборках .NET — пишем условия MSBuild
При разработке приложений на платформе .NET почти всегда возникает необходимость включить в сборку сторонние ресурсы. Среди них могут быть данные любого типа, от исполняемых файлов до изображений и файлов CSS. Также часто бывает необходимо использовать разные ресурсы для разных целевых платформ. Рассмотрим два примера настройки MSBuild с разными ресурсами для каждой из выбранных операционных систем, Windows и Linux в нашем случае (конкретные версии ОС, их дистрибутивы или разрядность в рамках статьи большого значения не имеют).
Начало
Создадим новый проект CrossPlatform (в рамках статьи будет использоваться Avalonia UI, у вас может быть любого другого типа), добавим в блок PropertyGroup
файла CrossPlatform.csproj
следующую строку:
win-x64; linux-x64
Этим действием мы указываем MSBuild, что приложение планируется к запуску на 64 разрядных Windows и Linux.
Случай 1: Исполняемое приложение
Допустим, что нам необходимо при нажатии на кнопку запускать исполняемый файл для выполнения каких-то важных операций.
Пример из реальной жизни
Это может быть приложение, которое вызывает pandoc
для преобразования документа, составленного пользователем, в формат pdf
В качестве примера будет использоваться приложение run
на C++ которое выводит текст на экран и завершается через 5 секунд, скомпилированное для платформ Windows и Linux.
Код этого приложения
#include
#include
#include
int main()
{
std::cout << "Some cool application running..." << std::endl;
std::this_thread::sleep_for(std::chrono::milliseconds(5000));
}
Ресурсы
Скопируем файлы run
(для Linux) и run.exe
(для Windows) в корень проекта. В файл CrossPlatform.csproj
после секции PropertyGroup
добавим следующий код:
PreserveNewest
PreserveNewest
Элемент
ItemGroup
определяет группу параметров, к которым применяется условие в атрибутеCondition
Атрибут
Condition
определяет условие, при котором содержимоеItemGroup
будет включено в сборку — если свойство RuntimeIdentifier начинается на 'linux' или 'windows' (подробнее по ссылке или в спойлере в конце статьи)Элемент
Content
определяет для ресурса действие при сборке — скопировать файл в выходную папку, аргументInclude
указывает на путь к нужному файлу относительно корня проекта (подробнее по ссылке)
При сборке, файл run.exe
будет скопирован в выходную папку, если проект собирается для операционной системы Windows любой разрядности, аналогично, run
будет скопирован если сборка происходит для Linux
UI
В файле MainWindow.xaml
добавим Grid и кнопку в нем, при нажатии на которую будет выполняться команда для запуска файла run
:
Опубликуем проект командами:
Пояснения к командам
Аргумент --runtime используется для указания целевой платформы, для которой будет публиковаться приложение.
Аргумент -p: PublishSingleFile=true нужен для того, чтобы приложение было упаковано в единый исполняемый файл (тут используется только для того, чтобы было меньше файлов — было наглядно видно, какие ресурсы попали в папку публикации)
Результат сборки (темный фон — Windows, светлый — Linux)
Видно, что в результате в папку для каждой из платформ попал только нужный файл run, а не оба одновременно. Запустим приложение, убедимся в работоспособности:
Приложение для двух платформ (темный фон — Windows, светлый — Linux)
При нажатии на кнопку на обоих окнах, в Windows открывается окно с сообщением Some cool application running...
, в Linux это сообщение отображается в консоли. Приложение работоспособно на обоих целевых платформах.
Случай 2: Встроенный ресурс
Допустим, что нам необходимо в окне программы показать изображение, причем свое для каждой из платформ.
Ресурсы
Подготовим 2 файла, windows.png
и linux.png
, создадим папку Images в корне проекта, поместим туда изображения.
Структура проекта
Доработаем файл CrossPlatform.csproj следующим образом:
Images.Banner.png
PreserveNewest
Images.Banner.png
PreserveNewest
Элемент
EmbeddedResource
определяет файл, который будет встроен в сборку (CrossPlatform.exe в нашем случае) при сборке, атрибутInclude
содержит ссылку на файл от корня проекта.Элемент
LogicalName
определяет имя, под которым можно будет из кода получить содержимое файла.
UI
Изменим Grid в файле MainWindow.xaml
следующим образом:
Мы добавили элемент Image
, атрибут Source
которого содержит ссылку на наш встроенный ресурс вида resm:Images.Banner.png
(подробнее о том, как подключить ресурсы в Avalonia UI по ссылке).
Опубликуем приложение командами из пункта 1 и посмотрим на результат:
Одно приложение на 2 платформах (темный фон — Windows, светлый — Linux)
Приложение успешно запускается, на разных платформах показывается разное изображение, код приложения одинаковый для обеих ОС. Можно убедиться в том, что в сборку встроилась только одна картинка, открыв файл CrossPlatform.exe в dotPeek
:
Ресурсы сборки
Что если не использовать условия MSBuild
Можно заставить окно отображать нужную картинку и другим способом:
//Подгрузить все ресурсы в сборку
Images.Banner.Linux.png
Images.Banner.Windows.png
//Сделать класс - контекст биндинга
public class DataProvider
{
private static string ImageResourceName => RuntimeInformation.IsOSPlatform(OSPlatform.Linux)
? "Images.Banner.Linux.png"
: "Images.Banner.Windows.png";
public static IImage Image =>
new Bitmap(Assembly.GetCallingAssembly().GetManifestResourceStream(ImageResourceName));
}
//Указать контекст в окне
//Забиндить Source изображения на свойство Image класса DataProvider
Данный подход обладает рядом недостатков:
Итоговый размер приложения больше чем мог бы быть, из-за неиспользуемого ресурсов
Код становится сложнее писать и поддерживать
Заключение
Использование условий сборщика MSBuild позволяет гибко управлять тем, что, как и в при каких обстоятельствах попадет в сборку вашего .NET приложения. Использование данных возможностей MSBuild поможет упростить некоторую часть кросс-платформенного кода, а также уменьшить размер выходных файлов.
Исходники проекта можно найти по ссылке на GitHub
Что еще можно использовать в условиях MSBuild
В условиях MSBuild можно использовать значения множества зарезервированных свойств (раз, два) или придумать собственные. Некоторые из стандартных свойств:
RuntimeIdentifier (s) — целевая (ые) платформа (ы) для текущей сборки
SelfContained — является ли приложение автономным (упакован ли runtime вместе с кодом приложения)
Configuration (обычно Debug или Release) — конфигурация сборки
ImplicitUsings — включать ли ссылки на сборки по умолчанию
Некоторые доступные операции
Подробнее тут
Сравнения (==, !=, <, >, <=, >=)
Exists ('filename') — существует ли указанный файл/папка
Логические (!, And, Or)
Доступные функции
Можно вызывать методы из этих типов BCL, например:
//Пример из статьи
Condition="$(RuntimeIdentifier.StartsWith('linux'))" // String.StartsWith
Condition="$(SomeCustomProperty.Trim('linux') == 'SomeCustomValue')" // String.Trim
//Регулярные выражения
Condition="$([System.Text.RegularExpressions.Regex]::IsMatch(
$(DefineConstants), '^(.*;)*SOME_CONTANT_NAME(;.*)*$'))"