From fb3f2f172fba0c1c1b3cb6a46cb7883e466e1096 Mon Sep 17 00:00:00 2001 From: choxos Date: Thu, 11 Jun 2026 19:48:31 -0400 Subject: [PATCH] Cross-distro bundled database, 33 log patterns, apk detection, PPM fallback fix The bundled fallback database now stores one row per (r_package, package_manager, system_package) with names for apt, dnf (reused by yum), zypper, and apk. The apt, dnf, and zypper names are generated from the Posit Package Manager database for Ubuntu 22.04, Red Hat 9, and openSUSE 15.6; the Alpine names are hand curated. backend = "bundled" and auto routing now work on Fedora, RHEL rebuilds, openSUSE, and Alpine. Log diagnosis patterns grew from 12 to 33, adding zlib, bzip2, xz, png, jpeg, tiff, freetype, fontconfig, cairo, SQLite, PostgreSQL, MariaDB, libsodium, GMP, MPFR, GLPK, GEOS, ImageMagick, poppler, leptonica, tesseract, ICU, webp, and Cyrus SASL, each with per-manager names. Fixes: Posit Package Manager query failures no longer stop with the misleading apt-only bundled error on non-apt platforms; installed-state detection now covers Alpine via apk info; the startup message suggests setup_advice() for the detected platform. --- NEWS.md | 25 + R/bundled-sysreqs.R | 843 +++++++++++++++++++++++++++--- R/check.R | 13 +- R/diagnose-log.R | 187 ++++++- R/plan.R | 2 + R/ppm.R | 16 +- R/zzz.R | 27 +- README.md | 8 +- data-raw/update-bundled-sysreqs.R | 179 ++++++- man/check_packages.Rd | 5 +- man/ppm_sysreqs.Rd | 3 +- tests/testthat/test-check.R | 139 +++-- tests/testthat/test-diagnose.R | 57 ++ tests/testthat/test-plan.R | 19 + tests/testthat/test-ppm.R | 39 ++ vignettes/faq.Rmd | 10 +- vignettes/preflight-setup.Rmd | 6 +- 17 files changed, 1399 insertions(+), 179 deletions(-) diff --git a/NEWS.md b/NEWS.md index 34e6872..b909aba 100644 --- a/NEWS.md +++ b/NEWS.md @@ -2,12 +2,31 @@ ## New features +* The bundled fallback database is now cross-distro: it stores system + package names for `apt`, `dnf` (also used by `yum` platforms), `zypper`, + and `apk`, so `backend = "bundled"` and offline fallbacks work on Fedora, + RHEL and its rebuilds, openSUSE, and Alpine, not only on Debian and + Ubuntu. The `apt`, `dnf`, and `zypper` names are generated from the Posit + Package Manager database; the Alpine names are hand curated. The `"auto"` + backend now prefers bundled data on all of these platforms. +* Log diagnosis recognizes many more missing libraries. The direct error + patterns grew from 12 to 33 and now cover, among others, zlib, bzip2, xz, + png, jpeg, tiff, freetype, fontconfig, cairo, SQLite, PostgreSQL, + MariaDB, libsodium, GMP, MPFR, GLPK, GEOS, ImageMagick, poppler, + leptonica, tesseract, ICU, webp, and Cyrus SASL, each with names for all + supported package managers. * New `gitlab_ci()` generates a GitLab CI YAML job that installs the system packages a plan needs. GitLab CI jobs usually run as root inside a container image, so the commands are emitted without `sudo`. * The bundled fallback database now also covers `igraph`, `rJava`, `jqr`, `odbc`, `av`, `rsvg`, `xslt`, and `protolite` (40 curated packages in total). +* Installed-state detection now works on Alpine: `missing_only` filtering + and the `installed` plan column use `apk info` when running on an `apk` + platform. +* The startup message suggests `setup_advice()` with the detected platform + instead of a hardcoded `ubuntu-24.04` example when the current host is a + supported Linux distribution. ## Documentation @@ -43,6 +62,12 @@ instead of pasting them into the command line. * Posit Package Manager requirements that consist only of post-install commands (for example `R CMD javareconf`) now keep their row in the plan. +* When a Posit Package Manager query fails (for example with no network), + the fallback no longer stops with "Bundled fallback data currently + supports apt platforms only." on non-apt platforms. The bundled fallback + now serves platform-matching names, and on platforms outside the bundled + data (such as Homebrew) an empty plan is returned with the original + error recorded in the `fallback_error` attribute. * `detect_platform()` now reports Alpine hosts as supported, matching `resolve_platform("alpine-3.20")` and the documented platform list. * `use_ppm()` documentation no longer claims that `scope` selects which diff --git a/R/bundled-sysreqs.R b/R/bundled-sysreqs.R index 63fc1dd..095f2bf 100644 --- a/R/bundled-sysreqs.R +++ b/R/bundled-sysreqs.R @@ -2,104 +2,821 @@ # BEGIN BUNDLED SYSREQS DATA bundled_sysreqs_db <- data.frame( r_package = c( - "curl", "curl", - "xml2", - "openssl", - "ragg", "ragg", "ragg", "ragg", "ragg", - "systemfonts", "systemfonts", - "textshaping", "textshaping", "textshaping", - "sf", "sf", "sf", "sf", "sf", - "terra", "terra", "terra", "terra", "terra", - "units", - "git2r", "git2r", "git2r", + "av", + "curl", + "curl", + "fftwtools", "gert", - "magick", "magick", - "pdftools", "pdftools", - "tesseract", "tesseract", "tesseract", - "RPostgres", + "git2r", + "git2r", + "git2r", + "gmp", + "gsl", + "hdf5r", + "igraph", + "igraph", + "jpeg", + "jqr", + "magick", + "magick", + "ncdf4", + "odbc", + "openssl", + "pdftools", + "pdftools", + "png", + "protolite", + "protolite", + "protolite", + "ragg", + "ragg", + "ragg", + "ragg", + "ragg", + "RcppGSL", + "rJava", "RMariaDB", + "Rmpfr", + "Rmpfr", "RMySQL", "RODBC", - "V8", + "RPostgres", + "rsvg", + "sf", + "sf", + "sf", + "sf", + "sf", "sodium", "stringi", - "webp", - "png", - "jpeg", + "systemfonts", + "systemfonts", + "terra", + "terra", + "terra", + "terra", + "terra", + "tesseract", + "tesseract", + "tesseract", + "textshaping", + "textshaping", + "textshaping", + "tiff", "tiff", + "units", + "V8", + "webp", + "xml2", + "xslt", + "av", + "curl", + "curl", + "fftwtools", + "gert", + "git2r", + "git2r", + "git2r", + "gmp", "gsl", + "hdf5r", + "igraph", + "igraph", + "jpeg", + "jqr", + "magick", + "magick", + "ncdf4", + "odbc", + "openssl", + "pdftools", + "pdftools", + "png", + "protolite", + "protolite", + "ragg", + "ragg", + "ragg", + "ragg", + "ragg", "RcppGSL", - "gmp", - "Rmpfr", "Rmpfr", + "rJava", + "RMariaDB", + "Rmpfr", + "Rmpfr", + "RMySQL", + "RODBC", + "RPostgres", + "rsvg", + "sf", + "sf", + "sf", + "sf", + "sf", + "sodium", + "stringi", + "systemfonts", + "systemfonts", + "terra", + "terra", + "terra", + "terra", + "terra", + "tesseract", + "tesseract", + "textshaping", + "textshaping", + "textshaping", + "tiff", + "tiff", + "units", + "V8", + "webp", + "xml2", + "xslt", + "av", + "curl", + "curl", "fftwtools", + "gert", + "git2r", + "git2r", + "git2r", + "gmp", + "gsl", "hdf5r", + "igraph", + "jpeg", + "jqr", + "magick", + "magick", "ncdf4", - "igraph", "igraph", + "odbc", + "openssl", + "pdftools", + "pdftools", + "png", + "protolite", + "ragg", + "ragg", + "ragg", + "ragg", + "ragg", + "RcppGSL", "rJava", + "RMariaDB", + "Rmpfr", + "Rmpfr", + "RMySQL", + "RODBC", + "RPostgres", + "rsvg", + "sf", + "sf", + "sf", + "sf", + "sf", + "sf", + "sodium", + "stringi", + "systemfonts", + "systemfonts", + "terra", + "terra", + "terra", + "terra", + "terra", + "terra", + "tesseract", + "tesseract", + "textshaping", + "textshaping", + "textshaping", + "tiff", + "tiff", + "webp", + "xml2", + "xslt", + "av", + "curl", + "curl", + "fftwtools", + "gert", + "git2r", + "git2r", + "git2r", + "gmp", + "gsl", + "hdf5r", + "igraph", + "igraph", + "jpeg", "jqr", + "magick", + "ncdf4", "odbc", - "av", + "openssl", + "pdftools", + "pdftools", + "png", + "protolite", + "ragg", + "ragg", + "ragg", + "ragg", + "ragg", + "RcppGSL", + "rJava", + "RMariaDB", + "Rmpfr", + "Rmpfr", + "RMySQL", + "RODBC", + "RPostgres", "rsvg", - "xslt", - "protolite", "protolite", "protolite" + "sf", + "sf", + "sf", + "sf", + "sf", + "sodium", + "stringi", + "systemfonts", + "systemfonts", + "terra", + "terra", + "terra", + "terra", + "terra", + "tesseract", + "tesseract", + "textshaping", + "textshaping", + "textshaping", + "tiff", + "tiff", + "units", + "webp", + "xml2", + "xslt" + ), + package_manager = c( + "apt", + "apt", + "apt", + "apt", + "apt", + "apt", + "apt", + "apt", + "apt", + "apt", + "apt", + "apt", + "apt", + "apt", + "apt", + "apt", + "apt", + "apt", + "apt", + "apt", + "apt", + "apt", + "apt", + "apt", + "apt", + "apt", + "apt", + "apt", + "apt", + "apt", + "apt", + "apt", + "apt", + "apt", + "apt", + "apt", + "apt", + "apt", + "apt", + "apt", + "apt", + "apt", + "apt", + "apt", + "apt", + "apt", + "apt", + "apt", + "apt", + "apt", + "apt", + "apt", + "apt", + "apt", + "apt", + "apt", + "apt", + "apt", + "apt", + "apt", + "apt", + "apt", + "apt", + "apt", + "apt", + "apt", + "apt", + "dnf", + "dnf", + "dnf", + "dnf", + "dnf", + "dnf", + "dnf", + "dnf", + "dnf", + "dnf", + "dnf", + "dnf", + "dnf", + "dnf", + "dnf", + "dnf", + "dnf", + "dnf", + "dnf", + "dnf", + "dnf", + "dnf", + "dnf", + "dnf", + "dnf", + "dnf", + "dnf", + "dnf", + "dnf", + "dnf", + "dnf", + "dnf", + "dnf", + "dnf", + "dnf", + "dnf", + "dnf", + "dnf", + "dnf", + "dnf", + "dnf", + "dnf", + "dnf", + "dnf", + "dnf", + "dnf", + "dnf", + "dnf", + "dnf", + "dnf", + "dnf", + "dnf", + "dnf", + "dnf", + "dnf", + "dnf", + "dnf", + "dnf", + "dnf", + "dnf", + "dnf", + "dnf", + "dnf", + "dnf", + "dnf", + "zypper", + "zypper", + "zypper", + "zypper", + "zypper", + "zypper", + "zypper", + "zypper", + "zypper", + "zypper", + "zypper", + "zypper", + "zypper", + "zypper", + "zypper", + "zypper", + "zypper", + "zypper", + "zypper", + "zypper", + "zypper", + "zypper", + "zypper", + "zypper", + "zypper", + "zypper", + "zypper", + "zypper", + "zypper", + "zypper", + "zypper", + "zypper", + "zypper", + "zypper", + "zypper", + "zypper", + "zypper", + "zypper", + "zypper", + "zypper", + "zypper", + "zypper", + "zypper", + "zypper", + "zypper", + "zypper", + "zypper", + "zypper", + "zypper", + "zypper", + "zypper", + "zypper", + "zypper", + "zypper", + "zypper", + "zypper", + "zypper", + "zypper", + "zypper", + "zypper", + "zypper", + "zypper", + "zypper", + "apk", + "apk", + "apk", + "apk", + "apk", + "apk", + "apk", + "apk", + "apk", + "apk", + "apk", + "apk", + "apk", + "apk", + "apk", + "apk", + "apk", + "apk", + "apk", + "apk", + "apk", + "apk", + "apk", + "apk", + "apk", + "apk", + "apk", + "apk", + "apk", + "apk", + "apk", + "apk", + "apk", + "apk", + "apk", + "apk", + "apk", + "apk", + "apk", + "apk", + "apk", + "apk", + "apk", + "apk", + "apk", + "apk", + "apk", + "apk", + "apk", + "apk", + "apk", + "apk", + "apk", + "apk", + "apk", + "apk", + "apk", + "apk", + "apk", + "apk", + "apk", + "apk" ), system_package = c( - "libcurl4-openssl-dev", "libssl-dev", - "libxml2-dev", + "libavfilter-dev", + "libcurl4-openssl-dev", "libssl-dev", - "libfreetype-dev", "libjpeg-dev", "libpng-dev", "libtiff-dev", "libwebp-dev", - "libfontconfig1-dev", "libfreetype-dev", - "libfreetype-dev", "libfribidi-dev", "libharfbuzz-dev", - "libgdal-dev", "gdal-bin", "libgeos-dev", "libproj-dev", "libsqlite3-dev", - "libgdal-dev", "gdal-bin", "libgeos-dev", "libproj-dev", "libsqlite3-dev", - "libudunits2-dev", - "libgit2-dev", "libssh2-1-dev", "libssl-dev", + "libfftw3-dev", "libgit2-dev", - "libmagick++-dev", "gsfonts", - "poppler-data", "libpoppler-cpp-dev", - "libleptonica-dev", "libtesseract-dev", "tesseract-ocr-eng", - "libpq-dev", - "default-libmysqlclient-dev", - "default-libmysqlclient-dev", + "libgit2-dev", + "libssh2-1-dev", + "libssl-dev", + "libgmp3-dev", + "libgsl-dev", + "libhdf5-dev", + "libglpk-dev", + "libxml2-dev", + "libjpeg-dev", + "libjq-dev", + "gsfonts", + "libmagick++-dev", + "libnetcdf-dev", "unixodbc-dev", - "libnode-dev", - "libsodium-dev", - "libicu-dev", - "libwebp-dev", + "libssl-dev", + "libpoppler-cpp-dev", + "poppler-data", "libpng-dev", + "libprotobuf-dev", + "libprotoc-dev", + "protobuf-compiler", + "libfreetype-dev", "libjpeg-dev", + "libpng-dev", "libtiff-dev", + "libwebp-dev", "libgsl-dev", - "libgsl-dev", - "libgmp3-dev", - "libgmp3-dev", "libmpfr-dev", - "libfftw3-dev", - "libhdf5-dev", - "libnetcdf-dev", - "libglpk-dev", "libxml2-dev", "default-jdk", - "libjq-dev", + "default-libmysqlclient-dev", + "libgmp3-dev", + "libmpfr-dev", + "default-libmysqlclient-dev", "unixodbc-dev", - "libavfilter-dev", + "libpq-dev", "librsvg2-dev", + "gdal-bin", + "libgdal-dev", + "libgeos-dev", + "libproj-dev", + "libsqlite3-dev", + "libsodium-dev", + "libicu-dev", + "libfontconfig1-dev", + "libfreetype-dev", + "gdal-bin", + "libgdal-dev", + "libgeos-dev", + "libproj-dev", + "libsqlite3-dev", + "libleptonica-dev", + "libtesseract-dev", + "tesseract-ocr-eng", + "libfreetype-dev", + "libfribidi-dev", + "libharfbuzz-dev", + "libjpeg-dev", + "libtiff-dev", + "libudunits2-dev", + "libnode-dev", + "libwebp-dev", + "libxml2-dev", "libxslt1-dev", - "libprotobuf-dev", "protobuf-compiler", "libprotoc-dev" + "libavfilter-free-devel", + "libcurl-devel", + "openssl-devel", + "fftw-devel", + "libgit2-devel", + "libgit2-devel", + "libssh2-devel", + "openssl-devel", + "gmp-devel", + "gsl-devel", + "hdf5-devel", + "glpk-devel", + "libxml2-devel", + "libjpeg-turbo-devel", + "jq-devel", + "ImageMagick-c++-devel", + "ImageMagick-devel", + "netcdf-devel", + "unixODBC-devel", + "openssl-devel", + "poppler-cpp-devel", + "poppler-data", + "libpng-devel", + "protobuf-compiler", + "protobuf-devel", + "freetype-devel", + "libjpeg-turbo-devel", + "libpng-devel", + "libtiff-devel", + "libwebp-devel", + "gsl-devel", + "java-11-openjdk-devel", + "mariadb-devel", + "gmp-devel", + "mpfr-devel", + "mariadb-devel", + "unixODBC-devel", + "libpq-devel", + "librsvg2-devel", + "gdal", + "gdal-devel", + "geos-devel", + "proj-devel", + "sqlite-devel", + "libsodium-devel", + "libicu-devel", + "fontconfig-devel", + "freetype-devel", + "gdal", + "gdal-devel", + "geos-devel", + "proj-devel", + "sqlite-devel", + "leptonica-devel", + "tesseract-devel", + "freetype-devel", + "fribidi-devel", + "harfbuzz-devel", + "libjpeg-turbo-devel", + "libtiff-devel", + "udunits2-devel", + "nodejs-libs", + "libwebp-devel", + "libxml2-devel", + "libxslt-devel", + "ffmpeg-4-libavfilter-devel", + "libcurl-devel", + "libopenssl-devel", + "fftw3-devel", + "libgit2-devel", + "libgit2-devel", + "libopenssl-devel", + "libssh2-devel", + "gmp-devel", + "gsl-devel", + "hdf5-devel", + "libxml2-devel", + "libjpeg8-devel", + "libjq-devel", + "ImageMagick-devel", + "libMagick++-devel", + "netcdf-devel", + "unixODBC-devel", + "libopenssl-devel", + "libpoppler-devel", + "poppler-data", + "libpng16-compat-devel", + "protobuf-devel", + "freetype2-devel", + "libjpeg8-devel", + "libpng16-compat-devel", + "libtiff-devel", + "libwebp-devel", + "gsl-devel", + "java-11-openjdk-devel", + "libmariadb-devel", + "gmp-devel", + "mpfr-devel", + "libmariadb-devel", + "unixODBC-devel", + "postgresql-devel", + "librsvg-devel", + "gdal", + "gdal-devel", + "geos-devel", + "proj", + "proj-devel", + "sqlite3-devel", + "libsodium-devel", + "libicu73_2-devel", + "fontconfig-devel", + "freetype2-devel", + "gdal", + "gdal-devel", + "geos-devel", + "proj", + "proj-devel", + "sqlite3-devel", + "leptonica-devel", + "tesseract-ocr-devel", + "freetype2-devel", + "fribidi-devel", + "harfbuzz-devel", + "libjpeg8-devel", + "libtiff-devel", + "libwebp-devel", + "libxml2-devel", + "libxslt-devel", + "ffmpeg-dev", + "curl-dev", + "openssl-dev", + "fftw-dev", + "libgit2-dev", + "libgit2-dev", + "libssh2-dev", + "openssl-dev", + "gmp-dev", + "gsl-dev", + "hdf5-dev", + "glpk-dev", + "libxml2-dev", + "libjpeg-turbo-dev", + "jq-dev", + "imagemagick-dev", + "netcdf-dev", + "unixodbc-dev", + "openssl-dev", + "poppler-data", + "poppler-dev", + "libpng-dev", + "protobuf-dev", + "freetype-dev", + "libjpeg-turbo-dev", + "libpng-dev", + "libwebp-dev", + "tiff-dev", + "gsl-dev", + "openjdk21-jdk", + "mariadb-connector-c-dev", + "gmp-dev", + "mpfr-dev", + "mariadb-connector-c-dev", + "unixodbc-dev", + "libpq-dev", + "librsvg-dev", + "gdal-dev", + "gdal-tools", + "geos-dev", + "proj-dev", + "sqlite-dev", + "libsodium-dev", + "icu-dev", + "fontconfig-dev", + "freetype-dev", + "gdal-dev", + "gdal-tools", + "geos-dev", + "proj-dev", + "sqlite-dev", + "leptonica-dev", + "tesseract-ocr-dev", + "freetype-dev", + "fribidi-dev", + "harfbuzz-dev", + "libjpeg-turbo-dev", + "tiff-dev", + "udunits-dev", + "libwebp-dev", + "libxml2-dev", + "libxslt-dev" ), stringsAsFactors = FALSE ) # END BUNDLED SYSREQS DATA +bundled_supported_managers <- c("apt", "dnf", "yum", "zypper", "apk") + +bundled_name_set <- function(package_manager) { + if (identical(package_manager, "yum")) "dnf" else package_manager +} + bundled_sysreqs <- function(packages, platform, repo = "cran", error = NULL) { - if (!identical(platform$package_manager, "apt")) { + pm <- platform$package_manager + if (!is.character(pm) || length(pm) != 1L || is.na(pm) || + !pm %in% bundled_supported_managers) { if (!is.null(error)) { stop(error) } - stop("Bundled fallback data currently supports apt platforms only.", call. = FALSE) + stop( + "Bundled fallback data currently supports apt, dnf, yum, zypper, ", + "and apk platforms only.", + call. = FALSE + ) } + name_set <- bundled_name_set(pm) packages <- compact_chr(packages) - data <- bundled_sysreqs_db[bundled_sysreqs_db$r_package %in% packages, , drop = FALSE] - unresolved <- setdiff(packages, bundled_sysreqs_db$r_package) + known <- unique(bundled_sysreqs_db$r_package) + data <- bundled_sysreqs_db[ + bundled_sysreqs_db$r_package %in% packages & + bundled_sysreqs_db$package_manager == name_set, , + drop = FALSE + ] + # Packages absent from the table entirely, plus packages the table knows + # for other package managers but has no name for on this one (for example + # V8 on zypper or apk). + unresolved <- c( + setdiff(packages, known), + setdiff(intersect(packages, known), unique(data$r_package)) + ) if (!nrow(data)) { if (!is.null(error)) { @@ -120,14 +837,22 @@ bundled_sysreqs <- function(packages, platform, repo = "cran", error = NULL) { data$pre_install <- NA_character_ data$post_install <- NA_character_ data$platform <- paste0(platform$distro, "-", platform$version) - data$package_manager <- platform$package_manager + data$package_manager <- pm data$installed <- NA data$source <- "bundled" data$confidence <- "medium" data$notes <- paste0( "Requirement from bundled sysreqR fallback data for repository `", repo, - "`." + "`.", + if (!identical(name_set, "apt")) { + paste0( + " Name from the bundled ", name_set, + " set; verify the exact name on your distribution." + ) + } else { + "" + } ) plan <- new_sysreqr_plan( diff --git a/R/check.R b/R/check.R index a36ae9b..9b00ebb 100644 --- a/R/check.R +++ b/R/check.R @@ -2,8 +2,9 @@ #' #' Resolves the system packages an R package needs on a given Linux platform #' and returns them in a structured plan. The `"auto"` backend prefers the -#' offline bundled database on `apt` platforms, then Posit Package Manager, -#' then `pak`. +#' offline bundled database (which carries names for the `apt`, `dnf`/`yum`, +#' `zypper`, and `apk` package managers), then Posit Package Manager, then +#' `pak`. #' #' @param packages Package names or package references. #' @param platform Platform specification accepted by [resolve_platform()]. @@ -85,9 +86,13 @@ select_backend <- function(packages, backend, platform = NULL) { return(backend) } - apt_platform <- !is.null(platform) && identical(platform$package_manager, "apt") + bundled_platform <- !is.null(platform) && + is.character(platform$package_manager) && + length(platform$package_manager) == 1L && + !is.na(platform$package_manager) && + platform$package_manager %in% bundled_supported_managers - if (apt_platform && all(is_simple_package_name(packages)) && bundled_has_packages(packages)) { + if (bundled_platform && all(is_simple_package_name(packages)) && bundled_has_packages(packages)) { "bundled" } else if (all(is_simple_package_name(packages))) { "ppm" diff --git a/R/diagnose-log.R b/R/diagnose-log.R index f8c0a77..36d9108 100644 --- a/R/diagnose-log.R +++ b/R/diagnose-log.R @@ -1,16 +1,44 @@ +# One row per missing library; the pattern matches the typical compiler +# header error and, where applicable, the linker error for the same library. +# Name columns: apt names follow the Debian/Ubuntu portability policy of the +# bundled database, dnf names follow Red Hat 9 (Fedora and the RHEL rebuilds +# use the same names; yum platforms reuse this column), zypper names follow +# openSUSE 15.6, apk names follow Alpine 3.20, brew names follow Homebrew +# formula names. diagnose_patterns <- data.frame( pattern = c( - "libxml/parser\\.h", - "curl/curl\\.h", - "openssl/ssl\\.h", - "\\bgdal\\.h\\b", - "\\bproj\\.h\\b", - "udunits2\\.h", - "harfbuzz/hb\\.h", - "fribidi\\.h", - "cannot find -lxml2", - "cannot find -lcurl", - "cannot find -lssl", + "libxml/parser\\.h|cannot find -lxml2", + "curl/curl\\.h|cannot find -lcurl", + "openssl/ssl\\.h|cannot find -lssl", + "\\bgdal\\.h\\b|cannot find -lgdal", + "\\bproj\\.h\\b|cannot find -lproj", + "udunits2\\.h|cannot find -ludunits2", + "harfbuzz/hb\\.h|cannot find -lharfbuzz", + "fribidi\\.h|cannot find -lfribidi", + "\\bzlib\\.h\\b|cannot find -lz\\b", + "bzlib\\.h|cannot find -lbz2", + "\\blzma\\.h\\b|cannot find -llzma", + "\\bpng\\.h\\b|cannot find -lpng", + "jpeglib\\.h|cannot find -ljpeg", + "\\btiff(io)?\\.h\\b|cannot find -ltiff", + "ft2build\\.h|cannot find -lfreetype", + "fontconfig\\.h|cannot find -lfontconfig", + "\\bcairo\\.h\\b|cannot find -lcairo", + "sqlite3\\.h|cannot find -lsqlite3", + "libpq-fe\\.h|cannot find -lpq\\b", + "\\bmysql\\.h\\b|cannot find -lmysqlclient|cannot find -lmariadb", + "sodium\\.h|cannot find -lsodium", + "\\bgmp\\.h\\b|cannot find -lgmp\\b", + "mpfr\\.h|cannot find -lmpfr", + "glpk\\.h|cannot find -lglpk", + "geos_c\\.h|cannot find -lgeos", + "Magick\\+\\+\\.h", + "poppler-document\\.h|cannot find -lpoppler", + "allheaders\\.h|cannot find -llept\\b", + "tesseract/baseapi\\.h|cannot find -ltesseract", + "unicode/u[a-z]+\\.h|cannot find -licu", + "webp/(de|en)code\\.h|cannot find -lwebp", + "sasl/sasl\\.h|cannot find -lsasl2", "pkg-config.*not found|pkg-config was not found" ), apt = c( @@ -22,9 +50,30 @@ diagnose_patterns <- data.frame( "libudunits2-dev", "libharfbuzz-dev", "libfribidi-dev", - "libxml2-dev", - "libcurl4-openssl-dev", - "libssl-dev", + "zlib1g-dev", + "libbz2-dev", + "liblzma-dev", + "libpng-dev", + "libjpeg-dev", + "libtiff-dev", + "libfreetype-dev", + "libfontconfig1-dev", + "libcairo2-dev", + "libsqlite3-dev", + "libpq-dev", + "default-libmysqlclient-dev", + "libsodium-dev", + "libgmp3-dev", + "libmpfr-dev", + "libglpk-dev", + "libgeos-dev", + "libmagick++-dev", + "libpoppler-cpp-dev", + "libleptonica-dev", + "libtesseract-dev", + "libicu-dev", + "libwebp-dev", + "libsasl2-dev", "pkg-config" ), dnf = c( @@ -36,9 +85,30 @@ diagnose_patterns <- data.frame( "udunits2-devel", "harfbuzz-devel", "fribidi-devel", - "libxml2-devel", - "libcurl-devel", - "openssl-devel", + "zlib-devel", + "bzip2-devel", + "xz-devel", + "libpng-devel", + "libjpeg-turbo-devel", + "libtiff-devel", + "freetype-devel", + "fontconfig-devel", + "cairo-devel", + "sqlite-devel", + "libpq-devel", + "mariadb-devel", + "libsodium-devel", + "gmp-devel", + "mpfr-devel", + "glpk-devel", + "geos-devel", + "ImageMagick-c++-devel", + "poppler-cpp-devel", + "leptonica-devel", + "tesseract-devel", + "libicu-devel", + "libwebp-devel", + "cyrus-sasl-devel", "pkgconf-pkg-config" ), zypper = c( @@ -50,9 +120,30 @@ diagnose_patterns <- data.frame( "udunits2-devel", "harfbuzz-devel", "fribidi-devel", - "libxml2-devel", - "libcurl-devel", - "libopenssl-devel", + "zlib-devel", + "libbz2-devel", + "xz-devel", + "libpng16-compat-devel", + "libjpeg8-devel", + "libtiff-devel", + "freetype2-devel", + "fontconfig-devel", + "cairo-devel", + "sqlite3-devel", + "postgresql-devel", + "libmariadb-devel", + "libsodium-devel", + "gmp-devel", + "mpfr-devel", + "glpk-devel", + "geos-devel", + "libMagick++-devel", + "libpoppler-devel", + "leptonica-devel", + "tesseract-ocr-devel", + "libicu-devel", + "libwebp-devel", + "cyrus-sasl-devel", "pkg-config" ), apk = c( @@ -64,9 +155,30 @@ diagnose_patterns <- data.frame( "udunits-dev", "harfbuzz-dev", "fribidi-dev", - "libxml2-dev", - "curl-dev", - "openssl-dev", + "zlib-dev", + "bzip2-dev", + "xz-dev", + "libpng-dev", + "libjpeg-turbo-dev", + "tiff-dev", + "freetype-dev", + "fontconfig-dev", + "cairo-dev", + "sqlite-dev", + "libpq-dev", + "mariadb-connector-c-dev", + "libsodium-dev", + "gmp-dev", + "mpfr-dev", + "glpk-dev", + "geos-dev", + "imagemagick-dev", + "poppler-dev", + "leptonica-dev", + "tesseract-ocr-dev", + "icu-dev", + "libwebp-dev", + "cyrus-sasl-dev", "pkgconf" ), brew = c( @@ -78,13 +190,34 @@ diagnose_patterns <- data.frame( "udunits", "harfbuzz", "fribidi", - "libxml2", - "curl", - "openssl@3", + "zlib", + "bzip2", + "xz", + "libpng", + "jpeg-turbo", + "libtiff", + "freetype", + "fontconfig", + "cairo", + "sqlite", + "libpq", + "mariadb-connector-c", + "libsodium", + "gmp", + "mpfr", + "glpk", + "geos", + "imagemagick", + "poppler", + "leptonica", + "tesseract", + "icu4c", + "webp", + "cyrus-sasl", "pkg-config" ), confidence = c( - rep("high", 11), + rep("high", 32), "medium" ), stringsAsFactors = FALSE diff --git a/R/plan.R b/R/plan.R index 17e694c..81a1109 100644 --- a/R/plan.R +++ b/R/plan.R @@ -272,6 +272,8 @@ list_installed_system_packages <- function(platform = NULL) { c("-qa", "--qf", shQuote("%{NAME}\n")), stdout = TRUE, stderr = FALSE ) + } else if (identical(pm, "apk") && nzchar(Sys.which("apk"))) { + system2("apk", "info", stdout = TRUE, stderr = FALSE) } else if (identical(pm, "brew") && nzchar(Sys.which("brew"))) { system2("brew", "list", stdout = TRUE, stderr = FALSE) } else { diff --git a/R/ppm.R b/R/ppm.R index b991742..52f3556 100644 --- a/R/ppm.R +++ b/R/ppm.R @@ -130,7 +130,8 @@ ppm_repo <- function(platform = NULL, repo = "cran", snapshot = "latest", #' Queries the Posit Package Manager `/sysreqs` endpoint for the given #' packages and platform, and normalizes the response into a #' `sysreqr_plan`. If the API call fails, the function falls back to the -#' bundled database and records the failure in the +#' bundled database (or to an empty plan when the bundled data has no names +#' for the platform's package manager) and records the failure in the #' `"fallback_error"` attribute of the returned plan. #' #' @param packages Package names. Required when `all = FALSE`. @@ -176,7 +177,18 @@ ppm_sysreqs <- function(packages = NULL, all = FALSE, platform = NULL, if (isTRUE(all)) { stop(res) } - plan <- bundled_sysreqs(packages, platform, repo = repo, error = NULL) + # The bundled fallback covers apt, dnf, yum, zypper, and apk. On other + # platforms (for example brew) return an empty plan that records the + # original Package Manager error instead of failing with a misleading + # message about the bundled data. + plan <- tryCatch( + bundled_sysreqs(packages, platform, repo = repo, error = NULL), + error = function(e2) { + empty <- new_sysreqr_plan(platform_info = platform, backend = "ppm") + attr(empty, "unresolved") <- packages + empty + } + ) if (isTRUE(check_installed)) { plan <- add_installed_state(plan, platform) } diff --git a/R/zzz.R b/R/zzz.R index d79c346..0018144 100644 --- a/R/zzz.R +++ b/R/zzz.R @@ -1,12 +1,37 @@ +startup_platform_example <- function() { + fallback <- "ubuntu-24.04" + platform <- tryCatch(detect_platform(), error = function(e) NULL) + if (is.null(platform) || !identical(platform$os, "linux") || + !isTRUE(platform$supported)) { + return(fallback) + } + distro <- platform$distro + version <- as.character(platform$version) + if (!is.character(distro) || is.na(distro) || !nzchar(distro) || + is.na(version) || !nzchar(version)) { + return(fallback) + } + key <- paste0(distro, "-", version) + # Only suggest keys that resolve_platform() accepts, so the startup hint + # never points users at an invalid platform string (for example when an + # os-release ID contains a dash). + resolved <- tryCatch(resolve_platform(key), error = function(e) NULL) + if (is.null(resolved)) { + return(fallback) + } + key +} + .onAttach <- function(libname, pkgname) { if (!interactive()) { return() } ver <- utils::packageVersion(pkgname) + example <- tryCatch(startup_platform_example(), error = function(e) "ubuntu-24.04") msg <- c( sprintf("sysreqr %s", ver), "", - "New here? Try setup_advice(platform = \"ubuntu-24.04\"),", + sprintf("New here? Try setup_advice(platform = \"%s\"),", example), "or help(package = \"sysreqr\") for the function reference.", "", "Guides and articles: https://choxos.github.io/sysreqR/articles/", diff --git a/README.md b/README.md index 5087fdb..a4b2dee 100644 --- a/README.md +++ b/README.md @@ -219,8 +219,10 @@ tested for: AlmaLinux): 8, 9, 10 * **Fedora**: current releases * **CentOS** 7 (legacy) -* **openSUSE Leap** / **SUSE Linux Enterprise**: 15.6 -* **Alpine**: 3.20 +* **openSUSE Leap** / **SUSE Linux Enterprise**: 15.6 (Leap 16.0 is + detected and gets `zypper` commands, but Posit Package Manager does not + publish binaries for it yet) +* **Alpine**: 3.20 and newer macOS and Windows are detected, but most package installation problems on those platforms are handled by CRAN binaries rather than system package @@ -233,7 +235,7 @@ checks. | `pak::pkg_sysreqs()` | Authoritative live resolver | Requires `pak`; no log diagnosis | | `remotes::system_requirements()` | Light; widely available | No log diagnosis, no project scanner | | `renv::sysreqs()` | Project-oriented; integrates with `renv` workflow | Requires `renv` | -| `sysreqr` | Zero runtime deps; log diagnosis; beginner UX | Bundled DB is small; biased toward `apt` | +| `sysreqr` | Zero runtime deps; log diagnosis; beginner UX | Bundled DB covers ~40 curated packages | `sysreqr` can use `pak` as one of its backends (`backend = "pak"`) when it is installed. The tools are complementary, not competitors. diff --git a/data-raw/update-bundled-sysreqs.R b/data-raw/update-bundled-sysreqs.R index 6b02a86..3cdf4d1 100644 --- a/data-raw/update-bundled-sysreqs.R +++ b/data-raw/update-bundled-sysreqs.R @@ -1,15 +1,27 @@ -# Release utility for refreshing the bundled apt system requirement data. +# Release utility for refreshing the bundled system requirement data. # Run from the package root as part of release preparation, e.g. -# Rscript data-raw/update-bundled-sysreqs.R ubuntu 22.04 +# Rscript data-raw/update-bundled-sysreqs.R +# Rscript data-raw/update-bundled-sysreqs.R cran https://packagemanager.posit.co # # The bundled table is a small, hand-curated fallback for common CRAN -# packages. It targets an apt baseline (Ubuntu 22.04 by default), but the -# committed names in R/bundled-sysreqs.R are deliberately chosen to be -# portable across Debian (bookworm/trixie) and Ubuntu (jammy/noble) -- for -# example "default-libmysqlclient-dev" rather than the Ubuntu-only -# "libmysqlclient-dev". Posit Package Manager returns Ubuntu-specific names, -# so the maintainer must review the diff this script produces and re-apply -# any portable substitutions before committing. The committed +# packages. Since 0.2.0 it stores one row per (r_package, package_manager, +# system_package): +# +# * apt names come from Posit Package Manager for Ubuntu 22.04, with +# overrides that keep them portable across Debian (bookworm/trixie) and +# Ubuntu (jammy/noble) -- for example "default-libmysqlclient-dev" +# rather than the Ubuntu-only "libmysqlclient-dev". +# * dnf names come from Package Manager for Red Hat 9 (also used for +# Fedora and the RHEL rebuilds; yum platforms reuse them at run time). +# * zypper names come from Package Manager for openSUSE 15.6. +# * apk names come from the hand-curated table below, because Package +# Manager does not cover Alpine. +# +# Build tools (make and friends) are excluded: setup_advice() handles the +# toolchain separately. When Package Manager has no entry for a curated +# package on some distribution, no row is emitted for that package manager; +# bundled_sysreqs() reports the gap in the plan notes. The maintainer must +# review the diff this script produces before committing. The committed # R/bundled-sysreqs.R is the source of truth. # # This script only refreshes the curated package set below; it never expands @@ -29,6 +41,78 @@ bundled_packages <- c( "igraph", "rJava", "jqr", "odbc", "av", "rsvg", "xslt", "protolite" ) +# Sources queried per package manager. yum platforms reuse the dnf names at +# run time, so no separate yum rows are stored. +ppm_sources <- list( + apt = list(distribution = "ubuntu", release = "22.04"), + dnf = list(distribution = "redhat", release = "9"), + zypper = list(distribution = "opensuse", release = "15.6") +) + +# Toolchain packages are advice for setup_advice(), not per-package +# requirements; drop them wherever Package Manager reports them. +build_tool_excludes <- c( + "make", "automake", "autoconf", "cmake", "gcc", "gcc-c++", "g++", + "build-essential", "pkg-config", "pkgconf", "pkgconf-pkg-config" +) + +# Portability overrides applied to apt names, so the committed names work on +# both Debian and Ubuntu. +apt_overrides <- c( + "libfreetype6-dev" = "libfreetype-dev", + "libgsl0-dev" = "libgsl-dev", + "libmysqlclient-dev" = "default-libmysqlclient-dev", + "libxslt-dev" = "libxslt1-dev" +) + +# Hand-curated Alpine (apk) names. Alpine is not covered by Package Manager; +# names follow the Alpine 3.20 main/community repositories. V8 is omitted: +# Alpine has no maintained libnode/libv8 development package. +apk_sysreqs <- list( + curl = c("curl-dev", "openssl-dev"), + xml2 = "libxml2-dev", + openssl = "openssl-dev", + ragg = c( + "freetype-dev", "libjpeg-turbo-dev", "libpng-dev", "tiff-dev", + "libwebp-dev" + ), + systemfonts = c("fontconfig-dev", "freetype-dev"), + textshaping = c("freetype-dev", "fribidi-dev", "harfbuzz-dev"), + sf = c("gdal-dev", "gdal-tools", "geos-dev", "proj-dev", "sqlite-dev"), + terra = c("gdal-dev", "gdal-tools", "geos-dev", "proj-dev", "sqlite-dev"), + units = "udunits-dev", + git2r = c("libgit2-dev", "libssh2-dev", "openssl-dev"), + gert = "libgit2-dev", + magick = "imagemagick-dev", + pdftools = c("poppler-data", "poppler-dev"), + tesseract = c("leptonica-dev", "tesseract-ocr-dev"), + RPostgres = "libpq-dev", + RMariaDB = "mariadb-connector-c-dev", + RMySQL = "mariadb-connector-c-dev", + RODBC = "unixodbc-dev", + sodium = "libsodium-dev", + stringi = "icu-dev", + webp = "libwebp-dev", + png = "libpng-dev", + jpeg = "libjpeg-turbo-dev", + tiff = c("libjpeg-turbo-dev", "tiff-dev"), + gsl = "gsl-dev", + RcppGSL = "gsl-dev", + gmp = "gmp-dev", + Rmpfr = c("gmp-dev", "mpfr-dev"), + fftwtools = "fftw-dev", + hdf5r = "hdf5-dev", + ncdf4 = "netcdf-dev", + igraph = c("glpk-dev", "libxml2-dev"), + rJava = "openjdk21-jdk", + jqr = "jq-dev", + odbc = "unixodbc-dev", + av = "ffmpeg-dev", + rsvg = "librsvg-dev", + xslt = "libxslt-dev", + protolite = "protobuf-dev" +) + quote_chr <- function(x) { paste0("\"", gsub("([\"\\\\])", "\\\\\\1", x), "\"") } @@ -62,7 +146,7 @@ download_ppm_sysreqs <- function(base_url, repo, distribution, release) { json_read_file(tmp) } -normalize_ppm_packages <- function(response) { +normalize_ppm_packages <- function(response, package_manager) { items <- response$requirements %||% list() rows <- list() @@ -70,6 +154,7 @@ normalize_ppm_packages <- function(response) { r_package <- item$name %||% NA_character_ req <- item$requirements %||% list() system_packages <- compact_chr(as_chr(req$packages)) + system_packages <- setdiff(system_packages, build_tool_excludes) if (!length(system_packages) || is.na(r_package) || !nzchar(r_package)) { next } @@ -77,6 +162,7 @@ normalize_ppm_packages <- function(response) { for (system_package in system_packages) { rows[[length(rows) + 1L]] <- data.frame( r_package = r_package, + package_manager = package_manager, system_package = system_package, stringsAsFactors = FALSE ) @@ -95,7 +181,19 @@ normalize_ppm_packages <- function(response) { call. = FALSE ) } - out[order(tolower(out$r_package), out$system_package), , drop = FALSE] + out +} + +apk_rows <- function() { + rows <- lapply(names(apk_sysreqs), function(r_package) { + data.frame( + r_package = r_package, + package_manager = "apk", + system_package = apk_sysreqs[[r_package]], + stringsAsFactors = FALSE + ) + }) + do.call(rbind, rows) } bundled_data_lines <- function(data) { @@ -105,6 +203,9 @@ bundled_data_lines <- function(data) { " r_package = c(", format_chr_vector(data$r_package), " ),", + " package_manager = c(", + format_chr_vector(data$package_manager), + " ),", " system_package = c(", format_chr_vector(data$system_package), " ),", @@ -132,29 +233,49 @@ arg_or <- function(args, i, default) { } args <- commandArgs(trailingOnly = TRUE) -distribution <- arg_or(args, 1L, "ubuntu") -release <- arg_or(args, 2L, "22.04") -repo <- arg_or(args, 3L, "cran") -base_url <- arg_or(args, 4L, "https://packagemanager.posit.co") - -response <- download_ppm_sysreqs( - base_url = base_url, - repo = repo, - distribution = distribution, - release = release -) -data <- normalize_ppm_packages(response) +repo <- arg_or(args, 1L, "cran") +base_url <- arg_or(args, 2L, "https://packagemanager.posit.co") + +parts <- lapply(names(ppm_sources), function(pm) { + src <- ppm_sources[[pm]] + response <- download_ppm_sysreqs( + base_url = base_url, + repo = repo, + distribution = src$distribution, + release = src$release + ) + normalize_ppm_packages(response, package_manager = pm) +}) +data <- do.call(rbind, c(parts, list(apk_rows()))) + +idx <- match(data$system_package, names(apt_overrides)) +override <- data$package_manager == "apt" & !is.na(idx) +data$system_package[override] <- unname(apt_overrides[idx[override]]) + +data <- unique(data) +pm_order <- match(data$package_manager, c("apt", "dnf", "zypper", "apk")) +data <- data[order(pm_order, tolower(data$r_package), data$system_package), , drop = FALSE] + +for (pm in c("apt", "dnf", "zypper", "apk")) { + covered <- unique(data$r_package[data$package_manager == pm]) + missing <- setdiff(bundled_packages, covered) + if (length(missing)) { + message( + "No ", pm, " rows for: ", paste(missing, collapse = ", "), + " (bundled_sysreqs() will report these in the plan notes)." + ) + } +} + replace_between_markers( path = file.path("R", "bundled-sysreqs.R"), replacement = bundled_data_lines(data) ) message( - "Updated bundled sysreq data for ", - distribution, - " ", - release, - " with ", + "Updated bundled sysreq data with ", nrow(data), - " rows." + " rows across ", + length(unique(data$package_manager)), + " package managers." ) diff --git a/man/check_packages.Rd b/man/check_packages.Rd index a964386..5cfd6d1 100644 --- a/man/check_packages.Rd +++ b/man/check_packages.Rd @@ -38,8 +38,9 @@ A \code{sysreqr_plan}. \description{ Resolves the system packages an R package needs on a given Linux platform and returns them in a structured plan. The \code{"auto"} backend prefers the -offline bundled database on \code{apt} platforms, then Posit Package Manager, -then \code{pak}. +offline bundled database (which carries names for the \code{apt}, \code{dnf}/\code{yum}, +\code{zypper}, and \code{apk} package managers), then Posit Package Manager, then +\code{pak}. } \examples{ plan <- check_packages(c("xml2", "curl"), platform = "ubuntu-22.04") diff --git a/man/ppm_sysreqs.Rd b/man/ppm_sysreqs.Rd index 69cb9d4..0f80fe8 100644 --- a/man/ppm_sysreqs.Rd +++ b/man/ppm_sysreqs.Rd @@ -34,7 +34,8 @@ A \code{sysreqr_plan}. Queries the Posit Package Manager \verb{/sysreqs} endpoint for the given packages and platform, and normalizes the response into a \code{sysreqr_plan}. If the API call fails, the function falls back to the -bundled database and records the failure in the +bundled database (or to an empty plan when the bundled data has no names +for the platform's package manager) and records the failure in the \code{"fallback_error"} attribute of the returned plan. } \examples{ diff --git a/tests/testthat/test-check.R b/tests/testthat/test-check.R index 2d8092e..9aa1e82 100644 --- a/tests/testthat/test-check.R +++ b/tests/testthat/test-check.R @@ -8,25 +8,25 @@ test_that("auto backend uses bundled data for known packages on apt", { expect_true("libcurl4-openssl-dev" %in% plan$system_package) }) -test_that("auto backend does not pick bundled on non-apt platforms (regression)", { - # Regression test: previously, select_backend() returned "bundled" for any - # apt-keyed package set even on Fedora/Rocky/SUSE, which then hard-errored - # with "Bundled fallback data currently supports apt platforms only." - fedora <- resolve_platform("fedora-40") - expect_false(identical(select_backend("xml2", "auto", fedora), "bundled")) - - rocky <- resolve_platform("rockylinux-9") - expect_false(identical(select_backend("xml2", "auto", rocky), "bundled")) - - ubuntu <- resolve_platform("ubuntu-22.04") - expect_equal(select_backend("xml2", "auto", ubuntu), "bundled") +test_that("auto backend picks bundled for known packages on all bundled managers", { + # Since the bundled table carries per-package-manager names, auto routing + # uses it on apt, dnf, yum, zypper, and apk platforms alike. + expect_equal(select_backend("xml2", "auto", resolve_platform("ubuntu-22.04")), "bundled") + expect_equal(select_backend("xml2", "auto", resolve_platform("fedora-40")), "bundled") + expect_equal(select_backend("xml2", "auto", resolve_platform("rockylinux-9")), "bundled") + expect_equal(select_backend("xml2", "auto", resolve_platform("centos7")), "bundled") + expect_equal(select_backend("xml2", "auto", resolve_platform("opensuse156")), "bundled") + expect_equal(select_backend("xml2", "auto", resolve_platform("alpine-3.20")), "bundled") }) -test_that("select_backend routes a known package to ppm on non-apt platforms", { - # Positive assertion (not just "not bundled"): a simple, known package on a - # non-apt platform must route to ppm. - expect_equal(select_backend("xml2", "auto", resolve_platform("fedora-40")), "ppm") - expect_equal(select_backend("xml2", "auto", resolve_platform("rockylinux-9")), "ppm") +test_that("select_backend routes around bundled when it cannot help", { + # Unknown packages route to ppm even when the platform is bundled-capable. + expect_equal( + select_backend("definitelynotbundled", "auto", resolve_platform("fedora-40")), + "ppm" + ) + # brew has no bundled name set, so known packages route to ppm there. + expect_equal(select_backend("xml2", "auto", resolve_platform("macos-14")), "ppm") }) test_that("bundled database uses cross-distro-portable package names", { @@ -44,36 +44,27 @@ test_that("bundled database uses cross-distro-portable package names", { expect_false("libfreetype6-dev" %in% ragg$system_package) }) -test_that("check_packages on Fedora falls through without the apt-only error", { - # When auto routing previously chose bundled on any platform with known - # packages, this call errored with "Bundled fallback data currently supports - # apt platforms only." Mock pak so the test does not depend on a real pak - # install and pin the route to ppm with a mock that returns a Fedora plan. - fedora_ppm_mock <- function(endpoint, query, base_url) { - if (identical(endpoint, "status")) { - return(list( - version = "test", - distros = list(list( - os = "linux", distribution = "fedora", release = "40", - name = "fedora-40", binaryURL = "fedora-40", - sysReqs = TRUE, binaries = TRUE - )) - )) - } - list(requirements = list(list( - name = "xml2", - requirements = list( - packages = list("libxml2-devel"), - install_scripts = list("dnf install -y libxml2-devel") - ) - ))) - } +test_that("check_packages on Fedora resolves dnf names without error", { + withr::local_options(sysreqr.installed_system_packages = character()) + + expect_no_error( + plan <- check_packages("xml2", platform = "fedora-40", backend = "auto") + ) + expect_s3_class(plan, "sysreqr_plan") + expect_equal(attr(plan, "backend"), "bundled") + expect_true("libxml2-devel" %in% plan$system_package) + expect_false("libxml2-dev" %in% plan$system_package) +}) +test_that("auto routing on Fedora falls through to pak for unknown packages", { + # Packages outside the bundled table route to ppm; Package Manager has no + # Fedora support, so auto routing must continue to the mocked pak backend + # instead of erroring. pak_mock <- function(pkg, ...) { list( packages = data.frame( sysreq = "libxml2", - packages = I(list("xml2")), + packages = I(list("definitelynotbundled")), system_packages = I(list("libxml2-devel")), pre_install = I(list(character())), post_install = I(list(character())), @@ -85,16 +76,15 @@ test_that("check_packages on Fedora falls through without the apt-only error", { } withr::local_options( - sysreqr.ppm_get = fedora_ppm_mock, sysreqr.pak_pkg_sysreqs = pak_mock, sysreqr.installed_system_packages = character() ) expect_no_error( - plan <- check_packages("xml2", platform = "fedora-40", backend = "auto") + plan <- check_packages("definitelynotbundled", platform = "fedora-40", backend = "auto") ) expect_s3_class(plan, "sysreqr_plan") - expect_false(identical(attr(plan, "backend"), "bundled")) + expect_equal(attr(plan, "backend"), "pak") }) test_that("bundled database covers the expanded package set", { @@ -112,6 +102,65 @@ test_that("bundled database covers the expanded package set", { expect_true("default-jdk" %in% rjava$system_package) }) +test_that("bundled backend resolves per-package-manager names", { + withr::local_options(sysreqr.installed_system_packages = character()) + + fedora <- check_packages("xml2", platform = "fedora-40", backend = "bundled") + expect_equal(fedora$system_package, "libxml2-devel") + expect_equal(fedora$package_manager, "dnf") + expect_match(fedora$notes, "verify the exact name", fixed = TRUE) + + centos <- check_packages("RPostgres", platform = "centos7", backend = "bundled") + expect_equal(centos$system_package, "libpq-devel") + expect_equal(centos$package_manager, "yum") + expect_match(centos$install_script, "^yum install") + + suse <- check_packages("curl", platform = "opensuse156", backend = "bundled") + expect_true(all(c("libcurl-devel", "libopenssl-devel") %in% suse$system_package)) + + alpine <- check_packages("sf", platform = "alpine-3.20", backend = "bundled") + expect_true(all( + c("gdal-dev", "geos-dev", "proj-dev", "sqlite-dev") %in% alpine$system_package + )) + + ubuntu <- check_packages("xml2", platform = "ubuntu-22.04", backend = "bundled") + expect_false(any(grepl("verify the exact name", ubuntu$notes, fixed = TRUE))) +}) + +test_that("bundled backend reports packages with no name set for the platform", { + withr::local_options(sysreqr.installed_system_packages = character()) + + plan <- check_packages("V8", platform = "opensuse156", backend = "bundled") + expect_equal(nrow(plan), 0L) + expect_true("V8" %in% attr(plan, "unresolved")) + + apt <- check_packages("V8", platform = "ubuntu-22.04", backend = "bundled") + expect_true("libnode-dev" %in% apt$system_package) +}) + +test_that("bundled backend still rejects unsupported package managers", { + platform <- resolve_platform("ubuntu-22.04") + platform$package_manager <- "brew" + expect_error( + check_packages("xml2", platform = platform, backend = "bundled"), + "apt, dnf, yum, zypper" + ) +}) + +test_that("bundled database has well-formed cross-distro rows", { + db <- getFromNamespace("bundled_sysreqs_db", "sysreqr") + + expect_setequal(unique(db$package_manager), c("apt", "dnf", "zypper", "apk")) + expect_false(any(is.na(db$system_package))) + expect_true(all(grepl("^[A-Za-z0-9][A-Za-z0-9._+:@-]*$", db$system_package))) + + # Every curated package must at least have apt names; the apt set is the + # baseline the package has shipped since 0.1.0. + known <- unique(db$r_package) + apt_known <- unique(db$r_package[db$package_manager == "apt"]) + expect_setequal(known, apt_known) +}) + test_that("check_packages errors when given no packages", { expect_error(check_packages(character(), platform = "ubuntu-22.04"), "at least one package") diff --git a/tests/testthat/test-diagnose.R b/tests/testthat/test-diagnose.R index c36b5db..a88580a 100644 --- a/tests/testthat/test-diagnose.R +++ b/tests/testthat/test-diagnose.R @@ -176,3 +176,60 @@ test_that("diagnose_failed_packages handles an empty package vector", { expect_s3_class(plan, "sysreqr_plan") expect_equal(nrow(plan), 0L) }) + +test_that("expanded log patterns map common libraries per package manager", { + cases <- list( + list("fatal error: png.h: No such file or directory", "ubuntu-22.04", "libpng-dev"), + list("fatal error: png.h: No such file or directory", "fedora-40", "libpng-devel"), + list("/usr/bin/ld: cannot find -lpq", "ubuntu-22.04", "libpq-dev"), + list("/usr/bin/ld: cannot find -lpq", "fedora-40", "libpq-devel"), + list("fatal error: sqlite3.h: No such file or directory", "opensuse156", "sqlite3-devel"), + list("fatal error: Magick++.h: No such file or directory", "ubuntu-22.04", "libmagick++-dev"), + list("fatal error: ft2build.h: No such file or directory", "alpine-3.20", "freetype-dev"), + list("fatal error: unicode/ucnv.h: No such file or directory", "fedora-40", "libicu-devel"), + list("/usr/bin/ld: cannot find -lz", "ubuntu-22.04", "zlib1g-dev"), + list("fatal error: cairo.h: No such file or directory", "ubuntu-22.04", "libcairo2-dev"), + list( + "fatal error: mysql.h: No such file or directory", + "ubuntu-22.04", "default-libmysqlclient-dev" + ), + list( + "fatal error: tesseract/baseapi.h: No such file or directory", + "fedora-40", "tesseract-devel" + ) + ) + for (case in cases) { + plan <- diagnose_log(text = case[[1]], platform = case[[2]], check_installed = FALSE) + expect_true( + case[[3]] %in% plan$system_package, + label = paste0(case[[2]], " / ", case[[1]], " -> ", case[[3]]) + ) + } +}) + +test_that("linker patterns do not over-match similar library names", { + # -lzstd must not trigger the zlib rule, -lgmpxx must not trigger gmp. + zstd <- diagnose_log( + text = "/usr/bin/ld: cannot find -lzstd", + platform = "ubuntu-22.04", + check_installed = FALSE + ) + expect_false("zlib1g-dev" %in% zstd$system_package) + + gmpxx <- diagnose_log( + text = "/usr/bin/ld: cannot find -lgmpxx", + platform = "ubuntu-22.04", + check_installed = FALSE + ) + expect_false("libgmp3-dev" %in% gmpxx$system_package) +}) + +test_that("a header and linker hit for the same library produce one suggestion", { + text <- paste( + "fatal error: png.h: No such file or directory", + "/usr/bin/ld: cannot find -lpng", + sep = "\n" + ) + plan <- diagnose_log(text = text, platform = "ubuntu-22.04", check_installed = FALSE) + expect_equal(sum(plan$system_package == "libpng-dev"), 1L) +}) diff --git a/tests/testthat/test-plan.R b/tests/testthat/test-plan.R index e542200..0ab5d6b 100644 --- a/tests/testthat/test-plan.R +++ b/tests/testthat/test-plan.R @@ -32,3 +32,22 @@ test_that("print.sysreqr_plan emits the expected sections", { expect_match(out, "Backend:", fixed = TRUE) expect_match(out, "libxml2-dev", fixed = TRUE) }) + +test_that("list_installed_system_packages has an apk branch", { + list_installed <- getFromNamespace("list_installed_system_packages", "sysreqr") + + skip_if(Sys.which("apk") == "", "apk is not available on this host") + installed <- list_installed(resolve_platform("alpine-3.20")) + expect_type(installed, "character") +}) + +test_that("startup_platform_example returns a usable platform key", { + helper <- getFromNamespace("startup_platform_example", "sysreqr") + example <- helper() + expect_type(example, "character") + expect_length(example, 1L) + expect_true(nzchar(example)) + # Whatever the host, the example must resolve so the startup suggestion + # never points users at an invalid platform string. + expect_s3_class(resolve_platform(example), "sysreqr_platform") +}) diff --git a/tests/testthat/test-ppm.R b/tests/testthat/test-ppm.R index 37379c0..6dc12bd 100644 --- a/tests/testthat/test-ppm.R +++ b/tests/testthat/test-ppm.R @@ -86,6 +86,45 @@ test_that("ppm_sysreqs falls back to bundled data on API error", { expect_match(attr(plan, "fallback_error"), "simulated PPM outage") }) +test_that("ppm_sysreqs API-error fallback uses platform-matching bundled names", { + failing_mock <- function(endpoint, query, base_url) { + stop("simulated PPM outage", call. = FALSE) + } + withr::local_options( + sysreqr.ppm_get = failing_mock, + sysreqr.installed_system_packages = character() + ) + + suse <- ppm_sysreqs("xml2", platform = "opensuse156") + expect_true("libxml2-devel" %in% suse$system_package) + expect_match(attr(suse, "fallback_error"), "simulated PPM outage") + + rocky <- ppm_sysreqs("xml2", platform = "rockylinux-9") + expect_true("libxml2-devel" %in% rocky$system_package) + expect_match(rocky$install_script, "^dnf install") +}) + +test_that("ppm_sysreqs API-error fallback returns an empty plan on brew", { + # Regression test: this previously hard-errored with "Bundled fallback data + # currently supports apt platforms only." instead of reporting the original + # Package Manager failure. + failing_mock <- function(endpoint, query, base_url) { + stop("simulated PPM outage", call. = FALSE) + } + withr::local_options( + sysreqr.ppm_get = failing_mock, + sysreqr.installed_system_packages = character() + ) + platform <- resolve_platform("ubuntu-22.04") + platform$package_manager <- "brew" + + plan <- ppm_sysreqs("xml2", platform = platform) + expect_s3_class(plan, "sysreqr_plan") + expect_equal(nrow(plan), 0L) + expect_equal(attr(plan, "unresolved"), "xml2") + expect_match(attr(plan, "fallback_error"), "simulated PPM outage") +}) + test_that("use_ppm dry_run returns repo configuration lines", { lines <- use_ppm("user", platform = "ubuntu-22.04", dry_run = TRUE) expect_true(any(grepl("packagemanager.posit.co", lines, fixed = TRUE))) diff --git a/vignettes/faq.Rmd b/vignettes/faq.Rmd index 70579b5..3411e0a 100644 --- a/vignettes/faq.Rmd +++ b/vignettes/faq.Rmd @@ -57,7 +57,7 @@ that in one place. | `pak::pkg_sysreqs()` | Authoritative live resolver | Requires `pak`; no log diagnosis | | `remotes::system_requirements()` | Light; widely available | No log diagnosis, no project scanner | | `renv::sysreqs()` | Project-oriented; integrates with `renv` workflow | Requires `renv` | -| `sysreqr` | Zero runtime deps; log diagnosis; beginner UX | Bundled DB is small; biased to `apt` | +| `sysreqr` | Zero runtime deps; log diagnosis; beginner UX | Bundled DB covers ~40 curated packages | `sysreqr` can use `pak` as one of its backends (`backend = "pak"`) when it is installed. The two tools are complementary, not competitors. @@ -126,9 +126,11 @@ openSUSE, the equivalent is `R-base`. ## I'm on Alpine. What's supported? Alpine uses `apk` and is detected as `alpine`. Install commands use -`apk add --no-cache`. The bundled fallback database currently uses -Debian/Ubuntu names, so prefer the `ppm` or `pak` backend on Alpine. Many -common system libraries on Alpine are in the +`apk add --no-cache`. The bundled fallback database carries Alpine package +names for its curated set, and log diagnosis maps error patterns to `apk` +names. Posit Package Manager does not cover Alpine, so for packages outside +the bundled set use the `pak` backend. Many common system libraries on +Alpine are in the [community repository](https://pkgs.alpinelinux.org/packages?branch=v3.20&repo=community). ## I'm behind a corporate proxy. PPM queries fail diff --git a/vignettes/preflight-setup.Rmd b/vignettes/preflight-setup.Rmd index 05872e0..d43f534 100644 --- a/vignettes/preflight-setup.Rmd +++ b/vignettes/preflight-setup.Rmd @@ -182,9 +182,11 @@ explain(plan) `check_packages()` accepts four backend modes. * `backend = "auto"` uses bundled data for simple, known CRAN packages on - `apt` platforms, then Package Manager, then `pak` when possible. + `apt`, `dnf`/`yum`, `zypper`, and `apk` platforms, then Package Manager, + then `pak` when possible. * `backend = "bundled"` uses only the static database shipped with the - installed `sysreqr` release. Currently optimized for `apt`. + installed `sysreqr` release. It carries package names for `apt`, + `dnf`/`yum`, `zypper`, and `apk`. * `backend = "ppm"` uses the Posit Package Manager API when network access is available. * `backend = "pak"` uses `pak::pkg_sysreqs()` when `pak` is installed.