Skip to content

Commit

Permalink
Add proc-macros and clean up code
Browse files Browse the repository at this point in the history
  • Loading branch information
Juici committed Feb 10, 2024
1 parent 18d1b25 commit 72d4b89
Show file tree
Hide file tree
Showing 17 changed files with 550 additions and 263 deletions.
3 changes: 2 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ jobs:
strategy:
fail-fast: false
matrix:
rust: [nightly, beta, stable, 1.64.0]
rust: [nightly, beta, stable, 1.71.0]
timeout-minutes: 45
steps:
- uses: actions/checkout@v4
Expand All @@ -66,6 +66,7 @@ jobs:
run: echo RUSTFLAGS=${RUSTFLAGS}\ --cfg=pkg_nightly_testing >> $GITHUB_ENV
if: matrix.rust == 'nightly'
- run: cargo test --workspace
- run: cargo check --no-default-features

minimal:
name: Minimal versions
Expand Down
41 changes: 29 additions & 12 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,23 +1,40 @@
[package]
name = "pkg"
version = "3.0.0"
authors = ["James Whaley <[email protected]>"]
version.workspace = true
authors.workspace = true
description = "Utility macros and functions for crates."
edition = "2021"
license = "MIT OR Apache-2.0"
edition.workspace = true
license.workspace = true
rust-version.workspace = true

documentation = "https://docs.rs/pkg"
repository = "https://github.com/Juici/pkg-rs"
repository.workspace = true

keywords = ["cargo", "package"]
categories = ["development-tools"]

[features]
default = ["std", "proc-macros", "git"]
std = []
proc-macros = ["pkg-macros"]
git = ["proc-macros", "pkg-macros?/git"]

[dependencies]
once_cell = { version = "1", optional = true }
pkg-macros = { version = "=4.0.0", path = "macros", optional = true }

[features]
default = ["semver", "rt_bin_name"]
[workspace]
members = ["macros"]
resolver = "2"

std = []
semver = []
rt_bin_name = ["once_cell", "std"]
[workspace.package]
version = "4.0.0"
authors = ["James Whaley <[email protected]>"]
edition = "2021"
license = "MIT OR Apache-2.0"
rust-version = "1.71"

repository = "https://github.com/Juici/pkg-rs"

[package.metadata.docs.rs]
all-features = true
targets = ["x86_64-unknown-linux-gnu"]
rustdoc-args = ["--cfg", "doc_cfg", "--generate-link-to-definition"]
40 changes: 8 additions & 32 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,42 +1,18 @@
# derive(IntEnum)
# pkg

[<img alt="github" src="https://img.shields.io/badge/github-juici/int--enum--rs-8da0cb?style=for-the-badge&logo=github" height="20">](https://github.com/Juici/int-enum-rs)
[<img alt="crates.io" src="https://img.shields.io/crates/v/int-enum?style=for-the-badge&color=fc8d62&logo=rust" height="20">](https://crates.io/crates/int-enum)
[<img alt="docs.rs" src="https://img.shields.io/badge/docs.rs-int--enum-4d76ae?style=for-the-badge&logo=docs.rs" height="20">](https://docs.rs/int-enum)
[<img alt="build status" src="https://img.shields.io/github/actions/workflow/status/Juici/int-enum-rs/ci.yml?branch=master&style=for-the-badge" height="20">](https://github.com/Juici/int-enum-rs/actions?query=branch%3Amaster)
[<img alt="github" src="https://img.shields.io/badge/github-juici/pkg--rs-8da0cb?style=for-the-badge&logo=github" height="20">](https://github.com/Juici/pkg-rs)
[<img alt="crates.io" src="https://img.shields.io/crates/v/pkg?style=for-the-badge&color=fc8d62&logo=rust" height="20">](https://crates.io/crates/pkg)
[<img alt="docs.rs" src="https://img.shields.io/badge/docs.rs-pkg-4d76ae?style=for-the-badge&logo=docs.rs" height="20">](https://docs.rs/pkg)
[<img alt="build status" src="https://img.shields.io/github/actions/workflow/status/Juici/pkg-rs/ci.yml?branch=master&style=for-the-badge" height="20">](https://github.com/Juici/pkg-rs/actions?query=branch%3Amaster)

This crate provides a convenient derive macro for the core library's [`From`]
and [`TryFrom`] traits for converting between integer and enum types.

[`From`]: https://doc.rust-lang.org/core/convert/trait.From.html
[`TryFrom`]: https://doc.rust-lang.org/core/convert/trait.TryFrom.html
This library provides utilities for getting build information.

```toml
[dependencies]
int-enum = "1.0"
pkg = "4.0"
```

_Compiler support: requires rustc 1.64+_

## Example

```rs
use int_enum::IntEnum;

#[repr(u8)]
#[derive(Debug, PartialEq, IntEnum)]
pub enum Ascii {
UpperA = b'A',
UpperB = b'B',
}

assert_eq!(u8::from(Ascii::UpperA), b'A');
assert_eq!(u8::from(Ascii::UpperB), b'B');

assert_eq!(Ascii::try_from(b'A'), Ok(Ascii::UpperA));
assert_eq!(Ascii::try_from(b'B'), Ok(Ascii::UpperB));
assert_eq!(Ascii::try_from(b'C'), Err(b'C'));
```
_Compiler support: requires rustc 1.71+_

## License

Expand Down
31 changes: 31 additions & 0 deletions macros/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
[package]
name = "pkg-macros"
version.workspace = true
authors.workspace = true
description = "Procedural macros for the `pkg` crate."
edition.workspace = true
license.workspace = true
rust-version.workspace = true

repository.workspace = true

[lib]
proc-macro = true

[features]
default = ["git"]
git = ["gix"]

[dependencies]
proc-macro2 = "1.0.78"
quote = "1.0.35"
syn = { version = "2.0.48", default-features = false }

thiserror = "1.0.56"

gix = { version = "0.58.0", default-features = false, optional = true }

[package.metadata.docs.rs]
all-features = true
targets = ["x86_64-unknown-linux-gnu"]
rustdoc-args = ["--cfg", "doc_cfg", "--generate-link-to-definition"]
1 change: 1 addition & 0 deletions macros/LICENSE-APACHE
1 change: 1 addition & 0 deletions macros/LICENSE-MIT
76 changes: 76 additions & 0 deletions macros/src/build_time.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
use std::ffi::{OsStr, OsString};
use std::fmt::Display;
use std::num::ParseIntError;
use std::sync::OnceLock;
use std::time::{SystemTime, UNIX_EPOCH};
use std::{env, fmt};

use proc_macro2::{Span, TokenStream};
use quote::quote;
use thiserror::Error;

pub fn build_time() -> syn::Result<TokenStream> {
static BUILD_TIME: OnceLock<Result<i64, BuildTimeError>> = OnceLock::new();

let timestamp = BUILD_TIME
.get_or_init(|| match get_reproducible_time() {
Ok(Some(timestamp)) => Ok(timestamp),
Ok(None) => get_system_time(),
Err(err) => Err(BuildTimeError::SourceDate(err)),
})
.as_ref()
.copied()
.map_err(|err| syn::Error::new(Span::call_site(), err))?;

Ok(quote! { #timestamp })
}

// https://reproducible-builds.org/docs/source-date-epoch/
fn get_reproducible_time() -> Result<Option<i64>, SourceDateError> {
match env::var_os("SOURCE_DATE_EPOCH") {
Some(timestamp) => Ok(Some(timestamp.into_string()?.parse::<i64>()?)),
None => Ok(None),
}
}

fn get_system_time() -> Result<i64, BuildTimeError> {
let now = SystemTime::now();

let timestamp = match now.duration_since(UNIX_EPOCH) {
Ok(since) => i128::from(since.as_secs()),
Err(err) => -i128::from(err.duration().as_secs()),
};

i64::try_from(timestamp).map_err(|_| BuildTimeError::Overflow(timestamp))
}

#[derive(Debug, Error)]
enum BuildTimeError {
#[error("system clock overflows unix timestamp: {0}")]
Overflow(i128),
#[error(transparent)]
SourceDate(#[from] SourceDateError),
}

#[derive(Debug, Error)]
enum SourceDateError {
#[error("invalid unicode in $SOURCE_DATE_EPOCH")]
NotUnicode(OsString),
#[error("failed to parse $SOURCE_DATE_EPOCH timestamp")]
Parse(#[from] ParseIntError),
}

impl From<OsString> for SourceDateError {
fn from(s: OsString) -> Self {
Self::NotUnicode(s)
}
}

struct InvalidUtf8<'a>(&'a OsStr);

impl Display for InvalidUtf8<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
// Hack to make use of unstable Utf8Chunks to output lossy string.
std::path::Path::new(&self.0).display().fmt(f)
}
}
42 changes: 42 additions & 0 deletions macros/src/git.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
use std::env;
use std::error::Error;
use std::path::PathBuf;

use gix::Repository;
use proc_macro2::TokenStream;
use quote::quote;
use thiserror::Error;

fn get_manifest_dir() -> Result<PathBuf, GitError> {
env::var_os("CARGO_MANIFEST_DIR").map(PathBuf::from).ok_or(GitError::MissingManifestDir)
}

fn get_repo() -> Result<Repository, GitError> {
let manifest_dir = get_manifest_dir()?;
let repo = gix::discover(manifest_dir).map_err(GitError::boxed)?;

Ok(repo)
}

pub fn commit_hash() -> Result<TokenStream, GitError> {
let repo = get_repo()?;
let head_id = repo.head_id().map_err(GitError::boxed)?;

let commit_hash = head_id.to_hex().to_string();

Ok(quote!(#commit_hash))
}

#[derive(Debug, Error)]
pub enum GitError {
#[error("missing CARGO_MANIFEST_DIR environment variable")]
MissingManifestDir,
#[error(transparent)]
Boxed(#[from] Box<dyn Error>),
}

impl GitError {
fn boxed<E: Error + 'static>(err: E) -> Self {
Self::Boxed(Box::new(err))
}
}
29 changes: 29 additions & 0 deletions macros/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
#![cfg_attr(doc_cfg, feature(doc_cfg))]
#![allow(clippy::module_name_repetitions)]

extern crate proc_macro;

use crate::traits::MacroResult;

#[macro_use]
mod macros;
mod build_time;
mod traits;

#[proc_macro]
pub fn build_time(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
parse_nothing!(input);

build_time::build_time().macro_result()
}

cfg_git! {
mod git;

#[proc_macro]
pub fn git_commit_hash(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
parse_nothing!(input);

git::commit_hash().macro_result()
}
}
23 changes: 23 additions & 0 deletions macros/src/macros.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
#![allow(unused_macros)]

macro_rules! cfg_git {
($($item:item)*) => {
$(
#[cfg(feature = "git")]
#[cfg_attr(doc_cfg, doc(cfg(feature = "git")))]
$item
)*
};
}

macro_rules! parse_nothing {
($input:expr) => {{
let input = ::proc_macro2::TokenStream::from($input);

if let Some(token) = input.into_iter().next() {
let err = ::syn::Error::new(token.span(), "unexpected token");

return err.into_compile_error().into();
}
}};
}
36 changes: 36 additions & 0 deletions macros/src/traits.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
use std::any::Any;
use std::fmt::Display;

use proc_macro2::{Span, TokenStream};

pub trait MacroResult {
fn macro_result(self) -> proc_macro::TokenStream;
}

impl<E> MacroResult for Result<TokenStream, E>
where
E: Display + 'static,
{
fn macro_result(self) -> proc_macro::TokenStream {
let result = match self {
Ok(tokens) => tokens,
Err(err) => match <dyn Any>::downcast_ref::<syn::Error>(&err) {
Some(err) => err.to_compile_error(),
None => syn::Error::new(Span::call_site(), err).to_compile_error(),
},
};
result.into()
}
}

impl MacroResult for TokenStream {
fn macro_result(self) -> proc_macro::TokenStream {
self.into()
}
}

impl MacroResult for proc_macro::TokenStream {
fn macro_result(self) -> proc_macro::TokenStream {
self
}
}
Loading

0 comments on commit 72d4b89

Please sign in to comment.