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
52impl RootPackage {
53 pub fn load(manifest: &Value, project_dir: &Path) -> Result<RootPackage, RootError> {
56 let mut config = manifest
57 .as_object()
58 .cloned()
59 .ok_or_else(|| RootError("composer.json is not an object".into()))?;
60 if !config.contains_key("name") {
61 config.insert("name".into(), Value::String("__root__".into()));
62 }
63 let mut auto_versioned = false;
64 if !config.contains_key("version") {
65 let rv = vivacity_core::root_version::detect(manifest, project_dir);
66 if rv.pretty_version == vivacity_core::root_version::DEFAULT_PRETTY_VERSION {
67 config.insert("version".into(), Value::String("1.0.0".into()));
68 auto_versioned = true;
69 } else {
70 config.insert("version".into(), Value::String(rv.pretty_version.clone()));
71 config.insert(
72 "version_normalized".into(),
73 Value::String(rv.version.clone()),
74 );
75 if let Some(commit) = rv.reference {
76 let r = serde_json::json!({"type": "", "url": "", "reference": commit});
77 config.insert("source".into(), r.clone());
78 config.insert("dist".into(), r);
79 }
80 }
81 }
82 let value = Value::Object(config.clone());
83 let (mut package, alias) =
84 loader::load(&value, Origin::Root, false).map_err(|e| RootError(e.0))?;
85 if auto_versioned {
86 package.pretty_version = DEFAULT_PRETTY_VERSION.to_owned();
87 }
88 let minimum_stability = match config.get("minimum-stability").and_then(Value::as_str) {
89 Some(s) => normalize_stability(s)?,
90 None => "stable".to_owned(),
91 };
92 let mut aliases = Vec::new();
93 let mut stability_flags = BTreeMap::new();
94 let mut references = BTreeMap::new();
95 for links in [&package.requires, &package.dev_requires] {
96 let map: Vec<(String, String)> = links
97 .iter()
98 .map(|l| {
101 let pretty = if l.pretty_constraint == "self.version" {
102 if auto_versioned {
105 "1.0.0".to_owned()
106 } else {
107 package.pretty_version.clone()
108 }
109 } else {
110 l.pretty_constraint.clone()
111 };
112 (l.target.clone(), pretty)
113 })
114 .collect();
115 extract_aliases(&map, &mut aliases)?;
116 extract_stability_flags(&map, &minimum_stability, &mut stability_flags);
117 extract_references(&map, &mut references);
118 if map.iter().any(|(n, _)| *n == package.name) {
119 return Err(RootError(format!(
120 "Root package '{}' cannot require itself in its composer.json\nDid you accidentally name your root package after an external package?",
121 package.pretty_name
122 )));
123 }
124 }
125 let prefer_stable = config
126 .get("prefer-stable")
127 .map(|v| match v {
128 Value::Bool(b) => *b,
129 Value::Number(n) => n.as_f64() != Some(0.0),
130 Value::String(s) => !(s.is_empty() || s == "0"),
131 Value::Null => false,
132 _ => true,
133 })
134 .unwrap_or(false);
135 let platform_overrides = config
136 .get("config")
137 .and_then(|c| c.get("platform"))
138 .and_then(Value::as_object)
139 .cloned()
140 .unwrap_or_default();
141 Ok(RootPackage {
142 package,
143 branch_alias: alias,
144 minimum_stability,
145 prefer_stable,
146 stability_flags,
147 aliases,
148 references,
149 platform_overrides,
150 manifest: manifest.clone(),
151 })
152 }
153
154 pub fn all_requires(&self) -> Links {
157 let mut out = self.package.requires.clone();
158 for l in self.package.dev_requires.iter() {
159 out.insert(l.clone());
160 }
161 out
162 }
163}
164
165pub fn normalize_stability(s: &str) -> Result<String, RootError> {
167 let lower = s.to_lowercase();
168 match lower.as_str() {
169 "stable" | "beta" | "alpha" | "dev" => Ok(lower),
170 "rc" => Ok("RC".to_owned()),
171 _ => Err(RootError(format!(
172 "Invalid stability string \"{s}\", expected one of stable, RC, beta, alpha or dev"
173 ))),
174 }
175}
176
177fn extract_aliases(
179 requires: &[(String, String)],
180 aliases: &mut Vec<RootAlias>,
181) -> Result<(), RootError> {
182 static RE: OnceLock<Regex> = OnceLock::new();
183 let re = regex(
184 &RE,
185 r"(?:^|\| *|, *)([^,\s#|]+)(?:#[^ ]+)? +as +([^,\s|]+)(?:$| *\|| *,)",
186 false,
187 );
188 for (name, req) in requires {
189 if let Ok(Some(caps)) = re.captures(req.as_bytes()) {
190 let v = group(&caps, 1);
191 let a = group(&caps, 2);
192 aliases.push(RootAlias {
193 package: name.to_lowercase(),
194 version: normalize(v, Some(req)).map_err(|e: VersionError| RootError(e.0))?,
195 alias: a.to_owned(),
196 alias_normalized: normalize(a, Some(req))
197 .map_err(|e: VersionError| RootError(e.0))?,
198 });
199 } else if req.contains(" as ") {
200 return Err(RootError(format!(
201 "Invalid alias definition in \"{name}\": \"{req}\". Aliases should be in the form \"exact-version as other-exact-version\"."
202 )));
203 }
204 }
205 Ok(())
206}
207
208fn split_constraints(req: &str) -> Vec<String> {
210 static OR: OnceLock<Regex> = OnceLock::new();
211 static AND: OnceLock<Regex> = OnceLock::new();
212 let or = regex(&OR, r"\s*\|\|?\s*", false);
213 let and = regex(
214 &AND,
215 r"(?<!^|as|[=>< ,]) *(?<!-)[, ](?!-) *(?!,|as|$)",
216 false,
217 );
218 let mut out = Vec::new();
219 for part in split(or, req.trim()) {
220 out.extend(split(and, &part));
221 }
222 out
223}
224
225fn split(re: &Regex, subject: &str) -> Vec<String> {
226 let bytes = subject.as_bytes();
227 let mut out = Vec::new();
228 let mut last = 0;
229 for m in re.find_iter(bytes).flatten() {
230 if m.start() == m.end() && m.start() == last && last == bytes.len() {
231 break;
232 }
233 out.push(subject[last..m.start()].to_owned());
234 last = m.end();
235 }
236 out.push(subject[last..].to_owned());
237 out
238}
239
240pub fn extract_stability_flags(
242 requires: &[(String, String)],
243 minimum_stability: &str,
244 flags: &mut BTreeMap<String, i32>,
245) {
246 static AT: OnceLock<Regex> = OnceLock::new();
247 static AS: OnceLock<Regex> = OnceLock::new();
248 static PLAIN: OnceLock<Regex> = OnceLock::new();
249 let at = regex(&AT, &format!("^[^@]*?@({STABILITIES_REGEX})$"), true);
250 let as_re = regex(&AS, r"^([^,\s@]+) as .+$", false);
251 let plain = regex(&PLAIN, r"^[^,\s@]+$", false);
252 let minimum = stability_rank(minimum_stability);
253 for (req_name, req) in requires {
254 let constraints = split_constraints(req);
255 let mut matched = false;
256 for c in &constraints {
257 if let Ok(Some(caps)) = at.captures(c.as_bytes()) {
258 let name = req_name.to_lowercase();
259 let Ok(stab) = normalize_stability(group(&caps, 1)) else {
260 continue;
261 };
262 let rank = stability_rank(&stab);
263 if flags.get(&name).is_some_and(|f| *f > rank) {
264 continue;
265 }
266 flags.insert(name, rank);
267 matched = true;
268 }
269 }
270 if matched {
271 continue;
272 }
273 for c in &constraints {
274 let stripped = match as_re.captures(c.as_bytes()) {
275 Ok(Some(caps)) => group(&caps, 1).to_owned(),
276 _ => c.clone(),
277 };
278 if plain.is_match(stripped.as_bytes()).unwrap_or(false) {
279 let stability = version::parse_stability(&stripped);
280 if stability != "stable" {
281 let name = req_name.to_lowercase();
282 let rank = stability_rank(stability);
283 if flags.get(&name).is_some_and(|f| *f > rank) || minimum > rank {
284 continue;
285 }
286 flags.insert(name, rank);
287 }
288 }
289 }
290 }
291}
292
293fn extract_references(requires: &[(String, String)], references: &mut BTreeMap<String, String>) {
295 static AS: OnceLock<Regex> = OnceLock::new();
296 static REF: OnceLock<Regex> = OnceLock::new();
297 let as_re = regex(&AS, r"^([^,\s@]+) as .+$", false);
298 let re = regex(&REF, r"^[^,\s@]+?#([a-f0-9]+)$", false);
299 for (name, req) in requires {
300 let stripped = match as_re.captures(req.as_bytes()) {
301 Ok(Some(caps)) => group(&caps, 1).to_owned(),
302 _ => req.clone(),
303 };
304 if let Ok(Some(caps)) = re.captures(stripped.as_bytes()) {
305 if version::parse_stability(&stripped) == "dev" {
306 references.insert(name.to_lowercase(), group(&caps, 1).to_owned());
307 }
308 }
309 }
310}
311
312pub fn root_constraint(pretty: &str) -> Result<crate::constraint::Constraint, VersionError> {
314 Ok(parse_constraints(pretty)?.constraint)
315}
316
317#[cfg(test)]
318mod tests {
319 use super::*;
320 use serde_json::json;
321
322 #[test]
323 fn extracts_flags_aliases_references() {
324 let reqs = vec![
325 ("a/b".to_owned(), "^1.0@beta".to_owned()),
326 ("c/d".to_owned(), "dev-main as 1.0.x-dev".to_owned()),
327 ("e/f".to_owned(), "dev-main#abcdef".to_owned()),
328 ("g/h".to_owned(), "1.x-dev || ^2.0".to_owned()),
329 ("i/j".to_owned(), "^1.0".to_owned()),
330 ];
331 let mut flags = BTreeMap::new();
332 extract_stability_flags(&reqs, "stable", &mut flags);
333 assert_eq!(flags.get("a/b"), Some(&10));
334 assert_eq!(flags.get("c/d"), Some(&20));
335 assert_eq!(flags.get("e/f"), Some(&20));
336 assert_eq!(flags.get("g/h"), Some(&20));
337 assert_eq!(flags.get("i/j"), None);
338 let mut aliases = Vec::new();
339 extract_aliases(&reqs, &mut aliases).unwrap();
340 assert_eq!(aliases.len(), 1);
341 assert_eq!(aliases[0].alias_normalized, "1.0.9999999.9999999-dev");
342 let mut refs = BTreeMap::new();
343 extract_references(&reqs, &mut refs);
344 assert_eq!(refs.get("e/f").map(String::as_str), Some("abcdef"));
345 }
346
347 #[test]
348 fn loads_root_without_git() {
349 let m = json!({"name": "acme/app", "require": {"php": "^8.1", "monolog/monolog": "^3"}, "minimum-stability": "RC", "prefer-stable": true});
350 let r = RootPackage::load(&m, Path::new("/nonexistent-vivacity")).unwrap();
351 assert_eq!(r.package.pretty_version, DEFAULT_PRETTY_VERSION);
352 assert_eq!(r.package.version, "1.0.0.0");
353 assert_eq!(r.minimum_stability, "RC");
354 assert!(r.prefer_stable);
355 assert_eq!(r.package.requires.len(), 2);
356 }
357}