use std::path::{Path, PathBuf};
use std::process::Command;
use std::{env, fs};
const VERSION: &str = "1.52.0";
const TARBALL_URL: &str =
"https://github.com/espeak-ng/espeak-ng/archive/refs/tags/1.52.0.tar.gz";
fn main() {
println!("cargo:rerun-if-changed=build.rs");
let bundled = env::var("CARGO_FEATURE_BUNDLED_ESPEAK").is_ok();
let c_oracle = env::var("CARGO_FEATURE_C_ORACLE").is_ok();
if bundled {
let install_dir = build_espeak_ng();
emit_bundled_env(&install_dir);
if c_oracle {
let lib_dir = install_dir.join("lib");
println!("cargo:rustc-link-search=native={}", lib_dir.display());
println!("cargo:rustc-link-lib=espeak-ng");
emit_bundled_link_deps();
}
} else if c_oracle {
link_system_espeak();
}
}
fn emit_bundled_link_deps() {
let out_dir = PathBuf::from(env::var("OUT_DIR").unwrap());
let build_dir = out_dir.join(format!("espeak-ng-{VERSION}-build"));
let ucd_dir = build_dir.join("src").join("ucd-tools");
println!("cargo:rustc-link-search=native={}", ucd_dir.display());
println!("cargo:rustc-link-lib=ucd");
let speech_dir = build_dir.join("src").join("speechPlayer");
println!("cargo:rustc-link-search=native={}", speech_dir.display());
println!("cargo:rustc-link-lib=speechPlayer");
#[cfg(target_os = "linux")]
println!("cargo:rustc-link-lib=stdc++");
#[cfg(target_os = "macos")]
println!("cargo:rustc-link-lib=c++");
}
fn link_system_espeak() {
let found = Command::new("pkg-config")
.args(["--libs", "--cflags", "espeak-ng"])
.output()
.map(|o| o.status.success())
.unwrap_or(false);
if found {
let out = Command::new("pkg-config")
.args(["--libs", "espeak-ng"])
.output()
.expect("pkg-config must be on PATH when c-oracle feature is enabled");
for token in String::from_utf8(out.stdout).unwrap().split_whitespace() {
if let Some(lib) = token.strip_prefix("-l") {
println!("cargo:rustc-link-lib={lib}");
} else if let Some(path) = token.strip_prefix("-L") {
println!("cargo:rustc-link-search=native={path}");
}
}
} else {
println!("cargo:rustc-link-lib=espeak-ng");
}
}
fn build_espeak_ng() -> PathBuf {
let out_dir = PathBuf::from(env::var("OUT_DIR").unwrap());
let install_dir = out_dir.join(format!("espeak-ng-{VERSION}-install"));
let stamp = install_dir.join(".build-complete");
if stamp.exists() {
eprintln!("[bundled-espeak] cached build found at {}", install_dir.display());
return install_dir;
}
eprintln!("[bundled-espeak] building espeak-ng {VERSION} – this may take a minute…");
let src_dir = download_and_extract(&out_dir);
cmake_build(&src_dir, &out_dir, &install_dir);
fs::write(&stamp, VERSION).unwrap();
eprintln!("[bundled-espeak] build complete → {}", install_dir.display());
install_dir
}
fn download_and_extract(out_dir: &Path) -> PathBuf {
let tarball = out_dir.join(format!("espeak-ng-{VERSION}.tar.gz"));
let src_dir = out_dir.join(format!("espeak-ng-{VERSION}"));
if !tarball.exists() {
eprintln!("[bundled-espeak] downloading {TARBALL_URL}");
download_file(TARBALL_URL, &tarball);
}
if !src_dir.exists() {
eprintln!("[bundled-espeak] extracting tarball…");
let status = Command::new("tar")
.args(["-xzf", tarball.to_str().unwrap(), "-C", out_dir.to_str().unwrap()])
.status()
.expect("[bundled-espeak] `tar` must be available");
assert!(status.success(), "[bundled-espeak] tar extraction failed");
}
src_dir
}
fn download_file(url: &str, dest: &Path) {
let curl_ok = Command::new("curl")
.args(["--fail", "--location", "--silent", "--show-error",
"--output", dest.to_str().unwrap(), url])
.status()
.map(|s| s.success())
.unwrap_or(false);
if curl_ok {
return;
}
eprintln!("[bundled-espeak] curl failed or not found, trying wget…");
let wget_ok = Command::new("wget")
.args(["--quiet", "-O", dest.to_str().unwrap(), url])
.status()
.map(|s| s.success())
.unwrap_or(false);
assert!(
wget_ok,
"[bundled-espeak] failed to download {url}\n\
Neither `curl` nor `wget` succeeded.\n\
Set ESPEAK_NG_SOURCE_DIR to a pre-extracted source directory to skip the download."
);
}
fn cmake_build(src: &Path, out_dir: &Path, install: &Path) {
let build_dir = out_dir.join(format!("espeak-ng-{VERSION}-build"));
fs::create_dir_all(&build_dir).unwrap();
let jobs = available_parallelism();
eprintln!("[bundled-espeak] cmake configure…");
let status = Command::new("cmake")
.args([
"-S", src.to_str().unwrap(),
"-B", build_dir.to_str().unwrap(),
&format!("-DCMAKE_INSTALL_PREFIX={}", install.display()),
"-DCMAKE_BUILD_TYPE=Release",
"-DUSE_LIBPCAUDIO=OFF",
"-DUSE_MBROLA=OFF",
"-DUSE_LIBSONIC=OFF",
"-DUSE_KLATT=ON",
"-DENABLE_TESTS=OFF",
])
.status()
.expect("[bundled-espeak] `cmake` must be installed (https://cmake.org)");
assert!(status.success(), "[bundled-espeak] cmake configure failed");
eprintln!("[bundled-espeak] cmake build (j={jobs})…");
let status = Command::new("cmake")
.args([
"--build", build_dir.to_str().unwrap(),
"--parallel", &jobs.to_string(),
])
.status()
.expect("[bundled-espeak] cmake build failed");
assert!(status.success(), "[bundled-espeak] cmake build failed");
eprintln!("[bundled-espeak] cmake install…");
let status = Command::new("cmake")
.args(["--install", build_dir.to_str().unwrap()])
.status()
.expect("[bundled-espeak] cmake install failed");
assert!(status.success(), "[bundled-espeak] cmake install failed");
}
fn emit_bundled_env(install: &Path) {
let bin = install.join("bin").join("espeak-ng");
assert!(
bin.exists(),
"[bundled-espeak] expected binary at {} but it was not found after build",
bin.display()
);
println!("cargo:rustc-env=ESPEAK_NG_BIN={}", bin.display());
let candidates = [
install.join("lib").join("espeak-ng-data"),
install.join("share").join("espeak-ng-data"),
install.join("lib").join("x86_64-linux-gnu").join("espeak-ng-data"),
install.join("lib").join("aarch64-linux-gnu").join("espeak-ng-data"),
];
let data_dir = candidates
.iter()
.find(|p| p.exists())
.unwrap_or_else(|| {
panic!(
"[bundled-espeak] could not find espeak-ng-data under {}.\n\
Searched:\n{}",
install.display(),
candidates.iter()
.map(|p| format!(" {}", p.display()))
.collect::<Vec<_>>()
.join("\n")
)
});
println!("cargo:rustc-env=ESPEAK_NG_DATA={}", data_dir.display());
eprintln!(
"[bundled-espeak] binary : {}\n\
[bundled-espeak] data : {}",
bin.display(),
data_dir.display()
);
}
fn available_parallelism() -> usize {
std::thread::available_parallelism()
.map(|n| n.get().min(8))
.unwrap_or(4)
}