Skip to main content

release_kit/
embedded.rs

1//! The embedded sources: everything the binary serves or lands, compiled
2//! in.
3//!
4//! `include_dir!` embeds each authored root at compile time, so the binary
5//! and the canon it carries cannot drift. Which roots exist is declared
6//! once, in [`crate::distribution_roots`], read here, by `build.rs` for
7//! change tracking, and by the packaging test; a test below holds this
8//! module to that inventory.
9
10use include_dir::{Dir, include_dir};
11
12pub use crate::distribution_roots::DISTRIBUTION_ROOTS;
13
14/// The technology-agnostic method chapters.
15pub static METHOD: Dir<'static> = include_dir!("$CARGO_MANIFEST_DIR/method");
16
17/// The per-technology bindings.
18pub static BINDINGS: Dir<'static> = include_dir!("$CARGO_MANIFEST_DIR/bindings");
19
20/// The human-facing runbooks `rk guide` renders.
21pub static RUNBOOKS: Dir<'static> = include_dir!("$CARGO_MANIFEST_DIR/runbooks");
22
23/// The per-forge documents answering the fifth axis.
24pub static FORGES: Dir<'static> = include_dir!("$CARGO_MANIFEST_DIR/forges");
25
26/// The setup scripts, one subtree per forge, executed by `rk setup` and
27/// landed nowhere.
28pub static SETUP: Dir<'static> = include_dir!("$CARGO_MANIFEST_DIR/setup");
29
30/// The deterministic files `rk init` lands, one subtree per technology,
31/// laid out exactly as they land in a target repository.
32pub static SNIPPETS: Dir<'static> = include_dir!("$CARGO_MANIFEST_DIR/snippets");
33
34/// The whole texts the binary writes outside `snippets/`.
35///
36/// The spliced blocks and the host-side hook body, authored as files so
37/// no human-faced artifact lives as a source literal; the readers in
38/// `src/projection.rs` and `src/setup/branch_reminder.rs` embed each file
39/// by name.
40pub static BLOCKS: Dir<'static> = include_dir!("$CARGO_MANIFEST_DIR/blocks");
41
42/// The agent skills, one directory per skill.
43pub static SKILLS: Dir<'static> = include_dir!("$CARGO_MANIFEST_DIR/skills");
44
45/// The artifacts every skill shares, installed once outside the skill roots.
46pub static SKILL_SHARED: Dir<'static> = include_dir!("$CARGO_MANIFEST_DIR/skill-shared");
47
48/// The pinned-tool registry.
49pub static VERSIONS: &str = include_str!("../versions.toml");
50
51/// One file per release that needs an operator step, copied whole into a
52/// stage's reference tree for the agent to select from.
53pub static GUIDANCE: Dir<'static> = include_dir!("$CARGO_MANIFEST_DIR/guidance");
54
55/// The release history, carried whole for the stage's reference tree.
56///
57/// Generated by release-plz, so a stage can hand an agent the changelog of
58/// the exact binary that staged it. Like the licenses it sits beside the
59/// root inventory rather than in it: history is crate metadata, not an
60/// authored source, and it names the repository that publishes it.
61pub static CHANGELOG: &str = include_str!("../CHANGELOG.md");
62
63/// The root license statement naming both halves.
64pub static LICENSE: &str = include_str!("../LICENSE");
65
66/// The MIT text covering the distribution.
67pub static LICENSE_MIT: &str = include_str!("../LICENSE-MIT");
68
69/// The CC BY 4.0 text covering the method.
70pub static LICENSE_CC_BY: &str = include_str!("../LICENSE-CC-BY-4.0");
71
72/// The sentinel marker a landed file may carry; `rk init --apply` reports
73/// every line holding one so nothing lands half-configured silently.
74pub const SENTINEL: &str = "TODO(release-kit)";
75
76/// Collect every file under `dir`, depth-first, as `(path, contents)` with
77/// the path relative to the embedded root, sorted by path.
78pub(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/// The files one distribution root carries, as `(path, bytes)` with the path
91/// carrying the root as its first segment, or `None` for a name the
92/// inventory does not declare.
93#[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/// Every embedded file, root by root in inventory order, sorted by path
118/// within each root.
119///
120/// The license files are deliberately absent: they are crate metadata the
121/// registry requires, not authored sources, and `rk license` serves them.
122#[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    /// The inventory and this module must name the same roots: a root
136    /// embedded here but absent from the inventory would be served without
137    /// change tracking, and a development build would then carry stale
138    /// bytes; a root declared but not embedded is a name the inventory
139    /// declares and nothing serves.
140    #[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    /// Every authored block ends in exactly one newline — the one the
182    /// repository's hooks enforce and the readers strip — so the bytes a
183    /// reader composes are identical to what the authored file holds
184    /// above that newline, and no landed target reads as drift.
185    #[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    /// No whole human-faced artifact lives as a Rust literal: every text
202    /// the binary writes into a target or host is authored under
203    /// `blocks/`, per `distribution:a-human-faced-artifact-is-authored-text`.
204    /// Two nets, both over production code only — everything above a
205    /// file's first `#[cfg(test)]`: a structural one that fails any
206    /// string literal spanning three or more source lines, whatever its
207    /// name, because a whole artifact body is multi-line and a message is
208    /// not; and a needle list holding the retired const names out and
209    /// pinning the one-line artifact signatures the structural net cannot
210    /// tell from a message.
211    #[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    /// The interpolation glues the decoded-break net exempts, by their
272    /// exact source text: the two splice compositions in
273    /// `src/projection.rs` and the header block in `src/setup/app_jwt.rs`.
274    /// Growing this list is a reviewed act; a whole artifact body never
275    /// belongs on it.
276    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    /// Every string literal in `text` whose decoded value spans three or
286    /// more lines, as `(starting line, decoded line count)`. A hand
287    /// scanner over the token stream: line comments are skipped, raw
288    /// literals end at their matching quote-and-hashes delimiter however
289    /// many hashes open them, and quoted literals honor backslash
290    /// escapes, so an artifact written on one source line as `\n`
291    /// escapes counts by what it decodes to, not by how it is typed.
292    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                    // A literal spanning source lines is judged whole. A
338                    // one-source-line literal is judged by its decoded
339                    // breaks, with the few known interpolation glues
340                    // allowlisted by their exact source text: a whole
341                    // artifact is static authored text, and anything new
342                    // that decodes to three lines answers here.
343                    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}