W zeszłym tygodniu wspomniałem o lokalnym serwerze NuGet dla firmy, który każdy z nas może sobie postawić i czerpać z niego korzyści.

W tym tygodniu chciałbym przedstawić rozwiązanie jakie zaimplementowałem w firmie dla paczek statycznych – tych, do których kodu nie posiadamy, a jedynie od czasu do czasu wersja DLLki się zmienia.

Założenia

  • Chciałem mieć możliwość generowania paczek nuget w locie, to było najważniejsze ze wszystkich punktów, po 20-30 minut spędzonych z wizualnym edytorem NuGet, byłem pewny, że to nie jest ta droga, która chcę dalej podążać.
  • Chciałem by osoba niekumająca w pełni sposobu deploymentu była wstanie zaktualizować lub dodać nową paczkę.
  • Chciałem by proces był szybki.
  • Chciałem by każdy w zespole mógł przejść przez cały proces.

Podejście

Szukałem i przeglądałem wiele rozwiązań, było pewne, że będę potrzebował jakiegoś skryptu, lub kawałka softu, który mi moje problemy rozwiąże. Kłopot był z wybraniem najodpowiedniejszego.

Ręczna zabawa byłaby fajna dla tych, którzy mają czas, ja sam do nich nie należę i mam inne taski niż zabawianie się z edytorem graficznym – co powiem na sprincie? Bawiłem się aplikacyjką przez cały kolejny dzień, dziś też będę się ją bawił. Więc zabawa z NuGet Desigenr odapdała.

Pomyślałem o rake dla .NET, miałem dobre wspomnienia korzystania z niego jak i pisania rozszerzenia. Ale tutaj pojawił się problem, że nie jest to dla każdego – nie każdy będzie mógł to odpalić, tylko osoby z odpowiednio skonfigurowanym środowiskiem.

Ale przecież większość ludzi ma VS, więc ma MSBuild, więc może coś takiego da się załatwić za pomocą odpowiedniego projektu MSBuildowego? I tutaj jak zwykle natrafiłem na ścianę, której przez całe życie nie byłem wstanie przebić. TEGO JĘZYKA KOMPLETNIE NIE ROZUMIEM. Już sam fakt, że udało mi się kiedyś napisać msbulild targets dla ilmerge jest cudem. Niezależnie ile na ten temat czytam, ile razy się do tego zabieram to trafia mnie nerwica. Jest to tak fatalne API, że wstyd, że tak potężny system udostępnia taki język. Sorki dla wszystkich maniaków i twórców msbuild, it just sucks on client API. KROPKA.

Minął więc dzień, przestałem się już wkurzać na MSBuild i stwierdziłem, że pora znaleźć coś co naprawdę jest łatwe w użyciu i JA TO ZROZUMIEM. W szczególności, iż miałem w planach kolejne funkcjonalności skryptów.

Zacząłem, więc przeglądać kody źródłowe na GitHub i natrafiłem na masę przykładów wykorzystania skryptu opartego o PowerShell – psake. Już wcześniej miałem z nim małą styczność, ale nie zależnie od zajebistości PowerShell, składnia tego języka nie jest składnią, którą jestem wstanie zapamiętać. Wystarczą mi 3 tygodnie nie korzystania z tego języka by znów przeglądać MSDN i wkurzać się, że piszą nie prawdę bo jak się okazuje, czegoś nie załadowałem.

Jednak to małe narządko udostępnia pełny zestaw poleceń, który jest potrzebny do wykonania zadania w prosty i przyjazny sposób, wystarczy tylko zrozumieć działanie PS. A z tym problemu nie ma.

Ograniczenia

Zacząłem więc się zastanawiać, skoro mam narzędzie, które chcę spróbować, to co muszę zrobić ja by ułatwić sobie sprawę z napisaniem skryptu. W końcu w jakiś sposób chcę spełnić swoje dwa pierwsze założenia – te które uważałem za najważniejsze.

I tutaj od razu pojawiło mi się coś w głowie o czym przy ostatniej lekturze Puzzle Learning dużo czytałem: constraints nakładają ograniczenia, które ułatwiają rozwiązanie problemu.

Więc biorąc to pod uwagę zastanowiłem się co ja mam tak naprawdę osiągnąć i co ograniczyć by to umożliwić. Doszedłem do wniosku, że najlepszym ograniczeniem i zarazem najprostszym jest ograniczenie poprzez zastosowanie pewnej konwencji.

Konwencja wydawała mi się na tyle banalna, że każdy jest ją wstanie zrozumieć, a mianowicie:

  • Każdy folder w main directory to paczka, chyba, że coś należy do exclude list wtedy jest olewane
  • Każdy podfolder to wersja, jego nazwa musi bezpośrednio oddawać wersję paczki – ułatwia to proces tworzenia różnych wersji paczek i powoduje, iż stara wersja wciąż jest dostępna i może być wykrozystana
  • Struktura folderów w podfolderach musi by zgodna ze strukturą folderów nuget – nuget sam narzuca konwencję struktury katalogów, czemu by z niej nie skorzystać?

Implementacja

Mając pełny zbiór danych zabrałem się za pisanie skryptu, który:

  • Dla każdego rodzaju paczki (main folder)
  • Sprawdzi czy jest paczka o określonej wersji
  • Jeżeli jest to z folderu paczki przekopiuje do podfolderu wersji nupack
  • Następnie zmieni jego wersji w locie na wersję opisaną na folderze
  • Wygeneruje wszystkie paczki w katalogu output
  • Następnie wszystkie paczki wgra na serwer

Kroki wydają się banalnie proste prawda? A jak z implementacją? To jak się okazało jest super proste!

Wszystko, co potrzebujemy to stworzyć nasz katalog tools, do którego wgramy psake i nuget. To są dwa narzędzia z których będziemy korzystać.

Pisanie skryptu psake jest dość proste, jednak by ominąć problem z ustawianiem execution policy na komputerze, warto dodać dwa pliki do projektu, które zajmą się ładowaniem naszego skryptu:

Build.cmd

Plik odpowiedzialny za ustawienie odpowiedniego policy i wywołanie naszego dodatkowego skryptu ładującego psake:

@echo off

powershell -NoProfile -ExecutionPolicy Bypass -Command "& '%~dp0build.ps1' %*; if ($psake.build_success -eq $false) { exit 1 } else { exit 0 }"

Build.ps1

Plik odpowiedzialny za załadowanie skryptu psake i odpalenie naszego głównego skryptu, który chcemy wywołać.

param($task = "default")

#currend execution path with file name (current file, ....build.ps1)
$scriptPath = $MyInvocation.MyCommand.Path
#strip execution path with file name to just a current directory
$scriptDir = Split-Path $scriptPath

#if psake exists, remove it from current session
get-module psake | remove-module

# and use one that is in tools folder
import-module (Get-ChildItem "$scriptDir_toolspsakepsake.psm1" | Select-Object -First 1)

invoke-psake "$scriptDirdefault.ps1" $task

default.ps1

Na końcu cały skrypt niezbędny do wykonania całej naszej operacji:

properties {
	$exclude = "_tools"
	$baseDir = Resolve-Path .
	$outputDir = "$baseDiroutput"
	$nugetPath = "$baseDir_toolsnuget"
	$apiKey = "some_api_key"
	$nugetServer = "http://192.168.0.1/"
}

task default -depends Publish

task Init {
    cls
}

task Clean -depends Init {

	Remove-Item $outputDir -Force -Recurse -ErrorAction SilentlyContinue
	New-Item $outputDir -Type directory | Out-Null
}

task Pack -depends Init,Clean {

	$folders = Get-ChildItem -Path $baseDir | where {$_.PSIsContainer } | where {$_ -notmatch $exclude }

	foreach ($folder in $folders) {
		
        $versions = Get-ChildItem -Path $folder.FullName | where {$_.PSIsContainer }
        $nuspec = Get-ChildItem -Path "$($folder.FullName)*.*" -Include *.nuspec
        
        if (!$nuspec){ continue }
        
        foreach($version in $versions) {
            
            $versionNo = $version.Name
            
            Copy-Item -Path "$($nuspec.FullName)" -destination "$($version.FullName)"
            $specFile = "$($version.FullName)$($nuspec.Name)";
			#just in case, tfs set's file to readonly
            Set-ItemProperty $specFile -name IsReadOnly -value $false

            #Verbosity quite - I don't want to see warnings from NuGet like framework not specified...
            exec { &"$nugetPathnuget" pack "$specFile" -Version "$versionNo" -OutputDirectory "$outputDir" -Verbosity quiet }
            
			Remove-Item "$specFile" -Force -ErrorAction SilentlyContinue
		}
	}
}

task Publish -depends Pack {
	$packages = Get-ChildItem -Path "$outputDir*.*" -Include *.nupkg
	
	foreach($package in $packages) {
		exec { &"$nugetPathnuget" push "$($package.FullName)" -s $nugetServer "$apiKey" }
	}

	#we can clean output or leave it
}

Prosty prawda? Aż dziw bierze, żę jest to tak czytelne, że naprawdę nie trzeba pisać do niego komentarzy – już to sobie wyobrażam jak by to było przy MSBuild ;)

Podsumowanie

Wiem, że długo dochodziłem do tego tego finalnego skryptu, ale chciałem wam pokazać tok myślenia, przez jaki przeszedłem. Może on pomóc innym w podobnych sytuacjach – zamiast po prostu kopiowania całego rozwiązania ;) bez w pełni jego zrozumienia – been there, done that.

Niebawem napiszę jak rozwiązałem pozostałe problemy z automatyzacją w projekcie, także z wykorzystaniem psake, tym razem będę więcej czasu poświęcał temu co robią konkretne kroki i dlaczego je zastosowałem – jako że sam uczyłem się go od podstaw, sądzę, że ta wiedza przyda się każdemu.