R/build.R

Defines functions which_package importRd tidy_code strip_white reformat_code roxygen_and_build

Documented in reformat_code roxygen_and_build

#' Roxygenize a package, clean up and build/check the package
#'
#' After the source package is roxygenized, this function will build the
#' package. Optionally it also installs or checks the package, reformats the
#' code in the example sections. Note \code{\link{rab}} is an alias of
#' \code{\link{roxygen_and_build}}.
#' @param pkg the root directory of the source package
#' @param build whether to build the package
#' @param build.opts options to be passed to \command{R CMD build}
#' @param install whether to install the package
#' @param install.opts options to be passed to \command{R CMD INSTALL}
#' @param check whether to check the package
#' @param check.opts options to check the package (e.g. \code{"--no-examples"})
#' @param remove.check whether to remove the directory generated by \command{R
#'   CMD check}
#' @param reformat whether to reformat the example code; see
#'   \code{\link{reformat_code}}
#' @param before an R expression to be evaluated under the package root
#'   directory before the package is roxygenized and built
#' @param ... other arguments passed to \code{\link[roxygen2]{roxygenize}}
#' @return \code{NULL}
#' @author Yihui Xie <\url{http://yihui.org}>
#' @rdname roxygen_and_build
#' @import utils
#' @export
#' @examples \dontrun{
#' roxygen_and_build("Rd2roxygen", install = TRUE)
#' ## or simply
#' rab('Rd2roxygen', install = TRUE)
#' }
roxygen_and_build = function(
  pkg, build = TRUE, build.opts = '--no-manual',
  install = FALSE, install.opts = if (build) '' else '--with-keep.source',
  check = FALSE, check.opts = '--as-cran --no-manual', remove.check = TRUE,
  reformat = TRUE, before = NULL, ...
) {
  if (missing(pkg)) pkg = head(commandArgs(TRUE), 1)
  if (length(pkg) != 1) stop('The package directory must be one character string')
  xfun::in_dir(pkg, before)
  xfun::Rscript_call(roxygen2::roxygenize, list(pkg, ...))
  desc = file.path(pkg, 'DESCRIPTION')
  pv = read.dcf(desc, fields = c('Package', 'Version'))
  if (reformat) {
    message('Reformatting examples')
    rd.list = list.files(
      file.path(pkg, 'man'), '.*\\.Rd$', all.files = TRUE, full.names = TRUE
    )
    for (f in rd.list) reformat_code(f)
  }
  # delete existing tarballs
  unlink(sprintf('%s_*.tar.gz', pv[1, 1]))
  if (build) {
    system(sprintf('%s CMD build %s %s', Rbin(), build.opts, pkg))
  }
  res = sprintf('%s_%s.tar.gz', pv[1, 1], pv[1, 2])
  if (install) {
    if (!build) res = pkg
    system(sprintf('%s CMD INSTALL %s %s ', Rbin(), install.opts, res))
    # generate Rd for objects imported and exported from other packages
    importRd(pkg, pv[1, 1])
  }
  if (check) {
    if (!build) stop('You must build the source package before running R CMD check')
    if ((system(sprintf('%s CMD check %s %s', Rbin(), res, check.opts)) == 0) &&
      remove.check) unlink(sprintf('%s.Rcheck', pv[1, 1]), TRUE)
  }
}

#' @rdname roxygen_and_build
#' @export
rab = roxygen_and_build


#' Format the code in the usage and examples sections
#'
#' The function \code{\link[formatR]{tidy_source}} in the \pkg{formatR} package
#' is used to polish the Rd files generated by \pkg{roxygen2} in the usage and
#' examples sections.
#' @param path the path of the Rd file
#' @param ... other arguments passed to \code{tidy_source}
#' @return \code{NULL}; as a side effect, the original Rd file will be updated
#' @export
#' @author Yihui Xie <\url{http://yihui.org}>
#' @seealso \code{\link[formatR]{tidy_source}}
#' @note If the usage or examples code is not syntactically correct, it will not
#'   be reformatted and a message will be printed on screen. One possible
#'   situation is the percent symbol \code{\%}, which should be escaped even in
#'   the examples code (cf Writing R Extensions), and this can make the code
#'   syntactically incorrect, e.g. \code{a \%in\% b} should be \code{a
#'   \\\%in\\\% b} but the latter is not valid R code. Anyway, this function
#'   will try to unescape the percent symbols before reformating the code, then
#'   escape them.
#' @examples
#' rd.file = system.file('examples', 'reformat_code_demo.Rd', package = 'Rd2roxygen')
#' file.copy(rd.file, tempdir())
#' fmt.file = file.path(tempdir(), 'reformat_code_demo.Rd')
#'
#' file.show(fmt.file)  ## show the raw Rd
#'
#' reformat_code(fmt.file)
#' file.show(fmt.file)  ## the formatted Rd
reformat_code = function(path, ...) {
  rd = readLines(path)
  tags = tags_possible
  if (length(idx0 <- grep('^\\\\examples\\{', rd))) {
    # tags after \examples?
    idx1 = grep(tags, rd)
    if (length(idx1) && any(idx1 > idx0))
      idx1 = min(idx1[idx1 > idx0]) - 1 else idx1 = tail(grep('\\}$', rd), 1)
    rd = tidy_examples(rd, idx0, idx1, ..., path = path)
  }
  writeLines(strip_white(rd), path)
  flush.console()
}

# strip beginning and trailing white spaces
strip_white = function(x) gsub('^\\s+|\\s+$', '', paste(x, collapse = '\n'))

# possible tags after \example{} or \usage{}
tags_possible = sprintf('^\\\\(%s)\\{', paste(c(
  'docType', 'name', 'alias', 'title', 'format', 'source', 'usage', 'arguments',
  'value', 'description', 'details', 'note', 'section', 'examples', 'author',
  'references', 'seealso', 'concept', 'keyword', 'subsection'
), collapse = '|'))

tidy_code = function(code, ...) {
  tidy = function(w) {
    formatR::tidy_source(text = code, output = FALSE, width.cutoff = w, ...)$text.tidy
  }
  res = try(tidy(80))
  if (inherits(res, 'try-error')) return(res)
  i = 1
  # R CMD check used to require code width to be less than 90
  while (any(nchar(unlist(strsplit(res, '\n'))) >= 90) && i <= 40) {
    res = tidy(90 - i)
    i = i + 1
  }
  if (i > 40) code else res
}

#' Generate R doc for functions imported from other packages and re-exported
#'
#' This function searches for exported functions in a package that are not
#' really created in this package, and generate documentation for them so that R
#' CMD check will not complain about the missing documentation for exported
#' functions.
#' @param path the path to the source package
#' @param package the package name
#' @noRd
importRd = function(path, package) {
  escape = getFromNamespace('escape.character', 'roxygen2')
  import = getNamespaceImports(package); import$base = NULL
  pkgs = .packages(TRUE)
  link = lapply(getNamespaceExports(package), function(name) {
    fun = getExportedValue(package, name)
    pkg = if (is.function(fun)) {
      packageName(environment(fun))
    } else {
      which_package(name, import)
    }
    if (is.null(pkg) || pkg == package || !(pkg %in% pkgs)) return()
    # if name is not exported there, you have to document it by yourself
    if (!(name %in% getNamespaceExports(pkg))) return()
    topic = basename(do.call(help, list(name, pkg))[[1]])
    name = as.character(escape(name))
    c(
      package = pkg, name = name,
      link = sprintf('\\code{\\link[%s:%s]{%s}}', pkg, topic, name)
    )
  })
  link = do.call(rbind, link)
  if (NROW(link) == 0) return()
  link = link[order(link[, 'package'], link[, 'name']), , drop = FALSE]
  alias = sprintf('\\alias{%s}', link[, 'name'])
  link = sapply(split(link[, 'link'], link[, 'package']), paste, collapse = ', ')
  doc = c(
    sprintf('\\name{%s-imports}', package),
    alias,
    '\\docType{import}',
    '\\title{Objects imported from other packages}',
    '\\description{',
    'These objects are imported from other packages. Follow the links to their documentation.',
    '\\describe{',
    sprintf('  \\item{%s}{%s}', names(link), link),
    '}}'
  )
  Rd = file.path(path, 'man', sprintf('%s-imports.Rd', package))
  if (file.exists(Rd) && !any(readLines(Rd) == '\\docType{import}')) {
    warning(
      'The Rd file ', Rd, ' already exists. It seems I should not overwrite it. ',
      'You may want to manually save the Rd below:', immediate. = TRUE
    )
    cat(doc, sep = '\n')
    return()
  }
  writeLines(doc, Rd)
}

which_package = function(name, exports) {
  for (i in names(exports)) {
    if (name %in% exports[[i]]) return(i)
  }
}
yihui/Rd2roxygen documentation built on April 16, 2024, 8:26 a.m.