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.