Skip to content

Resolution

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.

rv has two “types” of dependencies:

  • 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.

We’ll see this in the following examples:

Lets consider the following dependency and repository information:

Now consider the following config file excerpt:

rproject.toml
repositories = [
{ alias = "repo1", url = "https://cran-like-repo.com/repo1" },
]
dependencies = [
"foo"
]

In this example, the tooling determines the requirements to be:

  • Direct Dependencies:
    • foo
  • Recursive Dependencies:
    • bar >= 1.0.0

Since there is only one source, the resolution is foo 1.0.0 and bar 1.0.0 from repo1

Example 1 was simple, foo and bar MUST be sourced from repo since it is the only option. But rv allows for multiple repositories.

Lets consider the following dependency and repository information:

The addition of another repository, and more package versions, leads to multiple ways to resolve.

Given this 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 = [
"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:

Lets consider the same package and repository information as Example 2:

For this example, we will have installed bar from repo1 using the following config file, generating the following lockfile:

rv.lock
# This file is automatically @generated by rv.
# It is not intended for manual editing.
version = 2
r_version = "4.4"
[[packages]]
name = "bar"
version = "1.0.0"
source = { repository = "https://cran-like-repo.com/repo1" }
force_source = false
dependencies = []

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

In this example, we’ll add foo into the project in two different ways, one that is compatible with the locked version of bar and one that is not.

Example 3.3.1 - Incompatible Locked Version
Section titled “Example 3.3.1 - Incompatible Locked Version”

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.

Terminal window
$ rv plan
+ bar (2.0.0, binary from https://cran-like-repo.com/repo2)
+ foo (2.0.0, binary from https://cran-like-repo.com/repo2)

If the outcome is not desired, you can change the config file as needed. In this scenario, we can either:

  1. Make repo1 the first repository
  2. 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.

In Overriding Remotes and prefer_repositories_for, we have discussed Remotes resolution, but we’ll discuss further in this section.

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:

Lets consider the following package and information:

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.

In this example, we will install baz from its git repo using the following config file excerpt:

rproject.toml
repositories = [
{ alias = "repo2", url = "https://cran-like-repo.com/repo2" },
]
dependencies = [
{ name = "baz", git = "https://github.com/my-org/baz", branch = "main" },
]

To start resolution, rv consults the DESCRIPTION file of baz and ends up with the following requirements:

  • Direct Dependencies:
    • baz from my-org/baz@main
  • Recursive Dependencies:
    • bar from my-org/baz
    • bar >= 2.0.0.9000

Since bar is specified as a remote in baz, the resolution is baz 1.0.0.9000 from my-org/baz@main and bar 2.0.0.9000 from my-org/bar@main.

Example 4.2 - Satisfying Package Version in Repository

Section titled “Example 4.2 - Satisfying Package Version in Repository”

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:

Let’s keep the same configuration file as Example 4.1, but with repo3 instead:

rproject.toml
repositories = [
{ alias = "repo3", url = "https://cran-like-repo.com/repo3" },
]
dependencies = [
{ name = "baz", git = "https://github.com/my-org/baz", branch = "main" },
]

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" },
]
dependencies = [
{ name = "baz", git = "https://github.com/my-org/baz", branch = "main" },
]
prefer_repositories_for = [
"bar",
]

The requirements now become:

  • Direct Dependencies:
    • baz from my-org@main
    • bar from repository
  • Recursive Dependencies:
    • bar from my-org/baz
    • bar >= 2.0.0.9000

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:

  1. ✅ The dependency has a version requirement within the DESCRIPTION file
    • bar >= 2.0.0.9000 is within the baz DESCRIPTION file
  2. ✅ 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.