fslbldutils 0.1.1

Functions for injecting fossil repository metadata into build.
Documentation
//! Build script utility functions for passing repository-related metadata into
//! build environment using environment and cfg variables.

use std::path::PathBuf;

use hashbrown::HashSet;


/// Metadata returned to build script.
#[derive(Debug)]
#[non_exhaustive]
pub struct Status {
  /// Set to the repository project name.
  pub prjname: String,

  /// Set to the path of the checkout root.
  pub local_root: PathBuf,

  /// Set to the last commit hash in the checkout.
  pub checkout_hash: String,

  /// Set to the last commit timestamp in the checkout.
  pub checkout_ts: String,

  /// A set of tags for current checkout commit.
  pub tags: HashSet<String>,

  /// `true` if there are uncommitted changes in the checkout.  `false`
  /// otherwise.
  pub uncommitted: bool,

  /// `true` if there are untracked files in the checkout.  `false` otherwise.
  pub untracked: bool,

  /// Set to `Some(_)` if this is a tagged build, where the variant data is
  /// the tag name that was matched.
  pub tagged: Option<String>
}

/// Builder for generating environment/configuration variables.
#[derive(Default)]
pub struct Builder {
  lenient_clean: bool,

  #[allow(clippy::type_complexity)]
  match_tag: Option<Box<dyn Fn(&str) -> bool>>
}

impl Builder {
  /// Categorize as a "clean" build only by checking for uncommitted changes.
  ///
  /// By default a clean build requires both no uncommitted changes as well as
  /// no untracked files.
  #[must_use]
  pub const fn lenient_clean(mut self) -> Self {
    self.lenient_clean = true;
    self
  }

  /// Register a closure to be called to determine if a "tagged build" tag
  /// is present.
  #[must_use]
  pub fn detect_tagged<F>(mut self, f: F) -> Self
  where
    F: Fn(&str) -> bool + 'static
  {
    self.match_tag = Some(Box::new(f));
    self
  }

  /// Set up build environment/cfg variables.
  ///
  /// # Environment variables
  /// - `FSL_PRJCODE` - The repository project code.
  /// - `FSL_PRJNAME` - The repository project name.
  /// - `FSL_CHECKOUT_HASH` - The checkout's last commit hash.
  /// - `FSL_CHECKOUT_TS` - The checkout's last commit timestamp.
  /// - `FSL_CHECKOUT` - Set to `dirty` if the worktree contains uncommitted
  ///   changes or untracked files.  Set to `clean` otherwise.
  /// - `FSL_TAGGED` - Set to the tag name if this is a "tagged build".  Not
  ///   set if this isn't a tagged build.
  ///
  /// # cfg variables
  /// - `fsl_tagged`<br/>Only set if this is a "tagged build".
  /// - `fsl_checkout` = `"clean"` | `"dirty"`
  ///
  /// # Panics
  /// Because this is intended to be used from a build script, all errors are
  /// turned into panics.
  #[must_use]
  pub fn build(self) -> Status {
    let st = fslutils::status(None).unwrap();

    // Register cfg keywords
    println!(
      "cargo::rustc-check-cfg=cfg(fsl_checkout, values(\"dirty\", \"clean\"))"
    );
    println!("cargo::rustc-check-cfg=cfg(fsl_tagged)");

    // FSL_PRJNAME
    make_env_var("prjname", &st.prjname);

    // FSL_PRJCODE
    make_env_var("prjcode", &st.prjcode);

    // FSL_CHECKOUT_HASH
    make_env_var("checkout-hash", &st.checkout_hash);

    // FSL_CHECKOUT_TS
    make_env_var("checkout-ts", &st.checkout_ts);

    // FSL_CHECKOUT = "clean" | "dirty"
    let checkout_state = if self.lenient_clean {
      if st.uncommitted { "dirty" } else { "clean" }
    } else if st.uncommitted || st.untracked {
      "dirty"
    } else {
      "clean"
    };
    make_env_var("checkout", checkout_state);
    make_cfg_var("checkout", Some(checkout_state));

    // Determine if this is a "tagged" build.
    // FSL_TAGGED=<tag>
    let mut tagged = None;
    if let Some(match_tag) = self.match_tag {
      for tag in &st.tags {
        if match_tag(tag) {
          make_env_var("tagged", tag);
          make_cfg_var("tagged", None);
          tagged = Some(tag.clone());
          break;
        }
      }
    }

    Status {
      prjname: st.prjname,
      local_root: st.local_root,
      checkout_hash: st.checkout_hash,
      checkout_ts: st.checkout_ts,
      tags: st.tags,
      uncommitted: st.uncommitted,
      untracked: st.untracked,
      tagged
    }
  }
}

fn make_env_var(key: &str, val: &str) {
  let key = key.replace('-', "_").to_uppercase();
  println!("cargo:rustc-env=FSL_{key}={val}");
}

fn make_cfg_var(key: &str, val: Option<&str>) {
  let lkey = key.replace('-', "_").to_lowercase();
  if let Some(val) = val {
    println!(r#"cargo:rustc-cfg=fsl_{lkey}="{val}""#);
  } else {
    println!(r"cargo:rustc-cfg=fsl_{lkey}");
  }
}

// vim: set ft=rust et sw=2 ts=2 sts=2 cinoptions=2 tw=79 :