lumen_capability_macros/
lib.rs1use proc_macro::TokenStream;
26use proc_macro2::{Ident, Span};
27use quote::quote;
28use syn::parse::{Parse, ParseStream};
29use syn::{Expr, LitStr, Token, parse_macro_input};
30
31struct CapabilityEntry {
34 name: LitStr,
35 phase: Expr,
36 install: Expr,
37 preflight: Option<Expr>,
38 select: Option<Expr>,
39}
40
41impl Parse for CapabilityEntry {
42 fn parse(input: ParseStream) -> syn::Result<Self> {
43 let name: LitStr = input.parse().map_err(|e| {
44 syn::Error::new(
45 e.span(),
46 "lumen_capability! takes the capability's name first, as a string literal: \
47 lumen_capability!(\"os-tray\", Phase::Platform, install)",
48 )
49 })?;
50 input.parse::<Token![,]>()?;
51 let phase: Expr = input.parse()?;
52 input.parse::<Token![,]>()?;
53 let install: Expr = input.parse()?;
54 let mut preflight = None;
55 let mut select = None;
56 while input.peek(Token![,]) {
57 input.parse::<Token![,]>()?;
58 if input.is_empty() {
59 break;
60 }
61 let key: Ident = input.parse()?;
62 input.parse::<Token![=]>()?;
63 let slot = match key.to_string().as_str() {
64 "preflight" => &mut preflight,
65 "select" => &mut select,
66 _ => {
67 return Err(syn::Error::new(
68 key.span(),
69 "the options after the install entry are `preflight = <fn>` and \
70 `select = <Select>`",
71 ));
72 }
73 };
74 if slot.is_some() {
75 return Err(syn::Error::new(
76 key.span(),
77 format!("`{key}` is given twice"),
78 ));
79 }
80 *slot = Some(input.parse()?);
81 }
82 Ok(CapabilityEntry {
83 name,
84 phase,
85 install,
86 preflight,
87 select,
88 })
89 }
90}
91
92#[proc_macro]
97pub fn lumen_capability(input: TokenStream) -> TokenStream {
98 let CapabilityEntry {
99 name,
100 phase,
101 install,
102 preflight,
103 select,
104 } = parse_macro_input!(input as CapabilityEntry);
105
106 let declared = name.value();
107 if declared.trim().is_empty() {
108 return syn::Error::new(name.span(), "the capability's name must not be empty")
109 .to_compile_error()
110 .into();
111 }
112 let suffix = symbol_suffix(&declared);
113 let register_name = format!("lumen_capability_register_{suffix}");
114 let register_fn = Ident::new(&format!("register_{suffix}"), Span::call_site());
115 let preflight = match preflight {
116 Some(f) => quote! { ::core::option::Option::Some(#f) },
117 None => quote! { ::core::option::Option::None },
118 };
119 let select = match select {
120 Some(s) => quote! { #s },
121 None => quote! { ::lumen_capability::Select::Always },
122 };
123
124 quote! {
125 const _: () = {
126 #[unsafe(export_name = #register_name)]
127 extern "C" fn #register_fn() {
128 ::lumen_capability::register(::lumen_capability::Capability {
129 name: #name,
130 phase: #phase,
131 install: #install,
132 preflight: #preflight,
133 select: #select,
134 crate_name: env!("CARGO_PKG_NAME"),
137 });
138 }
139
140 #[used]
145 #[cfg_attr(
146 all(unix, not(target_vendor = "apple")),
147 unsafe(link_section = ".init_array")
148 )]
149 #[cfg_attr(
150 target_vendor = "apple",
151 unsafe(link_section = "__DATA,__mod_init_func")
152 )]
153 #[cfg_attr(windows, unsafe(link_section = ".CRT$XCU"))]
154 static CTOR: extern "C" fn() = #register_fn;
155 };
156 }
157 .into()
158}
159
160fn symbol_suffix(name: &str) -> String {
164 name.chars()
165 .map(|c| if c.is_ascii_alphanumeric() { c } else { '_' })
166 .collect()
167}
168
169#[cfg(test)]
170mod tests {
171 use super::symbol_suffix;
172
173 #[test]
174 fn a_hyphenated_name_underscores() {
175 assert_eq!(symbol_suffix("os-tray"), "os_tray");
176 }
177
178 #[test]
179 fn anything_a_symbol_cannot_carry_becomes_an_underscore() {
180 assert_eq!(symbol_suffix("http.fetch+2"), "http_fetch_2");
181 }
182}