Skip to content

Commit fc78f7f

Browse files
authored
SAT solver for requirements (#208)
* SAT solver for requirements Closes #204 * Add failing examples * Add test for remote conflict * Only insert dep found once * Avoid lookups to repo if possible * Better conflict output
1 parent 4beff27 commit fc78f7f

25 files changed

+984
-104
lines changed

.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
out
44
src/tests/RCMD/
55
example_projects/*/rv
6+
example_projects/test
67
example_projects/*/rv.lock
78
!example_projects/archive/rv.lock
89
from*/

example_projects/url-dep/rproject.toml

+1-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
name = "simple"
33
r_version = "4.4"
44
repositories = [
5-
{alias = "posit", url = "https://packagemanager.posit.co/cran/2024-12-16/"}
5+
{alias = "posit", url = "https://packagemanager.posit.co/cran/2024-12-16/", force_source = true}
66
]
77
dependencies = [
88
{name = "dplyr", url = "https://cran.r-project.org/src/contrib/Archive/dplyr/dplyr_1.1.3.tar.gz"}

src/activate.rs

+1-2
Original file line numberDiff line numberDiff line change
@@ -97,8 +97,7 @@ fn scripts_as_paths(is_home: bool) -> (PathBuf, PathBuf) {
9797

9898
fn write_activate_file(dir: impl AsRef<Path>, is_home: bool) -> Result<(), ActivateError> {
9999
let template = ACTIVATE_FILE_TEMPLATE.to_string();
100-
let global_wd_content = if is_home
101-
{
100+
let global_wd_content = if is_home {
102101
r#"
103102
owd <- getwd()
104103
setwd("~")

src/lockfile.rs

+15-11
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,12 @@ use std::io::Write;
55
use std::path::{Path, PathBuf};
66
use std::str::FromStr;
77

8+
use crate::package::Dependency;
9+
use crate::{ConfigDependency, Repository, ResolvedDependency, Version};
810
use fs_err as fs;
911
use serde::Deserialize;
1012
use toml_edit::{Array, ArrayOfTables, InlineTable, Item, Table, Value};
1113

12-
use crate::{ConfigDependency, Repository, ResolvedDependency, Version};
13-
1414
const CURRENT_LOCKFILE_VERSION: i64 = 1;
1515
const INITIAL_COMMENT: &str = r#"# This file is automatically @generated by rv.
1616
# It is not intended for manual editing.
@@ -89,6 +89,10 @@ impl Source {
8989
matches!(self, Source::Git { .. } | Source::Url { .. })
9090
}
9191

92+
pub fn is_repo(&self) -> bool {
93+
matches!(self, Source::Repository { .. })
94+
}
95+
9296
/// The key to use in the cache: URL for a package repository, git URL for a git repository
9397
/// and for local the actual path
9498
pub fn source_path(&self) -> &str {
@@ -231,11 +235,11 @@ impl fmt::Display for Source {
231235
}
232236
}
233237

234-
fn format_array(deps: &[String]) -> Array {
238+
fn format_array(deps: &[Dependency]) -> Array {
235239
let mut deps = deps
236240
.iter()
237241
.map(|d| {
238-
let mut value = Value::from(d);
242+
let mut value = d.as_toml_value();
239243
value.decor_mut().set_prefix("\n ");
240244
value
241245
})
@@ -257,10 +261,10 @@ pub struct LockedPackage {
257261
pub source: Source,
258262
pub path: Option<String>,
259263
pub force_source: bool,
260-
pub dependencies: Vec<String>,
264+
pub dependencies: Vec<Dependency>,
261265
/// Only filled if the package had install_suggests=True in the config file
262266
#[serde(default)]
263-
pub suggests: Vec<String>,
267+
pub suggests: Vec<Dependency>,
264268
}
265269

266270
impl LockedPackage {
@@ -274,9 +278,9 @@ impl LockedPackage {
274278
dependencies: dep
275279
.dependencies
276280
.into_iter()
277-
.map(|d| d.into_owned())
281+
.map(|x| x.into_owned())
278282
.collect(),
279-
suggests: dep.suggests.into_iter().map(|d| d.into_owned()).collect(),
283+
suggests: dep.suggests.into_iter().map(|x| x.into_owned()).collect(),
280284
}
281285
}
282286

@@ -350,8 +354,8 @@ impl Lockfile {
350354

351355
for p in &self.packages {
352356
for d in &p.dependencies {
353-
if !package_names.contains(d.as_str()) {
354-
not_found.insert(d.as_str());
357+
if !package_names.contains(d.name()) {
358+
not_found.insert(d.name());
355359
}
356360
}
357361
}
@@ -456,7 +460,7 @@ impl Lockfile {
456460
let mut out = HashSet::new();
457461
out.insert(p.name.as_str());
458462
for p in &p.dependencies {
459-
out.extend(self.get_package_tree(p.as_str(), None));
463+
out.extend(self.get_package_tree(p.name(), None));
460464
}
461465
out
462466
} else {

src/main.rs

+7
Original file line numberDiff line numberDiff line change
@@ -158,9 +158,16 @@ fn resolve_dependencies(context: &CliContext) -> Vec<ResolvedDependency> {
158158
);
159159
if !resolution.is_success() {
160160
eprintln!("Failed to resolve all dependencies");
161+
let req_error_messages = resolution.req_error_messages();
162+
161163
for d in resolution.failed {
162164
eprintln!(" {d}");
163165
}
166+
167+
if !req_error_messages.is_empty() {
168+
eprintln!("{}", req_error_messages.join("\n"));
169+
}
170+
164171
::std::process::exit(1)
165172
}
166173

src/package/mod.rs

+17-3
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ use serde::{Deserialize, Serialize};
44
use std::collections::HashMap;
55
use std::fmt;
66
use std::path::Path;
7+
use toml_edit::{InlineTable, Value};
78

89
mod description;
910
mod parser;
@@ -30,8 +31,9 @@ impl fmt::Display for PackageType {
3031
}
3132
}
3233

33-
#[derive(Debug, PartialEq, Clone, Encode, Decode, Serialize, Deserialize)]
34-
pub(crate) enum Dependency {
34+
#[derive(Debug, Hash, Eq, PartialEq, Clone, Encode, Decode, Serialize, Deserialize)]
35+
#[serde(untagged)]
36+
pub enum Dependency {
3537
Simple(String),
3638
Pinned {
3739
name: String,
@@ -55,6 +57,18 @@ impl Dependency {
5557
} => Some(requirement),
5658
}
5759
}
60+
61+
pub(crate) fn as_toml_value(&self) -> Value {
62+
match self {
63+
Self::Simple(name) => Value::from(name.as_str()),
64+
Self::Pinned { name, requirement } => {
65+
let mut table = InlineTable::new();
66+
table.insert("name", Value::from(name.as_str()));
67+
table.insert("requirement", Value::from(&requirement.to_string()));
68+
Value::InlineTable(table)
69+
}
70+
}
71+
}
5872
}
5973

6074
#[derive(Debug, Default, PartialEq, Clone, Encode, Decode)]
@@ -76,7 +90,7 @@ pub struct Package {
7690
pub(crate) remotes: HashMap<String, (Option<String>, PackageRemote)>,
7791
}
7892

79-
#[derive(Debug, Default, PartialEq, Clone, Serialize)]
93+
#[derive(Debug, Default, PartialEq, Clone)]
8094
pub struct InstallationDependencies<'a> {
8195
pub(crate) direct: Vec<&'a Dependency>,
8296
pub(crate) suggests: Vec<&'a Dependency>,

src/package/version.rs

+19-3
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,10 @@ use bincode::{Decode, Encode};
22
use serde::{Deserialize, Serialize};
33
use std::cmp::Ordering;
44
use std::fmt;
5+
use std::hash::Hash;
56
use std::str::FromStr;
67

7-
#[derive(Debug, PartialEq, Eq, Copy, Clone, Serialize, Deserialize, Encode, Decode)]
8+
#[derive(Debug, Hash, PartialEq, Eq, Copy, Clone, Serialize, Deserialize, Encode, Decode)]
89
pub enum Operator {
910
Equal,
1011
Greater,
@@ -99,6 +100,12 @@ impl PartialEq for Version {
99100
}
100101
}
101102

103+
impl Hash for Version {
104+
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
105+
self.original.hash(state);
106+
}
107+
}
108+
102109
impl Eq for Version {}
103110

104111
impl Ord for Version {
@@ -127,7 +134,8 @@ where
127134
/// A package can require specific version for some versions.
128135
/// Most of the time it's using >= but there are also some
129136
/// >, <, <= here and there and a couple of ==
130-
#[derive(Debug, PartialEq, Clone, Encode, Decode, Serialize, Deserialize)]
137+
#[derive(Debug, Hash, Eq, PartialEq, Clone, Encode, Decode, Serialize, Deserialize)]
138+
#[serde(try_from = "String")]
131139
pub struct VersionRequirement {
132140
pub(crate) version: Version,
133141
op: Operator,
@@ -150,7 +158,7 @@ impl VersionRequirement {
150158
}
151159

152160
impl FromStr for VersionRequirement {
153-
type Err = ();
161+
type Err = String;
154162

155163
// s is for format `(>= 4.5)`
156164
fn from_str(s: &str) -> Result<Self, Self::Err> {
@@ -187,6 +195,14 @@ impl FromStr for VersionRequirement {
187195
}
188196
}
189197

198+
impl TryFrom<String> for VersionRequirement {
199+
type Error = String;
200+
201+
fn try_from(s: String) -> Result<Self, Self::Error> {
202+
s.parse()
203+
}
204+
}
205+
190206
impl fmt::Display for VersionRequirement {
191207
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
192208
write!(f, "({} {})", self.op, self.version)

src/resolver/dependency.rs

+13-41
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ use serde::Deserialize;
33
use crate::cache::InstallationStatus;
44
use crate::http::HttpError;
55
use crate::lockfile::{LockedPackage, Source};
6-
use crate::package::{InstallationDependencies, Package, PackageRemote, PackageType};
6+
use crate::package::{Dependency, InstallationDependencies, Package, PackageRemote, PackageType};
77
use crate::resolver::QueueItem;
88
use crate::{Http, HttpDownload, Version, VersionRequirement};
99
use std::borrow::Cow;
@@ -20,8 +20,8 @@ pub struct ResolvedDependency<'d> {
2020
pub(crate) name: Cow<'d, str>,
2121
pub(crate) version: Cow<'d, Version>,
2222
pub(crate) source: Source,
23-
pub(crate) dependencies: Vec<Cow<'d, str>>,
24-
pub(crate) suggests: Vec<Cow<'d, str>>,
23+
pub(crate) dependencies: Vec<Cow<'d, Dependency>>,
24+
pub(crate) suggests: Vec<Cow<'d, Dependency>>,
2525
pub(crate) force_source: bool,
2626
pub(crate) install_suggests: bool,
2727
pub(crate) kind: PackageType,
@@ -56,16 +56,8 @@ impl<'d> ResolvedDependency<'d> {
5656
name: Cow::Borrowed(&package.name),
5757
version: Cow::Owned(Version::from_str(package.version.as_str()).unwrap()),
5858
source: package.source.clone(),
59-
dependencies: package
60-
.dependencies
61-
.iter()
62-
.map(|d| Cow::Borrowed(d.as_str()))
63-
.collect(),
64-
suggests: package
65-
.suggests
66-
.iter()
67-
.map(|s| Cow::Borrowed(s.as_str()))
68-
.collect(),
59+
dependencies: package.dependencies.iter().map(Cow::Borrowed).collect(),
60+
suggests: package.suggests.iter().map(Cow::Borrowed).collect(),
6961
// TODO: what should we do here?
7062
kind: if package.force_source {
7163
PackageType::Source
@@ -113,16 +105,8 @@ impl<'d> ResolvedDependency<'d> {
113105
name: Cow::Borrowed(&package.name),
114106
version: Cow::Borrowed(&package.version),
115107
source,
116-
dependencies: deps
117-
.direct
118-
.iter()
119-
.map(|d| Cow::Borrowed(d.name()))
120-
.collect(),
121-
suggests: deps
122-
.suggests
123-
.iter()
124-
.map(|d| Cow::Borrowed(d.name()))
125-
.collect(),
108+
dependencies: deps.direct.iter().map(|d| Cow::Borrowed(*d)).collect(),
109+
suggests: deps.suggests.iter().map(|d| Cow::Borrowed(*d)).collect(),
126110
kind: package_type,
127111
force_source,
128112
install_suggests,
@@ -148,15 +132,11 @@ impl<'d> ResolvedDependency<'d> {
148132
let deps = package.dependencies_to_install(install_suggests);
149133

150134
let res = Self {
151-
dependencies: deps
152-
.direct
153-
.iter()
154-
.map(|d| Cow::Owned(d.name().to_string()))
155-
.collect(),
135+
dependencies: deps.direct.iter().map(|&d| Cow::Owned(d.clone())).collect(),
156136
suggests: deps
157137
.suggests
158138
.iter()
159-
.map(|s| Cow::Owned(s.name().to_string()))
139+
.map(|&d| Cow::Owned(d.clone()))
160140
.collect(),
161141
kind: PackageType::Source,
162142
force_source: true,
@@ -183,15 +163,11 @@ impl<'d> ResolvedDependency<'d> {
183163
) -> (Self, InstallationDependencies) {
184164
let deps = package.dependencies_to_install(install_suggests);
185165
let res = Self {
186-
dependencies: deps
187-
.direct
188-
.iter()
189-
.map(|d| Cow::Owned(d.name().to_string()))
190-
.collect(),
166+
dependencies: deps.direct.iter().map(|&d| Cow::Owned(d.clone())).collect(),
191167
suggests: deps
192168
.suggests
193169
.iter()
194-
.map(|s| Cow::Owned(s.name().to_string()))
170+
.map(|&d| Cow::Owned(d.clone()))
195171
.collect(),
196172
kind: PackageType::Source,
197173
force_source: true,
@@ -219,15 +195,11 @@ impl<'d> ResolvedDependency<'d> {
219195
) -> (Self, InstallationDependencies) {
220196
let deps = package.dependencies_to_install(install_suggests);
221197
let res = Self {
222-
dependencies: deps
223-
.direct
224-
.iter()
225-
.map(|d| Cow::Owned(d.name().to_string()))
226-
.collect(),
198+
dependencies: deps.direct.iter().map(|&d| Cow::Owned(d.clone())).collect(),
227199
suggests: deps
228200
.suggests
229201
.iter()
230-
.map(|s| Cow::Owned(s.name().to_string()))
202+
.map(|&d| Cow::Owned(d.clone()))
231203
.collect(),
232204
kind,
233205
force_source: false,

0 commit comments

Comments
 (0)