概要
Nimの実行ファイルにリソースファイル(htmlやイメージファイル)を埋め込む方法の1つとしてご参考になればと思います。
リソースの埋め込み
Javaだとresourcesフォルダ配下のファイルをJarに固めたり、Windowsのバイナリならリソースコンパイラでコンパイルしたものを実行ファイルとリンクしてくれたりという機能がありますが、Nim自体にはそのような機能がありません(よね?)。
なので、ちょっと気の利いたWebアプリ(Jesterで作成)しても、実行モジュール1本をはいっと渡すだけではなくHTML/JavaScript/CSSなどのソースも付与しないとダメなわけで、ちょっとめんどくさくなるわけです。
GNU Binary Utilitiesのobjcopy
同じようなことを考える人は昔からいるもので、UNIX系ツールだとobjcopyというコマンドを使ってファイルをオブジェクトファイルに変換し、リンカーでリンクするといったことができるようです。
Nimでもobjcopyを使えばよさそうなんですが、ファイルがたくさんあった場合には、1個ずつファイルして指定するのもめんどくさい・・・(Windows版Nimでもmingw64/binにobjcopyがあるので使えそうですけど)
リソース埋め込み方式
以下2つの順番でリソース埋め込みを実装したいと思います。
- リソースとして埋め込みたいフォルダをZipの塊にし、そのZipをBase64に変換してソースに埋め込む
- 実行時にソースにあるZip(Base64)を実ファイル化し、Zipの中身を任意のフォルダに展開する
リソースを埋め込む手順(nimbleにタスクを追加します)
- 今回はhtmlフォルダ配下をリソースファイルとみなします。
- htmlフォルダ配下をzipに固めます。
- zip化されたリソースファイルを読み込んでBASE64の文字列に変換し、src/res/resource_file.nimを自動生成させます。
フォルダ構成
APPLICATION-FOLDER
+---html <= これをリソースフォルダとします
| | index.html
| |
| \---images
| nim.png
|
+---src
| | http.nim
| | nim.cfg
| |
| +---httppkg
| | main.nim <= リソースを展開する
| |
| \---res
| resources.nim <= BASE64を取得して、ZIPをファイル化して展開する
| resource_file.nim <= ZIP化したリソースをBASE64にして埋め込んだソース(自動生成)
|
\---util
embed_file.nim
make_resource.nim <= リソースフォルダをZip→Base64化するツール(nimbleから起動される)
nim.cfg
rename_app.nim
リソースを展開する手順
- アプリケーション起動時に、埋め込まれたリソース(BASE64化されたZIP)をTEMPフォルダにコピーし、アプリケーションがあるパスの配下に展開する
- アプリケーション側が、展開されたリソースをなんか使ったらいいんじゃない
htmlフォルダ配下は、こんなHTML
リソースフォルダをZIP化し、ソースファイルに埋め込む
nimbleファイルにタスクを追加します。実態は、util/make_resource.nimを実行します。
const resourceDir = "http"
task make_resource, "リソースを作成":
exec "nim c -r --out:bin/make_resource util/make_resource.nim " & resourceDir
Zipの作成&リソース埋め込みソースの自動生成するソース
import os
import base64
import zip/zipfiles
import strutils
proc makeZipToSrc(dirName, srcFileName : string) : bool =
result = true
# テンポラリフォルダにZIPファイルを作成し、その中にファイルを追加する
var zipFile : ZipArchive
let zipFilePath = getTempDir() / "sample.zip"
if zipFilePath.existsFile() == true :
zipFilePath.removeFile
if zipFile.open( zipFilePath, FileMode.fmWrite ) == false:
return
# ディレクトリを再帰探索してZIPに追加する
for f in walkDirRec(dirName, yieldFilter={pcFile}):
let tokens = f.split($DirSep)
let newFile = tokens[1..^1].join("/")
echo "newFile=>" & newFile
zipFile.addFile(newFile, f)
zipFile.close
# ZipFileを開いて、Base64を作成してソースを自動生成する
block:
let f = open(zipFilePath, FileMode.fmRead)
let b = f.readAll()
f.close
# 文字列を公開形式にしてソースを生成(後述)
let w = open(srcFileName, FileMode.fmWrite)
w.write("const resource_string* = \"\"\"" & encode(b) & "\"\"\"")
w.close
let src = "src/res/resource_file.nim"
var
dir = "html"
if paramCount() == 1:
dir = $os.commandLineParams()[0]
discard makeZipToSrc(dir,src)
ZIPが埋め込まれたソース(src/res/resource_file.nim)
こんなカンジでめっちゃ埋め込まれます。今思えば、16進データの配列でもよかったかと。
埋め込みリソースを展開する
アプリケーション起動時時(いわゆるMain処理の前)にリソースを展開します。
BASE64文字列(recource_file.resource_string)をZIPに戻して、任意のフォルダに展開します。
import os
import base64
import resource_file
import zip/zipfiles
proc expandResource* (dir: string) : bool =
# base64をzipファイルとして保存する
let s = decode(resource_string)
let zipFileName = getTempDir() / "resource.zip"
let f = open(zipFileName, FileMode.fmWrite)
f.write(s)
f.close()
# ZIPを任意のフォルダに解凍する
var z: ZipArchive
if not z.open(zipFileName):
echo "open zip fail"
z.extractAll(dir)
z.close()
# resource.zipは削除しても良いかも
メイン実行時に、(res.resources.)expandResourceを呼び出して、リソースを展開します。
import os
import docopt
import httppkg/main
import res/resources
when isMainModule:
# リソースを展開(アプリケーションのあるフォルダに展開する)
if expandResource(getAppDir()) == true :
let args = docopt(doc, version = "http 0.1.0")
# echo "args=>", args
let retCode = main(args)
quit(retCode)
else:
echo "expand resources failed"
テストしてみる
Windows/Linux/Macで動作確認
> git clone -b for-embed-file https://github.com/6in/simple-http.git resource_test
> cd resource_test
> mkdir bin
> nimble make_resource
> nimble build
> cd bin
> dir
12/16/2018 08:35 PM <DIR> .
12/16/2018 08:35 PM <DIR> ..
12/16/2018 08:35 PM 1,287,457 http.exe
12/16/2018 08:34 PM 363,535 make_resource.exe
> http --port=8888 --root=.
[file]=index.html
[file]=images/nim.png
root=>.
INFO Jester is making jokes at http://127.0.0.1:8888 (all interfaces)
# Linux/Macは、こちら
$ http --port=8888 --root=$PWD
ブラウザで表示してみると出ました。やったね。
Ctrl + C で停止してDir/Treeで確認すると、リソースが展開されています。
> dir
12/16/2018 08:36 PM <DIR> .
12/16/2018 08:36 PM <DIR> ..
12/16/2018 08:35 PM 1,287,457 http.exe
12/16/2018 08:36 PM <DIR> images
12/16/2018 08:36 PM 70 index.html
12/16/2018 08:34 PM 363,535 make_resource.exe
> tree /F /a
C:.
| http.exe
| index.html <= 展開されたリソース
| make_resource.exe
|
\---images <= 展開されたリソースフォルダ
nim.png <= 展開されたリソース
まとめ
- この手のやり方はいろいろとあるので、一つの例としてみてもらえればと思います。
- nimxあたりのソースでリソース(イメージだったかな)を埋め込むソースをちらと見かけたことがあります。
- GUI系のF/Wならなんかしら手段ありそうですけど未調査です。
- Webサーバー+サーバーロジックとそのコンテンツを1つの実行モジュールにパッケージングできるとなると、Dockerコンテナへのデプロイとか楽になるとかですかね・・・
- 途中にも書いていますが、Base64ではなく16進配列でも良かったかなと。
let resource_data : seq[int] = @[ 0x01, 0x02 , ... ] <= こんな感じ
- 作ってはみたものの、自分的に使う場面が思い当たらず。しいて言えば、ちょっとした管理系のツールをWebベースで作るとかでしょうかね。
- NimにはGUI系ライブラリがいくつかあるんですけど、どれも私的にあまりピンとこず(というか目移りしやすくてどれを選んだらよいかわからない)、各OS毎の動作確認も面倒だなと思うこともあり、ローカルのWebアプリにして配布しちゃうのもアリかなと最近感じています。
- 今回の仕組みを利用して、CUI系ツールだけど設定ファイルはWebの画面で編集できるハイブリッドなツールとかを手軽に作れるといいなあと妄想したりしています。
おまけ
- 最初は、zipモジュールを利用していたのですが、Windowsだとビルドできない箇所もあったので、リポジトリに組み込んでしまっています。