При разработке приложений на платформе .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 нужен для того, чтобы приложение было упаковано в единый исполняемый файл (тут используется только для того, чтобы было меньше файлов - было наглядно видно, какие ресурсы попали в папку публикации)

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

При нажатии на кнопку на обоих окнах, в 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 и посмотрим на результат:

Приложение успешно запускается, на разных платформах показывается разное изображение, код приложения одинаковый для обеих ОС. Можно убедиться в том, что в сборку встроилась только одна картинка, открыв файл 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(;.*)*$'))"