8 个版本 (稳定)
1.0.5 | 2024年2月25日 |
---|---|
1.0.4 | 2023年2月1日 |
1.0.3 | 2022年2月5日 |
1.0.2 | 2021年8月29日 |
0.1.1 | 2021年5月14日 |
#226 在 文件系统
每月 2,607 次下载
150KB
3K SLoC
Iftree: 包含文件树 🌳
将许多文件包含在您的 Rust 代码中,以创建自包含的二进制文件。
亮点:
另请参阅 相关项目。
动机
自包含的二进制文件易于分发,因为它们包含所需的任何文件数据,例如游戏资源、Web 模板等。
标准库的 std::include_str!
包含了给定文件的全部内容。Iftree 以两种方式泛化这一点
- 不仅是一个,而是可以一次包含多个文件,使用类似于
.gitignore
的格式中的 路径模式。模式灵活:您可以将多个文件夹包含在内,跳过隐藏文件,按文件扩展名过滤,选择固定文件列表等。 - 除了包含文件内容之外,文件还可以与 任何数据 字段相关联,例如额外的文件元数据。
概念上
std: include_str!("my_file")
Iftree: any_macro!("my_files/**")
用法
既然您已经了解了原因和内容,那么了解如何使用。以下快速入门展示了基本用法。
快速入门
// 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");
}
详细指南
-
将 依赖
iftree = "1.0"
添加到您的清单(Cargo.toml
)。 -
定义您的 资产类型,它只是自定义
struct
或类型别名。例如pub struct MyAsset;
-
接下来,通过注解您的资产类型来 过滤 要包含的文件。例如
#[iftree::include_file_tree("paths = '/my_assets/**'")] pub struct MyAsset;
宏参数是一个TOML字符串字面量。这里的
paths
选项支持类似于.gitignore
的路径模式,每行一个模式。这些路径默认相对于包含你的清单的文件夹。有关更多信息,请参阅paths
配置。 -
定义你的资产类型的数据字段。示例
#[iftree::include_file_tree("paths = '/my_assets/**'")] pub struct MyAsset { relative_path: &'static str, contents_bytes: &'static [u8], }
在构建你的项目时,将针对每个文件实例化资产类型一次。
默认情况下,如果存在,字段
relative_path
将填充文件路径,字段contents_bytes
将填充原始文件内容,并且通过名称识别一些其他标准字段。但是,你可以自定义以包含任意文件数据。
-
现在你可以通过生成的
ASSETS
数组访问你的文件数据。示例assert_eq!(ASSETS[0].relative_path, "my_assets/my_file"); assert_eq!(ASSETS[0].contents_bytes, b"file contents");
此外,对于每个文件
x/y/my_file
,将生成一个变量base::x::y::MY_FILE
(除非通过template.identifiers
配置禁用)。这样的变量是对相应的ASSETS
数组元素的引用。示例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");
示例
如果您想通过示例进行探索,有一个examples
文件夹。文档链接到有用的单个示例。
请注意,一些示例需要从清单的dev-dependencies
中获取额外的依赖项。
标准字段
当你只使用以下字段的子集时,将生成你的资产类型的初始化器而无需进一步配置。请参阅示例。
-
contents_bytes
: &'static [u8]
将文件内容作为字节数组,使用
std::include_bytes
。 -
contents_str
: &'static str
将文件内容解释为UTF-8字符串,使用
std::include_str
。 -
get_bytes
: fn() -> std::borrow::Cow<'static, [u8]>
在调试构建中(即,当
debug_assertions
被启用时),此函数每次在运行时都会重新读取文件。如果发生任何错误(例如,文件不存在),则会引发恐慌。这有助于加快开发速度,因为它可以避免仅更改资产文件内容时的重建(注意,如果添加、重命名或删除资产,您仍然需要重新构建)。在发布构建中,它返回编译时包含的文件内容,使用
std::include_bytes
。 -
get_str
: fn() -> std::borrow::Cow<'static, str>
与
get_bytes
相同,但用于将文件内容解释为UTF-8字符串,使用std::include_str
。 -
relative_path
: &'static str
相对于基本文件夹的文件路径,默认情况下是包含您的清单(
Cargo.toml
)的文件夹。路径组件由一个斜杠/
分隔,独立于您的平台。
自定义文件数据
要将与您的文件关联的自定义数据,您可以使用初始化每个资产的宏。示例
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");
}
(如上所示的初始化宏my_initialize
)必须返回一个常量表达式。可以使用类似once_cell
的库(懒加载)计算非常量数据。
为了对代码生成有更多的控制,还有访问者的概念。
名称清理
在根据路径生成标识符时,会清理名称。例如,文件名404_not_found.md
被清理为标识符_404_NOT_FOUND_MD
。
清理过程旨在生成有效的、传统的标识符。基本上,它将无效标识符字符替换为下划线"_"
,并根据上下文调整大小写。
更精确地说,这些转换是按照以下顺序应用的
- 调整字母的大小写以符合命名约定
- 文件夹全部小写(因为它们映射到模块名称)。
- 文件名全部大写(因为它们映射到静态变量)。
- 没有
XID_Continue
属性的字符被替换为"_"
。ASCII中XID_Continue
字符集是[0-9A-Z_a-z]
。 - 如果第一个字符不属于
XID_Start
并且不是"_"
,则会在前面添加"_"
。ASCII中XID_Start
字符集是[A-Za-z]
。 - 如果名称是
"_"
,"crate"
,"self"
,"Self"
或"super"
,则"_"
将被附加。
便携式文件路径
为了防止在不同平台上开发时出现问题,您的文件路径应遵循以下建议
- 路径组件由一个斜杠
/
分隔(即使在 Windows 上也是如此)。 - 文件名不包含反斜杠
\
(即使在类 Unix 系统上也是如此)。
故障排除
要检查生成的代码,有一个 debug
配置。
配方
这里有一些针对给定问题的示例解决方案。
资产类型种类
与其他库的集成
- 使用
include_flate
进行压缩 - 使用 Actix Web 的文件服务器
- 使用 Rocket 的文件服务器
- 使用 Tide 的文件服务器
- 使用 warp 的文件服务器
- 使用
lazy_static
进行惰性初始化 - 使用
once_cell
进行惰性初始化 - 使用
mime_guess
的媒体类型 - 使用 Handlebars 的模板
包含文件元数据
自定义构建
相关工作
最初,我研究 Iftree 是因为我找不到一个适用于此用例的库:根据文件名扩展名从文件夹中包含文件。该项目后来发展成为一个更灵活的解决方案。
以下是我认为Iftree与相关项目在给定标准下的比较。一般来说,虽然Iftree提供了默认设置来应对常见用例,但它对任意文件数据提供了第一类支持。
项目 | 文件选择 | 包含的文件数据 | 通过以下方式访问数据 |
---|---|---|---|
include_dir 0.7 |
单个文件夹 | 路径、内容、元数据 | 文件路径、嵌套迭代器、glob模式 |
includedir 0.6 |
多个文件、多个文件夹 | 路径、内容 | 文件路径、迭代器 |
Rust Embed 8.2 | 单个文件夹、包含排除路径模式 | 路径、内容、元数据 | 文件路径、迭代器 |
std::include_bytes |
单个文件 | 内容 | 文件路径 |
std::include_str |
单个文件 | 内容 | 文件路径 |
Iftree | 通过包含排除路径模式选择多个文件 | 路径、内容、自定义 | 文件路径(通过在常数时间内解析base::x::y::MY_FILE 变量),迭代器(ASSETS 数组),自定义 |
配置参考
使用以下字段的TOML字符串配置iftree::include_file_tree
宏。
base_folder
路径模式被解释为相对于此文件夹。
除非此路径是绝对的,否则它被解释为相对于环境变量CARGO_MANIFEST_DIR
给出的文件夹。也就是说,路径模式x/y/z
解析为[CARGO_MANIFEST_DIR]/[base_folder]/x/y/z
。
参见root_folder_variable
配置来自定义此设置。
默认值:""
参见示例。
debug
是否生成包含调试信息的字符串变量DEBUG
,例如生成的代码。
默认值:false
参见示例。
paths
每行包含一个路径模式以过滤文件。
它的工作方式类似于具有相反含义的.gitignore
文件。
- 如果最后一个匹配的模式被否定(使用
!
),则排除该文件。 - 如果最后一个匹配的模式没有被否定,则包含该文件。
- 如果没有模式匹配,则排除该文件。
模式语言与.gitignore
参考中所述相同,但有此区别:您必须使用x/y/*
而不是x/y/
来包含文件夹x/y/
中的文件;要递归地包含子文件夹,请使用x/y/**
。
默认情况下,路径模式相对于环境变量CARGO_MANIFEST_DIR
,即包含您的清单(Cargo.toml
)的文件夹。有关自定义此设置的说明,请参阅base_folder
配置。
常见模式
- 排除隐藏文件:
!.*
- 仅包含具有文件扩展名
xyz
的文件:*.xyz
这是一个没有默认值的必填选项。
请参阅示例。
root_folder_variable
一个环境变量,用于将相对的base_folder
解析为绝对路径。
环境变量的值应该是绝对路径。
默认值: "CARGO_MANIFEST_DIR"
template.identifiers
是否为每个文件生成一个标识符。
给定一个文件 x/y/my_file
,会生成一个静态变量 base::x::y::MY_FILE
,嵌套在文件夹的模块中。它们的根模块是 base
,代表基础文件夹。
每个变量都是对 ASSETS
数组对应元素的引用。
生成的标识符受名称净化的影响。因此,两个文件可能映射到同一个标识符,导致关于名称定义多次的错误。代码生成不会尝试自动解决此类冲突,因为这可能会引起对哪个标识符指向哪个文件的混淆。相反,您需要重命名任何受影响的路径(但如果您不需要生成的标识符,您可以通过template.identifiers = false
来禁用它们)。
默认值: true
请参阅示例。
template.initializer
用于为每个文件实例化资产类型的宏名称。
作为输入,宏通过逗号分隔传递以下参数
- 作为字符串字面量的相对文件路径。路径组件由
/
分隔。 - 作为字符串字面量的绝对文件路径。
作为输出,宏必须返回一个常量表达式。
默认值: 通过识别标准字段构建默认初始化器。
请参阅示例。
template
visitors
这是代码生成过程的最高灵活性定制。
本质上,一个访问者将所选文件的树转换为代码。它通过在这些级别调用自定义宏来实现这一点
- 对于基础文件夹,调用一个
visit_base
宏来包装一切(顶层)。 - 对于每个文件夹,调用一个
visit_folder
宏,包装从其文件和子文件夹生成的代码(递归)。 - 对于每个文件,调用一个
visit_file
宏(底层)。
这些宏通过逗号分隔传递以下输入
visit_base
:- 作为
usize
字面量的所选文件总数。 - 按文件名在Unicode代码点顺序排列的基文件夹条目的访问者输出。
- 作为
visit_folder
:- 文件夹名称作为字符串字面量。
- 净化后的文件夹名称作为标识符。
- 按文件名在Unicode代码点顺序排列的文件夹条目的访问者输出。
visit_file
:- 文件名作为字符串字面量。
- 净化后的文件名作为标识符。
- 作为
usize
字面量的选择文件中文件的零基索引。 - 作为字符串字面量的相对文件路径。路径组件由
/
分隔。 - 作为字符串字面量的绝对文件路径。
宏visit_folder
是可选的。如果不存在,则直接将visit_file
调用的输出作为输入传递给visit_base
调用。这对于生成数组等平面结构非常有用。同样,宏visit_base
也是可选的。
您可以配置多个访问者。它们按顺序应用。
要插入访问者,请为每个访问者添加以下配置
[[template]]
visit_base = 'visit_my_base'
visit_folder = 'visit_my_folder'
visit_file = 'visit_my_file'
visit_my_…
是您相应宏的名称。
查看示例
进一步资源
依赖关系
~4–13MB
~150K SLoC