diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index c4ed61c..fbd565b 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -29,6 +29,7 @@ jobs: pandoc: ./**/pandoc/** quarto-cli: ./**/quarto-cli/** r-apt: ./**/r-apt/** + r-dependent-packages: ./**/r-dependent-packages/** r-history: ./**/r-history/** r-packages: ./**/r-packages/** r-rig: ./**/r-rig/** @@ -90,6 +91,7 @@ jobs: - pandoc - quarto-cli - r-apt + - r-dependent-packages - r-history - r-packages - r-rig @@ -121,6 +123,7 @@ jobs: - pandoc - quarto-cli - r-apt + - r-dependent-packages - r-history - r-packages - r-rig diff --git a/src/r-dependent-packages/NOTES.md b/src/r-dependent-packages/NOTES.md new file mode 100644 index 0000000..579b479 --- /dev/null +++ b/src/r-dependent-packages/NOTES.md @@ -0,0 +1,83 @@ + + +## System Requirements + +Please use this with an R-installed image (e.g. [`ghcr.io/rocker-org/devcontainer/r-ver`](https://rocker-project.org/images/devcontainer/images.html)) +or this should be installed after installing Features that installs R +(e.g. [`ghcr.io/rocker-org/devcontainer-features/r-apt`](https://github.com/rocker-org/devcontainer-features/tree/main/src/r-apt), +[`ghcr.io/rocker-org/devcontainer-features/r-rig`](https://github.com/rocker-org/devcontainer-features/tree/main/src/r-rig)). + +```json +"features": { + "ghcr.io/rocker-org/devcontainer-features/r-rig:1": {}, + "ghcr.io/rocker-org/devcontainer-features/r-dependent-packages:latest": {} +} +``` + +## DESCRIPTION file format + +This feature installs packages listed in the manifest file named `DESCRIPTION`. +If you are new to the `DESCRIPTION` file, please refer to the +[`usethis::use_description()`](https://usethis.r-lib.org/reference/use_description.html) function's reference. + +Here is an minimal example of the `DESCRIPTION` file. +At least, the `Package` and `Version` fields are required. +The `Imports` field is a list of packages that will be installed. + +```dcf +Package: foo +Version: 0.0.0.9000 +Imports: + cli, + rlang +``` + +### Additional fields + +When developing an R package, we may want to specify dependencies that are only needed during development +in the `DESCRIPTION` file. In such cases, we can use any field name starting with `Config`, +and generally specify multiple fields prefixed with `Config/Needs` as follows: + +```dcf +Package: foo +Version: 0.0.0.9000 +Suggests: + cli +Config/Needs/website: + curl +Config/Needs/dev: + crayon +``` + +If we want use such fields to install dependencies, we can specify the `dependencyTypes` field of +this Feature like this: + +```json +"ghcr.io/rocker-org/devcontainer-features/r-dependent-packages:latest": { + "dependencyTypes": "all,Config/Needs/website,Config/Needs/dev" +} +``` + +## Environment variables + +Enviroment variables listed in [the `containerEnv` field](https://containers.dev/implementors/json_reference/#general-properties) +are used in the package installation process. +See [the reference of the `pak` package](https://pak.r-lib.org/reference/pak-config.html) for options for `pak`. + +```json +"containerEnv": { + "NOT_CRAN": "true", + "PKG_CRAN_MIRROR": "https://cloud.r-project.org/" +} +``` + +## Cache directory and cache volume + +The package cache directory in the container is set to `/pak/cache`. + +This directory is stored in a volume named `devcontainer-pak-cache` +and is shared among multiple containers. + +## References + +- [pak](https://pak.r-lib.org/) diff --git a/src/r-dependent-packages/devcontainer-feature.json b/src/r-dependent-packages/devcontainer-feature.json new file mode 100644 index 0000000..9883f55 --- /dev/null +++ b/src/r-dependent-packages/devcontainer-feature.json @@ -0,0 +1,81 @@ +{ + "name": "R packages from the DESCRIPTION file (via pak)", + "id": "r-dependent-packages", + "version": "0.1.0", + "description": "This Feature sets scripts to install dependent R packages from the DESCRIPTION file in the repository.", + "documentationURL": "https://github.com/rocker-org/devcontainer-features/tree/main/src/r-dependent-packages", + "options": { + "when": { + "type": "string", + "default": "postCreate", + "enum": [ + "onCreate", + "updateContent", + "postCreate" + ], + "description": "When to install the dependent R packages? Each option corresponds to the lifecycle scripts." + }, + "pakVersion": { + "type": "string", + "enum": [ + "auto", + "devel", + "stable" + ], + "default": "auto", + "description": "Version of pak to install. By default, the stable version is installed if needed." + }, + "manifestRoot": { + "type": "string", + "default": ".", + "description": "The root path of the DESCRIPTION file recording the dependent R packages. Passed to the `root` argument of the `pak::local_install_deps()` function.", + "proposals": [ + "." + ] + }, + "additionalRepositories": { + "type": "string", + "default": "", + "description": "String passed to the `pak::repo_add()` function.", + "proposals": [ + "", + "rhub = 'https://r-hub.r-universe.dev', jeroen = 'https://jeroen.r-universe.dev'" + ] + }, + "dependencyTypes": { + "type": "string", + "default": "all", + "description": "Comma separated list of dependency types to install. Passed to the `dependencies` argument of the `pak::local_install_deps()` function.", + "proposals": [ + "all", + "hard", + "all,Config/Needs/website" + ] + } + }, + "containerEnv": { + "PKG_PACKAGE_CACHE_DIR": "/pak/cache" + }, + "mounts": [ + { + "source": "devcontainer-pak-cache", + "target": "/pak/cache", + "type": "volume" + } + ], + "onCreateCommand": { + "r-dependent-packages": "/usr/local/share/rocker-devcontainer-features/r-dependent-packages/scripts/oncreate.sh" + }, + "updateContentCommand": { + "r-dependent-packages": "/usr/local/share/rocker-devcontainer-features/r-dependent-packages/scripts/updatecontent.sh" + }, + "postCreateCommand": { + "r-dependent-packages": "/usr/local/share/rocker-devcontainer-features/r-dependent-packages/scripts/postcreate.sh" + }, + "installsAfter": [ + "ghcr.io/devcontainers/features/common-utils", + "ghcr.io/rocker-org/devcontainer-features/r-apt", + "ghcr.io/rocker-org/devcontainer-features/r-packages", + "ghcr.io/rocker-org/devcontainer-features/r-rig" + ] +} diff --git a/src/r-dependent-packages/empty_script.sh b/src/r-dependent-packages/empty_script.sh new file mode 100755 index 0000000..c805354 --- /dev/null +++ b/src/r-dependent-packages/empty_script.sh @@ -0,0 +1,3 @@ +#!/usr/bin/env bash + +echo "This script is empty. Done nothing..." diff --git a/src/r-dependent-packages/install.sh b/src/r-dependent-packages/install.sh new file mode 100644 index 0000000..1fa0384 --- /dev/null +++ b/src/r-dependent-packages/install.sh @@ -0,0 +1,87 @@ +#!/usr/bin/env bash + +WHEN=${WHEN:-"postCreate"} +PAK_VERSION=${PAKVERSION:-"auto"} +ROOT=${MANIFESTROOT:-"."} +REPOS=${ADDITIONALREPOSITORIES:-""} +DEPS=${DEPENDENCYTYPES:-"all"} + +PKG_PACKAGE_CACHE_DIR=${PKG_PACKAGE_CACHE_DIR:-"/pak/cache"} + +USERNAME=${USERNAME:-${_REMOTE_USER}} + +LIFECYCLE_SCRIPTS_DIR="/usr/local/share/rocker-devcontainer-features/r-dependent-packages/scripts" + +set -e + +if [ "$(id -u)" -ne 0 ]; then + echo -e 'Script must be run as root. Use sudo, su, or add "USER root" to your Dockerfile before running this script.' + exit 1 +fi + +create_cache_dir() { + if [ -d "$1" ]; then + echo "Cache directory $1 already exists. Skip creation..." + else + echo "Create cache directory $1..." + mkdir -p "$1" + fi + + if [ -z "$2" ]; then + echo "No username provided. Skip chown..." + else + echo "Change owner of $1 to $2..." + chown -R "$2:$2" "$1" + fi +} + +check_r() { + if [ ! -x "$(command -v R)" ]; then + echo "(!) Cannot run R. Please install R before installing this Feature." + echo " Skip installation..." + exit 0 + fi +} + +install_pak() { + local version=$1 + + if [ "${version}" = "auto" ]; then + if su "${USERNAME}" -c "R -s -e 'packageVersion(\"pak\")'" >/dev/null 2>&1; then + echo "pak is already installed. Skip pak installation..." + return + else + version="stable" + fi + fi + + echo "Installing pak ${version}..." + # shellcheck disable=SC2016 + su "${USERNAME}" -c 'R -q -e "install.packages(\"pak\", repos = sprintf(\"https://r-lib.github.io/p/pak/'"${version}"'/%s/%s/%s\", .Platform\$pkgType, R.Version()\$os, R.Version()\$arch))"' +} + +export DEBIAN_FRONTEND=noninteractive + +create_cache_dir "${PKG_PACKAGE_CACHE_DIR}" "${USERNAME}" + +# Set Lifecycle scripts +mkdir -p "${LIFECYCLE_SCRIPTS_DIR}" + +POSSIBLE_LIFECYCLE=("onCreate" "updateContent" "postCreate") +for lifecycle in "${POSSIBLE_LIFECYCLE[@]}"; do + cp empty_script.sh "${LIFECYCLE_SCRIPTS_DIR}/${lifecycle,,}.sh" +done + +# Enxure pak installed +check_r +install_pak "${PAK_VERSION}" + +# Replace the target lifecycle script +echo "Set the lifecycle script for '${WHEN}'..." +sed \ + -e "s|@ROOT@|${ROOT}|" \ + -e "s|@REPOS@|${REPOS//"'"/'"'}|" \ + -e "s|@DEPS@|${DEPS}|" \ + lifecycle_script.sh >"${LIFECYCLE_SCRIPTS_DIR}/${WHEN,,}.sh" + +echo "Done!" diff --git a/src/r-dependent-packages/lifecycle_script.sh b/src/r-dependent-packages/lifecycle_script.sh new file mode 100755 index 0000000..e8f7315 --- /dev/null +++ b/src/r-dependent-packages/lifecycle_script.sh @@ -0,0 +1,27 @@ +#!/usr/bin/env bash + +PKG_PACKAGE_CACHE_DIR=${PKG_PACKAGE_CACHE_DIR:-"/pak/cache"} + +set -e + +fix_permissions() { + local dir + dir="${1}" + + if [ ! -w "${dir}" ]; then + echo "Fixing permissions of '${dir}'..." + sudo chown -R "$(id -u):$(id -g)" "${dir}" + echo "Done!" + else + echo "Permissions of '${dir}' are OK!" + fi +} + +fix_permissions "${PKG_PACKAGE_CACHE_DIR}" + +echo "Install dependent R packages..." + +R -q -e \ + 'pak::repo_add(@REPOS@); pak::local_install_deps("@ROOT@", dependencies = trimws(unlist(strsplit("@DEPS@", ","))))' + +echo "Done!" diff --git a/test/r-dependent-packages/r-ver-default.sh b/test/r-dependent-packages/r-ver-default.sh new file mode 100755 index 0000000..a685a48 --- /dev/null +++ b/test/r-dependent-packages/r-ver-default.sh @@ -0,0 +1,13 @@ +#!/usr/bin/env bash + +set -e + +# Optional: Import test library bundled with the devcontainer CLI +source dev-container-features-test-lib + +# Feature-specific tests +check "R cli package" bash -c "R -q -e 'names(installed.packages()[, 3])' | grep cli" +check "R rlang package" bash -c "R -q -e 'names(installed.packages()[, 3])' | grep rlang" + +# Report result +reportResults diff --git a/test/r-dependent-packages/r-ver-default/DESCRIPTION b/test/r-dependent-packages/r-ver-default/DESCRIPTION new file mode 100644 index 0000000..ec3037f --- /dev/null +++ b/test/r-dependent-packages/r-ver-default/DESCRIPTION @@ -0,0 +1,5 @@ +Package: foo +Version: 0.0.0.9000 +Imports: + cli, + rlang diff --git a/test/r-dependent-packages/r-ver-postcreate.sh b/test/r-dependent-packages/r-ver-postcreate.sh new file mode 100755 index 0000000..a685a48 --- /dev/null +++ b/test/r-dependent-packages/r-ver-postcreate.sh @@ -0,0 +1,13 @@ +#!/usr/bin/env bash + +set -e + +# Optional: Import test library bundled with the devcontainer CLI +source dev-container-features-test-lib + +# Feature-specific tests +check "R cli package" bash -c "R -q -e 'names(installed.packages()[, 3])' | grep cli" +check "R rlang package" bash -c "R -q -e 'names(installed.packages()[, 3])' | grep rlang" + +# Report result +reportResults diff --git a/test/r-dependent-packages/r-ver-postcreate/DESCRIPTION b/test/r-dependent-packages/r-ver-postcreate/DESCRIPTION new file mode 100644 index 0000000..ec3037f --- /dev/null +++ b/test/r-dependent-packages/r-ver-postcreate/DESCRIPTION @@ -0,0 +1,5 @@ +Package: foo +Version: 0.0.0.9000 +Imports: + cli, + rlang diff --git a/test/r-dependent-packages/r-ver-updatecontent.sh b/test/r-dependent-packages/r-ver-updatecontent.sh new file mode 100755 index 0000000..a685a48 --- /dev/null +++ b/test/r-dependent-packages/r-ver-updatecontent.sh @@ -0,0 +1,13 @@ +#!/usr/bin/env bash + +set -e + +# Optional: Import test library bundled with the devcontainer CLI +source dev-container-features-test-lib + +# Feature-specific tests +check "R cli package" bash -c "R -q -e 'names(installed.packages()[, 3])' | grep cli" +check "R rlang package" bash -c "R -q -e 'names(installed.packages()[, 3])' | grep rlang" + +# Report result +reportResults diff --git a/test/r-dependent-packages/r-ver-updatecontent/DESCRIPTION b/test/r-dependent-packages/r-ver-updatecontent/DESCRIPTION new file mode 100644 index 0000000..ec3037f --- /dev/null +++ b/test/r-dependent-packages/r-ver-updatecontent/DESCRIPTION @@ -0,0 +1,5 @@ +Package: foo +Version: 0.0.0.9000 +Imports: + cli, + rlang diff --git a/test/r-dependent-packages/realworld.sh b/test/r-dependent-packages/realworld.sh new file mode 100755 index 0000000..3053826 --- /dev/null +++ b/test/r-dependent-packages/realworld.sh @@ -0,0 +1,14 @@ +#!/usr/bin/env bash + +set -e + +# Optional: Import test library bundled with the devcontainer CLI +source dev-container-features-test-lib + +# Feature-specific tests +check "R cli package" bash -c "R -q -e 'names(installed.packages()[, 3])' | grep cli" +check "R rlang package" bash -c "R -q -e 'names(installed.packages()[, 3])' | grep curl" +check "R rlang package" bash -c "R -q -e 'names(installed.packages()[, 3])' | grep crayon" + +# Report result +reportResults diff --git a/test/r-dependent-packages/realworld/DESCRIPTION b/test/r-dependent-packages/realworld/DESCRIPTION new file mode 100644 index 0000000..8f5b1c4 --- /dev/null +++ b/test/r-dependent-packages/realworld/DESCRIPTION @@ -0,0 +1,8 @@ +Package: foo +Version: 0.0.0.9000 +Suggests: + cli +Config/Needs/website: + curl +Config/Needs/dev: + github::r-lib/crayon diff --git a/test/r-dependent-packages/scenarios.json b/test/r-dependent-packages/scenarios.json new file mode 100644 index 0000000..2ccca09 --- /dev/null +++ b/test/r-dependent-packages/scenarios.json @@ -0,0 +1,42 @@ +{ + "r-ver-default": { + "image": "rocker/r-ver:4", + "features": { + "r-dependent-packages": { + "manifestRoot": ".devcontainer" + } + } + }, + "r-ver-postcreate": { + "image": "rocker/r-ver:4", + "features": { + "r-dependent-packages": { + "manifestRoot": ".devcontainer", + "when": "postCreate" + } + } + }, + "r-ver-updatecontent": { + "image": "rocker/r-ver:4", + "features": { + "r-dependent-packages": { + "manifestRoot": ".devcontainer", + "when": "updateContent" + } + } + }, + "realworld": { + "image": "rocker/r-ver:4", + "features": { + "r-dependent-packages": { + "manifestRoot": ".devcontainer", + "dependencyTypes": "all, Config/Needs/website, Config/Needs/dev", + "additionalRepositories": "rlib = 'https://r-lib.r-universe.dev'" + } + }, + "containerEnv": { + "NOT_CRAN": "true", + "PKG_CRAN_MIRROR": "https://cloud.r-project.org/" + } + } +} diff --git a/test/r-dependent-packages/test.sh b/test/r-dependent-packages/test.sh new file mode 100755 index 0000000..6676bda --- /dev/null +++ b/test/r-dependent-packages/test.sh @@ -0,0 +1,11 @@ +#!/usr/bin/env bash + +set -e + +# Optional: Import test library bundled with the devcontainer CLI +source dev-container-features-test-lib + +# Feature-specific tests + +# Report result +reportResults