R/roxy.package.R

Defines functions roxy.package

Documented in roxy.package

# Copyright 2011-2022 Meik Michalke <meik.michalke@hhu.de>
#
# This file is part of the R package roxyPackage.
#
# roxyPackage is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# roxyPackage is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with roxyPackage.  If not, see <http://www.gnu.org/licenses/>.


#' Automatic doc creation, package building and repository update
#'
#' This function should help to create R packages with full documentation and updates to a local repository.
#' It supports source and binary packaging (Windows and Mac OS X; see Note section on the limitations).
#'
#' For the documentation \code{roxygen2}[1] is used. Next to the actual in-line documentation of the package's contents, you only need to
#' prepare a data.frame to be used to write a package \code{DESCRIPTION} file. See the example section for details on that. This means
#' that you \emph{neither} edit the \code{DESCRIPTION} \emph{nor} the \code{*-package.R} file manually, they will both be created \emph{automatically}
#' by this function with contents according to these settings!
#' 
#' @section Sandboxing:
#' If you want to check out the effects of roxy.package() without touching you actual package sources, try \code{\link[roxyPackage:sandbox]{sandbox}}
#' to set up a safe testing environment.
#'
#' @section Repository layout:
#' The repository will have this directory structure, that is, below the defined \code{repo.root}:
#'
#' \describe{
#'    \item{\code{./src/contrib}}{Here go the source packages}
#'    \item{\code{./bin/windows/contrib/$RVERSION}}{Here go the Windows binaries}
#'    \item{\code{./bin/macosx/contrib/$RVERSION}}{Here go the Mac OS X binaries (see \code{OSX.repo} for further options)}
#'    \item{\code{./pckg/index.html}}{A global package index with links to packages' index files, if actions included \code{"html"}}
#'    \item{\code{./pckg/web.css}}{A CRAN-style CSS file, if actions included \code{"html"}}
#'    \item{\code{./pckg/$PACKAGENAME}}{Here go documentation PDF and vignette, as well as a \code{ChangeLog} file, if found.
#'      and an \code{index.html} with package information, if actions included \code{"html"}.
#'      This is probably a bit off-standard, but practical if you several packages.}
#' }
#'
#' @section Converting ChangeLogs into NEWS:
#' See \code{\link[roxyPackage:cl2news]{cl2news}} for details.
#' 
#' @section Build for multiple R versions:
#' The options \code{R.libs} and \code{R.homes} can take a vector of strings. This can be used to build packages for multiple R versions,
#' provided you installed them on your system. By default, \code{roxy.package} will only use the first entry of both and ignore the rest,
#' except if you use the \code{"buildEmAll"} action. This makes it easy to use \code{roxy.package} in a script, as you can turn multiple builds
#' on and off with one action, and leave the rest untouched.
#' 
#' If you're running GNU/Linux, an easy way of preparing for multiple builds is to fetch the R sources from CRAN, calling
#' \code{"./configure"} with something like \code{"--prefix=$HOME/R/<R version>"}, so that \code{"make install"} installs to that path.
#' Let's assume you did that with R 3.4.4 and 3.3.3, you could then call \code{roxy.package} with options like
#' \code{R.homes=c("home/user/R/R-3.4.4", "home/user/R/R-3.3.3")} and \code{R.libs=c("home/user/R/R-3.4.4/lib64/R/library",}
#' \code{"home/user/R/R-3.3.3/lib64/R/library")}. If you add \code{"buildEmAll"} to the actions to perform, \code{roxy.package} will then
#' call itself recursively for each given R installation; if you omit \code{"buildEmAll"}, it will only build packages for R 3.4.4, as that
#' is the first configured version.
#'
#' One thing you should be aware of is that \code{roxy.package} will not perform all actions each time. That is because some of them, namely
#' \code{"roxy"}, \code{"cite"}, \code{"license"}, \code{"doc"}, \code{"cl2news"}, \code{"news2rss"}, \code{"cleanRd"}, \code{"readme"},
#' \code{"buildVignettes"}, and \code{"vignette"},
#' would overwrite previous results anyway, so they are only considered during the first run. Therefore, you should always place the R version which
#' should be used for these actions first in line. The \code{"html"} action will list all Windows and OS X binary packages. The \code{"deb"}
#' action will only actually debianize and build a binary package during the first run, too.
#'
#' @section Windows: On Windows, the actions \code{"doc"} and \code{"check"} will only work correctly if you have installed and configured LaTeX
#' accordingly, and you will also need Rtools set up for packaging.
#'
#' @section CRAN compliance: The CRAN policies can sometimes be very strict. This package should allow you to produce packages which are suitable
#' for release on CRAN. But some steps have to be taken care of by yourself. For instance, CRAN does currently not allow copies of common licenses
#' in a source package, nor a \code{debian} folder. Therefore, if your package is supposed to be released on CRAN, you should include
#' \code{Rbuildignore=c("debian", "LICENSE")} to the function call.
#' 
#' @section Temporary git checkouts: If you want to rebuild binaries of something that was already released, i.e. by using the \code{"binonly"} action,
#' and if your source directory is a git repository, then the action \code{"gitCheckout"} can temporarily checkout the source version to build
#' and switch back to the status quo afterwards again. This might or might not work as you expect, depending on whether you organize your code like
#' it is expected here. That is, each release must be tagged properly, with the exact version number as the tag name. You should also commit all
#' current changes to the code before you use this. Internally, \code{roxy.package} will try to find out the current branch of the git repository,
#' then checkout the version number you provided as the new branch or tag, do all the packaging, and checkout bach to the previous branch.
#'
#' @note The binary packaging is done simply by zipping (Windows) or targzipping (Mac OS X) the built and installed package. This should
#' do the trick as long as your package is written in pure R code. It will most likely not produce usable packages if it contains
#' code in other languages like C++.
#'
#' @param pck.source.dir Character string, path pointing to the root directory of your package sources.
#' @param pck.version Character string, defining the designated version number. Can be omitted if actions don't
#'    include \code{"roxy"}, then this information is read from the present DESCRIPTION file.
#' @param pck.description Data.frame holding the package description (see Examples section). Any data.frame with valid fields
#'    will do, but you should use \code{\link[roxyPackage:package_description]{package_description}} if possible because it does
#'    some basic validity checks.
#' @param R.libs Character string, valid path to the R library where the package should be installed to.
#' @param repo.root Character string, valid path to a directory where to build/update a local package repository.
#' @param pck.date Date class object or character string of the release date in YYYY-MM-DD format. Defaults to \code{Sys.Date()}.
#'    If actions don't include \code{"roxy"} and neither \code{Date}, \code{Packaged}, nor \code{Date/Publication} are found
#'    in the present DESCRIPTION file, then \code{pck.date} will be used. Otherwise, the information from the DESCRIPTION file is used.
#' @param actions Character vector, must contain at least one of the following values:
#'    \describe{
#'      \item{"roxy"}{Roxygenize the docs}
#'      \item{"cite"}{Update CITATION file}
#'      \item{"license"}{Update LICENSE file}
#'      \item{"readme"}{Generate initial README.md file}
#'      \item{"check"}{Do a full package check, calling \code{R CMD check}. Combine with \code{"package"} to do the check on the tarball, not the source directory.}
#'      \item{"package"}{Build & install the package, update source repository, calling \code{R CMD build} and \code{R CMD INSTALL}}
#'      \item{"binonly"}{Like \code{"package"}, but doesn't copy the source package to the repository, to enable binary-only rebuilds}
#'      \item{"gitCheckout"}{Treats \code{pck.source.dir} as a git repository and \code{pck.version} as a branch or tag to checkout temporarily;
#'        only valid in combination with \code{"binonly"}}
#'      \item{"cl2news"}{Try to convert a ChangeLog file into an NEWS.Rd file}
#'      \item{"news2rss"}{Try to convert \code{inst/NEWS.Rd} into an RSS feed. You must also set
#'        \code{URL} accordingly}
#'      \item{"doc"}{Update PDF documentation (\code{R CMD Rd2pdf}) and vignettes if present; }
#'      \item{"html"}{Update HTML index files and compile HTML versions of README.md and NEWS.md (if \code{pandoc} is available).}
#'      \item{"win"}{Update the Windows binary package}
#'      \item{"macosx"}{Update the Mac OS X binary package}
#'      \item{"log"}{Generate initial ChangeLog or update a present ChangeLog file}
#'      \item{"deb"}{Update the Debian binary package with \code{\link[roxyPackage:debianize]{debianize}} (works only on Debian systems;
#'        see \code{deb.options}, too). \code{URL} must also be set to generate Debian repository information}
#'      \item{"cleanRd"}{Insert line breaks in Rd files with lines longer than 90 chars}
#'      \item{"vignette"}{Generate initial vignette stub in directory \code{vignettes}; if \code{html.options} has a \code{flattr.id}, it will be included}
#'      \item{"buildVignettes"}{Re-build all vignettes during the \code{"package"} action, to force generation of a vignette index in
#'        the source package (recommended if \code{VignetteBuilder} is set in the package description)}
#'      \item{"buildEmAll"}{Build binary packages for all configured R versions, not just the first. Only effective if multiple versions of R are actually provided (see above)}
#'    }
#'    Note that \code{"cl2news"} will write the \code{NEWS.Rd} file to the \code{inst} directory of your sources, which will overwrite
#'    an existing file with the same name! Also note that if both a \code{NEWS/NEWS.Rd} and \code{ChangeLog} file are found, only
#'    news files will be linked by the \code{"html"} action.
#' @param cleanup Logical, if \code{TRUE} will remove backup files (matching \code{.*~$} or \code{.*backup$}) from the source directory.
#' @param rm.vignette Logical, if \code{TRUE} and a vignette was build during the \code{"doc"} action and vignettes live in the directory \code{inst/doc},
#'    they will not be kept in the source package but just be moved to the \code{./pckg/$PACKAGENAME} directory of the repository.
#' @param R.homes Path to the R installation to use. Can be set manually to build packages for other R versions than the default one,
#'    if you have installed them in parallel. Should probably be used together with \code{R.libs}.
#' @param R.libs.append An optional vector of paths pointing to R libraries to always be included for package lookup. These locations will be added
#'    to package build calls by appending them to the \code{R_LIBS_USER} environment variable accordingly, if not \code{NULL}.
#' @param Rcmd.options A named character vector with options to be passed on to the internal calls of \code{R CMD build},
#'    \code{R CMD INSTALL}, \code{R CMD check} and \code{R CMD Rd2pdf}. Change these only if you know what you're doing!
#'    Will be passed on as given here. To deactivate, options must explicitly be se to \code{""}, missing options will be used with the default values.
#'    Please note that if you've set \code{VignetteBuilder} in the package description, the vignettes will always be re-build if you enabled the
#'    \code{"buildVignettes"} action, even if you keep \code{--no-build-vignettes} in the build options.
#' @param URL Either a single character string defining the URL to the root of the repository (i.e., which holds the directories \code{src}
#'    etc., see below), or a named character vector if you need different URLs for different services. If you provide more than one URL, these are valid
#'    names for values:
#'    \describe{
#'      \item{\code{default}}{A mandatory fallback URL, will be used if not overridden by one of the other values. This is fully equivalent to the global value
#'        if only one character string is provided.}
#'      \item{\code{debian}}{Used for the Debian package repository if different from the default.}
#'      \item{\code{mirror.list}}{URL pointing to a list of mirrors users should choose from, rather than using one particular host name for the Debian repository.
#'        Will only be used in the HTML instructions for a Debian repository.}
#'      \item{\code{debian.path}}{Can be used to define a custom path users would need to specify in addition to the main URL.
#'        Defaults to \code{"/deb"}, and if given, it must start with a slash.
#'        Will be used in combination with \code{default}, \code{debian} or \code{mirror.list}. It is not advisable to combine it with \code{default}, because you will
#'        have to manually rename the directory generated after each run!}
#'    }
#'    These URLs are not the path to the local file system, but should be the URLs to the respecive repository as it is available
#'    via internet. This option is necessary for (and only interpreted by) the actions \code{"news2rss"}, \code{"deb"}, and possibly \code{"html"} --
#'    if \code{flattr.id} is also set in \code{html.options}, a Flattr meta tag be added to the HTML page.
#' @param deb.options A named list with parameters to pass through to \code{\link[roxyPackage:debianize]{debianize}}. By default, \code{pck.source.dir}
#'    and \code{repo.root} are set to the values given to the parameters above, and if packages are being build for R 3.5, the default \code{deb.dir} changes
#'    from \code{"deb"} to \code{"debR35"}, and if built for R 4.0  to \code{"debR40"}. As for the other options, if not set, the defaults of \code{debianize} will be used.
#' @param readme.options A named list with parameters that add optional extra information to an initial README.md file, namely instructions to install the package
#'    directly from a GitHub repository. Ignore this if you don't use GitHub. Theoretically, you can overwrite all values of the internal
#'    function \code{readme_text} (e.g., try \code{formals(roxyPackage:::readme_text)}). But in practice, these two should be all you need to set:
#'    \describe{
#'      \item{\code{githubUser}}{Your GitHub user name, can be used to contruct the GitHub repo URL}
#'    }
#'    All other missing values are then guessed from the other package information. It is then assumed that the GitHub repo has the same name as the package.
#' @param html.options A named list with parameters to be used for generating the HTML files of the repository. These values are recognized:
#'    \describe{
#'      \item{\code{index}}{A character string for the headline of the global index HTML file; if missing, "Available R Packages" will be used as default}
#'      \item{\code{title}}{A character string for the title tag prefix of the package index HTML file; if missing, "R package" will be used as default}
#'      \item{\code{flattr.id}}{A Flattr meta ID, will be added to the headers of package specific HTML files, and to a vignette stub if the \code{"vignette"} action is active}
#'      \item{\code{repo.flattr.id}}{A Flattr meta ID, will be added to the headers of all global HTML files of the repository}
#'      \item{\code{imprint}}{A named character string used as a URL to link to an imprint page; he name is used as the link text}
#'      \item{\code{privacy.policy}}{A named character string used as a URL to link to a privacy policy statement in accordance with GDPR; the name is used as the link text}
#'    }
#' @param ChangeLog A named list of character vectors with log entry items. The element names will be used as section names in the ChangeLog entry,
#'    and each character string in a vector will be pasted as a log item. The news you provide here will be appended to probably present news, while
#'    trying to prevent duplicate entries to appear. If you need more control, don't use the \code{"log"} action, but have a look at
#'    \code{\link[roxyPackage:updateChangeLog]{updateChangeLog}}. Also note that the date of altered entries will be updated automatically, unless
#'    you don't call the \code{"roxy"} action, too.
#' @param Rbuildignore A character vector to be used as lines of an \code{.Rbuildignore} file. If set, this will replace an existing \code{.Rbuildignore}
#'    file. Setting it to an empty string (\code{""}) will remove the file, the default value \code{NULL} will simply keep the file, if one is present.
#' @param Rinstignore A character vector to be used as lines of an \code{.Rinstignore} file. If set, this will replace an existing \code{.Rinstignore}
#'    file. Setting it to an empty string (\code{""}) will remove the file, the default value \code{NULL} will simply keep the file, if one is present.
#' @param OSX.repo A named list of character vectors, one named \code{"main"} defines the main directory below \code{./bin/macosx/} where packages for
#'    Mac OS X should be copied, and the second optional one named \code{"symlink"} can be used to set symbolic links, e.g., \code{symlinks="el-capitan"}
#'    would also make the repository available via \code{./bin/macosx/mavericks}. Symbolic links will be ignored when run on on Windows. If you use them,
#'    make sure they're correctly transferred to your server, where applicable.
#' @param pck.aliases A character vector, defining all aliases to be used in the \code{*-package.R} file. The default \code{NULL} results in
#'    \code{paste0(pck.description[["Package"]], c("", "-package"))} to be set in the \code{*-package.Rd} file after roxygenizing the docs.
#'    It can be necessary to limit this to \code{paste0(pck.description[["Package"]], "-package")} if your package has the same name as one of its
#'    exported objects (e.\,g. a function/method) to not end up with two aliases two \code{pck.description[["Package"]]} in different files.
#' @param ... Additional options passed through to \code{roxygenize}.
#' @references
#' [1] \url{https://CRAN.R-project.org/package=roxygen2}
#' @seealso
#'    \code{\link[roxyPackage:package_description]{package_description}} for proper package description, and
#'    \code{\link[roxyPackage:sandbox]{sandbox}} to run roxy.package() in a sandbox.
#' @importFrom roxygen2 roxygenize
#' @export
#' @examples
#' \dontrun{
#' ## package description as data.frame:
#' pckg.dscrptn <- package_description(
#'   Package="SquareTheCircle",
#'   Type="Package",
#'   Title="Squaring the circle using Heisenberg compensation",
#'   Author="Ernst Dölle [aut, cre, cph], Ludwig Dölle [trl, ctb] (initial translation to whitespace)",
#'   AuthorsR="c(person(given=\"Ernst\", family=\"Dölle\",
#'        email=\"e.a.doelle@example.com\",
#'        role=c(\"aut\", \"cre\", \"cph\")),
#'      person(given=\"Ludwig\", family=\"Dölle\",
#'        role=c(\"trl\", \"ctb\"),
#'        comment=\"initial translation to whitespace\")
#'      )",
#'   Maintainer="E.A. Dölle <doelle@@eternalwondermaths.example.org>",
#'   Depends="R (>= 2.10.0),heisenberg (>= 0.23),tools",
#'   Enhances="rkward",
#'   Description="This package squares the circle using Heisenberg compensation.
#'       The code came from a meeting with Yrla Nor that i had in a dream. Please
#'       don't forget to chain your computer to the ground, because these
#'       algorithms might make it fly.",
#'   License="GPL (>= 3)",
#'   Encoding="UTF-8",
#'   LazyLoad="yes",
#'   URL="http://eternalwondermaths.example.org"
#' )
#' # hint no. 1: you *don't* specify version number and release date here,
#' #   but all other valid fields for DESCRIPTION files must/can be defined
#' # hint no. 2: most of this rarely changes, so you can add this to the
#' #   internals of your package and refer to it as
#' #   roxy.package(pck.description=SquareTheCircle:::pckg.dscrptn, ...)
#' # hint no. 3: use "AuthorR" for the "Author@@R" field, or "AuthorsR" for
#' # R >= 2.14, to work around naming problems
#'
#' roxy.package(pck.source.dir="~/my_R_stuff/SquareTheCircle",
#'   pck.version="0.01-2",
#'   pck.description=pckg.dscrptn,
#'   R.libs="~/R",
#'   repo.root="/var/www/repo",
#'   actions=c("roxy", "package", "doc"))
#' }

roxy.package <- function(
  pck.source.dir,
  pck.version,
  pck.description,
  R.libs,
  repo.root,
  pck.date=Sys.Date(),
  actions=c("roxy", "package"),
  cleanup=FALSE,
  rm.vignette=FALSE,
  R.homes=R.home(),
  R.libs.append=NULL,
  Rcmd.options=c(
    install="--install-tests",
    build="--no-manual --no-build-vignettes --md5",
    check="--as-cran",
    Rd2pdf="--pdf --no-preview"),
  URL=NULL,
  deb.options=NULL,
  readme.options=NULL,
  html.options=NULL,
  ChangeLog=list(changed=c("initial release"), fixed=c("missing ChangeLog")),
  Rbuildignore=NULL,
  Rinstignore=NULL,
  OSX.repo=list(main="contrib", symlinks="el-capitan"),
  pck.aliases=NULL,
  ...){
  
  # ensure backwards compatibility
  extra_options <- list(...)
  if("html.index" %in% names(extra_options)){
    warning("The 'html.index' argument was moved to 'html.options', please see the docs and adjust your code!", call.=FALSE)
    html.options[["index"]] <- extra_options[["html.index"]]
  } else {}
  if("html.title" %in% names(extra_options)){
    warning("The 'html.title' argument was moved to 'html.options', please see the docs and adjust your code!", call.=FALSE)
    html.options[["title"]] <- extra_options[["html.title"]]
  } else {}
  if("flattrUser" %in% names(readme.options)){
    warning("The 'flattrUser' argument was renamed into 'flattr.id' and moved from 'readme.options' to 'html.options', please see the docs and adjust your code!", call.=FALSE)
    html.options[["flattr.id"]] <- readme.options[["flattrUser"]]
    readme.options[["flattrUser"]] <- NULL
  } else {}

  # avoid some NOTEs from R CMD check
  AuthorR <- AuthorsR <- Author.R <- Authors.R <- NULL

  # check the OS
  unix.OS <- isUNIX()
  
  # do we need to worry about multiple R versions?
  R.versions <- length(R.homes)
  R.libraries <- length(R.libs)
  if(R.versions > 1){
    if(R.libraries != R.versions){
      stop(simpleError("If you specify more than one R.home, you must also define as many R.libs!"))
    } else {}
    if("buildEmAll" %in% actions){
      # if so, iterate recursively through it and then end
      for (this.R in seq_along(R.homes)){
        this.home <- R.homes[this.R]
        this.libs <- R.libs[this.R]
        this.actions <- actions
        this.deb.options <- deb.options
        if("deb" %in% actions){
          if(this.R == 1){
            # for the time being, debianizing the package once is enough
            # we'll only do it the *first* run
            this.deb.options[["actions"]] <- deb.options[["actions"]][deb.options[["actions"]] %in% c("deb", "bin", "src")]
          } else {
            # all other cases: no debianizing
            this.deb.options[["actions"]] <- ""
          }
        } else {}
        if(this.R > 1){
          # well, the same is true for some other actions
          # we'll only perform them during the *first* run
          this.actions <- actions[!actions %in% c(
            "roxy",
            "cite",
            "license",
            "doc",
            "cl2news",
            "news2rss",
            "cleanRd",
            "readme",
            "vignette",
            "buildVignettes",
            "gitCheckout"
          )]
          # we also don't need to repeat handling of .Rbuildignore and .Rinstignore
          Rbuildignore <- Rinstignore <- NULL
        } else {}
        roxy.package(
          pck.source.dir=pck.source.dir,
          pck.version=pck.version,
          pck.description=pck.description,
          R.libs=this.libs,
          repo.root=repo.root,
          pck.date=pck.date,
          actions=this.actions,
          cleanup=cleanup,
          rm.vignette=rm.vignette,
          R.homes=this.home,
          R.libs.append=R.libs.append,
          Rcmd.options=Rcmd.options,
          URL=URL,
          deb.options=this.deb.options,
          readme.options=readme.options,
          html.options=html.options,
          ChangeLog=ChangeLog,
          Rbuildignore=Rbuildignore,
          Rinstignore=Rinstignore,
          OSX.repo=OSX.repo,
          ...)
      }
      return(invisible(NULL))
    } else {
      R.homes <- R.homes[1]
      R.libs <- R.libs[1]
      R.versions <- R.libraries <- 1
    }
  } else {}

  old.dir <- getwd()
  on.exit(
    setwd(old.dir),
    add=TRUE
  )

  # fist see if we need to rename an "AuthorR" field. data.frame definitions tend to
  # replace the "@" with a dot
  if("AuthorR" %in% names(pck.description)){
    pck.description$`Author@R` <- pck.description[["AuthorR"]]
    pck.description <- subset(pck.description, select=-AuthorR)
  } else if("AuthorsR" %in% names(pck.description)){
    pck.description$`Authors@R` <- pck.description[["AuthorsR"]]
    pck.description <- subset(pck.description, select=-AuthorsR)
  } else if("Author.R" %in% names(pck.description)){
    pck.description$`Author@R` <- pck.description[["Author.R"]]
    pck.description <- subset(pck.description, select=-Author.R)
  } else if("Authors.R" %in% names(pck.description)){
    pck.description$`Authors@R` <- pck.description[["Authors.R"]]
    pck.description <- subset(pck.description, select=-Authors.R)
  } else {}

  # normalize all root paths
  R.homes <- normalizePathByOS(path=R.homes, is.unix=unix.OS, mustWork=TRUE)
  repo.root <- normalizePathByOS(path=repo.root, is.unix=unix.OS, mustWork=FALSE)
  pck.source.dir <- normalizePathByOS(path=pck.source.dir, is.unix=unix.OS, mustWork=TRUE)

  # get info on the R version used
  R.Version.full <- getRvers(R.homes=R.homes)
  R.Version.win <- getRvers(R.homes=R.homes, win=TRUE)
  if(nchar(R.Version.full) < 3 | nchar(R.Version.win) < 3){
    stop(simpleError("R version number cannot be detected, it seems."))
  } else {}

  # keep checking for older R versions, as they might still be requested
  # in multi-version builds
  if(isTRUE(R_system_version(R.Version.full) < "3.0")){
    stop(simpleError("Sorry, but support for R < 3.0.0 was dropped with roxyPackage 0.06-1!"))
  } else {}

  # adjust the default repo directory if we're building against R 3.5, as those packages
  # can't be installed with previous R versions
  if(all(
    "deb" %in% actions,
    isTRUE(R_system_version(R.Version.full) >= "3.5"),
    is.null(deb.options[["deb.dir"]])
  )){
    if(isTRUE(R_system_version(R.Version.full) >= "4.0")){
      message("deb: R >= 4.0 detected, using \"debR40\" as the default directory for debian package files")
      deb.options[["deb.dir"]] <- "debR40"
    } else if(isTRUE(R_system_version(R.Version.full) >= "3.5")){
      message("deb: R >= 3.5 detected, using \"debR35\" as the default directory for debian package files")
      deb.options[["deb.dir"]] <- "debR35"
    } else {}
  } else {}

  # should we try to checkout a certain git tag?
  do_gitCheckout <- all(c("gitCheckout", "binonly") %in% actions)
  if(do_gitCheckout){
    if(!"roxy" %in% actions | is.null(pck.description)){
      # we'll repeat this step later on, after the checkout finished
      pck.dscrptn <- as.data.frame(read.dcf(file=file.path(pck.source.dir, "DESCRIPTION")), stringsAsFactors=FALSE)
      pck.package <- getDescField(pck.dscrptn, field="Package")
    } else {
      pck.package <- roxy.description("package", description=pck.description)
    }
    # if we're sandboxing, prepare the source dir before we do anything
    # this step will then be omitted by the further sandboxing steps,
    # like finding package dependencies etc., which could be different
    # for versions checked out here
    if(isTRUE(check.sandbox())){
      pck.source.dir <- prep.sndbx.source.dir(
        snd.pck.source.dir=slot(get.roxyEnv("sandbox"), "pck.source.dir"),
        pck.source.dir=pck.source.dir,
        package=pck.package
      )
    } else {}

    current_branch <- git_branch(src_dir=pck.source.dir)
    message(paste0("git: found git repository \"", pck.source.dir, "\""))
    message(paste0("git: current branch is \"", current_branch, "\", checking out \"", pck.version, "\""))
    successful_checkout <- git_checkout(src_dir=pck.source.dir, ref=pck.version)
    if(isTRUE(successful_checkout)){
      message(paste0("git: successfully checked out \"", pck.version, "\""))
      on.exit(
        {
          message(paste0("git: checking out \"", current_branch, "\""))
          successful_checkout <- git_checkout(src_dir=pck.source.dir, ref=current_branch)
          if(isTRUE(successful_checkout)){
            message(paste0("git: successfully checked out \"", current_branch, "\""))
          } else {}
        },
        add=TRUE
      )
    } else {
      stop(simpleError(paste0("git: unable to checkout branch/tag \"", pck.version, "\", are you sure it exists?")))
    }
  } else {}

  # special cases: if actions do not include "roxy",
  # take infos from the DESCRIPTION file
  if(!"roxy" %in% actions | is.null(pck.description)){
    pck.dscrptn <- as.data.frame(read.dcf(file=file.path(pck.source.dir, "DESCRIPTION")), stringsAsFactors=FALSE)
    # clean from newlines
    pck.dscrptn <- as.data.frame(t(sapply(pck.dscrptn, function(x) gsub("\n", " ", x))), stringsAsFactors=FALSE)
    pck.version <- getDescField(pck.dscrptn, field="Version")
    # if "Date" is missing in DESCRIPTION file, try some fallbacks
    if(any(c("Date","Packaged","Date/Publication") %in% colnames(pck.dscrptn))){
      pck.date <- as.character(as.Date(getDescField(pck.dscrptn, field=c("Date","Packaged","Date/Publication"))))
    } else {
      message("There was no 'Date', 'Packaged', or 'Date/Publication' in DESCRIPTION, using pck.date as fallback.")
      pck.dscrptn["Date"] <- pck.date
    }
    pck.package <- getDescField(pck.dscrptn, field="Package")
    pck.title <- getDescField(pck.dscrptn, field="Title")
    rss.description <- getDescField(pck.dscrptn, field="Description")
    pckg.license <- getDescField(pck.dscrptn, field="License")
    pckg.dscrptn <- pck.description <- pck.dscrptn
  } else {
    rss.description <- pck.description[["Description"]]
    pck.package <- roxy.description("package", description=pck.description)
    pck.title <- roxy.description("title", description=pck.description)
    pckg.dscrptn <- roxy.description("description", description=pck.description, version=pck.version, date=as.character(pck.date))
    pckg.license <- pck.description[["License"]]
  }
  pckg.package <- roxy.description(
    "pckg.description",
    description=pck.description,
    version=pck.version,
    date=as.character(pck.date),
    pck.aliases=pck.aliases
  )

  ## check for sandboxing
  if(isTRUE(check.sandbox())){
    message("preparing sandbox...")
    # prepare folder structure; this will also insure sane values and abort
    # if locations are invalid. the function returns a list of paths to use.
    if("check" %in% actions){
      # for check runs we need also all suggested packages
      initSuggests <- TRUE
    } else {
      initSuggests <- FALSE
    }
    adjust.paths <- prepare.sandbox(
      package=pck.package,
      description=pckg.dscrptn,
      pck.source.dir=pck.source.dir,
      R.libs=R.libs,
      R.version=R.Version.full,
      repo.root=repo.root,
      depLevel=c("Depends", "Imports"),
      initSuggests=initSuggests,
      gitCheckout=do_gitCheckout
    )
    # replace paths with sandbox
    pck.source.dir <- adjust.paths[["pck.source.dir"]]
    R.libs <- adjust.paths[["R.libs"]]
    repo.root <- adjust.paths[["repo.root"]]
    sandbox.status()
  } else {}

  # check environment
  R.bin <- file.path(R.homes, "bin", "R")
  message(paste("R environment\n  R.home:", R.homes, "\n  R.libs:", R.libs))

  repo.src.contrib <- file.path(repo.root, "src", "contrib")
  repo.win <- file.path(repo.root, "bin", "windows", "contrib", R.Version.win)
  # we'll do proper checks in the mac section, no need to freak out here just yet
  # if OSX.repo is set incorrectly
  repo.macosx <- file.path(repo.root, "bin", "macosx")
  repo.macosx.main <- file.path(repo.macosx, OSX.repo[["main"]])
  repo.macosx.R <- file.path(repo.macosx.main, R.Version.win)
  repo.pckg.info.main <- file.path(repo.root, "pckg")
  repo.pckg.info <- file.path(repo.pckg.info.main, pck.package)
  pckg.basename <- paste0(pck.package, "_", pck.version)
  pckg.name.src <- paste0(pckg.basename, ".tar.gz")
  pckg.name.win <- paste0(pckg.basename, ".zip")
  pckg.name.mac <- paste0(pckg.basename, ".tgz")
  pckg.inst.dir <- file.path(pck.source.dir, "inst")
  pckg.cite.file <- file.path(pckg.inst.dir, "CITATION")
  pckg.cite.file.html <- file.path(repo.pckg.info, "citation.html")
  src.changelog <- file.path(pck.source.dir, "ChangeLog")
  pckg.changelog <- file.path(repo.pckg.info, "ChangeLog")
  repo.src.gz <- file.path(repo.src.contrib, pckg.name.src)
  win.package <- file.path(repo.win, pckg.name.win)
  macosx.package <- file.path(repo.macosx.R, pckg.name.mac)
  pckg.NEWS.Rd <- file.path(pckg.inst.dir, "NEWS.Rd")
  pckg.NEWS.inst <- file.path(pckg.inst.dir, "NEWS")
  pckg.NEWS <- file.path(pck.source.dir, "NEWS")
  pckg.NEWS.html <- file.path(repo.pckg.info, "NEWS.html")
  RSS.file.name <- "RSS.xml"
  pckg.NEWS.rss <- file.path(repo.pckg.info, RSS.file.name)
  pckg.license.file <- file.path(pck.source.dir, "LICENSE")
  pckg.license.file.old <- file.path(pck.source.dir, "LICENSE.txt")
  pckg.readme.file <- file.path(pck.source.dir, "README.md")
  pckg.readme.html <- file.path(repo.pckg.info, "README.html")
  pckg.pdf.doc <- paste0(pck.package, ".pdf")
  pckg.vign.dir <- file.path(pck.source.dir, "vignettes")
  pckg.vign.file <- file.path(pckg.vign.dir, paste0(pck.package, "_vignette.Rmd"))
  clean.env.unix <- paste0("unset R_LIBS_USER R_BINARY R_CMD ; ")
  clean.env.win  <- paste0("SET R_LIBS_USER=\"\" & SET R_BINARY=\"\" & SET R_CMD=\"\" & ")
  if(!is.null(R.libs.append)){
    R_libs_append_unix <- paste0("unset R_LIBS_USER ; export R_LIBS_USER=\"", paste0(R.libs.append, collapse=":"), "\" ;")
    R_libs_append_win <- paste0("SET R_LIBS_USER=\"", paste0(R.libs.append, collapse=":"), "\" & ")
    set.env.unix <- paste0("export R_LIBS_USER=\"", paste0(c(R.libs, R.libs.append), collapse=":"), "\" ; ")
    set.env.win <- paste0("SET R_LIBS_USER=\"", paste0(c(R.libs, R.libs.append), collapse=":"), "\" & ")
  } else {
    R_libs_append_unix <- ""
    R_libs_append_win <- ""
    set.env.unix <- paste0("export R_LIBS_USER=\"", R.libs, "\" ; ")
    set.env.win <- paste0("SET R_LIBS_USER=\"", R.libs, "\" & ")
  }

  # take care of .Rbuildignore and .Rinstignore
  pckg.Rbuildignore <- configFile(root=pck.source.dir, type="build", content=Rbuildignore)
  pckg.Rinstignore <- configFile(root=pck.source.dir, type="inst", content=Rinstignore)

  # debian package specific stuff, probably needed for "html" action
  deb.defaults <- mergeOptions(
    someFunction=debianize,
    customOptions=deb.options,
    newDefaults=list(
      pck.source.dir=pck.source.dir,
      repo.root=repo.root,
      deb.keyring.options=list(URL=getURL(URL, purpose="debian")),
      keep.existing.orig="binonly" %in% actions,
      R.libs.append=R.libs.append
    )
  )
  # try to set pckg.name.deb and deb.repo.path.part
  # this will only work if repo.root is unchanged, the rest is too messy now...
  # don't ry to replace this without checking the outcome in the HTML file!
  deb.repo.path.part <- debRepoPath(
    dist=deb.defaults[["distribution"]],
    comp=deb.defaults[["component"]],
    arch=deb.defaults[["arch"]],
    part=TRUE,
    deb.dir=deb.defaults[["deb.dir"]]
  )
  # need to get repo.name to be able to call eval() on deb.defaults[["origin"]], because that pastes repo.name
  repo.name <- deb.defaults[["repo.name"]]
  deb.defaults[["origin"]] <- eval(deb.defaults[["origin"]])
  pckg.name.deb.part <- debianPkgName(package=pck.package, origin=deb.defaults[["origin"]], version=NULL, replace.dots=FALSE)
  pckg.name.deb <-  paste0(
    pckg.name.deb.part, "_",
    debianPkgVersion(version=pck.version, revision=deb.defaults[["revision"]], epoch=deb.defaults[["epoch"]]), "_",
    deb.defaults[["arch"]], ".deb"
  )
  deb.package <- file.path(repo.root, deb.repo.path.part, pckg.name.deb)

  # ensure we have at least 'index' and 'title' set to defaults
  html.options <- mergeOptions(
    someFunction=function(index, title, flattr.id=NULL, repo.flattr.id=NULL, imprint=NULL, privacy.policy=NULL){},
    customOptions=html.options,
    newDefaults=list(
      index="Available R Packages",
      title="R package"
    )
  )

  # check for additional CMD options
  if("build" %in% names(Rcmd.options)){
    Rcmd.opt.build <- paste0(Rcmd.options[["build"]], " ")
  } else {
    Rcmd.opt.build <- "--no-manual --no-build-vignettes "
  }
  
  Rcmd.opt.install <- ifelse("install" %in% names(Rcmd.options), paste0(Rcmd.options[["install"]], " "), "")
  Rcmd.opt.check <- ifelse("check" %in% names(Rcmd.options), paste0(Rcmd.options[["check"]], " "), "")
  Rcmd.cmd.Rd2pdf <- "Rd2pdf"
  if("Rd2pdf" %in% names(Rcmd.options)){
    Rcmd.opt.Rd2pdf <- paste0(Rcmd.options[["Rd2pdf"]], " ")
  } else {
    Rcmd.opt.Rd2pdf <- "--pdf --no-preview "
  }

  # check for/create info directory
  createMissingDir(dirPath=repo.pckg.info, action="repo")
  
  # clean up
  if(isTRUE(cleanup)){
    unlink(list.files(pck.source.dir, pattern=".*~$", full.names=TRUE, recursive=TRUE))
    unlink(list.files(pck.source.dir, pattern=".*backup$", full.names=TRUE, recursive=TRUE))
  } else {}

  if("license" %in% actions){
    if(checkLicence(pckg.license)){
      copyLicence(pckg.license, pckg.license.file, overwrite=TRUE)
      if(file.exists(pckg.license.file.old)){
        warning("license: you have both LICENSE and LICENSE.txt in your project! if LICENSE.txt is one of the standard licenses, please rename it to prevent its intallation.", call.=FALSE)
      } else {}
    } else {
      stop(simpleError(paste0("license: unrecognized license (", pckg.license, "), please provide your own LICENSE file! ")))
    }
  } else {}

  if("readme" %in% actions){
    if(file.exists(pckg.readme.file)){
      warning("readme: README.md exists, please remove it if you want it re-written!", call.=FALSE)
    } else {
      readme.defaults <- mergeOptions(
        someFunction=readme_text,
        customOptions=readme.options,
        newDefaults=list(
          package=pck.package,
          description=pck.description[["Description"]],
          url=pck.description[["URL"]],
          license=pck.description[["License"]],
          author=paste0(
            gsub(
              "[[:space:]]*<[^>]*>",
              "",
              get.authors(description=pck.description, maintainer=FALSE, contributor=FALSE, copyright=FALSE)[["aut"]]
            ),
            collapse=", "
          ),
          githubRepo=pck.package
        )
      )
      formals(readme_text) <- readme.defaults
      cat(
        readme_text(),
        file=pckg.readme.file
      )
      message(paste0("readme: generated initial ", pckg.readme.file, "."))
    }
  } else {}
  
  if("roxy" %in% actions){
    add.options <- list(...)
    if(any(c("local.roxy.dir", "roxy.unlink.target") %in% names(add.options))){
      stop(simpleError("you are still using \"local.roxy.dir\" and/or \"roxy.unlink.target\", these options have been removed from roxyPackage as of 0.04-2, since they are no longer supported by roxygen2. please change your roxy.package() call!"))
    } else {}

    # re-write DESCRIPTION files
    write.dcf(pckg.dscrptn, file=file.path(pck.source.dir, "DESCRIPTION"))
    # copy DESCRIPTION to pckg directory for easy HTML indexing
    stopifnot(file.copy(file.path(pck.source.dir, "DESCRIPTION"), file.path(repo.pckg.info, "DESCRIPTION"), overwrite=TRUE))
    pckg.package.file.R <- file.path(pck.source.dir, "R", paste0(pck.package, "-package.R"))
    cat(paste(pckg.package), file=pckg.package.file.R)
    message(paste0("roxy: updated ", pckg.package.file.R, "."))

    # another experimental feature:
    # sometimes roxygen2::roxygenize is somewhat buggy, but functions like devtools::document() still work.
    # manually setting "roxyFunction" makes it possible to use that function instead.
    # the default value is set in roxyPackage-internal.R!
    useRoxyFunction <- get.roxyEnv(name="roxyFunction")
    stopifnot(is.function(useRoxyFunction))
    # should normally default to roxygen2::roxygenize(pck.source.dir, ...)
    useRoxyFunction(pck.source.dir, ...)
  } else {}

  if("cleanRd" %in% actions){
    pckg.man.dir <- file.path(pck.source.dir, "man")
    Rd.files <- list.files(pckg.man.dir, pattern="*.Rd", ignore.case=TRUE)
    files_changed <- c()
    if(length(Rd.files)){
      for (this.file in Rd.files){
        # this step both checks the files and changes them if necessary,
        # and keeps track of changes
        if(isTRUE(sanitizeRdFiles(this.file, root.dir=pckg.man.dir, maxlength=90))){
          files_changed <- c(files_changed, this.file)
        }
      }
      if(length(files_changed)){
        warning(
          paste0(
            "cleanRd: one or more Rd files had lines >90 chars and were sanitized, please check:\n    ",
            paste0(shQuote(files_changed), collapse=",\n    ")
          ),
          call.=FALSE
        )
      } else {}
    } else {}
  } else {}

  if("cite" %in% actions){
    createMissingDir(dirPath=pckg.inst.dir, action="cite")
    # calling internal function to generate citation object
    cite.obj <- citationText(pck.dscr=pck.description, pck.version=pck.version, pck.date=pck.date)
    cat(cite.obj, file=pckg.cite.file)
    message(paste0("cite: updated ", pckg.cite.file, "."))
    if("html" %in% actions){
      cite.obj.html <- roxy.html.cite(
        cite.obj=eval(parse(text=cite.obj)),
        page.css="../web.css",
        package=pck.package,
        imprint=html.options[["imprint"]],
        privacy.policy=html.options[["privacy.policy"]]
      )
      cat(cite.obj.html, file=pckg.cite.file.html)
      message(paste0("cite: updated ", pckg.cite.file.html, "."))
    } else {}
  } else {}

  if("log" %in% actions){
    if(!file.exists(src.changelog)){
      newLog <- initChangeLog(entry=ChangeLog, package=pck.package, version=pck.version, date=pck.date)
      writeChangeLog(log=newLog, file=src.changelog)
      message("log: generated initial ChangeLog")
    } else {
      oldLog <- readChangeLog(src.changelog)
      newLog <- updateChangeLog(oldLog, entry=ChangeLog, version=pck.version, date=pck.date, append=TRUE)
      newLogEntry <- getChangeLogEntry(newLog, pck.version)
      if(!identical(oldLog, newLog)){
        writeChangeLog(log=newLog, file=src.changelog)
        message("log: updated ChangeLog")
        print(newLogEntry)
      } else {
        message("log: ChangeLog is up-to-date, skipped")
      }
    }
  } else {}

  ## generate vignette stub
  if("vignette" %in% actions){
    if(file.exists(pckg.vign.file)){
      warning("vignette: Rmd file exists, please remove it if you want it re-written!", call.=FALSE)
    } else {
      createMissingDir(dirPath=pckg.vign.dir, action="vignette")
      stub_text <- vignette_stub(
        preamble=NULL,
        txt_body=paste0(
          "\n# A Section\n\n",
          "## A Subsection\n\n",
          "* Level 1\n",
          "  + Level 2\n\n...\n\n",
          "Don't forget to add 'knitr,rmarkdown' to the 'Suggests' field and 'VignetteBuilder=\"knitr\"' to your package description!\n"
        ),
        flattr_id=html.options[["flattr.id"]],
        R.dscrptn=pck.description
      )
      cat(stub_text, file=pckg.vign.file)
      message(paste0("vignette: stub file written to ", pckg.vign.file))
      warning("vignette: don't forget to add 'knitr,rmarkdown' to the 'Suggests' field and 'VignetteBuilder=\"knitr\"' to your package description!", call.=FALSE)
    }
  }

  ## update docs (reference manual and vignettes)
  if("doc" %in% actions){
    # had to move this down here, because tools::pkgVignettes() and tools::buildVignettes()
    # don't work without an existing DESCRIPTION file since R 3.0.1... hooooray...
    # makes sense because buildVignettes() needs to know the VignetteBuilder from DESCRIPTION

    # any vignettes?
    pckg.vigns <- tools::pkgVignettes(dir=pck.source.dir)
    if(length(pckg.vigns$docs) > 0){
      # build vignettes and move them (partly inspired by devtools::build_vignettes)

      # create vignettes and gather fresh information
      tools::buildVignettes(dir=pck.source.dir, tangle=TRUE)
      pckg.vigns <- tools::pkgVignettes(dir=pck.source.dir, output=TRUE, source=TRUE)

      # only do the moving if any vignettes were built
      if(length(pckg.vigns$outputs) > 0){
        message("doc: build vignettes ", paste(basename(pckg.vigns$outputs), collapse=', '))

        # what to move and where to move it to
        what.mv <- unique(c(pckg.vigns$outputs, unlist(pckg.vigns$sources, use.names=FALSE)))
        what.cp <- pckg.vigns$docs
        # filter files according to .install_extras
        inst_extras_path <- file.path(pckg.vigns$dir, ".install_extras")
        what.mv <- keep_files(files=what.mv, install_extras=inst_extras_path)
        what.cp <- keep_files(files=what.cp, install_extras=inst_extras_path)
        to.bin.doc <- file.path(R.libs, pck.package, "doc")
        to.src.inst.doc <- file.path(pck.source.dir, "inst", "doc")

        # copy to bin-package
        createMissingDir(to.bin.doc, action="doc")
        if(length(what.mv) > 0){
          stopifnot(file.copy(what.mv, to.bin.doc, overwrite=TRUE))
        } else {}
        if(length(what.cp) > 0){
          stopifnot(file.copy(what.cp, to.bin.doc, overwrite=TRUE))
        } else {}

        ## copy to repo/website
        stopifnot(file.copy(pckg.vigns$outputs, repo.pckg.info, overwrite=TRUE))
        message("doc: updated vignettes ", paste(basename(pckg.vigns$outputs), collapse=', '), " in repo")

        # copy to src-package's inst/doc if not already there (this is the case if they live in directory vignettes)
        if(!grepl("inst/doc$", pckg.vigns$dir)){
          createMissingDir(to.src.inst.doc, action="doc")
          if(length(what.mv) > 0){
            stopifnot(file.copy(what.mv, to.src.inst.doc, overwrite=TRUE))
            stopifnot(file.remove(what.mv))
          } else {}
          if(length(what.cp) > 0){
            stopifnot(file.copy(what.cp, to.src.inst.doc, overwrite=TRUE))
          } else {}
        } else {
          # mimicking previous implementation
          if(isTRUE(rm.vignette)){
            if(length(what.mv) > 0){
              stopifnot(file.remove(what.mv))
            } else {}
          } else {}
        }
      } else {
        warning("doc: couldn't build all vignettes", call.=FALSE)
      }
    } else {}

    # do the reference manual
    pdf.docs <- file.path(repo.pckg.info, pckg.pdf.doc)
    removeIfExists(filePath=pdf.docs)
    if(isTRUE(unix.OS)){
      r.cmd.doc.call <- paste0(clean.env.unix, R.bin, " CMD ", Rcmd.cmd.Rd2pdf, " ", Rcmd.opt.Rd2pdf, "--output=", pdf.docs, " ", pck.source.dir)
      system(r.cmd.doc.call, ignore.stdout=TRUE, ignore.stderr=TRUE, intern=FALSE)
    } else {
      r.cmd.doc.call <- paste0(clean.env.win, R.bin, " CMD ", Rcmd.cmd.Rd2pdf, " ", Rcmd.opt.Rd2pdf, "--output=", shQuote(pdf.docs, type="cmd"), " ", shQuote(pck.source.dir, type="cmd"))
      shell(r.cmd.doc.call, translate=TRUE, ignore.stderr=TRUE, intern=FALSE)
    }
    message("doc: created PDF docs")
  } else {}

  if(any(c("package", "binonly") %in% actions)){
    ## fill source repo
    createMissingDir(dirPath=repo.src.contrib, action="repo")
    # if the package uses 'VignetteBuilder' we do need to rebuild vignettes
    # because otherwise the resulting tarball will be missing the vignette index
    if(
      all(
        "package" %in% actions,
        !"binonly" %in% actions,
        "VignetteBuilder" %in% names(pckg.dscrptn),
        grepl("--no-build-vignettes", Rcmd.opt.build)
      )
    ){
      if("buildVignettes" %in% actions){
        message("build: dropping '--no-build-vignettes' from build options to force generation of vignette index")
        Rcmd.opt.build <- gsub("--no-build-vignettes", "", Rcmd.opt.build)
      } else {
        warning("build: 'VignetteBuilder' is specified but '--no-build-vignettes' set; run with \"buildVignettes\" action to force generation of vignette index!", call.=FALSE)
      }
    } else {}
    ## TODO: find a solution without setwd()
    jmp.back <- getwd()
    pck.source.dir.parent <- dirname(file.path(pck.source.dir))
    setwd(pck.source.dir.parent)
    if(isTRUE(unix.OS)){
      r.cmd.build.call <- paste0(clean.env.unix, set.env.unix, R.bin, " CMD build ", Rcmd.opt.build, pck.source.dir)
      system(r.cmd.build.call, intern=TRUE)
    } else {
      r.cmd.build.call <- paste0(clean.env.win, set.env.win, R.bin, " CMD build ", Rcmd.opt.build, shQuote(pck.source.dir, type="cmd"))
      shell(r.cmd.build.call, translate=TRUE, ignore.stderr=TRUE, intern=TRUE)
    }
    if(!"binonly" %in% actions){
      file.mv(from=file.path(pck.source.dir.parent,pckg.name.src), to=repo.src.gz, overwrite=TRUE)
      message(paste0("repo: copied ", pckg.name.src, " to src/contrib."))
    } else {}
    setwd(jmp.back)
    # install.packages() doesn't work if we want to build for/with other R installations than
    # the actual running one, so we'll use  R CMD INSTALL instead
    if(isTRUE(unix.OS)){
      r.cmd.install.call <- paste0(clean.env.unix, R_libs_append_unix, R.bin, " CMD INSTALL -l ", R.libs, " ",
        Rcmd.opt.install, pck.source.dir)
      system(r.cmd.install.call, intern=TRUE)
    } else {
      r.cmd.install.call <- paste0(clean.env.win, R_libs_append_win, R.bin, " CMD INSTALL -l ", shQuote(R.libs, type="cmd"), " ",
        Rcmd.opt.install, shQuote(pck.source.dir, type="cmd"))
      shell(r.cmd.install.call, translate=TRUE, ignore.stderr=TRUE, intern=TRUE)
    }
    message("build: built and installed package")

    if(!"binonly" %in% actions){
      tools::write_PACKAGES(dir=repo.src.contrib, type="source", verbose=TRUE, latestOnly=FALSE)
      message("repo: updated src/contrib/PACKAGES (source)")

      ## update ChangeLog
      if(file.exists(src.changelog)){
        stopifnot(file.copy(src.changelog, pckg.changelog, overwrite=TRUE))
        message("pckg: updated ChangeLog")
      } else {}
    } else {}
  } else {}

  ## create NEWS.Rd from ChangeLog
  if("cl2news" %in% actions){
    createMissingDir(dirPath=pckg.inst.dir, action="news")
    cl2news(log=src.changelog, news=pckg.NEWS.Rd, codify=TRUE, overwrite=TRUE)
  } else {}

  ## create RSS feed from NEWS.Rd
  add.RSS <- FALSE
  if("news2rss" %in% actions){
    if(is.null(URL)){
      warning("news: no URL specified, RSS feed creation was skipped!", call.=FALSE)
    } else {
      package.URL <- paste(gsub("/*$", "", getURL(URL, purpose="default")), "pckg", pck.package, sep="/")
      RSS.atom.URL <- paste(package.URL, RSS.file.name, sep="/")
      news2rss(
        news=pckg.NEWS.Rd, rss=pckg.NEWS.rss, html=FALSE, encoding="UTF-8",
        channel=c(
          title=pck.package,
          link=package.URL,
          description=rss.description,
          atom=RSS.atom.URL))
      add.RSS <- TRUE
    }
  } else {}

  ## fill windows repo
  if("win" %in% actions){
    createMissingDir(dirPath=repo.win, action="repo")
    removeIfExists(filePath=win.package)
    ## TODO: find a solution without sedwd()
    jmp.back <- getwd()
    setwd(R.libs)
    # make a list of backup files to exclude
    win.exclude.files <- unique(list.files(pck.package, pattern=".*~$", recursive=TRUE))
    if(length(win.exclude.files) > 0){
      win.exclude.files <- paste0("-x \"", paste(file.path(pck.package, win.exclude.files), collapse="\" \""), "\"")
    } else {}
    # windows packages also include an MD5 file, so we'll create one and remove it again
    win.MD5.path <- file.path(R.libs, pck.package)
    installMD5sums(pkgDir=win.MD5.path)
    suppressWarnings(zip(win.package, pck.package, extras=win.exclude.files))
    unlink(file.path(win.MD5.path, "MD5"), recursive=FALSE) # clean up the MD5 file again
    message(paste0("repo: created ", pckg.name.win, " (windows)"))
    setwd(jmp.back)
    tools::write_PACKAGES(dir=repo.win, type="win.binary", verbose=TRUE, latestOnly=FALSE)
    message("repo: updated bin/PACKAGES (windows)")
  } else {}

  ## fill macosx repo
  if("macosx" %in% actions){
    # sanity check
    if(nchar(OSX.repo[["main"]]) > 0){
      createMissingDir(dirPath=repo.macosx.R, action="repo")
      if(!is.null(OSX.repo[["symlinks"]])){
        for (thisSymlink in OSX.repo[["symlinks"]]){
          thisSymPathRoot <- file.path(repo.macosx, thisSymlink)
          thisSymPath <- file.path(thisSymPathRoot, "contrib")
          if(isTRUE(unix.OS)){
            if(!file.exists(thisSymPath)){
              jmp.back <- getwd()
              message(paste0("repo: creating symbolic link ", thisSymPath))
              # if we create the symlink directly, some use cases (like github project pages)
              # get into trouble because of infinite link loops. so we first create the symlink directory
              if(!file.exists(thisSymPathRoot)){
                setwd(repo.macosx)
                r.cmd.symfldr.call <- paste0(Sys.which("mkdir"), " ", thisSymlink)
                system(r.cmd.symfldr.call, ignore.stdout=TRUE, ignore.stderr=TRUE, intern=FALSE)
              } else {}
              # now create the actual symlink in the newly created directory
              setwd(file.path(repo.macosx, thisSymlink))
              r.cmd.symlink.call <- paste0(Sys.which("ln"), " -s ../contrib .")
              system(r.cmd.symlink.call, ignore.stdout=TRUE, ignore.stderr=TRUE, intern=FALSE)
              setwd(jmp.back)
            } else {}
          } else {
            message(paste0("repo: skip creating symbolic link ", thisSymPath, " (windows)"))
          }
        }
      } else {}
    } else {
      stop(simpleError("repo: you *must* provide a character string for \"main\" via OSX.repo (mac)!"))
    }
    removeIfExists(filePath=macosx.package)
    # since not all tar implementations (especially the BSD default on Mac OS X) support --exclude-vcs,
    # we'll exclude these manually
    tar.extraFlags <- excludeVCSDirs(
      src=file.path(R.libs,pck.package),
      exclude.dirs=c(".svn", "CVS", ".git", "_darcs", ".hg"),
      action="repo", target="mac binary"
    )
    if(isTRUE(unix.OS)){
      # make more portable archives, should work with GNU and BSD tar
      tar.extraFlags <- paste0(tar.extraFlags, " --format=ustar")
    } else {}
    # failsafe exclusion of backup files
    # --exclude=*\\~ caused trouble for path names with tilde
    tilde.allFiles <- list.files(file.path(R.libs,pck.package))
    tilde.excludeFiles <- tilde.allFiles[grepl(paste(paste0(".*~$"), collapse="|"), tilde.allFiles)]
    if(length(tilde.excludeFiles) > 0){
      tar.extraFlags <- paste(paste0(" --exclude='", tilde.excludeFiles, "'"), tar.extraFlags, collapse="")
      message(paste("repo: excluded these files from the mac binary:\n  ", tilde.excludeFiles, collapse="\n  "))
    } else {}

    ## TODO: find a solution without sedwd()
    jmp.back <- getwd()
    setwd(R.libs)
    # it seems that tar() in R 3.3 produces archives that contain files twice and
    # cannot be unpacked with OS X's bsdtar, so we'll try this manually
    #     tar(macosx.package, files=pck.package,
    #       tar=Sys.which("tar"),
    #       compression="gzip", extra_flags=paste("-h ", tar.extraFlags))
    mac.tar.call <- paste0(Sys.which("tar"), " -czhf ", macosx.package, " ", tar.extraFlags, " ", pck.package)
    system(mac.tar.call, ignore.stdout=TRUE, ignore.stderr=TRUE, intern=FALSE)
    message(paste0("repo: created ", pckg.name.mac, " (mac OS X)"))
    setwd(jmp.back)
    tools::write_PACKAGES(dir=repo.macosx.R, type="mac.binary", verbose=TRUE, latestOnly=FALSE)
    message("repo: updated bin/PACKAGES (mac OS X)")
  } else {}

  ## fill debian repo
  if("deb" %in% actions){
    formals(debianize) <- deb.defaults
    debianize()
  } else {}

  ## update HTML index
  if("html" %in% actions){
    if(!"roxy" %in% actions){
      # copy DESCRIPTION to pckg directory
      stopifnot(file.copy(file.path(pck.source.dir, "DESCRIPTION"), file.path(repo.pckg.info, "DESCRIPTION"), overwrite=TRUE))
    } else {}
    # write CSS file, if none is present
    css.file <- file.path(repo.pckg.info.main, "web.css")
    if(!file_test("-f", css.file)){
      cat(rx.css(), file=css.file)
      message(paste0("html: created CSS file ", css.file))
    } else {}
    # copy RSS image, if not present
    RSS.image <- file.path(repo.pckg.info.main, "feed-icon-14x14.png")
    if(!file_test("-f", RSS.image)){
      RSS.local.image <- file.path(fetch_inst_from_package("images"), "feed-icon-14x14.png")
      stopifnot(file.copy(RSS.local.image, RSS.image))
      message(paste0("html: copied RSS image to ", RSS.image))
    } else {}
    # check for binaries to link
    url.src <- url.win <- url.mac <- url.deb <- url.doc <- url.vgn <- title.vgn <- deb.repo <- NULL
    if(file_test("-f", repo.src.gz)){
      url.src <- pckg.name.src
    } else {}
    if(file_test("-f", win.package)){
      url.win <- binPackageLinks(package=pck.package, version=pck.version, repo.root=repo.root, type="win")[["win"]]
    } else {}
    if(file_test("-f", macosx.package)){
      url.mac <- binPackageLinks(package=pck.package, version=pck.version, repo.root=repo.root, type="mac")[["mac"]]
    } else {}
    url.debRepo.info <- NULL
    if(file_test("-f", deb.package)){
      if(!is.null(URL)){
        # generate repository info
        url.debRepo.info <- file.path(repo.pckg.info, "deb_repo.html")
        cat(debRepoInfo(
          URL=URL,
          dist=deb.defaults[["distribution"]],
          comp=deb.defaults[["component"]],
          arch=deb.defaults[["arch"]],
          version=pck.version,
          revision=deb.defaults[["revision"]],
          compression=deb.defaults[["compression"]],
          repo=deb.defaults[["origin"]],
          repo.name=deb.defaults[["repo.name"]],
          repo.root=repo.root,
          package=pckg.name.deb.part,
          keyring.options=deb.defaults[["keyring.options"]],
          page.css="../web.css",
          package.full=pckg.name.deb,
          imprint=html.options[["imprint"]],
          privacy.policy=html.options[["privacy.policy"]],
          deb.dir=deb.defaults[["deb.dir"]]
        ),
        file=url.debRepo.info)
        message(paste0("html: updated ", url.debRepo.info))
      } else {
        message("html: you need to specify 'URL' to generate debian repository information!")
      }
    } else {}
    # check if there is actually any built debian package in the repo
    # if not, the link to the debian installation notes will be omitted
    deb.built.packages <- dir(file.path(repo.root, deb.repo.path.part), pattern=paste0(pckg.name.deb.part, ".*", ".deb"))
    url.deb.repo <- NULL
    if(length(deb.built.packages) > 0){
      url.deb.repo <- "deb_repo.html"
    } else {}
    # check for docs to link
    pdf.docs <- file.path(repo.pckg.info, pckg.pdf.doc)
    # gather information (title, file name) about built vignettes
    vig.info <- tools::getVignetteInfo(package=pck.package, lib.loc=R.libs)
    if(file_test("-f", pdf.docs)){
      url.doc <- pckg.pdf.doc
    } else {}
    if(nrow(vig.info) > 0){
      if(all( c('Title', 'PDF') %in% colnames(vig.info))){
        url.vgn <- vig.info[, "PDF"]
        title.vgn <- vig.info[, "Title"]
      } else{
        stop(
          simpleError(
            paste(
              "doc: the output format of tools::getVignetteInfo()", 
              "is not compatible with this version of roxyPackage.",
              "Please consider filing a bug report!"
            )
          )
        )
      }
    } else {}
    # treat markdown files (README.md and NEWS.md) if found
    for(mdfile in c("README", "NEWS")){
      this_md <- file.path(pck.source.dir, paste0(mdfile, ".md"))
      if(file.exists(this_md)){
        this_html <- file.path(repo.pckg.info, paste0(mdfile, ".html"))
        removeIfExists(filePath=this_html)
        pandoc_success <- pandoc(
          infile=this_md,
          outfile=this_html,
          title=paste0(pck.package, ": ", mdfile)
        )
        if(isTRUE(pandoc_success)){
          message(paste0("html: created ", mdfile, ".html from markdown document"))
        } else {
          warning(paste0("html: something went wrong while compiling ", mdfile, ".html from markdown document!"))
        }
      } else {}
    }
    # check for NEWS.Rd or NEWS file
    if(file_test("-f", pckg.NEWS.Rd)){
      roxy.NEWS2HTML(newsRd=pckg.NEWS.Rd, newsHTML=file.path(repo.pckg.info, "NEWS.html"), pckg=pck.package, css="../web.css")
      url.NEWS <- pckg.NEWS.html
    } else if(file_test("-f", pckg.NEWS.inst)){
      stopifnot(file.copy(pckg.NEWS.inst, file.path(repo.pckg.info, "NEWS"), overwrite=TRUE))
      url.NEWS <- file.path(repo.pckg.info, "NEWS")
    } else if(file_test("-f", pckg.NEWS)){
      stopifnot(file.copy(pckg.NEWS, file.path(repo.pckg.info, "NEWS"), overwrite=TRUE))
      url.NEWS <- file.path(repo.pckg.info, "NEWS")
    } else if(file_test("-f", pckg.NEWS.html)){
      url.NEWS <- file.path(repo.pckg.info, "NEWS.html")
    } else {
      url.NEWS <- ""
    }
    # generate package index file
    if(!file_test("-f", pckg.NEWS.rss)){
      RSS.file.name <- NULL
    } else {}
    package.html <- roxy.html(
      pckg.dscrptn,
      index=FALSE,
      css="web.css",
      R.version=R.Version.win,
      url.src=url.src,
      url.win=url.win,
      url.mac=url.mac,
      url.doc=url.doc,
      url.vgn=url.vgn,
      title.vgn=title.vgn,
      url.deb.repo=url.deb.repo,
      main.path.mac=OSX.repo[["main"]],
      title=html.options[["title"]],
      cite=pckg.cite.file.html,
      news=url.NEWS,
      changelog=pckg.changelog,
      readme=pckg.readme.html,
      rss.file=RSS.file.name,
      flattr.id=html.options[["flattr.id"]],
      URL=getURL(URL, purpose="default"),
      imprint=html.options[["imprint"]],
      privacy.policy=html.options[["privacy.policy"]]
    )
    # make sure there's an ORCID icon should we need one
    check_orcid_icon(pckg=pckg.dscrptn, repo=repo.pckg.info.main)
    target.file.pckg <- file.path(repo.pckg.info, "index.html")
    cat(package.html, file=target.file.pckg)
    message(paste0("html: updated ", target.file.pckg))
    # now generate the global index file by scanning for DESCRIPTIONs in all pckg folders
    all.descs <- list()
    for (this.elmt in file.path(repo.pckg.info.main, dir(repo.pckg.info.main))){
        desc.path <- file.path(this.elmt, "DESCRIPTION")
        if(file_test("-f", desc.path)){
          # ok, there a DESCRIPTION file
          all.descs[[length(all.descs) + 1]] <- read.dcf(desc.path)
        } else {}
      }
    target.file.pckg <- file.path(repo.pckg.info.main, "index.html")
    pckg.index.html <- roxy.html(
      all.descs,
      index=TRUE,
      css="web.css",
      title=html.options[["index"]],
      flattr.id=html.options[["repo.flattr.id"]],
      imprint=html.options[["imprint"]],
      privacy.policy=html.options[["privacy.policy"]]
    )
    cat(pckg.index.html, file=target.file.pckg)
    message(paste0("html: updated pckg index ", target.file.pckg))
    target.file.glob <- file.path(repo.root, "index.html")
    global.html <- roxy.html(
      all.descs,
      index=TRUE,
      css="web.css",
      title=html.options[["index"]],
      redirect="pckg/",
      flattr.id=html.options[["repo.flattr.id"]],
      imprint=html.options[["imprint"]],
      privacy.policy=html.options[["privacy.policy"]]
    )
    cat(global.html, file=target.file.glob)
    message(paste0("html: updated global index ", target.file.glob))
  } else {}

  if("check" %in% actions){
    # check for examples check file before
    chk.ex.file <- file.path(pck.source.dir, paste0(pck.package, "-Ex.R"))
    chk.ex.file.present <- ifelse(file_test("-f", chk.ex.file), TRUE, FALSE)
    tryCatch(chk.out.dir <- tempdir(), error=function(e) stop(e))
    # checks should better be performed on built packages not source directories
    if(all("package" %in% actions, file_test("-f", repo.src.gz))){
      pck.check.target <- repo.src.gz
    } else {
      warning(
        paste0(
          "check: checks should be done on built packages, not the unpackaged source. ",
          "to do so, add the \"package\" action."
        ),
        call.=FALSE
      )
      pck.check.target <- pck.source.dir
    }
    message(paste0("check: calling R CMD check, this might take a while..."))
    if(isTRUE(unix.OS)){
      r.cmd.check.call <- paste0("R_LIBS_USER=", R.libs, " ; ",
        R.bin, " CMD check --output=", chk.out.dir, " ", Rcmd.opt.check, pck.check.target)
      print(system(r.cmd.check.call, intern=TRUE))
    } else {
      r.cmd.check.call <- paste0("set R_LIBS_USER=", shQuote(R.libs, type="cmd"), " && ",
        R.bin, " CMD check --output=", chk.out.dir, " ", Rcmd.opt.check, shQuote(pck.check.target, type="cmd"))
      print(shell(r.cmd.check.call, translate=TRUE, intern=TRUE))
    }
    on.exit(
      message(paste0("check: saved results to ", chk.out.dir, "/", pck.package, ".Rcheck")),
      add=TRUE
    )
    # need to clean up?
    if(!isTRUE(chk.ex.file.present) & file_test("-f", chk.ex.file)){
      # there's an example file which wasn't here before
      unlink(chk.ex.file)
    } else {}
  } else {}

  return(invisible(NULL))
} ## end function roxy.package()
unDocUMeantIt/roxyPackage documentation built on July 26, 2024, 5:57 p.m.