sim_cookbook/
embed_codegen.rs1use std::io;
24use std::path::{Path, PathBuf};
25
26pub fn write_embed(recipes_subdir: &str) -> io::Result<()> {
30 let manifest_dir = std::env::var("CARGO_MANIFEST_DIR")
31 .map_err(|_| io::Error::other("CARGO_MANIFEST_DIR not set (call from build.rs)"))?;
32 let out_dir = std::env::var("OUT_DIR")
33 .map_err(|_| io::Error::other("OUT_DIR not set (call from build.rs)"))?;
34 let root = Path::new(&manifest_dir).join(recipes_subdir);
35 println!("cargo:rerun-if-changed={}", root.display());
36 let code = generate_embed_code(&root)?;
37 std::fs::write(Path::new(&out_dir).join("cookbook_recipes.rs"), code)
38}
39
40pub fn generate_embed_code(recipes_root: &Path) -> io::Result<String> {
44 let mut files: Vec<(String, PathBuf)> = Vec::new();
45 if recipes_root.is_dir() {
46 collect(recipes_root, recipes_root, &mut files)?;
47 }
48 files.sort();
49 validate_embedded_tree(recipes_root, &files)?;
50 let mut out = String::from("&[\n");
51 for (rel, abs) in &files {
52 out.push_str(&format!(
54 " ({:?}, include_bytes!({:?}) as &[u8]),\n",
55 rel,
56 abs.to_string_lossy(),
57 ));
58 }
59 out.push_str("]\n");
60 Ok(out)
61}
62
63fn validate_embedded_tree(recipes_root: &Path, files: &[(String, PathBuf)]) -> io::Result<()> {
71 if files.is_empty() {
72 return Ok(());
73 }
74 let owned = files
75 .iter()
76 .map(|(relative, path)| std::fs::read(path).map(|bytes| (relative.as_str(), bytes)))
77 .collect::<io::Result<Vec<_>>>()?;
78 let embedded = owned
79 .iter()
80 .map(|(relative, bytes)| (*relative, bytes.as_slice()))
81 .collect::<Vec<_>>();
82 crate::recipes_from_embedded(&embedded).map_err(|error| {
83 io::Error::new(
84 io::ErrorKind::InvalidData,
85 format!(
86 "invalid cookbook tree at {}: {error}",
87 recipes_root.display()
88 ),
89 )
90 })?;
91 Ok(())
92}
93
94fn collect(base: &Path, dir: &Path, out: &mut Vec<(String, PathBuf)>) -> io::Result<()> {
95 let mut entries: Vec<_> = std::fs::read_dir(dir)?.collect::<io::Result<Vec<_>>>()?;
96 entries.sort_by_key(|e| e.file_name());
97 for entry in entries {
98 let path = entry.path();
99 if path.is_dir() {
100 collect(base, &path, out)?;
101 } else if path.is_file() {
102 let rel = path
103 .strip_prefix(base)
104 .map_err(io::Error::other)?
105 .components()
106 .map(|c| c.as_os_str().to_string_lossy())
107 .collect::<Vec<_>>()
108 .join("/");
109 out.push((rel, path));
110 }
111 }
112 Ok(())
113}
114
115#[cfg(test)]
116mod tests {
117 use super::*;
118
119 #[test]
120 fn empty_dir_yields_empty_slice() {
121 let dir = std::env::temp_dir().join(format!("sim-cb-embed-empty-{}", std::process::id()));
122 let code = generate_embed_code(&dir).unwrap();
123 assert_eq!(code, "&[\n]\n");
124 }
125
126 #[test]
127 fn generates_sorted_include_bytes() {
128 let root = std::env::temp_dir().join(format!("sim-cb-embed-{}", std::process::id()));
129 let _ = std::fs::remove_dir_all(&root);
130 std::fs::create_dir_all(root.join("01-basics/add")).unwrap();
131 std::fs::write(
132 root.join("book.toml"),
133 b"book = \"x\"\ntitle = \"X\"\nchapters = [\"01-basics\"]\n",
134 )
135 .unwrap();
136 std::fs::write(
137 root.join("01-basics/add/recipe.toml"),
138 b"id = \"a\"\ntitle = \"A\"\ncodec = \"lisp\"\nsetup = \"setup.siml\"\npurpose = \"purpose.md\"\n",
139 )
140 .unwrap();
141 std::fs::write(root.join("01-basics/add/setup.siml"), b"(+ 1 2)\n").unwrap();
142 std::fs::write(root.join("01-basics/add/purpose.md"), b"Add values.\n").unwrap();
143 let code = generate_embed_code(&root).unwrap();
144 assert!(code.contains("\"01-basics/add/recipe.toml\""), "{code}");
145 assert!(code.contains("\"book.toml\""), "{code}");
146 assert!(code.contains("include_bytes!("), "{code}");
147 let recipe_at = code.find("recipe.toml").unwrap();
150 let book_at = code.find("book.toml").unwrap();
151 assert!(recipe_at < book_at, "expected sorted order, got:\n{code}");
152 let _ = std::fs::remove_dir_all(&root);
153 }
154
155 #[test]
156 fn rejects_a_tree_that_would_fail_when_the_library_is_loaded() {
157 let root =
158 std::env::temp_dir().join(format!("sim-cb-embed-invalid-{}", std::process::id()));
159 let _ = std::fs::remove_dir_all(&root);
160 std::fs::create_dir_all(&root).unwrap();
161 std::fs::write(root.join("book.toml"), b"title = \"Missing id\"\n").unwrap();
162
163 let error = generate_embed_code(&root).unwrap_err();
164 assert_eq!(error.kind(), io::ErrorKind::InvalidData);
165 assert!(
166 error.to_string().contains("missing required key `book`"),
167 "{error}"
168 );
169 let _ = std::fs::remove_dir_all(&root);
170 }
171}