以下自分のためにメモ。こちらも参照。
いろいろさまよったあげく、最終的にMac用のElectronスタンドアロンアプリにGoのhttpサーバーを仕込むことにした。
なぜ素直にJSでサーバーサイドを書かないのかというと、もうGoで作っちゃったからです。
GoのhttpサーバーバイナリをElectronアプリに仕込む
そのためにはGoのhttpサーバーをElectronアプリに仕込み、以下のいずれかを実現する必要がある:
- A: Electron からGo httpサーバーを起動する
- B: Go httpサーバーからElectronを起動する
どちらを選ぶにしても、Electronを終了したらGoサーバーもしゅっと終了して欲しい。当初Aにしようかと思ったけど、先にhttpが起動してGUIが後から起動するのが自然だろうということでBに決定。
GoからElectronを起動するには
Electronの起動バイナリElectron
は、Mac用アプリ内部のContents/MacOS/の下に置かれているので、同じ場所にGoライブラリも置くことにする。
そこではたと困ったのは、Goバイナリから隣のElectron
起動バイナリを起動する方法。
package main
import (
"fmt"
"os"
"os/exec"
)
func main() {
out, err := exec.Command("Electron").Output()
if err != nil {
fmt.Println(err)
os.Exit(1)
}
fmt.Println(string(out))
}
こちらを参考に上のコードをでっちあげて起動しても、うんともすんとも言わない。コマンドをpwd
に差し替えて調べると、Goの実行時のカレントディレクトリがホームディレクトリになってしまっていた。
シェルの仕様からしてわかるような気もするけど、とにかくGoの中からカレントディレクトリを取得する必要がある。
Goのすごい人であるmattnさんの技を使わせてもらおうと思ったのだけど、Windowsでしか動かないっぽいので、試行錯誤の上https://github.com/kardianos/osextでカレントディレクトリへのフルパスを取得できた。Macでしか試してないけど、WinやLinuxでも動くといいな。
今は意味ないけど、これまたmattnさんのgithub.com/mattn/go-pipelineを使ってシェルコマンドを複数行実行できるようにしてある。最後Println
でコマンドを実行するのが妙な感じ。
なんやかやで以下のようになった。本編アプリからこの記事用に抜粋したので冗長な部分があるけど勘弁。
https://github.com/hachi8833/electron-go-sample にも同じものを置きました。
package main
import (
"flag"
"log"
"net/http"
"os"
"github.com/k0kubun/pp"
"github.com/kardianos/osext"
"github.com/mattn/go-pipeline"
"github.com/zenazn/goji"
"github.com/zenazn/goji/graceful"
"github.com/zenazn/goji/web"
)
func hello(c web.C, w http.ResponseWriter, r *http.Request) {
pp.Fprintf(w, "<h1>Hello, Electron-Go!</h1>")
}
func main() {
flag.Set("bind", ":8080")
goji.Get("/", hello)
go goji.Serve()
err := launchElectron()
if err == nil {
terminate(0)
} else {
log.Fatal(err)
terminate(1)
}
// Termination.
defer func() {
terminate(0)
}()
return
}
func terminate(code int) {
graceful.ShutdownNow()
os.Exit(code)
}
func launchElectron() error {
// Get current path
var folderPath string
var err error
cond := "run" // go build の時は何か適当なのに変える(ダサ...)
var elec, elecarg string
if cond == "run" {
// launch directory
folderPath, err = os.Getwd()
if err != nil {
return err
}
elec = "electron"
elecarg = "./Electron"
} else {
// launch binary
folderPath, err = osext.ExecutableFolder()
if err != nil {
return err
}
elec = folderPath + "/Electron"
elecarg = ""
}
out, err := pipeline.Output(
[]string{elec, elecarg},
)
if err != nil {
pp.Println(err)
return err
}
pp.Println(string(out))
return nil
}
次はElectronアプリをでっちあげる。上のelec.go
と同じディレクトリに以下を作る。ディレクトリ名をElectron
にしてあるのは、MacのElectronアプリの起動バイナリと同じ名前にしようと思って。
Electron/
|-- main.js
|-- package.json
var app = require("app");
var BrowserWindow = require("browser-window");
var Menu = require("menu");
var mainWindow = null;
app.on("window-all-closed", function(){
app.quit();
});
app.on("ready", function () {
mainWindow = new BrowserWindow({
width: 1280,
height: 1024,
'node-integration': false //これを指定しないと httpサーバー側のJSが動かない
});
// mainWindow.openDevTools();
mainWindow.loadUrl('http://localhost:8080/');
mainWindow.on("closed", function () {
mainWindow = null;
});
// Create the Application's main menu
var template = [{
label: "Application",
submenu: [
{ label: "About enno_go", selector: "orderFrontStandardAboutPanel:" },
{ type: "separator" },
{ label: "Quit", accelerator: "Command+Q", click: function() { app.quit(); }}
]}, {
label: "Edit",
submenu: [
{ label: "Undo", accelerator: "CmdOrCtrl+Z", selector: "undo:" },
{ label: "Redo", accelerator: "Shift+CmdOrCtrl+Z", selector: "redo:" },
{ type: "separator" },
{ label: "Cut", accelerator: "CmdOrCtrl+X", selector: "cut:" },
{ label: "Copy", accelerator: "CmdOrCtrl+C", selector: "copy:" },
{ label: "Paste", accelerator: "CmdOrCtrl+V", selector: "paste:" },
{ label: "Select All", accelerator: "CmdOrCtrl+A", selector: "selectAll:" }
]}
];
Menu.setApplicationMenu(Menu.buildFromTemplate(template));
});
{
"name": "elec",
"version": "1.0.0",
"description": "Web GUI for enno_go by Electron",
"main": "main.js",
"keywords": [],
"author": "hachi8833",
"license": "BSD"
}
go run
ここまで作ったら、go run elec.go
を実行。goからElectronディレクトリを叩いてGUIが表示され、elec.go内部のhttpサーバーにアクセスして「Helloなんちゃら」が表示されることと、⌘+Qで終了することを確認。
ビルド
次はビルド。はなはだかっこ悪いけど、cond := "run"
の引用符の中を何か違うのに変える。Mac用Electronアプリのための対応なので、Windowsではこんなことしなくていいと思う。
go run
のときはビルド前のElectron/ディレクトリ、go build
やgo nstall
のときはビルド後のElectronバイナリを起動するための措置。
Goプログラムで、runのときとbuildのときで挙動を変える方法がわかったらそれを使いたいのだけど、うまくググれないでいるのでとりあえずこうしてある。どなたか教えてください。フラグや環境変数を与えるみたいに、人間様がいちいち指定したくないので。
追記: 最初、
go run
とgo build
の両方でosext.ExecutableFolder()でバイナリパスを取得できていたのだけど、Goを1.6にアップグレードした後マシンをリブートしたらなぜかgo run
で取得できなくなった。代わりに本来のos.Getwd()の方がなぜかgo run
でパスを取れるようになったので、上のコードとGithubのを修正。
追記: と思ったらまたosext.ExecutableFolder()でも動くようになった。狐に化かされたんだろうか。
そして以下を順に実行し、GoとElectronをそれぞれビルド。
go build
electron-packager Electron elec --platform=darwin --arch=x64 --version=0.36.7
mv elec /elec-darwin-x64/elec.app/Contents/MacOS
でGoバイナリをElectronアプリ内に仕込む。
最後に/elec-darwin-x64/elec.app/Contents/Info.plistを開いて
<key>CFBundleExecutable</key>
<string>Electron</string>
上を以下のように書き換える
<key>CFBundleExecutable</key>
<string>elec</string>
これで/elec-darwin-x64/内のelec.appをダブルクリックすればアプリが起動する。起動直後にMacのファイアウォールが警告を表示するけど、これはシステム環境設定で変更すれば非表示にできるはず。もしかするとElectronに証明書を置いたら出なくなるのだろうか。どなたかご存じですか。
上ではpackage.jsonの置き換えなどを手動でやってるけど、本編アプリではシェルスクリプトを適当にこしらえて一括でビルドしている。
スタンドアロンにしたことでアプリのサイズは100MB超えになってしまったけど、GoでクロスプラットフォームのウェブGUIスタンドアロンアプリを開発するめどが立ったのは本当にうれしい。自分にとっては「本物のGo GUI」。
まだ誰もやってないみたいだけど、こんな感じでそのうちElectron+GolangでMac以外にも、Win/Linux/Android/iOS向けのアプリを一括で焼き焼きできるようになったらいいな。
今後の夢
今は手作り感出すぎているのだけど、もっと手を加えて洗練させて、Go+ElectronのクロスプラットフォームウェブGUIフレームワークに成長したらいいな。誰かやりませんか。
以下ざっくりとメモ。
- Go httpサーバーから直接 Electronを起動するのをやめて、ElectronとGo httpサーバーの間に何らかのラッパーを置き、httpサーバーの起動と終了をラッパーで管理する。これならGo以外にどんなhttpサーバーでも使えるようになりそうだし、Electronがアップデートされたときにも更新しやすくなるのでは。
- このラッパーをGoパッケージ化し、気楽にElectronと連携できるようにする。
- ビルドスクリプトを整備し、
go run
、go build
、Electronなどコンパイルを一括で行えるようにする。 - このラッパーに、httpディレクトリをウォッチして更新を検出する機能を追加する。そうすれば、
go run
で起動しながら動的にサーバー用アセットディレクトリ内のhtmlやcssやjsを更新して、きびきび開発できるようになりそう。動的な更新検出にはViperが使えそう。 - さらにこのラッパーで、gulpみたいなsassやcoffee scriptコンパイルも行えるようにする。普通にラッパーからgulp呼ぶ方が早そうではあるけど、一か所にまとまっている方が楽。
- ORBだのhttpサーバーだのは各自好きなものを使えるようにしつつ、ゆるく標準化しておく。
「そんな車輪などとっくに発明されておるわ!」というのがあったら教えてくださいw
追記
こうやって無理やりGoバイナリを仕込んだElectronのMASバイナリ(0.35.6 を使用)を、後はApp Storeに登録するだけになったのだけど、codesign
でentitlementをどうしても指定できない。
- Entitlement(child/parent)を指定すると、署名は成功するのにElectronアプリが起動しなくなる。
- Entitlementを指定しないと、Electronアプリは起動するけど、今度はApp Storeで「sandoboxがねえぞゴルァ」と怒られる。
- codesign -iオプションでBundle IDを指定しても変わらない。
- Electron-packagerのチームは6.0.0でelectron-osx-signをpackagerに統合しようと奮闘中らしいけどまだリリースされていない。ドキュメントの一部が先行して更新されているのでいろいろ紛らわしい。
はまった...どなたか切り抜けた方はいらっしゃいますか?