Changelog
bincraft (development version)
bincraft no longer depends on the
pat-s/cranlike@s3andpat-s/desc@description-from-remoteforks, soDESCRIPTIONhas noRemotes:left (#33).upload_package_index()now builds the index records itself and uses the slot’s publishedPACKAGES.rdsas its cache instead ofPACKAGES.db. A record is reused while its tarball is listed with the same ETag; a new or replaced tarball is read again and stamped withBuilt, where cranlike only updated the MD5 of a replaced one. DESCRIPTION files are fetched from the CRAN mirror on GitHub in parallel, and a package the mirror does not have is read from its own tarball instead of being left out of the index on every run.PACKAGES.dbis no longer written or uploaded, the stale copy in each slot can be deleted, and the index files are written to a temporary directory instead of the working directory.upload_package_index()is faster: the slot is listed once instead of three times, andPACKAGESandPACKAGES.gzare written without refolding every value throughstrwrap(), which took over a minute per 25k-record slot. The files read back to the same records; only their line wrapping changes.add_to_package_index()is now defunct and errors; useupload_package_index().bincraft no longer imports data.table, dplyr, gert, gh, lubridate or purrr (#29). This drops 16 packages from a fresh install and the
libgit2system requirement; git itself is still needed and is now listed underSystemRequirements. Date ranges are now plainDatepairs:get_updated_cran_packages(),get_new_cran_packages(),get_removed_cran_packages(),process_cran_updates()andget_r_minor_sensitive_packages()default toc(Sys.Date() - n, Sys.Date())and accept a singleDate, ac(start, end)pair or, as before, alubridate::interval().query_metadata_table()still returns a lazydplyr::tbl(), but dplyr and dbplyr are now suggested packages it asks for when they are missing. GitHub tags are fetched through httr2 instead of gh; a token is read fromGITHUB_PATorGITHUB_TOKEN, but no longer from the git credential store. Cloning setsadvice.detachedHead=falsefor the clone only, instead of writing it to the global git config. Retries use an internalinsistently(); a request that keeps failing now reports the last error alongside the attempt count.Connections to the metadata database can now verify the server certificate (#225). libpq looks for the root certificate of
sslmode = "verify-ca"or"verify-full"in~/.postgresql/root.crt, not the system trust store, so verifying never worked in the build images. Every connection now passes the system CA bundle assslrootcertwhen the mode verifies, unlessPGSSLROOTCERTis set.query_metadata_table(),list_metadata_tables()andget_missing_pkgs_db(), which connect to the devxy database with a hardcoded mode, now useverify-fullinstead ofrequire.upload_package_index()no longer defaults to the devxy production database (#211).metadata_db_host,metadata_db_name,metadata_db_port,metadata_db_userandmetadata_db_passwordnow default toNULLinstead ofr-binaries.devxy.ioand thePGPASSenvironment variable. A per-minor call (r_minorset) now stops before touching S3 unless it is given a host, a database name and a non-empty password, and the error names the missing arguments. The generic index never reads the database, so a call withoutr_minoris unaffected.A failed dependency install or build start is no longer written to the metadata table when
store_build_metadata = FALSE(#36). Both paths decided whether to record the failure by whethermetadata_db_hostwas set, but the host is also set for the ABI cache, so a run with storage disabled still wrote error rows. They now followstore_build_metadata, like every other build path.fetch_tags_via_ls_remote()now keeps onlyrefs/tags/lines, so a branch orHEADin thegit ls-remoteoutput can no longer come back as a tag.upload_source_tarball()no longer replaces a published binary with CRAN’s source. It uploaded withoverwrite = TRUEwithout looking at the key, relying on its only internal caller to have checked first, but it is exported. It now asksremote_object_state()and skips the upload when a binary (or an object of unknown state) is there, while still replacing an earlier source fallback. It now returnsTRUEonly when it uploaded, andFALSEwhen the download failed or a binary was kept, where it used to returnTRUEfor a failed download.Architecture detection and binary tarball naming now work on every machine R runs on (#210). The tarball
R CMD INSTALL --buildwrites is now located by its real name,<pkg>_<version>_R_<R_PLATFORM>.tar.gz, instead of being reassembled from the machine and distribution, which failed on ppc64le, s390x and riscv64 and needed a scan of the output directory to spot an EPEL build of R on RHEL.set_bin_path(),upload_package_index(),archive_package()andbuild_binary_package()now share one architecture lookup, which fails with a clear message on an unsupported machine instead of “object ‘arch’ not found”.set_bin_path()gains anarchargument, andbuild_binary_package()now honours a suppliedarchinstead of overwriting it with the machine’s.build_binary_package()no longer creates an emptysrc/contrib/Archive/Archivein the output root.detect_linux_codename()now returnsfedora<version>on Fedora instead ofNULL. The unusedcran_mirror_version()andget_system_architecture_info()are removed.An older tag whose archived copy is a CRAN source fallback is now rebuilt instead of being skipped by the pre-build filter (#208).
list_archived_packages()counted every object underArchive/<pkg>/as present, so a failed build of an older version, once archived, was never retried. It now reads the archive withs3_dir_info()and leaves out objects whose MD5 or exact size matches CRAN’s, which costs no request beyond the listing; an object whose size cannot be read, or a CRAN that cannot be reached, still counts as present.check_for_binary()now delegates toremote_object_state(), so it recognises an archived version’s source fallback by size as well as a current one’s by MD5, and it also looks inArchive/<pkg>/when the root holds nothing. Archiving already moved a rebuilt binary over an archived source fallback; it now refuses the reverse and deletes a source fallback at the root rather than move it over an archived binary of the same version.check_s3_packages()no longer rebuilds every tag of a repository whose tags carry avprefix (#206). The slow path passed the latest tag unchanged tocheck_s3_root_package()andget_all_s3_packages(), so forv2.0.0they looked forpkg_v2.0.0.tar.gz, which is never published, and every run rebuilt binaries that already existed. Both lookups now use the artifact version (2.0.0), while the refs handed back for cloning keep theirv.install_pkg_sys_deps()now pre-installs only the patched binaries in the dependency tree of the package being built (#209).install_patched_into_library()installed every patched binary in the registry, so each build paid for patches it never used. The tree is resolved recursively overDepends,ImportsandLinkingTofrom the session-cached CRAN index; when it cannot be resolved completely, every patched binary is installed as before and an INFO message says so.prepare_patched_repo()now skips the target package’s own entry, which patches its source instead, and caches each package’s latest CRAN version for the session, so pkgsearch is queried once per patched package rather than once per build. A malformed version constraint in the target’s own entry now logs a warning that it is built unpatched instead of being ignored silently.get_updated_cran_packages()andget_removed_cran_packages()no longer silently drop packages from a long interval or return a package twice (#177). Both read a single page ofpkgsearch::cran_events()(2000 releases or 300 archivals) and never paged withfrom, so anything older than that page was cut off without warning. They now page until the feed reaches past the start ofdate_interval, andlimitis the page size; after 50 pages they stop and log a warning naming the date they reached. A package released twice in the interval produced two rows, so the older version could be built after the newer one; only the newest event per package is kept now.fetch_cran_versions()now logs a warning whenMeta/archive.rdscannot be downloaded. The failure was swallowed, so withtag_limit > 1it silently returned only the current version (#177).A single failed read of CRAN’s index no longer disables source-fallback detection for the rest of the session, and a source fallback for an archived version is now recognised before a build or upload (#171).
cran_source_md5()andcran_archive_size()cached the empty table from a failed read, so after one network blip everyremote_object_state()answered “binary”: no source fallback was replaced andclear_built_for_sources()leftBuilton all of them. A failed read is now retried after five minutes (or onrefresh = TRUE) instead of being kept, and only a successful read is cached.remote_object_state()only compared MD5s against CRAN’s current releases, so a source fallback of an older version always read as a binary. It now also matches the object’s exact size against CRAN’s archive index, asclear_built_for_sources()already does, and an unknown size or an unreachable CRAN still answers “binary”.A batch of small fixes from the package review (#178).
build_binary_package(),filter_tags()andprocess_tag_filtering()no longer error on a vector orNAtag, andprocess_tag_filtering()now resolves aNULLtag instead of returning it.move_and_rename_tarball()warns when the rename fails instead of ignoring it, and lists the output directory only at DEBUG rather than at INFO. Log messages keep their apostrophes and backticks instead of printing'', and{.function}tags, which cli does not know and printed literally, are now{.fun}.parse_bytes()accepts vectors and short units such as"1K", and errors on a unit it cannot parse instead of returningNA.upload_package_index()retries eachPACKAGES*upload, uploadsPACKAGESlast, and logs a package count based on the tarballs in the listing rather than the listing size minus 5.install_pkg_sys_deps()no longer fails with “promise already under evaluation” when called withoutplatform.process_unarchived_pkgs()now honoursworkers,get_missing_pkgs_db()takes the database connection settings as arguments, anddetect_linux_codename()recognises Debian, whose os-release has noID_LIKE. Arguments passed togit,uvrandR CMD INSTALLare shell-quoted, so paths with spaces no longer break them.Metadata DB connections, clone directories and patch Makevars files are now cleaned up on every exit path (#174).
remove_from_metadata()never disconnected, so pruning N removed packages inprocess_cran_updates()opened N connections, andstore_build_metadata(),filter_packages_with_errors(),list_metadata_tables()andget_missing_pkgs_db()leaked theirs on error.build_single_tag()left the clone behind when dependency installation failed, which filled/tmpduring large rebuilds, and theR_MAKEVARS_USERfile written for a registry patch was never removed. An unsupportedmetadata_db_typenow fails with a clear error instead of “object ‘driver’ not found”, andbuild_binary_package()now passesmetadata_db_typethrough to the per-tag builds.init_repo()is now exported and works. It reports a missing S3 bucket instead of silently continuing, fails when the metadata DB is unreachable, creates the metadata table with the columnsstore_build_metadata()writes (it previously failed with ‘argument “fields” is missing’), and disconnects afterwards. The bucket is not created; create it with your S3 provider first.add_to_package_index()is deprecated in favour ofupload_package_index()and now warns when called (#176). It never worked: it downloaded the remotePACKAGESonly when that file was missing, matched tarballs with the regex"<pkg>*"(so"sf"also picked ups_1.0.tar.gzandstars_1.0.tar.gz), and never uploaded the index it built. Nothing in bincraft or the other rpkgs repositories calls it, so it is deprecated rather than repaired. It also now derives the architecture viadetect_arch()and fails with a clear message on an unrecognised machine instead of “object ‘arch’ not found”.A per-minor
upload_package_index()now refuses to publish when it cannot read the ABI lookups, instead of publishing an index that carries known-broken generic binaries (#163).flat_safety_unsafe_set(),abi_cache_risky_packages()andrisky_packages_across_minors()returned an empty set on any database or S3 error, which reads as “nothing is unsafe”: with the database unreachable, Rcpp’s 4.5-built generic binary, recorded as unable to load under 4.4, went back into every 4.4 index. They now returnNULLwhen the lookup fails, and the per-minor index build aborts on it, leaving the previously published index in place as it already does for an unreadable generic index. The ABI lookups andflat_safety_store()no longer default to ther-binaries.devxy.iodatabase withPGPASS.upload_single_binary()gainsmetadata_db_*arguments, whichbuild_binary_package()andprocess_cran_updates()now pass through, so the flat-safety verdict lands in the database the build was configured with, and is not recorded at all without one.upload_package_index()gains the same arguments; their defaults are the previous production values with the password fromPGPASS, so existing pipelines keep working unchanged. A connection with an empty password is treated as not configured instead of being attempted, and an authentication failure is no longer retried, so an unsetPGPASSno longer stalls each lookup for about five minutes.Pruning packages removed from CRAN now also deletes their per-minor copies under
contrib/<r_minor>/(#175).process_removed_packages()listed only the generic slot, matched tarballs with a regex in which a.in the package name was a wildcard (so removingdata.tablewould also deletedataXtable), and failed with “object ‘arch’ not found” on an unrecognised machine. It now lists every per-minor slot, matches the package name as a fixed prefix, and stops with a clear error whendetect_arch()cannot determine the architecture. The affected slots are not re-indexed automatically, sinceupload_package_index()stampsBuiltfrom the running R; a warning names each slot whosePACKAGESindex must be republished.Parallel builds no longer install dependencies concurrently (#173). With more than one
futureworker, every tag’s worker ranuvr sync --install-system-depsagainst the same library at the same time, soapt-get/apk/dnfcollided on the package manager lock and the workers raced on the shared library.install_pkg_sys_deps()now holds a lock directory (.bincraft-deps.lockin the build library) for each install attempt, so workers take turns while builds themselves still run in parallel. The lock is released between retry attempts, and a lock left behind by a killed worker is reclaimed.A malformed
versionsconstraint in one patch entry no longer disables every dependency patch (#172).resolve_patch_version()let the “invalid version specification” error escape, soprepare_patched_repo()aborted andinstall_pkg_sys_deps()built every package without any patched dependency. The error now skips only that entry with a warning naming the constraint, andversionsaccepts a comma-separated range such as">=1.0, <2.0", all clauses of which must hold. Patched binaries already installed from the same tarball are no longer reinstalled withR CMD INSTALLon every build and retry. The dependency pre-install of an isolated patched build runs uvr on a copy of the DESCRIPTION, souvr.toml,uvr.lockand.r-versionno longer land in the patched package’s source tree.A package whose build failed is no longer rebuilt on every run despite its recorded error (#169).
check_s3_packages()returned before the metadata error filter whenever no binary was published at the root, which is always the case after a failure because the CRAN source fallback counts as absent, so the filter never ran for a single explicit tag. The filter now runs on that path too;force = TRUEremains the way to retry such a package. A build that errored beforebuild_single_tag()could record it (typically a failed clone) is now recorded under the artifact version (5.0.0) rather than the ref (v5.0.0), so the filter finds it, and its clone directory is cleaned up.A dependency installation that fails deterministically is no longer retried for minutes as a “network error” just because its output names a package such as
networkornetworkD3(#170).classify_uvr_error()matched the bare wordsnetworkandtimeoutanywhere in the uvr output, which lists every resolved package, so a failedapk addwas retried with backoff before failing anyway. It now matches specific phrases (network is unreachable,network error,timed out, …) instead.retry_with_backoff()also rethrows the error message unchanged, so build errors stored in the metadata database no longer contain doubled{{ }}braces (the log helpers already escape braces for cli), and each retry now logs the error message, not just its type.process_cran_updates()now honoursprocess_new,process_updated,codename,local_output_dir_rootandlocal_clone_dir(#166). The updated and new package sets were always fetched and built together, soprocess_new = FALSEstill built new packages and vice versa, and the three path/codename arguments never reachedbuild_binary_package(). A caller-suppliedcodenametherefore applied to removals while builds auto-detected theirs, so the two could target different S3 slots, and builds always wrote to"."and cloned into/tmp. Builds now clone intolocal_clone_dir, which defaults totempdir(). With both flagsFALSEnothing is fetched from CRAN at all.cran_newis now queried once per run instead of twice:get_updated_cran_packages()gains anew_packagesargument to reuse an already fetched set. The unusedpackage_nameandtagarguments are deprecated and emit a warning when supplied.process_cran_updates()no longer aborts when called with its defaultinterval(#165). The default waslubridate::today(), aDate, whichget_updated_cran_packages()andget_removed_cran_packages()fed straight into%within%(“No %within% method with signature a = Date, b = Date”), whileget_new_cran_packages()retried the failingint_start()for minutes insideinsistently()and then returned nothing. The default is nowlubridate::interval(today() - 1L, today()), and all four functions coerce a singleDateinto the one-day interval covering it. Any other value, or an interval that starts after it ends, now fails fast with a clear error instead of silently yielding an empty result. Theprocess_cran_updates()example passed exactly such a reversed interval and is fixed.r_minor_detection = "issue"(and the legacyfilter_r_minor_sensitive = TRUE) no longer abortsprocess_cran_updates()with “no applicable method for ‘filter’” (#164).get_r_minor_sensitive_packages()returned a character vector of names, orinvisible(TRUE)when nothing matched, where the caller expected thename/versiondata.frame of packages to build. Wheninterval,updated_packagesornew_packagesis given it now returns the matching rows of that data.frame, with zero rows when none match; called without them it still returns the package names from the issue. Issue lines are trimmed and Markdown list bullets stripped, so CRLF line endings and- pkglists match too. An unknownr_minor_packages_forge_typeor a missingr_minor_packages_issue_urlnow fails with a clear error instead of “object ‘body_vector’ not found” or an httr2 type error.A failed build’s source fallback now checks and uploads the version that was actually built, and only for CRAN packages (#159).
handle_post_build_actions()never passed the built tag on, socheck_for_binary()andupload_source_tarball()each asked thegithub.com/cranmirror for the version, two GitHub REST calls per package. A package not yet on the mirror got no fallback at all, a lagging mirror pointed the check at the previous version’s tarball, and a non-CRAN package sharing a CRAN package’s name could have CRAN’s source published in its place.upload_source_tarball()gains aversionargument andcheck_for_binary()now honours its existing one; the default"latest"resolves the current version from CRAN’s own metadata instead of the mirror.A package classified as R-minor sensitive is now built into its per-minor slot even when a binary of the same version already sits in the flat slot (#156).
check_s3_packages()correctly found the per-minor slot empty and scheduled the build, but the skip check inbuild_single_tag()only looked at the flat slot and the flatArchive/, found the old binary, and skipped, so the per-minor binary was never built.check_build_skip_conditions()gains anis_r_minor_sensitiveargument, and it andcheck_s3_root_package()now derive their keys from one helper,remote_binary_keys().A package version listed in
s3_package_cacheis no longer skipped when this slot holds only its CRAN source fallback, or holds nothing and the name came from another R minor’s directory or anArchive/(#155). The fast path incheck_s3_packages()comparedpkg_ver.tar.gzagainst the cache’s basenames with a plain%in%, so it had the source-fallback blind spot already fixed incheck_s3_root_package(), and a recursive listing reduced to basenames let4.5/pkg_1.0.tar.gzsatisfy a 4.6 or flat run. A cache hit is now only a candidate: the object at this run’s slot (its root, then itsArchive/<pkg>/) is checked withremote_object_state(), and only a binary counts as built. Versions missing from the cache still cost no S3 call.A package version whose ABI classification failed is now classified again on the next run instead of being pinned to per-minor builds forever (#161).
classify_r_minor_sensitive()falls back toTRUEwhen the CRAN source download or the classification errors, and it stored that fallback in the ABI cache as if it were a real verdict. Every later run got a cache hit, andabi_cache_risky_packages()fed the version into each per-minor index build as risky, so a single transient CRAN outage dropped its generic records for good. Only computed verdicts are cached now, and a failed cache lookup is logged at debug level instead of being swallowed silently.An ABI-risky package whose per-minor build fell back to source no longer disappears from that minor’s index (#160). The per-minor union let a generic binary replace the source fallback whenever the generic record’s
Builtstamp named the target minor, but that stamp is written slot-wide by the indexing R and says nothing about the object. The risky-package drop then removed the generic record as well, so the package was missing entirely, and with no risky signal the promoted binary could be one built under another minor.union_index_records()now promotes a generic binary only when its object has a recorded verdict that it loads under this minor, which is the same test the carried-record drop applies, so a promoted record is never dropped afterwards.A shared object that
nmcannot read now leaves the ABI verdict undecided instead of marking the binary portable (#162).system2(stdout = TRUE)does not raise on a non-zero exit but returnscharacter(0)with astatusattribute, whichnm_symbols()parsed as “references no symbols”. A non-ELF or unreadable.sotherefore madeshared_object_abi_verdict()report the package inspected and not r-minor-sensitive, so the binary went to the generic slot and was recorded safe.nm_symbols()now returnsNULLon a failing exit status, and the source classifier’s decision stands as documented.Packages with
v-prefixed tags (v5.0.0) build again when only some of their versions are missing from the bucket (#154).check_s3_packages()recovered the tags to build by parsing them back out of the artifact names, which carry the version without thev, so the clone asked for--branch=5.0.0and failed with “Remote branch not found”. It now returns the original refs, matched to the missing artifacts by position.archive_package()now keeps the highest version of a package in the root and archives the rest, so a freshly built binary is no longer moved toArchive/behind an older one (#158). The version to keep was read from the latest commit message ongithub.com/cran/<pkg>, without anypackage_versioncomparison, so whenever that mirror lagged CRAN the stale version stayed in the root index and the new binary was archived.find_old_versions()now parses the versions from the file names and compares them withpackage_version(), which also drops a GitHub API call per archived package and thepkgsearch::cran_package_history()fallback.archive_package()now archives old versions of ABI-sensitive packages (#157). Withis_r_minor_sensitive = TRUEit listedcontrib/TRUEinstead ofcontrib/<minor>, found no tarballs, and always skipped with “only one package versions exists”. Old versions therefore piled up incontrib/<minor>/, the per-minorArchive/andMeta/archive.rdsstayed empty, and only emptyArchive/<pkg>prefixes were created. It now lists the slot of the running R minor, the same one the archive paths are built from.Setting
BINCRAFT_LOG_LEVELin uppercase (e.g.DEBUG) no longer makes the package fail to load (#168)..onLoad()validated the level case-insensitively but passed the original string to lgr, which only accepts lowercase level names.The source fallback and archiving now act on the slot the binary was actually uploaded to (#152).
upload_single_binary()overrides the caller’sis_r_minor_sensitivewith the result of its symbol inspection, but returned onlyTRUE/FALSE, sohandle_post_build_actions()still checked, published and archived against the caller’s guess. A package flagged as sensitive but inspected as safe had its binary written to the flat slot and the CRAN source published into<minor>/, where the per-minor record shadowed the binary for clients on that minor and was never replaced, because the next run found the flat binary and skipped the package.upload_single_binary()still returnsTRUE/FALSE, now with anis_r_minor_sensitiveattribute naming the effective slot, andbuild_single_tag()passes it on the same way.isTRUE()andif ()on the return value behave as before, butidentical(x, TRUE)no longer holds after an upload.Regenerating the documentation no longer drops
set_bin_path()from the exports (#167).detect_arch()andvalid_patched_binary()had been inserted betweenset_bin_path()’s andprepare_patched_repo()’s roxygen blocks and their functions, so the nextdevtools::document()merged the blocks and the regenerated NAMESPACE no longer exportedset_bin_path(). Both helpers now sit above the blocks they had split,prepare_patched_repo()’s help no longer describes the pak-erafile://repo and pkgcache snapshot, and theman/pages that had drifted from their sources are regenerated.build_binary_package()with a customlocal_output_dir_rootnow uploads the binary it built instead of recording a successful build for a binary that never reached S3 (#153).build_single_tag()calledupload_single_binary()without the output root, so the upload looked for the tarball under the default".", and a missing local tarball was reported as a successful upload. The build row was then written as published,check_for_binary()found nothing in S3, and the CRAN source was published in place of the binary.upload_single_binary()gains abinary_output_pathargument, whichbuild_single_tag()now passes, and returnsFALSEwith a warning when the local tarball is missing.Dependency installation now reuses the clone
build_single_tag()already made instead of cloning the package a second time from the hardcodedgithub.com/cranmirror (#150). The second clone ignoredsource_org_url, so a package from any other organisation failed with “Remote branch … not found” whenever the mirror lacked the tag (everyv-prefixed tag, internal packages, versions newer than CRAN), and otherwise resolved dependencies from the CRAN release’s DESCRIPTION rather than the source being built.install_pkg_sys_deps()gains aclone_dirargument to reuse an existing clone and asource_org_urlargument for standalone use, whichclone_package_repo()now honours too. uvr runs on a copy of the DESCRIPTION, so itsuvr.toml,uvr.lockand.r-versionnever land in the source tree being built.The two remaining existence gates now tell a source fallback from a binary. Fixing
check_s3_root_package()let a rebuild get as far as the build step, where a second gate inbuild_single_tag()skipped it again with “already exists in S3 andforce = FALSE”, and a third inupload_single_binary()would then have refused to publish the binary because the source occupied its key. All three now share one helper,remote_object_state(), which answers"absent","source"or"binary"rather than a bare yes/no, and a source fallback is overwritten rather than left in place. Unknown stays “binary” throughout, so an unreadable ETag or an unreachable CRAN still cannot schedule a rebuild of the repository.The source-fallback detection now actually reads the object’s MD5.
remote_object_md5()looked for anETagcolumn, buts3fs::s3_file_info()snake-cases thehead_objectresponse and renamese_tagtoetag. Reading a missing column yieldsNULLrather than an error, so every object came back as “MD5 unknown”, every caller took the conservative “assume it is a binary” branch, and the checks added in 5.1.1 and 5.1.2 were inert. A 13,389-package rebuild ofamd64/alpine324running 5.1.2 still skipped every package with “All packages to be built already exist in the remote bucket”. Both spellings are now accepted, and the regression is covered by tests that feedremote_object_md5()a realistics3fsreturn value instead of stubbing it out.The pre-build skip now also tells a source fallback from a binary.
check_s3_root_package()decides whether a build runs at all, and it tested only that something occupied the key. A package whose build failed has its CRAN source published under exactly that name, so a rebuild skipped every one of them with “All packages to be built already exist in the remote bucket” – precisely the set a rebuild exists to fix. Observed onarm64/alpine324, where a 13,647-package rebuild skipped every package it was given. The previous release fixed the same blind spot incheck_for_binary(), which decides whether to publish a fallback, but not this one, which decides whether to build. An object whose MD5 matches CRAN’s publishedMD5sumis now reported as absent, and an MD5 that cannot be established still counts as a binary so an outage cannot trigger a full rebuild.A package that failed to build is no longer mistaken for a binary. When a build fails,
handle_post_build_actions()publishes the CRAN source tarball in its place so the package stays installable, butcheck_for_binary()only tested that an object existed at the path. The fallback therefore became permanent: the build was never retried, and the missing-binaries audit never reported it, because the object was there. Onamd64/alpine324that left 13,547 of 24,134 comparable objects (56%) as CRAN sources, all published during the June 2026 bootstrap;amd64/noblesits at 4.6% for comparison.check_for_binary()now compares the object’s MD5 against CRAN’s publishedMD5sum, which costs no downloads, and reports a source fallback as missing. When the MD5 cannot be established (a multipart ETag, or CRAN unreachable) the object still counts as a binary, so neither can trigger an endless rebuild.check_for_binary()now honoursis_r_minor_sensitive. It looked in the generic slot even for per-minor packages, while the source fallback wrote them to the per-minor slot, so it never found an existing per-minor build.The
Builtstamp is no longer written onto packages served as their CRAN source. The stamp is applied to a whole slot at once, so it landed on the source fallbacks too and advertised them as binaries:uvrthen installs one without the system-devlibraries a source build needs, and paquetier files it under a platform it was never built for.upload_package_index()now clearsBuilton records whose MD5 matches CRAN’s. Archived versions are left alone, since CRAN’s index carries only current releases.A per-minor
PACKAGES*index is now republished as a union with the generic slot. A per-minor slot carries only the ABI-sensitive packages, andcontrib.url()can only ever address<repos>/src/contrib, so those packages were unreachable frominstall.packages(): onamd64/alpine3242,886 packages,curlamong them, existed only in the per-minor slot and were absent from the index base R reads.upload_package_index()now merges the generic slot’s records into the per-minor index before uploading it, tagging the per-minor records withPath: <r_minor>so their tarballs still resolve into…/src/contrib/<r_minor>/while the generic ones resolve into…/src/contrib/. Nothing is copied or duplicated, and a package built for this minor shadows the generic one entirely. An unreadable or empty generic index is an error rather than a partial publish, because replacing a published union with a per-minor-only index would hide most of the repository from every client on that minor.built_stamp()now refuses an unusable platform or R version instead of stamping it. The stamp is written verbatim into every entry of a slot’sPACKAGESindex anduvrdecides binary-vs-source by matching the triple it carries, so a missing value does not degrade gracefully: it stamped the literalNA, no client matched it, and the whole slot silently reverted to source-only. This is howarm64/alpine321(22,930 entries,Built: R 4.4.0; NA) andarm64/alpine322(24,696 entries,Built: R 4.5.0; NA) were written during the one-time re-index on 2026-07-31; both slots need another full re-index to recover. Failing the index write is the cheaper outcome, so an absent,NA, empty or non-scalarplatform/r_versionis now an error.Dependency names are now written as quoted keys in the generated
uvr.toml. R package names may contain dots, and a baredata.table = "*"is a dotted TOML key: it parses as packagedatawith a sub-keytablerather than asdata.table. Where the parent name is itself a dependency, for examplerpartalongsiderpart.plot, the parse fails outright with “dotted keyrpartattempted to extend non-table type (string)”. Either wayuvr lockaborted and the build failed with “Error in installing dependencies for package …”, so every package depending on a dotted name was unbuildable.uvris now pinned to the R version that runs the build.run_uvr_install()writes a.r-versionfile into the package clone beforeuvr lock, so bothlockandsynctarget the session’s R. Previously bincraft told uvr nothing about the R version, and uvr picked the newest R it could find (managed installs first, then system R). On an image carrying a second, newer R, for example a/opt/R/currentsymlink already moved to 4.6 while the build runs in 4.5, uvr resolved the lockfile for the wrong R minor anduvr syncthen refused to install anything: “Refusing to install: uvr is running inside R 4.5 but the project pin/lockfile resolves to R 4.6”. That aborted every dependency install on the affected images.The S3 package index now advertises its binaries via the
Builtfield.upload_package_index()andadd_to_package_index()pass abuiltstamp (build R version plusR.version$platformtriple) to cranlike, which otherwise reads metadata from the CRAN sourceDESCRIPTIONand leavesBuiltempty for every entry. Without it, binary-aware clients such asuvrtreatcran.rpkgs.comas source-only and compile every package from source, which fails on Alpine when a system-devlibrary is missing (e.g.openssl).install.packages()was unaffected because it readsBuilt:from each tarball’s ownDESCRIPTION. Existing indexes need a one-time full rebuild to backfillBuilton entries already in the database.Dependencies and their system requirements are now installed with
uvrinstead of pak duringbuild_binary_package(). uvr reads/etc/os-release, resolves each dependency’s system requirements per distro, and installs them with the distro package manager (apk/dnf/apt-get) in the same pass as the R packages, so a build no longer fails when a dependency needs a system library the image does not ship (for examplegertneedinglibgit2). Patched binaries are installed into the build library first souvr synckeeps them, and pak is no longer a dependency (#83).Patched binary caches are now validated before reuse and written atomically.
prepare_patched_repo()verifies each cached/assembled tarball reads cleanly and carries the packageDESCRIPTIONplus a shared object (when it ships alibs/dir) before serving it, and rebuilds instead of reusing a corrupt entry. Fresh binaries are staged to a temp file and renamed into place, so a build killed mid-copy can no longer leave a truncated cache entry that made every dependent build fail withtar: A lone zero blockor a missingRcppParallel.so.CRAN package versions are now resolved from CRAN’s own metadata (
available.packages()plusMeta/archive.rds) instead of the GitHub REST tags API. The oldgh::gh("GET /repos/cran/{pkg}/tags")call counted against the authenticated user’s 5,000 requests/hour REST limit, which the weekly rebuild exhausted (HTTP 403 “API rate limit exceeded for user ID …”). The GitHub API is still used for genuine non-CRAN forge sources.classify_r_minor_sensitive()now downloads the CRAN source tarball (download_cran_source()) instead of cloninggithub.com/cran, and caches its verdict in the metadata database (tableabi_classification, created on first use) keyed on(package, version)plus a signature of the curated ABI lists. Because the r-minor-sensitivity verdict is a property of the package source (independent of OS, arch and R minor), a full rebuild for a new OS release reuses cached verdicts and performs no downloads. The function is now exported and takesmetadata_db_*arguments; pass them to enable caching.
bincraft 4.4.7
- The local patched-binary repo served to
pakis now assembled at a stable, content-addressed path undercache_dirinstead of a freshtempfile()per call. Everybuild_binary_package()resolution in a run passes the same patches/platform/arch/R, so they now resolve to the samefile://repo URL and pkgcache reuses a single_metadatasnapshot. Previously each resolution minted a new repo path, so pkgcache wrote a fresh ~70 MB_metadata/patched-<hash>snapshot that never repeated and never evicted, growing unboundedly during full-platform builds (up to ~165 GB observed). A fully assembled repo is now also reused as-is on subsequent resolutions.
bincraft 4.4.3
- Registry patches (and their
env/configure_args/makevarsoverrides) are now applied to the target package’s own source beforepkgbuild::build(), not only when building patched dependencies. Previously a package that was the build target had its patch applied to a throwaway dependency repo and ignored for the uploaded binary, so source-diff/override patches silently had no effect on the shipped binary. A target patch that fails to apply now aborts the build instead of uploading an unpatched binary.
bincraft 4.4.2
-
build_patched_binary()no longer fails withis.named(envs) is not TRUEfor a patch entry that sets no environment variables (e.g. a pure source-diff patch).withr::with_envvar()errors on an empty list, so it is now skipped when the entry’s env is empty.
bincraft 4.4.1
- Source patches are now applied with
git applyinstead of thepatchCLI, which is not present in all build environments (e.g. minimal Alpine images).gitis always available, so registry source diffs apply reliably across platforms. -
prepare_patched_repo()no longer emits a spuriousnormalizePath: No such filewarning when staging a freshly built patched binary. -
build_patched_binary()no longer fails withis.named(envs) is not TRUEfor a patch entry that sets no env vars (e.g. a pure source-diff patch); thewithr::with_envvar()wrapper is now skipped when the env is empty.
bincraft 4.3.1
-
process_cran_updates()gains apatchesargument that it forwards tobuild_binary_package(), so the daily update pipeline applies the same package patches (e.g. RcppParallel) as the bulk and targeted build paths.
bincraft 4.3.0
-
build_binary_package()gains apatchesargument: a registry of per-package env / configure / Makevars overrides and source diffs that are pre-built into patched binaries and served to pak, fixing compiler- and OS-specific failures (e.g. RcppParallel) including for transitive deps.
bincraft 4.2.1
-
write_archive_rds()(and thusupload_package_index()) no longer errors when a slot has no archived versions yet — it returns an empty index instead. Fixes theMeta/archive.rdsfailure the first time a package lands in a fresh per-minor slot.
bincraft 4.2.0
-
process_cran_updates()gainsr_minor_detection("none"/"issue"/"classifier") andr_minor_sensitive_only, classifying each candidate via the ABI classifier and routing onlyriskypackages to per-minor slots. -
upload_package_index()/add_to_package_index()gain anr_minorargument to write/serve a per-minorPACKAGES*index under…/contrib/<x.y>/.