Skip to main content

comfy_git_version_macro/
lib.rs

1extern crate proc_macro;
2
3use proc_macro::{Literal, TokenStream, TokenTree};
4
5mod utils;
6use self::utils::describe_cwd;
7
8/// Get the git version for the source code.
9///
10/// # Examples
11///
12/// ```ignore
13/// const VERSION: &str = git_version!();
14/// // Return "unknown" instead of hard-failing the build when `git describe`
15/// // can't run (e.g. no git binary, or building outside a git checkout).
16/// const VERSION: &str = git_version!(fallback = "unknown");
17/// ```
18#[proc_macro]
19pub fn git_version(input: TokenStream) -> TokenStream {
20	let fallback = parse_fallback(input);
21	let version = resolve_version(describe_cwd(), fallback);
22
23	TokenTree::Literal(Literal::string(&version)).into()
24}
25
26/// Pick the version string: the `git describe` result on success, else the
27/// caller's `fallback`. With no fallback a git failure is still fatal (a build
28/// that asked for the real version and can't get it should fail loudly).
29fn resolve_version(described: std::io::Result<String>, fallback: Option<String>) -> String {
30	match described {
31		Ok(version) => version,
32		Err(e) => match fallback {
33			Some(fallback) => fallback,
34			None => panic!("git describe failed and no `fallback = \"...\"` was provided: {}", e),
35		},
36	}
37}
38
39/// Parse `fallback = "literal"` out of the macro input, if present.
40fn parse_fallback(input: TokenStream) -> Option<String> {
41	let mut iter = input.into_iter();
42	while let Some(tok) = iter.next() {
43		let is_fallback = matches!(&tok, TokenTree::Ident(id) if id.to_string() == "fallback");
44		if !is_fallback {
45			continue;
46		}
47		match iter.next() {
48			Some(TokenTree::Punct(p)) if p.as_char() == '=' => {}
49			other => panic!("expected `=` after `fallback`, got {:?}", other),
50		}
51		match iter.next() {
52			Some(TokenTree::Literal(lit)) => return Some(unquote(&lit.to_string())),
53			other => panic!("expected a string literal after `fallback =`, got {:?}", other),
54		}
55	}
56	None
57}
58
59/// Strip the surrounding double quotes off a string-literal token's text.
60fn unquote(lit: &str) -> String {
61	let bytes = lit.as_bytes();
62	if bytes.len() >= 2 && bytes[0] == b'"' && bytes[bytes.len() - 1] == b'"' {
63		lit[1..lit.len() - 1].to_string()
64	} else {
65		lit.to_string()
66	}
67}
68
69#[cfg(test)]
70mod tests {
71	use super::{resolve_version, unquote};
72
73	fn err() -> std::io::Result<String> {
74		Err(std::io::Error::new(std::io::ErrorKind::Other, "boom"))
75	}
76
77	#[test]
78	fn fallback_used_when_git_fails() {
79		assert_eq!(resolve_version(err(), Some("unknown".into())), "unknown");
80	}
81
82	#[test]
83	fn ok_wins_over_fallback() {
84		assert_eq!(resolve_version(Ok("v1.2-3-gabc".into()), Some("unknown".into())), "v1.2-3-gabc");
85	}
86
87	#[test]
88	#[should_panic(expected = "no `fallback")]
89	fn no_fallback_still_panics_on_err() {
90		resolve_version(err(), None);
91	}
92
93	#[test]
94	fn unquote_strips_quotes() {
95		assert_eq!(unquote("\"unknown\""), "unknown");
96		assert_eq!(unquote("bare"), "bare");
97	}
98}