Pull to refresh

Кроссплатформенные ресурсы в сборках .NET — пишем условия MSBuild

Level of difficultyEasy
Reading time6 min
Views2.7K

При разработке приложений на платформе .NET почти всегда возникает необходимость включить в сборку сторонние ресурсы. Среди них могут быть данные любого типа, от исполняемых файлов до изображений и файлов CSS. Также часто бывает необходимо использовать разные ресурсы для разных целевых платформ. Рассмотрим два примера настройки MSBuild с разными ресурсами для каждой из выбранных операционных систем, Windows и Linux в нашем случае (конкретные версии ОС, их дистрибутивы или разрядность в рамках статьи большого значения не имеют).

Начало

Создадим новый проект CrossPlatform (в рамках статьи будет использоваться Avalonia UI, у вас может быть любого другого типа), добавим в блок PropertyGroup файла CrossPlatform.csproj следующую строку:

<RuntimeIdentifiers>win-x64; linux-x64</RuntimeIdentifiers>

Этим действием мы указываем MSBuild, что приложение планируется к запуску на 64 разрядных Windows и Linux.

Случай 1: Исполняемое приложение

Допустим, что нам необходимо при нажатии на кнопку запускать исполняемый файл для выполнения каких-то важных операций.

Пример из реальной жизни

Это может быть приложение, которое вызывает pandoc для преобразования документа, составленного пользователем, в формат pdf

В качестве примера будет использоваться приложение run на C++ которое выводит текст на экран и завершается через 5 секунд, скомпилированное для платформ Windows и Linux.

Код этого приложения
#include <iostream>
#include <chrono>
#include <thread>
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 добавим следующий код:

<ItemGroup Condition="$(RuntimeIdentifier.StartsWith('linux'))">
    <Content Include="run">
        <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
    </Content>
</ItemGroup>
    
<ItemGroup Condition="$(RuntimeIdentifier.StartsWith('win'))">
    <Content Include="run.exe">
        <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
    </Content>
</ItemGroup>
  • ЭлементItemGroup определяет группу параметров, к которым применяется условие в атрибуте Condition

  • Атрибут Condition определяет условие, при котором содержимое ItemGroup будет включено в сборку - если свойство RuntimeIdentifier начинается на 'linux' или 'windows' (подробнее по ссылке или в спойлере в конце статьи)

  • ЭлементContent определяет для ресурса действие при сборке - скопировать файл в выходную папку, аргумент Include указывает на путь к нужному файлу относительно корня проекта (подробнее по ссылке)

При сборке, файл run.exe будет скопирован в выходную папку, если проект собирается для операционной системы Windows любой разрядности, аналогично, run будет скопирован если сборка происходит для Linux

UI

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

<Grid HorizontalAlignment="Center" VerticalAlignment="Center">
  <Button x:Name="Opener">Нажми</Button>
</Grid>

Опубликуем проект командами:

  • Для Windows: dotnet publish --runtime win-x64 -p:PublishSingleFile=true

  • Для Linux: dotnet publish --runtime linux-x64 -p:PublishSingleFile=true

Пояснения к командам

Аргумент --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 следующим образом:

<ItemGroup Label="LinuxResources" Condition="$(RuntimeIdentifier.StartsWith('linux'))">
  <EmbeddedResource Include="Images\linux.png">
      <LogicalName>Images.Banner.png</LogicalName>
  </EmbeddedResource>
  
  <Content Include="run">
        <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
    </Content>
</ItemGroup>

<ItemGroup Label="WindowsResources" Condition="$(RuntimeIdentifier.StartsWith('win'))">
    <EmbeddedResource Include="Images\windows.png">
        <LogicalName>Images.Banner.png</LogicalName>
    </EmbeddedResource>
  
    <Content Include="run.exe">
        <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
    </Content>
</ItemGroup>
  • Элемент EmbeddedResource определяет файл, который будет встроен в сборку (CrossPlatform.exe в нашем случае) при сборке, атрибут Include содержит ссылку на файл от корня проекта.

  • Элемент LogicalName определяет имя, под которым можно будет из кода получить содержимое файла.

UI

Изменим Grid в файле MainWindow.xaml следующим образом:

<Grid HorizontalAlignment="Center" VerticalAlignment="Center" ColumnDefinitions="2* *">
    <Image Width="500" Margin="0 0 150 0" Grid.Column="0" Source="resm:Images.Banner.png"/>
    <Button Grid.Column="1" x:Name="Opener">Нажми</Button>
</Grid>

Мы добавили элемент Image, атрибут Source которого содержит ссылку на наш встроенный ресурс вида resm:Images.Banner.png (подробнее о том, как подключить ресурсы в Avalonia UI по ссылке).

Опубликуем приложение командами из пункта 1 и посмотрим на результат:

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

Приложение успешно запускается, на разных платформах показывается разное изображение, код приложения одинаковый для обеих ОС. Можно убедиться в том, что в сборку встроилась только одна картинка, открыв файл CrossPlatform.exe в dotPeek:

Ресурсы сборки
Ресурсы сборки

Что если не использовать условия MSBuild

Можно заставить окно отображать нужную картинку и другим способом:

//Подгрузить все ресурсы в сборку
<ItemGroup>
    <EmbeddedResource Include="Images\linux.png">
        <LogicalName>Images.Banner.Linux.png</LogicalName>
    </EmbeddedResource>
    <EmbeddedResource Include="Images\windows.png">
        <LogicalName>Images.Banner.Windows.png</LogicalName>
    </EmbeddedResource>
</ItemGroup>
//Сделать класс - контекст биндинга
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));    
}
//Указать контекст в окне
<Window xmlns:nc="using:CrossPlatform" x:DataType="nc:DataProvider" ...>
<Window.DataContext>
    <nc:DataProvider></nc:DataProvider>
</Window.DataContext>

//Забиндить Source изображения на свойство Image класса DataProvider
<Image Width="500" Margin="0 0 150 0" Grid.Column="0" Source="{Binding Image}"/>

Данный подход обладает рядом недостатков:

  • Итоговый размер приложения больше чем мог бы быть, из-за неиспользуемого ресурсов

  • Код становится сложнее писать и поддерживать

Заключение

Использование условий сборщика 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(;.*)*$'))"

Tags:
Hubs:
Total votes 8: ↑8 and ↓0+8
Comments1

Articles