Skip to main content

botan_src/
lib.rs

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
11// Pinned upstream release. Single source of truth lives in release.toml;
12// build.rs parses that file and re-exports it via `cargo:rustc-env`.
13pub 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    // On Windows we require the amalgamation, to work around the fact that
48    // otherwise the linker command lines become too long for Windows
49    #[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    // On Windows the Botan Makefile is generated for the MSVC toolchain, whose
103    // standard build tool is `nmake` (ships with Visual Studio and is on PATH
104    // inside a VS developer environment). GNU make is frequently absent from
105    // Windows build images, so use nmake there. nmake does not support GNU
106    // Make's jobserver or parallel target execution, so CARGO_MAKEFLAGS is not
107    // forwarded on Windows.
108    #[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        // Set MAKEFLAGS to the content of CARGO_MAKEFLAGS to give jobserver
122        // (parallel builds) support to the spawned sub-make.
123        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    // Windows without Developer Mode (or admin rights) cannot create symlinks.
171    // The Botan tarball contains a handful of symlinks (e.g. .github/codecov.yml)
172    // that are not needed to build the library. Skip them on Windows only.
173    #[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
189// After unpacking, find the single top-level directory the tarball
190// produced. Bundled Botan releases use `Botan-X.Y.Z`, but a developer's
191// custom tarball (BOTAN_SRC_TARBALL) might use anything.
192fn 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
207/// Returns the directory containing Botan sources to build against.
208///
209/// Resolution order, highest priority first:
210/// - `BOTAN_SRC_DIR` — use this directory as the source tree directly
211///   (no extraction, no checksum). Useful for testing a local git
212///   checkout or fork.
213/// - `BOTAN_SRC_TARBALL` — extract this `.tar.xz` instead of the bundled
214///   one. No checksum: the caller is responsible for what they hand us.
215/// - otherwise: extract the bundled `vendor/Botan-X.Y.Z.tar.xz`,
216///   verifying it matches the pinned SHA-256.
217fn 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}