Skip to main content

include_first/
lib.rs

1// Copyright 2026 The include-first Authors.
2// This project is dual-licensed under Apache 2.0 and MIT terms.
3// See LICENSE-APACHE and LICENSE-MIT for details.
4
5use proc_macro::{Delimiter, Group, Literal, TokenStream, TokenTree};
6use std::{env::current_dir, fs::read_to_string};
7
8/// Evaluates `include_str!` macros early.
9///
10/// This can be used to apply them in the context of a `macro_rules!` declaration, rather than in
11/// the context where the macro is used.
12///
13/// # Example
14///
15/// ```compile_fail
16/// use include_first::include_first;
17///
18/// #[macro_export]
19/// #[include_first]
20/// macro_rules! generate_asm {
21///     ($foo:expr) => {
22///         core::arch::global_asm!(
23///             include_str!("entrypoint.S"),
24///             foo = const $foo,
25///         );
26///     }
27/// }
28///
29/// ```
30#[proc_macro_attribute]
31pub fn include_first(_attr: TokenStream, item: TokenStream) -> TokenStream {
32    include_strings(item)
33}
34
35fn include_strings(stream: TokenStream) -> TokenStream {
36    let mut input = stream.into_iter().collect::<Vec<_>>();
37    let mut output = TokenStream::new();
38    let mut i = 0;
39    while i < input.len() {
40        if let TokenTree::Ident(ident) = &input[i]
41            && ident.span().source_text().unwrap_or_default() == "include_str"
42            && let Some(TokenTree::Punct(bang)) = input.get(i + 1)
43            && bang.as_char() == '!'
44            && let Some(TokenTree::Group(args)) = input.get(i + 2)
45            && args.delimiter() == Delimiter::Parenthesis
46            && let [TokenTree::Literal(filename)] =
47                args.stream().into_iter().collect::<Vec<_>>().as_slice()
48        {
49            let filename = filename.span().source_text().unwrap();
50            let filename = format!("src/{}", filename.trim_matches('"'));
51
52            // Read the file and output a string literal.
53            let file_contents = match read_to_string(&filename) {
54                Err(e) => panic!(
55                    "Error reading {filename} from {:?}/src: {e}",
56                    current_dir().unwrap()
57                ),
58                Ok(contents) => contents,
59            };
60            output.extend(Some(TokenTree::Literal(Literal::string(&file_contents))));
61
62            // Skip all the tokens.
63            i += 3;
64            continue;
65        }
66        if let TokenTree::Group(group) = &mut input[i] {
67            let stream = include_strings(group.stream());
68            output.extend(Some(TokenTree::Group(Group::new(
69                group.delimiter(),
70                stream,
71            ))));
72        } else {
73            output.extend(Some(input[i].clone()));
74        }
75        i += 1;
76    }
77    output
78}