1mod command;
2mod docker;
3mod git;
4
5use anyhow::{anyhow, Context, Error};
6use cargo_metadata::MetadataCommand;
7use chrono::{Date, Datelike, Utc};
8use docker::{Docker, Volume};
9use fehler::{throw, throws};
10use git::Repo;
11use log::info;
12use sha2::Digest;
13use std::fs;
14use std::io::Write;
15use std::path::{Path, PathBuf};
16use zip::ZipWriter;
17
18pub static DEFAULT_REPO: &str = "https://github.com/softprops/lambda-rust";
20pub static DEFAULT_REV: &str = "master";
23pub static DEFAULT_CONTAINER_CMD: &str = "docker";
25
26#[throws]
28fn ensure_dir_exists(path: &Path) {
29 let _ = fs::create_dir(path);
31 if !path.is_dir() {
32 throw!(anyhow!("failed to create directory {}", path.display()));
33 }
34}
35
36#[throws]
38fn get_package_binaries(path: &Path) -> Vec<String> {
39 let metadata = MetadataCommand::new().current_dir(path).no_deps().exec()?;
40 let mut names = Vec::new();
41 for package in metadata.packages {
42 for target in package.targets {
43 if target.kind.contains(&"bin".to_string()) {
44 names.push(target.name);
45 }
46 }
47 }
48 names
49}
50
51fn make_zip_name(name: &str, contents: &[u8], when: Date<Utc>) -> String {
59 let hash = sha2::Sha256::digest(&contents);
60 format!(
61 "{}-{}{:02}{:02}-{:.16x}.zip",
62 name,
63 when.year(),
64 when.month(),
65 when.day(),
66 hash
69 )
70}
71
72#[derive(Debug, Clone, Eq, PartialEq)]
74pub struct LambdaBuilder {
75 pub repo: String,
77
78 pub rev: String,
80
81 pub container_cmd: String,
84
85 pub project: PathBuf,
87}
88
89impl Default for LambdaBuilder {
90 fn default() -> Self {
91 LambdaBuilder {
92 repo: DEFAULT_REPO.into(),
93 rev: DEFAULT_REV.into(),
94 container_cmd: DEFAULT_CONTAINER_CMD.into(),
95 project: PathBuf::default(),
96 }
97 }
98}
99
100impl LambdaBuilder {
101 #[throws]
110 pub fn run(&self) -> Vec<PathBuf> {
111 let project_path = self.project.canonicalize().context(format!(
114 "failed to canonicalize {}",
115 self.project.display(),
116 ))?;
117
118 ensure_dir_exists(&project_path.join("target"))?;
125
126 let output_dir = project_path.join("lambda-target");
128 ensure_dir_exists(&output_dir)?;
129
130 let repo_url = &self.repo;
131 let repo = Repo::new(output_dir.join("lambda-rust"));
132 ensure_dir_exists(&repo.path)?;
133
134 if !repo.path.join(".git").exists() {
135 repo.clone(repo_url)?;
137 } else {
138 repo.remote_set_url(repo_url)?;
140 repo.fetch()?;
142 };
143
144 repo.checkout(&self.rev)?;
146
147 let image_tag = format!("lambda-build-{:.16}", repo.rev_parse("HEAD")?);
149 let docker = Docker::new(self.container_cmd.clone());
150 docker.build(&repo.path, &image_tag)?;
151
152 let registry_dir = output_dir.join("cargo-registry");
156 ensure_dir_exists(®istry_dir)?;
157 let git_dir = output_dir.join("cargo-git");
158 ensure_dir_exists(&git_dir)?;
159
160 docker.run(
162 &[
163 Volume {
165 src: project_path.clone(),
166 dst: Path::new("/code").into(),
167 read_only: true,
168 },
169 Volume {
171 src: registry_dir,
172 dst: Path::new("/cargo/registry").into(),
173 read_only: false,
174 },
175 Volume {
176 src: git_dir,
177 dst: Path::new("/cargo/git").into(),
178 read_only: false,
179 },
180 Volume {
182 src: output_dir.clone(),
183 dst: Path::new("/code/target").into(),
184 read_only: false,
185 },
186 ],
187 &image_tag,
188 )?;
189
190 let binaries = get_package_binaries(&project_path)?;
192
193 let mut zip_names = Vec::new();
200 let mut zip_paths = Vec::new();
201 for name in binaries {
202 let src = output_dir.join("lambda/release").join(&name);
203 let contents = fs::read(&src)
204 .context(format!("failed to read {}", src.display()))?;
205 let dst_name = make_zip_name(&name, &contents, Utc::now().date());
206 let dst = output_dir.join(&dst_name);
207 zip_names.push(dst_name);
208 zip_paths.push(dst.clone());
209
210 info!("writing {}", dst.display());
213 let file = fs::File::create(&dst)
214 .context(format!("failed to create {}", dst.display()))?;
215 let mut zip = ZipWriter::new(file);
216 let options = zip::write::FileOptions::default()
217 .compression_method(zip::CompressionMethod::Deflated);
218 zip.start_file("bootstrap", options)?;
219 zip.write_all(&contents)?;
220
221 zip.finish()?;
222 }
223
224 let latest_path = output_dir.join("latest");
225 info!("writing {}", latest_path.display());
226 fs::write(latest_path, zip_names.join("\n") + "\n")?;
227
228 zip_paths
229 }
230}
231
232#[cfg(test)]
233mod tests {
234 use super::*;
235 use chrono::TimeZone;
236
237 #[test]
238 fn test_zip_name() {
239 let when = Utc.ymd(2020, 8, 31);
240 assert_eq!(
241 make_zip_name("testexecutable", "testcontents".as_bytes(), when),
242 "testexecutable-20200831-7097a82a108e78da.zip"
243 );
244 }
245}