10
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

NimAdvent Calendar 2018

Day 19

Nimでリソースの埋め込み

Last updated at Posted at 2018-12-18

概要

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つの順番でリソース埋め込みを実装したいと思います。

  1. リソースとして埋め込みたいフォルダをZipの塊にし、そのZipをBase64に変換してソースに埋め込む
  2. 実行時にソースにある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

image.png

リソースフォルダをZIP化し、ソースファイルに埋め込む

nimbleファイルにタスクを追加します。実態は、util/make_resource.nimを実行します。

http.nimble
const resourceDir = "http"
task make_resource, "リソースを作成":
  exec "nim c -r --out:bin/make_resource util/make_resource.nim " & resourceDir

Zipの作成&リソース埋め込みソースの自動生成するソース

util/make_resource.nim
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進データの配列でもよかったかと。
image.png

埋め込みリソースを展開する

アプリケーション起動時時(いわゆるMain処理の前)にリソースを展開します。
BASE64文字列(recource_file.resource_string)をZIPに戻して、任意のフォルダに展開します。

src/res/resources.nim
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を呼び出して、リソースを展開します。

main.nim
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

ブラウザで表示してみると出ました。やったね。

image.png

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だとビルドできない箇所もあったので、リポジトリに組み込んでしまっています。
10
3
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
10
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?