CircleCIを使って.NET CoreアプリをWin/Linux/Mac向けに並行ビルドしてみた

# はじめに
2020年6月から縁あって[CircleCI](https://circleci.com/ja/)という会社でDeveloper Advocateを拝命しています 舟木将彦 と申します。そのCircleCIですが、ウェブサービスやモバイルアプリのビルド自動化(CI/CD)に関しては、こちらQiitaにもさまざまな体験例を投稿いただいているのですが(投稿して下さった皆さま、ありがとうございます:grinning:)、.NET系の例はあまりないのが現状ということでやってみました。

# 作ってみたもの
コマンドラインで指定した数字までの[フィボナッチ数](https://ja.wikipedia.org/wiki/%E3%83%95%E3%82%A3%E3%83%9C%E3%83%8A%E3%83%83%E3%83%81%E6%95%B0)の総和を表示するだけのプログラムです。ちなみに、フィボナッチ数列をプロットすると~~アンモナイトではなくて~~、Sybaseのマークになります。
![Sybase](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/19248/7407fcd6-d57a-3b54-1030-9930803c77ca.jpeg)
リポジトリはGitHub上にありますので、そちらをご覧ください([https://github.com/mfunaki/FiboNetApp](https://github.com/mfunaki/FiboNetApp))。

## ライブラリコード
フィボナッチ数に関わる処理を定義した`Fibo`クラス中を作成し、引数に指定された数までのフィボナッチ数の総和を求める`Compute`メソッドを定義したごくごく単純なコードです。

```cs:FiboClass.cs
using System;

namespace MyLibrary
{
    public class Fibo
    {
        public long Compute(int n)
        {
            if (n < 2)
            {
                return n;
            }
            return Compute(n - 1) + Compute(n - 2);
        }
    }
}
```

## テストコード
Xunitを使ったごくごく簡単なユニットテストを定義したコードを用意しました。

```cs:FiboLibraryTest.cs
using System;
using Xunit;
using MyLibrary;

namespace ClassLibraryTest
{

    public class FiboLibraryTest
    {
        private readonly Fibo fibo;
        
        public FiboLibraryTest()
        {
            fibo = new Fibo();
        }

        [Fact]
        public void TestCompute()
        {
            Assert.Equal(55, fibo.Compute(10));
            Assert.Equal(6765, fibo.Compute(20));
        }
    }
}
```

## コマンド実行用コード
こちらはコマンドラインから引数に渡した数までのフィボナッチ数の総和を求め、出力するためのラッパーです。

```cs:FiboApps.cs
using System;
using MyLibrary;

class Program
{
    static void Main(string[] args)
    {
        int num;
        num = int.Parse(args[0]);

        Fibo fibo = new Fibo();
        long result = fibo.Compute(num);
        Console.WriteLine(result);
    }
}
```

## CircleCIのコンフィグファイル(`config.yml`)
先に紹介したソースコードは Hello World的にごく簡単なものでしたが、ビルド、テスト、リリースを自動化するためのコンフィグ定義(`config.yml`ファイル)の内容を紹介していきます。windows ORBを使うことで全体の記述を簡潔にしています。

```yaml:config.yml
version: 2.1

orbs:
  win: circleci/windows@2.4.0
```

次に、ビルド(`build`)、テスト(`test`)、およびリリース(Windows向けは`release-win10-x64`、Linux向けは`release-linux-x64`、Mac向けは`release-osx1014-x64`)の合計3種類、5つのジョブを定義しています。

### ビルド
まずは、ビルドです。今回は(テストで使っているxUnit以外に)パッケージは使っていないのですが、パッケージを定義した *.csproj のチェックサムをキーに nugetのキャッシュをCircleCI側でキャッシュするようにしています。

```yaml:config.yml
jobs:
  build:
    executor: win/default
    steps:
      - checkout
      - restore_cache:
          keys:
            - dotnet-packages-v3-{{ checksum "FiboLibraryTest/FiboLibraryTest.csproj" }}
      - run:
          command: dotnet build
          shell: cmd.exe
      - save_cache:
          paths:
            - C:¥Users¥circleci¥.nuget¥packages
          key: dotnet-packages-v3-{{ checksum "FiboLibraryTest/FiboLibraryTest.csproj" }}
```

### テスト

次にテストです。

```yaml:config.yml
  test:
    executor: win/default
    steps:
      - checkout
      - restore_cache:
          keys:
            - dotnet-packages-v3-{{ checksum "FiboLibraryTest/FiboLibraryTest.csproj" }}
      - run:
          command: dotnet test
          shell: cmd.exe
```

### リリース

最後にリリースです。3プラットフォーム間でリリースに関わる記述(`dotnet publish`の実行、および`fibo`実行ファイルの保存)は「ほとんど同じ」なので、`release`コマンドとして切り出しておきます。引数に`target`にプラットフォーム(`win10-x64`, `linux-x64`, `osx.10.14-x64`)を渡して呼び出せば、渡した引数をパラメタ(`parameters.target`)として、作成された`fibo`コマンド(`fibo`や`fibo.exe`)がアーティファクト中に保存されます。

```yaml:config.yml
commands:
  release:
    parameters:
      target:
        type: enum
        enum: ['win10-x64', 'linux-x64', 'osx.10.14-x64']
    steps:
      - checkout
      - restore_cache:
          keys:
            - dotnet-packages-v3-{{ checksum "FiboLibraryTest/FiboLibraryTest.csproj" }}
      - run:
          command: dotnet publish -c Release -r << parameters.target >>
          shell: cmd.exe
      - store_artifacts:
          path: fibo/bin/Release/netcoreapp3.1/<< parameters.target >>/publish
```

その上で、ターゲットプラットフォームごとのリリースジョブを、先ほど定義した`release`コマンドのパラメタにプラットフォーム名を渡して呼び出すことで、再利用性が高い形で実装します(今回はやっていませんが、プラットフォームだけでなく.NETのバージョン違いに対応したリリースを複数用意することも簡単ですし、それに合わせてテストも.NETのバージョン違い、プラットフォームの違い、Debug/Releaseビルドの違いに合わせた形で「簡潔」に定義することができます)。

```yaml:config.yml
  release-win10-x64:
    executor: win/default
    steps:
      - release:
          target: win10-x64
  release-linux-x64:
    executor: win/default
    steps:
      - release:
          target: linux-x64
  release-osx1014-x64:
    executor: win/default
    steps:
      - release:
          target: osx.10.14-x64
```

### ワークフロー

ここまでに定義してきたジョブ(ビルド、テスト、リリース)を使って、ワークフローを定義します。

```yaml:config.yml
workflows:
  version: 2
  workflow:
    jobs:
      - build
      - test:
          requires: 
            - build
      - release-win10-x64:
          requires:
            - test
      - release-linux-x64:
          requires:
            - test
      - release-osx1014-x64:
          requires:
            - test
```

ポイントは`requires`を使って、テスト(`test`)実行の前提にはビルド(`build`)が成功していなければならず、各プラットフォーム向けのリリース(`release-win10-x64`, `release-linux-x64`, `release-osx1014-x64`)実行の前提にはテスト(`test`)が成功していなければならない反面、これら3つのリリース間には相互依存関係はありません(順番に実行しなくても、並行して実行すればよい)。

ですから、このワークフローを実行しているときのCircleCIの画面上はこのようにリリース作業が並列に実行されている、つまり3倍速で進んでいることがわかるかと思います。

![workflow](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/19248/ac023674-c0fc-6d26-6ad8-7bde9d0bdfac.jpeg)

## 各プラットフォームで実行
Windows, Linux, および macOS向けの実行ファイルは、前述の画面の`release-linux-x64`などと書かれた箱をクリックしていただき、ARTIFACTSタブをクリックしていただくと、ダウンロード可能です。

![ARTIFACTS](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/19248/c0dc37da-5a0e-2714-5d6c-ea2c41ce3c9c.jpeg)

なお、実行結果はこんな感じです。

|win10-x64|linux-x64|osx1014-x64|osx1014-x64 on M1|
|---------|---------|-----------|-----------------|
|![win10-x64](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/19248/e309fa0d-2027-707c-af9c-3598d7e69c7a.jpeg)|![linux-x64](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/19248/100c3d9c-9964-2df0-86a4-881bf81030a9.jpeg)|![スクリーンショット 2020-12-02 4.11.21.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/19248/fb83a402-8e92-3421-404e-449c08e1f5b1.jpeg)|![osx1014-x64 on M1](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/19248/2158fee8-b407-01d4-14e1-dab86beb608f.png)

# 終わりに
仕事の効率を上げるようなちょっとしたコマンドラインのツールを作るには、C#も悪くないな、というのが今回の感想です。GUIが必要であれば、Electronなど、一度の開発でより幅広い人たちに使っていただくには、プラットフォームの選択と、ビルド〜テスト〜リリース(配布)の自動化が重要です。
CIがみなさまの開発をより楽しく、速く、そして品質を支えることを願っています！(「やってみた」報告をぜひ、Twitterでもハッシュタグ #CircleCIJp をつけてつぶやいてくださいませ)