Skip to content
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
2 changes: 1 addition & 1 deletion src/renv-cache/cmd/renv-restore
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ usage() {
PROJECT_DIR="."
RESTORE=false
UPDATE=false
PKG_EXCLUDE=""
PKG_EXCLUDE="${PKGEXCLUDE:-""}"
DEBUG=false
USE_PAK=false
DEBUG_RENV=false
Expand Down
2 changes: 1 addition & 1 deletion src/renv-cache/cmd/renv-restore-build
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ usage() {
# Initialize variables
RESTORE=false
UPDATE=false
PKG_EXCLUDE=""
PKG_EXCLUDE="${PKGEXCLUDE:-""}"
DEBUG=false
USE_PAK=false
RENV_DIR="/usr/local/share/renv-cache/renv"
Expand Down
11 changes: 11 additions & 0 deletions src/renv-cache/devcontainer-feature.json
Original file line number Diff line number Diff line change
Expand Up @@ -50,8 +50,19 @@
"description": "Whether to print debug information during renv restore.",
"default": false
},
"repositories": {
"type": "string",
"description": "Comma-separated list of GitHub repos to clone. Must support branch and profile syntax: user/repo@branch:profile. If :profile is omitted, it defaults to the root renv.lock.",
"default": ""
},
"pkg": {
"type": "string",
"description": "Comma-separated list of specific packages to cache explicitly.",
"default": ""
},
"installSystemRequirements": {
"type": "boolean",
"description": "Uses the Posit API to install apt-dependencies.",
"default": true
},
"cranMirror": {
Expand Down
302 changes: 170 additions & 132 deletions src/renv-cache/install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -26,25 +26,29 @@ fi
# --- Everything below runs under bash ---
set -e

if ! command -v Rscript >/dev/null 2>&1; then
echo "(!) Cannot run Rscript. Please ensure R is installed before running the renv-cache feature."
exit 1
fi

# Configuration variables with default values
SET_R_LIB_PATHS="${SETRLIBPATHS:-true}"
OVERRIDE_TOKENS_AT_INSTALL="${OVERRIDETOKENSATINSTALL:-true}"
RESTORE="${RESTORE:-true}"
UPDATE="${UPDATE:-false}"
PKG_EXCLUDE="${PKGEXCLUDE:-}"
DEBUG="${DEBUG:-false}"
USE_PAK="${USEPAK:-false}"
RENV_DIR="${RENVDIR:-"/usr/local/share/renv-cache/renv"}"
DEBUG_RENV="${DEBUGRENV:-false}"

INSTALL_SYSREQS="${INSTALLSYSTEMREQUIREMENTS:-true}"
REPOSITORIES="${REPOSITORIES:-""}"
PKG="${PKG:-""}"
PKG_EXCLUDE="${PKGEXCLUDE:-""}"
INSTALL_SYSREQS="${INSTALLSYSTEMREQUIREMENTS:-"true"}"
CRAN_MIRROR="${CRANMIRROR:-"https://cloud.r-project.org"}"

if [ -n "$PKG" ] || [ -n "$REPOSITORIES" ] || { [ -n "$RENV_DIR" ] && [ -d "$RENV_DIR" ]; }; then
if ! command -v Rscript >/dev/null 2>&1; then
echo "(!) Cannot run Rscript. Please ensure R is installed before running the renv-cache feature."
exit 1
fi
fi

# Resolve target user
USERNAME=${USERNAME:-${_REMOTE_USER:-"automatic"}}
if [ "$USERNAME" = "auto" ] || [ "$USERNAME" = "automatic" ]; then
Expand All @@ -67,6 +71,94 @@ export USERNAME
export INSTALL_SYSREQS
export CRAN_MIRROR

apt-get update -y && apt-get install -y --no-install-recommends jq git curl lsb-release

if [ -n "$GITHUB_PAT" ]; then
export GITHUB_TOKEN="$GITHUB_PAT"
elif [ -n "$GITHUB_TOKEN" ]; then
export GITHUB_PAT="$GITHUB_TOKEN"
fi

# CORE FUNCTION: Process a directory containing an renv lockfile

process_renv_dir() {
local TARGET_DIR=$1
local PROFILE=$2

if [ -n "$PROFILE" ]; then
export RENV_PROFILE="$PROFILE"
local LOCK_PATH="renv/profiles/$PROFILE/renv.lock"
else
unset RENV_PROFILE
local LOCK_PATH="renv.lock"
fi

if [ ! -f "$TARGET_DIR/$LOCK_PATH" ]; then
echo "No lockfile found at $TARGET_DIR/$LOCK_PATH. Skipping."
return 0
fi

echo "Processing lockfile $LOCK_PATH in $TARGET_DIR..."
pushd "$TARGET_DIR" > /dev/null
chown -R "${USERNAME}:${USERNAME}" .

# System Requirements Check via Posit API
if [ "${INSTALL_SYSREQS}" = "true" ]; then
echo "Resolving system requirements..."
PKGS=$(jq -r '.Packages | keys[]' "$LOCK_PATH" | paste -sd, -)
OS_DIST=$(lsb_release -is | tr '[:upper:]' '[:lower:]')
OS_RELEASE=$(lsb_release -cs)
SYSREQ_URL="https://packagemanager.posit.co/__api__/repos/1/sysreqs?all=false&pkgname=${PKGS}&distribution=${OS_DIST}&release=${OS_RELEASE}"
APT_PKGS=$(curl -sL "$SYSREQ_URL" | jq -r '.requirements[]?.requirements?.packages[]?' | sort -u | paste -sd" " -)
if [ -n "$APT_PKGS" ]; then
export DEBIAN_FRONTEND=noninteractive
apt-get install -y --no-install-recommends $APT_PKGS
fi
fi

# Recursive Strip & Purge (Security)
if [ -n "$PKG_EXCLUDE" ]; then
echo "Recursively stripping skipped packages and purging cache..."
export PKG_EXCLUDE="$PKG_EXCLUDE"
export RENV_LOCK_PATH="$LOCK_PATH"

su "${USERNAME}" -c "Rscript -e \"
skip_list <- trimws(unlist(strsplit(Sys.getenv('PKG_EXCLUDE'), ',')))
lock_path <- Sys.getenv('RENV_LOCK_PATH')
lock_data <- renv:::renv_json_read(lock_path)

if (!is.null(lock_data\\\$Packages)) {
changed <- TRUE
while (changed) {
changed <- FALSE
for (pkg_name in names(lock_data\\\$Packages)) {
reqs <- lock_data\\\$Packages[[pkg_name]]\\\$Requirements
if (!is.null(reqs) && any(reqs %in% skip_list)) {
if (!(pkg_name %in% skip_list)) {
skip_list <- c(skip_list, pkg_name)
changed <- TRUE
}
}
}
}
for (pkg in skip_list) lock_data\\\$Packages[[pkg]] <- NULL
renv:::renv_json_write(lock_data, file = lock_path)
}

for (pkg in skip_list) {
tryCatch({
renv::purge(pkg, prompt = FALSE)
message('Purged ', pkg, ' from global cache.')
}, error = function(e) NULL)
}
\""
fi

echo "Warming cache from $TARGET_DIR (Profile: ${PROFILE:-default})..."
su "${USERNAME}" -c "Rscript -e \"options(repos = c(CRAN = '${CRAN_MIRROR}')); renv::restore()\""
popd > /dev/null
}


# Function to log debug messages if enabled
debug() {
Expand Down Expand Up @@ -278,73 +370,6 @@ reset_tokens_after_install() {
fi
}

# Function to restore R environment using renv
restore() {
# Copy and set execute permissions for renv restore scripts
copy_and_set_execute_bit renv-restore
copy_and_set_execute_bit renv-restore-build
copy_and_set_execute_bit renv-lockfile-cache

# set renv cache mode and user
debug "USERNAME: $USERNAME"
debug "USER: $USER"
debug "REMOTE_USER: $_REMOTE_USER"
debug "CONTAINER_USER: $_CONTAINER_USER"

export RENV_CACHE_MODE="0755"
debug "RENV_CACHE_MODE: $RENV_CACHE_MODE"
if [ -n "$_REMOTE_USER" ]; then
export RENV_CACHE_USER="$_REMOTE_USER"
debug "RENV_CACHE_USER: $_REMOTE_USER"
fi

# Construct the command as an array
local command=(/usr/local/bin/renv-cache-renv-restore-build)

# Append options based on conditions
if [ "$RESTORE" = "true" ]; then
command+=("--restore")
fi

if [ "$UPDATE" = "true" ]; then
command+=("--update")
fi

if [ "$DEBUG" = "true" ]; then
command+=("--debug")
fi

if [ "$DEBUG_RENV" = "true" ]; then
command+=("--debug-renv")
fi

if [ "$USE_PAK" = "true" ]; then
command+=("--pak")
fi

if [ -n "$PKG_EXCLUDE" ]; then
command+=("--exclude")
command+=("$PKG_EXCLUDE")
fi

if [ -n "$RENV_DIR" ]; then
command+=("--directory")
command+=("$RENV_DIR")
fi

# Log the command for debugging purposes
echo "Executing command: ${command[*]}"

# Change ownership of internal directories and RENV_DIR so USERNAME can write to them
chown -R "${USERNAME}:${USERNAME}" "$RENV_DIR" /usr/local/share/renv-cache 2>/dev/null || true

# Execute the command with error handling as target user
if ! su "${USERNAME}" -c "${command[*]}"; then
echo "[ERROR] renv-cache-renv-restore-build failed with command: ${command[*]}"
exit 0
fi
}

# Function to perform cleanup tasks
clean_up() {
# Remove specified temporary directories
Expand All @@ -354,64 +379,77 @@ clean_up() {
empty_dir /var/lib/apt/lists
}

# Main function to orchestrate the execution of all tasks
main() {
install_renvvv
create_path_post_create_command
set_r_libs
set_tokens_for_install

# System Requirements Resolution
if [ "${INSTALL_SYSREQS}" = "true" ]; then
echo "Resolving system requirements via Posit Package Manager API..."

# We must look for renv.lock in subdirectories of RENV_DIR
if [ -d "$RENV_DIR" ]; then
apt-get update -y && apt-get install -y jq curl lsb-release

OS_DIST=$(lsb_release -is | tr '[:upper:]' '[:lower:]')
OS_RELEASE=$(lsb_release -cs)

# Collect all unique packages from all lockfiles
ALL_PKGS=""
for lockfile in $(find "$RENV_DIR" -mindepth 2 -maxdepth 2 -name "renv.lock" 2>/dev/null); do
PKGS=$(jq -r '.Packages | keys[]?' "$lockfile" | paste -sd, -)
if [ -n "$PKGS" ]; then
if [ -n "$ALL_PKGS" ]; then
ALL_PKGS="$ALL_PKGS,$PKGS"
else
ALL_PKGS="$PKGS"
fi
fi
done

if [ -n "$ALL_PKGS" ]; then
# Deduplicate the comma-separated list
ALL_PKGS=$(echo "$ALL_PKGS" | tr ',' '\n' | sort -u | paste -sd, -)

# Query Posit API
SYSREQ_URL="https://packagemanager.posit.co/__api__/repos/1/sysreqs?all=false&pkgname=${ALL_PKGS}&distribution=${OS_DIST}&release=${OS_RELEASE}"

# Parse the required apt packages and install
APT_PKGS=$(curl -sL "$SYSREQ_URL" | jq -r '.requirements[]?.requirements?.packages[]?' | sort -u | paste -sd" " -)

if [ -n "$APT_PKGS" ]; then
echo "Installing system requirements: $APT_PKGS"
export DEBIAN_FRONTEND=noninteractive
apt-get install -y --no-install-recommends $APT_PKGS
else
echo "No additional system requirements found."
fi
else
echo "No packages found in lockfiles."
fi
# EXECUTION WORKFLOW
install_renvvv
create_path_post_create_command
set_r_libs
set_tokens_for_install

# 1. Process Local Lockfile (Backward Compatibility)
if [ -n "$RENV_DIR" ] && [ -d "$RENV_DIR" ]; then
# We must look for renv.lock in subdirectories of RENV_DIR
for dir in $(find "$RENV_DIR" -mindepth 1 -maxdepth 1 -type d 2>/dev/null); do
process_renv_dir "$dir" ""
done
fi

# 2. Process Dynamic Repositories
if [ -n "$REPOSITORIES" ]; then
TMP_REPO_DIR=$(mktemp -d)
IFS=',' read -ra REPO_ARRAY <<< "$REPOSITORIES"

for REPO_SPEC in "${REPO_ARRAY[@]}"; do
REPO_SPEC=$(echo "$REPO_SPEC" | xargs)

# Extract Profile (everything after the colon)
if [[ "$REPO_SPEC" == *":"* ]]; then
PROFILE="${REPO_SPEC##*:}"
REPO_SPEC="${REPO_SPEC%:*}"
else
PROFILE=""
fi
fi

restore
reset_tokens_after_install
clean_up
}
# Extract Branch (everything after the @)
if [[ "$REPO_SPEC" == *"@"* ]]; then
REPO_PATH="${REPO_SPEC%%@*}"
BRANCH="${REPO_SPEC##*@}"
else
REPO_PATH="$REPO_SPEC"
BRANCH=""
fi

TARGET_DIR="${TMP_REPO_DIR}/${REPO_PATH##*/}"
CLONE_URL="https://${GITHUB_PAT:-}@github.com/${REPO_PATH}.git"

echo "Cloning ${REPO_PATH}..."
if [ -n "$BRANCH" ]; then
git clone --branch "$BRANCH" --single-branch "$CLONE_URL" "$TARGET_DIR"
else
git clone "$CLONE_URL" "$TARGET_DIR"
fi

process_renv_dir "$TARGET_DIR" "$PROFILE"
done
rm -rf "$TMP_REPO_DIR"
fi

# 3. Process Explicit Packages (No Lockfile)
if [ -n "$PKG" ]; then
TMP_PKG_DIR=$(mktemp -d)
chown "${USERNAME}:${USERNAME}" "$TMP_PKG_DIR"
pushd "$TMP_PKG_DIR" > /dev/null

echo "Warming cache with explicit packages: ${PKG}..."
su "${USERNAME}" -c "Rscript -e \"
options(repos = c(CRAN = '${CRAN_MIRROR}'))
renv::init(bare = TRUE, restart = FALSE)
pkgs <- trimws(unlist(strsplit('${PKG}', ',')))
renv::install(pkgs)
\""
popd > /dev/null
rm -rf "$TMP_PKG_DIR"
fi

# Execute the main function
main
echo "renv-cache installation complete!"
reset_tokens_after_install
clean_up
Loading