C# 소스 생성기: 빌드 정보는 어떻게 얻나요? | Steven Giesel

소스 생성기는 C#에 도입된 강력한 기능으로, 개발자가 컴파일 과정 중에 추가 코드를 자동으로 생성할 수 있습니다. 상용구를 줄이고 성능을 개선하며 코드베이스를 간소화하는 데 도움이 됩니다.

이 블로그 게시물에서는 소스 생성기를 소개하고, 작동 방식을 설명하며, 빌드 정보를 생성하는 소스 생성기의 예제를 살펴봅니다.

여기서 무엇을 하려고 할까요?

이런 것만 있으면 됩니다.

var buildAt = BuildInformation.BuildAt; // 빌드가 생성된 UTC 기준
var configuration = BuildInformation.Configuration; // 릴리스 또는 디버그
var platform = BuildInformation.Platform; // AnyCPU 또는 arm64 같은 것

소스 생성기란 무엇인가요?

소스 생성기는 컴파일 과정에 포함할 추가 C# 소스 코드를 생성할 수 있는 C#의 컴파일 타임 기능입니다. 소스 생성기는 기본적으로 새 코드 파일을 생성한 다음 나머지 프로젝트와 함께 컴파일할 수 있는 사용자 지정 Roslyn 분석기입니다. 소스 생성기는 반복적인 작업을 위한 코드 생성을 자동화하고, 코딩 표준을 적용하며, 컴파일 시 코드를 최적화하는 데 도움이 될 수 있습니다.

소스 생성기는 어떻게 사용하나요?

먼저 netstandard2.0을 대상으로 하는 클래스 라이브러리 프로젝트를 새로 만들어야 합니다. 이 프로젝트는 다른 것이 아니라 netstandard2.0을 대상으로 해야 합니다. 그 이유는 여기에 있습니다:

컴파일러는 이전 .NET Framework와 새 .NET 모두에서 실행되어야 하므로 모든 생성기는 .NET Standard 2.0을 대상으로 해야 합니다.

그런 다음 다음 패키지를 추가합니다.

<ItemGroup>
  <PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.4">
    <PrivateAssets>all</PrivateAssets>
    <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
  </PackageReference>
  <PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.5.0" PrivateAssets="all" />
</ItemGroup>

이 두 패키지로 생성기를 생성할 수 있습니다. PrivateAssets은 패키지가 개발자 종속성이며 링크된 어셈블리와 함께 제공되지 않음을 의미합니다. 그리고 그것은 의미가 있습니다. 생성기는 생성기 자체가 아니라 링크된 어셈블리의 일부가 될 새로운 C# 파일을 생성할 텐데 왜 생성기를 함께 제공해야 할까요?

이제 다음과 같이 생성기를 구현할 수 있습니다.

[Generator]
public sealed class IncrementalBuildInformationGenerator : IIncrementalGenerator
{
    public void Initialize(IncrementalGeneratorInitializationContext context)

GeneratorAttribute가 필요하고 IIncrementalGenerator 인터페이스를 구현합니다. 이제 빌드 정보를 가져올 수 있는 소스 코드를 생성하는 제너레이터를 구현할 수 있습니다.

using Microsoft.CodeAnalysis;

[Generator]
public sealed class IncrementalBuildInformationGenerator : IIncrementalGenerator
{
    public void Initialize(IncrementalGeneratorInitializationContext context)
    {
        var compilerOptions = context.CompilationProvider.Select((s, _)  => s.Options);

        context.RegisterSourceOutput(compilerOptions, static (productionContext, options) =>
        {
            var buildInformation = new BuildInformationInfo
            {
                BuildAt = DateTime.UtcNow.ToString("O"),
                Platform = options.Platform.ToString(),
                WarningLevel = options.WarningLevel,
                Configuration = options.OptimizationLevel.ToString(),
            };

            productionContext.AddSource("LinkDotNet.BuildInformation.g", GenerateBuildInformationClass(buildInformation));
        });
    }

    private static string GenerateBuildInformationClass(BuildInformationInfo buildInformation)
    {
        return $@"
using System;
using System.Globalization;
public static class BuildInformation
{{
    /// <summary>
    /// Returns the build date (UTC).
    /// </summary>
    /// <remarks>Value is: {buildInformation.BuildAt}</remarks>
    public static readonly DateTime BuildAt = DateTime.ParseExact(""{buildInformation.BuildAt}"", ""O"", CultureInfo.InvariantCulture, DateTimeStyles.RoundtripKind);
    /// <summary>
    /// Returns the platform.
    /// </summary>
    /// <remarks>Value is: {buildInformation.Platform}</remarks>
    public const string Platform = ""{buildInformation.Platform}"";
    /// <summary>
    /// Returns the warning level.
    /// </summary>
    /// <remarks>Value is: {buildInformation.WarningLevel}</remarks>
    public const int WarningLevel = {buildInformation.WarningLevel};
    /// <summary>
    /// Returns the configuration.
    /// </summary>
    /// <remarks>Value is: {buildInformation.Configuration}</remarks>
    public const string Configuration = ""{buildInformation.Configuration}"";
}}
";
    }

    private sealed class BuildInformationInfo
    {
        public string BuildAt { get; set; } = string.Empty;
        public string Platform { get; set; } = string.Empty;
        public int WarningLevel { get; set; }
        public string Configuration { get; set; } = string.Empty;
    }
}

제공된 코드는 빌드 날짜, 플랫폼, 경고 수준 및 구성과 같은 빌드 관련 정보가 포함된 정적 BuildInformation 클래스를 생성하는 증분 소스 생성기의 예입니다. 단계별로 분석해 보겠습니다.

  1. IncrementalBuildInformationGenerator 클래스에는 [Generator] 특성이 표시되어 있으며 IIncrementalGenerator 인터페이스를 구현합니다. 이는 컴파일러에게 이 클래스가 증분 소스 생성기로 취급되어야 함을 알려줍니다.
  2. Initialize 메서드는 생성기의 종속성을 설정하고 출력을 등록하는 역할을 합니다. 이 예제에서 생성기는 컴파일러 옵션에 따라 달라집니다. context.CompilationProvider는 컴파일을 가져오는 데 사용되며, Select 메서드는 컴파일에서 Options 속성을 추출하는 데 사용됩니다.
  3. context.RegisterSourceOutput 메서드는 종속성(compilerOptions)에 따라 생성기의 출력을 등록하기 위해 호출됩니다. 이 메서드는 생성된 소스 코드를 컴파일에 추가할 수 있는 productionContext와 컴파일러 옵션이 포함된 options이라는 두 개의 매개변수가 있는 델리게이트를 받습니다.
  4. 생성된 파일의 고유한 이름(“LinkDotNet.BuildInformation.g”)과 생성된 소스 코드를 전달하면 productionContext.AddSource 메서드가 호출됩니다. 그러면 컴파일에 생성된 BuildInformation 클래스가 추가됩니다.

사용법

이제 생성기를 사용할 새 콘솔 프로젝트를 만들어 보겠습니다. 생성기를 포함하려면 이렇게 하면 됩니다.

<ItemGroup>
  <ProjectReference Include="..\LinkDotNet.BuildInformation\LinkDotNet.BuildInformation.csproj" OutputItemType="Analyzer" ReferenceOutputAssembly="false"/>
</ItemGroup>

보시다시피, 생겅기는 분석기로 간주되며 위에서 설명한 것처럼 출력에서 생성기를 참조하지 않습니다! 사용하는 IDE에 따라 클래스를 직접 사용하거나 “빌드” 버튼을 누를 수 있습니다.

Console.WriteLine($"Build at: {BuildInformation.BuildAt}");
Console.WriteLine($"Platform: {BuildInformation.Platform}");
Console.WriteLine($"Warning level: {BuildInformation.WarningLevel}");
Console.WriteLine($"Configuration: {BuildInformation.Configuration}");

다음과 같은 결과를 얻을 수 있습니다.

Build at: 2023-03-23T12:34:56.7890123Z
Platform: AnyCPU
Warning level: 4
Configuration: Debug

꽤 쉽습니다. 물론 이것은 생성기에 대한 쉬운 입문이었지만, 특히 전체 컴파일 객체를 처리해야 하는 경우 매우 빠르게 복잡해질 수 있습니다.

마무리

C#의 소스 생성기는 코드 생성을 자동화하고 코드베이스를 개선할 수 있는 혁신적인 방법을 제공합니다. 반복적인 작업을 단순화하고, 코딩 표준을 적용하고, 컴파일 시 코드를 최적화하는 데 사용할 수 있습니다. 증분 빌드 정보 생성기 예제에서는 빌드 정보를 생성하는 소스 생성기를 생성하여 프로젝트의 빌드 구성에 대한 중요한 세부 정보를 쉽게 추적하고 액세스할 수 있는 방법을 보여줍니다.

자료

프로젝트에서 BuildInformation 를 사용하려면 NuGet : NuGet Gallery | LinkDotNet.BuildInformation 1.0.0 을 사용
이 블로그 게시물의 소스 코드: 여기
모든 샘플 코드는 이 리포지토리에서 호스팅됩니다: 여기


3개의 좋아요