.NET Framework デスクトップアプリの自動ビルドを Azure Pipelines 上で流す

日々 Windows デスクトップアプリの開発に勤しんでいます💦

『Windows アプリは自分のパソコンでビルドするモンだ！』
『ビルド環境の構築は一苦労』
という固定観念から抜けてみたく、試行錯誤していました。

結論的には

- Azure Pipelines (Azure DevOps) を利用すれば、.NET Framework デスクトップアプリの自動ビルドは可能
- GitHub の Releases にリリースを作成可能 (執筆時点では GitHub Release は PREVIEW 扱い)
- ビルドごとに Arfifacts を残すことが可能
- signtool で署名することも可能 (signtool は持ち込みしました)
- CodeSign.pfx ファイルを格納する手立ても Azure DevOps にあります
- CodeSign.pfx ファイルのパスワードを格納する手立ても Azure DevOps にあります
- PowerShell, NSIS, MinGW などが使えます
- VM の細かいスペックは [Azure Pipelines Hosted VS2017 image](https://github.com/Microsoft/azure-pipelines-image-generation/blob/master/images/win/Vs2017-Server2016-Readme.md) のページが詳しいです

では大雑把に見ていきます

## GitHub の Releases にリリースを作成可能

Note: つぎのトピック `ビルドごとに Arfifacts を残すことが可能` で Artifacts が生成されていることをあてにしています。

対象プロジェクトを選択
1. `Pipelines` クリック
2. `Releases` クリック
3. `+ New` クリック
4. `New release pipeline` クリック

![image.png](https://qiita-image-store.s3.amazonaws.com/0/364126/4cf4c0ef-8b89-a559-64df-f8bd97eb018f.png)

さらに、

1. `Empty job` で

![image.png](https://qiita-image-store.s3.amazonaws.com/0/364126/d0c8ebc9-d3ec-825c-a58a-7809b9ef8ffb.png)

さらに

1. `Stage 1` をクリック
2. `Tasks` タブをクリック

![image.png](https://qiita-image-store.s3.amazonaws.com/0/364126/0b585f39-80bc-59c3-d249-aee786626c24.png)

さらに、

1. `Agent job` の `+` をクリック
2. `Utility` タブをクリック
3. `GitHub Release` を捜してきて、クリックして、`Add` をクリック

![image.png](https://qiita-image-store.s3.amazonaws.com/0/364126/0f4b45d8-4a47-a2e4-130a-5ffe12689e7c.png)

さらに、

1. `GitHub release (create)` をクリック。右側にプロパティ一覧らしきブレードが出てくるので埋めていきます
2. `GitHub connection (OAuth or PAT)*` は、～ 埋めてください。
3. `Repository*` も、～ 埋めてください。
4. `Action*` は `Create` のままで
5. `Target*` は `$(Build.SourceVersion)` のままで
6. `Tag source*` は **`$(Release.ReleaseName)`** にすると、`Release-1` のような名前が付きます
7. `Release title` は `$(Release.ReleaseName)` にしました
8. `Release notes source` は `Inline release notes` にしました
9. `Release notes` は `by DevOps $(Release.ReleaseWebURL)` にしました
10. `Assets` は `$(System.DefaultWorkingDirectory)/_COMPANYNAME.PROJECTNAME/drop/*` のようにしました。
  1. `_COMPANYNAME.PROJECTNAME` を何にすべきかは、2 つ下の画像の (1) `Source alias *` を参考に
  2. `drop` は、のちほど指定する `artifactName` です
11. `Draft release` はオフ (これは好みで)
12. `Pre-release` はオン (これも好みで)
13. `Add changelog` はオフ (これも好みで)
14. 上部の `Save` で保存

![image.png](https://qiita-image-store.s3.amazonaws.com/0/364126/b623b856-5ef2-9024-2ea7-1d21ce9bb9c3.png)

![image.png](https://qiita-image-store.s3.amazonaws.com/0/364126/54191b86-b8b6-f43a-80dd-81bbcd301550.png)

続きはまた今度

## ビルドごとに Arfifacts を残すことが可能

`azure-pipelines.yml` を編集します。

```yml
- task: CopyFiles@2
  inputs:
    contents: 'Setup_*.exe'
    targetFolder: $(Build.ArtifactStagingDirectory)

- task: PublishBuildArtifacts@1
  inputs:
    pathtoPublish: '$(Build.ArtifactStagingDirectory)' 
    artifactName: 'drop' 
```

成果物を `CopyFiles@2` タスクで `$(Build.ArtifactStagingDirectory)` へコピーして、
`PublishBuildArtifacts@1` タスクを実行します。

`artifactName: 'drop'` は、ビルド結果画面右上の `Artifacts` をプルダウンした時にリストされる名称です。下図参照

![image.png](https://qiita-image-store.s3.amazonaws.com/0/364126/57c2a1cb-201b-f837-7839-e1abb7c16b3a.png)

## signtool で署名することも可能 (signtool は持ち込みしました)

### MySign というツール

わたしは `MySign` という実行ファイル署名用のコマンドラインツールを自作しています。

使用感 `MySign a.exe b.dll`

署名しない環境では空の `MySign.bat` を作成します。

自分のパソコンでは、パスの通っている場所に GUI ツールの `MySign.exe` を設置しています。

バックグラウンドタスクで署名が必要な環境では、つぎのような内容で `MySign.bat` を書くと良いかもしれません。

```bat
%~dp0\signtool.exe sign /tr "http://rfc3161-url" /fd sha256 /n "COMPANY NAME" %*
```

フォルダ構成：

```text
ソースフォルダ
- BuildTools フォルダ
  - signtool フォルダ
    - MySign.bat
    - signtool.exe
```

### NSIS でも活用可能

わたしは NSIS を多用をするので `MySign` が必須です。

```text
!system 'MySign "bin\DEBUG\${APP}.exe"'
!finalize 'MySign "%1"'
```

`!system` でビルド後のファイルに署名を施し、
`!finalize` でセットアップファイルに署名を施します。

### デプロイしたい

`azure-pipelines.yml` を編集します。

`CopyFiles@2` で、`MySign.bat` と `signtool.exe` をソースフォルダにコピーしてきます。

```yml
- task: CopyFiles@2
  inputs:
    contents: 'BuildTools/signtool/*'
    targetFolder: '.'
    overWrite: true
    flattenFolders: true
```

## CodeSign.pfx ファイルを格納する手立て

**Note: pfx ファイルはエクスポートする際に、暗号化を `TripleDES-SHA1` に設定しておいた方が良いでしょう。** `AES256-SHA256` を選択すると、証明書をインポートできない、という問題が発生しました。この選択欄が存在しない場合は無視で問題ありません。

Azure DevOps でプロジェクトを開きます。
1. 左端の `Pipelines` を選択
2. `Library` をクリック
3. `Secure files` タブをクリック
4. `+ Secure file` をクリックします
5. CodeSign.pfx などをアップロードします

操作手順です

![image.png](https://qiita-image-store.s3.amazonaws.com/0/364126/3b86e1e0-2218-1fbe-d431-14c14d75976c.png)

ひとまずわたしは `Authorize for use in all pipelines` にチェックを入れておきました。

## CodeSign.pfx ファイルのパスワードを格納する手立て

Azure DevOps でプロジェクトを開きます。
1. 左端の `Pipelines` を選択
2. `Library` をクリック
3. `Variable groups` タブをクリック
4. `+ Variable group` をクリックします

![image.png](https://qiita-image-store.s3.amazonaws.com/0/364126/37b13ffc-e546-95eb-46f3-eb1ae6ad519d.png)

さらに、

1. `Variable group name` に適当な名称を設定。
2. `+ Add` をクリック
3. `Name` に `variable name` を入力。`PFX-PASSWD` にすると `$(PFX-PASSWD)` のように参照できます。
4. `Value` にパスワードを入力。
5. 南京錠をクリックして、`Value` を保護します。保護というのはわたしの認識では、
  1. Azure Pipelines 上では表示できなくする
  2. ビルドログについても `***` などで隠蔽される
6. `Save` で保存

![image.png](https://qiita-image-store.s3.amazonaws.com/0/364126/1a7d6f8a-b0f6-9734-0cb5-fcd09af2195b.png)

## Builds で CodeSign.pfx を使用する場合の準備

Builds の場合の `Variable group` 割り当て方法

1. 左端の `Pipelines` を選択
2. `Builds` をクリック
3. 対象のパイプラインをクリック
4. `Edit` をクリック

![image.png](https://qiita-image-store.s3.amazonaws.com/0/364126/9accf52d-edee-9742-1f31-0b7cdf4600ff.png)

さらに、

1. 右上の `...` をクリック
2. `Variables` をクリック

![image.png](https://qiita-image-store.s3.amazonaws.com/0/364126/d2da3bec-e7c3-0d91-f55c-39bf497b1ad1.png)

さらに、

1. `Variable groups` をクリック
2. `Link variable group` をクリック

![image.png](https://qiita-image-store.s3.amazonaws.com/0/364126/10203811-d4db-ca23-88ae-db269577b03f.png)

さらに、

1. `Sign.pfx.passwd` 選択
2. `Link` クリック

![image.png](https://qiita-image-store.s3.amazonaws.com/0/364126/450877cb-a3a9-40c2-3d43-eb23979ab87d.png)

さらに

1. `Save & queue` クリック
2. `Save` クリック
3. `Save` クリック

![image.png](https://qiita-image-store.s3.amazonaws.com/0/364126/28737b1f-364e-c2fd-a1e9-9f5bf3fd20f0.png)

これで `Sign.pfx.passwd` を使用する準備が整ったと思います。

`azure-pipelines.yml` を編集

```yml
steps:
- task: DownloadSecureFile@1
  inputs:
    secureFile: CodeSign.pfx

- powershell: |
    $secureString = ConvertTo-SecureString $env:MySecret -AsPlainText -Force
    Import-PfxCertificate -Exportable -FilePath $env:DOWNLOADSECUREFILE_SECUREFILEPATH -CertStoreLocation 'Cert:\CurrentUser\My' -Password $secureString
  displayName: Import pfx
  env:
    MySecret: $(PFX-PASSWD)
```

`secureFile: CodeSign.pfx` が、`Library` の `Secure Files` でアップロードしたファイル名、

`$(PFX-PASSWD)` は、 `Library` の `Variable groups` で定義した `Variables` `PFX-PASSWD` の `Value` が反映されます。

`DownloadSecureFile@1` で取得したファイルは環境変数 `DOWNLOADSECUREFILE_SECUREFILEPATH` に格納されることになっているようです。
PowerShell での参照方法は `$env:DOWNLOADSECUREFILE_SECUREFILEPATH` です。

これで証明書を VM にインストールするので、あとから signtool.exe で参照できるようになるはずです。

## signtool の動作チェックをする

パイプラインの最後の方でミスが発覚すると、消費した時間がむなしくなります…
先に動作チェックを走らせた方が良いでしょう。

```yml
- script: MySign EmptyExe.exe
  displayName: Sign test
```

EmptyExe.exe は自分用に作ったものですが、共有いたします。 https://github.com/HiraokaHyperTools/EmptyExe/releases

## chcp 65001 で動作

VM のコンソールはコードページ 65001 (UTF-8) で動作しているようです。
しかしながら `Unicode 対応ではないプログラムの言語` の設定は日本語ではありませんでした。

つまり、bat ファイルに CP932 (Shift_JIS) を使用すると、文字化けなどの問題が出ます。

bat ファイルと nsi ファイルについては CP932 → UTF-8 に文字コード変換するタスクを書きました。

```yml
- script: |
    for %%f in (*.bat) do (
      echo ##[debug]Convert: %%f
      iconv -f cp932 -t utf-8 %%f > %%f.new
      move /y %%f.new %%f
    )
    for %%f in (*.nsi) do (
      echo ##[debug]Convert: %%f
      iconv -f cp932 -t utf-8 %%f > %%f.new
      move /y %%f.new %%f
    )
  displayName: Needs to be UTF-8
```

UTF-8 に変換した bat nsi ファイルは、ビルド後に破棄されます。使い捨てです。
これらが Git レポジトリに書き戻ってくることは通常ありません。

## makensis

NSIS セットアップを作成したい。

1 つの exe に対して多数のセットアップを作成するプロジェクトなので、こうしています。

```yml
- script: |
    for %%f in (*.bat) do (
      echo ##[debug]Run: %%f
      %%f || exit /b 1
    )
  displayName: Build NSIS installers
  env:
    AZURE_PIPELINES_NSIS_OPTIONS: /INPUTCHARSET UTF8
```

`command || exit /b 1` は短絡評価の応用ですね…
`command` が成功 (ERRORLEVEL が 0) であれば、つぎの行へ。
`command` が失敗 (ERRORLEVEL が 1 以上) であれば… `||` の後の `exit /b 1` を実行。ERRORLEVEL 1 を返してビルド失敗を報告します。

`AZURE_PIPELINES_NSIS_OPTIONS: /INPUTCHARSET UTF8` は、先ほどの文字コード変換の結果、必要になった記述です。

その NSIS ビルドに使う bat ファイルは、Git レポジトリには CP932 で保存しておきます。通常の Windows 環境でも使いたいので…

```bat
"C:\Program Files (x86)\NSIS\Bin\makensis.exe" /DSPEC="顧客名" %AZURE_PIPELINES_NSIS_OPTIONS% Setup_ABC.nsi
```