extern crate bindgen;
extern crate cc;
extern crate parse_cfg;
extern crate walkdir;
use parse_cfg::*;
use std::env;
use std::path::{Path, PathBuf};
use std::process::Command;
use walkdir::WalkDir;
const CPAL_ASIO_DIR: &str = "CPAL_ASIO_DIR";
const ASIO_SDK_URL: &str = "https://www.steinberg.net/asiosdk";
const ASIO_HEADER: &str = "asio.h";
const ASIO_SYS_HEADER: &str = "asiosys.h";
const ASIO_DRIVERS_HEADER: &str = "asiodrivers.h";
fn host_os_is_windows() -> bool {
std::env::consts::OS == "windows"
}
fn is_msvc() -> bool {
let target: Target = std::env::var("TARGET")
.expect("Target not set.")
.parse()
.expect("Unable to parse target.");
let target_env = match target {
Target::Triple { env, .. } => env,
Target::Cfg(_) => panic!("cfg targets not supported"),
};
if let Some(env) = target_env {
env.contains("msvc")
} else {
false
}
}
fn main() {
println!("cargo:rerun-if-env-changed={}", CPAL_ASIO_DIR);
let cpal_asio_dir = get_asio_dir();
println!("cargo:rerun-if-changed={}", cpal_asio_dir.display());
let out_dir = PathBuf::from(env::var("OUT_DIR").expect("bad path"));
let mut lib_path = out_dir.clone();
lib_path.push("libasio.a");
if !lib_path.exists() {
if is_msvc() {
invoke_vcvars_if_not_set();
}
create_lib(&cpal_asio_dir);
}
println!("cargo:rustc-link-lib=dylib=ole32");
println!("cargo:rustc-link-lib=dylib=User32");
println!("cargo:rustc-link-search={}", out_dir.display());
println!("cargo:rustc-link-lib=static=asio");
println!("cargo:rustc-cfg=asio");
let mut binding_path = out_dir.clone();
binding_path.push("asio_bindings.rs");
if !binding_path.exists() {
if is_msvc() {
invoke_vcvars_if_not_set();
}
create_bindings(&cpal_asio_dir);
}
}
fn create_lib(cpal_asio_dir: &Path) {
let mut cpp_paths: Vec<PathBuf> = Vec::new();
let mut host_dir = cpal_asio_dir.to_path_buf();
let mut pc_dir = cpal_asio_dir.to_path_buf();
let mut common_dir = cpal_asio_dir.to_path_buf();
host_dir.push("host");
common_dir.push("common");
pc_dir.push("host/pc");
let walk_a_dir = |dir_to_walk, paths: &mut Vec<PathBuf>| {
for entry in WalkDir::new(dir_to_walk).max_depth(1) {
let entry = match entry {
Err(e) => {
println!("error: {}", e);
continue;
}
Ok(entry) => entry,
};
match entry.path().extension().and_then(|s| s.to_str()) {
None => continue,
Some("cpp") => {
if entry.path().file_name().unwrap().to_str() == Some("asiodrvr.cpp") {
continue;
}
paths.push(entry.path().to_path_buf())
}
Some(_) => continue,
};
}
};
walk_a_dir(host_dir, &mut cpp_paths);
walk_a_dir(pc_dir, &mut cpp_paths);
walk_a_dir(common_dir, &mut cpp_paths);
cc::Build::new()
.include(format!("{}/{}", cpal_asio_dir.display(), "host"))
.include(format!("{}/{}", cpal_asio_dir.display(), "common"))
.include(format!("{}/{}", cpal_asio_dir.display(), "host/pc"))
.include("asio-link/helpers.hpp")
.file("asio-link/helpers.cpp")
.files(cpp_paths)
.cpp(true)
.compile("libasio.a");
}
fn create_bindings(cpal_asio_dir: &PathBuf) {
let mut asio_header = None;
let mut asio_sys_header = None;
let mut asio_drivers_header = None;
for entry in WalkDir::new(cpal_asio_dir) {
let entry = match entry {
Err(_) => continue,
Ok(entry) => entry,
};
let file_name = match entry.path().file_name().and_then(|s| s.to_str()) {
None => continue,
Some(file_name) => file_name,
};
match file_name {
ASIO_HEADER => asio_header = Some(entry.path().to_path_buf()),
ASIO_SYS_HEADER => asio_sys_header = Some(entry.path().to_path_buf()),
ASIO_DRIVERS_HEADER => asio_drivers_header = Some(entry.path().to_path_buf()),
_ => (),
}
}
macro_rules! header_or_panic {
($opt_header:expr, $FILE_NAME:expr) => {
match $opt_header.as_ref() {
None => {
panic!(
"Could not find {} in {}: {}",
$FILE_NAME,
CPAL_ASIO_DIR,
cpal_asio_dir.display()
);
}
Some(path) => path.to_str().expect("Could not convert path to str"),
}
};
}
let asio_header = header_or_panic!(asio_header, ASIO_HEADER);
let asio_sys_header = header_or_panic!(asio_sys_header, ASIO_SYS_HEADER);
let asio_drivers_header = header_or_panic!(asio_drivers_header, ASIO_DRIVERS_HEADER);
let bindings = bindgen::Builder::default()
.header(asio_header)
.header(asio_sys_header)
.header(asio_drivers_header)
.header("asio-link/helpers.hpp")
.clang_arg("-x")
.clang_arg("c++")
.clang_arg("-std=c++14")
.clang_arg(format!("-I{}/{}", cpal_asio_dir.display(), "host/pc"))
.clang_arg(format!("-I{}/{}", cpal_asio_dir.display(), "host"))
.clang_arg(format!("-I{}/{}", cpal_asio_dir.display(), "common"))
.allowlist_type("AsioDrivers")
.allowlist_type("AsioDriver")
.allowlist_type("ASIOTime")
.allowlist_type("ASIOTimeInfo")
.allowlist_type("ASIODriverInfo")
.allowlist_type("ASIOBufferInfo")
.allowlist_type("ASIOCallbacks")
.allowlist_type("ASIOSamples")
.allowlist_type("ASIOSampleType")
.allowlist_type("ASIOSampleRate")
.allowlist_type("ASIOChannelInfo")
.allowlist_type("AsioTimeInfoFlags")
.allowlist_type("ASIOTimeCodeFlags")
.allowlist_function("ASIOGetChannels")
.allowlist_function("ASIOGetChannelInfo")
.allowlist_function("ASIOGetBufferSize")
.allowlist_function("ASIOGetSamplePosition")
.allowlist_function("ASIOOutputReady")
.allowlist_function("get_sample_rate")
.allowlist_function("set_sample_rate")
.allowlist_function("can_sample_rate")
.allowlist_function("ASIOInit")
.allowlist_function("ASIOCreateBuffers")
.allowlist_function("ASIOStart")
.allowlist_function("ASIOStop")
.allowlist_function("ASIODisposeBuffers")
.allowlist_function("ASIOExit")
.allowlist_function("load_asio_driver")
.allowlist_function("remove_current_driver")
.allowlist_function("get_driver_names")
.bitfield_enum("AsioTimeInfoFlags")
.bitfield_enum("ASIOTimeCodeFlags")
.generate()
.expect("Unable to generate bindings");
let out_path = PathBuf::from(env::var("OUT_DIR").expect("bad path"));
bindings
.write_to_file(out_path.join("asio_bindings.rs"))
.expect("Couldn't write bindings!");
}
fn get_asio_dir() -> PathBuf {
if let Ok(path) = env::var(CPAL_ASIO_DIR) {
println!("CPAL_ASIO_DIR is set at {path}");
return PathBuf::from(path);
}
let temp_dir = env::temp_dir();
let asio_dir = temp_dir.join("asio_sdk");
if asio_dir.exists() {
println!("CPAL_ASIO_DIR is set at {}", asio_dir.display());
return asio_dir;
}
println!("CPAL_ASIO_DIR is not set or contents are cached downloading from {ASIO_SDK_URL}",);
download_asio_sdk_to_temp_dir(&temp_dir);
for entry in walkdir::WalkDir::new(&temp_dir).min_depth(1).max_depth(1) {
let entry = entry.unwrap();
if entry.file_type().is_dir() && entry.file_name().to_string_lossy().starts_with("asio") {
std::fs::rename(entry.path(), &asio_dir).expect("Failed to rename directory");
break;
}
}
println!("CPAL_ASIO_DIR is set at {}", asio_dir.display());
asio_dir
}
fn download_asio_sdk_to_temp_dir(temp_dir: &Path) {
let asio_zip_path = temp_dir.join("asio_sdk.zip");
if host_os_is_windows() {
let status = Command::new("powershell")
.args([
"-NoProfile",
"-Command",
&format!(
"Invoke-WebRequest -Uri {ASIO_SDK_URL} -OutFile {}",
asio_zip_path.display()
),
])
.status()
.expect("Failed to execute PowerShell command");
if !status.success() {
panic!("Failed to download ASIO SDK");
}
println!("Downloaded ASIO SDK successfully");
println!("Extracting ASIO SDK..");
let status = Command::new("powershell")
.args([
"-NoProfile",
"-Command",
&format!(
"Expand-Archive -Path {} -DestinationPath {} -Force",
asio_zip_path.display(),
temp_dir.display()
),
])
.status()
.expect("Failed to execute PowerShell command for extracting ASIO SDK");
if !status.success() {
panic!("Failed to extract ASIO SDK");
}
} else {
let status = Command::new("sh")
.arg("-c")
.arg(&format!(
"curl -L --fail --output {} {}",
asio_zip_path.display(),
"https://www.steinberg.net/asiosdk" ))
.status()
.expect("Failed to execute curl command");
if !status.success() {
panic!("Failed to download ASIO SDK");
}
println!("Downloaded ASIO SDK successfully");
println!("Extracting ASIO SDK..");
let status = Command::new("unzip")
.args([
"-o",
asio_zip_path.to_str().unwrap(),
"-d",
temp_dir.to_str().unwrap(),
])
.status()
.expect("Failed to execute unzip command for extracting ASIO SDK");
if !status.success() {
panic!("Failed to extract ASIO SDK");
}
}
}
fn invoke_vcvars_if_not_set() {
if vcvars_set() {
return;
}
println!("VCINSTALLDIR is not set. Attempting to invoke vcvarsall.bat..");
println!("Invoking vcvarsall.bat..");
println!("Determining system architecture..");
let arch_arg = determine_vcvarsall_bat_arch_arg();
println!(
"Host architecture is detected as {}.",
std::env::consts::ARCH
);
println!("Architecture argument for vcvarsall.bat will be used as: {arch_arg}.");
let vcvars_all_bat_path = search_vcvars_all_bat();
println!(
"Found vcvarsall.bat at {}. Initializing environment..",
vcvars_all_bat_path.display()
);
let output = Command::new("cmd")
.args([
"/c",
vcvars_all_bat_path.to_str().unwrap(),
&arch_arg,
"&&",
"set",
])
.output()
.expect("Failed to execute command");
for line in String::from_utf8_lossy(&output.stdout).lines() {
let parts: Vec<&str> = line.splitn(2, '=').collect();
if parts.len() == 2 {
env::set_var(parts[0], parts[1]);
println!("{}={}", parts[0], parts[1]);
}
}
}
fn vcvars_set() -> bool {
env::var("VCINSTALLDIR").is_ok()
}
fn search_vcvars_all_bat() -> PathBuf {
if let Some(path) = guess_vcvars_all_bat() {
return path;
}
let paths = &[
"C:\\Program Files\\Microsoft Visual Studio\\",
"C:\\Program Files (x86)\\Microsoft Visual Studio\\",
];
println!("Searching for vcvarsall.bat in {paths:?}");
let mut found = None;
for path in paths.iter() {
for entry in WalkDir::new(path)
.into_iter()
.filter_map(Result::ok)
.filter(|e| !e.file_type().is_dir())
{
if entry.path().ends_with("vcvarsall.bat") {
found.replace(entry.path().to_path_buf());
}
}
}
match found {
Some(path) => path,
None => panic!(
"Could not find vcvarsall.bat. Please install the latest version of Visual Studio."
),
}
}
fn guess_vcvars_all_bat() -> Option<PathBuf> {
fn is_year(s: Option<&str>) -> Option<String> {
let Some(s) = s else {
return None;
};
if s.len() == 4 && s.chars().all(|c| c.is_ascii_digit()) {
Some(s.to_string())
} else {
None
}
}
fn is_edition(s: Option<&str>) -> Option<String> {
let Some(s) = s else {
return None;
};
let editions = ["Enterprise", "Professional", "Community", "Express"];
if editions.contains(&s) {
Some(s.to_string())
} else {
None
}
}
fn construct_path(base: &Path) -> Option<PathBuf> {
let mut constructed = base.to_path_buf();
for entry in WalkDir::new(&constructed).max_depth(1) {
let entry = match entry {
Err(_) => continue,
Ok(entry) => entry,
};
if let Some(year) = is_year(entry.path().file_name().and_then(|s| s.to_str())) {
constructed = constructed.join(year);
for entry in WalkDir::new(&constructed).max_depth(1) {
let entry = match entry {
Err(_) => continue,
Ok(entry) => entry,
};
if let Some(edition) =
is_edition(entry.path().file_name().and_then(|s| s.to_str()))
{
constructed = constructed
.join(edition)
.join("VC")
.join("Auxiliary")
.join("Build")
.join("vcvarsall.bat");
return Some(constructed);
}
}
}
}
None
}
let vs_2022_and_onwards_base = PathBuf::from("C:\\Program Files\\Microsoft Visual Studio\\");
let vs_2019_and_2017_base = PathBuf::from("C:\\Program Files (x86)\\Microsoft Visual Studio\\");
construct_path(&vs_2022_and_onwards_base).map_or_else(
|| construct_path(&vs_2019_and_2017_base).map_or_else(|| None, Some),
Some,
)
}
fn determine_vcvarsall_bat_arch_arg() -> String {
let host_architecture = std::env::consts::ARCH;
let target_architecture = std::env::var("CARGO_CFG_TARGET_ARCH").expect("Target not set.");
let arch_arg = if target_architecture == "x86_64" {
if host_architecture == "x86" {
"x86_amd64"
} else if host_architecture == "x86_64" {
"amd64"
} else if host_architecture == "aarch64" {
"arm64_amd64"
} else {
panic!("Unsupported host architecture {}", host_architecture);
}
} else if target_architecture == "x86" {
if host_architecture == "x86" {
"x86"
} else if host_architecture == "x86_64" {
"amd64_x86"
} else if host_architecture == "aarch64" {
"arm64_x86"
} else {
panic!("Unsupported host architecture {}", host_architecture);
}
} else if target_architecture == "arm" {
if host_architecture == "x86" {
"x86_arm"
} else if host_architecture == "x86_64" {
"amd64_arm"
} else if host_architecture == "aarch64" {
"arm64_arm"
} else {
panic!("Unsupported host architecture {}", host_architecture);
}
} else if target_architecture == "aarch64" {
if host_architecture == "x86" {
"x86_arm64"
} else if host_architecture == "x86_64" {
"amd64_arm64"
} else if host_architecture == "aarch64" {
"arm64"
} else {
panic!("Unsupported host architecture {}", host_architecture);
}
} else {
panic!("Unsupported target architecture.");
};
arch_arg.to_owned()
}