Skip to content

Commit 41019ad

Browse files
authored
migrate from remotes to pak (#29)
* migrate from remotes to pak * Add setup for user cache directory in tests to prevent pak subprocess failures * Ensure R_USER_CACHE_DIR is set for pak downloads and remove obsolete test setup * Use internal tar implementation for untar operations to ensure compatibility * Improve extraction logic in resolve_remote_pkg to handle zip files and fallback to untar * Preserve original reference in parse_remote_ref and enhance ref handling in build_pak_remote_ref * Enhance documentation and auto-discovery logic for remote package resolution - Added details on fallback mechanisms in `extract_code`, `to_txt`, and `resolve_remote_pkg` functions. - Updated examples in documentation to illustrate auto-discovery of packages in subdirectories. - Improved tests for `resolve_remote_pkg` to validate fallback behavior and auto-discovery functionality. * Update NEWS.md to include new features, improvements, and bug fixes for the development version
1 parent 52d3878 commit 41019ad

21 files changed

Lines changed: 1441 additions & 388 deletions

DESCRIPTION

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ BugReports: https://github.com/e-kotov/rdocdump/issues
1919
Suggests:
2020
curl,
2121
quarto,
22-
remotes (>= 2.2.0),
22+
pak,
2323
testthat (>= 3.0.0),
2424
withr
2525
VignetteBuilder:

NEWS.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,24 @@
11
# rdocdump (development version)
22

3+
### New Features
4+
5+
* Migrated to `pak` for remote package resolution and downloads.
6+
7+
* Added auto-discovery of R packages in subdirectories for remote repositories.
8+
9+
### Improvements
10+
11+
* Enhanced robustness of remote package resolution with automatic fallbacks (e.g., `git clone`).
12+
13+
* Improved cross-platform compatibility by using internal `tar` operations.
14+
15+
* Expanded support for various remote URL formats and platforms (Bioconductor, GitLab, etc.).
16+
17+
### Bug Fixes
18+
19+
* Fixed directory cleanup logic and improved performance of vignette concatenation.
20+
21+
322
# rdocdump 0.2.0 (2026-04-29)
423

524
* Added support for remote repository references (GitHub, GitLab, Bitbucket) in `rdd_to_txt()` and `rdd_extract_code()`. This includes support for specific branches/tags/commits and packages in subdirectories, as well as direct URLs (to branches and/or folders within repositories). Therefore, added the `remotes` package to `Suggests` to handle remote downloads.

R/extract_code.R

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,13 @@
2525
#' (e.g., `"https://github.com/apache/sedona-db/tree/main/r/sedonadb"`
2626
#' or `"https://github.com/ipeaGIT/r5r/tree/master/r-package"`).
2727
#' }
28+
#'
29+
#' @details
30+
#' For remote repositories, `rdocdump` uses `pak` for resolution. If `pak`
31+
#' cannot find an R package at the root or the specified subdirectory, the
32+
#' function will automatically fall back to downloading the full repository
33+
#' and searching for the shallowest directory containing a `DESCRIPTION` file.
34+
#'
2835
#' @param file Optional. Save path for the output text file. If set, the
2936
#' function will return the path to the file instead of the combined text.
3037
#' Defaults to `NULL`.
@@ -137,7 +144,10 @@ rdd_extract_code <- function(
137144
#' Extract code from an installed package using its namespace.
138145
#'
139146
#' This function retrieves all functions from the package namespace and
140-
#' deparses them to get their source code.
147+
#' deparses them to get their source code. Note that extracting from an
148+
#' installed package silently skips S4 classes, R6 classes, environment
149+
#' objects, and datasets since it filters for `is.function()`. For more
150+
#' complete code extraction, prefer extracting from source packages.
141151
#' @param pkg_name The name of the installed package.
142152
#' @return A single string containing the source code of all functions in the
143153
#' package.

R/to_txt.R

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,12 @@
9090
#'
9191
#' # Extract from GitLab
9292
#' docs <- rdd_to_txt("gitlab::user/repo")
93+
#'
94+
#' # Auto-discovery of packages in subdirectories (e.g., if repo root is not the pkg)
95+
#' docs <- rdd_to_txt("ipeaGIT/r5r")
96+
#'
97+
#' # Manual subdirectory specification (useful for disambiguation)
98+
#' docs <- rdd_to_txt("ipeaGIT/r5r/r-package")
9399
#' }
94100
#'
95101
#' \dontrun{

R/util_cleanup_files.R

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ cleanup_files <- function(
3232
if (
3333
!keep_files %in% c("extracted", "both") && !is.null(pkg_info$extracted_path)
3434
) {
35-
dir_to_remove <- dirname(pkg_info$extracted_path)
35+
dir_to_remove <- pkg_info$extracted_path
3636
res <- unlink(dir_to_remove, recursive = TRUE)
3737
if (res != 0L) {
3838
warning(

R/util_combine_vignettes.R

Lines changed: 4 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -28,19 +28,11 @@ combine_vignettes <- function(pkg_path) {
2828
return("")
2929
}
3030

31-
combined_text <- ""
32-
33-
for (vf in vignette_files) {
31+
vignette_texts <- lapply(vignette_files, function(vf) {
3432
header_line <- paste0(strrep("-", 80), "\nVignette: ", basename(vf), "\n")
3533
text <- readLines(vf, warn = FALSE)
36-
combined_text <- paste(
37-
combined_text,
38-
header_line,
39-
paste(text, collapse = "\n"),
40-
"\n\n",
41-
sep = "\n"
42-
)
43-
}
34+
paste(header_line, paste(text, collapse = "\n"), "\n", sep = "\n")
35+
})
4436

45-
return(combined_text)
37+
return(paste(unlist(vignette_texts), collapse = "\n"))
4638
}

R/util_pak_download.R

Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
1+
#' Find the target row from a `pak::pkg_download()` result
2+
#'
3+
#' Filters `dl_info` to rows where `direct` is `TRUE` if the column exists,
4+
#' and matches `pkg_ref` if provided. Always returns a 1-row data frame.
5+
#'
6+
#' @param dl_info Data frame returned by `pak::pkg_download()`.
7+
#' @param pkg_ref Optional package reference requested from pak.
8+
#' @return One-row data frame.
9+
#' @keywords internal
10+
#' @noRd
11+
find_pak_target_row <- function(dl_info, pkg_ref = NULL) {
12+
if (!is.data.frame(dl_info) || nrow(dl_info) < 1L) {
13+
stop("No package download information returned by pak.")
14+
}
15+
16+
if ("direct" %in% names(dl_info)) {
17+
direct <- !is.na(dl_info$direct) & dl_info$direct
18+
if (any(direct)) {
19+
dl_info <- dl_info[direct, , drop = FALSE]
20+
}
21+
}
22+
23+
if (!is.null(pkg_ref) && "ref" %in% names(dl_info)) {
24+
exact_ref <- !is.na(dl_info$ref) & dl_info$ref == pkg_ref
25+
if (any(exact_ref)) {
26+
dl_info <- dl_info[exact_ref, , drop = FALSE]
27+
}
28+
}
29+
30+
dl_info[1L, , drop = FALSE]
31+
}
32+
33+
#' Safely extract a single character value from a one-row data frame
34+
#'
35+
#' Returns `NA_character_` if the column is missing or the value is `NULL`.
36+
#'
37+
#' @param row One-row data frame.
38+
#' @param col Column name to extract.
39+
#' @return Single character value, or `NA_character_`.
40+
#' @keywords internal
41+
#' @noRd
42+
pak_row_value <- function(row, col) {
43+
if (!col %in% names(row)) {
44+
return(NA_character_)
45+
}
46+
val <- row[[col]][1L]
47+
if (is.null(val)) {
48+
return(NA_character_)
49+
}
50+
as.character(val)
51+
}
52+
53+
#' Select the archive downloaded by pak
54+
#'
55+
#' @param dl_info Data frame returned by `pak::pkg_download()`.
56+
#' @param dest_dir Destination directory passed to `pak::pkg_download()`.
57+
#' @param pkg_ref Package reference requested from pak.
58+
#' @return Path to a usable `.tar.gz` archive.
59+
#' @keywords internal
60+
#' @noRd
61+
select_pak_download_archive <- function(dl_info, dest_dir, pkg_ref = NULL) {
62+
row <- find_pak_target_row(dl_info, pkg_ref)
63+
paths <- pak_download_paths(row, dest_dir)
64+
65+
for (path in paths) {
66+
archive <- pak_materialize_archive(path, dest_dir)
67+
if (!is.null(archive)) {
68+
return(archive)
69+
}
70+
}
71+
72+
fallback_name <- pak_row_value(row, "package")
73+
stop(sprintf(
74+
"No downloaded archive found in %s for '%s'.",
75+
dest_dir,
76+
pkg_ref %||% (if (is.na(fallback_name)) "requested package" else fallback_name)
77+
))
78+
}
79+
80+
pak_download_paths <- function(row, dest_dir) {
81+
values <- character()
82+
83+
if ("fulltarget" %in% names(row) && !is.na(row$fulltarget[1L])) {
84+
values <- c(values, row$fulltarget[1L])
85+
}
86+
87+
if ("target" %in% names(row) && !is.na(row$target[1L])) {
88+
target <- row$target[1L]
89+
# Recognize POSIX absolute, Windows drive-letter, and Windows UNC paths.
90+
if (grepl("^(/|[A-Za-z]:|\\\\\\\\)", target)) {
91+
values <- c(values, target)
92+
} else {
93+
values <- c(values, file.path(dest_dir, target))
94+
}
95+
values <- c(values, file.path(dest_dir, basename(target)))
96+
}
97+
98+
unique(values[nzchar(values)])
99+
}
100+
101+
pak_materialize_archive <- function(path, dest_dir) {
102+
archive <- file.path(dest_dir, basename(sub("-t$", "", path)))
103+
104+
if (file.exists(path) && !dir.exists(path)) {
105+
return(copy_pak_archive(path, archive))
106+
}
107+
108+
tree_path <- if (grepl("-t$", path)) path else paste0(path, "-t")
109+
if (file.exists(tree_path) && !dir.exists(tree_path)) {
110+
return(copy_pak_archive(tree_path, archive))
111+
}
112+
113+
if (dir.exists(tree_path)) {
114+
return(tar_pak_tree(tree_path, archive))
115+
}
116+
117+
if (file.exists(archive) && !dir.exists(archive)) {
118+
return(archive)
119+
}
120+
121+
NULL
122+
}
123+
124+
copy_pak_archive <- function(from, to) {
125+
from_norm <- normalizePath(from, mustWork = TRUE)
126+
to_norm <- normalizePath(to, mustWork = FALSE)
127+
128+
if (from_norm == to_norm) {
129+
return(to)
130+
}
131+
132+
if (file.exists(to)) {
133+
unlink(to, recursive = TRUE)
134+
}
135+
136+
if (!dir.exists(dirname(to))) {
137+
dir.create(dirname(to), recursive = TRUE, showWarnings = FALSE)
138+
}
139+
140+
ok <- file.copy(from, to, overwrite = TRUE)
141+
if (!isTRUE(ok)) {
142+
stop(sprintf("Failed to copy downloaded archive to %s.", to))
143+
}
144+
145+
to
146+
}
147+
148+
tar_pak_tree <- function(tree_path, archive) {
149+
files <- list.files(tree_path, all.files = TRUE, no.. = TRUE)
150+
if (length(files) < 1L) {
151+
stop(sprintf("Downloaded package tree is empty: %s.", tree_path))
152+
}
153+
154+
if (file.exists(archive)) {
155+
unlink(archive, recursive = TRUE)
156+
}
157+
158+
if (!dir.exists(dirname(archive))) {
159+
dir.create(dirname(archive), recursive = TRUE, showWarnings = FALSE)
160+
}
161+
162+
# utils::tar() needs relative file paths to avoid absolute paths in archives.
163+
archive <- normalizePath(archive, mustWork = FALSE)
164+
old_wd <- setwd(tree_path)
165+
on.exit(setwd(old_wd), add = TRUE)
166+
167+
utils::tar(
168+
tarfile = archive,
169+
files = files,
170+
compression = "gzip",
171+
tar = "internal"
172+
)
173+
174+
if (!file.exists(archive)) {
175+
stop(sprintf(
176+
"Failed to create archive from downloaded package tree: %s.",
177+
archive
178+
))
179+
}
180+
181+
archive
182+
}

0 commit comments

Comments
 (0)