【nodejsシェルアプリ開発】Node.jsのSEA(Single Executable Applications)を試してみよう


※ 当ページには【広告/PR】を含む場合があります。
2024/08/21
SvelteKitとAWS Lambda@Edgeで始めるサーバーレスなハイブリット(SSR/SSG)ウェブページを楽々作成する
【nodejsアプリ開発】続・SvelteKitでポータブルなバイナリアプリを作れるか(今度はいい感じに成功)
蛸壺の技術ブログ|Node.jsのSEA(Single Executable Applications)を試してみよう

しばらくNode.jsアプリのバイナリ化の話題から離れていましたが、そうこうしているうちにNode.jsバイナリ化の代表格であった
『pkg』が開発停止となっていました。

pkgだけなく他のNode.jsのバイナリ化を目的とした有志のプロジェクトが、Node.jsのバージョンが上がるたびにその変更点の対応に追従できずにお蔵入りする傾向にあるようです。

pkg側の最後のメッセージに、

            
            pkg has been deprecated with 5.8.1 as the last release.
There are a number of successful forked versions of pkg already with various feature additions.
Further, we’re excited about Node.js 21’s support for single executable applications.

Thank you for the support and contributions over the years.
The repository will remain open and archived.
        
ということで、長い開発の歴史に幕を閉ざしました。

そこで言及されているのが、node21以降で正式に標準サポート入が決まった
『Single Executable Applications(以降、SEA)』です。

現状、Experimentalな段階で不安定な心許なさはありますが、この「SEA」を使って、以前pkg化で遊んでみた自作スネークゲームが動作できるかを検証してみます。

合同会社タコスキングダム|蛸壺の技術ブログ
【nodejsシェルアプリ開発】vercel/pkgでCLI版スネークゲームを作ってみる

vercel/pkgを使って簡単なCLIスネークゲームのバイナリをビルドして動かしてみます。


合同会社タコスキングダム|蛸壺の技術ブログ【効果的学習法レポート】nodejsをこれから学びたい人のためのオススメ書籍&教材特集

SEAのビルド手順

「SEA」はその名の通り、1つのjsファイルをターゲットとして、nodeコマンドで実行するためのバイナリ化するものです。

現状、ターゲットとするjsファイルは、「CommonJS」限定です。

なので、プロジェクト化したnode.jsアプリをバイナリ化したければ、
esbuildなどのバンドラーで、cjs形式の出力ファイルをひとまとめにしておく必要があります。

とりあえず公式のリファレンスを参考に、過去の記事で解説した「スネークゲーム」が動くかをやってみます。

参考|Node.js v22.6.0 documentation

エントリーポイントとなるJSファイル(CJS)を作成する

まず
スネークゲームの実装をそのままindex.jsとして利用します。

JSファイルがきちんとCommonJS形式でバンドルされている必要があるので、事前によく確認しておきましょう。

「sea-config.json」からblobファイルを生成する

実行ファイルに変換するにあたって、SEAでは
sea-config.jsonにしたがって、blobファイルを一旦作成する必要があります。

            
            {
    "main": "index.js",
    "output": "sea-prep.blob"
}
        
このsea-config.jsonを使って、中間ファイルとなるblobを出力します。

            
            $ node --experimental-sea-config sea-config.json
        

生node(実行ファイル)をコピー&アプリケーション名に変更

次にnode本体の実行ファイル(生node)を、アプリケーション名にリネームする形でコピーします。

ここでは、アプリケーション名を
「snake-game」とします。

            
            #Windows以外のOS
$ cp $(command -v node) snake-game

#Windowsで作業する場合
$ node -e "require('fs').copyFileSync(process.execPath, 'snake-game.exe')"
        
試しにこの時点で、snake-gameを実行してみましょう。

            
            $ ./snake-game
Welcome to Node.js v22.6.0.
Type ".help" for more information.
>
        
当然ですが、nodeが単体で立ち上がることが分かります。

バイナリからシグネチャを外す(macOSかWindowsのみ)

Linuxで作業する分には不要ですが、次の作業で使う
「postject」で躓かないようにファイルからシグネチャ情報を取り除くようにします。

            
            #macOSの場合
$ codesign --remove-signature snake-game

#Windowsの場合(オプション)
#※signtoolはWindows SDKに同梱されるツール
$ signtool remove /s snake-game.exe
        

「postject」ツールでコピーしたバイナリにblobをインジェクションする

作業の最終段階として「postject」から、nodeをコピーしたバイナリにblobを注入します。

postjectがコマンドから利用します。

            
            $ postject <注入先のnodeバイナリ> NODE_SEA_BLOB <注入するblob>
        
            
            --sentinel-fuse NODE_SEA_FUSE_fce680ab2cc467b6e072b8b5df1996b2:
    Node.jsプロジェクトの使う固有フューズ(各OS共通)
--macho-segment-name NODE_SEA:
    (macOSのみ)blobの格納されるバイナリのセグメント名
        
つまり、以下の要領で生nodeプログラムにblobを注入することになります。

            
            #Linuxの場合
$ npx postject snake-game NODE_SEA_BLOB sea-prep.blob \
    --sentinel-fuse NODE_SEA_FUSE_fce680ab2cc467b6e072b8b5df1996b2

#Windows(PowerShell)の場合
$ npx postject snake-game.exe NODE_SEA_BLOB sea-prep.blob `
    --sentinel-fuse NODE_SEA_FUSE_fce680ab2cc467b6e072b8b5df1996b2 

#Windows(コマンドプロンプト)の場合
$ npx postject snake-game.exe NODE_SEA_BLOB sea-prep.blob ^
    --sentinel-fuse NODE_SEA_FUSE_fce680ab2cc467b6e072b8b5df1996b2 

#macOSの場合
$ npx postject snake-game NODE_SEA_BLOB sea-prep.blob \
    --sentinel-fuse NODE_SEA_FUSE_fce680ab2cc467b6e072b8b5df1996b2 \
    --macho-segment-name NODE_SEA
        

バイナリにシグネチャをつけ直す(macOSかWindowsのみ)

出力されたバイナリにシグネチャを付け直します。

特にmacOSのアプリの実行時では必要になりますが、Windowsだとシグネチャなしでも動作するようです。

            
            #macOSの場合
$ codesign --sign - snake-game

#Windowsの場合(オプション):
$ signtool sign /fd SHA256 snake-game.exe
        

バイナリを実行してみる

最後に、ちゃんとスネークゲームが動作するか試してみましょう。

            
            #Windows以外
$ ./snake-game

#Windowsの場合
$ .\snake-game.exe
        
確かにバイナリ化に成功しているようです!

合同会社タコスキングダム|蛸壺の技術ブログ

しかし、若干裏でチラつく以下の警告表示が邪魔で気になります。

            
            (node:12582) ExperimentalWarning: Single executable application is an experimental feature and might change at any time
(Use `snake-game --trace-warnings ...` to show where the warning was created)
        
このExperimentalWarningを力業で消すには、

            
            $ NODE_NO_WARNINGS=1 ./snake-game
        
とするとエラーを非表示にすることができます。

合同会社タコスキングダム|蛸壺の技術ブログ


合同会社タコスキングダム|蛸壺の技術ブログ【効果的学習法レポート】nodejsをこれから学びたい人のためのオススメ書籍&教材特集

DockerコンテナでLinuxターゲットのビルド環境は整える場合の注意点

基本的にはクロスプラットホームビルドで、blobから各OSで動作するCUIコマンドがバイナリ化できます。

Linuxでビルドする場合、公式にもあるように、

            
            Single-executable support is tested regularly on CI only on the following platforms:

Windows
macOS
Linux (all distributions supported by Node.js except Alpine and all architectures supported by Node.js except s390x)

This is due to a lack of better tools to generate single-executables that can be used to test this feature on other platforms.
        
というように、Alpine以外のLinuxでサポートされています。

nodeの公式コンテナの定番であるalpineを何も考えずにビルド環境につかってしまうと、バイナリ化した実行ファイルは正常に動作しません。

alpineをやめてbookworm-slimなどのイメージをベースにするとちゃんと動くかと思います。


合同会社タコスキングダム|蛸壺の技術ブログ【効果的学習法レポート】nodejsをこれから学びたい人のためのオススメ書籍&教材特集

まとめ

以上、Node.js(CUI)アプリの実行バイナリ化で、pkgからSEAへの移行を試してみました。

未だExprerimentalの段階ながら、主だって動作しないわけではないことも分かります。

強いて贅沢を言うのであれば、出力された実行バイナリは100MBを超えてきます。

生nodeのプログラムをそのままコピーしているためある程度のサイズになることはしかた無い気もしますが、第三者へオンラインから配布するには100MB越えて、少し重さが気になるかもしれません。

そこはやり方次第ですが、blobファイルだけ提供しておき、SEAフューズは利用者の環境でやって貰っても良いかも...と考えます。

参考サイト

参考|Node single executable applicationsでnpmパッケージを使う