1use crate::decisions::Decisions;
6use crate::package::Package;
7use crate::platform::is_platform_package;
8use crate::pool::{Pool, Request};
9use serde_json::Value;
10use std::cmp::Ordering;
11use std::collections::{HashMap, HashSet};
12
13#[derive(Debug, Clone, PartialEq, Eq)]
14pub enum Operation {
15 Install(usize),
16 Update(usize, usize),
18 Uninstall(usize),
19 MarkAliasInstalled(usize),
20 MarkAliasUninstalled(usize),
21}
22
23fn abandoned(p: &Package) -> (bool, Option<String>) {
25 match p.raw.get("abandoned") {
26 None | Some(Value::Null) => (false, None),
27 Some(Value::String(s)) => (!s.is_empty() && s != "0", Some(s.clone())),
29 Some(Value::Bool(b)) => (*b, None),
30 Some(Value::Number(n)) => (n.as_f64() != Some(0.0), None),
31 Some(Value::Array(a)) => (!a.is_empty(), None),
32 Some(Value::Object(o)) => (!o.is_empty(), None),
33 }
34}
35
36fn package_sort(a: &Package, b: &Package) -> Ordering {
38 if a.name == b.name {
39 if a.is_alias() != b.is_alias() {
40 return if a.is_alias() {
41 Ordering::Less
42 } else {
43 Ordering::Greater
44 };
45 }
46 return b.version.as_bytes().cmp(a.version.as_bytes());
47 }
48 b.name.as_bytes().cmp(a.name.as_bytes())
49}
50
51pub struct Transaction {
52 pub operations: Vec<Operation>,
53}
54
55impl Transaction {
56 pub fn new(arena: &[Package], present: &[usize], result: &[usize]) -> Transaction {
58 let mut result_map: Vec<usize> = result.to_vec();
60 result_map.sort_by(|&a, &b| package_sort(&arena[a], &arena[b]));
61 let mut by_name: HashMap<String, Vec<usize>> = HashMap::new();
62 for &idx in result {
63 for name in arena[idx].names(true) {
64 by_name.entry(name).or_default().push(idx);
65 }
66 }
67 for list in by_name.values_mut() {
68 list.sort_by(|&a, &b| package_sort(&arena[a], &arena[b]));
69 }
70 let providers =
71 |target: &str| -> Vec<usize> { by_name.get(target).cloned().unwrap_or_default() };
72
73 let mut operations: Vec<Operation> = Vec::new();
75 let mut present_package_map: HashMap<String, usize> = HashMap::new();
76 let mut remove_map: Vec<(String, usize)> = Vec::new();
77 let mut present_alias_map: HashSet<String> = HashSet::new();
78 let mut remove_alias_map: Vec<(String, usize)> = Vec::new();
79 for &idx in present {
80 let p = &arena[idx];
81 if p.is_alias() {
82 let key = format!("{}::{}", p.name, p.version);
83 present_alias_map.insert(key.clone());
84 if let Some(slot) = remove_alias_map.iter_mut().find(|(k, _)| *k == key) {
85 slot.1 = idx;
86 } else {
87 remove_alias_map.push((key, idx));
88 }
89 } else {
90 present_package_map.insert(p.name.clone(), idx);
91 if let Some(slot) = remove_map.iter_mut().find(|(k, _)| *k == p.name) {
92 slot.1 = idx;
93 } else {
94 remove_map.push((p.name.clone(), idx));
95 }
96 }
97 }
98
99 let mut roots: Vec<usize> = result_map.clone();
101 for &idx in &result_map {
102 if !roots.contains(&idx) {
103 continue;
104 }
105 for link in arena[idx].requires.iter() {
106 for require in providers(&link.target) {
107 if require != idx {
108 roots.retain(|&r| r != require);
109 }
110 }
111 }
112 }
113
114 let mut stack: Vec<usize> = roots;
115 let mut visited: HashSet<usize> = HashSet::new();
116 let mut processed: HashSet<usize> = HashSet::new();
117 while let Some(idx) = stack.pop() {
118 if processed.contains(&idx) {
119 continue;
120 }
121 if !visited.contains(&idx) {
122 visited.insert(idx);
123 stack.push(idx);
124 let p = &arena[idx];
125 if let Some(base) = p.alias_of {
126 stack.push(base);
127 } else {
128 for link in p.requires.iter() {
129 for require in providers(&link.target) {
130 stack.push(require);
131 }
132 }
133 }
134 } else {
135 processed.insert(idx);
136 let p = &arena[idx];
137 if p.is_alias() {
138 let key = format!("{}::{}", p.name, p.version);
139 if present_alias_map.contains(&key) {
140 remove_alias_map.retain(|(k, _)| *k != key);
141 } else {
142 operations.push(Operation::MarkAliasInstalled(idx));
143 }
144 } else if let Some(&source) = present_package_map.get(&p.name) {
145 let s = &arena[source];
146 let (pa, pr) = abandoned(p);
147 let (sa, sr) = abandoned(s);
148 if p.version != s.version
149 || p.dist_reference() != s.dist_reference()
150 || p.source_reference() != s.source_reference()
151 || pa != sa
152 || pr != sr
153 {
154 operations.push(Operation::Update(source, idx));
155 }
156 remove_map.retain(|(k, _)| *k != p.name);
157 } else {
158 operations.push(Operation::Install(idx));
159 remove_map.retain(|(k, _)| *k != p.name);
160 }
161 }
162 }
163 for (_, idx) in &remove_map {
164 operations.insert(0, Operation::Uninstall(*idx));
165 }
166 for (_, idx) in &remove_alias_map {
167 operations.push(Operation::MarkAliasUninstalled(*idx));
168 }
169 let operations = Self::move_plugins_to_front(arena, operations);
170 let operations = Self::move_uninstalls_to_front(operations);
171 Transaction { operations }
172 }
173
174 fn op_package(op: &Operation) -> Option<usize> {
175 match op {
176 Operation::Install(p) => Some(*p),
177 Operation::Update(_, t) => Some(*t),
178 _ => None,
179 }
180 }
181
182 fn move_plugins_to_front(arena: &[Package], mut operations: Vec<Operation>) -> Vec<Operation> {
184 let mut dl_no_deps: Vec<Operation> = Vec::new();
185 let mut dl_with_deps: Vec<Operation> = Vec::new();
186 let mut dl_requires: Vec<String> = Vec::new();
187 let mut plugins_no_deps: Vec<Operation> = Vec::new();
188 let mut plugins_with_deps: Vec<Operation> = Vec::new();
189 let mut plugin_requires: Vec<String> = Vec::new();
190 let mut removed: Vec<usize> = Vec::new();
191 for idx in (0..operations.len()).rev() {
192 let op = &operations[idx];
193 let Some(pkg) = Self::op_package(op) else {
194 continue;
195 };
196 let package = &arena[pkg];
197 let is_dl_plugin = package.package_type == "composer-plugin"
198 && package
199 .raw
200 .get("extra")
201 .and_then(|e| e.get("plugin-modifies-downloads"))
202 == Some(&Value::Bool(true));
203 let names = package.names(true);
204 let non_platform_requires = || -> Vec<String> {
205 package
206 .requires
207 .iter()
208 .filter_map(|l| l.key.clone())
209 .filter(|req| !is_platform_package(req))
210 .collect()
211 };
212 if is_dl_plugin || names.iter().any(|n| dl_requires.contains(n)) {
213 let requires = non_platform_requires();
214 if is_dl_plugin && requires.is_empty() {
215 dl_no_deps.insert(0, op.clone());
216 } else {
217 dl_requires.extend(requires);
218 dl_with_deps.insert(0, op.clone());
219 }
220 removed.push(idx);
221 continue;
222 }
223 let is_plugin = package.package_type == "composer-plugin"
224 || package.package_type == "composer-installer";
225 if is_plugin || names.iter().any(|n| plugin_requires.contains(n)) {
226 let requires = non_platform_requires();
227 if is_plugin && requires.is_empty() {
228 plugins_no_deps.insert(0, op.clone());
229 } else {
230 plugin_requires.extend(requires);
231 plugins_with_deps.insert(0, op.clone());
232 }
233 removed.push(idx);
234 }
235 }
236 removed.sort_unstable_by(|a, b| b.cmp(a));
237 for idx in removed {
238 operations.remove(idx);
239 }
240 let mut out = dl_no_deps;
241 out.extend(dl_with_deps);
242 out.extend(plugins_no_deps);
243 out.extend(plugins_with_deps);
244 out.extend(operations);
245 out
246 }
247
248 fn move_uninstalls_to_front(operations: Vec<Operation>) -> Vec<Operation> {
250 let (uninst, rest): (Vec<Operation>, Vec<Operation>) =
251 operations.into_iter().partition(|op| {
252 matches!(
253 op,
254 Operation::Uninstall(_) | Operation::MarkAliasUninstalled(_)
255 )
256 });
257 let mut out = uninst;
258 out.extend(rest);
259 out
260 }
261}
262
263pub struct LockTransaction {
265 pub transaction: Transaction,
266 pub all: Vec<usize>,
268 pub non_dev: Vec<usize>,
269 pub dev: Vec<usize>,
270 pub present: Vec<usize>,
272}
273
274impl LockTransaction {
275 pub fn empty() -> LockTransaction {
276 LockTransaction {
277 transaction: Transaction {
278 operations: Vec::new(),
279 },
280 all: Vec::new(),
281 non_dev: Vec::new(),
282 dev: Vec::new(),
283 present: Vec::new(),
284 }
285 }
286
287 pub fn new(
288 pool: &Pool,
289 arena: &[Package],
290 request: &Request,
291 decisions: &Decisions,
292 ) -> LockTransaction {
293 let mut present: Vec<usize> = Vec::new();
295 for idx in request
296 .locked_repository
297 .iter()
298 .flatten()
299 .copied()
300 .chain(request.fixed_packages.iter().copied())
301 {
302 if !present.contains(&idx) {
303 present.push(idx);
304 }
305 }
306 let unlockable: HashSet<usize> = request.fixed_packages.iter().copied().collect();
307 let mut all = Vec::new();
309 let mut non_dev = Vec::new();
310 for i in (0..decisions.len()).rev() {
311 let literal = decisions.at_offset(i).literal;
312 if literal > 0 {
313 let idx = pool.literal_to_package(literal);
314 all.push(idx);
315 if !unlockable.contains(&idx) {
316 non_dev.push(idx);
317 }
318 }
319 }
320 let transaction = Transaction::new(arena, &present, &all);
321 LockTransaction {
322 transaction,
323 all,
324 non_dev,
325 dev: Vec::new(),
326 present,
327 }
328 }
329
330 pub fn set_non_dev_packages(&mut self, arena: &[Package], extraction: &LockTransaction) {
332 let packages = extraction.new_lock_packages(arena, false);
333 self.dev = std::mem::take(&mut self.non_dev);
334 self.non_dev = Vec::new();
335 for pkg in packages {
336 let name = &arena[pkg].name;
337 let mut i = 0;
338 while i < self.dev.len() {
339 if arena[self.dev[i]].name == *name {
340 let moved = self.dev.remove(i);
341 self.non_dev.push(moved);
342 } else {
343 i += 1;
344 }
345 }
346 }
347 }
348
349 pub fn new_lock_packages(&self, arena: &[Package], dev_mode: bool) -> Vec<usize> {
351 let source = if dev_mode { &self.dev } else { &self.non_dev };
352 source
353 .iter()
354 .copied()
355 .filter(|&idx| !arena[idx].is_alias())
356 .collect()
357 }
358
359 pub fn aliases(
361 &self,
362 arena: &[Package],
363 aliases: &[crate::root::RootAlias],
364 ) -> Vec<crate::root::RootAlias> {
365 let mut remaining: Vec<Option<&crate::root::RootAlias>> =
366 aliases.iter().map(Some).collect();
367 let mut used = Vec::new();
368 for &idx in &self.all {
369 if !arena[idx].is_alias() {
370 continue;
371 }
372 for slot in remaining.iter_mut() {
373 if let Some(a) = slot {
374 if a.package == arena[idx].name {
375 used.push((*a).clone());
376 *slot = None;
377 }
378 }
379 }
380 }
381 used.sort_by(|a, b| a.package.as_bytes().cmp(b.package.as_bytes()));
382 used
383 }
384}