JavaScript
Go
Mac
Electron

[2016年3月時点]Mac用Electronバイナリに署名してApp Storeにアップするためのメモ

More than 1 year has passed since last update.

速報: 新しいMASバイナリを使えるようになった

現時点の情報をざっくりまとめるためのメモ。同じ情報を何度もググるのが嫌になったので。App StoreなのですべてMacが前提。動きが激しいので早晩古くなる可能性あり。更新情報を末尾に追加する。

※お気づきの点などありましたらぜひコメントお願いします。

現状突き当たっている問題

Mac 用の Electron アプリにGo言語で作ったhttpサーバーを仕込んだアプリを作った
App Storeに掲載するために署名を行い、署名自体は成功するのに、そこにEntitlementsを指定するとアプリが起動しなくなってしまう。しかしEntitlementsを指定してSandboxで動作するようにしないと、App Storeに掲載できない。

その後解決しました(後述)。

メモ

気が遠くならないうちに。

作業の流れはElectron アプリを Mac App Store に登録する手順が参考になる。

Electron

  • Node.js(JavaScript のライブラリ) と Chromium ライブラリで構成された、マルチプラットフォームのGUIフレームワーク。
  • JSやHTMLでHTML/CSSベースのGUIを構築でき、Node.JSなどのJSライブラリも使えることもあり、人気も高い。
  • できることがありすぎていちいち書いてられない。ここで関係あるのは「自動アップデート機能を自前で持っていること」。
  • 以前はAtomシェルと呼ばれていたらしい。Atomエディタはこれで作られている。
  • ちょいと検索しにくいので、electron.jsで検索するとよい。
  • 内部にいくつかセキュリティ障壁のようなものがあるのでそれをくぐり抜けながらコードを書くことになる。
  • 内部にいろんなロケールディレクトリがたくさんあるので、いらんものは削除しておこう。いらなさそうなバイナリも。
  • Mac版Electronの内部にはざっくり以下のものがある:
    • Framework(Electronのコアライブラリなど)
      • どこかで見かけたのだけど、ここにあるフレームワークに署名する際にはパスにシンボリックリンクを含めないほうがいいらしい。具体的にはVersion/A/云々みたいなパスにすべきらしい。
    • Electron ヘルパー(3種類あり、それぞれがMacアプリなので中に同じような構造がさらにある)
    • MacOS(通常はここに実行ファイルを置く: ここにはElectronバイナリが置かれるが、Goバイナリもここに置いている)
    • Resources(アイコンやロケール情報やドキュメントを置く)
    • _Codesign(署名すると作成される)

Node.js

  • JavaScript(GoogleのV8)のプラットフォーム。
  • 他にもライバルがいっぱいいる。
  • jQueryよりも裏方仕事が得意そう。
  • npmというCLIツールで各種モジュールを一発インストールできる。
    • npmのモジュールはプロジェクトのローカルディレクトリにインストールしてJSから使ったり、-gを指定してグローバルにインストールしてCLIとして実行したりできる。
    • どっちにインストールするかは利用方法によって異なるのでよく考えておこう。
    • ローカルにインストールするとそこにpackage.jsonというリストが作成される。
    • グローバルインストールには少々面倒がつきまとう?
    • --saveオプションでインストールしたモジュールはElectronの中でrequireできるようになる。
    • --save-devオプションでインストールしたモジュールはElectronに含めたくない開発用モジュールに保存するときに使う。
    • 挙動がおかしいと思ったらnpm cache cleanかつプロジェクトディレクトリのnode_modules/ディレクトリ以下を削除してからnpm installすると回復する場合がある。
  • Node.jsのバージョンアップが激しく、ぼやぼやしているとすぐ古くなるし、バージョン依存するモジュールがあったりするとつらくなるので、最初にnodebrewを使ってNode.js自体のバージョンも管理するとよい。
    • そうすればコマンド一発でNode.jsを最新版や最新安定版にアップグレードしたり、Node.jsのあるバージョン用にインストールしたモジュールをコマンド一発で新しいNode.jsに移行することもできる。

MAS

  • Mac App Storeの略。この界隈では、App Storeに掲載できるElectronアプリをMASアプリとかMASバイナリなどと呼んでいる。
  • MASアプリが通常のMac用Electronアプリ(darwinと呼ばれる)と分かれているのは、darwinにあるElectron自慢の自動アップデート機能があるとApp Storeに掲載できない(審査に通らない)という悲しい問題があるため。
  • App Store自体にアップデート配布機能があるから確かにいらないんだけど、それに対応するためにdarwinから自動アップデート機能やクラッシュリポート機能などを除去したMASバイナリがわざわざ用意されている。つまりApp Storeに掲載するならMASバイナリを使わなくてはならない。
  • しかし困ったことに、v0.35.xより新しいMASバイナリには不具合があって(どうもChromium由来っぽい)、アップ後にAppleのレビューに通らないのだそうだ。従って、MASバージョンは今のところ0.35.xを使わなければならない(現在は回避方法あり: https://github.com/electron/electron/issues/3871#issuecomment-211883141)。

Electron-packager

  • Electron用に書いたJSやHTMLなどを一括でパッケージ化してくれるツール。Node.jsのモジュール。
  • Mac/Win/Linux用のパッケージをそれぞれ作れるすぐれもの
  • 「MASもできます」と書いてあるけど実は未対応。6.0.0から対応するべく怒涛のように作業中みたいで、ドキュメントが一部先行しているので、何とも紛らわしい。
  • 同じリポジトリにあるelectron-builderというサブプロジェクトとの違いがまだよくわかってない。どっちも頻繁にアップデートされている。どうもElectronの自動アップデートを活かすビルダーみたいなので、今回は関係なさそう。
  • そもそも今回は上記のMASアプリを流用して自分のElectronアプリにするしかないので、packagerそのものの出番は今のところなくなった。

codesign

  • 今回の要になるXCodeコマンド。これを使ってアプリに署名する。
  • Entitlements(後述)もこれで指定する。
  • 署名には証明書が必要で、証明書を入手するには有償でApple のDeveloperとして登録し、ウェブサイトから証明書を取得し、開発用Macのキーチェーンに読み込んでおく必要がある。やり方はあちこちにあるのでググってください。
  • codesignでは、証明書の名前を--signオプションの引数に指定すれば、キーチェーンから勝手に読みだして署名してくれる。
  • codesignには--deepというオプションがあり、そのバイナリに関連する外部ファイルにも署名して回るらしい。
    • しかし--deepオプションはなるべく使うべきでないという意見があり、その割にはApple自身もこのオプションを使ってたりする。よくわからん。
  • 同じく-iオプションで Bundle ID(後述)を指定するとうまくいくという説も見た。ちょっとやってみたけど変わらなかった。調べが足りないかも。
  • codesign --verbose=4 --deep --strict アプリ名などとするとバリデーションもできる。

spctl

  • このXCodeコマンドは、署名されたバイナリがストアで通るかどうかをチェックしたりいろいろできるらしい。
  • とりあえず spctl --assess --verbose --type execute --raw -v アプリ名みたいな感じでバリデーションに使っている。
    • いけそうならaccept、あかん感じならrejectと表示される。
    • --rawオプションを指定すると詳細がXMLで出力される。

証明書

Mac App Storeで使う証明書は次の2つ:

  • Mac App Distribution: 3rd Party Mac Developer Application: * (*)
  • Mac Installer Distribution: 3rd Party Mac Developer Installer: * (*)

Mac App Storeの「外」で使う証明書は次の2つ:

  • Developer ID Application: Developer ID Application: * (*)
  • Developer ID Installer: Developer ID Installer: * (*)

参考: https://github.com/electron-userland/electron-osx-sign/wiki/1.-Getting-Started

そうだ、もう一つ問題があったんだった。上のとおりなら証明書は3rd Party一択なのだけど、そっちにするとspctlでrejectされてしまう。Developer IDの証明書にするとacceptと表示される。何でだろう。本番でもrejectされるとは限らないかもしれないけど、イヤンな感じ。

Bundle ID

  • アプリを一意に識別するための、いわゆるcom.apple.アプリ名みたいな形式のID。
  • 自分勝手に作ってもStoreに拒否されるだけなので、iTunes ConnectかどこかでApple指定の形式で登録しておく必要がある。
  • 普通は自分のドメイン名を逆さにして登録する。
  • アプリのInfo.plist(後述)にもこれと正確に同じBundle IDを記述しておく必要がある。両者が一致しないと当然Storeには登録できない。

Info.plist

  • Macのアプリ内に置かれる、アプリ内部設定の総元締め。
  • XML形式なところに時代を感じる。
  • 自分でMASバイナリを加工するのに最初sedでネチョネチョ書き換えていたけど、plutilという専用のコマンドを使う方がずっと楽。
  • plutil -replaceで書き換え。
  • plutil -lintでバリデーションもできる。

Entitlements

今回立ちふさがっているもの。

  • 要するにパーミッションの指定。詳しくはAppleのサイトを参照。
  • App Storeに掲載するアプリは、安全のために実行バイナリをSandbox化しておく必要があり、Sandbox化されてないアプリはアップロードしたときに発見されてメッと怒られる。
  • Sandbox化するためのパーミッションを、Macのinfoファイル形式(つまりXML)で記述する。
    • com.apple.security.app-sandboxが最低限必要。
    • アプリにどんなことをさせるかによって、必要なEntitlementsを追加する。
    • 横着して全部足さないほうがいいとどこかで解説されていた。エントリ数に制限があるんだそうだ。必要なものだけを足すべし。
  • Electronの場合、このEntitlementファイルをchildとparentの2種類作成する。理由はよくわからない。
    • parentの方に追加のEntitlementsを記述する。parentはElectronアプリそのものに適用するらしい。
    • childにはcom.apple.security.app-sandboxの他にcom.apple.security.inheritを記述し、parentのEntitlementsを継承させる。childは、Electronアプリに含まれるすべての実行バイナリに適用するらしい。
  • しかし、このEntitlementsを適用すると自分のオレオレアプリが起動しなくなってしまうことに今困っている。

参考: https://github.com/electron-userland/electron-osx-sign/wiki/3.-App-Sandbox-and-Entitlements

electron-osx-sign

  • Electron-packagerのサブプロジェクト。
  • Electron-packagerが6.0.0になったときにマージされるらしい。
  • オプションが充実していて、Entitlementsを始めひととおりのパラメータを指定できる。
  • 署名スクリプトをbashとかでスクラッチで書くより、これを使う方が楽だと思う。
    • 今はどっちを使ってもEntitlementsを使う限り起動してくれないので一緒なのだけど...
  • このツールがありがたいのは、Electron内部にあるバイナリを全部チェックしてもれなく署名してくれている(らしい)こと。さらに--binariesで追加バイナリも指定できる。
  • とりあえずドキュメントはコード署名周りについてみっちり書かれているので読んでおいて損はない。

リンク

手順

解説

Issueなど

-App crashes when sandboxed. (codesignでは-iオプションでメインアプリと内部のヘルパーアプリにBundle IDを指定せよとのこと)
- Mac App Store Build Sandbox Help!

Electron-packager 6.0.0リリースに向けた動き関連

-Release 6.0.0 #266(以下の追加バイナリ関連でちょっと手こずってるらしい)
-Pass binaries option to electron-osx-sign #285

本家など

更新情報

electron-packager のMAS署名対応版

  • Unsupported platform mas; must be one of: darwin, linux, win32 #288を見ると、Electron-packager 6.0.0リリース前でもリポジトリからmas対応版をダウンロードできるとある。マジ?ここを参考にやってみよ。
    • 次の方法でひとまずインストールはできた:
      • プロジェクトのローカルpackage.jsonに以下のようにelectron-packager行を追加
      • npm installでmas対応版electron-packagerをプロジェクトローカルにインストール
      • ./node_modules/.bin/electron-packager Electronソース アプリ名 --platform=mas --arch=x64 --version=0.35.6
package.json
  "devDependencies": {
   (中略)
    "electron-packager": "git://github.com/electron-userland/electron-packager.git",
   (以下略)

起動方法の変更

  • 上の方法で新しいelectron-packageでmasバイナリを署名できるようにはなった。それ自体はとてもうれしい。しかし当然ながら当初の問題は相変わらず。
  • これまではGoからElectronをラウンチすることにこだわってしまっていたのだけど、どうもそれがバッドプラクティスだったような気がしている。もっと素直に、ElectronからGoを起動する方法に切り替えてみる。
    • GoコードからElectronバイナリを削除し、単なるhttpサーバーにする
    • Goコードの置き場所をMacOS/からResouces/に変更
      • Electronバイナリの名前がGoバイナリと同じになるので同じMacOSディレクトリに置けないため
    • ElectronからどうにかしてResources以下に置いたGoバイナリをラウンチする
      • Electronが終了したら確実にGoバイナリも死んで欲しい(プロセスを回収して欲しい)
    • go run用のスクリプトとか開発フォーメーションをもろもろ変更しないといけない

どうやらnode.jsのchild_processを使えば内蔵バイナリをラウンチできそうなのだけど、どうやったらいいだろう。関連しそうなのをメモ。
- https://github.com/atom/electron/issues/1777
- http://krasimirtsonev.com/blog/article/Nodejs-managing-child-processes-starting-stopping-exec-spawn
- もしかするとforeverでデーモン化するのがよいのか http://goo.gl/U0WqmI
- 同じようなことしている? http://www.slideshare.net/hiroyukianai/webelectron
- これも https://discuss.atom.io/t/loadurl-not-working-for-certain-site/25011

そして

ついに起動方法の切り替え完了。
Electronからの子プロセス管理が不安定だったらどうしようかと思っていたけど、require('child_process').execFile(myapp);が予想よりずっときびきび動いてくれている。
切替中に、Electronというかnode.jsのnode_modulesディレクトリの配置が今まででたらめだったことがわかり、一人恥じ入る。道理でNode.js用モジュールを足しても全然Electronで使えなかったわけだ。
そのうち別記事で書く。

さらにそして

ついにApp Storeへのアップロードに成功!sandboxのエラーも解決。
エウーレカユリイカ。
結局最初のGo->Electronという構成がキモかったのが原因ということか。素直にElectron->Goにすればよかった。

なお、署名したバイナリをcodesign --verbose=4でvalidationするとちゃんとパスするが、spctl --assess --verboseでvalidationすると「rejected」となる。にもかかわらずアップロードは正常にできた。もしかして「rejected」というのはアップロード後の話なんだろうか。

そしてさらに

electron-packagerが6.0になった

そしてさらに

暫定措置ではあるけれどelectron-packagerでもっと新しいMASバイナリを使える方法が示された

com.apple.security.temporary-exception.sbpl
(allow mach-lookup (global-name-regex #"^org.chromium.Chromium.rohitfork.[0-9]+$"))

上をparent.plistに追加してやるのだそうだ。
これを待ってましただよ。

そしてさらに

しばらく遠ざかっていたら、Electron 1.1.1 からは上の暫定措置が不要になったらしい。以下はもうparent.plistに追加しなくてよい。

<key>com.apple.security.temporary-exception.sbpl</key>
<string>(allow mach-lookup (global-name-regex #"^org.chromium.Chromium.rohitfork.[0-9]+$"))</string>

その代わり、1.1.1からはparent.plistとアプリのInfo.plistに以下の TeamIDを追加しなければならなくなった。これを行わないと、署名したアプリを起動するときにフリーズする(署名しなければフリーズしない)。

<key>com.apple.security.application-groups</key>
<string>TEAM_ID.your.bundle.id</string>

なお、TeamID は https://developer.apple.com/ にログインして左バーの Membership をクリックすれば確認できる。

electron-packagerで署名から何からすべて行うのであれば、アプリのInfo.plistへのTeamIDの追加はelectron-packagerのオプションとして、追加する内容を適当なtemp.plistみたいなファイルに保存したものを指定することになる。署名の後でInfo.plistを書き換えても不正と思われるのがオチなので。

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>ElectronTeamID</key>
    <string>自分のTeamID</string>
</dict>
</plist>

上のようなファイルを作っておいて --extend-info="plist/temp.plist" \オプションを electron-packagerの実行オプションに追加すればよい。