Tech Racho エンジニアの「?」を「!」に。
  • Ruby / Rails以外の開発一般

electron-builder による Windows インストーラー (NSIS) 作成でやりたかったことの実現方法まとめ

少し前に electron-builder を使用して Windows インストーラー (NSIS) の作成をしたのですが、中々苦戦したのでその備忘録を兼ねて記事を書いてみます。

electron-userland/electron-builder - GitHub

この記事は electron-builder のバージョン 22.9.1 時点の話となります。
また、インストーラーを出力するまでのチュートリアル的な内容はこの記事では割愛させていただきます。

実現したかった主な仕様一覧

今回は弊社の自社製品である超教科書の Windows インストーラーを作成しました。

実現したかったのは以下のような仕様です。
特に小難しかったり一般的ではない仕様は含んでいないつもりです。が、大分苦戦しました。

  • インストールされるアプリの exe のファイル名は「cho-textbook.exe」とする
  • インストーラーのメッセージや「プログラムと機能」などに表示される製品名は「超教科書」とする
  • Electron の app.getPath('userData') で取得できる appData ディレクトリの名称は「cho-textbook」とする
    • 以降このディレクトリは userData ディレクトリ と表記します
  • インストール先ディレクトリを選択できるようにする
  • インストーラーのヘッダやサイドバーに独自の画像を表示する
  • インストール時にファイルのインストール以外に以下を行う
    • HKEY_CLASSES_ROOT 配下のレジストリを追加
    • スタートメニューとデスクトップショートカットの作成を行う
      • 表示名称は「超教科書」とする
  • アンインストール時にファイルのアンインストール以外に以下を行う
    • 追加したレジストリの削除
    • 追加したスタートメニューとデスクトップショートカットの削除
    • userData ディレクトリの削除を行う
      • できればダイアログでユーザーに削除確認を行いたい
  • アップデートインストール時は userData ディレクトリを引き継ぐ

electron-builder で作成した NSIS 形式のインストーラーの基本的な動作

細かい点を説明する前に NSIS 形式のインストーラーの基本的な動作を記載しておきます。

前述の実現したい仕様に沿い、この記事内では NSIS の設定oneClick を false、allowToChangeInstallationDirectory を true に設定し、インストール先ディレクトリを選択できる形にしているものとします。

インストール

インストーラーを起動すると以下のような画面が起動し、インストール処理が進みます。

インストーラー上での操作は、必要に応じてインストール先ディレクトリを選択し「インストール」→「完了」を押すだけです。

細かい注意点としては、インストールディレクトリの名称が初期表示と同じ(基本的には製品名)であればその通りのパスにインストールされますが、違う名称のディレクトリを指定した場合、そのパスのさらに下に製品名ディレクトリが作成され、その中にインストールされるようです。

perMachine の設定が false (デフォルト)の場合は追加でインストールモードの選択(マシン単位またはユーザー単位)画面が表示されます。

メッセージは製品名の部分以外はOSの言語ごとに用意された標準のものが表示され、icon の設定(または installerIconの設定)をすれば上記の画面のように設定したアイコンが表示されます。

デフォルト設定だと createDesktopShortcutcreateStartMenuShortcut が true のため、スタートメニューとデスクトップショートカットも作成されます。

インストールする js ファイルなどは Common Configuration にある app の設定先(アプリケーションの package.json を含むディレクトリ)の内容が元となります。

レジストリ操作などの追加処理をしたい場合は Custom NSIS script を作成し、customInstall に処理を実装します。

アンインストール

インストール先ディレクトリに保存されるアンインストーラーの exe を直接起動するか、Windows の「プログラムと機能」の画面からアンインストールを実行するとアンインストーラーが起動します。

インストール時同様に画面表示に沿って「アンインストール」→「完了」と押せばアンインストールが完了します。

インストールファイル以外にスタートメニュー、デスクトップショートカットなども自動で削除されますが、customInstall などで行った処理を元に戻す場合は同様に customUnInstall
などに処理を記述する必要があります。

標準だとインストールディレクトリが丸ごと削除されますが、customRemoveFiles マクロを記述すれば削除処理を自作することも可能です。

アップデートインストール

既にインストール済みの状態でインストーラーを起動すると、インストール時と同じ画面が表示されます。
操作内容は同じですが「インストール」を押すと自動で「アンインストール」→「インストール」という流れで処理が進みます。

アップデートインストール時にインストール先ディレクトリを変更した場合も、旧バージョンのアンインストーラーによってファイルが削除されたあと、新規ディレクトリにインストールが行われます。

ここで旧バージョンという言葉を使ってしまいましたが、標準ではバージョンチェックは行われません。
単純にアンインストール→インストールを行うだけとなります。

バージョンチェックを行いたい場合は VersionCompare などを使用して自分で処理を記載する必要があります。(今回未使用のため詳細調査はしていません)

各仕様の実現方法

まず前提知識として、各種設定は Quick Setup Guide に記載の通りアプリケーションの package.json の標準フィールドと、Configuration で行います。

標準フィールドや Common Configuration の値を WindowsConfigurationNsisOptions で上書きできる設定項目もあります。

製品名や出力されるファイル名、ディレクトリ名などを指定する方法

ひとまず Common ConfigurationproductName に「超教科書」を設定してみます。

すると、以下の部分にこの名称が適用されます。

  • インストールされるアプリの exe の名称
    • 正確には「超教科書.exe」となる
    • ついでにアンインストーラーの名称にも適用される
  • インストーラーの exe の名称
    • 正確には「超教科書 Setup 1.0.0.exe」のような形となる、1.0.0 の部分はバージョン
  • インストールされるアプリの exe のプロパティに表示される「ファイルの説明」「製品名」の値
    • package.json の標準フィールドである description を指定しても「ファイルの説明」は上書きできないようです
  • インストーラーの exe のプロパティに表示される「製品名」の値
  • userData ディレクトリの名称
  • インストーラーで表示されるメッセージに含まれる製品名部分
  • 「プログラムと機能」に表示される名称
  • スタートメニューとデスクトップショートカットの名称

今回は exe と userData ディレクトリの名称は「cho-textbook」としたいのでこれだけでは不十分です。

22.9.1 時点ではドキュメントに載ってない?気がしますが issue #204electron-packager と同様に package.json で productName が指定できるようになっているようなので、試しにこの値を「cho-textbook」にしてみたら以下の項目に適用されました。

  • userData ディレクトリの名称
  • スタートメニューとデスクトップショートカットの名称

userData ディレクトリは目的の値になりましたが、スタートメニューなどにも適用されてしまいました。
しかしこれは NsisOptionsshortcutName を「超教科書」にすれば解消します。

※2020/12/18 追記: ショートカットの名称が変わっていたのは後述する パッチファイル の影響でした、実験中に勘違いしてしまったようです

さて、ここまででアプリの exe の名称以外は目的の値になりました。しかし、この名称を変えるすべが見当たりません。
※インストーラーの exe の名称は NsisOptionsartifactName で指定できます

試したことその1: afterSign でファイル名を上書き

まずは afterSign などでスクリプトを走らせ名称を強引に上書きしてしまえばいいのでは?と思い試してみました。

結果として名称自体は変更出来ました、しかしインストール完了画面で「超教科書を実行」をした際に起動に失敗してしまいました。
それ以外にも何か問題があるかも知れないし、この方法はダメそうです。

試したことその2: electron-builder を修正する (Pull Request)

そこでさらに issue を漁ったところ同じことに悩んでいる人を発見しました。

そしてこの issue 内で参照されているコードを見る限り 22.9.1 時点では変更することが不可能と分かります。
しかも issue は解決されず終了しており、あとで再度同じ内容の issue が作られますが、放置されてまた終了しています。

ここに至り、じゃあもう自分で修正するしかないね、ということになり Common ConfigurationproductFilename という設定を追加する Pull Request を出してみました。
同じことで悩んでいる方や、内容に賛同していただける方は是非上記の Pull Request にリアクションをいただけると嬉しいです。

ただ2カ月に1回くらい?しか活動が活発にならないようで、他も含めて Pull Request が放置され気味のようです。

※2020/12/22 追記

Pull Request が承認されました。待っていればいつか正式リリースされると思います。

最終的に内部値と同名の productFilename ではなく、既に LinuxConfiguration に存在した executableName を他のプラットフォームでも使用できる形になりました。

試したことその3: electron-builder を修正する (patch-package)

この記事投稿時点ではまだ前述の Pull Request に進展はありません。

そのため今回は patch-package で修正を適用することにしました。
patch-package の使い方は検索すれば記事が結構見つかると思いますので割愛させていただきます。

今回修正したのは app-builder-lib モジュールなので、patches/app-builder-lib+22.9.1.patch というパッチファイルが出来上がりました。

パッチファイルの中身は以下のような内容です。

※2020/12/22 追記: このパッチファイルには以下の副作用があることが判明しています。パッチファイルを利用する場合はご注意ください。

  • productFilename が NsisOptionsshortcutName のデフォルト値に適用される
  • productFilename が File MacrosproductName に適用される

レジストリの登録

この辺りは他に説明している記事が結構あるので詳細は割愛させていただきます。

概要だけ書くと NsisOptionsinclude に指定した installer.nsh ファイルに customInstall マクロを記載し、その中で WriteRegStr を実行します。

注意点としては、今回実現したい仕様にある HKEY_CLASSES_ROOT 配下へのレジストリ登録はインストールモードをマシン単位にしないとうまくいかないということです。

そのため、今回は perMachine を true にしました。
この設定にすると、インストーラー起動時にUACの権限昇格確認が表示されるようになります。

また、上記で登録したレジストリをアンインストール時に削除する場合は同様に installer.nshcustomUnInstall マクロを記載し、その中で DeleteRegKey を実行します。

この customUnInstall の処理はアップデートインストール時のアンインストール処理でも起動します。

アンインストール時に userData ディレクトリも削除する

標準のアンインストール処理では userData ディレクトリまでは削除してくれません。

そのためここでも以下のように installer.nsh に以下のような記述を書く必要があります。

RMDir がディレクトリの削除処理となります。/REBOOTOK を付けると削除できなかったときに再起動を促してくれます。

その手前に SetShellVarContextcurrent を指定していますが、これをやらないと $APPDATA の内容が C:\ProgramData を指してしまいました。
(ここでは C:\Users\xxx\AppData\Roaming を指してほしい)

これはインストールモードをマシン単位にしていることが原因で、$APPDATA 使用前に current を設定してやることで Roaming の方を指してくれるようになります。

これでアンインストール時に userData ディレクトリが削除されるようになりました。

しかし、customUnInstall の処理はアップデートインストール時にも動いてしまいます。
つまりこの記述だけだとアップデートする度に、アプリ内で保存しているユーザー設定が消えてしまいます。

それでは困るので条件判定を追加してアップデートインストール時には削除しないようにする必要があります。

が、この解決にめちゃくちゃ手間取りました。ひとまず electron-builderNSIS のドキュメントを見ても何も書いてありません。
当初は各マクロの呼ばれ方でフラグ管理しようと思ったんですが、customUnInstall はアンインストーラーの exe 上で動くため customInit などで設定した値を参照できず無理でした。

いったん諦めようかとも思ったのですがこれも issue を漁っていたらいくつか似たような話を見つけ、最終的に以下のような形で判定できることが分かりました。

GetParameters はコマンドラインパラメーターを取得する機能で、アップデートインストール時はアンインストーラーの exe 起動時に --updated が渡されるのでそれで判定しています。

これでひとまずやりたいことはできました。
今回はこれに加え、アンインストール時にユーザーに削除確認のダイアログを出して userData ディレクトリを残す選択肢も用意してあげることにしました。

結果的に以下のようなコードになっています。

インストーラーが標準で多言語対応のため、一応2言語対応だけしてみましたが、ちょっとダサいですね・・・。
メッセージの多言語化はちゃんとやるなら別の方法を取った方が良さそうです。

インストーラーのヘッダやサイドバーに独自の画像を表示する

これは NsisOptionsinstallerHeaderinstallerSidebar を設定するだけだから簡単!と思ったらそうではなく・・・。 😢

確かに画像を表示するだけであればそれで完了です。

実際表示に成功しました。が、なんか横に伸びてる・・・。
各画像の推奨サイズは installerHeader150x57installerSidebar164x314px らしいのでその通り指定しているのですが・・・。

これも検索でなかなか情報が見つからず大分解決に手間取りましたが、NSIS の Forums でそれらしい情報が見つかりました。

これを見るとなんと「日中韓のフォントが使用される場合」に画像が引き延ばされておかしくなる、とのこと。
試しに英語版の Windows を VM で立ち上げて確認してみたら確かにキレイに表示されました。

じゃあ、どうすればいいんだ、と思って読み進めていたら MUI_HEADERIMAGE_BITMAP_NOSTRETCH という単語が出てきます。

調べると以下のように installer.nsh に追記すれば設定ができるようです。
今までと違いマクロの中ではなく、ファイルの先頭に記述するだけでOKです。

!define MUI_HEADERIMAGE_BITMAP_NOSTRETCH

これを設定すると installerHeader に指定した画像が拡縮しなくなります。

同様に installerSidebar の画像は MUI_WELCOMEFINISHPAGE_BITMAP_NOSTRETCHMUI_UNWELCOMEFINISHPAGE_BITMAP_NOSTRETCH で指定できるようなのでこれも追加で設定します。

すると以下のようになりました。

画像自体はキレイに表示されるようになりましたが、、、これではダメですね。

そこでさらに調査を継続したところ先ほどの Forums の最後の方にリンクを見つけて飛んでみたところ AspectFitHeight という魅惑的なワードが登場しました。

これは!と思いさらに調査したところ以下のような指定ができることが分かりました。

!define MUI_HEADERIMAGE_BITMAP_STRETCH AspectFitHeight
!define MUI_WELCOMEFINISHPAGE_BITMAP_STRETCH AspectFitHeight
!define MUI_UNWELCOMEFINISHPAGE_BITMAP_STRETCH AspectFitHeight

この設定で作ったインストーラーが以下になります。

ようやくそれらしくなりました。ただ、画像が大分荒く見えます。

その後色々試した結果、以下のことをしたら大分見た目がよくなりました。

  • 画像を推奨サイズの2倍にする
  • 以下の設定を installer.nsh に追記する。(この設定もファイルの先頭に記述しておくだけでOKです)
ManifestDPIAware true

※4kディスプレイで 125% 表示の環境で検証

ManifestDPIAware を設定することでついでにフォントもキレイに表示されるようになりました。

いくつかの機能で問題が出るかも知れないので true にした場合は異なるDPI環境でテストしてね、とありますが、今のところ問題は発見されていません。

electron-builder を使ってこの記事に記載した範囲のカスタマイズで作成したインストーラーでは特に問題なさそうです。

その他諸々の対応について

以上で最初に記載した仕様は一通り出来上がったのですが、それ以外にいくつかやったことがあるのでまとめてここに記載します。

アプリケーションディレクトリ(app の設定先)内の不要ファイルをインストール対象から除外したい

今回は xxx.map と git 用の .empty を省きたかったので以下のような設定を Common Configuration に追記しました。

"build": {
  "files": [
    "!**/*.map",
    "!**/.empty"
  ]
}

これでインストールされる asar ファイルなどから該当ファイルが除外されます。

この辺りの設定の記載の仕方は Application Contents に書いてあります。

インストールフォルダに追加の LICENSE ファイルを配置したい

これも以下のような設定を Common Configuration に追記することで達成できました。

"build": {
  "extraFiles": [
    {
      "from": "build/LICENSE.BPS.txt",
      "to": "LICENSE.BPS.txt"
    }
  ]
}

この設定を追加すると開発ディレクトリに置いておいた build/LICENSE.BPS.txt がインストールディレクトリ直下に LICENSE.BPS.txt という名称で配置されます。

設定方法の詳細は前述の Application Contents のページを参照ください。

インストーラーの exe のプロパティの説明欄も埋めたい

今まで記載してきた設定内容だけだとインストーラーの exe のプロパティの「ファイルの説明」は空になります。

package.json の標準フィールドである description を指定しても「ファイルの説明」は上書きできないようです

アプリの exe の説明のところで上記のような記載をしたのですが、実はこの description の値がインストーラーの exe の「ファイルの説明」に反映されるようです。

むしろ未指定だとビルド時に description is missed in the package.json というエラーメッセージが出ます。
何故ここだけ Configuration 側の指定ではないのか・・・。

今回はインストーラーの指定はできるだけ Configuration にまとめたかったので以下の設定で上書きするようにしました。

"build": {
  "extraMetadata": {
     "description": "超教科書 Setup"
  }
}

Windows における appId の値とは

公式ドキュメントの例やら巷の記事では com.electron.xxx みたいな指定が多いのですが、よく読むと Application User Model ID for Windows と書いてあります。

この Application User Model ID の説明 を読んでみると Each section should be pascal-cased. と書いてあります。

CompanyName.ProductName.SubProduct.VersionInformation みたいな感じですね。

内容を読むと今回のアプリでは特にこの辺りは気にしなくて良さそうでしたが、一応 Bps.Xxx の形としておきました。

一応他のアプリはどんな感じなんだろう?と思って PowerShell で get-StartApps コマンドを打って確認してみました。

、、、あんまりみんな守ってなさそうですね。一応 Microsoft 製アプリは大体守っているようです。

コードサイニング証明書の適用時にエラーが発生する

コードサイニング証明書は環境変数 CSC_LINKCSC_KEY_PASSWORD を設定してからビルドすることで適用できます。
※EVコードサイニング証明書の場合は追加の設定が必要

自分の環境では特に問題なく成功したのですが、他の開発者の環境で証明書適用時だけビルドエラーになる、という報告を受けました。

First attempt to code sign failed, another attempt will be made in 15 seconds: Exit code: 1. Command failed: C:\Users\xxx\AppData\Local\electron-builder\Cache\winCodeSign\winCodeSign-2.6.0\windows-10\x64\signtool.exe sign /tr http://timestamp.digicert.com /f C:\Users\xxx\xxx\bps-codesign.pfx
(以降省略)

エラーメッセージは上記のような形です。

このメッセージだけ見てもよく分からなかったので再確認してもらったところ、「ビルドが成功する時と失敗する時がある」とのこと。

これ以上はコードを読まないと分からなかったので、エラーメッセージから該当箇所らしき場所を探り出しました。

if (e.message.includes("The file is being used by another process") || e.message.includes("The specified timestamp server either could not be reached")) {

どうやらエラー時に上記メッセージが含まれている場合になるようです。

現状この現象はそこまで困っているわけではないのでこれ以上詳しくは調べていませんが、最初のメッセージにも含まれている http://timestamp.digicert.com へのアクセス辺りで問題が発生していそうな雰囲気です。

まとめ

今回は現状 electron アプリのインストーラー作成で一番優勢に見えた electron-builder を使ってみました。

感想としては機能は大分充実していて、使いやすさも悪くないという印象です。

ただ、この記事にもあるように細かいところには手が届かないところもあります。
そしてドキュメント化されていない仕様も結構あるのでカスタマイズを頑張りたい方は electron-builder のソースをある程度読む覚悟はした方が良いかも知れません。

NSIS 周りもカスタマイズをほとんどしなければ非常に簡単に使えますが、インストーラーの画面をカスタマイズしようとすると electron-builder 側の設定だけでは無理なことが多く NSIS のスクリプトを勉強する必要があります。

ただ NSIS 以外の形式も含め、他OS向けの出力も electron-builder だけでまとめてできますし、物凄く凝ったことをしないのであれば選択肢としてはかなり有力だと思います。



CONTACT

TechRachoでは、パートナーシップをご検討いただける方からの
ご連絡をお待ちしております。ぜひお気軽にご意見・ご相談ください。