GitHub Actionsで、Windows上でもBazelの出力をキャッシュする

意外と情報がない（Remote Cachingを除く）のでメモ。
Linux上ではキャッシュを有効にしているものの、Windows上では1時間以上かけてビルドしているプロジェクトがままある（ただし、キャッシュを有効にすれば速くなるとも言えないので、意図的かもしれない）。

## Linuxでの例
setup-bazeliskのページに書かれている通りで、キャッシュディレクトリをそのまま `@actions/cache` でキャッシュすれば良い。
https://github.com/bazelbuild/setup-bazelisk#usage

```yml
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      ...
      - name: Mount bazel cache
        uses: actions/cache@v3
        with:
          path: "~/.cache/bazel"
          key: bazel-v1-cache
```

## Windowsでの問題の所在
ところが、Windowsだと上記の方法でうまく行かない。

```yml
jobs:
  build:
    runs-on: windows-latest
    steps:
      # ...
      - name: Mount bazel cache
        uses: actions/cache@v3
        with:
          path: C:\_bzl
          key: bazel-v1-cache
      - run: |
          bazel --output_user_root=C:\_bzl build ..
```

`output_user_root` が `C:\_bzl` だとして、`path` に `C:\_bzl` を指定すると、以下のようにキャッシュ作成処理（`Post Mount bazel cache`）に失敗する。

```txt
Post job cleanup.
C:\Windows\System32\tar.exe --posix -z -cf cache.tgz --exclude cache.tgz -P -C D:/a/repository/repository --files-from manifest.txt
tar.exe: Couldn't open C:/_bzl/l2nyujfy/java.log.fv-az455-530.runneradmin.log.java.20220701-105515.2912: Permission denied
tar.exe: Error exit delayed from previous errors.
Warning: Tar failed with error: The process 'C:\Windows\System32\tar.exe' failed with exit code 1
```

Job自体はエラーにならないものの、キャッシュがアップロードされていないので、次の実行時にもキャッシュが見つかることはない。
```txt
 Cache not found for input keys: bazel-v1-cache
```

これを解決する方法として、MSYS2でインストールされているGNUのtarを使う方法がある。

```yml
jobs:
  build:
    runs-on: windows-latest
    steps:
      # ...

      # Restoreする前に実行しないと、Keyがヒットしているにもかかわらず、Cache not foundになるので注意
      - run: echo "C:\msys64\usr\bin" >> $env:GITHUB_PATH

      - name: Mount bazel cache
        uses: actions/cache@v3
        with:
          path: C:\_bzl
          key: bazel-v1-cache
      - run: |
          bazel --output_user_root=C:\_bzl build ..
```

GNUのtarを使うと、キャッシュの作成・アップロードに成功するようになる。
```txt
Post job cleanup.
C:\msys64\usr\bin\tar.exe --posix --use-compress-program "zstd -T0" -cf cache.tzst --exclude cache.tzst -P -C D:/a/repository/repository --files-from manifest.txt --force-local
Cache Size: ~1833 MB (1922026652 B)
Cache saved successfully
Cache saved with key: bazel-v1-cache
```

しかし、次の実行時、キャッシュのリストアに失敗してしまう（Jobの実行自体はエラーにならない。後ほど、`bazel build`が失敗する可能性はあるが……）。
```txt
/usr/bin/tar: C\:/_bzl/l2nyujfy/external/androidsdk/ndk-bundle: Cannot create symlink to '/c/Android/android-sdk/ndk-bundle': Permission denied
/usr/bin/tar: C\:/_bzl/l2nyujfy/external/llvm-raw/libcxx/test/std/pstl: Cannot create symlink to '/c/_bzl/l2nyujfy/external/llvm-raw/pstl/test/std': No such file or directory
/usr/bin/tar: C:/_bzl/l2nyujfy/external/local_config_cc/windows_cc_toolchain_config.bzl: time stamp 2032-06-29 04:38:53.9788293 is 315262677.9849176 s in the future
/usr/bin/tar: Exiting with failure status due to previous errors
Warning: Tar failed with error: The process 'C:\msys64\usr\bin\tar.exe' failed with exit code 2
```

## 解決策
本質的には、read権限があるはずのファイルでなぜか発生する `Permission denied` (`tar.exe: Couldn't open C:/_bzl/l2nyujfy/java.log.fv-az455-530.runneradmin.log.java.20220701-105515.2912: Permission denied`) と、symlinkに関する問題の2つを解決する必要がある。

実は、前者の問題は、 `bazel shutdown` するだけで解決する（Bazelがlogファイルを開いているのが原因と思われる）。
後者のsymlinkは `bazel` が実行時に自動で作成するので、キャッシュ対象から外しておけば十分。

なお、bazeliskを使う場合は、bazeliskのキャッシュ自体も保存しておく必要がある。

```yml
jobs:
  build:
    runs-on: windows-2019
    steps:
      # ...

      # bazeliskがダウンロードしてくる `bazel.exe` をキャッシュする
      - name: Mount bazelisk cache
        uses: actions/cache@v3
        with:
          path: C:\Users\runneradmin\AppData\Local\bazelisk
          key: windows-bazelisk

      - name: Mount bazel cache
        uses: actions/cache@v3
        with:
          path: C:\_bzl
          key: bazel-v1-cache

      - run: |
          bazel --output_user_root=C:\_bzl build ..
          bazel --output_user_root=C:\_bzl shutdown
        shell: cmd

      # symlinkは `C:\_bzl\*\install` の1つ。これを削除しておく。
      # 端的に、actions/cacheのpathで除外しても良いはず。
      - run: |
          (Get-Item C:\_bzl\*\install).Delete()
```

### 注意
弊プロジェクトだと、キャッシュなしのビルドに1時間程かかり、その間、キャッシュ対象が9GBまで膨れ上がり、tarにしてアップロードするのに40分前後かかります……（つまり、初回実行にかかる時間が、60分->100分に増える）。
キャッシュありだとビルドは瞬間的に終わりますが、キャッシュには保存期限があるので、頻繁に実行するのでなければキャッシュを有効にする意味はないです。

ただ、個人プロジェクトで、たまにしか作業しない時も、ブランチ切ってpushしてから作業に入れば、作業内容をpushした時にキャッシュが生きていて嬉しいかも。