LoginSignup
17
6

More than 5 years have passed since last update.

Node.jsを実行可能ファイルにまとめるnexeを使ってappファイルを作る

Posted at

TL;DR

まずは、まとめから。

やりたかったこと

  • Mac 0S 10.9未満で動かしたい(具体的には10.7まではサポートしたい)
    • Electronが動かない環境
  • Node.jsを新しくインストールさせる作業はさせたくない
  • app形式で配布したい

できたこと

  • Electron向けに作っておいたindex.htmlとapp.jsは使い回し
  • nexeでNode.jsを含んだコンパイル
    • Nodeサーバを起動PC上で動作
    • 必要ファイルを起動PC上に複製
    • 起動時にブラウザ立ち上げ
  • nexeでビルドした実行可能ファイルをapp形式にする

できなかったこと

  • Chromiumを実行可能ファイルにまとめたけど、Chromiumが10.9以降じゃないと動かない。(そりゃそうだよね、Electronの中で動いているのChromiumだもんね)
    • ちなみに、Chromium.appをnexeのコンパイル時、resoucesに含めておいたらコンパイルに時間かかった。zipに固めればかなり改善された。
  • __dirnameとかchild_process.execSync("pwd").toString()で実行可能ファイルのパスを取得できると思っていたのに、取れなかった。

1. 必要なhtmlファイル、jsファイル、その他assetsを準備する

次のような構成を作りました。

assets/
├─ app.js
├─ index.html
└─ assets.zip
  • app.jsとindex.html
    • 事前に準備していたものです。より具体的には、Electronでの使用を想定してtypescriptで書き、webpackでまとめたjsとhtmlファイルです。
  • assets.zip
    • 色々入っています。zipに固めたのは、その方がファイル数減って後々nexeのビルドで時間がかからないからです。用途によってはzipに固めない方がいいと思うので、そこは適宜適切な方を選択する方向で。

2. appファイル用のディレクトリ構成を作る

MacOSXでシェルスクリプトを.app形式のアプリケーションにする
の記事を参考にしました。

以下のようなディレクトリ構成を作っておきます。

prod/Sample.app/
└─ Contents
   ├─ Info.plist
   ├─ MacOS
   └─ Resources

このSample.appがSampleという名前のアプリケーションとして起動される想定です。
Info.plistの中身は参考記事をご参照ください。
ただ、このMacOSの下にsampleというシェルスクリプトを保存しますので、そのような設定をしておいてください。

3. nexeを使ってビルドする

まず、nexeについて、GIT公式をGoogle先生に翻訳していただき、抜粋します。

Nexeは、Node.jsアプリケーションを単一の実行可能ファイルにコンパイルするコマンドラインユーティリティです。

特徴はMotivation and Featuresに載っている通りです。

今回、私がやりたかったことにマッチしたのは以下の一文。

  • ノード/ npmを必要とせずにバイナリを配布します。

ユーザの環境に新たにNodeJSを入れる必要がない!素晴らしい!

インストール

npmでインストールします。

npm install nexe

index.tsファイルを作る

以下のファイルを作ります。
このindex.tsファイルに色々と書き込んでいきます。

src/
└─ index.ts

コンパイル

私は自動コンパイルにしていましたが、tscとかを使ってindex.jsファイルを作っておきます。
そして、nexeのコンパイルに使ったjsファイルはこちら。

build.js
const nexe = require('nexe');

nexe.compile({
    input: './src/index.js',
    output: './prod/Sample.app/Contents/MacOS/sample',
    nodeVersion: '8.9.0',
    nodeTempDir: __dirname,
    resources: ["./assets/**/*"]
}, function (err) {
    if (err) console.log(err);
});

inputは呼び出すファイル。
outputはアプリケーションの起動ファイルとしたかったので、Contents/MacOSの配下にsampleという名前で作成。
nodeVersionは適当に。
nodeTempDirは、指定してあるけど、あまり関係ないかも。少なくとも、私の今回やりたかったことには関係してこなかったので、無視しても良かった。
resourcesは重要。nexeで実行可能ファイルにまとめる際に、一緒にまとめる対象です。

これでSample.appをダブルクリックすれば、nexeで固めたファイルが起動されます。

 【番外編】備忘録

a. カレントディレクトリの取得

結論から書くと、取得できませんでした。

やりたかったことを端的に書くと、
「Sample.appを配置したディレクトリまでのパスを取りたかった」
です。

より具体的に書くと、
「事前に/Sample.app/Contents/Resources/にChromiumを保存しておいて、そのChromiumにアクセス、あるいはローカルにChromiumを保存して使いたかったから、Sample.appを配置したディレクトリまでのパスを取りたかった」
です。

最終的にChromiumは使いませんでしたが、作業用のディレクトリを作る上でもカレントディレクトリは取得したかったんですけど、取得の仕方は分かりませんでした。

index.ts
import * as child_process from "child_process";

// "/"が取れた
process.cwd();

// "/"が取れた
child_process.execSync("pwd").toString()

カレントディレクトリの代わりに

カレントディレクトリが取得できなかったので以下のコードで、ローカルの作業用ディレクトリのパスを作ることにしました。
ちなみに、このパスはElectronでapp.getPath('userData');で取得されるパスを参考にしています。

index.ts
const path = `~/Library/Application Support/sample`;

b. 実行時にブラウザを起動する

Chromeがインストールされている環境なら、以下のコマンドでアプリケーションモードで起動するのがオススメです。
本当にアプリっぽくなるので。

child_process.execSync( 'open -a "Google Chrome" -n --args --app=http://localhost:' + portNo + '/' );

私が求められていた環境では使えなかったので、以下のコマンドでデフォルトブラウザに任せる。

child_process.execSync( "open http://localhost:" + portNo + "/" );

c. resourcesで指定したindex.htmlのリクエストを受けとる

普通のNodeサーバと同じです。

index.ts
import * as http from "http";
import * as fs from "fs";
import * as child_process from "child_process";

const server = http.createServer();
const indexHtml = "assets/index.html";

server.on( "request", function (
    req,
    res
) {
    // ここでreq.urlを見て処理を振り分けたりとか、色々やる

    // 静的ファイル読み込み
    fs.readFile( indexHtml, "binary", function (
        err,
        file
    ) {
        if ( err ) {
            res.writeHead( 500, { "Content-Type": "text/plain" } );
            res.write( err + "\n" );
            res.end();
            return;
        }
        res.writeHead( 200 );
        res.write( file, "binary" );
        res.end();
    } );
    return;
} );

server.listen( portNo );

// ブラウザを起動する
child_process.execSync( "open http://localhost:" + portNo + "/" );

d. resourcesで指定したzipファイルを解凍する

nexeで固めるとfsとかfs-extraとか使えるので、読み込んで解凍します。
あとは自由自在に使えます。

import * as fs from "fs";
import * as child_process from "child_process";

const workPath = "Users/hoge/fuga/";

const file = fs.readFileSync( "assets/assetes.zip" );
child_process.execSync( `unzip assets/assets.zip -d '${workPath}'` );
17
6
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
17
6