1use crate::error::{Error, Result};
13use crate::layout::Layout;
14use crate::lock::{Lock, LockPackage};
15use crate::pathutil::php_str;
16use crate::phpjson::{php_json_encode_with, FLAGS_JSONFILE};
17use crate::version::normalize_pretty;
18use serde_json::{Map, Value};
19
20pub const INSTALLED_VERSIONS_PHP: &str = include_str!("../assets/InstalledVersions.php");
21
22const ENTRY_KEY_ORDER: [&str; 33] = [
25 "name",
26 "version",
27 "version_normalized",
28 "target-dir",
29 "source",
30 "dist",
31 "require",
32 "conflict",
33 "provide",
34 "replace",
35 "require-dev",
36 "suggest",
37 "time",
38 "default-branch",
39 "bin",
40 "type",
41 "extra",
42 "installation-source",
43 "autoload",
44 "autoload-dev",
45 "notification-url",
46 "include-path",
47 "php-ext",
48 "archive",
49 "scripts",
50 "license",
51 "authors",
52 "description",
53 "homepage",
54 "keywords",
55 "repositories",
56 "support",
57 "funding",
58];
59
60#[derive(Debug, Clone)]
62pub struct RootPackage {
63 pub name: String,
64 pub pretty_version: String,
65 pub version: String,
66 pub reference: Option<String>,
67 pub package_type: String,
68 pub dev: bool,
69 pub aliases: Vec<String>,
71 pub alias_normalized: Option<String>,
73}
74
75impl RootPackage {
76 pub fn detect(manifest: &Value, project_dir: &std::path::Path, dev: bool) -> RootPackage {
80 let name = manifest
81 .get("name")
82 .and_then(Value::as_str)
83 .unwrap_or("__root__")
84 .to_owned();
85 let package_type = manifest
86 .get("type")
87 .and_then(Value::as_str)
88 .unwrap_or("library")
89 .to_owned();
90 let rv = crate::root_version::detect(manifest, project_dir);
91 let alias = crate::root_version::branch_alias(manifest, &rv);
92 RootPackage {
93 name,
94 pretty_version: rv.pretty_version,
95 version: rv.version,
96 reference: rv.reference,
97 package_type,
98 dev,
99 aliases: alias.iter().map(|(_, pretty)| pretty.clone()).collect(),
100 alias_normalized: alias.map(|(n, _)| n),
101 }
102 }
103
104 pub fn from_manifest(manifest: &Value, dev: bool) -> RootPackage {
106 let mut r = RootPackage::detect(
107 manifest,
108 std::path::Path::new("/nonexistent-vivacity-root"),
109 dev,
110 );
111 if manifest.get("version").is_none() && std::env::var("COMPOSER_ROOT_VERSION").is_err() {
112 r.pretty_version = crate::root_version::DEFAULT_PRETTY_VERSION.to_owned();
113 r.version = "1.0.0.0".to_owned();
114 r.reference = None;
115 r.aliases = Vec::new();
116 r.alias_normalized = None;
117 }
118 r
119 }
120}
121
122fn install_path_code(install_path: &str) -> String {
126 if crate::pathutil::is_absolute_path(install_path) {
127 php_str(install_path)
128 } else {
129 format!("__DIR__ . {}", php_str(&format!("/{install_path}")))
130 }
131}
132
133pub fn installed_json(lock: &Lock, with_dev: bool, layout: &Layout) -> Result<String> {
135 let mut entries: Vec<&LockPackage> = lock.wanted_packages(with_dev).collect();
136 entries.sort_by(|a, b| a.name().cmp(b.name()).then(a.version().cmp(b.version())));
137
138 let mut packages = Vec::new();
139 for p in &entries {
140 let mut entry = Map::new();
141 let mut src = p.raw.clone();
142 src.insert(
143 "version_normalized".to_owned(),
144 Value::String(normalize_pretty(p.version()).unwrap_or_else(|_| p.version().to_owned())),
145 );
146 if !p.is_metapackage() {
149 src.insert(
150 "installation-source".to_owned(),
151 Value::String("dist".to_owned()),
152 );
153 }
154 for key in ENTRY_KEY_ORDER {
155 if let Some(v) = src.remove(key) {
156 entry.insert(key.to_owned(), v);
157 }
158 }
159 for (k, v) in src {
161 entry.insert(k, v);
162 }
163 entry.insert(
164 "install-path".to_owned(),
165 layout
166 .install_path(p.name())
167 .map(Value::String)
168 .unwrap_or(Value::Null),
169 );
170 packages.push(Value::Object(entry));
171 }
172
173 let mut dev_names: Vec<Value> = lock
174 .packages_dev
175 .iter()
176 .map(|p| Value::String(p.name().to_ascii_lowercase()))
177 .collect();
178 dev_names.sort_by(|a, b| a.as_str().cmp(&b.as_str()));
179
180 let mut doc = Map::new();
181 doc.insert("packages".to_owned(), Value::Array(packages));
182 doc.insert("dev".to_owned(), Value::Bool(with_dev));
183 doc.insert(
184 "dev-package-names".to_owned(),
185 Value::Array(if with_dev { dev_names } else { Vec::new() }),
186 );
187 let mut text = php_json_encode_with(&Value::Object(doc), FLAGS_JSONFILE)?;
188 text.push('\n');
189 Ok(text)
190}
191
192#[derive(Debug, Default)]
194struct VersionEntry {
195 pretty_version: Option<String>,
196 version: Option<String>,
197 reference: Option<Option<String>>,
198 package_type: Option<String>,
199 install_path: Option<Option<String>>, dev_requirement: Option<bool>,
201 aliases: Vec<String>,
202 replaced: Vec<String>,
203 provided: Vec<String>,
204}
205
206fn is_platform_package(name: &str) -> bool {
209 let n = name.to_ascii_lowercase();
210 n == "php"
211 || n == "hhvm"
212 || n == "composer"
213 || n == "composer-plugin-api"
214 || n == "composer-runtime-api"
215 || matches!(
216 n.as_str(),
217 "php-64bit" | "php-ipv6" | "php-zts" | "php-debug"
218 )
219 || (n.starts_with("ext-") && !n.contains('/'))
220 || (n.starts_with("lib-") && !n.contains('/'))
221}
222
223pub fn installed_php(
225 lock: &Lock,
226 root: &RootPackage,
227 root_manifest: &Value,
228 with_dev: bool,
229 layout: &Layout,
230) -> Result<String> {
231 use std::collections::BTreeMap;
232
233 let mut versions: BTreeMap<String, VersionEntry> = BTreeMap::new();
234 let dev_names: std::collections::BTreeSet<&str> = lock
235 .packages_dev
236 .iter()
237 .map(|p| p.raw.get("name").and_then(Value::as_str).unwrap_or(""))
238 .collect();
239
240 let packages: Vec<&LockPackage> = lock.wanted_packages(with_dev).collect();
241 for p in &packages {
242 let name = p.name().to_owned();
243 let is_dev = dev_names.contains(name.as_str());
244 let reference = p
245 .dist_reference()
246 .or_else(|| {
247 p.raw
248 .get("source")
249 .and_then(|s| s.get("reference"))
250 .and_then(Value::as_str)
251 })
252 .map(str::to_owned);
253 let entry = versions.entry(name.clone()).or_default();
254 entry.pretty_version = Some(p.version().to_owned());
255 entry.version =
256 Some(normalize_pretty(p.version()).unwrap_or_else(|_| p.version().to_owned()));
257 entry.reference = Some(reference);
258 entry.package_type = Some(p.package_type().to_owned());
259 entry.install_path = Some(
260 layout
261 .install_path(p.name())
262 .map(|ip| install_path_code(&ip)),
263 );
264 entry.dev_requirement = Some(is_dev);
265 let default_branch = p
268 .raw
269 .get("default-branch")
270 .and_then(Value::as_bool)
271 .unwrap_or(false);
272 if let Some((_, pretty)) =
273 crate::root_version::branch_alias_of(p.version(), p.raw.get("extra"), default_branch)
274 {
275 entry.aliases.push(pretty);
276 }
277 }
278
279 for p in &packages {
281 let is_dev = dev_names.contains(p.name());
282 for (kind, is_replace) in [("replace", true), ("provide", false)] {
283 if let Some(map) = p.raw.get(kind).and_then(Value::as_object) {
284 for (target, constraint) in map {
285 if is_platform_package(target) {
286 continue;
287 }
288 let entry = versions.entry(target.clone()).or_default();
289 match entry.dev_requirement {
290 None => entry.dev_requirement = Some(is_dev),
291 Some(true) if !is_dev => entry.dev_requirement = Some(false),
292 _ => {}
293 }
294 let mut c = constraint.as_str().unwrap_or("*").to_owned();
295 if c == "self.version" {
296 c = p.version().to_owned();
297 }
298 let list = if is_replace {
299 &mut entry.replaced
300 } else {
301 &mut entry.provided
302 };
303 if !list.contains(&c) {
304 list.push(c);
305 }
306 }
307 }
308 }
309 }
310
311 for (kind, is_replace) in [("replace", true), ("provide", false)] {
313 if let Some(map) = root_manifest.get(kind).and_then(Value::as_object) {
314 for (target, constraint) in map {
315 if is_platform_package(target) {
316 continue;
317 }
318 let entry = versions.entry(target.clone()).or_default();
319 entry.dev_requirement.get_or_insert(false);
320 if entry.dev_requirement == Some(true) {
321 entry.dev_requirement = Some(false);
322 }
323 let mut c = constraint.as_str().unwrap_or("*").to_owned();
324 if c == "self.version" {
325 c = root.pretty_version.clone();
326 }
327 let list = if is_replace {
328 &mut entry.replaced
329 } else {
330 &mut entry.provided
331 };
332 if !list.contains(&c) {
333 list.push(c);
334 }
335 }
336 }
337 }
338
339 {
341 let entry = versions.entry(root.name.clone()).or_default();
342 entry.pretty_version = Some(root.pretty_version.clone());
343 entry.version = Some(root.version.clone());
344 entry.reference = Some(root.reference.clone());
345 entry.package_type = Some(root.package_type.clone());
346 entry.install_path = Some(Some("__DIR__ . '/../../'".to_owned()));
347 entry.dev_requirement = Some(false);
348 entry.aliases = root.aliases.clone();
349 }
350
351 for e in versions.values_mut() {
352 e.replaced.sort();
353 e.provided.sort();
354 }
355
356 let mut out = String::from("<?php return array(\n");
359 out.push_str(" 'root' => array(\n");
360 push_kv(&mut out, 2, "name", &php_str(&root.name));
361 push_kv(
362 &mut out,
363 2,
364 "pretty_version",
365 &php_str(&root.pretty_version),
366 );
367 push_kv(&mut out, 2, "version", &php_str(&root.version));
368 push_kv(
369 &mut out,
370 2,
371 "reference",
372 &root
373 .reference
374 .as_deref()
375 .map(php_str)
376 .unwrap_or_else(|| "null".to_owned()),
377 );
378 push_kv(&mut out, 2, "type", &php_str(&root.package_type));
379 push_kv(&mut out, 2, "install_path", "__DIR__ . '/../../'");
380 if root.aliases.is_empty() {
381 push_kv(&mut out, 2, "aliases", "array()");
382 } else {
383 push_string_list(&mut out, 2, "aliases", &root.aliases);
384 }
385 push_kv(&mut out, 2, "dev", if root.dev { "true" } else { "false" });
386 out.push_str(" ),\n");
387 out.push_str(" 'versions' => array(\n");
388 for (name, e) in &versions {
389 out.push_str(&format!(" {} => array(\n", php_str(name)));
390 if let Some(v) = &e.pretty_version {
391 push_kv(&mut out, 3, "pretty_version", &php_str(v));
392 }
393 if let Some(v) = &e.version {
394 push_kv(&mut out, 3, "version", &php_str(v));
395 }
396 if let Some(r) = &e.reference {
397 push_kv(
398 &mut out,
399 3,
400 "reference",
401 &r.as_deref()
402 .map(php_str)
403 .unwrap_or_else(|| "null".to_owned()),
404 );
405 }
406 if let Some(t) = &e.package_type {
407 push_kv(&mut out, 3, "type", &php_str(t));
408 }
409 if let Some(ip) = &e.install_path {
410 push_kv(&mut out, 3, "install_path", ip.as_deref().unwrap_or("null"));
411 }
412 if e.pretty_version.is_some() {
413 if e.aliases.is_empty() {
414 push_kv(&mut out, 3, "aliases", "array()");
415 } else {
416 push_string_list(&mut out, 3, "aliases", &e.aliases);
417 }
418 }
419 if let Some(d) = e.dev_requirement {
420 push_kv(
421 &mut out,
422 3,
423 "dev_requirement",
424 if d { "true" } else { "false" },
425 );
426 }
427 push_string_list(&mut out, 3, "replaced", &e.replaced);
428 push_string_list(&mut out, 3, "provided", &e.provided);
429 out.push_str(" ),\n");
430 }
431 out.push_str(" ),\n");
432 out.push_str(");\n");
433 Ok(out)
434}
435
436fn push_kv(out: &mut String, level: usize, key: &str, value: &str) {
437 for _ in 0..level {
438 out.push_str(" ");
439 }
440 out.push_str(&format!("{} => {},\n", php_str(key), value));
441}
442
443fn push_string_list(out: &mut String, level: usize, key: &str, values: &[String]) {
444 if values.is_empty() {
445 return;
446 }
447 for _ in 0..level {
448 out.push_str(" ");
449 }
450 out.push_str(&format!("{} => array(\n", php_str(key)));
451 for (i, v) in values.iter().enumerate() {
452 for _ in 0..=level {
453 out.push_str(" ");
454 }
455 out.push_str(&format!("{i} => {},\n", php_str(v)));
456 }
457 for _ in 0..level {
458 out.push_str(" ");
459 }
460 out.push_str("),\n");
461}
462
463pub fn write_state_files(
464 vendor_composer: &std::path::Path,
465 lock: &Lock,
466 root: &RootPackage,
467 root_manifest: &Value,
468 with_dev: bool,
469 layout: &Layout,
470) -> Result<()> {
471 std::fs::create_dir_all(vendor_composer).map_err(Error::io(vendor_composer))?;
472 let writes = [
473 ("installed.json", installed_json(lock, with_dev, layout)?),
474 (
475 "installed.php",
476 installed_php(lock, root, root_manifest, with_dev, layout)?,
477 ),
478 ("InstalledVersions.php", INSTALLED_VERSIONS_PHP.to_owned()),
479 ];
480 for (file, content) in writes {
481 let path = vendor_composer.join(file);
482 let tmp = vendor_composer.join(format!(".{file}.vivacity-tmp"));
483 std::fs::write(&tmp, content).map_err(Error::io(&tmp))?;
484 std::fs::rename(&tmp, &path).map_err(Error::io(&path))?;
485 }
486 Ok(())
487}
488
489#[cfg(test)]
490mod tests {
491 use super::*;
492 use serde_json::json;
493
494 fn sample_lock() -> Lock {
495 Lock::parse(
496 &json!({
497 "packages": [
498 {"name": "a/lib", "version": "v1.2.0", "type": "library",
499 "dist": {"type": "zip", "url": "https://x/a.zip", "reference": "abcdef1234567890"},
500 "replace": {"a/lib-compat": "self.version", "php": "*"},
501 "provide": {"psr/log-implementation": "1.0"}},
502 {"name": "a/meta", "version": "2.0.0", "type": "metapackage"}
503 ],
504 "packages-dev": [
505 {"name": "d/tool", "version": "3.1.4", "type": "library",
506 "dist": {"type": "zip", "url": "https://x/d.zip", "reference": "feedfacefeedface"}}
507 ]
508 })
509 .to_string(),
510 )
511 .expect("lock")
512 }
513
514 #[test]
515 fn installed_json_shape() {
516 let lock = sample_lock();
517 let layout = Layout::vendor_only(std::path::Path::new("/proj"), &lock, true);
518 let text = installed_json(&lock, true, &layout).expect("json");
519 let v: Value = serde_json::from_str(&text).expect("parse");
520 let names: Vec<&str> = v["packages"]
521 .as_array()
522 .expect("arr")
523 .iter()
524 .map(|p| p["name"].as_str().expect("name"))
525 .collect();
526 assert_eq!(
527 names,
528 vec!["a/lib", "a/meta", "d/tool"],
529 "global sort by name"
530 );
531 assert_eq!(v["packages"][0]["version_normalized"], "1.2.0.0");
532 assert_eq!(v["packages"][0]["installation-source"], "dist");
533 assert_eq!(v["packages"][0]["install-path"], "../a/lib");
534 assert_eq!(v["packages"][1]["install-path"], Value::Null, "metapackage");
535 assert_eq!(v["dev"], true);
536 assert_eq!(v["dev-package-names"][0], "d/tool");
537 let entry_text = text.split("\"a/lib\"").nth(1).expect("entry");
539 let vn = entry_text.find("version_normalized").expect("vn");
540 let dist = entry_text.find("\"dist\"").expect("dist");
541 assert!(vn < dist);
542
543 let lock = sample_lock();
544 let layout = Layout::vendor_only(std::path::Path::new("/proj"), &lock, false);
545 let no_dev = installed_json(&lock, false, &layout).expect("json");
546 let v: Value = serde_json::from_str(&no_dev).expect("parse");
547 assert_eq!(v["packages"].as_array().expect("arr").len(), 2);
548 assert_eq!(v["dev"], false);
549 }
550
551 #[test]
552 fn installed_php_contains_virtual_and_root_entries() {
553 let root = RootPackage {
554 name: "acme/app".to_owned(),
555 pretty_version: "1.0.0+no-version-set".to_owned(),
556 version: "1.0.0.0".to_owned(),
557 reference: None,
558 package_type: "project".to_owned(),
559 dev: true,
560 aliases: Vec::new(),
561 alias_normalized: None,
562 };
563 let lock = sample_lock();
564 let layout = Layout::vendor_only(std::path::Path::new("/proj"), &lock, true);
565 let text = installed_php(&lock, &root, &json!({}), true, &layout).expect("php");
566 assert!(text.starts_with("<?php return array(\n"));
567 assert!(text.contains("'acme/app' => array("));
568 assert!(text.contains("'a/lib-compat' => array("));
569 assert!(
570 text.contains("0 => 'v1.2.0',"),
571 "self.version resolved: {text}"
572 );
573 assert!(text.contains("'psr/log-implementation' => array("));
574 assert!(
575 !text.contains("'php' => array("),
576 "platform targets are excluded"
577 );
578 assert!(text.contains("'install_path' => __DIR__ . '/../a/lib',"));
579 assert!(
580 text.contains("'install_path' => null,"),
581 "metapackage without a path"
582 );
583 assert!(text.contains("'dev_requirement' => true,"));
584 }
585
586 #[test]
587 fn php_str_escapes() {
588 assert_eq!(php_str("a'b\\c"), r"'a\'b\\c'");
589 }
590}