2011-08-13

Jak přidat do projektu nový soubor při instalaci NuGet balíčku

Poprvé jsem byl postaven před nutnost napsat instalační skript pro NuGet balíček. Chtěl jsem úplně triviální věc – přidat do projektu link na několik souborů a nastavit jim vlastnost "Copy to Output Directory" na "Copy if newer". Nešlo to úplně tak hladce a proto si zde o tom ublogávám.


NuGet je boží. Předpokládám, že laskavý čtenář, který zabloudil na tyto stránky, již tuší o co jde. V opačném případě je možné, že vás sem nasměroval zlomyslný vyhledávač, a to co hledáte je tady.


Přidat do NuGet balíčku instalační skript je úplně jednoduché. Stačí vytvořit soubor s názvem install.ps1 a nakopírovat ho do složky tools (v případě že vytváříte balíček z convention-based directory), nebo ho přidat v .nuspec souboru pod target tools, například takto:

<files>
  <file src="*.ps1" target="tools"/>
</files>

Tím ovšem začíná dobrodružnější část: Zjistit, co má takový install.ps1 vlastně obsahovat a zajistit, aby fungoval.


Nejdříve musíme definovat hlavičku:

param($installPath, $toolsPath, $package, $project)

Parametr $toolsPath obsahuje cestu ke složce tools. Do té jsme si v balíčku připravili soubory, které budeme přidávat do projektu. Dále použijeme parametr $project typu EnvDTE.Project, který nám umožní manipulovat s Visual Studiovým projektem a jeho soubory. Hlavním smyslem typu EnvDTE.Project je zúžit okruh lidí, kteří budou psát rozšíření pro Visual Studio, takže zjistit jak přidat soubor a nastavit mu vlastnosti není úplně jednoduché.


Pro prozkoumávání struktury uložené v parametru $project se mi osvědčila Package Manager Console přímo ve Visual Studiu. Je to normální PowerShellová konzole, kterou vám do VS přidá při své instalaci NuGet. Příkazem Get-Project se dostaneme na objekt reprezentující projekt a jeho vlastnosti. Například seznam souborů v projektu dostaneme takto:

PM> get-project -name Dkl.Demo | 
  foreach { $_.ProjectItems } | 
  select { $_.Name }

Přidat nový soubor (jako link) je jednoduché, stačí použít metodu ProjectItems.AddFromFile. Horší je pak nastavit nějakou jeho vlastnost, protože jsou definované dynamicky a liší se podle typu projektu. Pro začátek si vypíšeme všechny vlastnosti určitého souboru:

PM> get-project -name Dkl.Demo | 
  foreach { $_.ProjectItems.Item('Class.cs').Properties } | 
  select { $_.Name, $_.Value }

$_.Name, $_.Value
-------------------
{Extension, .cs}
{FileName, Class.cs}
...
{BuildAction, 1}
...
{CopyToOutputDirectory, 0}
...

Takže už víme, že vlastnost kterou chceme nastavit, se jmenuje CopyToOutputDirectory, hurá! Teď ještě zjistit, co znamená hodnota 0, a jaké číslo použít když chceme nastavit "Copy if newer". Když už máme přesný název property, proklepnul jsem gůgl, jestli nenajde nějakou specifikaci na hlubokém internetu MSDN, a ejhle, objevil jsem dobře ukrytou perlu jmenných konvencí, VSLangProj80.FileProperties2. Bohužel ale ani zde není popsáno, jakou hodnotu použít pro CopyToOutputDirectory.


Nakonec jsem se uchýlil k podlému triku – vytvořil jsem v projektu soubor, kterému jsem nastavil CopyToOutputDirectory na "Copy if newer" a zjistil hodnotu tímto příkazem (všimněte si, že jsem musel použít foreach - čekal bych select, ale ten vypíše nic neříkající "System.__ComObject"):

PM> get-project -name Gmc.Fido.Shared | 
  foreach { $_.ProjectItems.Item('Common.Logging.dll').Properties.Item('CopyToOutputDirectory') }

Value       : 2
NumIndices  : 0
Application : 
Parent      : 
Name        : CopyToOutputDirectory
Collection  : System.__ComObject
Object      : 
DTE         : System.__ComObject
Uff, takže 2.

Konečně se můžeme vrátit k instalačnímu skriptu, který přidá soubory ze složky tools v balíčku jako linky do projektu a nastaví všem "Copy if newer":

param($installPath, $toolsPath, $package, $project)
  Get-ChildItem -path $toolsPath | 
    Where-Object { $_.Name -match '.*\.dll' } | 
    ForEach-Object { 
      $i = $project.ProjectItems.AddFromFile($_.FullName); 
      $i.Properties.Item("CopyToOutputDirectory").Value = 2 
    }

Zbývá ještě napsat odinstalační skript, který NuGet spustí při odebrání balíčku nebo jeho upgrade na vyšší verzi. Přidáme do tools soubor uninstall.ps1, který bude vypadat takto:

param($installPath, $toolsPath, $package, $project)
  $project.ProjectItems | 
    Where-Object { $_.Name -match '.*\.dll' } | 
    ForEach-Object { $_.Remove() }

A je to! Zbývá už jen pár silných tvrzení na závěr: NuGet je boží. EnvDTE není boží. EnvDTE dost sukuje.

Žádné komentáře:

Okomentovat