Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit daa4be4

Browse files
committedFeb 15, 2025
Rework build sorting in the new solver
Reuse the same build sorting logic as the original solver in order to get a deterministic result out of the solve. The solve can change drastically based on which build is selected first. Signed-off-by: J Robert Ray <[email protected]>
1 parent f3ed70c commit daa4be4

File tree

3 files changed

+212
-32
lines changed

3 files changed

+212
-32
lines changed
 

‎crates/spk-solve/crates/package-iterator/src/lib.rs

+2
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,11 @@ mod error;
77
mod package_iterator;
88
mod promotion_patterns;
99

10+
pub use build_key::BuildKey;
1011
pub use error::{Error, Result};
1112
pub use package_iterator::{
1213
BuildIterator,
14+
BuildToSortedOptName,
1315
EmptyBuildIterator,
1416
PackageIterator,
1517
RepositoryPackageIterator,

‎crates/spk-solve/crates/package-iterator/src/package_iterator.rs

+8-16
Original file line numberDiff line numberDiff line change
@@ -472,13 +472,13 @@ pub struct BuildToSortedOptName {}
472472

473473
impl BuildToSortedOptName {
474474
pub fn sort_builds<'a>(
475-
builds: impl Iterator<Item = &'a (Arc<Spec>, PackageSource)>,
475+
builds: impl Iterator<Item = &'a Arc<Spec>>,
476476
) -> (Vec<OptNameBuf>, HashMap<BuildIdent, OptionMap>) {
477477
let mut number_non_src_builds: u64 = 0;
478478
let mut build_name_values: HashMap<BuildIdent, OptionMap> = HashMap::default();
479479
let mut changes: HashMap<OptNameBuf, ChangeCounter> = HashMap::new();
480480

481-
for (build, _) in builds {
481+
for build in builds {
482482
// Skip this if it's a '/src' build because '/src' builds
483483
// won't use the build option values in their key, they
484484
// don't need to be looked at. They have a type of key
@@ -591,20 +591,9 @@ impl SortedBuildIterator {
591591
Ok(sbi)
592592
}
593593

594-
pub async fn new_from_builds(
595-
builds: VecDeque<BuildWithRepos>,
596-
builds_with_impossible_requests: HashMap<BuildIdent, Compatibility>,
597-
) -> Result<Self> {
598-
let mut sbi = SortedBuildIterator { builds };
599-
600-
sbi.sort_by_build_option_values(builds_with_impossible_requests)
601-
.await;
602-
Ok(sbi)
603-
}
604-
605594
/// Helper for making BuildKey structures used in the sorting in
606595
/// sort_by_build_option_values() below
607-
fn make_option_values_build_key(
596+
pub fn make_option_values_build_key(
608597
spec: &Spec,
609598
ordered_names: &Vec<OptNameBuf>,
610599
build_name_values: &HashMap<BuildIdent, OptionMap>,
@@ -632,8 +621,11 @@ impl SortedBuildIterator {
632621
) {
633622
let start = Instant::now();
634623

635-
let (key_entry_names, build_name_values) =
636-
BuildToSortedOptName::sort_builds(self.builds.iter().flat_map(|hm| hm.values()));
624+
let (key_entry_names, build_name_values) = BuildToSortedOptName::sort_builds(
625+
self.builds
626+
.iter()
627+
.flat_map(|hm| hm.values().map(|(spec, _src)| spec)),
628+
);
637629

638630
// Sort the builds by their generated keys generated from the
639631
// ordered names and values worth including.

‎crates/spk-solve/src/cdcl_solver/spk_provider.rs

+202-16
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,11 @@
55
use std::borrow::Cow;
66
use std::cell::RefCell;
77
use std::collections::{BTreeSet, HashMap, HashSet};
8+
use std::ops::Not;
89
use std::str::FromStr;
910
use std::sync::Arc;
1011

12+
use itertools::Itertools;
1113
use resolvo::utils::Pool;
1214
use resolvo::{
1315
Candidates,
@@ -38,7 +40,18 @@ use spk_schema::name::{OptNameBuf, PkgNameBuf};
3840
use spk_schema::prelude::{HasVersion, Named};
3941
use spk_schema::version::Version;
4042
use spk_schema::version_range::{parse_version_range, DoubleEqualsVersion, Ranged, VersionFilter};
41-
use spk_schema::{BuildIdent, Deprecate, Opt, OptionMap, Package, Recipe, Request, VersionIdent};
43+
use spk_schema::{
44+
BuildIdent,
45+
Deprecate,
46+
Opt,
47+
OptionMap,
48+
Package,
49+
Recipe,
50+
Request,
51+
Spec,
52+
VersionIdent,
53+
};
54+
use spk_solve_package_iterator::{BuildKey, BuildToSortedOptName, SortedBuildIterator};
4255
use spk_storage::RepositoryHandle;
4356
use tracing::{debug_span, Instrument};
4457

@@ -337,6 +350,51 @@ enum CanBuildFromSource {
337350
No(StringId),
338351
}
339352

353+
/// An iterator that yields slices of items that fall into the same partition.
354+
///
355+
/// The partition is determined by the key function.
356+
/// The items must be already sorted in ascending order by the key function.
357+
struct PartitionIter<'a, I, F, K>
358+
where
359+
F: for<'i> Fn(&'i I) -> K,
360+
K: PartialOrd,
361+
{
362+
slice: &'a [I],
363+
key_fn: F,
364+
}
365+
366+
impl<'a, I, F, K> PartitionIter<'a, I, F, K>
367+
where
368+
F: for<'i> Fn(&'i I) -> K,
369+
K: PartialOrd,
370+
{
371+
fn new(slice: &'a [I], key_fn: F) -> Self {
372+
Self { slice, key_fn }
373+
}
374+
}
375+
376+
impl<'a, I, F, K> Iterator for PartitionIter<'a, I, F, K>
377+
where
378+
F: for<'i> Fn(&'i I) -> K,
379+
K: PartialOrd,
380+
{
381+
type Item = &'a [I];
382+
383+
fn next(&mut self) -> Option<Self::Item> {
384+
let element = self.slice.first()?;
385+
386+
// Is a binary search overkill?
387+
let partition_key = (self.key_fn)(element);
388+
// No need to check the first element again.
389+
let p =
390+
1 + self.slice[1..].partition_point(|element| (self.key_fn)(element) <= partition_key);
391+
392+
let part = &self.slice[..p];
393+
self.slice = &self.slice[p..];
394+
Some(part)
395+
}
396+
}
397+
340398
pub(crate) struct SpkProvider {
341399
pub(crate) pool: Pool<RequestVS, ResolvoPackageName>,
342400
repos: Vec<Arc<RepositoryHandle>>,
@@ -564,6 +622,21 @@ impl SpkProvider {
564622
self.cancel_solving.borrow().is_some()
565623
}
566624

625+
/// Return an iterator that yields slices of builds that are from the same
626+
/// package version.
627+
///
628+
/// The provided builds must already be sorted otherwise the behavior is
629+
/// undefined.
630+
fn find_version_runs<'a>(
631+
builds: &'a [(SolvableId, &'a LocatedBuildIdentWithComponent, Arc<Spec>)],
632+
) -> impl Iterator<Item = &'a [(SolvableId, &'a LocatedBuildIdentWithComponent, Arc<Spec>)]>
633+
{
634+
PartitionIter::new(builds, |(_, ident, _)| {
635+
// partition by (name, version) ignoring repository
636+
(ident.ident.name(), ident.ident.version())
637+
})
638+
}
639+
567640
fn request_to_known_dependencies(&self, requirement: &Request) -> KnownDependencies {
568641
let mut known_deps = KnownDependencies::default();
569642
match requirement {
@@ -657,6 +730,44 @@ impl SpkProvider {
657730
}
658731
}
659732

733+
/// Order two builds based on which should be preferred to include in a
734+
/// solve as a candidate.
735+
///
736+
/// Generally this means a build with newer dependencies is ordered first.
737+
fn sort_builds(
738+
&self,
739+
build_key_index: &HashMap<SolvableId, BuildKey>,
740+
a: (SolvableId, &LocatedBuildIdentWithComponent),
741+
b: (SolvableId, &LocatedBuildIdentWithComponent),
742+
) -> std::cmp::Ordering {
743+
// This function should _not_ return `std::cmp::Ordering::Equal` unless
744+
// `a` and `b` are the same build (in practice this function will never
745+
// be called when that is true).
746+
747+
// Embedded stubs are always ordered last.
748+
match (a.1.ident.is_embedded(), b.1.ident.is_embedded()) {
749+
(true, false) => return std::cmp::Ordering::Greater,
750+
(false, true) => return std::cmp::Ordering::Less,
751+
_ => {}
752+
};
753+
754+
match (build_key_index.get(&a.0), build_key_index.get(&b.0)) {
755+
(Some(a_key), Some(b_key)) => {
756+
// BuildKey orders in reverse order from what is needed here.
757+
return b_key.cmp(a_key);
758+
}
759+
(Some(_), None) => return std::cmp::Ordering::Less,
760+
(None, Some(_)) => return std::cmp::Ordering::Greater,
761+
_ => {}
762+
};
763+
764+
// If neither build has a key, both packages failed to load?
765+
// Add debug assert to see if this ever happens.
766+
debug_assert!(false, "builds without keys");
767+
768+
a.1.ident.cmp(&b.1.ident)
769+
}
770+
660771
pub fn var_requirements(&mut self, requests: &[Request]) -> Vec<VersionSetId> {
661772
self.global_var_requests.reserve(requests.len());
662773
requests
@@ -967,13 +1078,94 @@ impl DependencyProvider for SpkProvider {
9671078
}
9681079

9691080
async fn sort_candidates(&self, _solver: &SolverCache<Self>, solvables: &mut [SolvableId]) {
970-
// This implementation just picks the highest version.
1081+
// Goal: Create a `BuildKey` for each build in `solvables`.
1082+
// The `BuildKey` factory needs as input the output from
1083+
// `BuildToSortedOptName::sort_builds`.
1084+
// `BuildToSortedOptName::sort_builds` needs to be fed builds from the
1085+
// same version.
1086+
// `solvables` can be builds from various versions so they need to be
1087+
// grouped by version.
1088+
let build_solvables = solvables
1089+
.iter()
1090+
.filter_map(|solvable_id| {
1091+
let solvable = self.pool.resolve_solvable(*solvable_id);
1092+
match &solvable.record {
1093+
SpkSolvable::LocatedBuildIdentWithComponent(
1094+
located_build_ident_with_component,
1095+
) =>
1096+
// sorting the source build (if any) is handled
1097+
// elsewhere; skip source builds.
1098+
{
1099+
located_build_ident_with_component
1100+
.ident
1101+
.is_source()
1102+
.not()
1103+
.then_some((*solvable_id, located_build_ident_with_component))
1104+
}
1105+
_ => None,
1106+
}
1107+
})
1108+
.sorted_by(
1109+
|(_, LocatedBuildIdentWithComponent { ident: a, .. }),
1110+
(_, LocatedBuildIdentWithComponent { ident: b, .. })| {
1111+
// build_solvables will be ordered by (pkg, version, build).
1112+
a.target().cmp(b.target())
1113+
},
1114+
)
1115+
.collect::<Vec<_>>();
1116+
1117+
// `BuildToSortedOptName::sort_builds` will need the package specs.
1118+
let mut build_solvables_and_specs = Vec::with_capacity(build_solvables.len());
1119+
for build_solvable in build_solvables {
1120+
let (solvable_id, located_build_ident_with_component) = build_solvable;
1121+
let repo = self
1122+
.repos
1123+
.iter()
1124+
.find(|repo| {
1125+
repo.name() == located_build_ident_with_component.ident.repository_name()
1126+
})
1127+
.expect("Expected solved package's repository to be in the list of repositories");
1128+
let Ok(package) = repo
1129+
.read_package(located_build_ident_with_component.ident.target())
1130+
.await
1131+
else {
1132+
// Any builds that can't load the spec will be sorted to the
1133+
// end. In most cases the package spec would already be loaded
1134+
// in cache at this point.
1135+
continue;
1136+
};
1137+
build_solvables_and_specs.push((
1138+
solvable_id,
1139+
located_build_ident_with_component,
1140+
package,
1141+
));
1142+
}
1143+
1144+
let mut build_key_index = HashMap::new();
1145+
build_key_index.reserve(build_solvables_and_specs.len());
1146+
1147+
// Find runs of the same package version.
1148+
for version_run in SpkProvider::find_version_runs(&build_solvables_and_specs) {
1149+
let (ordered_names, build_name_values) =
1150+
BuildToSortedOptName::sort_builds(version_run.iter().map(|(_, _, spec)| spec));
1151+
1152+
for (solvable_id, _, spec) in version_run {
1153+
let build_key = SortedBuildIterator::make_option_values_build_key(
1154+
spec,
1155+
&ordered_names,
1156+
&build_name_values,
1157+
false,
1158+
);
1159+
build_key_index.insert(*solvable_id, build_key);
1160+
}
1161+
}
1162+
9711163
// TODO: The ordering should take component names into account, so
9721164
// the run component or the build component is tried first in the
9731165
// appropriate situations.
974-
solvables.sort_by(|a, b| {
975-
let a = self.pool.resolve_solvable(*a);
976-
let b = self.pool.resolve_solvable(*b);
1166+
solvables.sort_by(|solvable_id_a, solvable_id_b| {
1167+
let a = self.pool.resolve_solvable(*solvable_id_a);
1168+
let b = self.pool.resolve_solvable(*solvable_id_b);
9771169
match (&a.record, &b.record) {
9781170
(
9791171
SpkSolvable::LocatedBuildIdentWithComponent(a),
@@ -1005,17 +1197,11 @@ impl DependencyProvider for SpkProvider {
10051197
(_, Build::Source) => return std::cmp::Ordering::Less,
10061198
_ => {}
10071199
};
1008-
// Sort embedded packges second last
1009-
match (a.ident.build(), b.ident.build()) {
1010-
(Build::Embedded(_), Build::Embedded(_)) => {
1011-
// TODO: Could perhaps sort on the parent
1012-
// package to prefer a newer parent.
1013-
std::cmp::Ordering::Equal
1014-
}
1015-
(Build::Embedded(_), _) => std::cmp::Ordering::Greater,
1016-
(_, Build::Embedded(_)) => std::cmp::Ordering::Less,
1017-
_ => std::cmp::Ordering::Equal,
1018-
}
1200+
self.sort_builds(
1201+
&build_key_index,
1202+
(*solvable_id_a, a),
1203+
(*solvable_id_b, b),
1204+
)
10191205
}
10201206
ord => ord,
10211207
}

0 commit comments

Comments
 (0)
Please sign in to comment.