1use crate::constraint::{Constraint, Op};
8use crate::lockfile::{dump_package, lock_data, lock_packages, LockInput};
9use crate::optimizer::PoolOptimizer;
10use crate::package::{Origin, Package};
11use crate::platform::{platform_packages, probe};
12use crate::platform_filter::PlatformRequirementFilter;
13use crate::policy::DefaultPolicy;
14use crate::pool::{OrderedMap, Pool, PoolError, Repository, RepositorySet, Request};
15use crate::repository::{
16 locked_repository, ComposerRepository, FileTransport, HttpTransport, HttpTransports,
17};
18use crate::root::RootPackage;
19use crate::solver::{SolveError, Solver};
20use crate::transaction::LockTransaction;
21use crate::version::{parse_stability, regex, stability_rank};
22use pcre2::bytes::Regex;
23use serde_json::{Map, Value};
24use std::collections::{BTreeMap, BTreeSet};
25use std::path::Path;
26use std::sync::OnceLock;
27
28#[derive(Debug, thiserror::Error)]
29#[error("{message}")]
30pub struct SessionError {
31 pub message: String,
32 pub kind: SessionErrorKind,
33}
34
35#[derive(Debug, Clone, Copy, PartialEq, Eq)]
39pub enum SessionErrorKind {
40 Other,
41 Unsolvable,
42}
43
44impl SessionError {
45 pub fn new(message: impl Into<String>) -> Self {
46 Self {
47 message: message.into(),
48 kind: SessionErrorKind::Other,
49 }
50 }
51}
52
53impl From<PoolError> for SessionError {
54 fn from(e: PoolError) -> SessionError {
55 SessionError::new(e.0)
56 }
57}
58
59#[derive(Debug, Clone, PartialEq)]
61pub struct RepoConfig {
62 pub key: RepoKey,
63 pub definition: Value,
64}
65
66#[derive(Debug, Clone, PartialEq, Eq)]
67pub enum RepoKey {
68 Named(String),
69 Indexed(u64),
70}
71
72pub fn merge_repositories(current: &mut Vec<RepoConfig>, new: &Value) {
75 static PACKAGIST: OnceLock<Regex> = OnceLock::new();
76 let entries: Vec<(RepoKey, Value)> = match new {
77 Value::Array(list) => list
78 .iter()
79 .enumerate()
80 .map(|(i, v)| (RepoKey::Indexed(i as u64), v.clone()))
81 .collect(),
82 Value::Object(map) => map
83 .iter()
84 .map(|(k, v)| {
85 let key = match k.parse::<u64>() {
86 Ok(i) if i.to_string() == *k => RepoKey::Indexed(i),
87 _ => RepoKey::Named(k.clone()),
88 };
89 (key, v.clone())
90 })
91 .collect(),
92 _ => return,
93 };
94 if entries.is_empty() {
95 return;
96 }
97 current.reverse();
98 let disable = |current: &mut Vec<RepoConfig>, name: &str| {
101 let key = match name.parse::<u64>() {
102 Ok(i) if i.to_string() == name => RepoKey::Indexed(i),
103 _ => RepoKey::Named(name.to_owned()),
104 };
105 if current.iter().any(|r| r.key == key) {
106 current.retain(|r| r.key != key);
107 } else if name == "packagist" {
108 current.retain(|r| r.key != RepoKey::Named("packagist.org".into()));
109 }
110 };
111 for (key, repository) in entries.into_iter().rev() {
112 if repository == Value::Bool(false) {
113 let name = match &key {
114 RepoKey::Named(n) => n.clone(),
115 RepoKey::Indexed(i) => i.to_string(),
116 };
117 disable(current, &name);
118 continue;
119 }
120 if let Some(obj) = repository.as_object() {
121 if obj.len() == 1 && obj.values().next() == Some(&Value::Bool(false)) {
122 disable(current, obj.keys().next().map(String::as_str).unwrap_or(""));
123 continue;
124 }
125 }
126 if repository.get("type").and_then(Value::as_str) == Some("composer") {
127 if let Some(url) = repository.get("url").and_then(Value::as_str) {
128 let re = regex(
129 &PACKAGIST,
130 r"^https?://(?:[a-z0-9-.]+\.)?packagist.org(/|$)",
131 false,
132 );
133 if re.is_match(url.as_bytes()).unwrap_or(false) {
134 disable(current, "packagist.org");
135 }
136 }
137 }
138 match key {
139 RepoKey::Indexed(i) => {
140 if current.iter().any(|r| r.key == RepoKey::Indexed(i)) {
141 let next = current
142 .iter()
143 .filter_map(|r| match r.key {
144 RepoKey::Indexed(j) => Some(j + 1),
145 _ => None,
146 })
147 .max()
148 .unwrap_or(0);
149 current.push(RepoConfig {
150 key: RepoKey::Indexed(next),
151 definition: repository,
152 });
153 } else {
154 current.push(RepoConfig {
155 key: RepoKey::Indexed(i),
156 definition: repository,
157 });
158 }
159 }
160 RepoKey::Named(name) => {
161 let name = if name == "packagist" {
162 "packagist.org".to_owned()
163 } else {
164 name
165 };
166 let key = RepoKey::Named(name);
167 if let Some(slot) = current.iter_mut().find(|r| r.key == key) {
168 slot.definition = repository;
169 } else {
170 current.push(RepoConfig {
171 key,
172 definition: repository,
173 });
174 }
175 }
176 }
177 }
178 current.reverse();
179}
180
181#[derive(Debug, Clone, Default)]
183pub struct MergedConfig {
184 pub repositories: Vec<RepoConfig>,
185 pub platform: Map<String, Value>,
187 pub policy: crate::policy_config::RawPolicyConfig,
189}
190
191impl MergedConfig {
192 pub fn load(
195 manifest: &Value,
196 composer_home: Option<&Path>,
197 ) -> Result<MergedConfig, SessionError> {
198 let mut cfg = MergedConfig {
199 repositories: vec![RepoConfig {
200 key: RepoKey::Named("packagist.org".into()),
201 definition: serde_json::json!({"type": "composer", "url": "https://repo.packagist.org"}),
202 }],
203 platform: Map::new(),
204 policy: crate::policy_config::RawPolicyConfig::default(),
205 };
206 if let Some(home) = composer_home {
207 let global = home.join("config.json");
208 if global.is_file() {
209 let text = std::fs::read_to_string(&global)
210 .map_err(|e| SessionError::new(format!("{}: {e}", global.display())))?;
211 let v: Value = serde_json::from_str(&text)
212 .map_err(|e| SessionError::new(format!("{}: {e}", global.display())))?;
213 cfg.merge(&v);
214 }
215 }
216 cfg.merge(manifest);
217 Ok(cfg)
218 }
219
220 fn merge(&mut self, config: &Value) {
221 if let Some(platform) = config.get("config").and_then(|c| c.get("platform")) {
224 self.platform = platform.as_object().cloned().unwrap_or_default();
225 }
226 if let Some(repos) = config.get("repositories") {
227 merge_repositories(&mut self.repositories, repos);
228 }
229 if let Some(cfg) = config.get("config").and_then(Value::as_object) {
230 self.policy.merge(cfg);
231 }
232 }
233}
234
235pub struct SolveReport {
238 pub transaction: LockTransaction,
239 pub decisions: Vec<i64>,
241 pub rules: usize,
243 pub learned: usize,
245}
246
247#[derive(Debug, Clone, Default)]
250pub struct UpdateOptions {
251 pub allow_list: Vec<String>,
253 pub transitive: Option<crate::pool::UpdateMode>,
254 pub no_blocking: bool,
256 pub root_patch: Option<crate::root::RootPatch>,
259}
260
261impl UpdateOptions {
262 pub fn partial(packages: &[String], transitive: crate::pool::UpdateMode) -> UpdateOptions {
264 let mut allow_list: Vec<String> = Vec::new();
265 for p in packages {
266 let l = p.to_lowercase();
267 if !allow_list.contains(&l) {
268 allow_list.push(l);
269 }
270 }
271 UpdateOptions {
272 allow_list,
273 transitive: Some(transitive),
274 ..Default::default()
275 }
276 }
277}
278
279pub struct UpdateSession {
281 pub arena: Vec<Package>,
282 pub root: RootPackage,
283 pub fixed_root: usize,
285 pub fixed_root_alias: Option<usize>,
286 pub platform: Vec<usize>,
287 pub locked: Option<Vec<usize>>,
288 pub set: RepositorySet,
289 pub request: Request,
290 pub config: MergedConfig,
291 pub dev_mode: bool,
292 pub prefer_stable: bool,
294 pub prefer_lowest: bool,
295 pub php_version: String,
298 pub policy_config: crate::policy_config::PolicyConfig,
300 pub ini_files: Vec<String>,
303 pub loaded_extensions: BTreeSet<String>,
305 pub installer_dev_mode: bool,
308}
309
310impl UpdateSession {
311 pub fn prepare(
312 project_dir: &Path,
313 composer_home: Option<&Path>,
314 dev_mode: bool,
315 ) -> Result<UpdateSession, SessionError> {
316 Self::prepare_with(project_dir, composer_home, dev_mode, None)
317 }
318
319 pub fn prepare_with(
321 project_dir: &Path,
322 composer_home: Option<&Path>,
323 dev_mode: bool,
324 http: Option<HttpTransports>,
325 ) -> Result<UpdateSession, SessionError> {
326 Self::prepare_full(project_dir, composer_home, dev_mode, http, None)
327 }
328
329 pub fn prepare_full(
332 project_dir: &Path,
333 composer_home: Option<&Path>,
334 dev_mode: bool,
335 http: Option<HttpTransports>,
336 cache_repo_dir: Option<&Path>,
337 ) -> Result<UpdateSession, SessionError> {
338 Self::prepare_update(
339 project_dir,
340 composer_home,
341 dev_mode,
342 http,
343 cache_repo_dir,
344 &UpdateOptions::default(),
345 )
346 }
347
348 pub fn prepare_update(
351 project_dir: &Path,
352 composer_home: Option<&Path>,
353 dev_mode: bool,
354 http: Option<HttpTransports>,
355 cache_repo_dir: Option<&Path>,
356 options: &UpdateOptions,
357 ) -> Result<UpdateSession, SessionError> {
358 let partial_update = !options.allow_list.is_empty();
359 let manifest_path = project_dir.join("composer.json");
360 let manifest_text = std::fs::read_to_string(&manifest_path)
361 .map_err(|e| SessionError::new(format!("{}: {e}", manifest_path.display())))?;
362 let manifest: Value = serde_json::from_str(&manifest_text)
363 .map_err(|e| SessionError::new(format!("{}: {e}", manifest_path.display())))?;
364 let config = MergedConfig::load(&manifest, composer_home)?;
365 let mut policy_config = crate::policy_config::PolicyConfig::from_raw(&config.policy)
366 .map_err(|e| SessionError::new(e.0))?;
367 policy_config
368 .apply_no_blocking(options.no_blocking)
369 .map_err(|e| SessionError::new(e.0))?;
370 let mut root =
371 RootPackage::load(&manifest, project_dir).map_err(|e| SessionError::new(e.0))?;
372 if let Some(patch) = &options.root_patch {
373 root.apply_patch(patch)
374 .map_err(|e| SessionError::new(e.0))?;
375 }
376 let probed = probe().map_err(|e| SessionError::new(e.0))?;
377 let php_version = probed
381 .iter()
382 .find(|p| p.get("name").and_then(Value::as_str) == Some("php"))
383 .and_then(|p| p.get("version").and_then(Value::as_str))
384 .map(|v| {
385 v.split(['.', '-', '+'])
386 .take(3)
387 .map(|part| part.trim_end_matches(|c: char| !c.is_ascii_digit()))
388 .collect::<Vec<_>>()
389 .join(".")
390 })
391 .unwrap_or_default();
392 let platform_pkgs =
393 platform_packages(&probed, &config.platform).map_err(|e| SessionError::new(e.0))?;
394
395 let mut arena: Vec<Package> = Vec::new();
396
397 let mut fixed = root.package.clone();
399 fixed.requires = Default::default();
400 fixed.dev_requires = Default::default();
401 arena.push(fixed);
402 let fixed_root = 0;
403 let fixed_root_alias = root.branch_alias.as_ref().map(|(normalized, pretty)| {
404 let a = arena[fixed_root].alias(fixed_root, normalized, pretty);
405 arena.push(a);
406 arena.len() - 1
407 });
408 let root_members: Vec<usize> = fixed_root_alias
409 .into_iter()
410 .chain(std::iter::once(fixed_root))
411 .collect();
412
413 let mut platform: Vec<usize> = Vec::new();
414 for p in platform_pkgs {
415 arena.push(p);
416 platform.push(arena.len() - 1);
417 }
418
419 let lock_path = project_dir.join("composer.lock");
423 let lock: Option<Value> = if lock_path.is_file() {
424 let text = std::fs::read_to_string(&lock_path)
425 .map_err(|e| SessionError::new(format!("{}: {e}", lock_path.display())))?;
426 match serde_json::from_str::<Value>(&text) {
427 Ok(v) if v.get("packages").is_some_and(|p| !p.is_null()) => Some(v),
428 Ok(_) => None,
429 Err(e) => {
430 if partial_update {
431 return Err(SessionError::new(format!(
432 "\"{}\" does not contain valid JSON\n{e}",
433 lock_path.display()
434 )));
435 }
436 None
437 }
438 }
439 } else {
440 None
441 };
442 let locked = match &lock {
443 Some(v) => Some(locked_repository(v, &mut arena).map_err(|e| SessionError::new(e.0))?),
444 None => None,
445 };
446
447 let mut stability_flags = root.stability_flags.clone();
450 let root_version = match fixed_root_alias {
451 Some(a) => arena[a].version.clone(),
452 None => arena[fixed_root].version.clone(),
453 };
454 stability_flags.insert(
455 root.package.name.clone(),
456 stability_rank(parse_stability(&root_version)),
457 );
458
459 let mut root_requires: OrderedMap<Constraint> = OrderedMap::default();
464 let requires = match &root.branch_alias {
465 Some((normalized, pretty)) => {
466 let aliased = root.package.alias(0, normalized, pretty);
467 let mut all = aliased.requires.clone();
468 for l in aliased.dev_requires.iter() {
469 all.insert(l.clone());
470 }
471 all
472 }
473 None => root.all_requires(),
474 };
475 for link in requires.iter() {
476 root_requires.insert(&link.target, link.constraint.clone());
477 }
478
479 let mut set = RepositorySet::new(
480 &root.minimum_stability,
481 stability_flags,
482 &root.aliases,
483 root.references.clone(),
484 root_requires,
485 Default::default(),
486 );
487 set.add_repository(Repository::Root(root_members));
488 set.add_repository(Repository::Platform(platform.clone()));
489 for repo in &config.repositories {
490 let origin = Origin::Repository(set.repositories.len());
491 set.add_repository(open_repository(
492 repo,
493 http.as_ref(),
494 cache_repo_dir,
495 project_dir,
496 origin,
497 &mut arena,
498 )?);
499 }
500 if let Some(ids) = &locked {
501 set.add_repository(Repository::Locked(ids.clone()));
502 }
503
504 if partial_update && locked.is_none() {
506 return Err(SessionError::new(
507 "Cannot update only a partial set of packages without a lock file present. Run `composer update` to generate a lock file.",
508 ));
509 }
510 let mut request = Request::new(locked.clone());
511 if partial_update {
512 request.set_update_allow_list(
513 options.allow_list.clone(),
514 options
515 .transitive
516 .unwrap_or(crate::pool::UpdateMode::OnlyListed),
517 );
518 }
519 if let Some(a) = fixed_root_alias {
520 request.fix_package(a);
521 }
522 request.fix_package(fixed_root);
523 for &p in &platform {
524 let provided = arena[fixed_root]
525 .provides
526 .get(&arena[p].name)
527 .map(|l| l.constraint.clone());
528 let provided_here = provided
529 .is_some_and(|c| c.matches(&Constraint::new(Op::Eq, arena[p].version.clone())));
530 if !provided_here {
531 request.fix_package(p);
532 }
533 }
534 for link in requires.iter() {
535 request.require_name_pretty(
536 &link.target,
537 Some(link.constraint.clone()),
538 &link.pretty_constraint,
539 )?;
540 }
541
542 Ok(UpdateSession {
543 arena,
544 root,
545 fixed_root,
546 fixed_root_alias,
547 platform,
548 locked,
549 set,
550 request,
551 config,
552 dev_mode,
553 prefer_stable: false,
554 prefer_lowest: false,
555 php_version,
556 policy_config,
557 ini_files: crate::platform::ini_files(&probed),
558 loaded_extensions: crate::platform::loaded_extensions(&probed),
559 installer_dev_mode: true,
560 })
561 }
562
563 pub fn create_pool(&mut self) -> Result<Pool, SessionError> {
564 Ok(self.set.create_pool(&mut self.request, &mut self.arena)?)
565 }
566
567 pub fn policy(&self) -> DefaultPolicy {
569 DefaultPolicy::new(
570 self.prefer_stable || self.root.prefer_stable,
571 self.prefer_lowest,
572 None,
573 )
574 }
575
576 pub fn extract_dev_packages(
580 &mut self,
581 transaction: &mut LockTransaction,
582 policy: &mut DefaultPolicy,
583 filter: &PlatformRequirementFilter,
584 ) -> Result<(), SessionError> {
585 if self.root.package.dev_requires.is_empty() {
586 return Ok(());
587 }
588 let dumps: Vec<Value> = transaction
591 .new_lock_packages(&self.arena, false)
592 .iter()
593 .map(|&idx| Value::Object(dump_package(&self.arena[idx])))
594 .collect();
595 let result_ids =
596 crate::loader::load_packages(&dumps, Origin::Result, &mut self.arena, false)
597 .map_err(|e| SessionError::new(e.0))?;
598 let mut members: Vec<usize> = Vec::new();
601 members.extend(self.fixed_root_alias);
602 members.push(self.fixed_root);
603 members.extend(self.platform.iter().copied());
604 members.extend(result_ids);
605 let mut pool_packages: Vec<usize> = Vec::new();
606 for idx in members {
607 pool_packages.push(idx);
608 let (name, version) = (
609 self.arena[idx].name.clone(),
610 self.arena[idx].version.clone(),
611 );
612 if let Some((alias, alias_normalized)) = self
613 .set
614 .root_aliases
615 .get(&name)
616 .and_then(|m| m.get(&version))
617 {
618 let mut base = idx;
619 while let Some(b) = self.arena[base].alias_of {
620 base = b;
621 }
622 let mut a = self.arena[base].alias(base, alias_normalized, alias);
623 a.root_package_alias = true;
624 a.origin = Origin::Detached;
625 self.arena.push(a);
626 pool_packages.push(self.arena.len() - 1);
627 }
628 }
629 let pool = Pool::new(pool_packages, Vec::new(), &self.arena);
630 let mut request = Request::new(None);
632 if let Some(a) = self.fixed_root_alias {
633 request.fix_package(a);
634 }
635 request.fix_package(self.fixed_root);
636 for &p in &self.platform {
637 let provided = self.arena[self.fixed_root]
638 .provides
639 .get(&self.arena[p].name)
640 .map(|l| l.constraint.clone());
641 let provided_here = provided.is_some_and(|c| {
642 c.matches(&Constraint::new(Op::Eq, self.arena[p].version.clone()))
643 });
644 if !provided_here {
645 request.fix_package(p);
646 }
647 }
648 let requires = match &self.root.branch_alias {
649 Some((normalized, pretty)) => self.root.package.alias(0, normalized, pretty).requires,
650 None => self.root.package.requires.clone(),
651 };
652 for link in requires.iter() {
653 request.require_name_pretty(
654 &link.target,
655 Some(link.constraint.clone()),
656 &link.pretty_constraint,
657 )?;
658 }
659 let mut solver = Solver::new(&pool, &self.arena);
660 let non_dev = match solver.solve(&request, policy, filter) {
661 Ok(t) => t,
662 Err(SolveError::Problems(problems)) => {
663 drop(solver);
664 let pretty = {
667 let mut ctx = crate::problem::MessageContext {
668 arena: &mut self.arena,
669 set: &self.set,
670 pool: &pool,
671 request: &request,
672 is_verbose: false,
673 ini_files: &self.ini_files,
674 loaded_extensions: &self.loaded_extensions,
675 };
676 crate::problem::pretty_string(&mut ctx, &problems, true)
677 };
678 return Err(SessionError {
679 message: format!("Unable to find a compatible set of packages based on your non-dev requirements alone.\nYour requirements can be resolved successfully when require-dev packages are present.\nYou may need to move packages from require-dev or some of their dependencies to require.\n{pretty}"),
680 kind: SessionErrorKind::Unsolvable,
681 });
682 }
683 Err(SolveError::Bug(b)) => return Err(SessionError::new(b)),
684 };
685 transaction.set_non_dev_packages(&self.arena, &non_dev);
686 Ok(())
687 }
688
689 fn platform_requirements(links: &crate::package::Links) -> Map<String, Value> {
691 let mut out = Map::new();
692 for l in links.iter() {
693 if crate::platform::is_platform_package(&l.target) {
694 out.insert(l.target.clone(), Value::String(l.pretty_constraint.clone()));
695 }
696 }
697 out
698 }
699
700 pub fn lock_json(
702 &self,
703 transaction: &LockTransaction,
704 manifest_text: &str,
705 ) -> Result<Value, SessionError> {
706 let content_hash = vivacity_core::content_hash::content_hash(manifest_text)
707 .map_err(|e| SessionError::new(e.to_string()))?;
708 let packages = lock_packages(
709 &self.arena,
710 &transaction.new_lock_packages(&self.arena, false),
711 )
712 .map_err(SessionError::new)?;
713 let packages_dev = lock_packages(
714 &self.arena,
715 &transaction.new_lock_packages(&self.arena, true),
716 )
717 .map_err(SessionError::new)?;
718 let (requires, dev_requires) = match &self.root.branch_alias {
719 Some((normalized, pretty)) => {
720 let a = self.root.package.alias(0, normalized, pretty);
721 (a.requires, a.dev_requires)
722 }
723 None => (
724 self.root.package.requires.clone(),
725 self.root.package.dev_requires.clone(),
726 ),
727 };
728 Ok(lock_data(LockInput {
729 content_hash: &content_hash,
730 packages,
731 packages_dev: Some(packages_dev),
732 platform: Self::platform_requirements(&requires),
733 platform_dev: Self::platform_requirements(&dev_requires),
734 aliases: &transaction.aliases(&self.arena, &self.root.aliases),
735 minimum_stability: &self.root.minimum_stability,
736 stability_flags: &self.root.stability_flags,
737 prefer_stable: self.prefer_stable || self.root.prefer_stable,
738 prefer_lowest: self.prefer_lowest,
739 platform_overrides: &self.config.platform,
740 }))
741 }
742
743 pub fn update(
746 &mut self,
747 manifest_text: &str,
748 filter: &PlatformRequirementFilter,
749 ) -> Result<(Value, SolveReport), SessionError> {
750 let trace = std::env::var_os("VIVACITY_TRACE").is_some();
751 let t = std::time::Instant::now();
752 let lap = |label: &str, t: &std::time::Instant| {
753 if trace {
754 eprintln!(
755 "trace: {label:<22} {:>7.1} ms",
756 t.elapsed().as_secs_f64() * 1000.0
757 );
758 }
759 };
760 let mut policy = self.policy();
761 let pool = self.create_filtered_pool()?;
762 for w in &pool.warnings {
763 eprintln!("{w}");
764 }
765 lap("pool", &t);
766 let pool = if std::env::var("COMPOSER_POOL_OPTIMIZER").as_deref() == Ok("0") {
767 pool
768 } else {
769 PoolOptimizer::new().optimize(&self.request, &pool, &self.arena, &mut policy)
770 };
771 lap("optimize", &t);
772 let mut report = match self.solve(&pool, &mut policy, filter) {
773 Ok(r) => r,
774 Err(SolveError::Problems(problems)) => {
775 let pretty = self.pretty_problems(&pool, &problems, false);
778 let mut message = format!(
779 "Your requirements could not be resolved to an installable set of packages.\n{pretty}"
780 );
781 if !self.installer_dev_mode {
782 message.push_str("\nRunning update with --no-dev does not mean require-dev is ignored, it just means the packages will not be installed. If dev requirements are blocking the update you have to resolve those problems.");
783 }
784 return Err(SessionError {
785 message,
786 kind: SessionErrorKind::Unsolvable,
787 });
788 }
789 Err(SolveError::Bug(b)) => return Err(SessionError::new(b)),
790 };
791 lap("solve", &t);
792 for &idx in &report.transaction.all {
796 crate::lockfile::validate_package(&self.arena[idx]).map_err(SessionError::new)?;
797 }
798 drop(pool);
799 let mut transaction = std::mem::replace(&mut report.transaction, LockTransaction::empty());
800 self.extract_dev_packages(&mut transaction, &mut policy, filter)?;
801 lap("extract dev", &t);
802 let lock = self.lock_json(&transaction, manifest_text)?;
803 lap("lock data", &t);
804 report.transaction = transaction;
805 Ok((lock, report))
806 }
807
808 pub fn pretty_problems(
811 &mut self,
812 pool: &Pool,
813 problems: &[crate::solver::SolvedProblem],
814 is_dev_extraction: bool,
815 ) -> String {
816 let mut ctx = crate::problem::MessageContext {
817 arena: &mut self.arena,
818 set: &self.set,
819 pool,
820 request: &self.request,
821 is_verbose: false,
822 ini_files: &self.ini_files,
823 loaded_extensions: &self.loaded_extensions,
824 };
825 crate::problem::pretty_string(&mut ctx, problems, is_dev_extraction)
826 }
827
828 pub fn find_packages_for_require(
835 &mut self,
836 name: &str,
837 ignore_stability: bool,
838 ) -> Result<Vec<usize>, SessionError> {
839 let name = name.to_lowercase();
840 let mut acceptable = BTreeMap::new();
841 let min = crate::version::stability_rank(&self.root.minimum_stability);
842 for st in ["stable", "RC", "beta", "alpha", "dev"] {
843 let rank = crate::version::stability_rank(st);
844 if ignore_stability || rank <= min {
845 acceptable.insert(st.to_owned(), rank);
846 }
847 }
848 let flags = BTreeMap::new();
849 let already = BTreeMap::new();
850 let map = vec![(name.clone(), Constraint::MatchAll)];
851 let mut found: Vec<usize> = Vec::new();
852 let (_, ids) = crate::pool::array_repository_load_packages(
853 &self.platform,
854 &map,
855 &acceptable,
856 &flags,
857 &already,
858 &self.arena,
859 );
860 found.extend(ids);
861 for (i, repo) in self.set.repositories.iter().enumerate() {
862 if let Repository::Composer(repo) = repo {
863 let (_, ids) = repo
864 .load_packages(
865 &map,
866 &acceptable,
867 &flags,
868 &already,
869 Origin::Repository(i),
870 &mut self.arena,
871 )
872 .map_err(|e| SessionError::new(e.0))?;
873 found.extend(ids);
874 }
875 }
876 Ok(found)
877 }
878
879 pub fn create_optimized_pool(
880 &mut self,
881 policy: &mut DefaultPolicy,
882 ) -> Result<Pool, SessionError> {
883 let pool = self.create_filtered_pool()?;
884 if std::env::var("COMPOSER_POOL_OPTIMIZER").as_deref() == Ok("0") {
885 return Ok(pool);
886 }
887 Ok(PoolOptimizer::new().optimize(&self.request, &pool, &self.arena, policy))
888 }
889
890 pub fn create_filtered_pool(&mut self) -> Result<Pool, SessionError> {
893 let mut pool = self.create_pool()?;
894 let before = pool.len();
895 let mut warnings = Vec::new();
896 pool = crate::pool_filters::security_advisory_filter(
897 pool,
898 &self.arena,
899 &self.set.repositories,
900 &self.request,
901 &self.policy_config,
902 &mut warnings,
903 )
904 .map_err(|e| SessionError::new(e.0))?;
905 pool = crate::pool_filters::filter_list_filter(
906 pool,
907 &self.arena,
908 &self.set.repositories,
909 &self.request,
910 &self.policy_config,
911 "update",
912 &mut warnings,
913 )
914 .map_err(|e| SessionError::new(e.0))?;
915 pool.warnings.extend(warnings);
916 if std::env::var_os("VIVACITY_TRACE").is_some() {
917 eprintln!(
918 "trace: policy filters {before} → {} package versions ({} removed by lists)",
919 pool.len(),
920 pool.filter_list_removed
921 .values()
922 .map(Vec::len)
923 .sum::<usize>()
924 );
925 }
926 Ok(pool)
927 }
928
929 pub fn solve(
932 &self,
933 pool: &Pool,
934 policy: &mut DefaultPolicy,
935 filter: &PlatformRequirementFilter,
936 ) -> Result<SolveReport, SolveError> {
937 let mut solver = Solver::new(pool, &self.arena);
938 let transaction = solver.solve(&self.request, policy, filter)?;
939 let decisions = solver.decisions.queue.iter().map(|d| d.literal).collect();
940 Ok(SolveReport {
941 learned: solver
942 .rules
943 .ids_of_type(crate::rule::RuleType::Learned)
944 .len(),
945 rules: solver.rule_set_size(),
946 decisions,
947 transaction,
948 })
949 }
950}
951
952pub fn install_policy_problems(
958 project_dir: &Path,
959 composer_home: Option<&Path>,
960 http: Option<HttpTransports>,
961 cache_repo_dir: Option<&Path>,
962 with_dev: bool,
963 no_blocking: bool,
964) -> Result<(Vec<String>, Vec<String>), SessionError> {
965 let manifest_text = std::fs::read_to_string(project_dir.join("composer.json"))
966 .map_err(|e| SessionError::new(format!("composer.json: {e}")))?;
967 let manifest: Value = serde_json::from_str(&manifest_text)
968 .map_err(|e| SessionError::new(format!("composer.json: {e}")))?;
969 let config = MergedConfig::load(&manifest, composer_home)?;
970 let mut policy = crate::policy_config::PolicyConfig::from_raw(&config.policy)
971 .map_err(|e| SessionError::new(e.0))?;
972 policy
973 .apply_no_blocking(no_blocking)
974 .map_err(|e| SessionError::new(e.0))?;
975 if !policy.malware_blocks("install") {
976 return Ok((Vec::new(), Vec::new()));
977 }
978 let lock_text = std::fs::read_to_string(project_dir.join("composer.lock"))
979 .map_err(|e| SessionError::new(format!("composer.lock: {e}")))?;
980 let lock: Value = serde_json::from_str(&lock_text)
981 .map_err(|e| SessionError::new(format!("composer.lock: {e}")))?;
982 let mut repositories: Vec<Repository> = Vec::new();
985 for repo in &config.repositories {
986 if repo.definition.get("type").and_then(Value::as_str) != Some("composer") {
987 continue;
988 }
989 repositories.push(open_repository_with(
992 repo,
993 http.as_ref(),
994 cache_repo_dir,
995 true,
996 )?);
997 }
998 let mut arena: Vec<Package> = Vec::new();
999 let locked = crate::repository::locked_repository_with(&lock, &mut arena, with_dev)
1000 .map_err(|e| SessionError::new(e.0))?;
1001 let mut request = Request::new(Some(locked.clone()));
1002 for &idx in &locked {
1003 request.fix_locked_package(idx);
1004 }
1005 let pool = Pool::new(locked.clone(), Vec::new(), &arena);
1006 let mut warnings = Vec::new();
1007 let pool = crate::pool_filters::filter_list_filter(
1008 pool,
1009 &arena,
1010 &repositories,
1011 &request,
1012 &policy,
1013 "install",
1014 &mut warnings,
1015 )
1016 .map_err(|e| SessionError::new(e.0))?;
1017 let problems: Vec<String> = locked
1018 .iter()
1019 .map(|&idx| &arena[idx])
1020 .filter(|p| pool.is_filter_list_removed(&p.name, &p.version))
1021 .map(|p| crate::pool_filters::locked_removed_problem_text(&pool, p))
1022 .collect();
1023 Ok((problems, warnings))
1024}
1025
1026fn open_repository(
1030 repo: &RepoConfig,
1031 http: Option<&HttpTransports>,
1032 cache_repo_dir: Option<&Path>,
1033 project_dir: &Path,
1034 origin: Origin,
1035 arena: &mut Vec<Package>,
1036) -> Result<Repository, SessionError> {
1037 if repo.definition.get("type").and_then(Value::as_str) == Some("path") {
1038 if repo.definition.get("only").is_some()
1039 || repo.definition.get("exclude").is_some()
1040 || repo.definition.get("canonical").is_some()
1041 {
1042 return Err(SessionError::new(format!(
1043 "repository filters (only/exclude/canonical) are not supported by vivacity update yet ({})",
1044 key_string(&repo.key)
1045 )));
1046 }
1047 let path_repo = crate::path_repo::open(&repo.definition, project_dir, origin, arena)
1048 .map_err(|e| SessionError::new(e.0))?;
1049 return Ok(Repository::Path(path_repo));
1050 }
1051 open_repository_with(repo, http, cache_repo_dir, false)
1052}
1053
1054fn open_repository_with(
1058 repo: &RepoConfig,
1059 http: Option<&HttpTransports>,
1060 cache_repo_dir: Option<&Path>,
1061 for_policies: bool,
1062) -> Result<Repository, SessionError> {
1063 let def = &repo.definition;
1064 let kind = def.get("type").and_then(Value::as_str).ok_or_else(|| {
1065 SessionError::new(format!(
1066 "Repository \"{}\" ({def}) must have a type defined",
1067 key_string(&repo.key)
1068 ))
1069 })?;
1070 if kind != "composer" {
1071 return Err(SessionError::new(format!(
1072 "repository type \"{kind}\" is not supported by vivacity update yet ({})",
1073 key_string(&repo.key)
1074 )));
1075 }
1076 if !for_policies
1077 && (def.get("only").is_some()
1078 || def.get("exclude").is_some()
1079 || def.get("canonical").is_some())
1080 {
1081 return Err(SessionError::new(format!(
1082 "repository filters (only/exclude/canonical) are not supported by vivacity update yet ({})",
1083 key_string(&repo.key)
1084 )));
1085 }
1086 let url = def.get("url").and_then(Value::as_str).ok_or_else(|| {
1087 SessionError::new(format!("repository {} has no url", key_string(&repo.key)))
1088 })?;
1089 let transport: Box<dyn crate::repository::Transport> = if url.starts_with("file://") {
1090 Box::new(FileTransport)
1091 } else if url.starts_with("http://") || url.starts_with("https://") || !url.contains("://") {
1092 match http {
1093 Some(h) => Box::new(HttpTransport {
1094 fetch: h.0.clone(),
1095 fetch_many: h.1.clone(),
1096 post: h.2.clone(),
1097 }),
1098 None => {
1099 return Err(SessionError::new(format!(
1100 "remote composer repositories need a network transport ({url})"
1101 )))
1102 }
1103 }
1104 } else {
1105 return Err(SessionError::new(format!(
1106 "unsupported repository url scheme ({url})"
1107 )));
1108 };
1109 let mut repo = ComposerRepository::open(url, transport).map_err(|e| SessionError::new(e.0))?;
1110 if let Some(options) = def.get("options") {
1111 repo.options = options.clone();
1112 }
1113 repo.set_user_filter(def.get("filter"))
1114 .map_err(|e| SessionError::new(e.0))?;
1115 if for_policies {
1116 repo.set_name_filter(def.get("only"), def.get("exclude"))
1117 .map_err(|e| SessionError::new(e.0))?;
1118 }
1119 if let Some(dir) = cache_repo_dir {
1120 repo.cache = Some(crate::metacache::MetadataCache::new(dir, &repo.url));
1121 }
1122 Ok(Repository::Composer(Box::new(repo)))
1123}
1124
1125fn key_string(key: &RepoKey) -> String {
1126 match key {
1127 RepoKey::Named(n) => n.clone(),
1128 RepoKey::Indexed(i) => i.to_string(),
1129 }
1130}
1131
1132#[cfg(test)]
1133mod tests {
1134 use super::*;
1135 use serde_json::json;
1136
1137 fn names(repos: &[RepoConfig]) -> Vec<String> {
1138 repos.iter().map(|r| key_string(&r.key)).collect()
1139 }
1140
1141 #[test]
1142 fn merges_repositories_like_composer_config() {
1143 let mut cfg = MergedConfig::load(&json!({}), None).unwrap();
1144 assert_eq!(names(&cfg.repositories), vec!["packagist.org"]);
1145 cfg.merge(&json!({"repositories": {"snapshot": {"type": "composer", "url": "file:///s"}, "packagist.org": false}}));
1147 assert_eq!(names(&cfg.repositories), vec!["snapshot"]);
1148 cfg.merge(&json!({"repositories": [{"type": "composer", "url": "https://packages.drupal.org/8"}]}));
1150 assert_eq!(names(&cfg.repositories), vec!["0", "snapshot"]);
1151 cfg.merge(
1154 &json!({"repositories": [{"type": "vcs", "url": "a"}, {"type": "vcs", "url": "b"}]}),
1155 );
1156 assert_eq!(names(&cfg.repositories), vec!["2", "1", "0", "snapshot"]);
1157 assert_eq!(cfg.repositories[0].definition["url"], "a");
1158 assert_eq!(cfg.repositories[1].definition["url"], "b");
1159 assert_eq!(
1160 cfg.repositories[2].definition["url"],
1161 "https://packages.drupal.org/8"
1162 );
1163 }
1164
1165 #[test]
1166 fn packagist_url_disables_default() {
1167 let mut cfg = MergedConfig::load(&json!({}), None).unwrap();
1168 cfg.merge(
1169 &json!({"repositories": [{"type": "composer", "url": "https://repo.packagist.org"}]}),
1170 );
1171 assert_eq!(names(&cfg.repositories), vec!["0"]);
1172 let mut cfg = MergedConfig::load(&json!({}), None).unwrap();
1173 cfg.merge(&json!({"repositories": [{"packagist": false}]}));
1174 assert!(cfg.repositories.is_empty());
1175 }
1176}