アカベコマイリ

HEAR NOTHING SEE NOTHING SAY NOTHING

Electron を試す 2 - パッケージ化におけるプラットフォーム固有処理とコンパイル分岐

October 04, 2015開発Electron, Node, Windows

Electron を試すで残された課題を解決したので、その内容を記録しておく。

  • 2015/10/5 追記 本記事のはてブにて id:Pasta-Kさんより .ico ファイルを反映させるために wine が必要との指摘があった。試してみたところ OS X 環境でもアイコンとバージョン情報変更を反映した Windows 向けパッケージを生成できたので追記した。

プラットフォーム固有処理

electron-packager--icon オプションに .ico ファイルを指定すると OS X でエラーになる問題だが Windows 環境で実行したらパッケージ化に成功。一方、Windows 環境だと OS X 版のパッケージ化がスキップされる。Linux 版は特別なオプションがないためか OS X と Windows のどちらでもパッケージ化できた。

この状況から察するに、アイコンの埋め込み処理などでプラットフォーム固有 API を利用している可能性がある。つまり完全なパッケージ化を実行する場合は対象プラットフォームと同じ環境でビルドしなければならない。

以上を踏まえリリース用の npm-scripts を再定義。

{
  "scripts": {
    "release:css": "stylus -c ./src/stylus/App.styl -o ./dist/src/bundle.css",
    "release:js-main": "browserify -t babelify ./src/js/main/Main.js --im --no-detect-globals | uglifyjs -c warnings=false -d DEBUG=false > ./dist/src/main.js",
    "release:js-renderer": "browserify -t babelify ./src/js/renderer/App.js | uglifyjs -c warnings=false -d DEBUG=false > ./dist/src/bundle.js",
    "release:clean": "rimraf ./dist/src",
    "release:copy": "cpx \"./src/**/{*.html,*.eot,*.svg,*.ttf,*.woff,package.json}\" ./dist/src",
    "release:build": "npm-run-all -s release:clean release:copy -p release:css release:js-main release:js-renderer",
    "release:pack-osx": "electron-packager ./dist/src Starter --out=dist/bin --cache=dist/cache --platform=darwin --arch=x64 --version=0.33.4 --overwrite --asar --icon=res/app.icns",
    "release:pack-win": "electron-packager ./dist/src Starter --out=dist/bin --cache=dist/cache --platform=win32 --arch=x64 --version=0.33.4 --overwrite --asar --icon=res/app.ico --version-string.CompanyName=\"Company\" --version-string.LegalCopyright=\"Copylight (C) USERNAME, All right reserved.\" --version-string.FileDescription=\"Electron application\" --version-string.OriginalFilename=\"Starter.exe\" --version-string.FileVersion=\"1.0.1\" --version-string.ProductVersion=\"1.0.1\" --version-string.ProductName=\"Starter\" --version-string.InternalName=\"Starter\"",
    "release:pack-linux": "electron-packager ./dist/src Starter --out=dist/bin --cache=dist/cache --platform=linux --arch=x64 --version=0.33.4 --overwrite --asar",
    "release:osx": "npm-run-all -s release:build release:pack-osx",
    "release:win": "npm-run-all -s release:build release:pack-win",
    "release:linux": "npm-run-all -s release:build release:pack-linux"
  }
}

従来は release:pack-XXXX 系だけ分岐して release へ統合していた。新スクリプトではリリース処理もプラットフォームごとに定義するようにしている。実行も対象プラットフォーム上で個別におこなう必要あり。

アイコンのファイル形式自体には問題なかったのだが OS X 版も透過になっていなかったりしたので作りなおした。Windows 版のアイコン作成は久しぶりに @icon変換 を利用。複数の透過 PNG からマルチ アイコンを作成できる。

Windows 版については、他にバージョン情報も設定している。これらは --version-string.XXXX オプションで指定。EXE ファイルの詳細タブに表示される。以下は参考画像。ダイアログの内容だけでなく左上のアイコンも Electron 標準ではなく独自のものに変更されていることを確認できる。

ファイル情報

ファイルのバージョンは #129 version-string.FileVersion is not working on Windows のためか Electron の 0.33.4.0 になってしまうようだ。

Windows 環境で実行する場合は Windows で node-gyp を使った npm を動かすための環境構築を先に実施しておくこと。その前に npm install してもビルドは成功するがアイコンとバージョン情報の書き変えに失敗する。そうなってしまったら環境構築した後に node_modules を削除してから改めて npm install することでビルド環境を正常化できる。

オマケとして OSX と Windows のアイコン作成方法を掲載しておく。

OS X

アイコンの元になる透過 PNG を以下のファイル名とサイズで用意。ファイル名の @2x というのは Retina 用。これの前につくサイズの倍となる。詳しくは Mac Developer Library の Optimizing for High Resolution を参照のこと。

ファイル名 サイズ
icon_16x16.png 16x16
icon_16x16@2x.png 32x32
icon_32x32.png 32x32
icon_32x32@2x.png 64x64
icon_128x128.png 128x128
icon_128x128@2x.png 256x256
icon_256x256.png 256x256
icon_256x256@2x.png 512x512
icon_512x512.png 512x512
icon_512x512@2x.png 1024x1024

これらを app.iconset というフォルダに格納。Terminal でこれが置かれたフォルダーまで移動してから以下のコマンドを実行。

$ iconutil -c icns app.iconset

すると app.icns という名前で OS X 用のアイコン ファイルが生成される。フォルダ名の app 部分がファイル名となるため、名前が決まっているなら別のものにしてもよい。

Windows

アイコンの元になる透過 PNG を用意する。OS X のものを使いまわしてもよい。必要なサイズは以下となる。

サイズ
16x16
24x24
32x32
48x48
64x64
128x128
256x256

次に @icon変換 を用意。最終バージョンは 2007/2/4 版と古いが現在でも実用十分なので気にしなくてよい。アプリを起動してウィンドウに画像ファイルをドラッグ & ドロップすると画面左のリストに登録される。リストの画像をすべて選択した状態 (スクリーンショットを参照のこと) にしておく。

@icon変換

メイン メニューから「ファイル」→「マルチiconとして保存」を選択するとリストされた画像を組み合わせたマルチ アイコン ファイルが生成される。

OS X 環境でアイコンとバージョン情報変更を反映した Windows 向けパッケージを生成する

本記事のはてブにて electron-packager でアイコン変更を含めた Windows 向けパッケージを生成するなら wine が必要という指摘があったので試してみた。wine は Homebrew からインストール。ただし X11 に依存しているらしく、先にこれを入れなさいとエラーが出る。

$ brew install wine
wine: XQuartz is required to install this formula.
You can install with Homebrew Cask:
  brew install Caskroom/cask/xquartz

You can download from:
  https://xquartz.macosforge.org
Error: An unsatisfied requirement failed this build.

最新の wine だと X11 なしで動くという話をどこかで聞いた気がする。しかしエラーでインストールを促されているので従う。

$ brew install Caskroom/cask/xquartz

次に wine をインストール。

$ brew install wine
==> Installing dependencies for wine: xz, libpng, freetype, jpeg, pkg-config, libtool, libusb, libusb-compat, fontconfig, libtiff, gd, libgphot
... 以下、略

依存パッケージが大量にあるため、すべてインストールするのに 30 分ぐらいかかった。wine を用意したので OS X 環境で Windows 向けパッケージを生成してみる。

$ npm run release:win
... 中略

> electron-starter@1.0.2 release:pack-win .../electron-starter
> electron-packager ./dist/src Starter --out=dist/bin --cache=dist/cache --platform=win32 --arch=x64 --version=0.33.6 --overwrite --asar --icon=res/app.ico --version-string.CompanyName="Company" --version-string.LegalCopyright="Copylight (C) USERNAME, All right reserved." --version-string.FileDescription="Electron application" --version-string.OriginalFilename="Starter.exe" --version-string.FileVersion="1.0.1" --version-string.ProductVersion="1.0.1" --version-string.ProductName="Starter" --version-string.InternalName="Starter"

Downloading electron-v0.33.6-win32-x64.zip
[============================================>] 100.0% of 48.85 MB (857 kB/s)
Packaging app for platform win32 x64 using electron v0.33.6
Wrote new app to dist/bin/Starter-win32-x64

ビルド成功。このパッケージを VMware FUSION 上の Windows 8.1 環境にコピーしてみたところアプリは正常動作。またアイコン変更とファイルのプロパティからバージョン情報変更が反映されていることも確認できた。

つまり OS X & wine な環境であれば全プラットフォーム向けのパッケージをまとめて生成できる。

コンパイル分岐

ソフトウェア開発においてデバッグとリリースで振るまいを変えたくなることがある。例えばデバッグ時にはログ出力や特殊なメニューを提供。リリース版ではそれらが顕在化しないようにコードごと削除する。

こうした処理を実現するためソースコードのコンパイル時におけるプリ プロセスがある。例えば Visual C++ ではコンパイル時の環境変数を定義する機能を利用可能。デバッグ版ではここに _DEBUG を指定して #ifdef _DEBUG から #endif までの区間を固有処理にする。

プロジェクトをデバッグ用ビルドした場合、コンパイルより前に環境変数が定義されるため区間のコードは有効になる。リリース用ビルドでは環境変数が存在しないから区間が不要と判定され抹消される。

この設計を JavaScript で実現する方法について watchify と連携させる場合、Source Maps と同時に指定する方法がわからなかった。また watchify によるコンパイルの都度 uglify を走らせる実行コストも気になる。そこで以下のようなハックを試みた。

まず Main プロセスのエントリーポイントあたりで global.DEBUGtrue に設定。

import App from 'app';

class Main {
  constructor() {
    this._mainWindow  = null;
    this._rendererIPC = null;

    // Compile switch
    global.DEBUG = true;
  }
}

const main = new Main();

次にこの DEBUG 変数による分岐処理を定義。

class Main {
  constructor() {
    // 初期化処理...
  }

  onReady() {
    if( DEBUG ) { console.log( 'Launched' ); }

    // Window 作成などを実行...
  }

  onWindowAllClosed() {
    if( DEBUG ) { console.log( 'Quit' ); }

    App.quit();
  }
}

const main = new Main();
App.on( 'ready',             main.onReady           );
App.on( 'window-all-closed', main.onWindowAllClosed );

このようにして以下の npm-scripts を実行すると global.DEBUG が有効となり、これを判定している if 文の内容が処理される。開発用コンパイルの変更は不要。従来どおりでよい。代わりにリリース用コンパイルを以下のように定義する。

{
  "scripts": {
    "release:js-main": "browserify -t babelify ./src/js/main/Main.js --im --no-detect-globals | uglifyjs -c warnings=false -d DEBUG=false > ./dist/src/main.js",
    "release:js-renderer": "browserify -t babelify ./src/js/renderer/App.js | uglifyjs -c warnings=false -d DEBUG=false > ./dist/src/bundle.js"
  }
}

長いので肝要の uglify 部分だけ抜粋。ファイル名が異なるだけで Main/Renderer プロセスで設定は共通。

uglifyjs -c warnings=false -d DEBUG=false > ./dist/src/main.js

-c は圧縮フラグ。warnings=false は不要箇所の検出による警告を抑止するためのもの。

-d または --define オプションの後に NAME=VALUE を指定すると、その内容でグローバル定数が宣言されているものとしてコンパイルされる。このスクリプトでは DEBUG=false としているため、これを if 文で判定している部分はすべて無効。これらはコンパイル結果から除去されるため結果としてリリース版では余計なコードが抹消される。

ここで疑問がわくはず。コード上にあった global.DEBUG = true という処理はどう扱われるのだ?と。これがあるならプリプロセス以降にグローバルな DEBUG という名前の変数が true に書き換えられ、これを判定している if 文は有効になってしまうのではないか?

実際に uglify されたファイルの当該箇所は以下のようになる。

global.DEBUG=!0

処理自体は残っており代入対象も !0 となっている。そのため判定部分は true になるだろう。しかしこの変換がおこなわれるのは uglify の処理時である。つまり global.DEBUG 自体の代入は有効な処理だが global 修飾なしに DEBUG を参照している箇所は uglify 側の定数宣言と判定がおこなわれ無効化された。

...上記の内容はコンパイル結果からみた推測である。というのも多分そう動くだろうなぁと思って実装してみたらあっさり成功してしまい私のほうがビックリしたぐらい。そのためハックと表現した。しかし現時点の uglify なら有効であり、この処理によりデバッグ用メニューをリリース版から除去できたのでよしとする。

なお Main/Rederer ともに同じ方法で分岐可能。DEBUG を有効にする処理はエントリー ポイントになるファイルへ仕込むのがよいだろう。

サンプル プロジェクト

今回の内容に加えてアイコン修正や全プラットフォーム同時生成スクリプトを追加した v1.0.2 をサンプルとして公開する。