git_version_macro/
lib.rs

1use proc_macro::TokenStream;
2use proc_macro2::TokenStream as TokenStream2;
3use quote::{quote, ToTokens};
4
5macro_rules! error {
6	($($args:tt)*) => {
7		syn::Error::new(proc_macro2::Span::call_site(), format!($($args)*))
8	};
9}
10
11mod args;
12mod utils;
13
14/// Get the git version for the source code.
15///
16/// The following (named) arguments can be given:
17///
18/// - `args`: The arguments to call `git describe` with.
19///   Default: `args = ["--always", "--dirty=-modified"]`
20///
21/// - `prefix`, `suffix`:
22///   The git version will be prefixed/suffexed by these strings.
23///
24/// - `cargo_prefix`, `cargo_suffix`:
25///   If either is given, Cargo's version (given by the CARGO_PKG_VERSION
26///   environment variable) will be used if git fails instead of giving an
27///   error. It will be prefixed/suffixed by the given strings.
28///
29/// - `fallback`:
30///   If all else fails, this string will be given instead of reporting an
31///   error.
32///
33/// # Examples
34///
35/// ```
36/// # use git_version::git_version;
37/// const VERSION: &str = git_version!();
38/// ```
39///
40/// ```
41/// # use git_version::git_version;
42/// const VERSION: &str = git_version!(args = ["--abbrev=40", "--always"]);
43/// ```
44///
45/// ```
46/// # use git_version::git_version;
47/// const VERSION: &str = git_version!(prefix = "git:", cargo_prefix = "cargo:", fallback = "unknown");
48/// ```
49#[proc_macro]
50pub fn git_version(input: TokenStream) -> TokenStream {
51	let args = syn::parse_macro_input!(input as args::Args);
52
53	let tokens = match git_version_impl(args) {
54		Ok(x) => x,
55		Err(e) => e.to_compile_error(),
56	};
57
58	TokenStream::from(tokens)
59}
60
61fn git_version_impl(args: args::Args) -> syn::Result<TokenStream2> {
62	let git_args = args.git_args.map_or_else(
63		|| vec!["--always".to_string(), "--dirty=-modified".to_string()],
64		|list| list.iter().map(|x| x.value()).collect(),
65	);
66
67	let cargo_fallback = args.cargo_prefix.is_some() || args.cargo_suffix.is_some();
68
69	let manifest_dir = std::env::var_os("CARGO_MANIFEST_DIR")
70		.ok_or_else(|| error!("CARGO_MANIFEST_DIR is not set"))?;
71
72	match utils::describe(manifest_dir, git_args) {
73		Ok(version) => {
74			let dependencies = utils::git_dependencies()?;
75			let prefix = args.prefix.iter();
76			let suffix = args.suffix;
77			Ok(quote!({
78				#dependencies;
79				concat!(#(#prefix,)* #version, #suffix)
80			}))
81		}
82		Err(_) if cargo_fallback => {
83			if let Ok(version) = std::env::var("CARGO_PKG_VERSION") {
84				let prefix = args.cargo_prefix.iter();
85				let suffix = args.cargo_suffix;
86				Ok(quote!(concat!(#(#prefix,)* #version, #suffix)))
87			} else if let Some(fallback) = args.fallback {
88				Ok(fallback.to_token_stream())
89			} else {
90				Err(error!("Unable to get git or cargo version"))
91			}
92		}
93		Err(_) if args.fallback.is_some() => Ok(args.fallback.to_token_stream()),
94		Err(e) => Err(error!("{}", e)),
95	}
96}
97
98/// Get the git version of all submodules below the cargo project.
99///
100/// This macro expands to `[(&str, &str), N]` where `N` is the total number of
101/// submodules below the root of the project (evaluated recursively)
102///
103/// Each entry in the array is a tuple of the submodule path and the version information.
104///
105/// The following (named) arguments can be given:
106///
107/// - `args`: The arguments to call `git describe` with.
108///   Default: `args = ["--always", "--dirty=-modified"]`
109///
110/// - `prefix`, `suffix`:
111///   The git version for each submodule will be prefixed/suffixed
112///   by these strings.
113///
114/// - `fallback`:
115///   If all else fails, this string will be given instead of reporting an
116///   error. This will yield the same type as if the macro was a success, but
117///   format will be `[("relative/path/to/submodule", {fallback})]`
118///
119/// # Examples
120///
121/// ```
122/// # use git_version::git_submodule_versions;
123/// # const N: usize = 0;
124/// const MODULE_VERSIONS: [(&str, &str); N] = git_submodule_versions!();
125/// for (path, version) in MODULE_VERSIONS {
126///     println!("{path}: {version}");
127/// }
128/// ```
129///
130/// ```
131/// # use git_version::git_submodule_versions;
132/// # const N: usize = 0;
133/// const MODULE_VERSIONS: [(&str, &str); N] = git_submodule_versions!(args = ["--abbrev=40", "--always"]);
134/// ```
135///
136/// ```
137/// # use git_version::git_submodule_versions;
138/// # const N: usize = 0;
139/// const MODULE_VERSIONS: [(&str, &str); N] = git_submodule_versions!(prefix = "git:", fallback = "unknown");
140/// ```
141#[proc_macro]
142pub fn git_submodule_versions(input: TokenStream) -> TokenStream {
143	let args = syn::parse_macro_input!(input as args::Args);
144
145	let tokens = match git_submodule_versions_impl(args) {
146		Ok(x) => x,
147		Err(e) => e.to_compile_error(),
148	};
149
150	TokenStream::from(tokens)
151}
152
153fn git_submodule_versions_impl(args: args::Args) -> syn::Result<TokenStream2> {
154	if let Some(cargo_prefix) = &args.cargo_prefix {
155		return Err(syn::Error::new_spanned(cargo_prefix, "invalid argument `cargo_prefix` for `git_submodule_versions!()`"));
156	}
157	if let Some(cargo_suffix) = &args.cargo_suffix {
158		return Err(syn::Error::new_spanned(cargo_suffix, "invalid argument `cargo_suffix` for `git_submodule_versions!()`"));
159	}
160
161	let manifest_dir = std::env::var_os("CARGO_MANIFEST_DIR")
162		.ok_or_else(|| error!("CARGO_MANIFEST_DIR is not set"))?;
163	let git_dir = crate::utils::git_dir(&manifest_dir)
164		.map_err(|e| error!("failed to determine .git directory: {}", e))?;
165
166	let modules = match crate::utils::get_submodules(&manifest_dir) {
167		Ok(x) => x,
168		Err(err) => return Err(error!("{}", err)),
169	};
170
171	// Ensure that the type of the empty array is still known to the compiler.
172	if modules.is_empty() {
173		return Ok(quote!([("", ""); 0]));
174	}
175
176	let git_args = args.git_args.as_ref().map_or_else(
177		|| vec!["--always".to_string(), "--dirty=-modified".to_string()],
178		|list| list.iter().map(|x| x.value()).collect(),
179	);
180
181	let root_dir = git_dir.join("..");
182	let mut versions = Vec::new();
183	for submodule in &modules {
184		let path = root_dir.join(submodule);
185		// Get the submodule version or fallback.
186		let version = match crate::utils::describe(path, &git_args) {
187			Ok(version) => {
188				let prefix = args.prefix.iter();
189				let suffix = args.suffix.iter();
190				quote!{
191					::core::concat!(#(#prefix,)* #version #(, #suffix)*)
192				}
193			}
194			Err(e) => {
195				if let Some(fallback) = &args.fallback {
196					quote!( #fallback )
197				} else {
198					return Err(error!("{}", e));
199				}
200			},
201		};
202		versions.push(version);
203	}
204
205	Ok(quote!({
206		[#((#modules, #versions)),*]
207	}))
208}