Crates.io | iftree |
lib.rs | iftree |
version | 1.0.5 |
source | src |
created_at | 2021-05-14 15:27:10.122788 |
updated_at | 2024-02-25 14:24:29.475521 |
description | Include many files in your Rust code for self-contained binaries |
homepage | https://github.com/evolutics/iftree |
repository | https://github.com/evolutics/iftree |
max_upload_size | |
id | 397429 |
size | 290,720 |
Include many files in your Rust code for self-contained binaries.
Highlights:
See also related projects.
Self-contained binaries are easy to ship, as they come with any required file data such as game assets, web templates, etc.
The standard library's std::include_str!
includes the contents of a given
file. Iftree generalizes this in two ways:
.gitignore
-like format. Patterns are flexible: you can include multiple
folders, skip hidden files, filter by filename extension, select a fixed file
list, etc.Conceptually:
std: include_str!("my_file")
Iftree: any_macro!("my_files/**")
Now that you know the why and the what, learn the how. The following quick start shows the basic usage.
// Say you have these files:
//
// my_assets/
// ├── file_a
// ├── file_b
// └── folder/
// └── file_c
// Include data from this file tree in your code like so:
#[iftree::include_file_tree("paths = '/my_assets/**'")]
pub struct MyAsset {
relative_path: &'static str,
contents_str: &'static str,
}
fn main() {
// Based on this, an array `ASSETS` of `MyAsset` instances is generated:
assert_eq!(ASSETS.len(), 3);
assert_eq!(ASSETS[0].relative_path, "my_assets/file_a");
assert_eq!(ASSETS[0].contents_str, "… contents file_a\n");
assert_eq!(ASSETS[1].contents_str, "… contents file_b\n");
assert_eq!(ASSETS[2].contents_str, "… file_c\n");
// Also, variables `base::x::y::MY_FILE` are generated (named by file path):
assert_eq!(base::my_assets::FILE_A.relative_path, "my_assets/file_a");
assert_eq!(base::my_assets::FILE_A.contents_str, "… contents file_a\n");
assert_eq!(base::my_assets::FILE_B.contents_str, "… contents file_b\n");
assert_eq!(base::my_assets::folder::FILE_C.contents_str, "… file_c\n");
}
Add the dependency iftree = "1.0"
to your manifest (Cargo.toml
).
Define your asset type, which is just a custom struct
or type alias.
Example:
pub struct MyAsset;
Next, filter files to be included by annotating your asset type. Example:
#[iftree::include_file_tree("paths = '/my_assets/**'")]
pub struct MyAsset;
The macro argument is a TOML string literal. Its paths
option here supports .gitignore
-like path patterns, with one pattern per
line. These paths are relative to the folder with your manifest by default.
See the paths
configuration for more.
Define the data fields of your asset type. Example:
#[iftree::include_file_tree("paths = '/my_assets/**'")]
pub struct MyAsset {
relative_path: &'static str,
contents_bytes: &'static [u8],
}
When building your project, code is generated that instantiates the asset type once per file.
By default, a field relative_path
(if any) is populated with the file path,
a field contents_bytes
is populated with the raw file contents, and a
couple of other standard fields are recognized by name.
However, you can customize this to include arbitrary file data.
Now you can access your file data via the generated ASSETS
array.
Example:
assert_eq!(ASSETS[0].relative_path, "my_assets/my_file");
assert_eq!(ASSETS[0].contents_bytes, b"file contents");
Additionally, for each file x/y/my_file
, a variable base::x::y::MY_FILE
is generated (unless disabled via
template.identifiers
configuration). Such a
variable is a reference to the respective element of the ASSETS
array.
Example:
assert_eq!(base::my_assets::MY_FILE.relative_path, "my_assets/my_file");
assert_eq!(base::my_assets::MY_FILE.contents_bytes, b"file contents");
If you like to explore by example, there is an
examples
folder.
The documentation links to individual examples where helpful.
You could get started with the basic example. For a more complex case, see the showcase example.
Note that some examples need extra dependencies from the dev-dependencies
of
the manifest.
When you use a subset of the following fields only, an initializer for your asset type is generated without further configuration. See example.
contents_bytes
: &'static [u8]
File contents as a byte array, using
std::include_bytes
.
contents_str
: &'static str
File contents interpreted as a UTF-8 string, using
std::include_str
.
get_bytes
: fn() -> std::borrow::Cow<'static, [u8]>
In debug builds (that is, when
debug_assertions
is enabled), this function reads the file afresh on each call at runtime. It
panics if there is any error such as if the file does not exist. This helps
with faster development, as it avoids rebuilding if asset file contents are
changed only (note that you still need to rebuild if assets are added,
renamed, or removed).
In release builds, it returns the file contents included at compile time,
using
std::include_bytes
.
get_str
: fn() -> std::borrow::Cow<'static, str>
Same as get_bytes
but for the file contents interpreted as a UTF-8 string,
using
std::include_str
.
relative_path
: &'static str
File path relative to the base folder, which is the folder with your manifest
(Cargo.toml
) by default. Path components are separated by a slash /
,
independent of your platform.
To associate custom data with your files, you can plug in a macro that initializes each asset. Toy example:
macro_rules! my_initialize {
($relative_path:literal, $absolute_path:literal) => {
MyAsset {
path: $relative_path,
size_in_bytes: include_bytes!($absolute_path).len(),
}
};
}
#[iftree::include_file_tree(
"
paths = '/my_assets/**'
template.initializer = 'my_initialize'
"
)]
pub struct MyAsset {
path: &'static str,
size_in_bytes: usize,
}
fn main() {
assert_eq!(base::my_assets::FILE_A.path, "my_assets/file_a");
assert_eq!(base::my_assets::FILE_A.size_in_bytes, 20);
assert_eq!(base::my_assets::FILE_B.path, "my_assets/file_b");
}
The initializer macro (my_initialize
above) must return a constant expression.
Non-constant data can still be computed (lazily) with a library like
once_cell
.
For even more control over code generation, there is the concept of visitors.
When generating identifiers based on paths, names are sanitized. For example, a
filename 404_not_found.md
is sanitized to an identifier _404_NOT_FOUND_MD
.
The sanitization process is designed to generate valid, conventional
identifiers.
Essentially, it replaces invalid identifier characters by underscores "_"
and
adjusts the letter case to the context.
More precisely, these transformations are applied in order:
XID_Continue
are replaced by "_"
. The set
of XID_Continue
characters in ASCII is [0-9A-Z_a-z]
.XID_Start
and is not "_"
, then
"_"
is prepended. The set of XID_Start
characters in ASCII is [A-Za-z]
."_"
, "crate"
, "self"
, "Self"
, or "super"
, then "_"
is appended.To prevent issues when developing on different platforms, your file paths should follow these recommendations:
/
(even on Windows).\
(even on Unix-like systems).To inspect the generated code, there is a debug
configuration.
Here are example solutions for given problems.
type X = …
)struct
with named fields)struct
with unnamed fields)struct
without field list)include_flate
lazy_static
once_cell
mime_guess
Originally, I've worked on Iftree because I couldn't find a library for this use case: including files from a folder filtered by filename extension. The project has since developed into something more flexible.
Here is how I think Iftree compares to related projects for the given criteria. Generally, while Iftree has defaults to address common use cases, it comes with first-class support for arbitrary file data.
Project | File selection | Included file data | Data access via |
---|---|---|---|
include_dir 0.7 |
Single folder | Path, contents, metadata | File path, nested iterators, glob patterns |
includedir 0.6 |
Multiple files, multiple folders | Path, contents | File path, iterator |
Rust Embed 8.2 | Single folder, inclusion-exclusion path patterns | Path, contents, metadata | File path, iterator |
std::include_bytes |
Single file | Contents | File path |
std::include_str |
Single file | Contents | File path |
Iftree | Multiple files by inclusion-exclusion path patterns | Path, contents, custom | File path (via base::x::y::MY_FILE variables in constant time), iterator (ASSETS array), custom |
The iftree::include_file_tree
macro is configured via a
TOML string with the following fields.
base_folder
Path patterns are interpreted as relative to this folder.
Unless this path is absolute, it is interpreted as relative to the folder given
by the environment variable CARGO_MANIFEST_DIR
. That is, a path pattern
x/y/z
resolves to [CARGO_MANIFEST_DIR]/[base_folder]/x/y/z
.
See the root_folder_variable
configuration to
customize this.
Default: ""
See example.
debug
Whether to generate a string variable DEBUG
with debug information such as the
generated code.
Default: false
See example.
paths
A string with a path pattern per line to filter files.
It works like a .gitignore
file with inverted meaning:
!
), the file is excluded.The pattern language is as documented in the
.gitignore
reference, with this
difference: you must use x/y/*
instead of x/y/
to include files in a folder
x/y/
; to also include subfolders (recursively), use x/y/**
.
By default, path patterns are relative to the environment variable
CARGO_MANIFEST_DIR
, which is the folder with your manifest (Cargo.toml
). See
the base_folder
configuration to customize this.
Common patterns:
!.*
xyz
only: *.xyz
This is a required option without default.
See example.
root_folder_variable
An environment variable that is used to resolve a relative
base_folder
to an absolute path.
The value of the environment variable should be an absolute path.
Default: "CARGO_MANIFEST_DIR"
template.identifiers
Whether to generate an identifier per file.
Given a file x/y/my_file
, a static variable base::x::y::MY_FILE
is
generated, nested in modules for folders. Their root module is base
, which
represents the base folder.
Each variable is a reference to the corresponding element of the ASSETS
array.
Generated identifiers are subject to name sanitization.
Because of this, two files may map to the same identifier, causing an error
about a name being defined multiple times. The code generation does not try to
resolve such collisions automatically, as this would likely cause confusion
about which identifier refers to which file. Instead, you need to rename any
affected paths (but if you have no use for the generated identifiers, you can
just disable them with template.identifiers = false
).
Default: true
See example.
template.initializer
A macro name used to instantiate the asset type per file.
As inputs, the macro is passed the following arguments, separated by comma:
/
.As an output, the macro must return a constant expression.
Default: A default initializer is constructed by recognizing standard fields.
See example.
template
visitorsThis is the most flexible customization of the code generation process.
Essentially, a visitor transforms the tree of selected files into code. It does so by calling custom macros at these levels:
visit_base
macro is called to wrap everything (top
level).visit_folder
macro is called, wrapping the code generated
from its files and subfolders (recursively).visit_file
macro is called (bottom level).These macros are passed the following inputs, separated by comma:
visit_base
:
usize
literal.visit_folder
:
visit_file
:
usize
literal./
.The visit_folder
macro is optional. If missing, the outputs of the
visit_file
calls are directly passed as an input to the visit_base
call.
This is useful to generate flat structures such as arrays. Similarly, the
visit_base
macro is optional.
You can configure multiple visitors. They are applied in order.
To plug in visitors, add this to your configuration for each visitor:
[[template]]
visit_base = 'visit_my_base'
visit_folder = 'visit_my_folder'
visit_file = 'visit_my_file'
visit_my_…
are the names of your corresponding macros.
See examples: