nandi/jolt-nativepublic Fork 0
fa4ecdfed6a830e6099d5fab9be24ef72ea1923b
Commits
Clone
git clone https://git.rickub.com/nandi/jolt-native.git
git clone ssh://git@rickub.com/nandi/jolt-native.git

Host key fingerprint (ed25519): SHA256:iycHnxEyq0Q7uyVpB7JlznP0G7JrTPXLYRcAU5CSLhc — verify it before your first connect.

build.rs · 623 lines · 21.3 KBRust Blame HistoryRaw
Lift freeq's AV media plane out of sleek 90f8b89 nandi 19d ago1extern crate bindgen;
2extern crate cc;
3extern crate parse_cfg;
4extern crate walkdir;
5
6use parse_cfg::*;
7use std::env;
8use std::path::{Path, PathBuf};
9use std::process::Command;
10use walkdir::WalkDir;
11
12const CPAL_ASIO_DIR: &str = "CPAL_ASIO_DIR";
13const ASIO_SDK_URL: &str = "https://www.steinberg.net/asiosdk";
14
15const ASIO_HEADER: &str = "asio.h";
16const ASIO_SYS_HEADER: &str = "asiosys.h";
17const ASIO_DRIVERS_HEADER: &str = "asiodrivers.h";
18
19/// Checks if the host OS is Windows
20fn host_os_is_windows() -> bool {
21 std::env::consts::OS == "windows"
22}
23
24/// Checks if the target env is MSVC
25fn is_msvc() -> bool {
26 let target: Target = std::env::var("TARGET")
27 .expect("Target not set.")
28 .parse()
29 .expect("Unable to parse target.");
30
31 let target_env = match target {
32 Target::Triple { env, .. } => env,
33 Target::Cfg(_) => panic!("cfg targets not supported"),
34 };
35
36 if let Some(env) = target_env {
37 env.contains("msvc")
38 } else {
39 false
40 }
41}
42
43fn main() {
44 // When building on docs.rs, skip the actual build and generate stub bindings
45 if std::env::var("DOCS_RS").is_ok() {
46 println!("cargo:warning=Building for docs.rs - generating stub bindings");
47 let out_dir = PathBuf::from(env::var("OUT_DIR").expect("bad path"));
48 create_stub_bindings(&out_dir);
49 return;
50 }
51
52 println!("cargo:rerun-if-env-changed={}", CPAL_ASIO_DIR);
53
54 // ASIO SDK directory
55 let cpal_asio_dir = get_asio_dir();
56 println!("cargo:rerun-if-changed={}", cpal_asio_dir.display());
57
58 // Directory where bindings and library are created
59 let out_dir = PathBuf::from(env::var("OUT_DIR").expect("bad path"));
60
61 // Check if library exists,
62 // if it doesn't create it
63 let mut lib_path = out_dir.clone();
64 lib_path.push("libasio.a");
65 if !lib_path.exists() {
66 if is_msvc() {
67 invoke_vcvars_if_not_set();
68 }
69 create_lib(&cpal_asio_dir);
70 }
71
72 // Print out links to needed libraries
73 println!("cargo:rustc-link-lib=dylib=advapi32");
74 println!("cargo:rustc-link-lib=dylib=ole32");
75 println!("cargo:rustc-link-lib=dylib=user32");
76 println!("cargo:rustc-link-search={}", out_dir.display());
77 println!("cargo:rustc-link-lib=static=asio");
78 println!("cargo:rustc-cfg=asio");
79
80 // Check if bindings exist
81 // If they don't create them
82 let mut binding_path = out_dir.clone();
83 binding_path.push("asio_bindings.rs");
84 if !binding_path.exists() {
85 if is_msvc() {
86 invoke_vcvars_if_not_set();
87 }
88 create_bindings(&cpal_asio_dir);
89 }
90}
91
92fn create_lib(cpal_asio_dir: &Path) {
93 let mut cpp_paths: Vec<PathBuf> = Vec::new();
94 let mut host_dir = cpal_asio_dir.to_path_buf();
95 let mut pc_dir = cpal_asio_dir.to_path_buf();
96 let mut common_dir = cpal_asio_dir.to_path_buf();
97 host_dir.push("host");
98 common_dir.push("common");
99 pc_dir.push("host/pc");
100
101 // Gathers cpp files from directories
102 let walk_a_dir = |dir_to_walk, paths: &mut Vec<PathBuf>| {
103 for entry in WalkDir::new(dir_to_walk).max_depth(1) {
104 let entry = match entry {
105 Err(e) => {
106 println!("error: {}", e);
107 continue;
108 }
109 Ok(entry) => entry,
110 };
111 match entry.path().extension().and_then(|s| s.to_str()) {
112 None => continue,
113 Some("cpp") => {
114 // Skip macos bindings
115 if entry.path().file_name().unwrap().to_str() == Some("asiodrvr.cpp") {
116 continue;
117 }
118 paths.push(entry.path().to_path_buf())
119 }
120 Some(_) => continue,
121 };
122 }
123 };
124
125 // Get all cpp files for building SDK library
126 walk_a_dir(host_dir, &mut cpp_paths);
127 walk_a_dir(pc_dir, &mut cpp_paths);
128 walk_a_dir(common_dir, &mut cpp_paths);
129
130 // build the asio lib
131 cc::Build::new()
132 .include(format!("{}/{}", cpal_asio_dir.display(), "host"))
133 .include(format!("{}/{}", cpal_asio_dir.display(), "common"))
134 .include(format!("{}/{}", cpal_asio_dir.display(), "host/pc"))
135 .include("asio-link/helpers.hpp")
136 .file("asio-link/helpers.cpp")
137 .files(cpp_paths)
138 .cpp(true)
139 .compile("libasio.a");
140}
141
142/// Creates stub bindings for docs.rs
143///
144/// Since docs.rs builds in a sandboxed environment without network access
145/// and cannot cross-compile Windows MSVC targets with C++ dependencies,
146/// we generate minimal stub bindings that allow documentation to be built.
147fn create_stub_bindings(out_dir: &Path) {
148 let stub_content = include_str!("asio_stub_bindings.rs");
149 let binding_path = out_dir.join("asio_bindings.rs");
150 std::fs::write(&binding_path, stub_content).expect("Failed to write stub bindings");
151}
152
153fn create_bindings(cpal_asio_dir: &PathBuf) {
154 let mut asio_header = None;
155 let mut asio_sys_header = None;
156 let mut asio_drivers_header = None;
157
158 // Recursively walk given cpal dir to find required headers
159 for entry in WalkDir::new(cpal_asio_dir) {
160 let entry = match entry {
161 Err(_) => continue,
162 Ok(entry) => entry,
163 };
164 let file_name = match entry.path().file_name().and_then(|s| s.to_str()) {
165 None => continue,
166 Some(file_name) => file_name,
167 };
168
169 match file_name {
170 ASIO_HEADER => asio_header = Some(entry.path().to_path_buf()),
171 ASIO_SYS_HEADER => asio_sys_header = Some(entry.path().to_path_buf()),
172 ASIO_DRIVERS_HEADER => asio_drivers_header = Some(entry.path().to_path_buf()),
173 _ => (),
174 }
175 }
176
177 macro_rules! header_or_panic {
178 ($opt_header:expr, $FILE_NAME:expr) => {
179 match $opt_header.as_ref() {
180 None => {
181 panic!(
182 "Could not find {} in {}: {}",
183 $FILE_NAME,
184 CPAL_ASIO_DIR,
185 cpal_asio_dir.display()
186 );
187 }
188 Some(path) => path.to_str().expect("Could not convert path to str"),
189 }
190 };
191 }
192
193 // Only continue if found all headers that we need
194 let asio_header = header_or_panic!(asio_header, ASIO_HEADER);
195 let asio_sys_header = header_or_panic!(asio_sys_header, ASIO_SYS_HEADER);
196 let asio_drivers_header = header_or_panic!(asio_drivers_header, ASIO_DRIVERS_HEADER);
197
198 // The bindgen::Builder is the main entry point
199 // to bindgen, and lets you build up options for
200 // the resulting bindings.
201 let bindings = bindgen::Builder::default()
202 // The input header we would like to generate
203 // bindings for.
204 .header(asio_header)
205 .header(asio_sys_header)
206 .header(asio_drivers_header)
207 .header("asio-link/helpers.hpp")
208 .clang_arg("-x")
209 .clang_arg("c++")
210 .clang_arg("-std=c++14")
211 .clang_arg(format!("-I{}/{}", cpal_asio_dir.display(), "host/pc"))
212 .clang_arg(format!("-I{}/{}", cpal_asio_dir.display(), "host"))
213 .clang_arg(format!("-I{}/{}", cpal_asio_dir.display(), "common"))
214 // Need to whitelist to avoid binding tp c++ std::*
215 .allowlist_type("AsioDrivers")
216 .allowlist_type("AsioDriver")
217 .allowlist_type("ASIOTime")
218 .allowlist_type("ASIOTimeInfo")
219 .allowlist_type("ASIODriverInfo")
220 .allowlist_type("ASIOBufferInfo")
221 .allowlist_type("ASIOCallbacks")
222 .allowlist_type("ASIOSamples")
223 .allowlist_type("ASIOSampleType")
224 .allowlist_type("ASIOSampleRate")
225 .allowlist_type("ASIOChannelInfo")
226 .allowlist_type("AsioTimeInfoFlags")
227 .allowlist_type("ASIOTimeCodeFlags")
228 .allowlist_function("ASIOGetChannels")
229 .allowlist_function("ASIOGetChannelInfo")
230 .allowlist_function("ASIOGetBufferSize")
231 .allowlist_function("ASIOGetLatencies")
232 .allowlist_function("ASIOGetSamplePosition")
233 .allowlist_function("ASIOOutputReady")
234 .allowlist_function("get_sample_rate")
235 .allowlist_function("set_sample_rate")
236 .allowlist_function("can_sample_rate")
237 .allowlist_function("ASIOInit")
238 .allowlist_function("ASIOCreateBuffers")
239 .allowlist_function("ASIOStart")
240 .allowlist_function("ASIOStop")
241 .allowlist_function("ASIODisposeBuffers")
242 .allowlist_function("ASIOExit")
243 .allowlist_function("load_asio_driver")
244 .allowlist_function("remove_current_driver")
245 .allowlist_function("get_driver_names")
246 .bitfield_enum("AsioTimeInfoFlags")
247 .bitfield_enum("ASIOTimeCodeFlags")
248 // Finish the builder and generate the bindings.
249 .generate()
250 // Unwrap the Result and panic on failure.
251 .expect("Unable to generate bindings");
252
253 // Write the bindings to the $OUT_DIR/bindings.rs file.
254 let out_path = PathBuf::from(env::var("OUT_DIR").expect("bad path"));
255
256 bindings
257 .write_to_file(out_path.join("asio_bindings.rs"))
258 .expect("Couldn't write bindings!");
259}
260
261/// Gets the ASIO SDK directory
262///
263/// If the CPAL_ASIO_DIR env var is set, it will use that.
264///
265/// If not set, it will check the temp directory for the ASIO SDK.
266///
267/// If not found, it will download the ASIO SDK to the temp directory.
268///
269/// It will then move the contents of the inner directory to the temp directory.
270///
271/// It will then return the path to the ASIO SDK directory.
272fn get_asio_dir() -> PathBuf {
273 // Check if CPAL_ASIO_DIR env var is set
274 if let Ok(path) = env::var(CPAL_ASIO_DIR) {
275 println!("CPAL_ASIO_DIR is set at {path}");
276 return PathBuf::from(path);
277 }
278
279 // If not set, check temp directory for ASIO SDK, maybe it is previously downloaded
280 let temp_dir = env::temp_dir();
281 let asio_dir = temp_dir.join("asio_sdk");
282 if asio_dir.exists() {
283 println!("CPAL_ASIO_DIR is set at {}", asio_dir.display());
284 return asio_dir;
285 }
286
287 // If not found, download ASIO SDK using PowerShell's Invoke-WebRequest
288 println!("CPAL_ASIO_DIR is not set or contents are cached downloading from {ASIO_SDK_URL}",);
289
290 download_asio_sdk_to_temp_dir(&temp_dir);
291
292 // Move the contents of the inner directory to asio_dir
293 for entry in walkdir::WalkDir::new(&temp_dir).min_depth(1).max_depth(1) {
294 let entry = entry.unwrap();
295 if entry.file_type().is_dir()
296 && entry
297 .file_name()
298 .to_string_lossy()
299 .to_lowercase()
300 .starts_with("asio")
301 {
302 std::fs::rename(entry.path(), &asio_dir).expect("Failed to rename directory");
303 break;
304 }
305 }
306 println!("CPAL_ASIO_DIR is set at {}", asio_dir.display());
307 asio_dir
308}
309
310/// Downloads the ASIO SDK to the temp directory of the host OS
311///
312/// It uses powershell's Invoke-WebRequest on Windows and curl on other platforms to download the SDK.
313///
314/// It then extracts the SDK using powershell's Expand-Archive on Windows and unzip on other platforms.
315fn download_asio_sdk_to_temp_dir(temp_dir: &Path) {
316 let asio_zip_path = temp_dir.join("asio_sdk.zip");
317 if host_os_is_windows() {
318 let status = Command::new("powershell")
319 .args([
320 "-NoProfile",
321 "-Command",
322 &format!(
323 "Invoke-WebRequest -Uri {ASIO_SDK_URL} -OutFile {}",
324 asio_zip_path.display()
325 ),
326 ])
327 .status()
328 .expect("Failed to execute PowerShell command");
329
330 if !status.success() {
331 panic!("Failed to download ASIO SDK");
332 }
333 println!("Downloaded ASIO SDK successfully");
334
335 // Unzip using PowerShell's Expand-Archive
336 println!("Extracting ASIO SDK..");
337 let status = Command::new("powershell")
338 .args([
339 "-NoProfile",
340 "-Command",
341 &format!(
342 "Expand-Archive -Path {} -DestinationPath {} -Force",
343 asio_zip_path.display(),
344 temp_dir.display()
345 ),
346 ])
347 .status()
348 .expect("Failed to execute PowerShell command for extracting ASIO SDK");
349
350 if !status.success() {
351 panic!("Failed to extract ASIO SDK");
352 }
353 } else {
354 let status = Command::new("sh")
355 .arg("-c")
356 .arg(&format!(
357 "curl -L --fail --output {} {}",
358 asio_zip_path.display(),
359 "https://www.steinberg.net/asiosdk" // Replace with the actual ASIO SDK URL
360 ))
361 .status()
362 .expect("Failed to execute curl command");
363
364 if !status.success() {
365 panic!("Failed to download ASIO SDK");
366 }
367 println!("Downloaded ASIO SDK successfully");
368
369 // Extract using `unzip`
370 println!("Extracting ASIO SDK..");
371 let status = Command::new("unzip")
372 .args([
373 "-o",
374 asio_zip_path.to_str().unwrap(),
375 "-d",
376 temp_dir.to_str().unwrap(),
377 ])
378 .status()
379 .expect("Failed to execute unzip command for extracting ASIO SDK");
380
381 if !status.success() {
382 panic!("Failed to extract ASIO SDK");
383 }
384 }
385}
386
387/// Invokes `vcvarsall.bat` to initialize the environment for building with MSVC
388///
389/// This function is only meant to be called when the host OS is Windows.
390fn invoke_vcvars_if_not_set() {
391 if vcvars_set() {
392 return;
393 }
394 println!("VCINSTALLDIR is not set. Attempting to invoke vcvarsall.bat..");
395
396 println!("Invoking vcvarsall.bat..");
397 println!("Determining system architecture..");
398
399 let arch_arg = determine_vcvarsall_bat_arch_arg();
400 println!(
401 "Host architecture is detected as {}.",
402 std::env::consts::ARCH
403 );
404 println!("Architecture argument for vcvarsall.bat will be used as: {arch_arg}.");
405
406 let vcvars_all_bat_path = search_vcvars_all_bat();
407
408 println!(
409 "Found vcvarsall.bat at {}. Initializing environment..",
410 vcvars_all_bat_path.display()
411 );
412
413 // Invoke vcvarsall.bat
414 let output = Command::new("cmd")
415 .args([
416 "/c",
417 vcvars_all_bat_path.to_str().unwrap(),
418 &arch_arg,
419 "&&",
420 "set",
421 ])
422 .output()
423 .expect("Failed to execute command");
424
425 for line in String::from_utf8_lossy(&output.stdout).lines() {
426 // Filters the output of vcvarsall.bat to only include lines of the form "VARNAME=VALUE"
427 let parts: Vec<&str> = line.splitn(2, '=').collect();
428 if parts.len() == 2 {
429 env::set_var(parts[0], parts[1]);
430 println!("{}={}", parts[0], parts[1]);
431 }
432 }
433}
434
435/// Checks if vcvarsall.bat has been invoked
436/// Assumes that it is very unlikely that the user would set `VCINSTALLDIR` manually
437fn vcvars_set() -> bool {
438 env::var("VCINSTALLDIR").is_ok()
439}
440
441/// Searches for vcvarsall.bat in the default installation directories
442///
443/// If it is not found, it will search for it in the Program Files directories
444///
445/// If it is still not found, it will panic.
446fn search_vcvars_all_bat() -> PathBuf {
447 if let Some(path) = guess_vcvars_all_bat() {
448 return path;
449 }
450
451 // Define search paths for vcvarsall.bat based on architecture
452 let paths = &[
453 // Visual Studio 2022+
454 "C:\\Program Files\\Microsoft Visual Studio\\",
455 // <= Visual Studio 2019
456 "C:\\Program Files (x86)\\Microsoft Visual Studio\\",
457 ];
458
459 // Search for vcvarsall.bat using walkdir
460 println!("Searching for vcvarsall.bat in {paths:?}");
461
462 let mut found = None;
463
464 for path in paths.iter() {
465 for entry in WalkDir::new(path)
466 .into_iter()
467 .filter_map(Result::ok)
468 .filter(|e| !e.file_type().is_dir())
469 {
470 if entry.path().ends_with("vcvarsall.bat") {
471 found.replace(entry.path().to_path_buf());
472 }
473 }
474 }
475
476 match found {
477 Some(path) => path,
478 None => panic!(
479 "Could not find vcvarsall.bat. Please install the latest version of Visual Studio."
480 ),
481 }
482}
483
484/// Guesses the location of vcvarsall.bat by searching it with certain heuristics.
485///
486/// It is meant to be executed before a top level search over Microsoft Visual Studio directories
487/// to ensure faster execution in CI environments.
488fn guess_vcvars_all_bat() -> Option<PathBuf> {
489 /// Checks if a string is a year
490 fn is_year(s: Option<&str>) -> Option<String> {
491 let Some(s) = s else {
492 return None;
493 };
494
495 if s.len() == 4 && s.chars().all(|c| c.is_ascii_digit()) {
496 Some(s.to_string())
497 } else {
498 None
499 }
500 }
501
502 /// Checks if a string is an edition of Visual Studio
503 fn is_edition(s: Option<&str>) -> Option<String> {
504 let Some(s) = s else {
505 return None;
506 };
507
508 let editions = ["Enterprise", "Professional", "Community", "Express"];
509 if editions.contains(&s) {
510 Some(s.to_string())
511 } else {
512 None
513 }
514 }
515
516 /// Constructs a path to vcvarsall.bat based on a base path
517 fn construct_path(base: &Path) -> Option<PathBuf> {
518 let mut constructed = base.to_path_buf();
519 for entry in WalkDir::new(&constructed).max_depth(1) {
520 let entry = match entry {
521 Err(_) => continue,
522 Ok(entry) => entry,
523 };
524 if let Some(year) = is_year(entry.path().file_name().and_then(|s| s.to_str())) {
525 constructed = constructed.join(year);
526 for entry in WalkDir::new(&constructed).max_depth(1) {
527 let entry = match entry {
528 Err(_) => continue,
529 Ok(entry) => entry,
530 };
531 if let Some(edition) =
532 is_edition(entry.path().file_name().and_then(|s| s.to_str()))
533 {
534 constructed = constructed
535 .join(edition)
536 .join("VC")
537 .join("Auxiliary")
538 .join("Build")
539 .join("vcvarsall.bat");
540
541 return Some(constructed);
542 }
543 }
544 }
545 }
546 None
547 }
548
549 let vs_2022_and_onwards_base = PathBuf::from("C:\\Program Files\\Microsoft Visual Studio\\");
550 let vs_2019_and_2017_base = PathBuf::from("C:\\Program Files (x86)\\Microsoft Visual Studio\\");
551
552 construct_path(&vs_2022_and_onwards_base).map_or_else(
553 || construct_path(&vs_2019_and_2017_base).map_or_else(|| None, Some),
554 Some,
555 )
556}
557
558/// Determines the right argument to pass to `vcvarsall.bat` based on the host and target architectures.
559///
560/// Windows on ARM is not supporting 32 bit arm processors.
561/// Because of this there is no native or cross compilation is supported for 32 bit arm processors.
562fn determine_vcvarsall_bat_arch_arg() -> String {
563 let host_architecture = std::env::consts::ARCH;
564 let target_architecture = std::env::var("CARGO_CFG_TARGET_ARCH").expect("Target not set.");
565
566 let arch_arg = if target_architecture == "x86_64" {
567 if host_architecture == "x86" {
568 // Arg for cross compilation from x86 to x64
569 "x86_amd64"
570 } else if host_architecture == "x86_64" {
571 // Arg for native compilation from x64 to x64
572 "amd64"
573 } else if host_architecture == "aarch64" {
574 // Arg for cross compilation from arm64 to amd64
575 "arm64_amd64"
576 } else {
577 panic!("Unsupported host architecture {}", host_architecture);
578 }
579 } else if target_architecture == "x86" {
580 if host_architecture == "x86" {
581 // Arg for native compilation from x86 to x86
582 "x86"
583 } else if host_architecture == "x86_64" {
584 // Arg for cross compilation from x64 to x86
585 "amd64_x86"
586 } else if host_architecture == "aarch64" {
587 // Arg for cross compilation from arm64 to x86
588 "arm64_x86"
589 } else {
590 panic!("Unsupported host architecture {}", host_architecture);
591 }
592 } else if target_architecture == "arm" {
593 if host_architecture == "x86" {
594 // Arg for cross compilation from x86 to arm
595 "x86_arm"
596 } else if host_architecture == "x86_64" {
597 // Arg for cross compilation from x64 to arm
598 "amd64_arm"
599 } else if host_architecture == "aarch64" {
600 // Arg for cross compilation from arm64 to arm
601 "arm64_arm"
602 } else {
603 panic!("Unsupported host architecture {}", host_architecture);
604 }
605 } else if target_architecture == "aarch64" {
606 if host_architecture == "x86" {
607 // Arg for cross compilation from x86 to arm
608 "x86_arm64"
609 } else if host_architecture == "x86_64" {
610 // Arg for cross compilation from x64 to arm
611 "amd64_arm64"
612 } else if host_architecture == "aarch64" {
613 // Arg for native compilation from arm64 to arm64
614 "arm64"
615 } else {
616 panic!("Unsupported host architecture {}", host_architecture);
617 }
618 } else {
619 panic!("Unsupported target architecture.");
620 };
621
622 arch_arg.to_owned()
623}