Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Avoid failing to install git remotes when packfile parsing fails for unnecessary files #355

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 8 additions & 4 deletions R/git-protocol.R
Original file line number Diff line number Diff line change
Expand Up @@ -523,6 +523,7 @@ unpack_packfile_repo <- function(parsed, output, url) {
done <- logical(length(trees))
idx <- 1L
wd <- character()
paths <- logical(0L) # named vector of (path, success)

mkdirp(output)

Expand All @@ -532,7 +533,9 @@ unpack_packfile_repo <- function(parsed, output, url) {
tr <- trees[[i]]$object
for (l in seq_len(nrow(tr))) {
idx <<- idx + 1L
opath <- file.path(output, paste(c(wd, tr$path[l]), collapse = "/"))
rpath <- paste(c(wd, tr$path[l]), collapse = "/")
opath <- file.path(output, rpath)
paths[[rpath]] <<- FALSE
if (tr$type[l] == "tree") {
tidx <- which(tr$hash[l] == names(trees))[1]
if (is.na(tidx)) {
Expand All @@ -543,10 +546,12 @@ unpack_packfile_repo <- function(parsed, output, url) {
}
wd <<- c(wd, tr$path[l])
mkdirp(opath)
paths[[rpath]] <<- TRUE
process_tree(tidx)
wd <<- utils::head(wd, -1)
} else if (tr$type[l] == "blob") {
} else if (tr$type[l] == "blob" && !is.null(parsed[[tr$hash[l]]])) {
writeBin(parsed[[tr$hash[l]]]$raw, opath)
paths[[rpath]] <<- TRUE
}
}
}
Expand All @@ -567,10 +572,9 @@ unpack_packfile_repo <- function(parsed, output, url) {
)
}


for (i in seq_along(trees)) process_tree(i)

invisible()
invisible(paths)
}

# -------------------------------------------------------------------------
Expand Down
65 changes: 65 additions & 0 deletions R/type-git.R
Original file line number Diff line number Diff line change
Expand Up @@ -93,11 +93,76 @@ download_remote_git <- function(resolution, target, target_tree,
pkgdir <- file.path(target_tree, resolution$package)
mkdirp(pkgdir)
async_git_download_repo(url, ref = sha, output = pkgdir)$
then(function(files) {
check_git_download_missing_files(
resolution$remote[[1]],
files = files,
output = pkgdir
)
})$
then(function() {
"Got"
})
}


#' Check Downloaded git Repo for Missing Files
#'
#' Throws a warning if any files failed to download which are necessary for the
#' building of the package from source. Will ignore the failure to download
#' files outside a specified remote sub-directory or omitted using a
#' `.Rbuildignore` file.
#'
#' @param remote A git remote
#' @param files A named logical vector with names corresponding to relative
#' file paths to each file in the git repository, and a logical value
#' indicating whether the file was successfully downloaded.
#' @param output The directory containing downloaded repository files
#'
#' @return NULL, function used for its side-effects
#'

check_git_download_missing_files <- function(remote, files, output) {
# subdir consistently trailing in path separator
sep <- .Platform$file.sep
subdir <- remote$subdir %||% ""
if (nchar(subdir) && !endsWith(subdir, sep)) {
subdir <- paste0(subdir, sep)
}

# ignore reporting files outside of subdirectory
in_subdir <- startsWith(names(files), subdir)
files <- files | !in_subdir

# ignore reporting files that would be ignored using `.Rbuildignore`
ignore_path_parts <- c(output, remote$subdir, ".Rbuildignore")
ignore_path <- do.call(file.path, as.list(ignore_path_parts))
if (file.exists(ignore_path)) {
subdir_path <- character(length(files))
subdir_path[in_subdir] <- substring(
names(files)[in_subdir],
nchar(subdir) + 1
)

ignore <- readLines(ignore_path, warn = FALSE)
for (pattern in ignore[nzchar(ignore)]) {
is_ignored <- grepl(pattern, subdir_path, perl = TRUE, ignore.case = TRUE)
files <- files | (in_subdir & is_ignored)
}
}

# warn if any files are necessary, but missing
if (any(!files)) {
throw(pkg_error(
"{sum(!files)} necessary file{?s} {?was/were} unable to be fetched",
paste0("'", names(files)[!files], "'", collapse = ", "),
.class = "pkgdepends_git_fetch"
))
Comment on lines +156 to +160
Copy link
Contributor Author

@dgkf dgkf Mar 9, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the initial PR this was originally a warning, but if files are required for properly building the package I felt it should probably be an error.

Ideally we could emit a warning and let R continue and fail when it tries to build the package, but R doesn't have any way to know that the files are missing so it would just happily build a faulty package. I think this would be a worse failure mode so I converted it to an error.

}

invisible(files)
}

satisfy_remote_git <- function(resolution, candidate,
config, ...) {
## 1. package name must match
Expand Down
Binary file added tests/testthat/fixtures/git-test-submod-opt.pack
Binary file not shown.
Binary file added tests/testthat/fixtures/git-test-submod-req.pack
Binary file not shown.
37 changes: 37 additions & 0 deletions tests/testthat/fixtures/test-rebuild.R
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
#' Rebuild fixtures
#'
#' @examples
#' testthat::test_dir(testthat::test_path("fixtures"))
#'
Comment on lines +1 to +5
Copy link
Contributor Author

@dgkf dgkf Mar 9, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added a "test" file in the tests/testthat/fixtures directory to rebuild the relevant packfiles. Happy to retool this if you have a different style in mind, I just wanted to make sure the process for generating some testing fixtures was reproducible.


test_that("Rebuilding Git Packfile Fixtures", {
fixture_updated <- function(msg) {
stop(errorCondition(msg, class = "error_fixture_updated"))
}

write_packfile <- function(packfile) {
function(x, ...) {
path <- file.path(
find_package_root(),
"tests",
"testthat",
"fixtures",
packfile
)
writeBin(x, path)
fixture_updated(sprintf("Updated fixture: '%s'", path))
}
}

url <- "https://github.com/dgkf/test-pkgdepends-git-remote-submod-req.git"
expect_error(class = "error_fixture_updated", with_mocked_bindings(
git_unpack = write_packfile("git-test-submod-req.pack"),
git_download_repo(url)
))

url <- "https://github.com/dgkf/test-pkgdepends-git-remote-submod-opt.git"
expect_error(class = "error_fixture_updated", with_mocked_bindings(
git_unpack = write_packfile("git-test-submod-opt.pack"),
git_download_repo(url)
))
})
49 changes: 49 additions & 0 deletions tests/testthat/test-type-git.R
Original file line number Diff line number Diff line change
Expand Up @@ -197,3 +197,52 @@ test_that("type_git_make_resolution", {
# TODO
expect_true(TRUE)
})

test_that("check_git_download_missing_files warns when missing required files", {
packfile <- test_path("fixtures", "git-test-submod-req.pack")
pack <- git_unpack(packfile)

url <- "https://website.org/repo.git"
output <- tempfile("pkgdepends-test-git-submod-req")
dir.create(output)
on.exit(unlink(output, recursive = TRUE))

# fixture successfully unpacks and produces expected file contents
files <- expect_silent(unpack_packfile_repo(pack, output, url = url))
expect_vector(files, logical())
expect_named(files)

# files required for build
expect_true("inst/generics" %in% names(files))
expect_false(files[["inst/generics"]]) # submodule "file" missing

expect_error(class = "pkgdepends_git_fetch", {
check_git_download_missing_files(list(), files, output)
})
})

test_that("check_git_download_missing_files is silent when missing files are unnecessary", {
packfile <- test_path("fixtures", "git-test-submod-opt.pack")
pack <- git_unpack(packfile)

url <- "https://website.org/repo.git"
output <- tempfile("pkgdepends-test-git-submod-req")
dir.create(output)
on.exit(unlink(output, recursive = TRUE))

# fixture successfully unpacks and produces expected file contents
files <- expect_silent(unpack_packfile_repo(pack, output, url = url))
expect_vector(files, logical())
expect_named(files)
expect_true("generics" %in% names(files))
expect_true("pkgdepends.test.002/inst/generics" %in% names(files))

# submodule "files" unnecessary for build
expect_false(files[["pkgdepends.test.002/inst/generics"]]) # .Rbuildignore'd
expect_false(files[["generics"]]) # outside subdir

expect_silent({
remote <- list(subdir = "pkgdepends.test.002")
check_git_download_missing_files(remote, files, output)
})
})
Loading