diff --git a/.github/workflows/run_preflight.yml b/.github/workflows/run_preflight.yml index e1d4691..412e112 100644 --- a/.github/workflows/run_preflight.yml +++ b/.github/workflows/run_preflight.yml @@ -14,6 +14,9 @@ jobs: steps: - uses: actions/checkout@v3 + - name: Set up R + uses: r-lib/actions/setup-r@v2 + - name: Set up Rust uses: actions-rs/toolchain@v1 with: @@ -24,6 +27,9 @@ jobs: working-directory: rust run: rustup component add clippy rustfmt + - name: Set R_HOME env var + run: echo "R_HOME=$(R RHOME)" >> $GITHUB_ENV + - name: Format check working-directory: rust run: cargo fmt --all -- --check diff --git a/.github/workflows/run_rust_unit_tests.yml b/.github/workflows/run_rust_unit_tests.yml index 1b26db2..1cacf8e 100644 --- a/.github/workflows/run_rust_unit_tests.yml +++ b/.github/workflows/run_rust_unit_tests.yml @@ -17,6 +17,9 @@ jobs: - name: Checkout code uses: actions/checkout@v3 + - name: Set up R + uses: r-lib/actions/setup-r@v2 + - name: Set up Rust uses: actions-rs/toolchain@v1 with: @@ -27,7 +30,9 @@ jobs: run: sudo apt-get update && sudo apt-get install -y libssl-dev pkg-config - name: Run tests - working-directory: rust - run: cargo test + run: | + export LD_LIBRARY_PATH=$(R RHOME)/lib:$LD_LIBRARY_PATH + cargo test + diff --git a/.gitignore b/.gitignore index 8efb0e7..5fe4768 100644 --- a/.gitignore +++ b/.gitignore @@ -33,4 +33,17 @@ venv/ .codegpt # prettier node -node_modules/ \ No newline at end of file +node_modules/ + +# R specific +.Rhistory +.RData +bindings/r/src/rust/vendor +bindings/r/src/Makevars +bindings/r/src/Makevars.win +*.o +*.so +*.dll +target +.cargo +.Rproj.user diff --git a/Cargo.toml b/Cargo.toml index 9813682..05d8e6a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,5 +2,6 @@ members = [ "rust/codelist-rs", "rust/codelist-validator-rs", - "bindings/python" + "bindings/python", + "bindings/r/src/rust" ] diff --git a/bindings/r/.Rbuildignore b/bindings/r/.Rbuildignore new file mode 100644 index 0000000..30401cc --- /dev/null +++ b/bindings/r/.Rbuildignore @@ -0,0 +1,8 @@ +^\.vscode$ +^src/\.cargo$ +^src/rust/vendor$ +^src/rust/target$ +^src/Makevars$ +^src/Makevars\.win$ +^.*\.Rproj$ +^\.Rproj\.user$ diff --git a/bindings/r/Cargo.toml b/bindings/r/Cargo.toml deleted file mode 100644 index 90cc117..0000000 --- a/bindings/r/Cargo.toml +++ /dev/null @@ -1,13 +0,0 @@ -[package] -name = "codelist" -version = "0.1.0" -edition = "2021" - -[lib] -crate-type = ["staticlib"] -name = "codelist" -path = "src/lib.rs" -# Build as a dynamic library - -[dependencies] -extendr-api = "0.7.1" diff --git a/bindings/r/DESCRIPTION b/bindings/r/DESCRIPTION index 2eab0a5..fb26451 100644 --- a/bindings/r/DESCRIPTION +++ b/bindings/r/DESCRIPTION @@ -10,4 +10,7 @@ Roxygen: list(markdown = TRUE) RoxygenNote: 7.3.2 Imports: rextendr -Config/rextendr/version: 0.3.1 +Config/rextendr/version: 0.4.0.9000 +SystemRequirements: Cargo (Rust's package manager), rustc +Depends: + R (>= 4.2) \ No newline at end of file diff --git a/bindings/r/NAMESPACE b/bindings/r/NAMESPACE index 0a80ddb..b4c29a2 100644 --- a/bindings/r/NAMESPACE +++ b/bindings/r/NAMESPACE @@ -1,2 +1,5 @@ +# Generated by roxygen2: do not edit by hand + +S3method("$",Codelist) +S3method("[[",Codelist) useDynLib(codelist, .registration = TRUE) -export(hello) \ No newline at end of file diff --git a/bindings/r/R/extendr-wrappers.R b/bindings/r/R/extendr-wrappers.R index 53199cf..f289fff 100644 --- a/bindings/r/R/extendr-wrappers.R +++ b/bindings/r/R/extendr-wrappers.R @@ -12,5 +12,19 @@ NULL hello <- function() .Call(wrap__hello) +Codelist <- new.env(parent = emptyenv()) + +Codelist$new <- function(name) .Call(wrap__Codelist__new, name) + +Codelist$set_name <- function(new_name) .Call(wrap__Codelist__set_name, self, new_name) + +Codelist$get_entries <- function() .Call(wrap__Codelist__get_entries, self) + +#' @export +`$.Codelist` <- function (self, name) { func <- Codelist[[name]]; environment(func) <- environment(); func } + +#' @export +`[[.Codelist` <- `$.Codelist` + # nolint end diff --git a/bindings/r/README.md b/bindings/r/README.md index 52101ed..3c3e6c8 100644 --- a/bindings/r/README.md +++ b/bindings/r/README.md @@ -1,8 +1,6 @@ # R Bindings -This package demonstrates how to call Rust functions from R using extendr. I -(@CarolineMorton) have not yet implemented this with our Codelist library but -rather with a simple "hello world" function. +This package demonstrates how to call Rust functions from R using extendr. ## Prerequisites @@ -35,19 +33,15 @@ which i said no to. ## Building and Installing -1. First build the Rust library: +1. First go into R folder and build the package. ```bash -cd r -cargo build --release +cd bindings/r ``` -This will create a shared library in `target/release/libcodelist.dylib` (macOS) -or `target/release/libcodelist.so` (Linux). - -2. Open R with the terminal command `R` and install required packages. If you - get an error message when running this, you may also need to install package - dependencies for `devtools` first if not already installed: +You will need to go into R at this point and install required packages of +`rextendr`. If you get an error message when running this, you may also need to +install package dependencies for `devtools` first if not already installed: ```r install.packages("devtools") @@ -60,8 +54,6 @@ I picked `64` as the mirror I wanted to use. ```r rextendr::document() -devtools::document() -devtools::load_all() ``` 4. Test that it works: @@ -70,18 +62,22 @@ devtools::load_all() hello() ``` -## Installing Permanently - -To install the package permanently: +Everytime you make changes to the Rust code, you will need to run the following +commands in R: ```r -devtools::build() -devtools::install() +rextendr::document() +library(codelist) ``` -After installation, you can use it like any other R package: +The `library(codelist)` command loads the package, so you can use the functions +and structs within in so it is important to run this after you have made changes +to the Rust code. -```r -library(codelist) -hello() -``` +#### Tips for non-R users + +- You can create R scripts by making a file that ends with `.R` and then running + it in R. It is probably easier at this point to opne in RStudio and run in the + console or by clicking at the script. Remember to save. +- Exit the R console with `q()` and then type `n` to not save the workspace + image. diff --git a/bindings/r/configure b/bindings/r/configure new file mode 100755 index 0000000..c608b11 --- /dev/null +++ b/bindings/r/configure @@ -0,0 +1,3 @@ +#!/usr/bin/env sh +: "${R_HOME=`R RHOME`}" +"${R_HOME}/bin/Rscript" tools/config.R diff --git a/bindings/r/configure.win b/bindings/r/configure.win new file mode 100644 index 0000000..57eb255 --- /dev/null +++ b/bindings/r/configure.win @@ -0,0 +1,2 @@ +#!/usr/bin/env sh +"${R_HOME}/bin${R_ARCH_BIN}/Rscript.exe" tools/config.R diff --git a/bindings/r/r.Rproj b/bindings/r/r.Rproj new file mode 100644 index 0000000..21a4da0 --- /dev/null +++ b/bindings/r/r.Rproj @@ -0,0 +1,17 @@ +Version: 1.0 + +RestoreWorkspace: Default +SaveWorkspace: Default +AlwaysSaveHistory: Default + +EnableCodeIndexing: Yes +UseSpacesForTab: Yes +NumSpacesForTab: 2 +Encoding: UTF-8 + +RnwWeave: Sweave +LaTeX: pdfLaTeX + +BuildType: Package +PackageUseDevtools: Yes +PackageInstallArgs: --no-multiarch --with-keep.source diff --git a/bindings/r/scripts/scratch.R b/bindings/r/scripts/scratch.R new file mode 100644 index 0000000..41397a8 --- /dev/null +++ b/bindings/r/scripts/scratch.R @@ -0,0 +1,8 @@ +library(codelist) + +c <- Codelist$new('test') +c$set_name('new_test') +e <- c$get_entries() + +first <- e[[1]]$code + diff --git a/bindings/r/src/Makevars b/bindings/r/src/Makevars index 6417f60..3655f9e 100644 --- a/bindings/r/src/Makevars +++ b/bindings/r/src/Makevars @@ -1,13 +1,42 @@ -LIBDIR = ../target/release +TARGET_DIR = ./rust/target +LIBDIR = $(TARGET_DIR)/debug STATLIB = $(LIBDIR)/libcodelist.a PKG_LIBS = -L$(LIBDIR) -lcodelist -all: C_clean +all: $(SHLIB) rust_clean + +.PHONY: $(STATLIB) $(SHLIB): $(STATLIB) +CARGOTMP = $(CURDIR)/.cargo +VENDOR_DIR = $(CURDIR)/vendor + + +# RUSTFLAGS appends --print=native-static-libs to ensure that +# the correct linkers are used. Use this for debugging if need. +# +# CRAN note: Cargo and Rustc versions are reported during +# configure via tools/msrv.R. +# +# vendor.tar.xz, if present, is unzipped and used for offline compilation. $(STATLIB): - cargo build --lib --release -C_clean: - rm -Rf $(SHLIB) $(STATLIB) $(OBJECTS) \ No newline at end of file + if [ -f ./rust/vendor.tar.xz ]; then \ + tar xf rust/vendor.tar.xz && \ + mkdir -p $(CARGOTMP) && \ + cp rust/vendor-config.toml $(CARGOTMP)/config.toml; \ + fi + + export CARGO_HOME=$(CARGOTMP) && \ + export PATH="$(PATH):$(HOME)/.cargo/bin" && \ + RUSTFLAGS="$(RUSTFLAGS) --print=native-static-libs" cargo build --lib --manifest-path=./rust/Cargo.toml --target-dir $(TARGET_DIR) + + # Always clean up CARGOTMP + rm -Rf $(CARGOTMP); + +rust_clean: $(SHLIB) + rm -Rf $(CARGOTMP) $(VENDOR_DIR) + +clean: + rm -Rf $(SHLIB) $(STATLIB) $(OBJECTS) $(TARGET_DIR) $(VENDOR_DIR) diff --git a/bindings/r/src/Makevars.in b/bindings/r/src/Makevars.in new file mode 100644 index 0000000..df74401 --- /dev/null +++ b/bindings/r/src/Makevars.in @@ -0,0 +1,42 @@ +TARGET_DIR = ./rust/target +LIBDIR = $(TARGET_DIR)/@LIBDIR@ +STATLIB = $(LIBDIR)/libcodelist.a +PKG_LIBS = -L$(LIBDIR) -lcodelist + +all: $(SHLIB) rust_clean + +.PHONY: $(STATLIB) + +$(SHLIB): $(STATLIB) + +CARGOTMP = $(CURDIR)/.cargo +VENDOR_DIR = $(CURDIR)/vendor + + +# RUSTFLAGS appends --print=native-static-libs to ensure that +# the correct linkers are used. Use this for debugging if need. +# +# CRAN note: Cargo and Rustc versions are reported during +# configure via tools/msrv.R. +# +# vendor.tar.xz, if present, is unzipped and used for offline compilation. +$(STATLIB): + + if [ -f ./rust/vendor.tar.xz ]; then \ + tar xf rust/vendor.tar.xz && \ + mkdir -p $(CARGOTMP) && \ + cp rust/vendor-config.toml $(CARGOTMP)/config.toml; \ + fi + + export CARGO_HOME=$(CARGOTMP) && \ + export PATH="$(PATH):$(HOME)/.cargo/bin" && \ + RUSTFLAGS="$(RUSTFLAGS) --print=native-static-libs" cargo build @CRAN_FLAGS@ --lib @PROFILE@ --manifest-path=./rust/Cargo.toml --target-dir $(TARGET_DIR) @TARGET@ + + # Always clean up CARGOTMP + rm -Rf $(CARGOTMP); + +rust_clean: $(SHLIB) + rm -Rf $(CARGOTMP) $(VENDOR_DIR) @CLEAN_TARGET@ + +clean: + rm -Rf $(SHLIB) $(STATLIB) $(OBJECTS) $(TARGET_DIR) $(VENDOR_DIR) diff --git a/bindings/r/src/Makevars.win.in b/bindings/r/src/Makevars.win.in new file mode 100644 index 0000000..bd5ff5d --- /dev/null +++ b/bindings/r/src/Makevars.win.in @@ -0,0 +1,39 @@ +TARGET = $(subst 64,x86_64,$(subst 32,i686,$(WIN)))-pc-windows-gnu + +TARGET_DIR = ./rust/target +LIBDIR = $(TARGET_DIR)/$(TARGET)/@LIBDIR@ +STATLIB = $(LIBDIR)/libcodelist.a +PKG_LIBS = -L$(LIBDIR) -lcodelist -lws2_32 -ladvapi32 -luserenv -lbcrypt -lntdll + +all: $(SHLIB) rust_clean + +.PHONY: $(STATLIB) + +$(SHLIB): $(STATLIB) + +CARGOTMP = $(CURDIR)/.cargo +VENDOR_DIR = vendor + +$(STATLIB): + mkdir -p $(TARGET_DIR)/libgcc_mock + touch $(TARGET_DIR)/libgcc_mock/libgcc_eh.a + + if [ -f ./rust/vendor.tar.xz ]; then \ + tar xf rust/vendor.tar.xz && \ + mkdir -p $(CARGOTMP) && \ + cp rust/vendor-config.toml $(CARGOTMP)/config.toml; \ + fi + + # Build the project using Cargo with additional flags + export CARGO_HOME=$(CARGOTMP) && \ + export LIBRARY_PATH="$(LIBRARY_PATH);$(CURDIR)/$(TARGET_DIR)/libgcc_mock" && \ + RUSTFLAGS="$(RUSTFLAGS) --print=native-static-libs" cargo build @CRAN_FLAGS@ --target=$(TARGET) --lib @PROFILE@ --manifest-path=rust/Cargo.toml --target-dir=$(TARGET_DIR) + + # Always clean up CARGOTMP + rm -Rf $(CARGOTMP); + +rust_clean: $(SHLIB) + rm -Rf $(CARGOTMP) $(VENDOR_DIR) @CLEAN_TARGET@ + +clean: + rm -Rf $(SHLIB) $(STATLIB) $(OBJECTS) $(TARGET_DIR) $(VENDOR_DIR) diff --git a/bindings/r/src/codelist-win.def b/bindings/r/src/codelist-win.def new file mode 100644 index 0000000..e69de29 diff --git a/bindings/r/src/lib.rs b/bindings/r/src/lib.rs index cc19cfa..8da83a6 100644 --- a/bindings/r/src/lib.rs +++ b/bindings/r/src/lib.rs @@ -1,12 +1,67 @@ +use codelist_rs::{codelist::CodeList as BaseCodelist, metadata::Metadata, types::CodeListType}; use extendr_api::prelude::*; #[extendr] fn hello() -> &'static str { println!("hello function called"); - "hello" + "hello there" +} + +#[extendr] +struct Codelist { + name: String, + inner: BaseCodelist, +} + +#[extendr] +impl Codelist { + fn new(name: String) -> Self { + let mut codelist = BaseCodelist::new( + "test_codelist".to_string(), + CodeListType::ICD10, + Metadata::default(), + None, + ); + codelist.add_entry("R65.2".to_string(), None, None).unwrap(); + + codelist + .add_entry( + "A48.51".to_string(), + Some("Infant botulism".to_string()), + Some("test comment".to_string()), + ) + .unwrap(); + let inner = codelist; + + Codelist { name, inner } + } + + fn set_name(&mut self, new_name: String) -> &mut Self { + self.name = new_name; + self + } + + fn get_entries(&self) -> List { + let entries: Vec = self.inner + .entries + .iter() + .map(|(code, (description, comment))| { + list!( + code = code.clone(), + description = description.clone().unwrap_or_default(), + comment = comment.clone().unwrap_or_default() + ) + }) + .collect(); + + List::from_values(entries) + } + + } extendr_module! { mod codelist; + impl Codelist; fn hello; -} +} \ No newline at end of file diff --git a/bindings/r/src/rust/Cargo.toml b/bindings/r/src/rust/Cargo.toml new file mode 100644 index 0000000..ac5b790 --- /dev/null +++ b/bindings/r/src/rust/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = 'codelist' +publish = false +version = '0.1.0' +edition = '2021' +rust-version = '1.65' + +[lib] +crate-type = [ 'staticlib' ] +name = 'codelist' + +[dependencies] +extendr-api = '*' +codelist-rs = { git = 'https://github.com/ehrtools/codelist-tools.git', package = 'codelist-rs' } \ No newline at end of file diff --git a/bindings/r/src/rust/src/lib.rs b/bindings/r/src/rust/src/lib.rs new file mode 100644 index 0000000..87bd246 --- /dev/null +++ b/bindings/r/src/rust/src/lib.rs @@ -0,0 +1,67 @@ +#![allow(clippy::incompatible_msrv)] +use codelist_rs::{codelist::CodeList as BaseCodelist, metadata::Metadata, types::CodeListType}; +use extendr_api::prelude::*; + +#[extendr] +fn hello() -> &'static str { + println!("hello function called"); + "hello there!" +} + +#[extendr] +struct Codelist { + name: String, + inner: BaseCodelist, +} + +#[extendr] +impl Codelist { + fn new(name: String) -> Self { + let mut codelist = BaseCodelist::new( + "test_codelist".to_string(), + CodeListType::ICD10, + Metadata::default(), + None, + ); + codelist.add_entry("R65.2".to_string(), None, None).unwrap(); + + codelist + .add_entry( + "A48.51".to_string(), + Some("Infant botulism".to_string()), + Some("test comment".to_string()), + ) + .unwrap(); + let inner = codelist; + + Codelist { name, inner } + } + + fn set_name(&mut self, new_name: String) -> &mut Self { + self.name = new_name; + self + } + + fn get_entries(&self) -> List { + let entries: Vec = self + .inner + .entries + .iter() + .map(|(code, (description, comment))| { + list!( + code = code.clone(), + description = description.clone().unwrap_or_default(), + comment = comment.clone().unwrap_or_default() + ) + }) + .collect(); + + List::from_values(entries) + } +} + +extendr_module! { + mod codelist; + impl Codelist; + fn hello; +} diff --git a/bindings/r/tools/config.R b/bindings/r/tools/config.R new file mode 100644 index 0000000..f317f06 --- /dev/null +++ b/bindings/r/tools/config.R @@ -0,0 +1,104 @@ +# Note: Any variables prefixed with `.` are used for text +# replacement in the Makevars.in and Makevars.win.in + +# check the packages MSRV first +source("tools/msrv.R") + +# check DEBUG and NOT_CRAN environment variables +env_debug <- Sys.getenv("DEBUG") +env_not_cran <- Sys.getenv("NOT_CRAN") + +# check if the vendored zip file exists +vendor_exists <- file.exists("src/rust/vendor.tar.xz") + +is_not_cran <- env_not_cran != "" +is_debug <- env_debug != "" + +if (is_debug) { + # if we have DEBUG then we set not cran to true + # CRAN is always release build + is_not_cran <- TRUE + message("Creating DEBUG build.") +} + +if (!is_not_cran) { + message("Building for CRAN.") +} + +# we set cran flags only if NOT_CRAN is empty and if +# the vendored crates are present. +.cran_flags <- ifelse( + !is_not_cran && vendor_exists, + "-j 2 --offline", + "" +) + +# when DEBUG env var is present we use `--debug` build +.profile <- ifelse(is_debug, "", "--release") +.clean_targets <- ifelse(is_debug, "", "$(TARGET_DIR)") + +# We specify this target when building for webR +webr_target <- "wasm32-unknown-emscripten" + +# here we check if the platform we are building for is webr +is_wasm <- identical(R.version$platform, webr_target) + +# print to terminal to inform we are building for webr +if (is_wasm) { + message("Building for WebR") +} + +# we check if we are making a debug build or not +# if so, the LIBDIR environment variable becomes: +# LIBDIR = $(TARGET_DIR)/{wasm32-unknown-emscripten}/debug +# this will be used to fill out the LIBDIR env var for Makevars.in +target_libpath <- if (is_wasm) "wasm32-unknown-emscripten" else NULL +cfg <- if (is_debug) "debug" else "release" + +# used to replace @LIBDIR@ +.libdir <- paste(c(target_libpath, cfg), collapse = "/") + +# use this to replace @TARGET@ +# we specify the target _only_ on webR +# there may be use cases later where this can be adapted or expanded +.target <- ifelse(is_wasm, paste0("--target=", webr_target), "") + +# read in the Makevars.in file checking +is_windows <- .Platform[["OS.type"]] == "windows" + +# if windows we replace in the Makevars.win.in +mv_fp <- ifelse( + is_windows, + "src/Makevars.win.in", + "src/Makevars.in" +) + +# set the output file +mv_ofp <- ifelse( + is_windows, + "src/Makevars.win", + "src/Makevars" +) + +# delete the existing Makevars{.win} +if (file.exists(mv_ofp)) { + message("Cleaning previous `", mv_ofp, "`.") + invisible(file.remove(mv_ofp)) +} + +# read as a single string +mv_txt <- readLines(mv_fp) + +# replace placeholder values +new_txt <- gsub("@CRAN_FLAGS@", .cran_flags, mv_txt) |> + gsub("@PROFILE@", .profile, x = _) |> + gsub("@CLEAN_TARGET@", .clean_targets, x = _) |> + gsub("@LIBDIR@", .libdir, x = _) |> + gsub("@TARGET@", .target, x = _) + +message("Writing `", mv_ofp, "`.") +con <- file(mv_ofp, open = "wb") +writeLines(new_txt, con, sep = "\n") +close(con) + +message("`tools/config.R` has finished.") diff --git a/bindings/r/tools/msrv.R b/bindings/r/tools/msrv.R new file mode 100644 index 0000000..59a61ab --- /dev/null +++ b/bindings/r/tools/msrv.R @@ -0,0 +1,116 @@ +# read the DESCRIPTION file +desc <- read.dcf("DESCRIPTION") + +if (!"SystemRequirements" %in% colnames(desc)) { + fmt <- c( + "`SystemRequirements` not found in `DESCRIPTION`.", + "Please specify `SystemRequirements: Cargo (Rust's package manager), rustc`" + ) + stop(paste(fmt, collapse = "\n")) +} + +# extract system requirements +sysreqs <- desc[, "SystemRequirements"] + +# check that cargo and rustc is found +if (!grepl("cargo", sysreqs, ignore.case = TRUE)) { + stop("You must specify `Cargo (Rust's package manager)` in your `SystemRequirements`") +} + +if (!grepl("rustc", sysreqs, ignore.case = TRUE)) { + stop("You must specify `Cargo (Rust's package manager), rustc` in your `SystemRequirements`") +} + +# split into parts +parts <- strsplit(sysreqs, ", ")[[1]] + +# identify which is the rustc +rustc_ver <- parts[grepl("rustc", parts)] + +# perform checks for the presence of rustc and cargo on the OS +no_cargo_msg <- c( + "----------------------- [CARGO NOT FOUND]--------------------------", + "The 'cargo' command was not found on the PATH. Please install Cargo", + "from: https://www.rust-lang.org/tools/install", + "", + "Alternatively, you may install Cargo from your OS package manager:", + " - Debian/Ubuntu: apt-get install cargo", + " - Fedora/CentOS: dnf install cargo", + " - macOS: brew install rust", + "-------------------------------------------------------------------" +) + +no_rustc_msg <- c( + "----------------------- [RUST NOT FOUND]---------------------------", + "The 'rustc' compiler was not found on the PATH. Please install", + paste(rustc_ver, "or higher from:"), + "https://www.rust-lang.org/tools/install", + "", + "Alternatively, you may install Rust from your OS package manager:", + " - Debian/Ubuntu: apt-get install rustc", + " - Fedora/CentOS: dnf install rustc", + " - macOS: brew install rust", + "-------------------------------------------------------------------" +) + +# Add {user}/.cargo/bin to path before checking +new_path <- paste0( + Sys.getenv("PATH"), + ":", + paste0(Sys.getenv("HOME"), "/.cargo/bin") +) + +# set the path with the new path +Sys.setenv("PATH" = new_path) + +# check for rustc installation +rustc_version <- tryCatch( + system("rustc --version", intern = TRUE), + error = function(e) { + stop(paste(no_rustc_msg, collapse = "\n")) + } +) + +# check for cargo installation +cargo_version <- tryCatch( + system("cargo --version", intern = TRUE), + error = function(e) { + stop(paste(no_cargo_msg, collapse = "\n")) + } +) + +# helper function to extract versions +extract_semver <- function(ver) { + if (grepl("\\d+\\.\\d+(\\.\\d+)?", ver)) { + sub(".*?(\\d+\\.\\d+(\\.\\d+)?).*", "\\1", ver) + } else { + NA + } +} + +# get the MSRV +msrv <- extract_semver(rustc_ver) + +# extract current version +current_rust_version <- extract_semver(rustc_version) + +# perform check +if (!is.na(msrv)) { + # -1 when current version is later + # 0 when they are the same + # 1 when MSRV is newer than current + is_msrv <- utils::compareVersion(msrv, current_rust_version) + if (is_msrv == 1) { + fmt <- paste0( + "\n------------------ [UNSUPPORTED RUST VERSION]------------------\n", + "- Minimum supported Rust version is %s.\n", + "- Installed Rust version is %s.\n", + "---------------------------------------------------------------" + ) + stop(sprintf(fmt, msrv, current_rust_version)) + } +} + +# print the versions +versions_fmt <- "Using %s\nUsing %s" +message(sprintf(versions_fmt, cargo_version, rustc_version))