Day 14: Nim Assets (bundle your assets into single binary)
Today we will implement nimassets
project heavily inspired by go-bindata
nimassets
Typically while developing projects we have assets like (icons, images, template files, css, javascript..etc) and It can be annoying to distribute them with your application or even risk losing them or misconfiguring paths or messed-up packaging script, so packaging all of them into the same binary would be an interesting option to have. these concerns were the reason to have something like go-bindata
or Qt resource system
What do we expect?
- Having single binary that has the actually resources into the executable.
- Generating nim file out of the
resources
we want to bundle. Maybe something likenimassets -d=templatesdir -o=assetsfile.nim
- Easy access to these bundled resources using
getAsset
proc
import assetsfile
echo assetsfile.getAsset("templatesdir/index.html")
The plan
So from a very highlevel
[ Resource1 ]
[ Resource2 ] -> converter (nimassets) -> [Nim file Representing the resources list]
[ Resource3 ]
The generated file should look like
import os, tables, strformat, base64, ospaths
var assets = initTable[string, string]()
proc getAsset*(path: string): string =
result = assets[path].decode()
assets[RESOURCE1_PATH] = BASE64_ENCODE(RESOURCE1_CONTENT)
assets[RESOURCE2_PATH] = BASE64_ENCODE(RESOURCE2_CONTENT)
assets[RESOURCE3_PATH] = BASE64_ENCODE(RESOURCE3_CONTENT)
...
...
...
...
- We store the resource path and its base64 encoded content in
assets
table - We will expose 1 proc
getAsset
that takespath
and returns the content bydecoding base64
content
Implementation
Let's go top down approach for the implementation
Command line arguments
const buildBranchName* = staticExec("git rev-parse --abbrev-ref HEAD") ## \
const buildCommit* = staticExec("git rev-parse HEAD") ## \
# const latestTag* = staticExec("git describe --abbrev=0 --tags") ## \
const versionString* = fmt"0.1.0 ({buildBranchName}/{buildCommit})"
proc writeHelp() =
echo fmt"""
nimassets {versionString} (Bundle your assets into nim file)
-h | --help : show help
-v | --version : show version
-o | --output : output filename
-f | --fast : faster generation
-d | --dir : dir to include (recursively)
"""
proc writeVersion() =
echo fmt"nimassets version {versionString}"
proc cli*() =
var
compress, fast : bool = false
dirs = newSeq[string]()
output = "assets.nim"
if paramCount() == 0:
writeHelp()
quit(0)
for kind, key, val in getopt():
case kind
of cmdLongOption, cmdShortOption:
case key
of "help", "h":
writeHelp()
quit()
of "version", "v":
writeVersion()
quit()
of "fast", "f": fast = true
of "dir", "d": dirs.add(val)
of "output", "o": output = val
else:
discard
else:
discard
for d in dirs:
if not dirExists(d):
echo fmt"[-] Directory doesnt exist {d}"
quit 2 # 2 means dir doesn't exist.
# echo fmt"compress: {compress} fast: {fast} dirs:{dirs} output:{output}"
createAssetsFile(dirs, output, fast, compress)
when isMainModule:
cli()
Pretty simple, we accept list of directories (using -d
or --dir
flag) to bundle into a nim file defined using output
flag (assets.nim
by default)
--fast
flag indicates if we should use threading or not to speed up a little
compress
used to allow compression we will pass it always as false
for version information (branch and commit id) we used some git commands combined with
staticExec
to ensure these values are available at compile time
createAssetsFile
this proc is the entry to our application as it receives seq of the directories we want to bundle, the output filename, code optimization, and will make use of compress flag in the future
proc createAssetsFile(dirs:seq[string], outputfile="assets.nim", fast=false, compress=false) =
var generator: proc(s:string): string
var data = assetsFileHeader
if fast:
generator = generateDirAssetsSpawn
else:
generator = generateDirAssetsSimple
for d in dirs:
data &= generator(d)
writeFile(outputfile, data)
Here we write (the header of the assets file and the result of generating the bundle of each directory) to the outputfile
and either we bundle files one by one (using generateDirAssetsSimple
) or separately (using generateDirAssetsSpawn
)
generateDirAssetsSimple
proc generateDirAssetsSimple(dir:string): string =
var key, val, valString: string
for path in expandTilde(dir).walkDirRec():
key = path
val = readFile(path).encode()
valString = " \"\"\"" & val & "\"\"\" "
result &= fmt"""assets.add("{path}", {valString})""" & "\n\n"
We walk recursively on the directory using walkDirRec
and write down the part assets[RESOURECE_PATH] = ENCODE_BASE64(RESOURCE CONTENT)
for each file in the directory.
generateDirAssetsSpawn
proc handleFile(path:string): string {.thread.} =
var val, valString: string
val = readFile(path).encode()
valString = " \"\"\"" & val & "\"\"\" "
result = fmt"""assets.add("{path}", {valString})""" & "\n\n"
proc generateDirAssetsSpawn(dir: string): string =
var results = newSeq[FlowVar[string]]()
for path in expandTilde(dir).walkDirRec():
results.add(spawn handleFile(path))
# wait till all of them are done.
for r in results:
result &= ^r
the same but as generateDirAssetsSimple
but using spawn to do generate the assets table entry
And that's basically it.
nimassets
All of the code is based on nimassets project. Feel free to send a PR or report issues.