From 23b0c1bf6bbf5b6f1e12075289b39df569c853d6 Mon Sep 17 00:00:00 2001 From: ge Date: Sun, 12 Jan 2025 21:09:12 +0300 Subject: [PATCH] init --- .editorconfig | 8 +++ .gitattributes | 8 +++ .gitignore | 29 +++++++++ Makefile | 20 ++++++ README.md | 61 ++++++++++++++++++ cmd/mkembedfs/help.txt | 11 ++++ cmd/mkembedfs/mkembedfs.v | 52 ++++++++++++++++ src/embedfs.v | 107 ++++++++++++++++++++++++++++++++ tests/mymod/assets/example.json | 1 + tests/mymod/main.v | 7 +++ tests/mymod_test.out | 12 ++++ tests/mymod_test.v | 18 ++++++ v.mod | 7 +++ 13 files changed, 341 insertions(+) create mode 100644 .editorconfig create mode 100644 .gitattributes create mode 100644 .gitignore create mode 100644 Makefile create mode 100644 README.md create mode 100644 cmd/mkembedfs/help.txt create mode 100644 cmd/mkembedfs/mkembedfs.v create mode 100644 src/embedfs.v create mode 100644 tests/mymod/assets/example.json create mode 100644 tests/mymod/main.v create mode 100644 tests/mymod_test.out create mode 100644 tests/mymod_test.v create mode 100644 v.mod diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..01072ca --- /dev/null +++ b/.editorconfig @@ -0,0 +1,8 @@ +[*] +charset = utf-8 +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true + +[*.v] +indent_style = tab diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..9a98968 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,8 @@ +* text=auto eol=lf +*.bat eol=crlf + +*.v linguist-language=V +*.vv linguist-language=V +*.vsh linguist-language=V +v.mod linguist-language=V +.vdocignore linguist-language=ignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..918fb52 --- /dev/null +++ b/.gitignore @@ -0,0 +1,29 @@ +# Binaries for programs and plugins +main +embedfs +/mkembedfs +*.exe +*.exe~ +*.so +*.dylib +*.dll + +# Ignore binary output folders +bin/ + +# Ignore common editor/system specific metadata +.DS_Store +.idea/ +.vscode/ +*.iml + +# ENV +.env + +# vweb and database +*.db +*.js + +# Others +doc +*_generated* diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..57330cf --- /dev/null +++ b/Makefile @@ -0,0 +1,20 @@ +SRC_DIR ?= src +DOC_DIR ?= doc +TESTS_DIR ?= . + +all: test + +test: + v test $(TESTS_DIR) + +doc: + v doc -f html -m ./$(SRC_DIR) -o $(DOC_DIR) + +serve: clean doc + v -e "import net.http.file; file.serve(folder: '$(DOC_DIR)')" + +clean: + rm -r $(DOC_DIR) || true + +cli: + v cmd/mkembedfs -o mkembedfs diff --git a/README.md b/README.md new file mode 100644 index 0000000..689aa20 --- /dev/null +++ b/README.md @@ -0,0 +1,61 @@ +## Code generator for embedding directories with files into executables + +The `$embed_file` statement in V embeds only a single file. This module makes +it easy to embed entire directories into the final executable. + +File embedding in V is done at compile time, and unfortunately there is no way +to dynamically embed arbitrary files into an application. The `embedfs` module +is a simple code generator that creates a separate `.v` file with the code for +embedding files. That is, the embedfs call must be made before the code is +compiled. So embedfs is a build dependency. + +``` +v install --git https://github.com/gechandesu/embedfs +``` + +## Usage + +For example you have following file structure: + +``` +v.mod +src/ + main.v + assets/ + css/style.css + js/app.js +``` + +Lets embed the `assets` directory. + +Create `embed_assets.vsh` next to your v.mod: + +```v +#!/usr/bin/env -S v + +import embedfs + +chdir('src')! +assets := embedfs.CodeGenerator{ + path: 'assets' +} +write_file('assets_generated.v', assets.generate())! +``` + +Run it: + +``` +v run embed_assets.vsh +``` + +Now you have `src/assets_generated.v`. Take a look inside it. So you can use +`embedfs` const in `src/main.v` in this way: + +```v +module main + +fn main() { + style := embedfs.files['assets/css/style.css']! + println(style.data.to_string()) +} +``` diff --git a/cmd/mkembedfs/help.txt b/cmd/mkembedfs/help.txt new file mode 100644 index 0000000..1aef539 --- /dev/null +++ b/cmd/mkembedfs/help.txt @@ -0,0 +1,11 @@ +mkembedfs - generate V code for embed directories with files into executable. +usage: mkembedfs [flags] [] +flags: + -help print this help message and exit + -chdir change working directory before codegen + -prefix path prefix for file keys, none by default + -ignore path globs to ignore (allowed multiple times) + -module-name generated module name, main by default + -const-name generated constant name with data, embedfs by default + -no-pub do not make symbols in generated module public + -force-mimetype set applicetion/octet-stream mime type for unknown files diff --git a/cmd/mkembedfs/mkembedfs.v b/cmd/mkembedfs/mkembedfs.v new file mode 100644 index 0000000..1cd48a0 --- /dev/null +++ b/cmd/mkembedfs/mkembedfs.v @@ -0,0 +1,52 @@ +module main + +import os +import flag +import embedfs + +fn main() { + mut path := os.getwd() + mut flags, no_matches := flag.to_struct[FlagConfig](os.args, skip: 1, style: .v) or { + eprintln('cmdline parsing error, see -help for info') + exit(2) + } + if no_matches.len > 1 { + eprintln('unrecognized arguments: ${no_matches[1..]}') + exit(2) + } else if no_matches.len == 1 { + path = no_matches[0] + } + if flags.help { + println($embed_file('help.txt').to_string().trim_space()) + exit(0) + } + if flags.chdir != '' { + os.chdir(flags.chdir) or { + eprintln(err) + exit(1) + } + } + generator := embedfs.CodeGenerator{ + path: path + prefix: flags.prefix + ignore_patterns: flags.ignore + module_name: flags.module_name + const_name: flags.const_name + make_pub: if flags.no_pub { false } else { true } + force_mimetype: flags.force_mimetype + } + println(generator.generate()) +} + +@[xdoc: 'generate code for embed directories with files into executable.'] +@[name: 'embedfs'] +struct FlagConfig { + help bool + chdir string + prefix string + ignore []string + module_name string = 'main' + const_name string = 'embedfs' + no_pub bool + force_mimetype bool +} diff --git a/src/embedfs.v b/src/embedfs.v new file mode 100644 index 0000000..252397d --- /dev/null +++ b/src/embedfs.v @@ -0,0 +1,107 @@ +module embedfs + +import os +import strings +import net.http.mime + +pub struct CodeGenerator { +pub: + // Path to file or directory to embed + path string + // Path prefix if you want to add extra prefix for file paths + prefix string + // Glob patterns to match files the must be ignored when generating the code + ignore_patterns []string + // If true set the default MIME-type for files if no MIME-type detected + force_mimetype bool + // Default MIME-type for files. See https://www.iana.org/assignments/media-types/media-types.xhtml + default_mimetype string = 'application/octet-stream' + // Name of generated module + module_name string = 'main' + // Name of constant which will contain embedded files + const_name string = 'embedfs' + // If true make symbols in generated module public + make_pub bool +} + +struct EmbedFileSpec { + key string + path string + name string + ext string + mimetype string +} + +pub fn (g CodeGenerator) generate() string { + visible := if g.make_pub == true { 'pub ' } else { '' } + mut b := strings.new_builder(1024 * 4) + b.writeln('// !WARNING! This file is generated by embedfs module, do not edit it.') + b.writeln('') + b.writeln('module ${g.module_name}') + b.writeln('') + b.writeln('import v.embed_file { EmbedFileData }') + b.writeln('') + b.writeln('${visible}struct EmbedFileMetadata {') + b.writeln('\tkey string') + b.writeln('\tname string') + b.writeln('\text string') + b.writeln('\tmimetype string') + b.writeln('}') + b.writeln('') + b.writeln('${visible}struct EmbedFile {') + b.writeln('\tdata EmbedFileData') + b.writeln('\tmeta EmbedFileMetadata') + b.writeln('}') + b.writeln('') + b.writeln('${visible}struct EmbedFileSystem {') + b.writeln('\tfiles map[string]EmbedFile') + b.writeln('}') + b.writeln('') + b.writeln('${visible}const ${g.const_name} = EmbedFileSystem{') + b.writeln('\tfiles: {') + for filespec in g.get_files() { + b.writeln("\t\t'${filespec.key}': EmbedFile{") + b.writeln("\t\t\tdata: \$embed_file('${filespec.path}')") + b.writeln('\t\t\tmeta: EmbedFileMetadata{') + b.writeln("\t\t\t\tkey: '${filespec.key}'") + b.writeln("\t\t\t\tname: '${filespec.name}'") + b.writeln("\t\t\t\text: '${filespec.ext}'") + b.writeln("\t\t\t\tmimetype: '${filespec.mimetype}'") + b.writeln('\t\t\t}') + b.writeln('\t\t},') + } + b.writeln('\t}') + b.writeln('}') + return b.str() +} + +fn (g CodeGenerator) get_files() []EmbedFileSpec { + mut file_list := &[]string{} + os.walk(g.path, fn [mut file_list] (file string) { + file_list << file + }) + mut files := []EmbedFileSpec{} + outer: for file in file_list { + for glob in g.ignore_patterns { + if file.match_glob(glob) { + continue outer + } + } + file_key := os.join_path_single(g.prefix, file) + file_path := file // the actual path used in $embed_file() statement + file_name := os.file_name(file_path) + file_ext := os.file_ext(file_name).replace_once('.', '') + mut mimetype := mime.get_mime_type(file_ext) + if g.force_mimetype && mimetype == '' { + mimetype = g.default_mimetype + } + files << EmbedFileSpec{ + key: file_key + path: file_path + name: file_name + ext: file_ext + mimetype: mimetype + } + } + return files +} diff --git a/tests/mymod/assets/example.json b/tests/mymod/assets/example.json new file mode 100644 index 0000000..c25fd09 --- /dev/null +++ b/tests/mymod/assets/example.json @@ -0,0 +1 @@ +{"some": "JSON data"} diff --git a/tests/mymod/main.v b/tests/mymod/main.v new file mode 100644 index 0000000..3757c61 --- /dev/null +++ b/tests/mymod/main.v @@ -0,0 +1,7 @@ +module main + +fn main() { + println(embedfs) + json_file := embedfs.files['assets/example.json'] or { EmbedFile{} } + println(json_file.data.to_string().trim_space()) +} diff --git a/tests/mymod_test.out b/tests/mymod_test.out new file mode 100644 index 0000000..e54f21d --- /dev/null +++ b/tests/mymod_test.out @@ -0,0 +1,12 @@ +EmbedFileSystem{ + files: {'assets/example.json': EmbedFile{ + data: embed_file.EmbedFileData{ len: 22, path: "assets/example.json", apath: "", uncompressed: 8462c4 } + meta: EmbedFileMetadata{ + key: 'assets/example.json' + name: 'example.json' + ext: 'json' + mimetype: 'application/json' + } + }} +} +{"some": "JSON data"} diff --git a/tests/mymod_test.v b/tests/mymod_test.v new file mode 100644 index 0000000..3b52289 --- /dev/null +++ b/tests/mymod_test.v @@ -0,0 +1,18 @@ +module main + +import os +import v.util.diff +import embedfs + +fn test_mymod() { + expected_out := os.read_file('tests/mymod_test.out')! + os.chdir('tests/mymod')! + gen := embedfs.CodeGenerator{ + path: 'assets' + make_pub: false + } + os.write_file('assets_generated.v', gen.generate())! + ret := os.execute('sh -c "v run ."') + dump(diff.compare_text(ret.output, expected_out)!) + assert ret.output == expected_out +} diff --git a/v.mod b/v.mod new file mode 100644 index 0000000..1c84f64 --- /dev/null +++ b/v.mod @@ -0,0 +1,7 @@ +Module { + name: 'embedfs' + description: 'Code generator for embedding directories with files into executables' + version: '0.0.1' + license: 'Unlicense' + dependencies: [] +}