Skip to content

05_figures.R reports writing fig_main.pdf when the PDF device failed, and leaves Rplots.pdf behind #156

Closed

Description

@pedrohcgs

Revision: 6246c35 (main, after the v2.6.0 merge). Found by the verifier agent during the v2.6.0 pre-commit check; reproduced by hand.

Observed. On macOS without XQuartz (R 4.3.1), Rscript scripts/R/00_run_all.R exits 0 and logs Wrote …/output/fig_main.pdf. No fig_main.pdf exists, and a stray Rplots.pdf (7,977 bytes) appears in the working directory (the repo root). The log also shows failed to load cairo DLL.

Expected. Either output/fig_main.pdf exists, or the run stops with an error that names the missing figure. No file is written outside output/.

Root cause. capabilities("cairo") returns TRUE because cairo is compiled in, but the library fails to load at run time (Library not loaded: /opt/X11/lib/libXrender.1.dylib). The script picks cairo_pdf from capabilities() alone. The device never opens, so ggsave draws on R's default device, which is Rplots.pdf in the working directory, and the "Wrote" message prints anyway because nothing checks the file.

Reproducer. On a machine where capabilities("cairo") is TRUE but the cairo DLL cannot load:

Rscript scripts/R/00_run_all.R; ls output/ Rplots.pdf

Result: 1 of 1 runs on the affected machine. Control: fig_main.svg (via svglite) is written correctly in the same run.

Consequence. A promised artifact goes silently missing. A Beamer deck that includes output/fig_main.pdf then fails later, far from the cause. Meanwhile the log claims success, which breaks the script's own contract to fail loudly rather than skip a promised figure. The stray Rplots.pdf can also end up committed by accident.

Scope. scripts/R/05_figures.R only. Non-scope: installing cairo or XQuartz, and other scripts (none of the others write a figure).

Acceptance criteria.

  1. The PDF device is chosen by actually opening it, not from capabilities(): cairo is used when it works, otherwise grDevices::pdf.
  2. After each ggsave, the file must exist and be non-empty, or the script stops and names the file.
  3. The "Wrote" message prints only after that check.
  4. No Rplots.pdf is created.
  5. On the affected machine the pipeline produces output/fig_main.pdf (through the fallback device) and exits 0, and the quality score stays at 100/100.

Activity

  1. pedrohcgs commented on Sep 27, 2026

    @pedrohcgs
    OwnerAuthor

    Landed on main in #163 (merge commit 97887fc).

    What was wrong. 05_figures.R chose cairo_pdf whenever capabilities("cairo") was TRUE. On macOS without XQuartz, cairo is compiled in but cannot load, so ggsave failed to write fig_main.pdf, the script still printed "Wrote fig_main.pdf", and R's default device left Rplots.pdf in the working directory.

    What was tried. Checking capabilities("cairo") more carefully does not help: it reports what R was built with, not what can be opened. Catching the ggsave error alone also falls short, because on this failure ggsave does not always raise.

    What fixed it (0425fd6). cairo_works() opens cairo_pdf on a temp file and falls back to grDevices::pdf if that fails. save_checked() deletes the target, saves, and stops with the file name if the file is missing or empty; "Wrote" is printed only after that check.

    How it was checked. A full 00_run_all.R run in a scratch copy at 0425fd6 writes output/fig_main.pdf (7,945 bytes) and fig_main.svg, and no Rplots.pdf. A seeded no-op ggsave makes the script stop, naming fig_main.pdf.

    What remains. Nothing.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions