├── README.md ├── .gitignore ├── LICENSE ├── tests ├── testthat.R └── testthat │ ├── test.R │ ├── helper.R │ ├── test-urls.R │ ├── test-metadata.R │ └── test-restore.R ├── .Rbuildignore ├── Makefile ├── inst ├── NEWS.md ├── README.Rmd └── README.md ├── NAMESPACE ├── man ├── get_pkg_type.Rd ├── r_minor_version.Rd ├── dir_exists.Rd ├── OR.Rd ├── str_trim.Rd ├── default_cran_mirror.Rd ├── download_urls.Rd ├── data_frame.Rd ├── pkg_from_filename.Rd ├── try_download.Rd ├── get_package_sources.Rd ├── get_description.Rd ├── drop_missing_deps.Rd ├── parse_deps.Rd ├── add_R_core.Rd ├── dep_types.Rd ├── check_R_core.Rd ├── filename_from_url.Rd ├── get_deps.Rd ├── pkg_download.Rd ├── install_order.Rd ├── drop_internal.Rd ├── restore.Rd ├── cran_file.Rd └── snap.Rd ├── DESCRIPTION ├── .travis.yml ├── appveyor.yml └── R ├── metadata.R ├── utils.R ├── snapshot.R ├── download.R ├── deps.R ├── restore.R └── urls.R /README.md: -------------------------------------------------------------------------------- 1 | inst/README.md -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .Rproj.user 2 | .Rhistory 3 | .RData 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | YEAR: 2015 2 | COPYRIGHT HOLDER: Mango Solutions 3 | -------------------------------------------------------------------------------- /tests/testthat.R: -------------------------------------------------------------------------------- 1 | library(testthat) 2 | library(pkgsnap) 3 | 4 | test_check("pkgsnap") 5 | -------------------------------------------------------------------------------- /.Rbuildignore: -------------------------------------------------------------------------------- 1 | ^.*\.Rproj$ 2 | ^\.Rproj\.user$ 3 | ^Makefile$ 4 | ^README.md$ 5 | ^.travis.yml$ 6 | ^appveyor.yml$ 7 | -------------------------------------------------------------------------------- /tests/testthat/test.R: -------------------------------------------------------------------------------- 1 | 2 | context("pkgsnap") 3 | 4 | test_that("pkgsnap works", { 5 | 6 | expect_true(TRUE) 7 | 8 | }) 9 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | 2 | all: README.md 3 | 4 | README.md: inst/README.Rmd 5 | Rscript -e "library(knitr); knit('$<', output = '$@', quiet = TRUE)" 6 | -------------------------------------------------------------------------------- /inst/NEWS.md: -------------------------------------------------------------------------------- 1 | # 1.0.1 2 | 3 | ## Bugfixes 4 | 5 | - Skip packages with NA as source (@ManuelaAlmeida) 6 | 7 | # 1.0.0 8 | 9 | First public release. 10 | -------------------------------------------------------------------------------- /tests/testthat/helper.R: -------------------------------------------------------------------------------- 1 | 2 | 3 | skip_if_offline <- function(host = "httpbin.org", port = 80) { 4 | 5 | res <- tryCatch( 6 | pingr::ping_port(host, count = 1L, port = port), 7 | error = function(e) NA 8 | ) 9 | 10 | if (is.na(res)) skip("No internet connection") 11 | } 12 | -------------------------------------------------------------------------------- /NAMESPACE: -------------------------------------------------------------------------------- 1 | # Generated by roxygen2: do not edit by hand 2 | 3 | export(restore) 4 | export(snap) 5 | importFrom(utils,download.file) 6 | importFrom(utils,install.packages) 7 | importFrom(utils,installed.packages) 8 | importFrom(utils,read.csv) 9 | importFrom(utils,untar) 10 | importFrom(utils,write.csv) 11 | -------------------------------------------------------------------------------- /man/get_pkg_type.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/urls.R 3 | \name{get_pkg_type} 4 | \alias{get_pkg_type} 5 | \title{What kind of packages to use by default.} 6 | \usage{ 7 | get_pkg_type() 8 | } 9 | \description{ 10 | \code{both} means binaries first, then source packages. 11 | } 12 | \keyword{internal} 13 | -------------------------------------------------------------------------------- /man/r_minor_version.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/urls.R 3 | \name{r_minor_version} 4 | \alias{r_minor_version} 5 | \title{Extract the minor version of the running R} 6 | \usage{ 7 | r_minor_version() 8 | } 9 | \description{ 10 | This is needed to calculate possible R package locations 11 | for downloads. 12 | } 13 | \keyword{internal} 14 | -------------------------------------------------------------------------------- /man/dir_exists.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/utils.R 3 | \name{dir_exists} 4 | \alias{dir_exists} 5 | \title{Check if a directory exists} 6 | \usage{ 7 | dir_exists(dir) 8 | } 9 | \arguments{ 10 | \item{dir}{Directory to check.} 11 | } 12 | \value{ 13 | Logical scalar. 14 | } 15 | \description{ 16 | Check if a directory exists 17 | } 18 | \keyword{internal} 19 | -------------------------------------------------------------------------------- /man/OR.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/utils.R 3 | \name{OR} 4 | \alias{OR} 5 | \alias{\%||\%} 6 | \title{LHS if not \code{NULL}, otherwise RHS} 7 | \usage{ 8 | l \%||\% r 9 | } 10 | \arguments{ 11 | \item{l}{LHS.} 12 | 13 | \item{r}{RHS.} 14 | } 15 | \value{ 16 | LHS if not \code{NULL}, otherwise RHS. 17 | } 18 | \description{ 19 | LHS if not \code{NULL}, otherwise RHS 20 | } 21 | \keyword{internal} 22 | -------------------------------------------------------------------------------- /man/str_trim.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/utils.R 3 | \name{str_trim} 4 | \alias{str_trim} 5 | \title{Trim whitespace from the beginning and end of a string} 6 | \usage{ 7 | str_trim(x) 8 | } 9 | \arguments{ 10 | \item{x}{Input string or character vector.} 11 | } 12 | \value{ 13 | Trimmed character vector. 14 | } 15 | \description{ 16 | Trim whitespace from the beginning and end of a string 17 | } 18 | -------------------------------------------------------------------------------- /man/default_cran_mirror.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/urls.R 3 | \docType{data} 4 | \name{default_cran_mirror} 5 | \alias{default_cran_mirror} 6 | \title{CRAN mirror to use} 7 | \format{An object of class \code{character} of length 1.} 8 | \usage{ 9 | default_cran_mirror 10 | } 11 | \description{ 12 | The RStudio mirror is in the Amazon cloud, so most times it has 13 | the best response times, and download speed. 14 | } 15 | \keyword{internal} 16 | -------------------------------------------------------------------------------- /man/download_urls.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/urls.R 3 | \name{download_urls} 4 | \alias{download_urls} 5 | \title{Get download urls for a bunch of packages} 6 | \usage{ 7 | download_urls(pkgs) 8 | } 9 | \arguments{ 10 | \item{pkgs}{Data frame of packages.} 11 | } 12 | \value{ 13 | A list of character vectors, a set of URLs for each package. 14 | } 15 | \description{ 16 | Get download urls for a bunch of packages 17 | } 18 | \keyword{internal} 19 | -------------------------------------------------------------------------------- /man/data_frame.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/utils.R 3 | \name{data_frame} 4 | \alias{data_frame} 5 | \title{Create a data frame, more robust than \code{data.frame}} 6 | \usage{ 7 | data_frame(...) 8 | } 9 | \arguments{ 10 | \item{...}{Data frame columns.} 11 | } 12 | \value{ 13 | The constructed data frame. 14 | } 15 | \description{ 16 | It does not create factor columns. 17 | It recycles columns to match the longest column. 18 | } 19 | \keyword{internal} 20 | -------------------------------------------------------------------------------- /man/pkg_from_filename.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/utils.R 3 | \name{pkg_from_filename} 4 | \alias{pkg_from_filename} 5 | \title{Extract the package name from a package tarball path or filename} 6 | \usage{ 7 | pkg_from_filename(path) 8 | } 9 | \arguments{ 10 | \item{path}{The package tarball path(s).} 11 | } 12 | \value{ 13 | Package name(s). 14 | } 15 | \description{ 16 | Extract the package name from a package tarball path or filename 17 | } 18 | \keyword{internal} 19 | -------------------------------------------------------------------------------- /man/try_download.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/download.R 3 | \name{try_download} 4 | \alias{try_download} 5 | \title{Try to download a file} 6 | \usage{ 7 | try_download(url, dest_file) 8 | } 9 | \arguments{ 10 | \item{url}{Download URL.} 11 | 12 | \item{dest_file}{Where to put the downloaded file.} 13 | } 14 | \value{ 15 | \code{TRUE} if the download was successful, \code{FALSE} 16 | otherwise. 17 | } 18 | \description{ 19 | Try to download a file 20 | } 21 | \keyword{internal} 22 | -------------------------------------------------------------------------------- /man/get_package_sources.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/metadata.R 3 | \name{get_package_sources} 4 | \alias{get_package_sources} 5 | \title{Check where a package was installed from} 6 | \usage{ 7 | get_package_sources(pkgs) 8 | } 9 | \arguments{ 10 | \item{pkgs}{The matrix of parsed DESCRIPTION files, 11 | out of `installed.packages`.} 12 | } 13 | \value{ 14 | Two columns, source and link 15 | } 16 | \description{ 17 | Check where a package was installed from 18 | } 19 | \keyword{internal} 20 | -------------------------------------------------------------------------------- /man/get_description.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/deps.R 3 | \name{get_description} 4 | \alias{get_description} 5 | \title{Extract and read the DESCRIPTION file from an R package tarball} 6 | \usage{ 7 | get_description(package_file) 8 | } 9 | \arguments{ 10 | \item{package_file}{Path and name of the tarball.} 11 | } 12 | \value{ 13 | A named list of DESCRIPTION fields. 14 | } 15 | \description{ 16 | Extract and read the DESCRIPTION file from an R package tarball 17 | } 18 | \keyword{internal} 19 | -------------------------------------------------------------------------------- /man/drop_missing_deps.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/restore.R 3 | \name{drop_missing_deps} 4 | \alias{drop_missing_deps} 5 | \title{Drop dependencies that were not included in the snapshot} 6 | \usage{ 7 | drop_missing_deps(deps) 8 | } 9 | \arguments{ 10 | \item{deps}{A named list of character vectors.} 11 | } 12 | \value{ 13 | A (filtered) named list of character vectors. 14 | } 15 | \description{ 16 | These are probably not needed for the installed functions 17 | to work. 18 | } 19 | \keyword{internal} 20 | -------------------------------------------------------------------------------- /man/parse_deps.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/deps.R 3 | \name{parse_deps} 4 | \alias{parse_deps} 5 | \title{Parse a DESCRIPTION dependency field} 6 | \usage{ 7 | parse_deps(type, deps) 8 | } 9 | \arguments{ 10 | \item{type}{Field name, e.g. \code{Imports}.} 11 | 12 | \item{deps}{The value of the field.} 13 | } 14 | \value{ 15 | A data frame with three columns: \code{type}, 16 | \code{package} and \code{version}. 17 | } 18 | \description{ 19 | Parse a DESCRIPTION dependency field 20 | } 21 | \keyword{internal} 22 | -------------------------------------------------------------------------------- /man/add_R_core.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/snapshot.R 3 | \name{add_R_core} 4 | \alias{add_R_core} 5 | \title{Add R version to package inventory} 6 | \usage{ 7 | add_R_core(pkgs) 8 | } 9 | \arguments{ 10 | \item{pkgs}{data.frame of installed packages with columns Package and Version.} 11 | } 12 | \value{ 13 | The same data.frame with the R version listed as "R" and the 14 | version in major.minor format (e.g. R 3.2.2). 15 | } 16 | \description{ 17 | Add R version to package inventory 18 | } 19 | \keyword{internal} 20 | -------------------------------------------------------------------------------- /man/dep_types.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/deps.R 3 | \docType{data} 4 | \name{dep_types} 5 | \alias{dep_types} 6 | \alias{hard_dep_types} 7 | \title{Dependency types in R DESCRIPTION files} 8 | \format{An object of class \code{character} of length 5.} 9 | \usage{ 10 | dep_types 11 | 12 | hard_dep_types 13 | } 14 | \description{ 15 | Dependency types in R DESCRIPTION files 16 | 17 | A dependency is hard if the depended package is required 18 | for installing and/or loading the package. 19 | } 20 | \keyword{internal} 21 | -------------------------------------------------------------------------------- /man/check_R_core.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/restore.R 3 | \name{check_R_core} 4 | \alias{check_R_core} 5 | \title{Check listed R version against installed} 6 | \usage{ 7 | check_R_core(pkgs, R) 8 | } 9 | \arguments{ 10 | \item{pkgs}{data.frame read from the csv file.} 11 | 12 | \item{R}{If TRUE it will error when the R versions mismatch. 13 | Otherwise it will just give a warning.} 14 | } 15 | \value{ 16 | The same data.frame with the R package removed. 17 | } 18 | \description{ 19 | Check listed R version against installed 20 | } 21 | \keyword{internal} 22 | -------------------------------------------------------------------------------- /man/filename_from_url.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/download.R 3 | \name{filename_from_url} 4 | \alias{filename_from_url} 5 | \title{Extract a file name from a package download URL} 6 | \usage{ 7 | filename_from_url(url, pkg) 8 | } 9 | \arguments{ 10 | \item{url}{The URL, a character scalar.} 11 | 12 | \item{pkg}{The name of the package the URL belongs to.} 13 | } 14 | \value{ 15 | Character scalar, the file name. 16 | } 17 | \description{ 18 | This is usually just the part after the last slash, 19 | but for https://github.com/cran/* URLs it is a bit trickier. 20 | } 21 | \keyword{internal} 22 | -------------------------------------------------------------------------------- /man/get_deps.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/deps.R 3 | \name{get_deps} 4 | \alias{get_deps} 5 | \title{Extract (hard) package dependencies from an R package tarball} 6 | \usage{ 7 | get_deps(package_file) 8 | } 9 | \arguments{ 10 | \item{package_file}{Path and name of the tarball.} 11 | } 12 | \value{ 13 | A character vector of depended packages. Version numbers 14 | are not included, as we don't need them for the current purposes 15 | of this package. 16 | } 17 | \description{ 18 | Hard dependencies include \code{Imports}, \code{Depends} and 19 | \code{LinkingTo}. 20 | } 21 | \keyword{internal} 22 | -------------------------------------------------------------------------------- /DESCRIPTION: -------------------------------------------------------------------------------- 1 | Package: pkgsnap 2 | Title: Backup and Restore CRAN Package Versions 3 | Version: 1.0.0 4 | Author: Gabor Csardi 5 | Maintainer: Gabor Csardi 6 | Description: Create a snapshot of your installed CRAN packages with 7 | 'snap', and then use 'restore' on another system to recreate 8 | exactly the same environment. 9 | License: MIT + file LICENSE 10 | LazyData: true 11 | URL: https://github.com/mangothecat/pkgsnap 12 | BugReports: https://github.com/mangothecat/pkgsnap/issues 13 | Suggests: 14 | pingr, 15 | remotes, 16 | testthat, 17 | withr 18 | Remotes: 19 | MangoTheCat/remotes 20 | RoxygenNote: 6.0.1 21 | -------------------------------------------------------------------------------- /man/pkg_download.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/download.R 3 | \name{pkg_download} 4 | \alias{pkg_download} 5 | \title{Download R packages (or other files)} 6 | \usage{ 7 | pkg_download(pkgs, dest_dir = ".") 8 | } 9 | \arguments{ 10 | \item{pkgs}{The data frame of packages to download.} 11 | 12 | \item{dest_dir}{Destination directory for the downloaded files. 13 | The actual file names are extracted from the URLs.} 14 | } 15 | \value{ 16 | Path to the downloaded file, or \code{NA_character_} 17 | if all URLs failed. 18 | } 19 | \description{ 20 | Download R packages (or other files) 21 | } 22 | \keyword{internal} 23 | -------------------------------------------------------------------------------- /man/install_order.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/restore.R 3 | \name{install_order} 4 | \alias{install_order} 5 | \title{Topological order of the packages} 6 | \usage{ 7 | install_order(graph) 8 | } 9 | \arguments{ 10 | \item{graph}{A named list of character vectors, interpreted as 11 | an adjacnecy list. If \code{A->B} then package \code{A} depends 12 | on package \code{B}, so package \code{B} must be loaded before 13 | package \code{A}.} 14 | } 15 | \value{ 16 | Character vector of package names in an order that 17 | can be used to install them. 18 | } 19 | \description{ 20 | This is the correct installation order. 21 | } 22 | \keyword{internal} 23 | -------------------------------------------------------------------------------- /man/drop_internal.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/deps.R 3 | \name{drop_internal} 4 | \alias{drop_internal} 5 | \title{Drop base and recommended packages, and \sQuote{R} from a list 6 | of R packages} 7 | \usage{ 8 | drop_internal(pkgs) 9 | } 10 | \arguments{ 11 | \item{pkgs}{Character vector of package names.} 12 | } 13 | \value{ 14 | Character vector of filtered package names. 15 | } 16 | \description{ 17 | \sQuote{R} can be included in the DESCRIPTION file, as a dependency, 18 | but we ignore this right now. We also ignore base and recommended 19 | packages, these are supposed to be installed on the system, together 20 | with R. 21 | } 22 | \keyword{internal} 23 | -------------------------------------------------------------------------------- /man/restore.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/restore.R 3 | \name{restore} 4 | \alias{restore} 5 | \title{Restore (=install) certain CRAN package versions} 6 | \usage{ 7 | restore(from = "packages.csv", R = TRUE, ...) 8 | } 9 | \arguments{ 10 | \item{from}{Name of a file created by \code{\link{snap}}. 11 | Alternatively a data frame with columns \code{Package} and 12 | \code{Version}.} 13 | 14 | \item{R}{If TRUE the target version of R must match. 15 | Otherwise it will only give a warning.} 16 | 17 | \item{...}{Additional arguments, passed to \code{install.packages}.} 18 | } 19 | \description{ 20 | Functions that were not installed from CRAN will not be restored, 21 | they will be ignored with a warning. The pkgsnap package itself is 22 | also ignored as it must be installed to run this function. 23 | } 24 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | ## Sample .travis.yml file for use with metacran/r-builder 2 | ## See https://github.com/metacran/r-builder for details. 3 | 4 | language: c 5 | sudo: required 6 | 7 | before_install: 8 | - curl -OL https://raw.githubusercontent.com/metacran/r-builder/master/pkg-build.sh 9 | - chmod 755 pkg-build.sh 10 | - ./pkg-build.sh bootstrap 11 | 12 | install: 13 | - ./pkg-build.sh install_deps 14 | - ./pkg-build.sh install_github jimhester/covr 15 | 16 | script: 17 | - ./pkg-build.sh run_tests 18 | 19 | after_failure: 20 | - ./pkg-build.sh dump_logs 21 | 22 | after_success: 23 | - if [[ ! -z "$COVERAGE" ]];then ./pkg-build.sh run_script -e 'covr::codecov()'; fi 24 | 25 | notifications: 26 | email: 27 | on_success: change 28 | on_failure: change 29 | 30 | env: 31 | matrix: 32 | - RVERSION=oldrel 33 | - RVERSION=release 34 | - RVERSION=devel 35 | 36 | -------------------------------------------------------------------------------- /man/cran_file.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/urls.R 3 | \name{cran_file} 4 | \alias{cran_file} 5 | \title{Get a list of candidate URLs for a certain version of a package} 6 | \usage{ 7 | cran_file(package, version, type = get_pkg_type(), 8 | r_minor = r_minor_version(), cran_mirror = default_cran_mirror) 9 | } 10 | \arguments{ 11 | \item{package}{Name of the package, e.g. \code{jsonlite}.} 12 | 13 | \item{version}{Version number of the package, as a string, e.g. 14 | \code{1.0.0}.} 15 | 16 | \item{type}{Package type, e.g. \code{binary}, \code{source}, etc. 17 | See the \code{type} argument of \code{utils::install.packages}.} 18 | 19 | \item{r_minor}{The minor R version to search for packages for. 20 | Defaults to the currently running R version.} 21 | } 22 | \value{ 23 | Character vector or URLs. 24 | } 25 | \description{ 26 | Get a list of candidate URLs for a certain version of a package 27 | } 28 | \keyword{internal} 29 | -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | # DO NOT CHANGE the "init" and "install" sections below 2 | 3 | # Download script file from GitHub 4 | init: 5 | ps: | 6 | $ErrorActionPreference = "Stop" 7 | Invoke-WebRequest http://raw.github.com/krlmlr/r-appveyor/master/scripts/appveyor-tool.ps1 -OutFile "..\appveyor-tool.ps1" 8 | Import-Module '..\appveyor-tool.ps1' 9 | 10 | install: 11 | ps: Bootstrap 12 | 13 | # Adapt as necessary starting from here 14 | 15 | build_script: 16 | - travis-tool.sh install_deps 17 | 18 | test_script: 19 | - travis-tool.sh run_tests 20 | 21 | on_failure: 22 | - travis-tool.sh dump_logs 23 | 24 | artifacts: 25 | - path: '*.Rcheck\**\*.log' 26 | name: Logs 27 | 28 | - path: '*.Rcheck\**\*.out' 29 | name: Logs 30 | 31 | - path: '*.Rcheck\**\*.fail' 32 | name: Logs 33 | 34 | - path: '*.Rcheck\**\*.Rout' 35 | name: Logs 36 | 37 | - path: '\*_*.tar.gz' 38 | name: Bits 39 | 40 | - path: '\*_*.zip' 41 | name: Bits 42 | -------------------------------------------------------------------------------- /man/snap.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/snapshot.R 3 | \name{snap} 4 | \alias{snap} 5 | \title{Write installed package versions to a file} 6 | \usage{ 7 | snap(to = "packages.csv", lib.loc = NULL, recommended = FALSE) 8 | } 9 | \arguments{ 10 | \item{to}{File to write the package versions to, 11 | defaults to \code{packages.csv}. If it is NULL, 12 | to output file is created and the result is returned 13 | as a data frame.} 14 | 15 | \item{lib.loc}{character vector describing the 16 | location of R library trees to search through, or 17 | \code{NULL} for all known trees (see 18 | \code{\link[base]{.libPaths}}).} 19 | 20 | \item{recommended}{if TRUE then recommended packages 21 | will be included in the snapshot.} 22 | } 23 | \value{ 24 | A two columns data frame, invisibly if it was 25 | written to a file. 26 | } 27 | \description{ 28 | Base and recommended packages are omitted. 29 | Packages that were installed from non-CRAN sources are 30 | also included, but you won't be able to restore them 31 | with \code{\link{restore}}. 32 | } 33 | \details{ 34 | The output file will have two columns, package name 35 | and package version. 36 | } 37 | \examples{ 38 | snap(to = tmp <- tempfile()) 39 | 40 | head(read.csv(tmp)) 41 | } 42 | -------------------------------------------------------------------------------- /tests/testthat/test-urls.R: -------------------------------------------------------------------------------- 1 | 2 | context("Download URLs") 3 | 4 | test_that("URLs for CRAN packages", { 5 | 6 | skip_on_cran() 7 | skip_if_offline() 8 | 9 | ## Get latest version of pkgconfig, TODO 10 | latest <- "2.0.0" 11 | 12 | pkgs <- data.frame( 13 | stringsAsFactors = FALSE, 14 | Package = c("pkgconfig", "pkgconfig"), 15 | Version = c(latest, "1.0.0"), 16 | Source = c("cran", "cran"), 17 | Link = c(NA_character_, NA_character_) 18 | ) 19 | 20 | urls <- download_urls(pkgs) 21 | expect_true(any( 22 | grepl(paste0("pkgconfig_", latest, ".tar.gz"), urls[[1]], fixed = TRUE) 23 | )) 24 | expect_true(any( 25 | grepl("pkgconfig_1.0.0.tar.gz", urls[[2]], fixed = TRUE) 26 | )) 27 | }) 28 | 29 | 30 | test_that("URLs for url packages", { 31 | 32 | pkgs <- data.frame( 33 | stringsAsFactors = FALSE, 34 | Package = c("pkgconfig", "pkgconfig"), 35 | Version = c("1.0.0", "2.0.0"), 36 | Source = c("cran", "url"), 37 | Link = c(NA_character_, "https://dummy-link.com") 38 | ) 39 | 40 | urls <- download_urls(pkgs) 41 | expect_true(any( 42 | grep(paste0("pkgconfig_1.0.0.tar.gz"), urls[[1]], fixed = TRUE) 43 | )) 44 | expect_true(any( 45 | grep("https://dummy-link.com", urls[[2]], fixed = TRUE) 46 | )) 47 | }) 48 | -------------------------------------------------------------------------------- /R/metadata.R: -------------------------------------------------------------------------------- 1 | 2 | extra_fields <- c("Repository", "biocViews", "RemoteType", "RemoteUrl") 3 | 4 | get_package_metadata <- function(lib.loc, priority) { 5 | pkgs <- installed.packages( 6 | lib.loc = lib.loc, 7 | priority = priority, 8 | fields = extra_fields 9 | ) 10 | 11 | sources <- get_package_sources(pkgs) 12 | 13 | pkgs <- cbind(pkgs[, c("Package", "Version"), drop = FALSE], sources) 14 | 15 | rownames(pkgs) <- NULL 16 | as.data.frame(pkgs, stringsAsFactors = FALSE) 17 | } 18 | 19 | #' Check where a package was installed from 20 | #' 21 | #' @param pkgs The matrix of parsed DESCRIPTION files, 22 | #' out of `installed.packages`. 23 | #' @return Two columns, source and link 24 | #' 25 | #' @keywords internal 26 | 27 | get_package_sources <- function(pkgs) { 28 | source <- rep(NA, nrow(pkgs)) 29 | link <- rep(NA, nrow(pkgs)) 30 | 31 | ## CRAN 32 | cran <- vapply(pkgs[, "Repository"], identical, TRUE, y = "CRAN") 33 | source[cran] <- "cran" 34 | 35 | ## R-Forge 36 | rforge <- vapply(pkgs[, "Repository"], identical, TRUE, y = "R-Forge") 37 | source[rforge] <- "rforge" 38 | 39 | ## BioC 40 | bioc <- ! vapply(pkgs[, "biocViews" ], is.na, TRUE) 41 | source[bioc] <- "bioc" 42 | 43 | ## Packages installed from URLs via devtools::install_url or 44 | ## remotes::install_url 45 | url <- 46 | vapply(pkgs[, "RemoteType"], identical, TRUE, y = "url") & 47 | ! vapply(pkgs[, "RemoteUrl"], is.na, TRUE) 48 | source[url] <- "url" 49 | link[url] <- pkgs[url, "RemoteUrl"] 50 | 51 | cbind(Source = source, Link = link) 52 | } 53 | -------------------------------------------------------------------------------- /tests/testthat/test-metadata.R: -------------------------------------------------------------------------------- 1 | 2 | context("Getting package metadata") 3 | 4 | test_that("works well for CRAN packages", { 5 | 6 | skip_on_cran() 7 | skip_if_offline() 8 | 9 | tmp <- tempfile() 10 | dir.create(tmp) 11 | on.exit(unlink(tmp, recursive = TRUE), add = TRUE) 12 | 13 | install.packages("pkgconfig", lib = tmp, quiet = TRUE) 14 | install.packages("falsy", lib = tmp, quiet = TRUE) 15 | 16 | pkgs <- get_package_metadata(tmp, priority = NA_character_) 17 | 18 | expect_equal( 19 | pkgs$Source, 20 | c("cran", "cran") 21 | ) 22 | 23 | }) 24 | 25 | test_that("works for bioc packages", { 26 | 27 | skip_on_cran() 28 | skip_if_offline() 29 | 30 | tmp <- tempfile() 31 | dir.create(tmp) 32 | on.exit(unlink(tmp, recursive = TRUE), add = TRUE) 33 | 34 | withr::with_libpaths(tmp, { 35 | ## Install 36 | install.packages("pkgconfig", lib = tmp, quiet = TRUE) 37 | source("https://bioconductor.org/biocLite.R") 38 | biocLite("BiocInstaller", lib = tmp, quiet = TRUE, ask = FALSE, 39 | suppressUpdates = TRUE) 40 | 41 | pkgs <- get_package_metadata(tmp, priority = NA_character_) 42 | 43 | expect_equal( 44 | pkgs$Source, 45 | c("bioc", "cran") 46 | ) 47 | }) 48 | }) 49 | 50 | test_that("works for url packages", { 51 | 52 | skip_on_cran() 53 | skip_if_offline() 54 | 55 | tmp <- tempfile() 56 | dir.create(tmp) 57 | on.exit(unlink(tmp, recursive = TRUE), add = TRUE) 58 | 59 | install.packages("pkgconfig", lib = tmp, quiet = TRUE) 60 | remotes::install_url( 61 | "https://cran.rstudio.com/src/contrib/sankey_1.0.0.tar.gz", 62 | lib = tmp, 63 | quiet = TRUE 64 | ) 65 | 66 | pkgs <- get_package_metadata(tmp, priority = NA_character_) 67 | 68 | expect_equal( 69 | pkgs$Source, 70 | c("cran", "url") 71 | ) 72 | 73 | expect_equal( 74 | pkgs$Link, 75 | c(NA_character_, 76 | "https://cran.rstudio.com/src/contrib/sankey_1.0.0.tar.gz" 77 | ) 78 | ) 79 | }) 80 | -------------------------------------------------------------------------------- /R/utils.R: -------------------------------------------------------------------------------- 1 | 2 | #' LHS if not \code{NULL}, otherwise RHS 3 | #' 4 | #' @param l LHS. 5 | #' @param r RHS. 6 | #' @return LHS if not \code{NULL}, otherwise RHS. 7 | #' 8 | #' @name OR 9 | #' @keywords internal 10 | 11 | `%||%` <- function(l, r) { 12 | if (is.null(l)) r else l 13 | } 14 | 15 | #' Create a data frame, more robust than \code{data.frame} 16 | #' 17 | #' It does not create factor columns. 18 | #' It recycles columns to match the longest column. 19 | #' 20 | #' @param ... Data frame columns. 21 | #' @return The constructed data frame. 22 | #' 23 | #' @keywords internal 24 | 25 | data_frame <- function(...) { 26 | 27 | args <- list(...) 28 | 29 | ## Replicate arguments if needed 30 | len <- vapply(args, length, numeric(1)) 31 | stopifnot(length(setdiff(len, 1)) <= 1) 32 | len <- max(0, max(len)) 33 | args <- lapply(args, function(x) rep(x, length.out = len)) 34 | 35 | ## Names 36 | names <- as.character(names(args)) 37 | length(names) <- length(args) 38 | names <- ifelse( 39 | is.na(names) | names == "", 40 | paste0("V", seq_along(args)), 41 | names) 42 | 43 | structure(args, 44 | class = "data.frame", 45 | names = names, 46 | row.names = seq_along(args[[1]])) 47 | } 48 | 49 | #' Check if a directory exists 50 | #' 51 | #' @param dir Directory to check. 52 | #' @return Logical scalar. 53 | #' 54 | #' @keywords internal 55 | 56 | dir_exists <- function(dir) { 57 | file.exists(dir) & file.info(dir)$isdir 58 | } 59 | 60 | #' Trim whitespace from the beginning and end of a string 61 | #' 62 | #' @param x Input string or character vector. 63 | #' @return Trimmed character vector. 64 | 65 | str_trim <- function(x) { 66 | sub("\\s*$", "", sub("^\\s*", "", x)) 67 | } 68 | 69 | #' Extract the package name from a package tarball path or filename 70 | #' 71 | #' @param path The package tarball path(s). 72 | #' @return Package name(s). 73 | #' 74 | #' @keywords internal 75 | 76 | pkg_from_filename <- function(path) { 77 | sub("_.*$", "", basename(path)) 78 | } 79 | -------------------------------------------------------------------------------- /R/snapshot.R: -------------------------------------------------------------------------------- 1 | 2 | #' Write installed package versions to a file 3 | #' 4 | #' Base and recommended packages are omitted. 5 | #' Packages that were installed from non-CRAN sources are 6 | #' also included, but you won't be able to restore them 7 | #' with \code{\link{restore}}. 8 | #' 9 | #' The output file will have two columns, package name 10 | #' and package version. 11 | #' 12 | #' @param to File to write the package versions to, 13 | #' defaults to \code{packages.csv}. If it is NULL, 14 | #' to output file is created and the result is returned 15 | #' as a data frame. 16 | #' @param lib.loc character vector describing the 17 | #' location of R library trees to search through, or 18 | #' \code{NULL} for all known trees (see 19 | #' \code{\link[base]{.libPaths}}). 20 | #' @param recommended if TRUE then recommended packages 21 | #' will be included in the snapshot. 22 | #' @return A two columns data frame, invisibly if it was 23 | #' written to a file. 24 | #' 25 | #' @export 26 | #' @importFrom utils installed.packages write.csv 27 | #' @examples 28 | #' snap(to = tmp <- tempfile()) 29 | #' 30 | #' head(read.csv(tmp)) 31 | 32 | snap <- function(to = "packages.csv", lib.loc = NULL, recommended = FALSE) { 33 | 34 | priority <- if (recommended) c( "recommended", NA_character_) else NA_character_ 35 | 36 | pkgs <- get_package_metadata(lib.loc = lib.loc, priority = priority) 37 | 38 | # Add the R version to the top of the list 39 | pkgs <- add_R_core(pkgs) 40 | 41 | if (!is.null(to)) { 42 | write.csv(pkgs, file = to, row.names = FALSE) 43 | invisible(pkgs) 44 | } else { 45 | pkgs 46 | } 47 | } 48 | 49 | #' Add R version to package inventory 50 | #' 51 | #' @param pkgs data.frame of installed packages with columns Package and Version. 52 | #' 53 | #' @return The same data.frame with the R version listed as "R" and the 54 | #' version in major.minor format (e.g. R 3.2.2). 55 | #' 56 | #' @keywords internal 57 | #' 58 | add_R_core <- function(pkgs) { 59 | 60 | # Check it's in the format we're expecting 61 | if(length(pkgs) != 4) stop("pkgs does not have 2 columns") 62 | if(!all(names(pkgs) == c("Package", "Version", "Source", "Link"))) { 63 | stop("pkgs Col names should be Package, Version, Source, Link") 64 | } 65 | 66 | coreVersion <- paste(R.version$major, R.version$minor, sep = ".") 67 | 68 | coreEntry <- data.frame( 69 | Package = "R", 70 | Version = coreVersion, 71 | Source = "R", 72 | Link = NA_character_, 73 | stringsAsFactors = FALSE 74 | ) 75 | 76 | rbind(coreEntry, pkgs) 77 | } 78 | -------------------------------------------------------------------------------- /tests/testthat/test-restore.R: -------------------------------------------------------------------------------- 1 | 2 | context("Restore") 3 | 4 | test_that("CRAN packages are fine", { 5 | 6 | skip_on_cran() 7 | skip_if_offline() 8 | 9 | tmp <- tempfile() 10 | dir.create(tmp) 11 | on.exit(unlink(tmp, recursive = TRUE), add = TRUE) 12 | 13 | withr::with_libpaths(tmp, { 14 | ## Install 15 | install.packages("pkgconfig", lib = tmp, quiet = TRUE) 16 | source("https://bioconductor.org/biocLite.R") 17 | biocLite("BiocGenerics", lib = tmp, quiet = TRUE, ask = FALSE, 18 | suppressUpdates = TRUE) 19 | 20 | ## Snapshot 21 | pkgs <- tempfile() 22 | snap(to = pkgs, lib.loc = tmp) 23 | 24 | ## Remove 25 | unlink(tmp, recursive = TRUE) 26 | dir.create(tmp) 27 | 28 | ## Restore 29 | restore(from = pkgs, lib = tmp) 30 | 31 | ## Check 32 | inst <- installed.packages(lib = tmp) 33 | expect_equal(rownames(inst), c("BiocGenerics", "pkgconfig")) 34 | }) 35 | }) 36 | 37 | 38 | test_that("Packages from URLs are fine", { 39 | 40 | skip_on_cran() 41 | skip_if_offline() 42 | 43 | tmp <- tempfile() 44 | dir.create(tmp) 45 | on.exit(unlink(tmp, recursive = TRUE), add = TRUE) 46 | 47 | ## Install 48 | install.packages("pkgconfig", lib = tmp, quiet = TRUE) 49 | remotes::install_url( 50 | "https://cran.rstudio.com/src/contrib/sankey_1.0.0.tar.gz", 51 | lib = tmp, 52 | quiet = TRUE 53 | ) 54 | 55 | ## Snapshot 56 | pkgs <- tempfile() 57 | snap(to = pkgs, lib.loc = tmp) 58 | 59 | ## Remove 60 | unlink(tmp, recursive = TRUE) 61 | dir.create(tmp) 62 | 63 | ## Restore 64 | restore(from = pkgs, lib = tmp) 65 | 66 | ## Check 67 | inst <- installed.packages(lib = tmp) 68 | expect_equal(rownames(inst), c("pkgconfig", "sankey")) 69 | 70 | }) 71 | 72 | test_that("Packages from R-Forge are fine", { 73 | 74 | skip_on_cran() 75 | skip_if_offline() 76 | 77 | tmp <- tempfile() 78 | dir.create(tmp) 79 | on.exit(unlink(tmp, recursive = TRUE), add = TRUE) 80 | 81 | ## Install 82 | install.packages("pkgconfig", lib = tmp, quiet = TRUE) 83 | suppressWarnings( 84 | install.packages( 85 | "MSToolkit", 86 | repos = "http://R-Forge.R-project.org", 87 | lib = tmp, 88 | quiet = TRUE 89 | ) 90 | ) 91 | 92 | ## Snapshot 93 | pkgs <- tempfile() 94 | snap(to = pkgs, lib.loc = tmp) 95 | 96 | ## Remove 97 | unlink(tmp, recursive = TRUE) 98 | dir.create(tmp) 99 | 100 | ## Restore 101 | restore(from = pkgs, lib = tmp) 102 | 103 | ## Check 104 | inst <- installed.packages(lib = tmp) 105 | expect_equal(rownames(inst), c("MSToolkit", "pkgconfig")) 106 | 107 | }) 108 | -------------------------------------------------------------------------------- /R/download.R: -------------------------------------------------------------------------------- 1 | 2 | #' Download R packages (or other files) 3 | #' 4 | #' @param pkgs The data frame of packages to download. 5 | #' @param dest_dir Destination directory for the downloaded files. 6 | #' The actual file names are extracted from the URLs. 7 | #' @return Path to the downloaded file, or \code{NA_character_} 8 | #' if all URLs failed. 9 | #' 10 | #' @keywords internal 11 | 12 | pkg_download <- function(pkgs, dest_dir = ".") { 13 | dest_dir <- as.character(dest_dir) 14 | 15 | stopifnot(all(!is.na(dest_dir)), length(dest_dir) == 1) 16 | stopifnot(dir_exists(dest_dir)) 17 | 18 | message("Downloading") 19 | urls <- download_urls(pkgs) 20 | result <- vapply(seq_along(urls), FUN.VALUE = "", FUN = function(i) { 21 | url <- urls[[i]] 22 | if (! length(url)) message(" ", pkgs[i, "Package"], " Error: no files.") 23 | res <- FALSE 24 | for (u in url) { 25 | dest_file <- file.path( 26 | dest_dir, 27 | filename_from_url(u, pkgs[i, "Package"]) 28 | ) 29 | message(" ", basename(u), "... ", appendLF = FALSE) 30 | if (res <- try_download(u, dest_file)) break 31 | } 32 | if (length(url)) message(if (res) " done." else "ERROR.") 33 | 34 | if (!res) { 35 | warning("Cannot download package ", pkgs[i, "Package"], call. = FALSE) 36 | NA_character_ 37 | 38 | } else { 39 | dest_file 40 | } 41 | }) 42 | 43 | names(result) <- paste(pkgs$Package, sep = "-", pkgs$Version) 44 | invisible(result) 45 | } 46 | 47 | #' Extract a file name from a package download URL 48 | #' 49 | #' This is usually just the part after the last slash, 50 | #' but for https://github.com/cran/* URLs it is a bit trickier. 51 | #' 52 | #' @param url The URL, a character scalar. 53 | #' @param pkg The name of the package the URL belongs to. 54 | #' @return Character scalar, the file name. 55 | #' 56 | #' @keywords internal 57 | 58 | filename_from_url <- function(url, pkg) { 59 | if (grepl("^https://[^/\\.]*\\.github.com/", url)) { 60 | paste0(sub("^[a-z]+:", "", sub("-", "_", pkg)), ".tar.gz") 61 | } else { 62 | basename(url) 63 | } 64 | } 65 | 66 | #' Try to download a file 67 | #' 68 | #' @param url Download URL. 69 | #' @param dest_file Where to put the downloaded file. 70 | #' @return \code{TRUE} if the download was successful, \code{FALSE} 71 | #' otherwise. 72 | #' 73 | #' @importFrom utils download.file 74 | #' @keywords internal 75 | 76 | try_download <- function(url, dest_file) { 77 | 78 | if (file.exists(dest_file)) return(TRUE) 79 | 80 | resp <- try( 81 | suppressWarnings( 82 | download.file(url, destfile = dest_file, quiet = TRUE) 83 | ), 84 | silent = TRUE 85 | ) 86 | 87 | if (inherits(resp, "try-error")) { 88 | unlink(dest_file, recursive = TRUE, force = TRUE) 89 | FALSE 90 | } else { 91 | TRUE 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /inst/README.Rmd: -------------------------------------------------------------------------------- 1 | 2 | ```{r, setup, echo = FALSE, message = FALSE} 3 | knitr::opts_chunk$set( 4 | comment = "#>", 5 | tidy = FALSE, 6 | error = FALSE, 7 | fig.width = 8, 8 | fig.height = 8) 9 | ``` 10 | 11 | # pkgsnap 12 | 13 | > Backup and Restore Certain CRAN Package Versions 14 | 15 | [![Project Status: Active - The project has reached a stable, usable state and is being actively developed.](http://www.repostatus.org/badges/latest/active.svg)](http://www.repostatus.org/#active) 16 | [![Linux Build Status](https://travis-ci.org/MangoTheCat/pkgsnap.svg?branch=master)](https://travis-ci.org/MangoTheCat/pkgsnap) 17 | [![Windows Build status](https://ci.appveyor.com/api/projects/status/github/MangoTheCat/pkgsnap?svg=true)](https://ci.appveyor.com/project/gaborcsardi/pkgsnap) 18 | [![Coverage Status](https://img.shields.io/codecov/c/github/mangothecat/pkgsnap/master.svg)](https://codecov.io/github/mangothecat/pkgsnap?branch=master) 19 | [![](http://www.r-pkg.org/badges/version/pkgsnap)](http://www.r-pkg.org/pkg/pkgsnap) 20 | [![CRAN RStudio mirror downloads](http://cranlogs.r-pkg.org/badges/pkgsnap)](http://www.r-pkg.org/pkg/pkgsnap) 21 | 22 | 23 | Create a snapshot of your installed CRAN packages with 'snap', and then 24 | use 'restore' on another system to recreate exactly the same environment. 25 | 26 | ## Installation 27 | 28 | ```{r eval = FALSE} 29 | devtools::install_github("mangothecat/pkgsnap") 30 | ``` 31 | 32 | ## Usage 33 | 34 | ```{r} 35 | library(pkgsnap) 36 | ``` 37 | 38 | For this experiment we create a new library directory, and install 39 | some packages there. We will then remove this directory entirely, 40 | and recreate it using `pkgsnap`. 41 | 42 | ```{r} 43 | lib_dir <- tempfile() 44 | dir.create(lib_dir) 45 | ``` 46 | 47 | We make this new library directory the default: 48 | 49 | ```{r} 50 | .libPaths(lib_dir) 51 | ``` 52 | 53 | The new library directory is currently empty: 54 | 55 | ```{r} 56 | installed.packages(lib_dir)[, c("Package", "Version")] 57 | ``` 58 | 59 | Let's install some packages here. Note that the dependencies of these 60 | packages will be also installed. 61 | 62 | ```{r} 63 | install.packages(c("testthat", "pkgconfig")) 64 | installed.packages(lib_dir)[, c("Package", "Version")] 65 | ``` 66 | 67 | We will now create a snapshot, and then scrap the temporary package 68 | library. 69 | 70 | ```{r} 71 | snapshot <- tempfile() 72 | snap(to = snapshot) 73 | read.csv(snapshot)[1:5,] 74 | unlink(lib_dir, recursive = TRUE) 75 | ``` 76 | 77 | Create a new package library. 78 | 79 | ```{r} 80 | new_lib_dir <- tempfile() 81 | dir.create(new_lib_dir) 82 | .libPaths(new_lib_dir) 83 | ``` 84 | 85 | Finally, recreate the same set of package versions, in a new package 86 | library. 87 | 88 | ```{r} 89 | restore(snapshot) 90 | installed.packages(new_lib_dir)[, c("Package", "Version")] 91 | ``` 92 | 93 | 94 | ## License 95 | 96 | MIT © [Mango Solutions](https://github.com/mangothecat). 97 | -------------------------------------------------------------------------------- /R/deps.R: -------------------------------------------------------------------------------- 1 | 2 | #' Dependency types in R DESCRIPTION files 3 | #' @keywords internal 4 | 5 | dep_types <- c("Imports", "Depends", "Suggests", "Enhances", "LinkingTo") 6 | 7 | #' 'Hard' dependency types in R DESCRIPTION files 8 | #' 9 | #' A dependency is hard if the depended package is required 10 | #' for installing and/or loading the package. 11 | #' 12 | #' @rdname dep_types 13 | #' @keywords internal 14 | 15 | hard_dep_types <- c("Imports", "Depends", "LinkingTo") 16 | 17 | #' Extract and read the DESCRIPTION file from an R package tarball 18 | #' 19 | #' @param package_file Path and name of the tarball. 20 | #' @return A named list of DESCRIPTION fields. 21 | #' 22 | #' @keywords internal 23 | #' @importFrom utils untar 24 | 25 | get_description <- function(package_file) { 26 | 27 | pkg <- pkg_from_filename(package_file) 28 | 29 | tmp <- tempfile() 30 | on.exit(unlink(tmp, recursive = TRUE), add = TRUE) 31 | 32 | untar( 33 | package_file, 34 | files = paste(pkg, sep = "/", "DESCRIPTION"), 35 | exdir = tmp 36 | ) 37 | 38 | desc_file <- file.path(tmp, pkg, "DESCRIPTION") 39 | as.list(read.dcf(desc_file)[1, ]) 40 | } 41 | 42 | #' Extract (hard) package dependencies from an R package tarball 43 | #' 44 | #' Hard dependencies include \code{Imports}, \code{Depends} and 45 | #' \code{LinkingTo}. 46 | #' 47 | #' @param package_file Path and name of the tarball. 48 | #' @return A character vector of depended packages. Version numbers 49 | #' are not included, as we don't need them for the current purposes 50 | #' of this package. 51 | #' 52 | #' @keywords internal 53 | 54 | get_deps <- function(package_file) { 55 | 56 | desc <- get_description(package_file) 57 | deps_present<- intersect(hard_dep_types, names(desc)) 58 | 59 | deps <- desc[deps_present] 60 | 61 | deps_df <- lapply(names(deps), function(x) parse_deps(x, deps[[x]])) 62 | 63 | dep_pkgs <- do.call(rbind, deps_df)$package %||% character() 64 | 65 | drop_internal(dep_pkgs) 66 | } 67 | 68 | #' Drop base and recommended packages, and \sQuote{R} from a list 69 | #' of R packages 70 | #' 71 | #' \sQuote{R} can be included in the DESCRIPTION file, as a dependency, 72 | #' but we ignore this right now. We also ignore base and recommended 73 | #' packages, these are supposed to be installed on the system, together 74 | #' with R. 75 | #' 76 | #' @param pkgs Character vector of package names. 77 | #' @return Character vector of filtered package names. 78 | #' 79 | #' @importFrom utils installed.packages 80 | #' @keywords internal 81 | 82 | drop_internal <- function(pkgs) { 83 | 84 | internal <- c( 85 | "R", 86 | rownames(installed.packages(priority = c("base", "recommended"))) 87 | ) 88 | 89 | pkgs <- setdiff(pkgs, internal) 90 | } 91 | 92 | #' Parse a DESCRIPTION dependency field 93 | #' 94 | #' @param type Field name, e.g. \code{Imports}. 95 | #' @param deps The value of the field. 96 | #' @return A data frame with three columns: \code{type}, 97 | #' \code{package} and \code{version}. 98 | #' 99 | #' @keywords internal 100 | 101 | parse_deps <- function(type, deps) { 102 | deps <- str_trim(strsplit(deps, ",")[[1]]) 103 | deps <- lapply(strsplit(deps, "\\("), str_trim) 104 | deps <- lapply(deps, sub, pattern = "\\)$", replacement = "") 105 | res <- data.frame( 106 | stringsAsFactors = FALSE, 107 | type = type, 108 | package = vapply(deps, "[", "", 1), 109 | version = vapply(deps, "[", "", 2) 110 | ) 111 | res [ is.na(res) ] <- "*" 112 | res 113 | } 114 | -------------------------------------------------------------------------------- /R/restore.R: -------------------------------------------------------------------------------- 1 | 2 | #' Restore (=install) certain CRAN package versions 3 | #' 4 | #' Functions that were not installed from CRAN will not be restored, 5 | #' they will be ignored with a warning. The pkgsnap package itself is 6 | #' also ignored as it must be installed to run this function. 7 | #' 8 | #' @param from Name of a file created by \code{\link{snap}}. 9 | #' Alternatively a data frame with columns \code{Package} and 10 | #' \code{Version}. 11 | #' @param R If TRUE the target version of R must match. 12 | #' Otherwise it will only give a warning. 13 | #' @param ... Additional arguments, passed to \code{install.packages}. 14 | #' 15 | #' @export 16 | #' @importFrom utils install.packages read.csv 17 | 18 | restore <- function(from = "packages.csv", R = TRUE, ...) { 19 | 20 | if (is.character(from)) { 21 | pkgs <- read.csv(from, stringsAsFactors = FALSE) 22 | 23 | } else { 24 | pkgs <- from 25 | } 26 | 27 | # Check the R version and remove from the list 28 | pkgs <- check_R_core(pkgs, R) 29 | 30 | # Remove this package (pkgsnap) from the list 31 | pkgs <- pkgs[pkgs$Package!="pkgsnap", ] 32 | 33 | # Don't try to install packages that have an unknown source 34 | unknown_source_rows <- is.na(pkgs$Source) 35 | if (any(unknown_source_rows)) { 36 | warning( 37 | "Source repository is unknown for ", 38 | paste(pkgs$Package[unknown_source_rows], collapse = ", ") 39 | ) 40 | } 41 | pkgs <- pkgs[!unknown_source_rows, ] 42 | 43 | ## Download and return the downloaded file names 44 | pkg_files <- pkg_download(pkgs, dest_dir = tempdir()) 45 | 46 | deps <- lapply(pkg_files, get_deps) 47 | 48 | deps <- drop_missing_deps(deps) 49 | 50 | order <- install_order(deps) 51 | 52 | message("Installing") 53 | for (p in pkg_files[order]) { 54 | message(" ", basename(p), " ... ", appendLF = FALSE) 55 | install.packages(p, repos = NULL, quiet = TRUE, ...) 56 | message("done.") 57 | } 58 | } 59 | 60 | #' Drop dependencies that were not included in the snapshot 61 | #' 62 | #' These are probably not needed for the installed functions 63 | #' to work. 64 | #' 65 | #' @param deps A named list of character vectors. 66 | #' @return A (filtered) named list of character vectors. 67 | #' 68 | #' @keywords internal 69 | 70 | drop_missing_deps <- function(deps) { 71 | pkgs <- names(deps) 72 | lapply(deps, intersect, pkgs) 73 | } 74 | 75 | #' Topological order of the packages 76 | #' 77 | #' This is the correct installation order. 78 | #' 79 | #' @param graph A named list of character vectors, interpreted as 80 | #' an adjacnecy list. If \code{A->B} then package \code{A} depends 81 | #' on package \code{B}, so package \code{B} must be loaded before 82 | #' package \code{A}. 83 | #' @return Character vector of package names in an order that 84 | #' can be used to install them. 85 | #' 86 | #' @keywords internal 87 | 88 | install_order <- function(graph) { 89 | 90 | V <- names(graph) 91 | N <- length(V) 92 | 93 | ## some easy cases 94 | if (length(graph) <= 1 || 95 | sum(sapply(graph, length)) == 0) return(V) 96 | 97 | marked <- 1L; temp_marked <- 2L; unmarked <- 3L 98 | marks <- structure(rep(unmarked, N), names = V) 99 | result <- character(N) 100 | result_ptr <- N 101 | 102 | visit <- function(n) { 103 | if (marks[n] == temp_marked) { 104 | stop("Dependency graph not a DAG: ", n, ", internal error") 105 | } 106 | if (marks[n] == unmarked) { 107 | marks[n] <<- temp_marked 108 | for (m in graph[[n]]) visit(m) 109 | marks[n] <<- marked 110 | result[result_ptr] <<- n 111 | result_ptr <<- result_ptr - 1 112 | } 113 | } 114 | 115 | while (any(marks == unmarked)) { 116 | visit(names(which(marks == unmarked))[1]) 117 | } 118 | 119 | rev(result) 120 | } 121 | 122 | #' Check listed R version against installed 123 | #' 124 | #' @param pkgs data.frame read from the csv file. 125 | #' @param R If TRUE it will error when the R versions mismatch. 126 | #' Otherwise it will just give a warning. 127 | #' @return The same data.frame with the R package removed. 128 | #' 129 | #' @keywords internal 130 | #' 131 | check_R_core <- function(pkgs, R) { 132 | 133 | # Find the row containing R 134 | ir <- which(pkgs$Package == "R") 135 | 136 | if (length(ir) == 1) { 137 | # The R version on this installation 138 | coreVersion <- paste(R.version$major, R.version$minor, sep = ".") 139 | pkgsVersion <- pkgs$Version[ir] 140 | 141 | if(pkgsVersion != coreVersion) { 142 | if (R) { 143 | stop("Packages were installed with R ", pkgsVersion, 144 | ", you have ", coreVersion, ". Call with R = FALSE", 145 | " to override.") 146 | } else { 147 | warning("Packages were installed with R ", pkgsVersion, 148 | ", you have ", coreVersion, ".") 149 | } 150 | } 151 | # Remove from the manifest 152 | pkgs <- pkgs[-ir, ] 153 | 154 | } else { 155 | warning("No R version listed with package list.") 156 | } 157 | 158 | pkgs 159 | } 160 | -------------------------------------------------------------------------------- /R/urls.R: -------------------------------------------------------------------------------- 1 | 2 | #' Default CRAN mirror to use 3 | #' 4 | #' The RStudio mirror is in the Amazon cloud, so most times it has 5 | #' the best response times, and download speed. 6 | #' @keywords internal 7 | 8 | default_cran_mirror <- "http://cran.rstudio.com" 9 | 10 | #' CRAN mirror to use 11 | #' 12 | #' If a CRAN mirror is configured, use it. Otherwise, use default 13 | #' @keywords internal 14 | 15 | get_cran_mirror <- function() { 16 | repos <- getOption("repos") 17 | cran_mirror <- if (!("CRAN" %in% names(repos)) || "@CRAN@" %in% repos) { 18 | default_cran_mirror 19 | } else { 20 | repos[["CRAN"]] 21 | } 22 | message(sprintf("Using CRAN mirror %s.", cran_mirror)) 23 | cran_mirror 24 | } 25 | 26 | #' Extract the minor version of the running R 27 | #' 28 | #' This is needed to calculate possible R package locations 29 | #' for downloads. 30 | #' @keywords internal 31 | 32 | r_minor_version <- function() { 33 | ver <- R.Version() 34 | paste0(ver$major, ".", strsplit(ver$minor, ".", fixed = TRUE)[[1]][1]) 35 | } 36 | 37 | #' What kind of packages to use by default. 38 | #' 39 | #' \code{both} means binaries first, then source packages. 40 | #' @keywords internal 41 | 42 | get_pkg_type <- function() { 43 | "both" 44 | } 45 | 46 | #' Get a list of candidate URLs for a certain version of a package 47 | #' 48 | #' @param package Name of the package, e.g. \code{jsonlite}. 49 | #' @param version Version number of the package, as a string, e.g. 50 | #' \code{1.0.0}. 51 | #' @param type Package type, e.g. \code{binary}, \code{source}, etc. 52 | #' See the \code{type} argument of \code{utils::install.packages}. 53 | #' @param r_minor The minor R version to search for packages for. 54 | #' @param cran_mirror The mirror to use for CRAN packages. Use 55 | #' \code{\link{default_cran_mirror}} if not configured in 56 | #' \code{getOption("repos")} 57 | #' @return Character vector or URLs. 58 | #' 59 | #' @keywords internal 60 | 61 | cran_file <- function(package, version, type = get_pkg_type(), 62 | r_minor = r_minor_version(), 63 | cran_mirror = get_cran_mirror()) { 64 | 65 | if (type == "both") { 66 | c(cran_file(package, version, type = "binary", r_minor = r_minor, cran_mirror = cran_mirror), 67 | cran_file(package, version, type = "source", r_minor = r_minor, cran_mirror = cran_mirror)) 68 | } else if (type == "binary") { 69 | cran_file(package, version, type = .Platform$pkgType, r_minor = r_minor, cran_mirror = cran_mirror) 70 | } else if (type == "source") { 71 | c(sprintf("%s/src/contrib/%s_%s.tar.gz", cran_mirror, package, version), 72 | sprintf("%s/src/contrib/Archive/%s/%s_%s.tar.gz", cran_mirror, 73 | package, package, version)) 74 | } else if (type == "win.binary") { 75 | sprintf("%s/bin/windows/contrib/%s/%s_%s.zip", cran_mirror, r_minor, 76 | package, version) 77 | } else if (type == "mac.binary.mavericks") { 78 | ## We try both, for BioC 79 | sprintf(c("%s/bin/macosx/mavericks/contrib/%s/%s_%s.tgz", 80 | "%s/bin/macosx/contrib/%s/%s_%s.tgz"), 81 | cran_mirror, r_minor, package, version) 82 | } else if (type == "mac.binary") { 83 | sprintf("%s/bin/macosx/contrib/%s/%s_%s.tgz", cran_mirror, r_minor, 84 | package, version) 85 | } else { 86 | stop("Unknown package type: ", type, " see ?options.") 87 | } 88 | } 89 | 90 | get_bioc_version <- function() { 91 | get(".BioC_version_associated_with_R_version", asNamespace("tools"))() 92 | } 93 | 94 | bioc_file <- function(...) { 95 | 96 | repos <- sprintf( 97 | c( 98 | "http://bioconductor.org/packages/%s/bioc", 99 | "http://bioconductor.org/packages/%s/data/annotation", 100 | "http://bioconductor.org/packages/%s/data/experiment", 101 | "http://bioconductor.org/packages/%s/extra" 102 | ), 103 | get_bioc_version() 104 | ) 105 | 106 | unname(unlist( 107 | lapply(repos, function(r) cran_file(..., cran_mirror = r)) 108 | )) 109 | } 110 | 111 | rforge_file <- function(package, version) { 112 | 113 | sprintf( 114 | "http://download.r-forge.r-project.org/src/contrib/%s_%s.tar.gz", 115 | package, 116 | version 117 | ) 118 | } 119 | 120 | #' Get download urls for a bunch of packages 121 | #' 122 | #' @param pkgs Data frame of packages. 123 | #' @return A list of character vectors, a set of URLs for each package. 124 | #' 125 | #' @keywords internal 126 | 127 | download_urls <- function(pkgs) { 128 | 129 | lapply(seq_len(nrow(pkgs)), function(i) { 130 | 131 | if (is.na(pkgs$Source[i])) { 132 | warning("Unknown package source: ", pkgs$repo[i]) 133 | 134 | } else if (pkgs$Source[i] == "cran") { 135 | cran_file(pkgs$Package[i], pkgs$Version[i]) 136 | 137 | } else if (pkgs$Source[i] == "bioc") { 138 | bioc_file(pkgs$Package[i], pkgs$Version[i]) 139 | 140 | } else if (pkgs$Source[i] == "rforge") { 141 | rforge_file(pkgs$Package[i], pkgs$Version[i]) 142 | 143 | } else if (pkgs$Source[i] == "url") { 144 | pkgs$Link[i] 145 | 146 | } else { 147 | warning("Unknown package source: ", pkgs$repo[i]) 148 | character() 149 | } 150 | }) 151 | } 152 | -------------------------------------------------------------------------------- /inst/README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | # pkgsnap 5 | 6 | > Backup and Restore Certain CRAN Package Versions 7 | 8 | [![Project Status: Active - The project has reached a stable, usable state and is being actively developed.](http://www.repostatus.org/badges/latest/active.svg)](http://www.repostatus.org/#active) 9 | [![Linux Build Status](https://travis-ci.org/MangoTheCat/pkgsnap.svg?branch=master)](https://travis-ci.org/MangoTheCat/pkgsnap) 10 | [![Windows Build status](https://ci.appveyor.com/api/projects/status/github/MangoTheCat/pkgsnap?svg=true)](https://ci.appveyor.com/project/gaborcsardi/pkgsnap) 11 | [![Coverage Status](https://img.shields.io/codecov/c/github/mangothecat/pkgsnap/master.svg)](https://codecov.io/github/mangothecat/pkgsnap?branch=master) 12 | [![](http://www.r-pkg.org/badges/version/pkgsnap)](http://www.r-pkg.org/pkg/pkgsnap) 13 | [![CRAN RStudio mirror downloads](http://cranlogs.r-pkg.org/badges/pkgsnap)](http://www.r-pkg.org/pkg/pkgsnap) 14 | 15 | 16 | Create a snapshot of your installed CRAN packages with 'snap', and then 17 | use 'restore' on another system to recreate exactly the same environment. 18 | 19 | ## Installation 20 | 21 | 22 | ```r 23 | devtools::install_github("mangothecat/pkgsnap") 24 | ``` 25 | 26 | ## Usage 27 | 28 | 29 | ```r 30 | library(pkgsnap) 31 | ``` 32 | 33 | For this experiment we create a new library directory, and install 34 | some packages there. We will then remove this directory entirely, 35 | and recreate it using `pkgsnap`. 36 | 37 | 38 | ```r 39 | lib_dir <- tempfile() 40 | dir.create(lib_dir) 41 | ``` 42 | 43 | We make this new library directory the default: 44 | 45 | 46 | ```r 47 | .libPaths(lib_dir) 48 | ``` 49 | 50 | The new library directory is currently empty: 51 | 52 | 53 | ```r 54 | installed.packages(lib_dir)[, c("Package", "Version")] 55 | ``` 56 | 57 | ``` 58 | #> Package Version 59 | ``` 60 | 61 | Let's install some packages here. Note that the dependencies of these 62 | packages will be also installed. 63 | 64 | 65 | ```r 66 | install.packages(c("testthat", "pkgconfig")) 67 | ``` 68 | 69 | ``` 70 | #> Installing packages into '/private/var/folders/ws/7rmdm_cn2pd8l1c3lqyycv0c0000gn/T/RtmpOssfTB/file1003d2f2dd0b1' 71 | #> (as 'lib' is unspecified) 72 | #> also installing the dependency 'praise' 73 | #> 74 | #> Package which is only available in source form, and may need 75 | #> compilation of C/C++/Fortran: 'testthat' 76 | ``` 77 | 78 | ``` 79 | #> 80 | #> The downloaded binary packages are in 81 | #> /var/folders/ws/7rmdm_cn2pd8l1c3lqyycv0c0000gn/T//RtmpOssfTB/downloaded_packages 82 | ``` 83 | 84 | ``` 85 | #> installing the source packages 'praise', 'testthat' 86 | ``` 87 | 88 | ```r 89 | installed.packages(lib_dir)[, c("Package", "Version")] 90 | ``` 91 | 92 | ``` 93 | #> Package Version 94 | #> pkgconfig "pkgconfig" "2.0.0" 95 | #> praise "praise" "1.0.0" 96 | #> testthat "testthat" "0.11.0" 97 | ``` 98 | 99 | We will now create a snapshot, and then scrap the temporary package 100 | library. 101 | 102 | 103 | ```r 104 | snapshot <- tempfile() 105 | snap(to = snapshot) 106 | read.csv(snapshot)[1:5,] 107 | ``` 108 | 109 | ``` 110 | #> Package Version Source Link 111 | #> 1 R 3.3.0 R NA 112 | #> 2 pkgconfig 2.0.0 cran NA 113 | #> 3 praise 1.0.0 cran NA 114 | #> 4 testthat 0.11.0 cran NA 115 | #> 5 BiocInstaller 1.21.3 bioc NA 116 | ``` 117 | 118 | ```r 119 | unlink(lib_dir, recursive = TRUE) 120 | ``` 121 | 122 | Create a new package library. 123 | 124 | 125 | ```r 126 | new_lib_dir <- tempfile() 127 | dir.create(new_lib_dir) 128 | .libPaths(new_lib_dir) 129 | ``` 130 | 131 | Finally, recreate the same set of package versions, in a new package 132 | library. 133 | 134 | 135 | ```r 136 | restore(snapshot) 137 | ``` 138 | 139 | ``` 140 | #> Downloading 141 | #> pkgconfig_2.0.0.tgz... done. 142 | #> praise_1.0.0.tgz... praise_1.0.0.tgz... praise_1.0.0.tar.gz... done. 143 | #> testthat_0.11.0.tgz... testthat_0.11.0.tgz... testthat_0.11.0.tar.gz... done. 144 | #> BiocInstaller_1.21.3.tgz... done. 145 | #> covr_1.2.0.tgz... covr_1.2.0.tgz... covr_1.2.0.tar.gz... done. 146 | #> crayon_1.3.1.tgz... done. 147 | #> curl_0.9.5.tgz... curl_0.9.5.tgz... curl_0.9.5.tar.gz... curl_0.9.5.tar.gz... done. 148 | #> devtools_1.10.0.tgz... devtools_1.10.0.tgz... devtools_1.10.0.tar.gz... done. 149 | #> digest_0.6.9.tgz... digest_0.6.9.tgz... digest_0.6.9.tar.gz... done. 150 | #> git2r_0.13.1.tgz... done. 151 | #> htmltools_0.3.tgz... htmltools_0.3.tgz... htmltools_0.3.tar.gz... done. 152 | #> httr_1.1.0.tgz... httr_1.1.0.tgz... httr_1.1.0.tar.gz... done. 153 | #> jsonlite_0.9.19.tgz... jsonlite_0.9.19.tgz... jsonlite_0.9.19.tar.gz... done. 154 | #> lazyeval_0.1.10.tgz... lazyeval_0.1.10.tgz... lazyeval_0.1.10.tar.gz... done. 155 | #> magrittr_1.5.tgz... magrittr_1.5.tgz... magrittr_1.5.tar.gz... done. 156 | #> memoise_1.0.0.tgz... memoise_1.0.0.tgz... memoise_1.0.0.tar.gz... done. 157 | #> mime_0.4.tgz... done. 158 | #> openssl_0.9.1.tgz... openssl_0.9.1.tgz... openssl_0.9.1.tar.gz... openssl_0.9.1.tar.gz... done. 159 | #> R6_2.1.2.tgz... R6_2.1.2.tgz... R6_2.1.2.tar.gz... done. 160 | #> rex_1.0.1.tgz... rex_1.0.1.tgz... rex_1.0.1.tar.gz... done. 161 | #> rstudioapi_0.5.tgz... done. 162 | #> simplegraph_1.0.0.tgz... simplegraph_1.0.0.tgz... simplegraph_1.0.0.tar.gz... done. 163 | #> whisker_0.3-2.tgz... done. 164 | #> withr_1.0.1.tgz... withr_1.0.1.tgz... withr_1.0.1.tar.gz... done. 165 | #> Installing 166 | #> pkgconfig_2.0.0.tgz ... done. 167 | #> praise_1.0.0.tar.gz ... done. 168 | #> testthat_0.11.0.tar.gz ... done. 169 | #> BiocInstaller_1.21.3.tgz ... done. 170 | #> covr_1.2.0.tar.gz ... done. 171 | #> crayon_1.3.1.tgz ... done. 172 | #> curl_0.9.5.tar.gz ... done. 173 | #> devtools_1.10.0.tar.gz ... done. 174 | #> digest_0.6.9.tar.gz ... done. 175 | #> git2r_0.13.1.tgz ... done. 176 | #> htmltools_0.3.tar.gz ... done. 177 | #> httr_1.1.0.tar.gz ... done. 178 | #> jsonlite_0.9.19.tar.gz ... done. 179 | #> lazyeval_0.1.10.tar.gz ... done. 180 | #> magrittr_1.5.tar.gz ... done. 181 | #> memoise_1.0.0.tar.gz ... done. 182 | #> mime_0.4.tgz ... done. 183 | #> openssl_0.9.1.tar.gz ... done. 184 | #> R6_2.1.2.tar.gz ... done. 185 | #> rex_1.0.1.tar.gz ... done. 186 | #> rstudioapi_0.5.tgz ... done. 187 | #> simplegraph_1.0.0.tar.gz ... done. 188 | #> whisker_0.3-2.tgz ... done. 189 | #> withr_1.0.1.tar.gz ... done. 190 | ``` 191 | 192 | ```r 193 | installed.packages(new_lib_dir)[, c("Package", "Version")] 194 | ``` 195 | 196 | ``` 197 | #> Package Version 198 | #> BiocInstaller "BiocInstaller" "1.21.3" 199 | #> covr "covr" "1.2.0" 200 | #> crayon "crayon" "1.3.1" 201 | #> curl "curl" "0.9.5" 202 | #> devtools "devtools" "1.10.0" 203 | #> digest "digest" "0.6.9" 204 | #> git2r "git2r" "0.13.1" 205 | #> htmltools "htmltools" "0.3" 206 | #> httr "httr" "1.1.0" 207 | #> jsonlite "jsonlite" "0.9.19" 208 | #> lazyeval "lazyeval" "0.1.10" 209 | #> magrittr "magrittr" "1.5" 210 | #> memoise "memoise" "1.0.0" 211 | #> mime "mime" "0.4" 212 | #> openssl "openssl" "0.9.1" 213 | #> pkgconfig "pkgconfig" "2.0.0" 214 | #> praise "praise" "1.0.0" 215 | #> R6 "R6" "2.1.2" 216 | #> rex "rex" "1.0.1" 217 | #> rstudioapi "rstudioapi" "0.5" 218 | #> simplegraph "simplegraph" "1.0.0" 219 | #> testthat "testthat" "0.11.0" 220 | #> whisker "whisker" "0.3-2" 221 | #> withr "withr" "1.0.1" 222 | ``` 223 | 224 | 225 | ## License 226 | 227 | MIT © [Mango Solutions](https://github.com/mangothecat). 228 | --------------------------------------------------------------------------------