Cake (C# Make) – independent build
W mojej pracy potrzebowałem narzędzia, które w łatwy sposób pozwoli na konfigurację kolejnych kroków budowania projektów i sprawdzania testów jednostkowych. Normalnie zadanie to zlecałem w 100% Bamboo, któremu przez pewien czas miałem przyjemność administrować pod kątem konfiguracji procesów CI. Problem pojawiał się w momencie, gdy cała konfiguracja leżała po stronie tego narzędzia, a deweloperzy potrzebowali zaktualizować proces o kolejne kroki budowania projektu nie mając odpowiednich uprawnień po stronie CI do zrobienia tego. W dodatku, gdy Bamboo okazuje się zbyt drogie i postanowimy popracować trochę z Jenkinsem to okazuje się, że trzeba wszystko konfigurować jeszcze raz, od nowa. Przy większych projektach jest to sporo straconego czasu na wykonanie tej samej pracy jeszcze raz. Z pomocą przyszło narzędzie o nazwie Cake. Upieczmy coś.
Cake – słowo wstępu
Jest to darmowe, cross platformowe narzędzie pozwalające na wykonanie build’a projektu, sprawdzenie testów jednostkowych, czy przygotowanie paczek pod deploy na dowolne środowisko. Ostatecznie po stronie CI całość sprowadza się do wywołania jednej komendy:
Dla systemu Windows:
1 |
./build.ps1 |
Dla systemu Linux/OS X:
1 |
./build.sh |
Proste prawda? Bootstrapery te otrzymujemy klonując przykładowe repozytorium udostępnione przez Cake lub ściągając paczkę zip. Oba pliki spełniają te same zadania. Na początku odczytywane są poszczególne parametry przekazane podczas wywołania komendy (np. konfiguracja projektu, target, czy ścieżki do odpowiednich plików). Następnie Cake sam (!) dba o pobranie plików potrzebnych mu do pracy z pomocą NuGet’a – znacie taki problem, że pierwszy build kończy się zawsze fail’em z powodu brakujących paczek? Na samym końcu odpalany jest już skrypt cake. Możemy dowolnie konfigurować argumenty z którymi chcemy wywołać nasz build.
build.cake – zbuduj ciasto
W bootstraperach zalecam jedynie edytowanie argumentów wejściowych, reszta w zasadzie nas nie interesuje. Dla mnie najważniejszym argumentem jest ścieżka do skryptu o rozszerzeniu cake. To właśnie w tym pliku konfigurujemy wszystkie procesy których potrzebujemy. W moich projektach tworzę jeden plik cake na solucję. Wywołanie bootstrapera wygląda wtedy mniej więcej tak:
1 |
./build.ps1 -Script .\MySolutionName\build.cake |
Sam plik cake piszemy w języku… C#. Narzędzie to oparte jest o kompilatory Roslyn i Mono, więc kod będzie prosty i zrozumiały dla każdego programisty tego języka. Zawartość pliku możemy podzielić na trzy części:
- Załadowanie argumentów do pamięci
- Przygotowanie zmiennych do pracy
- Przygotowanie zadań do wykonania
Załadujmy argumenty wejściowe
Na początku wspomniałem o możliwości zdefiniowania dowolnych argumentów wejściowych. To jest moment, w którym możemy ich użyć. Mogą to być ścieżki do pliku solucji lub katalogu bin, konfiguracja (Release/Debug), czy numer build’a. W pierwszej kolejności należy załadować argumenty do boostrapera (w przykładzie domyślne argumenty ze skryptu PowerShell):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
[CmdletBinding()] Param( [string]$Script = "build/build.cake", [string]$Target = "Default", [ValidateSet("Release", "Debug")] [string]$Configuration = "Release", [ValidateSet("Quiet", "Minimal", "Normal", "Verbose", "Diagnostic")] [string]$Verbosity = "Verbose", [switch]$Experimental, [Alias("DryRun","Noop")] [switch]$WhatIf, [switch]$Mono, [switch]$SkipToolPackageRestore, [Parameter(Position=0,Mandatory=$false,ValueFromRemainingArguments=$true)] [string[]]$ScriptArgs ) |
Następnie do komendy Invoke-Expression dopisujemy brakujące argumenty:
1 |
Invoke-Expression "& `"$CAKE_EXE`" `"$Script`" -target=`"$Target`" -configuration=`"$Configuration`" -verbosity=`"$Verbosity`" $UseMono $UseDryRun $UseExperimental $ScriptArgs" |
Dopiero teraz możemy je odczytać w naszym pliku cake:
1 2 3 4 5 6 7 8 |
////////////////////////////////////////////////////////////////////// // ARGUMENTS ////////////////////////////////////////////////////////////////////// var target = Argument("target", "Default"); var configuration = Argument("configuration", "Release"); var pathToBin = Argument("pathToBin", "./MyProject/bin"); var solutionFilePath = Argument("solutionFilePath", "./MyProject.sln"); |
Do odczytu służy nam polecenie Argument, któremu podajemy nazwę zmiennej (np. target) oraz wartość domyślną jaką ma przyjąć zmienna w momencie, gdy takiego argumentu nie odnajdzie. Następnie możemy przygotować zmienne lokalne potrzebne do wykonania kolejnych zadań.
1 2 3 4 5 |
////////////////////////////////////////////////////////////////////// // PREPARATION ////////////////////////////////////////////////////////////////////// var buildDir = Directory(pathToBin) + Directory(configuration); |
W tym przypadku przygotowaliśmy zmienną, która przetrzyma nam ściężkę do katalogu, w którym zostanie zbudowany nasz projekt. Po tych krokach należy przejść do zdefiniowania zadań.
Clean, restore packages, build – Taski
Do tego celu służy polecenie Task. Każde zadanie ma swoją nazwę i może zostać wykonane (lub nie) w zależności od wyniku innych zadań. W poniższym przykładzie przeprowadzę prosty build aplikacji napisanej w .NET Core:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
////////////////////////////////////////////////////////////////////// // TASKS ////////////////////////////////////////////////////////////////////// Task("Clean") .Does(() => { CleanDirectory(buildDir); }); Task("RestorePackages") .IsDependentOn("Clean") .Does(() => { DotNetCoreRestore(solutionFilePath); }); Task("Build") .IsDependentOn("RestorePackages") .Does(() => { var settings = new DotNetCoreBuildSettings { Configuration = configuration }; DotNetCoreBuild(solutionFilePath, settings); }); |
Pierwsze zadanie to clean – czyścimy katalog w którym zbudowana zostanie nasza aplikacja. Następnie po poprawnym wykonaniu się tego zadania zostaną pobrane wszystkie zależności potrzebne do działania projektu. Po tych dwóch krokach możemy wykonać build poleceniem DotNetCoreBuild. Proste i czytelne, prawda? Na samym końcu należy dodać domyślny task, który zapoczątkuje cały łańcuch zdarzeń i wywołać komendę RunTarget:
1 2 3 4 5 6 7 8 9 10 11 12 |
////////////////////////////////////////////////////////////////////// // TASK TARGETS ////////////////////////////////////////////////////////////////////// Task("Default") .IsDependentOn("Build"); ////////////////////////////////////////////////////////////////////// // EXECUTION ////////////////////////////////////////////////////////////////////// RunTarget(target); |
W efekcie po wywołaniu bootstrapera otrzymujemy logi z całego procesu i wynik końcowy:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
Build succeeded. 0 Warning(s) 0 Error(s) Time Elapsed 00:00:13.76 Finished executing task: Build ======================================== Default ======================================== Executing task: Default Finished executing task: Default Task Duration -------------------------------------------------- Clean 00:00:00.0113474 RestorePackages 00:00:06.4710878 Build 00:00:14.0890445 Default 00:00:00.0017795 -------------------------------------------------- Total: 00:00:20.5732592 |
I gotowe. Jeśli dostaniemy jakiś błąd, Cake zgłosi nam to do konsoli.
Unit Tests
Cake pozwala uruchomić testy jednostkowe. Wspiera takie frameworki jak MSTest, NUnit oraz xUnit. Wystarczy dodać kolejny krok w skrypcie, wyedytować Task o nazwie “Default” dodając mu zależność od kroku “Run-Unit-Tests” i gotowe:
1 2 3 4 5 6 |
Task("Run-Unit-Tests") .IsDependentOn("Build") .Does(() => { NUnit("path-to-your-test-dll"); }); |
Chcesz więcej? Sięgnij do dokumentacji!
To wszystko jest zbyt proste, prawda? Na szczęście Cake oferuje całe pole do popisu od komend do pracy z Dockerem do tajemniczej komendy ILMerge. Opisywanie każdej możliwej konfiguracji sprawiłoby, że przeczytanie tego artykułu zajęłoby całą noc (jak nie dłużej). Dlatego kolejne ciekawostki pozostawiam na przyszłe wpisy, a Ciebie drogi czytelniku zachęcam do zaparzenia kawy i zapoznania się z dokumentacją tego narzędzia. Jest co czytać 🙂
Podsumowując
Fajnie jest odejść od suchej konfiguracji pod konkretne środowisko CI. Dzięki Cake nasze buildy będą zachowywać się tak samo, niezależnie od narzędzia CI którego używamy. W dodatku każdy deweloper mający dostęp do pliku cake może dodać własne taski, te z kolei mogą przejść (lub nie) przez code review. Cake oferuje cały wachlarz funkcjonalności, a konfiguracja napisana w C# znajduje się w jednym miejscu . Jak się nie zakochać w tym narzędziu?
Duży plus za wpis i taki sam za powrót do bloggowania!
Był lekki przestój spowodowany różnymi zawirowaniami, poświęcę na to osobny wpis za jakiś czas 🙂