Dnes se podíváme na modul, který se nechal inspirovat z jiných jazyků a používá se nejčastěji na automatizaci buildu (nebudu zde kostrbatě překládát anglické dobře známé pojmy), ale stejně tak dobře najde uplatnění v situacích, kdy potřebujeme provést sekvenci na sobě závislých kroků. Modul si můžete stáhnout z https://github.com/JamesKovacs/psake a již je určitě jasné, že se jmenuje psake.
Odpověď je jednoduchá. Pokud rádi editujete xml, pak vám nevadí práce s msbuildem. Pro ostatní psake (respektive PowerShell) nabízí komfort skriptovacího jazyka, kterého jste se v xml museli vzdát. Msbuild má samozřejmě také své přednosti, takže nejlepší volbou je kombinovat psake a msbuild dohromady – jak už to obyčejně na světě bývá.
Krom toho je psake možné použít i pro administrátorské účely. Vše, co nabízí PowerShell, je přístupné v psake. Psake je totiž napsané v PowerShellu. Příklady snad řeknou více.
Na domovské stránce https://github.com/JamesKovacs/psake stačí po kliknutí na tlačítko Download zvolit poslední release číslo 4.00. Tento zip soubor pak rozbalte na disk (dále předpokládám adresář c:\psake). Spusťte PowerShell konzoli a pokračujte příkazy:
c:\psake
PS> sl c:\psake import-module .\psake.psm1 # psake modul je naimportovaný Get-Help Invoke-psake -Full
Psake je dobře zdokumentovaný modul, takže poslední příkaz vypíše přehled parametrů použitelných při volání Invoke-Psake. Navíc ale obsahuje několik příkladů, na kterých je použití dobře patrné.
Invoke-Psake
Pro rychlé uvedení do problematiky si tu ukážeme jednoduchý příklad, jehož cílem je:
Z popisu jednotlivých kroků je jasné, že po sobě následují. Za jistých okolností se ale může hodit spustit kterýkoliv z příkazů samostatně. V psake skriptu by pak kroky byly popsány pomocí tasks:
task default -depends Full task Full -depends Backup, ListFiles, Commit task Backup { Write-Host Backup gci d:\temp\code | ? { !$_.PsIscontainer } | copy-Item -destination d:\temp\codebak } task ListFiles { Write-Host Files list gci d:\temp\codebak | Select -exp FullName | sc d:\temp\codebak\files.txt } task Commit { Write-Host Commit start-Process GitExtensions.exe -ArgumentList commit, d:\temp\codebak }
Tento skript pak uložíme jako psake-build.ps1 a spustíme. Pokud neuvedeme jméno tasku, psake použije task se jménem default:
PS> Invoke-psake -buildFile d:\temp\psake\psake-build.ps1 Executing task: Backup Backup Executing task: ListFiles Files list Executing task: Commit Commit Build Succeeded! ---------------------------------------------------------------------- Build Time Report ---------------------------------------------------------------------- Name Duration ---- -------- Backup 00:00:00.1396781 ListFiles 00:00:00.0548712 Commit 00:00:00.3255901 Full 00:00:00.5288634 Total: 00:00:00.6099274
A po skončení "buildu" se otevře okno GitExtensions se změnami do VCS. Slovo build se prolíná celým psake, ale znovu podotýkám, že nejde pouze o build. V příkladu jsme nic nepřekládali a nevyvíjeli.
V případě, že bychom potřebovali zavolat pouze některé tasky, specifikujeme je pod parametrem -taskList:
-taskList
PS> Invoke-psake -buildFile d:\temp\psake\psake-build.ps1 -task Backup, Commit
Pokud dojde v některém kroku k chybě, následující kroky již nejsou vykonány. Jednoduše toho docílíme například kopírováním z neexistujícího adresáře: $codeDir = 'd:\temp\doesntexist':
$codeDir = 'd:\temp\doesntexist'
PS> Invoke-psake d:\temp\psake\psake-build.ps1 Executing task: Backup Backup psake-build.ps1:Cannot find path 'D:\temp\doesntexist' because it does not exist.
V případě, že bychom chtěli pokračovat navzdory chybám zaznamenaným při běhu tasku, můžeme toto u tasku určit parametrem -ContinueOnError:
-ContinueOnError
PS> Invoke-psake d:\temp\psake\psake-build.ps1 Executing task: Backup Backup ---------------------------------------------------------------------- Error in Task [Backup] Cannot find path 'D:\temp\doesntexist' because it does not exist. ---------------------------------------------------------------------- Executing task: ListFiles ....
Psake skript je samozřejmě pořád skript napsaný v PowerShellu. Proto můžeme jména adresářů uložit do proměnné, aby byl skript přehlednější. Když se ale podíváme do některých psake skriptů, uvidíme kontstrukci properties { $var1 = 'value'; ... }, co tedy použít?
properties { $var1 = 'value'; ... }
Pokud jde o konstanty a nebudeme je chtít měnit, můžeme použít kteroukoliv z možností. Pokud bychom ale chtěli ve skriptu definovat default hodnotu nějaké proměnné, použijeme konstrukci properties {... }. Tak později můžeme default hodnotu přetížit pomocí parametru -properties. Soubor psake-build.ps1 by pak vypadal takto:
properties {... }
-properties
#fixní proměnná, nedá se měnit jinak než zápisem zde ve skriptu $codeDir = 'd:\temp\code' properties { #měnitelná proměnná $backupDir = 'd:\temp\codebak' } task default -depends Full task Full -depends Backup, ListFiles, Commit task Backup { Write-Host Backup gci $codeDir | ? { !$_.PsIscontainer } | copy-Item -destination $backupDir } ...
A při volání bychom použili parametr -properties:
PS> Invoke-psake -buildFile d:\temp\psake\psake-build.ps1 -properties @{ backupdir = 'd:\temp\otherbackdir' }
Všimněte si, že jako hodnotu předáváme hashtable, ne scripblock. Každá položka v hashtable specifikuje proměnnou, která bude vyhodnocena ve stejném scope jako properties {... } v psake skriptu (ale později).
Poznámka: výše uvedené není tak úplně pravda. I v případě, že do build skriptu napíšete $backupdir = 'nejaka default cesta' mimo blok properties { ... }, i této proměnné lze nastavit jiná hodnota z command line Invoke-Psake ... -properties @{backupdir= 'jina cesta'}. Spíše bych ji ale nedoporučil; v pozdějších verzích se může jiným způsobem pracovat se scope proměnných a skript by pak mohl přestat správně fungovat.
$backupdir = 'nejaka default cesta'
properties { ... }
Invoke-Psake ... -properties @{backupdir= 'jina cesta'}
Parameters
Psake ještě umožňuje specifikovat parametr funkce Invoke-Psake jménem -parameters. Jde opět o hashtable se stejnou strukturou jako -properties, tj. z každé dvojice key-value bude vytvořena proměnná. Tyto proměnné pak mohou být používány ve funkci properties { ... } v build skriptu – znamená to tedy, že tímto parametrizujeme skript. Z příkladu bude jasný rozdíl.
-parameters
Předpokládejme, že build skript vypadá takto:
properties { $s = get-service $services } task default task stop { $s | stop-service -whatif } task start { $s | start-service -whatif }
A jako vstupní parametr skriptu bychom poslali jméno/jména servis, které bychom chtěli nastartovat/zastavit:
PS> Invoke-psake -buildFile d:\temp\psake\psake-services.ps1 -task start -parameters @{ $services = 'W3SVC' }
V bloku properties jsme do proměnné uložili seznam servis odpovídajících vstupnímu parametru $services. Zde jsme mohli dodat jakoukoliv (složitější) inicializační logiku. Někoho jistě napadne, jestli bychom mohli stejného efektu dosáhnout tím, že si nadefinujeme inicializační task a ten budeme volat před ostatními tasky, které na inicializaci závisí. Kód upraveného skriptu:
properties
$services
task default properties { $services = "noservice" } task init { $s = get-service $services } task stop -depends init { Write-Host stop service $s; $s | stop-service -whatif } task start -depends init { Write-Host start service $s; $s | start-service -whatif }
A skript bychom volali bez použití -parameters:
PS> Invoke-psake -buildFile d:\temp\psake\psake-services2.ps1 -task start -properties @{ $services = 'W3SVC' } Executing task: init Executing task: start start service psake-services2.ps1:Cannot bind argument to parameter 'Name' because it is null.
Z uvedeného je patrné, že myšlenka byla dobrá, ale psake na toto nebylo uzpůsobeno. Každý task (respektive jeho scriptblock) běží ve svém vlastním scope a proto proměnné přežívají pouze v rámci daného tasku. Mohli bychom sice takové chování obejít pomocí modifikátoru script:, ale taková úprava mění vnitřní stav modulu a proto zcela jistě není vhodná.
script:
Doposud jsme se bavili o psake pouze obecně a řekli jsme si většinu věcí, které může člověk potřebovat v případě, že si chce práci zautomatizovat a svázat úlohy pravidly. Programátor ocení funkci exec, která ukončí psake skript v případě, že program uvnitř skončí s chybou. Chyba je indikována návratovým kódem. Tělo je velmi jednoduché, můžeme se na něj podívat pomocí příkazu Get-Content function:\exec.
exec
Get-Content function:\exec
Task pro kompilaci solution vypadá s použitím exec velmi jednoduše:
$framework = '4.0' ... task Build { exec { msbuild $slnPath '/t:Build' } }
Msbuildu můžeme samozřejmě podstrčit další parametry, ale je vidět, že psake nám velmi usnadňuje práci s jeho zavoláním. Na začátku skriptu se říká, že se má použít msbuild pro verzi .NET 4.0. Psake si samo najde příslušný adresář a zajistí, že se spustí ten náš správný msbuild.
Jednoduchá programátorská automatizace buildu by pak mohla zahrnovat clean, build, spuštění testů, vytvoření databáze a nakopírování do release adresáře:
$framework = '4.0' $sln = 'c:\dev\.....sln' $outDir = 'c:\dev\...' task default -depends Rebuild,Test,Out task Rebuild -depends Clean,Build task Clean { #exec { msbuild $slnPath '/t:Clean' } Write-Host Clean.... } task Build { #exec { msbuild $slnPath '/t:Build' } Write-Host Build.... } task Test { # run nunit console or whatever tool you want Write-Host Test.... } task out { #gci somedir -include *.dll,*.config | copy-item -destination $outDir Write-Host Out.... }
Dá se toto ještě nějak zkrášlit? Ano – pro ty, kteří si rádi klikají (a mnohdy je to rychlejší, než vypisovat příkaz na příkazovou řádku) si můžeme vytvořit jednoduché GUI.
Pro vytvoření GUI použijeme WinForms. Nepůjde o krásu, ale o funkčnost, přizpůsobím tedy tomuto kód a maximálně jej zestručním.PowerShell musí běžet v režimu -STA.
-STA
Add-type -assembly System.Windows.Forms Add-type -assembly System.Drawing if (! (get-module psake)) { sl D:\temp\psake\JamesKovacs-psake-b0094de\ ipmo .\psake.psm1 } $form = New-Object System.Windows.Forms.Form $form.Text = 'Build' $form.ClientSize = New-Object System.Drawing.Size 70,100 ('build',10), ('test',30), ('out', 50) | % { $cb = new-object Windows.Forms.CheckBox $cb.Text = $_[0] $cb.Size = New-Object System.Drawing.Size 60,20 $cb.Location = New-Object System.Drawing.Point 10,$_[1] $form.Controls.Add($cb) Set-Item variable:\cb$($_[0]) -value $cb } $go = New-Object System.Windows.Forms.Button $go.Text = "Run!" $go.Size = New-Object System.Drawing.Size 60,20 $go.Location = New-Object System.Drawing.Point 10,70 $go.add_Click({ $form.Close() if ($cbbuild.Checked) { $script:tasks += 'Rebuild' } if ($cbtest.Checked) { $script:tasks += 'Test' } if ($cbout.Checked) { $script:tasks += 'Out' } }) $form.Controls.Add($go) $script:tasks = @() $form.ShowDialog() | Out-Null if ($script:tasks) { Invoke-psake -buildFile d:\temp\psake\psake-devbuild.ps1 -task $tasks }
Na několika řádcích kódu jsme schopni si naklikat konfiguraci buildu. Nenechme se ale unést – build by měl být především automatický, pokud možno na jeden klik. Komplexní GUI s mnoha nastaveními nemusí být žádoucí!
Kód s importem modulu kontroluje, zda je psake již naimportované. Pokud bychom importovali modul podruhé, následující spuštění Invoke-Psake by skončilo chybou. Jde o problém samotného psake. Obecně by mělo jít moduly importovat vícekrát bez problémů.
Poznámka: práce se $script:tasks mimo handler události vypadá těžkopádně. Proč nevolám Invoke-Psake přímo v handleru a předávám si seznam tasků ve zvláštní proměnné? Psake výsledek svého běhu (tabulku, časy, výpisy o běžícím tasku) posílá do pipeline, aby bylo možné výstup přesměrovat do souboru. V handleru je tento výstup zpracováván jinak, neposílá se do hlavní pipeline a proto o výstup přijdeme. Jediné viditelné jsou výstupy z Write-Host, které jsou samozřejmě vypsány do konzole.
$script:tasks
Write-Host
Krátce si tu ukážeme, jak bez změny souboru s modulem můžeme změnit chování modulu. Jde o techniku používanou spíše zřídka. Mění totiž prostředí modulu. Bez dobrých znalostí vnitřní struktury modulu, může samozřejmě přestat pracovat korektně. Přesto – proč si nerozšířit znalosti?
Řekněme, že chceme v závěrečném souhrnu také zobrazovat aktuálního uživatele a jméno stroje. Postup je jednoduchý – je potřeba změnit funkci Write-TaskTimeSummary. Ve skutečnosti ji ale nezměníme:
Write-TaskTimeSummary
PS> $module = Get-Module psake PS> & $module { ${function:script:Write-TaskTimeSummaryBak} = (gi function:\Write-TaskTimeSummary).ScriptBlock } PS> & $module { ${function:script:Write-TaskTimeSummary} = { . Write-TaskTimeSummaryBak "$env:USERNAME @$env:COMPUTERNAME" }}
Čeho jsme tím dosáhli? Ve skriptu jsme vytvořili novou funkci Write-TaskTimeSummaryBak a do ní jsme zazálohovali aktuální obsah funkce Write-TaskTimeSummary. Poté jsme změnili definici funkce Write-TaskTimeSummary tak, aby volala zálohovanou funkci a na konec připojila řetězec se jménem uživatele a počítače.
Write-TaskTimeSummaryBak
Tento trik zřejmě často používat nebudete. Hodí se ale určitě znát obrat použitelný s jakýmkoliv modulem:
& (Get-Module mujmodul) { prikaz, ktery chci provest ve scope modulu }
Popsali jsme si základ práce s psake. Více informací lze najít především na https://github.com/JamesKovacs/psake/wiki. Namátkou se můžete těšit na tipy, jak nastavit akce před každým taskem a po každém tasku, podmínky nutné ke spuštění tasku, jak spustit psake z psake a další.
Ať se vám s psake dobře pracuje!
- Josef Štefan