reproducible 3.1.0
new features
prepInputsCOG: new fast-path insideprepInputsfor remote tiled GeoTiff
files (including Cloud Optimized GeoTiffs). When theurlis HTTP(S) and at
least one ofto,cropTo, ormaskTois supplied, only the spatial window
of interest is fetched via GDAL's/vsicurl/— no full-file download. The
windowedSpatRasteris returned to the normalpostProcesspipeline for
crop/mask/write. The fast-path can be disabled with
options(reproducible.useCOG = FALSE).
enhancements
-
The options
reproducible.inputPathsandreproducible.inputPathsRecursive
have been renamed toreproducible.destinationPathSharedand
reproducible.destinationPathSharedRecursiverespectively (matching theprepInputs
naming family). The old names remain fully functional as backwards-compatible
aliases: ifreproducible.destinationPathSharedisNULLandreproducible.inputPaths
is set, the old value is used automatically (with a deprecation message).
Update your code by replacingoptions(reproducible.inputPaths = ...)with
options(reproducible.destinationPathShared = ...)at your convenience. -
alsoExtractinprepInputs/preProcessnow accepts regex patterns in
addition to exact filenames. For example,
alsoExtract = "CMD_sm|CMD_sp"will extract all archive members whose
name matches that regular expression. The expansion is performed against the
archive's file listing usinggrep(): if an element is a literal match it is
kept as-is; otherwise it is treated as a pattern. Special sentinel values
("similar","none",NA) are not affected. The expansion happens before
file extraction whether or not the archive was already present before the call. -
preProcessnow skips downloading when a local copy already exists and
matches the remote version, even if it was never recorded inCHECKSUMS.txt
for the currentdestinationPath. There is also a new<cacheId>.hashfile
placed alongside the cached repository files, that is a simpler mechanism than
theCHECKSUMS.txt, i.e., one file, one hash. This was implemented because there
were too many edge cases that were difficult to handle when there is a single
CHECKSUMS.txtfile. Nevertheless, theCHECKSUMS.txtfile is still used if
it is present, and the<>..hashfile is absent, so a user can manually
place a knownCHECKSUMS.txtfile into a directory as "the canonical version".
The newpp_remote_hash_checkstage fetches
remote metadata (ETag / content-length for HTTP; md5Checksum / size for Google
Drive) and compares against the local file. A stored.hashfile is checked
first; if absent, file-size equality is used as a fast proxy and the hash is
persisted for future runs. This fixes the common cluster pattern where an archive
lives inoptions("reproducible.inputPaths")but has not yet been checksummed
for the current run-specificdestinationPath.Design note — two-layer caching strategy: The remote hash check and the
localCHECKSUMS.txtare complementary, not competing. Remote metadata (ETags,
content-length, Google Drivemd5Checksum) solves the bootstrapping problem
— confirming a file is correct before it has ever been checksummed locally.
CHECKSUMS.txtthen takes over for all subsequent runs: it requires no network
round-trip, works on compute nodes without internet access, and is
content-addressable (survives URL changes). Note that HTTP ETags are not
universally content-hash-based — many servers derive them from inode + mtime,
making cross-server or post-migration comparisons unreliable — so the remote
check is intentionally a best-effort shortcut rather than a replacement for the
local checksum record. -
showCache(when useDBI = FALSE) now has lazy memory caching. This is relevant
for very large caches (e.g., >10,000 entries).
Cache()now lazy-spawns theshowCacheasync background scan against the
cachePath the call actually uses. With the lazy spawn, the firstCache()call
insimInit/spadeskicks off the fork, which then runs to completion in
parallel with the simulation; subsequentshowCache()calls return in
~1 second. The spawn helper is idempotent (~10 us per call). -
New exported helper
prepopulateCacheAsync(cachePath)lets workflows
kick off the async scan explicitly (e.g. early insetupProject()) so
the fork has even more wall-clock time to complete. -
Lots of new unit tests for new features, and to cover edge cases that were slipping
through.
bug fixes
-
Cache(omitArgs = TRUE)now drops every captured argument from the cache
digest, so the digest depends only onFUNitself (the function value
-- including its body, so source edits still bust the cache) and
.cacheExtra. Useful when a developer wants the cache key to be
insensitive to the function's inputs and pin freshness via.cacheExtra
(e.g. a quoted reference to runtime state) instead of enumerating every
input or every argument to omit. Character-vector
omitArgs = c("a", "b")still works as before. -
downloadRemote: the "before" snapshot ofdestinationPath
taken before evaluatingdlFunnow usesrecursive = TRUEto match the
"after" snapshot. Previously, the non-recursive snapshot omitted files
that were already present in subdirectories ofdestinationPath, so the
setdiff()of after vs. before classified those pre-existing subdirectory
files as newly created. They then propagated asdownloadResults$destFile,
triggered the "already exists at . Use overwrite = TRUE?" stop later
in the function, and surfaced as a confusing error mentioning unrelated
stashed files (e.g. shapefile pieces extracted by an earlierprepInputs
call into the samereproducible.inputPaths/reproducible.destinationPathShared). -
Cache(useCloud = ...)now accepts two character values, intended for
separating developer and user roles when sharing a cloud-cache folder:
"push"is equivalent toTRUE(developer role -- bidirectional;
downloads on a cloud hit, uploads on a miss);"pull"is read-only (user
role -- downloads on a cloud hit, never uploads). When"pull"is set
and the local cache already has the object, the Google Drive listing is
not fetched at all (the cloud is consulted only after a local miss). An
invalid character value now errors at the front door ofCache()rather
than silently being treated asFALSE. -
prepInputs/.guessAtTargetAndFun: no longer auto-selects OS-injected archive
metadata whentargetFileis unspecified. Previously, a Mac-created zip
containing bothfoo.shpand__MACOSX/._foo.shpcould pick the
AppleDouble copy and fail to load. Filtering covers macOS (__MACOSX/*,
._*AppleDouble,.DS_Store) and Windows (Thumbs.db,desktop.ini).
An explicittargetFilepointing at one of these is still honored. -
pp_remote_hash_check: now skips URLs that are nothttp://orhttps://.
Previously,file://URLs would attempt a HEAD-style metadata fetch and then
callmakeRemoteHashFile, whose URL-to-filename mapping only strips the
https?://prefix; on Windows the resulting.hashfilename retainedfile:
and the drive-letter colon, which is not a legal Windows path character. -
prepInputs/preProcess: when called withdlFunonly (nourl,
targetFile, orarchive), the file produced bydlFunis now treated as
the source of truth. Previously,runChecksumswould still scan
getOption("reproducible.inputPaths")and, with no canonical filename to
look up,Checksums()listed every file in the stash and matched any of
them against the stash'sCHECKSUMS.txt. A non-empty match silently
redirecteddestinationPathto the stash and causedprepInputsto load
an unrelated file (e.g., a previously stashed shapefile instead of the
GADM.rdsproduced bygeodata::gadm()). -
prepInputs/preProcess:dlFun = pkg::fn(args)(a function call passed
directly, withoutquote()-wrapping) is once again kept as a deferred call
object instead of being eagerly evaluated. The previous fix for
dlFun = if (cond) fn else NULLbroke this canonical usage by forcing the
lazy promise for all non-quote()expressions. The new logic keeps
function-call expressions (fn(args),pkg::fn(args),pkg:::fn(args))
deferred while still evaluating control-flow expressions and bare symbols. -
prepInputsCOGnow requires a GeoTiff-style URL extension (.tif,.tiff,
.cog,.gtiff) before attempting a/vsicurl/read. Previously, any
HTTP(S) URL combined with a spatial subsetting argument would trigger the
fast-path, producing a confusingGDAL Error 4 ... not recognized as being in a supported file formatwarning when the URL pointed to an archive
(e.g..zip,.tar.gz). -
lockFilenow usescheckPath(..., create = TRUE)instead of a silent
dir.create(..., showWarnings = FALSE)when creating the lock-file directory,
so a missing or unwritable cache directory (e.g., on a network filesystem)
produces a clear error rather than a confusingfilelockfailure. -
Fix
.listFilesInArchiveincorrectly returning an empty file list for zip
archives wherearchive::archive()reportssize = 0for every entry
(a known metadata-reading issue with certain compression variants such as
Deflate64). When all reported sizes are zero but the archive file itself is
non-empty, all paths are now included rather than being filtered out. -
Fix spurious "More than one possible files to load" message (and "Picking the
last one") printed bypreProcesseven whenfun = NA. When the user
explicitly passesfun = NA(meaning: do not load the file into R),
.guessAtTargetAndFunnow returns immediately without inspecting or
messaging about the extracted file list. -
Fix
pp_remote_hash_checkincorrectly treating a direct.tif(or other
non-archive) download as an archive when the remote hash matched. The stage
was unconditionally settingctx$archive <- localFile, which caused
downstreampp_extractto run7z/unzipon the plain raster file.
Fix: only setctx$archivewhen.isArchive(localFile)is non-NULL. -
Fix spurious
preProcess could not extract the files from the archiveerror
when files were already present on disk (e.g. extracted earlier in the same
call or found viareproducible.inputPaths). InextractFromArchive,result
was computed from the checkSums table before.checkSumsUpdate()was called,
so freshly-extracted files had no prior entry andNROW(result) == 0forced the
re-extraction branch even thoughall(isOK)was TRUE. Fix: computeresult
after.checkSumsUpdate()so it reflects current disk state. The error was
non-fatal (caught by the surroundingtry()) but printed an alarming message
and wasted effort attempting a zero-file extraction. -
Fix
Google Drive download failed: HTTP 401 Unauthorizederror that occurred
mid-session when downloading multiple tiles viaprepInputsWithTiles. The raw
access_tokenstring extracted from thegargle/googledrivetoken expired
(1-hour TTL) while tiles were being downloaded. Fix: force a gargle token
refresh (viaToken2.0$refresh()) before eachdownload_resumable_httr2call,
and retry once on 401 with a fresh token for both the httr2 and curl code paths. -
Fix
object 'fun' not founderror whenCache(prepInputs, ..., fun = fun, ...)is called
withfunas a local variable name.substitute(fun)captured the symbol rather than
the value; the symbol was then evaluated in the wrong frame (Cache's internal frame, not the
user's). Fix: force the R promise directly (funCaptured <- fun) so resolution happens in
the frame where the promise was created (the user's frame), regardless of call depth. -
Fix
filelock::lock()"Permission denied" error under high parallelism (30+ workers).
Three root causes: (1) deleting the.lockfile afterunlock()broke mutex correctness —
workers blocked onfcntl(F_SETLKW)held the old inode's lock while a fresh caller
created a new inode and acquired its own "lock" simultaneously; (2) stale.lockfiles
owned by root (from a prior sudo/root run) causedEACCESatopen(O_RDWR|O_CREAT);
(3) thetryCatchmatched on "Permission denied" which is locale-dependent (varies with
LC_MESSAGES) — now matches on "Cannot open lock file" (filelock's fixed C-level prefix).
Fix: stop deleting lock files after release; wrapfilelock::lock()intryCatchwith
a 5-attempt retry loop; match on the locale-independent error prefix. -
postProcess: when 2 large polygon datasets were provided (from and to), the pre-cropping step
failed as the buffer was not applied. This has been fixed and the buffer now scales with
the size of the polygons.