GitHub Actions self-hosted runner on FreeBSD


残念ながら、2024/1/27 現在 GitHub-hosted runner では BSD系のOSはサポートされていない。

ので、表題の通りself-hosted runnerを利用しようと思うのだが、これまた残念なことに、self-hosted runnerも公式にはWindows,Linux,macOSのサポートのみでBSD系OSはサポートされていないしする予定も今のところ無いようだ。

https://github.com/actions/runner/issues/385

というのも、GitHub Actions runnerはC#で実装されているのだが、処理系の.NETが公式にはBSD系OSをサポートしていないので、同様にサポートをするつもりはないらしい。

それでも、やっぱりBSD系OSのCI環境が欲しい。

そういえば、FreeBSDってLinux互換レイヤーがあったような...

## What will you do

方針

1. FreeBSDでLinux互換レイヤーを有効化
2. その上で、Linux版のself-hosted runnerを動作させる

筆者環境

macOS: 11.7.10
shell: zsh
Vagrant: 2.3.7
Docker: 24.0.6

OSやソフト等が若干古めだが、それぞれの仮想化ツールが実行できれば、バージョンはさほど問題にならないだろう。

なお、以降特別の記載がない限りは、ホストOS上のディレクトリはVagrantfileが置いてあるディレクトリを起点として記す。

## Setup FreeBSD

お手軽にFree BSDの環境が欲しかったので、Vagrant+VirtualBoxで用意することにした。

`Vagrantfile`に以下の内容を書き込んで`vagrant up`, `vagrant ssh`で接続

```ruby
Vagrant.configure("2") do |config|
  config.vm.box = "generic/freebsd14"
#   これだと上手いこと共有できなかったので、nfsとした
#   config.vm.synced_folder ".", "/home/vagrant/shared", type: "virtualbox"
  config.vm.network "private_network", ip: "192.168.33.10"
  config.vm.synced_folder ".", "/home/vagrant/shared", type: "nfs"
end
```

のちに必要になるコマンドをインストールしておく

```
sudo pkg install -y git
```


### Enable Linux Compat Layer

こちらを参考にLinux互換レイヤーを有効化。

https://docs.freebsd.org/ja/books/handbook/linuxemu/#linuxemu-lbc-install

```sh
# In BSD
sudo kldload linux
sudo kldload linux64
sudo pkg install -y emulators/linux_base-c7
echo 'linux_enable="YES"' | sudo tee -a /etc/rc.conf
```

### Install .NET

`actions/runner`にLinux版 .NETを簡単にインストールできるスクリプトがあるので、流用させていただく。

(わざわざ、ホストOSとの共有ディレクトリにcloneしているのは、のちの事情のためである)

```sh
# In BSD
cd /home/vagrant/shared
git clone https://github.com/actions/runner.git
cd runner/src
./Misc/dotnet-install.sh --version "6.0.418" --install-dir "/home/vagrant/_dotnetsdk/6.0.418" --os linux
```

パスも通しておく

```sh
export PATH="/home/vagrant/_dotnetsdk/6.0.418:$PATH"
```

```sh
# In BSD
dotnet --version
```

```sh
Process terminated. Couldn't find a valid ICU package installed on the system. Please install libicu using your package manager and try again. Alternatively you can set the configuration flag System.Globalization.Invariant to true if you want to run with no globalization support. Please see https://aka.ms/dotnet-missing-libicu for more information.
   at System.Environment.FailFast(System.String)
   at System.Globalization.GlobalizationMode+Settings..cctor()
   at System.Globalization.CultureData.CreateCultureWithInvariantData()
   at System.Globalization.CultureData.get_Invariant()
   at System.Globalization.CultureInfo..cctor()
   at System.Globalization.CultureInfo.get_CurrentUICulture()
   at System.TimeZoneInfo.GetUtcStandardDisplayName()
   at System.TimeZoneInfo.CreateUtcTimeZone()
   at System.TimeZoneInfo..cctor()
   at System.DateTime.get_Now()
   at Microsoft.DotNet.Cli.Program.Main(System.String[])
Abort trap
```

libicuがないため怒られた。

ないものは入れるのが適切なのだろうが、環境変数を設定することでlibicuを要求しないようにできるため今回はそれとする。

```sh
# In BSD
export DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=1
```

```sh
dotnet --version
```

```
6.0.418
```

無事怒られずに済んだ。

### Install GitHub Actions self-hosted runner

適当なレポジトリを作成して

`Settings > Actions > Runners > New self-hosted runner` から手順に沿ってインストールを実施

```sh
# In BSD
cd ~
mkdir actions-runner && cd actions-runner
curl -o actions-runner-linux-x64-2.312.0.tar.gz -L https://github.com/actions/runner/releases/download/v2.312.0/actions-runner-linux-x64-2.312.0.tar.gz
tar xzf ./actions-runner-linux-x64-2.312.0.tar.gz
```

## Let's try self-hosted runner on FreeBSD

### Set up self-hosted runner

それぞれのレポジトリによってURL等は変わってくるので注意

```sh
# In BSD
./config.sh --url https://github.com/<USER>/<REPO> --token <TOKEN>
```

```log
--------------------------------------------------------------------------------
|        ____ _ _   _   _       _          _        _   _                      |
|       / ___(_) |_| | | |_   _| |__      / \   ___| |_(_) ___  _ __  ___      |
|      | |  _| | __| |_| | | | | '_ \    / _ \ / __| __| |/ _ \| '_ \/ __|     |
|      | |_| | | |_|  _  | |_| | |_) |  / ___ \ (__| |_| | (_) | | | \__ \     |
|       \____|_|\__|_| |_|\__,_|_.__/  /_/   \_\___|\__|_|\___/|_| |_|___/     |
|                                                                              |
|                       Self-hosted runner registration                        |
|                                                                              |
--------------------------------------------------------------------------------

# Authentication


√ Connected to GitHub

# Runner Registration

Enter the name of the runner group to add this runner to: [press Enter for Default] 

Enter the name of runner: [press Enter for freebsd14] 

This runner will have the following labels: 'self-hosted', 'Linux', 'X64' 
Enter any additional labels (ex. label-1,label-2): [press Enter to skip] freebsd-latest

√ Runner successfully added
√ Runner connection is good

# Runner settings

Enter name of work folder: [press Enter for _work] 

√ Settings Saved.
```

`freebsd-latest`というラベルをつけたのみで、あとはデフォルトの設定のままだ。

```sh
# In BSD
./run.sh
```

```log

√ Connected to GitHub

Current runner version: '2.312.0'
2024-01-27 02:49:48Z: Listening for Jobs

```

ここまでは、目論見通りLinux互換レイヤーで.NETを動かすことに成功している！
あとは、実際にジョブを実行できるか試してみる。

### Run job

以下のような、ワークフローを用意して、レポジトリの`.github/workflows/test.yml`に置いてコミット＆プッシュ

```yaml
name: test
on:
  push:
jobs:
  test:
    runs-on: freebsd-latest
    steps:
      - uses: actions/checkout@v4
      - name: run freebsd-version
        run: freebsd-version
```

が、ここで問題が発生...
あるはずの、tarコマンドが存在しない旨のエラーが、表示される...

```sh
Current runner version: '2.312.0'
Runner name: 'freebsd13'
Runner group name: 'Default'
Machine name: 'freebsd13'
GITHUB_TOKEN Permissions
Secret source: Actions
Prepare workflow directory
Prepare all required actions
Getting action download info
Download action repository 'actions/checkout@v4' (SHA:b4ffde65f46336ab88eb53be808477a3936bae11)
Error: tar: command not found
```

が、BSD環境でtarコマンドの存在を確認してみても存在している...
```sh
# In BSD
which tar
```

```sh
/usr/bin/tar
```


紆余曲折、あったが苦労して調べてみると原因は`WhichUtil.Which`(whichコマンドと同じような動作をする)関数が内部で呼んでいる、`System.IO.Directory.GetFiles`から返ってくる値が、空の配列、つまり、ディレクトリ内のファイルの一覧が取得できないことに起因するtarコマンドの存在確認ができなかったことだった。
(当然だが、互換レイヤーだけで完璧に動くわけではないようだ...)

ので、最終的に要求されている、tarコマンドのフルパスを直接プログラムにベタ書きすることで、無理矢理動かすこととした。
ハードコーディングは本来すべきではないが動かないのだからいたしかたない...

(.NET 7以降 であれば、標準ライブラリで tar を展開する機能が使えるのでtarコマンドがなかったらそちらにフォールバックするようにすれば良いのだろうが、残念ながら今時点では`actions/runner`で利用しているのは.NET 6系である)

これらのファイルに次のようなパッチを当てた。

```diff
diff --git a/src/Runner.Listener/SelfUpdater.cs b/src/Runner.Listener/SelfUpdater.cs
index 419a14e..bdbf506 100644
--- a/src/Runner.Listener/SelfUpdater.cs
+++ b/src/Runner.Listener/SelfUpdater.cs
@@ -411,7 +411,7 @@ namespace GitHub.Runner.Listener
             }
             else if (archiveFile.EndsWith(".tar.gz", StringComparison.OrdinalIgnoreCase))
             {
-                string tar = WhichUtil.Which("tar", trace: Trace);
+                string tar = WhichUtil.Which("/usr/bin/tar", trace: Trace);
 
                 if (string.IsNullOrEmpty(tar))
                 {
diff --git a/src/Runner.Listener/SelfUpdaterV2.cs b/src/Runner.Listener/SelfUpdaterV2.cs
index b64619b..fc91e0a 100644
--- a/src/Runner.Listener/SelfUpdaterV2.cs
+++ b/src/Runner.Listener/SelfUpdaterV2.cs
@@ -371,7 +371,7 @@ namespace GitHub.Runner.Listener
             }
             else if (archiveFile.EndsWith(".tar.gz", StringComparison.OrdinalIgnoreCase))
             {
-                string tar = WhichUtil.Which("tar", trace: Trace);
+                string tar = WhichUtil.Which("/usr/bin/tar", trace: Trace);
 
                 if (string.IsNullOrEmpty(tar))
                 {
diff --git a/src/Runner.Worker/ActionManager.cs b/src/Runner.Worker/ActionManager.cs
index bf7838c..c08f2f3 100644
--- a/src/Runner.Worker/ActionManager.cs
+++ b/src/Runner.Worker/ActionManager.cs
@@ -851,7 +851,7 @@ namespace GitHub.Runner.Worker
                     throw new InvalidActionArchiveException($"Can't un-zip archive file: {archiveFile}. action being checked out: {downloadInfo.NameWithOwner}@{downloadInfo.Ref}. error: {e}.");
                 }
 #else
-                string tar = WhichUtil.Which("tar", require: true, trace: Trace);
+                string tar = WhichUtil.Which("/usr/bin/tar", require: true, trace: Trace);
 
                 // tar -xzf
                 using (var processInvoker = HostContext.CreateService<IProcessInvoker>())
```

そしてこれを、ビルドする。

これのために環境を汚したく無いので、ホストOS上で、dockerを利用してサクッとビルドしてしまう。

BSD上でビルドを実行しないのは、単純にビルドができないからだ。
`actions/runner`レポジトリに用意されている開発用のビルドスクリプトがBSD系をサポートしていない(当然だ)上に、動くように多少パッチを当てたところで、`dotnet build`コマンドをはじめとするビルド系のコマンドが上手く動作しない(途中でうんともすんとも言わなくなる...)
どうやら、互換レイヤーの上では.NETのマルチスレッド系の機能が一部上手く動作しないようで、途中で止まってしまっている様子である。

(ビルド成果物をBSD環境にコピーするのが面倒だったので、わざわざ、BSDとホストOSの共有ディレクトリ上に`actions/runner`レポジトリをcloneしていた。これがのちの事情というやつである)

```sh
# Host OS
cd shared/runner
docker run --rm -it -v `pwd`:/runner/ -w /runner/src ghcr.io/catthehacker/ubuntu:act-latest bash
```

イメージ自体はなんでも良いはずだが、必要そうな開発ツールが入っているイメージを選んだ。

```sh
# In docker container
./dev.sh layout Release linux-x64
./dev.sh package Release
```

ビルドが終わったら、これを動かす。
インストール手順は基本的に先ほどと一緒だが、DLしてくる代わりに自前でビルドしたものを使う。

```sh
cd ~
# 古い方を削除
rm -rf actions-runner
mkdir actions-runner && cd actions-runner
cp ~/shared/runner/_package/actions-runner-linux-x64-2.312.0.tar.gz .
tar xzf ./actions-runner-linux-x64-2.312.0.tar.gz
```

再度登録

```sh
# In BSD
./config.sh --url https://github.com/<USER>/<REPO> --token <TOKEN>
```

### Retry self-hosted runner on FreeBSD

```sh
# In BSD
./run.sh
```

ジョブの再実行をするか、レポジトリに適当な内容でコミット&プッシュ

```sh
Run actions/checkout@v4
/usr/home/vagrant/actions-runner/externals/node20/bin/node: /lib64/libm.so.6: version `GLIBC_2.27' not found (required by /usr/home/vagrant/actions-runner/externals/node20/bin/node)
/usr/home/vagrant/actions-runner/externals/node20/bin/node: /lib64/libstdc++.so.6: version `GLIBCXX_3.4.20' not found (required by /usr/home/vagrant/actions-runner/externals/node20/bin/node)
/usr/home/vagrant/actions-runner/externals/node20/bin/node: /lib64/libstdc++.so.6: version `CXXABI_1.3.9' not found (required by /usr/home/vagrant/actions-runner/externals/node20/bin/node)
/usr/home/vagrant/actions-runner/externals/node20/bin/node: /lib64/libstdc++.so.6: version `GLIBCXX_3.4.21' not found (required by /usr/home/vagrant/actions-runner/externals/node20/bin/node)
/usr/home/vagrant/actions-runner/externals/node20/bin/node: /lib64/libc.so.6: version `GLIBC_2.28' not found (required by /usr/home/vagrant/actions-runner/externals/node20/bin/node)
/usr/home/vagrant/actions-runner/externals/node20/bin/node: /lib64/libc.so.6: version `GLIBC_2.25' not found (required by /usr/home/vagrant/actions-runner/externals/node20/bin/node)
```

今度は、tarコマンドが無いとは言われなかったが、nodeを実行しようとしたら、複数の共有ライブラリがないと言われた。
これは、Linuxバイナリ用の共有ライブラリが無いのが原因のようなのだが、バージョンを揃えてそれぞれを用意するのが面倒だった&互換レイヤー周りの影響を受けたくなかったので、BSD版が提供されているなら、そっちを使えばいいじゃないということで、BSD版のnodeをインストールしてリンクをはることとした。

```sh
sudo pkg install -y node
node --verison
# v20.10.0
which node
# /usr/local/bin/node
ln -s /usr/local/bin/node /usr/home/vagrant/actions-runner/externals/node20/bin/node
```

(今回は、要求するランタイムがnode20だけだったので、node20だけ置き換えたが、node16の方も同様にリンクを張っておくとnode16をランタイムとして動くアクションも動作するようになる)

### Re Retry self-hosted runner on FreeBSD

```
Run freebsd-version
  freebsd-version
  shell: /bin/bash -e {0}
14.0-RELEASE
```

よし！無事に完走した！！！

# End

なんとかしてFreeBSD上で、GitHub Actions self-hosted runner を動かすことに成功したわけだが、当然全ての機能が正常に動く保証はないので、試される方は自己責任でお願いしたい。

今回紹介したのは、FreeBSD 14だが、他の環境でもおそらく似たような手順を踏めば動くはずだ。
チャレンジした人がいたら、ここに記録を残していただければ、他のチャレンジャーの手助けになるだろう。

また、今回未解決だった。`System.IO.Directory.GetFiles`が空の配列を返してくることについて、わかったことがあればコメントしていただけると、とても嬉しい。