= An Introduction to *packager* :Author: Andreas Dominik Cullmann :Date: 2021-12-08, 13:51:51 :toc2: //// %\VignetteIndexEntry{An Introduction to packager } %\VignetteEngine{rasciidoc::rasciidoc} %\VignetteEncoding{UTF-8} //// == Teaser +packager+ is a set of functions I use to create and maintain most of my +R+-packages using a build process such as https://CRAN.R-project.org/package=fakemake[+fakemake+] or, https://www.gnu.org/software/make/[GNU make]. It borrows heavily from packages +devtools+, +usethis+, +rcmdcheck+, +remotes+ and +lintr+. == +withr+ Due to https://cran.r-project.org/web/packages/policies.html[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 https://cran.r-project.org/[CRAN]. In real life, we would skip the temporary directory stuff. == Creating Packages To create a new package I use: //begin.rcode, results = "hide", message = FALSE path <- file.path(tempdir(), "myFirstPackage") packager::create(path, fakemake = "check") //end.rcode The package is built, tested, checked and committed into git: //begin.rcode list.files(path, recursive = FALSE) gert::git_status(repo = path) gert::git_log(repo = path) //end.rcode We can look at some of the files (the directory +myFirstPackage.Rcheck+ might be of interest): //begin.rcode cat(readLines(file.path(path, "log", "spell.Rout")), sep = "\n") tail(readLines(file.path(path, "log", "check.Rout")), sep = "\n") //end.rcode And we see what is left to do: //begin.rcode cat(readLines(file.path(path, "TODO.md")), sep = "\n") //end.rcode === Customizing We see that the package's DESCRIPTION is filled with default values. //begin.rcode cat(readLines(file.path(path, "DESCRIPTION")), sep = "\n") //end.rcode 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 //begin.rcode unlink(path, recursive = TRUE) if ("myFirstPackage" %in% .packages()) detach("package:myFirstPackage", unload = TRUE) //end.rcode 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): //begin.rcode, results = "hide", message = FALSE package_title <- "myOtherPackage" 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") //end.rcode //begin.rcode cat(readLines(file.path(path, "DESCRIPTION")), sep = "\n") //end.rcode The package's man page is set up accordingly: //begin.rcode, eval = FALSE pkgload::load_all(path) help(paste0(package_title, "-package")) //end.rcode //begin.rcode, echo = FALSE pkgload::load_all(path) # insert developement page help_file <- system.file("man", paste0(package_title, "-package.Rd"), package = devtools::as.package(path)$package) captured <- gsub('_\b', '', capture.output(tools:::Rd2txt(help_file) )) cat(captured, sep = "\n") //end.rcode I use //begin.rcode, eval = FALSE 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) //end.rcode in one of my startup files to set the author information globally. == Maintaining Packages Using https://CRAN.R-project.org/package=fakemake[+fakemake+] Our brand new package +r devtools::as.package(path)[["package"]]+ is checked into git already: //begin.rcode gert::git_status(repo = path) gert::git_log(repo = path) //end.rcode but we have so far only built the documentation from the +roxygen+ comments: //begin.rcode list.files(file.path(path, "log")) //end.rcode So we get a +makelist+ and look at its targets and aliases: //begin.rcode ml <- packager::get_package_makelist(is_cran = TRUE) cbind(lapply(ml, function(x) x[["target"]]), lapply(ml, function(x) x[["alias"]])) //end.rcode === Building the Package We choose to build the package: //begin.rcode suppressMessages(withr::with_dir(path, print(fakemake::make("build", ml, verbose = FALSE)))) //end.rcode We see that now there are untracked files in our package's log directory and that some files changed. //begin.rcode gert::git_status(repo = path) packager::git_diff(x = ".Rbuildignore", path = path) //end.rcode After inspecting the change, we commit: //begin.rcode withr::with_dir(path, packager::git_add_commit(path = ".", untracked = TRUE, message = "make build")) gert::git_status(repo = path) //end.rcode === Checking the Package So now we want the check the package: //begin.rcode suppressMessages(withr::with_dir(path, print(fakemake::make("check", ml, verbose = FALSE)))) //end.rcode We again see new files and changes to old files. //begin.rcode gert::git_status(repo = path) //end.rcode 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: //begin.rcode cat(tail(readLines(file.path(path, "log", "check.Rout")), n = 7), sep = "\n") check_log <- file.path(path, "log", "check.Rout") status <- packager::get_check_status(check_log) RUnit::checkEqualsNumeric(status[["status"]][["errors"]], 0) //end.rcode and commit again //begin.rcode withr::with_dir(path, packager::git_add_commit(path = ".", untracked = TRUE, message = "make check")) //end.rcode 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: //begin.rcode system.time(withr::with_dir(path, print(fakemake::make("check", ml, verbose = FALSE)))) //end.rcode 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 https://cran.r-project.org/[CRAN] (which we will not do here, but we want to!). // We provide comments to https://cran.r-project.org/[CRAN]: // // //begin.rcode // withr::with_dir(path, print(fakemake::make("cran_comments", ml, verbose = FALSE))) // cat(readLines(file.path(path, "cran-comments.md")), sep = "\n") // //end.rcode // After editing the contents we feel ready to submit: // // // //begin.rcode // try(packager::submit(path)) // //end.rcode // // Oops: we need to commit git first: // // //begin.rcode // packager::git_add_commit(path = path, untracked = TRUE, // message = "prepare for CRAN") // gert::git_status(repo = path) // // //end.rcode // // Now we try and fail again, because this vignette is built in batch mode // and there's a security query which then fails: We try and fail, because this vignette is built in batch mode and there's a security query: //begin.rcode try(packager::submit(path)) //end.rcode 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 https://cran.r-project.org/doc/manuals/R-exts.html[how to write R extensions] and https://cran.r-project.org/web/packages/policies.html[the CRAN policies]. Anyway, we might want to tag the current commit and commence developing our package: //begin.rcode packager::git_tag(path = path, message = "A Tag") packager::use_dev_version(path = path) desc::desc_get("Version", file = path) cat(readLines(file.path(path, "NEWS.md")), sep = "\n") //end.rcode //begin.rcode, echo = FALSE # remove the package unlink(path, recursive = TRUE) //end.rcode This is close to the workflow I have been using for most of my packages, substituting https://CRAN.R-project.org/package=fakemake[+fakemake+] with https://www.gnu.org/software/make/[GNU make] whenever possible.