const_pkg_version_macros/
lib.rs1use proc_macro::{Group, Ident, Literal, Span, TokenStream, TokenTree};
2
3mod error;
4use error::Error;
5
6#[proc_macro]
7pub fn major(input: TokenStream) -> TokenStream {
8 let tokens = match impl_u32_component(input, "CARGO_PKG_VERSION_MAJOR") {
9 Ok(x) => x,
10 Err(e) => return e.to_compile_error(),
11 };
12 surround_braces(tokens)
13}
14
15#[proc_macro]
16pub fn minor(input: TokenStream) -> TokenStream {
17 let tokens = match impl_u32_component(input, "CARGO_PKG_VERSION_MINOR") {
18 Ok(x) => x,
19 Err(e) => return e.to_compile_error(),
20 };
21 surround_braces(tokens)
22}
23
24#[proc_macro]
25pub fn patch(input: TokenStream) -> TokenStream {
26 let tokens = match impl_u32_component(input, "CARGO_PKG_VERSION_PATCH") {
27 Ok(x) => x,
28 Err(e) => return e.to_compile_error(),
29 };
30 surround_braces(tokens)
31}
32
33fn impl_u32_component(input: TokenStream, name: &str) -> Result<TokenStream, Error> {
34 let _ = MacroInput::parse(input)?;
35 let value = get_env_u32(name)?;
36 Ok([TokenTree::Literal(Literal::u32_unsuffixed(value))].into_iter().collect())
37}
38
39#[proc_macro]
40pub fn pre_release(input: TokenStream) -> TokenStream {
41 let tokens = match impl_string_component(input, "CARGO_PKG_VERSION_PRE") {
42 Ok(x) => x,
43 Err(e) => return e.to_compile_error(),
44 };
45 surround_braces(tokens)
46}
47
48fn impl_string_component(input: TokenStream, name: &str) -> Result<TokenStream, Error> {
49 let _ = MacroInput::parse(input)?;
50 let value = get_env_str(name)?;
51 let value = match value.as_str() {
52 "" => None,
53 x => Some(x),
54 };
55 Ok(option_str(value))
56}
57
58#[proc_macro]
59pub fn build_metadata(input: TokenStream) -> TokenStream {
60 let tokens = match impl_build_metadata(input, "CARGO_PKG_VERSION") {
61 Ok(x) => x,
62 Err(e) => return e.to_compile_error(),
63 };
64 surround_braces(tokens)
65}
66
67fn impl_build_metadata(input: TokenStream, name: &str) -> Result<TokenStream, Error> {
68 let _ = MacroInput::parse(input)?;
69 let value = get_env_str(name)?;
70 let (_, build_metadata) = split_once_optional(&value, '+');
71 Ok(option_str(build_metadata))
72}
73
74#[proc_macro]
75pub fn full(input: TokenStream) -> TokenStream {
76 let tokens = match impl_full(input, "CARGO_PKG_VERSION") {
77 Ok(x) => x,
78 Err(e) => return e.to_compile_error(),
79 };
80 surround_braces(tokens)
81}
82
83fn impl_full(input: TokenStream, name: &str) -> Result<TokenStream, Error> {
84 let input = MacroInput::parse(input)?;
85 let value = get_env_str(name)?;
86 let version = PkgVersion::from_str(&value).map_err(Error::call_site)?;
87
88 let mut output = StructBuilder::new_crate_local(input.self_crate, "Version");
89 output.field("major", [TokenTree::Literal(Literal::u32_suffixed(version.major))]);
90 output.field("minor", [TokenTree::Literal(Literal::u32_suffixed(version.minor))]);
91 output.field("patch", [TokenTree::Literal(Literal::u32_suffixed(version.patch))]);
92 output.field("pre_release", option_str(version.pre_release));
93 output.field("build_metadata", option_str(version.build_metadata));
94
95 Ok(output.finish())
96}
97
98fn get_env_str(name: &str) -> Result<String, Error> {
99 match std::env::var(name) {
100 Ok(x) => Ok(x),
101 Err(std::env::VarError::NotPresent) => Err(Error::call_site(format!("environment variable {name} not set"))),
102 Err(std::env::VarError::NotUnicode(_)) => Err(Error::call_site(format!("environment variable {name} contains non UTF-8 data"))),
103 }
104}
105
106fn get_env_u32(name: &str) -> Result<u32, Error> {
107 get_env_str(name)?
108 .parse()
109 .map_err(|e| Error::call_site(format!("environment variable {name} is not a valid u32: {e}")))
110}
111
112struct PkgVersion<'a> {
113 major: u32,
114 minor: u32,
115 patch: u32,
116 pre_release: Option<&'a str>,
117 build_metadata: Option<&'a str>,
118}
119
120impl<'a> PkgVersion<'a> {
121 fn from_str(input: &'a str) -> Result<Self, String> {
122 let rest = &input;
123 let (rest, build_metadata) = split_once_optional(rest, '+');
124 let (rest, pre_release) = split_once_optional(rest, '-');
125 let [major, minor, patch] = split_exact(rest, '.')
126 .map_err(|()| "invalid version: expected MAJOR.MINOR.PATCH")?;
127
128 let major = major.parse()
129 .map_err(|e| format!("invalid major version number: {e}"))?;
130 let minor = minor.parse()
131 .map_err(|e| format!("invalid minor version number: {e}"))?;
132 let patch = patch.parse()
133 .map_err(|e| format!("invalid patch version number: {e}"))?;
134 Ok(PkgVersion {
135 major,
136 minor,
137 patch,
138 pre_release,
139 build_metadata,
140 })
141 }
142}
143
144struct MacroInput {
145 self_crate: Ident,
146}
147
148impl MacroInput {
149 fn parse(input: TokenStream) -> Result<MacroInput, Error> {
150 let mut input = input.into_iter();
151
152 let self_crate = input.next()
153 .ok_or_else(|| Error::call_site("missing argument: `$crate`"))?;
154 let self_crate = match self_crate {
155 TokenTree::Ident(x) => x,
156 other => return Err(Error::new(other.span(), "expected `$crate`")),
157 };
158
159 match input.next() {
160 None => (),
161 Some(TokenTree::Punct(x)) if x.as_char() == ',' => {
162 match input.next() {
163 None => (),
164 Some(TokenTree::Punct(x)) => return Err(Error::new(x.span(), "unexpected puncutation")),
165 Some(other) => return Err(Error::new(other.span(), "unexpected argument")),
166 }
167 },
168 Some(x) => return Err(Error::new(x.span(), "unexpected token")),
169 };
170
171 Ok(MacroInput { self_crate })
172 }
173}
174
175
176fn surround_braces(tokens: TokenStream) -> TokenStream {
177 [TokenTree::Group(Group::new(proc_macro::Delimiter::Brace, tokens))].into_iter().collect()
178}
179
180fn surround_parens(tokens: TokenStream) -> TokenStream {
181 let expr = TokenTree::Group(Group::new(proc_macro::Delimiter::Parenthesis, tokens));
182 [expr].into_iter().collect()
183}
184
185fn split_once_optional(input: &str, delimiter: char) -> (&str, Option<&str>) {
186 match input.split_once(delimiter) {
187 Some((left, right)) => (left, Some(right)),
188 None => (input, None),
189 }
190}
191
192fn split_exact<const N: usize>(input: &str, delimiter: char) -> Result<[&str; N], ()> {
193 let mut output = [""; N];
194 let mut fields = input.split(delimiter);
195 for output in &mut output {
196 *output = fields.next().ok_or(())?;
197 }
198
199 if fields.next().is_some() {
200 return Err(());
201 }
202
203 Ok(output)
204}
205
206fn tokens(input: &str) -> TokenStream {
207 input.parse().unwrap()
208}
209
210struct StructBuilder {
211 name: TokenStream,
212 fields: TokenStream,
213}
214
215impl StructBuilder {
216 fn new(name: TokenStream) -> Self {
217 Self {
218 name,
219 fields: TokenStream::new(),
220 }
221 }
222
223 fn new_crate_local(crate_ident: Ident, local_path: &str) -> Self {
224 use proc_macro::{Punct, Spacing};
225
226 let mut name = TokenStream::new();
227 name.extend([
228 TokenTree::Ident(crate_ident),
229 TokenTree::Punct(Punct::new(':', Spacing::Joint)),
230 TokenTree::Punct(Punct::new(':', Spacing::Alone)),
231 ]);
232 name.extend(tokens(local_path));
233
234 Self::new(name)
235 }
236
237 fn field(&mut self, name: &str, data: impl IntoIterator<Item = TokenTree>) {
238 use proc_macro::{Punct, Spacing};
239
240 let name = Ident::new(name, Span::call_site());
241 self.fields.extend([
242 TokenTree::Ident(name),
243 TokenTree::Punct(Punct::new(':', Spacing::Alone)),
244 ]);
245 self.fields.extend(data);
246 self.fields.extend([
247 TokenTree::Punct(Punct::new(',', Spacing::Alone)),
248 ]);
249 }
250
251 fn finish(self) -> TokenStream {
252 let mut output = self.name;
253 output.extend(surround_braces(self.fields));
254 output
255 }
256}
257
258fn option_str(input: Option<&str>) -> TokenStream {
259 match input {
260 None => tokens("::core::option::Option::None::<&::core::primitive::str>"),
261 Some(x) => {
262 let mut output = tokens("::core::option::Option::Some");
263 output.extend(surround_parens([TokenTree::Literal(Literal::string(x))].into_iter().collect()));
264 output
265 }
266 }
267}