use anyhow::{anyhow, Context, Error};
use cargo_metadata::MetadataCommand;
use chrono::{Date, Datelike, Utc};
use fehler::{throw, throws};
use log::info;
use sha2::Digest;
use std::ffi::OsString;
use std::fs;
use std::io::Write;
use std::path::{Path, PathBuf};
use std::process::{Command, ExitStatus};
use zip::ZipWriter;
pub static DEFAULT_REPO: &str = "https://github.com/softprops/lambda-rust";
pub static DEFAULT_REV: &str = "master";
pub static DEFAULT_CONTAINER_CMD: &str = "docker";
fn cmd_str(cmd: &Command) -> String {
format!("{:?}", cmd).replace('"', "")
}
#[throws]
fn run_cmd_no_check(cmd: &mut Command) -> ExitStatus {
let cmd_str = cmd_str(cmd);
info!("{}", cmd_str);
cmd.status().context(format!("failed to run {}", cmd_str))?
}
#[throws]
fn run_cmd(cmd: &mut Command) {
let cmd_str = cmd_str(cmd);
let status = run_cmd_no_check(cmd)?;
if !status.success() {
throw!(anyhow!("command {} failed: {}", cmd_str, status));
}
}
fn git_cmd_in(repo_path: &Path) -> Command {
let mut cmd = Command::new("git");
cmd.arg("-C").arg(repo_path);
cmd
}
#[throws]
fn ensure_dir_exists(path: &Path) {
let _ = fs::create_dir(path);
if !path.is_dir() {
throw!(anyhow!("failed to create directory {}", path.display()));
}
}
#[throws]
fn get_package_binaries(path: &Path) -> Vec<String> {
let metadata = MetadataCommand::new().current_dir(path).no_deps().exec()?;
let mut names = Vec::new();
for package in metadata.packages {
for target in package.targets {
if target.kind.contains(&"bin".to_string()) {
names.push(target.name);
}
}
}
names
}
fn make_zip_name(name: &str, contents: &[u8], when: Date<Utc>) -> String {
let hash = sha2::Sha256::digest(&contents);
format!(
"{}-{}{:02}{:02}-{:.16x}.zip",
name,
when.year(),
when.month(),
when.day(),
hash
)
}
#[derive(Debug, Clone, Eq, PartialEq)]
pub struct LambdaBuilder {
pub repo: String,
pub rev: String,
pub container_cmd: String,
pub project: PathBuf,
}
impl Default for LambdaBuilder {
fn default() -> Self {
LambdaBuilder {
repo: DEFAULT_REPO.into(),
rev: DEFAULT_REV.into(),
container_cmd: DEFAULT_CONTAINER_CMD.into(),
project: PathBuf::default(),
}
}
}
impl LambdaBuilder {
#[throws]
pub fn run(&self) -> Vec<PathBuf> {
let project_path = self.project.canonicalize().context(format!(
"failed to canonicalize {}",
self.project.display(),
))?;
ensure_dir_exists(&project_path.join("target"))?;
let output_dir = project_path.join("lambda-target");
ensure_dir_exists(&output_dir)?;
let repo_url = &self.repo;
let repo_path = output_dir.join("lambda-rust");
ensure_dir_exists(&repo_path)?;
if !repo_path.join(".git").exists() {
run_cmd(
Command::new("git")
.args(&["clone", repo_url])
.arg(&repo_path),
)?;
} else {
run_cmd(
git_cmd_in(&repo_path)
.args(&["remote", "set-url", "origin"])
.arg(repo_url),
)?;
run_cmd(git_cmd_in(&repo_path).arg("fetch"))?;
};
let status = run_cmd_no_check(
git_cmd_in(&repo_path)
.args(&["checkout", &format!("origin/{}", self.rev)]),
)?;
if !status.success() {
run_cmd(git_cmd_in(&repo_path).args(&["checkout", &self.rev]))?;
}
let image_tag = "rust-lambda-build";
run_cmd(
Command::new(&self.container_cmd)
.current_dir(&repo_path)
.args(&["build", "--tag", image_tag, "."]),
)?;
let volume = |src: &Path, dst: &Path| {
let mut s = OsString::new();
s.push(src);
s.push(":");
s.push(dst);
s
};
let volume_read_only = |src, dst| {
let mut s = volume(src, dst);
s.push(":ro");
s
};
let registry_dir = output_dir.join("cargo-registry");
ensure_dir_exists(®istry_dir)?;
let git_dir = output_dir.join("cargo-git");
ensure_dir_exists(&git_dir)?;
run_cmd(
Command::new(&self.container_cmd)
.args(&["run", "--rm", "--init"])
.arg("-u")
.arg(format!(
"{}:{}",
users::get_current_uid(),
users::get_current_gid()
))
.arg("-v")
.arg(volume_read_only(&project_path, Path::new("/code")))
.arg("-v")
.arg(volume(®istry_dir, Path::new("/cargo/registry")))
.arg("-v")
.arg(volume(&git_dir, Path::new("/cargo/git")))
.arg("-v")
.arg(volume(&output_dir, Path::new("/code/target")))
.arg(image_tag),
)?;
let binaries = get_package_binaries(&project_path)?;
let mut zip_names = Vec::new();
let mut zip_paths = Vec::new();
for name in binaries {
let src = output_dir.join("lambda/release").join(&name);
let contents = fs::read(&src)
.context(format!("failed to read {}", src.display()))?;
let dst_name = make_zip_name(&name, &contents, Utc::now().date());
let dst = output_dir.join(&dst_name);
zip_names.push(dst_name);
zip_paths.push(dst.clone());
info!("writing {}", dst.display());
let file = fs::File::create(&dst)
.context(format!("failed to create {}", dst.display()))?;
let mut zip = ZipWriter::new(file);
let options = zip::write::FileOptions::default()
.compression_method(zip::CompressionMethod::Deflated);
zip.start_file("bootstrap", options)?;
zip.write_all(&contents)?;
zip.finish()?;
}
let latest_path = output_dir.join("latest");
info!("writing {}", latest_path.display());
fs::write(latest_path, zip_names.join("\n") + "\n")?;
zip_paths
}
}
#[cfg(test)]
mod tests {
use super::*;
use chrono::TimeZone;
#[test]
fn test_zip_name() {
let when = Utc.ymd(2020, 8, 31);
assert_eq!(
make_zip_name("testexecutable", "testcontents".as_bytes(), when),
"testexecutable-20200831-7097a82a108e78da.zip"
);
}
}