# Given a list of named lists, return a list of all the names used
collectFieldNames <- function(lists) {
allFieldNames <- character(0)
for (lst in lists)
allFieldNames <- union(allFieldNames, unique(names(lst)))
return(allFieldNames)
}
# Create a single-row data frame with the given column names and all NA values
naRow <- function(fieldNames) {
structure(
do.call(data.frame, as.list(rep.int(NA, length(fieldNames)))),
names = fieldNames
)
}
# Like rbind.data.frame but tolerates heterogeneous columns, filling in any
# missing values with NA
rbind2 <- function(df1, df2) {
allNames <- union(names(df1), names(df2))
missing1 <- setdiff(allNames, names(df1))
missing2 <- setdiff(allNames, names(df2))
return(rbind(
cbind(df1, naRow(missing1), row.names = NULL),
cbind(naRow(missing2), df2, row.names = NULL)
))
}
writeLockFile <- function(file, lockinfo) {
rver <- as.character(getRversion())
# Construct Repos as a key-value pair to write into the lock file
repos <- activeRepos(dirname(file))
# Windows automatically transforms \n to \r\n on write through write.dcf
separator <- ",\n"
reposString <- paste(names(repos), unname(repos), sep = "=", collapse = separator)
# The first record contains metadata about the project and lockfile
preamble <- data.frame(
PackratFormat = .packrat$packratFormat,
PackratVersion = as.character(packageVersion("packrat")),
RVersion = rver,
Repos = reposString
)
stopifnot(nrow(preamble) == 1)
# Remaining records are about the packages
if (length(lockinfo)) {
packages <- flattenPackageRecords(lockinfo, depInfo = TRUE, sourcePath = TRUE)
fieldNames <- collectFieldNames(packages)
packageInfo <- lapply(fieldNames, function(fieldName) {
values <- data.frame(vapply(packages, function(pkg) {
if (length(pkg[[fieldName]]))
pkg[[fieldName]]
else
NA_character_
}, character(1), USE.NAMES = FALSE))
names(values) <- fieldName
return(values)
})
packageInfoDf <- do.call(data.frame, packageInfo)
df <- rbind2(preamble, packageInfoDf)
} else {
df <- as.data.frame(preamble, stringsAsFactors = FALSE)
}
names(df) <- translate(names(df), r_aliases)
write_dcf(df, file)
invisible()
}
readLockFile <- function(file) {
df <- as.data.frame(readDcf(file), stringsAsFactors = FALSE)
df <- cleanupWhitespace(df)
# Used 'GitHub' instead of 'Github' for a brief period -- translate those
names(df) <- gsub("^GitHub", "Github", names(df))
# Translate the names according to the aliases we maintain
names(df) <- translate(names(df), aliases)
# Split the repos
repos <- gsub("[\r\n]", " ", df[1, 'Repos'])
repos <- strsplit(unlist(strsplit(repos, "\\s*,\\s*", perl = TRUE)), "=", fixed = TRUE)
# Support older-style lockfiles containing unnamed repositories
repoLens <- vapply(repos, length, numeric(1))
if (all(repoLens == 1)) {
# Support for old (unnamed) repositories
if (length(repoLens) > 1) {
# We warn if there were multiple repositories (if there was only one, we
# can safely assume it was CRAN)
warning("Old-style repository format detected; bumped to new version\n",
"Please re-set the repositories with options(repos = ...)\n",
"and call packrat::snapshot() to update the lock file.")
}
repos <- c(CRAN = repos[[1]])
} else if (all(repoLens == 2)) {
repos <- setNames(
sapply(repos, "[[", 2),
sapply(repos, "[[", 1)
)
}
packages <- if (nrow(df) > 1)
deserializePackages(utils::tail(df, -1))
else
list()
list(
packrat_format = df[1, 'PackratFormat'],
packrat_version = df[1, 'PackratVersion'],
r_version = df[1, 'RVersion'],
repos = repos,
packages = packages
)
}
# Remove leading and trailing whitespace from character vectors
# in the dataframe, and return the modified dataframe
cleanupWhitespace <- function(df) {
for (i in seq_along(df)) {
if (is.character(df[[i]]))
df[[i]] <- sub('^\\s*(.*?)\\s*$', '\\1', df[[i]])
}
return(df)
}
# @param graph Named list where the names are the packages and the values
# are the names of the packages that they depend on. Packages with no
# dependencies should have character(0) or NULL.
# @return Sorted character vector of package names
topoSort <- function(graph) {
packageNames <- names(graph)
# Key: dependency, Value: dependent
# Use this to answer: What things depend on this key?
dependents <- new.env(parent = emptyenv(), size = as.integer(length(packageNames) * 1.3))
# Key: dependent, Value: Number of dependencies
# Use this to answer: How many things does this key depend on?
dependencyCount <- new.env(parent = emptyenv(), size = as.integer(length(packageNames) * 1.3))
for (packageName in packageNames)
dependencyCount[[packageName]] <- 0
# Initialize dependents and dependencyCount
for (pkgName in packageNames) {
for (r in graph[[pkgName]]) {
dependents[[r]] <- c(dependents[[r]], pkgName)
dependencyCount[[pkgName]] <- dependencyCount[[pkgName]] + 1
}
}
if (length(setdiff(ls(dependents), packageNames)) > 0)
stop("Corrupted lockfile: missing dependencies") # TODO: better message
# Do topo sort
sortedNames <- character(0)
leaves <- packageNames[vapply(packageNames, function(pkgName) {
identical(dependencyCount[[pkgName]], 0)
}, logical(1))]
while (length(leaves) > 0) {
leaf <- leaves[[1]]
leaves <- utils::tail(leaves, -1)
sortedNames <- c(sortedNames, leaf)
# See who depends on the leaf
for (dependent in dependents[[leaf]]) {
# Decrease the dependency count for this dependent
dependencyCount[[dependent]] <- dependencyCount[[dependent]] - 1
# Is this dependent now a leaf?
if (identical(dependencyCount[[dependent]], 0)) {
leaves <- c(leaves, dependent)
do.call(rm, list(dependent, envir = dependencyCount))
}
}
if (exists(leaf, where = dependents))
do.call(rm, list(leaf, envir = dependents))
}
if (!setequal(sortedNames, packageNames))
stop("Corrupt lockfile: circular package dependencies detected")
sortedNames
}
deserializePackages <- function(df) {
packageNames <- df[, 'name']
## Begin validation
# Test for package records without names
if (any(is.na(packageNames)))
stop("Invalid lockfile format: missing package name detected")
dupNames <- packageNames[duplicated(packageNames)]
if (length(dupNames) > 0) {
stop("The following package(s) appear in the lockfile more than once: ",
paste(dupNames, collapse = ", "))
}
# TODO: Test that package names are valid (what are the rules?)
## End validation
graph <- lapply(seq.int(nrow(df)), function(i) {
req <- df[i, 'requires']
if (is.null(req) || is.na(req))
return(character(0))
reqs <- unique(strsplit(req, '\\s*,\\s*')[[1]])
if (identical(reqs, ''))
return(character(0))
return(reqs)
})
names(graph) <- packageNames
# Validate graph
undeclaredDeps <- setdiff(unique(unlist(graph)), packageNames)
if (length(undeclaredDeps) > 0) {
stop("The following dependencies are missing lockfile entries: ",
paste(undeclaredDeps, collapse = ", "))
}
topoSorted <- topoSort(graph)
# It's now safe to drop the requires info since it's encoded in the graph
df <- df[, names(df) != 'requires', drop = FALSE]
sortedPackages <- lapply(topoSorted, function(pkgName) {
pkg <- as.list(df[df$name == pkgName, ])
pkg <- pkg[!is.na(pkg)]
return(pkg)
})
names(sortedPackages) <- topoSorted
for (i in seq_along(sortedPackages)) {
pkg <- sortedPackages[[i]]
pkg$depends <- lapply(graph[[pkg$name]], function(depName) {
sortedPackages[[depName]]
})
sortedPackages[[i]] <- pkg
}
names(sortedPackages) <- NULL
return(sortedPackages)
}
translate <- function(x, dict) {
vapply(x, function(val) {
if (!(val %in% names(dict)))
val
else
as.vector(dict[[val]])
}, character(1))
}
# Translates persistent names with in-memory names (i.e. the names are what the
# fields are called in the lockfile, and the values are what the fields are
# called after they've been deserialized into package records).
#
# NB: This list must be maintained if additional fields are added to package
# records!
aliases <- c(
Package = "name",
Source = "source",
Version = "version",
Requires = "requires",
GithubRepo = "gh_repo",
GithubUsername = "gh_username",
GithubRef = "gh_ref",
GithubSha1 = "gh_sha1",
GithubSubdir = "gh_subdir",
RemoteHost = "remote_host",
RemoteRepo = "remote_repo",
RemoteUsername = "remote_username",
RemoteRef = "remote_ref",
RemoteSha = "remote_sha",
RemoteSubdir = "remote_subdir",
SourcePath = "source_path",
Hash = "hash"
)
r_aliases <- structure(names(aliases), names = aliases)
Add the following code to your website.
For more information on customizing the embed code, read Embedding Snippets.