Skip to main content

hector_build/
lib.rs

1use flate2::{Compression, write::GzEncoder};
2use std::env;
3use std::fs;
4use std::path::Path;
5use tar::Builder;
6use walkdir::WalkDir;
7
8/// Directories to skip entirely when walking the source tree.
9const SKIP_DIRS: &[&str] = &["target", ".git", ".hg", ".svn", "node_modules"];
10
11/// Walk `source_dir` and create a gzipped tar archive at `$OUT_DIR/hector_sources.tar.gz`.
12/// The [`hector::sources!`] macro embeds it directly via `include_bytes!`.
13///
14/// Pass `"."` (or `env!("CARGO_MANIFEST_DIR")`) to capture `Cargo.toml`, `build.rs`,
15/// and `src/` together so the tarball is self-contained and buildable.
16///
17/// ```rust,ignore
18/// fn main() {
19///     hector_build::collect_sources(".");
20/// }
21/// ```
22pub fn collect_sources(source_dir: impl AsRef<Path>) {
23    let source_dir = source_dir.as_ref();
24    let out_dir = env::var("OUT_DIR").expect("OUT_DIR not set");
25    let tarball_path = Path::new(&out_dir).join("hector_sources.tar.gz");
26
27    let mut files: Vec<(String, std::path::PathBuf)> = Vec::new();
28
29    for entry in WalkDir::new(source_dir)
30        .follow_links(false)
31        .into_iter()
32        .filter_entry(|e| {
33            // Prune well-known directories we never want to embed.
34            if e.file_type().is_dir() {
35                if let Some(name) = e.file_name().to_str() {
36                    return !SKIP_DIRS.contains(&name);
37                }
38            }
39            true
40        })
41        .filter_map(|e| e.ok())
42    {
43        let path = entry.path();
44        if !path.is_file() {
45            continue;
46        }
47
48        let file_name = match path.file_name().and_then(|n| n.to_str()) {
49            Some(n) => n,
50            None => continue,
51        };
52
53        let include = match path.extension().and_then(|e| e.to_str()) {
54            Some("rs") | Some("toml") => true,
55            _ => {
56                // No extension: Cargo.lock, or entirely uppercase names (README, LICENSE, …)
57                file_name == "Cargo.lock"
58                    || (!file_name.contains('.') && file_name.chars().all(|c| c.is_ascii_uppercase()))
59            }
60        };
61
62        if !include {
63            continue;
64        }
65
66        println!("cargo:rerun-if-changed={}", path.display());
67
68        let rel = path.to_string_lossy().into_owned();
69        files.push((rel, path.to_path_buf()));
70    }
71
72    println!("cargo:rerun-if-changed={}", source_dir.display());
73
74    let tar_gz_bytes = {
75        let buf = Vec::new();
76        let enc = GzEncoder::new(buf, Compression::best());
77        let mut ar = Builder::new(enc);
78
79        for (rel, path) in &files {
80            let data = match fs::read(path) {
81                Ok(d) => d,
82                Err(_) => continue,
83            };
84            let mut header = tar::Header::new_gnu();
85            header.set_size(data.len() as u64);
86            header.set_mode(0o644);
87            header.set_cksum();
88            ar.append_data(&mut header, rel, data.as_slice())
89                .expect("failed to add file to tar");
90        }
91
92        let enc = ar.into_inner().expect("failed to finalize tar");
93        enc.finish().expect("failed to finalize gzip")
94    };
95
96    fs::write(&tarball_path, &tar_gz_bytes).expect("failed to write hector_sources.tar.gz");
97}