As discussed in other sections, rv is a declarative package manager, which means the user declares the desired project state in a configuration file.
rv then takes the list of requirements and resolves them to a list of packages and sources that fulfill the requirements.
Direct Dependencies - These are the packages explicity stated in the dependencies section
of your config file. Direct dependencies can have additional configurations,
which determines the initial list of requirements.
Recursive Dependencies - These are the additional dependencies required by the direct dependencies in the config file.
Most packages depend on additional packages to work correctly. Packages define their dependencies as requirements, often a pacakage and
acceptable version constraint, and occassionally a remote source.
rv will resolve the complete dependency tree to ensure all direct and recursive dependency requirements are met before installing packages.
Unlike other language package distribution systems, CRAN only has one version of a package available at any point in time.
Which means to install different versions, different sources must be specified.
{ alias = "repo1", url = "https://cran-like-repo.com/repo1" },
{ alias = "repo2", url = "https://cran-like-repo.com/repo2" },
]
dependencies = [
"foo"
]
Given this set-up, we have the following requirements:
Direct Dependencies:
foo
Recursive Dependencies:
bar >= 2.0.0 if foo 2.0.0
bar >= 1.0.0 if foo 1.0.0
Since repo1 is first in the config file, we prefer packages to be sourced from that repository. If we try foo 1.0.0 from repo1, we keep the requirement
that bar >= 1.0.0, meaning bar 1.0.0 can be also be sourced from repo1, the preferred repository.
Therefore, we end up with the same resolution as Example 1: foo 1.0.0 and bar 1.0.0 from repo1.
Lets now switch the repositories section so that repo2 is first:
rproject.toml
repositories = [
{ alias = "repo2", url = "https://cran-like-repo.com/repo2" },
{ alias = "repo1", url = "https://cran-like-repo.com/repo1" },
]
dependencies = [
"foo"
]
This set-up leads to the same requirements:
Direct Dependencies:
foo
Recursive Dependencies:
bar >= 2.0.0 if foo 2.0.0
bar >= 1.0.0 if foo 1.0.0
But in this case, we give preference to repo2. If we try foo 2.0.0 from repo2, we keep the more restrictive requirement that bar >= 2.0.0,
but since the preferred repository repo2 contains bar 2.0.0 the resolution is foo 2.0.0 and bar 2.0.0 from repo2.
Lets return to Example 2.1.1 where repo1 is first, but this time ensuring I get foo 2.0.0 from repo2:
rproject.toml
repositories = [
{ alias = "repo1", url = "https://cran-like-repo.com/repo1" },
{ alias = "repo2", url = "https://cran-like-repo.com/repo2" },
]
dependencies = [
{ name = "foo", repository = "repo2" },
]
With this configuration, we have an additional requirement for the source of foo:
Direct Dependencies:
foo from repo2
Recursive Dependencies:
bar >= 2.0.0 if foo 2.0.0
bar >= 1.0.0 if foo 1.0.0
Since foo 2.0.0 is in repo2, we keep the more restrictive requirement of bar >= 2.0.0.
Source bar 1.0.0 from the preferred repository repo1 does not meet the requirement, therefore we override this preference and source bar 2.0.0 from repo2.
Thus, the resolution ends up being the same as if repo2 was first: foo 2.0.0 and bar 2.0.0 from repo2.
Sometimes, resolution from a configuration is not possible.
For example, consider the following config file excerpt:
rproject.toml
repositories = [
{ alias = "repo1", url = "https://cran-like-repo.com/repo1" },
{ alias = "repo2", url = "https://cran-like-repo.com/repo2" },
]
dependencies = [
{ name = "foo", repository = "repo2" },
{ name = "bar", repository = "repo1" },
]
With this configuration, we have an additional requirement for the source of bar:
Direct Dependencies:
foo from repo2
bar from repo1
Recursive Dependencies:
bar >= 2.0.0 if foo 2.0.0
bar >= 1.0.0 if foo 1.0.0
Since foo 2.0.0 is in repo2, we keep the restrictive requirement that bar >= 2.0.0. The only way to meet that requirement would be to install
bar 2.0.0 from repo2, but that is in direct conflict with the conflict bar must be sourced from repo1.
Therefore, we cannot resolve this configuration.
In addition to the configuration file, rv keeps a lockfile to track the package version and source to recreate the enviroment.
When a lockfile is present, the source tracked in the lockfile takes precedent over the configuration file. The few exceptions are:
The config file no longer contains the repository the package is locked as.
The dependency has a different source specified, or the specified source is removed.
The following examples will show how the lockfile works:
We first will add repo2 as the first repository in this config file excerpt:
rproject.toml
repositories = [
{ alias = "repo2", url = "https://cran-like-repo.com/repo2" },
{ alias = "repo1", url = "https://cran-like-repo.com/repo1" },
]
dependencies = [
"bar"
]
Without the lockfile, the resolution would be simple, and similar to Example 2.1,
bar 2.0.0 would come from repo2 since that repository comes first. But the presence of the lockfile adds additional
requirements:
Direct Dependencies:
bar
From Lockfile:
bar 1.0.0 from https://cran-like-repo.com/repo1
Since all requirements are met by using the version in the lockfile, the resolution ends up being bar 1.0.0 from repo1.
Lets now remove repo1 from the config file. By removing the repository from the config file, you are communicating to rv packages should not
be sourced from repo1 anymore.
rproject.toml
repositories = [
{ alias = "repo2", url = "https://cran-like-repo.com/repo2" },
# { alias = "repo1", url = "https://cran-like-repo.com/repo1" },
]
dependencies = [
"bar"
]
Therefore, by removing the source bar is locked to, we remove the requirement that bar must come from repo1:
Direct Dependencies:
bar
From Lockfile:
bar 1.0.0 from https://cran-like-repo.com/repo1
Since the only requirement is that bar is installed and we have one source, the resolution is bar 2.0.0 from repo2
Let’s return to Example 3.1 and add foo to the dependencies field.
rproject.toml
repositories = [
{ alias = "repo2", url = "https://cran-like-repo.com/repo2" },
{ alias = "repo1", url = "https://cran-like-repo.com/repo1" },
]
dependencies = [
"bar",
"foo",
]
Since foo is not within the lockfile, we consult the repository and get these requirements:
Direct Dependencies:
bar
foo
From Lockfile:
bar 1.0.0 from repo1
Recursive Dependencies:
bar >= 1.0.0 if foo 1.0.0
bar >= 2.0.0 if foo 2.0.0
Since repo2 is listed first, we prefer packages to be sourced from that repository. Since foo 2.0.0 meets all requirements and comes from the preferred
repository, repo2, we choose to use this version. This leads to conflicting requirements on bar since bar 1.0.0 cannot come from repo1
AND bar >= 2.0.0 since foo 2.0.0. In this case, we prefer to ingore the lockfile requirement and resolve to foo 2.0.0 and bar 2.0.0 from repo2.
Like we have discussed in other sections, one of the benefits of rv is the ability to resolve and see what will happen
before it actually occurs. rv plan will resolve and show what would occur upon calling rv sync, as seen below.
If the outcome is not desired, you can change the config file as needed. In this scenario, we can either:
Make repo1 the first repository
Specify foo should be sourced from repo1
Since changing the repositories order could change the installation behavior of other packages as they are added, we will only specify repo1 for foo.
rproject.toml
repositories = [
{ alias = "repo2", url = "https://cran-like-repo.com/repo2" },
{ alias = "repo1", url = "https://cran-like-repo.com/repo1" },
]
dependencies = [
"bar",
{ name = "foo", alias = "repo1" },
]
This configuration leads to the following requirements:
Direct Dependencies:
bar
foo from repo1
From Lockfile:
bar 1.0.0 from repo1
Recursive Dependencies:
bar >= 1.0.0 if foo 1.0.0
bar >= 2.0.0 if foo 2.0.0
Since foo is specified to come from repo1, we start with foo 1.0.0. This gives us the requirement that bar >= 1.0.0,
therefore the locked version of bar 1.0.0 is compatible with the rest of the requirements, making the resolution foo 1.0.0 and bar 1.0.0 from repo1.
Within a package’s DESCRIPTION file, the Remotes field can be set to install a dependencies from a nonstandard place. This often occurs when developing one package
alongside a development version of another. This is dicussed further in the R package book.
rv always respects the Remotes field of a package, unless the prefer_repositories_for configuration is specified.
We’ll see how this resolution works in the following example:
Additionally, we have the following new packages/versions:
bar 2.0.0.9000 - in development on the main branch of my-org/bar
baz 1.0.0.9000 - in development on the main branch of my-org/baz
Below is an excerpt from the baz DESCRIPTION file:
DESCRIPTION
Package: baz
Version: 1.0.0.9000
Imports:
bar (>= 2.0.0.9000)
Remotes:
my-org/bar
By not specifying a reference (tag/branch) on the remote, the latest commit on the default branch will be installed.
If my-org/bar@dev is specified, then the dev branch is installed.
The previous example did not have multiple resolutions since bar 2.0.0 from repo2 did not meet the requirement set by the baz 1.0.0.9000 DESCRIPTION file,
We’ll next look at what occurs if bar 3.0.0 was released in repo3.
The package and repository info for repo3 is as follows:
Despite a compatible version now being available in the repository, the resolution is the exact same as Example 4.1!
If a remote is listed, rv will install the dependency from the remote, unless that dependency is listed in prefer_repositories_for.
There are a number of reasons to not want to install a dependency from a remote, and also plenty of reasons to want to install from the remote.
One of the reasons may be that the dependency is now available in a repository and there is no need to continue to compile the package from the git repository.
This is the scenario we started to see in the example above.
Starting with the same repository/package information and config file as above, we’ll add bar to the
prefer_repositories_for section in the config file.
rproject.toml
repositories = [
{ alias = "repo3", url = "https://cran-like-repo.com/repo3" },
We now have two source requirements for bar. In order for the configuration requirement to “overrule” the Remote, we need to ensure the conditions
for prefer_repositories_for are met:
✅ The dependency has a version requirement within the DESCRIPTION file
bar >= 2.0.0.9000 is within the baz DESCRIPTION file
✅ There exists a package matching that version requirement within a repository
bar 3.0.0 is within repo3 and meets the version requirement >=2.0.0.9000
Since both conditions are met, the configuration requirement of bar being sourced from a repository is preferred over the Remote.
Therefore the resolution is baz 1.0.0.9000 from my-org/baz@main and bar 3.0.0 from repo3.
If either of the conditions for prefer_repositories_for were not met, the resolution would have included bar from my-org/bar@main,
like the resolution in the example above.