const_pkg_version_macros/
lib.rs

1use 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}