1#![cfg_attr(feature = "proc_macro_span", feature(proc_macro_span))]
5
6use std::{fmt::Display, path::PathBuf, process::Command, sync::Mutex};
7
8use proc_macro::TokenStream;
9use quote::{quote, ToTokens};
10use syn::{parse::ParseStream, parse_macro_input, spanned::Spanned};
11
12#[cfg(not(feature = "proc_macro_span"))]
14fn find_me(root: &str, pattern: &str) -> PathBuf {
15 let mut options = Vec::new();
16
17 for path in glob::glob(&std::path::Path::new(root).join("**/*.rs").to_string_lossy())
18 .unwrap()
19 .flatten()
20 {
21 if let Ok(mut f) = std::fs::File::open(&path) {
22 let mut contents = String::new();
23 std::io::Read::read_to_string(&mut f, &mut contents).ok();
24 if contents.contains(pattern) {
25 options.push(path.to_owned());
26 }
27 }
28 }
29
30 match options.as_slice() {
31 [] => panic!(
32 "could not find invocation point - maybe it was in a macro? \
33 If you are on nightly (or in the future), enable the `proc_macro_span` \
34 feature on `include-wasm-rs` to use advanced call site resolution, \
35 but until then each instance of the `build_wasm` must be present \
36 in the source text, and each must have a unique argument."
37 ),
38 [v] => v.clone(),
39 _ => panic!(
40 "found more than one contender for macro invocation location. \
41 If you are on nightly (or in the future), enable the `proc_macro_span` \
42 feature on `include-wasm-rs` to use advanced call site resolution, \
43 but until then each instance of the `build_wasm` must be present \
44 in the source text, and each must have a unique argument. \
45 Found potential invocation locations: {:?}",
46 options
47 .into_iter()
48 .map(|path| format!("`{}`", path.display()))
49 .collect::<Vec<String>>()
50 ),
51 }
52}
53
54#[derive(Default)]
55struct TargetFeatures {
56 atomics: bool,
57 bulk_memory: bool,
58 mutable_globals: bool,
59}
60
61impl TargetFeatures {
62 fn from_list_of_exprs(
63 elems: syn::punctuated::Punctuated<syn::Expr, syn::Token![,]>,
64 ) -> syn::parse::Result<Self> {
65 let mut res = Self::default();
66
67 for elem in elems {
68 let span = elem.span();
69 let name = match elem {
70 syn::Expr::Path(ident)
71 if ident.attrs.is_empty()
72 && ident.qself.is_none()
73 && ident.path.leading_colon.is_none()
74 && ident.path.segments.len() == 1
75 && ident.path.segments[0].arguments.is_empty() =>
76 {
77 ident.path.segments[0].ident.to_string()
78 }
79 _ => {
80 return Err(syn::Error::new(
81 span,
82 "expected a single token giving a feature",
83 ))
84 }
85 };
86
87 match name.as_str() {
88 "atomics" => res.atomics = true,
89 "bulk_memory" => res.bulk_memory = true,
90 "mutable_globals" => res.mutable_globals = true,
91 _ => return Err(syn::Error::new(span, "unknown feature")),
92 }
93 }
94
95 Ok(res)
96 }
97}
98
99fn degroup_expr(expr: syn::Expr) -> syn::Expr {
100 match expr {
101 syn::Expr::Group(syn::ExprGroup {
102 attrs,
103 group_token: _,
104 expr,
105 }) if attrs.is_empty() => degroup_expr(*expr),
106 expr => expr,
107 }
108}
109
110#[derive(Default)]
111struct Args {
112 module_dir: PathBuf,
113 features: TargetFeatures,
114 env_vars: Vec<(String, String)>,
115 release: bool,
116}
117
118impl syn::parse::Parse for Args {
119 fn parse(input: ParseStream) -> syn::parse::Result<Self> {
120 if input.peek(syn::LitStr) {
122 let path = input.parse::<syn::LitStr>()?;
123 return Ok(Self {
124 module_dir: PathBuf::from(path.value()),
125 ..Self::default()
126 });
127 }
128
129 let mut res = Self::default();
131
132 let dict =
133 syn::punctuated::Punctuated::<syn::FieldValue, syn::Token![,]>::parse_terminated(
134 input,
135 )?;
136 for mut value in dict {
137 if !value.attrs.is_empty() {
138 return Err(syn::Error::new(value.attrs[0].span(), "unexpected element"));
139 }
140 let name = match &value.member {
141 syn::Member::Named(name) => name.to_string(),
142 syn::Member::Unnamed(unnamed) => unnamed.index.to_string(),
143 };
144
145 value.expr = degroup_expr(value.expr);
146
147 match name.as_str() {
149 "path" => {
150 res.module_dir = match value.expr {
152 syn::Expr::Lit(syn::ExprLit {
153 attrs,
154 lit: syn::Lit::Str(path),
155 }) if attrs.is_empty() => PathBuf::from(path.value()),
156 _ => {
157 return Err(syn::Error::new(
158 value.expr.span(),
159 format!("expected literal string, got {:?}", value.expr),
160 ))
161 }
162 };
163 }
164 "release" => {
165 res.release = match value.expr {
167 syn::Expr::Lit(syn::ExprLit {
168 attrs,
169 lit: syn::Lit::Bool(release),
170 }) if attrs.is_empty() => release.value,
171 _ => return Err(syn::Error::new(value.expr.span(), "expected boolean")),
172 };
173 }
174 "features" => {
175 match value.expr {
177 syn::Expr::Array(syn::ExprArray {
178 attrs,
179 bracket_token: _,
180 elems,
181 }) if attrs.is_empty() => {
182 res.features = TargetFeatures::from_list_of_exprs(elems)?
183 }
184 _ => return Err(syn::Error::new(value.expr.span(), "expected boolean")),
185 };
186 }
187 "env" => {
188 match value.expr {
190 syn::Expr::Struct(syn::ExprStruct {
191 attrs,
192 qself: None,
193 path:
194 syn::Path {
195 leading_colon: None,
196 segments,
197 },
198 brace_token: _,
199 fields,
200 dot2_token: None,
201 rest: None,
202 }) if attrs.is_empty()
203 && segments.len() == 1
204 && segments[0].arguments.is_empty()
205 && segments[0].ident == "Env" =>
206 {
207 for field in fields {
208 let span = field.span();
209 if !field.attrs.is_empty() || field.colon_token.is_none() {
210 return Err(syn::Error::new(span, "expected key value pair"));
211 }
212
213 let env_name = match &field.member {
214 syn::Member::Named(name) => name.to_string(),
215 _ => {
216 return Err(syn::Error::new(
217 span,
218 "expected env variable name",
219 ))
220 }
221 };
222
223 let mut expr = &field.expr;
224 while let syn::Expr::Group(syn::ExprGroup {
225 attrs,
226 group_token: _,
227 expr: inner_expr,
228 }) = expr
229 {
230 if !attrs.is_empty() {
231 return Err(syn::Error::new(
232 attrs[0].span(),
233 "expected a string, int, float or bool",
234 ));
235 }
236
237 expr = inner_expr;
238 }
239
240 let env_val = match expr {
241 syn::Expr::Lit(syn::ExprLit { attrs, lit })
242 if attrs.is_empty() =>
243 {
244 match lit {
245 syn::Lit::Str(v) => v.value(),
246 syn::Lit::Int(i) => i.to_string(),
247 syn::Lit::Float(f) => f.to_string(),
248 syn::Lit::Bool(b) => b.value.to_string(),
249 _ => {
250 return Err(syn::Error::new(
251 lit.span(),
252 format!("expected a string, int, float or bool, found literal `{}`", lit.into_token_stream()),
253 ))
254 }
255 }
256 }
257 _ => {
258 return Err(syn::Error::new(
259 field.expr.span(),
260 format!("expected a string, int, float or bool, found `{}`", field.expr.into_token_stream()),
261 ))
262 }
263 };
264
265 res.env_vars.push((env_name, env_val));
266 }
267 }
268 _ => {
269 return Err(syn::Error::new(
270 value.expr.span(),
271 "expected key value pairs",
272 ))
273 }
274 }
275 }
276 option => {
277 return Err(syn::Error::new(
278 value.member.span(),
279 format!("unknown option `{}`", option),
280 ))
281 }
282 }
283 }
284
285 Ok(res)
286 }
287}
288
289impl Display for TargetFeatures {
290 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
291 if self.atomics {
292 write!(f, "+atomics,")?
293 }
294 if self.bulk_memory {
295 write!(f, "+bulk-memory,")?
296 }
297 if self.mutable_globals {
298 write!(f, "+mutable-globals,")?
299 }
300
301 Ok(())
302 }
303}
304
305static GLOBAL_LOCK: Mutex<()> = Mutex::new(());
307
308fn do_build_wasm(args: &Args) -> Result<PathBuf, String> {
310 let Args {
311 module_dir,
312 features,
313 env_vars,
314 release,
315 } = args;
316
317 let mut lock = GLOBAL_LOCK.lock();
319 while lock.is_err() {
320 GLOBAL_LOCK.clear_poison();
321 lock = GLOBAL_LOCK.lock();
322 }
323
324 let cargo_config = module_dir.join("Cargo.toml");
326 if !cargo_config.is_file() {
327 return Err(format!(
328 "target directory `{}` does not contain a `Cargo.toml` file",
329 module_dir.display()
330 ));
331 }
332 match std::fs::read_to_string(cargo_config) {
333 Ok(cfg) => {
334 if cfg.contains("[workspace]\n") {
335 return Err("provided directory points to a workspace, not a module".to_owned());
336 }
337 }
338 Err(e) => return Err(format!("failed to read target `Cargo.toml`: {e}")),
339 }
340
341 let mut target_dir = "target/".to_owned();
343 for (key, val) in env_vars.iter() {
344 target_dir += &format!("{}_{}", key, val);
345 }
346
347 let mut command = Command::new("cargo");
349
350 let out = command
351 .arg("update")
352 .current_dir(module_dir.clone())
353 .output();
354 match out {
355 Ok(out) => {
356 if !out.status.success() {
357 return Err(format!(
358 "failed to update module `{}`: \n{}",
359 module_dir.display(),
360 String::from_utf8_lossy(&out.stderr).replace('\n', "\n\t")
361 ));
362 }
363 }
364 Err(e) => {
365 return Err(format!(
366 "failed to update module `{}`: {e}",
367 module_dir.display()
368 ))
369 }
370 }
371
372 let mut command = Command::new("cargo");
374
375 const RUSTFLAGS: &str = "RUSTFLAGS";
377 let mut rustflags_value = format!("--cfg=web_sys_unstable_apis -C target-feature={features}");
378 command.env(RUSTFLAGS, &rustflags_value);
379
380 for (key, val) in env_vars.iter() {
381 if key == RUSTFLAGS {
382 rustflags_value += " ";
383 rustflags_value += val;
384 command.env(RUSTFLAGS, &rustflags_value);
385 } else {
386 command.env(key, val);
387 }
388 }
389
390 let mut args = vec![
392 "+nightly",
393 "build",
394 "--target",
395 "wasm32-unknown-unknown",
396 "-Z",
397 "build-std=panic_abort,std",
398 "--target-dir",
399 &target_dir,
400 ];
401 if *release {
402 args.push("--release");
403 }
404
405 let command = command.args(args).current_dir(module_dir.clone());
406 let command_debug = format!("{command:?}");
407 let out = command.output();
408 match out {
409 Ok(out) => {
410 if !out.status.success() {
411 return Err(format!(
412 "failed to build module `{}`: \nrunning `{}`\n{}",
413 module_dir.display(),
414 command_debug,
415 String::from_utf8_lossy(&out.stderr).replace('\n', "\n\t")
416 ));
417 }
418 }
419 Err(e) => {
420 return Err(format!(
421 "failed to build module `{}`: \nrunning `{}`\n{e}",
422 command_debug,
423 module_dir.display()
424 ))
425 }
426 }
427
428 let root_output = module_dir.join(target_dir).join("wasm32-unknown-unknown/");
430 let glob = if *release {
431 root_output.join("release/")
432 } else {
433 root_output.join("debug/")
434 }
435 .join("*.wasm");
436 let mut glob_paths = glob::glob(
437 glob.as_os_str()
438 .to_str()
439 .expect("output path should be unicode compliant"),
440 )
441 .expect("glob should be valid");
442
443 let output = match glob_paths.next() {
444 Some(Ok(output)) => output,
445 Some(Err(err)) => {
446 return Err(format!(
447 "failed to find output file matching `{glob:?}`: {err} - this is probably a bug",
448 ))
449 }
450 None => {
451 return Err(format!(
452 "failed to find output file matching `{}` - this is probably a bug",
453 glob.display()
454 ))
455 }
456 };
457
458 if let Some(Ok(_)) = glob_paths.next() {
460 return Err(format!("multiple output files matching `{}` were found - this may be because you recently changed the name of your module; try deleting the folder `{}` and rebuilding", glob.display(), root_output.display()));
461 }
462
463 drop(lock);
464
465 Ok(output)
466}
467
468fn all_module_files(path: PathBuf) -> Vec<String> {
469 let glob_paths = glob::glob(
470 path.as_os_str()
471 .to_str()
472 .expect("output path should be unicode compliant"),
473 )
474 .expect("glob should be valid");
475
476 glob_paths
477 .into_iter()
478 .filter_map(|path| {
479 let path = path.ok()?;
480 if !path.is_file() {
481 None
482 } else {
483 Some(path.to_string_lossy().to_string())
484 }
485 })
486 .collect()
487}
488
489#[proc_macro]
521pub fn build_wasm(args: TokenStream) -> TokenStream {
522 let mut args = parse_macro_input!(args as Args);
524
525 #[cfg(not(feature = "proc_macro_span"))]
526 let invocation_file = {
527 let root =
528 std::env::var("CARGO_MANIFEST_DIR").expect("proc macros should be run using cargo");
529 find_me(&root, &format!("\"{}\"", args.module_dir.to_string_lossy()))
530 };
531 #[cfg(feature = "proc_macro_span")]
532 let invocation_file = proc_macro::Span::call_site().source_file().path();
533 let invocation_file = invocation_file
534 .parent()
535 .unwrap()
536 .to_path_buf()
537 .canonicalize()
538 .unwrap();
539 args.module_dir = invocation_file.join(args.module_dir);
540
541 let result = do_build_wasm(&args);
543
544 match result {
546 Ok(bytes_path) => {
547 let bytes_path = bytes_path.to_string_lossy().to_string();
548 let module_paths = all_module_files(args.module_dir);
550
551 quote! {
552 {
553 #(
554 let _ = include_str!(#module_paths);
555 )*
556 include_bytes!(#bytes_path) as &'static [u8]
557 }
558 }
559 }
560 Err(err) => quote! {
561 {
562 compile_error!(#err);
563 const BS: &'static [u8] = &[0u8];
564 BS
565 }
566 },
567 }
568 .into()
569}