Building R Packages Using fakemake

Andreas Dominik Cullmann

2020-02-23, 16:23:49

We will now look at fakemake’s main purpose: building packages.

There’s a more elaborated vignette coming with package packager. Please see packager’s vignette.

Creating the Package

First, we need to create a sample package, so we create a package skeleton:

pkg_path <- file.path(tempdir(), "fakepack")
unlink(pkg_path, force = TRUE, recursive = TRUE)
usethis::create_package(pkg_path)

And add a minimal R code file:

file.copy(system.file("templates", "throw.R", package = "fakemake"),
          file.path(pkg_path, "R"))
## [1] TRUE

This package does not make any sense. It is just a minimal working example (in the sense that it passes R CMD build and a simple R CMD check). It does not provide any functionality apart from a single internal function that is not exported via the package’s NAMESPACE. It is just there to exemplify the usage of fakemake.

Setting Up the Makelist

Then we get a package makelist from fakemake:

ml <- fakemake::provide_make_list("vignette")
## Warning: 'package_makelist' is deprecated.
## Use 'packager::get_package_makelist' instead.
## See help("Deprecated")

This list is a bit more complex than the minimal example above, so we visualize it:

withr::with_dir(pkg_path, fakemake::visualize(ml))
## Warning: 'get_pkg_archive_path' is deprecated.
## Use 'packager::get_pkg_archive_path' instead.
## See help("Deprecated")

## Warning: 'get_pkg_archive_path' is deprecated.
## Use 'packager::get_pkg_archive_path' instead.
## See help("Deprecated")

Obviously the tarball depends on many files and the only target that’s no other target’s prerequisite is “log/check.Rout”. If you are more into hierarchical depictions, you can use the terminal target as root:

withr::with_dir(pkg_path, fakemake::visualize(ml, root = "log/check.Rout"))
## Warning: 'get_pkg_archive_path' is deprecated.
## Use 'packager::get_pkg_archive_path' instead.
## See help("Deprecated")

## Warning: 'get_pkg_archive_path' is deprecated.
## Use 'packager::get_pkg_archive_path' instead.
## See help("Deprecated")

But then you might be interested in this python program, it would leave with this graph: makefile2graph output

I regularly use it to visualize complex Makefiles.

More on Target Rules

Let’s take a look at the target rule that builds the tarball:

index <- which(sapply(ml, function(x) x["alias"] == "build"))
ml[[index]]
## $alias
## [1] "build"
## 
## $target
## [1] "get_pkg_archive_path(absolute = FALSE)"
## 
## $code
## [1] "print(pkgbuild::build(path = \".\", dest_path = \".\", vignettes = FALSE))"
## 
## $sink
## [1] "log/build.Rout"
## 
## $prerequisites
## [1] ".log.Rout"                                               
## [2] "list.files(\"R\", full.names = TRUE, recursive = TRUE)"  
## [3] "list.files(\"man\", full.names = TRUE, recursive = TRUE)"
## [4] "DESCRIPTION"                                             
## [5] "file.path(\"log\", \"lintr.Rout\")"                      
## [6] "file.path(\"log\", \"cleanr.Rout\")"                     
## [7] "file.path(\"log\", \"spell.Rout\")"                      
## [8] "file.path(\"log\", \"covr.Rout\")"                       
## [9] "file.path(\"log\", \"roxygen2.Rout\")"

Note that some of its items are strings giving file names, some are strings that parse as R expressions, and prerequisites is a mix of both. Obviously, fakemake parses and evaluates these character strings dynamically.

Let us take a look at the prerequisites for roxygen2:

index <- which(sapply(ml, function(x) x["alias"] == "roxygen2"))
ml[[index]][["prerequisites"]]
## [1] ".log.Rout"                                             
## [2] "list.files(\"R\", full.names = TRUE, recursive = TRUE)"

Building the Package

Now we build and check the package in one go:

withr::with_dir(pkg_path, print(fakemake::make("check", ml)))
## [1] ".log.Rout"                  "log/cleanr.Rout"           
## [3] "log/covr.Rout"              "log/lintr.Rout"            
## [5] "log/roxygen2.Rout"          "log/spell.Rout"            
## [7] "fakepack_0.0.0.9000.tar.gz" "log/check.Rout"

We should now detach the package to prevent the coverage target to crash in subsequent calls.

if ("fakepack" %in% .packages()) detach("package:fakepack", unload = TRUE)

We see the files created in the log directory correspond to the names given by make:

list.files(file.path(pkg_path, "log"))
## [1] "build.Rout"    "check.Rout"    "cleanr.Rout"   "covr.Rout"    
## [5] "lintr.Rout"    "roxygen2.Rout" "spell.Rout"

and we can take a look at one:

cat(readLines(file.path(pkg_path, "log", "check.Rout")), sep = "\n")
## Warning: 'check_archive_as_cran' is deprecated.
## Use 'packager::check_archive_as_cran' instead.
## See help("Deprecated")
## Warning: 'check_archive' is deprecated.
## Use 'packager::check_archive' instead.
## See help("Deprecated")
## Warning: 'get_pkg_archive_path' is deprecated.
## Use 'packager::get_pkg_archive_path' instead.
## See help("Deprecated")
## * using log directory ‘/tmp/RtmpLYHpPG/fakepack/fakepack.Rcheck’
## * using R version 3.3.3 (2017-03-06)
## * using platform: x86_64-pc-linux-gnu (64-bit)
## * using session charset: UTF-8
## * using option ‘--as-cran’
## * checking for file ‘fakepack/DESCRIPTION’ ... OK
## * this is package ‘fakepack’ version ‘0.0.0.9000’
## * package encoding: UTF-8
## * checking CRAN incoming feasibility ... NOTE
## Maintainer: ‘First Last <first.last@example.com>’
## 
## New submission
## 
## Version contains large components (0.0.0.9000)
## 
## Non-FOSS package license (What license it uses)
## * 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 ... NOTE
## Found the following hidden files and directories:
##   .log.Rout
## These were most likely included in error. See section ‘Package
## structure’ in the ‘Writing R Extensions’ manual.
## 
## CRAN-pack does not know about
##   .log.Rout
## * checking for portable file names ... OK
## * checking for sufficient/correct file permissions ... OK
## * checking whether package ‘fakepack’ can be installed ... OK
## * checking installed package size ... OK
## * checking package directory ... OK
## * checking DESCRIPTION meta-information ... WARNING
## Non-standard license specification:
##   What license it uses
## Standardizable: FALSE
## * checking top-level files ... OK
## * checking for left-over files ... OK
## * checking index information ... OK
## * checking package subdirectories ... OK
## * checking R 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 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 examples ... OK
## * checking PDF version of manual ... OK
## * DONE
## Status: 1 WARNING, 2 NOTEs
## 
## See
##   ‘/tmp/RtmpLYHpPG/fakepack/fakepack.Rcheck/00check.log’
## for details.

Rebuilding the package does not do anything (NULL is returned instead of the names of targets above), you save quite some CPU time compared to unconditionally rerunning the codes in the makelist:

system.time(suppressMessages(withr::with_dir(pkg_path,
                                             print(fakemake::make("check",
                                                                  ml)))))
## Warning: 'get_pkg_archive_path' is deprecated.
## Use 'packager::get_pkg_archive_path' instead.
## See help("Deprecated")

## Warning: 'get_pkg_archive_path' is deprecated.
## Use 'packager::get_pkg_archive_path' instead.
## See help("Deprecated")
## NULL
##    user  system elapsed 
##   0.124   0.004   0.130

Changing Files and Rebuilding the Package

Let us take a look at our testing coverage:

cat(readLines(file.path(pkg_path, "log", "covr.Rout")), sep = "\n")
##    filename functions line value
## 1 R/throw.R     throw   18     0
## 2 R/throw.R     throw   19     0
## 3 R/throw.R     throw   20     0
## 4 R/throw.R     throw   21     0
## fakepack Coverage: 0.00%
## R/throw.R: 0.00%

Well, poor. So we add a test file:

dir.create(file.path(pkg_path, "tests", "testthat"), recursive = TRUE)
file.copy(system.file("templates", "testthat.R", package = "fakemake"),
          file.path(pkg_path, "tests"))
## [1] TRUE
file.copy(system.file("templates", "test-throw.R", package = "fakemake"),
          file.path(pkg_path, "tests", "testthat"))
## [1] TRUE
withr::with_dir(pkg_path, fakemake::visualize(ml))
## Warning: 'get_pkg_archive_path' is deprecated.
## Use 'packager::get_pkg_archive_path' instead.
## See help("Deprecated")

## Warning: 'get_pkg_archive_path' is deprecated.
## Use 'packager::get_pkg_archive_path' instead.
## See help("Deprecated")

Now we re-build the package’s tarball again (of course we could make("check", ml) again, but for the sake of (CRAN’s) CPU time, I skip the check):

withr::with_dir(pkg_path, print(fakemake::make("build", ml)))
## [1] "log/cleanr.Rout"            "log/covr.Rout"             
## [3] "log/lintr.Rout"             "fakepack_0.0.0.9000.tar.gz"

We see that most of the build chain is rerun, except roxygenising, since the files under “tests/” are not prerequisites to log/royxgen2.Rout. Ah, and the test coverage is improved:

cat(readLines(file.path(pkg_path, "log", "covr.Rout")), sep = "\n")
## [1] filename  functions line      value    
## <0 rows> (or 0-length row.names)
## fakepack Coverage: 100.00%
## R/throw.R: 100.00%