use crate::core::backend::GeneratedFile;
use crate::core::config::{Language, ResolvedCrateConfig};
use crate::core::ir::ApiSurface;
use crate::core::template_versions as tv;
use crate::core::version::to_r_version;
use crate::{
scaffold::cargo_package_header, scaffold::core_dep_features, scaffold::detect_workspace_inheritance,
scaffold::scaffold_meta,
};
use std::path::PathBuf;
pub(crate) fn scaffold_r(api: &ApiSurface, config: &ResolvedCrateConfig) -> anyhow::Result<Vec<GeneratedFile>> {
let meta = scaffold_meta(config);
let version = to_r_version(&api.version);
let package_name = config.r_package_name();
let mut description = meta.description.clone();
if description.ends_with('.') {
description.pop();
}
let authors_r = if meta.authors.is_empty() {
r#"Authors@R: person("Author", "Name", email = "author@example.com", role = c("aut", "cre"))"#.to_string()
} else {
format!(
"Authors@R: person(\"{}\", email = \"author@example.com\", role = c(\"aut\", \"cre\"))",
meta.authors.first().unwrap_or(&"Author Name".to_string())
)
};
let content = format!(
r#"Package: {package}
Title: {title}
Version: {version}
{authors}
Description: {description}
Rust bindings generated with extendr.
URL: {repository}
BugReports: {repository}/issues
License: {license}
Depends: R (>= 4.2)
Imports: jsonlite
Suggests:
testthat (>= 3.0.0),
withr,
roxygen2,
lintr,
styler
SystemRequirements: Cargo (Rust's package manager), rustc (>= 1.91)
Config/rextendr/version: {rextendr}
Encoding: UTF-8
Roxygen: list(markdown = TRUE)
RoxygenNote: 7.3.3
Config/testthat/edition: 3
"#,
package = package_name,
title = meta.description,
version = version,
authors = authors_r,
description = description,
repository = meta.repository,
license = meta.license,
rextendr = tv::cran::REXTENDR,
);
Ok(vec![
GeneratedFile {
path: PathBuf::from("packages/r/DESCRIPTION"),
content,
generated_header: true,
},
GeneratedFile {
path: PathBuf::from("packages/r/.lintr"),
content: r#"linters: linters_with_defaults(
line_length_linter(120),
object_name_linter = NULL,
object_usage_linter = NULL,
commented_code_linter = NULL
)
"#
.to_string(),
generated_header: false,
},
])
}
pub(crate) fn scaffold_r_cargo(api: &ApiSurface, config: &ResolvedCrateConfig) -> anyhow::Result<Vec<GeneratedFile>> {
let meta = scaffold_meta(config);
let version = &api.version;
let core_crate_dir = config.core_crate_dir();
let ws = detect_workspace_inheritance(config.workspace_root.as_deref());
let pkg_header = cargo_package_header(
&format!("{core_crate_dir}-r"),
version,
"2024",
&meta.license,
&meta.description,
&meta.keywords,
&ws,
);
let has_async =
api.functions.iter().any(|f| f.is_async) || api.types.iter().any(|t| t.methods.iter().any(|m| m.is_async));
let has_trait_bridges = !config.trait_bridges.is_empty();
let features_str = core_dep_features(config, Language::R);
let mut dep_lines: Vec<String> = vec![
crate::scaffold::render_core_dep(
&config.name,
&format!("../../../../crates/{core_crate_dir}"),
&features_str,
version,
),
format!("extendr-api = \"{}\"", tv::cargo::EXTENDR_API),
"serde = { version = \"1\", features = [\"derive\"] }".to_owned(),
"serde_json = \"1\"".to_owned(),
];
if has_async {
dep_lines.push("tokio = { version = \"1\", features = [\"rt-multi-thread\"] }".to_owned());
}
if has_trait_bridges {
dep_lines.push("async-trait = \"0.1\"".to_owned());
}
dep_lines.sort();
let deps_section = dep_lines.join("\n");
let cargo_content = format!(
r#"{pkg_header}
[lib]
crate-type = ["staticlib", "lib"]
[dependencies]
{deps_section}
"#,
pkg_header = pkg_header,
deps_section = deps_section,
);
let r_package_name = config.r_package_name();
let lib_name = r_package_name.replace('-', "_");
let rust_lib_name = format!("{}_r", core_crate_dir).replace('-', "_");
let makevars_content = format!(
"CARGO_BUILD_ARGS = --release\nSTATLIB = ./rust/target/release/lib{rust_lib_name}.a\nPKG_LIBS = -L./rust/target/release -l{rust_lib_name} $(LAPACK_LIBS) $(BLAS_LIBS) $(FLIBS)\n\nall: $(SHLIB)\n\n$(STATLIB):\n\tcargo build --manifest-path ./rust/Cargo.toml $(CARGO_BUILD_ARGS)\n\n$(SHLIB): $(STATLIB)\n\nclean:\n\trm -f $(SHLIB) $(STATLIB)\n\tcargo clean --manifest-path ./rust/Cargo.toml\n",
rust_lib_name = rust_lib_name,
);
let makevars_in_content = makevars_content.clone();
let makevars_win_content = format!(
"CARGO_BUILD_ARGS = --release --target x86_64-pc-windows-gnu\nSTATLIB = ./rust/target/x86_64-pc-windows-gnu/release/{rust_lib_name}.lib\nPKG_LIBS = -L./rust/target/x86_64-pc-windows-gnu/release -l{rust_lib_name} -lws2_32 -ladvapi32 -luserenv -lbcrypt -lntdll\n\nall: $(SHLIB)\n\n$(STATLIB):\n\tcargo build --manifest-path ./rust/Cargo.toml $(CARGO_BUILD_ARGS)\n\n$(SHLIB): $(STATLIB)\n\nclean:\n\trm -f $(SHLIB) $(STATLIB)\n\tcargo clean --manifest-path ./rust/Cargo.toml\n",
rust_lib_name = rust_lib_name,
);
let entrypoint_c_content = format!(
"// Generated entrypoint: forwards to the extendr-generated init function.\n// Do not edit — regenerate with `alef generate`.\n#include <R_ext/Visibility.h>\n\nvoid R_init_{lib_name}_extendr(void *dll);\n\nvoid attribute_visible R_init_{lib_name}(void *dll) {{\n R_init_{lib_name}_extendr(dll);\n}}\n",
lib_name = lib_name,
);
Ok(vec![
GeneratedFile {
path: PathBuf::from("packages/r/src/rust/Cargo.toml"),
content: cargo_content,
generated_header: true,
},
GeneratedFile {
path: PathBuf::from("packages/r/src/Makevars"),
content: makevars_content,
generated_header: true,
},
GeneratedFile {
path: PathBuf::from("packages/r/src/Makevars.in"),
content: makevars_in_content,
generated_header: true,
},
GeneratedFile {
path: PathBuf::from("packages/r/src/Makevars.win.in"),
content: makevars_win_content,
generated_header: true,
},
GeneratedFile {
path: PathBuf::from("packages/r/src/entrypoint.c"),
content: entrypoint_c_content,
generated_header: false,
},
])
}