17 Commits

Author SHA1 Message Date
ge f45af2e4ba ci: Update actions, use latest V instead of weekly build 2026-05-05 23:18:03 +03:00
gechandesu f65457bf44 Update docs.yaml 2026-05-05 23:11:10 +03:00
gechandesu 6daea7e8dd Update docs.yaml 2026-05-05 23:09:15 +03:00
gechandesu 21e601d0cc Update docs.yaml 2026-05-05 23:08:07 +03:00
ge 95ceb5aeac new implementation 2026-05-05 23:02:06 +03:00
ge 171ec8fe4b rm Makefile 2025-05-29 22:47:13 +03:00
ge b7d5f4fcb1 all: stop using src/ dir, make tests reproducible, fix CI 2025-04-22 20:14:55 +03:00
ge e79f83f800 make path required 2025-04-13 07:13:37 +03:00
gechandesu 8416067007 ci: fix tests one more time 2025-04-13 06:13:38 +03:00
gechandesu 1723bf75b8 ci: fix tests again 2025-04-13 06:12:07 +03:00
gechandesu 8d68479194 ci: fix tests 2025-04-13 06:08:51 +03:00
ge 4ae38ec2c9 ci: add CI 2025-04-13 05:54:27 +03:00
ge 60dfc5f02d Add bare_map, make_const_pub, new test 2025-04-13 05:44:35 +03:00
ge 2568538cb3 cmd: update help text 2025-01-20 22:42:04 +03:00
ge 3096165165 fix test 2025-01-20 22:41:39 +03:00
ge 64a4bc9e48 add license 2025-01-14 03:06:35 +03:00
ge 6d0a074588 fix test 2025-01-13 20:58:30 +03:00
15 changed files with 296 additions and 206 deletions
+47
View File
@@ -0,0 +1,47 @@
name: Docs
on:
push:
branches: [ "master" ]
workflow_dispatch:
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v6
- name: Setup V
run: |
git clone --depth=1 https://github.com/vlang/v /tmp/v && cd /tmp/v && make
/tmp/v/v symlink
- name: Build docs
run: |
v doc -f html -m .
pushd _docs
ln -vs embedfs.html index.html
ls -alFh
popd
- name: Upload static files as artifact
id: deployment
uses: actions/upload-pages-artifact@v3
with:
path: _docs/
deploy:
needs: build
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
runs-on: ubuntu-latest
steps:
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v5
permissions:
contents: read
pages: write
id-token: write
+23
View File
@@ -0,0 +1,23 @@
name: Tests
on:
push:
branches: [ "master" ]
pull_request:
branches: [ "master" ]
workflow_dispatch:
jobs:
test:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v6
- name: Setup V
run: |
git clone --depth=1 https://github.com/vlang/v /tmp/v && cd /tmp/v && make
/tmp/v/v symlink
- name: Run tests
run: |
v -stats test .
-20
View File
@@ -1,20 +0,0 @@
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
+55 -26
View File
@@ -1,16 +1,17 @@
## Code generator for embedding directories with files into executables # Embedding Directories into Executables
The `$embed_file` statement in V embeds only a single file. This module makes The `$embed_file()` call in V embeds only a single file. This module makes it
it easy to embed entire directories into the final executable. easy to embed entire directories into the final executable.
File embedding in V is done at compile time, and unfortunately there is no way File embedding in V is done at compile time, and there is no way to dynamically
to dynamically embed arbitrary files into an application. The `embedfs` module embed arbitrary files into an application. The `embedfs` module is a simple
is a simple code generator that creates a separate `.v` file with the code for code generator that creates a separate `.v` file with the code for embedding
embedding files. That is, the embedfs call must be made before the code is files.
compiled. So embedfs is a build dependency.
## Installation
``` ```
v install --git https://github.com/gechandesu/embedfs v install https://github.com/gechandesu/embedfs
``` ```
## Usage ## Usage
@@ -18,28 +19,30 @@ v install --git https://github.com/gechandesu/embedfs
For example you have following file structure: For example you have following file structure:
``` ```
v.mod ./
src/ ├── src/
main.v │   ├── assets/
assets/ │   │   ├── css/
css/style.css │   │   │   └── style.css
js/app.js │   │   └── js/
│   │   └── app.js
│   └── main.v
└── v.mod
``` ```
Lets embed the `assets` directory. Lets embed the `assets` directory.
Create `embed_assets.vsh` next to your v.mod: Create `embed_assets.vsh` next to v.mod:
```v ```v
#!/usr/bin/env -S v #!/usr/bin/env v
import os
import embedfs import embedfs
chdir('src')! os.chdir('src')!
assets := embedfs.CodeGenerator{ assets := embedfs.generate('assets')!
path: 'assets' os.write_file('assets_generated.v', assets)!
}
write_file('assets_generated.v', assets.generate())!
``` ```
Run it: Run it:
@@ -48,14 +51,40 @@ Run it:
v run embed_assets.vsh v run embed_assets.vsh
``` ```
Now you have `src/assets_generated.v`. Take a look inside it. So you can use Now you have `src/assets_generated.v`. Take a look inside it:
`embedfs` const in `src/main.v` in this way:
```v
module main
const embed_files = {
'assets/css/style.css': $embed_file('assets/css/style.css')
'assets/js/app.js': $embed_file('assets/js/app.js')
}
```
You can use it in `src/main.v` in this way:
```v ```v
module main module main
fn main() { fn main() {
style := embedfs.files['assets/css/style.css']! style := unsafe { embed_files['assets/css/style.css'].to_string() }
println(style.data.to_string()) println(style)
} }
``` ```
The map type is `map[string]embed_file.EmbedFileData`, see the
[v.embed_file](https://modules.vlang.io/v.embed_file.html#EmbedFileData)
module docs for details.
## `bin2v` tool
Also there is `v bin2v` utility that generates the V modules with embedded
files. See:
```
v help bin2v
```
In contrast with embedfs, bin2v generates constants into which it writes
files as fixed-length byte arrays.
+24
View File
@@ -0,0 +1,24 @@
This is free and unencumbered software released into the public domain.
Anyone is free to copy, modify, publish, use, compile, sell, or
distribute this software, either in source code form or as a compiled
binary, for any purpose, commercial or non-commercial, and by any
means.
In jurisdictions that recognize copyright laws, the author or authors
of this software dedicate any and all copyright interest in the
software to the public domain. We make this dedication for the benefit
of the public at large and to the detriment of our heirs and
successors. We intend this dedication to be an overt act of
relinquishment in perpetuity of all present and future rights to this
software under copyright law.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
OTHER DEALINGS IN THE SOFTWARE.
For more information, please refer to <https://unlicense.org/>
+10 -8
View File
@@ -1,11 +1,13 @@
mkembedfs - generate V code for embed directories with files into executable. mkembedfs - generate V code to embed directories into executables.
usage: mkembedfs [flags] [<path>] usage: mkembedfs [flags] [<path>]
flags: flags:
-help print this help message and exit -help print this help message and exit.
-chdir <string> change working directory before codegen -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) -ignore <string> path globs to ignore (allowed multiple times)
-module-name <string> generated module name, main by default -prefix <string> path prefix for file keys, none by default.
-const-name <string> generated constant name with data, embedfs by default -path-prefix <string> path prefix for files in $embed_file() calls.
-no-pub do not make symbols in generated module public -compression apply zlib compression to $embed_file().
-force-mimetype set applicetion/octet-stream mime type for unknown files -module-name <string> generated module name, 'main' by default.
-const-name <string> generated const name, 'embed_files' by default.
-no-pub do not make symbols in generated module public.
-notice <string> notice comment text, set empty to disable.
+21 -16
View File
@@ -5,9 +5,9 @@ import flag
import embedfs import embedfs
fn main() { fn main() {
mut path := os.getwd() mut path := '.'
mut flags, no_matches := flag.to_struct[FlagConfig](os.args, skip: 1, style: .v) or { mut flags, no_matches := flag.to_struct[FlagConfig](os.args, skip: 1, style: .go_flag) or {
eprintln('cmdline parsing error, see -help for info') println('cmdline parsing error, see -help for info')
exit(2) exit(2)
} }
if no_matches.len > 1 { if no_matches.len > 1 {
@@ -26,27 +26,32 @@ fn main() {
exit(1) exit(1)
} }
} }
generator := embedfs.CodeGenerator{ dump(flags)
path: path dump(path)
prefix: flags.prefix code := embedfs.generate(path,
ignore_patterns: flags.ignore key_path_prefix: flags.prefix
file_path_prefix: flags.path_prefix
ignore: flags.ignore
module_name: flags.module_name module_name: flags.module_name
const_name: flags.const_name const_name: flags.const_name
make_pub: if flags.no_pub { false } else { true } const_public: !flags.no_pub
force_mimetype: flags.force_mimetype notice: flags.notice
} compression: flags.compression
println(generator.generate()) )!
print(code)
flush_stdout()
} }
@[xdoc: 'generate code for embed directories with files into executable.']
@[name: 'embedfs']
struct FlagConfig { struct FlagConfig {
mut:
help bool help bool
chdir string chdir string
prefix string
ignore []string ignore []string
prefix string
path_prefix string
module_name string = 'main' module_name string = 'main'
const_name string = 'embedfs' const_name string = 'embed_files'
no_pub bool no_pub bool
force_mimetype bool notice string = 'This file is generated by embedfs module, DO NOT EDIT!'
compression bool
} }
+21
View File
@@ -0,0 +1,21 @@
module ${module_name}
@if !skip_notice
/*
${notice}
*/
@endif
@if const_public
pub const ${const_name} = {
@else
const ${const_name} = {
@endif
@for key, value in files
@if compression
'${key}': $embed_file('${value}', .zlib)
@else
'${key}': $embed_file('${value}')
@endif
@endfor
}
+62
View File
@@ -0,0 +1,62 @@
module embedfs
import os
@[params]
pub struct EmbedFsParams {
pub:
module_name string = 'main' // name of the generated V module.
const_name string = 'embed_files' // name of the constant that will store the embedded data.
const_public bool // if true make the const public.
key_path_prefix string // path prefix for all embedded files, will be added to data map key.
file_path_prefix string // path prefix for files used in $embed_file() call in generated file.
compression bool // add zlib compression flag to $embed_file() call in generated file.
notice string = 'This file is generated by embedfs module, DO NOT EDIT!'
}
@[params]
pub struct GenerateParams {
EmbedFsParams
pub:
ignore []string // glob expressions for files that should not be embedded.
}
// generate generates the V code that embeds the contents of directory `dir`.
pub fn generate(dir string, params GenerateParams) !string {
files := collect_files(dir, params.ignore)
return generate_with(files, params.EmbedFsParams)!
}
// generate_with generates the V code that embeds the files listed in `paths`.
pub fn generate_with(paths []string, params EmbedFsParams) !string {
module_name := params.module_name
const_name := params.const_name
const_public := params.const_public
compression := params.compression
notice := params.notice
skip_notice := params.notice == ''
mut files := map[string]string{}
for file in paths {
file_path := if params.file_path_prefix == '' {
file
} else {
os.join_path_single(params.file_path_prefix, file)
}
files[os.join_path_single(params.key_path_prefix, file)] = file_path
}
result := $tmpl('embedfs.template')
return result
}
fn collect_files(path string, ignore []string) []string {
mut file_list := &[]string{}
os.walk(path, fn [mut file_list, ignore] (file string) {
for globexpr in ignore {
if file.match_glob(globexpr) {
return
}
}
file_list << file
})
return *file_list
}
-107
View File
@@ -1,107 +0,0 @@
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
}
+17
View File
@@ -0,0 +1,17 @@
import embedfs
fn test_generate() {
expected := r"module main
/*
This file is generated by embedfs module, DO NOT EDIT!
*/
const embed_files = {
'cmd/mkembedfs/mkembedfs.v': $embed_file('cmd/mkembedfs/mkembedfs.v')
'cmd/mkembedfs/help.txt': $embed_file('cmd/mkembedfs/help.txt')
}
"
data := embedfs.generate('cmd')!
assert data == expected
}
+2 -3
View File
@@ -1,7 +1,6 @@
module main module main
fn main() { fn main() {
println(embedfs) json_file := unsafe { embed_files['assets/example.json'] }
json_file := embedfs.files['assets/example.json'] or { EmbedFile{} } println(json_file.to_string().trim_space())
println(json_file.data.to_string().trim_space())
} }
-11
View File
@@ -1,12 +1 @@
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"} {"some": "JSON data"}
+5 -6
View File
@@ -5,14 +5,13 @@ import v.util.diff
import embedfs import embedfs
fn test_mymod() { fn test_mymod() {
oldpwd := os.getwd()
expected_out := os.read_file('tests/mymod_test.out')! expected_out := os.read_file('tests/mymod_test.out')!
os.chdir('tests/mymod')! os.chdir('tests/mymod')!
gen := embedfs.CodeGenerator{ gen := embedfs.generate('assets')!
path: 'assets' os.write_file('assets_generated.v', gen)!
make_pub: false ret := os.execute('${os.quoted_path(@VEXE)} run .')
}
os.write_file('assets_generated.v', gen.generate())!
ret := os.execute('sh -c "v run ."')
dump(diff.compare_text(ret.output, expected_out)!) dump(diff.compare_text(ret.output, expected_out)!)
assert ret.output == expected_out assert ret.output == expected_out
os.chdir(oldpwd)!
} }
+2 -2
View File
@@ -1,7 +1,7 @@
Module { Module {
name: 'embedfs' name: 'embedfs'
description: 'Code generator for embedding directories with files into executables' description: 'Embed directories into executables'
version: '0.0.1' version: '1.0.0'
license: 'Unlicense' license: 'Unlicense'
dependencies: [] dependencies: []
} }