Ansible role that installs R itself, R packages (from CRAN, archived and remote repos) and associated tools.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

105 lines
5.7 KiB

---
- name: Find all installed R package paths
ansible.builtin.find:
paths: "{{ RLIBS_SYSTEM }}"
recurse: yes
depth: 2
file_type: file
patterns: "DESCRIPTION"
register: R_library_package_paths
- debug:
msg: "Could not find any installed R packages in {{ RLIBS_SYSTEM }}! You should probably rerun this playbook with --extra-vars '{install_R: true}'"
when: R_library_package_paths.matched == 0
# https://www.middlewareinventory.com/blog/ansible-find-examples/
# https://stackoverflow.com/questions/29399581/using-set-facts-and-with-items-together-in-ansible
# note how default([]) handles that R_library_packages is initially undefined
# This task output is very verbose,
- name: Create a list of all R package names installed in the system library
set_fact:
R_library_packages: "{{ R_library_packages|default([]) + [ item.path | dirname | basename ] }}"
with_items: "{{ R_library_package_paths.files }}"
when: R_library_package_paths.matched != 0
# temporarily disable any ~/.Rprofile before updating R packages
# (because its output pollutes the playbook output/log)
- name: Temporarily disable ~/.Rprofile by renaming the file
ansible.builtin.command: "mv {{ ansible_env.HOME }}/.Rprofile {{ ansible_env.HOME }}/.Rprofile.moved"
ignore_errors: true
# > installed.packages()[, "Priority"] %>% unname() %>% unique()
# [1] NA "base" "recommended"
# > installed.packages(priority = "base")[,1] %>% unname()
# [1] "base" "compiler" "datasets" "graphics" "grDevices" "grid"
# [7] "methods" "parallel" "splines" "stats" "stats4" "tcltk"
# [13] "tools" "utils"
# The following elegant reticulate approach worked as intended in an interactive R session, but produced nonsense when run using Rscript. Not sure why.
# library(reticulate); use_python('/usr/bin/python3'); base.pkgs <- r_to_py(unname(installed.packages(priority = 'base')[,1])); import_builtins()$print(base.pkgs)
# Instead we construct a Pythonesque list of the base package names
- name: Create a list of R's base packages for exclusion from update
ansible.builtin.command: >
xvfb-run --auto-servernum Rscript --slave --no-save --no-restore-history -e
"cat(paste0(\"['\", paste0(unname(installed.packages(priority = 'base')[,1]), collapse = \"', '\"), \"']\"))"
register: R_base_packages
when: R_library_package_paths.matched != 0
# dev packages are specified in defaults/main.yml
- name: Create a list of development package names
set_fact:
R_dev_package_names: "{{ R_dev_package_names|default([]) + [ item.name ] }}"
with_items: "{{ R_dev_packages }}"
when: R_library_package_paths.matched != 0
# https://www.middlewareinventory.com/blog/ansible-facts-list-how-to-use-ansible-facts/#How_to_Parse_through_the_Dictionary_variable_in_a_loop
# https://stackoverflow.com/questions/21461649/how-to-update-a-package-in-r
# we use install.packages() instead of update.packages() to be able to run
# this command package-by-package and thus get some track of the task's progress
# QUESTION: is this equivalent to checkBuilt=TRUE when using update.packages?
# NOTE: this gives more output, but takes longer than update.packages()
# it's too bad we can't simply make update.packages more verbose
# IDEA: another approach might be easier: query old.packages()
# https://docs.ansible.com/ansible/latest/user_guide/playbooks_filters.html#set-theory-filters
- name: Update all packages in the R system library
ansible.builtin.command: >
xvfb-run --auto-servernum Rscript --slave --no-save --no-restore-history -e
"Sys.setenv(RPYTHON_PYTHON_VERSION=3);
install.packages(pkgs='{{ item }}', lib='{{ RLIBS_SYSTEM }}');
print('>>>> Upgraded package {{ ansible_loop.index }} out of {{ ansible_loop.length }}')"
when: R_library_package_paths.matched != 0
# difference() lets us exclude base packages
loop: "{{ R_library_packages | difference(R_base_packages.stdout) | difference(R_dev_package_names) }}"
loop_control:
extended: yes
# # When running update.packages() inside this playbook we should not need checkBuilt=TRUE since we would be on the same R version as when the packages were installed. If R itself has been upgraded we would also have reinstalled all packages.
# # This task gives no output during updating (due to the way it uses update.packages())
# # which gives us no way to gauge how far along it is
# # The problem I have with this task (and the reason I switched to install.packages) is
# # that update.packages() does not produce any output until *all* updates are completed.
# # IDEA: is it possible to *make* Ansible produce output during processing, somehow?
# - name: Update all R CRAN packages
# command: >
# xvfb-run --auto-servernum Rscript --slave --no-save --no-restore-history -e
# "Sys.setenv(RPYTHON_PYTHON_VERSION=3);
# update.packages(lib.loc='{{ RLIBS_SYSTEM }}', ask=FALSE, checkBuilt=FALSE)"
# # matched is the number of found R_library_package_paths (zero if RLIBS_SYSTEM was empty)
# when: R_library_package_paths.matched != 0
# Update R packages installed from Github
- name: "Update R dev packages using install_github()"
ansible.builtin.command: >
xvfb-run --auto-servernum Rscript --slave --no-save --no-restore-history -e
"withr::with_libpaths(new='{{ RLIBS_SYSTEM }}', remotes::install_github('{{ item.repo }}'));
print('Checked {{ item.name }} for updates');"
register: r_dev_package
failed_when: "r_dev_package.rc != 0 or 'had non-zero exit status' in r_dev_package.stderr"
changed_when: "'Checked' in r_dev_package.stdout"
with_items: "{{ R_dev_packages }}"
# move ~/.Rprofile back
- name: Enable our ~/.Rprofile again
ansible.builtin.command: "mv {{ ansible_env.HOME }}/.Rprofile.moved {{ ansible_env.HOME }}/.Rprofile"
ignore_errors: true