5 Not overwriting a manually edited R package
As described in the previous section, the function setup()
will only overwrite a directory if it is the unedited output from using litr
. The basic idea is that the function litr::render()
when creating a new package finishes by adding a hash to the DESCRIPTION file. (And likewise when rmarkdown::render()
is used with a litr
output format, as described here.) This hash is a function of everything in the package, so if anything about the package changes (any file is modified, added, or removed) then the function check_unedited()
will be able to detect that by recomputing the hash and seeing that it doesn’t match the hash in the DESCRIPTION file.
Let’s start by defining the function hash_package_directory()
that does the hashing. The hash is a function of everything in the outputted package except for that special line in the DESCRIPTION file with the hash. We use tools::md5sum()
and digest::digest()
to do the hashing.
#' Hash package directory
#'
#' Gets an identifier that can be used to uniquely (whp) identify the current
#' state of the package. This is formed by ignoring the `LitrId` field of the
#' DESCRIPTION file, which is the location where the output of this function is
#' stored when `litr::render` generates the package.
#'
#' @param package_dir Path to package
#' @keywords internal
hash_package_directory <- function(package_dir) {
pkg_files <- fs::dir_ls(package_dir, recurse = TRUE, all = TRUE, type = "file")
pkg_files <- stringr::str_subset(pkg_files, ".DS_Store$", negate = TRUE)
pkg_files <- normalizePath(pkg_files)
descr_file <- normalizePath(file.path(package_dir, "DESCRIPTION"))
i_descr <- which(pkg_files == descr_file)
if (length(i_descr) == 0) stop("Cannot find DESCRIPTION file.")
txt_descr <- readLines(pkg_files[i_descr])
txt_descr_mod <- stringr::str_subset(
txt_descr,
stringr::str_glue("{description_litr_hash_field_name()}: .+$"),
negate = TRUE)
hashes <- as.character(tools::md5sum(pkg_files[-i_descr]))
digest::digest(c(hashes, list(txt_descr_mod)))
}
We used digest
, so let’s import it:
## ✔ Adding 'digest' to Imports field in DESCRIPTION
## • Refer to functions with `digest::fun()`
We will store this hash in a special field within the DESCRIPTION file. Let’s call this field LitrId
. However, in case we ever decide to change the name of this field, it’s better that we only define it in one place. So we do this with the following function:
#' Generate litr hash field name for DESCRIPTION file
#' @keywords internal
description_litr_hash_field_name <- function() return("LitrId")
Ok, now let’s write the function that litr::render()
will call that will take the generated R package and add a line that puts the hash in the DESCRIPTION file under that special litr
field:
#' Write the hash of the package to the DESCRIPTION file
#'
#' @param package_dir Path to package
#' @keywords internal
write_hash_to_description <- function(package_dir) {
desc_file <- file.path(package_dir, "DESCRIPTION")
if (!file.exists(desc_file)) file.create(desc_file)
hash <- hash_package_directory(package_dir)
desc::desc_set(description_litr_hash_field_name(), hash, file = desc_file)
}
Let’s include the desc
package, which helps us manipulate DESCRIPTION files.
## ✔ Adding 'desc' to Imports field in DESCRIPTION
## • Refer to functions with `desc::fun()`
And of course we’ll need a function that can read the value of that field as well:
#' Get the hash of the package from the DESCRIPTION file
#'
#' @param package_dir Path to package
#' @keywords internal
read_hash_from_description <- function(package_dir) {
descr <- file.path(package_dir, "DESCRIPTION")
if (!file.exists(descr)) stop("Cannot find DESCRIPTION file.")
txt <- stringr::str_subset(
readLines(descr),
stringr::str_glue("{description_litr_hash_field_name()}: .+$"))
if (length(txt) > 1) stop("More than one hash found in DESCRIPTION.")
if (length(txt) == 0) stop("No hash found in DESCRIPTION.")
stringr::str_extract(txt, "\\S+$")
}
With all this hash functionality in place, the function check_unedited()
is actually quite simple to define:
#' Check if package directory is the unedited output of litr::render()
#'
#' Uses hash stored in a special `litr` field of DESCRIPTION file to check that
#' the current state of the R package directory is identical to its state at the
#' time that it was created by `litr::render()`.
#'
#' @param package_dir Path to package
#' @keywords internal
check_unedited <- function(package_dir) {
hash <- hash_package_directory(package_dir)
hash == read_hash_from_description(package_dir)
}
It simply computes the hash of the current package and checks whether that hash is the same as what was originally written to the DESCRIPTION file by litr::render()
.