Пишемо файл проєкту MSBuild з нуля
Привіт! Мене звати Андрій, і я Lead Software Engineer у компанії SoftServe. Переважно я розробляю на платформі .NET і мові C#. Під час роботи іноді виникає потреба аналізувати та оптимізувати процес збірки проєктів і в багатьох розробників виникають труднощі з виконанням таких задач через недостатнє розуміння цієї теми.
Я вирішив написати цю статтю, щоб познайомити людей з примітивами MSBuild, дати їм можливість поглибити знання в цій сфері, щоб бути готовими до виконання будь-яких задач з процесом збірки застосунків.
Ця стаття буде найбільш корисною розробникам, які вже мають досвід роботи з C#-проєктами та хочуть поглибити свої знання системи MSBuild. Вважається, що читач вже знає як виконувати базові операції з простими C#-проєктами за допомогою графічного інтерфейсу будь-якого інтегрованого середовища розробки або текстового редактора.
Перш ніж почати розглядати структуру та синтаксис файлів проєкту MSBuild, треба розглянути більш загальну картину, щоб зрозуміти навіщо потрібен MSBuild і які функції він виконує.
MSBuild та Roslyn
MSBuild та Roslyn є компонентами в екосистемі .NET, що взаємно доповнюють один одного, проте виконують різні задачі.
Система MSBuild автоматизує процес збірки, керуючи перетворенням вихідного коду на виконувані файли шляхом управління залежностями, порядком та конфігурацією збірки. Компілятор Roslyn же виконує саме компіляцію та аналіз коду для мови C# (і Visual Basic) з урахуванням параметрів, які йому передає MSBuild.
Чи можна використовувати лише MSBuild, без Roslyn? Ні, адже MSBuild спирається на Roslyn для компіляції коду, сам він не є компілятором і не може виконувати функції Roslyn.
Чи можна використовувати лише Roslyn, без MSBuild? Теоретично, можна. В такому випадку розробнику буде необхідно самостійно передавати всі файли з вихідним кодом, залежності та параметри компіляції в Roslyn.
Як ви побачите далі, на практиці це зробити буде дуже важко, незручно, і буде легко припуститися помилки. MSBuild виконує багато операцій замість розробника, має багато налаштувань за замовчуванням і суттєво спрощує процес збірки проєкту.
Що таке файл проєкту MSBuild
Файл проєкту MSBuild — це набір інструкцій для MSBuild, які вказують, які кроки треба виконати, щоб зібрати проєкт, і які параметри треба для цього використовувати. Для запису використовується мова XML.
Типовий файл проєкту
Розглянемо простий файл проєкту, в якому визначено конфігурацію для побудови програми на основі .NET SDK. Він містить один основний блок властивостей, де вказано цільову платформу як .NET 6.0, включено налаштування, за якого всі попередження розглядаються як помилки, активовано обробку Nullable-типів, а також автоматично встановлено копірайт з поточним роком.
Також присутні дві групи елементів: перша містить посилання на NuGet-пакети, що використовуються в цьому проєкті, друга — посилання на інший проєкт, що знаходиться у сусідній директорії. Переважна більшість C#-проєктів виглядає саме таким чином.
<Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> <TargetFramework>net6.0</TargetFramework> <Copyright>Company $([System.DateTime]::UtcNow.Year)</Copyright> <TreatWarningsAsErrors>true</TreatWarningsAsErrors> <Nullable>enable</Nullable> </PropertyGroup> <ItemGroup> <PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.10.8" /> <PackageReference Include="Microsoft.AspNetCore.HeaderPropagation" Version="6.0.15" /> <PackageReference Include="Microsoft.Extensions.Http.Polly" Version="6.0.20" /> <PackageReference Include="Serilog.Sinks.Seq" Version="5.2.3" /> <PackageReference Include="Swashbuckle.AspNetCore" Version="6.5.0" /> </ItemGroup> <ItemGroup> <ProjectReference Include="..\Service1.Contracts\Service1.Contracts.csproj" /> </ItemGroup> </Project>
Файл проєкту за замовчуванням
Розгляньмо, який файл проєкту створює .NET SDK за замовчуванням при створенні нового проєкту.
<Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> <OutputType>Exe</OutputType> <TargetFramework>net7.0</TargetFramework> <ImplicitUsings>enable</ImplicitUsings> <Nullable>enable</Nullable> </PropertyGroup> </Project>
Насправді це ще не найменший файл, адже налаштування ImplicitUsings
та Nullable
не є обов’язковими та мають значення за замовчуванням.
<Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> <OutputType>Exe</OutputType> <TargetFramework>net7.0</TargetFramework> </PropertyGroup> </Project>
Цей файл не містить майже нічого, але працює завдяки Sdk="Microsoft.NET.Sdk"
. Атрибут Sdk
каже MSBuild знайти інструкції для збірки за межами нашого проєкту. Вони можуть бути встановлені разом з .NET SDK, MSBuild, або можуть поширюватись незалежно як NuGet-пакети. Завдяки цьому механізму нам не доводиться кожного разу описувати всі кроки для збірки в файлах проєкту, а лише додавати деякі налаштування.
Пишемо самі
Хоча ми і отримали дуже простий файл проєкту, але, на жаль, не дізнались нічого про те, як він працює через використання вбудованого SDK, який ховає від розробників всю складність системи збірки. Тож в дидактичних цілях я вирішив написати файл проєкту без використання вбудованого SDK.
Файл проєкту потрібен, щоб щось збирати. Тож я буду збирати ось такий простий C#-застосунок, який використовує оператори верхнього рівня:
using System; Console.WriteLine("Привіт, Світе!");
Створюємо заготовку
Почнемо наш проєкт з нуля. Створимо порожній файл myapp.proj
. Хоча деякі інтегровані середовища розробки підтримують лише файли з розширенням csproj
та іноді деякими іншими (fsproj, vbproj, sqlproj, vcxproj, та інші), для MSBuild розширення файлу не є істотним. З урахуванням цього, я назвав цей поки що порожній файл проєкту myapp.proj
.
Спробувавши зібрати цей проєкт за допомогою команди dotnet build pyapp.proj
в оболонці bash, я отримав наступну помилку:
error MSB4025: The project file could not be loaded. Root element is missing.
З цього повідомлення і загальнодоступної документації Microsoft можна зробити висновок, що кожен проєкт має містити кореневий Project
.
<Project> </Project>
Знову спробуємо зібрати проєкт.
error MSB4040: There is no target in the project.
В нашому проєкті відсутній таргет, але що це таке? Про таргет MSBuild можна думати як про функцію в інших мовах програмування. Це іменований набір команд, який можна викликати з інших частин проєкту MSBuild. Додамо до нашого проєкту таргет Build
, який в майбутньому буде збирати наш проєкт.
<Project> <Target Name="Build"> </Target> </Project>
На відміну від C#, де метод Main
є точкою входу програми, в проєкті за замовчуванням виконання починається з першого за порядком таргету незалежно від його імені, але можна викликати будь-який таргет таким чином: dotnet build myapp.proj -t:targetname
. Ім’я Build
нашого таргету не має ніякого значення для MSBuild, воно лише потрібно нам для розуміння того, що цей таргет робить.
Хоча наш проєкт ще не робить нічого корисного, тепер він хоча б інтерпретується MSBuild без помилок.
MSBuild version 17.9.4+90725d08d for .NET Build succeeded. 0 Warning(s) 0 Error(s) Time Elapsed 00:00:00.21
Додаємо компіляцію
Мови загального призначення, як-от C#, призначені для широкого спектра програмних завдань. Вони надають різноманітні можливості, включаючи різні типи даних та велику екосистему бібліотек для різних потреб і дозволяють розробникам будувати додатки у різних доменах.
На відміну від них, спеціалізовані мови призначені для конкретних завдань або предметних сфер. Проєкти (сценарії) MSBuild, є прикладом спеціалізованої мови. Вона спеціально розроблена для автоматизації процесу збірки програмних проєктів і містить вбудовані команди для цього.
Команди MSBuild називаються тасками. Це атомарні операції, які виконують певні конкретні дії. Для виклику компілятора C# використаємо таск Csc
і передамо йому наш файл з C#-кодом.
<Project> <Target Name="Build"> <Csc Sources="Program.cs" /> </Target> </Project>
Program.cs(1,7): error CS0246: The type or namespace name 'System' could not be found (are you missing a using directive or an assembly reference?) Program.cs(3,7): error CS0518: Predefined type 'System.Object' is not defined or imported Program.cs(5,12): error CS0518: Predefined type 'System.Void' is not defined or imported
Отримуємо помилку і згадуємо, що в проєкті ми імпортували простір імен System
і використовували типи System.Object
та System.Void
(так, це справжній тип learn.microsoft.com/...us/dotnet/api/system.void). Нам потрібно додати посилання на бібліотеки, що містять ці типи. Для цього використаємо атрибут References
.
<Project> <Target Name="Build" > <Csc Sources="Program.cs" References="C:/Program Files/dotnet/packs/Microsoft.NETCore.App.Ref/7.0.14/ref/net7.0/System.Runtime.dll; C:/Program Files/dotnet/packs/Microsoft.NETCore.App.Ref/7.0.14/ref/net7.0/System.Console.dll" /> </Target> </Project>
Рефакторимо
Хоча ця мова є спеціалізованою, але вона дозволяє оголошувати скалярні та векторні змінні. Список залежностей нашого застосунку є гарним кандидатом для винесення в змінну типу списку. Це робиться наступним чином:
<Project> <ItemGroup> <Reference Include="C:/Program Files/dotnet/packs/Microsoft.NETCore.App.Ref/7.0.14/ref/net7.0/System.Runtime.dll" /> <Reference Include="C:/Program Files/dotnet/packs/Microsoft.NETCore.App.Ref/7.0.14/ref/net7.0/System.Console.dll" /> </ItemGroup> <Target Name="Build" > <Csc Sources="Program.cs" References="@(Reference)" /> </Target> </Project>
До елементу ItemGroup
додаємо декілька дочірніх елементів з однією назвою (в нашому випадку Reference
). Значення атрибутів Include
цих елементів будуть зібрані у список, який ми можемо отримати за допомогою синтаксису @(Reference)
. Reference
в цьому контексті — це просто назва змінної, вона може бути будь-якою.
Можна думати про Include як про метод, що додає ще один або більше елементів до списку Reference
. Також підтримуються шаблони заміщення як *. Скомпілюємо всі файли з розширенням *.cs в поточній директорії.
<Project> <ItemGroup> <Compile Include="*.cs" /> <Reference Include="C:/Program Files/dotnet/packs/Microsoft.NETCore.App.Ref/7.0.14/ref/net7.0/System.Runtime.dll" /> <Reference Include="C:/Program Files/dotnet/packs/Microsoft.NETCore.App.Ref/7.0.14/ref/net7.0/System.Console.dll" /> </ItemGroup> <Target Name="Build" > <Csc Sources="@(Compile)" References="@(Reference)" /> </Target> </Project>
Даємо назву нашому застосунку
ТаскCsc
має багато параметрів. Наприклад, можна вказати назву застосунку, який буде скомпільовано.
<Project> <ItemGroup> <Compile Include="*.cs" /> <Reference Include="C:/Program Files/dotnet/packs/Microsoft.NETCore.App.Ref/7.0.14/ref/net7.0/System.Runtime.dll" /> <Reference Include="C:/Program Files/dotnet/packs/Microsoft.NETCore.App.Ref/7.0.14/ref/net7.0/System.Console.dll" /> </ItemGroup> <Target Name="Build" > <Csc Sources="@(Compile)" References="@(Reference)" OutputAssembly="bin/MyApp.dll" /> </Target> </Project>
На жаль, при спробі зібрати цей проєкт ми отримаємо помилку через відсутність директорії bin
.
CSC : error CS2012: Cannot open 'C:\Users\...\bin\MyApp.dll' for writing -- 'Could not find a part of the path 'C:\Users\...\bin\MyApp.dll'.'
Для створення цієї директорії треба використати таск MakeDir
.
<Project> <ItemGroup> <Compile Include="*.cs" /> <Reference Include="C:/Program Files/dotnet/packs/Microsoft.NETCore.App.Ref/7.0.14/ref/net7.0/System.Runtime.dll" /> <Reference Include="C:/Program Files/dotnet/packs/Microsoft.NETCore.App.Ref/7.0.14/ref/net7.0/System.Console.dll" /> </ItemGroup> <Target Name="Build" > <MakeDir Directories="bin/" /> <Csc Sources="@(Compile)" References="@(Reference)" OutputAssembly="bin/MyApp.dll" /> </Target> </Project>
Знову рефакторимо
В нас з’явилось дублювання назви директорії в декількох місцях, використаємо змінну, щоб його прибрати. На відміну від векторних змінних, скалярні оголошуються в елементі PropertyGroup
таким чином:
<Project> <PropertyGroup> <AssemblyName>MyApp</AssemblyName> <OutputPath>bin/</OutputPath> </PropertyGroup> <ItemGroup> <Compile Include="*.cs" /> <Reference Include="C:/Program Files/dotnet/packs/Microsoft.NETCore.App.Ref/7.0.14/ref/net7.0/System.Runtime.dll" /> <Reference Include="C:/Program Files/dotnet/packs/Microsoft.NETCore.App.Ref/7.0.14/ref/net7.0/System.Console.dll" /> </ItemGroup> <Target Name="Build" > <MakeDir Directories="$(OutputPath)" /> <Csc Sources="@(Compile)" References="@(Reference)" OutputAssembly="$(OutputPath)$(AssemblyName).dll" /> </Target> </Project>
Якщо декілька разів оголосити скалярну змінну, то вона прийме останнє присвоєне їй значення, адже за визначенням вона може мати лише одне значення. Також її значення можна отримати за допомогою іншого синтаксису $(AssemblyName)
.
Висновок
В цій статті ми розглянули такі елементи файлу проєкту MSBuild як атрибут Sdk
, таргети, таски, скалярні і векторні змінні. За допомогою цих примітивів ми змогли написати простий файл проєкту, який зібрав наш С#-застосунок.
MSBuild також підтримує просунуті сценарії, як-от інкрементальна збірка та написання користувацьких SDK для роботи з іншими типами проєктів (як от MSBuild.Sdk.SqlProj
для .sqlproj-проєктів).
Тепер ви готові детально розбиратись з іншими SDK, адже вони так само є набором примітивів MSBuild, які були розглянуті в цій статті.
19 коментарів
Додати коментар Підписатись на коментаріВідписатись від коментарів