1use std::env;
2use std::fs;
3use std::io;
4use std::path::{Path, PathBuf};
5use std::process::Command;
6
7const BUILD_ERROR_MSG: &str = "Unable to build botan.";
8const SRC_DIR_ERROR_MSG: &str = "Unable to find the source directory.";
9const INCLUDE_DIR: &str = "build/include/public";
10
11pub const BOTAN_VERSION: &str = env!("BOTAN_VERSION");
14pub const BOTAN_TARBALL_SHA256: &str = env!("BOTAN_TARBALL_SHA256");
15pub const BOTAN_TARBALL_URL: &str = env!("BOTAN_TARBALL_URL");
16
17macro_rules! pathbuf_to_string {
18 ($s: ident) => {
19 $s.to_str().expect(BUILD_ERROR_MSG).to_string()
20 };
21}
22
23fn env_name_for(opt: &'static str) -> String {
24 assert!(opt[0..2] == *"--");
25 let to_var = opt[2..].to_uppercase().replace('-', "_");
26 format!("BOTAN_CONFIGURE_{to_var}")
27}
28
29fn configure(build_dir: &str) {
30 let mut configure = Command::new("python3");
31 configure.arg("configure.py");
32 configure.arg(format!("--with-build-dir={build_dir}"));
33 configure.arg("--build-targets=static");
34 configure.arg("--without-documentation");
35 configure.arg("--no-install-python-module");
36 configure.arg("--distribution-info=https://crates.io/crates/botan-src");
37
38 configure.arg(format!(
39 "--cpu={}",
40 env::var("CARGO_CFG_TARGET_ARCH").unwrap()
41 ));
42 configure.arg(format!("--os={}", env::var("CARGO_CFG_TARGET_OS").unwrap()));
43
44 #[cfg(debug_assertions)]
45 configure.arg("--with-debug-info");
46
47 #[cfg(target_os = "windows")]
50 configure.arg("--amalgamation");
51
52 let args = [
53 "--compiler-cache",
54 "--cc",
55 "--cc-bin",
56 "--cc-abi-flags",
57 "--cxxflags",
58 "--extra-cxxflags",
59 "--ldflags",
60 "--ar-command",
61 "--ar-options",
62 "--msvc-runtime",
63 "--system-cert-bundle",
64 "--module-policy",
65 "--enable-modules",
66 "--disable-modules",
67 ];
68
69 let flags = [
70 "--optimize-for-size",
71 "--amalgamation",
72 "--with-commoncrypto",
73 "--with-sqlite3",
74 ];
75
76 for arg_name in &args {
77 let env_name = env_name_for(arg_name);
78 if let Ok(arg_val) = env::var(env_name) {
79 let arg = format!("{arg_name}={arg_val}");
80 configure.arg(arg);
81 }
82 }
83
84 for flag_name in &flags {
85 let env_name = env_name_for(flag_name);
86 if env::var(env_name).is_ok() {
87 configure.arg(flag_name);
88 }
89 }
90
91 let status = configure
92 .spawn()
93 .expect(BUILD_ERROR_MSG)
94 .wait()
95 .expect(BUILD_ERROR_MSG);
96 if !status.success() {
97 panic!("configure terminated unsuccessfully");
98 }
99}
100
101fn make(build_dir: &str) {
102 #[cfg(target_os = "windows")]
109 let mut cmd = {
110 let mut cmd = Command::new("nmake");
111 cmd.arg("/NOLOGO")
112 .arg("/F")
113 .arg(format!("{build_dir}/Makefile"))
114 .arg("libs");
115 cmd
116 };
117
118 #[cfg(not(target_os = "windows"))]
119 let mut cmd = {
120 let mut cmd = Command::new("make");
121 if let Ok(val) = env::var("CARGO_MAKEFLAGS") {
124 cmd.env("MAKEFLAGS", val);
125 } else {
126 eprintln!("Can't set MAKEFLAGS as CARGO_MAKEFLAGS couldn't be read");
127 }
128 cmd.arg("-f")
129 .arg(format!("{build_dir}/Makefile"))
130 .arg("libs");
131 cmd
132 };
133
134 let status = cmd
135 .spawn()
136 .expect(BUILD_ERROR_MSG)
137 .wait()
138 .expect(BUILD_ERROR_MSG);
139 if !status.success() {
140 panic!("make terminated unsuccessfully");
141 }
142}
143fn bundled_tarball_path() -> PathBuf {
144 PathBuf::from(env!("CARGO_MANIFEST_DIR"))
145 .join("vendor")
146 .join(format!("Botan-{BOTAN_VERSION}.tar.xz"))
147}
148
149fn verify_sha256(path: &Path) {
150 use sha2::{Digest, Sha256};
151 let bytes = fs::read(path).expect("read tarball");
152 let actual = format!("{:x}", Sha256::digest(&bytes));
153 if actual != BOTAN_TARBALL_SHA256 {
154 panic!(
155 "Botan tarball at {} has unexpected sha256 (expected {}, got {})",
156 path.display(),
157 BOTAN_TARBALL_SHA256,
158 actual,
159 );
160 }
161}
162
163fn extract_tarball(tarball: &Path, dest: &Path) {
164 let file = fs::File::open(tarball).expect("open tarball");
165 let mut reader = io::BufReader::new(file);
166 let mut decompressed = Vec::new();
167 lzma_rs::xz_decompress(&mut reader, &mut decompressed).expect("xz decompress");
168 let mut archive = tar::Archive::new(io::Cursor::new(decompressed));
169
170 #[cfg(target_os = "windows")]
174 {
175 for entry in archive.entries().expect("read archive entries") {
176 let mut entry = entry.expect("read archive entry");
177 let entry_type = entry.header().entry_type();
178 if entry_type.is_symlink() || entry_type.is_hard_link() {
179 continue;
180 }
181 entry.unpack_in(dest).expect("unpack entry");
182 }
183 }
184
185 #[cfg(not(target_os = "windows"))]
186 archive.unpack(dest).expect("untar");
187}
188
189fn find_extracted_root(extract_root: &Path) -> PathBuf {
193 let mut dirs = fs::read_dir(extract_root)
194 .expect("read extract root")
195 .filter_map(Result::ok)
196 .map(|e| e.path())
197 .filter(|p| p.is_dir());
198 let first = dirs
199 .next()
200 .expect("tarball produced no top-level directory");
201 if dirs.next().is_some() {
202 panic!("tarball must contain exactly one top-level directory");
203 }
204 first
205}
206
207fn ensure_source(out_dir: &Path) -> PathBuf {
218 println!("cargo:rerun-if-env-changed=BOTAN_SRC_DIR");
219 println!("cargo:rerun-if-env-changed=BOTAN_SRC_TARBALL");
220
221 if let Some(custom_dir) = env::var_os("BOTAN_SRC_DIR") {
222 let path = PathBuf::from(custom_dir);
223 if !path.join("configure.py").is_file() {
224 panic!(
225 "BOTAN_SRC_DIR={} does not contain configure.py",
226 path.display()
227 );
228 }
229 return path;
230 }
231
232 let custom_tarball = env::var_os("BOTAN_SRC_TARBALL").map(PathBuf::from);
233 let tarball = custom_tarball.clone().unwrap_or_else(bundled_tarball_path);
234 let stamp_marker = match &custom_tarball {
235 Some(p) => format!("custom:{}", p.display()),
236 None => format!("bundled:{BOTAN_TARBALL_SHA256}"),
237 };
238
239 let extract_root = out_dir.join("botan-src");
240 let stamp = extract_root.join(".extracted");
241 let already_extracted = fs::read_to_string(&stamp)
242 .map(|s| s.trim() == stamp_marker)
243 .unwrap_or(false);
244 if !already_extracted {
245 if !tarball.exists() {
246 panic!("Botan source tarball missing at {}", tarball.display());
247 }
248 if custom_tarball.is_none() {
249 verify_sha256(&tarball);
250 }
251 let _ = fs::remove_dir_all(&extract_root);
252 fs::create_dir_all(&extract_root).expect("mkdir extract root");
253 extract_tarball(&tarball, &extract_root);
254 fs::write(&stamp, &stamp_marker).expect("write stamp");
255 }
256 find_extracted_root(&extract_root)
257}
258
259pub fn build() -> (String, std::path::PathBuf) {
260 let out_dir = env::var_os("OUT_DIR")
261 .map(PathBuf::from)
262 .expect("OUT_DIR is set when invoked from a build script");
263 let src_dir = ensure_source(&out_dir);
264 let build_dir = out_dir.join("botan-build");
265 let include_dir = build_dir.join(INCLUDE_DIR);
266 let build_dir = pathbuf_to_string!(build_dir);
267 let orig_dir = env::current_dir().expect(SRC_DIR_ERROR_MSG);
268 env::set_current_dir(&src_dir).expect(SRC_DIR_ERROR_MSG);
269 configure(&build_dir);
270 make(&build_dir);
271 env::set_current_dir(&orig_dir).expect("Unable to restore cwd");
272 (build_dir, include_dir)
273}