Kurz: Vytvoření vlastní úlohy pro generování kódu

V tomto kurzu vytvoříte vlastní úlohu v nástroji MSBuild v jazyce C#, která zpracovává generování kódu, a pak použijete úlohu v sestavení. Tento příklad ukazuje, jak pomocí nástroje MSBuild zpracovat operace čištění a opětovného sestavení. Příklad také ukazuje, jak podporovat přírůstkové sestavení, aby se kód vygeneroval pouze v případě, že se změnily vstupní soubory. Popsané techniky jsou použitelné pro širokou škálu scénářů generování kódu. Kroky také ukazují použití NuGetu k zabalení úlohy pro distribuci a kurz obsahuje volitelný krok pro použití prohlížeče BinLog ke zlepšení prostředí pro řešení potíží.

Požadavky

Měli byste znát koncepty nástroje MSBuild, jako jsou úkoly, cíle a vlastnosti. Viz koncepty nástroje MSBuild.

Příklady vyžadují nástroj MSBuild, který je nainstalován se sadou Visual Studio, ale lze je nainstalovat také samostatně. Viz Stažení nástroje MSBuild bez sady Visual Studio.

Úvod do příkladu kódu

Příklad přebírá vstupní textový soubor obsahující hodnoty, které se mají nastavit, a vytvoří soubor kódu jazyka C# s kódem, který tyto hodnoty vytvoří. I když je to jednoduchý příklad, stejné základní techniky lze použít ve složitějších scénářích generování kódu.

V tomto kurzu vytvoříte vlastní úlohu MSBuild s názvem AppSettingStronglyTyped. Úkol přečte sadu textových souborů a každý soubor s řádky s následujícím formátem:

propertyName:type:defaultValue

Kód vygeneruje třídu jazyka C# se všemi konstantami. Problém by měl zastavit sestavení a dát uživateli dostatek informací k diagnostice problému.

Kompletní ukázkový kód pro tento kurz je na vlastní úloze – generování kódu v úložišti ukázek .NET na GitHubu.

Vytvoření projektu AppSettingStronglyTyped

Vytvořte knihovnu tříd .NET Standard. Architektura by měla být .NET Standard 2.0.

Všimněte si rozdílu mezi úplným nástrojem MSBuild (ten, který Sada Visual Studio používá) a přenosným nástrojem MSBuild, který je součástí příkazového řádku .NET Core.

  • Full MSBuild: Tato verze nástroje MSBuild se obvykle nachází v sadě Visual Studio. Spouští se v rozhraní .NET Framework. Visual Studio to používá při spuštění sestavení v řešení nebo projektu. Tato verze je dostupná také v prostředí příkazového řádku, jako je visual Studio Developer Command Prompt nebo PowerShell.
  • .NET MSBuild: Tato verze nástroje MSBuild je součástí příkazového řádku .NET Core. Běží na .NET Core. Visual Studio přímo nevyvolá tuto verzi nástroje MSBuild. Podporuje pouze projekty, které se sestavují pomocí sady Microsoft.NET.SDK.

Pokud chcete sdílet kód mezi rozhraním .NET Framework a jakoukoli jinou implementací .NET, jako je .NET Core, měla by vaše knihovna cílit na .NET Standard 2.0 a chcete spustit v sadě Visual Studio, která běží na rozhraní .NET Framework. .NET Framework nepodporuje .NET Standard 2.1.

Vytvoření vlastní úlohy MsBuild AppSettingStronglyTyped

Prvním krokem je vytvoření vlastní úlohy NÁSTROJE MSBuild. Informace o tom, jak napsat vlastní úlohu NÁSTROJE MSBuild, vám můžou pomoct pochopit následující kroky. Vlastní úloha MSBuild je třída, která implementuje ITask rozhraní.

  1. Přidejte odkaz na balíček NuGet Microsoft.Build.Utilities.Core a pak vytvořte třídu s názvem AppSettingStronglyTyped odvozenou z Microsoft.Build.Utilities.Task.

  2. Přidejte tři vlastnosti. Tyto vlastnosti definují parametry úkolu, který uživatelé nastavují při použití úkolu v klientském projektu:

     //The name of the class which is going to be generated
     [Required]
     public string SettingClassName { get; set; }
    
     //The name of the namespace where the class is going to be generated
     [Required]
     public string SettingNamespaceName { get; set; }
    
     //List of files which we need to read with the defined format: 'propertyName:type:defaultValue' per line
     [Required]
     public ITaskItem[] SettingFiles { get; set; }
    

    Úloha zpracuje SettingFiles a vygeneruje třídu SettingNamespaceName.SettingClassName. Vygenerovaná třída bude mít sadu konstant na základě obsahu textového souboru.

    Výstupem úlohy by měl být řetězec s názvem vygenerovaného kódu:

     // The filename where the class was generated
     [Output]
     public string ClassNameFile { get; set; }
    
  3. Při vytváření vlastního úkolu dědíte z Microsoft.Build.Utilities.Task. K implementaci úlohy přepíšete metodu Execute() . Metoda Execute vrátí true , pokud je úkol úspěšný, a false jinak. Task implementuje Microsoft.Build.Framework.ITask a poskytuje výchozí implementace některých ITask členů a navíc poskytuje některé funkce protokolování. Pro diagnostiku a řešení potíží s úlohou je důležité výstupní stav protokolu, zejména pokud dojde k problému a úloha musí vrátit výsledek chyby (false). Při chybě třída signalizuje chybu voláním TaskLoggingHelper.LogError.

     public override bool Execute()
     {
     	//Read the input files and return a IDictionary<string, object> with the properties to be created. 
     	//Any format error it will return false and log an error
     	var (success, settings) = ReadProjectSettingFiles();
     	if (!success)
     	{
     			return !Log.HasLoggedErrors;
     	}
     	//Create the class based on the Dictionary
     	success = CreateSettingClass(settings);
    
     	return !Log.HasLoggedErrors;
     }
    

    Rozhraní API úloh umožňuje vrátit hodnotu false, což značí selhání bez toho, aby uživatele indikoval, co se nepovedlo. Nejlepší je vrátit !Log.HasLoggedErrors místo logického kódu a protokolovat chybu, když se něco nepovede.

Chyby protokolu

Osvědčeným postupem při protokolování chyb je poskytnutí podrobností, jako je číslo řádku a jedinečný kód chyby při protokolování chyby. Následující kód parsuje textový vstupní soubor a používá metodu TaskLoggingHelper.LogError s číslem řádku v textovém souboru, který chybu vytvořil.

private (bool, IDictionary<string, object>) ReadProjectSettingFiles()
{
	var values = new Dictionary<string, object>();
	foreach (var item in SettingFiles)
	{
		int lineNumber = 0;

		var settingFile = item.GetMetadata("FullPath");
		foreach (string line in File.ReadLines(settingFile))
		{
			lineNumber++;

			var lineParse = line.Split(':');
			if (lineParse.Length != 3)
			{
				Log.LogError(subcategory: null,
							 errorCode: "APPS0001",
							 helpKeyword: null,
							 file: settingFile,
							 lineNumber: lineNumber,
							 columnNumber: 0,
							 endLineNumber: 0,
							 endColumnNumber: 0,
							 message: "Incorrect line format. Valid format prop:type:defaultvalue");
							 return (false, null);
			}
			var value = GetValue(lineParse[1], lineParse[2]);
			if (!value.Item1)
			{
				return (value.Item1, null);
			}

			values[lineParse[0]] = value.Item2;
		}
	}
	return (true, values);
}

Pomocí technik zobrazených v předchozím kódu se chyby v syntaxi textového vstupního souboru zobrazují jako chyby sestavení s užitečnými diagnostickými informacemi:

Microsoft (R) Build Engine version 17.2.0 for .NET Framework
Copyright (C) Microsoft Corporation. All rights reserved.

Build started 2/16/2022 10:23:24 AM.
Project "S:\work\msbuild-examples\custom-task-code-generation\AppSettingStronglyTyped\AppSettingStronglyTyped.Test\bin\Debug\net6.0\Resources\testscript-fail.msbuild" on node 1 (default targets).
S:\work\msbuild-examples\custom-task-code-generation\AppSettingStronglyTyped\AppSettingStronglyTyped.Test\bin\Debug\net6.0\Resources\error-prop.setting(1): error APPS0001: Incorrect line format. Valid format prop:type:defaultvalue [S:\work\msbuild-examples\custom-task-code-generation\AppSettingStronglyTyped\AppSettingStronglyTyped.Test\bin\Debug\net6.0\Resources\testscript-fail.msbuild]
Done Building Project "S:\work\msbuild-examples\custom-task-code-generation\AppSettingStronglyTyped\AppSettingStronglyTyped.Test\bin\Debug\net6.0\Resources\testscript-fail.msbuild" (default targets) -- FAILED.

Build FAILED.

"S:\work\msbuild-examples\custom-task-code-generation\AppSettingStronglyTyped\AppSettingStronglyTyped.Test\bin\Debug\net6.0\Resources\testscript-fail.msbuild" (default target) (1) ->
(generateSettingClass target) ->
  S:\work\msbuild-examples\custom-task-code-generation\AppSettingStronglyTyped\AppSettingStronglyTyped.Test\bin\Debug\net6.0\Resources\error-prop.setting(1): error APPS0001: Incorrect line format. Valid format prop:type:defaultvalue [S:\work\msbuild-examples\custom-task-code-generation\AppSettingStronglyTyped\AppSettingStronglyTyped.Test\bin\Debug\net6.0\Resources\testscript-fail.msbuild]

	 0 Warning(s)
	 1 Error(s)

Když zachytíte výjimky v úloze, použijte metodu TaskLoggingHelper.LogErrorFromException . Tím se zlepší výstup chyby, například získáním zásobníku volání, kde byla vyvolán výjimka.

catch (Exception ex)
{
	// This logging helper method is designed to capture and display information
	// from arbitrary exceptions in a standard way.
	Log.LogErrorFromException(ex, showStackTrace: true);
	return false;
}

Implementace ostatních metod, které tyto vstupy používají k sestavení textu pro vygenerovaný soubor kódu, se zde nezobrazuje; viz AppSettingStronglyTyped.cs v ukázkovém úložišti.

Ukázkový kód vygeneruje kód jazyka C# během procesu sestavení. Úkol je stejný jako jakákoli jiná třída jazyka C#, takže až budete s tímto kurzem hotovi, můžete ho přizpůsobit a přidat jakékoli funkce potřebné pro váš vlastní scénář.

Vygenerování konzolové aplikace a použití vlastní úlohy

V této části vytvoříte standardní konzolovou aplikaci .NET Core, která tuto úlohu používá.

Důležité

Je důležité se vyhnout generování vlastní úlohy MSBuild ve stejném procesu MSBuild, který ho bude využívat. Nový projekt by měl být v kompletním jiném řešení sady Visual Studio nebo nový projekt používá předem vygenerovanou knihovnu DLL a znovu umístěnou ze standardního výstupu.

  1. V novém řešení sady Visual Studio vytvořte projekt konzoly .NET MSBuildConsoleExample.

    Normální způsob distribuce úkolu je prostřednictvím balíčku NuGet, ale během vývoje a ladění můžete zahrnout všechny informace přímo .props do souboru projektu aplikace a .targets pak přejít do formátu NuGet při distribuci úkolu ostatním.

  2. Upravte soubor projektu tak, aby spotřebovávat úlohu generování kódu. Výpis kódu v této části zobrazuje upravený soubor projektu po odkazování na úkol, nastavení vstupních parametrů pro úkol a zápis cílů pro zpracování operací čištění a opětovného sestavení tak, aby se vygenerovaný soubor kódu odebral tak, jak byste očekávali.

    Úlohy se registrují pomocí elementu UsingTask (MSBuild). Element UsingTask zaregistruje úlohu; informuje MSBuild název úlohy a jak vyhledat a spustit sestavení, které obsahuje třídu úlohy. Cesta sestavení je relativní vzhledem k souboru projektu.

    Obsahuje PropertyGroup definice vlastností, které odpovídají vlastnostem definovaným v úloze. Tyto vlastnosti jsou nastaveny pomocí atributů a název úlohy se používá jako název elementu.

    TaskName je název úlohy, na který se má odkazovat ze sestavení. Tento atribut by měl vždy používat plně zadané obory názvů. AssemblyFile je cesta k souboru sestavení.

    Chcete-li vyvolat úkol, přidejte úkol do příslušného cíle v tomto případě GenerateSetting.

    Cíl ForceGenerateOnRebuild zpracovává operace vyčištění a opětovného sestavení odstraněním vygenerovaného souboru. Nastaví se tak, aby běžel za CoreClean cílem nastavením atributu AfterTargets na CoreCleanhodnotu .

     <Project Sdk="Microsoft.NET.Sdk">
     	<UsingTask TaskName="AppSettingStronglyTyped.AppSettingStronglyTyped" AssemblyFile="..\..\AppSettingStronglyTyped\AppSettingStronglyTyped\bin\Debug\netstandard2.0\AppSettingStronglyTyped.dll"/>
    
     	<PropertyGroup>
     		<OutputType>Exe</OutputType>
     		<TargetFramework>net6.0</TargetFramework>
     		<RootFolder>$(MSBuildProjectDirectory)</RootFolder>
     		<SettingClass>MySetting</SettingClass>
     		<SettingNamespace>MSBuildConsoleExample</SettingNamespace>
     		<SettingExtensionFile>mysettings</SettingExtensionFile>
     	</PropertyGroup>
    
     	<ItemGroup>
     		<SettingFiles Include="$(RootFolder)\*.mysettings" />
     	</ItemGroup>
    
     	<Target Name="GenerateSetting" BeforeTargets="CoreCompile" Inputs="@(SettingFiles)" Outputs="$(RootFolder)\$(SettingClass).generated.cs">
     		<AppSettingStronglyTyped SettingClassName="$(SettingClass)" SettingNamespaceName="$(SettingNamespace)" SettingFiles="@(SettingFiles)">
     		<Output TaskParameter="ClassNameFile" PropertyName="SettingClassFileName" />
     		</AppSettingStronglyTyped>
     		<ItemGroup>
     			<Compile Remove="$(SettingClassFileName)" />
     			<Compile Include="$(SettingClassFileName)" />
     		</ItemGroup>
     	</Target>
    
     	<Target Name="ForceReGenerateOnRebuild" AfterTargets="CoreClean">
     		<Delete Files="$(RootFolder)\$(SettingClass).generated.cs" />
     	</Target>
     </Project>
    

    Poznámka:

    Místo přepsání cíle, jako CoreCleanje například , tento kód používá jiný způsob řazení cílů (BeforeTarget a AfterTarget). Projekty ve stylu sady SDK mají implicitní import cílů za posledním řádkem souboru projektu; to znamená, že nemůžete přepsat výchozí cíle, pokud ručně nezadáte import. Viz Přepsání předdefinovaných cílů.

    Outputs Atributy Inputs pomáhají nástroji MSBuild efektivněji tím, že poskytují informace pro přírůstková sestavení. Data vstupů se porovnávají s výstupy, abyste zjistili, jestli je potřeba spustit cíl, nebo jestli je možné znovu použít výstup předchozího sestavení.

  3. Vytvořte vstupní textový soubor s příponou definovanou, která se má zjistit. Pomocí výchozího rozšíření vytvořte MyValues.mysettings v kořenovém adresáři následující obsah:

     Greeting:string:Hello World!
    
  4. Znovu sestavte a vygenerovaný soubor by se měl vytvořit a sestavit. Zkontrolujte složku projektu pro soubor MySetting.generated.cs .

  5. Třída MySetting je v nesprávném oboru názvů, takže teď proveďte změnu pro použití oboru názvů aplikace. Otevřete soubor projektu a přidejte následující kód:

     <PropertyGroup>
     	<SettingNamespace>MSBuildConsoleExample</SettingNamespace>
     </PropertyGroup>
    
  6. Znovu sestavte a všimněte si, že třída je v MSBuildConsoleExample oboru názvů. Tímto způsobem můžete předefinovat vygenerovaný název třídy (SettingClass), textové přípony souborů (SettingExtensionFile), které se mají použít jako vstup, a umístění () z nich,RootFolder pokud chcete.

  7. Otevřete Program.cs a změňte pevně zakódovaný kód Hello World!! na uživatelem definovanou konstantu:

     static void Main(string[] args)
     {
     	Console.WriteLine(MySetting.Greeting);
     }
    

Spusťte program; vytiskne pozdrav z vygenerované třídy.

(Volitelné) Protokolování událostí během procesu sestavení

Je možné zkompilovat pomocí příkazu příkazového řádku. Přejděte do složky projektu. K vygenerování binárního protokolu použijete -bl možnost (binární protokol). Binární protokol bude mít užitečné informace, abyste věděli, co se děje během procesu sestavení.

# Using dotnet MSBuild (run core environment)
dotnet build -bl

# or full MSBuild (run on net framework environment; this is used by Visual Studio)
msbuild -bl

Oba příkazy generují soubor msbuild.binlogprotokolu, který lze otevřít pomocí msBuild Binary a Strukturovaného prohlížeče protokolů. Tato možnost /t:rebuild znamená spuštění cíle opětovného sestavení. Vynutí regeneraci vygenerovaného souboru kódu.

Blahopřejeme! Vytvořili jste úlohu, která generuje kód a používá ho v sestavení.

Zabalení úlohy pro distribuci

Pokud potřebujete použít vlastní úkol jenom v několika projektech nebo v jednom řešení, může být potřeba použít úkol jako nezpracované sestavení, ale nejlepší způsob, jak ho připravit na použití jinde nebo ho sdílet s ostatními, je jako balíček NuGet.

Balíčky úloh NÁSTROJE MSBuild mají několik klíčových rozdílů mezi balíčky NuGet knihovny:

  • Musí seskupit vlastní závislosti sestavení, místo aby tyto závislosti vystavovaly spotřebě projektu.
  • Nezabalí žádná požadovaná sestavení do lib/<target framework> složky, protože by to způsobilo zahrnutí sestavení NuGet do jakéhokoli balíčku, který využívá úlohu.
  • Stačí je zkompilovat pouze s sestaveními Microsoft.Build – za běhu je poskytne skutečný modul MSBuild, takže není nutné do balíčku zahrnout.
  • Vygenerují speciální .deps.json soubor, který msBuildu pomáhá načíst závislosti úlohy (zejména nativní závislosti) konzistentním způsobem.

Abyste dosáhli všech těchto cílů, musíte provést několik změn standardního souboru projektu nad rámec těch, které možná znáte.

Vytvoření balíčku NuGet

Vytvoření balíčku NuGet se doporučuje distribuovat vlastní úlohu ostatním.

Příprava na vygenerování balíčku

Chcete-li se připravit na vygenerování balíčku NuGet, proveďte některé změny v souboru projektu, abyste zadali podrobnosti popisované balíček. Počáteční projektový soubor, který jste vytvořili, se podobá následujícímu kódu:

<Project Sdk="Microsoft.NET.Sdk">

	<PropertyGroup>
		<TargetFramework>netstandard2.0</TargetFramework>
	</PropertyGroup>

	<ItemGroup>
		<PackageReference Include="Microsoft.Build.Utilities.Core" Version="17.0.0" />
	</ItemGroup>

</Project>

Pokud chcete vygenerovat balíček NuGet, přidejte následující kód, který nastaví vlastnosti balíčku. Úplný seznam podporovaných vlastností nástroje MSBuild najdete v dokumentaci k sadě Pack:

<PropertyGroup>
	... 
	<IsPackable>true</IsPackable>
	<Version>1.0.0</Version>
	<Title>AppSettingStronglyTyped</Title>
	<Authors>Your author name</Authors>
	<Description>Generates a strongly typed setting class base on a text file.</Description>
	<PackageTags>MyTags</PackageTags>
	<Copyright>Copyright ©Contoso 2022</Copyright>
	...
</PropertyGroup>

Označení závislostí jako soukromých

Závislosti úlohy MSBuild musí být zabaleny uvnitř balíčku; nemohou být vyjádřeny jako normální odkazy na balíčky. Balíček nezpřístupní externím uživatelům žádné běžné závislosti. Provede se to dvěma kroky: označí sestavení jako soukromá a ve skutečnosti je vloží do vygenerovaného balíčku. V tomto příkladu předpokládáme, že váš úkol závisí na Microsoft.Extensions.DependencyInjection práci, takže přidejte PackageReference do Microsoft.Extensions.DependencyInjection verze 6.0.0.

<ItemGroup>
	<PackageReference 
		Include="Microsoft.Build.Utilities.Core"
		Version="17.0.0" />
	<PackageReference
		Include="Microsoft.Extensions.DependencyInjection"
		Version="6.0.0" />
</ItemGroup>

Teď označte každou závislost tohoto projektu úkolu, a ProjectReference to jak s atributem, tak PackageReference i s atributemPrivateAssets="all". To nuGetu řekne, aby tyto závislosti nezpřístupnil vůbec pro využívání projektů. Další informace o řízení prostředků závislostí najdete v dokumentaci NuGet.

<ItemGroup>
	<PackageReference 
		Include="Microsoft.Build.Utilities.Core"
		Version="17.0.0"
		PrivateAssets="all"
	/>
	<PackageReference
		Include="Microsoft.Extensions.DependencyInjection"
		Version="6.0.0"
		PrivateAssets="all"
	/>
</ItemGroup>

Sbalte závislosti do balíčku.

Do balíčku úloh musíte také vložit prostředky modulu runtime našich závislostí. Existují dvě části: cíl nástroje MSBuild, který přidá naše závislosti do BuildOutputInPackage skupiny ItemGroup, a několik vlastností, které řídí rozložení těchto BuildOutputInPackage položek. Další informace o tomto procesu najdete v dokumentaci NuGet.

<PropertyGroup>
	...
	<!-- This target will run when MSBuild is collecting the files to be packaged, and we'll implement it below. This property controls the dependency list for this packaging process, so by adding our custom property we hook ourselves into the process in a supported way. -->
	<TargetsForTfmSpecificBuildOutput>
		$(TargetsForTfmSpecificBuildOutput);CopyProjectReferencesToPackage
	</TargetsForTfmSpecificBuildOutput>
	<!-- This property tells MSBuild where the root folder of the package's build assets should be. Because we are not a library package, we should not pack to 'lib'. Instead, we choose 'tasks' by convention. -->
	<BuildOutputTargetFolder>tasks</BuildOutputTargetFolder>
	<!-- NuGet does validation that libraries in a package are exposed as dependencies, but we _explicitly_ do not want that behavior for MSBuild tasks. They are isolated by design. Therefore we ignore this specific warning. -->
	<NoWarn>NU5100</NoWarn>
	...
</PropertyGroup>

...
<!-- This is the target we defined above. It's purpose is to add all of our PackageReference and ProjectReference's runtime assets to our package output.  -->
<Target
	Name="CopyProjectReferencesToPackage"
	DependsOnTargets="ResolveReferences">
	<ItemGroup>
		<!-- The TargetPath is the path inside the package that the source file will be placed. This is already precomputed in the ReferenceCopyLocalPaths items' DestinationSubPath, so reuse it here. -->
		<BuildOutputInPackage
			Include="@(ReferenceCopyLocalPaths)"
			TargetPath="%(ReferenceCopyLocalPaths.DestinationSubPath)" />
	</ItemGroup>
</Target>

Nezkompilujte sestavení Microsoft.Build.Utilities.Core

Jak je popsáno výše, tato závislost bude poskytována samotným nástrojem MSBuild za běhu, takže ji nemusíme sbalit do balíčku. Uděláte to tak, že do PackageReference něj přidáte ExcludeAssets="Runtime" atribut.

...
<PackageReference 
	Include="Microsoft.Build.Utilities.Core"
	Version="17.0.0"
	PrivateAssets="all"
	ExcludeAssets="Runtime"
/>
...

Vygenerování a vložení souboru deps.json

Nástroj MSBuild může použít soubor deps.json k zajištění načtení správných verzí závislostí. Budete muset přidat některé vlastnosti NÁSTROJE MSBuild, aby se soubor vygeneroval, protože se negeneruje ve výchozím nastavení pro knihovny. Pak přidejte cíl, který chcete zahrnout do výstupu balíčku, podobně jako jste to udělali u závislostí balíčku.

<PropertyGroup>
	...
	<!-- Tell the SDK to generate a deps.json file -->
	<GenerateDependencyFile>true</GenerateDependencyFile>
	...
</PropertyGroup>

...
<!-- This target adds the generated deps.json file to our package output -->
<Target
		Name="AddBuildDependencyFileToBuiltProjectOutputGroupOutput"
		BeforeTargets="BuiltProjectOutputGroup"
		Condition=" '$(GenerateDependencyFile)' == 'true'">

	 <ItemGroup>
		<BuiltProjectOutputGroupOutput
			Include="$(ProjectDepsFilePath)"
			TargetPath="$(ProjectDepsFileName)"
			FinalOutputPath="$(ProjectDepsFilePath)" />
	</ItemGroup>
</Target>

Zahrnutí vlastností a cílů nástroje MSBuild do balíčku

Pro pozadí této části si přečtěte o vlastnostech a cílech a potom, jak zahrnout vlastnosti a cíle do balíčku NuGet.

V některých případech můžete chtít přidat vlastní cíle sestavení nebo vlastnosti v projektech, které využívají váš balíček, například spuštění vlastního nástroje nebo procesu během sestavování. Uděláte to tak, že umístíte soubory do formuláře <package_id>.targets nebo <package_id>.props do build složky v projektu.

Soubory v kořenové složce sestavení projektu jsou považovány za vhodné pro všechny cílové architektury.

V této části připojíte implementaci úkolů a .props.targets soubory, které budou zahrnuty v našem balíčku NuGet a automaticky načteny z odkazujícího projektu.

  1. V souboru projektu úkolu přidejte do souboru projektu AppSettingStronglyTyped.csproj následující kód:

     <ItemGroup>
     	<!-- these lines pack the build props/targets files to the `build` folder in the generated package.
     		by convention, the .NET SDK will look for build\<Package Id>.props and build\<Package Id>.targets
     		for automatic inclusion in the build. -->
     	<Content Include="build\AppSettingStronglyTyped.props" PackagePath="build\" />
     	<Content Include="build\AppSettingStronglyTyped.targets" PackagePath="build\" />
     </ItemGroup>
    
  2. Vytvořte složku sestavení a v této složce přidejte dva textové soubory: AppSettingStronglyTyped.props a AppSettingStronglyTyped.targets. AppSettingStronglyTyped.props se naimportuje dříve v Microsoft.Common.props a vlastnosti definované později nejsou pro něj dostupné. Vyhněte se tedy odkazům na vlastnosti, které ještě nejsou definovány; vyhodnocují se jako prázdné.

    Adresář.Build.targets se naimportuje z Microsoft.Common.targets po importu .targets souborů z balíčků NuGet. Může tedy přepsat vlastnosti a cíle definované ve většině logiky sestavení nebo nastavit vlastnosti pro všechny projekty bez ohledu na to, co jednotlivé projekty nastaví. Viz pořadí importu.

    AppSettingStronglyTyped.props obsahuje úlohu a definuje některé vlastnosti s výchozími hodnotami:

     <?xml version="1.0" encoding="utf-8" ?>
     <Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
     <!--defining properties interesting for my task-->
     <PropertyGroup>
     	<!--The folder where the custom task will be present. It points to inside the nuget package. -->
     	<_AppSettingsStronglyTyped_TaskFolder>$(MSBuildThisFileDirectory)..\tasks\netstandard2.0</_AppSettingsStronglyTyped_TaskFolder>
     	<!--Reference to the assembly which contains the MSBuild Task-->
     	<CustomTasksAssembly>$(_AppSettingsStronglyTyped_TaskFolder)\$(MSBuildThisFileName).dll</CustomTasksAssembly>
     </PropertyGroup>
    
     <!--Register our custom task-->
     <UsingTask TaskName="$(MSBuildThisFileName).AppSettingStronglyTyped" AssemblyFile="$(CustomTasksAssembly)"/>
    
     <!--Task parameters default values, this can be overridden-->
     <PropertyGroup>
     	<RootFolder Condition="'$(RootFolder)' == ''">$(MSBuildProjectDirectory)</RootFolder>
     	<SettingClass Condition="'$(SettingClass)' == ''">MySetting</SettingClass>
     	<SettingNamespace Condition="'$(SettingNamespace)' == ''">example</SettingNamespace>
     	<SettingExtensionFile Condition="'$(SettingExtensionFile)' == ''">mysettings</SettingExtensionFile>
     </PropertyGroup>
     </Project>
    
  3. Při instalaci balíčku se automaticky zahrne soubor AppSettingStronglyTyped.props . Pak má klient k dispozici úlohu a některé výchozí hodnoty. Nikdy se ale nepoužívá. Chcete-li vložit tento kód do akce, definujte některé cíle v souboru AppSettingStronglyTyped.targets , který bude také automaticky zahrnut při instalaci balíčku:

     <?xml version="1.0" encoding="utf-8" ?>
     <Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
    
     <!--Defining all the text files input parameters-->
     <ItemGroup>
     	<SettingFiles Include="$(RootFolder)\*.$(SettingExtensionFile)" />
     </ItemGroup>
    
     <!--A target that generates code, which is executed before the compilation-->
     <Target Name="BeforeCompile" Inputs="@(SettingFiles)" Outputs="$(RootFolder)\$(SettingClass).generated.cs">
     	<!--Calling our custom task-->
     	<AppSettingStronglyTyped SettingClassName="$(SettingClass)" SettingNamespaceName="$(SettingNamespace)" SettingFiles="@(SettingFiles)">
     		<Output TaskParameter="ClassNameFile" PropertyName="SettingClassFileName" />
     	</AppSettingStronglyTyped>
     	<!--Our generated file is included to be compiled-->
     	<ItemGroup>
     		<Compile Remove="$(SettingClassFileName)" />
     		<Compile Include="$(SettingClassFileName)" />
     	</ItemGroup>
     </Target>
    
     <!--The generated file is deleted after a general clean. It will force the regeneration on rebuild-->
     <Target Name="AfterClean">
     	<Delete Files="$(RootFolder)\$(SettingClass).generated.cs" />
     </Target>
     </Project>
    

    Prvním krokem je vytvoření skupiny ItemGroup, která představuje textové soubory (může to být více než jeden) ke čtení a bude to některý z našich parametrů úkolu. Existují výchozí hodnoty pro umístění a rozšíření, kde hledáme, ale můžete přepsat hodnoty, které definují vlastnosti v klientském souboru projektu MSBuild.

    Pak definujte dva cíle NÁSTROJE MSBuild. Proces MSBuild rozšiřujeme a přepisujeme předdefinované cíle:

    • BeforeCompile: Cílem je volat vlastní úlohu pro vygenerování třídy a zahrnout třídu, která se má zkompilovat. Úkoly v tomto cíli se vloží před dokončení kompilace jádra. Vstupní a výstupní pole souvisejí s přírůstkovým sestavením. Jsou-li všechny výstupní položky aktuální, je cíl nástrojem MSBuild vynechán. Toto přírůstkové sestavení cíle může výrazně zlepšit výkon sestavení. Položka je považována za aktuální, pokud je její výstupní soubor stejně starý nebo novější než její vstupní soubor(y).

    • AfterClean: Cílem je odstranit vygenerovaný soubor třídy po obecném vyčištění. Úkoly v tomto cíli se vloží po vyvolání základních čistých funkcí. Vynutí opakování kroku generování kódu při spuštění cíle Znovu sestavit.

Vygenerování balíčku NuGet

Pokud chcete vygenerovat balíček NuGet, můžete použít Visual Studio (klikněte pravým tlačítkem na uzel projektu v Průzkumník řešení a vyberte Balíček). Můžete to také provést pomocí příkazového řádku. Přejděte do složky, kde je k dispozici soubor projektu úkolu AppSettingStronglyTyped.csproj , a spusťte následující příkaz:

// -o is to define the output; the following command chooses the current folder.
dotnet pack -o .

Blahopřejeme! Vygenerovali jste balíček NuGet s názvem \AppSettingStronglyTyped\AppSettingStronglyTyped\AppSettingStronglyTyped.1.0.0.nupkg.

Balíček má příponu .nupkg a je komprimovaný soubor ZIP. Můžete ho otevřít pomocí nástroje zip. Soubory .target a .props soubory jsou ve build složce. Soubor .dll je ve lib\netstandard2.0\ složce. Soubor AppSettingStronglyTyped.nuspec je na kořenové úrovni.

(Volitelné) Podpora cílení na více verzí

Měli byste zvážit podporu distribuce Full MSBuildu (.NET Framework) i Core (včetně .NET 5 a novějších), aby podporovaly nejširší možnou uživatelskou základnu.

U "normálních" projektů sady .NET SDK znamená multitargeting nastavení více targetFrameworks v souboru projektu. Když to uděláte, sestavení se aktivují pro TargetFrameworkMonikers a celkové výsledky se dají zabalit jako jeden artefakt.

To není celý příběh pro MSBuild. MSBuild má dvě primární přepravní vozidla: Visual Studio a sadu .NET SDK. Jedná se o velmi různá prostředí runtime; jeden běží v modulu runtime rozhraní .NET Framework a další běží na CoreCLR. To znamená, že zatímco váš kód může cílit na netstandard2.0, logika úlohy může mít rozdíly na základě toho, jaký typ modulu runtime MSBuild se právě používá. Prakticky platí, že existuje tolik nových rozhraní API v .NET 5.0 a novějších, dává smysl jak multitargetovat zdrojový kód úlohy MSBuild pro více cílových objektů TargetFrameworkMonikers, tak i multitarget cílovou logiku NÁSTROJE MSBuild pro více typů modulu runtime MSBuild.

Změny vyžadované pro více cílení

Cílení na více objektů TargetFrameworkMonikers (TFM):

  1. Změňte soubor projektu tak, aby používal soubory net472net6.0 TFM (druhá možnost se může změnit na základě toho, na jakou úroveň sady SDK chcete cílit). Možná budete chtít cílit netcoreapp3.1 , dokud nebude podpora .NET Core 3.1. Když to uděláte, struktura složek balíčku se změní z tasks/ na tasks/<TFM>/.

    <TargetFrameworks>net472;net6.0</TargetFrameworks>
    
  2. Aktualizujte .targets soubory tak, aby k načtení úloh používaly správnou sadu TFM. Požadovaný TFM se změní na základě toho, jaký .NET TFM jste zvolili výše, ale pro cílení net472 na projekt a net6.0, budete mít vlastnost jako:

<AppSettingStronglyTyped_TFM Condition=" '$(MSBuildRuntimeType)' != 'Core' ">net472</AppSettingStronglyTyped_TFM>
<AppSettingStronglyTyped_TFM Condition=" '$(MSBuildRuntimeType)' == 'Core' ">net6.0</AppSettingStronglyTyped_TFM>

Tento kód používá MSBuildRuntimeType vlastnost jako proxy pro aktivní hostitelské prostředí. Jakmile je tato vlastnost nastavená, můžete ji použít k UsingTask načtení správné AssemblyFile:

<UsingTask
    AssemblyFile="$(MSBuildThisFileDirectory)../tasks/$(AppSettingStronglyTyped_TFM)/AppSettingStronglyTyped.dll"
    TaskName="AppSettingStrongTyped.AppSettingStronglyTyped" />

Další kroky

Mnoho úloh zahrnuje volání spustitelného souboru. V některých scénářích můžete použít Úlohu Exec, ale pokud jsou omezení úlohy Exec problém, můžete také vytvořit vlastní úlohu. Následující kurz vás provede oběma možnostmi s realističtějším scénářem generování kódu: vytvoření vlastní úlohy pro generování klientského kódu pro rozhraní REST API.

Nebo se dozvíte, jak otestovat vlastní úlohu.