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 pub merged: Option<crate::merge_plugin::Merged>,
263}
264
265impl UpdateOptions {
266 pub fn partial(packages: &[String], transitive: crate::pool::UpdateMode) -> UpdateOptions {
268 let mut allow_list: Vec<String> = Vec::new();
269 for p in packages {
270 let l = p.to_lowercase();
271 if !allow_list.contains(&l) {
272 allow_list.push(l);
273 }
274 }
275 UpdateOptions {
276 allow_list,
277 transitive: Some(transitive),
278 ..Default::default()
279 }
280 }
281}
282
283#[derive(Debug, Clone)]
286pub struct FlexSession {
287 pub symfony_require: String,
288 pub symfony: Constraint,
289 pub versions: crate::flex_filter::FlexVersions,
290}
291
292pub struct UpdateSession {
293 pub flex: Option<FlexSession>,
295 pub arena: Vec<Package>,
296 pub root: RootPackage,
297 pub fixed_root: usize,
299 pub fixed_root_alias: Option<usize>,
300 pub platform: Vec<usize>,
301 pub locked: Option<Vec<usize>>,
302 pub set: RepositorySet,
303 pub request: Request,
304 pub config: MergedConfig,
305 pub dev_mode: bool,
306 pub prefer_stable: bool,
308 pub prefer_lowest: bool,
309 pub php_version: String,
312 pub policy_config: crate::policy_config::PolicyConfig,
314 pub ini_files: Vec<String>,
317 pub loaded_extensions: BTreeSet<String>,
319 pub installer_dev_mode: bool,
322}
323
324impl UpdateSession {
325 pub fn prepare(
326 project_dir: &Path,
327 composer_home: Option<&Path>,
328 dev_mode: bool,
329 ) -> Result<UpdateSession, SessionError> {
330 Self::prepare_with(project_dir, composer_home, dev_mode, None)
331 }
332
333 pub fn prepare_with(
335 project_dir: &Path,
336 composer_home: Option<&Path>,
337 dev_mode: bool,
338 http: Option<HttpTransports>,
339 ) -> Result<UpdateSession, SessionError> {
340 Self::prepare_full(project_dir, composer_home, dev_mode, http, None)
341 }
342
343 pub fn prepare_full(
346 project_dir: &Path,
347 composer_home: Option<&Path>,
348 dev_mode: bool,
349 http: Option<HttpTransports>,
350 cache_repo_dir: Option<&Path>,
351 ) -> Result<UpdateSession, SessionError> {
352 Self::prepare_update(
353 project_dir,
354 composer_home,
355 dev_mode,
356 http,
357 cache_repo_dir,
358 &UpdateOptions::default(),
359 )
360 }
361
362 pub fn prepare_update(
365 project_dir: &Path,
366 composer_home: Option<&Path>,
367 dev_mode: bool,
368 http: Option<HttpTransports>,
369 cache_repo_dir: Option<&Path>,
370 options: &UpdateOptions,
371 ) -> Result<UpdateSession, SessionError> {
372 let partial_update = !options.allow_list.is_empty();
373 let manifest_path = project_dir.join("composer.json");
374 let manifest_text = std::fs::read_to_string(&manifest_path)
375 .map_err(|e| SessionError::new(format!("{}: {e}", manifest_path.display())))?;
376 let manifest: Value = serde_json::from_str(&manifest_text)
377 .map_err(|e| SessionError::new(format!("{}: {e}", manifest_path.display())))?;
378 let config = MergedConfig::load(&manifest, composer_home)?;
379 let mut policy_config = crate::policy_config::PolicyConfig::from_raw(&config.policy)
380 .map_err(|e| SessionError::new(e.0))?;
381 policy_config
382 .apply_no_blocking(options.no_blocking)
383 .map_err(|e| SessionError::new(e.0))?;
384 let mut root =
385 RootPackage::load(&manifest, project_dir).map_err(|e| SessionError::new(e.0))?;
386 if let Some(merged) = &options.merged {
387 let flags = std::mem::take(&mut root.stability_flags);
394 root = RootPackage::load(&merged.manifest, project_dir)
395 .map_err(|e| SessionError::new(e.0))?;
396 root.stability_flags = flags;
397 for include in &merged.include_links {
400 crate::root::merge_plugin_stability_flags(
401 &mut root.stability_flags,
402 &root.minimum_stability,
403 &include.require,
404 );
405 }
406 for include in &merged.include_links {
407 crate::root::merge_plugin_stability_flags(
408 &mut root.stability_flags,
409 &root.minimum_stability,
410 &include.require_dev,
411 );
412 }
413 }
414 if let Some(patch) = &options.root_patch {
415 root.apply_patch(patch)
416 .map_err(|e| SessionError::new(e.0))?;
417 }
418 let probed = probe().map_err(|e| SessionError::new(e.0))?;
419 let php_version = probed
423 .iter()
424 .find(|p| p.get("name").and_then(Value::as_str) == Some("php"))
425 .and_then(|p| p.get("version").and_then(Value::as_str))
426 .map(|v| {
427 v.split(['.', '-', '+'])
428 .take(3)
429 .map(|part| part.trim_end_matches(|c: char| !c.is_ascii_digit()))
430 .collect::<Vec<_>>()
431 .join(".")
432 })
433 .unwrap_or_default();
434 let platform_pkgs =
435 platform_packages(&probed, &config.platform).map_err(|e| SessionError::new(e.0))?;
436
437 let mut arena: Vec<Package> = Vec::new();
438
439 let mut fixed = root.package.clone();
441 fixed.requires = Default::default();
442 fixed.dev_requires = Default::default();
443 arena.push(fixed);
444 let fixed_root = 0;
445 let fixed_root_alias = root.branch_alias.as_ref().map(|(normalized, pretty)| {
446 let a = arena[fixed_root].alias(fixed_root, normalized, pretty);
447 arena.push(a);
448 arena.len() - 1
449 });
450 let root_members: Vec<usize> = fixed_root_alias
451 .into_iter()
452 .chain(std::iter::once(fixed_root))
453 .collect();
454
455 let mut platform: Vec<usize> = Vec::new();
456 for p in platform_pkgs {
457 arena.push(p);
458 platform.push(arena.len() - 1);
459 }
460
461 let lock_path = project_dir.join("composer.lock");
465 let lock: Option<Value> = if lock_path.is_file() {
466 let text = std::fs::read_to_string(&lock_path)
467 .map_err(|e| SessionError::new(format!("{}: {e}", lock_path.display())))?;
468 match serde_json::from_str::<Value>(&text) {
469 Ok(v) if v.get("packages").is_some_and(|p| !p.is_null()) => Some(v),
470 Ok(_) => None,
471 Err(e) => {
472 if partial_update {
473 return Err(SessionError::new(format!(
474 "\"{}\" does not contain valid JSON\n{e}",
475 lock_path.display()
476 )));
477 }
478 None
479 }
480 }
481 } else {
482 None
483 };
484 let locked = match &lock {
485 Some(v) => Some(locked_repository(v, &mut arena).map_err(|e| SessionError::new(e.0))?),
486 None => None,
487 };
488
489 let mut stability_flags = root.stability_flags.clone();
492 let root_version = match fixed_root_alias {
493 Some(a) => arena[a].version.clone(),
494 None => arena[fixed_root].version.clone(),
495 };
496 stability_flags.insert(
497 root.package.name.clone(),
498 stability_rank(parse_stability(&root_version)),
499 );
500
501 let mut root_requires: OrderedMap<Constraint> = OrderedMap::default();
506 let requires = match &root.branch_alias {
507 Some((normalized, pretty)) => {
508 let aliased = root.package.alias(0, normalized, pretty);
509 let mut all = aliased.requires.clone();
510 for l in aliased.dev_requires.iter() {
511 all.insert(l.clone());
512 }
513 all
514 }
515 None => root.all_requires(),
516 };
517 for link in requires.iter() {
518 root_requires.insert(&link.target, link.constraint.clone());
519 }
520
521 let mut set = RepositorySet::new(
522 &root.minimum_stability,
523 stability_flags,
524 &root.aliases,
525 root.references.clone(),
526 root_requires,
527 Default::default(),
528 );
529 set.add_repository(Repository::Root(root_members));
530 set.add_repository(Repository::Platform(platform.clone()));
531 for repo in &config.repositories {
532 let origin = Origin::Repository(set.repositories.len());
533 set.add_repository(open_repository(
534 repo,
535 http.as_ref(),
536 cache_repo_dir,
537 project_dir,
538 origin,
539 &mut arena,
540 )?);
541 }
542 if let Some(ids) = &locked {
543 set.add_repository(Repository::Locked(ids.clone()));
544 }
545
546 if partial_update && locked.is_none() {
548 return Err(SessionError::new(
549 "Cannot update only a partial set of packages without a lock file present. Run `composer update` to generate a lock file.",
550 ));
551 }
552 let mut request = Request::new(locked.clone());
553 if partial_update {
554 request.set_update_allow_list(
555 options.allow_list.clone(),
556 options
557 .transitive
558 .unwrap_or(crate::pool::UpdateMode::OnlyListed),
559 );
560 }
561 if let Some(a) = fixed_root_alias {
562 request.fix_package(a);
563 }
564 request.fix_package(fixed_root);
565 for &p in &platform {
566 let provided = arena[fixed_root]
567 .provides
568 .get(&arena[p].name)
569 .map(|l| l.constraint.clone());
570 let provided_here = provided
571 .is_some_and(|c| c.matches(&Constraint::new(Op::Eq, arena[p].version.clone())));
572 if !provided_here {
573 request.fix_package(p);
574 }
575 }
576 for link in requires.iter() {
577 request.require_name_pretty(
578 &link.target,
579 Some(link.constraint.clone()),
580 &link.pretty_constraint,
581 )?;
582 }
583
584 Ok(UpdateSession {
585 flex: None,
586 arena,
587 root,
588 fixed_root,
589 fixed_root_alias,
590 platform,
591 locked,
592 set,
593 request,
594 config,
595 dev_mode,
596 prefer_stable: false,
597 prefer_lowest: false,
598 php_version,
599 policy_config,
600 ini_files: crate::platform::ini_files(&probed),
601 loaded_extensions: crate::platform::loaded_extensions(&probed),
602 installer_dev_mode: true,
603 })
604 }
605
606 pub fn create_pool(&mut self) -> Result<Pool, SessionError> {
607 let pre_pool = self.flex.as_ref().map(|f| {
608 let mut root_constraints = BTreeMap::new();
611 for link in self
612 .root
613 .package
614 .requires
615 .0
616 .iter()
617 .chain(self.root.package.dev_requires.0.iter())
618 {
619 root_constraints
620 .entry(link.target.clone())
621 .or_insert_with(|| link.constraint.clone());
622 }
623 crate::pool::PrePoolFilter {
624 symfony_require: f.symfony_require.clone(),
625 symfony: f.symfony.clone(),
626 root_constraints,
627 versions: f.versions.clone(),
628 }
629 });
630 Ok(self
631 .set
632 .create_pool(&mut self.request, &mut self.arena, pre_pool.as_ref())?)
633 }
634
635 pub fn policy(&self) -> DefaultPolicy {
639 let mut policy = DefaultPolicy::new(
640 self.prefer_stable || self.root.prefer_stable,
641 self.prefer_lowest,
642 None,
643 );
644 if self.flex.is_some() {
645 policy.prefer_dev_over_prerelease = true;
646 }
647 policy
648 }
649
650 pub fn extract_dev_packages(
654 &mut self,
655 transaction: &mut LockTransaction,
656 policy: &mut DefaultPolicy,
657 filter: &PlatformRequirementFilter,
658 ) -> Result<(), SessionError> {
659 if self.root.package.dev_requires.is_empty() {
660 return Ok(());
661 }
662 let dumps: Vec<Value> = transaction
665 .new_lock_packages(&self.arena, false)
666 .iter()
667 .map(|&idx| Value::Object(dump_package(&self.arena[idx])))
668 .collect();
669 let result_ids =
670 crate::loader::load_packages(&dumps, Origin::Result, &mut self.arena, false)
671 .map_err(|e| SessionError::new(e.0))?;
672 let mut members: Vec<usize> = Vec::new();
675 members.extend(self.fixed_root_alias);
676 members.push(self.fixed_root);
677 members.extend(self.platform.iter().copied());
678 members.extend(result_ids);
679 let mut pool_packages: Vec<usize> = Vec::new();
680 for idx in members {
681 pool_packages.push(idx);
682 let (name, version) = (
683 self.arena[idx].name.clone(),
684 self.arena[idx].version.clone(),
685 );
686 if let Some((alias, alias_normalized)) = self
687 .set
688 .root_aliases
689 .get(&name)
690 .and_then(|m| m.get(&version))
691 {
692 let mut base = idx;
693 while let Some(b) = self.arena[base].alias_of {
694 base = b;
695 }
696 let mut a = self.arena[base].alias(base, alias_normalized, alias);
697 a.root_package_alias = true;
698 a.origin = Origin::Detached;
699 self.arena.push(a);
700 pool_packages.push(self.arena.len() - 1);
701 }
702 }
703 let pool = Pool::new(pool_packages, Vec::new(), &self.arena);
704 let mut request = Request::new(None);
706 if let Some(a) = self.fixed_root_alias {
707 request.fix_package(a);
708 }
709 request.fix_package(self.fixed_root);
710 for &p in &self.platform {
711 let provided = self.arena[self.fixed_root]
712 .provides
713 .get(&self.arena[p].name)
714 .map(|l| l.constraint.clone());
715 let provided_here = provided.is_some_and(|c| {
716 c.matches(&Constraint::new(Op::Eq, self.arena[p].version.clone()))
717 });
718 if !provided_here {
719 request.fix_package(p);
720 }
721 }
722 let requires = match &self.root.branch_alias {
723 Some((normalized, pretty)) => self.root.package.alias(0, normalized, pretty).requires,
724 None => self.root.package.requires.clone(),
725 };
726 for link in requires.iter() {
727 request.require_name_pretty(
728 &link.target,
729 Some(link.constraint.clone()),
730 &link.pretty_constraint,
731 )?;
732 }
733 let mut solver = Solver::new(&pool, &self.arena);
734 let non_dev = match solver.solve(&request, policy, filter) {
735 Ok(t) => t,
736 Err(SolveError::Problems(problems)) => {
737 drop(solver);
738 let pretty = {
741 let mut ctx = crate::problem::MessageContext {
742 arena: &mut self.arena,
743 set: &self.set,
744 pool: &pool,
745 request: &request,
746 is_verbose: false,
747 ini_files: &self.ini_files,
748 loaded_extensions: &self.loaded_extensions,
749 };
750 crate::problem::pretty_string(&mut ctx, &problems, true)
751 };
752 return Err(SessionError {
753 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}"),
754 kind: SessionErrorKind::Unsolvable,
755 });
756 }
757 Err(SolveError::Bug(b)) => return Err(SessionError::new(b)),
758 };
759 transaction.set_non_dev_packages(&self.arena, &non_dev);
760 Ok(())
761 }
762
763 fn platform_requirements(links: &crate::package::Links) -> Map<String, Value> {
765 let mut out = Map::new();
766 for l in links.iter() {
767 if crate::platform::is_platform_package(&l.target) {
768 out.insert(l.target.clone(), Value::String(l.pretty_constraint.clone()));
769 }
770 }
771 out
772 }
773
774 pub fn lock_json(
776 &self,
777 transaction: &LockTransaction,
778 manifest_text: &str,
779 ) -> Result<Value, SessionError> {
780 let content_hash = vivacity_core::content_hash::content_hash(manifest_text)
781 .map_err(|e| SessionError::new(e.to_string()))?;
782 let packages = lock_packages(
783 &self.arena,
784 &transaction.new_lock_packages(&self.arena, false),
785 )
786 .map_err(SessionError::new)?;
787 let packages_dev = lock_packages(
788 &self.arena,
789 &transaction.new_lock_packages(&self.arena, true),
790 )
791 .map_err(SessionError::new)?;
792 let (requires, dev_requires) = match &self.root.branch_alias {
793 Some((normalized, pretty)) => {
794 let a = self.root.package.alias(0, normalized, pretty);
795 (a.requires, a.dev_requires)
796 }
797 None => (
798 self.root.package.requires.clone(),
799 self.root.package.dev_requires.clone(),
800 ),
801 };
802 Ok(lock_data(LockInput {
803 content_hash: &content_hash,
804 packages,
805 packages_dev: Some(packages_dev),
806 platform: Self::platform_requirements(&requires),
807 platform_dev: Self::platform_requirements(&dev_requires),
808 aliases: &transaction.aliases(&self.arena, &self.root.aliases),
809 minimum_stability: &self.root.minimum_stability,
810 stability_flags: &self.root.stability_flags,
811 prefer_stable: self.prefer_stable || self.root.prefer_stable,
812 prefer_lowest: self.prefer_lowest,
813 platform_overrides: &self.config.platform,
814 }))
815 }
816
817 pub fn update(
820 &mut self,
821 manifest_text: &str,
822 filter: &PlatformRequirementFilter,
823 ) -> Result<(Value, SolveReport), SessionError> {
824 let trace = std::env::var_os("VIVACITY_TRACE").is_some();
825 let t = std::time::Instant::now();
826 let lap = |label: &str, t: &std::time::Instant| {
827 if trace {
828 eprintln!(
829 "trace: {label:<22} {:>7.1} ms",
830 t.elapsed().as_secs_f64() * 1000.0
831 );
832 }
833 };
834 let mut policy = self.policy();
835 let pool = self.create_filtered_pool()?;
836 if let Some(notice) = &pool.flex_notice {
840 eprintln!("{notice}");
841 }
842 eprintln!("Updating dependencies");
843 for w in &pool.warnings {
844 eprintln!("{w}");
845 }
846 lap("pool", &t);
847 let pool = if std::env::var("COMPOSER_POOL_OPTIMIZER").as_deref() == Ok("0") {
848 pool
849 } else {
850 PoolOptimizer::new().optimize(&self.request, &pool, &self.arena, &mut policy)
851 };
852 lap("optimize", &t);
853 let mut report = match self.solve(&pool, &mut policy, filter) {
854 Ok(r) => r,
855 Err(SolveError::Problems(problems)) => {
856 let pretty = self.pretty_problems(&pool, &problems, false);
859 let mut message = format!(
860 "Your requirements could not be resolved to an installable set of packages.\n{pretty}"
861 );
862 if !self.installer_dev_mode {
863 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.");
864 }
865 return Err(SessionError {
866 message,
867 kind: SessionErrorKind::Unsolvable,
868 });
869 }
870 Err(SolveError::Bug(b)) => return Err(SessionError::new(b)),
871 };
872 lap("solve", &t);
873 for &idx in &report.transaction.all {
877 crate::lockfile::validate_package(&self.arena[idx]).map_err(SessionError::new)?;
878 }
879 drop(pool);
880 let mut transaction = std::mem::replace(&mut report.transaction, LockTransaction::empty());
881 self.extract_dev_packages(&mut transaction, &mut policy, filter)?;
882 lap("extract dev", &t);
883 let lock = self.lock_json(&transaction, manifest_text)?;
884 lap("lock data", &t);
885 report.transaction = transaction;
886 Ok((lock, report))
887 }
888
889 pub fn pretty_problems(
892 &mut self,
893 pool: &Pool,
894 problems: &[crate::solver::SolvedProblem],
895 is_dev_extraction: bool,
896 ) -> String {
897 let mut ctx = crate::problem::MessageContext {
898 arena: &mut self.arena,
899 set: &self.set,
900 pool,
901 request: &self.request,
902 is_verbose: false,
903 ini_files: &self.ini_files,
904 loaded_extensions: &self.loaded_extensions,
905 };
906 crate::problem::pretty_string(&mut ctx, problems, is_dev_extraction)
907 }
908
909 pub fn find_packages_for_require(
916 &mut self,
917 name: &str,
918 ignore_stability: bool,
919 ) -> Result<Vec<usize>, SessionError> {
920 let name = name.to_lowercase();
921 let mut acceptable = BTreeMap::new();
922 let min = crate::version::stability_rank(&self.root.minimum_stability);
923 for st in ["stable", "RC", "beta", "alpha", "dev"] {
924 let rank = crate::version::stability_rank(st);
925 if ignore_stability || rank <= min {
926 acceptable.insert(st.to_owned(), rank);
927 }
928 }
929 let flags = BTreeMap::new();
930 let already = BTreeMap::new();
931 let map = vec![(name.clone(), Constraint::MatchAll)];
932 let mut found: Vec<usize> = Vec::new();
933 let (_, ids) = crate::pool::array_repository_load_packages(
934 &self.platform,
935 &map,
936 &acceptable,
937 &flags,
938 &already,
939 &self.arena,
940 );
941 found.extend(ids);
942 for (i, repo) in self.set.repositories.iter().enumerate() {
943 if let Repository::Composer(repo) = repo {
944 let (_, ids) = repo
945 .load_packages(
946 &map,
947 &acceptable,
948 &flags,
949 &already,
950 Origin::Repository(i),
951 &mut self.arena,
952 )
953 .map_err(|e| SessionError::new(e.0))?;
954 found.extend(ids);
955 }
956 }
957 Ok(found)
958 }
959
960 pub fn create_optimized_pool(
961 &mut self,
962 policy: &mut DefaultPolicy,
963 ) -> Result<Pool, SessionError> {
964 let pool = self.create_filtered_pool()?;
965 if std::env::var("COMPOSER_POOL_OPTIMIZER").as_deref() == Ok("0") {
966 return Ok(pool);
967 }
968 Ok(PoolOptimizer::new().optimize(&self.request, &pool, &self.arena, policy))
969 }
970
971 pub fn create_filtered_pool(&mut self) -> Result<Pool, SessionError> {
974 let mut pool = self.create_pool()?;
975 let before = pool.len();
976 let flex_notice = pool.flex_notice.take();
979 let mut warnings = Vec::new();
980 pool = crate::pool_filters::security_advisory_filter(
981 pool,
982 &self.arena,
983 &self.set.repositories,
984 &self.request,
985 &self.policy_config,
986 &mut warnings,
987 )
988 .map_err(|e| SessionError::new(e.0))?;
989 pool = crate::pool_filters::filter_list_filter(
990 pool,
991 &self.arena,
992 &self.set.repositories,
993 &self.request,
994 &self.policy_config,
995 "update",
996 &mut warnings,
997 )
998 .map_err(|e| SessionError::new(e.0))?;
999 pool.warnings.extend(warnings);
1000 pool.flex_notice = flex_notice;
1001 if std::env::var_os("VIVACITY_TRACE").is_some() {
1002 eprintln!(
1003 "trace: policy filters {before} → {} package versions ({} removed by lists)",
1004 pool.len(),
1005 pool.filter_list_removed
1006 .values()
1007 .map(Vec::len)
1008 .sum::<usize>()
1009 );
1010 }
1011 Ok(pool)
1012 }
1013
1014 pub fn solve(
1017 &self,
1018 pool: &Pool,
1019 policy: &mut DefaultPolicy,
1020 filter: &PlatformRequirementFilter,
1021 ) -> Result<SolveReport, SolveError> {
1022 let mut solver = Solver::new(pool, &self.arena);
1023 let transaction = solver.solve(&self.request, policy, filter)?;
1024 let decisions = solver.decisions.queue.iter().map(|d| d.literal).collect();
1025 Ok(SolveReport {
1026 learned: solver
1027 .rules
1028 .ids_of_type(crate::rule::RuleType::Learned)
1029 .len(),
1030 rules: solver.rule_set_size(),
1031 decisions,
1032 transaction,
1033 })
1034 }
1035}
1036
1037pub fn install_policy_problems(
1043 project_dir: &Path,
1044 composer_home: Option<&Path>,
1045 http: Option<HttpTransports>,
1046 cache_repo_dir: Option<&Path>,
1047 with_dev: bool,
1048 no_blocking: bool,
1049) -> Result<(Vec<String>, Vec<String>), SessionError> {
1050 let manifest_text = std::fs::read_to_string(project_dir.join("composer.json"))
1051 .map_err(|e| SessionError::new(format!("composer.json: {e}")))?;
1052 let manifest: Value = serde_json::from_str(&manifest_text)
1053 .map_err(|e| SessionError::new(format!("composer.json: {e}")))?;
1054 let config = MergedConfig::load(&manifest, composer_home)?;
1055 let mut policy = crate::policy_config::PolicyConfig::from_raw(&config.policy)
1056 .map_err(|e| SessionError::new(e.0))?;
1057 policy
1058 .apply_no_blocking(no_blocking)
1059 .map_err(|e| SessionError::new(e.0))?;
1060 if !policy.malware_blocks("install") {
1061 return Ok((Vec::new(), Vec::new()));
1062 }
1063 let lock_text = std::fs::read_to_string(project_dir.join("composer.lock"))
1064 .map_err(|e| SessionError::new(format!("composer.lock: {e}")))?;
1065 let lock: Value = serde_json::from_str(&lock_text)
1066 .map_err(|e| SessionError::new(format!("composer.lock: {e}")))?;
1067 let mut repositories: Vec<Repository> = Vec::new();
1070 for repo in &config.repositories {
1071 if repo.definition.get("type").and_then(Value::as_str) != Some("composer") {
1072 continue;
1073 }
1074 repositories.push(open_repository_with(
1077 repo,
1078 http.as_ref(),
1079 cache_repo_dir,
1080 true,
1081 )?);
1082 }
1083 let mut arena: Vec<Package> = Vec::new();
1084 let locked = crate::repository::locked_repository_with(&lock, &mut arena, with_dev)
1085 .map_err(|e| SessionError::new(e.0))?;
1086 let mut request = Request::new(Some(locked.clone()));
1087 for &idx in &locked {
1088 request.fix_locked_package(idx);
1089 }
1090 let pool = Pool::new(locked.clone(), Vec::new(), &arena);
1091 let mut warnings = Vec::new();
1092 let pool = crate::pool_filters::filter_list_filter(
1093 pool,
1094 &arena,
1095 &repositories,
1096 &request,
1097 &policy,
1098 "install",
1099 &mut warnings,
1100 )
1101 .map_err(|e| SessionError::new(e.0))?;
1102 let problems: Vec<String> = locked
1103 .iter()
1104 .map(|&idx| &arena[idx])
1105 .filter(|p| pool.is_filter_list_removed(&p.name, &p.version))
1106 .map(|p| crate::pool_filters::locked_removed_problem_text(&pool, p))
1107 .collect();
1108 Ok((problems, warnings))
1109}
1110
1111fn open_repository(
1115 repo: &RepoConfig,
1116 http: Option<&HttpTransports>,
1117 cache_repo_dir: Option<&Path>,
1118 project_dir: &Path,
1119 origin: Origin,
1120 arena: &mut Vec<Package>,
1121) -> Result<Repository, SessionError> {
1122 if repo.definition.get("type").and_then(Value::as_str) == Some("path") {
1123 if repo.definition.get("only").is_some()
1124 || repo.definition.get("exclude").is_some()
1125 || repo.definition.get("canonical").is_some()
1126 {
1127 return Err(SessionError::new(format!(
1128 "repository filters (only/exclude/canonical) are not supported by vivacity update yet ({})",
1129 key_string(&repo.key)
1130 )));
1131 }
1132 let path_repo = crate::path_repo::open(&repo.definition, project_dir, origin, arena)
1133 .map_err(|e| SessionError::new(e.0))?;
1134 return Ok(Repository::Path(path_repo));
1135 }
1136 open_repository_with(repo, http, cache_repo_dir, false)
1137}
1138
1139fn open_repository_with(
1143 repo: &RepoConfig,
1144 http: Option<&HttpTransports>,
1145 cache_repo_dir: Option<&Path>,
1146 for_policies: bool,
1147) -> Result<Repository, SessionError> {
1148 let def = &repo.definition;
1149 let kind = def.get("type").and_then(Value::as_str).ok_or_else(|| {
1150 SessionError::new(format!(
1151 "Repository \"{}\" ({def}) must have a type defined",
1152 key_string(&repo.key)
1153 ))
1154 })?;
1155 if kind != "composer" {
1156 return Err(SessionError::new(format!(
1157 "repository type \"{kind}\" is not supported by vivacity update yet ({})",
1158 key_string(&repo.key)
1159 )));
1160 }
1161 if !for_policies
1162 && (def.get("only").is_some()
1163 || def.get("exclude").is_some()
1164 || def.get("canonical").is_some())
1165 {
1166 return Err(SessionError::new(format!(
1167 "repository filters (only/exclude/canonical) are not supported by vivacity update yet ({})",
1168 key_string(&repo.key)
1169 )));
1170 }
1171 let url = def.get("url").and_then(Value::as_str).ok_or_else(|| {
1172 SessionError::new(format!("repository {} has no url", key_string(&repo.key)))
1173 })?;
1174 let transport: Box<dyn crate::repository::Transport> = if url.starts_with("file://") {
1175 Box::new(FileTransport)
1176 } else if url.starts_with("http://") || url.starts_with("https://") || !url.contains("://") {
1177 match http {
1178 Some(h) => Box::new(HttpTransport {
1179 fetch: h.0.clone(),
1180 fetch_many: h.1.clone(),
1181 post: h.2.clone(),
1182 }),
1183 None => {
1184 return Err(SessionError::new(format!(
1185 "remote composer repositories need a network transport ({url})"
1186 )))
1187 }
1188 }
1189 } else {
1190 return Err(SessionError::new(format!(
1191 "unsupported repository url scheme ({url})"
1192 )));
1193 };
1194 let mut repo = ComposerRepository::open(url, transport).map_err(|e| SessionError::new(e.0))?;
1195 if let Some(options) = def.get("options") {
1196 repo.options = options.clone();
1197 }
1198 repo.set_user_filter(def.get("filter"))
1199 .map_err(|e| SessionError::new(e.0))?;
1200 if for_policies {
1201 repo.set_name_filter(def.get("only"), def.get("exclude"))
1202 .map_err(|e| SessionError::new(e.0))?;
1203 }
1204 if let Some(dir) = cache_repo_dir {
1205 repo.cache = Some(crate::metacache::MetadataCache::new(dir, &repo.url));
1206 }
1207 Ok(Repository::Composer(Box::new(repo)))
1208}
1209
1210fn key_string(key: &RepoKey) -> String {
1211 match key {
1212 RepoKey::Named(n) => n.clone(),
1213 RepoKey::Indexed(i) => i.to_string(),
1214 }
1215}
1216
1217#[cfg(test)]
1218mod tests {
1219 use super::*;
1220 use serde_json::json;
1221
1222 fn names(repos: &[RepoConfig]) -> Vec<String> {
1223 repos.iter().map(|r| key_string(&r.key)).collect()
1224 }
1225
1226 #[test]
1227 fn merges_repositories_like_composer_config() {
1228 let mut cfg = MergedConfig::load(&json!({}), None).unwrap();
1229 assert_eq!(names(&cfg.repositories), vec!["packagist.org"]);
1230 cfg.merge(&json!({"repositories": {"snapshot": {"type": "composer", "url": "file:///s"}, "packagist.org": false}}));
1232 assert_eq!(names(&cfg.repositories), vec!["snapshot"]);
1233 cfg.merge(&json!({"repositories": [{"type": "composer", "url": "https://packages.drupal.org/8"}]}));
1235 assert_eq!(names(&cfg.repositories), vec!["0", "snapshot"]);
1236 cfg.merge(
1239 &json!({"repositories": [{"type": "vcs", "url": "a"}, {"type": "vcs", "url": "b"}]}),
1240 );
1241 assert_eq!(names(&cfg.repositories), vec!["2", "1", "0", "snapshot"]);
1242 assert_eq!(cfg.repositories[0].definition["url"], "a");
1243 assert_eq!(cfg.repositories[1].definition["url"], "b");
1244 assert_eq!(
1245 cfg.repositories[2].definition["url"],
1246 "https://packages.drupal.org/8"
1247 );
1248 }
1249
1250 #[test]
1251 fn packagist_url_disables_default() {
1252 let mut cfg = MergedConfig::load(&json!({}), None).unwrap();
1253 cfg.merge(
1254 &json!({"repositories": [{"type": "composer", "url": "https://repo.packagist.org"}]}),
1255 );
1256 assert_eq!(names(&cfg.repositories), vec!["0"]);
1257 let mut cfg = MergedConfig::load(&json!({}), None).unwrap();
1258 cfg.merge(&json!({"repositories": [{"packagist": false}]}));
1259 assert!(cfg.repositories.is_empty());
1260 }
1261}