This commit is contained in:
ge 2025-01-12 21:09:12 +03:00
commit 23b0c1bf6b
13 changed files with 341 additions and 0 deletions

8
.editorconfig Normal file
View File

@ -0,0 +1,8 @@
[*]
charset = utf-8
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
[*.v]
indent_style = tab

8
.gitattributes vendored Normal file
View File

@ -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

29
.gitignore vendored Normal file
View File

@ -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*

20
Makefile Normal file
View File

@ -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

61
README.md Normal file
View File

@ -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())
}
```

11
cmd/mkembedfs/help.txt Normal file
View File

@ -0,0 +1,11 @@
mkembedfs - generate V code for embed directories with files into executable.
usage: mkembedfs [flags] [<path>]
flags:
-help print this help message and exit
-chdir <string> change working directory before codegen
-prefix <string> path prefix for file keys, none by default
-ignore <string> path globs to ignore (allowed multiple times)
-module-name <string> generated module name, main by default
-const-name <string> 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

52
cmd/mkembedfs/mkembedfs.v Normal file
View File

@ -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
}

107
src/embedfs.v Normal file
View File

@ -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
}

View File

@ -0,0 +1 @@
{"some": "JSON data"}

7
tests/mymod/main.v Normal file
View File

@ -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())
}

12
tests/mymod_test.out Normal file
View File

@ -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"}

18
tests/mymod_test.v Normal file
View File

@ -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
}

7
v.mod Normal file
View File

@ -0,0 +1,7 @@
Module {
name: 'embedfs'
description: 'Code generator for embedding directories with files into executables'
version: '0.0.1'
license: 'Unlicense'
dependencies: []
}