From 3a449538b7d263138455f5669685e7b2e50ac28d Mon Sep 17 00:00:00 2001 From: Ali Somay Date: Tue, 17 Oct 2023 12:13:30 +0200 Subject: [PATCH] Automate asio sys build (#804) --- README.md | 61 +++++++++-------- asio-sys/Cargo.toml | 4 +- asio-sys/build.rs | 161 +++++++++++++++++++++++++++++++++++++++++--- 3 files changed, 183 insertions(+), 43 deletions(-) diff --git a/README.md b/README.md index f1e7b0f04..ec76c8068 100644 --- a/README.md +++ b/README.md @@ -51,44 +51,46 @@ WASAPI including access to large numbers of channels and lower-latency audio processing. CPAL allows for using the ASIO SDK as the audio host on Windows instead of -WASAPI. To do so, follow these steps: - -1. **Download the ASIO SDK** `.zip` from [this - link](https://www.steinberg.net/en/company/developers.html). The version as - of writing this is 2.3.1. -2. Extract the files and place the directory somewhere you are happy for it to stay - (e.g. `~/.asio`). -3. Assign the full path of the directory (that contains the `readme`, `changes`, - `ASIO SDK 2.3` pdf, etc) to the `CPAL_ASIO_DIR` environment variable. This is - necessary for the `asio-sys` build script to build and bind to the SDK. -4. `bindgen`, the library used to generate bindings to the C++ SDK, requires +WASAPI. + +### Locating the ASIO SDK + +The location of ASIO SDK is exposed to CPAL by setting the `CPAL_ASIO_DIR` environment variable. + +The build script will try to find the ASIO SDK by following these steps in order: +1. Check if `CPAL_ASIO_DIR` is set and if so use the path to point to the SDK. +2. Check if the ASIO SDK is already installed in the temporary directory, if so use that and set the path of `CPAL_ASIO_DIR` to the output of `std::env::temp_dir().join("asio_sdk")`. +3. If the ASIO SDK is not already installed, download it from and install it in the temporary directory. The path of `CPAL_ASIO_DIR` will be set to the output of `std::env::temp_dir().join("asio_sdk")`. + +In an ideal situation you don't need to worry about this step. + +### Preparing the build environment + +1. `bindgen`, the library used to generate bindings to the C++ SDK, requires clang. **Download and install LLVM** from [here](http://releases.llvm.org/download.html) under the "Pre-Built Binaries" - section. The version as of writing this is 7.0.0. -5. Add the LLVM `bin` directory to a `LIBCLANG_PATH` environment variable. If + section. The version as of writing this is 17.0.1. +2. Add the LLVM `bin` directory to a `LIBCLANG_PATH` environment variable. If you installed LLVM to the default directory, this should work in the command prompt: ``` setx LIBCLANG_PATH "C:\Program Files\LLVM\bin" ``` -6. If you don't have any ASIO devices or drivers available, you can [**download +3. If you don't have any ASIO devices or drivers available, you can [**download and install ASIO4ALL**](http://www.asio4all.org/). Be sure to enable the "offline" feature during installation despite what the installer says about it being useless. -7. **Loading VCVARS**. `rust-bindgen` uses the C++ tool-chain when generating - bindings to the ASIO SDK. As a result, it is necessary to load some - environment variables in the command prompt that we used to build our project. - On 64-bit machines run: +4. Our build script assumes that Microsoft Visual Studio is installed. The script will try to find `vcvarsall.bat` + and execute it with the right machine architecture regardless of the Microsoft Visual Studio version. + If there are any errors encountered in this process which is unlikely, + you may find the `vcvarsall.bat` manually and execute it with your machine architecture as an argument. + The script will detect this and skip the step. + + A manually executed command example for 64 bit machines: ``` - "C:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\vcvarsall.bat" amd64 + "C:\Program Files (x86)\Microsoft Visual Studio\2019\Community\VC\Auxiliary\Build\vcvarsall.bat" amd64 ``` - On 32-bit machines run: - ``` - "C:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\vcvarsall.bat" x86 - ``` - Note that, depending on your version of Visual Studio, this script might be - in a slightly different location. -8. Select the ASIO host at the start of our program with the following code: +5. Select the ASIO host at the start of our program with the following code: ```rust let host; @@ -100,7 +102,7 @@ WASAPI. To do so, follow these steps: If you run into compilations errors produced by `asio-sys` or `bindgen`, make sure that `CPAL_ASIO_DIR` is set correctly and try `cargo clean`. -9. Make sure to enable the `asio` feature when building CPAL: +6. Make sure to enable the `asio` feature when building CPAL: ``` cargo build --features "asio" @@ -112,8 +114,5 @@ WASAPI. To do so, follow these steps: ```toml cpal = { version = "*", features = ["asio"] } ``` - -In the future we would like to work on automating this process to make it -easier, but we are not familiar enough with the ASIO license to do so yet. - + *Updated as of ASIO version 2.3.3.* diff --git a/asio-sys/Cargo.toml b/asio-sys/Cargo.toml index 7b6b0df9b..30d3ff07e 100644 --- a/asio-sys/Cargo.toml +++ b/asio-sys/Cargo.toml @@ -10,9 +10,9 @@ keywords = ["audio", "sound", "asio", "steinberg"] build = "build.rs" [target.'cfg(any(target_os = "windows"))'.build-dependencies] -bindgen = "0.64" +bindgen = "0.68" walkdir = "2" -cc = "1.0.25" +cc = "1.0.83" [dependencies] once_cell = "1.12" diff --git a/asio-sys/build.rs b/asio-sys/build.rs index 10e7a23f8..68bc72f67 100644 --- a/asio-sys/build.rs +++ b/asio-sys/build.rs @@ -4,9 +4,11 @@ extern crate walkdir; use std::env; use std::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"; @@ -15,24 +17,22 @@ const ASIO_DRIVERS_HEADER: &str = "asiodrivers.h"; fn main() { println!("cargo:rerun-if-env-changed={}", CPAL_ASIO_DIR); - // If ASIO directory isn't set silently return early - let cpal_asio_dir_var = match env::var(CPAL_ASIO_DIR) { - Err(_) => return, - Ok(var) => var, - }; - - // Asio directory - let cpal_asio_dir = PathBuf::from(cpal_asio_dir_var); + // ASIO SDK directory + let cpal_asio_dir = get_asio_dir(); println!("cargo:rerun-if-changed={}", cpal_asio_dir.display()); // Directory where bindings and library are created let out_dir = PathBuf::from(env::var("OUT_DIR").expect("bad path")); - // Check if library exists - // If it doesn't create it + // Check if library exists, + // if it doesn't create it let mut lib_path = out_dir.clone(); lib_path.push("libasio.a"); if !lib_path.exists() { + if !vcvars_set() { + println!("VCINSTALLDIR is not set. Attempting to invoke vcvarsall.bat.."); + invoke_vcvars(); + } create_lib(&cpal_asio_dir); } @@ -48,6 +48,10 @@ fn main() { let mut binding_path = out_dir.clone(); binding_path.push("asio_bindings.rs"); if !binding_path.exists() { + if !vcvars_set() { + println!("VCINSTALLDIR is not set. Attempting to invoke vcvarsall.bat.."); + invoke_vcvars(); + } create_bindings(&cpal_asio_dir); } } @@ -207,3 +211,140 @@ fn create_bindings(cpal_asio_dir: &PathBuf) { .write_to_file(out_path.join("asio_bindings.rs")) .expect("Couldn't write bindings!"); } + +fn get_asio_dir() -> PathBuf { + // Check if CPAL_ASIO_DIR env var is set + if let Ok(path) = env::var(CPAL_ASIO_DIR) { + println!("CPAL_ASIO_DIR is set at {path}"); + return PathBuf::from(path); + } + + // If not set, check temp directory for ASIO SDK, maybe it is previously downloaded + 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; + } + + // If not found, download ASIO SDK using PowerShell's Invoke-WebRequest + println!("CPAL_ASIO_DIR is not set or contents are cached downloading from {ASIO_SDK_URL}",); + + let asio_zip_path = temp_dir.join("asio_sdk.zip"); + 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"); + + // Unzip using PowerShell's Expand-Archive + 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"); + } + + // Move the contents of the inner directory to asio_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 invoke_vcvars() { + println!("Invoking vcvarsall.bat.."); + println!("Determining system architecture.."); + + // Determine the system architecture to be used as an argument to vcvarsall.bat + let arch = if cfg!(target_arch = "x86_64") { + "amd64" + } else if cfg!(target_arch = "x86") { + "x86" + } else if cfg!(target_arch = "arm") { + "arm" + } else if cfg!(target_arch = "aarch64") { + "arm64" + } else { + panic!("Unsupported architecture"); + }; + + println!("Architecture detected as {arch}."); + + // Define search paths for vcvarsall.bat based on architecture + let paths = if arch == "amd64" { + vec![ + "C:\\Program Files (x86)\\Microsoft Visual Studio\\", + "C:\\Program Files\\Microsoft Visual Studio\\", + ] + } else { + vec!["C:\\Program Files\\Microsoft Visual Studio\\"] + }; + + // Search for vcvarsall.bat using walkdir + println!("Searching for vcvarsall.bat in {paths:?}"); + 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") { + println!( + "Found vcvarsall.bat at {}. Initializing environment..", + entry.path().display() + ); + + // Invoke vcvarsall.bat + let output = Command::new("cmd") + .args(&["/c", entry.path().to_str().unwrap(), &arch, "&&", "set"]) + .output() + .expect("Failed to execute command"); + + for line in String::from_utf8_lossy(&output.stdout).lines() { + // Filters the output of vcvarsall.bat to only include lines of the form "VARNAME=VALUE" + let parts: Vec<&str> = line.splitn(2, '=').collect(); + if parts.len() == 2 { + env::set_var(parts[0], parts[1]); + println!("{}={}", parts[0], parts[1]); + } + } + return; + } + } + } + + panic!("Could not find vcvarsall.bat. Please install the latest version of Visual Studio."); +} +// Checks if vcvarsall.bat has been invoked +// Assumes that it is very unlikely that the user would set VCINSTALLDIR manually +fn vcvars_set() -> bool { + env::var("VCINSTALLDIR").is_ok() +}