Scoopを使ったWindows環境構築のススメ - Hyper!!!

この記事は連載物のひとつになっています。
前回扱った内容が前提になっているため(下に概要は載せていますが)一読しておくことをおすすめします。

守章:　[Scoopの基礎、簡単なManifestの作成](https://qiita.com/rkbk60/items/12dc8fb14042888113d0)
破章:　バージョン管理と自動アップデート　←　(今回はこれ)
離章:　Scoopを用いた環境構築する際の勘所  

今回はKaoriYa版Vimなどを題材に、Scoopでのバージョン管理を担う**checkver**属性と**autoupdate**属性について解説します。
そして今回も環境構築には触れません。公式Wikiより[App Manifest Autoupdate](https://github.com/lukesampson/scoop/wiki/App-Manifest-Autoupdate)の和訳です。

~~たぶんいないだろうけどKaoriYa版Vimのかんたん導入だけ知りたい人は前回の記事を読んだ上で[これ](https://github.com/rkbk60/scoop-for-jp)を使ってください。~~

## おさらい

前回はScoopを使うメリットと利用法を解説しました。PowerShell3.0以上があれば導入も操作も簡単です。
そして下のようなJSONをGitリポジトリに登録し公開することで、誰でも自由に拡張できることも知りました。

```ctrl2cap.json
{
    "##": "実行中にUAC昇格ポップアップが出ます。",
    "license": {
        "identifier": "Freeware",
        "url": "http://technet.microsoft.com/ja-jp/sysinternals/bb469936"
    },
    "depends": [
        "sudo"
    ],

    "version": "2.0",
    "url": "http://download.sysinternals.com/files/Ctrl2Cap.zip",
    "hash": "2d8c06374da140beda79ac1940ab2b06a56a9af182dba70a6338313d768a2ac2",

    "installer": {
        "script": [
            "sudo $dir\\ctrl2cap.exe /install",
            "if ($? == $false) {",
            "    Write-Host '===================================' -Foreground Red",
            "    Write-Host 'ctrl2capの実行に失敗しました。' -Foreground Red",
            "    Write-Host '===================================' -Foreground Red",
            "}"
        ]
    },
    "bin": "ctrl2cap.exe",
    "shortcuts": [
        [
            "ctrl2cap.exe",
            "Ctrl2cap"
        ]
    ],
    "notes": [
        "再設定をしたい場合は、以下の手順で行ってください。",
        "- スタートメニューから「Ctrl2cap」で検索、管理者権限で実行",
        "- 管理者権限のプロンプトで 'ctrl2cap /uninstall' を実行",
    ]
}
```

前回紹介したうち主要な属性の概要です。

| 属性 | 値 |
| --- | --- |
| **bin** | パスを通したいファイル |
| **depends** | 依存するManifest |
| **hash** | hash値 ※準必須属性 |
| **installer** | インストール処理 |
| **notes** | インストールの全処理完了後に出す文 |
| **shortcuts** | スタートメニューに作るショートカット |
| **url** | ダウンロードするファイル **※必須属性** |
| **version** | バージョン **※必須属性** |
| **##** | コメント |

## 補足: architecture属性、extract_dir属性

バージョン管理について知る前に、前回紹介できなかった**architecture**属性そして**extract_dir**属性について話しておきます。
以下に今回メインの題材となるKaoriYa版Vim(以降 Vim 表記)の簡単なManifestを載せておきます。

```vim-kaoriya-nogvim.json
{
    "homepage": "https://github.com/koron/vim-kaoriya",
    "description": "Vim + kaoriya build system, without gvim.",

    "version": "8.1.0005.20180520",
    "architecture": {
        "32bit": {
            "url": "https://github.com/koron/vim-kaoriya/releases/download/v8.1.0005-20180520/vim81-kaoriya-win32-8.1.0005-20180520.zip",
            "hash": "afacd6e27304136f5ebae3edddf6e747f410880fa6a986d80be772e03ef56d36",
            "extract_dir": "vim81-kaoriya-win32"
        },
        "64bit": {
            "url": "https://github.com/koron/vim-kaoriya/releases/download/v8.1.0005-20180520/vim81-kaoriya-win64-8.1.0005-20180520.zip",
            "hash": "53e8dd08e2249ce8a54784e16469151a7bf9857ac9acf3a1b341ac4e7da26fb2",
            "extract_dir": "vim81-kaoriya-win64"
        }
    },

    "bin": "vim.exe"
}
```

url/hash/extract_dirと3つの属性が**architecture**属性内の32bitおよび64bit属性に移動しています。
architecture属性を用いることで、このようにアーキテクチャ毎にリソースが異なる場合でも分離することができます。

参考までに、architecture属性によって分割できる属性は以下の通り。

- url
- hash
- bin
- shortcuts
- pre_install
- post_install
- installer
- uninstaller
- extract_dir

そして**extract_dir**属性は、使用するディレクトリを抜き取るための属性です。擬似的にルートディレクトリを変更する属性、とも言えるでしょうか。
説明のために64bit版zipファイルを展開したときのツリービューを載せておきます。

```text:tree
. 
└── vim81-kaoriya-win64
    ├── lua
    │   ├── ...
    ~
    ├── vim.exe
    ├── vimrc
    ├── vimrun.exe
    ├── winpty-agent.exe
    ├── winpty.dll
    └── xxd.exe
```

このままだとルート直下に`vim81-kaoriya-win64`しか存在していない状態となってしまい、もしvimにパスを通すならbin属性に`vim81-kaoriya-win64\\vim.exe`を渡さなければなりません。
しかしextract_dir属性に任意のディレクトリ、今回は`vim81-kaoriya-win64`を指定することで実質的にルートが変更され、bin属性の値が`vim.exe`とシンプルになるだけでなく後述するようにバージョン変更にも強くなります。

それでは、本題であるバージョン管理のための属性について見ていきましょう。

## checkver属性

checkver属性は以下の内容に対して検索をかけることで、バージョンの取得を行うための属性です。

- GitHubリリースページのURL
- HTMLファイル
- JSON

ちなみに前回解説し忘れてしまったのですが、**version**属性も含めてバージョンは英数字と`+-._`の組み合わせで構成されている必要があります。

### GitHubのリリースURLから取得

一番楽なのはGitHubの最新版リリースページのURLを見る方法です。
[nvm](https://github.com/ScoopInstaller/Main/blob/master/bucket/nvm.json)と[cmder](https://github.com/ScoopInstaller/Main/blob/master/bucket/cmder.json)がこれを採用しているので見てみましょう。

```nvm.json
"homepage": "https://github.com/coreybutler/nvim-windows",
"checkver": "github",
```

```cmder.json
"homepage": "https://cmder.net",
"checkver": {
    "github": "https://github.com/cmderdev/cmder"
},
```

ご覧の通り、homepage属性にGitHubリポジトリを当てているかで表記が変わります。
当てている場合はcheckver属性に`github`を、そうでない場合はgithub属性を持ったオブジェクトを当てます。処理内容はどちらも同じで、`https://github.com/USERNAME/REPONAME/releases/latest`のリダイレクト先URLにあるタグからバージョンを取得します。

ちなみにこの方法が使えるのは、

- プレリリース版ではない
- タグが`^v?[\d.$]+$`の正規表現に適合する

これら2点を満たしている場合のみです。限定的。

### HTMLから取得

つづいて、HTMLの中から探す方法を見てみましょう。
一番よく行われているのはダウンロードするファイル名を検索する方法です。
たとえば[git](https://github.com/ScoopInstaller/Main/blob/master/bucket/git.json)のcheckverは以前こんな感じになっていました。

```git.json
"checkver": {
    "url": "https://github.com/git-for-windows/git/releases/latest",
    "re": "v(?<version>[\\d\\w.]+)/PortableGit-(?<short>[\\d.]+).*\\.exe"
},
```

**url**属性に対象となるURLを指定するのですが、指定したHTMLにバージョン情報しか記載されていないことはまずないので、**re**(もしくは**regex**)属性も付け加えます。こうすることで得られるHTMLから正規表現を用いて情報を抜き出すことができます。

この正規表現にはちょっとした拡張があり、丸括弧中の先頭に`?<VAR_NAME>`という接頭辞をつけることで、一致した内容に名前をつけることができます。ここで`version`という名前をつけるとそれがバージョンとして認識される仕組みです。丸括弧がひとつだけなら`?<version>`すら不要です。

一方で、バージョンの構成要素が分散してしまっていたり、そのまま抜き出そうとすると曖昧すぎるマッチングになってしまう例もあるかと思います。このような場合は**replace**属性を利用しましょう。
拙作の[winmerge-jp](https://github.com/dooteeen/scoop-for-jp/blob/master/winmerge-jp.json#L23)がこれを使っているので例にします。

```winmerge-jp.json
"checkver": {
    "re": "<td nowrap=\"\">(?<v1>[\\d]+.[\\d]+.[\\d]+)+-jp-(?<v2>[\\d]+)</td>",
    "replace": "${v1}.${v2}"
},
```
- Before: `<td nowrap="">2.16.6+-jp-4</td>`
- After: `2.16.6.4`

このようにreplace属性内でマッチ結果をつなぎあわせることでバージョンを生成することができます。

それと、[x264](https://github.com/ScoopInstaller/Main/blob/master/bucket/x264.json#L26)の[リリースページ](https://download.videolan.org/pub/videolan/x264/binaries/win64/)のように、マッチする箇所が昇順に複数並んでいるけどそのうちの一番下にあるファイル名からコードを抜き出したい場合もあったりします。
そういうときは**reverse**属性に`true`を指定してください。

```x264.json
"checkver": {
    "url": "https://download.videolan.org/pub/videolan/x264/binaries/win64/",
    "re": "x264-r(?<version>[\\d]+)-(?<commit>[a-fA-F0-9]{7}).exe",
    "reverse": true
},
```

またhomepage属性に指定したURLにバージョンが入っている場合は省略した記法ができます。中身はre属性と同じですね。

```7zip.json
"homepage": "http://www.7-zip.org/",
"checkver": "Download 7-zip ([^\\ ]+)",
```

### JSONから取得

あまり見ないですが、JSONファイルにバージョンがある場合は**jp**属性にJSONPathで指定することで取得できます。
以下に[NuGet](https://github.com/ScoopInstaller/Main/blob/master/bucket/nuget.json#L11)のcheckverを挙げておきます。

```nuget.json
"checkver": {
    "url": "https://dist.nuget.org/index.json",
    "jp": "$.artifacts[0].versions[0].version"
},
```

### 実際にやってみる

一通り方法が分かったところで、先程のVimにcheckver属性を付けてみましょう。

このリポジトリのタグは`^v?[\d.]+$`の正規表現にマッチしないので、リリースページURLを利用する方法は使えません。
ここはわかりやすくリリースページのHTMLから取得する方法を採りましょう。

```vim-kaoriya-nogvim.json
{
    "homepage": "https://github.com/koron/vim-kaoriya",
    "description": "Vim + kaoriya build system, without gvim.",

    "version": "8.1.0005.20180520",
    "architecture": {
        "32bit": {
            "url": "https://github.com/koron/vim-kaoriya/releases/download/v8.1.0005-20180520/vim81-kaoriya-win32-8.1.0005-20180520.zip",
            "hash": "afacd6e27304136f5ebae3edddf6e747f410880fa6a986d80be772e03ef56d36",
            "extract_dir": "vim81-kaoriya-win32"
        },
        "64bit": {
            "url": "https://github.com/koron/vim-kaoriya/releases/download/v8.1.0005-20180520/vim81-kaoriya-win64-8.1.0005-20180520.zip",
            "hash": "53e8dd08e2249ce8a54784e16469151a7bf9857ac9acf3a1b341ac4e7da26fb2",
            "extract_dir": "vim81-kaoriya-win64"
        }
    },
    "bin": "vim.exe",

    "checkver": {
        "url": "https://github.com/koron/vim-kaoriya/releases/latest",
        "re": "vim(?<short>[\\d]+)-kaoriya-win32-(?<code>[\\d.]+)-(?<date>[\\d]{8}).zip",
        "replace": "${code}.${date}"
    }
}
```

`code`に該当する部分だけでもいいのですが、個人的には更新日も知りたかったので入れてみました[^1]。
ついでにcheckver属性を編集したら現在のversion属性もこれに合致するかどうか確認しておく癖を付けておくとなおいいです。

## autoupdate属性

autoupdate属性ではcheckver属性で取得したバージョンを用いて、以下の属性を更新するための雛形を定義できます。

- url
- hash
- extract_dir
- notes(バージョン情報の使用不可)

今回はurl属性を例に解説していきます。

### バージョン情報を埋め込む

バージョンは`$version`という文字をそのまま埋め込むだけです。

```ack.json
"autoupdate": {
    "url": "https://beyondgrep.com/ack-$version-single-file#/ack-single-file"
}
```

またバージョンがドット区切りであるならば各構成要素を示す特殊変数も使えます。
`$version`が4つに区切られている場合は順に`$majorVersion` `$minorVersion` `$patchVersion` `$buildVersion`となります。

```flux.json
"version": "4.76",
"url": "https://justgetflux.com/flux-setup4-76.exe#/flux-setup.exe",
"autoupdate": {
    "url": "https://justgetflux.com/flux-setup$majorVersion-$minorVersion.exe#/flux-setup.exe"
}
```

architecture属性でurlが分けられている場合は、autoupdate属性内にarchitecture属性をつくります。

```rust.json
"autoupdate": {
    "architecture": {
        "64bit": {
            "url": "https://static.rust-lang.org/dist/rust-$version-x86_64-pc-windows-gnu.msi"
        },
        "32bit": {
            "url": "https://static.rust-lang.org/dist/rust-$version-i686-pc-windows-gnu.msi"
        }
    }
}
```

### checkver属性で作った値を利用する

checkverのre属性で変数を設定することができましたが、これをautoupdate属性内で利用することができます。
ただしreplace属性とは違って、`abc`という変数は`matchAbc`と変数名が変わります。

先ほどのVimにはこれが使えそうですね。

```vim-kaoriya-nogvim.json
"checkver": {
    "url": "https://github.com/koron/vim-kaoriya/releases/latest",
    "re": "vim(?<short>[\\d]+)-kaoriya-win32-(?<code>[\\d.]+)-(?<date>[\\d]{8}).zip",
    "replace": "${code}.${date}"
},
"autoupdate": {
    "architecture": {
        "32bit": {
            "url": "https://github.com/koron/vim-kaoriya/releases/download/v$matchCode-$matchDate/vim$matchShort-kaoriya-win32-$matchCode-$matchDate.zip"
        },
        "64bit": {
            "url": "https://github.com/koron/vim-kaoriya/releases/download/v$matchCode-$matchDate/vim$matchShort-kaoriya-win64-$matchCode-$matchDate.zip"
        }
    }
},
```

### hash属性を設定する

url属性の値が変われば、当然それに紐づくhash属性も変化させなければいけません。
しかしありがたいことにhash属性については指定しなくても自動でダウンロードしたファイルから算出してくれるので基本的に指定する必要はありません。

もし個別で用意してあるのであればhash属性として付け加えることができます。
指定方法や抽出方法も充実しているので、詳しくは公式Wikiをご覧ください。

### 実際にやってみる

url属性の書き方は既に説明したので、あとはextract_dir属性だけすが、こいつにはurl属性と同じ変数と特殊変数が使えます。
ということでVimのManifestは以下のもので完成形となります。

```vim-kaoriya-nogvim.json
{
    "homepage": "https://github.com/koron/vim-kaoriya",
    "description": "Vim + kaoriya build system, without gvim.",

    "version": "8.1.0005.20180520",
    "architecture": {
        "32bit": {
            "url": "https://github.com/koron/vim-kaoriya/releases/download/v8.1.0005-20180520/vim81-kaoriya-win32-8.1.0005-20180520.zip",
            "hash": "afacd6e27304136f5ebae3edddf6e747f410880fa6a986d80be772e03ef56d36",
            "extract_dir": "vim81-kaoriya-win32"
        },
        "64bit": {
            "url": "https://github.com/koron/vim-kaoriya/releases/download/v8.1.0005-20180520/vim81-kaoriya-win64-8.1.0005-20180520.zip",
            "hash": "53e8dd08e2249ce8a54784e16469151a7bf9857ac9acf3a1b341ac4e7da26fb2",
            "extract_dir": "vim81-kaoriya-win64"
        }
    },
    "bin": "vim.exe",

    "checkver": {
        "url": "https://github.com/koron/vim-kaoriya/releases/latest",
        "re": "vim(?<short>[\\d]+)-kaoriya-win32-(?<code>[\\d.]+)-(?<date>[\\d]{8}).zip",
        "replace": "${versionCode}.${date}"
    },
    "autoupdate": {
        "architecture": {
            "32bit": {
                "url": "https://github.com/koron/vim-kaoriya/releases/download/v$matchCode-$matchDate/vim$matchShort-kaoriya-win32-$matchCode-$matchDate.zip",
                "extract_dir": "vim$matchShort-kaoriya-win32"
             },
            "64bit": {
                "url": "https://github.com/koron/vim-kaoriya/releases/download/v$matchCode-$matchDate/vim$matchShort-kaoriya-win64-$matchCode-$matchDate.zip",
                "extract_dir": "vim$matchShort-kaoriya-win64"
            }
        }
    }
}
```



## バージョンの自動更新を実行しよう

さて、checver属性もautoupdate属性も付けましたが、いったいどのタイミングで更新がされるのでしょうか?

利用者の`scoop update`実行時? いえいえ、これもManifest作成者がやらなくちゃいけないんです。

### Windows上で自動更新する

Scoopにはもともと`checkver.ps1`というManifest自動更新用のスクリプトが用意されており、*extras* Bucketなどはこれを利用するための同名スクリプトが用意されています。特に理由がない限りは*extras* Bucketにある`checkver.ps1`をそのまま自分のリポジトリのbinディレクトリ内に入れておきましょう[^2]。

これによって、リポジトリルートにて`.\bin\checkver.ps1 MANIFEST`を叩くと公式で管理されているバージョン確認処理が、`-u`オプションを付けることで自動更新の処理も併せて実行されるようになっています。

ただいちいちリポジトリに移動してコマンドを叩くのも面倒なので、いっそのこと関数にしてみましょう。どうせなら`scoop update`の際に一緒に実行できれば幸せですよね。
というわけでPowerShellスクリプトを作ってみました。

```update_buckets.ps1
# globalのuser.nameとBucket所有者名が一致するものを自動更新
# * リモート名は"origin"のみ対応
# * sshを使うようになっていないもの(=初期状態)はスルーするので
#   事前に該当リポジトリ内で"git remote set-url"を実行する必要あり
# * 自動でcommitおよびpushをするので注意

$my_name = git config --global user.name
$my_buckets = @()
$prompt_current_dir = Get-Location

$scoop_root = [environment]GetEnvironmentVariable('SCOOP', 'User')
if (!$scoop_root) {
    $scoop_root = "$HOME\scoop"
}
Get-ChildItem $scoop_root\buckets\* | ForEach-Object {
    Set-Location $_
    $url = git remote get-url --push "origin"
    if ($url -clike "git@*/$my_name/*") {
        $my_buckets += $_
    }
}

foreach ($bucket in $my_buckets) {
    Set-Location $bucket
    if (Test-Path .\bin\checkver.ps1) {
        Write-Host "Check Bucket: $_" -ForegroundColor Green
        Get-ChildItem .\*.json | ForEach-Object {
            $json = $_.Basename
            .\bin\checkver.ps1 $json -u
            if ($(git diff $_)) {
                Write-Host "  => Update: $json" -ForegroundColor Blue
                git commit -a -m "Update: $json"
            }
        }
        if ($(git diff)) {
            Write-Host "  => Push bucket..."
            git push
        }
    }
}

Set-Location $prompt_current_dir
```

こいつにパスを通して`update_buckets; scoop update; scoop update *`をエイリアスにしておくことで、Scoopのアップデートと同時に独自Bucketも更新がされるようになります。


### Dockerで全自動更新する (推奨)

おそらく公式が利用している手法で、cronによる自動更新のみならずプルリクの自動適用にも対応しています。GitHubにリポジトリが公開されているので利用してみましょう。

[GitHub - ScoopInstaller/Excavator: 🕳️ This container runs the updating services for all scoop manifest repos](https://github.com/ScoopInstaller/Excavator)
[GitHub - dooteeen/Excavator-JP: Fork of ScoopInstaller/Excavator.](https://github.com/dooteeen/Excavator-JP) (私の作成例)

1. 自分のbucketに自動更新を反映させるためのスクリプトを用意。 
binディレクトリの中にbucekt-updater.ps1を作成、指示通りの内容を記述して保存します。


    ```BUCKET-ROOT/bin/bucket-updater.ps1
    # scoop本体のauto-pr.ps1を起動。内部で前述のcheckver.ps1が実行される。
    param(
        # 引数にbucket情報が必要なのでここを編集
        # 例: "hogeman/bucket-hoge:master"
        [String]$upstream = "<GIT_USERNAME>/<BUCKET>:master"
    )
    if(!$env:SCOOP_HOME) { $env:SCOOP_HOME = resolve-path (split-path (split-path (scoop which scoop))) }
    $autopr = "$env:SCOOP_HOME/bin/auto-pr.ps1"
    $dir = "$psscriptroot/.." # checks the parent dir
    iex -command "$autopr -dir $dir -upstream $upstream $($args |% { "$_ " })"
    ```

2. タイムゾーン設定のためDockerfileを編集。

    ```Dockerfile:Excavator/Dockerfile
    FROM phusion/baseimage:0.11
    
    # Set timezone.
    RUN apt update && apt install -y --no-install-recommends tzdata
    ENV TZ "Asia/Tokyo"
    
    # Use baseimage-docker's init system.
    CMD ["/sbin/my_init"]
    
    # 以下省略
    ```

3. READMEにしたがって以下のようにdocker-compose.ymlを編集し、`docker-compose up -d --build`で実行。

    ```Excavator/docker-compose.yml
    version: "3"
    
    services:
      bucket:
        build:
          # imagesの代わりにbuildを利用
          # 手順2で編集したDockerfileを用いる設定
          context: .
          dockerfile: Dockerfile 
        deploy:
          mode: global
        volumes:
          - ssh:/root/.ssh
          - logs:/root/log
        environment:
          # 必要な情報をここに入力しておく
          GIT_USERNAME: "my-git-name"
          GIT_EMAIL: "my-address@gmail.com"
          BUCKET: "username/bucket-name"    # GitHub上にあるリポジトリを指定
          CRONTAB: "0 0 * * *"              # crontabの書式で実行頻度を指定  
    volumes:
      ssh:
      logs:
    ```

4. 生成されたSSHの公開鍵をGitHubに登録。あとは勝手に更新してくれるようになります。

上記の例ではGitHub上における単一リポジトリを対象としていますが、少し編集するだけで他ホスティングサービスに対応させたり複数のbucketを管理したりすることもできます。



## まとめ

- checkver属性でHTMLなどからバージョンの取得方法が指定できる
- autoupdate属性で更新内容を定義できる
    - url属性、extract_dir属性はバージョン情報とcheckverのre属性のマッチ結果も使える
    - hash属性は基本的に指定しなくてもいい
- Dockerが使えれば更新の全自動化が可能。手動で行う人のために`checkver.ps1`も完備


## あとがき

今回は自動更新にまつわる機能についての記事になりました。公式Wikiがかなり充実しているので記事にする必要もないかと思いましたが、Scoop普及のためにいろいろ頑張ってみました。
そしてこの記事を書くにあたって改めて、この自動アップデートすらも簡単に済ませられるScoopのお手軽さに感動しました。皆さんも勿論この便利さに涙を流しながら喜んでいることでしょう。私には分かってます。

あ、次回はいよいよ環境構築についての話です……が、書けるかどうかすら危うい。おのれWindows!!


[^1]: 後述の通り、本当はビルドバージョンを入れなきゃいけない。
[^2]: 文法エラーやリンク切れなどのチェックのために入れたほうがいい。