vivacity_resolver/
version_selector.rs1use std::collections::{BTreeMap, BTreeSet};
11
12use pcre2::bytes::Regex;
13use serde_json::{Map, Value};
14
15use crate::constraint::{Constraint, Op};
16use crate::package::Package;
17use crate::phpver::version_compare;
18use crate::platform::is_platform_package;
19use crate::platform_filter::PlatformRequirementFilter;
20use crate::version::{stability_rank, DEFAULT_BRANCH_ALIAS};
21
22pub fn platform_constraints(
25 platform: &[usize],
26 arena: &[Package],
27) -> BTreeMap<String, Vec<Constraint>> {
28 let mut out: BTreeMap<String, Vec<Constraint>> = BTreeMap::new();
29 for &idx in platform {
30 let p = &arena[idx];
31 out.entry(p.name.clone())
32 .or_default()
33 .push(Constraint::new(Op::Eq, &p.version));
34 }
35 out
36}
37
38#[derive(Debug, Clone, PartialEq, Eq)]
41pub struct Warning {
42 pub message: String,
43 pub verbose: bool,
44}
45
46pub fn find_best_candidate(
50 candidates: &[usize],
51 arena: &[Package],
52 preferred_stability: &str,
53 filter: &PlatformRequirementFilter,
54 platform: &BTreeMap<String, Vec<Constraint>>,
55 warnings: &mut Vec<Warning>,
56) -> Option<usize> {
57 let min_priority = stability_rank(preferred_stability);
58 let mut sorted: Vec<usize> = candidates.to_vec();
59 sorted.sort_by(|&a, &b| {
63 let (pa, pb) = (&arena[a], &arena[b]);
64 let (ra, rb) = (stability_rank(pa.stability), stability_rank(pb.stability));
65 if min_priority < ra && rb < ra {
66 return std::cmp::Ordering::Greater;
67 }
68 if min_priority < ra && ra < rb {
69 return std::cmp::Ordering::Less;
70 }
71 if min_priority >= ra && min_priority < rb {
72 return std::cmp::Ordering::Less;
73 }
74 version_compare(&pb.version, &pa.version)
75 });
76
77 let mut chosen = None;
78 if !platform.is_empty() && !matches!(filter, PlatformRequirementFilter::IgnoreAll) {
79 let mut already_warned: BTreeSet<String> = BTreeSet::new();
80 let mut already_seen: BTreeSet<String> = BTreeSet::new();
81 'candidates: for &idx in &sorted {
82 let pkg = &arena[idx];
83 let mut skip = false;
84 'links: for link in pkg.requires.iter() {
85 let name = link.key.as_deref().unwrap_or(&link.target);
86 if !is_platform_package(name) || filter.is_ignored(name) {
87 continue;
88 }
89 let reason = if let Some(provided) = platform.get(name) {
90 for provided_constraint in provided {
91 if link.constraint.matches(provided_constraint) {
92 continue 'links;
93 }
94 if filter.is_upper_bound_ignored(name)
95 && filter
96 .filter_constraint(name, &link.constraint, true)
97 .matches(provided_constraint)
98 {
99 continue 'links;
100 }
101 }
102 "is not satisfied by your platform"
103 } else {
104 "is missing from your platform"
105 };
106 let is_latest = !already_seen.contains(&pkg.name);
107 already_seen.insert(pkg.name.clone());
108 let key = format!("{}/{}", pkg.name, link.target);
109 let first = !already_warned.contains(&key);
110 already_warned.insert(key);
111 let latest = if is_latest { "'s latest version" } else { "" };
112 warnings.push(Warning {
113 message: format!(
114 "Cannot use {}{latest} {} as it {} {} {} which {reason}.",
115 pkg.pretty_name,
116 pkg.pretty_version,
117 link.kind.description(),
118 link.target,
119 link.pretty_constraint
120 ),
121 verbose: !first,
122 });
123 skip = true;
124 }
125 if skip {
126 continue 'candidates;
127 }
128 chosen = Some(idx);
129 break;
130 }
131 } else {
132 chosen = sorted.first().copied();
133 }
134
135 let idx = chosen?;
136 let pkg = &arena[idx];
137 if let Some(base) = pkg.alias_of {
138 if pkg.version == DEFAULT_BRANCH_ALIAS {
139 return Some(base);
140 }
141 }
142 Some(idx)
143}
144
145pub fn find_recommended_require_version(
148 pkg: &Package,
149 arena: &[Package],
150 php_version: &str,
151) -> String {
152 if pkg.name.starts_with("ext-") {
153 let ext_version: Vec<&str> = pkg.version.split('.').take(3).collect();
154 if php_version == ext_version.join(".") {
155 return "*".to_owned();
156 }
157 }
158 if !pkg.is_dev() {
159 return transform_version(&pkg.version, &pkg.pretty_version, pkg.stability);
160 }
161 let base = pkg.alias_of.map(|b| &arena[b]).unwrap_or(pkg);
165 let mut dumped = Map::new();
166 dumped.insert("version".into(), Value::String(pkg.pretty_version.clone()));
167 if let Some(extra) = base.raw.get("extra") {
168 dumped.insert("extra".into(), extra.clone());
169 }
170 if base.is_default_branch {
171 dumped.insert("default-branch".into(), Value::Bool(true));
172 }
173 if let Some(extra) = crate::loader::branch_alias(&dumped) {
174 if extra != DEFAULT_BRANCH_ALIAS {
175 static RE: std::sync::OnceLock<Regex> = std::sync::OnceLock::new();
176 let re = RE.get_or_init(|| {
177 Regex::new(r"^(\d+\.\d+\.\d+)(\.9999999)-dev$").unwrap_or_else(|e| panic!("{e}"))
178 });
179 if let Ok(Some(m)) = re.captures(extra.as_bytes()) {
180 let head = m
181 .get(1)
182 .and_then(|g| std::str::from_utf8(g.as_bytes()).ok())
183 .unwrap_or("");
184 let replaced = format!("{head}.0").replace(".9999999", ".0");
185 return transform_version(&replaced, &replaced, "dev");
186 }
187 }
188 }
189 pkg.pretty_version.clone()
190}
191
192fn transform_version(version: &str, pretty_version: &str, stability: &str) -> String {
195 let parts: Vec<&str> = version.split('.').collect();
196 let fourth_ok = parts.len() == 4 && parts[3].starts_with(|c: char| c.is_ascii_digit());
197 if !fourth_ok {
198 return pretty_version.to_owned();
199 }
200 let kept: &[&str] = if parts[0] == "0" {
201 &parts[..3]
202 } else {
203 &parts[..2]
204 };
205 let mut version = kept.join(".");
206 if stability != "stable" {
207 version.push('@');
208 version.push_str(stability);
209 }
210 format!("^{version}")
211}
212
213#[cfg(test)]
214mod tests {
215 use super::*;
216
217 #[test]
218 fn transform_versions() {
219 assert_eq!(transform_version("1.2.3.0", "1.2.3", "stable"), "^1.2");
220 assert_eq!(transform_version("0.2.3.0", "0.2.3", "stable"), "^0.2.3");
221 assert_eq!(
222 transform_version("1.0.0.0-RC1", "1.0.0-RC1", "RC"),
223 "^1.0@RC"
224 );
225 assert_eq!(
226 transform_version("2.0.0.0-beta2", "v2.0.0-beta2", "beta"),
227 "^2.0@beta"
228 );
229 assert_eq!(transform_version("dev-main", "dev-main", "dev"), "dev-main");
230 assert_eq!(
231 transform_version("20240101", "20240101", "stable"),
232 "20240101"
233 );
234 }
235}