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

от автора

При разработке приложений на платформе .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(;.*)*$'))"


ссылка на оригинал статьи https://habr.com/ru/articles/791512/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *