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(
96        StandardSection::ReadOnlyData,
97        unique_name.as_bytes(),
98        &[],
99        1,
100    );
101    let symbol_name = unique_name.as_bytes().to_vec();
102    let sym = object.add_symbol(Symbol {
103        name: symbol_name.clone(),
104        value: 0,
105        size: content.len() as _,
106        kind: SymbolKind::Data,
107        scope: SymbolScope::Linkage,
108        weak: false,
109        section: SymbolSection::Section(section),
110        flags: SymbolFlags::None,
111    });
112    object.add_symbol_data(sym, section, &content, 1);
113    object.write_stream(&mut obj_buf)?;
114
115    let object_file_name = format!("{unique_name}.o").into_bytes();
116    write_archive(&info, &mut out_file, &object_file_name, &obj_buf)?;
117
118    println!("cargo:rustc-link-lib=static={unique_name}");
119    println!("cargo:rustc-link-search=native={out_dir}");
120    Ok(())
121}
122
123fn write_archive(
124    target_info: &TargetInfo,
125    out_file: &mut (impl Write + Seek),
126    object_file_name: &[u8],
127    object_file_contents: &[u8],
128) -> Result<()> {
129    let member = NewArchiveMember {
130        buf: Box::new(object_file_contents),
131        object_reader: &DEFAULT_OBJECT_READER,
132        member_name: String::from_utf8(object_file_name.to_vec()).unwrap(),
133        mtime: 0,
134        uid: 0,
135        gid: 0,
136        perms: 0o644,
137    };
138    write_archive_to_stream(out_file, &[member], target_info.archive_kind, false, false)?;
139
140    Ok(())
141}
142
143struct TargetInfo {
144    binfmt: BinaryFormat,
145    arch: Architecture,
146    endian: Endianness,
147    archive_kind: ArchiveKind,
148}
149
150impl TargetInfo {
151    fn from_build_script_vars() -> Self {
152        let (binfmt, archive_kind) = match &*env::var("CARGO_CFG_TARGET_OS").unwrap() {
153            "macos" | "ios" => (BinaryFormat::MachO, ArchiveKind::Darwin64),
154            "windows" => (BinaryFormat::Coff, ArchiveKind::Gnu),
155            "linux" | "android" => (BinaryFormat::Elf, ArchiveKind::Gnu),
156            unk => panic!("unhandled operating system '{unk}'"),
157        };
158        let arch = match &*env::var("CARGO_CFG_TARGET_ARCH").unwrap() {
159            // NB: this is guesswork, because apparently the Rust team can't be bothered to document
160            // the *full* list anywhere (they differ from what the target triples use, which *are*
161            // fully documented)
162            "x86" => Architecture::I386,
163            "x86_64" => Architecture::X86_64,
164            "arm" => Architecture::Arm,
165            "aarch64" => Architecture::Aarch64,
166            "riscv32" => Architecture::Riscv32,
167            "riscv64" => Architecture::Riscv64,
168            "mips" => Architecture::Mips,
169            "mips64" => Architecture::Mips64,
170            "powerpc" => Architecture::PowerPc,
171            "powerpc64" => Architecture::PowerPc64,
172            unk => panic!("unhandled architecture '{unk}'"),
173        };
174        let endian = match &*env::var("CARGO_CFG_TARGET_ENDIAN").unwrap() {
175            "little" => Endianness::Little,
176            "big" => Endianness::Big,
177            unk => unreachable!("unhandled endianness '{unk}'"),
178        };
179
180        Self {
181            binfmt,
182            arch,
183            endian,
184            archive_kind,
185        }
186    }
187}
188
189fn lib_prefix_and_suffix() -> (&'static str, &'static str) {
190    if env::var_os("CARGO_CFG_UNIX").is_some() {
191        ("lib", ".a")
192    } else if env::var_os("CARGO_CFG_WINDOWS").is_some() {
193        ("", ".lib")
194    } else {
195        unimplemented!("target platform not supported");
196    }
197}