Skip to main content

vivacity_resolver/
transaction.rs

1//! Port of `Transaction` and `LockTransaction`: the packages kept by the
2//! decisions, the operations relative to the present lock, the packages to
3//! write into the lock.
4
5use 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    /// (initial, target).
17    Update(usize, usize),
18    Uninstall(usize),
19    MarkAliasInstalled(usize),
20    MarkAliasUninstalled(usize),
21}
22
23/// `CompletePackage::isAbandoned` / `getReplacementPackage`.
24fn abandoned(p: &Package) -> (bool, Option<String>) {
25    match p.raw.get("abandoned") {
26        None | Some(Value::Null) => (false, None),
27        // `(bool) "0"` is false, but `getReplacementPackage()` returns "0".
28        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
36/// `Transaction::$packageSort`.
37fn 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    /// `__construct($presentPackages, $resultPackages)` (arena indices).
57    pub fn new(arena: &[Package], present: &[usize], result: &[usize]) -> Transaction {
58        // setResultPackageMaps
59        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        // calculateOperations
74        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        // getRootPackages
100        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    /// `movePluginsToFront`.
183    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    /// `moveUninstallsToFront`.
249    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
263/// `Composer\DependencyResolver\LockTransaction`.
264pub struct LockTransaction {
265    pub transaction: Transaction,
266    /// `resultPackages['all'|'non-dev'|'dev']` (arena indices).
267    pub all: Vec<usize>,
268    pub non_dev: Vec<usize>,
269    pub dev: Vec<usize>,
270    /// `presentMap` (arena indices, lock order then fixed packages).
271    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        // getPresentMap: lock then fixed packages (no duplicates).
294        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        // setResultPackages: `foreach ($decisions ...)` from last to first.
308        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    /// `setNonDevPackages($extractionResult)`.
331    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    /// `getNewLockPackages($devMode)` without `updateMirrors`.
350    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    /// `getAliases($aliases)`: the root aliases in use, sorted by name.
360    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}