/*==========================================================
* 1. PROC R: MINIMAL EXAMPLE
*==========================================================*/
proc r;
submit;
x <- c(10, 20, 30, 40, 50)
mean(x)
endsubmit;
run;
/*==========================================================
* 2. MANAGING R ENVIRONMENTS: SETTINGS
*==========================================================*/
/* renv does not replace the administrator-level setup that makes R available
in SAS Viya. It adds dependency management per project on top of it. */
/* Folder of the R environment; the last folder is its name */
%let r_env_path = /sasdata/r_workspace/PROJECT;
/* Location of r_env.R on the server */
%let r_scripts = /sasdata/r_workspace/_scripts/r_env.R;
/* Packages to install, separated by spaces */
%let r_packages = jsonlite data.table arrow haven;
#===========================================================
# 3. MANAGING R ENVIRONMENTS: HELPER FUNCTIONS (R_ENV.R)
#===========================================================
# Helpers for working with renv environments from PROC R.
# Source this file at the start of every PROC R block.
# Server settings
r_home <- "/tmp"
# The server has no compiler, so packages have to arrive prebuilt. Posit Package
# Manager serves prebuilt Linux packages per distribution.
r_ppm <- "https://packagemanager.posit.co/cran/latest"
r_ppm_linux <- "https://packagemanager.posit.co/cran/__linux__/%s/latest"
# Reads /etc/os-release and turns it into the name Posit Package Manager uses.
r_detect_distro <- function() {
if (!file.exists("/etc/os-release")) return(NULL)
lines <- readLines("/etc/os-release", warn = FALSE)
lines <- lines[grepl("^[A-Z_]+=", lines)]
keys <- sub("=.*$", "", lines)
vals <- gsub("\"", "", sub("^[^=]*=", "", lines))
names(vals) <- keys
id <- vals[["ID"]]
version <- if ("VERSION_ID" %in% keys) vals[["VERSION_ID"]] else ""
major <- sub("\\..*$", "", version)
switch(id,
ubuntu = , debian = if ("VERSION_CODENAME" %in% keys) vals[["VERSION_CODENAME"]],
centos = if (major == "7") "centos7" else paste0("rhel", major),
rhel = , rocky = , almalinux = , ol = paste0("rhel", major),
`opensuse-leap` = , opensuse = , sles = paste0("opensuse", gsub("\\.", "", version)),
NULL)
}
# Everything a fresh R session needs. Every function below calls this, so the
# code also works right after `proc r restart`.
r_env_setup <- function(path) {
path <- normalizePath(path, mustWork = FALSE)
Sys.setenv(HOME = r_home, TMPDIR = r_home) # the real HOME is read-only
options(renv.consent = TRUE)
Sys.setenv(RENV_CONFIG_CACHE_ENABLED = "FALSE", # the renv cache is wiped every session
RENV_CONFIG_SYNCHRONIZED_CHECK = "FALSE")
# Posit Package Manager only serves prebuilt (binary) packages if R identifies
# its version and platform in the HTTP user agent. Without that it serves source
# packages, which cannot be installed here because the server has no compiler.
# So set the user agent before any package is downloaded.
options(HTTPUserAgent = sprintf("R/%s R (%s)", getRversion(),
paste(getRversion(), R.version$platform,
R.version$arch, R.version$os)))
distro <- r_detect_distro()
if (is.null(distro)) {
warning("Could not work out the Linux distribution. ",
"Packages will come as source and the install may fail.")
}
options(repos = c(PPM = if (is.null(distro)) r_ppm else sprintf(r_ppm_linux, distro)))
options(install.packages.compile.from.source = "never")
path
}
# Creates the environment. Run once; running it again changes nothing.
create_r_env <- function(path) {
path <- r_env_setup(path)
dir.create(path, recursive = TRUE, showWarnings = FALSE)
if (!dir.exists(file.path(path, "renv"))) {
setwd(path)
# bare = TRUE: do not scan the folder and install what it finds, that step fails here
renv::init(project = path, bare = TRUE, restart = FALSE)
}
load_env(path)
}
# Activates the environment. Cheap, so call it in every PROC R block.
load_env <- function(path, restore = TRUE) {
path <- r_env_setup(path)
setwd(path)
renv::load(project = path)
if (restore && file.exists(file.path(path, "renv.lock"))) {
renv::restore(project = path, library = renv::paths$library(project = path),
prompt = FALSE)
}
invisible(path)
}
# Installs packages and records them in renv.lock.
install_and_lock <- function(path, packages) {
path <- load_env(path, restore = FALSE)
lib <- renv::paths$library(project = path)
packages <- unlist(strsplit(packages, "[ ,]+"))
packages <- packages[nzchar(packages)]
todo <- setdiff(packages, rownames(installed.packages(lib.loc = lib)))
if (length(todo)) install.packages(todo, lib = lib)
missing <- setdiff(packages, rownames(installed.packages(lib.loc = lib)))
if (length(missing)) stop("Not installed: ", paste(missing, collapse = ", "))
# Lock by name: renv's default snapshot only looks at R scripts, which would
# leave the lockfile empty for an environment that is driven from SAS.
have <- installed.packages(lib.loc = lib)
renv::snapshot(project = path, library = lib,
packages = rownames(have)[is.na(have[, "Priority"])], prompt = FALSE)
invisible(path)
}
# Deletes the environment.
remove_env <- function(path) {
path <- normalizePath(path, mustWork = FALSE)
if (!dir.exists(file.path(path, "renv"))) stop(path, " is not an renv environment.")
setwd(dirname(path))
unlink(path, recursive = TRUE, force = TRUE)
invisible(!dir.exists(path))
}
# Shows where R is looking and what is installed.
r_env_status <- function() {
cat("Working directory:", getwd(), "\n")
cat("renv project :", Sys.getenv("RENV_PROJECT"), "\n")
cat("Repository :", paste(getOption("repos"), collapse = ", "), "\n")
cat("Library paths :\n"); cat(paste0(" ", .libPaths()), sep = "\n")
cat("Installed :",
paste(rownames(installed.packages(lib.loc = .libPaths()[1])), collapse = ", "), "\n")
}
/*==========================================================
* 4. MANAGING R ENVIRONMENTS: CREATE THE ENVIRONMENT (ONCE)
*==========================================================*/
/* By default only the person who creates the environment has permissions on
it. Sharing it with colleagues requires a permission change on the server,
after which everyone in that group can also delete it. */
proc r restart;
submit;
source(symget("r_scripts"))
create_r_env(symget("r_env_path"))
r_env_status()
endsubmit;
quit;
/*==========================================================
* 5. MANAGING R ENVIRONMENTS: INSTALL PACKAGES AND UPDATE RENV.LOCK
*==========================================================*/
/* Packages that are already installed are skipped, so you can add new ones to
r_packages and run this step again. */
proc r;
submit;
source(symget("r_scripts"))
install_and_lock(symget("r_env_path"), symget("r_packages"))
endsubmit;
quit;
/*==========================================================
* 6. MANAGING R ENVIRONMENTS: USE THE ENVIRONMENT
*==========================================================*/
/* Start every PROC R block with source() and load_env(). If load_env() is
forgotten, packages end up in the wrong library and seem to disappear later.
load_env() restores the packages from renv.lock when it exists, so the
lockfile can also be used to rebuild an environment or to copy one: create
a new environment, place the renv.lock file in it and run load_env(). */
proc r restart;
submit;
source(symget("r_scripts"))
load_env(symget("r_env_path"))
show(mtcars)
endsubmit;
quit;
/*==========================================================
* 7. MANAGING R ENVIRONMENTS: REMOVE THE ENVIRONMENT
*==========================================================*/
/* Check the path first: this deletes the packages and renv.lock as well. */
proc r restart;
submit;
source(symget("r_scripts"))
remove_env(symget("r_env_path"))
endsubmit;
quit;
/*==========================================================
* 8. PROC R: MOVING DATA BETWEEN SAS AND R
*==========================================================*/
%let r_env_path = /sasdata/r_workspace/PROJECT;
%let r_scripts = /sasdata/r_workspace/_scripts/r_env.R;
proc r restart;
submit;
source(symget("r_scripts"))
load_env(symget("r_env_path"))
# SAS -> R
class <- sd2df("SASHELP.CLASS")
# Work in R
means <- aggregate(cbind(Height, Weight) ~ Sex, data = class, FUN = mean)
# R -> SAS
df2sd(means, "means", "WORK")
endsubmit;
quit;
proc print data=work.means noobs;
title "Means per sex, calculated in R";
run;
title;
/*==========================================================
* 9. PROC R: SHOWING R OUTPUT IN THE RESULTS WINDOW
*==========================================================*/
%let r_env_path = /sasdata/r_workspace/PROJECT;
%let r_scripts = /sasdata/r_workspace/_scripts/r_env.R;
proc r restart;
submit;
source(symget("r_scripts"))
load_env(symget("r_env_path"))
class <- sd2df("SASHELP.CLASS")
show(class, "Class", 10) # requires the arrow package
endsubmit;
quit;
#===========================================================
# 10. STANDALONE R PROGRAM (R CODE EDITOR, NO PROC R WRAPPER)
# (SAS Viya Stable 2026.04 and later)
#===========================================================
# Run this as an R program in the R code editor (SAS Viya Stable 2026.04 and
# later). The macro variables of a SAS program are not available here, so the
# paths are written out.
source("/sasdata/r_workspace/_scripts/r_env.R")
load_env("/sasdata/r_workspace/PROJECT")
# SAS -> R
class <- sd2df("SASHELP.CLASS")
# Work in R
means <- aggregate(cbind(Height, Weight) ~ Sex, data = class, FUN = mean)
# R -> SAS
df2sd(means, "means", "WORK")
show(means, "Means per sex", 10)
#===========================================================
# 11. R-SWAT: INSTALLING THE PACKAGE
#===========================================================
install.packages(
"swat",
repos = c(
"https://sassoftware.r-universe.dev",
"https://cloud.r-project.org"
)
)
#===========================================================
# 12. R-SWAT: CONNECTING TO CAS WITH USERNAME AND PASSWORD
# (not allowed on every server, use section 13 instead)
#===========================================================
# The credentials are read from environment variables. Define them in your
# ~/.Renviron file, which you can open from R with: file.edit("~/.Renviron")
#
# Add one NAME=value per line, without quotes, and end the file with an empty
# line. Then save the file and restart R, because .Renviron is read at startup:
# SAS_USER=your_username
# SAS_PASSWORD=your_password
# SAS_TOKEN=your_access_token
# CAS_CLIENT_SSL_CA_LIST=/path/to/trustedcerts.pem
#
# CAS_CLIENT_SSL_CA_LIST is only needed for a self-signed or internal certificate.
# Keep .Renviron out of version control, it contains credentials.
# For a quick test you can set a value for the current session only:
# Sys.setenv(SAS_USER = "your_username")
library(swat)
conn <- CAS(
hostname = "https://myviya.example.com/cas-shared-default-http/",
port = 443,
username = Sys.getenv("SAS_USER"),
password = Sys.getenv("SAS_PASSWORD")
)
#===========================================================
# 13. R-SWAT: CONNECTING TO CAS WITH AN OAUTH TOKEN
# (use this when passwords are not accepted, and in production)
#===========================================================
conn <- CAS(
hostname = "https://myviya.example.com",
port = 443,
protocol = "https",
path = "/cas-shared-default-http",
password = Sys.getenv("SAS_TOKEN")
)
#===========================================================
# 14. R-SWAT: INTERACTING WITH CAS: LISTING TABLES
#===========================================================
cas.table.tableInfo(conn, caslib = "PUBLIC")
#===========================================================
# 15. R-SWAT: RETRIEVING DATA FROM CAS
#===========================================================
hmeq <- defCasTable(conn, table = "HMEQ", caslib = "PUBLIC")
hmeq_r <- to.casDataFrame(hmeq)
head(hmeq_r)
summary(hmeq_r)
#===========================================================
# 16. R-SWAT: SENDING DATA FROM R TO CAS
#===========================================================
as.casTable(
conn,
hmeq_r,
casOut = list(name = "hmeq_r", caslib = "PUBLIC", promote = TRUE)
)