token-goblin 0.1.0

Inline procedural macros without a separate proc-macro crate.
use std::{
    fs::{File, TryLockError},
    path::{Path, PathBuf},
};

use proc_macro2::Span;

use crate::Result;

pub(crate) const OUT_DIR: &str = env!("OUT_DIR");

/// Cache build directory
/// Single common directory for all macros:
/// - This avoid rebuilding of same crates like `syn`, `proc-macro2` for each macro call.
/// - But on the other hand, it prevent parallel build of multiple macros
///
/// Returns:
/// - If `per_project_cache` is `true`, returns `project_dir/build_cache`.
/// - If `per_project_cache` is `false`, returns `OUT_DIR/build_cache`.
pub fn build_dir(project_dir: &Path, per_project_cache: bool) -> PathBuf {
    if per_project_cache {
        return project_dir.join("build_cache");
    }
    PathBuf::from(OUT_DIR).join("build_cache")
}

/// Use `CARGO_MANIFEST_PATH` to get path to Cargo.toml:
/// 1. It might be file in `crate_root()`, or separate file, if custom build system is used.
/// 2. It might be manifest merged with workspace manifest.
/// 3. Or might be missing, if custom build system didn't use Cargo.toml.
pub fn manifest_path() -> Result<PathBuf> {
    let manifest_path = std::env::var("CARGO_MANIFEST_PATH")
        .map_err(|_| error!(Span::call_site() => "CARGO_MANIFEST_PATH is not set"))?;
    let manifest_path = PathBuf::from(&manifest_path);
    Ok(manifest_path)
}

/// Walk from `start` through parent directories looking for `Cargo.toml`.
/// Stops at the first manifest for which `extract` returns `Some`.
pub fn search_for_parent_manifest<U>(
    start: &Path,
    extract: impl Fn(&Path) -> Result<Option<U>>,
) -> Result<Option<U>> {
    for current in start.ancestors() {
        let try_path = current.join("Cargo.toml");
        // If file doesn't exist it is not an error, try parent folder
        if !try_path.exists() {
            continue;
        }
        let result = extract(&try_path)?;
        if let Some(value) = result {
            return Ok(Some(value));
        }
    }

    Ok(None)
}

/// Use source span to request path of macro definition.
///
/// Returns:
/// - Path to generated crate
/// - Whether this path is local (not remapped, generated, etc)
///
/// Format of path is:
/// `{OUT_DIR}/generated/{crate_name}_{crate_version}/{path_to_macro_definition}_{fn_name}_{line}_{column}`
pub fn calculate_generated_path(ident: proc_macro2::Span) -> (PathBuf, bool) {
    let span: proc_macro::Span = ident.unwrap();

    let mut stable = true;
    let file = span.local_file().map_or_else(
        || {
            stable = false;
            span.file()
        },
        |v| v.display().to_string(),
    );

    let crate_name = std::env::var("CARGO_PKG_NAME").unwrap_or_else(|_| {
        stable = false;
        "unknown".to_string()
    });
    let crate_version = std::env::var("CARGO_PKG_VERSION").unwrap_or_else(|_| {
        stable = false;
        "unknown".to_string()
    });
    let file = sanitize_path(&file);
    let line = span.line();
    let column = span.column();
    let path = format!("{OUT_DIR}/generated/{crate_name}_{crate_version}/{file}_{line}_{column}");
    (PathBuf::from(path), stable)
}

fn sanitize_path(path: &str) -> String {
    path.replace(['\\', '/'], "_")
}

/// Lock file guard.
/// Used to prevent concurrent pipeline running for the same crate.
///
/// E.g.
/// During macro expansion, we frist trying to create template,
/// then compile crate to dylib, and copy output to `OUT_DIR`.
/// To prevent race condition, we use lock file.
#[derive(Debug)]
pub struct FsLockGuard {
    path: PathBuf,
    file: File,
}

impl FsLockGuard {
    /// Create lock file and lock it.
    /// If lock file is already locked, wait for it to be unlocked.
    /// Returns:
    /// - Lock guard
    /// - Error if failed to create lock file or lock it.
    pub fn new(path: PathBuf) -> Result<Self> {
        let file = std::fs::create_dir_all(path.parent().unwrap())
            .and_then(|()| File::create(&path))
            .map_err(|e| error!(Span::call_site() => "Failed to create lock file: {e}"))?;

        let this = Self { path, file };
        this.lock()?;
        Ok(this)
    }
    fn lock(&self) -> Result<()> {
        match self.file.try_lock() {
            Err(TryLockError::WouldBlock) => {
                debug!("Waiting for lock file: {}", self.path.display());
                self.file.lock()
            }
            Err(TryLockError::Error(e)) => Err(e),
            Ok(()) => Ok(()),
        }
        .map_err(|e| error!(Span::call_site() => "Failed to lock lock file: {e}"))
    }
    fn unlock(&self) -> Result<()> {
        self.file
            .unlock()
            .map_err(|e| error!(Span::call_site() => "Failed to unlock lock file: {e}"))
    }
}

impl Drop for FsLockGuard {
    fn drop(&mut self) {
        self.unlock().unwrap();
    }
}