diff --git a/.Rbuildignore b/.Rbuildignore index d52a90bfee..77f2adccbe 100644 --- a/.Rbuildignore +++ b/.Rbuildignore @@ -44,6 +44,8 @@ ^src/library/curl/tools/option_table[.]txt$ ^src/library/jsonlite/src/yajl/libstatyajl[.]a$ ^src/library/pkgdepends/man/macros/eval2[.]Rd$ +^src/library/ts/man/macros/eval2[.]Rd$ +^src/library/tstoml/man/macros/eval2[.]Rd$ ^src/library/processx/src/supervisor/supervisor$ ^src/library/processx/src/tools/px$ ^src/library/processx/src/tools/sock$ diff --git a/.github/workflows/R-CMD-check.yaml b/.github/workflows/R-CMD-check.yaml index 55dd03e4cc..597a0cc30a 100644 --- a/.github/workflows/R-CMD-check.yaml +++ b/.github/workflows/R-CMD-check.yaml @@ -42,7 +42,7 @@ jobs: R_KEEP_PKG_SOURCE: yes steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - uses: r-lib/actions/setup-pandoc@v2 diff --git a/.github/workflows/linux-builder-containers.yaml b/.github/workflows/linux-builder-containers.yaml index 9f8bad1a08..ced0a740dd 100644 --- a/.github/workflows/linux-builder-containers.yaml +++ b/.github/workflows/linux-builder-containers.yaml @@ -30,10 +30,10 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Login to GitHub Container Registry - uses: docker/login-action@v3 + uses: docker/login-action@v4 with: registry: ghcr.io username: ${{ github.repository_owner }} @@ -69,10 +69,10 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Login to GitHub Container Registry - uses: docker/login-action@v3 + uses: docker/login-action@v4 with: registry: ghcr.io username: ${{ github.repository_owner }} diff --git a/.github/workflows/nightly.yaml b/.github/workflows/nightly.yaml index 5e4ec4a7cc..656820fb11 100644 --- a/.github/workflows/nightly.yaml +++ b/.github/workflows/nightly.yaml @@ -98,7 +98,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v6 with: fetch-depth: 10 @@ -155,7 +155,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v6 with: fetch-depth: 10 @@ -220,7 +220,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v6 with: fetch-depth: 10 @@ -291,7 +291,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v6 with: fetch-depth: 10 @@ -325,13 +325,13 @@ jobs: steps: - name: Set up QEMU - uses: docker/setup-qemu-action@v3 + uses: docker/setup-qemu-action@v4 - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 + uses: docker/setup-buildx-action@v4 - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v6 with: fetch-depth: 10 @@ -364,7 +364,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v6 with: fetch-depth: 10 @@ -412,7 +412,7 @@ jobs: sudo apt-get update - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v6 with: fetch-depth: 10 @@ -439,7 +439,7 @@ jobs: - name: Deploy to GitHub pages (test) if: github.event_name != 'pull_request' - uses: JamesIves/github-pages-deploy-action@v4.5.0 + uses: JamesIves/github-pages-deploy-action@v4.8.0 with: repository-name: r-lib/r-lib.github.io token: ${{ secrets.PAK_GHCR_TOKEN }} @@ -457,7 +457,7 @@ jobs: - name: Deploy to GitHub Pages (prod) if: github.event_name != 'pull_request' - uses: JamesIves/github-pages-deploy-action@v4.5.0 + uses: JamesIves/github-pages-deploy-action@v4.8.0 with: repository-name: r-lib/r-lib.github.io token: ${{ secrets.PAK_GHCR_TOKEN }} diff --git a/.github/workflows/pkgdown.yaml b/.github/workflows/pkgdown.yaml index f25e128acb..8267c25c50 100644 --- a/.github/workflows/pkgdown.yaml +++ b/.github/workflows/pkgdown.yaml @@ -23,7 +23,7 @@ jobs: permissions: contents: write steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - uses: r-lib/actions/setup-pandoc@v2 @@ -54,7 +54,7 @@ jobs: - name: Deploy to GitHub pages 🚀 if: github.event_name != 'pull_request' - uses: JamesIves/github-pages-deploy-action@v4.5.0 + uses: JamesIves/github-pages-deploy-action@v4.8.0 with: clean: false branch: gh-pages diff --git a/.github/workflows/pr-commands.yaml b/.github/workflows/pr-commands.yaml index 2edd93f27e..7b9913ea6f 100644 --- a/.github/workflows/pr-commands.yaml +++ b/.github/workflows/pr-commands.yaml @@ -18,7 +18,7 @@ jobs: permissions: contents: write steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - uses: r-lib/actions/pr-fetch@v2 with: @@ -57,7 +57,7 @@ jobs: permissions: contents: write steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - uses: r-lib/actions/pr-fetch@v2 with: diff --git a/.github/workflows/test-coverage.yaml b/.github/workflows/test-coverage.yaml deleted file mode 100644 index 0ab748d657..0000000000 --- a/.github/workflows/test-coverage.yaml +++ /dev/null @@ -1,62 +0,0 @@ -# Workflow derived from https://github.com/r-lib/actions/tree/v2/examples -# Need help debugging build failures? Start at https://github.com/r-lib/actions#where-to-find-help -on: - push: - branches: [main, master] - pull_request: - -name: test-coverage.yaml - -permissions: read-all - -jobs: - test-coverage: - runs-on: ubuntu-latest - env: - GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }} - - steps: - - uses: actions/checkout@v4 - - - uses: r-lib/actions/setup-r@v2 - with: - use-public-rspm: true - - - uses: r-lib/actions/setup-r-dependencies@v2 - with: - extra-packages: any::covr, any::xml2 - needs: coverage - - - name: Test coverage - run: | - cov <- covr::package_coverage( - quiet = FALSE, - clean = FALSE, - install_path = file.path(normalizePath(Sys.getenv("RUNNER_TEMP"), winslash = "/"), "package") - ) - print(cov) - covr::to_cobertura(cov) - shell: Rscript {0} - - - uses: codecov/codecov-action@v5 - with: - # Fail if error if not on PR, or if on PR and token is given - fail_ci_if_error: ${{ github.event_name != 'pull_request' || secrets.CODECOV_TOKEN }} - files: ./cobertura.xml - plugins: noop - disable_search: true - token: ${{ secrets.CODECOV_TOKEN }} - - - name: Show testthat output - if: always() - run: | - ## -------------------------------------------------------------------- - find '${{ runner.temp }}/package' -name 'testthat.Rout*' -exec cat '{}' \; || true - shell: bash - - - name: Upload test results - if: failure() - uses: actions/upload-artifact@v4 - with: - name: coverage-test-failures - path: ${{ runner.temp }}/package diff --git a/.gitignore b/.gitignore index 7d2bc2a0ab..158cac957d 100644 --- a/.gitignore +++ b/.gitignore @@ -31,6 +31,8 @@ /src/library/curl/tools/option_table.txt /src/library/jsonlite/src/yajl/libstatyajl.a /src/library/pkgdepends/man/macros/eval2.Rd +/src/library/ts/man/macros/eval2.Rd +/src/library/tstoml/man/macros/eval2.Rd /src/library/processx/src/supervisor/supervisor /src/library/processx/src/supervisor/supervisor.dSYM/ /src/library/processx/src/tools/px diff --git a/DESCRIPTION b/DESCRIPTION index 8d4d6468d5..4db7c2fe36 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -113,11 +113,13 @@ Config/needs/dependencies: keyring, lpSolve, pkgbuild, - pkgcache, - pkgdepends, + r-lib/pkgcache@feature/ppm-sso, + r-lib/pkgdepends, pkgsearch, processx, ps, + r-lib/ts, + gaborcsardi/tstoml, yaml Config/Needs/website: r-lib/asciicast, diff --git a/NAMESPACE b/NAMESPACE index 289acb58a1..c825febc75 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -58,6 +58,9 @@ export(ppm_platforms) export(ppm_r_versions) export(ppm_repo_url) export(ppm_snapshots) +export(ppm_sso_login) +export(ppm_sso_logout) +export(ppm_sso_status) export(repo_add) export(repo_auth) export(repo_auth_key_get) diff --git a/R/auth.R b/R/auth.R index f9e1771b5e..59b045158c 100644 --- a/R/auth.R +++ b/R/auth.R @@ -9,6 +9,8 @@ #' #' ```{r child = "man/chunks/auth.Rmd"} #' ``` +#' +#' @seealso [repo_auth()], [ppm_sso_login()]. NULL #' Query or set repository password in the system credential store diff --git a/R/embed.R b/R/embed.R index f446c9861c..f42b516c4e 100644 --- a/R/embed.R +++ b/R/embed.R @@ -225,6 +225,15 @@ embed <- local({ ) lib <- lib_dir() + + if (grepl("@", pkg)) { + pkgsplit <- strsplit(pkg, "@")[[1]] + pkg <- pkgsplit[1] + ref <- pkgsplit[2] + } else { + ref <- "main" + } + pkg_name <- sub("^.*/", "", pkg) if (mode == "add") { if (file.exists(file.path(lib, pkg_name))) { @@ -236,8 +245,9 @@ embed <- local({ on.exit(unlink(tmp, recursive = TRUE), add = TRUE) if (grepl("/", pkg)) { url <- sprintf( - "https://github.com/%s/archive/refs/heads/main.tar.gz", - pkg + "https://github.com/%s/archive/refs/heads/%s.tar.gz", + pkg, + ref ) path1 <- file.path(tmp, paste0(pkg_name, ".tar.gz")) download.file(url, path1) @@ -303,6 +313,7 @@ embed <- local({ rimraf(file.path(lib, pkg, "inst", "CITATION")) rimraf(file.path(lib, pkg, "MD5")) rimraf(file.path(lib, pkg, "README.md")) + rimraf(file.path(lib, pkg, "inst", "tsdocs")) } } diff --git a/R/onload.R b/R/onload.R index e3316cab4e..54c92a0c57 100644 --- a/R/onload.R +++ b/R/onload.R @@ -19,6 +19,7 @@ pkg_data <- new.env(parent = emptyenv()) # We don't use the env vars that cli supports, on purpose, because # they are inherited in the subprocess of the subprocess options( + pak.is_worker = TRUE, cli.num_colors = as.numeric(Sys.getenv("R_PKG_NUM_COLORS", "1")), rlib_interactive = (Sys.getenv("R_PKG_INTERACTIVE") == "TRUE"), cli.dynamic = (Sys.getenv("R_PKG_DYNAMIC_TTY") == "TRUE") diff --git a/R/ppm-sso.R b/R/ppm-sso.R new file mode 100644 index 0000000000..829ace45fc --- /dev/null +++ b/R/ppm-sso.R @@ -0,0 +1,114 @@ +#' Posit Package Manager single sign-on (SSO) authentication +#' +#' @details +#' ## Set up SSO authentication: +#' - Set the `PACKAGEMANAGER_ADDRESS` environment variable to the URL of +#' your RStudio Package Manager instance. For example, add this line to +#' your `.Renviron` file: +#' ``` +#' PACKAGEMANAGER_ADDRESS=https:// +#' ``` +#' Alternatively, you can also set it in your shell profile on Unix, +#' or in the System or User environment variables on Windows. +#' - Set `options(repos)` to include a repository from your Package Manager +#' instance. Include `__token__` as the username in the URL. For example: +#' ``` +#' options(repos = c( +#' PPM = "https://__token__@/", +#' getOption("repos") +#' )) +#' ``` +#' You probably want to add this to your `.Rprofile` file, so that it is +#' set in every R session. +#' - Call [repo_get()] to trigger authentication and caching of the token. +#' You should be prompted to log in via your browser, and the obtained +#' token will be cached for future use. Call `ppm_sso_status()` to check +#' the status of your authentication, including the path of the cached +#' token and its expiration time. +#' - Alternatively, you can call `ppm_sso_login()` directly to trigger +#' the login process directly. +#' +#' `ppm_sso_login()` initiates the SSO login process. You should be +#' prompted to log in via your browser, and the obtained token will be +#' cached for future use. +#' +#' @return `ppm_sso_login()` returns the obtained token invisibly. +#' +#' @seealso [Authenticated repositories], +#' +#' @export +#' @examplesIf FALSE +#' Sys.setenv(PACKAGEMANAGER_ADDRESS = "https://") +#' options(repos = c( +#' PPM = "https://__token__@/", +#' getOption("repos") +#' )) +#' ppm_sso_login() +#' ppm_sso_status() +#' ppm_sso_status(connect = TRUE) +#' ppm_sso_logout() + +ppm_sso_login <- function() { + res <- remote( + function() { + asNamespace("pkgcache")$ppm_sso_login() + }, + list() + ) + invisible(res) +} + +#' @rdname ppm_sso_login +#' @details +#' `ppm_sso_logout()` removes the cached token, effectively logging you +#' out. If there is no cached token, it does nothing. +#' @return `ppm_sso_logout()` does not return anything. +#' @export + +ppm_sso_logout <- function() { + res <- remote( + function() { + asNamespace("pkgcache")$ppm_sso_logout() + }, + list() + ) + invisible(res) +} + +#' @rdname ppm_sso_login +#' @param connect If `TRUE`, also checks if the token is valid by making a test +#' request to the Package Manager instance. This requires an active internet +#' connection and may take a few seconds. If `FALSE`, only checks if a +#' token is cached and not expired. +#' @details +#' `ppm_sso_status()` checks the status of your authentication, including +#' the path of the cached token and its expiration time. +#' @return `ppm_sso_status()` returns a list with the following components: +#' - `ppm_url`: The URL of the Package Manager instance. +#' - `token_file`: The path of the cached token file. +#' - `token`: The cached token (partially masked for display) or `NA` if +#' no token is found locally. +#' - `valid`: `TRUE` if the token is valid (only if `connect = TRUE`), +#' `FALSE` if invalid, or `NA` if not checked. +#' - `issuer`: The issuer of the token, or `NA` if not available. +#' - `subject`: The subject of the token, or `NA` if not available. +#' - `audience`: The audience of the token, or `NA` if not available. +#' - `issued_at`: The issue time of the token as a POSIXct object, or `NA` +#' if not available. +#' - `expires_at`: The expiration time of the token as a POSIXct object, +#' or `NA` if not available. +#' - `expired`: `TRUE` if the token is expired, `FALSE` if not expired, +#' or `NA` if expiration time is not available. +#' - `expires_in`: The time until expiration as a difftime object, or +#' `NA` if expiration time is not available or the token is already +#' expired. +#' @export +ppm_sso_status <- function(connect = FALSE) { + remote( + function(connect) { + ret <- asNamespace("pkgcache")$ppm_sso_status(connect) + asNamespace("pak")$pak_preformat(ret) + }, + list(connect) + ) +} diff --git a/R/subprocess.R b/R/subprocess.R index 6cfe161695..f2e9e70b89 100644 --- a/R/subprocess.R +++ b/R/subprocess.R @@ -74,10 +74,12 @@ remote <- function(func, args = list()) { opts <- options() extraopts <- c("Ncpus", "BioC_mirror") pkg_options <- opts[ - grepl("^pkg[.]", names(opts)) | grepl("^async_http_", names(opts)) | names(opts) %in% extraopts + grepl("^pkg[.]", names(opts)) | + grepl("^async_http_", names(opts)) | + names(opts) %in% extraopts ] envs <- Sys.getenv() - extraenvs <- c("R_BIOC_VERSION", "PATH") + extraenvs <- c("R_BIOC_VERSION", "PATH", "PACKAGEMANAGER_ADDRESS") if (any(grepl("@", subst_args[["__repos__"]]))) { extraenvs <- c(extraenvs, envs[grep("^https?://", names(envs))]) } diff --git a/_pkgdown.yml b/_pkgdown.yml index 281d2d15f3..ce342445de 100644 --- a/_pkgdown.yml +++ b/_pkgdown.yml @@ -93,6 +93,9 @@ reference: - ppm_r_versions - ppm_repo_url - ppm_snapshots + - ppm_sso_login + - ppm_sso_logout + - ppm_sso_status - repo_add - repo_auth - repo_auth_key_get diff --git a/inst/COPYRIGHTS b/inst/COPYRIGHTS index 961c8a1b27..806b67403c 100644 --- a/inst/COPYRIGHTS +++ b/inst/COPYRIGHTS @@ -42,6 +42,18 @@ pak (c) 2017-2024 Posit Software, PBC (formerly RStudio) (c) Giampaolo Rodola (c) Posit Software, PBC (formerly RStudio) +# ts + +(c) Gábor Csárdi +(c) Tree-sitter authors +(c) Posit Software, PBC (formerly RStudio) + +# tstoml + +(c) Gábor Csárdi +(c) Tree-sitter authors +(c) Posit Software, PBC (formerly RStudio) + ## yaml (c) Shawn P Garbett diff --git a/man/chunks/auth.Rmd b/man/chunks/auth.Rmd index e643ad776a..2abdee9d39 100644 --- a/man/chunks/auth.Rmd +++ b/man/chunks/auth.Rmd @@ -1,6 +1,17 @@ ## Configuring authenticated repositories +\if{text}{ + Note: the configuration of PPM SSO authentication is described in the + Posit Package Manager SSO authentication section below. +} + +
+Note: the configuration of PPM SSO authentication is described in the +Posit Package Manager SSO authentication section +below. +
+ To use authentication you need to include a user name in the repository URL. You can set the repository URL in the `repos` option with [base::options()] as usual, or you can use [repo_add()]. @@ -224,3 +235,40 @@ meta_list() E.g. here `meta_update()` outputs an authentication message, but `meta_list()` does not. + + +## Posit Package Manager SSO authentication + +1. To set up PPM SSO authentication, set the `PACKAGEMANAGER_ADDRESS` +environment variable to the URL of your RStudio Package Manager instance. +For example, add this line to your `.Renviron` file: +``` +PACKAGEMANAGER_ADDRESS=https:// +``` + +Alternatively, you can also set it in your shell profile on Unix, +or in the System or User environment variables on Windows. + +2. Set `options(repos)` to include a repository from your Package Manager +instance. Include `__token__` as the username in the URL. For example: +``` +options(repos = c( + PPM = "https://__token__@/", + getOption("repos") +)) +``` +You probably want to add this to your `.Rprofile` file, so that it is set +in every R session. + +3. Call [repo_get()] to trigger authentication and caching of the token. +You should be prompted to log in via your browser, and the obtained +token will be cached for future use. Call `ppm_sso_status()` to check +the status of your authentication, including the path of the cached +token and its expiration time. + +Alternatively, you can call `ppm_sso_login()` directly to trigger +the login process directly. + +See also the [Authentication chapter]( + https://docs.posit.co/rspm/admin/authentication/) of the +Posit Package Manager Documentation. diff --git a/man/ppm_sso_login.Rd b/man/ppm_sso_login.Rd new file mode 100644 index 0000000000..c40b573eb7 --- /dev/null +++ b/man/ppm_sso_login.Rd @@ -0,0 +1,110 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/ppm-sso.R +\name{ppm_sso_login} +\alias{ppm_sso_login} +\alias{ppm_sso_logout} +\alias{ppm_sso_status} +\title{Posit Package Manager single sign-on (SSO) authentication} +\usage{ +ppm_sso_login() + +ppm_sso_logout() + +ppm_sso_status(connect = FALSE) +} +\arguments{ +\item{connect}{If \code{TRUE}, also checks if the token is valid by making a test +request to the Package Manager instance. This requires an active internet +connection and may take a few seconds. If \code{FALSE}, only checks if a +token is cached and not expired.} +} +\value{ +\code{ppm_sso_login()} returns the obtained token invisibly. + +\code{ppm_sso_logout()} does not return anything. + +\code{ppm_sso_status()} returns a list with the following components: +\itemize{ +\item \code{ppm_url}: The URL of the Package Manager instance. +\item \code{token_file}: The path of the cached token file. +\item \code{token}: The cached token (partially masked for display) or \code{NA} if +no token is found locally. +\item \code{valid}: \code{TRUE} if the token is valid (only if \code{connect = TRUE}), +\code{FALSE} if invalid, or \code{NA} if not checked. +\item \code{issuer}: The issuer of the token, or \code{NA} if not available. +\item \code{subject}: The subject of the token, or \code{NA} if not available. +\item \code{audience}: The audience of the token, or \code{NA} if not available. +\item \code{issued_at}: The issue time of the token as a POSIXct object, or \code{NA} +if not available. +\item \code{expires_at}: The expiration time of the token as a POSIXct object, +or \code{NA} if not available. +\item \code{expired}: \code{TRUE} if the token is expired, \code{FALSE} if not expired, +or \code{NA} if expiration time is not available. +\item \code{expires_in}: The time until expiration as a difftime object, or +\code{NA} if expiration time is not available or the token is already +expired. +} +} +\description{ +Posit Package Manager single sign-on (SSO) authentication +} +\details{ +\subsection{Set up SSO authentication:}{ +\itemize{ +\item Set the \code{PACKAGEMANAGER_ADDRESS} environment variable to the URL of +your RStudio Package Manager instance. For example, add this line to +your \code{.Renviron} file: + +\if{html}{\out{
}}\preformatted{PACKAGEMANAGER_ADDRESS=https:// +}\if{html}{\out{
}} + +Alternatively, you can also set it in your shell profile on Unix, +or in the System or User environment variables on Windows. +\item Set \code{options(repos)} to include a repository from your Package Manager +instance. Include \verb{__token__} as the username in the URL. For example: + +\if{html}{\out{
}}\preformatted{options(repos = c( + PPM = "https://__token__@/", + getOption("repos") +)) +}\if{html}{\out{
}} + +You probably want to add this to your \code{.Rprofile} file, so that it is +set in every R session. +\item Call \code{\link[=repo_get]{repo_get()}} to trigger authentication and caching of the token. +You should be prompted to log in via your browser, and the obtained +token will be cached for future use. Call \code{ppm_sso_status()} to check +the status of your authentication, including the path of the cached +token and its expiration time. +\item Alternatively, you can call \code{ppm_sso_login()} directly to trigger +the login process directly. +} + +\code{ppm_sso_login()} initiates the SSO login process. You should be +prompted to log in via your browser, and the obtained token will be +cached for future use. +} + +\code{ppm_sso_logout()} removes the cached token, effectively logging you +out. If there is no cached token, it does nothing. + +\code{ppm_sso_status()} checks the status of your authentication, including +the path of the cached token and its expiration time. +} +\examples{ +\dontshow{if (FALSE) withAutoprint(\{ # examplesIf} +Sys.setenv(PACKAGEMANAGER_ADDRESS = "https://") +options(repos = c( + PPM = "https://__token__@/", + getOption("repos") +)) +ppm_sso_login() +ppm_sso_status() +ppm_sso_status(connect = TRUE) +ppm_sso_logout() +\dontshow{\}) # examplesIf} +} +\seealso{ +\link{Authenticated repositories}, +\url{https://docs.posit.co/rspm/admin/authentication/} +} diff --git a/man/repo-auth.Rd b/man/repo-auth.Rd index 280b7a5763..d77457cbbd 100644 --- a/man/repo-auth.Rd +++ b/man/repo-auth.Rd @@ -8,6 +8,18 @@ pak supports HTTP basic authentication when interacting with CRAN-like repositories. \subsection{Configuring authenticated repositories}{ +\if{text}{ + Note: the configuration of PPM SSO authentication is described in the + Posit Package Manager SSO authentication section below. +}\if{html}{\out{ +
+Note: the configuration of PPM SSO authentication is described in the +Posit Package Manager SSO authentication section +below. +
+}} + + To use authentication you need to include a user name in the repository URL. You can set the repository URL in the \code{repos} option with \code{\link[base:options]{base::options()}} as usual, or you can use \code{\link[=repo_add]{repo_add()}}. @@ -296,9 +308,53 @@ x Did not find credentials for repo , keyring E.g. here \code{meta_update()} outputs an authentication message, but \code{meta_list()} does not. + +\if{html}{\out{}}\if{html}{\out{}} +} + +\subsection{Posit Package Manager SSO authentication}{ +\enumerate{ +\item To set up PPM SSO authentication, set the \code{PACKAGEMANAGER_ADDRESS} +environment variable to the URL of your RStudio Package Manager instance. +For example, add this line to your \code{.Renviron} file: +} + +\if{html}{\out{
}}\preformatted{PACKAGEMANAGER_ADDRESS=https:// +}\if{html}{\out{
}} + +Alternatively, you can also set it in your shell profile on Unix, +or in the System or User environment variables on Windows. +\enumerate{ +\item Set \code{options(repos)} to include a repository from your Package Manager +instance. Include \verb{__token__} as the username in the URL. For example: +} + +\if{html}{\out{
}}\preformatted{options(repos = c( + PPM = "https://__token__@/", + getOption("repos") +)) +}\if{html}{\out{
}} + +You probably want to add this to your \code{.Rprofile} file, so that it is set +in every R session. +\enumerate{ +\item Call \code{\link[=repo_get]{repo_get()}} to trigger authentication and caching of the token. +You should be prompted to log in via your browser, and the obtained +token will be cached for future use. Call \code{ppm_sso_status()} to check +the status of your authentication, including the path of the cached +token and its expiration time. +} + +Alternatively, you can call \code{ppm_sso_login()} directly to trigger +the login process directly. + +See also the \href{https://docs.posit.co/rspm/admin/authentication/}{Authentication chapter} of the +Posit Package Manager Documentation. } } \seealso{ +\code{\link[=repo_auth]{repo_auth()}}, \code{\link[=ppm_sso_login]{ppm_sso_login()}}. + Other authenticated repositories: \code{\link{repo_auth}()}, \code{\link{repo_auth_key_get}()} diff --git a/src/dummy/ts/DESCRIPTION b/src/dummy/ts/DESCRIPTION new file mode 100644 index 0000000000..833fb35e69 --- /dev/null +++ b/src/dummy/ts/DESCRIPTION @@ -0,0 +1,2 @@ +Package: ts +Version: 100.0.0 diff --git a/src/dummy/tstoml/DESCRIPTION b/src/dummy/tstoml/DESCRIPTION new file mode 100644 index 0000000000..ef4e4038dd --- /dev/null +++ b/src/dummy/tstoml/DESCRIPTION @@ -0,0 +1,2 @@ +Package: tstoml +Version: 100.0.0 diff --git a/src/install-embedded.R b/src/install-embedded.R index 76a55ed9e2..70f306b24c 100644 --- a/src/install-embedded.R +++ b/src/install-embedded.R @@ -82,7 +82,10 @@ install_order <- function() { "jsonlite", "lpSolve", "ps", + "ts", "zip", + # ts + "tstoml", # ps, R6 "processx", # processx, R6 @@ -91,7 +94,7 @@ install_order <- function() { "desc", # callr, cli, desc, processx, R6 "pkgbuild", - # callr, cli, curl, filelock, jsonlite, prettyunis, processx, R6 + # callr, cli, curl, filelock, jsonlite, prettyunis, processx, R6, ts, tstoml "pkgcache", # curl, jsonlite "pkgsearch", diff --git a/src/library/pkgcache/DESCRIPTION b/src/library/pkgcache/DESCRIPTION index 138a247828..8a0600c246 100644 --- a/src/library/pkgcache/DESCRIPTION +++ b/src/library/pkgcache/DESCRIPTION @@ -15,19 +15,21 @@ URL: https://r-lib.github.io/pkgcache/, BugReports: https://github.com/r-lib/pkgcache/issues Depends: R (>= 3.4) Imports: callr (>= 2.0.4.9000), cli (>= 3.2.0), curl (>= 3.2), - filelock, jsonlite, processx (>= 3.3.0.9001), R6, tools, utils + filelock, jsonlite, processx (>= 3.3.0.9001), R6, tools, ts, + tstoml, utils Suggests: covr, debugme, desc, fs, keyring, pillar, pingr, rprojroot, sessioninfo, spelling, testthat (>= 3.2.0), webfakes (>= 1.1.5), withr, zip +Remotes: r-lib/ts, gaborcsardi/tstoml Config/Needs/website: tidyverse/tidytemplate Config/testthat/edition: 3 Config/usethis/last-upkeep: 2025-04-30 Encoding: UTF-8 Language: en-US Roxygen: list(markdown = TRUE, r6 = FALSE) -RoxygenNote: 7.3.2.9000 +RoxygenNote: 7.3.3 NeedsCompilation: yes -Packaged: 2026-04-22 20:18:20 UTC; gaborcsardi +Packaged: 2026-05-13 12:32:46 UTC; gaborcsardi Author: Gábor Csárdi [aut, cre], Posit Software, PBC [cph, fnd] (ROR: ) Maintainer: Gábor Csárdi diff --git a/src/library/pkgcache/NAMESPACE b/src/library/pkgcache/NAMESPACE index 6635a11224..ff8464fa86 100644 --- a/src/library/pkgcache/NAMESPACE +++ b/src/library/pkgcache/NAMESPACE @@ -1,7 +1,9 @@ # Generated by roxygen2: do not edit by hand S3method("[",pkgcache_repo_status_summary) +S3method(format,ppm_sso_status) S3method(print,pkgcache_repo_status_summary) +S3method(print,ppm_sso_status) S3method(summary,pkgcache_repo_status) export(bioc_devel_version) export(bioc_release_version) @@ -41,6 +43,9 @@ export(ppm_platforms) export(ppm_r_versions) export(ppm_repo_url) export(ppm_snapshots) +export(ppm_sso_login) +export(ppm_sso_logout) +export(ppm_sso_status) export(repo_add) export(repo_auth) export(repo_get) diff --git a/src/library/pkgcache/R/aa-assertthat.R b/src/library/pkgcache/R/aa-assertthat.R index 5d1d49007d..9beb8eef10 100644 --- a/src/library/pkgcache/R/aa-assertthat.R +++ b/src/library/pkgcache/R/aa-assertthat.R @@ -1,6 +1,8 @@ assert_that <- function(..., env = parent.frame(), msg = NULL) { res <- see_if(..., env = env, msg = msg) - if (res) return(TRUE) + if (res) { + return(TRUE) + } throw(new_assert_error(attr(res, "msg"))) } @@ -27,7 +29,9 @@ see_if <- function(..., env = parent.frame(), msg = NULL) { # Failed, so figure out message to produce if (!res) { - if (is.null(msg)) msg <- get_message(res, assertion, env) + if (is.null(msg)) { + msg <- get_message(res, assertion, env) + } return(structure(FALSE, msg = msg)) } } @@ -36,12 +40,14 @@ see_if <- function(..., env = parent.frame(), msg = NULL) { } check_result <- function(x) { - if (!is.logical(x)) + if (!is.logical(x)) { throw(new_assert_error( "assert_that: assertion must return a logical value" )) - if (any(is.na(x))) + } + if (any(is.na(x))) { throw(new_assert_error("assert_that: missing values present in assertion")) + } if (length(x) != 1) { throw(new_assert_error("assert_that: length of assertion is not 1")) } @@ -57,7 +63,9 @@ get_message <- function(res, call, env = parent.frame()) { } f <- eval(call[[1]], env) - if (!is.primitive(f)) call <- match.call(f, call) + if (!is.primitive(f)) { + call <- match.call(f, call) + } fname <- deparse(call[[1]]) fail <- on_failure(f) %||% base_fs[[fname]] %||% fail_default diff --git a/src/library/pkgcache/R/aaa-async.R b/src/library/pkgcache/R/aaa-async.R index 2227751ab4..d2d6640996 100644 --- a/src/library/pkgcache/R/aaa-async.R +++ b/src/library/pkgcache/R/aaa-async.R @@ -22,7 +22,9 @@ async <- function(fun) { fun <- as.function(fun) - if (is_async(fun)) return(fun) + if (is_async(fun)) { + return(fun) + } async_fun <- fun body(async_fun) <- bquote({ @@ -339,7 +341,9 @@ get_default_event_loop <- function() { push_event_loop <- function() { num_loops <- length(async_env$loops) - if (num_loops > 0) async_env$loops[[num_loops]]$suspend() + if (num_loops > 0) { + async_env$loops[[num_loops]]$suspend() + } new_el <- event_loop$new() async_env$loops <- c(async_env$loops, list(new_el)) new_el @@ -453,7 +457,9 @@ NULL async_next <- function(el = NULL) { el <- el %||% find_sync_frame()$new_el - if (is.null(el)) stop("No async context") + if (is.null(el)) { + stop("No async context") + } ## TODO: some visual indication that something has happened? if (!el$run("once")) message("[ASYNC] async phase complete") } @@ -466,7 +472,9 @@ async_next <- function(el = NULL) { async_step <- function() { el <- find_sync_frame()$new_el - if (is.null(el)) stop("No async context") + if (is.null(el)) { + stop("No async context") + } ## TODO: some visual indication that something has happened? old <- options(async_debug_steps = TRUE) on.exit(options(old)) @@ -492,7 +500,9 @@ async_step_back <- function() { async_list <- function(def = NULL) { def <- def %||% find_sync_frame()$res - if (is.null(def)) stop("No async context") + if (is.null(def)) { + stop("No async context") + } info <- list() find_parents <- function(def) { info <<- c(info, list(get_private(def)$get_info())) @@ -520,7 +530,9 @@ async_tree <- function(def = NULL) { async_debug <- function(id, action = TRUE, parent = TRUE) { def <- find_deferred(id) - if (is.null(def)) stop("Cannot find deferred `", id, "`") + if (is.null(def)) { + stop("Cannot find deferred `", id, "`") + } prv <- get_private(def) if (prv$state != "pending") { @@ -563,11 +575,17 @@ async_debug <- function(id, action = TRUE, parent = TRUE) { async_wait_for <- function(id) { el <- find_sync_frame()$new_el - if (is.null(el)) stop("No async context") + if (is.null(el)) { + stop("No async context") + } def <- find_deferred(id) - if (is.null(def)) stop("Cannot find deferred `", id, "`") + if (is.null(def)) { + stop("Cannot find deferred `", id, "`") + } priv <- get_private(def) - while (priv$state == "pending") el$run("once") + while (priv$state == "pending") { + el$run("once") + } message("[ASYNC] ", id, " resolved") } @@ -684,9 +702,13 @@ find_async_data_frame <- function() { find_deferred <- function(id, def = NULL) { def <- def %||% find_sync_frame()$res - if (is.null(def)) stop("No async context") + if (is.null(def)) { + stop("No async context") + } search_parents <- function(def) { - if (get_private(def)$id == id) return(def) + if (get_private(def)$id == id) { + return(def) + } prn <- get_private(def)$parents for (p in lapply(prn, search_parents)) { if (!is.null(p)) return(p) @@ -1132,7 +1154,7 @@ deferred <- R6Class( type = NULL, call = sys.call(-1), event_emitter = NULL - ) + ) { async_def_init( self, private, @@ -1145,7 +1167,8 @@ deferred <- R6Class( type, call, event_emitter - ), + ) + }, then = function(on_fulfilled) def_then(self, private, on_fulfilled), catch = function(...) def_catch(self, private, ...), finally = function(on_finally) def_finally(self, private, on_finally), @@ -1185,14 +1208,17 @@ deferred <- R6Class( reject = function(reason) def__reject(self, private, reason), progress = function(data) def__progress(self, private, data), - make_error_object = function(err) - def__make_error_object(self, private, err), + make_error_object = function(err) { + def__make_error_object(self, private, err) + }, - maybe_cancel_parents = function(reason) - def__maybe_cancel_parents(self, private, reason), + maybe_cancel_parents = function(reason) { + def__maybe_cancel_parents(self, private, reason) + }, add_as_parent = function(child) def__add_as_parent(self, private, child), - update_parent = function(old, new) - def__update_parent(self, private, old, new), + update_parent = function(old, new) { + def__update_parent(self, private, old, new) + }, get_info = function() def__get_info(self, private) ) @@ -1240,7 +1266,9 @@ async_def_init <- function( } def__run_action <- function(self, private) { - if (private$running) return() + if (private$running) { + return() + } action <- private$action private$running <- TRUE private$action <- NULL @@ -1261,7 +1289,9 @@ def__run_action <- function(self, private) { private$event_loop$add_next_tick( function() { - if (isTRUE(getOption("async_debug_steps", FALSE))) debug1(action) + if (isTRUE(getOption("async_debug_steps", FALSE))) { + debug1(action) + } `__async_data__` <- list(private$id, "action", self, skip = 2L) do.call(action, args) }, @@ -1275,8 +1305,11 @@ def__run_action <- function(self, private) { prt_priv <- get_private(prt) if (prt_priv$state != "pending") { def__call_then( - if (prt_priv$state == "fulfilled") "parent_resolve" else - "parent_reject", + if (prt_priv$state == "fulfilled") { + "parent_resolve" + } else { + "parent_reject" + }, self, prt_priv$value ) @@ -1337,7 +1370,9 @@ def_finally <- function(self, private, on_finally) { } def_cancel <- function(self, private, reason) { - if (private$state != "pending") return() + if (private$state != "pending") { + return() + } cancel_cond <- structure( list(message = reason %||% "Deferred computation cancelled", call = NULL), class = c("async_cancelled", "error", "condition") @@ -1352,8 +1387,12 @@ def__null <- function(self, private) { } def__resolve <- function(self, private, value) { - if (private$cancelled) return() - if (private$state != "pending") return() + if (private$cancelled) { + return() + } + if (private$state != "pending") { + return() + } if (is_deferred(value)) { private$parent_resolve <- def__make_parent_resolve(NULL) @@ -1460,8 +1499,12 @@ def__make_parent_reject_catch <- function(handlers) { } def__reject <- function(self, private, reason) { - if (private$cancelled) return() - if (private$state != "pending") return() + if (private$cancelled) { + return() + } + if (private$state != "pending") { + return() + } ## 'reason' cannot be a deferred here @@ -1483,11 +1526,17 @@ def__reject <- function(self, private, reason) { def__maybe_cancel_parents <- function(self, private, reason) { for (parent in private$parents) { - if (is.null(parent)) next + if (is.null(parent)) { + next + } parent_priv <- get_private(parent) - if (parent_priv$state != "pending") next - if (parent_priv$shared) next + if (parent_priv$state != "pending") { + next + } + if (parent_priv$shared) { + next + } parent$cancel(reason) } } @@ -1495,8 +1544,12 @@ def__maybe_cancel_parents <- function(self, private, reason) { def__call_then <- function(which, x, value) { force(value) private <- get_private(x) - if (!private$running) return() - if (private$state != "pending") return() + if (!private$running) { + return() + } + if (private$state != "pending") { + return() + } cb <- private[[which]] private$event_loop$add_next_tick( @@ -1527,7 +1580,9 @@ def__add_as_parent <- function(self, private, child) { private$children <- c(private$children, child) - if (get_private(child)$running) private$run_action() + if (get_private(child)$running) { + private$run_action() + } if (private$state == "pending") { ## Nothing to do } else if (private$state == "fulfilled") { @@ -1549,8 +1604,12 @@ def__update_parent <- function(self, private, old, new) { } def__progress <- function(self, private, data) { - if (private$state != "pending") return() - if (is.null(private$progress_callback)) return() + if (private$state != "pending") { + return() + } + if (is.null(private$progress_callback)) { + return() + } private$progress_callback(data) } @@ -1767,34 +1826,48 @@ event_loop <- R6Class( file = NULL, progress = NULL, data = NULL - ) el_add_http(self, private, handle, callback, file, progress, data), - http_setopt = function(total_con = NULL, host_con = NULL, multiplex = NULL) - el_http_setopt(self, private, total_con, host_con, multiplex), - - add_process = function(conns, callback, data) - el_add_process(self, private, conns, callback, data), - add_r_process = function(conns, callback, data) - el_add_r_process(self, private, conns, callback, data), - add_pool_task = function(callback, data) - el_add_pool_task(self, private, callback, data), - add_delayed = function(delay, func, callback, rep = FALSE) - el_add_delayed(self, private, delay, func, callback, rep), - add_next_tick = function(func, callback, data = NULL) - el_add_next_tick(self, private, func, callback, data), + ) { + el_add_http(self, private, handle, callback, file, progress, data) + }, + http_setopt = function( + total_con = NULL, + host_con = NULL, + multiplex = NULL + ) { + el_http_setopt(self, private, total_con, host_con, multiplex) + }, + + add_process = function(conns, callback, data) { + el_add_process(self, private, conns, callback, data) + }, + add_r_process = function(conns, callback, data) { + el_add_r_process(self, private, conns, callback, data) + }, + add_pool_task = function(callback, data) { + el_add_pool_task(self, private, callback, data) + }, + add_delayed = function(delay, func, callback, rep = FALSE) { + el_add_delayed(self, private, delay, func, callback, rep) + }, + add_next_tick = function(func, callback, data = NULL) { + el_add_next_tick(self, private, func, callback, data) + }, cancel = function(id) el_cancel(self, private, id), cancel_all = function() el_cancel_all(self, private), - run = function(mode = c("default", "nowait", "once")) - el_run(self, private, mode = match.arg(mode)), + run = function(mode = c("default", "nowait", "once")) { + el_run(self, private, mode = match.arg(mode)) + }, suspend = function() el_suspend(self, private), wakeup = function() el_wakeup(self, private) ), private = list( - create_task = function(callback, ..., id = NULL, type = "foobar") - el__create_task(self, private, callback, ..., id = id, type = type), + create_task = function(callback, ..., id = NULL, type = "foobar") { + el__create_task(self, private, callback, ..., id = id, type = type) + }, ensure_pool = function() el__ensure_pool(self, private), get_poll_timeout = function() el__get_poll_timeout(self, private), run_pending = function() el__run_pending(self, private), @@ -1839,7 +1912,9 @@ el_add_http <- function(self, private, handle, callback, progress, file, data) { type = "http" ) private$ensure_pool() - if (!is.null(outfile)) cat("", file = outfile) + if (!is.null(outfile)) { + cat("", file = outfile) + } content <- NULL @@ -1994,7 +2069,9 @@ el_run <- function(self, private, mode) { ## because some time we might switch to that. alive <- private$is_alive() - if (!alive) private$update_time() + if (!alive) { + private$update_time() + } while (alive && !private$stop_flag) { private$update_time() @@ -2229,8 +2306,12 @@ el__create_task <- function(self, private, callback, data, ..., id, type) { el__ensure_pool <- function(self, private) { getopt <- function(nm) { anm <- paste0("async_http_", nm) - if (!is.null(v <- getOption(anm))) return(v) - if (!is.na(v <- Sys.getenv(toupper(anm), NA_character_))) return(v) + if (!is.null(v <- getOption(anm))) { + return(v) + } + if (!is.na(v <- Sys.getenv(toupper(anm), NA_character_))) { + return(v) + } NULL } if (is.null(private$pool)) { @@ -2249,9 +2330,15 @@ el__ensure_pool <- function(self, private) { el_http_setopt <- function(self, private, total_con, host_con, multiplex) { private$ensure_pool() - if (!is.null(total_con)) private$http_opts$total_con <- total_con - if (!is.null(host_con)) private$http_opts$host_con <- host_con - if (!is.null(multiplex)) private$http_opts$multiplex <- multiplex + if (!is.null(total_con)) { + private$http_opts$total_con <- total_con + } + if (!is.null(host_con)) { + private$http_opts$host_con <- host_con + } + if (!is.null(multiplex)) { + private$http_opts$multiplex <- multiplex + } curl::multi_set( pool = private$pool, total_con = private$http_opts$total_con, @@ -2401,24 +2488,29 @@ event_emitter <- R6Class( public = list( initialize = function(async = TRUE) ee_init(self, private, async), - listen_on = function(event, callback) - ee_listen_on(self, private, event, callback), + listen_on = function(event, callback) { + ee_listen_on(self, private, event, callback) + }, - listen_off = function(event, callback) - ee_listen_off(self, private, event, callback), + listen_off = function(event, callback) { + ee_listen_off(self, private, event, callback) + }, - listen_once = function(event, callback) - ee_listen_once(self, private, event, callback), + listen_once = function(event, callback) { + ee_listen_once(self, private, event, callback) + }, emit = function(event, ...) ee_emit(self, private, event, ...), get_event_names = function() ee_get_event_names(self, private), - get_listener_count = function(event) - ee_get_listener_count(self, private, event), + get_listener_count = function(event) { + ee_get_listener_count(self, private, event) + }, - remove_all_listeners = function(event) + remove_all_listeners = function(event) { ee_remove_all_listeners(self, private, event) + } ), private = list( @@ -2426,8 +2518,9 @@ event_emitter <- R6Class( async = NULL, cleanup_events = function() ee__cleanup_events(self, private), - error_callback = function(err, res) + error_callback = function(err, res) { ee__error_callback(self, private, err, res) + } ) ) @@ -2468,7 +2561,9 @@ ee_emit <- function(self, private, event, ...) { list(...) tocall <- private$lsts[[event]] once <- vlapply(tocall, "[[", "once") - if (any(once)) private$lsts[[event]] <- tocall[!once] + if (any(once)) { + private$lsts[[event]] <- tocall[!once] + } ## a for loop is not good here, because it does not create ## a closure for lst @@ -2514,13 +2609,19 @@ ee__cleanup_events <- function(self, private) { } ee__error_callback <- function(self, private, err, res) { - if (is.null(err)) return() + if (is.null(err)) { + return() + } tocall <- private$lsts[["error"]] once <- vlapply(tocall, "[[", "once") - if (any(once)) private$lsts[["error"]] <- tocall[!once] + if (any(once)) { + private$lsts[["error"]] <- tocall[!once] + } if (length(tocall)) { - for (lst in tocall) lst$cb(err) + for (lst in tocall) { + lst$cb(err) + } } else { stop(err) } @@ -3012,7 +3113,9 @@ http_post <- function( if (!is.null(data_file)) { data <- readBin(data_file, "raw", file.size(data_file)) } - if (!is.null(data) && !is.raw(data)) data <- charToRaw(data) + if (!is.null(data) && !is.raw(data)) { + data <- charToRaw(data) + } options <- get_default_curl_options(options) make_deferred_http( @@ -3066,9 +3169,13 @@ http_delete <- mark_as_async(http_delete) get_default_curl_options <- function(options) { getopt <- function(nm) { - if (!is.null(v <- options[[nm]])) return(v) + if (!is.null(v <- options[[nm]])) { + return(v) + } anm <- paste0("async_http_", nm) - if (!is.null(v <- getOption(anm))) return(v) + if (!is.null(v <- getOption(anm))) { + return(v) + } if (!is.na(v <- Sys.getenv(toupper(anm), NA_character_))) return(v) } modifyList( @@ -3156,8 +3263,12 @@ make_deferred_http <- function(cb, file) { #' } http_stop_for_status <- function(resp) { - if (!is.integer(resp$status_code)) stop("Not an HTTP response") - if (resp$status_code < 400) return(invisible(resp)) + if (!is.integer(resp$status_code)) { + stop("Not an HTTP response") + } + if (resp$status_code < 400) { + return(invisible(resp)) + } stop(http_error(resp)) } @@ -3805,11 +3916,15 @@ async_retryable <- function(task, times) { async_sequence <- function(..., .list = NULL) { funcs <- c(list(...), .list) - if (length(funcs) == 0) stop("Function list empty in `async_sequence`") + if (length(funcs) == 0) { + stop("Function list empty in `async_sequence`") + } function(...) { dx <- async(funcs[[1]])(...) - for (i in seq_along(funcs)[-1]) dx <- dx$then(funcs[[i]]) + for (i in seq_along(funcs)[-1]) { + dx <- dx$then(funcs[[i]]) + } dx } } @@ -3891,7 +4006,9 @@ synchronise <- function(expr) { res <- expr - if (!is_deferred(res)) return(res) + if (!is_deferred(res)) { + return(res) + } ## We need an extra final promise that cannot be replaced, ## so priv stays the same. @@ -3909,8 +4026,12 @@ synchronise <- function(expr) { priv$null() priv$run_action() - if (isTRUE(getOption("async_debug"))) start_browser() - while (priv$state == "pending") new_el$run("once") + if (isTRUE(getOption("async_debug"))) { + start_browser() + } + while (priv$state == "pending") { + new_el$run("once") + } if (priv$state == "fulfilled") priv$value else stop(priv$value) } @@ -3984,7 +4105,9 @@ run_event_loop <- function(expr) { } distill_error <- function(err) { - if (is.null(err$aframe)) return(err) + if (is.null(err$aframe)) { + return(err) + } err$aframe <- list( frame = err$aframe$frame, deferred = err$aframe$data[[1]], @@ -4214,8 +4337,9 @@ async_timer <- R6Class( "async_timer", inherit = event_emitter, public = list( - initialize = function(delay, callback) - async_timer_init(self, private, super, delay, callback), + initialize = function(delay, callback) { + async_timer_init(self, private, super, delay, callback) + }, cancel = function() async_timer_cancel(self, private) ), @@ -4290,7 +4414,9 @@ async_try_each <- function(..., .list = list()) { call = sys.call(), action = function(resolve) { nx <<- length(defs) - if (nx == 0) resolve(NULL) + if (nx == 0) { + resolve(NULL) + } wh <<- 1L defs[[wh]]$then(self) }, @@ -4414,7 +4540,9 @@ call_with_callback <- function(func, callback, info = NULL) { recerror <<- e recerror$aframe <<- recerror$aframe %||% find_async_data_frame() recerror$calls <<- recerror$calls %||% sys.calls() - if (is.null(recerror[["call"]])) recerror[["call"]] <<- sys.call() + if (is.null(recerror[["call"]])) { + recerror[["call"]] <<- sys.call() + } recerror$parents <<- recerror$parents %||% sys.parents() recerror[names(info)] <<- info handler <- getOption("async.error") @@ -4471,7 +4599,9 @@ file_size <- function(...) { } read_all <- function(filename, encoding) { - if (is.null(filename)) return(NULL) + if (is.null(filename)) { + return(NULL) + } r <- readBin(filename, what = raw(0), n = file_size(filename)) s <- rawToChar(r) Encoding(s) <- encoding @@ -4739,21 +4869,24 @@ NULL worker_pool <- R6Class( public = list( initialize = function() wp_init(self, private), - add_task = function(func, args, id, event_loop) - wp_add_task(self, private, func, args, id, event_loop), + add_task = function(func, args, id, event_loop) { + wp_add_task(self, private, func, args, id, event_loop) + }, get_fds = function() wp_get_fds(self, private), get_pids = function() wp_get_pids(self, private), get_poll_connections = function() wp_get_poll_connections(self, private), - notify_event = function(pids, event_loop) - wp_notify_event(self, private, pids, event_loop), + notify_event = function(pids, event_loop) { + wp_notify_event(self, private, pids, event_loop) + }, start_workers = function() wp_start_workers(self, private), kill_workers = function() wp_kill_workers(self, private), cancel_task = function(id) wp_cancel_task(self, private, id), cancel_all_tasks = function() wp_cancel_all_tasks(self, private), get_result = function(id) wp_get_result(self, private, id), list_workers = function() wp_list_workers(self, private), - list_tasks = function(event_loop = NULL, status = NULL) + list_tasks = function(event_loop = NULL, status = NULL) { wp_list_tasks(self, private, event_loop, status) + } ), private = list( @@ -4775,7 +4908,9 @@ wp_start_workers <- function(self, private) { num <- worker_pool_size() ## See if we need to start more - if (NROW(private$workers) >= num) return(invisible()) + if (NROW(private$workers) >= num) { + return(invisible()) + } ## Yeah, start some more to_start <- num - NROW(private$workers) @@ -4842,11 +4977,17 @@ wp_notify_event <- function(self, private, pids, event_loop) { which <- match(pids, private$workers$pid) for (w in which) { msg <- private$workers$session[[w]]$read() - if (is.null(msg)) next + if (is.null(msg)) { + next + } if (msg$code == 200 || (msg$code >= 500 && msg$code < 600)) { - if (msg$code >= 500 && msg$code < 600) dead <- c(dead, w) + if (msg$code >= 500 && msg$code < 600) { + dead <- c(dead, w) + } wt <- match(private$workers$task[[w]], private$tasks$id) - if (is.na(wt)) stop("Internal error, no such task") + if (is.na(wt)) { + stop("Internal error, no such task") + } private$tasks$result[[wt]] <- msg private$tasks$status[[wt]] <- "done" private$workers$task[[w]] <- NA_character_ @@ -4876,7 +5017,9 @@ wp_kill_workers <- function(self, private) { wp_cancel_task <- function(self, private, id) { wt <- match(id, private$tasks$id) - if (is.na(wt)) stop("Unknown task") + if (is.na(wt)) { + stop("Unknown task") + } if (private$tasks$status[[wt]] == "running") { wk <- match(id, private$workers$task) @@ -4892,9 +5035,13 @@ wp_cancel_all_tasks <- function(self, private) { wp_get_result <- function(self, private, id) { wt <- match(id, private$tasks$id) - if (is.na(wt)) stop("Unknown task") + if (is.na(wt)) { + stop("Unknown task") + } - if (private$tasks$status[[wt]] != "done") stop("Task not done yet") + if (private$tasks$status[[wt]] != "done") { + stop("Task not done yet") + } result <- private$tasks$result[[wt]] private$tasks <- private$tasks[-wt, ] result @@ -4907,8 +5054,12 @@ wp_list_workers <- function(self, private) { wp_list_tasks <- function(self, private, event_loop, status) { dont_show <- c("func", "args", "result") ret <- private$tasks - if (!is.null(event_loop)) ret <- ret[ret$event_loop %in% event_loop, ] - if (!is.null(status)) ret <- ret[ret$status %in% status, ] + if (!is.null(event_loop)) { + ret <- ret[ret$event_loop %in% event_loop, ] + } + if (!is.null(status)) { + ret <- ret[ret$status %in% status, ] + } ret[, setdiff(colnames(private$tasks), dont_show)] } @@ -4918,7 +5069,9 @@ wp_list_tasks <- function(self, private, event_loop, status) { wp__try_start <- function(self, private) { sts <- vcapply(private$workers$session, function(x) x$get_state()) - if (all(sts != "idle")) return() + if (all(sts != "idle")) { + return() + } can_work <- sts == "idle" can_run <- private$tasks$status == "waiting" @@ -4971,7 +5124,9 @@ wp__try_start <- function(self, private) { wp__interrupt_worker <- function(self, private, pid) { ww <- match(pid, private$workers$pid) - if (is.na(ww)) stop("Unknown task in interrupt_worker() method") + if (is.na(ww)) { + stop("Unknown task in interrupt_worker() method") + } kill <- FALSE sess <- private$workers$session[[ww]] diff --git a/src/library/pkgcache/R/aaa-rstudio-detect.R b/src/library/pkgcache/R/aaa-rstudio-detect.R index 1428330e86..5d7d0084cf 100644 --- a/src/library/pkgcache/R/aaa-rstudio-detect.R +++ b/src/library/pkgcache/R/aaa-rstudio-detect.R @@ -51,8 +51,12 @@ rstudio <- local({ } # Cached? - if (clear_cache) data <<- NULL - if (!is.null(data)) return(get_caps(data)) + if (clear_cache) { + data <<- NULL + } + if (!is.null(data)) { + return(get_caps(data)) + } if ( (rspid <- Sys.getenv("RSTUDIO_SESSION_PID")) != "" && @@ -86,7 +90,9 @@ rstudio <- local({ pane <- Sys.getenv("RSTUDIO_CHILD_PROCESS_PANE") # this should not happen, but be defensive and fall back - if (pane == "") return(detect_old(clear_cache)) + if (pane == "") { + return(detect_old(clear_cache)) + } # direct subprocess new$type <- if (rspid == parentpid) { @@ -175,7 +181,9 @@ rstudio <- local({ } installing <- Sys.getenv("R_PACKAGE_DIR", "") - if (cache && installing == "") data <<- new + if (cache && installing == "") { + data <<- new + } get_caps(new) } diff --git a/src/library/pkgcache/R/archive.R b/src/library/pkgcache/R/archive.R index ed5e6eaefb..2c739d2deb 100644 --- a/src/library/pkgcache/R/archive.R +++ b/src/library/pkgcache/R/archive.R @@ -101,7 +101,7 @@ cran_archive_cache <- R6Class( replica_path = tempfile(), cran_mirror = default_cran_mirror(), update_after = as.difftime(7, units = "days") - ) + ) { cac_init( self, private, @@ -109,17 +109,20 @@ cran_archive_cache <- R6Class( replica_path, cran_mirror, update_after - ), + ) + }, - list = function(packages = NULL, update_after = NULL) - synchronise(self$async_list(packages, update_after)), - async_list = function(packages = NULL, update_after = NULL) + list = function(packages = NULL, update_after = NULL) { + synchronise(self$async_list(packages, update_after)) + }, + async_list = function(packages = NULL, update_after = NULL) { cac_async_list( self, private, packages, update_after %||% private$update_after - ), + ) + }, update = function() synchronise(self$async_update()), async_update = function() cac_async_update(self, private), @@ -136,25 +139,31 @@ cran_archive_cache <- R6Class( get_hash = function() { cli::hash_obj_md5(list(private$cran_mirror, private$cache_version)) }, - get_cache_file = function(which = c("primary", "replica")) - cac__get_cache_file(self, private, match.arg(which)), + get_cache_file = function(which = c("primary", "replica")) { + cac__get_cache_file(self, private, match.arg(which)) + }, - async_ensure_cache = function(max_age = private$update_after) - cac__async_ensure_cache(self, private, max_age), + async_ensure_cache = function(max_age = private$update_after) { + cac__async_ensure_cache(self, private, max_age) + }, - get_current_data = function(max_age) - cac__get_current_data(self, private, max_age), - get_memory_cache = function(max_age) - cac__get_memory_cache(self, private, max_age), + get_current_data = function(max_age) { + cac__get_current_data(self, private, max_age) + }, + get_memory_cache = function(max_age) { + cac__get_memory_cache(self, private, max_age) + }, load_replica = function(max_age) cac__load_replica(self, private, max_age), load_primary = function(max_age) cac__load_primary(self, private, max_age), update_memory_cache = function() cac__update_memory_cache(self, private), update_replica = function() cac__update_replica(self, private), - update_primary = function(lock = TRUE) - cac__update_primary(self, private, lock), - convert_archive_file = function(raw, out) - cac__convert_archive_file(self, private, raw, out), + update_primary = function(lock = TRUE) { + cac__update_primary(self, private, lock) + }, + convert_archive_file = function(raw, out) { + cac__convert_archive_file(self, private, raw, out) + }, cache_version = "1", @@ -220,8 +229,12 @@ cac_async_check_update <- function(self, private) { self private - if (!is.null(private$update_deferred)) return(private$update_deferred) # nocov - if (!is.null(private$chk_update_deferred)) return(private$chk_update_deferred) # nocov + if (!is.null(private$update_deferred)) { + return(private$update_deferred) + } # nocov + if (!is.null(private$chk_update_deferred)) { + return(private$chk_update_deferred) + } # nocov private$chk_update_deferred <- async(private$update_replica)()$then(function( ret @@ -297,7 +310,9 @@ cac__async_ensure_cache <- function(self, private, max_age) { } cac__get_current_data <- function(self, private, max_age) { - if (is.null(private$data)) stop("No data loaded") + if (is.null(private$data)) { + stop("No data loaded") + } if ( is.null(private$data_time) || Sys.time() - private$data_time > max_age @@ -311,7 +326,9 @@ cac__get_memory_cache <- function(self, private, max_age) { rds <- private$get_cache_file("primary") hash <- private$get_hash() hit <- cmc__data[[hash]] - if (is.null(hit)) stop("Not in memory cache") + if (is.null(hit)) { + stop("Not in memory cache") + } if (is.null(hit$data_time) || Sys.time() - hit$data_time > max_age) { stop("Memory cache outdated") } @@ -322,10 +339,14 @@ cac__get_memory_cache <- function(self, private, max_age) { cac__load_replica <- function(self, private, max_age) { rds <- private$get_cache_file("replica") - if (!file.exists(rds)) stop("No replica RDS in cache") + if (!file.exists(rds)) { + stop("No replica RDS in cache") + } time <- file_get_time(rds) - if (Sys.time() - time > max_age) stop("Replica RDS file outdated") + if (Sys.time() - time > max_age) { + stop("Replica RDS file outdated") + } private$data <- readRDS(rds) private$data_time <- time @@ -340,12 +361,18 @@ cac__load_primary <- function(self, private, max_age) { pri_lock <- paste0(pri_file, "-lock") mkdirp(dirname(pri_lock)) l <- filelock::lock(pri_lock, exclusive = FALSE, private$lock_timeout) - if (is.null(l)) stop("Cannot acquire lock to copy RDS") + if (is.null(l)) { + stop("Cannot acquire lock to copy RDS") + } on.exit(filelock::unlock(l), add = TRUE) - if (!file.exists(pri_file)) stop("No primary RDS file in cache") + if (!file.exists(pri_file)) { + stop("No primary RDS file in cache") + } time <- file_get_time(pri_file) - if (Sys.time() - time > max_age) stop("Primary RDS cache file outdated") + if (Sys.time() - time > max_age) { + stop("Primary RDS cache file outdated") + } file_copy_with_time(pri_file, rep_file) @@ -434,7 +461,9 @@ cac__update_primary <- function(self, private, lock) { pri_lock <- paste0(pri_file, "-lock") mkdirp(dirname(pri_lock)) l <- filelock::lock(pri_lock, exclusive = FALSE, private$lock_timeout) - if (is.null(l)) stop("Cannot acquire lock to copy RDS") + if (is.null(l)) { + stop("Cannot acquire lock to copy RDS") + } on.exit(filelock::unlock(l), add = TRUE) } diff --git a/src/library/pkgcache/R/async-http.R b/src/library/pkgcache/R/async-http.R index 95e97db5a2..72a9f1e364 100644 --- a/src/library/pkgcache/R/async-http.R +++ b/src/library/pkgcache/R/async-http.R @@ -17,9 +17,13 @@ default_http_version <- function() { update_async_timeouts <- function(options) { getopt <- function(nm) { - if (!is.null(v <- options[[nm]])) return(v) + if (!is.null(v <- options[[nm]])) { + return(v) + } anm <- paste0("pkgcache_", nm) - if (!is.null(v <- getOption(anm))) return(v) + if (!is.null(v <- getOption(anm))) { + return(v) + } if (!is.na(v <- Sys.getenv(toupper(anm), NA_character_))) return(v) } utils::modifyList( diff --git a/src/library/pkgcache/R/auth.R b/src/library/pkgcache/R/auth.R index 5b9baf664f..e3cf891a5b 100644 --- a/src/library/pkgcache/R/auth.R +++ b/src/library/pkgcache/R/auth.R @@ -83,7 +83,9 @@ repo_auth <- function( url <- res$url[w] if (check_credentials) { cred <- repo_auth_headers(url, warn = FALSE) - if (is.null(cred)) next + if (is.null(cred)) { + next + } res$username[w] <- cred$username res$has_password[w] <- cred$found res$auth_domains[w] <- list(cred$auth_domains) @@ -174,7 +176,7 @@ repo_auth_headers <- function( # - host URL w/o username # We try each with and without a keyring username urls <- unique(unlist( - parsed_url[c("repouserurl", "repourl", "hostuserurl", "hosturl")] + parsed_url[c("repouserurl", "repourl", "hostuserurl", "hosturl", "host")] )) if (use_cache) { @@ -197,10 +199,18 @@ repo_auth_headers <- function( error = NULL ) - pwd <- repo_auth_netrc(parsed_url$host, parsed_url$username) + pwd <- repo_auth_sso(parsed_url$repourl, parsed_url$username) if (!is.null(pwd)) { res$auth_domain <- parsed_url$host - res$source <- paste0(".netrc") + res$source <- "SSO" + } + + if (is.null(pwd)) { + pwd <- repo_auth_netrc(parsed_url$host, parsed_url$username) + if (!is.null(pwd)) { + res$auth_domain <- parsed_url$host + res$source <- paste0(".netrc") + } } if (is.null(pwd) && !requireNamespace("keyring", quietly = TRUE)) { @@ -315,7 +325,9 @@ parse_url_basic_auth <- function(url) { add_auth_status <- function(repos) { maybe_has_auth <- grepl("^https?://[^/]*@", repos$url) - if (!any(maybe_has_auth)) return(repos) + if (!any(maybe_has_auth)) { + return(repos) + } key <- random_key() on.exit(clear_auth_cache(key), add = TRUE) @@ -326,7 +338,9 @@ add_auth_status <- function(repos) { for (w in which(maybe_has_auth)) { url <- repos$url[w] creds <- repo_auth_headers(url, warn = FALSE) - if (is.null(creds)) next + if (is.null(creds)) { + next + } repos$username[w] <- creds$username repos$has_password[w] <- creds$found } @@ -342,7 +356,9 @@ repo_auth_netrc <- function(host, username) { netrc_path <- path.expand("~/_netrc") } } - if (!file.exists(netrc_path)) return(NULL) + if (!file.exists(netrc_path)) { + return(NULL) + } # netrc files do not allow port numbers host <- sub(":[0-9]+$", "", host) @@ -453,3 +469,28 @@ repo_auth_netrc <- function(host, username) { NULL } + +repo_auth_sso <- function(repourl, username) { + ppm_url <- Sys.getenv("PACKAGEMANAGER_ADDRESS", NA_character_) + if (is.na(ppm_url)) { + return(NULL) + } + + if (!startsWith(repourl, ppm_url)) { + return(NULL) + } + + token <- tryCatch( + ppm_sso_auth(repourl), + error = function(e) { + cli::cli_alert_warning( + "PPM SSO authentication failed for repo {.url {repourl}}: {conditionMessage(e)}" + ) + cli::cli_alert_info( + "Try calling {.code ppm_sso_login()} directly." + ) + NULL + } + ) + token +} diff --git a/src/library/pkgcache/R/bioc.R b/src/library/pkgcache/R/bioc.R index 0645af2b7d..5e8015bbfa 100644 --- a/src/library/pkgcache/R/bioc.R +++ b/src/library/pkgcache/R/bioc.R @@ -122,6 +122,7 @@ bioconductor <- local({ "4.2" = package_version("3.16"), "4.3" = package_version("3.18"), "4.4" = package_version("3.20"), + "4.5" = package_version("3.22"), NULL # Do not include R 4.5 <-> Bioc 3.21, because R 4.5 will use # Bioc 3.22 eventually. @@ -152,7 +153,9 @@ bioconductor <- local({ http_url <- sub("^https", "http", config_url()) new <- tryCatch(read_url(http_url), error = function(x) x) } - if (inherits(new, "error")) stop(new) + if (inherits(new, "error")) { + stop(new) + } yaml_config <<- new } @@ -160,7 +163,9 @@ bioconductor <- local({ } set_yaml_config <- function(text) { - if (length(text) == 1) text <- strsplit(text, "\n", fixed = TRUE)[[1]] + if (length(text) == 1) { + text <- strsplit(text, "\n", fixed = TRUE)[[1]] + } yaml_config <<- text } @@ -230,7 +235,9 @@ bioconductor <- local({ forget = FALSE ) { minor <- as.character(get_minor_r_version(r_version)) - if (minor %in% names(builtin_map)) return(builtin_map[[minor]]) + if (minor %in% names(builtin_map)) { + return(builtin_map[[minor]]) + } # If we are not in the map, then we need to look this up in # YAML data. It is possible that the current R version matches multiple @@ -251,7 +258,9 @@ bioconductor <- local({ mine <- rev(mine)[1] } } - if (!is.na(mine)) return(map$bioc_version[mine]) + if (!is.na(mine)) { + return(map$bioc_version[mine]) + } # If it is not even in the YAML, then it must be some very old # or very new version. If old, we fail. If new, we assume bioc-devel. @@ -281,8 +290,9 @@ bioconductor <- local({ BioCsoft = "{mirror}/packages/{bv}/bioc", BioCann = "{mirror}/packages/{bv}/data/annotation", BioCexp = "{mirror}/packages/{bv}/data/experiment", - BioCworkflows = if (bioc_version >= "3.7") - "{mirror}/packages/{bv}/workflows", + BioCworkflows = if (bioc_version >= "3.7") { + "{mirror}/packages/{bv}/workflows" + }, BioCextra = if (bioc_version <= "3.5") "{mirror}/packages/{bv}/extra", BioCbooks = if (bioc_version >= "3.12") "{mirror}/packages/{bv}/books" ) diff --git a/src/library/pkgcache/R/cran-app.R b/src/library/pkgcache/R/cran-app.R index 639eab7b93..258518e18d 100644 --- a/src/library/pkgcache/R/cran-app.R +++ b/src/library/pkgcache/R/cran-app.R @@ -16,14 +16,18 @@ make_dummy_package <- function(data, path) { )) unlink(package, recursive = TRUE) out <- dir() - if (length(out) != 1) stop("Failed to build package ", package, " :(") + if (length(out) != 1) { + stop("Failed to build package ", package, " :(") + } mkdirp(path) file.copy(out, path, overwrite = TRUE) out } dummy_so <- function() { - if (!is.null(fake_env$dummy_so)) return(fake_env$dummy_so) + if (!is.null(fake_env$dummy_so)) { + return(fake_env$dummy_so) + } mkdirp(tmp <- tempfile()) on.exit(unlink(tmp, recursive = TRUE), add = TRUE) @@ -175,15 +179,19 @@ make_dummy_repo_platform <- function(repo, packages = NULL, options = list()) { if ( options[["platform"]] == "source" && packages$Package[i] %in% options[["no_sources"]] - ) + ) { next + } if ( options[["platform"]] != "source" && packages$Package[i] %in% options[["no_binaries"]] - ) + ) { next + } if (extra$archive[i]) { - if (isTRUE(options$no_archive)) next + if (isTRUE(options$no_archive)) { + next + } pkg_dir <- file.path(pkgs_dir, "Archive", packages$Package[i]) } else { pkg_dir <- pkgs_dir @@ -271,7 +279,9 @@ cran_app <- function( app <- webfakes::new_app() # Log requests by default - if (log) app$use("logger" = webfakes::mw_log()) + if (log) { + app$use("logger" = webfakes::mw_log()) + } if (!is.null(basic_auth)) { app$use("basic auth" = function(req, res) { @@ -374,7 +384,9 @@ bioc_app <- function(packages = NULL, log = interactive(), options = list()) { app <- webfakes::new_app() # Log requests by default - if (log) app$use("logger" = webfakes::mw_log()) + if (log) { + app$use("logger" = webfakes::mw_log()) + } # Parse all kinds of bodies app$use("json body parser" = webfakes::mw_json()) diff --git a/src/library/pkgcache/R/data-frame.R b/src/library/pkgcache/R/data-frame.R index 2651c2a446..60192bddc4 100644 --- a/src/library/pkgcache/R/data-frame.R +++ b/src/library/pkgcache/R/data-frame.R @@ -2,7 +2,9 @@ find_in_data_frame <- function(df, ..., .list = NULL) { cols <- drop_nulls(c(list(...), .list)) idx <- seq_len(nrow(df)) for (i in seq_along(cols)) { - if (length(idx) == 0) break + if (length(idx) == 0) { + break + } n <- names(cols)[i] idx <- idx[df[[n]][idx] %in% cols[[i]]] } diff --git a/src/library/pkgcache/R/errors.R b/src/library/pkgcache/R/errors.R index 7d1d585acb..4dda3e0bde 100644 --- a/src/library/pkgcache/R/errors.R +++ b/src/library/pkgcache/R/errors.R @@ -224,16 +224,24 @@ err <- local({ always_trace <- isTRUE(getOption("rlib_error_always_trace")) .hide_from_trace <- 1L # .error_frame <- cond - if (!always_trace) signalCondition(cond) + if (!always_trace) { + signalCondition(cond) + } - if (is.null(cond$`_pid`)) cond$`_pid` <- Sys.getpid() - if (is.null(cond$`_timestamp`)) cond$`_timestamp` <- Sys.time() + if (is.null(cond$`_pid`)) { + cond$`_pid` <- Sys.getpid() + } + if (is.null(cond$`_timestamp`)) { + cond$`_timestamp` <- Sys.time() + } # If we get here that means that the condition was not caught by # an exiting handler. That means that we need to create a trace. # If there is a hand-constructed trace already in the error object, # then we'll just leave it there. - if (is.null(cond$trace)) cond <- add_trace_back(cond, frame = frame) + if (is.null(cond$trace)) { + cond <- add_trace_back(cond, frame = frame) + } # Set up environment to store .Last.error, it will be just before # baseenv(), so it is almost as if it was in baseenv() itself, like @@ -250,11 +258,15 @@ err <- local({ env$.Last.error.trace <- cond$trace # If we always wanted a trace, then we signal the condition here - if (always_trace) signalCondition(cond) + if (always_trace) { + signalCondition(cond) + } # If this is not an error, then we'll just return here. This allows # throwing interrupt conditions for example, with the same UI. - if (!inherits(cond, "error")) return(invisible()) + if (!inherits(cond, "error")) { + return(invisible()) + } .hide_from_trace <- NULL # Top-level handler, this is intended for testing only for now, @@ -492,8 +504,12 @@ err <- local({ for (start in hide_from) { hide_this <- invisible_frames[[funs[start]]] for (i in seq_along(hide_this)) { - if (start + i > length(funs)) break - if (funs[start + i] != hide_this[i]) break + if (start + i > length(funs)) { + break + } + if (funs[start + i] != hide_this[i]) { + break + } visibles[start + i] <- FALSE } } @@ -527,19 +543,32 @@ err <- local({ } get_call_scope <- function(call, ns) { - if (is.na(ns)) return("global") - if (!is.call(call)) return("") + if (is.na(ns)) { + return("global") + } + if (!is.call(call)) { + return("") + } if ( is.call(call[[1]]) && (call[[1]][[1]] == quote(`::`) || call[[1]][[1]] == quote(`:::`)) - ) + ) { + return("") + } + if (ns == "base") { + return("::") + } + if (!ns %in% loadedNamespaces()) { return("") - if (ns == "base") return("::") - if (!ns %in% loadedNamespaces()) return("") + } name <- call_name(call) nsenv <- asNamespace(ns)$.__NAMESPACE__. - if (is.null(nsenv)) return("::") - if (is.null(nsenv$exports)) return(":::") + if (is.null(nsenv)) { + return("::") + } + if (is.null(nsenv$exports)) { + return(":::") + } if (exists(name, envir = nsenv$exports, inherits = FALSE)) { "::" } else if (exists(name, envir = asNamespace(ns), inherits = FALSE)) { @@ -729,7 +758,9 @@ err <- local({ conditionMessage(cond$parent) } add_exp <- substr(cli::ansi_strip(msg[1]), 1, 1) != "!" - if (add_exp) msg[1] <- paste0(exp, msg[1]) + if (add_exp) { + msg[1] <- paste0(exp, msg[1]) + } c(format_header_line_cli(cond$parent, prefix = "Caused by error"), msg) } ) @@ -827,14 +858,18 @@ err <- local({ NULL } else { cl <- trimws(format(call)) - if (length(cl) > 1) cl <- paste0(cl[1], " ", cli::symbol$ellipsis) + if (length(cl) > 1) { + cl <- paste0(cl[1], " ", cli::symbol$ellipsis) + } cli::format_inline("{.code {cl}}") } } format_srcref_cli <- function(call, srcref = NULL) { ref <- get_srcref(call, srcref) - if (is.null(ref)) return("") + if (is.null(ref)) { + return("") + } link <- if (ref$file != "") { cli::style_hyperlink( @@ -993,14 +1028,18 @@ err <- local({ NULL } else { cl <- trimws(format(call)) - if (length(cl) > 1) cl <- paste0(cl[1], " ...") + if (length(cl) > 1) { + cl <- paste0(cl[1], " ...") + } paste0("`", cl, "`") } } format_srcref_plain <- function(call, srcref = NULL) { ref <- get_srcref(call, srcref) - if (is.null(ref)) return("") + if (is.null(ref)) { + return("") + } link <- if (ref$file != "") { paste0(basename(ref$file), ":", ref$line, ":", ref$col) @@ -1052,10 +1091,16 @@ err <- local({ get_srcref <- function(call, srcref = NULL) { ref <- srcref %||% utils::getSrcref(call) - if (is.null(ref)) return(NULL) - if (inherits(ref, "processed_srcref")) return(ref) + if (is.null(ref)) { + return(NULL) + } + if (inherits(ref, "processed_srcref")) { + return(ref) + } file <- utils::getSrcFilename(ref, full.names = TRUE)[1] - if (is.na(file)) file <- "" + if (is.na(file)) { + file <- "" + } line <- utils::getSrcLocation(ref) %||% "" col <- utils::getSrcLocation(ref, which = "column") %||% "" structure( diff --git a/src/library/pkgcache/R/installed.R b/src/library/pkgcache/R/installed.R index 21baebd1c8..8a4a770062 100644 --- a/src/library/pkgcache/R/installed.R +++ b/src/library/pkgcache/R/installed.R @@ -23,7 +23,9 @@ parse_description <- function(path) { } fix_encodings <- function(lst, col = "Encoding") { - if (!col %in% names(lst)) return(lst) + if (!col %in% names(lst)) { + return(lst) + } utf8 <- which(!is.na(lst[[col]]) & lst[[col]] == "UTF-8") other <- which(!is.na(lst[[col]]) & lst[[col]] != "UTf-8") unq <- unique(lst[[col]][other]) @@ -148,16 +150,18 @@ guess_packages_type <- function(path) { buf[1] == 0x1f && buf[2] == 0x8b && buf[3] == 0x08 - ) + ) { return("gzip") + } if ( length(buf) >= 3 && buf[1] == 0x42 && buf[2] == 0x5a && buf[3] == 0x68 - ) + ) { return("bzip2") + } if ( length(buf) >= 6 && @@ -167,15 +171,17 @@ guess_packages_type <- function(path) { buf[4] == 0x58 && buf[5] == 0x5A && buf[6] == 0x00 - ) + ) { return("xz") + } if ( length(buf) >= 2 && buf[1] %in% as.raw(c(0x58, 0x41, 0x42)) && buf[2] == 0x0a - ) + ) { return("rds") + } "uncompressed" } @@ -336,7 +342,9 @@ parse_installed <- function( tbl <- tbl[keep, ] } - if (reencode) tbl <- fix_encodings(tbl) + if (reencode) { + tbl <- fix_encodings(tbl) + } tbl } diff --git a/src/library/pkgcache/R/iso-date.R b/src/library/pkgcache/R/iso-date.R index 8736e15687..2137ae0408 100644 --- a/src/library/pkgcache/R/iso-date.R +++ b/src/library/pkgcache/R/iso-date.R @@ -10,7 +10,9 @@ ymd <- function(x) as.POSIXct(x, format = "%Y %m %d", tz = "UTC") yj <- function(x) as.POSIXct(x, format = "%Y %j", tz = "UTC") parse_iso_8601 <- function(dates, default_tz = "UTC") { - if (default_tz == "") default_tz <- Sys.timezone() + if (default_tz == "") { + default_tz <- Sys.timezone() + } dates <- as.character(dates) match <- re_match(dates, iso_regex) matching <- !is.na(match$.match) diff --git a/src/library/pkgcache/R/metadata-cache.R b/src/library/pkgcache/R/metadata-cache.R index bbb6c5644a..746b1056d6 100644 --- a/src/library/pkgcache/R/metadata-cache.R +++ b/src/library/pkgcache/R/metadata-cache.R @@ -179,7 +179,7 @@ cranlike_metadata_cache <- R6Class( cran_mirror = default_cran_mirror(), repos = getOption("repos"), update_after = as.difftime(7, units = "days") - ) + ) { cmc_init( self, private, @@ -191,21 +191,27 @@ cranlike_metadata_cache <- R6Class( cran_mirror, repos, update_after - ), - - deps = function(packages, dependencies = NA, recursive = TRUE) - synchronise(self$async_deps(packages, dependencies, recursive)), - async_deps = function(packages, dependencies = NA, recursive = TRUE) - cmc_async_deps(self, private, packages, dependencies, recursive), - - revdeps = function(packages, dependencies = NA, recursive = TRUE) - synchronise(self$async_revdeps(packages, dependencies, recursive)), - async_revdeps = function(packages, dependencies = NA, recursive = TRUE) - cmc_async_revdeps(self, private, packages, dependencies, recursive), + ) + }, + + deps = function(packages, dependencies = NA, recursive = TRUE) { + synchronise(self$async_deps(packages, dependencies, recursive)) + }, + async_deps = function(packages, dependencies = NA, recursive = TRUE) { + cmc_async_deps(self, private, packages, dependencies, recursive) + }, + + revdeps = function(packages, dependencies = NA, recursive = TRUE) { + synchronise(self$async_revdeps(packages, dependencies, recursive)) + }, + async_revdeps = function(packages, dependencies = NA, recursive = TRUE) { + cmc_async_revdeps(self, private, packages, dependencies, recursive) + }, list = function(packages = NULL) synchronise(self$async_list(packages)), - async_list = function(packages = NULL) - cmc_async_list(self, private, packages), + async_list = function(packages = NULL) { + cmc_async_list(self, private, packages) + }, update = function() synchronise(self$async_update()), async_update = function() cmc_async_update(self, private), @@ -219,32 +225,42 @@ cranlike_metadata_cache <- R6Class( ), private = list( - get_cache_files = function(which = c("primary", "replica")) - cmc__get_cache_files(self, private, match.arg(which)), - - async_ensure_cache = function(max_age = private$update_after) - cmc__async_ensure_cache(self, private, max_age), - - get_current_data = function(max_age) - cmc__get_current_data(self, private, max_age), - get_memory_cache = function(max_age) - cmc__get_memory_cache(self, private, max_age), - load_replica_rds = function(max_age) - cmc__load_replica_rds(self, private, max_age), - load_primary_rds = function(max_age) - cmc__load_primary_rds(self, private, max_age), - load_primary_pkgs = function(max_age) - cmc__load_primary_pkgs(self, private, max_age), + get_cache_files = function(which = c("primary", "replica")) { + cmc__get_cache_files(self, private, match.arg(which)) + }, + + async_ensure_cache = function(max_age = private$update_after) { + cmc__async_ensure_cache(self, private, max_age) + }, + + get_current_data = function(max_age) { + cmc__get_current_data(self, private, max_age) + }, + get_memory_cache = function(max_age) { + cmc__get_memory_cache(self, private, max_age) + }, + load_replica_rds = function(max_age) { + cmc__load_replica_rds(self, private, max_age) + }, + load_primary_rds = function(max_age) { + cmc__load_primary_rds(self, private, max_age) + }, + load_primary_pkgs = function(max_age) { + cmc__load_primary_pkgs(self, private, max_age) + }, update_replica_pkgs = function() cmc__update_replica_pkgs(self, private), - update_replica_rds = function(alert = TRUE) - cmc__update_replica_rds(self, private, alert), - update_primary = function(rds = TRUE, packages = TRUE, lock = TRUE) - cmc__update_primary(self, private, rds, packages, lock), + update_replica_rds = function(alert = TRUE) { + cmc__update_replica_rds(self, private, alert) + }, + update_primary = function(rds = TRUE, packages = TRUE, lock = TRUE) { + cmc__update_primary(self, private, rds, packages, lock) + }, update_memory_cache = function() cmc__update_memory_cache(self, private), - copy_to_replica = function(rds = TRUE, pkgs = FALSE, etags = FALSE) - cmc__copy_to_replica(self, private, rds, pkgs, etags), + copy_to_replica = function(rds = TRUE, pkgs = FALSE, etags = FALSE) { + cmc__copy_to_replica(self, private, rds, pkgs, etags) + }, ## We use this to make sure that different versions of pkgcache can ## share the same metadata cache directory. It is used to calculate @@ -345,7 +361,9 @@ cmc_async_list <- function(self, private, packages) { cmc_async_update <- function(self, private) { self private - if (!is.null(private$update_deferred)) return(private$update_deferred) + if (!is.null(private$update_deferred)) { + return(private$update_deferred) + } private$update_deferred <- async(private$update_replica_pkgs)()$then( function() private$update_replica_rds() @@ -358,8 +376,12 @@ cmc_async_check_update <- function(self, private) { self private - if (!is.null(private$update_deferred)) return(private$update_deferred) - if (!is.null(private$chk_update_deferred)) return(private$chk_update_deferred) + if (!is.null(private$update_deferred)) { + return(private$update_deferred) + } + if (!is.null(private$chk_update_deferred)) { + return(private$chk_update_deferred) + } private$chk_update_deferred <- async(private$update_replica_pkgs)()$then( function(ret) { @@ -540,7 +562,9 @@ ppm_binary_url <- function(urls, r_version) { # If multiple R versions are requested, then we give up, and pretend # that PPM binaries are source packages - if (length(r_version) != 1) return(res) + if (length(r_version) != 1) { + return(res) + } # http://rspm.infra/all/__linux__/bionic/latest -> # http://rspm.infra/all/latest/bin/linux/4.2-bionic/contrib/4.2/PACKAGES @@ -615,7 +639,9 @@ cmc__async_ensure_cache <- function(self, private, max_age) { cmc__get_current_data <- function(self, private, max_age) { "!!DEBUG Get current data?" - if (is.null(private$data)) stop("No data loaded") + if (is.null(private$data)) { + stop("No data loaded") + } if ( is.null(private$data_time) || Sys.time() - private$data_time > max_age @@ -634,7 +660,9 @@ cmc__get_memory_cache <- function(self, private, max_age) { "!!DEBUG Get from memory cache?" rds <- private$get_cache_files("primary")$rds hit <- cmc__data[[rds]] - if (is.null(hit)) stop("Not in the memory cache") + if (is.null(hit)) { + stop("Not in the memory cache") + } if (is.null(hit$data_time) || Sys.time() - hit$data_time > max_age) { stop("Memory cache outdated") } @@ -660,10 +688,14 @@ cmc__get_memory_cache <- function(self, private, max_age) { cmc__load_replica_rds <- function(self, private, max_age) { "!!DEBUG Load replica RDS?" rds <- private$get_cache_files("replica")$rds - if (!file.exists(rds)) stop("No replica RDS file in cache") + if (!file.exists(rds)) { + stop("No replica RDS file in cache") + } time <- file_get_time(rds) - if (Sys.time() - time > max_age) stop("Replica RDS cache file outdated") + if (Sys.time() - time > max_age) { + stop("Replica RDS cache file outdated") + } sts <- cli::cli_process_start("Loading metadata database") private$data <- readRDS(rds) @@ -692,12 +724,18 @@ cmc__load_primary_rds <- function(self, private, max_age) { mkdirp(dirname(pri_files$lock)) l <- filelock::lock(pri_files$lock, exclusive = FALSE, private$lock_timeout) - if (is.null(l)) stop("Cannot acquire lock to copy RDS") + if (is.null(l)) { + stop("Cannot acquire lock to copy RDS") + } on.exit(filelock::unlock(l), add = TRUE) - if (!file.exists(pri_files$rds)) stop("No primary RDS file in cache") + if (!file.exists(pri_files$rds)) { + stop("No primary RDS file in cache") + } time <- file_get_time(pri_files$rds) - if (Sys.time() - time > max_age) stop("Primary RDS cache file outdated") + if (Sys.time() - time > max_age) { + stop("Primary RDS cache file outdated") + } ## Metadata files might be missing or outdated, that's ok (?) pkgs_times <- file_get_time(pri_files$pkgs$path) @@ -741,7 +779,9 @@ cmc__load_primary_pkgs <- function(self, private, max_age) { ## Lock mkdirp(dirname(pri_files$lock)) l <- filelock::lock(pri_files$lock, exclusive = FALSE, private$lock_timeout) - if (is.null(l)) stop("Cannot acquire lock to copy PACKAGES files") + if (is.null(l)) { + stop("Cannot acquire lock to copy PACKAGES files") + } on.exit(filelock::unlock(l), add = TRUE) ## Check if PACKAGES exist and current. It is OK if metadata is missing @@ -819,11 +859,17 @@ cmc__update_replica_pkgs <- function(self, private) { missing_pkgs_note <- function(pkgs, result) { bad <- vlapply(result[seq_len(nrow(pkgs))], inherits, "error") - if (!any(bad)) return() + if (!any(bad)) { + return() + } repo_name <- function(type, url) { - if (type == "cran") return("CRAN") - if (type == "bioc") return("Bioconductor") + if (type == "cran") { + return("CRAN") + } + if (type == "bioc") { + return("Bioconductor") + } sub("^https?://([^/]*).*$", "\\1", url) } @@ -857,7 +903,9 @@ missing_pkgs_note <- function(pkgs, result) { cmc__update_replica_rds <- function(self, private, alert) { "!!DEBUG Update replica RDS" - if (alert) sts <- cli::cli_process_start("Updating metadata database") + if (alert) { + sts <- cli::cli_process_start("Updating metadata database") + } rep_files <- private$get_cache_files("replica") data_list <- lapply_rows( @@ -880,7 +928,9 @@ cmc__update_replica_rds <- function(self, private, alert) { data_list <- data_list[!vlapply(data_list, is.null)] - if (length(data_list) == 0) stop("No metadata available") + if (length(data_list) == 0) { + stop("No metadata available") + } private$data <- merge_packages_data(.list = data_list) save_rds(private$data, rep_files$rds) @@ -889,7 +939,9 @@ cmc__update_replica_rds <- function(self, private, alert) { private$update_memory_cache() - if (alert) cli::cli_process_done(sts) + if (alert) { + cli::cli_process_done(sts) + } private$data } @@ -905,7 +957,9 @@ cmc__update_replica_rds <- function(self, private, alert) { cmc__update_primary <- function(self, private, rds, packages, lock) { "!!DEBUG Updata primary cache" - if (!rds && !packages) return() + if (!rds && !packages) { + return() + } pri_files <- private$get_cache_files("primary") rep_files <- private$get_cache_files("replica") @@ -913,7 +967,9 @@ cmc__update_primary <- function(self, private, rds, packages, lock) { if (lock) { mkdirp(dirname(pri_files$lock)) l <- filelock::lock(pri_files$lock, exclusive = TRUE, private$lock_timeout) - if (is.null(l)) stop("Cannot acquire lock to update primary cache") + if (is.null(l)) { + stop("Cannot acquire lock to update primary cache") + } on.exit(filelock::unlock(l), add = TRUE) } @@ -954,7 +1010,9 @@ cmc__copy_to_replica <- function(self, private, rds, pkgs, etags) { mkdirp(dirname(pri_files$lock)) l <- filelock::lock(pri_files$lock, exclusive = FALSE, private$lock_timeout) - if (is.null(l)) stop("Cannot acquire lock to copy primary cache") + if (is.null(l)) { + stop("Cannot acquire lock to copy primary cache") + } on.exit(filelock::unlock(l), add = TRUE) if (rds) { @@ -995,9 +1053,13 @@ extract_deps <- function(pkgs, packages, dependencies, recursive) { pkgs$deps$package[pkgs$deps$upstream %in% new & pkgs$deps$type %in% dep], packages ) - if (!length(new)) break + if (!length(new)) { + break + } packages <- c(packages, new) - if (!recursive) break + if (!recursive) { + break + } dep <- tolower(realdep$indirect) } @@ -1021,9 +1083,13 @@ extract_revdeps <- function(pkgs, packages, dependencies, recursive) { pkgs$deps$upstream[pkgs$deps$ref %in% new & pkgs$deps$type %in% dep], packages ) - if (!length(new)) break + if (!length(new)) { + break + } packages <- c(packages, new) - if (!recursive) break + if (!recursive) { + break + } dep <- tolower(realdep$indirect) } diff --git a/src/library/pkgcache/R/onload.R b/src/library/pkgcache/R/onload.R index 7d69377cb4..1c679b4bcf 100644 --- a/src/library/pkgcache/R/onload.R +++ b/src/library/pkgcache/R/onload.R @@ -1,6 +1,7 @@ ## nocov start pkgenv <- new.env(parent = emptyenv()) +pkgenv$ppm_sso_cache <- new.env(parent = emptyenv()) pkgenv$r_versions <- list( list(version = "0.60", date = "1997-12-04T08:47:58.000000Z"), @@ -254,6 +255,7 @@ pkgenv$ppm_distros_cached <- focal linux focal ubuntu 20.04 TRUE jammy linux jammy ubuntu 22.04 TRUE noble linux noble ubuntu 24.04 TRUE + resolute linux resolute ubuntu 26.04 TRUE buster linux buster debian 10 FALSE bullseye linux bullseye debian 11 TRUE bookworm linux bookworm debian 12 TRUE diff --git a/src/library/pkgcache/R/package-cache.R b/src/library/pkgcache/R/package-cache.R index cf1fcec77e..6264fe7c4f 100644 --- a/src/library/pkgcache/R/package-cache.R +++ b/src/library/pkgcache/R/package-cache.R @@ -158,7 +158,9 @@ package_cache <- R6Class( # updates for unexpected PPM binaries and sources # need to update 'path', 'platform', 'sha256' - if (is.list(.headers)) .headers <- .headers[[1]] + if (is.list(.headers)) { + .headers <- .headers[[1]] + } .headers <- tolower(.headers) if ("x-repository-type: rspm" %in% .headers) { fields <- update_fields_for_ppm_download(path, extra, .headers) @@ -341,8 +343,9 @@ package_cache <- R6Class( on_progress http_headers async_constant()$then( - function() + function() { self$copy_to(target, url = urls[1], path = path, ..., .list = .list) + } )$then(function(res) { if (!nrow(res)) { ## Not in the cache, download and add it @@ -469,7 +472,9 @@ create_empty_db_file_if_needed <- function(path) { mkdirp(path) dbfile <- get_db_file(path) - if (file.exists(dbfile)) return() + if (file.exists(dbfile)) { + return() + } lockfile <- get_lock_file(path) @@ -495,18 +500,24 @@ make_empty_db_data_frame <- function() { update_fields_for_ppm_download <- function(path, extra, headers) { res <- list(path = path, extra = extra) pkg_type <- grep("^x-package-type:", headers, value = TRUE)[1] - if (is.na(pkg_type)) return(res) + if (is.na(pkg_type)) { + return(res) + } pkg_type <- sub("^x-package-type: ?", "", pkg_type) if (pkg_type == "binary") { # Got a binary package, check what kind bin_tag <- grep("x-package-binary-tag:", headers, value = TRUE)[1] - if (is.na(bin_tag)) return(res) + if (is.na(bin_tag)) { + return(res) + } bin_tag <- sub("x-package-binary-tag: ?", "", bin_tag) synchronise(async_get_ppm_status()) rver <- strsplit(bin_tag, "-")[[1]][[1]] binurl <- strsplit(bin_tag, "-")[[1]][[2]] - if (!binurl %in% pkgenv$ppm_distros$binary_url) return(res) + if (!binurl %in% pkgenv$ppm_distros$binary_url) { + return(res) + } # fix platform if neeeded if (!is.null(extra$platform) && extra$platform == "source") { diff --git a/src/library/pkgcache/R/platform-linux.R b/src/library/pkgcache/R/platform-linux.R index 6607215866..12e9d39fb8 100644 --- a/src/library/pkgcache/R/platform-linux.R +++ b/src/library/pkgcache/R/platform-linux.R @@ -57,9 +57,13 @@ remove_quotes <- function(x) { parse_os_release <- function(lines) { id <- grep("^ID=", lines, value = TRUE)[1] - if (is.na(id)) return(unknown_dist()) + if (is.na(id)) { + return(unknown_dist()) + } id <- trimws(sub("^ID=(.*)$", "\\1", id, perl = TRUE)) - if (is_quoted(id)) id <- remove_quotes(id) + if (is_quoted(id)) { + id <- remove_quotes(id) + } ver <- grep("^VERSION_ID=", lines, value = TRUE)[1] if (!is.na(ver)) { @@ -71,7 +75,9 @@ parse_os_release <- function(lines) { stringsAsFactors = FALSE, distribution = id ) - if (!is.na(ver)) out$release <- ver + if (!is.na(ver)) { + out$release <- ver + } if (is.na(ver) && id == "debian") { pn <- grep("^PRETTY_NAME=", lines, value = TRUE)[1] @@ -86,7 +92,9 @@ parse_os_release <- function(lines) { parse_redhat_release <- function(lines) { pcs <- strsplit(lines[1], " ", fixed = TRUE)[[1]] id <- tolower(pcs[1]) - if (id == "" || is.na(id)) return(unknown_dist()) + if (id == "" || is.na(id)) { + return(unknown_dist()) + } wver <- grepl("^[-\\.0-9]+$", pcs) @@ -94,7 +102,9 @@ parse_redhat_release <- function(lines) { stringsAsFactors = FALSE, distribution = id ) - if (any(wver)) out$release <- pcs[wver][1] + if (any(wver)) { + out$release <- pcs[wver][1] + } out } diff --git a/src/library/pkgcache/R/ppm-sso-app.R b/src/library/pkgcache/R/ppm-sso-app.R new file mode 100644 index 0000000000..376115320c --- /dev/null +++ b/src/library/pkgcache/R/ppm-sso-app.R @@ -0,0 +1,270 @@ +# nocov start + +# Fake PPM server that proxies to Auth0, for testing ppm_sso_device_flow(). +# Auth0 device flow does not use PKCE, so we verify the PKCE challenge +# locally and forward only the device_code to Auth0's /oauth/token. +ppm_sso_auth0_app <- function( + auth0_domain, + client_id, + audience = NULL, + scope = "openid profile email" +) { + app <- webfakes::new_app() + + app$use("logger" = webfakes::mw_log()) + app$use("urlencoded body parser" = webfakes::mw_urlencoded()) + app$use("json body parser" = webfakes::mw_json()) + + app$locals$challenges <- new.env(parent = emptyenv()) + app$locals$auth0_domain <- auth0_domain + app$locals$client_id <- client_id + app$locals$audience <- audience + app$locals$scope <- scope + + # Bearer-token check used by ppm_sso_can_authenticate(): any token passes. + app$get("/", function(req, res) { + res$set_status(200L)$send("ok") + }) + + app$post("/__api__/device", function(req, res) { + challenge <- req$form$code_challenge + method <- req$form$code_challenge_method %||% "S256" + if (!identical(method, "S256")) { + return(res$set_status(400L)$send_json( + auto_unbox = TRUE, + list(error = "unsupported_challenge_method") + )) + } + + payload <- list( + client_id = app$locals$client_id, + scope = app$locals$scope, + audience = app$locals$audience + ) + + upstream <- ppm_sso_post_form( + paste0("https://", app$locals$auth0_domain, "/oauth/device/code"), + payload + ) + + if (upstream$status >= 400L) { + return(res$set_status(upstream$status)$send_json( + auto_unbox = TRUE, + upstream$body + )) + } + + assign(upstream$body$device_code, challenge, envir = app$locals$challenges) + + res$send_json( + auto_unbox = TRUE, + list( + device_code = upstream$body$device_code, + user_code = upstream$body$user_code, + verification_uri = upstream$body$verification_uri, + verification_uri_complete = upstream$body$verification_uri_complete, + expires_in = upstream$body$expires_in, + interval = upstream$body$interval %||% 5L + ) + ) + }) + + app$post("/__api__/device_access", function(req, res) { + device_code <- req$form$device_code + verifier <- req$form$code_verifier + + if (!exists(device_code, envir = app$locals$challenges, inherits = FALSE)) { + return(res$set_status(400L)$send_json( + auto_unbox = TRUE, + list(error = "expired_token") + )) + } + expected <- get( + device_code, + envir = app$locals$challenges, + inherits = FALSE + ) + actual <- ppm_sso_base64url_encode(ppm_sso_sha256_raw(verifier)) + if (!identical(expected, actual)) { + return(res$set_status(400L)$send_json( + auto_unbox = TRUE, + list(error = "invalid_grant") + )) + } + + upstream <- ppm_sso_post_form( + paste0("https://", app$locals$auth0_domain, "/oauth/token"), + list( + grant_type = "urn:ietf:params:oauth:grant-type:device_code", + device_code = device_code, + client_id = app$locals$client_id + ) + ) + + if (upstream$status == 200L) { + rm(list = device_code, envir = app$locals$challenges) + return(res$send_json( + auto_unbox = TRUE, + list(id_token = upstream$body$id_token) + )) + } + + # Auth0 returns 403 for authorization_pending / slow_down; the PPM client + # only treats 400 as a soft pending state, so translate the status. + res$set_status(400L)$send_json( + auto_unbox = TRUE, + list(error = upstream$body$error %||% "unknown_error") + ) + }) + + # Trivial token exchange: echo subject_token back as access_token. + app$post("/__api__/token", function(req, res) { + if ( + !identical( + req$form$grant_type, + "urn:ietf:params:oauth:grant-type:token-exchange" + ) + ) { + return(res$set_status(400L)$send_json( + auto_unbox = TRUE, + list(error = "unsupported_grant_type") + )) + } + res$send_json( + auto_unbox = TRUE, + list( + access_token = req$form$subject_token, + token_type = "Bearer", + issued_token_type = "urn:ietf:params:oauth:token-type:access_token" + ) + ) + }) + + app +} + +ppm_sso_app <- function() { + app <- webfakes::new_app() + + app$use("logger" = webfakes::mw_log()) + app$use("urlencoded body parser" = webfakes::mw_urlencoded()) + app$use("json body parser" = webfakes::mw_json()) + + app$locals$challenges <- new.env(parent = emptyenv()) + + app$get("/", function(req, res) { + res$set_status(200L)$send("ok") + }) + + app$post("/__api__/device", function(req, res) { + challenge <- req$form$code_challenge + method <- req$form$code_challenge_method %||% "S256" + if (!identical(method, "S256")) { + return(res$set_status(400L)$send_json( + auto_unbox = TRUE, + list(error = "unsupported_challenge_method") + )) + } + + device_code <- ppm_sso_base64url_encode(.Call(pkgcache_rand_bytes, 32L)) + user_code <- "ABCD-EFGH" + verification_uri <- "https://example.invalid/activate" + + assign(device_code, challenge, envir = app$locals$challenges) + + res$send_json( + auto_unbox = TRUE, + list( + device_code = device_code, + user_code = user_code, + verification_uri = verification_uri, + verification_uri_complete = paste0( + verification_uri, + "?user_code=", + user_code + ), + expires_in = 300L, + interval = 1L + ) + ) + }) + + app$post("/__api__/device_access", function(req, res) { + device_code <- req$form$device_code + verifier <- req$form$code_verifier + + if (!exists(device_code, envir = app$locals$challenges, inherits = FALSE)) { + return(res$set_status(400L)$send_json( + auto_unbox = TRUE, + list(error = "expired_token") + )) + } + expected <- get( + device_code, + envir = app$locals$challenges, + inherits = FALSE + ) + actual <- ppm_sso_base64url_encode(ppm_sso_sha256_raw(verifier)) + if (!identical(expected, actual)) { + return(res$set_status(400L)$send_json( + auto_unbox = TRUE, + list(error = "invalid_grant") + )) + } + + rm(list = device_code, envir = app$locals$challenges) + res$send_json( + auto_unbox = TRUE, + list(id_token = ppm_sso_local_make_jwt()) + ) + }) + + app$post("/__api__/token", function(req, res) { + if ( + !identical( + req$form$grant_type, + "urn:ietf:params:oauth:grant-type:token-exchange" + ) + ) { + return(res$set_status(400L)$send_json( + auto_unbox = TRUE, + list(error = "unsupported_grant_type") + )) + } + res$send_json( + auto_unbox = TRUE, + list( + access_token = req$form$subject_token, + token_type = "Bearer", + issued_token_type = "urn:ietf:params:oauth:token-type:access_token" + ) + ) + }) + + app +} + +ppm_sso_local_make_jwt <- function( + iss = "https://ppm-sso-local.invalid/", + sub = "ppm-sso-local-user", + aud = "ppm-sso-local", + ttl = 3600L, + now = unclass(Sys.time()) +) { + header <- list(alg = "none", typ = "JWT") + payload <- list( + iss = iss, + sub = sub, + aud = aud, + iat = as.integer(now), + exp = as.integer(now + ttl) + ) + enc <- function(x) { + ppm_sso_base64url_encode(charToRaw( + jsonlite::toJSON(x, auto_unbox = TRUE) + )) + } + paste0(enc(header), ".", enc(payload), ".") +} + +# nocov end diff --git a/src/library/pkgcache/R/ppm-sso.R b/src/library/pkgcache/R/ppm-sso.R new file mode 100644 index 0000000000..43e797a15a --- /dev/null +++ b/src/library/pkgcache/R/ppm-sso.R @@ -0,0 +1,648 @@ +#' Posit Package Manager single sign-on (SSO) authentication +#' +#' @details +#' ## Set up SSO authentication: +#' - Set the `PACKAGEMANAGER_ADDRESS` environment variable to the URL of +#' your RStudio Package Manager instance. For example, add this line to +#' your `.Renviron` file: +#' ``` +#' PACKAGEMANAGER_ADDRESS=https:// +#' ``` +#' Alternatively, you can also set it in your shell profile on Unix, +#' or in the System or User environment variables on Windows. +#' - Set `options(repos)` to include a repository from your Package Manager +#' instance. Include `__token__` as the username in the URL. For example: +#' ``` +#' options(repos = c( +#' PPM = "https://__token__@/", +#' getOption("repos") +#' )) +#' ``` +#' You probably want to add this to your `.Rprofile` file, so that it is +#' set in every R session. +#' - Call [repo_get()] to trigger authentication and caching of the token. +#' You should be prompted to log in via your browser, and the obtained +#' token will be cached for future use. Call [ppm_sso_status()] to check +#' the status of your authentication, including the path of the cached +#' token and its expiration time. +#' - Alternatively, you can call `ppm_sso_login()` directly to trigger +#' the login process directly. +#' +#' `ppm_sso_login()` initiates the SSO login process. You should be +#' prompted to log in via your browser, and the obtained token will be +#' cached for future use. +#' +#' @return `ppm_sso_login()` returns the obtained token invisibly. +#' +#' @seealso +#' @export +#' @examplesIf FALSE +#' Sys.setenv(PACKAGEMANAGER_ADDRESS = "https://") +#' options(repos = c( +#' PPM = "https://__token__@/", +#' getOption("repos") +#' )) +#' ppm_sso_login() +#' ppm_sso_status() +#' ppm_sso_status(connect = TRUE) +#' ppm_sso_logout() + +ppm_sso_login <- function() { + ppm_url <- Sys.getenv("PACKAGEMANAGER_ADDRESS", NA_character_) + + identity_token <- ppm_sso_get_identity_token_from_file() %||% + ppm_sso_device_flow(ppm_url) + ppm_token <- ppm_sso_identity_to_ppm_token(ppm_url, identity_token) + ppm_sso_write_token_to_file(ppm_url, ppm_token) + + invisible(ppm_token) +} + +#' @rdname ppm_sso_login +#' @details +#' `ppm_sso_logout()` removes the cached token, effectively logging you +#' out. If there is no cached token, it does nothing. +#' @return `ppm_sso_logout()` does not return anything. +#' @export + +ppm_sso_logout <- function() { + ppm_url <- Sys.getenv("PACKAGEMANAGER_ADDRESS", NA_character_) + + # remove from cache if there + try_catch_null(suppressWarnings(rm( + list = ppm_url, + envir = pkgenv$ppm_sso_cache, + inherits = FALSE + ))) + parsed <- parse_url(ppm_url) + try_catch_null(suppressWarnings(rm( + list = parsed$host, + envir = pkgenv$credentials, + inherits = FALSE + ))) + + token_file_path <- ppm_sso_token_path() + if (!file.exists(token_file_path)) { + return(invisible()) + } + tokens <- try_catch_null({ + tokens <- suppressWarnings(tstoml::ts_read_toml(token_file_path)) + urls <- ts::ts_tree_unserialize( + ts::ts_tree_select(tokens, list("connections", TRUE, "address")) + ) + idx <- which(urls == ppm_url)[1] + tokens + }) + + if (is.na(idx)) { + return(invisible()) + } + + tokens <- ts::ts_tree_delete( + ts::ts_tree_select(tokens, list("connections", idx)) + ) + + ts::ts_tree_write(tokens, token_file_path) + + invisible() +} + +#' @rdname ppm_sso_login +#' @param connect If `TRUE`, also checks if the token is valid by making a test +#' request to the Package Manager instance. This requires an active internet +#' connection and may take a few seconds. If `FALSE`, only checks if a +#' token is cached and not expired. +#' @details +#' `ppm_sso_status()` checks the status of your authentication, including +#' the path of the cached token and its expiration time. +#' @return `ppm_sso_status()` returns a list with the following components: +#' - `ppm_url`: The URL of the Package Manager instance. +#' - `token_file`: The path of the cached token file. +#' - `token`: The cached token (partially masked for display) or `NA` if +#' no token is found locally. +#' - `valid`: `TRUE` if the token is valid (only if `connect = TRUE`), +#' `FALSE` if invalid, or `NA` if not checked. +#' - `issuer`: The issuer of the token, or `NA` if not available. +#' - `subject`: The subject of the token, or `NA` if not available. +#' - `audience`: The audience of the token, or `NA` if not available. +#' - `issued_at`: The issue time of the token as a POSIXct object, or `NA` +#' if not available. +#' - `expires_at`: The expiration time of the token as a POSIXct object, +#' or `NA` if not available. +#' - `expired`: `TRUE` if the token is expired, `FALSE` if not expired, +#' or `NA` if expiration time is not available. +#' - `expires_in`: The time until expiration as a difftime object, or +#' `NA` if expiration time is not available or the token is already +#' expired. +#' @export + +ppm_sso_status <- function(connect = FALSE) { + ppm_url <- Sys.getenv("PACKAGEMANAGER_ADDRESS", NA_character_) + ppm_sso_check_url(ppm_url) + token <- ppm_sso_get_cached_token(ppm_url, alive = TRUE) %||% + ppm_sso_get_existing_token(ppm_url, valid = FALSE) + + jwt <- token %&&% jwt_split(token) + iat <- .POSIXct(jwt$payload$iat %||% NA_real_) + exp <- .POSIXct(jwt$payload$exp %||% NA_real_) + now <- Sys.time() + auth <- if (connect) { + token %&&% + try_catch_null(ppm_sso_can_authenticate(ppm_url, token)) %||% + FALSE + } else { + NA + } + + structure( + list( + ppm_url = ppm_url, + token_file = ppm_sso_token_path(), + token = token %||% NA_character_, + valid = auth, + issuer = jwt$payload$iss %||% NA_character_, + subject = jwt$payload$sub %||% NA_character_, + audience = jwt$payload$aud %||% NA_character_, + issued_at = iat, + expires_at = exp, + expired = exp < now, + expires_in = if (!is.na(exp) && now < exp) { + exp - now + } else { + as.difftime(NA_real_, units = "secs") + } + ), + class = "ppm_sso_status" + ) +} + +jwt_split <- function(jwt) { + input <- strsplit(jwt, ".", fixed = TRUE)[[1]] + stopifnot(length(input) %in% c(2, 3)) + header <- jsonlite::fromJSON(rawToChar(ppm_sso_base64url_decode(input[1]))) + if (length(header$typ)) { + stopifnot(toupper(header$typ) == "JWT") + } + if (is.na(input[3])) { + input[3] = "" + } + sig <- ppm_sso_base64url_decode(input[3]) + payload <- jsonlite::fromJSON(rawToChar(ppm_sso_base64url_decode(input[2]))) + data <- charToRaw(paste(input[1:2], collapse = ".")) + if (!grepl("^none|EdDSA|[HRE]S(256|384|512)$", header$alg)) { + stop("Invalid algorithm: ", header$alg) + } + if (grepl(".S\\d\\d\\d", header$alg)) { + type <- match.arg(substring(header$alg, 1, 1), c("HMAC", "RSA", "ECDSA")) + keysize <- as.numeric(substring(header$alg, 3)) + } else { + type <- header$alg + keysize = NULL + } + list( + type = type, + keysize = keysize, + data = data, + sig = sig, + payload = payload, + header = header + ) +} + +#' @export + +print.ppm_sso_status <- function(x, ...) { + writeLines(format(x, ...)) + invisible(x) +} + +#' @export + +format.ppm_sso_status <- function(x, ...) { + token <- if (!is.na(x$token)) { + paste0( + substr(x$token, 1, 3), + "...", + substr(x$token, nchar(x$token) - 3, nchar(x$token)) + ) + } else { + NA_character_ + } + key <- function(x) { + cli::col_cyan(x) + } + url <- function(x) { + if (!is.na(x) && startsWith(x, "http")) { + cli::style_hyperlink(x, x) + } else { + x + } + } + tick <- function(x, invert = FALSE) { + txt <- if (isTRUE(x)) { + "yes" + } else if (isFALSE(x)) { + "no" + } else { + "?" + } + if (invert) { + x <- !x + } + if (isTRUE(x)) { + cli::col_green(txt) + } else if (isFALSE(x)) { + cli::col_magenta(txt) + } else { + txt + } + } + ein <- if (is.na(x$expires_in)) "-" else format_time$pretty_dt(x$expires_in) + c( + cli::rule("PPM SSO Status"), + paste(key("PPM URL: "), url(x$ppm_url)), + paste(key("Token file: "), x$token_file), + paste(key("Token: "), token), + paste(key("Valid: "), tick(x$valid)), + paste(key("Issuer: "), url(x$issuer)), + paste(key("Subject: "), x$subject), + paste(key("Audience: "), x$audience), + paste(key("Issued at: "), x$issued_at), + paste(key("Expires at: "), x$expires_at), + paste(key("Expired: "), tick(x$expired, invert = TRUE)), + paste(key("Expires in: "), ein), + NULL + ) +} + + +ppm_sso_check_url <- function(ppm_url) { + if (is.na(ppm_url)) { + stop( + "Please set the PACKAGEMANAGER_ADDRESS environment variable to ", + "the URL of your RStudio Package Manager instance." + ) + } + + if (is.na(parse_url(ppm_url)$host)) { + stop( + "The PACKAGEMANAGER_ADDRESS environment variable must be a valid URL, ", + "but got: ", + ppm_url + ) + } +} + +ppm_sso_auth <- function(repo) { + ppm_url <- Sys.getenv("PACKAGEMANAGER_ADDRESS", NA_character_) + parsed <- tryCatch( + parse_url(repo), + error = function(e) { + stop("Failed to parse repository URL: ", repo) + } + ) + repo_host <- paste0(parsed$protocol, "://", parsed$host) + if (repo_host != ppm_url) { + stop( + "The repository URL (", + repo_host, + ") does not match the configured ", + "Package Manager URL (", + ppm_url, + ")." + ) + } + + token <- ppm_sso_get_cached_token(ppm_url, alive = TRUE) %||% + ppm_sso_get_existing_token(ppm_url, valid = TRUE) %||% + ppm_sso_login() + + pkgenv$ppm_sso_cache[[ppm_url]] <- token + + token +} + +ppm_sso_get_cached_token <- function(ppm_url, alive = TRUE) { + token <- pkgenv$ppm_sso_cache[[ppm_url]] + + # no token in cache + if (is.null(token)) { + return(NULL) + } + + # no need to test if token is live + if (!alive) { + return(token) + } + + # no expiration date + jwt <- jwt_split(token) + exp <- jwt$payload$exp + if (is.null(exp)) { + return(token) + } + + # check if token is still valid + if (.POSIXct(exp) > Sys.time()) { + return(token) + } + + # not valid any more, remove from cache + pkgenv$ppm_sso_cache[[ppm_url]] <- NULL + + NULL +} + +ppm_sso_post_form <- function(url, payload) { + payload <- payload[!vapply(payload, is.null, logical(1))] + body <- paste( + paste0( + curl::curl_escape(names(payload)), + "=", + curl::curl_escape(unlist(payload, use.names = FALSE)) + ), + collapse = "&" + ) + h <- curl::new_handle() + curl::handle_setheaders( + h, + "Content-Type" = "application/x-www-form-urlencoded" + ) + curl::handle_setopt(h, post = TRUE, postfields = body) + resp <- curl::curl_fetch_memory(url, handle = h) + list( + status = resp$status_code, + body = tryCatch( + jsonlite::fromJSON(rawToChar(resp$content), simplifyVector = FALSE), + error = function(e) { + resp$content + } + ) + ) +} + +ppm_sso_token_path <- function() { + file.path( + path.expand("~"), + ".ppm", + "tokens.toml" + ) +} + +ppm_sso_get_existing_token <- function(ppm_url, valid = TRUE) { + path <- ppm_sso_token_path() + try_catch_null({ + ts_tokens <- suppressWarnings(tstoml::ts_read_toml(path)) + for (conn in ts_tokens[[list("connections", TRUE)]]) { + if (identical(conn$address, ppm_url)) { + if (valid && !ppm_sso_can_authenticate(ppm_url, conn$token)) { + return(NULL) + } + return(conn$token) + } + } + }) +} + +ppm_sso_get_identity_token_from_file <- function() { + token_file <- Sys.getenv("PACKAGEMANAGER_IDENTITY_TOKEN_FILE", unset = NA) + if (is.na(token_file)) { + return(NULL) + } + try_catch_null({ + trimws(readLines(token_file, n = 1, warn = FALSE)) + }) +} + +ppm_sso_device_flow_init <- function(ppm_url) { + verifier <- ppm_sso_new_pkce_verifier() + challenge <- ppm_sso_new_pkce_challenge(verifier) + + # 1. Initiate Device Auth + init_url <- paste0(ppm_url, "/__api__/device") + payload <- list( + code_challenge_method = "S256", + code_challenge = challenge + ) + init_resp <- ppm_sso_post_form(init_url, payload) + if (init_resp$status >= 400) { + stop( + "Failed to initiate device authorization (HTTP ", + init_resp$status, + ")." + ) + } + init_resp_body <- init_resp$body + + display_uri <- init_resp_body$verification_uri_complete %||% + init_resp_body$verification_uri + if (is.null(display_uri)) { + stop("No verification URI found in device auth response.") + } + + list( + verifier = verifier, + display_uri = display_uri, + user_code = init_resp_body$user_code, + device_code = init_resp_body$device_code, + expires_in = init_resp_body$expires_in, + interval = init_resp_body$interval + ) +} + +ppm_sso_device_flow_message <- function(ppm_url, init_result) { + cli::cli_rule("PPM SSO Login") + cli::cli_text("Login at {.url {init_result$display_uri}}") + cli::cli_text( + "and enter code {.emph {cli::col_magenta(init_result$user_code)}} + when prompted." + ) + if (interactive()) { + readline("Press ENTER to open in browser...") + utils::browseURL(init_result$display_uri) + } else if (isTRUE(getOption("pak.is_worker"))) { + # called from pak, make the UI slightly nicer. + # unfortunately we cannot interact with the user here + utils::browseURL(init_result$display_uri) + } +} + +ppm_sso_device_flow <- function(ppm_url) { + init_result <- ppm_sso_device_flow_init(ppm_url) + ppm_sso_device_flow_message(ppm_url, init_result) + token <- ppm_sso_device_flow_complete(ppm_url, init_result) + if (is.null(token)) { + stop("Failed to complete device authorization or obtain identity token.") + } + token +} + +ppm_sso_can_authenticate <- function(ppm_url, token) { + h <- curl::new_handle() + curl::handle_setheaders(h, "Authorization" = paste("Bearer", token)) + resp <- curl::curl_fetch_memory(ppm_url, handle = h) + status <- resp$status_code + status < 500 && status != 401 && status != 403 +} + +ppm_sso_identity_to_ppm_token <- function(ppm_url, identity_token) { + url <- paste0(ppm_url, "/__api__/token") + payload <- list( + grant_type = "urn:ietf:params:oauth:grant-type:token-exchange", + subject_token = identity_token, + subject_token_type = "urn:ietf:params:oauth:token-type:id_token" + ) + + resp <- ppm_sso_post_form(url, payload) + if (resp$status >= 400) { + stop( + "Failed to exchange identity token for PPM token (HTTP ", + resp$status, + ")." + ) + } + + token_data <- resp$body + if (is.null(token_data$access_token)) { + stop("Failed to exchange identity token for PPM token.") + } + + token_data$access_token +} + +ppm_sso_write_token_to_file <- function(ppm_url, token) { + # this is more difficult than it should be because TOML is unable + # to represent an empty array of tables + token_file_path <- ppm_sso_token_path() + mkdirp(dirname(token_file_path)) + new_conn <- list( + address = ppm_url, + token = token, + auth_type = "sso" + ) + + tokens <- try_catch_null({ + tokens <- suppressWarnings(tstoml::ts_read_toml(token_file_path)) + urls <- ts::ts_tree_unserialize( + ts::ts_tree_select(tokens, list("connections", TRUE, "address")) + ) + idx <- which(urls == ppm_url)[1] + tokens + }) + + if (is.null(tokens)) { + tokens <- tstoml::ts_parse_toml("") + tokens <- ts::ts_tree_insert(tokens, key = "connections", list(new_conn)) + } else if (!is.na(idx)) { + tokens <- ts::ts_tree_update( + ts::ts_tree_select(tokens, list("connections", idx, "token")), + new_conn$token + ) + } else if (length(urls) == 0) { + tokens <- ts::ts_tree_insert(tokens, key = "connections", list(new_conn)) + } else { + tokens <- ts::ts_tree_insert( + ts::ts_tree_select(tokens, "connections"), + list(new_conn) + ) + } + + bytes <- as.raw(tokens) + file.create(token_file_path) + Sys.chmod(token_file_path, "600") + writeBin(bytes, token_file_path) +} + +ppm_sso_base64url_decode <- function(x) { + # Add padding if missing + padding_needed <- (4 - nchar(x) %% 4) %% 4 + x <- paste0(x, strrep("=", padding_needed)) + # Replace URL-safe characters + x <- gsub("-", "+", gsub("_", "/", x)) + processx::base64_decode(x) +} + +ppm_sso_base64url_encode <- function(x) { + encoded <- processx::base64_encode(x) + # Make it URL-safe + gsub("\\+", "-", gsub("\\/", "_", gsub("=+$", "", encoded))) +} + +ppm_sso_hex_to_raw <- function(s) { + n <- nchar(s) + as.raw(strtoi(substring(s, seq(1L, n, 2L), seq(2L, n, 2L)), 16L)) +} + +ppm_sso_sha256_raw <- function(x) { + ppm_sso_hex_to_raw(cli::hash_sha256(x)) +} + +ppm_sso_new_pkce_verifier <- function() { + ppm_sso_base64url_encode(.Call(pkgcache_rand_bytes, 32L)) +} + +ppm_sso_new_pkce_challenge <- function(verifier) { + ppm_sso_base64url_encode(ppm_sso_sha256_raw(verifier)) +} + +ppm_sso_device_flow_complete <- function(ppm_url, init_result) { + device_code <- init_result$device_code + verifier <- init_result$verifier + interval <- init_result$interval %||% 5 + expires_in <- init_result$expires_in %||% 300 + + url <- paste0(ppm_url, "/__api__/device_access") + start_time <- Sys.time() + payload <- list( + device_code = device_code, + code_verifier = verifier + ) + + # PPM might not respond until the user completes auth, so show this + oldopt <- options(cli.progress_show_after = 0) + on.exit(options(oldopt), add = TRUE) + cli::cli_progress_bar( + format = "{cli::pb_spin} Waiting for browser." + ) + cli::cli_progress_update() + + while (as.numeric(Sys.time() - start_time) < expires_in) { + resp <- ppm_sso_post_form(url, payload) + status <- resp$status + + if (status == 200) { + cli::cli_progress_done() + cli::cli_alert_success("Authorization successful.") + return(resp$body$id_token) + } else if (status == 400) { + error_code <- resp$body$error + if (error_code == "access_denied") { + cli::cli_progress_done() + cli::cli_alert_danger("Authorization denied by user.") + stop("Access denied by user.") + } + if (error_code == "expired_token") { + cli::cli_progress_done() + cli::cli_alert_danger("Device authorization request expired.") + stop("Device authorization request expired.") + } + # For "authorization_pending" or "slow_down", just wait and retry. + } else { + cli::cli_progress_done() + cli::cli_alert_danger( + "Device authorization failed (HTTP {status})." + ) + stop("Device authorization failed.") + } + + deadline <- Sys.time() + interval + while (Sys.time() < deadline) { + Sys.sleep(.1) + cli::cli_progress_update() + } + } + + cli::cli_progress_done() + cli::cli_alert_danger("Device authorization timed out.") + stop("Device authorization timed out.") +} diff --git a/src/library/pkgcache/R/ppm.R b/src/library/pkgcache/R/ppm.R index 035ce6290c..da3af1d641 100644 --- a/src/library/pkgcache/R/ppm.R +++ b/src/library/pkgcache/R/ppm.R @@ -244,7 +244,9 @@ ppm_has_binaries <- function() { current$cpu == "x86_64" && (current$os == "mingw32" || grepl("linux", current$os)) - if (!binaries) return(FALSE) + if (!binaries) { + return(FALSE) + } current_rver <- get_minor_r_version(getRversion()) synchronise(async_get_ppm_status( @@ -274,7 +276,9 @@ ppm_has_binaries <- function() { ppm_match_platform <- function(distros, plt) { which(vlapply(distros$platforms, function(dplts) { - if (plt %in% dplts) return(TRUE) + if (plt %in% dplts) { + return(TRUE) + } res <- grep("^/.*/$", dplts, value = TRUE) any(vlapply(res, function(re) { re <- sub("/$", "$", sub("^/", "^", re)) diff --git a/src/library/pkgcache/R/progress-bar.R b/src/library/pkgcache/R/progress-bar.R index 23fec4ffbe..79114a874e 100644 --- a/src/library/pkgcache/R/progress-bar.R +++ b/src/library/pkgcache/R/progress-bar.R @@ -19,8 +19,7 @@ create_progress_bar <- function(data) { bar$data$current <- NA_integer_ bar$timer <- async_timer$new(1 / 10, function() show_progress_bar(bar)) - bar$timer$listen_on("error", function(...) { - }) + bar$timer$listen_on("error", function(...) {}) bar } diff --git a/src/library/pkgcache/R/repo-set.R b/src/library/pkgcache/R/repo-set.R index de63b15366..28725a017a 100644 --- a/src/library/pkgcache/R/repo-set.R +++ b/src/library/pkgcache/R/repo-set.R @@ -92,13 +92,15 @@ repo_resolve <- function(spec, username = NULL) { repo_add <- function(..., .list = NULL, username = NULL) { repo_add_internal(..., .list = .list, username = username) - invisible(suppressMessages(repo_get())) + invisible(repo_get()) } repo_add_internal <- function(..., .list = NULL, username = NULL) { new <- c(list(...), .list) - if (length(new) == 0) return(invisible(repo_get())) + if (length(new) == 0) { + return(invisible(repo_get())) + } toadd <- unlist(mapply( repo_sugar, @@ -187,17 +189,23 @@ repo_sugar <- function(x, nm, username = NULL) { } repo_sugar_url <- function(x, nm) { - if (is.null(nm) || nm == "") nm <- "EXTRA" + if (is.null(nm) || nm == "") { + nm <- "EXTRA" + } structure(x, names = nm) } repo_sugar_path <- function(x, nm) { - if (is.null(nm) || nm == "") nm <- "LOCAL" + if (is.null(nm) || nm == "") { + nm <- "LOCAL" + } structure(x, names = nm) } repo_sugar_mran <- function(x, nm) { - if (is.null(nm) || nm == "") nm <- "CRAN" + if (is.null(nm) || nm == "") { + nm <- "CRAN" + } date <- parse_spec(sub("^MRAN@", "", x)) if (date < "2017-10-10") { stop("PPM snapshots go back to 2017-10-10 only") @@ -211,7 +219,9 @@ repo_sugar_mran <- function(x, nm) { } repo_sugar_ppm <- function(x, nm) { - if (is.null(nm) || nm == "") nm <- "CRAN" + if (is.null(nm) || nm == "") { + nm <- "CRAN" + } x <- sub("^PPM@", "", x) x <- sub("^RSPM@", "", x) date <- parse_spec(x) @@ -406,7 +416,7 @@ next_day <- function(x) { #' for details. #' * `MRAN@...` repository specifications now resolve to PPM, but note that #' PPM snapshots are only available from 2017-10-10. See more about this -#' at . +#' at . #' * All dates (or times) can be specified in the ISO 8601 format. #' * If PPM does not have a snapshot available for a date, the next #' available date is used. diff --git a/src/library/pkgcache/R/repo-status.R b/src/library/pkgcache/R/repo-status.R index 79d3f77875..33643a01d8 100644 --- a/src/library/pkgcache/R/repo-status.R +++ b/src/library/pkgcache/R/repo-status.R @@ -162,7 +162,9 @@ summary.pkgcache_repo_status <- function(object, ...) { ssm <- data_frame(repository = unique(key)) pls <- unique(object$platform) - for (pl in pls) ssm[[pl]] <- TRUE + for (pl in pls) { + ssm[[pl]] <- TRUE + } ssm$ping <- NA_real_ for (i in seq_len(nrow(ssm))) { diff --git a/src/library/pkgcache/R/utils.R b/src/library/pkgcache/R/utils.R index 5093370716..e5787c4d0d 100644 --- a/src/library/pkgcache/R/utils.R +++ b/src/library/pkgcache/R/utils.R @@ -2,6 +2,10 @@ repoman_data <- new.env(parent = emptyenv()) `%||%` <- function(l, r) if (is.null(l)) r else l +`%&&%` <- function(l, r) if (is.null(l)) NULL else r + +isFALSE <- function(x) is.logical(x) && length(x) == 1L && !is.na(x) && !x + vcapply <- function(X, FUN, ...) { vapply(X, FUN, FUN.VALUE = character(1), ...) } @@ -22,10 +26,16 @@ vdapply <- function(X, FUN, ...) { mapx <- function(...) { args <- list(...) - if (length(args) == 0) stop("No arguments to `mapx()`") + if (length(args) == 0) { + stop("No arguments to `mapx()`") + } fun <- args[[length(args)]] - if (!is.function(fun)) stop("Last `mapx()` argument not a function") - if (length(args) == 1) stop("No data to `mapx()`") + if (!is.function(fun)) { + stop("Last `mapx()` argument not a function") + } + if (length(args) == 1) { + stop("No data to `mapx()`") + } data <- args[-length(args)] lens <- setdiff(unique(viapply(data, length)), 1L) @@ -167,7 +177,9 @@ file.size <- function(...) { msg_wrap <- function(..., .space = TRUE) { ret <- paste(strwrap(paste0(...)), collapse = "\n") - if (.space) ret <- paste0("\n", ret, "\n") + if (.space) { + ret <- paste0("\n", ret, "\n") + } ret } diff --git a/src/library/pkgcache/inst/WORDLIST b/src/library/pkgcache/inst/WORDLIST index 44bd7d327e..1d08957d29 100644 --- a/src/library/pkgcache/inst/WORDLIST +++ b/src/library/pkgcache/inst/WORDLIST @@ -13,11 +13,13 @@ Encodings FreeBSD MRAN PBC +POSIXct README ROR RSPM RStudio SHA +SSO Solaris Sur UTF @@ -25,6 +27,7 @@ UUID archs async devel +difftime encodings funder html diff --git a/src/library/pkgcache/inst/fixtures/bioc-config.yaml b/src/library/pkgcache/inst/fixtures/bioc-config.yaml index b407aa5bc5..b61c55584f 100644 --- a/src/library/pkgcache/inst/fixtures/bioc-config.yaml +++ b/src/library/pkgcache/inst/fixtures/bioc-config.yaml @@ -8,12 +8,12 @@ production_deploy_root: webadmin@master.bioconductor.org:/extra/www/bioc staging_dir: /loc/www/bioconductor-test-new ## CHANGE THIS WHEN WE RELEASE A VERSION: -release_version: "3.22" -r_version_associated_with_release: "4.5.0" +release_version: "3.23" +r_version_associated_with_release: "4.6.0" r_version_associated_with_devel: "4.6.0" ## CHANGE THIS WHEN WE RELEASE A VERSION: -devel_version: "3.23" +devel_version: "3.24" ## CHANGE THIS WHEN WE ADD A VERSION: ## This is the list of all versions for which we want to generate "new" @@ -24,26 +24,26 @@ devel_version: "3.23" ## let the no-longer-release version build one last time so package ## landing pages won't say "release version"): versions: -- "3.22" - "3.23" +- "3.24" ## CHANGE THIS (i.e., uncomment) as various parts of the new devel version ## become available. set to "[]" if none are available. devel_repos: - "bioc" - "data/experiment" -- "workflows" -- "data/annotation" +#- "workflows" +#- "data/annotation" ## CHANGE this when the build machines change: ## also, don't include machines that are not building yet (comment them out) active_release_builders: - linux: "nebbiolo2" + linux: "nebbiolo1" mac_monterey: "lconway" mac_ventura: "kjohnson3" # windows: "palomino8" active_devel_builders: - linux: "nebbiolo1" + linux: "nebbiolo2" # windows: "palomino7" # mac_monterey: "merida1" @@ -166,6 +166,7 @@ r_ver_for_bioc_ver: "3.21": "4.5" "3.22": "4.5" "3.23": "4.6" + "3.24": "4.6" # UPDATE THIS when we release a version release_dates: # old info from http://en.wikipedia.org/wiki/Bioconductor#Milestones "1.0": "1/5/2001" @@ -216,6 +217,7 @@ release_dates: # old info from http://en.wikipedia.org/wiki/Bioconductor#Milesto "3.20": "10/30/2024" "3.21": "04/16/2025" "3.22": "10/30/2025" + "3.23": "04/29/2026" release_last_built_dates: '2.0': 09/12/2007 '2.1': 04/09/2008 @@ -254,6 +256,7 @@ release_last_built_dates: '3.19': 10/18/2024 '3.20': 04/02/2025 '3.21': 10/16/2025 + '3.22': 04/08/2026 mirrors: - 0-Bioconductor: - institution: Bioconductor, automatic redirection to servers worldwide diff --git a/src/library/pkgcache/src/init.c b/src/library/pkgcache/src/init.c index 97944f830c..9334b0ab03 100644 --- a/src/library/pkgcache/src/init.c +++ b/src/library/pkgcache/src/init.c @@ -30,6 +30,8 @@ static const R_CallMethodDef callMethods[] = { REG(pkgcache_parse_packages_raw, 1), REG(pkgcache_graphics_api_version, 0), + REG(pkgcache_rand_bytes, 1), + REG(pkgcache__gcov_flush, 0), { NULL, NULL, 0 } }; diff --git a/src/library/pkgcache/src/pkgcache.h b/src/library/pkgcache/src/pkgcache.h index de0922f5ec..05c0a16954 100644 --- a/src/library/pkgcache/src/pkgcache.h +++ b/src/library/pkgcache/src/pkgcache.h @@ -12,3 +12,5 @@ SEXP pkgcache_parse_descriptions(SEXP paths, SEXP lowercase); SEXP pkgcache_parse_packages_raw(SEXP raw); SEXP pkgcache_graphics_api_version(void); + +SEXP pkgcache_rand_bytes(SEXP n); diff --git a/src/library/pkgcache/src/rand.c b/src/library/pkgcache/src/rand.c new file mode 100644 index 0000000000..f3b67e811e --- /dev/null +++ b/src/library/pkgcache/src/rand.c @@ -0,0 +1,85 @@ +#include "pkgcache.h" + +#include + +#if defined(_WIN32) +# include +# define RtlGenRandom SystemFunction036 +# ifdef __cplusplus +extern "C" +# endif +BOOLEAN NTAPI RtlGenRandom(PVOID RandomBuffer, ULONG RandomBufferLength); +# pragma comment(lib, "advapi32.lib") +#elif defined(__APPLE__) || defined(__FreeBSD__) || defined(__OpenBSD__) || \ + defined(__NetBSD__) || defined(__DragonFly__) +# include +#else +# include +# include +# include +# if defined(__linux__) +# include +# endif +#endif + +SEXP pkgcache_rand_bytes(SEXP n) { + int size = Rf_asInteger(n); + if (size == NA_INTEGER || size < 0) { + Rf_error("Invalid number of random bytes requested"); + } + SEXP res = PROTECT(Rf_allocVector(RAWSXP, size)); + if (size == 0) { + UNPROTECT(1); + return res; + } + unsigned char *buf = RAW(res); + +#if defined(_WIN32) + if (!RtlGenRandom((PVOID) buf, (ULONG) size)) { + Rf_error("Failed to obtain random bytes from RtlGenRandom"); + } + +#elif defined(__APPLE__) || defined(__FreeBSD__) || defined(__OpenBSD__) || \ + defined(__NetBSD__) || defined(__DragonFly__) + arc4random_buf(buf, (size_t) size); + +#else + size_t off = 0; +# if defined(__linux__) && defined(SYS_getrandom) + while (off < (size_t) size) { + long r = syscall(SYS_getrandom, buf + off, (size_t) size - off, 0); + if (r > 0) { + off += (size_t) r; + } else if (r < 0 && (errno == EINTR || errno == EAGAIN)) { + continue; + } else { + break; /* fall through to /dev/urandom */ + } + } +# endif + if (off < (size_t) size) { + int fd; + do { + fd = open("/dev/urandom", O_RDONLY); + } while (fd < 0 && errno == EINTR); + if (fd < 0) { + Rf_error("Failed to open /dev/urandom: %s", strerror(errno)); + } + while (off < (size_t) size) { + ssize_t r = read(fd, buf + off, (size_t) size - off); + if (r > 0) { + off += (size_t) r; + } else if (r < 0 && errno == EINTR) { + continue; + } else { + close(fd); + Rf_error("Failed to read from /dev/urandom: %s", strerror(errno)); + } + } + close(fd); + } +#endif + + UNPROTECT(1); + return res; +} diff --git a/src/library/pkgdepends/DESCRIPTION b/src/library/pkgdepends/DESCRIPTION index eb3791cdf8..f1d3826b39 100644 --- a/src/library/pkgdepends/DESCRIPTION +++ b/src/library/pkgdepends/DESCRIPTION @@ -34,9 +34,9 @@ Config/Needs/website: r-lib/asciicast, pkgdown (>= 2.0.2), Config/testthat/edition: 3 Config/usethis/last-upkeep: 2025-05-05 Encoding: UTF-8 -RoxygenNote: 7.3.2.9000 +RoxygenNote: 7.3.3 NeedsCompilation: yes -Packaged: 2026-04-23 10:52:26 UTC; gaborcsardi +Packaged: 2026-05-14 11:57:06 UTC; gaborcsardi Author: Gábor Csárdi [aut, cre], Posit Software, PBC [cph, fnd] (ROR: ) Maintainer: Gábor Csárdi diff --git a/src/library/pkgdepends/R/docs.R b/src/library/pkgdepends/R/docs.R index dd09b63912..c6fc232285 100644 --- a/src/library/pkgdepends/R/docs.R +++ b/src/library/pkgdepends/R/docs.R @@ -71,8 +71,8 @@ generate_config_docs <- function() { alldocs } -doc_share_rmd <- function(rmd, rds) { - md <- sub("[.]Rmd$", ".md", rmd) +doc_share_rmd <- function(rmd, rds, md = NULL) { + md <- md %||% sub("[.]Rmd$", ".md", rmd) if (md == rmd) { stop("Docs Rmd file must have extension `.Rmd`") } diff --git a/src/library/pkgdepends/R/download.R b/src/library/pkgdepends/R/download.R index 5f39539107..d9acf4d47d 100644 --- a/src/library/pkgdepends/R/download.R +++ b/src/library/pkgdepends/R/download.R @@ -11,7 +11,7 @@ #' #' ```{r child = {options(rx_downloads = TRUE); "tools/doc/resolution-result.Rmd" }} #' ``` -#' `r { options(rx_downloads = TRUE); doc_share_rmd("tools/doc/resolution-result.Rmd", "inst/docs/download-result.rds")}` +#' `r { options(rx_downloads = TRUE); doc_share_rmd("tools/doc/resolution-result.Rmd", "inst/docs/download-result.rds", "tools/doc/download-result.md")}` #' #' @name pkg_downloads #' @aliases pkg_download_result diff --git a/src/library/pkgdepends/R/errors.R b/src/library/pkgdepends/R/errors.R index 35112e4ca0..d336ad91b2 100644 --- a/src/library/pkgdepends/R/errors.R +++ b/src/library/pkgdepends/R/errors.R @@ -167,13 +167,13 @@ err <- local({ #' #' @noRd #' @param ... Parts of the error message, they will be converted to - #' character and then concatenated, like in [stop()]. + #' character and then concatenated, like in `stop()`. #' @param call. A call object to include in the condition, or `TRUE` - #' or `NULL`, meaning that [throw()] should add a call object + #' or `NULL`, meaning that `throw()` should add a call object #' automatically. If `FALSE`, then no call is added. #' @param srcref Alternative source reference object to use instead of #' the one of `call.`. - #' @param domain Translation domain, see [stop()]. We set this to + #' @param domain Translation domain, see `stop()`. We set this to #' `NA` by default, which means that no translation occurs. This #' has the benefit that the error message is not re-encoded into #' the native locale. @@ -193,10 +193,10 @@ err <- local({ #' It also adds the `rlib_error` class. #' #' @noRd - #' @param ... Passed to [new_cond()]. - #' @param call. Passed to [new_cond()]. - #' @param srcref Passed tp [new_cond()]. - #' @param domain Passed to [new_cond()]. + #' @param ... Passed to `new_cond()`. + #' @param call. Passed to `new_cond()`. + #' @param srcref Passed tp `new_cond()`. + #' @param domain Passed to `new_cond()`. #' @return Error condition object with classes `rlib_error`, `error` #' and `condition`. @@ -210,13 +210,13 @@ err <- local({ #' Throw a condition #' - #' If the condition is an error, it will also call [stop()], after + #' If the condition is an error, it will also call `stop()`, after #' signalling the condition first. This means that if the condition is - #' caught by an exiting handler, then [stop()] is not called. + #' caught by an exiting handler, then `stop()` is not called. #' #' @noRd #' @param cond Condition object to throw. If it is an error condition, - #' then it calls [stop()]. + #' then it calls `stop()`. #' @param parent Parent condition. #' @param frame The throwing context. Can be used to hide frames from #' the backtrace. @@ -340,7 +340,7 @@ err <- local({ #' @noRd #' @param expr Expression to evaluate. #' @param err Error object or message to use for the child error. - #' @param call Call to use in the re-thrown error. See [throw()]. + #' @param call Call to use in the re-thrown error. See `throw()`. chain_error <- function(expr, err, call = sys.call(-1), srcref = NULL) { .hide_from_trace <- 1 @@ -371,8 +371,8 @@ err <- local({ #' adds the `c_error` class. #' #' @noRd - #' @param .NAME Compiled function to call, see [.Call()]. - #' @param ... Function arguments, see [.Call()]. + #' @param .NAME Compiled function to call, see `.Call()`. + #' @param ... Function arguments, see `.Call()`. #' @return Result of the call. chain_call <- function(.NAME, ...) { @@ -406,14 +406,14 @@ err <- local({ #' Version of entrace_call that supports cleancall #' - #' This function is the same as [entrace_call()], except that it - #' uses cleancall's [.Call()] wrapper, to enable resource cleanup. + #' This function is the same as `entrace_call()`, except that it + #' uses cleancall's `.Call()` wrapper, to enable resource cleanup. #' See https://github.com/r-lib/cleancall#readme for more about #' resource cleanup. #' #' @noRd - #' @param .NAME Compiled function to call, see [.Call()]. - #' @param ... Function arguments, see [.Call()]. + #' @param .NAME Compiled function to call, see `.Call()`. + #' @param ... Function arguments, see `.Call()`. #' @return Result of the call. chain_clean_call <- function(.NAME, ...) { @@ -447,7 +447,7 @@ err <- local({ #' Create a traceback #' - #' [throw()] calls this function automatically if an error is not caught, + #' `throw()` calls this function automatically if an error is not caught, #' so there is currently not much use to call it directly. #' #' @param cond Condition to add the trace to diff --git a/src/library/pkgdepends/R/git-auth.R b/src/library/pkgdepends/R/git-auth.R index 30c7fca289..43619d9778 100644 --- a/src/library/pkgdepends/R/git-auth.R +++ b/src/library/pkgdepends/R/git-auth.R @@ -335,7 +335,7 @@ gitcreds <- local({ #' Run a `git credential` command #' #' @details - #' We set the [gitcreds_env()] environment variables, to avoid dialog boxes + #' We set the `gitcreds_env()` environment variables, to avoid dialog boxes #' from some credential helpers and also validation that potentiall needs #' an internet connection. #' @@ -347,7 +347,7 @@ gitcreds <- local({ #' @return Standard output, line by line. #' #' @noRd - #' @seealso [git_run()]. + #' @seealso `git_run()`. gitcreds_run <- function(command, input, args = character()) { env <- gitcreds_env() @@ -380,7 +380,7 @@ gitcreds <- local({ #' * `stderr`: the standard error of the command, line by line. #' #' @param args Command line arguments. - #' @param input The standard input (the `input` argument of [system2()]. + #' @param input The standard input (the `input` argument of `system2()`. #' @noRd #' @return Standard output, line by line. @@ -429,7 +429,7 @@ gitcreds <- local({ #' credentials. `TRUE` for replacing/deleting them. #' #' @noRd - #' @seealso [gitcreds_set()]. + #' @seealso `gitcreds_set()`. ack <- function(url, current, what = "Replace") { msg("\n-> Your current credentials for ", squote(url), ":\n") @@ -777,7 +777,7 @@ gitcreds <- local({ `%||%` <- function(l, r) if (is.null(l)) r else l - #' Like [message()], but print to standard output in interactive + #' Like `message()`, but print to standard output in interactive #' sessions #' #' To avoid red output in RStudio, RGui, and R.app. @@ -856,7 +856,7 @@ gitcreds <- local({ #' Read all of a file #' #' @param path File to read. - #' @param ... Passed to [readChar()]. + #' @param ... Passed to `readChar()`. #' @noRd #' @return String. diff --git a/src/library/pkgdepends/R/git-protocol.R b/src/library/pkgdepends/R/git-protocol.R index 11bbf8ffdf..19b3882c71 100644 --- a/src/library/pkgdepends/R/git-protocol.R +++ b/src/library/pkgdepends/R/git-protocol.R @@ -675,7 +675,7 @@ raw_as_utf8 <- function(x) { #' git protocol docs for what these are. #' * `data`: for `data-pkt` lines this is a raw vector of the data. #' * `text`: for `data-pkt` lines that are text, this is the text of the -#' data. We use [raw_as_utf8()] to convert raw data to text, and sometimes +#' data. We use `raw_as_utf8()` to convert raw data to text, and sometimes #' it might interpret binary data as text, especially if the data is #' short. So this field is for convenience only. #' @@ -815,7 +815,7 @@ git_create_message_v1 <- function(args = character(), caps = character()) { #' #' @inheritParams git_list_refs #' @inheritParams git_create_message_v2 -#' @return Response from git, already parsed with [git_parse_message()]. +#' @return Response from git, already parsed with `git_parse_message()`. #' #' @noRd @@ -927,7 +927,7 @@ pkt_line <- function(payload, ...) { #' queries. #' #' @inheritParams git_list_refs -#' @return Same as [git_list_refs()]. +#' @return Same as `git_list_refs()`. #' @noRd git_list_refs_v1 <- function(url) { @@ -953,7 +953,7 @@ async_git_list_refs_v1 <- function(url) { #' #' @param response Response from git, as returned by `async::http_get()`, which #' is the same as the object from `curl::curl_fetch_memory()`. -#' @return Same as [git_list_refs()]. +#' @return Same as `git_list_refs()`. #' @noRd git_list_refs_v1_process <- function(response, url) { @@ -1057,7 +1057,7 @@ git_parse_pkt_line_refs <- function(lines, url) { #' interested in a subset of refs. #' #' @inheritParams git_list_refs -#' @return Same as [git_list_refs()]. +#' @return Same as `git_list_refs()`. #' @noRd git_list_refs_v2 <- function(url, prefixes = character()) { @@ -1184,7 +1184,7 @@ check_initial_response <- function(psd, url) { #' @param reply The parsed message from the server. #' @param caps Capabilities that are passed in from the response to #' the initial client request. -#' @return Same as [git_list_refs()]. +#' @return Same as `git_list_refs()`. #' @noRd async_git_list_refs_v2_process_3 <- function(reply, caps, url) { @@ -1218,7 +1218,7 @@ async_git_list_refs_v2_process_3 <- function(reply, caps, url) { #' * `type`: `commit`, `tree`, `blob` or `tag`. #' * `object`: the contents of the object. For `commit` the commit #' data and message in a string. For `tree` a data frame, as returned -#' from [parse_tree()]. For `blob` and `tag` a raw vector. +#' from `parse_tree()`. For `blob` and `tag` a raw vector. #' * `size`: unpacked size. #' * `packed_size`: packed size (size header not included). #' diff --git a/src/library/pkgdepends/inst/WORDLIST b/src/library/pkgdepends/inst/WORDLIST index 8eee8b2d87..95cc7655ac 100644 --- a/src/library/pkgdepends/inst/WORDLIST +++ b/src/library/pkgdepends/inst/WORDLIST @@ -1,10 +1,13 @@ Acromine +Bioconductor CMD +CRAN Capitan Codecov Config DevOps El +FreeBSD GitLab HOWTO ILP @@ -19,6 +22,7 @@ RedHat Rproj SHA Shorthands +Solaris Sur async cli @@ -31,6 +35,7 @@ ggplot gitcreds glmnet hexbin +html keras kknn knitr @@ -53,6 +58,8 @@ rstanarm shorthands sparklyr sshfs +subdirectories +subdirectory submodule submodules svglite @@ -60,3 +67,4 @@ testthat tibbles uncompress uncompressing +xml diff --git a/src/library/pkgdepends/inst/docs/download-result.rds b/src/library/pkgdepends/inst/docs/download-result.rds index af9a60cd64..107855954e 100644 Binary files a/src/library/pkgdepends/inst/docs/download-result.rds and b/src/library/pkgdepends/inst/docs/download-result.rds differ diff --git a/src/library/pkgdepends/inst/docs/pkg-refs.rds b/src/library/pkgdepends/inst/docs/pkg-refs.rds index ea1dcf8ea7..b45e0885a9 100644 Binary files a/src/library/pkgdepends/inst/docs/pkg-refs.rds and b/src/library/pkgdepends/inst/docs/pkg-refs.rds differ diff --git a/src/library/pkgdepends/inst/docs/resolution-result.rds b/src/library/pkgdepends/inst/docs/resolution-result.rds index d973071cc9..29641af97d 100644 Binary files a/src/library/pkgdepends/inst/docs/resolution-result.rds and b/src/library/pkgdepends/inst/docs/resolution-result.rds differ diff --git a/src/library/ts/DESCRIPTION b/src/library/ts/DESCRIPTION new file mode 100644 index 0000000000..c863be4f7e --- /dev/null +++ b/src/library/ts/DESCRIPTION @@ -0,0 +1,32 @@ +Package: ts +Title: Tree-Sitter Parsing Tools +Version: 0.0.0.9000 +Authors@R: c( + person("Gábor", "Csárdi", , "csardi.gabor@gmail.com", + role = c("aut", "cre")), + person("Posit Software, PBC", role = c("cph", "fnd"), + comment = c(ROR = "03wc8by49")), + person("Tree-sitter authors", role = "cph", comment = "Tree-sitter C library") + ) +Description: Common tree-sitter parsing tools for R. It is meant to be + used by other packages that specialize in particular languages and + file formats. +License: MIT + file LICENSE +Depends: R (>= 4.1.0) +Imports: cli, utils +Suggests: magrittr, pillar, testthat (>= 3.0.0), tsjsonc, tstoml, withr +Remotes: gaborcsardi/tsjsonc, gaborcsardi/tstoml +Additional_repositories: https://github.com/r-lib/ts/releases/download +Encoding: UTF-8 +RoxygenNote: 7.3.3 +URL: https://github.com/r-lib/ts, https://r-lib.github.io/ts/ +BugReports: https://github.com/r-lib/ts/issues +Config/testthat/edition: 3 +Config/Needs/website: r-lib/asciicast, tidyverse/tidytemplate +Biarch: true +NeedsCompilation: yes +Packaged: 2026-05-13 07:38:59 UTC; gaborcsardi +Author: Gábor Csárdi [aut, cre], + Posit Software, PBC [cph, fnd] (ROR: ), + Tree-sitter authors [cph] (Tree-sitter C library) +Maintainer: Gábor Csárdi diff --git a/src/library/ts/LICENSE b/src/library/ts/LICENSE new file mode 100644 index 0000000000..f00d2e60d9 --- /dev/null +++ b/src/library/ts/LICENSE @@ -0,0 +1,2 @@ +YEAR: 2025--2026 +COPYRIGHT HOLDER: ts authors diff --git a/src/library/ts/LICENSE.note b/src/library/ts/LICENSE.note new file mode 100644 index 0000000000..c8336544bd --- /dev/null +++ b/src/library/ts/LICENSE.note @@ -0,0 +1,24 @@ +Tree-sitter C library +-------------------------------------------------------------------------------- + +The MIT License (MIT) + +Copyright (c) 2018--2026 Max Brunsfeld + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/src/library/ts/NAMESPACE b/src/library/ts/NAMESPACE new file mode 100644 index 0000000000..5b276a0d85 --- /dev/null +++ b/src/library/ts/NAMESPACE @@ -0,0 +1,58 @@ +# Generated by roxygen2: do not edit by hand + +S3method("$",ts_tree) +S3method("[",ts_tree) +S3method("[[",ts_tree) +S3method("[[<-",ts_tree) +S3method("ts_tree_select<-",ts_tree) +S3method(as.character,ts_caller_arg) +S3method(as.character,ts_tree) +S3method(as.raw,ts_tree) +S3method(format,ts_parse_error) +S3method(format,ts_tree) +S3method(print,ts_parse_error) +S3method(print,ts_tree) +S3method(ts_tree_ast,default) +S3method(ts_tree_dom,default) +S3method(ts_tree_mark_selection1,ts_tree) +S3method(ts_tree_new,ts_language) +S3method(ts_tree_query,default) +S3method(ts_tree_select1,default) +S3method(ts_tree_select1,ts_tree.NULL) +S3method(ts_tree_select1,ts_tree.character) +S3method(ts_tree_select1,ts_tree.integer) +S3method(ts_tree_select1,ts_tree.logical) +S3method(ts_tree_select1,ts_tree.numeric) +S3method(ts_tree_select1,ts_tree.ts_tree_selector_default) +S3method(ts_tree_select1,ts_tree.ts_tree_selector_ids) +S3method(ts_tree_select1,ts_tree.ts_tree_selector_regex) +S3method(ts_tree_select1,ts_tree.ts_tree_selector_tsquery) +S3method(ts_tree_sexpr,default) +S3method(ts_tree_write,default) +export("ts_tree_select<-") +export(as_ts_caller_arg) +export(ts_caller_arg) +export(ts_caller_env) +export(ts_check_named_arg) +export(ts_cnd) +export(ts_collapse) +export(ts_list_parsers) +export(ts_parse_error_cnd) +export(ts_tree_ast) +export(ts_tree_delete) +export(ts_tree_deleted) +export(ts_tree_dom) +export(ts_tree_format) +export(ts_tree_insert) +export(ts_tree_mark_selection1) +export(ts_tree_new) +export(ts_tree_query) +export(ts_tree_select) +export(ts_tree_select1) +export(ts_tree_selected_nodes) +export(ts_tree_selection) +export(ts_tree_sexpr) +export(ts_tree_unserialize) +export(ts_tree_update) +export(ts_tree_write) +useDynLib(ts, .registration = TRUE, .fixes = "c_") diff --git a/src/library/ts/NEWS.md b/src/library/ts/NEWS.md new file mode 100644 index 0000000000..a998f52a28 --- /dev/null +++ b/src/library/ts/NEWS.md @@ -0,0 +1,3 @@ +# ts (development version) + +* Initial CRAN submission. diff --git a/src/library/ts/R/bracket-ts-tree.R b/src/library/ts/R/bracket-ts-tree.R new file mode 100644 index 0000000000..03f47bc833 --- /dev/null +++ b/src/library/ts/R/bracket-ts-tree.R @@ -0,0 +1,96 @@ +#' Convert ts_tree object to a data frame +#' +#' @ts ts_tree_brackets_description +#' Create a data frame for the syntax tree of a JSON document, by indexing +#' a ts_tree object with single brackets. This is occasionally useful for +#' exploration and debugging. +#' @description \eval{ts:::doc_insert("ts_tree_brackets_description")} +#' +#' @ts ts_tree_brackets_details +#' A tree-sitter tree object has at least four classes: +#' * `ts_tree_`, e.g. `ts_tree_tsjsonc`, +#' * `ts_tree`, +#' * `tbl`, from the pillar package, for better printing when converted +#' to a data frame, and +#' * `data.frame`, since it is a data frame internally. +#' +#' The `ts_tree` class has custom [format()] and [print()] methods, that +#' show (part of) the underlying document, and also the selected elements, +#' if any. +#' +#'

+#' +#' It is sometimes useful to treat a `tree` `ts_tree` object as a data +#' frame, and drop the `ts_tree` classes. This can be done by indexing with +#' single brackets, e.g. `tree[]`. This returns a data frame with one +#' row per token, and various columns with information about the tokens. +#' See details in the 'Value' section or this page. +#' +#' @details \eval{ts:::doc_insert("ts::ts_tree_brackets_details")} +#' +#' @ts ts_tree_brackets_param_x +#' A `ts_tree` object. +#' @ts ts_tree_brackets_param_ij +#' Incides, passed to the regular data.frame indexing method, see +#' \code{\link[base:Extract]{'Extract'}}. +#' @ts ts_tree_brackets_param_drop +#' Passed to the regular data.frame indexing method, see +#' \code{\link[base:Extract]{'Extract'}}. +#' +#' @param x \eval{ts:::doc_insert("ts::ts_tree_brackets_param_x")} +#' @param i,j \eval{ts:::doc_insert("ts::ts_tree_brackets_param_ij")} +#' @param drop \eval{ts:::doc_insert("ts::ts_tree_brackets_param_drop")} +#' +#' @ts ts_tree_brackets_return +#' A data frame with one row per token, and columns: +#' * `id`: integer, the id of the token. The (root) document node has id 1. +#' * `parent`: integer, the id of the parent token. The root token has +#' parent `NA` +#' * `field_name`: character, the field name of the token in its parent. +#' * `type`: character, the type of the token. +#' * `code`: character, the actual code of the token. +#' * `start_byte`, `end_byte`: integer, the byte positions of the token +#' in the input. +#' * `start_row`, `start_column`, `end_row`, `end_column`: integer, the +#' position of the token in the input. +#' * `is_missing`: logical, whether the token is a missing token added by +#' the parser to recover from errors. +#' * `has_error`: logical, whether the token has a parse error. +#' * `children`: list of integer vectors, the ids of the children tokens. +#' * `dom_type`: character, the type of the node in the DOM tree. See +#' \code{\link[ts:ts_tree_dom]{ts_tree_dom()}}. Nodes that are not part +#' of the DOM tree have `NA_character_` here. +#' * `dom_children`: list of integer vectors, the ids of the children in the +#' DOM tree. See \code{\link[ts:ts_tree_dom]{ts_tree_dom()}}. +#' * `dom_parent`: integer, the parent of the node in the DOM tree. See +#' \code{\link[ts:ts_tree_dom]{ts_tree_dom()}}. Nodes that are not part +#' of the DOM tree and the document node have have `NA_integer_` here. +#' +#' Other, undocumented columns may also be present, these are considered +#' internal and may change without notice. +#' @return \eval{ts:::doc_insert("ts::ts_tree_brackets_return")} +#' +#' @name ts_tree-brackets +#' @family ts_tree exploration +#' @seealso \eval{ts:::doc_seealso("[")} +#' @export +#' @examplesIf requireNamespace("tsjsonc", quietly = TRUE) +#' # Create a parse tree with tsjsonc ------------------------------------- +#' tree <- tsjsonc::ts_parse_jsonc('{"foo": 42, "bar": [1, 2, 3]}') +#' +#' tree +#' +#' tree[] + +`[.ts_tree` <- function(x, i, j, drop = FALSE) { + class(x) <- setdiff(class(x), "ts_tree") + requireNamespace("pillar", quietly = TRUE) + NextMethod("[") +} + +#' @export + +`$.ts_tree` <- function(x, name) { + class(x) <- setdiff(class(x), "ts_tree") + NextMethod(`$`) +} diff --git a/src/library/ts/R/cleancall.R b/src/library/ts/R/cleancall.R new file mode 100644 index 0000000000..11e4d24607 --- /dev/null +++ b/src/library/ts/R/cleancall.R @@ -0,0 +1,3 @@ +call_with_cleanup <- function(ptr, ...) { + .Call(c_cleancall_call, pairlist(ptr, ...), parent.frame()) +} diff --git a/src/library/ts/R/coerce.R b/src/library/ts/R/coerce.R new file mode 100644 index 0000000000..d71a6e799f --- /dev/null +++ b/src/library/ts/R/coerce.R @@ -0,0 +1,36 @@ +#' Raw bytes of a document of a tree-sitter tree +#' +#' @param x A `ts_tree` object. +#' @return A raw vector containing the bytes of the document of the tree. +#' +#' @export +#' @seealso [as.character.ts_tree()] to get the document as a character scalar. +#' @examples +#' # Create a parse tree with tsjsonc ------------------------------------- +#' tree <- tsjsonc::ts_parse_jsonc('{"foo": 42, "bar": [1, 2, 3]}') +#' +#' tree +#' as.raw(tree) + +as.raw.ts_tree <- function(x) { + attr(x, "text") +} + +#' The document of a tree-sitter tree as a character scalar +#' +#' @param x A `ts_tree` object. +#' @param ... Ignored. +#' @return A character scalar containing the document of the tree. +#' +#' @export +#' @seealso [as.raw.ts_tree()] to get the document as a raw vector. +#' @examples +#' # Create a parse tree with tsjsonc ------------------------------------- +#' tree <- tsjsonc::ts_parse_jsonc('{"foo": 42, "bar": [1, 2, 3]}') +#' +#' tree +#' as.character(tree) + +as.character.ts_tree <- function(x, ...) { + rawToChar(as.raw(x)) +} diff --git a/src/library/ts/R/collapse.R b/src/library/ts/R/collapse.R new file mode 100644 index 0000000000..8331ad35fe --- /dev/null +++ b/src/library/ts/R/collapse.R @@ -0,0 +1,193 @@ +#' @rdname internal +#' @param s For `ts_collapse()` a character vector to collapse. +#' @param sep Separator string for most elements. +#' @param sep2 Separator string for two elements. +#' @param last Separator string before the last element. +#' @param trunc Integer, maximum number of elements to show before +#' truncation. +#' @param width Integer, maximum display width of the collapsed string. +#' If the collapsed string exceeds this width, it will be truncated +#' with `ellipsis`. +#' @param ellipsis String to indicate truncation. +#' @param style Character, the collapsing style to use. Possible values are +#' `"both-ends"` (the default), which shows the first few and last few +#' elements when truncating, and `"head"`, which shows only the first few +#' elements. +#' @return `ts_collapse()` returns a character scalar, the collapsed string. +#' @details `ts_collapse()` collapses a character vector into a single string, +#' with options for truncation by number of elements or display width. +#' It is useful for creating informative error messages. +#' @export +#' @examples +#' ts_collapse(letters[1:3]) +#' ts_collapse(letters[1:10], trunc = 5) + +ts_collapse <- function( + s, + sep = ", ", + sep2 = sub("^,", "", last), + last = ", and ", + trunc = Inf, + width = Inf, + ellipsis = "...", + style = c("both-ends", "head") +) { + style <- match.arg(style) + switch( + style, + "both-ends" = collapse_both_ends( + s, + sep, + sep2, + last, + trunc, + width, + ellipsis + ), + "head" = collapse_head(s, sep, sep2, last, trunc, width, ellipsis) + ) +} + +collapse_head_notrim <- function(x, trunc, sep, sep2, last, ellipsis) { + lnx <- length(x) + + if (lnx == 1L) { + return(x) + } + if (lnx == 2L) { + return(paste0(x, collapse = sep2)) + } + if (lnx <= trunc) { + # no truncation + return(paste0( + paste(x[1:(lnx - 1L)], collapse = sep), + last, + x[lnx] + )) + } else { + # truncation, no need for 'last' + return(paste0( + paste(x[1:trunc], collapse = sep), + sep, + ellipsis + )) + } +} + +collapse_head <- function(x, sep, sep2, last, trunc, width, ellipsis) { + trunc <- max(trunc, 1L) + x <- as.character(x) + lnx <- length(x) + + # special cases that do not need trimming + if (lnx == 0L) { + return("") + } else if (anyNA(x)) { + return(NA_character_) + } + + # easier case, no width trimming + if (width == Inf) { + return(collapse_head_notrim(x, trunc, sep, sep2, last, ellipsis)) + } + + # complex case, with width wrapping + # first we truncate + tcd <- lnx > trunc + if (tcd) { + x <- x[1:trunc] + } + + # then we calculate the width w/o trimming + wx <- nchar(x) + wsep <- nchar(sep, "width") + wsep2 <- nchar(sep2, "width") + wlast <- nchar(last, "width") + well <- nchar(ellipsis, "width") + if (!tcd) { + # x[1] + # x[1] and x[2] + # x[1], x[2], and x[3] + nsep <- if (lnx > 2L) lnx - 2L else 0L + nsep2 <- if (lnx == 2L) 1L else 0L + nlast <- if (lnx > 2L) 1L else 0L + wtot <- sum(wx) + nsep * wsep + nsep2 * wsep2 + nlast * wlast + if (wtot <= width) { + if (lnx == 1L) { + return(x) + } else if (lnx == 2L) { + return(paste0(x, collapse = sep2)) + } else { + return(paste0( + paste(x[1:(lnx - 1L)], collapse = sep), + last, + x[lnx] + )) + } + } + } else { + # x[1], x[2], x[trunc], ... + wtot <- sum(wx) + trunc * wsep + well + if (wtot <= width) { + return(paste0( + paste(x, collapse = sep), + sep, + ellipsis + )) + } + } + + # we need to find the longest possible truncation for the form + # x[1], x[2], x[trunc], ... + # each item is wx + wsep wide, so we search how many fits, with ellipsis + last <- function(x) if (length(x) >= 1L) x[length(x)] else x[NA_integer_] + trunc <- last(which(cumsum(wx + wsep) + well <= width)) + + # not even one element fits + if (is.na(trunc)) { + if (well > width) { + return(strtrim(ellipsis, width)) + } else if (well == width) { + return(ellipsis) + } else if (well + wsep >= width) { + return(paste0(strtrim(x[1L], width), ellipsis)) + } else { + return(paste0( + strtrim(x[1L], max(width - well - wsep, 0L)), + sep, + ellipsis + )) + } + } + + return(paste0( + paste(x[1:trunc], collapse = sep), + sep, + ellipsis + )) +} + +collapse_both_ends <- function(x, sep, sep2, last, trunc, width, ellipsis) { + # we always list at least 5 elements + trunc <- max(trunc, 5L) + trunc <- min(trunc, length(x)) + if (length(x) <= 5L || length(x) <= trunc) { + return(collapse_head(x, sep, sep2, last, trunc = trunc, width, ellipsis)) + } + + # we have at least six elements in the vector + # 1, 2, 3, ..., 9, and 10 + x <- as.character(c(x[1:(trunc - 2L)], x[length(x) - 1L], x[length(x)])) + paste0( + c(x[1:(trunc - 2L)], ellipsis, paste0(x[trunc - 1L], last, x[trunc])), + collapse = sep + ) +} + +trim <- function(x) { + has_newline <- function(x) any(grepl("\\n", x)) + if (length(x) == 0L || !has_newline(x)) { + return(x) + } + call_with_cleanup(c_trim, x) +} diff --git a/src/library/ts/R/compat-vctrs.R b/src/library/ts/R/compat-vctrs.R new file mode 100644 index 0000000000..8b4595e4e0 --- /dev/null +++ b/src/library/ts/R/compat-vctrs.R @@ -0,0 +1,643 @@ +# nocov start + +compat_vctrs <- local({ + # Modified from https://github.com/r-lib/rlang/blob/master/R/compat-vctrs.R + + # Construction ------------------------------------------------------------ + + # Constructs data frames inheriting from `"tbl"`. This allows the + # pillar package to take over printing as soon as it is loaded. + # The data frame otherwise behaves like a base data frame. + data_frame <- function(...) { + new_data_frame(df_list(...), .class = "tbl") + } + + new_data_frame <- function(.x = list(), ..., .size = NULL, .class = NULL) { + n_cols <- length(.x) + if (n_cols != 0 && is.null(names(.x))) { + stop("Columns must be named.", call. = FALSE) + } + + if (is.null(.size)) { + if (n_cols == 0) { + .size <- 0 + } else { + .size <- vec_size(.x[[1]]) + } + } + + structure( + .x, + class = c(.class, "data.frame"), + row.names = .set_row_names(.size), + ... + ) + } + + df_list <- function(..., .size = NULL) { + vec_recycle_common(list(...), size = .size) + } + + # Binding ----------------------------------------------------------------- + + vec_rbind <- function(...) { + xs <- vec_cast_common(list(...)) + do.call(base::rbind, xs) + } + + vec_cbind <- function(...) { + xs <- list(...) + + ptype <- vec_ptype_common(lapply(xs, `[`, 0)) + class <- setdiff(class(ptype), "data.frame") + + xs <- vec_recycle_common(xs) + out <- do.call(base::cbind, xs) + new_data_frame(out, .class = class) + } + + # Slicing ----------------------------------------------------------------- + + vec_size <- function(x) { + if (is.data.frame(x)) { + nrow(x) + } else { + length(x) + } + } + + vec_rep <- function(x, times) { + i <- rep.int(seq_len(vec_size(x)), times) + vec_slice(x, i) + } + + vec_recycle_common <- function(xs, size = NULL) { + sizes <- vapply(xs, vec_size, integer(1)) + + n <- unique(sizes) + + if (length(n) == 1 && is.null(size)) { + return(xs) + } + n <- setdiff(n, 1L) + + ns <- length(n) + + if (ns == 0) { + if (is.null(size)) { + return(xs) + } + } else if (ns == 1) { + if (is.null(size)) { + size <- n + } else if (ns != size) { + stop("Inputs can't be recycled to `size`.", call. = FALSE) + } + } else { + stop("Inputs can't be recycled to a common size.", call. = FALSE) + } + + to_recycle <- sizes == 1L + xs[to_recycle] <- lapply(xs[to_recycle], vec_rep, size) + + xs + } + + vec_slice <- function(x, i) { + if (is.logical(i)) { + i <- which(i) + } + stopifnot(is.numeric(i) || is.character(i)) + + if (is.null(x)) { + return(NULL) + } + + if (is.data.frame(x)) { + # We need to be a bit careful to be generic. First empty all + # columns and expand the df to final size. + out <- x[i, 0, drop = FALSE] + + # Then fill in with sliced columns + out[seq_along(x)] <- lapply(x, vec_slice, i) + + # Reset automatic row names to work around `[` weirdness + if (is.numeric(attr(x, "row.names"))) { + row_names <- .set_row_names(nrow(out)) + } else { + row_names <- attr(out, "row.names") + } + + return(out) + } + + d <- vec_dims(x) + if (d == 1) { + if (is.object(x)) { + out <- x[i] + } else { + out <- x[i, drop = FALSE] + } + } else if (d == 2) { + out <- x[i, , drop = FALSE] + } else { + j <- rep(list(quote(expr = )), d - 1) + out <- eval(as.call(list( + quote(`[`), + quote(x), + quote(i), + j, + drop = FALSE + ))) + } + + out + } + vec_dims <- function(x) { + d <- dim(x) + if (is.null(d)) { + 1L + } else { + length(d) + } + } + + vec_as_location <- function(i, n, names = NULL) { + out <- seq_len(n) + names(out) <- names + + # Special-case recycling to size 0 + if (is_logical(i, n = 1) && !length(out)) { + return(out) + } + + unname(out[i]) + } + + vec_init <- function(x, n = 1L) { + vec_slice(x, rep_len(NA_integer_, n)) + } + + vec_assign <- function(x, i, value) { + if (is.null(x)) { + return(NULL) + } + + if (is.logical(i)) { + i <- which(i) + } + stopifnot( + is.numeric(i) || is.character(i) + ) + + value <- vec_recycle(value, vec_size(i)) + value <- vec_cast(value, to = x) + + d <- vec_dims(x) + + if (d == 1) { + x[i] <- value + } else if (d == 2) { + x[i, ] <- value + } else { + stop("Can't slice-assign arrays.", call. = FALSE) + } + + x + } + + vec_recycle <- function(x, size) { + if (is.null(x) || is.null(size)) { + return(NULL) + } + + n_x <- vec_size(x) + + if (n_x == size) { + x + } else if (size == 0L) { + vec_slice(x, 0L) + } else if (n_x == 1L) { + vec_slice(x, rep(1L, size)) + } else { + stop("Incompatible lengths: ", n_x, ", ", size, call. = FALSE) + } + } + + # Coercion ---------------------------------------------------------------- + + vec_cast_common <- function(xs, to = NULL) { + ptype <- vec_ptype_common(xs, ptype = to) + lapply(xs, vec_cast, to = ptype) + } + + vec_cast <- function(x, to) { + if (is.null(x)) { + return(NULL) + } + if (is.null(to)) { + return(x) + } + + if (vec_is_unspecified(x)) { + return(vec_init(to, vec_size(x))) + } + + stop_incompatible_cast <- function(x, to) { + stop( + sprintf( + "Can't convert <%s> to <%s>.", + .rlang_vctrs_typeof(x), + .rlang_vctrs_typeof(to) + ), + call. = FALSE + ) + } + + lgl_cast <- function(x, to) { + lgl_cast_from_num <- function(x) { + if (any(!x %in% c(0L, 1L))) { + stop_incompatible_cast(x, to) + } + as.logical(x) + } + + switch( + .rlang_vctrs_typeof(x), + logical = x, + integer = , + double = lgl_cast_from_num(x), + stop_incompatible_cast(x, to) + ) + } + + int_cast <- function(x, to) { + int_cast_from_dbl <- function(x) { + out <- suppressWarnings(as.integer(x)) + if (any((out != x) | xor(is.na(x), is.na(out)))) { + stop_incompatible_cast(x, to) + } else { + out + } + } + + switch( + .rlang_vctrs_typeof(x), + logical = as.integer(x), + integer = x, + double = int_cast_from_dbl(x), + stop_incompatible_cast(x, to) + ) + } + + dbl_cast <- function(x, to) { + switch( + .rlang_vctrs_typeof(x), + logical = , + integer = as.double(x), + double = x, + stop_incompatible_cast(x, to) + ) + } + + chr_cast <- function(x, to) { + switch( + .rlang_vctrs_typeof(x), + character = x, + stop_incompatible_cast(x, to) + ) + } + + list_cast <- function(x, to) { + switch( + .rlang_vctrs_typeof(x), + list = x, + stop_incompatible_cast(x, to) + ) + } + + df_cast <- function(x, to) { + # Check for extra columns + if (length(setdiff(names(x), names(to))) > 0) { + stop( + "Can't convert data frame because of missing columns.", + call. = FALSE + ) + } + + # Avoid expensive [.data.frame method + out <- as.list(x) + + # Coerce common columns + common <- intersect(names(x), names(to)) + out[common] <- Map(vec_cast, out[common], to[common]) + + # Add new columns + from_type <- setdiff(names(to), names(x)) + out[from_type] <- lapply(to[from_type], vec_init, n = vec_size(x)) + + # Ensure columns are ordered according to `to` + out <- out[names(to)] + + new_data_frame(out) + } + + rlib_df_cast <- function(x, to) { + new_data_frame(df_cast(x, to), .class = "tbl") + } + tib_cast <- function(x, to) { + new_data_frame(df_cast(x, to), .class = c("tbl_df", "tbl")) + } + + switch( + .rlang_vctrs_typeof(to), + logical = lgl_cast(x, to), + integer = int_cast(x, to), + double = dbl_cast(x, to), + character = chr_cast(x, to), + list = list_cast(x, to), + + base_data_frame = df_cast(x, to), + rlib_data_frame = rlib_df_cast(x, to), + tibble = tib_cast(x, to), + + stop_incompatible_cast(x, to) + ) + } + + vec_ptype_common <- function(xs, ptype = NULL) { + if (!is.null(ptype)) { + return(vec_ptype(ptype)) + } + + xs <- Filter(function(x) !is.null(x), xs) + + if (length(xs) == 0) { + return(NULL) + } + + if (length(xs) == 1) { + out <- vec_ptype(xs[[1]]) + } else { + xs <- map(xs, vec_ptype) + out <- Reduce(vec_ptype2, xs) + } + + vec_ptype_finalise(out) + } + + vec_ptype_finalise <- function(x) { + if (is.data.frame(x)) { + x[] <- lapply(x, vec_ptype_finalise) + return(x) + } + + if (inherits(x, "rlang_unspecified")) { + logical() + } else { + x + } + } + + vec_ptype <- function(x) { + if (vec_is_unspecified(x)) { + return(.rlang_vctrs_unspecified()) + } + + if (is.data.frame(x)) { + out <- new_data_frame(lapply(x, vec_ptype)) + + attrib <- attributes(x) + attrib$row.names <- attr(out, "row.names") + attributes(out) <- attrib + + return(out) + } + + vec_slice(x, 0) + } + + vec_ptype2 <- function(x, y) { + stop_incompatible_type <- function(x, y) { + stop( + sprintf( + "Can't combine types <%s> and <%s>.", + .rlang_vctrs_typeof(x), + .rlang_vctrs_typeof(y) + ), + call. = FALSE + ) + } + + x_type <- .rlang_vctrs_typeof(x) + y_type <- .rlang_vctrs_typeof(y) + + if (x_type == "unspecified" && y_type == "unspecified") { + return(.rlang_vctrs_unspecified()) + } + if (x_type == "unspecified") { + return(y) + } + if (y_type == "unspecified") { + return(x) + } + + df_ptype2 <- function(x, y) { + set_partition <- function(x, y) { + list( + both = intersect(x, y), + only_x = setdiff(x, y), + only_y = setdiff(y, x) + ) + } + + # Avoid expensive [.data.frame + x <- as.list(vec_slice(x, 0)) + y <- as.list(vec_slice(y, 0)) + + # Find column types + names <- set_partition(names(x), names(y)) + if (length(names$both) > 0) { + common_types <- Map(vec_ptype2, x[names$both], y[names$both]) + } else { + common_types <- list() + } + only_x_types <- x[names$only_x] + only_y_types <- y[names$only_y] + + # Combine and construct + out <- c(common_types, only_x_types, only_y_types) + out <- out[c(names(x), names$only_y)] + new_data_frame(out) + } + + rlib_df_ptype2 <- function(x, y) { + new_data_frame(df_ptype2(x, y), .class = "tbl") + } + tib_ptype2 <- function(x, y) { + new_data_frame(df_ptype2(x, y), .class = c("tbl_df", "tbl")) + } + + ptype <- switch( + x_type, + + logical = switch( + y_type, + logical = x, + integer = y, + double = y, + stop_incompatible_type(x, y) + ), + + integer = switch( + .rlang_vctrs_typeof(y), + logical = x, + integer = x, + double = y, + stop_incompatible_type(x, y) + ), + + double = switch( + .rlang_vctrs_typeof(y), + logical = x, + integer = x, + double = x, + stop_incompatible_type(x, y) + ), + + character = switch( + .rlang_vctrs_typeof(y), + character = x, + stop_incompatible_type(x, y) + ), + + list = switch( + .rlang_vctrs_typeof(y), + list = x, + stop_incompatible_type(x, y) + ), + + base_data_frame = switch( + .rlang_vctrs_typeof(y), + base_data_frame = , + s3_data_frame = df_ptype2(x, y), + rlib_data_frame = rlib_df_ptype2(x, y), + tibble = tib_ptype2(x, y), + stop_incompatible_type(x, y) + ), + + rlib_data_frame = switch( + .rlang_vctrs_typeof(y), + base_data_frame = , + rlib_data_frame = , + s3_data_frame = rlib_df_ptype2(x, y), + tibble = tib_ptype2(x, y), + stop_incompatible_type(x, y) + ), + + tibble = switch( + .rlang_vctrs_typeof(y), + base_data_frame = , + rlib_data_frame = , + tibble = , + s3_data_frame = tib_ptype2(x, y), + stop_incompatible_type(x, y) + ), + + stop_incompatible_type(x, y) + ) + + vec_slice(ptype, 0) + } + + .rlang_vctrs_typeof <- function(x) { + if (is.object(x)) { + class <- class(x) + + if (identical(class, "rlang_unspecified")) { + return("unspecified") + } + if (identical(class, "data.frame")) { + return("base_data_frame") + } + if (identical(class, c("tbl", "data.frame"))) { + return("rlib_data_frame") + } + if (identical(class, c("tbl_df", "tbl", "data.frame"))) { + return("tibble") + } + if (inherits(x, "data.frame")) { + return("s3_data_frame") + } + + class <- paste0(class, collapse = "/") + stop(sprintf("Unimplemented class <%s>.", class), call. = FALSE) + } + + type <- typeof(x) + switch( + type, + NULL = return("null"), + logical = if (vec_is_unspecified(x)) { + return("unspecified") + } else { + return(type) + }, + integer = , + double = , + character = , + raw = , + list = return(type) + ) + + stop(sprintf("Unimplemented type <%s>.", type), call. = FALSE) + } + + vec_is_unspecified <- function(x) { + !is.object(x) && + typeof(x) == "logical" && + length(x) && + all(vapply(x, identical, logical(1), NA)) + } + + .rlang_vctrs_unspecified <- function(x = NULL) { + structure( + rep(NA, length(x)), + class = "rlang_unspecified" + ) + } + + .rlang_vctrs_s3_method <- function(generic, class, env = parent.frame()) { + fn <- get(generic, envir = env) + + ns <- asNamespace(topenv(fn)) + tbl <- ns$.__S3MethodsTable__. + + for (c in class) { + name <- paste0(generic, ".", c) + if (exists(name, envir = tbl, inherits = FALSE)) { + return(get(name, envir = tbl)) + } + if (exists(name, envir = globalenv(), inherits = FALSE)) { + return(get(name, envir = globalenv())) + } + } + + NULL + } + + environment() +}) + +data_frame <- compat_vctrs$data_frame + +as_data_frame <- function(x) { + if (is.matrix(x)) { + x <- as.data.frame(x, stringsAsFactors = FALSE) + } else { + x <- compat_vctrs$vec_recycle_common(x) + } + compat_vctrs$new_data_frame(x, .class = "tbl") +} + +# nocov end diff --git a/src/library/ts/R/docs.R b/src/library/ts/R/docs.R new file mode 100644 index 0000000000..932d5c0bdf --- /dev/null +++ b/src/library/ts/R/docs.R @@ -0,0 +1,315 @@ +dglue <- function(..., .envir = parent.frame()) { + glue(..., .open = "<<", .close = ">>", .envir = .envir) +} + +is_rcmd_check <- function() { + if (Sys.getenv("GITHUB_ACTIONS") == "true") { + return(FALSE) + } + Sys.getenv("_R_CHECK_PACKAGE_NAME_", "") != "" || + Sys.getenv("_R_RD_MACROS_PACKAGE_DIR_", "") != "" +} + +doc_has_method <- function(method, package) { + method <- paste0(method, ".ts_tree_", sub("^ts", "", package)) + length(utils::help( + topic = (method), + package = (package), + help_type = "text" + )) > + 0 +} + +doc_seealso <- function(method) { + ts_package <- get_env("R_TS_PACKAGE") + if (is.null(ts_package)) { + psrs <- ts_list_parsers() + psrs <- psrs[!duplicated(psrs$package), ] + links <- vapply( + psrs$package, + function(pkg) { + if (doc_has_method(method, pkg)) { + glue( + "\\code{{\\link[{pkg}:{method}.{pkg}]{{{method}()}}}}", + ) + } else { + "" + } + }, + "" + ) + links <- links[links != ""] + if (length(links) > 0) { + s <- if (length(links) > 1) "s" + glue("Method{s} in installed package{s}: {ts_collapse(links)}.") + } else { + "" + } + } else { + paste0( + "The generic of this method in the ts package: ", + glue("\\code{{\\link[ts:{method}]{{{method}()}}}}"), + "." + ) + } +} + +doc_insert <- function(key, manpkg = NULL) { + if (is_rcmd_check()) { + return("Placeholder.") + } + if (!is.null(manpkg)) { + Sys.setenv("R_TS_PACKAGE" = manpkg) + on.exit(Sys.unsetenv("R_TS_PACKAGE"), add = TRUE) + } + keypcs <- strsplit(key, "::", fixed = TRUE)[[1]] + if (length(keypcs) == 2) { + package <- keypcs[1] + key <- keypcs[2] + } else { + package <- "ts" + } + lib <- dirname(find.package(package)) + output <- doc_create_chunk(key, lib, package, 1L, "<>") + + mch <- gregexpr("\\\\eval\\{[^\\}]+\\}", output, perl = TRUE) + regmatches(output, mch)[[1]] <- lapply( + regmatches(output, mch)[[1]], + function(x) { + x <- sub("^\\\\eval\\{", "", x) + x <- sub("\\}$", "", x) + eval(parse(text = x)) + } + ) + output +} + +doc_tabs <- function(key) { + if (is_rcmd_check()) { + return("Placeholder.") + } + package <- if (nzchar(ev <- Sys.getenv("R_TS_PACKAGE"))) { + ev + } + + if (is.null(package)) { + doc_tabs_all(key) + } else { + doc_tabs_one(key, package) + } +} + +doc_tabs_all <- function(key) { + # list all installed ts packages + psrs <- ts_list_parsers() + psrs <- psrs[!duplicated(psrs$package), ] + + tsdocpath <- doc_path("ts") + t_tab <- read_char(file.path(tsdocpath, "tab.html")) + t_div <- read_char(file.path(tsdocpath, "tabs.html")) + t_btn <- read_char(file.path(tsdocpath, "btn.html")) + output <- buttons <- tabs <- "" + + for (i in seq_len(nrow(psrs))) { + language <- sub("^ts", "", psrs$package[i]) + tab <- doc_create_chunk(key, psrs$library[i], psrs$package[i], i, t_tab) + btn <- dglue( + t_btn, + .envir = c(psrs[i, ], list(idx = i, language = language)) + ) + if (!is.null(tab) && nzchar(tab)) { + tabs <- paste0(tabs, tab, "\n") + buttons <- paste0(buttons, btn, "\n") + } + } + if (tabs != "") { + output <- dglue(t_div, .envir = list(tabs = tabs, buttons = buttons)) + } + + output +} + +doc_tabs_one <- function(key, package) { + lib <- dirname(find.package(package)) + doc_create_chunk(key, lib, package, 1, "<>") +} + +doc_path <- function(package) { + pkgdir <- find.package(package) + docpath <- file.path(pkgdir, "tsdocs") + if (!file.exists(docpath)) { + docpath <- file.path(pkgdir, "inst", "tsdocs") + } + + docpath +} + +doc_create_chunk <- function(key, lib, package, idx, template) { + file <- paste0(key, ".Rd") + path <- file.path(doc_path(package), file) + if (!file.exists(path)) { + return("") + } + x <- read_char(path) + lns <- strsplit(x, "\n", fixed = TRUE)[[1]] + rulepos <- which(lns == "# ---") + lns <- lns[(rulepos + 1):length(lns)] + lang_data <- list( + idx = idx, + package = package, + language = sub("^ts", "", package), + contents = paste(lns, collapse = "\n") + ) + dglue(template, .envir = lang_data) +} + +doc_extra <- function() { + if (is_rcmd_check()) { + return("Placeholder.") + } + tsdocpath <- doc_path("ts") + jspath <- file.path(tsdocpath, "tabs.js") + js <- read_char(jspath) + + csspath1 <- file.path(doc_path("ts"), "w3.css") + css1 <- read_char(csspath1) + css <- "" + if (Sys.getenv("IN_PKGDOWN") == "true") { + csspath2 <- file.path(doc_path("ts"), "pkgdown.css") + css2 <- read_char(csspath2) + css <- gsub("%", "\\%", paste0(css1, "\n\n", css2), fixed = TRUE) + css <- paste0("\n") + } else { + # in Rd we insert CSS with JS, because tidy does not allow