各OSに対応したsetup系GitHub Actionsを作るTips

各OSに対応したsetup系GitHub Actionsを作るTips

こんにちは、お久しぶりです。@ueokandeです。 自分はbrowser-actionsという、ブラウザ関連のテストや自動化をするためのGitHub Actionsを公開しています。 そのなかでもsetup-***は、ブラウザのインストールを行うためのActionです。

  • setup-firefox ... Mozilla FirefoxをインストールするAction
  • setup-chrome ... Google ChromeをインストールするAction
  • setup-edge ... Microsoft EdgeをインストールするAction

この記事ではそれぞれのプラットフォームや配布形式ごとに、どのようにインストールするかを紹介します。

基本

setup系GitHub Actionsの基本的な構成は処理はおおよそ以下の流れです。

  1. ソフトウェアの配布元からバイナリをダウンロードする
  2. ダウンロードしたバイナリを適当な場所に展開・インストールする。
  3. インストールしたバイナリの場所をPATH環境変数に追加する。

ブラウザの配布元や対象OSによって、インストールの方法が大きく異なります。 またチャネル(BetaやDevなど)によっても配布形式が異なる場合があるので注意が必要です。 CLI上でインストール可能できるものもあれば、GUIインストーラーを用意しているものもあります。

各OS共通: .zip/.tar.gz形式

実装やデバッグが一番楽な形式です。 @actions/tool-cacheにzipやtar.gzを展開する機能があるので、対象OSを意識せずに展開できます。

import * as tc from "@actions/tool-cache";

const url = "https://example.com/example.zip";
const zip = await tc.downloadTool(url);
await tc.extractZip(path, zip);
import * as tc from "@actions/tool-cache";

const url = "https://example.com/example.tar.gz";
const tar = await tc.downloadTool(url);
await tc.extractTar(tar);

他にも extract7z()extractXar() があります。

OS固有のツールはトラブルシューティングが大変なので、ソフトウェアがzipやtar.gzを配布している場合は、まずはそちらを優先して利用するのが良いです。

Linux

deb形式

UbuntuやDebianなどのディストリビューションで使われるパッケージ形式です。 GitHub ActionsのデフォルトのLinux環境は展開するためのツールが一式揃っています。

dpkg コマンドでインストールできますが、既存のパッケージや複数バージョンインストール時の競合を考慮して、手動で展開してインストールします。 debパッケージは ar コマンドで展開できます。 ただし配布されているdebパッケージは、インストール先のディレクトリに依存する構成の場合は注意が必要です。

以下はsetup-chromeのコードから抜粋したものです。

const tmpdir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "deb-"));
const extdir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "chrome-"));
await exec.exec("ar", ["x", archive], { cwd: tmpdir });
await exec.exec("tar", [
  "-xf",
  path.join(tmpdir, "data.tar.xz"),
  "--directory",
  extdir,
  "--strip-components",
  "4",
  "./opt/google",
]);

// /opt/google/以下にインストールされることを期待してるためsymblinkが無効なパスを指している
// キャッシュ格納時にエラーが出るため削除する
await fs.promises.unlink(path.join(extdir, "google-chrome"));

const root = await tc.cacheDir(extdir, "chromium", version);
core.info(`Successfully Installed chromium to ${root}`);

return { root: extdir, bin: "chrome" };

macOS

macOSも比較的扱いが簡単です。 macOS向けにはdmgファイルとpkgファイルがあります。

dmg形式

dmgファイルはアーカイブ形式のように展開できませんが、 hdiutil コマンドでファイルシステムにマウントできます。 マウントされたディレクトリに、****.app という名前のディレクトリがあり、この中にバイナリが入っています。 以下はsetup-firefoxのコードから抜粋したものです。

const mountpoint = path.join("/Volumes", path.basename(archivePath));
const appPath = path.join(mountpoint, "Firefox.app");

await exec.exec("hdiutil", [
  "attach",
  "-quiet",
  "-noautofsck",
  "-noautoopen",
  "-mountpoint",
  mountpoint,
  archivePath,
]);
core.info(`Successfully extracted firefox ${version} to ${appPath}`);

core.info("Adding to the cache ...");
const cachedDir = await tc.cacheDir(appPath, "firefox", version);

pkg形式

pkgファイルは xar コマンドで展開できます。 展開したディレクトリの中にPayload というgzファイルがあり、更にその中にcpioアーカイブが入っています。 以下はsetup-edgeのコードから抜粋したものです。

const extdir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "msedge-")); // /tmp/msedge-xxxxxx/

await exec.exec("xar", ["-xf", archive], { cwd: extdir });

const pkgdir = (await fs.promises.readdir(extdir)).filter(
  (e) => e.startsWith("MicrosoftEdge") && e.endsWith(".pkg")
)[0];
if (!pkgdir) {
  throw new Error('"MicrosoftEdge*.pkg" not found in extracted archive');
}
const pkgroot = path.join(extdir, pkgdir); // /tmp/msedge-xxxx/MicrosoftEdge-xx.x.xxx.x.pkg/

await fs.promises.rename(
  path.join(pkgroot, "Payload"),
  path.join(pkgroot, "App.gz")
);
await exec.exec("gzip", ["--decompress", "App.gz"], { cwd: pkgroot });
await exec.exec("cpio", ["--extract", "--file", "App"], { cwd: pkgroot });

const app = path.join(pkgroot, this.appName(version));
const root = await tc.cacheDir(app, "msedge", version);

Windows

Windowsのmsi/exe形式が一番厄介です。 Linux、macOSと違って、Windowsはインストーラーの制御が難しいです。 またこれまでは既存のブラウザの上書きや、複数のバージョンのインストールを想定して独自のディレクトリにインストールしてました。 msi/exeでインストール先を指定できないと、異なるバージョンを一度にインストールするのが難しいです。 インストーラーによってはログが出力されないことがほとんどで、他のOSと比べて開発者体験があまり良くありません。

msi形式

msi形式は、バイナリファイルを直接実行するか、msiexecコマンドでインストールできます。 browser-actionsではmsi形式を使っている部分はないのですが、以下の方法でインストールできます。

await exec.exec("msiexec", ["/i", "path/to/installer.msi", "/qn"]);

exe形式

exe形式で配布されている場合は、実行してみないとわかりません。 GUI上でユーザーに入力を求めるインストーラーはGitHub Actions上では利用できません。 setup-edgeはexe形式のインストーラーを使っていますが、インストール終了判定をバイナリの有無で判定しています。 MSI形式を使わない理由は、Microsoft EdgeのCanary版がMSI形式で配布されていないからです。

以下はsetup-edgeのコードから抜粋したものです。 インストールパスを監視している waitInstall() 関数の実装はwatch.tsを参照してください。

const p = cp.spawn(archive);
p.stdout.on("data", (data: Buffer) =>
  process.stdout.write(data.toString())
);
p.stderr.on("data", (data: Buffer) =>
  process.stderr.write(data.toString())
);

try {
  await waitInstall(path.join(this.rootDir(version), "msedge.exe"));
} finally {
  p.kill();
}

おわりに

この記事ではそれぞれのOS上で利用できるsetup系GitHub ActionsのTipsを紹介しました。 もしsetup系のGitHub Actionsを作成する場合は、この記事を参考にしてみてください。

記事の前半にも書きましたが、zip/tar.gz形式の配布が利用可能であればそちらを選択するのが良いです。 LinuxのdpkgやmacOSは少しむりくりな対応をしており、将来も動作し続けるという保証はありません。 Windows版のexe形式も、ログが出力されず利用者から問題が報告されたときにトラブルシューティングが大変になります。


Profile picture

Shin'ya Ueoka

B2B向けSaaSを提供する会社の、元Webエンジニア。今はエンジニアリング組織のマネジメントをしている。