자습서: 코드 생성을 위한 사용자 지정 작업 만들기

이 자습서에서는 코드 생성을 처리하는 C#의 MSBuild 사용자 지정 작업을 만든 다음, 빌드에서 작업을 사용합니다. 이 예제에서는 MSBuild를 사용하여 정리 및 다시 빌드 작업을 처리하는 방법을 보여줍니다. 이 예제에서는 입력 파일이 변경된 경우에만 코드가 생성되도록 증분 빌드를 지원하는 방법도 보여 드립니다. 설명된 기술은 다양한 코드 생성 시나리오에 적용할 수 있습니다. 이 단계에서는 NuGet을 사용하여 배포 작업을 패키지하는 방법도 보여줍니다. 자습서에는 BinLog 뷰어를 사용하여 문제 해결 환경을 개선하는 선택적 단계가 포함되어 있습니다.

필수 조건

작업, 대상, 속성과 같은 MSBuild 개념을 이해해야 합니다. MSBuild 개념을 참조하십시오.

예제에는 Visual Studio와 함께 설치되는 MSBuild가 필요하지만 MSBuild를 별도로 설치할 수도 있습니다. Visual Studio 없이 MSBuild 다운로드를 참조하십시오.

코드 예제 소개

이 예제에서는 설정할 값이 포함된 입력 텍스트 파일을 사용하며, 이러한 값을 만드는 코드를 사용하여 C# 코드 파일을 만듭니다. 간단한 예제이지만 더 복잡한 코드 생성 시나리오에 동일한 기본 기술을 적용할 수 있습니다.

이 자습서에서는 AppSettingStronglyTyped라는 MSBuild 사용자 지정 작업을 만듭니다. 이 작업에서는 텍스트 파일 집합을 읽고 다음과 같은 형식의 줄이 있는 각 파일을 읽습니다.

propertyName:type:defaultValue

이 코드는 모든 상수와 함께 C# 클래스를 생성합니다. 문제가 있다면 빌드를 중지하고 해당 문제를 진단할 수 있는 충분한 정보를 사용자에게 제공해야 합니다.

이 자습서의 전체 샘플 코드는 GitHub의 .NET 샘플 리포지토리에서 사용자 지정 작업 - 코드 생성에 있습니다.

AppSettingStronglyTyped 프로젝트 만들기

.NET Standard 클래스 라이브러리를 만듭니다. 프레임워크는 .NET Standard 2.0이어야 합니다.

전체 MSBuild(Visual Studio가 사용하는 항목)과 .NET Core 명령줄에 번들로 제공되는 이식 가능한 MSBuild의 차이점을 확인합니다.

  • 전체 MSBuild: 이 버전의 MSBuild는 일반적으로 Visual Studio 내에 있습니다. .NET Framework에서 실행됩니다. Visual Studio는 솔루션 또는 프로젝트에서 Build를 실행할 때 이것을 사용합니다. 이 버전은 Visual Studio 개발자 명령 프롬프트 또는 PowerShell과 같은 명령줄 환경에서도 사용할 수 있습니다.
  • .NET MSBuild: 이 버전의 MSBuild는 .NET Core 명령줄에 번들로 제공됩니다. .NET Core에서 실행됩니다. Visual Studio는 이 버전의 MSBuild를 직접 호출하지 않습니다. Visual Studio는 Microsoft.NET.Sdk를 사용하여 빌드하는 프로젝트만 지원합니다.

.NET Framework와 그 밖의 .NET 구현(예: .NET Core) 간에 코드를 공유하려면 라이브러리가 .NET Standard 2.0을 대상으로 해야 하며 .NET Framework에서 실행되는 Visual Studio 내에서 실행하려고 합니다. .NET Framework는 .NET Standard 2.1을 지원하지 않습니다.

참조할 MSBuild API 버전 선택

사용자 지정 작업을 컴파일할 때 지원하려는 Visual Studio 및/또는 .NET SDK의 최소 버전과 일치하는 MSBuild API(Microsoft.Build.*) 버전을 참조해야 합니다. 예를 들어 Visual Studio 2019에서 사용자를 지원하려면 MSBuild 16.11에 대해 빌드해야 합니다.

AppSettingStronglyTyped MSBuild 사용자 지정 작업 만들기

첫 번째 단계는 MSBuild 사용자 지정 작업을 만드는 것입니다. MSBuild 사용자 지정 작업 작성 방법에 관한 정보는 다음 단계를 이해하는 데 도움이 될 수 있습니다. MSBuild 사용자 지정 작업은 ITask 인터페이스를 구현하는 클래스입니다.

  1. Microsoft.Build.Utilities.Core NuGet 패키지에 대한 참조를 추가한 다음, Microsoft.Build.Utilities.Task에서 파생된 AppSettingStronglyTyped라는 클래스를 만듭니다.

  2. 세 개의 속성을 추가합니다. 이러한 속성은 사용자가 클라이언트 프로젝트에서 작업을 사용할 때 설정하는 작업의 매개 변수를 정의합니다.

     //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; }
    

    작업은 SettingFiles를 처리하고 클래스 SettingNamespaceName.SettingClassName을 생성합니다. 생성된 클래스에는 텍스트 파일의 내용에 따라 상수 집합이 포함됩니다.

    작업 출력은 생성된 코드의 파일 이름을 제공하는 문자열이어야 합니다.

     // The filename where the class was generated
     [Output]
     public string ClassNameFile { get; set; }
    
  3. 사용자 지정 작업을 생성하면 Microsoft.Build.Utilities.Task에서 상속됩니다. 작업을 구현하려면 Execute() 메서드를 재정의합니다. 작업이 성공하면 Execute 메서드가 true를 반환하며 그렇지 않으면 false를 반환합니다. TaskMicrosoft.Build.Framework.ITask을 구현하고 일부 ITask 멤버의 기본 구현을 제공하며 일부 로깅 기능을 추가로 제공합니다. 특히 문제가 발생하고 작업이 오류 결과(false)를 반환해야 하는 경우, 작업을 진단하고 문제를 해결하기 위해 로그에 상태를 출력하는 것이 중요합니다. 오류 발생 시 클래스는 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;
     }
    

    작업 API를 사용하면 잘못된 내용을 사용자에게 표시하지 않고도 실패를 나타내는 false를 반환할 수 있습니다. 부울 코드 대신 !Log.HasLoggedErrors를 반환하고 문제가 발생하면 오류를 기록하는 것이 가장 좋습니다.

로그 오류

오류를 로깅할 때 가장 좋은 방법은 줄 번호 및 고유 오류 코드와 같은 세부 정보를 제공하는 것입니다. 다음 코드는 텍스트 입력 파일을 구문 분석하고 오류를 생성한 텍스트 파일의 줄 번호와 함께 TaskLoggingHelper.LogError 메서드를 사용합니다.

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);
}

이전 코드에 표시된 기술을 사용하면 텍스트 입력 파일 구문의 오류가 유용한 진단 정보와 함께 빌드 오류로 표시됩니다.

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)

작업에서 예외를 catch하는 경우 TaskLoggingHelper.LogErrorFromException 메서드를 사용합니다. 이렇게 하면 예를 들어 예외가 throw된 호출 스택을 가져와 오류 출력이 향상됩니다.

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;
}

생성된 코드 파일의 텍스트를 빌드하기 위해 이러한 입력을 사용하는 다른 메서드의 구현은 여기에 표시되지 않습니다. 샘플 리포지토리에서 AppSettingStronglyTyped.cs를 참조하세요.

예제 코드는 빌드 프로세스 중에 C# 코드를 생성합니다. 이 작업은 다른 C# 클래스와 같으므로 이 자습서를 완료하면 사용자 지정을 할 수 있으며 고유한 시나리오에 필요한 기능을 추가할 수 있습니다.

콘솔 앱 생성 및 사용자 지정 작업 사용

이 섹션에서는 작업을 사용하는 표준 .NET Core 콘솔 앱을 만듭니다.

Important

사용하려는 동일한 MSBuild 프로세스에서 MSBuild 사용자 지정 작업을 생성하지 않도록 하는 것이 중요합니다. 새 프로젝트는 완전히 다른 Visual Studio 솔루션에 있어야 하며, 그렇지 않으면 새 프로젝트는 표준 출력에서 미리 생성되고 다시 배치된 dll을 사용합니다.

  1. 새 Visual Studio Solution에서 .NET 콘솔 프로젝트 MSBuildConsoleExample을 만듭니다.

    작업을 배포하는 일반적인 방법은 NuGet 패키지를 사용하는 것이지만, 개발 및 디버깅 중에는 애플리케이션의 프로젝트 파일에 .props.targets에 관한 모든 정보를 직접 포함시킨 후 작업을 다른 사용자에게 배포할 때 NuGet 형식으로 이동할 수 있습니다.

  2. 코드 생성 작업을 사용하도록 프로젝트 파일을 수정합니다. 이 섹션의 코드 목록은 작업을 참조하고 작업에 대한 입력 매개 변수를 설정한 다음, 생성된 코드 파일이 예상대로 제거되도록 정리 및 다시 빌드 작업을 처리하기 위한 대상을 작성한 후 수정된 프로젝트 파일을 보여줍니다.

    작업은 UsingTask 요소(MSBuild)를 사용하여 등록됩니다. UsingTask 요소는 작업을 등록합니다. 이 요소는 MSBuild에 작업의 이름을 알려주며, 작업 클래스가 포함된 어셈블리를 찾고 실행하는 방법도 알려줍니다. 어셈블리 경로는 프로젝트 파일을 기준으로 합니다.

    PropertyGroup에는 작업에 정의된 속성에 해당하는 속성 정의가 포함됩니다. 이러한 속성은 특성을 사용하여 설정되며 작업 이름은 요소 이름으로 사용됩니다.

    TaskName은 어셈블리에서 참조할 작업의 이름입니다. 이 특성은 완전히 지정된 네임스페이스를 항상 사용해야 합니다. AssemblyFile은 어셈블리의 파일 경로입니다.

    작업을 호출하려면 해당 대상에 작업을 추가합니다. 이 경우에는 GenerateSetting입니다.

    대상 ForceGenerateOnRebuild는 생성된 파일을 삭제하여 정리 및 다시 빌드 작업을 처리합니다. 이 대상은 AfterTargets특성을 CoreClean으로 설정하여 CoreClean 대상 다음에 실행하도록 설정됩니다.

     <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>
    

    참고 항목

    이 코드는 CoreClean 같은 대상을 재정의하는 대신, 다른 방법을 사용해 대상(BeforeTarget 및 AfterTarget)을 정렬합니다. SDK 스타일 프로젝트는 프로젝트 파일의 마지막 줄 다음에 오는 대상을 암시적으로 가져옵니다. 즉, 가져오기를 수동으로 지정하지 않으면 기본 대상을 재정의할 수 없습니다. 미리 정의된 대상 재정의를 참조하세요.

    Inputs 특성 및 Outputs 특성은 증분 빌드에 대한 정보를 제공하여 MSBuild를 보다 효율적으로 만드는 데 도움이 됩니다. 입력 날짜를 출력 날짜와 비교하여 대상을 실행해야 하는지 또는 이전 빌드의 출력을 다시 사용할 수 있는지 확인합니다.

  3. 검색할 확장명이 정의된 입력 텍스트 파일을 만듭니다. 기본 확장명을 사용하여 다음 콘텐츠와 함께 루트에 MyValues.mysettings을 만듭니다.

     Greeting:string:Hello World!
    
  4. 다시 빌드한 후 생성된 파일을 만들고 빌드해야 합니다. MySetting.generated.cs 파일의 프로젝트 폴더를 확인합니다.

  5. MySetting 클래스가 잘못된 네임스페이스에 있으므로 이제 앱 네임스페이스를 사용하도록 변경합니다. 프로젝트 파일을 열고 다음 코드를 추가합니다.

     <PropertyGroup>
     	<SettingNamespace>MSBuildConsoleExample</SettingNamespace>
     </PropertyGroup>
    
  6. 다시 빌드하고 클래스가 MSBuildConsoleExample 네임스페이스에 있는지 확인합니다. 이렇게 하면 생성된 클래스 이름(SettingClass), 입력으로 사용할 텍스트 확장 파일(SettingExtensionFile) 및 그 파일의 위치(RootFolder)를 필요에 따라 다시 정의할 수 있습니다.

  7. Program.cs를 열어 하드 코딩된 'Hello World!!'를 다음의 사용자 정의 상수로 변경합니다.

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

프로그램을 실행합니다. 생성된 클래스에서 인사말을 인쇄합니다.

(선택 사항) 빌드 프로세스 중 이벤트 기록

명령줄 명령을 사용하여 컴파일할 수 있습니다. 프로젝트 폴더로 이동합니다. -bl(이진 로그) 옵션을 사용하여 이진 로그를 생성합니다. 이진 로그에는 빌드 프로세스 중에 진행 중인 작업을 파악하는 데 유용한 정보가 있습니다.

# 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

두 명령 모두 로그 파일 msbuild.binlog를 생성합니다. 이 파일은 MSBuild 이진 및 구조적 로그 뷰어로 열 수 있습니다. /t:rebuild 옵션은 Rebuild 대상을 실행하는 것을 의미합니다. 이 옵션을 선택하면 생성된 코드 파일이 강제로 다시 생성됩니다.

축하합니다! 코드를 생성하는 작업을 빌드했으며 이 작업을 빌드에서 사용했습니다.

배포를 위한 작업 패키지

일부 프로젝트 또는 단일 솔루션에서만 사용자 지정 작업을 사용해야 하는 경우 작업을 원시 어셈블리로 사용하는 것이 필요할 수 있지만 작업을 다른 곳에서 사용하거나 다른 사용자와 공유하도록 준비하는 가장 좋은 방법은 NuGet 패키지로 사용하는 것입니다.

MSBuild 작업 패키지에는 라이브러리 NuGet 패키지와는 다른 몇 가지 주요 차이점이 있습니다.

  • 자체 어셈블리 종속성을 사용 프로젝트에 노출하는 대신 번들로 묶어야 합니다.
  • NuGet이 작업을 사용하는 패키지에 있는 어셈블리를 포함하게 될 수 있으므로 필요한 어셈블리를 lib/<target framework> 폴더에 패키지하지 않습니다.
  • Microsoft.Build 어셈블리에 대해서만 컴파일하면 됩니다. 런타임 시 실제 MSBuild 엔진에서 제공되므로 패키지에 포함될 필요가 없습니다.
  • MSBuild가 작업의 종속성(특히 네이티브 종속성)을 일관된 방식으로 로드하는 데 도움이 되는 특별한 .deps.json 파일을 생성합니다.

이러한 모든 목표를 달성하려면 평소 알고 있던 것 이상으로 표준 프로젝트 파일에서 몇 가지를 변경해야 합니다.

NuGet 패키지 만들기

사용자 지정 작업을 다른 사용자에게 배포하려면 NuGet 패키지를 만드는 것이 좋습니다.

패키지 생성 준비

NuGet 패키지를 생성할 준비를 하려면 프로젝트 파일을 일부 변경하여 패키지를 설명하는 세부 정보를 지정합니다. 생성된 초기 프로젝트 파일은 다음 코드와 유사합니다.

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

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

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

</Project>

NuGet 패키지를 생성하려면 다음 코드를 추가하여 패키지의 속성을 설정합니다. 팩 설명서에서 지원되는 MSBuild 속성의 전체 목록을 볼 수 있습니다.

<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>
    <CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
    ...
</PropertyGroup>

종속성이 출력 디렉터리에 복사되도록 하려면 CopyLocalLockFileAssemblies 속성이 필요합니다.

종속성을 프라이빗으로 표시

MSBuild 작업의 종속성은 패키지 내에 패키징되어야 하며, 일반 패키지 참조로 표현할 수 없습니다. 패키지는 외부 사용자에게 일반 종속성을 노출하지 않습니다. 이렇게 하려면 두 단계를 수행합니다. 먼저 어셈블리를 프라이빗으로 표시하고, 그다음 생성된 패키지에 실제로 포함합니다. 이 예제에서는 작업이 Microsoft.Extensions.DependencyInjection을 사용하여 작업한다고 가정하므로 버전 6.0.0에서 PackageReferenceMicrosoft.Extensions.DependencyInjection에 추가합니다.

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

이제 이 작업 프로젝트의 모든 종속성을 PrivateAssets="all" 특성과 함께 PackageReferenceProjectReference로 표시합니다. 이렇게 하면 NuGet에 이러한 종속성을 사용 프로젝트에 전혀 노출하지 않도록 알릴 수 있습니다. 종속성 자산 제어에 대한 자세한 내용은 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>

패키지에 종속성 번들

또한 종속성의 런타임 자산을 작업 패키지에 포함해야 합니다. 이 작업은 두 부분으로 나뉘는데, 이는 BuildOutputInPackage ItemGroup에 종속성을 추가하는 MSBuild 대상과 해당 BuildOutputInPackage 항목의 레이아웃을 제어하는 몇 가지 속성입니다. 이 프로세스에 대한 자세한 내용은 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>
    <!-- Suppress NuGet warning NU5128. -->
    <SuppressDependenciesWhenPacking>true</SuppressDependenciesWhenPacking>
	...
</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>

Microsoft.Build.Utilities.Core 어셈블리를 번들로 묶지 말 것

위에서 설명한 대로 이 종속성은 런타임 시 MSBuild 자체에서 제공되므로 패키지에 번들로 묶을 필요가 없습니다. 번들로 묶으려면 ExcludeAssets="Runtime" 특성을 해당 PackageReference에 추가합니다.

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

deps.json 파일 생성 및 포함

올바른 버전의 종속성이 로드되도록 하기 위해 MSBuild에서 deps.json 파일을 사용할 수 있습니다. 이 파일은 라이브러리에 대해 기본적으로 생성되지 않으므로 파일이 생성되도록 하려면 일부 MSBuild 속성을 추가해야 합니다. 그런 다음 패키지 종속성에서 했던 것과 비슷하게 패키지 출력에 포함하기 위해 대상을 추가합니다.

<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>

패키지에 MSBuild 속성 및 대상을 포함

이 섹션에 관한 배경 정보를 알고 싶다면 속성 및 대상에 대해 읽은 다음, NuGet 패키지에 속성 및 대상을 포함하는 방법을 읽어보세요.

경우에 따라 패키지를 사용하는 프로젝트에 사용자 지정 빌드 대상 또는 속성(예: 빌드하는 동안 사용자 지정 도구 또는 프로세스 실행)을 추가할 수 있습니다. 이렇게 하려면 프로젝트의 build 폴더 내에 <package_id>.targets 또는 <package_id>.props 형식의 파일을 배치합니다.

프로젝트 루트 build 폴더의 파일은 모든 대상 프레임워크에 적합한 것으로 간주됩니다.

이 섹션에서는 NuGet 패키지에 포함되어 참조하는 프로젝트에서 자동으로 로드되는 .props 파일과 .targets 파일에서 작업 구현을 연결합니다.

  1. 이 작업의 프로젝트 파일인 AppSettingStronglyTyped.csproj에서 다음 코드를 추가합니다.

     <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. build 폴더를 만들고 해당 폴더에 AppSettingStronglyTyped.propsAppSettingStronglyTyped.targets라는 두 개의 텍스트 파일을 추가합니다. AppSettingStronglyTyped.propsMicrosoft.Common.props에서 먼저 가져올 수 있으며, 나중에 정의된 속성들은 여기에 적용할 수 없습니다. 따라서 아직 정의되지 않은 속성은 참조하지 마세요. 참조할 경우 비어 있는 것으로 평가됩니다.

    Directory.Build.targets는 NuGet 패키지에서 .targets 파일을 가져온 후 Microsoft.Common.targets에서 가져옵니다. 따라서 개별 프로젝트에서 무엇을 설정하는지와 관계없이 대부분의 빌드 논리에 정의된 속성과 대상을 재정의하거나 모든 프로젝트의 속성을 설정할 수 있습니다. 가져오기 순서를 참조하세요.

    AppSettingStronglyTyped.props는 작업을 포함하며 기본값을 사용하여 일부 속성을 정의합니다.

     <?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. AppSettingStronglyTyped.props 파일은 패키지가 설치될 때 자동으로 포함됩니다. 그런 다음, 클라이언트에 사용 가능한 작업과 일부 기본값이 있습니다. 그러나 이 파일은 결코 사용되지 않습니다. 이 코드를 실행하려면 AppSettingStronglyTyped.targets 파일에서 일부 대상을 정의합니다. 이 파일은 패키지가 설치될 때도 자동으로 포함됩니다.

     <?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>
    

    첫 단계에서는 ItemGroup을 만드는 데, 이 파일은 읽을 텍스트 파일(둘 이상일 수 있음)을 나타내며 작업 매개 변수 중 일부가 됩니다. 찾는 위치 및 확장명에 대한 기본값이 있으며 다만 클라이언트 MSBuild 프로젝트 파일에서 속성을 정의하는 값을 재정의할 수 있습니다.

    그런 다음, 두 개의 MSBuild 대상을 정의합니다. MSBuild 프로세스를 확장하여 미리 정의된 대상을 재정의합니다.

    • BeforeCompile: 사용자 지정 작업을 호출하여 클래스를 생성하고 컴파일할 클래스를 포함하는 것이 목표입니다. 이 대상의 작업은 핵심 컴파일이 완료되기 전에 삽입됩니다. 입력 및 출력 필드는 증분 빌드와 관련이 있습니다. 모든 출력 항목이 최신 상태인 경우 MSBuild는 대상을 건너뜁니다. 대상의 이 증분 빌드는 빌드 속도를 크게 향상시킬 수 있습니다. 출력 파일이 해당 입력 파일보다 최신이거나 두 파일의 타임스탬프가 같은 경우 항목이 최신 상태인 것으로 간주됩니다.

    • AfterClean: 일반적인 정리가 수행된 후 생성된 클래스 파일을 삭제하는 것이 목표입니다. 이 대상의 작업은 핵심 정리 기능이 호출된 후에 삽입됩니다. 다시 빌드 대상이 실행될 때 코드 생성 단계가 반복되도록 합니다.

NuGet 패키지를 생성합니다.

NuGet 패키지를 생성하기 위해 Visual Studio를 사용할 수 있습니다(솔루션 탐색기의 프로젝트 노드를 마우스 오른쪽 단추로 클릭하고 을 선택할 것). 명령줄을 사용하여 수행할 수도 있습니다. 작업 프로젝트 파일 AppSettingStronglyTyped.csproj가 있는 폴더로 이동하여 다음 명령을 실행합니다.

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

축하합니다! \AppSettingStronglyTyped\AppSettingStronglyTyped\AppSettingStronglyTyped.1.0.0.nupkg라는 NuGet 패키지를 생성했습니다.

패키지는 확장명이 .nupkg이며 압축된 zip 파일입니다. 패키지는 zip 도구를 사용하여 열 수 있습니다. .target 파일과 .props 파일이 build 폴더에 있습니다. .dll 파일은 lib\netstandard2.0\ 폴더에 있습니다. AppSettingStronglyTyped.nuspec 파일이 루트 수준에 있습니다.

(선택 사항) 멀티 타기팅 지원

가장 광범위한 사용자 기반을 지원하기 위해 Full(.NET Framework) 및 Core(.NET 5 이상 포함) MSBuild 배포를 모두 지원하는 것이 좋습니다.

‘normal’ .NET SDK 프로젝트의 경우 멀티 타기팅은 프로젝트 파일에 여러 TargetFrameworks를 설정하는 것을 의미합니다. 이렇게 하면 TargetFrameworkMonikers 모두에 대해 빌드가 트리거되고 전체 결과를 단일 아티팩트로 패키지할 수 있습니다.

MSBuild의 전체 사례가 아닙니다. MSBuild에는 Visual Studio와 .NET SDK의 두 가지 기본 배송 차량이 있습니다. 이는 매우 다른 런타임 환경입니다. 하나는 .NET Framework 런타임에서 실행되고 다른 하나는 CoreCLR에서 실행됩니다. 즉, 코드가 netstandard 2.0을 대상으로 할 수 있지만 작업 논리는 현재 사용 중인 MSBuild 런타임 유형에 따라 차이가 있을 수 있습니다. 실제로 .NET 5.0 이상에는 새로운 API가 너무 많기 때문에 여러 TargetFrameworkMonikers의 MSBuild 작업 소스 코드를 멀티 타기팅할 뿐만 아니라 여러 MSBuild 런타임 유형의 MSBuild 대상 논리를 멀티 타기팅하는 것이 좋습니다.

멀티 타기팅에 필요한 변경

여러 TFM(TargetFrameworkMonikers)을 대상으로 하려면:

  1. net472net6.0 TFM을 사용하도록 프로젝트 파일을 변경합니다(후자는 대상으로 지정하려는 SDK 수준에 따라 변경될 수 있음). .NET Core 3.1이 지원되지 않을 때까지 netcoreapp3.1을 대상으로 지정할 수 있습니다. 이렇게 하면 패키지 폴더 구조가 tasks/에서 tasks/<TFM>/으로 변경됩니다.

    <TargetFrameworks>net472;net6.0</TargetFrameworks>
    
  2. 올바른 TFM을 사용하여 작업을 로드하도록 .targets 파일을 업데이트합니다. 필요한 TFM은 위에서 선택한 .NET TFM을 기준으로 변경되지만 net472net6.0을 대상으로 하는 프로젝트의 경우 다음과 같은 속성이 있습니다.

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

이 코드는 MSBuildRuntimeType 속성을 활성 호스팅 환경의 프록시로 사용합니다. 이 속성이 설정되면 UsingTask에서 이 속성을 사용하여 올바른 AssemblyFile을 로드할 수 있습니다.

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

다음 단계

대부분의 작업에는 실행 파일 호출이 포함됩니다. 일부 시나리오에서는 Exec 작업을 사용할 수 있지만 Exec 작업의 제한 사항이 문제인 경우 사용자 지정 작업을 만들 수도 있습니다. 다음 자습서에서는 보다 현실적인 코드 생성 시나리오를 사용하여 REST API에 대한 클라이언트 코드를 생성하는 사용자 지정 작업을 만드는 두 옵션을 안내합니다.

또는 사용자 지정 작업을 테스트하는 방법을 알아봅니다.