🧑‍💻

#148 【Electron】electron-builderでbuild後にseleniumが起動しない場合の対策

に公開

はじめに

Seleniumを組み込んだスタンドアロンアプリをElectronで作成し、electron-builderでビルド後にビルド前には起動できていたSeleniumが起動しなくなるという現象が発生しました。
本記事では以上の件についての解決策を記載します。

原因

今回の事象に関する直接的な原因はselenium-manager(※1)を実行するためのpathがビルド時のとある要因でズレるということが原因でした。

※1:最新(2025/04/16)のSeleniumでは最適なwebdriverをダウンロードする機能があり、その際に使用しているのがselenium-managerのようです。
以下ディレクトリに格納されています。
selenium-webdriver/bin/{環境}/selenium-manager(.exe)

pathのズレについて

electron-builderでアプリケーションをビルドすると app.asar ファイルにパッケージされます。
この時、asarファイル内のexeファイルなどは実行できなくなるため、バイナリファイルを含むライブラリなどは暗黙的にapp.unpackedファイルに退避されます。(明示的に指定することも可能です。)
しかし、この時selenium-managerの場所を示すpathはapp.asarファイル配下のものになります。
このズレによってビルド後のアプリケーションではselenium-managerが起動できないという結果になります。

対策

対策として、selenium-managerのpathを明示的に指定するという方法を今回は紹介します。
とはいえ、driverのインスタンス作成時に明示的にpathを指定するということはなく、以下の環境変数に指定のpathを設定します。

SE_MANAGER_PATH = 指定のpath

上記のように設定することでビルド後でも指定したpathでselenium-managerを明示的に実行できるようになります。
ということで、もっと具体的に実際にmainプロセス内で環境変数を設定するコードを書いていきます。

main.ts
import { app, shell, BrowserWindow, ipcMain } from 'electron'
import path, { join } from 'path'
import { electronApp, optimizer, is, platform } from '@electron-toolkit/utils'
import icon from '../../resources/icon.png?asset'
import { Browser, Builder } from 'selenium-webdriver'

function createWindow(): void {
  // Create the browser window.
  const mainWindow = new BrowserWindow({
    width: 900,
    height: 670,
    show: false,
    autoHideMenuBar: true,
    ...(process.platform === 'linux' ? { icon } : {}),
    webPreferences: {
      preload: join(__dirname, '../preload/index.js'),
      sandbox: false
    }
  })

  mainWindow.on('ready-to-show', () => {
    mainWindow.show()
  })

  mainWindow.webContents.setWindowOpenHandler((details) => {
    shell.openExternal(details.url)
    return { action: 'deny' }
  })

  // HMR for renderer base on electron-vite cli.
  // Load the remote URL for development or the local html file for production.
  if (is.dev && process.env['ELECTRON_RENDERER_URL']) {
    mainWindow.loadURL(process.env['ELECTRON_RENDERER_URL'])
  } else {
    mainWindow.loadFile(join(__dirname, '../renderer/index.html'))
  }

  // 開発環境外、動作環境がwindowsの場合にのみ環境変数を設定する
  if (!is.dev && platform.isWindows) {
    const pathArray: Array<string> = __dirname.split(path.sep).filter(Boolean)
    const asarIndex: number = pathArray.indexOf('app.asar')
    pathArray.splice(asarIndex)

    const seManagerPathArray: Array<string> = pathArray.concat([
      'app.asar.unpacked',
      'node_modules',
      'selenium-webdriver',
      'bin',
      'windows',
      'selenium-manager.exe'
    ])

    // SE_MANAGER_PATHに明示的にPATHを設定する
    process.env.SE_MANAGER_PATH = path.join(...seManagerPathArray)
  }
}

このようにすることで createWindow() が呼ばれたタイミングで条件に一致すれば環境変数にpathが明示的に設定されるようになります。

該当箇所のみを抜き出すと以下のようになり、動作としては初期のpathから app.asar 以降を削除し、 unpacked 配下のパッキングされていない箇所に対してpathを向けるようにします。

main.ts
  if (!is.dev && platform.isWindows) {
    const pathArray: Array<string> = __dirname.split(path.sep).filter(Boolean)
    const asarIndex: number = pathArray.indexOf('app.asar')
    pathArray.splice(asarIndex)

    const seManagerPathArray: Array<string> = pathArray.concat([
      'app.asar.unpacked',
      'node_modules',
      'selenium-webdriver',
      'bin',
      'windows',
      'selenium-manager.exe'
    ])
    process.env.SE_MANAGER_PATH = path.join(...seManagerPathArray)
  }

また、当方の環境がwindowsということもあり今回はwindows向けの設定のみ記述しましたが、 @electron-toolkit/utilsplatform を使用し各platform向けの条件分岐を記述すれば他環境の場合でも柔軟に設定することが可能です。

参照:https://www.selenium.dev/documentation/selenium_manager/#custom-package-managers:~:text=from version 4.26.0.-,Building a Custom Selenium Manager,-In order to

おわりに

実際に調べると、webdriverのpathを明示的に指定するようなものは沢山でてくるのですが、selenium-manager自体のpathを指定するような記事はあまりなかったので今回記事にしてみました。
また、今回紹介した対策の他にそもそもasarとしてパッケージしないという方法もあります。

package.json
"build": {
  "asar": false
}

しかし、この方法は手軽な分、ソースコードが丸見えになることや、サイズが若干大きくなるなどのデメリットもあるのでその点を十分に考慮して選択する必要があると思います。
また、今後機会があればElectronの難読化について記事にできたらと思います。
ここまで読んでいただきありがとうございました。

Discussion