diff --git a/Cargo.lock b/Cargo.lock index c769083dc..7dd3de1be 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -86,7 +86,7 @@ dependencies = [ [[package]] name = "ark" -version = "0.1.4" +version = "0.1.5" dependencies = [ "amalthea", "anyhow", diff --git a/crates/ark/Cargo.toml b/crates/ark/Cargo.toml index cf6eafa77..f05e87820 100644 --- a/crates/ark/Cargo.toml +++ b/crates/ark/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ark" -version = "0.1.4" +version = "0.1.5" edition = "2021" rust-version = "1.70.0" description = """ @@ -46,6 +46,9 @@ uuid = "1.3.0" walkdir = "2" yaml-rust = "0.4.5" +[build-dependencies] +chrono = "0.4.23" + # The libR-sys package doesn't currently provide bindings for R on aarch64 # Linux platforms, so on those systems we need to add the use-bingen feature, # which causes the package to build its own bindings from scratch. diff --git a/crates/ark/build.rs b/crates/ark/build.rs new file mode 100644 index 000000000..dc98ca73e --- /dev/null +++ b/crates/ark/build.rs @@ -0,0 +1,26 @@ +// +// build.rs +// +// Copyright (C) 2023 Posit Software, PBC. All rights reserved. +// +// + +use std::process::Command; + +fn main() { + // Attempt to use `git rev-parse HEAD` to get the current git hash. If this + // fails, we'll just use the string "" to indicate that the git hash + // could not be determined.. + let git_hash = match Command::new("git") + .args(&["rev-parse", "--short", "HEAD"]) + .output() + { + Ok(output) => String::from_utf8(output.stdout).unwrap(), + Err(_) => String::from(""), + }; + println!("cargo:rustc-env=BUILD_GIT_HASH={}", git_hash); + + // Get the build date as a string + let build_date = chrono::Utc::now().to_rfc3339_opts(chrono::SecondsFormat::Secs, true); + println!("cargo:rustc-env=BUILD_DATE={}", build_date); +} diff --git a/crates/ark/src/modules/public/utils.R b/crates/ark/src/modules/public/utils.R index 23106e55c..9701d58b4 100644 --- a/crates/ark/src/modules/public/utils.R +++ b/crates/ark/src/modules/public/utils.R @@ -32,3 +32,7 @@ } } + +.ps.ark.version <- function() { + .ps.Call("ps_ark_version") +} diff --git a/crates/ark/src/version.rs b/crates/ark/src/version.rs index 8d7581af3..fae1e3d3d 100644 --- a/crates/ark/src/version.rs +++ b/crates/ark/src/version.rs @@ -5,10 +5,13 @@ // // +use std::collections::HashMap; use std::process::Command; use anyhow::Context; +use harp::object::RObject; use itertools::Itertools; +use libR_sys::*; pub struct RVersion { // Major version of the R installation @@ -63,3 +66,18 @@ pub fn detect_r() -> anyhow::Result { anyhow::bail!("Failed to extract R version"); } } + +#[harp::register] +pub unsafe extern "C" fn ps_ark_version() -> SEXP { + let mut info = HashMap::::new(); + // Set the version info in the map + info.insert( + String::from("version"), + String::from(env!("CARGO_PKG_VERSION")), + ); + info.insert(String::from("commit"), String::from(env!("BUILD_GIT_HASH"))); + info.insert(String::from("date"), String::from(env!("BUILD_DATE"))); + + let result = RObject::from(info); + result.sexp +} diff --git a/crates/harp/src/object.rs b/crates/harp/src/object.rs index 1bbaab09f..c79988a64 100644 --- a/crates/harp/src/object.rs +++ b/crates/harp/src/object.rs @@ -141,6 +141,10 @@ fn r_size(x: SEXP) -> usize { } } +fn r_length(x: SEXP) -> isize { + unsafe { Rf_xlength(x) } +} + impl RObject { pub unsafe fn new(data: SEXP) -> Self { RObject { @@ -183,6 +187,10 @@ impl RObject { pub fn size(&self) -> usize { r_size(self.sexp) } + + pub fn length(&self) -> isize { + r_length(self.sexp) + } } impl Clone for RObject { @@ -285,6 +293,41 @@ impl From for RObject { } } +// Convert a String -> String HashMap into named character vector. +impl From> for RObject { + fn from(value: HashMap) -> Self { + unsafe { + // Allocate the vector of values + let values = Rf_protect(Rf_allocVector(STRSXP, value.len() as isize)); + + // Allocate the vector of names; this will be protected by attaching + // it to the values vector as an attribute + let names = Rf_allocVector(STRSXP, value.len() as isize); + Rf_setAttrib(values, R_NamesSymbol, names); + + // Convert the hashmap to a sorted vector of tuples; we do this so that the + // order of the values and names is deterministic + let mut sorted: Vec<_> = value.into_iter().collect(); + sorted.sort_by(|a, b| a.0.cmp(&b.0)); + + // Loop over the values and names, setting them in the vectors + for (idx, (key, value)) in sorted.iter().enumerate() { + SET_STRING_ELT( + values, + idx as isize, + Rf_mkChar(value.as_ptr() as *mut c_char), + ); + SET_STRING_ELT(names, idx as isize, Rf_mkChar(key.as_ptr() as *mut c_char)); + } + + // Clean up the protect stack and return the RObject from the values + // vector + Rf_unprotect(1); + RObject::new(values) + } + } +} + /// Convert RObject into other types. impl From for SEXP { @@ -681,6 +724,29 @@ mod tests { } } + #[test] + #[allow(non_snake_case)] + fn test_tryfrom_RObject_hashmap_string() { + r_test! { + // Create a map of pizza toppings to their acceptability. + let mut map = HashMap::::new(); + map.insert(String::from("pepperoni"), String::from("OK")); + map.insert(String::from("sausage"), String::from("OK")); + map.insert(String::from("pineapple"), String::from("NOT OK")); + let len = map.len(); + + // Ensure we created an object of the same size as the map. + let robj = RObject::from(map); + assert_eq!(robj.length(), len as isize); + + // Ensure we can convert the object back into a map with the same values. + let out: HashMap = robj.try_into().unwrap(); + assert_eq!(out.get("pepperoni").unwrap(), "OK"); + assert_eq!(out.get("sausage").unwrap(), "OK"); + assert_eq!(out.get("pineapple").unwrap(), "NOT OK"); + } + } + #[test] #[allow(non_snake_case)] fn test_tryfrom_RObject_Vec_Option_String() {