include_blob/
lib.rs

1//! Include large files without the high compile time cost.
2//!
3//! This crate provides the [`include_blob!`] macro as an alternative to the standard
4//! [`include_bytes!`] macro, which embeds a file by copying it to an object file and linking to it.
5//! This can reduce the (quite high) compile time cost of [`include_bytes!`].
6//!
7//! In order for this to work, the user code has to first add a build script that calls the
8//! [`make_includable`] function.
9//!
10//! ```no_run
11//! // build.rs
12//! fn main() {
13//!     include_blob::make_includable("../../directory-with-big-files");
14//! }
15//! ```
16//!
17//! ```no_run
18//! let bytes: &[u8] = include_blob::include_blob!("test-project/blobs/file.txt");
19//! ```
20
21use ar_archive_writer::{
22    write_archive_to_stream, ArchiveKind, NewArchiveMember, DEFAULT_OBJECT_READER,
23};
24use object::{
25    write::{Object, StandardSection, Symbol, SymbolSection},
26    Architecture, BinaryFormat, Endianness, SymbolFlags, SymbolKind, SymbolScope,
27};
28use std::{
29    collections::hash_map::DefaultHasher,
30    env, error,
31    fs::{self, File},
32    hash::{Hash, Hasher},
33    io::{Seek, Write},
34    path::{Path, PathBuf},
35};
36
37pub use include_blob_macros::*;
38
39type Result<T> = std::result::Result<T, Box<dyn error::Error>>;
40
41/// Call this from your build script to make `path` includable via [`include_blob!`].
42///
43/// `path` can refer to a file or a directory (which processes every file in the directory).
44///
45/// `path` is relative to the directory the build script runs in (which is the package's "source
46/// directory" according to Cargo's docs, so probably the directory containing `Cargo.toml`).
47pub fn make_includable<A: AsRef<Path>>(path: A) {
48    make_includable_impl(path.as_ref()).unwrap();
49}
50
51fn make_includable_impl(path: &Path) -> Result<()> {
52    let path = path.canonicalize().unwrap_or_else(|_| {
53        panic!(
54            "could not find file '{}' (working directory is '{}')",
55            path.display(),
56            std::env::current_dir().unwrap().display(),
57        );
58    });
59    println!("cargo:rerun-if-changed={}", path.display());
60    let metadata = fs::metadata(&path)?;
61
62    if metadata.is_dir() {
63        for entry in fs::read_dir(&path)? {
64            let entry = entry?;
65            make_includable_impl(&entry.path())?;
66        }
67        Ok(())
68    } else if metadata.is_file() {
69        process_file(path, metadata)
70    } else {
71        panic!(
72            "cannot handle file type '{:?}' of '{}'",
73            metadata.file_type(),
74            path.display()
75        );
76    }
77}
78
79fn process_file(path: PathBuf, metadata: fs::Metadata) -> Result<()> {
80    let mut hasher = DefaultHasher::new();
81    path.hash(&mut hasher);
82    metadata.modified()?.hash(&mut hasher);
83    let unique_name = format!("include_blob_{:016x}", hasher.finish());
84
85    let content = fs::read(&path)?;
86
87    let (pre, post) = lib_prefix_and_suffix();
88    let out_dir = env::var("OUT_DIR")?;
89    let out_file_path = format!("{out_dir}/{pre}{unique_name}{post}");
90    let mut out_file = File::create(&out_file_path)?;
91
92    let info = TargetInfo::from_build_script_vars();
93    let mut obj_buf = Vec::new();
94    let mut object = Object::new(info.binfmt, info.arch, info.endian);
95    let section = object.add_subsection(StandardSection::ReadOnlyData, unique_name.as_bytes());
96    let symbol_name = unique_name.as_bytes().to_vec();
97    let sym = object.add_symbol(Symbol {
98        name: symbol_name.clone(),
99        value: 0,
100        size: content.len() as _,
101        kind: SymbolKind::Data,
102        scope: SymbolScope::Linkage,
103        weak: false,
104        section: SymbolSection::Section(section),
105        flags: SymbolFlags::None,
106    });
107    object.add_symbol_data(sym, section, &content, 1);
108    object.write_stream(&mut obj_buf)?;
109
110    let object_file_name = format!("{unique_name}.o").into_bytes();
111    write_archive(&info, &mut out_file, &object_file_name, &obj_buf)?;
112
113    println!("cargo:rustc-link-lib=static={unique_name}");
114    println!("cargo:rustc-link-search=native={out_dir}");
115    Ok(())
116}
117
118fn write_archive(
119    target_info: &TargetInfo,
120    out_file: &mut (impl Write + Seek),
121    object_file_name: &[u8],
122    object_file_contents: &[u8],
123) -> Result<()> {
124    let member = NewArchiveMember {
125        buf: Box::new(object_file_contents),
126        object_reader: &DEFAULT_OBJECT_READER,
127        member_name: String::from_utf8(object_file_name.to_vec()).unwrap(),
128        mtime: 0,
129        uid: 0,
130        gid: 0,
131        perms: 0o644,
132    };
133    write_archive_to_stream(out_file, &[member], target_info.archive_kind, false, false)?;
134
135    Ok(())
136}
137
138struct TargetInfo {
139    binfmt: BinaryFormat,
140    arch: Architecture,
141    endian: Endianness,
142    archive_kind: ArchiveKind,
143}
144
145impl TargetInfo {
146    fn from_build_script_vars() -> Self {
147        let (binfmt, archive_kind) = match &*env::var("CARGO_CFG_TARGET_OS").unwrap() {
148            "macos" | "ios" => (BinaryFormat::MachO, ArchiveKind::Darwin64),
149            "windows" => (BinaryFormat::Coff, ArchiveKind::Gnu),
150            "linux" | "android" => (BinaryFormat::Elf, ArchiveKind::Gnu),
151            unk => panic!("unhandled operating system '{unk}'"),
152        };
153        let arch = match &*env::var("CARGO_CFG_TARGET_ARCH").unwrap() {
154            // NB: this is guesswork, because apparently the Rust team can't be bothered to document
155            // the *full* list anywhere (they differ from what the target triples use, which *are*
156            // fully documented)
157            "x86" => Architecture::I386,
158            "x86_64" => Architecture::X86_64,
159            "arm" => Architecture::Arm,
160            "aarch64" => Architecture::Aarch64,
161            "riscv32" => Architecture::Riscv32,
162            "riscv64" => Architecture::Riscv64,
163            "mips" => Architecture::Mips,
164            "mips64" => Architecture::Mips64,
165            "powerpc" => Architecture::PowerPc,
166            "powerpc64" => Architecture::PowerPc64,
167            unk => panic!("unhandled architecture '{unk}'"),
168        };
169        let endian = match &*env::var("CARGO_CFG_TARGET_ENDIAN").unwrap() {
170            "little" => Endianness::Little,
171            "big" => Endianness::Big,
172            unk => unreachable!("unhandled endianness '{unk}'"),
173        };
174
175        Self {
176            binfmt,
177            arch,
178            endian,
179            archive_kind,
180        }
181    }
182}
183
184fn lib_prefix_and_suffix() -> (&'static str, &'static str) {
185    if env::var_os("CARGO_CFG_UNIX").is_some() {
186        ("lib", ".a")
187    } else if env::var_os("CARGO_CFG_WINDOWS").is_some() {
188        ("", ".lib")
189    } else {
190        unimplemented!("target platform not supported");
191    }
192}