1use include_dir::{Dir, include_dir};
11
12pub use crate::distribution_roots::DISTRIBUTION_ROOTS;
13
14pub static METHOD: Dir<'static> = include_dir!("$CARGO_MANIFEST_DIR/method");
16
17pub static BINDINGS: Dir<'static> = include_dir!("$CARGO_MANIFEST_DIR/bindings");
19
20pub static RUNBOOKS: Dir<'static> = include_dir!("$CARGO_MANIFEST_DIR/runbooks");
22
23pub static FORGES: Dir<'static> = include_dir!("$CARGO_MANIFEST_DIR/forges");
25
26pub static SETUP: Dir<'static> = include_dir!("$CARGO_MANIFEST_DIR/setup");
29
30pub static SNIPPETS: Dir<'static> = include_dir!("$CARGO_MANIFEST_DIR/snippets");
33
34pub static BLOCKS: Dir<'static> = include_dir!("$CARGO_MANIFEST_DIR/blocks");
41
42pub static SKILLS: Dir<'static> = include_dir!("$CARGO_MANIFEST_DIR/skills");
44
45pub static SKILL_SHARED: Dir<'static> = include_dir!("$CARGO_MANIFEST_DIR/skill-shared");
47
48pub static VERSIONS: &str = include_str!("../versions.toml");
50
51pub static GUIDANCE: Dir<'static> = include_dir!("$CARGO_MANIFEST_DIR/guidance");
54
55pub static CHANGELOG: &str = include_str!("../CHANGELOG.md");
62
63pub static LICENSE: &str = include_str!("../LICENSE");
65
66pub static LICENSE_MIT: &str = include_str!("../LICENSE-MIT");
68
69pub static LICENSE_CC_BY: &str = include_str!("../LICENSE-CC-BY-4.0");
71
72pub const SENTINEL: &str = "TODO(release-kit)";
75
76pub(crate) fn walk<'a>(dir: &Dir<'a>) -> Vec<(String, &'a [u8])> {
79 let mut out = Vec::new();
80 for file in dir.files() {
81 out.push((file.path().to_string_lossy().into_owned(), file.contents()));
82 }
83 for sub in dir.dirs() {
84 out.extend(walk(sub));
85 }
86 out.sort_by(|a, b| a.0.cmp(&b.0));
87 out
88}
89
90#[must_use]
94pub fn root_files(root: &str) -> Option<Vec<(String, &'static [u8])>> {
95 let dir = match root {
96 "method" => &METHOD,
97 "bindings" => &BINDINGS,
98 "runbooks" => &RUNBOOKS,
99 "forges" => &FORGES,
100 "snippets" => &SNIPPETS,
101 "blocks" => &BLOCKS,
102 "setup" => &SETUP,
103 "skills" => &SKILLS,
104 "skill-shared" => &SKILL_SHARED,
105 "guidance" => &GUIDANCE,
106 "versions.toml" => return Some(vec![(root.to_owned(), VERSIONS.as_bytes())]),
107 _ => return None,
108 };
109 Some(
110 walk(dir)
111 .into_iter()
112 .map(|(path, bytes)| (format!("{root}/{path}"), bytes))
113 .collect(),
114 )
115}
116
117#[must_use]
123pub fn artifacts() -> Vec<(String, &'static [u8])> {
124 DISTRIBUTION_ROOTS
125 .iter()
126 .filter_map(|root| root_files(root))
127 .flatten()
128 .collect()
129}
130
131#[cfg(test)]
132mod tests {
133 use super::{DISTRIBUTION_ROOTS, artifacts, root_files};
134
135 #[test]
141 fn the_inventory_and_the_embed_declare_the_same_roots() {
142 let source = include_str!("embedded.rs");
143 let mut embedded: Vec<String> = source
144 .lines()
145 .filter_map(|line| {
146 let (_, rest) = line.split_once("include_dir!(\"$CARGO_MANIFEST_DIR/")?;
147 let (root, _) = rest.split_once('"')?;
148 Some(root.to_owned())
149 })
150 .collect();
151 embedded.extend(source.lines().filter_map(|line| {
152 let (_, rest) = line.split_once("include_str!(\"../")?;
153 let (name, _) = rest.split_once('"')?;
154 (!name.starts_with("LICENSE") && name != "CHANGELOG.md").then(|| name.to_owned())
155 }));
156 embedded.sort();
157 let mut declared: Vec<String> =
158 DISTRIBUTION_ROOTS.iter().map(ToString::to_string).collect();
159 declared.sort();
160 assert_eq!(
161 embedded, declared,
162 "src/embedded.rs and src/distribution_roots.rs disagree on the distribution roots"
163 );
164 }
165
166 #[test]
167 fn every_declared_root_serves_at_least_one_file() {
168 for root in DISTRIBUTION_ROOTS {
169 let files = root_files(root).expect("a declared root resolves");
170 assert!(!files.is_empty(), "{root}: the root carries no file");
171 for (path, _) in &files {
172 assert!(
173 path == root || path.starts_with(&format!("{root}/")),
174 "{path}: an artifact path must carry its root"
175 );
176 }
177 }
178 assert!(root_files("no-such-root").is_none());
179 }
180
181 #[test]
186 fn every_block_is_authored_with_one_final_newline() {
187 let root = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("blocks");
188 for file in super::BLOCKS.files() {
189 let name = file.path().to_string_lossy().into_owned();
190 let disk = std::fs::read(root.join(&name)).expect("an embedded block exists on disk");
191 assert_eq!(disk, file.contents(), "{name}: embed and disk disagree");
192 let text = std::str::from_utf8(file.contents()).expect("a block is UTF-8");
193 assert!(text.ends_with('\n'), "{name}: a block ends in a newline");
194 assert!(
195 !text.ends_with("\n\n"),
196 "{name}: a block ends in exactly one newline"
197 );
198 }
199 }
200
201 #[test]
212 fn no_artifact_body_lives_as_a_source_literal() {
213 let needles = [
214 "## Releases",
215 "Installed by rk setup step branch-reminder",
216 "This project works in worktrees:",
217 "Branches are worked in the main checkout",
218 "stages: [commit-msg]",
219 "ROUTING_BLOCK",
220 "ROUTING_WORKTREE_LINE",
221 "ROUTING_BRANCHES_LINE",
222 "HOOKS_BLOCK",
223 "WORKTREE_GUARD_ENTRY",
224 "HOOK_BODY",
225 "use flake",
226 "rk self-depend sync --apply",
227 "release-kit.packages.",
228 "inputs.nixpkgs.follows",
229 ];
230 let src = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("src");
231 let mut offenders = Vec::new();
232 scan(&src, &needles, &mut offenders);
233 assert!(
234 offenders.is_empty(),
235 "an artifact body belongs under blocks/, not in the sources: {offenders:?}"
236 );
237 }
238
239 fn scan(dir: &std::path::Path, needles: &[&str], offenders: &mut Vec<String>) {
240 for entry in std::fs::read_dir(dir).expect("the source tree is readable") {
241 let entry = entry.expect("a directory entry is readable");
242 let path = entry.path();
243 if path.is_dir() {
244 scan(&path, needles, offenders);
245 continue;
246 }
247 if path.extension().is_none_or(|ext| ext != "rs") {
248 continue;
249 }
250 let text = std::fs::read_to_string(&path).expect("a source file is UTF-8");
251 let production = text.split("#[cfg(test)]").next().unwrap_or("");
252 for (index, line) in production.lines().enumerate() {
253 if line.trim_start().starts_with("//") {
254 continue;
255 }
256 for needle in needles {
257 if line.contains(needle) {
258 offenders.push(format!("{}:{}: {needle}", path.display(), index + 1));
259 }
260 }
261 }
262 for (line, span) in multiline_literals(production) {
263 offenders.push(format!(
264 "{}:{line}: a string literal spanning {span} lines",
265 path.display()
266 ));
267 }
268 }
269 }
270
271 const GLUE: [&str; 3] = [
277 "{}\\n\\n{block}\\n",
278 "{HOOK_TYPES_LINE}\\n\\nrepos:\\n{block}\\n",
279 concat!(
280 "Authorization: Bearer {jwt}\\nAccept: application/vnd.github+json\\n",
281 "X-GitHub-Api-Version: 2022-11-28\\n"
282 ),
283 ];
284
285 fn multiline_literals(text: &str) -> Vec<(usize, usize)> {
293 let bytes = text.as_bytes();
294 let mut spans = Vec::new();
295 let mut line = 1;
296 let mut i = 0;
297 while i < bytes.len() {
298 match bytes[i] {
299 b'\n' => {
300 line += 1;
301 i += 1;
302 }
303 b'/' if bytes.get(i + 1) == Some(&b'/') => {
304 while i < bytes.len() && bytes[i] != b'\n' {
305 i += 1;
306 }
307 }
308 b'r' if matches!(bytes.get(i + 1), Some(&b'#' | &b'"')) => {
309 let hashes = bytes[i + 1..]
310 .iter()
311 .take_while(|byte| **byte == b'#')
312 .count();
313 if bytes.get(i + 1 + hashes) != Some(&b'"') {
314 i += 1;
315 continue;
316 }
317 let body = i + hashes + 2;
318 let close = format!("\"{}", "#".repeat(hashes));
319 let end = text[body..]
320 .find(&close)
321 .map_or(bytes.len(), |at| body + at);
322 let physical = text[i..end].matches('\n').count();
323 if physical >= 2 {
324 spans.push((line, physical + 1));
325 }
326 line += physical;
327 i = (end + close.len()).min(bytes.len());
328 }
329 b'"' => {
330 let mut j = i + 1;
331 while j < bytes.len() && bytes[j] != b'"' {
332 j += if bytes[j] == b'\\' { 2 } else { 1 };
333 }
334 let segment = &text[i + 1..j.min(bytes.len())];
335 let physical = segment.matches('\n').count();
336 let decoded = physical + segment.matches("\\n").count();
337 if physical >= 2 || (decoded >= 2 && !GLUE.contains(&segment)) {
344 spans.push((line, decoded + 1));
345 }
346 line += physical;
347 i = j + 1;
348 }
349 _ => i += 1,
350 }
351 }
352 spans
353 }
354
355 #[test]
356 fn the_artifact_list_is_stable_and_complete() {
357 let listed = artifacts();
358 let total: usize = DISTRIBUTION_ROOTS
359 .iter()
360 .map(|root| root_files(root).expect("a declared root resolves").len())
361 .sum();
362 assert_eq!(listed.len(), total);
363 assert!(
364 listed.iter().any(|(path, _)| path == "versions.toml"),
365 "the single-file root must appear as itself"
366 );
367 }
368}