Большинство инструментов, которые Microsoft делают для dotnet можно назвать “It’s just work”. Но иногда стандартной конфигурации недостаточно и приходиться более подробно разбираться в проблеме. Этот пост как раз о том, как стандартного процесса сборки солюшена оказалось недостаточно и пришлось его пересоздать. И заодно узнать много нового про билд процесс.
- Dotnet solution structure
- MSBuild operations
- Shared output directory
Структура солюшена
Перед тем, как погрузиться в особенности работы билд процесса нужно понять что является входными аргументами этого процесса.
Если очень сильно обобщить и упроситить, то исходный код C# приложения имеет структуру:
SolutionDirectory/
ProjectName/
ProjectName.csproj
Solution.sln
Directory.Build.props
Directory.Build.targets
Структура сформировалась вокруг необходимости группировать и разделять исходный код. С одной стороны, приложение - это много код, который удобно было бы держать в разных файлах. А процесс сборки соответственно требует сборки всех файлов вместе. Для реализации такого объединения существуют проекты.
С другой стороны, код нужно разделять на компоненты и блоки, а значит хочется иметь несколько проектов. Но всё ещё остаётся необходимость собирать все эти проекты вместе. Для объединения проектов существуют солюшены, .sln
файлы.
Корень структуры - это солюшен, .sln
файл, который содержит ссылки на добавленные в него проекты (.csproj
файлы). Они описываются так:
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") =
"ProjectName",
"ProjectName\ProjectName.csproj",
"{E8F01267-DA18-42DC-9859-423209B99F3B}"
EndProject
где:
- “{9A19103F-16F7-4668-BE54-9A1E7A4F7556}” - это тип проекта
- ”ProjectName” - название проекта, как он отображается при открытии IDE
- ”ProjectName\ProjectName.csproj” - путь к файлу .csproj относительно .sln
- ”{E8F01267-DA18-42DC-9859-423209B99F3B}” - идентификатор проекта, сгенерированный GUID
Солюшены поддерживают возможность создавать директории, которые используются для структурирования проектов в иерархию. Структура директорий в солюшене не привязывается к расположении проектов на файловой системе.
Например, солюшен состоит из 8 проектов, из которых 3 - это тесты. Для них можно создать директорию Tests и иметь такую структуру:
Solutions
Project1
Project2
...
Tests/
TestProject6
TestProject7
TestProject8
Информация о созданных директориях хранится в sln файл. Для каждой директории создаётся запись аналогичная проекту:
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") =
"Tests",
"Tests",
"{14195214-591A-45B7-851A-19D3BA2413F9}"
EndProject
А для каждого проекта, который создаётся в директории, создаётся запись в NestedProjects:
GlobalSection(NestedProjects) = preSolution
{0031728E-A5D4-47C1-9C1A-6C859A765C9D} = {14195214-591A-45B7-851A-19D3BA2413F9}
Конфигурация солюшена
Ещё одна секция в sln файле - это описание конфигураций и платформ солюшена:
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
Подробно о конфигурациях описано в документации Microsoft - https://learn.microsoft.com/en-us/visualstudio/ide/understanding-build-configurations. Выбор конфигурации солюшена влияет на то, какая конфигурация будет выбрана для проектов. Эта информация указывается в sln файле:
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{AF1E6F8A-5C63-465F-96F4-5E5F183A33B9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{AF1E6F8A-5C63-465F-96F4-5E5F183A33B9}.Debug|Any CPU.Build.0 = Debug|Any CPU
Читать эту конфигурация нужно как:
Для проекта “{AF1E6F8A-5C63-465F-96F4-5E5F183A33B9}” при выбранной для солюшена конфигурации “Debug|Any CPU” нужно использовать конфигурацию и платформу “Debug|Any CPU”
Вторая строка указывает на необходимость собирать проект. Если её убрать, то при сборке солюшена в “Debug|Any CPU” данный проект не будет собираться совсем. Пример использования: убрать тестовые проекты из процесса сборки при выбранной Release конфигурации.
Структура проекта
[[Dotnet project|.csproj
файлы]] имеют более сложную судьбу. Существует два формата их описания. Первый формат был создан для .NET Framework. Этот формат можно опознать по объявлению Project ноды:
<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
Этот формат считается устаревшим. И вместе с .NET Core был добавлен новый формат, который называют SDK-style:
<Project Sdk="Microsoft.NET.Sdk">
Новый формат позиционировался как замена, поэтому все они имел схожий набор возможностей. Но поведение у них отличалось. Например, в старом формате записи требуется явно указывать все файлы, которые нужно скомпилировать. А вместе с выходом SDK-style появилось и стало использоваться по умолчанию свойство EnableDefaultItems, которое автоматически добавляло все *.cs файлы в проект и в процесс компиляции.
Одним из основных элементов .csproj
файла являются Properties. Properties - это пары ключ-значение. В csproj файлы свойства указываются внутри ноды PropertyGroup:
Пример такой пары - указание версии dotnet’а <TargetFramework>netstandard2.0</TargetFramework>
. В качестве ключа могут выступать не только стандартные имена свойств, которые заданы Microsoft’ом, но и любые пользовательские данные, которые можно использовать в качестве “переменных для MSBuild’а”.
При описании свойств можно ссылаться на другие свойства используя специальный синтаксис. Например, OutputPath позволяет указывать путь, куда будут складываться результаты сборки. По умолчанию это /bin/debug/net8/...
. При этом использование debug определяется выбранной конфигурацией и для Release будет использоваться Release. Записать это в csproj можно так:
Property можно дополнять условиями. Например, есть GeneratePackageOnBuild, которое указывает на то, нужно ли генерировать .nupkg файл при сборке проекта. Чтобы включить генерацию, нужно прописать <GeneratePackageOnBuild>True</GeneratePackageOnBuild>
. Но может появится запрос на то, чтобы генерировать файл только при сборке в Release конфигурации. Добиться такого можно таким изменением:
Ещё одним важным элементом являются Item’ы. Item’ы - это свойства, которые являются списком элементов, которые передаются билд системе для сборки. Обычно, это одно из:
- Список
.cs
файлов, которые нужно скомпилировать (<Compile Include = "Program.cs"/>
) - Список нюгет пакетов, которые подключены в проект (
<PackageReference Include="System.Text.Json" />
) - Список ссылок между проектами (
<ProjectReference Include="..\OtherProject\OtherProject.csproj" />
)
Запись вида <PackageReference Include=...>
можно интерпретировать как “Добавить в список нюгет пакетов ещё одно значение”. Item’ы указываются под ItemGroup нодой: