use std::{
fs,
path::{Path, PathBuf},
};
use anyhow::{Context, Result};
use clap::Parser;
use miniserde::{Deserialize, Serialize};
use regex::{RegexSet, RegexSetBuilder};
use super::{transfer, zip};
static WORKING_DIR: &str = ".pybuild";
#[derive(Parser, Debug, Serialize, Deserialize)]
pub struct BundleOptions {
#[clap(short = 'x', long)]
pub exclude: Option<String>,
#[clap(short, long)]
pub include: Option<String>,
#[clap(long)]
pub lambda_layer: bool,
#[clap(short, long)]
pub minify: bool,
#[clap(long, default_value = WORKING_DIR)]
pub out_dir: String,
#[clap(long)]
pub out_file: Option<String>,
#[clap(short, long)]
pub packages: Option<String>,
#[clap(short, long, default_value = ".")]
pub src: String,
#[clap(long)]
pub strip_comments: bool,
#[clap(long)]
pub strip_whitespace: bool,
#[clap(short, long)]
pub zip: bool,
}
impl BundleOptions {
pub fn new() -> Self {
BundleOptions {
exclude: None,
include: None,
lambda_layer: false,
minify: false,
out_dir: WORKING_DIR.to_string(),
out_file: None,
packages: None,
src: ".".to_string(),
strip_comments: false,
strip_whitespace: false,
zip: false,
}
}
}
impl Default for BundleOptions {
fn default() -> Self {
Self::new()
}
}
pub trait Bundle {
fn execute(&self) -> Result<()>;
}
impl Bundle for BundleOptions {
fn execute(&self) -> Result<()> {
let artifact_dir = create_artifact_dir(&self.out_dir, self.lambda_layer)?;
let include = &get_regex_set(&self.include, PatternType::Include)?;
let exclude = &get_regex_set(&self.exclude, PatternType::Exclude)?;
let strip = transfer::Strip {
comments: self.minify || self.strip_comments,
whitespace: self.minify || self.strip_whitespace,
};
let src = Path::new(&self.src);
copy_source(src, &artifact_dir, &strip, include, exclude)?;
copy_packages(&self.packages, &artifact_dir, &strip, include, exclude)?;
if self.zip || self.lambda_layer {
let prefix = &src.to_string_lossy();
let ofile = match &self.out_file {
Some(out_file) => PathBuf::from(out_file),
None => {
let filename = format!("{}.zip", &self.out_dir);
PathBuf::from(filename)
}
};
zip::zip_dir(&artifact_dir, &ofile, prefix).unwrap();
}
Ok(())
}
}
fn create_artifact_dir(artifact_dir: &str, is_lambda_layer: bool) -> Result<PathBuf> {
let dir = Path::new(artifact_dir);
let dist_dir = dir.join("dist");
if dist_dir.exists() {
fs::remove_dir_all(&dist_dir).unwrap();
}
if !dist_dir.exists() {
fs::create_dir_all(&dist_dir).with_context(|| format!("Failed to create directory {:?}", dist_dir))?;
}
let mut canonical_dir =
fs::canonicalize(&dist_dir).with_context(|| format!("Failed to canonicalize path: {:?}", dist_dir))?;
if is_lambda_layer {
canonical_dir.push("python");
}
fs::create_dir_all(&canonical_dir).with_context(|| format!("Failed to create directory {:?}", canonical_dir))?;
Ok(canonical_dir)
}
enum PatternType {
Include,
Exclude,
}
fn get_regex_set(pattern: &Option<String>, pattern_type: PatternType) -> Result<RegexSet> {
let mut patterns: Vec<String> = vec![];
match pattern_type {
PatternType::Include => patterns.push(r#"(.py)$"#.into()),
PatternType::Exclude => patterns.push(r#".git|test|__pycache__|dist-info|example|node_modules"#.into()),
}
if let Some(pattern) = pattern {
patterns.push(pattern.into());
};
RegexSetBuilder::new(patterns)
.build()
.with_context(|| format!("Failed to parse regex pattern {:?}", pattern))
}
fn copy_source(src: &Path, dest: &Path, strip: &transfer::Strip, include: &RegexSet, exclude: &RegexSet) -> Result<()> {
for entry in fs::read_dir(src)? {
let entry = entry?;
let path = entry.path();
if !path.ends_with(WORKING_DIR) {
let src_copy = transfer::DirCopy {
src: &path,
dest,
strip,
};
transfer::copy(&src_copy, include, exclude);
}
}
Ok(())
}
fn copy_packages(
packages: &Option<String>,
dest: &Path,
strip: &transfer::Strip,
include: &RegexSet,
exclude: &RegexSet,
) -> Result<()> {
let target_file = Path::new(WORKING_DIR).join(".target");
let packages_path = match packages {
Some(p) => Some(Path::new(p).to_path_buf()),
None => {
if target_file.exists() {
let contents = fs::read_to_string(target_file)?;
Some(Path::new(&contents).to_path_buf())
} else {
None
}
}
};
if let Some(pkgs_path) = packages_path {
let pkgs_copy = transfer::DirCopy {
src: pkgs_path.as_path(),
dest,
strip,
};
transfer::copy(&pkgs_copy, include, exclude);
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn out_dir_exists() {
let dir = "something".to_string();
let artifact_dir = create_artifact_dir(&dir, false).unwrap();
assert!(artifact_dir.exists());
fs::remove_dir_all(dir).unwrap();
}
}