libpybuild 0.0.2

Python src+dependency bundler library
Documentation
use std::{fs, path};

use anyhow::{Context, Result};
use log::{debug, error};
use once_cell::sync::Lazy;
use regex::{Regex, RegexBuilder, RegexSet};
use walkdir::{DirEntry, WalkDir};

// Settings for stripping from file transferred
pub(crate) struct Strip {
  pub comments: bool,
  pub whitespace: bool,
}

// Settings for transferring file from `src` -> `dest`
pub(crate) struct DirCopy<'a> {
  pub src: &'a path::Path,
  pub dest: &'a path::Path,
  pub strip: &'a Strip,
}

static RE_COMMENTS: Lazy<Regex> = Lazy::new(|| {
  RegexBuilder::new(r#"^\s*r?['"]{3}[\w\W]*?['"]{3}\n|^\s*#.*\n"#)
    .multi_line(true)
    .unicode(true)
    .build()
    .unwrap()
});

// TODO - get this to work with multi-line or vice-versa and reduce to one pattern
static RE_INLINE_COMMENTS: Lazy<Regex> = Lazy::new(|| RegexBuilder::new(r#"\s*#.*"#).unicode(true).build().unwrap());

static RE_WHITESPACE: Lazy<Regex> = Lazy::new(|| {
  RegexBuilder::new(r#"^s*\n"#)
    .multi_line(true)
    .unicode(true)
    .build()
    .unwrap()
});

// Recursively transfers `--src` to `--dest`
pub(crate) fn copy(dir_cp: &DirCopy, include: &RegexSet, exclude: &RegexSet) {
  // TODO: paralellize this
  // Ref: https://github.com/BurntSushi/walkdir/issues/21
  // Ref: https://github.com/jessegrosjean/jwalk
  // Ref: https://www.reddit.com/r/rust/comments/6eif7r/walkdir_users_we_need_you/
  // Ref: https://github.com/assert-rs/dir-diff/blob/407cbccc77593b0393efeb1e5c8f7199e8138042/src/lib.rs#L60
  //
  for entry in WalkDir::new(dir_cp.src)
    .into_iter()
    .filter_map(|e| e.ok())
    // Skip directories, we're just interested in files/filepaths
    .filter(|e| e.file_type().is_file())
    .filter(|e| keep_entry(e, include, exclude))
  {
    let file = entry.path().to_path_buf();
    match copy_file(file, dir_cp) {
      Ok(_x) => _x,
      Err(e) => error!("\n[ERROR]: {:?}\n", e),
    };
  }
}

// used to filter out filepath entries from copying/processing
fn keep_entry(entry: &DirEntry, include: &RegexSet, exclude: &RegexSet) -> bool {
  let ent = entry.path().to_str().unwrap();
  include.is_match(ent) && !exclude.is_match(ent)
}

// copies file into the the `--out-dir` location
fn copy_file(file: path::PathBuf, dir_cp: &DirCopy) -> Result<()> {
  // Have to take a copy here since the stripping of the prefix modifies in-place
  let src_file = file.clone();

  // 1. Strip `src` from `file` -> "normalizes" so the same directory structure is maintained when copying across
  let path_strip_prefix = dir_cp.src.to_path_buf();
  let normalized_dest_file = file
    .strip_prefix(path_strip_prefix)
    .with_context(|| format!("Failed to strip path {:?}", dir_cp.src))?;

  // Build up new file path
  let dest_file_target = dir_cp.dest.join(normalized_dest_file);

  // Ensure new directory structure exists before attempting to copy
  let target_dir = dest_file_target.parent().unwrap();
  fs::create_dir_all(target_dir)?;

  if dir_cp.strip.whitespace || dir_cp.strip.comments {
    // Copy the file over
    debug!("Copying: {:?}", &dest_file_target);

    let mut file_contents = fs::read_to_string(file)?;
    let stripped_contents = strip(&mut file_contents, dir_cp.strip)?;

    fs::write(&dest_file_target, stripped_contents).with_context(|| {
      format!(
        "Failed to write stripped file {:?} to {:?}",
        &src_file, &dest_file_target
      )
    })?;
  } else {
    // Copy the file over
    debug!("Copying: {:?}", &dest_file_target);

    fs::copy(&src_file, &dest_file_target)
      .with_context(|| format!("Failed to copy {:?} to {:?}", &src_file, &dest_file_target))?;
  }

  Ok(())
}

fn strip(file_contents: &mut str, strip: &Strip) -> Result<String> {
  let mut contents = file_contents.to_string();

  if strip.comments {
    contents = RE_COMMENTS.replace_all(&contents, "").to_string();
    contents = RE_INLINE_COMMENTS.replace_all(&contents, "").to_string();
  }

  if strip.whitespace {
    contents = RE_WHITESPACE.replace_all(&contents, "").to_string();
  }

  Ok(contents)
}