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}
257
258impl UpdateOptions {
259 pub fn partial(packages: &[String], transitive: crate::pool::UpdateMode) -> UpdateOptions {
261 let mut allow_list: Vec<String> = Vec::new();
262 for p in packages {
263 let l = p.to_lowercase();
264 if !allow_list.contains(&l) {
265 allow_list.push(l);
266 }
267 }
268 UpdateOptions {
269 allow_list,
270 transitive: Some(transitive),
271 no_blocking: false,
272 }
273 }
274}
275
276pub struct UpdateSession {
278 pub arena: Vec<Package>,
279 pub root: RootPackage,
280 pub fixed_root: usize,
282 pub fixed_root_alias: Option<usize>,
283 pub platform: Vec<usize>,
284 pub locked: Option<Vec<usize>>,
285 pub set: RepositorySet,
286 pub request: Request,
287 pub config: MergedConfig,
288 pub dev_mode: bool,
289 pub prefer_stable: bool,
291 pub prefer_lowest: bool,
292 pub php_version: String,
295 pub policy_config: crate::policy_config::PolicyConfig,
297 pub ini_files: Vec<String>,
300 pub loaded_extensions: BTreeSet<String>,
302 pub installer_dev_mode: bool,
305}
306
307impl UpdateSession {
308 pub fn prepare(
309 project_dir: &Path,
310 composer_home: Option<&Path>,
311 dev_mode: bool,
312 ) -> Result<UpdateSession, SessionError> {
313 Self::prepare_with(project_dir, composer_home, dev_mode, None)
314 }
315
316 pub fn prepare_with(
318 project_dir: &Path,
319 composer_home: Option<&Path>,
320 dev_mode: bool,
321 http: Option<HttpTransports>,
322 ) -> Result<UpdateSession, SessionError> {
323 Self::prepare_full(project_dir, composer_home, dev_mode, http, None)
324 }
325
326 pub fn prepare_full(
329 project_dir: &Path,
330 composer_home: Option<&Path>,
331 dev_mode: bool,
332 http: Option<HttpTransports>,
333 cache_repo_dir: Option<&Path>,
334 ) -> Result<UpdateSession, SessionError> {
335 Self::prepare_update(
336 project_dir,
337 composer_home,
338 dev_mode,
339 http,
340 cache_repo_dir,
341 &UpdateOptions::default(),
342 )
343 }
344
345 pub fn prepare_update(
348 project_dir: &Path,
349 composer_home: Option<&Path>,
350 dev_mode: bool,
351 http: Option<HttpTransports>,
352 cache_repo_dir: Option<&Path>,
353 options: &UpdateOptions,
354 ) -> Result<UpdateSession, SessionError> {
355 let partial_update = !options.allow_list.is_empty();
356 let manifest_path = project_dir.join("composer.json");
357 let manifest_text = std::fs::read_to_string(&manifest_path)
358 .map_err(|e| SessionError::new(format!("{}: {e}", manifest_path.display())))?;
359 let manifest: Value = serde_json::from_str(&manifest_text)
360 .map_err(|e| SessionError::new(format!("{}: {e}", manifest_path.display())))?;
361 let config = MergedConfig::load(&manifest, composer_home)?;
362 let mut policy_config = crate::policy_config::PolicyConfig::from_raw(&config.policy)
363 .map_err(|e| SessionError::new(e.0))?;
364 policy_config
365 .apply_no_blocking(options.no_blocking)
366 .map_err(|e| SessionError::new(e.0))?;
367 let root = RootPackage::load(&manifest, project_dir).map_err(|e| SessionError::new(e.0))?;
368 let probed = probe().map_err(|e| SessionError::new(e.0))?;
369 let php_version = probed
373 .iter()
374 .find(|p| p.get("name").and_then(Value::as_str) == Some("php"))
375 .and_then(|p| p.get("version").and_then(Value::as_str))
376 .map(|v| {
377 v.split(['.', '-', '+'])
378 .take(3)
379 .map(|part| part.trim_end_matches(|c: char| !c.is_ascii_digit()))
380 .collect::<Vec<_>>()
381 .join(".")
382 })
383 .unwrap_or_default();
384 let platform_pkgs =
385 platform_packages(&probed, &config.platform).map_err(|e| SessionError::new(e.0))?;
386
387 let mut arena: Vec<Package> = Vec::new();
388
389 let mut fixed = root.package.clone();
391 fixed.requires = Default::default();
392 fixed.dev_requires = Default::default();
393 arena.push(fixed);
394 let fixed_root = 0;
395 let fixed_root_alias = root.branch_alias.as_ref().map(|(normalized, pretty)| {
396 let a = arena[fixed_root].alias(fixed_root, normalized, pretty);
397 arena.push(a);
398 arena.len() - 1
399 });
400 let root_members: Vec<usize> = fixed_root_alias
401 .into_iter()
402 .chain(std::iter::once(fixed_root))
403 .collect();
404
405 let mut platform: Vec<usize> = Vec::new();
406 for p in platform_pkgs {
407 arena.push(p);
408 platform.push(arena.len() - 1);
409 }
410
411 let lock_path = project_dir.join("composer.lock");
415 let lock: Option<Value> = if lock_path.is_file() {
416 let text = std::fs::read_to_string(&lock_path)
417 .map_err(|e| SessionError::new(format!("{}: {e}", lock_path.display())))?;
418 match serde_json::from_str::<Value>(&text) {
419 Ok(v) if v.get("packages").is_some_and(|p| !p.is_null()) => Some(v),
420 Ok(_) => None,
421 Err(e) => {
422 if partial_update {
423 return Err(SessionError::new(format!(
424 "\"{}\" does not contain valid JSON\n{e}",
425 lock_path.display()
426 )));
427 }
428 None
429 }
430 }
431 } else {
432 None
433 };
434 let locked = match &lock {
435 Some(v) => Some(locked_repository(v, &mut arena).map_err(|e| SessionError::new(e.0))?),
436 None => None,
437 };
438
439 let mut stability_flags = root.stability_flags.clone();
442 let root_version = match fixed_root_alias {
443 Some(a) => arena[a].version.clone(),
444 None => arena[fixed_root].version.clone(),
445 };
446 stability_flags.insert(
447 root.package.name.clone(),
448 stability_rank(parse_stability(&root_version)),
449 );
450
451 let mut root_requires: OrderedMap<Constraint> = OrderedMap::default();
456 let requires = match &root.branch_alias {
457 Some((normalized, pretty)) => {
458 let aliased = root.package.alias(0, normalized, pretty);
459 let mut all = aliased.requires.clone();
460 for l in aliased.dev_requires.iter() {
461 all.insert(l.clone());
462 }
463 all
464 }
465 None => root.all_requires(),
466 };
467 for link in requires.iter() {
468 root_requires.insert(&link.target, link.constraint.clone());
469 }
470
471 let mut set = RepositorySet::new(
472 &root.minimum_stability,
473 stability_flags,
474 &root.aliases,
475 root.references.clone(),
476 root_requires,
477 Default::default(),
478 );
479 set.add_repository(Repository::Root(root_members));
480 set.add_repository(Repository::Platform(platform.clone()));
481 for repo in &config.repositories {
482 set.add_repository(open_repository(repo, http.as_ref(), cache_repo_dir)?);
483 }
484 if let Some(ids) = &locked {
485 set.add_repository(Repository::Locked(ids.clone()));
486 }
487
488 if partial_update && locked.is_none() {
490 return Err(SessionError::new(
491 "Cannot update only a partial set of packages without a lock file present. Run `composer update` to generate a lock file.",
492 ));
493 }
494 let mut request = Request::new(locked.clone());
495 if partial_update {
496 request.set_update_allow_list(
497 options.allow_list.clone(),
498 options
499 .transitive
500 .unwrap_or(crate::pool::UpdateMode::OnlyListed),
501 );
502 }
503 if let Some(a) = fixed_root_alias {
504 request.fix_package(a);
505 }
506 request.fix_package(fixed_root);
507 for &p in &platform {
508 let provided = arena[fixed_root]
509 .provides
510 .get(&arena[p].name)
511 .map(|l| l.constraint.clone());
512 let provided_here = provided
513 .is_some_and(|c| c.matches(&Constraint::new(Op::Eq, arena[p].version.clone())));
514 if !provided_here {
515 request.fix_package(p);
516 }
517 }
518 for link in requires.iter() {
519 request.require_name_pretty(
520 &link.target,
521 Some(link.constraint.clone()),
522 &link.pretty_constraint,
523 )?;
524 }
525
526 Ok(UpdateSession {
527 arena,
528 root,
529 fixed_root,
530 fixed_root_alias,
531 platform,
532 locked,
533 set,
534 request,
535 config,
536 dev_mode,
537 prefer_stable: false,
538 prefer_lowest: false,
539 php_version,
540 policy_config,
541 ini_files: crate::platform::ini_files(&probed),
542 loaded_extensions: crate::platform::loaded_extensions(&probed),
543 installer_dev_mode: true,
544 })
545 }
546
547 pub fn create_pool(&mut self) -> Result<Pool, SessionError> {
548 Ok(self.set.create_pool(&mut self.request, &mut self.arena)?)
549 }
550
551 pub fn policy(&self) -> DefaultPolicy {
553 DefaultPolicy::new(
554 self.prefer_stable || self.root.prefer_stable,
555 self.prefer_lowest,
556 None,
557 )
558 }
559
560 pub fn extract_dev_packages(
564 &mut self,
565 transaction: &mut LockTransaction,
566 policy: &mut DefaultPolicy,
567 filter: &PlatformRequirementFilter,
568 ) -> Result<(), SessionError> {
569 if self.root.package.dev_requires.is_empty() {
570 return Ok(());
571 }
572 let dumps: Vec<Value> = transaction
575 .new_lock_packages(&self.arena, false)
576 .iter()
577 .map(|&idx| Value::Object(dump_package(&self.arena[idx])))
578 .collect();
579 let result_ids =
580 crate::loader::load_packages(&dumps, Origin::Result, &mut self.arena, false)
581 .map_err(|e| SessionError::new(e.0))?;
582 let mut members: Vec<usize> = Vec::new();
585 members.extend(self.fixed_root_alias);
586 members.push(self.fixed_root);
587 members.extend(self.platform.iter().copied());
588 members.extend(result_ids);
589 let mut pool_packages: Vec<usize> = Vec::new();
590 for idx in members {
591 pool_packages.push(idx);
592 let (name, version) = (
593 self.arena[idx].name.clone(),
594 self.arena[idx].version.clone(),
595 );
596 if let Some((alias, alias_normalized)) = self
597 .set
598 .root_aliases
599 .get(&name)
600 .and_then(|m| m.get(&version))
601 {
602 let mut base = idx;
603 while let Some(b) = self.arena[base].alias_of {
604 base = b;
605 }
606 let mut a = self.arena[base].alias(base, alias_normalized, alias);
607 a.root_package_alias = true;
608 a.origin = Origin::Detached;
609 self.arena.push(a);
610 pool_packages.push(self.arena.len() - 1);
611 }
612 }
613 let pool = Pool::new(pool_packages, Vec::new(), &self.arena);
614 let mut request = Request::new(None);
616 if let Some(a) = self.fixed_root_alias {
617 request.fix_package(a);
618 }
619 request.fix_package(self.fixed_root);
620 for &p in &self.platform {
621 let provided = self.arena[self.fixed_root]
622 .provides
623 .get(&self.arena[p].name)
624 .map(|l| l.constraint.clone());
625 let provided_here = provided.is_some_and(|c| {
626 c.matches(&Constraint::new(Op::Eq, self.arena[p].version.clone()))
627 });
628 if !provided_here {
629 request.fix_package(p);
630 }
631 }
632 let requires = match &self.root.branch_alias {
633 Some((normalized, pretty)) => self.root.package.alias(0, normalized, pretty).requires,
634 None => self.root.package.requires.clone(),
635 };
636 for link in requires.iter() {
637 request.require_name_pretty(
638 &link.target,
639 Some(link.constraint.clone()),
640 &link.pretty_constraint,
641 )?;
642 }
643 let mut solver = Solver::new(&pool, &self.arena);
644 let non_dev = match solver.solve(&request, policy, filter) {
645 Ok(t) => t,
646 Err(SolveError::Problems(problems)) => {
647 drop(solver);
648 let pretty = {
651 let mut ctx = crate::problem::MessageContext {
652 arena: &mut self.arena,
653 set: &self.set,
654 pool: &pool,
655 request: &request,
656 is_verbose: false,
657 ini_files: &self.ini_files,
658 loaded_extensions: &self.loaded_extensions,
659 };
660 crate::problem::pretty_string(&mut ctx, &problems, true)
661 };
662 return Err(SessionError {
663 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}"),
664 kind: SessionErrorKind::Unsolvable,
665 });
666 }
667 Err(SolveError::Bug(b)) => return Err(SessionError::new(b)),
668 };
669 transaction.set_non_dev_packages(&self.arena, &non_dev);
670 Ok(())
671 }
672
673 fn platform_requirements(links: &crate::package::Links) -> Map<String, Value> {
675 let mut out = Map::new();
676 for l in links.iter() {
677 if crate::platform::is_platform_package(&l.target) {
678 out.insert(l.target.clone(), Value::String(l.pretty_constraint.clone()));
679 }
680 }
681 out
682 }
683
684 pub fn lock_json(
686 &self,
687 transaction: &LockTransaction,
688 manifest_text: &str,
689 ) -> Result<Value, SessionError> {
690 let content_hash = vivacity_core::content_hash::content_hash(manifest_text)
691 .map_err(|e| SessionError::new(e.to_string()))?;
692 let packages = lock_packages(
693 &self.arena,
694 &transaction.new_lock_packages(&self.arena, false),
695 )
696 .map_err(SessionError::new)?;
697 let packages_dev = lock_packages(
698 &self.arena,
699 &transaction.new_lock_packages(&self.arena, true),
700 )
701 .map_err(SessionError::new)?;
702 let (requires, dev_requires) = match &self.root.branch_alias {
703 Some((normalized, pretty)) => {
704 let a = self.root.package.alias(0, normalized, pretty);
705 (a.requires, a.dev_requires)
706 }
707 None => (
708 self.root.package.requires.clone(),
709 self.root.package.dev_requires.clone(),
710 ),
711 };
712 Ok(lock_data(LockInput {
713 content_hash: &content_hash,
714 packages,
715 packages_dev: Some(packages_dev),
716 platform: Self::platform_requirements(&requires),
717 platform_dev: Self::platform_requirements(&dev_requires),
718 aliases: &transaction.aliases(&self.arena, &self.root.aliases),
719 minimum_stability: &self.root.minimum_stability,
720 stability_flags: &self.root.stability_flags,
721 prefer_stable: self.prefer_stable || self.root.prefer_stable,
722 prefer_lowest: self.prefer_lowest,
723 platform_overrides: &self.config.platform,
724 }))
725 }
726
727 pub fn update(
730 &mut self,
731 manifest_text: &str,
732 filter: &PlatformRequirementFilter,
733 ) -> Result<(Value, SolveReport), SessionError> {
734 let trace = std::env::var_os("VIVACITY_TRACE").is_some();
735 let t = std::time::Instant::now();
736 let lap = |label: &str, t: &std::time::Instant| {
737 if trace {
738 eprintln!(
739 "trace: {label:<22} {:>7.1} ms",
740 t.elapsed().as_secs_f64() * 1000.0
741 );
742 }
743 };
744 let mut policy = self.policy();
745 let pool = self.create_filtered_pool()?;
746 for w in &pool.warnings {
747 eprintln!("Warning: {w}");
748 }
749 lap("pool", &t);
750 let pool = if std::env::var("COMPOSER_POOL_OPTIMIZER").as_deref() == Ok("0") {
751 pool
752 } else {
753 PoolOptimizer::new().optimize(&self.request, &pool, &self.arena, &mut policy)
754 };
755 lap("optimize", &t);
756 let mut report = match self.solve(&pool, &mut policy, filter) {
757 Ok(r) => r,
758 Err(SolveError::Problems(problems)) => {
759 let pretty = self.pretty_problems(&pool, &problems, false);
762 let mut message = format!(
763 "Your requirements could not be resolved to an installable set of packages.\n{pretty}"
764 );
765 if !self.installer_dev_mode {
766 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.");
767 }
768 return Err(SessionError {
769 message,
770 kind: SessionErrorKind::Unsolvable,
771 });
772 }
773 Err(SolveError::Bug(b)) => return Err(SessionError::new(b)),
774 };
775 lap("solve", &t);
776 for &idx in &report.transaction.all {
780 crate::lockfile::validate_package(&self.arena[idx]).map_err(SessionError::new)?;
781 }
782 drop(pool);
783 let mut transaction = std::mem::replace(&mut report.transaction, LockTransaction::empty());
784 self.extract_dev_packages(&mut transaction, &mut policy, filter)?;
785 lap("extract dev", &t);
786 let lock = self.lock_json(&transaction, manifest_text)?;
787 lap("lock data", &t);
788 report.transaction = transaction;
789 Ok((lock, report))
790 }
791
792 pub fn pretty_problems(
795 &mut self,
796 pool: &Pool,
797 problems: &[crate::solver::SolvedProblem],
798 is_dev_extraction: bool,
799 ) -> String {
800 let mut ctx = crate::problem::MessageContext {
801 arena: &mut self.arena,
802 set: &self.set,
803 pool,
804 request: &self.request,
805 is_verbose: false,
806 ini_files: &self.ini_files,
807 loaded_extensions: &self.loaded_extensions,
808 };
809 crate::problem::pretty_string(&mut ctx, problems, is_dev_extraction)
810 }
811
812 pub fn find_packages_for_require(
819 &mut self,
820 name: &str,
821 ignore_stability: bool,
822 ) -> Result<Vec<usize>, SessionError> {
823 let name = name.to_lowercase();
824 let mut acceptable = BTreeMap::new();
825 let min = crate::version::stability_rank(&self.root.minimum_stability);
826 for st in ["stable", "RC", "beta", "alpha", "dev"] {
827 let rank = crate::version::stability_rank(st);
828 if ignore_stability || rank <= min {
829 acceptable.insert(st.to_owned(), rank);
830 }
831 }
832 let flags = BTreeMap::new();
833 let already = BTreeMap::new();
834 let map = vec![(name.clone(), Constraint::MatchAll)];
835 let mut found: Vec<usize> = Vec::new();
836 let (_, ids) = crate::pool::array_repository_load_packages(
837 &self.platform,
838 &map,
839 &acceptable,
840 &flags,
841 &already,
842 &self.arena,
843 );
844 found.extend(ids);
845 for (i, repo) in self.set.repositories.iter().enumerate() {
846 if let Repository::Composer(repo) = repo {
847 let (_, ids) = repo
848 .load_packages(
849 &map,
850 &acceptable,
851 &flags,
852 &already,
853 Origin::Repository(i),
854 &mut self.arena,
855 )
856 .map_err(|e| SessionError::new(e.0))?;
857 found.extend(ids);
858 }
859 }
860 Ok(found)
861 }
862
863 pub fn create_optimized_pool(
864 &mut self,
865 policy: &mut DefaultPolicy,
866 ) -> Result<Pool, SessionError> {
867 let pool = self.create_filtered_pool()?;
868 if std::env::var("COMPOSER_POOL_OPTIMIZER").as_deref() == Ok("0") {
869 return Ok(pool);
870 }
871 Ok(PoolOptimizer::new().optimize(&self.request, &pool, &self.arena, policy))
872 }
873
874 pub fn create_filtered_pool(&mut self) -> Result<Pool, SessionError> {
877 let mut pool = self.create_pool()?;
878 let before = pool.len();
879 let mut warnings = Vec::new();
880 pool = crate::pool_filters::security_advisory_filter(
881 pool,
882 &self.arena,
883 &self.set.repositories,
884 &self.request,
885 &self.policy_config,
886 &mut warnings,
887 )
888 .map_err(|e| SessionError::new(e.0))?;
889 pool = crate::pool_filters::filter_list_filter(
890 pool,
891 &self.arena,
892 &self.set.repositories,
893 &self.request,
894 &self.policy_config,
895 "update",
896 &mut warnings,
897 )
898 .map_err(|e| SessionError::new(e.0))?;
899 pool.warnings.extend(warnings);
900 if std::env::var_os("VIVACITY_TRACE").is_some() {
901 eprintln!(
902 "trace: policy filters {before} → {} package versions ({} removed by lists)",
903 pool.len(),
904 pool.filter_list_removed
905 .values()
906 .map(Vec::len)
907 .sum::<usize>()
908 );
909 }
910 Ok(pool)
911 }
912
913 pub fn solve(
916 &self,
917 pool: &Pool,
918 policy: &mut DefaultPolicy,
919 filter: &PlatformRequirementFilter,
920 ) -> Result<SolveReport, SolveError> {
921 let mut solver = Solver::new(pool, &self.arena);
922 let transaction = solver.solve(&self.request, policy, filter)?;
923 let decisions = solver.decisions.queue.iter().map(|d| d.literal).collect();
924 Ok(SolveReport {
925 learned: solver
926 .rules
927 .ids_of_type(crate::rule::RuleType::Learned)
928 .len(),
929 rules: solver.rule_set_size(),
930 decisions,
931 transaction,
932 })
933 }
934}
935
936pub fn install_policy_problems(
942 project_dir: &Path,
943 composer_home: Option<&Path>,
944 http: Option<HttpTransports>,
945 cache_repo_dir: Option<&Path>,
946 with_dev: bool,
947 no_blocking: bool,
948) -> Result<(Vec<String>, Vec<String>), SessionError> {
949 let manifest_text = std::fs::read_to_string(project_dir.join("composer.json"))
950 .map_err(|e| SessionError::new(format!("composer.json: {e}")))?;
951 let manifest: Value = serde_json::from_str(&manifest_text)
952 .map_err(|e| SessionError::new(format!("composer.json: {e}")))?;
953 let config = MergedConfig::load(&manifest, composer_home)?;
954 let mut policy = crate::policy_config::PolicyConfig::from_raw(&config.policy)
955 .map_err(|e| SessionError::new(e.0))?;
956 policy
957 .apply_no_blocking(no_blocking)
958 .map_err(|e| SessionError::new(e.0))?;
959 if !policy.malware_blocks("install") {
960 return Ok((Vec::new(), Vec::new()));
961 }
962 let lock_text = std::fs::read_to_string(project_dir.join("composer.lock"))
963 .map_err(|e| SessionError::new(format!("composer.lock: {e}")))?;
964 let lock: Value = serde_json::from_str(&lock_text)
965 .map_err(|e| SessionError::new(format!("composer.lock: {e}")))?;
966 let mut repositories: Vec<Repository> = Vec::new();
969 for repo in &config.repositories {
970 if repo.definition.get("type").and_then(Value::as_str) != Some("composer") {
971 continue;
972 }
973 repositories.push(open_repository_with(
976 repo,
977 http.as_ref(),
978 cache_repo_dir,
979 true,
980 )?);
981 }
982 let mut arena: Vec<Package> = Vec::new();
983 let locked = crate::repository::locked_repository_with(&lock, &mut arena, with_dev)
984 .map_err(|e| SessionError::new(e.0))?;
985 let mut request = Request::new(Some(locked.clone()));
986 for &idx in &locked {
987 request.fix_locked_package(idx);
988 }
989 let pool = Pool::new(locked.clone(), Vec::new(), &arena);
990 let mut warnings = Vec::new();
991 let pool = crate::pool_filters::filter_list_filter(
992 pool,
993 &arena,
994 &repositories,
995 &request,
996 &policy,
997 "install",
998 &mut warnings,
999 )
1000 .map_err(|e| SessionError::new(e.0))?;
1001 let problems: Vec<String> = locked
1002 .iter()
1003 .map(|&idx| &arena[idx])
1004 .filter(|p| pool.is_filter_list_removed(&p.name, &p.version))
1005 .map(|p| crate::pool_filters::locked_removed_problem_text(&pool, p))
1006 .collect();
1007 Ok((problems, warnings))
1008}
1009
1010fn open_repository(
1013 repo: &RepoConfig,
1014 http: Option<&HttpTransports>,
1015 cache_repo_dir: Option<&Path>,
1016) -> Result<Repository, SessionError> {
1017 open_repository_with(repo, http, cache_repo_dir, false)
1018}
1019
1020fn open_repository_with(
1024 repo: &RepoConfig,
1025 http: Option<&HttpTransports>,
1026 cache_repo_dir: Option<&Path>,
1027 for_policies: bool,
1028) -> Result<Repository, SessionError> {
1029 let def = &repo.definition;
1030 let kind = def.get("type").and_then(Value::as_str).ok_or_else(|| {
1031 SessionError::new(format!(
1032 "Repository \"{}\" ({def}) must have a type defined",
1033 key_string(&repo.key)
1034 ))
1035 })?;
1036 if kind != "composer" {
1037 return Err(SessionError::new(format!(
1038 "repository type \"{kind}\" is not supported by vivacity update yet ({})",
1039 key_string(&repo.key)
1040 )));
1041 }
1042 if !for_policies
1043 && (def.get("only").is_some()
1044 || def.get("exclude").is_some()
1045 || def.get("canonical").is_some())
1046 {
1047 return Err(SessionError::new(format!(
1048 "repository filters (only/exclude/canonical) are not supported by vivacity update yet ({})",
1049 key_string(&repo.key)
1050 )));
1051 }
1052 let url = def.get("url").and_then(Value::as_str).ok_or_else(|| {
1053 SessionError::new(format!("repository {} has no url", key_string(&repo.key)))
1054 })?;
1055 let transport: Box<dyn crate::repository::Transport> = if url.starts_with("file://") {
1056 Box::new(FileTransport)
1057 } else if url.starts_with("http://") || url.starts_with("https://") || !url.contains("://") {
1058 match http {
1059 Some(h) => Box::new(HttpTransport {
1060 fetch: h.0.clone(),
1061 fetch_many: h.1.clone(),
1062 post: h.2.clone(),
1063 }),
1064 None => {
1065 return Err(SessionError::new(format!(
1066 "remote composer repositories need a network transport ({url})"
1067 )))
1068 }
1069 }
1070 } else {
1071 return Err(SessionError::new(format!(
1072 "unsupported repository url scheme ({url})"
1073 )));
1074 };
1075 let mut repo = ComposerRepository::open(url, transport).map_err(|e| SessionError::new(e.0))?;
1076 if let Some(options) = def.get("options") {
1077 repo.options = options.clone();
1078 }
1079 repo.set_user_filter(def.get("filter"))
1080 .map_err(|e| SessionError::new(e.0))?;
1081 if for_policies {
1082 repo.set_name_filter(def.get("only"), def.get("exclude"))
1083 .map_err(|e| SessionError::new(e.0))?;
1084 }
1085 if let Some(dir) = cache_repo_dir {
1086 repo.cache = Some(crate::metacache::MetadataCache::new(dir, &repo.url));
1087 }
1088 Ok(Repository::Composer(Box::new(repo)))
1089}
1090
1091fn key_string(key: &RepoKey) -> String {
1092 match key {
1093 RepoKey::Named(n) => n.clone(),
1094 RepoKey::Indexed(i) => i.to_string(),
1095 }
1096}
1097
1098#[cfg(test)]
1099mod tests {
1100 use super::*;
1101 use serde_json::json;
1102
1103 fn names(repos: &[RepoConfig]) -> Vec<String> {
1104 repos.iter().map(|r| key_string(&r.key)).collect()
1105 }
1106
1107 #[test]
1108 fn merges_repositories_like_composer_config() {
1109 let mut cfg = MergedConfig::load(&json!({}), None).unwrap();
1110 assert_eq!(names(&cfg.repositories), vec!["packagist.org"]);
1111 cfg.merge(&json!({"repositories": {"snapshot": {"type": "composer", "url": "file:///s"}, "packagist.org": false}}));
1113 assert_eq!(names(&cfg.repositories), vec!["snapshot"]);
1114 cfg.merge(&json!({"repositories": [{"type": "composer", "url": "https://packages.drupal.org/8"}]}));
1116 assert_eq!(names(&cfg.repositories), vec!["0", "snapshot"]);
1117 cfg.merge(
1120 &json!({"repositories": [{"type": "vcs", "url": "a"}, {"type": "vcs", "url": "b"}]}),
1121 );
1122 assert_eq!(names(&cfg.repositories), vec!["2", "1", "0", "snapshot"]);
1123 assert_eq!(cfg.repositories[0].definition["url"], "a");
1124 assert_eq!(cfg.repositories[1].definition["url"], "b");
1125 assert_eq!(
1126 cfg.repositories[2].definition["url"],
1127 "https://packages.drupal.org/8"
1128 );
1129 }
1130
1131 #[test]
1132 fn packagist_url_disables_default() {
1133 let mut cfg = MergedConfig::load(&json!({}), None).unwrap();
1134 cfg.merge(
1135 &json!({"repositories": [{"type": "composer", "url": "https://repo.packagist.org"}]}),
1136 );
1137 assert_eq!(names(&cfg.repositories), vec!["0"]);
1138 let mut cfg = MergedConfig::load(&json!({}), None).unwrap();
1139 cfg.merge(&json!({"repositories": [{"packagist": false}]}));
1140 assert!(cfg.repositories.is_empty());
1141 }
1142}