Skip to contents

courieR syncs installed R packages between R versions on the same machine. You can migrate from an old version to a new one, or selectively push packages from a source installation into a target installation — all from the R console or from a point-and-click dashboard.

Installation

Install courieR from CRAN:

install.packages("courieR")

Or install the development version from GitHub:

pak::pkg_install("lennon-li/courieR")

All dependencies — including the dashboard packages — are installed automatically.

Step 1 — Discover Your R Installations

find_routes() scans the system and returns a data frame of every R version it can find:

library(courieR)

routes <- find_routes()
routes
#>   version                                          rscript_path is_current
#> 1   4.4.2        C:/Program Files/R/R-4.4.2/bin/x64/Rscript.exe       TRUE
#> 2   4.3.1  C:/Users/you/AppData/Local/Programs/R/R-4.3.1/bin/...      FALSE
#> 3   4.1.3            C:/Users/you/Documents/R/R-4.1.3/bin/...      FALSE

is_current = TRUE marks the R session you are running right now.

Platform detection

Platform Locations checked
Windows HKLM/HKCU registry, %ProgramFiles%\R, %LOCALAPPDATA%\Programs\R, %USERPROFILE%\Documents\R, rig
macOS /Library/Frameworks/R.framework, ~/Library/Frameworks/R.framework, Homebrew (/opt/homebrew, /usr/local), rig
Linux /opt/R (rig system), ~/.local/share/rig/R (rig user), conda envs, system Rscript on $PATH

To include a non-standard path, pass it explicitly:

routes <- find_routes(search_paths = "/opt/custom-r/bin/Rscript")

CLI Workflow

The four core functions form a pipeline:

find_routes()  →  manifest()  →  inventory()  →  ship()
   discover        scan            compare         migrate

Step 2 — Scan a library with manifest()

manifest() runs a subprocess under a given Rscript and returns every installed package with its version and source:

# scan the first (newest) R
src_pkgs <- manifest(rscript_path = routes$rscript_path[1])
head(src_pkgs[, c("package", "version", "source")])
#>     package version source
#> 1     broom   1.0.7   CRAN
#> 2    callr   3.7.6   CRAN
#> 3   courieR   0.2.0 GitHub
#> 4   ggplot2   3.5.1   CRAN
#> 5      glue   1.8.0   CRAN
#> 6   stringr   1.5.1   CRAN

Calling manifest() with no arguments scans the library of the current R session:

my_pkgs <- manifest()
nrow(my_pkgs)
#> [1] 312

Base and recommended packages are excluded automatically — the returned data contains only user-installed packages.

Step 3 — Compare two libraries with inventory()

inventory() takes two manifests and returns a classified diff:

src_pkgs <- manifest(rscript_path = routes$rscript_path[2])  # old R
tgt_pkgs <- manifest(rscript_path = routes$rscript_path[1])  # new R

comp <- inventory(src_pkgs, tgt_pkgs)

The result is a list with three elements:

# packages in source but missing from target
nrow(comp$missing)
#> [1] 47

# packages where source has a newer version
nrow(comp$outdated)
#> [1] 12

# packages at the same version in both
nrow(comp$same)
#> [1] 201

Inspect what needs to move:

comp$missing[, c("package", "version", "source")]
#>       package version  source
#>  1:    bookdown   0.39    CRAN
#>  2:  brms   2.21.0    CRAN
#>  3:   officer   0.6.6    CRAN
#>  ...

comp$outdated[, c("package", "version.x", "version.y", "source")]
#>    package version.x version.y source
#> 1:  ggplot2     3.5.1     3.4.4   CRAN
#> 2:   tibble     3.2.1     3.2.0   CRAN

Step 4 — Migrate with ship()

ship() takes a source and target Rscript path, computes the plan internally, and runs it:

result <- ship(
  source_path = routes$rscript_path[2],   # old R — package source
  target_path = routes$rscript_path[1]    # new R — install destination
)

Always dry-run first

result <- ship(
  source_path = routes$rscript_path[2],
  target_path = routes$rscript_path[1],
  dry_run = TRUE
)

# review the plan before anything is installed
print(result$plan)
#>         package action  pak_spec
#>  1:    bookdown install bookdown
#>  2:        brms install     brms
#>  3:     ggplot2 upgrade  ggplot2
#>  ...

Include version upgrades

By default, ship() only installs missing packages. Pass upgrade = TRUE to also update packages that are present but at an older version (mirrors what the Bulk Dispatch tab does):

result <- ship(
  source_path = routes$rscript_path[2],
  target_path = routes$rscript_path[1],
  upgrade = TRUE
)

Check results

# per-package outcomes
result$results
#>     package  status                       message
#>  1: bookdown success                     Installed
#>  2:     brms success                     Installed
#>  3:    rJava   error  installation of rJava failed...

# count by outcome
table(result$results$status)
#>   error success
#>       1      58

# failed packages only
result$results[result$results$status == "error", ]

Migrate GitHub and Bioconductor packages

ship() detects source-specific packages automatically. CRAN packages are reinstalled from CRAN; GitHub packages become "owner/repo" pak specs; Bioconductor packages use "bioc::pkg" specs. No extra configuration needed.

# mixed-source example — all handled automatically
result$plan[, c("package", "source", "pak_spec")]
#>    package   source        pak_spec
#> 1:  ggplot2     CRAN         ggplot2
#> 2:  courieR   GitHub  lennon-li/courieR
#> 3:    DESeq2  Bioconductor  bioc::DESeq2

Transfer modes

ship() (and the dashboard’s Transfer mode selector) offers three mutually-exclusive ways to move packages. Pass one via mode =:

Mode What it does When to use
"online" (default) Reinstall each package fresh via pak — latest compatible version, dependencies resolved. Pure-R and local packages are copied directly (no rebuild); only compiled packages are reinstalled, preferring pre-built binaries. Moving between different R versions, where compiled packages must be rebuilt for the new R.
"offline" Copy package files directly from the source library — fast, no internet, exact same versions. Same R x.y series, or air-gapped machines.
"preserve" Same direct copy as offline, but anything that can’t be copied is reinstalled via pak at its exact source version. Same series, when you want a network safety net.

Why copy modes need the same R x.y series

A package’s compiled code (the .dll/.so files under libs/) is built against a specific R minor version’s binary interface. Packages compiled for R 4.5.x will not reliably load under R 4.6 — they must be rebuilt. So:

  • Within the same series (e.g. 4.5.0 ↔︎ 4.5.2) copying is binary-safe, and offline/preserve are the fast choice.
  • Across minor versions (e.g. 4.5 → 4.6) compiled packages must be reinstalled, so only online is safe. The dashboard hides the copy modes for such pairs automatically.

Pure-R packages (no libs/) are version-independent, so online copies those directly even across versions and only rebuilds the compiled minority.

# explicit mode
ship(source_path = old_r, target_path = new_r, mode = "offline")

Common Recipes

One-way migration (old R → new R)

The shortest path uses migrate(), which looks up installations by version string so you don’t need to handle paths yourself:

library(courieR)

# dry run first
migrate("4.5.2", "4.6.0", dry_run = TRUE)

# for real
result <- migrate("4.5.2", "4.6.0")
table(result$results$status)

For more control — filtering by package, custom paths, or progress callbacks — use ship() directly:

routes  <- find_routes()
old_r   <- routes$rscript_path[!routes$is_current][1]
new_r   <- routes$rscript_path[routes$is_current]

result <- ship(source_path = old_r, target_path = new_r, upgrade = TRUE)

Mirror two same-version installs

ship() always moves packages one way, from source_path into target_path. To bring two same-version installations into line with each other, run it twice, swapping the roles:

r_a <- routes$rscript_path[1]
r_b <- routes$rscript_path[2]

# push everything A has into B
ship(source_path = r_a, target_path = r_b, upgrade = TRUE)

# then push everything B has into A
ship(source_path = r_b, target_path = r_a, upgrade = TRUE)

Inspect before migrating

src  <- manifest(rscript_path = old_r)
tgt  <- manifest(rscript_path = new_r)
comp <- inventory(src, tgt)

# only migrate CRAN packages — skip GitHub/unknown sources
cran_missing <- comp$missing[comp$missing$source == "CRAN", ]
cat(nrow(cran_missing), "CRAN packages to install\n")

Save a manifest to disk

pkgs <- manifest(rscript_path = routes$rscript_path[1])
write.csv(pkgs[, c("package", "version", "source")], "my_packages.csv", row.names = FALSE)

Restore from a saved manifest on a new machine:

saved <- read.csv("my_packages.csv", stringsAsFactors = FALSE)
# pak installs from a character vector of package names
pak::pkg_install(saved$package)

Dashboard

hub() launches a Shiny dashboard that wraps the same pipeline:

Bulk Dispatch

The Bulk Dispatch tab compares two R libraries and ships all missing or outdated packages in one operation.

Bulk Dispatch: detect R installations, click Compare, review the colour-coded summary chips and package table, then Ship. The log pane on the right shows real-time progress; a live hero panel counts delivered packages during the ship.

Bulk Dispatch: detect R installations, click Compare, review the colour-coded summary chips and package table, then Ship. The log pane on the right shows real-time progress; a live hero panel counts delivered packages during the ship.

Workflow:

  1. The dashboard scans all R installations automatically — no configuration needed.
  2. Select a source and a target from the sidebar. The target list is constrained to the same-or-newer R minor version.
  3. Click Compare. The coloured chips above the table show counts for each status (missing from target, newer in source, etc.). By default only missing from target packages are shown — click any chip to add or remove statuses from the view.
  4. Click Ship to install the source’s packages into the target. A confirmation dialog shows the package count and an estimated time.
  5. The log pane updates in real time; a live hero panel counts packages as they are delivered. Detailed pak output streams to the R console.

The first sync on a machine can take 1–2 minutes while pak builds its metadata cache. Subsequent syncs are faster.

Custom Dispatch

The Custom Dispatch tab lets you cherry-pick individual packages instead of bulk-shipping everything.

Custom Dispatch: colour-coded filter chips narrow the table to the statuses you care about; check individual packages and press Ship. The Repo column highlights packages with an unknown source in red.

Custom Dispatch: colour-coded filter chips narrow the table to the statuses you care about; check individual packages and press Ship. The Repo column highlights packages with an unknown source in red.

Workflow:

  1. After a Compare in Bulk Dispatch, open the Custom Dispatch tab — the same comparison is shared, so no rescan is needed.
  2. Use the coloured filter chips to narrow the table (default: not in target only). Click any chip to toggle that status group on or off.
  3. The Repo column shows each package’s origin (CRAN, Bioconductor, GitHub, or red unknown for local/private packages).
  4. Check the packages you want to ship, choose Install online or Ship as-is, and press Ship N selected.
  5. The log pane and hero panel track progress in real time.

The Browse and Manifest panels under the Tools tab expose manifest() output and let you inspect any detected R installation’s full package list.


Tips

  • hub() is a short alias for hub() — both launch the dashboard
  • courieR skips base and recommended packages automatically — only user-installed packages are compared and migrated
  • ship() uses pak under the hood, which resolves dependencies automatically
  • Use mode = "offline" when you have no internet access — courieR copies package directories directly. Packages without a local source path are reported as skipped
  • Use mode = "preserve" to keep exact version numbers; courieR copies where possible and pins the version in the pak spec otherwise
  • The source R installation does not need any extra packages; only the target R needs pak installed, because installation runs under the target R process
  • GitHub packages require the source repository to be public, or a GITHUB_PAT to be set in the environment
  • If a package fails, check result$results — the message column has the pak error
  • Use dry_run = TRUE before any large migration to review the plan without installing anything
  • On Linux without rig, find_routes() may only detect the R on $PATH; pass additional paths via search_paths if needed