Publishing to CRAN
|
This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production. The reference material consulted while preparing these pages is listed in the section bibliography. |
Checked against R 4.6.1 (2026-06-24) on 2026-10-20; R 4.6.2 is scheduled for 2026-10-29. CRAN is the main repository of R packages: the place install.packages("name") looks first, a set of volunteer-run check machines that rebuild every package on several operating systems all the time, and a policy that a package has to satisfy before it is admitted and while it stays. This page follows a package on that road: what the CRAN Repository Policy asks, how to choose a name, how R CMD check --as-cran reproduces the incoming checks on your own machine, which other machines to try (win-builder, mac-builder, R-hub v2 and GitHub Actions), how to test the packages that depend on yours, what happens when you submit and resubmit, how version numbers, deprecation and archival work afterwards, and which repositories to use when CRAN is not the right place. The tools run for real are base R with its tools and utils packages, devtools 2.5.2, usethis 3.2.2, rcmdcheck 1.4.0 and desc 1.4.3, with the default linters of lintr 3.4.0 used only to check the scripts. The shelf package is built with the Suggests testthat 3.3.2, knitr 1.52 and rmarkdown 2.32. Named in a sentence and not run, because they are not installed or need an account or a remote service: rhub 2.0.1, revdepcheck, available 1.1.0, lifecycle, BiocCheck and BiocManager; pak 0.11.1 is installed and only named. Nothing is submitted to CRAN, win-builder, mac-builder, R-hub or Bioconductor on this page: the submission flow is described from the official documents, and every fragment that is not run says so right before it.
Every example is a script that was run with Rscript on R 4.6.1, and the lines that start with #> are what it printed; the scripts also pass the default lintr linters. The names books, loans and shelf_today and the book() constructor belong to the running library-shelf scenario described on the section index. The shelf package shown here was created, documented, tested, built and checked for real with the tools of the previous pages, and its files are shown as the tools wrote them: Creating Packages builds it, Documentation with Rd, roxygen2 and Vignettes documents it and Testing with testthat tests it. The checks below ran on a copy of that package. Paths are shortened to ~/shelf (the directory of the package) and ~ (the directory that holds the tarball), and the header of an R CMD check log, which names the platform, the compilers, the locale and the time, is left out of every excerpt; so are timings, because they differ on every machine. Two facts were read from the live CRAN site on 2026-10-21 and would change if CRAN changed: that a package called SHELF exists, and the list of folders under the incoming area.
The road to CRAN in one table
A release on CRAN is a short chain of steps, each answered by a different tool. The sections below take them in this order.
| Step | Tool | Question it answers |
|---|---|---|
Name |
|
Is the name free, irrespective of case, in CRAN, its archive and Bioconductor? |
Metadata |
|
Does the package describe itself the way CRAN expects? |
Local check |
|
Does the tarball pass the incoming checks without ERRORs, WARNINGs and avoidable NOTEs? |
Other systems |
win-builder, mac-builder, R-hub v2, GitHub Actions |
Does it also pass on Windows, macOS, other Linux flavours and R-devel? |
Reverse dependencies |
|
Does the update break a package that uses yours? |
Submission |
the CRAN web form, |
Is the tarball received, and has the maintainer confirmed it? |
Review |
the CRAN team, with the automatic checks |
Is it accepted, or is feedback to be answered with a new version? |
Afterwards |
the CRAN checks pages, |
Does it keep passing as R and its dependencies change? |
The policy opens with the idea that explains most of what follows: CRAN is a home for polished, non-trivial packages and not a place to develop them. The people who read submissions are volunteers whose time the policy calls CRAN’s most precious resource, which is why the rules are strict about checks that run clean, about not submitting too often and about answering feedback in the next submission and not by email. A development version, an experiment, a package that exists to track a moving service and a package that a single project uses are all better served by GitHub or by r-universe (see Alternatives: Bioconductor, r-universe and GitHub), and a CRAN release is the stable point you choose to publish.
What the CRAN Repository Policy requires
The policy is a short document (the revision used here is 6875, read on 2026-10-21) split into source packages, binary packages, submission and re-submission. It binds everyone who submits: sending in a package, an update included, counts as agreeing to it. The parts that decide whether a first submission survives are below; the policy page remains the authority, and CRAN may change it.
Authors, copyright holders, the maintainer and the licence
The ownership of copyright and intellectual property rights of every component of the package has to be clear, including the components downloaded at installation or during use. In the DESCRIPTION file that means an Authors@R field, which Writing R Extensions says is required for CRAN submissions, built with person() and the role codes: aut for an author, cre for the maintainer, ctb for a contributor and cph for a copyright holder. Code copied or derived from other work keeps its copyright and licence statements and its authors, preferably as ctb entries. Where the copyright belongs to someone other than the authors, the policy asks for cph entries in Authors@R, or for a Copyright field that may refer to a file inst/COPYRIGHTS. An ORCID or ROR identifier goes into the comment argument of person(), which CRAN strongly encourages.
There must be one designated maintainer, a person and not a mailing list, with a name and a valid email address that stays current and is usable without filtering, confirmation steps or forwarding, because the CRAN team and the automatic systems write to that address. The maintainer warrants that they act for all credited authors. The licence has to give CRAN the right to distribute the package in perpetuity, licences that are not in the licence database shipped with R (license.db) are generally refused, and a change of licence has to be highlighted in the next submission. The shelf package follows all of this with the placeholder maintainer maintainer@example.com, one person() carrying the roles aut, cre and cph, and License: MIT + file LICENSE.
Package: shelf
Title: Tools for a Small Library Catalogue
Version: 0.1.0
Authors@R:
person("Shelf", "Maintainers", , "maintainer@example.com", role = c("aut", "cre", "cph"))
Description: Cleans a small library catalogue and its loans table,
validates ISBN-13 strings, and reports overdue loans and overdue rates
by genre. Ships the example 'books' and 'loans' datasets used by the
documentation.
License: MIT + file LICENSE
Config/roxygen2/version: 8.1.1
Encoding: UTF-8
Roxygen: list(markdown = TRUE)
Suggests:
knitr,
rmarkdown,
testthat (>= 3.0.0),
withr
Config/testthat/edition: 3
Depends:
R (>= 4.1.0)
VignetteBuilder: knitr
LazyData: true
The LICENSE file written by usethis::use_mit_license() holds two lines, YEAR: 2026 and COPYRIGHT HOLDER: shelf authors, which is the form that the + file LICENSE part of the field refers to; the longer LICENSE.md the same function writes is left out of the tarball through .Rbuildignore. The text fields have conventions of their own, which the incoming check enforces and which the section on the incoming checks below shows failing: the Title is in title case, at most about 65 characters, without a final period and without repeating the package name; the Description is one paragraph of complete sentences that does not start with the package name or "This package"; names of other packages and external software are in single quotes, function names are written with parentheses and without quotes, citations are in author-year style with a DOI in angle brackets, and URLs are in angle brackets too. The next script reads those facts back from the shelf tree with desc 1.4.3; tools::toTitleCase() does the capitalisation for you when you are unsure how to write a title.
# Run from the package root, e.g. ~/shelf. desc 1.4.3 reads DESCRIPTION files.
d <- desc::desc("DESCRIPTION")
pkg <- unname(d$get("Package"))
# Authors@R: one maintainer (cre) with an email address, and a copyright
# holder (cph).
d$get_authors()
#> [1] "Shelf Maintainers <maintainer@example.com> [aut, cre, cph]"
d$get_maintainer()
#> [1] "Shelf Maintainers <maintainer@example.com>"
# Title: title case, at most 65 characters, no period at the end, and it does
# not repeat the package name.
title <- unname(d$get("Title"))
title
#> [1] "Tools for a Small Library Catalogue"
identical(tools::toTitleCase(title), title)
#> [1] TRUE
nchar(title) <= 65L
#> [1] TRUE
!endsWith(title, ".")
#> [1] TRUE
!grepl(pkg, title, ignore.case = TRUE)
#> [1] TRUE
# Description: does not start with the package name or "This package".
description <- unname(d$get("Description"))
!any(startsWith(description, c(pkg, "This package")))
#> [1] TRUE
# The licence field, which R CMD check compares with R's licence database.
d$get("License")
#> License
#> "MIT + file LICENSE"
What a package may and may not do on the user’s machine
The policy lists behaviour that a package must avoid because CRAN checks run unattended on shared machines and a package is code that runs on other people’s computers. It must not write anywhere in the user’s file space (the home directory and the clipboard included) apart from the temporary directory of the R session, and what it writes there has to be cleaned up. Since R 4.0.0 a package that must keep data, configuration or a cache between sessions may use the per-package user directories returned by tools::R_user_dir(), provided it needs a version dependency on R (>= 4.0) or uses the function conditionally, keeps the files small and removes outdated material. A package must not modify the global environment (the workspace), must not call q(), must not tamper with the code of the standard and recommended packages, must not use ::: or .Internal() and .Call() entry points that base R does not declare as API, must not start external programs such as viewers or browsers during examples and tests without closing them, must not send information about the session to third parties without the user’s confirmation, and must not turn off security provisions such as the verification of TLS certificates. Downloads of anything at installation or startup use https and, for more than a few megabytes, a large enough timeout.
# A package writes only below tempdir() (or a path the user chose); anything
# it creates there is removed again.
report <- file.path(tempdir(), "loans-report.csv")
write.csv(data.frame(genre = "Fiction", loans = 3L), report, row.names = FALSE)
file.exists(report)
#> [1] TRUE
basename(report)
#> [1] "loans-report.csv"
readLines(report)
#> [1] "\"genre\",\"loans\"" "\"Fiction\",3"
unlink(report)
file.exists(report)
#> [1] FALSE
# For data that has to persist, R 4.0.0 and later provide per-package user
# directories; the path depends on the user, so only its last part is shown.
basename(tools::R_user_dir("shelf", which = "cache"))
#> [1] "shelf"
Source: R documentation: R_user_dir
Two limits are checked mechanically. A package must never use more than two cores at the same time, and R CMD check --as-cran sets the environment variable _R_CHECK_LIMIT_CORES_ so that code in the parallel package that starts more than two workers fails (R Internals, "Tools"). The usual pattern is to default to one core and to cap at two whenever that variable is set.
# R CMD check --as-cran sets _R_CHECK_LIMIT_CORES_ (to TRUE) so that parallel
# code that starts more than two workers fails the check. A package should
# never use more than two cores by default.
default_cores <- function() {
limit <- Sys.getenv("_R_CHECK_LIMIT_CORES_", "")
if (nzchar(limit) && !identical(toupper(limit), "FALSE")) {
2L
} else {
getOption("shelf.cores", 1L)
}
}
default_cores()
#> [1] 1
Sys.setenv("_R_CHECK_LIMIT_CORES_" = "TRUE")
default_cores()
#> [1] 2
Sys.unsetenv("_R_CHECK_LIMIT_CORES_")
Source: R Internals: Tools
The second limit is the Internet. The policy says that a package that uses an Internet resource has to cope with the resource being unavailable or changed, with an informative message and neither a check warning nor a check error, that the use of external resources should be kept to a minimum, and that rate-limit errors such as the HTTP codes 429 and 403 must be avoided because other packages may use the same site. Writing R Extensions adds that installation itself should avoid the Internet where possible. The usual shape is a function that catches the failure, says what happened and returns an empty result, and tests and examples that skip when the resource is missing. The address below is a local port on which nothing listens, so no outside network is involved; the message that readLines() produces is the text R itself prints.
# A function that needs a web resource must fail gracefully: an informative
# message and NULL, not a check warning or error. The address below is a
# local port nobody listens on, so no outside network is involved.
fetch_catalogue <- function(url) {
tryCatch(
suppressWarnings(readLines(url)),
error = \(e) {
message("catalogue not available: ", conditionMessage(e))
NULL
}
)
}
res <- fetch_catalogue("http://localhost:1/books.csv")
#> catalogue not available: cannot open the connection to 'http://localhost:1/books.csv'
is.null(res)
#> [1] TRUE
# The same idea in a test or an example: skip when the resource is missing.
if (is.null(res)) message("skipping the example that needs the catalogue")
#> skipping the example that needs the catalogue
Writing R Extensions, in its chapter on portable packages, adds a list of things not to do in tests and examples because they pass on one machine and fail on another: do not test how long a command takes, do not test the exact format of messages from R or from other packages (they change and they can be translated, and the message text of the system’s C library or of libcurl changes between versions), do not test for the absence of warnings, compare numbers with a tolerance and only when you have done an error analysis, canonicalise file paths before comparing them, set a seed for anything random, use options(warn = 1) so that warnings are reported when they happen, and test your package in more than one time zone when it handles dates. A package must also run on at least two of the major R platforms; a Windows-only package is considered, though CRAN may not be the right place.
Time, size and examples
Checking a package should take as little CPU time as possible, because the check farm runs thousands of packages. The policy asks for examples that run for a few seconds at most, for long-running tests and vignette code to be made optional while the tests that are left still exercise every feature, and for tarballs that are small: neither data nor documentation should normally exceed 5 MB, the source tarball should not exceed 10 MB, and large data belongs in a separate data package that is rarely updated. R CMD check --as-cran sets --timings, which lists examples taking more than 5 seconds of CPU or elapsed time, and _R_CHECK_TIMINGS_ to 10 so that timings of 10 seconds or more are reported for installation, examples, tests and vignettes (Writing R Extensions, "Check timing"; R Internals, "Tools").
The Rd macro \donttest is how an example that is correct but slow, or that depends on something the check machine may lack, is kept out of the quick path of a plain check. Since R 4.0.0, R CMD check --as-cran runs the \donttest examples too, so the macro no longer hides code from CRAN: it only keeps it out of example() in a non-interactive session and out of a plain R CMD check. Writing R Extensions says that code inside \donttest has to be correct R code whose packages are declared in DESCRIPTION, and that the code should carry a comment saying why it needs the macro. \dontrun is for code that cannot run at all. The first script shows how the three kinds of example are treated by the Rd tools; the text block under it comes from a throw-away package whose single help page holds a plain example, a \donttest example and a \dontrun example, checked once with a plain R CMD check and once with --as-cran.
# How the three example wrappers of an Rd file reach R CMD check:
# plain code always runs, \donttest{} runs under --as-cran, \dontrun{} never.
rd <- c(
"\\name{demo}", "\\alias{demo}", "\\title{Demo}", "\\description{A demo.}",
"\\examples{",
"x <- 1 + 1",
"\\donttest{",
"y <- x * 2",
"}",
"\\dontrun{",
"stop(\"needs a server\")",
"}",
"}"
)
rd_file <- tempfile(fileext = ".Rd")
writeLines(rd, rd_file)
# tools::Rd2ex() extracts the examples. With commentDonttest = TRUE the code
# of \donttest{} is kept but commented out (##D), which is how example() treats
# it in a non-interactive session; \dontrun{} is always commented out.
out <- tempfile(fileext = ".R")
tools::Rd2ex(rd_file, out, commentDonttest = TRUE)
ex <- readLines(out)
ex[nzchar(ex) & !startsWith(ex, "###")]
#> [1] "x <- 1 + 1" "## No test: "
#> [3] "##D y <- x * 2" "## End(No test)"
#> [5] "## Not run: " "##D stop(\"needs a server\")"
#> [7] "## End(Not run)"
Source: R documentation: Rd2ex
R CMD check --no-manual dtdemo_0.1.0.tar.gz
ran: plain example
R CMD check --as-cran --no-manual dtdemo_0.1.0.tar.gz
ran: plain example
ran: donttest example
Source: R NEWS: Changes in R 4.0.0
The roxygen2 tag @examplesIf writes a conditional example into the Rd file (so that the example runs only when its condition holds, for instance interactive() or the availability of a service), and the test-side counterpart is skip_on_cran() from testthat, which skips a test unless the environment variable NOT_CRAN is set to true; both are explained in Documentation with Rd, roxygen2 and Vignettes and Testing with testthat. The shelf package has one skip_on_cran() test and one snapshot test that is skipped on CRAN, which is why its test summary shows two skips under --as-cran.
Dependencies and other repositories
The packages a CRAN package needs in Depends, Imports or LinkingTo (its strong dependencies) have to be available from CRAN or from the Bioconductor software repository. A package named in Suggests or Enhances that is on neither has to be located through the Additional_repositories field, a comma-separated list of repository URLs that R CMD check uses to confirm the package can be found, as a source package at least. The field does not install anything; it lets CRAN check that the optional package exists. A suggested package should be used conditionally in examples and tests when it cannot be installed easily on every major platform, which Writing R Extensions recommends doing always: requireNamespace("pkg", quietly = TRUE) before using it. Orphaned CRAN packages must not be strong dependencies. CRAN versions of packages are expected to work with the current CRAN and Bioconductor releases of what they depend on and must not recommend development versions of those or of themselves.
Suggests:
testthat (>= 3.0.0),
optionalpkg
Additional_repositories: https://example-user.r-universe.dev
That fragment is a placeholder, not a field of the shelf package, which has no strong dependency at all: outside base R it only calls utils::packageVersion(), and utils is attached by default. A package that calls utils:: or stats:: functions should still list them in Imports, which is what usethis::use_package("utils") writes; the check "checking dependencies in R code" in the list further down is the one that reports a package used but not declared. More about declaring dependencies is in Packages, Libraries and Namespaces and Creating Packages.
Updates, frequency and archival
The policy asks for updates to be submitted responsibly, with respect for the volunteers' time: once a package is established, the interval it considers appropriate is "no more than every 1-2 months". Before submitting a correction, look at the CRAN check page of the package at https://CRAN.R-project.org/web/checks/check_results_NAME.html (replace NAME with the package name) and wait until that page has been fully updated after the previous publication, which can take at least 48 hours. A package that shows an issue on the macOS arm64 flavour should be checked with mac-builder before resubmission. Packages are not normally removed from CRAN, but they are archived when the maintainer asks, when R CMD check gives an ERROR at the release of a new R x.y.0 and the maintainer has not set a firm deadline for an update and kept it, or when warnings and significant notes are left unfixed after the maintainer was asked to update. An archived package keeps its place in the Archive area, and its name stays reserved: the name check of the next section reports a clash with "CRAN archive" as well as with the current index. If an update changes the API of a package that others depend on, the policy expects you to contact their maintainers, suggest changes, give them at least two weeks, ideally more, and mention in the submission which packages are affected and that their maintainers were told.
Keep a NEWS.md (or NEWS) file that says what changed in each version: since R 4.3.0 R CMD check also reports problems it meets when reading that file, CRAN links the news from the package page, and news() reads it back from the installed package, as shown in the section on versions below. Keep README.md for the repository and cran-comments.md for the people who review the submission; the latter is excluded from the tarball with .Rbuildignore. None of the files that a GitHub project accumulates (workflow files, .github, issue templates, pkgdown sources) belongs in the tarball either: they are listed in .Rbuildignore by the usethis functions that create them. The tarball that CRAN receives should contain the package and nothing else, which the listing in the next section shows for shelf.
Choosing a package name
Settle the name before anything else, because a rename touches every file. The Package field of Writing R Extensions says that a name contains only ASCII letters, numbers and dots, has at least two characters, starts with a letter and does not end in a dot; if it needs explaining, the explanation goes into the Description field, not the Title. The CRAN policy adds the rule that matters most: a name must not conflict, irrespective of case, with any current or past CRAN package (the Archive area can be consulted), nor with any current Bioconductor package. Names are persistent, and changing the name of a published package is in general not permitted. By submitting, the maintainer gives CRAN the right to use the name, so the team can orphan a package and let another maintainer take it over. The first script applies the syntax rule as a regular expression and the case-insensitive rule to a small hand-typed list; the next one reads the real index.
# Writing R Extensions: a package name has only ASCII letters, numbers and
# dots, at least two characters, starts with a letter and does not end in a dot.
valid_name <- \(x) grepl("^[A-Za-z][A-Za-z0-9.]*[A-Za-z0-9]$", x)
candidates <- c(
"shelf", "shelfdesk", "shelf.loans", "shelf-tools", "shelf_tools",
"2shelf", "s", "shelf."
)
data.frame(name = candidates, valid = valid_name(candidates))
#> name valid
#> 1 shelf TRUE
#> 2 shelfdesk TRUE
#> 3 shelf.loans TRUE
#> 4 shelf-tools FALSE
#> 5 shelf_tools FALSE
#> 6 2shelf FALSE
#> 7 s FALSE
#> 8 shelf. FALSE
# CRAN names are unique irrespective of case. A hand-typed excerpt of the
# names that exist (the real index is read in the next example).
existing <- c("SHELF", "shiny", "shinyjs", "dplyr", "data.table", "lattice")
clash <- \(x) existing[tolower(existing) == tolower(x)]
clash("shelf")
#> [1] "SHELF"
clash("shelfdesk")
#> character(0)
clash("Shiny")
#> [1] "shiny"
The real index is the file PACKAGES that every repository publishes, and available.packages() downloads it (this script needs the network and prints only the answers, never the repository address; repos is given explicitly so that the script does not depend on the mirror the session has configured). The package of this page is called shelf, and the lower-case name does not exist, but that is not the question: R itself treats shelf and SHELF as different package names on a case-sensitive file system, and CRAN does not, since two names that differ only in case could not be installed side by side on the case-insensitive file systems that Windows and macOS usually have.
# Needs the network: available.packages() downloads CRAN's PACKAGES index.
repos <- c(CRAN = "https://cloud.r-project.org")
ap <- available.packages(repos = repos)
# Exact and case-insensitive matches for the name we would like to use.
"shelf" %in% rownames(ap)
#> [1] FALSE
"shelf" %in% tolower(rownames(ap))
#> [1] TRUE
rownames(ap)[tolower(rownames(ap)) == "shelf"]
#> [1] "SHELF"
# The name of a throw-away copy of the package is free.
tolower("shelfdesk") %in% tolower(rownames(ap))
#> [1] FALSE
The same answer comes out of R CMD check --as-cran as soon as it is allowed to look, which makes the check a good last guard. This is the real output for the tarball of the shelf package (the header, which names the platform, is trimmed, and so are the other checks, which were all OK): the status of the whole check is 1 ERROR, and the error is the name.
* using options ‘--no-manual --as-cran’
* checking for file ‘shelf/DESCRIPTION’ ... OK
* this is package ‘shelf’ version ‘0.1.0’
* package encoding: UTF-8
* checking CRAN incoming feasibility ... ERROR
Maintainer: ‘Shelf Maintainers <maintainer@example.com>’
New submission
Conflicting package names (submitted: shelf, existing: SHELF [https://CRAN.R-project.org/src/contrib])
Conflicting package names (submitted: shelf, existing: SHELF [CRAN archive])
* checking package namespace information ... OK
[... the other checks, all OK, are left out here ...]
* DONE
Status: 1 ERROR
The first message is the same on every first submission and is discussed below (Maintainer: and New submission). The two lines that follow are the finding: the submitted name shelf conflicts with an existing package whose name differs only in case, once with the current CRAN source area and once with the archive. A submission of this tree would be refused at this step, so the name has to change before the package is published; the lesson of this page is therefore to run the check before investing in the name. The package available (version 1.1.0 on CRAN; not installed here, not run) automates a wider version of the same question: its available() function checks that a candidate name is valid and that it is not already used on GitHub, CRAN and Bioconductor, and its suggest() function proposes names from the text of a title. The package continues below under the name shelf; the checks that follow run with the remote part switched off so that they can show everything else, and a throw-away copy renamed shelfdesk, a name that is not on CRAN, shows what a clean new submission looks like.
R CMD check --as-cran
R CMD check is the package checker described in the chapter "Checking and building packages" of Writing R Extensions. Run on a tarball made by R CMD build, it installs the package, runs the examples, the tests and the vignettes, and checks the files, the metadata, the code and the documentation. The manual strongly recommends that the final checks are made on such a tarball, and the policy requires that the tarball you upload is the one that R CMD check --as-cran passed, preferably with the current development version of R and, when that is not possible and explained, with R-patched or the current release, which is what the checks on this page used (R 4.6.1).
Build the tarball first
R CMD build shelf
The command is run from the directory that contains the package directory (here ~, so the package is ~/shelf). It installs the package into a temporary library because the vignette has to be built, builds the vignette, drops the files that .Rbuildignore lists and writes shelf_0.1.0.tar.gz. The log below is the real one.
* checking for file ‘shelf/DESCRIPTION’ ... OK
* preparing ‘shelf’:
* checking DESCRIPTION meta-information ... OK
* installing the package (it is needed to build vignettes)
* creating vignettes ... OK
* checking for LF line-endings in source and make files and shell scripts
* checking for empty or unneeded directories
* building ‘shelf_0.1.0.tar.gz’
What the tarball holds is what CRAN receives. The listing is real: the four patterns of .Rbuildignore keep cran-comments.md, LICENSE.md, .lintr and the data-raw directory out, the built vignette appears under inst/doc with a build/vignette.rds index, and NAMESPACE and the man pages are the files that roxygen2 wrote before the build.
shelf/build/
shelf/build/vignette.rds
shelf/data/
shelf/data/books.rda
shelf/data/loans.rda
shelf/DESCRIPTION
shelf/inst/
shelf/inst/CITATION
shelf/inst/doc/
shelf/inst/doc/shelf.html
shelf/inst/doc/shelf.R
shelf/inst/doc/shelf.Rmd
shelf/LICENSE
shelf/man/
shelf/NAMESPACE
shelf/NEWS.md
shelf/R/
shelf/R/book.R
shelf/R/clean.R
shelf/R/data.R
shelf/R/isbn.R
shelf/R/loans.R
shelf/R/sysdata.rda
shelf/R/zzz.R
shelf/tests/
shelf/tests/testthat/
shelf/tests/testthat/fixtures/
shelf/tests/testthat/fixtures/books-small.csv
shelf/tests/testthat/helper-shelf.R
shelf/tests/testthat.R
shelf/tests/testthat/_snaps/
shelf/tests/testthat/_snaps/book.md
shelf/tests/testthat/test-book.R
shelf/tests/testthat/test-clean.R
shelf/tests/testthat/test-loans.R
shelf/vignettes/
shelf/vignettes/shelf.Rmd
[... 9 more files in shelf/man/, one Rd file per help topic ...]
What --as-cran changes
The help of R CMD check describes --as-cran as a selection of customizations similar to those CRAN uses. It is a bundle of settings, not a separate program: it sets a number of _R_CHECK_*_ environment variables (the full list with their meaning is in the "Tools" chapter of R Internals), turns on some options such as --timings, and adds checks to the default list. The next lines are the options that matter for a CRAN submission, taken from the help of the same R.
--no-manual do not produce the PDF and HTML manuals
--no-build-vignettes do not build vignette outputs
--ignore-vignettes skip all tests on vignettes
--run-donttest do run \donttest sections in the Rd files
--use-valgrind use 'valgrind' when running examples/tests/vignettes
--timings record timings for examples
--as-cran select customizations similar to those used
--force-multiarch run tests on all sub-archs even for packages
The environment variables that matter most are these. _R_CHECK_CRAN_INCOMING_ turns on the check of whether the package is suitable for publication on CRAN, and _R_CHECK_CRAN_INCOMING_REMOTE_ (default: the value of the first) includes the part of it that needs remote access, the name lookup above and the URL checks. _R_CHECK_FORCE_SUGGESTS_ makes missing suggested packages an error; it defaults to true, but not for the submission checks of CRAN itself, and devtools::check() sets it to false through its argument force_suggests. _R_CHECK_LIMIT_CORES_ caps the cores that code can use, _R_CHECK_TIMINGS_ and --timings report slow steps, _R_CHECK_SYSTEM_CLOCK_ compares the clock of the machine with an external clock as part of "checking for future file timestamps", and _R_CHECK_DONTTEST_EXAMPLES_ can be set to a false value to skip the \donttest examples temporarily while developing. Four changes to the list came with specific releases of R: --as-cran runs \donttest examples from R 4.0.0, validates the HTML version of the manual with HTML Tidy from R 4.2.0, traps leftover browser() calls with _R_CHECK_BROWSER_NONINTERACTIVE_ and checks the KaTeX rendering of mathematics in Rd files from R 4.3.0, and notes bad parts in the URL fields of DESCRIPTION from R 4.5.0.
How much the bundle changes can be measured. The first command below lists the names of the steps of a plain check and of an --as-cran check of the shelf tarball (with the remote part off, so that both finish the same way) and compares them: the plain check has 53 steps and the --as-cran check has 59, and the six that are added are listed.
R CMD check --no-manual shelf_0.1.0.tar.gz > plain.log
_R_CHECK_CRAN_INCOMING_REMOTE_=false R CMD check --as-cran --no-manual shelf_0.1.0.tar.gz > ascran.log
grep '^\* checking' plain.log | sed 's/ \.\.\. .*//' | sort > plain.steps
grep '^\* checking' ascran.log | sed 's/ \.\.\. .*//' | sort > ascran.steps
wc -l plain.steps ascran.steps
diff plain.steps ascran.steps
53 plain.steps
59 ascran.steps
112 total
3a4
> * checking CRAN incoming feasibility
10a12
> * checking for detritus in the temp directory
13a16
> * checking for future file timestamps
16a20
> * checking for non-standard things in the check directory
37a42
> * checking Rd line widths
46a52
> * checking use of S3 registration
Lines that start with > are in the --as-cran list only: the CRAN incoming feasibility check, the check for detritus in the temporary directory, the check for future file timestamps, the check for non-standard things in the check directory, the check of the widths of the Rd lines and the check of the registration of S3 methods. The environment is changed as well, and that cannot be seen in the step names. A test file in a throw-away package, tests/probe.R, prints the variables that start with _R_CHECK_ while R CMD check runs it; the first listing is a plain check and the second an --as-cran check, which sets forty-two variables where the plain check sets three, among them _R_CHECK_LIMIT_CORES_, _R_CHECK_TIMINGS_, _R_CHECK_NO_STOP_ON_TEST_ERROR_, _R_CHECK_BROWSER_NONINTERACTIVE_ and _R_CHECK_CODE_USAGE_VIA_NAMESPACES_.
# tests/probe.R of a throw-away package: R CMD check runs it and keeps the
# printed lines in envprobe.Rcheck/tests/probe.Rout.
vars <- sort(grep("^_R_CHECK_", names(Sys.getenv()), value = TRUE))
vals <- vapply(vars, Sys.getenv, "")
cat(length(vars), "variables\n")
other <- vars[vals != "TRUE"]
cat(sprintf("%s = %s\n", other, vals[other]), sep = "")
cat(strwrap(paste(vars[vals == "TRUE"], collapse = " "), 78), sep = "\n")
Source: R Internals: Tools
This script is a test file, so it is a fragment: run by Rscript it prints only 0 variables and an empty line, because R CMD check is what sets the variables. Under R CMD check it prints the count, then the variables whose value is not TRUE one per line, then the names of the variables whose value is TRUE, wrapped. The output of a plain R CMD check --no-manual is this:
3 variables
_R_CHECK_INTERNALS2_ = 1
_R_CHECK_PACKAGE_NAME_ = envprobe
_R_CHECK_LICENSE_
Source: R Internals: Tools
and the output of R CMD check --as-cran --no-manual is this:
42 variables
_R_CHECK_EXCESSIVE_IMPORTS_ = 20
_R_CHECK_INTERNALS2_ = 1
_R_CHECK_PACKAGES_USED_IGNORE_UNUSED_IMPORTS_ = FALSE
_R_CHECK_PACKAGE_NAME_ = envprobe
_R_CHECK_R_DEPENDS_ = warn
_R_CHECK_SCREEN_DEVICE_ = stop
_R_CHECK_TIMINGS_ = 10
_R_CHECK_BASHISMS_ _R_CHECK_BROWSER_NONINTERACTIVE_
_R_CHECK_CODE_USAGE_VIA_NAMESPACES_
_R_CHECK_CODE_USAGE_WITH_ONLY_BASE_ATTACHED_
_R_CHECK_CODOC_VARIABLES_IN_USAGES_ _R_CHECK_COMPILATION_FLAGS_
_R_CHECK_CONNECTIONS_LEFT_OPEN_ _R_CHECK_DATALIST_
_R_CHECK_DEPENDS_ONLY_DATA_ _R_CHECK_DOT_FIRSTLIB_
_R_CHECK_FUTURE_FILE_TIMESTAMPS_ _R_CHECK_INSTALL_DEPENDS_ _R_CHECK_LICENSE_
_R_CHECK_LIMIT_CORES_ _R_CHECK_MATRIX_DATA_ _R_CHECK_MBCS_CONVERSION_FAILURE_
_R_CHECK_NATIVE_ROUTINE_REGISTRATION_ _R_CHECK_NEWS_IN_PLAIN_TEXT_
_R_CHECK_NO_RECOMMENDED_ _R_CHECK_NO_STOP_ON_TEST_ERROR_ _R_CHECK_ORPHANED_
_R_CHECK_PACKAGES_USED_CRAN_INCOMING_NOTES_ _R_CHECK_PACKAGES_USED_IN_DEMO_
_R_CHECK_PACKAGES_USED_IN_TESTS_USE_SUBDIRS_
_R_CHECK_PACKAGE_DATASETS_SUPPRESS_NOTES_ _R_CHECK_PRAGMAS_
_R_CHECK_RCPP_NOT_NEEDED_ _R_CHECK_RD_CONTENTS_KEYWORDS_
_R_CHECK_RD_NOTE_LOST_BRACES_ _R_CHECK_R_ON_PATH_
_R_CHECK_S3_METHODS_SHOW_POSSIBLE_ISSUES_ _R_CHECK_SHLIB_OPENMP_FLAGS_
_R_CHECK_URLS_SHOW_301_STATUS_ _R_CHECK_VALIDATE_UTF8_
_R_CHECK_XREFS_NOTE_MISSING_PACKAGE_ANCHORS_
Source: R Internals: Tools
The values are the ones that this R 4.6.1 sets, and CRAN’s own machines add more. The documentation of devtools::check(cran = TRUE), discussed below, says the same about itself: it imitates the settings of CRAN on a best-effort basis, because they keep moving, and is more than a call with --as-cran.
The list of checks
The whole list of what R CMD check --as-cran does for the shelf package, in the order it runs, is shown below, with the remote CRAN-incoming checks switched off through _R_CHECK_CRAN_INCOMING_REMOTE_=false. Every step is OK, the status is OK and the tests step shows that R CMD check ran testthat.R (the skip_on_cran() tests are among the skipped ones).
* checking for file ‘shelf/DESCRIPTION’ ... OK
* this is package ‘shelf’ version ‘0.1.0’
* package encoding: UTF-8
* checking CRAN incoming feasibility ... OK
* checking package namespace information ... OK
* checking package dependencies ... OK
* checking if this is a source package ... OK
* checking if there is a namespace ... OK
* checking for executable files ... OK
* checking for hidden files and directories ... OK
* checking for portable file names ... OK
* checking for sufficient/correct file permissions ... OK
* checking whether package ‘shelf’ can be installed ... OK
* checking installed package size ... OK
* checking package directory ... OK
* checking for future file timestamps ... OK
* checking ‘build’ directory ... OK
* checking DESCRIPTION meta-information ... OK
* checking top-level files ... OK
* checking for left-over files ... OK
* checking index information ... OK
* checking package subdirectories ... OK
* checking code files for non-ASCII characters ... OK
* checking R files for syntax errors ... OK
* checking whether the package can be loaded ... OK
* checking whether the package can be loaded with stated dependencies ... OK
* checking whether the package can be unloaded cleanly ... OK
* checking whether the namespace can be loaded with stated dependencies ... OK
* checking whether the namespace can be unloaded cleanly ... OK
* checking loading without being on the library search path ... OK
* checking whether startup messages can be suppressed ... OK
* checking use of S3 registration ... OK
* checking dependencies in R code ... OK
* checking S3 generic/method consistency ... OK
* checking replacement functions ... OK
* checking foreign function calls ... OK
* checking R code for possible problems ... OK
* checking Rd files ... OK
* checking Rd metadata ... OK
* checking Rd line widths ... OK
* checking Rd cross-references ... OK
* checking for missing documentation entries ... OK
* checking for code/documentation mismatches ... OK
* checking Rd \usage sections ... OK
* checking Rd contents ... OK
* checking for unstated dependencies in examples ... OK
* checking contents of ‘data’ directory ... OK
* checking data for non-ASCII characters ... OK
* checking LazyData ... OK
* checking data for ASCII and uncompressed saves ... OK
* checking R/sysdata.rda ... OK
* checking installed files from ‘inst/doc’ ... OK
* checking files in ‘vignettes’ ... OK
* checking examples ... OK
* checking for unstated dependencies in ‘tests’ ... OK
* checking tests ...
Running ‘testthat.R’
OK
* checking for unstated dependencies in vignettes ... OK
* checking package vignettes ... OK
* checking re-building of vignette outputs ... OK
* checking for non-standard things in the check directory ... OK
* checking for detritus in the temp directory ... OK
* DONE
Status: OK
Grouped by what they protect, the steps are these.
| Group | Steps in the list above | What goes wrong when they fail |
|---|---|---|
The package as a file tree |
executable files, hidden files and directories, portable file names, file permissions, package directory, |
Stray binaries, |
Metadata |
DESCRIPTION meta-information, CRAN incoming feasibility, namespace information, dependencies, source package, installed package size, future file timestamps |
Missing or malformed fields, a title or description CRAN rejects, a declared package that does not exist, a tarball over the size limits |
Installation and loading |
installed, can be loaded (with the usual default packages and with only base), can be unloaded, namespace loaded with stated dependencies, loading without being on the library search path, startup messages can be suppressed, S3 registration |
A package that needs another one it does not declare, a startup message printed with |
R code |
code files for non-ASCII characters, R files for syntax errors, dependencies in R code, S3 generic and method consistency, replacement functions, foreign function calls, R code for possible problems |
A non-ASCII character in a source file, |
Documentation |
Rd files, Rd metadata, Rd line widths, Rd cross-references, missing documentation entries, code and documentation mismatches, Rd |
A documented argument that does not exist, an exported object without a help page, a link to a topic that is not there |
Data |
contents of the |
Badly compressed |
Examples, tests and vignettes |
installed files from |
An example that fails or runs for minutes, a test that needs a package it does not declare, a vignette that cannot be rebuilt |
Two of the steps are worth a second look because they decide whether the check finishes in a state CRAN accepts. "Checking R code for possible problems" runs codetools over the code, which finds a variable that is used but never defined, a call to a function that does not exist and arguments that do not match; _R_CHECK_CODETOOLS_PROFILE_="suppressLocalUnused=FALSE" additionally reports local variables that are assigned and never used, which often points at a mistyped name or an assignment in the wrong scope. "Checking re-building of vignette outputs" re-makes the vignettes from their sources in a copy of the package, so a vignette that depends on a file that .Rbuildignore removed fails here and nowhere else. The chapter on style and linting, Style, Linting and Static Analysis, shows codetools::checkUsage() on its own.
The incoming feasibility check and the notes of a new submission
"Checking CRAN incoming feasibility" is the step that reproduces what the CRAN machines do first with a submission. Without network access it checks what can be decided locally; with it (_R_CHECK_CRAN_INCOMING_REMOTE_, on by default for --as-cran) it also looks up the package name in the indexes of CRAN and its archive and checks the URLs in the sources. The shelf package passes this step when the remote part is off (the step is reported as OK in the list above) and fails it when it is on. A copy renamed to a free name shows what the step reports for a perfectly good new package. The excerpt is real: the status is a single NOTE.
* using options ‘--no-manual --as-cran’
* checking for file ‘shelfdesk/DESCRIPTION’ ... OK
* this is package ‘shelfdesk’ version ‘0.1.0’
* package encoding: UTF-8
* checking CRAN incoming feasibility ... NOTE
Maintainer: ‘Shelf Maintainers <maintainer@example.com>’
New submission
* checking package namespace information ... OK
[... the other checks, all OK, are left out here ...]
* DONE
Status: 1 NOTE
Source: CRAN: Checklist for submissions
That NOTE, Maintainer: followed by New submission, is the one that cannot be removed and that the template of usethis::use_cran_comments() anticipates: it tells the CRAN team that this is a first submission and that a human has to look at it. Say so in the comments ("This is a new release") and move on. The usual notes of a new submission, with what each means, are these.
| Message | Meaning | Action |
|---|---|---|
|
CRAN shows the maintainer it will write to and flags a first submission |
Check the address; mention the new release in |
|
A spell checker ( |
Put names, acronyms and technical words in single quotes, or fix the typo; explain real words that remain in the comments |
|
A URL in |
Fix, redirect to the final address, or remove the URL (see below) |
A NOTE in the step "checking for future file timestamps" with the text |
The step compares the clock of the machine with an external clock ( |
Run the check where the Internet is reachable, or set |
|
The conventions of the |
Rewrite the fields (the next excerpt) |
A NOTE in the step "checking for non-standard things in the check directory" |
A file was left by the check in the directory, such as a |
Fix the cause (the manual section below) |
Source: R Internals: Tools
The conventions of the text fields are checked mechanically, so they are easy to break and easy to repair. A second copy of the package, renamed shelfbad, was given a lower-case title and a description that starts with "This package"; the real excerpt shows both findings, and tools::toTitleCase() computes the correction.
* using options ‘--no-manual --as-cran’
* checking for file ‘shelfbad/DESCRIPTION’ ... OK
* this is package ‘shelfbad’ version ‘0.1.0’
* package encoding: UTF-8
* checking CRAN incoming feasibility ... NOTE
Maintainer: ‘Shelf Maintainers <maintainer@example.com>’
New submission
The Title field should be in title case. Current version is:
‘tools for a small library catalogue’
In title case that is:
‘Tools for a Small Library Catalogue’
The Description field should not start with the package name,
'This package' or similar.
* checking package namespace information ... OK
[... the other checks, all OK, are left out here ...]
* DONE
Status: 1 NOTE
URLs are checked with the equivalent of curl -I -L: the check reports a URL that cannot be fetched, that answers with an error status, or that is redirected permanently (when the redirection is permanent, CRAN wants the final address), and the notes on the CRAN site explain false alarms such as sites that refuse the request type, need cookies or answer 403 and 503 to programs. A placeholder address in inst/CITATION of the shelf package was reported during development as a 404 and was fixed by removing the URL; the CITATION file of shelf has no url today. A third copy, renamed shelfurl, puts the placeholder host example.com back so that the report can be shown; in the container the connection to it failed at the TLS handshake, which gives a different status line from the 404 seen during development, with the same layout of the NOTE.
* using options ‘--no-manual --as-cran’
* checking for file ‘shelfurl/DESCRIPTION’ ... OK
* this is package ‘shelfurl’ version ‘0.1.0’
* package encoding: UTF-8
* checking CRAN incoming feasibility ... NOTE
Maintainer: ‘Shelf Maintainers <maintainer@example.com>’
New submission
Found the following (possibly) invalid URLs:
URL: https://example.com/shelf
From: inst/CITATION
Status: Error
Message: libcurl error code 35:
OpenSSL SSL_connect: SSL_ERROR_SYSCALL in connection to example.com:443
* checking package namespace information ... OK
[... the other checks, all OK, are left out here ...]
* DONE
Status: 1 NOTE
Source: CRAN: URL checks
Both the 404 and the TLS error mean that the check could not confirm the address, and the cure is the same: use an address that answers, or no address. \url{} markup in Rd files is for always-active URLs only (examples that use localhost use \samp{}). If a site answers 403 or 503 to programs but works in a browser, say so in the submission comments; if a permanent redirect is reported, replace the address by the final one.
The manual, LaTeX and --no-manual
R CMD check also builds the PDF manual of the package, which is how a bad Rd file is found, and validates the HTML version of the manual with HTML Tidy (R 4.2.0). The first needs a TeX installation and suitable fonts, the second a recent tidy. The image used for this page has neither, so the commands above use --no-manual, and the same check without that option shows what a missing TeX looks like. The lines are real (a temporary path was removed from the middle of the ERROR text):
* checking PDF version of manual ... WARNING
LaTeX errors when creating PDF version.
This typically indicates Rd problems.
* checking PDF version of manual without index ... ERROR
Re-running with no redirection of stdout/stderr.
Hmm ... looks like a package
Converting parsed Rd's to LaTeX
Creating pdf output from LaTeX ...
Warning in texi2dvi(file = file, pdf = TRUE, clean = clean, quiet = quiet, :
texi2dvi script/program not available, using emulation
Error in texi2dvi(file = file, pdf = TRUE, clean = clean, quiet = quiet, :
pdflatex is not available
Warning in texi2dvi(file = file, pdf = TRUE, clean = clean, quiet = quiet, :
texi2dvi script/program not available, using emulation
Error in texi2dvi(file = file, pdf = TRUE, clean = clean, quiet = quiet, :
pdflatex is not available
Error in running tools::texi2pdf()
* checking HTML version of manual ... NOTE
Skipping checking HTML validation: no command 'tidy' found.
Please obtain a recent version of HTML Tidy by downloading a binary
release or compiling the source code from <https://www.html-tidy.org/>.
* checking for non-standard things in the check directory ... NOTE
Found the following files/directories:
‘shelf-manual.tex’
[...]
Status: 1 ERROR, 1 WARNING, 2 NOTEs
On a machine without TeX, --no-manual is the right way to run the checks that matter (devtools::check() passes it by default through manual = FALSE); before submitting, build the manual once on a machine that has TeX, because that is where a bad Rd file shows up. The file shelf-manual.tex reported by the last NOTE is what the failed build left in the check directory.
rcmdcheck and devtools::check(cran = TRUE)
The R package rcmdcheck (1.4.0) runs R CMD check from R, with its output and its results available as an object: the checks of devtools and of GitHub Actions workflows are built on it. It takes the path of a tarball or a package directory, the arguments for R CMD check and for R CMD build, a check directory and extra environment variables, and returns an object with errors, warnings and notes lists, the package name and version, the exit status and the log. The script below is run from the package root after R CMD build shelf; quiet = TRUE suppresses the live log, and the Duration: line that print() adds (a timing) is dropped from the printed summary.
# Run from the package root (HOME is the directory that holds the tarball made
# by R CMD build). rcmdcheck 1.4.0 runs R CMD check and returns its results.
res <- rcmdcheck::rcmdcheck(
"~/shelf_0.1.0.tar.gz",
args = c("--as-cran", "--no-manual"),
env = c("_R_CHECK_CRAN_INCOMING_REMOTE_" = "false"),
check_dir = tempfile("check"),
quiet = TRUE
)
class(res)
#> [1] "rcmdcheck"
c(res$package, res$version)
#> [1] "shelf" "0.1.0"
c(
errors = length(res$errors),
warnings = length(res$warnings),
notes = length(res$notes)
)
#> errors warnings notes
#> 0 0 0
# The printed summary; the Duration line is left out because it is a timing.
out <- capture.output(print(res))
writeLines(out[!startsWith(out, "Duration")])
#> ── R CMD check results ──────────────────────────────────────── shelf 0.1.0 ────
#>
#> 0 errors ✔ | 0 warnings ✔ | 0 notes ✔
The same call with the remote checks left on (the default for --as-cran) returns the error of the name. This script needs the network, and what it prints depends on the contents of CRAN on the day it runs; the timing that rcmdcheck adds to the first line of the error is left out.
# Needs the network: with --as-cran and no override, R CMD check also runs the
# remote CRAN incoming checks, which compare the name with CRAN's index.
res <- rcmdcheck::rcmdcheck(
"~/shelf_0.1.0.tar.gz",
args = c("--as-cran", "--no-manual"),
check_dir = tempfile("check"),
quiet = TRUE
)
length(res$errors)
#> [1] 1
cat(res$errors, sep = "\n")
#> checking CRAN incoming feasibility ... ERROR
#> Maintainer: ‘Shelf Maintainers <maintainer@example.com>’
#>
#> New submission
#>
#> Conflicting package names (submitted: shelf, existing: SHELF [https://CRAN.R-project.org/src/contrib])
#>
#> Conflicting package names (submitted: shelf, existing: SHELF [CRAN archive])
devtools::check() is rcmdcheck behind the usual development loop: it builds the package in an independent session, optionally runs devtools::document() first (by default only when the installed roxygen2 matches the version recorded in DESCRIPTION) and checks the result with the settings of CRAN. Its cran = TRUE default asks for the settings CRAN uses, on a best-effort basis; remote = FALSE leaves the remote incoming checks off, incoming follows remote, force_suggests = FALSE keeps the check from failing when suggested packages are absent, run_dont_test = FALSE only matters for R 3.6 and earlier, because in R 4.0 and later \donttest code always runs as part of the CRAN checks, manual = FALSE is --no-manual, and env_vars = c(NOT_CRAN = "true") makes the skip_on_cran() tests run. Pass remote = TRUE for the real incoming check. The result is the same rcmdcheck object, with the same summary.
# Run from the package root. devtools 2.5.2: cran = TRUE is the default, and
# remote checks are off unless remote = TRUE. manual = FALSE means --no-manual.
unlist(formals(devtools::check)[c("cran", "remote", "manual")])
#> cran remote manual
#> TRUE FALSE FALSE
res <- devtools::check(
".",
document = FALSE,
manual = FALSE,
quiet = TRUE,
check_dir = tempfile("check")
)
out <- capture.output(print(res))
writeLines(out[!startsWith(out, "Duration")])
#> ── R CMD check results ──────────────────────────────────────── shelf 0.1.0 ────
#>
#> 0 errors ✔ | 0 warnings ✔ | 0 notes ✔
The policy and the community guidance agree on the target: no ERRORs, no WARNINGs, and as few NOTEs as possible. In principle a package has to pass without warnings or significant notes to enter the main CRAN area, and a NOTE that cannot be removed is listed and explained in cran-comments.md or in the comment of the submission form.
Checking on other systems
A check on your own machine tells you about your own machine. CRAN checks a package on several flavours of Linux, on macOS and on Windows, with the released R, R-patched and R-devel, and a bug that depends on the operating system, the compiler, the C library, the locale or the version of R shows up there first. The submission checklist of CRAN names two services to use before submitting, win-builder and R-hub, and the policy adds mac-builder for the macOS arm64 flavour. Everything in this section is described from the official documents and none of it was run: the services need an account, an upload or a GitHub repository, and a result by email or on a web page.
win-builder
win-builder (https://win-builder.r-project.org/) is the R project’s service for building and checking source packages on Windows, meant for people who have no Windows machine, and the one the CRAN submission checklist names, saying that the CRAN team uses the same infrastructure for the packages submitted. It builds and checks a source package on Windows with R-devel, R-release or R-oldrelease, each under its own upload directory; the page lists the R version each directory currently holds and the Rtools release it uses. The procedure is to build the tarball with R CMD build, make sure the Maintainer field of DESCRIPTION holds your own email address (the answer is sent there and nowhere else), upload the file by passive FTP in binary mode as an anonymous user to the directory of the R version, or through the upload form, and wait about half an hour for an email that links to a randomly named directory with the binary and the check logs, which is deleted after roughly 72 hours. All CRAN and Bioconductor software packages are already installed on the service; a package that needs another one can be preceded by uploading that one first. The service is a shared machine, so the disclaimer on its page applies: do not upload other people’s packages, and do not expect confidentiality.
devtools wraps the upload so that you do not leave R. The functions check_win_devel(), check_win_release() and check_win_oldrelease() bundle the source package, upload it and make the answer arrive by email at the maintainer’s address (or at email =), which usually takes about 30 minutes; webform = TRUE uses the web form instead of passive FTP. They are a fragment here: running them submits the package to a remote machine.
# A fragment; it uploads the package to win-builder and needs the Internet
# and a maintainer address that can receive the reply.
devtools::check_win_devel()
devtools::check_win_release()
devtools::check_win_oldrelease()
mac-builder
mac-builder (https://mac.r-project.org/macbuilder/submit.html) is the macOS counterpart, maintained by the people who build the CRAN macOS binaries. You upload a source tarball prepared with R CMD build that lists you as the maintainer, choose the R version (release or development) and optionally add custom dependencies that are not on CRAN (they must not have cycles and must pass their checks). By default the machine has the same setup and packages as the CRAN M1 build machine, so an issue reported on the macOS arm64 flavour of the CRAN check page can be reproduced there. The CRAN policy asks you to check with mac-builder before resubmitting a package that shows issues for macos-arm64. devtools::check_mac_release() and devtools::check_mac_devel() upload the tarball and return the URL with the results. This is a fragment too.
# A fragment; it uploads the package to mac-builder.
devtools::check_mac_release()
devtools::check_mac_devel()
R-hub v2
R-hub is the community service for checking a package on many platforms; it is not maintained by CRAN, so its settings can differ from the ones CRAN uses. Since rhub 2.0.0 (2024-04-11) it is a completely new system: R-hub v2 runs R CMD check and similar checks on GitHub Actions, and the functions of the first version (rhub::check(), rhub::check_for_cran(), rhub::validate_email() and friends) are deprecated and defunct. The current release is rhub 2.0.1 (2025-03-06). It is not installed in the image used for this page, so what follows is the documented workflow with its command names, and nothing was run.
The requirements are a GitHub account, a package in a GitHub repository whose origin remote points to it, and a GitHub personal access token stored in the git credential store (for instance with gitcreds::gitcreds_set()). rhub_setup() adds the workflow file .github/workflows/rhub.yaml to the package, which then has to be committed and pushed to the default branch of the repository, rhub_doctor() checks that everything is set up, rhub_platforms() lists the platforms, and rhub_check() starts a check on the platforms you choose, in an interactive menu or through the platforms argument. The platforms are of two kinds: virtual machines on GitHub Actions (linux, macos, macos-arm64, windows) and containers with many more configurations, such as R-devel under several compilers, sanitizers, valgrind, no long doubles, a build without suggested packages, and the donttest container that runs the \donttest examples. The check picks the package up from GitHub, so changes have to be pushed first; the branch argument selects another branch, which needs the workflow file as well; the results are on the Actions tab of the repository and arrive as GitHub notifications, not as an email. Private repositories work and use the free minutes of the GitHub plan. The four calls below are a fragment: they need the repository and the token, and none of them was run.
# A fragment; it needs a GitHub repository, a token and rhub 2.0.1.
rhub::rhub_setup()
rhub::rhub_doctor()
rhub::rhub_platforms()
rhub::rhub_check(platforms = c("linux", "windows"))
The workflow file is generated and rarely edited by hand; the first lines of the canonical file in the r-hub/actions repository are shown below as it was on 2026-10-21 (the header comments and the jobs after setup are left out). It starts from workflow_dispatch, which is how rhub_check() triggers it, with a comma-separated list of platforms in the config input, and the rest are jobs built from the r-hub/actions building blocks: setup, checkout, platform-info, setup-deps and run-check, with setup-r for the virtual machines.
name: R-hub
run-name: >-
${{ github.event.inputs.id || format('Manual run by @{0}', github.triggering_actor) }}:
${{ github.event.inputs.name || github.event.inputs.config }}
on:
workflow_dispatch:
inputs:
config:
description: >-
Comma-separated list of R-hub platforms to use.
Full list: summary(rhub::rhub_platforms())
type: string
default: 'linux,windows,macos'
name:
description: 'Run name. You can leave this empty.'
type: string
id:
description: 'Unique ID. You can leave this empty.'
type: string
jobs:
setup:
runs-on: ubuntu-latest
outputs:
containers: ${{ steps.rhub-setup.outputs.containers }}
platforms: ${{ steps.rhub-setup.outputs.platforms }}
steps:
# NO NEED TO CHECKOUT HERE
- uses: r-hub/actions/setup@v1
with:
config: ${{ github.event.inputs.config }}
id: rhub-setup
# linux-containers and other-platforms jobs follow: checkout, platform-info,
# setup-deps and run-check from r-hub/actions@v1
A package that is not on GitHub can still use the shared runners of the R Consortium. rc_new_token() obtains a token for the email address (once per machine and address; rc_list_local_tokens() lists the tokens already present), and rc_submit() submits the package, path or tarball, and the platforms, which must be named explicitly in a non-interactive session; the maintainer’s email address is used unless email = says otherwise. The limits are real: the package and the check output are public in the organization https://github.com/r-hub2, the runners are shared so a build may wait, at least five minutes must pass between submissions, and a GitHub account is needed to read the logs. For anything private, put the package in a private GitHub repository and use rhub_setup() and rhub_check(). The two calls below are a fragment, not run.
# A fragment; it sends the package to public runners of the R Consortium.
rhub::rc_new_token()
rhub::rc_submit()
GitHub Actions with r-lib/actions
If the package already lives on GitHub, the quickest way to check it on every push is the standard workflow of the r-lib/actions repository, which usethis::use_github_action("check-standard") copies into .github/workflows/R-CMD-check.yaml (when the name starts with check- the file is saved as R-CMD-check.yaml and a badge is requested for the README), and adds the workflow file to .Rbuildignore. The check-standard workflow runs R CMD check with the latest R release on Linux, macOS and Windows, and with R-devel and the previous release on Linux, which usethis presents as a good baseline for a package that goes to CRAN. It does not change the policy rule that the check must also pass with a development version of R before submission: the devel entry in the matrix does that.
on:
push:
branches: [main, master]
pull_request:
name: R-CMD-check.yaml
permissions: read-all
jobs:
R-CMD-check:
runs-on: ${{ matrix.config.os }}
name: ${{ matrix.config.os }} (${{ matrix.config.r }})
strategy:
fail-fast: false
matrix:
config:
- {os: macos-latest, r: 'release'}
- {os: windows-latest, r: 'release'}
- {os: ubuntu-latest, r: 'devel', http-user-agent: 'release'}
- {os: ubuntu-latest, r: 'release'}
- {os: ubuntu-latest, r: 'oldrel-1'}
env:
GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }}
R_KEEP_PKG_SOURCE: yes
steps:
- uses: actions/checkout@v6
- uses: r-lib/actions/setup-pandoc@v2
- uses: r-lib/actions/setup-r@v2
with:
r-version: ${{ matrix.config.r }}
http-user-agent: ${{ matrix.config.http-user-agent }}
- uses: r-lib/actions/setup-r-dependencies@v2
with:
extra-packages: any::rcmdcheck
needs: check
- uses: r-lib/actions/check-r-package@v2
with:
upload-snapshots: true
build_args: 'c("--no-manual","--compact-vignettes=gs+qpdf")'
The file above is the check-standard example of the v2 branch of r-lib/actions as it was on 2026-10-21 (the header comments are left out); use_github_action() takes the example of the latest published release of the repository, so what it writes can differ in small details. check-r-package runs rcmdcheck with the arguments c("--no-manual", "--as-cran") by default, fails the job on a warning (error-on is "warning" unless you change it), uploads the check results as an artifact when the job fails, and sets _R_CHECK_FORCE_SUGGESTS_ and _R_CHECK_CRAN_INCOMING_ to false when they are not already set in the environment of the job. That last detail matters for this page: with _R_CHECK_CRAN_INCOMING_ off, the incoming feasibility step, and with it the name lookup, is not run by the standard workflow, so a name clash such as the one above is found by R CMD check --as-cran on a developer machine, by R-hub or by CRAN, and not by the default workflow. Its sibling setup-r-dependencies installs the dependencies of the package with pak and caches them.
Reverse dependencies
A package that other packages use cannot be updated as if it were private. The packages that list yours in Depends, Imports or LinkingTo are its reverse strong dependencies, those that list it in Suggests or Enhances are the weaker ones, and each of them is checked against your new version by CRAN. The policy tells you to check, for an update, that the packages depending on yours still pass R CMD check, and to check the reverse strong dependencies, the reverse suggests and the recursive strong dependencies of these. A new package has no reverse dependencies, but the same habit starts on the day someone depends on it.
Finding them
tools::package_dependencies(reverse = TRUE) computes the reverse dependencies from a matrix shaped like the result of available.packages(): a Package column and the dependency fields Depends, Imports, LinkingTo, Suggests and Enhances. Its which argument takes the field names or the shorthands "strong" (the default), "most" (adds Suggests) and "all", and recursive follows the chain of dependencies. The first script builds that matrix by hand for four imaginary packages that use shelf, so it needs no network and its output never changes. The second one asks the live index about a long-established package and prints only comparisons, because the counts change every day; it needs the network.
# tools::package_dependencies() reads a matrix shaped like the result of
# available.packages(). This one is typed by hand for a few imaginary packages
# that use shelf, so the example needs no network.
db <- rbind(
shelf = c(Depends = "R (>= 4.1.0)", Imports = NA, Suggests = "testthat"),
shelfreports = c(Depends = NA, Imports = "shelf, tools", Suggests = NA),
shelfdash = c(Depends = "shelf", Imports = "shelfreports", Suggests = NA),
shelfapi = c(Depends = NA, Imports = NA, Suggests = "shelf"),
shelfutil = c(Depends = NA, Imports = "stats", Suggests = NA)
)
db <- cbind(Package = rownames(db), db, LinkingTo = NA, Enhances = NA)
# Strong reverse dependencies (Depends, Imports, LinkingTo): the packages whose
# checks a change to shelf can break directly.
tools::package_dependencies("shelf", db, reverse = TRUE)
#> $shelf
#> [1] "shelfreports" "shelfdash"
#>
# Including Suggests, and following the chain of strong dependencies.
tools::package_dependencies("shelf", db, which = "most", reverse = TRUE)
#> $shelf
#> [1] "shelfreports" "shelfdash" "shelfapi"
#>
tools::package_dependencies(
"shelf", db,
which = "most", reverse = TRUE, recursive = "strong"
)
#> $shelf
#> [1] "shelfapi" "shelfdash" "shelfreports"
#>
# Needs the network: the index of the packages that exist on CRAN today.
repos <- c(CRAN = "https://cloud.r-project.org")
ap <- available.packages(repos = repos)
class(ap)
#> [1] "matrix" "array"
c("Package", "Version", "Depends", "Imports", "Suggests") %in% colnames(ap)
#> [1] TRUE TRUE TRUE TRUE TRUE
# The packages that depend on a long-established one, taken from the same
# index. The numbers change every day, so only a comparison is printed.
strong <- tools::package_dependencies("MASS", ap, reverse = TRUE)
class(strong)
#> [1] "list"
length(strong[["MASS"]]) > 100L
#> [1] TRUE
most <- tools::package_dependencies("MASS", ap, which = "most", reverse = TRUE)
length(most[["MASS"]]) >= length(strong[["MASS"]])
#> [1] TRUE
The CRAN web page of a package lists its reverse dependencies, and devtools::revdep() returns them as a vector (the first call in a session downloads the metadata of CRAN and Bioconductor); the policy also points to the reverse_dependencies_with_maintainers function on the developer site of R to obtain the maintainers' addresses that have to be told about an API change. The maintainers of affected packages need advance notice, at least two weeks according to the policy.
Checking them
tools::check_packages_in_dir() is the tool the policy names. It checks every source tarball in a directory, and with a reverse argument it also downloads, installs and checks the reverse dependencies of those packages: reverse = list() takes the defaults (the repositories of getOption("repos"), the strong dependencies and no recursion), and the components repos, which and recursive can be set. The check of each reverse dependency is run against your package, so a failure that is new points at your change. tools::check_packages_in_dir_changes() compares two such runs, one made against the old release and one against the new, and tells you which packages changed status. The script below runs the function for the shelf tarball without reverse, which is all there is to do for a package with no dependents, and lists the directory it creates: Outputs holds the logs, Library the packages installed for the check and shelf.Rcheck the check itself; with reverse, the directories of the reverse dependencies are prefixed rdepends_.
# Run from the package root (HOME is the directory that holds the tarball made
# by R CMD build). check_packages_in_dir() checks every tarball in a directory;
# with reverse = list() it would also download and check the reverse
# dependencies of each, which a package that is not on CRAN does not have.
check_dir <- tempfile("checkdir")
dir.create(check_dir)
invisible(file.copy("~/shelf_0.1.0.tar.gz", check_dir))
tools::check_packages_in_dir(
check_dir,
check_args = c("--as-cran", "--no-manual"),
check_env = "_R_CHECK_CRAN_INCOMING_REMOTE_=false"
)
#> checking shelf_0.1.0.tar.gz ...
tools::summarize_check_packages_in_dir_results(check_dir)
#> Check status summary:
#> OK
#> Source packages 1
#>
#> Check results summary:
#> shelf ... OK
sort(list.files(check_dir))
#> [1] "Library" "Outputs" "PACKAGES"
#> [4] "PACKAGES.gz" "PACKAGES.rds" "shelf_0.1.0.tar.gz"
#> [7] "shelf.Rcheck" "timings.tab"
Checking hundreds of packages takes hours and a lot of disk space, which is why most maintainers use a tool that does it incrementally. The package revdepcheck of the r-lib organisation (not installed, not run) runs R CMD check twice for each reverse dependency, once with the CRAN version of your package and once with the local development version, in parallel, and reports the difference, so that failures that already existed do not count against you; it writes its summaries into a revdep directory, and the R Packages book (community material) suggests pasting revdep/cran.md into cran-comments.md. Both of the calls below are fragments.
# Fragments; they download and check every package that depends on yours.
tools::check_packages_in_dir(".", reverse = list())
revdepcheck::revdep_check(num_workers = 4)
When the reverse checks show a failure that your change causes, you have three possible answers: change your update so that it does not break the user, send the maintainers a patch before submitting, or explain in the comments that the break is intended and that they have been informed. The R Packages book notes that CRAN accepts an update that breaks other packages when the maintainers were given sufficient advance notice.
Submitting to CRAN
Nothing in this section was run: it describes the submission flow from the CRAN policy, the submission page and the guidance of the usethis and devtools packages. The shelf package is not submitted anywhere, and, as shown above, would be refused because of its name.
The web form and the confirmation email
A package is submitted through the form at https://cran.r-project.org/submit.html, and not by email: the policy is explicit that the submission form is the way in, and that the package itself must never be emailed. The form has three steps (upload, submission, confirmation). In the first you give your name, the email address of the maintainer, the package file (only .tar.gz files, at most 100 MB) and an optional comment; the page reminds you that the tarball has to come from R CMD build, that DESCRIPTION must hold a valid maintainer field of the form NAME <EMAIL>, and that you have to know the rest of the policies. The upload must be a source tarball made by R CMD build and named PACKAGE_VERSION.tar.gz. If someone other than the maintainer submits (a co-author, for instance), say so in the comment: the designated maintainer will have to confirm the submission. If the maintainer’s email address changed, explain it in the comment and, if possible, send a confirmation from the previous address to the submissions mailbox of CRAN as a separate message. If the package needs special treatment (vignettes that can only be rebuilt on the maintainer’s machine or that take very long), say so in the comment too.
CRAN then sends a confirmation email to the maintainer’s address, with a link that must be followed: at that link you are asked to confirm that you have followed the policies and that you want the package submitted. Until you do, the package is not submitted. The email check also confirms that the address in DESCRIPTION works, which is why that address has to be valid and unfiltered. After the confirmation the package enters CRAN’s automatic checks, which the R Packages book (community material) describes as a first round of R CMD check on Windows and Linux, probably with released and development R, with a second email with links to the results usually within hours. A new package is then looked at by a member of the CRAN team; the book adds that the process can be fully automated for an update of a package that is already on CRAN, and that reverse dependency checks follow when the update passes the first round.
Do not submit a package again while the first upload is pending and no reply has arrived, which can take a few days. The policy says you can check that the upload was received by looking at the directory listing at https://CRAN.R-project.org/incoming/. The listing shows, at the time of writing (2026-10-21), the folders archive, inspect, newbies, pending, pretest, publish, recheck, special and waiting, next to folders named after people, each with a last-modified time. CRAN does not document on the policy or submission pages what each folder means, and the names are an implementation detail of the submission system that may change, so use the listing only for what the policy says it is for, to see that a file arrived, and expect the answer by email.
Feedback and resubmission
CRAN’s reply is either an acceptance (the usual short message from the CRAN team saying, in community write-ups, that the package is "on its way to CRAN") or a list of things to change. Feedback is normally about something automatic that failed, about the text of DESCRIPTION (the title and the description are real hotspots, according to the R Packages book), about examples and tests that take too long, about the policy items in this page, or about a note that you did not explain. Fix everything in the list, not just the first item.
Resubmission is done in the same way as submission, using the optional comment field of the web form to explain how the feedback was addressed, and not a separate email. Updates to a package that is already on CRAN must have a higher version number, and the policy says that increasing the version at each submission reduces confusion, so it is preferred even when the previous submission was rejected: a new package that was rejected with version 0.1.0 is resubmitted as 0.1.1. The R Packages book adds that this means gaps in released version numbers, which does not matter. Start the comment with a "Resubmission" heading and a list of the changes made, and keep the results of the new checks below it (the next section shows the file).
cran-comments.md
cran-comments.md is not read by R CMD check and is not part of the package: it is a note to the people who review the submission, kept in the repository, excluded from the tarball with .Rbuildignore and pasted (or sent by devtools::submit_cran()) into the comment field. usethis::use_cran_comments() creates it and adds the .Rbuildignore line, as this script shows in a throw-away project. The template has the results of R CMD check in the format 0 errors | 0 warnings | 1 note, and a bullet for the explanation of the single NOTE that a first submission has.
# usethis 3.2.2. A throw-away package in a temporary directory; the messages
# that usethis prints about the active project are switched off.
options(usethis.quiet = TRUE)
path <- file.path(tempdir(), "demo")
usethis::create_package(path, open = FALSE, rstudio = FALSE)
usethis::with_project(path, usethis::use_cran_comments(open = FALSE))
writeLines(readLines(file.path(path, "cran-comments.md")))
#> ## R CMD check results
#>
#> 0 errors | 0 warnings | 1 note
#>
#> * This is a new release.
"cran-comments.md" %in% list.files(path)
#> [1] TRUE
grep("cran", readLines(file.path(path, ".Rbuildignore")), value = TRUE)
#> [1] "^cran-comments\\.md$"
The shelf package carries exactly that file:
## R CMD check results
0 errors | 0 warnings | 1 note
* This is a new release.
The convention is to say what was tested and what the results were: the systems and R versions on which the package was checked (your own machine, win-builder, mac-builder, R-hub or GitHub Actions), the results of R CMD check (the counts, and every NOTE explained), the results of the reverse dependency checks for an update, and the changes made in response to feedback. For a first submission of a package that is not called shelf and that has passed every check on this page, a complete file would look like the first block below; a resubmission adds a section at the top, as in the second one. Both blocks are illustrations, not output of a tool.
## Test environments
* local: R 4.6.1
* GitHub Actions: ubuntu-latest (release, devel and oldrel-1), macos-latest, windows-latest
* win-builder: R-devel, R-release
* mac-builder: R-release
## R CMD check results
0 errors | 0 warnings | 1 note
* This is a new release.
## Reverse dependencies
This is a new release, so there are no reverse dependencies.
## Resubmission
This is a resubmission. In this version I have:
* Converted the title in DESCRIPTION to title case and rewritten the
description so that it no longer starts with "This package".
* Removed the unreachable URL from inst/CITATION.
* Changed the package name, because the original one clashed with an existing
package that differs only in case.
## R CMD check results
0 errors | 0 warnings | 1 note
* This is a new release.
For an update, keep the same sections and write what you tested against the previous version: the number of reverse dependencies checked, the packages that fail with the new version and what you did about it (a patch sent to the maintainer, an announcement with the date). If a NOTE cannot be removed, explain it; if a URL check reports a false alarm, say so (CRAN’s URL notes describe the usual causes).
devtools::submit_cran() and the release checklist
devtools::submit_cran() uploads the tarball through the same web form, filling in your name and email from DESCRIPTION and the comment from cran-comments.md, and then you still have to follow the confirmation link in the email. It is described by devtools as the part of the release process that usethis::use_release_issue() defines. The older devtools::release() was the all-in-one route for years (it asked a list of yes-or-no questions and then submitted), but in devtools 2.5.2 it is deprecated in favour of usethis::use_release_issue(), because its authors no longer use it and cannot promise it follows current practice. use_release_issue() creates a checklist as a GitHub issue, based on the type of release (patch, minor or major), with the steps from the first check until the submission and the steps after acceptance; it can be edited, and you can add your own bullets through an unexported release_bullets() function in the package. The checklist is a plain list of tasks, not an automatic procedure. These are fragments: they need a GitHub repository and, for the second, the web form.
# Fragments; use_release_issue() creates a GitHub issue and submit_cran()
# uploads the package to CRAN's form.
usethis::use_release_issue("0.1.0")
devtools::submit_cran()
Versions and the life of a package
Version numbers
A package version is a sequence of at least two, and usually three, non-negative integers separated by single . or - characters, and it is not a decimal number: 0.9 is smaller than 0.75, because 9 is smaller than 75. R parses and compares versions with the class package_version (a numeric_version), which compares component by component. The convention that the R Packages book and usethis follow is major.minor.patch for a released version (a patch for a bug fix without new features, a minor version for compatible new features, a major version for changes that can break users), with a fourth component, .9000, for the development version between releases: usethis::use_dev_version() raises 0.1.0 to 0.1.0.9000 right after a release, so that the development version sorts after the release and before the next one. R accepts - as a separator (1.2-3 is 1.2.3), but the dot form is the recommended one.
# A package version is a sequence of integers separated by "." or "-"; it is
# compared component by component, not as a decimal number.
package_version("0.9") < package_version("0.75")
#> [1] TRUE
package_version("1.10.0") > package_version("1.9.0")
#> [1] TRUE
sort(c("1.9.0", "1.10.0", "1.2.0"))
#> [1] "1.10.0" "1.2.0" "1.9.0"
sort(package_version(c("1.9.0", "1.10.0", "1.2.0")))
#> [1] ‘1.2.0’ ‘1.9.0’ ‘1.10.0’
# A development version adds a fourth component and sorts after the release.
package_version("0.1.0.9000") > package_version("0.1.0")
#> [1] TRUE
package_version("0.1.0.9000") < package_version("0.2.0")
#> [1] TRUE
package_version("1.2-3") == package_version("1.2.3")
#> [1] TRUE
unlist(package_version("1.2-3"))
#> [1] 1 2 3
# "1.0-beta" is not a valid version.
tryCatch(package_version("1.0-beta"), error = conditionMessage)
#> [1] "invalid version specification ‘1.0-beta’"
# Depends: R (>= 4.1.0) is a test of this kind, made against R's own version.
getRversion() >= "4.1.0"
#> [1] TRUE
Source: R documentation: numeric_version
usethis::use_version() takes the level to raise ("major", "minor", "patch" or "dev"), writes the new Version, adds a heading to NEWS.md if the file exists and, in a repository that uses Git, commits those changes (push = TRUE also pushes). The script runs it in a throw-away project that is not under Git, with the usethis messages switched off: the new project starts at 0.0.0.9000, use_version("minor") makes 0.1.0 and writes the first NEWS.md heading, and use_dev_version() makes 0.1.0.9000 and adds the heading of the development version.
# usethis 3.2.2: use_version() raises the Version field and adds a heading to
# NEWS.md; use_dev_version() starts the next development version (.9000).
options(usethis.quiet = TRUE)
path <- file.path(tempdir(), "demo")
usethis::create_package(path, open = FALSE, rstudio = FALSE)
version_of <- \() read.dcf(file.path(path, "DESCRIPTION"), "Version")[[1L]]
version_of()
#> [1] "0.0.0.9000"
usethis::with_project(path, usethis::use_news_md(open = FALSE))
usethis::with_project(path, usethis::use_version("minor"))
version_of()
#> [1] "0.1.0"
usethis::with_project(path, usethis::use_dev_version())
version_of()
#> [1] "0.1.0.9000"
writeLines(readLines(file.path(path, "NEWS.md")))
#> # demo (development version)
#>
#> # demo 0.1.0
#>
#> * Initial CRAN submission.
The version of R is part of the contract as well. Depends: R (>= 4.1.0) states the oldest R that can run the package, and it should be there when the code needs something newer: the shelf package uses the backslash lambda \(i), which exists since R 4.1.0, and without the line R CMD build adds the field itself and warns about it. The manual says to drop trailing zeroes (R (>= 4.0) and not R (>= 4.0.0)) and that declaring a dependence on R without a version makes no sense. Do not raise the requirement without a reason: users on older R lose the package. The Imports and Suggests fields take version requirements the same way, testthat (>= 3.0.0), and the policy says that CRAN versions are expected to work with the current CRAN releases of their dependencies.
Deprecating and removing functions
A function that is going away is first deprecated, then defunct, then removed, with the steps spread over releases so that users and the packages that depend on yours can adapt; this is the practice base R itself follows for its own functions (see help("base-deprecated")). .Deprecated() goes in the body of the old function with the name of the replacement; it signals a warning of class deprecatedWarning with the fields old, new and package. When package is given, the message points to the help page <package>-deprecated that should list the deprecated functions of the package (the help page of Deprecated says so), so the package needs such a help page. .Defunct() does the same with an error of class defunctError. The script shows the messages as R 4.6.1 prints them; the code that the check looks for is _R_CHECK_DEPRECATED_DEFUNCT_, which is on for CRAN submissions and notes the use of recently deprecated or defunct functions in other people’s code.
# Retiring a function in steps: first a deprecation warning, later an error.
# `package = "shelf"` makes the message point to the package's
# shelf-deprecated help page (see the roxygen2 chapter for writing it).
old_summary <- function(...) {
.Deprecated("loan_summary", package = "shelf")
invisible(NULL)
}
gone_summary <- function(...) {
.Defunct("loan_summary", package = "shelf")
}
w <- tryCatch(old_summary(), warning = identity)
class(w)
#> [1] "deprecatedWarning" "warning" "condition"
writeLines(conditionMessage(w))
#> 'old_summary' is deprecated.
#> Use 'loan_summary' instead.
#> See help("Deprecated") and help("shelf-deprecated").
e <- tryCatch(gone_summary(), error = identity)
class(e)
#> [1] "defunctError" "error" "condition"
writeLines(conditionMessage(e))
#> 'gone_summary' is defunct.
#> Use 'loan_summary' instead.
#> See help("Defunct") and help("shelf-defunct").
# Without a replacement the message is shorter.
retired <- function() .Deprecated()
writeLines(conditionMessage(tryCatch(retired(), warning = identity)))
#> 'retired' is deprecated.
#> See help("Deprecated")
Source: R documentation: Deprecated
For anything more than a handful of functions, the lifecycle package (not installed here, not run) from the tidyverse team provides a vocabulary and tools: badges (experimental, stable, superseded, deprecated) in the documentation and functions such as lifecycle::deprecate_warn() that warn once every so often and not at every call, and lifecycle::deprecate_stop() for the defunct stage. The R Packages book and the lifecycle site describe the stages. Whatever the tool, give users at least one release with a warning before an error, say in NEWS.md which function replaces which, and use the reverse-dependency check to see who is affected.
A breaking change is not only a removed function. A different return type, a changed default, a warning that becomes an error, a different order of arguments and a changed class all break callers. The policy expects the maintainers of affected packages to be told in advance (at least two weeks, ideally more), expects the submission to name those packages, and warns that a change which causes significant disruption to other packages has to be agreed with the CRAN maintainers well before any publicity. If breaking is inevitable, a new major version, a new function name next to the old one, or a new package for the new design are all better than a silent change.
NEWS, CITATION and the installed package
The two files that readers meet after installation are NEWS.md and inst/CITATION, and both are read back by base R. news(package = "shelf") reads the NEWS.md of the installed package (from R 4.3.0 R CMD check also reports problems it meets reading that file), and citation("shelf") prints the entry that the CITATION file defines, with a BibTeX entry. The CITATION of shelf carries only a title, an author, a year and a note; a url that did not answer would have turned into the NOTE shown earlier. This script needs the installed package, so it loads the build of shelf 0.1.0 installed into a scratch library; loading it prints the startup message first.
# The installed shelf 0.1.0: the NEWS.md file and the CITATION file are read
# back by news() and citation(). R CMD check --as-cran examines both.
library(shelf)
#> shelf 0.1.0: loan period is 21 days
news(package = "shelf")
#> Changes in version 0.1.0
#>
#> - Initial version: the book class, clean_books(), clean_loans(),
#> overdue_loans(), loan_summary() and loans_per_month(), with the
#> books and loans datasets.
citation("shelf")
#> To cite package ‘shelf’ in publications use:
#>
#> Maintainers S (2026). _shelf: Tools for a Small Library Catalogue_. R
#> package version 0.1.0.
#>
#> A BibTeX entry for LaTeX users is
#>
#> @Manual{,
#> title = {shelf: Tools for a Small Library Catalogue},
#> author = {Shelf Maintainers},
#> year = {2026},
#> note = {R package version 0.1.0},
#> }
Source: R documentation: citation
Archival, orphaned packages and the CRAN checks page
CRAN re-checks every package continuously, on the flavours listed on the page https://cran.r-project.org/web/checks/ (the three current flavours of R on Debian GNU/Linux, Fedora, macOS and Windows), and each package has its own page at https://CRAN.R-project.org/web/checks/check_results_NAME.html with the status per flavour, the logs and the additional issues (compiler warnings, sanitizers, the donttest run and so on). A maintainer who no longer wants to maintain a package can ask for it to be archived; a package whose check gives an ERROR when a new R x.y.0 is released is archived unless the maintainer has promised and kept a firm date for an update; a package whose maintainer cannot be reached is archived or orphaned. The policy keeps the name: the Archive remains, names stay reserved, and a new maintainer can take over a package with the written agreement of the previous maintainer, or without it for a formally orphaned package. A package whose strong dependency leaves CRAN no longer meets the dependency rule described earlier, which is why the policy treats strong dependencies so strictly.
The checks page is also where a mistake after publication shows. Binary packages for Windows and macOS are built by CRAN, not by the maintainer, by batch jobs, and can take a day or two to appear, longer to reach mirrors. Look at the checks page a few days after each release, and wait until it is fully updated (at least 48 hours) before submitting a fix; the policy asks for exactly that.
After the package is accepted
When the CRAN team accepts the package it is published on the master site, and CRAN builds the binaries itself: the policy says that binary packages are not accepted from maintainers, that they are made automatically and that the Windows and macOS binaries can take a day or two to appear. The package page has the canonical address https://cran.r-project.org/package=NAME (Writing R Extensions prefers it to the longer /web/packages/NAME form), built from the Title, Description, authors, links (URL and BugReports), the NEWS file, the vignettes and the reference manual; install it with install.packages("NAME"). The package cranlogs (2.1.1 on CRAN; not installed, not run) queries a web service that keeps the download counts of the RStudio CRAN mirror, and the same service makes the download badges; usethis::use_cran_badge() adds a CRAN status badge to the README. Tag the release on GitHub (usethis::use_github_release() does it from the CRAN-SUBMISSION file that devtools::submit_cran() writes) and move the version to .9000 with usethis::use_dev_version(). The development of the next version goes on in the repository, which is why the policy can say that CRAN is not a development platform: between releases the work lives on GitHub.
Alternatives: Bioconductor, r-universe and GitHub
CRAN is not the only repository, and it is not always the right one.
Bioconductor (https://contributions.bioconductor.org/) is the project for high-throughput genomic analysis (sequencing, expression and other microarrays, flow cytometry, mass spectrometry and image analysis), and its software packages must address that area and interoperate with other Bioconductor packages by reusing their data structures. A package can be submitted to Bioconductor or to CRAN, not to both. Bioconductor releases twice a year, around April and October, tied to a version of R, with a devel branch where new features go and a release branch for bug fixes. Version numbers are x.y.z with an even y in a release and an odd y in devel, and a new package starts at 0.99.0. Packages live in git: you submit by opening an issue in the Bioconductor contributions repository on GitHub with the link to your repository (the default branch is used, so it must contain only package code), and the build system checks the package against devel; after acceptance you push to the Bioconductor git server and your package is built on a schedule against the release and devel branches (the maintainer is expected to read the build reports, which come weekly to daily). The package must pass R CMD build, R CMD check and BiocCheck (a package of the Bioconductor project, not installed here, not run) without ERROR; the DESCRIPTION carries at least two leaf terms of biocViews, which place the package in the Bioconductor taxonomy, and its description should be at least three complete sentences. Its guidelines on names follow CRAN’s (unique irrespective of case among current and past Bioconductor and CRAN packages). Install packages with BiocManager::install() (not installed here; see Packages, Libraries and Namespaces).
r-universe (https://docs.r-universe.dev/) is a platform from rOpenSci, now a project of the R Consortium, that builds packages from Git repositories and serves them as a CRAN-like repository with binaries, documentation and a catalogue. To publish, create a repository named <user>.r-universe.dev in lower case on your GitHub account, containing a file packages.json that lists the Git repositories of the packages (the packages do not have to be on GitHub; any public Git server works), and install the r-universe app on the account. The packages then appear at https://<user>.r-universe.dev after they are built. A registry entry has a package name and a url, with optional fields such as subdir for a package in a subdirectory and branch for a branch, a tag or the value "*release" for the most recent GitHub release.
[
{
"package": "shelf",
"url": "https://github.com/example-user/shelf"
},
{
"package": "shelfextras",
"url": "https://github.com/example-user/shelfextras",
"branch": "*release"
}
]
That registry is a placeholder: example-user is not an account, and the packages are the ones of this page. Users install from a universe by putting its address in the repositories ahead of CRAN, for one call or for the session. The script shows the option that results, with the placeholder address, and does not install anything.
# Installing from a universe means putting its address first in `repos`; the
# address below is a placeholder, so this script only shows the setting.
options(repos = c(
universe = "https://example-user.r-universe.dev",
CRAN = "https://cloud.r-project.org"
))
getOption("repos")
#> universe CRAN
#> "https://example-user.r-universe.dev" "https://cloud.r-project.org"
names(getOption("repos"))
#> [1] "universe" "CRAN"
Source: R documentation: options
The call that uses it is a fragment, since it would download from the placeholder address:
# A fragment; the universe is a placeholder.
install.packages(
"shelf",
repos = c("https://example-user.r-universe.dev", "https://cloud.r-project.org")
)
A CRAN package may use an optional package from a universe: list the universe in Additional_repositories, put the package in Suggests, use it conditionally, and document how to install it. r-universe also indexes CRAN and Bioconductor packages that have a Git URL in their metadata; a Config/runiverse/noindex: true line in DESCRIPTION opts out. Bioconductor is integrating r-universe into its own build process.
GitHub alone is the simplest alternative: a package in a repository can be installed with pak::pak("user/repo") (pak 0.11.1 is installed here and not run; the older remotes::install_github() and devtools::install_github() do the same). It needs no review and no check, which is why it suits development versions, internal packages and anything CRAN would not accept, and why it is not a substitute for a release: the user needs the build tools if the package has compiled code, and there are no binaries unless the workflow produces them.
Version notes
The R CMD check and CRAN items below come from the NEWS files of R and were read in the NEWS of 4.6.1 (see also the R-devel NEWS for upcoming changes).
| R version | Change that matters when checking for CRAN |
|---|---|
4.0.0 |
|
4.2.0 |
|
4.3.0 |
|
4.4.0 |
|
4.5.0 |
|
4.6.0 |
The default C++ standard is C++20 where available (a package can request C++17 if essential); |
The portability notes follow the compilers: a package with compiled code is compiled with C23 and C++20 by default on the R versions above, so code that relied on the older standards needs a SystemRequirements entry or a fix. The details of compiled code are in Interfacing C, C++ and Fortran. The shelf package has no compiled code and none of these apply to it. R 4.6.2 is scheduled for 2026-10-29 (a bug-fix release) and R-devel is 4.7.0; the policy asks for the final checks to be made with the current development version, or with R-patched or the current release when R-devel is not possible and the submission says so.
Where older tutorials differ
| Older advice | Current practice | Why |
|---|---|---|
Upload the tarball by FTP to the incoming area, or email it to the CRAN address |
Use the web form (or |
The policy tells you to use the form and not to send an email, and the form is the intake that CRAN reads |
|
|
A plain check skipped six steps on |
Test only with the released R |
Add R-devel (win-builder, R-hub or the |
The policy asks for the check with R-devel, or an explanation |
R-hub v1: |
R-hub v2: |
rhub 2.0.0 replaced the first system and made its functions deprecated and defunct |
|
|
|
Test on win-builder and nothing else |
win-builder, mac-builder, R-hub or GitHub Actions for macOS, Windows and Linux, and R-devel |
Windows-only testing misses the macOS arm64 flavour that the policy now asks you to check |
|
|
|
|
Only for a package with a |
|
Name found by trial and error at submission |
|
A clash with a name that differs only in case is refused at the incoming check, as the real output above shows |
A |
A real address or none |
The URL checks of |
|
|
|
A package that writes to the home directory or to the working directory |
Files below |
Policy: nothing outside the temporary directory |
Bundling the vignette output ( |
Source in |
The check rebuilds vignette outputs from their sources |
Where to go next
-
Creating Packages for the structure of a package, the
DESCRIPTIONandNAMESPACEfields, and theusethisanddevtoolsworkflow that produced theshelfpackage checked here. -
Documentation with Rd, roxygen2 and Vignettes for the Rd files,
@examplesIf, vignettes,NEWS.mdand theCITATIONfile that the checks above read. -
Testing with testthat for the tests that
R CMD checkruns,skip_on_cran()and the snapshot tests that are skipped on CRAN. -
Style, Linting and Static Analysis for
codetools::checkUsage(), lintr and the static checks thatR CMD checkalso runs. -
Packages, Libraries and Namespaces for
DependsagainstImports, repositories,options(repos)and installing from CRAN, Bioconductor and r-universe. -
Reproducible Environments with renv for locking the versions of the packages a project uses once they are on CRAN.
-
Interfacing C, C++ and Fortran for packages with compiled code, which add portability checks and the C23 and C++20 defaults noted above.
-
Scripts and the Command Line for running
RscriptandR CMDcommands from scripts and from continuous integration.
References
-
CRAN Repository Policy: Source packages, Submission, Re-submission
-
Writing R Extensions: 1.3.1 Checking packages, 1.3.2 Building package tarballs, 1.6 Writing portable packages, The DESCRIPTION file, Package Dependencies
-
R NEWS: Changes in R 4.0.0 to 4.6.1
-
devtools package documentation: check, check_win, check_mac_release, submit_cran, release
-
usethis package documentation: use_cran_comments, use_version, use_release_issue, use_github_action
-
r-universe documentation: Set up your own universe, Using dependencies from R-universe
Community material named in the text and not listed above as documentation: the R Packages book by Hadley Wickham and Jennifer Bryan (chapter "Releasing to CRAN"), the package available, the package lifecycle and the package cranlogs.