Robert's Data Science Blog

The {cranitor} R Package

I have recently released the unofficial cranitor package for R. The goal of cranitor is to maintain the backend of a CRAN – that is, the folder structure and metadata needed for install.packages and remotes::install_version.

As an example, consider a CRAN with the packages foo (with versions 0.0.1 and 0.0.2) and bar (with version 0.0.1).

The backend of such a CRAN with source versions and Windows versions for R 3.6.x are the following folder structure and files:

.
├── bin
│   └── windows
│       └── contrib
│           └── 3.6
│               ├── bar_0.0.1.zip
│               └── foo_0.0.2.zip
└── src
    └── contrib
        ├── Archive
        │   └── foo
        │       └── foo_0.0.1.tar.gz
        ├── bar_0.0.1.tar.gz
        ├── foo_0.0.2.tar.gz
        ├── Meta
        │   └── archive.rds
        ├── PACKAGES
        ├── PACKAGES.gz
        └── PACKAGES.rds

In more details:

The difference between the binary zip versions and the tar.gz versions is that the binary versions are compiled – both byte compilation of the R code and compilation of any C/C++/Fortran code. The latter is the reason that it is so much faster to install packages on Windows than on Linux, which use the tar.gz versions.

A CRAN can also have a bin/macosx folder with binary packages compiled for macOS. Just as for Windows, the binary packages for macOS can only be created with R on macOS.

Using a CRAN

The cranitor package does not handle hosting of the CRAN. But any hosting service should be capable of hosting a simple folder structure. In the tests of cranitor I use the servr package to test that I can install packages in the expected way. I am actually quite pleased with how easily I can run a server as part of the tests.

When trying out a homemade CRAN it can be tempting to try it out on a local computer. But beware that a CRAN is meant/expected to be served over the http protocol.

Base R does support a “file protocol” by specifying the path in this manner:

cran_path <- file.path("file://", normalizePath(cran_root, winslash = "/"))

normalizePath is used to get the correct number of slashes. This works with install.packages:

install.packages("foo", type = "source", repos = cran_path)

However, it does not work with remotes::install_version.

remotes::install_version("foo", version = "0.0.1", repos = cran_path)

On Linux I get an error like this:

Downloading package from url: file:////path/to/cran/src/contrib/Archive/foo/foo_0.0.1.tar.gz
Error in utils::download.file(url, path, method = method, quiet = quiet,  :
   cannot open URL 'file:////path/to/cran/src/contrib/Archive/foo/foo_0.0.1.tar.gz'

On Windows the error looks like this:

tar.exe: Error opening archive: truncated gzip input
Warning messages:
1: In utils::untar(tarfile, ...) :
  ‘tar.exe -xf "C:\Users\robert\AppData\Local\Temp\RtmpQTtWcH\file2b5c166e5518.tar.gz" -C "C:/Users/robert/AppData/Local/Temp/RtmpQTtWcH/remotes2b5c59f945e8"’ returned error code 1
2: In system(cmd, intern = TRUE) :
  running command 'tar.exe -tf "C:\Users\robert\AppData\Local\Temp\RtmpQTtWcH\file2b5c166e5518.tar.gz"' had status 1

Location aware packages

My usecase of a local CRAN is to host internal packages. To make a package aware of which CRAN it is located in the DESCRIPTION file should include the field

Repository: <CRAN url>

If a package depends on other packages from a specific CRAN it can be specified that R should also look here by including yet another field:

Additional_repositories: <CRAN url>

In the build pipelines we use to test and upload our R packages to our local CRAN these fields are set automatically.