1use crate::constraint::{Constraint, Op};
6use crate::intervals;
7use crate::package::Package;
8use crate::policy::DefaultPolicy;
9use crate::pool::{OrderedMap, Pool, Request};
10use std::collections::{BTreeMap, HashMap, HashSet};
11
12#[derive(Debug, Clone)]
14struct IndexedMap<V> {
15 entries: Vec<(String, V)>,
16 index: HashMap<String, usize>,
17}
18
19impl<V> Default for IndexedMap<V> {
20 fn default() -> Self {
21 IndexedMap {
22 entries: Vec::new(),
23 index: HashMap::new(),
24 }
25 }
26}
27
28impl<V> IndexedMap<V> {
29 fn contains(&self, key: &str) -> bool {
30 self.index.contains_key(key)
31 }
32 fn entry_or_insert_with(&mut self, key: &str, default: impl FnOnce() -> V) -> &mut V {
34 let i = match self.index.get(key) {
35 Some(&i) => i,
36 None => {
37 self.entries.push((key.to_owned(), default()));
38 let i = self.entries.len() - 1;
39 self.index.insert(key.to_owned(), i);
40 i
41 }
42 };
43 &mut self.entries[i].1
44 }
45 fn iter(&self) -> impl Iterator<Item = (&String, &V)> {
46 self.entries.iter().map(|(k, v)| (k, v))
47 }
48}
49
50pub struct PoolOptimizer {
51 expansion_cache: HashMap<String, Vec<(String, Constraint)>>,
55 irremovable: HashSet<usize>,
56 require_constraints: HashMap<String, IndexedMap<Constraint>>,
58 conflict_constraints: HashMap<String, IndexedMap<Constraint>>,
59 to_remove: HashSet<usize>,
60 aliases_per_package: HashMap<usize, Vec<usize>>,
62 removed_versions_by_package: BTreeMap<usize, BTreeMap<String, String>>,
64}
65
66impl PoolOptimizer {
67 pub fn new() -> PoolOptimizer {
68 PoolOptimizer {
69 expansion_cache: HashMap::new(),
70 irremovable: HashSet::new(),
71 require_constraints: HashMap::new(),
72 conflict_constraints: HashMap::new(),
73 to_remove: HashSet::new(),
74 aliases_per_package: HashMap::new(),
75 removed_versions_by_package: BTreeMap::new(),
76 }
77 }
78
79 pub fn optimize(
82 mut self,
83 request: &Request,
84 pool: &Pool,
85 arena: &[Package],
86 policy: &mut DefaultPolicy,
87 ) -> Pool {
88 self.prepare(request, pool, arena);
89 self.optimize_by_identical_dependencies(pool, arena, policy);
90 self.optimize_impossible_packages_away(request, pool, arena);
91 let mut kept: Vec<usize> = Vec::new();
94 let mut removed_versions: BTreeMap<String, BTreeMap<String, String>> = BTreeMap::new();
95 for id in 1..=pool.len() {
96 let idx = pool.package_by_id(id);
97 if self.to_remove.contains(&id) {
98 let p = &arena[idx];
99 removed_versions
100 .entry(p.name.clone())
101 .or_default()
102 .insert(p.version.clone(), p.pretty_version.clone());
103 } else {
104 kept.push(idx);
105 }
106 }
107 let mut optimized = pool.with_packages(kept, arena);
108 optimized.removed_versions = removed_versions;
109 optimized.removed_versions_by_package = self.removed_versions_by_package;
110 optimized
111 }
112
113 fn expand_disjunctive(constraint: &Constraint) -> Vec<Constraint> {
114 let compact = intervals::compact_constraint(constraint);
115 match &compact {
116 Constraint::Multi {
117 constraints,
118 conjunctive: false,
119 } => constraints.clone(),
120 _ => vec![compact],
121 }
122 }
123
124 fn extract(
127 cache: &mut HashMap<String, Vec<(String, Constraint)>>,
128 map: &mut HashMap<String, IndexedMap<Constraint>>,
129 package: &str,
130 pretty: &str,
131 constraint: &Constraint,
132 ) {
133 let pretty = if pretty == "self.version" {
135 format!("\u{0}{constraint}")
136 } else {
137 pretty.to_owned()
138 };
139 let expansions = cache.entry(pretty).or_insert_with(|| {
140 Self::expand_disjunctive(constraint)
141 .into_iter()
142 .map(|c| (c.to_string(), c))
143 .collect()
144 });
145 let per_name = map.entry(package.to_owned()).or_default();
146 for (key, expanded) in expansions {
147 if !per_name.contains(key) {
150 per_name.entry_or_insert_with(key, || expanded.clone());
151 }
152 }
153 }
154
155 fn prepare(&mut self, request: &Request, pool: &Pool, arena: &[Package]) {
157 let mut irremovable_groups: OrderedMap<Vec<Constraint>> = OrderedMap::default();
158 for idx in request.fixed_or_locked_packages() {
159 let p = &arena[idx];
160 let c = Constraint::new(Op::Eq, p.version.clone());
161 match irremovable_groups.0.iter_mut().find(|(k, _)| *k == p.name) {
162 Some((_, list)) => list.push(c),
163 None => irremovable_groups.insert(&p.name, vec![c]),
164 }
165 }
166 for (name, constraint) in request.requires.iter() {
167 let pretty = format!("\u{0}{constraint}");
170 Self::extract(
171 &mut self.expansion_cache,
172 &mut self.require_constraints,
173 name,
174 &pretty,
175 constraint,
176 );
177 }
178 for id in 1..=pool.len() {
179 let p = &arena[pool.package_by_id(id)];
180 for link in p.requires.iter() {
181 Self::extract(
182 &mut self.expansion_cache,
183 &mut self.require_constraints,
184 &link.target,
185 &link.pretty_constraint,
186 &link.constraint,
187 );
188 }
189 for link in p.conflicts.iter() {
190 Self::extract(
191 &mut self.expansion_cache,
192 &mut self.conflict_constraints,
193 &link.target,
194 &link.pretty_constraint,
195 &link.constraint,
196 );
197 }
198 if let Some(base) = p.alias_of.and_then(|b| pool.id_of(b)) {
199 self.aliases_per_package.entry(base).or_default().push(id);
200 }
201 }
202 let irremovable: HashMap<String, Constraint> = irremovable_groups
203 .0
204 .into_iter()
205 .map(|(name, constraints)| {
206 let c = if constraints.len() == 1 {
207 constraints.into_iter().next().expect("one")
208 } else {
209 Constraint::Multi {
210 constraints,
211 conjunctive: false,
212 }
213 };
214 (name, c)
215 })
216 .collect();
217 for id in 1..=pool.len() {
218 let p = &arena[pool.package_by_id(id)];
219 let Some(c) = irremovable.get(&p.name) else {
220 continue;
221 };
222 if c.matches_version(&p.version) {
223 self.mark_irremovable(id, pool, arena);
224 }
225 }
226 }
227
228 fn mark_irremovable(&mut self, id: usize, pool: &Pool, arena: &[Package]) {
229 self.irremovable.insert(id);
230 if let Some(base) = arena[pool.package_by_id(id)]
231 .alias_of
232 .and_then(|b| pool.id_of(b))
233 {
234 self.mark_irremovable(base, pool, arena);
235 }
236 if let Some(aliases) = self.aliases_per_package.get(&id) {
237 for a in aliases.clone() {
238 self.irremovable.insert(a);
239 }
240 }
241 }
242
243 fn dependency_hash(p: &Package) -> String {
245 let mut hash = String::new();
246 for (key, links) in [
247 ("requires", &p.requires),
248 ("conflicts", &p.conflicts),
249 ("replaces", &p.replaces),
250 ("provides", &p.provides),
251 ] {
252 if links.is_empty() {
253 continue;
254 }
255 hash.push_str(key);
256 hash.push(':');
257 let mut sub: BTreeMap<Vec<u8>, String> = BTreeMap::new();
258 for link in links.iter() {
259 sub.insert(link.target.as_bytes().to_vec(), link.constraint.to_string());
260 }
261 for (target, constraint) in sub {
262 hash.push_str(&String::from_utf8_lossy(&target));
263 hash.push('@');
264 hash.push_str(&constraint);
265 }
266 }
267 hash
268 }
269
270 fn optimize_by_identical_dependencies(
272 &mut self,
273 pool: &Pool,
274 arena: &[Package],
275 policy: &mut DefaultPolicy,
276 ) {
277 let mut identical: IndexedMap<IndexedMap<IndexedMap<Vec<usize>>>> = IndexedMap::default();
279 for id in 1..=pool.len() {
280 if self.irremovable.contains(&id) {
281 continue;
282 }
283 self.to_remove.insert(id);
284 let p = &arena[pool.package_by_id(id)];
285 let dependency_hash = Self::dependency_hash(p);
286 let replace_parts: String = p
289 .replaces
290 .iter()
291 .filter(|l| l.constraint.matches_version(&p.version))
292 .map(|l| format!("require:{}", l.constraint))
293 .collect();
294 for name in p.names(false) {
295 let Some(requires) = self.require_constraints.get(&name) else {
296 continue;
297 };
298 let conflict_parts: String = match self.conflict_constraints.get(&name) {
300 Some(conflicts) => conflicts
301 .iter()
302 .filter(|(_, c)| c.matches_version(&p.version))
303 .map(|(key, _)| format!("conflict:{key}"))
304 .collect(),
305 None => String::new(),
306 };
307 for (key, require_constraint) in requires.iter() {
308 let matched = require_constraint.matches_version(&p.version);
309 if !matched && replace_parts.is_empty() && conflict_parts.is_empty() {
310 continue;
311 }
312 let mut group_hash = String::with_capacity(
313 key.len() + 8 + replace_parts.len() + conflict_parts.len(),
314 );
315 if matched {
316 group_hash.push_str("require:");
317 group_hash.push_str(key);
318 }
319 group_hash.push_str(&replace_parts);
320 group_hash.push_str(&conflict_parts);
321 identical
322 .entry_or_insert_with(&name, IndexedMap::default)
323 .entry_or_insert_with(&group_hash, IndexedMap::default)
324 .entry_or_insert_with(&dependency_hash, Vec::new)
325 .push(id);
326 }
327 }
328 }
329 for (name, groups) in identical.entries {
330 for (_, group) in groups.entries {
331 for (_, ids) in group.entries {
332 if ids.len() == 1 {
333 self.keep_package_in_group(ids[0], pool, arena, &name, &ids);
334 continue;
335 }
336 let literals: Vec<i64> = ids.iter().map(|&i| i as i64).collect();
337 for preferred in policy.select_preferred_packages(pool, arena, &literals, None)
338 {
339 self.keep_package_in_group(preferred as usize, pool, arena, &name, &ids);
340 }
341 }
342 }
343 }
344 }
345
346 fn keep_package_in_group(
351 &mut self,
352 id: usize,
353 pool: &Pool,
354 arena: &[Package],
355 name: &str,
356 ids: &[usize],
357 ) {
358 let mut versions: BTreeMap<String, String> = BTreeMap::new();
359 for &gid in ids {
360 let mut idx = pool.package_by_id(gid);
361 let gp = &arena[idx];
362 if let Some(base) = gp.alias_of.filter(|_| gp.pretty_version == "9999999-dev") {
363 idx = base;
364 }
365 versions.insert(
366 arena[idx].version.clone(),
367 arena[idx].pretty_version.clone(),
368 );
369 }
370 let idx = pool.package_by_id(id);
371 self.record_removed_versions(idx, arena, name, &versions);
372 if !self.to_remove.contains(&id) {
373 return;
374 }
375 self.to_remove.remove(&id);
376 let p = &arena[idx];
377 if let Some(base_idx) = p.alias_of {
378 if let Some(base) = pool.id_of(base_idx) {
379 self.to_remove.remove(&base);
380 self.record_removed_versions(base_idx, arena, name, &versions);
381 if let Some(aliases) = self.aliases_per_package.get(&base).cloned() {
382 for a in aliases {
383 self.to_remove.remove(&a);
384 self.record_removed_versions(pool.package_by_id(a), arena, name, &versions);
385 }
386 }
387 }
388 return;
389 }
390 if let Some(aliases) = self.aliases_per_package.get(&id).cloned() {
391 for a in aliases {
392 self.to_remove.remove(&a);
393 self.record_removed_versions(pool.package_by_id(a), arena, name, &versions);
394 }
395 }
396 }
397
398 fn record_removed_versions(
401 &mut self,
402 idx: usize,
403 arena: &[Package],
404 name: &str,
405 versions: &BTreeMap<String, String>,
406 ) {
407 if !arena[idx].names(false).iter().any(|n| n == name) {
408 return;
409 }
410 let entry = self.removed_versions_by_package.entry(idx).or_default();
411 for (v, p) in versions {
412 entry.insert(v.clone(), p.clone());
413 }
414 }
415
416 fn optimize_impossible_packages_away(
418 &mut self,
419 request: &Request,
420 pool: &Pool,
421 arena: &[Package],
422 ) {
423 let locked = request.locked_packages_all();
424 if locked.is_empty() {
425 return;
426 }
427 let mut index: HashMap<String, Vec<(usize, usize)>> = HashMap::new();
429 for id in 1..=pool.len() {
430 if self.irremovable.contains(&id) {
431 continue;
432 }
433 let idx = pool.package_by_id(id);
434 let p = &arena[idx];
435 if self.aliases_per_package.contains_key(&id) || p.is_alias() {
436 continue;
437 }
438 if request.is_fixed_package(idx) || request.is_locked_package(idx) {
439 continue;
440 }
441 index.entry(p.name.clone()).or_default().push((id, idx));
442 }
443 for locked_idx in locked {
444 let p = &arena[locked_idx];
445 let unused = p
446 .names(false)
447 .iter()
448 .all(|n| !self.require_constraints.contains_key(n));
449 if unused {
450 continue;
451 }
452 for link in p.requires.iter() {
453 let Some(candidates) = index.get_mut(&link.target) else {
454 continue;
455 };
456 let mut kept = Vec::new();
457 for (id, idx) in candidates.iter().copied() {
458 if !link.constraint.matches_version(&arena[idx].version) {
459 self.to_remove.insert(id);
460 } else {
461 kept.push((id, idx));
462 }
463 }
464 *candidates = kept;
465 }
466 }
467 }
468}
469
470impl Default for PoolOptimizer {
471 fn default() -> Self {
472 Self::new()
473 }
474}