Кроссплатформенные ресурсы в сборках .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)

Результат сборки (темный фон — Windows, светлый — Linux)

Видно, что в результате в папку для каждой из платформ попал только нужный файл run, а не оба одновременно. Запустим приложение, убедимся в работоспособности:

Приложение для двух платформ (темный фон - Windows, светлый - Linux)

Приложение для двух платформ (темный фон — 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)

Одно приложение на 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(;.*)*$'))"

© Habrahabr.ru