require("tools", quietly = TRUE) require("utils", quietly = TRUE) ## ## Design issues. ## * For also checking reverse dependencies, we need a way to specify ## 'which' such dependencies to take, whether to include recursive ## reverse dependencies or not, and which repositories to use for the ## reverse dependencies. ## The current mechanism is to have argument 'reverse' as a list with ## names partially matching 'which', 'recursive' and 'repos' and ## suitable default values; alternatively, giving 'TRUE' (currently) ## also turns on reverse dependency checking with the defaults. ## * When reverse dependencies are also checked, we may need to have one ## set of default check args for the packages, and one for the reverse ## dependencies [example: '--as-cran' for the former, but not for the ## latter]. This can be achieved by giving 'check_args' a *list* of ## length 2. ## * A similar mechanism applies for 'check_env'. ## ## ## Should we use ## 00_Library ## 00_Outputs ## 00_rdepends ## ??? ## check_packages_in_dir <- function(dir, check_args = character(), check_args_db = list(), reverse = NULL, check_env = character(), xvfb = TRUE, Ncpus = getOption("Ncpus", 1L), clean = TRUE, ...) { owd <- getwd() dir <- normalizePath(dir) setwd(dir) on.exit(setwd(owd)) pfiles <- Sys.glob("*.tar.gz") if(!length(pfiles)) { message("no packages to check") return(invisible()) } pnames <- sub("_.*", "", pfiles) os_type <- .Platform$OS.type ## Xvfb usage and options. ## We do not use Xvfb on Windows. ## Otherwise, if argument 'xvfb' is ## * a logical, Xvfb is used only if identical to TRUE; ## * something else, then as.character(xvfb) gives the Xvfb options. xvfb_options <- "-screen 0 1280x1024x24" if(os_type == "windows") xvfb <- FALSE else if(is.logical(xvfb)) { if(!identical(xvfb, TRUE)) xvfb <- FALSE } else { xvfb_options <- as.character(xvfb) xvfb <- TRUE } curl <- if(os_type == "windows") sprintf("file:///%s", dir) else sprintf("file://%s", dir) libdir <- file.path(dir, "Library") dir.create(libdir, showWarnings = FALSE) outdir <- file.path(dir, "Outputs") dir.create(outdir, showWarnings = FALSE) ## Determine packages using fake/no install for checking. ## Handle these as follows: ## * For packages using '--install=no', forward dependencies do not ## need to installed, and reverse dependencies do not need to be ## checked. ## * For packages using '--install=fake', forward dependencies must ## be available for checking, and checking reverse dependencies ## makes sense (e.g, to spot missing Rd xrefs). pnames_using_install_no <- character() pnames_using_install_fake <- character() check_args_db <- as.list(check_args_db) if(length(check_args_db) && !is.null(nms <- names(check_args_db))) { args <- lapply(check_args_db, function(e) scan(text = e, what = character(), quiet = TRUE)) pnames_using_install_no <- nms[sapply(args, function(e) any(e == "--install=no"))] pnames_using_install_fake <- nms[sapply(args, function(e) any(e == "--install=fake"))] } else { ## If check_args_db has no names it is useless. ## Perhaps complain? check_args_db <- list() } ## Build a package db from the source packages in the working ## directory. tools::write_PACKAGES(dir, type = "source") curls <- c(curl, contrib.url(getOption("repos"), type = "source")) available <- available.packages(contriburl = curls, type = "source") ## As of c52164, packages with OS_type different from the current ## one are *always* checked with '--install=no'. ## These packages are also filtered out by default (via the OS_type ## filter) from the repository package computations. ## Hence move packages in the install=fake list not listed by ## available.packages() to the install=no list. pnames_using_install_no <- c(pnames_using_install_no, setdiff(pnames_using_install_fake, available[, "Package"])) pnames_using_install_fake <- intersect(pnames_using_install_fake, available[, "Package"]) if(!is.null(reverse) && !identical(reverse, FALSE)) { ## Determine and download reverse dependencies to be checked as ## well. reverse <- as.list(reverse) ## Merge with defaults, using partial name matching. defaults <- list(which = c("Depends", "Imports", "LinkingTo"), recursive = FALSE, repos = getOption("repos")) pos <- pmatch(names(reverse), names(defaults), nomatch = 0L) defaults[pos] <- reverse[pos > 0L] rnames <- tools::package_dependencies(setdiff(pnames, pnames_using_install_no), available, which = defaults$which, recursive = defaults$recursive, reverse = TRUE) rnames <- intersect(unlist(rnames, use.names = FALSE), available[, "Package"]) rnames <- setdiff(rnames, pnames) pos <- match(rnames, available[, "Package"], nomatch = 0L) if(!identical(defaults$repos, getOption("repos"))) { pos <- split(pos[pos > 0L], available[pos, "Repository"]) ## Only want the reverse dependencies for which Repository ## is pmatched by contrib.url(defaults$repos). nms <- names(pos) pos <- unlist(pos[unique(c(outer(defaults$repos, nms, pmatch, nomatch = 0L)))], use.names = FALSE) } rnames <- available[pos, "Package"] rfiles <- sprintf("%s_%s.tar.gz", rnames, available[pos, "Version"]) if(length(rfiles)) { message("downloading reverse dependencies ...") rfurls <- sprintf("%s/%s", available[pos, "Repository"], rfiles) for(i in seq_along(rfiles)) { message(sprintf("downloading %s ... ", rfiles[i]), appendLF = FALSE) status <- if(!download.file(rfurls[i], rfiles[i])) "ok" else "failed" message(status) } message("") } } else { rfiles <- rnames <- character() } pnames <- c(pnames, rnames) ## Install what is needed. depends <- tools::package_dependencies(pnames, available, which = "most") depends <- setdiff(unique(unlist(depends, use.names = FALSE)), unlist(tools:::.get_standard_package_names(), use.names = FALSE)) ## Need to install depends which are not installed or installed but ## old. libs <- c(libdir, .libPaths()) installed <- installed.packages(libs)[, "Package"] depends <- c(setdiff(depends, installed), intersect(intersect(depends, installed), old.packages(libs, contriburl = curls)[, "Package"])) if(length(depends)) { message(paste(strwrap(sprintf("installing dependencies %s", paste(sQuote(depends), collapse = ", ")), exdent = 2L), collapse = "\n")) ## ## Ideally we would capture stdout and stderr in e.g. ## outdir/install_stdout.txt ## outdir/install_stderr.txt ## But using several CPUs uses Make to install, which seems to ## write to stdout/stderr "directly" ... so using sink() will ## not work. message("") ## ## Since c61934, argument 'INSTALL_opts' to install.packages() ## can be a named list of package specific options. ## Remove old code separating full and fake installs eventually. if(as.numeric(R.version[["svn rev"]]) >= 61934) { iflags <- as.list(rep.int("--fake", length(pnames_using_install_fake))) names(iflags) <- pnames_using_install_fake install.packages(depends, lib = libdir, contriburl = curls, available = available, dependencies = TRUE, INSTALL_opts = iflags, Ncpus = Ncpus, type = "source") } else { if(length(pkgs <- setdiff(depends, pnames_using_install_fake))) install.packages(pkgs, lib = libdir, contriburl = curls, available = available, dependencies = TRUE, Ncpus = Ncpus, type = "source") if(length(pkgs <- intersect(depends, pnames_using_install_fake))) install.packages(pkgs, lib = libdir, contriburl = curls, available = available, dependencies = TRUE, INSTALL_opts = "--fake", Ncpus = Ncpus, type = "source") } ## message("") ## } ## Merge check_args and check_args_db into check_args_db used for ## checking. check_args <- if(is.list(check_args)) { c(rep.int(list(check_args[[1L]]), length(pfiles)), rep.int(list(check_args[[2L]]), length(rfiles))) } else { rep.int(list(check_args), length(pnames)) } check_args_db <- check_args_db[pnames] check_args_db <- Map(c, check_args, check_args_db) names(check_args_db) <- pnames check_env <- if(is.list(check_env)) { c(rep.int(list(check_env[[1L]]), length(pfiles)), rep.int(list(check_env[[2L]]), length(rfiles))) } else { rep.int(list(check_env), length(pnames)) } ## No user level check_env_db for now. check_env_db <- as.list(check_env) names(check_env_db) <- pnames pfiles <- c(pfiles, rfiles) check_package <- function(pfile, args_db = NULL, env_db = NULL) { message(sprintf("checking %s ...", pfile)) pname <- sub("_.*", "", basename(pfile)) out <- file.path(outdir, sprintf("check_%s_stdout.txt", pname)) err <- file.path(outdir, sprintf("check_%s_stderr.txt", pname)) env <- c(check_env_db[[pname]], sprintf("R_LIBS=%s", shQuote(libdir))) system.time(system2(file.path(R.home("bin"), "R"), c("CMD", "check", "--timings", args_db[[pname]], pfile), stdout = out, stderr = err, env = env)) } if(xvfb) { pid <- start_virtual_X11_fb(xvfb_options) on.exit(close_virtual_X11_db(pid), add = TRUE) } if(Ncpus > 1L) { if(os_type != "windows") { timings <- parallel:::mclapply(pfiles, check_package, check_args_db, check_env_db, mc.cores = Ncpus) } else { cl <- parallel::makeCluster(Ncpus) timings <- parallel::parLapply(cl, pfiles, check_package, check_args_db, check_env_db) parallel::stopCluster(cl) } } else { timings <- lapply(pfiles, check_package, check_args_db, check_env_db) } timings <- do.call(rbind, lapply(timings, summarize_proc_time)) rownames(timings) <- pnames write.table(timings, "timings.tab") file.rename(sprintf("%s.Rcheck", rnames), sprintf("rdepends_%s.Rcheck", rnames)) if(clean) { file.remove(rfiles) } else { file.rename(rfiles, sprintf("rdepends_%s", rfiles)) } invisible(pfiles) } start_virtual_X11_fb <- function(options) { ## Determine the display number from the options, or the PID of the ## current R process (alternatively, could mimic xvfb-run). args <- scan(text = options, what = character(), quiet = TRUE) ind <- grepl("^:[[:digit:]]+$", args) if(any(ind)) { num <- args[ind][1L] } else { num <- paste0(":", Sys.getpid()) options <- c(num, options) } dis <- Sys.getenv("DISPLAY", unset = NA) ## We need to start Xvfb with the given options and obtain its pid ## so that we can terminate it when done checking. ## This could be done via ## system2("Xvfb", options, stdout = FALSE, stderr = FALSE, ## wait = FALSE) ## and then determine the pid as ## pid <- scan(text = ## grep(sprintf("Xvfb %s", num), ## system2("ps", "auxw", stdout = TRUE), ## value = TRUE, ## fixed = TRUE), ## what = character(), ## quiet = TRUE)[2L] ## A better approach (suggested by BDR) is to create a shell script ## containing the call to start Xvfb in the background and display ## the pid of this as available in the shell's $! parameter. tf <- tempfile() on.exit(unlink(tf)) writeLines(c(paste(c(shQuote("Xvfb"), options, ">/dev/null 2>&1 &"), collapse = " "), "echo ${!}"), tf) pid <- system2("sh", tf, stdout = TRUE) Sys.setenv("DISPLAY" = num) ## Propagate both pid and original setting of DISPLAY so that the ## latter can be restored when Xvfb is closed. attr(pid, "display") <- dis pid } close_virtual_X11_db <- function(pid) { tools::pskill(pid) if(is.na(dis <- attr(pid, "display"))) Sys.unsetenv("DISPLAY") else Sys.setenv("DISPLAY" = dis) } ## ## c61917 added a summary() method for proc_time objects: use this ## eventually ... summarize_proc_time <- function(x, ...) { if(!is.na(x[4L])) x[1L] <- x[1L] + x[4L] if(!is.na(x[5L])) x[2L] <- x[2L] + x[5L] x <- x[1L : 3L] names(x) <- c(gettext("user"), gettext("system"), gettext("elapsed")) x } ## ## Example use for KH CRAN incoming checks: ## foo <- function() { ## cran <- getOption("repos")["CRAN"] ## check_packages_in_dir("~/tmp/CRAN", ## check_args = list("--as-cran", character()), ## reverse = list(repos = cran), ## env = ## c("LC_ALL=en_US.UTF-8", ## "_R_CHECK_WARN_BAD_USAGE_LINES_=TRUE"), ## Ncpus = 4) ## } R_check_outdirs <- function(dir, all = FALSE) { dir <- normalizePath(dir) outdirs <- dir(dir, pattern = "\\.Rcheck") if(!all) outdirs <- outdirs[!grepl("^rdepends_", outdirs)] file.path(dir, outdirs) } summarize_check_packages_in_dir_depends <- function(dir, all = FALSE) { for(d in R_check_outdirs(dir, all = all)) { dfile <- Sys.glob(file.path(d, "00_pkg_src", "*", "DESCRIPTION"))[1L] if(file_test("-f", dfile)) { meta <- tools:::.read_description(dfile) has_depends <- !is.na(meta["Depends"]) has_imports <- !is.na(meta["Imports"]) if(has_depends || has_imports) { writeLines(c(sprintf("Package: %s", meta["Package"]), if(has_depends) strwrap(sprintf("Depends: %s", meta["Depends"]), indent = 2L, exdent = 4L), if(has_imports) strwrap(sprintf("Imports: %s", meta["Imports"]), indent = 2L, exdent = 4L))) } } } } summarize_check_packages_in_dir_results <- function(dir, all = TRUE, full = FALSE) { dir <- normalizePath(dir) outdirs <- R_check_outdirs(dir, all = all) ## Re-arrange to have reverse dependencies last. ind <- grepl("^rdepends_", basename(outdirs)) outdirs <- c(outdirs[!ind], outdirs[ind]) for(d in outdirs) { pname <- sub("\\.Rcheck$", "", basename(d)) log <- readLines(file.path(d, "00check.log"), warn = FALSE) m <- regexpr("\\.\\.\\. *(\\[.*\\])? *(NOTE|WARN|ERROR)", log, useBytes = TRUE) ind <- (m > 0L) if(any(ind)) { status <- if(all(grepl("NOTE$", regmatches(log, m), useBytes = TRUE))) { "NOTE" } else "PROBLEM" writeLines(c(sprintf("%s ... %s", pname, status), log[ind])) } else { writeLines(sprintf("%s ... OK", pname)) } } if(full) { writeLines("") source(file.path(R_scripts_dir, "check.R")) inspect_check_details_db(check_details_db(dir)) } } summarize_check_packages_in_dir_timings <- function(dir, all = FALSE, full = FALSE) { dir <- normalizePath(dir) tfile <- file.path(dir, "timings.tab") if(file_test("-f", tfile)) { timings <- read.table(tfile) ## Should we store the information about reverse dependencies in ## some place (rather than rely on the naming convention)? if(!all) { rdepends <- Sys.glob(file.path(dir, "rdepends_*.Rcheck")) timings <- timings[is.na(match(rownames(timings), sub("rdepends_(.*).Rcheck", "\\1", basename(rdepends)))), ] } print(timings) } if(full) { tfiles <- Sys.glob(file.path(R_check_outdirs(dir, all = all), "*-Ex.timings")) if(length(tfiles)) message("") timings <- lapply(tfiles, read.table, header = TRUE) ## Order by CPU time. timings <- lapply(timings, function(x) x[order(x$user, decreasing = TRUE), ]) ## This looks silly, but we want a common alignment. timings <- split(as.data.frame(lapply(do.call(rbind, timings), format)), rep.int(sub("\\.Rcheck$", "", basename(dirname(tfiles))), sapply(timings, nrow))) invisible(Map(function(x, y) { writeLines(sprintf("Example timings for package '%s':", x)) cat(rbind(" ", t(as.matrix(y))), sep = c(" ", " ", " ", " ", "\n")) }, names(timings), timings)) } }