lumen_modules/link_kit.rs
1//! The link kit: one target's link line, its inputs, and the modules on it.
2//!
3//! A link kit is what lets a machine with no Rust toolchain produce an
4//! executable with the engine and a chosen set of runtime modules compiled
5//! in. The release workflow builds the static launcher once per target with a
6//! recorder in the linker's place (`tools/link-recorder`), keeps every file
7//! that link read, and writes a [`Manifest`] describing the command that
8//! produced the binary. Replaying that command with a module's object files
9//! left out, or its register symbol forced in, is how one prebuilt kit turns
10//! into any of the executables its modules can spell.
11//!
12//! A second kind of kit records the link of the engine's shared library
13//! instead ([`ArtifactKind::SharedEngine`]): `liblumen_engine` on Linux and
14//! macOS, `lumen.dll` on Windows. Replaying it with only the capabilities an
15//! app uses, and with the export list cut down to what the files shipped
16//! beside it resolve, gives each packaged app an engine of its own.
17//!
18//! Two things about the recorded line are not obvious and are why the
19//! manifest is typed rather than a list of strings:
20//!
21//! - Some arguments name files that must travel with the kit (the rlibs and
22//! the temporary objects), some name directories that must not (the host's
23//! `/usr/lib`), and some name neither. A replay has to tell them apart to
24//! re-root the first kind and leave the third alone.
25//! - A module's contribution to the line is a subset of it: its rlib, and the
26//! native libraries its own crate graph asked for. Dropping a module means
27//! dropping exactly that subset, which is what the `module` attribution on
28//! [`LinkArg::File`] and [`LinkArg::SysLib`] records.
29//!
30//! Everything here is data. Nothing in this module opens a file, and nothing
31//! in the engine reads it: the producer is `lumenc link-kit emit` and the
32//! consumer is a `lumenc` running on someone else's machine.
33//!
34//! The schema is versioned and pre-1.0, so it changes whenever a better shape
35//! turns up; a kit whose [`Manifest::schema`] is not [`SCHEMA_VERSION`] is
36//! refused rather than guessed at.
37
38use std::collections::BTreeMap;
39
40use serde::{Deserialize, Serialize};
41
42use crate::{REGISTER_PREFIX, entry_symbol};
43
44/// The manifest version this build writes and accepts.
45pub const SCHEMA_VERSION: u32 = 4;
46
47/// One target's link kit.
48#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
49pub struct Manifest {
50 /// Schema version, checked against [`SCHEMA_VERSION`] before anything
51 /// else is read.
52 pub schema: u32,
53 /// Release-asset target name, for example `linux-x86_64`.
54 pub target: String,
55 /// The Rust target triple the recorded link was for.
56 pub rust_triple: String,
57 /// `rustc --version` of the toolchain that produced the inputs, for a
58 /// report when a replay fails.
59 pub rustc: String,
60 /// The Lumen version the kit was built from. A kit and the app artifact
61 /// it links are only one build together.
62 pub lumen_version: String,
63 /// The program that replays [`Manifest::args`].
64 pub driver: Driver,
65 /// The recorded link line, one entry per argument.
66 pub args: Vec<LinkArg>,
67 /// Every runtime module the kit can link in.
68 pub modules: Vec<KitModule>,
69 /// Every optional subsystem the kit can link in, and when an app gets
70 /// each one.
71 pub capabilities: Vec<KitCapability>,
72 /// How the app's compiled artifact reaches the executable.
73 pub artifact: Artifact,
74 /// The engine build the recorded inputs are, as `lumen_engine_build_id`
75 /// reports it. A shared-engine kit records it so a replay can check it is
76 /// relinking the engine the rest of the toolchain was built against;
77 /// absent where the recorded binary carries no engine build id.
78 #[serde(default, skip_serializing_if = "Option::is_none")]
79 pub build_id: Option<String>,
80}
81
82/// What replays the line.
83#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
84pub struct Driver {
85 /// The kind of program the arguments are written for.
86 pub kind: DriverKind,
87 /// The object format's dialect, named after LLD's flavors: `gnu`,
88 /// `darwin`, or `link`. It is what says how a symbol is forced onto the
89 /// line - `-u` for `gnu` and `darwin`, `/INCLUDE:` for `link`.
90 pub flavor: String,
91 /// Kit-relative path of the driver the kit ships, when it ships one.
92 /// Absent means the driver is the consumer's own (`cc`).
93 #[serde(default, skip_serializing_if = "Option::is_none")]
94 pub path: Option<String>,
95}
96
97/// The kind of program a replay runs.
98#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
99#[serde(rename_all = "snake_case")]
100pub enum DriverKind {
101 /// The platform's C compiler, which drives the system linker and
102 /// contributes the C runtime startup files. The consumer's own.
103 Cc,
104 /// LLD, run directly. Shipped in the kit, because a Windows machine with
105 /// no toolchain has no linker to borrow.
106 Lld,
107}
108
109/// One argument of the recorded link line.
110///
111/// Every variant renders to exactly one argument. Where a flag and its value
112/// share a token, the flag is the entry's `prefix` and the value is what the
113/// consumer resolves; where they are two tokens, the flag is a
114/// [`LinkArg::Lit`] of its own.
115#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
116#[serde(tag = "kind", rename_all = "snake_case")]
117pub enum LinkArg {
118 /// Passed through as it was recorded.
119 Lit {
120 /// The argument.
121 value: String,
122 },
123 /// Where the output path goes. The consumer substitutes its own.
124 Out {
125 /// Flag this value is joined to, empty when the flag is a separate
126 /// argument.
127 prefix: String,
128 },
129 /// A file the kit carries, at `path` under the kit's `stage` directory.
130 File {
131 /// Path relative to the kit's `stage` directory.
132 path: String,
133 /// The module this file belongs to, when it belongs to one. A replay
134 /// that leaves that module out leaves this file out.
135 #[serde(default, skip_serializing_if = "Option::is_none")]
136 module: Option<String>,
137 },
138 /// A directory the linker searches.
139 #[serde(rename = "sysdir")]
140 SysDir {
141 /// Flag this value is joined to, empty when the flag is a separate
142 /// argument.
143 prefix: String,
144 /// Kit-relative when `staged`, otherwise a path on the machine that
145 /// recorded the line, passed through for the consumer's own copy of
146 /// the same system directory.
147 path: String,
148 /// Whether the kit carries this directory.
149 staged: bool,
150 },
151 /// The list of symbols a library exports, which rustc writes for every
152 /// library link: a version script for the GNU linkers, an exported-symbols
153 /// list for ld64, a module-definition file for MSVC. The kit carries the
154 /// recorded list; a replay either passes it on or puts its own in its
155 /// place, which is what decides what the relinked library keeps.
156 ExportList {
157 /// The flag the path is joined to, such as `-Wl,--version-script=`.
158 prefix: String,
159 /// The recorded list, relative to the kit's `stage` directory.
160 path: String,
161 },
162 /// A native library the linker resolves by name.
163 #[serde(rename = "syslib")]
164 SysLib {
165 /// Flag this name is joined to: `-l` for the Unix drivers, empty for
166 /// the MSVC one, which names libraries outright.
167 prefix: String,
168 /// The library's name, as the driver spells it.
169 name: String,
170 /// The module whose crate graph asked for this library, when the
171 /// producer could attribute it. A replay that leaves that module out
172 /// leaves this entry out, so the executable does not depend on a
173 /// system library it makes no calls into.
174 #[serde(default, skip_serializing_if = "Option::is_none")]
175 module: Option<String>,
176 },
177}
178
179/// One runtime module a kit can link in.
180#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
181pub struct KitModule {
182 /// The name an app declares the module under in `lumen.toml`.
183 pub name: String,
184 /// The symbol that forces the module onto the line. It is a module's
185 /// registration entry, and the pre-main constructor that calls it sits in
186 /// the same object file, so naming it is what pulls both out of the rlib.
187 /// It is the only symbol a replay names: the module installs itself
188 /// through the registry its constructor reaches, so the install entry is
189 /// never called across this boundary.
190 pub register_symbol: String,
191}
192
193impl KitModule {
194 /// The entry for a module declared under `name`, spelled the way
195 /// `lumen_module!` spelled it when the module was compiled.
196 pub fn new(name: &str) -> KitModule {
197 KitModule {
198 name: name.to_string(),
199 register_symbol: entry_symbol(REGISTER_PREFIX, name),
200 }
201 }
202}
203
204/// One optional subsystem a kit can link in.
205#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
206pub struct KitCapability {
207 /// The name the app's `[capabilities]` table keys it by.
208 pub name: String,
209 /// The symbol that forces it onto the line: its registration entry, whose
210 /// object also holds the pre-main constructor, exactly as a module's does.
211 pub register_symbol: String,
212 /// When an app that does not name it gets it anyway.
213 pub select: KitSelect,
214}
215
216/// When a package carries a capability the app did not name.
217#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
218#[serde(rename_all = "kebab-case")]
219pub enum KitSelect {
220 /// Every app.
221 Always,
222 /// An app whose sources mention any of these.
223 OnUse(Vec<String>),
224 /// No app; only a `[capabilities]` entry brings it in.
225 OnRequest,
226 /// An app whose `lumen.toml` sets `key` (a dotted path) to one of
227 /// `any_of`, reading `default` when the key is absent.
228 OnConfig {
229 /// Dotted path of the key.
230 key: String,
231 /// The values that select the capability.
232 any_of: Vec<String>,
233 /// The value an absent key reads as.
234 default: String,
235 },
236}
237
238impl KitSelect {
239 /// Whether an app with these sources and this `lumen.toml` gets the
240 /// capability. `opaque` says the sources are not all the app runs, so
241 /// every use rule answers yes.
242 fn selects(&self, sources: &str, config: &toml::Table, opaque: bool) -> bool {
243 match self {
244 KitSelect::Always => true,
245 KitSelect::OnUse(_) if opaque => true,
246 KitSelect::OnUse(markers) => markers.iter().any(|m| sources.contains(m.as_str())),
247 KitSelect::OnRequest => false,
248 KitSelect::OnConfig {
249 key,
250 any_of,
251 default,
252 } => {
253 let value = config_value(config, key).unwrap_or(default.as_str());
254 any_of.iter().any(|v| v == value)
255 }
256 }
257 }
258}
259
260/// The string at dotted path `key` in `config`, when there is one.
261fn config_value<'a>(config: &'a toml::Table, key: &str) -> Option<&'a str> {
262 let mut parts = key.split('.');
263 let mut value = config.get(parts.next()?)?;
264 for part in parts {
265 value = value.as_table()?.get(part)?;
266 }
267 value.as_str()
268}
269
270impl From<&lumen_capability::Capability> for KitCapability {
271 fn from(capability: &lumen_capability::Capability) -> Self {
272 KitCapability {
273 name: capability.name.to_string(),
274 register_symbol: lumen_capability::register_symbol(capability.name),
275 select: match capability.select {
276 lumen_capability::Select::Always => KitSelect::Always,
277 lumen_capability::Select::OnUse(markers) => {
278 KitSelect::OnUse(markers.iter().map(|m| m.to_string()).collect())
279 }
280 lumen_capability::Select::OnRequest => KitSelect::OnRequest,
281 lumen_capability::Select::OnConfig {
282 key,
283 any_of,
284 default,
285 } => KitSelect::OnConfig {
286 key: key.to_string(),
287 any_of: any_of.iter().map(|v| v.to_string()).collect(),
288 default: default.to_string(),
289 },
290 },
291 }
292 }
293}
294
295impl KitCapability {
296 /// The markers its use rule scans sources for; empty for any other rule.
297 pub fn markers(&self) -> &[String] {
298 match &self.select {
299 KitSelect::OnUse(markers) => markers,
300 _ => &[],
301 }
302 }
303}
304
305/// The capabilities a package of one app carries, out of the ones a kit
306/// offers.
307///
308/// `requested` is the app's `[capabilities]` table, and an entry there
309/// settles that capability outright. Every other one follows its own rule
310/// against `sources`, the app's markup, scripts, styles and config read into
311/// one haystack, and `config`, the app's parsed `lumen.toml`. `opaque` says
312/// the app runs code the scan cannot read (a C++ or Python program driving
313/// the engine), so every use rule answers yes and only the table, a
314/// request-only rule, or a config rule leaves a capability out. A requested
315/// name the kit does not carry is an error naming what it does, so a
316/// misspelling is caught rather than ignored.
317pub fn select_capabilities<'a>(
318 kit: &'a [KitCapability],
319 sources: &str,
320 config: &toml::Table,
321 requested: &BTreeMap<String, bool>,
322 opaque: bool,
323) -> Result<Vec<&'a KitCapability>, String> {
324 if let Some(unknown) = requested
325 .keys()
326 .find(|name| !kit.iter().any(|c| &c.name == *name))
327 {
328 let offered: Vec<&str> = kit.iter().map(|c| c.name.as_str()).collect();
329 return Err(format!(
330 "[capabilities] names '{unknown}', and this link kit carries no capability by \
331 that name. It offers: {}.",
332 if offered.is_empty() {
333 "nothing".to_string()
334 } else {
335 offered.join(", ")
336 }
337 ));
338 }
339 Ok(kit
340 .iter()
341 .filter(|capability| match requested.get(&capability.name) {
342 Some(wanted) => *wanted,
343 None => capability.select.selects(sources, config, opaque),
344 })
345 .collect())
346}
347
348/// The capabilities `requested` turns off although `sources` mention their
349/// builtins, each with the markers found. An app that says `os-tray = false`
350/// and calls `tray_icon` packages without the tray, and the call does
351/// nothing; this is what the package step warns about.
352pub fn dropped_but_used<'a>(
353 kit: &'a [KitCapability],
354 sources: &str,
355 requested: &BTreeMap<String, bool>,
356) -> Vec<(&'a KitCapability, Vec<&'a str>)> {
357 kit.iter()
358 .filter(|capability| requested.get(&capability.name) == Some(&false))
359 .filter_map(|capability| {
360 let found: Vec<&str> = capability
361 .markers()
362 .iter()
363 .map(String::as_str)
364 .filter(|m| sources.contains(m))
365 .collect();
366 (!found.is_empty()).then_some((capability, found))
367 })
368 .collect()
369}
370
371/// How the app's compiled artifact reaches the executable a replay produces.
372#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
373pub struct Artifact {
374 /// The mechanism this platform uses.
375 pub kind: ArtifactKind,
376}
377
378/// How a replay's output carries the app: the two ways a launcher finds the
379/// artifact it runs, and the shared engine, which carries none.
380#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
381#[serde(rename_all = "snake_case")]
382pub enum ArtifactKind {
383 /// Appended to the executable after the link, behind the launcher's
384 /// footer magic.
385 Append,
386 /// Written into the executable by the link itself, as a Mach-O section.
387 /// Appending is not available there: a signature has to cover the whole
388 /// file, so anything added after the link invalidates it. The launcher
389 /// names the segment and section it reads, so the manifest does not.
390 MachoSection,
391 /// The kit links the engine's shared library rather than an executable,
392 /// and the app's artifact goes into the launcher beside it the way an
393 /// unlinked package does. Nothing is appended and nothing is put on the
394 /// line.
395 SharedEngine,
396}
397
398/// One line of the link recorder's JSON Lines output.
399///
400/// `tools/link-recorder` writes these; the fields are the whole of what a
401/// kit is built from. A build runs several links, so a reader picks the one
402/// whose [`Record::out`] is the binary it wants.
403#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
404pub struct Record {
405 /// The `-o` / `/OUT:` value of this link.
406 #[serde(default)]
407 pub out: Option<String>,
408 /// The link's arguments, with every response file expanded.
409 pub argv: Vec<String>,
410 /// The same list with every argument that named a file replaced by the
411 /// name it was staged under. An index where the two lists differ is a
412 /// file the kit has to carry; an index where they agree is not.
413 pub staged_argv: Vec<String>,
414 /// The directory the link ran in, which is what a relative argument in it
415 /// is relative to.
416 #[serde(default)]
417 pub cwd: String,
418 /// The environment entries the line depends on.
419 #[serde(default)]
420 pub env: RecordEnv,
421}
422
423/// Environment the recorded line reads.
424#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
425pub struct RecordEnv {
426 /// `LIB`, the MSVC linker's library search path. Windows records need it:
427 /// the line names the C runtime and the Windows SDK libraries by bare
428 /// file name and resolves them through this variable.
429 #[serde(rename = "LIB", default, skip_serializing_if = "Option::is_none")]
430 pub lib: Option<String>,
431}
432
433#[cfg(test)]
434mod tests {
435 use std::collections::BTreeMap;
436
437 use super::{
438 ArtifactKind, KitCapability, KitModule, KitSelect, LinkArg, Record, select_capabilities,
439 };
440
441 fn capability(name: &str, select: KitSelect) -> KitCapability {
442 KitCapability {
443 name: name.to_string(),
444 register_symbol: lumen_capability::register_symbol(name),
445 select,
446 }
447 }
448
449 fn names(selected: &[&KitCapability]) -> Vec<String> {
450 selected.iter().map(|c| c.name.clone()).collect()
451 }
452
453 /// Each rule against the sources: always in, in when mentioned, in only
454 /// when asked.
455 #[test]
456 fn a_capability_follows_its_own_rule_when_the_app_does_not_name_it() {
457 let kit = vec![
458 capability("os-tray", KitSelect::OnUse(vec!["tray_icon".to_string()])),
459 capability(
460 "os-hotkey",
461 KitSelect::OnUse(vec!["register_hotkey".to_string()]),
462 ),
463 capability("mcp", KitSelect::OnRequest),
464 capability("core-ish", KitSelect::Always),
465 ];
466 let selected = select_capabilities(
467 &kit,
468 "fn on_start() { tray_icon(\"app\", \"icon.png\", \"\"); }",
469 &toml::Table::new(),
470 &BTreeMap::new(),
471 false,
472 )
473 .expect("nothing was requested");
474 assert_eq!(names(&selected), ["os-tray", "core-ish"]);
475 }
476
477 /// A `[capabilities]` entry wins over the rule, either way.
478 #[test]
479 fn a_requested_capability_is_settled_by_the_request() {
480 let kit = vec![
481 capability("os-tray", KitSelect::OnUse(vec!["tray_icon".to_string()])),
482 capability("mcp", KitSelect::OnRequest),
483 ];
484 let requested = BTreeMap::from([("os-tray".to_string(), false), ("mcp".to_string(), true)]);
485 let selected =
486 select_capabilities(&kit, "tray_icon(", &toml::Table::new(), &requested, false)
487 .expect("both are known");
488 assert_eq!(names(&selected), ["mcp"]);
489 }
490
491 /// Sources the scan cannot read in full answer yes to every use rule; a
492 /// request-only rule and the app's own table still decide.
493 #[test]
494 fn opaque_sources_keep_every_capability_a_use_rule_could_want() {
495 let kit = vec![
496 capability("os-tray", KitSelect::OnUse(vec!["tray_icon".to_string()])),
497 capability("os-power", KitSelect::OnUse(vec!["keep_awake".to_string()])),
498 capability("mcp", KitSelect::OnRequest),
499 ];
500 let requested = BTreeMap::from([("os-power".to_string(), false)]);
501 let selected = select_capabilities(&kit, "", &toml::Table::new(), &requested, true)
502 .expect("all known");
503 assert_eq!(names(&selected), ["os-tray"]);
504 let scanned = select_capabilities(&kit, "", &toml::Table::new(), &requested, false)
505 .expect("all known");
506 assert!(
507 scanned.is_empty(),
508 "nothing is mentioned: {:?}",
509 names(&scanned)
510 );
511 }
512
513 /// A capability the table turns off while the sources call into it is
514 /// reported with the markers found, and only that one.
515 #[test]
516 fn a_capability_turned_off_while_used_is_reported_with_its_markers() {
517 let kit = vec![
518 capability(
519 "os-tray",
520 KitSelect::OnUse(vec!["tray_icon".to_string(), "tray_menu".to_string()]),
521 ),
522 capability("os-power", KitSelect::OnUse(vec!["keep_awake".to_string()])),
523 capability("mcp", KitSelect::OnRequest),
524 ];
525 let requested = BTreeMap::from([
526 ("os-tray".to_string(), false),
527 ("os-power".to_string(), false),
528 ("mcp".to_string(), false),
529 ]);
530 let reported =
531 super::dropped_but_used(&kit, "tray_icon(\"id\", \"a.png\", \"\")", &requested);
532 assert_eq!(reported.len(), 1);
533 assert_eq!(reported[0].0.name, "os-tray");
534 assert_eq!(reported[0].1, ["tray_icon"]);
535 }
536
537 /// A name the kit does not carry is refused, naming what it does carry.
538 #[test]
539 fn a_requested_capability_the_kit_lacks_is_an_error_naming_the_offer() {
540 let kit = vec![capability("os-tray", KitSelect::Always)];
541 let requested = BTreeMap::from([("os-trey".to_string(), true)]);
542 let error = select_capabilities(&kit, "", &toml::Table::new(), &requested, false)
543 .expect_err("a misspelling");
544 assert!(error.contains("'os-trey'"), "{error}");
545 assert!(error.contains("os-tray"), "{error}");
546 }
547
548 /// Two interchangeable capabilities keyed on one config value: the value
549 /// picks one, a value both answer to picks both, an absent key reads as
550 /// the default, and a `[capabilities]` entry still wins.
551 #[test]
552 fn a_config_value_picks_between_capabilities() {
553 let on = |any_of: &[&str]| KitSelect::OnConfig {
554 key: "render.backend".to_string(),
555 any_of: any_of.iter().map(|v| v.to_string()).collect(),
556 default: "auto".to_string(),
557 };
558 let kit = vec![
559 capability("render-gpu", on(&["gpu", "auto"])),
560 capability("render-cpu", on(&["cpu", "auto"])),
561 ];
562 let pick = |toml: &str, requested: &[(&str, bool)]| {
563 let config: toml::Table = toml::from_str(toml).expect("valid toml");
564 let requested = requested
565 .iter()
566 .map(|(n, w)| (n.to_string(), *w))
567 .collect::<BTreeMap<_, _>>();
568 names(&select_capabilities(&kit, "", &config, &requested, false).expect("known"))
569 };
570 assert_eq!(pick("[render]\nbackend = \"cpu\"\n", &[]), ["render-cpu"]);
571 assert_eq!(pick("[render]\nbackend = \"gpu\"\n", &[]), ["render-gpu"]);
572 assert_eq!(pick("", &[]), ["render-gpu", "render-cpu"]);
573 assert_eq!(
574 pick("[render]\nbackend = \"auto\"\n", &[("render-gpu", false)]),
575 ["render-cpu"]
576 );
577 // A key that is not a string reads as absent rather than matching.
578 assert_eq!(
579 pick("[render]\nbackend = 3\n", &[]),
580 ["render-gpu", "render-cpu"]
581 );
582 }
583
584 /// The registry's entry becomes the kit's, rule included.
585 #[test]
586 fn a_registered_capability_becomes_a_kit_entry() {
587 fn nothing(_: &mut lumen_core::app::App, _: &lumen_capability::CapabilityEnv) {}
588 let entry = KitCapability::from(&lumen_capability::Capability {
589 name: "os-tray",
590 phase: lumen_capability::Phase::Platform,
591 install: nothing,
592 preflight: None,
593 select: lumen_capability::Select::OnUse(&["tray_icon"]),
594 crate_name: "lumen-os-tray-capability",
595 });
596 assert_eq!(entry.register_symbol, "lumen_capability_register_os_tray");
597 assert_eq!(
598 entry.select,
599 KitSelect::OnUse(vec!["tray_icon".to_string()])
600 );
601 }
602
603 #[test]
604 fn a_module_entry_spells_the_name_spaced_register_symbol() {
605 let module = KitModule::new("lumen-audio");
606 assert_eq!(module.register_symbol, "lumen_module_register_lumen_audio");
607 }
608
609 #[test]
610 fn an_argument_round_trips_through_its_tag() {
611 let args = vec![
612 LinkArg::Lit {
613 value: "-pie".to_string(),
614 },
615 LinkArg::Out {
616 prefix: String::new(),
617 },
618 LinkArg::File {
619 path: "aabbccdd-liblumen_fs.rlib".to_string(),
620 module: Some("lumen-fs".to_string()),
621 },
622 LinkArg::SysDir {
623 prefix: "-B".to_string(),
624 path: "bin".to_string(),
625 staged: true,
626 },
627 LinkArg::SysLib {
628 prefix: "-l".to_string(),
629 name: "asound".to_string(),
630 module: None,
631 },
632 LinkArg::ExportList {
633 prefix: "-Wl,--version-script=".to_string(),
634 path: "eeff0011-list".to_string(),
635 },
636 ];
637 let json = serde_json::to_string(&args).expect("the arguments encode");
638 assert!(json.contains(r#"{"kind":"out","prefix":""}"#), "{json}");
639 assert!(json.contains(r#""kind":"sysdir""#), "{json}");
640 assert!(json.contains(r#""kind":"syslib""#), "{json}");
641 assert!(json.contains(r#""kind":"export_list""#), "{json}");
642 // An unattributed entry writes no `module` key at all, so a manifest
643 // reads as the short list of what is attributed.
644 assert!(!json.contains(r#""module":null"#), "{json}");
645 assert_eq!(
646 serde_json::from_str::<Vec<LinkArg>>(&json).expect("and decode"),
647 args
648 );
649 }
650
651 #[test]
652 fn an_artifact_kind_is_spelled_in_snake_case() {
653 assert_eq!(
654 serde_json::to_string(&ArtifactKind::MachoSection).expect("encodes"),
655 r#""macho_section""#
656 );
657 assert_eq!(
658 serde_json::to_string(&ArtifactKind::SharedEngine).expect("encodes"),
659 r#""shared_engine""#
660 );
661 }
662
663 #[test]
664 fn a_record_reads_without_the_windows_only_fields() {
665 let record: Record = serde_json::from_str(
666 r#"{"out":"app","argv":["-o","app"],"staged_argv":["-o","app"],"cwd":"/tmp"}"#,
667 )
668 .expect("the Unix recorder writes no LIB");
669 assert_eq!(record.out.as_deref(), Some("app"));
670 assert_eq!(record.env.lib, None);
671 }
672}