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