From 7192637c957e7bdaa56b1b1ebae9a177b5d1da42 Mon Sep 17 00:00:00 2001 From: Dane Kennedy Date: Tue, 21 Jul 2026 16:24:28 +0200 Subject: [PATCH 01/11] Add RStudio 2026.07.0-139 / R 4.6.1 on Ubuntu noble Bumps the RStudio line from 2025.05.1-513 / R 4.5.1 and moves it off jammy (standard support ends April 2027) onto ubuntu-noble-compile, which is already built in this repo and used by python-ml-base. Two template changes were needed for a noble build: - The libgsl conditional was `!= "jammy" -> libgsl23`, so noble would have taken the libgsl23 branch; that package does not exist on 24.04, which ships libgsl27. Inverted to a whitelist, leaving bionic/focal unchanged. - Posit publishes a single .deb for Ubuntu 22 and 24 under /server/jammy/ (/server/noble/ 404s), so the download path can no longer be derived from the base release. Added an optional rstudio_deb_release field defaulting to base_container_version_name, so all existing rows render byte-identically. Also fixes three pre-existing issues in the RStudio files: - The task name interpolated {{ item.version_name }}, which no rstudio_versions entry defines, so it rendered with an empty version. - RStudio.lua was the only container module in the repo missing load("singularity"); the wrappers call singularity bare, so it was resolving from the ambient system PATH rather than the module. - RStudio.sh tested a directory with -f and had mangled indentation. Verified: yamllint clean, ansible-lint production profile passes, and RStudio.def renders byte-identically to HEAD for bionic/focal/jammy rows while noble gets libgsl27 and the jammy .deb path. No container build was run -- that needs sudo singularity build on the cluster. Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_01RACvroiDb6D5nuP8uCiv1X --- ansible/roles/containers/tasks/common/RStudio.yaml | 2 +- ansible/roles/containers/tasks/common/main.yml | 6 ++++++ .../roles/containers/templates/common/RStudio.def | 4 ++-- .../roles/containers/templates/common/RStudio.lua | 2 ++ ansible/roles/containers/templates/common/RStudio.sh | 12 ++++++------ 5 files changed, 17 insertions(+), 9 deletions(-) diff --git a/ansible/roles/containers/tasks/common/RStudio.yaml b/ansible/roles/containers/tasks/common/RStudio.yaml index b0de6be5..8a99fc6a 100644 --- a/ansible/roles/containers/tasks/common/RStudio.yaml +++ b/ansible/roles/containers/tasks/common/RStudio.yaml @@ -1,5 +1,5 @@ --- -- name: Create RStudio container {{ item.version_name }} +- name: Create RStudio container RStudio{{ item.rstudio_version }}-R{{ item.r_version }} vars: base_container_image: "{{ common_container_dir }}/{{ item.base_container_name }}.sif" container_name: "RStudio{{ item.rstudio_version }}-R{{ item.r_version }}" diff --git a/ansible/roles/containers/tasks/common/main.yml b/ansible/roles/containers/tasks/common/main.yml index 0b639183..82606951 100644 --- a/ansible/roles/containers/tasks/common/main.yml +++ b/ansible/roles/containers/tasks/common/main.yml @@ -28,6 +28,12 @@ - { base_container_name: ubuntu-focal-compile, base_container_version_name: focal, r_version: 4.3.3, rstudio_version: 2023.06.1-524 } - { base_container_name: ubuntu-jammy-compile, base_container_version_name: jammy, r_version: 4.4.1, rstudio_version: 2023.06.1-524 } - { base_container_name: ubuntu-jammy-compile, base_container_version_name: jammy, r_version: 4.5.1, rstudio_version: 2025.05.1-513 } + # Posit publishes a single .deb for Ubuntu 22 and 24, under the jammy path. + - base_container_name: ubuntu-noble-compile + base_container_version_name: noble + rstudio_deb_release: jammy + r_version: 4.6.1 + rstudio_version: 2026.07.0-139 ansible.builtin.include_tasks: file: RStudio.yaml loop: "{{ rstudio_versions }}" diff --git a/ansible/roles/containers/templates/common/RStudio.def b/ansible/roles/containers/templates/common/RStudio.def index 7822154e..0c0cd92a 100644 --- a/ansible/roles/containers/templates/common/RStudio.def +++ b/ansible/roles/containers/templates/common/RStudio.def @@ -108,7 +108,7 @@ From: {{ base_container_image }} libfribidi-bin \ libfribidi-dev \ libgit2-dev \ - {% if item.base_container_version_name != "jammy" %}libgsl23{% else %}libgsl27{% endif %} \ + {% if item.base_container_version_name in ["jammy", "noble"] %}libgsl27{% else %}libgsl23{% endif %} \ libgsl-dev \ libharfbuzz-dev \ libhdf5-dev \ @@ -140,7 +140,7 @@ From: {{ base_container_image }} echo "R_LIBS_SITE=/library:\${R_LIBS_SITE}" >> /usr/lib/R/etc/Renviron.site cd /tmp - wget https://download2.rstudio.org/server/{{ item.base_container_version_name }}/amd64/rstudio-server-${RSTUDIO_VERSION}-amd64.deb + wget https://download2.rstudio.org/server/{{ item.rstudio_deb_release | default(item.base_container_version_name) }}/amd64/rstudio-server-${RSTUDIO_VERSION}-amd64.deb gdebi -n rstudio-server-${RSTUDIO_VERSION}-amd64.deb rm rstudio-server-${RSTUDIO_VERSION}-amd64.deb diff --git a/ansible/roles/containers/templates/common/RStudio.lua b/ansible/roles/containers/templates/common/RStudio.lua index 4ba3286d..83cd7eab 100644 --- a/ansible/roles/containers/templates/common/RStudio.lua +++ b/ansible/roles/containers/templates/common/RStudio.lua @@ -14,6 +14,8 @@ whatis("Description: RStudio container") local singularity_image = "{{ container_image }}" +load("singularity") + setenv ("R_INSTALL_STAGED" ,"false") prepend_path("PATH", "{{ R_binary_dir }}") diff --git a/ansible/roles/containers/templates/common/RStudio.sh b/ansible/roles/containers/templates/common/RStudio.sh index 49fbd55a..f2e43a2f 100644 --- a/ansible/roles/containers/templates/common/RStudio.sh +++ b/ansible/roles/containers/templates/common/RStudio.sh @@ -2,11 +2,11 @@ SERVER_DIR="${HOME}/.RStudio.rserver" -if [ ! -d ${SERVER_DIR}/data ]; then - mkdir -p "${SERVER_DIR}/data" || { echo "Could not create ${SERVER_DIR}, aborting..."; exit 1; } - fi - if [ ! -f "${SERVER_DIR}/var/lib" ]; then - mkdir -p "${SERVER_DIR}/var/lib" || { echo "Could not create ${SERVER_DIR}, aborting..."; exit 1; } - fi +if [ ! -d "${SERVER_DIR}/data" ]; then + mkdir -p "${SERVER_DIR}/data" || { echo "Could not create ${SERVER_DIR}/data, aborting..."; exit 1; } +fi +if [ ! -d "${SERVER_DIR}/var/lib" ]; then + mkdir -p "${SERVER_DIR}/var/lib" || { echo "Could not create ${SERVER_DIR}/var/lib, aborting..."; exit 1; } +fi singularity run --app rstudio --bind /software:/software --bind "${SERVER_DIR}/var/lib":/var/lib/rstudio-server {{ container_image }} "$@" From 1213ab4eab237e28b4589f257cc17fd867c371f2 Mon Sep 17 00:00:00 2001 From: Dane Kennedy Date: Tue, 21 Jul 2026 16:38:32 +0200 Subject: [PATCH 02/11] Build containers with singularity 4.4.1, rootless via --fakeroot group_vars/all still pinned the build binary at singularity 4.1.0, which ships a setuid starter-suid (-rwsr-xr-x). The --without-suid migration landed in singularity.yaml and in the version list, but never reached this var, so every container build was still going through the suid-era binary. Repoint at 4.4.1, which is built --without-suid: its libexec/singularity/bin contains only starter, squashfuse_ll and singularity-buildkitd, with no starter-suid at all. Not 4.4.2 -- it is registered in roles/compiled/tasks/common/main.yml but is not installed on /software (only 3.11.3, 4.1.0, 4.1.3 and 4.4.1 are), so pointing at it would break every build until it is deployed. Since the binary no longer needs suid, drop `become: true` from all 21 container build tasks and pass --fakeroot instead. --fakeroot is required, not optional: singularity refuses a def-file build as a non-root user without it ("--remote, --fakeroot, or the proot command are required to build this source as a non-root user"), and that check fires before any namespace work, so it is independent of host configuration. Caveat: --fakeroot needs unprivileged user namespaces on the build host. This could not be verified end to end -- a test build on an Ubuntu 24.04 workstation failed at "Failed to create mount namespace" because kernel.apparmor_restrict_unprivileged_userns=1 there. Production is expected to permit it; if not, this needs revisiting. Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_01RACvroiDb6D5nuP8uCiv1X --- ansible/group_vars/all | 5 ++++- ansible/roles/containers/tasks/astro/atfncat.yaml | 3 +-- ansible/roles/containers/tasks/bio/ClinSV.yaml | 3 +-- ansible/roles/containers/tasks/bio/DIANN.yaml | 6 ++---- ansible/roles/containers/tasks/bio/PopGen_suite.yaml | 3 +-- ansible/roles/containers/tasks/bio/bcbio.yaml | 3 +-- ansible/roles/containers/tasks/bio/beast.yaml | 3 +-- ansible/roles/containers/tasks/bio/gatk.yaml | 3 +-- ansible/roles/containers/tasks/bio/mspipe.yaml | 3 +-- ansible/roles/containers/tasks/bio/pggb.yaml | 3 +-- ansible/roles/containers/tasks/bio/proteowizard.yaml | 3 +-- ansible/roles/containers/tasks/bio/snvstory.yaml | 3 +-- ansible/roles/containers/tasks/bio/spectronaut.yaml | 3 +-- ansible/roles/containers/tasks/bio/trycycler.yaml | 3 +-- ansible/roles/containers/tasks/bio/vep.yaml | 3 +-- ansible/roles/containers/tasks/common/RStudio.yaml | 3 +-- ansible/roles/containers/tasks/common/gnuplot.yaml | 3 +-- ansible/roles/containers/tasks/common/python-ml-base.yaml | 3 +-- ansible/roles/containers/tasks/common/python-ml.yaml | 3 +-- ansible/roles/containers/tasks/common/ubuntu.yaml | 6 ++---- 20 files changed, 25 insertions(+), 43 deletions(-) diff --git a/ansible/group_vars/all b/ansible/group_vars/all index fee5db53..09181da6 100644 --- a/ansible/group_vars/all +++ b/ansible/group_vars/all @@ -52,4 +52,7 @@ common_modules: "{{ modules_dir }}/common" admin_group: "idia-admins" -singularity: "/software/common/singularity/4.1.0/bin/singularity" +# 4.4.1 is built --without-suid (see roles/compiled/tasks/common/singularity.yaml); +# 4.1.0 still ships a setuid starter-suid. Note 4.4.2 is registered in +# roles/compiled/tasks/common/main.yml but is not yet installed on /software. +singularity: "/software/common/singularity/4.4.1/bin/singularity" diff --git a/ansible/roles/containers/tasks/astro/atfncat.yaml b/ansible/roles/containers/tasks/astro/atfncat.yaml index 514bd31d..8ee09893 100644 --- a/ansible/roles/containers/tasks/astro/atfncat.yaml +++ b/ansible/roles/containers/tasks/astro/atfncat.yaml @@ -31,10 +31,9 @@ dest: "{{ container_recipe }}" mode: u=rw,g=r,o=r - name: Build singularity container - ansible.builtin.command: "{{ singularity }} build {{ container_image }} {{ container_recipe }}" + ansible.builtin.command: "{{ singularity }} build --fakeroot {{ container_image }} {{ container_recipe }}" args: creates: "{{ container_image }}" - become: true environment: SINGULARITY_TMPDIR: "{{ lookup('env', 'SINGULARITY_TMPDIR') | default('/dev/shm') }}" - name: Ensure atfncat binary directory exists diff --git a/ansible/roles/containers/tasks/bio/ClinSV.yaml b/ansible/roles/containers/tasks/bio/ClinSV.yaml index 3a67023d..45db710c 100644 --- a/ansible/roles/containers/tasks/bio/ClinSV.yaml +++ b/ansible/roles/containers/tasks/bio/ClinSV.yaml @@ -21,10 +21,9 @@ dest: "{{ container_recipe }}" mode: u=rw,g=r,o=r - name: Build container {{ container_image }} - ansible.builtin.command: "{{ singularity }} build {{ container_image }} {{ container_recipe }}" + ansible.builtin.command: "{{ singularity }} build --fakeroot {{ container_image }} {{ container_recipe }}" args: creates: "{{ container_image }}" - become: true - name: Ensure data dir exists ansible.builtin.file: path: "{{ ClinSV_data_dir }}" diff --git a/ansible/roles/containers/tasks/bio/DIANN.yaml b/ansible/roles/containers/tasks/bio/DIANN.yaml index 78613301..2431cd15 100644 --- a/ansible/roles/containers/tasks/bio/DIANN.yaml +++ b/ansible/roles/containers/tasks/bio/DIANN.yaml @@ -62,10 +62,9 @@ dest: "{{ container_recipe }}" mode: u=rw,g=r,o=r - name: Build container {{ container_image }} - ansible.builtin.command: "{{ singularity }} build {{ container_image }} {{ container_recipe }}" + ansible.builtin.command: "{{ singularity }} build --fakeroot {{ container_image }} {{ container_recipe }}" args: creates: "{{ container_image }}" - become: true - name: Build DIANN zip container when: installer_type == 'zip' @@ -76,10 +75,9 @@ dest: "{{ container_recipe }}" mode: u=rw,g=r,o=r - name: Build container {{ container_image }} - ansible.builtin.command: "{{ singularity }} build {{ container_image }} {{ container_recipe }}" + ansible.builtin.command: "{{ singularity }} build --fakeroot {{ container_image }} {{ container_recipe }}" args: creates: "{{ container_image }}" - become: true - name: Ensure DIANN binary directory exists ansible.builtin.file: diff --git a/ansible/roles/containers/tasks/bio/PopGen_suite.yaml b/ansible/roles/containers/tasks/bio/PopGen_suite.yaml index aa385daa..6f573047 100644 --- a/ansible/roles/containers/tasks/bio/PopGen_suite.yaml +++ b/ansible/roles/containers/tasks/bio/PopGen_suite.yaml @@ -92,10 +92,9 @@ mode: u=rw,g=r,o=r - name: Build container {{ container_image }} - ansible.builtin.command: "{{ singularity }} build {{ container_image }} {{ container_recipe }}" + ansible.builtin.command: "{{ singularity }} build --fakeroot {{ container_image }} {{ container_recipe }}" args: creates: "{{ container_image }}" - become: true - name: Install PopGen module {{ container_name }} block: diff --git a/ansible/roles/containers/tasks/bio/bcbio.yaml b/ansible/roles/containers/tasks/bio/bcbio.yaml index 3a26d047..0f268b3b 100644 --- a/ansible/roles/containers/tasks/bio/bcbio.yaml +++ b/ansible/roles/containers/tasks/bio/bcbio.yaml @@ -27,10 +27,9 @@ dest: "{{ container_recipe }}" mode: u=rw,g=r,o=r - name: Build container {{ container_image }} - ansible.builtin.command: "{{ singularity }} build {{ container_image }} {{ container_recipe }}" + ansible.builtin.command: "{{ singularity }} build --fakeroot {{ container_image }} {{ container_recipe }}" args: creates: "{{ container_image }}" - become: true environment: SINGULARITY_TMPDIR: "/dev/shm" diff --git a/ansible/roles/containers/tasks/bio/beast.yaml b/ansible/roles/containers/tasks/bio/beast.yaml index d789b83c..34fced3e 100644 --- a/ansible/roles/containers/tasks/bio/beast.yaml +++ b/ansible/roles/containers/tasks/bio/beast.yaml @@ -47,10 +47,9 @@ dest: "{{ container_recipe }}" mode: u=rw,g=r,o=r - name: Build container {{ container_image }} - ansible.builtin.command: "{{ singularity }} build {{ container_image }} {{ container_recipe }}" + ansible.builtin.command: "{{ singularity }} build --fakeroot {{ container_image }} {{ container_recipe }}" args: creates: "{{ container_image }}" - become: true # environment: # SINGULARITY_TMPDIR: "/dev/shm" diff --git a/ansible/roles/containers/tasks/bio/gatk.yaml b/ansible/roles/containers/tasks/bio/gatk.yaml index fef6ee30..fc0ecfdb 100644 --- a/ansible/roles/containers/tasks/bio/gatk.yaml +++ b/ansible/roles/containers/tasks/bio/gatk.yaml @@ -20,10 +20,9 @@ dest: "{{ container_recipe }}" mode: u=rw,g=r,o=r - name: Build container {{ container_image }} - ansible.builtin.command: "{{ singularity }} build {{ container_image }} {{ container_recipe }}" + ansible.builtin.command: "{{ singularity }} build --fakeroot {{ container_image }} {{ container_recipe }}" args: creates: "{{ container_image }}" - become: true - name: Ensure gatk binary directory exists ansible.builtin.file: path: "{{ gatk_binary_dir }}" diff --git a/ansible/roles/containers/tasks/bio/mspipe.yaml b/ansible/roles/containers/tasks/bio/mspipe.yaml index 033cbebf..8b3223d8 100644 --- a/ansible/roles/containers/tasks/bio/mspipe.yaml +++ b/ansible/roles/containers/tasks/bio/mspipe.yaml @@ -20,10 +20,9 @@ dest: "{{ bio_container_recipe_dir }}/{{ container_name }}.{{ version_number }}.def" mode: u=rw,g=r,o=r - name: "Build singularity container {{ container_image }}" - ansible.builtin.command: "{{ singularity }} build {{ container_image }} {{ bio_container_recipe_dir }}/{{ container_name }}.{{ version_number }}.def" + ansible.builtin.command: "{{ singularity }} build --fakeroot {{ container_image }} {{ bio_container_recipe_dir }}/{{ container_name }}.{{ version_number }}.def" args: creates: "{{ container_image }}" - become: true environment: SINGULARITY_TMPDIR: "{{ lookup('env', 'SINGULARITY_TMPDIR') | default('/dev/shm') }}" - name: Ensure mspipe binary directory exists diff --git a/ansible/roles/containers/tasks/bio/pggb.yaml b/ansible/roles/containers/tasks/bio/pggb.yaml index cded1b81..9f37667e 100644 --- a/ansible/roles/containers/tasks/bio/pggb.yaml +++ b/ansible/roles/containers/tasks/bio/pggb.yaml @@ -18,10 +18,9 @@ dest: "{{ bio_container_recipe_dir }}/{{ container_name }}.{{ tag }}.def" mode: u=rw,g=r,o=r - name: Build singularity container - ansible.builtin.command: "{{ singularity }} build {{ container_image }} {{ bio_container_recipe_dir }}/{{ container_name }}.{{ tag }}.def" + ansible.builtin.command: "{{ singularity }} build --fakeroot {{ container_image }} {{ bio_container_recipe_dir }}/{{ container_name }}.{{ tag }}.def" args: creates: "{{ container_image }}" - become: true environment: SINGULARITY_TMPDIR: "{{ lookup('env', 'SINGULARITY_TMPDIR') | default('/dev/shm') }}" diff --git a/ansible/roles/containers/tasks/bio/proteowizard.yaml b/ansible/roles/containers/tasks/bio/proteowizard.yaml index f0c417b5..8313c783 100644 --- a/ansible/roles/containers/tasks/bio/proteowizard.yaml +++ b/ansible/roles/containers/tasks/bio/proteowizard.yaml @@ -41,10 +41,9 @@ dest: "{{ container_recipe }}" mode: u=rw,g=r,o=r - name: Build container {{ container_image }} - ansible.builtin.command: "{{ singularity }} build {{ container_image }} {{ container_recipe }}" + ansible.builtin.command: "{{ singularity }} build --fakeroot {{ container_image }} {{ container_recipe }}" args: creates: "{{ container_image }}" - become: true - name: Ensure ProteoWizard binary directory exists ansible.builtin.file: path: "{{ proteowizard_binary_dir }}" diff --git a/ansible/roles/containers/tasks/bio/snvstory.yaml b/ansible/roles/containers/tasks/bio/snvstory.yaml index 766c36b2..d0422ca7 100644 --- a/ansible/roles/containers/tasks/bio/snvstory.yaml +++ b/ansible/roles/containers/tasks/bio/snvstory.yaml @@ -20,11 +20,10 @@ repo: git@github.com:nch-igm/snvstory.git dest: "/dev/shm/snv_story_build" - name: Build singularity container - ansible.builtin.command: "{{ singularity }} build {{ container_image }} {{ container_recipe }}" + ansible.builtin.command: "{{ singularity }} build --fakeroot {{ container_image }} {{ container_recipe }}" args: creates: "{{ container_image }}" chdir: "/dev/shm/snv_story_build" - become: true environment: SINGULARITY_TMPDIR: "{{ lookup('env', 'SINGULARITY_TMPDIR') | default('/dev/shm') }}" - name: Ensure snvstory binary directory exists diff --git a/ansible/roles/containers/tasks/bio/spectronaut.yaml b/ansible/roles/containers/tasks/bio/spectronaut.yaml index 83a9aeb2..4de13829 100644 --- a/ansible/roles/containers/tasks/bio/spectronaut.yaml +++ b/ansible/roles/containers/tasks/bio/spectronaut.yaml @@ -32,10 +32,9 @@ dest: "{{ bio_container_recipe_dir }}/{{ container_name }}.{{ version_number }}.def" mode: u=rw,g=r,o=r - name: "Build singularity container {{ container_image }}" - ansible.builtin.command: "{{ singularity }} build {{ container_image }} {{ bio_container_recipe_dir }}/{{ container_name }}.{{ version_number }}.def" + ansible.builtin.command: "{{ singularity }} build --fakeroot {{ container_image }} {{ bio_container_recipe_dir }}/{{ container_name }}.{{ version_number }}.def" args: creates: "{{ container_image }}" - become: true environment: SINGULARITY_TMPDIR: "{{ lookup('env', 'SINGULARITY_TMPDIR') | default('/dev/shm') }}" - name: Ensure spectronaut binary directory exists diff --git a/ansible/roles/containers/tasks/bio/trycycler.yaml b/ansible/roles/containers/tasks/bio/trycycler.yaml index 1dccb7d7..099ecb16 100644 --- a/ansible/roles/containers/tasks/bio/trycycler.yaml +++ b/ansible/roles/containers/tasks/bio/trycycler.yaml @@ -19,10 +19,9 @@ dest: "{{ container_recipe }}" mode: u=rw,g=r,o=r - name: Build container {{ container_image }} - ansible.builtin.command: "{{ singularity }} build {{ container_image }} {{ container_recipe }}" + ansible.builtin.command: "{{ singularity }} build --fakeroot {{ container_image }} {{ container_recipe }}" args: creates: "{{ container_image }}" - become: true - name: Ensure Trycycler binary directory exists ansible.builtin.file: path: "{{ trycycler_binary_dir }}" diff --git a/ansible/roles/containers/tasks/bio/vep.yaml b/ansible/roles/containers/tasks/bio/vep.yaml index 4dae6142..8a41e044 100644 --- a/ansible/roles/containers/tasks/bio/vep.yaml +++ b/ansible/roles/containers/tasks/bio/vep.yaml @@ -17,10 +17,9 @@ dest: "{{ container_recipe }}" mode: u=rw,g=r,o=r - name: Build container {{ container_image }} - ansible.builtin.command: "{{ singularity }} build {{ container_image }} {{ container_recipe }}" + ansible.builtin.command: "{{ singularity }} build --fakeroot {{ container_image }} {{ container_recipe }}" args: creates: "{{ container_image }}" - become: true - name: Ensure vep data directory exists {{ data_dir }} ansible.builtin.file: path: "{{ data_dir }}" diff --git a/ansible/roles/containers/tasks/common/RStudio.yaml b/ansible/roles/containers/tasks/common/RStudio.yaml index 8a99fc6a..a1370ec5 100644 --- a/ansible/roles/containers/tasks/common/RStudio.yaml +++ b/ansible/roles/containers/tasks/common/RStudio.yaml @@ -28,10 +28,9 @@ dest: "{{ container_recipe }}" mode: u=rw,g=r,o=r - name: Build container {{ container_image }} - ansible.builtin.command: "{{ singularity }} build {{ container_image }} {{ container_recipe }}" + ansible.builtin.command: "{{ singularity }} build --fakeroot {{ container_image }} {{ container_recipe }}" args: creates: "{{ container_image }}" - become: true - name: Ensure R binary directory exists ansible.builtin.file: path: "{{ R_binary_dir }}" diff --git a/ansible/roles/containers/tasks/common/gnuplot.yaml b/ansible/roles/containers/tasks/common/gnuplot.yaml index 9a1f5c87..a3275652 100644 --- a/ansible/roles/containers/tasks/common/gnuplot.yaml +++ b/ansible/roles/containers/tasks/common/gnuplot.yaml @@ -18,10 +18,9 @@ dest: "{{ common_container_recipe_dir }}/{{ container_name }}.def" mode: u=rw,g=r,o=r - name: Build singularity container - ansible.builtin.command: "{{ singularity }} build {{ container_image }} {{ common_container_recipe_dir }}/{{ container_name }}.def" + ansible.builtin.command: "{{ singularity }} build --fakeroot {{ container_image }} {{ common_container_recipe_dir }}/{{ container_name }}.def" args: creates: "{{ container_image }}" - become: true environment: SINGULARITY_TMPDIR: "{{ lookup('env', 'SINGULARITY_TMPDIR') | default('/dev/shm') }}" - name: Ensure Gnuplot binary directory exists diff --git a/ansible/roles/containers/tasks/common/python-ml-base.yaml b/ansible/roles/containers/tasks/common/python-ml-base.yaml index 1ca0d8d3..08856e0c 100644 --- a/ansible/roles/containers/tasks/common/python-ml-base.yaml +++ b/ansible/roles/containers/tasks/common/python-ml-base.yaml @@ -43,8 +43,7 @@ dest: "{{ container_recipe }}" mode: u=rw,g=r,o=r - name: Build container {{ container_image }} - ansible.builtin.command: "{{ singularity }} build {{ container_image }} {{ container_recipe }}" + ansible.builtin.command: "{{ singularity }} build --fakeroot {{ container_image }} {{ container_recipe }}" args: creates: "{{ container_image }}" chdir: "{{ common_src }}" - become: true diff --git a/ansible/roles/containers/tasks/common/python-ml.yaml b/ansible/roles/containers/tasks/common/python-ml.yaml index c1e0675b..858c97a9 100644 --- a/ansible/roles/containers/tasks/common/python-ml.yaml +++ b/ansible/roles/containers/tasks/common/python-ml.yaml @@ -29,10 +29,9 @@ dest: "{{ container_recipe }}" mode: u=rw,g=r,o=r - name: Build container {{ container_image }} - ansible.builtin.command: "{{ singularity }} build {{ container_image }} {{ container_recipe }}" + ansible.builtin.command: "{{ singularity }} build --fakeroot {{ container_image }} {{ container_recipe }}" args: creates: "{{ container_image }}" - become: true - name: Ensure python-ml binary directory exists ansible.builtin.file: path: "{{ python_ml_binary_dir }}" diff --git a/ansible/roles/containers/tasks/common/ubuntu.yaml b/ansible/roles/containers/tasks/common/ubuntu.yaml index 655cdb44..b1ba62c0 100644 --- a/ansible/roles/containers/tasks/common/ubuntu.yaml +++ b/ansible/roles/containers/tasks/common/ubuntu.yaml @@ -14,10 +14,9 @@ dest: "{{ base_container_recipe }}" mode: u=rw,g=r,o=r - name: Build container {{ base_container_image }} - ansible.builtin.command: "{{ singularity }} build {{ base_container_image }} {{ base_container_recipe }}" + ansible.builtin.command: "{{ singularity }} build --fakeroot {{ base_container_image }} {{ base_container_recipe }}" args: creates: "{{ base_container_image }}" - become: true environment: SINGULARITY_TMPDIR: "{{ lookup('env', 'SINGULARITY_TMPDIR') | default('/dev/shm') }}" @@ -49,9 +48,8 @@ dest: "{{ container_recipe }}" mode: u=rw,g=r,o=r - name: Build container {{ container_image }} - ansible.builtin.command: "{{ singularity }} build {{ container_image }} {{ container_recipe }}" + ansible.builtin.command: "{{ singularity }} build --fakeroot {{ container_image }} {{ container_recipe }}" args: creates: "{{ container_image }}" - become: true environment: SINGULARITY_TMPDIR: "{{ lookup('env', 'SINGULARITY_TMPDIR') | default('/dev/shm') }}" From d97ac698a0942d532eacdaca9357202b5f2df41f Mon Sep 17 00:00:00 2001 From: Dane Kennedy Date: Tue, 21 Jul 2026 16:52:10 +0200 Subject: [PATCH 03/11] Guard pandoc-citeproc and libfreetype6-dev for noble in RStudio.def The noble build failed at the first apt-get install with "Package 'pandoc-citeproc' has no installation candidate". pandoc-citeproc was dropped after jammy -- citeproc is built into pandoc itself since 2.11, and noble ships pandoc 3.1.3. ubuntu-compile.def already guards this package for noble; that guard was never carried into RStudio.def. Checking the rest of the package list inside ubuntu-noble-compile.sif rather than one package at a time turned up a second casualty in the block that had not run yet: libfreetype6-dev does not exist on noble, which ships libfreetype-dev. jammy has both, so that one is a rename rather than a removal. Verified by resolving the rendered noble package list against real apt inside the base image: - all 39 base packages: apt-get install --simulate OK - r-base=4.6.1* and friends from CRAN's noble-cran40: --simulate OK, candidate 4.6.1-1.2404.1 - bionic/focal/jammy still render byte-identically to HEAD An earlier availability check via packages.ubuntu.com was unreliable: it serves a page for obsolete packages, so pandoc-citeproc looked present when apt has no candidate for it. Checked against apt in the target image from now on. Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_01RACvroiDb6D5nuP8uCiv1X --- ansible/roles/containers/templates/common/RStudio.def | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ansible/roles/containers/templates/common/RStudio.def b/ansible/roles/containers/templates/common/RStudio.def index 0c0cd92a..7057164d 100644 --- a/ansible/roles/containers/templates/common/RStudio.def +++ b/ansible/roles/containers/templates/common/RStudio.def @@ -73,7 +73,7 @@ From: {{ base_container_image }} libfribidi-dev \ libharfbuzz-dev \ pandoc \ - pandoc-citeproc \ + {% if item.base_container_version_name not in ["noble"] %}pandoc-citeproc{% endif %} \ python-is-python3 \ pkg-config \ texlive \ @@ -104,7 +104,7 @@ From: {{ base_container_image }} libcairo2-dev \ libcurl4-openssl-dev \ libfontconfig-dev \ - libfreetype6-dev \ + {% if item.base_container_version_name in ["noble"] %}libfreetype-dev{% else %}libfreetype6-dev{% endif %} \ libfribidi-bin \ libfribidi-dev \ libgit2-dev \ From 7bffb2d9ea894658df5f1b698b3b14c552c64797 Mon Sep 17 00:00:00 2001 From: Dane Kennedy Date: Tue, 21 Jul 2026 18:41:01 +0200 Subject: [PATCH 04/11] Add libuv1-dev to RStudio.def for fs 2.1.0 The noble build reached the CRAN install wave and failed configuring 'fs': "Configuration failed because libuv was not found ... deb: libuv1-dev". fs 2.1.0 links system libuv instead of bundling it, and fs sits under both devtools and tidyverse, so it takes much of the stack down with it. Rather than fix the one reported package, the full explicitly-installed set was checked against Posit's sysreqs API for ubuntu 24.04 -- the 13 CRAN packages plus BiocManager, and the six Bioconductor packages (DESeq2, edgeR, limma, MethylSeekR, methylKit, bsseq). Their combined system requirements were diffed against what the recipe already installs, by comparing simulated apt installs inside ubuntu-noble-compile.sif. Only two candidates came back, and one was a false positive: libfontconfig-dev already Provides libfontconfig1-dev. libuv1-dev is the single genuine gap. Added unconditionally rather than guarded for noble: fs 2.1.0 needs libuv on every release, so rebuilding any existing version today would fail identically. libuv1-dev is present on focal (1.34.2), jammy (1.43.0) and noble (1.48.0), all confirmed in the base images. This is the one intentional render change for pre-existing versions; the render check now asserts it is the ONLY difference vs HEAD rather than dropping the byte-identity assertion. Verified in ubuntu-noble-compile.sif: pkg-config finds libuv 1.48.0, fs 2.1.0 compiles from source and loads. Not covered: bcbioRNASeq and DESeqAnalysis come from r.acidgenomics.com, which Posit's sysreqs index does not cover, so their system deps were not checked. Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_01RACvroiDb6D5nuP8uCiv1X --- ansible/roles/containers/templates/common/RStudio.def | 1 + 1 file changed, 1 insertion(+) diff --git a/ansible/roles/containers/templates/common/RStudio.def b/ansible/roles/containers/templates/common/RStudio.def index 7057164d..7cd93d50 100644 --- a/ansible/roles/containers/templates/common/RStudio.def +++ b/ansible/roles/containers/templates/common/RStudio.def @@ -117,6 +117,7 @@ From: {{ base_container_image }} libpng-dev \ libssl-dev \ libtiff5-dev \ + libuv1-dev \ libxml2-dev \ libxt-dev \ netcdf-bin \ From 09340935bb899d31ac8f2078ddaa07201fda4d44 Mon Sep 17 00:00:00 2001 From: Dane Kennedy Date: Tue, 21 Jul 2026 19:48:23 +0200 Subject: [PATCH 05/11] Use ansible_facts[] in mashmap.yaml and seqtk.yaml CI has been failing on this branch, but not because of anything in it: yamllint and ansible-lint both pass, and the failure is the third workflow step, the grep gate for top-level fact injection (ansible-lint has no rule for it). The 8 hits are in two files this branch does not touch. They arrived in 19d84f5 ("Add verkko-fillet 0.1.25"), which reintroduced the pattern that 58ff247 had swept from the tree, and merged while red. Master's own most recent run (29835997086) fails with a byte-identical list, so master is currently red and this branch inherited it. Fixed in place rather than in a separate PR, at the repo owner's request: ansible_env.X -> ansible_facts['env'].X (mashmap, 6) ansible_processor_vcpus -> ansible_facts['processor_vcpus'] (mashmap 1, seqtk 1) This matches the convention 58ff247 established and rsem.yaml already uses. Verified by running the workflow's exact grep locally: no bare references remain. Note, deliberately not changed: the `| default('')` idiom here leaves a trailing colon in LD_LIBRARY_PATH etc. when the variable is unset, which means "current directory". rsem.yaml avoids that with a `{% if ... in ansible_facts['env'] %}` guard. That is pre-existing behaviour and out of scope for this PR. Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_01RACvroiDb6D5nuP8uCiv1X --- ansible/roles/compiled/tasks/bio/mashmap.yaml | 14 +++++++------- ansible/roles/compiled/tasks/bio/seqtk.yaml | 2 +- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/ansible/roles/compiled/tasks/bio/mashmap.yaml b/ansible/roles/compiled/tasks/bio/mashmap.yaml index dd9ca992..42d6e196 100644 --- a/ansible/roles/compiled/tasks/bio/mashmap.yaml +++ b/ansible/roles/compiled/tasks/bio/mashmap.yaml @@ -43,14 +43,14 @@ chdir: "{{ unzip_dir }}" creates: "{{ unzip_dir }}/build/Makefile" environment: &mashmap_build_env - LD_LIBRARY_PATH: "{{ gsl }}/lib:{{ htslib }}/lib:{{ ansible_env.LD_LIBRARY_PATH | default('') }}" - LIBRARY_PATH: "{{ gsl }}/lib:{{ htslib }}/lib:{{ ansible_env.LIBRARY_PATH | default('') }}" - INCLUDE: "{{ gsl }}/include:{{ htslib }}/include:{{ ansible_env.INCLUDE | default('') }}" - C_INCLUDE_PATH: "{{ gsl }}/include:{{ htslib }}/include:{{ ansible_env.C_INCLUDE_PATH | default('') }}" - CPATH: "{{ gsl }}/include:{{ htslib }}/include:{{ ansible_env.CPATH | default('') }}" - PKG_CONFIG_PATH: "{{ gsl }}/lib/pkgconfig:{{ htslib }}/lib/pkgconfig:{{ ansible_env.PKG_CONFIG_PATH | default('') }}" + LD_LIBRARY_PATH: "{{ gsl }}/lib:{{ htslib }}/lib:{{ ansible_facts['env'].LD_LIBRARY_PATH | default('') }}" + LIBRARY_PATH: "{{ gsl }}/lib:{{ htslib }}/lib:{{ ansible_facts['env'].LIBRARY_PATH | default('') }}" + INCLUDE: "{{ gsl }}/include:{{ htslib }}/include:{{ ansible_facts['env'].INCLUDE | default('') }}" + C_INCLUDE_PATH: "{{ gsl }}/include:{{ htslib }}/include:{{ ansible_facts['env'].C_INCLUDE_PATH | default('') }}" + CPATH: "{{ gsl }}/include:{{ htslib }}/include:{{ ansible_facts['env'].CPATH | default('') }}" + PKG_CONFIG_PATH: "{{ gsl }}/lib/pkgconfig:{{ htslib }}/lib/pkgconfig:{{ ansible_facts['env'].PKG_CONFIG_PATH | default('') }}" - name: Run cmake build for mashmap {{ version_number }} - ansible.builtin.shell: "{{ cmake_bin }} --build build -- -j {{ ansible_processor_vcpus }}" + ansible.builtin.shell: "{{ cmake_bin }} --build build -- -j {{ ansible_facts['processor_vcpus'] }}" args: chdir: "{{ unzip_dir }}" creates: "{{ unzip_dir }}/build/bin/mashmap" diff --git a/ansible/roles/compiled/tasks/bio/seqtk.yaml b/ansible/roles/compiled/tasks/bio/seqtk.yaml index 39d8af8d..cf3e5212 100644 --- a/ansible/roles/compiled/tasks/bio/seqtk.yaml +++ b/ansible/roles/compiled/tasks/bio/seqtk.yaml @@ -37,7 +37,7 @@ community.general.make: chdir: "{{ unzip_dir }}" environment: - MAKEFLAGS: "-j {{ ansible_processor_vcpus }}" + MAKEFLAGS: "-j {{ ansible_facts['processor_vcpus'] }}" - name: Copy seqtk {{ version_number }} binary to {{ install_dir }} ansible.builtin.copy: src: "{{ unzip_dir }}/seqtk" From 60015783666e3f3550e00ffd193b35c1726d03c3 Mon Sep 17 00:00:00 2001 From: Dane Kennedy Date: Tue, 21 Jul 2026 20:03:19 +0200 Subject: [PATCH 06/11] Bump RStudio to 2026.07.1-147 Posit released 2026.07.1-147 after this branch was opened; the docs page now lists it as the current OSS release. Both debs are still served (2026.07.1-147 and 2026.07.0-139 each return HTTP 200, ~232 MiB), so this is a forward bump rather than a fix for a dead URL. R stays at 4.6.1 -- still the newest in noble-cran40 (4.5.3, 4.6.0, 4.6.1). This renames the artifacts: the tag pair becomes -t RStudio,RStudio2026.07.1-147-R4.6.1 and the image becomes RStudio2026.07.1-147-R4.6.1.sif. Note that a complete RStudio2026.07.0-139-R4.6.1.sif (3.9 GB) already exists on /software from an earlier run and is left in place, along with its .def. It is now orphaned -- nothing in the repo refers to that version. Verified working before the bump: rserver 2026.07.0+139, R 4.6.1, fs 2.1.0, tidyverse 2.0.0, devtools, shiny 1.14.0, DESeq2 1.52.0. Removing it is a manual decision. Separately, that image revealed a silent failure worth carrying forward: bcbioRNASeq is MISSING from it while DESeqAnalysis 0.7.3 installed fine. Both come from r.acidgenomics.com. install.packages() warns rather than errors, so the build reports success either way and the %test block only checks R's version. Not addressed here. Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_01RACvroiDb6D5nuP8uCiv1X --- ansible/roles/containers/tasks/common/main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ansible/roles/containers/tasks/common/main.yml b/ansible/roles/containers/tasks/common/main.yml index 82606951..8a31b4a6 100644 --- a/ansible/roles/containers/tasks/common/main.yml +++ b/ansible/roles/containers/tasks/common/main.yml @@ -33,7 +33,7 @@ base_container_version_name: noble rstudio_deb_release: jammy r_version: 4.6.1 - rstudio_version: 2026.07.0-139 + rstudio_version: 2026.07.1-147 ansible.builtin.include_tasks: file: RStudio.yaml loop: "{{ rstudio_versions }}" From a71585302dce3c1be9214c0fc96269934a9d678e Mon Sep 17 00:00:00 2001 From: Dane Kennedy Date: Wed, 22 Jul 2026 13:01:39 +0200 Subject: [PATCH 07/11] Build RStudio containers in SINGULARITY_TMPDIR (/dev/shm) The production build was very slow because the RStudio build had no SINGULARITY_TMPDIR, so singularity assembled the sandbox under /tmp (visible in an earlier failure as /tmp/build-temp-885908027/rootfs). Nine other container tasks already set this. RStudio was the outlier, and the block was not merely commented out -- it sat at the end of the "Create R binaries" template loop rather than on the build task, so uncommenting it as-is would not have affected the build. Moved onto the build command and switched to the same expression the other tasks use, which honours an operator override: SINGULARITY_TMPDIR: "{{ lookup('env', 'SINGULARITY_TMPDIR') | default('/dev/shm') }}" Sizing, measured from the built 2026.07.0-139 image: 3.7 GB compressed, 8.9 GB unpacked (6.9 GB /usr, mostly texlive; 1.7 GB /library of R packages). The sandbox plus apt and R build temp wants roughly 15-20 GB of /dev/shm. /dev/shm is tmpfs, so that is RAM. The lookup above lets a smaller host point elsewhere without editing the repo. Nine further build tasks still lack this and remain slow (trycycler, ClinSV, proteowizard, PopGen_suite, vep, gatk, python-ml, python-ml-base, DIANN). Left alone as unrelated to this PR. Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_01RACvroiDb6D5nuP8uCiv1X --- ansible/roles/containers/tasks/common/RStudio.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ansible/roles/containers/tasks/common/RStudio.yaml b/ansible/roles/containers/tasks/common/RStudio.yaml index a1370ec5..4d2571d8 100644 --- a/ansible/roles/containers/tasks/common/RStudio.yaml +++ b/ansible/roles/containers/tasks/common/RStudio.yaml @@ -31,6 +31,8 @@ ansible.builtin.command: "{{ singularity }} build --fakeroot {{ container_image }} {{ container_recipe }}" args: creates: "{{ container_image }}" + environment: + SINGULARITY_TMPDIR: "{{ lookup('env', 'SINGULARITY_TMPDIR') | default('/dev/shm') }}" - name: Ensure R binary directory exists ansible.builtin.file: path: "{{ R_binary_dir }}" @@ -48,8 +50,6 @@ - { name: "rstudio", template: "RStudio.sh" } loop_control: loop_var: binary -# environment: -# SINGULARITY_TMPDIR: "/dev/shm" - name: Install RStudio module {{ container_name }} block: From d691e572aa55986d155f88df66b2dc19a91c6dbd Mon Sep 17 00:00:00 2001 From: Dane Kennedy Date: Wed, 22 Jul 2026 13:04:27 +0200 Subject: [PATCH 08/11] Log playbook runs to ansible/logs/ansible.log Adds log_path to ansible.cfg so every run appends to a file instead of only scrolling past in the terminal. Useful for the long container builds, where the interesting failure is thousands of lines back. log_path = ./logs/ansible.log The directory is tracked via a .gitkeep because ansible does not create it: if it is missing, ansible warns and silently disables logging rather than failing. Contents are gitignored (*.log was already covered globally; an explicit ansible/logs/* rule with negations for .gitkeep and README.md makes the intent local and obvious). Verified: the log is created, contains the run, and a second run appends to it rather than truncating (12 -> 24 lines). git correctly ignores ansible.log while staging only .gitkeep and README.md. Three caveats documented in ansible/logs/README.md rather than left to be discovered: - Nothing rotates this and container builds are verbose. A logrotate snippet is included there. - Task output is captured verbatim. no_log is honoured, but everything else a module prints ends up in the clear, so the file should be treated as sensitive. - The path is relative, so it only resolves as intended when playbooks are run from ansible/ -- already required for ansible.cfg to be found at all. ANSIBLE_LOG_PATH overrides it per run, which is handy for giving a long build its own file. Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_01RACvroiDb6D5nuP8uCiv1X --- .gitignore | 6 ++++++ ansible/ansible.cfg | 8 ++++++++ ansible/logs/.gitkeep | 0 ansible/logs/README.md | 38 ++++++++++++++++++++++++++++++++++++++ 4 files changed, 52 insertions(+) create mode 100644 ansible/logs/.gitkeep create mode 100644 ansible/logs/README.md diff --git a/.gitignore b/.gitignore index 43d4fba7..f1505539 100644 --- a/.gitignore +++ b/.gitignore @@ -136,3 +136,9 @@ dmypy.json # ansible-lint / ansible runtime cache ansible/.ansible/ + +# Playbook logs (ansible.cfg sets log_path = ./logs/ansible.log). +# The directory itself is tracked so ansible has somewhere to write. +ansible/logs/* +!ansible/logs/.gitkeep +!ansible/logs/README.md diff --git a/ansible/ansible.cfg b/ansible/ansible.cfg index 6f60cfde..58c58d57 100644 --- a/ansible/ansible.cfg +++ b/ansible/ansible.cfg @@ -12,3 +12,11 @@ inject_facts_as_vars = False callbacks_enabled = profile_tasks result_format = yaml + +# Append every playbook run to a log. Relative to the working directory, so this +# assumes playbooks are run from ansible/ (which is also where ansible.cfg is +# discovered from). The directory must exist -- ansible warns and silently +# disables logging if it does not, hence logs/.gitkeep. +# Override per-run with ANSIBLE_LOG_PATH=/somewhere/else. +# Not rotated: see logs/README.md before this grows unbounded. +log_path = ./logs/ansible.log diff --git a/ansible/logs/.gitkeep b/ansible/logs/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/ansible/logs/README.md b/ansible/logs/README.md new file mode 100644 index 00000000..409eb816 --- /dev/null +++ b/ansible/logs/README.md @@ -0,0 +1,38 @@ +# Playbook logs + +`ansible.cfg` sets `log_path = ./logs/ansible.log`, so every run from `ansible/` +appends here. The contents are gitignored; this directory is tracked only so +that ansible has somewhere to write — it warns and disables logging if the +directory is missing. + +## Things to know + +- **It appends, and nothing rotates it.** A full container build logs a lot. Add + a logrotate rule if this becomes a problem: + + ``` + /path/to/repo/ansible/logs/ansible.log { + weekly + rotate 8 + compress + missingok + notifempty + copytruncate + } + ``` + +- **Treat it as sensitive.** Task output lands here verbatim. Ansible honours + `no_log: true`, but anything else a module prints — paths, hostnames, package + URLs, command output — is captured in the clear. + +- **It depends on the working directory.** The path is relative, so it only + resolves as intended when playbooks are run from `ansible/`. That is already + required for `ansible.cfg` itself to be discovered. + +- **To send a run somewhere else**, override per invocation: + + ```bash + ANSIBLE_LOG_PATH=/tmp/rstudio-build.log ansible-playbook site.yaml -t RStudio,... + ``` + + This is useful for keeping a long container build's output in its own file. From fd3fb91c8811e2608d3fee63bb063a6907ec5f58 Mon Sep 17 00:00:00 2001 From: Dane Kennedy Date: Wed, 22 Jul 2026 13:23:54 +0200 Subject: [PATCH 09/11] Actually use /dev/shm for RStudio builds The SINGULARITY_TMPDIR expression added in a715853 never worked. lookup('env') returns '' rather than undefined when the variable is unset, and `| default(x)` only substitutes for undefined values, so the task was setting SINGULARITY_TMPDIR to the empty string. Verified with singularity 4.4.1, printing SINGULARITY_ROOTFS from %setup: SINGULARITY_TMPDIR="" -> /tmp/build-temp-889771773/rootfs SINGULARITY_TMPDIR unset -> /tmp/build-temp-3153017378/rootfs SINGULARITY_TMPDIR=/dev/shm -> /dev/shm/build-temp-472999930/rootfs So the empty string behaves exactly like unset and builds fall back to /tmp. Adding the boolean argument makes '' take the default: | default('/dev/shm', true) Note this pattern is copied verbatim in nine other container tasks (ubuntu, gnuplot, atfncat, spectronaut, mspipe, bcbio, pggb, beast, snvstory), which have therefore also been building in /tmp rather than /dev/shm. Not changed here: for the larger images that is a real behaviour change, since /dev/shm is RAM and python-ml-base alone is 9.7 GB compressed. Worth a separate, deliberate pass. Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_01RACvroiDb6D5nuP8uCiv1X --- ansible/roles/containers/tasks/common/RStudio.yaml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/ansible/roles/containers/tasks/common/RStudio.yaml b/ansible/roles/containers/tasks/common/RStudio.yaml index 4d2571d8..fc120ec8 100644 --- a/ansible/roles/containers/tasks/common/RStudio.yaml +++ b/ansible/roles/containers/tasks/common/RStudio.yaml @@ -32,7 +32,10 @@ args: creates: "{{ container_image }}" environment: - SINGULARITY_TMPDIR: "{{ lookup('env', 'SINGULARITY_TMPDIR') | default('/dev/shm') }}" + # `| default(x)` alone is not enough: lookup('env') returns '' (not + # undefined) when unset, and singularity treats '' exactly like + # unset, falling back to /tmp. The `true` makes '' take the default. + SINGULARITY_TMPDIR: "{{ lookup('env', 'SINGULARITY_TMPDIR') | default('/dev/shm', true) }}" - name: Ensure R binary directory exists ansible.builtin.file: path: "{{ R_binary_dir }}" From dc20596f88a21a78882fc5cb7682cf1f455fc1fd Mon Sep 17 00:00:00 2001 From: Dane Kennedy Date: Wed, 22 Jul 2026 13:27:55 +0200 Subject: [PATCH 10/11] Fix SINGULARITY_TMPDIR across the remaining container tasks Applies the fd3fb91 fix everywhere else the pattern appears, so container builds actually land in /dev/shm rather than silently falling back to /tmp. | default('/dev/shm') -> | default('/dev/shm', true) 8 occurrences across 7 files: ubuntu (x2), gnuplot, atfncat, spectronaut, mspipe, pggb, snvstory. Two files needed something different: - beast.yaml had the block commented out. Unlike RStudio's, it was correctly positioned on the build task, so it only needed enabling. Switched to the lookup form for consistency. - bcbio.yaml hardcoded "/dev/shm", so it was the one task already building in the right place. Normalised to the lookup form; same default, but an operator can now redirect it with SINGULARITY_TMPDIR. Verified: the expression resolves to /dev/shm when the variable is unset and to the variable's value when set (checked with /scratch). All 11 occurrences are attached to a build task -- confirmed by walking each file and pairing the setting with its enclosing task name, since RStudio's had been sitting on an unrelated template loop. Deliberately not touched: nine build tasks set no SINGULARITY_TMPDIR at all (trycycler, ClinSV, proteowizard, PopGen_suite, vep, gatk, python-ml, python-ml-base, DIANN) and keep building in /tmp. Adding it there is a genuine behaviour change rather than a bug fix -- /dev/shm is RAM, and python-ml-base is 9.1 GB compressed, so it could exhaust /dev/shm on a host where the build currently succeeds. That needs per-image sizing, not a blanket edit. Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_01RACvroiDb6D5nuP8uCiv1X --- ansible/roles/containers/tasks/astro/atfncat.yaml | 2 +- ansible/roles/containers/tasks/bio/bcbio.yaml | 2 +- ansible/roles/containers/tasks/bio/beast.yaml | 4 ++-- ansible/roles/containers/tasks/bio/mspipe.yaml | 2 +- ansible/roles/containers/tasks/bio/pggb.yaml | 2 +- ansible/roles/containers/tasks/bio/snvstory.yaml | 2 +- ansible/roles/containers/tasks/bio/spectronaut.yaml | 2 +- ansible/roles/containers/tasks/common/gnuplot.yaml | 2 +- ansible/roles/containers/tasks/common/ubuntu.yaml | 4 ++-- 9 files changed, 11 insertions(+), 11 deletions(-) diff --git a/ansible/roles/containers/tasks/astro/atfncat.yaml b/ansible/roles/containers/tasks/astro/atfncat.yaml index 8ee09893..f42a23dd 100644 --- a/ansible/roles/containers/tasks/astro/atfncat.yaml +++ b/ansible/roles/containers/tasks/astro/atfncat.yaml @@ -35,7 +35,7 @@ args: creates: "{{ container_image }}" environment: - SINGULARITY_TMPDIR: "{{ lookup('env', 'SINGULARITY_TMPDIR') | default('/dev/shm') }}" + SINGULARITY_TMPDIR: "{{ lookup('env', 'SINGULARITY_TMPDIR') | default('/dev/shm', true) }}" - name: Ensure atfncat binary directory exists ansible.builtin.file: path: "{{ atfncat_binary_dir }}" diff --git a/ansible/roles/containers/tasks/bio/bcbio.yaml b/ansible/roles/containers/tasks/bio/bcbio.yaml index 0f268b3b..880abf14 100644 --- a/ansible/roles/containers/tasks/bio/bcbio.yaml +++ b/ansible/roles/containers/tasks/bio/bcbio.yaml @@ -31,7 +31,7 @@ args: creates: "{{ container_image }}" environment: - SINGULARITY_TMPDIR: "/dev/shm" + SINGULARITY_TMPDIR: "{{ lookup('env', 'SINGULARITY_TMPDIR') | default('/dev/shm', true) }}" - name: Install bcbio module {{ container_name }} block: diff --git a/ansible/roles/containers/tasks/bio/beast.yaml b/ansible/roles/containers/tasks/bio/beast.yaml index 34fced3e..2bf06af8 100644 --- a/ansible/roles/containers/tasks/bio/beast.yaml +++ b/ansible/roles/containers/tasks/bio/beast.yaml @@ -50,8 +50,8 @@ ansible.builtin.command: "{{ singularity }} build --fakeroot {{ container_image }} {{ container_recipe }}" args: creates: "{{ container_image }}" -# environment: -# SINGULARITY_TMPDIR: "/dev/shm" + environment: + SINGULARITY_TMPDIR: "{{ lookup('env', 'SINGULARITY_TMPDIR') | default('/dev/shm', true) }}" - name: Install beast module {{ container_name }} block: diff --git a/ansible/roles/containers/tasks/bio/mspipe.yaml b/ansible/roles/containers/tasks/bio/mspipe.yaml index 8b3223d8..d54d64ac 100644 --- a/ansible/roles/containers/tasks/bio/mspipe.yaml +++ b/ansible/roles/containers/tasks/bio/mspipe.yaml @@ -24,7 +24,7 @@ args: creates: "{{ container_image }}" environment: - SINGULARITY_TMPDIR: "{{ lookup('env', 'SINGULARITY_TMPDIR') | default('/dev/shm') }}" + SINGULARITY_TMPDIR: "{{ lookup('env', 'SINGULARITY_TMPDIR') | default('/dev/shm', true) }}" - name: Ensure mspipe binary directory exists ansible.builtin.file: path: "{{ mspipe_binary_dir }}" diff --git a/ansible/roles/containers/tasks/bio/pggb.yaml b/ansible/roles/containers/tasks/bio/pggb.yaml index 9f37667e..dc5349fe 100644 --- a/ansible/roles/containers/tasks/bio/pggb.yaml +++ b/ansible/roles/containers/tasks/bio/pggb.yaml @@ -22,7 +22,7 @@ args: creates: "{{ container_image }}" environment: - SINGULARITY_TMPDIR: "{{ lookup('env', 'SINGULARITY_TMPDIR') | default('/dev/shm') }}" + SINGULARITY_TMPDIR: "{{ lookup('env', 'SINGULARITY_TMPDIR') | default('/dev/shm', true) }}" - name: Install pggb module {{ container_name }} block: diff --git a/ansible/roles/containers/tasks/bio/snvstory.yaml b/ansible/roles/containers/tasks/bio/snvstory.yaml index d0422ca7..9eabc194 100644 --- a/ansible/roles/containers/tasks/bio/snvstory.yaml +++ b/ansible/roles/containers/tasks/bio/snvstory.yaml @@ -25,7 +25,7 @@ creates: "{{ container_image }}" chdir: "/dev/shm/snv_story_build" environment: - SINGULARITY_TMPDIR: "{{ lookup('env', 'SINGULARITY_TMPDIR') | default('/dev/shm') }}" + SINGULARITY_TMPDIR: "{{ lookup('env', 'SINGULARITY_TMPDIR') | default('/dev/shm', true) }}" - name: Ensure snvstory binary directory exists ansible.builtin.file: path: "{{ snvstory_binary_dir }}" diff --git a/ansible/roles/containers/tasks/bio/spectronaut.yaml b/ansible/roles/containers/tasks/bio/spectronaut.yaml index 4de13829..d8607396 100644 --- a/ansible/roles/containers/tasks/bio/spectronaut.yaml +++ b/ansible/roles/containers/tasks/bio/spectronaut.yaml @@ -36,7 +36,7 @@ args: creates: "{{ container_image }}" environment: - SINGULARITY_TMPDIR: "{{ lookup('env', 'SINGULARITY_TMPDIR') | default('/dev/shm') }}" + SINGULARITY_TMPDIR: "{{ lookup('env', 'SINGULARITY_TMPDIR') | default('/dev/shm', true) }}" - name: Ensure spectronaut binary directory exists ansible.builtin.file: path: "{{ spectronaut_binary_dir }}" diff --git a/ansible/roles/containers/tasks/common/gnuplot.yaml b/ansible/roles/containers/tasks/common/gnuplot.yaml index a3275652..3ed9f3a3 100644 --- a/ansible/roles/containers/tasks/common/gnuplot.yaml +++ b/ansible/roles/containers/tasks/common/gnuplot.yaml @@ -22,7 +22,7 @@ args: creates: "{{ container_image }}" environment: - SINGULARITY_TMPDIR: "{{ lookup('env', 'SINGULARITY_TMPDIR') | default('/dev/shm') }}" + SINGULARITY_TMPDIR: "{{ lookup('env', 'SINGULARITY_TMPDIR') | default('/dev/shm', true) }}" - name: Ensure Gnuplot binary directory exists ansible.builtin.file: path: "{{ gnuplot_binary_dir }}" diff --git a/ansible/roles/containers/tasks/common/ubuntu.yaml b/ansible/roles/containers/tasks/common/ubuntu.yaml index b1ba62c0..2ba2e27b 100644 --- a/ansible/roles/containers/tasks/common/ubuntu.yaml +++ b/ansible/roles/containers/tasks/common/ubuntu.yaml @@ -18,7 +18,7 @@ args: creates: "{{ base_container_image }}" environment: - SINGULARITY_TMPDIR: "{{ lookup('env', 'SINGULARITY_TMPDIR') | default('/dev/shm') }}" + SINGULARITY_TMPDIR: "{{ lookup('env', 'SINGULARITY_TMPDIR') | default('/dev/shm', true) }}" - name: Create ubuntu compile container {{ item.version_name }} vars: @@ -52,4 +52,4 @@ args: creates: "{{ container_image }}" environment: - SINGULARITY_TMPDIR: "{{ lookup('env', 'SINGULARITY_TMPDIR') | default('/dev/shm') }}" + SINGULARITY_TMPDIR: "{{ lookup('env', 'SINGULARITY_TMPDIR') | default('/dev/shm', true) }}" From 59033b5c0023eab9fbc9d272289ed6a9a9492565 Mon Sep 17 00:00:00 2001 From: Dane Kennedy Date: Wed, 22 Jul 2026 13:41:55 +0200 Subject: [PATCH 11/11] Build every container in SINGULARITY_TMPDIR (/dev/shm) Completes the sweep: the nine tasks that set no SINGULARITY_TMPDIR at all now do, so every container build lands in /dev/shm instead of /tmp. trycycler, ClinSV, proteowizard, PopGen_suite, vep, gatk, python-ml, python-ml-base, DIANN (x2) -- 10 build commands across 9 files. Unlike the previous commit this is a behaviour change rather than a bug fix, since these tasks were never trying to use /dev/shm. Confirmed acceptable: production nodes have >200 GiB RAM, so the default /dev/shm of half of RAM is ~100 GiB. The largest image involved, python-ml-base, is 9.1 GB compressed -- comfortably within that. Smaller build hosts can still redirect via SINGULARITY_TMPDIR, which the lookup honours. Insertion was done programmatically to get the indentation right, since it varies (vep uses 6 spaces where the rest use 10) and python-ml-base carries an extra `chdir` inside args that had to stay there. Both were checked by hand in the diff. Verified: - no build task is left without the setting - 21 build commands, 21 SINGULARITY_TMPDIR settings -- exact parity - every setting pairs with a Build task, checked by walking each file and printing the enclosing task name, since RStudio's had previously been stranded on an unrelated template loop - yamllint clean, ansible-lint production profile passes Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_01RACvroiDb6D5nuP8uCiv1X --- ansible/roles/containers/tasks/bio/ClinSV.yaml | 2 ++ ansible/roles/containers/tasks/bio/DIANN.yaml | 4 ++++ ansible/roles/containers/tasks/bio/PopGen_suite.yaml | 2 ++ ansible/roles/containers/tasks/bio/gatk.yaml | 2 ++ ansible/roles/containers/tasks/bio/proteowizard.yaml | 2 ++ ansible/roles/containers/tasks/bio/trycycler.yaml | 2 ++ ansible/roles/containers/tasks/bio/vep.yaml | 2 ++ ansible/roles/containers/tasks/common/python-ml-base.yaml | 2 ++ ansible/roles/containers/tasks/common/python-ml.yaml | 2 ++ 9 files changed, 20 insertions(+) diff --git a/ansible/roles/containers/tasks/bio/ClinSV.yaml b/ansible/roles/containers/tasks/bio/ClinSV.yaml index 45db710c..52ded59b 100644 --- a/ansible/roles/containers/tasks/bio/ClinSV.yaml +++ b/ansible/roles/containers/tasks/bio/ClinSV.yaml @@ -24,6 +24,8 @@ ansible.builtin.command: "{{ singularity }} build --fakeroot {{ container_image }} {{ container_recipe }}" args: creates: "{{ container_image }}" + environment: + SINGULARITY_TMPDIR: "{{ lookup('env', 'SINGULARITY_TMPDIR') | default('/dev/shm', true) }}" - name: Ensure data dir exists ansible.builtin.file: path: "{{ ClinSV_data_dir }}" diff --git a/ansible/roles/containers/tasks/bio/DIANN.yaml b/ansible/roles/containers/tasks/bio/DIANN.yaml index 2431cd15..a514eed3 100644 --- a/ansible/roles/containers/tasks/bio/DIANN.yaml +++ b/ansible/roles/containers/tasks/bio/DIANN.yaml @@ -65,6 +65,8 @@ ansible.builtin.command: "{{ singularity }} build --fakeroot {{ container_image }} {{ container_recipe }}" args: creates: "{{ container_image }}" + environment: + SINGULARITY_TMPDIR: "{{ lookup('env', 'SINGULARITY_TMPDIR') | default('/dev/shm', true) }}" - name: Build DIANN zip container when: installer_type == 'zip' @@ -78,6 +80,8 @@ ansible.builtin.command: "{{ singularity }} build --fakeroot {{ container_image }} {{ container_recipe }}" args: creates: "{{ container_image }}" + environment: + SINGULARITY_TMPDIR: "{{ lookup('env', 'SINGULARITY_TMPDIR') | default('/dev/shm', true) }}" - name: Ensure DIANN binary directory exists ansible.builtin.file: diff --git a/ansible/roles/containers/tasks/bio/PopGen_suite.yaml b/ansible/roles/containers/tasks/bio/PopGen_suite.yaml index 6f573047..ba4d64d4 100644 --- a/ansible/roles/containers/tasks/bio/PopGen_suite.yaml +++ b/ansible/roles/containers/tasks/bio/PopGen_suite.yaml @@ -95,6 +95,8 @@ ansible.builtin.command: "{{ singularity }} build --fakeroot {{ container_image }} {{ container_recipe }}" args: creates: "{{ container_image }}" + environment: + SINGULARITY_TMPDIR: "{{ lookup('env', 'SINGULARITY_TMPDIR') | default('/dev/shm', true) }}" - name: Install PopGen module {{ container_name }} block: diff --git a/ansible/roles/containers/tasks/bio/gatk.yaml b/ansible/roles/containers/tasks/bio/gatk.yaml index fc0ecfdb..69dca185 100644 --- a/ansible/roles/containers/tasks/bio/gatk.yaml +++ b/ansible/roles/containers/tasks/bio/gatk.yaml @@ -23,6 +23,8 @@ ansible.builtin.command: "{{ singularity }} build --fakeroot {{ container_image }} {{ container_recipe }}" args: creates: "{{ container_image }}" + environment: + SINGULARITY_TMPDIR: "{{ lookup('env', 'SINGULARITY_TMPDIR') | default('/dev/shm', true) }}" - name: Ensure gatk binary directory exists ansible.builtin.file: path: "{{ gatk_binary_dir }}" diff --git a/ansible/roles/containers/tasks/bio/proteowizard.yaml b/ansible/roles/containers/tasks/bio/proteowizard.yaml index 8313c783..f4e1b019 100644 --- a/ansible/roles/containers/tasks/bio/proteowizard.yaml +++ b/ansible/roles/containers/tasks/bio/proteowizard.yaml @@ -44,6 +44,8 @@ ansible.builtin.command: "{{ singularity }} build --fakeroot {{ container_image }} {{ container_recipe }}" args: creates: "{{ container_image }}" + environment: + SINGULARITY_TMPDIR: "{{ lookup('env', 'SINGULARITY_TMPDIR') | default('/dev/shm', true) }}" - name: Ensure ProteoWizard binary directory exists ansible.builtin.file: path: "{{ proteowizard_binary_dir }}" diff --git a/ansible/roles/containers/tasks/bio/trycycler.yaml b/ansible/roles/containers/tasks/bio/trycycler.yaml index 099ecb16..1bc04d6a 100644 --- a/ansible/roles/containers/tasks/bio/trycycler.yaml +++ b/ansible/roles/containers/tasks/bio/trycycler.yaml @@ -22,6 +22,8 @@ ansible.builtin.command: "{{ singularity }} build --fakeroot {{ container_image }} {{ container_recipe }}" args: creates: "{{ container_image }}" + environment: + SINGULARITY_TMPDIR: "{{ lookup('env', 'SINGULARITY_TMPDIR') | default('/dev/shm', true) }}" - name: Ensure Trycycler binary directory exists ansible.builtin.file: path: "{{ trycycler_binary_dir }}" diff --git a/ansible/roles/containers/tasks/bio/vep.yaml b/ansible/roles/containers/tasks/bio/vep.yaml index 8a41e044..c562842f 100644 --- a/ansible/roles/containers/tasks/bio/vep.yaml +++ b/ansible/roles/containers/tasks/bio/vep.yaml @@ -20,6 +20,8 @@ ansible.builtin.command: "{{ singularity }} build --fakeroot {{ container_image }} {{ container_recipe }}" args: creates: "{{ container_image }}" + environment: + SINGULARITY_TMPDIR: "{{ lookup('env', 'SINGULARITY_TMPDIR') | default('/dev/shm', true) }}" - name: Ensure vep data directory exists {{ data_dir }} ansible.builtin.file: path: "{{ data_dir }}" diff --git a/ansible/roles/containers/tasks/common/python-ml-base.yaml b/ansible/roles/containers/tasks/common/python-ml-base.yaml index 08856e0c..450e0f62 100644 --- a/ansible/roles/containers/tasks/common/python-ml-base.yaml +++ b/ansible/roles/containers/tasks/common/python-ml-base.yaml @@ -47,3 +47,5 @@ args: creates: "{{ container_image }}" chdir: "{{ common_src }}" + environment: + SINGULARITY_TMPDIR: "{{ lookup('env', 'SINGULARITY_TMPDIR') | default('/dev/shm', true) }}" diff --git a/ansible/roles/containers/tasks/common/python-ml.yaml b/ansible/roles/containers/tasks/common/python-ml.yaml index 858c97a9..00e0d776 100644 --- a/ansible/roles/containers/tasks/common/python-ml.yaml +++ b/ansible/roles/containers/tasks/common/python-ml.yaml @@ -32,6 +32,8 @@ ansible.builtin.command: "{{ singularity }} build --fakeroot {{ container_image }} {{ container_recipe }}" args: creates: "{{ container_image }}" + environment: + SINGULARITY_TMPDIR: "{{ lookup('env', 'SINGULARITY_TMPDIR') | default('/dev/shm', true) }}" - name: Ensure python-ml binary directory exists ansible.builtin.file: path: "{{ python_ml_binary_dir }}"