usethis::create_package("path/to/mypackage")32 Building packages
You email analysis.R to a colleague. They run it and get an error because they don’t have the janitor package. They install it, try again, and hit a different wall: your script calls source("helpers.R"), a file they don’t have. You send it. They put it in the wrong directory. Two days later, they give up. A script has no way to say what it needs or to carry its helpers with it. A package does both, and once you see the structure, you can build one in ten minutes.
32.1 Why packages
Your colleague runs pak::pak("username/mypackage"), and everything installs: functions, documentation, dependencies. They type ?my_function and see how to use it. No back-and-forth. No guessing.
That is what a package is: the unit of shareable, testable, documented R code. It carries its dependencies, its documentation, and its tests as one installable unit that either works or tells you why it doesn’t. Even if you never publish to CRAN, writing one forces you to describe what each function does, lets you prove it works with automated tests, makes you declare what you depend on, and keeps your names from colliding with other packages’ names.
In Chapter 7, you saw that functions are values. A package is a named collection of functions with metadata, and that metadata is what separates “a folder of scripts” from “something someone else can install and use.”
If you have written the same function in three scripts, it belongs in a package. Packages are not just for CRAN.
The complete reference for everything in this chapter is R Packages (2nd edition) by Hadley Wickham and Jenny Bryan, freely available at r-pkgs.org. This chapter gives you enough to build your first package. That book gives you enough to build your twentieth.
32.2 The anatomy of a package
The minimum viable package has three components: the R/ directory holds function definitions, NAMESPACE controls which ones are visible, and DESCRIPTION records metadata. A standard layout looks like this:
mypackage/
├── DESCRIPTION # metadata: name, version, authors, dependencies
├── NAMESPACE # what you export, what you import (auto-generated)
├── R/ # your functions
├── man/ # documentation (auto-generated by roxygen2)
├── tests/ # test files
├── vignettes/ # long-form documentation
├── data/ # included datasets
├── .Rbuildignore # files to exclude from the built package
└── LICENSE # license file
DESCRIPTION declares the package name, title, version, authors, license, and dependencies. CRAN checks every field, install.packages() reads it, and other packages’ Imports reference it. A minimal example:
Package: mypackage
Title: What My Package Does (One Line, Title Case)
Version: 0.1.0
Authors@R: person("Jane", "Doe", email = "jane@example.com",
role = c("aut", "cre"))
Description: A longer description of what the package does. This can
span multiple lines. It should explain the purpose, not list
functions.
License: MIT + file LICENSE
Encoding: UTF-8
Roxygen: list(markdown = TRUE)
RoxygenNote: 7.3.1
NAMESPACE lists what users can see (exports) and what you borrow from other packages (imports). Never edit it by hand; roxygen2 generates it from comments in your code, as Section 32.4 shows.
Two hooks, .onLoad() and .onAttach(), let you run code when the package loads; for the details, see R Packages (2e), chapter 6.
The namespace is an environment (Section 18.1), and NAMESPACE defines which names are visible in it. When you call dplyr::filter(), R looks in dplyr’s namespace environment; when you call just filter(), R searches the environment chain, and which filter it finds depends on what is attached.
A package namespace is a closure at the module level. Functions inside can reference each other because they share the namespace environment, and the outside world sees only what is exported. The NAMESPACE file marks the same boundary that a lambda abstraction marks between bound and free variables.
32.3 Creating a package
One function call scaffolds everything:
This creates the directory with DESCRIPTION, NAMESPACE, and an empty R/; inside RStudio it also adds an .Rproj file and the .Rbuildignore and .gitignore entries that keep it out of the build. The package is loadable immediately.
The development loop has four verbs:
devtools::load_all() # simulate installing the package (Ctrl+Shift+L)
devtools::document() # regenerate man/ and NAMESPACE from roxygen (Ctrl+Shift+D)
devtools::test() # run all tests (Ctrl+Shift+T)
devtools::check() # run R CMD check (Ctrl+Shift+E)load_all() sources every file in R/, makes the exports available, and simulates a fresh install without installing anything, so the cycle becomes: edit a function, hit Ctrl+Shift+L, try it, repeat. An installed package cannot be edited in place; load_all() gives you an editable copy to iterate on, and installing freezes the result. Without it, you would rebuild and reinstall the whole package after every change.
The usethis::use_*() functions handle setup tasks:
usethis::use_r("bmi") # create R/bmi.R
usethis::use_test("bmi") # create tests/testthat/test-bmi.R
usethis::use_package("dplyr") # add dplyr to Imports
usethis::use_mit_license() # add MIT license
usethis::use_readme_rmd() # add README.Rmd
usethis::use_github_action() # add CI/CDEach one does one task and knows the boilerplate, so you do not have to.
Never create package files by hand. usethis knows the right boilerplate. You focus on the code.
Exercises
- Create a package called
mymathusingusethis::create_package(). Add a fileR/square.Rwith a functionsquare <- function(x) x^2. Load it withdevtools::load_all()and test thatsquare(5)returns 25. - Run
devtools::check()on your package. How many errors, warnings, and notes do you get? Read the output carefully.
32.4 Writing documentation with roxygen2
Documentation lives above your function as special comments (#'):
#' Calculate body mass index
#'
#' Computes BMI from weight and height using the standard formula.
#'
#' @param weight Weight in kilograms.
#' @param height Height in meters.
#' @return A numeric vector of BMI values.
#' @export
#' @examples
#' bmi(70, 1.75)
bmi <- function(weight, height) {
weight / height^2
}@param describes each argument: its type, its meaning, and any constraints. @return says what comes back. @export makes the function visible to users; without it, the function is internal, reachable through ::: but not ::. @examples holds runnable code, and R CMD check runs it. Two more tags earn their keep once a package grows: @seealso and @family cross-reference related functions, and @inheritParams borrows parameter documentation from another function instead of repeating it.
A pure function is the easiest kind to document: its contract is fully specified by inputs and outputs, so @param and @return capture everything a caller needs to know. A function with side effects needs extra prose about what state it modifies and when.
Markdown in roxygen is enabled by adding Roxygen: list(markdown = TRUE) to DESCRIPTION, which lets you use **bold**, *italic*, `code`, and [function()] for cross-links.
devtools::document() converts roxygen comments to .Rd files in man/ and updates NAMESPACE. You write roxygen; you never touch man/ directly.
Document the “why”, not just the “what”. @param x A numeric vector tells me the type. @param x Body mass in grams; must be positive tells me how to use it.
A common pattern in real packages is documenting multiple related functions on the same help page using @rdname:
#' Arithmetic operations
#'
#' @param x A numeric vector.
#' @return A numeric vector.
#' @name arithmetic
NULL
#' @rdname arithmetic
#' @export
square <- function(x) x^2
#' @rdname arithmetic
#' @export
cube <- function(x) x^3Both ?square and ?cube now open the same help page, which suits functions that are easier to understand side by side.
Exercises
- Add roxygen documentation to the
square()function from the previous exercise. Include@param,@return,@export, and@examples. Rundevtools::document()and view the help with?square. - Create a second function
cube()in the same package. Document both using@rdnameso they share a help page. Rundevtools::document()and verify with?square.
32.5 Dependencies
Your package will use functions from other packages, and how you declare that relationship decides whether it installs cleanly or fails with missing symbols. Start with a call that looks harmless:
filter(data, x > 0)Which filter? stats::filter() and dplyr::filter() both exist, and the answer depends on what the user happens to have attached. Inside a package, write the prefix:
dplyr::filter(data, x > 0)and declare the package in DESCRIPTION under Imports, which usethis::use_package("dplyr") does for you. That is the rule for anything your code needs to run: Imports plus ::. Dependencies stay explicit and names never collide. If typing dplyr::filter() everywhere feels verbose, the roxygen tag @importFrom dplyr filter writes importFrom(dplyr, filter) into NAMESPACE and lets you call filter() bare. Use it for one or two heavily used functions per dependency, not for everything you call.
Packages needed only by tests, vignettes, or examples go under Suggests, with usethis::use_package("ggplot2", type = "Suggests"). They are not installed automatically with your package, so guard the code that uses them:
test_that("plotting works", {
skip_if_not_installed("ggplot2")
p <- ggplot2::ggplot(data, ggplot2::aes(x, y)) + ggplot2::geom_point()
expect_s3_class(p, "ggplot")
})skip_if_not_installed() keeps the test from failing on a machine without ggplot2.
The third field, Depends, attaches the package when yours is loaded, so library(mypackage) also runs library(dependency) and fills the user’s search path with names they did not ask for. It is almost always wrong; use Imports.
Keep the list short. Every dependency is a place your package can break, and if you need one function from a package, consider writing it yourself. Two checks catch mistakes early: usethis::use_package() refuses to add a package that is not installed, and R CMD check warns when Imports lists a package your code never uses.
32.6 Testing with testthat
usethis::use_testthat(edition = 3) sets up the test infrastructure: tests/testthat/, tests/testthat.R, and the necessary DESCRIPTION fields.
usethis::use_test("bmi") creates tests/testthat/test-bmi.R. Tests are organized in test_that() blocks:
test_that("bmi computes correctly", {
expect_equal(bmi(70, 1.75), 70 / 1.75^2)
expect_length(bmi(c(70, 80), c(1.75, 1.80)), 2)
})
test_that("bmi rejects invalid input", {
expect_error(bmi("a", 1.75))
})expect_equal() compares with a tolerance, which is what you want for floating-point results; expect_identical() compares exactly and suits integers, strings, and logicals. expect_true() and expect_false() check a condition, expect_error(), expect_warning(), and expect_message() check that a condition is signalled, and expect_length() and expect_named() check shape. For output that is hard to specify by hand, expect_snapshot() records it once and compares later runs against the stored copy.
devtools::test() runs all tests. devtools::test_active_file() runs just the file you are editing.
Pure functions are also the easiest to test: same inputs always produce the same output, so each expect_equal() call is a complete specification of behavior with no setup or teardown required.
How many tests should you write? Enough to cover the normal case, the edge cases, and the error cases. For a function like bmi(), that means correct output for typical input, correct output for vectorized input, correct behavior for zero or negative values, and an error for non-numeric input. Four or five tests per function is a reasonable starting point, and writing them forces you to think about your function’s contract more carefully than any amount of staring at the implementation would.
Test the contract, not the implementation. Your test should pass even if you rewrite the function body, as long as the inputs and outputs stay the same.
Exercises
- Write tests for the
square()function: test thatsquare(3)is 9,square(-2)is 4,square(0)is 0, andsquare(c(1, 2, 3))returnsc(1, 4, 9). - Add a test that checks
square()returns a numeric vector. (Hint:expect_type().)
32.7 Vignettes
Function documentation is reference: you look up a specific function when you already know what you are looking for. A vignette shows a newcomer how the pieces fit together, walking through a workflow from start to finish.
usethis::use_vignette("getting-started")This creates a template in vignettes/. R Markdown vignettes (.Rmd) are the standard: code chunks run during package build, and the output is HTML. Quarto vignettes (.qmd) are the newer option, with more features but requiring Quarto as a system dependency.
A good package has at least one vignette: “Getting Started” or “Introduction to mypackage.” Write it as if the reader has never seen your package before and has five minutes to decide whether to keep reading.
A vignette template looks like this:
---
title: "Getting Started with mypackage"
output: rmarkdown::html_vignette
vignette: >
%\VignetteIndexEntry{Getting Started with mypackage}
%\VignetteEngine{knitr::rmarkdown}
%\VignetteEncoding{UTF-8}
---
## Installation
Install from GitHub:
```r
pak::pak("username/mypackage")
```
## Basic usage
Load the package and run a simple example:
```r
library(mypackage)
result <- bmi(70, 1.75)
result
```The YAML header is boilerplate; the content is yours.
32.8 R CMD check
R CMD check runs more than 50 checks against your package: does the NAMESPACE match the exports? Do all examples run without error? Are all arguments documented? Do the tests pass? Is the package installable?
devtools::check()Each finding comes back at one of three levels. An ERROR means something is broken and must be fixed. A WARNING means something is wrong; CRAN will not accept it, and you should fix it anyway. A NOTE means something is unusual: fix it if you can, explain it if you cannot. The goal is 0 errors, 0 warnings, 0 notes. Each check exists because someone, somewhere, shipped a broken package, and the strictness reflects the cost of breakage on CRAN, where close to 25,000 packages depend on each other.
A few findings account for most first runs. “Undocumented arguments” means a @param tag is missing. “Undefined global functions or variables” usually means you called a function from another package without ::; write dplyr::filter() or add @importFrom dplyr filter. The same note appears as “no visible binding for global variable” in tidyverse code that uses bare column names, and the fix is .data$column from rlang or a utils::globalVariables() declaration. “Non-standard file/directory found” means a file belongs in .Rbuildignore, which usethis::use_build_ignore("filename") handles.
Run check() often during development, not just at the end. Each run should then find at most one or two issues; wait until the end and you face dozens at once. A clean check means the package is well-formed on your machine, and the last section shows how to get the same answer for Linux and macOS.
Exercises
- Introduce a deliberate error in your package: remove the
@paramtag for one argument. Rundevtools::check()and find the WARNING. Fix it and re-check. - Add
dplyr::filterto a function without adding dplyr toImports. Rundevtools::check(). What feedback do you get?
32.9 Sharing your package
Push your package to GitHub, and users install it with one line, pak::pak("user/mypackage") or devtools::install_github("user/mypackage"). That is the lowest barrier to sharing, and three more layers sit on top of it. pkgdown builds a website from your documentation, README, and vignettes; usethis::use_pkgdown_github_pages() sets it up with automatic deployment through GitHub Actions. CRAN is the official repository, reached through devtools::submit_cran(); its review requires 0 errors and 0 warnings, ideally 0 notes, and human reviewers check the CRAN policies on top. R-universe (r-universe.dev) sits between the two: point it at your GitHub repository and it builds Windows and macOS binaries with no review step.
Version numbers follow semantic versioning (semver.org): MAJOR.MINOR.PATCH, where a PATCH release fixes bugs, a MINOR release adds features without breaking existing code, and a MAJOR release may break it. The tidyverse follows a looser version of the same idea and saves breaking changes for major releases where it can, so when dplyr goes from 1.0 to 1.1 your code will usually keep working, and when it goes to 2.0 you read the changelog.
For GitHub-hosted packages, continuous integration is nearly free:
usethis::use_github_action("check-standard")This adds a GitHub Actions workflow that runs R CMD check on Linux, macOS, and Windows every time you push. If the check fails, GitHub shows a red X next to the commit; if it passes, you get a green checkmark and a badge for your README. The real value is catching platform-specific bugs you would never find on your own machine: path separators (Windows uses \, everything else uses /), case-sensitive file systems (Linux is case-sensitive, macOS and Windows are not), and system library availability.
Exercises
- Push a package to GitHub and install it on another machine (or in a fresh R session) using
pak::pak(). Does it install cleanly?
Start with GitHub. Add pkgdown when your package has users. Submit to CRAN when it is stable and general-purpose. CRAN gives your package visibility, credibility, and a guarantee that it installs cleanly across platforms. If your package solves a real problem, aim for CRAN.