Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ members = ["crates/*"]
resolver = "2"

[workspace.package]
version = "0.1.0-rc.6"
version = "0.1.0-rc.7"
authors = [
"Lorenzo Montant <lo.montant.pro@gmail.com>",
"Maxim Doucet <maxim.doucet@gmail.com>",
Expand All @@ -23,8 +23,8 @@ lazy_static = "1.5.0"
rand = "0.8.5"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
rer-version = { path = "crates/rer-version", version = "0.1.0-rc.6" }
rer-resolver = { path = "crates/rer-resolver", version = "0.1.0-rc.6" }
rer-version = { path = "crates/rer-version", version = "0.1.0-rc.7" }
rer-resolver = { path = "crates/rer-resolver", version = "0.1.0-rc.7" }
pyo3 = { version = "0.23.5", features = ["extension-module"] }
# `mimalloc` is wired into the bench binary as a `#[global_allocator]`.
# Callgrind shows ~33 % of cycles in libc malloc/free; mimalloc has measurably
Expand Down
58 changes: 40 additions & 18 deletions crates/rer-python/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,14 @@ pub struct SolveResult {
/// Number of solve steps the solver performed.
#[pyo3(get)]
pub num_iterations: u32,
/// Resolved ephemerals as rez-style requirement strings, e.g.
/// `[".feature-1.5", ".mode-debug"]`. Each entry is the intersected
/// range of every ephemeral (`.foo`) requirement that participated in
/// the solve. Empty when no ephemerals were involved, and always empty
/// for `"failed"` / `"error"` results. Mirrors rez's
/// `Solver.resolved_ephemerals`.
#[pyo3(get)]
pub resolved_ephemerals: Vec<String>,
}

#[pymethods]
Expand All @@ -218,6 +226,7 @@ impl SolveResult {
failure_description: Some(message),
solve_time_ms: start.elapsed().as_secs_f64() * 1000.0,
num_iterations: 0,
resolved_ephemerals: Vec::new(),
}
}
}
Expand Down Expand Up @@ -338,6 +347,7 @@ fn solve(
failure_description: Some(scope_err.to_string()),
solve_time_ms: start.elapsed().as_secs_f64() * 1000.0,
num_iterations: 0,
resolved_ephemerals: Vec::new(),
});
}
Err(_) => {
Expand All @@ -353,36 +363,47 @@ fn solve(

Ok(match solver.status() {
SolverStatus::Solved => {
let variants = solver.resolved_packages().unwrap_or_default();
let resolved_packages: Vec<ResolvedVariant> = variants
.iter()
.map(|v| ResolvedVariant {
name: v.name().to_string(),
version: v.version().to_string(),
variant_index: v.index(),
requires: v
.requires()
.requirements()
.iter()
.map(|r| r.to_string())
.collect(),
uri: match v.index() {
Some(idx) => format!("{}/{}/package.py[{}]", v.name(), v.version(), idx),
None => format!("{}/{}/package.py", v.name(), v.version()),
},
// Use the borrowing iterator forms — no intermediate Vec, and
// ephemerals are streamed as `&Requirement` rather than cloned.
let resolved_packages: Vec<ResolvedVariant> = solver
.resolved_packages_iter()
.map(|it| {
it.map(|v| ResolvedVariant {
name: v.name().to_string(),
version: v.version().to_string(),
variant_index: v.index(),
requires: v
.requires()
.requirements()
.iter()
.map(|r| r.to_string())
.collect(),
uri: match v.index() {
Some(idx) => {
format!("{}/{}/package.py[{}]", v.name(), v.version(), idx)
}
None => format!("{}/{}/package.py", v.name(), v.version()),
},
})
.collect()
})
.collect();
.unwrap_or_default();
let resolved: Vec<(String, String, Option<usize>)> = resolved_packages
.iter()
.map(|rv| (rv.name.clone(), rv.version.clone(), rv.variant_index))
.collect();
let resolved_ephemerals: Vec<String> = solver
.resolved_ephemerals_iter()
.map(|it| it.map(|r| r.to_string()).collect())
.unwrap_or_default();
SolveResult {
status: "solved".to_string(),
resolved_packages,
resolved,
failure_description: None,
solve_time_ms,
num_iterations,
resolved_ephemerals,
}
}
SolverStatus::Failed => SolveResult {
Expand All @@ -392,6 +413,7 @@ fn solve(
failure_description: solver.failure_description(),
solve_time_ms,
num_iterations,
resolved_ephemerals: Vec::new(),
},
other => SolveResult::error(format!("unexpected solver status: {other:?}"), start),
})
Expand Down
38 changes: 34 additions & 4 deletions crates/rer-resolver/src/rez_solver/phase.rs
Original file line number Diff line number Diff line change
Expand Up @@ -493,11 +493,41 @@ impl ResolvePhase {

/// The solved variants of this (solved) phase, in scope order.
pub fn solved_variants(&self) -> Vec<Rc<PackageVariant>> {
self.iter_solved_variants().collect()
}

/// Borrowing iterator form of [`Self::solved_variants`]. Lets callers
/// iterate the resolved variants without allocating an intermediate `Vec`.
/// Internally still clones the scope vec because `get_solved_variant`
/// takes `&mut self` (it triggers a deferred sort inside the variant
/// slice); the saving vs `solved_variants` is the trailing `.collect()`.
///
/// The yielded `Rc<PackageVariant>` are refcount bumps — cheap.
pub fn iter_solved_variants(&self) -> impl Iterator<Item = Rc<PackageVariant>> + '_ {
let mut scopes = self.scopes.clone();
scopes
.iter_mut()
.filter_map(|s| Rc::make_mut(s).get_solved_variant())
.collect()
std::iter::from_fn(move || loop {
let scope = scopes.first_mut()?;
let variant = Rc::make_mut(scope).get_solved_variant();
scopes.remove(0);
if let Some(v) = variant {
return Some(v);
}
})
}

/// The intersected requirement of each ephemeral (`.foo`) scope in this
/// (solved) phase, in scope order. Mirrors rez's
/// `_ResolvePhase.get_resolved_ephemerals`.
pub fn solved_ephemerals(&self) -> Vec<Requirement> {
self.iter_solved_ephemerals().cloned().collect()
}

/// Borrowing iterator form of [`Self::solved_ephemerals`]. Lets callers
/// stream resolved ephemerals (`&Requirement`) without allocating an
/// intermediate `Vec` or cloning every entry — the heavier of the two
/// costs in the existing API.
pub fn iter_solved_ephemerals(&self) -> impl Iterator<Item = &Requirement> + '_ {
self.scopes.iter().filter_map(|s| s.get_solved_ephemeral())
}
}

Expand Down
150 changes: 150 additions & 0 deletions crates/rer-resolver/src/rez_solver/solver.rs
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,41 @@ impl Solver {
Some(self.phase_stack.last().unwrap().solved_variants())
}

/// Borrowing iterator form of [`Self::resolved_packages`]. `None` if the
/// solve did not succeed; otherwise an iterator that yields each resolved
/// variant (cheap `Rc` refcount bumps) without allocating an intermediate
/// `Vec`.
pub fn resolved_packages_iter(
&self,
) -> Option<impl Iterator<Item = Rc<PackageVariant>> + '_> {
if self.status() != SolverStatus::Solved {
return None;
}
Some(self.phase_stack.last().unwrap().iter_solved_variants())
}

/// The resolved ephemerals (intersected ranges of every `.foo` requirement
/// that participated in the solve), or `None` if the solve did not
/// succeed. Mirrors rez's `Solver.resolved_ephemerals`.
pub fn resolved_ephemerals(&self) -> Option<Vec<Requirement>> {
if self.status() != SolverStatus::Solved {
return None;
}
Some(self.phase_stack.last().unwrap().solved_ephemerals())
}

/// Borrowing iterator form of [`Self::resolved_ephemerals`]. `None` if the
/// solve did not succeed; otherwise an iterator that yields each resolved
/// ephemeral as `&Requirement`, with no intermediate `Vec` and no clones.
/// The largest saving of the four iterator-form accessors — `Requirement`
/// clones are heavier than `Rc::clone`.
pub fn resolved_ephemerals_iter(&self) -> Option<impl Iterator<Item = &Requirement> + '_> {
if self.status() != SolverStatus::Solved {
return None;
}
Some(self.phase_stack.last().unwrap().iter_solved_ephemerals())
}

/// A human-readable failure description, or `None` if the solve did not
/// fail. Mirrors the gist of rez's `Solver.failure_description`.
pub fn failure_description(&self) -> Option<String> {
Expand Down Expand Up @@ -405,6 +440,121 @@ mod tests {
assert_eq!(resolved_set(&solver), vec![("foo".into(), "1.0".into())]);
}

#[test]
fn test_iter_resolved_packages_matches_vec_form() {
let solver = solve(
repo(vec![
("app", vec![("1.0", pkg(&["lib-2"], &[]))]),
("lib", vec![("1.0", pkg(&[], &[])), ("2.0", pkg(&[], &[]))]),
]),
&["app"],
);
let owned: Vec<(String, String)> = solver
.resolved_packages()
.unwrap()
.iter()
.map(|v| (v.name().to_string(), v.version().to_string()))
.collect();
let borrowed: Vec<(String, String)> = solver
.resolved_packages_iter()
.unwrap()
.map(|v| (v.name().to_string(), v.version().to_string()))
.collect();
assert_eq!(owned, borrowed);
// Iter form on a failed solve returns None too.
let failed = solve(
repo(vec![(
"foo",
vec![("1.0", pkg(&[], &[])), ("2.0", pkg(&[], &[]))],
)]),
&["foo-1", "foo-2"],
);
assert!(failed.resolved_packages_iter().is_none());
}

#[test]
fn test_iter_resolved_ephemerals_matches_vec_form() {
let solver = solve(
repo(vec![("foo", vec![("1.0", pkg(&[], &[]))])]),
&["foo", ".feature-1+<3", ".feature-2+"],
);
let owned: Vec<String> = solver
.resolved_ephemerals()
.unwrap()
.iter()
.map(|r| r.to_string())
.collect();
let borrowed: Vec<String> = solver
.resolved_ephemerals_iter()
.unwrap()
.map(|r| r.to_string())
.collect();
assert_eq!(owned, borrowed);
// The borrowing form yields `&Requirement` — confirm callers can
// collect without forcing an owned copy of the requirements.
let by_ref: Vec<&Requirement> =
solver.resolved_ephemerals_iter().unwrap().collect();
assert_eq!(by_ref.len(), 1);
assert_eq!(by_ref[0].to_string(), ".feature-2+<3");
}

#[test]
fn test_resolved_ephemerals_intersect_request_only() {
// Two ephemerals on the request intersect to the narrower range.
let solver = solve(
repo(vec![("foo", vec![("1.0", pkg(&[], &[]))])]),
&["foo", ".feature-1+<3", ".feature-2+"],
);
assert_eq!(solver.status(), SolverStatus::Solved);
let eph: Vec<String> = solver
.resolved_ephemerals()
.expect("solved => Some")
.iter()
.map(|r| r.to_string())
.collect();
assert_eq!(eph, vec![".feature-2+<3".to_string()]);
}

#[test]
fn test_resolved_ephemerals_from_package_requires() {
// The ephemeral is contributed by a resolved package's `requires`.
let solver = solve(
repo(vec![("app", vec![("1.0", pkg(&[".mode-debug"], &[]))])]),
&["app"],
);
assert_eq!(solver.status(), SolverStatus::Solved);
let eph: Vec<String> = solver
.resolved_ephemerals()
.expect("solved => Some")
.iter()
.map(|r| r.to_string())
.collect();
assert_eq!(eph, vec![".mode-debug".to_string()]);
}

#[test]
fn test_resolved_ephemerals_empty_when_none_in_solve() {
let solver = solve(repo(vec![("foo", vec![("1.0", pkg(&[], &[]))])]), &["foo"]);
assert_eq!(solver.status(), SolverStatus::Solved);
assert_eq!(
solver.resolved_ephemerals().expect("solved => Some"),
Vec::<Requirement>::new()
);
}

#[test]
fn test_resolved_ephemerals_none_when_not_solved() {
let solver = solve(
repo(vec![(
"foo",
vec![("1.0", pkg(&[], &[])), ("2.0", pkg(&[], &[]))],
)]),
&["foo-1", "foo-2"],
);
assert_eq!(solver.status(), SolverStatus::Failed);
assert!(solver.resolved_ephemerals().is_none());
}

#[test]
fn test_variant_selection() {
// foo has two variants; the solver should pick one and resolve its deps.
Expand Down
2 changes: 1 addition & 1 deletion docs/config.toml
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ weight = 10
name = "GitHub"
pre = '<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-github"><path d="M9 19c-5 1.5-5-2.5-7-3m14 6v-3.87a3.37 3.37 0 0 0-.94-2.61c3.14-.35 6.44-1.54 6.44-7A5.44 5.44 0 0 0 20 4.77 5.07 5.07 0 0 0 19.91 1S18.73.65 16 2.48a13.38 13.38 0 0 0-7 0C6.27.65 5.09 1 5.09 1A5.07 5.07 0 0 0 5 4.77a5.44 5.44 0 0 0-1.5 3.78c0 5.42 3.3 6.61 6.44 7A3.37 3.37 0 0 0 9 18.13V22"></path></svg>'
url = "https://github.com/doubleailes/rer"
post = "v0.1.0-rc.6"
post = "v0.1.0-rc.7"
weight = 20

# Footer contents
Expand Down
2 changes: 1 addition & 1 deletion docs/content/_index.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ title = "rer — Rez En Rust"
lead = "A faithful Rust port of <a href=\"https://github.com/AcademySoftwareFoundation/rez\">rez</a>'s package solver — callable from Python via PyO3, resolves match rez 1:1."
url = "/docs/getting-started/introduction/"
url_button = "Get started"
repo_version = "GitHub v0.1.0-rc.6"
repo_version = "GitHub v0.1.0-rc.7"
repo_license = "MIT-licensed."
repo_url = "https://github.com/doubleailes/rer"

Expand Down
2 changes: 1 addition & 1 deletion docs/content/docs/getting-started/quick-start.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ Add the resolver crate to your `Cargo.toml`:

```toml
[dependencies]
rer-resolver = "0.1.0-rc.6"
rer-resolver = "0.1.0-rc.7"
```

Then call the solver against an in-memory repository:
Expand Down
Loading
Loading