Послепрочтениястатьи"КакготовитьCake,используятолькоFrosting"мнепришлавголову мысль:"Какойбольшойпроектдлятакогопростогопроцессасборки".Послеэтогояирешилнаписатьмини-статьюпроаналогCakeNuke.
Если я правильно понимаю историю проекта, Nuke появился как более простой и удобный аналог Cake, Psake и Fake. Автору Nuke хотелось писать скрипты сборки именно на C#, поэтому ему не подошли Psake и Fake. Судя по всему, когда создавался Nuke, не существовал Frosting, и все Cake скрипты были странными файлами с DSL на основе C#, которые можно было писать и отлаживать только в VS Code. Скрипт сборки Nuke изначально задумывался как обычное консольное приложение, которое легко можно писать и отлаживать в любой IDE.
ДавайтесоздадимпростейшийскриптсборкинаNukeисоберёмсегопомощьюприложение!
1. Создаёмприложение
Создадим новое приложение в папке src репозитория. Это не обязательно, но далее станет понятно, почему удобнее использовать для этого отдельную папку.
2. Устанавливаем Global Tool
dotnet tool install Nuke.GlobalTool --global
3. Создаём проект со скриптом сборки
Дляэтогонеобходимовыполнитькоманду:
nuke :setup
Это запустит консольный конфигуратор проекта сборки. Привожу анимацию его работы из документации Nuke.
Давайте посмотрим, что изменилось в репозитории:
Если не трогать настройки по умолчанию, то скрипт установки nuke
создаст проект сборки в отдельной папке build
репозитория. Таким образом, скрипты сборки лежат в
build
, а исходники в src
. Те же настройки
по умолчанию предполагают использование папки output
в
корне репозитория в качестве выходной директории для сборки.
Разберём изменения по отдельности:
-
.nuke файл-маркер, по которому nuke определяет корневую папку
-
build.* бутстрапперы для запуска сборки. При необходимости могут и .net установить, если его нет на машине, на которой происходит сборка
-
_build.csproj файл проекта сборки. Имеет такое странное название для того, чтобы всегда быть первым в списке проектов в Solution Explorer
-
.editorconfig и _build.csproj.DotSettings просто настройки стиля
А теперь самое интересное:
3.1. Изменения в sln
Проект _build был добавлен в решение. Но был добавлен очень хитрым способом. Он отображается в IDE, его можно собрать, но если выполнить сборку всего решения, то мы увидим следующий вывод:
1>------ Rebuild All started: Project: Demo, Configuration: Debug Any CPU ------2>------ Skipped Rebuild All: Project: _build, Configuration: Debug Any CPU ------2>Project not selected to build for this solution configuration 1>Demo -> C:\Users\buldo\source\repos\nuke-example\src\Demo\Demo\bin\Debug\netcoreapp3.1\Demo.dll========== Rebuild All: 1 succeeded, 0 failed, 1 skipped ==========
То есть при пересборке решения проект _build не собирается. Действительно, если бы скрипт сборки при работе вызывал ещё и пересборку самого себя, это могло бы привести к проблемам.
3.2. Build.cs
Приведу полный код сгенерированного файла и объясню основные моменты.
[CheckBuildProjectConfigurations][ShutdownDotNetAfterServerBuild]class Build : NukeBuild{ public static int Main () => Execute<Build>(x => x.Compile); [Parameter("Configuration to build - Default is 'Debug' (local) or 'Release' (server)")] readonly Configuration Configuration = IsLocalBuild ? Configuration.Debug : Configuration.Release; [Solution] readonly Solution Solution; [GitRepository] readonly GitRepository GitRepository; [GitVersion] readonly GitVersion GitVersion; AbsolutePath SourceDirectory => RootDirectory / "src"; AbsolutePath OutputDirectory => RootDirectory / "output"; Target Clean => _ => _ .Before(Restore) .Executes(() => { SourceDirectory.GlobDirectories("**/bin", "**/obj").ForEach(DeleteDirectory); EnsureCleanDirectory(OutputDirectory); }); Target Restore => _ => _ .Executes(() => { DotNetRestore(s => s .SetProjectFile(Solution)); }); Target Compile => _ => _ .DependsOn(Restore) .Executes(() => { DotNetBuild(s => s .SetProjectFile(Solution) .SetConfiguration(Configuration) .SetAssemblyVersion(GitVersion.AssemblySemVer) .SetFileVersion(GitVersion.AssemblySemFileVer) .SetInformationalVersion(GitVersion.InformationalVersion) .EnableNoRestore()); });}
Данный класс является точкой входа в приложения. В методе
Main
запускается сборка и выбирается цель сборки по
умолчанию.
Цели сборки представлены свойствами типа Target
.
Синтаксис с кучей стрелочек сначала пугает, но потом привыкаешь.
Для целей сборки можно задавать зависимости
(DependsOn()
), явный порядок выполнения
(Before()
, After()
), условия выполнения
(как статические, так и динамические), а главное указывать с
помощью Executes()
, что вообще делает этот
Target
.
Следующая важная вещь для начинающего пользователя Nuke
параметры сборки. Достаточно просто пометить поле атрибутом
[Parameter]
, и можно передавать значение этого поля
через командную строку. В данном случае конфигурацию сборки Debug
или Release. Механизм очень гибок можно сделать параметр
необходимым, и без него сборка не запустится. Также параметр можно
делать необходимым только для конкретного Target
.
Nuke предоставляет необходимые абстракции над решениями и
проектами. Несколько решений в проекте? Нет проблем. Добавьте новое
свойство типа Solution и пометьте его атрибутом
[Solution]
Nuke найдёт решение, распарсит его и
предоставит доступ его содержимому.
Также в этом файле можно заметить пример расширения Nuke модуль
для работы с git установлен отдельным пакетом. С помощью полей
GitRepository
и GitVersion
можно легко
оперировать состоянием репозитория. Например, ориентируясь на имя
ветки (master/не master), выбрать ставить или нет preview метку при
сборке nuget пакета.
4. Запуск сборки
Сборка запускается с помощью бутстраппера:
.\build.ps1 [targets] [arguments]
В данном случае для того, чтобы собрать приложение, необходимо выполнить следующую команду:
.\build.ps1 --target Compile --configuration release
На выходе получаем лог сборки, завершающийся удобной статистикой:
TargetStatusDurationRestoreExecuted0:01CompileExecuted0:03Total0:04Build succeeded on 29.12.2020 21:38:10.
На самом деле, если установлен NET 5, сборка скорее всего
завершится ошибкой. Волшебный фикс заменить атрибут поля
GitVersion
на [GitVersion(Framework =
"netcoreapp3.1")]
.
5. Делаем что-то полезное
По самому первому скриншоту статьи можно было заметить, что был создан проект на Avalonia. Так давайте соберём наше приложение, да так, чтобы оно было в виде одного исполняемого файла, включающего рантайм. Это значит, что придётся собирать несколько исполняемых файлов.
Далее я приведу код цели сборки с подробными комментариями:
Target Publish => _ => _ .Executes(() => { var rids = new[] {"win-x64", "linux-x64"}; // Перечисляем RID'ы, для которых собираем приложение DotNetPublish(s => s // Теперь вызываем dotnet publish .SetAssemblyVersion(GitVersion.AssemblySemVer) .SetFileVersion(GitVersion.AssemblySemFileVer) .SetInformationalVersion(GitVersion.InformationalVersion) .SetProject(Solution.GetProject("Demo")) // Для dotnet publish желательно указывать проект .SetPublishSingleFile(true) // Собираем в один файл .SetSelfContained(true) // Вместе с рантаймом .SetConfiguration(Configuration) // Для определённой конфигурации .CombineWith(rids, (s, rid) => s // Но нам нужны разные комбинации параметров .SetRuntime(rid) // Устанавливаем RID .SetOutput(OutputDirectory/rid))); // Делаем так, чтобы сборки с разными RID попали в разные папки });
Заключение
Nuke удобная система сборки. Она проста и минималистична в начале вашего проекта, но достаточно мощна для того, чтобы развиваться вместе с ним.