Skip to main content

gvdb_macros/
lib.rs

1//! This crate offers convenience macros for [gvdb](https://!github.com/felinira/gvdb-rs).
2//! The macros are [`include_gresource_from_xml!()`] and
3//! [`include_gresource_from_dir!()`]
4//!
5//! ## Examples
6//!
7//! Compile a GResource XML file and include the bytes in the file.
8//!
9//! ```
10//! use gvdb_macros::include_gresource_from_xml;
11//! static GRESOURCE_BYTES: &[u8] = include_gresource_from_xml!("test-data/gresource/test3.gresource.xml");
12//! ```
13//!
14//! Scan a directory and create a GResource file with all the contents of the directory.
15//!
16//! ```
17//! use gvdb_macros::include_gresource_from_dir;
18//! static GRESOURCE_BYTES: &[u8] = include_gresource_from_dir!("/gvdb/rs/test", "test-data/gresource");
19//! ```
20
21#![warn(missing_docs)]
22#![doc = include_str!("../README.md")]
23
24extern crate proc_macro;
25
26use litrs::{Literal, StringLit};
27use proc_macro2::TokenTree;
28use quote::quote;
29use std::path::PathBuf;
30
31fn quote_bytes(bytes: &[u8]) -> proc_macro2::TokenStream {
32    let bytes_lit = proc_macro2::Literal::byte_string(bytes);
33
34    quote! {
35        {{
36            #[repr(align(16))]
37            #[doc(hidden)]
38            struct __GvdbAligned<T: ?Sized>(T);
39            #[doc(hidden)]
40            static __GVDB_DATA: &'static __GvdbAligned<[u8]> = &__GvdbAligned(*#bytes_lit);
41
42            &__GVDB_DATA.0
43        }}
44    }
45}
46
47fn include_gresource_from_xml_with_filename(filename: &str) -> proc_macro2::TokenStream {
48    let path = PathBuf::from(filename);
49    let xml = gvdb::gresource::XmlManifest::from_file(&path).unwrap();
50    let builder = gvdb::gresource::BundleBuilder::from_xml(xml).unwrap();
51    let data = builder.build().unwrap();
52
53    quote_bytes(&data)
54}
55
56fn include_gresource_from_xml_inner(input: proc_macro2::TokenStream) -> proc_macro2::TokenStream {
57    let mut iter = input.into_iter();
58
59    let first = iter
60        .next()
61        .expect("Expected exactly one string literal argument (gresource file location)");
62    let second = iter.next();
63    if let Some(second) = second {
64        panic!(
65            "Unexpected token '{second}', expected exactly one string literal argument (gresource file location)"
66        )
67    }
68
69    match Literal::try_from(first) {
70        Err(e) => proc_macro2::TokenStream::from(e.to_compile_error()),
71        Ok(Literal::String(str)) => include_gresource_from_xml_with_filename(str.value()),
72        Ok(other) => panic!(
73            "Unexpected token '{other:?}', expected exactly one string literal argument (gresource file location)"
74        ),
75    }
76}
77
78/// Compile a GResource XML file to its binary representation and include it in the source file.
79///
80/// ```
81/// use gvdb_macros::include_gresource_from_xml;
82/// static GRESOURCE_BYTES: &[u8] = include_gresource_from_xml!("test-data/gresource/test3.gresource.xml");
83/// ```
84#[proc_macro]
85pub fn include_gresource_from_xml(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
86    let input = proc_macro2::TokenStream::from(input);
87    let output = include_gresource_from_xml_inner(input);
88    proc_macro::TokenStream::from(output)
89}
90
91fn include_gresource_from_dir_str(prefix: &str, directory: &str) -> proc_macro2::TokenStream {
92    let path = PathBuf::from(directory);
93    let builder =
94        gvdb::gresource::BundleBuilder::from_directory(prefix, &path, true, true).unwrap();
95    let data = builder.build().unwrap();
96
97    quote_bytes(&data)
98}
99
100fn include_gresource_from_dir_inner(input: proc_macro2::TokenStream) -> proc_macro2::TokenStream {
101    let err_msg = "expected exactly two string literal arguments (prefix, gresource directory)";
102    let (prefix, directory) = match &*input.into_iter().collect::<Vec<_>>() {
103        [
104            TokenTree::Literal(str1),
105            TokenTree::Punct(comma),
106            TokenTree::Literal(str2),
107        ] => {
108            if comma.as_char() != ',' {
109                panic!("{}", err_msg);
110            }
111
112            (
113                StringLit::try_from(str1).expect(err_msg),
114                StringLit::try_from(str2).expect(err_msg),
115            )
116        }
117        _ => panic!("{}", err_msg),
118    };
119
120    include_gresource_from_dir_str(prefix.value(), directory.value())
121}
122
123/// Scan a directory and create a GResource file with all the contents of the directory.
124///
125/// This will ignore any files that end with gresource.xml and meson.build, as
126/// those are most likely not needed inside the GResource.
127///
128/// This is equivalent to the following XML:
129///
130/// ```xml
131/// <gresources>
132///   <gresource prefix="`prefix`">
133///     <!-- file entries for each file with path beginning from `directory` as root -->
134///   </gresource>
135/// </gresources>
136/// ```
137///
138/// The first argument to this macro is the prefix for the GResource file. The second argument is
139/// the path to the folder containing the files to include in the file.
140///
141/// This acts as if every xml file uses the option `xml-stripblanks` in the GResource XML and every
142/// JSON file uses `json-stripblanks`.
143///
144/// JSON files are all files with the extension '.json'.
145/// XML files are all files with the extensions '.xml', '.ui', '.svg'
146///
147/// All files that end with `.ui` and `.css` are compressed.
148/// ```
149/// use gvdb_macros::include_gresource_from_dir;
150/// static GRESOURCE_BYTES: &[u8] = include_gresource_from_dir!("/gvdb/rs/tests/data", "test-data/gresource");
151/// ```
152#[proc_macro]
153pub fn include_gresource_from_dir(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
154    let input = proc_macro2::TokenStream::from(input);
155    let output = include_gresource_from_dir_inner(input);
156    proc_macro::TokenStream::from(output)
157}
158
159#[cfg(test)]
160mod tests {
161    use super::*;
162    use quote::quote;
163
164    #[test]
165    fn include_gresource_from_xml() {
166        let tokens =
167            include_gresource_from_xml_inner(quote! {"test-data/gresource/test3.gresource.xml"});
168        assert!(tokens.to_string().contains(r#"b"GVariant"#));
169    }
170
171    #[test]
172    #[should_panic]
173    fn include_gresource_from_xml_panic() {
174        include_gresource_from_xml_inner(quote! {4});
175    }
176
177    #[test]
178    #[should_panic]
179    fn include_gresource_from_xml_panic2() {
180        include_gresource_from_xml_inner(quote! { "test", 4 });
181    }
182
183    #[test]
184    #[should_panic]
185    fn include_gresource_from_xml_panic3() {
186        include_gresource_from_xml_inner(quote! { test });
187    }
188
189    #[test]
190    fn include_gresource_from_dir() {
191        let tokens =
192            include_gresource_from_dir_inner(quote! {"/gvdb/rs/test", "test-data/gresource"});
193        assert!(tokens.to_string().contains(r#"b"GVariant"#));
194    }
195
196    #[test]
197    #[should_panic]
198    fn include_gresource_from_dir_panic1() {
199        include_gresource_from_dir_inner(quote! {"/gvdb/rs/test",});
200    }
201
202    #[test]
203    #[should_panic]
204    fn include_gresource_from_dir_panic2() {
205        include_gresource_from_dir_inner(quote! {"/gvdb/rs/test"});
206    }
207
208    #[test]
209    #[should_panic]
210    fn include_gresource_from_dir_panic3() {
211        include_gresource_from_dir_inner(quote! {"/gvdb/rs/test","bla","bla"});
212    }
213
214    #[test]
215    #[should_panic]
216    fn include_gresource_from_dir_panic4() {
217        include_gresource_from_dir_inner(quote! {"/gvdb/rs/test","INVALID_DIRECTORY"});
218    }
219
220    #[test]
221    #[should_panic]
222    fn include_gresource_from_dir_panic5() {
223        include_gresource_from_dir_inner(quote! {"/gvdb/rs/test"."test-data/gresource"});
224    }
225}