Chocolateyのパッケージを作る

Windowsの環境を作るとき zipファイルの展開とパスを通すとか、何回もやっていると面倒臭い
Chocolateyのパッケージがあれば便利！だけど、パッケージが無い場合自分で作るしかない
で、その作り方のメモです。

ここで作ったpackageはgithubに置いてあります
https://github.com/yasuoohno/chocolatey-packages
※ 不要ファイル等の削除等をちゃんとしていないので、散らかってます。すいません。

## 更新情報

- 2021/1/26  
auto shimmingの項に、他のアプリケーションと共有したい DLLがパッケージに含まれている場合の注意を追記しました。

## 題材

[winlibs.com](http://winlibs.com/)の mingw-w64をパッケージ化してみる。

パッケージ名は `winlibs-mingw-llvm` にします。

## インストール作業の中身

このインストールを手でやる場合の作業は以下の通りです。

1. [winlibs-x86_64-posix-seh-gcc-10.1.0-llvm-10.0.0-mingw-w64-7.0.0-r3.7z](https://github.com/brechtsanders/winlibs_mingw/releases/download/10.1.0-10.0.0-7.0.0-r3/winlibs-x86_64-posix-seh-gcc-10.1.0-llvm-10.0.0-mingw-w64-7.0.0-r3.7z)をダウンロードする
2. ダウンロードしたファイルをどこかのフォルダに展開する
3. 展開したフォルダのmingw64\binにPATHを通す

ここで作るChocolateyパッケージは以下の事を自動的に行います。

1. %ChocolateyInstall%\lib\以下にパッケージディレクトリが作られる  
   ここの題材でデフォルトだと C:\ProgramData\chocolatey\winlibs-mingw-llvm\
2. パッケージディレクトリのtoolsに chocolateyinstall.ps1, chocolateyuninstall.ps1 が展開
3. chocolateyinstall.ps1が実行される
4. リリースバイナリがダウンロードされてハッシュの確認が行われる
5. リリースバイナリを toolsに展開
6. 展開した mingw64\bin を環境変数PATHに設定する

## 下準備

1. Chocolateyのインストール
2. AUのインストール  
   [Chocolatey Automatic Package Updater Module](https://chocolatey.org/packages/au)

AUは あまり覚えていませんが、`choco install au` で いけたと思います。

## 1. githubで templateを forkする

まずは以下を github上で forkする。

[chocolatey-packages-template](https://github.com/chocolatey-community/chocolatey-packages-template)

## 2. local に git cloneする

私は `%USERPROFILE%\work` にいつもcloneしているのでこんな感じ

```cmd
cd /d "%USERPROFILE%"
mkdir /p work
cd work
git clone "https://github.com/[YOUR_GITHUB_ID]/chocolatey-packages.git"
```

## 3. automatic 配下にパッケージのフォルダを作る

以後の作業は作成したパッケージフォルダの下で行います。

```cmd
cd /d "%USERPROFILE%\work\chocolatey-packages\automatic"
mkdir winlibs-mingw-llvm
cd winlibs-mingw-llvm
```

automatic フォルダにはデフォルトでいくつかのパッケージが存在しています。
不要なので `git rm -r`等で消してしまってかまいません。

## 4. winlibs-mingw-llvm.nuspecを書く

パッケージ名.nuspecというファイルを__UTF-8__で作成します。BOMは無くてOK。
7zや zip を展開してパスを通す程度であれば書く場所は多くありません。
パッケージを公開する気がないのであれば id, version それと filesぐらいです。

idはパッケージ名を設定します。

versionは後で自動的に埋めるので、今は わかっていても__0.0__で設定してください。

filesは パッケージディレクトリ= `C:\ProgramData\Chocolatey\lib\[PackageName]` にコピーするファイルを書きます。
とりあえず今はtoolsの下にあるInstall/Uninstall用スクリプトをコピーするようにしてあります。

```xml:winlibs-mingw-llvm.nuspec
<?xml version="1.0" encoding="utf-8"?>
<package xmlns="http://schemas.microsoft.com/packaging/2015/06/nuspec.xsd">
  <metadata>
    <id>winlibs-mingw-llvm</id>
    <version>0.0</version>
    <title>WinLibs standalone build of GCC,LLVM and MinGW-w64 for Windows</title>
    <authors>Brecht Sanders</authors>
    <projectUrl>http://winlibs.com/</projectUrl>
    <tags>compiler gcc mingw mingw-w64 llvm</tags>
    <summary>It's a free C and C++ compiler for Microsoft Windows.</summary>
    <description>This is a standalone build, which means this download offers a complete compiler environment for Windows.</description>
  </metadata>
  <files>
    <file src="tools\**" target="tools" />
  </files>
</package>
```

## 5. tools\chocolateyinstall.ps1 を書く

次にインストールを処理するスクリプトを書きます。__UTF-8 BOM付き__です。
インストール時の処理を色々追加したい場合はこのファイルを編集します。

```powershell:tools\chocolateyinstall.ps1
$ErrorActionPreference = 'Stop'
$packageName           = "$env:ChocolateyPackageName"
$toolsDir              = "$(Split-Path -parent $MyInvocation.MyCommand.Definition)"
$binPath               = "$toolsDir\mingw64\bin"

$packageArgs = @{
  packageName    = $packageName
  unzipLocation  = $toolsDir
  url64bit       = ''
  checksum64     = ''
  checksumType64 = 'sha256'
}

Install-ChocolateyZipPackage @packageArgs

#
# Disable auto shimming.
#
$files = get-childitem $toolsDir -include *.exe -recurse

foreach ($file in $files) {
  #generate an ignore file
  New-Item "$file.ignore" -type file -force | Out-Null
}

#
# add path
#
Install-ChocolateyPath -PathToInstall "$binPath" -PathType 'Machine'
```

説明

1. packageName変数にパッケージ名を取得
2. toolsDir変数に このスクリプトがインストールされたディレクトリパスを取得
3. binPathは後でパスを通すフォルダの設定
4. installするパッケージのパラメータをpackageArgsに設定  
   このとき、url64bitやchecksum64は後で自動設定するので '' で良いです。
5. Zipパッケージをダウンロードしてインストールする
6. .exeのwrapperを作成を止めるために .exe ファイルを検索して .exe.ignoreというファイルを作成しておく
7. パスを追加する

## 6. tools\chocolateyuninstall.ps1 を書く

続いてアンインストール時の処理です。こちらも__UTF-8 BOM付き__です。

```powershell:tools\chocolateyuninstall.ps1
$ErrorActionPreference = 'Stop'
$packageName           = "$env:ChocolateyPackageName"
$toolsDir              = "$(Split-Path -parent $MyInvocation.MyCommand.Definition)"
$binPath               = "$toolsDir\mingw64\bin"

$PathToRemove = $binPath
foreach ($path in [Environment]::GetEnvironmentVariable("PATH","Machine").split(';'))
{
  If ($Path)
  {
    If (($path -ine "$PathToRemove") -AND ($path -ine "$PathToRemove\"))
    {
      [string[]]$Newpath += "$path"
    }
  }
}
$AssembledNewPath = ($newpath -join(';')).trimend(';')

;[Environment]::SetEnvironmentVariable("PATH",$AssembledNewPath,"Machine")
```

最後の行の行頭の;ですが、本来は不要です。行頭が\[で始まっていると Qiitaのmarkdownの処理で消えてしまうので仕方なく入れてあります。なんでじゃろ。Footerのリンク扱いになってるんですかね？ code block内で escapeする方法がわからんです……。

処理としては toolsDirは勝手に削除されるので インストール時に設定した環境変数を戻しています。
Install-ChocolateyPathの逆をする関数は無いので [Issue](https://github.com/chocolatey/choco/issues/310)で紹介されていたスクリプトで処理しています。

## 7. update.ps1を書く

Automatic Updateモジュールを使って nuspecと installスクリプトを自動更新するためのスクリプトを書きます。

```powershell:update.ps1
import-module au

$latest_url = 'https://github.com/brechtsanders/winlibs_mingw/releases/latest'

function global:au_SearchReplace {
  @{
    ".\tools\chocolateyInstall.ps1" = @{
      "(?i)(^\s*url64bit\s*=\s*)('.*')" = "`$1'$($Latest.URL)'"
      "(?i)(^\s*checksum64\s*=\s*)('.*')" = "`$1'$($Latest.Checksum64)'"
    }
  }
}

function global:au_GetLatest {
  $download_page = Invoke-WebRequest -UseBasicParsing -Uri $latest_url
  $url = $download_page.Links | ? href -match 'winlibs-x86_64-posix-seh-gcc-[\d.]+-llvm-[\d.]+-mingw-w64-[\d.]+-r[\d]+\.7z$' | % href | select -First 1
  $url = 'https://github.com' + $url
  $fn = $url -split '/' | select -Last 1
  $release = $fn -split '-' | select -Skip 11 -First 1
  $release = $release -replace 'r([\d]+)\.7z$','$1'
  $version = $fn -split '-' | select -Skip 5 -First 1

  @{
    URL = $url
    Version = $version + '.' + $release
  }
}

update -ChecksumFor 64
```

説明

1. auモジュールのimport
2. 最新版のダウンロードURLやバージョンを取得するためのURLの設定
3. インストールスクリプト等の中に含まれる、ダウンロードURLとチェックサムを書き換える関数を定義
4. 最新版のVersionとダウンロードURLを取得するための関数を定義
5. 更新処理の実行

## 8. 最新版をチェックして更新

ここまでで以下のような状態になっているはずです。

```text
winlibs-mingw-llvm
|   update.ps1
|   winlibs-mingw-llvm.nuspec
\---tools
        chocolateyinstall.ps1
        chocolateyuninstall.ps1
```

powershell上で update.ps1を実行します。

```cmd
powershell .\update.ps1
```

正常に実行されると以下のように nupkgが作成され、さらにインストールスクリプト等が更新されます。

```text
C:\Users\USERNAME\work\chocolatey-packages\automatic\winlibs-mingw-llvm>powershell .\update.ps1
winlibs-mingw-llvm - checking updates using au version 2019.5.22

警告: Invalid nuspec file Version '' - using 0.0
URL check
  https://github.com/brechtsanders/winlibs_mingw/releases/download/10.1.0-10.0.0-7.0.0-r3/winlibs-x86_64-posix-seh-gcc-10.1.0-llvm-10.0.0-mingw-w64-7.0.0-r3.7z
nuspec version: 0.0
remote version: 10.1.0.3
New version is available
Automatic checksum started
Downloading chocolatey\winlibs-mingw-llvm 64 bit
  from 'https://github.com/brechtsanders/winlibs_mingw/releases/download/10.1.0-10.0.0-7.0.0-r3/winlibs-x86_64-posix-seh-gcc-10.1.0-llvm-10.0.0-mingw-w64-7.0.0-r3.7z'

Download of winlibs-x86_64-posix-seh-gcc-10.1.0-llvm-10.0.0-mingw-w64-7.0.0-r3.7z (142.32 MB) completed.
Package downloaded and hash calculated for 64 bit version
Updating files
  $Latest data:
    Checksum64                (String)     376354566491b56b87ff2bc9a28971902967229a51d541293c75f53db3f490fb
    ChecksumType64            (String)     sha256
    FileType                  (String)     7z
    NuspecVersion             (String)     0.0
    PackageName               (String)     winlibs-mingw-llvm
    URL                       (String)     https://github.com/brechtsanders/winlibs_mingw/releases/download/10.1.0-10.0.0-7.0.0-r3/winlibs-x86_64-posix-seh-gcc-10.1.0-llvm-10.0.0-mingw-w64-7.0.0-r3.7z
    Version                   (String)     10.1.0.3
  winlibs-mingw-llvm.nuspec
    setting id: winlibs-mingw-llvm
    updating version: 0.0 -> 10.1.0.3
  .\tools\chocolateyInstall.ps1
    (?i)(^\s*checksum64\s*=\s*)('.*')   = $1'376354566491b56b87ff2bc9a28971902967229a51d541293c75f53db3f490fb'
    (?i)(^\s*url64bit\s*=\s*)('.*')     = $1'https://github.com/brechtsanders/winlibs_mingw/releases/download/10.1.0-10.0.0-7.0.0-r3/winlibs-x86_64-posix-seh-gcc-10.1.0-llvm-10.0.0-mingw-w64-7.0.0-r3.7z'
Attempting to build package from 'winlibs-mingw-llvm.nuspec'.
Successfully created package 'C:\Users\USERNAME\work\chocolatey-packages\automatic\winlibs-mingw-llvm\winlibs-mingw-llvm.10.1.0.3.nupkg'
Package updated

Path          : C:\Users\USERNAME\work\chocolatey-packages\automatic\winlibs-mingw-llvm
Name          : winlibs-mingw-llvm
Updated       : True
Pushed        : False
RemoteVersion : 10.1.0.3
NuspecVersion : 0.0
Result        : {winlibs-mingw-llvm - checking updates using au version 2019.5.22, , URL check,   https://github.com/br
                echtsanders/winlibs_mingw/releases/download/10.1.0-10.0.0-7.0.0-r3/winlibs-x86_64-posix-seh-gcc-10.1.0-
                llvm-10.0.0-mingw-w64-7.0.0-r3.7z...}
Error         :
NuspecPath    : C:\Users\USERNAME\work\chocolatey-packages\automatic\winlibs-mingw-llvm\winlibs-mingw-llvm.nuspec
NuspecXml     : #document
Ignored       : False
IgnoreMessage :
StreamsPath   : C:\Users\USERNAME\work\chocolatey-packages\automatic\winlibs-mingw-llvm\winlibs-mingw-llvm.json
Streams       :

C:\Users\USERNAME\work\chocolatey-packages\automatic\winlibs-mingw-llvm>
```

## 9. ローカルからインストールして確認

管理者権限でPowerShellを開いて automaticのフォルダに行ってからchocoを叩きます。

```powershell
cd C:\Users\USERNAME\work\chocolatey-packages\automatic
choco install winlibs-mingw-llvm -s .
```

## 補足 : インストール先について

今回はパッケージディレクトリにリリースバイナリを展開しています。

アンインストール時、パッケージディレクトリは自動的に削除されるため、
この方針ではアンインストール処理が簡単になります。

ただ、インストールした場所を PATHを追加したい場合

- PATHの設定値が長くなる
- auto shimmingを回避する必要がある

といったデメリットがあります。

パッケージディレクトリ下ではなく、%ChocolateyToolsLocation%に展開する方法もありますが、
その場合Uninstallスクリプトの中で削除する必要があります。

ChocolateyToolsLocationへの展開と削除について 以下の二つのヘルパーが使えます。

https://chocolatey.org/docs/helpers-get-tools-location
https://chocolatey.org/docs/helpers-uninstall-chocolatey-zip-package

ChocolateyToolsLocationへのインストールは以下のchocotoolsブランチにソースがあります。
インストール場所を変更しているのと、Uninstall側でZipPackageの削除を行っています。

https://github.com/yasuoohno/chocolatey-packages/tree/chocotools/automatic

## 補足 : auto shimmingについて

https://docs.chocolatey.org/en-us/features/shim/

インストール後、パッケージディレクトリの中に実行ファイルがあっても、
そのままでは PATHを設定しない限り それらの実行ファイルをコマンドライン等から自動的に見つける事が出来ません。

Chocolatey自身のインストール時に C:\ProgramData\chocolatey\binが PATHに追加されています。
PATHを変更せずに実行ファイルを見つけ出せるようにするために
Chocolateyでは パッケージのインストール後、パッケージディレクトリ内に .exeファイルを見つけると、
自動的に同じ名前の Wrapperファイルを このディレクトリに作ってくれる機能があります。

このおかげで簡単なパッケージはPATHに追加することなく、実行ファイルを見つけ出す事が
できるようになっています。

### 2021/1/26追記

今回題材にした様な .exeを出力するような開発ツールでは
パッケージ内に共有DLL (例えば bin\*.dll) が存在することがあります。

auto shimmingにより作られた wrapperは 元の.exeを呼び出すので、
それと同一フォルダに格納された dllについては 特に問題無く実行時にリンク可能です。

参考: [DLLの検索順](https://docs.microsoft.com/ja-jp/windows/win32/dlls/dynamic-link-library-search-order?redirectedfrom=MSDN)

ただし、ツールにより生成された .exeが それらの .dllとリンクされている場合、
PATH内に .dllが存在しないためリンクが出来ず開発時に実行時エラーとなる可能性があり ちょっと不便です。

この記事の場合、.exeファイルと同じ場所に .exe.ignoreという空ファイルを作る事でshimを作らせない様にし、
更に PATHにフォルダを追加する事でこの問題を回避しています。

## 最後に

とりあえず、これで zipファイルをインストールする手間が 少しは減らせます。
メンテナになる根性があれば community feedに pushすると他の方が幸せになれるかもしれません。