Teaser

packager is a set of functions I use to create and maintain most of my R-packages using a build process such as fakemake or, GNU make. It borrows heavily from packages devtools, usethis, rcmdcheck, remotes and lintr.

withr

Due to the CRAN policy of not writing "anywhere else on the file system apart from the R session’s temporary directory", throughout this vignette I use R's temporary directory, often by using path ← file.path(tempdir(), "my_path") followed by withr::with_dir(path, …) or the like. I do this because this is a vignette and its codes are run on CRAN.

In real life, we would skip the temporary directory stuff.

Creating Packages

To create a new package I use:

path <- file.path(tempdir(), "myFirstPackage")
packager::create(path, fakemake = "check")

The package is built, tested, checked and committed into git:

list.files(path, recursive = FALSE)
##  [1] "DESCRIPTION"                 "LICENSE"
##  [3] "Makefile"                    "Meta"
##  [5] "NAMESPACE"                   "NEWS.md"
##  [7] "R"                           "README.Rmd"
##  [9] "README.md"                   "TODO.md"
## [11] "devel.R"                     "doc"
## [13] "inst"                        "log"
## [15] "make.R"                      "man"
## [17] "man-roxygen"                 "myFirstPackage.Rcheck"
## [19] "myFirstPackage_0.1.0.tar.gz" "tests"
## [21] "vignettes"
gert::git_status(repo = path)
## # A tibble: 0 x 3
## # … with 3 variables: file <chr>, status <chr>, staged <lgl>
gert::git_log(repo = path)
## # A tibble: 17 x 6
##    commit        author     time                files merge message
##  * <chr>         <chr>      <dttm>              <int> <lgl> <chr>
##  1 dd8734a864aa… fvafrcu <… 2021-01-29 12:23:05    28 FALSE "Packager Changes\n"
##  2 ec7874693959… fvafrcu <… 2021-01-29 12:21:54     2 FALSE "Adding `man-roxyge…
##  3 10c64734d584… fvafrcu <… 2021-01-29 12:21:54     2 FALSE "Adding `make.R` fr…
##  4 aee45a777f5b… fvafrcu <… 2021-01-29 12:21:54     1 FALSE "Adding `inst/runit…
##  5 6fbff01ca8cf… fvafrcu <… 2021-01-29 12:21:54     1 FALSE "Adding `tests/runi…
##  6 4017320ff8e3… fvafrcu <… 2021-01-29 12:21:54     1 FALSE "Adding `tests/test…
##  7 5d6392ee6493… fvafrcu <… 2021-01-29 12:21:54     1 FALSE "Adding `R/throw.R`…
##  8 abc72e5ff177… fvafrcu <… 2021-01-29 12:21:53     2 FALSE "Adding `devel.R` f…
##  9 5ea0ef960da9… fvafrcu <… 2021-01-29 12:21:53     2 FALSE "Adding `.gitlab-ci…
## 10 f5cd5026a571… fvafrcu <… 2021-01-29 12:21:53     2 FALSE "Adding `.gitlab-ci…
## 11 7b067363ff11… fvafrcu <… 2021-01-29 12:21:53     2 FALSE "Adding `.travis.ym…
## 12 ca1400691650… fvafrcu <… 2021-01-29 12:21:53     2 FALSE "Adding `README.Rmd…
## 13 32693ac66439… fvafrcu <… 2021-01-29 12:21:53     1 FALSE "Adding `NEWS.md` f…
## 14 95ac7771776c… fvafrcu <… 2021-01-29 12:21:53     1 FALSE "Adding `vignettes/…
## 15 29328e701293… fvafrcu <… 2021-01-29 12:21:53     2 FALSE "Adding `Makefile` …
## 16 08169ae99d17… fvafrcu <… 2021-01-29 12:21:53     1 FALSE "Clean DESCRIPTION\…
## 17 ba7f53339b1b… fvafrcu <… 2021-01-29 12:21:53     2 FALSE "Initial Commit\n"

We can look at some of the files (the directory myFirstPackage.Rcheck might be of interest):

cat(readLines(file.path(path, "log", "spell.Rout")), sep = "\n")
## DESCRIPTION does not contain 'Language' field. Defaulting to 'en-US'.
##   WORD      FOUND IN
## RStudio   README.Rmd:8,9
##           README.md:8,9
## gitlab    README.Rmd:43
##           README.md:43
## Spell check failed, see /tmp/Rtmph8NLyT/myFirstPackage/log/spell.Rout for details.
tail(readLines(file.path(path, "log", "check.Rout")), sep = "\n")
## [1] ""
## [2] "See"
## [3] "  ‘/tmp/Rtmph8NLyT/myFirstPackage/myFirstPackage.Rcheck/00check.log’"
## [4] "for details."
## [5] ""
## [6] ""

And we see what is left to do:

cat(readLines(file.path(path, "TODO.md")), sep = "\n")
## - make sure https://gitlab.com/fvafrcu/myFirstPackage exists!

Customizing

We see that the package’s DESCRIPTION is filled with default values.

cat(readLines(file.path(path, "DESCRIPTION")), sep = "\n")
## Package: myFirstPackage
## Title: What it Does (One Line, Title Case)
## Version: 0.1.0
## Authors@R:
##     person(given = "Andreas Dominik",
##            family = "Cullmann",
##            role = c("aut", "cre"),
##            email = "fvafrcu@mailbox.org")
## Description: What the package does (one paragraph).
## License: BSD_2_clause + file LICENSE
## URL: https://gitlab.com/fvafrcu/myFirstPackage
## Depends:
##     R (>= 4.1.0)
## Suggests:
##     knitr,
##     rmarkdown,
##     pkgload,
##     testthat (>= 3.0.0),
##     RUnit,
##     devtools,
##     rprojroot
## VignetteBuilder:
##     knitr
## Encoding: UTF-8
## LazyData: true
## Roxygen: list(markdown = TRUE)
## RoxygenNote: 7.1.1
## Config/testthat/edition: 3

We could set the package information on the existing package, but we rather create a new one now. So we get rid of our first package

unlink(path, recursive = TRUE)
if ("myFirstPackage" %in% .packages()) detach("package:myFirstPackage",
                                              unload = TRUE)

and customize the package creation (but we skip the process of testing, building and checking for the sake of CPU time, we just build the docs):

package_title <- "veryImportantPackage"
path <- file.path(tempdir(), package_title)
a  <- utils::person(given = "Your", family = "Name", email = "some@whe.re",
                    role = c("aut", "cre"))
packager::create(path, author_at_r = a, title = package_title,
                 description = "This is very important.",
                 details = "At least to me.", fakemake = "roxygen2")
cat(readLines(file.path(path, "DESCRIPTION")), sep = "\n")
## Package: veryImportantPackage
## Title: veryImportantPackage
## Version: 0.1.0
## Authors@R:
##     person(given = "Your",
##            family = "Name",
##            role = c("aut", "cre"),
##            email = "some@whe.re")
## Description: This is very important.
## License: BSD_2_clause + file LICENSE
## URL: https://gitlab.com/fvafrcu/veryImportantPackage
## Depends:
##     R (>= 4.1.0)
## Suggests:
##     knitr,
##     rmarkdown,
##     pkgload,
##     testthat (>= 3.0.0),
##     RUnit,
##     devtools,
##     rprojroot
## VignetteBuilder:
##     knitr
## Encoding: UTF-8
## LazyData: true
## Roxygen: list(markdown = TRUE)
## RoxygenNote: 7.1.1
## Config/testthat/edition: 3

The package’s man page is set up accordingly:

pkgload::load_all(path)
help(paste0(package_title, "-package"))
## Loading veryImportantPackage
## veryImportantPackage
##
## Description:
##
##      This is very important.
##
## Details:
##
##      You will find the details in
##      'vignette("An_Introduction_to_veryImportantPackage", package =
##      "veryImportantPackage")'.

I use

adc <- utils::person(given = "Andreas Dominik",
                      family = "Cullmann",
                      email = "fvafrcu@mailbox.org",
                      role = c("aut", "cre"))
pop <- as.list(getOption("packager"))
pop[["whoami"]] <- adc
options(packager = pop)

in one of my startup files to set the author information globally.

Maintaining Packages Using fakemake

Our brand new package veryImportantPackage is checked into git already:

gert::git_status(repo = path)
## # A tibble: 0 x 3
## # … with 3 variables: file <chr>, status <chr>, staged <lgl>
gert::git_log(repo = path)
## # A tibble: 17 x 6
##    commit        author     time                files merge message
##  * <chr>         <chr>      <dttm>              <int> <lgl> <chr>
##  1 8adec18119ac… fvafrcu <… 2021-01-29 12:23:07    12 FALSE "Packager Changes\n"
##  2 e3c3e889f4ae… fvafrcu <… 2021-01-29 12:23:06     2 FALSE "Adding `man-roxyge…
##  3 b09e2efd7e3a… fvafrcu <… 2021-01-29 12:23:06     2 FALSE "Adding `make.R` fr…
##  4 50bb566cdd85… fvafrcu <… 2021-01-29 12:23:06     1 FALSE "Adding `inst/runit…
##  5 240b4e3788d9… fvafrcu <… 2021-01-29 12:23:06     1 FALSE "Adding `tests/runi…
##  6 abf04ed8129c… fvafrcu <… 2021-01-29 12:23:06     1 FALSE "Adding `tests/test…
##  7 a070059bd63a… fvafrcu <… 2021-01-29 12:23:06     1 FALSE "Adding `R/throw.R`…
##  8 a7c6c4a25fb1… fvafrcu <… 2021-01-29 12:23:06     2 FALSE "Adding `devel.R` f…
##  9 526c456eddcc… fvafrcu <… 2021-01-29 12:23:06     2 FALSE "Adding `.gitlab-ci…
## 10 c8c8528151bf… fvafrcu <… 2021-01-29 12:23:06     2 FALSE "Adding `.gitlab-ci…
## 11 a9648d672e1d… fvafrcu <… 2021-01-29 12:23:06     2 FALSE "Adding `.travis.ym…
## 12 78deb40e0545… fvafrcu <… 2021-01-29 12:23:06     2 FALSE "Adding `README.Rmd…
## 13 7029951824ab… fvafrcu <… 2021-01-29 12:23:06     1 FALSE "Adding `NEWS.md` f…
## 14 49008d9464f7… fvafrcu <… 2021-01-29 12:23:06     1 FALSE "Adding `vignettes/…
## 15 20c1785f1ef3… fvafrcu <… 2021-01-29 12:23:05     2 FALSE "Adding `Makefile` …
## 16 53c7a03819c4… fvafrcu <… 2021-01-29 12:23:05     1 FALSE "Clean DESCRIPTION\…
## 17 d0022a3058aa… fvafrcu <… 2021-01-29 12:23:05     2 FALSE "Initial Commit\n"

but we have so far only built the documentation from the roxygen comments:

list.files(file.path(path, "log"))
## [1] "dependencies.Rout" "roxygen2.Rout"

So we get a makelist and look at its targets and aliases:

ml <- packager::get_package_makelist(is_cran = TRUE)
cbind(lapply(ml, function(x) x[["target"]]),
      lapply(ml, function(x) x[["alias"]]))
##       [,1]                                               [,2]
##  [1,] "log/roxygen2.Rout"                                "roxygen2"
##  [2,] "log/dependencies.Rout"                            "dependencies"
##  [3,] "log/spell.Rout"                                   "spell"
##  [4,] "log/cleanr.Rout"                                  "cleanr"
##  [5,] "log/lintr.Rout"                                   "lint"
##  [6,] "log/covr.Rout"                                    "covr"
##  [7,] "packager::get_pkg_archive_path(absolute = FALSE)" "build"
##  [8,] ".log.Rout"                                        ".log"
##  [9,] "README.md"                                        "README.md"
## [10,] "log/testthat.Rout"                                "testthat"
## [11,] "log/vignettes.Rout"                               "vignettes"
## [12,] "log/check_codetags.Rout"                          "check_codetags"
## [13,] "log/news_rd.Rout"                                 "news_rd"
## [14,] "log/news.Rout"                                    "news"
## [15,] "log/usage.Rout"                                   "usage"
## [16,] "log/winbuilder.Rout"                              "winbuilder"
## [17,] "log/check.Rout"                                   "check"
## [18,] "log/install.Rout"                                 "install"
## [19,] "cran-comments.md"                                 "cran_comments"
## [20,] "log/submit.Rout"                                  "submit"

Building the Package

We choose to build the package:

suppressMessages(withr::with_dir(path,
                                  print(fakemake::make("build", ml,
                                                       verbose = FALSE))))
##  [1] "README.md"                         "log/check_codetags.Rout"
##  [3] "log/cleanr.Rout"                   "log/covr.Rout"
##  [5] "log/lintr.Rout"                    "log/news.Rout"
##  [7] "log/news_rd.Rout"                  "log/spell.Rout"
##  [9] "log/testthat.Rout"                 "log/usage.Rout"
## [11] "log/vignettes.Rout"                "veryImportantPackage_0.1.0.tar.gz"

We see that now there are untracked files in our package’s log directory and that some files changed.

gert::git_status(repo = path)
## # A tibble: 17 x 3
##    file                    status   staged
##    <chr>                   <chr>    <lgl>
##  1 .Rbuildignore           modified FALSE
##  2 .gitignore              modified FALSE
##  3 README.md               new      FALSE
##  4 inst/NEWS.rd            new      FALSE
##  5 inst/vignettes_code/    new      FALSE
##  6 log/build.Rout          new      FALSE
##  7 log/check_codetags.Rout new      FALSE
##  8 log/cleanr.Rout         new      FALSE
##  9 log/covr.Rout           new      FALSE
## 10 log/lintr.Rout          new      FALSE
## 11 log/news.Rout           new      FALSE
## 12 log/news_rd.Rout        new      FALSE
## 13 log/readme.Rout         new      FALSE
## 14 log/spell.Rout          new      FALSE
## 15 log/testthat.Rout       new      FALSE
## 16 log/usage.Rout          new      FALSE
## 17 log/vignettes.Rout      new      FALSE
git_diff(x = ".Rbuildignore", path = path)
## Error in git_diff(x = ".Rbuildignore", path = path): unused arguments (x = ".Rbuildignore", path = path)

After inspecting the change, we commit:

withr::with_dir(path, packager::git_add_commit(path = ".", untracked = TRUE,
                                               message = "make build"))
## [1] "20d7cfd45657c20f7afbadc38b0d93aa31fde7a3"
gert::git_status(repo = path)
## # A tibble: 0 x 3
## # … with 3 variables: file <chr>, status <chr>, staged <lgl>

Checking the Package

So now we want the check the package:

suppressMessages(withr::with_dir(path,
                                 print(fakemake::make("check", ml,
                                                      verbose = FALSE))))
## [1] "log/cleanr.Rout"                   "log/covr.Rout"
## [3] "log/lintr.Rout"                    "log/testthat.Rout"
## [5] "veryImportantPackage_0.1.0.tar.gz" "log/check.Rout"

We again see new files and changes to old files.

gert::git_status(repo = path)
## # A tibble: 4 x 3
##   file              status   staged
##   <chr>             <chr>    <lgl>
## 1 log/build.Rout    modified FALSE
## 2 log/check.Rout    new      FALSE
## 3 log/lintr.Rout    modified FALSE
## 4 log/testthat.Rout modified FALSE

Note that the RUnit test files are run while checking the tarball, hence we see output from RUnit in our log directory.

We assume that we passed the check:

cat(tail(readLines(file.path(path, "log", "check.Rout")), n = 7), sep = "\n")
##
## Status: 1 NOTE
## See
##   ‘/tmp/Rtmph8NLyT/veryImportantPackage/veryImportantPackage.Rcheck/00check.log’
## for details.
check_log <- file.path(path, "log", "check.Rout")
status <- packager::get_check_status(check_log)
RUnit::checkEqualsNumeric(status[["status"]][["errors"]], 0)
## [1] TRUE

and commit again

withr::with_dir(path, packager::git_add_commit(path = ".", untracked = TRUE,
                                               message = "make check"))
## [1] "3774b56851aac29e0c138c8833f8627215ccb0f1"

If we choose to rerun the check without touching any files "down the make chain" (i.e. no files that any of our make targets depend on), we see there’s nothing to be done:

system.time(withr::with_dir(path, print(fakemake::make("check", ml, verbose = FALSE))))
## NULL
##    user  system elapsed
##   0.902   0.000   0.930

This is the big difference between running the check via fakemake with a set of dependencies (set up with packager) and running the check (be it using R CMD check or rcmdcheck::rcmdcheck or its wrapper devtools::check) unconditionally: the latter method rebuilds and checks the whole package every time. This is why I wrote packager and fakemake.

Submitting the Package

Now we would like to submit our package to CRAN (which we will not do here, but we want to!) We provide comments to CRAN:

withr::with_dir(path, print(fakemake::make("cran_comments", ml, verbose = FALSE)))
## R CMD check results
## 0 errors | 0 warnings | 1 note
## [1] "log/install.Rout" "cran-comments.md"
cat(readLines(file.path(path, "cran-comments.md")), sep = "\n")
## Make target auto-generated by fakemake.

After editing the contents we feel ready to submit:

try(packager::submit(path))
## Warning in packager::submit(path): You have no upstream!
## Ready to submit?Error in utils::menu(qs[rand]) : menu() cannot be used non-interactively

Oops: we need to commit git first:

packager::git_add_commit(path = path, untracked = TRUE,
                         message = "prepare for CRAN")
## [1] "f5444d113467299304c7c131131af307f2bdcfb3"
gert::git_status(repo = path)
## # A tibble: 0 x 3
## # … with 3 variables: file <chr>, status <chr>, staged <lgl>

Now we try and fail again, because this vignette is built in batch mode and there’s a security query which then fails:

try(packager::submit(path))
## Warning in packager::submit(path): You have no upstream!
## Ready to submit?Error in utils::menu(qs[rand]) : menu() cannot be used non-interactively

Should you run this code interactively, you will be prompted for the security query (as you might be used from devtools::release()). Best you know how to write R extensions and the CRAN policies.

Anyway, we might want to tag the current commit and commence developing our package:

packager::git_tag(path = path, message = "A Tag")
## h6 unix
## # A tibble: 1 x 3
##   name  ref             commit
##   <chr> <chr>           <chr>
## 1 0.1.0 refs/tags/0.1.0 53f9c227bc71beaade7fdbd4e794cd481f4388ad
packager::use_dev_version(path = path)
## Package version bumped from '0.1.0' to '0.1.0.9000'
## [1] "caf2e601904a35ddab6d2c134ef2f6f3beb3e593"
desc::desc_get("Version", file = path)
##      Version
## "0.1.0.9000"
cat(readLines(file.path(path, "NEWS.md")), sep = "\n")
## # veryImportantPackage 0.1.0.9000
##
## * FIXME
##
## # veryImportantPackage 0.1.0
##
## * Added a `NEWS.md` file to track changes to the package.

This is close to the workflow I have been using for most of my packages, substituting fakemake with GNU make whenever possible.