#  File src/library/tools/R/Vignettes.R
#  Part of the R package, https://www.R-project.org
#
#  Copyright (C) 1995-2025 The R Core Team
#
#  This program 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 2 of the License, or
#  (at your option) any later version.
#
#  This program 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.
#
#  A copy of the GNU General Public License is available at
#  https://www.R-project.org/Licenses/

vignette_is_tex <- function(file, ...) {
    endsWith(tolower(file), ".tex")
}

# Infers the vignette type (PDF or HTML) from the filename of the
# final vignette product.
vignette_type <- function(file) {
    ext <- tolower(file_ext(file))
    type <- c(pdf="PDF", html="HTML")[ext]
    if (is.na(type))
        stop(gettextf("Vignette product %s does not have a known filename extension (%s)",
                      sQuote(file), paste(sQuote(names(type)), collapse=", ")),
             domain = NA)
    unname(type)
}

# Locates the vignette weave, tangle and texi2pdf product(s) based on the
# vignette name.   All such products must have the same name as their filename
# prefix (i.e. "^<name>").
# For weave, final = TRUE will look for <name>.pdf and <name>.html, whereas
# with final = FALSE it also looks for <name>.tex (if <name>.pdf is also
# found, it will be returned).  For tangle, main = TRUE will look for <name>.R,
# whereas main = FALSE will look for <name><anything>*.R.
# For texi2pdf, <name>.pdf is located.
find_vignette_product <-
    function(name, by = c("weave", "tangle", "texi2pdf"),
             final = FALSE, main = TRUE, dir = ".", engine, ...)
{
    stopifnot(length(name) == 1L, dir.exists(dir))
    by <- match.arg(by)
    exts <- ## (lower case here):
	switch(by,
	       "weave" = if (final) c("pdf", "html") else c("pdf", "html", "tex"),
	       "tangle" = c("r", "s"),
	       "texi2pdf" = "pdf")

    exts <- c(exts, toupper(exts))
    pattern1 <- sprintf("^%s[.](%s)$", name, paste(exts, collapse = "|"))
    output0 <- list.files(path = dir, no.. = TRUE)
    output0 <- output0[file_test("-f", file.path(dir, output0))]
    output <- grep(pattern1, output0, value = TRUE)
    # If main is FALSE, we want to find all other files with related
    # names.  We make sure that the main file is in position 1.
    # FIXME: we should check a timestamp or something to see that
    #	      these were produced by tangling for the "name" vignette,
    #	      they aren't just coincidentally similar names.
    if (!main) {
	pattern2 <- sprintf("^%s.*[.](%s)$", name, paste(exts, collapse = "|"))
	output2 <- grep(pattern2, output0, value = TRUE)
	output <- c(output, setdiff(output2, output))
    }
    fmt_file_sizes <- function(files)
	paste(sprintf("%s (%g bytes)", sQuote(files), file.size(file.path(dir, files))),
	      collapse=", ")
    if (by == "weave") {
        if (length(output) == 0L)
            stop(gettextf("Failed to locate %s output file %s or %s for vignette with name %s and engine %s. The following files exist in working directory %s: %s",
                          sQuote(by),
                          sQuote(paste0(name, ".pdf")), sQuote(paste0(name, ".html")),
                          sQuote(name),
                          sQuote(sprintf("%s::%s", engine$package, engine$name)),
                          sQuote(normalizePath(dir)),
                          fmt_file_sizes(output0)),
                 domain = NA)
        if (length(output) > 2L || (final && length(output) > 1L))
            stop(gettextf("Located more than one %s output file (by engine %s) for vignette with name %s: %s", sQuote(by),
                          sQuote(sprintf("%s::%s", engine$package, engine$name)),
                          sQuote(name),
                          fmt_file_sizes(output)),
                 domain  = NA)
	# If weave produced a TeX and then a PDF without cleaning out
	# the TeX, consider the newer one (PDF wins a tie) as the weave product
	if (length(output) == 2L) {
	    idxs <- match(tolower(file_ext(output)), exts)
	    output <- output[order(idxs)]
	    if (file_test("-nt", output[2L], output[1L])) output <- output[2L]
	    else output <- output[1L]
        }
    } else if (by == "tangle") {
        if (main)
            stopifnot(length(output) <= 1L)
    } else if (by == "texi2pdf") {
        if (length(output) == 0L)
            stop(gettextf("Failed to locate %s output file %s for vignette with name %s and engine %s. The following files exist in working directory %s: %s",
                          sQuote(by),
                          sQuote(paste0(name, ".pdf")),
                          sQuote(name),
                          sQuote(sprintf("%s::%s", engine$package, engine$name)),
                          sQuote(normalizePath(dir)),
                          fmt_file_sizes(output0)),
                 domain = NA)
        if (length(output) > 1L)
            stop(gettextf("Located more than one %s output file (by engine %s) for vignette with name %s: %s",
                          sQuote(by),
                          sQuote(sprintf("%s::%s", engine$package, engine$name)),
                          sQuote(name),
                          fmt_file_sizes(output)),
                 domain = NA)
    }

    ## return :
    if (length(output) > 0L) {
        if (dir == ".")
            basename(output)
        else
            file.path(dir, output)
    } ## else NULL
}



### * checkVignettes
###
### Run a tangle+source and a weave on all vignettes of a package.

checkVignettes <-
function(package, dir, lib.loc = NULL,
         tangle = TRUE, weave = TRUE, latex = FALSE,
         workdir = c("tmp", "src", "cur"),
         keepfiles = FALSE)
{
    vigns <- pkgVignettes(package = package, dir = dir, lib.loc = lib.loc)
    if(is.null(vigns)) return(NULL)

    workdir <- match.arg(workdir)
    wd <- getwd()
    if (is.null(wd))
        stop("current working directory cannot be ascertained")
    if(workdir == "tmp") {
        tmpd <- tempfile("Sweave")   ## <= Rename?
        if(!dir.create(tmpd))
            stop(gettextf("unable to create temp directory %s ", sQuote(tmpd)),
                 domain = NA)
        setwd(tmpd)
    }
    else {
        keepfiles <- TRUE
        if(workdir == "src") setwd(vigns$dir)
    }

    on.exit({
        setwd(wd)
        if(!keepfiles) unlink(tmpd, recursive = TRUE)
    })

    file.create(".check.timestamp")
    result <- list(tangle = list(), weave = list(),
                   source = list(), latex = list())

    ## pkgVignettes has already done this
    loadVignetteBuilder(vigns$pkgdir)

    startdir <- getwd()
    for(i in seq_along(vigns$docs)) {
        path <- vigns$docs[i]
        file <- basename(path)
        name <- vigns$names[i]
    	engine <- vignetteEngine(vigns$engines[i])
	enc <- vigns$encodings[i]
        if (enc == "non-ASCII")
            stop(gettextf("Vignette '%s' is non-ASCII but has no declared encoding", name),
                 domain = NA)
        if(tangle) {
            message("  Running ", sQuote(file))
            .eval_with_capture({
                result$tangle[[file]] <- tryCatch({
                    engine$tangle(path, quiet = TRUE, encoding = enc)
                    setwd(startdir) # in case a vignette changes the working dir
                    find_vignette_product(name, by = "tangle", main = FALSE, engine = engine)
                }, error = identity)
            })
        }
        if(weave) {
            setwd(startdir) # in case a vignette changes the working dir then errored out
            .eval_with_capture({
                result$weave[[file]] <- tryCatch({
                    engine$weave(path, quiet = TRUE, encoding = enc)
                    setwd(startdir)
                    find_vignette_product(name, by = "weave", engine = engine)
                }, error = identity)
            })
        }
        setwd(startdir) # in case a vignette changes the working dir then errored out
    }

    # Assert that output files were not overwritten
    for (name in c("weave", "tangle")) {
        resultsT <- result[[name]]
        if (length(resultsT) <= 1L)
            next

        for (i in 1L:(length(resultsT)-1L)) {
            outputsI <- resultsT[[i]]
            if (inherits(outputsI, "error"))
                next;
            outputsI <- normalizePath(outputsI)

            for (j in (i+1L):length(resultsT)) {
                 outputsJ <- resultsT[[j]]
                 if (inherits(outputsJ, "error"))
                     next;
                 outputsJ <- normalizePath(outputsJ)
                 bad <- intersect(outputsJ, outputsI)
                 if (length(bad) > 0L) {
                     stop(gettextf("Vignette %s overwrites the following %s output by vignette %s: %s",
                                   sQuote(basename(names(resultsT)[j])),
                                   sQuote(name),
                                   sQuote(basename(names(resultsT)[i])),
                                   paste(basename(bad), collapse=", ")),
                          domain = NA)
                 }
            }
        }
    }

    if(tangle) {
        ## Tangling can create several source files if splitting is on,
        ## and these can be .R or .S (at least).  However, there is
        ## no guarantee that running them in alphabetical order in a
        ## session will work -- with named chunks it normally will not.
        cwd <- getwd()
        if (is.null(cwd))
            stop("current working directory cannot be ascertained")
        for(i in seq_along(result$tangle)) {
            sources <- result$tangle[[i]]
            if (inherits(sources, "error"))
                next
            sources <- sources[file_test("-nt", sources, ".check.timestamp")]
            for(file in sources) {
                .eval_with_capture({
                    result$source[[file]] <- tryCatch({
                        source(file)
                    }, error = identity)
                })
                setwd(startdir)
            }
        }
    }

    if(weave && latex) {
        if("Makefile" %notin% list.files(vigns$dir)) {
            ## <NOTE>
            ## This used to run texi2pdf on *all* vignettes, including
            ## the ones already known from the above to give trouble.
            ## In addition, texi2pdf errors were not caught, so that in
            ## particular the results of the previous QC analysis were
            ## *not* returned in case of such errors ...
            ## Hence, let us
            ## * Only run texi2pdf() on previously unproblematic vignettes
            ## * Catch texi2pdf() errors similar to the above.
            ## * Do *not* immediately show texi2pdf() output as part of
            ##   running checkVignettes().
            ## (For the future, maybe keep this output and provide it as
            ## additional diagnostics ...)
            ## </NOTE>
            for (i in seq_along(result$weave)) {
                file <- names(result$weave)[i]
                output <- result$weave[[i]]
                if (inherits(output, "error"))
                    next
                if (!vignette_is_tex(output))
                    next
                ## Ensure that the vignette dir is in TEX/BIBINPUTS.
                ## This will often fail, however, when checking from an
                ## installed 'package', as bib files are usually not installed
                .eval_with_capture({
                    result$latex[[file]] <- tryCatch({
                       texi2pdf(file = output, clean = FALSE, quiet = TRUE,
                                texinputs = vigns$dir)
                       find_vignette_product(file_path_sans_ext(output),
                                             by = "texi2pdf", engine = engine)
                    }, error = identity)
                })
            }
        }
    }

    # Cleanup results
    for (name in c("tangle", "weave", "source", "latex")) {
        resultsT <- result[[name]]
        resultsT <- lapply(resultsT, FUN = function(res) {
          if (inherits(res, "error"))
              conditionMessage(res)
          else
              NULL
        })
        resultsT <- resultsT[!vapply(resultsT, is.null, NA)]
        result[[name]] <- resultsT
    }

    file.remove(".check.timestamp")
    class(result) <- "checkVignettes"
    result
}

format.checkVignettes <-
function(x, ...)
{
    myfmt <- function(y, title) {
        if(length(y)) {
            paste(c(paste0("\n", title, "\n"),
                    unlist(Map(c,
                               paste0("File ", names(y), ":"),
                               lapply(y, as.character)),
                           use.names = FALSE)),
                  collapse = "\n")
        }
    }
    c(character(),
      myfmt(x$tangle, "*** Tangle Errors ***"),
      myfmt(x$source, "*** Source Errors ***"),
      myfmt(x$weave,  "*** Weave Errors ***"),
      myfmt(x$latex,  "*** PDFLaTeX Errors ***"))
}

### get the engine from a file

getVignetteEngine <- function(filename, lines = readLines(filename, warn=FALSE)) {
    c(.get_vignette_metadata(lines, "Engine"), "utils::Sweave")[1L]
}

### * engineMatches
###
### does the engine from a vignette match one of the registered ones?
###
engineMatches <- function(regengine, vigengine) {
    if (!grepl("::", vigengine))
	regengine <- sub("^.*::", "", regengine)
    regengine == vigengine
}

### * pkgVignettes
###
### Get an object of class pkgVignettes which contains a list of
### vignette source files, the registered vignette engine for
### each of them, and the name of the directory which contains them.

### A vector of 'subdirs' is allowed for historical reasons but the
### first which exists is used.

pkgVignettes <-
function(package, dir, subdirs = NULL, lib.loc = NULL, output = FALSE,
         source = FALSE, check = FALSE)
{
    ## Argument handling.
    if(!missing(package)) {
        if(length(package) != 1L)
            stop("argument 'package' must be of length 1")
        dir <- find.package(package, lib.loc)
    }
    if(missing(dir))
	stop("you must specify 'package' or 'dir'")
    ## Using sources from directory @code{dir} ...
    if(!dir.exists(dir))
	stop(gettextf("directory '%s' does not exist", dir), domain = NA)
    else {
	dir <- file_path_as_absolute(dir)
	if (is.null(subdirs))
	    subdirs <- if (missing(package)) "vignettes" else "doc"
	for (subdir in subdirs) {
	    docdir <- file.path(dir, subdir)
	    if(dir.exists(docdir))
		break
	}
    }

    if(!dir.exists(docdir)) return(NULL)

    # Locate all vignette files
    buildPkgs <- loadVignetteBuilder(dir, mustwork = FALSE, lib.loc = lib.loc)
    engineList <- vignetteEngine(package = buildPkgs) # could be character()

    docs <- names <- engines <- patterns <- character()
    allFiles <- list.files(docdir, all.files = FALSE, full.names = TRUE)
    exclude <- inRbuildignore(sub(paste0(dir, "/"), "", allFiles, fixed = TRUE), dir)
    allFiles <- allFiles[!exclude]

    matchedPattern <- rep.int(FALSE, length(allFiles))
    msg <- character()
    if (length(allFiles) > 0L) {
        for (name in names(engineList)) {
            engine <- engineList[[name]]
            for (pattern in engine$pattern) {
                idxs <- grep(pattern, allFiles)
		matchedPattern[idxs] <- TRUE
		keep <- vapply(allFiles[idxs], function(.d.)
			       engineMatches(name, getVignetteEngine(.d.)), NA)
		if (any(keep)) {
		    idxs <- idxs[keep]
                    if (is.function(engine$weave)) {
                        docsT <- allFiles[idxs]
                        docs <- c(docs, docsT)
                        names <- c(names, gsub(pattern, "", basename(docsT)))
			engines	 <- c(engines,	rep.int(name,	 length(idxs)))
			patterns <- c(patterns, rep.int(pattern, length(idxs)))
                    }
		    matchedPattern <- matchedPattern[-idxs]
                    allFiles <- allFiles[-idxs]
                    if (length(allFiles) == 0L)
                        break
                }
            }
        }
	if (check && any(matchedPattern)) {
            files <- substring(allFiles[matchedPattern], nchar(dir) + 2)
            msg <- c("Files named as vignettes but with no recognized vignette engine:",
                     paste("  ", sQuote(files)),
                     "(Is a VignetteBuilder field missing?)")
        }
    }

    # Assert
    stopifnot(length(names)    == length(docs),
	      length(engines)  == length(docs),
	      length(patterns) == length(docs), !anyDuplicated(docs))

    defaultEncoding <- .get_package_metadata(dir)["Encoding"]
    encodings <- vapply(docs, getVignetteEncoding, "", default = defaultEncoding)

    z <- list(docs = docs, names = names, engines = engines,
              patterns = patterns, encodings = encodings,
              dir = docdir, pkgdir = dir, msg = msg)

    if (output) {
        outputs <- character(length(docs))
        for (i in seq_along(docs)) {
            file <- docs[i]
            name <- names[i]
            engine <- vignetteEngine(engines[i])
            outputI <- find_vignette_product(name, by = "weave", dir = docdir, engine = engine)
            outputs[i] <- outputI
        }
        z$outputs <- outputs
    }

    if (source) {
        sources <- list()
        for (i in seq_along(docs)) {
            file <- docs[i]
            name <- names[i]
            engine <- vignetteEngine(engines[i])
            sourcesI <- find_vignette_product(name, by = "tangle", main = FALSE, dir = docdir, engine = engine)
            sources[[file]] <- sourcesI
        }
        ## If a package has vignettes 'foo.Rnw' and 'foo-xxx.Rnw' with
        ## extracted sources 'foo.R' and 'foo-xxx.R', the above will
        ## give both .R files as sources for 'foo.Rnw', as tangling
        ## could split into several files and so matching file name
        ## roots (without extensions) cannot look for exact matches
        ## only.  However, if there were multiple matches but all
        ## vignettes sources have an exact match, then we can drop the
        ## non-exact matches from the multiple matches.
        ## Ideally, we would teach R CMD build to process one vignette
        ## source at a time and record the vignette products, and have
        ## pkgVignettes() use the recorded info if available.
        if(any(ind <- (lengths(sources) > 1L))) {
            rootify <- function(s) sub("[.][^.]+$", "", basename(s))
            dnm <- rootify(names(sources))
            snm <- lapply(sources, rootify)
            if(!any(is.na(match(dnm, unlist(snm))))) {
                for(i in which(ind)) {
                    sources[[i]] <-
                        sources[[i]][is.na(match(snm[[i]], dnm[-i]))]
                }
            }
        }
        z$sources <- sources
    }

    class(z) <- "pkgVignettes"
    z
}


### * buildVignettes
###
### Run a weave and pdflatex on all vignettes of a package
### (except for those named in 'skip', where TRUE means to skip vignettes
### with unavailable \VignetteDepends, as used by R CMD check)
### and try to remove all temporary files that were created.
### Exported version, used in R CMD build/check
buildVignettes <-
    function(package, dir, lib.loc = NULL, quiet = TRUE, clean = TRUE,
             tangle = FALSE, skip = NULL, ser_elibs = NULL)
{
    separate <- !is.null(ser_elibs)
    if (separate) elibs <- readRDS(ser_elibs)
    ## This has side effects, including loading vignette-buider pkgs
    vigns <- pkgVignettes(package = package, dir = dir, lib.loc = lib.loc,
                          check = TRUE)
    if (is.null(vigns)) return(invisible())
    if (length(vigns$docs) <= 1L) separate <- FALSE
    if (length(vigns$msg))
        warning(paste(vigns$msg, collapse = "\n"), domain = NA)

    ## Check that duplicated vignette names do not exist, e.g.
    ## 'vig' and 'vig' from 'vig.Rnw' and 'vig.Snw'.
    dups <- duplicated(vigns$names)
    if (any(dups)) {
        names <- unique(vigns$names[dups])
        docs <- sort(basename(vigns$docs[vigns$names %in% names]))
        stop(gettextf("Detected vignette source files (%s) with shared names (%s) and therefore risking overwriting each other's output files",
                      paste(sQuote(docs), collapse = ", "),
                      paste(sQuote(names), collapse = ", ")),
             domain = NA)
    }

    if (isTRUE(skip)) { # look for unavailable \VignetteDepends
        installed <- rownames(utils::installed.packages())
    } else if (!is.null(skip)) {
        if (isFALSE(skip)) skip <- NULL else stopifnot(is.character(skip))
    }

    ## unset SWEAVE_STYLEPATH_DEFAULT here to avoid problems
    Sys.unsetenv("SWEAVE_STYLEPATH_DEFAULT")

    op <- options(warn = 1) # we run may run vignettes in this process
    wd <- getwd()
    if (is.null(wd))
        stop("current working directory cannot be ascertained")
    on.exit({
        setwd(wd)
        options(op)
    })

    setwd(vigns$dir)

    ## FIXME: should this recurse into subdirs?
    origfiles <- list.files(all.files = TRUE)

    ## Note, as from 2.13.0, only this case
    have.makefile <- "Makefile" %in% origfiles

    file.create(".build.timestamp")

    ## pkgVignettes has already done this
    ## loadVignetteBuilder(vigns$pkgdir)
    outputs <- character()
    sourceList <- list()
    startdir <- getwd()
    skipped <- character()
    fails <- character()
    for(i in seq_along(vigns$docs)) {
        thisOK <- TRUE
        file <- basename(vigns$docs[i])
        name <- vigns$names[i]
        thisSKIP <- if (isTRUE(skip)) {
                        vinfo <- vignetteInfo(file)
                        length(missdeps <- vinfo$depends %w/o% installed) > 0
                    } else name %in% skip
        if (thisSKIP) {
            msg <- if (isTRUE(skip)) .pretty_format2(
                    sprintf("Note: skipping %s due to unavailable dependencies:",
                            sQuote(file)),  # grepped in check
                    missdeps)
                else gettextf("Note: skipping %s", sQuote(file))
            message(paste0(c(msg, ""), collapse = "\n"),
                    domain = NA)
            skipped <- c(skipped, file)
            next
        }
        enc <- vigns$encodings[i]
        if (enc == "non-ASCII") {
            message(gettextf("Error: Vignette '%s' is non-ASCII but has no declared encoding",
                             file))
            fails <- c(fails, file)
            next
        }
        engine <- vignetteEngine(vigns$engines[i])

        if (separate) {  # --- run in separate process
            tf2 <- gsub("\\", "/", tempfile(fileext = ".rds"), fixed = TRUE)
            saveRDS(engine, tf2)
            Rcmd <- sprintf('tools:::.buildOneVignette("%s", "%s", %s, %s, "%s", "%s", "%s")',
                            file, vigns$pkgdir, quiet, have.makefile,
                            name, enc, tf2)
            tlim <- get_timeout(Sys.getenv("_R_CHECK_ONE_VIGNETTE_ELAPSED_TIMEOUT_",
                                           Sys.getenv("_R_CHECK_ELAPSED_TIMEOUT_")))
            tf <- tempfile()
            status <- R_runR(Rcmd, "--vanilla --no-echo", elibs,
                             stdout = tf, stderr = tf, timeout = tlim)
            unlink(tf2)
            ##print(status)
            if (!status) {
                this <- readLines(tf)
                patt <- "^[+]-[+]"
                l <- grepl(patt, this)
                output <- gsub(patt, "", this[l])
                outputs <- c(outputs, output)
                cat(this[!l], sep = "\n")
            } else {
                fails <- c(fails, file)
                cat(readLines(tf), sep = "\n")
            }
            unlink(tf)
        } else {  # --- run in this process
            message(gettextf("--- re-building %s using %s",
                             sQuote(file), engine$name))
            tryCatch({
                engine$weave(file, quiet = quiet, encoding = enc)
                setwd(startdir) # In case weave/vignette changed it
                output <- find_vignette_product(name, by = "weave", engine = engine)
                if (!have.makefile && vignette_is_tex(output)) {
                    ## This can fail if run in a directory whose path contains spaces.
                    texi2pdf(file = output, clean = FALSE, quiet = quiet)
                    output <- find_vignette_product(name, by = "texi2pdf",
                                                    engine = engine)
                }
                outputs <- c(outputs, output)
            }, error = function(e) {
                thisOK <<- FALSE
                fails <<- c(fails, file)
                message(gettextf("Error: processing vignette '%s' failed with diagnostics:\n%s",
                                 file, conditionMessage(e)))
            })
        }         # end if (separate)

        if (tangle && !separate) {  # This is set for all engines as of 3.0.2
            ## It is unlikely that weave succeeds but tangle fails,
            ## so we don't bother to report tangle failures specifically.
            output <- tryCatch({
                engine$tangle(file, quiet = quiet, encoding = enc)
                setwd(startdir) # In case tangle/vignette changed it
                find_vignette_product(name, by = "tangle", main = FALSE, engine = engine)
            }, error = function(e) {
                thisOK <<- FALSE
                fails <<- c(fails, file)
                message(gettextf("Error: tangling vignette '%s' failed with diagnostics:\n%s",
                     file, conditionMessage(e)))
            })
            sourceList[[file]] <- output
        }
        if (!separate) {
            if (thisOK)
                message(gettextf("--- finished re-building %s\n", sQuote(file)))
            else
                message(gettextf("--- failed re-building %s\n", sQuote(file)))
        }
    } # end loop over vignettes

    if (have.makefile) {
        WINDOWS <- .Platform$OS.type == "windows"
        if (WINDOWS) {
            ## Some people have *assumed* that R_HOME uses / in Makefiles
            ## Spaces in paths might still cause trouble.
            rhome <- chartr("\\", "/", R.home())
            Sys.setenv(R_HOME = rhome)
        }
    	make <- Sys.getenv("MAKE", "make")
        if (!nzchar(make)) make <- "make"
        yy <- system(make)
        if (yy > 0) stop("running 'make' failed")
        ## See if Makefile has a clean: target, and if so run it.
        if (clean &&
	   any(startsWith(readLines("Makefile", warn = FALSE), "clean:")))
            system(paste(make, "clean"))
    } else {
        ## Badly-written vignettes open a pdf() device on Rplots.pdf and
        ## fail to close it.
        grDevices::graphics.off()

        keep <- c(outputs, unlist(sourceList))
        if (clean) {
            f <- setdiff(list.files(all.files = TRUE, no.. = TRUE), keep)
            newer <- file_test("-nt", f, ".build.timestamp")
            ## some packages, e.g. SOAR, create directories
            unlink(f[newer], recursive = TRUE)
            f <- setdiff(list.files(all.files = TRUE, no.. = TRUE),
                         c(keep, origfiles))
            f <- f[file_test("-f", f)]
            file.remove(f)
        }
    }

    if (file.exists(".build.timestamp")) file.remove(".build.timestamp")
    ## Might have been in origfiles ...

    if (length(fails)) {
        message(ngettext(length(fails),
                         "SUMMARY: processing the following file failed:",
                         "SUMMARY: processing the following files failed:"))
        message(paste(.pretty_format(fails), collapse = "\n"))
        message()
    }

    ## Assert
    if (length(fails) || (length(outputs) != (length(vigns$docs) - length(skipped)))) {
        msg <- "Vignette re-building failed."
        stop(msg, domain = NA, call. = FALSE)
    }

    vigns$outputs <- outputs
    vigns$sources <- sourceList

    invisible(vigns) ## not documented on the help page.
}

### * buildVignette
###
### Run a weave and/or tangle on one vignette and try to
### remove all temporary files that were created.
### Also called from 'R CMD Sweave' via .Sweave() in ../../utils/R/Sweave.R
buildVignette <-
    function(file, dir = ".", weave = TRUE, latex = TRUE, tangle = TRUE,
             quiet = TRUE, clean = TRUE, keep = character(),
             engine = NULL, buildPkg = NULL,
	     encoding = getVignetteEncoding(file), ...)
{
    if (!file_test("-f", file))
	stop(gettextf("file '%s' not found", file), domain = NA)
    if (!dir.exists(dir))
	stop(gettextf("directory '%s' does not exist", dir), domain = NA)

    if (!is.null(buildPkg))
	for (pkg in buildPkg)
	    suppressPackageStartupMessages(loadNamespace(pkg))

    if (is.null(engine))
    # Infer vignette engine from vignette content
	engine <- getVignetteEngine(file)

    # Get the vignette engine
    if (is.character(engine))
	engine <- vignetteEngine(engine, package = buildPkg)

    # Infer the vignette name
    names <- sapply(engine$pattern, FUN = sub, "", file)
    name <- basename(names[(names != file)][1L])

    # A non-matching filename?
    if (is.na(name))
	stop(gettextf("vignette filename '%s' does not match any of the '%s' filename patterns",
		file, paste(engine$package, engine$name, sep="::")),
		domain = NA)

    if (encoding == "non-ASCII")
    	stop(gettextf("Vignette '%s' is non-ASCII but has no declared encoding", name))

    # Set output directory temporarily
    file <- file_path_as_absolute(file)
    olddir <- setwd(dir)
    if (!is.null(olddir)) on.exit(setwd(olddir))

    # Record existing files (not to be cleaned)
    if (is.na(clean) || clean)
        origfiles <- list.files(all.files = TRUE)

    tdir <- getwd()# if 'dir' was relative, resetting to tdir will work
    output <- NULL

    # Weave
    final <- if (weave) {
	engine$weave(file, quiet = quiet, encoding = encoding, ...)
	setwd(tdir)  # In case weave/vignette changed it
	output <- find_vignette_product(name, by = "weave", engine = engine)

	# Compile TeX to PDF?
	if(latex && vignette_is_tex(output)) {
	    texi2pdf(file = output, clean = FALSE, quiet = quiet)
	    find_vignette_product(name, by = "texi2pdf", engine = engine)
	} else
	    output
    } # else NULL

    # Tangle
    sources <- if (tangle) {
	engine$tangle(file, quiet = quiet, encoding = encoding, ...)
	setwd(tdir)  # In case tangle changed it
	find_vignette_product(name, by = "tangle", main = FALSE, engine = engine)
    } # else NULL

    ## Cleanup newly created files unless those in 'keep'
    keep <- c(sources, final, keep)
    if (is.na(clean)) {  # Use NA to signal we want .tex (or .md) files kept.
	keep <- c(keep, output)
	clean <- TRUE
    }
    if (clean) {
	f <- setdiff(list.files(all.files = TRUE, no.. = TRUE),
                     c(keep, origfiles))
	## some packages create directories
	unlink(f, recursive = TRUE)
    }

    unique(keep)
}

### * .buildOneVignette

## helper to be run in a separate process
.buildOneVignette <-
    function(file, pkgdir, quiet = TRUE, have.makefile = FALSE,
             name, enc, ser_engine)
{
    op <- options(warn = 1)      # we run vignettes in this process
    engine <- readRDS(ser_engine)
    ## This is more than we need, but we cannot be sure that 'engine'
    ## has references to all the namespaces required.  But packages
    ## typically only have one engine, at most two.
    loadVignetteBuilder(pkgdir)
    OK <- TRUE
    startdir <- getwd()
    output <- character()

    message(gettextf("--- re-building %s using %s",
                     sQuote(file), engine$name))
    tryCatch({
        engine$weave(file, quiet = quiet, encoding = enc)
        setwd(startdir)  # In case weave/vignette changed it
        output <- find_vignette_product(name, by = "weave", engine = engine)
        if(!have.makefile && vignette_is_tex(output)) {
            texi2pdf(file = output, clean = FALSE, quiet = quiet)
            output <- find_vignette_product(name, by = "texi2pdf",
                                            engine = engine)
        }
    }, error = function(e) {
        OK <<- FALSE
        message(gettextf("Error: processing vignette '%s' failed with diagnostics:\n%s",
                         file, conditionMessage(e)))
    })

    if (OK)
        message(gettextf("--- finished re-building %s\n", sQuote(file)))
    else {
        message(gettextf("--- failed re-building %s\n", sQuote(file)))
        q("no", status = 9L)
    }

    message("+-+", output)
    invisible(output)
}


### * getVignetteEncoding

getVignetteEncoding <-  function(file, ...)
{
    lines <- readLines(file, warn = FALSE)
    .getVignetteEncoding(lines, ...)
}

.getVignetteEncoding <- function(lines, default = NA)
{
    res <- .get_vignette_metadata(lines, "Encoding")[1L]

    if(is.na(res)) {
        poss <- grep("^[[:space:]]*%+[[:space:]]*\\\\SweaveUTF8[[:space:]]*$", lines, useBytes = TRUE)
        if (length(poss))
	    "UTF-8"
        else {
            ## Look for input enc lines using inputenc or inputenx
            ## Note, multiple encodings are excluded.
            poss <-
                grep("^[[:space:]]*\\\\usepackage\\[([[:alnum:]]+)\\]\\{inputen[cx]\\}",
                     lines, useBytes = TRUE)
            ## Check it is in the preamble
            start <- grep("^[[:space:]]*\\\\begin\\{document\\}",
                          lines, useBytes = TRUE)
            if(length(start))
                poss <- poss[poss < start[1L]]
            if(length(poss)) {
        	poss <- lines[poss[1L]]
                poss <- gsub("%.*", "", poss, useBytes = TRUE) # strip off comment which
                                                               # may be non-ASCII
        	res <- gsub("^[[:space:]]*\\\\usepackage\\[([[:alnum:]]+)\\].*", "\\1",
                            poss)               # This line should be ASCII.
		## see Rd2latex.R.
		## Currently utf8, utf8x, latin1, latin9 and ansinew are in use.
		switch(res,
		       "utf8" =, "utf8x" = "UTF-8",
		       "latin1" =, "iso-8859-1" = "latin1",
		       "latin2" =, "iso-8859-2" = "latin2",
		       "latin9" =, "iso-8859-15" = "latin-9", # only form known to GNU libiconv
		       "latin10" =, "iso-8859-16" = "latin10",
		       "cyrillic" =, "iso-8859-5" =  "ISO-8859-5", # inputenx
		       "koi8-r" =  "KOI8-R", # inputenx
		       "arabic" = "ISO-8859-6", # Not clear next 3 are known to latex
		       "greek" =, "iso-8859-7" = "ISO-8859-7",
		       "hebrew" =, "iso-8859-8" = "ISO-8859-8",
		       "ansinew" = "CP1252",
		       "applemac" = "macroman",
		       ## assume these only get used on Windows
		       "cp1250" = "CP1250",
		       "cp1252" = "CP1252",
		       "cp1257" = "CP1257",
		       "unknown")
	    } else if (!is.na(default)) {
		default
            } else { # Nothing else has indicated an encoding, maybe it's just ASCII
                asc <- iconv(lines, "latin1", "ASCII")
		if(anyNA(asc) || any(asc != lines)) "non-ASCII" else "" # or "ASCII"
            }
        }
    } else
	res
}

### * .build_vignette_index

.get_vignette_metadata <-
function(lines, tag)
{
    ## <FIXME>
    ## Why don't we anchor this to the beginning of a line?
    meta_RE <- paste0("[[:space:]]*%+[[:space:]]*\\\\Vignette",
                      tag, "\\{([^}]*(\\{[^}]*\\})*[^}]*)\\}.*")
    ## </FIXME>
    meta <- grep(meta_RE, lines, value = TRUE, useBytes = TRUE)
    trimws(gsub(meta_RE, "\\1", meta))
}

vignetteInfo <- function(file)
{
    lines <- readLines(file, warn = FALSE)

    ## <FIXME>
    ## Can only proceed with lines which are valid in the current locale.
    ## Unfortunately, vignette encodings are a mess: package encodings
    ## might apply, but be overridden by \inputencoding commands.
    ## For now, assume that vignette metadata occur in all ASCII lines.
    ## (Could also iconv() using sub = "byte".)
    lines[is.na(nchar(lines, "c", TRUE))] <- ""
    ## </FIXME>

    ## \VignetteIndexEntry
    title <- c(.get_vignette_metadata(lines, "IndexEntry"), "")[1L]
    ## \VignetteDepends
    depends <- .get_vignette_metadata(lines, "Depends")
    if(length(depends))
        depends <- unlist(strsplit(depends[1L], ", *"))
    ## \VignetteKeyword and old-style \VignetteKeywords
    keywords <- .get_vignette_metadata(lines, "Keywords")
    keywords <- if(!length(keywords)) {
        ## No old-style \VignetteKeywords entries found.
        .get_vignette_metadata(lines, "Keyword")
    } else unlist(strsplit(keywords[1L], ", *"))
    ## no point in recording the file path since this is called on
    ## package installation.
    engine <- getVignetteEngine(lines=lines)
    list(file = basename(file), title = title, depends = depends,
         keywords = keywords, engine = engine)
}

## builds vignette indices from 'vigns', a pkgVignettes() result
.build_vignette_index <- function(vigns)
{
    stopifnot(inherits(vigns, "pkgVignettes"))

    files <- vigns$docs
    names <- vigns$names
    dir <- vigns$dir
    sources <- vigns$sources

    if(!dir.exists(dir))
        stop(gettextf("directory '%s' does not exist", dir), domain = NA)

    nvigns <- length(files)
    if(nvigns == 0L) {
        out <- data.frame(File = character(),
                          Title = character(),
                          PDF = character(),
			  R = character(),
                          stringsAsFactors = FALSE)
        out$Depends <- list()
        out$Keywords <- list()
        return(out)
    }

    # Check for duplicated vignette names
    if (any(dups <- duplicated(names))) {
    	dupname <- names[dups][1L]
    	dup <- basename(files[dups][1L])
    	orig <- basename(files[ names == dupname ][1L])
    	stop(gettextf("In '%s' vignettes '%s' and '%s' have the same vignette name",
    		      basename(dirname(dir)), orig, dup),
             domain = NA)
    }

    # Read vignette annotation from vignette source files
    contents <- vector("list", length = nvigns * 5L)
    dim(contents) <- c(nvigns, 5L)
    for(i in seq_along(files))
        contents[i, ] <- vignetteInfo(files[i])
    colnames(contents) <- c("File", "Title", "Depends", "Keywords", "Engine")

    ## This is to cover a temporary package installation
    ## by 'R CMD build' (via 'R CMD INSTALL -l <lib>)
    ## which in case vignettes have not been built.
    outputs <- vigns$outputs
    outputs <- if(!is.null(outputs)) basename(outputs) else character(nvigns)

    out <- data.frame(File = unlist(contents[, "File"]),
                      Title = unlist(contents[, "Title"]),
                      PDF = outputs,	# Not necessarily PDF, but name it that for back compatibility
		      R = "",		# May or may not be present
                      row.names = NULL, # avoid trying to compute row
                                        # names
                      stringsAsFactors = FALSE)
    # Optional
    for (i in seq_along(sources))
	if (length(s <- sources[[i]]))
	    out$R[which(names(sources)[i] == files)] <- basename(s[1L])
    out$Depends <- contents[, "Depends"]
    out$Keywords <- contents[, "Keywords"]

    stopifnot(NROW(out) == nvigns)

    out
}

### * .check_vignette_index

.check_vignette_index <-
function(vignetteDir, pkgdir = ".")
{
    dir <- file.path(pkgdir, vignetteDir)
    if(!dir.exists(dir))
        stop(gettextf("directory '%s' does not exist", dir), domain = NA)

    subdir <- gsub(pkgdir, "", dir, fixed=TRUE)
    vigns <- pkgVignettes(dir = pkgdir, subdirs = subdir)

    vignetteIndex <- .build_vignette_index(vigns)
    badEntries <-
        vignetteIndex[grep("^[[:space:]]*$", vignetteIndex[, "Title"]), "File"]
    class(badEntries) <- "check_vignette_index"
    badEntries
}

print.check_vignette_index <-
function(x, ...)
{
    if(length(x)) {
        writeLines(c("Vignettes with missing or empty \\VignetteIndexEntry:",
                     paste0("  ", basename(unclass(x)))))
    }
    invisible(x)
}


### * .writeVignetteHtmlIndex

.writeVignetteHtmlIndex <-
function(pkg, con, vignetteIndex = NULL)
{
    html <- c(HTMLheader("Vignettes and other documentation",
                         up = "../html/00Index.html",
                         css = "../html/R.css", # installed since R 2.13.0
                         ## relative paths to 'top' and 'logo' will only work
                         ## for the (site-)library in RHOME (or dynamic help)
                         Rhome = "../../.."),
              paste0("<h2>Vignettes from package '", pkg,"'</h2>"),
              if(NROW(vignetteIndex) == 0L) ## NROW(NULL) = 0
                  "The package contains no vignette meta-information."
              else {
                  vignetteIndex <- cbind(Package = pkg,
                                         as.matrix(vignetteIndex[, c("File", "Title", "PDF", "R")]))
                  makeVignetteTable(vignetteIndex, depth = NULL)
              })
    otherfiles <- list.files(system.file("doc", package = pkg))
    if(NROW(vignetteIndex))
        otherfiles <- setdiff(otherfiles,
                              c(vignetteIndex[, c("PDF", "File", "R")], "index.html"))
    if (length(otherfiles)) {
    	otherfiles <- ifelse(dir.exists(system.file(file.path("doc", otherfiles), package = pkg)),
			     paste0(otherfiles, "/"),
			     otherfiles)
	urls <- paste0('<a href="', otherfiles, '">', otherfiles, '</a>')
        html <- c(html, '<h2>Other files in the <span class="samp">doc</span> directory</h2>',
                  '<table style="width: 100%;">',
		  '<col style="width: 24%;">',
		  '<col style="width: 50%;">',
		  '<col style="width: 24%;">',
                  paste0('<tr><td></td>',
                         '<td><span class="samp">',
                         iconv(urls, "", "UTF-8"),
                         "</span></td>",
                         "<td></td></tr>"),
                  "</table>")
    }
    html <- c(html, "</div></body></html>")
    writeLines(html, con=con)
}

getVigDepMtrx <-
function(vigDeps)
{
    ## Taken almost directly out of 'package.dependencies'
    if (length(vigDeps)) {
        z <- unlist(strsplit(vigDeps, ",", fixed=TRUE))
        z <- sub("^[[:space:]]*(.*)", "\\1", z)
        z <- sub("(.*)[[:space:]]*$", "\\1", z)
        pat <- "^([^\\([:space:]]+)[[:space:]]*\\(([^\\)]+)\\).*"
        depMtrx <- cbind(sub(pat, "\\1", z),
                         sub(pat, "\\2", z),
                         NA)
        noversion <- depMtrx[, 1L] == depMtrx[, 2L]
        depMtrx[noversion, 2L] <- NA
        pat <- "[[:space:]]*([[<>=]+)[[:space:]]+(.*)"
        depMtrx[!noversion, 2:3] <-
            c(sub(pat, "\\1", depMtrx[!noversion, 2L]),
              sub(pat, "\\2", depMtrx[!noversion, 2L]))
        depMtrx
    }
    else
        NA
}

### * .run_one_vignette
### helper for R CMD check

.run_one_vignette <-
function(vig_name, docDir, encoding = "", pkgdir)
{
    ## The idea about encodings here is that Stangle reads the
    ## file, converts on read and outputs in the current encoding.
    ## Then source() can assume the current encoding.
    td <- tempfile()
    dir.create(td)
    file.copy(docDir, td, recursive = TRUE)
    setwd(file.path(td, basename(docDir)))

    subdir <- gsub(pkgdir, "", docDir, fixed=TRUE)
    vigns <- pkgVignettes(dir=pkgdir, subdirs=subdir)
    if (is.null(vigns)) {
       cat("\n  When running vignette ", sQuote(vig_name), ":\n", sep="")
       stop("No vignettes available", call. = FALSE, domain = NA)
    }

    i <- which(basename(vigns$docs) == vig_name)
    if (length(i) == 0L) {
       cat("\n  When running vignette ", sQuote(vig_name), ":\n", sep="")
       stop("No such vignette ", sQuote(vig_name), call. = FALSE, domain = NA)
    }
    stopifnot(length(i) == 1L)

    loadVignetteBuilder(pkgdir)
    file <- basename(vigns$docs[i])
    name <- vigns$names[i]
    engine <- vignetteEngine(vigns$engines[i])

    output <- tryCatch({
        engine$tangle(file, quiet = TRUE, encoding = encoding)
        find_vignette_product(name, by = "tangle", engine = engine)
    }, error = function(e) {
        cat("\n  When tangling ", sQuote(file), ":\n", sep="")
        stop(conditionMessage(e), call. = FALSE, domain = NA)
    })

    if(length(output) == 1L) {
        tryCatch({
            source(output, echo = TRUE)
        }, error = function(e) {
            cat("\n  When sourcing ", sQuote(output), ":\n", sep="")
            stop(conditionMessage(e), call. = FALSE, domain = NA)
        })
    }

    cat("\n *** Run successfully completed ***\n")
}

vignetteEngine <- local({
    registry <- new.env(parent = emptyenv())

    engineKey <- function(name, package) {
        key <- strsplit(name, split = "::", fixed = TRUE)[[1L]]
        if (length(key) == 1L) {
	    if (missing(package))
		stop("Vignette engine package not specified", call.=FALSE)
            key[2L] <- key[1L]
            key[1L] <- package
        } else if (length(key) != 2L) {
            stop("Unsupported engine name ", sQuote(name))
        }
        key
    }

    ## FIXME: return a character vector, not stop here.
    getEngine <- function(name, package) {
        if (missing(name)) {
            result <- as.list(registry)
            if (length(result) > 0L && !is.null(package)) {
               package <- unique(package)
               pkgs <- sapply(result, function(engine) engine$package)
               keep <- is.element(pkgs, package)
               if (!any(keep)) {
                   ## was stop() in R 4.4.0
                   msg <-ngettext(length(package),
                                  "Package %s does not have a registered vignette engine",
                                  "None of packages %s have registered vignette engines")
                   warning(sprintf(msg, paste(sQuote(package), collapse = ", ")),
                           domain = NA, call. = FALSE)
                   ## return character() below
               }
               result <- result[keep]
               pkgs <- pkgs[keep]
               if (length(package) > 1L) {
                 result <- result[order(match(pkgs, package))]
               }
            }
        } else {
            result <- NULL
            if (is.null(package)) {
                if (name == "Sweave") {
                    key <- engineKey(name, package = "utils")
                } else {
                    key <- engineKey(name)
                }
		suppressPackageStartupMessages(loadNamespace(key[1]))
                name <- paste(key, collapse = "::")
                result <- registry[[name]]
                if (is.null(result))
                    stop(gettextf("Vignette engine %s is not registered",
                                  sQuote(name)), domain = NA)
            } else {
                for (pkg in package) {
                    key <- engineKey(name, pkg)
		    try(suppressPackageStartupMessages(loadNamespace(key[1])),
                        silent = TRUE)
                    nameT <- paste(key, collapse = "::")
                    result <- registry[[nameT]]
                    if (!is.null(result))
                        break
                }
                if (is.null(result))
                    stop(gettextf("Vignette engine %s is not registered by any of the packages %s",
                                  sQuote(name),
                                  paste(sQuote(package), collapse = ", ")),
                         domain = NA)
            }

            if (!is.null(package) && !is.element(result$package, package))
                stop(gettextf("Vignette engine %s is not registered by any of the packages %s",
                              sQuote(name),
                              paste(sQuote(package), collapse = ", ")),
                     domain = NA)
        }
        result
    }

    setEngine <- function(name, package, pattern, weave, tangle,
                          aspell = list()) {
        key <- engineKey(name, package)
        if (!is.null(package) && key[1L] != package)
            stop(gettextf("Engine name %s and package %s do not match",
                          sQuote(name), sQuote(package)), domain = NA)


        rname <- paste(key, collapse = "::")
        if (is.null(weave)) {
            result <- NULL
            if (exists(rname, envir = registry))
                rm(list = rname, envir = registry)
        } else {
            if (!is.function(weave) && is.na(weave)) {
                if (missing(tangle))
                    tangle <- NA
            } else {
                if (!is.function(weave))
                    stop(gettextf("Argument %s must be a function and not %s",
                                  sQuote("weave"), sQuote(class(weave)[1L])),
                         domain = NA)
                if (!is.function(tangle))
                    stop(gettextf("Argument %s must be a function and not %s",
                                  sQuote("tangle"), sQuote(class(tangle)[1L])),
                         domain = NA)
            }
            if (is.null(pattern))
                pattern <- "[.][rRsS](nw|tex)$"
            else if (!is.character(pattern))
                stop(gettextf("Argument %s must be a character vector or NULL and not %s",
                              sQuote("pattern"), sQuote(class(pattern)[1L])),
                     domain = NA)

            result <-
                list(name = key[2L], package = key[1L], pattern = pattern,
                     weave = weave, tangle = tangle, aspell = aspell)
            assign(rname, result, registry)
        }

        result
    }

    setEngine(name = "Sweave", package = "utils", pattern = NULL,
              weave = function(...) utils::Sweave(...),
              tangle = function(...) utils::Stangle(...),
              aspell = list(filter = "Sweave", control = "-t"))

    function(name, weave, tangle, pattern = NULL, package = NULL,
             aspell = list()) {
        if (missing(weave)) { # we're getting the engine
            getEngine(name, package)
        } else { # we're setting a new engine
            if (is.element(name, c("Sweave", "utils::Sweave"))) {
                stop(gettextf("Cannot change the %s engine or use an engine of that name",
                              sQuote("Sweave")), domain = NA)
            }
            if (missing(package))
                package <- utils::packageName(parent.frame())
            result <-
                setEngine(name, package, pattern = pattern,
                          weave = weave, tangle = tangle, aspell = aspell)
            invisible(result)
        }
    }
})

loadVignetteBuilder <-
function(pkgdir, mustwork = TRUE, lib.loc = NULL)
{
    pkgs <- .get_package_metadata(pkgdir)["VignetteBuilder"]
    if (is.na(pkgs))
        pkgs <- NULL
    else if (length(pkgs)) {
        pkgs <- unlist(strsplit(pkgs, ","))
        pkgs <- gsub('[[:space:]]', '', pkgs)
    }
    pkgs <- unique(c(pkgs, "utils"))

    for (pkg in pkgs) {
	res <- tryCatch(suppressPackageStartupMessages(loadNamespace(pkg,
                                                                     lib.loc = lib.loc)),
                        error = identity)
	if (mustwork && inherits(res, "error"))
            stop(gettextf("vignette builder '%s' not found", pkg), domain = NA)
    }
    pkgs
}

# This gets the info for installed packages

getVignetteInfo <- function(package = NULL, lib.loc = NULL, all = TRUE)
{
    paths <-
        if (is.null(package)) {
            package <- .packages(all.available = all, lib.loc)
            ## allow for misnamed dirs
            find.package(package, lib.loc, quiet = TRUE)
        } else
            find.package(package, lib.loc)

    ## Find the directories with a 'doc' subdirectory *possibly*
    ## containing vignettes.

    paths <- paths[dir.exists(file.path(paths, "doc"))]

    empty <- cbind(Package = character(0),
                   Dir = character(0),
                   Topic = character(0),
                   File = character(0),
                   Title = character(0),
                   R = character(0),
                   PDF = character(0))

    getVinfo <- function(dir) {
        entries <- NULL
        if (file.exists(INDEX <- file.path(dir, "Meta", "vignette.rds")))
            entries <- readRDS(INDEX)
        if (NROW(entries) > 0) {
            # FIXME:  this test is unnecessary?
            R <- entries$R %||% rep.int("", NROW(entries))
            file <- basename(entries$File)
            pdf <- entries$PDF
            topic <- file_path_sans_ext(ifelse(R == "", ifelse(pdf == "", file, pdf), R))
            cbind(Package = basename(dir),
                  Dir = dir,
                  Topic = topic,
                  File = file,
                  Title = entries$Title,
                  R = R,
                  PDF = pdf)[order(entries$Title), , drop=FALSE]
        }
        else empty
    }

    if (length(paths))
    	do.call(rbind, lapply(paths, getVinfo))
    else
    	empty
}

### Local variables: ***
### mode: outline-minor ***
### outline-regexp: "### [*]+" ***
### End: ***
