diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index c3bd6c7..fe62039 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -15,7 +15,6 @@ jobs: env: working-dir: ./bindings/rust NYXSTONE_LLVM_PREFIX: "/usr/lib/llvm-15/" - NYXSTONE_LINK_FFI: "1" steps: - uses: actions/checkout@v4 - name: Packages @@ -43,10 +42,10 @@ jobs: - name: Packages run: brew install llvm@15 - name: Build - run: RUSTFLAGS="-L$(brew --prefix zstd)/lib" NYXSTONE_LLVM_PREFIX="$(brew --prefix llvm@15)" NYXSTONE_LINK_FFI=1 cargo build + run: RUSTFLAGS="-L$(brew --prefix zstd)/lib" NYXSTONE_LLVM_PREFIX="$(brew --prefix llvm@15)" cargo build working-directory: ${{ env.working-dir }} - name: Run tests - run: RUSTFLAGS="-L$(brew --prefix zstd)/lib" RUSTDOCFLAGS="-L$(brew --prefix zstd)/lib" NYXSTONE_LLVM_PREFIX="$(brew --prefix llvm@15)" NYXSTONE_LINK_FFI=1 cargo test + run: RUSTFLAGS="-L$(brew --prefix zstd)/lib" RUSTDOCFLAGS="-L$(brew --prefix zstd)/lib" NYXSTONE_LLVM_PREFIX="$(brew --prefix llvm@15)" cargo test working-directory: ${{ env.working-dir }} mac-llvm-16: @@ -58,10 +57,10 @@ jobs: - name: Packages run: brew install llvm@16 - name: Build - run: RUSTFLAGS="-L$(brew --prefix zstd)/lib" NYXSTONE_LLVM_PREFIX="$(brew --prefix llvm@16)" NYXSTONE_LINK_FFI=1 cargo build + run: RUSTFLAGS="-L$(brew --prefix zstd)/lib" NYXSTONE_LLVM_PREFIX="$(brew --prefix llvm@16)" cargo build working-directory: ${{ env.working-dir }} - name: Run tests - run: RUSTFLAGS="-L$(brew --prefix zstd)/lib" RUSTDOCFLAGS="-L$(brew --prefix zstd)/lib" NYXSTONE_LLVM_PREFIX="$(brew --prefix llvm@16)" NYXSTONE_LINK_FFI=1 cargo test + run: RUSTFLAGS="-L$(brew --prefix zstd)/lib" RUSTDOCFLAGS="-L$(brew --prefix zstd)/lib" NYXSTONE_LLVM_PREFIX="$(brew --prefix llvm@16)" cargo test working-directory: ${{ env.working-dir }} mac-llvm-17: @@ -73,10 +72,10 @@ jobs: - name: Packages run: brew install llvm@17 - name: Build - run: RUSTFLAGS="-L$(brew --prefix zstd)/lib" NYXSTONE_LLVM_PREFIX="$(brew --prefix llvm@17)" NYXSTONE_LINK_FFI=1 cargo build + run: RUSTFLAGS="-L$(brew --prefix zstd)/lib" NYXSTONE_LLVM_PREFIX="$(brew --prefix llvm@17)" cargo build working-directory: ${{ env.working-dir }} - name: Run tests - run: RUSTFLAGS="-L$(brew --prefix zstd)/lib" RUSTDOCFLAGS="-L$(brew --prefix zstd)/lib" NYXSTONE_LLVM_PREFIX="$(brew --prefix llvm@17)" NYXSTONE_LINK_FFI=1 cargo test + run: RUSTFLAGS="-L$(brew --prefix zstd)/lib" RUSTDOCFLAGS="-L$(brew --prefix zstd)/lib" NYXSTONE_LLVM_PREFIX="$(brew --prefix llvm@17)" cargo test working-directory: ${{ env.working-dir }} mac-llvm-18: @@ -88,9 +87,9 @@ jobs: - name: Packages run: brew install llvm@18 - name: Build - run: RUSTFLAGS="-L$(brew --prefix zstd)/lib" NYXSTONE_LLVM_PREFIX="$(brew --prefix llvm@18)" NYXSTONE_LINK_FFI=1 cargo build + run: RUSTFLAGS="-L$(brew --prefix zstd)/lib" NYXSTONE_LLVM_PREFIX="$(brew --prefix llvm@18)" cargo build working-directory: ${{ env.working-dir }} - name: Run tests - run: RUSTFLAGS="-L$(brew --prefix zstd)/lib" RUSTDOCFLAGS="-L$(brew --prefix zstd)/lib" NYXSTONE_LLVM_PREFIX="$(brew --prefix llvm@18)" NYXSTONE_LINK_FFI=1 cargo test + run: RUSTFLAGS="-L$(brew --prefix zstd)/lib" RUSTDOCFLAGS="-L$(brew --prefix zstd)/lib" NYXSTONE_LLVM_PREFIX="$(brew --prefix llvm@18)" cargo test working-directory: ${{ env.working-dir }} diff --git a/bindings/rust/Cargo.toml b/bindings/rust/Cargo.toml index 806db84..814d442 100644 --- a/bindings/rust/Cargo.toml +++ b/bindings/rust/Cargo.toml @@ -24,6 +24,7 @@ clap = { version = "4.5", features = ["derive"] } [build-dependencies] cxx-build = "1.0.94" anyhow = { version = "1.0.68", default-features = true } +cmake-package = "0.1.2" [lib] path = "src/lib.rs" diff --git a/bindings/rust/README.md b/bindings/rust/README.md index 5d12963..c96ac44 100644 --- a/bindings/rust/README.md +++ b/bindings/rust/README.md @@ -7,11 +7,10 @@ Official bindings for the Nyxstone assembler/disassembler engine. ## Building -The project can be build via `cargo build`, as long as LLVM with a major version in the range 15-18 is installed in the `$PATH` or the environment variable `$NYXSTONE_LLVM_PREFIX` points to the installation location of a LLVM library. - -LLVM might be linked against FFI, but not correctly report this fact via `llvm-config`. If your LLVM does link FFI and -Nyxstone fails to run, set the `NYXSTONE_LINK_FFI` environment variable to `1`, which will ensure that Nyxstone -links against `libffi`. +The project can be build via `cargo build`, as long as LLVM with a major version in the range 15 to 18 is availabe to CMake +and cmake is installed on the system. If your installation of LLVM is installed in a non-standard location, you can set +`NYXSTONE_LLVM_PREFIX` to tell Nyxstone or use the `CMAKE_INCLUDE_PATH` environment variable to add the directory to the +CMake search path. ## Installation diff --git a/bindings/rust/build.rs b/bindings/rust/build.rs index 81d06bb..9af692a 100644 --- a/bindings/rust/build.rs +++ b/bindings/rust/build.rs @@ -1,11 +1,9 @@ -use anyhow::{anyhow, ensure, Context, Result}; use std::env; -use std::ffi::OsStr; -use std::path::{Path, PathBuf}; -use std::process::{Command, Output}; -const ENV_LLVM_PREFIX: &str = "NYXSTONE_LLVM_PREFIX"; -const ENV_FORCE_FFI_LINKING: &str = "NYXSTONE_LINK_FFI"; +use cmake_package::find_package; + +const SEARCH_ENV_VAR_NAME: &str = "NYXSTONE_LLVM_PREFIX"; +const CMAKE_SEARCH_NAME: &str = "CMAKE_INCLUDE_PATH"; fn main() { let headers = [ @@ -24,80 +22,69 @@ fn main() { "src/nyxstone_ffi.cpp", ]; - // Tell cargo about NYXSTONE_LLVM_PREFIX - println!("cargo:rerun-if-env-changed={}", ENV_LLVM_PREFIX); - if let Ok(path) = env::var(ENV_LLVM_PREFIX) { - println!("cargo:rerun-if-changed={}", path); - } - // Exit early if we are in the docs.rs builder, since we do not need to build the c++ files or link against llvm. if std::env::var("DOCS_RS").is_ok() { return; } - // Commented, because it is not currently needed, but might in the future: - // The cxxbridge include dir is required to include auto generated c++ header files, - // that contains shared data types defined in the rust part of the CXX bridge. - - // Generate path for CXX bridge generated files: project_name/target/cxxbridge - // let out_dir = std::env::var("OUT_DIR").unwrap(); - // let cxxbridge_dir = out_dir + "/../../../../cxxbridge"; - - // === The following code is adapted from llvm-sys, see below for licensing === - let llvm_config_path = match search_llvm_config() { - Ok(config) => config, - Err(e) => panic!("{e} Please either install LLVM version >= 15 into your PATH or supply the location via $NYXSTONE_LLVM_PREFIX"), - }; - - // Tell cargo about the library directory of llvm. - let libdir = llvm_config(&llvm_config_path, ["--libdir"]); - - // Export information to other crates - println!("cargo:config_path={}", llvm_config_path.display()); // will be DEP_LLVM_CONFIG_PATH - println!("cargo:libdir={}", libdir); // DEP_LLVM_LIBDIR - - // Link LLVM libraries - println!("cargo:rustc-link-search=native={}", libdir); - for link_search_dir in get_system_library_dirs() { - println!("cargo:rustc-link-search=native={}", link_search_dir); - } - // We need to take note of what kind of libraries we linked to, so that - // we can link to the same kind of system libraries - let (kind, libs) = get_link_libraries(&llvm_config_path); - for name in libs { - println!("cargo:rustc-link-lib={}={}", kind.string(), name); + // Tell cargo about the relevant environment variables + println!("cargo:rerun-if-env-changed={}", SEARCH_ENV_VAR_NAME); + println!("cargo:rerun-if-env-changed={}", CMAKE_SEARCH_NAME); + if let Ok(search) = env::var(SEARCH_ENV_VAR_NAME) { + unsafe { + if let Ok(current_search_paths) = env::var(CMAKE_SEARCH_NAME) { + let separator = if cfg!(windows) { ";" } else { ":" }; + env::set_var(CMAKE_SEARCH_NAME, current_search_paths + separator + &search); + } else { + env::set_var(CMAKE_SEARCH_NAME, search); + } + } } - // Link system libraries - // We get the system libraries based on the kind of LLVM libraries we link to, but we link to - // system libs based on the target environment. - let sys_lib_kind = LibraryKind::Dynamic; - for name in get_system_libraries(&llvm_config_path, kind) { - println!("cargo:rustc-link-lib={}={}", sys_lib_kind.string(), name); - } + // Get the include directory for the c++ code. + const LLVM_VERSIONS: [&str; 5] = ["15", "16", "17", "18.0", "18.1"]; + let llvm = LLVM_VERSIONS + .into_iter() + // Always use the newest version available + .rev() + .map(|version| { + find_package("LLVM") + .components([ + "core".into(), + "mc".into(), + "AllTargetsCodeGens".into(), + "AllTargetsAsmParsers".into(), + "AllTargetsDescs".into(), + "AllTargetsDisassemblers".into(), + "AllTargetsInfos".into(), + "AllTargetsMCAs".into(), + ]) + .version(version) + .find() + }) + .find_map(Result::ok) + .expect("Could not find LLVM with version 15-18"); - if target_env_is("msvc") && is_llvm_debug(&llvm_config_path) { - println!("cargo:rustc-link-lib=msvcrtd"); - } + println!( + "cargo:warning=LLVM version: {}", + llvm.version.as_ref().expect("LLVM version was requested") + ); - // Sometimes, llvm-config might not report that ffi is needed as a system library. - // There is no way to detect this, thus the user must notify us via the environment - // variable. - if env::var(ENV_FORCE_FFI_LINKING).is_ok() { - println!("cargo:rustc-link-lib=dylib=ffi"); - } + let llvm = llvm + .target("LLVM") + .expect("The target name LLVM was not found in the LLVM cmake package, please report a bug at https://github.com/emproof-com/nyxstone"); - // ==================================================== + println!("cargo:warning=LLVM libraries: {:?}", llvm.link_libraries); - // Get the include directory for the c++ code. - let llvm_include_dir = llvm_config(&llvm_config_path, ["--includedir"]); + // Tell cargo about the libraries needed by LLVM. + llvm.link(); // Import Nyxstone C++ lib cxx_build::bridge("src/lib.rs") .std("c++17") .include("nyxstone/include") .include("nyxstone/vendor") - .include(llvm_include_dir.trim()) + .includes(llvm.include_directories) // .include(cxxbridge_dir) .files(sources) .compile("nyxstone_wrap"); @@ -107,366 +94,3 @@ fn main() { println!("cargo:rerun-if-changed={}", file); } } - -/// Searches for LLVM in $NYXSTONE_LLVM_PREFIX/bin and in the path and ensures proper version -/// # Returns -/// `Ok()` and PathBuf to llvm-config, `Err()` otherwise -fn search_llvm_config() -> Result { - let prefix = env::var(ENV_LLVM_PREFIX) - .and_then(|p| { - if p.is_empty() { - return Err(std::env::VarError::NotPresent); - } - - Ok(PathBuf::from(p).join("bin")) - }) - .unwrap_or_else(|_| PathBuf::new()); - - for name in llvm_config_binary_names() { - let llvm_config = Path::new(&prefix).join(name); - - let Ok(version) = get_major_version(&llvm_config) else { - continue; - }; - - let version = version.parse::().context("Parsing LLVM version")?; - - ensure!( - (15..=18).contains(&version), - "LLVM major version is {}, must be 15-18.", - version - ); - - return Ok(llvm_config); - } - - Err(anyhow!( - "No llvm-config found in {}", - if prefix == PathBuf::new() { - "$PATH" - } else { - "$NYXSTONE_LLVM_PREFIX" - } - )) -} - -fn get_major_version(binary: &Path) -> Result { - Ok(llvm_config_ex(binary, ["--version"]) - .context("Extracting LLVM major version")? - .split('.') - .next() - .expect("Unexpected llvm-config output.") - .to_owned()) -} - -// All following functions taken from llvm-sys crate and are licensed according to the following -// license: -// Copyright (c) 2015 Peter Marheine -// Permission is hereby granted, free of charge, to any person obtaining a copy of -// this software and associated documentation files (the "Software"), to deal in -// the Software without restriction, including without limitation the rights to -// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies -// of the Software, and to permit persons to whom the Software is furnished to do -// so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. - -fn target_env_is(name: &str) -> bool { - match env::var_os("CARGO_CFG_TARGET_ENV") { - Some(s) => s == name, - None => false, - } -} - -fn target_os_is(name: &str) -> bool { - match env::var_os("CARGO_CFG_TARGET_OS") { - Some(s) => s == name, - None => false, - } -} - -/// Return an iterator over possible names for the llvm-config binary. -fn llvm_config_binary_names() -> impl Iterator { - let base_names = (15..=18) - .flat_map(|version| { - [ - format!("llvm-config-{}", version), - format!("llvm-config{}", version), - format!("llvm{}-config", version), - ] - }) - .chain(["llvm-config".into()]) - .collect::>(); - - // On Windows, also search for llvm-config.exe - if target_os_is("windows") { - IntoIterator::into_iter(base_names) - .flat_map(|name| [format!("{}.exe", name), name]) - .collect::>() - } else { - base_names.to_vec() - } - .into_iter() -} - -/// Invoke the specified binary as llvm-config. -fn llvm_config(binary: &Path, args: I) -> String -where - I: IntoIterator, - S: AsRef, -{ - llvm_config_ex(binary, args).expect("Surprising failure from llvm-config") -} - -/// Invoke the specified binary as llvm-config. -/// -/// Explicit version of the `llvm_config` function that bubbles errors -/// up. -fn llvm_config_ex(binary: &Path, args: I) -> anyhow::Result -where - I: IntoIterator, - S: AsRef, -{ - let mut cmd = Command::new(binary); - (|| { - let Output { status, stdout, stderr } = cmd.args(args).output()?; - let stdout = String::from_utf8(stdout).context("stdout")?; - let stderr = String::from_utf8(stderr).context("stderr")?; - if status.success() { - Ok(stdout) - } else { - Err(anyhow::anyhow!( - "status={status}\nstdout={}\nstderr={}", - stdout.trim(), - stderr.trim() - )) - } - })() - .with_context(|| format!("{cmd:?}")) -} - -/// Get the names of the dylibs required by LLVM, including the C++ standard -/// library. -fn get_system_libraries(llvm_config_path: &Path, kind: LibraryKind) -> Vec { - let link_arg = match kind { - LibraryKind::Static => "--link-static", - LibraryKind::Dynamic => "--link-shared", - }; - - llvm_config(llvm_config_path, ["--system-libs", link_arg]) - .split(&[' ', '\n'] as &[char]) - .filter(|s| !s.is_empty()) - .map(|flag| { - if target_env_is("msvc") { - // Same as --libnames, foo.lib - flag.strip_suffix(".lib") - .unwrap_or_else(|| panic!("system library '{}' does not appear to be a MSVC library file", flag)) - } else { - if let Some(flag) = flag.strip_prefix("-l") { - // Linker flags style, -lfoo - if target_os_is("macos") { - // .tdb libraries are "text-based stub" files that provide lists of symbols, - // which refer to libraries shipped with a given system and aren't shipped - // as part of the corresponding SDK. They're named like the underlying - // library object, including the 'lib' prefix that we need to strip. - if let Some(flag) = flag.strip_prefix("lib").and_then(|flag| flag.strip_suffix(".tbd")) { - return flag; - } - } - - if let Some(i) = flag.find(".so.") { - // On some distributions (OpenBSD, perhaps others), we get sonames - // like "-lz.so.7.0". Correct those by pruning the file extension - // and library version. - return &flag[..i]; - } - return flag; - } - - let maybe_lib = Path::new(flag); - if maybe_lib.is_file() { - // Library on disk, likely an absolute path to a .so. We'll add its location to - // the library search path and specify the file as a link target. - println!("cargo:rustc-link-search={}", maybe_lib.parent().unwrap().display()); - - // Expect a file named something like libfoo.so, or with a version libfoo.so.1. - // Trim everything after and including the last .so and remove the leading 'lib' - let soname = maybe_lib - .file_name() - .unwrap() - .to_str() - .expect("Shared library path must be a valid string"); - let (stem, _rest) = soname - .rsplit_once(target_dylib_extension()) - .expect("Shared library should be a .so file"); - - stem.strip_prefix("lib") - .unwrap_or_else(|| panic!("system library '{}' does not have a 'lib' prefix", soname)) - } else { - panic!("Unable to parse result of llvm-config --system-libs: {}", flag) - } - } - }) - .chain(get_system_libcpp()) - .map(str::to_owned) - .collect() -} - -/// Return additional linker search paths that should be used but that are not discovered -/// by other means. -/// -/// In particular, this should include only directories that are known from platform-specific -/// knowledge that aren't otherwise discovered from either `llvm-config` or a linked library -/// that includes an absolute path. -fn get_system_library_dirs() -> impl IntoIterator { - if target_os_is("openbsd") { - Some("/usr/local/lib") - } else { - None - } -} - -fn target_dylib_extension() -> &'static str { - if target_os_is("macos") { - ".dylib" - } else { - ".so" - } -} - -/// Get the library that must be linked for C++, if any. -fn get_system_libcpp() -> Option<&'static str> { - if target_env_is("msvc") { - // MSVC doesn't need an explicit one. - None - } else if target_os_is("macos") { - // On OS X 10.9 and later, LLVM's libc++ is the default. On earlier - // releases GCC's libstdc++ is default. Unfortunately we can't - // reasonably detect which one we need (on older ones libc++ is - // available and can be selected with -stdlib=lib++), so assume the - // latest, at the cost of breaking the build on older OS releases - // when LLVM was built against libstdc++. - Some("c++") - } else if target_os_is("freebsd") || target_os_is("openbsd") { - Some("c++") - } else if target_env_is("musl") { - // The one built with musl. - Some("c++") - } else { - // Otherwise assume GCC's libstdc++. - // This assumption is probably wrong on some platforms, but would need - // testing on them. - Some("stdc++") - } -} - -#[derive(Clone, Copy, PartialEq, Eq)] -enum LibraryKind { - Static, - Dynamic, -} - -impl LibraryKind { - pub fn string(&self) -> &'static str { - match self { - LibraryKind::Static => "static", - LibraryKind::Dynamic => "dylib", - } - } -} - -/// Get the names of libraries to link against, along with whether it is static or shared library. -fn get_link_libraries(llvm_config_path: &Path) -> (LibraryKind, Vec) { - // Using --libnames in conjunction with --libdir is particularly important - // for MSVC when LLVM is in a path with spaces, but it is generally less of - // a hack than parsing linker flags output from --libs and --ldflags. - - fn get_link_libraries_impl(llvm_config_path: &Path, kind: LibraryKind) -> anyhow::Result { - // Windows targets don't get dynamic support. - // See: https://gitlab.com/taricorp/llvm-sys.rs/-/merge_requests/31#note_1306397918 - if target_env_is("msvc") && kind == LibraryKind::Dynamic { - anyhow::bail!("Dynamic linking to LLVM is not supported on Windows"); - } - - let link_arg = match kind { - LibraryKind::Static => "--link-static", - LibraryKind::Dynamic => "--link-shared", - }; - llvm_config_ex(llvm_config_path, ["--libnames", link_arg]) - } - - // Prefer static linking - let preferences = [LibraryKind::Static, LibraryKind::Dynamic]; - - for kind in preferences { - match get_link_libraries_impl(llvm_config_path, kind) { - Ok(s) => return (kind, extract_library(&s, kind)), - Err(err) => { - println!("failed to get {} libraries from llvm-config: {err:?}", kind.string()) - } - } - } - - panic!("failed to get linking libraries from llvm-config",); -} - -fn extract_library(s: &str, kind: LibraryKind) -> Vec { - s.split(&[' ', '\n'] as &[char]) - .filter(|s| !s.is_empty()) - .map(|name| { - // --libnames gives library filenames. Extract only the name that - // we need to pass to the linker. - match kind { - LibraryKind::Static => { - // Match static library - if let Some(name) = name.strip_prefix("lib").and_then(|name| name.strip_suffix(".a")) { - // Unix (Linux/Mac) - // libLLVMfoo.a - name - } else if let Some(name) = name.strip_suffix(".lib") { - // Windows - // LLVMfoo.lib - name - } else { - panic!("'{}' does not look like a static library name", name) - } - } - LibraryKind::Dynamic => { - // Match shared library - if let Some(name) = name.strip_prefix("lib").and_then(|name| name.strip_suffix(".dylib")) { - // Mac - // libLLVMfoo.dylib - name - } else if let Some(name) = name.strip_prefix("lib").and_then(|name| name.strip_suffix(".so")) { - // Linux - // libLLVMfoo.so - name - } else if let Some(name) = - IntoIterator::into_iter([".dll", ".lib"]).find_map(|suffix| name.strip_suffix(suffix)) - { - // Windows - // LLVMfoo.{dll,lib} - name - } else { - panic!("'{}' does not look like a shared library name", name) - } - } - } - .to_string() - }) - .collect::>() -} - -fn is_llvm_debug(llvm_config_path: &Path) -> bool { - // Has to be either Debug or Release - llvm_config(llvm_config_path, ["--build-mode"]).contains("Debug") -}