しばやん雑記

Azure とメイドさんが大好きなフリーランスのプログラマーのブログ

Azure Pipelines の Hosted Agent を使うと C# 7.1 以降のコードがビルド出来ないのを直す

Azure Functions なプロジェクトのビルドで Azure Pipelines を使っていたところ、手元ではビルド出来ていたコードが Azure Pipelines だとビルドエラーになってしまったので、最適な解決策を調べていました。

具体的には C# 7.1 以降のコードが Azure Pipelines の Hosted Agent だとビルド出来ません。以下のように default の型推論をつかったコードを書くとエラーになります。

string test = default;

実際に Azure Pipelines でビルドさせると、この機能は C# 7.0 では使えないので 7.1 以降のバージョンを使うように、というエラーが出ます。

そしてよく見ると分かるように、MSBuild のバージョンが 15.x です。

f:id:shiba-yan:20190816020438p:plain

原因としては Azure Pipelines の Hosted Agent に Visual Studio 2017 向けの .NET Core SDK 2.2.105 がインストールされているので、MSBuild のバージョンが古いというオチです。

ちなみに Visual Studio 2019 や Ubuntu の Hosted Agent を使っても同じ結果です。

開発環境とビルド環境で使える言語バージョンが異なっているのは避けたいので、以下の 3 つの方法のどれかを使って回避することにします。

  • csproj で LangVersion を latest に設定する
  • 最新の .NET Core SDK を手動でインストールする
  • Container jobs で最新バージョンの .NET Core SDK Docker Image を使う

設定や使い方を実際に動かした例を交えながら紹介します。割とこの問題には悩まされたので、自分用のメモを兼ねて YAML 定義も残しておきます。

LangVersion を設定

C# 7.x がデフォルトでコンパイルできるようになったのは MSBuild 16.x 系なので、Hosted Agent が使っている MSBuild 15.x 系では LangVersion で指定しない限り C# 7.0 までしか有効化されていません。

csproj で LangVersionlatest を指定すると最新のバージョンが使えます。

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

  <PropertyGroup>
    <TargetFramework>netcoreapp2.1</TargetFramework>
    <AzureFunctionsVersion>v2</AzureFunctionsVersion>
    <!-- ↓ を追加 -->
    <LangVersion>latest</LangVersion>
  </PropertyGroup>

</Project>

これで MSBuild 15.x でもC# 7.1 以降のコードがビルド出来るようになりますが、プロジェクトを弄るのは正直あまりやりたくないです。

Visual Studio 2019 では設定なしで使えるので、開発環境側に合わせておきたい気持ちが大きいです。

最新の .NET Core SDK をインストール

そもそもの問題としては、Visual Studio 2019 な Hosted Agent に 2019 向けの .NET Core SDK が入っていないことなので、明示的に最新の SDK をインストールするようにしても解決します。インストールされている .NET Core SDK が古いのは別の問題です。

ちなみに Hosted Agent の .NET Core SDK は 4 月あたりから更新されていないです。いくつも Issue が上がっていますが、正直なところ最新の SDK が Agent 更新の度に入るようになるのには時間がかかりそうです。

trigger:
- master

pool:
  vmImage: 'windows-2019'

variables:
  DOTNET_SKIP_FIRST_TIME_EXPERIENCE: true
  NUGET_XMLDOC_MODE: skip

steps:
- task: UseDotNet@2
  inputs:
    packageType: 'sdk'
    version: '2.2.x'

- task: DotNetCoreCLI@2
  inputs:
    command: 'build'
    projects: '**/*.csproj'
    arguments: '-c Release'

UseDotNet@2 を使って 2.2.x 系で最新の SDK をインストールするようにしています。

これでパイプラインを実行すると、正常にビルド出来るようになります。新しい MSBuild を使うと NuGet パッケージの復元が早くなる気がします。少なくともログは少なくなって見やすいです。

f:id:shiba-yan:20190816015723p:plain

そのままでは .NET Core SDK インストール直後にパッケージキャッシュの展開が走ってしまって、パッケージの復元で異常に時間がかかるようになるので、DOTNET_SKIP_FIRST_TIME_EXPERIENCE を環境変数に追加して展開しないようにします。これで処理時間が多少は短縮できます。

この辺りは他の CI SaaS でも共通の方法なので、以前 CircleCI でも同じ設定を使い高速化しました。

Ubuntu のイメージには最初からスキップする設定が入っているようですが、何故か Windows のイメージには入ってませんでした。とても不具合くさいです。

Container jobs で最新の .NET Core SDK を使う

タスクを使って .NET Core SDK をインストールする以外に、公式提供されている SDK の Docker Image と Container jobs を使って、最新の SDK でビルドする方法もあります。

こっちの方がタスクはシンプルに保てますが、Docker Image のサイズと必要なプラットフォームと相談という感じです。Windows が必要な場合は時間的に厳しいです。

trigger:
- master

pool:
  vmImage: 'ubuntu-latest'

container: 'mcr.microsoft.com/dotnet/core/sdk:2.2'

steps:
- task: DotNetCoreCLI@2
  inputs:
    command: 'build'
    projects: '**/*.csproj'
    arguments: '-c Release'

Ubuntu の場合は下手に .NET Core SDK をインストールするよりも、処理時間を短縮できそうです。

最新の .NET Core SDK 2.2.x な Docker Image を使うので、もちろん正常にビルドが行えます。

f:id:shiba-yan:20190816014449p:plain

Docker Image にはパッケージキャッシュが含まれているので、手動で .NET Core SDK をインストールするのに比べて、NuGet パッケージの復元にかかる時間が少なくて済みます。

本来なら Hosted Agent に最新の SDK がインストールされていて、そのままの状態で MSBuild 16.x 系を使ったビルドが行えるのが理想ですが、暫くは各自対策する必要がありそうです。