Skip to content
Open
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
10 changes: 5 additions & 5 deletions .github/workflows/main.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,12 @@ jobs:
run: |
set -x
if [[ "${{ github.ref }}" == 'refs/heads/main' ]]; then
# build, test and publish both versions on main.
# build, test and publish all versions on main.
# v1 will take ~2 hours
echo 'versions=["v1", "v2"]' >> "$GITHUB_OUTPUT"
echo 'versions=["v1", "v2", "v3"]' >> "$GITHUB_OUTPUT"
else
# only build v2 in CI as v1 is too slow
echo 'versions=["v2"]' >> "$GITHUB_OUTPUT"
# only build v2 and v3 in CI as v1 is too slow
echo 'versions=["v2", "v3"]' >> "$GITHUB_OUTPUT"
fi
tests:
needs: matrix
Expand All @@ -43,7 +43,7 @@ jobs:
uses: astral-sh/setup-uv@4db96194c378173c656ce18a155ffc14a9fc4355

- name: check
run: just check
run: just check ${{ matrix.version }}

- name: build
run: just build ${{ matrix.version }}
Expand Down
18 changes: 10 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,30 +4,32 @@ This repo manages the Docker image for the OpenSAFELY R runtime and rstudio-serv
images are based on a base Ubuntu LTS version, and come pre-installed with
a set of standard R packages.

The current latest version is `v2`, and you should use that unless you have
The current latest version is `v3`, and you should use that unless you have
a specific reason. You can use it in your `project.yaml` like so:

```
actions:
my_action:
run: r:v2 analysis/my_script.R
run: r:v3 analysis/my_script.R
```

The rstudio images are designed for use with `opensafely launch`. For example, to launch an rstudio-server session (i.e., RStudio running in a browser window) run the following in a Terminal from your research repository.

```sh
opensafely launch rstudio:v2
opensafely launch rstudio:v3
```

## Version List

Current available versions for the r image, in reverse chronological order:

- v3: Ubuntu 22.04 and R 4.5.3 - [full package list](v3/packages.md)
- v2: Ubuntu 22.04 and R 4.4.3 - [full package list](v2/packages.md)
- v1: Ubuntu 20.04 and R 4.0.5 - [full package list](v1/packages.md) - please note that v1 is deprecated, new projects should use r:v2.
- v1: Ubuntu 20.04 and R 4.0.5 - [full package list](v1/packages.md) - please note that v1 is deprecated, new projects should use r:v3.

Current available versions for the rstudio image, in reverse chronological order:

- v3: rstudio-server running r:v3
- v2: rstudio-server running r:v2
- v1: rstudio-server running r:v1

Expand All @@ -46,10 +48,10 @@ use `v1` instead of `latest`.

We do not plan to add anymore packages to the v1 image.

For the v2 image, the versions of R packages from CRAN tied to a CRAN date.
For the v2 and v3 images, the versions of R packages from CRAN tied to a CRAN date.
We can easily install any additional package from the same CRAN date.
Additional packages may be requested by creating an issue in the repo.
In the v2 image you can request packages that are not on CRAN.
In the v2 and v3 images you can request packages that are not on CRAN.

Occasionally, we will create a new major version of the image with all packages
updated to their latest version. We may also possibly remove old and uneeded
Expand All @@ -58,8 +60,8 @@ incompatible changes, which is occasionally needed.

### User installed R packages

Especially when working in say a Codespace using the rstudio:v2 image you may wish to add some packages for your post release analysis.
The v2 images are configured to allow you to install your own packages, which will be saved to a _.local-packages/r/v2_ directory in your research repository.
Especially when working in say a Codespace using the rstudio:v2 or rstudio:v3 images you may wish to add some packages for your post release analysis.
The v2 and v3 images are configured to allow you to install your own packages, which will be saved to a _.local-packages/r/v#_ directory in your research repository.
If the packages is on CRAN, to do this simply run the following.

```r
Expand Down
11 changes: 7 additions & 4 deletions justfile
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,9 @@ build version:
cp ${MAJOR_VERSION}/renv.lock ${MAJOR_VERSION}/renv.lock.bak
# cannot use docker compose run as it mangles the output
docker run --platform linux/amd64 --rm r:{{ version }} cat /renv/renv.lock > ${MAJOR_VERSION}/renv.lock
elif [ "{{ version }}" = "v2" ]; then
elif [[ "{{ version }}" =~ ^v[23]$ ]]; then
# update pkg.lock
cp ${MAJOR_VERSION}/pkg.lock ${MAJOR_VERSION}/pkg.lock.bak
[[ -f ${MAJOR_VERSION}/pkg.lock ]] && cp ${MAJOR_VERSION}/pkg.lock ${MAJOR_VERSION}/pkg.lock.bak || true
# cannot use docker compose run as it mangles the output
docker run --platform linux/amd64 --rm r:{{ version }} cat /pkg.lock > ${MAJOR_VERSION}/pkg.lock
fi
Expand Down Expand Up @@ -84,5 +84,8 @@ publish-rstudio version:
docker push ghcr.io/opensafely-core/rstudio:latest
fi

check:
uvx --python 3.13 toml-validator v2/packages.toml
check version:
#!/usr/bin/env bash
if [[ "{{ version }}" =~ ^v[23]$ ]]; then
uvx --python 3.13 toml-validator {{ version }}/packages.toml
fi
Original file line number Diff line number Diff line change
@@ -1,10 +1,24 @@
---
title: ""
output:
github_document:
html_preview: false
---

```{r setup, include=FALSE}
v <- Sys.getenv("MAJOR_VERSION")
r_img <- paste0("`r:", v, "`")
rstudio_img <- paste0("`rstudio:", v, "`")
local_dir <- paste0("_.local-packages/r/", v, "_")
```

# .local-packages a directory for user installed libraries

This directory contains user installed libraries, for say use running OpenSAFELY code in a Codespace or locally.
This directory contains user installed libraries, for say running OpenSAFELY code in a Codespace or locally.

## How to add and remove user installed R packages

The _.local-packages/r/v2_ directory contains user installed R packages to work in the `r:v2` and `rstudio:v2` images.
The `r local_dir` directory contains user installed R packages to work in the `r r_img` and `r rstudio_img` images.

You can install additional packages by running

Expand All @@ -14,7 +28,7 @@ install.packages("PACKAGENAME")

or by using the RStudio _Package_ pane _Install_ button.

Note that packages are installed from the same date from CRAN as the other packages in the `r:v2` image. This is to ensure user installed packages work with the other packages in the image (at the time of installation).
Note that packages are installed from the same date from CRAN as the other packages in the `r r_img` image. This is to ensure user installed packages work with the other packages in the image (at the time of installation).

If you no longer require a package you can also remove it as follows.

Expand Down
5 changes: 3 additions & 2 deletions scripts/packages.Rmd
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ pkgs <- pkgs %>%
arrange(Package, .locale = "en")
knitr::kable(pkgs, row.names = FALSE)
}
if (Sys.getenv("MAJOR_VERSION") == "v2") {
if (Sys.getenv("MAJOR_VERSION") %in% c("v2", "v3")) {
suppressMessages(library(dplyr))
pkglock <- jsonlite::fromJSON(paste0("/out/", Sys.getenv("MAJOR_VERSION"), "/pkg.lock"))
pkgs <- pkglock[["packages"]]
Expand All @@ -46,11 +46,12 @@ pkgs <- pkgs %>% mutate(
.default = metadataremoterepos
)
)
r_minor_version <- if (Sys.getenv("MAJOR_VERSION") == "v3") "4.5" else "4.4"
pkgs <- pkgs %>%
mutate(
url = case_when(
repotype == "cran" ~ paste0("<https://cran.r-project.org/package=", package, ">"),
repotype == "cranlike" ~ paste0("<", sub("/bin/linux/noble/4.4/", "/", metadataremoterepos), package, ">")
repotype == "cranlike" ~ paste0("<", sub(paste0("/bin/linux/noble/", r_minor_version, "/"), "/", metadataremoterepos), package, ">")
)
)
pkgs <- pkgs %>%
Expand Down
12 changes: 7 additions & 5 deletions scripts/rprofile-site-append-2.R
Original file line number Diff line number Diff line change
@@ -1,15 +1,17 @@
# If the local package directory exists set as first entry of R's library path
# This is so that install.packages() and remove.packages() will use this directory by default.
if (dir.exists('/workspace/.local-packages/r/v2'))
.libPaths(c('/workspace/.local-packages/r/v2', .libPaths()))
.local_pkg_dir <- paste0('/workspace/.local-packages/r/', Sys.getenv("MAJOR_VERSION"))
if (dir.exists(.local_pkg_dir))
.libPaths(c(.local_pkg_dir, .libPaths()))

# Monkey patch install.packages to create local package directory if it doesn't exist and
# create a .gitignore file within in and copy in the README.md.
# In R ... passes function arguments through to a subsequent call.
install.packages <- function(...) {
if (!dir.exists("/workspace/.local-packages/r/v2")) {
dir.create("/workspace/.local-packages/r/v2", recursive = TRUE)
.libPaths(c("/workspace/.local-packages/r/v2", .libPaths()))
local_pkg_dir <- paste0("/workspace/.local-packages/r/", Sys.getenv("MAJOR_VERSION"))
if (!dir.exists(local_pkg_dir)) {
dir.create(local_pkg_dir, recursive = TRUE)
.libPaths(c(local_pkg_dir, .libPaths()))
# create .gitignore file
file.create("/workspace/.local-packages/.gitignore")
fileconn <- file("/workspace/.local-packages/.gitignore")
Expand Down
8 changes: 4 additions & 4 deletions tests/test-loading-packages.R
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,12 @@ npkgs <- length(pkgs)

for (i in 1:npkgs) {
package <- pkgs[i]
suppressMessages({
if (!library(package, logical.return = TRUE, character.only = TRUE)) {
stop(paste("Package", package, "failed to load, please investigate"))
}
tryCatch({
suppressMessages(library(package, character.only = TRUE))
dtch <- paste0("package:", package)
detach(dtch, force = TRUE, unload = TRUE, character.only = TRUE)
}, error = function(e) {
stop(paste0("Package ", package, " failed to load: ", conditionMessage(e)))
})
}

Expand Down
5 changes: 3 additions & 2 deletions tests/test-preinstalled-user-package-step-1.R
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
# .libPaths() should not have been amended yet
stopifnot(.libPaths()[1] != "/workspace/.local-packages/r/v2")
local_pkg_dir <- paste0("/workspace/.local-packages/r/", Sys.getenv("MAJOR_VERSION"))
stopifnot(.libPaths()[1] != local_pkg_dir)
# install package to user library in repo
install.packages("tmsens")
# .libPaths() should have been amended
stopifnot(.libPaths()[1] == "/workspace/.local-packages/r/v2")
stopifnot(.libPaths()[1] == local_pkg_dir)
print("Step 1: installing user package and amending .libPaths() paths test passed")
2 changes: 1 addition & 1 deletion tests/test-preinstalled-user-package-step-2.R
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# User library directory should already exist, hence .libPaths() is amended
stopifnot(.libPaths()[1] == "/workspace/.local-packages/r/v2")
stopifnot(.libPaths()[1] == paste0("/workspace/.local-packages/r/", Sys.getenv("MAJOR_VERSION")))
# Load the pre-installed package from previous R session which is now in repo
library(tmsens)
# Delete /workspace/.local-packages
Expand Down
2 changes: 1 addition & 1 deletion tests/test-user-install-package.R
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ library(tmsens)
print("Loading user installed package test passed.")

print("Library paths")
stopifnot(.libPaths()[1] == "/workspace/.local-packages/r/v2")
stopifnot(.libPaths()[1] == paste0("/workspace/.local-packages/r/", Sys.getenv("MAJOR_VERSION")))

# Test adding a second package works
# (again bpbounds chosen because it also has no hard dependencies)
Expand Down
6 changes: 3 additions & 3 deletions tests/test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ if [ "${MAJOR_VERSION}" = "v1" ]; then
# Test all R packages can be loaded
python3 -c "import json; print('\n'.join(json.load(open(\"./$MAJOR_VERSION/renv.lock\"))['Packages']))" | xargs -I {} echo "if (!library({}, warn.conflicts = FALSE, logical.return = TRUE)) {stop(\"Package {} failed to load, please investigate\")}" > .tests.R
run_test "${MAJOR_VERSION}" .tests.R
elif [ "${MAJOR_VERSION}" = "v2" ]; then
elif [[ "${MAJOR_VERSION}" =~ ^v[23]$ ]]; then
# Test all R packages can be attached then detached
run_test "${MAJOR_VERSION}" ./tests/test-loading-packages.R
fi
Expand All @@ -31,8 +31,8 @@ run_test "${MAJOR_VERSION}" ./tests/test-arrow.R
# Test loading the 14 base packages
run_test "${MAJOR_VERSION}" ./tests/test-loading-base.R

# Test user installed paackages on v2
if [ "${MAJOR_VERSION}" = "v2" ]; then
# Test user installed paackages on v2 and v3
if [[ "${MAJOR_VERSION}" =~ ^v[23]$ ]]; then
# Test installing and loading in same R session
run_test "${MAJOR_VERSION}" ./tests/test-user-install-package.R

Expand Down
7 changes: 6 additions & 1 deletion v2/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,9 @@ EOF
# Finally, build the actual image from the base-r image
FROM base-r AS r
ARG REPOS="default-arg-to-silence-docker"
ARG MAJOR_VERSION=v2
ENV MAJOR_VERSION=${MAJOR_VERSION}
RUN echo "MAJOR_VERSION=${MAJOR_VERSION}" >> /usr/lib/R/etc/Renviron.site

# Some static metadata for this specific image, as defined by:
# https://github.com/opencontainers/image-spec/blob/master/annotations.md#pre-defined-annotation-keys
Expand All @@ -62,7 +65,9 @@ WORKDIR /workspace
# Settings to make it easier for users to add packages
RUN --mount=type=bind,source=scripts/rprofile-site-append-2.R,target=/tmp/rprofile-site-append-2.R \
cat /tmp/rprofile-site-append-2.R >> /usr/lib/R/etc/Rprofile.site
COPY scripts/local-packages-README.md /usr/local-packages-README.md
RUN --mount=type=bind,source=scripts/local-packages-README.Rmd,target=/tmp/local-packages-README.Rmd <<EOF
Rscript -e "rmarkdown::render('/tmp/local-packages-README.Rmd', output_file = '/usr/local-packages-README.md')"
EOF

#################################################
#
Expand Down
2 changes: 1 addition & 1 deletion v2/env
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
MAJOR_VERSION=v2
BASE=22.04
RSTUDIO_BASE_URL=https://download2.rstudio.org/server/jammy/amd64/
RSTUDIO_DEB=rstudio-server-2026.01.0-392-amd64.deb
RSTUDIO_DEB=rstudio-server-2026.01.2-418-amd64.deb
CRAN_DATE=2025-03-05
REPOS=https://packagemanager.posit.co/cran/__linux__/jammy/${CRAN_DATE}
105 changes: 105 additions & 0 deletions v3/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
# syntax=docker/dockerfile:1.11
# enable docker linting
# check=error=true
#
#################################################
ARG BASE=22.04
FROM ghcr.io/opensafely-core/base-action:$BASE AS base-r
ARG MAJOR_VERSION=v3

# add cran repo for R packages and install
RUN --mount=type=cache,target=/var/cache/apt,id=apt-2204,sharing=locked \
--mount=type=cache,target=/var/lib/apt,id=apt-2204,sharing=locked \
--mount=type=bind,source=${MAJOR_VERSION}/dependencies.txt,target=/tmp/dependencies.txt <<EOF
echo "deb https://cloud.r-project.org/bin/linux/ubuntu jammy-cran40/" > /etc/apt/sources.list.d/cran.list
/usr/lib/apt/apt-helper download-file 'https://cloud.r-project.org/bin/linux/ubuntu/marutter_pubkey.asc' /etc/apt/trusted.gpg.d/cran_ubuntu_key.asc
/root/docker-apt-install.sh /tmp/dependencies.txt
ln -sf /usr/lib/x86_64-linux-gnu/openblas-pthread/libblas.so.3 /usr/lib/R/lib/libRblas.so
ln -sf /usr/lib/x86_64-linux-gnu/openblas-pthread/liblapack.so.3 /usr/lib/R/lib/libRlapack.so
EOF

# use pak to install packages
ENV PKG_CACHE_DIR=/cache/main
ENV PKG_PACKAGE_CACHE_DIR=/cache/package
ENV PKG_METADATA_CACHE_DIR=/cache/metadata
ARG REPOS="default-arg-to-silence-docker"
ARG CRAN_DATE="default-arg-to-silence-docker"
RUN --mount=type=cache,target=/cache,id=/cache-2204,sharing=locked \
--mount=type=bind,source=scripts/build-toml.R,target=/tmp/scripts/build-toml.R \
--mount=type=bind,source=${MAJOR_VERSION}/packages.toml,target=/tmp/packages.toml \
--mount=type=bind,source=scripts/rprofile-site-append-1.R,target=/tmp/rprofile-site-append-1.R <<EOF
echo "REPOS=${REPOS}" >> /usr/lib/R/etc/Renviron.site
cat /tmp/rprofile-site-append-1.R >> /usr/lib/R/etc/Rprofile.site
Rscript /tmp/scripts/build-toml.R
EOF


################################################
#
# Finally, build the actual image from the base-r image
FROM base-r AS r
ARG REPOS="default-arg-to-silence-docker"
ARG MAJOR_VERSION=v3
ENV MAJOR_VERSION=${MAJOR_VERSION}
RUN echo "MAJOR_VERSION=${MAJOR_VERSION}" >> /usr/lib/R/etc/Renviron.site

# Some static metadata for this specific image, as defined by:
# https://github.com/opencontainers/image-spec/blob/master/annotations.md#pre-defined-annotation-keys
# The org.opensafely.action label is used by the jobrunner to indicate this is
# an approved action image to run.
LABEL org.opencontainers.image.title="r" \
org.opencontainers.image.description="R action for opensafely.org" \
org.opencontainers.image.source="https://github.com/opensafely-core/r-docker" \
org.opensafely.action="r" \
org.opensafely.version="${MAJOR_VERSION}" \
org.opensafely.cran-date="${CRAN_DATE}"

# ACTION_EXEC is our default executable
ENV ACTION_EXEC="/usr/bin/Rscript"
# INTERACTIVE_EXEC is used when running w/o any args, implying an interactive session.
# See: https://github.com/opensafely-core/base-docker/blob/main/entrypoint.sh#L5
ENV INTERACTIVE_EXEC="/usr/bin/R"

# setup /workspace
RUN mkdir /workspace
WORKDIR /workspace

# Settings to make it easier for users to add packages
RUN --mount=type=bind,source=scripts/rprofile-site-append-2.R,target=/tmp/rprofile-site-append-2.R \
cat /tmp/rprofile-site-append-2.R >> /usr/lib/R/etc/Rprofile.site
RUN --mount=type=bind,source=scripts/local-packages-README.Rmd,target=/tmp/local-packages-README.Rmd <<EOF
Rscript -e "rmarkdown::render('/tmp/local-packages-README.Rmd', output_file = '/usr/local-packages-README.md')"
EOF

#################################################
#
# Add rstudio-server to r image - creating rstudio image
ARG RSTUDIO_BASE_URL="default-arg-to-silence-docker"
ARG RSTUDIO_DEB="default-arg-to-silence-docker"
FROM r AS rstudio

# Install rstudio-server (and a few dependencies)
RUN --mount=type=cache,target=/var/cache/apt,id=apt-2204,sharing=locked \
--mount=type=cache,target=/var/lib/apt,id=apt-2204,sharing=locked \
--mount=type=bind,source=rstudio/rstudio-dependencies.txt,target=/tmp/rstudio-dependencies.txt <<EOF
/root/docker-apt-install.sh /tmp/rstudio-dependencies.txt
test -f /var/cache/apt/"${RSTUDIO_DEB}" || /usr/lib/apt/apt-helper download-file "${RSTUDIO_BASE_URL}${RSTUDIO_DEB}" /var/cache/apt/"${RSTUDIO_DEB}"
apt-get install --no-install-recommends -y /var/cache/apt/"${RSTUDIO_DEB}"
EOF

# Configuration
## Start by setting up rstudio user using approach in opensafely-core/research-template-docker
RUN useradd rstudio -m
# copy R/rstudio config into user home dir
COPY rstudio/home/* /home/rstudio/
COPY rstudio/etc/* /etc/rstudio/

RUN <<EOF
chown -R rstudio:rstudio /home/rstudio
echo "R_LIBS_SITE=/usr/local/lib/R/site-library" >> /usr/lib/R/etc/Renviron.site
EOF

COPY rstudio/rstudio-entrypoint.sh /usr/local/bin/rstudio-entrypoint.sh

ENV USER=rstudio
ENTRYPOINT ["/usr/local/bin/rstudio-entrypoint.sh"]
Loading