Nothing
# =================================================================
# multiversion, multi-version package library management tool
# Copyright (C) 2019 S.C. Frouws, The Hague, The Netherlands
#
# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation; either
# version 2.1 of the License, or (at your option) any later version.
#
# This library 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
# Lesser General Public License for more details.
# =================================================================
# ============== MAIN FUNCTIONALITY ==============
#' Load package from R_MV_library
#'
#' There are two ways you can provide a package or vector of packages that need to be loaded: \cr
#' 1: just provide them directly (the \code{...} input). All not recognized named variables
#' will be interpreted as package names or (if it's a named argument) as a package name=version combination. \code{\link{lib.load}(DBI = '0.5', assertthat, R6)}
#' 2: provide the \code{loadPackages} input in the following way: \code{\link{lib.load}(loadPackages = c(DBI = '0.5', assertthat = '', R6 = ''))} \cr
#' \cr
#' If an empty string e.g. \code{dplyr = ''}, or only the package name is specified, one of two things will happen:
#' - if one version is available, this one is used.
#' - if multiple versions are available, the first or last version is loaded depending on the 'pick.last' value (FALSE by default). \cr
#' \cr
#' if >= or > is used, as in \code{dplyr = '>= 2.5'}, it will decide for the first or last compatible version, depending on the 'pick.last' parameter.
#' If another version is desired, please define it in the input list of packages to load, prior to the package that depends on it.
#'
#' @details
#'
#' Dependencies are checked by recursively running this function with \code{dry_run = TRUE}.
#' Then the paths of the found dependencies are temporarily appended (\code{.libPaths()}) when the actual package is loaded.
#' This makes that dependencies are not loaded automatically, but are added to the namespace.
#' To access a dependency directly, load it explicitly.
#' Because the \code{.libPaths()} does not include the package it's location, this still needs to be done by \code{lib.load}.\cr
#' In other words, dependencies are remembered, but not loaded.\cr
#' \cr
#' Using \code{dry_run} will show the packages that will be used and will crash when no option is feasible (not installed or not compliant packages).
#' If you are trying to setup a proper \code{\link{lib.load}} call, it is always a good idea to work with dry_run's.
#' Once an incorrect package has been loaded, it is very likely you will have to restart
#' your R session to unload it (\code{Ctrl} + \code{shift} + \code{F10}). Unloading packages in R often leaves traces. \cr
#' \cr
#' The .libPaths of specific package versions can be appended when using 'appendLibPaths = TRUE'.
#' Afterwards, the normal \code{library} call can be used to load the package since it's path is in the \code{.LibPaths} vector.
#' For example: \cr
#' \code{
#' lib.load(c(dplyr = '0.5.0'), dry_run = TRUE, appendLibPaths = TRUE)
#' library(dplyr)} \cr
#' \cr
#' How this works is that \code{dry_run} skips the loading step, and \code{appendLibPaths}
#' adds the paths of dplyr and it's dependencies to \code{.libPaths}, which make a \code{library} call work. \cr
#' \cr
#' One reason to use \code{appendLibPaths = TRUE} is to make these packages accessible by
#' a new 'child' R session. This is the case if \code{devtools::test()} is ran
#' by using \code{cntrl} + \code{shift} + \code{TRUE} in Rstudio. When running it directly,
#' it will use the packages it can find in the available libraries (\code{.libPath()}) and return an error if they cannot be found. \cr
#'
#' The inputs .packNameVersionList [vector of named versions] and .skipDependencies [vector of names] can be
#' left blank in general. They are used internally and might be deprecated in the future.
#'
#' @section Major version differences:
#' By default, when chosing the right version to load, only versions are looked up within the
#' same major version.
#' For example, when \code{pick.last = TRUE}, the version \code{'> 15.3.0'} is requested and
#' the versions \code{c('15.5.0', '15.9.0', '16.0.0')} are available, the version \code{15.9.0} is chosen.
#' When a requested (dependency) version \code{'>= 0.5'} is provided, and only the versions
#' \code{c('0.4.0', '1.5.0', '1.7.0')} are available,
#' it will throw a warning that the first available version is a major release higher,
#' and pick \code{'1.5.0'} or \code{'1.7.0'} depending on the \code{pick.last} value.\cr
#' \cr
#' This behavior can be disabled by setting \bold{\code{options(mv_prefer_within_major_version = 'no')}}.
#'
#' @section Base packages:
#' The packages within the directory returned by \code{.Library} are considered 'base packages'.
#' Of these, only one version can exist, and these cannot be included in the multiversion library. \cr
#'
#' @section Problem solving:
#' If you receive the error "\code{cannot unload ...}" it means that it tries to load a package,
#' but another version is already loaded.
#' To unload this other (older) version, run detach(package = '...'). If it is a dependency
#' of an other package, you will receive this error.
#' Try restarting your RStudio with a clean workspace (environment). If that doesn't help,
#' the only workaround (when using this in R studio) is to close your Rstudio session (NOTE: save your unsaved process before proceeding!!), rename (or remove) the folder
#' "\code{YourRProject/.Rproj.user/.../sources/prop}" and start Rstudio again. If it doesn't
#' work, try "\code{/sources/per}" also. Where the \code{...} stands for a hash that is used in the current session e.g. \code{/F3B1663E/}.
#' After this, the packages should be unloaded and you should be able to load a new batch of
#' packages. Most times it will suffice to clear the workspace (environment) and reload the project while saving the empty environment.
#'
#' @param ... All packages and their versions you would like to load e.g.
#' \code{\link{lib.load}(DBI = '0.5', assertthat = '', R6 = '', quietly = TRUE)}.
#' Input names like \code{quietly} will be recognized and interpreted as expected.
#' @param loadPackages Supports providing a named character vector of packages and their
#' versions in the shape that is supported by all other functions in this package.
#' e.g. \code{c(DBI = '0.5', assertthat = '', R6 = '')}
#' @param lib_location The folder containing a structure where this package must load
#' packages from. By default, it checks the environment variable \code{R_MV_LIBRARY_LOCATION} for this directory.
#' @param dry_run Will make it perform a dry run. It will check all dependencies and
#' if \code{appendLibPaths} it will add
#' their paths to \code{.libPaths} but it will not load those packages. If the paths
#' are added this way, you should be able to just call the located packages with \code{library(...)}
#' @param quietly Indicates if the loading must happen silently. No messages and
#' warnings will be shown if the value is set to true.
#' @param verbose Indicates if additional information must be shown that might
#' help with debugging the decision flow of this function. More specifically,
#' when false, it will wrap 'library' calls in \code{suppressWarnings(suppressMessages(...))}
#' and suppress unloading attempts.
#' @param appendLibPaths When true, the path to every package that is loaded will
#' be appended to \code{.libPath(...)}. That configured path is the location
#' where \code{library()} will look for packages. For a usecase for this feature,
#' see the description above.
#' @param pick.last Changes the way a decision is made. In the scenario where a
#' dependency of \code{>} or \code{>=} is defined, multiple versions may be
#' available to choose from. By default, the lowest compliant version is chosen.
#' Setting this to true will choose the highest version.
#' @param also_load_from_temp_lib when true, it will also load packages from the
#' temporary installation directory (created when packages are installed in the R_MV_library).
#' Can be usefull when installing using: \code{lib.install("new package!", install_temporarily = TRUE)}.
#'
#' @param .packNameVersionList See main description. Should be left blank.
#' @param .skipDependencies See main description. Should be left blank.
#'
#' @return Will return a named character vector indicating which version of
#' which package is loaded (or will be loaded, when dry_run = TRUE).
#' In general, this function is called for it's side effect. It will load
#' specific versions of specific packages from a special \code{multiversion} library.
#'
#' @importFrom stats setNames
#' @importFrom utils packageDescription sessionInfo
#'
#' @export
#'
lib.load <- function(..., loadPackages = NULL, lib_location = lib.location(),
dry_run = FALSE, quietly = FALSE,
verbose = FALSE, appendLibPaths = FALSE,
pick.last = FALSE, also_load_from_temp_lib = FALSE,
.packNameVersionList = c(), .skipDependencies = c()) {
# Some input checks.
stopifnot(length(dry_run) == 1, length(quietly) == 1,
length(verbose) == 1, length(appendLibPaths) == 1,
length(pick.last) == 1, length(also_load_from_temp_lib) == 1,
length(lib_location) == 1)
stopifnot(is.logical(dry_run), is.logical(quietly),
is.logical(verbose), is.logical(appendLibPaths),
is.logical(pick.last), is.logical(also_load_from_temp_lib),
is.character(lib_location))
if (verbose && quietly) {
stop('We cannot be quiet and verbose at the same time...')
}
# If `loadPackages` is not provided, make use of the ... input via `match.call()`.
# It will list all input names and values from which I will use all but the ones excluded.
if (is.null(loadPackages)) {
loadPackages <- raw_input_parser(as.list(match.call()), varnames_to_exclude = c('loadPackages', 'lib_location', 'dry_run', 'quietly', 'verbose', 'appendLibPaths', 'pick.last', 'also_load_from_temp_lib', '.packNameVersionList', '.skipDependencies'))
}
if (length(loadPackages) == 1 && (is.null(names(loadPackages)) || strtrim(names(loadPackages), 2) == 'c(')) {
stop('Please make sure that you call `lib.load(loadPackages = c(pack = \'version\'))` when you would like to use a named character vector.\nAlternatively, remove the vector elements and call `lib.load(pack = \'version\')` directly.')
}
# If still other libraries are set as active libraries, reset the library to just 1 lib for the build in functions (= `.Library`).
# if (!quietly & interactive() & length(sys.calls()) == 1 & !all(grepl(normPath(lib_location), normPath(.libPaths())) | grepl(normPath(.Library), normPath(.libPaths())))) {
# warning(paste0('\nlib.load: Extra libraries were found.\n',
# 'lib.load will exclude those when loading packages, please be aware `library()` does not\n',
# 'and might load a package from an unexpected location.\n',
# 'Please use `.libPaths(.Library)` before using lib.load to suppress this warning.\n\n'))
# }
# check if the package version that is provided is a correct version (this catches a wrong input like `lib.load(dplyr = '< 0.5.0')`)
if (length(loadPackages) != 0 && !any(is.na(loadPackages)) &&
any(sapply(loadPackages, function(x) {attributes(regexpr('>?=?\\s?\\d+(\\.\\d+){1,3}', x))$match.length != nchar(x) && nchar(x) > 0}) & !lib.is_basepackage(names(loadPackages)))) {
stop(sprintf('Not all package versions that are provided seem to be valid version numbers. The following was received:\n%s', paste(paste0(names(loadPackages), ' (', loadPackages, ')'), collapse = ', ')))
}
n_skipped <- 0
for (iPackage in unique(names(loadPackages))) {
# Here, the package loading string prefix is created:
n_recursive <- sum(grepl('^lib.load|^library_VC', sapply(sys.calls(), function(x) {x[[1]]})))
if (n_recursive == 1) {
stackStr <- '+_'
} else {
# if it is the first of the packages to be load, start with backward-slash:
startChar <- ifelse(iPackage == names(loadPackages)[1 + n_skipped], ' \\_', ' |')
# starting from the second 'dependency' depth, an additional pipe needs to be printed:
stackStr <- paste(collapse = '', c(rep(' |', n_recursive - 2), startChar))
}
stackStr <- paste0(collapse = '', c(stackStr, rep('_', max(16 - nchar(stackStr), 1))))
# if base package, simply load and continue
if (lib.is_basepackage(iPackage)) {
library(iPackage, character.only = TRUE, quietly = quietly)
n_skipped <- n_skipped + 1
next
}
# Also checking 'RVClibrary' for backwards compatibility
if (iPackage %in% c(.skipDependencies, 'multiversion', 'RVClibrary')) {
if (lib.check_compatibility(loadPackages[iPackage], .packNameVersionList[iPackage])) {
n_skipped <- n_skipped + 1
next
} else {
# Package was explicitly requested by previous load operation, and is already loaded:
if (paste0('package:', iPackage) %in% search()) {
# ERROR
error_packageAlreadyLoaded(iPackage, .packNameVersionList[iPackage], lib.package_version_loaded(iPackage))
}
# If package was only loaded by namespace, try at least to continue and overwrite with a compatible version.
.packNameVersionList <- .packNameVersionList[!names(.packNameVersionList) == iPackage]
}
# If package is loaded directly or indirectly:
} else if (isNamespaceLoaded(iPackage)) {
if (!lib.check_compatibility(loadPackages[iPackage], lib.package_version_loaded(iPackage))) {
# Try to unload namespace (EXPERIMENTAL)
tryCatch({
unloadNamespace(iPackage)
dll <- getLoadedDLLs()[[iPackage]]
if (!is.null(dll)) {
tryCatch({
library.dynam.unload(iPackage, dirname(dirname(dll[["path"]])))
if (verbose) {message('Unloaded ', iPackage, ' dll: ', dll[["path"]])}
}, error = function(e) NULL)
}
if (!quietly) {message('Unloaded ', iPackage, ' because the currently attached version was not compatible with the new requirements: "', loadPackages[iPackage], '".')}
},
error = function(e) {
error_packageAlreadyLoaded(iPackage, loadPackages[iPackage], lib.package_version_loaded(iPackage))
})
}
}
if (!quietly) {message(stackStr, appendLF = FALSE)}
# Compose the installation dir
temp_lib_dir <- lib.location_install_dir(lib_location, FALSE)
additional_lib <- c()
# If (only temporarily) installed packages must also be loaded, add them to the search path in the load operation.
if (also_load_from_temp_lib) {
additional_lib <- temp_lib_dir
}
# Check if the package is found among the (temporarily) installed packages, and if no other compatible version can be found, use this package.
av_versions <- lib.available_versions(iPackage, lib_location)
if (also_load_from_temp_lib && (length(av_versions) == 0 ||
!any(lib.check_compatibility(loadPackages[iPackage], av_versions))) && file.exists(file.path(temp_lib_dir, iPackage))) {
package_loc <- temp_lib_dir
packVersion <- utils::packageDescription(iPackage, package_loc)$Version
if (!quietly) message(sprintf("Version %-7s INSTALLED for package '%s'", packVersion, iPackage))
} else {
# Messages like: "Version ... is chosen for package '...'" are printed here.
packVersion <- lib.decide_version(loadPackages[iPackage], lib_location, pick.last = pick.last, print_version_choice = !quietly, warn_for_major_diff = !quietly)
package_loc <- paste(lib_location, iPackage, packVersion, sep = '/')
}
if (!dir.exists(package_loc)) {
stop(sprintf('\nThe package "%s" or its version "%s" could not be accessed and might not be present. Consider running `lib.clean(clean_temp_lib = FALSE)`.', iPackage, packVersion))
}
# load dependencies: [1] from an override dependency file [2] from the original dependencies.
# The override dependency file never exists when the package is loaded from the temp_install_library.
overrideFile <- file.path(package_loc, 'vc_override_dependencies.txt')
if (file.exists(overrideFile)) {
dependingPackages <- lib.packs_str2vec(readChar(overrideFile, file.info(overrideFile)$size))
} else {
packDesc <- utils::packageDescription(iPackage, lib.loc = package_loc)
dependingPackages <- lib.packs_str2vec(gsub(paste0(packDesc$Depends, ',', packDesc$Imports), pattern = ',,', replacement = ','))
}
# recusively load dependencies
# The .skipDependencies is necessary for `dry_run=TRUE`.
# With `dry_run=FALSE` the dependency is loaded and skipped in the next iteration.
.packNameVersionList <- lib.load(loadPackages = dependingPackages,
lib_location = lib_location,
.packNameVersionList = .packNameVersionList,
.skipDependencies = c( names(.packNameVersionList), .skipDependencies ),
pick.last = pick.last,
also_load_from_temp_lib = also_load_from_temp_lib,
quietly = quietly,
verbose = verbose,
dry_run = TRUE)
.packNameVersionList <- append(.packNameVersionList, stats::setNames(packVersion, iPackage))
currentLibs <- .libPaths()
# Reset .libPaths on failure
on.exit({
if (appendLibPaths) {
lib.set_libPaths(.packNameVersionList, lib_location, additional_lib)
.libPaths(c(.libPaths(), currentLibs))
}
# The standard case is processed in the below if statement (!dry_run) without an 'on.exit'.
}, add = TRUE)
if (!dry_run) {
lib.set_libPaths(.packNameVersionList, lib_location, additional_lib)
# Will remove all paths but those that are part of the R_MV_library
lib.clean_libPaths(lib_location = lib_location)
if (verbose) {
library(iPackage, lib.loc = package_loc, character.only = TRUE)
} else {
suppressWarnings(suppressMessages(library(iPackage, lib.loc = package_loc, character.only = TRUE, quietly = TRUE)))
}
.libPaths(currentLibs)
}
}
# Cleanup the list to only consist unique calls:
.packNameVersionList <- unique_highest_package_versions(.packNameVersionList)
# Load namespaces of dependencies (so that these locations will be stored in a small database (read `? loadNamespace`),
# and these packages will not be confused with local packages when `.libPath` changes. This way, the package will be found,
# even when the user library is still in the search path.)
if (!dry_run) {
current_status <- utils::sessionInfo()
ns_to_load <- .packNameVersionList[!names(.packNameVersionList) %in% c(names(current_status$loadedOnly), names(current_status$otherPkgs), names(loadPackages))]
lib.load_namespaces(ns_to_load, lib_location, additional_lib)
}
# if not quietly, and at top level of stack, show an example library call.
if (verbose && length(sys.calls()) == 1) {
lib.printVerboseLibCall(.packNameVersionList)
}
return(invisible(.packNameVersionList))
}
Any scripts or data that you put into this service are public.
Add the following code to your website.
For more information on customizing the embed code, read Embedding Snippets.