1use crate::constraint::parse_constraints;
9use crate::loader;
10use crate::package::{Links, Origin, Package};
11use crate::version::{
12 self, group, normalize, regex, stability_rank, VersionError, STABILITIES_REGEX,
13};
14use pcre2::bytes::Regex;
15use serde_json::{Map, Value};
16use std::collections::BTreeMap;
17use std::path::Path;
18use std::sync::OnceLock;
19
20pub const DEFAULT_PRETTY_VERSION: &str = "1.0.0+no-version-set";
21
22#[derive(Debug, Clone, PartialEq, Eq)]
24pub struct RootAlias {
25 pub package: String,
26 pub version: String,
27 pub alias: String,
28 pub alias_normalized: String,
29}
30
31#[derive(Debug, Clone)]
32pub struct RootPackage {
33 pub package: Package,
34 pub branch_alias: Option<(String, String)>,
36 pub minimum_stability: String,
37 pub prefer_stable: bool,
38 pub stability_flags: BTreeMap<String, i32>,
40 pub aliases: Vec<RootAlias>,
41 pub references: BTreeMap<String, String>,
43 pub platform_overrides: Map<String, Value>,
45 pub manifest: Value,
46}
47
48#[derive(Debug, thiserror::Error)]
49#[error("{0}")]
50pub struct RootError(pub String);
51
52#[derive(Debug, Clone, Default)]
55pub struct RootPatch {
56 pub requirements: Vec<(String, String)>,
58 pub dev: bool,
59 pub removals: Vec<(bool, String)>,
61}
62
63impl RootPackage {
64 pub fn load(manifest: &Value, project_dir: &Path) -> Result<RootPackage, RootError> {
67 let mut config = manifest
68 .as_object()
69 .cloned()
70 .ok_or_else(|| RootError("composer.json is not an object".into()))?;
71 if !config.contains_key("name") {
72 config.insert("name".into(), Value::String("__root__".into()));
73 }
74 let mut auto_versioned = false;
75 if !config.contains_key("version") {
76 let rv = vivacity_core::root_version::detect(manifest, project_dir);
77 if rv.pretty_version == vivacity_core::root_version::DEFAULT_PRETTY_VERSION {
78 config.insert("version".into(), Value::String("1.0.0".into()));
79 auto_versioned = true;
80 } else {
81 config.insert("version".into(), Value::String(rv.pretty_version.clone()));
82 config.insert(
83 "version_normalized".into(),
84 Value::String(rv.version.clone()),
85 );
86 if let Some(commit) = rv.reference {
87 let r = serde_json::json!({"type": "", "url": "", "reference": commit});
88 config.insert("source".into(), r.clone());
89 config.insert("dist".into(), r);
90 }
91 }
92 }
93 let value = Value::Object(config.clone());
94 let (mut package, alias) =
95 loader::load(&value, Origin::Root, false).map_err(|e| RootError(e.0))?;
96 if auto_versioned {
97 package.pretty_version = DEFAULT_PRETTY_VERSION.to_owned();
98 }
99 let minimum_stability = match config.get("minimum-stability").and_then(Value::as_str) {
100 Some(s) => normalize_stability(s)?,
101 None => "stable".to_owned(),
102 };
103 let mut aliases = Vec::new();
104 let mut stability_flags = BTreeMap::new();
105 let mut references = BTreeMap::new();
106 for links in [&package.requires, &package.dev_requires] {
107 let map: Vec<(String, String)> = links
108 .iter()
109 .map(|l| {
112 let pretty = if l.pretty_constraint == "self.version" {
113 if auto_versioned {
116 "1.0.0".to_owned()
117 } else {
118 package.pretty_version.clone()
119 }
120 } else {
121 l.pretty_constraint.clone()
122 };
123 (l.target.clone(), pretty)
124 })
125 .collect();
126 extract_aliases(&map, &mut aliases)?;
127 extract_stability_flags(&map, &minimum_stability, &mut stability_flags);
128 extract_references(&map, &mut references);
129 if map.iter().any(|(n, _)| *n == package.name) {
130 return Err(RootError(format!(
131 "Root package '{}' cannot require itself in its composer.json\nDid you accidentally name your root package after an external package?",
132 package.pretty_name
133 )));
134 }
135 }
136 let prefer_stable = config
137 .get("prefer-stable")
138 .map(|v| match v {
139 Value::Bool(b) => *b,
140 Value::Number(n) => n.as_f64() != Some(0.0),
141 Value::String(s) => !(s.is_empty() || s == "0"),
142 Value::Null => false,
143 _ => true,
144 })
145 .unwrap_or(false);
146 let platform_overrides = config
147 .get("config")
148 .and_then(|c| c.get("platform"))
149 .and_then(Value::as_object)
150 .cloned()
151 .unwrap_or_default();
152 Ok(RootPackage {
153 package,
154 branch_alias: alias,
155 minimum_stability,
156 prefer_stable,
157 stability_flags,
158 aliases,
159 references,
160 platform_overrides,
161 manifest: manifest.clone(),
162 })
163 }
164
165 pub fn apply_patch(&mut self, patch: &RootPatch) -> Result<(), RootError> {
173 if !patch.requirements.is_empty() {
174 let map: Map<String, Value> = patch
175 .requirements
176 .iter()
177 .map(|(n, c)| (n.clone(), Value::String(c.clone())))
178 .collect();
179 let kind = if patch.dev {
180 crate::package::LinkType::DevRequire
181 } else {
182 crate::package::LinkType::Require
183 };
184 let new_links = loader::parse_links(
185 &self.package.name,
186 &self.package.pretty_version,
187 kind,
188 Some(&Value::Object(map)),
189 false,
190 )
191 .map_err(|e| RootError(e.0))?;
192 let (own_links, other_links) = if kind == crate::package::LinkType::DevRequire {
193 (&mut self.package.dev_requires, &mut self.package.requires)
194 } else {
195 (&mut self.package.requires, &mut self.package.dev_requires)
196 };
197 for l in new_links.iter() {
198 own_links.insert(l.clone());
199 }
200 for (name, _) in &patch.requirements {
201 other_links.remove(name);
202 }
203 let pairs: Vec<(String, String)> = patch.requirements.clone();
204 extract_references(&pairs, &mut self.references);
205 extract_stability_flags(&pairs, &self.minimum_stability, &mut self.stability_flags);
206 }
207 for (dev, name) in &patch.removals {
210 if *dev {
211 self.package.dev_requires.remove(name);
212 } else {
213 self.package.requires.remove(name);
214 }
215 }
216 Ok(())
217 }
218
219 pub fn all_requires(&self) -> Links {
220 let mut out = self.package.requires.clone();
221 for l in self.package.dev_requires.iter() {
222 out.insert(l.clone());
223 }
224 out
225 }
226}
227
228pub fn normalize_stability(s: &str) -> Result<String, RootError> {
230 let lower = s.to_lowercase();
231 match lower.as_str() {
232 "stable" | "beta" | "alpha" | "dev" => Ok(lower),
233 "rc" => Ok("RC".to_owned()),
234 _ => Err(RootError(format!(
235 "Invalid stability string \"{s}\", expected one of stable, RC, beta, alpha or dev"
236 ))),
237 }
238}
239
240fn extract_aliases(
242 requires: &[(String, String)],
243 aliases: &mut Vec<RootAlias>,
244) -> Result<(), RootError> {
245 static RE: OnceLock<Regex> = OnceLock::new();
246 let re = regex(
247 &RE,
248 r"(?:^|\| *|, *)([^,\s#|]+)(?:#[^ ]+)? +as +([^,\s|]+)(?:$| *\|| *,)",
249 false,
250 );
251 for (name, req) in requires {
252 if let Ok(Some(caps)) = re.captures(req.as_bytes()) {
253 let v = group(&caps, 1);
254 let a = group(&caps, 2);
255 aliases.push(RootAlias {
256 package: name.to_lowercase(),
257 version: normalize(v, Some(req)).map_err(|e: VersionError| RootError(e.0))?,
258 alias: a.to_owned(),
259 alias_normalized: normalize(a, Some(req))
260 .map_err(|e: VersionError| RootError(e.0))?,
261 });
262 } else if req.contains(" as ") {
263 return Err(RootError(format!(
264 "Invalid alias definition in \"{name}\": \"{req}\". Aliases should be in the form \"exact-version as other-exact-version\"."
265 )));
266 }
267 }
268 Ok(())
269}
270
271fn split_constraints(req: &str) -> Vec<String> {
273 static OR: OnceLock<Regex> = OnceLock::new();
274 static AND: OnceLock<Regex> = OnceLock::new();
275 let or = regex(&OR, r"\s*\|\|?\s*", false);
276 let and = regex(
277 &AND,
278 r"(?<!^|as|[=>< ,]) *(?<!-)[, ](?!-) *(?!,|as|$)",
279 false,
280 );
281 let mut out = Vec::new();
282 for part in split(or, req.trim()) {
283 out.extend(split(and, &part));
284 }
285 out
286}
287
288fn split(re: &Regex, subject: &str) -> Vec<String> {
289 let bytes = subject.as_bytes();
290 let mut out = Vec::new();
291 let mut last = 0;
292 for m in re.find_iter(bytes).flatten() {
293 if m.start() == m.end() && m.start() == last && last == bytes.len() {
294 break;
295 }
296 out.push(subject[last..m.start()].to_owned());
297 last = m.end();
298 }
299 out.push(subject[last..].to_owned());
300 out
301}
302
303pub fn merge_plugin_stability_flags(
310 flags: &mut BTreeMap<String, i32>,
311 minimum_stability: &str,
312 requires: &[(String, String)],
313) {
314 static AT: OnceLock<Regex> = OnceLock::new();
315 static AS: OnceLock<Regex> = OnceLock::new();
316 let at = regex(&AT, &format!("^[^@]*?@({STABILITIES_REGEX})$"), true);
317 let as_re = regex(&AS, r"^([^,\s@]+) as .+$", false);
318 let minimum = stability_rank(minimum_stability);
319 for (req_name, pretty) in requires {
320 let name = req_name.to_lowercase();
321 let mut explicit: Option<i32> = None;
322 for c in split_constraints(pretty) {
323 if let Ok(Some(caps)) = at.captures(c.as_bytes()) {
324 let Ok(stab) = normalize_stability(group(&caps, 1)) else {
325 continue;
326 };
327 let rank = stability_rank(&stab);
328 explicit = Some(explicit.map_or(rank, |e| e.max(rank)));
329 }
330 }
331 let stability = match explicit {
332 Some(e) => Some(e),
333 None => {
334 let v = match as_re.captures(pretty.as_bytes()) {
336 Ok(Some(caps)) => group(&caps, 1).to_owned(),
337 _ => pretty.clone(),
338 };
339 let rank = stability_rank(version::parse_stability(&v));
340 if rank == stability_rank("stable") || minimum > rank {
341 None
342 } else {
343 Some(rank)
344 }
345 }
346 };
347 if let Some(st) = stability {
348 let current = flags.get(&name).copied();
349 flags.insert(name, current.map_or(st, |c| c.max(st)));
350 }
351 }
352}
353
354pub fn extract_stability_flags(
356 requires: &[(String, String)],
357 minimum_stability: &str,
358 flags: &mut BTreeMap<String, i32>,
359) {
360 static AT: OnceLock<Regex> = OnceLock::new();
361 static AS: OnceLock<Regex> = OnceLock::new();
362 static PLAIN: OnceLock<Regex> = OnceLock::new();
363 let at = regex(&AT, &format!("^[^@]*?@({STABILITIES_REGEX})$"), true);
364 let as_re = regex(&AS, r"^([^,\s@]+) as .+$", false);
365 let plain = regex(&PLAIN, r"^[^,\s@]+$", false);
366 let minimum = stability_rank(minimum_stability);
367 for (req_name, req) in requires {
368 let constraints = split_constraints(req);
369 let mut matched = false;
370 for c in &constraints {
371 if let Ok(Some(caps)) = at.captures(c.as_bytes()) {
372 let name = req_name.to_lowercase();
373 let Ok(stab) = normalize_stability(group(&caps, 1)) else {
374 continue;
375 };
376 let rank = stability_rank(&stab);
377 if flags.get(&name).is_some_and(|f| *f > rank) {
378 continue;
379 }
380 flags.insert(name, rank);
381 matched = true;
382 }
383 }
384 if matched {
385 continue;
386 }
387 for c in &constraints {
388 let stripped = match as_re.captures(c.as_bytes()) {
389 Ok(Some(caps)) => group(&caps, 1).to_owned(),
390 _ => c.clone(),
391 };
392 if plain.is_match(stripped.as_bytes()).unwrap_or(false) {
393 let stability = version::parse_stability(&stripped);
394 if stability != "stable" {
395 let name = req_name.to_lowercase();
396 let rank = stability_rank(stability);
397 if flags.get(&name).is_some_and(|f| *f > rank) || minimum > rank {
398 continue;
399 }
400 flags.insert(name, rank);
401 }
402 }
403 }
404 }
405}
406
407fn extract_references(requires: &[(String, String)], references: &mut BTreeMap<String, String>) {
409 static AS: OnceLock<Regex> = OnceLock::new();
410 static REF: OnceLock<Regex> = OnceLock::new();
411 let as_re = regex(&AS, r"^([^,\s@]+) as .+$", false);
412 let re = regex(&REF, r"^[^,\s@]+?#([a-f0-9]+)$", false);
413 for (name, req) in requires {
414 let stripped = match as_re.captures(req.as_bytes()) {
415 Ok(Some(caps)) => group(&caps, 1).to_owned(),
416 _ => req.clone(),
417 };
418 if let Ok(Some(caps)) = re.captures(stripped.as_bytes()) {
419 if version::parse_stability(&stripped) == "dev" {
420 references.insert(name.to_lowercase(), group(&caps, 1).to_owned());
421 }
422 }
423 }
424}
425
426pub fn root_constraint(pretty: &str) -> Result<crate::constraint::Constraint, VersionError> {
428 Ok(parse_constraints(pretty)?.constraint)
429}
430
431#[cfg(test)]
432mod tests {
433 #[test]
434 fn merge_plugin_flags_follow_the_plugin() {
435 use super::merge_plugin_stability_flags as flags_of;
436 use std::collections::BTreeMap;
437 let req = |pairs: &[(&str, &str)]| -> Vec<(String, String)> {
438 pairs
439 .iter()
440 .map(|(n, c)| ((*n).to_owned(), (*c).to_owned()))
441 .collect()
442 };
443 let mut f = BTreeMap::new();
446 flags_of(
447 &mut f,
448 "stable",
449 &req(&[("Acme/Lib", "^1.0@beta || ^2.0@RC")]),
450 );
451 assert_eq!(f.get("acme/lib"), Some(&10));
452 let mut f = BTreeMap::new();
456 flags_of(
457 &mut f,
458 "stable",
459 &req(&[("a/b", "dev-main || ^1.0"), ("c/d", "^1.0")]),
460 );
461 assert_eq!(f.get("a/b"), Some(&20));
462 assert!(!f.contains_key("c/d"));
463 let mut f = BTreeMap::new();
464 flags_of(&mut f, "dev", &req(&[("a/b", "1.0.0-beta")]));
465 assert!(f.is_empty(), "beta is more stable than the dev minimum");
466 let mut f: BTreeMap<String, i32> = [("a/b".to_owned(), 20)].into();
469 flags_of(
470 &mut f,
471 "stable",
472 &req(&[("a/b", "^1.0@beta"), ("a/b", "^1.0")]),
473 );
474 assert_eq!(f.get("a/b"), Some(&20));
475 let mut f = BTreeMap::new();
477 flags_of(&mut f, "stable", &req(&[("a/b", "dev-main as 1.0.0")]));
478 assert_eq!(f.get("a/b"), Some(&20));
479 }
480
481 use super::*;
482 use serde_json::json;
483
484 #[test]
485 fn extracts_flags_aliases_references() {
486 let reqs = vec![
487 ("a/b".to_owned(), "^1.0@beta".to_owned()),
488 ("c/d".to_owned(), "dev-main as 1.0.x-dev".to_owned()),
489 ("e/f".to_owned(), "dev-main#abcdef".to_owned()),
490 ("g/h".to_owned(), "1.x-dev || ^2.0".to_owned()),
491 ("i/j".to_owned(), "^1.0".to_owned()),
492 ];
493 let mut flags = BTreeMap::new();
494 extract_stability_flags(&reqs, "stable", &mut flags);
495 assert_eq!(flags.get("a/b"), Some(&10));
496 assert_eq!(flags.get("c/d"), Some(&20));
497 assert_eq!(flags.get("e/f"), Some(&20));
498 assert_eq!(flags.get("g/h"), Some(&20));
499 assert_eq!(flags.get("i/j"), None);
500 let mut aliases = Vec::new();
501 extract_aliases(&reqs, &mut aliases).unwrap();
502 assert_eq!(aliases.len(), 1);
503 assert_eq!(aliases[0].alias_normalized, "1.0.9999999.9999999-dev");
504 let mut refs = BTreeMap::new();
505 extract_references(&reqs, &mut refs);
506 assert_eq!(refs.get("e/f").map(String::as_str), Some("abcdef"));
507 }
508
509 #[test]
510 fn loads_root_without_git() {
511 let m = json!({"name": "acme/app", "require": {"php": "^8.1", "monolog/monolog": "^3"}, "minimum-stability": "RC", "prefer-stable": true});
512 let r = RootPackage::load(&m, Path::new("/nonexistent-vivacity")).unwrap();
513 assert_eq!(r.package.pretty_version, DEFAULT_PRETTY_VERSION);
514 assert_eq!(r.package.version, "1.0.0.0");
515 assert_eq!(r.minimum_stability, "RC");
516 assert!(r.prefer_stable);
517 assert_eq!(r.package.requires.len(), 2);
518 }
519}