
Create custom R metapackages from local packages.
bigbang builds tidyverse-style metapackages from
local package archives. Every metapackage ends in
-verse—tidyverse, teamverse, yours.
This package creates them: one function call, and a new -verse
exists.
Its reason to exist is distributing a set of packages as a single unit.
Say you maintain four packages of your own that are used together and
depend on each other. Someone joins the team, or another office asks for
them. They are not on CRAN, so the way to hand them over is a folder of
.tar.gz files.
At best you add instructions: install this one first, then that one, this version goes with that one. But instructions are manual work for whoever receives them, and one more document to keep current every time a version changes or a package joins the set.
bigbang puts that knowledge inside the package instead. The order comes from the real dependency graph and the versions are recorded in the generated metapackage, so there is nothing to follow by hand and nothing that can drift out of step with what the folder actually contains. The set you curated is the set they install, in a single line.
create_metapackage() scaffolds a complete, documented,
CRAN-checkable metapackage from your own package archives..tar.gz,
.tar and .zip, carry no version in the
filename, be listed in a manifest file, or be source directories that
bigbang builds for you. Identity and version are read from each
archive’s DESCRIPTION, not guessed from its name.<meta>_install() works with no arguments and no path
has to be agreed on between machines.cran_deps = "skip" is the default. This is what makes
bigbang work behind an institutional firewall, but it is just as useful
for shipping a versioned bundle anywhere.The generated package has two separate jobs:
library(<meta>) attaches components that are
already installed and reports missing ones.<meta>_install() explicitly installs local
archives once, in topological dependency order, and then attaches
them.The stable version is on CRAN:
install.packages("bigbang")The development version is served as a binary from r-universe, so it needs no compilation:
install.packages("bigbang", repos = c("https://sebollin.r-universe.dev",
"https://cloud.r-project.org"))Or from the sources on GitHub:
# install.packages("pak")
pak::pak("sebollin/bigbang")
# or
remotes::install_github("sebollin/bigbang")And true to the package’s offline spirit, a local source checkout installs without any network at all:
install.packages("path/to/bigbang", repos = NULL, type = "source")Suppose archives/ contains:
archives/
├── datahelpers_1.2.0.tar.gz
└── reports_0.9.1.tar.gz
Create the metapackage in a new directory:
library(bigbang)
result <- create_metapackage(
name = "teamverse",
packages = c("datahelpers_1.2.0", "reports_0.9.1"),
pkg_dir = "archives",
dest_dir = tempdir(),
document = TRUE
)
resultBuild and install teamverse by the usual R package
workflow. Component installation remains explicit:
library(teamverse) # attaches what is already installed
teamverse_install() # installs them, in dependency orderAttached component exports are available directly (for example,
report()) or through their own namespace
(reports::report()). They are not copied into the
metapackage namespace, so teamverse::report() is not
supported.
To expose explicit component exports through read-only runtime
bindings, use reexport = TRUE when generating. Components
remain outside Imports and Depends, so the
metapackage can be installed and loaded offline before they exist.
Evaluating a binding never throws: when a component is absent, cannot be
loaded, or is an older installation that no longer exports the symbol,
it returns a callable placeholder. Calling it reports the component,
installed version, missing export, and the
<meta>_install() call that repairs the installation.
This also keeps namespace inspection (as.list(),
mget(), and IDE environment panels) safe. For non-function
exports, access returns the placeholder instead of the object until
installation. The binding then resolves the real function or object
without reloading the metapackage. Only explicit export()
directives become bindings, including non-syntactic names, which are
quoted safely in NAMESPACE. S4 classes and methods remain available by
loading the component. An object restored with readRDS()
does not load a component by itself, so base R cannot dispatch that
component’s S3 method until the component has been loaded.
When several components export the same symbol, use
reexport_prefer = c(symbol = "component") to select its
provider or reexport_exclude = "symbol" to omit it. Every
collision requires one of those choices because static analysis cannot
prove that two exported objects are the same at runtime. The diagnostic
labels collisions as probable_same_object,
distinct_definitions, or undetermined, and
reports ordered source reasons. For a preferred
probable_same_object, <meta>_install()
verifies installed owners in a clean R subprocess using the same ordered
libraries as runtime: the destination library first, followed by
.libPaths(). An owner installed only in a later library is
therefore resolved and compared to the object the user will obtain. The
result distinguishes not installed, not exported, and loaded from
another library; an installed owner that no longer exports the symbol is
reported with the same verification warning. If the subprocess cannot
run, the result is explicitly unverified. A namespace already loaded
from another library is reported before verification.
<meta>_conflicts() repeats the check and emits the
warning again, so it is the way to re-verify after installation. A
FALSE identity warning distinguishes equivalent function
copies (same body and formals) from distinct objects. If
on_component_error = "skip" omits a required owner,
generation errors rather than emitting a binding to a component that is
not included. The returned conflicts object keeps masking conflicts and
stores the verification data frame as an attribute. Use
<meta>_reexport_verification(conflicts) to read it,
so a component exported under that name remains visible in the conflict
list.
The collision analysis is a diagnostic aid. The guarantee is the
explicit reexport_prefer or reexport_exclude
choice plus <meta>_install()’s verification;
library(<meta>) alone does not verify installed
owners. The scanner is conservative and may count a never-forced
delayedAssign, an if (FALSE) branch, or a
reg.finalizer() body.
teamverse carries its components, so the call takes no
arguments and that is all anyone who receives it has to do. Hand over
the built teamverse_0.1.0.tar.gz and nothing else: no
folder of archives alongside it, and no path to agree on beforehand. If
the archives should stay in a shared location instead, generate with
include_archives = FALSE; then
teamverse_install() requires an explicit
pkg_dir.
An interrupted update = TRUE leaves a durable journal
beside the project. The next update inspects it before writing; use
dry_run = TRUE to preview the action. After confirming that
no other update is running, pass recover = TRUE to preserve
unknown user bytes and complete the rollback or recovery.
The lock is decided by the owner before the claimant. A live
published owner always blocks. An uncertain owner blocks unless
recover = TRUE; a dead owner can be reclaimed. A discarded
lock with a live owner.rds is restored or blocks on its PID
and is never deleted. Only when that owner is proven dead does the
claimant state decide whether the entry is blocked, needs
recover = TRUE, or can be discarded. Lock and journal
mutations revalidate ownership before continuing; if the published owner
changed, the update aborts before its next mutation. A symlink at the
lock name is reported as a symlink, and recover = TRUE
moves the link itself without following its target. During discard, the
journal is first renamed to an unpredictable private sibling after its
inventory is checked, and every deletion rechecks ancestors and MD5 just
before unlink(). R has no
unlinkat()/O_NOFOLLOW, so a same-user process
that actively replaces journal directories during discard remains an
integrity boundary, like forged records; the remaining race window is
measured by the last recheck-to-unlink interval.
On Linux, liveness uses /proc/<pid> and treats
zombie (Z) and dead (X) states as dead.
Without /proc, a failed kill(pid, 0) is
uncertain unless ps -p proves that the PID is absent;
permission errors and another user’s process are never treated as dead.
A same-user process with write permission can forge these records; that
is outside the integrity model. Recovery also requires the owner fields
of state.rds and marker.rds to match,
otherwise the journal is set aside rather than used for rollback.
The liveness proof and process-start token are selected by platform:
| Platform | Existence proof | Start token | Policy |
|---|---|---|---|
Linux with /proc |
/proc/<pid>/stat |
field 20, source proc |
A non-terminal PID whose token matches is proven live. |
macOS, BSD, or Unix without /proc |
kill(pid, 0) or
LC_ALL=C ps -p <pid> |
LC_ALL=C ps -o lstart= -p <pid>, source
ps |
A live PID whose lstart matches is proven live. |
| Windows | Never probed with tools::pskill() |
None | Ownership is uncertain; recovery never overrides a proven live owner. |
The source is stored with the token, so a /proc token is
never compared with a ps token. LANGUAGE and
LC_TIME cannot change the portable ps
token.
cran_deps = "skip" is the default and never accesses the
network. Use "error" to fail immediately when a non-local
dependency is missing, or "install" with an explicitly
configured repos value to allow repository
installation.
Generated installers also accept upgrade = "newer" (the
default), "always", or "never";
force = TRUE is the concise form of
upgrade = "always". Generated metapackages use an optional
cli two-column attachment message and fall back to their
ASCII banner when cli is unavailable. Set
options(teamverse.quiet = TRUE) to silence startup
messages, or call teamverse_conflicts() to inspect masking
conflicts.
For an ordered pipeline guide, supply every component once in a named workflow:
workflow = c("Import" = "datahelpers", "Report" = "reports")During generation, bigbang validates everything that protects the recipient of a generated metapackage. Unsafe or malformed archives, invalid component metadata, duplicate components, unsatisfied local version constraints, and dependency cycles are always hard errors and cannot be disabled. Installation is more tolerant: an already installed component can be kept when an archive it will not use cannot be read, and the reason is reported.
Checks about project tidiness can be relaxed individually and explicitly:
create_metapackage(
# ...,
tolerate = c("filename_mismatch", "unincluded_local_dep")
)"filename_mismatch" silences warnings when an archive
filename differs from its DESCRIPTION identity.
"unincluded_local_dep" changes the error for a local
dependency available in the supplied sources but omitted from
packages into a warning. The generated metapackage will not
ship that dependency, so the recipient must provide it through
pkg_dir or a repository with
cran_deps = "install". Applied relaxations are recorded in
result$tolerated; unknown names are errors. There is
deliberately no switch that disables all validation.
bigbang does not run R CMD check on
component packages. A component with check warnings or notes can be
included; validation is limited to whether the distributed metapackage
can identify and safely install its components.
See vignette("getting-started", package = "bigbang") for
a reproducible toy project created entirely under
tempdir().
Any element of packages that is an existing file is used
as a path; anything else is resolved as a stem in pkg_dir,
which accepts more than one directory. So all of these work, including
mixed together in one call:
create_metapackage(
"teamverse",
packages = c(
"/srv/archives/first_1.2.0.tar.gz", # a path, any directory
"~/builds/second.zip", # another directory, another format
"third_0.4.0", # a stem resolved in pkg_dir
"~/src/fourth" # a source directory, built for you
),
pkg_dir = c("/srv/archives", "~/builds"),
dest_dir = "~/projects"
)A filename without a version is fine: Package and
Version come from the archive’s DESCRIPTION.
If the filename disagrees, bigbang warns and trusts the
DESCRIPTION.
A bare package name such as "geomides" also works when
exactly one archive in pkg_dir declares
Package: geomides. Matching uses the declared package
identity, so "geo" never selects geomides; if
several versions or sources match, bigbang lists the candidates and asks
for an explicit stem or path. Unreadable archives encountered during
that search are excluded with a warning that names each affected
file.
Source directories are built with the optional pkgbuild
package, in a temporary directory, and require
include_archives = TRUE, because the archive built for them
does not outlive the call.
packages can also be the path to a
manifest: one component per line, # for
comments. Relative paths in it resolve against the manifest’s own
directory; absolute paths and ~ paths are used as written;
and bare filenames are also looked up in pkg_dir, so the
list can live under version control while the archives do not.
plan <- create_metapackage(..., dry_run = TRUE) # resolve, validate, write nothing
plan$order # installation order
plan$files # what would be written
plan$findings # every validation findingdry_run = TRUE does not create dest_dir and
does not touch the destination at all, so it is a safe way to see what a
call would do before it does it. During an update it also plans
reconciliation of every sibling journal folder and reports each path and
action without mutating those folders.
on_component_error = "skip" generates from the
components that are valid instead of aborting, and reports the ones it
left out. The exclusion is transitive: a component that depends on an
excluded one is excluded too, and the chain is reported. When an invalid
archive still has a readable DESCRIPTION, its declared
package name drives this propagation; otherwise bigbang falls back to
the filename and reports that limitation. Excluding everything is an
error. During an update, a failed input never authorizes deletion of an
archive already shipped by the project. If the old component cannot be
identified unambiguously, archive reconciliation waits for a clean
update.update = TRUE regenerates in place. Generation records
a manifest of the files it wrote together with their content hashes;
update rewrites only those, and refuses to run if the
manifest is missing or if a generated file was modified or removed by
hand. Files bigbang did not write are never touched. Before changing an
existing project, bigbang backs up every generated file and its
manifest. A failed update restores that state so the same update can be
retried. Both dry runs and real results list removed paths in
removed_files. Removing a component removes its shipped
archive, which may be the last available copy. When an update grows the
plan, files absent from both the manifest and the project are new and
are written; existing files outside the manifest are treated as user
content and the update aborts without overwriting them. The result
reports new paths in added_files, including a newly added
component, a re-added component, a component version bump, or a workflow
vignette. Updates hold an exclusive project lock from preparation
through rollback and journal publication. The lock is published only by
renaming a sibling temporary folder that already contains
owner.rds, so every published lock has an owner. An orphan
is first renamed to a unique discarded name; the winner rechecks that
owner before publishing its replacement. A regular file or other user
entry at the lock name is set aside as
.<name>.bigbang-apartado-*, without deleting its
bytes. recover = TRUE resolves uncertainty (Windows,
missing /proc, another host, or an unreadable owner), but
never overrides a proven live owner: same host, live PID, and the same
process-start token. That case errors and reports the PID. The sibling
names .<name>.bigbang-update,
.<name>.bigbang-update.armando-*,
.<name>.bigbang-update.lock,
.<name>.bigbang-update.lock.armando-*,
.<name>.bigbang-update.lock.descartado-*,
.<name>.bigbang-update.descartado-*, and
.<name>.bigbang-apartado-* are reserved for these
operations. Updates also refuse to write through a symbolic project root
or symbolic links inside the generated project, including links in
parent directories of generated files..<name>.bigbang-update.armando-* folder and renamed
to .<name>.bigbang-update only after the marker and
backup have been verified. An empty unmarked preparation is removed; any
non-empty unmarked preparation is atomically set aside as
.<name>.bigbang-apartado-*, without copying or
deleting bytes. Discarding a journal first writes an atomic tombstone
with the exact recursive relative-path and MD5 inventory of entries
bigbang wrote, records a digest beside the tombstone, and then renames
it to .<name>.bigbang-update.descartado-*, so cleanup
resumes after another interruption. Cleanup removes only files whose
path and MD5 match that inventory, and only empty inventory directories.
Any other file, directory, or symbolic link sets the whole folder aside
and the update continues. A missing or changed tombstone digest has the
same outcome. The next update = TRUE call can recover a
project moved together with its journal. Renaming a project is not
supported because generated file names contain the metapackage name:
rename the project and its journal back to <name>. A
byte-for-byte copy placed at the same path and name as the moved
original is indistinguishable from that original, so the journal treats
it as the project. If the original still exists beside a copied journal,
that journal is not adopted or changed. A discarded sibling from another
generation or project is set aside with an actionable message. A user
file with the same path and MD5 as an inventory entry is an unavoidable
boundary: the bytes are identical, so deleting it loses no content, but
ownership is not provable. It records every intended write and removal
and is designed to survive process interruptions such as SIGKILL, an R
error, or Ctrl-C; it does not promise fsync durability against an OS or
power shutdown. If a path contains neither its original nor an intended
value, recovery stops instead of overwriting it. On Windows, an absent
destination is known only while the matching intended temporary remains
in the journal staging area. Every original file restored while absent
is listed in the recovery result and message. After confirming that no
update is still running, recover = TRUE preserves those
unknown bytes in a reported sibling directory and then recovers. A dry
run reports the pending action without changing the project, lock, or
any sibling journal folder. The lock is only evaluated and reported as
free, live, orphaned, or uncertain. Documentation generation failures in
the staging copy are warnings; a failure while promoting a documentation
file aborts and rolls the complete update back. On Windows liveness is
never tested with tools::pskill(), because it would
terminate the probed process.install_upgrade fixes the default upgrade policy of the
installer that gets emitted, so you decide when generating whether
recipients stay pinned to the versions you ship ("always")
or keep anything newer they already have ("newer", the
default).The generated installer also takes only to install a
subset — local dependencies of the selection are added automatically —
and lib to choose the library it installs into.
create_metapackage() creates a complete metapackage
source tree.install_local_pkg() installs one local archive and its
dependencies.diagnose_dependencies() reports possible implicit
dependencies.scan_bigbang_artifact() scans old source trees,
archives, or installed packages for historical deletion signatures
without loading them.ZIP archives are classified by content. A ZIP containing
Meta/package.rds is a Windows binary and is installed with
type = "win.binary" on Windows only. Other ZIPs containing
DESCRIPTION are unpacked into an owned temporary directory and installed
as source packages.
All generated text is written explicitly as UTF-8. CI is prepared for
R release on Windows and macOS and for release, devel, and oldrel on
Ubuntu. The declared minimum is R 3.6.0, following the minimum of the
imported brio release.
English is the source language for code, help, and runtime messages. A complete Spanish runtime catalog is included through R’s gettext mechanism:
Sys.setLanguage("es") # R >= 4.2On earlier R versions, set LANGUAGE=es before starting
R. A complete Spanish guide is available in
vignette("bigbang-es", package = "bigbang") and as README.es.md.
Rd help remains English because R has no stable native mechanism for
translated help; a separate bigbang.es module can be
considered if rhelpi18n becomes production-ready and
reaches CRAN.
<meta>_packages() design in
bigbang-generated metapackages draw on tidyverse and on metaverse
(Westgate and colleagues).| Need | Best fit |
|---|---|
| Distribute one curated, version-pinned set of local archives as a single installable unit | bigbang |
| A conventional local repository with indexes, multiple packages, and repository semantics | miniCRAN or drat |
| A small metapackage around packages already available from repositories | pkgverse |
bigbang deliberately does not replace a repository
manager. If a team needs version retention, repository indexes, or
dependency distribution to many projects,
miniCRAN/drat is the stronger abstraction.
bigbang is useful when the distributed unit is a curated
metapackage plus a directory of archives.
An unreleased predecessor generated cleanup code that could remove directories named after components from the user’s working directory. The startup installer and all cwd-relative cleanup paths were removed before this CRAN submission and are covered by destructive regression tests that run only in disposable trees.
Do not load or document an old generated artifact before classifying it:
scan <- scan_bigbang_artifact("path/to/artifact", dry_run = TRUE)
scanIf scan$vulnerable is true, quarantine the artifact and
generate a new version in a new, empty destination. Never regenerate an
unclassified source tree in place. The full remediation procedure is
documented in the Spanish guide and in RELEASE.md.
bigbang started from a suggestion by Richard Detomasi, who proposed building a metapackage tool and pointed to pegeler/metapackage as an antecedent. The design and implementation—including the graph-based dependency resolution—are by Sebastián Lucas. The hex logo was created with hexSticker.
Contributions are welcome: bug reports and feature ideas through issues, and pull requests following CONTRIBUTING.md (spelling, lint, and test expectations are documented there). The package aims to stay small and focused — see Choosing the right tool above for what deliberately stays out of scope.
citation("bigbang")@Manual{bigbang2026,
title = {bigbang: Build 'Tidyverse'-Style Meta-Packages from Local Package Files},
author = {Sebastián Lucas},
note = {R package version 0.3.0},
year = {2026},
url = {https://sebollin.github.io/bigbang/},
}The complete test suite includes unit, portability, i18n, scanner,
installation, and data-loss regression tests.
R CMD check --as-cran runs with the PDF manual enabled for
both bigbang and a generated metapackage, on every push,
across Ubuntu (release, devel, oldrel), Windows, and macOS. win-builder
and rhub::rhub_check() results are reviewed before each
CRAN submission.