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

Detach bindgen for next release #250

Merged
merged 2 commits into from
Jun 28, 2024
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
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);
}
Loading