Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Automate asio sys build #804

Merged
merged 6 commits into from
Oct 17, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 30 additions & 31 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <https://www.steinberg.net/asiosdk> 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;
Expand All @@ -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"
Expand All @@ -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.*
4 changes: 2 additions & 2 deletions asio-sys/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
161 changes: 151 additions & 10 deletions asio-sys/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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);
}

Expand All @@ -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);
}
}
Expand Down Expand Up @@ -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()
}
Loading