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 extract_stability_flags(
305 requires: &[(String, String)],
306 minimum_stability: &str,
307 flags: &mut BTreeMap<String, i32>,
308) {
309 static AT: OnceLock<Regex> = OnceLock::new();
310 static AS: OnceLock<Regex> = OnceLock::new();
311 static PLAIN: OnceLock<Regex> = OnceLock::new();
312 let at = regex(&AT, &format!("^[^@]*?@({STABILITIES_REGEX})$"), true);
313 let as_re = regex(&AS, r"^([^,\s@]+) as .+$", false);
314 let plain = regex(&PLAIN, r"^[^,\s@]+$", false);
315 let minimum = stability_rank(minimum_stability);
316 for (req_name, req) in requires {
317 let constraints = split_constraints(req);
318 let mut matched = false;
319 for c in &constraints {
320 if let Ok(Some(caps)) = at.captures(c.as_bytes()) {
321 let name = req_name.to_lowercase();
322 let Ok(stab) = normalize_stability(group(&caps, 1)) else {
323 continue;
324 };
325 let rank = stability_rank(&stab);
326 if flags.get(&name).is_some_and(|f| *f > rank) {
327 continue;
328 }
329 flags.insert(name, rank);
330 matched = true;
331 }
332 }
333 if matched {
334 continue;
335 }
336 for c in &constraints {
337 let stripped = match as_re.captures(c.as_bytes()) {
338 Ok(Some(caps)) => group(&caps, 1).to_owned(),
339 _ => c.clone(),
340 };
341 if plain.is_match(stripped.as_bytes()).unwrap_or(false) {
342 let stability = version::parse_stability(&stripped);
343 if stability != "stable" {
344 let name = req_name.to_lowercase();
345 let rank = stability_rank(stability);
346 if flags.get(&name).is_some_and(|f| *f > rank) || minimum > rank {
347 continue;
348 }
349 flags.insert(name, rank);
350 }
351 }
352 }
353 }
354}
355
356fn extract_references(requires: &[(String, String)], references: &mut BTreeMap<String, String>) {
358 static AS: OnceLock<Regex> = OnceLock::new();
359 static REF: OnceLock<Regex> = OnceLock::new();
360 let as_re = regex(&AS, r"^([^,\s@]+) as .+$", false);
361 let re = regex(&REF, r"^[^,\s@]+?#([a-f0-9]+)$", false);
362 for (name, req) in requires {
363 let stripped = match as_re.captures(req.as_bytes()) {
364 Ok(Some(caps)) => group(&caps, 1).to_owned(),
365 _ => req.clone(),
366 };
367 if let Ok(Some(caps)) = re.captures(stripped.as_bytes()) {
368 if version::parse_stability(&stripped) == "dev" {
369 references.insert(name.to_lowercase(), group(&caps, 1).to_owned());
370 }
371 }
372 }
373}
374
375pub fn root_constraint(pretty: &str) -> Result<crate::constraint::Constraint, VersionError> {
377 Ok(parse_constraints(pretty)?.constraint)
378}
379
380#[cfg(test)]
381mod tests {
382 use super::*;
383 use serde_json::json;
384
385 #[test]
386 fn extracts_flags_aliases_references() {
387 let reqs = vec![
388 ("a/b".to_owned(), "^1.0@beta".to_owned()),
389 ("c/d".to_owned(), "dev-main as 1.0.x-dev".to_owned()),
390 ("e/f".to_owned(), "dev-main#abcdef".to_owned()),
391 ("g/h".to_owned(), "1.x-dev || ^2.0".to_owned()),
392 ("i/j".to_owned(), "^1.0".to_owned()),
393 ];
394 let mut flags = BTreeMap::new();
395 extract_stability_flags(&reqs, "stable", &mut flags);
396 assert_eq!(flags.get("a/b"), Some(&10));
397 assert_eq!(flags.get("c/d"), Some(&20));
398 assert_eq!(flags.get("e/f"), Some(&20));
399 assert_eq!(flags.get("g/h"), Some(&20));
400 assert_eq!(flags.get("i/j"), None);
401 let mut aliases = Vec::new();
402 extract_aliases(&reqs, &mut aliases).unwrap();
403 assert_eq!(aliases.len(), 1);
404 assert_eq!(aliases[0].alias_normalized, "1.0.9999999.9999999-dev");
405 let mut refs = BTreeMap::new();
406 extract_references(&reqs, &mut refs);
407 assert_eq!(refs.get("e/f").map(String::as_str), Some("abcdef"));
408 }
409
410 #[test]
411 fn loads_root_without_git() {
412 let m = json!({"name": "acme/app", "require": {"php": "^8.1", "monolog/monolog": "^3"}, "minimum-stability": "RC", "prefer-stable": true});
413 let r = RootPackage::load(&m, Path::new("/nonexistent-vivacity")).unwrap();
414 assert_eq!(r.package.pretty_version, DEFAULT_PRETTY_VERSION);
415 assert_eq!(r.package.version, "1.0.0.0");
416 assert_eq!(r.minimum_stability, "RC");
417 assert!(r.prefer_stable);
418 assert_eq!(r.package.requires.len(), 2);
419 }
420}