#file-path #file-tree #file #file-content #folder #embed #directory

iftree

将许多文件包含在您的 Rust 代码中,以创建自包含的二进制文件

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文件系统

Download history 2770/week @ 2024-04-27 2345/week @ 2024-05-04 1816/week @ 2024-05-11 2064/week @ 2024-05-18 2227/week @ 2024-05-25 1522/week @ 2024-06-01 800/week @ 2024-06-08 1055/week @ 2024-06-15 1485/week @ 2024-06-22 1210/week @ 2024-06-29 813/week @ 2024-07-06 709/week @ 2024-07-13 523/week @ 2024-07-20 470/week @ 2024-07-27 595/week @ 2024-08-03 895/week @ 2024-08-10

每月 2,607 次下载

MIT 许可证

150KB
3K SLoC

Iftree: 包含文件树 🌳

将许多文件包含在您的 Rust 代码中,以创建自包含的二进制文件。

亮点:

  • 使用 路径模式 包含或排除文件。
  • 编译时检查的文件查找在运行时 快速(常数时间)。
  • 可定制:将任何数据与文件关联。
  • 许多 示例,包括 食谱

另请参阅 相关项目

Test crates.io

动机

自包含的二进制文件易于分发,因为它们包含所需的任何文件数据,例如游戏资源、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");
}

详细指南

  1. 依赖 iftree = "1.0" 添加到您的清单(Cargo.toml)。

  2. 定义您的 资产类型,它只是自定义 struct 或类型别名。例如

    pub struct MyAsset;
    
  3. 接下来,通过注解您的资产类型来 过滤 要包含的文件。例如

    #[iftree::include_file_tree("paths = '/my_assets/**'")]
    pub struct MyAsset;
    

    宏参数是一个TOML字符串字面量。这里的paths选项支持类似于.gitignore的路径模式,每行一个模式。这些路径默认相对于包含你的清单的文件夹。有关更多信息,请参阅paths配置。

  4. 定义你的资产类型的数据字段。示例

    #[iftree::include_file_tree("paths = '/my_assets/**'")]
    pub struct MyAsset {
        relative_path: &'static str,
        contents_bytes: &'static [u8],
    }
    

    在构建你的项目时,将针对每个文件实例化资产类型一次。

    默认情况下,如果存在,字段relative_path将填充文件路径,字段contents_bytes将填充原始文件内容,并且通过名称识别一些其他标准字段

    但是,你可以自定义以包含任意文件数据。

  5. 现在你可以通过生成的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

清理过程旨在生成有效的、传统的标识符。基本上,它将无效标识符字符替换为下划线"_",并根据上下文调整大小写。

更精确地说,这些转换是按照以下顺序应用的

  1. 调整字母的大小写以符合命名约定
    • 文件夹全部小写(因为它们映射到模块名称)。
    • 文件名全部大写(因为它们映射到静态变量)。
  2. 没有XID_Continue属性的字符被替换为"_"。ASCII中XID_Continue字符集是[0-9A-Z_a-z]
  3. 如果第一个字符不属于XID_Start并且不是"_",则会在前面添加"_"。ASCII中XID_Start字符集是[A-Za-z]
  4. 如果名称是 "_""crate""self""Self""super",则 "_" 将被附加。

便携式文件路径

为了防止在不同平台上开发时出现问题,您的文件路径应遵循以下建议

  • 路径组件由一个斜杠 / 分隔(即使在 Windows 上也是如此)。
  • 文件名不包含反斜杠 \(即使在类 Unix 系统上也是如此)。

故障排除

要检查生成的代码,有一个 debug 配置

配方

这里有一些针对给定问题的示例解决方案。

资产类型种类

与其他库的集成

包含文件元数据

自定义构建

最初,我研究 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

用于为每个文件实例化资产类型的宏名称。

作为输入,宏通过逗号分隔传递以下参数

  1. 作为字符串字面量的相对文件路径。路径组件由 / 分隔。
  2. 作为字符串字面量的绝对文件路径。

作为输出,宏必须返回一个常量表达式

默认值: 通过识别标准字段构建默认初始化器。

请参阅示例

template visitors

这是代码生成过程的最高灵活性定制。

本质上,一个访问者将所选文件的树转换为代码。它通过在这些级别调用自定义宏来实现这一点

  • 对于基础文件夹,调用一个 visit_base 宏来包装一切(顶层)。
  • 对于每个文件夹,调用一个 visit_folder 宏,包装从其文件和子文件夹生成的代码(递归)。
  • 对于每个文件,调用一个 visit_file 宏(底层)。

这些宏通过逗号分隔传递以下输入

  • visit_base:
    1. 作为 usize 字面量的所选文件总数。
    2. 按文件名在Unicode代码点顺序排列的基文件夹条目的访问者输出。
  • visit_folder:
    1. 文件夹名称作为字符串字面量。
    2. 净化后的文件夹名称作为标识符。
    3. 按文件名在Unicode代码点顺序排列的文件夹条目的访问者输出。
  • visit_file:
    1. 文件名作为字符串字面量。
    2. 净化后的文件名作为标识符。
    3. 作为usize字面量的选择文件中文件的零基索引。
    4. 作为字符串字面量的相对文件路径。路径组件由 / 分隔。
    5. 作为字符串字面量的绝对文件路径。

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