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