Skip to content

Commit

Permalink
Merge pull request #23 from baxterjo/master
Browse files Browse the repository at this point in the history
Add Support for Submodule Versioning
  • Loading branch information
de-vri-es authored Dec 11, 2023
2 parents c7836c9 + 59f6c35 commit 14f3942
Show file tree
Hide file tree
Showing 5 changed files with 298 additions and 27 deletions.
176 changes: 176 additions & 0 deletions git-version-macro/src/describe_submodules.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
extern crate proc_macro;
use crate::canonicalize_path;
use crate::git_dependencies;
use crate::utils::run_git;
use proc_macro2::{Span, TokenStream as TokenStream2};
use quote::quote;
use std::ffi::OsStr;
use std::path::Path;
use std::process::Command;
use syn::{
bracketed,
parse::{Parse, ParseStream},
punctuated::Punctuated,
token::{Comma, Eq},
Ident, LitStr,
};

macro_rules! error {
($($args:tt)*) => {
syn::Error::new(Span::call_site(), format!($($args)*))
};
}

#[derive(Default)]
pub(crate) struct GitModArgs {
args: Option<Punctuated<LitStr, Comma>>,
prefix: Option<LitStr>,
suffix: Option<LitStr>,
fallback: Option<LitStr>,
}

impl Parse for GitModArgs {
fn parse(input: ParseStream) -> syn::Result<Self> {
let mut result = GitModArgs::default();
loop {
if input.is_empty() {
break;
}
let ident: Ident = input.parse()?;
let _: Eq = input.parse()?;
let check_dup = |dup: bool| {
if dup {
Err(error!("`{} = ` can only appear once", ident))
} else {
Ok(())
}
};
match ident.to_string().as_str() {
"args" => {
check_dup(result.args.is_some())?;
let content;
bracketed!(content in input);
result.args = Some(Punctuated::parse_terminated(&content)?);
}
"prefix" => {
check_dup(result.prefix.is_some())?;
result.prefix = Some(input.parse()?);
}
"suffix" => {
check_dup(result.suffix.is_some())?;
result.suffix = Some(input.parse()?);
}
"fallback" => {
check_dup(result.fallback.is_some())?;
result.fallback = Some(input.parse()?);
}
x => Err(error!("Unexpected argument name `{}`", x))?,
}
if input.is_empty() {
break;
}
let _: Comma = input.parse()?;
}
Ok(result)
}
}

pub(crate) fn git_module_versions_impl(args: GitModArgs) -> syn::Result<TokenStream2> {
let mut modules = match get_modules() {
Ok(x) => x,
Err(err) => return Err(error!("{}", err)),
};

modules.retain(|path| !path.is_empty());

let mut describe_paths: Vec<(String, String)> = vec![];

for path in modules {
let path_obj = Path::new(&path);
let path_obj = canonicalize_path(path_obj)?;
describe_paths.push((path, path_obj));
}

let git_describe_args = args.args.map_or_else(
|| vec!["--always".to_string(), "--dirty=-modified".to_string()],
|list| list.iter().map(|x| x.value()).collect(),
);

let prefix = match args.prefix {
Some(x) => x.value(),
_ => "".to_string(),
};
let suffix = match args.suffix {
Some(x) => x.value(),
_ => "".to_string(),
};
let fallback = args.fallback.map(|x| x.value());

match describe_modules(describe_paths, &git_describe_args, prefix, suffix, fallback) {
Ok(result) => {
let dependencies = git_dependencies()?;
let (paths, versions) = result;

Ok(quote!({
#dependencies;

[#((#paths, #versions)),*]

}))
}
Err(e) => Err(error!("{}", e)),
}
}

/// Run `git submodule foreach` command to discover submodules in the project.
fn get_modules() -> Result<Vec<String>, String> {
let mut args: Vec<String> = "submodule foreach --quiet --recursive"
.to_string()
.split(' ')
.map(|x| x.to_string())
.collect();

args.push("echo $displaypath".to_string());

let result = run_git("git submodule", Command::new("git").args(args))?;

Ok(result.split('\n').map(|x| x.to_string()).collect())
}

/// Run `git describe` for each submodule to get the git version with the specified args.
fn describe_modules<I, S>(
paths: Vec<(String, String)>,
describe_args: I,
prefix: String,
suffix: String,
fallback: Option<String>,
) -> Result<(Vec<String>, Vec<String>), String>
where
I: IntoIterator<Item = S> + Clone,
S: AsRef<OsStr>,
{
let mut paths_out: Vec<String> = vec![];
let mut versions: Vec<String> = vec![];

for (rel_path, abs_path) in paths.into_iter() {
// Get the submodule version or fallback.
let result = match run_git(
"git describe",
Command::new("git")
.current_dir(abs_path)
.arg("describe")
.args(describe_args.clone()),
) {
Ok(version) => version,
Err(_git_err) if fallback.is_some() => fallback.clone().unwrap(),
Err(git_err) => {
// If git error and no fallback provided, return error.
return Err(git_err);
}
};
paths_out.push(rel_path);
versions.push(format!("{}{}{}", prefix, result, suffix))
}

Ok((paths_out, versions))
}
94 changes: 74 additions & 20 deletions git-version-macro/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ use syn::parse::{Parse, ParseStream};
use syn::punctuated::Punctuated;
use syn::token::{Comma, Eq};
use syn::{Expr, Ident, LitStr};

pub(crate) mod describe_submodules;
mod utils;
use self::utils::{describe_cwd, git_dir_cwd};

Expand All @@ -17,8 +17,7 @@ macro_rules! error {
}

fn canonicalize_path(path: &Path) -> syn::Result<String> {
path
.canonicalize()
path.canonicalize()
.map_err(|e| error!("failed to canonicalize {}: {}", path.display(), e))?
.into_os_string()
.into_string()
Expand All @@ -29,12 +28,18 @@ fn canonicalize_path(path: &Path) -> syn::Result<String> {
fn git_dependencies() -> syn::Result<TokenStream2> {
let git_dir = git_dir_cwd().map_err(|e| error!("failed to determine .git directory: {}", e))?;

let deps: Vec<_> = ["logs/HEAD", "index"].iter().flat_map(|&file| {
canonicalize_path(&git_dir.join(file)).map(Some).unwrap_or_else(|e| {
eprintln!("Failed to add dependency on the git state: {}. Git state changes might not trigger a rebuild.", e);
None
let deps: Vec<_> = ["logs/HEAD", "index"]
.iter()
.flat_map(|&file| {
canonicalize_path(&git_dir.join(file)).map(Some).unwrap_or_else(|e| {
eprintln!(
"Failed to add dependency on the git state: {}. Git state changes might not trigger a rebuild.",
e
);
None
})
})
}).collect();
.collect();

Ok(quote! {
#( include_bytes!(#deps); )*
Expand All @@ -55,7 +60,9 @@ impl Parse for Args {
fn parse(input: ParseStream) -> syn::Result<Self> {
let mut result = Args::default();
loop {
if input.is_empty() { break; }
if input.is_empty() {
break;
}
let ident: Ident = input.parse()?;
let _: Eq = input.parse()?;
let check_dup = |dup: bool| {
Expand Down Expand Up @@ -94,7 +101,9 @@ impl Parse for Args {
}
x => Err(error!("Unexpected argument name `{}`", x))?,
}
if input.is_empty() { break; }
if input.is_empty() {
break;
}
let _: Comma = input.parse()?;
}
Ok(result)
Expand Down Expand Up @@ -151,7 +160,7 @@ pub fn git_version(input: TokenStream) -> TokenStream {
fn git_version_impl(args: Args) -> syn::Result<TokenStream2> {
let git_args = args.git_args.map_or_else(
|| vec!["--always".to_string(), "--dirty=-modified".to_string()],
|list| list.iter().map(|x| x.value()).collect()
|list| list.iter().map(|x| x.value()).collect(),
);

let cargo_fallback = args.cargo_prefix.is_some() || args.cargo_suffix.is_some();
Expand All @@ -170,20 +179,65 @@ fn git_version_impl(args: Args) -> syn::Result<TokenStream2> {
if let Ok(version) = std::env::var("CARGO_PKG_VERSION") {
let prefix = args.cargo_prefix.iter();
let suffix = args.cargo_suffix;
Ok(quote!(
concat!(#(#prefix,)* #version, #suffix)
))
Ok(quote!(concat!(#(#prefix,)* #version, #suffix)))
} else if let Some(fallback) = args.fallback {
Ok(fallback.to_token_stream())
} else {
Err(error!("Unable to get git or cargo version"))
}
}
Err(_) if args.fallback.is_some() => {
Ok(args.fallback.to_token_stream())
}
Err(e) => {
Err(error!("{}", e))
}
Err(_) if args.fallback.is_some() => Ok(args.fallback.to_token_stream()),
Err(e) => Err(error!("{}", e)),
}
}

/// Get the git version for submodules below the cargo project.
///
/// This macro will not infer type if there are no submodules in the project.
///
/// This macro expands to `[(&str, &str), N]` where `N` is the total number of
/// submodules below the root of the project (evaluated recursively)
///
/// The format of the array is as follows:
///
/// `[("relative/path/to/submodule", "{prefix}{git_describe_output}{suffix}")]`
///
/// The following (named) arguments can be given:
///
/// - `args`: The arguments to call `git describe` with.
/// Default: `args = ["--always", "--dirty=-modified"]`
///
/// - `prefix`, `suffix`:
/// The git version for each submodule will be prefixed/suffixed
/// by these strings.
///
/// - `fallback`:
/// If all else fails, this string will be given instead of reporting an
/// error. This will yield the same type as if the macro was a success, but
/// format will be `[("relative/path/to/submodule", {fallback})]`
///
/// # Examples
///
/// ```
/// const MODULE_VERSIONS: [(&str, &str), N] = git_version_modules!();
/// ```
///
/// ```
/// const MODULE_VERSIONS: [(&str, &str), N] = git_version_modules!(args = ["--abbrev=40", "--always"]);
/// ```
///
/// ```
/// # use git_version::git_version_modules;
/// const MODULE_VERSIONS: [(&str, &str), N] = git_version_modules!(prefix = "git:", fallback = "unknown");
/// ```
#[proc_macro]
pub fn git_module_versions(input: TokenStream) -> TokenStream {
let args = syn::parse_macro_input!(input as describe_submodules::GitModArgs);

let tokens = match describe_submodules::git_module_versions_impl(args) {
Ok(x) => x,
Err(e) => e.to_compile_error(),
};

TokenStream::from(tokens)
}
12 changes: 7 additions & 5 deletions git-version-macro/src/utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ pub fn git_dir_cwd() -> Result<PathBuf, String> {
Ok(PathBuf::from(path))
}

fn run_git(program: &str, command: &mut std::process::Command) -> Result<String, String> {
pub(crate) fn run_git(program: &str, command: &mut std::process::Command) -> Result<String, String> {
let output = command
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::piped())
Expand All @@ -34,8 +34,8 @@ fn run_git(program: &str, command: &mut std::process::Command) -> Result<String,

let output = collect_output(program, output)?;
let output = strip_trailing_newline(output);
let output = String::from_utf8(output)
.map_err(|_| format!("Failed to parse output of `{}`: output contains invalid UTF-8", program))?;
let output =
String::from_utf8(output).map_err(|_| format!("Failed to parse output of `{}`: output contains invalid UTF-8", program))?;
Ok(output)
}

Expand All @@ -48,7 +48,9 @@ fn collect_output(program: &str, output: std::process::Output) -> Result<Vec<u8>
// If the command terminated with non-zero exit code, return an error.
} else if let Some(status) = output.status.code() {
// Include the first line of stderr in the error message, if it's valid UTF-8 and not empty.
let message = output.stderr.split(|c| *c == b'\n')
let message = output
.stderr
.split(|c| *c == b'\n')
.next()
.and_then(|x| std::str::from_utf8(x).ok())
.filter(|x| !x.is_empty());
Expand Down Expand Up @@ -82,8 +84,8 @@ fn strip_trailing_newline(mut input: Vec<u8>) -> Vec<u8> {

#[test]
fn test_git_dir() {
use std::path::Path;
use assert2::{assert, let_assert};
use std::path::Path;

let_assert!(Ok(git_dir) = git_dir_cwd());
let_assert!(Ok(git_dir) = git_dir.canonicalize());
Expand Down
2 changes: 1 addition & 1 deletion src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
//! These macros do not depend on libgit, but simply uses the `git` binary directly.
//! So you must have `git` installed somewhere in your `PATH`.

pub use git_version_macro::git_version;
pub use git_version_macro::{git_module_versions, git_version};

/// Run `git describe` at compile time with custom flags.
///
Expand Down
Loading

0 comments on commit 14f3942

Please sign in to comment.