diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index be29c36f..fa3d8c53 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -223,8 +223,9 @@ jobs: if: matrix.config.skip-tests != 'true' run: | . ./ci-cargo.ps1 - ci-cargo test -vv --features use-bindgen,layout_tests $(if ($env:RUST_TARGET -ne '') {"--target=$env:RUST_TARGET"} ) '--' --nocapture -ActionName "Running bindgen tests for target: $env:RUST_TARGET" - ci-cargo test -vv --features use-bindgen,non-api,layout_tests $(if ($env:RUST_TARGET -ne '') {"--target=$env:RUST_TARGET"} ) '--' --nocapture -ActionName "Running bindgen tests for target: $env:RUST_TARGET (with non-API)" + # ci-cargo test -vv --features use-bindgen,layout_tests $(if ($env:RUST_TARGET -ne '') {"--target=$env:RUST_TARGET"} ) '--' --nocapture -ActionName "Running bindgen tests for target: $env:RUST_TARGET" + # ci-cargo test -vv --features use-bindgen,non-api,layout_tests $(if ($env:RUST_TARGET -ne '') {"--target=$env:RUST_TARGET"} ) '--' --nocapture -ActionName "Running bindgen tests for target: $env:RUST_TARGET (with non-API)" + ci-cargo test -vv $(if ($env:RUST_TARGET -ne '') {"--target=$env:RUST_TARGET"} ) '--' --nocapture -ActionName "Running bindgen tests for target: $env:RUST_TARGET" env: RUST_TARGET: ${{ matrix.config.target }} @@ -233,7 +234,7 @@ jobs: id: build run: | . ./ci-cargo.ps1 - ci-cargo build -vv --features use-bindgen $(if ($env:RUST_TARGET -ne '') {"--target=$env:RUST_TARGET"} ) -ActionName "Building for target: $env:RUST_TARGET" + # ci-cargo build -vv --features use-bindgen $(if ($env:RUST_TARGET -ne '') {"--target=$env:RUST_TARGET"} ) -ActionName "Building for target: $env:RUST_TARGET" env: LIBRSYS_BINDINGS_OUTPUT_PATH: generated_bindings RUST_TARGET: ${{ matrix.config.target }} diff --git a/Cargo.toml b/Cargo.toml index 4db0ba7d..22326f19 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,26 +16,6 @@ links = "R" documentation = "https://docs.rs/libR-sys/latest/libR_sys/" repository = "https://github.com/extendr/libR-sys" -[dependencies] - -[build-dependencies] -bindgen = { version = "0.69.4", optional = true, features = ["experimental"] } -regex = { version = "*", optional = true, default-features = false } - -[features] -default = ["runtime"] -# By default, we use pre-computed bindings that ship with the library. This may fail! -# Turn on the 'use-bindgen' feature to generate bindings on the fly for your platform. -use-bindgen = ["bindgen", "regex"] - -# retain non-API bindings from R (requires build-time generation of bindings) -non-api = ["use-bindgen"] - -runtime = ["bindgen/runtime"] - -# Enables generation of layout-tests in bindgen -layout_tests = ["use-bindgen"] - [lib] # Some code comments on R's source code might be accidentally treated as Rust's # doc test. See https://github.com/extendr/libR-sys/issues/194 for the details. diff --git a/build.rs b/build.rs index 33303da4..edbb34b6 100644 --- a/build.rs +++ b/build.rs @@ -13,16 +13,10 @@ use std::os::unix::ffi::OsStrExt; #[cfg(target_family = "windows")] use std::os::windows::ffi::OsStringExt; -// -// Environmental variables -// - // The environmental variables that are usually set by R. These might be needed // to set manually if we compile libR-sys outside of an R session. // // c.f., https://stat.ethz.ch/R-manual/R-devel/library/base/html/EnvVar.html -#[cfg(feature = "use-bindgen")] -const ENVVAR_R_INCLUDE_DIR: &str = "R_INCLUDE_DIR"; const ENVVAR_R_HOME: &str = "R_HOME"; // An R version (e.g., "4.1.2" or "4.2.0-devel"). When this is set, the actual R @@ -31,24 +25,11 @@ const ENVVAR_R_HOME: &str = "R_HOME"; const ENVVAR_R_VERSION: &str = "LIBRSYS_R_VERSION"; // A path to a dir containing pre-computed bindings (default: "bindings"). -#[cfg(not(feature = "use-bindgen"))] const ENVVAR_BINDINGS_PATH: &str = "LIBRSYS_BINDINGS_PATH"; -// A path to libclang toolchain. If this is set, the path is added to the -// compiler arguments on executing bindgen. -#[cfg(feature = "use-bindgen")] -const ENVVAR_LIBCLANG_INCLUDE_PATH: &str = "LIBRSYS_LIBCLANG_INCLUDE_PATH"; - -// A path to an output dir of bindings in addition to the default "bindings" -// dir. If this is set, generated bindings are also put there. -#[cfg(feature = "use-bindgen")] -const ENVVAR_BINDINGS_OUTPUT_PATH: &str = "LIBRSYS_BINDINGS_OUTPUT_PATH"; - #[derive(Debug)] struct InstallationPaths { r_home: PathBuf, - #[cfg(feature = "use-bindgen")] - include: PathBuf, library: PathBuf, } @@ -209,32 +190,6 @@ fn get_r_library(r_home: &Path) -> PathBuf { } } -// Get the path to the R include directory either from an envvar or by executing the actual R binary. -#[cfg(feature = "use-bindgen")] -fn get_r_include(r_home: &Path, library: &Path) -> io::Result { - // If the environment variable R_INCLUDE_DIR is set we use it - if let Some(include) = env::var_os(ENVVAR_R_INCLUDE_DIR) { - return Ok(PathBuf::from(include)); - } - - // Otherwise, we try to execute `R` to find the include dir. Here, - // we're using the R home we found earlier, to make sure we're consistent. - let r_binary = InstallationPaths { - r_home: r_home.to_path_buf(), - #[cfg(feature = "use-bindgen")] - include: PathBuf::new(), // get_r_binary() doesn't use `include` so fill with an empty PathBuf. - library: library.to_path_buf(), - } - .get_r_binary(); - - let rout = r_command(r_binary, r#"cat(normalizePath(R.home('include')))"#)?; - if !rout.is_empty() { - Ok(PathBuf::from(rout)) - } else { - Err(Error::new(ErrorKind::Other, "Cannot find R include.")) - } -} - fn probe_r_paths() -> io::Result { // First we locate the R home let r_home = get_r_home()?; @@ -242,14 +197,8 @@ fn probe_r_paths() -> io::Result { // Now the library location. On Windows, it depends on the target architecture let library = get_r_library(&r_home); - // Finally the include location. It may or may not be located under R home - #[cfg(feature = "use-bindgen")] - let include = get_r_include(&r_home, &library)?; - Ok(InstallationPaths { r_home, - #[cfg(feature = "use-bindgen")] - include, library, }) } @@ -377,192 +326,6 @@ fn set_r_version_vars(ver: &RVersionInfo) { println!("cargo:r_version_devel={}", ver.devel); // Becomes DEP_R_R_VERSION_DEVEL for clients } -#[cfg(all(feature = "use-bindgen", not(feature = "non-api")))] -fn get_non_api() -> std::collections::HashSet { - // Several non-APIs are required for extendr-engine, so we explicitly allow - // these here. If extendr-engine (or other crate) requires more non-APIs, - // add it here with caution. - const REQUIRED_NON_API: [&str; 6] = [ - "R_CStackLimit", - "R_CleanTempDir", - "R_RunExitFinalizers", - "Rf_endEmbeddedR", - "Rf_initialize_R", - "setup_Rmainloop", - ]; - - // nonAPI.txt is generated by - // Rscript -e 'cat(tools:::nonAPI, sep = "\n")' | tr -d '\r' | sort -u | tee ./nonAPI.txt - let non_api = include_str!("./nonAPI.txt") - .lines() - .filter(|e| !REQUIRED_NON_API.contains(e)) - .map(|s| s.to_string()); - - std::collections::HashSet::from_iter(non_api) -} - -#[cfg(feature = "use-bindgen")] -/// Generate bindings by calling bindgen. -fn generate_bindings(r_paths: &InstallationPaths, version_info: &RVersionInfo) { - // The bindgen::Builder is the main entry point - // to bindgen, and lets you build up options for - // the resulting bindings. - let mut bindgen_builder = bindgen::Builder::default(); - - #[cfg(all(feature = "use-bindgen", not(feature = "non-api")))] - { - let blocklist = get_non_api().into_iter().collect::>().join("|"); - bindgen_builder = bindgen_builder.blocklist_item(blocklist); - } - - bindgen_builder = bindgen_builder - .emit_diagnostics() - .translate_enum_integer_types(true) - .merge_extern_blocks(true) - // The input header we would like to generate - // bindings for. - .header("wrapper.h") - // Tell cargo to invalidate the built crate whenever any of the - // included header files changed. - .parse_callbacks(Box::new(bindgen::CargoCallbacks::new())); - - // Use enum-definition of `SEXPTYPE`, as it is available and compatible - bindgen_builder = bindgen_builder.clang_arg("-Denum_SEXPTYPE"); - - // Collect C-enums into idiomatic Rust-style enums - bindgen_builder = bindgen_builder.default_enum_style(bindgen::EnumVariation::Rust { - non_exhaustive: false, - }); - - if cfg!(feature = "layout_tests") { - bindgen_builder = bindgen_builder.layout_tests(true); - } else { - bindgen_builder = bindgen_builder.layout_tests(false); - } - - let target = env::var("TARGET").expect("Could not get the target triple"); - let target_os = std::env::var("CARGO_CFG_TARGET_OS").unwrap(); - let target_arch = env::var("CARGO_CFG_TARGET_ARCH").unwrap(); - - println!( - "Generating bindings for target: {target}, os: {target_os}, architecture: {target_arch}" - ); - - let r_include_path = r_paths.include.display().to_string().replace(r"\", r"/"); - let r_include_path_escaped = regex::escape(&r_include_path); - let r_include_path_escaped = format!("{r_include_path_escaped}.*"); - - // Point to the correct headers - bindgen_builder = - bindgen_builder.clang_args([format!("-I{r_include_path}",), format!("--target={target}")]); - - // this effectively ignores all non-R headers from sneaking in - bindgen_builder = bindgen_builder - .allowlist_file(r_include_path_escaped) - .allowlist_file(".*wrapper\\.h$"); - - // stops warning about ignored attributes, - // e.g. ignores `__format__` attributes caused by `stdio.h` - bindgen_builder = bindgen_builder.clang_arg("-Wno-ignored-attributes"); - - // allow injection of an alternative include path to libclang - if let Some(alt_include) = env::var_os(ENVVAR_LIBCLANG_INCLUDE_PATH) { - bindgen_builder = - bindgen_builder.clang_arg(format!("-I{}", PathBuf::from(alt_include).display())); - } - - // Remove constants defined by C-headers as - // there are rust equivalents for them. - let bindgen_builder = bindgen_builder - .blocklist_item("M_E") - .blocklist_item("M_LOG2E") - .blocklist_item("M_LOG10E") - .blocklist_item("M_LN2") - .blocklist_item("M_LN10") - .blocklist_item("M_PI") - .blocklist_item("M_PI_2") - .blocklist_item("M_PI_4") - .blocklist_item("M_1_PI") - .blocklist_item("M_2_PI") - .blocklist_item("M_2_SQRTPI") - .blocklist_item("M_SQRT2") - .blocklist_item("M_SQRT1_2") - .blocklist_item("M_2PI") - .blocklist_item("M_LOG10_2"); - - // `VECTOR_PTR` is deprecated, use `DATAPTR` and friends instead - let bindgen_builder = bindgen_builder.blocklist_item("VECTOR_PTR"); - - // Remove all Fortran items, these are items with underscore _ postfix - let bindgen_builder = bindgen_builder.blocklist_item("[A-Za-z_][A-Za-z0-9_]*[^_]_$"); - - // Ensure that `SEXPREC` is opaque to Rust - let bindgen_builder = bindgen_builder.blocklist_item("SEXPREC"); - - // Replace `TYPEOF` definition with one that gives same type as `SEXPTYPE`. - let bindgen_builder = bindgen_builder.blocklist_item("TYPEOF"); - - // Replace this with a function that handles enum-version of SEXPTYPE correctly - let bindgen_builder = bindgen_builder.blocklist_item("Rf_isS4"); - - // Replace second arg with SEXPTYPE, see lib.rs - let bindgen_builder = bindgen_builder.blocklist_type("R_altrep_Coerce_method_t"); - - // Extra, and unnecessary item being generated by bindgen - let bindgen_builder = bindgen_builder.blocklist_item("Rcomplex__bindgen_ty_1"); - - // Finish the builder and generate the bindings. - let bindings = bindgen_builder - .raw_line(format!( - "/* libR-sys version: {} */", - env!("CARGO_PKG_VERSION") - )) - .raw_line(format!( - "/* bindgen clang version: {} */", - bindgen::clang_version().full - )) - .raw_line(format!("/* r version: {} */", version_info.full)) - .generate_comments(true) - .parse_callbacks(Box::new(TrimCommentsCallbacks)) - .clang_arg("-fparse-all-comments") - .generate() - // Unwrap the Result and panic on failure. - .expect("Unable to generate bindings"); - - // Write the bindings to the $OUT_DIR/bindings.rs file. - let out_path = PathBuf::from(env::var_os("OUT_DIR").unwrap()); - - bindings - .write_to_file(out_path.join("bindings.rs")) - .expect("Couldn't write bindings to default output path!"); - - // Also write the bindings to a folder specified by `LIBRSYS_BINDINGS_OUTPUT_PATH`, if defined - if let Some(alt_target) = env::var_os(ENVVAR_BINDINGS_OUTPUT_PATH) { - let out_path = PathBuf::from(alt_target); - // if folder doesn't exist, try to create it - if !out_path.exists() { - fs::create_dir(&out_path).unwrap_or_else(|_| { - panic!( - "Couldn't create output directory for bindings: {}", - out_path.display() - ) - }); - } - - let bindings_file_full = version_info.get_r_bindings_filename(&target_os, &target_arch); - let out_file = out_path.join(bindings_file_full); - - bindings - .write_to_file(&out_file) - .unwrap_or_else(|_| panic!("Couldn't write bindings: {}", out_file.display())); - } else { - println!( - "cargo:warning=Couldn't write the bindings since `LIBRSYS_BINDINGS_OUTPUT_PATH` is not set." - ); - } -} - -#[cfg(not(feature = "use-bindgen"))] /// Retrieve bindings from cache, if available. Errors out otherwise. fn retrieve_prebuild_bindings(version_info: &RVersionInfo) { let target_os = std::env::var("CARGO_CFG_TARGET_OS").unwrap(); @@ -599,25 +362,6 @@ fn retrieve_prebuild_bindings(version_info: &RVersionInfo) { println!("cargo:rerun-if-changed={}", from.display()); } -/// Provide extra cleaning of the processed elements in the headers. -#[cfg(feature = "use-bindgen")] -#[derive(Debug)] -struct TrimCommentsCallbacks; - -#[cfg(feature = "use-bindgen")] -impl bindgen::callbacks::ParseCallbacks for TrimCommentsCallbacks { - fn process_comment(&self, comment: &str) -> Option { - // trim comments - let comment = comment.trim(); - - // replace bare brackets in comments - let comment = comment.replace('[', r"\["); - let comment = comment.replace(']', r"\]"); - - Some(comment) - } -} - fn main() { let r_paths = probe_r_paths(); @@ -652,8 +396,5 @@ fn main() { get_r_version(ENVVAR_R_VERSION, &r_paths).expect("Could not obtain R version"); set_r_version_vars(&version_info); - #[cfg(feature = "use-bindgen")] - generate_bindings(&r_paths, &version_info); - #[cfg(not(feature = "use-bindgen"))] retrieve_prebuild_bindings(&version_info); }