Skip to content

Commit

Permalink
Detach bindgen for next release (#250)
Browse files Browse the repository at this point in the history
  • Loading branch information
CGMossa authored Jun 28, 2024
1 parent 975af4e commit 37fdd30
Show file tree
Hide file tree
Showing 3 changed files with 4 additions and 282 deletions.
7 changes: 4 additions & 3 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }}

Expand All @@ -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 }}
Expand Down
20 changes: 0 additions & 20 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
259 changes: 0 additions & 259 deletions build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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,
}

Expand Down Expand Up @@ -209,47 +190,15 @@ 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<PathBuf> {
// 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<InstallationPaths> {
// First we locate the R home
let r_home = get_r_home()?;

// 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,
})
}
Expand Down Expand Up @@ -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<String> {
// 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::<Vec<_>>().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();
Expand Down Expand Up @@ -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<String> {
// 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();

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

0 comments on commit 37fdd30

Please sign in to comment.