use std::path::{Path, PathBuf};
use std::process::Command;
use anyhow::{Context, Result};
use cmake_package::{Error as CmakeError, Version, VersionError, find_package};
const CUVS_COMPONENT: &str = "c_api";
const CUVS_C_API_TARGET: &str = "cuvs::c_api";
const CUVS_CMAKE_INSPECTION_FAILED: &str = "CMake failed while inspecting cuVS. Check the build environment for missing tools such as ninja/make, C/C++ compilers, or CUDA dependencies.";
const PACKAGE_VERSION: &str = env!("CARGO_PKG_VERSION");
const PYTHON_PRINT_LIBCUVS_PACKAGE_DIR: &str = r#"
from importlib.util import find_spec
from pathlib import Path
spec = find_spec("libcuvs")
if spec is None or spec.submodule_search_locations is None:
raise ModuleNotFoundError("libcuvs")
print(Path(next(iter(spec.submodule_search_locations))).resolve())
"#;
pub(crate) struct CuvsMetadata {
pub(crate) include_dir: PathBuf,
#[cfg(feature = "generate-bindings")]
pub(crate) bindgen_include_dirs: Vec<PathBuf>,
pub(crate) lib_dir: PathBuf,
}
fn cmake_unavailable_error() -> anyhow::Error {
anyhow::anyhow!(
"CMake is not installed or does not satisfy this build's requirements. Install the required CMake version and try again."
)
}
fn cuvs_package_not_found_error() -> anyhow::Error {
anyhow::anyhow!(
"Could not find a cuVS CMake package compatible with cuvs-sys {PACKAGE_VERSION}.\n\n\
Install cuVS via one of:\n\
- conda: conda install -c rapidsai libcuvs\n\
- pip: pip install libcuvs-cu<CUDA_VERSION> and set LIBCUVS_USE_PYTHON=1\n\
Or set CMAKE_PREFIX_PATH to point to your cuVS build/install directory."
)
}
fn cuvs_incompatible_version_error(
required_version: &Version,
candidates: &[cmake_package::Considered],
) -> anyhow::Error {
let considered = candidates
.iter()
.map(|candidate| format!("- {} (version: {})", candidate.config, candidate.version))
.collect::<Vec<_>>()
.join("\n");
anyhow::anyhow!(
"Found cuVS CMake package candidates, but none are compatible with cuvs-sys {PACKAGE_VERSION}.\n\n\
Required compatibility: same major/minor as {required_version} and not older than {required_version}.\n\n\
Considered candidates:\n{considered}"
)
}
fn cmake_package_error(error: CmakeError, required_version: &Version) -> anyhow::Error {
match error {
CmakeError::CMakeNotFound | CmakeError::UnsupportedCMakeVersion => {
cmake_unavailable_error()
}
CmakeError::PackageNotFound => cuvs_package_not_found_error(),
CmakeError::Version(VersionError::VersionIncompatible(candidates)) => {
cuvs_incompatible_version_error(required_version, &candidates)
}
CmakeError::Version(VersionError::InvalidVersion) => anyhow::anyhow!(
"{CUVS_CMAKE_INSPECTION_FAILED}\n\nCMake reported an invalid cuVS package version."
),
CmakeError::IO(error) => {
anyhow::anyhow!("{CUVS_CMAKE_INSPECTION_FAILED}\n\nUnderlying error: {error}")
}
CmakeError::Internal => anyhow::anyhow!("{CUVS_CMAKE_INSPECTION_FAILED}"),
}
}
fn find_target(
package: &cmake_package::CMakePackage,
target_name: &str,
) -> Result<cmake_package::CMakeTarget> {
package.target(target_name).with_context(|| {
format!("Found CMake package {}, but target {target_name} was not exported.", package.name)
})
}
fn find_cuvs_package(
required_version: &Version,
python_package_dir: Option<&Path>,
) -> Result<cmake_package::CMakePackage> {
let mut package = find_package("cuvs")
.version(*required_version)
.components([CUVS_COMPONENT.to_owned()])
.define("CMAKE_FIND_PACKAGE_PREFER_CONFIG", "TRUE");
if let Some(python_package_dir) = python_package_dir {
package = package.prefix_paths(vec![python_package_dir.to_path_buf()]);
}
package.find().map_err(|error| cmake_package_error(error, required_version))
}
#[cfg(feature = "generate-bindings")]
fn find_dlpack_package() -> Result<cmake_package::CMakePackage> {
find_package("dlpack").find().map_err(|e| match e {
CmakeError::CMakeNotFound | CmakeError::UnsupportedCMakeVersion => {
cmake_unavailable_error()
}
_ => anyhow::anyhow!(
"Could not find DLPack CMake package.\n\n\
Install DLPack so that `find_package(dlpack)` succeeds."
),
})
}
pub(crate) fn try_find_cuvs_package(
required_version: &Version,
python_package_dir: Option<&Path>,
) -> Result<CuvsMetadata> {
let package = find_cuvs_package(required_version, python_package_dir)?;
let target = find_target(&package, CUVS_C_API_TARGET)?;
let include_dir = target
.include_directories
.first()
.map(PathBuf::from)
.context("cuVS CMake target did not export any include directories")?;
#[cfg(feature = "generate-bindings")]
let bindgen_include_dirs: Vec<_> = {
let dlpack = find_dlpack_package()?;
let dlpack_target = find_target(&dlpack, "dlpack::dlpack")?;
dlpack_target
.include_directories
.iter()
.map(PathBuf::from)
.filter(|dir| dir.is_dir())
.filter(|dir| dir != &include_dir)
.collect()
};
let lib_dir = target
.location
.as_deref()
.and_then(|location| Path::new(location).parent())
.map(Path::to_path_buf)
.or_else(|| target.link_directories.first().map(PathBuf::from))
.context("cuVS CMake target did not export a library location or link directory")?;
target.link();
Ok(CuvsMetadata {
include_dir,
#[cfg(feature = "generate-bindings")]
bindgen_include_dirs,
lib_dir,
})
}
fn find_python_cuvs_package_dir() -> Result<PathBuf> {
let python =
Path::new(if std::env::var_os("VIRTUAL_ENV").is_some() { "python" } else { "python3" });
let output = Command::new(python)
.arg("-c")
.arg(PYTHON_PRINT_LIBCUVS_PACKAGE_DIR)
.output()
.with_context(|| format!("LIBCUVS_USE_PYTHON is set, but failed to run {:?}.", python))?;
anyhow::ensure!(
output.status.success(),
"LIBCUVS_USE_PYTHON is set, but {:?} could not locate the Python libcuvs package.\n\n\
Install the libcuvs wheel in that Python environment, or unset LIBCUVS_USE_PYTHON.\n\n\
{}",
python,
String::from_utf8_lossy(&output.stderr).trim()
);
let package_dir = PathBuf::from(String::from_utf8_lossy(&output.stdout).trim());
let cmake_dir = package_dir.join("lib64/cmake/cuvs");
anyhow::ensure!(
cmake_dir.is_dir(),
"LIBCUVS_USE_PYTHON is set, but the Python libcuvs package at {} does not contain a cuVS CMake package under {}.",
package_dir.display(),
cmake_dir.display(),
);
Ok(package_dir)
}
pub(crate) fn locate_cuvs() -> Result<CuvsMetadata> {
let required_version: Version = PACKAGE_VERSION
.try_into()
.expect("workspace package version must be a valid semantic version");
let python_package_dir = std::env::var_os("LIBCUVS_USE_PYTHON")
.map(|_| find_python_cuvs_package_dir())
.transpose()?;
try_find_cuvs_package(&required_version, python_package_dir.as_deref())
}