1use std::collections::HashSet;
16use std::sync::atomic::{AtomicBool, Ordering};
17use std::sync::{Mutex, RwLock};
18
19use lumen_core::warn_line;
20
21static ANY: AtomicBool = AtomicBool::new(false);
24
25static UNLINKED: RwLock<Vec<(String, Vec<String>)>> = RwLock::new(Vec::new());
27
28static WARNED: Mutex<Option<HashSet<String>>> = Mutex::new(None);
30
31pub fn record_unlinked(unlinked: impl IntoIterator<Item = (String, Vec<String>)>) {
35 let unlinked: Vec<(String, Vec<String>)> = unlinked.into_iter().collect();
36 ANY.store(!unlinked.is_empty(), Ordering::Relaxed);
37 *UNLINKED.write().unwrap_or_else(|e| e.into_inner()) = unlinked;
38}
39
40pub fn unlinked_subsystem(builtin: &str) -> Option<String> {
42 if !ANY.load(Ordering::Relaxed) {
43 return None;
44 }
45 UNLINKED
46 .read()
47 .unwrap_or_else(|e| e.into_inner())
48 .iter()
49 .find(|(_, markers)| markers.iter().any(|m| reaches(builtin, m)))
50 .map(|(name, _)| name.clone())
51}
52
53pub(crate) fn warn_if_unlinked(builtin: &str) {
56 let Some(subsystem) = unlinked_subsystem(builtin) else {
57 return;
58 };
59 let mut warned = WARNED.lock().unwrap_or_else(|e| e.into_inner());
60 if warned
61 .get_or_insert_with(HashSet::new)
62 .insert(builtin.to_string())
63 {
64 warn_line!(
65 "lumen: warning: `{builtin}` does nothing here: this package was built without \
66 {subsystem}. Name it in lumen.toml [capabilities] to package it in."
67 );
68 }
69}
70
71fn reaches(builtin: &str, marker: &str) -> bool {
76 match marker.strip_suffix('(') {
77 Some(callee) => !callee.is_empty() && builtin.ends_with(callee),
78 None => !marker.is_empty() && builtin.contains(marker),
79 }
80}
81
82#[cfg(test)]
83mod tests {
84 use super::*;
85
86 #[test]
87 fn a_marker_reaches_the_builtins_a_scan_would_have_counted() {
88 assert!(reaches("tray_icon", "tray_icon"));
89 assert!(reaches("tray_icon_menu", "tray_icon"));
90 assert!(!reaches("notify", "tray_icon"));
91 assert!(reaches("fetch", "fetch("));
92 assert!(!reaches("fetch_all", "fetch("));
93 assert!(!reaches("anything", ""));
94 assert!(!reaches("anything", "("));
95 }
96
97 #[test]
100 fn a_recorded_subsystem_answers_for_its_builtins_until_cleared() {
101 assert_eq!(unlinked_subsystem("tray_icon"), None);
102 record_unlinked([(
103 "os-tray".to_string(),
104 vec!["tray_icon".to_string(), "fetch(".to_string()],
105 )]);
106 assert_eq!(unlinked_subsystem("tray_icon").as_deref(), Some("os-tray"));
107 assert_eq!(unlinked_subsystem("fetch").as_deref(), Some("os-tray"));
108 assert_eq!(unlinked_subsystem("notify"), None);
109 warn_if_unlinked("tray_icon");
110 warn_if_unlinked("tray_icon");
111 assert_eq!(
112 WARNED
113 .lock()
114 .unwrap()
115 .as_ref()
116 .map(|w| w.contains("tray_icon")),
117 Some(true)
118 );
119 record_unlinked(Vec::new());
120 assert_eq!(unlinked_subsystem("tray_icon"), None);
121 }
122}