lattice_wit/lib.rs
1//! The canonical plugin API — lattice's `wit/` package — as a crate, so a
2//! plugin can depend on a named ABI generation instead of a copied directory
3//! (WT.1).
4//!
5//! Design: `docs/dev/architecture/wit-ownership.md`.
6//!
7//! **WIT is the canonical plugin API, and lattice owns it.** Every copy of
8//! `wit/` in a plugin tree is a *cache* of this package, never a fork — but
9//! until this crate existed nothing said so and nothing enforced it. A plugin
10//! got its copy once, at scaffold time, and then it silently drifted: three ABI
11//! changes in a day left two installed components unloadable and the editor
12//! said nothing at all.
13//!
14//! The copy exists for a real reason — `wit_bindgen::generate!` resolves its
15//! `path` when the macro expands, so the files must be on disk beside the crate
16//! being compiled. That is a *build-time* need, and a build-time need is met by
17//! the build:
18//!
19//! ```ignore
20//! // a plugin's build.rs
21//! fn main() {
22//! lattice_wit::write_to("wit").expect("write the lattice API package");
23//! }
24//! ```
25//!
26//! `wit/` becomes generated output, and which ABI a plugin targets becomes a
27//! pinned dependency — which is what it always was.
28//!
29//! ## Zero dependencies, deliberately
30//!
31//! A plugin needs this at build time without building the editor. A crate that
32//! pulled in any editor crate would defeat its own purpose. That constraint is
33//! also why [`ABI_FINGERPRINT`] is an FNV-1a rather than a sha2.
34
35use std::io;
36use std::path::Path;
37
38include!(concat!(env!("OUT_DIR"), "/wit_assets.rs"));
39
40/// Write the embedded package into `dir`, creating it if needed.
41///
42/// Overwrites whatever is there: the directory is a cache of this package, and
43/// a partial refresh — some files current, some stale — resolves into a WIT
44/// package that is internally inconsistent, which fails in ways far harder to
45/// read than a clean overwrite.
46///
47/// Files present in `dir` that are **not** part of the package are left alone.
48/// A plugin may legitimately keep its own world file beside the package (org
49/// does not, but the scaffolds' `world.wit` shape would), and deleting a file
50/// this crate did not write would be taking ownership of a directory it only
51/// contributes to.
52///
53/// **A file already holding the right bytes is not rewritten**, and that is
54/// load-bearing rather than an optimisation. WT.2b calls this from the plugin
55/// build service on every boot, immediately before the source's staleness is
56/// judged — and staleness is judged on mtime. An unconditional write would move
57/// every file's mtime forward on every boot, so every source would look edited,
58/// so every plugin would rebuild from cold on every start. The requirement the
59/// build service exists to protect is precisely the opposite one.
60pub fn write_to(dir: impl AsRef<Path>) -> io::Result<()> {
61 let dir = dir.as_ref();
62 std::fs::create_dir_all(dir)?;
63 for (name, contents) in FILES {
64 let path = dir.join(name);
65 // `read` rather than metadata+len: a truncated or hand-edited file of
66 // coincidentally equal length is exactly the drift this crate exists to
67 // repair, and it must not survive the comparison.
68 if std::fs::read(&path).is_ok_and(|on_disk| on_disk == contents.as_bytes()) {
69 continue;
70 }
71 std::fs::write(path, contents)?;
72 }
73 Ok(())
74}
75
76/// Names of the files this package writes.
77pub fn file_names() -> impl Iterator<Item = &'static str> {
78 FILES.iter().map(|(name, _)| *name)
79}
80
81#[cfg(test)]
82mod tests {
83 #![allow(clippy::unwrap_used)]
84
85 use super::*;
86
87 #[test]
88 fn the_package_carries_the_load_bearing_files() {
89 assert!(!FILES.is_empty(), "the embedded package is not empty");
90 for wanted in ["types.wit", "plugin.wit", "modes.wit", "grammar.wit"] {
91 assert!(
92 file_names().any(|n| n == wanted),
93 "the package carries {wanted}"
94 );
95 }
96 }
97
98 /// The fixture and bundled-plugin worlds stay out. A plugin has no use for
99 /// another plugin's world, and shipping the host's test fixtures into every
100 /// user's config directory would be noise that also has to be kept current.
101 #[test]
102 fn fixture_and_bundled_worlds_are_excluded() {
103 for unwanted in [
104 "auto-pair.wit",
105 "init-fixture.wit",
106 "multiseam-fixture.wit",
107 "trampoline-fixture.wit",
108 ] {
109 assert!(
110 !file_names().any(|n| n == unwanted),
111 "{unwanted} is a world no plugin needs"
112 );
113 }
114 }
115
116 #[test]
117 fn write_to_produces_every_file_and_creates_the_directory() {
118 let tmp = std::env::temp_dir().join(format!(
119 "lattice-wit-write-{}-{}",
120 std::process::id(),
121 line!()
122 ));
123 let _ = std::fs::remove_dir_all(&tmp);
124 // The nested path proves `create_dir_all`, not just `create_dir`.
125 let target = tmp.join("nested").join("wit");
126 write_to(&target).unwrap();
127 for name in file_names() {
128 let written = std::fs::read_to_string(target.join(name)).unwrap();
129 assert!(!written.is_empty(), "{name} written non-empty");
130 }
131 // A package resolve needs the whole set present at once, so count too.
132 assert_eq!(
133 std::fs::read_dir(&target).unwrap().count(),
134 FILES.len(),
135 "every embedded file landed and nothing else did"
136 );
137 let _ = std::fs::remove_dir_all(&tmp);
138 }
139
140 /// A file the package does not own survives a write. The directory is one
141 /// this crate contributes to, not one it owns — a plugin may keep its own
142 /// world file beside the package.
143 #[test]
144 fn write_to_leaves_a_file_it_does_not_own_alone() {
145 let tmp = std::env::temp_dir().join(format!(
146 "lattice-wit-keep-{}-{}",
147 std::process::id(),
148 line!()
149 ));
150 let _ = std::fs::remove_dir_all(&tmp);
151 std::fs::create_dir_all(&tmp).unwrap();
152 std::fs::write(tmp.join("my-world.wit"), "// mine\n").unwrap();
153 write_to(&tmp).unwrap();
154 assert_eq!(
155 std::fs::read_to_string(tmp.join("my-world.wit")).unwrap(),
156 "// mine\n"
157 );
158 let _ = std::fs::remove_dir_all(&tmp);
159 }
160
161 /// **A second write must not touch a file that already holds the right
162 /// bytes.** WT.2b's build service calls `write_to` on every boot just
163 /// before judging the source stale by mtime; if the write were
164 /// unconditional, every boot would move every mtime forward and every
165 /// plugin would rebuild from cold every start — inverting the one
166 /// requirement the build cache exists for.
167 ///
168 /// The sleep is what makes this a test about writing rather than about
169 /// timestamp resolution: without a gap the filesystem could report the same
170 /// mtime for a rewrite that genuinely happened, and the assertion would
171 /// pass on the broken version.
172 #[test]
173 fn an_identical_file_is_not_rewritten() {
174 let tmp = std::env::temp_dir().join(format!(
175 "lattice-wit-idem-{}-{}",
176 std::process::id(),
177 line!()
178 ));
179 let _ = std::fs::remove_dir_all(&tmp);
180 write_to(&tmp).unwrap();
181
182 let probe = tmp.join(file_names().next().unwrap());
183 let mtime = |p: &Path| std::fs::metadata(p).unwrap().modified().unwrap();
184 let before = mtime(&probe);
185
186 std::thread::sleep(std::time::Duration::from_millis(20));
187 write_to(&tmp).unwrap();
188 assert_eq!(
189 mtime(&probe),
190 before,
191 "a file already holding the right bytes is left entirely alone"
192 );
193
194 // ...and a file that DID drift is repaired, contents and all.
195 std::fs::write(&probe, "// someone edited the cache\n").unwrap();
196 write_to(&tmp).unwrap();
197 let repaired = std::fs::read_to_string(&probe).unwrap();
198 assert!(
199 !repaired.contains("someone edited"),
200 "a drifted file is overwritten from the package"
201 );
202 assert_ne!(mtime(&probe), before, "and its mtime moves");
203 let _ = std::fs::remove_dir_all(&tmp);
204 }
205
206 /// The fingerprint is DERIVED FROM the embedded contents.
207 ///
208 /// Recomputed here with the same FNV-1a `build.rs` uses, so the constant
209 /// cannot drift from the files it claims to describe — a constant that was
210 /// emitted but not actually hashed over the package would pass every other
211 /// test in this file while telling WT.3's staleness check a lie, and the
212 /// symptom would be an artifact that never looks stale no matter what the
213 /// ABI does.
214 #[test]
215 fn the_fingerprint_is_derived_from_the_files() {
216 let mut hash: u64 = 0xcbf2_9ce4_8422_2325;
217 let fnv = |bytes: &[u8], h: &mut u64| {
218 for b in bytes {
219 *h ^= u64::from(*b);
220 *h = h.wrapping_mul(0x0000_0100_0000_01b3);
221 }
222 };
223 for (name, contents) in FILES {
224 fnv(name.as_bytes(), &mut hash);
225 fnv(contents.as_bytes(), &mut hash);
226 }
227 assert_eq!(
228 format!("{hash:016x}"),
229 ABI_FINGERPRINT,
230 "the emitted constant is the hash of the embedded package"
231 );
232 }
233
234 /// Sorted, so the fingerprint is a function of content and not of the order
235 /// the filesystem happened to hand the files back. Without this a rebuild
236 /// on another machine could report an ABI change that did not happen.
237 #[test]
238 fn the_package_is_in_sorted_order() {
239 let names: Vec<&str> = file_names().collect();
240 let mut sorted = names.clone();
241 sorted.sort_unstable();
242 assert_eq!(names, sorted);
243 }
244
245 /// The fingerprint is what WT.3 stamps into a built artifact and compares
246 /// at load. It has to be stable within a build, and it has to be a function
247 /// of the package's CONTENT rather than of readdir order — otherwise every
248 /// rebuild would report an ABI change.
249 #[test]
250 fn the_fingerprint_is_stable_and_content_shaped() {
251 assert_eq!(ABI_FINGERPRINT, ABI_FINGERPRINT);
252 assert_eq!(ABI_FINGERPRINT.len(), 16, "a 64-bit hash, hex");
253 assert!(ABI_FINGERPRINT.chars().all(|c| c.is_ascii_hexdigit()));
254 assert_ne!(
255 ABI_FINGERPRINT, "0000000000000000",
256 "the hash actually ran over the files"
257 );
258 }
259}