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}
63
64impl PoolOptimizer {
65 pub fn new() -> PoolOptimizer {
66 PoolOptimizer {
67 expansion_cache: HashMap::new(),
68 irremovable: HashSet::new(),
69 require_constraints: HashMap::new(),
70 conflict_constraints: HashMap::new(),
71 to_remove: HashSet::new(),
72 aliases_per_package: HashMap::new(),
73 }
74 }
75
76 pub fn optimize(
79 mut self,
80 request: &Request,
81 pool: &Pool,
82 arena: &[Package],
83 policy: &mut DefaultPolicy,
84 ) -> Pool {
85 self.prepare(request, pool, arena);
86 self.optimize_by_identical_dependencies(pool, arena, policy);
87 self.optimize_impossible_packages_away(request, pool, arena);
88 let kept: Vec<usize> = (1..=pool.len())
89 .filter(|id| !self.to_remove.contains(id))
90 .map(|id| pool.package_by_id(id))
91 .collect();
92 pool.with_packages(kept, arena)
93 }
94
95 fn expand_disjunctive(constraint: &Constraint) -> Vec<Constraint> {
96 let compact = intervals::compact_constraint(constraint);
97 match &compact {
98 Constraint::Multi {
99 constraints,
100 conjunctive: false,
101 } => constraints.clone(),
102 _ => vec![compact],
103 }
104 }
105
106 fn extract(
109 cache: &mut HashMap<String, Vec<(String, Constraint)>>,
110 map: &mut HashMap<String, IndexedMap<Constraint>>,
111 package: &str,
112 pretty: &str,
113 constraint: &Constraint,
114 ) {
115 let pretty = if pretty == "self.version" {
117 format!("\u{0}{constraint}")
118 } else {
119 pretty.to_owned()
120 };
121 let expansions = cache.entry(pretty).or_insert_with(|| {
122 Self::expand_disjunctive(constraint)
123 .into_iter()
124 .map(|c| (c.to_string(), c))
125 .collect()
126 });
127 let per_name = map.entry(package.to_owned()).or_default();
128 for (key, expanded) in expansions {
129 if !per_name.contains(key) {
132 per_name.entry_or_insert_with(key, || expanded.clone());
133 }
134 }
135 }
136
137 fn prepare(&mut self, request: &Request, pool: &Pool, arena: &[Package]) {
139 let mut irremovable_groups: OrderedMap<Vec<Constraint>> = OrderedMap::default();
140 for idx in request.fixed_or_locked_packages() {
141 let p = &arena[idx];
142 let c = Constraint::new(Op::Eq, p.version.clone());
143 match irremovable_groups.0.iter_mut().find(|(k, _)| *k == p.name) {
144 Some((_, list)) => list.push(c),
145 None => irremovable_groups.insert(&p.name, vec![c]),
146 }
147 }
148 for (name, constraint) in request.requires.iter() {
149 let pretty = format!("\u{0}{constraint}");
152 Self::extract(
153 &mut self.expansion_cache,
154 &mut self.require_constraints,
155 name,
156 &pretty,
157 constraint,
158 );
159 }
160 for id in 1..=pool.len() {
161 let p = &arena[pool.package_by_id(id)];
162 for link in p.requires.iter() {
163 Self::extract(
164 &mut self.expansion_cache,
165 &mut self.require_constraints,
166 &link.target,
167 &link.pretty_constraint,
168 &link.constraint,
169 );
170 }
171 for link in p.conflicts.iter() {
172 Self::extract(
173 &mut self.expansion_cache,
174 &mut self.conflict_constraints,
175 &link.target,
176 &link.pretty_constraint,
177 &link.constraint,
178 );
179 }
180 if let Some(base) = p.alias_of.and_then(|b| pool.id_of(b)) {
181 self.aliases_per_package.entry(base).or_default().push(id);
182 }
183 }
184 let irremovable: HashMap<String, Constraint> = irremovable_groups
185 .0
186 .into_iter()
187 .map(|(name, constraints)| {
188 let c = if constraints.len() == 1 {
189 constraints.into_iter().next().expect("one")
190 } else {
191 Constraint::Multi {
192 constraints,
193 conjunctive: false,
194 }
195 };
196 (name, c)
197 })
198 .collect();
199 for id in 1..=pool.len() {
200 let p = &arena[pool.package_by_id(id)];
201 let Some(c) = irremovable.get(&p.name) else {
202 continue;
203 };
204 if c.matches_version(&p.version) {
205 self.mark_irremovable(id, pool, arena);
206 }
207 }
208 }
209
210 fn mark_irremovable(&mut self, id: usize, pool: &Pool, arena: &[Package]) {
211 self.irremovable.insert(id);
212 if let Some(base) = arena[pool.package_by_id(id)]
213 .alias_of
214 .and_then(|b| pool.id_of(b))
215 {
216 self.mark_irremovable(base, pool, arena);
217 }
218 if let Some(aliases) = self.aliases_per_package.get(&id) {
219 for a in aliases.clone() {
220 self.irremovable.insert(a);
221 }
222 }
223 }
224
225 fn dependency_hash(p: &Package) -> String {
227 let mut hash = String::new();
228 for (key, links) in [
229 ("requires", &p.requires),
230 ("conflicts", &p.conflicts),
231 ("replaces", &p.replaces),
232 ("provides", &p.provides),
233 ] {
234 if links.is_empty() {
235 continue;
236 }
237 hash.push_str(key);
238 hash.push(':');
239 let mut sub: BTreeMap<Vec<u8>, String> = BTreeMap::new();
240 for link in links.iter() {
241 sub.insert(link.target.as_bytes().to_vec(), link.constraint.to_string());
242 }
243 for (target, constraint) in sub {
244 hash.push_str(&String::from_utf8_lossy(&target));
245 hash.push('@');
246 hash.push_str(&constraint);
247 }
248 }
249 hash
250 }
251
252 fn optimize_by_identical_dependencies(
254 &mut self,
255 pool: &Pool,
256 arena: &[Package],
257 policy: &mut DefaultPolicy,
258 ) {
259 let mut identical: IndexedMap<IndexedMap<IndexedMap<Vec<usize>>>> = IndexedMap::default();
261 for id in 1..=pool.len() {
262 if self.irremovable.contains(&id) {
263 continue;
264 }
265 self.to_remove.insert(id);
266 let p = &arena[pool.package_by_id(id)];
267 let dependency_hash = Self::dependency_hash(p);
268 let replace_parts: String = p
271 .replaces
272 .iter()
273 .filter(|l| l.constraint.matches_version(&p.version))
274 .map(|l| format!("require:{}", l.constraint))
275 .collect();
276 for name in p.names(false) {
277 let Some(requires) = self.require_constraints.get(&name) else {
278 continue;
279 };
280 let conflict_parts: String = match self.conflict_constraints.get(&name) {
282 Some(conflicts) => conflicts
283 .iter()
284 .filter(|(_, c)| c.matches_version(&p.version))
285 .map(|(key, _)| format!("conflict:{key}"))
286 .collect(),
287 None => String::new(),
288 };
289 for (key, require_constraint) in requires.iter() {
290 let matched = require_constraint.matches_version(&p.version);
291 if !matched && replace_parts.is_empty() && conflict_parts.is_empty() {
292 continue;
293 }
294 let mut group_hash = String::with_capacity(
295 key.len() + 8 + replace_parts.len() + conflict_parts.len(),
296 );
297 if matched {
298 group_hash.push_str("require:");
299 group_hash.push_str(key);
300 }
301 group_hash.push_str(&replace_parts);
302 group_hash.push_str(&conflict_parts);
303 identical
304 .entry_or_insert_with(&name, IndexedMap::default)
305 .entry_or_insert_with(&group_hash, IndexedMap::default)
306 .entry_or_insert_with(&dependency_hash, Vec::new)
307 .push(id);
308 }
309 }
310 }
311 for (name, groups) in identical.entries {
312 for (_, group) in groups.entries {
313 for (_, ids) in group.entries {
314 if ids.len() == 1 {
315 self.keep_package_in_group(ids[0], pool, arena, &name, &ids);
316 continue;
317 }
318 let literals: Vec<i64> = ids.iter().map(|&i| i as i64).collect();
319 for preferred in policy.select_preferred_packages(pool, arena, &literals, None)
320 {
321 self.keep_package_in_group(preferred as usize, pool, arena, &name, &ids);
322 }
323 }
324 }
325 }
326 }
327
328 fn keep_package_in_group(
331 &mut self,
332 id: usize,
333 pool: &Pool,
334 arena: &[Package],
335 _name: &str,
336 _ids: &[usize],
337 ) {
338 if !self.to_remove.contains(&id) {
339 return;
340 }
341 self.to_remove.remove(&id);
342 let p = &arena[pool.package_by_id(id)];
343 if let Some(base) = p.alias_of.and_then(|b| pool.id_of(b)) {
344 self.to_remove.remove(&base);
345 if let Some(aliases) = self.aliases_per_package.get(&base) {
346 for a in aliases {
347 self.to_remove.remove(a);
348 }
349 }
350 return;
351 }
352 if let Some(aliases) = self.aliases_per_package.get(&id) {
353 for a in aliases {
354 self.to_remove.remove(a);
355 }
356 }
357 }
358
359 fn optimize_impossible_packages_away(
361 &mut self,
362 request: &Request,
363 pool: &Pool,
364 arena: &[Package],
365 ) {
366 let locked = request.locked_packages_all();
367 if locked.is_empty() {
368 return;
369 }
370 let mut index: HashMap<String, Vec<(usize, usize)>> = HashMap::new();
372 for id in 1..=pool.len() {
373 if self.irremovable.contains(&id) {
374 continue;
375 }
376 let idx = pool.package_by_id(id);
377 let p = &arena[idx];
378 if self.aliases_per_package.contains_key(&id) || p.is_alias() {
379 continue;
380 }
381 if request.is_fixed_package(idx) || request.is_locked_package(idx) {
382 continue;
383 }
384 index.entry(p.name.clone()).or_default().push((id, idx));
385 }
386 for locked_idx in locked {
387 let p = &arena[locked_idx];
388 let unused = p
389 .names(false)
390 .iter()
391 .all(|n| !self.require_constraints.contains_key(n));
392 if unused {
393 continue;
394 }
395 for link in p.requires.iter() {
396 let Some(candidates) = index.get_mut(&link.target) else {
397 continue;
398 };
399 let mut kept = Vec::new();
400 for (id, idx) in candidates.iter().copied() {
401 if !link.constraint.matches_version(&arena[idx].version) {
402 self.to_remove.insert(id);
403 } else {
404 kept.push((id, idx));
405 }
406 }
407 *candidates = kept;
408 }
409 }
410 }
411}
412
413impl Default for PoolOptimizer {
414 fn default() -> Self {
415 Self::new()
416 }
417}