Skip to content

pak::pkg_install() fails on corporate Windows where AppLocker blocks cmdunzip.exe #849

@scschwa

Description

@scschwa

Problem

pak::pkg_install() downloads packages successfully but fails during extraction on corporate Windows machines with AppLocker or Software Restriction Policies:

pak::pkg_install("dplyr")
#> Downloaded 27 packages (53.27 MB) in 40.9s
#> Error:
#> ! error in pak subprocess
#> Caused by error in `process_initialize(...)`:
#> ! Native call to `processx_exec` failed
#> Caused by error:
#> ! create process 'C:/Users/.../pak/library/zip/bin/x64/cmdunzip.exe'
#>   (system error 1260, This program is blocked by group policy.)

The root cause is that pkgdepends:::make_install_process() calls make_unzip_process()zip::unzip_process(), which spawns cmdunzip.exe as an external process. Corporate group policy blocks this .exe.

Standard R configuration (R_UNZIPCMD, options(unzip=), TAR) has no effect (I had TAR available on my corporate PC) because pak's internal code path ignores all of these.

Impact

This completely blocks pak on corporate Windows. The alternative is install.packages(), which loses pak's parallel downloads and dependency resolution and takes 6-10x longer (over an hour vs ~10 minutes in my case).

Proposed solution

Short term: Add a pak configuration option (e.g., PKG_ZIP_METHOD = "internal") that tells pkgdepends to use zip::unzip() (in-process C code, no external .exe) instead of zip::unzip_process() for .zip extraction.

Long term: The upstream fix belongs in the zip package — see r-lib/zip#135. If zip::unzip_process() could fall back to zip::unzip() when process creation fails, pak would inherit the fix automatically.

Working workaround

We developed a patch that replaces make_install_process in pak's worker subprocess. It extracts .zip files synchronously using zip::unzip(), then returns a real processx::process object (a finished cmd /c exit 0) so pak's async polling loop continues normally. Non-zip archives use the original code path.

library(pak)

pak:::remote(function() {
  ns <- asNamespace("pkgdepends")
  original <- get("make_install_process", envir = ns)

  replacement <- function(filename, lib = .libPaths()[[1L]], metadata = NULL) {
    type <- ns$detect_package_archive_type(filename)
    if (type != "zip") return(original(filename, lib = lib, metadata = metadata))

    now <- Sys.time()
    lib_cache <- ns$library_cache(lib)
    ns$mkdirp(pkg_cache <- tempfile(tmpdir = lib_cache))
    zip::unzip(filename, exdir = pkg_cache, overwrite = TRUE)
    ns$install_extracted_binary(filename, lib_cache, pkg_cache, lib, metadata, now)

    p <- processx::process$new("cmd.exe", c("/c", "exit", "0"),
                                stdout = "|", stderr = "2>&1")
    p$wait()
    reg.finalizer(p, function(...) unlink(pkg_cache, recursive = TRUE), onexit = TRUE)
    p
  }

  unlockBinding("make_install_process", ns)
  assign("make_install_process", replacement, envir = ns)
  lockBinding("make_install_process", ns)
  invisible(TRUE)
})

pak::pkg_install("dplyr")  # works

Tested successfully with 100+ packages on Windows 11 Enterprise with AppLocker.
Only issue with this is I have to source("patch_pak.R") after each library(pak) load or jam it into my .Rprofile, which would be fine if it was just me, but there's a few hundred R users where I am.

Environment

  • Windows 11 Enterprise with AppLocker/SRP enabled
  • R 4.4.x
  • pak 0.8.0+

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions